/* ==========================================================================
   VotingSite · swipe deck, onboarding quiz and match list.
   Loaded only on /swipe*, after style.css, and reusing its tokens
   (--bg, --panel, --panel-2, --border, --text, --muted, --accent, --gold,
   --cyan, --danger, --r, --r-lg, --gap, --shadow). Mobile-first: the deck is
   full-bleed with thumb-reachable actions, and widens into a centred stack
   with keyboard hints from 720px up.

   Everything degrades: without JS the cards are stacked forms that POST, so
   only the drag affordances (.is-dragging, .stamp) go unused.
   ========================================================================== */

/* ------------------------------------------------------------- local tokens */
.swipe-page,
.quiz,
.matches {
  --card-max: 460px;
  --deck-h: min(74vh, 640px);
  --like: var(--accent);
  --pass: var(--danger);
  --stack-peek: 10px;
}

/* --------------------------------------------------------------- page head */
.swipe-head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: var(--gap);
}
.swipe-head h1 { margin-bottom: .2em; }
.swipe-head p { margin: 0; font-size: .9rem; }

.swipe-nav { display: flex; flex-wrap: wrap; gap: 6px; }
.swipe-nav .btn { font-size: .85rem; }

.pill {
  display: inline-block;
  min-width: 1.4em;
  padding: 0 6px;
  border-radius: 99px;
  background: var(--panel-2);
  border: 1px solid var(--border);
  color: var(--text);
  font-size: .75rem;
  font-weight: 700;
  line-height: 1.5;
  text-align: center;
}

/* Nudge toward the quiz while the score is still unavailable. */
.quiz-nudge {
  display: flex;
  align-items: center;
  gap: 4px 10px;
  flex-wrap: wrap;
  padding: 10px 14px;
  margin-bottom: var(--gap);
  border: 1px dashed var(--border);
  border-radius: var(--r);
  background: var(--panel);
  color: var(--text);
}
.quiz-nudge:hover { border-color: var(--accent); text-decoration: none; }
.quiz-nudge span { color: var(--muted); font-size: .88rem; }
.quiz-nudge-go { margin-left: auto; font-size: 1.4rem; line-height: 1; }

/* ============================================================ the card deck */
.deck {
  position: relative;
  height: var(--deck-h);
  max-width: var(--card-max);
  margin: 0 auto;
  /* Full-bleed on phones: the card should feel like the whole screen. */
  touch-action: pan-y;
}

.deck-status {
  position: absolute;
  left: 0; right: 0;
  bottom: -26px;
  margin: 0;
  text-align: center;
  font-size: .82rem;
  color: var(--muted);
  min-height: 1.2em;
}
.deck-status.is-error { color: var(--danger); }

/* ------------------------------------------------------------------- card */
.swipe-card {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow);
  /* Own stacking context so the stamps never escape the card. */
  isolation: isolate;
  will-change: transform;
}

/* The two cards behind peek out, scaled down and pushed back. */
.swipe-card.is-peek {
  transform: translateY(var(--stack-peek)) scale(.96);
  filter: saturate(.7);
  pointer-events: none;
  z-index: 1;
}
.swipe-card.is-peek + .swipe-card.is-peek {
  transform: translateY(calc(var(--stack-peek) * 2)) scale(.92);
  z-index: 0;
}
.swipe-card.is-top { z-index: 3; }

.swipe-card.is-animating { transition: transform .28s ease, opacity .28s ease; }
.swipe-card.is-dragging { transition: none; cursor: grabbing; }
.swipe-card.is-gone { opacity: 0; pointer-events: none; }

/* ------------------------------------------------------------- card art */
.scard-art { position: relative; flex: 0 0 auto; background: var(--panel-2); }

.shots {
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
  aspect-ratio: 16 / 9;
  max-height: 34vh;
}
.shots::-webkit-scrollbar { display: none; }
.shots .shot {
  flex: 0 0 100%;
  width: 100%;
  height: 100%;
  object-fit: cover;
  scroll-snap-align: center;
  /* Screenshots are user-supplied: never let one drag out of the card. */
  -webkit-user-drag: none;
  user-select: none;
}
.shots-empty {
  aspect-ratio: 21 / 9;
  background:
    radial-gradient(120% 140% at 20% 0%, color-mix(in srgb, var(--accent) 22%, transparent), transparent 60%),
    linear-gradient(160deg, var(--panel-2), var(--panel));
}

