/* ============================================================
   THE RAFFLE — COMPONENTS
   One self-contained block per component; media queries are
   colocated with their component. Every value traces back to
   /DESIGN.md via css/tokens.css. Blocks are designed to be
   lifted individually into a future component framework.
   ============================================================ */

/* ============================================================
   BUTTONS
   Primary (Deep Forest, flat), Blush Secondary, Mint Tertiary,
   Flat Glass (tertiary), Outlined Light, Ghost, Rounded
   In-Context. All buttons are flat — box-shadow: none, hierarchy
   via fill, border and typography only (DESIGN.md Button System
   Principles).

   EVERY COLOUR ON EVERY BUTTON IS ONE OF THE TENANT'S SEVEN
   ROLES, a brand-independent neutral, or a functional status
   colour — and nothing else. There is no hue in this block, and
   a button added to it must not introduce one, because a button
   is the one control a shop is judged on and a literal here is a
   button that cannot be rebranded.

     fill                    ink                    role pair
     --------------------------------------------------------------
     Primary / In-context    primary-strong         highlight
     Blush Secondary         secondary              on-secondary
     Mint Tertiary           highlight              on-highlight
     Flat Glass              (white glass)          highlight
     Outlined / Ghost        (neutral)              (neutral)
     Danger                  --danger               Snow

   A PALE FILL TAKES ITS OWN CHECKED INK. `on-highlight` and
   `on-secondary` exist because a light fill needs a text colour
   chosen against it, and `colorChecks` (js/settings-data.js)
   validates exactly those two pairings — so a tenant cannot save
   a mint or blush button nobody can read. Writing any other role
   on those fills would be a pairing the console never checks.
   The two dark fills carry `highlight`, which is what the shipped
   design writes on them.

   Hierarchy: Primary = Deep Forest fill + Mint text — the main
   action everywhere EXCEPT on a Deep Forest surface, where it has
   no contrast. On Deep Forest panels/tiles, use Blush Secondary
   (.btn-pink-accent) for the primary-emphasis action and Flat
   Glass (.btn-glass) for the lesser action instead.
   ============================================================ */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--element-gap);
  padding: 12px 16px;
  border-radius: var(--radius-buttons);
  font-family: var(--font-body);
  font-size: var(--text-body);
  font-weight: var(--font-weight-medium);
  line-height: 1;
  cursor: pointer;
  border: 1px solid transparent;
  text-decoration: none;
  white-space: nowrap;
  /* Transform runs faster than colour so presses feel instant
     (press feedback 120-160ms per DESIGN.md Motion Philosophy). */
  transition: transform 160ms var(--ease-deck-out), border-color var(--duration-micro) ease, background var(--duration-micro) ease, color var(--duration-micro) ease, opacity var(--duration-micro) ease;
}
/* The -1px lift is decorative hover motion, so it is gated: on a
   touch device :hover latches after a tap and the button stays lifted
   until something else is tapped (DESIGN.md → Motion Philosophy). The
   press below is NOT gated — it is the only feedback a thumb gets. */
@media (hover: hover) and (pointer: fine) {
  .btn:hover { text-decoration: none; transform: translateY(-1px); }
}
.btn:active { transform: translateY(0) scale(0.97); }
.btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
/* A label is a control's name, not content: a long press on a button
   should press it, never offer to select the word inside it. */
.btn, .text-link { -webkit-user-select: none; user-select: none; }
/* An anchor worn as a button has no `disabled` attribute to answer
   to, so it says so the one way an anchor can. `pointer-events` is
   what actually stops it: a greyed link is still a navigation. */
.btn[aria-disabled="true"] { opacity: 0.5; cursor: not-allowed; transform: none; pointer-events: none; }
/* Flat 2px focus ring (DESIGN.md → Button System Principles):
   Race Green on light surfaces; Mint for the dark-surface variants. */
.btn:focus-visible { outline: 2px solid var(--color-brand-primary); outline-offset: 2px; }
.btn-glass:focus-visible { outline-color: var(--color-brand-highlight); }
/* A pale fill takes its own ink for the ring: the highlight would
   be a pale ring on a pale button. */
.btn-pink-accent:focus-visible { outline-color: var(--color-brand-on-secondary); }
.btn-mint:focus-visible { outline-color: var(--color-brand-on-highlight); }

/* Primary: the main conversion CTA — the strong surface filled,
   the highlight written on it. Never place on that same surface —
   use Blush Secondary or Flat Glass there instead. */
.btn-primary {
  background: var(--color-brand-primary-strong);
  color: var(--color-brand-highlight);
  border-color: rgba(var(--color-brand-highlight-rgb), 0.18);
  box-shadow: var(--shadow-cta);
}
@media (hover: hover) and (pointer: fine) {
  .btn-primary:hover { color: var(--color-brand-highlight); border-color: rgba(var(--color-brand-highlight-rgb), 0.32); }
}

/* Outlined Light: stroke-and-type secondary action. Neutral by
   design — a stroke-and-type control reads on any brand, which is
   why DESIGN.md gives it a grey rather than a role.
   `.btn-outline` is the same control under the name the lean
   layouts call it; it is declared here so a page using it is not
   an unstyled button on this layout alone. */
.btn-secondary,
.btn-outline {
  background: var(--color-snow);
  color: var(--color-graphite);
  border-color: var(--color-graphite);
}
@media (hover: hover) and (pointer: fine) {
  .btn-secondary:hover,
  .btn-outline:hover { color: var(--color-ink); background: rgba(var(--color-brand-primary-rgb), 0.04); }
}

/* Ghost: quiet outlined pill on any light surface */
.btn-ghost {
  background: transparent;
  color: var(--color-ink);
  border-color: var(--color-pebble);
}
@media (hover: hover) and (pointer: fine) {
  .btn-ghost:hover { background: var(--color-snow); border-color: var(--color-graphite); }
}

/* Rounded In-Context: embedded rect-radius action for dense light
   surfaces — checkout rows, deck details, modal footers. Shares
   Primary's role pair; do not use on the strong surface itself
   (see Flat Glass below for that context). */
.btn-dark {
  background: var(--color-brand-primary-strong);
  color: var(--color-brand-highlight);
  border-color: rgba(var(--color-brand-highlight-rgb), 0.16);
  border-radius: var(--radius-buttons-rect);
  padding: 12px 16px;
}
@media (hover: hover) and (pointer: fine) {
  .btn-dark:hover { color: var(--color-brand-highlight); border-color: rgba(var(--color-brand-highlight-rgb), 0.3); }
}

/* Blush Secondary: the primary-emphasis action on the two brand
   surfaces, where the Primary button would disappear — Buy
   tickets, Browse card decks, View deck, Learn more. The accent
   filled, carrying its own checked ink. Pill radius so it matches
   its sibling buttons regardless of base variant (.btn-dark is
   normally a rounded-rect, not a pill). */
.btn-pink-accent { background: var(--color-brand-secondary); color: var(--color-brand-on-secondary); border-color: rgba(var(--color-brand-on-secondary-rgb), 0.10); border-radius: var(--radius-buttons); }
@media (hover: hover) and (pointer: fine) {
  .btn-pink-accent:hover { background: var(--color-brand-secondary); color: var(--color-brand-on-secondary); border-color: rgba(var(--color-brand-secondary-rgb), 0.42); }
}

/* Mint Tertiary: the highlight filled, carrying its own checked
   ink — a warm, quiet alternative to the Primary on light
   surfaces (Snow, Mist, Parchment). Flat like every button; the
   pale fill reads as a gentle, premium call-to-action rather than a
   loud one. Standalone variant — do not combine with Outlined/Ghost. */
.btn-mint { background: var(--color-brand-highlight); color: var(--color-brand-on-highlight); border-color: rgba(var(--color-brand-on-highlight-rgb), 0.10); border-radius: var(--radius-buttons); }
@media (hover: hover) and (pointer: fine) {
  .btn-mint:hover { background: var(--color-brand-highlight); color: var(--color-brand-on-highlight); border-color: rgba(var(--color-brand-on-highlight-rgb), 0.24); }
}

/* Flat Glass: tertiary action on Deep Forest/Race Green surfaces
   and photographic imagery — the quieter companion to Blush
   Secondary when two actions sit on the same dark panel. */
.btn-glass {
  background: var(--glass-fill-dark);
  color: var(--color-brand-highlight);
  border-color: var(--deck-glass-border);
  -webkit-backdrop-filter: blur(var(--glass-blur));
  backdrop-filter: blur(var(--glass-blur));
}
@media (hover: hover) and (pointer: fine) {
  .btn-glass:hover { color: var(--color-brand-highlight); background: rgba(255, 255, 255, 0.12); border-color: var(--glass-border-light); }
}

.btn-danger { background: var(--danger); color: var(--color-snow); border-color: transparent; }
@media (hover: hover) and (pointer: fine) {
  .btn-danger:hover { color: var(--color-snow); }
}

.btn-sm { padding: 8px 14px; font-size: var(--text-body-sm); }
.btn-lg { padding: 14px 24px; font-size: var(--text-body-md); }

.btn-block { width: 100%; }
.btn-icon { padding: 10px; }

/* Info button — a bare 20px mark that hangs off a heading and opens
   the explanation behind it (a draw page's How to enter). Deliberately
   not a .btn: it must not read as an action competing with the primary
   pill beneath it. Steel at rest, Deep Forest on hover, and on touch it
   grows an invisible 44px hit box rather than a bigger glyph, so the
   compact desktop density survives. */
.info-btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: none;
  background: none;
  color: var(--color-steel);
  cursor: pointer;
  transition: color var(--duration-micro) ease, transform 160ms var(--ease-deck-out);
}
@media (hover: hover) and (pointer: fine) {
  .info-btn:hover { color: var(--color-brand-primary-strong); }
}
.info-btn:active { transform: scale(0.9); }
.info-btn:focus-visible { outline: 2px solid var(--color-brand-primary); outline-offset: 2px; border-radius: var(--radius-md); }
@media (pointer: coarse) {
  .info-btn::after {
    content: "";
    position: absolute;
    top: 50%; left: 50%;
    width: var(--touch-target);
    height: var(--touch-target);
    transform: translate(-50%, -50%);
  }
}
@media (prefers-reduced-motion: reduce) {
  .info-btn { transition: none; }
}

/* Shine modifier — the collectible deck's .deck-ticket-tag effect
   (components.css → COLLECTIBLE DECK TILE) applied verbatim to a CTA:
   a screen-blended lengthwise sheen (::before) plus a perimeter glint
   travelling once around the stroke (::after), auto-looping on the same
   5s cadence via the shared deck-ticket keyframes and --se-shine angle.
   The mint fill matches .deck-ticket-tag's, so it reads identically.
   Motion is opacity/transform/filter only and gated behind
   prefers-reduced-motion; at rest it is the plain button. */
.btn-shine { position: relative; overflow: hidden; isolation: isolate; }
/* Lengthwise sheen — screen-blended luminance running across the mint fill */
.btn-shine::before {
  content: "";
  position: absolute;
  top: -2px;
  bottom: -2px;
  left: 0;
  width: 52%;
  z-index: -1;                 /* above the fill, below the label */
  pointer-events: none;
  opacity: 0;
  mix-blend-mode: screen;
  transform: translateX(-210%) skewX(-14deg);
  filter: blur(0.3px);
  background: linear-gradient(100deg,
      transparent 8%,
      rgba(110, 200, 255, 0.42) 23%,   /* cyan       */
      rgba(130, 255, 205, 0.52) 35%,   /* mint       */
      rgba(255, 255, 255, 0.95) 50%,   /* white core */
      rgba(255, 226, 150, 0.50) 60%,   /* gold       */
      rgba(255, 150, 240, 0.58) 70%,   /* orchid     */
      rgba(180, 150, 255, 0.44) 80%,   /* violet     */
      transparent 92%);
}
/* Perimeter glint — bright conic arc masked to a 2px stroke ring */
.btn-shine::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  opacity: 0;
  border-radius: inherit;
  padding: 2px;
  filter: blur(0.6px) brightness(1.05);
  background: conic-gradient(from var(--se-shine),
      transparent 0deg,
      transparent 30deg,
      rgba(110, 200, 255, 0.50) 42deg,  /* cyan   */
      rgba(130, 255, 205, 0.60) 52deg,  /* mint   */
      rgba(255, 255, 255, 1) 62deg,     /* core   */
      rgba(255, 226, 150, 0.60) 72deg,  /* gold   */
      rgba(255, 150, 240, 0.65) 82deg,  /* orchid */
      rgba(180, 150, 255, 0.50) 92deg,  /* violet */
      transparent 104deg,
      transparent 360deg);
  -webkit-mask:
      linear-gradient(#000 0 0) content-box,
      linear-gradient(#000 0 0);
          mask:
      linear-gradient(#000 0 0) content-box,
      linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
}
@media (prefers-reduced-motion: no-preference) {
  /* one sweep + one glint pass, then a rest before the loop repeats */
  .btn-shine::before { animation: deck-ticket-sheen 5s linear infinite; }
  .btn-shine::after  { animation: deck-ticket-ring  5s linear infinite; }
}

/* Text Link Button (DESIGN.md) — no fill, no border. Race Green
   on light surfaces; use Mint via .text-link--dark on dark ones. */
.text-link { font-size: var(--text-body-sm); font-weight: var(--font-weight-semibold); color: var(--color-brand-primary); }
@media (hover: hover) and (pointer: fine) {
  .text-link:hover { text-decoration: underline; }
}
.text-link:active { opacity: 0.7; }
.text-link--dark { color: var(--color-brand-highlight); }

/* ── Touch ──────────────────────────────────────────────────
   DESIGN.md sets a compact density, which is right for a mouse
   and too tight for a thumb: .btn lands at 40px and .btn-sm at
   31px. Raise the box to the 44px floor on coarse pointers only,
   so the desktop rhythm is untouched. Padding does the work, not
   height, so a wrapped two-line label still grows. */
@media (pointer: coarse) {
  .btn { min-height: var(--touch-target); }
  .btn-sm { min-height: var(--touch-target); padding-block: 11px; }
  .btn-icon { min-width: var(--touch-target); padding: 12px; }
  /* Text links are prose-scale: the 20px line gets 12px above and
     below to clear the floor without the link looking like a button. */
  .text-link { display: inline-block; padding-block: var(--spacing-12); }
}

/* Smallest phones — `white-space: nowrap` keeps short labels on one
   line, but it also means a long one ("Upload a proof of address
   document") has a min-content width wider than the screen and drags
   the whole page sideways, since a button cannot shrink below it.
   Below 480px the label wraps instead. It only ever wraps when it
   genuinely does not fit, and a two-line button is a far smaller
   problem than a horizontally scrolling page. */
@media (max-width: 480px) {
  .btn { white-space: normal; text-wrap: balance; }
  .btn-lg { padding-inline: var(--spacing-20); }
}

/* ============================================================
   FORMS
   Inputs: Snow surface, 14px radius, Inter 14px, ash placeholder.
   ============================================================ */
.form-group { display: flex; flex-direction: column; gap: var(--element-gap); }
.form-label { font-size: var(--text-body-sm); font-weight: var(--font-weight-semibold); color: var(--color-ink); }
.form-hint { font-size: var(--text-body-xs); line-height: var(--leading-body-xs); color: var(--color-steel); }

/* ── Vertical rhythm ────────────────────────────────────────
   A form block owns the gap above itself, never below. That way
   the rhythm survives any container — a panel body, a card, a bare
   <form> — without the container having to know a form is in it,
   and the last field never leaves a dangling margin for the action
   row to add to. Fields sit 20px apart; the actions clear the last
   field by 32px, always. */
.form-group + .form-group,
.form-group + .form-row,
.form-group + .form-row-3,
.form-row + .form-group,
.form-row + .form-row,
.form-row-3 + .form-group,
.form-row-3 + .form-row-3,
.form-check + .form-group,
.form-group + .form-check,
.form-check + .form-check { margin-top: var(--spacing-20); }
/* Inside a row the columns are side by side, so the sibling rule
   above must not push the second one down. */
.form-row > .form-group + .form-group,
.form-row-3 > .form-group + .form-group { margin-top: 0; }

.form-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--spacing-12);
  margin-top: var(--spacing-32);
}
/* A full-width submit is the whole row, not an item in it. */
.form-actions .btn-block { flex: 1 1 100%; }
/* Stepped flows put the way back and the way on at opposite ends,
   so the forward action always sits under the thumb on the right. */
.form-actions--split { justify-content: space-between; }

@media (max-width: 600px) {
  /* Stacked, the primary keeps the top slot — the thumb reaches it
     first and it is the action the form exists for. */
  .form-actions { flex-direction: column; align-items: stretch; }
}

.form-control {
  width: 100%;
  padding: 12px 12px 12px 16px;
  background: var(--color-snow);
  border: 1px solid var(--color-pebble);
  border-radius: var(--radius-inputs);
  font-family: var(--font-body);
  font-size: var(--text-body);
  font-weight: var(--font-weight-regular);
  color: var(--color-ink);
  transition: border-color var(--duration-micro) ease, box-shadow var(--duration-micro) ease;
}
.form-control:focus {
  outline: none;
  border-color: var(--color-deep-forest);
  box-shadow: 0 0 0 3px rgba(var(--color-brand-primary-strong-rgb), 0.08);
}
.form-control::placeholder { color: var(--color-ash); }

textarea.form-control { resize: vertical; min-height: 100px; line-height: var(--leading-body); }

.form-select {
  width: 100%;
  padding: 12px 36px 12px 16px;
  background: var(--color-snow);
  border: 1px solid var(--color-pebble);
  border-radius: var(--radius-inputs);
  font-family: var(--font-body);
  font-size: var(--text-body);
  color: var(--color-ink);
  cursor: pointer;
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%2371717a' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 14px center;
}
.form-select:focus { outline: none; border-color: var(--color-graphite); }

.input-group { display: flex; gap: 0; }
.input-group .form-control { border-radius: var(--radius-inputs) 0 0 var(--radius-inputs); flex: 1; }
.input-group .btn { border-radius: 0 var(--radius-inputs) var(--radius-inputs) 0; }

.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--spacing-16); }

/* The box aligns to the first line of its label, not to the middle
   of a wrapped block — a consent that runs to three lines would
   otherwise float its box down beside line two. `flex-shrink: 0` is
   load-bearing: a fixed width still shrinks in a flex row, so a long
   label was squeezing the box narrower than its neighbours' and the
   three consents on the register form came out three sizes. */
.form-check { display: flex; align-items: flex-start; gap: var(--spacing-12); }
.form-check input[type="checkbox"],
.form-check input[type="radio"] {
  width: 18px;
  height: 18px;
  flex: 0 0 18px;
  margin: 0;
  margin-top: calc((1.5em - 18px) / 2);   /* optical centre of line one */
  accent-color: var(--color-deep-forest);
  cursor: pointer;
}
.form-check label { font-size: var(--text-body); line-height: 1.5; cursor: pointer; }
/* A switch is taller than a checkbox, so it centres on its row. */
.form-check .switch { margin-top: 0; }
.form-check:has(.switch) { align-items: center; }

/* Validation states — the border swaps to the functional status
   colour with a matching soft ring; a .form-error message replaces
   the hint. Never a coloured fill (DESIGN.md → Forms). */
.form-control.is-invalid,
.form-select.is-invalid { border-color: var(--danger); }
.form-control.is-invalid:focus { border-color: var(--danger); box-shadow: 0 0 0 3px rgba(176, 58, 58, 0.10); }

.form-error { font-size: var(--text-body-xs); line-height: var(--leading-body-xs); color: var(--danger); }

/* Toggle switch — 40×22 pill track (Pebble → Deep Forest when on),
   Snow thumb moves transform-only. */
.switch { position: relative; display: inline-flex; flex-shrink: 0; width: 40px; height: 22px; cursor: pointer; }
.switch input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; }
.switch-track { position: absolute; inset: 0; border-radius: var(--radius-pill); background: var(--color-pebble); transition: background var(--duration-micro) ease; pointer-events: none; }
.switch-thumb { position: absolute; top: 3px; left: 3px; width: 16px; height: 16px; border-radius: var(--radius-pill); background: var(--color-snow); transition: transform var(--duration-micro) ease; pointer-events: none; }
.switch input:checked ~ .switch-track { background: var(--color-deep-forest); }
.switch input:checked ~ .switch-thumb { transform: translateX(18px); }
.switch input:focus-visible ~ .switch-track { outline: 2px solid var(--color-race-green); outline-offset: 2px; }

@media (max-width: 768px) {
  .form-row, .form-row-3 { grid-template-columns: 1fr; }
  /* 16px floor stops iOS Safari zooming inputs on focus */
  .form-control, .form-select { font-size: var(--text-body-lg); }
  /* An input + button pair fused into one pill has no room for both
     at phone width; split it into two stacked full-width controls. */
  .input-group { flex-direction: column; gap: var(--element-gap); }
  .input-group .form-control,
  .input-group .btn { border-radius: var(--radius-inputs); width: 100%; }
  .email-cta-row { flex-direction: column; align-items: stretch; }
  .email-cta-row .btn { width: 100%; }
}

@media (pointer: coarse) {
  .form-control, .form-select { min-height: var(--touch-target); }
  /* The 18px box is a mouse target. Grow the control and widen the
     label's reach so the whole row is tappable. */
  .form-check input[type="checkbox"],
  .form-check input[type="radio"] { width: 22px; height: 22px; }
  .form-check { padding-block: var(--spacing-4); }
  .form-check label { min-height: var(--touch-target); display: flex; align-items: center; }
  /* The switch keeps its 40×22 look and gains an invisible hit box. */
  .switch { position: relative; }
  .switch input { min-width: var(--touch-target); min-height: var(--touch-target); left: 50%; top: 50%; transform: translate(-50%, -50%); inset: auto; }
}

/* ── Email input + CTA row (hero capture, notify rows) ─────── */
.email-cta-row { display: flex; gap: var(--element-gap); align-items: center; }
.email-cta-row .form-control { border-color: transparent; flex: 1; }
.email-cta-row .btn { flex-shrink: 0; }

/* ============================================================
   CARDS
   Light Surface Card (Snow, 28px, flat) and
   Muted Surface Card (Fog, 24px, flat).
   ============================================================ */
.card {
  background: var(--surface-card-white);
  border: none;
  border-radius: var(--radius-cards);
  overflow: hidden;
  padding: var(--card-padding);
  /* The press takes the press band, not the hover's: a transform
     eased over 250ms on a symmetric curve lags the thumb that
     started it (DESIGN.md → Motion Philosophy, 120–160ms on the
     strong ease-out). Everything else here stays at micro speed. */
  transition: transform 160ms var(--ease-deck-out), box-shadow var(--duration-micro) ease;
}
.card-hover { cursor: pointer; }
@media (hover: hover) and (pointer: fine) {
  .card-hover:hover { box-shadow: var(--shadow-flat-hairline); transform: translateY(-3px); }
}
.card-hover:active { transform: scale(0.99); }

.card-muted {
  background: var(--surface-card-muted);
  border: none;
  border-radius: var(--radius-cards-compact);
  padding: var(--card-padding-compact);
}

.card-img { width: 100%; height: 220px; object-fit: cover; }

.card-body { padding: var(--spacing-20); }
.card-title { font-family: var(--font-display); letter-spacing: var(--ls-rg); font-size: var(--text-heading-sm); font-weight: var(--font-weight-semibold); line-height: 1; margin-bottom: var(--spacing-8); }
.card-meta { font-size: var(--text-body-xs); line-height: var(--leading-body-xs); color: var(--color-steel); }

/* At phone width the 28px card padding eats a tenth of the screen on
   each side, and the card sits inside a 20px gutter already. Step it
   down to the compact value so content keeps its measure; the radius
   follows, since a 28px curve on a near-full-width card reads as a
   pill rather than a corner. */
@media (max-width: 768px) {
  .card { padding: var(--card-padding-compact); border-radius: var(--radius-cards-compact); }
  .card-body { padding: var(--spacing-16); }
  .card-img { height: 180px; }
}

/* ============================================================
   DARK PANEL (Dark Problem Panel / trust & mechanics sections)
   28px radius, Switzer weight-contrast rows, drawn on the dark
   ground — `--dark-*` (tokens.css → Dark sections), which is the
   set an operator authors rather than the brand palette: a
   deployment's primary colour is what its shop is branded in, and
   nothing says it is dark enough to stand a section on. Ash stays
   where it is: a neutral quiet ink needs no brand behind it.
   ============================================================ */
.dark-panel {
  background: var(--dark-surface);
  color: var(--dark-heading);
  border-radius: var(--radius-cards);
  padding: var(--spacing-48) var(--spacing-40);
}
.dark-panel h1, .dark-panel h2, .dark-panel h3, .dark-panel h4 { color: var(--dark-heading); }
.dark-panel p { color: var(--color-ash); }

/* A micro-label is Steel on light ground; on Deep Forest that is a
   grey line on a green one, so it takes the muted Mint every other
   quiet line on this surface takes. */
.dark-panel .micro-label,
.surface-forest .micro-label { color: var(--dark-body-quiet); }

.dark-panel-row {
  display: flex;
  align-items: baseline;
  gap: var(--spacing-12);
  font-family: var(--font-display); letter-spacing: var(--ls-subheading);
  font-size: var(--text-subheading);
  line-height: 1;
  padding: var(--spacing-12) 0;
}
.dark-panel-row::before {
  content: '';
  width: 10px;
  height: 10px;
  border-radius: var(--radius-pill);
  background: var(--color-graphite);
  flex-shrink: 0;
  align-self: center;
}
.dark-panel-row .dp-lead { color: var(--color-ash); font-weight: var(--font-weight-regular); }
.dark-panel-row strong { color: var(--dark-heading); font-weight: var(--font-weight-semibold); }

@media (max-width: 768px) {
  .dark-panel { padding: var(--spacing-32) var(--spacing-24); border-radius: var(--radius-cards-compact); }
}

/* ============================================================
   TRUST BAND
   Full-bleed Mint Ice Cream canvas. Cards are the light-surface
   Glass Card (DESIGN.md → Glass Card): rgba(255,255,255,.64)
   fill, rgba(var(--color-brand-primary-rgb), .10) border, blur, Race Green text/icon.
   ============================================================ */

.trust-band {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--spacing-24);
}
.trust-band-item {
  text-align: left;
  padding: var(--card-padding-compact);
  background: var(--glass-fill-light);
  border: 1px solid rgba(var(--color-brand-primary-rgb), 0.10);
  border-radius: var(--radius-cards-compact);
  -webkit-backdrop-filter: blur(var(--glass-blur));
  backdrop-filter: blur(var(--glass-blur));
}
.trust-band-item i[data-icon] { margin-bottom: var(--spacing-16); color: var(--color-race-green); }
.trust-band-title {
  font-family: var(--font-display); letter-spacing: var(--ls-rg);
  /* heading-md role (24px); base stays Switzer Light for the .hatton pairing */
  font-size: var(--text-heading-md);
  font-weight: var(--font-weight-light);
  line-height: 1.1;
  margin-bottom: var(--spacing-8);
  color: var(--color-race-green);
}
.trust-band-body { font-size: var(--text-body-sm); line-height: var(--leading-body-sm); color: rgba(var(--color-brand-primary-rgb), 0.7); }

@media (max-width: 768px) {
  .trust-band { grid-template-columns: 1fr; gap: var(--spacing-16); }
}

/* ============================================================
   ANNOUNCEMENT BANNER
   Frosted Deep Forest strip, 28px radius, blush inline CTA.
   ============================================================ */
.announcement-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--spacing-16);
  background: rgba(var(--color-brand-primary-strong-rgb), 0.92);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
  color: var(--color-mint-ice-cream);
  border-radius: var(--radius-hero);
  padding: var(--spacing-12) var(--spacing-24);
  font-size: var(--text-body);
}
.announcement-banner a {
  color: var(--color-blush-pink);
  font-weight: var(--font-weight-medium);
  white-space: nowrap;
  opacity: 0.85;
}
@media (hover: hover) and (pointer: fine) {
  .announcement-banner a:hover { opacity: 1; }
}
.announcement-banner a:active { opacity: 1; }

@media (max-width: 768px) {
  .announcement-banner { flex-wrap: wrap; border-radius: var(--radius-3xl); }
}

/* ============================================================
   COUNTDOWN BANNER
   Full-width urgency strip: Deep Forest 28px panel with a Switzer
   Light headline, a live small countdown (Snow chips, lightened
   labels for the dark surface), and a Blush CTA. One per page.
   ============================================================ */
.countdown-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--spacing-24);
  flex-wrap: wrap;
  background: var(--color-deep-forest);
  border-radius: var(--radius-cards);
  padding: var(--spacing-28) var(--spacing-40);
}
.countdown-banner-copy { max-width: 340px; }
.countdown-banner-title {
  font-family: var(--font-display); letter-spacing: var(--ls-heading-md);
  font-size: var(--text-heading-md);
  font-weight: var(--font-weight-light);
  line-height: 1.2;
  color: var(--color-mint-ice-cream);
}
.countdown-banner-meta { font-size: var(--text-body-sm); color: rgba(var(--color-brand-highlight-rgb), 0.6); margin-top: var(--spacing-4); }
/* Timer is a self-contained Ember pill now — no dark-surface label overrides. */

@media (max-width: 768px) {
  .countdown-banner { padding: var(--spacing-24) var(--spacing-20); border-radius: var(--radius-cards-compact); }
}

/* ============================================================
   BADGES
   12px radius, 4px 8px, Inter 12px. Three system variants:
   overlay (on imagery), dark-filled (on light), ember (urgency).
   Functional status tints for operational states only.
   ============================================================ */
.badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 4px 8px;
  border-radius: var(--radius-badges);
  font-family: var(--font-body);
  font-size: var(--text-body-xs);
  line-height: 1.35;
  font-weight: var(--font-weight-medium);
}

/* Dark Overlay Badge — on imagery / dark surfaces */
.badge-overlay {
  background: rgba(var(--color-brand-primary-strong-rgb), 0.14);
  color: var(--color-snow);
  border: 1px solid rgba(var(--color-brand-highlight-rgb), 0.34);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
}

/* Dark Filled Badge — categories/tiers on light surfaces */
.badge-dark-filled, .badge-dark {
  background: var(--color-race-green);
  color: var(--color-mint-ice-cream);
}

/* Shine Badge — the collectible deck's .deck-ticket-tag colourway and
   light play (components.css → COLLECTIBLE DECK TILE) as a badge: Mint
   Ice Cream fill, Race Green icon + label, a screen-blended lengthwise
   sheen (::before) and a perimeter glint (::after) on the shared 5s
   deck-ticket cadence. Used for free tickets that ride along with a
   purchase (ticket options, purchase modal), where the tickets are the
   reward and should read as one. Motion is opacity/transform/filter
   only and gated behind prefers-reduced-motion; at rest it is a plain
   mint badge. */
.badge-shine {
  position: relative;
  isolation: isolate;      /* contains the screen blend; ::before above fill, below text */
  overflow: hidden;        /* clips the sheen to the pill silhouette */
  background: var(--color-mint-ice-cream);
  color: var(--color-race-green);
  font-weight: var(--font-weight-semibold);
  letter-spacing: 0.02em;
  padding: 5px 10px;
  white-space: nowrap;
}
.badge-shine .icon { width: 15px; height: 15px; flex: 0 0 auto; color: var(--color-race-green); }
/* Lengthwise sheen — screen-blended luminance running across the mint fill */
.badge-shine::before {
  content: "";
  position: absolute;
  top: -2px;
  bottom: -2px;
  left: 0;
  width: 52%;
  z-index: -1;
  pointer-events: none;
  opacity: 0;
  mix-blend-mode: screen;
  transform: translateX(-210%) skewX(-14deg);
  filter: blur(0.3px);
  background: linear-gradient(100deg,
      transparent 8%,
      rgba(110, 200, 255, 0.42) 23%,   /* cyan       */
      rgba(130, 255, 205, 0.52) 35%,   /* mint       */
      rgba(255, 255, 255, 0.95) 50%,   /* white core */
      rgba(255, 226, 150, 0.50) 60%,   /* gold       */
      rgba(255, 150, 240, 0.58) 70%,   /* orchid     */
      rgba(180, 150, 255, 0.44) 80%,   /* violet     */
      transparent 92%);
}
/* Perimeter glint — bright conic arc masked to a 2px stroke ring */
.badge-shine::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  opacity: 0;
  border-radius: inherit;
  padding: 2px;
  filter: blur(0.6px) brightness(1.05);
  background: conic-gradient(from var(--se-shine),
      transparent 0deg,
      transparent 30deg,
      rgba(110, 200, 255, 0.50) 42deg,  /* cyan   */
      rgba(130, 255, 205, 0.60) 52deg,  /* mint   */
      rgba(255, 255, 255, 1) 62deg,     /* core   */
      rgba(255, 226, 150, 0.60) 72deg,  /* gold   */
      rgba(255, 150, 240, 0.65) 82deg,  /* orchid */
      rgba(180, 150, 255, 0.50) 92deg,  /* violet */
      transparent 104deg,
      transparent 360deg);
  -webkit-mask:
      linear-gradient(#000 0 0) content-box,
      linear-gradient(#000 0 0);
          mask:
      linear-gradient(#000 0 0) content-box,
      linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
}
@media (prefers-reduced-motion: no-preference) {
  /* one sweep + one glint pass, then a rest before the loop repeats */
  .badge-shine::before { animation: deck-ticket-sheen 5s linear infinite; }
  .badge-shine::after  { animation: deck-ticket-ring  5s linear infinite; }
}
/* Wide variant — the shine badge blown up to a full-width line item
   (purchase modal): same fill and light play, sized to sit among the
   price rows, and carries no price. */
.badge-shine--wide {
  display: flex;
  width: 100%;
  justify-content: center;
  gap: var(--spacing-8);
  padding: var(--spacing-12) var(--spacing-16);
  border-radius: var(--radius-inputs);
  font-size: var(--text-body);
  letter-spacing: 0.01em;
}
.badge-shine--wide .icon { width: 18px; height: 18px; }

/* Ember Badge — live / verified / closing soon / draw status ONLY */
.badge-ember {
  background: var(--color-ember);
  color: var(--color-snow);
  font-weight: var(--font-weight-semibold);
}

/* Live pulse — opt-in dot for genuinely live draws only (never
   Verified/Closing soon). Transform+opacity pulse, killed by the
   reduced-motion switch. */
.badge-live::before {
  content: '';
  width: 6px;
  height: 6px;
  border-radius: var(--radius-pill);
  background: var(--color-snow);
  animation: livePulse 2s var(--ease-deck-soft) infinite;
}
@keyframes livePulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: 0.5; transform: scale(0.7); }
}

/* Legacy names mapped into the system */
.badge-gold { background: var(--color-ember); color: var(--color-snow); font-weight: var(--font-weight-semibold); }
.badge-red { background: var(--color-ember); color: var(--color-snow); font-weight: var(--font-weight-semibold); }
.badge-green { background: rgba(53, 113, 83, 0.12); color: var(--success); }
.badge-blue { background: rgba(74, 108, 140, 0.12); color: var(--info); }
.badge-gray { background: var(--color-fog); color: var(--color-steel); }

/* ============================================================
   PRIZE SPECS (draw detail → Details tab)
   The host's own key/value rows for the prize itself — brand,
   movement, mileage, beam. Every prize needs different ones, so
   the list is authored a row at a time in the console and the
   names are free text; the component only has to lay an unknown
   number of unknown pairs out legibly.

   A hairline-separated two-column list rather than the card grid
   above it: those are four fixed draw facts and read as tiles,
   these are a variable run of prize facts and read as a spec
   sheet, where scanning down one column of names is the point.
   Below 480px the value drops under its name — a 200px name
   column against a phone leaves nothing for the value.
   ============================================================ */
.prize-specs { margin: 0; }
.prize-spec {
  display: grid;
  grid-template-columns: minmax(0, 200px) minmax(0, 1fr);
  gap: var(--spacing-16);
  padding: var(--spacing-12) 0;
  border-bottom: 1px solid var(--color-pebble);
}
.prize-spec:first-child { padding-top: 0; }
.prize-spec:last-child { border-bottom: none; padding-bottom: 0; }
.prize-spec-key {
  margin: 0;
  font-size: var(--text-body-sm);
  color: var(--color-steel);
}
.prize-spec-value {
  margin: 0;
  font-size: var(--text-body-sm);
  font-weight: var(--font-weight-medium);
  color: var(--color-obsidian);
}

@media (max-width: 480px) {
  .prize-spec { grid-template-columns: minmax(0, 1fr); gap: var(--spacing-4); }
}

/* ============================================================
   PRIZE TILE (raffle discovery)
   Full-bleed prize image clipped at 28px; bottom-left overlay
   with Switzer 20px title and overlay badges. Sparse metadata.
   ============================================================ */
.prize-tile {
  position: relative;
  display: block;
  border-radius: var(--radius-cards);
  overflow: hidden;
  background: var(--color-fog);
  aspect-ratio: 4 / 5;
  color: var(--color-snow);
  text-decoration: none;
  isolation: isolate;
  /* The tile appears at three very different widths — a ~420px desktop
     grid cell, a 280px swipe rail, a ~170px two-up phone grid — so it
     answers to its OWN box, not the viewport. A viewport breakpoint
     cannot tell those apart: the rail is narrow on a wide screen and
     the grid cell is wide on a tablet. See the ≤220px rules below. */
  container-type: inline-size;
  /* THE PRESS IS THE TRANSFORM'S DURATION, AND IT IS NOT THE HOVER'S.
     DESIGN.md puts a press at 120–160ms on the strong ease-out and a
     hover in the 0.2–0.35s band; one transform transition cannot be
     both, and this tile was serving the press at the hover's 250ms on
     a symmetric `ease` — a curve that starts slow, on the single
     most-tapped object in the shop. The transform takes the press
     speed, because a lag on the way down is the one a thumb feels;
     the shadow keeps micro speed, because nothing is waiting on it. */
  transition: transform 160ms var(--ease-deck-out), box-shadow var(--duration-micro) ease;
}
@media (hover: hover) and (pointer: fine) {
  .prize-tile:hover { text-decoration: none; transform: translateY(-3px); box-shadow: var(--shadow-flat-hairline); }
}
/* The single most-tapped object in the shop, and until now the tap
   had no answer at all. A tile is a big surface, so it takes the
   shallow end of the 0.94–0.97 band. */
.prize-tile:active { transform: scale(0.97); }
.prize-tile:focus-visible { outline: 2px solid var(--color-race-green); outline-offset: 3px; }

.prize-tile-media {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transform: scale(1.025);
  transition: transform var(--duration-deck-image) var(--ease-deck-soft);
}
@media (hover: hover) and (pointer: fine) {
  .prize-tile:hover .prize-tile-media { transform: scale(1.05); }
}

.prize-tile-scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(var(--color-brand-primary-strong-rgb), 0) 42%, rgba(var(--color-brand-primary-strong-rgb), 0.72) 100%);
  pointer-events: none;
}

.prize-tile-flag { position: absolute; top: var(--spacing-16); left: var(--spacing-16); display: flex; gap: var(--element-gap); }

.prize-tile-overlay {
  position: absolute;
  left: var(--spacing-20);
  right: var(--spacing-20);
  bottom: var(--spacing-20);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--element-gap);
}
.prize-tile-title {
  font-family: var(--font-display); letter-spacing: var(--ls-heading-sm);
  font-size: var(--text-heading-sm);
  font-weight: var(--font-weight-semibold);
  line-height: 1.15;
  color: var(--color-snow);
  text-shadow: rgba(0, 0, 0, 0.34) 0 1px 8px;
}
.prize-tile-host { font-size: var(--text-body-xs); color: rgba(255, 255, 255, 0.78); }

.prize-tile-meta {
  display: flex;
  justify-content: space-between;
  width: 100%;
  gap: var(--spacing-12);
  font-size: var(--text-body-xs);
  color: rgba(255, 255, 255, 0.82);
  padding-top: var(--spacing-8);
  border-top: 1px solid rgba(255, 255, 255, 0.18);
}
.prize-tile-meta strong { color: var(--color-snow); font-weight: var(--font-weight-semibold); }

/* ── Narrow tile (two-up on a phone, ~170px) ──────────────────
   Everything that assumed a 400px tile has to give. The order of
   sacrifice is deliberate: the picture and the price survive intact,
   because those are what a buyer scans a listing for. The date row
   and the second badge go first — both are restated on the draw page
   one tap away, and neither is what makes someone stop scrolling. */
@container (max-width: 220px) {
  /* The copy block is the same three lines in a tile half the height,
     so it reaches much further up the image — past where the desktop
     scrim has faded out. Text on photography is always Snow
     (DESIGN.md), so the fix is the scrim, not the text: start the
     fade higher and land it darker. */
  .prize-tile-scrim {
    background: linear-gradient(180deg,
      rgba(var(--color-brand-primary-strong-rgb), 0) 18%,
      rgba(var(--color-brand-primary-strong-rgb), 0.46) 52%,
      rgba(var(--color-brand-primary-strong-rgb), 0.88) 100%);
  }
  .prize-tile--limited .prize-tile-scrim {
    background: linear-gradient(180deg,
      rgba(var(--color-brand-primary-rgb), 0) 18%,
      rgba(var(--color-brand-primary-rgb), 0.20) 52%,
      rgba(var(--color-brand-primary-rgb), 0.46) 100%);
  }
  .prize-tile-overlay {
    left: var(--spacing-12);
    right: var(--spacing-12);
    bottom: var(--spacing-12);
    gap: 2px;
  }
  .prize-tile-title { font-size: var(--text-body-lg); line-height: 1.2; }
  /* Two lines, then ellipsis — a five-line prize name would push the
     price out of the tile. */
  .prize-tile-title {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }
  /* Host and fill state stay — provenance is the trust signal — but
     capped at two lines so the block height stays predictable and the
     price never gets pushed off the bottom. */
  .prize-tile-host {
    font-size: var(--text-caption);
    line-height: 1.4;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }
  /* The date and the price cannot share a 145px line. The price keeps
     the row; the date drops, since the tile already says how full the
     draw is and the countdown lives on the draw page. */
  .prize-tile-meta { padding-top: var(--spacing-4); }
  .prize-tile-meta > span { display: none; }
  .prize-tile-meta > strong { font-size: var(--text-body-sm); }
  /* Flags stack rather than squeezing two badges onto one short line. */
  .prize-tile-flag {
    top: var(--spacing-8);
    left: var(--spacing-8);
    right: var(--spacing-8);
    flex-wrap: wrap;
    gap: 4px;
  }
  .prize-tile-flag .badge { font-size: var(--text-caption); padding: 3px 7px; }
}

/* ── Phone (≤768px): the tile sheds its two quiet lines ────────
   A phone scrolls a page of these, and at that speed a tile is
   scanned rather than read: the picture, the prize name and the
   price are what stop somebody, and every other line is competing
   with them. The host and the draw date are the two that go —
   both are restated on the draw page one tap away, and neither is
   what makes a buyer stop scrolling.

   This one is the VIEWPORT's call rather than the tile's own box,
   which is the reverse of the rule below it and deliberately so:
   the 280px rail tile and the 169px grid tile are the same
   crowded glance on a phone, and a 280px cell on a desktop is
   not. What the tile does with the room it has is its box's; how
   much a phone should be asked to read is the screen's.

   Hidden, never dropped from the markup, so the tile stays one
   object at every width and what a given screen shows is the
   stylesheet's decision rather than each render's. */
@media (max-width: 768px) {
  .prize-tile-host,
  .prize-tile-meta > span { display: none; }
}

/* Rare limited-edition wash — Blush Pink, max one per grid row.
   Light fill needs dark text for contrast, unlike the photographic
   tiles around it which carry light text over a scrim. */
.prize-tile--limited { background: var(--color-blush-pink); }
.prize-tile--limited .prize-tile-scrim { background: linear-gradient(180deg, rgba(var(--color-brand-primary-rgb), 0) 30%, rgba(var(--color-brand-primary-rgb), 0.32) 100%); }
.prize-tile--limited .prize-tile-title,
.prize-tile--limited .prize-tile-meta strong { color: var(--color-race-green); text-shadow: none; }
.prize-tile--limited .prize-tile-host,
.prize-tile--limited .prize-tile-meta { color: rgba(var(--color-brand-primary-rgb), 0.7); }
.prize-tile--limited .prize-tile-meta { border-top-color: rgba(var(--color-brand-primary-rgb), 0.18); }

/* ── Mobile rail modifier for draw-card grids ──────────────────
   Desktop keeps the grid; below 768px a .grid-2/3/4 carrying this
   class becomes a swipeable, edge-bled scroll-snap rail so draw
   cards keep a real card width (280px) instead of stacking
   full-width. Padding matches the 20px mobile gutter so the first
   card aligns with the content edge; the matching scroll-padding
   makes scrollLeft 0 the first snap point — without it, mandatory
   snap scrolls the gutter away on load. */
@media (max-width: 768px) {
  .rail-mobile {
    display: flex;
    gap: var(--spacing-16);
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    scroll-padding-inline: var(--spacing-20);
    /* A flick that runs off the end of the rail must not become the
       browser's own back gesture, or page history follows the tile. */
    overscroll-behavior-x: contain;
    scrollbar-width: none;
    width: 100vw;
    margin-left: calc(50% - 50vw);
    padding: 0 var(--spacing-20) var(--spacing-12);
  }
  .rail-mobile::-webkit-scrollbar { display: none; }
  .rail-mobile > * {
    flex: 0 0 280px;
    min-width: 280px;
    scroll-snap-align: start;
  }
}

/* ============================================================
   SHOWCASE HERO + STRIP
   Coupled component: a centred filmstrip (the strip) navigates a
   full-bleed hero stage. The centred strip item is highlighted
   and drives the stage, which renders either a certified draw or
   a promotional slide. Two media layers crossfade (opacity only,
   per DESIGN.md motion rules); reduced motion holds a still image
   and disables auto-advance (base.css kill-switch).
   ============================================================ */

/* Pull the showcase behind the transparent fixed nav. The half-gap
   below is this block's share of the section rhythm: a .section pays
   48px on each of its own edges (base.css → Layout primitives), so a
   full-bleed head that carries no padding of its own owes the section
   under it the other half. */
.showcase {
  margin-top: calc(-1 * var(--nav-h));
  margin-bottom: var(--spacing-48);
}
@media (max-width: 768px) { .showcase { margin-bottom: var(--spacing-32); } }

/* ── Stage ─────────────────────────────────────────────────── */
.showcase-stage {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  background: var(--color-deep-forest);
  /* Horizontal gestures belong to the slide swipe (index.html →
     initShowcase); vertical stays the page's, because the stage is a
     screen tall on a phone and must never trap a scroll. */
  touch-action: pan-y;
  /* svh, not vh: `vh` is the viewport with the browser chrome
     COLLAPSED, so a vh-measured hero is taller than the screen on
     load and then RESIZES as the URL bar retracts — the picture
     jumps while it is being read. `svh` is the smallest the viewport
     gets, so the stage is stable from the first paint. */
  height: calc(80svh - var(--dev-bar-h));
  min-height: 480px;
  max-height: 900px;
  display: flex;
  align-items: flex-end;
}

/* Two stacked layers, each holding an injected <img> or <video> */
.showcase-media { position: absolute; inset: 0; z-index: 0; }
.showcase-layer {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 0.7s var(--ease-deck-soft);
}
.showcase-layer.is-active { opacity: 1; }
.showcase-layer img,
.showcase-layer video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.showcase-scrim {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(180deg, rgba(var(--color-brand-primary-strong-rgb), 0.15) 0%, rgba(var(--color-brand-primary-strong-rgb), 0.34) 48%, rgba(var(--color-brand-primary-strong-rgb), 0.78) 100%);
}

/* Wrapper spans the full stage width, aligns content via .container */
.showcase-content-wrap {
  position: relative;
  z-index: 2;
  width: 100%;
  /* Top pad clears the transparent nav the stage sits behind, so
     bottom-aligned content never rides up under it on short stages */
  padding-top: calc(var(--nav-h) + var(--spacing-16));
  padding-bottom: var(--spacing-40);
}
/* The stage is full-bleed, so in landscape on a notched phone the
   headline starts under the notch. The gutter absorbs it. */
@media (orientation: landscape) {
  .showcase-content-wrap > .container {
    padding-inline: calc(var(--spacing-40) + env(safe-area-inset-left, 0px)) calc(var(--spacing-40) + env(safe-area-inset-right, 0px));
  }
}
.showcase-content {
  max-width: 640px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--spacing-20);
}

/* Eyebrow — the pill over the stage. A DRAW pill is the highlight
   filled, with the singular category set in the display face: it
   names the prize, and a solid colour is how it reads as a label.

   A PROMO pill is FLAT GLASS, and that is the whole difference.
   Blush is a button colour — it fills the stage's call to action
   directly beneath — so a blush pill sitting two lines above a blush
   button reads as a second button somebody has failed to make
   clickable. Glass is the recipe this page already uses for a label
   over a photograph (`.page-banner-eyebrow`, same three properties),
   so the promo pill is not a new treatment; it is the one already
   in the build, applied where the colour was doing a button's job. */
.showcase-eyebrow {
  /* THE LINE BOX BOTH PILLS STAND ON. A draw pill is as tall as its
     category word and a promo pill is as tall as a line of 13px
     uppercase, so left to themselves the two come out visibly
     different heights — the same pill in two sizes. One value drives
     the category's size AND the promo's line box, so they cannot
     drift: change it and both pills move together. */
  --eyebrow-cap: var(--text-subheading);
  display: inline-flex;
  align-items: center;     /* keeps "Win this" centred against the larger .hatton word */
  gap: 4px;                /* space between the lead-in word and the .hatton category */
  align-self: flex-start;
  margin-bottom: -6px;     /* tighten just the eyebrow→title gap (the flex gap is uniform) */
  padding: 8px 16px;
  border-radius: var(--radius-pill);
  font-size: var(--text-body-sm);
  font-weight: var(--font-weight-semibold);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  line-height: 1;
}
/* Singular category accent inside the product pill — Switzer Bold
   Italic sits visually low against the flex box-height centering
   (its glyph metrics run heavier below the baseline than Inter's
   uppercase lead-in text), so nudge it up to optically match. */
.showcase-eyebrow .hatton {
  text-transform: none;
  letter-spacing: var(--ls-rg);
  font-size: 1.2em;
  position: relative;
  top: -1px;
}
/* Tighter side padding than the base: the category is set in the
   display face, which carries its own optical sidebearing. */
.showcase-eyebrow--draw  { background: var(--color-brand-highlight); padding: 8px 14px; color: var(--color-brand-on-highlight); }
.showcase-eyebrow--draw .hatton { font-size: var(--eyebrow-cap); }
/* No category to stand it up, so it takes the cap as a line box
   instead — which is what makes the two pills the same height rather
   than two paddings that happen to agree today. */
.showcase-eyebrow--promo {
  background: var(--glass-fill-dark);
  border: 1px solid rgba(var(--color-brand-highlight-rgb), 0.34);
  -webkit-backdrop-filter: blur(var(--glass-blur));
          backdrop-filter: blur(var(--glass-blur));
  /* Snow, not the highlight: this pill stands on a photograph, and
     text over imagery is white everywhere on this platform. */
  color: var(--color-snow);
  /* The border the draw pill does not carry would make this one 2px
     taller, so the padding gives the 2px back and the pair still
     stand on one line box. */
  padding-block: 7px;
  line-height: var(--eyebrow-cap);
}

/* ── Draw eyebrow — "light reveal" on slide entrance ─────────
   When a draw slide becomes the active slide, renderContent()
   rebuilds the stage markup, so this pill mounts fresh each time
   it appears, and the CSS animations below replay on every
   entrance with no JS hook. A luminous, faintly iridescent light
   sweeps lengthwise THROUGH the pill (::before, screen-blended so
   it actually brightens the green as it passes) while a brighter
   glint travels once AROUND its stroke (::after), under a scale
   pop of the pill. A short delay lets it land just as the slide
   settles in. Motion is transform/opacity/filter only and gated
   behind prefers-reduced-motion; at rest, and for reduced-motion
   users, the pill is the plain solid pill above. The hue tints
   are transient refracted light, not a surface colour — accent
   discipline on the resting pill holds.

   A DRAW SLIDE'S CALL TO ACTION TAKES THE SAME LIGHT, one
   declaration rather than a second one that drifts. The CTA carried
   a different entrance effect until now — a masked iridescence band
   plus a hundred-odd hand-placed sparkle dots — which meant the two
   objects on one slide caught light two different ways, and the
   sparkle wave was 111 lines serving two selectors. The eyebrow's
   sheen and glint were already geometrically identical to it
   (`.btn-shine` shares the same pair on a 5s loop), so the CTA and
   the embeddable widget's CTA now join these rules and the wave is
   gone.

   A PROMO SLIDE'S CTA IS THE SAME BUTTON AND TAKES NONE OF IT, and
   that is the same split the pill above it keeps: `--draw` reveals,
   `--promo` does not. The sweep is a prize announcing itself. A
   promo is a message about the platform, and a message that
   sparkles competes with the prize on the next slide — so the hook
   (`.showcase-cta`) is written onto a draw slide's button only, and
   a promo slide's is a plain pill. The widget renders draw slides
   exclusively, so `.rw-cta` always carries it.

   Which of the four animations each object takes is the one thing
   left that differs, and it is stated where they are declared. */

/* Drives the perimeter glint's travel (Houdini angle, falls back
   to the static 0deg start where unsupported). */
@property --se-shine {
  syntax: '<angle>';
  inherits: false;
  initial-value: 0deg;
}

.showcase-eyebrow--draw,
.showcase-cta,
.rw-cta {
  position: relative;
  overflow: hidden;        /* clips the sheen to the silhouette */
  isolation: isolate;      /* contains the screen blend; keeps ::before above the fill, below the text */
}

/* Lengthwise sheen — screen-blended luminance above the fill,
   below the lettering, so the surface visibly catches light. */
.showcase-eyebrow--draw::before,
.showcase-cta::before,
.rw-cta::before {
  content: "";
  position: absolute;
  top: -2px;
  bottom: -2px;
  left: 0;
  width: 52%;
  z-index: -1;
  pointer-events: none;
  opacity: 0;
  mix-blend-mode: screen;
  transform: translateX(-210%) skewX(-14deg);
  filter: blur(0.3px);
  background: linear-gradient(100deg,
      transparent 8%,
      rgba(110, 200, 255, 0.42) 23%,   /* cyan       */
      rgba(130, 255, 205, 0.52) 35%,   /* mint       */
      rgba(255, 255, 255, 0.95) 50%,   /* white core */
      rgba(255, 226, 150, 0.50) 60%,   /* gold       */
      rgba(255, 150, 240, 0.58) 70%,   /* orchid     */
      rgba(180, 150, 255, 0.44) 80%,   /* violet     */
      transparent 92%);
}

/* Perimeter glint — a bright conic arc masked to a 2px stroke ring */
.showcase-eyebrow--draw::after,
.showcase-cta::after,
.rw-cta::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  opacity: 0;
  border-radius: inherit;
  padding: 2px;
  filter: blur(0.6px) brightness(1.05);   /* lets the light radiate softly off the edge */
  background: conic-gradient(from var(--se-shine),
      transparent 0deg,
      transparent 30deg,
      rgba(110, 200, 255, 0.50) 42deg,  /* cyan   */
      rgba(130, 255, 205, 0.60) 52deg,  /* mint   */
      rgba(255, 255, 255, 1) 62deg,     /* core   */
      rgba(255, 226, 150, 0.60) 72deg,  /* gold   */
      rgba(255, 150, 240, 0.65) 82deg,  /* orchid */
      rgba(180, 150, 255, 0.50) 92deg,  /* violet */
      transparent 104deg,
      transparent 360deg);
  -webkit-mask:
      linear-gradient(#000 0 0) content-box,
      linear-gradient(#000 0 0);
          mask:
      linear-gradient(#000 0 0) content-box,
      linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
}

@media (prefers-reduced-motion: no-preference) {
  /* pop = scale/opacity; glow = a soft halo bloom (filter only) timed
     to the sweep, so the pill visibly radiates light as it passes.
     THE PILL TAKES BOTH; THE BUTTON TAKES NEITHER. The pop fills
     `both`, so its final `scale(1)` would stay applied and out-rank
     the button's own `:hover` lift and `:active` press — a control
     whose press does nothing is a worse control than one that does
     not pop (DESIGN.md → Motion Philosophy; CLAUDE.md → "a press
     that wipes a resting transform is a bug"). The glow is a
     drop-shadow, and a button on this platform carries none. What
     the button gets is the light itself, which is the effect. */
  .showcase-eyebrow--draw         { animation: se-eyebrow-pop 0.6s var(--ease-deck-out) both,
                                                se-eyebrow-glow 1.1s linear 0.2s backwards; }
  /* Linear travel so the light glides the full length / all the way
     round, instead of whipping to one end and vanishing. */
  .showcase-eyebrow--draw::before { animation: se-eyebrow-sheen 0.9s  linear 0.26s backwards; }
  .showcase-eyebrow--draw::after  { animation: se-eyebrow-ring  0.95s linear 0.22s backwards; }
  /* The stage's call to action, a beat behind the eyebrow above it,
     so the light reads as one pass down the slide rather than two
     things flashing at once. */
  .showcase-cta::before           { animation: se-eyebrow-sheen 0.9s  linear 0.5s backwards; }
  .showcase-cta::after            { animation: se-eyebrow-ring  0.95s linear 0.46s backwards; }
}

/* Arrival pop — a clear lift to +5% then settle */
@keyframes se-eyebrow-pop {
  0%   { opacity: 0; transform: scale(0.9); }
  55%  { opacity: 1; transform: scale(1.05); }
  100% { opacity: 1; transform: scale(1); }
}

/* Soft iridescent halo bloom (mint + orchid), then settles flat */
@keyframes se-eyebrow-glow {
  0%   { filter: drop-shadow(0 0 0 rgba(150, 255, 210, 0))     drop-shadow(0 0 0 rgba(255, 165, 240, 0)); }
  40%  { filter: drop-shadow(0 0 13px rgba(150, 255, 210, 0.45)) drop-shadow(0 0 18px rgba(255, 165, 240, 0.34)); }
  100% { filter: drop-shadow(0 0 0 rgba(150, 255, 210, 0))     drop-shadow(0 0 0 rgba(255, 165, 240, 0)); }
}

/* Light running the length of the pill */
@keyframes se-eyebrow-sheen {
  0%   { opacity: 0; transform: translateX(-210%) skewX(-14deg); }
  14%  { opacity: 1; }
  86%  { opacity: 1; }
  100% { opacity: 0; transform: translateX(360%) skewX(-14deg); }
}

/* Light travelling once around the stroke */
@keyframes se-eyebrow-ring {
  0%   { opacity: 0;   --se-shine: 0deg; }
  16%  { opacity: 1; }
  84%  { opacity: 1; }
  100% { opacity: 0;   --se-shine: 360deg; }
}
.showcase-title {
  font-family: var(--font-display); letter-spacing: var(--ls-display);
  font-size: var(--text-display-sm);
  font-weight: var(--font-weight-light);
  line-height: 1;
  color: var(--color-snow);
  text-shadow: rgba(0, 0, 0, 0.3) 0 1px 12px;
}

.showcase-desc {
  font-size: var(--text-body-lg);
  line-height: var(--leading-body-lg);
  color: rgba(255, 255, 255, 0.86);
  max-width: 520px;
  text-shadow: rgba(0, 0, 0, 0.28) 0 1px 10px;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Countdown on the dark stage — keeps the documented Snow chips,
   only lightens labels + separators so they read on imagery. */
.showcase-countdown .count-label { color: rgba(255, 255, 255, 0.72); }
.showcase-countdown .count-sep { color: rgba(255, 255, 255, 0.55); }

/* ── Showcase CTA row + countdown pill ─────────────────────────
   The countdown sits beside the stage's call to action on draw
   slides. Its box metrics (padding, font size/weight, line-height,
   radius, 1px border) mirror .btn-lg so the two pills are exactly
   the same height. The base pill wears the one action-coloured skin
   the whole countdown family wears — flat, no highlight, no glow
   and no shadow, sitting with the system's flat CTA surfaces
   (DESIGN.md → Countdown); on the stage it takes the Flat Glass
   variant below instead. Ticking is wired in startCountdown().

   STACKED ON A PHONE IT GIVES THE ROW BACK to the CTA beside it
   (see the 768 step below): this is a reading, and the pill under
   it is the action. */
.showcase-cta-row {
  display: flex;
  align-items: center;
  gap: var(--spacing-12);
  flex-wrap: wrap;
}

.countdown-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--element-gap);
  padding: 14px 24px 14px 18px;             /* btn-lg top/right/bottom; tighter left for the icon */
  border: 1px solid transparent;            /* == .btn border, keeps heights equal */
  border-radius: var(--radius-buttons);     /* == .btn-lg */
  font-family: var(--font-body);
  font-size: var(--text-body-md);           /* == .btn-lg */
  font-weight: var(--font-weight-medium);   /* == .btn */
  line-height: 1;
  white-space: nowrap;
  color: var(--color-snow);
  /* The action role, flat — the same fill every status mark on the
     platform takes, because a closing clock IS one. It was a raw
     literal orange that was not even the token it imitated. */
  background: var(--color-brand-action);
}
.countdown-pill__label { color: rgba(255, 255, 255, 0.85); font-style: italic; }
.countdown-pill .icon {
  width: 18px;
  height: 18px;
  margin-block: -2px;        /* neutralise the taller icon so pill height stays == button */
  color: rgba(255, 255, 255, 0.92);
}
.countdown-pill__time {
  color: var(--color-snow);
  font-weight: var(--font-weight-bold);
  font-variant-numeric: tabular-nums;       /* digits hold width as they tick */
  letter-spacing: 0.01em;
}

/* ── Flat Glass countdown ──────────────────────────────────────
   THE SAME TIMER, WEARING THE FLAT GLASS BUTTON'S MATERIAL — the
   three properties `.btn-glass` is made of (the dark glass fill,
   the mint-glass hairline, the blur), so the pill and the button
   beside it are one surface in two shapes.

   It exists because the stage's call to action is Blush now. A
   solid Ember pill next to a Blush button is two saturated fills
   competing on a photograph, and the reading is the one that has
   to give way: the clock is a fact about the draw, the button is
   the thing to press. Glass lets the photograph through and leaves
   exactly one filled control on the stage.

   The solid variant stays for every surface where the timer IS the
   urgency and there is no button beside it — a ticket card, a
   sticky rail, the countdown banner. */
.countdown-pill--glass {
  background: var(--glass-fill-dark);
  border-color: var(--deck-glass-border);
  -webkit-backdrop-filter: blur(var(--glass-blur));
          backdrop-filter: blur(var(--glass-blur));
  color: var(--color-snow);
}
.countdown-pill--glass .countdown-pill__time { color: var(--color-snow); }
.countdown-pill--glass .countdown-pill__label { color: rgba(255, 255, 255, 0.78); }
.countdown-pill--glass .icon { color: rgba(255, 255, 255, 0.86); }

/* ── Strip (centred filmstrip navigator) ─────────────────────
   THE ONE PART OF THE HERO THAT CAN BE TURNED OVER. The stage
   above is a photograph carrying white type, and white on a
   picture is not a choice an operator gets to make — so when this
   section is set light or dark (js/cms-sections.js → GROUND.hero),
   the strip is the whole of what changes under this theme.

   Every colour in the strip is one of six properties declared
   here, so the light dressing is six values in one rule rather
   than an override per selector. The dark set IS the shipped one,
   which is why `.showcase--dark` needs no rules of its own: a
   dressing that restates the base is a second copy of it. */
.showcase-strip {
  --strip-item-w: 280px;
  --strip-ground: var(--dark-surface);
  --strip-ink: var(--dark-heading);
  --strip-meta: var(--dark-body);
  /* Let down from the heading ink rather than from white: the ink
     is the one colour guaranteed to read on whatever ground the
     operator set, and a white hairline on a pale "dark" ground is
     a hairline nobody can see. */
  --strip-edge: color-mix(in srgb, var(--strip-ink) 8%, transparent);
  --strip-wash: color-mix(in srgb, var(--strip-ink) 6%, transparent);
  --strip-ring: color-mix(in srgb, var(--strip-ink) 50%, transparent);
  --strip-draw: var(--dark-heading);
  --strip-promo: var(--dark-accent);
  position: relative;
  display: flex;
  align-items: stretch;
  background: var(--strip-ground);
  border-top: 1px solid var(--strip-edge);
  border-bottom: 1px solid var(--strip-edge);
}

/* Set LIGHT, the strip takes the platform's light ground — the same
   Mint Ice Cream every other section turns over onto, so the hero
   agrees with the page under it rather than inventing a third
   surface. The promo bar takes the promo pill's own ink, which is
   the colour that already tells a promotion from a draw in this
   component. */
.showcase--light .showcase-strip {
  --strip-ground: var(--color-mint-ice-cream);
  --strip-ink: var(--color-race-green);
  --strip-meta: rgba(var(--color-brand-primary-rgb), 0.7);
  --strip-edge: rgba(var(--color-brand-primary-rgb), 0.14);
  --strip-wash: rgba(var(--color-brand-primary-rgb), 0.06);
  --strip-ring: rgba(var(--color-brand-primary-rgb), 0.5);
  --strip-draw: var(--color-race-green);
  /* The promo pill's own ink: on Mint the Blush that marks a promo
     on the dark strip has no contrast, so the bar takes the colour
     that already tells a promo from a draw here (DESIGN.md →
     Showcase). It is the role rather than the hue, so it follows
     the pill it is quoting. */
  --strip-promo: var(--color-brand-on-secondary);
}

.strip-nav {
  flex: 0 0 auto;
  width: 56px;
  display: grid;
  place-items: center;
  padding: 0;
  background: none;
  border: none;
  color: var(--strip-meta);
  cursor: pointer;
  z-index: 2;
  transition: color var(--duration-micro) ease, background var(--duration-micro) ease, transform 160ms var(--ease-deck-out);
}
@media (hover: hover) and (pointer: fine) {
  .strip-nav:hover { color: var(--strip-ink); background: var(--strip-wash); }
}
.strip-nav:active { transform: scale(0.92); }
.strip-nav:focus-visible { outline: 2px solid var(--strip-ring); outline-offset: -4px; }

/* Moving track sits inside a masked viewport; the active item is
   translated to centre. Transform-only motion. */
.strip-viewport {
  position: relative;
  flex: 1 1 auto;
  overflow: hidden;
  touch-action: pan-y;   /* same swipe as the stage above it */
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 8%, #000 92%, transparent 100%);
  mask-image: linear-gradient(to right, transparent 0, #000 8%, #000 92%, transparent 100%);
}
.strip-track {
  position: relative; /* offset parent for centring math (offsetLeft) */
  display: flex;
  align-items: stretch;
  gap: var(--spacing-20);
  padding: var(--spacing-16) 0;
  will-change: transform;
  /* NO TRANSITION, AND THAT IS THE COMPONENT. The track's position is
     written per frame by the strip's spring (js/storefront-home.js →
     Motion), which is what lets it be grabbed, reversed and thrown
     mid-move. A transition here would be a second opinion about where
     the track is: every frame the spring wrote would be eased AGAIN
     towards the one after it, so the strip would lag the finger by a
     whole duration and a re-grab would still have a target position
     to jump to. The 0.65s carousel duration this replaced is what
     `response` is spending instead. */
}

.strip-item {
  position: relative;
  overflow: hidden;
  flex: 0 0 auto;
  width: var(--strip-item-w);
  display: flex;
  align-items: center;
  gap: var(--spacing-12);
  padding: var(--spacing-12);
  border-radius: 0;
  background: transparent;
  border: 1px solid transparent;
  color: var(--strip-meta);
  text-align: left;
  cursor: pointer;
  opacity: 0.55;
  transition: opacity var(--duration-micro) ease, transform 160ms var(--ease-deck-out);
  -webkit-user-select: none; user-select: none;
}
@media (hover: hover) and (pointer: fine) {
  .strip-item:hover { opacity: 0.85; }
}
.strip-item:active { opacity: 1; transform: scale(0.98); }
.strip-item.is-center { opacity: 1; }
.strip-item:focus-visible { outline: 2px solid var(--strip-ring); outline-offset: 2px; }

.strip-progress {
  position: absolute;
  bottom: 0;
  left: 50%;
  width: calc(var(--strip-item-w) - 2 * var(--spacing-12));
  margin-left: calc((var(--strip-item-w) - 2 * var(--spacing-12)) / -2);
  height: 6px;
  transform-origin: left center;
  animation: progress-fill 8s linear forwards;
  pointer-events: none;
  z-index: 3;
}
.strip-progress--draw  { background: var(--strip-draw); }
.strip-progress--promo { background: var(--strip-promo); }
@keyframes progress-fill {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

.strip-thumb {
  width: 60px;
  height: 60px;
  flex-shrink: 0;
  border-radius: 12px;
  overflow: hidden;
  border: 1px solid color-mix(in srgb, var(--strip-ink) 14%, transparent);
}
.strip-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }

.strip-text { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.strip-name {
  color: var(--strip-ink);
  font-weight: var(--font-weight-semibold);
  font-size: var(--text-body);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.strip-meta {
  font-size: var(--text-body-xs);
  color: var(--strip-meta);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

@media (max-width: 768px) {
  .showcase-stage { min-height: 480px; }
  .showcase-content-wrap { padding-bottom: var(--spacing-24); }
  .showcase-content { gap: var(--spacing-16); }
  .showcase-title { font-size: var(--text-heading-lg); line-height: 1; }
  .showcase-desc { font-size: var(--text-body-md); }
  /* THE TWO PILLS ARE SIDE BY SIDE ON A DESKTOP AND STACKED HERE,
     and stacked they were the wrong way round: the Ember countdown
     is a wide loud pill and the Deep Forest CTA under it is a short
     dark one on a dark photograph, so the thing the hero exists to
     be tapped for read as the quieter of the two. Same components,
     same colours — the action simply takes the row, which is what
     it has on the desktop layout too. The countdown keeps its own
     width, because it is a reading and not a control. */
  .showcase-cta-row { align-items: stretch; gap: var(--spacing-8); }
  .showcase-cta-row .btn { width: 100%; }
  .showcase-cta-row .countdown-pill { align-self: flex-start; }
  .showcase-strip { --strip-item-w: 232px; }
  .strip-nav { width: 40px; }
  .strip-item { padding: var(--spacing-8); }
  .strip-thumb { width: 50px; height: 50px; }
}

/* The strip's arrows are the only way to page it without a swipe,
   and 40px is short of the touch floor. The strip is a fixed-height
   row, so the control grows its width rather than its box. */
@media (pointer: coarse) {
  .strip-nav { min-width: var(--touch-target); }
}

/* ============================================================
   LEGAL PAGE
   The one layout every legal page shares: a Deep Forest heading
   band, then a single column of authored prose.

   It is deliberately the plainest page on the storefront. A legal
   page is read to find one clause, so what it owes the reader is a
   measure short enough to scan and headings big enough to jump
   between — not cards, not columns, not a photograph. The prose
   itself is `.rich-text` (base.css), which is the same block set
   the console's editor can produce, so nothing can appear here
   that an operator cannot go back and change.

   No image, so no scrim: this band is flat Deep Forest rather than
   .page-banner's photograph and washes, which is what makes it a
   different component instead of that one with the picture missing.
   ============================================================ */
.legal-head {
  width: 100vw; margin-left: calc(50% - 50vw);
  background: var(--color-deep-forest);
  padding: var(--spacing-64) 0 var(--spacing-48);
  text-align: center;
  /* This block's share of the section rhythm — see .page-banner. */
  margin-bottom: var(--spacing-48);
}
@media (max-width: 768px) { .legal-head { margin-bottom: var(--spacing-32); } }
/* No eyebrow. A page called "Privacy Policy" does not also need a
   chip above it saying LEGAL — the title is the label, and a band
   that introduces its own heading is a heading drawn twice. */
.legal-head-title {
  font-family: var(--font-display); letter-spacing: var(--ls-display);
  font-size: var(--text-display-sm); font-weight: var(--font-weight-light);
  line-height: 1.08; color: var(--color-mint-ice-cream);
  margin-bottom: var(--spacing-12);
}
.legal-head-standfirst {
  font-size: var(--text-body-lg); line-height: var(--leading-body-lg);
  color: rgba(255, 255, 255, 0.72);
  max-width: 560px; margin: 0 auto;
}
/* When the page last changed, printed by the record rather than
   typed into the prose — a date somebody has to remember to bump is
   a date that is eventually wrong. It sits a step quieter than the
   standfirst: it is a fact about the page, not part of what it says. */
.legal-head-updated {
  display: block; margin-top: var(--spacing-16);
  font-size: var(--text-body-sm); color: rgba(255, 255, 255, 0.52);
  letter-spacing: 0.02em;
}
/* The measure. Prose read for one clause wants a short line and a
   lot of air between headings; the column is centred because there
   is nothing beside it to align to. */
.legal-body { max-width: 720px; margin: 0 auto; }
.legal-body .rich-text { font-size: var(--text-body-lg); line-height: var(--leading-body-lg); }
.legal-body .rich-text h3 { margin-top: var(--spacing-40); }
.legal-body .rich-text h3:first-child { margin-top: 0; }
.legal-foot {
  margin-top: var(--spacing-40); padding-top: var(--spacing-24);
  border-top: 1px solid var(--color-fog);
  display: flex; flex-wrap: wrap; gap: var(--spacing-12) var(--spacing-24);
  align-items: baseline; justify-content: space-between;
}

@media (max-width: 768px) {
  .legal-head { padding: var(--spacing-40) 0 var(--spacing-32); }
  .legal-head-title { font-size: var(--text-heading-lg); line-height: 1.1; }
  .legal-body .rich-text { font-size: var(--text-body); }
}

/* ============================================================
   PAGE BANNER
   Static, shorter sibling of .showcase for sub-page headers —
   full-bleed photographic image, scrim, white overlay text.
   Same visual DNA as the homepage showcase (scrim curve, Snow
   text, text-shadow) with no carousel/video/countdown: one
   fixed image, one message. Sits under the solid nav (unlike
   .showcase, it does not pull up behind a transparent nav).
   ============================================================ */
.page-banner {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  width: 100vw;
  margin-left: calc(50% - 50vw);
  height: 360px;
  display: flex;
  align-items: flex-end;
  background: var(--color-deep-forest);
  /* This block's share of the section rhythm — a .section pays 48px on
     each of its own edges (base.css → Layout primitives), so a head
     with no padding of its own owes the section under it the other
     half of the 96px gap. */
  margin-bottom: var(--spacing-48);
}
@media (max-width: 768px) { .page-banner { margin-bottom: var(--spacing-32); } }
.page-banner-media { position: absolute; inset: 0; z-index: 0; }
.page-banner-media img { width: 100%; height: 100%; object-fit: cover; object-position: center 15%; }
/* Two scrims: a left-to-right wash keeps the text zone readable
   regardless of what sits behind it, layered under the standard
   bottom-heavy vertical fade also used on .showcase. */
.page-banner-scrim {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background:
    linear-gradient(90deg, rgba(var(--color-brand-primary-strong-rgb), 0.75) 0%, rgba(var(--color-brand-primary-strong-rgb), 0.32) 42%, rgba(var(--color-brand-primary-strong-rgb), 0.1) 68%),
    linear-gradient(180deg, rgba(var(--color-brand-primary-strong-rgb), 0.3) 0%, rgba(var(--color-brand-primary-strong-rgb), 0.36) 45%, rgba(var(--color-brand-primary-strong-rgb), 0.82) 100%);
}
.page-banner-content {
  position: relative;
  z-index: 2;
  width: 100%;
  padding-bottom: var(--spacing-40);
}
.page-banner-eyebrow {
  display: inline-flex;
  align-items: center;
  padding: 7px 16px;
  border-radius: var(--radius-pill);
  background: rgba(255, 255, 255, 0.12);
  border: 1px solid rgba(var(--color-brand-highlight-rgb), 0.34);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
  color: var(--color-snow);
  font-size: var(--text-body-sm);
  font-weight: var(--font-weight-medium);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin-bottom: var(--spacing-16);
}
.page-banner-title {
  font-family: var(--font-display); letter-spacing: var(--ls-display);
  font-size: var(--text-display-sm);
  font-weight: var(--font-weight-light);
  line-height: 1.08;
  color: var(--color-snow);
  text-shadow: rgba(0, 0, 0, 0.3) 0 1px 12px;
  max-width: 700px;
  margin-bottom: var(--spacing-16);
}
.page-banner-desc {
  font-size: var(--text-body-lg);
  line-height: var(--leading-body-lg);
  color: rgba(255, 255, 255, 0.86);
  max-width: 520px;
  text-shadow: rgba(0, 0, 0, 0.28) 0 1px 10px;
}

@media (max-width: 768px) {
  .page-banner { height: 340px; }
  .page-banner-title { font-size: var(--text-heading-lg); line-height: 1.1; }
  .page-banner-content { padding-bottom: var(--spacing-24); }
  /* No room for the left/right split at this width, text and
     image occupy the same narrow column, so fall back to one
     even, darker wash instead of the two-scrim desktop split. */
  .page-banner-scrim {
    background: linear-gradient(180deg, rgba(var(--color-brand-primary-strong-rgb), 0.5) 0%, rgba(var(--color-brand-primary-strong-rgb), 0.56) 45%, rgba(var(--color-brand-primary-strong-rgb), 0.88) 100%);
  }
}

/* ============================================================
   CATEGORY SHOWCASE CAROUSEL
   Large 28px video tiles in a horizontal snap rail. Imagery is
   the emotional layer (DESIGN.md); chrome stays achromatic.
   ============================================================ */
.category-rail {
  display: flex;
  gap: var(--spacing-24);
  overflow: hidden;
  padding-bottom: var(--spacing-12);
  scrollbar-width: none;
  overscroll-behavior-x: contain;
  /* The rail has no scroller of its own (overflow is hidden — the
     carousel owns the position), so the horizontal axis is the
     gesture's and the vertical stays the page's. Without this a
     drag on a rail that fills a phone screen would be swallowed
     whole and the reader would be stranded on it. */
  touch-action: pan-y;
  /* Break out of .container to bleed edge-to-edge */
  width: 100vw;
  margin-left: calc(50% - 50vw);
}
.category-rail::-webkit-scrollbar { display: none; }
/* A SURFACE THAT MOVES UNDER THE POINTER SAYS SO. The class is put
   on by the carousel rather than written into the markup, because
   the rail is only draggable where that behaviour is running — on
   the lean layouts it is the browser's own scroller and the cursor
   belongs to the browser. Grab cursors are a fine-pointer idea; a
   thumb has no cursor to change. */
@media (hover: hover) and (pointer: fine) {
  .category-rail.is-grabbable { cursor: grab; }
  .category-rail.is-grabbing { cursor: grabbing; }
  /* Mid-drag the pointer is not "over" anything in the way a hover
     means, and a tile lighting up under a finger that is pushing
     the rail past it reads as a misfire. */
  .category-rail.is-grabbing * { pointer-events: none; }
}

.category-card-lg {
  position: relative;
  flex: 0 0 700px;
  aspect-ratio: 16 / 9;
  border-radius: var(--radius-cards);
  overflow: hidden;
  background: var(--color-fog);
  color: var(--color-snow);
  text-decoration: none;
  isolation: isolate;
  transition: transform 160ms var(--ease-deck-out);
}
.category-card-lg:active { transform: scale(0.98); }
.category-card-lg:focus-visible { outline: 2px solid var(--color-race-green); outline-offset: 3px; }
@media (hover: hover) and (pointer: fine) {
  .category-card-lg:hover { text-decoration: none; }
}
/* A CATEGORY WITH NO ARTWORK YET still has to be readable: this tile
   is white type over a photograph, and with no photograph the type
   is white on the Fog it falls back to. So a bare tile is given the
   one ground white is always drawn on (DESIGN.md → text over
   imagery is Snow), and its glass chip a ground to sit on rather
   than a picture. It is a state of the record, not a variant an
   author picks — js/storefront-home.js sets it when the category
   has neither a still of its own nor a live draw to borrow one
   from. */
.category-card-lg--bare { background: var(--color-deep-forest); }
.category-card-lg--bare .category-card-chip {
  background: rgba(var(--color-brand-highlight-rgb), 0.10);
  border-color: rgba(var(--color-brand-highlight-rgb), 0.24);
}

.category-card-media {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transform: scale(1.025);
  transition: transform var(--duration-deck-image) var(--ease-deck-soft);
}
@media (hover: hover) and (pointer: fine) {
  .category-card-lg:hover .category-card-media { transform: scale(1.05); }
}
/* Poster image sits under the video layer; revealed if the video
   fails to load and removes itself (see onerror in the markup). */
.category-card-poster { z-index: -1; }

.category-card-chip {
  position: absolute;
  top: var(--spacing-20);
  left: var(--spacing-20);
  width: 40px;
  height: 40px;
  border-radius: var(--radius-icons);
  display: grid;
  place-items: center;
  color: var(--color-snow);
  background: rgba(var(--color-brand-primary-strong-rgb), 0.14);
  border: 1px solid rgba(var(--color-brand-highlight-rgb), 0.34);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
}

.category-card-overlay {
  position: absolute;
  left: var(--spacing-20);
  right: var(--spacing-20);
  bottom: var(--spacing-20);
  display: flex;
  flex-direction: column;
  gap: var(--spacing-4);
}
.category-card-title {
  font-family: var(--font-display); letter-spacing: var(--ls-heading-sm);
  font-size: var(--text-heading-sm);
  font-weight: var(--font-weight-semibold);
  line-height: 1;
  color: var(--color-snow);
  text-shadow: rgba(0, 0, 0, 0.34) 0 1px 8px;
}
.category-card-meta {
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: var(--text-body-xs);
  color: rgba(255, 255, 255, 0.82);
}
.category-card-meta strong { color: var(--color-snow); font-weight: var(--font-weight-semibold); }

@media (max-width: 768px) {
  .category-card-lg { flex-basis: 360px; }
}

/* ── Rail arrow controls (shared by horizontal rails) ──────── */
.rail-controls { display: flex; gap: var(--element-gap); }
/* A rail that fits hides its own controls. `display: flex` above
   beats the attribute's own UA rule, so the state has to be said
   here or `hidden` does nothing. */
.rail-controls[hidden] { display: none; }
.rail-nav {
  width: 44px;
  height: 44px;
  flex-shrink: 0;   /* a fixed width still shrinks in a flex row */
  border-radius: var(--radius-pill);
  border: 1px solid var(--color-pebble);
  background: var(--color-snow);
  color: var(--color-ink);
  display: grid;
  place-items: center;
  cursor: pointer;
  transition: border-color var(--duration-micro) ease, transform 160ms var(--ease-deck-out);
}
@media (hover: hover) and (pointer: fine) {
  .rail-nav:hover:not(:disabled) { border-color: var(--color-graphite); transform: translateY(-1px); }
}
.rail-nav:active:not(:disabled) { transform: scale(0.95); }
.rail-nav:focus-visible { outline: 2px solid var(--color-race-green); outline-offset: 2px; }
.rail-nav:disabled { opacity: 0.32; cursor: not-allowed; }

/* Small — for a rail whose items are chips rather than cards, where
   the 44px control is taller than the row it pages. Desktop only by
   construction: below 768 the whole control stands down and the
   swipe takes over, so it never owes the 44px touch floor. */
.rail-nav--sm { width: 32px; height: 32px; }
.rail-nav--sm .icon { width: 16px; height: 16px; }

/* Primary variant — Deep Forest fill, for rails on light surfaces
   where the arrow doubles as a call to action (DESIGN.md Primary
   Pill Button material). */
.rail-nav-primary {
  background: var(--color-deep-forest);
  border-color: rgba(var(--color-brand-highlight-rgb), 0.18);
  color: var(--color-mint-ice-cream);
}
@media (hover: hover) and (pointer: fine) {
  .rail-nav-primary:hover:not(:disabled) { border-color: rgba(var(--color-brand-highlight-rgb), 0.32); }
}

@media (max-width: 768px) {
  .rail-controls { display: none; } /* touch swipe takes over */
}

/* ============================================================
   CATEGORY GRID
   Every category the platform runs, on a page of its own
   (buyer/categories.html), drawn with the tile the homepage rail
   draws — the rail is the featured few, this is the catalogue,
   and one tile means the two cannot come to describe a category
   differently.

   The track is fitted to however many there are rather than
   ruled into the number this deployment happens to hold: how
   many categories exist is the operator's (Storefront ▸
   Categories), and a grid cut to four columns is the first
   draft's count written into the stylesheet. Posters only —
   the clip is the rail's, and the reason this page exists is
   that a catalogue cannot afford one per tile.
   ============================================================ */
.category-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: var(--spacing-20);
}
/* The tile is sized by the rail it usually stands in; in a grid the
   track sets the width and the 16:9 sets the height. */
.category-grid > .category-card-lg { flex: none; width: auto; }

@media (max-width: 768px) {
  .category-grid { gap: var(--spacing-16); }
}

/* ============================================================
   CATEGORY MOSAIC
   Editorial imagery grid of category cards: one 2×2 feature tile
   plus singles, reusing .category-card-lg with the rail sizing
   overridden. Poster images only — the carousel owns video.
   ============================================================ */
.category-mosaic {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-auto-rows: 200px;
  gap: var(--spacing-16);
}
.category-mosaic .category-card-lg {
  flex: none;
  width: auto;
  height: 100%;
  aspect-ratio: auto;
}
.mosaic-tall { grid-column: span 2; grid-row: span 2; }

@media (max-width: 900px) {
  .category-mosaic { grid-template-columns: repeat(2, 1fr); grid-auto-rows: 170px; }
}

/* ============================================================
   PROGRESS BAR
   ============================================================ */
.progress { height: 6px; background: var(--color-fog); border-radius: var(--radius-pill); overflow: hidden; }
.progress-md { height: 10px; }
.progress-lg { height: 14px; }
.progress-fill { height: 100%; background: var(--color-deep-forest); border-radius: var(--radius-pill); transition: width 0.5s var(--ease-deck-soft); }
.progress-fill.danger { background: var(--danger); }
.progress-fill.success { background: var(--success); }

/* ── Glow variant — entries-fill "approaching sell-out" cue ─────
   Static: the Deep Forest fill warms toward a bright Mint leading
   edge (the "gradient into a subtle glow at the end"). Once entries
   pass 70%, initProgressGlow() adds .is-approaching and sets --glow
   (0 at 70% → 1 at 100%); a soft light wave then sweeps the bar,
   quickening and brightening toward sell-out. Transform/opacity only,
   one CSS animation (off the main thread), paused under reduced-motion.
   Reserved for entries-fill bars — not KPI/operational meters. */
.progress-fill--glow {
  position: relative;
  overflow: hidden;                         /* clip the wave to the pill */
  background: linear-gradient(90deg,
    var(--color-race-green) 0%,
    var(--color-deep-forest) 55%,
    color-mix(in oklab, var(--color-deep-forest), var(--color-mint-ice-cream) 26%) 100%);
}
/* Leading-edge glow — always present, strengthens with --glow. */
.progress-fill--glow::after {
  content: "";
  position: absolute;
  inset: 0 0 0 auto;
  width: 42%;
  background: linear-gradient(90deg, transparent, rgba(var(--color-brand-highlight-rgb), calc(0.14 + var(--glow, 0) * 0.42)));
  opacity: calc(0.55 + var(--glow, 0) * 0.45);
  pointer-events: none;
}
/* Sweeping wave — held still until entries approach (≥70%). */
.progress-fill--glow::before {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg,
    transparent 38%,
    rgba(var(--color-brand-highlight-rgb), 0.45) 50%,
    transparent 62%);
  transform: translateX(-100%);
  opacity: 0;
  pointer-events: none;
}
.progress-fill--glow.is-approaching::before {
  opacity: calc(0.3 + var(--glow, 0) * 0.6);
  animation: progress-wave calc(2.6s - var(--glow, 0) * 1.2s) linear infinite;
}
@keyframes progress-wave {
  from { transform: translateX(-100%); }
  to   { transform: translateX(100%); }
}

.fill-meta { display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--spacing-8); font-size: var(--text-body-xs); color: var(--color-steel); }
.fill-pct { font-weight: var(--font-weight-bold); color: var(--color-ink); }

/* ── Goal marker — a target sitting partway along the bar ───────
   A capped draw has a go-live point somewhere below sell-out; the
   marker shows where it falls without turning the bar into a
   second chart. The wrapper exists because .progress clips its own
   overflow, so the tick has to live outside it. Position with
   `left: N%`. Decorative: the number is always stated in the meta
   row beside it, never only here. */
.progress-goal { position: relative; }
.progress-goal-mark {
  position: absolute;
  top: -3px; bottom: -3px;
  width: 2px;
  margin-left: -1px;
  border-radius: var(--radius-pill);
  background: var(--color-race-green);
  pointer-events: none;
}

/* ============================================================
   COUNTDOWN
   Unit timer (Days/Hrs/Min/Sec) wearing the .countdown-pill skin:
   one solid Ember pill, white numerals and translucent labels.
   Consolidated so every timer — pill, unit, banner, rail — reads as
   one family. Flat (no shadow/glow) to match the system's flat CTA
   surfaces; the Ember fill mirrors .countdown-pill, duplicated here
   (not shared) so each component block stays liftable. A translucent
   vertical rule (.count-lead border) parts the "Ends in" lead from
   the numerals.
   ============================================================ */
.countdown {
  display: inline-flex;
  align-items: center;
  gap: var(--spacing-16);
  padding: 14px 24px;
  border: 1px solid transparent;
  border-radius: var(--radius-inputs);      /* rounded-rect (== .pack-row); the pill stays fully round */
  background: var(--color-brand-action);     /* == .countdown-pill */
}
.count-unit { text-align: center; }
.count-num {
  display: block;
  font-family: var(--font-display); letter-spacing: var(--ls-heading-md);
  font-size: var(--text-heading-md);
  font-weight: var(--font-weight-bold);
  font-variant-numeric: tabular-nums;       /* digits hold width as they tick */
  line-height: 1;
  color: var(--color-snow);
}
.count-label {
  font-size: var(--text-caption);
  font-weight: var(--weight-caption);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: rgba(255, 255, 255, 0.85);
  margin-top: var(--spacing-4);
}
.count-sep {
  font-size: var(--text-heading-md);
  font-weight: var(--font-weight-bold);
  line-height: 1;
  color: rgba(255, 255, 255, 0.55);
  align-self: flex-start;
}

/* Lead-in — a clock mirroring a count-unit: the icon sits where a
   numeral would, with an "Ends in" label beneath in the same style as
   the unit labels (.count-label). Stands in for the "ending in" the
   pill spells out. White tone matches the pill icon. A translucent
   vertical rule parts it from the numerals — the border rides the
   lead's full height so it lines up with the numeral column, and the
   trailing pad + flex gap frame it evenly on both sides. */
.count-lead {
  text-align: center;
  padding-inline-end: var(--spacing-16);
  border-inline-end: 1px solid rgba(255, 255, 255, 0.35);
}
.count-lead .count-label { white-space: nowrap; }   /* keep "Ends in" on one line in narrow tiles */
.count-lead__icon {
  display: block;
  margin-inline: auto;
  width: 26px;                              /* a touch above the 24px numerals */
  height: 26px;
  /* the icon is 2px taller than the numerals; pull 1px off each end so
     it centres on the numeral row and "Ends in" sits on the same
     baseline as Days/Hrs/Min/Sec */
  margin-block: -1px;
  color: rgba(255, 255, 255, 0.92);         /* == .countdown-pill icon */
}

/* Compact — sticky rails, ticket cards, banner. Numerals stay at the
   unified 24px; only the padding and gaps tighten. */
.countdown-sm { gap: var(--spacing-12); padding: 10px 18px; }

/* Tile — full-width variant for the raffle-detail sticky rail, so the
   timer lines up with the pack-row tiles beneath it: spans the
   container and spreads the units edge to edge. Radius and numerals are
   the shared rounded-rect 24px from the base. */
.countdown-tile {
  display: flex;
  width: 100%;
  justify-content: space-between;
}

/* The tile answers to its host, not the viewport: a .countdown-host
   wrapper (inline-size container) steps the numerals down whenever the
   host runs narrow — narrow desktop rails and mobile cards alike — so
   all four units always fit. Mirrors the ≤768px viewport step-down. */
.countdown-host { container-type: inline-size; }
@container (max-width: 340px) {
  .countdown-tile { gap: var(--spacing-8); padding: 12px 14px; }
  .countdown-tile .count-num, .countdown-tile .count-sep { font-size: var(--text-subheading); }
  .countdown-tile .count-lead__icon { width: var(--text-heading-sm); height: var(--text-heading-sm); }
}

/* Bare variant — my-entries (tickets) cards. Strips the solid Ember
   fill, glow and the "Ends in" clock lead-in for a bare inline timer:
   no ground, no padding, with Deep Forest numerals and labels so the
   timer reads as a slim ink line inside the card's meta row. Separators
   sit a step lighter (ash) so the numerals stay dominant. */
/* Two class names, not one: the width media queries at the foot of
   this block re-pad and re-ground a plain .countdown, and at equal
   specificity they'd win on source order — a bare timer has to stay
   bare at every width, the way .countdown.is-ended does. */
.countdown.countdown-bare {
  display: inline-flex;
  width: auto;
  justify-content: flex-start;   /* the phone step spreads the pill; a bare timer stays packed */
  padding: 0;
  border: 0;
  background: none;
  box-shadow: none;
}
.countdown-bare .count-lead { display: none; }
.countdown-bare .count-num,
.countdown-bare .count-label { color: var(--color-deep-forest); }
.countdown-bare .count-sep { color: var(--color-ash); }

/* Ended state — once the target passes, initCountdown swaps the units
   for a single .countdown-ended tag and flags the host .is-ended. The
   host sheds its ground here so a closed timer never reads as a stuck
   "00:00:00:00" inside a live Ember pill — regardless of variant. */
.countdown.is-ended {
  padding: 0;
  border: 0;
  gap: 0;
  background: none;
  box-shadow: none;
}
/* Clear closed-draw tag: a solid Deep Forest pill with a Mint label,
   mirroring the primary-pill pairing so "Ended" reads as a deliberate
   status, not an absence. */
.countdown-ended {
  display: inline-flex;
  align-items: center;
  padding: 8px 16px;
  border-radius: var(--radius-badges);
  background: var(--color-deep-forest);
  color: var(--color-mint-ice-cream);
  font-family: var(--font-display);
  font-size: var(--text-caption);
  font-weight: var(--font-weight-bold);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  line-height: 1;
}

@media (max-width: 768px) {
  /* Numerals step down so the compact variants + lead-in still fit
     narrow rails and cards; the icon tracks them. */
  .countdown { gap: var(--spacing-12); padding: 12px 18px; }
  .countdown-sm { gap: var(--spacing-8); }
  .count-num, .count-sep { font-size: var(--text-subheading); }
  .count-lead__icon { width: var(--text-heading-sm); height: var(--text-heading-sm); }  /* 20px = 18px numerals + 2, keeps the -1px alignment */
}

/* Smallest phones — four units, three separators and the lead-in no
   longer fit a 280px content column. The timer wraps to full width
   and spreads, rather than the numerals shrinking further and losing
   the tabular rhythm that makes a ticking clock readable. */
@media (max-width: 480px) {
  .countdown {
    display: flex;
    width: 100%;
    justify-content: space-between;
    gap: var(--spacing-8);
    padding: 12px 14px;
  }
  .count-lead { padding-inline-end: var(--spacing-8); }
}

/* ============================================================
   TABS
   A row of underlined section switches over a shared rule. The
   row is authored to fit on desktop; below 768px a draw page's
   six tabs are far wider than the viewport, so the row becomes a
   swipeable rail rather than wrapping to two lines (wrapping
   moves the rule off the active tab and pushes the content down
   a full row for no gain). The rule still runs the full width —
   it sits on the wrapper, not the scrolling track.
   ============================================================ */
.tab-nav { display: flex; gap: 0; border-bottom: 1px solid var(--color-fog); margin-bottom: var(--spacing-24); }
.tab-btn {
  padding: var(--spacing-12) var(--spacing-20);
  background: none;
  border: none;
  border-bottom: 2px solid transparent;
  font-family: var(--font-body);
  font-size: var(--text-body);
  font-weight: var(--font-weight-medium);
  color: var(--color-steel);
  cursor: pointer;
  margin-bottom: -1px;
  transition: color var(--duration-micro) ease, transform 160ms var(--ease-deck-out);
  -webkit-user-select: none; user-select: none;
}
@media (hover: hover) and (pointer: fine) {
  .tab-btn:hover { color: var(--color-ink); }
}
.tab-btn:active { transform: scale(0.96); }
.tab-btn:focus-visible { outline: 2px solid var(--color-race-green); outline-offset: -2px; }
.tab-btn.active { color: var(--color-deep-forest); border-bottom-color: var(--color-deep-forest); font-weight: var(--font-weight-semibold); }
/* How many rows sit behind a filter. Quiet by default and quieter
   still than the label it follows, because it qualifies the tab
   rather than competing with it. */
.tab-count {
  margin-left: 6px;
  font-size: var(--text-body-xs);
  font-weight: var(--font-weight-medium);
  color: var(--color-ash);
}
.tab-btn.active .tab-count { color: var(--color-steel); }

.tab-pane { display: none; }
.tab-pane.active { display: block; }

@media (max-width: 768px) {
  /* Swipeable rail. The track bleeds to the gutter edges so a
     half-visible tab at the right edge reads as "more this way",
     and the negative margin is paid back as padding so the first
     tab still lines up with the content above it. */
  .tab-nav {
    overflow-x: auto;
    /* A flick past the last tab must not become page history. */
    overscroll-behavior-x: contain;
    scrollbar-width: none;
    flex-wrap: nowrap;
    scroll-snap-type: x proximity;
    margin-inline: calc(var(--spacing-20) * -1);
    padding-inline: var(--spacing-20);
    scroll-padding-inline: var(--spacing-20);
    /* Fade the trailing edge so the cut-off tab reads as a hint
       rather than a clipping bug. */
    -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 32px), transparent 100%);
            mask-image: linear-gradient(to right, #000 calc(100% - 32px), transparent 100%);
  }
  .tab-nav::-webkit-scrollbar { display: none; }
  .tab-nav > .tab-btn {
    flex: 0 0 auto;
    scroll-snap-align: start;
    padding-inline: var(--spacing-16);
  }
  /* Once scrolled to the end there is nothing more to hint at. */
  .tab-nav.is-scroll-end {
    -webkit-mask-image: none;
            mask-image: none;
  }
}

/* Touch floor — the tab is a 14px label in a 12px pad (41px tall);
   two more pixels of padding clear 44 without shifting the rule. */
@media (pointer: coarse) {
  .tab-btn { min-height: var(--touch-target); padding-block: var(--spacing-12); }
}

/* ============================================================
   BREADCRUMB
   Quiet Inter 13px wayfinding trail above detail pages: links in
   Steel (hover Ink), current page Ink weight 500, 16px chevron
   separators in Pebble. No backgrounds or pills (DESIGN.md →
   Breadcrumb).
   ============================================================ */
.breadcrumb {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--element-gap);
  padding: var(--spacing-24) 0;
  font-size: var(--text-body-sm);
  color: var(--color-steel);
}
.breadcrumb a { color: var(--color-steel); transition: color var(--duration-micro) ease; }
@media (hover: hover) and (pointer: fine) {
  .breadcrumb a:hover { color: var(--color-ink); text-decoration: none; }
}
.breadcrumb a:active { color: var(--color-ink); }
.breadcrumb i[data-icon] { color: var(--color-pebble); }
.breadcrumb-current { color: var(--color-ink); font-weight: var(--font-weight-medium); }

/* ============================================================
   DROPDOWN MENU
   The one menu primitive on the storefront: a Snow card on a Fog
   hairline that hangs off a trigger — the nav avatar, and the
   account nav below 1024px. Items are borderless rows that wash
   Mist on hover, the same relationship the nav links use, so a
   menu never introduces a second interaction language.

   It is not a modal. No scrim, no focus trap, no body lock: it
   closes on Escape, on a click outside, and on choosing an item.
   Sits above the mobile nav panel because the burger and the
   avatar are siblings and either can be open.
   ============================================================ */
.dropdown { position: relative; }

.dropdown-panel {
  position: absolute;
  top: calc(100% + var(--spacing-8));
  right: 0;
  z-index: calc(var(--z-mobile-menu) + 1);
  min-width: 232px;
  padding: var(--spacing-8);
  background: var(--surface-card-white);
  border: 1px solid var(--color-fog);
  border-radius: var(--radius-2xl-2);
  box-shadow: var(--shadow-modal);
  /* Kept in the box so a long menu on a short screen still reaches
     its last item, the way the mobile nav panel does. */
  max-height: calc(100dvh - var(--dev-bar-h) - var(--nav-h) - var(--spacing-24));
  overflow-y: auto;
  overscroll-behavior: contain;
  opacity: 0;
  /* A menu belongs to the control that opened it, so it grows FROM
     that control rather than from its own middle: the panel hangs off
     the trigger's right edge, so that is where the scale starts.
     0.97, never 0 — nothing in the world appears from nothing
     (DESIGN.md → Motion Philosophy, entrances). */
  transform: translateY(-4px) scale(0.97);
  transform-origin: top right;
  pointer-events: none;
  /* This rule is the state the panel RETURNS to, so it times the
     close: a menu the reader has finished with should be out of the
     way before they have looked away from it. The open rule below
     times the entrance, which is the half they watch. */
  transition: opacity 150ms ease, transform 150ms var(--ease-deck-out);
}
.dropdown-panel.open {
  opacity: 1;
  transform: none;
  pointer-events: auto;
  transition: opacity 220ms ease, transform 220ms var(--ease-deck-out);
}
.dropdown-panel--left { transform-origin: top left; }
.dropdown-panel--left { right: auto; left: 0; }

/* Identity header — who you are signed in as, never an action. */
.dropdown-head {
  padding: var(--spacing-8) var(--spacing-12) var(--spacing-12);
  border-bottom: 1px solid var(--color-fog);
  margin-bottom: var(--spacing-8);
}
.dropdown-name { font-size: var(--text-body); font-weight: var(--font-weight-semibold); color: var(--color-ink); }
.dropdown-meta {
  font-size: var(--text-body-xs);
  line-height: var(--leading-body-xs);
  color: var(--color-steel);
  margin-top: 2px;
  overflow-wrap: anywhere;
}

.dropdown-item {
  display: flex;
  align-items: center;
  gap: var(--spacing-12);
  width: 100%;
  padding: var(--spacing-8) var(--spacing-12);
  border: none;
  background: none;
  border-radius: var(--radius-controls-sm);
  font-family: var(--font-body);
  font-size: var(--text-body);
  font-weight: var(--font-weight-medium);
  color: var(--color-slate);
  text-align: left;
  text-decoration: none;
  cursor: pointer;
  transition: background var(--duration-micro) ease, color var(--duration-micro) ease;
  -webkit-user-select: none; user-select: none;
}
@media (hover: hover) and (pointer: fine) {
  .dropdown-item:hover { background: var(--color-mist); color: var(--color-ink); text-decoration: none; }
}
.dropdown-item:active { background: var(--color-fog); }
.dropdown-item:focus-visible { outline: 2px solid var(--color-race-green); outline-offset: -2px; }
.dropdown-item.active { background: var(--color-fog); color: var(--color-deep-forest); font-weight: var(--font-weight-semibold); }
/* A row's glyph, whichever way it arrived: markup writes
   `<i data-icon>` and js/icons.js fills it, a JS-built menu writes
   the `<svg class="icon">` itself. One rule has to cover both or
   the same menu is drawn two ways depending on who built it. */
.dropdown-item :is(i[data-icon], .icon) { color: var(--color-ash); flex-shrink: 0; }
.dropdown-item:active :is(i[data-icon], .icon),
.dropdown-item.active :is(i[data-icon], .icon) { color: currentColor; }
@media (hover: hover) and (pointer: fine) {
  .dropdown-item:hover :is(i[data-icon], .icon) { color: currentColor; }
}

/* The one route out of the storefront and into a back-office. It
   reads as a destination, not a menu row — Deep Forest on Mint, the
   Tertiary button relationship, with the jump icon pushed right. */
.dropdown-item--cta {
  background: var(--color-mint-ice-cream);
  color: var(--color-deep-forest);
  font-weight: var(--font-weight-semibold);
}
@media (hover: hover) and (pointer: fine) {
  .dropdown-item--cta:hover { background: var(--color-mint-ice-cream); color: var(--color-deep-forest); filter: brightness(0.97); }
}
.dropdown-item--cta:active { filter: brightness(0.94); }
.dropdown-item--cta :is(i[data-icon], .icon) { color: var(--color-race-green); margin-left: auto; }

.dropdown-sep { height: 1px; background: var(--color-fog); margin: var(--spacing-8) var(--spacing-4); }

/* Trailing count on a menu row (unread notifications, pending items). */
.dropdown-tail {
  margin-left: auto;
  font-size: var(--text-body-xs);
  font-weight: var(--font-weight-semibold);
  color: var(--color-steel);
}

/* The panel is a fixed 232px against the right gutter on desktop; on
   a phone that can overhang the viewport, so it anchors to the screen
   edges instead and takes the width that is actually there. */
@media (max-width: 480px) {
  .dropdown-panel {
    position: fixed;
    top: calc(var(--dev-bar-h) + var(--nav-h) + var(--spacing-4));
    left: var(--spacing-12);
    right: var(--spacing-12);
    min-width: 0;
    /* Pinned to the screen rather than hanging off the trigger, so
       the scale starts from the edge it is pinned to. */
    transform-origin: top center;
    /* A menu that reaches the bottom of the screen ends on the home
       indicator. */
    max-height: calc(100dvh - var(--dev-bar-h) - var(--nav-h) - var(--spacing-24) - env(safe-area-inset-bottom, 0px));
  }
}

@media (pointer: coarse) {
  .dropdown-item { min-height: var(--touch-target); }
}

@media (prefers-reduced-motion: reduce) {
  .dropdown-panel { transition: none; transform: none; }
}

/* ============================================================
   MODAL
   Snow dialog at 28px radius over a blurred Deep Forest wash.
   Centred on desktop; on mobile it becomes a bottom sheet —
   a full-width overlay card entering from below the viewport.
   The header close control appears only when the footer has no
   dismiss action (Cancel/Close) — never both.
   ============================================================ */
.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(var(--color-brand-primary-strong-rgb), 0.55);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  z-index: var(--z-modal);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--spacing-24);
}
.modal-overlay.hidden { display: none; }

/* Entrance: wash fades, dialog scales up from 0.96 (never from 0 —
   nothing appears from nothing). Occasional UI, so a standard
   200ms ease-out entrance; close is instant (exit faster than
   enter). Modals stay centre-origin — they have no trigger anchor. */
.modal-overlay:not(.hidden) { animation: overlayIn 200ms ease; }
.modal-overlay:not(.hidden) .modal-box,
.modal-overlay:not(.hidden) .modal { animation: modalIn 220ms var(--ease-deck-out); }
@keyframes overlayIn { from { opacity: 0; } }
@keyframes modalIn { from { opacity: 0; transform: scale(0.96) translateY(8px); } }
@keyframes modalSheetIn { from { transform: translateY(100%); } }

@media (prefers-reduced-motion: reduce) {
  .modal-overlay:not(.hidden),
  .modal-overlay:not(.hidden) .modal-box,
  .modal-overlay:not(.hidden) .modal { animation: none; }
}

.modal, .modal-box {
  background: var(--surface-card-white);
  border-radius: var(--radius-cards);
  max-width: 520px;
  width: 100%;
  max-height: calc(100dvh - 2 * var(--spacing-24));   /* never taller than the visible viewport — tall content scrolls */
  overscroll-behavior: contain;                       /* a dialog's own scroll never drags the page behind it */
  box-shadow: var(--shadow-modal);
  overflow-y: auto;
  overflow-x: hidden;
}

/* Wide variant — for dialogs carrying laid-out content rather than a
   short confirmation: a two-column perk grid, a bundle table. Mobile
   ignores it, where every dialog is already a full-width sheet. */
.modal-box--lg { max-width: 660px; }

/* Stacked variant — a dialog raised over another dialog, so what
   triggered it is still there underneath when it is dismissed (the
   golden ticket reveal over a purchase receipt). Its own wash sits
   on the collectible-overlay layer; nothing else changes. */
.modal-overlay--above { z-index: var(--z-deck-overlay); }

.modal-header { padding: var(--spacing-28) var(--spacing-28) 0; display: flex; align-items: flex-start; justify-content: space-between; }
.modal-title { font-family: var(--font-display); letter-spacing: var(--ls-rg); font-size: var(--text-heading-sm); font-weight: var(--font-weight-semibold); }
.modal-close {
  position: relative;
  background: none; border: none; padding: 0;
  font-size: 1.5rem; line-height: 1;
  color: var(--color-steel);
  cursor: pointer;
  transition: color var(--duration-micro) ease, transform 160ms var(--ease-deck-out);
}
@media (hover: hover) and (pointer: fine) {
  .modal-close:hover { color: var(--color-ink); }
}
.modal-close:active { transform: scale(0.9); }
.modal-close:focus-visible { outline: 2px solid var(--color-race-green); outline-offset: 2px; border-radius: var(--radius-md); }
/* The cross stays a 24px glyph aligned with the title beside it; on
   touch it gains an invisible 44px box so the dismiss is not the
   hardest thing on the dialog to hit. */
@media (pointer: coarse) {
  .modal-close::after {
    content: "";
    position: absolute;
    top: 50%; left: 50%;
    width: var(--touch-target);
    height: var(--touch-target);
    transform: translate(-50%, -50%);
  }
}
.modal-body { padding: var(--spacing-28); }
.modal-footer { padding: 0 var(--spacing-28) var(--spacing-28); display: flex; gap: var(--spacing-12); justify-content: flex-end; }

/* Mobile bottom sheet: the wash pins the dialog to the bottom
   edge and the card slides up from below (transform only), top
   corners keeping the 28px card radius. Desktop stays centred. */
@media (max-width: 768px) {
  .modal-overlay { align-items: flex-end; padding: 0; }
  .modal, .modal-box {
    margin: 0;
    width: 100%;
    max-width: none;
    border-radius: var(--radius-cards) var(--radius-cards) 0 0;
    max-height: calc(100dvh - var(--spacing-48));   /* a strip of wash always shows above the sheet */
  }
  .modal-overlay:not(.hidden) .modal-box,
  .modal-overlay:not(.hidden) .modal { animation: modalSheetIn 300ms var(--ease-deck-out); }
  @media (prefers-reduced-motion: reduce) {
    .modal-overlay:not(.hidden) .modal-box,
    .modal-overlay:not(.hidden) .modal { animation: none; }
  }
  /* A sheet sits on the bottom edge, so its last row is exactly where
     the home indicator is. Pay the inset back as padding. */
  .modal-footer,
  .modal-box.p-7 { padding-bottom: calc(var(--spacing-28) + env(safe-area-inset-bottom, 0px)); }
  /* Right-aligned footer buttons become a full-width stack — a
     confirm/cancel pair is the whole decision at this size. */
  .modal-footer { flex-direction: column-reverse; }
  .modal-footer > .btn { width: 100%; }
}

/* ============================================================
   DATA TABLES
   ============================================================ */
.table-wrap { overflow-x: auto; overscroll-behavior-x: contain; border-radius: var(--radius-cards-compact); border: 1px solid var(--color-fog); background: var(--surface-card-white); }

.data-table { width: 100%; border-collapse: collapse; font-size: var(--text-body); }
.data-table th {
  background: var(--surface-card-muted);
  padding: var(--spacing-12) var(--spacing-20);
  text-align: left;
  font-size: var(--text-body-xs);
  font-weight: var(--font-weight-semibold);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--color-steel);
  border-bottom: 1px solid var(--color-fog);
  white-space: nowrap;
}
.data-table td {
  padding: var(--spacing-16) var(--spacing-20);
  border-bottom: 1px solid var(--color-fog);
  color: var(--color-ink);
  vertical-align: middle;
}
.data-table tr:last-child td { border-bottom: none; }
@media (hover: hover) and (pointer: fine) {
  .data-table tbody tr:hover { background: var(--color-mist); }
}

/* Lead cell — a thumbnail beside the row's name. The name is the
   only cell that carries weight; every other column stays regular so
   the eye reads down a column, not across a wall of bold. */
.cell-lead { display: flex; align-items: center; gap: var(--spacing-12); min-width: 0; }
.cell-title { font-weight: var(--font-weight-semibold); color: var(--color-ink); min-width: 0; }
/* The row's action column: shrink to the control and keep it right,
   so a table of mixed-length names still has one action edge. */
.cell-actions { width: 1%; white-space: nowrap; text-align: right; }
/* An inline fill bar with its own figure beside it. */
.cell-fill { display: flex; align-items: center; gap: var(--spacing-8); min-width: 120px; }
.cell-fill .progress { flex: 1; }

@media (max-width: 768px) {
  /* A table is the one component that genuinely cannot reflow: its
     columns mean something in relation to each other. So it keeps its
     shape and scrolls, rather than collapsing into stacked key/value
     pairs that lose the comparison the table exists to make. */
  .table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; overscroll-behavior-x: contain; }
  table { min-width: 560px; }
  .data-table th { padding: var(--spacing-8) var(--spacing-12); }
  .data-table td { padding: var(--spacing-12); }
}

/* ============================================================
   PAGINATION
   Centred pill row: 36px circular page controls on Snow with
   Pebble borders; the current page fills Deep Forest with Mint
   text; prev/next are chevron icon controls; ellipsis plain Ash
   (DESIGN.md → Pagination). Flat — no shadows, no Ember.
   ============================================================ */
.pagination { display: flex; align-items: center; justify-content: center; gap: var(--element-gap); }
.page-btn {
  min-width: 36px;
  height: 36px;
  padding: 0 var(--spacing-8);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-pill);
  border: 1px solid var(--color-pebble);
  background: var(--color-snow);
  color: var(--color-steel);
  font-family: var(--font-body);
  font-size: var(--text-body-sm);
  font-weight: var(--font-weight-medium);
  cursor: pointer;
  text-decoration: none;
  transition: border-color var(--duration-micro) ease, color var(--duration-micro) ease, transform 160ms var(--ease-deck-out);
}
@media (hover: hover) and (pointer: fine) {
  .page-btn:hover:not(:disabled) { border-color: var(--color-graphite); color: var(--color-ink); text-decoration: none; transform: translateY(-1px); }
}
.page-btn:active:not(:disabled) { transform: scale(0.95); }
.page-btn:focus-visible { outline: 2px solid var(--color-race-green); outline-offset: 2px; }
.page-btn.active { background: var(--color-deep-forest); border-color: var(--color-deep-forest); color: var(--color-mint-ice-cream); font-weight: var(--font-weight-semibold); }
.page-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.page-ellipsis { color: var(--color-ash); padding: 0 var(--spacing-4); }

@media (pointer: coarse) {
  .page-btn { min-width: var(--touch-target); height: var(--touch-target); }
}

/* A long page row cannot shrink below one control per page number, so
   on a compact phone it scrolls sideways rather than wrapping into a
   block of circles. Centred until it overflows, then scrollable. */
@media (max-width: 480px) {
  .pagination {
    justify-content: flex-start;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
    padding-bottom: var(--spacing-4);
  }
  .pagination::-webkit-scrollbar { display: none; }
  .pagination > * { flex: 0 0 auto; }
}

/* ============================================================
   STAT NUMBER BLOCK
   Borderless raw typographic stat: Switzer numeral + steel label.
   ============================================================ */
.stat-card { background: transparent; border: none; border-radius: 0; padding: 0; }
.stat-card-icon { font-size: var(--text-heading-sm); margin-bottom: var(--spacing-8); }
.stat-value {
  font-family: var(--font-display); letter-spacing: var(--ls-heading-lg);
  font-size: var(--text-heading-lg);
  font-weight: var(--font-weight-bold);
  color: var(--color-race-green);
  line-height: 1;
  margin-bottom: var(--spacing-4);
}
.stat-label { font-size: var(--text-body-sm); line-height: 1.56; color: var(--color-steel); font-weight: var(--font-weight-regular); margin-bottom: var(--spacing-8); }
.stat-change { font-size: var(--text-body-xs); display: flex; align-items: center; gap: 4px; }
.stat-change.up { color: var(--success); }
.stat-change.down { color: var(--danger); }

/* Boxed variant — a KPI row where the stats read as objects sitting
   beside each other rather than as raw numerals set into the page.
   Snow surface at the compact card radius, with a Fog chip holding
   the outline glyph that names the measure. Everything else is
   inherited from .stat-card, so the two can never drift apart. The
   numeral steps down one role because it now sits inside padding. */
.stat-card--boxed {
  display: flex;
  flex-direction: column;
  background: var(--surface-card-white);
  border-radius: var(--radius-cards-compact);
  padding: var(--card-padding-compact);
}
.stat-card--boxed .stat-card-icon {
  width: 36px; height: 36px;
  margin-bottom: var(--spacing-16);
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0;
  border-radius: var(--radius-pill);
  background: var(--color-fog);
  color: var(--color-race-green);
  font-size: inherit;
}
/* Two steps down from the raw variant: boxed, the numeral sits
   inside padding and beside a glyph, and it must not tie with the
   page title above it — four stats reading as loud as the page's
   own name flattens the hierarchy. */
.stat-card--boxed .stat-value { font-size: var(--text-heading-md); line-height: var(--leading-heading-md); }
/* Pinned to the bottom so a missing change line never shortens one
   card in the row. */
.stat-card--boxed .stat-change { margin-top: auto; }

.stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--spacing-24); margin-bottom: var(--spacing-32); }
.stats-grid-3 { grid-template-columns: repeat(3, 1fr); }
.stats-grid > * { min-width: 0; }

@media (max-width: 1024px) { .stats-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 768px) {
  .stats-grid, .stats-grid-3 { grid-template-columns: 1fr 1fr; gap: var(--spacing-12); }
  .stat-card--boxed { padding: var(--spacing-16); }
  .stat-card--boxed .stat-card-icon { width: 32px; height: 32px; margin-bottom: var(--spacing-12); }
  .stat-card--boxed .stat-value { font-size: var(--text-heading-sm); }
}

/* ============================================================
   MARQUEE RAIL
   No panel — sits directly on the page canvas like the category
   rail. Full-bleed row, hard-clipped at the viewport edge (same
   break-out trick as .category-rail). Prize tiles scroll in a
   slow, continuous loop: the caller repeats the list until each
   half of the track covers the screen, and the track slides
   translateX(-50%) so the loop point is seamless. Transform-only
   motion; pauses on hover and freezes under prefers-reduced-motion
   (base.css kill-switch).

   The rail sizes whatever it carries — the WIDTH only, so a tile
   keeps its own aspect ratio and a draw in this rail is the same
   shape as the same draw in the featured grid.

   IT ONLY MOVES WHEN IT HAS ENOUGH TO MOVE. How many draws are
   closing this week is nobody's decision, so a short list gets
   `is-static` from the caller: drawn once, centred, standing
   still, wrapping rather than running off the side. A loop with
   a visible hole in it is worse than a row that never moved.
   ============================================================ */
.marquee-rail {
  overflow: hidden;
  padding-bottom: var(--spacing-12);
  /* Break out of .container to bleed edge-to-edge */
  width: 100vw;
  margin-left: calc(50% - 50vw);
}
.marquee-rail-track {
  display: flex;
  width: max-content;
  gap: var(--spacing-24);
  animation: marqueeRail 36s linear infinite;
}
/* Pausing on hover is a courtesy to a cursor reading a passing tile.
   A touch device has no hover to pause with, and latching one would
   stop the rail for good on the first tap. */
@media (hover: hover) and (pointer: fine) {
  .marquee-rail:hover .marquee-rail-track { animation-play-state: paused; }
}
@keyframes marqueeRail { from { transform: translateX(0); } to { transform: translateX(-50%); } }

/* THE RAIL SETS THE WIDTH AND NOTHING ELSE. The tile keeps its own
   4/5, so a draw in this rail is the same shape as the same draw in
   the featured grid — one prize tile, one silhouette, wherever the
   page puts it. A marquee-only ratio was a second tile shape to keep
   in step, and it drifted. */
.marquee-rail-track > * { flex: 0 0 340px; }

/* Too few to loop: the row stands still, centred in the page and
   wrapping if the screen is narrower than what it is holding. */
.marquee-rail.is-static { overflow: visible; }
.marquee-rail.is-static .marquee-rail-track {
  width: auto;
  animation: none;
  justify-content: center;
  flex-wrap: wrap;
  padding: 0 var(--spacing-24);
}

/* A section whose rail has nothing in it goes, heading and all —
   the same instinct as a draw with no specifications rendering no
   specifications block. Set by the render, never by the record's
   own live switch, which owns `.hidden`. */
.section-empty { display: none; }

@media (max-width: 768px) {
  .marquee-rail-track > * { flex-basis: 260px; }
}

/* ============================================================
   HOW-IT-WORKS STEPS
   Full-bleed Deep Forest section canvas. Cards are the
   dark-surface Glass Card (DESIGN.md → Glass Card):
   rgba(255,255,255,.06) fill, rgba(var(--color-brand-highlight-rgb), .18) border,
   blur, Mint Ice Cream text, Blush Pink icon — so cards read as
   a step up off the panel without a solid colour fill. Icon
   sits bare on the card — no chip, no decoration, just colour.
   Title borrows the hero banner's two-font treatment (Switzer
   Light base + Bold Italic .hatton accent) at H2 size, per
   DESIGN.md's headline-accent rule.
   ============================================================ */

.steps-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--spacing-24); }
.step-item {
  text-align: left;
  padding: var(--card-padding-compact);
  background: var(--glass-fill-dark);
  border: 1px solid var(--deck-glass-border);
  border-radius: var(--radius-cards-compact);
  -webkit-backdrop-filter: blur(var(--glass-blur));
  backdrop-filter: blur(var(--glass-blur));
}
.step-icon { margin-bottom: var(--spacing-16); color: var(--color-blush-pink); }
.step-title {
  font-family: var(--font-display); letter-spacing: var(--ls-rg);
  /* heading-md role (24px); base stays Switzer Light for the .hatton pairing */
  font-size: var(--text-heading-md);
  font-weight: var(--font-weight-light);
  line-height: 1.1;
  margin-bottom: var(--spacing-8);
  color: var(--color-mint-ice-cream);
}
.step-desc { font-size: var(--text-body-sm); line-height: var(--leading-body-sm); color: rgba(var(--color-brand-highlight-rgb), 0.7); }

@media (max-width: 768px) {
  .steps-grid { grid-template-columns: 1fr; gap: var(--spacing-16); }
}

/* ── Step indicator (multi-step forms) ─────────────────────── */
.step-indicator { display: flex; align-items: center; gap: 0; margin-bottom: var(--spacing-32); }
/* Edge spacers (flex 1) + double-weight connectors (flex 2) place each
   dot at the centre of an equal column, so the flex-1 .step-label-item
   below sits exactly beneath its dot at any step count. */
.step-indicator::before, .step-indicator::after { content: ''; flex: 1; }
.step-indicator:has(+ .step-labels) { margin-bottom: 0; }
.step-dot {
  width: 32px; height: 32px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--color-pebble);
  background: var(--color-snow);
  display: flex; align-items: center; justify-content: center;
  font-size: var(--text-body-sm);
  font-weight: var(--font-weight-bold);
  color: var(--color-steel);
  position: relative;
  z-index: 1;
}
.step-dot.active { border-color: var(--color-deep-forest); color: var(--color-snow); background: var(--color-deep-forest); }
.step-dot.done { border-color: var(--success); background: var(--success); color: var(--color-snow); }
.step-connector { flex: 2; height: 2px; background: var(--color-fog); }
.step-connector.done { background: var(--success); }
.step-labels { display: flex; justify-content: space-between; margin-top: var(--spacing-12); }
.step-label-item { font-size: var(--text-body-xs); color: var(--color-steel); text-align: center; flex: 1; }
.step-label-item.active { color: var(--color-deep-forest); font-weight: var(--font-weight-semibold); }
.step-label-item.done { color: var(--success); }

/* ============================================================
   TICKET SELECTOR
   ============================================================ */
.ticket-selector { display: flex; align-items: center; gap: var(--spacing-16); }
.ticket-qty-btn {
  width: 40px; height: 40px;
  /* Load-bearing: a fixed width still SHRINKS in a flex row, so the
     44px the touch rule below asks for was being squeezed back to 33
     by the readout beside it — a control that measures 44 in the
     stylesheet and 33 on the screen. The same guard .form-check's
     box carries, for the same reason. */
  flex-shrink: 0;
  border-radius: var(--radius-pill);
  border: 1px solid var(--color-pebble);
  background: var(--color-snow);
  font-size: var(--text-heading-sm);
  cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  color: var(--color-ink);
  transition: border-color var(--duration-micro) ease, transform 160ms var(--ease-deck-out);
}
@media (hover: hover) and (pointer: fine) {
  .ticket-qty-btn:hover { border-color: var(--color-deep-forest); }
}
.ticket-qty-btn:active { transform: scale(0.94); }
.ticket-qty-btn:focus-visible { outline: 2px solid var(--color-race-green); outline-offset: 2px; }
.ticket-qty { font-family: var(--font-display); letter-spacing: var(--ls-rg); font-size: var(--text-heading-sm); font-weight: var(--font-weight-bold); min-width: 40px; text-align: center; }
.ticket-price-calc { font-size: var(--text-body); color: var(--color-steel); }

@media (pointer: coarse) {
  .ticket-qty-btn { width: var(--touch-target); height: var(--touch-target); }
}

/* ============================================================
   QUANTITY STEPPER
   Compact − [field] + control sized to the button height (40px). The
   field is read-only until tapped (then you can type); otherwise the
   − / + buttons are the primary editors. Used on deck purchase cards:
   inline with a Buy button (.deck-buy-row) on premium tiles, or on its
   own in each pack row feeding a single global Buy (.deck-buy-bar).
   ============================================================ */
.qty-stepper {
  display: inline-flex;
  align-items: stretch;
  height: 40px;                              /* == .btn height (12+12 pad + 14 line + 2 border) */
  border: 1px solid var(--color-pebble);
  border-radius: var(--radius-buttons);
  background: var(--color-snow);
  overflow: hidden;                          /* clip the end buttons to the pill */
  flex: none;
}
.qty-stepper__btn {
  width: 34px;
  border: none;
  background: none;
  padding: 0;
  font-size: var(--text-heading-sm);
  line-height: 1;
  color: var(--color-ink);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background var(--duration-micro) ease, transform 160ms var(--ease-deck-out);
}
@media (hover: hover) and (pointer: fine) {
  .qty-stepper__btn:hover:not(:disabled) { background: var(--color-mist); }
}
.qty-stepper__btn:active { transform: scale(0.86); }
.qty-stepper__btn:focus-visible { outline: 2px solid var(--color-race-green); outline-offset: -2px; }
.qty-stepper__btn:disabled { opacity: 0.32; cursor: not-allowed; }
.qty-stepper__input {
  width: 42px;
  border: none;
  background: none;
  padding: 0;
  text-align: center;
  font-family: var(--font-display); letter-spacing: var(--ls-rg);
  font-size: var(--text-body-lg);
  font-weight: var(--font-weight-bold);
  color: var(--color-ink);
  font-variant-numeric: tabular-nums;
  cursor: pointer;                           /* read-only: signals "tap to edit" */
}
.qty-stepper__input:not([readonly]) { cursor: text; box-shadow: inset 0 -2px 0 var(--color-race-green); }
.qty-stepper__input:focus { outline: none; }

/* Buy row — stepper inline with the Buy CTA (premium deck tiles). The
   Buy button fills the remaining width; both are the same height. */
.deck-buy-row { display: flex; align-items: center; gap: var(--spacing-8); }
.deck-buy-row .btn { flex: 1; }
.deck-actions .deck-buy-row { flex: 1; }

/* Buy bar — one global Buy beneath a list of pack rows, each of which
   carries its own stepper. The button reflects the summed selection. */
.deck-buy-bar { margin-top: var(--spacing-12); }
.deck-buy-bar .btn { width: 100%; }

/* Pack-row stepper sits under the price line, right-aligned. */
.pack-row-qty { display: flex; justify-content: flex-end; margin-top: var(--spacing-8); }

/* Touch — the − / + are the primary editors of a purchase quantity,
   so they get the full floor. The control grows as one piece: the
   shell to 44, both ends to 44 wide, and the field with it, keeping
   the pill proportions rather than stretching one part of it. */
@media (pointer: coarse) {
  /* 46, not 44: the shell's 1px border top and bottom is outside the
     buttons, so a 44px shell leaves them 42 and the actual target
     falls short of the floor. */
  .qty-stepper { height: 46px; }
  .qty-stepper__btn { width: var(--touch-target); }
  .qty-stepper__input { width: 48px; }
}

/* Below 480px a stepper sitting inline with a Buy button leaves the
   button too narrow to hold its label; stack them instead. */
@media (max-width: 480px) {
  .deck-buy-row { flex-wrap: wrap; }
  .deck-buy-row .qty-stepper { flex: 1 0 auto; }
  .deck-buy-row .btn { flex: 1 0 100%; }
}

/* ============================================================
   TICKET OPTION (direct-ticket entry)
   The direct-purchase counterpart to the Pack Row: the row a
   buyer picks when tickets are sold straight, with no deck around
   them. A pack row leads with card art because the art IS the
   product; here the QUANTITY is, so a ticket mark and the count lead
   instead —
   Switzer bold against a lighter unit word (DESIGN.md's inline
   tonal contrast), with a bundle's free tickets stamped beside it
   as a Shine badge. That badge carries the brand note the old art
   stage used to, at a fraction of the weight, and keeps the free
   tickets — the reason to take a bundle — in the title rather than
   below it, with the collectible deck's light play to mark them as
   the reward they are. That badge leads with "+" and carries no icon
   of its own: the row already has its ticket mark, and the badge is
   an addition to the count beside it, not a second count.

   Shares the Pack Row's 14px Snow shell, Mist hover, Deep Forest
   best-value border and per-row-stepper-into-one-global-Buy
   contract, so a draw page reads the same whichever way the
   jurisdiction sells entries. The Ember value flag pins to the
   top-right corner (a pack row pins its flag to the art stage;
   without a stage the row's own corner is the anchor).
   ============================================================ */
.ticket-option {
  position: relative;
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 420px;
  text-align: left;
  color: inherit;
  background: var(--color-snow);
  border: 1px solid var(--color-pebble);
  border-radius: var(--radius-inputs);
  padding: var(--spacing-16);
  transition: background-color var(--duration-micro) ease, border-color var(--duration-micro) ease;
}
@media (hover: hover) and (pointer: fine) {
  .ticket-option:hover { background: var(--color-mist); }
}
.ticket-option.is-flagged { border-color: var(--color-deep-forest); }

/* ── Head — the quantity, and what a bundle adds to it ──────── */
.ticket-option-head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--element-gap);
}
.ticket-option-name {
  margin: 0;
  font-family: var(--font-display); letter-spacing: var(--ls-heading-sm);
  font-size: var(--text-heading-sm);
  line-height: 1.15;
  font-weight: var(--font-weight-semibold);   /* one weight across the line */
  color: var(--color-obsidian);
}
.ticket-option-count { font-variant-numeric: tabular-nums; }
.ticket-option-head .badge { flex: 0 0 auto; }

/* Leading ticket mark — inside the heading so it can never wrap away
   from the count it belongs to. Optically aligned to the cap height of
   the display face rather than the baseline. */
.ticket-option-icon {
  display: inline-flex;
  vertical-align: -0.14em;
  margin-right: var(--spacing-8);
  color: var(--color-race-green);
}
.ticket-option-icon .icon { width: 20px; height: 20px; }

/* Value flag — pinned to the row's top-right corner. It is out of
   flow, so a flagged head reserves the width back. */
.ticket-option-flag { position: absolute; top: 10px; right: 10px; z-index: 2; pointer-events: none; }
.ticket-option-flag .badge { font-size: var(--text-caption); padding: 3px 7px; }
.ticket-option.is-flagged .ticket-option-head { padding-right: 76px; }

.ticket-option-desc { margin: 4px 0 0; font-size: var(--text-body-xs); color: var(--color-steel); }

/* ── Price line — cost and the stepper ──────────────────────── */
.ticket-option-priceline {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--element-gap);
  padding-top: var(--spacing-12);
}
.ticket-option-price {
  font-family: var(--font-display); letter-spacing: var(--ls-subheading);
  font-size: var(--text-subheading);
  font-weight: var(--font-weight-bold);
  color: var(--color-obsidian);
}
.ticket-option-qty { margin-left: auto; }

/* Freeroll variant — the host's free entry. No stepper (it is
   claimed, not counted), so the Claim action takes its own
   full-width row beneath the price. */
.ticket-option--freeroll .ticket-option-qty {
  flex: 1 0 100%;
  margin-left: 0;
  margin-top: var(--spacing-4);
}
.ticket-option--freeroll .ticket-option-qty .btn { width: 100%; }

/* One global Buy beneath the list, reflecting the summed selection. */
.entry-buy-bar { margin-top: var(--spacing-12); }
.entry-buy-bar .btn { width: 100%; }

/* Narrow rows — the quantity title, its free-tickets badge and the
   corner value flag all compete for one line. Below 480px the flag's
   reserved 76px is most of the row, so the head gives up the reserve
   and the badge drops beneath the title instead of squeezing it. */
@media (max-width: 480px) {
  .ticket-option { max-width: none; padding: var(--spacing-12); }
  .ticket-option.is-flagged .ticket-option-head { padding-right: 0; padding-top: var(--spacing-20); }
  .ticket-option-flag { top: 8px; right: 8px; }
  .ticket-option-priceline { padding-top: var(--spacing-8); }
}

@media (prefers-reduced-motion: reduce) {
  .ticket-option { transition: none; }
}

/* ============================================================
   DRAW ACTION BAR (mobile purchase anchor)
   A draw page is two columns on desktop: the buyer reads the
   gallery and tabs on the left while the purchase rail stays in
   view on the right. Below 1024px that rail unstacks BELOW the
   tab content (base.css → .layout-split), so on a phone the
   decks/tickets sit thousands of pixels down the page, past six
   tabs of copy. The buyer loses the thing they came to do.

   This bar puts it back in the thumb zone: a fixed strip at the
   bottom of the viewport carrying the cheapest way in and a
   button that jumps to the purchase card. It is an anchor, not a
   second checkout — nothing is bought here, so it never competes
   with the real rail and never has to mirror its state.

   It exists ONLY where the rail has unstacked (≤1024px), and
   hides itself again whenever the purchase card is on screen —
   offering a jump to something already in view is noise. Both
   draw pages carry it, labelled for what that jurisdiction
   actually sells: "Buy decks" (cards) / "Buy tickets" (direct).
   ============================================================ */
.draw-cta-bar {
  display: none;                             /* desktop: the rail is already in view */
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: var(--z-action-bar);
  align-items: center;
  gap: var(--spacing-16);
  padding: var(--spacing-12) var(--spacing-20);
  /* Sit clear of the iOS home indicator / Android gesture bar. */
  padding-bottom: calc(var(--spacing-12) + env(safe-area-inset-bottom, 0px));
  background: rgba(255, 255, 255, 0.92);
  border-top: 1px solid var(--color-fog);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
}
.draw-cta-bar-copy { min-width: 0; }
.draw-cta-bar-label {
  font-size: var(--text-body-xs);
  line-height: 1;
  color: var(--color-steel);
}
.draw-cta-bar-value {
  display: block;
  margin-top: 3px;
  font-family: var(--font-display); letter-spacing: var(--ls-subheading);
  font-size: var(--text-subheading);
  font-weight: var(--font-weight-bold);
  line-height: 1;
  color: var(--color-obsidian);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.draw-cta-bar .btn { margin-left: auto; flex-shrink: 0; }

@media (max-width: 1024px) {
  /* Slides away rather than blinking out — transform/opacity only. */
  .draw-cta-bar {
    display: flex;
    transform: translateY(0);
    opacity: 1;
    transition: transform var(--duration-micro) var(--ease-deck-out),
                opacity var(--duration-micro) ease;
  }
  .draw-cta-bar.is-hidden {
    transform: translateY(110%);
    opacity: 0;
    pointer-events: none;
  }
  /* Reserve the strip's height so the last card and the footer are
     never pinned under it. */
  body.has-draw-cta-bar { padding-bottom: calc(72px + env(safe-area-inset-bottom, 0px)); }
}

@media (prefers-reduced-motion: reduce) {
  .draw-cta-bar { transition: none; }
}

/* The anchor target clears the fixed dev bar + nav so the purchase
   card lands under the chrome, not behind it. */
.draw-buy-anchor { scroll-margin-top: calc(var(--dev-bar-h) + var(--nav-h) + var(--spacing-16)); }

/* Gallery specimen only — drops out of fixed position so the bar can
   be inspected in the component library at any width. Never on a page. */
.draw-cta-bar--static {
  display: flex;
  position: static;
  border: 1px solid var(--color-fog);
  border-radius: var(--radius-cards-compact);
  padding-bottom: var(--spacing-12);
}

/* ============================================================
   ENTRY TALLY
   The live readout of what the current selection buys, pinned
   above the Buy bar on a direct-ticket purchase card.

   Mint Ice Cream fill with Race Green ink — the same pairing the
   free-tickets Shine badge uses on the rows above it, so the tile
   reads as the sum of those badges rather than as a separate dark
   panel interrupting the column of Snow cards.

   Three lines, in the order a buyer asks them:
     1. the total they end up with,
     2. how it splits — paid tickets + free tickets, because a
        bundle's free tickets are the reason to take the bundle and
        must never look like part of what was charged for. Both
        halves are always stated, zeroed before anything is picked,
        so the line never changes shape as the buyer taps,
     3. the draw date, always, so the tile answers "when" without
        being read against the rail.
   Capped draws append their live odds to the split line ("1 in
   17,900"); uncapped draws have no denominator to quote, so the
   line simply ends. The split line is also where the over-limit
   warning lands, since it is the line the buyer must act on.
   ============================================================ */
.entry-tally {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--spacing-8) var(--spacing-12);
  padding: var(--spacing-12) var(--spacing-16);
  border-radius: var(--radius-inputs);
  background: var(--color-mint-ice-cream);
  border: 1px solid var(--color-mint-ice-cream);
}
.entry-tally-label {
  font-size: var(--text-body-xs);
  font-weight: var(--font-weight-semibold);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-race-green);
  opacity: 0.72;
}
.entry-tally-value {
  font-family: var(--font-display); letter-spacing: var(--ls-heading-sm);
  font-size: var(--text-heading-sm);
  line-height: 1;
  font-weight: var(--font-weight-bold);
  color: var(--color-race-green);
  font-variant-numeric: tabular-nums;
  margin-left: auto;
}

/* Split — paid and free stated separately, joined by the same "+"
   the free-tickets badge leads with. The counts carry the weight;
   the unit words stay light, so the line scans as numbers. */
.entry-tally-split {
  flex: 1 0 100%;
  font-size: var(--text-body-sm);
  line-height: var(--leading-body-sm);
  color: var(--color-race-green);
  opacity: 0.86;
  font-variant-numeric: tabular-nums;
}
.entry-tally-split b { font-weight: var(--font-weight-semibold); }
.entry-tally-split .entry-tally-plus {
  padding: 0 var(--spacing-4);
  font-weight: var(--font-weight-semibold);
}
/* Over the per-player limit: the one state the buyer has to fix. The
   functional danger red, not Ember — Ember is a badge accent (DESIGN.md)
   and at 2.7:1 on this mint fill it would not carry body text anyway. */
.entry-tally-split.is-over { color: var(--danger); opacity: 1; font-weight: var(--font-weight-medium); }

/* Draw date — the constant line, hairline-separated from the split
   above it so it reads as a footnote rather than another figure. */
.entry-tally-note {
  flex: 1 0 100%;
  padding-top: var(--spacing-8);
  border-top: 1px solid rgba(var(--color-brand-primary-rgb), 0.14);
  font-size: var(--text-body-xs);
  line-height: var(--leading-body-xs);
  color: var(--color-race-green);
  opacity: 0.66;
}

/* Nothing picked yet: the total and the split are both real zeros, so
   they dim to read as a placeholder rather than as a figure. The line
   keeps its shape either way — only the numbers move. */
.entry-tally.is-empty .entry-tally-value { opacity: 0.4; }
.entry-tally.is-empty .entry-tally-split { opacity: 0.45; }

/* The label and the count share a baseline row until the tile runs
   narrow, at which point the uppercase label wraps mid-word rather
   than yielding. Below 480px the count takes its own line under the
   label so both stay whole. */
@media (max-width: 480px) {
  .entry-tally { padding: var(--spacing-12); }
  .entry-tally-value { margin-left: 0; flex: 1 0 100%; }
}

/* ============================================================
   ENTRY PERKS
   How a buyer gets into a direct-ticket draw, one Muted Surface
   Card per route: paid tickets, bundles, freeroll, instant win.
   Muted rather than Snow because these are the draw's mechanics —
   the surface DESIGN.md reserves for rules and eligibility notes —
   and because the page's Snow cards are the things you act on.
   The icon chip inverts to Snow to step up off the Fog fill.

   Two columns, and an odd last card takes the full width so the
   grid always closes rather than trailing a gap. A draw only shows
   the routes its host actually switched on, so the count varies
   between two and four.
   ============================================================ */
.entry-perks {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--spacing-16);
}
.entry-perks > .entry-perk:last-child:nth-child(odd) { grid-column: 1 / -1; }

.entry-perk {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--spacing-12);
  padding: var(--card-padding-compact);
  border-radius: var(--radius-cards-compact);
  background: var(--surface-card-muted);
}
.entry-perk-icon {
  flex: 0 0 auto;
  width: 36px; height: 36px;
  display: grid; place-items: center;
  border-radius: var(--radius-controls-sm);
  background: var(--color-snow);
  color: var(--color-race-green);
}
.entry-perk-title { font-size: var(--text-body-md); font-weight: var(--font-weight-semibold); color: var(--color-ink); }
.entry-perk-desc { margin-top: 4px; font-size: var(--text-body-sm); line-height: var(--leading-body-sm); color: var(--color-steel); }

@media (max-width: 600px) {
  .entry-perks { grid-template-columns: 1fr; gap: var(--spacing-12); }
}

/* ============================================================
   INTEREST PICKER
   "What are you interested in?" — the one question the platform
   asks at sign-up, and the record behind the Users section's
   interest filter (js/users-data.js).

   IT IS A GRID OF THE PLATFORM'S OWN CATEGORIES, not a list of
   checkboxes. Somebody is picking between Watches and Yachts, and
   that is a choice made by recognising a thing rather than by
   reading a label beside a box — so each option is a tile the
   size of a target, and the whole tile is the control.

   FITTED TO WHAT IS THERE. `auto-fit` rather than a fixed column
   count, because how many categories the platform runs is the
   operator's and the eleventh one must not land in a row of its
   own. Same rule as the certification wall.

   THE SELECTED STATE IS THE SURFACE, NOT A TICK IN A CORNER.
   Deep Forest fill with Mint Ice Cream ink — the platform's own
   primary pair — so a glance across the grid reads as a shape
   rather than as eleven small marks to check one at a time. The
   tick is added because colour alone is not a state.
   ============================================================ */
.interest-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));
  gap: var(--spacing-12);
}

.interest-tile {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--spacing-8);
  min-height: 92px;
  padding: var(--spacing-16);
  text-align: left;
  border: 1px solid var(--color-pebble);
  border-radius: var(--radius-cards-compact);
  background: var(--surface-card-white);
  color: var(--color-ink);
  cursor: pointer;
  /* The press takes the press band, not the hover's: a transform
     eased over 250ms on a symmetric curve lags the thumb that
     started it (DESIGN.md → Motion Philosophy, 120–160ms on the
     strong ease-out). Everything else here stays at micro speed. */
  transition: border-color var(--duration-micro) ease,
              background var(--duration-micro) ease,
              color var(--duration-micro) ease,
              transform 160ms var(--ease-deck-out);
}
@media (hover: hover) and (pointer: fine) {
  .interest-tile:hover { border-color: var(--color-graphite); transform: translateY(-2px); }
}
.interest-tile:focus-visible { outline: 2px solid var(--color-deep-forest); outline-offset: 2px; }

.interest-tile-icon { color: var(--color-slate); transition: color var(--duration-micro) ease; }
.interest-tile-name {
  font-size: var(--text-body-md);
  font-weight: var(--font-weight-semibold);
  line-height: 1.25;
}

/* Chosen: the platform's primary pair, and a tick in the corner
   so the state does not rest on colour alone. */
.interest-tile[aria-pressed="true"] {
  background: var(--color-deep-forest);
  border-color: var(--color-deep-forest);
  color: var(--color-mint-ice-cream);
}
.interest-tile[aria-pressed="true"] .interest-tile-icon { color: var(--color-blush-pink); }
@media (hover: hover) and (pointer: fine) {
  .interest-tile[aria-pressed="true"]:hover { border-color: var(--color-race-green); }
}

.interest-tile-check {
  position: absolute;
  top: var(--spacing-12); right: var(--spacing-12);
  width: 20px; height: 20px;
  display: grid; place-items: center;
  border-radius: var(--radius-full);
  background: var(--color-mint-ice-cream);
  color: var(--color-deep-forest);
  opacity: 0;
  transform: scale(0.6);
  transition: opacity var(--duration-micro) ease, transform var(--duration-micro) ease;
}
.interest-tile[aria-pressed="true"] .interest-tile-check { opacity: 1; transform: none; }
.interest-tile-check .icon { width: 13px; height: 13px; }

/* ── The readback ────────────────────────────────────────
   Wherever the interests are READ rather than set — the account's
   own preferences panel, and the console's user card — they are a
   run of chips holding the categories that account actually
   holds. Never the whole grid: a panel that listed every category
   the platform runs in order to say that three are set grows with
   the catalogue and tells the reader nothing.

   It is the tile's own language at a size that can sit in a row:
   the same glyph, the same face, on the Fog surface the design
   system keeps for a quiet object on Snow. */
.interest-chips { display: flex; flex-wrap: wrap; gap: var(--spacing-8); }
.interest-chip {
  display: inline-flex; align-items: center; gap: var(--spacing-8);
  padding: 6px 14px 6px 12px;
  border-radius: var(--radius-pill);
  background: var(--surface-card-muted);
  color: var(--color-ink);
  font-size: var(--text-body-sm);
  font-weight: var(--font-weight-medium);
  line-height: 1.4;
}
.interest-chip .icon { width: 16px; height: 16px; color: var(--color-slate); flex-shrink: 0; }

/* How many are picked, under the grid. It is a reading of the
   grid rather than a rule about it — nothing here has a minimum,
   because a sign-up step that refuses to let somebody past is a
   sign-up step people abandon. */
.interest-count {
  margin-top: var(--spacing-16);
  font-size: var(--text-body-sm);
  color: var(--color-steel);
}

@media (max-width: 480px) {
  .interest-grid { grid-template-columns: repeat(auto-fit, minmax(118px, 1fr)); gap: var(--spacing-8); }
  .interest-tile { min-height: 84px; padding: var(--spacing-12); }
}
@media (pointer: coarse) {
  .interest-tile { min-height: 96px; }
}
@media (prefers-reduced-motion: reduce) {
  .interest-tile, .interest-tile-check { transition: none; }
  .interest-tile:hover { transform: none; }
}

/* ============================================================
   ALERTS (functional status — muted to fit the neutral system)
   ============================================================ */
.alert {
  padding: var(--spacing-16) var(--spacing-20);
  border-radius: var(--radius-inputs);
  border: 1px solid;
  font-size: var(--text-body);
  display: flex;
  align-items: flex-start;
  gap: var(--spacing-12);
}
.alert-icon { font-size: var(--text-body-lg); flex-shrink: 0; margin-top: 1px; }
.alert-success { background: #f0f6f2; border-color: #cfe2d6; color: #2d5e45; }
.alert-warning { background: #faf5ec; border-color: #e8d9bc; color: #7a5618; }
.alert-danger { background: #faf0f0; border-color: #e8c9c9; color: #7a2929; }
.alert-info { background: #eef2f6; border-color: #cdd9e4; color: #2e4d68; }

/* ============================================================
   CHARTS (CSS bar charts)
   ============================================================ */
.bar-chart { display: flex; align-items: flex-end; gap: var(--spacing-12); height: 200px; padding-bottom: var(--spacing-24); position: relative; }
.bar-chart::after { content: ''; position: absolute; bottom: var(--spacing-24); left: 0; right: 0; height: 1px; background: var(--color-fog); }
.bar-col { display: flex; flex-direction: column; align-items: center; justify-content: flex-end; flex: 1; height: 100%; }
.bar-fill { width: 100%; background: var(--color-graphite); border-radius: var(--radius-md) var(--radius-md) 0 0; transition: height 0.5s var(--ease-deck-soft); min-height: 4px; }
.bar-fill.blue { background: var(--color-slate); }
.bar-value { font-size: var(--text-body-xs); font-weight: var(--font-weight-bold); color: var(--color-ink); margin-bottom: 4px; }
.bar-label { font-size: var(--text-body-xs); color: var(--color-steel); margin-top: var(--spacing-8); text-align: center; }

/* ============================================================
   SPARKLINE
   Micro trend bars for KPI stat cards: 36px-tall Fog bars with
   the current period in Race Green. Heights via data-h (same JS
   setter as the bar chart) — decorative, so no labels or axes.
   ============================================================ */
.sparkline { display: flex; align-items: flex-end; gap: 3px; height: 36px; margin-top: var(--spacing-12); max-width: 160px; }
.spark { flex: 1; min-width: 4px; background: var(--color-fog); }
.spark.is-now { background: var(--color-race-green); }

/* ============================================================
   ACCOUNT SHELL
   The buyer's account: one sticky nav column beside the page, on
   every account destination — dashboard, entries, cards, wins,
   settings. It mirrors the console's data-driven sidebar, not its
   stylesheet: the console lives on css/console.css and its own
   token set, so nothing here is shared but the shape.

   Links carry no stroke. Rest is Steel, hover washes Mist, active
   fills Fog and goes Deep Forest semibold — the same two-step
   relationship the notifications list uses for read vs unread, so
   active never collides with hover the way a Mist-on-Mist pair
   would.

   Three states down two breakpoints, each one narrower than the
   last and each still a navigation rather than a menu:

     > 1024   expanded 220px column, or the 64px icon rail —
              the buyer's choice, kept in localStorage
     ≤ 1024   the rail, forced. The column stays beside the page
              and gives up its labels
     ≤ 768    an off-canvas tray behind a burger. A 64px rail out
              of a 375px screen is a sixth of the reading width
              spent on navigation the buyer is not using, so here
              it leaves the layout entirely — and because it is no
              longer competing for width, the tray shows the full
              labelled nav rather than the rail

   The tray is the console sidebar's off-canvas pattern verbatim
   (chrome.css → #sidebar): same 768 step, same wash, same
   min(width, 80vw), same 0.25s slide. Two off-canvas panels in one
   app should not be two different objects.

   Which state is live is decided in js/account-nav.js, not by a
   second copy of these rules — the collapse state is JS-owned
   already (the toggle and its localStorage key), so this file
   describes the states and nothing chooses between them twice.
   ============================================================ */
.account-shell {
  --account-nav-w: 220px;
  --account-nav-w-tray: 280px;
  display: grid;
  grid-template-columns: var(--account-nav-w) 1fr;
  gap: var(--spacing-40);
  align-items: start;   /* a stretched track cannot stick */
  padding-bottom: var(--spacing-64);
}
.account-shell.is-collapsed { --account-nav-w: 64px; }
.account-main { min-width: 0; }

.account-nav {
  position: sticky;
  top: var(--sticky-top);
  display: flex;
  flex-direction: column;
  gap: var(--spacing-4);
  max-height: calc(100dvh - var(--sticky-top) - var(--spacing-24));
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: none;
}
.account-nav::-webkit-scrollbar { display: none; }

/* Identity anchor. The top nav already shows the avatar; this one
   earns its place by naming the account the column belongs to. */
.account-nav-id {
  display: flex;
  align-items: center;
  gap: var(--spacing-12);
  padding: var(--spacing-12) var(--spacing-12) var(--spacing-16);
  margin-bottom: var(--spacing-4);
  border-bottom: 1px solid var(--color-fog);
  min-width: 0;
}
.account-nav-avatar {
  width: 40px; height: 40px;
  flex-shrink: 0;
  border-radius: var(--radius-pill);
  background: var(--color-obsidian);
  color: var(--color-snow);
  display: flex; align-items: center; justify-content: center;
  font-size: var(--text-body-sm);
  font-weight: var(--font-weight-bold);
}
.account-nav-who { min-width: 0; }
.account-nav-name {
  font-size: var(--text-body);
  font-weight: var(--font-weight-semibold);
  color: var(--color-ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.account-nav-status { margin-top: 3px; }

.account-nav-group { display: flex; flex-direction: column; gap: 2px; }
.account-nav-group + .account-nav-group { margin-top: var(--spacing-20); }
.account-nav-label {
  font-size: var(--text-body-xs);
  font-weight: var(--font-weight-semibold);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-steel);
  padding: var(--spacing-8) var(--spacing-12) var(--spacing-4);
}

.account-nav-link {
  display: flex;
  align-items: center;
  gap: var(--spacing-12);
  padding: var(--spacing-8) var(--spacing-12);
  border-radius: var(--radius-controls-sm);
  font-size: var(--text-body);
  font-weight: var(--font-weight-medium);
  color: var(--color-steel);
  text-decoration: none;
  white-space: nowrap;
  transition: background var(--duration-micro) ease, color var(--duration-micro) ease;
}
@media (hover: hover) and (pointer: fine) {
  .account-nav-link:hover { background: var(--color-mist); color: var(--color-ink); text-decoration: none; }
}
.account-nav-link:active { background: var(--color-fog); }
.account-nav-link:focus-visible { outline: 2px solid var(--color-race-green); outline-offset: -2px; }
.account-nav-link.active { background: var(--color-fog); color: var(--color-deep-forest); font-weight: var(--font-weight-semibold); }
.account-nav-link i[data-icon] { color: var(--color-ash); flex-shrink: 0; }
.account-nav-link.active i[data-icon] { color: currentColor; }
@media (hover: hover) and (pointer: fine) {
  .account-nav-link:hover i[data-icon] { color: currentColor; }
}
.account-nav-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; }

.account-nav-toggle {
  display: flex;
  align-items: center;
  gap: var(--spacing-12);
  margin-top: var(--spacing-12);
  padding: var(--spacing-8) var(--spacing-12);
  border: none;
  border-top: 1px solid var(--color-fog);
  border-radius: 0;
  background: none;
  font-family: var(--font-body);
  font-size: var(--text-body-xs);
  font-weight: var(--font-weight-medium);
  color: var(--color-steel);
  cursor: pointer;
  padding-top: var(--spacing-16);
  transition: color var(--duration-micro) ease, transform 160ms var(--ease-deck-out);
}
@media (hover: hover) and (pointer: fine) {
  .account-nav-toggle:hover { color: var(--color-ink); }
}
.account-nav-toggle:active { transform: scale(0.92); }
.account-nav-toggle:focus-visible { outline: 2px solid var(--color-race-green); outline-offset: -2px; }
.account-nav-toggle i[data-icon] { flex-shrink: 0; }

/* ── Collapsed rail ─────────────────────────────────────────
   Labels go to .sr-only rather than display:none, so the link
   keeps the same accessible name it has when expanded and the
   two states can never drift apart. */
.is-collapsed .account-nav-link,
.is-collapsed .account-nav-toggle,
.is-collapsed .account-nav-id { justify-content: center; padding-inline: 0; }
.is-collapsed .account-nav-text,
.is-collapsed .account-nav-who,
.is-collapsed .account-nav-label {
  position: absolute; width: 1px; height: 1px;
  overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap;
}
/* The group label was the visual separator; collapsed, the rule is. */
.is-collapsed .account-nav-group + .account-nav-group {
  margin-top: var(--spacing-12);
  padding-top: var(--spacing-12);
  border-top: 1px solid var(--color-fog);
}
.is-collapsed .account-nav-link { gap: 0; padding-block: var(--spacing-12); }
/* With no label beside it, the glyph is the whole control — it takes
   the standard 20px rather than the 16px it uses next to text. */
.is-collapsed .account-nav-link .icon-sm { width: 20px; height: 20px; }

/* ── At and below 1024: the rail, always ────────────────────
   The rail is already the collapsed state, so there is nothing to
   restyle — only the choice to take away. The gutter closes up,
   because at these widths the content column needs every pixel the
   rail is not using, and the collapse control goes: there is no
   second state to toggle to. */
@media (max-width: 1024px) {
  .account-shell,
  .account-shell.is-collapsed { gap: var(--spacing-20); }
  .account-nav-toggle { display: none; }
}

/* ── At and below 768: the tray ─────────────────────────────
   The nav leaves the layout and the content column takes the whole
   width. The burger is the only thing left in the flow, and it
   sits where the column used to start, so the navigation is still
   found in the same place it always was. */
/* The burger reads as a control, not as the page's action: a quiet
   Pebble pill, the same object the console's sidebar toggle is. It
   says "Account" rather than the current section — the page title
   directly above already names that, and repeating it here would
   make the burger look like a section switcher it is not.

   Its appearance is declared here and only its visibility inside the
   breakpoint, so the gallery can show the control at any width. */
.account-nav-burger {
  display: none;
  align-items: center;
  gap: var(--element-gap);
  align-self: start;
  justify-self: start;
  padding: 8px 14px;
  background: var(--surface-card-white);
  border: 1px solid var(--color-pebble);
  border-radius: var(--radius-pill);
  font-family: var(--font-body);
  font-size: var(--text-body-sm);
  font-weight: var(--font-weight-medium);
  color: var(--color-ink);
  cursor: pointer;
  transition: border-color var(--duration-micro) ease, transform 160ms var(--ease-deck-out);
}
@media (hover: hover) and (pointer: fine) {
  .account-nav-burger:hover { border-color: var(--color-graphite); }
}
.account-nav-burger:active { transform: scale(0.96); }
.account-nav-burger:focus-visible { outline: 2px solid var(--color-race-green); outline-offset: 2px; }
.account-nav-burger i[data-icon] { color: var(--color-steel); flex-shrink: 0; }

.account-nav-scrim { display: none; }

@media (max-width: 768px) {
  .account-shell,
  .account-shell.is-collapsed {
    grid-template-columns: 1fr;
    gap: var(--spacing-16);
  }

  .account-nav-burger { display: inline-flex; }

  /* Starts under the dev bar, exactly where the console's sidebar
     starts — the tool bar is the one thing that outranks a drawer.
     It does cover the site nav, which is what a drawer is for. */
  .account-nav {
    position: fixed;
    top: var(--dev-bar-h); bottom: 0; left: 0;
    z-index: var(--z-mobile-menu);
    width: min(var(--account-nav-w-tray), 80vw);
    max-height: none;
    padding: var(--spacing-20) var(--spacing-16);
    /* A drawer pinned to the bottom edge ends on the home indicator,
       and its own scroll must not drag the page behind it. */
    padding-bottom: calc(var(--spacing-20) + env(safe-area-inset-bottom, 0px));
    padding-left: calc(var(--spacing-16) + env(safe-area-inset-left, 0px));
    background: var(--surface-card-white);
    box-shadow: var(--shadow-modal);
    overflow-y: auto;
    overscroll-behavior: contain;
    transform: translateX(-100%);
    /* The drawer is what the reader is watching, so it gets the
       drawer band rather than the micro one, on the strong ease-out
       — and this rule is the closed state, so it times the CLOSE.
       Exits are always faster than entries. */
    transition: transform 0.2s var(--ease-deck-out);
  }
  .account-nav.is-open { transform: translateX(0); transition: transform 0.3s var(--ease-deck-out); }
  /* The tray has room for the labels, so the rail's sr-only
     treatment must not follow it in when the shell is collapsed. */
  .account-shell.is-collapsed .account-nav { width: min(var(--account-nav-w-tray), 80vw); }

  .account-nav-scrim {
    display: block;
    position: fixed;
    inset: var(--dev-bar-h) 0 0;
    z-index: calc(var(--z-mobile-menu) - 1);
    background: rgba(var(--color-brand-primary-strong-rgb), 0.55);
    -webkit-backdrop-filter: blur(2px);
    backdrop-filter: blur(2px);
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--duration-micro) ease;
  }
  .account-nav-scrim.is-open { opacity: 1; pointer-events: auto; }
}

@media (pointer: coarse) {
  .account-nav-link,
  .account-nav-burger,
  .account-nav-toggle { min-height: var(--touch-target); }
}

@media (prefers-reduced-motion: reduce) {
  .account-nav,
  .account-nav-scrim { transition: none; }
}

/* Settings sections behind the nav's Profile settings group. Named
   apart from .tab-pane on purpose: these panes have no .tab-btn, so
   they must never be reachable by initTabs' index pairing. */
.account-pane { display: none; }
.account-pane.active { display: block; }

/* ============================================================
   PANEL
   Admin/host list scaffolding: a Snow shell with a ruled head
   (title + actions), an optional padded toolbar row, a flush
   body (tables lose their own border inside), and a ruled foot
   for counts + pagination.
   ============================================================ */
.panel { background: var(--surface-card-white); border-radius: var(--radius-cards); overflow: hidden; }
.panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--spacing-16);
  flex-wrap: wrap;
  padding: var(--spacing-20) var(--spacing-24);
  border-bottom: 1px solid var(--color-fog);
}
.panel-title { font-family: var(--font-display); letter-spacing: var(--ls-rg); font-size: var(--text-heading-sm); font-weight: var(--font-weight-semibold); color: var(--color-ink); }
.panel-toolbar {
  display: flex;
  align-items: center;
  gap: var(--spacing-12);
  flex-wrap: wrap;
  padding: var(--spacing-12) var(--spacing-24);
  border-bottom: 1px solid var(--color-fog);
}
.panel .table-wrap { border: none; border-radius: 0; }
.panel-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--spacing-16);
  flex-wrap: wrap;
  padding: var(--spacing-16) var(--spacing-24);
  border-top: 1px solid var(--color-fog);
}

@media (max-width: 768px) {
  .panel-head, .panel-toolbar, .panel-foot { padding-inline: var(--spacing-16); }
  /* The head's title and actions already wrap; make the actions span
     the row when they do, so a lone button isn't stranded mid-line. */
  .panel-head > :not(.panel-title) { flex: 1 1 100%; }
}

/* ============================================================
   SETTINGS ROWS
   Preference rows for profile/admin settings: name + description
   left, one control right, Fog hairlines between. The danger row
   swaps the name to the functional danger colour.
   ============================================================ */
.settings-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--spacing-24);
  padding: var(--spacing-16) 0;
  border-bottom: 1px solid var(--color-fog);
}
.settings-row:first-child { padding-top: 0; }
.settings-row:last-child { border-bottom: none; padding-bottom: 0; }
.settings-copy { max-width: 52ch; }
.settings-name { font-size: var(--text-body); font-weight: var(--font-weight-semibold); color: var(--color-ink); }
.settings-desc { font-size: var(--text-body-sm); line-height: var(--leading-body-sm); color: var(--color-steel); margin-top: 2px; }
.settings-row .form-select { width: 200px; flex-shrink: 0; }
.settings-row--danger .settings-name { color: var(--danger); }

@media (max-width: 600px) {
  .settings-row { flex-direction: column; align-items: flex-start; gap: var(--spacing-12); }
  .settings-row .form-select { width: 100%; }
}

/* ============================================================
   NOTIFICATIONS LIST
   One notification is one card, the same object an entry is:
   Snow at the compact card radius, spaced rather than ruled, and
   lifting on hover like every other clickable card. It was a
   ruled list on a bare page, which made it the only account
   surface in the app that was not made of cards.

   The unread signal stays the Fog wash on the whole row (DESIGN.md
   → Notifications List) — now it reads as Fog card against Snow
   card, which is the same two-step the account nav uses for active
   against hover.
   ============================================================ */
.notification-list {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-16);
}

.notification-item {
  display: flex;
  gap: var(--spacing-16);
  padding: var(--spacing-20);
  background: var(--surface-card-white);
  border-radius: var(--radius-cards-compact);
  /* The press takes the press band, not the hover's: a transform
     eased over 250ms on a symmetric curve lags the thumb that
     started it (DESIGN.md → Motion Philosophy, 120–160ms on the
     strong ease-out). Everything else here stays at micro speed. */
  transition: background var(--duration-micro) ease, transform 160ms var(--ease-deck-out), box-shadow var(--duration-micro) ease;
  cursor: pointer;
}
@media (hover: hover) and (pointer: fine) {
  .notification-item:hover { transform: translateY(-3px); box-shadow: var(--shadow-flat-hairline); }
}
.notification-item:active { transform: translateY(-1px); }
.notification-item.unread { background: var(--color-fog); }
/* On a Fog card the icon plate needs its own stroke back to read as
   a plate rather than a hole; on Snow the Fog fill does that job. */
.notif-icon { font-size: var(--text-heading-sm); flex-shrink: 0; width: 40px; height: 40px; background: var(--color-fog); border-radius: var(--radius-icons); display: flex; align-items: center; justify-content: center; border: 1px solid transparent; }
.notification-item.unread .notif-icon { background: var(--color-snow); border-color: var(--color-pebble); }
.notif-body { flex: 1; }
.notif-title { font-size: var(--text-body); font-weight: var(--font-weight-semibold); margin-bottom: 2px; }
.notif-desc { font-size: var(--text-body-sm); color: var(--color-steel); line-height: var(--leading-body-sm); }
.notif-time { font-size: var(--text-body-xs); color: var(--color-steel); white-space: nowrap; flex-shrink: 0; }

/* The timestamp is the first thing to give up its column: on a
   compact phone it drops under the body copy rather than pinching
   the title into a two-word column. */
@media (max-width: 480px) {
  .notification-list { gap: var(--spacing-12); }
  .notification-item { flex-wrap: wrap; gap: var(--spacing-12); padding: var(--spacing-16); }
  .notif-body { flex: 1 1 auto; min-width: 0; }
  /* Padding, not margin — the row is 100% wide, so an indent has to
     come out of its own box or it pushes the page sideways. */
  .notif-time { flex: 1 0 100%; padding-left: calc(40px + var(--spacing-12)); }
}

/* ============================================================
   TICKET CARDS (entries)
   The buyer's entry as the platform's standard media row —
   the same shape .order-item uses and the same reading order the
   dashboard's Active entries table gives: prize art, then the
   draw and its reference, then the live timer, then the status
   and its action.

   The art sits ON the card rather than running to its edge. A
   perforated stub (full-bleed art, dashed rail) was the only
   place in the app where imagery touched a card's boundary, and
   it read as a different system; the thumbnail is now inset with
   the card's own padding all round, at the .order-img radius.

   Grid rather than flex, because the row has four fixed jobs and
   the timer has to hold a column of its own: with the slack all
   on the body, every card's timer and action land on the same
   vertical line down the list, the way a table's columns do.

   Active entries link through to their draw, so they join the
   interactive-card family — the same flat-hairline lift as
   listing/prize cards.
   ============================================================ */
.ticket-grid {
  display: grid;
  /* One per row: the four-column row wants the full width, and
     these entries are read against each other — which draw closes
     first, which reference is which. */
  grid-template-columns: 1fr;
  gap: var(--spacing-16);
  /* The row answers to its own box, not the viewport. The account
     nav collapses to an icon rail, so one window width gives this
     list two quite different widths — its steps are @container. */
  container-type: inline-size;
}
.ticket-grid .empty-state { grid-column: 1 / -1; }

.ticket-card {
  background: var(--surface-card-white);
  border: none;
  border-radius: var(--radius-cards-compact);
  padding: var(--spacing-20);
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto auto;
  grid-template-areas: 'art body timer actions';
  align-items: center;
  gap: var(--spacing-20);
  /* The press takes the press band, not the hover's: a transform
     eased over 250ms on a symmetric curve lags the thumb that
     started it (DESIGN.md → Motion Philosophy, 120–160ms on the
     strong ease-out). Everything else here stays at micro speed. */
  transition: transform 160ms var(--ease-deck-out), box-shadow var(--duration-micro) ease;
}
/* Interactive entries lift on hover and settle on press — matches
   .listing-card so every clickable card behaves as one family. */
.ticket-card--link { cursor: pointer; }
@media (hover: hover) and (pointer: fine) {
  .ticket-card--link:hover { transform: translateY(-3px); box-shadow: var(--shadow-flat-hairline); }
}
.ticket-card--link:active { transform: translateY(-1px); }
/* Winner + past modifiers (were inline styles) */
.ticket-card--won { border: 2px solid var(--color-ink); }
.ticket-card--past { opacity: 0.65; }

/* Inset thumbnail. Fog beneath so a portrait crop or a slow image
   leaves a plate rather than a hole, matching .thumb-sm's ground
   in the dashboard table. */
.ticket-left {
  grid-area: art;
  width: 88px;
  aspect-ratio: 1;
  border-radius: var(--radius-xl);
  overflow: hidden;
  background: var(--color-fog);
}
.ticket-left img { display: block; width: 100%; height: 100%; object-fit: cover; }

.ticket-body { grid-area: body; min-width: 0; display: flex; flex-direction: column; gap: var(--spacing-4); }
.ticket-raffle { font-family: var(--font-display); letter-spacing: var(--ls-rg); font-size: var(--text-body-lg); font-weight: var(--font-weight-semibold); }
/* Deck/quantity plus the entry reference, as one quiet line — the
   dashboard's Card and Entry columns read together. Not a chip:
   the chip made a secondary line the loudest object in the card. */
.ticket-number { font-size: var(--text-body-sm); color: var(--color-steel); }
/* Status then date, on one line under the details. The badge reads
   as a property of the entry, which is where a buyer looks for it —
   not as a second thing to scan over in the action column. It is a
   taller object than the lines above, so this row buys itself a
   little more air than the 4px the body gap gives. */
.ticket-meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--spacing-8);
  font-size: var(--text-body-sm);
  color: var(--color-steel);
  margin-top: var(--spacing-8);
}

/* The timer takes the slack between the details and the action —
   the one thing on this card that changes while the buyer is
   looking at it, given room of its own rather than trailing the
   draw date. A wider gutter on its trailing edge stops it reading
   as part of the action cluster. It is still meta, not the
   headline a draw page makes it, so the bare numerals step down
   to the body scale and the row keeps the dashboard's density.

   The min-width holds the column still: a draw inside 100 days
   prints two digits where one further out prints three, and
   without a floor every such row would step sideways. It is also
   the width the "Ended" tag sits in, so a closed entry lands on
   the same line as a live one. */
.ticket-card > .countdown { grid-area: timer; min-width: 168px; margin-inline-end: var(--spacing-20); }
.ticket-card .countdown-bare { gap: var(--spacing-8); }
.ticket-card .countdown-bare .count-num,
.ticket-card .countdown-bare .count-sep { font-size: var(--text-body-lg); }
.ticket-card .countdown-bare .count-label { margin-top: 1px; }

/* ── Won entry: the win record, on the entry that earned it ──
   There is no My wins page, so the card carries what that page
   used to — but only what the platform can actually attest to: the
   draw date and the opaque result reference published with the
   proof. Label over value, tabular figures, wrapping as a row
   rather than a grid — short facts under the meta line, not a
   second card.

   No prize value, no claim deadline and no claim action: the
   platform stops when the winner is selected and passes that
   customer's details to the Host (RFL-9, RFL-103). */
.ticket-win {
  display: flex;
  flex-wrap: wrap;
  gap: var(--spacing-8) var(--spacing-20);
  margin-top: var(--spacing-12);
  padding-top: var(--spacing-12);
  border-top: 1px solid var(--color-fog);
}
/* min-width:0 so a fact may shrink below its content: the result
   reference is 64 characters of hex, and a flex item defaults to
   min-width:auto, which would push it out past the card's edge and
   under the action rather than wrapping inside it. It wraps rather
   than truncating — a half-printed reference invites a false match
   against the published proof, which is the one thing it exists to
   make possible. */
.ticket-win-fact { display: flex; flex-direction: column; min-width: 0; font-size: var(--text-body-sm); color: var(--color-ink); font-weight: var(--font-weight-semibold); }
.ticket-win-fact .ref-text { overflow-wrap: anywhere; }
.ticket-win-label { font-size: var(--text-caption); font-weight: var(--font-weight-regular); color: var(--color-steel); }

/* The action alone, centred on the row — the status moved into the
   body, so there is nothing left to stack it under and nothing to
   pull it off centre. No rule between it and the rest: the card's
   own padding is the separation everywhere else in the app. */
.ticket-right { grid-area: actions; display: flex; flex-direction: column; align-items: flex-end; justify-content: center; gap: var(--spacing-12); }

/* Below ~840px the four columns stop fitting on one line. The
   timer is what gives: it drops beneath the details, where it sat
   before it had a column, and the art and the action hold their
   places either side. */
@container (max-width: 840px) {
  .ticket-card {
    grid-template-columns: auto minmax(0, 1fr) auto;
    grid-template-areas:
      'art body  actions'
      'art timer actions';
    row-gap: var(--spacing-12);
  }
  /* The art spans both rows now, so centring floats it below the
     title it belongs to — pin it to the top instead. */
  .ticket-left { align-self: start; }
  .ticket-card > .countdown { margin-inline-end: 0; }
}

/* Phone width: the thumbnail shrinks rather than the art stacking
   above the title — the same call .order-item makes, and for the
   same reason: the row is scannable because the art sits beside
   the name. The action takes a full-width line and stretches to
   it, the way .form-actions does below 600px: one action, and the
   thumb should not have to find a pill floating in a wide row. */
@container (max-width: 520px) {
  .ticket-card {
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-areas:
      'art     body'
      'art     timer'
      'actions actions';
    padding: var(--spacing-16);
    column-gap: var(--spacing-12);
  }
  .ticket-left { width: 56px; }
  .ticket-right { align-items: stretch; }
  .ticket-right .btn { justify-content: center; }
}

/* ============================================================
   LISTING CARDS (marketplace)
   ============================================================ */
.listing-card {
  background: var(--surface-card-white); border: none; border-radius: var(--radius-cards); overflow: hidden;
  /* The press takes the press band, not the hover's: a transform
     eased over 250ms on a symmetric curve lags the thumb that
     started it (DESIGN.md → Motion Philosophy, 120–160ms on the
     strong ease-out). Everything else here stays at micro speed. */
  transition: transform 160ms var(--ease-deck-out), box-shadow var(--duration-micro) ease;
}
@media (hover: hover) and (pointer: fine) {
  .listing-card:hover { box-shadow: var(--shadow-flat-hairline); transform: translateY(-3px); }
}
.listing-card:active { transform: scale(0.99); }
.listing-img { width: 100%; height: 200px; object-fit: cover; overflow: hidden; position: relative; }
.listing-body { padding: var(--spacing-20); }
.listing-title { font-family: var(--font-display); letter-spacing: var(--ls-rg); font-size: var(--text-body-lg); font-weight: var(--font-weight-semibold); margin-bottom: var(--spacing-8); }
.listing-price { font-size: var(--text-subheading); font-weight: var(--font-weight-bold); color: var(--color-ink); }
.listing-rrp { font-size: var(--text-body-sm); color: var(--color-steel); }
.listing-rrp del { color: var(--color-steel); }
.listing-price-above { color: var(--success); font-size: var(--text-body-sm); }
.listing-price-below { color: var(--warning); font-size: var(--text-body-sm); }

@media (max-width: 768px) {
  .listing-img { height: 160px; }
  .listing-body { padding: var(--spacing-16); }
}

/* ============================================================
   ORDER ITEMS
   ============================================================ */
.order-item { display: flex; gap: var(--spacing-20); padding: var(--spacing-20); border: none; border-radius: var(--radius-cards-compact); background: var(--surface-card-white); }
.order-img { width: 80px; height: 80px; object-fit: cover; border-radius: var(--radius-xl); flex-shrink: 0; }
.order-body { flex: 1; }
.order-title { font-weight: var(--font-weight-semibold); margin-bottom: var(--spacing-4); }
.order-meta { font-size: var(--text-body-sm); color: var(--color-steel); }

/* The 80px thumbnail plus 20px gaps leaves ~210px of body at phone
   width, which breaks a draw title across four lines. Shrink the
   thumbnail rather than stacking — the row's scannability comes from
   the image sitting beside the title, not above it. */
@media (max-width: 480px) {
  .order-item { gap: var(--spacing-12); padding: var(--spacing-16); }
  .order-img { width: 56px; height: 56px; }
}

/* ============================================================
   PROVENANCE CHAIN
   ============================================================ */
.provenance-chain { display: flex; flex-direction: column; gap: 0; }
.provenance-step { display: flex; gap: var(--spacing-16); position: relative; padding-bottom: var(--spacing-20); }
.provenance-step::before { content: ''; position: absolute; left: 14px; top: 28px; bottom: 0; width: 2px; background: var(--color-fog); }
.provenance-step:last-child::before { display: none; }
.prov-dot { width: 28px; height: 28px; border-radius: var(--radius-pill); background: var(--color-deep-forest); display: flex; align-items: center; justify-content: center; font-size: var(--text-body-xs); color: var(--color-snow); flex-shrink: 0; font-weight: var(--font-weight-bold); }
.prov-body { padding-top: 2px; }
.prov-label { font-weight: var(--font-weight-semibold); font-size: var(--text-body); }
.prov-detail { font-size: var(--text-body-sm); color: var(--color-steel); margin-top: 2px; }

/* Dark surface variant — on Deep Forest / Race Green the dots
   invert to the dark ground's accent, the connector takes its
   hairline, and the copy lifts to the heading and body inks. */
.surface-forest .provenance-step::before { background: var(--dark-line); }
.surface-forest .prov-dot { background: var(--dark-accent); color: var(--dark-on-accent); }
.surface-forest .prov-label { color: var(--dark-heading); }
.surface-forest .prov-detail { color: var(--dark-body); }

/* How It Works "purchase to draw" split — the chain is shorter
   than the villa image, so centre both on the row instead of
   top-aligning, and open the image's aspect ratio up a little
   taller than the standard .media-split-media 4/3.

   KEYED ON THE CHAIN, NOT ON THE GROUND IT HAPPENS TO BE ON. The
   section can be set light or dark from the console (components.css
   → SECTION GROUND), and a layout that moved with a colour would
   re-crop the picture the operator cropped. */
.grid-2:has(.provenance-chain) { align-items: center; }
.grid-2:has(.provenance-chain) .media-split-media { aspect-ratio: 4 / 3.4; }

/* ============================================================
   WINNER CARDS
   ============================================================ */
.winner-card { background: var(--surface-card-white); border: none; border-radius: var(--radius-cards-compact); overflow: hidden; }
.winner-card-header { background: var(--surface-dark-surface); padding: var(--spacing-20); display: flex; gap: var(--spacing-16); align-items: center; }

/* Icon accents on the Deep Forest header follow the dark-surface
   rule: Blush Pink, never Ink. */
.winner-card-header i[data-icon] { color: var(--color-blush-pink); }
.winner-name { color: var(--color-mint-ice-cream); font-family: var(--font-display); letter-spacing: var(--ls-rg); font-size: var(--text-subheading); font-weight: var(--font-weight-semibold); }
.winner-prize { color: var(--color-ash); font-size: var(--text-body-sm); }
.winner-card-body { padding: var(--spacing-20); }

@media (max-width: 480px) {
  .winner-card-header,
  .winner-card-body { padding: var(--spacing-16); }
  .winner-card-header { gap: var(--spacing-12); }
}

/* ============================================================
   KYC / VERIFICATION
   ============================================================ */
.kyc-status-card { background: var(--surface-card-white); border: none; border-radius: var(--radius-cards-compact); padding: var(--card-padding-compact); }
.kyc-icon { font-size: var(--text-heading-lg); margin-bottom: var(--spacing-12); }
.kyc-level { font-family: var(--font-display); letter-spacing: var(--ls-rg); font-size: var(--text-heading-sm); font-weight: var(--font-weight-semibold); margin-bottom: var(--spacing-8); }
.kyc-requirement { display: flex; align-items: center; gap: var(--spacing-12); padding: var(--spacing-12) 0; border-bottom: 1px solid var(--color-fog); }
.kyc-requirement:last-child { border-bottom: none; }
.kyc-req-icon { font-size: var(--text-body-lg); flex-shrink: 0; }
.kyc-req-text { flex: 1; font-size: var(--text-body-sm); }
.kyc-req-title { font-size: var(--text-body); font-weight: var(--font-weight-semibold); color: var(--color-ink); }
.kyc-req-icon--ok { color: var(--success); }
.kyc-req-icon--wait { color: var(--warning); }

/* The account's current standing: a Snow row led by a Mint-washed
   check, the level and its scope, and the review date on the right.
   Verification copy is factual and calm, so the confirmation is the
   `--success` ring and nothing louder. */
.kyc-standing { display: flex; align-items: flex-start; gap: var(--spacing-20); border: 2px solid var(--success); }
.kyc-standing-mark {
  width: 64px; height: 64px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  border-radius: var(--radius-pill);
  background: var(--color-mint-ice-cream);
  color: var(--color-race-green);
}
.kyc-standing-body { flex: 1; min-width: 0; }
.kyc-standing-title { font-family: var(--font-display); letter-spacing: var(--ls-rg); font-size: var(--text-subheading); font-weight: var(--font-weight-semibold); color: var(--color-ink); margin-bottom: var(--spacing-4); }
.kyc-standing-aside { text-align: right; flex-shrink: 0; }

/* One tier of verification. The tier you are on takes an Ink ring;
   the ones behind you sit back at reduced opacity rather than being
   hidden, because the ladder is the point. */
.kyc-tier { position: relative; }
.kyc-tier--done { opacity: 0.7; }
.kyc-tier--current { border: 2px solid var(--color-ink); }
.kyc-tier-head { display: flex; justify-content: space-between; align-items: flex-start; gap: var(--spacing-12); margin-bottom: var(--spacing-12); }
.kyc-tier-name { font-size: var(--text-body-lg); font-weight: var(--font-weight-bold); color: var(--color-ink); margin-bottom: var(--spacing-4); }
.kyc-tier-mark { color: var(--color-ash); flex-shrink: 0; }
.kyc-tier--current .kyc-tier-mark { color: var(--color-ink); }
.kyc-tier-limit { margin-top: var(--spacing-16); text-align: center; }
.kyc-tier--current .kyc-tier-limit { border: 1px solid var(--color-ink); }

@media (max-width: 768px) {
  .kyc-standing { flex-wrap: wrap; gap: var(--spacing-16); }
  .kyc-standing-aside { text-align: left; }
}

/* ============================================================
   PRICE BREAKDOWN
   ============================================================ */
.price-row { display: flex; justify-content: space-between; gap: var(--spacing-16); padding: var(--spacing-8) 0; font-size: var(--text-body); }
.price-row.total { font-weight: var(--font-weight-bold); font-size: var(--text-body-lg); padding-top: var(--spacing-12); border-top: 2px solid var(--color-fog); margin-top: var(--spacing-8); }
.price-row.muted { color: var(--color-steel); }
/* The figure never wraps or truncates — it is the number the buyer is
   checking. A long label gives way instead. */
.price-row > :last-child { flex-shrink: 0; text-align: right; }

/* ============================================================
   CONFETTI
   ============================================================ */
.confetti-wrap { position: relative; }
.confetti-particle {
  position: absolute;
  width: 8px; height: 8px;
  border-radius: 2px;
  animation: confettiFall 3s ease-in forwards;
}
@keyframes confettiFall {
  0% { transform: translateY(-20px) rotate(0deg); opacity: 1; }
  100% { transform: translateY(400px) rotate(720deg); opacity: 0; }
}

/* ============================================================
   SUCCESS STATE (confirmation moment)
   Centred confirmation block closing the wallet top-up, deck
   purchase modal, checkout-success, and marketplace listing
   flows: 48px outline icon over a Switzer title, muted copy,
   then a Ghost + Primary CTA pair. Pages keep the block
   `.hidden` until the flow confirms; at most one confetti
   burst may accompany it.
   ============================================================ */
.success-state { text-align: center; padding: var(--spacing-40) var(--spacing-24); }
.success-title { font-family: var(--font-display); letter-spacing: var(--ls-rg); font-size: var(--text-heading-sm); font-weight: var(--font-weight-bold); margin-bottom: var(--spacing-12); }
.success-icon { display: block; text-align: center; margin-bottom: var(--spacing-16); }
/* The one place a UI icon scales past the 20px system size —
   proportionate to the confirmation title beside it. */
.success-icon .icon { width: 48px; height: 48px; }
.success-cta-row { display: flex; gap: var(--spacing-12); justify-content: center; max-width: 320px; margin: 0 auto; }

/* Two pills in 320px is already tight; below 480px they stack so
   neither label truncates at the moment of confirmation. */
@media (max-width: 480px) {
  .success-state { padding: var(--spacing-32) var(--spacing-16); }
  .success-cta-row { flex-direction: column; max-width: none; }
}

/* ============================================================
   IMAGE GALLERY
   ============================================================ */
.gallery { display: grid; grid-template-columns: 2fr 1fr 1fr; grid-template-rows: 1fr 1fr; gap: var(--spacing-12); height: 420px; border-radius: var(--radius-cards); overflow: hidden; }
.gallery-main { grid-row: 1 / 3; }
.gallery img { width: 100%; height: 100%; object-fit: cover; }

.gallery-hero { border-radius: var(--radius-cards); overflow: hidden; aspect-ratio: 4 / 3; background: var(--color-fog); margin-bottom: var(--spacing-12); }
.gallery-hero img { width: 100%; height: 100%; object-fit: cover; }
.gallery-thumb {
  width: 80px; height: 60px;
  object-fit: cover;
  border-radius: var(--radius-xl);
  cursor: pointer;
  border: 2px solid transparent;
  opacity: 0.65;
  transition: opacity var(--duration-micro) ease, border-color var(--duration-micro) ease, transform 160ms var(--ease-deck-out);
}
@media (hover: hover) and (pointer: fine) {
  .gallery-thumb:hover { opacity: 1; }
}
.gallery-thumb:active { opacity: 1; transform: scale(0.95); }
.gallery-thumb.active { border-color: var(--color-deep-forest); opacity: 1; }

@media (max-width: 768px) {
  .gallery { grid-template-columns: 1fr; height: auto; }
  .gallery-main { grid-row: auto; }
  .gallery .gallery-thumb { display: none; }
  /* The hero thumb strip is the draw page's only way to see the other
     prize shots, so it scrolls rather than wrapping under the hero and
     pushing the tabs down a row. */
  .gallery-thumbs {
    flex-wrap: nowrap;
    overflow-x: auto; overscroll-behavior-x: contain;
    scrollbar-width: none;
    margin-inline: calc(var(--spacing-20) * -1);
    padding-inline: var(--spacing-20);
  }
  .gallery-thumbs::-webkit-scrollbar { display: none; }
  .gallery-thumbs > .gallery-thumb { flex: 0 0 auto; }
}

/* ============================================================
   SEARCH & FILTER BAR
   ONE ROW, because every control on it is the same kind of
   question asked three ways: what are you looking for, how
   would you like it ordered, and which part of the catalogue.
   It was two rows — the search on top, then the sort and a
   scrolling run of category chips under it — and the second row
   was a line of the card spent on a control that could only be
   READ by moving it. A closed field says the same thing in one
   word, holds twenty categories as easily as three, and gives
   the row back to the results.

   The search takes the room that is left. It is the widest
   thing on the bar because it is the one control a reader
   arrives already holding an answer for; the two fields beside
   it are a fixed width, because a `<select>` wider than its
   longest option is a field with a hole in it.

   IT FOLDS ONCE, AND ONLY WHERE A PHONE HAS NO ROOM. Below the
   768 step the search takes its own line and the fields share
   the one under it — two lines, in the order the questions are
   asked, and never the four a wrapping row would have made.
   ============================================================ */
.filter-bar {
  background: var(--surface-card-white);
  border: none;
  border-radius: var(--radius-cards-compact);
  padding: var(--spacing-20);
  display: flex;
  align-items: center;
  gap: var(--spacing-12);
  margin-bottom: var(--spacing-24);
  /* IT FOLDS WHEN IT HAS TO, not at a width somebody guessed. The
     bar carries two filters on the catalogue and three on the
     marketplace, and three fixed fields beside the search are wider
     than a tablet — on `nowrap` the marketplace ran off the side of
     the page between the phone step and the desktop one. Wrapping
     costs a two-filter page nothing (it never fills the line) and
     gives a three-filter page its second line instead of a
     horizontal scrollbar. */
  flex-wrap: wrap;
}

/* ── The search ─────────────────────────────────────────────
   `align-items: stretch` is what makes the CTA exactly as tall
   as the field beside it without either being given a height:
   the field's own padding sets the row, and the button fills
   it. A height typed here is a height to keep in step with the
   FORMS block every time an input's padding changes. */
.filter-search {
  display: flex;
  align-items: stretch;
  gap: var(--spacing-12);
  /* The widest thing on the bar, and the one that gives ground
     first: a basis rather than `auto`, so the two fields beside
     it are never squeezed by a placeholder. */
  flex: 1 1 320px;
  min-width: 0;
}
.filter-search > .btn { flex: 0 0 auto; }

.search-field { position: relative; flex: 1 1 auto; min-width: 0; }
/* The glyph is a label, not a control — the click belongs to
   the field under it, all the way into the gutter. */
.search-field > i[data-icon] {
  position: absolute;
  left: var(--spacing-16);
  top: 50%;
  transform: translateY(-50%);
  color: var(--color-steel);
  pointer-events: none;
  transition: color var(--duration-micro) ease;
}
.search-field:focus-within > i[data-icon] { color: var(--color-ink); }
/* 16 gutter + 20 glyph + 12 clear */
.search-field .form-control { height: 100%; padding-left: 48px; }

/* ── The ways to narrow it ───────────────────────────────────
   Nothing is drawn between them: the bar's own gap is what
   separates the search from the fields, and a rule across a
   card this short reads as two cards that failed to come
   apart. */
.filter-row {
  display: flex;
  align-items: center;
  gap: var(--spacing-12);
  flex: 0 1 auto;
  min-width: 0;
}
/* 208px is what a field WANTS, not what it is owed: a shrinkable
   basis keeps it at 208 wherever there is room and lets it give
   ground where there is not. `min-width: 0` is what makes that
   possible at all — a <select> has an intrinsic minimum that would
   otherwise refuse to shrink and push the row wide. */
.filter-select { position: relative; flex: 0 1 208px; min-width: 0; }
.filter-select > i[data-icon] {
  position: absolute;
  left: var(--spacing-16);
  top: 50%;
  transform: translateY(-50%);
  color: var(--color-steel);
  pointer-events: none;
}
.filter-select .form-select { padding-left: 48px; }

/* ── Chips ───────────────────────────────────────────────────
   No longer part of this bar — a category is a field here — but
   still the toolbar control an admin panel narrows a list with
   (PANEL), so the object stays and only its address changed. */
.filter-chips {
  display: flex;
  gap: var(--element-gap);
  flex-wrap: wrap;
}
.filter-chips > .filter-chip { flex: 0 0 auto; }

.filter-chip {
  padding: 6px 14px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--color-pebble);
  background: var(--color-snow);
  font-family: var(--font-body);
  font-size: var(--text-body-sm);
  font-weight: var(--font-weight-medium);
  cursor: pointer;
  color: var(--color-steel);
  white-space: nowrap;
  transition: color var(--duration-micro) ease, border-color var(--duration-micro) ease, transform 160ms var(--ease-deck-out);
}
@media (hover: hover) and (pointer: fine) {
  .filter-chip:hover { border-color: var(--color-graphite); color: var(--color-ink); }
}
.filter-chip:active { transform: scale(0.96); }
.filter-chip:focus-visible { outline: 2px solid var(--color-race-green); outline-offset: 2px; }
.filter-chip.active { background: var(--color-deep-forest); border-color: var(--color-deep-forest); color: var(--color-mint-ice-cream); font-weight: var(--font-weight-semibold); }

/* The one fold. The search keeps its own line and the fields
   share the next, each taking half of it — a `<select>` at half
   a 360px screen still shows its longest option, and two of them
   stacked is a third line for no gain. */
@media (max-width: 768px) {
  .filter-bar { padding: var(--spacing-16); gap: var(--spacing-12); flex-direction: column; align-items: stretch; }
  .filter-search { flex: 0 0 auto; }
  .filter-row { flex: 0 0 auto; flex-wrap: wrap; }
  /* Two fields fit a phone's line and three do not — this bar
     carries two on the catalogue and three on the marketplace, so
     the row wraps at a basis rather than being ruled into a count.
     A page with two filters keeps the two lines it was asked for;
     one with three takes a third line rather than clipping all of
     them. */
  /* NOT EQUAL HALVES — each field takes the room its own longest
     value needs. `flex-basis: auto` on a `<select>` is that value's
     width, so the category field (which has to hold "All
     categories") gets more of the line than the sort beside it,
     instead of both being cut to exactly half and one of them
     clipping. */
  .filter-select { flex: 1 1 140px; }
  /* THE GLYPH GOES AND THE VALUE STAYS. Half a 360px screen is
     146px, and 48px of it spent on an icon leaves "All categories"
     clipped to "All categ". The field's own value is what says
     which filter it is — a word that has to be read in full beats
     a picture that hints at it — so the glyph stands down and the
     field takes the room back. The search keeps its own: a
     magnifier is read at a glance and the field beside it is the
     one control on that line. */
  .filter-select > i[data-icon] { display: none; }
  /* And the field's own chrome comes in with it: 16 + 36 of
     padding out of 146px is more than a third of the control spent
     on nothing, and "All categories" is the value that has to fit
     for the field to read as a field rather than as a fault. */
  .filter-select .form-select { padding: 12px 28px 12px var(--spacing-12); }
}

/* Touch — the chip is a 13px label in 6px of padding (~31px). */
@media (pointer: coarse) {
  .filter-chip { padding: 11px 18px; }
}

/* ============================================================
   PAYOUT ITEMS
   ============================================================ */
.payout-item { display: flex; justify-content: space-between; align-items: center; padding: var(--spacing-16) var(--spacing-20); border: none; border-radius: var(--radius-cards-compact); background: var(--surface-card-white); margin-bottom: var(--spacing-12); gap: var(--spacing-16); }
.payout-item:last-child { margin-bottom: 0; }
.payout-item .text-right { display: flex; flex-direction: column; align-items: flex-end; gap: var(--spacing-4); }
.payout-meta { font-size: var(--text-body-sm); color: var(--color-steel); margin-top: 2px; }
.payout-amount { font-family: var(--font-display); letter-spacing: var(--ls-rg); font-size: var(--text-subheading); font-weight: var(--font-weight-bold); }
.payout-amount.pending { color: var(--warning); }
.payout-amount.paid { color: var(--success); }

/* Description left, amount right works until the description wraps to
   three lines against a one-line amount. On a compact phone the
   amount takes its own left-aligned line beneath. */
@media (max-width: 480px) {
  .payout-item { flex-direction: column; align-items: flex-start; gap: var(--spacing-8); }
  .payout-item .text-right { align-items: flex-start; text-align: left; }
}

/* ============================================================
   ACTIVITY FEED
   ============================================================ */
.activity-feed { display: flex; flex-direction: column; gap: 0; }
.activity-item { display: flex; gap: var(--spacing-12); padding: var(--spacing-12) 0; border-bottom: 1px solid var(--color-fog); font-size: var(--text-body); align-items: flex-start; }
.activity-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--color-graphite); flex-shrink: 0; margin-top: 6px; }
.activity-dot.green { background: var(--success); }
.activity-dot.red { background: var(--danger); }
.activity-text { flex: 1; min-width: 0; color: var(--color-ink); }
.activity-time { font-size: var(--text-body-xs); color: var(--color-steel); flex-shrink: 0; }

@media (max-width: 480px) {
  .activity-item { flex-wrap: wrap; }
  .activity-time { flex: 1 0 100%; padding-left: calc(8px + var(--spacing-12)); }
}

/* ============================================================
   MISC SURFACES
   ============================================================ */
/* Highlight box (formerly gold-box) — quiet Fog emphasis card */
.gold-box { background: var(--surface-card-muted); border: none; border-radius: var(--radius-cards-compact); padding: var(--card-padding-compact); }

/* Fog cell — the smallest surface in the system: a quiet chip for a
   single figure inside a card that already has a border of its own.
   Not a card (a card inside a card is always wrong), which is why it
   has no shadow, no stroke, and no title slot. Receipt grids, wallet
   and wallet figures. */
.fog-cell { background: var(--color-fog); border-radius: var(--radius-xl); padding: var(--spacing-16); }
@media (max-width: 768px) {
  .fog-cell { padding: var(--spacing-12); }
}

/* Certification badge block */
.cert-badge-lg { display: flex; align-items: center; gap: var(--spacing-12); background: var(--surface-card-muted); border: none; border-radius: var(--radius-cards-compact); padding: var(--spacing-16) var(--spacing-20); }
.cert-badge-lg .cert-icon { font-size: var(--text-heading-sm); }
.cert-badge-lg .cert-num { font-size: var(--text-body-xs); color: var(--color-steel); font-weight: var(--font-weight-semibold); }
.cert-badge-lg .cert-title { font-size: var(--text-body); font-weight: var(--font-weight-bold); color: var(--color-ink); }

/* Avatar circle (hosts, users) */
.avatar-circle {
  width: 44px; height: 44px;
  border-radius: var(--radius-pill);
  background: var(--color-obsidian);
  color: var(--color-snow);
  display: flex; align-items: center; justify-content: center;
  font-weight: var(--font-weight-bold);
  font-size: var(--text-body-sm);
  flex-shrink: 0;
}

/* Pack tile — compact purchasable deck tile (detail rail).
   Carries the full collectible-deck information set — fanned portrait
   art, Ember value flag, name, "N cards · Digital assets", price, the
   Mint ticket tag and a Collect action — laid out horizontally so it
   fits the narrow sticky rail. Reuses the deck language verbatim
   (.deck-stage gradient, .deck-ticket-tag, badge-ember) so the
   quick-buy list reads as the same family as the Decks-tab deck card.
   Max-width caps it in wide gallery contexts; the rail is always
   narrower so production is unaffected. */
.pack-row {
  position: relative;              /* anchors the stretched Collect hit-area */
  display: flex;
  gap: var(--spacing-16);
  align-items: stretch;
  width: 100%;
  max-width: 420px;
  text-align: left;
  color: inherit;
  background: var(--color-snow);
  border: 1px solid var(--color-pebble);
  border-radius: var(--radius-inputs);
  padding: var(--element-gap);
  transition: background-color var(--duration-micro) ease;
}
/* Every tile is uniform Snow + grey stroke; hover just warms to Mist.
   Each row carries its own quantity stepper feeding one global Buy
   (.deck-buy-bar), so the row itself is no longer a single click target. */
@media (hover: hover) and (pointer: fine) {
  .pack-row:hover { background: var(--color-mist); }
}
/* A ROW IS NOT A BUTTON, so what a press buys it is the state the
   pointer would have given it — arriving at once rather than after
   the hover's 250ms, because a press is not a hover that happened
   quickly. It is a warm rather than a scale for the reason the
   console's list rows are: a 420px row shrinking under a thumb
   reads as the list moving, not as the row being pressed. Until
   now a touch device got no answer here at all — this is the row a
   buyer taps to choose a deck. */
.pack-row:active { background: var(--color-mist); transition-duration: 0ms; }

/* ── Portrait art stage — single card or a slight static fan ── */
.pack-row-stage {
  position: relative;
  flex: 0 0 88px;
  aspect-ratio: 4 / 5;              /* == .magnetic-layer (fills the 4/5 .deck-stage) */
  align-self: center;              /* hold the ratio rather than stretch to row height */
  border-radius: var(--radius-controls-sm);
  overflow: hidden;
  isolation: isolate;
  display: grid;
  place-items: center;
  background:
    radial-gradient(circle at 50% 18%, rgba(255, 255, 255, .12), transparent 32%),
    linear-gradient(180deg, var(--deck-stage-primary) 0%, var(--color-ink) 62%, var(--deck-stage-secondary) 100%);
}
.pack-row.is-limited .pack-row-stage {
  background:
    radial-gradient(circle at 50% 16%, rgba(255, 255, 255, .30), transparent 30%),
    linear-gradient(180deg, var(--color-blush-pink) 0%, color-mix(in oklab, var(--color-blush-pink), var(--color-deep-forest) 55%) 58%, var(--color-deep-forest) 130%);
}
.pack-row-flag { position: absolute; top: 8px; left: 8px; z-index: 4; pointer-events: none; }
.pack-row-flag .badge { font-size: var(--text-caption); padding: 3px 7px; }

.pack-row-fan { position: relative; width: 66%; aspect-ratio: 3 / 4; }
.pack-row-card {
  position: absolute;
  inset: 0;
  border-radius: var(--radius-md);
  overflow: hidden;
  background: var(--deck-card-paper);
  border: 1px solid rgba(255, 255, 255, .5);
  box-shadow: 0 6px 16px rgba(0, 0, 0, .32);
  transform-origin: 50% 90%;
  transition: transform var(--duration-micro) var(--ease-deck-out);
}
.pack-row-card img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Fan geometry — back cards tilt out, the front card sits straight. */
.pack-row-card:nth-of-type(1) { transform: rotate(-8deg) translateX(-5px); filter: brightness(.85); z-index: 1; }
.pack-row-card:nth-of-type(2) { transform: rotate(5deg) translateX(4px); filter: brightness(.93); z-index: 2; }
.pack-row-card:nth-of-type(3) { transform: rotate(0deg); z-index: 3; }
.pack-row-fan[data-count="1"] .pack-row-card:nth-of-type(1) { transform: none; filter: none; }
.pack-row-fan[data-count="2"] .pack-row-card:nth-of-type(2) { transform: rotate(0deg); filter: none; }
@media (hover: hover) and (pointer: fine) {
  .pack-row:hover .pack-row-fan .pack-row-card:nth-of-type(1) { transform: rotate(-13deg) translateX(-9px); }
  .pack-row:hover .pack-row-fan .pack-row-card:nth-of-type(2) { transform: rotate(9deg) translateX(7px); }
  .pack-row:hover .pack-row-fan[data-count="1"] .pack-row-card:nth-of-type(1) { transform: none; }
  .pack-row:hover .pack-row-fan[data-count="2"] .pack-row-card:nth-of-type(2) { transform: rotate(0deg); }
}

/* ── Body — name/desc up top, price + ticket + Collect anchored low ── */
.pack-row-body { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.pack-row-name {
  margin: 0;
  font-family: var(--font-display); letter-spacing: var(--ls-rg);
  font-size: var(--text-body-lg);
  line-height: 1.2;
  font-weight: var(--font-weight-semibold);
  color: var(--color-obsidian);
}
.pack-row-desc { margin: 3px 0 0; font-size: var(--text-body-xs); color: var(--color-steel); }
.pack-row-priceline { display: flex; align-items: center; flex-wrap: wrap; gap: var(--spacing-8); margin-top: auto; padding-top: var(--spacing-12); }
.pack-row-price {
  font-family: var(--font-display); letter-spacing: var(--ls-subheading);
  font-size: var(--text-subheading);
  font-weight: var(--font-weight-bold);
  color: var(--color-obsidian);
}

/* Freeroll variant — the host-enabled free entry row. A flat Deep
   Forest stage carries the generic The Raffle branded card, and the
   action is a single Claim button instead of a quantity stepper. */
.pack-row--freeroll .pack-row-stage { background: var(--color-deep-forest); }
.pack-row--freeroll .pack-row-qty { margin-top: var(--spacing-12); }
.pack-row--freeroll .pack-row-qty .btn { width: 100%; }

/* Owned variant — my-cards. The deck is already bought, so the row
   carries no price, no ticket tag and no stepper: it is a library
   entry, and its one action opens the asset vault. It also stops
   being a rail item, so the 420px cap comes off and the rows tile
   across the account column in .pack-row-grid. */
.pack-row-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
  gap: var(--element-gap);
}
@media (max-width: 480px) {
  .pack-row-grid { grid-template-columns: 1fr; }
}
.pack-row-grid .empty-state { grid-column: 1 / -1; }
.pack-row--owned { max-width: none; }
/* margin-top: auto, like .pack-row-priceline — deck names run to one
   or two lines, and the actions must still land on one line across
   the row of the grid they sit in. */
.pack-row-actions { display: flex; margin-top: auto; padding-top: var(--spacing-16); }
.pack-row-actions .btn { width: 100%; }

/* Narrow rails — the 88px art stage plus gaps takes a third of a
   350px row, which breaks the deck name across three lines. The art
   gives up the width, not the name: the fan stays legible at 68px
   and the body gets its measure back. */
@media (max-width: 480px) {
  .pack-row { gap: var(--spacing-12); }
  .pack-row-stage { flex-basis: 68px; }
}

@media (prefers-reduced-motion: reduce) {
  .pack-row, .pack-row-card { transition: none; }
}

/* Empty state */
.empty-state { text-align: center; padding: var(--spacing-64) var(--spacing-24); color: var(--color-steel); }
.empty-state p { margin-inline: auto; }

.empty-state-title { font-family: var(--font-display); letter-spacing: var(--ls-rg); font-size: var(--text-heading-sm); font-weight: var(--font-weight-semibold); color: var(--color-ink); margin-bottom: var(--spacing-8); }

/* Tutorial video card — a 16:9 poster under a dimming scrim, one
   glass play control, and the runtime pinned bottom-right. The
   poster is the only colour; chrome stays achromatic so a wall of
   these reads as a set rather than six competing thumbnails. */
.video-card { overflow: hidden; padding: 0; cursor: pointer; text-align: left; width: 100%; border: none; background: var(--surface-card-white); font-family: var(--font-body); }
.video-card:focus-visible { outline: 2px solid var(--color-race-green); outline-offset: 2px; }
.video-poster { position: relative; aspect-ratio: 16 / 9; background: var(--surface-main-dark); display: flex; align-items: center; justify-content: center; }
.video-poster img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; opacity: 0.42; transition: opacity var(--duration-micro) ease, transform var(--duration-micro) ease; }
@media (hover: hover) and (pointer: fine) {
  .video-card:hover .video-poster img { opacity: 0.52; transform: scale(1.02); }
}
.video-play {
  position: relative; z-index: 1;
  width: 52px; height: 52px;
  border-radius: var(--radius-pill);
  display: flex; align-items: center; justify-content: center;
  background: var(--glass-fill-light);
  color: var(--color-deep-forest);
  -webkit-backdrop-filter: blur(var(--glass-blur));
  backdrop-filter: blur(var(--glass-blur));
  /* The press takes the press band, not the hover's: a transform
     eased over 250ms on a symmetric curve lags the thumb that
     started it (DESIGN.md → Motion Philosophy, 120–160ms on the
     strong ease-out). Everything else here stays at micro speed. */
  transition: transform 160ms var(--ease-deck-out);
}
@media (hover: hover) and (pointer: fine) {
  .video-card:hover .video-play { transform: scale(1.06); }
}
.video-card:active .video-play { transform: scale(0.94); }
.video-time {
  position: absolute; bottom: var(--spacing-8); right: var(--spacing-8); z-index: 1;
  padding: 2px var(--spacing-8);
  border-radius: var(--radius-md);
  background: var(--deck-overlay-wash);
  color: var(--color-snow);
  font-size: var(--text-body-xs);
  font-variant-numeric: tabular-nums;
}
.video-meta { padding: var(--spacing-16); }
.video-title { font-size: var(--text-body); font-weight: var(--font-weight-semibold); color: var(--color-ink); margin-bottom: var(--spacing-4); }

/* Numbered step in a vertical how-to list. The disc is the ordinal,
   so it stays Ink-on-Snow and never becomes a decorative badge. */
.step-row { display: flex; gap: var(--spacing-12); }
.step-disc {
  width: 28px; height: 28px; flex-shrink: 0;
  border-radius: var(--radius-pill);
  background: var(--color-deep-forest);
  color: var(--color-snow);
  display: flex; align-items: center; justify-content: center;
  font-size: var(--text-body-xs);
  font-weight: var(--font-weight-bold);
  font-variant-numeric: tabular-nums;
}
.step-row-title { font-size: var(--text-body); font-weight: var(--font-weight-semibold); color: var(--color-ink); margin-bottom: 2px; }

/* Responsive helper kept for legacy two-col inline grids */
/* A `1fr` track takes its automatic minimum from the widest thing
   inside it, so one nowrap row is enough to push the whole column —
   and the page — past the viewport. min-width: 0 lets the track hold
   the width it was given, which is what `1fr` was meant to say.
   Same guard .layout-split carries in base.css. */
.resp-grid-2col > * { min-width: 0; }
@media (max-width: 1024px) { .resp-grid-2col { grid-template-columns: 1fr !important; } }
@media (max-width: 768px) {
  .resp-grid-2col { grid-template-columns: 1fr !important; }
  .grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; }
  .section-header.flex-between, .page-header .flex-between { flex-wrap: wrap; gap: var(--spacing-12); }
  .flex-between { flex-wrap: wrap; gap: var(--spacing-12); }
  .page-actions { flex-wrap: wrap; }
}
@media (max-width: 1024px) { .grid-4 { grid-template-columns: repeat(2, 1fr); } }

/* ============================================================
   CHECKOUT SUCCESS (order receipt flow)
   Centered single-column flow for buyer/checkout-success:
   success hero, ink-bordered receipt card, next-step cards,
   confirmation alerts, CTA row. The receipt's own cells are
   `.fog-cell` — see MISC SURFACES.
   ============================================================ */
.success-flow { max-width: 560px; margin: 0 auto; display: grid; gap: var(--spacing-40); }
.receipt-card { border: 2px solid var(--color-ink); }

@media (max-width: 768px) {
  .success-flow { gap: var(--spacing-24); }
}

/* ============================================================
   WALLET TOP-UP
   Centered single-column flow: balance tile, amount grid,
   payment card, success state.
   ============================================================ */
.topup-wrap {
  max-width: 520px;
  margin: 0 auto;
  padding: var(--spacing-32) 0 var(--spacing-64);
}
/* Same title block as every account page — Switzer 32 over Inter 15
   — led by the way back, because this flow is always entered from
   somewhere else. */
.topup-head { margin-bottom: var(--spacing-24); }
.topup-back { display: inline-block; margin-bottom: var(--spacing-12); font-size: var(--text-body-sm); color: var(--color-steel); text-decoration: none; }
@media (hover: hover) and (pointer: fine) {
  .topup-back:hover { color: var(--color-ink); text-decoration: none; }
}
.topup-back:active { color: var(--color-ink); }
/* A standalone way back is a control, not a link in a sentence, so
   it gets the box rather than an invisible hit area. */
@media (pointer: coarse) {
  .topup-back { display: inline-flex; align-items: center; min-height: var(--touch-target); }
}
.topup-head .page-title { margin-bottom: var(--spacing-8); }

/* What the buyer is about to be charged, restated on a Fog chip
   immediately above the button that charges it. */
.topup-summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--spacing-12);
  background: var(--color-fog);
  border-radius: var(--radius-xl);
  padding: var(--spacing-12) var(--spacing-16);
  margin-top: var(--spacing-20);
}
.topup-summary-amount { font-family: var(--font-display); letter-spacing: var(--ls-rg); font-size: var(--text-subheading); font-weight: var(--font-weight-bold); }
.balance-tile {
  background: var(--surface-card-white);
  border: none;
  border-radius: var(--radius-cards);
  padding: var(--spacing-24) var(--spacing-32);
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: var(--spacing-24);
}
.balance-label {
  font-size: var(--text-body-xs);
  font-weight: var(--font-weight-medium);
  color: var(--color-steel);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin-bottom: var(--spacing-4);
}
.balance-amount { font-family: var(--font-display); letter-spacing: var(--ls-rg); font-size: var(--text-heading); font-weight: var(--font-weight-bold); }

.amount-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--spacing-12);
  margin-bottom: var(--spacing-20);
}
.amount-grid + .payment-form-card { margin-top: 0; }
.amount-tile {
  background: var(--surface-card-white);
  border: 2px solid transparent;
  border-radius: var(--radius-cards-compact);
  padding: var(--spacing-16) var(--spacing-8);
  text-align: center;
  cursor: pointer;
  transition: border-color var(--duration-micro) ease, transform 160ms var(--ease-deck-out);
  font-family: var(--font-display); letter-spacing: var(--ls-subheading);
  font-size: var(--text-subheading);
  font-weight: var(--font-weight-bold);
}
@media (hover: hover) and (pointer: fine) {
  .amount-tile:hover { border-color: var(--color-pebble); }
}
.amount-tile:active { transform: scale(0.97); }
.amount-tile.selected { border-color: var(--color-deep-forest); background: var(--color-fog); }
.amount-tile .tile-label { font-size: var(--text-body-xs); font-weight: var(--font-weight-regular); font-family: var(--font-body); color: var(--color-steel); margin-top: 2px; }

.payment-form-card {
  background: var(--surface-card-white);
  border: none;
  border-radius: var(--radius-cards);
  padding: var(--spacing-24) var(--spacing-32);
}
/* Field rhythm is the FORMS block's job — this card adds nothing. */

.card-brand-row { display: flex; gap: var(--element-gap); margin-bottom: var(--spacing-16); }
.card-brand {
  background: var(--color-fog);
  border-radius: var(--radius-badges);
  padding: 4px 8px;
  font-size: var(--text-body-xs);
  font-weight: var(--font-weight-medium);
  color: var(--color-steel);
}

/* Four preset amounts across 350px gives each tile ~80px, which a
   four-figure amount cannot hold. Two rows of two instead — the grid
   is a set of choices, not a scale, so splitting it costs nothing. */
@media (max-width: 480px) {
  .topup-wrap { padding-top: var(--spacing-20); }
  .amount-grid { grid-template-columns: repeat(2, 1fr); }
  .balance-tile,
  .payment-form-card { padding-inline: var(--spacing-20); }
  .balance-amount { font-size: var(--text-heading-md); }
}

@media (pointer: coarse) {
  .amount-tile { min-height: 56px; }
}

/* Success state lives in the shared SUCCESS STATE block above. */

/* ============================================================
   AUTH LAYOUT (login / register)
   A single centred column on Mist. The form is the whole page —
   nothing beside it to look at, argue with or scroll past — so it
   is the same centred, card-on-Mist shape as the wallet top-up
   flow, and arriving signed-out looks like arriving signed-in.
   ============================================================ */
.auth-layout {
  min-height: calc(100dvh - var(--dev-bar-h) - var(--nav-h));
  display: grid;
  grid-template-columns: 1fr;
}
.auth-panel {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--spacing-48) var(--spacing-24);
}
/* Widened from 400 now that the form sits inside a card and has to
   pay for its own padding — the field column keeps the width it had. */
.auth-form-wrap { width: 100%; max-width: 452px; }

/* Same title block as every account page: Switzer 32 over Inter 15,
   so arriving at the storefront signed-out looks like arriving at it
   signed-in. `.page-header`'s 40px top padding belongs to a page
   inside chrome; this column starts at the top of its own panel. */
.auth-head { margin-bottom: var(--spacing-24); }
.auth-head .page-title { margin-bottom: var(--spacing-8); }

/* The form's surface, matching the wallet top-up payment card:
   Snow at the card radius, 24/32 padding. */
.auth-card {
  background: var(--surface-card-white);
  border-radius: var(--radius-cards);
  padding: var(--spacing-24) var(--spacing-32);
}
.social-btn {
  width: 100%;
  padding: 11px 16px;
  border: 1px solid var(--color-pebble);
  border-radius: var(--radius-pill);
  background: var(--color-snow);
  color: var(--color-ink);
  font-size: var(--text-body);
  font-weight: var(--font-weight-medium);
  font-family: var(--font-body);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  transition: border-color var(--duration-micro) ease, transform 160ms var(--ease-deck-out);
}
@media (hover: hover) and (pointer: fine) {
  .social-btn:hover { border-color: var(--color-graphite); }
}
.social-btn:active { transform: scale(0.97); }

.divider-or {
  display: flex;
  align-items: center;
  gap: var(--spacing-12);
  color: var(--color-steel);
  font-size: var(--text-body-sm);
  margin: var(--spacing-20) 0;
}
.divider-or::before, .divider-or::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--color-fog);
}

@media (max-width: 768px) {
  /* A short form centres nicely; a long one should start at the top
     rather than hang off the fold. */
  .auth-layout { min-height: auto; }
  .auth-panel { padding: var(--spacing-24) var(--spacing-20); align-items: flex-start; }
  /* The card's side padding on top of the panel gutter leaves the
     fields in a narrow trench; the surface gives way, the rhythm
     inside it does not. */
  .auth-card { padding: var(--spacing-20) var(--spacing-16); }
}

/* ============================================================
   CARD ARTWORK MASK (locked collectible card face)
   The state every card face is in until its deck is owned.

   A Deep Forest plate covers the artwork, with the card's suite
   die-cut out of it — a suite-shaped window onto the art, rimmed
   by the lit edge (images/masks/<key>-edge.svg, a radial white→
   black stroke on a 30° axis). The Freeroll carries no suite, so
   it cuts the brand R mark and stamps a "Freeroll" tag under it.
   Card faces stay pure artwork otherwise: that tag is the one
   piece of type on a face, and it belongs to the lock, not to the
   art (DESIGN.md → Collectible Deck Tile).

   The window is the only place artwork pixels are ever painted —
   a CSS mask over a background image, so there is no <img> to
   right-click, drag out or long-press. Markup: js/card-mask.js
   `cardFaceHTML`. Faces drop into any positioned card box
   (.deck-card, .carousel-card-inner, .deck-viewer-card,
   .pack-row-card) as a straight swap for the <img>.

   Both assets are 3:4 and fitted with `contain`, so the cut and
   its rim stay registered to each other on card boxes that are
   not exactly 3:4 (the deck-tile stack is a little narrower).
   ============================================================ */
.card-face {
  position: absolute;
  inset: 0;
  display: block;
  border-radius: inherit;
  overflow: hidden;
  background: var(--color-deep-forest);
  container-type: inline-size;
  /* The artwork is not the buyer's yet: nothing here is a drag
     source, a selection or a long-press save target. */
  pointer-events: none;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
  -webkit-user-drag: none;
}

/* Die-cut registry — one pair per key in js/card-mask.js.
   Adding a suite means adding its two assets and one line here. */
.card-face[data-mask="diamonds"] { --card-cut: url('/images/masks/diamonds-cut.svg'); --card-edge: url('/images/masks/diamonds-edge.svg'); }
.card-face[data-mask="clubs"]    { --card-cut: url('/images/masks/clubs-cut.svg');    --card-edge: url('/images/masks/clubs-edge.svg'); }
.card-face[data-mask="hearts"]   { --card-cut: url('/images/masks/hearts-cut.svg');   --card-edge: url('/images/masks/hearts-edge.svg'); }
.card-face[data-mask="spades"]   { --card-cut: url('/images/masks/spades-cut.svg');   --card-edge: url('/images/masks/spades-edge.svg'); }
.card-face[data-mask="freeroll"] { --card-cut: url('/images/masks/freeroll-cut.svg'); --card-edge: url('/images/masks/freeroll-edge.svg'); }

.card-face__window {
  position: absolute;
  inset: 0;
  -webkit-mask-image: var(--card-cut);
          mask-image: var(--card-cut);
  -webkit-mask-size: contain;
          mask-size: contain;
  -webkit-mask-position: center;
          mask-position: center;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
}

/* Separate from the window so the artwork can breathe on hover
   (matching .deck-card img) without moving the cut with it. */
.card-face__art {
  position: absolute;
  inset: 0;
  background-image: var(--card-art);
  background-size: cover;
  background-position: center;
  transform: scale(1.025);
  transition: transform var(--duration-deck-image) var(--ease-deck-out);
}

.card-face__edge {
  position: absolute;
  inset: 0;
  background-image: var(--card-edge);
  background-size: contain;
  background-position: center;
  background-repeat: no-repeat;
}

/* Freeroll tag — sits in the clear band under the R mark. Sized in
   container units so it holds on a 300px carousel card and a 60px
   pack-row fan card alike. */
.card-face__tag {
  position: absolute;
  top: 78%;
  left: 50%;
  transform: translateX(-50%);
  z-index: 2;
  padding: 0.36em 0.85em;
  border-radius: var(--radius-pill);
  border: 1px solid rgba(var(--color-brand-highlight-rgb), .3);
  font-family: var(--font-body);
  font-size: clamp(6px, 5.4cqw, 12px);
  font-weight: var(--font-weight-semibold);
  letter-spacing: .14em;
  line-height: 1;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--color-mint-ice-cream);
}

/* Thumbnail context (basket rows). A card is portrait, so this is the
   portrait counterpart of base.css's .thumb-md rather than a squashed
   crop of one; it holds a face at either lock state. */
.card-face-thumb {
  position: relative;
  width: 60px;
  aspect-ratio: 3 / 4;
  border-radius: var(--radius-xl);
  overflow: hidden;
  flex-shrink: 0;
}
.card-face-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* Below a ~76px face the stamp can no longer be a legible 6px word
   without running past the card edge — the pack-row fan on a phone.
   The R mark still says Freeroll on its own, and every context that
   shrinks a face this far already names the deck in type beside it. */
@container (max-width: 76px) {
  .card-face__tag { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .card-face__art { transition: none; }
}

/* ============================================================
   COLLECTIBLE DECK TILE
   Behavioral reference: user-iterated deck component; visual
   tokens per DESIGN.md (Race Green/Deep Forest stage, Snow
   shell, Ember/Blush Pink accents, Switzer/Inter type).
   ============================================================ */
/* Tiles in a row stretch to the tallest, and the chain below them is
   a full-height flex column so the price/ticket row and any actions
   land on one line across the row. Without it a two-line deck name
   drops its own footer 27px lower than its neighbours', which reads
   as a broken grid rather than as a longer title. */
.deck-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  align-items: stretch;
  justify-items: center;
  justify-content: center;
  column-gap: var(--spacing-24);
  row-gap: var(--spacing-24);
}
.deck-grid > .deck-instance,
.deck-grid > .deck-instance > .premium-deck-card,
.deck-grid > .deck-instance > .premium-deck-card > .product-link-area { height: 100%; }
.deck-grid > .deck-instance > .premium-deck-card,
.deck-grid > .deck-instance > .premium-deck-card > .product-link-area { display: flex; flex-direction: column; }
.deck-grid .deck-footer { flex: 1; }
.deck-grid .deck-footer-row { margin-top: auto; }

.deck-instance {
  /* Per-deck accents may be overridden via inline custom props */
  --stage-bg:
    radial-gradient(circle at 50% 20%, rgba(255, 255, 255, .12), transparent 30%),
    linear-gradient(180deg, var(--deck-stage-primary) 0%, var(--color-ink) 52%, var(--deck-stage-secondary) 100%);
  --badge-bg: var(--color-graphite);
  --deck-glow: rgba(63, 63, 70, .42);
  flex: 0 0 auto;
  width: 100%;
  max-width: 360px;
}

/* Rare / limited edition — Blush Pink wash */
.deck-instance.is-limited {
  --stage-bg:
    radial-gradient(circle at 50% 18%, rgba(255, 255, 255, .30), transparent 30%),
    linear-gradient(180deg, var(--color-blush-pink) 0%, color-mix(in oklab, var(--color-blush-pink), var(--color-deep-forest) 55%) 55%, var(--color-deep-forest) 130%);
  --badge-bg: var(--color-blush-pink);
  --deck-glow: rgba(var(--color-brand-secondary-rgb), .25);
}

.premium-deck-card {
  width: 100%;
  border-radius: var(--radius-cards);
  background: var(--color-snow);
  box-shadow: var(--shadow-flat-hairline);
  padding: var(--element-gap);
  position: relative;
}

.product-link-area {
  display: block;
  border-radius: calc(var(--radius-cards) - 6px);
  color: inherit;
  text-decoration: none;
  outline: none;
  cursor: pointer;
}
@media (hover: hover) and (pointer: fine) {
  .product-link-area:hover { text-decoration: none; }
}
.product-link-area:focus-visible { box-shadow: 0 0 0 3px rgba(var(--color-brand-primary-strong-rgb), .16); }

.deck-stage {
  position: relative;
  aspect-ratio: 4 / 5;
  overflow: hidden;
  border-radius: var(--radius-deck-stage);
  background: var(--stage-bg);
  display: grid;
  place-items: center;
  isolation: isolate;
  cursor: pointer;
  perspective: 1200px;
  border: 1px solid rgba(var(--color-brand-highlight-rgb), .08);
}

.deck-stage::before {
  content: "";
  position: absolute;
  inset: 18px;
  border-radius: calc(var(--radius-deck-stage) - 6px);
  border: 1px solid rgba(255, 255, 255, .12);
  pointer-events: none;
  z-index: 1;
}

.deck-stage::after {
  content: "";
  position: absolute;
  width: 230px;
  height: 230px;
  left: 50%;
  top: 48%;
  transform: translate(-50%, -50%);
  border-radius: var(--radius-pill);
  background: rgba(255, 255, 255, .055);
  filter: blur(4px);
  opacity: .75;
  pointer-events: none;
  z-index: 0;
  transition: transform .35s var(--ease-deck-soft), opacity .35s var(--ease-deck-soft);
}

.magnetic-layer {
  position: relative;
  width: 100%;
  height: 100%;
  display: grid;
  place-items: center;
  transform-style: preserve-3d;
  transition: transform .28s var(--ease-deck-soft);
  z-index: 2;
}

.deck-stack {
  position: relative;
  width: 68%;
  height: 78%;
  transform: scale(1.02);
  transform-style: preserve-3d;
  transition: transform var(--duration-deck-fan) var(--ease-deck-out);
}

.deck-shadow {
  position: absolute;
  left: 50%;
  bottom: -34px;
  width: 92%;
  height: 38px;
  transform: translateX(-50%);
  border-radius: 50%;
  background: rgba(0, 0, 0, .38);
  filter: blur(16px);
  opacity: .58;
  transition: transform .45s var(--ease-deck-soft), opacity .45s var(--ease-deck-soft);
}

.deck-card {
  position: absolute;
  inset: 0;
  border-radius: var(--radius-cards-compact);
  overflow: hidden;
  background: var(--deck-card-paper);
  border: 1px solid rgba(255, 255, 255, .45);
  box-shadow: var(--shadow-deck);
  transform-origin: 50% 88%;
  transform-style: preserve-3d;
  transition: transform var(--duration-deck-fan) var(--ease-deck-out), filter .45s var(--ease-deck-soft), box-shadow .45s var(--ease-deck-soft);
  will-change: transform;
  backface-visibility: hidden;
}

.deck-card:nth-of-type(1) { transform: translate3d(-3px, 9px, 0) rotate(-4deg); z-index: 1; filter: brightness(.78) saturate(.92); }
.deck-card:nth-of-type(2) { transform: translate3d(0, 6px, 22px) rotate(-1.8deg); z-index: 2; filter: brightness(.86) saturate(.96); }
.deck-card:nth-of-type(3) { transform: translate3d(3px, 3px, 44px) rotate(1.4deg); z-index: 3; filter: brightness(.94) saturate(1); }
.deck-card:nth-of-type(4) { transform: translate3d(0, 0, 66px) rotate(0deg); z-index: 4; }

.deck-card img, .carousel-card img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  user-select: none;
  -webkit-user-drag: none;
  transform: scale(1.025);
  transition: transform var(--duration-deck-image) var(--ease-deck-out), filter .35s var(--ease-deck-soft);
}

/* Suit pip — each stack card carries its suite as an icon-only
   corner chip (the deck-viewer suit chip recipe, no label). Cards
   fan with the highest suite in front, so its pip reads on top. */
.deck-card-suit {
  position: absolute;
  top: 10px;
  right: 10px;
  z-index: 4;               /* above the holo foil sheen (z 3) */
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--deck-glass-border);
  background: rgba(var(--color-brand-primary-strong-rgb), .38);
  -webkit-backdrop-filter: blur(var(--glass-blur));
  backdrop-filter: blur(var(--glass-blur));
  color: var(--color-blush-pink);
  pointer-events: none;
}

/* THE FAN IS A CURSOR'S REWARD, AND ONLY A CURSOR'S. A touch device
   latches :hover on the first tap and holds it until something else
   is tapped — so an ungated fan opens on the tap that was meant to
   open the deck, and then stays open behind whatever the tap
   navigated to. The touch answer is the slider and the always-on
   "View deck" pill below, not a fan nobody can close. */
@media (hover: hover) and (pointer: fine) {
  .deck-stage:hover::after { transform: translate(-50%, -54%) scale(1.12); opacity: 1; }
  .deck-stage:hover .deck-stack { transform: translateY(-24px) scale(.92) rotateX(4deg); }
  .deck-stage:hover .deck-shadow { transform: translateX(-50%) scale(1.18); opacity: .42; }
  .deck-stage:hover .deck-card:nth-of-type(1) { transform: translate3d(-70px, 18px, 0) rotate(-15deg); }
  .deck-stage:hover .deck-card:nth-of-type(2) { transform: translate3d(-28px, -10px, 42px) rotate(-6deg); }
  .deck-stage:hover .deck-card:nth-of-type(3) { transform: translate3d(34px, -10px, 84px) rotate(7deg); }
  .deck-stage:hover .deck-card:nth-of-type(4) { transform: translate3d(76px, 20px, 126px) rotate(16deg); }
  .deck-stage:hover .deck-card img,
  .deck-stage:hover .deck-card .card-face__art { transform: scale(1.05); }
}

/* 2-card fan — override nth-of-type defaults so card 2 is the front card */
.deck-instance[data-card-count="2"] .deck-card:nth-of-type(1) { transform: translate3d(-4px, 8px, 0) rotate(-5deg); z-index: 1; filter: brightness(.82) saturate(.94); }
.deck-instance[data-card-count="2"] .deck-card:nth-of-type(2) { transform: translate3d(0, 0, 44px) rotate(0deg); z-index: 4; filter: none; }
@media (hover: hover) and (pointer: fine) {
  .deck-instance[data-card-count="2"] .deck-stage:hover .deck-card:nth-of-type(1) { transform: translate3d(-44px, 16px, 0) rotate(-14deg); }
  .deck-instance[data-card-count="2"] .deck-stage:hover .deck-card:nth-of-type(2) { transform: translate3d(38px, 16px, 44px) rotate(12deg); }
}

/* 1-card deck — nothing to fan. The card rests centred at full
   brightness; hover raises it slightly (0.45s on the deck ease-out,
   snappier than the multi-card fan). */
.deck-instance[data-card-count="1"] .deck-card:nth-of-type(1) {
  transform: none;
  z-index: 4;
  filter: none;
  transition-duration: 0.45s;
}
.deck-instance[data-card-count="1"] .deck-stack { transition-duration: 0.45s; }
@media (hover: hover) and (pointer: fine) {
  .deck-instance[data-card-count="1"] .deck-stage:hover .deck-stack { transform: translateY(-6px) scale(1.02); }
  .deck-instance[data-card-count="1"] .deck-stage:hover .deck-card:nth-of-type(1) { transform: translateY(-8px); }
}

/* 3-card fan — override so card 3 is the front card */
.deck-instance[data-card-count="3"] .deck-card:nth-of-type(1) { transform: translate3d(-3px, 9px, 0) rotate(-4deg); z-index: 1; filter: brightness(.78) saturate(.92); }
.deck-instance[data-card-count="3"] .deck-card:nth-of-type(2) { transform: translate3d(0, 6px, 22px) rotate(-1.8deg); z-index: 2; filter: brightness(.86) saturate(.96); }
.deck-instance[data-card-count="3"] .deck-card:nth-of-type(3) { transform: translate3d(0, 0, 44px) rotate(0deg); z-index: 4; filter: none; }
@media (hover: hover) and (pointer: fine) {
  .deck-instance[data-card-count="3"] .deck-stage:hover .deck-card:nth-of-type(1) { transform: translate3d(-58px, 16px, 0) rotate(-14deg); }
  .deck-instance[data-card-count="3"] .deck-stage:hover .deck-card:nth-of-type(2) { transform: translate3d(0px, -10px, 42px) rotate(-2deg); }
  .deck-instance[data-card-count="3"] .deck-stage:hover .deck-card:nth-of-type(3) { transform: translate3d(54px, 16px, 84px) rotate(12deg); }
}

/* Value flag — the ONE badge a deck tile carries ("Best value",
   "Most popular", …), pinned top-left of the stage in Ember. Card
   faces themselves are pure artwork: no text, no badges. */
.deck-flag {
  position: absolute;
  top: 14px;
  left: 14px;
  z-index: 6;
  pointer-events: none;
}

/* ── Holographic treatment — collectible cards ONLY ────────── */
.is-holographic::before {
  content: "";
  position: absolute;
  inset: -1px;
  border-radius: inherit;
  background:
    radial-gradient(circle at var(--mx, 50%) var(--my, 50%), rgba(255, 255, 255, .75), transparent 22%),
    linear-gradient(125deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, .25) 28%, rgba(255, 255, 255, 0) 48%),
    linear-gradient(45deg, rgba(var(--color-brand-secondary-rgb), .22), rgba(var(--color-brand-highlight-rgb), .14), rgba(255, 255, 255, .12));
  mix-blend-mode: screen;
  opacity: .16;
  pointer-events: none;
  z-index: 3;
  transition: opacity .35s var(--ease-deck-soft);
}

.foil-sheen {
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: .14;
  background: linear-gradient(105deg, transparent 18%, rgba(255, 255, 255, .75) 44%, transparent 64%);
  transform: translateX(-140%);
  z-index: 3;
}

@media (hover: hover) and (pointer: fine) {
  .deck-stage:hover .deck-card.is-holographic::before,
  .carousel-card:hover .carousel-card-inner.is-holographic::before { opacity: .34; }
  .deck-stage:hover .deck-card.is-holographic .foil-sheen,
  .carousel-card:hover .foil-sheen { animation: sheen 1.45s var(--ease-deck-soft); }
}

@keyframes sheen { to { transform: translateX(140%); } }

/* ── View deck glass pill ──────────────────────────────────── */
.view-deck-button {
  position: absolute;
  left: 50%;
  bottom: 22px;
  transform: translateX(-50%) translateY(10px);
  z-index: 8;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--element-gap);
  min-height: 42px;
  padding: 12px 16px;
  -webkit-user-select: none; user-select: none;
  border: 1px solid var(--deck-glass-border);
  border-radius: var(--radius-pill);
  background: var(--glass-fill-dark);
  color: var(--color-mint-ice-cream);
  font-family: var(--font-body);
  font-size: var(--text-body);
  line-height: 1;
  font-weight: var(--font-weight-medium);
  white-space: nowrap;
  box-shadow: none;
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(1.25);
  backdrop-filter: blur(var(--glass-blur)) saturate(1.25);
  opacity: 0;
  cursor: pointer;
  /* The reveal is the deck's own slower clock; the PRESS is not.
     A control that answers a thumb 240ms later has already been
     tapped twice (DESIGN.md → Motion Philosophy). */
  transition: opacity .24s var(--ease-deck-soft), transform 160ms var(--ease-deck-out), background .24s var(--ease-deck-soft), border-color .24s var(--ease-deck-soft);
  appearance: none;
  -webkit-appearance: none;
  text-decoration: none;
}
.view-deck-button:focus-visible {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}
@media (hover: hover) and (pointer: fine) {
  .deck-stage:hover .view-deck-button {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
  }
}
@media (hover: hover) and (pointer: fine) {
  .view-deck-button:hover { background: rgba(255, 255, 255, .12); border-color: var(--glass-border-light); }
}
.view-deck-button svg { width: 16px; height: 16px; flex: 0 0 auto; }

.view-deck-button:active { transform: translateX(-50%) scale(0.96); }

/* Touch devices have no hover — keep the button reachable, and take
   it the last two pixels to the 44px floor while we are here. */
@media (hover: none) {
  .view-deck-button { opacity: 1; transform: translateX(-50%) translateY(0); }
}
@media (pointer: coarse) {
  .view-deck-button { min-height: var(--touch-target); }
  .view-deck-button:active { transform: translateX(-50%) scale(0.96); }
}

/* ── Deck footer ───────────────────────────────────────────── */
.deck-footer {
  padding: var(--spacing-20) var(--spacing-16) var(--spacing-12);
  display: flex;
  flex-direction: column;
  gap: var(--spacing-12);
}
/* Price row — € price (left) and the ticket tag (right, in line) */
.deck-footer-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--spacing-12);
}
.deck-copy { min-width: 0; }
/* Deck name — h3 heading role, matching the sticky-rail #right-title
   (font-display, --text-heading-sm, semibold); margin reset so the
   description's own margin-top owns the gap. */
.deck-label {
  margin: 0;
  color: var(--color-obsidian);
  font-family: var(--font-display);
  font-size: var(--text-heading-sm);
  line-height: var(--leading-heading-sm);
  font-weight: var(--font-weight-semibold);
  letter-spacing: var(--ls-rg);
  /* A collection tile leads with the draw's own name, which runs far
     longer than a tier label — capped so one long prize name cannot
     set the height of an entire grid row. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.deck-description {
  margin: 4px 0 0;
  color: var(--color-steel);
  font-size: var(--text-body-xs);
  line-height: 1.56;
  font-weight: var(--font-weight-regular);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
/* Ticket tag — draw entries, sitting on the price row (right-aligned).
   Same colourway + light play as the showcase "Win this …" eyebrow:
   Mint Ice Cream fill, Race Green ticket icon + count, a screen-blended
   lengthwise sheen (::before) and a perimeter glint (::after), looped. */
.deck-ticket-tag {
  flex: 0 0 auto;
  margin-left: auto;       /* pins the tag to the right, in line with the price */
  position: relative;
  isolation: isolate;      /* contains the screen blend; ::before above fill, below text */
  overflow: hidden;        /* clips the sheen to the pill silhouette */
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 10px;
  border-radius: var(--radius-badges);
  background: var(--color-mint-ice-cream);
  white-space: nowrap;
}
.deck-ticket-tag .icon {
  width: 15px;
  height: 15px;
  flex: 0 0 auto;
  color: var(--color-race-green);
}
.deck-ticket-tag__count {
  font-size: 11px;
  line-height: 1.35;
  font-weight: var(--font-weight-semibold);
  letter-spacing: 0.02em;
  color: var(--color-race-green);
}
/* Lengthwise sheen — screen-blended luminance running across the mint fill */
.deck-ticket-tag::before {
  content: "";
  position: absolute;
  top: -2px;
  bottom: -2px;
  left: 0;
  width: 52%;
  z-index: -1;
  pointer-events: none;
  opacity: 0;
  mix-blend-mode: screen;
  transform: translateX(-210%) skewX(-14deg);
  filter: blur(0.3px);
  background: linear-gradient(100deg,
      transparent 8%,
      rgba(110, 200, 255, 0.42) 23%,   /* cyan       */
      rgba(130, 255, 205, 0.52) 35%,   /* mint       */
      rgba(255, 255, 255, 0.95) 50%,   /* white core */
      rgba(255, 226, 150, 0.50) 60%,   /* gold       */
      rgba(255, 150, 240, 0.58) 70%,   /* orchid     */
      rgba(180, 150, 255, 0.44) 80%,   /* violet     */
      transparent 92%);
}
/* Perimeter glint — bright conic arc masked to a 2px stroke ring */
.deck-ticket-tag::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  opacity: 0;
  border-radius: inherit;
  padding: 2px;
  filter: blur(0.6px) brightness(1.05);
  background: conic-gradient(from var(--se-shine),
      transparent 0deg,
      transparent 30deg,
      rgba(110, 200, 255, 0.50) 42deg,  /* cyan   */
      rgba(130, 255, 205, 0.60) 52deg,  /* mint   */
      rgba(255, 255, 255, 1) 62deg,     /* core   */
      rgba(255, 226, 150, 0.60) 72deg,  /* gold   */
      rgba(255, 150, 240, 0.65) 82deg,  /* orchid */
      rgba(180, 150, 255, 0.50) 92deg,  /* violet */
      transparent 104deg,
      transparent 360deg);
  -webkit-mask:
      linear-gradient(#000 0 0) content-box,
      linear-gradient(#000 0 0);
          mask:
      linear-gradient(#000 0 0) content-box,
      linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
}
@media (prefers-reduced-motion: no-preference) {
  /* one sweep + one glint pass, then a rest before the loop repeats */
  .deck-ticket-tag::before { animation: deck-ticket-sheen 5s linear infinite; }
  .deck-ticket-tag::after  { animation: deck-ticket-ring  5s linear infinite; }
}
/* Light runs the length of the pill, then rests */
@keyframes deck-ticket-sheen {
  0%   { opacity: 0; transform: translateX(-210%) skewX(-14deg); }
  4%   { opacity: 1; }
  20%  { opacity: 1; }
  24%  { opacity: 0; transform: translateX(360%) skewX(-14deg); }
  100% { opacity: 0; transform: translateX(360%) skewX(-14deg); }
}
/* Glint travels once around the stroke, then rests */
@keyframes deck-ticket-ring {
  0%   { opacity: 0; --se-shine: 0deg; }
  5%   { opacity: 1; }
  19%  { opacity: 1; }
  24%  { opacity: 0; --se-shine: 360deg; }
  100% { opacity: 0; --se-shine: 360deg; }
}

/* Wide ticket tile — the .deck-ticket-tag mint pill blown up to a
   full-width line item (purchase modal): same mint fill + iridescent
   sheen, sized to sit among the price rows, and carries no price. */
.deck-ticket-tag--wide {
  margin-left: 0;
  width: 100%;
  justify-content: center;
  gap: var(--spacing-8);
  padding: var(--spacing-12) var(--spacing-16);
  border-radius: var(--radius-inputs);
}
.deck-ticket-tag--wide .icon { width: 18px; height: 18px; }
.deck-ticket-tag--wide .deck-ticket-tag__count { font-size: var(--text-body); letter-spacing: 0.01em; }

/* ============================================================
   PURCHASE — express payment + low-funds action
   Wallet is the default. The low-funds alert carries its own
   right-aligned top-up button, and a faster-checkout block offers
   Apple / Google Pay (mobile) plus card, besides the wallet.
   ============================================================ */
.alert-action { margin-left: auto; align-self: center; flex: 0 0 auto; white-space: nowrap; }

.pay-or {
  display: flex; align-items: center; gap: var(--spacing-12);
  margin: var(--spacing-20) 0 var(--spacing-16);
  font-size: var(--text-body-sm); color: var(--color-steel);
}
.pay-or::before, .pay-or::after { content: ""; flex: 1; height: 1px; background: var(--color-fog); }
.pay-express-row { display: flex; flex-direction: column; gap: var(--spacing-8); }
.pay-express-row .btn { width: 100%; }
.pay-option { gap: var(--spacing-8); }
.pay-option .icon { width: 18px; height: 18px; }
.pay-apple  { background: var(--color-deep-forest); color: var(--color-mint-ice-cream); border-color: transparent; }
@media (hover: hover) and (pointer: fine) {
  .pay-apple:hover { color: var(--color-mint-ice-cream); }
}
.pay-google, .pay-card { background: var(--color-snow); color: var(--color-ink); border: 1px solid var(--color-pebble); }
@media (hover: hover) and (pointer: fine) {
  .pay-google:hover, .pay-card:hover { border-color: var(--color-deep-forest); }
}
.pay-mobile-only { display: none; }
@media (max-width: 768px) { .pay-mobile-only { display: inline-flex; } }

.deck-price {
  font-family: var(--font-display); letter-spacing: var(--ls-heading-sm);
  font-size: var(--text-heading-sm);
  font-weight: var(--font-weight-bold);
  color: var(--color-obsidian);
}

.deck-actions { display: flex; align-items: center; justify-content: space-between; gap: var(--spacing-12); padding: 0 var(--spacing-16) var(--spacing-16); }

/* Stacked variant: meta row (price/badges) on top, full-width action below */
.deck-actions--stacked { flex-direction: column; align-items: stretch; gap: var(--spacing-12); }

@media (max-width: 760px) {
  .deck-grid { grid-template-columns: minmax(0, 360px); column-gap: 0; }
}

/* ── Phone swipe rail modifier ─────────────────────────────────
   A deck tile is a tall object — stage, footer and a buy row — so
   a column of them on a phone is one tile per screen and a long
   scroll to find out there were five. Below 480px a .deck-grid
   carrying this class becomes an edge-bled scroll-snap rail
   instead. Above that the grid already fits two or more tiles
   across and needs no rail.

   Snap is `center`, not `start` like .rail-mobile: the tile is
   narrower than the scrollport by design, so the decks either
   side stay visible at the edges and the rail reads as having
   more in it. The width is a percentage rather than a pixel cap
   so that peek stays the same fraction of the screen on every
   phone; capping it in px would leave a 360px tile marooned in
   the middle of a 430px screen.

   The 20px padding matches the mobile container gutter, so the
   first tile still lines up with the copy above it while the
   rail itself bleeds to the screen edges. */
@media (max-width: 480px) {
  .deck-grid--swipe {
    display: flex;
    /* Overrides the grid's `start`: tiles run to the tallest so the
       rail has one baseline (see the action-row rules below). */
    align-items: stretch;
    /* The grid centres its tracks; carried into flex that centres the
       OVERFLOW too, parking the first tile in negative scroll space
       the rail can never reach. */
    justify-content: flex-start;
    gap: var(--spacing-16);
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory;
    scrollbar-width: none;
    width: 100vw;
    margin-left: calc(50% - 50vw);
    padding: 0 var(--spacing-20) var(--spacing-12);
  }
  .deck-grid--swipe::-webkit-scrollbar { display: none; }
  .deck-grid--swipe > * {
    flex: 0 0 78%;
    min-width: 0;
    scroll-snap-align: center;
    display: flex;
  }

  /* The tile is otherwise sized for a full-bleed column below 430px
     (min(360px, 100vw - 36px)) — wider than the rail's slot, so every
     tile is drawn overlapping the one after it. In a rail the slot is
     the width. */
  .deck-grid--swipe .premium-deck-card {
    width: 100%;
    display: flex;
    flex-direction: column;
  }

  /* Equal-height tiles with their action row on a shared baseline. A
     rail puts the neighbours in view either side, so a Freeroll ending
     short of the deck beside it reads as a broken card rather than as
     a smaller one — and it does end short, because a paid tile's buy
     row stacks its stepper over its button below 480px while the
     Freeroll's single claim button stays one line. Stretching to the
     tallest and letting the link area absorb the slack pins the
     actions to the bottom, so every Buy/Claim sits on one line while
     the stage, name, description and price row — all identical
     heights — stay aligned across the rail. The gap lands in the one
     place nothing has to line up: between the footer and the
     buttons. */
  .deck-grid--swipe .product-link-area {
    flex: 1;
    display: flex;
    flex-direction: column;
  }
}

/* ============================================================
   GOLDEN TICKET REVEAL
   The instant-win moment (DESIGN.md → Golden Ticket Reveal).
   A golden ticket is a collectible object, not UI chrome, so it
   is presented the way every other collectible is: on the deck
   tile's own stage — 22px radius, Deep Forest/Race Green
   gradient, inner hairline frame, clipped — with the ticket
   rising out of the floor on the deck fan's exact motion
   signature (--duration-deck-fan on --ease-deck-out). At rest it
   carries the .deck-ticket-tag light play verbatim, reusing the
   shared deck-ticket-sheen / deck-ticket-ring keyframes on the
   same 5s cadence, retuned to the gold foil ramp. The gold lives
   only inside this block — see the --gold-foil-* tokens.
   Sits inside a standard .modal-box, so the dialog chrome,
   mobile bottom sheet and dismiss behaviour are the modal's.
   ============================================================ */
/* Narrower than a standard dialog: this one carries a single object
   and a sentence, and at the shared 520px the stage sprawls and the
   ticket stops being the thing you are looking at. */
.golden-reveal {
  max-width: 400px;
  padding: var(--spacing-16) var(--spacing-16) var(--spacing-28);
}

.golden-stage {
  position: relative;
  aspect-ratio: 1 / 1;
  display: grid;
  place-items: center;
  /* The deck stage's own depth, so the ticket's entrance twist is a
     real turn in space rather than a horizontal squash. */
  perspective: 1200px;
  overflow: hidden;                 /* the slot the ticket rises out of */
  isolation: isolate;
  border-radius: var(--radius-deck-stage);
  border: 1px solid var(--deck-glass-border);
  background:
    radial-gradient(circle at 50% 56%, color-mix(in srgb, var(--gold-foil-glow) 42%, transparent), transparent 58%),
    radial-gradient(circle at 50% 20%, var(--glass-fill-dark), transparent 30%),
    linear-gradient(180deg, var(--deck-stage-primary) 0%, var(--color-ink) 52%, var(--deck-stage-secondary) 100%);
}
/* Inner hairline frame — the .deck-stage inset border */
.golden-stage::before {
  content: "";
  position: absolute;
  inset: 14px;
  border-radius: calc(var(--radius-deck-stage) - 6px);
  border: 1px solid var(--glass-border-light);
  pointer-events: none;
  z-index: 4;
}

/* Halo — the gold bloom that opens behind the ticket as it lands */
.golden-stage__halo {
  position: absolute;
  left: 50%;
  top: 52%;
  width: 88%;
  aspect-ratio: 1;
  border-radius: var(--radius-pill);
  background: radial-gradient(circle,
      var(--gold-foil-glow) 0%,
      color-mix(in srgb, var(--gold-foil-glow) 52%, transparent) 34%,
      transparent 70%);
  filter: blur(22px);
  transform: translate(-50%, -50%) scale(0.84);
  opacity: 0;
  pointer-events: none;
  z-index: 1;
  will-change: transform, opacity;
}

/* ── The ticket ────────────────────────────────────────────── */
.golden-ticket {
  /* Translucent foil ramp: the gold reads as glass over the stage
     rather than as a flat printed swatch. */
  --foil-a: color-mix(in srgb, var(--gold-foil-highlight) 92%, transparent);
  --foil-b: color-mix(in srgb, var(--gold-foil-light) 88%, transparent);
  --foil-c: color-mix(in srgb, var(--gold-foil-mid) 86%, transparent);
  --foil-d: color-mix(in srgb, var(--gold-foil-deep) 90%, transparent);

  /* ── Die-cut geometry ──────────────────────────────────────
     The notches are real holes, not dark discs painted on top,
     so the stage shows through them and — the reason it matters —
     the perimeter glint can be masked to the same silhouette and
     travel *around* each notch instead of straight across it.
     The notch centres sit on the perforation, so the stub has a
     fixed height and both read off the one number. */
  --gt-notch: var(--radius-lg);        /* notch radius */
  /* Perforation → bottom edge, 12% up from the base 40px step. The
     stub is the only fixed band; the face takes what is left, so the
     plate's total height does not move. */
  --gt-stub-h: calc(var(--spacing-40) * 1.12);
  --gt-ring: 3px;                      /* glint stroke width */
  /* The cut itself, one gradient per end of the perforation:
     transparent inside the notch, opaque everywhere else. */
  --gt-cut-l: radial-gradient(circle at 0 calc(100% - var(--gt-stub-h)),
      transparent calc(var(--gt-notch) - 0.5px), #000 calc(var(--gt-notch) + 0.5px));
  --gt-cut-r: radial-gradient(circle at 100% calc(100% - var(--gt-stub-h)),
      transparent calc(var(--gt-notch) - 0.5px), #000 calc(var(--gt-notch) + 0.5px));
  /* The same cuts grown by the stroke width, so the glint keeps an
     even weight as it rounds the arc. */
  --gt-cut-l-in: radial-gradient(circle at 0 calc(100% - var(--gt-stub-h)),
      transparent calc(var(--gt-notch) + var(--gt-ring) - 0.5px), #000 calc(var(--gt-notch) + var(--gt-ring) + 0.5px));
  --gt-cut-r-in: radial-gradient(circle at 100% calc(100% - var(--gt-stub-h)),
      transparent calc(var(--gt-notch) + var(--gt-ring) - 0.5px), #000 calc(var(--gt-notch) + var(--gt-ring) + 0.5px));

  position: relative;
  z-index: 2;
  isolation: isolate;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  height: 74%;
  aspect-ratio: 2 / 3;
  border-radius: var(--radius-inputs);
  /* The printed rim. It stops where the die-cut starts, which is
     what a punched hole actually looks like — the cut edge is raw. */
  border: 1px solid color-mix(in srgb, var(--gold-foil-highlight) 70%, transparent);
  /* Engraved hairlines over the foil ramp — the security-print
     texture that gives the gold a material rather than a fill. */
  background:
    repeating-linear-gradient(45deg,
      color-mix(in srgb, var(--gold-foil-deep) 11%, transparent) 0 1px,
      transparent 1px 8px),
    linear-gradient(140deg, var(--foil-a) 0%, var(--foil-b) 24%, var(--foil-c) 58%, var(--foil-d) 100%);
  color: var(--gold-foil-ink);
  transform-origin: 50% 100%;        /* hinges out of the slot, not about its middle */
  backface-visibility: hidden;
  opacity: 0;
  will-change: transform, opacity;
  /* Punch both notches out of the ticket and everything in it. */
  -webkit-mask-image: var(--gt-cut-l), var(--gt-cut-r);
          mask-image: var(--gt-cut-l), var(--gt-cut-r);
  -webkit-mask-composite: source-in;
          mask-composite: intersect;
}

/* Specular bloom — the static highlight that makes the foil glassy:
   a soft corner catch plus a narrow raking streak across it. It sits
   ABOVE the wordmark, because it is light on the surface and the
   wordmark is struck into it. */
.golden-ticket__gloss {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 2;
  background:
    radial-gradient(60% 44% at 16% 8%, color-mix(in srgb, var(--color-snow) 58%, transparent) 0%, transparent 72%),
    linear-gradient(118deg,
      transparent 30%,
      color-mix(in srgb, var(--color-snow) 26%, transparent) 42%,
      transparent 54%);
}

/* Lengthwise sheen — the .deck-ticket-tag sweep on the foil ramp:
   same screen-blended luminance, warmed so the core reads as gold
   refracting rather than as the mint pill's cyan. It passes OVER the
   wordmark, which is the whole point of debossing it: the light has
   to cross the letterforms' walls, blowing out the lit lip and
   leaving the shaded one, so the type catches the sweep as relief
   rather than sitting in front of it like a label. */
.golden-ticket::before {
  content: "";
  position: absolute;
  top: -2px;
  bottom: -2px;
  left: 0;
  width: 52%;
  z-index: 3;
  pointer-events: none;
  opacity: 0;
  mix-blend-mode: screen;
  transform: translateX(-210%) skewX(-14deg);
  filter: blur(0.3px);
  background: linear-gradient(100deg,
      transparent 8%,
      rgba(255, 226, 150, 0.40) 23%,   /* gold      */
      rgba(255, 246, 204, 0.62) 35%,   /* highlight */
      rgba(255, 255, 255, 0.95) 50%,   /* white core */
      rgba(255, 246, 204, 0.60) 60%,   /* highlight */
      rgba(255, 150, 240, 0.34) 70%,   /* orchid    */
      rgba(180, 150, 255, 0.26) 80%,   /* violet    */
      transparent 92%);
}
/* Perimeter glint — the same conic arc masked to a 2px stroke ring,
   but cut to the *die-cut* silhouette rather than to the plain
   rounded rectangle, so the light runs around each notch instead of
   straight across it.
   The mask is the ring the ticket's edge actually describes:
       (border box − both notches) − (content box − both grown notches)
   which composites bottom-up as six layers — content box, grown out
   by the stroke at each notch, excluded from the border box to give
   the ring, then the notches themselves taken back out of it. */
.golden-ticket::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 4;
  pointer-events: none;
  opacity: 0;
  border-radius: inherit;
  padding: var(--gt-ring);
  /* A touch more bloom than the mint pill needs: the glint here is
     travelling over foil that is already near-white in places, so it
     has to burn a little brighter to read at all. */
  filter: blur(0.8px) brightness(1.14);
  background: conic-gradient(from var(--se-shine),
      transparent 0deg,
      transparent 28deg,
      rgba(255, 226, 150, 0.72) 40deg,  /* gold      */
      rgba(255, 246, 204, 0.92) 52deg,  /* highlight */
      rgba(255, 255, 255, 1) 62deg,     /* core      */
      rgba(255, 246, 204, 0.92) 72deg,  /* highlight */
      rgba(255, 150, 240, 0.62) 84deg,  /* orchid    */
      rgba(180, 150, 255, 0.48) 94deg,  /* violet    */
      transparent 108deg,
      transparent 360deg);
  -webkit-mask-image:
      var(--gt-cut-r),
      var(--gt-cut-l),
      linear-gradient(#000 0 0),
      var(--gt-cut-r-in),
      var(--gt-cut-l-in),
      linear-gradient(#000 0 0);
          mask-image:
      var(--gt-cut-r),
      var(--gt-cut-l),
      linear-gradient(#000 0 0),
      var(--gt-cut-r-in),
      var(--gt-cut-l-in),
      linear-gradient(#000 0 0);
  -webkit-mask-clip: border-box, border-box, border-box, border-box, border-box, content-box;
          mask-clip: border-box, border-box, border-box, border-box, border-box, content-box;
  -webkit-mask-composite: source-in, source-in, xor, source-in, source-in, source-over;
          mask-composite: intersect, intersect, exclude, intersect, intersect, add;
}

/* Face + stub, split by the perforation. The ticket says one thing —
   what it is — and says it as relief in the metal. Nothing is printed
   on it: what it is worth, and to whom, is the dialog's job, which is
   why one ticket serves free tickets and custom gifts alike. */
.golden-ticket__face {
  position: relative;
  z-index: 1;
  flex: 1 1 auto;
  min-height: 0;
  display: grid;
  place-items: center;
  padding: var(--spacing-16) var(--spacing-12);
  text-align: center;
}
/* Debossed wordmark. The letters are not a colour on the foil — they
   are the foil, struck a shade deeper along the same 140deg axis, so
   only their walls read: a lit lower lip and a shaded upper-left one,
   taking light from the same corner as the specular. Because it is
   relief and not ink, the sweep above it does the rest. */
.golden-ticket__wordmark {
  font-family: var(--font-display);
  letter-spacing: var(--ls-rg);
  font-size: var(--text-heading);
  /* Set as a two-line lockup, not as copy: the scale's display
     leading closes the gap between the words so they read as one
     struck mark. Both words are ascender-only, so nothing clips. */
  line-height: var(--leading-display);
  font-weight: var(--font-weight-bold);
  /* The groove floor: deeper than the foil around it, and shading
     further away from the light, because that is what a recess does.
     A shallow cut — the floor stays close to the surface it sits in,
     so the letters are found rather than announced. */
  color: transparent;
  /* Struck deep into the plate: the same three stops, each darkened
     30% and then 30% again — 49% of the way to black — so the groove
     floor falls well away from the light without leaving the foil. */
  background: linear-gradient(140deg,
      color-mix(in srgb, color-mix(in srgb, var(--gold-foil-mid) 92%, var(--gold-foil-deep)) 51%, #000) 0%,
      color-mix(in srgb, color-mix(in srgb, var(--gold-foil-mid) 62%, var(--gold-foil-deep)) 51%, #000) 52%,
      color-mix(in srgb, color-mix(in srgb, var(--gold-foil-mid) 26%, var(--gold-foil-deep)) 51%, #000) 100%);
  -webkit-background-clip: text;
          background-clip: text;
  /* Each wall of the cut is two strokes, not one: a half-pixel line
     right at the lip, then a soft fall behind it. That is what makes
     the relief read as tooled rather than as a drop shadow — and it
     buys the detail while every peak alpha comes DOWN, which is what
     makes it shallower at the same time. Lit from the top-left like
     everything else on the plate: the lower-right lip catches, the
     upper-left one is in shade. */
  text-shadow:
    -0.5px -0.5px 0    color-mix(in srgb, var(--gold-foil-deep) 40%, transparent),
    -1px   -1px   1.5px color-mix(in srgb, var(--gold-foil-deep) 22%, transparent),
     0.5px  0.5px 0    color-mix(in srgb, var(--gold-foil-highlight) 54%, transparent),
     1px    1.5px 2px  color-mix(in srgb, var(--gold-foil-highlight) 26%, transparent);
}
/* The stub is a fixed height because the die-cut is measured off it
   (see --gt-stub-h): the notch centres have to land on this line.
   The face flexes, so growing the stub moves the perforation up the
   plate rather than making the ticket taller. */
.golden-ticket__stub {
  position: relative;
  z-index: 1;
  flex: 0 0 auto;
  height: var(--gt-stub-h);
  display: grid;
  place-items: center;
  /* The tear line, read as a real perforation: a solid cut above a
     lit lip below, so it sits *in* the foil the way the wordmark
     does rather than being drawn on top of it. */
  border-top: 1px dashed color-mix(in srgb, var(--gold-foil-deep) 96%, transparent);
  box-shadow: 0 1px 0 color-mix(in srgb, var(--gold-foil-highlight) 60%, transparent);
}
/* Edition number, struck into the stub on the wordmark's own axis at
   caption scale — the one thing printed on the plate, and it says
   which of the draw's golden tickets this is, never what it is worth. */
.golden-ticket__serial {
  font-family: var(--font-display);
  font-size: var(--text-caption);
  line-height: var(--leading-caption);
  letter-spacing: 0.08em;
  font-weight: var(--font-weight-semibold);
  text-transform: uppercase;
  color: color-mix(in srgb, var(--gold-foil-deep) 70%, #000);
  text-shadow:
    -0.5px -0.5px 0 color-mix(in srgb, var(--gold-foil-deep) 30%, transparent),
     0.5px  0.5px 0 color-mix(in srgb, var(--gold-foil-highlight) 60%, transparent);
}
.golden-ticket__serial em {
  font-style: italic;
  text-transform: lowercase;
  letter-spacing: 0.04em;
}

/* ── Dialog copy below the stage ───────────────────────────── */
.golden-reveal__body { padding: var(--spacing-24) var(--spacing-12) 0; text-align: center; }

/* ── Motion ────────────────────────────────────────────────── */
/* Three beats, in order: the dialog arrives (the modal's own
   220ms entrance), then the ticket rises out of the floor with
   the halo blooming behind it, then the light play starts as it
   lands. The rise waits one micro-interaction so it begins on a
   dialog that has already settled rather than on one still
   moving. Everything is transform / opacity / filter. */
@media (prefers-reduced-motion: no-preference) {
  /* Two animations, deliberately. The rise owns `transform` and
     nothing else, so its keyframes can be exactly two — one
     uninterrupted eased interpolation, which is the only way the
     travel comes out smooth. The fade owns `opacity` on its own
     shorter clock; folded into the rise it would have needed a third
     keyframe, and any extra keyframe re-segments the transform's
     easing and puts the step back in. */
  .golden-ticket {
    animation:
      golden-ticket-rise var(--duration-deck-fan) var(--ease-deck-soft) var(--duration-micro) both,
      golden-ticket-fade var(--duration-micro) linear var(--duration-micro) both;
  }
  /* The halo starts with the ticket but runs a beat longer and on the
     softer curve: light does not snap into place, and it should still
     be settling after the plate has stopped. */
  .golden-stage__halo {
    animation: golden-halo-bloom calc(var(--duration-deck-fan) + var(--duration-micro))
               var(--ease-deck-soft) var(--duration-micro) both;
  }
  /* The light starts a third of the way up, not on arrival: a sweep
     raking across a plate that is still moving is what sells the
     movement — catch it after it has stopped and the two read as
     separate events. It carries on over the settled ticket. */
  .golden-ticket::before {
    animation: deck-ticket-sheen 5s linear calc(var(--duration-micro) + var(--duration-deck-fan) / 3) infinite;
  }
  .golden-ticket::after {
    animation: deck-ticket-ring 5s linear calc(var(--duration-micro) + var(--duration-deck-fan) / 3) infinite;
  }
}
/* The rise: one move, from below and turned away to square and at
   rest, carried by a single ease-out — fast off the floor, slowing
   all the way into place, never reversing.
   Exactly two keyframes, and transform is the only thing in them.
   Every extra keyframe re-segments the curve and the eye reads the
   segment boundaries as steps, and an animated `filter: blur()`
   rasterises at discrete radii, which steps for the same reason.
   Smoothness here is a matter of leaving things out. */
@keyframes golden-ticket-rise {
  from { transform: translateY(112%) rotateY(-46deg) rotate(-3deg) scale(0.94); }
  to   { transform: translateY(0)    rotateY(0deg)   rotate(0deg)  scale(1); }
}
@keyframes golden-ticket-fade {
  from { opacity: 0; }
  to   { opacity: 1; }
}
/* Same rule as the rise: two keyframes, one direction, no peak to
   fall back from. A reversal at the end reads as a hitch, and the
   glow is the one thing on the stage that must not draw attention
   to its own timing. */
@keyframes golden-halo-bloom {
  from { opacity: 0;    transform: translate(-50%, -50%) scale(0.84); }
  to   { opacity: 0.62; transform: translate(-50%, -50%) scale(1); }
}
/* Reduced motion: the ticket is simply already there. */
@media (prefers-reduced-motion: reduce) {
  .golden-ticket { opacity: 1; }
  .golden-stage__halo { opacity: 0.62; transform: translate(-50%, -50%) scale(1); }
}

/* Below the sheet breakpoint the dialog is already full-width, so the
   narrow desktop shell has to stand down or the sheet stops reaching
   the edges. */
@media (max-width: 768px) {
  .golden-reveal { max-width: none; }
}

/* On a phone the ticket's width follows the stage's height, which
   follows the sheet's width — so the wordmark steps down a notch
   rather than being set at a size the plate cannot hold. */
@media (max-width: 480px) {
  .golden-reveal { padding: var(--spacing-12) var(--spacing-12) var(--spacing-24); }
  .golden-ticket { --gt-stub-h: calc(var(--spacing-36) * 1.12); }
  .golden-ticket__face { padding: var(--spacing-12) var(--spacing-8); }
  .golden-ticket__wordmark { font-size: var(--text-heading-md); }
  .golden-reveal__body { padding: var(--spacing-20) var(--spacing-8) 0; }
}

/* ============================================================
   DECK CAROUSEL OVERLAY
   Full-screen inspection: Deep Forest wash + deck-derived glow,
   draggable track, active/near/far states, dots + counter.
   ============================================================ */
.deck-overlay {
  position: fixed;
  inset: 0;
  z-index: var(--z-deck-overlay);
  display: grid;
  place-items: center;
  align-items: center;
  justify-items: center;
  background:
    radial-gradient(circle at 50% 18%, var(--deck-glow, rgba(63, 63, 70, .42)), transparent 34%),
    var(--deck-overlay-wash);
  -webkit-backdrop-filter: blur(24px);
  backdrop-filter: blur(24px);
  opacity: 0;
  pointer-events: none;
  transition: opacity .32s var(--ease-deck-soft);
}
.deck-instance.is-open .deck-overlay,
.deck-overlay.is-open {
  opacity: 1;
  pointer-events: auto;
}

.deck-overlay .overlay-close {
  position: fixed;
  /* The only way out of a full-screen overlay must not be under the
     notch or the Dynamic Island. */
  top: calc(28px + env(safe-area-inset-top, 0px));
  right: calc(28px + env(safe-area-inset-right, 0px));
  z-index: 70;
  width: 44px;
  height: 44px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--deck-glass-border);
  background: var(--glass-fill-dark);
  color: var(--color-mint-ice-cream);
  display: grid;
  place-items: center;
  box-shadow: none;
  -webkit-backdrop-filter: blur(var(--glass-blur));
  backdrop-filter: blur(var(--glass-blur));
  cursor: pointer;
}
@media (hover: hover) and (pointer: fine) {
  .deck-overlay .overlay-close:hover { background: rgba(255, 255, 255, .12); border-color: var(--glass-border-light); }
}
/* The only way out of a full-screen overlay, and it had nothing to
   say to a thumb. Declared on both close controls below through one
   selector, because they are the same control in two overlays. */
.deck-overlay .overlay-close svg { width: 20px; height: 20px; }

.carousel-shell {
  position: relative;
  width: min(1180px, calc(100vw - 36px));
  height: min(720px, calc(100dvh - 96px));
  margin: auto;
  display: grid;
  grid-template-columns: 58px minmax(0, 1fr) 58px;
  align-items: center;
  justify-items: center;
  gap: var(--spacing-20);
}

.carousel-viewport {
  position: relative;
  height: min(720px, calc(100dvh - 96px));
  width: 100%;
  overflow: hidden;
  display: grid;
  place-items: center;
  align-items: center;
  padding: 26px 0 78px;
  perspective: 1400px;
  touch-action: pan-y;
  cursor: grab;
  overscroll-behavior: contain;
  -webkit-mask-image: linear-gradient(to right, transparent 0%, rgba(0, 0, 0, .35) 6%, rgba(0, 0, 0, 1) 14%, rgba(0, 0, 0, 1) 86%, rgba(0, 0, 0, .35) 94%, transparent 100%);
  mask-image: linear-gradient(to right, transparent 0%, rgba(0, 0, 0, .35) 6%, rgba(0, 0, 0, 1) 14%, rgba(0, 0, 0, 1) 86%, rgba(0, 0, 0, .35) 94%, transparent 100%);
}
.carousel-viewport.is-dragging { cursor: grabbing; }
.carousel-viewport.is-dragging .carousel-track { transition: none !important; }

.carousel-track {
  position: absolute;
  left: 0;
  top: 50%;
  display: flex;
  align-items: center;
  gap: var(--spacing-24);
  transform: translateY(-50%);
  will-change: transform;
  user-select: none;
  -webkit-user-select: none;
}

.carousel-card {
  position: relative;
  flex: 0 0 310px;
  width: 310px;
  height: 420px;
  border-radius: var(--radius-cards);
  perspective: 1200px;
  cursor: pointer;
  transition: opacity .45s var(--ease-deck-soft), filter .45s var(--ease-deck-soft), transform .45s var(--ease-deck-soft);
  transform: scale(.86);
  opacity: .48;
  filter: grayscale(.18) brightness(.82);
}
.carousel-card.is-near { opacity: .72; filter: grayscale(.08) brightness(.94); transform: scale(.93); }
.carousel-card.is-far { opacity: .28; filter: grayscale(.3) brightness(.72); transform: scale(.86); }
.carousel-card.is-active { opacity: 1; filter: none; transform: scale(1); }

.carousel-card-inner {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  overflow: hidden;
  background: var(--color-snow);
  box-shadow: var(--shadow-deck);
  transform-style: preserve-3d;
  transition: transform .28s var(--ease-deck-soft), box-shadow .28s var(--ease-deck-soft);
  will-change: transform;
}
.carousel-card-inner > img { position: absolute; inset: 0; }
.carousel-card.is-active .carousel-card-inner { box-shadow: var(--shadow-carousel-active); }

.carousel-nav {
  width: 58px;
  height: 58px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--deck-glass-border);
  background: var(--glass-fill-dark);
  color: var(--color-mint-ice-cream);
  box-shadow: none;
  -webkit-backdrop-filter: blur(var(--glass-blur));
  backdrop-filter: blur(var(--glass-blur));
  display: grid;
  place-items: center;
  cursor: pointer;
  transition: transform .22s var(--ease-deck-out), opacity .22s var(--ease-deck-soft), background .22s var(--ease-deck-soft), border-color .22s var(--ease-deck-soft);
  z-index: 3;
}
@media (hover: hover) and (pointer: fine) {
  .carousel-nav:hover:not(:disabled) { transform: translateY(-2px); background: rgba(255, 255, 255, .12); border-color: var(--glass-border-light); }
}
.carousel-nav:active:not(:disabled) { transform: scale(0.92); }
.carousel-nav:disabled { opacity: .32; cursor: not-allowed; }
.carousel-nav svg { width: 23px; height: 23px; }

.carousel-status {
  position: absolute;
  left: 50%;
  bottom: 20px;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--element-gap);
  z-index: 6;
}
.carousel-counter {
  color: var(--color-ash);
  font-family: var(--font-body);
  font-size: var(--text-body-sm);
  line-height: 1.56;
  font-weight: var(--font-weight-medium);
  text-align: center;
}
.carousel-dots { display: flex; justify-content: center; gap: var(--element-gap); }
.carousel-dot {
  width: 8px;
  height: 8px;
  padding: 0;
  border: none;
  border-radius: var(--radius-pill);
  background: rgba(var(--color-brand-highlight-rgb), .28);
  cursor: pointer;
  transition: width .22s var(--ease-deck-out), background .22s var(--ease-deck-soft);
}
.carousel-dot.is-active { width: 28px; background: var(--color-mint-ice-cream); }

@media (max-width: 900px) {
  .carousel-shell { grid-template-columns: 1fr; }
  .carousel-shell > .carousel-nav { position: fixed; bottom: calc(26px + env(safe-area-inset-bottom, 0px)); }
  .carousel-shell > .carousel-nav.prev { left: calc(24px + env(safe-area-inset-left, 0px)); }
  .carousel-shell > .carousel-nav.next { right: calc(24px + env(safe-area-inset-right, 0px)); }
  .carousel-card { flex-basis: 270px; width: 270px; height: 370px; }
}

@media (max-width: 430px) {
  .premium-deck-card { width: min(360px, calc(100vw - 36px)); }
  .carousel-card { flex-basis: 242px; width: 242px; height: 336px; }
  .carousel-track { gap: var(--spacing-16); }
}

/* ============================================================
   DECK VIEWER OVERLAY
   The "View deck" screen (js/deck.js → openDeckViewer): every
   available deck for a draw in one scrollable Deep Forest
   overlay. Each deck is a dark Glass Card — name + Ember flag,
   suite chips, description, includes list, price + ticket tag —
   beside its own horizontal card rail (glass arrows appear only
   when the rail overflows).
   ============================================================ */
.deck-viewer {
  position: fixed;
  inset: 0;
  z-index: var(--z-deck-overlay);
  overflow-y: auto;
  background:
    radial-gradient(circle at 50% 0%, rgba(var(--color-brand-highlight-rgb), .08), transparent 38%),
    var(--deck-overlay-wash);
  -webkit-backdrop-filter: blur(24px);
  backdrop-filter: blur(24px);
  padding: clamp(24px, 5vw, 56px) clamp(16px, 4vw, 48px);
  padding-top: calc(clamp(24px, 5vw, 56px) + env(safe-area-inset-top, 0px));
  padding-bottom: calc(clamp(24px, 5vw, 56px) + env(safe-area-inset-bottom, 0px));
  overscroll-behavior: contain;
  opacity: 0;
  pointer-events: none;
  transition: opacity .32s var(--ease-deck-soft);
}
.deck-viewer.is-open { opacity: 1; pointer-events: auto; }

.deck-overlay .overlay-close,
.deck-viewer .overlay-close {
  transition: background var(--duration-micro) ease, border-color var(--duration-micro) ease,
              transform 160ms var(--ease-deck-out);
}
.deck-overlay .overlay-close:active,
.deck-viewer .overlay-close:active { transform: scale(0.94); }

.deck-viewer .overlay-close {
  position: fixed;
  top: calc(28px + env(safe-area-inset-top, 0px));
  right: calc(28px + env(safe-area-inset-right, 0px));
  z-index: 70;
  width: 44px;
  height: 44px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--deck-glass-border);
  background: var(--glass-fill-dark);
  color: var(--color-mint-ice-cream);
  display: grid;
  place-items: center;
  box-shadow: none;
  -webkit-backdrop-filter: blur(var(--glass-blur));
  backdrop-filter: blur(var(--glass-blur));
  cursor: pointer;
}
@media (hover: hover) and (pointer: fine) {
  .deck-viewer .overlay-close:hover { background: rgba(255, 255, 255, .12); border-color: var(--glass-border-light); }
}
.deck-viewer .overlay-close svg { width: 20px; height: 20px; }

.deck-viewer-shell {
  width: min(1080px, 100%);
  margin: 0 auto;
}

.deck-viewer-head { margin-bottom: var(--spacing-32); padding-right: 64px; }
.deck-viewer-eyebrow {
  font-size: var(--text-body-xs);
  font-weight: var(--font-weight-semibold);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--color-blush-pink);
  margin-bottom: var(--spacing-8);
}
.deck-viewer-title {
  font-family: var(--font-display);
  font-size: var(--text-heading-lg);
  font-weight: var(--font-weight-semibold);
  line-height: 1.12;
  color: var(--color-mint-ice-cream);
}
.deck-viewer-sub {
  margin-top: var(--spacing-8);
  font-size: var(--text-body-sm);
  line-height: var(--leading-body-sm);
  color: rgba(var(--color-brand-highlight-rgb), .7);
  max-width: 62ch;
}

.deck-viewer-body {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-16);
}

/* One deck = one dark Glass Card: meta rail-left, card art rail-right */
.deck-viewer-deck {
  display: grid;
  grid-template-columns: minmax(240px, 330px) 1fr;
  gap: var(--spacing-24);
  align-items: center;
  border-radius: var(--radius-cards);
  background: var(--glass-fill-dark);
  border: 1px solid var(--deck-glass-border);
  padding: clamp(18px, 3vw, 28px);
  transform: translateY(12px);
  transition: transform .32s var(--ease-deck-soft);
}
.deck-viewer.is-open .deck-viewer-deck { transform: translateY(0); }

.deck-viewer-name-row {
  display: flex;
  align-items: center;
  gap: var(--spacing-12);
  margin-bottom: var(--spacing-12);
}
.deck-viewer-name {
  font-family: var(--font-display);
  font-size: var(--text-heading-md);
  font-weight: var(--font-weight-semibold);
  color: var(--color-mint-ice-cream);
}

/* Suite chips — small glass pills, Blush icon strokes (DESIGN.md
   dark-surface Glass Card icon rule) */
.deck-viewer-suits {
  display: flex;
  flex-wrap: wrap;
  gap: var(--spacing-8);
  margin-bottom: var(--spacing-12);
}
.deck-viewer-suit {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 12px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--deck-glass-border);
  background: rgba(255, 255, 255, .04);
  font-size: var(--text-body-xs);
  font-weight: var(--font-weight-medium);
  color: var(--color-mint-ice-cream);
  white-space: nowrap;
}
.deck-viewer-suit .icon { color: var(--color-blush-pink); }

.deck-viewer-desc {
  font-size: var(--text-body-sm);
  line-height: var(--leading-body-sm);
  color: rgba(var(--color-brand-highlight-rgb), .7);
  margin-bottom: var(--spacing-12);
}

.deck-viewer-includes {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-8);
  margin-bottom: var(--spacing-16);
}
.deck-viewer-includes li {
  display: flex;
  align-items: flex-start;
  gap: var(--spacing-8);
  font-size: var(--text-body-sm);
  line-height: var(--leading-body-sm);
  color: rgba(var(--color-brand-highlight-rgb), .82);
}
.deck-viewer-includes li .icon {
  flex: 0 0 auto;
  margin-top: 2px;
  color: var(--color-blush-pink);
}

.deck-viewer-priceline {
  display: flex;
  align-items: center;
  gap: var(--spacing-12);
}
.deck-viewer-price {
  font-family: var(--font-display);
  font-size: var(--text-heading-md);
  font-weight: var(--font-weight-semibold);
  color: var(--color-snow);
}
.deck-viewer-priceline .deck-ticket-tag { margin-left: 0; }

/* Card rail — its own carousel when the artwork overflows */
.deck-viewer-rail {
  position: relative;
  min-width: 0;
}
.deck-viewer-cards {
  display: flex;
  gap: var(--spacing-16);
  overflow-x: auto; overscroll-behavior-x: contain;
  scroll-snap-type: x proximity;
  padding: var(--spacing-4);
  scrollbar-width: none;
}
.deck-viewer-cards::-webkit-scrollbar { display: none; }
.deck-viewer-cards:focus-visible { outline: 2px solid var(--color-mint-ice-cream); outline-offset: 2px; border-radius: 14px; }

.deck-viewer-card {
  position: relative;
  flex: 0 0 auto;
  width: clamp(140px, 17vw, 196px);
  aspect-ratio: 3 / 4;
  border-radius: 14px;
  overflow: hidden;
  isolation: isolate;
  scroll-snap-align: start;
  border: 1px solid rgba(var(--color-brand-highlight-rgb), .12);
  transition: transform .25s var(--ease-deck-soft);
}
.deck-viewer-card img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
@media (hover: hover) and (pointer: fine) {
  .deck-viewer-card:hover { transform: translateY(-4px); }
}
@media (hover: hover) and (pointer: fine) {
  .deck-viewer-card:hover .foil-sheen { animation: sheen 1.45s var(--ease-deck-soft); }
}

/* Rail arrows — Flat Glass icon buttons, shown only on overflow */
.deck-viewer-nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 2;
  width: 40px;
  height: 40px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--deck-glass-border);
  background: var(--glass-fill-dark);
  color: var(--color-mint-ice-cream);
  display: grid;
  place-items: center;
  -webkit-backdrop-filter: blur(var(--glass-blur));
  backdrop-filter: blur(var(--glass-blur));
  cursor: pointer;
  transition: background var(--duration-micro) ease, transform 160ms var(--ease-deck-out);
}
.deck-viewer-nav[hidden] { display: none; }
.deck-viewer-nav.prev { left: -8px; }
.deck-viewer-nav.next { right: -8px; }
.deck-viewer-nav svg { width: 18px; height: 18px; }
@media (hover: hover) and (pointer: fine) {
  .deck-viewer-nav:hover:not(:disabled) { background: rgba(255, 255, 255, .12); }
}
/* The control is vertically centred BY a transform, so the press
   has to carry it — a bare scale() would drop the button half its
   own height the moment a thumb landed on it. */
.deck-viewer-nav:active:not(:disabled) { transform: translateY(-50%) scale(0.92); }
.deck-viewer-nav:disabled { opacity: .35; cursor: default; }

@media (max-width: 820px) {
  .deck-viewer-deck { grid-template-columns: 1fr; align-items: start; }
  .deck-viewer-card { width: clamp(132px, 38vw, 170px); }
  .deck-viewer .overlay-close { top: 16px; right: 16px; }
}

@media (prefers-reduced-motion: reduce) {
  .deck-viewer,
  .deck-viewer-deck,
  .deck-viewer-card { transition: none; transform: none; }
  .deck-viewer-card:hover .foil-sheen { animation: none; }
}

/* ============================================================
   ASSET VAULT OVERLAY
   The "View assets" screen (js/deck.js → openAssetVault): what a
   bought deck actually contains. It borrows the deck viewer's
   overlay chrome (.deck-viewer wash, close button, shell, head)
   because it is the same room seen from the other side of the
   purchase — the difference is the content, not the frame.

   One group per card: the whole face (never masked — the deck is
   owned) beside the files cut from it, each a Flat Glass download
   on a dark Glass Card.
   ============================================================ */
.asset-vault-body {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
  gap: var(--spacing-16);
}

.asset-group {
  display: grid;
  grid-template-columns: 116px 1fr;
  gap: var(--spacing-20);
  align-items: start;
  border-radius: var(--radius-cards);
  background: var(--glass-fill-dark);
  border: 1px solid var(--deck-glass-border);
  padding: clamp(16px, 2.4vw, 24px);
  transform: translateY(12px);
  transition: transform .32s var(--ease-deck-soft);
}
.asset-vault.is-open .asset-group { transform: translateY(0); }

.asset-group-card {
  position: relative;
  aspect-ratio: 3 / 4;
  border-radius: 14px;
  overflow: hidden;
  isolation: isolate;
  border: 1px solid rgba(var(--color-brand-highlight-rgb), .12);
}
.asset-group-card img { width: 100%; height: 100%; object-fit: cover; display: block; }

.asset-group-body { min-width: 0; }
.asset-group-head { margin-bottom: var(--spacing-12); }

.asset-file-list {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-8);
}
.asset-file {
  display: flex;
  align-items: center;
  gap: var(--spacing-12);
  padding: var(--spacing-8) 0;
  border-top: 1px solid var(--deck-glass-border);
}
.asset-file:first-child { border-top: 0; padding-top: 0; }
.asset-file-icon { display: flex; color: var(--color-blush-pink); flex: 0 0 auto; }
.asset-file-copy { display: flex; flex-direction: column; min-width: 0; flex: 1; }
.asset-file-name {
  font-size: var(--text-body-sm);
  font-weight: var(--font-weight-medium);
  color: var(--color-mint-ice-cream);
}
.asset-file-meta { font-size: var(--text-caption); color: rgba(var(--color-brand-highlight-rgb), .6); }
.asset-file .btn { flex: 0 0 auto; }

/* Below the two-up breakpoint the art gives up width before the file
   rows do — a download row that wraps its button is unreadable. */
@media (max-width: 560px) {
  .asset-group { grid-template-columns: 84px 1fr; gap: var(--spacing-12); }
  .asset-file .btn span { display: none; }
}

@media (pointer: coarse) {
  .asset-file .btn { min-height: 44px; }
}

@media (prefers-reduced-motion: reduce) {
  .asset-group { transition: none; transform: none; }
}

/* ============================================================
   CONTENT SURFACES (section rhythm)
   Reusable full-section colour surfaces so content pages can
   alternate dark → Mint Ice Cream → Mist per DESIGN.md's surface
   discipline. Each sets its own section-title/subtitle colours;
   cards nested inside must use the matching Glass Card recipe
   (never a solid opaque fill on a coloured surface).

   THE DARK ONE IS AUTHORED, THE LIGHT ONES ARE THE PALETTE'S.
   `.surface-forest` and everything under it read `--dark-*`
   (tokens.css → Dark sections), because the ground a section is
   TURNED OVER onto is a decision a deployment makes and not one
   its brand palette can be assumed to answer — see Settings ›
   Brand › Dark mode. The light grounds are Mint and Mist, which
   every brand has by virtue of the page being light.

   .surface-forest and .surface-mint are also what an operator sets
   a storefront section to, one section at a time — see SECTION
   GROUND at the foot of this file, which is where the rest of the
   "this component on the other ground" rules live.
   ============================================================ */
.surface-forest { background: var(--dark-surface); }
.surface-forest .section-title,
.surface-forest .eyebrow-label { color: var(--dark-heading); }
.surface-forest .section-subtitle { color: var(--dark-body); }

.surface-mint { background: var(--color-mint-ice-cream); }
.surface-mint .section-title { color: var(--color-race-green); }
.surface-mint .section-subtitle { color: rgba(var(--color-brand-primary-rgb), 0.7); }

/* Contained Mint Ice Cream panel (DESIGN.md → Mint Ice Cream
   Section) — the light-surface sibling of .dark-panel, for
   featured/stat/trust modules inside an otherwise light page. */
.mint-panel {
  background: var(--color-mint-ice-cream);
  border-radius: var(--radius-cards);
  padding: var(--spacing-48) var(--spacing-40);
}
.mint-panel .section-title { color: var(--color-race-green); }
.mint-panel .section-subtitle { color: rgba(var(--color-brand-primary-rgb), 0.7); }
/* .stats-grid carries a bottom margin for dashboards where content
   follows; inside a panel it is the last child, so drop it (and the
   trailing stat-label margin) to keep the panel padding symmetric. */
/* Either panel, because the section can be set to either ground
   (components.css → SECTION GROUND) and the padding must not go
   lopsided when it is. */
.mint-panel .stats-grid, .dark-panel .stats-grid { margin-bottom: 0; }
.mint-panel .stat-label, .dark-panel .stat-label { margin-bottom: 0; }
@media (max-width: 768px) {
  .mint-panel { padding: var(--spacing-32) var(--spacing-24); border-radius: var(--radius-cards-compact); }
}

/* ============================================================
   GLASS CARD (canonical)
   The single reusable Glass Card for content nested on a coloured
   surface (DESIGN.md → Glass Card). Two recipes share one content
   model (icon top-left, heading-md title, small body):
     .glass-card--dark   on Deep Forest / Race Green
     .glass-card--light  on Mint Ice Cream
   Never a solid opaque fill on a coloured surface. This supersedes
   the ad-hoc glass treatments baked into feature/bento/trust tiles.
   ============================================================ */
.glass-card {
  border-radius: var(--radius-cards);
  padding: var(--card-padding);
  -webkit-backdrop-filter: blur(var(--glass-blur));
  backdrop-filter: blur(var(--glass-blur));
}
.glass-card > i[data-icon] { margin-bottom: var(--spacing-16); }
.glass-card-title {
  font-family: var(--font-display); letter-spacing: var(--ls-heading-md);
  font-size: var(--text-heading-md);
  font-weight: var(--font-weight-semibold);
  line-height: 1.15;
  margin-bottom: var(--spacing-8);
}
.glass-card-body { font-size: var(--text-body-sm); line-height: var(--leading-body-sm); }

/* Dark recipe — on Deep Forest / Race Green */
.glass-card--dark { background: var(--glass-fill-dark); border: 1px solid var(--deck-glass-border); }
.glass-card--dark > i[data-icon] { color: var(--color-blush-pink); }
.glass-card--dark .glass-card-title { color: var(--color-mint-ice-cream); }
.glass-card--dark .glass-card-body { color: rgba(var(--color-brand-highlight-rgb), 0.7); }

/* Light recipe — on Mint Ice Cream (Blush icons lose contrast on
   mint, so icons are Race Green here) */
.glass-card--light { background: var(--glass-fill-light); border: 1px solid rgba(var(--color-brand-primary-rgb), 0.10); }
.glass-card--light > i[data-icon] { color: var(--color-race-green); }
.glass-card--light .glass-card-title { color: var(--color-race-green); }
.glass-card--light .glass-card-body { color: rgba(var(--color-brand-primary-rgb), 0.7); }

/* Small overline that labels a section topic. Reuses micro-label
   scale but tuned as a section eyebrow (used sparingly — the
   headline usually stands alone). */
.eyebrow-label {
  display: inline-block;
  font-size: var(--text-body-xs);
  font-weight: var(--font-weight-semibold);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--color-race-green);
  margin-bottom: var(--spacing-12);
}

/* ============================================================
   FEATURE GRID
   Icon + title + body cards on a light canvas. Snow cards sit a
   background-step above the Mist canvas (no border, per DESIGN.md
   flat-card rule). Icon bare in Race Green, title at heading-md.
   ============================================================ */
.feature-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--spacing-24);
}
.feature-card {
  background: var(--surface-card-white);
  border-radius: var(--radius-cards);
  padding: var(--card-padding);
}
.feature-card i[data-icon] { color: var(--color-race-green); margin-bottom: var(--spacing-16); }
.feature-card-title {
  font-family: var(--font-display); letter-spacing: var(--ls-heading-md);
  font-size: var(--text-heading-md);
  font-weight: var(--font-weight-semibold);
  line-height: 1.15;
  color: var(--color-ink);
  margin-bottom: var(--spacing-8);
}
.feature-card-body { font-size: var(--text-body-sm); line-height: var(--leading-body-sm); color: var(--color-steel); }

/* On a Deep Forest surface the same grid uses dark Glass Cards
   (blush icon, mint text) — the DESIGN.md dark Glass Card recipe. */
.surface-forest .feature-card {
  background: var(--glass-fill-dark);
  border: 1px solid var(--deck-glass-border);
  -webkit-backdrop-filter: blur(var(--glass-blur));
  backdrop-filter: blur(var(--glass-blur));
}
.surface-forest .feature-card i[data-icon] { color: var(--dark-accent); }
.surface-forest .feature-card-title { color: var(--dark-heading); }
.surface-forest .feature-card-body { color: var(--dark-body); }

@media (max-width: 900px) { .feature-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 600px) { .feature-grid { grid-template-columns: 1fr; } }

/* ============================================================
   BENTO GRID
   Asymmetric tile composition on a Deep Forest surface. Base tile
   is a dark Glass Card; modifiers span cells or swap the fill for
   a full-bleed image, a raw stat, or a Blush accent. Exactly as
   many tiles as content — no empty cells.
   ============================================================ */
.bento {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-auto-rows: 180px;
  gap: var(--spacing-16);
}
.bento-item {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-cards-compact);
  padding: var(--card-padding-compact);
  background: var(--glass-fill-dark);
  border: 1px solid var(--deck-glass-border);
  -webkit-backdrop-filter: blur(var(--glass-blur));
  backdrop-filter: blur(var(--glass-blur));
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  gap: var(--spacing-8);
}
/* Icon sits top-left like every other card. Without align-self it
   would stretch full-width (inline-flex child in a stretch column)
   and centre its glyph. */
.bento-item > i[data-icon] { align-self: flex-start; color: var(--color-blush-pink); margin-bottom: auto; }
.bento-title {
  font-family: var(--font-display); letter-spacing: var(--ls-heading-md);
  font-size: var(--text-heading-md);
  font-weight: var(--font-weight-semibold);
  line-height: 1.1;
  color: var(--color-mint-ice-cream);
}
.bento-text { font-size: var(--text-body-sm); line-height: var(--leading-body-sm); color: rgba(var(--color-brand-highlight-rgb), 0.7); }
.bento-lg   { grid-column: span 2; grid-row: span 2; }
.bento-wide { grid-column: span 2; }

/* Image tile — full-bleed prize art with a bottom scrim, a badge
   pinned top-left, and a white overlay title (image-overlay text
   is Snow, never Mint). */
.bento-img { padding: var(--spacing-20); justify-content: flex-end; }
.bento-img img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; z-index: -2; }
.bento-img::after {
  content: '';
  position: absolute; inset: 0; z-index: -1;
  background: linear-gradient(180deg, rgba(var(--color-brand-primary-strong-rgb), 0) 40%, rgba(var(--color-brand-primary-strong-rgb), 0.78) 100%);
}
.bento-img .badge { position: absolute; top: var(--spacing-20); left: var(--spacing-20); }
.bento-img .bento-title { color: var(--color-snow); text-shadow: rgba(0, 0, 0, 0.34) 0 1px 8px; }

/* Stat tile — raw Switzer numeral, no extra chrome. */
.bento-stat { justify-content: center; }
.bento-stat .bento-num {
  font-family: var(--font-display); letter-spacing: var(--ls-heading-lg);
  font-size: var(--text-heading-lg);
  font-weight: var(--font-weight-bold);
  line-height: 1;
  color: var(--color-mint-ice-cream);
}
.bento-stat .bento-text { color: rgba(var(--color-brand-highlight-rgb), 0.6); }

/* Blush accent tile — one soft collectable highlight per grid. */
.bento-accent { background: var(--color-blush-pink); border-color: rgba(var(--color-brand-primary-rgb), 0.10); }
.bento-accent i[data-icon],
.bento-accent .bento-title { color: var(--color-race-green); }
.bento-accent .bento-text { color: rgba(var(--color-brand-primary-rgb), 0.72); }

@media (max-width: 900px) {
  /* Tiles no longer span 2 rows here, so let rows grow to fit copy
     (fixed heights would clip the taller text tiles once stacked). */
  .bento { grid-template-columns: repeat(2, 1fr); grid-auto-rows: minmax(180px, auto); }
  .bento-lg { grid-column: span 2; grid-row: span 1; }
}
@media (max-width: 560px) {
  .bento { grid-template-columns: 1fr; grid-auto-rows: minmax(160px, auto); }
  .bento-lg, .bento-wide { grid-column: span 1; }
}

/* ============================================================
   MEDIA SPLIT (image + content)
   Two-column content block: 28px-clipped image on one side,
   copy on the other. --reverse flips the order. The section can
   stack several, alternating sides for editorial rhythm.
   ============================================================ */
.media-split {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--spacing-48);
  align-items: center;
}
/* A `1fr` track's automatic minimum is its content, so one long word
   or a nowrap row inside the copy column widens the track past the
   viewport once stacked. min-width: 0 holds the track to the width
   it was given. */
.media-split > * { min-width: 0; }
.media-split-media {
  border-radius: var(--radius-cards);
  overflow: hidden;
  aspect-ratio: 4 / 3;
}
.media-split-media img { width: 100%; height: 100%; object-fit: cover; }
.media-split-title {
  font-family: var(--font-display); letter-spacing: var(--ls-heading);
  font-size: var(--text-heading);
  font-weight: var(--font-weight-light);
  line-height: 1.1;
  color: var(--color-obsidian);
  margin-bottom: var(--spacing-16);
}
/* The copy column comes two ways and they render identically.
   .media-split-body / .media-split-list are the hand-written form —
   a standfirst, then a fixed run of points, marked up by class.
   .media-split-copy is the AUTHORED form: one block an operator
   writes in the console's rich-text field, so the SHAPE of a step is
   theirs — two points or six, a subheading part-way down — while the
   type scale stays the design's. The editor's whole block set is
   mapped below and nothing outside it is styled, because nothing
   outside it can be emitted.

   The ticks on an authored list are put there by js/cms-sections.js
   from the icon registry, never written into the page markup and
   never redrawn in CSS: one definition of what a point looks like. */
.media-split-body,
.media-split-copy p { font-size: var(--text-body-lg); line-height: var(--leading-body-lg); color: var(--color-steel); margin-bottom: var(--spacing-24); }
.media-split-list,
.media-split-copy ul { display: flex; flex-direction: column; gap: var(--spacing-12); margin-bottom: var(--spacing-24); }
.media-split-list li,
.media-split-copy ul > li { display: flex; align-items: flex-start; gap: var(--spacing-12); font-size: var(--text-body-md); color: var(--color-ink); }
.media-split-list i[data-icon],
.media-split-copy li > i[data-icon] { color: var(--color-race-green); flex-shrink: 0; margin-top: 1px; }
/* A numbered list is a sequence, so it keeps its numbers — the global
   reset strips markers, and an authored list is one of the few places
   they carry meaning. */
.media-split-copy ol {
  margin-bottom: var(--spacing-24);
  padding-left: var(--spacing-20);
  list-style: decimal;
}
.media-split-copy ol > li { font-size: var(--text-body-md); color: var(--color-ink); margin-bottom: var(--spacing-12); }
/* Subheadings inside the copy sit under .media-split-title, which is
   still the heading of the block they are inside — so the title level
   is the display face at the smallest size it is set at anywhere
   (.rich-text h4's own 16px), and the subtitle level below it drops to
   the body face at normal tracking. Switzer at 15px is under the scale
   it is drawn for, and its -2% tracking closes up at that size. */
.media-split-copy h3,
.media-split-copy h4 {
  font-weight: var(--font-weight-semibold);
  color: var(--color-obsidian);
  margin: var(--spacing-24) 0 var(--spacing-8);
}
.media-split-copy h3 {
  font-family: var(--font-display); letter-spacing: var(--ls-rg);
  font-size: var(--text-body-lg); line-height: 1.25;
}
.media-split-copy h4 {
  font-family: var(--font-body); font-size: var(--text-body-md);
  /* Off the global heading tracking and line-height: an Inter role is
     set at normal tracking (DESIGN.md → type scale), and a heading
     that can wrap needs a body leading rather than the 1 the display
     roles are set at. */
  letter-spacing: normal; line-height: var(--leading-body-md);
}
.media-split-copy > :first-child { margin-top: 0; }
.media-split-copy b, .media-split-copy strong { font-weight: var(--font-weight-semibold); }
/* Inline stat row for editorial splits — mirrors the host spotlight's
   hairline-ruled stat band (same 40px gap, 32/24px inset), in Deep
   Forest ink for the light surface rather than the dark panel's mint. */
.media-split-stats {
  display: flex;
  flex-wrap: wrap;
  gap: var(--spacing-40);
  margin-top: var(--spacing-32);
  margin-bottom: var(--spacing-24);
  padding-top: var(--spacing-24);
  border-top: 1px solid rgba(var(--color-brand-primary-rgb), 0.14);
}
.media-split-stat-num {
  font-family: var(--font-display); letter-spacing: var(--ls-heading);
  font-size: var(--text-heading);
  font-weight: var(--font-weight-bold);
  line-height: 1;
  color: var(--color-obsidian);
}
.media-split-stat-label { font-size: var(--text-body-xs); color: var(--color-steel); margin-top: var(--spacing-4); }
.media-split--reverse .media-split-media { order: 2; }
/* Taller portrait slot for spotlight/editorial splits — shares the
   prize-tile 4/5 ratio so hero imagery reads as the same object
   family as the discovery tiles. */
.media-split-media--tall { aspect-ratio: 4 / 5; }

/* Framed variant — the collectible deck tile's shell language at
   section scale: a Snow 28px shell with the same 8px padding frame
   .premium-deck-card gives its stage, the image stepping down to
   the 22px deck-stage radius inside it, and the copy column
   carrying its own inner padding on the white surface. */
.media-split--framed {
  background: var(--color-snow);
  border-radius: var(--radius-cards);
  box-shadow: var(--shadow-flat-hairline);
  padding: var(--element-gap);
  gap: var(--spacing-16);
  align-items: stretch;
}
.media-split--framed .media-split-media {
  border-radius: var(--radius-deck-stage);
  height: 100%;
}
.media-split--framed > div:not(.media-split-media) {
  align-self: center;
  padding: var(--spacing-40) var(--spacing-40) var(--spacing-40) var(--spacing-24);
}
.media-split--framed.media-split--reverse > div:not(.media-split-media) {
  padding: var(--spacing-40) var(--spacing-24) var(--spacing-40) var(--spacing-40);
}
/* THE FRAME'S TWO COLOURWAYS. The shell above is the shape; these
   are what it is painted, and an operator picks between them per
   section (components.css → SECTION GROUND). Parchment is the warm
   putty light dress — the one the featured drop ships in, pairing
   with the Mint Tertiary CTA — and Deep Forest is the dark one.

   ANYTHING ABOUT THE SHAPE HANGS OFF `--framed`, NEVER OFF A
   COLOURWAY: the square image below is the frame's, so a card turned
   over does not re-crop the picture the operator cropped to it. */
.media-split--parchment { background: var(--color-parchment); }
.media-split--forest { background: var(--color-deep-forest); }
/* Featured drop: a square image to match the host spotlight, filling
   the frame with the same even 8px inset on every side. */
.media-split--framed .media-split-media { aspect-ratio: 1 / 1; }
/* Deep Forest ink for the title and stat figures on the parchment
   shell, in place of the default Obsidian. The dark dress takes its
   ink from .surface-forest, which travels with it. */
.media-split--parchment .media-split-title,
.media-split--parchment .media-split-stat-num { color: var(--color-deep-forest); }

/* The framed spotlight runs out of horizontal room earlier than a plain
   split: its image is height-matched to the copy column (height:100%),
   so once the copy grows taller than the image would be at half-width,
   the image's fixed ratio (1:1 on the frame) drives its width past its
   grid track and over the copy. That crossover sits around ~1090px for
   this content, so stack the framed variant into its single column at
   1120px — well before the break — rather than at the generic 860px. */
@media (max-width: 1120px) {
  .media-split--framed {
    grid-template-columns: 1fr;
    gap: 0;
  }
  .media-split--framed .media-split-media { height: auto; }
  .media-split--framed.media-split--reverse .media-split-media { order: 0; }
  .media-split--framed > div:not(.media-split-media),
  .media-split--framed.media-split--reverse > div:not(.media-split-media) {
    padding: var(--spacing-24) var(--spacing-20) var(--spacing-28);
  }
}

@media (max-width: 860px) {
  .media-split { grid-template-columns: 1fr; gap: var(--spacing-24); }
  .media-split--reverse .media-split-media { order: 0; }
}

/* ============================================================
   MEDIA BAND (full-width image with overlay copy + CTA)
   A 28px-clipped photographic band with a centred message. Text
   over imagery is always Snow. Reusable for "moment" breaks
   between text-heavy sections.
   ============================================================ */
.media-band {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  border-radius: var(--radius-cards);
  min-height: 340px;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: var(--spacing-48) var(--spacing-40);
}
.media-band img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; z-index: -2; }
.media-band::after {
  content: '';
  position: absolute; inset: 0; z-index: -1;
  background: linear-gradient(180deg, rgba(var(--color-brand-primary-strong-rgb), 0.55) 0%, rgba(var(--color-brand-primary-strong-rgb), 0.68) 100%);
}
.media-band-inner { max-width: 620px; }
.media-band-title {
  font-family: var(--font-display); letter-spacing: var(--ls-heading-lg);
  font-size: var(--text-heading-lg);
  font-weight: var(--font-weight-light);
  line-height: 1.1;
  color: var(--color-snow);
  text-shadow: rgba(0, 0, 0, 0.3) 0 1px 12px;
  margin-bottom: var(--spacing-16);
}
.media-band-body { font-size: var(--text-body-lg); line-height: var(--leading-body-lg); color: rgba(255, 255, 255, 0.86); margin-bottom: var(--spacing-24); text-shadow: rgba(0, 0, 0, 0.28) 0 1px 10px; }

/* Story variant — left-aligned narrative band for winner stories
   and host features: badges row above the title, copy anchored to
   the left edge where the scrim is strongest. */
.media-band--story { justify-content: flex-start; text-align: left; }
.media-band--story .media-band-inner { max-width: 560px; }
.media-band--story::after {
  background: linear-gradient(90deg, rgba(var(--color-brand-primary-strong-rgb), 0.72) 0%, rgba(var(--color-brand-primary-strong-rgb), 0.42) 55%, rgba(var(--color-brand-primary-strong-rgb), 0.18) 100%);
}

@media (max-width: 600px) { .media-band { padding: var(--spacing-32) var(--spacing-24); } }

/* ============================================================
   CTA DUO
   Two photographic conversion tiles side by side (collect / host),
   or ONE holding the row on its own where a page has a single
   audience — an operator switches a tile off in the console and
   the other takes the width, because a tile at half width beside
   an empty half reads as a component that failed to load.

   Full-bleed imagery under a wash weighted to the copy side, Snow
   overlay copy centred in the tile's height, one CTA each — Blush
   or Flat Glass, per the photographic-surface button rule. The
   wash runs across rather than up precisely because the copy sits
   in the middle of the picture: the standard bottom scrim would
   leave it on bare photograph. Hover zooms the image inside the
   mask and deepens the wash a touch; the tile is a link, so it
   also has to say so without the underline every other link gets
   (the category card opts out the same way, for the same reason).
   ============================================================ */
.cta-duo {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--spacing-24);
}
/* One tile, one row — whether the page only writes one or the
   record has switched the other off (`.hidden`, js/cms-sections.js). */
.cta-tile:only-child,
.cta-duo:has(> .cta-tile.hidden) > .cta-tile:not(.hidden) { grid-column: 1 / -1; }

.cta-tile {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  border-radius: var(--radius-cards);
  min-height: 340px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: flex-start;
  gap: var(--spacing-8);
  padding: var(--spacing-40);
  color: var(--color-snow);
  text-decoration: none;
  transition: transform 160ms var(--ease-deck-out);
}
@media (hover: hover) and (pointer: fine) {
  .cta-tile:hover { text-decoration: none; }
}
.cta-tile:active { transform: scale(0.99); }
.cta-tile img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: -2;
  transform: scale(1.025);
  transition: transform var(--duration-deck-image) var(--ease-deck-soft);
}
.cta-tile::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  /* Weighted to the side the copy is on rather than to the bottom:
     the words then sit on a dark ground wherever they are in the
     tile's height, and the photograph stays alive on the far side —
     which is what keeps a single full-width tile reading as a
     picture with something written on it. Under it sits a flat
     floor, so a tile stacked to full width on a phone — where the
     copy runs the whole way across — never puts a line of body text
     on bare sky. */
  background:
    linear-gradient(100deg, rgba(var(--color-brand-primary-strong-rgb), 0.80) 0%, rgba(var(--color-brand-primary-strong-rgb), 0.54) 46%, rgba(var(--color-brand-primary-strong-rgb), 0.22) 100%),
    linear-gradient(0deg, rgba(var(--color-brand-primary-strong-rgb), 0.22), rgba(var(--color-brand-primary-strong-rgb), 0.22));
  opacity: 0.92;
  transition: opacity var(--duration-micro) ease;
}
@media (hover: hover) and (pointer: fine) {
  .cta-tile:hover img { transform: scale(1.05); }
  .cta-tile:hover::after { opacity: 1; }
}
.cta-tile-kicker {
  font-size: var(--text-body-xs);
  font-weight: var(--font-weight-semibold);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: rgba(255, 255, 255, 0.78);
}
.cta-tile-title {
  font-family: var(--font-display); letter-spacing: var(--ls-heading);
  font-size: var(--text-heading);
  font-weight: var(--font-weight-light);
  line-height: 1.1;
  color: var(--color-snow);
  text-shadow: rgba(0, 0, 0, 0.3) 0 1px 10px;
}
.cta-tile-body { font-size: var(--text-body-md); line-height: var(--leading-body-md); color: rgba(255, 255, 255, 0.85); max-width: 40ch; }
.cta-tile .btn { margin-top: var(--spacing-16); }

@media (max-width: 860px) {
  .cta-duo { grid-template-columns: 1fr; }
  .cta-tile { min-height: 280px; padding: var(--spacing-24); }
}

/* ============================================================
   PROMO BAND
   Anything a page has to say that is NOT a draw: what a purchase
   is worth keeping for, the free route in, an app, a partnership.
   A picture beside a copy column — eyebrow, headline, standfirst,
   a run of chips and one way through — nested on Deep Forest
   (.dark-panel).

   Everything in it is authored: the picture is uploaded and the
   chips are a run an operator adds to, so the band carries no
   assumption about what a deployment sells. It replaced the
   wallpaper promo, whose two CSS phone frames held two hard-coded
   card artworks — a component only one jurisdiction could use,
   and the only place device chrome appeared in the system.
   ============================================================ */
.promo-band {
  display: grid;
  grid-template-columns: 1.1fr 1fr;
  gap: var(--spacing-48);
  align-items: center;
}
.promo-band-copy .section-subtitle { margin-bottom: var(--spacing-20); }
/* The chips are a run the operator writes, so the row goes when
   there is nothing in it rather than holding its margin open. */
.promo-band-chips { display: flex; gap: var(--element-gap); flex-wrap: wrap; margin-bottom: var(--spacing-24); }
.promo-band-chips:empty { display: none; }
/* The frame is square and the picture always covers it, which is
   the ratio the section's own upload slot crops to — a component
   that re-crops what the cropper framed is a component the console
   cannot preview honestly. */
.promo-band-media {
  aspect-ratio: 1 / 1;
  border-radius: var(--radius-cards);
  overflow: hidden;
  background: rgba(255, 255, 255, 0.06);
}
.promo-band-media img { width: 100%; height: 100%; object-fit: cover; }

@media (max-width: 860px) {
  .promo-band { grid-template-columns: 1fr; gap: var(--spacing-24); }
}

/* ============================================================
   CARD ART SHOWCASE
   A spread of collectible card artwork laid out like prints on a
   table: alternating tilt, deck object shadows (the one sanctioned
   elevated artefact), straighten-and-lift on hover. Lives on Deep
   Forest so the art carries the colour. Transform-only motion.
   ============================================================ */
.card-art-strip {
  display: flex;
  justify-content: center;
  align-items: center;
  max-width: 1040px;
  margin: 0 auto;
  padding: var(--spacing-12) 0;
}
/* HOW MANY CARDS ARE ON THE TABLE IS THE OPERATOR'S, so the card
   takes a share rather than a fixed width: four deal out to fill the
   strip, three sit centred and narrower, and more than that shrink
   to fit rather than running off the side of the panel. */
.card-art {
  flex: 0 1 27.5%;
  min-width: 0;
  border-radius: var(--radius-3xl);
  overflow: hidden;
  aspect-ratio: 3 / 4;
  box-shadow: var(--shadow-deck);
  transition: transform 0.45s var(--ease-deck-out);
}
.card-art img { width: 100%; height: 100%; object-fit: cover; }
/* A card whose artwork has not been uploaded yet is not dealt onto
   the table: an empty 3:4 box with a deck shadow under it reads as a
   picture that failed to load. The record marks the picture `hidden`
   and the strip decides what that means (js/cms-sections.js). */
.card-art:not(:has(img:not([hidden]))) { display: none; }
/* Slight overlap, like cards actually dealt onto the table; the
   dropped even cards sit above their neighbours, hover on top. */
.card-art + .card-art { margin-left: -3.5%; }
.card-art:nth-child(odd) { transform: rotate(-2.4deg); }
.card-art:nth-child(even) { transform: rotate(2deg) translateY(10px); z-index: 1; }
@media (hover: hover) and (pointer: fine) {
  .card-art:hover { transform: rotate(0deg) translateY(-8px); z-index: 2; }
}

@media (max-width: 768px) {
  .card-art-strip { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--spacing-16); max-width: none; }
  .card-art { width: auto; }
  .card-art + .card-art { margin-left: 0; }
}

/* ============================================================
   TICKET LADDER
   The direct-ticket jurisdiction's answer to the Card Art
   Showcase: the same slot on the homepage, saying the thing that
   is actually true where tickets are sold outright rather than
   dealt with a deck. One rung per bundle the host authored,
   climbing left to right, each stating what it works out at a
   ticket — so the ladder's whole point is read off the row rather
   than claimed in a sentence above it.

   Glass Cards on Deep Forest (DESIGN.md → Glass Card, dark
   recipe), so nest inside .dark-panel. The corner tag is the
   host's own word, exactly as it is on a draw page's ticket
   option — nothing here derives one, and a rung with no tag keeps
   the row the tag would have taken so every rung's figures line
   up across the ladder.
   ============================================================ */
.ticket-ladder {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(196px, 1fr));
  gap: var(--spacing-16);
}
.ladder-rung {
  display: grid;
  grid-template-rows: auto auto 1fr;
  gap: var(--spacing-12);
  padding: var(--spacing-24) var(--spacing-20);
  border-radius: var(--radius-cards-compact);
  background: var(--deck-glass-fill);
  border: 1px solid var(--deck-glass-border);
  backdrop-filter: blur(var(--glass-blur));
  box-shadow: none;
  transition: transform var(--duration-micro) var(--ease-deck-out),
              border-color var(--duration-micro) ease,
              background-color var(--duration-micro) ease;
}
@media (hover: hover) and (pointer: fine) {
  .ladder-rung:hover {
    transform: translateY(-2px);
    border-color: var(--glass-border-light);
    background: rgba(255, 255, 255, 0.09);
  }
}

/* The tag's row is drawn whether or not this rung carries one:
   a ladder is read across, and figures that shuffle up and down
   by the height of a badge cannot be. */
.ladder-rung-tag { min-height: 22px; }
.ladder-rung-tag .badge { font-size: var(--text-caption); padding: 3px 7px; }

.ladder-rung-head { display: flex; flex-direction: column; align-items: flex-start; gap: var(--spacing-8); }
.ladder-rung-count {
  font-family: var(--font-display);
  font-size: var(--text-heading);
  line-height: var(--leading-heading);
  font-weight: var(--font-weight-bold);
  letter-spacing: var(--ls-rg);
  font-variant-numeric: tabular-nums;
  color: var(--color-mint-ice-cream);
}
.ladder-rung-unit {
  margin-left: 6px;
  font-family: var(--font-body);
  font-size: var(--text-body-sm);
  font-weight: var(--font-weight-medium);
  letter-spacing: 0;
  color: rgba(var(--color-brand-highlight-rgb), 0.7);
}

/* The payoff line, pinned to the foot of the rung so the ladder
   descends along one baseline. */
.ladder-rung-value { align-self: end; }
.ladder-rung-each {
  font-family: var(--font-display);
  font-size: var(--text-heading-sm);
  font-weight: var(--font-weight-bold);
  letter-spacing: var(--ls-rg);
  font-variant-numeric: tabular-nums;
  color: var(--color-mint-ice-cream);
}
.ladder-rung-each span {
  font-family: var(--font-body);
  font-size: var(--text-body-sm);
  font-weight: var(--font-weight-medium);
  letter-spacing: 0;
  color: rgba(var(--color-brand-highlight-rgb), 0.7);
}
.ladder-rung-total {
  margin-top: var(--spacing-4);
  font-size: var(--text-body-xs);
  color: rgba(var(--color-brand-highlight-rgb), 0.6);
  font-variant-numeric: tabular-nums;
}

/* Below the tablet step two rungs read side by side; a phone takes
   them one at a time rather than shrinking the figures. */
@media (max-width: 768px) {
  .ticket-ladder { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 480px) {
  .ticket-ladder { grid-template-columns: 1fr; gap: var(--spacing-12); }
  .ladder-rung { grid-template-rows: auto auto auto; padding: var(--spacing-20); }
  /* One rung to a screen: there is nothing beside it to line the
     figures up with, so an untagged rung stops reserving the row. */
  .ladder-rung-tag:empty { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  .ladder-rung { transition: none; }
  .ladder-rung:hover { transform: none; }
}

/* ============================================================
   HOST SPOTLIGHT
   Deep Forest editorial split: a Switzer Light pull-quote with a
   .hatton anchor, host attribution, a hairline-ruled stat row in
   Mint, and a tall prize image. Blush eyebrow per the dark-surface
   accent rule. Nest inside .dark-panel (or a full-bleed
   .surface-forest section).
   ============================================================ */
.host-spotlight {
  display: grid;
  grid-template-columns: 1.15fr 1fr;
  gap: var(--spacing-48);
  align-items: center;
}
.host-spotlight .eyebrow-label { color: var(--color-blush-pink); }
/* Prize image squared to the artwork's native 1:1 so nothing crops,
   sitting on the panel's own even 48/40px inset. Shorter than the
   default 4/5 portrait so the quote and enlarged stat row lead. */
.host-spotlight .media-split-media { aspect-ratio: 1 / 1; align-self: center; }
.host-spotlight-quote {
  font-family: var(--font-display); letter-spacing: var(--ls-heading);
  font-size: var(--text-heading);
  font-weight: var(--font-weight-light);
  line-height: 1.22;
  color: var(--color-mint-ice-cream);
  margin-bottom: var(--spacing-24);
}
.host-spotlight-name { color: var(--color-mint-ice-cream); font-size: var(--text-body); font-weight: var(--font-weight-semibold); }
.host-spotlight-role { font-size: var(--text-body-xs); color: rgba(var(--color-brand-highlight-rgb), 0.6); }
.host-spotlight-stats {
  display: flex;
  gap: var(--spacing-40);
  margin-top: var(--spacing-32);
  padding-top: var(--spacing-24);
  border-top: 1px solid rgba(var(--color-brand-highlight-rgb), 0.14);
}
.host-spotlight-stat-num {
  font-family: var(--font-display); letter-spacing: var(--ls-heading);
  font-size: var(--text-heading);
  font-weight: var(--font-weight-bold);
  line-height: 1;
  color: var(--color-mint-ice-cream);
}
.host-spotlight-stat-label { font-size: var(--text-body-xs); color: rgba(var(--color-brand-highlight-rgb), 0.6); margin-top: var(--spacing-4); }

@media (max-width: 860px) {
  .host-spotlight { grid-template-columns: 1fr; gap: var(--spacing-24); }
  .host-spotlight-stats { flex-wrap: wrap; gap: var(--spacing-24); }
}

/* ============================================================
   TESTIMONIALS / QUOTES
   Snow quote cards on the light canvas. Quiet quote glyph, quote
   body at subheading size, attribution row with an initials
   avatar (no stock faces). Icon accents on light use Race Green.
   ============================================================ */
.quote-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--spacing-24);
}
.quote-card {
  background: var(--surface-card-white);
  border-radius: var(--radius-cards);
  padding: var(--card-padding);
  display: flex;
  flex-direction: column;
  gap: var(--spacing-16);
}
.quote-card > i[data-icon] { color: var(--color-pebble); }
.quote-body {
  font-family: var(--font-display); letter-spacing: var(--ls-subheading);
  font-size: var(--text-subheading);
  font-weight: var(--font-weight-light);
  line-height: 1.45;
  color: var(--color-ink);
  flex: 1;
}
.quote-attrib { display: flex; align-items: center; gap: var(--spacing-12); }
.quote-attrib .avatar-circle { background: var(--color-race-green); width: 40px; height: 40px; }
.quote-name { font-size: var(--text-body-sm); font-weight: var(--font-weight-semibold); color: var(--color-ink); }
.quote-role { font-size: var(--text-body-xs); color: var(--color-steel); }

@media (max-width: 900px) { .quote-grid { grid-template-columns: 1fr; } }

/* ============================================================
   LOGO / TRUST WALL
   Monochrome partner / certification marks in a quiet row. Icon +
   wordmark pairs in Slate; logos only, no category captions.
   ============================================================ */
/* HOW MANY MARKS THERE ARE IS THE OPERATOR'S, so the wall is not
   ruled into five: `auto-fit` collapses the tracks nothing is
   standing in, which gives four marks four columns and seven marks
   seven, wrapping only when a mark would be narrower than it can be
   read at. A grid fixed at the number the first draft drew is the
   same mistake in CSS as a form with five fields in it. */
.logo-wall {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: var(--spacing-24);
  align-items: center;
}
.logo-item {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--spacing-8);
  color: var(--color-slate);
  font-family: var(--font-display); letter-spacing: var(--ls-subheading);
  font-size: var(--text-subheading);
  font-weight: var(--font-weight-semibold);
  opacity: 0.72;
  transition: opacity var(--duration-micro) ease;
}
@media (hover: hover) and (pointer: fine) {
  .logo-item:hover { opacity: 1; }
}
/* A MARK IS ITS LOGO WHERE THERE IS ONE AND ITS NAME WHERE THERE IS
   NOT, so a certification can go on the wall the day it is awarded
   rather than the day its artwork turns up. The record sets the
   picture and says whether there is one (js/cms-sections.js); which
   of the two is drawn is the component's own business, which is why
   it is settled here and not there. */
.logo-mark {
  max-height: 36px; max-width: 100%; width: auto;
  object-fit: contain;
  filter: grayscale(1);
  transition: filter var(--duration-micro) ease;
}
@media (hover: hover) and (pointer: fine) {
  .logo-item:hover .logo-mark { filter: grayscale(0); }
}
.logo-item:has(.logo-mark:not([hidden])) .logo-name { display: none; }

@media (max-width: 760px) { .logo-wall { grid-template-columns: repeat(2, 1fr); gap: var(--spacing-16); } }

/* ============================================================
   FAQ ACCORDION
   Native <details>/<summary> so it works with zero JS. Single
   hairline divider between rows; chevron rotates on open. Kept in
   a narrow reading column.
   ============================================================ */
.faq { max-width: 760px; margin: 0 auto; }
.faq-item { border-bottom: 1px solid var(--color-pebble); }
.faq-q {
  list-style: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--spacing-16);
  padding: var(--spacing-20) 0;
  font-family: var(--font-display); letter-spacing: var(--ls-subheading);
  font-size: var(--text-subheading);
  font-weight: var(--font-weight-semibold);
  color: var(--color-ink);
}
.faq-q::-webkit-details-marker { display: none; }
.faq-q:focus-visible { outline: 2px solid var(--color-race-green); outline-offset: 4px; }
.faq-q i[data-icon] { color: var(--color-steel); flex-shrink: 0; transition: transform var(--duration-micro) var(--ease-deck-soft); }
.faq-item[open] .faq-q i[data-icon] { transform: rotate(180deg); }
.faq-a { padding: 0 0 var(--spacing-20); color: var(--color-steel); font-size: var(--text-body-md); line-height: var(--leading-body-lg); max-width: 640px; }

/* The question is the tap target for the whole row, so it keeps its
   full padding on touch; only the 18px display size steps down, where
   a long question would otherwise run to four lines. */
@media (max-width: 480px) {
  .faq-q { font-size: var(--text-body-lg); padding-block: var(--spacing-16); gap: var(--spacing-12); }
}

/* ============================================================
   PRICING CARDS
   Three Snow tiers on the light canvas; the featured tier flips
   to a Deep Forest fill with a Blush CTA (Primary would vanish on
   Deep Forest). Feature list uses check / x status icons.
   ============================================================ */
.pricing-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--spacing-24);
  align-items: start;
}
.pricing-card {
  background: var(--surface-card-white);
  border-radius: var(--radius-cards);
  padding: var(--card-padding);
  display: flex;
  flex-direction: column;
  gap: var(--spacing-16);
}
.pricing-tier { font-size: var(--text-body-sm); font-weight: var(--font-weight-semibold); text-transform: uppercase; letter-spacing: 0.06em; color: var(--color-steel); }
.pricing-price {
  font-family: var(--font-display); letter-spacing: var(--ls-heading-lg);
  font-size: var(--text-heading-lg);
  font-weight: var(--font-weight-bold);
  line-height: 1;
  color: var(--color-obsidian);
}
.pricing-price span { font-family: var(--font-body); font-size: var(--text-body-md); font-weight: var(--font-weight-regular); color: var(--color-steel); letter-spacing: 0; }
.pricing-list { display: flex; flex-direction: column; gap: var(--spacing-12); flex: 1; }
.pricing-list li { display: flex; align-items: flex-start; gap: var(--spacing-12); font-size: var(--text-body-sm); color: var(--color-ink); }
.pricing-list i[data-icon] { flex-shrink: 0; margin-top: 1px; color: var(--success); }
.pricing-list li.is-off { color: var(--color-ash); }
.pricing-list li.is-off i[data-icon] { color: var(--color-pebble); }
.pricing-card--featured { background: var(--color-deep-forest); }
.pricing-card--featured .pricing-tier { color: var(--color-blush-pink); }
.pricing-card--featured .pricing-price { color: var(--color-mint-ice-cream); }
.pricing-card--featured .pricing-price span,
.pricing-card--featured .pricing-list li { color: rgba(var(--color-brand-highlight-rgb), 0.82); }
.pricing-card--featured .pricing-list i[data-icon] { color: var(--color-mint-ice-cream); }

@media (max-width: 860px) { .pricing-grid { grid-template-columns: 1fr; } }

/* ============================================================
   ARTICLE / BLOG CARDS
   Image-topped Snow cards: clipped cover, category badge, meta
   row, heading-md title, excerpt, text-link. Reusable for blog
   indexes and "related reading" rails.
   ============================================================ */
.article-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--spacing-24);
}
.article-card {
  background: var(--surface-card-white);
  border-radius: var(--radius-cards);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  text-decoration: none;
  color: inherit;
  /* The press takes the press band, not the hover's: a transform
     eased over 250ms on a symmetric curve lags the thumb that
     started it (DESIGN.md → Motion Philosophy, 120–160ms on the
     strong ease-out). Everything else here stays at micro speed. */
  transition: transform 160ms var(--ease-deck-out);
}
@media (hover: hover) and (pointer: fine) {
  .article-card:hover { text-decoration: none; transform: translateY(-3px); }
}
.article-card:active { transform: scale(0.99); }
.article-cover { position: relative; aspect-ratio: 16 / 10; overflow: hidden; }
.article-cover img { width: 100%; height: 100%; object-fit: cover; transition: transform var(--duration-deck-image, .6s) var(--ease-deck-soft); }
@media (hover: hover) and (pointer: fine) {
  .article-card:hover .article-cover img { transform: scale(1.05); }
}
.article-cover .badge { position: absolute; top: var(--spacing-12); left: var(--spacing-12); }
.article-body { padding: var(--card-padding-compact); display: flex; flex-direction: column; gap: var(--spacing-8); flex: 1; }
.article-meta { font-size: var(--text-body-xs); color: var(--color-steel); }
.article-title {
  font-family: var(--font-display); letter-spacing: var(--ls-heading-md);
  font-size: var(--text-heading-md);
  font-weight: var(--font-weight-semibold);
  line-height: 1.15;
  color: var(--color-ink);
}
.article-excerpt { font-size: var(--text-body-sm); line-height: var(--leading-body-sm); color: var(--color-steel); flex: 1; }
.article-link { font-size: var(--text-body-sm); font-weight: var(--font-weight-semibold); color: var(--color-race-green); margin-top: var(--spacing-4); }

@media (max-width: 900px) { .article-grid { grid-template-columns: 1fr; } }

/* ============================================================
   TEAM GRID
   Centred member cards with an initials avatar (no stock faces),
   name, role, and a quiet social row.
   ============================================================ */
.team-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--spacing-24);
}
.team-card {
  background: var(--surface-card-white);
  border-radius: var(--radius-cards);
  padding: var(--card-padding);
  text-align: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--spacing-4);
}
.team-card .avatar-circle { width: 72px; height: 72px; font-size: var(--text-heading-sm); background: var(--color-race-green); margin-bottom: var(--spacing-12); }
.team-name { font-family: var(--font-display); letter-spacing: var(--ls-rg); font-size: var(--text-heading-md); font-weight: var(--font-weight-semibold); color: var(--color-ink); }
.team-role { font-size: var(--text-body-sm); color: var(--color-steel); }
.team-social { display: flex; gap: var(--spacing-12); margin-top: var(--spacing-12); }
.team-social a {
  position: relative;
  display: inline-flex;
  color: var(--color-slate);
  transition: color var(--duration-micro) ease, transform 160ms var(--ease-deck-out);
}
@media (hover: hover) and (pointer: fine) {
  .team-social a:hover { color: var(--color-race-green); }
}
.team-social a:active { color: var(--color-race-green); transform: scale(0.9); }
.team-social a:focus-visible { outline: 2px solid var(--color-race-green); outline-offset: 4px; border-radius: var(--radius-md); }
/* An icon-only link has no label to grow the box with, and a 20px
   glyph is a mark rather than a target. The glyph keeps its size and
   the hit area grows behind it — the row's 12px gap means the boxes
   meet rather than overlap. */
@media (pointer: coarse) {
  .team-social a::after {
    content: "";
    position: absolute;
    top: 50%; left: 50%;
    width: var(--touch-target); height: var(--touch-target);
    transform: translate(-50%, -50%);
  }
}

@media (max-width: 900px) { .team-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 480px) { .team-grid { grid-template-columns: 1fr; } }

/* ============================================================
   NEWSLETTER PANEL
   Deep Forest capture panel: headline + email row. Pairs the
   email input with a Blush CTA (Primary would vanish on Forest).
   ============================================================ */
.newsletter { text-align: center; }
.newsletter .email-cta-row { max-width: 460px; margin: var(--spacing-24) auto 0; }
.newsletter .form-control { border-color: transparent; }

@media (max-width: 520px) {
  .newsletter .email-cta-row { flex-direction: column; }
  .newsletter .email-cta-row .btn { width: 100%; }
}

/* ============================================================
   EMBEDDABLE RAFFLE WIDGET
   One responsive product placed on third-party sites via an SDK
   or <div> embed (each instance ideally its own <iframe>). It shows
   a carousel of certified draws — draw image, "Win this {category}"
   eyebrow, title, live countdown and an "Enter Draw" CTA that
   reuses the hero banner's iridescent-sparkle wave (see the shared
   `.rw-cta` selectors up in the SHOWCASE CTA block).

   RESPONSIVE BY CONTAINER, NOT VIEWPORT — the widget reads its own
   box via CSS container queries, so the SAME markup adapts whether
   it sits in a 300px sidebar or a full-width mobile slot, and every
   demo frame on widgets.html morphs independently on one page. Four
   internal layout modes are triggered by the container's width AND
   height (mutually exclusive, so exactly one applies):
     · strip         — any box ≤160px tall            (728×90, 970×90, 320×100)
     · wide-feature  — ≥560px wide AND ≥180px tall     (970×250)
     · tall-panel    — <560px wide AND ≥420px tall     (300×600)
     · compact-card  — the base/default (narrow, mid)  (300×250, 336×280)

   Motion is transform/opacity/filter only; the CTA + eyebrow light
   reveals replay per active slide and are gated behind
   prefers-reduced-motion. Every value traces to tokens.css.
   ============================================================ */
.rw {
  container-type: size;            /* children query THIS box, not the page */
  container-name: rw;
  --rw-frame-pad: var(--element-gap);       /* 8px shell frame around the image (deck-tile language) */
  --rw-frame-radius: var(--radius-2xl-2);   /* image radius steps down concentrically from the shell */
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
  border-radius: var(--radius-cards-compact);
  /* A lift of the primary surface toward the highlight, not a green
     of its own: the shell is the tenant's dark surface with a sheen
     across it, and a literal here is a widget that stays The
     Raffle's whatever brand the publisher's shop is. */
  background: linear-gradient(135deg, color-mix(in oklab, var(--color-brand-primary) 84%, var(--color-brand-highlight)) 0%, var(--color-brand-primary) 100%);
  color: var(--color-snow);
  font-family: var(--font-body);
  isolation: isolate;
  -webkit-user-select: none;
  user-select: none;
}
.rw *, .rw *::before, .rw *::after { box-sizing: border-box; }

/* Viewport + sliding track — transform-only carousel */
.rw-viewport { position: relative; width: 100%; height: 100%; overflow: hidden; }
.rw-track {
  display: flex;
  width: 100%;
  height: 100%;
  will-change: transform;
  transition: transform var(--duration-carousel) var(--ease-deck-soft);
}
@media (prefers-reduced-motion: reduce) { .rw-track { transition: none; } }
.rw-slide {
  position: relative;
  flex: 0 0 100%;
  width: 100%;
  height: 100%;
  display: flex;                   /* base = compact-card: media over body */
  flex-direction: column;
  overflow: hidden;
}

/* ── Media ─────────────────────────────────────────────────────
   The image sits inside a small shell frame (var(--rw-frame-pad))
   with its own rounded corners, so the gradient shows as a border
   and the photo never bleeds to the widget edge — the same 8px-frame
   language the framed media-split (featured drop) and deck tiles use. */
.rw-media { position: relative; flex: 1 1 auto; min-height: 0; }
/* Explicit width/height (not inset alone) — an <img> is a replaced
   element and won't stretch to fill an inset box the way a div does,
   so it needs its size stated for object-fit:cover to fill the frame
   with equal padding on every side. */
.rw-img {
  position: absolute;
  inset: var(--rw-frame-pad);
  width: calc(100% - var(--rw-frame-pad) * 2);
  height: calc(100% - var(--rw-frame-pad) * 2);
  border-radius: var(--rw-frame-radius);
  object-fit: cover;
  display: block;
}
/* Bottom-anchored scrim, clipped to the framed image so the gutter
   stays clean gradient (compact/strip overlay legibility). */
.rw-scrim {
  position: absolute;
  inset: var(--rw-frame-pad);
  border-radius: var(--rw-frame-radius);
  background: linear-gradient(to top, rgba(var(--color-brand-primary-strong-rgb), 0.82) 0%, rgba(var(--color-brand-primary-strong-rgb), 0.30) 42%, rgba(var(--color-brand-primary-strong-rgb), 0) 72%);
  pointer-events: none;
}

/* ── Body — groups the copy, the actions and the brand so they travel
   together: stacked below the media in most modes, or as one centred
   column beside the media in the wide layout. This is what keeps the
   CTA + countdown directly under the host/price line rather than in a
   separate side panel. ── */
.rw-body {
  position: relative;
  z-index: 2;
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  gap: var(--spacing-12);
  padding: var(--spacing-16);
  min-width: 0;
}

/* ── Content (eyebrow + title + sub) ───────────────────────── */
.rw-content {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--spacing-8);
  min-width: 0;
}
/* Eyebrow reuses .showcase-eyebrow(.--draw); nudge the type down a
   touch for the smaller widget footprint. The base pill pulls the next
   line up 6px — override that to a positive margin so the eyebrow sits
   clearly apart from the title. */
.rw .showcase-eyebrow--draw {
  padding: 6px 9px;   /* side padding tracks the 14px main pill, scaled to the widget's smaller type */
  font-size: var(--text-caption);
  letter-spacing: 0.05em;
  margin-bottom: var(--spacing-8);
}
.rw .showcase-eyebrow--draw .hatton { font-size: 1.5em; }   /* scales with the smaller base */

.rw-title {
  font-family: var(--font-display);
  font-weight: var(--font-weight-light);
  letter-spacing: var(--ls-rg);
  line-height: 1.12;
  color: var(--color-snow);
  font-size: var(--text-heading-sm);
  text-shadow: rgba(0, 0, 0, 0.28) 0 1px 10px;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.rw-sub {
  display: flex;
  align-items: center;
  gap: var(--spacing-8);
  flex-wrap: wrap;
  font-size: var(--text-body-xs);
  color: rgba(255, 255, 255, 0.82);
}
.rw-sub strong { color: var(--color-snow); font-weight: var(--font-weight-semibold); }
.rw-sep { width: 3px; height: 3px; border-radius: 50%; background: rgba(255, 255, 255, 0.5); flex: 0 0 auto; }

/* ── Actions — the primary "Enter Draw" CTA leads, the countdown
   follows (button above/before the timer). Direction flips per mode:
   a row where the box is wide, a stack where it is tall. ── */
.rw-actions {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--spacing-8);
  min-width: 0;
}
.rw-cta {
  width: 100%;
  justify-content: center;
  padding: 12px 18px;
  font-size: var(--text-body);
}

/* Countdown reuses the hero .countdown-pill skin, shrunk to fit.
   In compact contexts it drops the italic "ending in" label. */
.rw .countdown-pill {
  padding: 9px 14px;
  font-size: var(--text-body-sm);
  gap: 6px;
}
.rw .countdown-pill .icon { width: 15px; height: 15px; }
.rw .countdown-pill__label { display: none; }
.rw-countdown { align-self: stretch; }
.rw-countdown .countdown-pill { width: 100%; }   /* full-width in stacked modes; overridden to auto in row modes */

/* ── Carousel controls ─────────────────────────────────────────
   Navigation is dots + swipe + autoplay. Prev/next arrows were
   dropped: at any size their vertical-centre position overlapped the
   eyebrow (narrow modes) or the CTA (wide), and the dots already give
   direct, non-overlapping access to every draw. */
.rw-dots {
  position: absolute;
  z-index: 5;
  bottom: 8px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  gap: 6px;
  padding: 5px 8px;
  border-radius: var(--radius-pill);
  background: rgba(var(--color-brand-primary-strong-rgb), 0.34);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
}
.rw-dot {
  width: 6px;
  height: 6px;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.42);
  cursor: pointer;
  transition: background var(--duration-micro) ease, transform var(--duration-micro) ease;
}
@media (hover: hover) and (pointer: fine) {
  .rw-dot:hover { background: rgba(255, 255, 255, 0.7); }
}
.rw-dot.is-active { background: var(--color-snow); transform: scale(1.15); }
.rw-dot:focus-visible { outline: 2px solid var(--color-mint-ice-cream); outline-offset: 2px; }
/* A 6px dot is a mark, not a target. On touch it keeps its size and
   gains a hit box that overlaps its neighbours' gap — the dots are
   6px apart, so the boxes are capped at the gap to stay distinct. */
@media (pointer: coarse) {
  .rw-dot { position: relative; }
  .rw-dot::after {
    content: "";
    position: absolute;
    top: 50%; left: 50%;
    width: 24px; height: 32px;
    transform: translate(-50%, -50%);
  }
}

/* ── Per-active-slide effect replay ────────────────────────────
   All slides are pre-rendered in the track, so the hero's mount-time
   animations can't fire per view. Instead the eyebrow light-reveal
   and the CTA iridescent-sparkle wave are held off until a slide
   becomes `.is-active`; toggling the class restarts them, replaying
   the reveal each time a draw scrolls into view. Reuses the hero
   @keyframes verbatim. */
.rw .showcase-eyebrow--draw,
.rw .showcase-eyebrow--draw::before,
.rw .showcase-eyebrow--draw::after,
.rw .rw-cta::before,
.rw .rw-cta::after { animation: none; }
@media (prefers-reduced-motion: no-preference) {
  .rw-slide.is-active .showcase-eyebrow--draw {
    animation: se-eyebrow-pop 0.6s var(--ease-deck-out) both,
               se-eyebrow-glow 1.1s linear 0.2s backwards;
  }
  .rw-slide.is-active .showcase-eyebrow--draw::before { animation: se-eyebrow-sheen 0.9s linear 0.32s backwards; }
  .rw-slide.is-active .showcase-eyebrow--draw::after  { animation: se-eyebrow-ring  0.95s linear 0.28s backwards; }

  .rw-slide.is-active .rw-cta::before { animation: se-eyebrow-sheen 0.9s  linear 0.44s backwards; }
  .rw-slide.is-active .rw-cta::after  { animation: se-eyebrow-ring  0.95s linear 0.40s backwards; }
}

/* ── Whole-card light reveal (subtle) ──────────────────────────
   A drastically toned-down cousin of the eyebrow's light sweep, run
   across the ENTIRE card as each slide becomes active — same
   iridescent palette and screen blend, but wide, slow and low-opacity
   so it reads as the whole surface briefly catching light. It starts a
   touch BEFORE the eyebrow's own sweep, then the eyebrow lights up on
   top of it. Sits above the media but below the copy (z-index 1) so
   text stays crisp; transform/opacity only, gated behind
   prefers-reduced-motion (at rest it is fully transparent). */
.rw-slide::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  opacity: 0;
  mix-blend-mode: screen;
  background: linear-gradient(100deg,
      transparent 30%,
      rgba(110, 200, 255, 0.10) 42%,   /* cyan   */
      rgba(130, 255, 205, 0.13) 48%,   /* mint   */
      rgba(255, 255, 255, 0.18) 52%,   /* core   */
      rgba(255, 226, 150, 0.11) 57%,   /* gold   */
      rgba(255, 150, 240, 0.12) 63%,   /* orchid */
      rgba(180, 150, 255, 0.10) 70%,   /* violet */
      transparent 82%);
  transform: translateX(-110%) skewX(-10deg);
}
@media (prefers-reduced-motion: no-preference) {
  .rw-slide.is-active::after { animation: rw-card-sheen 1.5s var(--ease-deck-soft) 0.1s backwards; }
}
/* Light gliding the full width of the card, once, then gone */
@keyframes rw-card-sheen {
  0%   { opacity: 0; transform: translateX(-110%) skewX(-10deg); }
  14%  { opacity: 1; }
  86%  { opacity: 1; }
  100% { opacity: 0; transform: translateX(110%) skewX(-10deg); }
}

/* ══ MODE: strip — any box ≤160px tall (728×90, 970×90, 320×100) ══
   A single horizontal row: thumbnail · [eyebrow + title] · CTA · count-
   down. No scrim/overlay (media is a discrete thumb), no dots. The body
   turns into a row so the copy and the actions sit side by side.
   ALWAYS-VISIBLE at every strip width: thumb, eyebrow, title, CTA — so
   the eyebrow is kept compact (one line) and the TITLE leads. The live
   countdown is the first thing to fold away when the row runs short (it
   only returns on roomy leaderboards below), and the host/price sub is
   always dropped here — the strip is a teaser, tap through for the rest. */
@container rw (max-height: 160px) {
  .rw-slide {
    flex-direction: row;
    align-items: center;
    gap: var(--spacing-12);
    padding: 0 var(--spacing-12) 0 0;
  }
  .rw-media { flex: 0 0 auto; width: clamp(64px, 24%, 120px); height: 100%; }
  .rw-scrim { display: none; }
  .rw-body {
    flex: 1 1 auto;
    flex-direction: row;
    align-items: center;
    gap: var(--spacing-12);
    padding: var(--spacing-8) 0;
    min-width: 0;
  }
  .rw-content { flex: 1 1 auto; gap: 3px; justify-content: center; min-width: 0; }
  /* Eyebrow compact + always on, so the title can be the prominent line.
     nowrap holds it to a single tidy pill (a flex min-content pass will
     otherwise break it in two even when one line would fit). */
  .rw .showcase-eyebrow--draw { padding: 2px 8px; font-size: var(--text-caption); letter-spacing: 0.04em; margin-bottom: 0; max-width: 100%; white-space: nowrap; }
  .rw .showcase-eyebrow--draw .hatton { font-size: 1.3em; }
  .rw-title {
    font-size: var(--text-body-lg);
    font-weight: var(--font-weight-regular);
    line-height: 1.15;
    -webkit-line-clamp: 1;
  }
  .rw-sub { display: none; }
  .rw-actions {
    flex: 0 0 auto;
    flex-direction: row;
    align-items: center;
    gap: var(--spacing-8);
  }
  .rw-cta { width: auto; padding: 10px 16px; font-size: var(--text-body-sm); }
  .rw-countdown { display: none; }   /* folds away first; returns on roomy strips below */
  .rw-countdown .countdown-pill { width: auto; }
  .rw-dots { display: none; }
}
/* Roomy strips (leaderboards ≥ 640 wide): the title steps up to lead
   (scaling with the extra width, capped so it stays on one line at 90px
   tall) and the live countdown returns alongside the CTA. */
@container rw (max-height: 160px) and (min-width: 640px) {
  .rw-media { width: clamp(96px, 20%, 120px); }
  .rw-content { gap: 5px; }
  .rw .showcase-eyebrow--draw { padding: 3px 10px; font-size: var(--text-body-xs); }
  .rw-title { font-size: clamp(var(--text-heading-sm), 3.3cqw, 25px); font-weight: var(--font-weight-light); }
  .rw-countdown { display: block; }
}
/* Very narrow strips (mobile banners, e.g. 320×100): trim the thumb, the
   CTA and the eyebrow so the always-on eyebrow + title keep room to read
   and the eyebrow holds a single line right down to the smallest box. */
@container rw (max-height: 160px) and (max-width: 400px) {
  .rw-slide { gap: var(--spacing-8); }
  .rw-media { width: 56px; }
  .rw-body { gap: var(--spacing-8); }
  .rw .showcase-eyebrow--draw { padding: 2px 7px; letter-spacing: 0.02em; }
  .rw .showcase-eyebrow--draw .hatton { font-size: 1.15em; }
  .rw-title { font-size: var(--text-body-md); }
  .rw-cta { padding: 8px 12px; font-size: var(--text-body-xs); }
}

/* ══ MODE: wide-feature — ≥560px wide & ≥161px tall (970×250) ══
   Two zones: media (34%) · one centred body column. Inside the body the
   CTA + countdown sit as a row below the host/price line — no separate
   side panel — so the countdown sizes to its content and the title gets
   the full body width (fluid via cqw). Vertical padding + gaps track the
   container height (cqh) and the body is vertically centred, so the whole
   stack stays on the card at every height from a short banner up to a
   full billboard — the actions never spill past the floor. */
@container rw (min-width: 560px) and (min-height: 161px) {
  .rw-slide {
    flex-direction: row;
    align-items: stretch;
  }
  .rw-media { flex: 0 0 34%; height: 100%; }
  .rw-scrim { display: none; }
  .rw-body {
    flex: 1 1 auto;
    justify-content: center;
    gap: clamp(var(--spacing-8), 3cqh, var(--spacing-16));
    padding: clamp(var(--spacing-16), 7cqh, var(--spacing-32)) var(--spacing-32);
    min-width: 0;
  }
  .rw-content { gap: var(--spacing-8); flex: 0 0 auto; }
  .rw-title { font-size: clamp(22px, 4.4cqw, 34px); -webkit-line-clamp: 2; }
  .rw-sub { font-size: var(--text-body-sm); }
  .rw-actions {
    flex-direction: row;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--spacing-12);
  }
  .rw-cta { width: auto; padding: 13px 24px; font-size: var(--text-body-md); }
  .rw .countdown-pill { padding: 12px 16px; font-size: var(--text-body); }
  .rw-countdown { align-self: auto; }
  .rw-countdown .countdown-pill { width: auto; }
  .rw-dots { bottom: 10px; left: auto; right: 12px; transform: none; }
}
/* Full billboards (≥ 250 tall, incl. the 970×250 flagship): the copy rises
   to the top and the actions pin to the floor — the "what/who" vs "act now"
   separation the billboard was built for. Shorter wide banners (below)
   stay centred and compact instead. */
@container rw (min-width: 560px) and (min-height: 250px) {
  .rw-body { justify-content: space-between; }
  .rw-actions { margin-top: auto; }
}
/* The "ending in" label only when the box is wide AND tall enough for the
   CTA + labelled countdown to stay on ONE row — otherwise the pill stays
   compact so the actions never wrap a long two-line title past the floor. */
@container rw (min-width: 720px) and (min-height: 250px) {
  .rw .countdown-pill__label { display: inline; }
}
/* Short wide banners (161–249 tall): drop the host/price line and the
   countdown label, hold the title to a tighter size, and trim the CTA so
   the actions stay a single row that clears the floor — a long, two-line
   title would otherwise push a wrapped countdown past the bottom edge. The
   min-height floor keeps this off the shorter strip mode (≤160 tall). */
@container rw (min-width: 560px) and (min-height: 161px) and (max-height: 249px) {
  .rw-body { padding-block: clamp(12px, 6cqh, 18px); gap: var(--spacing-8); }
  .rw-sub { display: none; }
  .rw-title { font-size: clamp(20px, 3.6cqw, 26px); line-height: 1.12; }
  .rw-cta { padding: 10px 20px; font-size: var(--text-body-sm); }
  .rw .countdown-pill { padding: 9px 14px; font-size: var(--text-body-sm); }
}

/* ══ MODE: tall-panel — <560px wide & ≥420px tall (300×600) ══
   Full vertical experience: tall media, then the complete stack. CTA
   sits above the countdown, both full-width, pinned to the bottom. With
   plenty of vertical room and a lot of the prize on show, the title is
   allowed to grow a touch with the container height (cqh). */
@container rw (max-width: 559px) and (min-height: 420px) {
  .rw-media { flex: 0 0 46%; }
  .rw-body { flex: 1 1 auto; gap: var(--spacing-12); padding: var(--spacing-20); }
  .rw-content { gap: var(--spacing-8); }
  .rw-title { font-size: clamp(var(--text-heading-md), 4.4cqh, var(--text-heading)); }
  .rw-sub { font-size: var(--text-body-sm); }
  .rw-actions { align-items: stretch; gap: var(--spacing-12); margin-top: auto; }
  .rw-cta { padding: 14px 22px; font-size: var(--text-body-md); }
  .rw .countdown-pill { padding: 11px 16px; font-size: var(--text-body); justify-content: center; }
  .rw .countdown-pill__label { display: inline; }
  .rw-dots { bottom: auto; top: 10px; }
}
/* Short tall panels (420–479 tall): at the low end there isn't room for the
   host/price line beneath a long two-line title, so drop it and tighten the
   padding — the CTA + countdown must stay on the floor. The 300×600 preset
   sits well above this and keeps the full stack. */
@container rw (max-width: 559px) and (min-height: 420px) and (max-height: 479px) {
  .rw-body { padding: var(--spacing-16); }
  .rw-sub { display: none; }
}

/* ══ MODE: compact-card — base default (300×250, 336×280) ═══════
   Overlay the eyebrow + title on the lower media, keep the CTA on the
   card floor so the widget reads complete at a glance. The CTA leads and
   is always shown full-width; the live countdown only joins it once the
   card is wide enough for both to sit side by side without crowding the
   CTA off the floor (below) — otherwise it folds away. */
@container rw (max-width: 559px) and (min-height: 161px) and (max-height: 419px) {
  .rw-slide { justify-content: flex-end; }
  .rw-media { position: absolute; inset: 0; flex: none; }
  .rw-body { gap: var(--spacing-8); padding: var(--spacing-12) var(--spacing-16) var(--spacing-16); }
  .rw-content { gap: 6px; }
  .rw-title { font-size: var(--text-heading-md); line-height: 1.08; }
  .rw-sub { display: none; }
  .rw-actions {
    flex-direction: row;
    align-items: center;
    gap: var(--spacing-8);
  }
  .rw-cta { flex: 1 1 auto; width: auto; }
  .rw-countdown { flex: 0 0 auto; align-self: auto; display: none; }   /* folds away on narrow cards */
  .rw-countdown .countdown-pill { width: auto; }
  .rw-dots { bottom: auto; top: 10px; background: rgba(var(--color-brand-primary-strong-rgb), 0.44); }
}
/* Wide enough (≥ 336px) for the CTA and a compact live countdown to
   share the floor without either being clipped. */
@container rw (min-width: 336px) and (max-width: 559px) and (min-height: 161px) and (max-height: 419px) {
  .rw-countdown { display: block; }
  .rw .countdown-pill { padding: 8px 12px; font-size: var(--text-body-sm); }
}

/* ══ DRESSING: light ═══════════════════════════════════════════
   The same widget, for a publisher whose page is white. A widget
   lands on somebody else's site, and that site is usually one of
   two things — so the dressing is a RECORD (js/widget-data.js,
   authored under Storefront ▸ Widgets) and a class, never a second
   stylesheet: one widget, two grounds, and
   every layout rule in this block shared between them.

   It is not a light THEME. There is one light theme (DESIGN.md);
   this is which ground the widget's own card is drawn on, which is
   the section-ground question asked of an object that travels.

   WHICH IS WHY THE COMPACT CARD BARELY TURNS. In that mode the
   copy stands on the photograph, and text over photography is
   white — so what turns over is the card AROUND the picture and
   never the words on it. That is the showcase hero's rule
   (DESIGN.md → Section Ground) applied one component down. */
.rw--light {
  background: var(--color-snow);
  color: var(--color-ink);
  /* DESIGN.md → Elevation: the card hairline for a light surface.
     Inset, because in a real embed the widget IS the document —
     an outset ring would sit half outside its own iframe. */
  box-shadow: inset 0 0 0 1px rgba(var(--color-brand-primary-rgb), 0.10);
}
.rw--light .rw-title { color: var(--color-obsidian); text-shadow: none; }
.rw--light .rw-sub { color: var(--color-slate); }
.rw--light .rw-sub strong { color: var(--color-ink); }
.rw--light .rw-sep { background: var(--color-pebble); }

/* The CTA stands on the widget's own ground in three of the four
   modes, so it is the platform's Primary Pill — Deep Forest fill,
   Mint text (DESIGN.md → Primary Pill Button). Flat Glass is the
   material for a control over Deep Forest or over a photograph,
   and neither is what it is standing on here. The one-time
   entrance wave is untouched: it is an effect on the button, not
   a colour of it. */
.rw--light .rw-cta {
  background: var(--color-deep-forest);
  color: var(--color-mint-ice-cream);
  border-color: rgba(var(--color-brand-highlight-rgb), 0.18);
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}
@media (hover: hover) and (pointer: fine) {
  .rw--light .rw-cta:hover { background: var(--color-race-green); color: var(--color-mint-ice-cream); border-color: rgba(var(--color-brand-highlight-rgb), 0.18); }
}

/* The dots sit on the card rather than on the picture, so the
   blurred plate goes with the dark ground that needed it — a wash
   exists to lift a control off photography. */
.rw--light .rw-dots {
  background: var(--color-fog);
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}
.rw--light .rw-dot { background: var(--color-ash); }
@media (hover: hover) and (pointer: fine) {
  .rw--light .rw-dot:hover { background: var(--color-steel); }
}
.rw--light .rw-dot.is-active { background: var(--color-deep-forest); }
.rw--light .rw-dot:focus-visible { outline-color: var(--color-deep-forest); }

/* The whole-card sweep is light crossing a dark surface. There is no
   dark here for it to cross, and a screen-blended iridescence over
   Snow reads as a colour cast rather than as light — so the card
   does not sweep. The eyebrow and the CTA keep their own entrance,
   which is where that effect was drawn to live. */
.rw--light .rw-slide::after { display: none; }

/* Compact card — the one mode that stands its copy ON the picture.
   A light widget doing that would be a dark card on a white page,
   and the setting would read as having done nothing — so here the
   dressing takes the shape the tall panel already has: the picture
   on top, the words on the card's own floor. The media absorbs
   whatever the copy does not need, so a long prize name eats into
   the photograph rather than off the bottom of the slot.

   Declared after the mode blocks above so it wins on source order;
   a container query adds no specificity of its own. */
@container rw (max-width: 559px) and (min-height: 161px) and (max-height: 419px) {
  .rw--light .rw-slide { justify-content: flex-start; }
  .rw--light .rw-media { position: relative; inset: auto; flex: 1 1 auto; min-height: 0; }
  .rw--light .rw-scrim { display: none; }
  .rw--light .rw-body { padding: var(--spacing-12) var(--spacing-16) var(--spacing-16); }
  .rw--light .rw-title { font-size: var(--text-subheading); }
  /* The dots are the one thing still standing on the picture, so
     they keep the plate that lifts them off it. */
  .rw--light .rw-dots {
    background: rgba(var(--color-brand-primary-strong-rgb), 0.44);
    -webkit-backdrop-filter: blur(6px);
    backdrop-filter: blur(6px);
  }
  .rw--light .rw-dot { background: rgba(255, 255, 255, 0.42); }
  .rw--light .rw-dot:hover { background: rgba(255, 255, 255, 0.7); }
  .rw--light .rw-dot.is-active { background: var(--color-snow); }
  .rw--light .rw-dot:focus-visible { outline-color: var(--color-mint-ice-cream); }
}

/* Demo-only frame scaffolding (widgets.html + components.html). Each
   preset is a real IAB/Google ad slot at its exact pixel size; the
   playground is drag-resizable so one widget visibly morphs across
   modes. Not used on product pages. */
.rw-frame {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-cards-compact);
  box-shadow: var(--shadow-flat-hairline);
  background: var(--color-race-green);
  /* Fixed presets keep their exact IAB pixel size so each triggers its
     intended mode; oversized ones sit inside a .rw-scroll rail. Only the
     playground caps to the page width (below). */
  flex: 0 0 auto;
}
.rw-frame--billboard   { width: 970px; height: 250px; }
.rw-frame--halfpage    { width: 300px; height: 600px; }
.rw-frame--rectangle   { width: 300px; height: 250px; }
.rw-frame--leaderboard { width: 728px; height: 90px; }
.rw-frame--mobile      { width: 320px; height: 100px; }
.rw-frame--playground {
  width: min(720px, 100%);
  height: 300px;
  resize: both;
  min-width: 300px;
  min-height: 90px;
  max-width: 100%;
}
/* Oversized presets get a horizontal-scroll rail on narrow screens so
   they stay at true pixel size without breaking page layout. */
.rw-scroll { max-width: 100%; overflow-x: auto; overscroll-behavior-x: contain; padding-bottom: var(--spacing-8); }

/* Demo preset caption — dimensions + placement note under each frame */
.rw-caption { margin-top: var(--spacing-12); }
.rw-caption-size {
  /* A measurement, not code — Inter with tabular figures so the
     dimensions line up down the column of presets. */
  font-family: var(--font-body);
  font-weight: var(--font-weight-medium);
  font-variant-numeric: tabular-nums;
  font-size: var(--text-body-xs);
  color: var(--color-race-green);
  background: var(--color-mint-ice-cream);
  padding: 2px 8px;
  border-radius: var(--radius-md);
}
.rw-caption-name { font-weight: var(--font-weight-semibold); color: var(--color-ink); margin-left: var(--spacing-8); }
.rw-caption-note { display: block; margin-top: 4px; font-size: var(--text-body-xs); color: var(--color-steel); }

/* Copy-paste embed snippet — Deep Forest code card */
.rw-code {
  margin: 0;
  padding: var(--spacing-20) var(--spacing-24);
  background: var(--surface-main-dark);
  color: var(--color-mint-ice-cream);
  border-radius: var(--radius-cards-compact);
  /* Literal code the host copies — the one sanctioned mono face. */
  font-family: var(--font-mono);
  font-size: var(--text-body-sm);
  line-height: 1.7;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}
.rw-code .tok-tag { color: var(--color-blush-pink); }
.rw-code .tok-attr { color: #9fe0c4; }
.rw-code .tok-str { color: var(--color-snow); }
.rw-code .tok-com { color: rgba(var(--color-brand-highlight-rgb), 0.5); }

/* ============================================================
   SECTION GROUND (per-section light / dark)
   A storefront section that sits on a SOLID colour can be set
   light or dark one section at a time, from its own settings in
   the console (js/cms-sections.js → the ground a section is drawn
   on). It is not a dark theme for the page — there is one light
   theme (DESIGN.md) — it is which of the platform's two grounds
   this section is drawn on, and it is what gives a page its
   rhythm: a break between two light sections, and no two of the
   same colour left touching when the one between them is switched
   off.

   THREE SHAPES OF GROUND, and they are the shapes the pages already
   draw. WHERE A SECTION HAS A CARD IT IS THE CARD THAT TURNS OVER —
   the page behind it is not that section's to paint, and painting it
   would put a dark band round a light card rather than turning the
   card over. Only a section whose content sits directly on the page
   canvas is a band.

     PANEL  — the contained 28px surface: .dark-panel + .surface-forest
              for the dark dress, .mint-panel + .surface-mint for the
              light one. Both already exist; nothing here re-states
              what either is made of.
     FRAMED — the framed editorial card (the featured drop), whose
              ground is the card: .media-split--forest for dark,
              .media-split--parchment for light, canvas showing round
              it either way. Its two colourways live with the
              component, in MEDIA SPLIT.
     BAND   — the section's own full width. Dark is .surface-forest on
              the <section> itself; light is the page canvas, which
              is what the section already sits on, so it is no class
              at all.

   SO .surface-forest AND .surface-mint ARE THE ONE HOOK: they mean
   "the ground under this content is Deep Forest / Mint Ice Cream",
   wherever they are and whatever shape they are. Everything below
   is a component saying what it looks like on the ground it was
   not originally drawn for — the other half of a pairing that
   already existed for the feature grid and the provenance chain.

   The rules that follow are all colour and one glass recipe swap.
   NO SPACING: a band's padding IS the coloured ground its content
   sits on, and .section pays half the gap on each of its own edges
   (base.css → Layout primitives), so the 48px below a band's last
   line is already the same 48px above its first. The override that
   used to sit here — a second 96px of padding-bottom, to draw an
   edge the top-only model left missing — was that model's absence
   written out per component, and it made a dark band the only thing
   on the page with 192px under it. No component changes shape with
   its ground; a section that is unreadable on Deep Forest is a
   section that should not be offered the choice (declare no
   `theme`).
   ============================================================ */

/* ── Buttons ────────────────────────────────────────────────
   DESIGN.md → Button System Principles: a dark fill has no
   contrast on a dark ground, and the dark ground's accent is one
   that loses itself on Mint. So an action
   takes the material its ground calls for rather than the one the
   page happened to be written with — which is what lets the same
   markup be set either way. */
.surface-forest .btn-primary,
.surface-forest .btn-dark {
  background: var(--dark-accent);
  color: var(--dark-on-accent);
  border-color: rgba(var(--color-brand-primary-rgb), 0.10);
  box-shadow: none;
}
@media (hover: hover) and (pointer: fine) {
  .surface-forest .btn-primary:hover,
  .surface-forest .btn-dark:hover { color: var(--dark-on-accent); border-color: color-mix(in srgb, var(--dark-accent) 42%, transparent); }
}
.surface-forest .btn-primary:focus-visible,
.surface-forest .btn-dark:focus-visible { outline-color: var(--dark-heading); }
/* Outlined and Ghost assume a light ground: on Deep Forest they are
   an invisible stroke, so they take Flat Glass. */
.surface-forest .btn-secondary,
.surface-forest .btn-ghost {
  background: var(--glass-fill-dark);
  color: var(--dark-heading);
  border-color: var(--deck-glass-border);
  -webkit-backdrop-filter: blur(var(--glass-blur));
  backdrop-filter: blur(var(--glass-blur));
}
@media (hover: hover) and (pointer: fine) {
  .surface-forest .btn-secondary:hover,
  .surface-forest .btn-ghost:hover { color: var(--dark-heading); background: rgba(255, 255, 255, 0.12); border-color: var(--glass-border-light); }
}
/* Mint Tertiary is the light-surface sibling of Blush Secondary and
   does not belong on Deep Forest (DESIGN.md → Mint Tertiary), so the
   framed card's CTA changes hands with the card. */
.surface-forest .btn-mint { background: var(--dark-accent); color: var(--dark-on-accent); border-color: rgba(var(--color-brand-primary-rgb), 0.10); }
@media (hover: hover) and (pointer: fine) {
  .surface-forest .btn-mint:hover { background: var(--dark-accent); color: var(--dark-on-accent); border-color: color-mix(in srgb, var(--dark-accent) 42%, transparent); }
}

.surface-mint .btn-pink-accent,
.surface-mint .btn-glass {
  background: var(--color-deep-forest);
  color: var(--color-mint-ice-cream);
  border-color: rgba(var(--color-brand-highlight-rgb), 0.18);
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}
@media (hover: hover) and (pointer: fine) {
  .surface-mint .btn-pink-accent:hover,
  .surface-mint .btn-glass:hover { background: var(--color-deep-forest); color: var(--color-mint-ice-cream); border-color: rgba(var(--color-brand-highlight-rgb), 0.32); }
}
.surface-mint .btn-pink-accent:focus-visible,
.surface-mint .btn-glass:focus-visible { outline-color: var(--color-race-green); }

/* ── Rail arrows ────────────────────────────────────────────
   The primary variant is a Deep Forest fill for exactly the reason
   the Primary button is, so it moves for the same one. */
.surface-forest .rail-nav,
.surface-forest .rail-nav-primary {
  background: var(--glass-fill-dark);
  border-color: var(--deck-glass-border);
  color: var(--dark-heading);
}
@media (hover: hover) and (pointer: fine) {
  .surface-forest .rail-nav:hover:not(:disabled),
  .surface-forest .rail-nav-primary:hover:not(:disabled) { border-color: var(--glass-border-light); }
}
.surface-forest .rail-nav:focus-visible { outline-color: var(--dark-heading); }

/* ── Editorial split (how-it-works steps, featured drop) ──── */
.surface-forest .media-split-title,
.surface-forest .media-split-copy h3,
.surface-forest .media-split-copy h4,
.surface-forest .media-split-stat-num { color: var(--dark-heading); }
.surface-forest .media-split-body,
.surface-forest .media-split-copy p { color: var(--dark-body); }
.surface-forest .media-split-list li,
.surface-forest .media-split-copy ul > li,
.surface-forest .media-split-copy ol > li { color: var(--dark-heading); }
.surface-forest .media-split-list i[data-icon],
.surface-forest .media-split-copy li > i[data-icon] { color: var(--dark-accent); }
.surface-forest .media-split-stats { border-top-color: var(--dark-line); }
.surface-forest .media-split-stat-label { color: var(--dark-body-quiet); }

/* ── FAQ ───────────────────────────────────────────────────── */
.surface-forest .faq-item { border-bottom-color: var(--dark-line); }
.surface-forest .faq-q { color: var(--dark-heading); }
.surface-forest .faq-q i[data-icon] { color: var(--dark-body-quiet); }
.surface-forest .faq-q:focus-visible { outline-color: var(--dark-heading); }
.surface-forest .faq-a { color: var(--dark-body); }

/* ── Trust / certification wall ─────────────────────────────
   A mark's artwork is knocked back to one ink so a wall of logos
   reads as a row rather than as a paint chart; on Deep Forest that
   ink is the light one. Hover still restores the mark's own
   colours, which is what it does on either ground. */
.surface-forest .logo-item { color: var(--dark-body); }
.surface-forest .logo-mark { filter: grayscale(1) brightness(0) invert(1); }
@media (hover: hover) and (pointer: fine) {
  .surface-forest .logo-item:hover .logo-mark { filter: none; }
}

/* ── Figures ───────────────────────────────────────────────── */
.surface-forest .stat-value { color: var(--dark-heading); }
.surface-forest .stat-label { color: var(--dark-body); }

/* ── Three-promise band ─────────────────────────────────────
   The dark Glass Card recipe, which is the other half of the
   pairing the component was already written with (DESIGN.md →
   Glass Card). */
.surface-forest .trust-band-item {
  background: var(--glass-fill-dark);
  border-color: var(--deck-glass-border);
}
.surface-forest .trust-band-item i[data-icon] { color: var(--dark-accent); }
.surface-forest .trust-band-title { color: var(--dark-heading); }
.surface-forest .trust-band-body { color: var(--dark-body); }

/* ── The steps grid, the other way round ────────────────────
   Written for Deep Forest, so it is the light Glass Card recipe
   that has to be stated. Icons go Race Green rather than Blush:
   Blush Pink and Mint Ice Cream are both pale pastels and one on
   the other is not a contrast (DESIGN.md → Glass Card, light). */
.surface-mint .step-item {
  background: var(--glass-fill-light);
  border-color: rgba(var(--color-brand-primary-rgb), 0.10);
}
.surface-mint .step-icon { color: var(--color-race-green); }
.surface-mint .step-title { color: var(--color-race-green); }
.surface-mint .step-desc { color: rgba(var(--color-brand-primary-rgb), 0.7); }

/* ── Feature grid on Mint ───────────────────────────────────
   The Snow card is a card on the canvas; on a Mint ground it is
   the light Glass Card, same as everything else nested there. */
.surface-mint .feature-card {
  background: var(--glass-fill-light);
  border: 1px solid rgba(var(--color-brand-primary-rgb), 0.10);
  -webkit-backdrop-filter: blur(var(--glass-blur));
  backdrop-filter: blur(var(--glass-blur));
}
.surface-mint .feature-card i[data-icon] { color: var(--color-race-green); }
.surface-mint .feature-card-title { color: var(--color-race-green); }
.surface-mint .feature-card-body { color: rgba(var(--color-brand-primary-rgb), 0.7); }

/* ── Host spotlight on Mint ─────────────────────────────────── */
.surface-mint .host-spotlight .eyebrow-label,
.surface-mint .host-spotlight-quote,
.surface-mint .host-spotlight-name,
.surface-mint .host-spotlight-stat-num { color: var(--color-race-green); }
.surface-mint .host-spotlight-role,
.surface-mint .host-spotlight-stat-label { color: rgba(var(--color-brand-primary-rgb), 0.6); }
.surface-mint .host-spotlight-stats { border-top-color: rgba(var(--color-brand-primary-rgb), 0.14); }

/* ── Promo band on Mint ─────────────────────────────────────
   The chips are scoped to the band: .badge-overlay is also the
   badge a prize tile pins over its photograph, and a tile inside a
   Mint section is still a photograph. */
.surface-mint .promo-band-media { background: rgba(var(--color-brand-primary-rgb), 0.06); }
.surface-mint .promo-band-chips .badge-overlay {
  background: rgba(var(--color-brand-primary-rgb), 0.06);
  color: var(--color-race-green);
  border-color: rgba(var(--color-brand-primary-rgb), 0.18);
}

/* ============================================================
   COMPONENT GALLERY (components.html scaffolding only)
   Specimen framing for the pattern-library page: a divider label
   above each block and a soft frame around light specimens so
   Snow-on-Mist examples read as discrete samples.
   ============================================================ */
.spec { padding-top: var(--spacing-48); }
/* The gallery keeps a tighter rhythm than a page — 48px between
   specimens, carried on the top edge, because a specimen is a row in
   a reference rather than a section of an argument. The last one
   still owes the footer the half of the page rhythm every block in
   the flow pays (base.css → Layout primitives), or the closing
   specimen fuses to the dark surface under it. */
section.spec:last-of-type { padding-bottom: var(--spacing-48); }
@media (max-width: 768px) { section.spec:last-of-type { padding-bottom: var(--spacing-32); } }
.spec-head { display: flex; align-items: baseline; gap: var(--spacing-12); margin-bottom: var(--spacing-24); flex-wrap: wrap; }
.spec-num { font-family: var(--font-display); letter-spacing: var(--ls-rg); font-size: var(--text-body-sm); font-weight: var(--font-weight-bold); color: var(--color-ember); }
.spec-name { font-family: var(--font-display); letter-spacing: var(--ls-rg); font-size: var(--text-heading-sm); font-weight: var(--font-weight-semibold); color: var(--color-ink); }
.spec-note { font-size: var(--text-body-sm); color: var(--color-steel); }

/* Compact Deep Forest demo surface — shows Blush / Flat Glass
   controls and overlay badges on their native dark surface. */
.spec-dark { background: var(--color-deep-forest); border-radius: var(--radius-cards-compact); padding: var(--spacing-24); }
/* Swatch row — wraps chips/buttons/badges with even spacing. */
.spec-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--spacing-12); }

/* Anchored sections land clear of the fixed dev bar + nav. */
.spec[id], .spec-part[id] { scroll-margin-top: calc(var(--dev-bar-h) + var(--nav-h) + var(--spacing-16)); }

/* Atomic part divider — opens each tier (Atoms / Molecules /
   Organisms) with an editorial rule, an Ember part number, and a
   Switzer Light + .hatton headline. An oversized Fog ghost numeral
   sits on the rule as a quiet chapter mark. */
.spec-part { padding-top: var(--spacing-80); }
.spec-part-head { border-top: 2px solid var(--color-ink); padding-top: var(--spacing-24); position: relative; }
.spec-part-ghost {
  position: absolute;
  right: 0;
  top: var(--spacing-16);
  font-family: var(--font-display); letter-spacing: var(--ls-display);
  font-size: var(--text-display);
  font-weight: var(--font-weight-bold);
  line-height: 1;
  color: var(--color-fog);
  pointer-events: none;
  user-select: none;
}
@media (max-width: 768px) { .spec-part-ghost { display: none; } }

/* Scroll reveal — library page only. JS adds .spec-reveal and
   flips .is-in on intersection; without JS nothing is hidden.
   base.css's reduced-motion kill-switch makes it instant. */
.spec-reveal { opacity: 0; transform: translateY(16px); transition: opacity 0.6s var(--ease-deck-out), transform 0.6s var(--ease-deck-out); }
.spec-reveal.is-in { opacity: 1; transform: none; }
.spec-part-kicker {
  display: flex;
  align-items: baseline;
  gap: var(--spacing-12);
  margin-bottom: var(--spacing-12);
}
.spec-part-num { font-family: var(--font-display); letter-spacing: var(--ls-rg); font-size: var(--text-body-sm); font-weight: var(--font-weight-bold); color: var(--color-ember); }
.spec-part-title {
  font-family: var(--font-display); letter-spacing: var(--ls-heading-lg);
  font-size: var(--text-heading-lg);
  font-weight: var(--font-weight-light);
  line-height: 1.1;
  color: var(--color-obsidian);
  margin-bottom: var(--spacing-8);
}
.spec-part-desc { font-size: var(--text-body-md); line-height: var(--leading-body-md); color: var(--color-steel); max-width: 620px; }

/* In-page contents — generated from the spec sections at load
   (see components.html) so the index can never drift from the
   page. One column per part; quiet Ash numerals lead each link. */
.spec-index { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--spacing-32); align-items: start; }
.spec-index-part {
  display: block;
  font-family: var(--font-display); letter-spacing: var(--ls-subheading);
  font-size: var(--text-subheading);
  font-weight: var(--font-weight-semibold);
  color: var(--color-ink);
  padding-bottom: var(--spacing-8);
  border-bottom: 1px solid var(--color-pebble);
  margin-bottom: var(--spacing-8);
}
@media (hover: hover) and (pointer: fine) {
  .spec-index-part:hover { color: var(--color-race-green); text-decoration: none; }
}
.spec-index-part:active { color: var(--color-race-green); }
.spec-index-part:focus-visible { outline: 2px solid var(--color-race-green); outline-offset: 2px; border-radius: var(--radius-md); }
.spec-index-link {
  display: flex;
  align-items: baseline;
  gap: var(--spacing-8);
  padding: 3px 0;
  font-size: var(--text-body-sm);
  color: var(--color-slate);
  transition: color var(--duration-micro) ease, transform 160ms var(--ease-deck-out);
}
.spec-index-link span {
  font-family: var(--font-display); letter-spacing: var(--ls-rg);
  font-size: var(--text-body-xs);
  font-weight: var(--font-weight-bold);
  color: var(--color-ash);
  min-width: 30px;
}
@media (hover: hover) and (pointer: fine) {
  .spec-index-link:hover { color: var(--color-race-green); text-decoration: none; }
}
.spec-index-link:active { color: var(--color-race-green); transform: scale(0.99); transform-origin: left center; }
.spec-index-link:focus-visible { outline: 2px solid var(--color-race-green); outline-offset: 2px; border-radius: var(--radius-md); }
@media (max-width: 768px) { .spec-index { grid-template-columns: 1fr; gap: var(--spacing-24); } }
/* A 22px row in a list of sixty is a list nobody can use with a
   thumb. The index is the page's own navigation, so its rows take
   the box — the same rule the storefront footer's links take. */
@media (pointer: coarse) {
  .spec-index-link { min-height: var(--touch-target); align-items: center; padding-block: 0; }
  .spec-index-part { display: flex; align-items: flex-end; min-height: var(--touch-target); }
}

/* Specimen width cap — keeps single-control demos (buy row) at a
   realistic in-context width instead of filling the card. */
.spec-cap-sm { max-width: 320px; }

/* Two-up specimen row — pins a pair of draw tiles to the width they
   get on a phone (169px at 390px viewport) so the tile's own
   container-query treatment is visible at ANY viewport, which is the
   whole point of showing it in the library. A media query could not
   demonstrate this on a desktop screen. */
.spec-tile-twoup {
  display: grid;
  grid-template-columns: repeat(2, 169px);
  gap: var(--spacing-12);
}
@media (max-width: 480px) { .spec-tile-twoup { grid-template-columns: repeat(2, 1fr); } }

/* Card-face specimens — bare 3:4 card boxes on the dark panel, so
   the die-cut plate is seen in its native context without borrowing
   the deck tile's stage or the viewer's rail. */
.spec-face-row { display: flex; flex-wrap: wrap; gap: var(--spacing-16); }
.spec-face { width: 150px; margin: 0; }
.spec-face-box {
  position: relative;
  aspect-ratio: 3 / 4;
  border-radius: var(--radius-2xl);
  overflow: hidden;
  border: 1px solid rgba(var(--color-brand-highlight-rgb), .12);
}
.spec-face-box img { width: 100%; height: 100%; object-fit: cover; display: block; }
.spec-face-cap {
  margin-top: var(--spacing-8);
  font-size: var(--text-body-xs);
  color: var(--color-mint-ice-cream);
  opacity: .7;
}

/* Confetti demo stage — gives the burst room to fall. */
.spec-confetti-stage {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 220px;
  background: var(--color-mist);
  border-radius: var(--radius-cards-compact);
}

/* Colour token swatches. */
.swatch-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: var(--spacing-16); }
.swatch { background: var(--color-snow); border-radius: var(--radius-cards-compact); overflow: hidden; }
.swatch-chip { height: 72px; border-bottom: 1px solid rgba(0, 0, 0, 0.04); }
.swatch-body { padding: var(--spacing-12) var(--spacing-16); }
.swatch-name { font-size: var(--text-body-sm); font-weight: var(--font-weight-semibold); color: var(--color-ink); }
.swatch-val { font-family: var(--font-mono); font-size: var(--text-body-xs); color: var(--color-steel); text-transform: uppercase; }
/* A pinned-open dropdown is absolutely positioned, so the specimen
   reserves the height it would otherwise take out of the page. */
.spec-menu-stage { min-height: 380px; align-items: flex-start; }

/* The auth layout fills the viewport in place; inside a gallery
   section it is boxed so it can be read next to its note. */
.spec-auth-stage { background: var(--color-mist); border-radius: var(--radius-cards); overflow: hidden; }
/* A legal page is a full-bleed page shell, so the gallery boxes it:
   the band gives up its 100vw pull and the whole thing is clipped to
   a card. Gallery scaffolding only — nothing on a product page. */
.spec-legal-stage { background: var(--color-mist); border-radius: var(--radius-cards); overflow: hidden; }
/* Inside the specimen the stage supplies the spacing under the band,
   so the head's own half-gap comes off with the full-bleed width. */
.spec-legal-stage .legal-head { width: auto; margin-left: 0; margin-bottom: 0; padding: var(--spacing-40) 0 var(--spacing-32); }
.spec-legal-stage .legal-head-title { font-size: var(--text-heading-lg); }
.spec-auth-stage .auth-layout { min-height: 0; }
.spec-auth-stage .auth-panel { padding: var(--spacing-32); }

/* Account-shell specimens: the real column is sticky inside a page
   grid and its trigger only exists below 1024px. Here both are shown
   standing still, side by side, at any viewport. */
.spec-account-nav {
  position: static;
  max-height: none;
  width: 220px;
  padding: var(--spacing-12);
  background: var(--surface-card-white);
  border-radius: var(--radius-cards-compact);
}
.spec-account-nav--rail { width: 64px; }
/* The burger only shows itself below 768; the gallery shows the
   control at whatever width the reader is on. */
.spec-account-burger { display: inline-flex; }
@media (max-width: 900px) { .swatch-grid { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 560px) { .swatch-grid { grid-template-columns: repeat(2, 1fr); } }

/* Type-scale specimen rows. */
.type-row {
  display: grid;
  grid-template-columns: 220px 1fr;
  gap: var(--spacing-24);
  align-items: baseline;
  padding: var(--spacing-16) 0;
  border-bottom: 1px solid var(--color-fog);
}
.type-row:last-child { border-bottom: none; }
.type-row-meta { font-size: var(--text-body-xs); line-height: var(--leading-body-xs); color: var(--color-steel); }
.type-row-meta strong { display: block; color: var(--color-ink); font-weight: var(--font-weight-semibold); }
.type-row-sample { min-width: 0; overflow-wrap: break-word; }
@media (max-width: 768px) { .type-row { grid-template-columns: 1fr; gap: var(--spacing-8); } }

/* Icon wall — the Tabler registry at its native 20px. */
.icon-wall { display: grid; grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); gap: var(--spacing-12); }
.icon-cell {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--spacing-8);
  padding: var(--spacing-16) var(--spacing-8);
  background: var(--color-snow);
  border-radius: var(--radius-xl);
  color: var(--color-ink);
}
.icon-cell span { font-size: var(--text-caption); font-weight: var(--weight-caption); color: var(--color-steel); text-align: center; overflow-wrap: anywhere; }

/* ============================================================
   REDUCED TRANSPARENCY
   Thirty surfaces in this sheet are glass — the nav's buttons, the
   category chips, the deck overlays and their close controls, the
   modal wash, the video play control, the widget. Not one of them
   had an answer for a reader who has asked the system for less
   translucency, which is a LEGIBILITY setting rather than a taste
   one: it is set by people who cannot read type over a blurred
   photograph, and serving them the blurred photograph anyway is
   the whole of the failure.

   It is one block rather than thirty because the answer is one
   answer — make the surface solid and take the blur off — and
   because most of these surfaces are already painted through two
   tokens. Redefining those two turns the majority of the sheet
   opaque without naming a single component, and the blanket rule
   underneath catches the ones that spell their own blur out.

   The two GROUNDS invert, and that is the point: `--glass-fill-dark`
   is a 6% white film that only reads because there is a blurred
   picture behind it, so with the blur gone it has to become the
   dark plate it was standing in for rather than a brighter film.

   `prefers-contrast: more` asks the same question more firmly and
   gets the same answer, plus the defined edge it is really after.
   ============================================================ */
@media (prefers-reduced-transparency: reduce), (prefers-contrast: more) {
  :root {
    --glass-fill-dark: rgba(var(--color-brand-primary-strong-rgb), 0.92);
    --glass-fill-light: rgba(255, 255, 255, 0.97);
    --glass-blur: 0px;
    --deck-glass-fill: rgba(var(--color-brand-primary-strong-rgb), 0.92);
    --glass-border-light: rgba(var(--color-brand-highlight-rgb), 0.42);
    --deck-glass-border: rgba(var(--color-brand-highlight-rgb), 0.38);
  }
  /* A blur nobody can see through is a blur nobody should download
     the frames for. The tokens above cover what is painted through
     them; this covers every rule that wrote its own. */
  *, *::before, *::after {
    -webkit-backdrop-filter: none !important;
    backdrop-filter: none !important;
  }
  /* A wash that was 55% because a blur was doing the other half has
     to do the whole job on its own. */
  .modal-overlay { background: rgba(var(--color-brand-primary-strong-rgb), 0.86); }
  .deck-overlay, .deck-viewer, .asset-vault { background: rgba(var(--color-brand-primary-strong-rgb), 0.94); }
}

/* `prefers-contrast: more` wants an edge it can find, not just a
   solid ground — a glass control that has lost its blur is a flat
   shape on a photograph until something draws its outline. */
@media (prefers-contrast: more) {
  .btn-glass, .category-card-chip, .video-play,
  .deck-overlay .overlay-close, .deck-viewer .overlay-close {
    border: 1px solid var(--color-snow);
  }
}
