:root{
  --gold:#ffd24a;
  --gold-hi:#fff2a8;
  --gold-lo:#a05c0a;
  --bird-blue:#5fb6e8;
  --bird-blue-dk:#2e7fbf;
}
/* color-scheme tells the browser to paint ITS OWN surfaces dark — the canvas it shows during a
   repaint, before our background has drawn. Without it that surface is white, which is where the
   one-frame white flash on entering and leaving Settings came from: those cards used to carry
   backdrop-filter: blur(), and building and tearing down blur layers is expensive enough on iOS to
   expose a frame of the browser's own canvas. The explicit background on html is the same idea by a
   second route, for the case where body's background is not propagated up.
   Keep both — they are cheap and they cover any repaint gap, not only that one.

   18.9: THE BLURS ARE GONE. That note used to end "this does not remove the compositing cost — it
   makes the gap invisible instead of white", and the cost it declined to remove turned out to be
   the bug. A tester's iPhone died with WebKit's "a problem repeatedly occurred" after moving in and
   out of the leaderboard quickly — a content process killed for memory, which is why reloading
   re-killed it and only closing the app helped. Every screen transition was building and tearing
   down GPU-backed blur layers: two on the home screen's corner buttons, one on the back button,
   more on every card. Eleven declarations, removed. Each surface already had a translucent
   background of its own, so what is lost is the blur behind the glass and nothing else. */