/* Most servers have no screenshots and no banner, so this is the common case,
   not the edge case. Instead of a blank panel, the server's MOTD or description
   fills the space · same box, same card height, but it now says something. */
.shots-blurb {
  display: flex;
  align-items: center;
  justify-content: center;
  /* Horizontal padding clears the 38px edge columns on both sides. */
  padding: 18px 46px;
  text-align: center;
}
.shots-blurb-text {
  margin: 0;
  /* Clamped rather than truncated by the server: the box is a fixed aspect
     ratio, so long MOTDs have to stop somewhere visible. */
  display: -webkit-box;
  -webkit-line-clamp: 4;
  -webkit-box-orient: vertical;
  overflow: hidden;
  font-size: .95rem;
  line-height: 1.5;
  color: var(--text);
  text-wrap: balance;
  overflow-wrap: anywhere;
}
/* Quote marks make it read as the server talking rather than as UI copy. */
.shots-blurb-text::before { content: "\201C"; }
.shots-blurb-text::after { content: "\201D"; }

.shot-dots {
  position: absolute;
  left: 0; right: 0;
  bottom: 8px;
  display: flex;
  justify-content: center;
  gap: 5px;
}
.shot-dots i {
  width: 5px; height: 5px;
  border-radius: 50%;
  background: #fff;
  opacity: .55;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, .35);
}

/* Drag stamps · revealed by public/js/swipe.js via --dx / [data-dir]. */
.scard-stamp {
  position: absolute;
  top: 16px;
  padding: 4px 12px;
  border: 3px solid currentColor;
  border-radius: var(--r-sm);
  font-family: var(--font-display);
  font-size: 1.2rem;
  font-weight: 800;
  letter-spacing: 2px;
  opacity: 0;
  pointer-events: none;
  z-index: 2;
}
.stamp-like { left: 16px; color: var(--like); transform: rotate(-14deg); }
.stamp-pass { right: 16px; color: var(--pass); transform: rotate(14deg); }
.swipe-card[data-dir="like"] .stamp-like,
.swipe-card[data-dir="pass"] .stamp-pass { opacity: var(--stamp, 1); }
/* The stamp always pairs its colour with an icon and a word · never hue alone,
   which red/green cannot carry for the ~8% of men with a red/green deficiency.
   A solid backing plate keeps both legible over a screenshot or a pale card. */
.scard-stamp {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  background: color-mix(in srgb, var(--bg-elev) 82%, transparent);
}
.scard-stamp b { font-size: 1.1em; line-height: 1; }

/* Colour wash under the stamp, driven by the same --stamp strength public/js/
   swipe.js already sets from drag distance (and snaps to 1 for the ← / →
   shortcuts), so the gesture tells you what will happen before you release. */
.scard-wash {
  position: absolute;
  inset: 0;
  z-index: 1;
  opacity: 0;
  pointer-events: none;
  transition: opacity .12s linear;
}
.swipe-card[data-dir="like"] .scard-wash,
.swipe-card[data-dir="pass"] .scard-wash { opacity: calc(var(--stamp, 1) * .55); }
.swipe-card[data-dir="like"] .scard-wash {
  background: linear-gradient(270deg, color-mix(in srgb, var(--like) 55%, transparent), transparent 72%);
}
.swipe-card[data-dir="pass"] .scard-wash {
  background: linear-gradient(90deg, color-mix(in srgb, var(--pass) 55%, transparent), transparent 72%);
}
/* The stamp has to sit above its own wash. */
.scard-stamp { z-index: 3; }

/* Standing edge rails.
   These began as 3px slivers, which at render size read as a rendering artefact
   rather than an affordance. They are now full-height bands carrying the word
   as well as the colour · so the two directions are legible at rest, and legible
   without relying on red vs green, which is the pair red/green colour blindness
   actually loses. The resting state stays quieter than the active drag: opacity
   climbs with the same --stamp strength public/js/swipe.js already sets.

   They are <button type="submit" name="decision"> now, not <span>, so the whole
   column is the control it already looked like. The button reset lives here
   because .btn in style.css is a different thing entirely and inheriting from it
   would fight every rule below. */
