/* Interval Bingo — Beat Kitchen brand palette (teal/blue, Roboto) */
:root{
  --bg:#0d2027; --panel:#123039; --panel2:#17414c; --line:#255a66;
  --ink:#eef4f5; --dim:#9fb6bd;
  --accent:#2dacb3;        /* BKS teal */
  --accent-ink:#04262b;
  --accent2:#7fd6db;       /* light teal */
  --blue:#124e68;          /* deep brand blue */
  --good:#3ecf7a; --bad:#ff6b6b; --free:#255a66;
}
*{box-sizing:border-box}
/* behave like a native app on iOS: no long-press magnifier / callout, no accidental text selection (everywhere) */
*{-webkit-touch-callout:none !important;-webkit-user-select:none;user-select:none}
svg,svg *,img{-webkit-touch-callout:none !important;-webkit-user-select:none !important;pointer-events:auto}
html{-webkit-text-size-adjust:100%;text-size-adjust:100%;background:var(--bg);scrollbar-gutter:stable}   /* 🩹 (Nathan) the board "shipping right + white column behind it" = a scrollbar toggling (modal/reflow) shifted content and the UNSTYLED html showed white. Dark html bg kills the white flash; stable gutter reserves the scrollbar so content never shifts. */
body{margin:0;color-scheme:dark;font-family:"Roboto",-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
     background:var(--bg);color:var(--ink);-webkit-font-smoothing:antialiased;
     touch-action:manipulation;-webkit-tap-highlight-color:transparent}
/* ⚠️ `overscroll-behavior:none` USED TO BE ON body, AND IT COVERS THE BOTTOM EDGE.
   In a full-screen WKWebView with `web.scrollView.bounces = false` (ContentView.swift:47) that
   combination makes the page absorb an upward drag at the bottom of the screen and never yield it —
   which is the iOS home / app-switcher gesture. The synth hit the UIKit version of this exact bug
   (562fbead, 2026-08-06: preferredScreenEdgesDeferringSystemGestures returning UIRectEdgeAll claimed
   the bottom edge and removed the only way out of the app, force-quit included). Ours is the web-layer
   analogue and produces the same two symptoms Nathan reports: a scroll that stalls and then catches
   up, and a home swipe that does nothing — which reads as a dead device because you cannot leave.
   Restore the old behavior with ?overscroll=lock to A/B it on the device. */
body.overscroll-lock{overscroll-behavior:none}
/* 🎨 MODE BACKGROUNDS — each game type wears a BOLD, unmistakable signature so you know at a glance what you're in
   (Nathan: make the colors distinctive). Every mode sets --gm (its accent); the wash, the pinned bars, and a top
   glow all pull from it, so the whole screen shifts color per game. Applies in free-play AND the tournament. */
body[data-gm]{transition:background .5s ease}
/* solo = GREEN */
body[data-gm="solo"]{background:radial-gradient(120% 78% at 50% -14%, rgba(34,192,122,.5) 0%, transparent 46%), linear-gradient(180deg, #0d4a34 0%, var(--bg) 42%)}
body[data-gm="solo"] .brandbar{background:#0d4a34;border-bottom:2px solid rgba(34,192,122,.6)}
body[data-gm="solo"] .gridmsg{background:#0d4a34}
/* daily quiz = BLUE */
body[data-gm="daily"]{background:radial-gradient(120% 78% at 50% -14%, rgba(63,160,230,.5) 0%, transparent 46%), linear-gradient(180deg, #123a58 0%, var(--bg) 42%)}
body[data-gm="daily"] .brandbar{background:#123a58;border-bottom:2px solid rgba(63,160,230,.6)}
body[data-gm="daily"] .gridmsg{background:#123a58}
/* classic chorDLE = PURPLE */
body[data-gm="count"]{background:radial-gradient(120% 78% at 50% -14%, rgba(168,119,238,.5) 0%, transparent 46%), linear-gradient(180deg, #2f2150 0%, var(--bg) 42%)}
body[data-gm="count"] .brandbar{background:#2f2150;border-bottom:2px solid rgba(168,119,238,.6)}
body[data-gm="count"] .gridmsg{background:#2f2150}
/* collectible = ORANGE */
body[data-gm="named"]{background:radial-gradient(120% 78% at 50% -14%, rgba(240,151,63,.5) 0%, transparent 46%), linear-gradient(180deg, #4a3016 0%, var(--bg) 42%)}
body[data-gm="named"] .brandbar{background:#4a3016;border-bottom:2px solid rgba(240,151,63,.6)}
body[data-gm="named"] .gridmsg{background:#4a3016}
/* Daily Chord = PINK JEWEL (the most special) */
body[data-gm="cotd"]{background:radial-gradient(126% 88% at 50% -14%, rgba(240,106,168,.62) 0%, transparent 48%), linear-gradient(180deg, #4a1730 0%, #2a1220 34%, var(--bg) 64%)}
body[data-gm="cotd"] .brandbar{background:#4a1730;border-bottom:2px solid rgba(240,106,168,.6)}
body[data-gm="cotd"] .gridmsg{background:#4a1730}
/* let text in the events banner still be selectable if needed */
.ib-events,.ib-events *{-webkit-user-select:text;user-select:text}

/* brand header — always clear the notch / Dynamic Island (env is 0 on non-notch devices) */
.brandbar{display:flex;align-items:center;gap:12px;max-width:1140px;margin:0 auto;
  position:sticky;top:0;z-index:20;background:var(--bg);   /* PINNED top bar — brand/back + nav stay put while the board scrolls under it */
  /* status bar is hidden in-game, so most of the inset is wasted. Logo + buttons live BESIDE the centered
     cutout, so they can ride up near the top; we keep ~28px less than the full inset. Tunable if it clips. */
  padding:calc(env(safe-area-inset-top, 0px) + 4px) max(11px, env(safe-area-inset-right, 0px) + 6px) 8px max(11px, env(safe-area-inset-left, 0px) + 6px)}   /* sit BELOW the notch + restored status bar on EVERY screen (Daily Quiz / Classic used the stale "status bar hidden → ride up" value → menu peeked behind the cutout — Nathan). Side inset trimmed 20→11px so the ⚡pill+› and ⚙ both fit on a 375px phone; landscape notch still respected via the safe-area max(). */

/* web-only site nav — shown only in a normal browser tab (body.web); mirrors the main site header so the
   game reads as part of the site. Hidden in the iOS app + installed PWA (standalone). White like the real header. */
.site-nav{display:none}
body.web .site-nav{display:flex;align-items:center;gap:16px;flex-wrap:nowrap;
  position:sticky;top:0;z-index:30;box-sizing:border-box;height:44px;
  background:#fff;border-bottom:1px solid #e6e6e6;padding:0 16px}
body.web .site-nav .sn-logo{display:flex;align-items:center;flex:0 0 auto}
body.web .site-nav .sn-logo img{height:24px;width:auto;display:block}
body.web .site-nav .sn-links{display:flex;align-items:center;gap:18px;flex:1 1 auto;min-width:0;
  overflow-x:auto;white-space:nowrap;-webkit-overflow-scrolling:touch;scrollbar-width:none}
body.web .site-nav .sn-links::-webkit-scrollbar{display:none}
body.web .site-nav .sn-links a{color:#111;text-decoration:none;font-size:14px;font-weight:500;
  padding:2px 0;border-bottom:2px solid transparent;transition:color .15s,border-color .15s}
body.web .site-nav .sn-links a:hover{color:var(--accent)}
body.web .site-nav .sn-links a.active{color:#111;border-bottom-color:var(--accent)}
/* Sticks below the site-nav. The 44px was HARDCODED, so any nav taller than that — an iOS landscape
   safe-area inset, a wrapped link row, a bigger logo — left the brandbar pinned underneath it and the
   two headers collided. --navh is measured from the real element at load, on resize and on orientation
   change; 44px stays as the fallback for when JS has not run yet. (Nathan, on iPad in landscape.) */
body.web .brandbar{top:var(--navh,44px)}
.brandbar img{height:30px;width:auto;display:block}
/* 🅱️ BRANDBAR v2 (Nathan): logo+⚡points stacked into a square top-left by the notch; online = inset pill; nav below */
/* v3 HEADER (Nathan) — bigger centered wordmark; icons + points/online badges all ride on ONE line, 📈🏆+pts far LEFT, online+⚙ far RIGHT. */
.brandbar-v3{align-items:center;justify-content:space-between;gap:6px}
.brandbar-v3 .bb-side{display:flex;align-items:center;gap:7px;flex:1 1 0;min-width:0}
.brandbar-v3 .bb-left{justify-content:flex-start}
.brandbar-v3 .bb-right{justify-content:flex-end}
.brandbar-v3 .navbtn{width:32px;height:32px;font-size:16px}   /* a few px smaller (Nathan) */
.brandbar-v3 .bb-logo{display:flex;align-items:center;justify-content:center;flex:0 0 auto;margin-top:-3px}   /* wordmark nudged a touch higher */
.brandbar-v3 .bb-logo .brand-mark{height:34px;width:34px;display:block;color:var(--ink);opacity:.9}
/* The keyboard is structure, the puzzle piece is the brand — so the piece takes the accent and the
   keys stay quiet. One colored element, not two competing ones. */
/* Line drawing: the keyboard is the quiet structure, the piece keeps the accent so the mark still
   reads as chorDLE's at a glance. Fills are only the black keys — everything else is outline. */
.brand-mark .bm-piece{stroke:var(--accent);fill:none;stroke-width:1.7}
.brand-mark .bm-kb{opacity:.8}

body.at-menu .brandbar.brandbar-v3 .bb-logo .brand-mark{display:block}   /* beat the generic at-menu brand-mark HIDE (line ~95) — v3 logo lives IN the header, not the hero */
.brandbar-v3 .bb-side .navpoints{position:static;transform:none;left:auto;right:auto;bottom:auto;font-weight:800;font-size:13px;line-height:1;color:#e8c14a;letter-spacing:.02em;white-space:nowrap;text-shadow:none}   /* points is an INLINE badge; MUST reset the transform/left/bottom that leak from the ".brandbar .navpoints" absolute rule below (line ~1521) — they were shoving ⚡ out the left of the pill + leaving dead space right (Nathan) */
/* Online = a BARE dot + number, no pill (Nathan: "the online button doesn't need a pill, could be smaller"). */
.brandbar-v3 .navonline{display:inline-flex;align-items:baseline;gap:4px;line-height:1;background:none;border:0;padding:0;margin:0}

/* ── STATUS STRIP, not a secondary nav (Nathan) ────────────────────────────────────────────────
   In the app there is no site nav above this bar, and inside the /chordle/ iframe the game's own
   site-nav is hidden too — so in BOTH real contexts this strip is the top of the interface. It
   should read as information. Every item is now a VALUE with its word beside it; nothing here is
   a bare glyph or a lone chevron. The pill shape is kept deliberately: style.css:109 records that
   the '›' was added because Nathan could not tell the points pill was tappable, so the affordance
   has to survive even though the chevron is gone — the word inside the pill now carries it. */
/* A READOUT, not three buttons. Bordered pills at opposite ends of the bar still read as a nav —
   Nathan: "it doesn't look appreciably better or different than the existing header". So: one
   continuous line of readings, flush left, separated by a middot, with the only real CONTROL
   (settings) alone on the right. Borders gone; the values carry themselves. */
.brandbar-v3 .navstat{display:inline-flex;align-items:baseline;gap:4px;padding:0;margin:0;
  border:0;background:none;cursor:pointer;color:var(--ink);height:auto}
.brandbar-v3 .navstat + .navstat{margin-left:0}
.brandbar-v3 .navstat + .navstat::before{content:"·";margin:0 6px;opacity:.34;font-weight:700;
  align-self:center;color:var(--dim)}
/* ── 📱 AND IT STILL HAS TO FIT AN SE. Four readings survive down to 430 and 393 comfortably; at 375
   the dot gutters are the only fat left, so they close up rather than any reading being dropped.
   Nothing here removes information — Nathan has been clear that an unlabelled number in this bar is
   worse than no number at all. */
@media (max-width:440px){
  .brandbar-v3 .navstat + .navstat::before{ margin:0 4px }
  .brandbar-v3 .nav-lab{ font-size:10px }
}
@media (max-width:400px){
  .brandbar-v3 .navstat + .navstat::before{ margin:0 3px }
  .brandbar-v3 .bb-side.bb-left{ padding-left:7px; margin-left:5px }
}
/* ⚠️ THE BAR MUST FIT THE NARROWEST PHONE IT SHIPS TO. Four readings plus the gear overflowed a 375pt
   viewport by 89px and the "online" reading ran off the edge — measured, and visible in Nathan's
   screenshot. Dropping "progress 15/12" to 📈 took it to 22px; trimming the dot gutters from 9 to 6
   closes the rest. Re-measure at 375 / 393 / 430 before adding a fifth reading. */
/* The readout sizes to its content; the right side only holds settings, so it should not claim a
   third of the bar. My earlier overflow:hidden clipped the "online" label — the group needed 216px
   and had 175px. Measured, not guessed. */
/* Each reading is its own baseline-aligned flex, but the ROW was centring them — so boxes of
   different heights (13px value + 10.5px label, vs the 6px online dot) had their centers lined up
   and their baselines not. Align the row on the baseline so the numbers sit on one line. */
.brandbar-v3 .bb-left{gap:0;flex-wrap:nowrap;flex:1 1 auto;min-width:0;align-items:baseline}
.brandbar-v3 .bb-right{flex:0 0 auto}
/* Tappability without a chevron: the VALUE is inked and the label is dim, and the whole reading
   lifts on press. style.css:109 records Nathan could not tell the old pill was tappable, so the
   affordance has to survive the pill's removal. */
.brandbar-v3 .navstat:active{opacity:.55}
.brandbar-v3 .navstat b{font-size:13px;font-weight:800;line-height:1;letter-spacing:.01em;
  font-variant-numeric:tabular-nums}
.nav-lab{font-size:10.5px;font-weight:600;letter-spacing:.03em;opacity:.62;line-height:1;white-space:nowrap}
.brandbar-v3 .navonline .nodot{width:7px;height:7px;border-radius:50%;background:#2ec16a;display:inline-block;box-shadow:0 0 5px rgba(46,193,106,.7)}
/* ⚡ POINTS — ONE clean tappable pill (retired the overlapping-circle toggle: un-clickable, reflowed the bar, mis-taps). Sparse Wordscapes-style bar: points + settings only; the tournament lives in the gold ribbon + the menu Enter card. */
.brandbar-v3 .navpts{display:inline-flex;align-items:baseline;gap:4px;flex:0 0 auto;padding:0;margin:0;border:0;background:none;cursor:pointer;box-sizing:border-box}
.brandbar-v3 .navpts b{color:#e8c14a}
.brandbar-v3 .navonline .nodot{width:6px;height:6px;border-radius:50%;background:#2ec16a;align-self:center;margin-right:1px}
.brandbar-v3 .navonline b{color:#2ec16a}
.brandbar-v3 .navpts:active{background:rgba(232,193,74,.24)}
.brandbar-v3 .navpts .navpoints{font-variant-numeric:tabular-nums}
.brandbar-v3 .navpts{align-items:baseline}
.brandbar-v3 .navpts .nav-lab{color:#e8c14a;opacity:.72}
/* 🏆 TOURNEY chip — lives RIGHT of the wordmark (Nathan: "lots of room right side of wordmark for a tourney score/ranking"). Separate from ⚡ lifetime points → solves points-vs-tourney. Shown only while LIVE; gated to ≥480px so it never crowds a phone (the header+chip min-content ~448px fits desktop/tablet/landscape; portrait phones keep the pill-boards entry). */
.brandbar-v3 .navtour{display:inline-flex;align-items:center;gap:5px;flex:0 0 auto;height:26px;padding:0 11px;margin:0 6px;border-radius:999px;cursor:pointer;border:1.5px solid rgba(232,193,74,.55);background:linear-gradient(90deg,rgba(232,193,74,.18),rgba(242,211,114,.12));color:#f0d67a;font-weight:800;font-size:13px;line-height:1;white-space:nowrap;box-sizing:border-box;font-variant-numeric:tabular-nums;animation:navtourpulse 2.6s ease-in-out infinite}
.brandbar-v3 .navtour .nt-ic{font-size:13px}
.brandbar-v3 .navtour .nt-rank{opacity:.85;font-weight:700;font-size:11.5px}
.brandbar-v3 .navtour:active{background:rgba(232,193,74,.28)}
@keyframes navtourpulse{0%,100%{box-shadow:0 0 0 0 rgba(232,193,74,.5)}50%{box-shadow:0 0 11px 2px rgba(232,193,74,.85)}}
@media (max-width:479px){.brandbar-v3 .navtour{display:none!important}}   /* portrait phones can't fit pill + wordmark + chip + gear — fold tourney into the ⚡ pill there */
@media (prefers-reduced-motion:reduce){.brandbar-v3 .navtour{animation:none}}
.brandbar .wm{font-weight:500;letter-spacing:.3px;color:var(--dim);font-size:15px}
/* in-game: logo-only top bar — hide the centered wordmark so nothing hits the island, and ride higher */
.ingame .brandbar .wm{display:none}
.ingame .brandbar{padding-top:calc(env(safe-area-inset-top, 0px) + 4px);padding-bottom:2px}   /* sit BELOW the notch + restored status bar (Nathan: was riding UP above the cutout) */
.ingame #app{padding-top:2px}   /* trim the gap under the brandbar in-game so the board rides higher */
.brandbar:hover .wm b{color:var(--accent2)}
.brandbar .wm b{color:var(--ink);font-weight:700}
.brandbar .navbtn{background:var(--panel2);border:1px solid var(--line);color:var(--accent2);
  width:38px;height:38px;border-radius:50%;font-size:18px;line-height:1;cursor:pointer;flex:0 0 auto}
.brandbar .navbtn:first-of-type{margin-left:auto}
.brandbar .navbtn:active{background:var(--panel)}
/* The chorDLE brand mark IS the return-to-menu (tap it → main screen); a little taller for presence. A left
   chevron sits by the wordmark on every non-menu screen to read as "back". On the menu both are hidden (the big
   hero logo is right below) — see body.at-menu. */
.brandbar .brand-mark{height:34px;cursor:pointer}
.brandbar .back-arrow{font-size:30px;line-height:1;color:var(--accent2);cursor:pointer;margin-left:-4px;margin-right:16px;padding:6px 2px 6px 6px;font-weight:600}   /* real gap + bigger tap target (extends LEFT/vertical, away from 📈) so it stops colliding with the progress button (Nathan) */
/* ON EVERY SCREEN INCLUDING HOME. This hid the mark on the menu, which is exactly where a newcomer
   first meets the app — and now that the mark IS the back control, hiding it takes the control away.
   On home it has nowhere to go back to and simply re-renders home, which is the right no-op. */
body.at-menu .brandbar .back-arrow{display:none}

#app{max-width:1140px;margin:0 auto;padding:6px 18px 70px}
/* (menu safe-area + opaque pinned header handled by the body.at-menu .brandbar rule + its #app padding below) */
.setacct-lbl,.setacct-btn{display:block;text-align:left;margin:4px 0}   /* Account row on its own lines: guest label · Sign in with Apple · Sign in with Discord (Nathan) */
h1{font-size:26px;margin:0 0 6px;font-weight:700}
h2{font-size:20px;margin:6px 0 14px;font-weight:500}
h3{font-size:13px;letter-spacing:.5px;text-transform:uppercase;color:var(--dim);margin:20px 0 10px;font-weight:500}
.sub{color:var(--dim);font-size:14px;line-height:1.55;margin:0 0 18px;max-width:640px}
.mono{font-family:"Roboto Mono",ui-monospace,Menlo,monospace}
.muted{color:var(--dim);font-size:13px}
.hint{color:var(--dim);font-size:12px;line-height:1.5}
button{font:inherit;cursor:pointer}
.panel{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:22px}

.choose{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-top:8px}
@media(max-width:720px){.choose{grid-template-columns:1fr}}
/* menu = one evenly-spaced vertical list; every game button carries the same weight */
.gamelist{display:flex;flex-direction:column;gap:12px;margin-top:8px}
.gamelist>.big{margin:0}
/* Multiplayer group — gold-bordered, both games full weight (no diagonal split) */
.mpgroup{border:1px solid #e8c14a;border-radius:14px;padding:10px;display:flex;flex-direction:column;gap:10px;background:rgba(232,193,74,.05)}
.mplabel{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:#e8c14a;font-weight:800;margin:0 2px}
.mpgroup .big.mp{border-color:#e8c14a}
.mpgroup .big.mp:hover{border-color:#ffdd6a}
.big{width:100%;box-sizing:border-box;background:var(--panel2);border:1px solid var(--line);color:var(--ink);border-radius:12px;
     padding:22px 16px;font-size:17px;font-weight:600;display:flex;flex-direction:column;gap:8px;text-align:left}
.big small{font-weight:400;color:var(--dim);font-size:13px}
.big:hover{border-color:var(--accent)}
.big:focus{outline:none}   /* a stray focus ring reads as a 'pre-selected' button; a tap navigates instantly, so suppress it */
/* Per-game color identity — a colored band + soft tint so the flagship Solo practice (and each game) isn't a plain box.
   Daily quiz keeps its own full-border treatment below to stay differentiated. Custom icons to come. */
/* 🏠 (Nathan) the two PRIMARY choices — simple, bold, a little bling (gradient + glow + top sheen), NO emoji */
.big.home-primary{border:none;border-radius:16px;padding:19px 20px 17px;text-align:left;position:relative;overflow:hidden;display:block;transition:filter .12s ease,transform .12s ease}
.big.home-primary::before{content:'';position:absolute;left:0;right:0;top:0;height:52%;background:linear-gradient(180deg,rgba(255,255,255,.16),rgba(255,255,255,0));pointer-events:none}   /* glossy top sheen = the bling */
.big.home-primary .hp-title{display:block;font-size:24px;font-weight:800;letter-spacing:.2px;color:#fff;line-height:1.06;text-shadow:0 1px 2px rgba(0,0,0,.32)}
.big.home-primary .hp-sub{display:block;margin-top:5px;font-size:13.5px;font-weight:500;color:rgba(255,255,255,.82);line-height:1.4}
.big.home-primary:active{transform:translateY(1px)}
/* stepped back from the top gradient so Chord Trainer reads as the primary call — see .prog above */
.big.home-primary.play{background:linear-gradient(135deg,#2b7f92 0%,#226a7d 55%,#1a5666 100%);box-shadow:0 10px 30px rgba(45,172,179,.42),inset 0 0 0 1px rgba(185,242,246,.30)}
.big.home-primary.play:hover,.big.home-primary.play:active{filter:brightness(1.07)}
.big.home-primary.daily{background:linear-gradient(135deg,#2c6274 0%,#234a5c 55%,#1b3a49 100%);box-shadow:0 8px 22px rgba(0,0,0,.42),inset 0 0 0 1px rgba(127,214,219,.20)}
.big.home-primary.daily .hp-title{color:#eaf7f9}
.big.home-primary.daily:hover,.big.home-primary.daily:active{filter:brightness(1.08)}
.big.g-solo{border-color:#3ecf7a;background:rgba(62,207,122,.09)}
.big.g-chordle{border-color:#c58af0;background:rgba(197,138,240,.09)}
.big.g-sing{border-color:#b58cff;background:rgba(181,140,255,.09)}
.big.g-hear{border-color:#5aa0ff;background:rgba(90,160,255,.09)}
.big.sandbox{border-color:var(--band-2);background:rgba(168,119,238,.10)}   /* ALL practice mini-games share the purple (Nathan) */
/* Interval Taster = the highlighted on-ramp: three-family wash + Try it! badge (Nathan) */
/* mini-game rows: fixed ICON COLUMN so titles line up regardless of emoji count/size (Nathan) */
.big.mg{flex-direction:row;align-items:center;gap:12px;padding:15px 16px}
.big.mg .mg-ico{flex:0 0 60px;text-align:center;font-size:20px;line-height:1.05;white-space:nowrap}
.big.mg .mg-body{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:2px}
.big.mg .mg-title{font-weight:800;font-size:18px;line-height:1.15}
.big.mg .mg-body small{color:var(--dim);font-weight:400;font-size:13px}
.big.taster-hl{border-color:rgba(232,121,162,.6);background:linear-gradient(100deg,rgba(232,121,162,.16),rgba(207,154,78,.14),rgba(127,166,200,.16))}
.feat-badge.taster-badge{background:#e879a2;color:#2a0a17}
.big.pro{border-color:#e8c14a;background:rgba(232,193,74,.08)}   /* personalized trainer = same yellow as multiplayer (future Pro) */
.big.g-solo:hover{border-color:#3ecf7a;background:rgba(62,207,122,.16)}
.big.g-chordle:hover{border-color:#c58af0;background:rgba(197,138,240,.16)}
.big.g-sing:hover{border-color:#b58cff;background:rgba(181,140,255,.16)}
.big.g-hear:hover{border-color:#5aa0ff;background:rgba(90,160,255,.16)}
.g-title{font-weight:700;font-size:18px}
.big.resume{border-color:var(--accent);background:#123039;grid-column:1/-1}
.big.newbie{border-color:var(--accent2);background:#123a44;margin-bottom:14px}
/* landing easy-mode — clear space from the buttons above, full-width row (no awkward wrap) */
.landing-easy{display:flex;justify-content:flex-start;align-items:center;gap:7px;flex-wrap:nowrap;white-space:nowrap;margin:34px 0 6px;font-size:13.5px}
/* top-players board on the landing (the space the old footer links used) */
.landboard{margin-top:22px;border-top:1px solid var(--line);padding-top:16px}
.landboard-h{font-size:12px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent2);font-weight:600;margin-bottom:10px}
.introsteps{display:flex;flex-direction:column;gap:12px;margin:14px 0 18px}
.introstep{display:flex;gap:12px;align-items:flex-start;background:var(--panel2);border:1px solid var(--line);border-radius:10px;padding:12px 14px}
.introstep .ic{font-size:24px;line-height:1;flex:0 0 auto}
.introstep b{color:var(--ink)}
.introstep p{margin:3px 0 0;color:var(--dim);font-size:14px;line-height:1.45}

.primary{background:var(--accent);color:var(--accent-ink);border:0;border-radius:9px;padding:11px 18px;font-weight:600}
.primary:hover{background:var(--accent2)}
.danger{background:transparent;color:var(--accent2);border:1px solid var(--accent2);border-radius:9px;padding:10px 16px;font-weight:600}
.ghost{background:var(--panel2);color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:9px 13px}
.ghost:hover{border-color:var(--accent)}
.link{background:none;border:0;color:var(--dim);text-decoration:underline;padding:4px 0}
.back{display:block;margin-bottom:8px}

.cfg{display:flex;gap:14px;flex-wrap:wrap;margin:10px 0 18px;align-items:flex-end}
.cfg label{display:flex;flex-direction:column;gap:6px;font-size:13px;color:var(--dim)}
.cfg select,.cfg input,.panel input{background:#0a1a20;border:1px solid var(--line);color:var(--ink);
     border-radius:8px;padding:9px 11px;font-size:15px;min-width:120px}
/* aligned two-column setup form */
.setup{display:grid;grid-template-columns:max-content minmax(200px,340px);gap:12px 16px;align-items:center;margin:14px 0 20px}
.setup label{justify-self:end;text-align:right;color:var(--dim);font-size:13px}
.setup select{width:100%;background:#0a1a20;border:1px solid var(--line);color:var(--ink);border-radius:8px;padding:9px 11px;font-size:15px}
.setup input[type=checkbox]{justify-self:start;width:18px;height:18px}
.setup .lvlshow{color:var(--ink);font-size:15px}
.setup .lvlshow small{color:var(--dim);font-size:12px}
.lvlpick{display:flex;gap:8px;align-items:center}
.lvlpick select{flex:1}
.lvlpick .mini{flex:0 0 auto;min-width:84px;padding:8px 10px}
/* advanced foldable settings */
details.advanced{margin:6px 0 16px;border:1px solid var(--line);border-radius:10px;background:var(--panel2)}
details.advanced>summary{cursor:pointer;padding:11px 14px;font-size:13px;font-weight:600;color:var(--accent2);
  letter-spacing:.3px;list-style:none;user-select:none}
details.advanced>summary::-webkit-details-marker{display:none}
details.advanced>summary::before{content:"▸ ";color:var(--dim)}
details.advanced[open]>summary::before{content:"▾ "}
details.advanced .setup{margin:0;padding:4px 14px 14px}
details.advanced .settingsrow{padding:4px 14px 14px}
@media(max-width:560px){.setup{grid-template-columns:1fr;gap:4px 0}.setup label{justify-self:start;text-align:left;margin-top:8px}}

/* STACKED header: title alone, pills in a full-width row, progress bar spanning the full column width
   (5 pills + title can't share one line on a 375px phone, so they get their own row) */
.hostbar{display:flex;flex-flow:row wrap;align-items:flex-start;gap:5px 8px;position:relative;
         background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:8px 12px;margin-bottom:8px}
.hostbar small{display:block;color:var(--dim);font-size:12px;margin-top:2px}
.hb-title{min-width:0;display:flex;flex-direction:column;gap:1px;flex:0 1 auto}   /* size to content (don't grow) so the pill cluster sits BESIDE it, top-right */
.hb-title b{line-height:1.2;white-space:nowrap}
.hb-lvl{color:var(--dim);font-size:12px;line-height:1.25}   /* second line — wraps, never truncated */
/* header LEVEL PICKER dropdown — sits where the band name was; band-colored + a small caret (Nathan) */
.hdr-lvlsel{-webkit-appearance:none;appearance:none;background:transparent;border:1px solid transparent;border-radius:7px;color:var(--accent2);font-weight:800;font-size:12.5px;letter-spacing:.01em;line-height:1.3;padding:2px 17px 2px 4px;margin-left:-4px;max-width:64vw;cursor:pointer;text-overflow:ellipsis;
  background-image:linear-gradient(45deg,transparent 50%,currentColor 50%),linear-gradient(135deg,currentColor 50%,transparent 50%);
  background-position:calc(100% - 9px) 52%, calc(100% - 5px) 52%; background-size:4px 4px,4px 4px; background-repeat:no-repeat}
.hdr-lvlsel:active{background-color:rgba(255,255,255,.06)} .hdr-lvlsel:focus{outline:none;border-color:var(--line)}
.hdr-lvlsel.band-0{color:var(--band-0)} .hdr-lvlsel.band-1{color:var(--band-1)} .hdr-lvlsel.band-2{color:var(--band-2)} .hdr-lvlsel.band-3{color:var(--band-3)}
/* ===== STYLED FLIGHT/LEVEL PICKER (Nathan: HTML/CSS, not native select; color-coded blocks; cross-device) ===== */
.lvlmenu-btn{display:flex;flex-direction:column;align-items:flex-start;gap:1px;background:none;border:none;padding:0;margin:0;cursor:pointer;text-align:left;min-width:0}
.lvlmenu-btn .hb-flight{font-size:16px;font-weight:800;letter-spacing:.01em;line-height:1.15}
.lvlmenu-btn .hb-flight.band-0{color:var(--band-0)} .lvlmenu-btn .hb-flight.band-1{color:var(--band-1)} .lvlmenu-btn .hb-flight.band-2{color:var(--band-2)} .lvlmenu-btn .hb-flight.band-3{color:var(--band-3)} .lvlmenu-btn .hb-flight.band-4{color:var(--band-4)}
.lvlmenu-btn .hb-lvl{color:var(--dim);font-size:12px;line-height:1.25;display:inline-flex;align-items:center;gap:5px}
.lvlmenu-caret{font-size:9px;opacity:.8;transition:transform .15s}
.lvlmenu-btn.open .lvlmenu-caret{transform:rotate(180deg)}
.lvlmenu-panel{position:absolute;top:100%;left:8px;right:8px;margin-top:6px;z-index:60;max-height:84vh;overflow-y:auto;-webkit-overflow-scrolling:touch;
  background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:8px;box-shadow:0 14px 40px rgba(0,0,0,.5);display:flex;flex-direction:column;gap:8px}
.lvlmenu-panel[hidden]{display:none}
/* Invisible click-catcher so "tap outside to close" also works over the wheel iframe, whose clicks
   never reach this document. Sits UNDER the panel (60) and over everything else. */
.lvlmenu-scrim{position:fixed;inset:0;z-index:59;background:transparent}
.lvlmenu-flight{border-radius:11px;padding:9px 10px;border-left:4px solid var(--line);background:var(--panel2)}
.lvlmenu-flight.band-0{border-left-color:var(--band-0);background:rgba(45,172,179,.09)}
.lvlmenu-flight.band-1{border-left-color:var(--band-1);background:rgba(62,207,122,.08)}
.lvlmenu-flight.band-2{border-left-color:var(--band-2);background:rgba(168,119,238,.09)}
.lvlmenu-flight.band-3{border-left-color:var(--band-3);background:rgba(232,193,74,.09)}
.lvlmenu-flight.band-4{border-left-color:var(--band-4);background:rgba(232,86,74,.09)}
.lvlmenu-flight.tm{border-left-color:var(--fam-sweet);background:linear-gradient(100deg,rgba(232,112,154,.12),rgba(217,154,78,.08),rgba(143,163,189,.1))}
.lvlmenu-fhead{position:sticky;top:0;z-index:3;font-size:11px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;color:var(--ink);margin:-9px -10px 7px;padding:9px 10px 6px;background:var(--panel);border-radius:9px 9px 0 0;opacity:1}   /* 📌 sticky flight title — pins while you scroll its levels, until the NEXT flight pushes it up (Nathan) */
.lvlmenu-lvls{display:flex;flex-direction:column;gap:5px}
/* 📏 A ROW IS A THUMB TARGET. At 9px of padding these were a list to read rather than a set of buttons
   to hit, on the one screen where choosing the wrong thing costs you a level. (Nathan: "I think those
   levels need to be taller so it's easier to hit the buttons.") */
.lvlmenu-lvl{display:block;width:100%;text-align:left;background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:14px 13px;min-height:52px;color:var(--ink);font-size:13.5px;font-weight:600;cursor:pointer;transition:background .1s,border-color .1s}
/* 🎨 the one level that is ABOUT two colors is titled in them */
/* every level title in caps — one species of thing (Nathan) */
.rungbtn .hb-flight, .lvlmenu-lvl{ text-transform:uppercase }
.lvlmenu-lvl{ letter-spacing:.02em }
/* the keyboard is away entirely while a major/minor hand is still a question */
/* ⚠️ THE KEYBOARD GOES AWAY — THE TRANSPORT DOES NOT. The transport (play, mic, loop) lives INSIDE
   #pgKeys, so hiding the whole row took the play button with it. That was harmless while this only
   fired on Major/minor; when the Major/minor condition was dropped (push of 2026-09-19) every wheel
   rung lost its play button and mic until the hand was graded. Nathan: "Is the play button and
   microphone supposed to be absent while it's playing? I don't think I remember that." It wasn't.
   visibility keeps the keyboard's space reserved, so nothing moves when it comes back. */
#pgKeys.kb-away > :not(.pg-transport){ visibility:hidden }
/* the same letterforms the two big targets wear, so the level reads as its own brand */
.mmt-maj, .mmt-min{ font-weight:900; letter-spacing:.06em }
.mmt-maj{ color:#FFD79B }
.mmt-min{ color:#8E7EF0 }
/* 🧍 "YOU" IS LABELLED ON ITS OWN MARK. It hung to one side like the reference labels, so at a low
   streak it sat well left of its tick and read as pointing at empty track. (Nathan: "the you marker is
   disassociated with its position on the meter.") Centerd over the tick, it can only mean one place. */
/* ⚠️ THE OFFSET FOLLOWS THE POSITION, so the pill cannot hang off either end. A fixed -50% centers it
   on its point, which is right in the middle of the bar and wrong at the extremes: at a streak of 0 it
   sits at the far left and half of it lands outside #pgLvlBtn, which is overflow:hidden — so it was
   sliced. (Nathan: "the YOU pill gets clipped by the left side of the container when it is at its
   bottom position.") Scaling the shift by --at slides it from left-aligned at 0, through centerd at
   50, to right-aligned at 100: always touching its point, never past the edge. */
.rungbtn .lvl-now b{ left:0!important; right:auto!important;
                      transform:translateX(calc(-1% * var(--at, 50))); text-align:center }
.lvlmenu-lvl:hover{border-color:var(--accent2)}
.lvlmenu-lvl:active{background:var(--panel2)}
.lvlmenu-lvl.cur{border-color:var(--accent2);background:rgba(45,172,179,.16);font-weight:800}
.lvlmenu-lvl.cur::after{content:"● here";float:right;color:var(--accent2);font-size:11px;font-weight:800}
.lvlmenu-lvl.locked{opacity:.5;cursor:not-allowed;font-weight:500}
.lvlmenu-lvl.f-sweet{border-color:var(--fam-sweet)}
/* 🎓 GRADUATION levels (the flight's 'gate' board) — gold, so it reads as "graduation" in every band (Nathan) */
.lvlmenu-lvl.grad{border-color:var(--band-3);background:linear-gradient(100deg,rgba(232,193,74,.16),rgba(232,193,74,.05));font-weight:800}
.lvlmenu-gradtag{float:right;color:var(--band-3);font-size:9.5px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;border:1px solid var(--band-3);border-radius:999px;padding:1px 7px;margin:1px 0 0 6px}
/* named BANDS (over the numeric ladder) — band name + a quiet progress cue; internal number is admin-only (Nathan) */
.hb-lvl .bandname{color:var(--accent2);font-weight:800;letter-spacing:.02em}
.hb-lvl .bandcue{display:inline-flex;align-items:center;gap:3px;margin-left:7px;vertical-align:middle}
.hb-lvl .bandcue i{width:5px;height:5px;border-radius:50%;background:rgba(255,255,255,.22);transition:background .2s}
.hb-lvl .bandcue i.on{background:var(--accent2)}
.hb-lvl .bandnum{margin-left:7px;font-size:10px;font-weight:700;color:var(--dim);border:1px solid var(--line);border-radius:6px;padding:0 5px;opacity:.65}
/* FORMALIZED band/path PALETTE (Nathan) — one cool→warm progression, shared by the 4 bands AND the ①②③ path badges */
:root{ --band-0:#2dacb3; --band-1:#3ecf7a; --band-2:#a877ee; --band-3:#e8c14a; --band-4:#e8564a; }   /* Incubator teal · Foundations green · Lab purple · The Pass gold · The Shed red (hardest) */
/* INTERVAL-FAMILY palette (Nathan, Option A) — full colors chosen NOT to clash with the quality-letter colors
   (Major gold · minor periwinkle · Perfect teal · Tritone lime) and to read distinct at a glance. accent = border/text,
   bg = the full tile fill. Spicy is a DARK steel so the periwinkle/gold/lime letters pop instead of blending. */
:root{ --fam-sweet:#5aa9e6; --fam-sweet-bg:#1e3a54; --fam-savory:#d99a4e; --fam-savory-bg:#6e4a1f; --fam-salty:#e8564a; --fam-salty-bg:#4d1e1a; }   /* 🎨 Nathan N11: sweet = lighter BLUE (was pink), spicy(salty) = chili RED (was blue-gray), savory = amber stays */
.hb-lvl .bandname.band-0{color:var(--band-0)} .hb-lvl .bandname.band-1{color:var(--band-1)} .hb-lvl .bandname.band-2{color:var(--band-2)} .hb-lvl .bandname.band-3{color:var(--band-3)}
.hb-lvl .bandcue.band-0 i.on{background:var(--band-0)} .hb-lvl .bandcue.band-1 i.on{background:var(--band-1)} .hb-lvl .bandcue.band-2 i.on{background:var(--band-2)} .hb-lvl .bandcue.band-3 i.on{background:var(--band-3)}
/* 🎵 DAILY CHORD — marked as special: big title + a "collect it" tagline (Nathan) */
.cotd-title{font-size:21px !important;font-weight:800;letter-spacing:.02em;color:#ffd0e6;text-shadow:0 1px 10px #e87fb066}
.cotd-tag{color:#f2bcd6 !important;font-weight:600}
/* 💥 NOTE-COUNT MILESTONE explosion ("THREE NOTES!!!") — Nathan */
.note-milestone{display:flex;align-items:center;justify-content:center;z-index:999}
.nm-card{background:linear-gradient(180deg,#1a2f38,#0b1c21);border:2px solid #e8c14a;border-radius:22px;padding:32px 28px;text-align:center;max-width:420px;box-shadow:0 14px 55px rgba(0,0,0,.6),0 0 70px rgba(232,193,74,.22);animation:nmPop .5s cubic-bezier(.2,1.3,.4,1)}
@keyframes nmPop{0%{transform:scale(.55);opacity:0}100%{transform:scale(1);opacity:1}}
.nm-boom{font-size:60px;line-height:1;animation:nmBoom .6s ease-out}
@keyframes nmBoom{0%{transform:scale(0) rotate(-25deg)}60%{transform:scale(1.5) rotate(12deg)}100%{transform:scale(1) rotate(0)}}
.nm-big{font-size:46px;font-weight:900;letter-spacing:.02em;margin:6px 0 8px;line-height:1.05;background:linear-gradient(90deg,#3ecf7a,#e8c14a,#f06aa8);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}
.nm-sub{color:#cfe6ea;font-size:14.5px;line-height:1.5;margin-bottom:20px}
/* 🏁 endgame nudge — a gentle bottom offer, not a blocking modal (Nathan) */
.endnudge{position:fixed;left:0;right:0;bottom:calc(env(safe-area-inset-bottom,0px) + 14px);z-index:998;display:flex;justify-content:center;pointer-events:none;padding:0 14px}
.en-card{pointer-events:auto;background:linear-gradient(180deg,#1a2f38,#0b1c21);border:2px solid #e8c14a;border-radius:16px;padding:14px 18px;text-align:center;box-shadow:0 10px 40px rgba(0,0,0,.6);animation:nmPop .35s cubic-bezier(.2,1.2,.4,1)}
.en-card>b{display:block;font-size:17px;margin-bottom:9px;color:#ffe9a8}
.en-btns{display:flex;flex-direction:column;gap:7px;align-items:stretch}
.en-btns .link{font-size:12px}
/* pills = ONE horizontal row in the upper-right beside the title (Nathan) — NEVER a vertical stack.
   nowrap keeps them on a single line; if there's no room beside the title the whole cluster drops to ONE
   full-width row below (not multiple wrapped rows); scrolls only if truly too many to fit. */
.hb-right{display:flex;align-items:center;gap:5px;flex-wrap:nowrap;justify-content:flex-end;flex:0 1 auto;min-width:0;margin-left:auto;overflow-x:auto;overflow-y:hidden;padding:5px 1px;scrollbar-width:none}   /* vertical padding so the 🐦 glow isn't clipped by the scroll container */
.hb-right::-webkit-scrollbar{display:none}
.cluehd{color:var(--dim);font-size:12px;margin:0 0 5px}
.scorechip{background:var(--panel2);border:1px solid var(--accent);color:var(--accent2);font-weight:700;font-size:12.5px;padding:2px 8px;border-radius:20px;font-variant-numeric:tabular-nums;white-space:nowrap;flex:0 0 auto}
.ingame .hb-right{gap:4px}
.ingame .hb-right .scorechip{font-size:11px;padding:1px 6px}   /* tighter in-game so the whole pill cluster fits ONE line beside the title (Nathan: still line-breaking → smaller) */
.scorechip[data-tip]{cursor:pointer}   /* tappable info pills — tap to learn what they mean */
.hbslot-off{visibility:hidden}   /* 🔒 RESERVE the slot, hide the content — the 🎯 accuracy + 🎁 bonus pills exist from tile 0 (just invisible) so when they light up mid-game the cluster width is UNCHANGED → zero grid shift (Nathan #1 pet peeve) */
.hb-right .scorechip.hbnum{display:inline-block;min-width:3.4em;text-align:center}   /* fixed width so digit growth (🎁 5→237, 🎯 1/1→12/15) can't jog the neighbours either */
.ingame .hb-lvl{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}   /* in-game: the level line can NEVER wrap to a 2nd row — that wrap was what grew the header and shoved the board down when the pill cluster widened */
.pilltip{position:fixed;left:50%;top:66px;transform:translateX(-50%) translateY(-8px);background:var(--panel2);border:1px solid var(--accent2);color:#fff;padding:8px 14px;border-radius:16px;font-size:13px;font-weight:600;z-index:80;opacity:0;transition:opacity .25s ease,transform .25s ease;pointer-events:none;max-width:88vw;text-align:center;box-shadow:0 6px 20px rgba(0,0,0,.55)}
.pilltip.show{opacity:1;transform:translateX(-50%) translateY(0)}
/* 📣 live-ops banner (broadcasts + new-version nudge) — sits just under the notch, above everything, dismissible */
.livebanner{position:fixed;left:8px;right:8px;top:max(env(safe-area-inset-top,0px),8px);margin:0 auto;max-width:620px;display:flex;align-items:center;gap:10px;z-index:120;padding:10px 12px;border-radius:14px;font-size:13.5px;font-weight:600;color:#fff;opacity:0;transform:translateY(-12px);transition:opacity .28s ease,transform .28s ease;box-shadow:0 8px 26px rgba(0,0,0,.5)}
.livebanner.show{opacity:1;transform:translateY(0)}
.livebanner .lb-msg{flex:1;line-height:1.35}
.livebanner .lb-go{flex:none;background:#fff;color:#0d1f24;border:0;border-radius:10px;padding:6px 12px;font-weight:800;font-size:13px;cursor:pointer}
.livebanner .lb-x{flex:none;background:transparent;border:0;color:rgba(255,255,255,.8);font-size:16px;line-height:1;cursor:pointer;padding:2px 4px}
.livebanner.bcast{background:linear-gradient(135deg,#7a3fb0,#4453bd);border:1px solid rgba(255,255,255,.25)}
.livebanner.ver{background:linear-gradient(135deg,#177a5a,#3ecf7a);border:1px solid rgba(255,255,255,.25)}
.about-links{display:flex;flex-direction:column;gap:8px;margin-top:12px}
.about-legal{margin-top:14px;text-align:center;font-size:13px}
.about-legal a{color:var(--accent2)}
.about-copy{margin-top:14px;font-size:12px;color:var(--dim);text-align:center;line-height:1.6}
.rp-desc{width:100%;box-sizing:border-box;margin:8px 0;background:var(--panel2);border:1px solid var(--line);border-radius:8px;color:#fff;padding:10px;font-size:15px;font-family:inherit;resize:vertical}
/* gambit forfeit — quick shake of the free square */
@keyframes gmshake{0%,100%{transform:translateX(0)}18%{transform:translateX(-4px)}38%{transform:translateX(4px)}58%{transform:translateX(-3px)}78%{transform:translateX(2px)}}
.cell.gm-shake{animation:gmshake .44s ease-in-out}
/* progress meter — full width of the column */
.hb-meter{display:block;height:6px;width:100%;flex-basis:100%;border-radius:3px;background:rgba(255,255,255,.13);overflow:hidden}   /* flex-basis:100% → always its own full-width row at the bottom of the header */
.hb-meter>i{display:block;height:100%;background:var(--accent2);border-radius:3px;transition:width .3s ease}
/* clue-card badge row: "Clue N" + speaker (tap-to-hear affordance, replaces the "listen…" text) */
.cc-badgerow{display:flex;align-items:center;gap:6px}
.cc-spk{color:var(--accent2);flex:0 0 auto;opacity:.9}
.carslide .chordcard .cc-spk{position:absolute;top:8px;left:10px;z-index:4;opacity:.75;overflow:visible}
.carslide .chordcard .cc-spk:active{opacity:1}
/* clue-tile achievement icons in the SIDE column: ✓ = all notes; 🏆 = all intervals (wild-square win, pulses). Small. */
.cc-chk{color:var(--good);font-weight:900;font-size:12px;margin-right:1px}
.cc-ach{display:inline-flex;align-items:center;vertical-align:middle;margin-right:2px}
.cc-ach svg{display:block}
.cc-ach.pulse{animation:achpulse 1.15s ease-in-out infinite;transform-origin:center}
@keyframes achpulse{0%,100%{transform:scale(1);filter:drop-shadow(0 0 1px rgba(232,193,74,.5))}50%{transform:scale(1.35);filter:drop-shadow(0 0 7px #e8c14a)}}
/* leaderboard name — clean standard field (16px avoids iOS zoom-on-focus) */
.nameinput{-webkit-appearance:none;appearance:none;width:100%;background:#0a1a20;border:1px solid var(--line);
  color:var(--ink);border-radius:10px;padding:12px 14px;font-size:16px;margin:14px 0;display:block}
.nameinput::placeholder{color:var(--dim)}
.nameinput:focus{outline:none;border-color:var(--accent)}
.trophy .tr-score{font-size:26px;font-weight:800;margin-top:6px;line-height:1.1;letter-spacing:-.5px;font-variant-numeric:tabular-nums}
.trophy .tr-sum{font-size:12px;font-weight:700;opacity:.8;margin-top:1px;font-variant-numeric:tabular-nums}   /* the breakdown that used to crowd the number, kept but quiet */
.trophy .tr-btns{display:flex;flex-direction:column;gap:8px;margin-top:10px}
.trophy .tr-alt{display:flex;gap:18px;justify-content:center}
/* the two secondary actions: dark ink on gold, no underline, a real tap target */
.trophy .tr-alt .link{color:#0a3540;opacity:.72;text-decoration:none;font-weight:800;font-size:13px;
  letter-spacing:.2px;padding:8px 12px;border-radius:8px;white-space:nowrap}
.trophy .tr-alt .link:active{opacity:1;background:rgba(10,53,64,.09)}
/* stop single words falling to a line of their own */
.trophy .tr-main,.trophy .tr-sub,.trophy .tr-mull{text-wrap:balance}
/* one rhythm instead of four arbitrary gaps: title, then the number, then the rest */
.trophy .tr-sub{margin-top:2px}
.trophy .tr-score{margin-top:10px}
.trophy .tr-mull{margin-top:10px}
.trophy .tr-btns{margin-top:12px}
.trophy .tr-alt{margin-top:2px}   /* on-bingo secondary destinations: Review · Menu (New game is the primary Cash-out button) */
.trophy .tr-btns .big{margin:0;padding:14px;font-size:16px;text-align:center;align-items:center}
.trophy .tr-btns .ghost{flex:0 0 auto;padding:14px 16px}
.trophy .tr-cta{margin-top:10px;font-size:13px}
.trophy .tr-cta a{color:#0a3540;font-weight:800;text-decoration:underline}
.trophy .tr-score .link{color:#0a3540;font-weight:700;text-decoration:underline}
.primary.sm{padding:7px 12px;font-size:13px;border-radius:8px}
.code{font-size:24px;font-weight:700;letter-spacing:4px;color:var(--accent2);cursor:pointer}   /* 🔑 (Nathan) tap the room code to copy it — no button, no iOS magnifier loupe (a single tap, not a text-selection) */
/* 🎠 (Nathan) MULTIPLAYER now REUSES solo's clue interface verbatim — phone = swipeable .carousel (global CSS),
   iPad/desktop = a comfortable grid of the SAME cc-side/cc-kbwrap cards. No bespoke MP card layout: the shared
   .chordcard + .carslide rules do the work, so the room looks/behaves exactly like solo play. */
.mpstrip.strip{grid-template-columns:repeat(auto-fill,minmax(230px,1fr));max-width:none}   /* wide: readable multi-card grid (not the cramped 150px cells) */
.mp-play{background:linear-gradient(180deg,rgba(126,90,214,.10),rgba(126,90,214,.03) 220px),var(--bg)}   /* 🎨 distinct room tint (violet wash) so a live room is instantly not solo */
.mp-play .statusbar{border-color:rgba(126,90,214,.35)}
.mp-play h3{color:#c9b6f0;margin:12px 0 6px}   /* 🩹 (Nathan) tighten the section gaps — "too much space between clue and board" */
.mp-play .carousel{margin:2px 0 4px}
.mp-play .carousel .cardots{margin-bottom:8px}
/* 🩹 (Nathan iPhone) compact the clue card so the board sits closer AND leaves page-scroll room above/below the
   swipe strip (the carousel is touch-action:pan-x, so vertical drags need a non-carousel gutter to grab). */
.mp-play .carslide .chordcard{padding:26px 12px 10px}
.mp-play .carscroll{padding:2px 0 6px}
.mp-play .grid{margin-top:2px}
.phasepill{font-size:12px;text-transform:uppercase;letter-spacing:1px;padding:5px 11px;border-radius:20px;
           border:1px solid var(--line);color:var(--dim)}
.phasepill.playing{color:var(--accent);border-color:var(--accent)}
.phasepill.ended{color:var(--good);border-color:var(--good)}

.hostcols{display:grid;grid-template-columns:1fr 1fr;gap:22px}
@media(max-width:820px){.hostcols{grid-template-columns:1fr}}
.col{min-width:0}
.builder-row{display:flex;gap:8px;margin-top:10px;flex-wrap:wrap}
.builder-row input{flex:1;min-width:140px;background:#0a1a20;border:1px solid var(--line);color:var(--ink);border-radius:8px;padding:9px 11px}
.builder-row select{background:#0a1a20;border:1px solid var(--line);color:var(--ink);border-radius:8px;padding:9px 11px;font-size:14px}
.flash{margin-top:10px;color:var(--bad);font-size:13px}

.pianobox{background:var(--panel2);border:1px solid var(--line);border-radius:10px;padding:8px}
.piano{display:block;touch-action:none;user-select:none}
.wkey{fill:#f3f6f6;stroke:#b9c6c9;stroke-width:1}
.wkey.on{fill:#E74C3C}      /* branded eraser red for active notes (not blue) */
.ebmark.ebblink{animation:ebblink .9s ease-in-out infinite}   /* MISSED-note eraser blinks on the end-game review */
@keyframes ebblink{0%,100%{opacity:1}50%{opacity:.18}}
/* full-stack (all notes found) status dot gets a white center pip */
/* clue-dot state (Nathan's ring spec) — all notes = a center dot · TRUE full house = center dot + a hugging outer ring (reads as two rings with the border) */
.cardot.allnotes,.cardot.fullhouse{position:relative}
.cardot.allnotes::after,.cardot.fullhouse::after{content:'';position:absolute;left:50%;top:50%;width:4px;height:4px;border-radius:50%;background:var(--dc);transform:translate(-50%,-50%);pointer-events:none}
.cardot.fullhouse{box-shadow:0 0 0 2px var(--dc)}
.cardot.fullhouse::after{background:#fff;box-shadow:0 0 4px rgba(255,255,255,.9)}
/* leaderboard "+N earned since you last looked" chip */
.lbdelta{display:inline-block;font-size:11px;font-weight:800;color:#2ecc71;background:rgba(46,204,113,.14);border:1px solid rgba(46,204,113,.3);border-radius:10px;padding:0 6px;margin:0 6px}
.bkey{fill:#0e2830;stroke:#000;stroke-width:1}
.bkey.on{fill:#ff7a6a}
.minikb{margin-top:8px;background:#f3f6f6;border-radius:6px;padding:3px}

.chordlist{display:flex;flex-direction:column;gap:9px}
.chordrow{background:var(--panel2);border:1px solid var(--line);border-radius:10px;padding:10px 12px}
.cr-top{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.cr-notes{color:var(--ink);font-weight:600}
.cr-ivs{color:var(--dim);font-size:12px;margin:4px 0}
.cr-guessers{font-size:12px;margin:2px 0 4px;display:flex;gap:10px;flex-wrap:wrap}
.cr-guessers .gg.ok{color:var(--good)}
.cr-guessers .gg.bad{color:var(--bad)}
.cr-guessers.muted{color:var(--dim);font-style:italic}
.cr-actions{display:flex;gap:7px;flex-wrap:wrap;margin-top:6px}
.cr-top{display:flex;align-items:center;gap:8px}
.cr-top .eraser{margin-left:auto}
.cr-toggles{display:flex;flex-direction:column;gap:6px;margin-top:8px}
.cr-toggles .tgl{display:flex;align-items:center;gap:8px;font-size:13px;cursor:pointer}
.cr-toggles .tgl small{color:var(--dim);font-weight:400}
.cr-toggles .tgl input{width:16px;height:16px;flex:0 0 auto}
.num{color:var(--accent2);font-weight:700}
.badge{font-size:11px;padding:2px 8px;border-radius:12px;border:1px solid var(--line);color:var(--dim)}
.badge.rev{color:var(--good);border-color:var(--good)}
.badge.open{color:var(--accent);border-color:var(--accent)}
.badge.hid{color:var(--dim)}
.badge.bad{color:var(--bad);border-color:var(--bad)}

.playerlist{display:flex;flex-direction:column;gap:7px}
.prow{display:flex;align-items:center;gap:10px;background:var(--panel2);border:1px solid var(--line);border-radius:9px;padding:8px 12px;cursor:pointer}
.prow .pn{font-weight:500}
.prow .pscore{color:var(--good);font-size:12px;font-variant-numeric:tabular-nums}
.prow .spybtn{margin-left:auto}
.prow.watching{border-color:var(--accent);background:#0f2a33}

/* spectate a player's live card (Band Leader) */
.spypanel{margin-top:12px;background:var(--panel2);border:1px solid var(--line);border-radius:10px;padding:12px}
.spypanel h4{margin:0 0 10px}
.spygrid .cell{min-height:auto;padding:6px}
.spygrid .cell .iv{font-size:13px}

/* room roster / leaderboard (player view) */
.roompanel{background:var(--panel2);border:1px solid var(--line);border-radius:10px;padding:10px 12px;margin:12px 0}
.roompanel h4{margin:0 0 8px}
.lb{display:flex;flex-direction:column;gap:5px}
.lbrow{display:flex;align-items:center;gap:9px;font-size:14px;padding:3px 4px;border-radius:7px}
.lbrow.me{background:#0f2a33}
.lbrow .rank{width:20px;text-align:center;color:var(--dim);font-variant-numeric:tabular-nums}
.lbrow .lbname{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}   /* one line, ellipsize — no wrap bloat; right columns stay aligned */
.champ-medal{margin-left:3px;white-space:nowrap}   /* reigning tournament champ — 🏅 by their name until someone tops them (Nathan) */
.ob-taster{background:linear-gradient(90deg,rgba(232,112,154,.13),rgba(217,154,78,.11),rgba(143,163,189,.13));border:1px solid rgba(255,255,255,.14)}   /* three-family wash — STEP 1 of the warm-up (Nathan); sits in the numbered list */
.ob-taster small{color:rgba(255,255,255,.66)}
.ob-taster.ob-done{opacity:.72}   /* completed step 1 reads done like the others */
.lbrow .lblevel{color:var(--accent2);font-size:11px;font-weight:600;background:#123039;border:1px solid var(--line);border-radius:8px;padding:1px 7px}
.lbrow .lbscore{color:var(--good);font-size:12px;font-variant-numeric:tabular-nums;min-width:34px;text-align:right}
.landboard-h .mylevel{display:block;text-transform:none;letter-spacing:0;color:var(--dim);font-weight:400;font-size:12px;margin-top:2px}
/* My ears — per-interval accuracy bars */
.eartotals{color:var(--dim);font-size:13px;margin:6px 0 14px}
/* My Progress dashboard */
.prog-hero{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin:6px 0 10px}
.pcard{background:var(--panel2);border:1px solid var(--line);border-radius:10px;padding:10px 6px;text-align:center}
.pcard .pnum{font-size:20px;font-weight:800;color:var(--ink);font-variant-numeric:tabular-nums;line-height:1.1}
.pcard .plabel{font-size:11px;color:var(--dim);margin-top:3px}
@media(max-width:420px){.pcard .pnum{font-size:17px}}
.psec{font-size:14px;margin:18px 0 8px;color:var(--accent2)}
.psec .muted{font-weight:400;font-size:12px}
.spark{width:100%;aspect-ratio:600/180;height:auto;max-height:300px;display:block}   /* aspect-ratio matches the viewBox → uniform scaling: round dots, no horizontal stretch, fills the space */
.spark polyline{fill:none;stroke:var(--accent2);stroke-width:2;vector-effect:non-scaling-stroke}
.spark circle{fill:var(--accent)}
.spark .sparkbase{stroke:var(--line);stroke-width:1;vector-effect:non-scaling-stroke}
.sparkcap{display:flex;justify-content:space-between;font-size:11px;color:var(--dim);margin-top:2px}
.conflist{display:flex;flex-direction:column;gap:6px}
.confrow{display:flex;align-items:center;gap:8px;background:var(--panel2);border:1px solid var(--line);border-radius:8px;padding:7px 10px;font-size:14px}
.confrow b{color:var(--ink)}
.confrow .wrongtint{color:var(--bad)}
.confrow .confarrow{color:var(--dim);font-size:12px}
.confrow .muted{margin-left:auto;font-size:12px}
.needswork{margin-top:16px;padding:10px 12px;border-radius:10px;background:#14283a;border:1px solid var(--accent2);color:var(--ink);font-size:13px}
.trajhdr{display:flex;align-items:baseline;gap:12px;margin-bottom:8px;flex-wrap:wrap}
.trajdelta{font-size:30px;font-weight:800;font-variant-numeric:tabular-nums}
.trajdelta.up{color:var(--good)} .trajdelta.down{color:var(--bad)} .trajdelta.flat{color:var(--dim)}
.trajmsg{font-size:14px;color:var(--ink)}
.pstats-tabs{margin-bottom:14px}
/* glowing interval constellation (galaxy / harmony-wheel aesthetic) */
.constellation{width:100%;max-width:440px;height:auto;display:block;margin:6px auto 2px}
/* 🌐 you-vs-everyone ear-map toggle + Beat Kitchen branded crowd view (Nathan) */
.earmap-toggle{display:flex;gap:6px;justify-content:center;margin:0 0 6px}
.earmap-toggle .emt{background:transparent;border:1px solid var(--line);color:var(--dim);border-radius:999px;padding:5px 15px;font-size:13px;font-weight:600;cursor:pointer;-webkit-tap-highlight-color:transparent}
.earmap-toggle .emt.on{background:var(--bks-cta,#2dacb3);color:#04232a;border-color:transparent}
.crowd-brand{display:flex;align-items:center;gap:8px;justify-content:center;margin:4px 0 2px}
.crowd-brand img{height:15px;width:auto;opacity:.95}
.crowd-brand span{font-size:11px;letter-spacing:.14em;color:var(--dim);text-transform:uppercase}
.crowd-stat{text-align:center;font-size:12px;color:var(--dim);margin:0 0 2px;font-variant-numeric:tabular-nums}
.constellation .cst-ring{stroke:rgba(126,214,219,.14);stroke-width:1}
.constellation .cst-core{fill:var(--accent2);filter:drop-shadow(0 0 7px var(--accent2));animation:cstpulse 2.4s ease-in-out infinite}
@keyframes cstpulse{0%,100%{opacity:.55}50%{opacity:1}}
.constellation .cst-lbl{fill:#cfe3e6;font-size:13px;text-anchor:middle;font-weight:700}
.cst-key{display:flex;gap:14px;justify-content:center;font-size:11px;color:var(--dim);margin:2px 0 4px}
.cst-key .k::before{content:'';display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:5px;vertical-align:middle}
.cst-key .good::before{background:#3ecf7a}.cst-key .mid::before{background:#e8b04a}.cst-key .weak::before{background:#ff6b6b}.cst-key .none::before{background:#33465a}
/* swipeable chart carousel */
.pchart-dots{display:flex;flex-wrap:wrap;gap:7px;justify-content:center;margin:2px 0 12px}
.pchart-dots .cardot{width:24px;height:24px;font-size:11px}
.pchart-scroll{display:flex;overflow-x:auto;scroll-snap-type:x mandatory;gap:0;scrollbar-width:none;-webkit-overflow-scrolling:touch}
.pchart-scroll::-webkit-scrollbar{display:none}
.pchart-slide{flex:0 0 100%;scroll-snap-align:center;padding:0 2px;box-sizing:border-box}
.pchart{background:var(--panel2);border:1px solid var(--line);border-top:3px solid var(--cc,var(--accent2));border-radius:14px;padding:15px 14px;min-height:300px;display:flex;flex-direction:column;box-shadow:0 0 22px -14px var(--cc,transparent)}
.pchart-t{font-size:15px;color:var(--cc,var(--accent2));margin:0 0 12px;font-weight:800}
.pchart .chart-note{font-size:13px;color:var(--dim);line-height:1.5;margin:12px 0 0}
.pchart .chart-note b{color:var(--ink)}
.pchart-fix{margin-top:auto;width:100%}
.pchart .earbars,.pchart .conflist{margin-top:2px}
/* practice calendar (galaxy heat) */
.calgrid{display:grid;grid-template-columns:repeat(7,1fr);gap:6px;max-width:260px;margin:8px auto 0}
.calcell{aspect-ratio:1;border-radius:5px;background:#1a2836}
.calcell.l1{background:#5a2a30;box-shadow:0 0 5px #ff6b6b55}
.calcell.l2{background:#6b5a24;box-shadow:0 0 6px #e8b04a66}
.calcell.l3{background:#1f5a3a;box-shadow:0 0 8px #3ecf7a88}
.calcap{text-align:center;margin-top:8px;font-size:11px}
.calstreak{text-align:center;font-size:16px;font-weight:800;color:#ff9d3c;margin-bottom:8px}
.pchart-fix{text-align:center}
.constellation .cst-spoke{stroke:rgba(126,214,219,.09);stroke-width:1}
/* clearer, color-coded chart dots */
.pchart-dots .cardot{border-color:var(--cc,var(--line));color:var(--dim);transition:all .15s}
.pchart-dots .cardot.cur{background:var(--cc,var(--accent2));border-color:var(--cc,var(--accent2));color:#04222a;transform:scale(1.3);box-shadow:0 0 12px var(--cc,var(--accent2))}
.constellation .cst-ringc{fill:none;stroke:rgba(126,214,219,.14);stroke-width:1}
.scatter{width:100%;max-width:320px;height:auto;display:block;margin:4px auto 0}
.scatter .axis{stroke:var(--line);stroke-width:1}
.scatax{display:flex;justify-content:space-between;max-width:320px;margin:2px auto 0;font-size:11px;color:var(--dim)}
/* iPad/desktop: divider between the clue cards and the grid */
.clue-divider{display:flex;align-items:center;gap:12px;margin:18px 0 14px}
.clue-divider::before,.clue-divider::after{content:'';flex:1;height:1px;background:var(--line)}
.clue-divider span{font-size:11px;text-transform:uppercase;letter-spacing:1px;color:var(--dim)}
/* iPad / desktop landscape: a pinned title bar across the top, then clues (left, independent scroll) + grid (right,
   aligned to the top of the clue list). The progress line under the header acts as the divider. */
/* ?narrow=1 pins the PHONE layout at any window width, so a desktop Safari window can be compared with
   the phone without Responsive Design Mode (which needs the Develop menu enabled). */
body.pg-narrow .solo-wrap{display:block!important;max-width:420px;margin:0 auto}
body.pg-narrow .solo-wrap>*{grid-column:auto!important;grid-row:auto!important;max-height:none!important;
  width:auto!important;justify-self:auto!important;align-self:auto!important}
body.pg-narrow .solo-wrap>.grid{aspect-ratio:auto!important;width:100%!important}
body.pg-narrow .solo-wrap>.strip{overflow-y:visible!important}

@media(min-width:820px){   /* match JS "wide" threshold so iPad 11" portrait (834px) gets two columns */ 
  body.ingame #app{padding-bottom:8px}
  body.ingame .cta-block{display:none}   /* Nathan: in-game, the "what's coming up / free guides" block below created a dead-margin scroll on short games — hide it so the Solo-practice header + grid stay in view (it shows on the menu). A tall grid still scrolls for the grid itself. */
  .stuck-peek{display:block;margin:8px auto 0;font-size:13px;opacity:.8}   /* secondary "peek at the board" under the prominent New-game exit when exhausted */
  /* Two columns · two rows. Row 1: [Solo-practice badge (with its own progress meter)] | [coaching message].
     Row 2: [clue strip, scrolls] | [a big SQUARE grid that grows to fill the space]. The full-width progress
     divider is gone (meter lives in the badge), which frees the top-right for messaging and lets the grid grow. */
  .solo-wrap{display:grid;grid-template-columns:388px minmax(0,1fr);grid-template-rows:auto minmax(0,1fr);column-gap:30px;row-gap:10px;align-items:start}
  .solo-wrap.play>.hostbar,.solo-wrap>.hostbar{grid-column:1;grid-row:1;margin:0;max-width:none;align-items:stretch}
  .solo-wrap>.gridmsg{grid-column:2;grid-row:3;align-self:start;margin:0;min-height:72px;box-sizing:border-box;display:block;position:static}   /* messaging ABOVE the grid (block so spaces around <b> survive) — reserve for the TALLEST message (3 lines) so NO message state (miss/tries-left/mulligan/default) ever grows it and shifts the grid (Nathan's #1 pet peeve) */
  /* GRID grows to a large square (no scrolling to see it), centered in the right column. 250px ≈ brandbar + badge + controls. */
  /* align-self was CENTER: row 2's height is set by the clue strip in column 1, so every graded clue made
     that column taller and the centerd board DRIFTED DOWN with it — 186px -> 265px over five clues, i.e.
     the chooser moving under the player's thumb. Pinned to the top of the row; the row can grow, the
     board cannot move. (Nathan's standing no-layout-shift rule.) */
  .solo-wrap.play>.grid,.solo-wrap>.grid{grid-column:2;grid-row:2;justify-self:center;align-self:start;width:min(100%,calc(100dvh - 250px));max-width:none;aspect-ratio:1;margin:0}
  /* the reading strip rides with the BOARD, not the clue list: on an iPad in side-by-side it was at the
     bottom of the left column, below the fold — it belongs directly above the wheel. (Nathan) */
  .solo-wrap>.pg-read-strip{grid-column:2;grid-row:1;align-self:end;margin:0 0 4px;justify-self:stretch}
  .solo-wrap>*:not(.grid):not(.hostbar):not(.gridmsg):not(.pg-read-strip){grid-column:1}                   /* clues + controls in the left column */
  /* strip scrolls INDEPENDENTLY of the grid — a FIXED height cap (matching the grid square) is what makes overflow
     actually engage; height:100% of a content-sized row never scrolls. Few clues fit; many scroll, grid stays put. */
  .solo-wrap.play>.strip,.solo-wrap>.strip{grid-column:1;grid-row:2;display:flex;flex-direction:column;gap:12px;max-height:calc(100dvh - 250px);min-height:0;overflow-y:auto;-webkit-overflow-scrolling:touch;touch-action:pan-y;padding:34px 8px 0 0;margin:0;max-width:none}   /* overscroll-behavior:contain REMOVED: on iPad it makes Safari arbitrate at the column's boundary before the page can move, which is the delay Nathan feels before scrolling "starts rolling". touch-action:pan-y is what actually routes a vertical drag into this column, so reachability is kept. ?scroll=legacy restores the old behavior to A/B on the device. */
  body.scroll-legacy .solo-wrap>.strip{overscroll-behavior:contain}
  .solo-wrap>.strip .chordcard{width:100%;max-width:none;margin:0;flex:0 0 auto}
  .solo-wrap>.clue-divider{display:none}
  .solo-wrap>.playbar,.solo-wrap>.foot{grid-column:1}
}
/* "you made a mistake on this clue" indicator */
.cc-wrong{color:var(--bad);font-size:10px;font-weight:700;margin-left:auto;white-space:nowrap}
.chordcard.had-wrong{border-color:rgba(255,107,107,.45)}
/* difficulty level rows with replay */
.lvlchart{display:flex;flex-direction:column;gap:9px}
.lvlrow{display:flex;align-items:center;gap:9px}
.lvlrow .lvlname{width:108px;font-size:12px;font-weight:700;color:var(--ink);flex:0 0 auto}
.lvlrow .lvlname small{display:block;color:var(--dim);font-weight:400;font-size:10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.lvlrow .earbar{flex:1;height:18px;border-radius:9px}
.lvlrow .earbar span{transition:width .4s ease}
.lvlrow .earbar.good span{box-shadow:0 0 8px var(--good)}
.lvlrow .earbar.none{visibility:hidden}
.lvlrow .earpct{width:34px}
.lvlrow .lvlreplay{flex:0 0 auto;padding:7px 15px;font-size:15px;line-height:1}
.lvlrow .lvlreplay.locked{opacity:.4;filter:grayscale(.7);cursor:default;background:var(--panel2);border-color:var(--line)}
.lvlrow.future .lvlname,.lvlrow.future .earpct{opacity:.5}
.lvlrow.cur .lvlname{color:var(--accent)}
.lvlrep{display:inline-block;margin-left:5px;font-size:10px;font-weight:800;color:var(--accent);background:rgba(127,214,219,.14);border:1px solid rgba(127,214,219,.4);border-radius:6px;padding:1px 5px;vertical-align:middle}
/* school CTA on the consistency screen */
.school-cta{margin-top:16px;padding:12px 13px;border-radius:10px;background:linear-gradient(150deg,#14283a,#122c26);border:1px solid var(--accent2);font-size:12.5px;line-height:1.5;color:var(--dim)}
.school-cta b{color:var(--ink)}
.school-cta-btn{display:inline-block;margin-top:9px;color:var(--accent2);font-weight:700;text-decoration:none}
.earbars{display:flex;flex-direction:column;gap:8px}
.earrow{display:flex;align-items:center;gap:10px}
.earrow .eariv{width:44px;font-weight:700;color:var(--ink);font-size:14px}
.earrow .earbar{flex:1;height:12px;border-radius:6px;background:var(--panel2);border:1px solid var(--line);overflow:hidden}
.earrow .earbar span{display:block;height:100%}
.earrow .earbar.good span{background:var(--good)}
.earrow .earbar.mid span{background:#e8b04a}
.earrow .earbar.weak span{background:var(--bad)}
.earrow .earpct{width:38px;text-align:right;font-size:12px;font-variant-numeric:tabular-nums;color:var(--dim)}
.earrow .earseen{width:34px;text-align:right;font-size:11px;color:var(--dim)}
.lbrow .youtag{display:inline-block;background:var(--accent);color:var(--accent-ink);font-size:10px;font-weight:800;
  padding:1px 6px;border-radius:8px;margin-left:6px;letter-spacing:.03em;vertical-align:middle}
.lbcta{display:flex;gap:12px;margin-top:18px}
.lbcta .primary,.lbcta .ghost{flex:1}
.lbrow .lbdots{margin-left:auto;color:var(--good);letter-spacing:1px;font-size:11px}

.claim{border-radius:12px;padding:12px 16px;margin-bottom:14px;font-size:15px}
.claim.good{background:#123a2a;border:1px solid var(--good)}
.claim.bad{background:#3a1c1c;border:1px solid var(--bad)}

.hintbox{display:inline-flex;gap:7px;align-items:center;color:var(--dim);font-size:13px;margin:0 0 10px;cursor:pointer}
.settingsrow{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:10px 22px;align-items:center;
  margin:4px 0 18px;padding:14px 16px;background:var(--panel);border:1px solid var(--line);border-radius:12px}
.settingsrow .hintbox{margin:0;justify-content:flex-start}
.settingsrow select{background:#0a1a20;border:1px solid var(--line);color:var(--ink);border-radius:6px;padding:5px 7px;margin-left:auto}
.minikb{max-width:320px}
.cnum{position:absolute;top:4px;left:5px;min-width:16px;height:16px;border-radius:8px;padding:0 4px;
      font-size:11px;font-weight:700;color:#08252b;display:flex;align-items:center;justify-content:center}
.cbadge{display:inline-flex;height:19px;border-radius:9px;padding:0 9px;letter-spacing:.02em;
      font-size:11.5px;font-weight:800;color:#08252b;align-items:center;justify-content:center;white-space:nowrap}
.btnrow{display:flex;gap:4px;width:100%;margin-top:6px}
.mini{flex:1;border-radius:6px;padding:5px 4px;font-size:12px;font-weight:600;cursor:pointer;
      background:var(--panel);border:1px solid var(--line)}
.cc-top .mini{flex:0 0 auto;padding:9px 14px;font-size:13px}   /* give-up: a comfortable tap target on the card */
.mini.go{color:var(--accent2);border-color:var(--accent2)}      /* neutral teal — not "right" green */
.mini.go:hover{background:rgba(45,172,179,.15)}
.mini.cancel{color:var(--dim)}
.mini.cancel:hover{border-color:var(--dim)}
.cell.committed input{opacity:.65}
/* NYC-subway-style guess bullets */
.bullets{display:flex;justify-content:center;gap:0;margin:3px 0 0;padding:0 4px;flex-wrap:nowrap}   /* small flow deck in the gap between description & keyboard — no overlap, keyboard stays bottom-anchored */
.cell .bul{width:17px;height:17px;font-size:11px;font-weight:800;border:2px solid #fff;box-shadow:0 1px 3px rgba(0,0,0,.5),0 0 7px rgba(255,255,255,.28);position:relative}   /* brighter/bolder guess markers so it's clear which tiles have a guess (Nathan) */
.cell.hasbul{box-shadow:inset 0 0 0 2px rgba(255,255,255,.26)}   /* faint ring on any tile that holds a guess */
.cell .bullets .bul+.bul{margin-left:-7px}   /* fanned-deck overlap (like cards) — packs many clue numbers into one row */
.cell .bul.cur{z-index:6;transform:scale(1.15);box-shadow:0 0 0 2px var(--bg),0 2px 5px rgba(0,0,0,.6),0 0 9px rgba(255,255,255,.55)}   /* active clue's badge lifted to the front of the fan so you can glance which tiles hold it (Nathan) */
.bul{position:relative;width:26px;height:26px;border-radius:50%;display:flex;align-items:center;justify-content:center;
     font-size:14px;font-weight:800;color:#08252b;cursor:pointer;border:2px solid rgba(0,0,0,.25);user-select:none}
/* wrong guess: dim the number and strike a clear white ✕ through it so it reads as WRONG, not just red */
.bul.bad{color:rgba(255,255,255,.55);border-color:#ff6b6b}
.bul.bad::after{content:"";position:absolute;left:50%;top:50%;width:150%;height:3px;background:#fff;border-radius:2px;
     transform:translate(-50%,-50%) rotate(-45deg);box-shadow:0 0 0 1px rgba(0,0,0,.35)}
.bul.confirm{outline:3px solid #ffd76a;outline-offset:1px;animation:bulconfirm .6s ease-in-out infinite}
@keyframes bulconfirm{0%,100%{transform:scale(1)}50%{transform:scale(1.15)}}
.bul.dim{filter:saturate(.62) brightness(.82)}   /* dim WITHOUT opacity so overlapping circles stay solid (no bleed-through) */
.bul:hover{filter:brightness(1.12)}
.status.warn{color:var(--accent)}
.mini.go.on{background:var(--accent);color:var(--accent-ink);border-color:var(--accent)}
.wildtag{margin-top:6px;font-size:11px;font-weight:700;letter-spacing:1px;color:#e8c14a}
.bonusbar{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:14px;max-width:560px;
          background:rgba(232,193,74,.08);border:1px solid rgba(232,193,74,.4);border-radius:10px;padding:8px 12px;
          animation:bonusglow 1.7s ease-in-out infinite}   /* a banked mulligan glows — it's the high point */
@keyframes bonusglow{0%,100%{box-shadow:0 0 0 0 rgba(232,193,74,0);border-color:rgba(232,193,74,.4)}50%{box-shadow:0 0 18px 2px rgba(232,193,74,.45);border-color:#e8c14a}}
/* shimmer the tiles a banked mulligan can bring back — advertise the reward before you even spend it */
.cell.mull-shimmer{animation:mullshimmer 2.6s ease-in-out infinite}
.cell.mull-shimmer:nth-child(3n){animation-delay:.55s} .cell.mull-shimmer:nth-child(3n+1){animation-delay:1s}
/* Spend-a-mulligan: ONE gold tone on every claimable/unlockable square (overrides red-locked + dark-dead so it reads uniform) */
.cell.spendglow{border-color:#3ecf7a!important;box-shadow:0 0 14px 2px rgba(62,207,122,.5),inset 0 0 0 1px #3ecf7a!important;opacity:1!important;background:#16321f}   /* 🟢 Mulligan (activated spend) = GREEN, not gold — GOLD is now reserved for the BINGO LINE (Nathan) */
@keyframes mullshimmer{0%,100%{box-shadow:inset 0 0 0 1.5px rgba(232,193,74,.5)}50%{box-shadow:inset 0 0 0 1.5px rgba(232,193,74,.85)}}
.btok{color:#e8c14a;font-weight:700}
.bonusbar .mini{flex:0 0 auto;min-width:110px}
.listen{touch-action:none;user-select:none}
.strip{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:10px;max-width:560px}  /* uniform grid; lone card doesn't stretch */
/* HORIZONTAL clue card: [Clue N badge + status] column · full keyboard · give-up overlaid top-right of the keyboard.
   Height is driven by the keyboard (width-fit), so every key is fully visible + TAPPABLE — nothing clipped. */
.chordcard{background:var(--panel2);border:1px solid var(--line);border-radius:12px;padding:24px 10px 10px;min-width:0;box-sizing:border-box;overflow:visible;cursor:pointer;display:flex;flex-direction:row;align-items:center;gap:11px;position:relative}
/* badge/label at the TOP of the card; status below it, leaving room for per-clue messaging as the game progresses */
/* LAYOUT B: left column = stats stack (top) + give-up (pinned bottom); Clue N pill overlays the keyboard center. */
.cc-side{flex:0 0 auto;align-self:stretch;width:48px;display:flex;flex-direction:column;gap:4px;align-items:flex-start;padding-top:2px}   /* 🎹 was 78px for the "tap the keys to hear" text — text is gone, so reclaim ~30px of width for the keyboard so the keys are readable (Nathan) */
.cc-statwrap{display:flex;flex-direction:column;gap:4px;align-items:flex-start;min-height:0}
/* give-up: pinned to the keyboard's TOP-RIGHT corner (overlapping like the number badge) so clue tiles keep a FIXED height (Nathan) */
.give-side{position:absolute;top:-11px;right:2px;z-index:4;padding:3px 8px;font-size:10.5px;font-weight:700;line-height:1.15;border-radius:7px;background:var(--panel);border:1px solid var(--line);color:var(--dim);white-space:nowrap;cursor:pointer;text-align:center;box-shadow:0 2px 6px rgba(0,0,0,.35)}
.give-side.confirm{background:var(--bad);border-color:var(--bad);color:#fff;font-weight:700}
/* status badge rides the SAME top-right corner as give-up (swaps in once resolved) — never grows the tile (Nathan) */
.cc-cornerstat{position:absolute;top:-11px;right:2px;z-index:4}
.cc-cornerstat .cc-status{box-shadow:0 2px 6px rgba(0,0,0,.4);font-weight:700}
/* the answer (intervals) becomes a caption pinned over the keyboard bottom — out of the flex flow, so no push-down */
/* 🎨 (Nathan) revealed interval list = a wrapping row of BRANDED tags that lives BELOW the keyboard, not an absolute pill crammed over the keys ("it's just a list… put it below the SVG, branded"). */
.cc-anscap.cc-empty{min-height:22px}   /* 📐 the answer line's SPACE exists from the first render; only its contents arrive later */
.cc-anscap{margin:6px 4px 2px;display:flex;flex-wrap:wrap;gap:4px 5px;justify-content:center;align-items:center;font-size:11px;line-height:1.3;pointer-events:none;min-height:22px}
/* 📐 RESERVE THE ROW, DO NOT GROW INTO IT. This row is empty until a clue is solved and then fills
   with interval tags — so the card grew by a whole row at the moment of solving and shoved the entire
   board down under it. (Nathan, 2026-10-08, from two screenshots a minute apart: "whatever made the
   board move down I'd like to find it and stop it… I don't think there's any reason for that card to
   resize as much as it does.") A tag is ~22px with its padding, so the row holds that height whether
   it has tags in it or not, and solving changes what the card SAYS without changing where it ends.
   The same rule the rest of the game already follows: reserve the space, never reflow under a thumb. */
.cc-anscap .cc-iv{font-weight:800;color:#e6f2f5}
.cc-ivtag{display:inline-flex;align-items:center;font-size:11.5px;font-weight:800;letter-spacing:.02em;padding:2px 7px;border-radius:6px;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.15);color:#e6f2f5}
/* ── 🎨 ONE SET OF FAMILY COLOURS, FROM THE TOKENS. Three different schemes for the same three
   families were live at once: the :root tokens (sweet BLUE, savory AMBER, salty RED), this one
   (sweet amber, savory cyan, salty purple), and the review list below (sweet PINK, salty BLUE-GREY)
   — and those last two are the colours Nathan RETIRED in N11, still rendering. (Nathan, 2026-10-05:
   "inheriting those colors and making sure they are used consistently around the game.") */
.cc-ivtag.fam-sweet{color:var(--fam-sweet);border-color:color-mix(in srgb,var(--fam-sweet) 42%,transparent)} .cc-ivtag.fam-savory{color:var(--fam-savory);border-color:color-mix(in srgb,var(--fam-savory) 42%,transparent)} .cc-ivtag.fam-salty{color:var(--fam-salty);border-color:color-mix(in srgb,var(--fam-salty) 42%,transparent)}
.cc-anscap .fam-sweet{color:var(--fam-sweet);font-weight:700} .cc-anscap .fam-savory{color:var(--fam-savory);font-weight:700} .cc-anscap .fam-salty{color:var(--fam-salty);font-weight:700}
.cc-anscap .cc-song{display:block;white-space:normal;font-size:8.5px;font-style:italic;font-weight:600;color:#0a3a44;margin-top:1px;line-height:1.15}   /* 🎵 tune reference on its own line INSIDE the clue pill (Nathan) — the interval·family line stays nowrap above it */
.cc-playhint{font-size:10px;color:var(--dim);opacity:.7;text-align:center;margin-top:4px;white-space:nowrap;line-height:1.2}   /* replaces the two ▶/↗ buttons: the revealed keyboard is directly playable now (tap=together, swipe=arp) — just a hint, no layout change (Nathan) */
.cc-playbtns{margin-top:auto;align-self:stretch;display:flex;flex-direction:column;gap:4px}
.cc-pb{padding:5px 4px;font-size:10.5px;font-weight:700;border-radius:6px;background:var(--panel);border:1px solid var(--accent2);color:var(--accent2);cursor:pointer;text-align:center;white-space:nowrap}
@media(hover:hover){ .cc-pb:hover{background:rgba(45,172,179,.15)} }
.cc-pb:active{background:var(--panel2)}
@media(hover:hover){ .give-side:hover{border-color:var(--bad);color:#fff} }
/* The clue label is now a NUMBERED STATUS CIRCLE (was a "Clue N" pill): chord-colored, pops above the keyboard,
   and inherits status (green ring = solved/all-notes, red = locked/off-card) — same language as the carousel dots. */
.cc-cluelbl{position:absolute;top:-30px;left:50%;transform:translateX(-50%);z-index:3;display:inline-flex;align-items:center;justify-content:center;text-align:center;font-variant-numeric:tabular-nums;width:38px;height:38px;padding:0;border-radius:50%;font-size:19px;font-weight:800;color:#08252b;text-shadow:0 0 3px rgba(255,255,255,.85),0 1px 1px rgba(255,255,255,.6);box-shadow:0 4px 10px rgba(0,0,0,.45);border:3px solid rgba(255,255,255,.7);background:var(--cc)}   /* 🔢 (Nathan) the dark number was illegible on darker chord colors — a light halo makes it pop on any --cc */
.cc-cluelbl.st-solved,.cc-cluelbl.st-allnotes{border-color:var(--good);box-shadow:0 0 0 2px var(--good),0 4px 10px rgba(0,0,0,.45)}
.cc-cluelbl.st-failed,.cc-cluelbl.st-cleared{background:var(--bad);color:#fff}
/* auto-advance: the new clue number pulses so you SEE the move (Nathan). Gentle scale (no overshoot) + a bright ring flash — modest enough not to clip the carousel's top. */
@keyframes cluemoved{0%{transform:translateX(-50%) scale(1)}30%{transform:translateX(-50%) scale(1.16);box-shadow:0 0 0 4px rgba(255,255,255,.85),0 4px 14px rgba(0,0,0,.5)}100%{transform:translateX(-50%) scale(1)}}
.cc-cluelbl.moved{animation:cluemoved .5s ease-out}
.cc-status{font-size:11.5px;font-weight:700;color:var(--ink);line-height:1.2;background:rgba(8,20,24,.72);border:1px solid rgba(255,255,255,.2);border-radius:7px;padding:3px 8px;box-shadow:0 2px 6px rgba(0,0,0,.4)}   /* Nathan: give the "✓ all notes" / "N found" badge the same container as the give-up pill so it's legible over the SVG keyboard */
.cc-status.dim{color:var(--dim);font-weight:600}
.cc-status.ok{color:var(--good)} .cc-status.mid{color:var(--accent2)} .cc-status.bad{color:var(--bad)}
/* 🎚️ per-clue PROGRESS METER (Nathan — drop hide-the-count): the badge background IS the bar, filling teal to --p%; text rides on top. Green when every interval is found, so at a glance you see which clues are done vs. left. */
.cc-status.cc-prog{color:#dff2f5;font-variant-numeric:tabular-nums;letter-spacing:.03em;background:linear-gradient(90deg, rgba(79,214,224,.32) var(--p,0%), rgba(8,20,24,.72) var(--p,0%))}
.cc-status.cc-prog.full{color:#0a2a17;font-weight:800;background:rgba(62,207,122,.9);border-color:rgba(62,207,122,.9)}
.scorechip.strikechip{font-variant-numeric:tabular-nums;letter-spacing:.02em}   /* 🎯 board-strike counter — reserved in the header (scored mode only), reddens as strikes accrue */
.scorechip.strikechip.hit{color:#ff8a8a;border-color:#ff6b6b66}
.cc-ans{font-size:10px;color:var(--accent2);font-weight:700;line-height:1.15}
.cc-kbwrap{position:relative;flex:1;min-width:0;display:flex;flex-direction:column;align-items:center;justify-content:center}   /* 🎹 (Nathan) COLUMN not row: the answer-tag list (.cc-anscap) must sit BELOW the keyboard, not beside it — the row layout was shrinking the keys the moment a clue solved. Keyboard now stays full-width; tags wrap underneath. */
.cc-kbwrap .minikb{width:100%;margin:0;background:#f3f6f6;border-radius:6px;padding:3px;box-sizing:border-box}
/* HERO clue card = STACK vertically so the keyboard goes FULL-WIDTH (it was squeezed to a ~36px strip beside the
   78px side column, which killed the note markers at wide levels). Applies to EVERY carousel card (phone shows
   one at a time — active OR revealed/locked) and to the ACTIVE card in the iPad strip (others stay compact).
   Controls drop to a compact row UNDER the keyboard; the Clue-N slide handle pops up centered above it. */
.carslide{min-height:155px}   /* 📐 THE NO-JUMP FLOOR. One floor on the slide holds the board still whether the active card is short or tall — the old per-card reservation made EVERY card tall and cost ~50px of screen. 155px = the tallest card measured. A floor on .carstage did NOT work: measurement showed the SLIDE and the scroller collapsing, not the stage. */
.carslide .chordcard{flex-direction:column;align-items:stretch;gap:9px;padding:30px 12px 12px}
.carslide .chordcard .cc-kbwrap{order:1;width:100%}
.carslide .chordcard .cc-side{order:2;flex-direction:row;width:auto;align-self:stretch;align-items:center;gap:10px;padding-top:0;flex-wrap:wrap;min-height:0;box-sizing:border-box}   /* 📐 was RESERVED at 27px — see 2026-10-09: with the speaker moved to the corner this row is empty on a solved card, and reserving a row for nothing is what pushed the Collect button off the bottom of the screen. The no-jump guarantee moves to the carousel, which is already as tall as its tallest card. OLD NOTE (Nathan, 2026-10-07: "the game board with its tiles should really never move"). 27px = the tallest thing this row holds (the give-up pill, measured at 26.6px). Empty it still holds its line, so a solved clue cannot shrink its card and drag the board up. */
.carslide .chordcard .cc-statwrap{flex-direction:row;align-items:center;gap:8px;min-height:0;flex-wrap:wrap}
.carslide .chordcard .give-side{margin-top:0;margin-left:auto;align-self:center;flex:0 0 auto;padding:6px 14px}
.carslide .chordcard .cc-playbtns{margin-top:0;flex-direction:row;align-self:center}
/* Keyboard sized by WIDTH (fills the wrap) so all keys top-to-bottom are visible/tappable — never clipped. The
   viewBox ratio sets the height; a generous cap keeps any one card from getting too tall. Range is C-aligned +
   >=2 octaves (set in app.js) so it's always cut on C, never mid-key, with room on the sides. */
.minikb svg,.minikb .piano{display:block;width:100%;height:auto;max-height:158px}
.give-ov{position:absolute;top:6px;right:6px;z-index:3;padding:4px 9px;font-size:11px;font-weight:600;border-radius:7px;
         background:rgba(8,20,24,.72);border:1px solid rgba(255,255,255,.2);color:#e3f0f2;white-space:nowrap;cursor:pointer}
.give-ov.confirm{background:var(--bad);border-color:var(--bad);color:#fff;font-weight:700}
@media(hover:hover){ .give-ov:hover{border-color:var(--bad);color:#fff} }
.cc-count{font-size:12px;color:var(--dim);font-weight:600}
.card-solved{border-color:var(--good)!important}
.card-solved .cbadge{box-shadow:0 0 0 2px var(--good)}
.card-allnotes{border-color:#e8c14a!important}
.card-failed{opacity:.94;border-color:#7a3b3b!important}   /* revealed/given-up = the ANSWER; keep it legible (was .75 → hard to read, esp. compounded with the carousel peek-fade) */
.card-cleared{opacity:.96;border-color:var(--good)!important}
.badge.mid{background:#3a2f12;border:1px solid #e8c14a;color:#ffdd8a}
/* SELECTED clue — must be unmistakable (playtest: "hardly looks different"). Bold ring + heavy glow + tint + a pop. */
.chordcard.active{border-color:var(--accent2);box-shadow:0 0 0 3px var(--accent2),0 0 26px 4px rgba(45,172,179,.6);background:rgba(45,172,179,.16);transform:scale(1.02);z-index:2}
/* Nathan: when a clue IS selected, DIM every other clue so which one you're placing on is unmistakable — a silent auto-advance (esp. on web, no scroll cue) used to be invisible and sent a correct answer to the wrong clue. */
#app:has(.chordcard.active) .chordcard.playable:not(.active):not(.inert):not(.cleared){opacity:.42;filter:saturate(.55);transition:opacity .12s ease,filter .12s ease}
/* (removed the left teal accent bar — redundant now that the active card has a full accent border + glow, and
   in the centered carousel it read as an odd floating stripe on the card's left edge) */
.chordcard.active .cbadge{outline:2px solid #fff;outline-offset:1px}
.chordcard{position:relative;transition:transform .12s ease,box-shadow .12s ease}
/* when placing, invite a tap (crosshair) and pulse ONLY the tiles already holding the selected chord
   (NOT empty tiles — that wrongly implied the answer was limited to them) */
.grid.placing [data-assign]{cursor:crosshair}
.cell.activehere{animation:placepulse 2.4s ease-in-out infinite}
@keyframes placepulse{0%,100%{box-shadow:inset 0 0 0 2px rgba(45,172,179,.5)}50%{box-shadow:inset 0 0 0 2px rgba(45,172,179,.95)}}
.cell.activemine{animation:minepulse 2.2s ease-in-out infinite}   /* selected chord's placed tiles pulse in ITS color */
@keyframes minepulse{0%,100%{box-shadow:inset 0 0 0 2px rgba(255,255,255,.55)}50%{box-shadow:inset 0 0 0 2px rgba(255,255,255,.8),0 0 7px rgba(255,255,255,.42)}}   /* active clue's placed tiles = a NEUTRAL white pulse (was the clue's color) — which-clue is the number badge; borders/pulses mean STATE now (Nathan, scheme B) */
/* a clear banner while a chord is selected */
.placebar{display:flex;align-items:center;gap:8px;background:#123039;border:1px solid var(--accent2);border-radius:10px;padding:8px 12px;margin:6px 0 10px;font-weight:600;color:var(--accent2)}
/* persistent status line — always present (min-height) so messages never shift the layout below */
.statusbar{height:58px;box-sizing:border-box;display:flex;align-items:center;gap:8px;padding:9px 12px;margin:6px 0 12px;border-radius:10px;font-size:14px;line-height:1.3;overflow:hidden}   /* FIXED height — message changes never shift anything below */
.statusbar.act{background:#123039;border:1px solid var(--accent2);color:var(--accent2);font-weight:600}
.statusbar.warn{background:#3a2a12;border:1px solid #e8b04a;color:#ffd98a;font-weight:600}
.statusbar.dim{background:var(--panel2);border:1px solid var(--line);color:var(--dim)}
.statusbar.empty{background:transparent;border-color:transparent}   /* dismissed intro hint → invisible but still 58px tall (no layout shift) */
.pchip{width:22px;height:22px;flex:0 0 auto;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;color:#04262b;font-weight:800;font-size:13px}
.chordcard:hover{filter:brightness(1.08)}
.cc-top{display:flex;gap:8px;align-items:center;margin-bottom:8px;min-width:0}
.cc-top .cbadge{flex:0 0 auto}
.cc-top .give{flex:0 0 auto;margin-left:auto;padding:4px 8px;font-size:11px;white-space:nowrap;color:var(--dim);border-color:var(--line);background:transparent}
/* hover only on hover-capable devices — otherwise the red state sticks after a tap on touchscreens */
@media(hover:hover){ .cc-top .give:hover{color:var(--bad);border-color:var(--bad)} }
.cc-top .give:active{color:var(--bad);border-color:var(--bad)}
.cc-top .give.confirm{color:#fff;background:var(--bad);border-color:var(--bad);font-weight:700}
.cc-ivs{font-size:12px;color:var(--accent2);margin-top:3px;font-weight:600}
/* obvious "play the chord" affordance on the clue card (the whole card is tappable, this makes it clear) */
.cc-play{margin:10px 0 6px;padding:10px;text-align:center;border-radius:9px;background:#12333c;border:1px solid var(--accent);color:var(--accent2);font-weight:700;font-size:15px}
.chordcard:active .cc-play{background:#164450}
/* high-stakes gambit button + outcomes */
.mini.gambit{border-color:#e8a04a;color:#ffcf8a;margin-left:6px}
.mini.gambit.confirm{background:#4a2a12;border-color:#ff6b6b;color:#ffd98a}
/* gamble control + its dismissible confirm modal */
.gamblewrap{margin:2px 0 10px;text-align:center}
.gamblebtn{width:100%;padding:11px;border-radius:10px;background:#2a1c0a;border:1px solid #e8a04a;color:#ffcf8a;font-weight:700;font-size:14px;cursor:pointer}
.gamblebtn:active{background:#3a2610}
.modal-ov{position:fixed;inset:0;z-index:400;background:rgba(4,14,18,.72);display:flex;align-items:center;justify-content:center;padding:20px}
.modal{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:18px;max-width:360px;width:100%;box-shadow:0 20px 60px rgba(0,0,0,.6)}
.modal h3{margin:0 0 8px}
.modal p{margin:6px 0;font-size:14px;color:var(--ink)}
.modal .modal-win{color:var(--good)}
.modal .modal-lose{color:#ff9a6a}
.iv-picker{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin:12px 0}
.iv-pick{padding:11px 4px;border-radius:9px;background:var(--panel2);border:1px solid var(--line);color:var(--ink);font-weight:700;font-size:15px;cursor:pointer}
.iv-pick:hover{border-color:var(--accent2);color:var(--accent2)}
.iv-pick:active{background:var(--panel)}
/* gambit dialog: bigger surface + GOLD to match the FREE/gambit square; every interval shows its full name */
.gambit-modal{max-width:560px;border-color:#e8c14a;box-shadow:0 20px 60px rgba(0,0,0,.6),0 0 0 1px rgba(232,193,74,.4)}
.gambit-modal h3{color:#e8c14a}
.iv-picker.big{grid-template-columns:repeat(auto-fill,minmax(94px,1fr));gap:10px;max-height:54vh;overflow-y:auto;padding-right:4px}
.iv-picker.big .iv-pick{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;padding:12px 6px;min-height:58px}
.iv-pick b{font-size:17px;line-height:1}
.iv-pick small{font-size:10px;color:var(--dim);font-weight:600;line-height:1.1;text-align:center;white-space:normal}
.iv-picker.big .iv-pick:hover{border-color:#e8c14a;color:#e8c14a}
.iv-picker.big .iv-pick:hover small{color:#e8c14a}
.modal-btns{display:flex;gap:10px;margin-top:14px}
.modal-btns button{flex:1;padding:11px;border-radius:9px;font-weight:700;cursor:pointer}
.villain-face{width:46px;height:46px;border-radius:50%;background:#2a1c0a;border:2px solid #e8c14a;color:#e8c14a;font-size:26px;font-weight:900;display:flex;align-items:center;justify-content:center;margin:0 auto 8px}
/* the gambit taunt: "?"s pop in at random angles, fill the free square, then all fade — with a long pause between */
.qm-field{position:absolute;inset:-6px;pointer-events:none;overflow:visible}   /* scatter a little AROUND the tile too */
.qm-field .qm{position:absolute;font-weight:900;color:#e8c14a;opacity:0;transform-origin:center;
  animation:qmPop 8s ease-in-out calc(4.5s + var(--d)) infinite}   /* ~4.5s searching, then fill/fade with a long gap */
@keyframes qmPop{
  0%,58%,100%{opacity:0;transform:scale(.2) rotate(var(--r))}
  8%{opacity:.95;transform:scale(1.15) rotate(var(--r))}
  16%{opacity:.9;transform:scale(1) rotate(var(--r))}
  44%{opacity:.9;transform:scale(1) rotate(var(--r))}
  52%{opacity:0;transform:scale(.5) rotate(var(--r))} }
.cell.free[data-freegambit]{cursor:pointer;box-shadow:0 0 0 2px #2dacb3,0 0 14px rgba(45,172,179,.5);border-color:#2dacb3;animation:freeGlow 2.6s ease-in-out infinite}   /* 🔵 gambit-in-play = TEAL, not gold — distinct from Mulligan (green) AND the bingo line (gold) (Nathan) */
.cell.free[data-freegambit]:hover{border-color:#2dacb3}
.cell.free .iv{position:relative;z-index:1}
@keyframes freeGlow{ 0%,100%{box-shadow:0 0 0 2px #2dacb3,0 0 10px rgba(45,172,179,.4);border-color:#2dacb3} 50%{box-shadow:0 0 0 2px #2dacb3,0 0 22px rgba(45,172,179,.72);border-color:#5fd6db} }   /* teal pulse (gambit) — gold freed for the bingo line */
/* cartoon speech-bubble caption pointing at the free square */
.gambit-cap{position:absolute;transform:translate(-50%,-118%);z-index:30;width:210px;max-width:62vw}
.gambit-cap.below{transform:translate(-50%,55%)}
.gambit-bubble{background:#e8c14a;color:#26200f;font-size:12px;font-weight:600;line-height:1.35;padding:9px 11px;border-radius:11px;box-shadow:0 10px 26px rgba(0,0,0,.5);position:relative}
.gambit-bubble .mini{margin-top:8px;width:100%;background:#1c1708;color:#fff;border-color:#1c1708;font-weight:800}   /* readable "Got it" — was low-contrast gold-on-gold (Nathan) */
/* family "what's this?" on-demand help link + its popup buttons */
.tm-whatlink{background:none;border:none;color:var(--accent2);font-size:12.5px;font-weight:700;text-decoration:underline;text-underline-offset:2px;cursor:pointer;padding:0 2px}
.fh-btns{display:flex;gap:8px;width:100%} .fh-btns button{flex:1}
/* veteran self-declared skip — intro link + persistent in-game bar (Nathan) */
.tm-skiplink{display:block;width:100%;margin-top:2px;background:none;border:none;color:var(--dim);font-size:13px;cursor:pointer;padding:6px}
.tm-skiplink b{color:var(--accent2)}
/* Review-these-intervals — prominent button in the space under the small (3×3) grid; open any time, no penalty (Nathan) */
.ivreview-row{margin:14px 0 4px;text-align:center}
.ivreview-btn{width:100%;max-width:340px;background:var(--panel2);border:1px solid var(--bks-cta,#2dacb3);border-radius:12px;color:var(--ink);font-size:15px;font-weight:700;padding:13px 16px;cursor:pointer}
.ivreview-btn:hover,.ivreview-btn:active{background:var(--bks-cta,#2dacb3);color:#04232a}
.tm-skipbar{margin:12px 0 4px;text-align:center}
.tm-skipbtn{background:var(--panel2);border:1px solid var(--line);border-radius:10px;color:var(--dim);font-size:12.5px;font-weight:600;padding:8px 14px;cursor:pointer;max-width:100%}
.tm-skipbtn b{color:var(--accent2)} .tm-skipbtn:hover{border-color:var(--accent2)}
.gambit-bubble::after{content:'';position:absolute;left:50%;transform:translateX(-50%);border:9px solid transparent}
.gambit-cap:not(.below) .gambit-bubble::after{bottom:-9px;border-top-color:#e8c14a;border-bottom:0}
.gambit-cap.below .gambit-bubble::after{top:-9px;border-bottom-color:#e8c14a;border-top:0}
/* slim progress line (replaces the old clues block) */
/* above-grid coaching message (which square to work / wild prompts / bingo win) */
.gridmsg{font-size:13.5px;color:var(--dim);line-height:1.5;margin:2px 0 6px;min-height:58px;box-sizing:border-box;display:flex;align-items:center;justify-content:center;text-align:center;position:sticky;top:calc(max(env(safe-area-inset-top,0px) - 6px, 8px) + 42px);z-index:6;background:var(--bg);padding:4px 2px;border-radius:8px}   /* FIXED reserved height (Nathan's giant pet peeve): covers the tallest state — the 2-line Mulligan/spend .beat banner (8px 14px pad) — so NO message ever shoves the grid up/down. border-box so the banner's bigger padding fits the same box. */   /* block (NOT flex) so spaces around <b> words survive — flex trims inter-item whitespace → "PlacingClue1" */
.gridmsg b{color:var(--ink)}
.gridmsg.act:not(.gm-flex){display:block;line-height:1.45;padding:7px 12px}   /* 📱 mulligan/spend messages are plain inline text (<b> + text + button) — force BLOCK so they FLOW & wrap normally instead of the base flex spreading each word/child apart on phone (Nathan) */
.gridmsg.act{color:var(--accent2)} .gridmsg.act b{color:var(--accent2)}
.gridmsg.win{color:var(--good);font-weight:700;font-size:15px} .gridmsg.win b{color:var(--good)}
.gridmsg .gm-x{margin-left:8px;background:transparent;border:1px solid currentColor;color:inherit;border-radius:50%;width:20px;height:20px;font-size:11px;line-height:1;cursor:pointer;opacity:.65;padding:0;vertical-align:middle}
.gridmsg .gm-x:active{opacity:1}
.modal-btns .primary.ddDanger{background:#c0392b;border-color:#c0392b;color:#fff}   /* destructive action — the "Delete my data" confirm */
.tunewrap{display:inline-flex;align-items:center;gap:6px}
.tunewrap input[type=number]{width:78px;padding:6px 8px;border-radius:8px;border:1px solid var(--line);background:var(--panel2);color:var(--ink);font-size:14px;font-variant-numeric:tabular-nums;text-align:right}
.tunewrap small{color:var(--dim);font-size:11px}
.lbrow .lblevel{flex:none;min-width:48px;text-align:center}
.lbrow .lbmode{flex:none;min-width:76px;text-align:right;color:var(--dim);font-size:12px;white-space:nowrap}
.climb{color:#3ecf7a;font-weight:800;font-size:12px}   /* ↗ they've gained points this session */
.lb-online,#navOnline{cursor:pointer}
.lb-explain{color:var(--dim);font-size:12px;margin:2px 0 10px;line-height:1.45}
.dqperfect{margin:4px 0 6px;color:#e8c14a;font-weight:800;font-size:17px;text-align:center;text-shadow:0 0 12px rgba(232,193,74,.4)}
.landfeed{margin:14px 0 6px}
.landfeed-h{font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--dim);font-weight:800;margin:0 0 6px}
.feedrow{display:flex;align-items:center;gap:8px;padding:6px 10px;border:1px solid var(--line);border-radius:10px;margin-bottom:6px;background:var(--panel2);font-size:12.5px;line-height:1.3;animation:feedin .35s ease}
.feedrow .feedtxt{flex:1;min-width:0}
.feedago{color:var(--dim);font-size:11px;font-weight:500;white-space:nowrap}   /* relative time on Right-now actions (Nathan) */
.fheart{flex:0 0 auto;background:transparent;border:1px solid var(--line);color:#ff6b8a;border-radius:999px;width:30px;height:30px;font-size:15px;line-height:1;cursor:pointer;transition:transform .12s}
.fheart:active{transform:scale(1.2)}
.fheart.sent{border:none;background:transparent;font-size:16px;display:inline-grid;place-items:center;width:30px;height:30px}
.fheart.sent.pop{animation:heartpop .5s ease}
@keyframes heartpop{0%{transform:scale(.4)}60%{transform:scale(1.4)}100%{transform:scale(1)}}
@keyframes feedin{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
.cluebar-thin{height:4px;background:var(--panel2);border:1px solid var(--line);border-radius:3px;overflow:hidden;margin:0 0 10px}
.cluebar-thin span{display:block;height:100%;background:var(--accent)}
.gambit-tip{display:flex;gap:10px;align-items:center;margin:10px 0;padding:10px 12px;border-radius:10px;
  background:#2a1c0a;border:1px solid #e8a04a;color:#ffe0b0;font-size:13px;line-height:1.35}
.gambit-tip .mini{flex:0 0 auto}
.chordcard.cleared{opacity:.92;border-color:var(--good)!important}
.cell.gambit-why{border-color:#7fd6db!important;box-shadow:0 0 0 2px #7fd6db,0 0 14px rgba(127,214,219,.6)}   /* "here's the interval that WAS on your grid" after a failed gambit */
.cell.bonusfree{border-color:#e8c14a}
.cell.bonusfree .iv{color:#e8c14a}   /* a converted (won-gambit) free square — distinct from the built-in one */
.cell.swapped{position:relative}
.cell.swapped::after{content:"⇄";position:absolute;top:2px;left:3px;font-size:11px;line-height:1;color:#e8c14a;opacity:.85;pointer-events:none}   /* replay marker: this tile came from a won-gambit exchange */
.cell.freelocked{border-color:#ff6b6b!important;background:#2a1418}
.cell.freelocked .iv{opacity:.5;text-decoration:line-through}
.cell.freelocked .freelock{font-size:9px;color:#ff6b6b;font-weight:800;letter-spacing:.5px;margin-top:2px}
.clues h3{margin-bottom:6px}
.cluebar{height:6px;border-radius:4px;background:var(--panel2);border:1px solid var(--line);overflow:hidden;margin:0 0 8px}
.cluebar span{display:block;height:100%;background:var(--good);transition:width .3s ease}
/* solved-clue keyboard IS playable (only the chord notes sound; scrub across them); tile diagrams never playable */
.sqkb{pointer-events:none}
.cell .sqkb,.cell .sqkb svg,.cell .sqkb svg *{pointer-events:none!important}   /* the grid keyboard must NOT eat taps (svg{pointer-events:auto} was re-enabling it) — the whole open tile places */
.chordcard .ghost{width:100%}
.chordcard.playable{user-select:none;touch-action:pan-y}   /* 🆕 Nathan: the growing clue cards must scroll the column when you drag ANYWHERE outside the keyboard — only the piano (.minikb, below) is the play target. Was touch-action:none, which ate every vertical drag on the card and made the column scroll almost impossible to grab. The carousel keeps pan-x via its more-specific rule above. */
.cc-play{text-align:center;color:var(--dim);font-size:12px;padding:10px 4px;border:1px dashed var(--line);border-radius:8px}
.chordcard:hover .cc-play{color:var(--ink);border-color:var(--accent)}
.cc-notes{margin-top:6px;font-weight:600;text-align:center}
.chordcard.cc-revealed .minikb{max-width:100%}   /* 🎹 (Nathan) revealed MP clue → full-width labeled keyboard so a class can actually SEE + discuss the notes (the reveal was a cramped waste of space) */
.chordcard.cc-revealed .cc-notes{font-size:15px;letter-spacing:.04em;color:#e6f2f5;margin-top:4px}   /* pitch names, prominent + legible for group discussion */
.fnew{display:inline-block;font-size:9.5px;font-weight:800;letter-spacing:.04em;color:#8ef0b6;background:rgba(62,207,122,.16);border:1px solid rgba(62,207,122,.55);border-radius:5px;padding:1px 5px;vertical-align:1px}   /* 🆕 "✨ new" chip — dark-bg green-text so it reads on the dark feed row (Nathan: bright-green bg was hard to read) */
.chordcard.inert{opacity:.4;filter:grayscale(.55);cursor:default}
/* phone carousel: status dots + one active clue card (iPad keeps the full .strip) */
.carousel{margin:4px 0 4px}
.cardots{display:flex;flex-wrap:wrap;gap:10px;justify-content:center;margin-bottom:12px;touch-action:manipulation}
/* Numberless STATUS PIPS: small dots spread across the full width — a purely visual count of how many puzzles
   there are + each one's state. open = chord-color ring · solved = filled · wrong = red · revealed = dashed. */
.cardots{gap:6px;justify-content:space-between;padding:0 4px;max-width:340px;margin-left:auto;margin-right:auto;width:100%;box-sizing:border-box}
.cardot{width:12px;height:12px;border-radius:50%;border:2px solid var(--line);background:transparent;font-size:0;line-height:0;padding:0;cursor:pointer;flex:0 0 auto;opacity:.72;transition:opacity .15s,transform .15s,box-shadow .15s}   /* off-focus dots dimmed but status still reads; the current one pops */
/* Legible-at-11px status language (dashes didn't read): HOLLOW ring = still to solve · SOLID = resolved.
   Color says how it resolved — chord-color = solved, red = wrong lock, grey = revealed/given up. */
.cardot.open{border-color:var(--dc);background:transparent}
.cardot.solved{background:var(--dc);border-color:var(--dc);opacity:.95}
.cardot.wrong{background:#ff6b6b;border-color:#ff6b6b;opacity:.95}
.cardot.revealed{background:transparent;border-color:var(--dim);opacity:.72}
/* current = a bright, larger ring with a halo; pops even though the others are dimmed AND keeps its status fill */
.cardots .cardot.cur{opacity:1;transform:scale(1.4);box-shadow:0 0 0 2px var(--bg),0 0 0 4px var(--dc)}
.carstage{display:flex;align-items:center;gap:6px;justify-content:center}   /* 📐 THE NO-JUMP GUARANTEE LIVES HERE NOW (2026-10-09), not inside every card. One floor on the stage holds the board still whether the active card is short or tall, where the old per-card reservation made EVERY card tall and cost ~50px of screen. 166px = the tallest card measured (157.4) plus its breathing room. */
.carscroll{flex:1 1 auto;min-width:0;display:flex;align-items:flex-start;gap:12px;overflow-x:auto;scroll-snap-type:x mandatory;-webkit-overflow-scrolling:touch;scrollbar-width:none;padding:2px 0}   /* align-items:flex-start — cards size to THEIR content, not stretched to the tallest clue (killed ~70px of dead space under short cards)
.carscroll::-webkit-scrollbar{display:none}
.carscroll::before,.carscroll::after{content:'';flex:0 0 11%}   /* breathing room so clue 1 AND the LAST clue can CENTER — 11% (was 8%, ≈ the exact centering amount with ~0 margin, so the last card landed a hair left); the extra slack guarantees full centering */
.carslide{flex:0 0 84%;scroll-snap-align:center;scroll-snap-stop:always;opacity:.45;transform:scale(.93);transition:opacity .2s,transform .2s}   /* next card peeks; non-centered fade back; snap-stop:always = ONE card per swipe (no skipping) */
.carslide.cur{opacity:1;transform:none}
.carslide .chordcard{margin:0}
.carscroll{touch-action:pan-x}
.carslide .chordcard.playable{touch-action:pan-x}   /* let a horizontal swipe scroll the carousel (tap still plays) */
.carslide:not(.cur){cursor:pointer}   /* tap a peeking card to bring it into view */
.chordcard .minikb{touch-action:none;cursor:pointer}   /* the PIANO is the play target — sound is intentional */
/* robber reward: reclaim a locked tile */
.cell.unlockhere{animation:unlockpulse 1s ease-in-out infinite}
@keyframes unlockpulse{0%,100%{box-shadow:0 0 0 2px #3ecf7a inset,0 0 8px #3ecf7a44}50%{box-shadow:0 0 0 3px #3ecf7a inset,0 0 18px #3ecf7a99}}
.convert-banner{margin-top:10px;padding:9px 12px;border-radius:10px;background:#123028;border:1px solid #3ecf7a66;color:#bff0d6;font-size:13px;text-align:center;line-height:1.35}
.convert-banner b{color:#7ef0b4}
.winpop{position:fixed;left:50%;top:37%;z-index:9999;pointer-events:none;white-space:nowrap;
  font-weight:800;font-size:22px;color:#3ecf7a;background:rgba(8,20,24,.94);border:1px solid rgba(62,207,122,.55);border-radius:14px;padding:12px 22px;box-shadow:0 12px 44px rgba(0,0,0,.55),0 0 24px rgba(62,207,122,.22);text-shadow:0 1px 6px #0009;transform:translate(-50%,-50%) scale(.6);animation:winpopin .28s cubic-bezier(.2,1.5,.4,1) forwards}
.winpop.out{animation:winpopout .3s ease forwards}
@keyframes winpopin{to{transform:translate(-50%,-50%) scale(1)}}
@keyframes winpopout{to{opacity:0;transform:translate(-50%,-58%) scale(1.05)}}
.carnav{flex:0 0 auto;width:34px;height:128px;border-radius:11px;border:1px solid var(--line);background:var(--panel2);color:var(--accent2);font-size:24px;line-height:1;cursor:pointer;padding:0}
.carnav:active{background:var(--panel)}
@media(hover:none){.carnav{display:none}}   /* touch devices swipe; arrows are for mouse */
.tally{font-size:11px;margin-top:6px;text-align:center;line-height:1.4}
.tally .tc{color:var(--good);font-weight:700}
.tally .tw{color:var(--bad);font-weight:700}
.tally .tm{color:var(--accent)}
.tally .tdone{color:#7fd6db;font-weight:700}
.tally .tall{color:#e8c14a;font-weight:800}
.sqkb{margin-top:6px;background:#f3f6f6;border-radius:4px;padding:2px;width:100%}
/* grid mini-keyboards: same consistent-aspect fix as the clue cards — drive by a definite height so keys keep
   their natural proportion (no tall/stubby mismatch between a 2-key and a wide tile), width auto + meet-fit. */
.sqkb svg,.sqkb .piano{display:block;width:100%;height:auto;max-height:70px}   /* fit by width → C-aligned keyboard never clipped on the sides */

.grid{display:grid;gap:8px;max-width:560px;width:100%;position:relative}
.strikeline{position:absolute;height:6px;margin-top:-3px;transform-origin:left center;border-radius:3px;pointer-events:none;z-index:6;
  background:linear-gradient(90deg,#e8c14a,#fff3c0,#e8c14a);box-shadow:0 0 12px rgba(232,193,74,.9)}
.cell.winline{border-color:#e8c14a!important;box-shadow:0 0 14px rgba(232,193,74,.7)!important}
/* BINGO banner floats as a fixed TOP toast — overlays the top of the screen (visible immediately), never
   pushes the grid/strikethrough down. */
.trophy{position:fixed;left:12px;right:12px;top:auto;bottom:calc(118px + env(safe-area-inset-bottom));z-index:200;   /* LOWER (Nathan): sits above the bottom bar so collected points visibly fly UP to the cup */
  text-align:center;font-weight:800;letter-spacing:1px;color:#08252b;border-radius:12px;padding:12px;
  font-size:18px;max-width:560px;margin:0 auto;box-shadow:0 10px 30px rgba(0,0,0,.55);
  background:linear-gradient(90deg,#e8c14a,#ffe08a,#e8c14a);animation:trPop .35s ease}
@keyframes trPop{from{opacity:0;transform:translateY(-16px)}to{opacity:1;transform:none}}
.trophy .tr-main{font-size:20px;font-weight:800;letter-spacing:1px}
.trophy .tr-sub{font-size:13px;font-weight:600;letter-spacing:.2px;margin-top:4px;opacity:.85}
.trophy .winchords{display:inline-flex;gap:6px;vertical-align:middle;margin-left:6px}
.trophy .winchords .bul{border-color:rgba(0,0,0,.35)}
.cell{background:var(--panel2);border:1px solid var(--line);border-radius:10px;padding:8px 6px;
      display:flex;flex-direction:column;align-items:center;justify-content:space-between;
      aspect-ratio:1;min-height:0;overflow:hidden;text-align:center;position:relative}   /* always a fixed square — placing a guess never grows it */
.cell .iv{font-size:clamp(15px,2.4vw,20px);font-weight:700;line-height:1}
.cell .lg{font-size:10px;color:var(--dim);margin-top:3px;line-height:1.1}
.cell input{width:100%;margin-top:6px;background:#0a1a20;border:1px solid var(--line);color:var(--ink);
      border-radius:7px;text-align:center;font-size:15px;padding:5px 2px;outline:none}
.cell input:focus{border-color:var(--accent)}
.cell input:disabled{opacity:.6}
.cell.free{background:var(--free);justify-content:center}
.cell.free .iv{color:var(--accent2)}
.cell.ok{border-color:var(--good);box-shadow:inset 0 0 0 1px var(--good)}
/* a wrong answer makes the tile ineligible for bingo — tint it red + faint diagonal hatch so that reads clearly */
.cell.err{border-color:var(--bad);box-shadow:inset 0 0 0 1px var(--bad);
  background-image:linear-gradient(135deg,rgba(255,107,107,.13) 0 25%,transparent 25% 50%,rgba(255,107,107,.13) 50% 75%,transparent 75%);
  background-size:14px 14px}
.cell.pop{animation:cellpop .36s ease-out}
@keyframes cellpop{0%{transform:scale(1)}30%{transform:scale(1.09);box-shadow:inset 0 0 0 2px var(--good),0 0 14px rgba(62,207,122,.55)}100%{transform:scale(1)}}
.cell.dead{opacity:.6;cursor:not-allowed;background:#2a1a1c}
.cell.dead::after{content:"locked";position:absolute;bottom:3px;right:5px;font-size:9px;letter-spacing:.04em;text-transform:uppercase;color:var(--bad);font-weight:700}
.cell{position:relative}
.cell.bonus{border-color:#e8c14a!important;animation:bonuspulse 1.4s ease-in-out infinite}
@keyframes bonuspulse{
  0%,100%{box-shadow:inset 0 0 0 2px #e8c14a,0 0 5px rgba(232,193,74,.3)}
  50%{box-shadow:inset 0 0 0 2px #e8c14a,0 0 16px rgba(232,193,74,.8)}
}
.sparkfx{position:fixed;pointer-events:none;font-weight:700;z-index:9999;will-change:transform,opacity;animation:sparkle 1000ms ease-out forwards}
@keyframes sparkle{0%{opacity:0;transform:translate(0,0) scale(.4)}15%{opacity:1}100%{opacity:0;transform:translate(var(--dx),var(--dy)) scale(1.25)}}
.mark{position:absolute;top:4px;right:6px;font-size:13px;font-weight:700}
.mark.ok{color:var(--good)} .mark.err{color:var(--bad)}

.playbar{display:flex;align-items:stretch;gap:14px;margin-top:16px;flex-wrap:wrap}
.playbar>button{display:inline-flex;align-items:center;justify-content:center}   /* stretch = every button on the row matches the tallest (End round / Hint / Training all equal height) */
.status{color:var(--dim);font-size:13px}
/* footers: wrap as WHOLE links (never break a label mid-phrase; the "·" separator rides with the next
   link so it never dangles at a line end) */
.foot{margin-top:22px;display:flex;flex-wrap:wrap;align-items:center;gap:6px 0}
.link{white-space:nowrap}
/* 🐛 THE UNDERLINED BULLET. .link carries text-decoration:underline and a ::before inherits it, so
   the separator was drawn with a little line under it — Nathan: "the bullet that separates those items
   is underlined, looks odd." It is punctuation, not part of the link text.
   Also: the row wrapped mid-list and left "← leave" hanging on its own line against the left edge
   while the rest sat centerd. Center the whole row and let it wrap as a unit. */
.foot{justify-content:center;row-gap:8px}
/* 🔊 THE SEPARATOR WAS BEING READ ALOUD. A ::before's content lands in the ACCESSIBLE NAME, so
   VoiceOver announced "dot undo", "dot names on", "dot deal again" — confirmed in the AX tree. CSS
   generated content cannot be aria-hidden, so the divider must not be text at all. A border does the
   same job visually and is invisible to a screen reader. */
.foot .link ~ .link{border-left:1px solid var(--line);padding-left:14px;margin-left:14px}
/* SETTINGS foot — long destructive links (Restart/Reset/Delete + the loud restart CONFIRM) stack vertically, WRAP instead
   of running off the right edge, and drop the inline · separators that became stray leading dots when stacked (Nathan). */
.foot-settings{flex-direction:column;align-items:flex-start;gap:10px}
.foot-settings .link{white-space:normal;text-align:left;max-width:100%;overflow-wrap:anywhere}
.foot-settings .link ~ .link::before{content:none;margin:0}
.cta-block{max-width:1140px;margin:26px auto 40px;padding:16px 18px;border-top:1px solid var(--line);
  color:var(--dim);font-size:14px;line-height:1.6}
.cta-block b{color:var(--accent2)}
.cta-about{color:#999;font-size:13px;line-height:1.5;margin:16px 0 4px}
.cta-about b{color:var(--accent2)}
.cta-links{display:flex;gap:18px;flex-wrap:wrap;margin-top:10px}
.cta-block a{color:var(--accent);text-decoration:none;font-weight:600}
.cta-block a:hover{text-decoration:underline}
@media(max-width:600px){.cta-links{display:flex;margin:8px 0 0}}

/* live "Now Playing" + next-up events banner (shared /js/upcoming-events.js, dark-themed for this page) */
.ib-events{margin:12px 0}
.ib-events:empty{display:none}
.ib-events-h{font-size:12px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent2);font-weight:600;margin-bottom:8px}
/* site-wide standard: a clean VERTICAL LIST — headshot · title+times · countdown, hairline dividers (matches the Harmony Wheel) */
.ib-events .upcoming-events-cards{display:flex;flex-direction:column;gap:0;margin:0}
.ib-events .upcoming-event-card{display:flex;align-items:center;gap:12px;
  background:none;border:none;border-radius:0;padding:11px 2px;
  text-decoration:none;color:#ccc;transition:background .15s}
.ib-events .upcoming-event-card + .upcoming-event-card{border-top:1px solid rgba(255,255,255,0.1)}
.ib-events .upcoming-event-card:hover{background:rgba(255,255,255,0.05)}
.ib-events .upcoming-event-card.upcoming-event-live{background:rgba(45,172,179,0.08)}
.ib-events .instructor-headshot-sm{width:46px;height:46px;border-radius:50%;object-fit:cover;flex:0 0 auto}
/* CTA face cluster — real instructor headshots (window.BKS_INSTRUCTOR_PHOTOS), overlapping row */
.cta-faces{display:flex;justify-content:center;align-items:center;margin:0 0 10px;padding-left:9px}
.cta-faces .instructor-headshot-sm{width:38px;height:38px;border-radius:50%;object-fit:cover;margin-left:-9px;border:2px solid rgba(18,22,28,.92);box-shadow:0 1px 3px rgba(0,0,0,.45)}
.ib-events .upcoming-event-text{flex:1;min-width:0;font-size:14px;line-height:1.4}
.ib-events .upcoming-event-title{font-weight:700;color:#fff}
.ib-events .upcoming-instructor{color:#2DACB3;font-size:13px}
.ib-events .upcoming-meta{display:block;color:#999;font-size:12.5px;margin-top:2px}
.ib-events .upcoming-countdown{flex:0 0 auto;margin-left:auto;color:#2DACB3;font-size:13px;font-weight:700;
  font-variant-numeric:tabular-nums;text-align:right;white-space:nowrap}
.ib-events .upcoming-countdown--soon{color:#e8b04a}
.ib-events .upcoming-countdown--live{color:var(--bad)}
.ib-events .upcoming-today{display:inline-block;background:#e8862a;color:#fff;font-size:10px;font-weight:700;
  padding:1px 5px;border-radius:3px;letter-spacing:.04em;vertical-align:middle;margin-right:4px}
.ib-events .upcoming-live-badge{display:inline-flex;align-items:center;gap:5px;font-size:11px;font-weight:700;
  text-transform:uppercase;letter-spacing:.04em;color:var(--bad);margin-right:4px}
.ib-events .upcoming-live-badge .live-dot{width:8px;height:8px;border-radius:50%;background:var(--bad);
  display:inline-block;animation:ib-live-blink 1.2s ease-in-out infinite}
.ib-events .upcoming-join{color:var(--accent);font-weight:600;font-size:12px}
@keyframes ib-live-blink{0%,100%{opacity:1}50%{opacity:.2}}

/* ---- promo rotator (one cycling card slot: classes / events / products) ---- */
.pr-h{display:flex;align-items:center;gap:8px}
.pr-dots{display:flex;gap:5px;margin-left:auto}
.pr-dots i{width:6px;height:6px;border-radius:50%;background:var(--line);display:block;cursor:pointer;transition:background .2s}
.pr-dots i.on{background:var(--accent)}
.pr-slot{margin-top:2px}
.pr-row{display:flex;gap:10px;align-items:stretch}
.pr-card{flex:1 1 0;min-width:0;display:flex;gap:12px;align-items:stretch;background:var(--panel);border:1px solid var(--line);
  border-radius:14px;overflow:hidden;min-height:80px;text-decoration:none;color:var(--ink);
  transition:border-color .15s,background .15s}
.pr-card:hover{background:var(--panel2)}
.pr-media{width:80px;flex:0 0 80px;background-size:cover;background-position:center;
  display:flex;align-items:center;justify-content:center}
.pr-media .pr-icon{width:54px;height:54px;border-radius:14px;display:block}
.pr-media .pr-head{width:52px;height:52px;border-radius:50%;object-fit:cover;display:block}
.pr-body{flex:1;min-width:0;padding:10px 13px 10px 0;display:flex;flex-direction:column;justify-content:center}
.pr-tag{font-size:9.5px;font-weight:800;letter-spacing:.07em;text-transform:uppercase;margin-bottom:3px;color:var(--accent2)}
.pr-title{font-size:15.5px;font-weight:800;color:#fff;line-height:1.13}
.pr-sub{font-size:12.5px;color:var(--dim);margin-top:1px}
.pr-foot{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:7px}
.pr-meta{font-size:12px;color:var(--accent2)}
.pr-btn{background:var(--accent);color:var(--accent-ink);border-radius:7px;padding:6px 12px;
  font-size:12.5px;font-weight:700;white-space:nowrap;flex:0 0 auto}
/* per-type accents */
.pr-class .pr-media{border-left:3px solid #F08640} .pr-class .pr-tag{color:#F08640}
.pr-event .pr-media{border-left:3px solid var(--accent)}
.pr-live{background:rgba(45,172,179,0.08)} .pr-live .pr-tag{color:var(--accent)}
.pr-product .pr-media{border-left:3px solid var(--accent2)} .pr-product .pr-tag{color:var(--accent2)}
.pr-msg .pr-media{border-left:3px solid #e8b04a} .pr-msg .pr-tag{color:#e8b04a}
/* live countdown + TODAY cue — parity with the site / Harmony Wheel / Tetrachord toolkit event cards */
.pr-cd{font-weight:700;color:var(--accent2);font-variant-numeric:tabular-nums}
.pr-cd--soon{color:#F08640}   /* under 2h */
.pr-cd--live{color:#e74c3c}
.pr-today-pill{display:inline-block;background:#F08640;color:#0d2027;font-weight:800;font-size:8.5px;letter-spacing:.06em;padding:1px 5px;border-radius:4px;margin-right:5px;vertical-align:1px}
.pr-card.pr-today{outline:1.5px solid #F08640;outline-offset:-1px;background:rgba(240,134,64,0.06)}
.pr-card.pr-today .pr-media{border-left-color:#F08640}

/* "N playing now" live indicator on the leaderboard header */
.lb-online{font-size:12px;font-weight:600;color:var(--accent2);vertical-align:middle;margin-left:8px;white-space:nowrap}
.lb-online:empty{display:none}
.lb-online .lbdot{display:inline-block;width:8px;height:8px;border-radius:50%;background:#2ecc71;margin-right:5px;vertical-align:middle;animation:lbpulse 2s infinite}
.lb-online.refreshing{opacity:.45}
.nudge-future{font-size:12.5px;color:var(--dim);font-style:italic;margin-top:-4px}
@keyframes lbpulse{0%{box-shadow:0 0 0 0 rgba(46,204,113,.55)}70%{box-shadow:0 0 0 6px rgba(46,204,113,0)}100%{box-shadow:0 0 0 0 rgba(46,204,113,0)}}

/* ---- mobile ---- */
@media(max-width:600px){
  .brandbar{padding:calc(env(safe-area-inset-top, 0px) + 4px) 12px 0}   /* phone: honor the notch on EVERY bare screen (Classic/Progress/Daily) — the old flat 12px rode the menu UP behind the cutout so its buttons sat under the clock, unclickable (Nathan) */
  .brandbar img{height:24px}
  .brandbar .wm{font-size:13px}
  #app{padding:12px 12px 60px}
  .hostbar{flex-wrap:wrap;gap:6px 8px}   /* title + score on line 1; the meter drops to its OWN row — nowrap made the full-width meter (flex-basis:100%) overlap the title/★ chip (Nathan) */
  .hb-title{flex:1 1 auto}               /* shrink so the score cluster sits beside the title, not below */
  .hb-right{flex:0 1 auto;min-width:0}   /* 🩹 SHRINK + scroll (overflow-x:auto from base), never WRAP — winning a Mulligan added the 🐦 badge and the old flex:0 0 auto pushed the cluster onto a NEW ROW, growing the header and shoving the board down. Now the badges stay on the right and scroll horizontally — zero vertical shift (Nathan's #1 pet peeve). */
  .hb-meter{flex-basis:100%;order:9}     /* force the progress bar onto its own full-width row */
  .code{font-size:20px}
  .strip{gap:8px}
  .chordcard{flex:1 1 116px;padding:11px}
  .chordcard .listen{padding:8px 6px}
  .grid{gap:6px}
  .cell{min-height:74px;padding:7px 4px}
  .cell .iv{font-size:17px}
  .cell .lg{font-size:10px;margin-top:2px}
  .cell input{font-size:15px;padding:4px 3px;margin-top:4px}
  .cnum{top:2px;left:3px;min-width:16px;height:16px;font-size:11px}
  .mark{font-size:12px;top:2px;right:4px}
  .btnrow{gap:3px;margin-top:4px}
  .mini{padding:7px 6px;font-size:13px}
  .cfg{gap:10px}
  /* bigger tap targets — checkboxes, bullets, buttons read as too small on iPhone */
  input[type=checkbox]{width:22px;height:22px}
  .setup input[type=checkbox]{width:22px;height:22px}
  .bul{width:30px;height:30px;font-size:14px}
  .hintbox,.tgl{font-size:14px}
  .settingsrow{gap:10px}
  .btn,.ghost,.primary,.danger{padding:11px 14px;font-size:15px}
  .big{padding:18px 14px;font-size:16px}
  .sub{font-size:14px}
  h3{font-size:12px}
}
/* landscape phones: give the grid more room, keep it app-like */
@media(max-width:900px) and (orientation:landscape){
  #app{padding:10px 14px 50px}
  .cell{min-height:56px}
}
/* installed / standalone (Add-to-Home-Screen or the native wrapper): edge-to-edge, respect the notch,
   drop the panel chrome so it reads like a native app */
/* wide screens / iPad / desktop — keep the card comfortable, don't stretch it edge-to-edge */
@media(min-width:820px){
  .play .grid, .play .strip{max-width:640px;margin-left:auto;margin-right:auto}
  .play .cell{min-height:92px}
  .play .cell .iv{font-size:22px}
  .play h3, .play .clues, .play .placebar, .play .playbar, .play .bonusbar, .play .foot, .play .hostbar{max-width:900px;margin-left:auto;margin-right:auto}
  .panel{max-width:720px;margin-left:auto;margin-right:auto}
}
@media all and (display-mode: standalone){
  #app{padding-left:max(10px, env(safe-area-inset-left));padding-right:max(10px, env(safe-area-inset-right));padding-bottom:calc(60px + env(safe-area-inset-bottom))}
  .panel{border-left:none;border-right:none;border-radius:0;padding-left:12px;padding-right:12px}
  .cta-block{border-top-color:transparent}
}

/* ── Sign-in with Discord (landing) ── reuses the portal's OAuth; Discord blurple accent ── */
.authrow{margin:0 0 14px}
.authbtn{width:100%;box-sizing:border-box;display:flex;flex-direction:column;gap:2px;text-align:left;
  background:#5865F2;border:1px solid #4752c4;color:#fff;border-radius:12px;padding:12px 14px;
  font-size:15px;font-weight:600;cursor:pointer}
.authbtn small{font-weight:400;color:#dfe2fb;font-size:12.5px}
.authbtn:hover{background:#4752f0}
.authbtn + .authbtn, .authbtn + .authbtn.joinbtn{margin-top:10px}   /* two stacked purple auth buttons were touching -> read as one; give them a gap */
.authpill{display:flex;align-items:center;justify-content:space-between;gap:10px;
  background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:10px 14px}
.authpill.signedin{border-color:var(--accent)}
.authwho{color:var(--ink);font-size:14px}
.authwho b{color:var(--accent2)}
.authtier{display:inline-block;margin-left:6px;padding:1px 7px;border-radius:9px;background:var(--accent);
  color:var(--accent-ink);font-size:11px;font-weight:700;text-transform:capitalize;vertical-align:middle}
.authpill .link{white-space:nowrap}

/* ── Daily quiz button (landing) ── */
.big.daily{border-color:var(--accent);background:#12333c;margin-bottom:14px}
.big.daily small{color:var(--accent2)}

/* ── Interval palette (click-to-hear) ── */
.palette{display:grid;grid-template-columns:repeat(auto-fill,minmax(92px,1fr));gap:8px;margin-top:6px}
.palchip{display:flex;flex-direction:column;align-items:center;gap:2px;padding:12px 6px;cursor:pointer;
  background:var(--panel2);border:1px solid var(--line);border-radius:12px;color:var(--ink);transition:border-color .12s,transform .08s}
.palchip b{font-size:18px;font-weight:700}
.palchip span{font-size:11px;color:var(--dim);text-align:center;line-height:1.15}
/* 🎨 the palette is the screen where the visual language is TAUGHT, so it carries it: each chip wears
   its family colour, and its label goes through ivGlyph so the M/m is coloured like everywhere else. */
.palchip.fam-sweet{border-color:color-mix(in srgb,var(--fam-sweet) 55%,transparent)}
.palchip.fam-savory{border-color:color-mix(in srgb,var(--fam-savory) 55%,transparent)}
.palchip.fam-salty{border-color:color-mix(in srgb,var(--fam-salty) 55%,transparent)}
.palchip:hover{border-color:var(--accent)}
.palchip.ring{border-color:var(--accent);background:#154049;transform:scale(1.04)}

/* ── Daily quiz screen ── */
.dqhead{display:flex;align-items:baseline;justify-content:space-between;gap:10px;flex-wrap:wrap}
.dqmeta{color:var(--dim);font-size:13px;font-variant-numeric:tabular-nums}
.big.dqplay{margin:6px 0 12px;border-color:var(--accent);background:#12333c;text-align:center}
/* NOTE-SPACING dial = difficulty; points scale with it (Nathan) */
.dq-space{background:var(--panel2);border:1px solid var(--line);border-radius:12px;padding:10px 14px;margin:0 0 12px}
.dq-space-lbl{font-size:13px;font-weight:700;color:var(--ink);display:flex;align-items:center;gap:8px;margin-bottom:6px}
.dq-space-lbl #dqSpaceWord{color:var(--accent2);font-weight:800}
.dq-space .dq-mult{margin-left:auto;font-size:12px;font-weight:800;color:var(--band-3);font-variant-numeric:tabular-nums}
.dq-space input[type=range]{width:100%;accent-color:var(--accent);touch-action:manipulation}
.dq-space-ends{display:flex;justify-content:space-between;font-size:10.5px;color:var(--dim);margin-top:2px;text-transform:uppercase;letter-spacing:.05em}
.dqopts{display:grid;gap:10px}
.dqopt{display:flex;flex-direction:column;align-items:center;gap:2px;padding:16px 8px;cursor:pointer;
  background:var(--panel2);border:1px solid var(--line);border-radius:12px;color:var(--ink);transition:border-color .12s}
.dqopt b{font-size:22px;font-weight:700}
.dqopt span{font-size:12px;color:var(--dim)}
@media (hover:hover){.dqopt:hover:not(:disabled){border-color:var(--accent)}}   /* desktop only — sticky touch-hover was 'leading' the last-tapped option (Nathan) */
.dqopt:focus:not(:disabled){outline:none}   /* a focus ring on load reads as a pre-selected answer — suppress it */
.singdone{display:block;margin:14px auto 0;min-width:200px}
.setacct{font-size:14px;color:var(--ink);display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.setacct b{color:var(--accent2)}
button.working{opacity:.85;animation:btnworking 1s ease-in-out infinite}
@keyframes btnworking{0%,100%{opacity:.85}50%{opacity:.55}}
/* sing result: group the "listen again" ghost buttons, then a clear gap before the single move-on button */
.singlisten{flex-wrap:wrap;justify-content:center;gap:8px}
/* ⚠️ .lbcta gives every child flex:1, which is right for TWO buttons and wrong for four: each one
   squeezed to a quarter of the row and "What you sang" became three stacked words in a tall column.
   Here they take their natural width and wrap to a second ROW instead. (Seen on sight, 2026-10-05.) */
.singlisten .ghost{flex:0 1 auto;min-width:0;white-space:nowrap}
/* 🔔 the notification ask — real example messages, ticked or not. The examples are quoted so they read
   as things the phone would say rather than as settings labels. */
.nxmodal .nx-lead{ margin:14px 0 6px; color:var(--dim); font-size:13px }
.nxmodal .nx-row{ display:flex; align-items:flex-start; gap:10px; padding:9px 10px; margin-bottom:6px;
  border:1px solid var(--line); border-radius:10px; cursor:pointer; text-align:left }
.nxmodal .nx-row input{ margin-top:2px; flex:0 0 auto }
.nxmodal .nx-eg{ font-size:13.5px; line-height:1.35; color:var(--fg) }
.nxmodal .nx-fine{ display:block; margin-top:12px; color:var(--dim); font-size:11.5px; text-align:center }

/* 🔔 the tier headings inside Settings → Notifications: a caption, not a row you can press */
.notiftier{ padding:12px 2px 4px; border-top:1px solid var(--line); margin-top:4px }
.notiftier:first-child{ border-top:0; margin-top:0; padding-top:4px }
.notiftier b{ display:block; font-size:13px; letter-spacing:.02em; text-transform:uppercase; color:var(--accent2) }
.notiftier small{ display:block; color:var(--dim); font-size:12px; line-height:1.3; margin-top:2px }

/* ── 📐 ON A TABLET THE COLUMN IS CAPPED, the way the trainer caps its own (body.at-prog.pg-desk
   .solo-wrap{max-width:460px} / .pg-tall{max-width:640px}). Measured on iPad: the trainer centred at
   460px while Sing ran to 732px portrait and 988px landscape — a near-metre-wide header over a short
   progress bar. On a phone they already matched, so this only ever showed up at tablet width, which is
   exactly why Nathan asked whether it renders correctly there. */
@media (min-width:700px){ .singwrap{max-width:640px;margin-left:auto;margin-right:auto} }
.singnext{margin-top:30px}   /* big gap so nobody taps Next meaning "hear again" */
.dqopt:disabled{cursor:default;opacity:.92}
.dqopt.right{border-color:var(--good);background:#123a2a}
.dqopt.right b{color:var(--good)}
.dqopt.wrong{border-color:var(--bad);background:#3a1a1e}
.dqopt.wrong b{color:var(--bad)}
.dqfeed{min-height:22px;margin-top:10px;text-align:center;font-size:15px;color:var(--ink)}
.dqresult{text-align:center;margin:18px 0}
.dqscore{font-size:52px;font-weight:800;color:var(--accent2);line-height:1}
.dqsub{color:var(--dim);font-size:14px;margin-top:4px}

/* ── Leaderboard mode tabs ── */
.lbtabs{display:flex;gap:8px;margin:4px 0 10px}
.lbtab{flex:1;padding:9px 0;background:var(--panel2);border:1px solid var(--line);color:var(--dim);
  border-radius:9px;cursor:pointer;font-size:14px;font-weight:600}
.lbtab.on{border-color:var(--accent);color:var(--ink);background:#12333c}

/* ── Sing the interval ── */
.singstage{display:flex;flex-direction:column;align-items:center;gap:10px;margin:14px 0;padding:18px;
  background:var(--panel2);border:1px solid var(--line);border-radius:14px}
.singstep{font-size:16px;color:var(--ink);min-height:22px;text-align:center}
.singmeter{font-size:44px;font-weight:800;color:var(--accent2);font-variant-numeric:tabular-nums;line-height:1;min-height:46px}
.singresult{text-align:center;font-size:20px;font-weight:700;margin:14px 0 4px}
.singresult.good{color:var(--good)}
.singresult.bad{color:var(--bad)}

/* Sign in with Apple — native iOS only (guideline 4.8). Apple-logo glyph renders on Apple devices. */
.authbtn.apple{background:#000;border-color:#000;margin-top:8px}
.authbtn.apple small{color:#c9c9c9}
.authbtn.apple:hover{background:#161616}
/* 🍎 OFFICIAL Sign in with Apple button (HIG / App Store guideline 4): Apple logo + "Sign in with Apple",
   centered, black, standard height + radius, no subtitle inside. Explanatory copy lives in .siwa-cap below. */
.siwa-btn{width:100%;box-sizing:border-box;height:44px;margin-top:8px;display:flex;align-items:center;justify-content:center;gap:8px;background:#000;color:#fff;border:1px solid #000;border-radius:8px;font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","Helvetica Neue",system-ui,sans-serif;font-size:17px;font-weight:500;letter-spacing:-.01em;cursor:pointer;padding:0 16px;line-height:1;text-align:center}
.siwa-btn .siwa-logo{width:16px;height:16px;display:block;margin-top:-2px;flex:0 0 auto}
.siwa-btn:hover{background:#161616}
.siwa-btn:active{background:#242424}
.siwa-btn.siwa-sm{height:38px;font-size:15px;margin-top:6px}
.siwa-cap{display:block;font-size:12px;color:var(--dim);margin:5px 2px 0;line-height:1.35}

/* ── Palette play-mode toggle (ascending / descending / together) ── */
.pmodes{display:flex;gap:8px;margin:2px 0 12px}
.pmode{flex:1;padding:9px 0;background:var(--panel2);border:1px solid var(--line);color:var(--dim);
  border-radius:9px;cursor:pointer;font-size:13px;font-weight:600}
.pmode.on{border-color:var(--accent);color:var(--ink);background:#12333c}
/* shared sound (timbre) selector — same control everywhere */
.tbrow{margin:2px 0 10px}
.tbrow .pmode.on{border-color:var(--accent2);box-shadow:0 0 0 1px var(--accent2) inset}
.tbwrap{margin:2px 0 4px}
.setup .tbrow{margin:0}
.cntreveal{margin-top:12px;min-height:196px}   /* reserve space so the answer/Next don't bounce the layout (keeps the CTA steady) */
.cntreveal .scblk{margin:0 auto;max-width:300px}
.cntreveal.fading{opacity:0;transition:opacity .6s ease}
.minikb{margin-left:auto;margin-right:auto}   /* reveal keyboards are centered everywhere */
/* long-press "hold to hear" must never pop the iOS text-selection / callout menu */
.dqplay,.cntopt,.cntopts,.cntreveal,.minikb,.scblk,.singkb,.palchip,.dqopt,.chordcard,.cardot,.carnav{-webkit-user-select:none;user-select:none;-webkit-touch-callout:none}

/* Daily quiz option = a PICTURE of the interval (mini keyboard at the round's real pitches) */
.dqkb{width:100%;max-width:150px;margin:0 auto 6px;pointer-events:none}
.dqkb svg{width:100%;height:auto;display:block;border-radius:6px}
.dqopt{padding:12px 8px}

/* Sing mode — mic input picker */
.micpick{display:flex;align-items:center;gap:8px;margin:6px 0 12px;color:var(--dim);font-size:14px}
.micpick select{flex:1;background:#0a1a20;border:1px solid var(--line);color:var(--ink);border-radius:8px;padding:8px 10px;font-size:14px}

/* Sing mode — live tuner keyboard: light the pitch class you're singing (teal), distinct from eraser red */
.singkb{max-width:300px;margin:10px auto 0}
.singkb .piano{filter:none}
.wkey.lit{fill:#2dacb3}     /* BKS teal — "you are here" */
.bkey.lit{fill:#7fd6db}

/* Sing manual-capture + palette keyboards + daily speed badge */
.singcap{text-align:center;color:var(--dim);font-size:15px;margin:10px 0}
.singcap b{color:var(--ink)}
.dqspeed{display:inline-block;margin-left:6px;padding:1px 7px;border-radius:9px;background:var(--accent);color:var(--accent-ink);font-size:12px;font-weight:700}
.palette{grid-template-columns:repeat(auto-fill,minmax(150px,1fr))}
.palkb{width:100%;margin:0 auto 6px;pointer-events:none}
.palkb svg{width:100%;height:auto;display:block;border-radius:5px}

/* Sing hands-free fill meter + result compare */
.singbar{width:100%;max-width:300px;height:16px;margin:8px auto;background:#0a1a20;border:1px solid var(--line);border-radius:9px;overflow:hidden}
.singbar>span{display:block;height:100%;width:0%;background:var(--accent);border-radius:9px;transition:width .08s linear}
.singbar>span.near{background:var(--good)}
.singcompare{display:flex;gap:12px;justify-content:center;margin:10px 0}
.scblk{flex:1;max-width:220px;text-align:center}
.scblk .singkb{margin-top:0}
.sclabel{font-size:12px;color:var(--dim);margin-bottom:4px}

/* Close-X (top-right of a panel) + intro "don't show again" */
.panel{position:relative}
.closeX{position:absolute;top:10px;right:12px;width:34px;height:34px;border-radius:50%;
  background:var(--panel2);border:1px solid var(--line);color:var(--dim);font-size:16px;line-height:1;cursor:pointer;z-index:2}
.closeX:hover{color:var(--ink);border-color:var(--accent)}
.introdont{margin-top:14px;justify-content:center}

/* Combined "Overall" leaderboard columns */
.lbcol{width:40px;text-align:center;color:var(--dim);font-size:13px;font-variant-numeric:tabular-nums;flex:none}
.lbrow.lbhead{background:transparent;border:none;color:var(--dim);font-size:11px;text-transform:uppercase;letter-spacing:.04em;padding-bottom:2px}
.lbrow.lbhead .lbname,.lbrow.lbhead .lbscore{color:var(--dim);font-weight:600}

/* Compact clue cards so the grid is visible alongside them (esp. cards with no keyboard graphic) */
.strip{grid-template-columns:repeat(auto-fill,minmax(132px,1fr));gap:8px}
.chordcard{min-height:auto;padding:8px 10px}
.cc-top{margin-bottom:4px}

/* Leveled sing mode — level tag, per-note breakdown */
.singlvl{text-align:center;color:var(--accent2);font-size:13px;font-weight:600;margin-bottom:4px}
.singnotes{display:flex;flex-wrap:wrap;gap:8px;justify-content:center;margin:10px 0}
.singnote{padding:5px 11px;border-radius:10px;font-size:14px;font-weight:600;border:1px solid var(--line);background:transparent;font-family:inherit;cursor:pointer}   /* now a <button> — every note on the results screen is tappable to hear it (Nathan). Reset the UA chrome so the tiles look exactly as they did. */
.singnote:active{transform:scale(.94)}
.singnote.good{color:var(--good);border-color:var(--good);background:#123a2a}
.singnote.bad{color:var(--bad);border-color:var(--bad);background:#3a1a1e}
.singnote.sang{color:var(--accent2);border-color:var(--accent2);background:transparent}
.singnotes .rowtag{align-self:center;font-size:11px;text-transform:uppercase;letter-spacing:.5px;color:var(--dim);min-width:58px;text-align:right}
.singresult.mid{color:var(--accent2)}
.singcap .cur{color:var(--accent2)}

/* How many notes? — number-choice tiles */
.cntopts{display:grid;grid-template-columns:repeat(auto-fit,minmax(64px,1fr));gap:10px;margin:4px 0}
.cntopt{padding:18px 0;font-size:26px;font-weight:800;cursor:pointer;background:var(--panel2);
  border:1px solid var(--line);border-radius:12px;color:var(--ink);font-variant-numeric:tabular-nums}
@media (hover:hover){.cntopt:hover:not(:disabled){border-color:var(--accent)}}
.cntopt:disabled{cursor:default;opacity:.92}
.cntopt:disabled:not(.right):not(.wrong){opacity:.4}   /* awaiting playback — can't guess until you've heard it */
.cntopt.right{border-color:var(--good);background:#123a2a;color:var(--good)}
.cntopt.wrong{border-color:var(--bad);background:#3a1a1e;color:var(--bad)}

/* chorDLE! wordmark — lean on brand colors instead of emoji */
.wordmark{font-weight:800;letter-spacing:.01em;white-space:nowrap;display:inline-block}
.wordmark span{color:var(--accent2)}
h1.wordmark{line-height:0}
.wm-logo{height:auto;width:auto;max-width:min(340px,72vw);max-height:120px;display:block;margin:2px auto 0}
.wm-sm{color:var(--accent2)}
.big.dqplay{touch-action:none;user-select:none}

.buildstamp{text-align:center;color:var(--dim);opacity:.55;font-size:11px;margin-top:14px;font-variant-numeric:tabular-nums}

.singready{opacity:.72;font-size:15px;font-weight:600}
.singready.on{opacity:1;color:#3ecf7a;font-weight:800}

.mp-out{background:#3a1a1f;color:#ff9d9d;border:1px solid #7a2c34;border-radius:10px;padding:10px 14px;text-align:center;font-weight:700}
.mp-warn{font-size:12px;opacity:.72;text-align:center;margin-top:6px}

.autolbl{font-size:12px;opacity:.75;align-self:center;margin-right:2px}

.g-train{border-color:#c58af0}
.perk{display:inline-block;font-size:10px;font-weight:800;letter-spacing:.5px;text-transform:uppercase;color:#c58af0;border:1px solid #c58af0;border-radius:6px;padding:1px 6px;margin-left:6px;vertical-align:middle}

.resume-wrap{display:flex;gap:6px;align-items:stretch}
.resume-wrap .resume{flex:1}
.resume-x{flex:0 0 auto;width:44px;border:1px solid var(--line,#2a3340);background:transparent;color:var(--dim,#8b98a5);border-radius:12px;font-size:18px;cursor:pointer}
.resume-x:active,.resume-x:hover{color:#ff6b6b;border-color:#ff6b6b}

.authbtn.joinbtn{display:block;text-decoration:none;border-color:#5865F2}
.authbtn.joinbtn:hover{background:rgba(88,101,242,.12)}

.grid.hardbul .bul{pointer-events:none}   /* hard mode: bullets not removable → whole tile is the tap target (phone misclick fix) */

.solostats{margin:14px 0 4px;padding:12px 14px;background:rgba(255,255,255,.04);border:1px solid var(--line,#2a3340);border-radius:12px;font-size:13px}
.solostats .ss-h{font-weight:700;margin-bottom:8px;color:var(--ink)}
.solostats .ss-row{margin:5px 0;line-height:1.5}
.ss-tag{display:inline-block;font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:.5px;padding:1px 7px;border-radius:6px;margin-right:6px}
.ss-tag.ok{background:rgba(62,207,122,.18);color:#3ecf7a}
.ss-tag.no{background:rgba(255,107,107,.18);color:#ff6b6b}

.mini.spend{background:linear-gradient(180deg,#e8c14a,#d4a838);color:#1a1205;border:none;font-weight:800;text-decoration:underline;box-shadow:0 0 0 2px rgba(232,193,74,.35),0 0 12px rgba(232,193,74,.5);animation:spendpulse 1.3s ease-in-out infinite}
.mini.spend.on{background:transparent;color:#e8c14a;border:1px solid #e8c14a;box-shadow:none;animation:none;text-decoration:none}
@keyframes spendpulse{0%,100%{box-shadow:0 0 0 2px rgba(232,193,74,.35),0 0 8px rgba(232,193,74,.4)}50%{box-shadow:0 0 0 3px rgba(232,193,74,.6),0 0 16px rgba(232,193,74,.75)}}

.perk.on{color:#3ecf7a;border-color:#3ecf7a}

.big.g-train{position:relative;padding-bottom:30px}
.cornerbadge{position:absolute;left:14px;bottom:9px;margin:0}

.pchart-fix{position:relative;display:flex;flex-direction:column;align-items:flex-start;gap:3px;padding-bottom:26px;text-align:left}
.pchart-fix .pf-title{font-weight:800;font-size:16px}
.pchart-fix .pf-title,.pchart-fix .pf-sub{color:#08252b}
.pchart-fix .pf-sub{opacity:.9;font-weight:600;font-size:12.5px;line-height:1.35;color:#08252b}

/* ---- Daily Tournament ---- */
.big.g-tourney{background:linear-gradient(135deg,rgba(232,193,74,.20),rgba(232,193,74,.05));border-color:#e8c14a}
/* 🏆 LIVE tournament — prominent, persistent entry (Nathan) */
.big.g-tourlive{background:linear-gradient(135deg,rgba(232,193,74,.30),rgba(232,193,74,.08));border:1.5px solid #e8c14a;box-shadow:0 0 0 1px rgba(232,193,74,.25),0 4px 18px rgba(232,193,74,.18)}
.big.g-tourlive .g-title{color:#f2d372}
.tourlive-badge{background:#2dacb3;color:#04231f}
.navtourney{position:relative;animation:ntpulse 2.2s ease-in-out infinite;filter:drop-shadow(0 0 4px rgba(232,193,74,.7))}
.navtourney::after{content:'';position:absolute;top:2px;right:2px;width:7px;height:7px;border-radius:50%;background:#3ecf7a;box-shadow:0 0 5px #3ecf7a}
@keyframes ntpulse{0%,100%{transform:scale(1)}50%{transform:scale(1.14)}}
.concede-next{color:#f0d478 !important}
@media(hover:hover){.big.g-tourney:hover{border-color:#f2d372}}
.tourney-live{display:inline-block;background:rgba(62,207,122,.16);border:1px solid #3ecf7a;color:#7fe0a4;font-weight:800;padding:6px 13px;border-radius:20px;margin:4px 0 12px}
.tourney-count{font-size:20px;font-weight:800;color:#e8c14a;margin:10px 0 14px}
.tourney-board{margin-top:18px}
.tourney-board .lbrow.me{outline:1px solid var(--accent2)}

/* MP online presence */
.pstat{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:7px;vertical-align:middle;flex:0 0 auto}
.pstat.on{background:#3ecf7a;box-shadow:0 0 5px rgba(62,207,122,.7)}
.pstat.off{background:#5a6b72}

/* Tournament countdown callout (top of menu) */
.tourney-callout{display:flex;align-items:center;justify-content:center;gap:6px;width:100%;box-sizing:border-box;text-align:center;background:linear-gradient(135deg,rgba(232,193,74,.20),rgba(232,193,74,.05));border:1px solid #e8c14a;color:#f0d478;border-radius:12px;padding:9px 12px;margin:0 0 14px;font-size:13.5px;cursor:pointer;font-weight:600}
.tourney-callout b{color:#fff;font-weight:800}
/* 🍬🥓🌶️ one-time new-trainer nudge — three-family wash, dismissible (Nathan) */
.newtrainer-nudge{position:relative;width:100%;box-sizing:border-box;border-radius:12px;padding:10px 34px 10px 12px;margin:0 0 14px;font-size:13px;line-height:1.4;cursor:pointer;background:linear-gradient(100deg,rgba(232,121,162,.16),rgba(207,154,78,.14),rgba(127,166,200,.16));border:1px solid rgba(232,121,162,.4)}
.newtrainer-nudge b{color:#fff;font-weight:800}
.newtrainer-nudge .nt-go{color:var(--accent2);font-weight:700;white-space:nowrap}
.newtrainer-nudge .nt-x{position:absolute;top:5px;right:6px;width:22px;height:22px;border:none;background:transparent;color:var(--dim);font-size:14px;line-height:1;cursor:pointer;border-radius:6px}
.newtrainer-nudge .nt-x:hover{background:rgba(255,255,255,.08);color:#fff}
.tourney-callout .tc-go{margin-left:auto;font-size:18px;opacity:.7}
.tourney-callout.live{border-color:#3ecf7a;color:#9fe8bd;background:rgba(62,207,122,.14)}

/* Tournament board refresh affordances */
.tb-head{display:flex;align-items:center;justify-content:space-between;margin:0 0 6px}
.tb-live{font-size:11px;color:var(--dim)}
.tb-refresh,.tb-head .mini{padding:3px 9px;font-size:13px;border-radius:8px;background:var(--panel2);border:1px solid var(--line);color:var(--accent2);cursor:pointer}

/* HINTS */
.cell.hinthere{outline:3px solid #e8c14a;outline-offset:-2px;box-shadow:0 0 16px rgba(232,193,74,.65);z-index:2}
.cell.guidehere{outline:3px solid #3ecf7a;outline-offset:-2px;box-shadow:0 0 18px rgba(62,207,122,.7);z-index:2}   /* 🎯 guided warm-up: the intervals hiding in the active clue glow green — tap them, chord comes into focus */
/* 🎹 Step-3 tab: the branded-eraser keyboard cycling the chord's intervals */
.ob-kbcyc{display:block;margin-top:7px;max-width:170px;pointer-events:none}   /* sublabel-sized — just replaces the old text (Nathan) */
.ob-kbcyc .piano{display:block;width:100%;height:auto}
/* 🏆 tourney tab: countdown-before-start */
.tb-countdown{text-align:center;padding:28px 14px 20px}
.tb-clk{font-size:27px;font-weight:800;color:var(--accent2);letter-spacing:-.01em;margin-bottom:8px;font-variant-numeric:tabular-nums}
/* 🚪 onboarding: blink the CLUE card until it's been played (hear before you place) */
.chordcard.clue-blink{animation:clueBlink 1s ease-in-out infinite}
@keyframes clueBlink{0%,100%{box-shadow:0 0 10px 1px rgba(92,198,211,.45)}50%{box-shadow:0 0 30px 7px rgba(92,198,211,.95)}}
.cell.dimlock{opacity:.25;filter:grayscale(.6);pointer-events:none}
.mini.hintbtn{padding:6px 12px;font-size:13px;border-radius:8px;background:rgba(232,193,74,.14);border:1px solid #e8c14a;color:#f0d478;cursor:pointer;font-weight:700}
.mini.hintbtn b{color:#fff}
.mini.hintbtn.on{background:#e8c14a;color:#08252b}
.mini.hintbtn.spent{opacity:.5}
.mini.hintbtn.pulse{animation:hintpulse 1.15s ease-in-out infinite}
@keyframes hintpulse{0%,100%{box-shadow:0 0 0 0 rgba(232,193,74,0);background:rgba(232,193,74,.14)}50%{box-shadow:0 0 12px 2px rgba(232,193,74,.6);background:rgba(232,193,74,.32)}}
.mini.trainbtn{padding:6px 12px;font-size:13px;border-radius:8px;background:rgba(127,214,219,.14);border:1px solid #7fd6db;color:#a6e6ea;cursor:pointer;font-weight:700}
.mini.trainbtn.on{background:#7fd6db;color:#08252b;box-shadow:0 0 10px 1px rgba(127,214,219,.5)}
.cell.auditioning{outline:2px solid #7fd6db;outline-offset:-2px;box-shadow:0 0 12px 2px rgba(127,214,219,.55) inset;transition:box-shadow .1s}
/* Floating pill — fixed, out of flow, so it NEVER shifts End round / Hint / Training */
.matchpick{position:fixed;left:50%;bottom:20px;transform:translateX(-50%);z-index:60;background:#3ecf7a;color:#08252b;border:1px solid #3ecf7a;font-weight:800;border-radius:24px;padding:13px 24px;box-shadow:0 6px 22px rgba(0,0,0,.4);animation:matchpickin .18s ease-out}
@keyframes matchpickin{from{transform:translateX(-50%) scale(.9);opacity:0}to{transform:translateX(-50%) scale(1);opacity:1}}
.cell.matchsel{position:relative;outline:3px solid #3ecf7a;outline-offset:-2px;z-index:3;animation:matchselblink .8s ease-in-out infinite}   /* blink the tile "Choose this one" refers to */
.cell.matchsel::after{content:'tap to pick ✓';position:absolute;bottom:0;left:0;right:0;text-align:center;font-size:9px;font-weight:800;letter-spacing:.2px;color:#08251a;background:#3ecf7a;border-radius:0 0 7px 7px;padding:1px 0;pointer-events:none}   /* make the SECOND step (pick, not just hear) explicit on the tile itself */
@keyframes matchselblink{0%,100%{box-shadow:0 0 0 0 rgba(62,207,122,.12) inset}50%{box-shadow:0 0 16px 3px rgba(62,207,122,.7) inset}}
.mini.trainbtn.glow{animation:trainglow 1.1s ease-in-out infinite}   /* invite training after errors pile up */
@keyframes trainglow{0%,100%{box-shadow:0 0 0 0 rgba(127,214,219,0);background:rgba(127,214,219,.14)}50%{box-shadow:0 0 12px 2px rgba(127,214,219,.65);background:rgba(127,214,219,.32)}}
.cell.matchpop{animation:matchpop .9s ease-out}
@keyframes matchpop{0%{box-shadow:0 0 0 0 rgba(62,207,122,.7)}30%{box-shadow:0 0 22px 6px rgba(62,207,122,.75);transform:scale(1.06)}100%{box-shadow:0 0 0 0 rgba(62,207,122,0);transform:scale(1)}}
/* TRAINING ON — unmistakable at a glance, WITHOUT shifting layout: status chip + board outline + brighter button */
.scorechip.trainchip{background:#7fd6db;color:#08252b;font-weight:800;border:1px solid #7fd6db;box-shadow:0 0 10px rgba(127,214,219,.6);animation:trainchippulse 2s ease-in-out infinite}
@keyframes trainchippulse{0%,100%{box-shadow:0 0 6px rgba(127,214,219,.4)}50%{box-shadow:0 0 14px rgba(127,214,219,.85)}}
.solo-wrap.training-on{outline:3px solid #7fd6db;outline-offset:-3px;border-radius:12px}
.train-banner{margin:6px 0 2px;padding:9px 12px;border-radius:10px;background:linear-gradient(90deg,#7fd6db,#a6e6ea);color:#08252b;font-weight:800;font-size:13.5px;text-align:center;letter-spacing:.2px;box-shadow:0 0 14px rgba(127,214,219,.55);animation:trainbannerpulse 1.8s ease-in-out infinite}
.grid.placing{outline:3px solid var(--cluec,var(--accent));outline-offset:4px;border-radius:14px;transition:outline-color .2s}   /* VISUAL language: the grid frames itself in the active clue's color — "this grid receives Clue N's pick", no text */
.chordcard.invite{animation:cardinvite 1.6s ease-in-out infinite}   /* no clue selected → the centered card pulses to invite the first tap (motion cue, not an instruction) */
@keyframes cardinvite{0%,100%{outline:2px solid transparent;outline-offset:2px}50%{outline:2px solid var(--accent);outline-offset:5px}}
@keyframes trainbannerpulse{0%,100%{box-shadow:0 0 8px rgba(127,214,219,.4)}50%{box-shadow:0 0 20px rgba(127,214,219,.8)}}
.mini.trainbtn.on{background:#7fd6db;color:#08252b;box-shadow:0 0 12px 2px rgba(127,214,219,.7);font-weight:800}
.mini{white-space:nowrap}
.mini.hintbtn b{margin-left:3px}
/* Onboarding coach-marks — bubble flips to the side OPPOSITE the highlighted target so the target stays visible */
.coach-bubble{position:fixed;left:50%;transform:translateX(-50%);z-index:130;width:min(92vw,440px);background:#0f2a33;border:1px solid #2dacb3;border-radius:14px;padding:14px 16px;box-shadow:0 10px 30px rgba(0,0,0,.55);color:#eaf6f7;animation:coachin .2s ease-out}
.coach-bubble.coach-top{top:58px}
.coach-bubble.coach-bottom{bottom:92px}   /* clears the floating "Choose" pill (bottom:20px) */
.coach-wait{color:#7fd6db;font-size:13px;font-style:italic;font-weight:700}
@keyframes coachin{from{transform:translateX(-50%) translateY(-8px);opacity:0}to{transform:translateX(-50%) translateY(0);opacity:1}}
.coach-step{font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:#7fd6db;margin-bottom:5px;font-weight:700}
.coach-txt{font-size:15px;line-height:1.5;margin-bottom:12px}
.coach-txt a{color:#7fd6db;font-weight:800;text-decoration:underline}   /* Discord link was dark-purple on black — make it legible (Nathan) */
.coach-actions{display:flex;justify-content:space-between;align-items:center;gap:10px}
.coach-skip{background:none;border:none;color:#8fb3b7;font-size:13px;cursor:pointer;padding:6px}
.coach-next{background:#2dacb3;color:#08252b;border:none;border-radius:9px;padding:10px 20px;font-weight:800;cursor:pointer}
.coach-spot{position:relative;z-index:115;outline:3px solid #ffd166;outline-offset:3px;border-radius:8px;animation:coachspot 1.1s ease-in-out infinite}
@keyframes coachspot{0%,100%{box-shadow:0 0 0 0 rgba(255,209,102,.5)}50%{box-shadow:0 0 20px 6px rgba(255,209,102,.75)}}
/* Onboarding ear-check screen */
.ob-card{background:rgba(127,214,219,.08);border:1px solid #2dacb3;border-radius:14px;padding:20px 18px;margin:16px 0;text-align:center}
.ob-q{font-size:16px;line-height:1.5;margin-bottom:16px}
.ob-hl{display:flex;gap:12px;justify-content:center;margin-top:16px}
.ob-hl .mini{flex:0 0 auto;min-width:130px;padding:13px 16px;font-size:16px}
.ob-feedback{margin-top:16px;font-size:15px;line-height:1.5}
.ob-feedback.ok{color:#3ecf7a;font-weight:800}
.ob-feedback.no{color:#e8c14a}
.ob-route{display:flex;gap:10px;justify-content:center;margin-top:14px;flex-wrap:wrap}
/* Solo = THE main game — featured emphasis + badge */
.big.featured{position:relative;overflow:visible}   /* ANY featured tile anchors its own .feat-badge — was scoped to .g-solo only, so the Pentathlon tile's ADMIN badge escaped to the screen's top-right corner (Nathan) */
.big.g-solo.featured{background:linear-gradient(135deg,rgba(45,172,179,.24),rgba(127,214,219,.07));border:1.5px solid #2dacb3;box-shadow:0 0 20px rgba(45,172,179,.28)}
.big.g-tourney.featured{border:1.5px solid #a877ee;background:linear-gradient(135deg,rgba(168,119,238,.18),rgba(168,119,238,.05))}
.big.g-solo.featured .g-title{font-size:1.18em;font-weight:800}
.feat-badge{position:absolute;top:-10px;right:14px;background:#e8c14a;color:#08252b;font-size:10px;font-weight:800;letter-spacing:.06em;padding:3px 9px;border-radius:10px;box-shadow:0 2px 8px rgba(0,0,0,.35)}
.feat-badge.admin-badge{background:#a877ee;color:#1a0f2e}   /* ADMIN = purple, distinct from the gold MAIN GAME badge — only you see the item it sits on */
/* onboarding funnel PATH badges (Nathan): ① Start here → ② Practice → ③ Play — a guided newcomer route */
.feat-badge.start-badge{background:var(--band-0);color:#04222a}
.feat-badge.practice-badge{background:var(--band-1);color:#04222a}
.feat-badge.play-badge{background:var(--band-3);color:#08252b}
.game-icon{height:22px;width:auto;vertical-align:-5px;margin-right:3px}   /* the puzzle-piece game icon (Nathan's) on the Solo/Play button */
.ico-title{display:flex;align-items:center;gap:7px;font-weight:600;font-size:17px}   /* icon + label INLINE on a mini-game button — flex row so a wide PNG can't stretch to full width; matches .big body weight */
.ico-title .game-icon{margin-right:0}   /* gap handles the spacing */
.banner-icon{height:23px;width:auto;vertical-align:-6px;margin-right:3px;border-radius:3px}   /* custom icon on the top callout banners (Daily Chord = keys+gold ?) — bumped up so it reads (Nathan) */
.menu-divider{margin:16px 0 6px;font-size:11px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;color:var(--band-2);border-top:1px solid var(--line);padding-top:12px}   /* dividing space before the PURPLE sandbox mini-games (Nathan) */
/* 📣 ONE rotating promo banner (Tournament · Daily Chord · Featured minigame) — replaces the old 4-banner stack */
.promobanner{border-radius:14px;overflow:hidden;margin:0 0 12px;background:#0d2b25;box-shadow:0 4px 14px rgba(0,0,0,.25)}
.pb-slide{display:none;align-items:center;gap:11px;padding:12px 14px;cursor:pointer;color:#fff}
.pb-slide.on{display:flex}
.pb-em{font-size:22px;flex:0 0 auto}
.pb-txt{display:flex;flex-direction:column;line-height:1.25;min-width:0}
.pb-txt b{font-size:14px} .pb-txt small{font-size:11.5px;color:rgba(255,255,255,.78);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.pb-go{margin-left:auto;font-size:18px;opacity:.75;flex:0 0 auto}
.pb-tour{background:linear-gradient(100deg,#3a2350,#4a2e63)}
.pb-daily{background:linear-gradient(100deg,#123a58,#164a63)}
.pb-feat{background:linear-gradient(100deg,#123830,#1a4d40)}
.pb-dots{display:flex;gap:5px;justify-content:center;padding:5px 0 7px}
.pb-dot{width:6px;height:6px;border-radius:50%;background:#2e544a;cursor:pointer;transition:background .2s,transform .2s}
.pb-dot.on{background:var(--accent2,#2dacb3);transform:scale(1.25)}
/* menu expanders — the mini-games + multiplayer collapse under these */
.menuexp{border:1px solid var(--line);border-radius:13px;background:rgba(0,0,0,.14);overflow:hidden}
.menuexp>summary{list-style:none;padding:14px 15px;font-weight:800;font-size:15px;cursor:pointer;display:flex;align-items:center;gap:9px;color:var(--ink,#eaf3f1)}
.menuexp>summary::-webkit-details-marker{display:none}
/* a section header that carries a second line — the label stacks over what it holds, and the caret
   still rides right on the flex row (see .menuexp>summary). min-width:0 so a long line wraps inside
   the flex child instead of shoving the caret off the edge on a narrow phone. */
/* rung numbers in the level menu — the number is the shareable part of a streak, so it reads as a
   label rather than decoration; BONUS replaces it on the off-ladder rungs (Colors, Outliers). */
/* the tiles that are NOT receiving the next vote step back, so the one that is stands out by
   contrast rather than by shouting. Filled tiles keep more presence than empty ones — what you
   already answered is still worth reading. Never applied to a graded card. */
.pg-slot.notaimed{ opacity:.62; transition:opacity .18s ease }
.pg-slot.notaimed.pend:not(.mm-rightnow):not(.pg-fixed){ opacity:.5 }
@media (prefers-reduced-motion:reduce){ .pg-slot.notaimed{ transition:none } }
/* ── 🏅 the milestone card. Waits for a tap rather than a timeout — the whole point is that the old
   flourish vanished before it could be read. Sits above the transport, clear of the wheel, and never
   covers the clue row. */
/* ⚠️ #8FE34A WAS NOT A BRAND COLOR. The card arrived in a lime that appears nowhere else in the
   app — Nathan: "the green color is offbrand." The palette is BKS teal (--accent #2dacb3), light
   teal (--accent2) and gold (#e8c14a); gold is what the game already uses for points and records,
   so a personal best wears gold and the action wears teal. The number is the hero: it is the thing
   being celebrated and the thing being shared. */
.pg-streakcard{
  position:fixed; left:50%; bottom:calc(84px + env(safe-area-inset-bottom,0px));
  transform:translate(-50%,10px); z-index:90; opacity:0; pointer-events:none;
  /* ⚠️ THE MIDDLE COLUMN WAS BEING CRUSHED. As a three-item row — count, label, buttons — the label
     got whatever width was left and "level 7 · The whole songbook" wrapped into a four-line ribbon,
     making the card twice as tall as it needed to be. (Nathan: "the formatting is ugly.") Grid with
     a fixed button column instead: the count and its label share the space that is actually left. */
  display:grid; grid-template-columns:auto 1fr auto; align-items:center; gap:4px 12px;
  width:min(92vw,420px); box-sizing:border-box;
  padding:12px 12px 12px 14px; border-radius:16px;
  background:linear-gradient(180deg, rgba(18,44,52,.97), rgba(10,28,34,.97));
  border:1px solid rgba(232,193,74,.42);
  box-shadow:0 12px 34px rgba(0,0,0,.55), inset 0 1px 0 rgba(255,255,255,.05);
  transition:opacity .22s ease, transform .22s ease;
}
.pg-streakcard.on{ opacity:1; transform:translate(-50%,0); pointer-events:auto }
/* the count, given room to be the headline */
.pg-streakcard b{ font-size:19px; font-weight:800; color:#e8c14a; white-space:nowrap;
                  letter-spacing:-.01em; text-shadow:0 0 14px rgba(232,193,74,.25) }
.pg-streakcard small{ font-size:11.5px; color:var(--dim); line-height:1.3; min-width:0;
                      overflow:hidden; text-overflow:ellipsis; white-space:nowrap }
.pg-sc-btns{ margin-left:auto; display:flex; align-items:center; gap:4px; flex:none }
/* 44px tall: the iOS minimum for a thing you are meant to hit with a thumb */
.pg-sc-share{ background:var(--accent,#2dacb3); color:#04262b; border:0; border-radius:999px;
  padding:0 15px; min-height:38px; font-weight:800; font-size:13px; cursor:pointer;
  box-shadow:0 2px 10px rgba(45,172,179,.34) }
.pg-sc-share:active{ transform:translateY(1px) }
.pg-sc-x{ background:transparent; border:0; color:var(--dim); font-size:16px; line-height:1;
  padding:4px 6px; cursor:pointer; min-width:38px; min-height:38px }
@media (prefers-reduced-motion:reduce){ .pg-streakcard{ transition:none } }
.lvlmenu-n{ opacity:.55; font-variant-numeric:tabular-nums; margin-right:1px }
.lvlmenu-bonus{ font-size:9.5px; font-weight:800; letter-spacing:.06em; padding:1px 5px; border-radius:999px;
  background:rgba(201,167,245,.18); color:#C9A7F5; vertical-align:1px; margin-right:4px }
.menuexp-t{display:flex;flex-direction:column;gap:2px;min-width:0}
.menuexp-t small{font-weight:600;font-size:11.5px;color:var(--dim);line-height:1.3;white-space:normal}
.menuexp-car{margin-left:auto;color:var(--dim);font-size:22px;line-height:1;font-weight:700;transition:transform .2s}   /* closed = points RIGHT (▸ convention); bigger (Nathan) */
.menuexp[open]>summary .menuexp-car{transform:rotate(90deg)}   /* open = points DOWN */
.menuexp-in{padding:0 9px 10px;display:flex;flex-direction:column;gap:10px}

/* Accuracy (batting average) badge */
.acc{font-weight:800;font-size:12.5px;margin:0 6px;white-space:nowrap;flex:0 0 auto}
.acc small{opacity:.55;font-weight:600}
.acc-good{color:#3ecf7a} .acc-mid{color:#e8c14a} .acc-low{color:#ff6b6b}
.scorechip.acc-good{color:#3ecf7a;border-color:rgba(62,207,122,.5)} .scorechip.acc-mid{color:#e8c14a;border-color:rgba(232,193,74,.5)} .scorechip.acc-low{color:#ff6b6b;border-color:rgba(255,107,107,.5)}

/* Landscape on a PHONE isn't ready — ask for portrait. Scope to TOUCH devices (pointer:coarse): a desktop
   browser (fine pointer) can't rotate a monitor and already has the min-width:820px two-column layout, so it
   must NEVER see this — a short desktop window was tripping the old max-height guard (Nathan: "she's on desktop"). */
@media (orientation:landscape) and (max-height:500px) and (pointer:coarse){
  .brandbar,#app{display:none!important}
  body::after{content:"↻  Please rotate to portrait\A\A Landscape isn’t ready yet — coming soon.";white-space:pre-wrap;position:fixed;inset:0;z-index:99999;background:var(--bg);color:var(--ink);display:flex;align-items:center;justify-content:center;text-align:center;font-size:18px;font-weight:700;padding:36px;line-height:1.5}
}

/* Reward trophy in the status bar (replaces the Spend button) */
.trophybadge{cursor:pointer;font-size:15px;border-color:#2dacb3!important;background:rgba(45,172,179,.18)!important;color:#8fe6ee!important;animation:mullpulse 1.8s ease-in-out infinite}   /* 🐦 Mulligan badge: teal (its own identity, not the gold trophy) + a clip-free GLOW pulse — the old translateY dance got cut off by hb-right's overflow (Nathan) */
.trophybadge.on{background:#2dacb3!important;color:#04262b!important;animation:none}
@keyframes mullpulse{0%,100%{box-shadow:0 0 0 0 rgba(45,172,179,0)}50%{box-shadow:0 0 9px 2px rgba(45,172,179,.55)}}
.dontshow{font-size:12px;color:var(--dim);display:flex;align-items:center;gap:5px;margin-right:auto;cursor:pointer}

/* Lifetime points bank (menu banner) + trophy total — the reward pot ("points", not coins) */
.pointsbank{display:inline-flex;align-items:center;gap:6px;margin:2px auto 10px;padding:6px 14px;border-radius:999px;background:linear-gradient(180deg,rgba(232,193,74,.18),rgba(232,193,74,.08));border:1px solid rgba(232,193,74,.5);color:#fff;font-weight:800;font-size:16px;box-shadow:0 2px 10px rgba(232,193,74,.12)}
.pointsbank .pb-lbl{font-weight:600;font-size:12px;color:var(--dim);letter-spacing:.4px;text-transform:uppercase}
.tr-total{margin-top:4px;font-size:15px;color:#fff;font-weight:700}
.tr-total span{font-variant-numeric:tabular-nums}
.tr-total .tr-dump{color:#ffd666;font-size:13px;margin-left:6px;font-weight:800;text-shadow:0 0 8px rgba(255,214,102,.4)}
/* Collect button BECKONS — gold glow + swell, so you know to cash out (Nathan: "pulse the collect points button") */
.trophy .tr-btns .ghost.cashpulse{color:#ffd666;border-color:#ffd666;animation:cashpulse 1.1s ease-in-out infinite}
@keyframes cashpulse{0%,100%{transform:scale(1);box-shadow:0 0 0 0 rgba(255,214,102,0)}50%{transform:scale(1.06);box-shadow:0 0 16px 3px rgba(255,214,102,.55)}}
button.cashpulse{animation:cashpulse 1.1s ease-in-out infinite}
button.nextbtn{background:linear-gradient(180deg,#4fc9d0,#2dacb3);color:#04262b;font-size:1.15em;font-weight:800;padding:16px 22px;letter-spacing:.01em;box-shadow:0 4px 18px rgba(45,172,179,.42)}   /* Nathan: brand TEAL, not green */
.trophy .tr-mull{display:inline-block;margin:8px auto 2px;font-size:15px;font-weight:800;color:#8fe6ee;background:#08252b;border:1px solid #2dacb3;border-radius:9px;padding:7px 14px;box-shadow:0 2px 8px rgba(0,0,0,.35)}   /* DARK pill so the Mulligan line is legible against the GOLD win card (Nathan: was illegible) */
.scorechip.bonuschip{color:#ffd666;font-weight:800}
.brandbar .navpoints.pump{animation:navpump .3s ease-in-out infinite;color:#ffd666}
@keyframes navpump{0%,100%{transform:translateX(-50%) scale(1)}50%{transform:translateX(-50%) scale(1.55)}}

/* Speed-bonus floating badge (⚡ quick correct answer) */
.floatbadge{position:absolute;top:2px;left:50%;font-weight:800;font-size:15px;color:#ffe08a;text-shadow:0 1px 3px rgba(0,0,0,.6);pointer-events:none;z-index:8;animation:floatup 1.05s ease-out forwards}
@keyframes floatup{0%{opacity:0;transform:translate(-50%,6px) scale(.8)}20%{opacity:1}100%{opacity:0;transform:translate(-50%,-26px) scale(1.1)}}

/* GAME-OVER review: amber blink on clues/squares you could still have gotten (not locked) — "left on the table" */
.cardot.missed{animation:missblinkdot 1s ease-in-out infinite}
@keyframes missblinkdot{0%,100%{box-shadow:0 0 0 2px var(--bg),0 0 0 4px #e8c14a;opacity:1}50%{box-shadow:none;opacity:.45}}
.cell.missed{animation:missblinkcell 1.1s ease-in-out infinite}
@keyframes missblinkcell{0%,100%{box-shadow:inset 0 0 0 2px #e8c14a}50%{box-shadow:inset 0 0 0 2px rgba(232,193,74,.15)}}
.ss-tag.miss{background:rgba(232,193,74,.18);color:#f0d98a;border:1px solid rgba(232,193,74,.5)}

/* Mulligan/gambit: a dancing action badge in the top bar + eligible squares pulsing to the SAME beat (amber, distinct from green-selected) */
.actionbadge{border-color:#e8c14a!important;background:rgba(232,193,74,.18)!important;color:#f0d98a!important;font-weight:800;animation:trophydance 1.2s ease-in-out infinite;transform-origin:center bottom}
.cell.beathere{z-index:2;animation:beatpulse 1.2s ease-in-out infinite}
@keyframes beatpulse{0%,100%{box-shadow:inset 0 0 0 2px rgba(232,193,74,.4);transform:scale(1)}50%{box-shadow:inset 0 0 0 3px #e8c14a,0 0 16px 2px rgba(232,193,74,.5);transform:scale(1.04)}}

/* Your lifetime points, pinned at the top of the leaderboard (always visible near the board) */
.lb-mypoints{display:inline-block;margin:0 0 10px;padding:6px 14px;border-radius:999px;background:linear-gradient(180deg,rgba(232,193,74,.18),rgba(232,193,74,.08));border:1px solid rgba(232,193,74,.5);color:#fff;font-weight:700;font-size:15px}
.lb-mypoints b{font-variant-numeric:tabular-nums}

/* Points flying off a tile toward the leaderboard icon (casual-game payoff) */
.flypts{position:fixed;z-index:260;font-weight:800;font-size:17px;color:#ffe08a;text-shadow:0 2px 6px rgba(0,0,0,.65);pointer-events:none;transform:translate(0,0);transition:transform .78s cubic-bezier(.35,0,.2,1),opacity .78s ease-in;will-change:transform,opacity}   /* z above the .trophy (200) so the win payoff animates OVER the trophy, not buried behind it */
/* header "who's online" pill (ported from main) */
.navonline{display:inline-flex;align-items:center;gap:5px;font-size:12px;font-weight:700;color:#2ecc71;background:rgba(46,204,113,.12);border:1px solid rgba(46,204,113,.32);border-radius:20px;padding:2px 9px;white-space:nowrap;line-height:1.6}
.navonline .nodot{width:7px;height:7px;border-radius:50%;background:#2ecc71;display:inline-block;animation:lbpulse 2s infinite}
@keyframes lbpulse{0%{box-shadow:0 0 0 0 rgba(46,204,113,.55)}70%{box-shadow:0 0 0 6px rgba(46,204,113,0)}100%{box-shadow:0 0 0 0 rgba(46,204,113,0)}}

/* Trophy flying to the badge (mulligan bling) + running points total by the leaderboard icon */
.flytrophy{position:fixed;z-index:61;font-size:36px;pointer-events:none;transform:translate(0,0) scale(1);transition:transform .76s cubic-bezier(.3,0,.2,1),opacity .76s ease;will-change:transform,opacity;filter:drop-shadow(0 2px 6px rgba(0,0,0,.5)) drop-shadow(0 0 11px rgba(255,214,102,.9))}
#navBoard{position:relative;overflow:visible}
.brandbar .navpoints{position:absolute;left:50%;bottom:-6px;transform:translateX(-50%);font-weight:800;font-size:9.5px;line-height:1;color:#fff;white-space:nowrap;font-variant-numeric:tabular-nums;text-shadow:0 1px 2px rgba(0,0,0,.95),0 0 2px rgba(0,0,0,.8);pointer-events:none}
.pourpt{position:fixed;z-index:260;font-size:28px;color:#ffd54a;pointer-events:none;transform:translate(0,0) scale(1);transition:transform .98s cubic-bezier(.4,0,.25,1),opacity .98s ease-in;filter:drop-shadow(0 0 6px rgba(255,213,74,.8));will-change:transform,opacity;text-shadow:0 1px 3px rgba(0,0,0,.5)}   /* above the trophy (200) — the ⚡ pour reads OVER the win banner */

/* Personal Trainer (weak-spot MC drill) */
.g-solo.trainer,.big.trainer{border-color:rgba(232,193,74,.5)}
.tr-nopts{color:var(--dim);font-size:12px;white-space:nowrap}
.tr-drilled{display:flex;flex-wrap:wrap;gap:8px;justify-content:center;margin:10px 0}
.tr-chip{padding:4px 10px;border-radius:999px;font-weight:700;font-size:13px;border:1px solid var(--line)}
.tr-chip.good{color:#3ecf7a;border-color:rgba(62,207,122,.5)} .tr-chip.mid{color:#e8c14a;border-color:rgba(232,193,74,.5)} .tr-chip.low{color:#ff6b6b;border-color:rgba(255,107,107,.5)}

/* Personal Trainer hub card */
.trainer-card{background:var(--panel2);border:1px solid var(--line);border-radius:14px;padding:16px;margin-top:12px;text-align:center}
.trainer-card .primary{margin-top:10px}

/* Perfect-pitch lab cards */
.ptcards{display:flex;flex-direction:column;gap:10px;margin:10px 0}
.ptcard{background:var(--panel2);border:1px solid var(--line);border-radius:12px;padding:10px 12px;display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.ptcard>b{font-size:20px;min-width:42px}
.ptcard .ptkb{flex:1 1 120px;min-width:120px}
.ptcard .mini{flex:0 0 auto}
.ptcard.right{border-color:var(--good);box-shadow:0 0 0 1px var(--good)}
.ptcard.wrong{border-color:#ff6b6b}

/* Personal Trainer — distinct violet identity so you know you're in trainer mode */
.trainer-panel{border-top:3px solid #a98bff}
.trainer-panel .dqhead h2,.trainer-card+.lbtabs,.panel .lbtab.on{}
.dqhead h2{}

/* ============ PERSONAL TRAINER — distinct violet module identity ============ */
body.in-trainer{ --accent2:#c2a8ff; background:#02040a }
body.in-trainer .panel{ background:#05080e; border:1px solid rgba(160,130,255,.32); box-shadow:0 0 0 1px rgba(160,130,255,.1),0 12px 44px rgba(0,0,0,.55) }
body.in-trainer .dqhead h2{ color:#d7c6ff }
body.in-trainer .brandbar .navbtn{ border-color:rgba(160,130,255,.4) }
.trainer-top{ display:flex; align-items:center; gap:8px; margin-bottom:12px }
.trainer-top .tr-exit{ flex:0 0 auto; color:#c2a8ff; font-weight:700 }
.trainer-tabbar{ display:flex; gap:6px; flex:1; overflow-x:auto; scrollbar-width:none }
.trainer-tabbar::-webkit-scrollbar{ display:none }
.trainer-tabbar .ttab{ background:var(--panel2); border:1px solid rgba(160,130,255,.3); color:var(--dim); border-radius:999px; padding:6px 13px; font-weight:700; font-size:13px; white-space:nowrap; cursor:pointer; flex:0 0 auto }
.trainer-tabbar .ttab.on{ background:#7c5cd6; border-color:#7c5cd6; color:#fff; box-shadow:0 2px 10px rgba(124,92,214,.4) }
/* pitch lab: keyboard = play (big), card = pick */
.ptcards{ display:flex; flex-direction:column; gap:12px; margin:12px 0 }
.ptcard{ flex-direction:column; align-items:stretch; gap:8px; cursor:pointer; transition:border-color .12s,transform .08s }
.ptcard:active{ transform:scale(.99) }
.ptcard .ptkb{ width:100%; flex:none; cursor:pointer }
.ptlabel{ display:flex; flex-direction:column; align-items:center; font-weight:800; font-size:19px; color:#d7c6ff }
.ptlabel small{ font-weight:500; font-size:10.5px; color:var(--dim); letter-spacing:.3px }
.pt-skip{ width:100%; margin-top:2px }
.tr-chordwas{ display:block; color:var(--dim); font-size:12.5px; margin-top:5px }

/* Gambit/exchange: pulse the instruction to the beat + hint-branded trade + chooser-shaped status badge */
.gridmsg.beat{border-radius:9px;padding:8px 14px;animation:gmbeat 1.2s ease-in-out infinite}
@keyframes gmbeat{0%,100%{box-shadow:0 0 0 0 rgba(232,193,74,0)}50%{box-shadow:inset 0 0 0 1px rgba(232,193,74,.5),0 0 14px rgba(232,193,74,.28)}}
.gm-hinttrade{display:inline-block;background:rgba(232,193,74,.16);border:1px solid #e8c14a;color:#f0d478;border-radius:8px;padding:2px 9px;font-weight:800;font-size:12.5px;cursor:pointer;margin-left:3px;white-space:nowrap;animation:gmtradepulse 1.2s ease-in-out infinite}
@keyframes gmtradepulse{0%,100%{box-shadow:0 0 13px rgba(232,193,74,.5);opacity:1}50%{box-shadow:0 0 0 rgba(232,193,74,0);opacity:.7}}
.gm-hinttrade:active{background:#e8c14a;color:#08252b}
.actionbadge.ivbadge{display:inline-flex;flex-direction:column;align-items:center;justify-content:center;gap:1px;padding:4px 9px;border-radius:9px;background:var(--panel2)!important;border:1px solid #e8c14a!important;line-height:1;color:#f0d98a!important}
.actionbadge.ivbadge b{font-size:15px;line-height:1}
.actionbadge.ivbadge small{font-size:8px;color:var(--dim);font-weight:600;white-space:nowrap;line-height:1}

/* Gold tile that flies from the gambit selector up to the status badge (reusable: trophies/coins later) */
.fly-gold{will-change:transform,opacity;display:flex;flex-direction:column;align-items:center;justify-content:center;border-radius:9px}
.iv-pick.gold,.fly-gold.gold{background:#e8c14a!important;border:1px solid #e8c14a!important;color:#08252b!important;box-shadow:0 0 24px rgba(232,193,74,.75)!important;transition:background .18s,box-shadow .18s}
.iv-pick.gold b,.fly-gold.gold b{color:#08252b!important}
.iv-pick.gold small,.fly-gold.gold small{color:#4a3a08!important}
.badge-land{animation:badgeland .5s ease}
@keyframes badgeland{0%{transform:scale(1.55);filter:drop-shadow(0 0 13px rgba(255,214,102,.95))}55%{transform:scale(.9)}100%{transform:scale(1);filter:drop-shadow(0 0 0 rgba(255,214,102,0))}}

/* Fading gold trail behind a flyGold tile */
.fly-trail{position:fixed;width:9px;height:9px;margin:-4px 0 0 -4px;border-radius:50%;background:#f0d478;box-shadow:0 0 9px 2px rgba(232,193,74,.7);pointer-events:none;z-index:215;animation:trailfade .52s ease-out forwards}
@keyframes trailfade{0%{opacity:.9;transform:scale(1)}100%{opacity:0;transform:scale(.25)}}

/* Gambit chooser: an interval you've already used is crossed out (usable once) */
.iv-pick.used{opacity:.42;cursor:default;border-style:dashed}
.iv-pick.used b,.iv-pick.used small{text-decoration:line-through}

/* small inline cancel in the spend prompt */
.gm-cancel{display:inline-block;background:none;border:0;color:var(--dim);font-size:12.5px;font-weight:700;cursor:pointer;padding:0 4px;text-decoration:underline;white-space:nowrap}
.gm-cancel:active{color:#ff8a8a}

/* sing: visible pulse while the target plays (audio can be quiet under the mic) */
.big.dqplay.playing{animation:singplay .75s ease-in-out infinite;border-color:#b58cff}
@keyframes singplay{0%,100%{box-shadow:0 0 0 0 rgba(181,140,255,.5)}50%{box-shadow:0 0 18px 5px rgba(181,140,255,.75)}}

/* big pointing finger that follows the current coach-mark target */
.coach-finger{position:fixed;transform:translateX(-50%);font-size:42px;z-index:126;pointer-events:none;filter:drop-shadow(0 2px 4px rgba(0,0,0,.5));animation:coachfinger 1s ease-in-out infinite}
@keyframes coachfinger{0%,100%{transform:translateX(-50%) translateY(0)}50%{transform:translateX(-50%) translateY(7px)}}

/* arrival fork (welcome runway) */
.welcome-panel .sub{max-width:520px;margin:0 auto 6px}
.welcome-choices{display:flex;flex-direction:column;gap:12px;max-width:520px;margin:16px auto 10px}
.big.wc{display:flex;align-items:center;gap:14px;text-align:left;padding:16px 16px}
.wc .wc-emoji{font-size:30px;flex:0 0 auto}
.wc .wc-body{display:flex;flex-direction:column;gap:2px}
.wc .wc-body b{font-size:1.05em}
.welcome-skip{display:block;margin:6px auto 0;opacity:.75}
/* clear "tap to hear" on the clue card */
.cc-hear{margin-left:6px;font-size:11px;font-weight:700;letter-spacing:.02em;color:var(--accent2);text-transform:uppercase;vertical-align:middle}

/* pinned "what's next" event card (does not rotate away) */
.pr-pinned{margin-bottom:8px}
.pr-pinned .pr-card{outline:1.5px solid var(--accent2);outline-offset:-1px}

/* GLOBAL button press feedback — every button visibly depresses when tapped/clicked */
.big:active, .mini:active, .authbtn:active, .wc:active, .dqplay:active, .primary:active, .ghost:active, .danger:active, .link:active, .navbtn:active, .cardot:active{
  transform:scale(.97); filter:brightness(.9); transition:transform .04s ease, filter .04s ease;
}
/* ── 👆 AND IT HAS TO COVER EVERYTHING TAPPABLE. The rule above missed the level menu, the new
   controls and the collection rows — and on a screen that takes a moment, the press state IS the
   receipt that the tap landed. Fast, so it reads as the button moving under the finger. */
.lvlmenu-btn:active, .lvlmenu-lvl:active, .pstab:active, .colsongs:active, .colplay:active,
.colfav:active, .mm-b:active, .pg-nextb:active, .bb-home:active, .navstat:active, .pg-slot:active,
.pg-micb:active, .pg-loopb:active, .colrow:active{
  transform:scale(.975); filter:brightness(.88); transition:transform .06s ease, filter .06s ease;
}

/* "How many notes" game: frame the game so it's clearly THE GAME, not the ad below it */
.in-count #app > .panel{ border:2px solid var(--accent2); border-radius:16px; box-shadow:0 0 0 5px rgba(45,172,179,.07); padding-top:12px }
.in-count .cta-block{ opacity:.9 }

/* leaderboard scrollable — show everyone, no cap (we don't have so many people) */
#lbrows{}   /* full board scrolls with the page (inner scroll frame felt broken); the landing board keeps its own scroll */
.landboard .lb{max-height:46vh;overflow-y:auto;-webkit-overflow-scrolling:touch}

/* SETTINGS: checkbox rows read box-first (input precedes its label in the DOM) — tighter, one line each (Nathan) */
.setup input[type=checkbox]{justify-self:end}
.setup input[type=checkbox]+label{justify-self:start;text-align:left;color:var(--ink)}
@media(max-width:640px){ .setup{grid-template-columns:max-content 1fr;gap:10px 12px} .setup label{justify-self:start;text-align:left} }
.lvlrow .lvlfrom{font-size:10px;letter-spacing:.02em;padding:6px 8px;white-space:nowrap}
/* while points pour to the cup, the board gets out of the way (Nathan: card obscured the payoff) */
.pouring #app .grid,.pouring #app .carousel,.pouring #app .strip,.pouring #app .gridmsg{opacity:.22;transition:opacity .25s}
.pouring .trophy{opacity:.15;transition:opacity .25s}
.train-banner.review{background:linear-gradient(90deg,#8a7cff,#b0a6ff);color:#161038;animation:none}

/* 🃏 COLLECTIBLES — dossier popup + collection drawer */
.setrow-btns{display:flex;gap:10px;flex-wrap:wrap;margin:8px 0 18px}
.setrow-btns .primary,.setrow-btns .ghost{flex:1 1 auto}
.modal.dossier{max-width:430px}
.dos-new{color:#3ecf7a;font-weight:700;letter-spacing:.08em;font-size:12px;margin-bottom:6px}
.dos-tier{color:#e8c14a;font-size:13px;margin-bottom:2px}
.dos-name{margin:2px 0}
.dos-aka{color:var(--dim);font-size:12px;margin-bottom:8px}
.dos-kb{margin:10px 0 4px}
.dos-notes{color:var(--dim);font-size:12px;margin-bottom:8px}
.dos-blurb{font-size:14px;line-height:1.45}
.col-tier{margin:16px 0 8px}
.col-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(132px,1fr));gap:8px}
.col-card{border:1px solid var(--line);border-radius:10px;padding:10px 8px;font-size:13px;text-align:center;background:#0a1a20;color:var(--ink)}
.col-card.lost{color:var(--dim);opacity:.5}
.col-card.found{border-color:#3ecf7a55;cursor:pointer}

/* 🏆 win toast: thinner + tighter (Nathan) */
.trophy{padding:10px 14px}
.trophy .tr-main{font-size:17px}
.trophy .tr-sub{font-size:12px;margin-top:2px}
.trophy .tr-score{font-size:24px;margin-top:4px}
  .trophy .tr-sum{font-size:11.5px}
.trophy .tr-btns{margin-top:8px}
.trophy .tr-cta{margin-top:6px;font-size:12px}
.dos-src{color:var(--dim);font-size:11px;margin:6px 0 2px;font-style:italic}

/* first/last clue cards can actually CENTER — spacers give the scroller lead-in room (card is 84% wide → 8% each side). This was why clue 1 sat flush-left since day one. */
.carscroll::before,.carscroll::after{content:'';flex:0 0 8%;min-width:8%}

/* 📖 review: ONE amber pulse, everywhere in unison (title-bar chip + clue dots + board squares) */
@keyframes tableblink{0%,100%{opacity:1}50%{opacity:.22}}
.tablechip{color:#e8c14a;border:1px solid #e8c14a88;animation:tableblink 1.6s ease-in-out infinite}
.cardot.missed{background:transparent !important;border-color:#e8c14a !important;animation:tableblink 1.6s ease-in-out infinite}
.cell.missed{animation:tableblink 1.6s ease-in-out infinite;box-shadow:inset 0 0 0 2px #e8c14a77;opacity:.8}   /* hollow outline, no fill — reads "just for fun", not a task */
.namedchip{color:#e8c14a}

/* tries-remaining on the clue card (hard mode) — you always know how close a clue is to locking */
.cc-tries{display:block;color:var(--dim);font-size:10.5px;margin-top:3px;letter-spacing:.02em}
.cc-tries.last{color:#ff6b6b;font-weight:700;animation:tableblink 1.2s ease-in-out infinite}

/* 🃏 audition: chord name shown on the clue card (testing toggle in Settings → Collectibles) */
.cc-named{display:block;color:#e8c14a;font-size:11px;font-weight:700;letter-spacing:.03em;margin-bottom:2px}
.cc-bonuschord{display:block;color:#ff9db4;font-size:10.5px;font-weight:800;letter-spacing:.02em;margin-bottom:2px}   /* 🎵 graduation bonus chord — "hear it in a chord" (Nathan) */
.cc-named.lost{color:var(--dim);font-weight:600}
.cc-named.lost small{font-size:9px;font-style:italic}

/* mulligan-ready banner: single flex line, ✕ pinned right (wrapped ✕ was pushing the grid down) */
.gridmsg.gm-flex{display:flex;align-items:center;gap:8px}   /* flex ONLY where content is span-wrapped — bare flex discarded the whitespace between inline <b> tags (Nathan's 'Nice ear!Tap aglowingtile') */
.gridmsg .gm-txt{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:left}
.gridmsg .gm-x{flex:0 0 auto;margin-left:8px}

/* 🔬 deck review (admin) */
.da-actions{display:flex;gap:10px;margin:10px 0 6px;flex-wrap:wrap}
.da-card{border:1px solid var(--line);border-radius:12px;padding:12px 14px;margin:8px 0;background:#0a1a20}
.da-head{display:flex;align-items:center;gap:8px;font-size:15px}
.da-found{color:#3ecf7a;font-size:11px}
.da-notes{color:var(--dim);font-size:12px;margin:4px 0}
.da-blurb{font-size:13px;line-height:1.45;margin:6px 0 2px}
.pc-collect .pc-cnum{font-size:34px;font-weight:800;color:#e8c14a}
.pc-collect .pc-cnum small{font-size:13px;color:var(--dim);font-weight:600}
.pc-collect .pc-cnames{margin-top:8px;font-size:13px;line-height:1.6}

/* ↗ leaderboard motion — someone climbed since you last looked */
@keyframes lbup{0%,100%{transform:translateY(0)}50%{transform:translateY(-2px)}}
.lbup{color:#3ecf7a;font-weight:800;margin-left:5px;display:inline-block;animation:lbup 1.1s ease-in-out infinite;text-shadow:0 0 6px #3ecf7a66}

/* ↻ pull-to-refresh bar — stretches with the drag, springs shut after refresh */
.ptr{height:0;overflow:hidden;display:flex;align-items:center;justify-content:center;color:var(--dim);transition:height .3s cubic-bezier(.2,.85,.3,1.18)}
.ptr .ptr-ic{font-size:18px;transition:transform .15s,color .15s}
.ptr.ready .ptr-ic{color:#3ecf7a;transform:rotate(180deg) scale(1.15)}
@keyframes ptrspin{to{transform:rotate(360deg)}}
.ptr.spin .ptr-ic{animation:ptrspin .8s linear infinite;color:#3ecf7a}
.lbnew{margin-left:5px;font-size:12px}
.gridmsg.gm-flex .gm-hinttrade{flex:0 0 auto;margin-left:auto}
.gridmsg.gm-flex .gm-x{flex:0 0 auto;margin-left:6px}

/* ❤️ received toast — named senders + one-tap heart back */
.hearttoast{display:flex;flex-direction:column;gap:6px;align-items:flex-start}
.hearttoast .ht-row{display:flex;align-items:center;gap:8px}
.hearttoast .ht-back{background:#0a1a20;border:1px solid #e8547a88;color:#ff8aa8;border-radius:999px;padding:3px 10px;font-size:12px;cursor:pointer}
.hearttoast .ht-sent{color:#ff8aa8;font-size:12px;font-weight:700}

/* ★ the game pot — the visible HOLDER that empties into the cup at cash-out */
.scorechip.gamepot{border:1px solid #e8c14a99;background:#e8c14a14;font-weight:800}
.scorechip.gamepot.banked{opacity:.5}
@keyframes htpop{0%{transform:scale(.6)}45%{transform:scale(1.5)}100%{transform:scale(1)}}
.ht-sent.pop{display:inline-block;animation:htpop .5s cubic-bezier(.2,.8,.3,1.3);text-shadow:0 0 10px #ff8aa8}

/* reaction palette (fixed vocabulary) */
.rstrip{display:inline-flex;gap:4px;align-items:center}
.remo{background:#0a1a20;border:1px solid var(--line);border-radius:999px;width:30px;height:30px;font-size:15px;line-height:1;padding:0;cursor:pointer}
.remo:active{transform:scale(1.25)}
.ht-mute{background:none;border:none;color:var(--dim);font-size:10px;text-decoration:underline;cursor:pointer;margin-left:4px}

/* 🎵 send-a-chord puzzle picker */
.dos-pick{max-height:180px;overflow-y:auto;margin-top:4px}
.dp-row{display:flex;align-items:center;gap:8px;width:100%;text-align:left;background:#0a1a20;border:1px solid var(--line);border-radius:8px;padding:8px 10px;margin:3px 0;color:var(--ink);font-size:14px;cursor:pointer}

/* sent-state must be READABLE on touch: emoji + explicit SENT tag, dimmed, clearly not a button */
.fheart.sent{display:inline-flex;flex-direction:column;align-items:center;gap:0;opacity:.75;border:none;background:none;line-height:1}
.fheart.sent small{font-size:7px;letter-spacing:.08em;color:var(--dim);margin-top:1px}
button.fheart{border:1px solid var(--line);background:#0a1a20;border-radius:999px;width:30px;height:30px;line-height:1}
.sparkgrid{stroke:var(--line);stroke-dasharray:3 6;stroke-width:1;opacity:.6}
.sparklbl{fill:var(--dim);font-size:10px}
.cotd-name{font-size:22px;font-weight:800;color:#e8c14a;margin-top:4px}
.cotd-spell{font-size:15px;letter-spacing:.04em;margin:6px 0}

/* iOS long-press: no text-selection loupe / callout anywhere in the game (Nathan got the magnifier
   holding the daily chord). Inputs stay selectable. Also prereq for the hold-the-chord interaction. */
body{-webkit-user-select:none;user-select:none;-webkit-touch-callout:none}
input,textarea,[contenteditable="true"]{-webkit-user-select:text;user-select:text;-webkit-touch-callout:default}
.cotd-icing{color:#e8c14a;font-size:13px;margin:6px 0;font-weight:600}
.lb-legend{color:var(--dim);font-size:11px;margin:2px 0 6px;letter-spacing:.02em}
.tourney-callout.cotd.done{border-color:#3ecf7a55}
.sg-kbwrap{margin:2px 0}
.sg-kb{width:100%}
.sg-kb{width:100%;min-height:96px}
.cotd-kb{margin:10px 0 4px}
.cotd-imdone{margin-top:6px}   /* 🛟 the elegant "That's all I can hear" exit — prominent (primary big) so it's actually noticed, but it's the only extra option so nothing competes */
/* 🏁 no-bingo-path hand-hold (Nathan: a tiny link doesn't cut it — TELL them what to do) */
.nopath-banner{background:rgba(232,193,74,.12);border:1px solid #e8c14a;border-radius:12px;padding:12px 14px;margin:0 0 10px;font-size:14px;line-height:1.5;color:var(--ink);text-align:left}
.nopath-go{width:100%}
/* 🏆 UNMISTAKABLE tournament chrome (Nathan: tournament play must be INSTANTLY recognizable — its absence tells you you left) */
.tourframe{position:fixed;inset:0;pointer-events:none;z-index:45;box-shadow:inset 0 0 0 4px #e8c14a, inset 0 0 34px rgba(232,193,74,.26)}
.tf-ribbon{position:fixed;top:4px;left:50%;transform:translateX(-50%);max-width:94vw;pointer-events:auto;cursor:pointer;background:linear-gradient(90deg,#e8c14a,#f2d372);color:#26200f;font-weight:800;font-size:12px;text-align:center;padding:3px 13px;border-radius:20px;box-shadow:0 2px 9px rgba(0,0,0,.45);z-index:46;display:inline-flex;align-items:center;gap:6px;white-space:nowrap;line-height:1.3}   /* 🏆 a compact gold PILL, not a full-width banner (Nathan hated the banner) */
.tf-ribbon .tf-live{background:#166534;color:#eafff0;border-radius:20px;padding:1px 8px;font-size:10.5px;letter-spacing:.4px}
.tf-ribbon .tf-peek{opacity:.75;text-decoration:underline;font-weight:700}
body.tourney-on{padding-top:24px}
.tourpeek .tp-list{display:flex;flex-direction:column;gap:3px;margin:10px 0;max-height:52vh;overflow:auto}
.tp-row{display:flex;align-items:center;gap:9px;padding:8px 11px;border-radius:9px;background:var(--panel2)}
.tp-row.me{background:rgba(232,193,74,.18);border:1px solid #e8c14a}
.tp-rank{width:22px;text-align:center;font-weight:800;color:var(--dim)}
.tp-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.tp-pts{font-weight:800;font-variant-numeric:tabular-nums;color:#f0d478}

/* 📖 guide link in dossier — clearly LEAVES the app to beatkitchen.io */
.dos-guide{display:block;margin:10px 0 2px;padding:9px 11px;border:1px solid #e8c14a55;border-radius:9px;color:#e8c14a;font-size:13px;font-weight:600;text-decoration:none;background:#e8c14a11}
.dos-guide .ext{display:block;color:var(--dim);font-size:10px;font-weight:500;margin-top:2px;letter-spacing:.02em}

/* 👤 player card */
.modal.pcard{max-width:340px;position:relative}
.pcard .back{position:absolute;right:8px;top:6px}
.pc-name{margin:4px 0 2px;display:flex;align-items:center;gap:8px}
.pc-sub{color:var(--dim);font-size:12px;margin-bottom:12px}
.pc-stats{display:flex;gap:12px;margin:8px 0}
.pc-stat{flex:1;background:#0a1a20;border:1px solid var(--line);border-radius:10px;padding:10px;text-align:center}
.pc-stat b{font-size:22px;color:#e8c14a;display:block}
.pc-stat small{color:var(--dim);font-size:11px}
.pc-line{font-size:14px;margin:8px 0}
.pc-guest{color:#e8c14a;font-size:12px;margin:8px 0;padding:8px;border:1px dashed #e8c14a55;border-radius:8px}
.pc-actions{display:flex;gap:10px;margin-top:14px}
.pc-actions .ghost{flex:1}
.lbrow.tapcard{cursor:pointer}

/* 🙈 blindfold mode — hide every chord/interval picture; ear + names only */
body.bf .sqkb, body.bf .cc-kbwrap .minikb, body.bf .cc-diagram{display:none !important}
body.bf .cc-kbwrap{min-height:54px;display:flex;align-items:center;justify-content:center}
/* ♿ was: ::after{content:'🙈 tap to hear'} — a DECOY. VoiceOver reads generated content aloud, so a
   blind player was told to tap something that was not a control and had no handler behind it
   (Angela's review, 2026-09-30). The real button is .bf-hear in the markup; this only shows it. */
body.bf .bf-hear{display:inline-flex;align-items:center;gap:6px;background:none;border:0;color:var(--dim);font-size:12px;padding:8px 10px;min-height:44px;cursor:pointer}
body.bf .bf-hear:focus-visible{outline:2px solid var(--accent2);outline-offset:2px;border-radius:8px}
.bf-hear{display:none}
body.bf .cell{min-height:56px}
body.bf .dqkb, body.bf #cntKb, body.bf .cntkb, body.bf #dqKb{display:none !important}
body.bf .dqopt{min-height:52px}
.dqopt .dqlong{display:block;color:var(--dim);font-size:11px;margin-top:1px}
body.bf .dqopt .dqlong{font-size:13px;color:var(--ink);margin-top:4px}

/* 🙈 blindfold ON badge — visible whenever blindfold is active; tap to jump to the setting */
.bfbadge{display:none}
body.bf .bfbadge{display:flex;align-items:center;gap:6px;position:fixed;left:50%;transform:translateX(-50%);bottom:calc(72px + env(safe-area-inset-bottom));z-index:240;background:#1a1030;border:1px solid #e8c14a99;color:#e8c14a;font-size:12px;font-weight:700;padding:7px 14px;border-radius:999px;box-shadow:0 4px 14px #0007;cursor:pointer}
.flashrow{outline:2px solid #e8c14a;outline-offset:3px;border-radius:6px;transition:outline .2s}

/* 🗣️ voice mode */
.g-voice{--g:#7fd6db}
.voicep .beta{font-size:11px;color:var(--dim);vertical-align:middle}
.vc-score{color:var(--dim);font-size:13px;margin:6px 0 14px}
.vc-stage{text-align:center;padding:18px 0}
.vc-mic{font-size:44px}
.vc-status{font-size:17px;font-weight:600;margin-top:10px;min-height:24px}
.vc-heard{color:var(--dim);font-size:13px;margin-top:6px;min-height:18px}
.vc-btns{display:flex;flex-direction:column;gap:10px;margin-top:8px}

/* 🗣️ voice mode live log — see exactly what it's hearing */
.vc-log{margin-top:14px;border:1px solid var(--line);border-radius:10px;padding:8px 10px;background:#0a1a20;max-height:190px;overflow-y:auto}
.vc-logh{color:var(--dim);font-size:10px;letter-spacing:.08em;text-transform:uppercase;margin-bottom:4px}
.vc-logrow{font-size:13px;padding:3px 0;border-top:1px solid var(--line)}
.vc-logrow .vl-said{color:var(--ink)}
.vc-logrow .vl-v{float:right;color:var(--dim);font-size:12px}

/* 🗣️ voice: live mic-level oscillator bars (you can SEE it listening) */
.vc-mic.live{display:inline-flex;align-items:flex-end;gap:4px;height:40px;font-size:0}
.vc-mic.live .vbar{width:6px;height:6px;background:#3ecf7a;border-radius:3px;transition:height .06s linear}
.vc-worth{color:#e8c14a;font-size:13px;font-weight:700;margin-top:4px}
@keyframes vcwarm{0%,100%{opacity:.45;transform:scale(1)}50%{opacity:1;transform:scale(1.14)}}
.vc-mic.warming{animation:vcwarm .8s ease-in-out infinite;display:inline-block}

/* 🎧 reps nudge — tongue-in-cheek "you haven't trained today" */
.reps-nudge{position:relative;background:#1a1030;border:1px solid #e8c14a55;border-radius:12px;padding:11px 34px 11px 12px;margin-bottom:12px;font-size:13px;line-height:1.4}
.reps-nudge .reps-x{position:absolute;right:8px;top:8px;background:none;border:none;color:var(--dim);font-size:14px;cursor:pointer}
.reps-nudge .reps-fine{color:var(--dim);font-size:9px;font-style:italic;margin-top:3px}
.vc-level{color:var(--accent2);font-size:13px;font-weight:600;margin:2px 0 8px}

/* 🏅 pentathlon — color-coded stages (see the switch-ups at a glance) */
.tri-prog{display:flex;gap:5px;margin:8px 0 8px;flex-wrap:wrap}
.tri-dot{width:22px;height:22px;border-radius:50%;border:2px solid var(--m,var(--line));display:flex;align-items:center;justify-content:center;font-size:11px;color:var(--m,var(--dim));opacity:.55}
.tri-dot.done{background:var(--m,#3ecf7a);border-color:var(--m,#3ecf7a);color:#08201a;opacity:1}
.tri-dot.now{color:var(--m,var(--accent2));opacity:1;animation:tableblink 1.4s ease-in-out infinite}
.tri-legend{display:flex;flex-wrap:wrap;gap:6px 12px;margin:0 0 14px;font-size:11px;color:var(--dim)}
.tri-legend span{display:inline-flex;align-items:center;gap:4px}
.tri-legend i{width:9px;height:9px;border-radius:50%;display:inline-block}
.tri-modechip{display:inline-block;font-size:11px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--m,#888);border:1px solid var(--m,#888);border-radius:999px;padding:2px 10px}
.tri-place{margin:14px 0;padding:12px 14px;border:1px solid var(--accent2);border-radius:12px;background:rgba(62,207,122,.08)}
.tri-help{display:block;margin-top:14px;font-size:13px;color:var(--accent2);text-decoration:none;text-align:center}
.tri-help b{color:var(--accent2)}

/* 🎯 guided first game */
.fg .fg-clue{margin:16px 0}
.fg .fg-play{width:100%}
.fg-tiles{display:grid;grid-template-columns:repeat(2,1fr);gap:10px;margin-top:6px}
.fg-tile{display:flex;flex-direction:column;align-items:center;gap:2px;padding:14px 8px;border:1px solid var(--line);border-radius:12px;background:var(--card);color:var(--ink);font-size:17px;transition:opacity .2s,transform .1s}
.fg-tile:disabled{opacity:.32}
.fg-tile small{font-size:11px;color:var(--dim);font-weight:400}
.fg-tile.coach-answer{border-color:var(--accent2);box-shadow:0 0 0 2px var(--accent2),0 0 18px rgba(62,207,122,.5)}
.fg-tile.fg-got{background:#3ecf7a;color:#04222a;border-color:#3ecf7a}
.fg-skip{display:block;margin:18px auto 0;font-size:12px;color:var(--dim)}
/* 🎁 prize disclaimer */
.prize-note{margin:12px 0;padding:12px 14px;border:1px solid #e8c14a66;border-radius:12px;background:rgba(232,193,74,.09);font-size:14px;line-height:1.5}
.prize-signin{display:block;margin-top:10px;width:100%;padding:11px;border:0;border-radius:10px;background:#5865F2;color:#fff;font-weight:700;font-size:15px}

/* notch: on the MENU the top cluster is CENTERED (under the Dynamic Island), so clear the FULL inset —
   the -6px ride-high is only safe in-game where the logo sits BESIDE the island (Nathan: online row clipped). */
/* reclaim wasted top space on the MENU (Nathan: interface pushed below the cutout) — the nav row rides BESIDE the island, not below the full inset; smaller logo + tighter gaps bring the tiles above the fold */
/* MENU: the brandbar only holds the corner points/leaderboard button here — take it OUT OF FLOW so it stops
   reserving a full sticky row and shoving everything below the cutout (Nathan, repeatedly). The hero logo in
   #app now clears the notch itself and rides right up under the island; the nav floats top-right. */
body.lb-on #app{padding-top:calc(var(--lbh,60px) + 16px)}   /* content clears any live banner so it never covers the tourney button (Nathan) */
body.web.at-menu .brandbar{top:44px}   /* web: menu nav clears the site header (was cut off in Chrome — Alex) */
body.at-menu .brandbar{position:fixed;left:0;right:0;top:0;max-width:none;margin:0;background:var(--bg);border-bottom:1px solid var(--line);z-index:60;   /* OPAQUE full-width pinned header — was background:none + capped at 1140px, so the menu scrolled visibly BEHIND/past the nav icons (Nathan) */
  padding:calc(env(safe-area-inset-top,0px) + 4px) max(14px, env(safe-area-inset-right,0px) + 6px) 8px max(14px, env(safe-area-inset-left,0px) + 6px)}   /* v3: header sits BELOW the notch + restored OS status bar (Nathan) */
body.at-menu .brandbar>*{pointer-events:auto}
body.at-menu #app{padding-top:max(env(safe-area-inset-top,0px) + 52px, 62px)}   /* content clears the fixed v3 header row */
/* v3 MENU: the header carries the logo (+ points), so the big content hero logo/points pill is redundant here → hide it, keep the welcome copy. */
body.at-menu .wm-logo,body.at-menu h1.wordmark,body.at-menu .pointsbank{display:none}
body.at-menu .tourney-callout{margin-bottom:6px}
body.at-menu > #app .sub{margin-top:4px}
body.at-menu .gamelist,body.at-menu .choose{margin-top:4px}

/* 🙈 blindfold badge — in landscape (iPad) the bottom CTA lives where the badge sat → collision.
   Move it just below the brand bar (still center) so it clears the CTA and stays tappable. */
@media (orientation:landscape){
  body.bf .bfbadge{ bottom:auto; top:calc(env(safe-area-inset-top, 0px) + 52px) }
}

/* 🏆 tournament: in-game live countdown pill + intro-screen clock */
.tourclock{display:none;position:fixed;top:auto;bottom:calc(env(safe-area-inset-bottom, 0px) + 10px);left:50%;right:auto;transform:translateX(-50%);z-index:210;align-items:center;gap:6px;background:#1a1030;border:1px solid #e8c14a99;color:#e8c14a;font-weight:700;font-size:12px;padding:5px 14px;border-radius:999px;box-shadow:0 3px 12px #0007;white-space:nowrap}
body.in-tourney .tourclock{display:flex}
.tour-clock.big{text-align:center;font-size:18px;font-weight:800;color:#e8c14a;margin:14px 0;padding:12px;border:1px solid #e8c14a55;border-radius:12px;background:rgba(232,193,74,.08)}

/* 👤 player card modal — UNIQUE classes (old .pc-stats was display:flex → my rows overflowed sideways) */
.modal.pcardm{max-width:330px;width:88vw}
.pcardm .pcm-head{display:flex;align-items:center;gap:8px;margin-bottom:6px}
.pcardm .pcm-head h3{margin:0;font-size:18px}
.pcardm .pcm-big{font-size:28px;font-weight:800;color:#e8c14a;text-align:center;margin:6px 0 14px}
.pcardm .pcm-big small{font-size:11px;color:var(--dim);font-weight:600;text-transform:uppercase;letter-spacing:.5px;margin-left:6px}
.pcardm .pcm-stats{display:block}
.pcardm .pcm-row{display:flex;justify-content:space-between;align-items:baseline;gap:12px;padding:9px 2px;border-top:1px solid var(--line);font-size:14px}
.pcardm .pcm-row span{color:var(--dim)}
.pcardm .pcm-row b{color:var(--ink);font-variant-numeric:tabular-nums}
.pcardm .pcm-sub{font-size:10px;color:var(--accent2);text-transform:uppercase;letter-spacing:.6px;margin:14px 0 0}

/* 🎭 default-name nudge (once/day, past the onboarding wall) */
.namenudge{position:fixed;left:50%;bottom:calc(78px + env(safe-area-inset-bottom));transform:translateX(-50%) translateY(14px);z-index:230;display:flex;align-items:center;gap:8px;max-width:92vw;background:var(--panel2);border:1px solid var(--accent2);border-radius:14px;padding:10px 12px;box-shadow:0 8px 24px rgba(0,0,0,.5);opacity:0;transition:opacity .28s ease,transform .28s ease}
.namenudge.show{opacity:1;transform:translateX(-50%) translateY(0)}
.namenudge .nn-txt{font-size:13px;color:var(--ink);cursor:pointer;line-height:1.4}
.namenudge .nn-txt small{color:var(--accent2)}
.namenudge .nn-x{background:none;border:0;color:var(--dim);font-size:15px;cursor:pointer;flex:0 0 auto;padding:2px 4px}

/* 🏅 pentathlon "Next up" eyebrow */
.tri-nextup{font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:var(--dim);margin:10px 0 4px}
.tri-nextup b{color:var(--accent2)}

/* 🔬 admin player detail */
.pdetail h3{font-size:12px;text-transform:uppercase;letter-spacing:.05em;color:var(--accent2);margin:18px 0 8px}
.pd-top{font-size:13px;color:var(--dim);background:var(--panel2);border:1px solid var(--line);border-radius:10px;padding:10px 12px;margin:6px 0;line-height:1.6}
.pd-top b{color:var(--ink);font-size:19px}
.pd-list{display:flex;flex-direction:column;gap:6px}
.pd-row{display:flex;align-items:center;gap:8px;font-size:13px}
.pd-lbl{flex:0 0 70px;color:var(--ink)}
.pd-barwrap{flex:1 1 auto}
.pd-bar{display:block;height:8px;border-radius:4px;background:var(--line);overflow:hidden}
.pd-bar i{display:block;height:100%}
.pd-bar i.good{background:#3ecf7a}.pd-bar i.mid{background:#e8c14a}.pd-bar i.weak{background:#ff6b6b}.pd-bar i.none{background:var(--dim)}
.pd-val{flex:0 0 auto;color:var(--dim);min-width:82px;text-align:right}
/* legend under the top-players header — explains the dots (only shown when a Discord player is on the board) */
.lb-legend{display:flex;flex-wrap:wrap;align-items:center;gap:6px;font-size:11.5px;color:var(--dim);margin:2px 0 8px;line-height:1.4}
.lb-legend .dbadge,.lb-legend .rbadge{margin-left:0}
.feedtxt.tapstat{cursor:pointer}
.feedtxt.tapstat:active{opacity:.7}
/* onboarding hub — warm-up progress before Solo unlocks */
/* first-run WELCOME hero (Nathan: it's the first thing a new user sees — make it feel like a welcome) */
.ob-welcome{text-align:center;padding:6px 4px 4px}
.ob-welcome .ob-wm{height:auto;width:auto;max-width:min(240px,58vw);max-height:74px;display:block;margin:2px auto 10px}
.ob-eyebrow{font-size:12px;font-weight:800;letter-spacing:.14em;text-transform:uppercase;color:var(--accent2);margin-bottom:4px}
.ob-welcome-h{font-size:24px;line-height:1.2;text-wrap:balance;margin:0 0 8px}
.ob-welcome .sub{text-align:center}
.ob-skiplink{float:right;font-size:13px;opacity:.7}
.ob-plan-h{font-weight:700;margin-top:16px}
.ob-prog{background:var(--panel2);border:1px solid var(--line);border-radius:12px;padding:12px 14px;margin:12px 0 4px;display:flex;flex-direction:column;gap:7px}
.ob-row{font-size:14px;color:var(--ink);line-height:1.4}
.ob-row.muted{font-size:12.5px;color:var(--dim)}
.ob-tick{display:inline-block;width:18px;color:var(--dim);font-weight:800}
.ob-tick.done{color:var(--good)}
.ob-celebrate{background:rgba(62,207,122,.12);border:1px solid var(--good);border-radius:10px;padding:9px 12px;margin:6px 0 4px;font-size:13.5px;color:var(--ink);line-height:1.4}
.sub-mini{font-size:12.5px;color:var(--dim);margin:2px 0 4px}
.ob-skip{display:block;width:100%;text-align:center;white-space:normal;margin-top:8px}
.big.ob-done{opacity:.72}
/* end-game exit — clear + findable, but calm (red 'danger' scared people off the one way out — Nathan: losing people who can't find how to end the game kills retention) */
.endbtn{background:var(--panel2);border:1.5px solid var(--accent2);color:var(--accent2);font-weight:700;border-radius:12px;padding:11px 16px;white-space:nowrap}
.endbtn:active{background:var(--panel)}
/* one dot per person so badges don't compete (Nathan): purple = Discord-connected, teal = Beat Kitchen resident */
.dbadge{display:inline-block;width:7px;height:7px;border-radius:50%;background:#5865F2;vertical-align:middle;margin-left:5px}
.rbadge{display:inline-block;width:8px;height:8px;border-radius:50%;background:#1fc7b6;vertical-align:middle;margin-left:5px;box-shadow:0 0 0 1.5px rgba(255,255,255,.3),0 0 5px rgba(31,199,182,.6)}

/* pentathlon/tournament context in the game title bar */
.hb-tri{display:block;font-size:11px;color:var(--accent2);font-weight:700;letter-spacing:.03em;margin-bottom:1px}

/* 🎵 Daily Chord: compact the clue card so 'Reveal & move on' isn't pushed below the fold */
.cotd-solo .carslide .chordcard{padding:30px 12px 8px;gap:5px}
.cotd-solo .carslide .chordcard .cc-kbwrap .minikb{padding:2px}
.cotd-solo .cc-side{padding-top:0}

/* kill the iOS long-press magnifier/loupe on game tap targets (global rule lacked !important) */
.cell,.cell *,.chordcard,.chordcard *,.fg-tile,.fg-tile *,.dqopt,.dqopt *,.cntopt,.cntopt *,.palchip,.cardot,.give-side{-webkit-user-select:none !important;user-select:none !important;-webkit-touch-callout:none !important;-webkit-tap-highlight-color:transparent}
/* 🎵 Daily Chord: a bit shorter still — cap the reference keyboard + tighten the gap (keeps all info) */
.cotd-solo .carslide .chordcard{padding:30px 12px 6px;gap:3px}
.cotd-solo .carslide .minikb .piano{max-height:80px}
/* 🎹 the Daily-Chord CLUE has a whole empty column to itself — make its keyboard BIG so you can actually read the keys when checking answers (Nathan, repeatedly) */
.cotd-solo .cc-kbwrap .minikb .piano,.cotd-solo .sqkb .piano,.cotd-solo .cc-kbwrap .minikb svg,.cotd-solo .sqkb svg{max-height:150px}

/* 🎵 Daily Chord dossier — show the chord you're hearing (was missing entirely) */
.cotd-kb{background:#f3f6f6;border-radius:8px;padding:4px;margin:8px 0}
.cotd-kb .piano{max-height:96px}

/* daily-chord callout: let the text flow instead of each bit being a squeezed flex item */
.tourney-callout .tc-body{flex:1 1 auto;min-width:0;text-align:left}

/* interval quality color-coding (Nathan — M/m disambiguation; swappable). Dark game ground. */
.ivq{font-weight:inherit;font-size:1em !important;vertical-align:baseline;line-height:inherit}   /* the quality letter matches the number EXACTLY — !important beats `.dqopt span{font-size:12px}` which was shrinking the M (Nathan) */
.iv-maj{color:#f2b83f} .iv-min{color:#8f9ff0} .iv-perf{color:#5cc6d3} .iv-tt{color:#d3e64d}
/* ⚠️ …AND THE SAME COLLISION TAKES THE COLOUR, which the earlier fix missed. ivGlyph emits a <span>, so
   any `.dqopt span`-style rule (0,1,1) outranks a bare `.iv-min` (0,1,0) and the quality letter renders
   in --dim. Measured on the Ear quiz: the m in "m2" computed to rgb(159,182,189), plain grey, with the
   class correctly applied — the class being PRESENT is not the colour being APPLIED. The note beside
   .ivq above records this exact fight being won for font-size and nothing else; a fix whose reason is a
   rule has to sweep every property that rule governs. Compound selectors (0,2,0) win it without
   !important. */
.ivq.iv-maj{color:#f2b83f} .ivq.iv-min{color:#8f9ff0} .ivq.iv-perf{color:#5cc6d3} .ivq.iv-tt{color:#d3e64d}
/* ---- INTERVAL FAMILY color-wash (Nathan's sweet/savory/salty) — a subtle whole-tile shade UNDER the
   M/m letter color. Applies to NEUTRAL tiles only; solved (ok) / wrong (err) / dead states keep winning. ---- */
.cell.fam-sweet:not(.ok):not(.err):not(.dead):not(.free),
.dqopt.fam-sweet{background:rgba(90,169,230,.24)}
.cell.fam-savory:not(.ok):not(.err):not(.dead):not(.free),
.dqopt.fam-savory{background:rgba(217,154,78,.24)}
.cell.fam-salty:not(.ok):not(.err):not(.dead):not(.free),
.dqopt.fam-salty{background:rgba(232,86,74,.24)}   /* 🎨 N3: raise solo-tile flavor tint .15→.24 so the family reads clearly + match N11 recolor (sweet blue, spicy red) */
/* family trainer — the family IS the identity here, so a stronger wash + matching border */
.fam-row{display:flex;flex-direction:column;gap:10px;margin-top:14px}
.fambtn{display:flex;align-items:center;gap:14px;text-align:left;padding:15px 16px;border-radius:14px;border:1.5px solid var(--line);background:var(--panel2);color:var(--ink);font-weight:800;font-size:19px;cursor:pointer;transition:transform .08s}
.fambtn .fe{font-size:32px;line-height:1;flex:0 0 auto}
.fambtn .fl{display:flex;flex-direction:column;gap:3px;min-width:0}
.fambtn small{font-weight:500;font-size:11.5px;color:var(--dim);line-height:1.3}
.fambtn .fam-ivs{display:block;margin-top:6px;font-weight:800;font-size:14px;letter-spacing:.07em;opacity:.92}   /* branded interval abbreviations — teaching which intervals live in each family (Nathan) */
/* balanced luminance across the three so none looks pre-selected — sweet's rose read too bright / salty's slate too dim (Nathan) */
.fambtn.sweet{border-color:var(--fam-sweet);background:var(--fam-sweet-bg)}
.fambtn.savory{border-color:var(--fam-savory);background:var(--fam-savory-bg)}
.fambtn.salty{border-color:var(--fam-salty);background:var(--fam-salty-bg)}
.fambtn.sweet small,.fambtn.savory small,.fambtn.salty small{color:rgba(255,255,255,.74)}   /* blurb legible on the fuller tile fills */
.fambtn:active{transform:scale(.96)}
.fambtn{position:relative}
.fambtn.right{border-color:#3ecf7a;background:rgba(62,207,122,.22);box-shadow:0 0 0 2px rgba(62,207,122,.45)}
.fambtn.wrong{border-color:#ff6b6b;background:rgba(255,107,107,.2)}
/* superimposed ✓/✗ badge — no reflow (Nathan): correct = green ✓ bright, my wrong pick = red ✗; other wrongs dimmed */
.fambtn.right::after{content:'✓';position:absolute;top:7px;right:11px;font-size:20px;font-weight:900;color:#3ecf7a}
.fambtn.wrong::after{content:'✗';position:absolute;top:7px;right:11px;font-size:20px;font-weight:900;color:#ff6b6b}
.fambtn.dim{opacity:.4}
.fambtn.dim.wrong{opacity:.82}   /* my wrong pick stays readable so its red ✗ reads */
.fambtn.gated{opacity:.5}   /* locked until the clue has SOUNDED (hear-gate) */
.fambtn.playing{border-color:var(--accent2);box-shadow:0 0 0 2px rgba(45,172,179,.3)}   /* preview: the family you just auditioned */
/* 600ms cooldown after a tap — dim + untappable so it reads as "playing," not broken; the active family stays bright (Nathan) */
.fam-row.cooling .fambtn{opacity:.4;pointer-events:none;transition:opacity .1s}
.fam-row.cooling .fambtn.playing{opacity:1}
.fam-head{margin:2px 0 12px;flex-wrap:nowrap;border-radius:12px;padding:8px 10px}   /* doubles as a .hostbar (solo-style header); NEVER wrap — a wrapping pill was jumping the header (Nathan) */
.fam-head .hb-title{min-width:0}
.fam-head .hb-title b{display:block;overflow:hidden;text-overflow:ellipsis;max-width:100%}
.fam-head .hb-right{display:flex;align-items:center;gap:6px;flex:0 0 auto;flex-wrap:nowrap;margin-left:auto;white-space:nowrap}
.fam-head .scorechip{font-variant-numeric:tabular-nums}
/* per-pairing header background so a switch is unmistakable — matches the stage edge color (Nathan) */
.fam-head.fam-stage-0{background:rgba(232,112,154,.15)} .fam-head.fam-stage-1{background:rgba(217,154,78,.15)} .fam-head.fam-stage-2{background:rgba(143,163,189,.16)} .fam-head.fam-stage-3{background:rgba(45,172,179,.13)}   /* pairing tints aligned to the family palette (Nathan Option A) */
.fam-stage{font-weight:800;font-size:17px;margin-right:auto}
.kbplay{display:flex;flex-direction:column;align-items:center;gap:6px;width:100%;background:var(--panel2);border:1px solid var(--line);border-radius:14px;padding:15px;cursor:pointer;transition:transform .08s}
.kbplay:active{transform:scale(.99)}
.kbplay-svg{width:150px;height:58px;filter:drop-shadow(0 1px 2px rgba(0,0,0,.4))}
.kbplay-lbl{font-size:12px;color:var(--dim);font-weight:700;letter-spacing:.04em;text-transform:uppercase}
.fam-reveal{min-height:0;margin-top:0}   /* mnemonic moved UP into the cluefoot (Nathan) — this bottom block is now empty, so it reserves nothing */
.fam-kb{max-width:100%;margin:0 0 4px;padding:0;box-sizing:border-box;touch-action:none}   /* NO side padding — the swipe stays ON the keys, away from the screen edge, so iOS edge-back never fires (Nathan) */
.fam-kb .piano,.fam-kb svg{max-width:300px;height:auto;max-height:118px;display:block;margin:0;touch-action:none}   /* LEFT-aligned so the keyboard's left edge lines up with the text (Nathan); touch-action:none or the browser scrolls and cancels the drag */
.fam-kb svg *{touch-action:none}
.fam-hint{text-align:center;font-size:11.5px;color:var(--dim);margin:0 0 8px;letter-spacing:.02em}
/* first-time tap/swipe gesture popup (tone-neutral graphic — Nathan) */
.gpop-ov{position:fixed;inset:0;z-index:400;background:rgba(0,0,0,.62);display:flex;align-items:center;justify-content:center;padding:24px}
.gpop{background:var(--panel);border:1px solid var(--line);border-radius:16px;padding:22px 20px;max-width:340px;width:100%;max-height:90vh;overflow-y:auto;text-align:center;display:flex;flex-direction:column;gap:16px;align-items:center}   /* scroll on small screens / many-interval levels so it always fits an iPhone (Nathan) */
.gpop-svg{width:150px;height:80px}
.gpop-t{font-size:15px;line-height:1.5;color:var(--ink)}
.gpop .big{width:100%;text-align:center}
.gpop-fams{display:flex;flex-direction:column;gap:8px;width:100%;margin:2px 0}
.gpop-fam{border:1px solid var(--line);border-radius:10px;padding:9px 12px;text-align:left}
.gpop-fam b{display:block;font-size:15px} .gpop-fam small{color:var(--dim);font-size:11px}
.gpop-fam.fam-sweet{border-color:var(--fam-sweet)} .gpop-fam.fam-savory{border-color:var(--fam-savory)} .gpop-fam.fam-salty{border-color:var(--fam-salty)}
/* proof-of-ear tally — "prove 3 of each flavor to advance" (Nathan) */
.tm-proof{font-size:11px;font-weight:800;margin-top:6px;letter-spacing:.02em;opacity:.92}
.tm-proof .fam-sweet,.gridmsg .fam-sweet{color:var(--fam-sweet)} .tm-proof .fam-savory,.gridmsg .fam-savory{color:var(--fam-savory)} .tm-proof .fam-salty,.gridmsg .fam-salty{color:var(--fam-salty)}
/* intro flavor cards are tap-to-PREVIEW buttons; the NEW flavor blinks so it lands (Nathan: "oh wow, Savory") */
.gpop-fam-btn{display:block;width:100%;background:transparent;color:inherit;font:inherit;cursor:pointer;-webkit-tap-highlight-color:transparent}
.gpop-fam .fam-hearhint{display:block;margin-top:4px;font-size:10.5px;font-weight:800;letter-spacing:.02em;opacity:.8}
.gpop-fam.fam-sweet .fam-hearhint{color:var(--fam-sweet)} .gpop-fam.fam-savory .fam-hearhint{color:var(--fam-savory)} .gpop-fam.fam-salty .fam-hearhint{color:var(--fam-salty)}
/* 🎼 interval WELCOME preview — cards with a keyboard SVG + a random song revealed on play (Nathan) */
.ivintro-grid{flex-direction:row;flex-wrap:wrap;justify-content:center;gap:8px;width:100%}
/* 🩹 intervals-you'll-hear intro (Nathan): on iPad the 340px .gpop was a tall THIN strip with the "Let's play →"
   button buried below the scroll. Widen so cards flow into columns (not one narrow stack), cap the height, SCROLL the
   cards only, and PIN the button so the way-out is always visible + obviously present. */
.gpop-ivintro{max-width:min(720px,92vw);overflow:hidden}
.gpop-ivintro .ivintro-grid{flex:1 1 auto;min-height:0;overflow-y:auto;-webkit-overflow-scrolling:touch;align-content:flex-start;padding:2px}
.gpop-ivintro #ivOk{flex-shrink:0;position:relative;z-index:1;box-shadow:0 -14px 18px -12px var(--panel)}   /* top shadow = a cue that cards scroll above the pinned button */
.ivintro-card{flex:1 1 120px;min-width:0;max-width:168px;box-sizing:border-box;text-align:center;padding:8px 8px}   /* min-width:0 so cards can shrink to fit 2-up on the narrowest iPhone (no horizontal overflow) */
.ivintro-card>b{font-size:17px}
.ivintro-kb{background:#f3f6f6;border-radius:6px;padding:3px;margin:6px auto 2px;max-width:144px;pointer-events:none}
.ivintro-kb svg{width:100%;height:auto;display:block}
.ivintro-song{height:56px;overflow:hidden;font-size:11px;font-style:italic;line-height:1.24;color:var(--dim);margin:2px 0 0;display:flex;flex-direction:column;justify-content:center}   /* FIXED height (not just min) so the card + button NEVER change size whether the song is shown or not — zero layout shift (Nathan's pet peeve). Long songs clip rather than grow. */
.ivintro-song .mn-lead{font-style:normal;font-weight:800;cursor:pointer;color:#e8c14a}   /* the yellow "leap" syllables — TAP to hear the two notes melodically (Nathan) */
/* 🎨 level-intro cards: family color as a subtle BACKGROUND (the existing solo-tile pattern), not a border — a colored border read as right/wrong (Nathan) */
.ivintro-card.fam-sweet{border-color:var(--line) !important;background:rgba(90,169,230,.15)}
.ivintro-card.fam-savory{border-color:var(--line) !important;background:rgba(217,154,78,.15)}
.ivintro-card.fam-salty{border-color:var(--line) !important;background:rgba(232,86,74,.15)}
.gpop-fam.previewing{transform:scale(1.03)} .gpop-fam-btn{transition:transform .12s,box-shadow .18s}
.gpop-fam.is-new{animation:famNewBlink 1.05s ease-in-out infinite}
@keyframes famNewBlink{0%,100%{box-shadow:0 0 0 0 rgba(217,154,78,0)}50%{box-shadow:0 0 0 3px var(--fam-savory),0 0 14px rgba(217,154,78,.55)}}
@media (prefers-reduced-motion:reduce){.gpop-fam.is-new{animation:none;box-shadow:0 0 0 2px var(--fam-savory)}}
/* quiz progress bar — how many rounds left (Nathan) */
.fam-progress{height:6px;background:var(--panel2);border-radius:4px;overflow:hidden;margin:0 0 12px}
.fam-progress i{display:block;height:100%;background:var(--accent);border-radius:4px;transition:width .45s ease}
/* clear RIGHT/WRONG banner on the clue after a guess (Nathan) */
.fam-result{font-weight:800;font-size:15px;margin-bottom:4px}
.fam-result.ok{color:#7fd6a0} .fam-result.no{color:#ff8a8a}
/* stage-colored card edge so a task switch is unmistakable (Nathan) */
.fam-stage-0{border-left:4px solid var(--fam-sweet)} .fam-stage-1{border-left:4px solid var(--fam-savory)} .fam-stage-2{border-left:4px solid var(--fam-salty)} .fam-stage-3{border-left:4px solid var(--accent2)}
.fam-mnem{cursor:pointer}
.famex-kb{max-width:100%}
/* the example lives in its OWN card, visually separate from the taster menu so the keyboard doesn't look glued
   to the Spicy button (Nathan). Family label sits top-LEFT as the card header. */
.fam-excard{position:relative;background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:14px 12px;margin-top:16px}
/* GLOW while the clue hasn't sounded yet — "start here / tap to hear" (Nathan) */
.fam-excard.await-hear{animation:hearGlow 1.15s ease-in-out infinite}
@keyframes hearGlow{0%,100%{box-shadow:0 0 0 1px var(--line)}50%{box-shadow:0 0 16px 2px rgba(45,172,179,.55),0 0 0 1px rgba(45,172,179,.6)}}
@media (prefers-reduced-motion:reduce){.fam-excard.await-hear{animation:none;box-shadow:0 0 0 2px rgba(45,172,179,.5)}}
/* compact one-line result inside the reserved clue footer — interval sits inline so nothing grows (Nathan) */
.fam-result .fx-iv{font-weight:900} .fam-result .fx-long{color:var(--dim);font-weight:600;font-size:13px}
.fam-answer-detail{margin-top:2px}
.fam-answer-detail .fam-desc{text-align:left;font-size:14.5px;line-height:1.5;margin:0 0 2px}
.fam-exhead{text-align:left;font-weight:800;font-size:16px;margin:0 0 8px}
.fam-exhead.fam-sweet{color:var(--fam-sweet)} .fam-exhead.fam-savory{color:var(--fam-savory)} .fam-exhead.fam-salty{color:var(--fam-salty)}
.fam-excard .fam-hint{text-align:left;margin:6px 0 8px}   /* left-aligned to match the keyboard (Nathan) */
.fam-exrow{display:flex;align-items:baseline;justify-content:flex-start;gap:10px;margin-top:4px}
.fam-exrow .fx-iv{font-weight:800;font-size:19px}
.fam-exrow .fx-long{color:var(--dim);font-size:13px}
.fam-desc{font-size:14.5px;line-height:1.5;text-align:center}
/* clue footer — RESERVED height so the family buttons never bounce when the result appears; left-aligned (Nathan) */
.fam-cluefoot{min-height:184px;text-align:left;padding-top:8px}   /* reserved for the ANSWERED content (✓/✗ + description + 🎵 song + Next) so revealing it never shifts the family buttons (Nathan) — bumped to fit the mnemonic now that it lives here */
.fam-cluefoot .fam-desc{text-align:left;font-size:14px;margin:2px 0 0}
.fam-cluefoot .lbcta{text-align:center}
.fam-cluetip{color:var(--dim);font-size:14px;padding-top:26px;text-align:center}   /* the blind prompt lives in the same reserved space, centered under the clue */
/* TASTER clue keyboard — centered (Nathan: "center the chord"); the description below stays left-aligned for reading */
#famClueKb{margin:0 auto}
#famClueKb .piano,#famClueKb svg{margin:0 auto}
/* ===== TASTING BINGO (family-binary board) ===== */
.tb-clue{margin:10px 0 14px}
.tb-clue.await{animation:hearGlow 1.15s ease-in-out infinite}
.tb-clue .tb-clue-kb{margin:0 auto;max-width:320px}   /* CENTER the clue keyboard (Nathan) */
.tb-clue .tb-clue-kb .piano,.tb-clue .tb-clue-kb svg{margin:0 auto;max-width:100%}
.tb-clue .fam-cluefoot{text-align:center}
.tb-clue .fam-cluetip{padding-top:12px}
/* family-coated tile face (Tasting Menu in the real engine) — big emoji until a clue lands */
.cell .iv.famcoat{font-size:34px;line-height:1}
.tm-vs{font-size:22px;line-height:1.2} .tm-vs span{color:var(--dim);font-weight:600;font-size:15px;margin:0 4px}
.tm-sub{font-size:13px;color:var(--dim);font-weight:700;letter-spacing:.03em;text-transform:uppercase}
/* last-week's tourney winner — a distinct, link-like color so it reads as a person/role (even if not clickable) — Nathan */
.tour-winner{color:#7fd0ff;font-weight:800;text-decoration:underline;text-decoration-color:rgba(127,208,255,.5);text-underline-offset:2px}
.tb-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;max-width:420px;margin:0 auto}
.tb-cell{aspect-ratio:1;border-radius:14px;border:2px solid var(--line);background:var(--panel2);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;cursor:pointer;position:relative;overflow:hidden;transition:transform .08s}
.tb-cell:active{transform:scale(.96)}
.tb-cell.sweet{background:var(--fam-sweet-bg);border-color:var(--fam-sweet)}
.tb-cell.salty{background:var(--fam-salty-bg);border-color:var(--fam-salty)}
.tb-cell .tb-coat{font-size:30px;opacity:.85}
.tb-cell.free{background:linear-gradient(135deg,rgba(45,172,179,.22),rgba(45,172,179,.05));border-color:var(--accent2);cursor:default}
.tb-cell.free span{font-weight:800;color:var(--accent2);letter-spacing:.04em}
.tb-cell.claimed{outline:3px solid var(--accent2);outline-offset:-2px;cursor:default}
.tb-cell.locked{opacity:.45;cursor:default;filter:grayscale(.4)}
.tb-cell .tb-iv{font-weight:900;font-size:18px;line-height:1}
.tb-cell .tb-kb{width:100%;max-width:96px;pointer-events:none}
.tb-cell .tb-kb .piano,.tb-cell .tb-kb svg{max-width:100%;height:auto;max-height:40px;display:block;margin:0 auto}
.tb-grid.done .tb-cell{cursor:default}
.fam-mnem{font-size:13px;margin-top:8px;color:var(--ink)}
.fam-mnem .mn-lead{color:#e8c14a;font-weight:800}   /* the interval-leap syllables, color-coded inside the brackets */
.fam-mnem .mn-song{color:var(--dim);font-style:italic}
/* the clue keyboard PULSES while blind, inviting a touch */
@keyframes fam-pulse{0%,100%{opacity:.66}50%{opacity:1}}
.fam-kb.pulse{animation:fam-pulse 1.4s ease-in-out infinite}
@media (prefers-reduced-motion:reduce){ .fam-kb.pulse{animation:none;opacity:.85} }
.fam-desc .ivname{font-weight:800}
.fam-desc .fam-sweet{color:var(--fam-sweet)} .fam-desc .fam-savory{color:var(--fam-savory)} .fam-desc .fam-salty{color:var(--fam-salty)}   /* family text tint on the reveal */
.famrev-btn{padding:5px 10px;font-size:12px}
.famrev-btn.on{border-color:var(--accent2);color:var(--accent2)}
.fam-session{margin-top:16px;border-top:1px solid var(--line);padding-top:12px}
.fam-session-h{font-size:11px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;color:var(--dim);margin-bottom:8px}
.frow{display:flex;align-items:center;gap:10px;padding:7px 4px;border-bottom:1px solid rgba(255,255,255,.05);font-size:14px}
.frow .fr-iv{flex:0 0 42px;font-weight:800}
.frow .fr-fam{flex:1;font-weight:700}
/* these three were the PRE-N11 colours — pink sweet and blue-grey salty — surviving in the review list */
.frow .fr-fam.fam-sweet{color:var(--fam-sweet)} .frow .fr-fam.fam-savory{color:var(--fam-savory)} .frow .fr-fam.fam-salty{color:var(--fam-salty)}
.frow .fr-pick{font-size:12.5px;color:var(--dim)}
.frow.no .fr-pick{color:#ff8a8a}
.frow.ok .fr-pick{color:#7fd6a0}
.fambtn:disabled{cursor:default;opacity:.85}
.fam-why{min-height:40px;text-align:center;font-size:14px;color:var(--dim);margin-top:12px;line-height:1.4}
.fam-why b{color:var(--ink)}
.fam-play{display:flex;align-items:center;justify-content:center;gap:10px;margin:6px 0 2px}
.ivn-maj{color:#fff}

.ob-locked{opacity:.42;cursor:not-allowed}
.big:disabled{cursor:not-allowed}

/* primary (teal) button subtitle was --dim grey on teal = illegible (Nathan). Use the button's ink at reduced opacity. */
.big.primary small{color:var(--accent-ink);opacity:.78}

/* ── Progressions mode: the clue's chord slots are the primary tap target ──────────────────────
   They are what you press to hear one chord on its own and to take a pick back, so they get a real
   touch target (44px) and sit hard left where the thumb lands, instead of 26px centerd. (Nathan) */
/* Big touch targets, but they must never be wider than their column — at 8 chords a fixed 44px
   overflowed the card. Caps at 44px and shrinks only when the column forces it. */
.chordcard.pgcard .bul{width:min(38px,100%);height:auto;aspect-ratio:1;font-size:clamp(11px,3.2vw,15px);font-weight:800;box-sizing:border-box;margin-bottom:2px}
.chordcard.pgcard .cc-anscap{gap:8px;flex-wrap:wrap}
.chordcard.pgcard .cc-ivtag{padding:4px 9px;line-height:1.5}

/* ── Progressions clue: HORIZONTAL slots, one column per chord ─────────────────────────────



.pg-row.pend{background:rgba(255,255,255,.03)}
.pg-hold{opacity:.35;font-style:normal}
.pg-tot b.pend{opacity:.55;font-weight:700}   /* SAME size as the score it becomes — a smaller one grew the card on grading */
.pg-msg{min-height:5.4em;display:block}   /* RESERVED for the tallest state — a wrapped level/score line PLUS the
   wrapped "right relationship, different key" sentence, both at 390px. A second .pg-msg rule further down
   was silently overriding this with 1.4em, so the reading note grew the box and pushed the footer 15px
   (Nathan's standing no-layout-shift rule). One rule now; measure before changing it. */

/* ── Progressions clue: one COLUMN per chord, under its own number ───────────────────────────
   Vertical rows grew the card with every extra chord; columns stay one row tall at any length. */
.pg-slots{display:grid;gap:4px;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}
.pg-slot{position:relative;display:flex;flex-direction:column;align-items:center;gap:0;padding:4px 2px 3px;border-radius:9px;background:rgba(255,255,255,.045);min-width:0;cursor:pointer}
/* ⚠️ THE VERDICT IS A STROKE, NOT A FILL. Nathan: "correct and incorrect can be designated with a
   stroke rather than a fill." These three used to tint the tile's BACKGROUND, which put the verdict in
   direct competition with the major/minor wash for the same surface — two channels arguing over one
   pixel, which is a large part of why the colors stopped reading as a system. The fill now belongs
   entirely to QUALITY and the outline entirely to the VERDICT, so a tile can say both at once without
   either shouting over the other. (They never collide with the aim cursor, which is also an outline:
   the cursor exists only before a hand is graded, the verdict only after.) */
.chordcard.pgcard .pg-slot.ok{   border-color:#5ED68F }
.chordcard.pgcard .pg-slot.part{ border-color:#F0C450 }
.chordcard.pgcard .pg-slot.bad{  border-color:#E8806F }
.pg-slot.cur{outline:2px solid var(--accent2);outline-offset:-2px;background:rgba(45,172,179,.22);
  box-shadow:0 0 0 3px rgba(45,172,179,.20)}
.pg-slot.cur::before{content:"";position:absolute;top:-1px;left:50%;transform:translateX(-50%);
  width:0;height:0;border-left:5px solid transparent;border-right:5px solid transparent;
  border-top:5px solid var(--accent2);pointer-events:none}
.pg-slot.cur .dial-core{animation:pgTarget 1.5s ease-in-out infinite}
@keyframes pgTarget{0%,100%{box-shadow:0 0 0 0 rgba(45,172,179,.55)}
                    50%{box-shadow:0 0 0 5px rgba(45,172,179,0)}}
/* a slot that already holds a vote reads as SETTLED; an empty one stays faint */
.pg-slot .bul.filled .dial-core{box-shadow:0 0 0 2px rgba(255,255,255,.42) inset}
@media (prefers-reduced-motion:reduce){ .pg-slot.cur .dial-core{animation:none} }
.pg-slot .bul{margin-bottom:2px}
.pg-you{font-size:11.5px;font-weight:800;opacity:.8;line-height:1.35;max-width:100%;overflow:hidden;text-overflow:ellipsis}
/* the pill for a chord you got wrong, in the your-read row — same red the tile's ✗ uses, so the row
   and the tile say the same thing. Stroke only: the fill still carries the chord's own identity. */
.tok.tok-miss{ box-shadow:0 0 0 2px #FF6B6B, 0 0 6px rgba(255,107,107,.45); }
.pg-was{font-size:11.5px;font-weight:700;line-height:1.35;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}

.pg-hold{opacity:.3;font-style:normal}
.pg-sc{font-size:11.5px;font-weight:800;font-variant-numeric:tabular-nums;line-height:1.35}

.pg-tot{font-size:19px;font-weight:800}
/* the PROPOSED points, printed beside the % while we compare the two systems */
.pg-pts{display:block;font-size:12px;font-weight:800;color:var(--accent2);letter-spacing:.01em;margin-top:1px}
/* not a deduction — points that were available and went unclaimed. Amber, not red: red is for a
   thing that went wrong, and correcting a chord is not that. */
.pg-pen{font-style:normal;color:#C9A63A;font-weight:700;font-size:10.5px;margin-left:6px;opacity:.9}
/* A swipe between clues has to READ as movement — the card was being replaced in place, which looked
   like nothing happened (Nathan). Out in the swipe's direction, in from the other side. */
@keyframes pgSlideOutL{to{transform:translateX(-38%);opacity:0}}
@keyframes pgSlideOutR{to{transform:translateX(38%);opacity:0}}
@keyframes pgSlideInL{from{transform:translateX(38%);opacity:0}}
@keyframes pgSlideInR{from{transform:translateX(-38%);opacity:0}}
.pg-slide-out-left{animation:pgSlideOutL .13s ease-in forwards}
.pg-slide-out-right{animation:pgSlideOutR .13s ease-in forwards}
.pg-slide-in-left{animation:pgSlideInL .26s cubic-bezier(.22,.9,.3,1)}
.pg-slide-in-right{animation:pgSlideInR .26s cubic-bezier(.22,.9,.3,1)}
@media (prefers-reduced-motion: reduce){
  .pg-slide-out-left,.pg-slide-out-right,.pg-slide-in-left,.pg-slide-in-right{animation:none}
}
.pg-tot.ok{color:var(--good)} .pg-tot.bad{color:var(--bad)}
.pg-prog{font-size:12px;opacity:.5;font-weight:700;margin-top:2px}
/* Occupies the SAME reserved line as .pg-prog, so revealing a clue never moves the card: the key the
   clue actually sounded in, and — only when the grader accepted a transposition — the key your own
   numerals put it in. */
.pg-keyline{font-size:12px;opacity:.7;font-weight:700;margin-top:2px;line-height:1.3;
            display:inline-flex;align-items:center;gap:4px}
/* ── READING STRIP ─────────────────────────────────────────────────────────────────────────────
   Sits where the "Your card" heading was, directly above the wheel. FIXED height, always in the DOM,
   whether or not there is a message — the chooser below it must never move up or down (Nathan). Reserved
   for the tallest state: the two-line "read correctly / equally valid" sentence at 390px. */
/* ⚠️ SIZED FOR ONE LINE, NOT TWO. 62px was the tallest state the strip can reach — a two-line
   sentence — reserved permanently, so a hand with one line (or, on an all-correct hand, no words at
   all) left a visible band of nothing between the clue card and the wheel. That band is what Nathan
   photographed. The reserve now fits the common case and `.tall` expands for the rare long sentence;
   collapsing it entirely was rejected because the wheel must not move under a finger that is still
   using it. Reclaims 34px on every hand. */
.pg-read-strip{height:34px;flex-direction:column;gap:1px;overflow:visible;box-sizing:border-box;display:flex;align-items:center;justify-content:flex-start;padding-top:2px;
  text-align:center;margin:4px 0 4px;padding:0 2px;
  font-size:12px;line-height:1.45;color:var(--dim);font-weight:600}
/* The sentence is INLINE text, not flex items. As flex children every <b> became its own item with a gap,
   which spread the chords apart and split "V → I" across two lines. */
.pg-read-strip>/* badge BESIDE the comparison, not stacked above it — stacking pushed the grid into the card */
.pg-line-in{display:flex;align-items:center;justify-content:center;gap:8px;width:100%;flex-wrap:wrap;
  row-gap:2px}   /* wrap: at four chords the verdict ran off the right edge at 390px */
.pg-read-strip b{font-weight:800}
.pg-pair{white-space:nowrap;display:inline-block;margin:0 2px}

/* ── ONE TOKEN VOCABULARY (Nathan) ────────────────────────────────────────────────────────────
   A chord NAME is a round color-coded chip. A FUNCTION numeral is a squarer, quieter pill. Both take
   the slot's color from --tc, so the two notations of one chord read as a pair wherever they appear —
   the reading strip and the clue card use exactly the same shapes. */
.tok{display:inline-block;font-weight:800;line-height:1;vertical-align:middle;margin:0 2px;
     font-variant-numeric:tabular-nums;white-space:nowrap}
/* 🕶 THE PILL SITS ON TOP OF A WEDGE, SO IT NEEDS SOMETHING BEHIND IT. The fill was the chord's own
   color at 14% over transparent, which is fine on the page's dark ground and nearly invisible when the
   pill lands on a bright wedge — the name washed straight into the color underneath. It now carries a
   dark ground of its own with the tint on top, and the same dark ring the numeral badge already had,
   which is the "border that makes it look like it has a shadow" worth giving all of them. (Nathan.) */
.tok-ch{position:relative;border-radius:999px;padding:4px 11px 4px 8px;min-width:22px;text-align:center;
        color:var(--tc);border:1.5px solid var(--tc);
        background:color-mix(in srgb, var(--tc) 16%, var(--bg));
        box-shadow:0 0 0 1.5px rgba(6,18,22,.78), 0 1px 3px rgba(0,0,0,.45);
        margin:0 calc(13px * var(--txt-scale)) 0 3px}   /* the numeral badge hangs ~9px past the right
           edge — the next pill has to start clear of it or they collide (visible in the reading strip
           with three or four chords). ⚠️ The gap scales with the numeral: now that the disc grows with
           the text setting, a fixed 13px would let a large numeral crash into the next chord. */
/* The Roman numeral is its own little disc floating off the pill's top-right corner, deliberately
   breaking the border — one object per chord, but the function reads as a separate mark on it (Nathan).
   Solid page-colored fill so it punches cleanly through the pill's edge instead of blending into it. */
/* ── ♿ AND THE NUMERAL DISC GROWS TOO. Nathan, 2026-09-27: "There are other Roman numerals that need
   to grow." This one was a flat 9px, so at the largest text setting the chord name grew 70% and the
   numeral beside it did not move — which is the one pairing the game is teaching. It keeps its RATIO to
   the name (it must stay smaller; equal size read "like AV"), and the disc that holds it grows with it,
   so a scaled numeral is not clipped by a pill sized for 9px. */
.tok-rn{position:absolute;top:calc(-8px * var(--txt-scale));right:calc(-6px * var(--txt-scale));
        font-size:calc(9px * var(--txt-scale));font-weight:800;line-height:1;
        min-width:calc(15px * var(--txt-scale));padding:calc(2.5px * var(--txt-scale)) calc(4px * var(--txt-scale));border-radius:999px;letter-spacing:.02em;
        color:var(--tc);background:var(--bg);border:1.5px solid var(--tc);opacity:1;
        box-shadow:0 0 0 1.5px rgba(6,18,22,.78);
        display:flex;align-items:center;justify-content:center}
.tok-fn{border-radius:4px;padding:2px 6px;font-size:.92em;
        color:var(--tc);border:1px solid color-mix(in srgb, var(--tc) 45%, transparent);opacity:.85}
@supports not (color: color-mix(in srgb, red 10%, transparent)){
  .tok-ch{background:var(--bg)} .tok-fn{border-color:var(--tc)}   /* still a solid ground, just untinted */
}
.pg-line .tok{margin:0 1px}
/* on a board tile the token uses the tile's own function color, not a slot color */
.cell .tilewrap{overflow:visible;display:flex;justify-content:center;align-items:center;padding-top:4px}
.cell .tilewrap .tok-ch{--tc:var(--ink);border-color:rgba(255,255,255,.28);background:transparent;
  font-size:13px;margin:0 7px 0 0;padding:3px 7px}
.cell .tilewrap .tok-rn{background:var(--panel);border-color:rgba(255,255,255,.32);color:var(--dim);
  top:-7px;right:-8px}
.tok-arr{font-style:normal;opacity:.45;font-weight:700;margin:0 1px}
/* ── 🔤 THE FUNCTION MUST NOT SIT ON THE LETTER. `.tok-rn` is anchored by its RIGHT edge just outside the
   pill, so a wide numeral grows LEFTWARD straight across the chord name. Short ones (IV) barely reach,
   which is why only some pills looked wrong — Nathan: "the functions are crowding the actual letters…
   the F is mostly covered up… it could say G flat because it's covered." ♭II, ♭III and ♭VII are the wide
   ones, and they were burying the letter underneath them.
   In the read strip the badge is now anchored from the LEFT edge of the pill's right side, so it always
   grows outward into room reserved for it, and the letter is never covered. Scoped to this strip on
   purpose: the same markup is used by the clue tiles, which are not showing the problem. */
.pg-stacks .tok-ch{ margin-right:16px }
.pg-stacks .tok-rn{ right:auto; left:100%; margin-left:3px; top:-6px }
.pg-stacks .pg-stack{ margin-right:4px }

.tok-sep{opacity:.35;margin:0 6px;font-weight:800}

/* the SLOT carries the verdict now that the wrong guess is gone from the line */
/* The border lives on EVERY slot (transparent until there's a verdict) so adding it on grading cannot
   change the box and shift the page — putting it only on .bad/.part moved the wheel 4px. */
.pg-slot{border:2px solid transparent;border-radius:8px}
.pg-slot.bad{background:rgba(230,57,70,.20);border-color:rgba(230,57,70,.55)}
.pg-slot.part{background:rgba(232,193,74,.14);border-color:rgba(232,193,74,.4)}
/* the badge names which ear is being graded — relative now, perfect-pitch later */
.pg-badge{display:inline-block;margin-right:6px;line-height:1;font-size:9.5px;font-weight:800;letter-spacing:.1em;text-transform:uppercase;
  border-radius:999px;padding:2px 8px;border:1px solid currentColor;flex:none;vertical-align:1px}
.pg-badge.rel{color:var(--accent2);background:rgba(45,172,179,.13)}
.pg-badge.perf{color:var(--band-3);background:rgba(232,193,74,.13)}
/* you landed on the actual chords — a pat on the back, never points (Nathan) */
.pg-badge.abs{color:var(--good);background:rgba(62,207,122,.13)}
/* the badge is a BUTTON that swaps the card between your reading and what was played — it has to look
   like it does something (Nathan), so it carries a swap glyph and a pressed state */
/* the label sat high in the pill and was cramped — even padding, real line-height, room to breathe */
/* BIGGER, with a tick — a relative reading is a correct answer and the control has to read as positive
   before it reads as anything else (Nathan). Hold it to peek at what actually sounded. */
/* FIXED WIDTH: "relative" and "as played" are different lengths, and a resizing pill shifted the whole
   row sideways on every flip. The tick is a SIBLING outside it, so the pill's box never changes. */
.pg-framewrap{display:inline-flex;align-items:center;gap:6px;flex:none}
.pg-frame{cursor:pointer;font:inherit;font-size:11px;font-weight:800;letter-spacing:.08em;
  text-transform:uppercase;display:inline-flex;align-items:center;justify-content:center;
  line-height:1;padding:7px 4px;min-height:28px;width:104px;box-sizing:border-box;border-width:1.5px;
  touch-action:none;user-select:none;-webkit-user-select:none;-webkit-touch-callout:none}
/* outside the pill — it marks that the ANSWER was right, which is true in either frame */
.pg-frame-tick{font-style:normal;font-size:15px;font-weight:900;line-height:1;color:var(--good);flex:none}
/* a reading that was NOT valid: neutral, no tick — the green ✓ RELATIVE badge is positive feedback and
   was appearing on clues that scored zero (Nathan) */
.pg-badge.missread{color:var(--dim);background:rgba(255,255,255,.05);border-color:var(--line)}
.pg-badge.missread[aria-pressed="true"]{color:#0d2027;background:var(--good);border-color:var(--good)}
.pg-frame:hover{filter:brightness(1.25)}
.pg-frame:active{transform:translateY(1px)}
/* AS PLAYED = a temporary annotation, so ONE green across the badge, the clue card and the wheel marks.
   The color flip is the whole affordance — no glyph needed (Nathan). */
.pg-frame[aria-pressed="true"]{color:#0d2027;background:var(--good);border-color:var(--good)}
.chordcard.pg-asplayed{border-color:var(--good)!important;box-shadow:0 0 0 1px var(--good) inset}
.chordcard.pg-asplayed .tok-ch{--tc:var(--good)!important}
.chordcard.pg-asplayed .pg-tot{color:var(--good)}
.chordcard.pg-asplayed .pg-slot.bad{background:rgba(62,207,122,.10);border-color:rgba(62,207,122,.35)}
/* ONE CLAUSE PER LINE. "…is equally valid" was wrapping onto a centerd line by itself and reading as a
   fragment; a clause now breaks as a unit or not at all. */
.pg-clause{display:inline-block;margin:2px 4px}
.tok-green{--tc:#8FE34A}
/* as-played tints the LABEL, not the wedge — the thumbnail keeps its slot color so it stays readable */
.pg-dial{ }
/* names hidden: the pill is the numeral alone, so a perfect-pitch ear can't read the root off the label */
.tok-fnonly{padding:4px 10px;min-width:30px;font-size:.98em}
/* the reading comparison: a label column + one auto column per chord, so chord 1 sits above chord 1 and
   you can read the pairs DOWNWARD (Nathan). Auto columns size to the widest cell in each column. */
.pg-cmp{display:grid;grid-template-columns:auto repeat(var(--n),auto) auto;
  align-items:center;justify-items:start;gap:1px 8px;text-align:left}
/* ── STACKED READINGS (Nathan) ────────────────────────────────────────────────────────────────
   The same chord, read two ways, sitting on top of each other: the one behind offset up-and-left with a
   shadow so it reads as a second card, the one in front OPAQUE so nothing bleeds through it. Holding the
   badge flips which is on top. */
/* the FRONT pill sits on the row's center line with the badge; the back one overhangs above it, so the
   line reads straight across and only the shadow card is off-axis (Nathan) */
.pg-stacks{display:inline-flex;align-items:center;gap:22px;padding:0 0 0 12px}
/* A box you got RIGHT has only one answer to show, so it is a single pill in normal flow — no hidden
   sizer behind it, nothing absolutely positioned. It still takes ONE position in the row, so a
   four-chord progression reads as four whether you got them right or wrong. (Nathan: "I'm looking for
   a chord pattern of four and because I got one wrong it's five.") */
.pg-stack.solo{position:relative;display:inline-block;line-height:1}

/* ── THE KEYBOARD STRIP ─────────────────────────────────────────────────────────────────────────
   Takes the color legend's line once the clue is graded. The keys themselves are buildPiano()'s
   branded-eraser keyboard (.piano/.wkey/.bkey + eraser markers), styled once elsewhere and shared with
   Sing, the taster and the clue reveal — so there are NO key colors defined here on purpose. */
.pg-keys{display:flex;flex-direction:column;align-items:center;gap:3px;width:100%}
/* the transport that used to be crammed into the clue card, given a row of its own */
/* ── THE PROGRESS BAR, inside the level picker, across its bottom. 3px of an existing control instead
   of a 24px row of its own. A readout, not a control: <i> elements, no handlers, no hit targets. */
.pg-streak{font-weight:800;color:#ffd75e;white-space:nowrap;letter-spacing:.01em}
.pg-streak i{font-style:normal;font-weight:700;color:rgba(255,255,255,.45)}
/* ACROSS THE BOTTOM, on its own line. Left inline it took all the remaining width of the row and
   pushed the points and the caret off the end of the button. flex-basis:100% makes the row wrap once,
   and the bar owns the second line at 3px — still cheaper than the 24px row it replaced. */
/* The button is a flex COLUMN, so the bar is simply the next row — no wrapping needed. align-self
   stretches it to the button's width instead of 100% of a box that is wider than the screen. */
/* ── THE PROGRESS METER: one rule that fills, plus the streak in words beside it. No segments — a bar
   per hand was clutter for something nobody reads hand-by-hand, and the space it frees pays for the
   two numbers that actually mean something. */
.cardots.pg-progress,.solo-wrap .cardots.pg-progress{display:flex;align-items:center;gap:10px;
  max-width:330px;width:100%;margin:0 auto 7px;padding:0;height:auto;box-sizing:border-box}
.pg-progress .pgmeter{flex:1 1 auto;min-width:0;height:3px;border-radius:2px;
  background:rgba(255,255,255,.13);overflow:hidden;display:block}
.pg-progress .pgmeter i{display:block;height:3px;border-radius:2px;background:var(--accent2);
  transition:width .35s ease}
/* spelled out, because a glyph is something you have to be taught and a word is not */
.pg-progress .pgstreak{flex:0 0 auto;font-size:10.5px;font-weight:600;color:rgba(255,255,255,.5);
  white-space:nowrap;letter-spacing:.01em}
.pg-progress .pgstreak b{font-weight:800;color:#ffd75e;font-size:11.5px}
.pg-progress .pgbest{color:rgba(255,255,255,.35)}
/* ── 🎚️ MAJOR / MINOR: the slot as one question with two answers. Same grid and the same positions as
   the ordinary slots, so the card keeps its shape and nothing else on the screen has to know. */
/* ── 🎡 THE INSTRUCTION ROLLER. Big enough to read at a glance from across a room, quiet enough to
   ignore. It lives in the reading strip, which is empty exactly when the roller has something to say —
   and is hidden the moment anything else needs that space. */
.ct-tips{position:absolute;left:0;right:0;top:0;height:100%;display:flex;align-items:center;
  justify-content:center;pointer-events:none;overflow:hidden;opacity:0;transition:opacity .2s ease}
.ct-tips.on{opacity:1}
.ct-tiproll{display:block;font-size:17px;font-weight:700;letter-spacing:.01em;color:#8fb3bb;
  white-space:nowrap;max-width:100%;overflow:hidden;text-overflow:ellipsis}
/* the odometer: each new line arrives from below and settles */
.ct-tiproll.go{animation:ctRoll .42s cubic-bezier(.22,.9,.24,1) both}
@keyframes ctRoll{ from{ transform:translateY(90%); opacity:0 } to{ transform:none; opacity:1 } }
@media (prefers-reduced-motion:reduce){ .ct-tiproll.go{animation:none} }

.pg-slot.mm{gap:5px;padding:6px 3px;justify-content:center}
.pg-slot.mm .mm-n{font-size:10px;font-weight:800;color:rgba(255,255,255,.35);line-height:1}
/* ⚠️ ONE ROW, FORCED. The slot's own content was wide enough to push the grid into two rows even with
   four 1fr columns — the MAJ/MIN pair has a min-content width the grid was respecting. min-width:0 on
   the slot and the buttons lets them shrink into their column instead of overflowing it, so the card
   stays exactly as tall as the standard one. (Nathan: "the clue card cannot take any more space in
   this mode than it does in standard. One row.") */
.pg-slots .pg-slot.mm{min-width:0}
.pg-slot.mm .mm-b{min-width:0;overflow:hidden}
/* ⚠️ ONE ROW, LIKE THE STANDARD CARD. Stacking MAJ over MIN doubled the slot's height, which pushed
   four slots into a 2×2 grid and made this mode taller than the normal game. (Nathan: "it's breaking
   into a 2×2 grid... the clue card cannot take any more space in this mode than it does in standard.
   One row.") Side by side, the pair costs the same height as one ordinary slot. */
.mm-sw{display:flex;flex-direction:row;gap:3px;width:100%;padding:0 2px;box-sizing:border-box}
.mm-b{flex:1 1 0;min-width:0;appearance:none;border:1px solid var(--line);background:rgba(255,255,255,.04);
  color:var(--dim);border-radius:7px;padding:9px 2px;font-size:10px;font-weight:800;letter-spacing:.03em;
  cursor:pointer;-webkit-tap-highlight-color:transparent;touch-action:manipulation;
  transition:background .14s,color .14s,border-color .14s}
.mm-b:active{transform:scale(.96)}
.mm-b.on{background:var(--accent2);border-color:var(--accent2);color:#062024}
/* once graded, the RIGHT answer is outlined whether or not you chose it — the point of the mode is to
   learn which one it was, so it must be legible even on a box you got wrong */
.mm-b.truth{border-color:#8FE34A;color:#8FE34A}
.mm-b.on.truth{background:#8FE34A;border-color:#8FE34A;color:#062024}
.mm-b[disabled]{cursor:default;opacity:.92}
/* 👍 PLAY OWNS THE MIDDLE OF THIS ROW. Nathan: "let's make that play button bigger on the bottom. It
   can crowd some of the other elements. It should be a really big clear thumb target right in the
   center at all times." So the gap closes and the neighbours give way — the row is centerd on one
   control and the mic / LOOP / next are what has to yield, not the other way round. */
/* ⚠️ A GRID, NOT A CENTRED FLEX ROW. Centring a flex row centers the ROW, so play drifted off the
   screen's middle by however much the left and right neighbours differed — measured 10px left, and it
   would move again the moment "next" appeared. Three columns with play in the fixed middle one puts
   it on the center line no matter what flanks it. Nathan: "right in the center at all times." */
/* ⚠️ THE BIG BUTTON MUST NOT COST THE KEYBOARD ITS ROOM. Going from 60px to 84px added 32px to this
   row and pushed the whole bottom strip 17px off the bottom of the screen — measured, keys wrap
   bottom=973 against a 956 viewport, which is why Nathan saw the keyboard "pushed down… using so much
   extra room". It was me. The button stays large; it just stops charging the row for all of it, by
   overhanging upward into the wheel's own bottom padding (the wheel is already tucked under the strip
   above it, so a few px there costs nothing and is not a tap target). */
.pg-transport{display:grid;grid-template-columns:1fr auto 1fr;align-items:center;gap:12px;width:100%;min-height:0;position:relative}
/* ── 🐞 THE REPORT BUTTON. Out of the grid flow on purpose: the left cell already stacks the bass clef
   and the mic, so a third grid child would have landed on top of them. Absolute at the left edge means
   it cannot move the play button off center — Nathan's standing rule is no layout shift — and it only
   exists on a graded hand, so it is never competing with the controls during play.
   Deliberately quiet: this is a safety valve, not a call to action. It brightens on press. */
.pg-bugb{
  position:absolute; left:0; top:50%; transform:translateY(-50%);
  width:30px; height:30px; padding:0; line-height:1; font-size:15px;
  display:flex; align-items:center; justify-content:center;
  background:rgba(255,255,255,.06); border:1px solid var(--line); border-radius:999px;
  opacity:.82; transition:opacity .2s ease, border-color .2s ease; cursor:pointer;
}
/* ⚠️ IT WAS TOO FAINT TO READ AND SAID NOTHING. At .38 opacity on a dark board the glyph barely existed,
   and a lone bug with no word beside it does not explain itself. Nathan: "the transparency of the bug
   button makes it hard to read. It's not clear what that button is or what it does." It now has a
   readable weight, a faint ground so it reads as a button, and the word "report" beside it — it only
   appears on a graded hand, so there is room for two syllables. */
.pg-bugb::after{
  content:"report"; margin-left:5px; font-size:9.5px; font-weight:700;
  letter-spacing:.02em; color:rgba(255,255,255,.72);
}
.pg-bugb{ width:auto; padding:0 8px 0 6px; gap:0 }
.pg-bugb:hover{ opacity:.85; border-color:var(--accent2) }
.pg-bugb:active{ opacity:1; transform:translateY(-50%) scale(.94) }
/* on a phone the left cell's control sits near the middle of its column, so the far-left edge is clear;
   shrink a touch anyway so a narrow screen never crowds it */
@media (max-width:420px){ .pg-bugb{ width:26px; height:26px; font-size:13px } }
@media (prefers-reduced-motion:reduce){ .pg-bugb{ transition:none } }
.pg-transport>*{justify-self:center}
.pg-transport>.pg-playbtn{grid-column:2}
/* everything that is not play shares the outer columns and wraps rather than pushing the center */
/* ⚠️ THE MIC MOVES AWAY FROM PLAY. justify-self:end pinned it right up against the play button, and
   play is 76px of thumb target — Nathan: "move the dictation button a little farther away from the
   play button, it's too hard to hit by accident." Hands-free is a MODE switch: hitting it by mistake
   does not just make a wrong sound, it changes how the game behaves. It sits at the outside edge of
   its column now, as far from play as the row allows. */
/* between the two extremes: `end` had it touching play, `start` flung it to the screen edge. Centerd in
   its own column it keeps a comfortable gap from play without looking exiled. (Nathan, both ways:
   "too hard to hit by accident" then "now too far over".) */
.pg-transport>.pg-micb{grid-column:1;justify-self:center}
/* NOWRAP on purpose: wrapping to a second line would change the row's height and shove the keyboard,
   which is the layout shift we just spent a build removing. They shrink instead. */
.pg-transport>.pg-tright{grid-column:3;justify-self:start;display:flex;align-items:center;gap:8px;min-width:0;flex-wrap:nowrap}
@media (max-width:410px){
  .pg-transport{gap:8px}
  .pg-transport .pg-loopb,.pg-transport .pg-nextb{padding:7px 10px;font-size:10.5px}
  .pg-transport .pg-playbtn{width:76px;height:76px}
  .pg-transport .pg-playbtn .pg-playtri{margin-left:6px;border-left:26px solid #062024;
    border-top:17px solid transparent;border-bottom:17px solid transparent}
}
/* ⚠️ .pg-loopb carries `margin:5px auto 0` from its old life stacked in the card — `auto` in a flex ROW
   is a spring, which is why the transport sat with the mic jammed at x=3 and LOOP flung to x=247. */
.pg-transport .pg-loopb{display:inline-block;margin:0;padding:8px 16px;font-size:11px;line-height:1.4}
/* the way on, once there is one. Quietly insistent: it breathes so the eye finds it without it
   shouting over the result you are still reading. */
.pg-nextb{appearance:none;border:1px solid var(--accent2);background:rgba(45,172,179,.14);color:var(--accent2);
  border-radius:999px;padding:8px 15px;font-size:12px;font-weight:800;letter-spacing:.03em;cursor:pointer;
  -webkit-tap-highlight-color:transparent;touch-action:manipulation}
.pg-nextb:active{transform:scale(.95)}
/* ── ✨ THE INVITATION BELONGS ON PLAY, NOT ON SKIP. This pulse was on the NEXT button — so the one
   control that says "press me" was the one that walks away from the hand, while play, which everybody
   already understands and which is the whole point of the row, sat there inert. (Nathan: "everyone knows
   what a play button does, it should be glistening but it isn't. Instead the skip button is glistening.
   That's the treatment we put on something to invite someone to press it.")
   It rests once the hand is graded — by then play is no longer the thing to do. */
/* ⚠️ THERE WAS ALREADY A PULSE — `playIdle`, defined further down — but it only ran when the button
   carried an `.idle` class, which is evidently not always set: Nathan's play button was sitting inert
   while SKIP pulsed. Rather than add a second, competing animation, the existing look is simply made
   unconditional here. It stops while the chord is sounding (the button is a stop control then) and once
   the hand is graded, when play is no longer the thing to do. */
.pg-transport .pg-playbtn{ animation:playIdle 2.3s ease-out infinite }
.pg-transport .pg-playbtn.stop{ animation:none }
@media (prefers-reduced-motion:reduce){ .pg-transport .pg-playbtn{ animation:none } }
/* thumb-sized, not icon-sized. (Nathan: "the transport controls are too small.") */
.pg-transport .pg-micb{height:46px;width:46px;min-height:0;padding:0;border:1px solid var(--line);
  border-radius:999px;background:none;font-size:22px;display:inline-flex;align-items:center;justify-content:center}
/* PLAY IS THE MAIN CONTROL ON THIS ROW, so it should look like one. It was a bare 15px triangle
   floating in space — the smallest thing on a row it is the point of. (Nathan: "the playback in
   transport bar is underwhelming as well.") A real, round, filled button: the thing your thumb goes to. */
/* ⚠️ NO NEGATIVE MARGIN AT THE BOTTOM. The keyboard comes after this in the document, so anything the
   button overhangs downward gets painted over — which is the "bottom of the play button is cut off as
   if it's sitting in a frame" Nathan saw. Nothing was clipping it; the keyboard was simply on top of
   it. It overhangs UPWARD only now, into the wheel's own padding where nothing is drawn, and takes a
   z-index so it stays above its neighbours regardless. */
.pg-transport .pg-playbtn{width:76px;height:76px;margin:-8px 0 0;position:relative;z-index:2;min-width:0;flex:0 0 auto;padding:0;border:0;border-radius:999px;
  background:var(--accent2);display:inline-flex;align-items:center;justify-content:center;
  box-shadow:0 3px 14px rgba(0,0,0,.4);transition:transform .12s ease}
.pg-transport .pg-playbtn:active{transform:scale(.93)}
.pg-transport .pg-playbtn .pg-playtri{margin-left:6px;border-left:26px solid #062024;
  border-top:17px solid transparent;border-bottom:17px solid transparent}
/* ■ while it is sounding — the same control, so there is one place to start and stop */
.pg-transport .pg-playbtn.stop .pg-playtri{margin:0;border:0;width:24px;height:24px;background:#062024;border-radius:3px}
/* the neighbours shrink rather than push play off center; they may be crowded, play may not be */
.pg-transport .pg-micb{flex:0 1 auto}
.pg-transport .pg-loopb,.pg-transport .pg-nextb{flex:0 1 auto;min-width:0}
/* the keys need a gutter like everything else; they were running 3px to 437px of a 440px screen */
.pg-keys .pg-kb{padding:0 12px;box-sizing:border-box}
/* NARROWER IS SHORTER. The keyboard's height follows its width — three octaves stretched to the full
   434px came out 98px tall and pushed itself off the bottom of the screen. Capping the width brings the
   height down with it, which is what buys the room, and the keys stay perfectly readable because there
   are 37 of them either way. (Nathan: "there's extra height in the clue area right now. I don't think
   you need to shrink the wheel.") */
.pg-keys .pg-kb{width:100%;max-width:520px}
.pg-keys .piano{display:block;width:100%}
/* a pill you can play should say so under a finger, without shouting while you are reading */
.pg-read-strip .tok-ch[data-rn]{cursor:pointer}
.pg-read-strip .tok-ch[data-rn]:active{filter:brightness(1.18)}
.pg-read-strip .tok-ch[data-rn]:focus-visible{outline:2px solid #8FE34A;outline-offset:2px}
/* the chord name rides under the lit keys (see kbPaint); the row reserves its height either way so
   nothing shifts when a chord sounds */
#pgKeys{ position:relative; padding-bottom:15px }
.kb-nm{font-size:11.5px;font-weight:700;letter-spacing:.02em;opacity:.85;min-height:14px;line-height:14px;
  position:absolute; bottom:0; left:50%; transform:translateX(-50%); white-space:nowrap;
  text-shadow:0 1px 3px rgba(6,18,22,.8)}
.pg-verdict{font-size:11px;font-weight:700;opacity:.72;text-align:center;
  /* ITS OWN ROW. width:100% + nowrap inside the flex row meant this sentence sat on top of the chord
     stacks at phone widths — "the line that says yours is as valid is obscuring the pills". */
  order:2;flex:0 0 100%;white-space:normal;line-height:1.25;margin-top:2px}
.pg-verdict.dim{opacity:.5}
/* only a 4-chord clue needs the second row of stacks */
/* SIZE TO THE RESULT. 142px was a fixed reserve for the worst case, so a short as-played reading
   still pushed the wheel down by the full amount — Nathan measured 100px+ of dead space below it.
   The rule about not moving applies while a clue is LIVE; once it is graded he has said the messaging
   outranks the layout, so this one may size to its content. */
.pg-read-strip.tall{height:auto;min-height:34px}
/* NOTHING TO SAY, NO ROOM TAKEN. While a clue is live this strip is childless, and it was holding
   60px + 14px of margin open above the wheel for the whole of every clue — the entire gap Nathan
   measured between the card and the wheel. The no-shift rule protects a target he is AIMING at, and
   an empty strip has nothing to aim at; when it fills, the wheel moves DOWN, which is the direction he
   already sanctioned ("once the round is over, it's OK to shift the wheel down to display the result").
   :empty is exact here — the strip either has child nodes or it does not. */
.pg-read-strip:empty{height:0;min-height:0;padding:0;border:0}   /* margins stay: 14px of deliberate air */
/* …and the same when it holds markup that renders no words. :empty only catches a literally empty
   node, so an all-correct hand kept a 62px band of nothing. The margins go too here — on a hand with
   nothing to say there is no reason to reserve air for it either. */
/* Nothing can appear in the strip until the hand is graded, so while you are playing it reserves
   nothing for nothing — directly above the wheel, pushing the transport down. It takes its height at
   the moment the hand ends, which is the one moment Nathan has said a shift is acceptable. */
.pg-read-strip.idle{height:0;min-height:0;padding:0;border:0;margin:0;overflow:hidden}
.pg-stack{position:relative;display:inline-block;line-height:1}
/* the sizer is invisible but IN FLOW: it holds the stack at the width of the WIDER reading, so a flip can
   never resize it and slide the row sideways. Both visible pills are absolute inside that fixed box.
   (Nathan's standing rule: everything stays put.) */
.pg-stack .pg-sizer{visibility:hidden;pointer-events:none;position:relative;z-index:0}
.pg-stack .pg-back{position:absolute;left:-11px;top:-11px;z-index:1;opacity:.55}   /* a hint, not a competing label (Nathan) */
/* the pill BEHIND shows only its edge — its numeral badge collided with the front one and two competing
   badges made it unreadable which reading you were looking at. One badge at a time. */
.pg-stack .pg-back .tok-rn{display:none}
.pg-stack .pg-front{position:absolute;left:0;top:0;z-index:2;
  background:color-mix(in srgb, var(--tc) 16%, var(--bg));   /* OPAQUE — the tint alone let the back show through */
  box-shadow:-2px 3px 7px rgba(0,0,0,.55)}
@supports not (color: color-mix(in srgb, red 10%, blue)){ .pg-stack .pg-front{background:var(--bg)} }
.pg-stack .pg-front .tok-rn{z-index:3}
.pg-cmp-lbl{font-size:11px;font-weight:700;opacity:.75;white-space:nowrap;justify-self:end}
.pg-cmp-c{display:flex;align-items:center;justify-content:center;min-height:24px}
.pg-cmp-tail{font-size:11.5px;font-weight:700;opacity:.75;white-space:nowrap;padding-left:2px}
/* the per-clue score moved off the card so four chord tiles fit across it */
.pg-stripscore{display:inline-block;margin-left:8px;font-size:11px;font-weight:800;opacity:.6;
  font-variant-numeric:tabular-nums}
/* graded cards have no score column, so the slots take the whole width and stay legible at four chords */
.chordcard.pgcard.cleared .cc-kbwrap,.chordcard.pgcard .cc-kbwrap{flex:1 1 auto;min-width:0}
.chordcard.pgcard .pg-slots{gap:2px}
.chordcard.pgcard .bul.dial{width:66px;height:66px}
/* The key-reading nuance is ONE sentence in the reserved message line, not a token in every slot —
   per-slot tokens made the card unreadable (Nathan). Rides in .pg-msg, which is always in the DOM. */
.bul.cur{box-shadow:0 0 0 2px rgba(255,255,255,.85)}   /* flush ring — an offset outline collided with the slot's own outline */

.pg-sc i{font-style:normal;font-weight:600;opacity:.5;font-size:9.5px}
.pg-slot .pg-hold{opacity:.3}
/* the key chooser belongs to the BOARD, not the footer (Nathan): inside the wheel area, outside the wheel */
.pg-wheelbar{display:flex;justify-content:flex-end;align-items:center;padding:0 2px 6px}

/* ── our own "show in" chooser: native selects don't match the game (Nathan) ── */
.pgsel{position:relative;display:inline-block}
.pgsel-btn{background:var(--panel2);color:var(--fg,#e7f3f5);border:1px solid var(--line);border-radius:8px;
           padding:6px 11px;font:inherit;font-size:12px;font-weight:700;cursor:pointer;letter-spacing:.01em}
.pgsel-btn:hover{border-color:var(--accent2)}

/* Two compact CHIPS in the wheel bar instead of a sentence (Nathan: "it looks like a programming
   language when you refer to C+ ... too much text"). Left = how the wedges are named (auto / a pinned
   key / off). Right = the Roman-numeral glyph, simply lit or unlit. Both are the same pill so the pair
   reads as one control. */
.pgchip{display:inline-flex;align-items:center;gap:7px;background:var(--panel2);color:var(--fg,#e7f3f5);
        border:1px solid var(--line);border-radius:999px;padding:4px 5px 4px 11px;font:inherit;font-size:12px;
        font-weight:800;cursor:pointer;letter-spacing:.02em;line-height:1.25;font-variant-numeric:tabular-nums}
.pgchip:hover{border-color:var(--accent2)}
/* KEY: <value>. The VALUE sits in its own bubble so it can never be read as part of a chord name —
   the same mistake the unlabelled key in the result card caused (Nathan). */
.pgchip-lbl{font-size:10px;font-weight:800;letter-spacing:.1em;opacity:.6;white-space:nowrap}
.pgchip-val{background:var(--panel);border:1px solid var(--line);border-radius:999px;padding:1px 9px;
            min-width:34px;text-align:center;color:var(--accent2);font-weight:800;font-size:12px}
/* The FUNCTION toggle is an abstract stack — the analysis layer over the chord names — not "I·V",
   which read as an actual progression (Nathan: "we need something more Meta"). */
.pgchip-fn{margin-left:6px;padding:5px 9px;opacity:.4}
.pgfn-ico{display:block}
.pgchip-fn.on{opacity:1;color:var(--accent2);border-color:var(--accent2);background:rgba(45,172,179,.14)}
.pgchip-fn.locked{cursor:default;opacity:.72}
.pgchip-fn.locked:hover{border-color:var(--line)}
.pgsel-menu{position:absolute;right:0;top:calc(100% + 6px);z-index:40;background:var(--panel);
            border:1px solid var(--line);border-radius:10px;padding:6px;min-width:172px;
            box-shadow:0 10px 30px rgba(0,0,0,.5);max-height:60vh;overflow:auto}
.pgsel-opt{display:block;width:100%;text-align:left;background:transparent;color:inherit;border:0;
           border-radius:7px;padding:7px 10px;font:inherit;font-size:12.5px;cursor:pointer}
.pgsel-opt:hover{background:rgba(255,255,255,.07)}
.pgsel-opt.on{background:rgba(45,172,179,.22);font-weight:800}
.pgsel-opt.tog{border-top:1px solid var(--line);border-radius:0 0 7px 7px;margin-top:2px;opacity:.9}
.pgsel-sep{font-size:10px;opacity:.45;text-transform:uppercase;letter-spacing:.09em;padding:6px 10px 3px}
.pg-was i{font-style:normal;opacity:.55;font-size:9.5px}

/* a clue you're not on and haven't answered: one slim line, not a full card */
.chordcard.pg-mini{display:flex;align-items:center;gap:10px;padding:7px 12px;min-height:0;opacity:.55}
.chordcard.pg-mini:hover{opacity:.85}
.pg-mini-lbl{font-size:11.5px;font-weight:700;opacity:.75}
.pg-mini-dots{display:flex;gap:5px;margin-left:auto}
.pg-mini-dot{width:9px;height:9px;border-radius:50%;display:inline-block}

/* reason token: top-right corner of its own slot, out of the reading line */
.pg-why{position:absolute;top:3px;left:5px;font-size:9px;opacity:.45;font-weight:700;letter-spacing:.03em}
/* guess and answer share ONE line: wrong in red, right answer in green, correct just gets a tick */
.pg-line{font-size:11.5px;font-weight:700;line-height:1.35;text-align:center;white-space:nowrap;
         overflow:visible;text-overflow:ellipsis;max-width:100%;display:flex;gap:5px;justify-content:center;
         align-items:center}   /* no fixed height: the answer lives in the dial core now, not on its own row */   /* FIXED: the answer tokens are taller than the "?" placeholder,
         so grading a clue grew every card by 4px and pushed the wheel down the page (Nathan). */
.pg-bad{color:var(--bad);text-decoration:line-through;opacity:.8}
.pg-good{color:var(--good)}
.pg-ok{color:var(--good)}

/* The active card's glow + scale(1.02) was being clipped by the scrolling strip's edge. Drop the scale
   here (it also caused the 2px twitch on grading) and give the column room for the glow. */
.chordcard.pgcard.active{transform:none}
.solo-wrap>.strip:has(.pgcard){padding-left:5px;padding-right:12px}

/* verdict first: a tick or cross as a superscript in the slot's top-right corner */
/* ⚠️ THE VERDICT MUST NOT LIVE UNDER THE CLEAR BUTTON. .pg-slotx is an OPAQUE 20x20 circle at
   z-index:3 sitting at top:-6/right:-6 — and this mark was at top:1/right:4, which is entirely
   INSIDE that box. So a wrong vote drew its ✗ underneath the button and you never saw it, while the
   corrected mark was big enough to poke out past the button's edge and showed fine. That is the
   whole of "the treatment on both are different" (Nathan, 2026-09-24): not a design choice, an
   occlusion. ⚠️ THE FIX IS z-index, NOT A NEW CORNER: moving it to the left put it beside the green
   tick that .pg-slot.ok::after already draws on a graded tile, so every correct slot wore TWO ticks.
   The clear button only exists while the hand is UNGRADED, so a graded mark was never buried — it is
   only the live one that needed lifting. Same corner as always, one layer up. */
.pg-mark{position:absolute;top:1px;right:4px;z-index:4;font-size:13px;font-weight:900;line-height:1}
.pg-mark.yes{color:var(--good)}
.pg-mark.no{color:var(--bad)}

/* the slot's circle becomes a tiny wheel once the clue is graded */
/* ── THE DIAL IS THE ANSWER (Nathan) ────────────────────────────────────────────────────────────
   The wedge diagram and the chord token are one element: the token sits in the dial's core instead of
   on its own line underneath, which reclaims a row per clue. BOTH states — the graded dial and the
   ungraded slot number — are the same 74px box, so grading can never resize the card. */
.chordcard.pgcard .bul.dial{background:transparent!important;box-shadow:none;border:0;padding:0;
  width:74px;height:74px;position:relative;overflow:visible;display:block;margin:0 auto 2px}
.chordcard.pgcard .bul.dial.dim{opacity:.45}
.chordcard.pgcard .bul.dial.cur .pg-dial{filter:drop-shadow(0 0 5px rgba(255,255,255,.4))}
.dial-core{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
  display:flex;align-items:center;justify-content:center;white-space:nowrap;z-index:2}
.dial-core .tok-ch{background:var(--panel);font-size:12px;margin:0;padding:4px 9px}
.dial-core .tok-rn{background:var(--panel2)}
.dial-num{width:26px;height:26px;border-radius:50%;font-weight:800;font-size:13px}
.pg-dial{width:100%;height:100%;display:block}

/* ── 2/4 switch, right end of the level-chooser bar ─────────────────────────────────────────────
   Apple-style track. It sits in the ~240px the bar's text never uses, so the bar does not grow and
   nothing below it moves. Fixed width in both states — the number swaps, the box does not resize. */
.hostbar{position:relative}
.pg-4sw{position:absolute;right:0;top:50%;transform:translateY(-50%);z-index:3;
  display:flex;align-items:center;gap:7px;background:none;border:0;
  padding:10px 16px 10px 12px;cursor:pointer;touch-action:manipulation}
.pg-4sw-n{width:11px;text-align:center;font-weight:900;font-size:14px;color:var(--muted);
  font-variant-numeric:tabular-nums;transition:color .18s}
.pg-4sw.on .pg-4sw-n{color:#2dacb3}
.pg-4sw-t{position:relative;width:42px;height:24px;border-radius:12px;background:rgba(255,255,255,.13);
  border:1px solid var(--line);transition:background .18s,border-color .18s}
.pg-4sw-t::after{content:"";position:absolute;top:2px;left:2px;width:18px;height:18px;border-radius:50%;
  background:#eaf6f7;box-shadow:0 1px 3px rgba(0,0,0,.45);transition:transform .18s}
.pg-4sw.on .pg-4sw-t{background:#2dacb3;border-color:#2dacb3}
.pg-4sw.on .pg-4sw-t::after{transform:translateX(18px)}
.pg-4sw:focus-visible{outline:2px solid #2dacb3;outline-offset:3px;border-radius:14px}
@media (prefers-reduced-motion:reduce){ .pg-4sw-t,.pg-4sw-t::after,.pg-4sw-n{transition:none} }
/* the song a progression came from — a light credit line, not a label */
.pg-song{font-size:11.5px;font-weight:400;font-style:normal;letter-spacing:.01em;
  color:rgba(233,246,247,.42);max-width:96%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  line-height:15px;height:15px}
/* The credit must READ as a credit. A bare em-dash and a title looked like a cryptic signed message —
   worse when the title is something like "This Is Me". Label it, quote it, name the writer. */
/* two rows: the song, then the "also appears in N" control. Both rows are fixed-height so a longer or
   shorter title can never move anything vertically, and the columns inside row 1 are fixed so cycling
   cannot slide the label sideways. */
.pg-song{display:block;width:100%;max-width:372px;margin:0 auto;text-align:left}
.pg-song-l1{display:flex;justify-content:flex-end;align-items:baseline;column-gap:5px;height:15px}
.pg-song-l2{display:flex;gap:8px;align-items:center;height:14px;line-height:14px;font-size:9.5px;
  font-weight:600;letter-spacing:.04em}
.pg-song-src{color:rgba(233,246,247,.4);text-transform:uppercase;letter-spacing:.06em;white-space:nowrap}
/* an explicit affordance — Nathan: "there's nothing indicating that anything is going to happen if I click" */
/* words, not a glyph: an unsupported character collapsed the pill to a bare border — on Nathan's phone
   that read as "a little blue line" with no indication anything was tappable. */
.pg-song-more{color:#0d2a2e;background:rgba(45,172,179,.85);border-radius:9px;font-weight:800;
  padding:0 7px;white-space:nowrap;line-height:14px;font-size:9px;letter-spacing:.05em}
.pg-song.more:active .pg-song-more{background:rgba(45,172,179,.22)}
.pg-song-lbl{font-size:9px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;
  color:rgba(233,246,247,.34);white-space:nowrap;line-height:15px}
.pg-song-t{font-style:normal;color:rgba(233,246,247,.62);overflow:hidden;text-overflow:ellipsis;
  white-space:nowrap;line-height:15px;min-width:0}
.pg-song-t i{font-style:italic}
.pg-song-by{font-style:normal;color:rgba(233,246,247,.42)}
/* the credit is a control when there is more than one song to see */
.pg-song.more{cursor:pointer;-webkit-tap-highlight-color:transparent}
.pg-song.more:hover .pg-song-t,.pg-song.more:focus-visible .pg-song-t{color:rgba(233,246,247,.9)}
/* no underline here either — same reasoning as .pg-cardsong: weight and hover, not a fake link */
.pg-song.more .pg-song-t{text-decoration:none;font-weight:600;
  text-decoration-color:rgba(45,172,179,.55);text-underline-offset:3px}
.pg-song.more:focus-visible{outline:2px solid #2dacb3;outline-offset:2px;border-radius:6px}
/* the count badge is retired — line 2 says it in words */

/* KILL THE 300ms TAP DELAY. Safari/Chrome hold every tap to see whether it becomes a double-tap-to-zoom.
   That delay is what makes rapid retriggering feel laggy — the 45ms debounce was never the limit.
   `manipulation` opts out of double-tap zoom (pan and pinch still work) so a tap fires immediately.
   (Nathan: "a 16th note requires a faster repeat… everything feels laggy".) */
.chordcard, .pgcard, .pg-slot, .pg-slots, [data-pos],
.grid .cell, .lvlmenu-btn, .lvlmenu-lvl, .pgwheelwrap, #pgWheelFrame,
.foot .link, .cardot { touch-action: manipulation; }
/* a slot that HOLDS a chord shows the chord, not its index — the number told you nothing about what you
   had voted for, so corrections meant undoing something you could not read. */
.pg-slot .dial-pick{display:flex;align-items:center;justify-content:center;font-weight:800;
  font-size:12px;letter-spacing:-.01em;line-height:1;padding:0 2px}
.pg-slot .bul.filled{opacity:1}
.pg-slot .bul.dim{opacity:.42}
/* A pick that does not hold, marked WHILE you play. The tock already said so; this says it in a way you
   can look at without guessing. The correct chord stays hidden — only your own guess is shown. */
.pg-slot.livebad .dial-core{outline:2px solid #e5484d;outline-offset:1px;
  box-shadow:0 0 0 3px rgba(229,72,77,.18)}
.pg-slot.livebad .dial-pick{text-decoration:line-through;text-decoration-thickness:1.5px;opacity:.85}

/* ── reveal strip: badge LEFT of the pills, citation right-aligned underneath ──────────────────
   The badge was centerd above the pill row and collided with it, and "yours is as valid as the
   textbook reading" sat directly over "FROM Georgia On My Mind" so the two read as one sentence.
   (Nathan's screenshot.) */
/* ⚠️ flex-wrap is declared ONCE. It used to say wrap and then nowrap in the same block, and the later
   one won — so the badge, the chords and the verdict (each flex-basis:100%) sat on ONE line at 300% of
   the width and the whole reveal was pushed off the right edge of the phone. A cascade collision inside
   a single rule, which is the easiest kind to miss. (Nathan: "completely off the screen on my iPhone.
   I have to scroll to the right in order to see it.") */
.pg-read-strip .pg-line-in{display:flex;flex-wrap:wrap;align-items:center;gap:6px 10px;width:100%;
  justify-content:flex-start;min-width:0;padding:0 6px;box-sizing:border-box}
/* 🐛 flex:0 0 100% HERE is what pushed the pills onto a second row — not .pg-stacks, and not the
   strip's flex-direction. The badge claimed the whole line, so everything after it wrapped and the
   width beside it sat empty. Third attempt at this; the first two were aimed at the wrong element.
   (Nathan: "it should flex to fit that space. There's no reason it should be on a different line.") */
.pg-read-strip .pg-framewrap{order:0;flex:0 1 auto;display:flex;justify-content:flex-start}
/* ITS OWN ROW. This badge was taking ~150px of a 366px strip, leaving 119px for four chords that need
   146 — so the fourth was off-screen and, since scrolling is not discoverable, simply "cut off". That
   is the complaint I kept measuring as "scrollable, therefore fine". It is not fine. On its own row the
   chords get the full width and all four fit.
   (Shrinking it instead was worse: below its content width its text spilled onto the pills.) */
/* ONE LINE. flex:0 0 100% forced a row break, so the badge sat alone above the pills with the whole
   width empty beside it. (Nathan: "it should flex to fit that space. There's no reason it should be on
   a different line.") Now it shares the row with the badge and shrinks to fit. */
/* flex-basis must be 0, not auto. With `auto` the basis is the CONTENT width (408px here), and
   flexbox WRAPS an item that does not fit before it will shrink one — so the pills jumped to the next
   line while 280px sat free beside the badge. Basis 0 lets it take the space that is actually left and
   scroll inside itself. */
.pg-read-strip .pg-stacks{order:1;flex:1 1 0;min-width:0;padding-left:10px;justify-content:flex-start;
  /* was overflow:hidden, which CLIPPED the fourth chord on a phone — the reveal was literally cut off.
     Scroll it instead so every chord in the clue is reachable.
     ⚠️ overflow-y:visible is IGNORED here: once one axis is auto the other cannot be visible, so the
     numeral badge that sits ABOVE each pill was being clipped off the top. Pad the box instead of
     asking for an overflow the spec will not give. (Nathan: "cut off on the top and on the right".) */
  overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none;
  padding-top:15px;padding-bottom:3px;padding-right:8px}
.pg-read-strip .pg-stacks::-webkit-scrollbar{display:none}
/* the verdict takes its OWN line. With flex:none it competed for row 1 and squeezed the pills down to
   78px; it is a sentence, not a chip, and belongs under them. */
.pg-read-strip .pg-verdict{order:2;flex:0 0 100%;width:100%;text-align:left;opacity:.6;font-size:10px}
.pg-read-strip .pg-badge{flex:none;align-self:center}
.pg-read-strip .pg-stack{flex:0 1 auto;min-width:0}
/* the citation reads as an attribution, not a caption: smaller, dimmer, hugging the right edge */
.pg-song{text-align:right;max-width:none;width:100%;padding-right:6px;box-sizing:border-box}
.pg-song-l1{justify-content:flex-end;font-size:10.5px}
.pg-song-lbl.rew{cursor:pointer;-webkit-tap-highlight-color:transparent}
.pg-song-lbl.rew:hover,.pg-song-lbl.rew:focus-visible{color:rgba(45,172,179,.95)}
.pg-song-l2{justify-content:flex-end;font-size:8.5px}
.pg-song-t{flex:0 1 auto}

/* the level's wheel state, drawn rather than listed */
.pg-reachdial{vertical-align:-4px;margin-right:6px;color:var(--accent2);opacity:.95;flex:none}
.lvlmenu-lvl .pg-reachdial{vertical-align:-5px;margin-right:8px}
/* neutral header for the four-chord ladder — band-4 was red and read as a warning (Nathan) */
.hb-flight.band-neutral{color:var(--fg);opacity:.92}
/* citation stepper, pinned at the right of the title line */
.pg-song-nav{display:inline-flex;gap:2px;margin-left:7px;flex:none;vertical-align:baseline}
.pg-songarrow{appearance:none;background:rgba(45,172,179,.16);border:1px solid rgba(45,172,179,.34);
  color:rgba(45,172,179,.95);border-radius:5px;width:17px;height:15px;line-height:12px;padding:0;
  font-size:12px;font-weight:800;cursor:pointer;touch-action:manipulation}
.pg-songarrow:hover,.pg-songarrow:focus-visible{background:rgba(45,172,179,.3)}
.pg-song-t{cursor:pointer}


/* ── LOOP IS A GESTURE ──────────────────────────────────────────────────────
   The toggle is gone: hold the clue area to start it going round, tap anywhere
   to stop. This is just the cue that a hold is arming, so it never feels like a
   dead press. (Nathan: "not worth the real estate and we can replace it with a
   gesture.") */
.chordcard.looparm{ box-shadow:inset 0 0 0 2px rgba(127,214,219,.55); }
@media (prefers-reduced-motion:reduce){ .chordcard.looparm{ box-shadow:inset 0 0 0 2px rgba(127,214,219,.55) } }


/* ── SLOT ZONES ─────────────────────────────────────────────────────────────
   Inside the circle CLEARS, the square around it PLAYS, the container outside
   the squares replays the sequence. One job per zone, and the circle is never
   a play target — replaying is repeated tapping, so a circle that also played
   would keep eating the answer you were only checking. (Nathan.) */
.pg-slot{ position:relative; }


/* Holding a slot clears it — show that it is happening, so a hold never feels like a dead tap
   and you can let go before it fires. (Tap = play, hold = clear; a double tap would have killed
   rapid-fire retriggering.) */
.pg-slot.clearing{ position:relative; }
.pg-slot.clearing::after{ content:''; position:absolute; inset:0; border-radius:10px;
  border:2px solid #ff8f8f; animation:pgClearHold .48s linear forwards; pointer-events:none; }
@keyframes pgClearHold{ from{ opacity:.15; transform:scale(.94) } to{ opacity:1; transform:scale(1) } }
@media (prefers-reduced-motion:reduce){ .pg-slot.clearing::after{ animation:none; opacity:.7 } }


/* ── THE WHEEL, IN THIS PAGE ────────────────────────────────────────────────
   Drawn as inline SVG rather than loaded in a cross-origin iframe. overflow is
   visible on purpose: the spinning wedge is deliberately drawn LARGER than the
   ring, which is exactly what the frame made impossible. */
.wh-svg{ display:block; width:100%; height:auto; overflow:visible; touch-action:none;
  -webkit-touch-callout:none; user-select:none; }
.wh-w{ cursor:pointer; }
/* ── ♿ A TEXT SCALE THE PLAYER CONTROLS. Josephine, daily player, across BOTH apps: "The chord names
   on the wedges and the Roman numerals are particularly small for me. Even the song titles are quite
   a strain to read." The numerals were 8.5px — small enough that a sighted adult struggles, let alone
   anyone with reduced vision.
   --txt-scale is set from Settings and defaults to 1, so nothing moves for anyone who does not want
   it. It is applied ONLY to the labels she named: wedge chord names, wedge numerals, song titles.
   ⚠️ Deliberately not applied globally — scaling every string would reflow layouts that are already
   tight at phone width, and she asked for these three things, not for a bigger everything. */
:root{ --txt-scale: 1 }
.wh-nm{ font-weight:700; font-size:calc(11.5px * var(--txt-scale));  text-anchor:middle; dominant-baseline:central; fill:#fff;
  paint-order:stroke; stroke:#000; stroke-width:.7px; pointer-events:none; }
.wh-fn{ font-weight:700; font-size:calc(8.5px * var(--txt-scale));  text-anchor:middle; dominant-baseline:central; fill:#fff;
  opacity:.95; letter-spacing:.3px; paint-order:stroke; stroke:#000; stroke-width:1px;   /* ↑ .6 — white-on-colour needs a real outline to hold its edge */
  pointer-events:none; }
.wh-mk{ stroke:#10242a; stroke-width:1.5; pointer-events:none; }
/* ⚠️ THE VERDICT RING HAS TO LIVE IN CSS, NOT ON THE ELEMENT. badgeSVG writes stroke="#E8806F" as a
   presentation ATTRIBUTE, and a CSS declaration — the rule directly above — outranks any presentation
   attribute. So every badge has been painting with the dark stroke no matter what the code wrote, and
   a wrong answer has never once shown red on the wheel. Nathan asked for this twice; both times I
   "verified" it by reading getAttribute('stroke'), which reports what was WRITTEN and says nothing
   about what is PAINTED. The attribute was right and the pixels were wrong every time.
   These classes are already on the elements, so the fix is to state the ring where it can actually win.
   (This matters beyond looks: the plan is to tap a wedge and see what you played against what was
   right, so the badge has to carry its verdict.) */
.wh-mk.wh-mk-answer{ stroke:#0b1f24; stroke-width:2 }
/* ⚠️ A PROPER RED, AND IT HAS TO SURVIVE LANDING ON A RED WEDGE. Nathan: "give it a proper red. And
   if it's on the red wedge just invert the fill and the number or something. No one thinks of coral
   as being a wrong color." Right on both counts — coral reads as a warm accent, not as an error.
   Rather than special-casing the dominant wedges, the ring carries a white CASING: a hard red edge
   with a thin white halo outside it. That is the standard cartographic trick for a line that has to
   cross unknown ground, and it means the same ring reads on the yellow tonic, the blue predominant
   AND the red dominant without the wheel needing to know which it landed on. */
.wh-mk.wh-mk-wrong{
  stroke:#FF3B30; stroke-width:3.5;
  filter:drop-shadow(0 0 2.6px rgba(255,255,255,.95)) drop-shadow(0 0 1px rgba(255,255,255,.9));
}
.wh-mkn{ font-weight:800; font-size:9px;  text-anchor:middle; fill:#0a2a18; pointer-events:none; }
/* ✕ on a badge whose chord disagrees with the answer — see badgeSVG(). It rides OUTSIDE the disc so
   the clue number stays legible; the white halo is what keeps it readable over a light wedge as well
   as a dark one, the same trick the red ring already uses. Not interactive: the disc under it is the
   tap target, and a cross that ate the tap would break "tap a wrong badge to ask about it". */
.wh-mk-x{ pointer-events:none; }
.wh-mk-x line{
  stroke:#FF3B30; stroke-width:2.6; stroke-linecap:round;
  filter:drop-shadow(0 0 2.2px rgba(255,255,255,.95)) drop-shadow(0 0 1px rgba(255,255,255,.9));
}
@media (prefers-reduced-motion:reduce){ .wh-mk-x line{ filter:none } }
.wh-hub{ fill:#fff; stroke:#e5e7eb; stroke-width:2; cursor:pointer; touch-action:none; }
.wh-hubname{ font-weight:700; font-size:26px;  text-anchor:middle; dominant-baseline:central; fill:#10242a; pointer-events:none; }
.wh-hubname.sm{ font-size:17px; opacity:.55; }
.wh-hubname.sp{ font-size:15px; }
.wh-spin{ cursor:pointer; }
/* Everything painted over the spinning wedge is decoration. If any of it takes a pointer, the wedge
   stops being tappable and the chord cannot be voted. */
.wh-seg{ pointer-events:none; stroke:rgba(255,255,255,.35); stroke-width:.8; }
.wh-segl{ text-anchor:middle; dominant-baseline:central; font-weight:700; fill:#fff;
  opacity:.62; pointer-events:none; }   /* font-size is per-band, set on the element */
.wh-segl.on{ opacity:1; font-weight:800; paint-order:stroke; stroke:#0b1f24; stroke-width:2.2px; }
.wh-tick{ fill:#fff; opacity:.92; pointer-events:none; }
/* An svg hit-tests only where it PAINTS. Without this the gaps between wedges swallow a drag. */
.wh-back{ fill:transparent; }
.wh-tickhit{ fill:transparent; cursor:pointer; }   /* a real 44px-ish target around the little arrow */
.wh-scrim{ fill:rgba(6,20,26,.62); pointer-events:none; }
.wh-rootbig{ text-anchor:middle; dominant-baseline:central; font-weight:700; fill:#fff;
  paint-order:stroke; stroke:#000; stroke-width:.6px; pointer-events:none; }
.wh-fanq{ cursor:pointer; }
.wh-qpill{ stroke:rgba(0,0,0,.30); stroke-width:.5; pointer-events:none; }
.wh-qpilll{ text-anchor:middle; dominant-baseline:central; font-weight:800;
  letter-spacing:.2px; pointer-events:none; }
/* same treatment as a wedge's own name: weight 700, hairline stroke */
.wh-fanl{ text-anchor:middle; dominant-baseline:central; font-weight:700; fill:#fff;
  paint-order:stroke; stroke:#000; stroke-width:.7px; pointer-events:none; }
.wh-callout{ font-weight:600; font-size:13px;  text-anchor:start; dominant-baseline:central; fill:rgba(255,255,255,.86); letter-spacing:.2px; pointer-events:none; }
.wh-bub{ fill:rgba(255,255,255,.22); stroke:#fff; stroke-width:2; pointer-events:none; }
.wh-ext{ cursor:pointer; }

/* ── First-run overview for Progressions ─────────────────────────────────────
   One screen, once. Dismissed for good; window.__pgShowIntro() brings it back. */
.pg-intro{position:fixed;inset:0;z-index:400;background:rgba(6,20,26,.86);
  display:flex;align-items:center;justify-content:center;padding:18px;overflow-y:auto;
  -webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px)}
.pg-intro-card{max-width:400px;width:100%;background:#12303a;border:1px solid rgba(127,214,219,.35);
  border-radius:16px;padding:20px 20px 18px;box-shadow:0 18px 50px rgba(0,0,0,.5)}
.pg-intro-card h2{margin:0 0 4px;font-size:21px;font-weight:800;color:#7fd6db;letter-spacing:.2px}
.pg-intro-sub{margin:0 0 14px;font-size:14px;line-height:1.45;color:rgba(255,255,255,.78)}
.pg-intro-card ul{margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:10px}
.pg-intro-card li{font-size:14px;line-height:1.45;color:rgba(255,255,255,.9);
  padding-left:14px;border-left:2px solid rgba(127,214,219,.4)}
.pg-intro-card li b{color:#fff;font-weight:800}
.pg-intro-foot{margin:14px 0 0;font-size:12px;line-height:1.5;color:rgba(255,255,255,.55)}
.pg-intro-card button{margin-top:16px;width:100%;min-height:46px;border:0;border-radius:11px;
  background:#7fd6db;color:#08222a;font-size:15px;font-weight:800;cursor:pointer}

/* Chord Trainer sits above chorDLE on the home screen — same shape as the other
   primaries so the home stays one column of identical blocks. */
/* ── CHORD TRAINER IS THE HERO ────────────────────────────────────────────────────────────────
   Nathan is making a video for it and expects traffic: "I want to get people playing it so we can
   look at performance metrics." It was the dimmest card on the screen while chorDLE! took the bright
   gradient. Swapped: Chord Trainer gets the vivid treatment, chorDLE! steps back to a mid tone, and
   the Daily Quiz stays quietest. Order on the page is unchanged.
   🐛 THE CLIPPED BADGE: .feat-badge sits at top:-10px, outside its card, and .big.home-primary sets
   overflow:hidden (to clip the ::before sheen to the rounded corners) — so "New" was sliced in half.
   .big.featured solves this with overflow:visible, which is why "Start here" renders fine. Do the
   same here and give the sheen its own radius so nothing spills. */
.big.home-primary.prog{position:relative;overflow:visible;
  background:linear-gradient(135deg,#3ad0c4 0%,#2196a0 52%,#17707d 100%);
  box-shadow:0 12px 34px rgba(58,208,196,.40),inset 0 0 0 1px rgba(255,255,255,.16)}
.big.home-primary.prog::before{border-radius:16px 16px 0 0}
.big.home-primary.prog .hp-title{color:#fff;font-size:26px}
.big.home-primary.prog .hp-sub{color:rgba(255,255,255,.9)}
.big.home-primary.prog:hover,.big.home-primary.prog:active{filter:brightness(1.06)}
/* the New badge, now that it can actually be seen */
.big.home-primary.prog .feat-badge{top:-9px;right:16px;background:#ffd75e;color:#08252b;
  box-shadow:0 2px 8px rgba(0,0,0,.45)}

/* Play + citation ride INSIDE the clue card, so each clue carries its own song. */
.pg-read-strip{position:relative}
/* ⚠️ THIS ROW MUST WRAP. It holds a mic, a transport, a score, a song title, a credit, a source link
   and a favorite star. At 390px they cannot share one line, and the thing that lost was always the
   title — truncated to "H..." with the source clipped to "W". (Nathan: "everything inside that card is
   crowded. You can't read the full text of anything.") The controls keep the first line; the song and
   its source take a line of their own, where there is room to actually read them. */
/* min-height 30 = the height of the favorite star, which only appears once the hand is graded. Without
   it the card grew 5px at grade time and the wheel stepped down with it, on every single clue.
   (Nathan: "interface still jumps up and down depending on what's displayed.") */
/* ⏳ HOLD THE SONG'S LINE OPEN BEFORE THE SONG ARRIVES. The credit comes from a corpus that loads
   after the first hands are already dealt, so the card was growing a line taller the moment it landed
   — and everything below it moved. (Nathan: "the clue card expands once it downloads the song name.
   Maybe we should keep it at a size that accommodates it even if it's not loaded yet.")
   48px = the controls line plus the song line, which is the card's settled shape. */
.pg-cardfoot{display:flex;align-items:center;flex-wrap:wrap;gap:0 10px;margin-top:2px;min-height:48px;
  align-content:center;width:100%}
/* the song + its source SHARE the second line — 100% on the song pushed the pill onto a third line and
   made the card 50px taller than it needs to be. 55% is enough for a real title and still leaves the
   pill its width. */
/* TWO LINES, NEVER THREE. Controls on the first, song + source sharing the second. 100% on the song
   pushed the source pill onto a third line and cost ~22px on every graded card — which is exactly the
   height the keyboard needs on a smaller phone. */
.pg-cardfoot .pg-cardsong{flex:1 1 58%;order:2}
.pg-cardfoot .pg-micb,.pg-cardfoot .pg-playbtn{order:0}
.pg-cardfoot .pg-srclink{order:3;flex:0 0 auto}
/* the persistent play control in the card's left column */
.pg-playbig{width:46px;height:52px;padding:0;border-radius:11px;
  border:1.5px solid rgba(127,214,219,.6);background:rgba(127,214,219,.14);
  display:flex;align-items:center;justify-content:center;cursor:pointer;
  -webkit-tap-highlight-color:transparent}
.pg-playbig:active{background:rgba(127,214,219,.3)}
.pg-playbig .pg-playtri{margin-left:4px;border-left:15px solid #7fd6db;
  border-top:10px solid transparent;border-bottom:10px solid transparent}
.pg-cardsong{flex:1 1 auto;min-width:0;font-size:calc(12px * var(--txt-scale));line-height:1.35;
  color:rgba(255,255,255,.72);display:flex;align-items:baseline;gap:6px;overflow:hidden}
.pg-songcount{flex:0 0 auto;font-size:10.5px;font-weight:700;color:rgba(255,255,255,.42);white-space:nowrap}
.pg-songlead{flex:0 0 auto;font-size:10px;letter-spacing:.4px;text-transform:uppercase;
  color:rgba(255,255,255,.38)}
/* the rolling half: one line tall, clipped, so a swap can travel through it */
.pg-songroll{flex:1 1 auto;min-width:0;display:block;overflow:hidden;white-space:nowrap;
  text-overflow:ellipsis;transition:transform .18s ease, opacity .18s ease}
.pg-songroll.rolling-out{transform:translateY(-100%);opacity:0}
.pg-songroll.rolling-in{animation:pgRollIn .22s ease}
@keyframes pgRollIn{from{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}
.pg-cardsong i{font-style:italic}
.pg-cardsong.more{cursor:pointer}
/* NO UNDERLINE (Nathan): "I'd prefer to telegraph that it is a link by font choice and weight and
   styling. Hover state, etc." The citation is not a link anyway — it is a control — so it now reads as
   one through weight and a hover lift, and the Source pill beside it carries the actual link. */
.pg-cardsong.more .pg-songroll{text-decoration:none;font-weight:600;
  text-underline-offset:3px}
.pg-cardsong.more b{font-weight:700;color:#7fd6db;text-decoration:none}
@media (prefers-reduced-motion:reduce){
  .pg-songroll{transition:none}
  .pg-songroll.rolling-in{animation:none}
}
.pg-cardsong.dim{color:rgba(255,255,255,.34);font-style:normal}
.pg-playbtn{flex:0 0 auto;width:38px;height:38px;padding:0;
  border-radius:50%;border:1.5px solid rgba(127,214,219,.55);background:rgba(127,214,219,.13);
  display:flex;align-items:center;justify-content:center;cursor:pointer;
  -webkit-tap-highlight-color:transparent}
/* a mini (non-active) clue card shows no foot — it has no room and no song yet */
.chordcard.pg-mini .pg-cardfoot{display:none}
.pg-playbtn:active{background:rgba(127,214,219,.28)}
.pg-playtri{width:0;height:0;margin-left:3px;
  border-left:12px solid #7fd6db;border-top:8px solid transparent;border-bottom:8px solid transparent}

/* FULL WIDTH ON A PHONE. #app carries 18px of side padding for the site pages; the
   Progressions board wants the glass. (Nathan: "full width on phone.") */
@media (max-width:520px){
  /* must out-specify AND out-order the narrow-screen `#app{padding:12px 12px 60px}` rule above */
  /* ⚠️ THE CONDITION IS "THIS SCREEN CARRIES THE HOSTBAR", not "this screen is Progressions". Sing it
     back mounts the same header and was left in the 18px gutters, so the identical control rendered
     68px narrower there — 320px against the trainer's 388. (Nathan, on his iPhone: "it's encapsulated
     inside a narrow container instead of taking up the width of the screen.") Add the screen here when
     it takes the hostbar. */
  body.at-prog #app, body.at-sing #app{padding-left:3px;padding-right:3px}
}

/* The warm-up's skip is the way out for anyone who is not a beginner — it was a
   faint text link. (Nathan: "that skip button needs to be more prominent.") */
.ob-skiplink, .ob-skip{ opacity:1 !important; font-weight:700 !important;
  color:#7fd6db !important; font-size:15px !important; padding:10px 14px !important;
  border:1px solid rgba(127,214,219,.45) !important; border-radius:10px !important;
  background:rgba(127,214,219,.10) !important; min-height:44px }

/* One-time piano download, shown inside the overview where the wait already is. */
.pg-load{margin-top:14px}
.pg-load-bar{height:6px;border-radius:4px;background:rgba(255,255,255,.12);overflow:hidden}
.pg-load-bar>i{display:block;height:100%;width:0;background:#7fd6db;border-radius:4px;
  transition:width .25s linear}
.pg-load-txt{margin-top:7px;font-size:12px;color:rgba(255,255,255,.62)}

/* The clue dots were flush against the header above and the card below. */
.solo-wrap .cardots{margin-top:8px;padding-top:2px}

/* One control in the clue's left column, centerd on the slot row. */
.chordcard.pgcard .cc-side{display:flex;align-items:center;justify-content:center}
.chordcard.pgcard .cc-statwrap{display:flex;align-items:center;justify-content:center;height:100%}
/* the citation pauses where you tapped it */
.pg-cardsong.held .pg-songroll{color:#fff}
/* a solved slot says so inside the box */
.pg-slot.ok::after{content:'';position:absolute;top:3px;right:4px;width:9px;height:5px;
  border-left:2px solid rgba(94,214,143,.95);border-bottom:2px solid rgba(94,214,143,.95);
  transform:rotate(-45deg)}
.pg-slot.ok{box-shadow:inset 0 0 0 1.5px rgba(94,214,143,.45)}
.pg-slot.part{box-shadow:inset 0 0 0 1.5px rgba(240,196,80,.4)}
.pg-slot.bad{box-shadow:inset 0 0 0 1.5px rgba(232,76,60,.38)}

/* The clue card is a column: [ play | slots ] on top, citation row beneath. */
.chordcard.pgcard{display:block}
.pg-cardmain{display:flex;align-items:stretch;gap:10px}
.pg-cardmain .cc-kbwrap{flex:1 1 auto;min-width:0}
.chordcard.pgcard .cc-side{flex:0 0 auto;display:flex;align-items:center;justify-content:center}
/* ⬇️ THE CLUE AREA IS WHERE THE SPARE HEIGHT WAS. (Nathan: "there's extra height in the clue area right
   now. I don't think you need to shrink the wheel.") These three stacked controls made the LIVE card
   32px taller than the graded one, and since the card holds its playing height so nothing shifts, that
   difference was dead space for the whole postmortem. Tighter buttons, same targets: all three stay
   comfortably above the 44px minimum in their long dimension. */
.chordcard.pgcard .cc-side .cc-statwrap{gap:2px}
.chordcard.pgcard .pg-playbig{min-height:34px;padding:0 4px}
.chordcard.pgcard .pg-micb{height:28px;min-height:0;padding:0 4px;font-size:15px}
.chordcard.pgcard .pg-loopb{padding:2px 7px;font-size:10px;line-height:1.3}

/* Play is a bare triangle — the box around it was the last thing fighting the square slots. */
.pg-playbig{width:auto;height:auto;min-width:44px;min-height:44px;border:0;background:none;
  border-radius:0;padding:0 6px}
.pg-playbig:active{background:none;transform:scale(.92)}
.pg-playbig .pg-playtri{margin-left:0;border-left:20px solid #7fd6db;
  border-top:13px solid transparent;border-bottom:13px solid transparent}
.pg-cardfoot .pg-playbtn{border:0;background:none;width:auto;min-width:34px;height:28px;padding:0 4px}
.pg-cardfoot .pg-micb{height:28px;min-height:0;padding:0 4px}
.pg-cardfoot .pg-fav{height:28px;width:28px;font-size:24px}
.pg-cardfoot .pg-cardsong{line-height:1.25;padding-block:0}
.pg-cardfoot .pg-playbtn:active{background:none;transform:scale(.92)}
.pg-songby{color:rgba(255,255,255,.5);font-style:normal}

/* The current clue's dot gets an extra ring, so the row needs room for it. */
.solo-wrap .cardots{margin-top:10px;padding:5px 6px 3px}
.cardot{margin:2px}

/* ── LEVEL DRAWER, COLLAPSED (Chord Trainer only) ──────────────────────────────────────────────
   The level header was a two-line block — name stacked above the meter/clue-count/score — costing
   57px of the 408px of chrome above the wheel on a phone. (Nathan: "I just don't think we need to
   spend that much space on it here.") One line now, and it still opens the full picker on tap:
   nothing is hidden, the same control is simply laid out flat.
   SCOPED to .pg-hostbar ON PURPOSE. The main chorDLE board builds its own hostbars (app.js 3822 /
   4193 / 4883) off the same .hostbar class; porting this there is a separate decision, so this
   selector must never be widened to bare .hostbar. */
.pg-hostbar{padding:5px 10px}

.pg-hostbar .hb-title{flex:1 1 100%}
.pg-hostbar .lvlmenu-btn{flex-direction:row;align-items:center;gap:8px;width:100%}
.pg-hostbar .lvlmenu-btn .hb-flight{font-size:14px;flex:0 0 auto}
.pg-hostbar .lvlmenu-btn .hb-lvl{flex:1 1 auto;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}

/* ── SOURCE LINK ───────────────────────────────────────────────────────────────────────────────
   A small, separate target beside the citation — Nathan: "don't make the entire link tapable. Use a
   small link icon that says full chart or Source." The citation line keeps its own tap (hold/cycle);
   leaving the app is a deliberate little control of its own. Underlined because it IS a link, unlike
   the citation, which only looked like one. */
/* 🐛 flex:0 0 AUTO + nowrap = a pill that could neither shrink nor wrap, so a long source name pushed
   it past the right edge and the whole PAGE scrolled sideways. My first fix let it CLIP, which stopped
   the scroll and turned "Wikifonia" into "W" — a worse bug than the one it fixed. The row wraps now, so
   the pill has a line it fits on and needs neither to overflow nor to be cut. */
.pg-srclink{display:inline-flex;align-items:center;gap:4px;flex:0 1 auto;min-width:0;max-width:100%;margin-left:0;
  font-size:11px;font-weight:700;letter-spacing:.02em;color:var(--dim);text-decoration:none;
  border:1px solid var(--line);border-radius:999px;padding:2px 8px 2px 6px;line-height:1.5;
  white-space:nowrap;opacity:.85;transition:opacity .15s,border-color .15s,color .15s}
.pg-srclink:hover,.pg-srclink:focus-visible{opacity:1;color:var(--accent2);border-color:var(--accent2)}
/* the glyph is drawn, not a font character — an emoji link icon renders at a different size on every
   platform and would not sit on the text baseline. */
.pg-srcico{width:11px;height:11px;flex:0 0 auto;background-color:currentColor;
  -webkit-mask:var(--srcico) center/contain no-repeat;mask:var(--srcico) center/contain no-repeat;
  --srcico:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6'/%3E%3Cpath d='M15 3h6v6'/%3E%3Cpath d='M10 14 21 3'/%3E%3C/svg%3E")}

/* The foot can now be genuinely empty (no play button, no credit yet) — collapse it rather than
   leaving a blank strip under the slots. See the app.js note at the card foot. */
/* NOT on the progressions card: collapsing it when empty is what let the card grow later. */
.pg-cardfoot:empty{display:none;margin:0;padding:0;min-height:0}
.pgcard .pg-cardfoot:empty{display:flex;min-height:48px}

/* A BIGGER TARGET. The citation was a bare text run — Nathan could not reliably hit it to pause.
   Padding plus a hit area that does not depend on where the words happen to end. */
.pg-cardsong.more{padding:6px 8px;margin:-6px -8px;border-radius:8px;cursor:pointer;
  -webkit-tap-highlight-color:transparent;touch-action:manipulation;user-select:none}
.pg-cardsong.more.held{background:rgba(255,255,255,.06)}
/* paused: say so, rather than leaving it looking identical to a roll that happens to be mid-beat */
/* the word "paused" is gone — the ▮▮ mark beside the lead-in says it, and costs no width */

/* ── CITATION: PAUSE MARK + STEP ARROWS ────────────────────────────────────────────────────────
   Nathan: a pause ICON rather than the word, and forward/back arrows while it is stopped. The arrows
   exist only when paused — a rolling line with controls beside it invites you to chase a moving
   target, which is the thing he has objected to all along. */
.pg-cardsong.more .pg-songroll{transition:color .15s}
.pg-cardsong.more:hover .pg-songroll{color:#fff}
.pg-songpause{flex:0 0 auto;width:8px;height:9px;margin:0 1px 0 -2px;opacity:0;
  background:linear-gradient(to right,currentColor 0 3px,transparent 3px 5px,currentColor 5px 8px);
  transition:opacity .15s}
.pg-cardsong.more.held .pg-songpause{opacity:.75}
.pg-songstep{flex:0 0 auto;display:inline-flex;align-items:center;gap:2px;margin-left:6px;
  transition:opacity .15s}
/* RESERVE THE SPACE. display:none collapsed the controls, so pausing SHIFTED the pause mark and the
   arrows sideways — and then every repeat press was a moving target. Nathan's standing rule, and this
   is the case that shows why: "anytime I have my mouse and expect to press repeatedly, I should not
   need to chase that button." Hidden, not removed. */
.pg-songstep.off{visibility:hidden;pointer-events:none}
.pg-stepb{width:22px;height:22px;padding:0;border:1px solid var(--line);border-radius:6px;
  background:none;cursor:pointer;color:var(--dim);position:relative;
  -webkit-tap-highlight-color:transparent;touch-action:manipulation}
.pg-stepb:hover,.pg-stepb:focus-visible{color:var(--accent2);border-color:var(--accent2)}
/* drawn triangles — an emoji or a glyph sits differently on every platform and would not center */
.pg-stepb::before{content:'';position:absolute;top:50%;left:50%;width:0;height:0;
  border-top:4px solid transparent;border-bottom:4px solid transparent;
  border-right:5px solid currentColor;transform:translate(-60%,-50%)}
.pg-stepb.fwd::before{border-right:0;border-left:5px solid currentColor;transform:translate(-40%,-50%)}

/* ── THE iOS EQUIVALENT OF HOVER: PAUSE AND IT UNFURLS ─────────────────────────────────────────
   The citation truncates on a phone ("Havona — Jaco Pastorius (Weathe…") and a title tooltip is a
   desktop-only answer — Nathan: "we don't have an equivalent for iOS. Suggestion?"
   This is it: while PAUSED the line wraps instead of truncating. Pausing already means "I am reading
   this one", so the gesture you would make anyway is the one that shows you the rest. No hover, no
   long-press to learn, and it works identically on every device. */
/* PAUSED = SCROLL, NOT WRAP (Nathan). Wrapping to a second line reflowed the whole foot and pushed
   the controls around — the exact chasing-a-moving-target problem. The line keeps its height, clips
   with an ellipsis, and can be dragged sideways to read the rest. */
.pg-cardsong.more.held .pg-songroll{white-space:nowrap;overflow-x:auto;overflow-y:hidden;
  text-overflow:ellipsis;line-height:1.35;scrollbar-width:none}
.pg-cardsong.more.held .pg-songroll::-webkit-scrollbar{display:none}

/* HOLD TO LOOP — the affordance the loop never had */
/* LOOP IS A TARGET NOW, not a hidden hold. See the app.js note where hold-to-loop was removed. */
/* the pill sits UNDER the triangle and the triangle stays centerd over it — see .pg-playbig below,
   which now centers itself in the column rather than hanging off its left edge. */
.pg-loopb{display:block;margin:5px auto 0;padding:2px 9px;border:1px solid var(--line);border-radius:999px;
  background:none;color:var(--dim);font-size:9px;font-weight:800;letter-spacing:.08em;
  text-transform:uppercase;line-height:1.5;cursor:pointer;
  -webkit-tap-highlight-color:transparent;touch-action:manipulation}
.pg-loopb:hover,.pg-loopb:focus-visible{color:var(--accent2);border-color:var(--accent2)}
.pg-loopb.on{color:#062024;background:var(--accent2);border-color:var(--accent2)}
/* Nathan: "the triangle play button should remain centered and the loop pill below it should be
   aligned as it is currently." The column packs to flex-start, so the narrower triangle sat left of
   the wider pill; center the button itself and the two line up on one axis. */
.cc-side .cc-statwrap{align-items:center;align-self:stretch;width:100%}
.cc-side .pg-playbig{margin-left:auto;margin-right:auto}

/* ── SONGS-USING-THIS-PROGRESSION PANEL ────────────────────────────────────────────────────────
   Opened from the count beside the citation. Answers three of Nathan's asks at once: a way back to
   the song the roll started on, the full list of what a progression belongs to, and something
   readable to land on instead of a raw .jams file. */
.pg-songs{position:fixed;inset:0;z-index:9000;display:flex;align-items:center;justify-content:center;
  background:rgba(4,14,18,.72);-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px);padding:16px}
.pg-songs-card{position:relative;background:var(--bg);border:1px solid var(--line);border-radius:14px;
  width:min(560px,100%);max-height:min(78vh,700px);display:flex;flex-direction:column;
  padding:18px 18px 6px;box-shadow:0 18px 60px rgba(0,0,0,.55)}
.pg-songs-card h3{margin:0 6px 4px 0;font-size:15px;font-weight:800;line-height:1.3;
  text-transform:none;letter-spacing:0;color:var(--ink)}   /* ← the global h3 rule uppercases, and in
  Roman numerals CASE IS THE CHORD: ii is minor, II is major. "ii I iii ii" was being displayed as
  "II I III II", which is a different progression entirely. Never let this inherit. */
.pg-songs-card h3 b{color:var(--accent2);font-weight:800;letter-spacing:.02em}
/* Nathan's disclaimer, in his words: sources vary in interpretation and quality; a listing is not a
   claim of authority — the idea is that songs SHARE progressions. */
.pg-songs-sub{margin:0 0 12px;font-size:11px;line-height:1.45;color:var(--dim)}
.pg-songs-sub a{color:var(--accent2);text-decoration:none;font-weight:700}
.pg-songs-sub a:hover{color:#fff}
.pg-songs-x{position:absolute;top:8px;right:10px;width:30px;height:30px;border:0;border-radius:8px;
  background:none;color:var(--dim);font-size:21px;line-height:1;cursor:pointer}
.pg-songs-x:hover,.pg-songs-x:focus-visible{color:#fff;background:rgba(255,255,255,.07)}
.pg-songs-list{overflow-y:auto;min-height:0;margin:0 -18px;padding:0 18px 12px;overscroll-behavior:contain}
.sl-group{margin-bottom:14px}
.sl-group h4{margin:0 0 5px;font-size:10px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;
  color:var(--dim);display:flex;align-items:center;gap:6px}
.sl-group h4 i{font-style:normal;font-size:9.5px;font-weight:700;opacity:.6;
  border:1px solid var(--line);border-radius:999px;padding:0 5px}
.sl-group ul{list-style:none;margin:0;padding:0}
/* 🐛 was display:flex, which put the expanded chart BESIDE the title and squeezed a long name like
   "Crucifixus (from the B Minor mass, BWV 232)" down to one letter per line. The chart belongs under
   the row, not next to it. (Nathan: "there's also some text formatting that needs to be fixed.") */
.sl-group li{display:block;padding:5px 0;border-top:1px solid rgba(255,255,255,.05);
  font-size:12.5px;line-height:1.35}
.sl-t{flex:0 1 auto;font-style:italic;font-weight:600;min-width:0;overflow-wrap:anywhere}
.sl-a{flex:1 1 auto;color:rgba(255,255,255,.5);font-size:11.5px;min-width:0;overflow-wrap:anywhere}
.sl-src{flex:0 0 auto;font-size:9.5px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;
  color:var(--dim);text-decoration:none;border:1px solid var(--line);border-radius:999px;padding:1px 7px}
.sl-src:hover,.sl-src:focus-visible{color:var(--accent2);border-color:var(--accent2)}

/* the count beside the citation — dead text before, the way into the panel now */
.pg-songall{flex:0 0 auto;min-width:22px;height:22px;padding:0 6px;margin-left:6px;
  border:1px solid var(--line);border-radius:999px;background:none;color:var(--dim);
  font-size:10.5px;font-weight:800;cursor:pointer;line-height:1;
  -webkit-tap-highlight-color:transparent;touch-action:manipulation}
.pg-songall:hover,.pg-songall:focus-visible{color:var(--accent2);border-color:var(--accent2)}

/* ── SONG ROW → THE CHART, DECODED FROM OUR OWN DATA ───────────────────────────────────────────
   Tapping a song shows where the progression sits inside it. No fetch: the sequence and key are
   already in progressions2.json. The raw .jams link stays as a footnote for provenance — it is a
   machine format and should never have been the primary destination. */
.sl-row{display:flex;align-items:baseline;gap:8px;width:100%;padding:0;border:0;background:none;
  color:inherit;font:inherit;text-align:left;cursor:pointer;-webkit-tap-highlight-color:transparent}
.sl-row:hover .sl-t,.sl-row:focus-visible .sl-t{color:var(--accent2)}
.sl-caret{flex:0 0 auto;margin-left:auto;width:0;height:0;border-left:4px solid transparent;
  border-right:4px solid transparent;border-top:5px solid var(--dim);transition:transform .15s}
.sl-row[aria-expanded="true"] .sl-caret{transform:rotate(180deg)}
.sl-exp{padding:6px 0 4px}
.sl-chart-row{display:flex;flex-wrap:wrap;gap:4px;align-items:center;max-height:168px;overflow-y:auto;
  padding:2px;overscroll-behavior:contain}   /* a full chart can be 100+ chords — scroll it, don't let
  it push the rest of the list off screen */
.sl-ch{font-size:11.5px;font-weight:700;line-height:1;padding:4px 7px;border-radius:6px;border:0;
  background:rgba(255,255,255,.05);color:rgba(255,255,255,.6);white-space:nowrap;cursor:pointer;
  font-family:inherit;-webkit-tap-highlight-color:transparent;touch-action:manipulation}
.sl-ch:hover,.sl-ch:focus-visible{background:rgba(255,255,255,.12);color:#fff}
/* ── BARS ──────────────────────────────────────────────────────────────────────────────────────
   One group per measure, separated by a rule. Nathan asked for bar lines rather than an arbitrary
   four-per-line wrap, which "is going to make it way too long". */
.sl-bar{display:inline-flex;align-items:center;gap:3px;padding:0 7px;
  border-left:1px solid rgba(255,255,255,.22)}
.sl-bar:first-child{border-left:0;padding-left:0}
.sl-chart.barred .sl-chart-row{gap:3px 0}
.sl-ch.hit{background:var(--accent2);color:#062024}   /* the progression you just played */
.sl-el{font-size:11px;color:var(--dim);opacity:.6;padding:0 2px}
.sl-chart-foot{margin-top:5px;font-size:10px;color:var(--dim);opacity:.75}
.sl-chart.none{font-size:11px;color:var(--dim);opacity:.7}

/* ▶ becomes ■ while anything is sounding (Nathan). One control, two states — before this the only way
   to stop a loop was to tap somewhere else and hope.
   The triangle is drawn with BORDERS on a zero-size box, so clip-path cannot square it — the borders
   have to be cleared and a real box given instead. Same footprint either way, so nothing reflows. */
.pg-playbig.stop .pg-playtri{width:17px;height:17px;margin-left:0;border:0;border-radius:3px;
  background:#7fd6db}
.pg-playbtn.stop .pg-playtri{width:12px;height:12px;margin-left:0;border:0;border-radius:2px;
  background:#7fd6db}

/* visually hidden but announced — the live region and any screen-reader-only text */
.sr-only{position:absolute!important;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;
  clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0}

/* ── COLLAPSED HAND: four states ───────────────────────────────────────────────────────────────
   A hand you are not playing shows its number, its four clue dots, and what happened to it.
   "working" matters as much as the rest — a half-answered hand used to look untouched. */
.pg-mini{display:flex!important;align-items:center;gap:10px;padding:8px 12px!important;min-height:0!important;
  cursor:pointer;transition:border-color .15s,background .15s}
.pg-mini .pg-mini-lbl{flex:0 0 auto;font-size:11px;font-weight:800;letter-spacing:.05em;
  text-transform:uppercase;color:var(--dim)}
.pg-mini .pg-mini-dots{display:flex;gap:4px;flex:1 1 auto}
.pg-mini .pg-mini-dot{width:9px;height:9px;border-radius:50%}
.pg-mini .pg-mini-mark{flex:0 0 auto;font-size:12px;font-weight:800;font-variant-numeric:tabular-nums}
.pg-mini-untouched{border-color:var(--line)}
.pg-mini-working{border-color:rgba(127,214,219,.5)}
.pg-mini-working .pg-mini-mark{color:var(--accent2)}
.pg-mini-right{border-color:rgba(94,209,138,.55);background:rgba(94,209,138,.07)}
.pg-mini-right .pg-mini-mark{color:#5ed18a}
.pg-mini-right .pg-mini-lbl{color:rgba(94,209,138,.85)}
.pg-mini-wrong{border-color:rgba(232,110,110,.5);background:rgba(232,110,110,.06)}
.pg-mini-wrong .pg-mini-mark{color:#e86e6e}
.pg-mini:hover{border-color:var(--accent2)}

/* × ON A FILLED SLOT — the visible way to delete a vote. The 480ms hold still works and stays as a
   shortcut, but nothing announced it, which is the same reason hold-to-loop had to go. Present only
   where there is a vote to remove, so it never competes with an empty slot's play target. */
.pg-slot{position:relative}
/* ⚠️ THE ONLY WAY TO TAKE A VOTE BACK BY TOUCH. The footer's undo is gone, so this 20px disc became
   the whole story — and 20px is half Apple's 44px floor. Nathan: "now that we don't have the undo
   button, we need to make sure those tap targets are big enough on votes to remove them."
   The DISC stays small on purpose, because it sits on a crowded tile and a big red × would shout —
   what grows is the invisible hit area around it, via a ::before that reaches out to 44px. You aim at
   what you see and hit what you meant. */
.pg-slotx::before{ content:""; position:absolute; top:-12px; right:-12px; bottom:-12px; left:-12px; }
.pg-slotx{position:absolute;top:-6px;right:-6px;z-index:3;width:20px;height:20px;padding:0;
  border:1px solid var(--line);border-radius:50%;background:var(--bg);color:var(--dim);
  font-size:13px;font-weight:700;line-height:1;cursor:pointer;
  display:flex;align-items:center;justify-content:center;
  -webkit-tap-highlight-color:transparent;touch-action:manipulation;
  transition:color .12s,border-color .12s,transform .12s}
.pg-slotx:hover,.pg-slotx:focus-visible{color:#e86e6e;border-color:#e86e6e;transform:scale(1.1)}

/* ⭐ SAVE A PROGRESSION — inside the clue box, SET BACK behind the Source pill.
   Nathan: "above and to the left of the Source pill… it can be behind a bit so it's set back and maybe
   shaded. I'm suggesting we allow for some overlap but make it look intentional."
   So: absolutely placed against the foot's right edge, offset up and left of the pill, z-index BELOW
   it, large and dim so it reads as a watermark the pill is resting on. Lighting up on hover — and
   filling solid once saved — is what makes the overlap look deliberate rather than like a collision. */
.pg-cardfoot{position:relative}
/* ⚠️ IT HAS TO PROTRUDE. First attempt sat it squarely under the pill, which has a higher z-index —
   so the pill swallowed every tap and the star could not be clicked at all. It now sits genuinely UP
   and to the LEFT, so its top-left corner clears the pill and that corner is the hit target. */
/* ⭐ IN THE ROW, NOT BEHIND IT. This was absolutely positioned at a fixed offset so the Source pill
   would sit on top of it — that overlap was deliberate. Once the footer wrapped, the pill moved to the
   second line and the star was left hanging in empty space with nothing to relate to. (Nathan: "the
   favorite star is floating in space.") It is a flex item now: it rides the end of the controls line,
   so it moves with the row instead of guessing where the row will be. */
.pg-fav{margin-left:auto;order:1;flex:0 0 auto;width:34px;height:34px;padding:0;border:0;
  background:none;color:rgba(255,255,255,.16);font-size:30px;line-height:1;cursor:pointer;
  display:flex;align-items:center;justify-content:center;
  -webkit-tap-highlight-color:transparent;touch-action:manipulation;
  transition:color .15s,transform .15s,text-shadow .15s}
.pg-fav:hover,.pg-fav:focus-visible{color:rgba(255,215,94,.75);transform:scale(1.08)}
.pg-fav.on{color:#ffd75e;text-shadow:0 0 10px rgba(255,215,94,.45)}
/* (the pill used to sit ON TOP of the star; they no longer share a place, so nothing to stack) */
.pg-srclink{position:relative;z-index:1}

/* ── PLAYHEAD ───────────────────────────────────────────────────────────────────────────────────
   The chord sounding RIGHT NOW, while the loop runs. Deliberately a different channel from the other
   two states so it cannot be misread: aiming is a border, a vote is a filled pill, and this is a glow
   on the slot NUMBER that travels with the playback. (Nathan.) */
/* ⚠️ This rule MUST out-specify `.chordcard.pgcard .bul.dial` (style.css ~2565), which sets
   box-shadow:none at (0,4,0). The original playhead was (0,3,0) and targeted `.pg-slotnum` —
   an element the dial redesign deleted — so BOTH halves died silently and playback stopped
   following the hand. Glow the DIAL the way the house already does it (`.bul.dial.cur .pg-dial`
   uses drop-shadow), not box-shadow, because the dial's own box is transparent. */
/* ── ▶︎ THE PLAYHEAD HAS TO SHOW ON EVERY KIND OF SLOT.
   markPlayhead() has always added .sounding to the slot that is currently sounding — but the only
   styling for it targeted `.bul.dial`, the DIAL bullet. On a card rendering the plain numbered bullet
   nothing drew at all, so the highlight never appeared and there was no way to tell which chord you
   were hearing. (Nathan: "is the highlight supposed to advance across the clue cards while it is
   playing so I know which one I am hearing? I thought it did that in the other game.")
   It did, on one variant. This lights the SLOT, so it works whatever is inside it. */
/* ⚠️ A BORDER WOULD BE THE WRONG SIGN. This file already sets the vocabulary: aiming draws a BORDER,
   a vote fills a pill, and the playhead GLOWS THE NUMBER and travels. My first attempt outlined the
   whole slot, which made the chord being heard look identical to the chord being aimed at — two
   different things wearing the same clothes. The number glows instead, so the two never collide. */
.chordcard.pgcard .pg-slot.sounding .bul{
  transform:scale(1.10);
  box-shadow:0 0 0 2px rgba(79,209,197,.7), 0 0 8px 1px rgba(79,209,197,.4);
  transition:transform .09s ease, box-shadow .09s ease;
}
@media (prefers-reduced-motion:reduce){
  .chordcard.pgcard .pg-slot.sounding .bul{transform:none}
}
@media (prefers-reduced-motion:reduce){
  .chordcard.pgcard .pg-slot.sounding .bul{transform:none}
}
.chordcard.pgcard .pg-slot.sounding .bul.dial .pg-dial{
  filter:drop-shadow(0 0 9px rgba(255,255,255,.78));
  transition:filter .07s ease-out}
.chordcard.pgcard .pg-slot.sounding .bul.dial .dial-core{
  transform:translate(-50%,-50%) scale(1.05);
  transition:transform .07s ease-out}
.chordcard.pgcard .pg-slot .bul.dial .pg-dial{transition:filter .34s ease-out}
.chordcard.pgcard .pg-slot .bul.dial .dial-core{transition:transform .34s ease-out}
@media (prefers-reduced-motion:reduce){
  .chordcard.pgcard .pg-slot.sounding .bul.dial .pg-dial,
  .chordcard.pgcard .pg-slot.sounding .bul.dial .dial-core{transition:none}
  .chordcard.pgcard .pg-slot.sounding .bul.dial .dial-core{transform:translate(-50%,-50%)}
}

/* ── V versus v ─────────────────────────────────────────────────────────────────────────────────
   Roman numeral CASE carries the quality — V is major, v is minor — but with the two wedges apart on
   the ring there is nothing to compare against, and a lone "V" is genuinely ambiguous. (Nathan: "is
   there a way to deal with that at the typography level without something like an underscore?")
   No extra character: lowercase numerals get a lighter weight, a slightly smaller size, and a touch of
   letter-spacing, so minor reads as visibly *lighter* than major at a glance — the same signal the
   color ladder already uses, applied to the type. */
.wh-fn{font-weight:800;letter-spacing:0}
/* ⚠️ NO font-size HERE. `.88em` resolves against the PARENT, not this element's own size, so the
   lowercase numerals came out BIGGER than the uppercase ones (14px vs 8.5px) — the opposite of the
   intent. Weight, slant and tracking carry it, and they inherit cleanly. */
/* lowercase stays ITALIC — that is what separates ii from II at a glance — but 400 at 8.5px was the
   lightest text on the screen, laid over saturated wedges. The slant carries the distinction; the
   weight was not doing any work except making it unreadable. */
.wh-fn.lc{font-weight:600;letter-spacing:.08em;font-style:italic}

/* per-hand score + points, in the card foot. The only number on screen before this was the running
   total in the level chooser, which accumulates across the deck. */
.pg-handpts{flex:0 0 auto;display:inline-flex;align-items:baseline;gap:5px;padding:2px 8px;
  border:1px solid var(--line);border-radius:999px;font-size:11px;line-height:1.5;color:var(--dim);
  font-variant-numeric:tabular-nums;white-space:nowrap}
.pg-handpts b{font-weight:800;color:rgba(255,255,255,.75)}
.pg-handpts i{font-style:normal;font-weight:700;color:#ffd75e}
.pg-handpts.ok{border-color:rgba(94,209,138,.5)}
.pg-handpts.ok b{color:#5ed18a}

/* ══ 🚪 THE FRONT DOOR (#door) ═══════════════════════════════════════════════════════════════
   One hero, everything else small and equal, community one line each. The whole argument is
   RANK — see the block comment above screenFrontDoor() in app.js. Scoped under .door so it
   cannot leak into the existing landing while both are live. */
.panel.door{ max-width:460px; margin:0 auto; }

  .door-play{ width:64px; height:64px; margin:12px auto; }
  .door-sec{ margin:18px 0 8px; }
}
.door-top{ display:flex; align-items:center; justify-content:flex-end; gap:8px; margin:2px 0 12px; }
.door-brand{ font-weight:900; font-size:20px; letter-spacing:-.03em; flex:1; }
.door-brand span{ color:var(--accent); }
.door-chip{ border:1px solid transparent; border-radius:20px; padding:4px 2px; font-size:12px;
            font-weight:700; color:var(--dim); white-space:nowrap; }
.door-chip b{ color:var(--ink); }

.door-hero{ display:block; width:100%; text-align:left; border:1px solid var(--accent); border-radius:16px;
            padding:18px; background:linear-gradient(140deg,#1d747a,#17414c); color:var(--ink); cursor:pointer; }
.door-hero .k{ display:block; font-size:11px; letter-spacing:.14em; text-transform:uppercase;
               color:#a8e4e7; font-weight:800; }
.door-hero .t{ display:block; font-size:23px; font-weight:900; letter-spacing:-.02em; margin:3px 0 2px; }
.door-hero .s{ display:block; font-size:13.5px; color:#cdeced; }
.door-bar{ display:block; height:8px; border-radius:5px; background:rgba(0,0,0,.28); overflow:hidden; margin:13px 0 6px; }
.door-bar i{ display:block; height:100%; background:var(--accent); border-radius:5px; }
.door-mini{ display:flex; justify-content:space-between; font-size:12px; color:#bfe6e8;
            font-variant-numeric:tabular-nums; }

.door-coach{ display:flex; gap:9px; align-items:flex-start; padding:13px 2px 2px; margin-top:2px;
             font-size:13.5px; color:var(--dim); line-height:1.45; }
.door-coach .i{ font-size:15px; line-height:1.3; }
.door-coach .i.lk{ width:15px; height:15px; color:var(--band-3); flex:none; line-height:0; margin-top:1px; }
.door-coach b{ color:var(--ink); font-weight:600; }

.door{ counter-reset:rung; }
.door-sec{ font-size:10.5px; letter-spacing:.14em; text-transform:uppercase; color:var(--dim); font-weight:700;
           margin:28px 0 10px; display:flex; align-items:center; gap:9px; opacity:.85; }
/* The rung number is what turns a menu into a scaffold — you can see where you are and that there
   is an order. Reference sits outside the ladder, so it takes no number. */
.door-sec:not(.bd-t1):before{ counter-increment:rung; content:counter(rung);
  flex:none; width:17px; height:17px; border-radius:50%; border:1px solid currentColor;
  display:flex; align-items:center; justify-content:center; font-size:9.5px; letter-spacing:0;
  opacity:.85; }
.door-sec:after{ content:""; flex:1; height:1px; background:var(--line); }
/* The section header carries its group's color — same signal as the dot on its tiles, so the
   grouping TEACHES the palette instead of leaving three colors undecodable. Text only: a filled
   block here would be a third colored thing per row. */
.door-sec.bd-t0{ color:var(--band-0); } .door-sec.bd-t2{ color:var(--band-2); }
.door-sec.bd-t3{ color:var(--band-3); }
.door-sec.bd-t0, .door-sec.bd-t2, .door-sec.bd-t3{ opacity:.9; }
/* Reference is NOT a rung, and green is already spoken for — it means "someone is online" in the
   status readout, and band-1 (#3ecf7a) was close enough to the presence dot (#2ec16a) to read as
   the same signal. Reference stays neutral, which also says it is not part of the ladder. */
.door-sec.bd-t1{ color:var(--dim); }
/* Blindfold groups by HOW you play, not what you practise — the hands-free modes. It sits with
   Reference below the numbered rungs because it is a different axis, not a step on the ladder. */
.door-sec.bd-t5{ color:var(--band-4); opacity:.9; }
.door-g .ico.bd-5{ background:var(--band-4)!important; }
.door-g .ico.bd-1{ background:var(--dim)!important; opacity:.7; }
.door-shelf + .door-sec{ margin-top:22px; }

/* ── 🪟 TWO COLUMNS, AND THIS TIME THE TILE CAN AFFORD IT.
   It went to one column for a real reason: three lines per tile (title / who it is for / what it
   does) do not fit a 150px column — every description wrapped to five or six lines and the door
   grew to 1662px. The fix was the wrong end of the problem. Two columns doesn't fail because a
   phone is narrow, it fails because the tile is wordy.
   The tile is now an ICON, a NAME and one SHORT line, and the section heading carries the "who this
   is for" that used to sit on every tile. That fits 175px with room over.
   (Nathan: "I was enjoying the two column display on my phone a while ago. It got changed to one at
   some point but we need to simplify.")
   Below 340px — the 4th-gen SE and the fold-closed phones — it still drops to one, because two
   88px columns is not a tap target. */
.door-shelf{ display:grid; grid-template-columns:1fr 1fr; gap:9px; }
@media (max-width:339px){ .door-shelf{ grid-template-columns:1fr; } }
/* ⚠️ NO TABLET RULE HERE. A full responsive story for the door already exists at the END of this
   file (search "DOOR RESPONSIVENESS") — 2-up at 760px inside a 720px panel, 3-up at 1040px inside
   920px, plus a short-and-wide landscape block. It is deliberately last so it wins on specificity.
   I added a competing auto-fill rule here and it was silently overridden, which is the system
   working. The 2-up base above is the phone case; everything wider is settled down there. */
.door-g{ background:transparent; border:1px solid var(--line); border-radius:14px; padding:11px 12px 12px;
         text-align:left; position:relative; display:flex; flex-direction:column; gap:1px;
         color:var(--ink); cursor:pointer; min-width:0; }
/* ⚠️ min-width:0 IS LOAD-BEARING. A grid item defaults to min-width:auto, which means it refuses to
   shrink below its longest unbreakable content — so "chorDLE! Interval Bingo" pushed its column past
   half the screen and the shelf overflowed the phone to the right. The columns are 1fr each and it
   still overflowed, which is the classic grid/flex min-content trap. */
.door-g .nm{ overflow-wrap:anywhere; }
/* the icon carries the band color, which is why the colored dot is gone: two colored things on a
   175px tile is exactly the clutter Nathan warned about, and the drawing says more than the dot. */
.door-ico{ width:25px; height:25px; stroke:currentColor; fill:none; stroke-width:1.4;
           stroke-linecap:round; stroke-linejoin:round; opacity:.95; flex:0 0 auto; }
.door-g.bd-t0{ color:var(--band-0); } .door-g.bd-t1{ color:var(--band-1); }
.door-g.bd-t2{ color:var(--band-2); } .door-g.bd-t3{ color:var(--band-3); }
/* ⚠️ HANDS-FREE WAS WEARING THE HARDEST-MATERIAL COLOR. bd 5 resolved to --band-4 (#e8564a, "The
   Shed, hardest"), so Blindfold and Sing It Back — the two least theory-dependent things on the
   page — were painted the danger color. (Nathan: "Blindfold games which should not be in Red by
   the way.") They take Foundations green: not a difficulty, a way of playing. */
.door-g.bd-t5{ color:var(--band-1); }
.door-g .nm{ margin-top:0; }
.door-g .ico{ display:inline-block; width:7px; height:7px; border-radius:50%; background:var(--dim);
         margin-right:8px; vertical-align:middle; position:relative; top:-1px; }
.bd-0{ background:var(--band-0)!important; } .bd-1{ background:var(--band-1)!important; }
.bd-2{ background:var(--band-2)!important; } .bd-3{ background:var(--band-3)!important; }
.bd-4{ background:var(--band-4)!important; }
.door-g .nm{ font-weight:700; font-size:14px; letter-spacing:-.012em; line-height:1.25;
             color:var(--ink); }
.door-g .ds{ font-size:11.5px; color:var(--dim); line-height:1.35; }
/* Reserve the corner ONLY on tiles that actually have a level. Reserving on all nine wrapped
   every description onto two lines to make room for something four of them never show. */
.door-g.haslv .ds{ padding-right:0; }
/* the level marker moves to the top-right, clear of both the icon and the two text lines */
.door-g .door-lv{ top:10px; right:11px; bottom:auto; }
/* PLACEHOLDER COPY — deliberately ugly. Red means the words are mine and Nathan has not replaced
   them yet. Remove the `draft` flag on a tile in doorGames() as each line is replaced and the red
   goes with it. When no red is left, the door is in his voice. */
.door-g .ds.draft{ }   /* the red placeholder state is retired — copy has been through Nathan */
/* An unbuilt rung says so on its face rather than pretending to be playable. */
/* ── TILE STATE ────────────────────────────────────────────────────────────────────────────────
   Three lines per tile: WHAT it is, WHO it is for, WHAT it does. Plus one badge answering "is this
   for me, now?" — never tried / a stretch / recommended / how you are doing. */
/* Align the kicker and description under the TITLE TEXT, not under the dot — the dot is 7px plus
   an 8px gap, so everything below the title indents to match it. (Nathan: "Align the description
   under the title. Don't indent it all the way left to where the dot is.") */
.door-g .fr{ padding-left:0; }
.door-g .ds{ padding-left:0; }
/* THE KICKER IS THE COLOR NOW. With no drawing and no dot on the tile, this line is the only thing
   carrying the band — so it takes currentColor from .door-g.bd-t*, rather than being teal on every
   tile regardless of which section it is in. It is also the first half of the two-part description:
   the need, above the thing that answers it. */
/* ⚠️ NOT UPPERCASE, AND THAT IS THE POINT. Set in caps with letter-spacing, "Quick training on the
   go" runs to three lines in a 175px column and every tile in the row ends up a different shape —
   and the tiny-caps micro-label is itself a large part of what reads as generic. Sentence case at
   the same size fits most kickers on one line and sounds like a person wrote it.
   It is also the only thing carrying the band color now that the drawings are out, so it takes
   currentColor from .door-g.bd-t* instead of being teal on every tile whatever section it is in. */
.door-g .fr{ font-size:10.5px; font-weight:700; letter-spacing:.005em;
  color:currentColor; opacity:.95; margin-bottom:2px; line-height:1.35; }
.door-g .fr.draft{ }
.door-lv.b-next{ color:var(--accent)!important; opacity:1; font-weight:800; letter-spacing:.06em;
  text-transform:uppercase; font-size:9.5px; }
.door-lv.b-new{ color:var(--dim)!important; letter-spacing:.06em; text-transform:uppercase; font-size:9.5px; }
.door-lv.b-stretch{ color:var(--band-3)!important; opacity:.8; letter-spacing:.04em; font-size:9.5px; }
.door-lv.b-soon, .door-soon{ color:var(--band-2)!important; opacity:.85; letter-spacing:.06em;
  text-transform:uppercase; font-size:9.5px; }
/* the recommended tile is the only one that gets a lit edge — one suggestion, not nine */
.door-g.b-next{ border-color:var(--accent); background:rgba(45,172,179,.06); }
.door-g.b-stretch{ opacity:.72; }
.door-g.b-new .nm, .door-g.b-next .nm{ color:var(--ink); }
.door-coach.empty{ display:none; }
/* every tile reserves the badge corner now, not just levelled ones */
/* the badge is bottom-right and the description now runs under it, so the corner is reserved again
   — without this, "START" and "rung 2" print straight through "Sweet, salty or savoury?" */
.door-g.b-next .ds, .door-g.b-new .ds, .door-g.b-stretch .ds, .door-g.b-lvl .ds, .door-g.b-soon .ds,
.door-g.haslv .ds{ padding-right:42px; }
/* HOW FAR YOU HAVE GOT in this mode, in the corner the padlock used to occupy — a fact where
   there was a fence. Quiet on purpose: the tile's dot already carries the color, and two
   colored things per tile is the clutter Nathan warned about. Absent until you have played. */
/* Bottom-right, where the description runs out rather than where the title needs the room.
   A two-column tile at 390px is ~150px wide — not enough for a 15px title AND a marker on one
   line, which pushed every long name onto two lines. The grey description takes the reserve. */
.door-lv{ position:absolute; right:14px; bottom:13px; font-size:10.5px; font-weight:600;
         color:var(--dim); letter-spacing:.02em; font-variant-numeric:tabular-nums; opacity:.8; }

.door-room{ background:var(--panel); border:1px solid var(--line); border-radius:13px; overflow:hidden; }
.door-room .r{ display:flex; align-items:center; gap:9px; padding:12px 13px; font-size:13.5px; width:100%;
               text-align:left; color:var(--ink); border:0; border-bottom:1px solid rgba(37,90,102,.5);
               background:none; cursor:pointer; }
.door-room .r:last-child{ border-bottom:0; }
.door-room .r .sp{ flex:1; color:var(--dim); font-size:12.5px; }
.door-room .r .ch{ color:var(--dim); }
.door-room .dot{ width:7px; height:7px; border-radius:50%; background:#3fbf7f; flex:none; }
.door-room .rsp{ width:7px; flex:none; }
.door-old{ display:block; margin:18px auto 0; color:var(--dim); font-size:13px; }

/* ── the twenty-second placement ─────────────────────────────────────────── */
.doortest{ text-align:center; }
.door-step{ font-size:11px; letter-spacing:.16em; text-transform:uppercase; color:var(--accent);
            font-weight:800; margin-bottom:14px; }
.door-h{ font-size:27px; font-weight:900; letter-spacing:-.025em; line-height:1.15; margin:0 0 6px; text-wrap:balance; }
.door-p{ color:var(--dim); font-size:14.5px; margin:0 0 4px; }
.door-play{ width:92px; height:92px; border-radius:50%; background:var(--accent); margin:22px auto;
            display:grid; place-items:center; border:0; cursor:pointer; }
.door-play:after{ content:""; border-left:28px solid var(--accent-ink); border-top:18px solid transparent;
                  border-bottom:18px solid transparent; margin-left:7px; }
.door-play.ring{ animation:doorPulse 1.6s infinite; }
@keyframes doorPulse{ 0%{box-shadow:0 0 0 0 rgba(45,172,179,.45)} 70%{box-shadow:0 0 0 20px rgba(45,172,179,0)}
                      100%{box-shadow:0 0 0 0 rgba(45,172,179,0)} }
.door-opts{ display:flex; flex-direction:column; gap:9px; }
.door-opt{ border:1.5px solid var(--line); border-radius:13px; padding:15px 17px; background:var(--panel);
           font-weight:700; font-size:16px; display:flex; justify-content:space-between; align-items:center;
           width:100%; color:var(--ink); cursor:pointer; }
.door-opt .em{ width:13px; height:13px; border-radius:50%; background:var(--dim); flex:none; }
.door-opt.right{ border-color:#3fbf7f; background:rgba(63,191,127,.13); }
.door-opt.wrong{ border-color:#CE6F6F; background:rgba(206,111,111,.12); }
.door-opt[disabled]{ opacity:.55; }
.door-fb{ font-size:14px; font-weight:700; margin:14px 0 0; min-height:21px; }
.door-fb.ok{ color:#3fbf7f; } .door-fb.no{ color:#F0AD21; }
.door-dots{ display:flex; gap:6px; justify-content:center; margin-top:16px; }
.door-dots i{ width:7px; height:7px; border-radius:50%; background:var(--panel2); }
.door-dots i.done{ background:var(--accent); } .door-dots i.now{ background:var(--ink); }
@media (prefers-reduced-motion:reduce){ .door-play.ring,.door-g.shake{ animation:none; } }

/* ── NAME THIS CHORD (prototype, admin only) ───────────────────────────────────────────────────
   The name is BLURRED and sharpens as the voicing collapses toward root position — Nathan's
   "until its name comes into focus", taken literally. */
.nc{ max-width:460px; margin:0 auto; }
.nc-head{ text-align:center; margin:6px 0 2px; }
.nc-kick{ display:block; font-size:10px; letter-spacing:.16em; text-transform:uppercase;
  color:var(--band-2); opacity:.8; margin-bottom:6px; }
.nc-name{ font-size:27px; font-weight:900; letter-spacing:-.02em; margin:0;
  transition:filter .18s ease-out; }
.nc-sub{ text-align:center; margin:6px 0 0; }
.nc-play{ margin:16px auto 14px; width:64px; height:64px; }
.nc-play:after{ border-left-width:20px; border-top-width:13px; border-bottom-width:13px; }
.nc-kb{ display:block; width:100%; height:132px; background:var(--panel);
  border:1px solid var(--line); border-radius:10px; overflow:visible; touch-action:none; }
.nc-kb .nc-w{ fill:#e9eef0; stroke:#0d2027; stroke-width:.18; vector-effect:non-scaling-stroke; }
.nc-kb .nc-b{ fill:#16232a; }
.nc-tok circle{ fill:var(--accent); stroke:#04222a; stroke-width:.3; cursor:grab;
  vector-effect:non-scaling-stroke; transition:fill .12s; }
.nc-tok.nc-root circle{ fill:var(--band-3); }
.nc-tok.drag circle{ cursor:grabbing; fill:#fff; }
.nc-tok:focus{ outline:none } .nc-tok:focus circle{ stroke:#fff; stroke-width:.9; }
.nc-foot{ display:flex; justify-content:space-between; align-items:center; margin-top:10px;
  font-size:12px; color:var(--dim); }
.nc-foot b{ color:var(--ink); font-variant-numeric:tabular-nums; }
.nc-win{ margin-top:14px; padding:14px; border:1px solid var(--accent); border-radius:13px;
  background:rgba(45,172,179,.08); }
.nc-win b{ display:block; font-size:19px; margin-bottom:4px; }
.nc-win span{ display:block; font-size:13px; color:var(--dim); line-height:1.45; margin-bottom:12px; }

/* ── DOOR RESPONSIVENESS — must live at the END of the file: .door-shelf's 2-up rule is declared
   later than the door block, so at equal specificity these lost. iPad and desktop: let the door use the width instead of stranding a phone column in the middle.
   Nathan: "I just checked it on my iPad and landscape and it is not particularly responsive." */
@media (min-width:760px){
  .panel.door{ max-width:720px; }
  .door-shelf{ grid-template-columns:repeat(2,1fr); }
  .door-hero .t{ font-size:26px; }
}
@media (min-width:1040px){
  .panel.door{ max-width:920px; }
  .door-shelf{ grid-template-columns:repeat(3,1fr); }   /* 4-up made every row a narrow column again */
}
/* Short and wide — a phone on its side, or a small iPad split view. Keep the hero from eating the
   whole viewport before anything else is reachable. */
@media (max-height:520px) and (orientation:landscape){
  .door-hero{ padding:12px 14px; }
  .door-hero .t{ font-size:20px; }
  .door-play{ width:64px; height:64px; margin:12px auto; }
  .door-sec{ margin:18px 0 8px; }
}

/* ── KEYBOARD FOCUS ON THE WHEEL ───────────────────────────────────────────────────────────────
   The wedges became focusable for VoiceOver, but a browser's default outline on an SVG <path> is
   drawn on the bounding box and is near invisible against the wedge fills — a sighted keyboard
   user needs to see where they are. On :focus rather than :focus-visible: an SVG path does not
   take focus from a pointer tap, so only a keyboard or a screen reader lands here, and :focus is
   the half I can actually verify headlessly. */
/* ⚠️ WAS :focus — which is why the white ring appeared on some taps and not others. Focus says where
   the keyboard is, not what is sounding; :focus-visible keeps it for keyboard users and stops a touch
   tap leaving a ring behind. What is SOUNDING is .wh-playing, set by soundRn, every time. */
/* ⚠️ THE BLUE SQUARE WAS MINE. Narrowing the old rule from :focus to :focus-visible took `outline:none`
   with it, so a touch tap fell through to WebKit's DEFAULT focus ring — a blue rectangle around the
   wedge's bounding box, which on a pie slice looks like nothing to do with the wedge at all.
   Suppressing the outline is a separate job from drawing the highlight, so it gets its own rule and
   covers EVERY kind of focus. */
svg.wh-svg .wh-w:focus{ outline:none !important }
svg.wh-svg .wh-w:focus-visible, svg.wh-svg .wh-w.wh-playing{ outline:none; stroke:#fff !important; stroke-width:3.5 !important;
  paint-order:stroke; filter:drop-shadow(0 0 4px rgba(255,255,255,.9)); }
.pg-slot:focus{ outline:2px solid #fff; outline-offset:2px; border-radius:10px; }

/* ── THE MARK ──────────────────────────────────────────────────────────────────────────────────
   Nathan's glyph: a puzzle piece with the keyboard inside it — the app icon's two ideas, which is
   why it reads instantly. No filter. The artwork is a light piece carrying dark keys, so it sits
   correctly on the dark bar as drawn.
   ⚠️ Do NOT re-attempt this as a CSS mask. A mask reads ALPHA only, so the piece and the keys have
   identical alpha and the keys disappear — it renders as a blank piece. That cost me a long detour
   because the keys are invisible against the white background in a preview. */
.brandbar-v3 .bb-logo .brand-mark{
  display:block; width:34px; height:34px; object-fit:contain; opacity:.95;
}

/* ── HANDS-FREE. One button beside the transport, and a hint that appears when you switch it on and
   then leaves. Nathan: "I like how simple the cord Trainer interface looks right now and I'm really
   nervous about putting a bunch more information in there... as far as persistent information goes,
   it's about as cluttered as it should be right now." So: one icon persistent, everything else
   transient. */
.pg-micb{background:transparent;border:1px solid var(--line);border-radius:999px;
         width:38px;height:38px;font-size:16px;line-height:1;cursor:pointer;opacity:.75}
.pg-micb.on{border-color:#2f9e6b;background:rgba(47,158,107,.16);opacity:1}
/* ⬆️ NOT OVER THE TRANSPORT. Anchored to the bottom of the screen it sat directly on the play, loop
   and mic buttons — the very controls the instructions are telling you about. (Nathan: "when I engage
   the microphone, the help screen covers up the transport controls. That would be better placed if it
   were in the top area near where the AP display goes.") It is absolutely positioned inside the
   reading strip now, so it opens where the as-played comparison appears and overlays the wheel
   downward, leaving every control reachable while you read it. */
/* @keyframes vhint{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}

/* ── THINKING. The gap between speaking and the chord landing is real — the recognizer streams for
   most of a second — and an interface that shows nothing in that gap reads as one that did not hear
   you. Transient by design: it fades the moment the words are acted on. */
/* ⬆️ OFF THE TRANSPORT. Anchored 64px from the bottom it sat on the play, loop and mic buttons.
   (Nathan: "the area that displays the detected speech collides with controls.") It lives in the
   reading strip now — the top band the as-played comparison uses, which is empty while you are still
   speaking, so the words appear where you are already looking and nothing is covered. */
.ct-think{position:absolute;left:0;right:0;top:0;
          z-index:72;display:flex;align-items:center;gap:10px;pointer-events:none;
          background:#0f1a1d;border:1px solid #2c4248;border-radius:999px;padding:9px 15px;
          opacity:0;transform:translateY(6px);transition:opacity .16s ease, transform .16s ease}
.ct-think.on{opacity:1;transform:none}
.ct-think .ct-heard{font-size:13px;color:#c7d6da;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ct-dots{display:inline-flex;gap:4px;flex:0 0 auto}
.ct-dots i{width:6px;height:6px;border-radius:50%;background:#4fd1c5;display:block;
           animation:ctd 1s ease-in-out infinite}
.ct-dots i:nth-child(2){animation-delay:.15s}
.ct-dots i:nth-child(3){animation-delay:.3s}
@keyframes ctd{0%,60%,100%{opacity:.25;transform:translateY(0)}30%{opacity:1;transform:translateY(-3px)}}
@media (prefers-reduced-motion:reduce){ .ct-dots i{animation:none;opacity:.7} .ct-think{transition:none} }

/* The glyph now sits with the chevron and behaves like it — one control, two shapes. */
.brandbar-v3 .back-arrow{background:transparent;border:0;padding:0 2px 0 6px;color:inherit;
                          font-size:20px;line-height:1;cursor:pointer;opacity:.85}
.brandbar-v3 .bb-home{background:transparent;border:0;padding:0 8px 0 0;cursor:pointer;
                      display:inline-flex;align-items:center;opacity:.9}
.brandbar-v3 .bb-home:active,.brandbar-v3 .back-arrow:active{opacity:.55}
.brandbar-v3 .bb-home .brand-mark{height:34px;width:auto;display:block}
@media (min-width:560px){ .brandbar-v3 .bb-home .brand-mark{height:38px} }


/* ── 🗂️ THE COLLECTION — progressions Nathan starred, finally somewhere he can see them.
   A tab of the progress section, as asked, and each row cites its songs through the game's own index. */
.pstabs{display:flex;gap:6px;margin:2px 0 14px}
.pstab{appearance:none;border:1px solid var(--line);background:transparent;color:var(--dim);
  border-radius:999px;padding:7px 14px;font-size:12.5px;font-weight:700;cursor:pointer;
  -webkit-tap-highlight-color:transparent}
.pstab.on{background:var(--accent2);border-color:var(--accent2);color:#062024}
.collist{display:flex;flex-direction:column;gap:10px}
.colrow{display:grid;grid-template-columns:1fr auto;gap:6px 10px;align-items:center;
  background:var(--panel2);border:1px solid var(--line);border-radius:12px;padding:11px 12px}
.colmain{min-width:0}
.colrn{display:flex;flex-wrap:wrap;gap:5px;margin-bottom:3px}
.coltok{font-size:13px;font-weight:800;color:var(--ink);background:rgba(255,255,255,.06);
  border-radius:6px;padding:2px 7px;letter-spacing:.01em}
.colnm{font-size:12px;color:var(--dim);overflow:hidden;text-overflow:ellipsis}
.colkey{opacity:.55;font-size:11px}
.colside{display:flex;gap:6px;align-items:center}
.colplay{appearance:none;border:0;background:var(--accent2);color:#062024;width:34px;height:34px;
  border-radius:999px;font-size:13px;cursor:pointer;display:inline-flex;align-items:center;justify-content:center}
.colfav{appearance:none;border:0;background:none;color:#ffd75e;font-size:20px;cursor:pointer;padding:0 2px}
/* the way OUT of the collection. Deliberately quiet — it is a word, not a red button: Nathan on the
   new door, "the red styling looks like a warning placed on a sensitive term". It only turns warm
   under the finger, which is where a destructive action should declare itself. */
.colrm{appearance:none;border:1px solid var(--line);background:none;color:var(--dim);
  border-radius:999px;padding:6px 11px;font-size:11px;font-weight:700;letter-spacing:.02em;
  cursor:pointer;-webkit-tap-highlight-color:transparent;touch-action:manipulation;white-space:nowrap}
.colrm:active{border-color:#E8806F;color:#E8806F}
/* what stands in the row's place afterwards, so a removal is never a silent disappearance */
.colundo{opacity:.8}
.colundo-t{font-size:12.5px;color:var(--dim)}
.colundo-t b{color:var(--ink);font-weight:800}
.colundo-b{appearance:none;border:1px solid var(--accent2);background:rgba(45,172,179,.14);
  color:var(--accent2);border-radius:999px;padding:6px 14px;font-size:11.5px;font-weight:800;
  cursor:pointer;-webkit-tap-highlight-color:transparent;touch-action:manipulation}
.colundo-b:active{transform:scale(.95)}
.colsongs{grid-column:1 / -1;appearance:none;border:0;background:none;color:var(--accent2);
  font-size:12px;font-weight:700;text-align:left;padding:2px 0 0;cursor:pointer}
.colsongs.dim{color:rgba(255,255,255,.3);font-weight:600;cursor:default}
.colsonglist{grid-column:1 / -1;list-style:none;margin:6px 0 0;padding:8px 0 0;
  border-top:1px solid var(--line);display:flex;flex-direction:column;gap:5px;max-height:240px;overflow:auto}
.colsonglist li{font-size:12.5px;color:var(--ink);display:flex;gap:7px;align-items:baseline}
.colsonglist .sl-a{color:var(--dim);font-size:11.5px}

/* the chord sounding RIGHT NOW during a solved replay — the ring finally animates */
.wh-mk.sounding{stroke:#fff;stroke-width:3.5;filter:drop-shadow(0 0 5px rgba(255,255,255,.75))}
/* 🔊 THE WEDGE THAT IS RINGING. The travelling highlight used to live only on the numbered badge —
   which sits on the wedge you CHOSE, so a wrong chord lit the wrong wedge while the right one sounded.
   It belongs on the wedge whose chord is in the air. Rim only: the fill stays the wedge's own color,
   because the color IS the function and overriding it would teach the wrong thing for a beat. */
.wh-w.sounding{ stroke:#fff; stroke-width:3; filter:drop-shadow(0 0 6px rgba(255,255,255,.55));
  transition:stroke-width .12s ease }
@media (prefers-reduced-motion:reduce){ .wh-w.sounding{ filter:none } }
.wh-mkn.sounding{font-weight:900}
@media (prefers-reduced-motion:reduce){ .wh-mk.sounding{filter:none} }
.colsong{font-size:11.5px;color:rgba(255,255,255,.45);font-style:italic;margin-top:2px;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

/* the progress button names its destination; the count is the small print */
.brandbar-v3 .navstat .nav-lab-main{font-size:12.5px;font-weight:800;letter-spacing:.01em;color:var(--ink)}
.brandbar-v3 #navProgress .nav-lab{font-size:10.5px;opacity:.6}
/* the saved-progression play button, while it is sounding */
.colplay.playing{animation:colPlay 1.1s ease-in-out infinite}
@keyframes colPlay{ 0%,100%{ transform:scale(1) } 50%{ transform:scale(1.12) } }
@media (prefers-reduced-motion:reduce){ .colplay.playing{animation:none} }

/* ── 🎚️ MAJOR / MINOR: the board IS the switch. Two buttons big enough to hit without looking, which
   is the point of a beginner mode — the question is binary, so the answer should be one confident tap. */
/* 📐 IT RESERVES THE WHEEL'S SQUARE. Nathan: "the play controls should not move when the wheel is
   drawn." The wheel is a square SVG at width:100%, max 640 — so the toggle is the same square, the
   swap on grading changes nothing below it, and the buttons get the height to be genuinely huge. */
/* ⚠️ CENTRED, NOT SPACE-BETWEEN. The board reserves the wheel's whole square, and space-between pinned
   the buttons to the bottom of it — measured: 150px of dead air above them and SEVEN pixels between
   them and the play control. Nathan: "the major and minor voting button surfaces are out of balance
   with the rest of the interface and they are almost touching the play button." Centring the group
   splits that slack evenly, so the buttons sit with roughly the same air above and below. */
.mm-board{display:flex;flex-direction:column;gap:14px;align-items:stretch;justify-content:center;
  width:100%;max-width:640px;margin:0 auto;padding:4px 0}
.mm-ask{text-align:center;font-size:13px;color:var(--dim);letter-spacing:.02em}
.mm-ask b{color:var(--ink);font-weight:800}
/* the buttons FILL the reserved square — no cap. Dead space between the clue and the toggle was the
   whole of the wheel's footprint sitting empty; giving it to the targets is what the mode is for. */
/* ⚖️ BIG, NOT ENORMOUS. Nathan: "the major minor buttons don't need to be quite so massive." They
   filled the whole of the wheel's reserved square, which made two buttons the size of the screen.
   Capped now — still a confident thumb target, no longer a wall — and the square stays reserved so
   nothing below moves when the wheel takes its place. */
.mm-big{display:flex;gap:12px;flex:0 1 auto;min-height:0;height:min(42vh,260px)}
.mm-huge{flex:1 1 0;min-width:0;appearance:none;border:2px solid var(--line);border-radius:18px;
  background:var(--panel2);color:var(--ink);padding:30px 8px;cursor:pointer;display:flex;
  flex-direction:column;align-items:center;justify-content:center;gap:5px;
  -webkit-tap-highlight-color:transparent;
  touch-action:manipulation;transition:transform .07s ease,filter .07s ease,border-color .15s}
.mm-huge:active{transform:scale(.97);filter:brightness(.9)}
.mm-huge-t{font-size:24px;font-weight:900;letter-spacing:.06em;line-height:1}
.mm-huge-s{font-size:11px;font-weight:600;color:var(--dim);letter-spacing:.04em}
.mm-huge.maj{border-color:rgba(255,215,155,.55)}
.mm-huge.maj .mm-huge-t{color:#FFD79B}
.mm-huge.min{border-color:rgba(142,126,240,.55)}
.mm-huge.min .mm-huge-t{color:#8E7EF0}
.mm-replay{align-self:center;appearance:none;border:1px solid var(--line);background:none;color:var(--dim);
  border-radius:999px;padding:8px 18px;font-size:12px;font-weight:700;cursor:pointer}

/* the clue card in this mode is a READOUT, not a control: number, what you said, what it was */
.pg-slot.mm{gap:3px;padding:7px 3px;justify-content:center;min-width:0}
.pg-slot.mm .mm-n{font-size:10px;font-weight:800;color:rgba(255,255,255,.35);line-height:1}
.mm-said{font-size:15px;font-weight:800;color:rgba(255,255,255,.28);line-height:1.1}
.mm-said.has{color:var(--ink)}
.mm-said.wrong{color:#E8806F;text-decoration:line-through}
.mm-truth{font-size:11px;font-weight:800;color:#8FE34A;line-height:1}

/* ── ⚡ THE BOOT HAIRLINE. Not a skeleton — a single 3px rule that crawls while the board builds.
   The first version drew a card-sized box and a wheel-sized circle, which read as a broken screen
   rather than a loading one. The wait is a few frames; this is all it is worth. */
.prog-boot{padding:10px 0 0}
.prog-boot .pb-bar{height:3px;border-radius:2px;background:rgba(255,255,255,.08);overflow:hidden}
.prog-boot .pb-bar i{display:block;height:3px;width:32%;border-radius:2px;background:var(--accent2);
  animation:pbCrawl .8s ease-in-out infinite}
@keyframes pbCrawl{ 0%{ transform:translateX(-110%) } 100%{ transform:translateX(330%) } }
@media (prefers-reduced-motion:reduce){ .prog-boot .pb-bar i{animation:none;width:100%} }

/* force the major/minor readout onto one row — four equal columns, nothing allowed to push wider */
.pg-slots:has(.pg-slot.mm){grid-auto-flow:column;grid-auto-columns:minmax(0,1fr)}
.pg-slot.mm{min-width:0;overflow:hidden}


/* ── 😺 THE CHESHIRE REVEAL ────────────────────────────────────────────────────────────────────────
   Nathan: "in the reveal, the wedges should pop into place on their own as they are played like the
   Cheshire cat and then at the end the rest of the wheel be exposed."
   So after a major/minor hand the wheel comes back as NOTHING — app.js puts `mm-cheshire` on the host
   before the badges are painted — and each badge appears on the beat its chord sounds, hanging where
   its wedge will be. When the pass ends app.js drops the class and the ring fades up behind them.
   The badges are flat siblings inside .wh-svg (no groups), so a plain descendant rule can hide
   everything and .mm-shown can bring back exactly the ones that have played. */
#pgWheelHost.mm-cheshire .wh-svg *{ opacity:0; transition:opacity .55s ease }
#pgWheelHost.mm-cheshire .wh-svg .mm-shown{
  opacity:1;
  transform-box:fill-box; transform-origin:center;
  animation:mmPop .26s cubic-bezier(.2,1.7,.4,1) 1;
}
/* 🎯 YOUR RECORD ON EACH WEDGE. Three states, drawn as a stroke on the wedge itself so it reads as a
   property of that chord rather than as another object on the wheel. Nothing is drawn for a chord you
   have not met enough times — silence is the fourth state and the most common one at first. */
/* ── 🔴🟡🟢 THE ACCURACY MARK — a dot in the margin, not a ring on the work.
   Stroking the wedge itself put a red border around a chord Nathan had just answered CORRECTLY, and
   red already means "you missed this one" on the clue badge. (Nathan: "it reads more like a mistake.")
   So the mark moved OUTSIDE the wheel, where it annotates rather than accuses.
   This is a look at the data, not the finished design — it is admin-only while we work out what a
   player should actually see (Nathan wants a gleam on what you do well, and something for the rest
   that "isn't a bummer"). Small, unfilled of meaning until you know the key: green good, amber fair,
   red needs work. */
/* ── 🎨 MAJOR AND MINOR, SAID THE SAME WAY EVERYWHERE.
   The two colors the vote buttons and the tiles already use (#FFD79B / #8E7EF0), now on the chord
   name inside its wedge, so someone arriving from the major/minor level reads the wheel in a language
   they already know. (Nathan: "that language should be applied everywhere.") !important because the
   fill is set inline per wedge for contrast; the inherited stroke stays, which is what keeps the name
   legible on a light wedge. */
/* The pill takes the CHORD TILES' reveal styling exactly: a dark ground with the color in the ink,
   not a color-filled lozenge. (Nathan: "use the purple on black and orange on black styling that you
   use in the chord tiles during the reveal.") Same two inks the tiles and the vote buttons use, so a
   player arriving from rung zero reads the wheel without being taught anything new. */
/* the chord name under the keyboard, wearing the wedge pill's clothes — dark ground, colored ink,
   so the keys, the wheel and the vote buttons all say major and minor the same way. */
.kb-nm.kb-q-maj, .kb-nm.kb-q-min{
  background:#0b1215; border-radius:999px; padding:2px 9px; font-weight:800;
  border:1px solid transparent;
}
.kb-nm.kb-q-maj{ color:#FFD79B; border-color:rgba(255,215,155,.55) }
.kb-nm.kb-q-min{ color:#8E7EF0; border-color:rgba(142,126,240,.6) }
/* ⚠️ A WEDGE'S NAME IS PAINT, NOT A TARGET. These are drawn LAST so a grown bonus wedge cannot bury
   them — which also put them on top of everything under your finger. They are not wedges, so the
   background-press handler treated a tap on a chord's own label as a tap on empty space: holding a
   wedge to vote drew the prediction arrows instead of opening the quality fan, and voting stopped
   working entirely. (Nathan: "I can't even vote on this one. I'm trying to vote for F minor seven and
   I click and hold it and it gives me a bunch of arrows.") Presses fall through to the wedge beneath. */
#pgWheelHost .wh-svg .wh-nm, #pgWheelHost .wh-svg .wh-nmpill{ pointer-events:none }
#pgWheelHost .wh-svg .wh-nmpill{ fill:#0b1215; fill-opacity:.9; stroke-width:1 }
#pgWheelHost .wh-svg .wh-nmpill.wh-q-maj{ stroke:rgba(255,215,155,.55) }
#pgWheelHost .wh-svg .wh-nmpill.wh-q-min{ stroke:rgba(142,126,240,.6) }
#pgWheelHost .wh-svg .wh-nm.wh-q-maj{ fill:#FFD79B !important; stroke:none !important; font-weight:800 }
#pgWheelHost .wh-svg .wh-nm.wh-q-min{ fill:#8E7EF0 !important; stroke:none !important; font-weight:800 }
#pgWheelHost .wh-accdot{ stroke:#0b1f24; stroke-width:.8; pointer-events:auto; cursor:help }
#pgWheelHost .wh-svg .wh-accdot.acc-hi{  fill:#3ecf7a }
#pgWheelHost .wh-svg .wh-accdot.acc-mid{ fill:#FFB454 }
#pgWheelHost .wh-svg .wh-accdot.acc-lo{  fill:#E74C3C }
/* 🫥 a wedge this level has not taught is not on the board at all — see whOnlyThisLevel */
#pgWheelHost .wh-svg .wh-off{ display:none }
/* an invisible wedge must not be tappable — the ring is scenery until it is drawn */
#pgWheelHost.mm-cheshire .wh-svg{ pointer-events:none }
/* 🎬 THE POP, ON ITS OWN CLASS. `.mm-shown` means "this has arrived and stays"; re-adding it to a
   wedge that already arrived is a no-op, so a repeated chord in a progression landed silently. This
   class carries only the movement, retriggered per chord, so the walk reads the same whether the
   chord is new to the wheel or coming round again. (Nathan: "that behavior needs to be consistent.") */
#pgWheelHost .wh-svg .mm-pop{
  transform-box:fill-box; transform-origin:center;
  animation:mmPop .26s cubic-bezier(.2,1.7,.4,1) 1;
}
@media (prefers-reduced-motion:reduce){ #pgWheelHost .wh-svg .mm-pop{ animation:none } }
@keyframes mmPop{
  0%  { opacity:0; transform:scale(.25) }
  62% { opacity:1; transform:scale(1.22) }
  100%{ opacity:1; transform:scale(1) }
}
@media (prefers-reduced-motion:reduce){
  #pgWheelHost.mm-cheshire .wh-svg *{ transition:none }
  #pgWheelHost.mm-cheshire .wh-svg .mm-shown{ animation:none }
}

/* ── 💛💚 THE CARD PULSES IN THE COLOR OF THE CHORD IT IS PLAYING ─────────────────────────────────
   Nathan: "the glow around the clue card should pulse on the reveal using the major and minor color
   you have set in the chooser." markPlayhead() sets --mmglow from the SAME hexes the big buttons
   wear, so the color you pressed is the color that comes back. No fill-mode: when the pulse ends
   the card returns to its own glow, and the next chord pulses again. */
/* ⏱️ SHORTER THAN ONE CHORD. The reveal now runs at twice the asking tempo (~577ms a chord), so a
   950ms pulse never finished before the next chord restarted it — the glow just sat there instead of
   pulsing. It has to complete inside a step to read as a beat. */
.chordcard.pgcard.mm-pulse{ animation:mmGlowPulse .5s ease-out 1 }
@keyframes mmGlowPulse{
  0%  { box-shadow:0 0 0 3px var(--mmglow,#8E7EF0), 0 0 30px 9px var(--mmglow-soft,rgba(142,126,240,.42)) }
  65% { box-shadow:0 0 0 2px var(--mmglow,#8E7EF0), 0 0 16px 3px var(--mmglow-dim,rgba(142,126,240,.16)) }
  100%{ box-shadow:0 0 0 0 rgba(0,0,0,0), 0 0 0 0 rgba(0,0,0,0) }
}
@media (prefers-reduced-motion:reduce){ .chordcard.pgcard.mm-pulse{ animation:none } }

/* ⚠️ THE PLAYHEAD RING DOES NOT BELONG ON A DIAL. `.bul.dial` is a 66px TRANSPARENT box around a
   ~26px colored number, so the box-shadow ring above drew a wide empty circle floating clear of the
   number instead of hugging it — visible in the capture as a halo twice the size of the "3". The dial
   has its own playhead treatment two rules down (a drop-shadow on the dial graphic plus a nudge on
   the core), which is the one that should run. Plain bullets keep the tight ring. */
.chordcard.pgcard .pg-slot.sounding .bul.dial{ box-shadow:none; transform:none }


/* ── 🎨 ONE MEANING PER CHANNEL ──────────────────────────────────────────────────────────────────
   Nathan: "there looks to be no correlation between colors… I need a system that encompasses all of
   this with a visual language that's easy to see at a glance." Fair — color was carrying two or three
   different jobs at once and they were fighting. Three channels, three jobs, and nothing borrows
   another's ink:

     WHICH CHORD IS THIS?   the clue's POSITION color (chordColor 1..4).
                            Worn by the numbered dot in the tile AND by the badge on the wedge, which
                            is the whole point — clue 2's dot and the "2" on the ring match, so you can
                            follow one to the other. ("the numbers on the wedges should also correlate
                            with the clue number.")
     WHAT QUALITY WAS IT?   the mode's two colors — major gold, minor teal.
                            Worn by the tile's WASH and by the chord name inside it. ("in major/minor
                            mode the answers inside the clue tile should correspond to the major or
                            minor color scheme.")
     DID I GET IT RIGHT?    the bar across the top of the tile, and nothing else.
                            ("you don't need the color bar across the top to indicate anything other
                            than success, in which case maybe let's just make it red.") It only exists
                            once a hand is graded, because before that there is no verdict to report. */
.chordcard.pgcard .pg-slot{ position:relative }
/* (the verdict bar across the top has gone — it said the same thing the outline now says, and a bar
   IS a fill, which is the thing that was competing with the quality wash.) */
/* quality: the wash, and the chord name inside. `tok` sets --tc to the POSITION color, which is right
   in the standard trainer and wrong here — in this mode the name should say major or minor. */
.chordcard.pgcard .pg-slot.mm-maj{ background:rgba(255,215,155,.15) }
.chordcard.pgcard .pg-slot.mm-min{ background:rgba(142,126,240,.18) }
/* !important because `tok` writes --tc as an INLINE style, which a stylesheet rule cannot outrank.
   Measured: without it the names kept coming out in the position colors. */
.chordcard.pgcard .pg-slot.mm-maj .tok-ch{ --tc:#FFD79B !important }
.chordcard.pgcard .pg-slot.mm-min .tok-ch{ --tc:#8E7EF0 !important }


/* ── ⬆️ THE WHEEL MOVES UP UNDER THE READING STRIP ───────────────────────────────────────────────
   Nathan: "I think there's room to move the wheel up a little bit without it hitting the AP display.
   Even if the wheel tucks underneath a little bit of that element when it displays at the very top
   it's probably OK." The wheel's SVG already carries padding inside its own viewBox, so the visible
   ring starts well below the box — which is why there was ~30px of dead air doing nothing.
   ⚠️ THE TOGGLE TAKES THE SAME PULL. The major/minor board stands in the wheel's place, so if only
   one of them moved up, everything below — the transport, the keyboard — would shift by that amount
   the moment a hand graded. Same number on both, or the thing we just fixed comes straight back.
   The strip is painted OVER the wheel, so a tuck reads as a tuck rather than a collision. */
/* ⚠️ THE WHEEL NO LONGER CLIMBS ONTO THE STATUS LINE. Both of these were pulled up 26px to tighten
   the gap, but .mm-ask sits directly above and is 40px tall — so the wheel parked on its lower half
   and "All 4 right." rendered on top of the wedges. Nathan, twice: "there's a little remnant of text
   right next to the tonic wedge", then "look at all the garbage on the screen. The leftover text."
   The board keeps its pull (nothing sits above it); the wheel gives the line its room back. */
.mm-board{ margin-top:-26px }
/* ⚠️ THE WHEEL PAINTS OUTSIDE ITS OWN BOX. Its viewBox reserves room for the RING, but a grown bonus
   wedge reaches well past that — pills and all — so the drawing overflows upward into the status line
   and lands on top of it. Nathan has now reported this twice: first "All 4 right" across the wedges,
   now "play a chord to answer it". Removing the negative pull was not enough because the overflow is
   the wedges themselves. This reserves a strip the wheel cannot climb into. */
#pgWheelHost{ margin-top:14px }
.mm-ask{ position:relative; z-index:4; margin-bottom:4px }
/* ⚠️ IT PAINTS OVER THE WHEEL, SO IT MUST NOT CATCH THE WHEEL'S CLICKS. The strip sits above the
   wheel on purpose (a tuck, not a collision) — but it is a full-width block, so its empty half was
   swallowing every tap on the top wedge. (Nathan: "in FreePlay I am unable to click the top Home wedge.
   I assume there is something related to a button stealing that click even though it's not displayed.")
   Worse since the wheel was tucked up under it. The strip is now transparent to the pointer and only
   the things actually IN it — the reading control and the pills — take a tap. */
.pg-read-strip{ position:relative; z-index:2; pointer-events:none }
.pg-read-strip button, .pg-read-strip .pg-stack, .pg-read-strip .pg-stacks,
.pg-read-strip .tok, .pg-read-strip a{ pointer-events:auto }
#pgWheelHost{ position:relative; z-index:0 }

/* ── 🔦 AFTER THE REVEAL: THE FOUR THAT PLAYED STAY UP, THE REST STAND DOWN ──────────────────────
   Nathan: "leave the Cheshire wedges at full capacity and dim the others slightly." The reveal ends
   with the whole ring exposed, but the four chords you just heard are the content — so they hold
   full strength and everything else drops back, which leaves the progression legible on the wheel
   instead of dissolving into ten equal wedges. Slightly: still perfectly readable, just not shouting.
   `.mm-shown` is already on exactly the elements that popped, so there is nothing new to track. */
/* ⚠️ NEVER WHILE THE CHESHIRE IS RUNNING. `.wh-focus .wh-w:not(.mm-shown)` scores (1,3,0) against the
   Cheshire's (1,2,1) and sits later in the file, so once a hand had revealed — which is when wh-focus
   goes on — it OUTRANKED the rule that hides the wheel, and held every unshown wedge at .55 instead
   of 0. The wheel never went dark, so nothing appeared to walk in. It broke the graduation lap (which
   always runs right after a reveal) and every reveal after the first in a session.
   (Nathan: "it's currently playing through the animation to introduce the wheel and the Cheshire
   thing isn't happening.") The two states cannot both be true, so the selector now says that. */
#pgWheelHost.wh-focus:not(.mm-cheshire) .wh-w:not(.mm-shown),
#pgWheelHost.wh-focus:not(.mm-cheshire) .wh-nm:not(.mm-shown),
#pgWheelHost.wh-focus:not(.mm-cheshire) .wh-fn:not(.mm-shown),
#pgWheelHost.wh-focus:not(.mm-cheshire) .wh-sub:not(.mm-shown){ opacity:.55; transition:opacity .45s ease }
/* …and a tap brings one back. Nathan: "in FreePlay, clicking a wedge should make it full capacity."
   The dimming is a reading aid, not a lock — pressing a wedge to hear it should light it up. */
#pgWheelHost.wh-focus .wh-w{ transition:opacity .2s ease }
@media (prefers-reduced-motion:reduce){
  #pgWheelHost.wh-focus .wh-w:not(.mm-shown),
  #pgWheelHost.wh-focus .wh-nm:not(.mm-shown),
  #pgWheelHost.wh-focus .wh-fn:not(.mm-shown){ transition:none }
}


/* ── ▶︎ THE TRAVELLING PLAYHEAD, TAKE THREE ─────────────────────────────────────────────────────
   Nathan, for the third time: "there is still no indication of what clue is playing when it plays
   back." The class was never the problem — measured, .sounding advances slot0 → slot1 → slot2 on the
   right card every time. It was that NOTHING VISIBLE WAS ATTACHED TO IT on a graded card, which is
   the only time a reveal plays. A graded slot's bullet is `.bul.dial`: a transparent box holding a
   38px dial whose segments sit at opacity .16, and the only styling was a white drop-shadow on that
   — a faint glow on a faint thing. I also suppressed the one solid ring I had added, because on a
   dial it drew a halo twice the size of the number. So both channels were off at once.
   A BAR UNDER THE SLOT, which is what a playhead looks like to anyone who has used a DAW. It cannot
   be confused with the aim cursor (a full border) or with a vote (a filled pill), and it travels. */
/* drawn with an INSET shadow rather than ::after, because the waiting tile below needs both of its
   pseudo-elements for the rotating beam and a slot can be waiting and sounding at the same moment. */
/* ⚠️ IT WAS LITERALLY AN UNDERLINE. `inset 0 -4px` paints a bar along the BOTTOM EDGE ONLY, which is
   exactly what Nathan kept reporting: "the selected tile during playback still has a highlight that
   looks skewed towards the bottom and reads more like an underline." It was, every time — I had been
   hunting for a misaligned ring while the real thing was a deliberate bottom bar. An even inset ring
   now, same color and weight on all four sides, with the glow it already had. */
.chordcard.pgcard .pg-slot.sounding{
  background:rgba(127,214,219,.18);
  box-shadow:inset 0 0 0 3px #7fd6db, 0 0 12px 1px rgba(127,214,219,.45);
}
@media (prefers-reduced-motion:reduce){
  .chordcard.pgcard .pg-slot.sounding{ box-shadow:inset 0 0 0 3px #7fd6db }
}

/* ── ⏳ THE TILE THAT IS WAITING FOR YOU ─────────────────────────────────────────────────────────
   Nathan's own design: "a subtle glow around the edge of the individual clue tile was rotating and
   the north-east corner of that glow favoured a major color and the south-west favoured the minor."
   Two colored shadows 180° apart, orbiting the tile — major yellow starts north-east, minor teal
   south-west, exactly as described. Done with box-shadows rather than a conic gradient on purpose:
   conic needs @property to animate its angle, which this WebKit does not have, and it would have
   failed silently on the phone while looking right on the Mac. */
/* ⚠️ TWO ORBITING SHADOWS WAS THE WRONG SHAPE. Nathan: "it looks like a little corner of glow leaking
   out of one side. I want this to be more like a glow whose intensity rotates around the edge of the
   square. Imagine a light behind the tile pointed like the hand of a clock but obviously a wider beam.
   And as that hand swings past the 5 o'clock position its color smoothly moves between major and
   minor until it hits the 10 o'clock position."
   So it is literally a light behind the tile: a wide conic wedge on a pseudo-element BEHIND the slot
   (z-index:-1), blurred into a beam, rotating. TWO of them — one major, one minor — spinning in lock
   step with their opacities crossfading, which is how the color blends smoothly without needing
   @property to animate a gradient's own color stops. @property is the obvious way to do this and it
   is NOT in this WebKit: it would have looked right on the Mac and done nothing on his phone.
   THE CLOCK MATHS: the beam sits ~48° ahead of the element's rotation, so the beam points at 5 o'clock
   (150°) when rotation is 102° — 28% of the way round — and at 10 o'clock (300°) at rotation 252°, or
   70%. Hence the crossfade keyframes below; change the beam's offset and these move with it. */
@keyframes beamSpin{ from{ transform:translate(-50%,-50%) rotate(0deg) }
                       to{ transform:translate(-50%,-50%) rotate(360deg) } }
@keyframes beamMaj { 0%,28%{ opacity:1 } 70%{ opacity:0 } 100%{ opacity:1 } }
@keyframes beamMin { 0%,28%{ opacity:0 } 70%{ opacity:1 } 100%{ opacity:0 } }
/* ⚠️ isolation:isolate IS LOad-BEARING. Without it the beam sits at z-index:-1 inside the CARD's
   stacking context (the active card has a transform and a z-index, so it makes one) — which puts it
   behind the card's own background, so the only part of the glow you could see was the bit overlapping
   the tile itself. That is precisely the "little corner of glow leaking out of one side" Nathan
   described. Making the SLOT its own stacking context puts the beam above the card's background and
   below the tile's, so it reads as a light behind the tile and spills properly around all four edges. */
.chordcard.pgcard .pg-slot.waiting{ overflow:visible; isolation:isolate }
/* ⛔ ONE RING ON THE ARMED TILE. The outline sits 2px INSIDE and a box-shadow sat 2px OUTSIDE, so the
   two together made a 4px band in two slightly different teals — which is what made the edge look
   thick and misaligned rather than crisp. The outline is the marker; nothing else draws an edge. */
/* ⚠️ …BUT NOT WHILE IT IS ALSO SOUNDING. Chord one is armed AND playing at the same moment, so this
   blanket rule wiped its sounding ring and it was the one tile that never lit up. (Nathan, twice: "the
   first chord still does not get the same highlight as the second.") Mine, both times. */
.chordcard.pgcard .pg-slot.mm-armed:not(.sounding){ box-shadow:none!important }
/* ⚠️ LONGHAND, NOT `inset`. With the shorthand the pseudo-element computed to width:0 height:0 and
   painted nothing at all — measured, `::before w=0px h=0px` — and older WebKit does not know `inset`
   either, so this would have failed twice over on the phone. Four explicit offsets stretch it. */
.chordcard.pgcard .pg-slots.slots-waiting{ overflow:visible }
/* ⚠️ A RING, NOT A DISC. Masking a conic beam to a circle put a visible CIRCLE behind a square tile —
   Nathan saw it immediately: "I can see the circle shape behind the square… what I'm looking for is
   not actually a circle. The closest thing I can describe would be an LED strip around the border."
   So the shape is now a rounded-RECTANGLE band: the <i> is masked to a 4px strip following the tile's
   own corner radius, and the light that travels round it is the <b> spinning inside. The mask is the
   standard two-layer xor (border-box minus content-box), which is what makes the middle hollow.
   ⚠️ NO filter:blur() ANY MORE. A continuously rotating blurred conic gradient has to be re-rastered
   every frame on a phone, and Nathan reported taps going missing right after it shipped. Softness is
   baked into the gradient's own stops instead, so the strip is a static texture the GPU only has to
   rotate. Unproven as the cause of the missed taps, but it is the kind of thing that causes them and
   it costs nothing to not do. */
/* ── ✨ THE COLLECTIBLE SHINE ────────────────────────────────────────────────────────────────────
   Nathan, after I overcorrected into near-invisibility: "too subtle, can hardly see it. Let's go with
   something established. Make it look like it's got a little shiny edge the way a collectible in a
   video game might look… like a little shimmer of light around the edge of a square coin. Let's change
   the motion so that it is one quick trip around the circumference and a pause."
   That is a well-worn game idiom and it has two parts, which is what every earlier attempt was missing:
     1. a STEADY lit rim, so the tile always reads as "this one is pickupable" even standing still;
     2. a fast SPECULAR sweep — narrow, near-white, bright — that runs once and then goes away.
   Constant motion was the mistake. A highlight that never stops is a worm; the same highlight fired
   once every few seconds is a glint. The pause is what makes it read as a surface catching the light.
   The rim lives on the masked ring itself and the glint on the element spinning inside it, so at rest
   there is no bright spot parked anywhere — it fades out before the sweep ends. */
/* ⚠️ ALIGNED TO THE TILE. These sat 6px outside it on every edge, so the armed tile carried a second
   rounded rectangle peeking out from behind its own ring — which reads as a shadow, or an underline
   along the bottom. (Nathan: "you often make a mistake where the highlighted border is offset
   incorrectly and looks more like an underline. I think that's happening here.") One tile, one edge. */
.chordcard.pgcard .pg-slot .slot-beam{
  position:absolute; inset:-2px;        /* the tile has a 2px transparent border; this is its outer edge */
  border-radius:14px;                   /* …and 12px + that 2px border is the radius at that edge */ z-index:-1; pointer-events:none; display:block;
  padding:4px; overflow:hidden; box-sizing:border-box;
  filter:blur(2px);                       /* crisp enough to read as a shine, not a smudge */
  -webkit-mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite:xor; mask-composite:exclude;
}
.chordcard.pgcard .pg-slot .slot-beam>b{
  /* ⚠️ THE TROPE IS A SWIPE, NOT A LAP. Six attempts at this were all the same wrong idea: a bright
     point travelling the perimeter. Nathan, finally: "I'm basically asking for a trope." He is right,
     and the trope — card shine, holo foil, loading shimmer, every collectible glint in every game — is
     a DIAGONAL BAND OF LIGHT SWEPT ACROSS THE OBJECT. Nothing orbits. I anchored on his first phrase
     ("around the edge of the square") and hand-rolled a chase six times instead of reaching for the
     effect everyone already uses.
     Clipped to the rim by the mask above, a straight band crossing a rounded rectangle lights it in
     TWO places that travel apart and come back together — which is exactly what a coin edge does when
     light passes over it, and it falls out of the standard recipe instead of being faked. */
  /* ⚠️ DISTRACTION IS CONTROLLED BY FREQUENCY, NOT BY BRIGHTNESS — and getting that backwards is what
     sent this round in circles. Nathan said "lighten either the weight OR the intensity"; I did both,
     and sped it up as well. Three subtractions at once, and the result was invisible — "too subtle,
     can hardly see it" — which then read as a failure of the whole idea rather than of my dosing.
     A dim thing that keeps happening is muddy AND still nags. A bright thing that happens rarely is
     calm and legible. So: the peak goes back UP to where a specular actually reads as light, the band
     stays narrow, and the pass becomes RARE — once every four seconds instead of every one and a half.
     ⚠️ AND IT IS SLOWER, NOT FASTER. 260ms is below the threshold at which the eye registers a
     travelling highlight; it lands as a flicker you half-catch, which is unsettling rather than
     pleasant. ~700ms is the range every card-shine and holo-foil effect actually uses, and it is what
     lets the movement be seen as movement. Nathan: "maybe it's actually going too fast. Maybe actually
     slow to go. I don't know, I'm not an animator." He should not have to be. */
  position:absolute; top:-60%; left:0; width:32%; height:220%; display:block;
  will-change:transform;
  background:linear-gradient(90deg,
             rgba(255,255,255,0)   0%,
             rgba(255,255,255,.42) 38%,
             rgba(255,255,255,.85) 50%,
             rgba(255,255,255,.42) 62%,
             rgba(255,255,255,0)  100%);
  transform:translateX(-170%) skewX(-20deg);
  animation:shineSwipe 4s ease-in-out infinite;
}
/* ONE DIRECTION (Nathan). The return pass is gone. My worry about a one-way sweep having to "teleport
   home" does not apply here: the band is parked fully OUTSIDE the ring at both ends of its travel and
   the mask clips it, so the reset happens where nothing can be seen. ~700ms per pass (17.5% of a 4s
   cycle), then a 3.3s rest — the same once-every-four-seconds rhythm, just always the same way.
   ease-in-out so the pass accelerates and settles rather than running at a constant speed. */
@keyframes shineSwipe{
  0%        { transform:translateX(-170%) skewX(-20deg) }
  17.5%     { transform:translateX(320%)  skewX(-20deg) }
  100%      { transform:translateX(320%)  skewX(-20deg) }
}
/* the rim keeps the mode's two colors — "maintain the colors for the color quadrant" — just quieter,
   and at twice the swipe's period so consecutive passes alternate major-gold and minor-teal. */
.chordcard.pgcard .pg-slot .beam-maj{ background:rgba(255,215,155,.26); animation:beamMaj 8s linear infinite }
.chordcard.pgcard .pg-slot .beam-min{ background:rgba(142,126,240,.28); animation:beamMin 8s linear infinite }
/* twice the sweep's period, so consecutive glints alternate major-gold and minor-teal */
@media (prefers-reduced-motion:reduce){
  .chordcard.pgcard .pg-slot .slot-beam,
  .chordcard.pgcard .pg-slot .slot-beam>b{ animation:none }
  .chordcard.pgcard .pg-slot .slot-beam>b{ opacity:0 }   /* the rim stays, the glint does not fire */
  .chordcard.pgcard .pg-slot .beam-min{ opacity:.5 }
}

/* ── ▶︎ AND THE BUTTON BREATHES WHILE THE HAND IS UNTOUCHED ─────────────────────────────────────
   "Let's pulse the play button on an idle screen with no answers yet." Quiet — a ring that swells and
   fades, not a flash. It stops the moment anything is answered. */
@keyframes playIdle{
  0%,100% { box-shadow:0 3px 14px rgba(0,0,0,.4), 0 0 0 0 rgba(127,214,219,.5) }
  55%     { box-shadow:0 3px 14px rgba(0,0,0,.4), 0 0 0 14px rgba(127,214,219,0) }
}
.pg-transport .pg-playbtn.idle{ animation:playIdle 2.3s ease-out infinite }
@media (prefers-reduced-motion:reduce){ .pg-transport .pg-playbtn.idle{ animation:none } }


/* ── ✦ THE PROGRESSION'S SHAPE ON THE WHEEL ──────────────────────────────────────────────────────
   The line joining badge 1 to 2 to 3 to 4. It PAINTS ITSELF in order, which is how the figure says
   which way it goes without needing arrowheads — you watch it travel from the first chord to the
   last. The closing segment back to 1 is fainter and arrives after, so a four-chord loop reads as a
   loop rather than as a fifth leg.
   It draws UNDER the badges — the badge markup is held back in its own string so the order is wedges,
   line, numbers. With the numbers safely on top the stroke can be brighter and heavier than when it
   was crossing over them. */
.wh-shape, .wh-shape-back, .wh-shape-ans{ fill:none; stroke-linecap:round; stroke-linejoin:round; pointer-events:none }
/* ── 🛣️ THE PATH THE PIECE TAKES, under the path you took. Green because green has one meaning on this
   wheel and it is "this is the answer". It sits quiet by default — on a clean hand it is directly under
   your own line and you should read ONE path, not two — and comes forward while a tapped chord rings,
   which is when you are asking where it should have gone. (Nathan: "when I play it back switch to what
   I should have played.") */
.wh-shape-ans{
  stroke:rgba(94,214,143,.98); stroke-width:3.0;
  /* ⚠️ NO DRAW-ON ANIMATION ANY MORE. This is now the wheel's ONE permanent path, and the wheel
     re-renders on every playback tick — so a 1.2s draw-on restarted several times a bar and the line
     strobed. It is a standing piece of information, so it is simply drawn. */
  filter:drop-shadow(0 0 3px rgba(0,0,0,.5));
}
/* playing a slot back: the correction is the thing being read, so yours stands down for as long as it
   rings. The numbered circles do NOT move — they are still on the wedges you chose. */
.wh-correcting .wh-shape{ stroke:rgba(255,255,255,.22); stroke-width:1.8 }
.wh-shape{ transition:stroke .25s ease, stroke-width .25s ease }
@media (prefers-reduced-motion:reduce){ .wh-shape-ans{ animation:none; stroke-dashoffset:0 } }
.wh-shape{
  stroke:rgba(255,255,255,.8); stroke-width:2.4;
  stroke-dasharray:1400; stroke-dashoffset:1400;
  animation:whShapeDraw 1.2s cubic-bezier(.3,0,.2,1) .25s forwards;
  filter:drop-shadow(0 0 3px rgba(0,0,0,.55));
}
.wh-shape-back{
  stroke:rgba(255,255,255,.26); stroke-width:1.6; stroke-dasharray:4 5;
  opacity:0; animation:whShapeBack .5s ease-out 1.5s forwards;
}
@keyframes whShapeDraw{ to{ stroke-dashoffset:0 } }
@keyframes whShapeBack{ to{ opacity:1 } }
@media (prefers-reduced-motion:reduce){
  .wh-shape{ animation:none; stroke-dashoffset:0 }
  .wh-shape-back{ animation:none; opacity:1 }
}


/* the clue's own number, bottom corner of its tile — the only thing on a graded tile that still says
   WHICH clue it is. Small and quiet: it is a label, not a headline. Painted in the clue's position
   color, the same one its badge wears on the wedge, so the two read as the same object. */
.chordcard.pgcard .pg-slot .pg-cnum{
  position:absolute; left:4px; bottom:2px; font-size:9px; font-weight:800; line-height:1;
  opacity:.75; pointer-events:none; z-index:3; font-variant-numeric:tabular-nums;
}


/* While anything is sounding the tile's border belongs to the playhead, so the shimmer stands down.
   ⚠️ !important IS REQUIRED HERE, and not as a shortcut. The rim carries a running CSS animation on
   OPACITY (the major/minor crossfade), and a running animation outranks any normal declaration — so
   a plain `opacity:0` would have been silently ignored and the shimmer would have kept crossing the
   playhead exactly as before. An important declaration is the one thing that outranks an animation. */
body.pg-sounding .chordcard.pgcard .pg-slot .slot-beam{ opacity:0 !important }


/* ── ⬇️ NOTHING BELOW THE FOLD WHILE A GAME IS OPEN ──────────────────────────────────────────────
   Nathan: "sometimes when I go to vote I inadvertently scroll the screen down, which wouldn't need to
   happen if there was nothing down there." Measured on the major/minor screen: the document ran 1418px
   against a 956px viewport — 462px of scrollable page, essentially all of it this block. A vote is a
   press on a target near the bottom of the screen, and any page that CAN scroll will sometimes read
   that press as a drag. Take the scroll away and the ambiguity goes with it.
   Hidden rather than deleted: this is also the school's only promotion on the public web page, and
   removing it from the product outright is Nathan's call to make, not a side effect of a layout fix. */
body.at-prog #cta{ display:none }


/* ── the progress meter, as a hairline along the level chooser's bottom edge ─────────────────────
   Not inline — that was the mistake that made me tell Nathan it wouldn't fit. Pinned to the button's
   own edge it spans the full width, pushes nothing off the end, and costs no height at all, which
   retires the 21px row it used to need. */
/* ⚠️ IT HAS TO READ AS A METER, NOT AS AN UNDERLINE. At 1 hand of 6 the fill is ~17% wide, which put
   a short cyan bar directly under the word "Minor" and made it look like the level name was underlined
   — Nathan: "collision in the meter." A meter needs a TRACK: with a faint full-width rail behind it,
   a short fill reads as "not much progress yet" instead of as a text decoration. It also sits hard on
   the button's bottom edge, clear of the text's own baseline. */
/* ⚠️ IT IS A THUMB TARGET, NOT A LABEL. Nathan: "that chooser is a little bit too small to hit with
   my thumb easily." It opens the level menu — the single most consequential control on the screen —
   and it was sized like a status line. Apple's own floor is 44px and this now clears it with room. */
/* ⚠️ THE CHOOSER HAD A BORDER BUT NO SIDE PADDING, so the level name sat hard against its own
   left edge. (Nathan: "the level picker is crowding the left side of its container.") The bar and
   its marks are absolutely positioned, which resolves against the PADDING box — so they still run
   the full width of the control and only the text inset moves. */
/* three bands have to fit under the title: ticks, the reference labels, and "you" above them. The
   height for that comes back out of the two big targets below, which are still a generous thumb. */
/* ── 🪪 THE BAR IS STYLED BY WHAT IT IS, NOT BY WHOSE BUTTON IT IS IN.
   Every rule below used to be scoped to `#pgLvlBtn` — the chord trainer's element ID — so the renderer
   could be shared (see rungBarHTML) and the LOOK still could not. Sing it back got the right markup and
   no styling at all: the marks fell inline and piled onto the level line. Scoped to `.rungbtn` instead,
   any screen that builds this control gets the control, and restyling it here restyles it everywhere.
   (Nathan, 2026-10-05: "That is the template for our interface going forward and it should be
   consistent everywhere... I want to design this stuff once and update it everywhere going forward.") */
.rungbtn{ position:relative; overflow:hidden; padding:9px 12px 54px; min-height:78px }
/* 📏 THE RUN METER IS THE THING PEOPLE WATCH, SO IT GETS ROOM AND PROPER ENDS. It ran edge to edge along
   the very bottom of a rounded, clipped container — square ends meeting round corners, so it read as cut
   off, and the labels crowded the rim. (Nathan: "the progress meter feels crowded at the bottom of that
   container. It's literally getting cut off a little bit. There should be some kind of endcap actually on
   that line. Probably on both sides… This is the area that people are supposed to be watching to see how
   they are doing. It looks like an afterthought.") It now sits INSIDE the control with its own capped
   track, and every mark on it is measured against that track rather than the whole button. */
:root{ --lvl-inset:14px }
/* ── 🔥 THE STREAK IS THE REWARD, SO IT GETS TO BEHAVE LIKE ONE ─────────────────────────────────
   Nathan: "the main reward mechanic is the streak. And it's not really noticeable. It deserves a
   broader stroke and even some animation." It was a 3px hairline that grew silently. Now: twice the
   stroke, a slow shimmer travelling along the filled part so a live run reads as ALIVE at a glance,
   and a flare on the head of the bar each time it takes a step. */
.rungbtn::after{
  content:""; position:absolute; left:var(--lvl-inset); right:var(--lvl-inset); bottom:10px; height:7px;
  background:rgba(255,255,255,.12); border-radius:999px; pointer-events:none;
  box-shadow:inset 0 0 0 1px rgba(255,255,255,.07);
}
.rungbtn .lvl-meter{
  position:absolute; left:var(--lvl-inset); bottom:10px; height:7px; display:block; z-index:1; overflow:hidden;
  width:calc((100% - var(--lvl-inset) * 2) * var(--pct, 0) / 100);
  border-radius:999px;
  background:linear-gradient(90deg, var(--accent2,#2dacb3), #7fd6db);
  transition:width .45s cubic-bezier(.3,0,.2,1); pointer-events:none;
  box-shadow:0 0 6px rgba(127,214,219,.28);
}
/* the shimmer rides the filled track only — it is clipped by the meter's own width, so it is a
   readout of the run rather than decoration painted over the whole control */
.rungbtn .lvl-meter::before{
  content:""; position:absolute; inset:0; border-radius:inherit; pointer-events:none;
  background:linear-gradient(100deg, rgba(255,255,255,0) 30%, rgba(255,255,255,.62) 47%, rgba(255,255,255,0) 64%);
  background-size:260% 100%; animation:lvlShine 3s linear infinite;
}
@keyframes lvlShine{ from{ background-position:170% 0 } to{ background-position:-90% 0 } }
/* …and the step itself: the bar brightens and its leading edge flares as the width slides out */
.rungbtn .lvl-meter.step{ animation:lvlStep .66s cubic-bezier(.2,1.3,.4,1) 1 }
@keyframes lvlStep{
  0%   { filter:brightness(1);    box-shadow:0 0 6px rgba(127,214,219,.28) }
  30%  { filter:brightness(1.75); box-shadow:0 0 16px 3px rgba(127,214,219,.75) }
  100% { filter:brightness(1);    box-shadow:0 0 6px rgba(127,214,219,.28) }
}
@media (prefers-reduced-motion:reduce){
  .rungbtn .lvl-meter::before{ animation:none; opacity:.25 }
  .rungbtn .lvl-meter.step{ animation:none }
}
#pgLvlBtn .hb-streak{ color:var(--accent2,#2dacb3); font-weight:800 }
#pgLvlBtn .hb-best{ color:var(--dim); font-weight:600 }
@media (prefers-reduced-motion:reduce){ #pgLvlBtn .lvl-meter{ transition:none } }


/* ── 🚫 "YOU HAVEN'T HEARD IT YET" ───────────────────────────────────────────────────────────────
   Three signals for one refusal, because they reach you in different situations: the hollow tock if
   you are looking at the wheel, the shake if your thumb is already on its way, and the pulse on the
   play button to say where to go instead — a refusal that only says "no" leaves you guessing. */
@keyframes voteNope{
  0%,100% { transform:translateX(0) }
  15%     { transform:translateX(-7px) }
  32%     { transform:translateX(6px) }
  50%     { transform:translateX(-4px) }
  68%     { transform:translateX(3px) }
  85%     { transform:translateX(-1px) }
}
.vote-nope{ animation:voteNope .5s cubic-bezier(.36,.07,.19,.97) both }
@keyframes playNudge{
  0%,100% { box-shadow:0 3px 14px rgba(0,0,0,.4), 0 0 0 0 rgba(255,59,48,.55); transform:scale(1) }
  30%     { box-shadow:0 3px 14px rgba(0,0,0,.4), 0 0 0 12px rgba(255,59,48,0);  transform:scale(1.06) }
}
.pg-transport .pg-playbtn.nudge{ animation:playNudge .46s ease-out 3 }
/* ── 🔁 "ALREADY YOURS", NOT "REJECTED". Pressing the wedge a slot already holds plays the chord back;
   the screen used to do nothing at all, which Nathan read as the vote being refused. A short affirming
   pulse — NOT the voteNope shake, which means no — says the tap landed and the slot is unchanged. */
@keyframes pgAlready{
  0%,100% { box-shadow:0 0 0 0 rgba(255,255,255,0);   transform:scale(1) }
  35%     { box-shadow:0 0 0 4px rgba(255,255,255,.34); transform:scale(1.045) }
}
.pg-slot.pg-already{ animation:pgAlready .68s ease-out both }
@media (prefers-reduced-motion:reduce){
  .vote-nope{ animation:none }
  .pg-slot.pg-already{ animation:none; outline:2px solid rgba(255,255,255,.5); outline-offset:2px }
  .pg-transport .pg-playbtn.nudge{ animation:none; outline:3px solid #FF3B30; outline-offset:3px }
}


/* what you voted, spelled out on the tile — the dictation check. Small and quiet: the number is still
   the identity, this is a caption under it. Colored by the quality so it agrees with the tile's wash
   and with the chooser button you pressed. */
/* ⚠️ PINNED TO THE BOTTOM, not flowed. `.dial-core` (the number) is absolutely positioned, so the only
   in-flow child of the bullet floats to the TOP — the word landed above the number and ran into the
   clear button. It is a caption, so it sits under the number where a caption goes. */
.chordcard.pgcard .pg-slot .bul.dial{ position:relative }
.chordcard.pgcard .pg-slot .mm-said{
  position:absolute; left:0; right:0; bottom:-1px; text-align:center;
  font-size:9.5px; font-weight:800; line-height:1;
  letter-spacing:.02em; text-transform:lowercase; pointer-events:none;
}
.chordcard.pgcard .pg-slot .mm-said-maj{ color:#FFD79B }
.chordcard.pgcard .pg-slot .mm-said-min{ color:#8E7EF0 }


/* ── 🔥 THE STREAK HAS SOMETHING AT STAKE IN IT ──────────────────────────────────────────────────
   A number that looks the same at 2 as it does at 11 carries no tension. Three states, and they only
   appear once there is a record worth chasing (best of 3+), so a first-time player is never shown
   drama about a streak of one.
     CLOSING — within two of your best. Warm, and it starts breathing.
     LEVEL   — you have matched it. The moment with the most to lose, so it holds rather than pulses.
     RECORD  — past it. Gold, and every hand from here extends it. */
#pgLvlBtn .hb-streak.closing{ color:#FFB454; animation:streakClose 1.6s ease-in-out infinite }
#pgLvlBtn .hb-streak.level{
  color:#FFD79B; background:rgba(255,215,155,.14); border-radius:999px; padding:1px 8px;
  box-shadow:0 0 0 1px rgba(255,215,155,.45);
}
#pgLvlBtn .hb-streak.record{
  color:#0d2027; background:linear-gradient(90deg,#FFD79B,#FFB454); border-radius:999px; padding:1px 8px;
  font-weight:900; box-shadow:0 0 10px rgba(255,180,84,.45);
}
@keyframes streakClose{ 0%,100%{ opacity:1 } 50%{ opacity:.62 } }
@media (prefers-reduced-motion:reduce){ #pgLvlBtn .hb-streak.closing{ animation:none } }


/* the pressure valve's status. Deliberately calm — it is not a warning and not a badge of shame; it
   reports which voicings are in play, the way a key signature would. */
#pgLvlBtn .hb-valve{
  color:#8fd3e8; font-weight:700; font-size:.92em;
  background:rgba(143,211,232,.12); border-radius:999px; padding:1px 7px;
}


/* NEXT is the way out of a finished hand and was sized like a footnote beside a 76px play button.
   It also picks up the same travelling shine the waiting tile uses, so the two read as one invitation:
   the card is done, the next one is over here. (Nathan: "do the shimmer effect on the next button at
   the same time to show they are connected.") */
.pg-transport .pg-nextb{
  position:relative; overflow:hidden; padding:11px 20px; font-size:13.5px; min-height:44px;
  /* 📐 ONE LINE, ALWAYS. The label used to switch to the longer "new card" and wrap to two lines,
     shoving the transport around; the label is now always "next", so it only has to be kept off a
     second line. A width FLOOR lived here briefly to hold both labels' width — with one label it had
     nothing to hold and simply pushed the button off the right edge of a phone. */
  white-space:nowrap; text-align:center;
}
/* 📐 THREE CONTROLS HAVE TO FIT A PHONE. The right-hand pair sits beside a big centerd play button, and
   the widest case is the revealed hand: "loop" is a wider word than "3×" and it pushed "next" off the
   screen. Trim the padding at phone widths rather than shrink the type — these are thumb targets. */
@media (max-width:440px){
  .pg-transport .pg-nextb{ padding:11px 13px }
  .pg-transport .pg-loopb{ padding:9px 10px }
  .pg-transport>.pg-tright{ gap:6px }
}
.pg-transport .pg-nextb::after{
  content:""; position:absolute; top:-60%; left:0; width:32%; height:220%; pointer-events:none;
  background:linear-gradient(90deg, rgba(255,255,255,0) 0%, rgba(255,255,255,.42) 38%,
             rgba(255,255,255,.85) 50%, rgba(255,255,255,.42) 62%, rgba(255,255,255,0) 100%);
  transform:translateX(-170%) skewX(-20deg);
  animation:shineSwipe 4s ease-in-out infinite;
}
@media (prefers-reduced-motion:reduce){ .pg-transport .pg-nextb::after{ animation:none; opacity:0 } }


/* the wrong badge is a question you can ask, so it has to look askable */
.wh-mk.wh-mk-wrong{ cursor:pointer; pointer-events:auto }
.wh-mkn{ cursor:pointer }

/* ⚠️ THE CHOOSER HAS TO LOOK LIKE A CONTROL. Nathan: he was not sure it reads as pickable. It is the
   one control that changes what the whole screen is doing, and it was drawn as a status line with a
   small caret. A border and a surface say "press me" in a way a caret alone does not. */
.rungbtn{ border:1px solid var(--line); background:rgba(255,255,255,.04); border-radius:12px; }
.rungbtn:active{ background:rgba(255,255,255,.09) }
.rungbtn .lvlmenu-caret{ opacity:.9 }


/* ── THE BAR IS A SCALE WITH TWO MARKS ON IT: where you are now, and your best.
   The old tick was pinned to the RIGHT EDGE because the far end of the bar WAS your best — which
   left the target unlabellable and gave a record nowhere to go. The scale now runs past the target,
   so both marks sit inside the bar and both can carry a number.
   Each mark is one element: <u> is the tick, <b> is the number, and the mark's color drives both
   through currentColor so a state change is one declaration. side-l / side-r hang the number off
   the left or right of its tick — the trailing mark takes the left, the leading one the right, so
   the two numbers splay apart rather than landing on top of each other as the streak closes. */
.rungbtn .lvl-mk{
  position:absolute; bottom:10px; z-index:3; width:0; height:0; font-style:normal; pointer-events:none;
  left:calc(var(--lvl-inset) + (100% - var(--lvl-inset) * 2) * var(--at, 0) / 100);
}
/* 🔻 EACH MARK IS ITS OWN COLOR AND ITS OWN SHAPE, so a filled track can never hide one. The average
   was a 2px grey tick that vanished the moment the run passed it. (Nathan: "I think I see the tick mark
   for my average. It's just small and grey and deserves a better marker. Maybe even a little orange
   triangle… let's color code those labels so if there is a track that is filled past the average, I can
   still see where it lives.") The tick sits ABOVE the track rather than inside it, so the fill cannot
   swallow it, and each one is drawn in the color its label wears. */
.rungbtn .lvl-mk u{
  position:absolute; left:-1.5px; bottom:-1px; width:3px; height:11px; border-radius:1.5px;
  background:currentColor; text-decoration:none;
  box-shadow:0 0 0 1.5px rgba(6,18,22,.75);
}
/* the average gets a pointer, not a hair */
.rungbtn .lvl-avg u{
  width:0; height:0; left:-5px; bottom:8px; border-radius:0; background:none;
  box-shadow:none;                      /* the ticks' dark outline would draw a square behind a triangle */
  border-left:5px solid transparent; border-right:5px solid transparent;
  border-top:7px solid currentColor;
  filter:drop-shadow(0 0 1.5px rgba(6,18,22,.9));
}
.rungbtn .lvl-avg{ color:#FFB454 }
.rungbtn .lvl-goal{ color:#7fd6db }
/* 📏 LEGIBLE, AND CLEAR OF THE TRACK. At 9.5px sitting 11px up, the descenders were cutting into the
   fill. (Nathan: "it's clipping the bottom of the text against the fill of the track… there's no reason
   to make this so small it is illegible.") */
/* ⚠️ THE LABEL ROW SITS ABOVE THE TICKS, not across them. Measured: "usual 1.5" ran from x55 to x95
   while the "you" tick stood at x62 — so the tick printed straight through the number and it read as
   clipped. (Nathan: "still clipping".) Labels start above the tallest tick now. */
.rungbtn .lvl-mk b{
  position:absolute; bottom:27px; font-size:11px; font-weight:800; line-height:1.15;
  letter-spacing:.02em; white-space:nowrap; color:currentColor;
}
/* ── 💊 THE MOVING MARK IS A PILL, AND YOU CAN SEE THROUGH IT. It is the only label that travels, so
   it is the only one that will run over the others as your run grows. A solid label just hid whatever
   it landed on; a translucent pill lets the track, the ticks and a neighbouring label read straight
   through it. (Nathan: "maybe it is a pill that pulses gently so you can see what's behind it when it
   collides with something else.") The pulse is on the BACKGROUND only — the text stays at full
   contrast, because a label that fades in and out is a label you cannot read at the wrong moment.
   ⚠️ This does NOT replace the clearance rule that makes `best` stand down when the two are close:
   translucency fixes a pill over a TICK, it cannot fix text printed over text. */
/* ⚠️ THE PILL MASKS, IT NO LONGER SHOWS THROUGH. The background was 72% translucent on purpose so the
   track read through it — but translucency over a TICK is fine and translucency over another LABEL is
   not: "whatever is underneath it doesn't bleed through when it collides with the other elements on
   the scale" (Nathan). It is now opaque, with its own dark halo so it reads as lifted off whatever it
   is currently sitting on top of.
   ⚠️ AND IT SITS ON THE SAME LINE AS THE OTHER LABELS. `bottom` anchors the bottom EDGE, so this
   pill's 2px of padding pushed its text up while `level up` / `best here` stayed put — the word "you"
   floated above everything. The emoji made it worse: `em` set a font-size but no line-height, and an
   emoji's line box is taller than the Latin text beside it, lifting the whole pill again. Pulling
   `bottom` back by the padding and pinning the glyph's line box puts all four labels on one line. */
.rungbtn .lvl-now b{
  padding:2px 6px; border-radius:999px;
  background:var(--panel2,#0e2830);
  box-shadow:0 0 0 1px color-mix(in srgb, currentColor 55%, transparent),
             0 2px 8px rgba(0,0,0,.55);
  /* 3.2s, not 2.4 — the whole label now disappears and returns, and that needs long enough at each
     end to actually read the number rather than catching it mid-fade. */
  animation:youPulse 3.2s ease-in-out infinite;
}
/* the glyph must not set the pill's height */
.rungbtn .lvl-now b em{ line-height:1; vertical-align:baseline; display:inline-block }
@supports not (color: color-mix(in srgb, red 10%, blue)){
  .rungbtn .lvl-now b{ background:#0e2830; box-shadow:0 0 0 1px rgba(45,172,179,.55), 0 2px 8px rgba(0,0,0,.55) }
}
/* A VISIBLE pulse. The old one animated a 1px ring between 45% and 80% of its own color, which is
   invisible on a phone — Nathan: "it's in a pill but it does not pulse". The ring now grows into a
   halo you can actually see. Background and text never change, so the label stays readable and the
   box never changes size (no layout shift against the marks beside it). */
/* ⚠️ THE PILL BREATHES, SO THE COLLISION STOPS MATTERING. Masking alone left "level up 13" sliced in
   half — the pill covered the word instead of the word being readable. Nathan's fix is better than
   mine: "if the YOU pill were pulsing and disappearing, the collision wouldn't matter." So the
   BACKGROUND fades out and back, and whatever it is sitting on top of is legible for half the cycle.
   The TEXT never fades — a label you cannot read at the wrong moment is not a label — so both numbers
   are always there, taking turns at being the one in front. */
/* ⚠️ THE WHOLE PILL GOES, NOT JUST ITS BACKGROUND. I animated the backing and held the TEXT at full
   opacity on the reasoning that a label you cannot read at the wrong moment is not a label. That was
   wrong here, and Nathan said so twice: "the only thing that blinks is the background that masks the
   collision. The whole thing needs to come and go to reveal what's underneath it." If the text stays,
   it is still sitting on top of the number it is covering — the mask just got transparent behind it.
   Fading the ENTIRE element is what actually takes turns: for part of every cycle the label beneath is
   completely unobstructed, and both numbers get read. Slow enough to read either one, and it never
   reaches zero, so the pill never appears to have gone away. */
@keyframes youPulse{
  0%,100%{ opacity:1;
           box-shadow:0 0 0 1px color-mix(in srgb, currentColor 95%, transparent),
                      0 0 0 4px color-mix(in srgb, currentColor 22%, transparent),
                      0 2px 8px rgba(0,0,0,.55) }
  62%    { opacity:.12;
           box-shadow:0 0 0 1px color-mix(in srgb, currentColor 30%, transparent),
                      0 0 0 0 color-mix(in srgb, currentColor 0%, transparent),
                      0 2px 8px rgba(0,0,0,0) }
}
/* ── ↔️ THE TWO LABELS TAKE TURNS. "you" and "level up" land on the same pixels, and the YOU pill
   already faded out to let whatever was beneath it show. But the goal label was STATIC, so during
   YOU's visible half they simply overlapped — which is what Nathan kept seeing, and why the two
   numbers looked like one reading. (Nathan: "Blink the YOU in opposition to the level up so I can
   see they are not correlated. Let it go back-and-forth.")
   Same 3.2s cycle, inverted: when YOU is up the goal is down, and the moment YOU fades the goal
   arrives. The alternation is the message — two things trading a space are visibly two things.
   ⚠️ The pill must NEVER disappear entirely at the same moment as the goal, or the bar blinks empty:
   the crossover sits at 62%, where one is arriving as the other leaves. */
@keyframes goalPulse{
  0%,100%{ opacity:.14 }
  62%    { opacity:1 }
}
.rungbtn .lvl-goal b{ animation:goalPulse 3.2s ease-in-out infinite }
@media (prefers-reduced-motion:reduce){
  .rungbtn .lvl-now b{ animation:none }
  .rungbtn .lvl-goal b{ animation:none }
}
.rungbtn .lvl-mk.side-l b{ right:4px }
.rungbtn .lvl-mk.side-r b{ left:4px }
.rungbtn .lvl-best{ color:#C9A7F5 }
/* the lifetime record — same family as "best here", dimmer, because the one you can beat today is the
   one that should draw the eye. Both are on the bar; neither hides in a tooltip a phone cannot show. */
.rungbtn .lvl-life{ color:#C9A7F5; opacity:.62 }
.rungbtn .lvl-life b{ font-weight:600 }
.rungbtn .lvl-best b{ font-weight:600 }
/* the travelling mark paints ABOVE the fixed ones — it is the one that moves, so it is the one that
   lands on top of things, and its mask is only a mask if it wins the stacking order. It is rendered
   last today, which makes this incidental rather than stated; state it. */
.rungbtn .lvl-now{ color:var(--accent2,#2dacb3); z-index:6 }
.rungbtn .lvl-goal, .rungbtn .lvl-best, .rungbtn .lvl-life{ z-index:3 }
/* the same three states the prose used to carry, now carried by the mark itself */
.rungbtn .lvl-now.closing{ color:#FFB454; animation:streakClose 1.6s ease-in-out infinite }
.rungbtn .lvl-now.level{ color:#FFD79B }
.rungbtn .lvl-now.record{ color:#FFB454; filter:drop-shadow(0 0 4px rgba(255,180,84,.6)) }
@media (prefers-reduced-motion:reduce){ #pgLvlBtn .lvl-now.closing{ animation:none } }
.rungbtn .lvl-meter.record{
  background:linear-gradient(90deg,#FFD79B,#FFB454);
  box-shadow:0 0 8px rgba(255,180,84,.5);
}


/* the streak restart is an ACTION sitting in a list of places you can go, so it must not look like
   another level. Quieter, and it goes flat when there is no streak to restart. */
/* ⚠️ THE RESTART IS AN ACTION, NOT A PLACE. It sat in a column of levels wearing a level's panel and
   border, so it read as somewhere you could go. (Nathan: "the switch to restart my streak looks like
   another level and it's confusing.") Stripped to a plain amber control — no panel, no level border, a
   dashed edge — so the eye sorts it out of the list before reading a word of it. */
/* 📎 THE MARGIN: a footnote at the foot of the menu. No panel, no heading, no row — it is a tangent you
   can take, not a place you can go. (Nathan, repeatedly: "resetting a streak does not deserve a line…
   it belongs in margin somewhere. It is not a level.") */
/* -- THE MARGIN: one quiet row under the ladder, not a stack of wrapping links.
   It was right-aligned underlined text, so a sentence-length label broke across lines and ragged
   itself against the levels above. (Nathan: "the alignment of the text is terrible and this could be
   communicated without a line break... we could probably easily lose an entire line.")
   The notes now sit side by side as small pills on one row -- a shape that reads as a control you can
   switch rather than a footnote, and one that cannot wrap. */
.lvlmenu-margin{
  display:flex; flex-wrap:wrap; align-items:center; gap:8px;
  padding:9px 4px 3px; margin-top:2px; border-top:1px solid var(--line);
}
.lvlmenu-note{
  appearance:none; cursor:pointer; text-align:left; white-space:nowrap;
  background:rgba(255,255,255,.04); border:1px solid var(--line); border-radius:999px;
  color:var(--dim); font-size:11px; font-weight:700; letter-spacing:.01em;
  padding:5px 11px; text-decoration:none; text-transform:none; line-height:1.1;
}
.lvlmenu-note:not(:disabled):hover{ color:#FFB454; border-color:#FFB454 }
.lvlmenu-note:disabled{ opacity:.45; cursor:default }
/* a mode that is ON has to look on -- a control that quietly stops paying you is one people think broke */
.lvlmenu-note.note-on{ color:#0b1f24; background:#FFB454; border-color:#FFB454 }
.lvlmenu-note.note-on:hover{ color:#0b1f24; background:#ffc274 }
@media (max-width:420px){ .lvlmenu-note{ font-size:10.5px; padding:5px 9px } }
.lvlmenu-flight.band-neutral .lvlmenu-lvl[data-rs]:not(.locked){ color:#FFB454 }
.lvlmenu-flight.band-neutral .lvlmenu-lvl[data-rs]:hover{ border-color:#FFB454; background:rgba(255,180,84,.08) }
.lvlmenu-flight.band-neutral .lvlmenu-lvl[data-rs]::after{ content:none!important }   /* never "● here" */
.lvlmenu-flight.band-neutral .lvlmenu-lvl[data-rs].locked{ opacity:.55 }


/* ── 💧 THE DRAIN. The bar you just lost, emptying from where it stood and fading as it goes. It is
   drawn OVER the (now empty) live fill for one shot only, so it reads as the old streak leaving
   rather than as a new one arriving. Short, and it does not block anything. */
.rungbtn .lvl-drain{
  position:absolute; left:var(--lvl-inset); bottom:10px; height:4px; z-index:2; pointer-events:none;
  background:linear-gradient(90deg, rgba(255,120,120,.25), #FF6B6B);
  border-radius:999px;
  animation:streakDrain .62s cubic-bezier(.4,0,.6,1) forwards;
}
@keyframes streakDrain{
  0%   { width:calc((100% - var(--lvl-inset) * 2) * var(--from-pct, 0) / 100); opacity:.95 }
  70%  { opacity:.85 }
  100% { width:0; opacity:0 }
}

/* the glyph rides in front of the number inside the same mark, so one element still carries the
   whole state and the color reaches both through currentColor */
.rungbtn .lvl-mk b em{ font-style:normal; margin-right:3px; font-size:11.5px; letter-spacing:0 }
/* the figure beside each label: same color, a touch heavier, never underlined */
.rungbtn .lvl-mk b s{ text-decoration:none; font-variant-numeric:tabular-nums; opacity:.95; margin-left:1px }

/* ── ✨ THE MILESTONE. Fires on three, on entering striking range, on matching, and past the record
   — never on an ordinary hand, because a celebration that fires constantly is wallpaper. One pop of
   scale and glow, then it is gone and the mark sits still again. */
.rungbtn .lvl-mk.flash b{ animation:streakFlash .72s cubic-bezier(.2,1.4,.4,1) 1 }
@keyframes streakFlash{
  0%   { transform:scale(1);    filter:none }
  28%  { transform:scale(1.55); filter:drop-shadow(0 0 7px currentColor) }
  100% { transform:scale(1);    filter:none }
}
.rungbtn .lvl-mk.flash u{ animation:streakFlashTick .72s ease-out 1 }
@keyframes streakFlashTick{
  0%   { height:7px }
  28%  { height:13px; box-shadow:0 0 8px currentColor }
  100% { height:7px }
}
@media (prefers-reduced-motion:reduce){
  .rungbtn .lvl-drain{ animation-duration:.01s }
  .rungbtn .lvl-mk.flash b, .rungbtn .lvl-mk.flash u{ animation:none }
}


/* the goalpost: the same mark shape as the others, but it is a TARGET not a reading, so it is drawn
   in the neutral ink the level name uses rather than in a streak state color. It never animates. */
/* ⚠️ COLOR-CODED (Nathan) — each reference mark has its own hue, so a filled track cannot hide one */
.rungbtn .lvl-goal{ color:#7fd6db; opacity:1 }
.rungbtn .lvl-goal u{ height:13px; opacity:1 }
.rungbtn .lvl-goal b{ font-weight:700 }
/* the trophy tick with no label, when best sits too close to the goal to print a number */
.rungbtn .lvl-best u{ height:13px }


/* the three reference markers are LABELS, not readings — same weight as each other, quieter than
   "you", and none of them animates. "you" is the only thing on the line that moves. */
.rungbtn .lvl-avg{ color:#FFB454; opacity:1 }
.rungbtn .lvl-avg u{ }
.rungbtn .lvl-goal b, .rungbtn .lvl-avg b, .rungbtn .lvl-best b{
  font-weight:700; font-size:11px; letter-spacing:.02em; text-transform:lowercase;
}
.rungbtn .lvl-now b{ font-weight:800 }


/* ── TWO TIERS, BECAUSE ONE WAS NOT ENOUGH. With every label hung off the same line, "you" landed on
   top of "average" the moment you were near it — which is exactly when you most want to read both.
   The three REFERENCE markers (level up, average, best) sit on the lower tier against the track;
   YOU sits on the upper tier with a tall tick spanning both, so it reads as the thing that moves
   across a fixed scale and it can never be overwritten by a marker it is passing. */
/* ⚠️ THIS is the rule that governs the label rows — it is later than the one further up, so it wins.
   Both were wrong together: 9px labels sitting 5px off the bottom, with the "you" tick deliberately
   spanning BOTH tiers. That span is what printed a tick straight through "usual 1.5" and made it read
   as clipped. Three clean bands now: ticks up to 20px, the reference labels from 22, "you" from 35 —
   and the tick stops below the labels instead of crossing them. (Nathan: "still clipping".) */
/* ⚠️ ONE ROW NOW, NOT TWO. The upper tier above was my answer to "you" colliding with the reference
   labels, and Nathan rejected it twice: "'You' shouldn't be on its own row. Let it collide with level
   up", then "the word you is still floating above everything else". Lifting it 13px solved overlap by
   making the label look detached from the scale it is measuring. The opaque pulsing pill is the fix
   for collision — it MASKS what it lands on — so the tier is no longer paying for itself.
   ⚠️ A `bottom` for .lvl-now set anywhere ABOVE this line is dead: this rule is later and wins. I set
   one up there and measured no change at all. Change the number HERE. */
.rungbtn .lvl-mk b{ bottom:22px; font-size:11px }
.rungbtn .lvl-now b{ bottom:22px; font-size:11px }
/* the tick no longer has two tiers to span */
.rungbtn .lvl-now u{ height:11px }
/* the ticks are tall enough to find. The average is EXCLUDED — it is a triangle, not a bar, and a
   height here would deform it. */
.rungbtn .lvl-goal u, .rungbtn .lvl-best u{ height:11px }
.rungbtn .lvl-mk.flash u{ height:15px }
@keyframes streakFlashTick{ 0%{ height:15px } 28%{ height:22px; box-shadow:0 0 8px currentColor } 100%{ height:15px } }




/* the hands-free heading loses the red with its tiles */
.door-sec.bd-t5{ color:var(--band-1); opacity:.9; }
/* A LONE TILE spans both columns rather than sitting at half width with a hole beside it. It stays
   a COLUMN — icon over name over line. Laying it out as a row would put the name and its
   description side by side, because they are siblings of the icon, not wrapped together. */
.door-shelf > .door-g:only-child{ grid-column:1 / -1; min-height:0; }
.door-shelf > .door-g:only-child .nm{ margin-top:4px; }


/* ── ✏️ THE TILE WITHOUT THE DRAWING. Nathan is hand-drawing the icons; until they land — and if any
   one of them is ever missing — the tile must still look composed rather than broken. A hairline in
   the band color takes the icon's place: same height reserved, same baseline for the name, and the
   band color still present on the tile. Strip every drawing out and the door reads as a deliberate
   minimal design, which is the test. ("Make it so it's beautiful even if it is stripped.") */
.door-rule{ display:block; width:22px; height:2px; border-radius:2px; background:currentColor;
            opacity:.55; flex:0 0 auto; margin:11px 0 10px; }
/* the streak chip lost its flame — no emoji on this page */
.door-chip.streak{ color:var(--band-3); }
.door-chip.streak b{ color:var(--band-3); font-weight:800; }


/* the drawings are out for now — no icon, and no hairline standing in for one */
.door-ico, .door-rule{ display:none; }
/* three short lines, so the level marker sits clear of all of them */
.door-g .door-lv{ top:auto; bottom:11px; right:11px; }
.door-g .nm{ margin-top:1px; }
.door-g .ds{ margin-top:3px; }


/* ── THE MARKER GOES BACK TO THE TOP. Sitting bottom-right it trailed the description and read as
   part of it — "Follow the changes. rung 2". Beside the kicker it reads as what it is: a status on
   the tile, not a word in the sentence. The kicker reserves the corner instead of the description,
   which is the better trade because the kicker is the shorter line. */
.door-g .door-lv{ top:11px; right:12px; bottom:auto; }
.door-g.b-next .fr, .door-g.b-new .fr, .door-g.b-stretch .fr, .door-g.b-lvl .fr,
.door-g.b-soon .fr, .door-g.haslv .fr{ padding-right:44px; }
.door-g.b-next .ds, .door-g.b-new .ds, .door-g.b-stretch .ds, .door-g.b-lvl .ds, .door-g.b-soon .ds,
.door-g.haslv .ds{ padding-right:0; }


/* ── ✂️ THE MODES THAT CAME OFF THE SHELF. Still reachable, still one tap, but they no longer ask
   for a decision at the same weight as a game. A row of quiet rules rather than four more boxes —
   the point of cutting the door down was to reduce the number of things competing for the thumb,
   and turning them into smaller boxes would not have done that. */
.door-more{ display:flex; flex-direction:column; gap:0; border:1px solid var(--line);
            border-radius:13px; overflow:hidden; background:var(--panel); }
.door-m{ display:flex; align-items:baseline; gap:8px; width:100%; padding:11px 13px; background:none;
         border:0; border-bottom:1px solid var(--line); color:var(--ink); font:inherit;
         font-size:13.5px; font-weight:600; cursor:pointer; text-align:left; }
.door-m:last-child{ border-bottom:0; }
.door-m small{ color:var(--dim); font-size:11.5px; font-weight:400; margin-left:auto;
               text-align:right; max-width:58%; }
.door-m:active{ background:rgba(255,255,255,.05); }
/* the daily's own color — its own box now that "Start here" is one thing */
.door-g.bd-t6{ color:var(--band-0); }
.door-sec.bd-t6{ color:var(--band-0); opacity:.9; }


/* ── "press to play" on the tile that is waiting for you. It is an INSTRUCTION, not a status, so it
   reads in the accent rather than the dim ink and breathes gently — but slowly, because it sits on
   the clue row and a fast pulse there competes with the playhead. It disappears the moment the hand
   starts playing, and the moment the slot is answered. */
.pg-slot .pg-hint{ display:none }   /* retired — replaced by .pg-hear, a real control */
/* ▶ HEAR IT AGAIN, on every answerable tile. A control, not advice: it plays that chord and, because
   a tile tap is a question, arms it. Quiet on an ordinary tile and lit on the live one, so the tile
   you are answering also carries the loudest invitation to re-hear it. */
.pg-slot .pg-hear{
  display:block; text-align:center; font-size:9px; font-weight:700; letter-spacing:.01em;
  color:var(--dim); margin-top:4px; line-height:1.2; white-space:nowrap; opacity:.75;
  border-radius:999px; padding:2px 0; cursor:pointer;
}
.pg-slot.live .pg-hear{
  color:var(--accent,#2dacb3); opacity:1;
  background:rgba(45,172,179,.12); box-shadow:inset 0 0 0 1px rgba(45,172,179,.4);
}
@keyframes hintBreathe{ 0%,100%{ opacity:.95 } 50%{ opacity:.45 } }
@media (prefers-reduced-motion:reduce){ .pg-slot .pg-hint{ animation:none } }


/* ── 🔒 THE VOTE CONTROLS ARE INERT UNTIL A CHORD IS LIVE.
   Genuinely disabled, not dimmed-but-tappable: a reflexive tap must do NOTHING rather than land in
   the wrong place. The player learns the rhythm by watching them turn on, which teaches better than
   any label — and it is the difference between a guardrail and a suggestion. */
.mm-big.mm-off .mm-huge{ opacity:.28; filter:saturate(.25); cursor:default; }
.mm-big.mm-off .mm-huge:active{ transform:none; }
.mm-huge[disabled]{ pointer-events:none; }
/* live: the buttons wear the chord's own color and its number, so the target is never inferred */
.mm-huge{ position:relative; transition:opacity .18s ease, filter .18s ease, box-shadow .18s ease; }
.mm-big:not(.mm-off) .mm-huge{ box-shadow:inset 0 0 0 2px var(--slotc, transparent); }
.mm-huge-n{
  position:absolute; top:9px; left:0; right:0; text-align:center;
  font-size:13px; font-weight:900; letter-spacing:.02em; color:var(--slotc,var(--dim)); opacity:.95;
}
.mm-big.mm-off .mm-huge-n{ opacity:0; }
/* the prompt above them says which chord, or says to play one */
.mm-ask b{ color:var(--accent,#2dacb3); }


/* ── THE CHORD YOU ARE ANSWERING, on the card. Persists after playback stops, because the target
   persists — it is the last chord you heard, not a window you have to hit. Deliberately a RING, so
   it sits in a different channel from the playhead (which glows the number) and from a placed vote
   (which fills the pill). Three states, three treatments, no two meaning the same thing. */
.pg-slot.live{ box-shadow:inset 0 0 0 2px var(--accent,#2dacb3); border-radius:12px; }
.pg-slot.live .dial-num{ box-shadow:0 0 0 2px rgba(45,172,179,.5); }


/* ── ✅ THE CONFIRM STEP, first three hands. It says the answer AND the place out loud, because the
   place is the thing new players get wrong. Then it retires itself. */
.mm-confirm-ov{ z-index:420 }            /* above .modal-ov's 400 — nothing else is up here */
.mm-confirm{
  width:100%; max-width:330px; border:1px solid var(--accent,#2dacb3); border-radius:16px;
  padding:18px 16px 14px; background:var(--panel,#12303a); text-align:center;
  box-shadow:0 18px 50px rgba(0,0,0,.5);
}
.mmc-q{ font-size:15px; font-weight:600; margin-bottom:11px; line-height:1.35 }
.mmc-q b{ font-weight:900 }
.mmc-maj{ color:#FFD79B } .mmc-min{ color:#8E7EF0 }
.mmc-row{ display:flex; gap:9px }
.mmc-row button{ flex:1; padding:13px 10px; border-radius:11px; font:inherit; font-weight:800;
  font-size:14px; cursor:pointer; border:1px solid var(--line); background:transparent; color:var(--ink) }
.mmc-yes{ background:var(--accent,#2dacb3)!important; border-color:var(--accent,#2dacb3)!important;
  color:#07222a!important }
.mmc-off{ margin-top:9px; background:none; border:0; color:var(--dim); font:inherit; font-size:11.5px;
  text-decoration:underline; cursor:pointer; padding:4px }
/* the scrim does the work now — no need to dim the buttons underneath it */


/* the chord's circle, inside a line of prose. Deliberately NOT .dial-core — that rule is absolutely
   positioned for the tile and would fall out of the sentence. Same look, standalone. */
/* ⚠️ THE NUMERAL WAS OFF CENTRE. inline-grid centring PLUS a vertical-align nudge is two ways to
   position one glyph and they fought each other. One inline-flex box, line-height:1 so the glyph's
   own leading cannot push it up, and no nudge. (Nathan: "you would have cut that if you had checked
   with a photo" — correct, and it is checked now.) */
/* ⚠️ vertical-align:middle, NOT a top nudge. top:5px was a guess and it sat the circle low against
   the line — Nathan saw it twice. `middle` aligns the box's center with the text's own optical
   middle, which is exactly the comparison the eye is making. No magic numbers. */
/* ⚠️ SIZE AND RAISE, BOTH MEASURED. It read low twice because a 25px circle centerd on the text's
   x-height hangs further BELOW the baseline than any letter does — the eye reads that overhang as
   "low" even when the centring is correct. Smaller, and lifted so its center lands on the middle of
   the CAPITALS, which is what it sits beside. Compared against four other options in a render before
   choosing, rather than nudged a third time. */
.mmc-dot{
  display:inline-flex; align-items:center; justify-content:center;
  width:22px; height:22px; border-radius:50%; line-height:1; font-weight:800; font-size:12.5px;
  margin:0 4px; vertical-align:middle; position:relative; top:-2px;
}
/* ── THE QUALITY WEARS THE BOX IT CAME FROM. Nathan: "wrap the word major or minor in some kind of
   box to associate it with the selection box it came from." Same colors as the button that was
   pressed, so the sentence names an object the player just touched rather than a word. */
.mmc-chip{
  display:inline-flex; align-items:center; justify-content:center; padding:2px 8px;
  border-radius:7px; font-size:13px; font-weight:900; letter-spacing:.04em; line-height:1.35;
  vertical-align:middle; position:relative; top:-1px;
}
.mmc-chip-maj{ background:rgba(255,215,155,.16); color:#FFD79B;
               box-shadow:inset 0 0 0 1.5px rgba(255,215,155,.55) }
.mmc-chip-min{ background:rgba(142,126,240,.18); color:#A99BFF;
               box-shadow:inset 0 0 0 1.5px rgba(142,126,240,.6) }
/* the checkbox that replaces the dismiss link: full width, its own row, a real target */
.mmc-chk{
  display:flex; align-items:center; justify-content:center; gap:10px; margin-top:11px;
  padding-top:11px; border-top:1px solid var(--line); color:var(--dim); font-size:12.5px;
  cursor:pointer; min-height:44px;
}
.mmc-chk input{ width:20px; height:20px; accent-color:var(--accent,#2dacb3); flex:0 0 auto; margin:0 }
/* the app's own primary button carries the confirm; the decline stays quiet */
.mmc-row .mmc-no{ background:transparent; border:1px solid var(--line); color:var(--dim) }


/* ── ✅ THE CONFIRM SITS ON TOP, CENTRED. Nathan: "that message should not sit at the bottom of the
   screen like it is fitting into the existing framework. It should be plopped right on top."
   It passes taps through everywhere except itself, because the CHORD TILES must stay live — you
   change where the answer goes by tapping a chord, and the question follows. What it does block is
   a second vote: the quality you chose is held until you confirm or cancel. */
/* ⚠️ NO SCRIM. A full-screen wash dims the focused clue too — Nathan: "whichever clue holds the
   focus should not be muted at all." So the overlay paints NOTHING; the dimming is done per element
   by .mm-aim and .mm-holding, which can leave the live tile completely untouched. */
.mm-confirm-ov{
  z-index:440; pointer-events:none; align-items:center; justify-content:center;
  background:transparent;
}
.mm-confirm{
  pointer-events:auto; width:calc(100% - 34px); max-width:330px; text-align:center;
  border:1px solid var(--accent,#2dacb3); border-radius:16px; padding:18px 16px 8px;
  background:var(--panel,#12303a); box-shadow:0 22px 60px rgba(0,0,0,.6);
}
.mmc-q{ font-size:17px; font-weight:700; line-height:1.45; margin-bottom:14px }
.mmc-q small{ display:block; font-size:12.5px; font-weight:400; color:var(--dim); margin-top:7px;
              line-height:1.45 }
.mmc-row{ display:flex; gap:9px }
.mmc-row button{ flex:1; padding:14px 10px; border-radius:11px; font:inherit; font-weight:800;
  font-size:15px; cursor:pointer; border:1px solid var(--line); background:transparent; color:var(--dim) }
.mmc-yes{ background:var(--accent,#2dacb3)!important; border-color:var(--accent,#2dacb3)!important;
  color:#07222a!important }
.mmc-yes[disabled]{ opacity:.35; cursor:default }
.mmc-maj{ color:#FFD79B } .mmc-min{ color:#8E7EF0 }

/* ── 🎯 THE AIM. Everything dims except the chord the answer will land on, so where it is going is
   never in doubt — and it re-aims live as you tap a different chord. */
/* ⚠️ THE OTHER TILES ARE NOT INERT AND MUST NOT LOOK IT. At .3 they read as disabled, so nobody
   would think to tap one — and tapping one is how you move the answer. They stay legible and carry a
   dashed edge, which is the standard "this is a drop target" cue: available, not chosen.
   (Nathan: "it would be better to use some kind of visual clue to let people know those other tiles
   are not inert.") */
.mm-aim .pg-slot{ opacity:.62; transition:opacity .2s ease, box-shadow .2s ease }
.mm-aim .pg-slot:not(.live){
  box-shadow:inset 0 0 0 1.5px rgba(45,172,179,.45); border-radius:12px;
}
.mm-aim .pg-slot.live{ opacity:1 }
/* a held choice cannot be swapped for the other one without cancelling first — that is the "retains
   one vote and blocks me from voting again" half of the rule. */
.mm-holding .mm-big{ opacity:.32; pointer-events:none; transition:opacity .2s ease }


/* the rest of the screen steps back while a choice is held — but never the focused clue, and never
   by painting over it. Each element dims itself, so opacity:1 really is untouched. */
/* ⚠️ THE PLAY BUTTON IS NOT DIMMED. It is the other action the message says you may take — Nathan:
   "don't suppress the play button because it is the other action that you are telling me is
   permitted." Dimming the thing you are being told to press is the opposite of a guard rail. */
.mm-holding #pgLvlBtn, .mm-holding .pg-song,
.mm-holding .kb, .mm-holding .pgwheelwrap{ opacity:.35; transition:opacity .2s ease }
.mm-aim .pg-slot.live{ opacity:1 !important }


/* the chord the message is asking you to tap: lit and breathing, so "tap chord one" has something
   to point at rather than leaving a beginner to guess which box is chord one. */
 /* same fault, same fix: a breathing box-shadow repaints every frame. */
.pg-slot.point{ opacity:1 !important; position:relative; border-radius:12px }
.pg-slot.point::after{
  content:""; position:absolute; inset:0; border-radius:12px; pointer-events:none;
  box-shadow:inset 0 0 0 2px rgba(45,172,179,1); will-change:opacity;
  animation:pointBreathe 1.8s ease-in-out infinite;
}
@keyframes pointBreathe{ 0%,100%{ opacity:1 } 50%{ opacity:.35 } }
@media (prefers-reduced-motion:reduce){ .pg-slot.point{ animation:none } }


/* ── ⚡ THE GUARD-RAIL CUE. One class lights every control the message names, so "you are being
   directed here" always looks the same way. (Nathan: "use that effect to highlight anything where
   I am being directed in the guard rail system.") */

/* 1. a 200ms border flash walking the four tiles, then a rest — it says "any of these", which is
      the truth, where pointing at one tile said something false. */
/* the walk is FAST and then RESTS. Nathan: "speed up that flash sequence and I think either just
   play at once or more intermittently." So the sweep takes 400ms end to end — quick enough to read
   as one gesture across the row rather than four separate blinks — and then the row is still for
   two and a half seconds. Frequency is what makes a cue distracting, not brightness. */
/* ⚠️⚠️ THIS IS WHAT WAS STALLING, AND IT WAS THE ANIMATION ITSELF — NOT A SLOW POPUP.
   The first version animated BOX-SHADOW on four tiles at once. box-shadow cannot be composited, so
   every frame forced a full repaint of the card; the sweep hitched, sometimes froze on one tile, and
   recovered when the thread freed up. Exactly what Nathan described, and why he never saw the amber
   readout — the panel was arriving in 30ms all along.
   OPACITY IS COMPOSITED ON THE GPU. The ring is painted ONCE on a pseudo-element and only its
   opacity moves, so the sweep costs no repaints and cannot stutter behind other work.
   Once, not looping — it is an introduction, not a status. */
.mm-cue .pg-slot{ position:relative }
.mm-cue .pg-slot::after{
  content:""; position:absolute; inset:0; border-radius:12px; pointer-events:none;
  box-shadow:inset 0 0 0 2.5px rgba(45,172,179,.95);   /* static: painted once, never animated */
  opacity:0; will-change:opacity;
  animation:tileWalk var(--cue-beat) linear 1 both;
}
/* ── ⏱ ONE CLOCK FOR EVERYTHING THAT FLASHES. Nathan: "when things are flashing on the screen, use
   the same timer for all of them." The tile walk and the play-button shine share --cue-beat, so they
   are the same length and start together — the row sweeps, the button shines, the screen rests, and
   it repeats as one gesture instead of two rhythms sliding against each other. The same instinct as
   the BEAT metronome in app.js, which exists because a 4s shine against a 4.4s citation meant the
   screen never had a still moment. */
/* one clock for everything that flashes, and now it is one PASS of that clock */
:root{ --cue-beat:1.5s }
.mm-cue .pg-slot:nth-of-type(1){ animation-delay:0s }
.mm-cue .pg-slot:nth-of-type(2){ animation-delay:.1s }
.mm-cue .pg-slot:nth-of-type(3){ animation-delay:.2s }
.mm-cue .pg-slot:nth-of-type(4){ animation-delay:.3s }
@keyframes tileWalk{ 0%{opacity:0} 4%{opacity:1} 16%{opacity:1} 30%,100%{opacity:0} }

/* 2. the play button takes the same travelling shine the next button uses, so the two read as one
      language: this is where you are being sent. */
.mm-cue .pg-transport .pg-playbig, .mm-cue .pg-transport .pg-playbtn{
  position:relative; overflow:hidden;
}
.mm-cue .pg-transport .pg-playbig::after, .mm-cue .pg-transport .pg-playbtn::after{
  content:""; position:absolute; top:-60%; left:0; width:34%; height:220%; pointer-events:none;
  background:linear-gradient(90deg, rgba(255,255,255,0) 0%, rgba(255,255,255,.40) 38%,
             rgba(255,255,255,.85) 50%, rgba(255,255,255,.40) 62%, rgba(255,255,255,0) 100%);
  transform:translateX(-170%) skewX(-20deg);
  animation:shineSwipe var(--cue-beat) ease-in-out 1;
}

/* 3. the word in the sentence that names the thing you tap */
.mmc-hi{ color:var(--accent,#2dacb3); font-weight:800 }
/* and the word "play", which IS the play button */
/* ⚠️ NO TAP FOCUS BOX. iOS leaves a focus ring on a <button> after a tap, which is the "little box
   around the play button text" Nathan kept seeing frozen on screen. Keyboard focus still gets a ring
   via :focus-visible; a finger does not. */
.mmc-playword{
  background:none; border:0; padding:0 2px; font:inherit; font-weight:800; cursor:pointer;
  color:var(--accent,#2dacb3); text-decoration:underline; text-underline-offset:3px;
  -webkit-tap-highlight-color:transparent;
}
.mmc-playword:focus{ outline:none }
.mmc-playword:focus-visible{ outline:2px solid var(--accent,#2dacb3); outline-offset:2px; border-radius:4px }
@media (prefers-reduced-motion:reduce){
  .mm-cue .pg-slot{ animation:none; box-shadow:inset 0 0 0 1.5px rgba(45,172,179,.5) }
  .mm-cue .pg-transport .pg-playbig::after, .mm-cue .pg-transport .pg-playbtn::after{ animation:none; opacity:0 }
}

/* mmtime — TEMPORARY timing readout, removed with the probe */
.mm-time{ font-size:10.5px; color:#FFB454; opacity:.9; padding:6px 0 2px; font-variant-numeric:tabular-nums }

/* the lag log — admin only, dense, and silent until something has been slow */
.setlag{ border:1px solid var(--line); border-radius:10px; padding:8px 10px; background:var(--panel2);
         font-size:12px; max-height:210px; overflow:auto }
.lagrow{ padding:3px 0; border-bottom:1px solid rgba(37,90,102,.4); font-variant-numeric:tabular-nums }
.lagrow:last-of-type{ border-bottom:0 }
.lagrow b{ color:#FFB454 }
.lagt{ color:var(--dim) } .lagat{ color:var(--faint); float:right }

/* the copyable form of the lag log — a real selectable field, not rendered text */
.lagtxt{ width:100%; box-sizing:border-box; margin:8px 0 6px; font:11px ui-monospace,Menlo,monospace;
  background:#0a1a20; color:var(--ink); border:1px solid var(--line); border-radius:8px; padding:7px;
  -webkit-user-select:text; user-select:text; resize:vertical }


/* ── 📣 THE STATUS LINE. One sentence, one next thing.
   ⚠️ NOT A BUTTON. The first version had a border and a filled background, which is exactly what a
   control looks like — Nathan: "you have formatted it in a way that makes it look like it's a button
   when it isn't." A status line is read, not pressed. So there is no box: the state is carried by the
   TEXT — its color and weight — plus a small leading mark, which is how every status bar says
   "waiting on you" without inviting a tap. Fixed height so a longer sentence never pushes the board. */
.mm-ask{
  min-height:40px; display:flex; align-items:center; justify-content:center; gap:0; text-align:center;
  margin:6px 12px 0; padding:0 8px; font-size:15px; font-weight:600; line-height:1.35;
  color:var(--ink); background:none; border:0; box-shadow:none; cursor:default;
  -webkit-user-select:none; user-select:none; transition:color .2s ease;
}
/* the leading mark: a small pulse for "your move", a tick for a clean hand, nothing for a plain result */
.mm-ask::before{
  content:""; flex:0 0 auto; width:7px; height:7px; border-radius:50%; margin-right:9px;
  background:var(--accent,#2dacb3); will-change:opacity;
  /* opacity, NOT a box-shadow ring — box-shadow forces a repaint every frame, which is precisely what
     made the tile sweep stall earlier. This one is composited. */
  animation:askPulse 2s ease-in-out infinite;
}
@keyframes askPulse{ 0%,100%{ opacity:1 } 50%{ opacity:.3 } }
.mm-ask[data-mode="result"]{ color:var(--dim); font-weight:500 }
.mm-ask[data-mode="result"]::before{ display:none }
.mm-ask[data-mode="good"]{ color:#B8F07F }
.mm-ask[data-mode="good"]::before{ animation:none; background:#8FE34A; box-shadow:none }
@media (prefers-reduced-motion:reduce){ .mm-ask::before{ animation:none } }
.mm-ask .mmq-hi{ color:var(--accent,#2dacb3); font-weight:800 }
/* the two answers carry the same colors as the buttons they name */
.mm-ask .mmq-maj{ color:#FFD79B; font-weight:800 }
.mm-ask .mmq-min{ color:#A99BFF; font-weight:800 }


/* ── ❌ WRONG AT THE MOMENT IT WAS VOTED. A red ring on the tile and its label in red, immediately —
   so a wrong answer never looks like the hand simply moved on. Same red the graded hand already uses,
   so the early mark and the final verdict are one language. Ring is a static inset shadow (painted
   once), and nothing about it animates. */
/* a miss is the red word plus the red corner dot — the same dot a corrected chord keeps. The inset
   ring fought the teal "armed" outline on the same tile (Nathan: "incorrect votes look funny at first"). */
.pg-slot.mm-wrongnow{ position:relative }

.pg-slot.mm-wrongnow .mm-said{ color:#FF6B6B !important; font-weight:800 }
.mmc-playword{ display:inline; -webkit-user-select:none; user-select:none; outline:none }
/* the second line under a wedge that carries a diminished chord standing in for its dominant: D7♭9 / (F♯°7) */
.wh-sub{ font-weight:600; font-size:8.5px; text-anchor:middle; dominant-baseline:central; fill:#fff;
  paint-order:stroke; stroke:#000; stroke-width:.6px; pointer-events:none; opacity:.92; }
/* ARMED: a vote will land. The word carries the slot's weight so "the system is armed" reads at a glance. */
.mm-ask[data-mode="armed"] .mmq-armed{ letter-spacing:.06em; text-transform:uppercase; font-size:.82em;
  color:#0b1f24; background:#7fd6db; border-radius:4px; padding:1px 6px; margin-right:2px }
/* the quiet way out, under the question — an option, not an instruction, so it never pulls rank on
   the meter or the two big targets */
.mm-ask .mm-hint{ display:block; margin-top:2px; font-size:10.5px; font-weight:600; letter-spacing:.01em;
  color:rgba(219,238,240,.52); text-transform:none }
/* a vote with nothing armed: the line itself answers */
.mm-ask[data-mode="nudge"]{ color:#f0d478 }
.mm-ask.mm-nudge{ animation:voteNope .5s cubic-bezier(.36,.07,.19,.97) both }
@media (prefers-reduced-motion:reduce){ .mm-ask.mm-nudge{ animation:none } }
/* CORRECTED: the answer is right now, but it was wrong first — a small red corner mark stays, so a
   missing credit at the end is never a mystery. */
.pg-slot.mm-fixed{ position:relative }


/* 🔁 "HEAR IT AGAIN" — present, but silent until you have been sitting on a chord for a beat. It grows
   in rather than blinking on, so it reads as an offer and never as an alarm. */
/* 🔁 the offer lives on the TILE's chip, not in the status line — see .mm-askhear below */

/* 🔁 WAITING ON YOU: the chip has swapped to "hear it again", so the tile itself asks to be pressed.
   A slow breath rather than a blink — it is an offer, not an alarm. */
.chordcard.pgcard .pg-slot.mm-askhear{ animation:mmAskHear 1.8s ease-in-out infinite }
/* the second state is a shift, not an alarm: the same chip, warmed */
/* ⚠️ must out-rank the armed chip's own mint, which is set further down the file at equal weight */
.chordcard.pgcard .pg-slot.mm-armed.mm-askhear .mm-armtag{ background:#E9D9A6; color:#2a1c06 }
@keyframes mmAskHear{
  0%,100%{ box-shadow:0 0 0 0 rgba(255,215,155,0) }
  50%    { box-shadow:0 0 0 4px rgba(255,215,155,.55) }
}
@media (prefers-reduced-motion:reduce){ .chordcard.pgcard .pg-slot.mm-askhear{ animation:none; box-shadow:0 0 0 3px rgba(255,215,155,.5) } }
/* 🟥 a tile holding a wrong answer reads as wrong, not as a confident major or minor — it out-ranks the
   answer color for as long as the slot is still open */
.chordcard.pgcard .pg-slot.mm-open-wrong{
  background:rgba(231,76,60,.20)!important;
  box-shadow:inset 0 0 0 1.5px rgba(231,76,60,.65) }
.chordcard.pgcard .pg-slot.mm-open-wrong .mm-said{ color:rgba(255,214,208,.92) }
/* 🔴 a corrected miss stops looking like an open question: the dot goes, the number keeps the record */
/* ❌ THE MISS IS MARKED ON THE CHORD'S OWN CIRCLE. Ringing the little corner number produced what
   looked like a SECOND number on a tile that already had one. (Nathan: "my incorrect vote places a
   second red number two on a chord that has a number two. There's no reason to reinvent this label.
   Just ring the main chord circle if it's wrong and disqualified.") */
/* ⚠️ THE MISS MARKS THE NUMBER, NOT THE TILE. Ringing the dial drew a big circle around everything,
   out of character with the rest of the card and colliding with the answer underneath. (Nathan: "that
   circle is out of character with anything else we do and it collides with the actual answer. Border the
   circle that contains the clue number in red and maybe reduce the transparency of the entire thing.")
   So: a red border on the numbered circle itself, and the tile sits back. */
.chordcard.pgcard .pg-slot.mm-was-wrong .dial-core{
  box-shadow:0 0 0 2px rgba(231,76,60,.95); border-radius:50% }
.chordcard.pgcard .pg-slot.mm-was-wrong{ opacity:.72 }
/* 👉 THE REDIRECT: the chord in play pulls focus, calmly. Deliberately not red and deliberately not a
   shake — a vote that did not land is not a mistake, and the two must never look alike. */
/* 👉 IT VIBRATES, AND IN THE MESSAGE'S OWN COLOR. A ring alone was too polite to find, and teal said
   nothing about the amber sentence underneath it. (Nathan: "I want to see some vibration on the card
   that is about to get the vote if it hasn't been played yet. And it should reflect that orange color
   in the message.") A loose wobble, not the sharp flinch a wrong ANSWER gets — this is "you have not
   heard this one yet", which is not a mistake. */
.chordcard.pgcard .pg-slot.mm-point{
  animation:mmPointShake 1.5s cubic-bezier(.36,.07,.19,.97) 1 both;
  box-shadow:0 0 0 2.5px rgba(240,212,120,.9), 0 0 16px 2px rgba(240,212,120,.35) }
@keyframes mmPointShake{
  0%,  100%{ transform:translateX(0) }
  6%,  18%,  30%{ transform:translateX(-4px) }
  12%, 24%, 36%{ transform:translateX(4px) }
  44%{ transform:translateX(0) }
}
@media (prefers-reduced-motion:reduce){ .chordcard.pgcard .pg-slot.mm-point{ animation:none } }
.mm-ask .mm-again{ color:#FFD79B; opacity:.95 }
/* ❌ THE MISTAKE SHAKES, so it is felt and not just noticed. A 9px dot in a corner reads as a status
   light; a tile that flinches reads as "that one was wrong". */
.chordcard.pgcard .pg-slot.mm-wrongshake{ animation:mmWrongShake .5s cubic-bezier(.36,.07,.19,.97) both }
@keyframes mmWrongShake{
  10%,90%{ transform:translateX(-2px) } 20%,80%{ transform:translateX(4px) }
  30%,50%,70%{ transform:translateX(-7px) } 40%,60%{ transform:translateX(7px) }
  0%,100%{ transform:translateX(0) }
}
.chordcard.pgcard .pg-slot.mm-wrongshake{ box-shadow:0 0 0 2px rgba(231,76,60,.85), 0 0 14px 2px rgba(231,76,60,.4) }
@media (prefers-reduced-motion:reduce){ .chordcard.pgcard .pg-slot.mm-wrongshake{ animation:none } }
.pg-slot .mm-miss{ position:absolute; top:7px; right:7px; width:9px; height:9px; border-radius:50%;
  background:#FF3B30; box-shadow:0 0 0 2px rgba(11,31,36,.9); pointer-events:none; z-index:3 }
/* ARMED on the tile itself: the word sits where the vote word goes, in the status line's armed color,
   sized to its text so it never covers the tile's corner number */
.pg-slot .mm-armtag{ display:none }
/* an empty chip is no chip — only the nudge has anything to say */
.chordcard.pgcard .pg-slot .mm-armtag:empty{ display:none!important }
.chordcard.pgcard .pg-slot.mm-armed .mm-armtag{ display:block; position:absolute; left:50%; bottom:8px; transform:translateX(-50%);
  white-space:nowrap; padding:1px 6px; border-radius:4px; background:#7fd6db; color:#0b1f24;
  font-size:9px; font-weight:800; letter-spacing:.06em; text-transform:uppercase; line-height:14px;
  pointer-events:none; z-index:3; overflow:hidden }
.chordcard.pgcard .pg-slot.mm-armed .mm-said,
.chordcard.pgcard .pg-slot.mm-cued .mm-said{ visibility:hidden }
/* 🫱 the window closed — the tag stays, goes quiet, and says what to do. Deliberately NOT the armed
   mint and deliberately not animated: it is an instruction, not a countdown. */
.chordcard.pgcard .pg-slot.mm-cued .mm-armtag{ display:block; position:absolute; left:50%; bottom:8px;
  transform:translateX(-50%); white-space:nowrap; padding:1px 6px; border-radius:4px;
  background:rgba(127,214,219,.15); color:rgba(219,238,240,.72); box-shadow:inset 0 0 0 1px rgba(127,214,219,.28);
  font-size:9px; font-weight:800; letter-spacing:.06em; text-transform:uppercase; line-height:14px;
  pointer-events:none; z-index:3; overflow:hidden }

/* the play button's glow runs ~20px below it, and the message bar right under it has a solid background
   (it needs one: it is sticky) — so the glow was sliced flat along the bar's top edge. Room above the bar
   instead of a transparent bar, so the sticky behavior keeps covering what scrolls beneath it. */
body.at-prog .solo-wrap > .gridmsg.pg-msg{ margin-top:18px }
/* the mic's empty slot where hands-free cannot listen: same footprint, nothing to see or press */
.pg-micb.pg-micb-off{ visibility:hidden; pointer-events:none }
/* ── 🖥 iPAD / WIDE: MAJOR OR MINOR TAKES THE WHEEL'S PLACE. Nathan: "on iPad landscape, major minor
   mode is a train wreck." At ≥820px the trainer is two columns and only the wheel (.grid) was placed in
   the right one, so the MAJOR/MINOR board fell into the left column under the hand list — below the fold
   — and the right column held nothing but the transport. The board sits exactly where the wheel will
   (same cell, same square), so nothing moves when the hand grades and the wheel returns.
   And the message bar and the transport were both assigned row 3 of column 2, so the bar's solid
   background covered the play button; the bar gets its own row. */
@media(min-width:820px){
  /* specificity: the generic "everything else goes in column 1" rule is five classes deep */
  body.at-prog .solo-wrap.play>.mm-board.mm-board.mm-board.mm-board{ grid-column:2; grid-row:2; justify-self:center; align-self:start;
    width:min(100%, calc(100dvh - 250px)); max-width:640px; margin:0 }
  body.at-prog .solo-wrap>.gridmsg.pg-msg{ grid-column:2; grid-row:4 }
  /* and the board (wheel OR major/minor) is sized to the HEIGHT too, so the play controls under it stay on
     screen: at 1180×820 a 640px square put the play button 15px below the bottom edge. The wheel host's
     width is inline, hence !important. */
  body.at-prog .solo-wrap>#pgWheelHost,
  body.at-prog .solo-wrap.play>.mm-board.mm-board.mm-board.mm-board{
    width:min(100%, 640px, calc(100dvh - 300px)) !important; justify-self:center }
  body.at-prog .solo-wrap.play>.mm-board.mm-board.mm-board.mm-board{ height:auto; aspect-ratio:1 }
}
/* ── 🖥 DESKTOP WEB: the trainer in its portrait column, framed like the landing panel (see app.js pg-desk).
   Same width and frame as the page you came from, so walking in from the landing does not jump from a
   720px column to a full-width spread. The column inside stays phone-width so the board keeps its shape. */
body.at-prog.pg-desk #app{ max-width:720px; margin:14px auto 0; background:var(--panel); border:1px solid var(--line);
  border-radius:14px; padding:18px 0 22px }
body.at-prog.pg-desk .solo-wrap{display:block!important;max-width:460px;margin:0 auto}
body.at-prog.pg-desk .solo-wrap>*{grid-column:auto!important;grid-row:auto!important;max-height:none!important;
  width:auto!important;justify-self:auto!important;align-self:auto!important}
body.at-prog.pg-desk .solo-wrap>.grid{aspect-ratio:auto!important;width:100%!important}
body.at-prog.pg-desk .solo-wrap>.strip{overflow-y:visible!important}
/* the wheel is square, so on a laptop-height window it pushed the play button below the fold: size it by the
   window's height as well (about 520px of header, card and transport sit around it) */
body.at-prog.pg-desk .solo-wrap>#pgWheelHost{ width:min(100%, max(320px, calc(100dvh - 520px)))!important; margin-left:auto!important; margin-right:auto!important }
body.at-prog.pg-desk .solo-wrap.play>.mm-board.mm-board.mm-board.mm-board{ width:100%!important }
/* ── 👓 THE STACKS HAVE TO BE READABLE. Nathan: "the as-played display is not legible. The Roman numerals
   are colliding with the letters in such a way that you can't read what's under them." Two collisions:
   the pill BEHIND sat only 11px up, so the front pill covered the lower half of its letters; and the front
   pill's numeral badge overlapped the end of its own chord name ("E" under "♭VI"). The back pill now rises
   high enough that its whole name clears the front pill, the badge sits off the pill's corner with room
   reserved for it, and the stacks are spaced so a badge never touches the next stack. */
.pg-stack .pg-back{ left:-12px; top:-17px; opacity:.6 }
.pg-stack .pg-front{ padding-right:12px }
.pg-stack .pg-front .tok-rn{ top:-11px; right:-15px }
.pg-read-strip .pg-stacks, .pg-stacks{ gap:30px }
/* PHONE: four stacks and the frame button share ~230px, so a floating badge and an up-left offset cannot
   both fit — they squeezed together and collided again. On a narrow screen the numeral rides INSIDE the
   pill after the chord name, and the pill behind sits straight above instead of up-left, so no stack
   overhangs its neighbour and they can sit close. The sizer measures the same inline numeral. */
@media (max-width:560px){
  .pg-read-strip .pg-stack{ flex:0 0 auto }
  .pg-stack .pg-front .tok-rn, .pg-stack .pg-sizer .tok-rn{ position:static; display:inline-block; margin-left:3px;
    background:none; border:0; padding:0; min-width:0; height:auto; font-size:9px; opacity:.85; vertical-align:1px }
  .pg-stack .pg-front{ padding-right:revert }
  .pg-stack .pg-back{ left:0; top:-16px }
  .pg-read-strip .pg-stacks, .pg-stacks{ gap:7px; padding-top:19px; padding-left:6px }
  /* one line per pill (the inline numeral was wrapping out of sight) and tighter pills so all four fit */
  .pg-stack .tok-ch{ white-space:nowrap; margin:0; padding:4px 7px }
}
/* ⏳ the quick-entry pill drains for exactly what is left of the window (see mmSyncVote) */
.mm-ask[data-mode="armed"] .mmq-armed.quick{ position:relative; overflow:hidden; isolation:isolate }
/* ⏳ THE DRAIN BAR PAINTS OVER THE PILL'S FILL, NOT UNDER IT. It used to be z-index:-1 inside a parent
   with isolation:isolate AND an opaque mint background — so the bar animated flawlessly and was never
   once visible. (Nathan, three builds running: "I don't see any kind of draining animation." The numbers
   all said it worked; only a screenshot showed it didn't.) It now sits above the fill at z-index:0 with
   the label lifted above it, and drains from the right so the tinted part IS the time you have left. */
.qe-drain{ position:absolute; inset:0; background:rgba(11,31,36,.30); transform-origin:right center;
  animation-name:qeDrain; animation-timing-function:linear; animation-fill-mode:forwards; z-index:0 }
/* ⚠️ URGENCY, NOT ALARM. The WHOLE pill warms over its last third — amber, never red, because nothing is
   lost when it empties (Nathan: "I don't want to add a false sense of urgency and make people think they
   are going to lose"). Warming the draining bar itself was the obvious move and the wrong one: the bar is
   at its thinnest exactly when the warning matters, so the color had nowhere to show. This layer covers
   the pill and carries the same inline duration, so the color tracks the real deadline. */
.qe-warm{ position:absolute; inset:0; z-index:0; background:rgba(240,150,40,0);
  animation-name:qeWarmGlow; animation-timing-function:linear; animation-fill-mode:forwards }
@keyframes qeWarmGlow{ 0%,55%{ background:rgba(245,152,10,0) } 100%{ background:rgba(245,152,10,.94) } }
.qe-lab{ position:relative; z-index:1 }
/* 🪫 THE DARK PART IS THE TIME YOU HAVE SPENT, growing in from the right — so a full window is a
   BRIGHT chip and an empty one is dark. It ran the other way first (dark = time remaining), which made
   the chip darkest at the moment it was most alive, and during playback — a full, un-started window —
   the tag sat fully tinted and read as disabled. Nathan: "the highlighted clue is less obvious and I
   can't put my finger on what changed." That was it. */
@keyframes qeDrain{ from{ transform:scaleX(0) } to{ transform:scaleX(1) } }
@media (prefers-reduced-motion:reduce){ .qe-drain{ animation:none; opacity:.25 } }
/* the song-count superscript at the head of the citation line — pixels, not characters */
.pg-cardsong{ position:relative }
.pg-cardsong .pg-songn{ position:absolute; left:5px; top:1px; font-size:8.5px; font-weight:800; line-height:1;
  color:var(--accent,#2dacb3); opacity:.9; white-space:nowrap; pointer-events:none }
.pg-cardsong .pg-songroll{ padding-left:22px }
/* 👀 THE NOTE MUST NOT COVER THE THING IT IS POINTING AT. Centerd like every other modal, it sat
   squarely over the wheel while telling you to look at where the chords sit — so it rides high and the
   backdrop barely dims, leaving the wheel readable underneath it. */
.modal-ov.gradtip{ align-items:flex-start; background:rgba(4,12,14,.34) }
.modal-ov.gradtip .modal{ margin-top:58px; max-width:330px }
/* 🧭 one step of the tour: the wedge itself, pictured in place, above the name */
.modal-ov.gradstep .modal{ max-width:300px }
.gradcard .gs-pic{ display:flex; justify-content:center; margin:2px 0 8px }
/* the helper renders at 20px with width/height ATTRIBUTES; CSS wins, so the illustration can be big */
.gradcard .gs-pic .pg-reachdial{ width:78px!important; height:78px!important; display:block }
.gradcard .gc-rule{ margin:6px 0 0 }
/* 🎓 the level-up card + the wheel's victory lap */
.gradcard{ text-align:center; max-width:340px }
/* the headline of a promotion is the promotion */
.gradcard .gc-up{ font-size:15px; color:var(--accent2); letter-spacing:.14em; margin-bottom:2px }
.gradcard .gc-kicker{ font-size:11px; font-weight:800; letter-spacing:.1em; text-transform:uppercase; color:var(--dim) }
.gradcard h3{ margin:6px 0 10px; font-size:22px }
.gradcard .gc-sub{ margin:0 0 8px; font-size:12px; color:var(--dim) }
.gradcard .gc-chips{ display:flex; flex-wrap:wrap; gap:8px; justify-content:center; margin-bottom:10px }
.gradcard .gc-chip{ font-size:14px; font-weight:800; padding:5px 10px; border-radius:999px;
  background:rgba(127,214,219,.12); border:1.5px solid rgba(127,214,219,.5); color:var(--ink) }
.gradcard .gc-chip sup{ font-size:9px; margin-left:3px; opacity:.75 }
/* a FUNCTION pill: the wedge's own color, the numeral at full size, no letter anywhere */
.gradcard .gc-chip.gc-fn{ background:color-mix(in srgb, var(--tc) 22%, var(--bg));
  border-color:var(--tc); color:var(--tc); font-size:17px; letter-spacing:.04em;
  box-shadow:0 0 0 1.5px rgba(6,18,22,.7) }
.gradcard .gc-warn{ color:#FFD79B }
.gradcard .gc-hint{ font-size:11.5px; color:var(--dim); margin:0 0 4px }
#pgWheelHost .wh-lap{ filter:brightness(1.45); transition:filter .12s ease }
@media (prefers-reduced-motion:reduce){ #pgWheelHost .wh-lap{ transition:none } }
/* the window has not started yet — the bar sits full while the progression plays */
.qe-drain.qe-full{ animation:none; transform:scaleX(0) }   /* nothing spent yet: the chip is pure mint */
/* 📏 THE CHORD TRAINER FITS THE PHONE. Nathan: "why is my game scrolling when I try and click
   sometimes. There's nothing at the bottom of the screen." There were 101px of scroll under a 390x844
   phone and nothing in it: 60px of #app's bottom padding (trimmed for `body.ingame`, but the trainer
   never sets that class) plus ~45px of genuine overflow, which put the message bar under the fold and
   let a tap turn into a drag. The padding goes, and the two big targets take what is actually left
   instead of a flat 260px — floored at 170px so a small phone keeps a real thumb target rather than
   two slivers. Phone only: the iPad and desktop columns are sized elsewhere and are untouched. */
@media (max-width:819px){
  body.at-prog #app{ padding-bottom:calc(10px + env(safe-area-inset-bottom)) }
  /* the square is driven by WIDTH (aspect-ratio 1/1), so capping the buttons only opened dead space
     inside it — the square itself has to come down. Floored at 260px so the wheel stays readable on a
     small phone, which is the one case that may still scroll a little. The inline max-width in the
     markup is why this needs !important. */
  body.at-prog{ --pg-sq:min(640px, max(260px, 100dvh - 516px)) }
  body.at-prog .mm-board, body.at-prog #pgWheelHost{ max-width:var(--pg-sq)!important }
  /* the gap between the two targets and the play button was never set — it was whatever slack the
     square happened to have left over, and shrinking the square ate it, putting the play button ON the
     cards. The targets now leave that gap deliberately instead of by accident. */
  /* 📐 …AND THEY LEAVE ROOM FOR THE PLAY ROW. Measured: the play button's top sat 1px ABOVE the targets'
     bottom edge, so its glow crowded them. There is no spare height on the screen, so the room comes out
     of the targets — which are still a big thumb. (Nathan: "the play button and those on that row are now
     crowding the voting surface… we can make the voting surface a little less tall to allow for some
     padding.") 144 = the square's own furniture plus the gap this buys. */
  body.at-prog .mm-board .mm-big{ height:min(42vh, 260px, calc(var(--pg-sq) - 156px)) }
  /* 📐 THE TWO TARGETS LINE UP WITH THE HAND ABOVE THEM. The board is a SQUARE because it reserves the
     wheel's footprint — but major/minor has no wheel, so the square only made the buttons narrower than
     the clue card and the screen stopped reading as one block. (Nathan: "the width of the voting surface
     does not align with that of the hand holding the clue cards. This should sit in a visual block on
     mobile.") Only where the board holds the two targets; the wheel's square is untouched. */
  body.at-prog .mm-board:has(.mm-big){ max-width:none!important; aspect-ratio:auto!important; height:auto!important;
    /* …and inset to exactly the clue strip's own padding (5px / 12px), so the two targets share an edge
       with the hand above them rather than floating narrower inside it */
    padding-left:5px!important; padding-right:12px!important;
    /* the room freed by the shorter targets becomes a real gap: the transport sits directly under this
       board, so shrinking the buttons alone just moved the whole stack up and kept the crowding */
    margin-bottom:24px!important }
  /* 🎹 A GRADED WHEEL HAND IS THE TALLEST THE SCREEN EVER GETS: the read strip appears AND the keyboard
     grows, which pushed the keyboard off the bottom with no sane way to scroll to it. (Nathan: "there's
     no way to scroll it unless I put my finger and scroll in the voting area which is awkward. There's
     plenty of room to move the wheel up.") So the wheel tucks up under the read strip — which already
     paints above it — and gives back a little size. The mask fades the wheel's top edge into the strip
     so the overlap reads as a tuck rather than a collision. */
  body.at-prog:has(.pg-read-strip:not(.idle)){ --pg-sq:min(640px, max(240px, 100dvh - 580px)) }
  body.at-prog:has(.pg-read-strip:not(.idle)) #pgWheelHost{
    margin-top:-60px!important;   /* a later rule zeroes this row's margin; the tuck has to out-rank it */
    -webkit-mask-image:linear-gradient(to bottom, transparent 0, #000 26px, #000 100%);
            mask-image:linear-gradient(to bottom, transparent 0, #000 26px, #000 100%);
  }
}
/* 🔊 WHILE A PASS IS PLAYING, THE TILE THAT IS SOUNDING IS THE LOUD ONE. The armed ring is a 2px
   outline and a fill; "sounding" only tinted the little dial — so with chord 2 playing and chord 1
   armed, the brightest thing on the card was the tile that was NOT making the sound, and the eye went
   to the wrong place. The armed tile keeps its tag and steps back to a hairline until the pass ends. */
body.pg-sounding .chordcard.pgcard .pg-slot.sounding{ outline:2px solid var(--accent2); outline-offset:-2px;
  background:rgba(45,172,179,.22) }
/* ⚠️ NOT A SECOND-CLASS EDGE. I dimmed the armed tile so the sounding one could win, and it ended up
   looking like the only tile that does not get a proper border. (Nathan: "the first tile never gets the
   benefit of the bright edge that the others do… the highlight reads as a second class citizen.") Both
   get a full-strength edge; they are told apart by KIND, not by brightness — the sounding tile is filled
   and glows, the armed tile is a crisp outline with its chip. */
body.pg-sounding .chordcard.pgcard .pg-slot.cur:not(.sounding){ outline-color:var(--accent2);
  background:rgba(45,172,179,.14) }
body.pg-sounding .chordcard.pgcard .pg-slot.cur:not(.sounding)::before{ opacity:.8 }
/* 👁 the blink that hands the next chord its tile on a roll */
.chordcard.pgcard .pg-slot.mm-blink{ animation:mmBlink .34s ease-out 1 }
@keyframes mmBlink{ 0%{ box-shadow:0 0 0 0 rgba(127,214,219,.85) } 100%{ box-shadow:0 0 0 7px rgba(127,214,219,0) } }
@media (prefers-reduced-motion:reduce){ .chordcard.pgcard .pg-slot.mm-blink{ animation:none } }
/* the armed tile breathes once the play-through is over */
/* ⛔ NO SECOND RING WHILE ARMED. The tile already carries its outline; a pulse on top of it was a third
   concentric shape. (Nathan: "fix the bad border first so it looks clean. Then the highlight alone tells
   the story.") The breathing is kept only for the later "hear it again" nudge, which has a job to do. */
.chordcard.pgcard .pg-slot.mm-armed-live{ animation:none }
@keyframes qeTilePulse{ 0%,100%{ box-shadow:0 0 0 0 rgba(127,214,219,0) } 50%{ box-shadow:0 0 0 3px rgba(127,214,219,.55) } }
@media (prefers-reduced-motion:reduce){ .chordcard.pgcard .pg-slot.mm-armed-live{ animation:none; box-shadow:0 0 0 3px rgba(127,214,219,.45) } }


/* ── 🏠 THE WAY OUT, STATED ────────────────────────────────────────────────────────────────────
   The logo is the back button, and it sat unlabelled right beside the word "progress" — which then
   read as its caption. Two changes, both small: the logo carries its own word, and a real divider
   separates it from the stats so nothing beside it can be mistaken for its label. */
.bb-home{ display:flex; align-items:center; gap:5px }
.bb-home .bb-home-lab{
  font-size:10px; font-weight:800; letter-spacing:.06em; text-transform:uppercase;
  color:var(--dim); line-height:1;
}
.bb-home:hover .bb-home-lab, .bb-home:focus-visible .bb-home-lab{ color:var(--ink) }
/* the divider: the stats are a separate thought from the exit */
.bb-side.bb-left{ position:relative; padding-left:11px; margin-left:9px }
.bb-side.bb-left::before{
  content:''; position:absolute; left:0; top:50%; transform:translateY(-50%);
  width:1px; height:17px; background:var(--line); opacity:.85;
}
@media (max-width:400px){
  .bb-home .bb-home-lab{ font-size:9px; letter-spacing:.04em }
  .bb-side.bb-left{ padding-left:8px; margin-left:6px }
}


/* ── 🏹 THE PULL PREDICTOR ─────────────────────────────────────────────────────────────────────
   Arrows from a wedge toward everywhere the songbook actually goes next, length = strength. */
#pgWheelHost{ position:relative }

/* ── 🎯 THE HUB IS THE SWITCH, AND ITS COLOR IS THE STATE.
   No icon, no label: the hub is already what you tap and already where the arrows come from.
   (Nathan: "let the tap change the color of the hub slightly to indicate on or off.") A tint, not a
   repaint — it has to read as the same hub in two states, not as two different objects. */
#pgWheelHost .wh-svg .wh-hub, #pgWheelHost .wh-svg .wh-hubname{ cursor:pointer }
#pgWheelHost .wh-svg .wh-hub{ transition:fill .22s ease }
#pgWheelHost.pull-on .wh-svg .wh-hub{ fill:#e4f6ea }
@media (prefers-reduced-motion:reduce){ #pgWheelHost .wh-svg .wh-hub{ transition:none } }
.wh-pull{ pointer-events:none }
/* the progression's own line steps aside while the arrows are up — two sets of thin strokes over the
   same ring cannot both be read (Nathan). */
#pgWheelHost.pull-hidepath .wh-shape,
#pgWheelHost.pull-hidepath .wh-shape-back{ opacity:0; transition:opacity .12s ease }
/* ── ✏️ SHARPEN THE PATH ARROWS. Nathan: "they are hard to read against the wheel… it might be the color
   combination and font. It's always going to litter the interface a little but I wonder if it could be
   made a bit sharper." The strokes are green over yellow, teal, olive and maroon wedges, so on roughly half
   the ring they sit at almost no contrast. A dark casing under every stroke and arrowhead — the same trick
   that keeps map labels legible over any terrain — makes them read the same on every wedge, and the greens
   are lifted so the line reads as deliberate rather than muddy. */
.wh-pull{ filter:drop-shadow(0 0 2.2px rgba(4,10,11,.95)) drop-shadow(0 0 1px rgba(4,10,11,.9)) }
/* ⚠️ YELLOW, WITH AN OUTLINE. These were green on a wheel whose wedges are already green, teal and
   red — Nathan: "they are hard to read at this color. Let's try yellow with a border." Yellow is the
   one hue the ring does not use for a wedge body, and the dark outline keeps it readable over the
   pale wedges as well as the dark ones. paint-order puts the outline behind the fill so the stroke
   never eats the line's weight. */
.wh-pull-p{ fill:none; stroke:#FFD34D; stroke-linecap:round;
            paint-order:stroke fill; }
.wh-pull-p.strong{ stroke:#FFE27A }
.wh-pull-p.faint{ stroke:#C9A63A }
/* the head carries DIRECTION, which is binary — it never fades with the pull. The dark outline is
   what lets it read when the tip lands on a lit wedge rather than on the dark ring. */
/* ⚠️ NO HEAVY OUTLINE ON THE HEAD. A 1.5px dark border turned each head into its own little object,
   and with ten arrows leaving one point they read as triangles scattered over unrelated lines.
   A hairline is enough to hold it against a bright wedge. */
.wh-pull-h{ fill:#FFD34D; stroke:#1b1405; stroke-width:.6; paint-order:stroke fill }
.wh-pull-h.strong{ fill:#FFE27A }
.wh-pull-h.faint{ fill:#C9A63A }
/* 🎯 WHERE THE ARROWS COME FROM. They radiated from a wedge that was not marked in any way, so on a
   fresh hand — where the predictor falls back to the tonic — they appeared to come from nowhere.
   (Nathan: "you should always know where those arrows are coming from… while someone is holding down
   the background, the highlight should appear on the tonic wedge.") Only while the arrows are up. */
.wh-svg .wh-w.wh-pull-src{ stroke:#FFD34D !important; stroke-width:3 !important;
                           filter:drop-shadow(0 0 5px rgba(255,211,77,.55)) }
/* the numbers were 7.5px mono behind a see-through casing — small enough that the casing blurred the
   glyphs instead of separating them. Bigger, heavier, and cased in solid ink. */
.wh-pull-t{
  fill:#8affbe; font:700 9px/1 "IBM Plex Mono",ui-monospace,monospace;
  text-anchor:middle; paint-order:stroke; stroke:#040a0b; stroke-width:3;
  stroke-linejoin:round;
}
.wh-pull-t.strong{ font-size:11.5px; font-weight:800; fill:#a8ffd0 }
.wh-pull-t.faint{ fill:#9ab5aa; font-size:8px; stroke-width:2.4 }
@media (prefers-reduced-motion:reduce){ .wh-pull{ transition:none } }


/* ── 🕑 RECENT: the board as a feed. No rank column — the grid is who / when / how much. */
.lbrow.lbrecent{ grid-template-columns:14px 1fr auto auto; gap:10px; align-items:center }
.lbrow.lbrecent .lbwhen{ font-size:12px; color:var(--dim); white-space:nowrap; font-variant-numeric:tabular-nums }
.lbrow.lbrecent .lbscore{ font-weight:800; color:#FFB454; font-variant-numeric:tabular-nums; min-width:58px; text-align:right }
@media (max-width:400px){
  .lbrow.lbrecent{ gap:7px }
  .lbrow.lbrecent .lbwhen{ font-size:11px }
  .lbrow.lbrecent .lbscore{ min-width:48px }
}




/* ✅ THE TILE SAYS YES. A correct vote had no visual of its own — the wedge lights on every pick, and
   the chord plays whenever you tap one — so right and wrong looked the same until the hand graded.
   Deliberately quiet: a green edge and one soft pulse, not a firework. The game is a listening test,
   and the answer should feel confirmed rather than celebrated. */
.pg-slot.mm-rightnow{
  border-color:rgba(143,227,74,.85) !important;
  box-shadow:0 0 0 1px rgba(143,227,74,.35), 0 0 14px -4px rgba(143,227,74,.55);
  animation:slotYes .42s ease-out 1;
}
.pg-slot.mm-rightnow .pg-cnum{ color:#B8F07F !important }
@keyframes slotYes{
  0%   { transform:scale(1);    box-shadow:0 0 0 0 rgba(143,227,74,.55) }
  45%  { transform:scale(1.045); box-shadow:0 0 0 5px rgba(143,227,74,0) }
  100% { transform:scale(1) }
}
@media (prefers-reduced-motion:reduce){ .pg-slot.mm-rightnow{ animation:none } }


/* 🎯 the dial as a VOTE RECEIPT, before the hand is graded: the wedge you chose, ringed, nothing
   filled. Slightly dimmer than the graded dial so the two read as different moments — this one says
   "recorded", the graded one says "here is the answer". */
.pg-slot .dial-vote{ opacity:.9 }
.pg-slot.mm-rightnow .dial-vote{ opacity:1 }


/* 🔁 A CORRECTED SLOT. The answer in it is right and the strike already happened — both are true, and
   the card said neither. An amber corner mark keeps the record of the miss without contradicting the
   green that says this answer is now correct. Reads as "fixed", not as "wrong". */
.pg-slot.pg-fixed{ position:relative }
.pg-slot.pg-fixed .pg-cnum{ color:#FFB454 !important }


/* ✅/✗ the corner verdict, shown from the moment you vote rather than only at grading. Quieter than
   the graded mark — this one is a running commentary, that one is the final word. */
/* SAME WEIGHT, LIVE OR GRADED. At 11px and 85% a live miss was quieter than the mark you got for
   FIXING it, so correcting a mistake looked worse than making one. (Nathan: "it looks like what I did
   was worse than it was when it was wrong.") */
.pg-mark.live{ opacity:1; font-size:13px }
.pg-mark.live.yes{ color:#8FE34A }
.pg-mark.live.no{ color:#FF6B6B }


/* ══ 🪭 THE FAN — the old fan's look, one arc, three sections (quality · 4th note · color) ══════
   Copied from .wh-scrim / .wh-fanq / .wh-qpill / .wh-qpilll so the two cannot drift apart on look.
   Its own class names because the wheel's document-level handlers treat .wh-fanq as the OLD fan. */
.fan-scrim{ position:fixed; inset:0; z-index:60; background:transparent }
.fan-svg{ overflow:visible }
.fan2-scrim{ fill:rgba(6,20,26,.62); pointer-events:none }
.fan2-cell{ cursor:pointer }
.fan2-cell .fan2-slice{ stroke:#fff; stroke-width:1.2 }
.fan2-back{ fill:#0a171c; stroke:none }
.fan2-pill{ stroke:rgba(0,0,0,.30); stroke-width:.5; pointer-events:none }
.fan2-pilll{ text-anchor:middle; dominant-baseline:central; font-weight:800; letter-spacing:.2px; pointer-events:none }
.fan2-cell .wh-fn{ pointer-events:none }
/* chosen: a heavy white edge and a lit pill — the radio/checkbox state, readable at a glance */
.fan2-cell.on .fan2-slice{ stroke-width:3.2 }
.fan2-cell.on .fan2-pill{ stroke:#fff; stroke-width:1.6 }

.fan-hub circle{ fill:#fff; stroke:#0b161b; stroke-width:1.5 }
.fan-hubnm{ fill:#4f6670; font-weight:900; font-size:15px; pointer-events:none }
.fan-hubgo{ fill:#04232a; font-weight:800; font-size:9px; letter-spacing:.08em; text-transform:uppercase; pointer-events:none }
.fan-hub.ready{ cursor:pointer } .fan-hub.ready circle{ fill:#2dacb3; animation:fanPulse 1.5s ease-in-out infinite }
.fan-hub.ready .fan-hubnm{ fill:#04232a }
@keyframes fanPulse{ 0%,100%{ stroke-width:1.5; stroke:#0b161b } 50%{ stroke-width:5; stroke:rgba(45,172,179,.55) } }
@media (prefers-reduced-motion:reduce){ .fan-hub.ready circle{ animation:none } }
/* ⌨️ WHILE BUILDING, THE KEYBOARD SITS WHERE THE PLAY BUTTON IS. The two are never used at the same
   time, and appended below the transport the keyboard ran off the bottom of the phone. (Nathan: "the
   keyboard can occupy the space used by the play button… it is clipping the viewport on my phone.")
   The transport is only made invisible, never removed, so the row keeps its height and nothing moves. */
#pgKeys.kb-build{ position:relative }
#pgKeys.kb-build > .pg-transport{ visibility:hidden }
#pgKeys.kb-build > .pg-kb{ position:absolute; left:0; right:0; top:50%; transform:translateY(-50%) }

/* 🎨 the color bonus on the reveal — bottom-right, clear of the slot number at bottom-left */
.pg-bonus{ position:absolute; right:5px; bottom:4px; font-size:9.5px; font-weight:800; letter-spacing:.02em;
           color:#1d1606; background:#E2B84A; border-radius:999px; padding:1px 6px; line-height:1.35; pointer-events:none }
.pg-bonus.neg{ color:#ffd9d9; background:rgba(255,107,107,.28) }
/* a color that sounded and went unnamed: the same pill, hollow */
.pg-bonus.miss{ color:#E2B84A; background:transparent; border:1px dashed rgba(226,184,74,.8); padding:0 5px }

/* 🎸 the bass switch MIRRORS the 3× button across play: same size, same height, the same 12px gap on
   the other side. (Nathan: "it's too big and too far left… make it mirror the position of the 3× button.")
   Positioned from the center, out of the grid, so nothing else moves. Play is 76px wide. */
.pg-transport{ position:relative }
.pg-bassb{ position:absolute; right:calc(50% + 38px + 12px); top:50%; transform:translateY(-50%); z-index:1;
           width:36px; height:35px; padding:0; border-radius:999px; border:1px solid var(--line,#24404e);
           background:transparent; color:var(--dim,#8aa6b0); cursor:pointer;
           display:flex; align-items:center; justify-content:center }
.pg-bassclef{ width:20px; height:20px; fill:currentColor; stroke:currentColor }
.pg-bassb.on{ color:#eaf6f8; border-color:#2dacb3; background:rgba(45,172,179,.18) }
@media (max-width:410px){ .pg-bassb{ right:calc(50% + 38px + 8px) } }
.pg-bassb.on{ color:#eaf6f8; border-color:#2dacb3; background:rgba(45,172,179,.18) }

/* 🏆 the front board's two headings — plainly buttons, so "Recent" is findable */
.landboard-tabs{ display:flex; align-items:center; gap:6px; flex-wrap:wrap }
.landboard-tabs .lbt{ font:inherit; font-weight:800; font-size:13px; padding:5px 11px; border-radius:999px; cursor:pointer;
  color:var(--dim,#8aa6b0); background:transparent; border:1px solid var(--line,#24404e) }
.landboard-tabs .lbt.on{ color:var(--ink,#eaf6f8); background:rgba(45,172,179,.18); border-color:#2dacb3 }
.landboard-tabs .mylevel{ margin-left:auto }

/* the drill-in, kept apart from the two toggles so it cannot be mistaken for one */
.landboard-tabs .lbsee{ margin-left:auto; font:inherit; font-size:12.5px; font-weight:700; color:#2dacb3; background:none; border:0; cursor:pointer; padding:5px 2px }
.landboard .lbsub{ font-size:12px; margin:4px 0 6px }

/* three toggles on ONE line at phone width; the drill-in lives at the foot of the box */
.landboard-tabs{ flex-wrap:nowrap; gap:5px }
.landboard-tabs .lbt{ font-size:12px; padding:5px 9px; white-space:nowrap }
.landboard .lbsee-foot{ display:block; margin:10px 0 0 auto; font:inherit; font-size:12.5px; font-weight:700; color:#2dacb3; background:none; border:0; cursor:pointer }

/* not chosen: the slice fades toward its dark backing, the label a little less; colors are never altered */
.fan2-cell.off .fan2-slice{ opacity:.38 }
.fan2-cell.off .fan2-lab{ opacity:.62 }

/* one pill per chord; the other reading small underneath (green when it is what sounded) */
.pg-stack.pg-stack2{ display:inline-flex; flex-direction:column; align-items:center; gap:2px; vertical-align:top }
.pg-was{ font-size:10.5px; font-weight:700; line-height:1.1; color:var(--dim,#8aa6b0); white-space:nowrap }
.pg-was.snd{ color:#8FE34A }
/* the numeral rides small and high off the letter name, the way the pill in front already sets it —
   at equal size "A" + "V" read as the word "AV" (Nathan, 2026-09-26) */
.pg-was .pg-was-rn{ font-style:normal; font-size:.72em; font-weight:700; opacity:.72;
  vertical-align:.42em; margin-left:.18em; letter-spacing:.01em }
.pg-was .pg-was-lab{ font-style:normal; font-weight:600; opacity:.6 }
/* ── 🎯 "a wedge that would have been accepted", offset off the dial so it reads without competing with
   the wedge label itself. Small white letters, exactly as Nathan specified. It is ABSOLUTELY positioned
   so it cannot change the tile's height — the card must be the same size graded and ungraded. */
.bul.dial{ position:relative }
/* ⚠️ IT HUNG BELOW THE TILE AND THE STRIP CUT IT IN HALF. The card is overflow:visible, but the strip
   it sits in scrolls horizontally — and a horizontally scrolling box clips vertically too. So this label
   was sliced off under the last card and could not be read at all. (Nathan: "There's also a label written
   underneath the last tile that I cannot read.") It now sits INSIDE the dial's own box, just under the
   numbered circle, where nothing can clip it and it still reads as belonging to that wedge. */
.dial-would{
  position:absolute; left:50%; transform:translateX(-50%);
  bottom:1px; white-space:nowrap; pointer-events:none;
  font-size:9.5px; font-weight:700; letter-spacing:.02em;
  color:rgba(255,255,255,.78); text-shadow:0 1px 2px rgba(0,0,0,.65);
}
@media (max-width:380px){ .dial-would{ font-size:9px; bottom:0 } }

/* corrected: the ✗ it earned first, with the ✓ it ended on laid over it — right now, but it broke the run */
/* ── ⚠️ CORRECTED IS A FOOTNOTE, NOT AN ERROR. This used to superimpose the ✓ on the ✗ — Nathan's own
   earlier idea, and it read as the loudest mark on the card: two glyphs, 19px, on the one tile you had
   already put right. He replaced it himself: "instead of an X, I think a yellow warning triangle would
   message what happened… we want this to look more like a footnote than a straight up error. It will
   already have no green check so it looks different." So: one amber triangle, the same size as every
   other verdict, and the absent ✓ carries the rest of the meaning. */
/* corrected = right, just not first time. A quieter gold than the alarm amber it wore. */
.pg-mark.fixed{ width:auto; height:auto; color:#C9A63A }
/* the whole corrected card says it too: amber, not green — right now, but it broke the run */
.pg-slot.pg-fixed{ border-color:#FFB454 !important; box-shadow:0 0 0 1px rgba(255,180,84,.35) }

/* the reserved message line draws no panel while it has nothing to say */
.gridmsg.pg-msg.blank{ background:transparent !important; box-shadow:none !important; border-color:transparent !important }
/* wheel controls: no square focus box on a mouse click; a round, wheel-shaped ring for keyboard focus */
#pgWheelHost svg :focus, #cbOv svg :focus{ outline:none }
#pgWheelHost svg .wh-hub:focus-visible{ stroke:#7fd6db; stroke-width:3 }
#pgWheelHost svg .wh-w:focus-visible, #cbOv .fan2-cell:focus-visible .fan2-slice{ stroke:#fff; stroke-width:3 }

/* 🔎 song search — the button sits beside the star in the card foot */
.pg-srch{ flex:0 0 auto; order:1; width:28px; height:28px; padding:3px; border:0; background:transparent; color:#7fd6db; opacity:.75; cursor:pointer }
.pg-srch:hover, .pg-srch:focus-visible{ opacity:1 }
.pg-srch svg{ width:100%; height:100% }
.ss-ov .ss-card{ width:min(560px, calc(100vw - 32px)); max-height:min(80vh, 720px); display:flex; flex-direction:column; padding:14px }
.ss-head{ display:flex; gap:8px; align-items:center }
.ss-in{ flex:1; min-width:0; font-size:16px; padding:10px 12px; border-radius:10px; border:1px solid rgba(127,214,219,.45); background:#0b1f24; color:#e8f4f4 }
.ss-x{ flex:0 0 auto; width:36px; height:36px; border:0; border-radius:10px; background:rgba(255,255,255,.08); color:#e8f4f4; cursor:pointer }
.ss-sum{ font-size:12.5px; color:#9fc4c6; margin:8px 2px 6px; min-height:1.2em }
.ss-list{ overflow-y:auto; display:flex; flex-direction:column; gap:6px }
.ss-row{ text-align:left; display:flex; flex-direction:column; gap:2px; padding:9px 11px; border-radius:10px; border:1px solid rgba(255,255,255,.08); background:rgba(255,255,255,.04); color:#e8f4f4; cursor:pointer }
.ss-row:hover, .ss-row:focus-visible{ background:rgba(127,214,219,.12) }
.ss-row.other{ opacity:.78 }
.ss-t{ font-weight:700; font-size:14px }
.ss-a{ font-size:12px; color:#9fc4c6 }
.ss-chs{ display:flex; flex-wrap:wrap; gap:6px; margin-top:3px }
.ss-ch{ font-size:12.5px; font-weight:700; background:#0b1f24; border-radius:999px; padding:1px 8px }
.ss-ch sup{ font-size:9px; margin-left:3px; color:#9fc4c6 }
.ss-bks{ font-size:10px; font-weight:800; color:#1d1606; background:#E2B84A; border-radius:999px; padding:0 6px; margin-left:6px; vertical-align:middle }
.ss-more{ font-size:12px; color:#9fc4c6; padding:6px 2px }

/* the always-there songbook search, top right of the level line */
.pg-hostbar{ position:relative }
.pg-hostbar .hb-srch{ position:absolute; top:10px; right:12px; width:30px; height:30px; z-index:3 }

/* ── 📱 iPAD LANDSCAPE IS ONE HAND, NOT SIX. The wide layout still listed "Hand 2 … Hand 6" down the left
   column — the six-clue, swipe-between-clues model borrowed from bingo, which this game no longer is.
   (Nathan: "This is no longer a six clue model with a swipe to move in between clues.") The phone shows the
   hand you are on; so does the iPad. The status line moves above the wheel instead of sitting on the play
   button, and the wheel is sized so the keyboard under it stays on screen. */
@media(min-width:820px){
  body.at-prog:not(.pg-desk) .solo-wrap .pgcard.pg-mini{ display:none !important }
  /* above the wheel, in the cell the as-played strip uses after a hand; the line is empty then, so they never meet */
  body.at-prog:not(.pg-desk) .solo-wrap>.mm-ask{ grid-column:2 !important; grid-row:1 !important; justify-self:center; align-self:end; margin:0 0 6px !important }
  body.at-prog:not(.pg-desk) .solo-wrap>#pgWheelHost{ width:min(100%, 640px, calc(100dvh - 560px)) !important }
}

/* iPad upright: the single column, given the room the tablet has */
body.at-prog.pg-desk.pg-tall #app{ max-width:780px }
body.at-prog.pg-desk.pg-tall .solo-wrap{ max-width:640px }
body.at-prog.pg-desk.pg-tall .solo-wrap>#pgWheelHost{ width:min(100%, max(320px, calc(100dvh - 660px)))!important }

/* ── 📱 iPAD LANDSCAPE, POLISHED. (Nathan, on the major/minor screen: "match the layout and polish so it doesn't
   look worse than everything else.") The right column reserved the wheel's full 624px square, so the capped
   major/minor buttons floated in the middle of a void with the question above it and the play button far below.
   The square now fits the screen height (the controls still never move when the wheel replaces the board), the
   hand card's glow is no longer clipped into a hard-edged box, and the two columns start on the same line. */
@media(min-width:820px){
  body.at-prog:not(.pg-desk){ --pg-sq:min(560px, max(300px, 100dvh - 440px)) }
  body.at-prog:not(.pg-desk) .solo-wrap>#pgWheelHost{ width:min(100%, var(--pg-sq)) !important }
  body.at-prog:not(.pg-desk) .solo-wrap.play>.mm-board.mm-board.mm-board.mm-board{ width:min(100%, var(--pg-sq)) !important; max-width:var(--pg-sq) !important }
  body.at-prog:not(.pg-desk) .solo-wrap>.strip{ overflow:visible !important; max-height:none !important; background:transparent !important }
  /* the whole two-column block sits in the middle of the screen, not stuck to the top with a void below */
  body.at-prog:not(.pg-desk) .solo-wrap{ align-content:center; min-height:calc(100dvh - 70px) }
}

/* ══ 📐 iPAD LANDSCAPE — THE BLOCK LAYOUT (Nathan, 2026-09-22) ══════════════════════════════════════════════
   "Keep things from shifting around… there's a lot more space for messaging… the pieces in landscape are
   still out of balance." Every region has a fixed home and a fixed size, so nothing moves when the play
   button, the keyboard, next/skip or a message appears:
       ┌──────────── level + progress, full width ────────────┐
       │ the hand            │ message line (always reserved) │
       │                     │ WHEEL  or  MAJOR | MINOR       │
       │ songbook panel      │                                │
       │ keyboard            │ bass · ▶ · loop · next/skip    │
       │ streak              │ notes                          │
       └─────────────────────┴────────────────────────────────┘
   Landscape iPad only: phones and upright tablets keep their single column. */
/* the wheel says it has armed for turning: the ring lifts and the home label lights */
#pgWheelHost.wh-armed .wh-svg{ filter:drop-shadow(0 0 12px rgba(127,214,219,.45)); cursor:grabbing }
#pgWheelHost.wh-armed .wh-turnlab{ background:rgba(127,214,219,.3); border-color:rgba(127,214,219,.85) }
#pgWheelHost.wh-snap .wh-turnlab{ background:rgba(127,214,219,.5); color:#03282c; font-weight:800 }
/* ── 📅 THE EVENT TICKER. On the iPad it simply lives at the foot of the board: always there, quietly
   rotating, its height fixed by the rotator's own card so the board above it never moves. On a phone it has
   nowhere to stand, so it rises over the bottom of the screen at a moment that already feels like progress
   and withdraws by itself. Never during a hand — see promoFlash(). */
/* ── ✕ GET IT OUT OF THE WAY. One press clears the panel for the hand you are on; the next hand brings it
   back. No preference, nothing remembered — it is not a setting, it is a "not right now". */
.pg-promo{ display:none }
.pg-promo.pg-promo-off{ display:none !important }
.pg-promo-in{ min-width:0; overflow:hidden }
.pgp-x{
  position:absolute; top:26px; right:10px; z-index:3;   /* on the card itself, not floating above its heading */
  width:24px; height:24px; line-height:1; padding:0;
  font:700 16px/1 system-ui,-apple-system,sans-serif; color:#9fc4c6;
  background:rgba(4,10,11,.55); border:1px solid rgba(127,214,219,.28); border-radius:50%;
  cursor:pointer; opacity:.7;
}
.pgp-x:hover, .pgp-x:focus-visible{ opacity:1; color:#e8f4f4; background:rgba(4,10,11,.8) }
@media (min-width:820px) and (orientation:landscape){
  body.at-prog:not(.pg-desk) .solo-wrap>.pg-promo{
    position:relative;
    grid-area:songs !important; align-self:end !important; justify-self:stretch !important;
    display:block; min-width:0; overflow:hidden; max-height:150px; z-index:2;
    margin:0 12px 0 5px; opacity:.94;
  }
  /* ✂️ ONE CARD, WHOLE. The rotator draws a pinned NEXT UP/LIVE NOW card AND a rotating slot beneath it;
     in this column only the pinned one fits, so the rotating card was being sliced off — the "strange pink
     block below it, like part of something else trying to draw and getting clipped" (Nathan). On the iPad
     the pinned card IS the always-visible thing he asked for, so the rotating slot stands down here.
     The phone keeps the rotation — see the portrait rules below. */
  body.at-prog:not(.pg-desk) .solo-wrap>.pg-promo .pg-promo-in{ max-height:150px; overflow:hidden }
  /* ── 📐 LINE IT UP WITH SONGBOOK. Measured: the Songbook title started at 179 and this heading at 164,
     so the two section titles were 15px out of step in the same column. The panel's own padding is what
     insets Songbook; this block sits outside it, so it takes the same inset explicitly. The dots were
     flush to the right edge with ZERO inset — that is the crowding. (Nathan: "align upcoming and featured
     vertically with songbook and don't crowd the three dots.") */
  body.at-prog:not(.pg-desk) .solo-wrap>.pg-promo{ padding-left:15px; padding-right:13px }
  body.at-prog:not(.pg-desk) .solo-wrap>.pg-promo .ib-events-h{
    font-size:12px; font-weight:800; letter-spacing:.09em; text-transform:uppercase;
    color:#9fc4c6; margin-bottom:7px }
  body.at-prog:not(.pg-desk) .solo-wrap>.pg-promo .pr-dots{ margin-right:2px; gap:6px }
  body.at-prog:not(.pg-desk) .solo-wrap>.pg-promo .pr-h{ gap:12px }
  /* ⚠️ NOT THE EVENT THE HEADER ALREADY SHOWS. I kept the pinned NEXT UP card here — but that was decided
     before the countdown bar existed in the menu row. With the header owning "what is next and what is
     live", the pinned card printed the same event twice on one screen. (Nathan: "right away I asked you to
     send the upcoming event in the header. So this is already wrong.") The pinned card stands down; the
     rotation runs here instead, carrying the featured classes this space was meant to hold. */
  body.at-prog:not(.pg-desk) .solo-wrap>.pg-promo .pr-pinned{ display:none !important }
  body.at-prog:not(.pg-desk) .solo-wrap>.pg-promo .pr-slot{ display:block !important; margin:0 !important }
  body.at-prog:not(.pg-desk) .solo-wrap>.pg-promo .pr-pinned{ margin:0 !important }
  /* …and the library stops short of it, so the list never runs underneath */
  body.at-prog:not(.pg-desk) .solo-wrap>.pg-sbook{ padding-bottom:156px !important }
}
@media (max-width:819px), (orientation:portrait){
  body.pg-promo-up .pg-promo{
    display:block; position:fixed; left:0; right:0; bottom:0; z-index:58;
    padding:8px 10px calc(8px + env(safe-area-inset-bottom, 0px));
    background:linear-gradient(180deg, rgba(10,26,29,.86), #0b1f24 34%);
    border-top:1px solid rgba(127,214,219,.3);
    animation:pgsUp .22s ease-out;
  }
}
.pg-sbook{ display:none }

/* content styles for the library list — these apply on EVERY screen. They used to sit inside the
   landscape-only block, so the phone sheet rendered an unstyled list with the title and artist run
   together. Layout (grid-area, widths) stays landscape-only below. */
.pg-sbook .pgs-head{ display:flex; gap:6px; align-items:center }
.pg-sbook .pgs-in{ width:100%; box-sizing:border-box; font-size:15px; padding:8px 11px; border-radius:10px;
    border:1px solid rgba(127,214,219,.4); background:#0b1f24; color:#e8f4f4 }
.pg-sbook .pgs-sum{ font-size:12px; color:#9fc4c6; margin:8px 2px 6px; min-height:1.2em }
.pg-sbook .pgs-list{ overflow-y:auto; display:flex; flex-direction:column; gap:5px; min-height:0; flex:1 }
.pg-sbook .pgs-row{ text-align:left; display:flex; flex-direction:column; gap:1px; padding:7px 10px; border-radius:9px;
    border:1px solid rgba(255,255,255,.07); background:rgba(255,255,255,.035); color:#e8f4f4 }
.pg-sbook .pgs-t{ font-weight:700; font-size:13.5px }
.pg-sbook .pgs-a{ font-size:11.5px; color:#9fc4c6 }
/* the collection credit reads as part of the byline, not as a control — it only looks like a link when
   you go near it. It is the attribution the licence asks for, so it must be visible, but it should not
   compete with the song. */
.pgs-src-link{ font:inherit; font-size:inherit; color:#8fb3b6; background:none; border:0; padding:0;
  text-decoration:underline; text-underline-offset:2px; text-decoration-color:rgba(143,179,182,.4);
  cursor:pointer }
.pgs-src-link:hover, .pgs-src-link:focus-visible{ color:#cfeef0; text-decoration-color:currentColor }
.pg-sbook .pgs-row.pgs-cur{ box-shadow:inset 0 0 0 2px rgba(127,214,219,.55) }
.pg-sbook .pgs-row.pgs-on{ background:rgba(127,214,219,.18); border-color:rgba(127,214,219,.5) }

/* ── 📱 THE LIBRARY ON A PHONE. Nathan: "I think the library should be on the phone too. But I think the only
   place to access it is probably when you are looking at a song." It is a sheet, raised by pressing the song
   count on the credit line, and it is the only way in. Jumping chord-to-chord and turning the wheel are left
   off here until he has judged them on the iPad. */
@media (max-width:819px), (orientation:portrait){
  body.pg-sbook-up .pg-sbook{
    display:flex; flex-direction:column; position:fixed; left:0; right:0; bottom:0;
    /* ⚠️ A DEFINITE HEIGHT, NOT JUST A CEILING. max-height alone let the sheet size itself to its
       CONTENT, so the list — which is flex:1 — got only the leftover space and could collapse to
       zero. Measured on an iPad in portrait: a 188px sheet with a 0px list, nothing to scroll and
       the search sitting wherever it landed. (Nathan: "I can't scroll. There's no way to get to the
       top of the menu and search.") With a real height the list always has an area to scroll inside,
       whatever the content does. min() keeps it off small screens. */
    height:min(76vh, 560px); max-height:min(76vh, 560px); z-index:60; box-sizing:border-box;
    padding:10px 12px calc(12px + env(safe-area-inset-bottom, 0px));
    background:#0e1b1d; border-top:1px solid rgba(127,214,219,.35);
    border-radius:14px 14px 0 0; box-shadow:0 -14px 34px rgba(0,0,0,.55);
    animation:pgsUp .18s ease-out;
  }
  body.pg-sbook-up .pg-sbook .pgs-list{ overflow-y:auto; -webkit-overflow-scrolling:touch; flex:1 1 auto; min-height:0 }
  /* and the search stays PUT at the top — it is the way out of a long list, so it must never scroll
     away or get squeezed by the rows beneath it. */
  body.pg-sbook-up .pg-sbook .pgs-head,
  body.pg-sbook-up .pg-sbook .pgs-sum{ flex:0 0 auto }
  body.pg-sbook-up .pg-sbook .pgs-in{ flex:0 0 auto }
  body.pg-sbook-up .pg-sbook .pgs-ch{ cursor:default }
  .pg-sbook .pgs-close{
    align-self:flex-end; flex:0 0 auto; margin:0 0 6px; padding:7px 14px; font:inherit; font-size:13px;
    color:#cfeef0; background:rgba(127,214,219,.14); border:1px solid rgba(127,214,219,.4);
    border-radius:999px; cursor:pointer; min-height:34px;
  }
  /* the count is the door — make it look like one */
  .pg-cardsong .pg-songn{ cursor:pointer; padding:1px 4px; border-radius:6px; background:rgba(127,214,219,.16) }
}
@keyframes pgsUp{ from{ transform:translateY(14px); opacity:.6 } to{ transform:none; opacity:1 } }
.pg-sbook .pgs-close{ display:none }
body.pg-sbook-up .pg-sbook .pgs-close{ display:block }
@media (min-width:820px) and (orientation:landscape){
  body.at-prog:not(.pg-desk){ --pg-sq:min(620px, max(280px, 100dvh - 500px)) }
  body.at-prog:not(.pg-desk) .solo-wrap{ display:grid !important; box-sizing:border-box;
    grid-template-columns:minmax(320px, .9fr) minmax(0, 1.25fr) !important;
    /* ── 📐 THE KEYBOARD'S BAND IS RESERVED, AND THE MESSAGE SITS BELOW IT ─────────────────────────
       Nathan, 2026-10-01: "the messaging that says cord one major or minor moves to a lower spot after
       I click the play button. When I press play it moves back up to the top now."
       MEASURED (probe_askline.js --why): the ask line moved 86px, and nothing about the ask line was
       at fault. `keys` was an AUTO track, and `.pg-kb` does not exist until kbPaint() builds it on the
       first play — so the track went 0 -> 85.5px (a 64px keyboard plus its 22px top margin). `songs`
       spans rows 3-6, so its total never changed; the only flexible track, row 3, gave up exactly
       those 86px, and every row beneath it — including the one holding the message — slid up 86px.
       Worse: through all of that the keyboard is `kb-away`, i.e. visibility:hidden. An element that is
       deliberately invisible for the entire question was the thing moving the question.
       TWO changes, and the shift cannot come back:
         · `keys` is a FIXED 86px, so the keyboard appearing (on play, or later on grading) resizes
           nothing. This is the reserve-space rule, applied to the element that was breaking it.
         · `msg` moves BELOW `keys`, so the message is always the row directly above the transport —
           the spot that was signed off, "just over the play button" — instead of inheriting whatever
           the keyboard left behind.
       `msg` is 44px rather than 34px because the line it holds is 40px tall and was overflowing its
       own track. The 10px comes out of row 3, which keeps 320px of board inside a 335px span. */
    grid-template-areas:"top top" "card board" "songs board" "songs keys" "songs msg" "songs transport" "streak note" !important;
    grid-template-rows:auto auto minmax(0,1fr) 86px 44px 84px 0 !important;
    column-gap:40px; row-gap:10px; align-content:stretch !important;
    height:calc(100dvh - var(--pgwrap-top, 100px) - 12px); min-height:0 !important; overflow:hidden; max-width:1280px; margin:0 auto; padding:6px 28px 10px }
  body.at-prog:not(.pg-desk) .solo-wrap>*{ min-width:0 }
  body.at-prog:not(.pg-desk) .solo-wrap>.hostbar{ grid-area:top !important; width:auto !important }
  body.at-prog:not(.pg-desk) .solo-wrap>.strip{ grid-area:card !important; align-self:start !important }
  body.at-prog:not(.pg-desk) .solo-wrap>.pg-sbook{ grid-area:songs !important; display:flex; flex-direction:column; min-height:0;
    background:var(--panel); border:1px solid var(--line); border-radius:14px; padding:10px 12px; overflow:hidden; height:100%; max-height:100% }
  body.at-prog:not(.pg-desk) .solo-wrap>.pg-read-strip,
  body.at-prog:not(.pg-desk) .solo-wrap>.mm-ask{ grid-area:msg !important; align-self:center !important; justify-self:center !important; margin:0 !important }
  /* ONE streak reading, not two: the level bar already shows it (Nathan: "I see 2 different streak metrics") */
  body.at-prog:not(.pg-desk) .solo-wrap>.cardots{ display:none !important }
  /* the clue and the songbook are the same column, so they are the same width */
  body.at-prog:not(.pg-desk) .solo-wrap>.strip, body.at-prog:not(.pg-desk) .solo-wrap>.pg-sbook{ width:100% !important; max-width:none !important; box-sizing:border-box }
  /* the clue's visible box is inset inside its row; the library takes the same inset so the two edges line up */
  body.at-prog:not(.pg-desk) .solo-wrap>.pg-sbook{ margin:0 12px 0 5px !important; width:auto !important }
  /* the level bar's magnifier means nothing here either — the library owns the search (Nathan: "the search icon
     in the level section isn't meaningful here. It doesn't belong there at all") */
  body.at-prog:not(.pg-desk) .pg-hostbar .hb-srch{ display:none !important }
  /* the reading strip has room in landscape: the "green is what sounded" line gets its own line, not a squeeze */
  body.at-prog:not(.pg-desk) .solo-wrap>.pg-read-strip{ display:flex; flex-direction:column; gap:6px; align-items:center }
  body.at-prog:not(.pg-desk) .solo-wrap>.pg-read-strip .pg-verdict{ display:block; margin:0; white-space:nowrap }
  /* the search lives in the library here, so the card's magnifier goes — it was also what made the citation
     line jump when a long title wrapped (Nathan: "is the magnifying glass necessary?") */
  body.at-prog:not(.pg-desk) .pg-cardfoot .pg-srch{ display:none !important }
  /* the library's own controls, on the search line */

  .pg-sbook .pgs-nav{ flex:0 0 auto; width:34px; height:34px; border-radius:9px; border:1px solid rgba(127,214,219,.35);
    background:rgba(255,255,255,.04); color:#e8f4f4; cursor:pointer; font-size:13px; line-height:1 }
  .pg-sbook .pgs-nav[disabled]{ opacity:.35; cursor:default }
  .pg-sbook .pgs-nav.on{ background:rgba(127,214,219,.22); border-color:rgba(127,214,219,.7) }

  /* the keyboard has two rows to itself here — no need for the phone's squash */
  body.at-prog:not(.pg-desk) .solo-wrap>#pgKeys>.pg-kb{ height:auto !important; max-height:none !important }
  body.at-prog:not(.pg-desk) .solo-wrap>#pgKeys>.pg-kb{ padding:0 !important; max-width:none !important }
  body.at-prog:not(.pg-desk) .solo-wrap>#pgKeys>.pg-kb .piano{ width:100% !important; height:auto !important; max-height:none !important }

  body.at-prog:not(.pg-desk) .solo-wrap>#pgWheelHost,
  body.at-prog:not(.pg-desk) .solo-wrap.play>.mm-board.mm-board.mm-board.mm-board{ grid-area:board !important; justify-self:center !important; align-self:start !important;
    width:min(100%, var(--pg-sq)) !important; max-width:var(--pg-sq) !important; margin:0 !important; align-self:start !important }
  body.at-prog:not(.pg-desk) .solo-wrap>#pgKeys{ display:contents !important }
  body.at-prog:not(.pg-desk) .solo-wrap>#pgKeys>.pg-transport{ grid-area:transport !important; align-self:center }
  body.at-prog:not(.pg-desk) .solo-wrap>#pgKeys>.pg-kb{ grid-area:keys !important; position:static !important; transform:none !important; align-self:center; justify-self:center; width:100%;
    /* under the wheel, where the column is wide: the keyboard scales by WIDTH, so here it is naturally tall
       instead of the phone's squashed strip (Nathan: "it has room to be taller… it might belong in the other column") */ }
  /* the chord name under the keyboard gets the message line, not the keys themselves */
  /* ── 🅰️ THE KEYBOARD'S CHORD NAME HAD BEEN PARKED IN THE MESSAGE LINE, which is also where the read
     strip lives — so after a graded hand it was drawn ON TOP of the read pills. That is the "amber A with
     a blinking blue dot" Nathan could not account for: it is this label (kb-q-maj is amber #FFD79B) naming
     the chord the keyboard is showing, landing on the second pill. He guessed it himself; my own layout
     dump had both elements in grid-area msg at top 790–824 and 797–817 and I had not read it.
     It now sits directly above the keys it names, which is where it was always about. */
  body.at-prog:not(.pg-desk) .solo-wrap>#pgKeys>.kb-nm{ grid-area:keys !important; align-self:start; justify-self:center; position:static !important; margin:0 0 2px !important }
  body.at-prog:not(.pg-desk) .solo-wrap>#pgKeys>.pg-kb{ margin-top:22px !important }   /* room for the name above */
  body.at-prog:not(.pg-desk) .solo-wrap>.cardots{ grid-area:streak !important; align-self:center; width:auto !important; margin:0 !important }
  body.at-prog:not(.pg-desk) .solo-wrap>.gridmsg{ grid-area:note !important; margin:0 !important; min-height:0 !important; max-height:40px; overflow:hidden; align-self:center; position:static !important }
  /* the two big targets take the square they are given, without becoming a wall */
  /* the buttons start on the clue card's line — the board's own padding used to drop them below it */
  body.at-prog:not(.pg-desk) .solo-wrap.play>.mm-board.mm-board.mm-board.mm-board{ padding-top:0 !important; justify-content:flex-start !important; margin-top:34px !important }
  /* with the question moved out, the buttons take the square they are in */
  body.at-prog:not(.pg-desk) .mm-board .mm-big{ height:min(420px, calc(100% - 12px)) !important }
  /* folded while you play: just the search line. It opens to the full list when the hand is solved. */
  body.at-prog:not(.pg-desk) .solo-wrap>.pg-sbook:not(.open){ align-self:start; height:auto }
  /* the songbook panel */




  .pg-sbook button.pgs-row{ cursor:pointer }
  .pg-sbook button.pgs-row:hover, .pg-sbook button.pgs-row:focus-visible{ background:rgba(127,214,219,.12) }


}
/* skip / next share one reserved slot in every layout, so play never moves when the word changes */
.pg-tright .pg-nextb{ min-width:92px }

/* a song in the library opens: its chords in that song's numerals, where the transcription came from, and a play */
/* ── 📣 THE SITE'S COUNTDOWN BAR, LIVING IN THE MENU BAR. Same class names and the same brand colors as
   the website's own bar (themes/bks2025 _countdown.scss + lib/_vars.scss), so there is one look and one
   behavior whether you are in the app or in a browser. It sits in a strip that already exists, so it
   costs no height — which is the whole point. (Nathan: "discreet because it doesn't take up any more
   space but punchy because we already have formatting that does it.") */
/* ── ↔️ STAND IT OFF FROM THE GEAR. Sitting flush against Settings, it read as though pressing the gear
   might join the class. (Nathan: "so it does not look like clicking settings is going to join that
   class or something.") It now sits in the middle of the gap with real air on the right, and a hairline
   separates it from the controls rather than leaving them to run together. */
/* ── ↔️ CENTRED IN THE GAP, WITH NOTHING TOUCHING THE GEAR. Nathan: "I want it centered in the space
   available between the settings button and the last item. Otherwise it looks like the settings button is
   related to the event." Auto margins on BOTH sides, so the free space is split evenly — it belongs to the
   gap rather than leaning on either neighbour. The divider line is gone too; it was doing the opposite of
   separating, by tying the event to the controls beside it. */
/* ── ↔️ ACTUALLY CENTRED. `margin:0 auto` did nothing here: both `.bb-side` groups are `flex:1 1 0`, so
   THEY grow to eat the row and the bar just gets squeezed between them — which is why it sat hard against
   the gear. (I measured 6px either side and called it centerd; I had measured the gap to two already-
   stretched blocks, not the position within the empty space. Wrong measurement, wrong conclusion.)
   The bar itself takes the middle and centers its own content; the two groups sit at their natural width. */
.brandbar-v3 > .bb-side{ flex:0 1 auto }
.pg-nowbar.countdown-bar{
  display:flex; align-items:center; justify-content:center; gap:7px; cursor:pointer;
  flex:1 1 auto; min-width:0; overflow:hidden;
  background:none; border:0; padding:0 10px; margin:0;
  font-size:12px; line-height:1; white-space:nowrap;
}
.pg-nowbar .countdown-headshot{
  width:20px; height:20px; border-radius:50%; object-fit:cover; flex:0 0 auto;
  border:1px solid rgba(255,255,255,.25);
}
.pg-nowbar .next-event-label{ font-weight:500; color:#fff; opacity:.75; flex:0 0 auto }
.pg-nowbar .event-name{ font-weight:600; color:#E0F200; overflow:hidden; text-overflow:ellipsis }
.pg-nowbar .event-instructor{ color:#fff; font-style:italic; font-weight:300; opacity:.8; flex:0 0 auto }
.pg-nowbar .event-datetime{ color:#2DACB3; font-weight:500; white-space:nowrap; flex:0 0 auto }
.pg-nowbar:hover .event-name{ text-decoration:underline }
/* live: the lemon goes to the time as well, and it pulses so "now" reads as now */
.pg-nowbar.live .event-datetime{ color:#E0F200; font-weight:800; letter-spacing:.06em; animation:nbPulse 1.9s ease-in-out infinite }
@keyframes nbPulse{ 0%,100%{ opacity:1 } 50%{ opacity:.45 } }
@media (prefers-reduced-motion:reduce){ .pg-nowbar.live .event-datetime{ animation:none } }
/* ── 📵 NOT ON A PHONE-WIDTH ROW. I had this gate when the bar lived in the level panel and dropped it
   when the styles were rewritten for the menu bar — Nathan: "it shows up on my iPhone where it collides
   against the entire top row. I thought you had this gated to a device that could handle it." Measured at
   390px: nothing technically overlaps, but four items share the row and the bar is squeezed to 101px while
   progress/points/online are crushed to 115px. Comfortable from ~844px up (iPhone landscape and every
   iPad), so that is the gate. Where this goes on a phone is still an open question, not a solved one. */
.pg-nowbar.countdown-bar{ display:none }
@media (min-width:820px){
  .pg-nowbar.countdown-bar{ display:flex }
  /* narrow tablets and phone-landscape: keep the photo, the name and the time; drop the words */
  @media (max-width:900px){
    .pg-nowbar .next-event-label, .pg-nowbar .event-instructor{ display:none }
    .pg-nowbar.countdown-bar{ font-size:11px; margin:0 auto; padding:0 }
    .pg-nowbar .event-name{ max-width:30vw }
  }
}

/* the licence line: present and readable, but never competing with the songs */
/* ── 🔗 ONE LINE INSTEAD OF A PARAGRAPH. Attribution stays in the UI (the grant depends on it) without
   eating the panel; the whole story is one press away. */
.pgs-srcbtn{ flex:0 0 auto; margin:9px 2px 0; padding:6px 0 0; width:100%; text-align:left;
  font:inherit; font-size:10.5px; color:#7f9a90; background:none; border:0;
  border-top:1px solid rgba(127,214,219,.12); cursor:pointer; text-decoration:underline;
  text-underline-offset:2px; text-decoration-color:rgba(127,214,219,.3) }
.pgs-srcbtn:hover, .pgs-srcbtn:focus-visible{ color:#cfeef0 }
/* the sheet behind it */
.sb-list{ display:flex; flex-direction:column; gap:1px; margin:10px 0 }
.sb-row{ display:grid; grid-template-columns:minmax(120px,auto) 62px 1fr; gap:10px; align-items:baseline;
  padding:7px 9px; border-radius:8px; background:rgba(255,255,255,.035); font-size:12.5px }
.sb-row .sb-n{ font-weight:700; color:#e8f4f4; text-decoration:underline; text-underline-offset:2px;
  text-decoration-color:rgba(127,214,219,.35) }
.sb-row a.sb-n:hover, .sb-row a.sb-n:focus-visible{ color:#8affbe; text-decoration-color:currentColor }
.sb-row .sb-c{ font-variant-numeric:tabular-nums; text-align:right; color:#8affbe; font-weight:700 }
.sb-row .sb-w{ color:#9fc4c6; font-size:11.5px }
.sb-row .sb-w i{ opacity:.8 }
.sb-ours{ margin-top:8px; background:rgba(138,255,190,.10); border:1px solid rgba(138,255,190,.28) }
.sb-foot{ margin-top:12px !important; font-size:11px !important; opacity:.85 }
@media (max-width:600px){
  .sb-row{ grid-template-columns:1fr auto; }
  .sb-row .sb-w{ grid-column:1 / -1 }
}
.pgs-legal a{ color:#8fb3b6; text-decoration:underline }
.pgs-title{ display:flex; align-items:center; gap:8px; margin:0 2px 7px; flex:0 0 auto; min-height:26px }
.pgs-titlet{ font-size:12px; font-weight:800; letter-spacing:.09em; text-transform:uppercase; color:#9fc4c6 }
/* 🔆 THE STATE READS AS A LIT BADGE, not a sentence. Off the board is a mode, and a mode should announce
   itself the way a mode does — a small box that lights up — so the return button beside it has an obvious
   job. (Nathan: "that way it lights up so you know you've left the game.") */
.pgs-xpbadge{
  font-size:9.5px; font-weight:800; letter-spacing:.07em; text-transform:uppercase;
  color:#0c1a0f; background:#8affbe; border:1px solid #b6ffd8; border-radius:6px;
  padding:3px 7px; white-space:nowrap; box-shadow:0 0 10px rgba(138,255,190,.45);
  animation:xpGlow 2.4s ease-in-out infinite;
}
@keyframes xpGlow{ 0%,100%{ box-shadow:0 0 10px rgba(138,255,190,.45) } 50%{ box-shadow:0 0 15px rgba(138,255,190,.75) } }
@media (prefers-reduced-motion:reduce){ .pgs-xpbadge{ animation:none } }
/* the way back sits at the far right of the same row — never under the search box */
.pgs-title .pgs-back{
  margin-left:auto; flex:0 0 auto; font:inherit; font-size:11px; font-weight:700;
  color:#cfeef0; background:rgba(127,214,219,.16); border:1px solid rgba(127,214,219,.5);
  border-radius:999px; padding:4px 10px; cursor:pointer; white-space:nowrap; min-height:26px;
}
.pgs-title .pgs-back:hover{ background:rgba(127,214,219,.28) }
.pg-sbook .pgs-row{ cursor:pointer }
.pg-sbook .pgs-more{ display:none; margin-top:6px; gap:8px; align-items:center; flex-wrap:wrap }
.pg-sbook .pgs-row.pgs-open .pgs-more{ display:flex }
.pg-sbook .pgs-prog{ font-size:12px; font-weight:700; color:#e8f4f4; background:#0b1f24; border-radius:999px; padding:2px 8px }
.pg-sbook .pgs-src{ font-size:11.5px; color:#9fc4c6 }
.pg-sbook .pgs-play{ margin-left:auto; font-size:11.5px; padding:3px 9px; border-radius:999px; cursor:pointer;
  border:1px solid rgba(127,214,219,.45); background:rgba(127,214,219,.12); color:#e8f4f4 }

/* a song's own chords, pressable: press one and those four become the hand */
.pg-sbook .pgs-chords{ display:flex; flex-wrap:wrap; gap:4px; width:100%; margin-top:5px; max-height:92px; overflow-y:auto }
.pg-sbook .pgs-ch{ font-size:11.5px; font-weight:700; padding:2px 7px; border-radius:7px; cursor:pointer;
  border:1px solid rgba(255,255,255,.14); background:rgba(255,255,255,.06); color:#e8f4f4 }
.pg-sbook .pgs-ch:hover, .pg-sbook .pgs-ch:focus-visible{ background:rgba(127,214,219,.22); border-color:rgba(127,214,219,.6) }
.pg-sbook .pgs-nochords{ font-size:11px; color:#9fc4c6 }
.pg-sbook .pgs-prog{ font-weight:600 }

/* 🎡 the wheel's turning handle — only on a finished or explored hand */
.wh-home .wh-homeknob{ fill:#0b1f24; stroke:#E2B84A; stroke-width:2; cursor:grab }
.wh-home.turning .wh-homeknob{ cursor:grabbing; fill:#E2B84A }
.wh-home .wh-homedot{ font-size:11px; font-weight:800; fill:#E2B84A; text-anchor:middle; dominant-baseline:central; pointer-events:none }
.wh-home.turning .wh-homedot{ fill:#1d1606 }
.wh-home .wh-homelab{ font-size:12px; font-weight:800; fill:#E2B84A; text-anchor:middle; dominant-baseline:central; pointer-events:none }

/* the four chords currently on the wheel, marked in the song's own chord line */
.pg-sbook .pgs-ch.pgs-ch-on{ background:rgba(127,214,219,.3); border-color:rgba(127,214,219,.85); color:#fff }

/* turning the ring to re-home: the label rides under the wheel, clear of the level bar */
#pgWheelHost{ position:relative }
/* ── 🎯 THE LANDING READOUT, OUTSIDE AND STILL. It sits ABOVE the ring rather than on it, so it never turns
   with the wheel, and it names the chord AND the job it will take. (Nathan: "there's no indication of the
   destination function of the wedge. That needs to be printed on the outside so it doesn't move.") */
#pgWheelHost .wh-turnlab{ position:absolute; left:50%; top:-4px; bottom:auto; transform:translateX(-50%); opacity:0;
  display:flex; align-items:baseline; gap:7px;
  font-size:13px; font-weight:800; color:#1d1606; background:#E2B84A; border-radius:999px; padding:4px 13px;
  pointer-events:none; transition:opacity .12s ease; white-space:nowrap; z-index:6 }
#pgWheelHost .wh-turnlab.on{ opacity:1 }
#pgWheelHost .wh-turnlab b{ font-size:15px }
#pgWheelHost .wh-turnlab span{ font-size:11px; font-weight:800; opacity:.72; letter-spacing:.06em }
/* ── 🙈 A BARE RING WHILE IT TURNS. Riding labels jumble the moment the ring moves, and two attempts at
   counter-rotating them failed on a real device — blanked once, "spinning weirdly off into space" the
   second time. They simply go; the readout above says where it will land. */
#pgWheelHost.wh-turning .wh-nm,
#pgWheelHost.wh-turning .wh-nmpill,
#pgWheelHost.wh-turning .wh-fn,
#pgWheelHost.wh-turning .wh-mkn{ opacity:0; transition:opacity .1s ease }
#pgWheelHost.wh-turning .wh-svg{ transition:none }


/* 🆘 THE HUB HINT — instruction, not praise. It appears after a missed hand on a rung that can deal
   chords the wheel has no wedge for, and only for someone who has never built one by hand. Quiet on
   purpose: the celebration surface would have thrown confetti at a mistake. Sits above the board,
   clear of the wheel, and reserves its own space rather than shifting anything under it. */
/* ⚠️ IT WAS A TALL NARROW COLUMN OVER THE WHEEL. Laid out as a ROW with a nowrap title, the heading
   took half the width and squeezed the sentence into a ribbon about fifteen characters wide, so a
   two-line tip became a 600px tower covering the board. (Nathan, one word: "Ugly.") Stacked instead:
   the title on its own line, the sentence under it at full width, and the dismiss in the corner
   rather than competing for horizontal space. */
.pg-hubhint{
  position:fixed; left:50%; transform:translateX(-50%); bottom:calc(env(safe-area-inset-bottom,0px) + 86px);
  z-index:70; width:min(92vw,460px); box-sizing:border-box;
  display:block; padding:.7rem 2.1rem .75rem .9rem; border-radius:14px;
  background:#15222b; border:1px solid #2c4a57; box-shadow:0 8px 26px rgba(0,0,0,.55);
  font:500 13.5px/1.42 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; color:#DCE8EE;
  animation:hubHintIn .22s ease-out;
}
.pg-hubhint b{ display:block; color:#7fd6db; font-weight:800; font-size:13px; margin-bottom:2px }
.pg-hubhint span{ display:block }
.pg-hubhint .pg-hh-x{
  position:absolute; top:4px; right:4px; background:none; border:0; color:#7e99a6;
  font-size:15px; line-height:1; padding:6px 8px; cursor:pointer; min-width:34px; min-height:34px;
}
@keyframes hubHintIn{ from{ opacity:0; transform:translate(-50%,6px) } to{ opacity:1; transform:translate(-50%,0) } }
@media (prefers-reduced-motion:reduce){ .pg-hubhint{ animation:none } }


/* 🎓 THE HUB LESSON — three steps that teach the gesture the top rungs depend on. Deliberately plain:
   it is instruction, and the celebration surfaces are already busy at a promotion. */
.hublesson .hl-card{ max-width:min(92vw,400px); text-align:left }
.hl-step{ font:800 11px/1 -apple-system,BlinkMacSystemFont,sans-serif; letter-spacing:.09em;
          text-transform:uppercase; color:#7e99a6; margin-bottom:.45rem }
.hl-card h3{ margin:0 0 .6rem; font-size:1.06rem }
.hl-body{ margin:.55rem 0 .2rem; color:#c6d6de; font-size:.92rem; line-height:1.45 }
/* the diagram reserves its height up front so stepping through never shifts the buttons */
.hl-art{ position:relative; height:104px; border-radius:10px; background:#0e1a22;
         border:1px solid #243a45; overflow:hidden }
.hl-art i{ position:absolute; left:50%; top:50%; width:46px; height:46px; margin:-23px 0 0 -23px;
           border-radius:50%; background:#16323c; border:1px solid #2f5c6a; display:block }
.hl-art u{ position:absolute; left:50%; top:50%; width:16px; height:16px; margin:-8px 0 0 -8px;
           border-radius:50%; background:#7fd6db; display:block; opacity:.9 }
/* step 1: a finger pressing and holding */
.hl-art-hold u{ animation:hlPress 1.9s ease-in-out infinite }
@keyframes hlPress{ 0%,100%{ transform:scale(1); opacity:.55 } 45%{ transform:scale(2.1); opacity:1 } }
/* step 2: the same touch travelling around the ring */
.hl-art-rotate u{ animation:hlSpin 2.6s linear infinite }
@keyframes hlSpin{ from{ transform:rotate(0) translateX(34px) } to{ transform:rotate(360deg) translateX(34px) } }
/* step 3: settling on a choice */
.hl-art-quality u{ animation:hlPick 2.2s ease-in-out infinite }
@keyframes hlPick{ 0%,100%{ transform:translateX(-26px) } 50%{ transform:translateX(26px) } }
@media (prefers-reduced-motion:reduce){ .hl-art u{ animation:none } }

/* the predictor's caption — small, at the foot of the wheel, never over a wedge */
.wh-pull-cap{ fill:#FFD34D; stroke:#1b1405; stroke-width:2.6; paint-order:stroke fill;
              font:800 11px/1 -apple-system,BlinkMacSystemFont,sans-serif; letter-spacing:.02em }


/* ── 🎨 THE COLOR SECTION, GRADED BY DISSONANCE. The five extension pills were one amber family
   stepped only by lightness, so finding the 6 meant reading five labels every time. Three visibly
   different families instead — Nathan: "make it easier to find common ones like six. Let's grade
   them in terms of dissonance."
   The SLICE carries the grade, not the label: labels stay white so they are readable on every one,
   and the existing .off dimming still works because these only set a fill. */
#cbOv .fan2-cell.diss-open  .fan2-slice{ fill:#2f7f8c }   /* 2/9, 4/11 — leans and resolves */
#cbOv .fan2-cell.diss-sweet .fan2-slice{ fill:#e8c14a }   /* 6/13 — color, no tension, and the common one */
#cbOv .fan2-cell.diss-tense .fan2-slice{ fill:#b4553f }   /* ♯11, ♯9 — dissonant on purpose */
/* the sweet one is also the one you reach for most, so it gets the only bright rim */
#cbOv .fan2-cell.diss-sweet .fan2-slice{ stroke:#fff3c0 }
#cbOv .fan2-cell.diss-sweet.on .fan2-slice{ stroke:#fff }
/* dark text on the light slice, pale text on the dark ones — contrast, not a single rule */
#cbOv .fan2-cell.diss-sweet .fan2-lab{ fill:#241d05 }


/* 🫙 THE POT — points earned but not yet paid, sitting beside the sitting's running total. Gold,
   because it is money; quiet until there is something in it. It grows when a hand lands and empties
   with the pour animation when you leave, so the points have a visible life instead of being
   "proposed" and silently dropped. */
/* ── ⚡ WHAT THIS HAND JUST EARNED, PARKED IN ONE PLACE.
   It used to trail the level name inline at 11px, where it moved with the title's length and Nathan
   simply never saw it: "perhaps you think you are displaying my score but I don't see it anywhere here
   after completing this hand." His placement: "the place to hold the points while they are in
   transition is probably in the level chooser somewhere along the progress line as long as it is in a
   consistent spot." So it is pinned to the right end of the progress line — same pixel every hand,
   whatever the rung is called — and big enough to read at a glance. */
#pgLvlBtn{ position:relative }
#pgLvlBtn .pg-pot{ font-style:normal; margin:0; font-weight:800; font-size:14px; letter-spacing:.01em;
                   color:#e8c14a; opacity:0; transition:opacity .22s ease, transform .22s ease;
                   position:absolute; right:10px; bottom:6px; transform:translateY(3px);
                   display:inline-block; pointer-events:none; white-space:nowrap;
                   font-variant-numeric:tabular-nums;
                   text-shadow:0 1px 3px rgba(6,18,22,.85) }
#pgLvlBtn .pg-pot.on{ opacity:1; transform:translateY(0) }
/* banked: it has been awarded and is waiting for the bar to catch up on the next hand */
#pgLvlBtn .pg-pot.paid{ color:#7fd6db }
@media (max-width:380px){ #pgLvlBtn .pg-pot{ font-size:12.5px; right:7px; bottom:5px } }





/* ── 📈 EAR OVER TIME. Bars are the difficulty-weighted score per day, not raw accuracy, so the line
   rises when you take on harder chords instead of falling. Height is relative to your own best day —
   this is a personal trend, not a league table. */
.pc-ear .pc-earbars{ display:flex; align-items:flex-end; gap:3px; height:96px; padding:6px 0 0 }
.pc-ear .pc-earbars i{ flex:1 1 0; min-width:3px; border-radius:3px 3px 0 0;
  background:linear-gradient(180deg,#7fd6db,#2dacb3); opacity:.9 }
.pc-ear .pc-earbars i:last-child{ background:linear-gradient(180deg,#f2d98a,#e8c14a); opacity:1 }
.pc-earfoot{ display:flex; align-items:baseline; gap:8px; margin-top:10px }
.pc-earfoot b{ font-size:26px; font-weight:800; color:#e8c14a; line-height:1 }
.pc-earfoot small{ color:var(--dim); font-size:11px }
.pc-eardelta{ margin-left:auto; font-weight:800; font-size:12px; padding:3px 8px; border-radius:999px }
.pc-eardelta.up{ color:#0d2a17; background:#3ecf7a }
.pc-eardelta.down{ color:#f2d5cd; background:#6b3a2c }

/* ── 🏆 BEST RUNS. Best-ever came off the level bar (it collided, and it is not the target on the rung
   you are standing on) and lives here, with every level's own record beside it. */
.pc-bestever{ display:flex; align-items:baseline; gap:8px; margin-bottom:10px }
.pc-bestever b{ font-size:30px; font-weight:800; color:#e8c14a; line-height:1 }
.pc-bestever small{ color:var(--dim); font-size:11px }
.pc-runlist{ display:flex; flex-direction:column; gap:4px }
.pc-runrow{ display:flex; align-items:center; justify-content:space-between; gap:10px;
  padding:6px 9px; border-radius:8px; background:rgba(255,255,255,.04); font-size:12.5px }
.pc-runrow span{ color:#c6d6de; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap }
.pc-runrow b{ color:#C9A7F5; font-weight:800; flex:none }


/* ══════════════════════════════════════════════════════════════════════════════════════════════
   🗣️ BLINDFOLD TEST — FACELIFT. Nathan, shooting an ad on it: "it looks like somebody designed it
   in visual basic for Windows 1995." He is right, and against the landing screen the gap is obvious:
   that screen is rounded gradient cards with real hierarchy, this was an underlined text link, two
   dense unstyled stat lines, three square full-width buttons and a bare all-caps box — and on iPad
   PORTRAIT the whole thing stranded in the top third with a huge dead area under it.
   Restyled ONLY. No markup changes, so nothing about the game's behavior moves.
   ══════════════════════════════════════════════════════════════════════════════════════════════ */

/* 1. FILL THE PAGE. The content is short, so on a tall screen it must center rather than pile at the
      top. Capped so it never sprawls into a wide, thin ribbon on a landscape iPad. */
body[data-gm="voice"] .panel, .panel:has(> .vc-stage){
  max-width:min(92vw, 560px); margin-inline:auto;
  min-height:min(78vh, 900px); display:flex; flex-direction:column; justify-content:center;
}

/* 2. THE HEADER. Two dense lines of "Level 1 · notes one then the other · round 2/10 · streak to
      climb: 8" and "Score 0 · streak 0" become chips you can read at a glance from a tripod. */
.vc-level{
  color:var(--accent2,#7fd6db); font-size:13px; font-weight:700; margin:2px 0 10px;
  display:inline-flex; flex-wrap:wrap; gap:6px; align-items:center;
}
.vc-score{
  display:inline-flex; gap:14px; align-items:baseline; margin:0 0 18px;
  color:var(--dim); font-size:12px; letter-spacing:.02em;
  padding:7px 12px; border-radius:999px; background:rgba(255,255,255,.045);
  border:1px solid rgba(255,255,255,.07);
}
.vc-score b{ color:#e8c14a; font-size:17px; font-weight:800; margin-left:3px }

/* 3. THE STAGE — the thing the camera is actually pointed at. It was a 44px emoji and some text on a
      flat background; now it is the panel's center of gravity. */
.vc-stage{
  text-align:center; padding:30px 18px 26px; border-radius:18px;
  background:linear-gradient(180deg, rgba(45,172,179,.10), rgba(10,28,34,.55));
  border:1px solid rgba(127,214,219,.20);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.05), 0 10px 30px rgba(0,0,0,.35);
}
.vc-mic{ font-size:62px; line-height:1; filter:drop-shadow(0 6px 18px rgba(0,0,0,.45)) }
.vc-status{ font-size:20px; font-weight:700; margin-top:14px; min-height:26px; letter-spacing:-.01em }
.vc-heard{ color:var(--dim); font-size:13px; margin-top:8px; min-height:18px }
.vc-worth{
  display:inline-block; margin-top:12px; color:#241d05; background:#e8c14a;
  font-size:12px; font-weight:800; padding:4px 11px; border-radius:999px;
}

/* 4. THE BUTTONS. Square, equal-weight and stacked read as a form. The primary is now clearly the
      thing to press, and the other two step back to secondary controls sitting side by side. */
.vc-btns{ display:flex; flex-direction:column; gap:10px; margin-top:18px }
.vc-btns .primary.big{
  padding:17px 20px; font-size:18px; font-weight:800; border-radius:14px;
  box-shadow:0 6px 20px rgba(45,172,179,.30);
}
.vc-btns .primary.big:active{ transform:translateY(1px) }
.vc-btns .ghost{ border-radius:12px; padding:13px 16px; font-size:14px; font-weight:600 }
@media (min-width:430px){
  /* the two secondary controls share a row, so the primary keeps the eye */
  .vc-btns{ display:grid; grid-template-columns:1fr 1fr; gap:10px }
  .vc-btns .primary.big{ grid-column:1 / -1 }
}

/* 5. THE TRANSCRIPT. A bare bordered box with a shouty all-caps label became a quiet panel that only
      asserts itself once there is something in it. */
.vc-log{
  margin-top:16px; border:1px solid rgba(255,255,255,.08); border-radius:14px;
  padding:12px 14px; background:rgba(6,16,20,.6); max-height:190px; overflow-y:auto;
}
.vc-logh{
  color:var(--dim); font-size:10px; letter-spacing:.1em; text-transform:uppercase;
  margin-bottom:6px; font-weight:700;
}
.vc-logrow{ font-size:13.5px; padding:6px 0; border-top:1px solid rgba(255,255,255,.06) }

/* 6. ACCESSIBILITY. Nathan: "we could also probably make it more accessible." The status line is the
      one thing a blind player needs and it was 17px of normal weight; the back control was an
      underlined word with no hit area. Both now meet a 44px target and carry real contrast. */
.vc-stage .vc-status{ color:#F2F7F8 }
.panel:has(> .vc-stage) > .link.back{
  display:inline-flex; align-items:center; gap:6px; min-height:44px; padding:0 4px;
  font-size:15px; font-weight:600; text-decoration:none; color:var(--accent2,#7fd6db);
}
.panel:has(> .vc-stage) > .link.back:active{ opacity:.7 }
@media (prefers-reduced-motion:reduce){ .vc-mic{ animation:none !important } }


/* ── 🎨 SECOND PASS: BORROW THE CHORD TRAINER'S LANGUAGE. Nathan: "the best looking games and styling
   are now the chord trainer and maybe the interval bingo game. Borrow from that language if you're
   not sure what to do." Three things carry that look and none of them were here:
     · the LEVEL NAME as a big accent-colored title (THE WHOLE SONGBOOK), not a grey status line
     · the live card wearing a teal GLOW, so the thing in play announces itself
     · a tight stat row where labels are dim and only the numbers are bright
   Applied to the Blindfold Test so the two screens read as one product. */

/* the level line becomes the title it deserves to be */
.vc-level{
  display:block; font-size:19px; font-weight:800; letter-spacing:.01em; line-height:1.25;
  color:var(--accent2,#7fd6db); margin:6px 0 12px; text-transform:none;
}
.vc-level span{ font-weight:600; color:var(--dim); font-size:13px }

/* the stat row: dim words, bright numbers, no box around it — exactly the Chord Trainer header */
.vc-score{
  display:flex; gap:16px; align-items:baseline; margin:0 0 20px;
  padding:0; background:none; border:0;
  color:var(--dim); font-size:12.5px; letter-spacing:.01em;
}
.vc-score b{ color:#e8c14a; font-size:19px; font-weight:800; margin-left:4px }

/* the stage wears the live-card glow */
.vc-stage{
  border:1px solid rgba(127,214,219,.34);
  box-shadow:0 0 0 1px rgba(45,172,179,.14), 0 0 26px rgba(45,172,179,.18),
             inset 0 1px 0 rgba(255,255,255,.05), 0 10px 30px rgba(0,0,0,.35);
}

/* ⚠️ AND STOP STRANDING IT. Centring inside a tall panel left a dead band above and below on iPad
   portrait — the screen is short, so it should sit high and compact rather than float. */
body[data-gm="voice"] .panel, .panel:has(> .vc-stage){
  min-height:0; justify-content:flex-start; padding-top:18px;
}

/* the transcript only earns its space once there is something in it */
.vc-log:not(:has(.vc-logrow)){
  background:none; border:0; padding:6px 0 0; max-height:none;
}
.vc-log:not(:has(.vc-logrow)) .vc-logh{ opacity:.55 }


/* 🎹 THE REVEAL KEYBOARD. Two keys lit is the whole lesson — "it was a minor third" teaches nothing,
   seeing WHERE teaches immediately. Reserves no space until it has something to show, so the layout
   does not jump when a round ends. */
/* ⚠️ max-height WAS CLIPPING IT. A two-octave keyboard drawn to the panel's width is taller than the
   150px cap I first set, so the bottom of the keys was simply cut off — Nathan: "you're clipping the
   pictures." The collapse animation still needs a height to animate BETWEEN, so the open state gets a
   ceiling generous enough to never bite (the SVG scales to its own aspect ratio well below it). */
.vc-kb{ max-height:0; overflow:hidden; opacity:0; transition:max-height .3s ease, opacity .22s ease, margin .3s ease }
.vc-kb.on{ max-height:340px; opacity:1; margin-top:16px }
/* the drawing keeps its own proportions and never squeezes to fit a box */
.vc-kb svg{ width:100%; height:auto; display:block; max-width:520px; margin:0 auto }
@media (prefers-reduced-motion:reduce){ .vc-kb{ transition:none } }


/* ⚠️ THE SEPARATOR BECAME A FLEX ITEM. "Score 0 · streak 0" is one text node with a middot in it, so
   flex + gap spaced the dot as though it were a third column — "Score  0  ·  streak  0". Inline-block
   keeps it as a sentence and lets the numbers stay big. */
.vc-score{ display:block; white-space:nowrap }
.vc-score b{ display:inline-block; margin:0 2px 0 4px }

/* the round line is the title; the detail after it steps back so the level reads first from a tripod */
.vc-level{ font-size:17px }

/* nothing below the transcript, so let the panel end where the content ends */
.panel:has(> .vc-stage){ padding-bottom:20px }


/* 🔥 THE STREAK BADGE. Small and permanent — the point is familiarity, not pressure: people should be
   used to seeing their tracker long before it ever gates anything. Lit when today is already banked,
   outlined when the day is still open. */
.streak-badge{
  display:inline-flex; align-items:center; gap:3px; font-weight:800; font-size:12px;
  padding:3px 8px; border-radius:999px; line-height:1; vertical-align:middle;
}
.streak-badge.on{ color:#241d05; background:#e8c14a }
.streak-badge.risk{ color:#e8c14a; background:transparent; box-shadow:inset 0 0 0 1px rgba(232,193,74,.55) }

/* the countdown inside the badge — the number that is actually ticking gets the weight */
.streak-badge b{ font-weight:800; margin-left:1px }
.streak-badge small{ font-weight:600; opacity:.75; margin-left:2px; font-size:10px }

/* ⏳ the free-tier timer, sitting with the other nav stats rather than floating above the login
   button where Nathan found it with no label at all. Gold: it is the thing that keeps the app free. */
.navstat.navfree b{ color:#e8c14a; font-weight:800 }
/* on the plan = the pass is live (warm gold); not yet = the same badge reading as a target, not a prize */
.navstat.navfree:not(.on) b{ color:var(--dim,#8aa6b0) }
/* ── 🚦 THE VELOCITY TIER DOTS. The bar that used to live here read as a quantity, in a strip with no
   room to label one. Nathan, 2026-09-27: "the colored line of dots needs to be quite small and the
   information lives in a pop-up." So: four dots, deliberately tiny, saying only WHICH TIER — an unlit
   dot is dim, never red, because the bottom tier is a floor and not a failure. Fixed footprint so the
   top bar cannot reflow as tiers change. */
.freedots{ display:inline-flex; align-items:center; gap:2.5px; vertical-align:middle;
  margin-right:5px; flex:0 0 auto }
.freedots > i{ display:block; width:4px; height:4px; border-radius:50%;
  background:rgba(255,255,255,.20); transition:background-color .3s ease }
.freedots > i.lit{ background:#8aa6b0 }
.navfree.fm-green > .freedots > i.lit{ background:#35C759 }
.navfree.fm-amber > .freedots > i.lit{ background:#E8C14A }
.navfree.fm-red   > .freedots > i.lit{ background:#FF6B5A }
.navfree.fm-green .nav-lab{ color:#35C759; opacity:.85 }
@media (max-width:400px){ .freedots{ margin-right:3px; gap:2px } }
@media (max-width:380px){ .freedots > i{ width:3.5px; height:3.5px } .brandbar-v3 .nav-lab{ font-size:9.5px } }
@media (prefers-reduced-motion:reduce){ .freedots > i{ transition:none } }
/* the old bar, kept only so a cached page that still has one in the DOM does not show a stray block */
.freemeter{ display:none !important }
.navstat.navfree[hidden]{ display:none }
/* ── 📈 THE PROGRESS READING IS AN ICON AGAIN. "progress 15/12" cost 84px in a bar that overflowed a
   375pt phone by 89 — the "online" reading ran off the edge under the gear. One glyph, no label; the
   count lives in the tooltip. Sized and baseline-shifted to sit on the same line as the numbers
   beside it, because the row is baseline-aligned and an emoji's box is taller than a digit's. */
.brandbar-v3 .navstat.navprog{ align-items:center }
.brandbar-v3 .navstat .nav-ic{ font-size:14px; line-height:1; position:relative; top:1px;
  filter:saturate(.85); display:inline-block }
.brandbar-v3 .navstat .nav-lab[hidden]{ display:none }

/* points lead the by-mode row; the game count is the aside */
.pcm-row b small{ font-weight:600; opacity:.7; font-size:11px }

/* ── 🍞 TOASTS LEAVE. Every note in the trainer used to wait for a tap on its own ✕ — a thing meant to
   inform you became one more thing to clear. They now drift up and fade on their own, or on a tap
   anywhere outside them. (Nathan: "they should be a little [toasts] that appear, and then maybe float
   away on their own… Otherwise it's a chore.") */
.pg-hubhint.leaving, .pg-streakcard.leaving{
  animation: bkToastOut .26s ease-in forwards; pointer-events:none;
}
@keyframes bkToastOut{ to{ opacity:0; transform:translateY(-10px) } }
@media (prefers-reduced-motion:reduce){
  .pg-hubhint.leaving, .pg-streakcard.leaving{ animation:none; opacity:0 }
}

/* a graded hand's award stays put and reads as settled, not as a thing still drifting upward */
.pg-pot.paid{ opacity:1; transform:none; animation:none }

/* ── 🪪 WHO YOU ARE PLAYING AS. Wide screens only: the iPad menu bar has the room Nathan pointed at,
   and on a phone the bar is already full, so the name lives behind the gear there instead.
   A GUEST name is the one you will not recognize, so it is the one that gets the warmer color. */
.navstat.navwho{ display:none }
@media (min-width:820px){ .navstat.navwho{ display:inline-flex } }
.navstat.navwho b{ font-weight:700; max-width:14ch; overflow:hidden; text-overflow:ellipsis; white-space:nowrap }
.navstat.navwho.guest b{ color:#E8C14A }
.navstat.navwho.guest .nav-lab{ color:#E8C14A; opacity:.8 }
.navstat.navwho[hidden]{ display:none !important }

/* ── 🎟️ FREE: STACK THE WORD OVER THE DOTS. Side by side, the dots and the word "free" cost twice the
   width the badge needs, in a bar that has none to spare. Nathan: "it takes up twice the width that it
   needs to. Put the word free above the dots. Don't use up any more space than it already does."
   So the badge becomes one narrow column — label on top, dots beneath — and the row keeps its height. */
/* ⚠️ AND IT MUST NOT GROW THE ROW. Stacking it without capping the height made the badge TALLER than
   the bar, so "free" and its dots fell out of the row and sat under the points — Nathan saw it on his
   phone before I did, which is the thing I am supposed to prevent by looking first. The column now has
   a fixed height matching its siblings and cannot push the bar around. */
/* ⚠️ MEASURED, NOT GUESSED. Capping the height was not enough: `.navstat` carries vertical padding, so
   the content box was ~8px while the stacked label+dots need ~15 — and the dots spilled 6px BELOW the
   badge, which is the "free" hanging under the row that Nathan photographed. Padding goes to zero on
   this one badge so the column actually fits the height it claims. Verified by reading the boxes back:
   label and dots must both end at or above the row's bottom edge. */
/* ── 📐 LEFT-ALIGNED, STACKED, IN THIS ORDER: NAME · "playing free" · DOTS.
   Nathan: "I'd like the free badge to be aligned left with the dots underneath it. And I believe it
   should say playing free. That way ideally it says Beat Kitchen playing free." Centring made the three
   rows read as three separate things; a shared left edge makes them one block, which is what they are —
   who you are, what you are on, and how you are doing on it. */
/* ⚠️ justify-content MUST be flex-start, not center. Once this became `row wrap`, justify-content
   switched from being the cross-axis rule to the MAIN one — so the second row (status + dots) was being
   centerd under the name instead of sharing its left edge, by about 6px. (Nathan: "please make sure all
   elements are aligned to the left.") */
.navstat.navfree{
  flex-direction:column; align-items:flex-start; justify-content:flex-start;
  gap:0; line-height:0; font-size:0; padding:0 4px; box-sizing:border-box;
  align-self:center; overflow:visible; text-align:left;
}
/* ── 📖 TWO LINES, NOT THREE. Three stacked rows inside a 51px bar forced every one of them down to
   8–9px, and the result was a block Nathan could not read: "the username plus the guest playing free
   plus the blinking dots is too small to read. Get rid of a line or make it two things side-by-side.
   There's room now I think." There is — dropping the word MENU freed about 45px. So the status and the
   dots share the second row, which buys both lines real size. */
.navstat.navfree .nav-lab{ order:0; font-size:11.5px; line-height:1.1; letter-spacing:.01em; margin:0;
  font-weight:700; max-width:132px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap }
/* row two: "guest · playing free" and the meter, side by side on one baseline */
/* ⚠️ MEASURED: this and the dots were landing on SEPARATE lines — three rows, not two, which is why
   the block stayed cramped after I "fixed" it. The status is ~81px and the dots ~22px, and the cap was
   104px, so the pair could never share a row. The cap below is set from those numbers, with the right
   margin trimmed, so row two actually holds both. (Nathan: "guest playing free should be on the same
   line as the indicator, not the user.") */
#navFree b#navFreeT{ display:inline-block !important; order:1; font-size:9.5px; line-height:1.1;
  font-weight:600; letter-spacing:.02em; opacity:.78; margin:2px 4px 0 0; white-space:nowrap;
  vertical-align:middle }
.navstat.navfree .freedots{ order:2; margin:2px 0 0; height:6px; align-self:center }
.navstat.navfree .freedots > i{ width:4.5px; height:4.5px }
/* the second row is a row: the badge itself stays a column, and these two wrap onto one line together */
.navstat.navfree{ flex-flow:row wrap }
.navstat.navfree .nav-lab{ flex:0 0 100% }
/* ⚠️ AND THE BLOCK IS CAPPED, or the name simply pushes the whole left group into the gear — measured at
   30px of overlap at 390 the moment the type got bigger. The cap is what makes the ellipsis do its job:
   a long name shortens instead of shoving its neighbours. */
/* ⚠️ FIXED WIDTH, NOT max-width. Sizing to content means the block changes width the moment the text
   does — and it does: signing in drops the word "guest" from the status line. Nathan watched the bar move
   under him: "something shifted while I was watching and my name and playing free moved over to the
   right. I can't tell what changed but I did." A reserved lane cannot shift, whoever is signed in. */
.navstat.navfree{ width:180px; max-width:180px; flex:0 0 180px }
.navstat.navfree .nav-lab{ max-width:100% }
@media (max-width:460px){
  .navstat.navfree{ max-width:150px }
  .navstat.navfree .nav-lab{ font-size:11px }
  #navFree b#navFreeT{ font-size:9px; margin-right:5px }
  .navstat.navfree .freedots > i{ width:4px; height:4px }
}
@media (max-width:400px){
  /* ── 📍 THE ONLINE COUNT NOW LIVES WITH THE GEAR, not in the left group. It was competing with the
     player's NAME for width, which is a contest the name has to win — and every time I made room for one
     I broke the other. On the right it sits in a group that is sized to its contents, so neither can
     starve the other again.
     MEASURED at 390: the block's own 4px side padding means an inner width 8px under the cap, so 112 was
     still one pixel short of holding status + dots together and it fell back to three rows. 116 inner,
     with the status a half-step down, fits the pair and leaves the gear its lane. The NAME keeps its
     size — that is the line he needs to read. */
  .navstat.navfree{ max-width:120px }
  .navstat.navfree .nav-lab{ font-size:10.5px }
  #navFree b#navFreeT{ font-size:8.5px }
  /* …and the last 8px comes off the brand mark, which is 34px of pure picture next to a name the player
     actually has to read. 26px is still plainly the puzzle piece. Nothing with words in it shrinks. */
  .brandbar-v3 .bb-home .brand-mark{ width:22px; height:22px }
  .brandbar-v3 .bb-side{ gap:4px }
}
/* ⚠️ AND IT HAD TO BE HIDDEN HARDER. `.navstat.navfree b{display:none}` lost to a more specific rule
   in the bar, so the empty <b> kept its 14px line — which pushed the stacked content 8px down and spilled
   the dots below the row. That, measured, is the whole bug. */
@media (max-width:400px){ .navstat.navfree .nav-lab{ font-size:9px } }

/* ── ✕ THE CLEAR BUTTON WAS SIMPLY TOO SMALL. 20px, and on a desktop that is a genuinely hard target.
   I first scoped this to mice, reasoning that press-and-hold covered touch — Nathan corrected me: "No
   press and hold works fine. The little X is too small." The hold was never the problem; the button was,
   and a 20px control is small on every device. So it grows everywhere, with a hit area larger than the
   button itself. Nothing about the hold gesture changes. */
.pg-slotx{ width:26px; height:26px; top:-9px; right:-9px; font-size:15px }
.pg-slotx::before{ top:-14px; right:-14px; bottom:-14px; left:-14px }
@media (hover:hover) and (pointer:fine){ .pg-slotx:hover{ transform:scale(1.18) } }

/* ── ↔︎ THE BAR HAS ROOM; STOP CROWDING THE LEFT. The stats were packed against each other at a 7px gap
   while a wide empty stretch sat before the gear — "look at all the wasted space in my menu bar. There's
   no reason to crowd this items." The free badge in particular jammed straight up against "pts", which
   read as one lump rather than two facts. Breathing room where the space already exists; the gear and
   the event block are untouched. */
.brandbar-v3 .bb-side{ gap:12px }
.brandbar-v3 .navstat.navfree{ margin-left:4px; margin-right:4px }
@media (min-width:820px){ .brandbar-v3 .bb-side{ gap:16px } }
@media (max-width:380px){ .brandbar-v3 .bb-side{ gap:9px } }

/* 💡 the quality the game is ASSUMING, marked in the fan the way it already is on the hub. Brighter rim
   and a soft glow — it reads as "this is what a tap takes" without competing with your own choice. */
.wh-fanq.assumed{ stroke:#FFD79B; stroke-width:2.4; filter:drop-shadow(0 0 4px rgba(255,215,155,.55)) }

/* 🎨 the color you were credited for, printed on the slot. Small, on the opposite corner to the verdict
   so neither crowds the other — a tooltip is useless on a phone, which is why this is on screen. */
.pg-slot[data-color]::after{
  content:attr(data-color); position:absolute; left:3px; bottom:2px;
  font-size:8.5px; font-weight:700; letter-spacing:.02em; line-height:1;
  color:#FFD79B; opacity:.9; pointer-events:none; text-shadow:0 1px 2px rgba(0,0,0,.7);
}

/* ── ↗ DIRECTION, NOT JUST LEVEL. The lit dots say how far along you are; the arrow says which way you
   are moving. Rising reads green whatever the level, because climbing out of the red IS the good news
   and coloring it red would contradict the arrow. */
.navstat.navfree.vel-up   .nav-lab{ color:#35C759 }
.navstat.navfree.vel-down .nav-lab{ color:#E8C14A }
.navstat.navfree .nav-lab{ white-space:nowrap }

/* ⭐ SAVED PROGRESSIONS — the honest guest notice. Not a toast and not a modal: it is a standing fact
   about where the data lives, so it stays on the screen it describes. Amber for the guest state (a
   warning, not an error — nothing is broken yet), quiet green once signing in has made it untrue. */
.pswarn{ margin:10px 0 14px; padding:10px 12px; border-radius:10px; font-size:13px; line-height:1.45;
  background:rgba(255,176,32,.10); border:1px solid rgba(255,176,32,.38); color:#ffd89a; }
.pswarn.ok{ background:rgba(60,200,120,.10); border-color:rgba(60,200,120,.34); color:#9ce5b8; }
.pswarn-in{ display:inline-block; margin-left:2px; padding:3px 10px; border-radius:999px; cursor:pointer;
  font:inherit; font-weight:700; background:rgba(255,176,32,.20); border:1px solid rgba(255,176,32,.55);
  color:#ffd89a; }
.pswarn-in:hover{ background:rgba(255,176,32,.30); }
/* Its own line, and the space is reserved by the block itself so nothing below it jumps when the state
   flips after the server restore lands. */
.pswarn-sub{ display:block; margin-top:5px; opacity:.72; font-size:12px; }

/* ── 🎯 THE LANDINGS ARE LIT; EVERYTHING ELSE STANDS DOWN.
   Nathan's replacement for drawing a second path: "the targeted wedges need to have slightly higher
   opacity than the ones that are dim… the highlighted number falling on a dim wedge tells that story."
   A numbered badge on a LIT wedge means he got there; the same badge on a DIM one means he went
   somewhere the progression never went — told by contrast alone, with no extra line to read.
   ⚠️ The badges themselves are deliberately NOT dimmed. They are his answer, they are the thing being
   read against the lighting, and fading them would hide exactly the case this exists to show. */
/* .34 was too deep — a dimmed wedge read as switched off rather than as "not this time", and on a hand
   where the lighting mis-fired it made correct answers look wrong. Lifted so the contrast still reads
   but an unlit wedge stays plainly legible. */
.wh-svg.wh-lit .wh-w,
.wh-svg.wh-lit .wh-nm,
.wh-svg.wh-lit .wh-sub,
.wh-svg.wh-lit .wh-nmpill,
/* ⚠ THE NUMERALS HAVE TO SURVIVE THE REVEAL. This dropped them to 55% exactly when the wheel is
   being READ — off-path wedges are muted at the same moment, so the dimming compounded and Nathan
   could not read a single numeral on a graded hand. The point of muting is to push WEDGES back, not
   to take the labels with them: the numeral is how you know which wedge you are looking at. */
.wh-svg.wh-lit .wh-fn{ opacity:.85; transition:opacity .28s ease }
.wh-svg.wh-lit .wh-w.wh-dest,
.wh-svg.wh-lit .wh-nm.wh-dest,
.wh-svg.wh-lit .wh-sub.wh-dest,
.wh-svg.wh-lit .wh-nmpill.wh-dest,
.wh-svg.wh-lit .wh-fn.wh-dest{ opacity:1 }
/* a lit landing also earns a quiet rim, so it reads as a destination and not merely as "less faded" */
.wh-svg.wh-lit .wh-w.wh-dest{ stroke:rgba(255,255,255,.34); stroke-width:1.4 }
/* the wedge that is RINGING still outranks the lighting — see .wh-w.sounding above */
.wh-svg.wh-lit .wh-w.sounding{ opacity:1 }
@media (prefers-reduced-motion:reduce){
  .wh-svg.wh-lit .wh-w, .wh-svg.wh-lit .wh-nm, .wh-svg.wh-lit .wh-sub,
  .wh-svg.wh-lit .wh-nmpill, .wh-svg.wh-lit .wh-fn{ transition:none }
}

/* ── 🔋 THE FREE METER READS LIKE A BATTERY (Nathan chose this shape over a travelling sweep).
   Solid dots are the charge. Exactly ONE dot is ever in motion — the one at the edge of the charge —
   and its COLOR is the direction: green arriving, red leaving. The solid dots keep their own color
   for where you stand, so the two readings never fight. Nothing else on the meter moves, which is what
   keeps a permanent fixture in the menu bar from nagging. */
/* ── 🪛 THE ENVELOPE IS THE DIRECTION — A SAWTOOTH, NOT A SINE.
   Both states used the same symmetric ease-in-out pulse and differed only in color, so the meter read
   as "something is blinking" and never as "going up" or "going down". Nathan: "if it blinks like a
   reverse sawtooth wave, that might indicate it's going down instead of up — it blinks on and slowly
   fades out for one direction and the opposite for charging."
   So the two are now mirror images, and the SHAPE carries the reading even if you never learn the
   colors:
     draining — snaps to full, then bleeds away over the whole cycle. The loop's wrap IS the attack.
     charging — creeps up from dark to full, then resets instantly. The wrap IS the release.
   Both are `linear`, because an ease on a sawtooth rounds off the very corner that distinguishes them,
   and both are faster than before (1.05s against 1.5s) because he could not see it changing — the ramp
   also runs the full range now, ending near invisible instead of at 18%. Scale rides along: a 4px dot
   moving only in opacity is below the threshold of noticing on a phone. */
@keyframes fmLeave {      /* ↘ reverse sawtooth: instant on, slow fade */
  0%  { opacity:1;   transform:scale(1) }
  18% { opacity:.78; transform:scale(.94) }
  55% { opacity:.34; transform:scale(.80) }
  100%{ opacity:.05; transform:scale(.66) }
}
@keyframes fmArrive{      /* ↗ forward sawtooth: slow swell, instant reset */
  0%  { opacity:.05; transform:scale(.66) }
  45% { opacity:.34; transform:scale(.80) }
  82% { opacity:.78; transform:scale(.94) }
  100%{ opacity:1;   transform:scale(1) }
}
@keyframes fmHold  { 0%,100%{ opacity:1 } 50%{ opacity:.45 } }
/* arriving — an empty slot filling in */
.freedots > i.vgo{ background:#35C759 !important; box-shadow:none;
  animation:fmArrive 1.05s linear infinite }
/* leaving — the top lit dot on its way out. Its colour is the ZONE IT IS HEADING INTO, set by
   applyMeterVelocity; red belongs to the last dot alone, never to a full meter. */
.freedots > i.vstop{ animation:fmLeave 1.05s linear infinite }
.freedots > i.vstop.t-green{ background:#35C759 !important }
.freedots > i.vstop.t-amber{ background:#E8C14A !important }
.freedots > i.vstop.t-red  { background:#FF6B5A !important }
/* topped up and steady — one slow breath, four seconds apart */
.freedots > i.vhold{ animation:fmHold 4s ease-in-out infinite }
@media (prefers-reduced-motion:reduce){
  .freedots > i.vgo, .freedots > i.vstop, .freedots > i.vhold{ animation:none }
  .freedots > i.vgo{ opacity:.55 }
}

/* 🎹 after the home chord sounds on a fresh hand, the play button says "your turn". Three slow beats,
   then it stops — a control that pulses forever stops meaning anything. */
/* 💡 A CUE NOBODY SEES IS NOT A CUE. The first version moved the button 5.5% and drew a ring at
   16% alpha — invisible on a phone in a lit room, which is the only place it matters. The ring now
   carries real weight and expands as it fades, so the eye catches the MOTION rather than the color,
   and the button lifts enough to read as "your turn" without looking like it has been pressed. */
@keyframes tonicNudge{
  0%     { transform:scale(1);    box-shadow:0 0 0 0   rgba(127,214,219,.55) }
  45%    { transform:scale(1.10); box-shadow:0 0 0 14px rgba(127,214,219,0) }
  100%   { transform:scale(1);    box-shadow:0 0 0 0   rgba(127,214,219,0) }
}
.pg-transport .tonic-nudge{ animation:tonicNudge 1s ease-out infinite }
@media (prefers-reduced-motion:reduce){ .pg-transport .tonic-nudge{ animation:none } }

/* 🪪 a guest name wears the amber, because a name you will not recognize as yours is the one worth
   marking. Nathan has flagged that this color may later carry his Discord role as well, so it is set
   on the NAME itself rather than baked into the badge — one hook, whatever decides it later. */
.navstat.navfree.as-guest .nav-lab{ color:#E8C14A }
.navstat.navfree.as-guest #navFreeT{ color:#E8C14A; opacity:.8 }

/* ── 🚧 THE GEAR OWNS ITS LANE, AND NOTHING MAY GROW INTO IT.
   MEASURED at 390 / 393 / 430: the word "online" was sitting 20–26px ON TOP of the ⚙, and on iPad the
   gear slid sideways whenever the left group's width changed between screens. Both are one fault —
   .bb-left was allowed to grow without limit and .bb-right had no reserved width, so the right group's
   position was a function of the left group's content. (Nathan: "the online users is colliding with the
   settings button" and "when I press the settings menu, the settings button in the menu bar shifts.
   Those should always stay in one spot.")
   Fix the lane, not the symptom: the right group is taken out of the flex negotiation entirely, the left
   group is forbidden from overflowing its own box, and the one piece of the left group that is pure
   decoration — the WORD "online", not the count — steps aside at phone widths where the space isn't
   there. The dot and the number stay, because those are the information. */
.brandbar-v3 .bb-right{ flex:0 0 auto !important; min-width:0; margin-left:auto; position:relative; z-index:3 }
/* ⚠️ NO overflow:hidden HERE. It does not make things fit — it CUTS THEM OFF, and worse, it hides the
   failure from measurement: a clipped element's rectangle stops overlapping, so a collision check comes
   back clean while the thing itself has been sliced out of existence. That is precisely what happened —
   the online count on the phone became a half-drawn green dot with no number, and I reported the bar as
   clean. If content does not fit, the fix is to make it fit, and the overflow is what tells you it does
   not. (Nathan: "did you look?" — the numbers said yes, the screenshot said no.) */
.brandbar-v3 .bb-left{ min-width:0 }
.brandbar-v3 .bb-left > *{ flex:0 0 auto }
/* 📏 WHAT GOES FIRST WHEN THE BAR RUNS OUT: the word MENU, then the word "online".
   Nathan set the order: "if there's not room in the menu bar, the word menu should be the first to go…
   the icon probably does most of the work on its own." He is right that it is the cheapest thing on the
   row — the puzzle piece is the brand and is already a button, and the word buys nothing the icon does
   not already say. Dropping it returns ~45px to the left group, which was the whole shortfall.
   The button keeps its aria-label and title, so nothing is lost to a screen reader or a hover. */
/* MEASURED at 390 with the bigger two-line name block: everything up to the dots ended at 321px, the
   gear began at 338, and the online badge needed 25px that did not exist — so it landed on top of the
   gear. About 40px short. Nathan's own rule decides what goes: the WORDS go before the information.
   "menu" (~45px), "online" (~26px) and "pts" (~20px) are all labels for things that already say what
   they are — a puzzle piece, a green dot, a ⚡. The numbers and the name stay. */
@media (max-width:460px){
  .brandbar-v3 .bb-home-lab{ display:none }
  .brandbar-v3 #navOnline .nav-lab{ display:none }
  .brandbar-v3 #navOnline{ margin-right:2px }
  /* the ⚡ already says these are points */
  .brandbar-v3 #navPointsBtn .nav-lab{ display:none }
}

/* ── 💠 THE HUB SPEAKING. Deliberately NOT the wedge treatment: a ringing wedge wears a white rim, and
   the hub has to read as a different kind of event — the home key, not a chord in the progression.
   Blue, and rings rather than a rim, so the difference is legible without a legend. */
#pgWheelHost.hub-speaking .wh-hub{
  filter:drop-shadow(0 0 10px rgba(96,180,255,.55));
  transition:filter .25s ease;
}
/* the ring itself is animated by SMIL on the element (see hubPing) — CSS only paints it, because
   animating `r` from a stylesheet is unreliable in WebKit and WebKit is what ships here */
.wh-svg .wh-hubwave{
  fill:none; stroke:rgba(96,180,255,.85); stroke-width:1.6; pointer-events:none; opacity:0;
}
@media (prefers-reduced-motion:reduce){
  .wh-svg .wh-hubwave{ display:none }
  #pgWheelHost.hub-speaking .wh-hub{ filter:drop-shadow(0 0 6px rgba(96,180,255,.5)) }
}

/* ── 🤍 YOUR READ DRAWS IN WHITE; AS PLAYED STAYS GREEN.
   They are two different claims about the same hand — the progression in the game's key, and the
   progression in the key he heard it in. Toggling between them must read as a change of subject, and a
   shared color made the two views look like the same picture redrawn. (Nathan: "green for as played,
   whatever you want for your read. I think you have it white.") */
.wh-shape-ans.wh-shape-read{ stroke:rgba(255,255,255,.92) }

/* ── ✉️ A REPLY TO A BUG REPORT. Deliberately plain: this is a person answering a person, and the one
   thing it must not look like is a promotion or an achievement popup. */
.modal.supreply{ max-width:420px }
.supreply-from{ margin:0 0 10px; font-size:12px; letter-spacing:.04em; text-transform:uppercase;
  color:var(--dim, #8aa6b0) }
.supreply-text{ margin:0 0 6px; font-size:15px; line-height:1.55; white-space:pre-wrap }
.modal.supreply .modal-btns a.ghost{ text-decoration:none; display:inline-flex; align-items:center }

/* ── 🐞 THE REPORT INBOX (admin). Built to be read fast: the state, the person, what they said, and the
   three audio facts that decide every no-sound report — without opening a JSON dump. */
.replist{ display:flex; flex-direction:column; gap:10px; margin-top:12px }
.repcard{ background:rgba(255,255,255,.03); border:1px solid var(--line,#1d3540); border-radius:11px; padding:12px 13px }
.rep-head{ display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin-bottom:6px }
.rep-head b{ font-size:14px }
.rep-when{ margin-left:auto; font-size:11px; color:var(--dim,#8aa6b0); font-variant-numeric:tabular-nums }
.rep-st{ font-size:10px; font-weight:800; letter-spacing:.06em; text-transform:uppercase;
  padding:2px 7px; border-radius:999px }
.rep-st.open{ color:#FF6B5A; background:rgba(255,107,90,.14); border:1px solid rgba(255,107,90,.45) }
.rep-st.ans { color:#7fd6db; background:rgba(127,214,219,.13); border:1px solid rgba(127,214,219,.42) }
.rep-st.done{ color:#8aa6b0; background:rgba(138,166,176,.12); border:1px solid rgba(138,166,176,.35) }
.rep-desc{ margin:0 0 6px; font-size:14px; line-height:1.5 }
/* the audio facts, in the order they rule things out */
.rep-sig{ margin:0 0 3px; font-size:11.5px; color:#9ce5b8; font-family:ui-monospace,Menlo,monospace }
.rep-ua{ margin:0 0 9px; font-size:11px; color:var(--dim,#8aa6b0) }
.rep-box{ width:100%; box-sizing:border-box; background:rgba(0,0,0,.25); color:inherit; font:inherit;
  border:1px solid var(--line,#1d3540); border-radius:8px; padding:7px 9px; resize:vertical }
.rep-btns{ display:flex; gap:8px; margin-top:7px }

/* 🧹 a minted wedge this frame has no use for. Hidden rather than removed: the wheel's geometry is laid
   out once, and pulling a wedge out of the ring mid-reveal would move every wedge after it. */
.wh-svg.wh-lit .wh-spent{ opacity:0 !important; pointer-events:none }

/* ── 🥇 THE FIRST CHORD LIGHTS LIKE EVERY OTHER ONE.
   The reveal fades the whole wheel out over 550ms, and the first chord sounds inside that window — so
   its wedge was fading IN while everything around it faded OUT, and the highlight never read. Nathan:
   "the first tonic wedge doesn't light up during playback… every wedge flashes when it plays regardless
   of what number it is in the sequence." The wedge that is ringing opts out of the fade entirely. */
#pgWheelHost.mm-cheshire .wh-svg .wh-w.sounding,
#pgWheelHost.mm-cheshire .wh-svg .wh-w.sounding.mm-shown{
  opacity:1 !important; transition:none !important;
}


/* ── 🔋 THE FREE-TO-PLAY CARD, SHOWN ONCE WHEN SOMEONE SIGNS IN.
   The meter at the top is the real .freedots component under a .navfree parent, so it inherits the
   live colors and the live arriving-dot animation. It is scaled up here — 4px dots read fine as a
   status line in the menu bar and read as dust in the middle of a card — using transform rather than
   new sizes, so there is still exactly one definition of what the meter looks like. */
.freeintro{ text-align:center }
.freeintro .fi-meter{ display:flex; justify-content:center; margin:2px 0 14px }
.freeintro .fi-meter .freedots{ transform:scale(2.2); transform-origin:center; margin:8px 0 }
/* the house h3 (small, dim, uppercase) is kept deliberately — it reads as a label and lets Nathan's
   sentence be the thing you actually read. It also means the heading must never contain the brand
   name: uppercase would flatten "chorDLE!" to "CHORDLE!". */
.freeintro h3{ margin:0 0 10px }
.freeintro .fi-text{ margin:0; font-size:15px; line-height:1.5 }
.freeintro .modal-btns{ justify-content:center; margin-top:16px }


/* 📍 an anchored hint drops from the element that opened it (the menu-bar meter), instead of the
   bottom-of-screen position .pg-hubhint inherits from the trainer's hub. `top` is set inline from the
   anchor's own rect; everything else, including the centring, is unchanged. */
.pg-hubhint.hh-anchored{ bottom:auto }


/* ⚡ the power-up ask, inside the meter hint. It is deliberately quiet: a full-width primary button on
   a status popup reads as a paywall, which is the one thing this is not. */
.pg-hubhint .hh-act{ margin-top:10px; display:flex; flex-direction:column; gap:4px; align-items:flex-start }
.pg-hubhint .hh-act-b{
  background:#1d3b45; color:#9fe6ea; border:1px solid #2f5f6b; border-radius:9px;
  padding:7px 13px; font:700 13px/1 inherit; cursor:pointer; min-height:34px;
}
.pg-hubhint .hh-act-b:active{ background:#245060 }
.pg-hubhint .hh-act small{ color:#8fa8b3; font-size:11.5px }
.pg-hubhint .hh-done{ color:#7fd6db; font-size:12.5px; font-weight:600 }
.pg-hubhint .hh-count{ color:#DCE8EE; font-size:12.5px; font-weight:600 }


/* 🆔 Google sign-in button — Google's brand guidance wants its mark on a neutral surface with the
   wordmark intact, so this deliberately does NOT take the teal treatment the other auth buttons wear. */
.authbtn.googbtn{ background:#fff; color:#1f1f1f; border:1px solid #dadce0; align-items:stretch }
.authbtn.googbtn .goog-row{ display:flex; align-items:center; gap:10px }
.authbtn.googbtn small{ color:#5f6368 }
.authbtn.googbtn .goog-logo{ width:18px; height:18px; flex:0 0 18px }
.authbtn.googbtn:active{ background:#f1f3f4 }
.authbtn.googbtn.mini{ padding:9px 12px; font-size:13.5px; margin-top:8px }


/* 🧱 Apple now carries the same two-part body as the others (icon row + one subtitle), so it has to
   be a column flex like .authbtn rather than the single row it used to be. Matching padding and
   subtitle metrics across all three is what actually makes the three heights agree. */
.siwa-btn{ flex-direction:column; align-items:stretch; gap:2px }
.siwa-btn .goog-row{ display:flex; align-items:center; gap:10px }
.siwa-btn small{ font-weight:400; font-size:12.5px; opacity:.72 }
.siwa-cap{ display:none }   /* the loose caption lives inside the button now */


/* 🪚 the residents' steer. Quiet, above the Discord button — guidance for a group, not a caption on
   a control, so it must not read as loudly as the buttons it sits between. */
.authnote{ margin:10px 0 4px; font-size:12.5px; line-height:1.35; color:#8fa8b3; text-align:left }


/* 🧱 the sign-in block. A quiet panel, not a card — it groups the doors without competing with the
   game tiles underneath, which are the brightest things on that screen and should stay that way. */
/* ⚠ TEMPORARY, BY NATHAN'S OWN DESCRIPTION — the whole sign-in block is a staging post while there
   are three doors to choose between, and it will go once one of them wins. So the shade is deliberately
   a LIFT off the page rather than a new colour in the palette: nothing else has to be re-tuned around
   it, and removing the block later leaves no hole where a theme colour used to be.
   .035 was too close to the page to read as a container at all (Nathan: "put the background a shade
   different to set it off"). This is a touch lighter and very slightly cool, which separates it from
   the warm teal panel behind without competing with the game tiles below. */
.authblock{ background:rgba(180,214,228,.075); border:1px solid rgba(180,214,228,.17); border-radius:14px;
  padding:12px; margin:10px 0 }
.authblock > .authbtn:first-child, .authblock > .siwa-btn:first-child{ margin-top:0 }

/* 🏷 the residents' steer wears the amber the app already uses for "this one is about YOU"
   (the guest name in the menu bar wears the same colour). Nathan asked for a yellow badge behind it. */
.authblock .authnote{
  display:block; margin:10px 0 8px; padding:7px 10px; border-radius:9px;
  background:rgba(232,193,74,.14); border:1px solid rgba(232,193,74,.34); color:#E8C14A;
  font-size:12.5px; line-height:1.35; font-weight:600; text-align:left;
}


/* 📊 the free-plan progress bar on the nudge card. Slim, full width, under the sentence it
   illustrates. Space is RESERVED whatever the fill, so the card is the same height at 0% and 100% and
   the page below it never moves (no layout shift). */
.reps-nudge .reps-bar{ display:block; height:6px; margin:9px 0 1px; border-radius:99px;
  background:rgba(255,255,255,.10); overflow:hidden }
.reps-nudge .reps-bar > i{ display:block; height:100%; border-radius:99px;
  background:#E8C14A; transition:width .5s ease }
.reps-nudge .reps-bar.ok > i{ background:#35C759 }
@media (prefers-reduced-motion:reduce){ .reps-nudge .reps-bar > i{ transition:none } }


/* 🧾 the hand's receipt, on the progress line. The total stays the loud part; the reasons sit
   beneath it, quiet and small, so the line reads as one figure with its working shown rather than a
   row of competing numbers. It wraps rather than truncating — a reason you cannot finish reading is
   the problem this replaces. */
.pg-pot.paid .pot-why{
  display:block; font-style:normal; font-weight:600; font-size:10.5px; line-height:1.3;
  color:#8fa8b3; margin-top:1px; white-space:normal;
}
@media (max-width:400px){ .pg-pot.paid .pot-why{ font-size:10px } }


/* ➿ THE LEGS RAMP, so a path that crosses itself can be read and its direction is obvious without an
   arrowhead. Earliest leg quietest and thinnest; the last one full strength. They are separate
   elements painted in order, so the later leg genuinely lies OVER the earlier one at a crossing. */
.wh-shape-ans[data-leg="0"]{ opacity:.42; stroke-width:2.2 }
.wh-shape-ans[data-leg="1"]{ opacity:.62; stroke-width:2.5 }
.wh-shape-ans[data-leg="2"]{ opacity:.82; stroke-width:2.8 }
.wh-shape-ans[data-leg="3"]{ opacity:1;   stroke-width:3.1 }


/* ── 🧾 THE HAND'S SCORE, SOMEWHERE IT CAN ACTUALLY BE SEEN.
   Every rule for this element was scoped to `#pgLvlBtn`, which pinned it absolutely into a corner at
   opacity 0 with white-space:nowrap — a 14px figure tucked behind the level panel with no room for a
   reason. Nathan asked three separate times tonight where his points were, which is the answer.
   These rules are UNSCOPED and come later, so they win wherever the element renders: it sits in normal
   flow, it is opaque, and it may wrap. Nothing above is deleted — if the corner placement is still
   wanted somewhere, it still applies until this overrides it. */
.pg-pot{
  position:static !important; opacity:1 !important; transform:none !important;
  display:block; white-space:normal; margin:6px 0 0;
  font-style:normal; font-weight:800; font-size:14px; letter-spacing:.01em;
  color:#e8c14a; font-variant-numeric:tabular-nums;
  text-shadow:0 1px 3px rgba(6,18,22,.85);
}
/* ⚠ NO :empty RULE HERE. It hid the element whenever the panel rendered before an award existed —
   which is every hand, because the panel does not redraw on grading. Once hidden it could only come
   back if something else forced a render, which is exactly what Nathan described: "it's never showing
   unless I take an additional action." An empty <i> occupies nothing anyway. */
.pg-pot.paid{ color:#7fd6db }
.pg-pot .pot-why{
  display:block; font-style:normal; font-weight:600; font-size:11px; line-height:1.35;
  color:#8fa8b3; margin-top:2px; white-space:normal;
}


/* 🧾 the hand's award, in an element this code creates and owns — see potPaint(). Centred above the
   wheel, big enough to read at a glance, with its reason beneath in a quieter weight. */
/* the floating sticker is gone — the award prints in the card (see .pg-pot above). */


/* ── 🐞 THE REPORT BUTTON MUST NOT SIT ON THE MICROPHONE.
   It is absolutely positioned at the left edge of the transport, which is exactly where the mic lives
   when the app can listen — so on a device with listening available the two overlap and the mic is
   unreachable. (Nathan: "the bug report button is colliding with the microphone.")
   Offset it only when a live mic is actually present: on the web, and on app builds without listening,
   the mic is absent or inert and the report keeps the left edge to itself. `:has` is supported on the
   iOS this ships to; where it is not, nothing moves and the layout is exactly as it was. */
/* ⚠ The first attempt scoped this to a mic INSIDE the transport. It is not — the mic is anchored
   separately to the bottom of the screen (see its own note above), so the selector matched nothing and
   the two stayed on top of each other. An unconditional offset cannot miss: where there is no mic the
   report simply sits a little further in, which costs nothing. */
/* ── 🐞 SHORTER, AND OUT FROM UNDER THE MIC.
   Two attempts at nudging this sideways both landed it ON the mic instead of beside it — shipped
   without once measuring the result. Nathan: "are you fucking watching what you're doing?" No.
   The label is now "bug" rather than "report", which is what he asked for and also what makes the
   button narrow enough to sit beside the mic instead of under it. */
/* ── 🐞 NO PILL. Two round-ish buttons of different sizes sitting side by side read as a mistake
   rather than a pair — Nathan: "looks better but it's ugly. The balance is wrong. Maybe just print the
   ladybug button without the circle."
   Stripping the background and border leaves the ladybug and its word floating on the page, so the
   MIC is the only circle down there and there is nothing for it to look mismatched against. */
/* ── 🐞 A SMALL SQUARE, WORD OVER BUG. Nathan sketched it: the label and the ladybug stacked fit
   in a square, so it can wear a border again without becoming a wide pill that crowds the mic.
   Stacked and shrunk it is ~36px against the mic's 46 — a pair of small controls rather than a long
   lozenge beside a circle, which is what read as unbalanced. */
.pg-bugb{ left:0 !important; width:36px !important; height:36px !important;
          flex-direction:column !important; gap:0 !important; padding:2px 0 !important;
          border-radius:10px !important; font-size:13px !important; line-height:1 !important;
          background:rgba(255,255,255,.05) !important; border:1px solid var(--line) !important; }
.pg-bugb::after{ content:"bug" !important; margin-left:0 !important; font-size:8px !important;
                 line-height:1 !important; margin-top:1px !important; }


/* 🔋📱 device and free-meter beside a name in Right Now. Both are context, not headlines, so they
   stay small and quiet — the name is still the thing you read first. */
/* 📱💻 the device, big enough to recognise. 13px rendered as an unreadable smudge — Nathan had
   to zoom in ten times. Emoji need real size or they say nothing at all. */
.fdev{ font-size:16px; line-height:1; vertical-align:-2px; margin-right:4px; opacity:.9 }
/* ⚠ A SINGLE DOT IS NOT A METER. Trimming four dots to one to save width threw away the only thing
   it was there to show — how far along someone is. Nathan: "I still cannot see any charge up other
   than one dot." The dots come back; the width comes from dropping the decorative wave instead. */
.fchg{ display:inline-flex; align-items:center; gap:2px; vertical-align:middle; margin-right:4px }
.fchg > i{ width:4px; height:4px; border-radius:50%; background:rgba(255,255,255,.20) }
.fchg.t0 > i.lit{ background:#FF6B5A }
.fchg > i.lit{ background:#35C759 }
.fchg.t3 > i.lit{ background:#7fd6db }


/* ⚠ I hid the WORKING wave here and left the decorative one. Reverted; the emoji in the row text
   is the one that goes (see app.js). */


/* ── ⬅ BREATHING ROOM BEFORE THE NAME. The identity dot sat hard against the first letter and read as
   part of it (Nathan: "there needs to be more space between the beginning of my name and what
   precedes it"). Spacing the badge rather than the name keeps the row's own rhythm intact. */
.feedrow .dbadge, .feedrow .rbadge{ margin-right:7px }
/* the device now travels with the meter at the end of the row */
.feedrow .fdev{ margin-right:5px }





   overflow — so the rolling tip rendered at full opacity inside a 0px box and was invisible.
   Measured on device: h:0, hostH:0, overflow:hidden, opacity:1 (2026-09-30). Reserve the strip's
   normal height while a tip is up, and let the tip stand on its own rather than inherit zero. */

/* The tip TAKES THE PLACE of the prompt rather than drawing over it — overlaying the two made them
   collide (Nathan, 2026-10-01). The prompt returns the instant the tip stands down or voice goes off,
   and its space is never left empty, so nothing shifts either way. */
   so one line occupies that space instead of two overlapping ones. */



/* 🧾 the games behind a Recent-points figure, under the name. Small and quiet — it answers
   "where did that come from" without competing with the number itself. (Nathan, 2026-10-01:
   "there's no way for me to see which games generated those points. Not even as admin.") */
.lbrecent .lbname{ display:flex; flex-direction:column; align-items:flex-start; gap:1px; min-width:0 }
.lbrecent .lbsrc{ white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:100% }
.lbrecent .lbdelta{ margin-left:6px }

/* 🎤 the microphone button answers the press before the hardware does. Acquiring the mic on iPad is
   not instant, and a control that looks unchanged gets pressed again — which, being a toggle, undid
   what Nathan wanted. (He asked for "a message saying that the mic is activating so I know pressing
   it did something".) 'listening' takes over the moment the recorder really starts. */
.pg-micb.waking{ animation: micWaking .9s ease-in-out infinite; }
@keyframes micWaking{ 0%,100%{ opacity:.55 } 50%{ opacity:1 } }
@media (prefers-reduced-motion:reduce){ .pg-micb.waking{ animation:none; opacity:.8 } }

/* 👥 RIGHT NOW SCROLLS INSIDE ITS CARD rather than being cut to six with no indication that anyone
   else is on. (Nathan: "make the right now list scrollable inside of its container.") Same scroll
   treatment the clue column already uses — momentum scrolling, vertical pans routed to the list, and
   a height that stops it pushing the rest of the page down. */
.landfeed{ max-height:min(44vh, 320px); overflow-y:auto; -webkit-overflow-scrolling:touch; touch-action:pan-y }
.landfeed::-webkit-scrollbar{ width:6px }
.landfeed::-webkit-scrollbar-thumb{ background:rgba(255,255,255,.16); border-radius:3px }
.landfeed .landfeed-h{ position:sticky; top:0; z-index:2; background:var(--panel,#10232b); padding-bottom:4px }

/* ⚙ Settings v2 (2026-10-02): one row component, grouped cards. Scoped to .setv2 so other .setup grids are untouched. */
.setv2 h3{margin:22px 0 6px}
.setv2 .sgroup{background:rgba(255,255,255,.03);border:1px solid var(--line);border-radius:12px;padding:0 14px;margin:0 0 4px}
.setv2 .srow{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:11px 0;border-top:1px solid var(--line);min-height:44px;box-sizing:border-box;cursor:default}
.setv2 label.srow{cursor:pointer}
.setv2 .sgroup > .srow:first-child,.setv2 .sgroup > .slink:first-child,.setv2 details.sgroup > summary + *{border-top:0}
.setv2 .srow > .sl{color:var(--ink);font-size:15px;flex:1 1 auto;min-width:0;text-align:left}
.setv2 .srow > .sl small{display:block;color:var(--dim);font-size:12px;margin-top:2px;font-weight:400}
.setv2 .srow > .sc{flex:0 1 auto;display:flex;gap:8px;align-items:center;flex-wrap:wrap;justify-content:flex-end;min-width:0;max-width:65%}
.setv2 .srow.stack{flex-direction:column;align-items:stretch;gap:8px}
.setv2 .srow.stack > .sc{max-width:none;justify-content:flex-start}
.setv2 .srow input[type=checkbox]{width:22px;height:22px;flex:0 0 auto;margin:0}
.setv2 .srow select{width:auto;max-width:100%;background:#0a1a20;border:1px solid var(--line);color:var(--ink);border-radius:8px;padding:8px 10px;font-size:15px}
.setv2 .srow .nameinput{max-width:100%;width:190px}
.setv2 .slink{display:block;width:100%;text-align:left;padding:13px 0;border:0;border-top:1px solid var(--line);background:none;color:var(--ink);font:inherit;font-size:15px;cursor:pointer}
.setv2 details.sgroup > summary{list-style:none;cursor:pointer;padding:13px 0;color:var(--ink);font-size:15px;font-weight:700}
.setv2 details.sgroup > summary::-webkit-details-marker{display:none}
.setv2 details.sgroup > summary::after{content:'\203A';float:right;color:var(--dim);transition:transform .15s}
.setv2 details.sgroup[open] > summary::after{transform:rotate(90deg)}
.setv2 details.sgroup[open] > summary{border-bottom:1px solid var(--line)}
.setv2 details.sgroup{margin-top:18px}
.setv2 .setseg{gap:6px}
.setv2 .setlag{display:block;max-width:100%;overflow-x:auto}
.setv2 .lvlpick{width:100%;min-width:0}
.setv2 .lvlpick select{min-width:0;width:auto;flex:1 1 0}
.setv2 label.srow > .sc{flex-wrap:nowrap}
.setv2 .tbrow{display:flex;gap:8px;width:100%}
.setv2 .tbrow .pmode{flex:1 1 0;padding:10px 0;font-size:14px}
.setv2 .srow > .sc select{max-width:100%}
.setv2 .srow:not(.stack) > .sc select{max-width:190px}

/* 💬 hands-free command tip, briefly OVER the message line (2026-10-02) — the line's own text stays underneath */
.mm-ask{ position:relative }
.mm-ask.tipping > :not(.mm-tipover){ visibility:hidden }
.mm-tipover{ position:absolute; inset:0; display:flex; align-items:center; justify-content:center; text-align:center;
  color:var(--accent2,#7fd3e0); font-size:inherit; font-style:italic; background:var(--panel,#13303a); border-radius:8px; animation:tipin .25s ease-out }
@keyframes tipin{ from{ opacity:0 } to{ opacity:1 } }

/* ✍️ the out-of-games notice leads with the deal (2026-10-02): big, bold, green, one glow + one small shake */
.ocmodal .oclead{ font-size:21px; font-weight:800; line-height:1.25; color:var(--good,#35C759); margin:2px 0 10px;
  animation: ocglow 1.8s ease-out 1, ocshake .55s ease-in-out .25s 1 }
.ocmodal .ocpath{ font-size:15px; margin:4px 0 }
.ocmodal .ocfree{ font-weight:700; color:var(--ink); margin-top:10px }
@keyframes ocglow{ 0%{ text-shadow:0 0 0 rgba(53,199,89,0) } 30%{ text-shadow:0 0 18px rgba(53,199,89,.9) } 100%{ text-shadow:0 0 0 rgba(53,199,89,0) } }
@keyframes ocshake{ 0%,100%{ transform:translateX(0) } 20%{ transform:translateX(-4px) } 40%{ transform:translateX(4px) } 60%{ transform:translateX(-3px) } 80%{ transform:translateX(2px) } }
@media (prefers-reduced-motion:reduce){ .ocmodal .oclead{ animation:none } }

/* ⚡ CHARGE CARD + METER PULSE (2026-10-03): where you stand on the 3-days-in-a-row deal, and the day counting */
.chgcard{position:fixed;left:50%;top:calc(70px + var(--lbh,0px));transform:translateX(-50%) translateY(-10px);width:min(92vw,380px);box-sizing:border-box;background:var(--panel2,#16323a);border:1px solid #35C759;border-radius:14px;padding:10px 14px;z-index:90;opacity:0;transition:opacity .25s ease,transform .25s ease;box-shadow:0 8px 24px rgba(0,0,0,.35);display:flex;flex-direction:column;gap:2px;cursor:pointer}
.chgcard.show{opacity:1;transform:translateX(-50%) translateY(0)}
.chgcard b{color:#fff;font-size:15px}
.chgcard span{color:#cfe3e8;font-size:13px;line-height:1.35}
.chgcard.big{border-color:#e8c14a;box-shadow:0 0 0 2px rgba(232,193,74,.25),0 10px 30px rgba(0,0,0,.4)}
.chgcard.big b{color:#ffd666;font-size:17px}
#navFree.chgup .freedots > i{animation:chgDot .5s ease-out both;background:#35C759}
#navFree.chgfull .freedots > i{animation:chgDot .5s ease-out both;background:#ffd666;box-shadow:0 0 6px #ffd666}
#navFree .freedots > i:nth-child(2){animation-delay:.12s}
#navFree .freedots > i:nth-child(3){animation-delay:.24s}
#navFree .freedots > i:nth-child(4){animation-delay:.36s}
@keyframes chgDot{0%{transform:scale(1)}45%{transform:scale(2.1)}100%{transform:scale(1)}}
.ocbuy{display:flex;align-items:center;justify-content:space-between;gap:10px;text-align:left}
.ocbuy-l{display:flex;flex-direction:column;font-weight:700}
.ocbuy-l small{font-weight:500;font-size:12px;opacity:.85}
.ocbuy-p{display:flex;flex-direction:column;align-items:flex-end;line-height:1.1}
.ocbuy-p s{font-size:12px;opacity:.7}
.ocbuy-p b{font-size:17px}
.ocstay{margin:8px 0 0;font-size:13.5px;line-height:1.4}
.ocused{font-size:11.5px;opacity:.62;margin-top:10px}
.ocgift{margin:8px 0 0;font-size:11.5px;line-height:1.35;opacity:.72;text-align:center}
/* 🪧 dismissable charge card (2026-10-04) */
.chgmodal{text-align:center;border:1px solid #35C759;max-width:360px}
.chgmodal.big{border-color:#e8c14a;box-shadow:0 0 0 2px rgba(232,193,74,.25),0 12px 34px rgba(0,0,0,.45)}
.chgm-t{font-size:24px;font-weight:800;color:#35C759;margin:4px 0 8px}
.chgmodal.big .chgm-t{color:#ffd666}
.chgm-d{font-size:16px;line-height:1.4;margin:0 0 16px;color:#e6f1f3}
.chgmodal .primary{width:100%}
.flypts.flybig{font-size:26px;color:#ffd666;text-shadow:0 0 10px rgba(255,214,102,.55),0 2px 6px rgba(0,0,0,.7)}
/* 🧭 level list: where you are (2026-10-04) */
.lvlmenu-lvl.cur{background:rgba(45,172,179,.42)!important;border:2px solid #7fd6db!important;color:#fff;box-shadow:0 0 0 3px rgba(127,214,219,.18)}
.lvlmenu-lvl.cur::after{content:none!important}
.lvlmenu-here{float:right;margin-left:8px;padding:2px 8px;border-radius:999px;background:#7fd6db;color:#0d2a30;font-size:10.5px;font-weight:900;letter-spacing:.04em}
.lvlmenu-yours{float:right;margin-left:8px;padding:2px 8px;border-radius:999px;border:1px solid #ffd666;color:#ffd666;font-size:10.5px;font-weight:800;letter-spacing:.04em}
.lvlmenu-lvl.ahead{opacity:.5;font-weight:500}
.lvlmenu-lvl.ahead:hover,.lvlmenu-lvl.ahead:active{opacity:.85}
.lvlmenu-go{float:right;margin-left:8px;font-size:18px;line-height:1;opacity:.9}
/* 🖼️ MAIN MENU FRAME (Nathan, 2026-10-04: "wrapped inside of a rounded lighter colored container that ends at the top with no
   padding between it and the menu bar… I don't want to waste any space"). On a phone the frame holds nothing together —
   the cards are already their own shapes — so it goes, and its 22px of padding is given back. On a wide screen it stays as
   a frame, with air above it so it no longer butts into the bar. */
@media (max-width: 699px){
  body.at-menu #app > .panel{ background:transparent; border:0; border-radius:0; padding:8px 2px 0; box-shadow:none }
}
@media (min-width: 700px){
  body.at-menu #app > .panel{ margin-top:14px }
}
.lvlmenu-here.lvlmenu-both{background:#ffd666;color:#2a2008}
#pgPot.pump{animation:navpump .3s ease-in-out 2;color:#ffd666}
/* 👥 friends (2026-10-04) */
.frbadge{display:inline-block;min-width:18px;padding:0 6px;margin-left:6px;border-radius:999px;background:#e8604c;color:#fff;font-size:12px;font-weight:800;line-height:18px;text-align:center;vertical-align:middle}
.frrow{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:10px}
.frok{color:#35C759;font-weight:800}
.frnote{display:block;margin-top:10px}
.frlist{display:flex;flex-direction:column;gap:10px}
.frcard{background:var(--panel2,#16323a);border:1px solid var(--line);border-radius:12px;padding:12px}
.frtop{display:flex;align-items:center;gap:8px}
.frtop small{margin-left:auto}
.frrecent{display:block;margin-top:6px;color:#ffd666}
.frbtns{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-top:10px}
.frreact .remo{font-size:20px;background:transparent;border:1px solid var(--line);border-radius:10px;padding:4px 8px;margin-right:4px}
.frreact .remo.sent{opacity:.45}
.frprefs{display:flex;flex-direction:column;gap:10px}
.frpref{display:flex;gap:10px;align-items:flex-start}
.frpref small{display:block;opacity:.75;font-weight:400}
.frpref input{margin-top:4px;transform:scale(1.2)}
.frpref input[type=checkbox]{flex:0 0 auto;width:20px!important;height:20px;margin:3px 4px 0 0}
.frpref span{flex:1 1 auto;text-align:left}
.frprefs .frpref{display:grid!important;grid-template-columns:28px 1fr!important;column-gap:10px;align-items:start;justify-items:start;text-align:left;width:100%}
.flypts.flypop{font-size:30px;animation:flypop 1s ease-out both}
@keyframes flypop{0%{transform:scale(.5);opacity:0}25%{transform:scale(1.6);opacity:1}45%{transform:scale(1.35)}100%{transform:scale(1.4);opacity:1}}
.frchip{margin-left:auto;margin-right:6px;padding:4px 9px;border-radius:999px;border:1px solid var(--accent2,#2dacb3);background:transparent;color:var(--ink);font-size:12px;font-weight:700;white-space:nowrap;cursor:pointer}
.frchip.sent{border-style:dashed;opacity:.65;cursor:default}
/* the name gets room on wide screens (2026-10-04: "no reason for it to truncate my username when it looks like this") */
@media (min-width:700px){ .navstat.navfree{ width:260px; max-width:260px; flex:0 0 260px } .navstat.navfree .nav-lab{ max-width:260px } }

/* The Sing streak, styled like the Chord Trainer's own counters rather than a new look.
   Cold (no current run) is muted so a lapsed best does not shout. */

/* ── SING IT BACK WEARS THE CHORD TRAINER'S SKIN — by USING ITS CSS, not by copying it.
   The lookalike that stood here (.singpicker/.lvlpip/.singprog/.singbar/.singpips/.singstkmeter) is
   gone: Sing now renders .hostbar.pg-hostbar > .lvlmenu-btn + rungBarHTML(), so every rule above
   already applies and restyling the trainer restyles Sing. (Nathan: "design this stuff once and
   update it everywhere going forward.")
   ⚠️ It also took .singbar with it, which was ALREADY the live tuner's fill bar (see ~line 1397) —
   my 5px hairline was overriding a 16px meter that two screens depend on. */


/* 🔋 the decaying charge + its direction, on every leaderboard row (Nathan, 2026-10-08).
   Quiet by default: this sits beside a name and must not out-shout it. The arrow carries the
   meaning, so the colour is on the arrow and the number stays neutral. */
.lbchg{margin-left:7px;font-size:11.5px;font-weight:700;color:var(--dim);white-space:nowrap;font-variant-numeric:tabular-nums}
.lbchg .chgarr{font-style:normal;font-size:9px;margin-left:2px;vertical-align:1px}
.lbchg.up .chgarr{color:var(--good)}
.lbchg.dn .chgarr{color:var(--accent2);opacity:.75}

/* 🎨 the one charge card that is bad news does not wear the celebration colour (2026-10-08).
   Amber, matching the meter's own low state — concerned, not alarmed: nothing has been lost. */
.chgmodal.chg-warn{border-color:rgba(232,193,74,.55)}
.chgmodal.chg-warn .chgm-t{color:#e8c14a}

/* 📜 THE CONTRACT — the first thing a new player reads, and the only time they read it.
   Deliberately plainer than the celebration cards: this is an agreement, not a prize. No gold,
   no confetti, no struck price. The promise is the biggest line; the price is not mentioned at
   all, because the headline is the gift (Nathan, 2026-10-10). */
.contractcard{max-width:340px;text-align:center}
.contractcard .ct-h{font-size:13px;font-weight:800;letter-spacing:.2em;text-transform:uppercase;
  opacity:1;margin-bottom:9px;color:var(--accent2,#7fd6db);
  position:relative;display:inline-block;overflow:hidden}
.contractcard .ct-h > span{position:relative;z-index:1}
/* the collectible glint — a real band moved by transform, because WKWebView will not repaint an
   animated background-position on clipped text (the dots animate opacity, which is why they work) */
.contractcard .ct-h::after{
  content:'';position:absolute;top:-40%;bottom:-40%;left:0;width:46%;z-index:2;pointer-events:none;
  background:linear-gradient(90deg,rgba(255,255,255,0),rgba(255,255,255,.7) 42%,rgba(255,255,255,1) 50%,rgba(255,255,255,.7) 58%,rgba(255,255,255,0));
  mix-blend-mode:overlay;
  transform:translateX(-200%) skewX(-18deg);
  animation:ctGlint 4s ease-in-out infinite;
}
/* one pass, then a long rest — see shineSwipe: a rare bright pass reads as a glint, constant motion reads as a worm */
@keyframes ctGlint{
  0%        { transform:translateX(-200%) skewX(-18deg) }
  26%, 100% { transform:translateX(330%)  skewX(-18deg) }
}
@media (prefers-reduced-motion:reduce){ .contractcard .ct-h::after{ animation:none; opacity:0 } }
.contractcard .ct-lead{font-size:17px;font-weight:800;line-height:1.25;margin:0 0 7px;
  color:var(--good);text-wrap:balance}
.contractcard .ct-body{font-size:13px;line-height:1.42;margin:0 0 7px;opacity:.92;text-wrap:balance}
.contractcard .ct-foot{font-size:11.5px;line-height:1.4;margin:0 0 11px;opacity:.6;text-wrap:balance}
.contractcard .big{width:100%;text-align:center;justify-content:center;align-items:center;display:flex}
.contractcard .ct-status{display:flex;flex-direction:column;align-items:center;gap:4px;
  margin:0 0 10px;padding:9px 12px;border-radius:10px;background:rgba(255,255,255,.045);
  border:1px solid rgba(255,255,255,.09)}
.contractcard .ct-now{font-size:13px;font-weight:700;opacity:.95;text-wrap:balance;margin-top:1px}
.contractcard .ct-status .navfree{display:inline-flex;align-items:center;justify-content:center;
  transform:scale(1.4)}   /* the real meter, larger while being introduced. The display is NOT optional:
     outside the brand bar the meter has no layout of its own and collapses to a block, left-aligning its dots. */
.contractcard .ct-meterrow{display:flex;align-items:center;justify-content:center}
.contractcard .ct-name{font-size:11px;font-weight:800;letter-spacing:.14em;text-transform:uppercase;opacity:.6;order:-1}   /* label ABOVE the dots */
/* no animation here on purpose: the cloned meter brings its own, and it is the approved one */

.contractcard .ct-again{display:flex;align-items:center;justify-content:center;gap:7px;
  font-size:12px;opacity:.6;margin:0 0 9px;cursor:pointer}
.contractcard .ct-again input{margin:0;cursor:pointer}
/* the button is 55px of mostly padding in a card we are trying to shorten */
.contractcard .primary.big{padding:11px 14px;font-size:15px}

/* ── ♿ THE ROOT MENU (2026-10-11) ───────────────────────────────────────────────────────────────
   Angela, a blind player: "there's NO way for Voiceover users to change the root of the chord!"
   The root lives on the wheel's hub behind a 340ms hold and then a DRAG, and VoiceOver can do
   neither. This is the second door Nathan asked for — real buttons, reachable by swipe and
   activated by a double-tap, which is a plain click. Deliberately ordinary DOM, not SVG, so a
   screen reader treats it as the list it is.
   It sits under the wheel rather than over it: the builder's fan is the thing being read at that
   moment and nothing should cover it. */
.cb-rootwrap{position:fixed;left:50%;transform:translateX(-50%);
  bottom:calc(14px + env(safe-area-inset-bottom, 0px));z-index:2;
  display:flex;flex-direction:column-reverse;align-items:center;gap:8px;pointer-events:auto}
.cb-rootbtn{appearance:none;border:1px solid var(--line,#2a4a52);background:var(--panel2,#16323a);
  color:var(--ink,#eef4f5);font-size:12px;font-weight:700;letter-spacing:.04em;
  padding:7px 14px;border-radius:999px;cursor:pointer;white-space:nowrap;
  box-shadow:0 2px 10px rgba(0,0,0,.35)}
.cb-rootbtn b{color:var(--accent2,#7fd6db);margin-left:4px}
.cb-rootbtn:focus-visible{outline:2px solid var(--accent2,#7fd6db);outline-offset:2px}
.cb-rootmenu{display:grid;grid-template-columns:repeat(4,minmax(52px,1fr));gap:5px;
  padding:8px;border-radius:12px;background:var(--panel2,#16323a);
  border:1px solid var(--line,#2a4a52);box-shadow:0 8px 28px rgba(0,0,0,.5);max-width:92vw}
.cb-rootitem{appearance:none;border:1px solid transparent;border-radius:8px;cursor:pointer;
  background:rgba(255,255,255,.06);color:var(--ink,#eef4f5);
  font-size:15px;font-weight:700;padding:11px 6px;min-height:44px}   /* 44px — a real touch target */
.cb-rootitem[aria-selected="true"]{border-color:var(--accent2,#7fd6db);color:var(--accent2,#7fd6db)}
.cb-rootitem:focus-visible{outline:2px solid var(--accent2,#7fd6db);outline-offset:2px}
