/* ========================================
   SET GAME PWA — Fully Responsive Stylesheet
   Mobile-first, scales to desktop beautifully
   ======================================== */

:root {
  --bg-deep:      #0d0118;
  --bg-card:      #160825;
  --bg-surface:   #1e0d35;
  --bg-elevated:  #2a1248;

  /* The card colours, measured rather than picked by eye — see the light
     block below for how. The set these replaced (#f43f5e / #22c55e / #a855f7)
     put red and green at ΔE 8 for deuteranopes, near enough the same colour;
     a red that leans scarlet against a green that sits light and minty holds
     every pair above ΔE 44 under all three simulations. */
  --red:    #f03a2c;
  --green:  #5ef0a8;
  --purple: #b574ff;

  --accent-violet: #8b5cf6;
  --accent-pink:   #ec4899;
  --accent-orange: #f97316;

  --text-primary:   #f1e6ff;
  --text-secondary: #9d85b8;
  --text-muted:     #5c4478;

  --border:        rgba(139,92,246,0.2);
  --border-bright: rgba(139,92,246,0.5);

  --radius-card: 12px;
  --radius-btn:  10px;

  --font-display: 'Space Grotesk', sans-serif;
  --font-mono:    'Space Mono', monospace;

  --shadow-glow: 0 0 20px rgba(139,92,246,0.4);

  /* Surfaces and inks that used to be written in place. Each one has a light
     counterpart in the block below; anything added later that paints a colour
     belongs here too, or it stays dark when the page goes light. */
  color-scheme: dark;
  --gold:          #fbbf24;
  --splash-bg:     radial-gradient(ellipse at 50% 10%, #2d0a5e 0%, #0d0118 65%);
  --go-bg:         radial-gradient(ellipse at 50% 0%, #2d0a5e 0%, #0d0118 70%);
  --title-ramp:    linear-gradient(135deg, #a855f7, #ec4899, #f97316);
  --mc-red-ink:    #fda4af;
  --mc-purple-ink: #d8b4fe;
  --mc-green-ink:  #86efac;
  --modal-veil:    rgba(0,0,0,0.78);
  --modal-bg:      linear-gradient(180deg, rgba(26,10,46,0.98), rgba(21,7,39,0.98));
  --hover-wash:    rgba(255,255,255,0.06);
  --footer-bg:     rgba(13,1,24,0.96);
  --sheet-veil:    rgba(5,0,12,0.55);
  --sheet-bg:      #1a0a2e;
  --sheet-shadow:  0 -18px 40px rgba(0,0,0,0.55);
  --sheet-grab:    #4a3568;
  --sheet-stat-bg: #221038;
  --card-edge:     rgba(255,255,255,0.08);
  --card-sheen:    rgba(255,255,255,0.04);
  --card-rest:     0 0 0 transparent;
  --card-lift:     0 6px 20px rgba(0,0,0,0.5);
  --pill-shade:    inset 0 2px 4px rgba(0,0,0,0.5),
                   inset 0 -1px 0 rgba(255,255,255,0.07),
                   0 1px 0 rgba(255,255,255,0.05);
  --mark-shade:    0 1px 2px rgba(0,0,0,0.6), inset 0 1px 0 rgba(255,255,255,0.22);
  --ok-ink:        #4ade80;
  --bad-ink:       #fb7185;
  --info-ink:      #c4b5fd;
  /* How strongly a striped shape's lines are drawn, and how thick, in the
     shape's own 24x40 units against a 3.5-unit stripe pitch. */
  --stripe-alpha:  0.75;
  --stripe-width:  1.4;
  --hint-rgb:      251 191 36;

  /* Safe-area insets behind variables so they can be reasoned about in one
     place — and simulated in tests, since env() always reports 0 off-device. */
  --sat: env(safe-area-inset-top, 0px);
  --sar: env(safe-area-inset-right, 0px);
  --sab: env(safe-area-inset-bottom, 0px);
  --sal: env(safe-area-inset-left, 0px);

  /* Fluid spacing scale */
  --sp-xs:  clamp(4px,  1vw,  6px);
  --sp-sm:  clamp(8px,  2vw,  12px);
  --sp-md:  clamp(12px, 2.5vw,18px);
  --sp-lg:  clamp(16px, 3vw,  24px);
  --sp-xl:  clamp(20px, 4vw,  32px);
}

/* ── Light ──
   data-theme is written before first paint by the inline script in
   index.html's head, then kept by the shared theme switch in the title bar's
   menu. It always names a real scheme; following the device is the switch's
   business, not this file's.

   The card colours are the game, so they were chosen by measurement rather
   than taken from the dark set: each pair stays apart under protanopia,
   deuteranopia and tritanopia simulation (min ΔE 31), and each clears 3:1 on
   a white card. A red that drifts toward orange and a green that sits deep
   are what buy that — the obvious #e11d48 / #15803d pair collapses to ΔE 14
   for deuteranopes. Dark mirrors it: there the green is the light one. */
:root[data-theme="light"] {
  color-scheme: light;

  --bg-deep:      #f5f1fb;
  --bg-card:      #ffffff;
  --bg-surface:   #ede6f8;
  --bg-elevated:  #f3eefb;

  --red:    #f23a1f;
  --green:  #0f5f44;
  --purple: #9a2ee8;

  --accent-violet: #6d28d9;
  --accent-pink:   #be185d;

  --text-primary:   #1d1030;
  --text-secondary: #574a6b;
  --text-muted:     #6f6385;

  --border:        rgba(109,40,217,0.16);
  --border-bright: rgba(109,40,217,0.42);

  --shadow-glow: 0 0 16px rgba(109,40,217,0.28);

  --gold:          #a35208;
  --splash-bg:     radial-gradient(ellipse at 50% 10%, #e6d8ff 0%, #f5f1fb 65%);
  --go-bg:         radial-gradient(ellipse at 50% 0%, #e6d8ff 0%, #f5f1fb 70%);
  --title-ramp:    linear-gradient(135deg, #7c3aed, #db2777, #ea580c);
  --mc-red-ink:    #c0260f;
  --mc-purple-ink: #7e22ce;
  --mc-green-ink:  #0f5f44;
  --modal-veil:    rgba(29,16,48,0.42);
  --modal-bg:      #ffffff;
  --hover-wash:    rgba(29,16,48,0.06);
  --footer-bg:     rgba(245,241,251,0.96);
  --sheet-veil:    rgba(29,16,48,0.28);
  --sheet-bg:      #ffffff;
  --sheet-shadow:  0 -12px 32px rgba(60,30,110,0.18);
  --sheet-grab:    #cbbfe0;
  --sheet-stat-bg: #f5f1fb;
  /* A white card on a pale page needs an edge and a little lift, or the board
     reads as one sheet with shapes printed across it. */
  --card-edge:     rgba(29,16,48,0.13);
  --card-sheen:    transparent;
  --card-rest:     0 1px 2px rgba(60,30,110,0.10);
  --card-lift:     0 6px 18px rgba(60,30,110,0.20);
  --pill-shade:    inset 0 1px 3px rgba(60,30,110,0.22),
                   inset 0 -1px 0 rgba(255,255,255,0.8),
                   0 1px 0 rgba(255,255,255,0.7);
  --mark-shade:    0 1px 2px rgba(60,30,110,0.35), inset 0 1px 0 rgba(255,255,255,0.3);
  --ok-ink:        #0f5f44;
  --bad-ink:       #b91c1c;
  --info-ink:      #5b21b6;
  --stripe-alpha:  1;
  /* Thicker on white, where thin dark lines thin out further against the
     card: at 2.4 of the 3.5 pitch they read as bold stripes while the gaps
     still show, so striped never passes for solid. */
  --stripe-width:  2.4;
  /* Amber-400 vanishes against a white card; 600 holds the ring. */
  --hint-rgb:      217 119 6;
}

/* ── Reset ── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

html {
  height: 100%;
  /* Prevent font size inflation on mobile */
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

body {
  font-family: var(--font-display);
  background: var(--bg-deep);
  color: var(--text-primary);
  min-height: 100vh;
  min-height: 100dvh;
  overflow: hidden;
  overflow-x: hidden;
  /* Prevent tap flash on mobile */
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  cursor: auto;
}

body * {
  cursor: auto !important;
}

/* The blanket `body *` rule above forces the arrow cursor onto everything, so
   anything interactive has to opt back in — links included, or the app cards
   and their child img/span show a plain arrow on desktop. */
button,
button *,
a[href],
a[href] * {
  cursor: pointer !important;
}

.hidden { display: none !important; }

/* ════════════════════════════════════════
   SPLASH SCREEN
   ════════════════════════════════════════ */
.splash {
  position: fixed; inset: 0; z-index: 100;
  background: var(--splash-bg);
  display: flex; align-items: flex-start; justify-content: center;
  padding: max(var(--sp-lg), var(--sat)) max(var(--sp-lg), var(--sar)) var(--sp-lg) max(var(--sp-lg), var(--sal));
  /* body is overflow:hidden, so a full-screen overlay taller than the viewport
     (landscape phones) would clip with no way to reach the rest. Centring is
     done with auto margins on the child instead of align-items:center, which
     cuts off the TOP of overflowing content and makes it unscrollable. */
  overflow-y: auto;
}
.splash-inner {
  display: flex; flex-direction: column;
  align-items: center;
  gap: clamp(16px, 3.5vw, 26px);
  width: 100%;
  max-width: 420px;
  text-align: center;
  /* Fill the splash so the footer can sit at the bottom of the screen. The
     content above it centres in whatever is left, via .splash-main's own
     flex: 1. When the content is taller than the viewport — landscape — this
     grows past 100% and .splash scrolls, rather than clipping the top. */
  min-height: 100%;
}
.splash-main {
  flex: 1;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  gap: clamp(12px, 2.5vw, 20px);
  width: 100%;
}
/* The wordmark IS the card row — one letter per card. */
.splash-title {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: clamp(8px, 2vw, 12px);
}
.splash-sub {
  font-size: clamp(0.72rem, 2.5vw, 0.9rem);
  color: var(--text-secondary);
  letter-spacing: 0.08em; text-transform: uppercase;
}
.mini-card {
  /* 2/3 like the real board cards, so the wordmark reads as three cards. */
  width: clamp(56px, 16vw, 80px);
  aspect-ratio: 2 / 3;
  border-radius: 11px;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Sized so the row still carries the weight the old headline text did. */
  font-size: clamp(1.9rem, 7.5vw, 2.9rem);
  font-weight: 700;
  line-height: 1;
  animation: cardPop 0.6s ease backwards;
}
.mini-card:nth-child(2) { animation-delay: 0.1s; }
.mini-card:nth-child(3) { animation-delay: 0.2s; }
@keyframes cardPop {
  from { transform: scale(0.5) rotate(-10deg); opacity: 0; }
  to   { transform: scale(1) rotate(0); opacity: 1; }
}
/* Letter takes a lighter tint of its own card so it stays legible against the
   translucent fill rather than blending into the border colour. */
.mc-red    { background: linear-gradient(135deg,#f43f5e44,#f43f5e22); border:2px solid #f43f5e88; color:var(--mc-red-ink); }
.mc-purple { background: linear-gradient(135deg,#a855f744,#a855f722); border:2px solid #a855f788; color:var(--mc-purple-ink); }
.mc-green  { background: linear-gradient(135deg,#22c55e44,#22c55e22); border:2px solid #22c55e88; color:var(--mc-green-ink); }

/* ── Personal best ── */
.best-strip {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 7px 15px;
  border-radius: 999px;
  background: rgba(251, 191, 36, 0.10);
  border: 1px solid rgba(251, 191, 36, 0.32);
  font-size: clamp(0.72rem, 2.1vw, 0.84rem);
  color: var(--text-secondary);
  animation: cardPop 0.5s ease backwards;
  animation-delay: 0.3s;
}
.best-badge { font-size: 1.1rem; line-height: 1; }
.best-line strong { color: var(--gold); font-weight: 700; }

.go-best {
  font-size: clamp(0.78rem, 2.2vw, 0.9rem);
  color: var(--text-secondary);
}
.go-best.is-new {
  color: var(--gold);
  font-weight: 700;
  animation: bounce 0.7s ease;
}

/* Splash buttons */
.splash-inner .btn-primary { max-width: 320px; }
.splash-inner .btn-ghost   { max-width: 320px; }

/* ── Secondary actions: icon buttons in one row (splash + game over) ── */
/* Equal-width columns, not flex. As flex items the buttons were sized by their
   LABELS — "How to play" is 75px against 56px for "Share" — so a uniform gap
   between buttons produced uneven gaps between the squares (34.8 vs 25.0px).
   Equal columns with the square centred in each makes the rhythm depend on the
   squares rather than on how long a caption happens to be. */
.icon-action-row {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  justify-items: center;
  gap: clamp(8px, 2.5vw, 14px);
  width: 100%;
  max-width: 300px;
  margin-inline: auto;
}
.icon-action {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: 0;
  background: none;
  border: none;
  font-family: var(--font-display);
}
.icon-action-glyph {
  width: clamp(46px, 13vw, 56px);
  aspect-ratio: 1;
  display: grid;
  place-items: center;
  border-radius: 14px;
  border: 2px solid var(--border-bright);
  background: rgba(139, 92, 246, 0.07);
  color: var(--text-primary);
  transition: border-color 0.15s, background 0.15s, transform 0.15s;
}
.icon-action-glyph svg {
  /* Icon sits at ~38% of the square, leaving a clear ring of padding — at 46%
     it crowded the rounded border and read heavy. */
  width: 38%;
  height: auto;
  display: block;
}
@media (hover: hover) {
  .icon-action:hover .icon-action-glyph {
    border-color: var(--accent-violet);
    background: rgba(139, 92, 246, 0.16);
    transform: translateY(-2px);
  }
}
.icon-action:active .icon-action-glyph { transform: scale(0.96); }
.icon-action-label {
  font-size: clamp(0.58rem, 1.8vw, 0.66rem);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-muted);
}

.install-steps {
  color: var(--text-secondary);
  font-size: clamp(0.8rem, 2.2vw, 0.92rem);
  line-height: 1.5;
  margin-bottom: 18px;
}

/* The carousel lives here, so the type styling belongs on the version line
   rather than the footer: <app-switcher> inherits font-family through its
   shadow root and paints from currentColor, and would come out mono and
   muted if the footer set them. */
.splash-footer {
  display: flex; flex-direction: column;
  align-items: center;
  gap: clamp(8px, 2vw, 14px);
  width: 100%;
}
.splash-version {
  font-family: var(--font-mono);
  font-size: clamp(0.62rem, 1.8vw, 0.72rem);
  letter-spacing: 0.14em;
  color: var(--text-muted);
}

/* ════════════════════════════════════════
   BUTTONS — fluid, touch-safe
   ════════════════════════════════════════ */
.btn-primary {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  padding: clamp(12px,2.5vw,16px) clamp(20px,4vw,28px);
  background: linear-gradient(135deg, #7c3aed, #ec4899);
  border: none; border-radius: var(--radius-btn);
  color: #fff; font-family: var(--font-display);
  font-size: clamp(0.9rem, 2.5vw, 1.05rem); font-weight: 600;
  cursor: pointer; letter-spacing: 0.04em;
  line-height: 1.1;
  transition: box-shadow 0.15s;
  box-shadow: 0 4px 20px rgba(139,92,246,0.4);
  transform: translateY(0);
  /* Minimum touch target 44px */
  min-height: 44px;
}
.btn-primary:hover  { box-shadow: 0 6px 28px rgba(139,92,246,0.6); }
.btn-primary:active { transform: translateY(0); }

.btn-ghost {
  width: 100%;
  padding: clamp(10px,2vw,14px) clamp(20px,4vw,28px);
  background: transparent; border: 2px solid var(--border-bright);
  border-radius: var(--radius-btn);
  color: var(--text-secondary); font-family: var(--font-display);
  font-size: clamp(0.85rem, 2.3vw, 1rem); font-weight: 500;
  cursor: pointer; transition: all 0.15s;
  min-height: 44px;
}
.btn-ghost:hover { border-color: var(--accent-violet); color: var(--text-primary); }

.btn-outline {
  padding: clamp(8px,1.5vw,11px) clamp(14px,3vw,22px);
  background: transparent;
  border: 1.5px solid var(--border-bright);
  border-radius: var(--radius-btn);
  color: var(--text-secondary); font-family: var(--font-display);
  font-size: clamp(0.78rem, 2vw, 0.88rem); font-weight: 500;
  cursor: pointer; transition: all 0.2s;
  min-height: 40px; white-space: nowrap;
}
.btn-outline:hover { border-color: var(--accent-violet); color: var(--text-primary); background: rgba(139,92,246,0.1); }

.full-w { width: 100%; margin-top: 8px; }

/* ════════════════════════════════════════
   MODAL
   ════════════════════════════════════════ */
.modal {
  position: fixed; inset: 0; z-index: 200;
  display: flex; align-items: center; justify-content: center;
  padding: clamp(12px, 3vw, 24px);
  opacity: 0;
  transform: translateY(16px);
  transition: opacity 180ms ease, transform 180ms ease;
}
.modal:not(.hidden) {
  opacity: 1;
  transform: translateY(0);
}
.modal-backdrop {
  position: absolute; inset: 0;
  background: var(--modal-veil);
  backdrop-filter: blur(10px);
}
.modal-box {
  position: relative; z-index: 1;
  background: var(--modal-bg);
  border: 1px solid rgba(139,92,246,0.24);
  border-radius: 24px;
  padding: clamp(20px, 4vw, 32px) clamp(18px, 4vw, 28px);
  width: 100%; max-width: 500px;
  max-height: 92vh; max-height: 92dvh;
  overflow-y: auto;
  box-shadow: 0 24px 80px rgba(0,0,0,0.35);
}
.modal-box {
  transform: translateY(10px);
  opacity: 0;
  transition: transform 180ms ease, opacity 180ms ease;
}
.modal:not(.hidden) .modal-box {
  transform: translateY(0);
  opacity: 1;
}
.modal-sm { max-width: 360px; text-align: center; }
.modal-sm .btn-primary { margin-bottom: 14px; }
.modal-sm .btn-ghost,
.modal-sm .btn-outline {
  width: 100%;
}
.modal-sm .btn-outline { margin-bottom: 10px; }
.modal-close {
  position: absolute; top: 14px; right: 14px;
  background: none; border: none;
  color: var(--text-secondary); font-size: 1rem;
  cursor: pointer; padding: 6px 10px;
  border-radius: 8px; transition: color 0.15s;
  min-width: 36px; min-height: 36px;
  display: flex; align-items: center; justify-content: center;
}
.modal-close:hover { color: var(--text-primary); background: var(--hover-wash); }

.modal-box h2 {
  font-size: clamp(1.3rem, 4vw, 1.7rem); font-weight: 700;
  margin-bottom: clamp(14px, 3vw, 22px); padding-right: 32px;
  background: var(--title-ramp);
  -webkit-background-clip: text; -webkit-text-fill-color: transparent;
  background-clip: text;
}
.modal-sm h2 { margin-bottom: 14px; padding-right: 0; }

/* Rules */
.rules-grid {
  display: grid; grid-template-columns: 1fr 1fr;
  gap: clamp(8px, 2vw, 14px); margin-bottom: clamp(14px, 3vw, 22px);
}
.rule-block {
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: clamp(10px, 2.5vw, 16px);
}
.rule-icon { font-size: clamp(1.3rem, 4vw, 1.7rem); margin-bottom: 6px; }
.rule-block h3 { font-size: clamp(0.75rem,2vw,0.88rem); font-weight:600; margin-bottom:4px; color:var(--accent-violet); }
.rule-block p  { font-size: clamp(0.7rem, 1.8vw, 0.8rem); color: var(--text-secondary); line-height: 1.5; }
.rule-block strong { color: var(--text-primary); }

.example-set {
  background: var(--bg-elevated); border: 1px solid var(--border);
  border-radius: 12px; padding: clamp(12px,3vw,18px);
  margin-bottom: clamp(14px,3vw,20px); text-align: center;
}
.example-set h3 { font-size: 0.82rem; color: var(--text-secondary); margin-bottom: 12px; }
.example-cards  { display: flex; gap: clamp(8px,2vw,12px); justify-content: center; margin-bottom: 8px; }
.example-note   { font-size: 0.78rem; color: var(--green); }

/* ════════════════════════════════════════
   GAME SCREEN — flex column, full height
   ════════════════════════════════════════ */
.game-screen {
  display: flex; flex-direction: column;
  height: 100vh; height: 100dvh;
  overflow: hidden;
  position: relative;          /* the stats sheet anchors here, not to the viewport */
  margin: 0 auto;
  max-width: 800px;
}

.game-footer {
  flex-shrink: 0;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  /* Bottom inset clears the home indicator. Trimmed by 10px: the hint button
     sits above this padding and its hit area now reaches 7px below its box, so
     a full inset left a visible dead band under the row while the gesture area
     stays clear either way. */
  padding: 2px max(10px, var(--sar)) max(4px, calc(var(--sab) - 10px)) max(10px, var(--sal));
  background: var(--footer-bg);
  border-top: 1px solid var(--border);
  backdrop-filter: blur(10px);
  box-sizing: border-box;
}

/* ── The shared stat slot ──
   Score and deck left trade places on a slow cross-fade: half a period apart,
   so one fades up exactly as the other fades down and the slot is never blank.
   1.2s of the 8s is the hand-over, leaving 2.8s steady on each. The old
   score/clock pair swapped in 300ms, which read as a cut rather than a fade. */
.stat-cycle {
  position: relative;
  flex: none;
  width: clamp(104px, 30vw, 124px);
  height: 26px;
}
.cycle-item {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: baseline;
  gap: 6px;
  opacity: 0;
  animation: statCycle 8s cubic-bezier(0.45, 0, 0.55, 1) infinite;
}
.cycle-item + .cycle-item { animation-delay: 4s; }
@keyframes statCycle {
  0%, 35%  { opacity: 1; }
  50%, 85% { opacity: 0; }
  100%     { opacity: 1; }
}
.cycle-label {
  font-size: clamp(0.6rem, 1.6vw, 0.68rem);
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-secondary);
}
.cycle-val {
  font-family: var(--font-mono);
  font-size: clamp(1rem, 2.4vw, 1.18rem);
  font-weight: 700;
  color: var(--text-primary);
}

/* ── The handle ── */
.sheet-toggle {
  position: relative;
  flex: none;
  /* 32px tall to match the hint button, so neither makes the row taller than
     the other. The hit area is extended past the box, as elsewhere here. */
  width: 36px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  background: none;
  border: 0;
  padding: 0;
  color: var(--text-secondary);
  cursor: pointer;
  transition: color 0.15s ease, transform 0.22s ease;
}
.sheet-toggle::after {
  content: '';
  position: absolute;
  inset: -10px -12px;
}
.sheet-toggle svg { width: 20px; height: 20px; }
.sheet-toggle[aria-expanded="true"] { transform: rotate(180deg); }

/* ── The sheet ──
   Pinned to the game screen rather than the viewport, so it rides above the
   footer without covering the board entirely. */
.sheet-scrim {
  position: absolute;
  inset: 0;
  z-index: 60;
  background: var(--sheet-veil);
  opacity: 0;
  transition: opacity 0.24s ease;
}
.sheet-scrim.is-open { opacity: 1; }

.stat-sheet {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  z-index: 61;
  background: var(--sheet-bg);
  border-top: 1px solid var(--border-bright);
  border-radius: 20px 20px 0 0;
  box-shadow: var(--sheet-shadow);
  padding: 10px clamp(14px, 5vw, 20px) max(16px, var(--sab));
  transform: translateY(100%);
  transition: transform 0.28s cubic-bezier(0.22, 1, 0.36, 1);
  touch-action: none;          /* the sheet handles its own drag */
}
.stat-sheet.is-open { transform: translateY(0); }
/* While a finger is on it the sheet follows that finger, not the clock. */
.stat-sheet.is-dragging { transition: none; }

.sheet-grab {
  width: 40px; height: 4px;
  margin: 0 auto 6px;
  border-radius: 2px;
  background: var(--sheet-grab);
}
.sheet-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-bottom: 12px;
}
.sheet-title {
  margin: 0;
  font-size: 0.7rem;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-secondary);
}
.sheet-close {
  position: relative;
  width: 44px; height: 44px;
  display: flex; align-items: center; justify-content: flex-end;
  background: none; border: 0; padding: 0;
  color: var(--text-secondary);
  cursor: pointer;
}
.sheet-close::after {
  content: '';
  position: absolute;
  inset: -6px -10px;
}
.sheet-close svg { width: 20px; height: 20px; }

.sheet-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
}
.sheet-stat {
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: 13px 15px;
  background: var(--sheet-stat-bg);
  border: 1px solid var(--border);
  border-radius: 13px;
}
.sheet-label {
  font-size: 0.62rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-secondary);
}
.sheet-val {
  font-family: var(--font-mono);
  font-size: 1.4rem;
  font-weight: 700;
  color: var(--accent-violet);
}
.sheet-val.is-time { color: var(--accent-pink); }
.sheet-val.is-best { color: var(--gold); }

@media (prefers-reduced-motion: reduce) {
  /* Alternating text carries meaning, so it does not simply stop mid-cycle —
     it settles on the score and stays there. */
  .cycle-item { animation: none; opacity: 0; }
  .cycle-item:first-child { opacity: 1; }
  .stat-sheet, .sheet-scrim, .sheet-toggle { transition: none; }
}
.icon-btn {
  flex-shrink: 0;
  background: none;
  border: none;
  border-radius: 12px;
  color: var(--text-secondary);
  font-size: 1.5rem;
  /* The box is 44px; the tap target is not. ::after extends the hit area past
     the visual bounds, so the HUD row gets 20px of height back to the board
     without the menu becoming harder to hit — it stays the 64px it was. */
  position: relative;
  width: 44px;
  height: 44px;
  display: flex; align-items: center; justify-content: center;
  cursor: pointer;
  transition: all 0.15s ease;
}
.icon-btn::after {
  content: '';
  position: absolute;
  inset: -10px;
}
.icon-btn:hover {
  border-color: rgba(139,92,246,0.55);
  color: var(--text-primary);
}

.header-actions {
  display: flex;
  gap: 6px;
  align-items: center;
}
.hint-btn {
  background: rgba(234,179,8,0.12); border: 1px solid rgba(234,179,8,0.3);
  border-radius: 8px; color: var(--gold);
  font-family: var(--font-display);
  font-size: clamp(0.72rem, 1.8vw, 0.82rem);
  padding: clamp(4px,0.7vw,6px) clamp(8px,1.5vw,10px);
  cursor: pointer; transition: all 0.15s; font-weight: 500;
  min-height: 32px; white-space: nowrap;
  /* Same trick as .icon-btn, in the other direction: the pill stays 32px so
     the footer stays thin, but the hit area clears 44px. This is the control
     the player reaches for most during a game, and it was the smallest. */
  position: relative;
}
.hint-btn::after {
  content: '';
  position: absolute;
  inset: -10px -12px;
}
.hint-btn:hover    { background: rgba(234,179,8,0.2); }
.hint-btn:disabled { opacity: 0.4; cursor: not-allowed; }

/* ── Toast ── */
.toast {
  position: fixed;
  /* The title bar publishes its own height as --toast-offset-top, measured
     after the safe-area inset, so the toast clears the bar instead of landing
     across the title. The clamp is the fallback for the splash and game over,
     where there is no bar to ask. */
  top: var(--toast-offset-top, clamp(70px, 12vh, 90px));
  left: 50%; transform: translateX(-50%);
  z-index: 300;
  padding: clamp(8px,1.5vw,11px) clamp(16px,4vw,24px);
  border-radius: 40px;
  font-size: clamp(0.8rem, 2.2vw, 0.92rem); font-weight: 600;
  white-space: nowrap; pointer-events: none;
  animation: toastIn 0.25s ease;
}
@keyframes toastIn {
  from { transform: translateX(-50%) translateY(-10px); opacity:0; }
  to   { transform: translateX(-50%) translateY(0); opacity:1; }
}
.toast-success { background: rgba(34,197,94,0.2);  border:1px solid #22c55e; color:var(--ok-ink); }
.toast-error   { background: rgba(244,63,94,0.2);   border:1px solid #f43f5e; color:var(--bad-ink); }
.toast-info    { background: rgba(139,92,246,0.2);  border:1px solid #8b5cf6; color:var(--info-ink); }

/* ════════════════════════════════════════
   CARD BOARD — the heart of the game
   Fills available space, never clips cards
   ════════════════════════════════════════ */
.board-wrapper {
  flex: 1;
  overflow: hidden;
  min-height: 0;
  /* Only needs to clear the selected-card lift and ring; more than that is
     screen space the board could be using. */
  padding: 7px 8px;
  display: flex;
}

/* ── Card ── */
.card {
  aspect-ratio: 2 / 3;
  background: var(--bg-card);
  border: 1px solid var(--card-edge);
  border-radius: var(--radius-card);
  box-shadow: var(--card-rest);
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  gap: 3px;
  padding: 4px;
  transition: transform 0.15s, border-color 0.15s, box-shadow 0.15s;
  position: relative; overflow: hidden;
  animation: cardDeal 0.3s ease backwards;
  user-select: none; -webkit-user-select: none;
  touch-action: manipulation;
}

.card::before {
  content: '';
  position: absolute; inset: 0;
  background: radial-gradient(ellipse at 50% 0%, var(--card-sheen), transparent 70%);
  pointer-events: none;
}
@keyframes cardDeal {
  from { transform: scale(0.7) rotate(-5deg); opacity: 0; }
  to   { transform: scale(1) rotate(0); opacity: 1; }
}

/* Hover only on non-touch devices */
@media (hover: hover) {
  .card:hover {
    transform: translateY(-3px);
    border-color: var(--border-bright);
    box-shadow: var(--card-lift);
  }
}
.card:active { transform: scale(0.97); }

/* Focus/state rings are drawn INSET. An outer ring on the top row was being
   clipped by .board-wrapper's overflow, so a selected card looked ringless. */
.card.selected {
  border-color: var(--accent-violet);
  box-shadow: inset 0 0 0 3px rgba(139,92,246,0.75), var(--shadow-glow);
  transform: translateY(-4px) scale(1.02);
  background: rgba(139,92,246,0.1);
}
.card.hint-highlight {
  border-color: rgb(var(--hint-rgb)) !important;
  box-shadow: inset 0 0 0 3px rgb(var(--hint-rgb) / 0.75) !important;
  animation: hintPulse 1.2s ease infinite;
}
@keyframes hintPulse {
  0%,100% { box-shadow: inset 0 0 0 3px rgb(var(--hint-rgb) / 0.75); }
  50%     { box-shadow: inset 0 0 0 7px rgb(var(--hint-rgb) / 0.35); }
}
.card.correct-flash {
  border-color: var(--green) !important;
  box-shadow: inset 0 0 0 3px rgba(34,197,94,0.8), 0 0 24px rgba(34,197,94,0.3) !important;
  animation: correctFlash 0.5s ease;
}
@keyframes correctFlash {
  0%   { transform: scale(1.02); }
  50%  { transform: scale(1.07); }
  100% { transform: scale(1.02); }
}
.card.wrong-flash {
  border-color: var(--red) !important;
  box-shadow: inset 0 0 0 3px rgba(244,63,94,0.8) !important;
  animation: wrongShake 0.4s ease;
}
@keyframes wrongShake {
  0%,100% { transform: translateX(0); }
  20%     { transform: translateX(-6px); }
  60%     { transform: translateX(6px); }
  80%     { transform: translateX(-3px); }
}

/* ── Shapes inside cards ── */
.shape-row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(4px, 0.8vw, 6px);
  width: 100%;
  height: 100%;
  padding: 6px 0;
  flex-wrap: nowrap;
}
.shape-svg {
  display: block;
  flex-shrink: 0;
  width: 30% !important;
  max-width: 44px;
  min-width: 18px;
  height: auto !important;
}

/* ── Bottom Bar ── */
/* ════════════════════════════════════════
   GAME OVER
   ════════════════════════════════════════ */
.game-over {
  position: fixed; inset: 0; z-index: 150;
  background: var(--go-bg);
  display: flex; align-items: flex-start; justify-content: center;
  padding: max(var(--sp-lg), var(--sat)) max(var(--sp-lg), var(--sar)) var(--sp-lg) max(var(--sp-lg), var(--sal));
  overflow-y: auto;          /* see .splash — same clipping trap in landscape */
}
.go-inner {
  display: flex; flex-direction: column;
  align-items: center; text-align: center;
  gap: clamp(12px, 2.5vw, 20px);
  max-width: 400px; width: 100%;
  margin: auto 0;            /* centres when it fits, scrolls when it doesn't */
}
.go-emoji { font-size: clamp(2.8rem,8vw,4rem); animation: bounce 0.8s ease; }
@keyframes bounce {
  0%   { transform: scale(0); }
  60%  { transform: scale(1.2); }
  100% { transform: scale(1); }
}
.go-title {
  font-size: clamp(1.7rem, 6vw, 2.4rem); font-weight: 700;
  background: var(--title-ramp);
  -webkit-background-clip: text; -webkit-text-fill-color: transparent;
  background-clip: text;
}
.go-stats { display: flex; gap: clamp(16px, 5vw, 32px); margin: 4px 0; }
.go-stat  { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.go-stat-val {
  font-family: var(--font-mono);
  font-size: clamp(1.5rem, 5vw, 2rem); font-weight: 700;
  color: var(--text-primary);
}
.go-stat-label { font-size: clamp(0.62rem, 1.6vw, 0.74rem); color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.06em; }
.go-rating {
  background: var(--bg-elevated); border: 1px solid var(--border-bright);
  border-radius: 12px; padding: clamp(10px,2vw,14px) clamp(16px,3vw,22px);
  font-size: clamp(0.82rem,2.2vw,0.94rem); color: var(--text-secondary);
  font-style: italic; max-width: 300px;
}
.go-inner .btn-primary { margin-top: 4px; }

/* ════════════════════════════════════════
   RESPONSIVE BREAKPOINTS
   Mobile-first grid → locked to 3 columns and 4 rows
   ════════════════════════════════════════ */

.card-board {
  display: grid;
  align-items: stretch;
  justify-items: stretch;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 8px;
  width: 100%;
  max-width: 100%;
  height: 100%;
  /* Rows are implicit so a short endgame board (fewer than 12 cards) fills
     the space instead of reserving a blank fourth row. */
  grid-auto-rows: minmax(0, 1fr);
  /* Declared on the base rule so the board animates BOTH out and back in.
     Living inside .shuffling made the fade-in snap instantly. */
  transition: opacity 360ms ease, filter 360ms ease, transform 360ms ease;
}

/* Shuffle transition: fade the board out, redeal, fade back in. */
.card-board.shuffling {
  opacity: 0.18;
  filter: blur(6px) saturate(0.9);
  transform: scale(0.992);
}

.card,
.shape-row,
.shape-svg {
  min-width: 0;
}

.card {
  width: 100%;
  max-width: 100%;
  border: 1px solid var(--card-edge);
}

.board-wrapper {
  min-height: 0;
}

.shape-row {
  flex-wrap: wrap;
  justify-content: center;
}

.hint-btn {
  min-width: 0;
}

@media (max-width: 479px) {
  .card-board {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 3px;
  }
  /* Tighter than the default, but still clears the selected-card lift. */
  .board-wrapper { padding: 6px 6px; }
  .hint-btn { min-width: 0; }
  .card {
    padding: 3px;
    min-height: 0;
    min-width: 0;
  }
}

@media (min-width: 1024px) {
  .board-wrapper {
    padding: clamp(16px, 2vw, 32px);
  }
}

@media (min-width: 1280px) {
  .card-board {
    gap: clamp(12px, 1.2vw, 18px);
    width: min(100%, 1200px);
  }
}

@media (orientation: landscape) and (max-height: 640px) {
  .game-screen { height: 100vh; height: 100dvh; }
  .board-wrapper {
    padding-left: max(12px, var(--sal));
    padding-right: max(12px, var(--sar));
  }
  /* 4 x 3 rather than 3 x 4. Height is the scarce axis in landscape, and four
     short rows squashed the cards flat; three columns of four fit the 2:3 card
     shape far better. */
  .card-board { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

/* ── Reduced motion ── */
@media (prefers-reduced-motion: reduce) {
  .mini-card, .card, .go-emoji { animation: none; }
  .card.hint-highlight { animation: none; }
}

/* ════════════════════════════════════════
   SCROLLBAR
   ════════════════════════════════════════ */
::-webkit-scrollbar { width: 4px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border-bright); border-radius: 4px; }

/* ════════════════════════════════════════
   SAFE AREA (notches, home bar)
   ════════════════════════════════════════ */
.splash-inner { padding-bottom: max(32px, var(--sab)); }
.game-over    { padding-bottom: max(24px, var(--sab)); }

    :root {
      --font-display: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
      --font-mono: 'SF Mono', 'Menlo', 'Monaco', 'Courier New', monospace;
    }

/* ════════════════════════════════════════
   TITLE BAR + GAME HUD
   ════════════════════════════════════════ */

/* The shared bar is chrome and takes no colour of its own, so SET lends it
   the violet it already uses everywhere else. Pointing at the token rather
   than repeating its value, so a retune of the palette carries here too. */
/* The blanket reset at the top of this file reaches custom elements too, and a
   rule from this document outranks a :host rule inside a component's shadow
   root. `padding: 0` was therefore stripping the bar's own
   `padding-top: env(safe-area-inset-top)`, and the bar rendered underneath the
   iPhone status bar — logo, title and key all hidden behind it, with its height
   still reserved below as a band of apparently empty screen.

   It never showed in a desktop browser, because env() reports 0 there and the
   override costs nothing. Re-assert it here, where the reset cannot outrank it. */
badhri-titlebar {
  padding-top: var(--sat);
  --titlebar-accent: var(--accent-violet);
  /* At rest the key is three dots and nothing else: the bar drops its face and
     shadow, and any touch brings the whole key back. The dots are left at the
     weight of the menu glyph opposite them, not whispered — now that the stats
     sheet lives behind the chevron, this key is the only way to anything else
     on this screen, so it has to read as a control.

     0.6 is where it matches: the key's glyph is near-white (#f3edff) on the
     bar, the menu is violet-grey (#9d85b8), and the per-channel solve for the
     same painted brightness lands at 0.62 / 0.55 / 0.69. Opacity cannot match
     the hue as well, and --titlebar-fg would take the dropdown's text down with
     it, so brightness is the thing matched. */
  --titlebar-idle-opacity: 0.6;
  /* Under SET's overlays. The bar defaults to 900, which would put it over the
     splash (100), Game Over (150), modals (200) and the toast (300) — a dialog
     would open behind it. Lowering the bar is one line; renumbering four sets
     of overlays is not. The board itself sits at 1 and 2, so 50 still covers
     the page while staying under everything that should cover the bar. */
  --titlebar-z: 50;
}

/* What belongs to the game rather than to the chrome: the menu, and the score
   and clock the player is actually watching. These sit in the bar's `lead`
   slot, so they cost no height of their own — the bar is already as tall as
   its key. They stay in the light DOM, which is why these rules still reach
   them from here. */
/* The bar's leading slot holds the menu and nothing else. Score and deck left
   went to the footer, where the player's eyes already are. */

/* The title sits in the middle of the row rather than wherever the leading
   group happens to end. `.text` is a flex child that takes the space left over
   after the lead, so a left-aligned title lands well right of centre — 72px
   right of it on a 390px screen. Taking the title out of flow centres it on the
   bar itself. The two exposed parts are the only handles the component gives,
   and that is enough.

   The logo goes with it: in flow it sits immediately before the title and the
   two overlap once the title moves. The icon is on the home screen and the
   splash already; inside the game it was the least of what the row carries. */
/* The mark is drawn as the title's own ::before rather than left to the bar's
   logo img, which sits outside the title in normal flow and would be stranded
   at the left edge while the title centres. Inside the title it is one object:
   mark and word travel together and the pill wraps both. The img the bar would
   otherwise render is never requested, because this screen passes no `logo`. */
badhri-titlebar::part(title)::before {
  content: '';
  flex: none;
  width: 0.78em;
  height: 0.78em;
  border-radius: 23%;
  background: url('/icons/icon-192.png') center / contain no-repeat;
  /* Raised the same way the word is: shade under it, a catch of light on top. */
  box-shadow: var(--mark-shade);
}
badhri-titlebar::part(title) {
  position: absolute;
  left: 50%;
  /* Anchored to the bar's bottom rather than left at its static position,
     which sat the pill low enough to hang over the board. The containing block
     is the bar's host, so this measures from the bar's own bottom edge and
     holds whatever the safe-area inset turns out to be. */
  bottom: 7px;
  top: auto;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 0.28em;
  /* Snug vertically and generous horizontally: the bar's height is set by its
     key and the menu, and the title is out of flow, so a taller pill does not
     push the bar out — it hangs over the board. */
  padding: 0.11em 0.58em 0.15em;
  border-radius: 999px;
  border: 1px solid rgba(139,92,246,0.3);
  font-size: 32px;
  font-weight: 800;
  letter-spacing: 0.1em;
  line-height: 1;
  /* Two background layers, clipped differently: the gradient to the glyphs and
     the tint to the padding box. One element carries both the lettering and
     the pill it sits in, which a single background could not do. The ramp is
     the one .go-title already uses, so this screen and the end screen share a
     title treatment rather than each having its own. */
  background:
    var(--title-ramp),
    rgba(139,92,246,0.12);
  -webkit-background-clip: text, padding-box;
  background-clip: text, padding-box;
  -webkit-text-fill-color: transparent;
  color: transparent;
  /* The pill is a slot pressed into the bar — shade along its top lip, a thin
     highlight along its bottom one, and a hairline on the bar surface just
     below. The mark and the word then sit raised inside it. */
  box-shadow: var(--pill-shade);
  /* No text-shadow: background-clip makes the glyphs windows onto the gradient,
     so a shadow behind them shows straight through and muddies the ramp. The
     depth comes from the slot and the mark instead. */
}

/* Same cause: the reset's `margin: 0` overrides the launcher's own
   `margin-inline: auto`. It has been centred by its flex parents rather than by
   itself, which held only as long as every parent did that. */
app-switcher {
  margin-inline: auto;
}

/* ════════════════════════════════════════
   SET CELEBRATION
   A matched trio lifts off the board, gathers into a fanned hand at centre,
   then shatters. The board is redealt behind the glass, so the shards clear
   onto the cards that replaced them.
   ════════════════════════════════════════ */
.celebrate-layer {
  position: fixed; inset: 0;
  z-index: 190;            /* over the board and HUD, under modals (200) */
  pointer-events: none;    /* the board stays tappable the moment it redeals */
  overflow: hidden;
}

.celebrate-card {
  position: absolute; top: 0; left: 0;
  transform-origin: 50% 50%;
  will-change: transform;
}

/* The card face, reused for the whole card and again inside every shard —
   each shard carries a full-size face and shows only its own clipped wedge. */
.celebrate-face {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  background: var(--bg-card);
  border: 1px solid rgba(34,197,94,0.7);
  border-radius: var(--radius-card);
  box-shadow:
    inset 0 0 0 2px rgba(34,197,94,0.4),
    0 0 34px rgba(34,197,94,0.3);
  overflow: hidden;
}

.celebrate-shard {
  position: absolute; inset: 0;
  will-change: transform, opacity;
}
/* Shards fly apart; the outer glow belongs to the intact card, not the pieces. */
.celebrate-shard .celebrate-face {
  box-shadow: inset 0 0 0 2px rgba(34,197,94,0.4);
}

.celebrate-ring {
  position: absolute;
  border-radius: 50%;
  border: 2px solid rgba(34,197,94,0.5);
  transform: translate(-50%, -50%);
  will-change: transform, opacity;
}

@media (prefers-reduced-motion: reduce) {
  .celebrate-layer { display: none; }
}