.scard-edge {
  position: absolute;
  /* Spans the whole card, not just the art: as 3px slivers, and then as bands
     confined to the art panel, these still read as a rendering artefact rather
     than a control. A full-height column in the direction's own colour, with
     the word in it, is unmistakable. */
  top: 0;
  bottom: 0;
  width: 38px;
  z-index: 3;
  /* A column, not a line of vertical text: the icons stay upright and only the
     word is rotated. `space-between` puts one icon hard at the top, one hard at
     the bottom and the word dead centre between them · the two icons are the
     same size, so the word's centre is the rail's centre. */
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  /* The bottom icon sits beside the Pass / Like buttons in .scard-actions,
     which is exactly where a thumb already is. */
  padding: 12px 0 calc(12px + env(safe-area-inset-bottom, 0px));
  margin: 0;
  border: 0;
  font: inherit;
  cursor: pointer;
  opacity: .85;
  transition: opacity .12s linear;
  /* A rail is a big dark target on a phone; the grey flash on tap is noise. */
  -webkit-tap-highlight-color: transparent;
}
/* Big enough to be the thing you see rather than a detail you find: 24px inside
   a 38px column, against the 14px these were before (.78rem x 1.15, and 12.5px
   on a phone). The word beside it is the display face; the mark is not, because
   neither ✕ nor ♥ is in that face and the per-glyph fallback was picking its own
   size and baseline anyway. */
.scard-edge-icon {
  font-family: var(--font);
  font-size: 1.5rem;
  font-weight: 400;
  line-height: 1;
}
.scard-edge-word {
  writing-mode: vertical-rl;
  font-family: var(--font-display);
  font-size: .78rem;
  letter-spacing: 3px;
  text-transform: uppercase;
}
/* Runs bottom-to-top on the left edge, so both rails read outward-up. Only the
   word turns · an upside-down ♥ or ✕ is just a wrong-looking icon. */
.scard-edge-pass .scard-edge-word { transform: rotate(180deg); }

.scard-edge:hover { opacity: 1; }
.scard-edge:active { opacity: 1; filter: brightness(1.08); }
/* Inside the edge: the card sets `overflow: hidden`, so an outward outline on a
   rail flush with the card border would be clipped to nothing. */
.scard-edge:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -5px;
  opacity: 1;
}
/* Solid columns rather than a fade: at column width a gradient just looks like
   a smudge. The ink is the theme's own --accent-ink/white so the word stays
   legible on the fill in both themes. */
.scard-edge-pass {
  left: 0;
  color: #fff;
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--pass) 78%, transparent),
    color-mix(in srgb, var(--pass) 62%, transparent));
}
.scard-edge-like {
  right: 0;
  color: #fff;
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--like) 78%, transparent),
    color-mix(in srgb, var(--like) 62%, transparent));
}
/* Ink colour is decided by the composited fill, not by the theme's mood.
   Measured over the real card background at both gradient stops:
     light theme  white 2.1-3.2:1   dark ink 5.8-8.9:1   -> dark ink
     dark theme   white 4.7-7.7:1   dark ink 2.4-3.9:1   -> white
   The light-theme accent is a DARK green (#1f8a34) while the dark-theme one is
   a bright green (#3fb950), so the correct ink is the opposite of the intuition
   in both cases. Applies to both rails. */