html{ color-scheme: dark; background:#120a2c; }
html,body{height:100%; height:100dvh;}
body{
  margin:0;
  font-family:"Nunito", system-ui, -apple-system, sans-serif;
  background: radial-gradient(ellipse at 50% 0%, #2a1456 0%, #120a2c 60%, #0a0618 100%);
  color:#fff;
  -webkit-font-smoothing:antialiased;
  overflow:hidden;
  /* Prevent elastic scroll on iOS */
  overscroll-behavior: none;
  touch-action: none;
}

/* Phone shell */
.stage{
  width:100vw;
  height:100vh;         /* fallback */
  height:100svh;        /* mobile: stable viewport (excludes browser chrome) */
  display:flex; align-items:center; justify-content:center;
  padding:0;
}
/* The playfield used to be locked to exactly 16/9. On a modern phone in landscape that leaves real
   dead space: an iPhone 13 is 844x390, i.e. 2.16:1, so a 16/9 box is 693px wide and 151px of screen
   sits unused as bars either side — most visible once installed to the Home Screen, where there is
   no browser chrome to disguise it.
   So instead of one fixed shape, the aspect is allowed to FLOAT between 16/9 and 2:1, and the box
   takes whatever the screen gives inside that band:
     • screen wider than 2:1  → capped at 2:1, limited by height
     • between 16/9 and 2:1   → fills the screen completely      ← every modern phone, landscape
     • narrower than 16/9     → 16/9 as before, limited by width ← tablets, desktop windows
   height:min(availH, availW*9/16) expresses all three, since in the first two cases availW*9/16 is
   already >= availH. On an iPhone 13 that is 693px -> 750px, +8.2%.
   The safe-area insets are subtracted rather than ignored: at 16/9 the dead bars happened to be
   wider than the notch, so nothing was ever clipped by accident. Widening removes that accident,
   and without this the tuner strip would slide under the notch in landscape. viewport-fit=cover is
   already set in the meta tag, so these report real values.
   Both game modes follow width on their own — solo reads trackRef.offsetWidth and listens for
   resize, the duel derives X_ME / X_OPP from live bounding rects — so nothing needed rescaling. */
/* --pw-vw / --pw-vh / --pw-sa* are mirrored from JS by the viewport script in index.html.html, and
   every one of them falls back to the value it replaces. iOS reports svh and the insets LATE — an
   installed app opened straight into landscape lays out with portrait numbers for the first frames,
   which is why the margins came up wrong and only a rotation fixed them. The script re-measures
   until they settle; if it never runs, these fall back and the layout is what it always was. */
#game-viewport{
  --pw-availw: calc(var(--pw-vw, 100vw) - var(--pw-sal, env(safe-area-inset-left, 0px)) - var(--pw-sar, env(safe-area-inset-right, 0px)));
  --pw-max-aspect: 2;
  width:  min(var(--pw-availw), calc(100vh * var(--pw-max-aspect)));
  width:  min(var(--pw-availw), calc(var(--pw-vh, 100svh) * var(--pw-max-aspect)));
  height: min(100vh,  calc(var(--pw-availw) * 9 / 16));
  height: min(var(--pw-vh, 100svh), calc(var(--pw-availw) * 9 / 16));
  margin: auto;
  position: relative;
  container-type: size;      /* → cqw / cqh / cqmin units scale UI with the game area, any device */
  container-name: game;
}
.phone{
  position:relative;
  width:100%;
  height:100%;               /* was aspect-ratio:16/9, which would re-impose the shape we just freed */
  overflow:hidden;
  background:#1a1037;
  border-radius: 0;
  box-shadow: none;
}
/* Rounded "device" corners only once the box is genuinely letterboxed and reads as an object on a
   backdrop. That used to start at 16/9; the box now fills the screen up to 2:1, so below that
   threshold there is no surrounding space for corners to sit against. */
@media (min-aspect-ratio: 2/1) {
  .phone{
    border-radius: 42px;
    box-shadow:
      0 30px 80px -20px rgba(0,0,0,.6),
      0 0 0 1.5px rgba(255,255,255,.06) inset,
      0 0 40px -6px rgba(149,108,255,.25);
  }
}

/* Sunset sky */
.sky{
  position:absolute; inset:0;
  background:
    radial-gradient(140% 70% at 50% 115%,
      #ffe48f 0%,
      #ff9a3e 14%,
      #ff5a8a 32%,
      #b73fb8 52%,
      #5a2bba 72%,
      #1b1140 100%);
}
.sky::after{
  content:""; position:absolute; inset:0;
  background: radial-gradient(120% 100% at 50% 0%, rgba(0,0,0,0) 55%, rgba(0,0,0,.30) 100%);
  pointer-events:none;
}

/* Stars */
.stars{position:absolute; inset:0; pointer-events:none; z-index:1;}
.stars i{
  position:absolute; width:2px; height:2px; background:#fff; border-radius:50%;
  opacity:.6; box-shadow: 0 0 4px rgba(255,255,255,.6);
  animation: twinkle 3.6s ease-in-out infinite;
}
@keyframes twinkle{
  0%,100%{opacity:.2; transform:scale(.8);}
  50%{opacity:.85; transform:scale(1.1);}
}

/* Clouds */
/* The glow used to be filter: blur(18px) over a radial gradient — a soft shape blurred a second
   time. That filter is GPU work on a buffer the size of the element, five of them, inside <Decor/>,
   which is mounted on EVERY screen and rebuilt on every navigation. On the iPhone that killed the
   content process it was some of the most expensive paint in the app, and it was buying almost
   nothing: the gradient is already soft, so widening its stops gives the same haze for free. */
.cloud{
  position:absolute; border-radius:999px;
  opacity:.42;
  background: radial-gradient(closest-side, rgba(255,214,163,.9), rgba(246,163,106,.45) 55%, transparent 100%);
  pointer-events:none; z-index:2;
}

/* Floating islands */
.island-l, .island-r{
  position:absolute; pointer-events:none; z-index:3;
  filter: drop-shadow(0 8px 10px rgba(0,0,0,.45));
}
.island-l{ left:-3%; top:24%; width:32%; opacity:.85;}
.island-r{ right:-3%; top:30%; width:30%; opacity:.78;}

/* Waveform */
.waveform{
  position:absolute; left:0; right:0; bottom:0;
  height:18%;
  pointer-events:none;
  z-index:8;
  opacity:.95;
  /* Single filter on the SVG parent — one GPU op instead of 80 */
  filter: drop-shadow(0 0 6px rgba(91,196,255,.8)) drop-shadow(0 0 12px rgba(91,196,255,.45));
  will-change: contents;
}
.waveform .bar{
  fill: url(#wfGrad);
  /* filter removed from individual bars — handled by parent SVG above */
  animation: wfBar 1.4s ease-in-out infinite;
  transform-origin: bottom center;
}
@keyframes wfBar{
  0%,100%{ transform: scaleY(.4);}
  50%{ transform: scaleY(1);}
}

/* River */
.river{
  position:absolute; left:0; right:0; bottom:5%; height:18%;
  z-index:4; pointer-events:none;
}

/* Floating music notes */
.notes{position:absolute; inset:0; pointer-events:none; z-index:4; overflow:hidden;}
.note{
  position:absolute;
  color:#ffd24a;
  filter: drop-shadow(0 0 5px rgba(255,210,74,.45)) drop-shadow(0 0 12px rgba(255,176,31,.25));
  animation: floatNote ease-in-out infinite;
  opacity:.9;
}
@keyframes floatNote{
  0%{transform: translateY(0) rotate(var(--r,0deg));}
  50%{transform: translateY(-10px) rotate(calc(var(--r,0deg) + 4deg));}
  100%{transform: translateY(0) rotate(var(--r,0deg));}
}

/* Content layout — landscape: title on the left, bird + buttons on the right */
.content{
  position:absolute; inset:0; z-index:10;
  display:flex; flex-direction:row; align-items:center; justify-content:center;
  gap: 5cqw;
  padding: 3cqh 7cqw 4cqh;
}

/* Title */
.title-wrap{
  flex: 0 0 auto;
  text-align:center;
  line-height:.84;
  /* The whole glow, in one place, behind the letters. Tight passes give the neon edge the old
     text-shadow used to give; the wide ones are kept low because they are pure glare. */
  filter:
    drop-shadow(0 0 2px rgba(91,196,255,.85))
    drop-shadow(0 0 7px rgba(91,196,255,.40))
    drop-shadow(0 0 18px rgba(91,196,255,.20))
    drop-shadow(0 4px 6px rgba(0,0,0,.45));
}
.title{
  font-family:"Bowlby One", "Fredoka", sans-serif;
  font-weight:900;
  font-size: clamp(34px, 16cqh, 82px);
  letter-spacing: 1.4px;
  background:
    linear-gradient(180deg,
      #fff6c8 0%,
      #ffe27a 16%,
      #ffc036 38%,
      #ff8a14 70%,
      #e25a08 100%);
  -webkit-background-clip:text; background-clip:text; color:transparent;
  /* No text-shadow here, deliberately, and it is not only about glare.
     text-shadow paints AFTER the element's background, and this text is filled by a background
     gradient clipped to the glyphs with color:transparent. So the cyan shadows were painted
     straight over the gold, and with nothing opaque above them the wordmark rendered as a cyan
     blob — the gold-to-orange gradient underneath has never actually been visible. Six shadows
     here, two drop-shadow filters on .title-wrap and a blurred white highlight in ::after all
     compounded on top of that.
     The glow now lives entirely in .title-wrap's filter, because drop-shadow renders BEHIND what it
     is applied to. Same neon edge, half the brightness, and the gradient finally shows. */
  padding: 4px 0;
  transform: rotate(-1.5deg);
  position:relative;
}
.title.line2{ transform: rotate(1.5deg); margin-top:-2px;}
.title-wrap::after{
  content:"";
  position:absolute; left:14%; right:14%; top:8%; height:14%;
  border-radius:50%;
  background: linear-gradient(180deg, rgba(255,255,255,.55), rgba(255,255,255,0));
  filter: blur(6px);
  pointer-events:none;
  opacity:.3;
}

/* Center group — bird stacked above the two buttons, on the right side */
.center-group{
  flex: 0 1 auto;
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  width:auto;
}
.bird-wrap{
  width: 19cqh;
  max-width: 100px;
  aspect-ratio: 1/1;
  margin-bottom: -3cqh;
  z-index:3;
  filter:
    drop-shadow(0 10px 14px rgba(0,0,0,.45))
    drop-shadow(0 0 18px rgba(91,196,255,.55))
    drop-shadow(0 0 34px rgba(91,196,255,.3));
  animation: bob 2.8s ease-in-out infinite;
}
@keyframes bob{
  0%,100%{transform: translateY(0) rotate(-.8deg);}
  50%{transform: translateY(-7px) rotate(.8deg);}
}
.bird-wrap svg{width:100%; height:100%; display:block;}

/* PLAY button */
.play{
  position:relative;
  width: 66%;
  max-width: 230px;
  padding: 16px 24px 18px;
  border-radius: 999px;
  border:none; cursor:pointer;
  background:
    radial-gradient(120% 80% at 50% 25%, #f3ffcb 0%, #c8f56a 22%, #8fdc2a 52%, #56a818 88%, #3a7e0e 100%);
  box-shadow:
    0 0 0 3px rgba(255,255,255,.18),
    0 0 0 7px rgba(120,220,42,.18),
    0 0 22px rgba(184,242,74,.55),
    0 14px 26px -6px rgba(40,90,10,.6),
    inset 0 -12px 16px rgba(0,60,0,.32),
    inset 0 -2px 4px rgba(0,0,0,.25),
    inset 0 8px 14px rgba(255,255,255,.7),
    inset 0 2px 1px rgba(255,255,255,.9);
  transition: transform .15s ease;
}
.play::before{
  content:""; position:absolute; inset:-12px; border-radius:inherit;
  background: radial-gradient(closest-side, rgba(184,242,74,.5), rgba(184,242,74,0) 70%);
  z-index:-1;
  animation: pulse 2.4s ease-in-out infinite;
}
.play::after{
  content:""; position:absolute; left:10%; right:10%; top:7%; height:38%;
  border-radius: 999px;
  background: linear-gradient(180deg, rgba(255,255,255,.85) 0%, rgba(255,255,255,0) 70%);
  filter: blur(1.5px); opacity:.7; pointer-events:none;
}
@keyframes pulse{
  0%,100%{transform:scale(1); opacity:.6;}
  50%{transform:scale(1.05); opacity:.95;}
}
.play:hover{ transform: translateY(-2px);}
.play:active{ transform: translateY(1px);}
.play .label{
  display:block; text-align:center;
  font-family:"Bowlby One", sans-serif;
  font-size: clamp(24px, 6.6cqh, 32px);
  letter-spacing: 2.5px;
  color:#ffffff;
  -webkit-text-stroke: 2.5px #2e6b0a;
  paint-order: stroke fill;
  text-shadow:
    0 2px 0 #2e6b0a,
    0 3px 0 #1a3b00,
    0 5px 6px rgba(0,0,0,.35),
    0 0 14px rgba(255,255,255,.4);
  position:relative;
}

/* Subtitle */
.subtitle{
  margin-top: 22px;
  margin-bottom: 8px;
  font-family:"Fredoka", sans-serif;
  font-weight:700;
  font-size: 17px;
  color:#fff;
  letter-spacing:.4px;
  text-shadow: 0 1px 4px rgba(0,0,0,.55), 0 0 10px rgba(0,0,0,.4);
}

/* Bottom row */
.bottom-row{
  position:absolute; left:0; right:0; bottom: 14%;
  display:flex; align-items:flex-end; justify-content:space-between;
  padding: 0;   /* symmetric, buttons at the outer edges */
  z-index:12;
}
.bottom-row .icon-btn{ transform:scale(.9); }   /* 10% smaller */
.bottom-row--solo{ justify-content:flex-start; padding-left:0; }   /* Settings flush to the left edge */
.bottom-row--solo .icon-btn{ margin-left:-2px; }
.icon-btn{
  background:transparent; border:none; cursor:pointer;
  display:flex; flex-direction:column; align-items:center; gap:6px;
  padding: 4px 8px; border-radius: 14px;
  transition: transform .15s ease, background .15s ease;
}
.icon-btn:hover{ transform: translateY(-2px); background: rgba(255,255,255,.04);}
.icon-btn .ico{
  width: 42px; height: 42px;
  display:flex; align-items:center; justify-content:center;
  filter: drop-shadow(0 4px 6px rgba(0,0,0,.45));
}
.icon-btn .lbl{
  font-family:"Fredoka", sans-serif; font-weight:600;
  font-size: 13px; color:#fff;
  text-shadow: 0 1px 4px rgba(0,0,0,.55);
  letter-spacing:.2px;
}

button:focus{outline:none;}
button:focus-visible{outline:2px solid #ffd24a; outline-offset:3px;}

/* =========================================
   GAMEPLAY SCREEN
   ========================================= */
.play-screen{ position:absolute; inset:0; overflow:hidden; z-index:6; contain: layout paint;}

/* Tuner bar — left side vertical strip */
.tuner-bar{
  position:absolute; left:0; top:0; bottom:0;
  width:36px; z-index:25;
  background: rgba(0,0,0,.30);
  border-right: 1px solid rgba(255,255,255,.12);
  pointer-events:none;
}
.tuner-note{
  position:absolute; right:4px;
  transform:translateY(-50%);
  font-family:"Fredoka", sans-serif;
  font-size:9px; font-weight:600;
  /* Was .42, which measured 3.99:1 against the tuner strip — under the 4.5:1 that WCAG 1.4.3 asks
     of text. The identical value was caught on .copyright-text in the accessibility pass and
     raised to .58; this one wears the same number and was missed. Measured across the whole sky
     gradient, .46 is the floor and .66 clears every band at 6.7:1 or better. */
  color:rgba(255,255,255,.66);
  letter-spacing:.3px;
  transition: color .12s ease, text-shadow .12s ease;
}
.tuner-note.active{
  color:#00CED1;
  text-shadow: 0 0 8px rgba(0,206,209,.85);
}
.tuner-needle{
  position:absolute; left:0; right:0;
  height:2px;
  background:rgba(0,206,209,.75);
  box-shadow: 0 0 6px rgba(0,206,209,.9), 0 0 12px rgba(0,206,209,.5);
  transform:translateY(-50%);
  pointer-events:none;
}

/* Target note label inside glass sphere */
.target-label{
  position:absolute; inset:0;
  display:flex; align-items:center; justify-content:center;
  font-family:"Fredoka", sans-serif;
  font-size:13px; font-weight:700;
  color:rgba(255,255,255,.85);
  text-shadow: 0 0 6px rgba(0,200,255,.7);
  pointer-events:none;
  user-select:none;
}

/* Staff lines */
.staff-bg{
  position:absolute; left:0; right:0; top:32%; height:34%;
  pointer-events:none; z-index:4;
}
.staff-bg .stl{
  position:absolute; left:6%; right:6%; height:1.5px;
  background: linear-gradient(90deg,
    rgba(255,255,255,0) 0%,
    rgba(255,255,255,.3) 12%,
    rgba(255,255,255,.3) 88%,
    rgba(255,255,255,0) 100%);
  box-shadow: 0 0 6px rgba(255,255,255,.18);
}

/* HUD — score + lives */
.game-hud{
  position:absolute; top:0; left:0; right:0;
  height:12%; display:flex; align-items:center; justify-content:space-between;
  padding:0 14% 0 14%;
  z-index:30; pointer-events:none;
}
.hud-lives{
  font-size:18px; letter-spacing:3px;
  color:#ff6b8a;
  filter:drop-shadow(0 0 6px rgba(255,80,120,.55));
}
.hud-score{
  font-family:"Bowlby One", sans-serif;
  font-size:clamp(26px, 7.5cqh, 38px);
  background:linear-gradient(180deg, #fff6c8 0%, #ffd24a 45%, #e76a14 100%);
  -webkit-background-clip:text; background-clip:text; color:transparent;
  filter:drop-shadow(0 2px 4px rgba(0,0,0,.6));
}

/* Bird (gameplay) — top% is JS-driven via birdRef */
.bird-game{
  position:absolute; left:11%; top:47%;
  /* 14%/69px before GAP_H was tightened to ±50c; 56px was a first pass, and on a real phone the
     bird still visibly rubbed the pillars. Measured with getBBox on the actual Bird SVG: its ink is
     72.2% of this box, so the bird you SEE is 0.722 × 16/9 × this width, as a share of playfield
     height. At 56px that was 14.65% against a 6.5% gap — 2.25× too tall.
     Making it match the gap exactly would need ~25px, below the 34px that already once made the
     duel bird read as twice as agitated, so that is not on offer: the note POSITIONS do not move
     when the gap shrinks, so a wobble stays the same pixels and only the body it is measured
     against gets smaller. 44px is the compromise: 11.5% of playfield height against a 6.5% gap, so
     1.77× instead of 2.25× — a fifth less bird, with the ±50c window untouched. Next stops if it
     still reads badly are 40px (1.61×) and 36px (1.45×), and below that the wobble problem above
     starts to bite. The collision test is on the bird's CENTRE point, so this was only ever
     cosmetic. Keep in step with .duel-bird. */
  width: 8.96%; max-width:44px; aspect-ratio:1/1;
  z-index:20; transform: translateY(-50%);
  will-change: transform;
  /* Static filter — no animation on filter (not GPU-compositable) */
  filter:
    drop-shadow(0 6px 10px rgba(0,0,0,.5))
    drop-shadow(0 0 14px rgba(0,200,255,.40));
  animation: hover 2.4s ease-in-out infinite;
}
/* Glow pulse via opacity on ::after — opacity IS GPU-composited */
.bird-game::after{
  content:"";
  position:absolute; inset:-14px;
  border-radius:50%;
  background: radial-gradient(closest-side, rgba(0,220,255,.38), transparent 70%);
  animation: birdGlow 1.4s ease-in-out infinite;
  pointer-events:none;
  z-index:-1;
}
@keyframes birdGlow{
  0%,100%{ opacity:.35; transform:scale(1);}
  50%{     opacity:1;   transform:scale(1.1);}
}
@keyframes hover{
  0%,100%{ transform: translateY(-50%) translateX(0) rotate(-2deg);}
  50%{ transform: translateY(calc(-50% - 12px)) translateX(2px) rotate(2deg);}
}
.bird-game svg{width:100%; height:100%; display:block;}

/* Pillars */
.pillar-track{
  position:absolute; inset:0;
  pointer-events:none; z-index:10;
  overflow:hidden;
}
/* left:0 + transform:translateX(px) driven by requestAnimationFrame in script.js */
.pillar-pair{
  position:absolute; top:0; bottom:0; left:0;
  width: 4.2%;
  will-change: transform;
}
.pillar{
  position:absolute; left:50%; transform:translateX(-50%);
  width:100%;
  border-radius: 6px;
  background:
    linear-gradient(180deg,
      rgba(0,255,255,.55) 0%,
      rgba(0,191,255,.55) 25%,
      rgba(106,58,208,.55) 70%,
      rgba(75,0,130,.7) 100%);
  /* The three pillars are full-height, always on screen, and were the brightest thing in the game:
     five glow layers each, three of them white, over an already-bright cyan gradient. Softened
     roughly a third across the board — the neon character survives, the glare does not. Photo-
     sensitivity is about flash RATE rather than steady brightness (that is handled at birdHit), but
     staring into this for a whole run is a comfort and legibility problem in its own right. */
  box-shadow:
    0 0 14px rgba(255,255,255,.28),
    0 0 28px rgba(0,255,255,.22),
    0 0 44px rgba(75,0,130,.16),
    inset 0 0 10px rgba(255,255,255,.30),
    inset -2px 0 6px rgba(0,0,0,.18),
    inset 2px 0 6px rgba(255,255,255,.28);
  border: 1px solid rgba(255,255,255,.40);
  /* backdrop-filter removed — too expensive on moving elements */
}
.pillar::before{
  content:""; position:absolute; left:22%; top:6%; width:18%; bottom:6%;
  background: linear-gradient(180deg, rgba(255,255,255,.7), rgba(255,255,255,0));
  border-radius: 4px;
  filter: blur(1px);
}
.pillar.top{ top:0;}
.pillar.bottom{ bottom:0;}

/* Glass crystal sphere */
.g-target{
  position:absolute; left:50%; transform:translate(-50%,-50%);
  width:32px; height:32px; border-radius:50%;
  background:
    radial-gradient(circle at 32% 28%,
      rgba(255,255,255,.55) 0%,
      rgba(210,245,255,.30) 14%,
      rgba(140,210,255,.11) 42%,
      rgba(80,130,230,.06) 72%,
      rgba(75,0,130,.10) 100%);
  border: 1.5px solid rgba(255,255,255,.43);
  box-shadow:
    0 0 10px rgba(0,200,255,.33),
    0 0 24px rgba(0,200,255,.17),
    inset 0 0 10px rgba(255,255,255,.27),
    inset -3px -5px 8px rgba(0,100,200,.10),
    inset 3px 5px 8px rgba(255,255,255,.33);
  /* backdrop-filter removed — too expensive on moving elements */
  animation: gPulse 1.6s ease-in-out infinite;
}
.g-target::after{
  content:""; position:absolute; left:18%; top:14%;
  width:32%; height:26%; border-radius:50%;
  background: radial-gradient(closest-side, rgba(255,255,255,.58), rgba(255,255,255,0));
  filter: blur(.5px);
}
@keyframes gPulse{
  0%,100%{ box-shadow:
    0 0 10px rgba(0,200,255,.33),
    0 0 24px rgba(0,200,255,.17),
    inset 0 0 10px rgba(255,255,255,.27),
    inset -3px -5px 8px rgba(0,100,200,.10),
    inset 3px 5px 8px rgba(255,255,255,.33);}
  50%{ box-shadow:
    0 0 18px rgba(0,220,255,.55),
    0 0 38px rgba(0,200,255,.36),
    0 0 58px rgba(100,80,255,.14),
    inset 0 0 14px rgba(255,255,255,.41),
    inset -3px -5px 8px rgba(0,100,200,.10),
    inset 3px 5px 8px rgba(255,255,255,.41);}
}

/* Dynamic waveform (gameplay) — neon turquoise; filter on SVG parent, not per-bar */
.wf-dynamic{
  filter: drop-shadow(0 0 6px rgba(0,206,209,.9)) drop-shadow(0 0 14px rgba(0,206,209,.55));
}
.wf-dynamic .bar{
  fill: url(#wfGradTurq) !important;
  animation: wfDyn .9s ease-in-out infinite;
}
@keyframes wfDyn{
  0%{ transform: scaleY(.2);}
  20%{ transform: scaleY(1);}
  40%{ transform: scaleY(.45);}
  60%{ transform: scaleY(.9);}
  80%{ transform: scaleY(.35);}
  100%{ transform: scaleY(.2);}
}

/* Exit button */
.exit-chip{
  position:absolute; top:5%; right:5%; z-index:40;
  width:38px; height:38px; border-radius:50%;
  border:1.5px solid rgba(255,255,255,.45);
  background:rgba(10,6,24,.72);
  color:#fff; cursor:pointer;
  display:flex; align-items:center; justify-content:center;
  box-shadow: 0 4px 14px rgba(0,0,0,.9), 0 0 12px rgba(255,255,255,.15), inset 0 0 10px rgba(255,255,255,.08);
  transition: transform .15s ease, background .15s ease;
}
.exit-chip:hover{ transform: scale(1.06); background: rgba(20,12,40,.7);}

/* Screen transition */
.mode-fade{ animation: fadeIn .35s ease-out both;}
@keyframes fadeIn{ from{opacity:0; transform: scale(.985);} to{opacity:1; transform:scale(1);} }

/* ── Settings Screen ──────────────────────────────── */
.settings-content{
  position:absolute; inset:0; z-index:10;
  display:flex; flex-direction:column; align-items:center;
  padding: 5cqh 9cqw;
  /* The home indicator is a real object that sits over the bottom of the screen, and until now
     nothing here knew about it: PLAY on Duel Setup finished 14px from the bottom edge, which on a
     phone means underneath it. `max()` keeps the existing breathing room on hardware that has no
     indicator and grows only where there is something to clear. */
  padding-bottom: max(5cqh, var(--pw-sab, env(safe-area-inset-bottom, 0px)) + 10px);
  overflow: hidden auto;         /* landscape: scroll vertically if needed, never horizontally */
}

.back-btn{
  position:absolute; top:5%; left:5%;
  width:38px; height:38px; border-radius:50%;
  border:1.5px solid rgba(255,255,255,.45);
  background:rgba(10,6,24,.72);
  color:#fff; cursor:pointer;
  display:flex; align-items:center; justify-content:center;
  box-shadow:0 4px 14px rgba(0,0,0,.9), 0 0 12px rgba(255,255,255,.15);
  transition:transform .15s ease, background .15s ease;
  z-index:20;
}
.back-btn:hover{ transform:scale(1.06); background:rgba(20,12,40,.7);}

.settings-title{
  font-family:"Bowlby One", sans-serif;
  font-size:clamp(26px, 7.2cqh, 36px);
  letter-spacing:1px; line-height:1.05; text-align:center;
  background:linear-gradient(180deg, #fff6c8 0%, #ffd24a 45%, #e76a14 100%);
  -webkit-background-clip:text; background-clip:text; color:transparent;
  filter:drop-shadow(0 2px 6px rgba(0,0,0,.6));
  margin-bottom:22px; margin-top:6px;
}

.settings-card{
  width:100%; max-width:680px; margin-inline:auto;
  background:rgba(255,255,255,.06);
  border:1px solid rgba(255,255,255,.13);
  border-radius:20px;
  padding:22px 20px;
  box-shadow:0 8px 32px rgba(0,0,0,.4), inset 0 1px 0 rgba(255,255,255,.1);
  margin-bottom:12px;
}

/* Settings links (About & Legal) */
.settings-links{ display:flex; flex-direction:column; gap:8px; }
.settings-link{
  text-align:left; font-family:"Fredoka", sans-serif; font-size:14px; font-weight:600;
  color:#bfe6ff; background:rgba(91,196,255,.10);
  border:1px solid rgba(91,196,255,.22); border-radius:12px;
  padding:11px 14px; cursor:pointer; transition:background .15s ease, transform .1s ease;
}
.settings-link:hover{ background:rgba(91,196,255,.18); }
.settings-link:active{ transform:scale(.98); }

/* ── Add to Home Screen ──────────────────────────────────────────────────────
   Sits over the home screen, sized in container units so it survives a landscape phone (the
   shortest viewport this ever renders in) without scrolling. Follows .consent-card's glass
   treatment so it reads as part of the game, not as a browser nag. */
.install-wrap{
  position:absolute; inset:0; z-index:14;
  display:flex; align-items:center; justify-content:center;
  padding:3cqh 5cqw; background:rgba(6,3,18,.55);
}
.install-card{
  width:100%; max-width:430px; box-sizing:border-box; text-align:center;
  background:rgba(12,8,28,.86);
  border:1.5px solid rgba(255,255,255,.16); border-radius:18px;
  padding:2.6cqh 20px 2.2cqh; box-shadow:0 18px 50px -20px rgba(0,0,0,.85);
}
/* Before / after: browser bars, then none. Sized in cqh so the pair survives a landscape phone,
   which is the shortest viewport this card ever renders in. */
.install-compare{
  display:flex; align-items:center; justify-content:center; gap:min(2cqw,12px);
  margin:.6cqh 0 1.2cqh;
}
.install-compare figure{ margin:0; flex:0 1 auto; min-width:0; }
.install-shot{
  display:block; width:100%; max-width:132px; height:auto;
  border-radius:8px; filter:drop-shadow(0 4px 10px rgba(0,0,0,.45));
}
.install-compare figcaption{
  margin-top:.4cqh; text-align:center;
  font-family:"Nunito", sans-serif; font-size:min(2.1cqh,11px); font-weight:700;
  color:rgba(255,255,255,.62);
}
.install-arrow{
  flex:0 0 auto; align-self:center; margin-bottom:2.2cqh;
  font-size:min(3.4cqh,18px); font-weight:700; color:#5bc4ff;
}
.install-title{
  font-family:"Fredoka", sans-serif; font-weight:600;
  font-size:min(3.8cqh,19px); color:#fff; margin-bottom:.6cqh;
}
.install-text{
  font-family:"Nunito", sans-serif; font-size:min(2.6cqh,13px); font-weight:500; line-height:1.5;
  color:rgba(255,255,255,.72); margin:0 0 1.4cqh;
}
/* The fallback wording shown only from Settings, when the browser has offered no install event and
   is not iOS — instructions for a menu we cannot open for them. Quieter than the main copy because
   it is a consolation, not the thing we wanted to say. */
.install-text--quiet{ color:rgba(255,255,255,.55); font-size:min(2.3cqh,12px); }
.install-text--quiet b{ color:rgba(255,255,255,.8); }
.install-steps{
  margin:0 0 1.4cqh; padding:0; list-style:none; text-align:left;
  font-family:"Nunito", sans-serif; font-size:min(2.5cqh,12.5px); font-weight:600; line-height:1.5;
  color:rgba(255,255,255,.82); counter-reset:step;
}
.install-steps li{
  position:relative; padding:.5cqh 0 .5cqh 26px; counter-increment:step;
}
.install-steps li::before{
  content:counter(step); position:absolute; left:0; top:50%; transform:translateY(-50%);
  width:18px; height:18px; border-radius:50%;
  background:rgba(91,196,255,.22); border:1px solid rgba(91,196,255,.5);
  color:#9fdcff; font-size:10.5px; font-weight:800;
  display:flex; align-items:center; justify-content:center;
}
/* The iOS Share glyph, inline in the sentence — "the square with an arrow" is not findable
   from a written description alone. */
.install-glyph{
  display:inline-flex; vertical-align:-4px; margin:0 1px; color:#5bc4ff;
}
.install-go{ margin:0 auto .6cqh; }
.install-later{
  display:block; margin:0 auto; padding:6px 12px; cursor:pointer;
  background:none; border:none;
  font-family:"Nunito", sans-serif; font-size:min(2.4cqh,12.5px); font-weight:700;
  color:rgba(255,255,255,.58);
}
.install-later:hover{ color:rgba(255,255,255,.85); }

/* Name gate — reuses the consent card's glass so it reads as part of the same opening sequence. */
.namegate-card{ max-width:460px; text-align:center; }
.namegate-card .nick-input{ margin:0 auto; max-width:300px; text-align:center; font-size:17px; }
.namegate-status{
  font-family:"Nunito",sans-serif; font-size:min(2.4cqh,12.5px); font-weight:700;
  margin:.8cqh 0 1.2cqh; min-height:1.2em;      /* fixed height so the button never jumps */
  color:rgba(255,255,255,.55);
}
.namegate-status--taken{ color:#ff8a9b; }
.namegate-status--error{ color:#ffb15b; }
.namegate-status--idle{ color:#7bd47b; }
/* Suggested variants when a name is taken. Tappable, because reading a suggestion and then typing
   it out by hand is the part people give up on. */
.namegate-ideas{
  display:flex; flex-wrap:wrap; gap:6px; justify-content:center; margin:0 0 1.2cqh;
}
.namegate-idea{
  font-family:"Nunito",sans-serif; font-size:min(2.4cqh,13px); font-weight:800;
  color:#9fdcff; background:rgba(91,196,255,.14);
  border:1px solid rgba(91,196,255,.42); border-radius:999px;
  padding:6px 13px; cursor:pointer; transition:background .12s ease, transform .1s ease;
}
.namegate-idea:hover{ background:rgba(91,196,255,.26); }
.namegate-idea:active{ transform:scale(.96); }
/* The way past the email step. Deliberately a real, plainly-worded button rather than faint grey
   text: an offer people cannot obviously decline is a toll gate, and the reason email was kept off
   the opening screen in the first place was to avoid exactly that. */
.namegate-skip{
  display:block; margin:1cqh auto 0; padding:8px 16px; cursor:pointer;
  background:none; border:none;
  font-family:"Nunito",sans-serif; font-size:min(2.4cqh,12.5px); font-weight:700;
  color:rgba(255,255,255,.62); text-decoration:underline; text-underline-offset:3px;
}
.namegate-skip:hover{ color:rgba(255,255,255,.9); }

/* ── Gate buttons ─────────────────────────────────────────────────────────────
   The name gate, the code screen and the code sign-in are FORMS. They were using `.play` —
   the home screen's lozenge, sized and lit to be the loudest object on the display — and
   `.consent-btn` then stretched it edge to edge. Measured on a 568×320 screen that is a 130px
   button: forty percent of everything the player can see, spent on "This is me".
   Three separate complaints came out of that one decision. The button was absurd; "Copy code"
   next to it looked like a footnote even though copying is the whole point of that screen; and
   "Played before? Enter your player code" — the only route back to an existing account — was a
   grey underlined line beneath a caption, under all of it. It was reported as missing. It was
   there. A control nobody can find is a control that does not exist, so the report was right.
   Same family, a third of the volume, and a REAL secondary instead of a footnote. Two stacked
   gate buttons come to about 110px, so this also gives height back rather than taking it. */
.gate-btn{
  display:block; width:100%; max-width:300px; margin:0 auto;
  min-height:46px; padding:11px 22px; cursor:pointer;
  font-family:"Fredoka", sans-serif; font-size:clamp(15px,3.6cqh,18px); font-weight:600;
  letter-spacing:.3px; color:#0e2b06;
  background:linear-gradient(180deg,#b6ef5c,#7fc82a);
  border:none; border-radius:999px;
  box-shadow:0 3px 0 #4e8a12, 0 6px 14px rgba(0,0,0,.35);
  transition:transform .1s ease, filter .15s ease;
}
.gate-btn:hover{ filter:brightness(1.06); }
.gate-btn:active{ transform:translateY(2px); box-shadow:0 1px 0 #4e8a12, 0 3px 8px rgba(0,0,0,.35); }
.gate-btn:disabled{ opacity:.5; cursor:default; box-shadow:0 3px 0 #4e8a12; }

/* The secondary. Quiet enough not to compete, solid enough to read as a button — which is the
   whole difference between "there is another way in" and a line of small print. */
.gate-btn--ghost{
  margin-top:9px;
  color:rgba(255,255,255,.92);
  background:rgba(255,255,255,.07);
  border:1.5px solid rgba(255,255,255,.26);
  box-shadow:none;
  font-weight:600;
}
.gate-btn--ghost:hover{ background:rgba(255,255,255,.13); filter:none; }
.gate-btn--ghost:active{ transform:translateY(1px); box-shadow:none; }
.ng-name{ color:#ffd24a; }

/* Pitch engine status badge (the neural/classic toggle it sat next to is gone) */
.engine-badge{ font-size:11px; font-weight:700; margin-left:6px; vertical-align:middle; }
.engine-badge.ok{ color:#7bd47b; }
.engine-badge.bad{ color:#ff6b7a; }
.engine-badge.wait{ color:#ffb84a; }

/* Profile */
.profile-row{
  display:flex; align-items:center; gap:12px; text-align:left; cursor:pointer;
  transition:background .15s ease, transform .1s ease;
}
.profile-row:hover{ background:rgba(255,255,255,.10); }
.profile-row:active{ transform:scale(.99); }
.profile-row__text{ flex:1; min-width:0; }
.profile-row__chevron{ font-size:22px; color:rgba(255,255,255,.5); font-family:"Fredoka",sans-serif; }
.profile-avatar{
  width:72px; height:72px; margin:0 auto 12px; border-radius:50%;
  background:radial-gradient(circle at 50% 35%, #2a3d66, #16213f);
  border:2px solid rgba(91,196,255,.4);
  display:flex; align-items:center; justify-content:center; overflow:hidden;
  box-shadow:0 0 18px rgba(91,196,255,.3);
}
.profile-avatar svg{ width:78%; height:78%; }
.profile-avatar--sm{ width:44px; height:44px; margin:0; border-width:1.5px; }
.nick-input{
  width:100%; box-sizing:border-box; margin:4px 0 4px;
  font-family:"Fredoka",sans-serif; font-size:16px; font-weight:600; color:#fff;
  background:rgba(0,0,0,.28); border:1.5px solid rgba(255,255,255,.22); border-radius:12px;
  padding:11px 14px; outline:none; transition:border-color .15s ease;
}
.nick-input:focus{ border-color:#5bc4ff; }
.nick-input::placeholder{ color:rgba(255,255,255,.4); }
.nick-err{ font-family:"Nunito",sans-serif; font-size:12px; font-weight:700; color:#ff6b7a; margin:2px 0 4px; }
/* Form actions — Save, Send, and their friends.
   These used to reuse `.play`, the big green lozenge from the home screen. That control is sized
   and lit to be the loudest thing on a screen, which is right for "Sing Solo" and wrong for saving
   a text field: inside a settings card it shouted louder than the game itself and read as though
   something dramatic was about to happen. Same family, a third of the volume. */
/* Typing a player code back in. Spaced out so twelve digits can be checked against a note without
   losing your place — but not as widely as the old six-digit email code, which at .35em ran past
   the edge of the card the moment the string doubled in length. */
.code-input{
  text-align:center; letter-spacing:.16em; text-indent:.16em;
  font-size:19px; font-weight:800; max-width:270px; margin-inline:auto;
  font-variant-numeric:tabular-nums;
}

/* The player code itself, shown once at the name gate and permanently in Settings. Large and
   unmissable on purpose: it is the only text in this app that cannot be recovered once it is lost,
   so it is styled as what it is — a key — rather than as a caption.
   `user-select:all` makes one press select the whole thing. That is the fallback when the clipboard
   API is unavailable (private windows, older iOS, an insecure origin), and without it a player who
   cannot copy has to transcribe twelve digits by eye. */
.pcode{
  user-select:all; -webkit-user-select:all;
  display:block; margin:10px auto 2px; padding:10px 14px; max-width:min(300px,92%);
  font-family:"Fredoka",sans-serif; font-variant-numeric:tabular-nums;
  font-size:clamp(20px,5.2cqh,28px); font-weight:700; letter-spacing:.06em; text-align:center;
  color:#ffd24a;
  background:rgba(255,210,74,.08);
  border:1px solid rgba(255,210,74,.32); border-radius:14px;
}
.pcode--sm{ font-size:clamp(17px,4cqh,21px); padding:8px 12px; }
/* Deliberately NOT styled as an error. Nothing has gone wrong — this is the one consequence of a
   design with no password to reset, and it needs to be read rather than dismissed as a warning. */
.pcode-warn{
  font-family:"Nunito",sans-serif; font-size:min(2.2cqh,12px); font-weight:600; line-height:1.5;
  color:rgba(255,255,255,.62); margin:8px 0 0; text-align:center;
}
.pcode-warn b{ color:#ffb84a; }
.form-btn{
  display:inline-block; margin-top:10px; padding:9px 22px;
  font-family:"Fredoka", sans-serif; font-size:14px; font-weight:600; letter-spacing:.3px;
  color:#0e2b06; cursor:pointer;
  background:linear-gradient(180deg,#b6ef5c,#7fc82a);
  border:none; border-radius:999px;
  box-shadow:0 2px 0 #4e8a12, 0 4px 10px rgba(0,0,0,.35);
  transition:transform .1s ease, filter .15s ease;
}
.form-btn:hover{ filter:brightness(1.06); }
.form-btn:active{ transform:translateY(2px); box-shadow:0 0 0 #4e8a12, 0 2px 6px rgba(0,0,0,.35); }
.form-btn:disabled{ opacity:.5; cursor:default; box-shadow:0 2px 0 #4e8a12; }
.logout-btn{
  margin-top:14px; width:100%; padding:11px; cursor:pointer;
  font-family:"Fredoka",sans-serif; font-size:14px; font-weight:600; color:#ff8a97;
  background:rgba(255,80,90,.10); border:1px solid rgba(255,80,90,.30); border-radius:12px;
  transition:background .15s ease;
}
.logout-btn:hover{ background:rgba(255,80,90,.18); }

/* Legal document */
/* First-run consent gate */
.consent-wrap{
  position:absolute; inset:0; z-index:12; display:flex; align-items:center; justify-content:center;
  padding:4cqh 6cqw; overflow:hidden auto;
}
.consent-card{
  width:100%; max-width:560px;
  background:rgba(12,8,28,.72);
  border:1.5px solid rgba(255,255,255,.16); border-radius:18px;
  padding:22px 22px 20px; box-shadow:0 18px 50px -20px rgba(0,0,0,.8);
}
.consent-title{
  font-family:"Fredoka",sans-serif; font-weight:700; font-size:clamp(20px,5cqh,26px);
  color:#fff; text-align:center; margin-bottom:4px;
}
.consent-lead{ text-align:center; color:rgba(255,255,255,.6); font-size:14px; margin:0 0 14px; }
.consent-list{ list-style:none; margin:0 0 14px; padding:0; display:flex; flex-direction:column; gap:9px; }
.consent-list li{
  font-family:"Nunito",sans-serif; font-size:13.5px; line-height:1.5; color:rgba(255,255,255,.82);
  background:rgba(255,255,255,.05); border:1px solid rgba(255,255,255,.08);
  border-radius:11px; padding:9px 12px;
}
.consent-list b{ color:#fff; font-weight:700; }
.consent-age{
  display:flex; align-items:flex-start; gap:9px; cursor:pointer;
  font-family:"Nunito",sans-serif; font-size:13.5px; color:rgba(255,255,255,.9);
  background:rgba(91,196,255,.10); border:1px solid rgba(91,196,255,.3);
  border-radius:11px; padding:11px 12px; margin-bottom:12px;
}
.consent-age input{ width:19px; height:19px; margin-top:1px; accent-color:#5bc4ff; flex:none; cursor:pointer; }
.consent-links{ font-family:"Nunito",sans-serif; font-size:12.5px; color:rgba(255,255,255,.6); text-align:center; margin:0 0 14px; line-height:1.6; }

/* A phone in landscape is wide and short, and this card was built tall: three stacked bullets made
   it 466px inside a 390px playfield — 76px over, clipped at BOTH ends, so the very first screen a
   new player ever saw had its title cut off and its button on the edge, and it scrolled.
   Same fix Duel Setup already uses: spend the width instead of the height. The three points go side
   by side and every vertical margin is trimmed. Border-box because this card sets width:100% AND
   padding, which is how it overshot its own container in the first place. */
/* Settings deliberately still scrolls in one column.
   It needed 515px more height than a landscape phone has, and both ways of spending the width
   instead were worse than the scroll. A 2-column grid aligns rows to the tallest card, so the tall
   slider card forced a row gap that gave back most of the height it saved. CSS multi-column flows
   the uneven cards properly, but in a fixed-height box it just keeps making columns and overflows
   HORIZONTALLY — the engine card ended up clipped off the right edge, which is strictly worse than
   a scrollbar.
   A settings screen that scrolls is a normal pattern with a visible scrollbar and no hidden state;
   the consent card below was a real bug because it was CLIPPED at both ends with its title cut off
   and no indication anything was missing. Those are different problems and only one needed fixing. */
@container game (max-height: 460px){
  .consent-card, .consent-card *{ box-sizing:border-box; }
  .consent-card{ max-width:740px; padding:14px 18px 12px; }
  .consent-title{ font-size:clamp(17px,4.4cqh,22px); margin-bottom:1px; }
  .consent-lead{ font-size:12px; margin:0 0 9px; }
  .consent-list{
    display:grid; grid-template-columns:repeat(3, minmax(0,1fr));
    gap:8px; margin:0 0 9px;
  }
  .consent-list li{ font-size:11.5px; line-height:1.42; padding:8px 10px; }
  .consent-age{ font-size:12.5px; padding:8px 11px; margin-bottom:8px; }
  .consent-age input{ width:17px; height:17px; }
  .consent-links{ font-size:11.5px; margin:0 0 9px; line-height:1.45; }
}
.consent-link{
  background:none; border:none; padding:0; cursor:pointer;
  color:#7cc4ff; font-weight:700; font-size:12.5px; font-family:inherit; text-decoration:underline;
}
.consent-btn{ width:100%; max-width:none; display:block; }

/* ── Sing safely: the health notice as a screen ──────────────────────────────
   Six short lines with an icon each, because the version of this that lives in Terms section 4 is
   four dense paragraphs and the whole point of moving it here is that somebody reads it. Two
   columns on a short landscape screen for the same reason the consent list is: it has to fit
   without scrolling or the last two points are the ones nobody sees. */
.safety-card{ max-width:660px; text-align:left; }
.safety-card .consent-title, .safety-card .consent-lead{ text-align:center; }
.safety-list{
  list-style:none; padding:0; margin:0 0 1.2cqh;
  display:grid; grid-template-columns:repeat(2, minmax(0,1fr)); gap:7px 14px;
}
.safety-list li{
  display:flex; gap:8px; align-items:flex-start;
  font-family:"Nunito",sans-serif; font-size:min(2.3cqh,12px); font-weight:600; line-height:1.4;
  color:rgba(255,255,255,.78);
  background:rgba(255,255,255,.05);
  border:1px solid rgba(255,255,255,.10); border-radius:12px; padding:7px 10px;
}
.safety-list b{ color:#ffd24a; font-weight:800; }
.safety-icon{ flex:none; font-size:15px; line-height:1.2; }
.safety-foot{
  font-family:"Nunito",sans-serif; font-size:min(2.1cqh,11px); font-weight:600; line-height:1.5;
  color:rgba(255,255,255,.62); margin:0 0 1.2cqh; text-align:center;
}
.safety-foot b{ color:rgba(255,255,255,.85); }
@container game (max-aspect-ratio: 1/1){
  .safety-list{ grid-template-columns:1fr; }
}
/* On the shortest landscape screens this ran 13px past its box. It scrolls rather than clipping,
   so nothing was hidden — but this is the first screen every player meets, and one that arrives
   already scrolled reads as "too much to read". Trimmed to fit instead. */
@container game (max-height: 360px){
  .safety-card .consent-lead{ font-size:11px; margin-bottom:7px; }
  .safety-list{ gap:5px 10px; margin-bottom:7px; }
  .safety-list li{ padding:5px 8px; line-height:1.32; }
  .safety-foot{ margin-bottom:7px; line-height:1.4; }
}

/* ── Break reminder ──────────────────────────────────────────────────────────
   Sits over whatever screen is showing, but never over a game in progress — App only renders it
   between rounds. Interrupting somebody mid-note to tell them to rest their voice would be its own
   small injury. */
.break-wrap{
  position:absolute; inset:0; z-index:40;
  display:flex; align-items:center; justify-content:center;
  padding:4cqh 6cqw;
  background:rgba(8,4,22,.72);
}
.break-card{
  width:100%; max-width:420px; text-align:center;
  background:linear-gradient(180deg, rgba(38,22,74,.98), rgba(24,12,52,.98));
  border:1px solid rgba(255,255,255,.16); border-radius:20px;
  padding:3cqh 5cqw;
  box-shadow:0 24px 60px -18px rgba(0,0,0,.7);
}
.break-icon{ font-size:clamp(26px,7cqh,38px); line-height:1; margin-bottom:.6cqh; }
.break-title{
  font-family:"Fredoka",sans-serif; font-size:clamp(17px,4.6cqh,23px); font-weight:600;
  color:#ffd24a; margin-bottom:.8cqh;
}
.break-text{
  font-family:"Nunito",sans-serif; font-size:min(2.4cqh,12.5px); font-weight:600; line-height:1.5;
  color:rgba(255,255,255,.82); margin:0 0 .8cqh;
}
.break-text--quiet{ color:rgba(255,255,255,.62); }

/* ── Feedback form ──────────────────────────────────────────────────────────
   The one card in the game that is taller than its own screen on a short phone, because it holds
   three questions and a text box. So it scrolls INSIDE itself rather than growing past the frame —
   the consent screen already taught us what happens otherwise: the Agree button ended up 96px below
   the visible area and nobody could find it. */
.break-card--form{ max-height:92cqh; overflow-y:auto; text-align:left; }
.break-card--form .break-icon,
.break-card--form .break-title,
.break-card--form .break-text{ text-align:center; }

.fb-q{
  font-family:"Fredoka",sans-serif; font-size:min(2.5cqh,13px); font-weight:600;
  color:#ffd24a; margin:1.1cqh 0 .5cqh;
}
.fb-row{ display:flex; gap:6px; }
/* Icon BESIDE the label, not above it. Stacked cost 51px of height, which is exactly what pushed
   "Not now" off the bottom of a landscape phone — and a dismissal you have to scroll to find is a
   dismissal that isn't really offered. */
.fb-opt{
  flex:1 1 0; min-width:0; min-height:38px;          /* well past the 24px minimum target size */
  display:flex; align-items:center; justify-content:center; gap:5px;
  padding:6px 4px; cursor:pointer;
  background:rgba(255,255,255,.06);
  border:1px solid rgba(255,255,255,.14); border-radius:12px;
  transition:background .15s ease, border-color .15s ease, transform .1s ease;
}
.fb-opt:hover{ background:rgba(255,255,255,.11); }
.fb-opt:active{ transform:translateY(1px); }
/* Selection is carried by BORDER and BACKGROUND, not by colour alone — a player who cannot
   distinguish the highlight colour can still see which of the three is chosen (WCAG 1.4.1). */
.fb-opt--on{
  background:rgba(124,207,36,.20);
  border-color:#7ccf24; border-width:2px; padding:5px 3px;
}
.fb-opt__icon{ font-size:min(2.7cqh,15px); line-height:1.1; }
.fb-opt__label{
  font-family:"Nunito",sans-serif; font-size:min(2.1cqh,11px); font-weight:700;
  color:rgba(255,255,255,.82); text-align:center; line-height:1.15;
}
.fb-opt--on .fb-opt__label{ color:#fff; }

.fb-note{
  width:100%; box-sizing:border-box; resize:none;
  font-family:"Nunito",sans-serif; font-size:min(2.3cqh,12.5px); font-weight:600; line-height:1.45;
  color:#fff; background:rgba(0,0,0,.26);
  border:1px solid rgba(255,255,255,.18); border-radius:12px;
  padding:10px 12px;
}
.fb-note::placeholder{ color:rgba(255,255,255,.38); }
.fb-note:focus{ outline:none; border-color:#5bc4ff; background:rgba(0,0,0,.34); }
.fb-warn{
  font-family:"Nunito",sans-serif; font-size:min(2cqh,10.5px); font-weight:600;
  color:rgba(255,255,255,.55); margin:.5cqh 0 1cqh;
}

/* The Settings entry point. A card that is also a button — same shape as the cards around it so it
   reads as part of the list, not as an advert wedged into it. */
.settings-card--action{
  display:block; text-align:left; cursor:pointer;
  transition:background .15s ease, transform .1s ease;
}
.settings-card--action:hover{ background:rgba(255,255,255,.10); }
.settings-card--action:active{ transform:translateY(1px); }

/* ── Find-your-range screen ─────────────────────────────────────────────────
   The note being heard has to be big. It is the first proof a player gets that the game is
   responding to their voice at all, and on this screen that proof matters more than the number. */
.rf-meter{
  width:100%; max-width:420px; margin:1.4cqh auto 0; text-align:center;
  background:rgba(255,255,255,.06); border:1px solid rgba(255,255,255,.13);
  border-radius:18px; padding:1.6cqh 16px;
}
.rf-note{
  font-family:"Fredoka",sans-serif; font-weight:600;
  font-size:clamp(26px,9cqh,52px); line-height:1.1;
  color:rgba(255,255,255,.35); transition:color .12s ease, transform .12s ease;
}
.rf-note--on{ color:#7ccf24; transform:scale(1.06); }
.rf-hint{
  font-family:"Nunito",sans-serif; font-size:min(2.2cqh,12px); font-weight:600;
  color:rgba(255,255,255,.55); margin-top:.4cqh;
}
.rf-got{
  font-family:"Nunito",sans-serif; font-size:min(2.4cqh,12.5px); font-weight:600;
  color:rgba(255,255,255,.75); text-align:center; margin-top:1cqh;
}
.rf-got b{ color:#ffd24a; }
.rf-deaf{
  max-width:420px; margin:1.2cqh auto 0; padding:10px 14px;
  background:rgba(255,159,67,.12); border:1px solid #ff9f43; border-radius:12px;
  font-family:"Nunito",sans-serif; font-size:min(2.2cqh,12px); font-weight:600; line-height:1.5;
  color:rgba(255,255,255,.85); text-align:center;
}
.rf-link{
  background:none; border:none; cursor:pointer; padding:0;
  font-family:"Nunito",sans-serif; font-size:min(2.3cqh,12.5px); font-weight:700;
  color:#5bc4ff; text-decoration:underline;
}
.rf-link--block{ display:block; margin:1.6cqh auto 0; min-height:24px; }

/* Shown only inside Facebook/Instagram's embedded browser, which is why it can afford to be loud:
   the few who see it are about to lose a game to a microphone that will never work. */
.inapp-warn{
  margin:0 0 10px; padding:9px 12px; border-radius:12px;
  background:rgba(255,159,67,.14); border:1px solid #ff9f43;
  font-family:"Nunito",sans-serif; font-size:min(2.2cqh,12px); font-weight:600; line-height:1.45;
  color:#ffd9b0; text-align:center;
}
.inapp-warn b{ color:#fff; }

/* "We can't hear you" — shown over the game when the mic is live and nothing has ever arrived.
   Deliberately NOT a modal: the game carries on underneath, because the player may fix it (sing
   louder) and see it disappear, and because stopping the round would be a second punishment on top
   of a microphone that already is not working. */
.nohear{
  position:absolute; left:50%; top:52%; transform:translate(-50%,-50%);
  z-index:30; width:min(88%, 420px); pointer-events:none;
}
.nohear-card{
  background:rgba(12,8,28,.92); border:1.5px solid #ff9f43; border-radius:16px;
  padding:14px 16px; box-shadow:0 18px 50px -20px rgba(0,0,0,.9);
}
.nohear-title{
  font-family:"Fredoka",sans-serif; font-size:min(3cqh,16px); font-weight:600;
  color:#ffb765; margin-bottom:6px; text-align:center;
}
.nohear-list{
  margin:0; padding-inline-start:18px;
  font-family:"Nunito",sans-serif; font-size:min(2.3cqh,12.5px); font-weight:600; line-height:1.5;
  color:rgba(255,255,255,.85);
}
.nohear-list b{ color:#fff; }

/* First-run onboarding tour */
.tour-wrap{
  position:absolute; inset:0; z-index:12; display:flex; align-items:center; justify-content:center;
  padding:4cqh 6cqw; overflow:hidden auto;
}
.tour-skip{
  position:absolute; top:3cqh; right:5cqw; z-index:2; cursor:pointer;
  background:rgba(10,6,24,.5); border:1.5px solid rgba(255,255,255,.22); border-radius:999px;
  color:rgba(255,255,255,.75); font-family:"Fredoka",sans-serif; font-size:13px; font-weight:600; padding:6px 14px;
}
.tour-skip:hover{ background:rgba(20,12,40,.7); }
.tour-card{
  width:100%; max-width:440px; text-align:center;
  background:rgba(12,8,28,.72);
  border:1.5px solid rgba(255,255,255,.16); border-radius:20px;
  padding:26px 24px 22px; box-shadow:0 18px 50px -20px rgba(0,0,0,.8);
}
.tour-icon{ font-size:clamp(44px,11cqh,64px); line-height:1; margin-bottom:10px;
  filter:drop-shadow(0 4px 10px rgba(0,0,0,.4)); }
.tour-title{ font-family:"Fredoka",sans-serif; font-weight:700; font-size:clamp(21px,5.5cqh,27px); color:#fff; margin-bottom:8px; }
.tour-text{ font-family:"Nunito",sans-serif; font-size:15px; line-height:1.6; color:rgba(255,255,255,.8); margin:0 auto 18px; max-width:34ch; }
.tour-dots{ display:flex; justify-content:center; gap:7px; margin-bottom:18px; }
.tour-dot{ width:8px; height:8px; border-radius:50%; background:rgba(255,255,255,.25); transition:all .2s ease; }
.tour-dot.on{ background:#5bc4ff; width:22px; border-radius:999px; }
.tour-nav{ display:flex; align-items:center; justify-content:center; gap:12px; }
.tour-back{
  cursor:pointer; background:none; border:1.5px solid rgba(255,255,255,.22); border-radius:999px;
  color:rgba(255,255,255,.8); font-family:"Fredoka",sans-serif; font-size:14px; font-weight:600; padding:10px 18px;
}
.tour-back:hover{ background:rgba(255,255,255,.08); }
.tour-next{ width:auto; min-width:130px; }

.legal-tabs{ max-width:420px; }
.legal-doc{
  width:100%; max-width:680px; margin:12px auto 0; flex:1; min-height:0; overflow-y:auto;
  padding:2px 4px 8px;
}
/* .45 measured 4.44:1 — under 4.5:1 by a hair, but under it. */
.legal-updated{ font-family:"Nunito",sans-serif; font-size:12px; color:rgba(255,255,255,.55); margin:0 0 14px; }
.legal-sec{ margin-bottom:16px; }
.legal-h{ font-family:"Fredoka",sans-serif; font-size:15px; font-weight:600; color:#fff; margin:0 0 6px; }
.legal-p{ font-family:"Nunito",sans-serif; font-size:13px; font-weight:500; line-height:1.6;
  color:rgba(255,255,255,.72); margin:0 0 8px; }
.legal-foot{ margin-top:12px; color:rgba(255,255,255,.55); }   /* .4 measured 3.76:1 */

/* Open-source notices (Credits tab) */
.legal-tabs{ max-width:520px; }
.credit-item{
  border:1px solid rgba(255,255,255,.10); border-radius:12px;
  background:rgba(255,255,255,.045); padding:9px 11px; margin-bottom:8px;
}
.credit-top{ display:flex; align-items:baseline; justify-content:space-between; gap:10px; flex-wrap:wrap; }
.credit-name{ font-family:"Fredoka",sans-serif; font-size:14px; font-weight:600; color:#fff; }
.credit-lic{
  font-family:"Nunito",sans-serif; font-size:10.5px; font-weight:700; white-space:nowrap;
  color:#8ddfff; border:1px solid rgba(141,223,255,.4); border-radius:6px; padding:1px 6px;
}
.credit-meta{ font-family:"Nunito",sans-serif; font-size:11.5px; font-weight:600;
  color:rgba(255,255,255,.55); margin-top:3px; }
.credit-note{ font-family:"Nunito",sans-serif; font-size:12px; font-weight:500; line-height:1.5;
  color:rgba(255,255,255,.68); margin-top:4px; }
.credit-url{ font-family:"Nunito",sans-serif; font-size:11px; font-weight:600;
  color:rgba(141,223,255,.72); margin-top:4px; word-break:break-all; }

/* Full licence texts — collapsed so the screen stays scannable, but the text ships in the page. */
.lic-doc{
  border:1px solid rgba(255,255,255,.12); border-radius:12px;
  background:rgba(255,255,255,.04); margin-bottom:8px; overflow:hidden;
}
.lic-doc > summary{
  cursor:pointer; list-style:none; padding:9px 12px;
  font-family:"Fredoka",sans-serif; font-size:13.5px; font-weight:600; color:#8ddfff;
}
.lic-doc > summary::-webkit-details-marker{ display:none; }
.lic-doc > summary::before{ content:'▸ '; color:rgba(141,223,255,.7); }
.lic-doc[open] > summary::before{ content:'▾ '; }
.lic-doc > summary:focus-visible{ outline:2px solid #8ddfff; outline-offset:-2px; }
.lic-holders{
  font-family:"Nunito",sans-serif; font-size:11.5px; font-weight:600; line-height:1.5;
  color:rgba(255,255,255,.6); margin:0; padding:0 12px 8px;
}
.lic-text{
  margin:0; padding:10px 12px 12px;
  border-top:1px solid rgba(255,255,255,.08);
  font-family:"IBM Plex Mono",ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  font-size:10.5px; line-height:1.5; color:rgba(255,255,255,.68);
  white-space:pre-wrap; word-break:break-word;   /* legal text must never scroll sideways */
  max-height:none;
}

.setting-label{
  font-family:"Fredoka", sans-serif;
  font-size:17px; font-weight:600; color:#fff;
  margin-bottom:4px;
}
.setting-desc{
  font-family:"Nunito", sans-serif;
  font-size:12px; font-weight:600;
  color:rgba(255,255,255,.5);
  line-height:1.4; margin-bottom:14px;
}

.slider-wrap{
  display:flex; align-items:center; gap:10px;
}
.slider-end-label{
  font-family:"Fredoka", sans-serif;
  font-size:12px; font-weight:600;
  color:rgba(255,255,255,.45);
  min-width:28px;
}

/* The track is 6px because a hairline looks right. The ELEMENT was 6px too, which is a different
   claim entirely: on a range input the element itself is what receives the pointer, so the whole
   control was a six-pixel band to hit with a thumb. WCAG 2.5.8 asks for 24×24, and anyone who has
   tried to nudge one of these on a phone knows why.
   The fix keeps the hairline and grows only the hit area: paint the gradient on the TRACK
   pseudo-element, give the input real height, and make its own background transparent. Nothing
   moves visually and the target becomes 28px tall. */
.sens-slider{
  flex:1; -webkit-appearance:none; appearance:none;
  height:28px; background:transparent;
  outline:none; cursor:pointer;
}
.sens-slider::-webkit-slider-runnable-track{
  height:6px; border-radius:999px;
  background:linear-gradient(90deg,
    #00CED1 0%, #5bc4ff var(--pct, 70%), rgba(255,255,255,.18) var(--pct, 70%));
}
.sens-slider::-moz-range-track{
  height:6px; border-radius:999px;
  background:linear-gradient(90deg,
    #00CED1 0%, #5bc4ff var(--pct, 70%), rgba(255,255,255,.18) var(--pct, 70%));
}
/* WebKit centres the thumb on the TRACK box, not on the element, so without this the thumb rides
   along the top edge once the element is taller than the track. Half the thumb minus half the
   track, negated. */
.sens-slider::-webkit-slider-thumb{ margin-top:-8px; }
.sens-slider::-webkit-slider-thumb{
  -webkit-appearance:none;
  width:22px; height:22px; border-radius:50%;
  background:radial-gradient(circle at 35% 35%, #fff, #00CED1 60%, #007f8a);
  border:2px solid rgba(255,255,255,.7);
  box-shadow:0 2px 8px rgba(0,0,0,.5), 0 0 12px rgba(0,206,209,.6);
  cursor:pointer;
}
.sens-slider::-moz-range-thumb{
  width:22px; height:22px; border-radius:50%;
  background:radial-gradient(circle at 35% 35%, #fff, #00CED1 60%, #007f8a);
  border:2px solid rgba(255,255,255,.7);
  box-shadow:0 2px 8px rgba(0,0,0,.5), 0 0 12px rgba(0,206,209,.6);
  cursor:pointer;
}

.slider-value{
  text-align:center; margin-top:8px;
  font-family:"Fredoka", sans-serif;
  font-size:13px; font-weight:600;
  color:#00CED1;
  text-shadow:0 0 8px rgba(0,206,209,.6);
}

.setting-row--sep{
  border-top:1px solid rgba(255,255,255,.08);
  margin-top:16px;
  padding-top:16px;
}

.voice-select{
  width:100%; margin-top:10px;
  background:rgba(255,255,255,.08); color:#fff;
  border:1px solid rgba(255,255,255,.22); border-radius:12px;
  padding:10px 14px;
  font-family:"Fredoka", sans-serif; font-size:15px;
  -webkit-appearance:none; appearance:none; cursor:pointer;
  outline:none;
}
.voice-select option{ background:#1a1a2e; color:#fff; }

.hud-badge{
  position:absolute; top:4px; left:50%; transform:translateX(-50%);
  font-family:"Fredoka", sans-serif; font-size:11px; font-weight:600;
  color:rgba(255,255,255,.50); letter-spacing:.5px; white-space:nowrap;
  pointer-events:none;
}

/* ── Hourly scale badge (home screen) ───────────────── */
.hourly-badge{
  display:flex; flex-direction:column; align-items:center; gap:2px;
  margin-top:10px;
  padding:7px 18px;
  background:rgba(255,255,255,.07);
  border:1px solid rgba(255,255,255,.14);
  border-radius:20px;
}
.hourly-scale{
  font-family:"Bowlby One", sans-serif;
  font-size:clamp(14px, 3.8cqh, 18px);
  background:linear-gradient(180deg, #fff6c8 0%, #ffd24a 60%, #e76a14 100%);
  -webkit-background-clip:text; background-clip:text; color:transparent;
}
.hourly-timer{
  font-family:"Fredoka", sans-serif;
  font-size:11px; font-weight:600;
  color:rgba(255,255,255,.40); letter-spacing:.5px;
}

/* ── Bird hit flash ────────────────────────────────── */
/* WCAG 2.3.1 (Three Flashes or Below Threshold) — Level A, the absolute minimum: content must not
   flash more than three times in any one second.
   This was `birdHit 0.12s infinite`, cycling opacity 1 -> 0.2 -> 1: a full flash every 0.12s, or
   8.3 per second — nearly three times over the limit, on a glowing bird against a dark background,
   every single time you clip a pillar. There is a small-area exemption in the standard that this
   might have squeaked under, but leaning on an exemption when the fix is one line is not a
   judgement call worth making.
   Now a single pulse: one dip and back, no repeat. Still reads as an impact, and one flash cannot
   exceed a rate limit at all. */
@keyframes birdHit{
  0%   { opacity:1; }
  45%  { opacity:0.35; }
  100% { opacity:1; }
}
.bird-game--hit{
  animation: birdHit 0.3s ease-out 1 !important;
  filter:
    drop-shadow(0 0 14px rgba(255,80,80,.7))
    drop-shadow(0 6px 10px rgba(0,0,0,.5)) !important;
}

/* ── Screen hit flash ──────────────────────────────── */
.hit-flash{
  position:absolute; inset:0;
  pointer-events:none; z-index:200;
}
@keyframes screenHit{
  /* A full-screen red wash is the single largest luminance event in the game. One flash, so it is
     nowhere near the three-per-second limit, but it does not need to be this violent to read. */
  0%  { box-shadow: inset 0 0 80px 30px rgba(255,40,40,.50); }
  55% { box-shadow: inset 0 0 50px 20px rgba(255,40,40,.24); }
  100%{ box-shadow: inset 0 0  0px  0px rgba(255,40,40,0);   }
}
.hit-flash.active{ animation: screenHit 0.45s ease-out forwards; }

/* ── Copyright notice ─────────────────────────────── */
.copyright-text {
  position: absolute;
  bottom: 3%;
  left: 0; right: 0;
  font-size: 10px;
  /* No opacity here. It used to be 0.35, which MULTIPLIED with the .42 alpha set on this class
     further down — an effective alpha near .15, far below anything readable. Carry the whole
     value in `color` so there is one number to reason about. */
  color: #ffffff;
  text-align: center;
  letter-spacing: 0.05em;
  line-height: 1.4;
  pointer-events: none;
  z-index: 11;
}

/* ── Home screen personal best ────────────────────── */
.home-best{
  font-family:"Fredoka", sans-serif;
  font-size:13px; font-weight:600;
  color:rgba(255,255,255,.45);
  margin-top:6px; letter-spacing:.4px;
}
.home-best strong{
  font-family:"Bowlby One", sans-serif;
  font-size:15px;
  background:linear-gradient(180deg, #fff6c8 0%, #ffd24a 60%, #e76a14 100%);
  -webkit-background-clip:text; background-clip:text; color:transparent;
}

/* ── First-visit Voice Setup screen ───────────────── */
.voice-setup-content{
  position:absolute; inset:0; z-index:10;
  display:flex; flex-direction:column; align-items:center;
  padding: 10% 8% 6%;
}
.voice-setup-title{
  font-family:"Bowlby One", sans-serif;
  font-size:clamp(22px, 6cqh, 32px);
  background:linear-gradient(180deg, #fff6c8 0%, #ffd24a 60%, #e76a14 100%);
  -webkit-background-clip:text; background-clip:text; color:transparent;
  text-align:center; margin-bottom:10px;
}
.voice-setup-sub{
  font-family:"Fredoka", sans-serif;
  font-size:13px; color:rgba(255,255,255,.55);
  text-align:center; margin:0 0 16px; line-height:1.5;
}
.voice-setup-list{
  width:100%; display:flex; flex-direction:column; gap:9px;
  overflow-y:auto; flex:1; padding-right:2px;
}
.voice-option{
  width:100%; background:rgba(255,255,255,.06);
  border:1.5px solid rgba(255,255,255,.1);
  border-radius:14px; padding:11px 16px;
  display:flex; align-items:center; justify-content:space-between;
  color:#fff; cursor:pointer; transition:all .15s;
}
.voice-option.selected{
  background:rgba(255,210,74,.18);
  border-color:#ffd24a;
}
.voice-option__name{
  font-family:"Fredoka", sans-serif;
  font-size:15px; font-weight:600;
}
.voice-option__range{
  font-size:11px; color:rgba(255,255,255,.5);
}

/* ── Leaderboard screen ────────────────────────────── */
.lb-content{
  position:absolute; inset:0; z-index:10;
  display:flex; flex-direction:column; align-items:center;
  padding: 4cqh 16px 3cqh;
}
.lb-title{
  font-family:"Bowlby One", sans-serif;
  font-size:clamp(22px, 6.5cqh, 30px); letter-spacing:2px;
  background:linear-gradient(180deg, #fff6c8 0%, #ffd24a 55%, #e76a14 100%);
  -webkit-background-clip:text; background-clip:text; color:transparent;
  filter:drop-shadow(0 2px 8px rgba(0,0,0,.5));
  margin-bottom:14px;
}
/* Scrollable list — ~10 compact rows visible, the rest scroll (top 100) */
.lb-list{
  width:100%; max-width:560px; margin-inline:auto;
  display:flex; flex-direction:column; gap:3px;
  flex:1; min-height:0; overflow-y:auto; padding:0 4px 4px;
  -webkit-overflow-scrolling:touch;
}
.lb-list::-webkit-scrollbar{ width:5px; }
.lb-list::-webkit-scrollbar-thumb{ background:rgba(255,255,255,.18); border-radius:3px; }
/* Exactly ~10 rows visible on a phone. The row height is driven by cqh (not fixed px) so ten fit
   whatever the screen is, capped at 40px so a desktop shows a few more rather than sparse,
   stretched rows. NB: Bowlby One's default line box is ~1.53x its font-size, which is what made
   these rows 34.5px — the explicit line-heights below are what actually buys the space. */
.lb-row{
  display:flex; align-items:center; gap:10px;
  /* border-box, or min-height lands on the CONTENT box and padding+border get ADDED on top —
     the same content-box trap that pushed the Duel Setup card out of its column. */
  box-sizing:border-box;
  padding:2px 12px; flex:0 0 auto;
  min-height:min(6.7cqh, 40px);
  background:rgba(255,255,255,.06);
  border:1px solid rgba(255,255,255,.10);
  border-radius:9px;
}
.lb-rank-1{ border-color:rgba(255,210,74,.40); background:rgba(255,210,74,.08); }
.lb-rank-2{ border-color:rgba(200,200,220,.30); background:rgba(200,200,220,.06); }
.lb-rank-3{ border-color:rgba(205,127,50,.30);  background:rgba(205,127,50,.06); }
.lb-you{ border-color:rgba(91,196,255,.55); background:rgba(91,196,255,.14); }
.lb-rank{
  min-width:30px; text-align:center; font-size:15px; line-height:1;
  font-family:"Fredoka", sans-serif; font-weight:700; color:rgba(255,255,255,.55);
}
.lb-rank-1 .lb-rank,.lb-rank-2 .lb-rank,.lb-rank-3 .lb-rank{ font-size:18px; line-height:1.2; }
.lb-name{
  flex:1; text-align:left; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
  font-family:"Fredoka", sans-serif; font-size:14px; font-weight:600; line-height:1.2;
  color:rgba(255,255,255,.88);
}
.lb-you .lb-name{ color:#bfe6ff; }
.lb-score{
  font-family:"Bowlby One", sans-serif; font-size:16px; line-height:1.2;
  background:linear-gradient(180deg, #fff6c8 0%, #ffd24a 55%, #e76a14 100%);
  -webkit-background-clip:text; background-clip:text; color:transparent;
}
.lb-empty{
  text-align:center;
  font-family:"Fredoka", sans-serif; font-size:17px; font-weight:600;
  color:rgba(255,255,255,.40); line-height:1.7;
  margin-top:30px;
}

/* ── Game Over screen ──────────────────────────────── */
.gameover-overlay{
  position:absolute; inset:0; z-index:9;
  background:rgba(0,0,0,.50);
}
.gameover-content{
  position:absolute; inset:0; z-index:10;
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  gap:9px;
  padding: 4cqh 9%;
  overflow: hidden auto;   /* landscape: never clip the buttons on a short screen */
}
.gameover-title{
  font-family:"Bowlby One", sans-serif;
  font-size:clamp(26px, 7cqh, 38px); letter-spacing:1.5px;
  background:linear-gradient(180deg, #ffb0b0 0%, #ff4444 50%, #cc0000 100%);
  -webkit-background-clip:text; background-clip:text; color:transparent;
  filter:drop-shadow(0 2px 8px rgba(255,80,80,.6));
  margin-bottom:0;
}
.gameover-final{
  font-family:"Fredoka", sans-serif;
  font-size:13px; font-weight:600;
  color:rgba(255,255,255,.55); letter-spacing:1.5px; text-transform:uppercase;
  margin-bottom:-8px;
}
.gameover-score{
  font-family:"Bowlby One", sans-serif;
  font-size:clamp(46px, 13cqh, 66px); line-height:1;
  background:linear-gradient(180deg, #fff6c8 0%, #ffd24a 45%, #e76a14 100%);
  -webkit-background-clip:text; background-clip:text; color:transparent;
  filter:drop-shadow(0 2px 8px rgba(0,0,0,.6));
  margin-bottom:2px;
}

/* ── Game Over auto-return timer ──────────────────── */
.go-timer{
  position:absolute; bottom:6%; left:50%; transform:translateX(-50%);
  width:44px; height:44px;
  display:flex; align-items:center; justify-content:center;
  z-index:12;
}
.go-timer svg{ position:absolute; inset:0; }
.go-timer__n{
  font-family:"Fredoka", sans-serif;
  font-size:15px; font-weight:700;
  color:rgba(255,255,255,.8);
  position:relative; z-index:1;
}

/* (Ad banner + ad overlays removed for MVP — no ads while testing.) */
@keyframes adFadeOut{ to{ opacity:0; pointer-events:none; } }

/* ── Revive button (Game Over screen) ─────────────── */
.revive-btn{
  width:100%;
  background:linear-gradient(160deg, #6d28d9 0%, #7c3aed 30%, #a855f7 65%, #c084fc 100%);
  color:#fff; border:none; border-radius:16px;
  padding:12px 20px;
  font-family:"Nunito", sans-serif;
  font-size:clamp(13px,3.8cqh,16px); font-weight:900; cursor:pointer;
  box-shadow:
    0 0 0 2px rgba(168,85,247,.35),
    0 0 22px rgba(168,85,247,.55),
    0 0 44px rgba(168,85,247,.25),
    0 6px 20px rgba(0,0,0,.50),
    inset 0 1px 0 rgba(255,255,255,.28),
    inset 0 -3px 6px rgba(0,0,0,.25);
  animation: revivePulse 2.2s ease-in-out infinite;
  transition: filter .15s ease;
}
.revive-btn:hover{ filter:brightness(1.12); }
.revive-btn:active{ transform:scale(.97); animation:none; }

@keyframes revivePulse{
  0%,100%{
    transform:scale(1);
    box-shadow:
      0 0 0 2px rgba(168,85,247,.35),
      0 0 22px rgba(168,85,247,.55),
      0 0 44px rgba(168,85,247,.25),
      0 6px 20px rgba(0,0,0,.50),
      inset 0 1px 0 rgba(255,255,255,.28),
      inset 0 -3px 6px rgba(0,0,0,.25);
  }
  50%{
    transform:scale(1.03);
    box-shadow:
      0 0 0 3px rgba(192,132,252,.50),
      0 0 34px rgba(192,132,252,.75),
      0 0 66px rgba(168,85,247,.45),
      0 8px 26px rgba(0,0,0,.50),
      inset 0 1px 0 rgba(255,255,255,.35),
      inset 0 -3px 6px rgba(0,0,0,.25);
  }
}

/* ── Play Again button — Game Over variant ─────────── */
.play--gameover{
  width:100%;
  max-width:none;
  border-radius:40px;
  padding:12px 24px;
}
.play--gameover .label{
  font-size:clamp(21px, 6.2cqh, 28px);
  letter-spacing:2px;
}

/* ── Game Over secondary "Home" button (full-width, ghost) ── */
.gameover-home{
  width:100%; min-height:46px;
  background:rgba(255,255,255,.07);
  border:1.5px solid rgba(255,255,255,.22);
  border-radius:14px; cursor:pointer;
  color:rgba(255,255,255,.9);
  font-family:"Fredoka", sans-serif; font-size:16px; font-weight:600; letter-spacing:.5px;
  transition:transform .12s ease, background .15s ease, border-color .15s ease;
}
.gameover-home:hover{ background:rgba(255,255,255,.12); border-color:rgba(255,255,255,.35); }
.gameover-home:active{ transform:scale(.97); }

/* ── 3-2-1 go countdown overlay (inside PlayingView) ── */
.go-overlay{
  position:absolute; inset:0; z-index:500;
  background:rgba(0,0,0,.55);
  display:flex; align-items:center; justify-content:center;
  font-family:"Bowlby One", sans-serif;
  font-size:clamp(88px,26cqh,148px);
  font-weight:900; color:#fff;
  text-shadow:0 4px 24px rgba(0,0,0,.6);
  pointer-events:none;
}

/* ── Scale picker screen ───────────────────────────── */
/* Home hourly badge, now clickable */
.hourly-badge--btn{
  cursor:pointer; transition:transform .15s ease, background .15s ease;
  font:inherit;
}
.hourly-badge--btn:hover{ transform:scale(1.04); background:rgba(255,255,255,.12); }

.scale-sub{
  font-family:"Nunito", sans-serif;
  font-size:12px; font-weight:600;
  color:rgba(255,255,255,.55);
  /* margin-top was -6%, tuned to a title margin that no longer exists. It pulled this line UP into
     the heading above it: on Choose a Scale the subtitle overlapped the title by 14.8px, measured.
     Range Select and Duel Setup had each already been patched to cancel it, which is the signal
     that the default itself was wrong — Leagues and the roulette were about to need a third and
     fourth patch. Fixed at the root; those two overrides still set their own margins and are
     unaffected. */
  text-align:center; margin:0 0 5%;
}

.scale-hourly{
  width:100%; display:flex; align-items:center; justify-content:space-between;
  gap:10px; margin-bottom:12px; padding:10px 16px;
  background:rgba(255,210,74,.14);
  border:1.5px solid rgba(255,210,74,.55);
  border-radius:14px; color:#fff; cursor:pointer;
  transition:transform .15s ease, background .15s ease;
}
.scale-hourly:hover{ transform:scale(1.02); background:rgba(255,210,74,.22); }
.scale-hourly__tag{
  font-family:"Fredoka", sans-serif; font-size:12px; font-weight:600;
  letter-spacing:.5px; color:rgba(255,255,255,.7);
}
.scale-hourly__name{
  font-family:"Bowlby One", sans-serif; font-size:16px;
  background:linear-gradient(180deg, #fff6c8 0%, #ffd24a 60%, #e76a14 100%);
  -webkit-background-clip:text; background-clip:text; color:transparent;
}

.scale-tabs{
  width:100%; display:flex; gap:6px; margin-bottom:12px;
}
.scale-tab{
  flex:1; min-height:44px; padding:0 4px; border-radius:12px; cursor:pointer;
  background:rgba(255,255,255,.06);
  border:1.5px solid rgba(255,255,255,.1);
  color:rgba(255,255,255,.72);
  font-family:"Fredoka", sans-serif; font-size:14px; font-weight:600;
  transition:transform .12s ease, background .15s ease, border-color .15s ease, color .15s ease;
}
.scale-tab:active{ transform:scale(.95); }
.scale-tab.active{
  background:rgba(91,196,255,.20);
  border-color:#5bc4ff; color:#fff;
}

.scale-grid{
  width:100%; flex:1; min-height:0; overflow-y:auto;
  display:grid; grid-template-columns:1fr 1fr; gap:8px;
  padding-right:2px; align-content:start;
}
/* The star is a sibling button now, not nested inside .scale-btn — this cell is what it
   positions against. */
.scale-cell{ position:relative; display:flex; }
.scale-btn{
  flex:1 1 auto; min-width:0;
  display:flex; flex-direction:column; align-items:flex-start; gap:2px;
  background:rgba(255,255,255,.06);
  border:1.5px solid rgba(255,255,255,.1);
  border-radius:13px; padding:10px 13px;
  color:#fff; cursor:pointer; transition:all .15s;
}
.scale-star{
  position:absolute; top:5px; right:7px; z-index:3;
  background:none; border:none;
  /* was .34 → measured 3.03:1 against the composited card, under the 4.5:1 that WCAG 1.4.3
     asks for body text. .55 clears it with headroom and still reads as secondary. */
  font-size:16px; line-height:1; color:rgba(255,255,255,.55);
  padding:3px; border-radius:8px; cursor:pointer;
  transition:transform .12s ease, color .12s ease;
}
.scale-star.on{ color:#ffd24a; text-shadow:0 0 6px rgba(255,210,74,.5); }
.scale-star:hover{ transform:scale(1.22); }
.scale-star:active{ transform:scale(.9); }
.scale-btn:hover{
  background:rgba(91,196,255,.16);
  border-color:#5bc4ff; transform:translateY(-1px);
}
.scale-btn__name{
  font-family:"Fredoka", sans-serif; font-size:15px; font-weight:600;
  padding-right:20px;   /* room for the ★ in the corner */
}
.scale-btn__range{
  font-family:"Nunito", sans-serif; font-size:11px; font-weight:600;
  color:rgba(255,255,255,.5);
}
.scale-empty{
  grid-column:1 / -1; text-align:center;
  font-family:"Fredoka", sans-serif; font-size:14px; font-weight:600;
  color:rgba(255,255,255,.6); line-height:1.6; padding:28px 12px;
}

/* ── Range picker screen ───────────────────────────── */
.range-row{
  display:flex; align-items:center; justify-content:space-between; gap:12px;
  padding:6px 0;
}
.range-row--sep{ border-top:1px solid rgba(255,255,255,.1); margin-top:12px; padding-top:16px; }
.range-label{
  font-family:"Fredoka", sans-serif; font-size:17px; font-weight:600; color:#fff;
}
.range-picker{ display:flex; gap:8px; }
.range-sel{
  -webkit-appearance:none; appearance:none;
  min-height:48px;
  background:rgba(255,255,255,.09); color:#fff;
  border:1.5px solid rgba(255,255,255,.18); border-radius:12px;
  font-family:"Fredoka", sans-serif; font-size:18px; font-weight:600;
  padding:12px 18px; cursor:pointer; text-align:center;
  transition:transform .12s ease, border-color .15s ease, background .15s ease;
}
.range-sel:focus{ outline:none; border-color:#5bc4ff; }
.range-sel:active{ transform:scale(.97); }
.range-sel--oct{ padding:12px 16px; }
.range-sel option{ background:#1a1030; color:#fff; }
.range-hint{
  margin-top:16px; text-align:center;
  font-family:"Fredoka", sans-serif; font-size:13px; font-weight:600;
  color:#8fe3a0; letter-spacing:.3px;
}
.range-hint--warn{ color:#ffb15b; }

/* ── Menu polish: press feedback, tap targets, readability ── */
.play, .icon-btn, .scale-btn, .back-btn { transition:transform .12s ease, filter .15s ease, background .15s ease; }
.play:active   { transform:scale(.97); }
.icon-btn      { min-height:48px; }
.icon-btn:active { transform:scale(.94); }
.scale-btn:active{ transform:scale(.97); }
.back-btn      { width:44px; height:44px; }
.back-btn:active { transform:scale(.92); }
/* Raise low-contrast secondary text for readability (WCAG on dark) */
.scale-sub        { font-size:13px; color:rgba(255,255,255,.62); }
.scale-btn__range { color:rgba(255,255,255,.62); }
.slider-end-label { color:rgba(255,255,255,.58); }
.setting-desc     { color:rgba(255,255,255,.62); }
.copyright-text   { color:rgba(255,255,255,.58); }   /* .42 measured 4.02:1 */
.hourly-timer, .home-best { color:rgba(255,255,255,.62); }

/* ── Warm-up / tuning-zone indicator (first 15 pillars) ── */
.warmup-pill{
  position:absolute; top:13.5%; left:50%; transform:translateX(-50%);
  z-index:14; pointer-events:none; white-space:nowrap;
  background:linear-gradient(180deg, rgba(91,196,255,.92), rgba(56,150,230,.92));
  color:#fff; font-family:"Fredoka", sans-serif; font-size:12.5px; font-weight:600;
  letter-spacing:.4px; padding:5px 14px; border-radius:999px;
  border:1px solid rgba(255,255,255,.30);
  box-shadow:0 2px 10px rgba(0,0,0,.4), 0 0 16px rgba(91,196,255,.5);
  animation:warmupPulse 1.8s ease-in-out infinite;
  transition:opacity .45s ease, transform .45s ease;
}
.warmup-pill--hidden{ opacity:0; transform:translateX(-50%) translateY(-10px); }
@keyframes warmupPulse{
  0%,100%{ box-shadow:0 2px 10px rgba(0,0,0,.4), 0 0 14px rgba(91,196,255,.45); }
  50%    { box-shadow:0 2px 10px rgba(0,0,0,.4), 0 0 24px rgba(91,196,255,.55); }
}

/* ── Home: split Solo / Duel buttons (Sing Duel step 1) ── */
.play-choice{
  display:flex; flex-direction:row; align-items:center; justify-content:center; gap:3cqw;
  width:auto;
}
.play--half{
  width:auto;
  min-width: 27cqw;
  max-width:210px;
  padding: 1.7cqh 2.4cqw 2cqh;
}
.play--half .label--sm{
  font-size:clamp(13px, 4.4cqh, 21px);
  letter-spacing:1px; white-space:nowrap;
  -webkit-text-stroke:1.7px #2e6b0a;
}
.play--duel{
  /* same color for now (per spec); distinguishable later if desired */
}
.play--duel .duel-cost{
  display:block;
  margin-top:2px;
  font-family:"Fredoka", sans-serif;
  font-size:11px;
  font-weight:600;
  letter-spacing:.3px;
  color:#fffbe6;
  text-shadow:0 1px 1px rgba(0,0,0,.35);
}

/* ── Trophy / League corner (lobby, top-right) ── */
.trophy-corner{
  position:absolute; top:3.5%; right:4%; z-index:25;
  display:flex; align-items:center; gap:8px;
}
.tc-trophies, .tc-league{
  display:flex; align-items:center; justify-content:center; gap:5px;
  min-height:40px; padding:6px 12px; cursor:pointer;
  background:rgba(10,6,24,.72);
  border:1.5px solid rgba(255,255,255,.22); border-radius:999px;
  transition:transform .12s ease, background .15s ease;
}
.tc-trophies:active, .tc-league:active{ transform:scale(.94); }
.tc-icon{ font-size:15px; }
.tc-count{
  font-family:"Fredoka", sans-serif; font-size:16px; font-weight:700; color:#ffd24a;
}
.tc-league{ padding:6px 10px; }
.tc-medal{ font-size:20px; line-height:1; }

/* ── Leagues screen ── */
.league-list{
  width:100%; display:flex; flex-direction:column; gap:8px; margin-top:8px;
  overflow-y:auto;
}
.league-row{
  display:flex; align-items:center; gap:12px;
  padding:12px 16px; border-radius:14px;
  background:rgba(255,255,255,.05);
  border:1.5px solid rgba(255,255,255,.09);
}
.league-row.current{
  background:rgba(255,210,74,.16);
  border-color:#ffd24a;
  box-shadow:0 0 16px rgba(255,210,74,.25);
}
.league-medal{ font-size:26px; line-height:1; }
.league-name{
  font-family:"Fredoka", sans-serif; font-size:17px; font-weight:600; color:#fff; flex:1;
}
.league-range{
  font-family:"Nunito", sans-serif; font-size:12px; font-weight:700; color:rgba(255,255,255,.6);
}

/* ── Duel: scale-type checkboxes ── */
.duel-types{
  width:100%; display:grid; grid-template-columns:1fr 1fr; gap:8px; margin-top:12px;
}
/* Difficulty picker (no default — player must choose) */
.duel-diff-label{ margin:16px 0 6px; font-weight:700; }
.duel-diff{ width:100%; display:grid; grid-template-columns:1fr 1fr; gap:8px; }
.duel-diff-btn{
  display:flex; flex-direction:column; align-items:center; gap:2px; padding:10px 12px;
  border-radius:13px; cursor:pointer; text-align:center;
  background:rgba(255,255,255,.06); border:1.5px solid rgba(255,255,255,.12);
  transition:transform .12s ease, background .15s ease, border-color .15s ease;
}
.duel-diff-btn:active{ transform:scale(.97); }
.duel-diff-btn.on{ background:rgba(91,196,255,.18); border-color:#5bc4ff; }
.ddb-title{ font-family:"Fredoka",sans-serif; font-size:15px; font-weight:700; color:#fff; }
.ddb-sub{ font-family:"Nunito",sans-serif; font-size:11.5px; font-weight:600; color:rgba(255,255,255,.6); }
.duel-diff-btn.on .ddb-sub{ color:rgba(191,230,255,.85); }
.duel-chk{
  display:flex; align-items:center; gap:8px; min-height:46px; padding:8px 12px;
  border-radius:13px; cursor:pointer;
  background:rgba(255,255,255,.06); border:1.5px solid rgba(255,255,255,.12);
  color:#fff; font-family:"Fredoka", sans-serif; font-size:15px; font-weight:600;
  transition:transform .12s ease, background .15s ease, border-color .15s ease;
}
.duel-chk:active{ transform:scale(.96); }
.duel-chk.on{ background:rgba(91,196,255,.18); border-color:#5bc4ff; }
.duel-chk.soon{ opacity:.5; cursor:default; }
.chk-box{
  width:22px; height:22px; flex-shrink:0; border-radius:6px;
  border:1.5px solid rgba(255,255,255,.4);
  display:flex; align-items:center; justify-content:center;
  font-size:14px; color:#5bc4ff;
}
.duel-chk.on .chk-box{ background:#5bc4ff; color:#0a0618; border-color:#5bc4ff; }
.chk-label{ flex:1; }
.chk-soon{
  font-size:10px; font-weight:700; color:#ffb15b;
  border:1px solid #ffb15b; border-radius:6px; padding:1px 5px;
}

/* ── Duel record, above the league ladder ────────────────────────────────────
   Three numbers on one row rather than a card each: this screen already has a ladder of seven
   leagues under it and is short and wide, so the record has to earn its height. */
.duel-record{
  display:flex; justify-content:center; gap:5cqw;
  margin:0 0 1.6cqh; padding:8px 16px;
  background:rgba(255,255,255,.05);
  border:1px solid rgba(255,255,255,.12); border-radius:14px;
}
.dr-cell{ display:flex; flex-direction:column; align-items:center; line-height:1.15; }
.dr-cell b{
  font-family:"Fredoka",sans-serif; font-size:clamp(17px,4.4cqh,22px); font-weight:700;
  color:#fff; font-variant-numeric:tabular-nums;
}
.dr-cell span{
  font-family:"Nunito",sans-serif; font-size:min(2.2cqh,11.5px); font-weight:700;
  color:rgba(255,255,255,.62); letter-spacing:.3px;
}
.dr-win { color:#7bd47b; }
.dr-loss{ color:#ff8a9b; }

/* ── Duel Setup: fit the whole screen, never scroll ──────────────────────────
   A 16:9 viewport is short and wide. Stacked in one column this screen ran 628px tall inside a
   390px phone-landscape viewport (238px of scroll). Two columns use the width instead, and every
   vertical margin here is trimmed rather than letting anything overlap. */
.settings-content--fit{ overflow:hidden; justify-content:center; }
/* This screen was already 3px taller than its box, clipped invisibly at both ends by the
   overflow:hidden that keeps it from scrolling. That was survivable until the container started
   reserving room for the home indicator — the reserved space comes out of the same budget, so a
   3px overflow becomes a 24px one and eats the title. The rhythm is trimmed to pay for it. */
.duel-setup .settings-title{ margin:0 0 1.2cqh; font-size:clamp(20px, 5.6cqh, 32px); }
.duel-setup .scale-sub{ margin:0 0 5px; font-weight:700; }

.duel-setup-cols{
  width:100%; max-width:660px; margin-inline:auto;
  display:grid; grid-template-columns:1fr 1fr; gap:1.4cqh 16px; align-items:start;
}
/* .settings-card is content-box, so its padding was pushing it 26px past its grid track and into
   the next column. Border-box keeps every child inside its own column. */
.duel-setup-cols, .duel-setup-cols *{ box-sizing:border-box; }
.duel-col{ display:flex; flex-direction:column; min-width:0; }
.duel-col + .duel-col{ align-self:stretch; }
.duel-setup .duel-col > .scale-sub + *{ margin-bottom:1.6cqh; }

/* From / To side by side instead of stacked — halves this card's height. Shared by Duel Setup and
   the solo Range Select, so both range screens stay identical and neither needs to scroll. */
.settings-card.range-card--row{
  display:flex; flex-wrap:wrap; gap:12px; padding:10px 12px; border-radius:14px; margin:0;
  box-sizing:border-box;
}
/* basis 0, NOT a px basis: inside the duel's ~272px column a 130px basis made the two fields wrap
   onto separate lines and pushed that screen back into scrolling. They must always share one line;
   only the hint (flex-basis 100%, below) is allowed to wrap. */
.range-card--row .range-row{
  flex:1 1 0; min-width:0; flex-direction:column; align-items:flex-start;
  gap:4px; padding:0; border:none; margin:0;
}
.range-card--row .range-label{ font-size:13px; color:rgba(255,255,255,.7); }
.range-card--row .range-picker{ width:100%; gap:6px; }
.range-card--row .range-sel{ flex:1; min-height:38px; padding:6px 8px; font-size:15px; }
.range-card--row .range-sel--oct{ flex:0 0 52px; }
/* Solo Range Select: the card is the whole screen here, so keep it from stretching, and let the
   hint sit on its own line under both pickers. */
.range-select .settings-card.range-card--row{ max-width:440px; margin-inline:auto; }
.range-select .range-card--row .range-hint{ flex:1 0 100%; margin:2px 0 0; }
.range-select .settings-title{ margin-bottom:6px; }
/* The global .scale-sub carries margin-top:-6%, a hack tuned to the ORIGINAL 22px title margin.
   Trimming the title above made that negative margin pull the subtitle up into the title, so it
   has to be neutralised here (the duel setup needs the same override, for the same reason). */
.range-select .scale-sub{ margin:0 0 10px; }

.duel-setup .duel-types{ margin-top:0; gap:6px; }
.duel-setup .duel-chk{ min-height:36px; padding:5px 9px; font-size:13.5px; gap:6px; }
.duel-setup .chk-box{ width:18px; height:18px; font-size:12px; }
.duel-setup .duel-diff{ gap:6px; }
.duel-setup .duel-diff-btn{ padding:6px 8px; }
.duel-setup .ddb-title{ font-size:13.5px; }
.duel-setup .ddb-sub{ font-size:10.5px; }
.duel-setup .range-hint{ margin:.8cqh 0 0; }
/* Trimmed from 1.6cqh so the safe-area padding added to .settings-content buys clearance rather
   than pushing this screen back into the scrolling it was carefully tuned out of. */
.duel-setup .play{ margin-top:1cqh; padding:8px 22px 9px; }

/* Narrow screens (portrait phone): one column again, still no scrolling. */
@container game (max-aspect-ratio: 1/1){
  .duel-setup-cols{ grid-template-columns:1fr; }
}

/* ── Portrait gate ───────────────────────────────────────────────────────────
   Hidden by default and shown ONLY on a portrait touch device. Gating on
   `pointer: coarse` matters: without it, a desktop user with a tall narrow window would be told
   to rotate a phone they are not holding. Tablets in portrait get it too, correctly — they letterbox
   just as badly. */
#rotate-gate{ display:none; }
@media (orientation: portrait) and (pointer: coarse){
  #rotate-gate{
    display:flex; position:fixed; inset:0; z-index:9999;
    align-items:center; justify-content:center; padding:28px;
    background:linear-gradient(170deg, #1b0f3a 0%, #2a1456 55%, #4a1d52 100%);
    color:#fff; text-align:center;
  }
  /* Stop the letterboxed strip from peeking around the gate */
  body{ overflow:hidden; }
}
.rotate-card{ max-width:340px; display:flex; flex-direction:column; align-items:center; gap:6px; }
.rotate-icon{
  color:#ffd24a; margin-bottom:6px;
  filter:drop-shadow(0 0 14px rgba(255,210,74,.35));
  animation:rotateHint 2.6s ease-in-out infinite;
}
@keyframes rotateHint{
  0%, 55%, 100% { transform:rotate(0deg); }
  72%,  92%     { transform:rotate(-90deg); }
}
.rotate-title{
  font-family:"Fredoka", sans-serif; font-size:22px; font-weight:700; margin:0; color:#fff;
  text-wrap:balance;
}
.rotate-text{
  font-family:"Nunito", sans-serif; font-size:15px; font-weight:600; line-height:1.5;
  color:rgba(255,255,255,.82); margin:0;
}
.rotate-hint{
  font-family:"Nunito", sans-serif; font-size:12.5px; font-weight:500; line-height:1.45;
  color:rgba(255,255,255,.55); margin:10px 0 0;
}
/* ── Reduced motion ──────────────────────────────────────────────────────────
   For players with a vestibular disorder, migraine, epilepsy or motion sensitivity, continuous
   looping movement is not a preference — it causes real nausea. This used to cover exactly one
   icon; there are 21 keyframe animations in this file.

   Deliberately NOT the usual blanket `*{animation:none}`. Some of these loops carry information a
   player needs — degFlash is the duel's "sing this degree" cue, confirmPulse is a countdown running
   out, birdHit and revivePulse are feedback on something that just happened. Killing those would
   trade a health problem for an unplayable game.

   So the split is by ROLE, not by syntax: ambient decoration that loops forever and says nothing
   stops; anything that communicates state keeps moving. Gameplay motion itself (bird, ball,
   pillars) is rAF-driven, not CSS, and is untouched here — it IS the game. */
@media (prefers-reduced-motion: reduce){
  /* Ambient decoration — pure atmosphere, no information. */
  .stars i,             /* twinkling background stars */
  .note,                /* music notes drifting up the sky */
  .waveform .bar,       /* idle waveform bars */
  .wf-dynamic .bar,     /* live waveform bars */
  .bird-wrap,           /* menu bird bobbing */
  .bird-game,           /* in-game bird's idle hover (position is rAF-driven, not this) */
  .bird-game::after,    /* glow pulsing around it */
  .play::before,        /* halo breathing behind the PLAY button */
  .g-target,            /* target sphere pulsing */
  .rotate-icon{
    animation:none !important;
  }
  /* Screen transitions: land on the final state instead of sliding into it. */
  .mode-fade{ animation:none !important; }
  /* Everything else keeps its motion but stops repeating — one pass says what it needs to say. */
  *, *::before, *::after{ animation-iteration-count:1 !important; }
}

/* Withdraw-consent link: readable, but deliberately not styled like a primary action. */
.settings-link--warn{ color:#ffb15b; }

/* "no live opponent yet" — sits under the opponent name, quiet but legible. */
.duel-bot-tag{
  position:absolute; top:78px; left:calc(50% - 8px); transform:translateX(-50%); z-index:22;
  font-family:"Nunito",sans-serif; font-size:9.5px; font-weight:700; letter-spacing:.3px;
  color:rgba(255,255,255,.45); white-space:nowrap;
}

/* Leaderboard: subtitle + the honest empty state that replaced the invented players. */
.lb-sub{
  font-family:"Nunito",sans-serif; font-size:12px; font-weight:600; text-align:center;
  color:rgba(255,255,255,.55); margin:-8px 0 12px; max-width:420px;
}
.lb-empty{
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  flex:1; min-height:0; text-align:center; gap:4px; padding:12px;
}
.lb-empty-icon{ font-size:34px; line-height:1; opacity:.75; margin-bottom:2px; }
.lb-empty-title{
  font-family:"Fredoka",sans-serif; font-size:17px; font-weight:600; color:#fff;
}
/* Your own past runs. No medals, no rank column — this is history, not a ranking. */
.lb-runs-head{
  font-family:"Fredoka",sans-serif; font-size:11.5px; font-weight:600; letter-spacing:.4px;
  text-transform:uppercase; color:rgba(255,255,255,.42);
  margin:14px 0 2px; padding-inline:4px;
}
.lb-run{
  display:flex; align-items:center; gap:10px; box-sizing:border-box;
  padding:2px 12px; min-height:min(5.6cqh, 32px); flex:0 0 auto;
  border-bottom:1px solid rgba(255,255,255,.06);
}
.lb-run-n{
  min-width:30px; text-align:center; line-height:1.2;
  font-family:"Nunito",sans-serif; font-size:12px; font-weight:700; color:rgba(255,255,255,.35);
}
.lb-run-score{
  flex:1; text-align:right; line-height:1.2;
  font-family:"Fredoka",sans-serif; font-size:14px; font-weight:600; color:rgba(255,255,255,.7);
}

.lb-empty-text{
  font-family:"Nunito",sans-serif; font-size:13px; font-weight:500;
  color:rgba(255,255,255,.6); max-width:280px;
}

/* ── Microphone failure overlay (solo + duel) ────────────────────────────────
   Sits above everything, freezes play, and says what to actually do about it. */
.mic-error{
  position:absolute; inset:0; z-index:80;
  display:flex; align-items:center; justify-content:center; padding:16px;
  background:rgba(8,4,20,.82);
}
.mic-error-card{
  width:100%; max-width:520px; box-sizing:border-box;
  background:rgba(30,20,58,.96);
  border:1px solid rgba(255,255,255,.16); border-radius:18px;
  padding:12px 20px 14px; text-align:center;
  box-shadow:0 18px 50px rgba(0,0,0,.6);
  max-height:100%; overflow:hidden auto;
}
.mic-error-icon{ font-size:24px; line-height:1; margin-bottom:2px; }
.mic-error-title{
  font-family:"Fredoka",sans-serif; font-size:18px; font-weight:700; color:#ffd24a;
  margin:0 0 5px;
}
.mic-error-body{
  font-family:"Nunito",sans-serif; font-size:13.5px; font-weight:600; line-height:1.45;
  color:rgba(255,255,255,.86); margin:0 0 6px;
}
.mic-error-hint{
  font-family:"Nunito",sans-serif; font-size:12px; font-weight:500; line-height:1.45;
  color:rgba(255,255,255,.62); margin:0 0 11px;
}
.mic-error-actions{ display:flex; align-items:center; justify-content:center; gap:14px; }
/* the base .play is a big 66%-wide pill — here it must be a compact, non-wrapping button */
.mic-error-actions .play{
  width:auto; min-width:0; max-width:none; padding:9px 26px 11px; white-space:nowrap;
}

/* ── Duel: roulette ── */
/* ── Wheel of fortune (duel mode picker) ── */
.wheel-wrap{
  position:relative; width:min(52cqh, 46cqw, 300px); aspect-ratio:1/1;
  margin:12px auto 10px; flex:0 0 auto;
}
.wheel-rotor{
  position:absolute; inset:0; border-radius:50%;
  border:5px solid #ffd24a;
  box-shadow:0 0 0 3px rgba(0,0,0,.5), 0 8px 26px rgba(0,0,0,.5), inset 0 0 30px rgba(0,0,0,.35);
  transition:transform 4.2s cubic-bezier(.13,.72,.16,1);   /* long decelerating spin */
}
.wheel-lbl{
  position:absolute; inset:0; display:flex; justify-content:center;
  pointer-events:none;
}
.wheel-lbl span{
  margin-top:7%; max-width:34%; text-align:center;
  font-family:"Fredoka", sans-serif; font-weight:700; line-height:1;
  color:#fff; text-shadow:0 1px 2px rgba(0,0,0,.6);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.wheel-pointer{
  position:absolute; top:-9px; left:50%; transform:translateX(-50%); z-index:5;
  width:0; height:0; border-left:12px solid transparent; border-right:12px solid transparent;
  border-top:20px solid #fff; filter:drop-shadow(0 2px 3px rgba(0,0,0,.6));
}
.wheel-hub{
  position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); z-index:4;
  width:20%; height:20%; border-radius:50%;
  background:radial-gradient(circle at 40% 35%, #fff6c8, #ffd24a 60%, #e0921a);
  border:3px solid #fff; box-shadow:0 2px 8px rgba(0,0,0,.5);
  display:flex; align-items:center; justify-content:center; font-size:clamp(14px,4cqh,22px);
}
.wheel-result{ color:#ffd24a; font-weight:700; }

/* Confirm countdown after the wheel lands */
.duel-confirm-hint{
  margin-top:10px; text-align:center;
  font-family:"Nunito", sans-serif; font-size:12px; font-weight:700;
  color:rgba(255,255,255,.55); letter-spacing:.3px;
}
.duel-confirm-hint.urgent{ color:#ff5a6a; animation:confirmPulse .6s ease infinite; }
@keyframes confirmPulse{ 0%,100%{opacity:1;} 50%{opacity:.45;} }

/* "Time's up" technical-loss flash */
.duel-timeout{ margin-top:18px; text-align:center; animation:timeoutIn .3s ease; }
.duel-timeout__title{
  font-family:"Bowlby One", sans-serif; font-size:26px; color:#ff5a6a;
  text-shadow:0 2px 10px rgba(255,40,60,.5); margin-bottom:4px;
}
@keyframes timeoutIn{ 0%{transform:scale(.8); opacity:0;} 100%{transform:scale(1); opacity:1;} }

/* ── Sing Duel arena (4a static split-screen) ── */
.duel-arena{ position:absolute; inset:0; display:flex; flex-direction:row-reverse; z-index:10; }
.duel-arena::before{
  content:''; position:absolute; left:50%; top:0; bottom:0; width:2px;
  background:linear-gradient(180deg, transparent, rgba(255,255,255,.18), transparent); z-index:5;
}
.duel-side{ position:relative; flex:1; height:100%; overflow:hidden; }
/* Opponent (right half) tuner mirrored to the right (outer) edge; player keeps the default left edge */
.duel-side--opp .tuner-bar{ left:auto; right:0; border-right:none; border-left:1px solid rgba(255,255,255,.12); }
.duel-side--opp .tuner-note{ right:auto; left:5px; }

/* Duel only: pull every degree back except the two that mean something right now — the note I have
   to sing (.flash) and the note I am actually singing (.active). In solo the pillars themselves say
   where to go, so the full ladder is useful context; in the duel it competes with a ball, a paddle,
   two birds and a target band, and a degree that is neither target nor current pitch is noise at
   exactly the moment I need to read fastest.

   The first version did this with opacity:.34 — and opacity MULTIPLIES with the colour's own alpha,
   the trap this project has now hit twice. .42 × .34 = .143, which measured 1.50:1. Those labels
   were not de-emphasised, they were erased; asked whether the dimming looked right, the answer was
   "I could not see anything", which was literally true.
   Emphasis now comes from making the target LOUDER rather than the rest invisible: the ladder stays
   at its ordinary readable weight, and the two notes that matter get colour, glow and size. Nothing
   here drops below the contrast floor. */
.duel-side--me .tuner-note{
  transition:opacity .14s ease, color .12s ease, text-shadow .12s ease, transform .12s ease;
}
.duel-side--me .tuner-note.flash,
.duel-side--me .tuner-note.active{
  transform:translateY(-50%) scale(1.25);
  font-weight:800;
}

/* Same on-screen size as the solo bird (.bird-game = min(8.96% of a 16:9 width, 44px), and
   8.96 × 16/9 = 15.9cqh). At 34px the duel bird was half solo's size, so identical movement read
   ~2x more agitated — these two numbers must move together or that comes straight back. */
.duel-bird{ position:absolute; width:min(15.9cqh, 44px); height:min(15.9cqh, 44px); top:50%; z-index:20; transform:translateY(-50%); }
.duel-bird--me{ left:46px; transform:translateY(-50%) scaleX(-1); }   /* just off the tuner (tiny gap) */
.duel-bird--opp{ right:46px; }

.duel-hearts{
  position:absolute; top:42px; left:50%; transform:translateX(-50%); z-index:22;
  color:#ff6b8a; font-size:13px; letter-spacing:2px; white-space:nowrap;
  filter:drop-shadow(0 0 5px rgba(255,80,120,.5));
}
/* nudge each side's cluster toward its own tuner so it clears the center badge & exit button */
.duel-side--opp .duel-hearts{ left:calc(50% - 8px); }
.duel-side--me  .duel-hearts{ left:calc(50% + 8px); }
.duel-name{
  position:absolute; top:62px; left:50%; transform:translateX(-50%); z-index:22;
  font-family:"Fredoka", sans-serif; font-size:11px; font-weight:600;
  color:rgba(255,255,255,.6); letter-spacing:.5px; white-space:nowrap;
}
.duel-side--opp .duel-name{ left:calc(50% - 8px); }
.duel-side--me  .duel-name{ left:calc(50% + 8px); }
.duel-warm{
  position:absolute; top:80px; left:50%; transform:translateX(-50%); z-index:22;
  font-family:"Fredoka", sans-serif; font-size:11px; font-weight:700;
  color:#ffb84a; white-space:nowrap; text-shadow:0 1px 3px rgba(0,0,0,.5);
  animation:confirmPulse 1s ease infinite;
}
.duel-side--opp .duel-warm{ left:calc(50% - 8px); }
.duel-side--me  .duel-warm{ left:calc(50% + 8px); }

.duel-ball{
  position:absolute; left:50%; top:50%; width:18px; height:18px;
  transform:translate(-50%,-50%); border-radius:50%; z-index:30;
  background:radial-gradient(circle at 35% 30%, rgba(255,255,255,.95), rgba(170,215,255,.35));
  border:1.5px solid rgba(255,255,255,.55);
  box-shadow:0 0 14px rgba(150,200,255,.6);
}
.duel-scale-badge{
  position:absolute; top:8px; left:50%; transform:translateX(-50%); z-index:31;
  font-family:"Fredoka", sans-serif; font-size:12px; font-weight:600; color:#ffd24a;
  background:rgba(10,6,24,.6); padding:3px 12px; border-radius:999px;
  border:1px solid rgba(255,210,74,.4); white-space:nowrap;
}

/* ── Duel 4b: only the BOT bird glides between landing notes. The player's bird is
   RAF-driven from live pitch (no transition) so it tracks the voice exactly like solo. ── */
.duel-bird--opp{ transition: top 1.25s ease; }

/* The target band — the duel's answer to solo's pillar gap. Its height is set from GAP_H in JS,
   so the window you SEE is exactly the window you are judged by. */
.duel-target-band{
  position:absolute; left:0; right:0; z-index:6; pointer-events:none;
  background:linear-gradient(90deg, rgba(255,210,74,.22), rgba(255,154,62,.09) 65%, transparent);
  border-top:1px solid rgba(255,210,74,.5);
  border-bottom:1px solid rgba(255,210,74,.5);
  opacity:0; transition:opacity .16s ease, top .16s ease, background .12s ease, border-color .12s ease;
}
.duel-target-band.on{ opacity:1; }
/* lights up the moment the bird is actually inside — instant, honest feedback */
.duel-target-band.on.inside{
  background:linear-gradient(90deg, rgba(0,206,209,.30), rgba(0,206,209,.12) 65%, transparent);
  border-top-color:rgba(0,230,233,.85); border-bottom-color:rgba(0,230,233,.85);
}

/* ── Duel 4c: flashing target degree, red target, miss flash, end overlay ── */
.tuner-note.flash{
  color:#fff; font-weight:800;
  text-shadow:0 0 6px #ffd24a, 0 0 12px #ff9a3e;
  animation:degFlash .5s ease-in-out infinite;
}
@keyframes degFlash{ 0%,100%{ opacity:.5; transform:translateY(-50%) scale(1);} 50%{ opacity:1; transform:translateY(-50%) scale(1.25);} }

.duel-paddle{
  /* Sized as a % of the bird (was -3/5/16/22px against a 34px bird) so it keeps exactly the same
     proportions and placement now that the bird matches the solo bird's size. */
  position:absolute; left:-8.8%; top:14.7%; width:47%; height:64.7%; z-index:4;
  transform:rotate(-24deg); transform-origin:bottom center;
  filter:drop-shadow(0 2px 3px rgba(0,0,0,.4)); pointer-events:none;
}
.duel-paddle svg{ width:100%; height:100%; display:block; }
.duel-paddle.swing{ animation:paddleSwing .3s ease; }
@keyframes paddleSwing{ 0%{transform:rotate(-24deg);} 45%{transform:rotate(20deg) scale(1.1);} 100%{transform:rotate(-24deg);} }

.duel-side.miss::after{
  content:''; position:absolute; inset:0; z-index:40; pointer-events:none;
  background:radial-gradient(circle at 50% 50%, rgba(255,40,40,.38), transparent 72%);
  animation:missFlash .3s ease;
}
@keyframes missFlash{ 0%{opacity:1;} 100%{opacity:0;} }

.duel-over{
  position:absolute; inset:0; z-index:60;
  display:flex; flex-direction:column; align-items:center; justify-content:center; gap:20px;
  background:rgba(0,0,0,.62);
}
.duel-over__title{
  font-family:"Bowlby One", sans-serif; font-size:clamp(30px,9cqh,46px); letter-spacing:1px;
  background:linear-gradient(180deg, #ffb0b0 0%, #ff4444 55%, #cc0000 100%);
  -webkit-background-clip:text; background-clip:text; color:transparent;
  filter:drop-shadow(0 2px 8px rgba(255,80,80,.5));
}

/* ── Duel 4e: pot chip + win styling ── */
.duel-pot{
  position:absolute; top:8px; left:10px; z-index:31;
  font-family:"Fredoka", sans-serif; font-size:11px; font-weight:700; color:#ffd24a;
  background:rgba(10,6,24,.6); padding:3px 10px; border-radius:999px;
  border:1px solid rgba(255,210,74,.4); white-space:nowrap;
}
.duel-over__title.win{
  background:linear-gradient(180deg, #fff6c8 0%, #ffd24a 50%, #e76a14 100%);
  -webkit-background-clip:text; background-clip:text; color:transparent;
  filter:drop-shadow(0 2px 8px rgba(255,210,74,.5));
}
.duel-over__pot{
  font-family:"Bowlby One", sans-serif; font-size:clamp(22px,7cqh,32px); margin-top:-8px;
  color:#ffd24a; filter:drop-shadow(0 2px 6px rgba(0,0,0,.5));
}

.engine-why{ color:#ff9aa5; font-weight:700; font-size:12px; }

/* Game over: why a score is not on the global board. Shown only when there is a reason — an
   offline run keeps its score here and never reaches the public ranking, and staying silent about
   that is indistinguishable from the board being broken. */
.gameover-note{
  font-family:"Nunito",sans-serif; font-size:min(2.2cqh,12px); font-weight:700; line-height:1.4;
  color:#ffb84a; margin:2px 0 6px; max-width:320px; text-align:center;
}

/* The optional analytics box, visibly separate from the age confirmation it sits under. Consent
   bundled into the thing people press to get past a screen is not freely given, and the layout has
   to say that as clearly as the wording does. */
.consent-age--opt{
  margin-top:6px; align-items:flex-start;
  background:rgba(255,255,255,.04);
  border:1px solid rgba(255,255,255,.10); border-radius:12px; padding:7px 10px;
}
.consent-age--opt span{ color:rgba(255,255,255,.72); }
.consent-age--opt b{ color:#ffd24a; }
/* This screen was tuned to fit exactly, and a whole extra block pushed it 96px past its box — far
   enough that the Agree button sat below the fold on the shortest phones. It scrolls rather than
   clipping, so nothing was hidden, but a welcome screen that arrives already scrolled is a welcome
   screen people bounce off. Compacted at short heights instead of everywhere. */
@container game (max-height: 360px){
  .consent-card .consent-lead{ margin-bottom:6px; }
  .consent-list{ gap:5px; margin-bottom:6px; }
  .consent-list li{ padding:5px 8px; line-height:1.3; }
  .consent-age{ margin-top:4px; }
  .consent-age--opt{ margin-top:4px; padding:5px 8px; line-height:1.3; }
  .consent-links{ margin:5px 0; }
}

/* The league badge, moved onto the trophy button. It belongs beside the trophy count — both are
   duel currency — and NOT on the button that opens the solo leaderboard, where a Bronze medal read
   as "third place" to somebody who was first. */
.tc-league-badge{ font-size:.8em; margin-left:3px; vertical-align:middle; }

/* The scale a leaderboard score was earned on, under the name. Quiet on purpose — it is context
   for the number beside it, not a second thing to read. */
.lb-scale{
  display:block;
  font-family:"Nunito",sans-serif; font-size:min(2cqh,10.5px); font-weight:700;
  color:#ffd24a; letter-spacing:.2px; margin-top:1px;
}