html[data-theme="light"] .scard-edge { color: #0b1a0f; }
/* The side you are dragging towards goes to full strength; the other fades back
   so there is never any doubt which one is armed. */
.swipe-card[data-dir="pass"] .scard-edge-pass,
.swipe-card[data-dir="like"] .scard-edge-like { opacity: 1; }
.swipe-card[data-dir="pass"] .scard-edge-like,
.swipe-card[data-dir="like"] .scard-edge-pass { opacity: calc(.85 - var(--stamp, 1) * .55); }

/* ------------------------------------------------------------ card body */
.scard-body {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 12px 14px;
  overflow-y: auto;
  overscroll-behavior: contain;
}

.scard-head { display: flex; align-items: flex-start; gap: 10px; }
.scard-head .srv-icon { flex: 0 0 auto; width: 44px; height: 44px; }
/* The "EM"/"OA" initials looked off-centre in their tile: the pixel display
   font's glyphs sit high inside their metrics box, so flex centring centres the
   box and not the letters. Collapse the box, then nudge for the residual ink
   offset · the same correction as .logo, .rank-num and the stat values. The
   tile here is 44px rather than the 64px default, so the nudge is smaller. */
.scard-head .srv-icon-gen {
  font-size: .95rem;
  line-height: 1;
  padding-top: 2px;
}
.scard-name { flex: 1 1 auto; min-width: 0; }
.scard-name h2 {
  margin: 0 0 2px;
  font-size: 1.15rem;
  line-height: 1.25;
  overflow-wrap: anywhere;
}
.scard-name h2 a { color: var(--text); }
.scard-tagline {
  margin: 0;
  color: var(--muted);
  font-size: .88rem;
  line-height: 1.4;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* --------------------------------------------------------- the match ring */
/* The ring is a circle, but its contents were laid out against the circle's
   square *bounding box*. Measured on a real card, "rough match" came out 62px
   wide · the full diameter · so at the height it sat, where the circle is much
   narrower, the words crossed the stroke on both sides, and the number was
   pushed to 4px from the top against 41px at the bottom. Same mistake as the
   "Take the quiz" badge below.
   The label does not belong inside a circle: it goes underneath, and the ring
   is left holding the number, which is the one thing a ring is for. */
.match {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  /* Sized by its content now, not by a hard-coded box. */
  width: auto;
  height: auto;
  background: none;
  border-radius: 0;
  color: var(--text);
  text-align: center;
}
.match::after { display: none; }

.match .match-num {
  position: relative;
  /* Contains the z-index:-1 disc below, so it can sit above the conic-gradient
     background but still behind the digits. */
  isolation: isolate;
  /* Flex, not grid: .match-num's contents are a bare text node ("49") plus a
     <small>%</small>, and in a grid container those become two anonymous grid
     items · which stacked the number above the percent sign in separate rows.
     A flex row keeps them on one line and still centres them in the circle. */
  display: flex;
  align-items: center;
  justify-content: center;
  width: 62px;
  height: 62px;
  border-radius: 50%;
  font-family: var(--font-display);
  font-size: 1.05rem;
  font-weight: 800;
  /* --pct is 0..100; the ring is the honest number, nothing decorative. */
  background: conic-gradient(var(--accent) calc(var(--pct, 0) * 1%), var(--border) 0);
  /* Display-font metrics again: the glyphs sit high inside their box, so
     centring the box leaves the digits riding above the middle of the ring.
     Collapse the box, then nudge for the residual ink offset · the same
     correction as .logo, .rank-num and the stat tile values. */
  line-height: 1;
  padding-top: 2px;
}
.match .match-num::before {
  content: "";
  position: absolute;
  inset: 5px;
  border-radius: 50%;
  background: var(--panel);
  z-index: -1;
}
.match-num small { font-size: .6em; opacity: .7; }

.match-label {
  font-size: .6rem;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: .4px;
  line-height: 1.25;
  /* Free of the circle, so it may wrap on its own terms. */
  max-width: 5.5rem;
  text-wrap: balance;
}

/* Thin data behind the number · say so rather than dress it up. */
.match.is-thin .match-num {
  background: conic-gradient(var(--gold) calc(var(--pct, 0) * 1%), var(--border) 0);
}

/* The score variant holds "56%" over "match", which fits a circle. This one
   holds "Take the quiz", which does not: at 60px the label was wider than the
   circle is at the height the label sits, so the words visibly crossed the
   round border. It is a call to action rather than a reading of a number, so
   drop the ring metaphor and give it a pill with room for its own text. */
.match-unknown {
  width: auto;
  min-width: 62px;
  max-width: 8.5rem;
  height: auto;
  min-height: 62px;
  padding: 8px 12px;
  border-radius: var(--r-lg);
  gap: 3px;
  background: var(--panel-2);
  border: 1px dashed var(--border);
  color: var(--muted);
  text-decoration: none;
}
.match-unknown::after { display: none; }
.match-unknown:hover { border-color: var(--accent); color: var(--text); text-decoration: none; }
/* This variant shares .match-num with the scored ring above, but it is a "?"
   inside a pill, not a percentage inside a ring · so undo the circle. */
.match-unknown .match-num {
  width: auto;
  height: auto;
  background: none;
  border-radius: 0;
  padding-top: 0;
  margin-top: 4px;
  font-size: 1.3rem;
}
.match-unknown .match-num::before { display: none; }
.match-unknown .match-label {
  font-size: .52rem;
  line-height: 1.25;
  /* Let it wrap to two lines inside the pill instead of pushing the box wider
     than the card's header row can afford. */
  white-space: normal;
  text-wrap: balance;
}

/* ------------------------------------------------------------- key facts */
.scard-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 0;
  padding: 0;
  list-style: none;
}
.scard-chips .fact {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 9px;
  border: 1px solid var(--border);
  border-radius: 99px;
  background: var(--panel-2);
  color: var(--muted);
  font-size: .78rem;
  white-space: nowrap;
}
.scard-chips .fact .dot { margin: 0; }

/* Why this card is here. Short, factual, never more than four. */
.scard-reasons {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: .86rem;
}
.scard-reasons li { display: flex; gap: 8px; align-items: baseline; color: var(--text); }
.scard-reasons li::before { content: "✓"; color: var(--accent); font-weight: 700; flex: 0 0 auto; }

/* --------------------------------------------------------------- actions */
.scard-actions {
  flex: 0 0 auto;
  display: grid;
  grid-template-columns: 1fr 1.4fr 1fr;
  gap: 8px;
  padding: 10px 12px calc(10px + env(safe-area-inset-bottom, 0px));
  border-top: 1px solid var(--border-soft);
  background: var(--bg-elev);
}

.swipe-btn {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  /* Thumb-sized on phones · this is the only control that matters. */
  min-height: 54px;
  padding: 8px 6px;
  border: 1px solid var(--border);
  border-radius: var(--r);
  background: var(--panel);
  color: var(--text);
  font: inherit;
  font-size: 1.25rem;
  line-height: 1;
  cursor: pointer;
}
.swipe-btn:hover { border-color: var(--muted); }
.swipe-btn:active { transform: translateY(1px); }
.swipe-btn-label { font-size: .72rem; color: var(--muted); letter-spacing: .2px; }
/* Sized off the button's own font so it sits at the same optical weight as the
   ✕ and ♥ glyphs on either side, rather than at whatever the emoji font chose. */
.swipe-btn-icon { width: 1em; height: 1em; display: block; }

.btn-pass { color: var(--pass); }
.btn-pass:hover { border-color: var(--pass); }
.btn-like { color: var(--like); }
.btn-like:hover { border-color: var(--like); }
.btn-join { background: var(--panel-2); font-size: 1rem; }
/* The deck CTA carries BOTH .copy-ip and .btn-join. style.css gives
   `.copy-ip.copied` a solid --accent background with --accent-ink text; this
   rule used to set only `color: var(--accent)`. Same specificity (0,2,0), and
   swipe.css loads last · so it won the colour and lost the background, leaving
   green text on a green fill: the button that "went blank when you clicked it".
   Declare the whole copied appearance here so the two can never half-apply. */
.btn-join.copied {
  background: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 65%, #000);
  color: var(--accent-ink);
}
/* Clipboard writes genuinely fail (insecure origin, denied permission), and the
   fallback message has to be as readable as the success one. */
.btn-join.copy-failed,
.copy-ip.copy-failed {
  border-color: var(--gold);
  color: var(--gold);
  background: var(--panel-2);
}

/* .swipe-btn-label sets its own `color: var(--muted)`, which is correct against
   the resting --panel-2 button but survived into the copied state: the button
   went solid --accent with --accent-ink text while the label stayed --muted,
   i.e. grey on green at about 1.6:1. That is the "solid green block with the
   label barely visible" report · the fill was never the bug, the label colour
   was. Any state that repaints the button owns its label and icon too. */
.btn-join.copied .swipe-btn-label,
.btn-join.copied > span,
.btn-join.copy-failed .swipe-btn-label,
.btn-join.copy-failed > span { color: inherit; }

/* White on light-theme --accent (#1f8a34) is 4.43:1 · just under AA for the
   11.5px label. --accent-dim takes it to 5.43:1 for the price of a slightly
   deeper green. Dark theme is already 7.29:1 and is left alone. */
html[data-theme="light"] .btn-join.copied {
  background: var(--accent-dim);
  border-color: color-mix(in srgb, var(--accent-dim) 65%, #000);
}

/* ------------------------------------------------------- keyboard hints */
.kbd-hints {
  display: none;
  margin: 38px 0 0;
  text-align: center;
  color: var(--muted);
  font-size: .82rem;
}
.kbd-hints kbd {
  display: inline-block;
  min-width: 1.6em;
  padding: 1px 6px;
  border: 1px solid var(--border);
  border-bottom-width: 2px;
  border-radius: var(--r-sm);
  background: var(--panel-2);
  color: var(--text);
  font-size: .85em;
}
/* The arrow keys only exist once public/js/swipe.js is running, so the hint
   only appears then · and only where a keyboard is likely. */
@media (min-width: 720px) {
  .deck.is-enhanced ~ .kbd-hints { display: block; }
}

.deck-count { margin: 10px 0 0; text-align: center; font-size: .82rem; }

/* ============================================================ empty deck */
.deck-empty { text-align: center; }
.deck-empty h2 { margin-bottom: .3em; }

.relax { margin-top: var(--gap); text-align: left; }
.relax h3 { font-size: 1rem; margin-bottom: 8px; }
.relax-list { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 8px; }
.relax-list .btn { display: inline-flex; align-items: center; gap: 8px; }

.deck-empty-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 8px;
  margin-top: var(--gap);
}

.liked-preview { margin-top: calc(var(--gap) * 1.5); }

/* ================================================================== quiz */
/* The progress bar was flush against the sticky header · the quiz opened with
   "QUESTION 1 OF 6" jammed under the nav and no room to breathe. */
.quiz { max-width: 620px; margin: 0 auto; padding-block: 26px 32px; }

/* The form is the body of the quiz; it needs the same rhythm as the heading
   block above it, which it had no rule to give it. */
.quiz-form { margin: var(--gap) 0 0; }
/* margin-top declared explicitly, not left to the UA default, so the gap below
   "QUESTION 1 OF 6" is owned by exactly one rule. Adjacent siblings collapse to
   the larger of the two margins, and a UA h1 margin sneaking in here is the
   difference between a deliberate gap and whichever value happened to win. */
.quiz h1 { margin: 0 0 .2em; }
.quiz-hint { margin: 0 0 var(--gap); font-size: .9rem; }
/* 16px rather than 4. The label and the question were touching: the display face
   carries a tall ascender and its glyphs never reach the baseline, so a nominal
   4px of margin read as nothing at all once both boxes were drawn. Fixed px, not
   em · this is the gap between two elements at very different sizes, and tying
   it to either one's font size makes it drift when that size changes. */
.quiz-step { margin: 8px 0 16px; font-size: .8rem; letter-spacing: .3px; text-transform: uppercase; }

.quiz-progress {
  height: 6px;
  border-radius: 99px;
  background: var(--panel-2);
  border: 1px solid var(--border);
  overflow: hidden;
}
.quiz-progress span {
  display: block;
  height: 100%;
  background: var(--accent);
  transition: width .3s ease;
}

.quiz-options {
  display: grid;
  grid-template-columns: 1fr;
  gap: 10px;
}
.quiz-option {
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 64px;
  padding: 12px 16px;
  border: 1px solid var(--border);
  border-radius: var(--r);
  background: var(--panel);
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.quiz-option:hover { border-color: var(--accent); background: var(--panel-2); }
.quiz-option.is-chosen { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); }
.quiz-icon { font-size: 1.6rem; line-height: 1; flex: 0 0 auto; }
.quiz-label { font-size: 1rem; font-weight: 600; }

.quiz-nav {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-top: var(--gap);
}

/* ============================================================== matches */
.match-list { list-style: none; margin: 0 0 var(--gap); padding: 0; display: grid; gap: 10px; }
.match-row {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--r);
}
.match-row .srv-icon { width: 40px; height: 40px; }
.match-meta { min-width: 0; }
.match-meta p { margin: 2px 0; font-size: .85rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.match-facts { font-size: .8rem; }
.match-pill {
  align-self: start;
  padding: 3px 9px;
  border-radius: 99px;
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 45%, transparent);
  color: var(--accent);
  font-weight: 700;
  font-size: .8rem;
}
.match-actions {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
}
.match-actions .srv-ip { flex: 1 1 auto; text-align: left; }
.matches-reset { text-align: center; }

/* =============================================================== desktop */
@media (min-width: 720px) {
  .deck { --deck-h: min(78vh, 660px); }
  /* hints appear via .deck.is-enhanced above */
  .scard-actions { padding: 12px 14px; }
  .swipe-btn { min-height: 48px; }
  .match-row { grid-template-columns: auto 1fr auto auto; }
  .match-actions { grid-column: auto; flex-wrap: nowrap; }
  .match-actions .srv-ip { flex: 0 0 auto; }
  .quiz-options-4, .quiz-options-3, .quiz-options-2 { grid-template-columns: 1fr 1fr; }
  .quiz-option { flex-direction: column; align-items: flex-start; justify-content: center; min-height: 96px; }
  .quiz-icon { font-size: 2rem; }
}

/* Below 520px the deck goes edge to edge · the card is the interface. */
@media (max-width: 519px) {
  .deck {
    width: calc(100% + 24px);
    margin-left: -12px;
    margin-right: -12px;
    --deck-h: min(76vh, 620px);
  }
  .swipe-card { border-radius: var(--r); border-left: 0; border-right: 0; }
  .swipe-head { flex-direction: column; }
  .swipe-nav { width: 100%; }
}

/* ============================================================ light theme */
html[data-theme="light"] .shot-dots i { box-shadow: 0 0 0 1px rgba(0, 0, 0, .2); }
html[data-theme="light"] .swipe-card { box-shadow: 0 2px 4px rgba(16, 32, 48, .06), 0 16px 32px rgba(16, 32, 48, .1); }
html[data-theme="light"] .shots-empty {
  background:
    radial-gradient(120% 140% at 20% 0%, color-mix(in srgb, var(--accent) 14%, transparent), transparent 60%),
    linear-gradient(160deg, var(--panel-2), var(--panel));
}
html[data-theme="light"] .scard-stamp { text-shadow: 0 1px 0 rgba(255, 255, 255, .6); }
/* Light mode is the case where a pale tint disappears against a near-white
   card, so the wash and the edge hints are pushed harder here, not softer. */
html[data-theme="light"] .swipe-card[data-dir="like"] .scard-wash,
html[data-theme="light"] .swipe-card[data-dir="pass"] .scard-wash { opacity: calc(var(--stamp, 1) * .7); }
html[data-theme="light"] .scard-stamp { background: rgba(255, 255, 255, .9); }

/* ==================================================== reduced motion / a11y */
@media (prefers-reduced-motion: reduce) {
  .swipe-card,
  .swipe-card.is-animating { transition: none !important; }
  .quiz-progress span { transition: none; }
  .shots { scroll-behavior: auto; }
  /* No rotation and no fly-off: cards simply swap. */
  .swipe-card.is-dragging { transform: none !important; }
  .scard-stamp { transform: none; }
  /* The wash still appears · it is the feedback, not decoration · but it snaps
     in at full strength instead of fading with drag distance. */
  .scard-wash { transition: none; }
  .swipe-card[data-dir="like"] .scard-wash,
  .swipe-card[data-dir="pass"] .scard-wash { opacity: .55; }
}

.swipe-btn:focus-visible,
.quiz-option:focus-visible,
.match-unknown:focus-visible,
.shots:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

@media (forced-colors: active) {
  .swipe-card, .quiz-option, .scard-chips .fact { border: 1px solid CanvasText; }
  .match { background: Canvas; border: 1px solid CanvasText; }
}

/* ==========================================================================
   Edge columns: content inset  (visual pass, round 3)
   Placed at the end of the file deliberately · .scard-body and .scard-actions
   set their own padding further up, and these have the same specificity, so
   they only win from here. The columns are 38px, so the gutters clear them.
   ========================================================================== */
.scard-body { padding-inline: 46px; }
.scard-actions { padding-inline: 44px; }

@media (max-width: 420px) {
  /* Three buttons plus two 30px gutters is already a lot on a small phone; the
     columns narrow rather than the buttons becoming unusable. 30px rather than
     the old 26px because the rail is a tap target now and not just a marker ·
     measured at 320px the three action buttons still come out 68/95/68. */
  .scard-edge { width: 30px; padding-block: 10px; gap: 6px; }
  .scard-edge-icon { font-size: 1.25rem; }
  .scard-edge-word { font-size: .68rem; letter-spacing: 2px; }
  .scard-body { padding-inline: 36px; }
  .scard-actions { padding-inline: 34px; }
  .shots-blurb { padding-inline: 38px; }
}

/* ==========================================================================
   Out of cards: the way to ask for what the deck could not offer.

   Sits under the reset buttons at the end of an exhausted deck, which is the
   one moment a player has proved that nothing listed fits them.
   ========================================================================== */
.deck-ask {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin: 14px 0 0;
  padding-top: 14px;
  border-top: 1px solid var(--border);
}
.deck-ask .muted { font-size: .9rem; }
