/* ════════════════════════════════════════════════════════════
   CSS VARIABLES — colours, tile sizes (JS updates tile sizes on resize)
   ════════════════════════════════════════════════════════════ */
:root{
  --jade:#1a6b45;--jl:#2a9e67;--jd:#0d3d28;
  --gold:#c9a84c;--gl:#f0d080;--gd:#8a6b20;--cream:#f5edd8;
  /* The game's backdrop — jade glow low-left, gold glow high-right over deep
     green. Shared by the title screen's .bg, the win cutscene (.cs-dim) and
     the standings card (#s-end.re-standings-screen). */
  --bg-theme:radial-gradient(ellipse at 20% 85%,rgba(26,107,69,.3),transparent 55%),radial-gradient(ellipse at 80% 15%,rgba(201,168,76,.14),transparent 55%),radial-gradient(ellipse at 50% 50%,rgba(12,35,20,.85),#040a05 100%);
  /* Responsive tile sizes via CSS vars — JS updates these on resize */
  --tlg-w:40px;--tlg-h:56px;
  --tmd-w:28px;--tmd-h:36px;
  --tsm-w:16px;--tsm-h:22px;
}
/* ════ RESET & BASE ════ */
*{margin:0;padding:0;box-sizing:border-box;}
html,body{height:100%;overflow:hidden;}
body{background:#060c07;color:var(--cream);font-family:'Noto Sans JP',sans-serif;user-select:none;-webkit-user-select:none;touch-action:manipulation;}
/* Themed scrollbar — replaces the OS-default bar with the gold-on-dark
   look everywhere content can scroll (About/User Profile/etc. modals via
   .about-modal, the Settings side panel, in-game chat history). Global
   (`*`) rather than listing each scrollable selector individually so any
   future scrolling container picks it up automatically; harmless on
   non-scrolling elements since these only render where a scrollbar
   actually appears. Firefox uses the scrollbar-width/color shorthand;
   everything Chromium/WebKit-based (including this app's target browsers)
   uses the ::-webkit-scrollbar-* pseudo-elements instead. */
*{scrollbar-width:thin;scrollbar-color:rgba(201,168,76,.5) rgba(0,0,0,.3);}
*::-webkit-scrollbar{width:8px;height:8px;}
*::-webkit-scrollbar-track{background:rgba(0,0,0,.3);}
*::-webkit-scrollbar-thumb{background:rgba(201,168,76,.5);border-radius:4px;}
*::-webkit-scrollbar-thumb:hover{background:var(--gold);}
/* ════ BACKGROUND LAYERS ════ */
.bg{position:fixed;inset:0;z-index:0;background:var(--bg-theme);}
.screen{position:fixed;inset:0;z-index:10;display:flex;align-items:center;justify-content:center;}
.screen.hidden{display:none!important;}
#s-end.hidden{display:none!important;}

/* ════════════════════════════════════════════════════════════
   LOBBY SCREEN
   ════════════════════════════════════════════════════════════ */
/* Same clamped scale-unit approach as #s-start (see .logo-k/#bstart notes
   below) — every font-size/letter-spacing/padding in the hall-entry card
   multiplies this ONE variable, so they all stay proportional to each
   other at any resolution instead of each hitting its own px cap at a
   different screen size. */
#s-lobby{--ui-vmin:clamp(6px,1vmin,10px);flex-direction:column;gap:18px;}
.logo{text-align:center;animation:float 5s ease-in-out infinite;}
@keyframes float{0%,100%{transform:translateY(0)}50%{transform:translateY(-8px)}}
/* Plain vmin (no clamp min/max), same fix as the game banners: logo-k was
   capped at 72px above a 720px-wide viewport while logo-s kept scaling on
   vw until 560px — in between, the two drifted out of proportion with each
   other. Coefficients chosen so a ~720px-vmin screen (e.g. 1280x720) still
   renders at the old capped 72px/14px look. */
.logo-k{font-family:'Noto Serif JP',serif;font-size:10vmin;font-weight:900;letter-spacing:1.7vmin;
  background:linear-gradient(135deg,#ffe680,#c9a84c,#8a6b20);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;color:transparent;filter:drop-shadow(0 0 24px rgba(201,168,76,.7));}
/* Start-press kanji burst — a brief brighten+scale flash on the 雀心
   characters, layered on top of the screen-out fade (see enterLobby()).
   Lives on .logo-k (not .logo, which already owns the infinite float
   animation) so the two animations don't fight over the shorthand. */
.logo-k.burst{animation:logoBurst .45s ease;}
@keyframes logoBurst{
  0%{filter:drop-shadow(0 0 24px rgba(201,168,76,.7));transform:scale(1);}
  40%{filter:drop-shadow(0 0 48px rgba(255,224,102,1)) brightness(1.3);transform:scale(1.1);}
  100%{filter:drop-shadow(0 0 24px rgba(201,168,76,.7));transform:scale(1);}
}
.logo-s{font-family:'Cinzel Decorative',serif;font-size:2vmin;color:var(--jl);letter-spacing:1.1vmin;margin-top:2px;}
.card{background:linear-gradient(155deg,rgba(20,55,35,.95),rgba(8,20,12,.98));border:1px solid rgba(201,168,76,.12);border-radius:0;
  --pg:calc(var(--ui-vmin) * 5.5);--pw:calc(var(--ui-vmin) * .2);
  padding:calc(var(--ui-vmin) * 4.5) calc(var(--ui-vmin) * 6.9);
  /* Was a flat min(440px,96vw) — the box stayed capped at 440px while its
     own font-size/padding kept scaling up to --ui-vmin's 10px ceiling, so
     on a 4K screen the (now larger) text read as cramped into a box that
     never grew. Tying width to the same --ui-vmin (61.11 * 7.2 = 440, so
     it still renders at 440px at the same ~720px-vmin reference screen)
     lets it grow in step with its own content instead of stalling. */
  width:min(calc(var(--ui-vmin) * 61.11), 96vw);
  box-shadow:0 0 80px rgba(0,0,0,.7),inset 0 1px 0 rgba(201,168,76,.25);position:relative;overflow:hidden;}
/* Diagonal-stagger gold frame, same as #bstart and the .about-modal family
   — the lobby card doesn't scroll internally, so ::before/::after at the
   edge work with no flex refactor. ::before = outer rule cut top-left +
   bottom-right, ::after = inner rule cut the other two. Replaces the old
   gradient top-bar. */
.card::before,.card::after{content:'';position:absolute;pointer-events:none;z-index:2;background-repeat:no-repeat;}
.card::before{inset:0;
  background-image:linear-gradient(115deg,#b5903f 0%,#ffe9b0 48%,#cfa74d 100%);
  -webkit-mask:
    linear-gradient(90deg,transparent 0 var(--pg),#000 var(--pg)) top/100% var(--pw) no-repeat,
    linear-gradient(90deg,#000 0 calc(100% - var(--pg)),transparent calc(100% - var(--pg))) bottom/100% var(--pw) no-repeat,
    linear-gradient(180deg,transparent 0 var(--pg),#000 var(--pg)) left/var(--pw) 100% no-repeat,
    linear-gradient(180deg,#000 0 calc(100% - var(--pg)),transparent calc(100% - var(--pg))) right/var(--pw) 100% no-repeat;
  mask:
    linear-gradient(90deg,transparent 0 var(--pg),#000 var(--pg)) top/100% var(--pw) no-repeat,
    linear-gradient(90deg,#000 0 calc(100% - var(--pg)),transparent calc(100% - var(--pg))) bottom/100% var(--pw) no-repeat,
    linear-gradient(180deg,transparent 0 var(--pg),#000 var(--pg)) left/var(--pw) 100% no-repeat,
    linear-gradient(180deg,#000 0 calc(100% - var(--pg)),transparent calc(100% - var(--pg))) right/var(--pw) 100% no-repeat;}
.card::after{inset:calc(var(--ui-vmin) * .6);opacity:.7;
  background-image:linear-gradient(115deg,#a37f35 0%,#f4e0ac 48%,#bd9740 100%);
  -webkit-mask:
    linear-gradient(90deg,#000 0 calc(100% - var(--pg)),transparent calc(100% - var(--pg))) top/100% var(--pw) no-repeat,
    linear-gradient(90deg,transparent 0 var(--pg),#000 var(--pg)) bottom/100% var(--pw) no-repeat,
    linear-gradient(180deg,#000 0 calc(100% - var(--pg)),transparent calc(100% - var(--pg))) left/var(--pw) 100% no-repeat,
    linear-gradient(180deg,transparent 0 var(--pg),#000 var(--pg)) right/var(--pw) 100% no-repeat;
  mask:
    linear-gradient(90deg,#000 0 calc(100% - var(--pg)),transparent calc(100% - var(--pg))) top/100% var(--pw) no-repeat,
    linear-gradient(90deg,transparent 0 var(--pg),#000 var(--pg)) bottom/100% var(--pw) no-repeat,
    linear-gradient(180deg,#000 0 calc(100% - var(--pg)),transparent calc(100% - var(--pg))) left/var(--pw) 100% no-repeat,
    linear-gradient(180deg,transparent 0 var(--pg),#000 var(--pg)) right/var(--pw) 100% no-repeat;}
.card h2{font-family:'Cinzel Decorative',serif;font-size:calc(var(--ui-vmin) * 1.528);letter-spacing:calc(var(--ui-vmin) * .694);color:var(--gold);text-align:center;}
/* Back button, header, and Rankings/Match Settings all on one row/
   baseline — align-items:center is what actually lines up the back
   button's/icon buttons' vertical centres with the header text's,
   regardless of their differing box heights (a fixed height on the icon
   buttons alone, matched against the header's line-height, wouldn't by
   itself guarantee the same centre — flex centring here does).
   Negative side margins pull just THIS row out past .card's own
   (deliberately generous, for the rest of the card's content — inputs,
   buttons, mode pickers) padding so the corner icons can sit at their own
   tighter inset instead of being stuck at that same wide padding — shrinking
   .card's padding itself to fit the icons cramped every other element in
   the card along with them. The row's own small padding below re-adds
   just enough inset to keep the icons off the literal card edge. */
.card-header-row{display:flex;align-items:center;justify-content:space-between;
  margin:0 calc(var(--ui-vmin, 7.2px) * -5) 18px;
  padding:0 calc(var(--ui-vmin, 7.2px) * 1.389);}
.card-header-row h2{flex:1;min-width:0;margin:0;}
/* Other active rules, directly below the header row (#lobby-header covers
   mode/player-count) — same element/copy whether it's reflecting live
   Match Settings picks (updateMatchSummary) or a joined room's frozen
   ones (renderWaitingRules), so it stays put across that handoff instead
   of two separate readouts in two different spots. */
.lobby-rules-summary{font-size:calc(var(--ui-vmin) * 1.5);color:rgba(245,237,216,.5);text-align:center;margin:-10px 0 16px;letter-spacing:calc(var(--ui-vmin) * .069);}
/* var(--ui-vmin, 7.2px) — same fallback convention as .mode-btn/.about
   .settings-icon-btn: --ui-vmin is only ever defined on #s-lobby/#s-start,
   so .ig used inside a .about-modal (Profile Picker, Developer Settings)
   has no ancestor defining it — without the fallback, the calc() goes
   invalid and padding collapses to 0, leaving input text flush against
   the left edge. */
.ig{margin-bottom:16px;}.ig label{display:block;font-size:calc(var(--ui-vmin, 7.2px) * 1.389);letter-spacing:calc(var(--ui-vmin, 7.2px) * .417);color:var(--gold);opacity:.9;margin-bottom:6px;text-transform:uppercase;}
.ig input{width:100%;background:rgba(0,0,0,.5);border:1px solid rgba(201,168,76,.2);border-radius:0;
  padding:calc(var(--ui-vmin, 7.2px) * 1.528) calc(var(--ui-vmin, 7.2px) * 1.944);color:var(--cream);
  font-size:calc(var(--ui-vmin, 7.2px) * 2.222);outline:none;transition:border-color .3s;font-family:'Noto Sans JP',sans-serif;}
.ig input:focus{border-color:var(--jl);}
.ig select{width:100%;background:rgba(0,0,0,.5);border:1px solid rgba(201,168,76,.2);border-radius:0;
  padding:calc(var(--ui-vmin, 7.2px) * 1.528) calc(var(--ui-vmin, 7.2px) * 1.944);color:var(--cream);
  font-size:calc(var(--ui-vmin, 7.2px) * 2.222);outline:none;transition:border-color .3s;font-family:'Noto Sans JP',sans-serif;}
.ig select:focus{border-color:var(--jl);}
.ig textarea{width:100%;background:rgba(0,0,0,.5);border:1px solid rgba(201,168,76,.2);border-radius:0;
  padding:calc(var(--ui-vmin, 7.2px) * 1.528) calc(var(--ui-vmin, 7.2px) * 1.944);color:var(--cream);
  font-size:calc(var(--ui-vmin, 7.2px) * 2);outline:none;transition:border-color .3s;font-family:'Noto Sans JP',sans-serif;
  resize:vertical;min-height:calc(var(--ui-vmin, 7.2px) * 12);box-sizing:border-box;}
.ig textarea:focus{border-color:var(--jl);}
.ig input[readonly]{cursor:pointer;opacity:.85;}
/* Error text for the profile-picker/dev-panel forms — small, unobtrusive
   until something actually goes wrong. */
.profile-error{color:#ff8080;font-size:calc(var(--ui-vmin, 7.2px) * 1.667);min-height:calc(var(--ui-vmin, 7.2px) * 2.222);margin:calc(var(--ui-vmin, 7.2px) * .833) 0;}
/* "Stay logged in" — a plain small checkbox+label sitting right under the
   code field (see #profile-code-section in index.html), not the bigger
   sp-toggle switch used for settings elsewhere — this is a minor one-off
   option on a login form, not something that deserves its own row's worth
   of visual weight. */
.profile-remember-row{display:flex;align-items:center;gap:calc(var(--ui-vmin, 7.2px) * .694);
  font-size:calc(var(--ui-vmin, 7.2px) * 1.389);color:var(--cream);opacity:.8;
  margin:calc(var(--ui-vmin, 7.2px) * -.556) 0 calc(var(--ui-vmin, 7.2px) * 1.111);cursor:pointer;}
.profile-remember-row input{width:calc(var(--ui-vmin, 7.2px) * 1.944);height:calc(var(--ui-vmin, 7.2px) * 1.944);
  accent-color:var(--gold);cursor:pointer;flex-shrink:0;}
/* Room List (see #roomlist-modal/renderRoomList) — one card per open room,
   same rgba(42,58,42,...)+gold-border language as .icon-btn/.theme-btn
   elsewhere in this modal family, just a row instead of a grid cell since
   each entry needs a title line, a sub line, AND a Join button. */
.room-card{display:flex;align-items:center;justify-content:space-between;gap:calc(var(--ui-vmin, 7.2px) * 1.111);
  background:rgba(42,58,42,.3);border:1px solid rgba(201,168,76,.2);border-radius:2px;
  padding:calc(var(--ui-vmin, 7.2px) * 1.25) calc(var(--ui-vmin, 7.2px) * 1.389);
  margin-bottom:calc(var(--ui-vmin, 7.2px) * .833);}
.room-card-title{font-size:calc(var(--ui-vmin, 7.2px) * 1.528);color:var(--cream);}
.room-card-sub{font-size:calc(var(--ui-vmin, 7.2px) * 1.25);color:var(--gold);opacity:.85;margin-top:calc(var(--ui-vmin, 7.2px) * .278);}
.room-card-join{flex-shrink:0;padding:calc(var(--ui-vmin, 7.2px) * .833) calc(var(--ui-vmin, 7.2px) * 1.667);
  background:linear-gradient(135deg,var(--jade),var(--jd));border:1px solid var(--jl);border-radius:2px;
  color:var(--cream);font-size:calc(var(--ui-vmin, 7.2px) * 1.389);cursor:pointer;font-family:'Noto Sans JP',sans-serif;}
.room-card-join:disabled{opacity:.4;cursor:default;}
.bp{width:100%;padding:calc(var(--ui-vmin) * 1.806);background:linear-gradient(135deg,var(--jade),var(--jd));border:1px solid var(--jl);border-radius:2px;color:var(--cream);
  font-size:calc(var(--ui-vmin) * 1.806);font-family:'Cinzel Decorative',serif;letter-spacing:calc(var(--ui-vmin) * .417);cursor:pointer;transition:all .25s;}
/* Lobby's two front-screen actions (Quick Match / Room List, see
   index.html) — side by side instead of one ambiguous button. Smaller
   font + letter-spacing than a lone .bp so "⚔ Quick Match"/"Room List"
   both fit comfortably at half width down to a narrow phone screen. */
.lobby-actions{display:flex;gap:calc(var(--ui-vmin, 7.2px) * 1.111);}
.lobby-actions .bp{width:auto;flex:1;min-width:0;
  font-size:calc(var(--ui-vmin) * 1.6);letter-spacing:calc(var(--ui-vmin) * .278);}
.bp:hover{transform:translateY(-2px);box-shadow:0 10px 28px rgba(26,107,69,.55);border-color:var(--gold);}
.bp:disabled{opacity:.4;cursor:default;transform:none;}
.bs{width:100%;padding:calc(var(--ui-vmin) * 1.389);background:transparent;border:1px solid rgba(201,168,76,.3);border-radius:2px;color:rgba(245,237,216,.65);
  font-size:calc(var(--ui-vmin) * 1.667);font-family:'Cinzel Decorative',serif;letter-spacing:calc(var(--ui-vmin) * .417);cursor:pointer;transition:all .25s;margin-top:8px;}
.bs:hover{border-color:var(--gold);color:var(--gl);}
.wa{display:none;margin-top:14px;}
.wm{text-align:center;color:var(--jl);font-size:calc(var(--ui-vmin) * 1.667);letter-spacing:calc(var(--ui-vmin) * .278);animation:pulse 1.5s infinite;}
@keyframes pulse{0%,100%{opacity:.5}50%{opacity:1}}
@keyframes ld-bounce{0%,80%,100%{transform:scale(0);opacity:.3}40%{transform:scale(1);opacity:1}}
.ld-dot{width:10px;height:10px;border-radius:50%;background:var(--gold);animation:ld-bounce 1.2s ease-in-out infinite;}
.slots{display:flex;gap:8px;justify-content:center;margin-top:12px;}
/* flex-shrink instead of a rigid 80px: on a narrow phone, .card can end up
   narrower than 4*80px + gaps (e.g. 96vw at 320px wide is well under the
   344px the four slots need), so they'd wrap unpredictably into 2x2 —
   changing the panel's layout depending on screen width. flex:0 1 80px
   keeps the current 80px look on any screen with room for it (matches
   .mode-btn's flex:1 above, already used for this same "N equal items in
   a row" problem) while letting them shrink together, in one row, rather
   than wrap, when space is tight. */
.slot{position:relative;flex:0 1 80px;min-width:0;min-height:64px;border-radius:2px;border:2px solid rgba(201,168,76,.2);display:flex;align-items:center;justify-content:center;flex-direction:column;gap:3px;background:rgba(0,0,0,.4);transition:border-color .4s,background .4s,box-shadow .4s;padding:6px 4px;}
.slot .si{font-size:calc(var(--ui-vmin) * 2.778);line-height:1;}
/* Real player icon (see iconSvg in script.js) — bots keep the plain
   letter-avatar .si above instead, since they have no profile icon. */
.slot .s-icon{width:calc(var(--ui-vmin) * 7y);height:calc(var(--ui-vmin) * 7);display:block;}
.slot .s-icon svg{width:100%;height:100%;display:block;}
.slot .sn{font-size:calc(var(--ui-vmin) * 1.25);color:rgba(245,237,216,.7);font-weight:600;}
.slot.filled{border-color:var(--jl);background:rgba(26,107,69,.25);}
.slot.bot{border-color:rgba(201,168,76,.35);background:rgba(50,35,5,.4);}
/* Ready state is communicated purely by frame colour/glow now — no more
   "Not ready"/"✓ Ready" text (see updateSlots in script.js). */
.slot.rdy{border-color:#4eff9a;box-shadow:0 0 14px rgba(78,255,154,.3);}
/* One-shot pulse the moment a slot actually TRANSITIONS to ready (added
   only on that specific render — see the prevReady tracking in
   updateSlots, which prevents this from replaying on every unrelated
   room_update, e.g. another seat's difficulty change). Ends on exactly
   .slot.rdy's steady box-shadow/scale so it blends into the persistent
   glow instead of visibly snapping when the animation finishes. */
@keyframes rdyFlash{
  0%{box-shadow:0 0 0 rgba(78,255,154,0);transform:scale(1);}
  35%{box-shadow:0 0 26px rgba(78,255,154,.85);transform:scale(1.07);}
  100%{box-shadow:0 0 14px rgba(78,255,154,.3);transform:scale(1);}
}
.slot.rdy-flash{animation:rdyFlash .5s ease-out;}
.slot.clickable-bot{cursor:pointer;}
.slot .sd{font-size:calc(var(--ui-vmin) * 1.0);color:var(--gold);opacity:.75;margin-top:1px;}
/* Small inline Easy/Normal/Hard picker anchored under a clicked bot slot —
   populated in renderDiffPopup() (script.js). */
.diff-popup{position:absolute;top:100%;left:50%;transform:translateX(-50%);margin-top:6px;
  display:flex;gap:4px;background:#0a160c;border:1px solid rgba(201,168,76,.4);border-radius:8px;
  padding:6px;box-shadow:0 8px 24px rgba(0,0,0,.6);z-index:50;}
.diff-popup-btn{padding:5px 8px;font-size:10px;white-space:nowrap;}
.cd{font-size:calc(var(--ui-vmin) * 1.389);color:rgba(201,168,76,.9);text-align:center;margin-top:8px;letter-spacing:calc(var(--ui-vmin) * .139);}
/* Invite Players — a plain text link (see #binvite in index.html), not a
   third full-width button alongside Ready/Cancel below it. */
.invite-link{display:block;text-align:left;padding-left:2vmin;font-size:calc(var(--ui-vmin) * 1.389);
  color:rgba(245,237,216,.55);text-decoration:none;letter-spacing:calc(var(--ui-vmin) * .139);cursor:pointer;transition:color .2s;}
.invite-link:hover{color:var(--gold);}
/* Create Room's Invite Players link sits in an .about-section wrapper
   (see #rules-invite-section in index.html) purely so JS can show/hide it
   as one unit — keeps that class's own top margin/divider-line/padding (a
   thin rule + gap, same as every other .about-section) so it reads as its
   own section below Rules rather than butting right up against the toggle
   list, but drops the BOTTOM margin .about-section doesn't otherwise have
   an opinion on — "+ Create Room" right below (see
   #rules-modal-create-btn) sits snug against it/its invited-chip list
   instead of leaving a gap. */
#rules-invite-section{margin-bottom:0;}
/* Fallback (7.2px), same value/reason as .about-close below: .mode-btn is
   used both in #s-lobby (defines --ui-vmin) and, since Game Mode/Players
   moved into #rules-modal, inside a modal that never defines it — without
   the fallback, --ui-vmin resolves to nothing there, calc() goes invalid,
   and padding (not an inherited property) collapses to 0, pinning the
   label text against the button edges. */
.mode-btn{flex:1;padding:calc(var(--ui-vmin, 7.2px) * 1.389) calc(var(--ui-vmin, 7.2px) * .833);background:rgba(0,0,0,.3);border:1px solid rgba(201,168,76,.2);border-radius:2px;color:rgba(245,237,216,.9);font-size:calc(var(--ui-vmin, 7.2px) * 1.806);font-family:'Noto Serif JP',serif;font-weight:700;cursor:pointer;transition:all .2s;line-height:1.6;text-align:center;}
.mode-btn.active{background:rgba(26,107,69,.4);border-color:var(--jl);color:var(--cream);box-shadow:0 0 12px rgba(42,158,103,.3);}
.mode-btn:hover:not(.active){border-color:rgba(201,168,76,.4);color:var(--cream);}
.ready-btn{margin-top:calc(var(--ui-vmin) * 1.667);}

/* ════════════════════════════════════════════════════════════
   GAME SCREEN — LAYOUT
   ════════════════════════════════════════════════════════════ */
#s-game:not(.hidden){display:block;}
/* --ui-vmin: same clamped-scale-unit approach as #s-start/#s-lobby/
   #dora-panel/.chat-bar, but with a lower ceiling (7.5px vs 10px) — the
   topbar's own height stays a fixed 36px (.table hardcodes top:36px to
   sit right below it, so growing the bar means re-plumbing that too),
   so its contents need to stop scaling well before a 10px unit would
   make .bl (height 3.611 * unit) crowd a 36px-tall strip. At 7.5px the
   bar renders almost identically to the original fixed-px sizes on any
   normal-to-large screen, only shrinking further on small phones (floor
   still 6px). */
.topbar{--ui-vmin:clamp(6px,1vmin,7.5px);
  position:fixed;top:0;left:0;right:0;height:36px;display:flex;align-items:center;justify-content:space-between;padding:0 10px;z-index:50;
  /* "Gilded trim": a lacquer-box edge — a bright gold rule along the
     bottom with a fainter second rule just under it (::after). */
  background:linear-gradient(#0d110e,#050806);box-shadow:inset 0 -1px 0 rgba(201,168,76,.65);}
.topbar::after{content:'';position:absolute;left:0;right:0;bottom:-4px;height:1px;background:rgba(201,168,76,.3);pointer-events:none;}
.tb-title{display:flex;align-items:baseline;gap:calc(var(--ui-vmin) * .8);white-space:nowrap;
  font-family:'Noto Serif JP',serif;font-size:calc(var(--ui-vmin) * 1.944);color:var(--gold);letter-spacing:calc(var(--ui-vmin) * .278);}
/* 雀心 in the START button's gold leaf. */
.tb-jp{font-weight:900;background:linear-gradient(115deg,#b5903f 0%,#ffe9b0 48%,#cfa74d 100%);-webkit-background-clip:text;background-clip:text;color:transparent;}
.tb-en{font-family:'Cinzel Decorative',serif;font-weight:700;font-size:calc(var(--ui-vmin) * 1.35);letter-spacing:.28em;}
/* Whose turn. */
.tb-st{font-size:calc(var(--ui-vmin) * 1.944);color:rgba(245,237,216,.5);max-width:40vw;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.tb-st.my{color:var(--gl);font-weight:700;text-shadow:0 0 8px rgba(240,208,128,.45);animation:pulse 1.5s infinite;}
/* Leave: a small slanted plate like the action buttons (.ba) — the
   button is the red edge, ::before (same slant, inset) the fill. */
.topbar .bl{position:relative;isolation:isolate;display:inline-flex;align-items:center;
  padding:0 calc(var(--ui-vmin) * 2.2);height:calc(var(--ui-vmin) * 3.611);border:none;border-radius:0;background:#d44;color:#ffd8d8;
  font-family:'Cinzel Decorative',serif;font-weight:700;font-size:calc(var(--ui-vmin) * 1.45);letter-spacing:.12em;cursor:pointer;white-space:nowrap;flex-shrink:0;
  clip-path:polygon(calc(var(--ui-vmin) * 1.1) 0,100% 0,calc(100% - var(--ui-vmin) * 1.1) 100%,0 100%);}
.topbar .bl::before{content:'';position:absolute;z-index:-1;inset:1.5px 2px;background:#5a1010;
  clip-path:polygon(calc(var(--ui-vmin) * .95) 0,100% 0,calc(100% - var(--ui-vmin) * .95) 100%,0 100%);}
.topbar .bl:hover{filter:brightness(1.2);}
.bl{padding:0 calc(var(--ui-vmin) * 1.389);height:calc(var(--ui-vmin) * 3.611);background:rgba(120,20,20,.8);border:1px solid rgba(200,60,60,.4);border-radius:6px;color:rgba(255,180,180,.85);font-size:calc(var(--ui-vmin) * 1.806);cursor:pointer;font-family:'Noto Sans JP',sans-serif;white-space:nowrap;flex-shrink:0;}
.bl:hover{background:rgba(160,30,30,.9);}

/* ── Table container ── */
.table{position:fixed;top: 36px;;left:0;right:0;bottom:0;overflow:hidden;
  display:flex;align-items:center;justify-content:center;}

/* Sanma (3-player): no seat sits directly across (toimen), so that
   player-area is hidden — script.js toggles this class on .table once per
   game, from game_start's numPlayers. In the centre panel the top seat's
   score and riichi stick are hidden below (visibility:hidden, so the
   layout doesn't shift) rather than showing a "—" placeholder or a stray
   riichi stick for a seat nobody occupies. */
.table.sanma .pa-t,
.table.sanma .np-wrap-t,
.table.sanma .ml-t,
.table.sanma .kita-t,
.table.sanma .dzt{display:none;}
.table.sanma #ctr-s-t,
.table.sanma .rs-top{visibility:hidden;}

/* ── Sizing variable — single source of truth ── */
/* Plain 110vmin assumes .table's box is the full viewport, but .table
   sits below the 36px-tall .topbar (top:36px), so its real height is
   (100vh - 36px). Since vmin = min(vw, vh), using raw vh under-reports
   how little vertical room the container actually has whenever height
   is the limiting dimension — which is most landscape/widescreen
   viewports, not just short mobile-landscape windows. That let the felt
   (and everything anchored to its edge — seat rows, emoji reactions)
   render partly outside .table's clipped bounds. Swapping the vh side
   for the container's real height keeps the portrait/width-limited case
   byte-for-byte identical (min(vw,...) still wins there) while fixing
   every height-limited one. */
:root{
  --table-size: min(110vw, calc((100vh - 36px) * 1.1));
  --felt-size: calc(var(--table-size) * 0.86);
}

/* ── Wood table — sits behind the felt ──
   Picked in the "Table Textures" mockup (2026-10-16): today's frame
   style, darker (hsl lightness 11%, highlight +10%, shadow -7%). */
.wood-table{
  width:var(--table-size);
  height:var(--table-size);
  background:radial-gradient(ellipse at 40% 30%,hsl(20 46% 21%),hsl(20 46% 11%) 50%,hsl(20 46% 4%));
  box-shadow:0 8px 48px rgba(0,0,0,.8),0 2px 0 rgba(255,255,255,.04) inset;
  border:2px solid #0d0502;
  flex-shrink:0;}

/* ── Felt (green play area) ──
   Same mockup: a smooth, darker green (lightness 17%) lit from directly
   above (a soft pool in the middle, darker toward the edges), set into
   the frame with a bevelled step — a faint highlight just outside the
   felt's top edge, a shadow below, and a shadow cast onto the felt. */
.felt{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
  width:var(--felt-size);
  height:var(--felt-size);
  background:radial-gradient(circle at 50% 50%,rgba(255,250,220,.09),rgba(0,0,0,0) 40%,rgba(0,0,0,.35) 100%),hsl(150 48% 17%);
  border:none;
  box-shadow:0 calc(var(--table-size) * -.004) 0 calc(var(--table-size) * .006) rgba(255,230,190,.08),
    0 calc(var(--table-size) * .004) 0 calc(var(--table-size) * .006) rgba(0,0,0,.35),
    inset 0 calc(var(--table-size) * .006) calc(var(--table-size) * .012) rgba(0,0,0,.5),
    inset 0 0 calc(var(--felt-size) * .07) rgba(0,0,0,.45);}

/* ── Centre panel size — shared by .ctr and the four discard ponds (.dz*),
   which hang off its edges, so the panel and the ponds always move
   together. It matches the ponds' own width (6 tiles of --tmd-w, which
   script.js sizes to 26% of the felt). Everything inside the panel is
   measured in --cu = 1cqw x --ctr-k; --ctr-k stays 1 at this size, and is
   the knob for keeping the contents' size if --ctr-size ever changes. A
   bigger panel (28.5% / 33% on phones) was tried and reverted: it pushed
   the ponds out too far. */
.felt{--ctr-size:min(26vmin,26%);--ctr-k:1;}

/* ── Centre info panel (round / remaining wall / scores) ──
   A square in the nameplates' own #1a1a1a (see .np), same size as before, sized inside in cqw (container-type)
   so every piece scales with the panel itself. In the middle: the round
   (東1) over the remaining wall (残 68), inside a gauge ring (#cd-arc) that
   drains as the wall runs down (--p set in updateCenterPanel). Along each
   edge, inside that seat's rotated layer: its score centred, with its
   riichi stick above it; the current player's score is lit gold. All the
   Japanese here is Noto Serif JP, the 雀心 logo's font (.logo-k). No
   outer box-shadow: one spread past the panel's edges darkened the discard
   rows right beside it. Diagonal hairlines (::before) run from each corner
   to the gauge, splitting the panel into one triangular area per seat. */
.ctr{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
  z-index:1;pointer-events:none;container-type:inline-size;
  width:var(--ctr-size);height:var(--ctr-size);
  --cu:calc(1cqw * var(--ctr-k));
  padding:0;overflow:hidden;border-radius:2px;
  background:#1a1a1a;
  border:1px solid rgba(201,168,76,.35);}
.ctr::before{content:'';position:absolute;inset:0;pointer-events:none;
  --ln:transparent calc(50% - calc(.35 * var(--cu))),rgba(201,168,76,.45) calc(50% - calc(.35 * var(--cu))) calc(50% + calc(.35 * var(--cu))),transparent calc(50% + calc(.35 * var(--cu)));
  background:linear-gradient(45deg,var(--ln)),linear-gradient(-45deg,var(--ln));
  -webkit-mask:radial-gradient(circle,transparent calc(30 * var(--cu)),#000 calc(30.2 * var(--cu)));mask:radial-gradient(circle,transparent calc(30 * var(--cu)),#000 calc(30.2 * var(--cu)));}
/* Wall gauge: gold for what's left, a faint track for what's been drawn. */
.cd-arc{--p:100;position:absolute;left:50%;top:50%;translate:-50% -50%;width:calc(56 * var(--cu));height:calc(56 * var(--cu));border-radius:50%;
  background:conic-gradient(from 180deg,var(--gl) calc(var(--p) * 1%),rgba(245,237,216,.1) 0);
  -webkit-mask:radial-gradient(circle,transparent calc(25.6 * var(--cu)),#000 calc(25.8 * var(--cu)));mask:radial-gradient(circle,transparent calc(25.6 * var(--cu)),#000 calc(25.8 * var(--cu)));}
/* Each seat's layer covers the whole panel, rotated toward that seat (see
   .ctr-score-top/-left/-right below), so all four read from their own
   player's side. */
.ctr-score{position:absolute;inset:0;z-index:1;}
/* The current player's triangle (edge to centre), faintly lit. One wedge
   for the whole panel, turned to the active seat by setCtrTurn in
   script.js, so it sweeps round to the next player instead of jumping.
   .no-anim places it instantly (first state, reconnect). */
.ctr-turn{position:absolute;inset:0;clip-path:polygon(0 100%,100% 100%,50% 50%);
  background:rgba(240,208,128,.1);transition:transform .35s cubic-bezier(.4,0,.2,1),opacity .3s;
  -webkit-mask:radial-gradient(circle,transparent calc(30 * var(--cu)),#000 calc(30.2 * var(--cu)));mask:radial-gradient(circle,transparent calc(30 * var(--cu)),#000 calc(30.2 * var(--cu)));}
.ctr-turn.no-anim{transition:none;}
.ctr-turn.off{opacity:0;}
/* Each edge is one row, centred as a pair: the score on the left, a
   riichi-stick slot on the right (from that seat's own view). The slot
   (::after) is always there; declaring riichi drops the stick (.rs-*)
   into it. The score box has a fixed width and is right-aligned, so the
   slot never shifts as the score's digit count changes. Everything in the
   row is sized from the score's font size --fs (in --cu), with fixed
   ratios, so the score and stick keep the same proportions on every
   screen; phones below only change --fs, --lh and --row-b.
   --row-b row bottom, --row-h score line height, --sw score box width
   (fits 7 characters, e.g. -12,300), --gap, --kw/--kh slot size, --stw
   stick width (its svg is 44:10, with the stick itself drawn 42:8 inside
   it). The slot hugs the drawn stick with a hairline of room (~.08fs)
   on each side, so a dropped stick sits snugly in it. */
.ctr-score{--fs:8.4;--lh:1.2;--row-b:6;
  --row-h:calc(var(--fs) * var(--lh));--sw:calc(var(--fs) * 3.95);--gap:calc(var(--fs) * .395);
  --kw:calc(var(--fs) * 3.18);--kh:calc(var(--fs) * .73);--stw:calc(var(--fs) * 3.158);}
.ctr-score::after{content:'';position:absolute;
  left:calc(50% + (var(--sw) + var(--gap) - var(--kw)) / 2 * var(--cu));
  bottom:calc((var(--row-b) + var(--row-h) / 2 - var(--kh) / 2) * var(--cu));
  width:calc(var(--kw) * var(--cu));height:calc(var(--kh) * var(--cu));border-radius:calc(var(--fs) * .14 * var(--cu));
  background:rgba(0,0,0,.16);box-shadow:inset 0 calc(.4 * var(--cu)) calc(.8 * var(--cu)) rgba(0,0,0,.35),0 0 0 1px rgba(201,168,76,.1);}
.table.sanma .ctr-score-top::after{visibility:hidden;}
.ctr-score-val{position:absolute;left:calc(50% - (var(--sw) + var(--gap) + var(--kw)) / 2 * var(--cu));width:calc(var(--sw) * var(--cu));
  bottom:calc(var(--row-b) * var(--cu));text-align:right;white-space:nowrap;
  font-family:'Noto Serif JP',serif;font-weight:700;font-size:calc(var(--fs) * var(--cu));line-height:var(--lh);font-variant-numeric:tabular-nums;
  color:rgba(245,237,216,.85);transition:color .3s,text-shadow .3s;}
.ctr-score.cd-on .ctr-score-val{color:var(--gl);text-shadow:0 0 calc(2 * var(--cu)) rgba(240,208,128,.55);}
/* Middle: the round over the remaining wall, inside the gauge. */
.ctr-mid{position:absolute;z-index:2;left:50%;top:50%;translate:-50% -50%;display:flex;align-items:center;justify-content:center;}
.ctr-info{display:flex;flex-direction:column;align-items:center;gap:calc(1.4 * var(--cu));}
.cw{display:flex;align-items:baseline;font-family:'Noto Serif JP',serif;font-weight:900;line-height:1;color:var(--gl);}
/* The round number sits as a subscript of the round wind (東₁). */
#cw-wind{font-size:calc(16 * var(--cu));}
#cw-kyoku{font-size:calc(8.5 * var(--cu));margin-left:calc(.6 * var(--cu));translate:0 calc(1.6 * var(--cu));}
.ci:empty{display:none;}
/* The wall line: tiles left in the live wall, the number in Noto Serif JP
   with a small sans "tiles" after it. */
.cwl{font-family:'Noto Serif JP',serif;font-weight:700;font-size:calc(8.5 * var(--cu));line-height:1;letter-spacing:.06em;color:rgba(245,237,216,.85);white-space:nowrap;}
.cwl-unit{margin-left:.3em;font-family:'Noto Sans JP',sans-serif;font-weight:400;font-size:.62em;letter-spacing:.04em;color:rgba(245,237,216,.65);}
/* Small panels (phones, where the square is ~100-120px): the same layout
   with larger type. The gauge keeps its desktop size. The score row is
   as big as fits across the seat's area between the two diagonals. */
@container (max-width:150px){
  .ctr-info{gap:calc(1.6 * var(--cu));}
  #cw-wind{font-size:calc(18 * var(--cu));}
  #cw-kyoku{font-size:calc(10 * var(--cu));translate:0 calc(2 * var(--cu));}
  .cwl{font-size:calc(10 * var(--cu));letter-spacing:0;}
  .ctr-score{--fs:9.2;--lh:1;--row-b:4.5;}
}
@media (prefers-reduced-motion:reduce){.ctr-score-val,.ctr-turn{transition:none;}}

/* ── Dora indicator strip ── */
/* Same corner radius as the revealed indicator tile (.tdora), so a tile
   flipping into its slot lines up with the slot's outline. An empty slot
   is a faint recess (like the riichi stick slots) rather than a dashed
   outline. */
.dora-slot{width:var(--tlg-w,46px);height:var(--tlg-h,64px);border-radius:0.4vmin;
  background:rgba(0,0,0,.28);box-shadow:inset 0 .25vmin .5vmin rgba(0,0,0,.6),0 0 0 1px rgba(201,168,76,.12);
  display:flex;align-items:center;justify-content:center;}
.dora-slot.filled{border:none;background:none;box-shadow:none;padding:0;}
/* Kan dora reveal (animatePlaced in script.js; durations mirrored there as
   DORA_FLIP_MS / DORA_GLOW_MS). The indicator turns over edge-on: its
   ::after is the tile back (.kan-back's blue) for the first half, then drops
   away so the face opens out. The slot then flashes gold. The glow stays
   within ~1vmin since #dora-panel scrolls (overflow-y:auto) and would clip
   anything wider. */
.dora-flip{animation:dora-flip 480ms cubic-bezier(.45,0,.25,1) both;}
.dora-flip::after{content:'';position:absolute;inset:0;z-index:1;border-radius:inherit;
  background:linear-gradient(135deg,#1a3a6a,#0d1f3a);animation:dora-flip-back 480ms both;animation-delay:inherit;}
@keyframes dora-flip{0%{scale:1 1}50%{scale:0 1.06}100%{scale:1 1}}
@keyframes dora-flip-back{0%,49.9%{opacity:1}50%,100%{opacity:0}}
.dora-slot.dora-new{border-radius:5px;animation:dora-new 1s ease-out both;}
@keyframes dora-new{
  0%,45%{box-shadow:0 0 0 0 rgba(240,208,128,0)}
  58%{box-shadow:0 0 0 .3vmin #f0d080,0 0 1.2vmin rgba(240,208,128,.9)}
  100%{box-shadow:0 0 0 .3vmin rgba(240,208,128,0),0 0 1.2vmin rgba(240,208,128,0)}}
@media (prefers-reduced-motion:reduce){.dora-flip,.dora-flip::after,.dora-slot.dora-new{animation:none;}.dora-flip::after{display:none;}}
/* Ankan face-down tiles */
.kan-back{
  width:var(--tmd-w,26px);height:var(--tmd-h,36px);
  background:linear-gradient(135deg,#1a3a6a,#0d1f3a);
  border:1px solid #2a4a8a;border-radius:0.4vmin;
  flex-shrink:0;display:inline-flex;
  box-shadow:1px 2px 4px rgba(0,0,0,.5);
}
@keyframes bounce{0%,100%{transform:translateX(-50%) translateY(0)}50%{transform:translateX(-50%) translateY(-5px)}}

/* ════════════════════════════════════════════════════════════
   DISCARD ZONES
   ════════════════════════════════════════════════════════════ */
/* transform-origin:left top set here (not just on .dzt/.dzl/.dzr below,
   which redundantly restate it) so .dzb — which has no rotation, and so no
   origin override of its own — also anchors there instead of defaulting to
   center. Matters for fitDiscardZone (script.js): it scales a deep pile
   down toward its own anchor corner rather than letting it slide when it
   would otherwise overlap that seat's nameplate — scaling from a center
   origin would shift the pile's visible anchor point instead of shrinking
   it in place. */
.dz{position:absolute;display:flex;flex-direction:column;;pointer-events:none;transform-origin:left top;}
.dz-row{display:flex;flex-direction:row;}
/* Natural ponds: each discard's slight lean and up/down nudge (set per
   tile by pondJitter in script.js). Uses the individual rotate/translate
   properties, so it stacks with the riichi tile's sideways transform and
   the tile-drop animation's scale. Tiles may touch but never overlap:
   layoutPond spaces each tile (--gx) and row (--gy) by exactly what the
   leans need, in tile widths. Remove these rules for a strict grid. */
.dz .tmd{rotate:var(--rz,0deg);translate:0 calc(var(--tmd-h,36px) * var(--dy,0));margin-left:calc(var(--tmd-w,26px) * var(--gx,0));}
.dz .tmd.dz-riichi{margin-left:calc(var(--tmd-h,40px) / 2 - var(--tmd-w,30px) / 2 + var(--tmd-w,26px) * var(--gx,0));}
.dz-row{margin-top:calc(var(--tmd-w,26px) * var(--gy,0));}


/* Every pond hangs off a corner of the centre panel (--ctr-size, see
   .felt); --dz-half is half the panel. */
.dz{--dz-half:calc(var(--ctr-size) / 2);}

/* BOTTOM: top-left of zone at panel bottom-left. No rotation. */
.dzb{top:calc(50% + var(--dz-half));left:calc(50% - var(--dz-half));}

/* TOP: their visual top-left (= DOM bottom-right after 180°) at panel top-left.
   Panel top-left = (50%-H, 50%-H). 
   right=50%+H → element right edge at 50%-H.
   bottom=50%+H → element bottom at 50%-H.
   transform-origin: right bottom. 
   Rows: DOM row-reverse so tiles go their left→right after 180° flip.
   Columns: column-reverse so first row is at visual top (nearest panel). */
.dzt{
  top:calc(50% - var(--dz-half));
  left:calc(50% + var(--dz-half));
  transform:rotate(180deg);
  transform-origin:left top;}

/* LEFT: their visual top-left (= DOM bottom-left after 90°CW) at panel bottom-left.
   Panel bottom-left = (50%-H, 50%+H).
   left=50%-H → element left edge at 50%-H.
   bottom=50%-H → element bottom at 50%+H.
   transform-origin: left bottom. */
.dzl{
  left:calc(50% - var(--dz-half));
  top:calc(50% - var(--dz-half));
  transform:rotate(90deg);
  transform-origin:left top;}

/* RIGHT: their visual top-left (= DOM top-left after -90°) at panel bottom-right.
   Panel bottom-right = (50%+H, 50%+H).
   left=50%+H, top=50%+H. transform-origin: left top. */
.dzr{
  top:calc(50% + var(--dz-half));
  left:calc(50% + var(--dz-half));
  transform:rotate(-90deg);
  transform-origin:left top;}

/* Riichi tile: rotated 90° within its zone row (from each player's perspective) */
.dz-riichi{display:inline-flex;transform:rotate(-90deg);transform-origin:center center;
  margin:calc((var(--tmd-w,40px) - var(--tmd-h,30px))/2) calc(var(--tmd-h,40px)/2 - var(--tmd-w,30px)/2);}

/* ════════════════════════════════════════════════════════════
   PLAYER AREAS (hand + melds)
   ════════════════════════════════════════════════════════════ */
/* Bottom (me): flush to bottom edge, column layout (nameplate above tiles) */
.pa-b{position:absolute;bottom:1vmin;left:10vmin;}
/* Top: flush to top edge, column layout (tiles first, nameplate below) */
.pa-t{position:absolute;top:1vmin;right:10vmin;transform-origin:center center;transform:rotate(180deg);}
/* Left: centre at left edge, vertically centred, rotate 90° CW */
/* Left: pivot at tile row left edge, centred on felt, rotate 90° CW */
.pa-l{position:absolute;top:calc(10vmin - var(--tsm-h,22px));left:1vmin;transform-origin:left bottom;transform:rotate(90deg);}
/* Right: pivot at tile row left edge, centred on felt, rotate -90° CCW */
.pa-r{position:absolute;bottom:calc(10vmin - var(--tsm-h,22px));right:1vmin;transform-origin:right top;transform:rotate(90deg);}

/* Nameplates */
/* cursor:pointer applies even to bot seats (openPlayerCardInGame no-ops for
   them rather than the cursor silently lying) — CSS has no way to know
   which seat is a bot at load time, only inGameSeatInfo (script.js) does,
   and gating the cursor per-seat via JS for a purely cosmetic hint isn't
   worth the extra DOM churn on every game start. */
.np{background:#1a1a1a;border-radius:1px;cursor:pointer;
  /* table-size-scaled, same reasoning as .wb/.pname/etc below — was raw
     vmin, which let this padding stay near its floor on short/wide windows
     while the felt (sized off --table-size) kept shrinking, oversizing the
     plate relative to the felt. */
  /* Floors lowered from 6px/14px (padding) and a flat 9px (gap) — those
     were still the OLD vmin-clamp floors, tuned only so text stayed
     legible, not with the icon/padding/gap "chrome" around it in mind. On
     an actual phone (not just a short/wide desktop window, which is what
     the table-size switch above targeted) those floors were the binding
     constraint — the plate stopped shrinking well before the felt did,
     reading as oversized. Padding/gap have no legibility floor of their
     own the way font-size does, so they can shrink further. */
  /* Right padding larger than the other 3 sides (~10-32px vs 4-13px) — was
     equalized to match briefly, then restored: with the plate's contents
     now bigger (13px name, 36px icon), the extra breathing room on the
     right reads as intentional again rather than an unexplained gap. */
  /* Whole plate — this padding/gap plus every wb/pname/ptitle size below —
     scaled to 60% of its prior value in one pass, keeping every floor,
     coefficient and ceiling in the same proportion to each other so the
     nameplate just gets uniformly smaller rather than any one part (icon
     vs text vs breathing room) changing shape relative to the rest. */
  padding:clamp(2px,calc(var(--table-size) * 0.004364),8px) clamp(6px,calc(var(--table-size) * 0.010909),19px) clamp(2px,calc(var(--table-size) * 0.004364),8px) clamp(2px,calc(var(--table-size) * 0.004364),8px);display:flex;align-items:center;gap:clamp(3px,calc(var(--table-size) * 0.00873),7px);
  position:relative;
  z-index:20;transition:all .25s;flex-shrink:0;white-space:nowrap;
  box-shadow:0 0 0 1px #222,inset 0 1px 0 rgba(255,255,255,.04);}
/* Whose turn it is shows on the centre panel (lit score + seat triangle),
   so the plate itself only lightens a touch — no pulsing gold frame. */
.np.active{background:#222;}
/* Nameplate skins live further down, right after .np-preview (see that
   class's own comment for why: it and this block's selectors are both
   single classes, so they have to stay in that relative order for the
   skins to actually win the cascade against .np-preview's own defaults). */
/* Chat "is typing…" indicator (see onTypingStatus in script.js, driven by
   room.js's typing()) — 3 real dot elements (.typing-dots span), not CSS
   `content`, specifically so each one can bounce on its own staggered
   delay (see @keyframes typingBounce below) — a single text/pseudo-element
   node can't animate its characters independently. onTypingStatus
   creates/removes this whole span on demand rather than it living
   permanently in index.html's markup, same "no extra DOM per seat unless
   actually needed" reasoning .status-dot's own comment gives for why only
   bottom's markup has one. No background (bare dots directly over the
   felt) rather than squeezed into the plate's own already-tight layout
   (see .pname/.ptitle's own max-width comments). Anchored above the seat
   wind icon specifically, not centered over the whole plate — `left` here
   replicates .wb's own left-edge offset (.np's left padding) plus half its
   width, i.e. .wb's horizontal centre. Positioned relative to .np (set
   position:relative above), which is what actually holds this element
   (appended as its child) — so it tracks the plate at any seat's
   position/rotation without its own per-seat placement math. A cool blue,
   not .active's gold — this isn't a problem state, just activity. */
.typing-dots{position:absolute;top:calc(var(--table-size) * -0.02);
  left:calc(clamp(2px,calc(var(--table-size) * 0.004364),8px) + clamp(22px,calc(var(--table-size) * 0.03571),63px)/2);
  transform:translateX(-50%);display:flex;gap:clamp(2px,calc(var(--table-size) * 0.00219),4px);
  z-index:26;pointer-events:none;}
.typing-dots span{width:clamp(3px,calc(var(--table-size) * 0.00328),6px);
  height:clamp(3px,calc(var(--table-size) * 0.00328),6px);border-radius:50%;background:#8fd6ff;
  animation:typingBounce .9s ease-in-out infinite;}
.typing-dots span:nth-child(2){animation-delay:.15s;}
.typing-dots span:nth-child(3){animation-delay:.3s;}
/* translateY in %, not a fixed/clamped px — relative to the dot's OWN
   height, so the bounce distance scales automatically with the dot size
   above instead of needing its own separate clamp() kept in sync. */
@keyframes typingBounce{0%,60%,100%{transform:translateY(0);opacity:.5;}
  30%{transform:translateY(-120%);opacity:1;}}
/* wb/pname/ptitle/btag below: floor+ceiling clamps
   added on every font-size/width/height that was plain unclamped `vmin` —
   at a 375px phone (vmin≈3.75px) that math put .ba's 1.6vmin label text at
   ~6px and this row's 1.2vmin badges at ~4.5px, well under anything
   legible; at 4K (vmin≈21.6px) the same numbers put .wb's wind badge at
   ~86x86px, far larger than any dialog/panel elsewhere in the app (all of
   which are capped via --ui-vmin — see .about-modal/#settings-panel).
   Floor/ceiling chosen from the vmin range ordinary desktop windows
   actually produce (~7px at 1366-wide, ~16px at 2560-wide) so nothing
   changes at any typical desktop size — only the phone/4K extremes are
   caught, same "protect the edges, leave the middle alone" approach as
   #cw-wind/.ci/.cwl's existing clamp()s just above in this file. */
/* wb/pname/ptitle/btag sizing below is expressed as a
   fraction of --table-size (same value the felt/tiles derive from — see
   --felt-size and updateTileSizes()'s feltSize read), not raw vmin. Raw vmin
   reads the viewport's min(width,height) directly, which ignores the top
   bar's fixed height and the 110%/86% factors baked into --table-size/
   --felt-size; on a short, wide window that mismatch let the nameplate stay
   near its px floor while the felt (sized off --table-size) kept shrinking
   faster, so the plate ended up oversized relative to the felt and covered
   the discard row. Each coefficient here is the old vmin value divided by
   110 (--table-size ≈ 110vmin on typical desktop aspect ratios), so normal
   windows render identically to before; only extreme aspect ratios differ,
   and now they differ in step with the felt instead of against it. */
/* Icon floor lowered 28px→22px — same "chrome, not text" reasoning as
   .np's padding/gap floors above; a circular seat-wind icon stays perfectly
   readable well below the old floor, unlike a floor on actual text. */
.wb{font-family:'Noto Serif JP',serif;font-weight:900;display:flex;align-items:center;justify-content:center;border-radius:50%;background:rgba(201,168,76,.18);color:var(--gl);border:1px solid rgba(201,168,76,.45);flex-shrink:0;
  /* Floor/coefficient/ceiling scaled up again (22→25→36, font 17→19→27) —
     the plate's height is driven by the name+2-line-title text column next
     to it (see .ptitle below), not by this icon, so there was still room
     to grow the icon well past 25px without the plate growing any taller;
     36px (icon width/height) is that column's actual measured height on a
     412px phone (see fitNameplateText/textColHeight math this was checked
     against) — any bigger and the icon itself would start forcing the
     plate taller instead of just filling it. */
  font-size:clamp(16px,calc(var(--table-size) * 0.02599),46px);width:clamp(22px,calc(var(--table-size) * 0.03571),63px);height:clamp(22px,calc(var(--table-size) * 0.03571),63px);}
/* max-width used to be `none` — the nameplate grew freely with the
   player's name (see the kita-strip's own comment on .kita-b, which
   anchors at the felt's horizontal center specifically BECAUSE "no
   realistic name" was assumed to reach halfway). That held for names alone,
   but an unlocked achievement TITLE (.ptitle below) can run much longer —
   e.g. "YAKUMAN COLLECTOR" — and on a narrow phone-portrait felt the
   nameplate's un-capped width reached far enough right to sit underneath
   the bottom seat's own kita-strip tiles instead of stopping short of them.
   Capped to a fraction of --felt-size (with white-space:nowrap so
   overflow/ellipsis actually has something to truncate against, rather than
   just wrapping to a second line) restores that safety margin for every
   screen size, not just the ones a human happened to test with a short
   name. */
/* Font floor 13px→11px→9px→13px — the name is the identity people
   actually recognize a seat by at a glance; the title below is secondary
   flavour text, so once both had room to grow (see .wb's comment above on
   where that room came from) the name got the larger share and .ptitle's
   own floor came back down to compensate (see its own comment) rather than
   both growing in lockstep. */
.pname{font-size:clamp(8px,calc(var(--table-size) * 0.01163),21px);font-weight:700;color:#fff;
  max-width:calc(var(--felt-size) * 0.132);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
/* Title under the in-game nameplate's name (see startGame in script.js,
   title in room.js's _seatInfo) — empty/collapses to nothing for
   guests/bots, which have no profile to hold one. table-size-scaled like
   .pname above rather than a fixed px, so it stays proportional at any
   window size. Same max-width cap as .pname above and for the same reason
   — this is the field that actually triggered the overlap (see comment
   there), since titles routinely run longer than names. */
/* Wraps to up to 2 lines instead of clipping to 1 — even at
   fitNameplateText's (script.js) 7px shrink floor, the longest title
   ("Disciplined Safe Player") still measured wider than this max-width on
   an actual phone (412px-wide viewport, checked directly against
   scrollWidth/clientWidth), so a single-line layout could never show it in
   full no matter how far the font shrank. Two lines gives it roughly
   double the effective width to lay out in, which is enough for every
   title in server/achievements.js/profiles.js at a size still readable —
   -webkit-line-clamp:2 (supported unprefixed as `line-clamp` in newer
   engines too, but the -webkit- form has the broadest support and doesn't
   need a fallback here) caps it at 2 visual lines and adds its own
   trailing ellipsis if some future title is still too long even wrapped,
   so fitNameplateText's shrink-then-clip fallback still applies past that
   point. word-break — a title is a curated string from achievements.js,
   not user input, but this keeps a single overlong word from overflowing
   the max-width sideways instead of wrapping. */
/* Most titles (server/achievements.js) are a single short word/phrase —
   only a handful ("Disciplined/Elite/Legendary Safe Player",
   "Thirteen-Sided Master") are long enough to actually need the 2-line
   wrap below. fitTitleText (script.js) picks between .one-line/.two-line
   per-title at render time: it first tries .one-line's larger size, and
   only falls back to .two-line's smaller wrapped size if that overflows
   the max-width — so a short title reads bigger instead of always paying
   the small size reserved for the longest one. */
.ptitle{color:var(--gold);opacity:.8;font-family:'Cinzel Decorative',serif;
  max-width:calc(var(--felt-size) * 0.132);overflow:hidden;line-height:1.15;
  word-break:break-word;text-align:left;
  /* Separates it from .pname above — with no gap the name and the first
     title line read as one crowded block. table-size-scaled like every
     other measurement here (checked at 3px on a 412px phone) rather than a
     flat px, so it stays proportional at any window size. */
  margin-top:clamp(1px,calc(var(--table-size) * 0.003974),4px);}
/* Matches .pname's own scale (see its comment above) — a short title at
   this size sits comfortably next to a same-tier name instead of looking
   like an afterthought. */
.ptitle.one-line{font-size:clamp(6px,calc(var(--table-size) * 0.007272),13px);
  white-space:nowrap;text-overflow:ellipsis;}
/* Floor 8px — compensates for .one-line above being bigger than the old
   single flat size (name is still the primary identity, title secondary),
   and stays readable specifically because it wraps to 2 lines instead of
   needing to fit "Disciplined Safe Player" on one. Re-checked at 8px on a
   412px phone: still 2 lines, well under the max-width cap. */
.ptitle.two-line{font-size:clamp(5px,calc(var(--table-size) * 0.00582),10px);
  white-space:normal;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;}
/* table-size-scaled like .pname/.ptitle above (was a fixed 10px) — this sits
   directly beside both in a bot's nameplate, so a flat size here stood out
   more and more as the window shrank instead of shrinking with them. */
.btag{font-size:clamp(9px,calc(var(--table-size) * 0.010909),19px);color:rgba(201,168,76,.5);}
/* Tenpai/furiten indicator — replaces the old .tenpai/.furiten-badge
   floating pills (see git history if reviving them). Those were positioned
   independently on the felt (bottom+left offsets tuned to approximate the
   nameplate's own position), which repeatedly drifted out of alignment on
   non-desktop aspect ratios — this is anchored directly INSIDE .np instead
   (see #statusdot-b in index.html, a child of #npb), so it inherits the
   nameplate's own already-correct position/size and structurally can't
   drift away from it on any screen shape.
   Only one dot, not two — furiten in this ruleset only ever happens WHILE
   tenpai (a winning tile sitting in your own discards, or passed on one),
   so "tenpai but not furiten" and "tenpai AND furiten" are the only two
   real states; there's no standalone "furiten but not tenpai". Colour
   alone carries that distinction (gold vs red below) rather than needing
   two separate badges that were never actually independent of each other.
   display:none by default; script.js's renderState toggles between the
   plain 'status-dot', '.status-dot tenpai', and '.status-dot furiten'
   classNames — .tenpai/.furiten below switch display back on, the base
   rule's display:none is what actually hides it the rest of the time. */
.status-dot{display:none;position:absolute;top:calc(var(--table-size) * -0.007);right:calc(var(--table-size) * -0.007);
  width:clamp(8px,calc(var(--table-size) * 0.018),16px);height:clamp(8px,calc(var(--table-size) * 0.018),16px);
  border-radius:50%;box-shadow:0 0 0 2px #1a1a1a;z-index:25;}
.status-dot.tenpai{display:block;background:#ffdd44;}
.status-dot.furiten{display:block;background:#e24b4a;}

/* ════════════════════════════════════════════════════════════
   TILES
   ══════════════════════════════════════════════════════
/* ── TILES ──
   All sizes driven by CSS vars set dynamically by JS on resize.
   This makes everything scale to fit any screen. */
.tile{position:relative;display:inline-flex;align-items:center;justify-content:center;cursor:default;flex-shrink:0;overflow:hidden;pointer-events:none;}

.tlg{
  width:var(--tlg-w,40px);height:var(--tlg-h,56px);border-radius:0.4vmin;
  background:linear-gradient(170deg,#fffdf4,#f8edd8);
  border:0.1vmin solid #c8a84a;border-bottom:0.2vmin solid #7a5c10;
  transition:transform .12s ease,box-shadow .12s ease;}
.tlg.play{cursor:pointer;pointer-events:auto;}
.tlg.play:hover,.tlg.play:active{transform:translateY(-1vmin);box-shadow:2px 12px 18px rgba(0,0,0,.7),inset 0 2px 0 rgba(255,255,255,.95);z-index:20;}
/* Selected: lifted with a deeper shadow only — no coloured outline ring. */
.tlg.sel{transform:translateY(-1vmin);box-shadow:2px 12px 18px rgba(0,0,0,.75);z-index:25;}

/* Draw tile — far right, taller glow, shimmer animation */
.tlg.drw{
  border-color:#ffe066;
  z-index:5;}

.tmd{
  width:var(--tmd-w,26px);height:var(--tmd-h,36px);border-radius:0.3vmin;
  background:linear-gradient(170deg,#fffdf4,#f8edd8);
  border:0.1vmin solid #c8a84a;border-bottom:0.2vmin solid #7a5c10;
  display:inline-flex;align-items:center;justify-content:center;}
/* Opponent concealed hand — a tile seen from above: the pale top face fills
   most of it, and the blue back face shows as a band along the near edge
   (toward table centre). Same 0.2vmin thickness edge as the player's own
   tiles (.tlg). #hdl / #hdt are counter-rotated 180deg (below) so that near
   edge lands toward centre for every seat. (Previously a flat teal back.) */
.tsm{
  display:inline-flex;
  width:var(--tsm-w,16px);height:var(--tsm-h,22px);
  border-radius:0.2vmin;
  background:linear-gradient(180deg,
    #fdf7e6 0%, #ecdcb6 68%,
    #fff8e6 70%, #fff8e6 71%,
    #37589f 72%, #223f7d 87%, #1a2c56 100%);
  border:0.1vmin solid #c8a84a;
  border-bottom:0.2vmin solid #12203c;
  flex-shrink:0;position:relative;
  box-shadow:0 0.15vmin 0.35vmin rgba(0,0,0,.4);}
/* Inset frame on the blue back-face band. */
.tsm::before{content:'';position:absolute;left:0.16vmin;right:0.16vmin;bottom:0.16vmin;top:76%;
  border:0.1vmin solid rgba(255,255,255,.18);border-radius:0.1vmin;}
.tsm.rot{width:var(--tsm-h,22px);height:var(--tsm-w,16px);}
/* Left & top opponents share the same container rotation as their neighbour
   opposite them, which would land the blue near-edge on the far side — spin
   each row 180deg so the top face points outward toward its owner. */
#hdl,#hdt{transform:rotate(180deg);}
.tdora{width:var(--tlg-w,46px);height:var(--tlg-h,64px);border-radius:0.4vmin;
  background:linear-gradient(170deg,#fffdf4,#f8edd8);border:0.1vmin solid #c8a84a;border-bottom:0.3vmin solid #7a5c10;
  display:inline-flex;align-items:center;justify-content:center;}

/* Hand layout */
.hr{display:flex;align-items:flex-end;flex-wrap:nowrap;}
/* Gap between main hand and draw tile */
.draw-sep{width:clamp(5px,1.5vmin,10px);flex-shrink:0;}
.mg{display:flex;margin-right:0;margin-left:0;align-items:flex-end;}
/* Called tile (chi/pon) rotated 90° to show it was taken from another player */
.mg .called{transform:rotate(-90deg) translateX(calc((var(--tmd-w,40px) - var(--tmd-h,26px))/2 + 1px));z-index:1;
  margin-right:calc(var(--tmd-h,40px)/2 - var(--tmd-w,26px)/2);z-index:2;
  margin-left:calc(var(--tmd-h,40px)/2 - var(--tmd-w,26px)/2);z-index:2;}
/* Kakan stack */
.mg .called-stack{position:relative;display:inline-flex;align-items:flex-end;justify-content:center;
  width:var(--tmd-h,40px);
  height:var(--tmd-h,35px);
  overflow:visible;
  margin:0;}
.mg .called-stack .kakan-base{position:absolute;bottom:calc((var(--tmd-w,26px) - var(--tmd-h,35px)) /2);left:50%;transform:translateX(-50%) rotate(-90deg);flex-shrink:0;z-index:4;}
.mg .called-stack .kakan-add{position:absolute;bottom:calc(var(--tmd-w,26px) - 1vmin);left:50%;transform:translateX(-50%) rotate(-90deg);z-index:3;}
.mr{display:flex;gap:0;align-items:flex-end;flex-shrink:0;}

/* Tile placement animations — classes added by animatePlaced() in
   script.js, which also mirrors these durations (PLACE_DROP_MS /
   PLACE_SLIDE_MS) so an animation can resume mid-flight after renderState
   rebuilds the DOM. Both animate the standalone scale/translate properties
   rather than transform, so tiles that already carry a transform of their
   own (.dz-riichi's rotate, .called, .kakan-add) keep it untouched.
   tile-drop: discards, kita and the kakan 4th tile — set down from above:
     starts enlarged (closer to the eye) with a soft halo shadow, lands with a
     small squash and settles. The halo is a centred blur, not an offset
     shadow, so it reads the same in every seat's rotated zone.
   meld-slide: a new meld group slides in from its owner's left toward the
     right edge of their meld row (each .ml-* container is already rotated to
     its owner, so "left" is the owner's own left for every seat). */
.tile-drop{animation:tile-drop 340ms cubic-bezier(.3,.7,.4,1) both;}
@keyframes tile-drop{
  0%  {opacity:0;scale:1.7;filter:drop-shadow(0 0 8px rgba(0,0,0,.55));}
  55% {opacity:1;scale:.94;filter:drop-shadow(0 0 2px rgba(0,0,0,.35));}
  78% {scale:1.03;}
  100%{opacity:1;scale:1;filter:none;}
}
.meld-slide{animation:meld-slide 420ms cubic-bezier(.2,.9,.3,1.12) both;}
@keyframes meld-slide{
  0%  {opacity:0;translate:calc(var(--tmd-w,26px) * -4) 0;}
  45% {opacity:1;}
  100%{opacity:1;translate:0 0;}
}
/* tile-draw: a freshly drawn tile slides in from the wall side (toward the
   table centre) into its hand row. That's -Y for my own tiles (.tlg) and +Y
   for opponents' (.tsm, whose near edge already faces the centre). */
.tile-draw{animation:tile-draw 300ms cubic-bezier(.2,.8,.3,1) both;}
.tsm.tile-draw{--draw-dir:1;}
@keyframes tile-draw{
  0%  {opacity:0;translate:0 calc(var(--draw-dir,-1) * 90%);}
  50% {opacity:1;}
  100%{opacity:1;translate:0 0;}
}
@media (prefers-reduced-motion:reduce){.tile-drop,.meld-slide,.tile-draw{animation:none;}}

/* ════════════════════════════════════════════════════════════
   ACTION BAR & BUTTONS
   ════════════════════════════════════════════════════════════ */
.actbar{display:flex;align-items:center;}
/* Floating action overlay — appears above the hand. flex-wrap:wrap (was
   nowrap) — up to 5 buttons can appear at once (RON+PON+KAN+CHI+SKIP, see
   drawActions in script.js), and on a narrow phone that row can run wider
   than the space actually left beside the nameplate; nowrap+flex-shrink:0
   just let it run off the felt/viewport edge instead, pushing the last
   button(s) out of reach. Now it wraps onto a second row instead.
   No width/max-width set here on purpose — drawActions (script.js) sets an
   explicit inline width every render, capped at whatever room is actually
   left beside the nameplate (which isn't fixed: see .pname/.ptitle's own
   max-width comments — a longer name or title widens the plate up to ITS
   cap). That has to happen in JS, not CSS: max-width alone left the actual
   used width up to the browser's ambiguous sizing pass for a wrapping flex
   item inside a shrink-to-fit ancestor (.np-wrap-b is position:absolute
   with no explicit width), wrapping buttons that fit comfortably under the
   intended budget. drawActions also caps that inline width at whatever the
   buttons ACTUALLY need (not always the full budget) — .act-overlay.visible
   below makes the whole box pointer-events:auto, so forcing it wider than
   its content for a 1-2 button turn would leave an invisible tap-blocking
   dead zone over the felt/hand tiles beneath it. row-gap slightly tighter
   than the button gap itself so two stacked rows read as one group, not
   two separate ones. */
.act-overlay{display:flex;row-gap:0.6vmin;align-items:center;
  z-index:40;pointer-events:none;flex-wrap:wrap;}
.act-overlay.visible{pointer-events:auto;}
.chi-picker{position:fixed;left:50%;bottom:18vh;transform:translateX(-50%);
  display:flex;gap:1.4vmin;align-items:center;z-index:60;
  background:rgba(8,18,12,.92);border:1px solid rgba(201,168,76,.35);
  border-radius:clamp(7px,0.8vmin,12px);padding:clamp(8px,1vmin,14px) clamp(10px,1.4vmin,18px);box-shadow:0 8px 28px rgba(0,0,0,.7);}
.chi-picker-opt{display:flex;gap:2px;align-items:flex-end;background:none;
  border:1px solid transparent;border-radius:8px;padding:4px;cursor:pointer;
  transition:all .15s;}
.chi-picker-opt:hover{background:rgba(201,168,76,.15);border-color:rgba(201,168,76,.5);
  transform:translateY(-2px);}
.chi-picker-disc{box-shadow:0 0 0 2px #00ffaa;border-radius:3px;}
.chi-picker-cancel{height:auto;align-self:stretch;}
/* Chonbo declare-confirm popup — reuses .chi-picker's overlay positioning
   but stacks vertically (message, then confirm, then cancel) instead of a
   row of tile options. */
.declare-picker{flex-direction:column;align-items:stretch;gap:10px;
  max-width:min(90vw,360px);border-color:rgba(255,128,64,.5);}
.declare-picker-msg{font-size:clamp(11px,1.4vmin,15px);line-height:1.5;color:rgba(245,237,216,.85);text-align:center;}
.declare-picker-confirm{width:100%;}
/* table-size-scaled (not raw vmin) with lowered floors — same fix, same
   reasoning, as .wb/.pname/.np's own comments above: raw vmin ignores the
   top bar/--table-size's 110%/86% factors, so on a short/wide window (or
   just a small phone, where these floors bind more often than intended)
   the buttons stayed near their old floor while the felt kept shrinking
   around them, reading as oversized. Floors lowered the same amount as
   .wb's icon (28px→22px) for height, proportionally for padding/font — a
   button label is short, all-caps text (TSUMO/RIICHI/PON/...), which stays
   legible well below the old floor, same as .wb's icon did. */
.ba{--ba-h:clamp(22px,calc(var(--table-size) * 0.03636),64px);--ba-cut:calc(var(--ba-h) * .32);--ba-edge:calc(var(--ba-h) * .045);
  --ba-bg:#333;--ba-bd:#666;color:var(--cream);
  position:relative;isolation:isolate;overflow:hidden;
  padding:0 calc(var(--ba-h) * .64);height:var(--ba-h);border:none;border-radius:0;
  font-family:'Cinzel Decorative',serif;font-size:clamp(9px,calc(var(--table-size) * 0.014545),26px);
  letter-spacing:.12em;cursor:pointer;transition:filter .15s;font-weight:700;white-space:nowrap;flex-shrink:0;
  /* F5 "slant": a forward-leaning parallelogram. The button itself is the
     edge colour; ::before, inset by --ba-edge with the same slant, holds
     the fill — clip-path can't take a border. It also clips box-shadow,
     so the drop shadow lives on the row (.actbar / .chi-picker) as a
     filter instead. */
  background:var(--ba-bd);
  clip-path:polygon(var(--ba-cut) 0,100% 0,calc(100% - var(--ba-cut)) 100%,0 100%);}
.ba::before{content:'';position:absolute;z-index:-1;inset:var(--ba-edge) calc(var(--ba-edge) * 1.5);background:var(--ba-bg);
  clip-path:polygon(calc(var(--ba-cut) - var(--ba-edge)) 0,100% 0,calc(100% - var(--ba-cut) + var(--ba-edge)) 100%,0 100%);}
/* The call's kanji (BA_KANJI in script.js), ghosted large behind the label
   and pushed toward the right edge, where the slant crops it. */
.ba-kj{position:absolute;z-index:-1;right:calc(var(--ba-h) * .2);top:50%;translate:0 -50%;
  font-family:'Noto Serif JP',serif;font-weight:900;font-style:normal;font-size:calc(var(--ba-h) * .95);line-height:1;
  letter-spacing:0;color:currentColor;opacity:.2;pointer-events:none;}
.ba:hover{filter:brightness(1.2);}
.ba:disabled{opacity:.3;cursor:default;filter:none;}
.hint{display:none;}
.bdis{--ba-bg:#8a1818;--ba-bd:#d44;color:#ffd8d8;}
.btsu{--ba-bg:#8a6500;--ba-bd:#f0d060;color:#fff8d0;font-weight:900;}
.brii{--ba-bg:#1e1e8a;--ba-bd:#77f;color:#d0d0ff;}
/* Ron can't glow outside its own edge any more (clip-path), so its fill
   is lit from inside instead. */
.bron{--ba-bg:radial-gradient(ellipse at 50% 35%,#d42020,#aa1010 70%);--ba-bd:#f66;color:#fff;font-weight:900;}
.bchi{--ba-bg:#145a14;--ba-bd:#4d4;color:#c0ffc0;}
.bpon{--ba-bg:#6a3800;--ba-bd:#ca0;color:#ffe0b0;}
.bkan{--ba-bg:linear-gradient(135deg,#3a0a5a,#6a1a9a);--ba-bd:#a060d0;color:#e8d0ff;}
/* Kita's teal — the same accent the kita call glow uses (.cg-kita), like
   .bpon/.bchi/.bkan matching their own calls. */
.bkita{--ba-bg:linear-gradient(135deg,#0a3a45,#0a6a80);--ba-bd:#20b0c0;color:#b0f0ff;}
.bpas{--ba-bg:#282828;--ba-bd:#555;color:rgba(245,237,216,.7);}
/* Kyuushu Kyuuhai — previously had no rule of its own at all (rendered as
   the browser's default grey button). */
.bkyu{--ba-bg:linear-gradient(135deg,#3a3a3a,#4a4a4a);--ba-bd:#777;color:#f5edd8;}
/* Chonbo (rules.chonbo) "attempt" declare — deliberately muted rather than
   a bold solid colour like the other action buttons, so it visually
   recedes instead of inviting a tap; the real confirmation is the
   declare-picker popup this opens (.declare-picker-confirm below, which
   DOES use the full warning colour, since that tap is the actual
   deliberate commit). */
.bchonbo{--ba-bg:#3a1a0c;--ba-bd:rgba(255,128,64,.6);color:#ffb080;opacity:.8;}
.bchonbo:hover{opacity:1;}
.declare-picker-confirm{--ba-bg:linear-gradient(135deg,#5a1a0a,#9a3a1a);--ba-bd:#ff8040;color:#ffd0b0;}

/* ════════════════════════════════════════════════════════════
   CHAT
   ════════════════════════════════════════════════════════════ */
/* Chat bar: fixed to bottom-right, sits above action bar */
/* --ui-vmin: same clamped-scale-unit approach as #s-start/#s-lobby/
   #dora-panel — everything below multiplies this one variable so it
   scales together instead of drifting apart across resolutions. */
/* Glass theme (chosen from the "Chat Bar Themes" mockup): frosted glass
   that lets the felt show through, square corners, one set of line icons.
   --cr is the corner radius for the bar and its popups; --cr-in, a step
   smaller, is for the buttons inside so their corners nest in the bar's.
   The chat popups below are siblings of .chat-bar, so they restate these. */
.chat-bar{--ui-vmin:clamp(6px,1vmin,10px);--cr:calc(var(--ui-vmin) * .5);--cr-in:calc(var(--cr) * .7);
  position:fixed;bottom:20px;right:20px;
  display:none;flex-direction:row;align-items:center;gap:calc(var(--ui-vmin) * .6);
  background:rgba(10,18,13,.45);-webkit-backdrop-filter:blur(10px) saturate(1.2);backdrop-filter:blur(10px) saturate(1.2);
  border-radius:var(--cr);padding:calc(var(--ui-vmin) * .6);z-index:65;
  box-shadow:inset 0 0 0 1px rgba(255,255,255,.08),0 6px 20px rgba(0,0,0,.35);}
/* Beginner-assist row — a child of .chat-bar (which is itself position:
   fixed, so it's already a containing block for this), absolutely anchored
   to sit directly above the bar's own left edge — left:0 here lines it up
   exactly with .chat-color-btn/.chat-emoji-btn, the first two children of
   .chat-bar right below it, rather than the input/send/history side.
   Being a child means it shows/hides automatically with the bar itself, no
   separate visibility toggle needed in script.js. Bare icons (no
   border/background "button" chrome, unlike .chat-color-btn/
   .chat-emoji-btn) — a drop-shadow instead keeps them legible over the
   felt table without needing a frame. */
.assist-bar{position:absolute;left:0;bottom:calc(100% + 8px);
  display:flex;flex-direction:row;gap:calc(var(--ui-vmin) * 1.111);}
.assist-btn{width:calc(var(--ui-vmin) * 3.6);height:calc(var(--ui-vmin) * 3.6);padding:0;cursor:pointer;
  display:flex;align-items:center;justify-content:center;color:rgba(245,237,216,.85);border:none;border-radius:var(--cr-in);
  background:rgba(10,18,13,.45);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);
  box-shadow:inset 0 0 0 1px rgba(255,255,255,.08);transition:color .2s,background .2s;}
.assist-btn:hover{color:var(--gl);background:rgba(10,18,13,.65);}
.assist-btn svg{width:56%;height:56%;display:block;}
.chat-input{width:calc(var(--ui-vmin) * 22.22);height:calc(var(--ui-vmin) * 3.611);padding:0 calc(var(--ui-vmin) * 1.111);background:rgba(255,255,255,.06);
  border:none;border-radius:var(--cr-in);
  color:var(--cream);font-size:calc(var(--ui-vmin) * 1.667);outline:none;font-family:'Noto Sans JP',sans-serif;}
.chat-input::placeholder{color:rgba(245,237,216,.42);}
.chat-input:focus{background:rgba(255,255,255,.1);box-shadow:inset 0 0 0 1px rgba(240,208,128,.5);}
/* The square glass buttons in the bar (colour, reactions, history) share
   this tile; send is the same tile in gold. */
.chat-color-btn,.chat-emoji-btn,.chat-hist-btn,.chat-send{width:calc(var(--ui-vmin) * 3.611);height:calc(var(--ui-vmin) * 3.611);
  flex-shrink:0;padding:0;border:none;border-radius:var(--cr-in);cursor:pointer;
  display:flex;align-items:center;justify-content:center;color:rgba(245,237,216,.85);background:rgba(255,255,255,.07);
  transition:background .2s,color .2s;}
.chat-color-btn:hover,.chat-emoji-btn:hover,.chat-hist-btn:hover{background:rgba(255,255,255,.13);color:var(--gl);}
.chat-emoji-btn.open,.chat-hist-btn.open,.chat-color-btn.open{background:rgba(240,208,128,.18);color:var(--gl);}
.chat-emoji-btn svg,.chat-hist-btn svg,.chat-send svg{width:56%;height:56%;display:block;}
/* Message colour swatch + popup palette */
/* The message colour shows as a small dot inside the tile (--chat-col is
   set by initColorPicker/selectChatColor in script.js). */
.chat-color-btn::after{content:'';width:42%;height:42%;border-radius:calc(var(--cr-in) * .6);background:var(--chat-col,#fff);box-shadow:0 0 0 1px rgba(0,0,0,.25);}
.chat-color-picker{--ui-vmin:clamp(6px,1vmin,10px);--cr:calc(var(--ui-vmin) * .5);--cr-in:calc(var(--cr) * .7);
  position:fixed;bottom:70px;right:20px;z-index:66;display:none;
  grid-template-columns:repeat(4,1fr);gap:8px;padding:10px;
  background:rgba(10,18,13,.6);-webkit-backdrop-filter:blur(14px) saturate(1.2);backdrop-filter:blur(14px) saturate(1.2);border-radius:var(--cr);
  box-shadow:inset 0 0 0 1px rgba(255,255,255,.08),0 10px 28px rgba(0,0,0,.4);}
.chat-color-picker.open{display:grid;}
.chat-color-swatch{width:24px;height:24px;border-radius:var(--cr-in);border:2px solid transparent;
  cursor:pointer;padding:0;transition:transform .15s,border-color .15s;}
.chat-color-swatch:hover{transform:scale(1.12);}
.chat-color-swatch.selected{border-color:var(--gl);}
/* Seat-emoji reaction swatch + popup — same shape/size as the colour
   swatch button right next to it, just showing an emoji face instead of a
   flat colour. .chat-emoji-picker reuses .chat-color-picker's grid/popup
   styling wholesale (see the two classes stacked on the div in
   index.html) rather than redefining the popup shell from scratch. */
.chat-emoji-picker .emoji-swatch{width:32px;height:32px;border-radius:var(--cr-in);border:2px solid transparent;
  background:none;font-size:1.4rem;line-height:1;cursor:pointer;padding:0;
  transition:transform .15s,border-color .15s,background .15s;}
.chat-emoji-picker .emoji-swatch:hover{transform:scale(1.15);background:rgba(255,255,255,.08);}
.chat-send{background:var(--gl);color:#1a1206;}
.chat-send:hover{background:#ffe9b0;}
/* History button: unread count in a small square badge on its corner */
.chat-hist-btn{position:relative;}
.chat-hist-btn .badge{position:absolute;top:calc(var(--ui-vmin) * -.6);right:calc(var(--ui-vmin) * -.6);
  background:#e0503f;color:#fff;font-size:calc(var(--ui-vmin) * 1.111);font-weight:700;
  min-width:calc(var(--ui-vmin) * 1.944);height:calc(var(--ui-vmin) * 1.944);border-radius:calc(var(--cr-in) * .6);display:none;
  align-items:center;justify-content:center;padding:0 calc(var(--ui-vmin) * .278);}
.chat-hist-btn .badge.show{display:flex;}
/* History popup — opens upward from chat bar. A SIBLING of .chat-bar in
   index.html, not a descendant (both sit directly under #s-game), so it
   never inherited .chat-bar's own --ui-vmin — its text was flat px this
   whole time while everything else in the game screen scaled with the
   window. --ui-vmin here (same clamp as .chat-bar/#s-lobby/etc.) plus
   converting those flat sizes to the calc() pattern brings it back in
   line, especially on small screens where the mismatch was worst. */
.chat-panel{--ui-vmin:clamp(6px,1vmin,10px);--cr:calc(var(--ui-vmin) * .5);--cr-in:calc(var(--cr) * .7);position:fixed;bottom:106px;right:12px;
  width:clamp(220px,35vw,300px);max-height:260px;
  background:rgba(10,18,13,.6);-webkit-backdrop-filter:blur(14px) saturate(1.2);backdrop-filter:blur(14px) saturate(1.2);
  border-radius:var(--cr);z-index:74;display:none;flex-direction:column;
  overflow:hidden;box-shadow:inset 0 0 0 1px rgba(255,255,255,.08),0 10px 32px rgba(0,0,0,.45);
  animation:slideUpChat .2s ease;}
@keyframes slideUpChat{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
.chat-panel.open{display:flex;}
.chat-panel-header{padding:calc(var(--ui-vmin) * .972) calc(var(--ui-vmin) * 1.667);
  border-bottom:1px solid rgba(255,255,255,.07);font-size:calc(var(--ui-vmin) * 1.389);font-weight:700;letter-spacing:.04em;
  color:var(--cream);font-family:'Noto Sans JP',sans-serif;
  display:flex;justify-content:space-between;align-items:center;}
.chat-close{background:none;border:none;color:rgba(245,237,216,.45);
  font-size:calc(var(--ui-vmin) * 2.083);cursor:pointer;line-height:1;padding:0;}
.chat-close:hover{color:var(--cream);}
.chat-history{flex:1;overflow-y:auto;padding:calc(var(--ui-vmin) * 1.111);display:flex;flex-direction:column;gap:5px;}
.chat-history::-webkit-scrollbar{width:4px;}
.chat-history::-webkit-scrollbar-thumb{background:rgba(201,168,76,.3);border-radius:2px;}
.chat-entry{display:flex;flex-direction:column;gap:1px;}
.chat-entry-who{font-size:calc(var(--ui-vmin) * 1.25);letter-spacing:1px;font-weight:700;}
/* Text bubble + its reaction pills share one row so the pills read as
   attached to the end of THIS message, not a separate block for the
   whole entry — wraps onto its own line only if it doesn't fit. */
.chat-entry-msg-row{display:flex;align-items:center;flex-wrap:wrap;gap:5px;}
.chat-entry-text{font-size:calc(var(--ui-vmin) * 1.667);color:var(--cream);background:rgba(255,255,255,.08);
  border-radius:var(--cr-in);padding:calc(var(--ui-vmin) * .556) calc(var(--ui-vmin) * 1.111);line-height:1.4;word-break:break-word;}
.chat-entry-time{font-size:calc(var(--ui-vmin) * 1.111);color:rgba(245,237,216,.3);margin-top:1px;}
/* Long-press feedback (see attachLongPress in script.js) — a brief
   highlight while the hold timer is counting down, so touch/mouse users
   get some indication something's about to happen before the picker
   actually appears. */
.chat-entry.pressing .chat-entry-text{background:rgba(201,168,76,.18);}
/* Per-message reaction pills — one per DISTINCT emoji used on that
   message (two people picking the same emoji still collapse to one pill,
   per request — see renderMessageReactions). .mine marks a pill that
   includes MY OWN reaction, so re-long-pressing the MESSAGE (opening the
   picker again) can show which one I already picked (see
   openReactionPicker) — long-pressing the PILL itself instead shows who's
   behind it (see attachPillLongPress/showReactionNames), hence the
   cursor:pointer/select:none affordance here. */
.chat-entry-reactions{display:flex;flex-wrap:wrap;gap:4px;}
.chat-entry-reactions:empty{display:none;}
.reaction-pill{display:inline-flex;align-items:center;
  font-size:calc(var(--ui-vmin) * 1.5);line-height:1.4;cursor:pointer;user-select:none;
  background:rgba(201,168,76,.12);border:1px solid rgba(201,168,76,.25);
  border-radius:var(--cr-in,3px);padding:0 calc(var(--ui-vmin) * .694);}
.reaction-pill.mine{background:rgba(201,168,76,.3);border-color:var(--gl);}
/* Long-press reaction picker — same dark/gold popup shell as
   .chat-color-picker/.chat-panel, positioned dynamically (JS, via the
   pressed message's own getBoundingClientRect) rather than pinned to a
   fixed corner like those two, since it needs to appear next to whichever
   message was pressed. */
.msg-react-picker{--ui-vmin:clamp(6px,1vmin,10px);--cr:calc(var(--ui-vmin) * .5);--cr-in:calc(var(--cr) * .7);position:fixed;z-index:400;display:flex;gap:6px;
  padding:calc(var(--ui-vmin) * .833) calc(var(--ui-vmin) * 1.111);
  background:rgba(10,18,13,.6);-webkit-backdrop-filter:blur(14px) saturate(1.2);backdrop-filter:blur(14px) saturate(1.2);border-radius:var(--cr);
  box-shadow:inset 0 0 0 1px rgba(255,255,255,.08),0 10px 28px rgba(0,0,0,.4);animation:slideUpChat .15s ease;}
/* Names-behind-a-pill popup (see showReactionNames in script.js) — same
   shell as .msg-react-picker again, just a text label instead of buttons. */
.reaction-names-popup{--ui-vmin:clamp(6px,1vmin,10px);position:fixed;z-index:400;
  padding:calc(var(--ui-vmin) * .556) calc(var(--ui-vmin) * .972);
  font-size:calc(var(--ui-vmin) * 1.389);color:var(--cream);white-space:nowrap;
  background:rgba(10,18,13,.6);-webkit-backdrop-filter:blur(14px) saturate(1.2);backdrop-filter:blur(14px) saturate(1.2);border-radius:calc(var(--ui-vmin) * .5);
  box-shadow:inset 0 0 0 1px rgba(255,255,255,.08),0 10px 28px rgba(0,0,0,.4);animation:slideUpChat .15s ease;}
.msg-react-picker button{font-size:1.3rem;line-height:1;background:none;border:2px solid transparent;
  border-radius:var(--cr-in);width:34px;height:34px;padding:0;cursor:pointer;
  transition:transform .15s,border-color .15s,background .15s;}
.msg-react-picker button:hover{transform:scale(1.15);background:rgba(201,168,76,.15);}
.msg-react-picker button.selected{border-color:var(--gl);}
/* Portrait phones: --ui-vmin's 6px floor left the chat buttons ~22px — too
   small to tap. A bigger unit here (buttons ~33px) with the bar stretched
   across the bottom and the input taking the spare width, so it still fits
   a 375px screen. The input's text is 16px so iOS doesn't zoom in on focus.
   The popups move up to clear the taller bar and the help icons above it. */
@media (max-width:600px){
  .chat-bar,.chat-color-picker,.chat-panel,.msg-react-picker,.reaction-names-popup{--ui-vmin:9px;}
  .chat-bar{left:12px;right:12px;bottom:12px;}
  .chat-input{flex:1;width:auto;min-width:0;font-size:16px;}
  .chat-color-picker{bottom:118px;right:12px;}
  .chat-color-swatch{width:32px;height:32px;}
  .chat-emoji-picker .emoji-swatch{width:38px;height:38px;font-size:1.6rem;}
  .chat-panel{bottom:118px;right:12px;left:12px;width:auto;max-height:45vh;}
}

/* Floating chat messages — niconico-style danmaku: each message spawns at
   a random height and slides right-to-left across the full screen at a
   constant opacity, disappearing (clipped by .chat-float's overflow, no
   fade) once it crosses the left edge. .chat-float is a clipped
   full-viewport layer; each .chat-msg carries its own random `top` (set
   inline in JS, see showChatMsg) and animates independently rather than
   the old centred fade-in-place stack. Default text colour is white;
   see .chat-color-picker for the per-player colour override. */
.chat-float{position:fixed;inset:0;z-index:300;pointer-events:none;overflow:hidden;}
/* Glass theme: Noto Sans JP with a dark outline (paint-order keeps the
   stroke behind the fill) instead of MS Gothic, so messages stay readable
   over tiles and match the rest of the UI. */
.chat-msg{position:absolute;left:100%;white-space:nowrap;color:#fff;
  font-family:'Noto Sans JP',sans-serif;font-weight:700;-webkit-text-stroke:.06em rgba(0,0,0,.85);paint-order:stroke fill;
  font-size:clamp(20px,3.2vmin,34px);letter-spacing:1px;
  text-shadow:0 0 6px rgba(0,0,0,.9),0 0 16px rgba(0,0,0,.6),1px 1px 3px rgba(0,0,0,.95);
  animation:chatSlide 7s linear forwards;}
.chat-msg .chat-who{font-size:.7em;color:var(--gl);display:inline;letter-spacing:1px;}
.chat-msg .chat-who::after{content:": ";color:rgba(240,208,128,.6);}
/* Travels its own width plus the full viewport width, so it always fully
   exits past the left edge regardless of message length. No opacity
   animation — .chat-float's overflow:hidden is what makes it disappear. */
@keyframes chatSlide{
  0%{transform:translateX(0);}
  100%{transform:translateX(calc(-100vw - 100%));}
}

/* Seat-emoji popups (see showEmojiPopup in script.js) — a separate
   unrotated fixed layer, positioned per-popup via inline left/top (JS
   computes each from the target seat's on-screen rect, since .pa-t/.pa-l/
   .pa-r are themselves CSS-rotated — see the layer's own comment in
   index.html). 4s total lifetime matches the emojiPop animation length
   exactly, so JS's setTimeout(...,4000) removal lands right as it
   finishes fading rather than cutting it off mid-animation. */
/* z-index:150 — above every table/game element (nameplates/tiles/action
   bar top out around 60) but below the round-start card (.rs-layer:210), game-start roll
   call (.gs-layer:210), toast (.tc:200), loading overlay (#game-loading:
   200), and the end-game panel (#s-end:250) — a reaction shouldn't ever
   cover any of those. */
.emoji-layer{position:fixed;inset:0;z-index:150;pointer-events:none;overflow:hidden;}
/* Plain anchor point — left/top set inline (JS) to the seat's on-screen
   centre, no transform of its own. See showEmojiPopup's own comment in
   script.js for what each of the three nested elements below is for. */
.emoji-pop{position:fixed;left:0;top:0;}
/* STATIC centre+rotate (set once, inline, by JS — not animated). Rotating
   HERE rather than on .emoji-pop-motion means the motion keyframe's own
   translateY below runs inside this already-rotated frame, so "float up"
   comes out as the correct on-screen direction for whichever seat this
   is, not always screen-up regardless of seat. */
.emoji-pop-rotate{display:inline-block;}
/* Max raised well past mobile's actual reach (28-48px, still clamp's
   floor there) so the glyph keeps growing on genuinely large/4K
   viewports instead of capping out at a size that reads as tiny relative
   to how big the rest of the table renders there. */
/* line-height:1 (not the default `normal`, ~1.15-1.2x font-size) — without
   it, this box is taller than the glyph itself by a platform/font-
   dependent margin, and .emoji-pop-rotate's translate(-50%,-50%) centres
   on THIS box, not the visible ink inside it. Different platforms' native
   emoji fonts (Segoe UI Emoji on Windows/desktop vs. Apple/Noto Color
   Emoji on phones) pad that extra space differently, so the same
   centering math could still look visibly off-centre on one platform and
   fine on another — this removes that variable rather than trying to
   compensate for a specific font's metrics. */
.emoji-pop-motion{display:inline-block;line-height:1;font-size:clamp(28px,6vmin,100px);
  text-shadow:0 0 10px rgba(0,0,0,.6);
  animation:emojiPop 4s ease forwards;}
/* Distances in vmin (viewport-relative), not px or %-of-self — a fixed px
   float distance cleared the tile row fine on a phone-sized viewport but
   nowhere near enough on a 4K screen, where the same vmin-based table
   layout renders far larger in actual pixels. Floats away almost
   immediately (by 18%, ~0.7s in) rather than lingering at the anchor
   centre for most of the 4s lifetime, so it's not still sitting on top of
   the tiles for the bulk of the animation. */
@keyframes emojiPop{
  0%{opacity:0;transform:scale(.4);}
  10%{opacity:1;transform:scale(1.15);}
  18%{opacity:1;transform:translateY(-7vmin) scale(1);}
  85%{opacity:1;transform:translateY(-8vmin) scale(1);}
  100%{opacity:0;transform:translateY(-9.5vmin) scale(.9);}
}

/* ── Toast notifications ── */
.tc{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:200;pointer-events:none;}
.toast{background:rgba(0,0,0,.9);border:2px solid var(--gold);border-radius:12px;padding:12px 28px;font-family:'Cinzel Decorative',serif;font-size:clamp(14px,4vw,20px);letter-spacing:3px;color:var(--gl);text-align:center;animation:ta 2.8s ease forwards;box-shadow:0 0 50px rgba(201,168,76,.4);margin-bottom:8px;}
@keyframes ta{0%{opacity:0;transform:scale(.8) translateY(18px)}15%{opacity:1;transform:scale(1)}75%{opacity:1}100%{opacity:0;transform:scale(1.05) translateY(-8px)}}

/* ── Pon/chi/kan/kita cue (see callGlow in script.js) — the caller's edge of
   the felt glows in the call's colour, behind their tiles (it's .felt's first
   child). No text: the side says who called, the colour says what. Sized off
   --felt-size like the rest of the table; 1.5s. ── */
.cg-pon{--cg:#e3b84a;}.cg-chi{--cg:#4fbf72;}.cg-kan{--cg:#a77be0;}.cg-kita{--cg:#2bb3c4;}
.cg-edge{position:absolute;pointer-events:none;opacity:0;animation:cg-glow 1.5s ease forwards;
  --cg-depth:calc(var(--felt-size) * .16);}
.cg-edge.cg-b{left:0;right:0;bottom:0;height:var(--cg-depth);background:linear-gradient(0deg,var(--cg),transparent);}
.cg-edge.cg-t{left:0;right:0;top:0;height:var(--cg-depth);background:linear-gradient(180deg,var(--cg),transparent);}
.cg-edge.cg-l{top:0;bottom:0;left:0;width:var(--cg-depth);background:linear-gradient(90deg,var(--cg),transparent);}
.cg-edge.cg-r{top:0;bottom:0;right:0;width:var(--cg-depth);background:linear-gradient(270deg,var(--cg),transparent);}
@keyframes cg-glow{0%{opacity:0}15%{opacity:.55}75%{opacity:.4}100%{opacity:0}}
@media (prefers-reduced-motion:reduce){
  .cg-edge{animation:none;opacity:.45;}
}

/* ── Round-start title card — ink brush-in, sits above #game-loading (z-index
   200) since it's meant to be read together with the "dealing tiles" scrim
   rather than racing it. ── */
.rs-layer{position:fixed;inset:0;z-index:210;pointer-events:none;
  display:flex;align-items:center;justify-content:center;}
/* While the round-start card is actually showing, block clicks on the
   board underneath — #game-loading (round 1's "dealing tiles" overlay)
   already does this naturally since it has no pointer-events:none, but it
   hides as soon as the next game_state arrives (often well under a
   second), while this card keeps animating for its full 3.4s. Without
   this, round 2+ becomes clickable/discardable underneath a still-visible
   "東2局" announcement. Toggled by roundStartBanner() in script.js,
   on for exactly as long as the scrim/plate are in the DOM. */
.rs-layer.blocking{pointer-events:auto;}
/* Styled like the roll call and the standings card: the cutscene's themed
   backdrop, the game type, a brush 東二局 stamp (cs-stamp) with spaced
   romaji, and one ink strip (re-st-row) with the dealer and honba. */
.rs-layer{--ui-vmin:clamp(5px,min(1vw,1.4vh),12px);}
.rs-scrim{position:absolute;inset:0;z-index:0;
  background:repeating-linear-gradient(45deg,rgba(201,168,76,.025) 0 2px,transparent 2px 6px),var(--bg-theme);background-color:#060c07;
  opacity:0;animation:rs-scrim-hold 3.2s ease forwards;}
.rs-plate{position:relative;z-index:1;display:flex;flex-direction:column;align-items:center;gap:calc(var(--ui-vmin) * .6);
  width:min(calc(var(--ui-vmin) * 56),94vw);
  animation:rs-hold 3.2s ease forwards;}
.rs-sub{font-family:'Noto Serif JP',serif;font-weight:700;font-size:calc(var(--ui-vmin) * 1.8);letter-spacing:.25em;
  color:rgba(245,237,216,.75);opacity:0;animation:cs-drop-in .25s ease .28s forwards;}
.rs-kanji{font-family:'Noto Serif JP',serif;font-weight:900;font-size:calc(var(--ui-vmin) * 11);line-height:1.1;letter-spacing:.06em;white-space:nowrap;
  color:var(--gl);text-shadow:0 .6vmin 0 var(--gd);opacity:0;animation:cs-stamp .38s cubic-bezier(.2,1.6,.4,1) .12s forwards;}
.rs-romaji{font-family:'Cinzel Decorative',serif;font-weight:700;font-size:calc(var(--ui-vmin) * 1.7);letter-spacing:.55em;
  color:var(--gold);opacity:0;animation:cs-rise .25s ease .34s forwards;}
.rs-strip{align-self:stretch;display:flex;align-items:center;justify-content:center;gap:calc(var(--ui-vmin) * 2.4);
  margin-top:calc(var(--ui-vmin) * 1.8);padding:calc(var(--ui-vmin) * 1.2) calc(var(--ui-vmin) * 4);
  background:linear-gradient(90deg,transparent,rgba(11,13,12,.92) 8%,rgba(11,13,12,.92) 92%,transparent);
  clip-path:polygon(2% 0,100% 0,98% 100%,0 100%);
  opacity:0;animation:re-st-row .45s cubic-bezier(.2,1.3,.4,1) .6s forwards;}
.rs-strip>span{display:flex;align-items:baseline;gap:calc(var(--ui-vmin) * 1);min-width:0;}
.rs-strip b{flex:none;font-family:'Noto Serif JP',serif;font-weight:700;font-size:calc(var(--ui-vmin) * 2.6);line-height:1;color:var(--gl);}
.rs-v{font-family:'Cinzel Decorative',serif;font-weight:700;font-size:calc(var(--ui-vmin) * 2);color:var(--cream);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.rs-strip>i{flex:none;width:1px;align-self:stretch;background:rgba(201,168,76,.4);}
@keyframes rs-hold{0%,78%{opacity:1;}100%{opacity:0;}}
@keyframes rs-scrim-hold{0%{opacity:0;}12%,78%{opacity:1;}100%{opacity:0;}}
@media (prefers-reduced-motion:reduce){
  .rs-plate,.rs-plate>*,.rs-scrim{animation:none;opacity:1;transform:none;}
}

/* ── Game-start roll call — shown once per game, right after the FIRST real
   game_state (i.e. once the board underneath is already fully dealt, not
   during the "配牌中/DEALING TILES" placeholder — see gameStartIntro() and
   the game_state branch of onMsg in script.js for why it waits). Same
   z-index/gating pattern as .rs-layer, reusing roundStartBanner's own
   roundStartBannerActive/pendingBannerGameState gate rather than a
   parallel one — the two never actually overlap, since this only ever
   plays once per game (right after its first state) and .rs-layer only
   from round 2 onward. Rows clone .up-nameplate/.pnp-* — the SAME
   icon+name+title plate used in the User Profile hub and in another
   player's card (see index.html's #userprofile-modal and renderPlayerCard
   in script.js) — rather than the in-game .np nameplate, so this reads as
   each player's actual profile identity, not their at-the-table plate.
   --ui-vmin here matches .about-modal's own value exactly (both scoped
   locally, not on :root) so a cloned plate renders at the identical scale
   it would inside that modal. */
.gs-layer{--ui-vmin:clamp(5px,min(1vw,1.4vh),12px);position:fixed;inset:0;z-index:210;
  pointer-events:none;display:flex;align-items:center;justify-content:center;}
.gs-layer.blocking{pointer-events:auto;}
/* Styled as the win cutscene's standings card (.re-standings): the same
   themed backdrop, a brush stamp heading, and one ink strip per seat
   sliding in (re-st-row), reusing the cutscene's keyframes. */
.gs-scrim{position:absolute;inset:0;z-index:0;
  background:repeating-linear-gradient(45deg,rgba(201,168,76,.025) 0 2px,transparent 2px 6px),var(--bg-theme);background-color:#060c07;
  opacity:0;animation:rs-scrim-hold 5s ease forwards;}
.gs-plate{position:relative;z-index:1;display:flex;flex-direction:column;
  align-items:center;gap:calc(var(--ui-vmin) * 1.8);
  width:min(calc(var(--ui-vmin) * 64),94vw);padding-block:calc(var(--ui-vmin) * 2.5);
  max-height:calc(100vh - 48px);overflow-y:auto;overflow-x:hidden;
  opacity:0;animation:rs-hold 5s ease forwards;}
.gs-head{display:flex;flex-direction:column;align-items:center;gap:calc(var(--ui-vmin) * .5);}
.gs-sub{font-family:'Noto Serif JP',serif;font-weight:700;font-size:calc(var(--ui-vmin) * 1.7);letter-spacing:.25em;
  color:rgba(245,237,216,.75);opacity:0;animation:cs-drop-in .25s ease .28s forwards;}
.gs-kanji{font-family:'Noto Serif JP',serif;font-weight:900;font-size:calc(var(--ui-vmin) * 6.2);line-height:1.1;letter-spacing:.08em;
  color:var(--gl);text-shadow:0 .5vmin 0 var(--gd);opacity:0;animation:cs-stamp .38s cubic-bezier(.2,1.6,.4,1) .12s forwards;}
.gs-romaji{font-family:'Cinzel Decorative',serif;font-weight:700;font-size:calc(var(--ui-vmin) * 1.6);letter-spacing:.55em;
  color:var(--gold);opacity:0;animation:cs-rise .25s ease .34s forwards;}
.gs-rows{display:flex;flex-direction:column;align-self:stretch;gap:calc(var(--ui-vmin) * .6);}
/* Wind sits OUTSIDE the cloned .up-nameplate — the seat wind is this
   game's seating, not part of a player's profile identity. */
.gs-row{position:relative;display:flex;align-items:center;gap:calc(var(--ui-vmin) * 2);
  padding:calc(var(--ui-vmin) * 1) calc(var(--ui-vmin) * 3.5);
  background:linear-gradient(90deg,transparent,rgba(11,13,12,.92) 6%,rgba(11,13,12,.92) 94%,transparent);
  clip-path:polygon(2% 0,100% 0,98% 100%,0 100%);
  opacity:0;animation:re-st-row .45s cubic-bezier(.2,1.3,.4,1) forwards;}
.gs-row.gs-me::after{content:'';position:absolute;left:4%;right:6%;bottom:0;height:2px;background:var(--gold);}
.gs-row .gs-wind{flex:none;width:calc(var(--ui-vmin) * 4.4);text-align:center;
  font-family:'Noto Serif JP',serif;font-weight:900;font-size:calc(var(--ui-vmin) * 3.8);line-height:1;color:var(--cream);}
.gs-row.gs-dealer .gs-wind{color:var(--gl);text-shadow:0 .3vmin 0 var(--gd);}
/* Unboxed player tag (playerTag in script.js) on the roll call and the
   game-over standings: icon, name and title straight on the ink strip, the
   tier shown as metal lettering — the name filled with the tier metal, the
   icon frame and title in its colour. Bronze (no tier class) is the default.
   --m1/--m2/--m3 run light→dark down the letters; --mt is the flat colour. */
.pl-tag{--m1:#f0b47a;--m2:#a86e38;--m3:#5a3518;--mt:#d9965a;
  display:flex;align-items:center;gap:calc(var(--ui-vmin, 7.2px) * 1.4);flex:1 1 auto;min-width:0;}
.pl-tag.tier-silver{--m1:#ffffff;--m2:#b5b7be;--m3:#5c5d63;--mt:#d6d8de;}
.pl-tag.tier-gold{--m1:#fff3c0;--m2:#d9a640;--m3:#7a5410;--mt:#f0c860;}
.pl-tag.tier-platinum{--m1:#ffffff;--m2:#a9d0da;--m3:#4f7680;--mt:#bfe2ea;}
.pl-tag.tier-diamond{--m1:#ffffff;--m2:#8fd3f0;--m3:#3a7fb0;--mt:#bfe8fb;}
.pl-icon{flex:none;width:calc(var(--ui-vmin, 7.2px) * 5.2);height:calc(var(--ui-vmin, 7.2px) * 5.2);}
.pl-icon svg{display:block;width:100%;height:100%;}
/* Every free icon (and the guest icon) opens with its gold frame rect, the
   only rect drawn with a stroke; paid icons' art is left alone. */
.pl-icon svg>rect[stroke]:first-child{stroke:var(--mt);}
.pl-who{display:flex;flex-direction:column;min-width:0;}
.pl-name{font-family:'Cinzel Decorative',serif;font-weight:700;font-size:calc(var(--ui-vmin, 7.2px) * 2.1);line-height:1.25;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
  color:transparent;background:linear-gradient(180deg,var(--m1) 0%,var(--m2) 55%,var(--m3) 100%);-webkit-background-clip:text;background-clip:text;
  filter:drop-shadow(0 calc(var(--ui-vmin, 7.2px) * .2) 0 rgba(0,0,0,.6));}
.pl-title{font-size:calc(var(--ui-vmin, 7.2px) * 1.25);letter-spacing:.14em;line-height:1.3;min-height:1.3em;color:var(--mt);opacity:.85;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
@media (prefers-reduced-motion:reduce){
  .gs-plate,.gs-head>*,.gs-row,.gs-scrim{animation:none;opacity:1;}
}

/* ════════════════════════════════════════════════════════════
   ROUND END SCREEN
   ════════════════════════════════════════════════════════════ */
/* Darkened overlay, not a solid screen — #s-game is deliberately left
   visible underneath (see show() in script.js) so the final board state
   reads as a dimmed backdrop instead of a blank black screen. Matches the
   existing modal-backdrop darkness (.about-backdrop) for consistency. */
/* --ui-vmin: same clamped-scale-unit approach as #s-start/#s-lobby (see
   their own comments) — without this, everything below that references
   var(--ui-vmin) (the reused .ready-btn/.bp buttons in .reb, plus every
   calc(var(--ui-vmin)*N) rule added below) had no ancestor defining it and
   silently fell back to invalid/zero sizing. The panel used flat px/vw
   values before, which don't share the 6-10px clamp everything else here
   plateaus at — this brings it in line with the rest of the app instead of
   scaling independently (or not at all) on very small/large screens. */
#s-end{--ui-vmin:clamp(6px,1vmin,10px);background:rgba(0,0,0,.6);z-index:250;flex-direction:column;}
/* Width trimmed from 77.78 to 70 units — at the old width, the game-over
   action row's two buttons (fixed-width .reb-left + auto-width Leave
   Room) left a large dead gap between them under justify-content:
   space-between, since .rec was far wider than the row's actual content
   needed. Still comfortably fits the widest realistic content (a 17-tile
   hand + a meld — see fitEndTileSize in script.js, which sizes those
   tiles to whatever width .rec actually gives it, so this doesn't risk
   re-breaking that). */
/* Horizontal padding uses clamp(...,vw,...) rather than pure --ui-vmin —
   --ui-vmin bottoms out at a fixed 6px floor (see its own comment above),
   so past that floor this padding used to stay frozen at ~38px/side even
   as the panel itself kept shrinking with the viewport. That fixed cost
   was exactly what starved .reb-split's game-over action row of room to
   stay on one line on real phone widths (~360-414px) — see .reb-split's
   own comment. The vw term lets padding keep shrinking below the vmin
   floor on narrow screens instead of eating into content width forever. */
.rec{background:linear-gradient(155deg,rgba(18,52,30,.98),rgba(6,16,9,.99));border:2px solid var(--gold);border-radius:16px;
  padding:calc(var(--ui-vmin, 7.2px) * 4.722) clamp(14px, 4.5vw, calc(var(--ui-vmin, 7.2px) * 6.389));text-align:center;
  box-shadow:0 0 100px rgba(201,168,76,.35);width:min(calc(var(--ui-vmin, 7.2px) * 70), 96vw);position:relative;}
.rec::before{content:'';position:absolute;top:0;left:0;right:0;height:2px;background:linear-gradient(90deg,transparent,var(--gold),transparent);}
.ret{font-family:'Cinzel Decorative',serif;font-size:calc(var(--ui-vmin, 7.2px) * 3.125);color:var(--gl);filter:drop-shadow(0 0 14px rgba(201,168,76,.6));margin-bottom:5px;}
/* Double/triple ron prev/next nav (see renderWinnerNav in script.js) —
   display:none until .visible is added; there's nothing to navigate for
   every other outcome (a single winner or none). */
.re-winner-nav{display:none;align-items:center;justify-content:center;
  gap:calc(var(--ui-vmin, 7.2px) * 1.111);}
.re-winner-nav.visible{display:flex;margin-bottom:calc(var(--ui-vmin, 7.2px) * 1.111);}
.re-winner-arrow{background:rgba(0,0,0,.4);border:1px solid rgba(201,168,76,.3);border-radius:50%;
  width:calc(var(--ui-vmin, 7.2px) * 2.5);height:calc(var(--ui-vmin, 7.2px) * 2.5);flex-shrink:0;
  color:var(--gold);font-size:calc(var(--ui-vmin, 7.2px) * 1.1);line-height:1;cursor:pointer;
  display:flex;align-items:center;justify-content:center;transition:all .2s;}
.re-winner-arrow:hover{border-color:var(--gold);background:rgba(201,168,76,.15);}
/* Just a position count ("1 / 2"), not the winner's name — that already
   shows in #rehn right below, so repeating it here would be redundant. */
.re-winner-indicator{font-family:'Cinzel Decorative',serif;color:rgba(245,237,216,.75);
  font-size:calc(var(--ui-vmin, 7.2px) * 1.25);letter-spacing:1px;
  min-width:calc(var(--ui-vmin, 7.2px) * 6);text-align:center;}
/* Wraps #rehn/#reyl/#rewt only (see index.html) — switching winner tabs
   restarts this animation (toggle animation to 'none' then back, forcing a
   reflow in between — the standard way to replay a CSS animation that's
   already applied) so the newly-selected winner's info visibly slides in
   rather than just snapping to the new content. Plays on the very first
   render too (nothing suppresses it there), which is deliberate — every
   winner reveal reads consistently, not just tab switches. */
.re-win-content{animation:re-win-slide-in .35s ease;}
@keyframes re-win-slide-in{from{opacity:0;transform:translateX(24px);}to{opacity:1;transform:translateX(0);}}
@media (prefers-reduced-motion:reduce){
  .re-win-content{animation:none;}
}
.rehn{font-size:calc(var(--ui-vmin, 7.2px) * 1.5625);color:var(--jl);letter-spacing:2px;margin-bottom:12px;}
.re-hand{color:rgba(245,237,216,.6);font-size:0.9em;}
/* Chonbo's offender name reuses .re-winner's markup (see onEnd in
   script.js) but must NOT read as a win — .rehn's own green (--jl)
   otherwise applies here by inheritance, same "success" colour used for
   an actual winner. */
.re-offender{color:#e07070;}
.re-ord{display:inline-block;font-weight:700;margin-right:calc(var(--ui-vmin, 7.2px) * 1.389);}
/* Placement change (see reorderEndStandings): the new placement pops in,
   and a row that climbed flashes gold on the name as it slides up. */
.re-rank-pop{animation:cs-han-pop .4s cubic-bezier(.2,1.6,.4,1);}
.re-rank-up .re-pname{animation:re-rank-flash 1.4s ease;}
@keyframes re-rank-flash{0%,35%{color:var(--gl);text-shadow:0 0 calc(var(--ui-vmin, 7.2px) * 1.2) rgba(240,208,128,.7);}}
@media (prefers-reduced-motion:reduce){.re-rank-pop,.re-rank-up .re-pname{animation:none;}}
.re-ord-jp{display:none;}
.re-tag{display:inline-block;margin-left:calc(var(--ui-vmin, 7.2px) * .833);padding:0 calc(var(--ui-vmin, 7.2px) * .694);
  border:1px solid currentColor;border-radius:4px;vertical-align:middle;
  font-family:'Cinzel Decorative',serif;font-size:calc(var(--ui-vmin, 7.2px) * 1);letter-spacing:1px;}
.re-tag b{font-family:'Noto Serif JP',serif;font-weight:900;margin-right:.4em;letter-spacing:.05em;}
.re-tag-win{color:var(--gl);}

/* ── Standings card after a tsumo/ron (see setEndStandings in script.js).
   The win cutscene has already shown the hand, yaku and payments, so only
   placement, score and gain/loss remain, styled as the cutscene's closing
   frame rather than a separate panel: the same themed backdrop, a brush
   順位 stamp (reusing the cutscene's cs-stamp/cs-rise keyframes), rows as
   ink strips that slide in one after another with brush placement
   numerals, and a hanko-style Win tag. Rows enter in the previous
   placement order and slide into the new one (reorderEndStandings). ── */
#s-end.re-standings-screen{background:repeating-linear-gradient(45deg,rgba(201,168,76,.025) 0 2px,transparent 2px 6px),var(--bg-theme);background-color:#060c07;overflow-x:hidden;}
/* A larger --ui-vmin than #s-end's own (6px–10px) scales the whole card up
   at once — heading, rows, numerals, tags, buttons — for readability. The
   6px floor matches #s-end's, so phones (width-bound, names already tight)
   stay as they were. */
.rec.re-standings{--ui-vmin:clamp(6px,1.35vmin,13.5px);background:none;border:none;box-shadow:none;
  padding-block:0 calc(var(--ui-vmin, 7.2px) * 2);width:min(calc(var(--ui-vmin, 7.2px) * 78), 96vw);}
.rec.re-standings::before{display:none;}
.rec.re-standings .ret,.rec.re-standings .re-win-content,.rec.re-standings .re-winner-nav,.rec.re-standings .re-tenpai{display:none;}

.re-st-head{position:relative;display:grid;place-items:center;
  height:calc(var(--ui-vmin, 7.2px) * 15);margin-bottom:calc(var(--ui-vmin, 7.2px) * 1);}
.re-st-text{position:relative;display:flex;flex-direction:column;align-items:center;
  gap:calc(var(--ui-vmin, 7.2px) * .4);}
.re-st-kanji{font-family:'Noto Serif JP',serif;font-weight:900;font-size:calc(var(--ui-vmin, 7.2px) * 7);line-height:1;
  letter-spacing:.08em;color:var(--gl);text-shadow:0 .5vmin 0 var(--gd);
  opacity:0;animation:cs-stamp .38s cubic-bezier(.2,1.6,.4,1) .12s forwards;}
.re-st-romaji{font-family:'Cinzel Decorative',serif;font-weight:700;font-size:calc(var(--ui-vmin, 7.2px) * 1.6);
  letter-spacing:.55em;color:var(--gold);opacity:0;animation:cs-rise .25s ease .34s forwards;}
.re-st-round{order:-1;font-family:'Noto Serif JP',serif;font-weight:700;font-size:calc(var(--ui-vmin, 7.2px) * 1.7);letter-spacing:.25em;
  color:rgba(245,237,216,.75);opacity:0;animation:cs-drop-in .25s ease .28s forwards;}

.rec.re-standings .st{margin:0;display:flex;flex-direction:column;gap:calc(var(--ui-vmin, 7.2px) * .8);}
.rec.re-standings .st tbody{display:contents;}
.rec.re-standings .st tr{position:relative;display:grid;grid-template-columns:minmax(0,1fr) auto auto;align-items:center;
  column-gap:calc(var(--ui-vmin, 7.2px) * 2);padding:calc(var(--ui-vmin, 7.2px) * 1) calc(var(--ui-vmin, 7.2px) * 3.5);
  background:linear-gradient(90deg,transparent,rgba(11,13,12,.92) 6%,rgba(11,13,12,.92) 94%,transparent);
  clip-path:polygon(2% 0,100% 0,98% 100%,0 100%);
  opacity:0;animation:re-st-row .45s cubic-bezier(.2,1.3,.4,1) forwards;}
.rec.re-standings .st tr:nth-child(1){animation-delay:.5s;}
.rec.re-standings .st tr:nth-child(2){animation-delay:.62s;}
.rec.re-standings .st tr:nth-child(3){animation-delay:.74s;}
.rec.re-standings .st tr:nth-child(4){animation-delay:.86s;}
@keyframes re-st-row{from{opacity:0;transform:translateX(-6vmin)}to{opacity:1;transform:none}}
.rec.re-standings .st tr.re-row-win{background:linear-gradient(90deg,transparent,rgba(46,34,8,.94) 6%,rgba(20,16,6,.94) 94%,transparent);}
.rec.re-standings .st tr.re-row-win::after,.rec.re-standings .st tr.re-row-dealin::after{content:'';position:absolute;
  left:4%;right:6%;bottom:0;height:2px;background:var(--gold);}
.rec.re-standings .st tr.re-row-dealin::after{background:#c8322a;}
.rec.re-standings .st td{border:none;padding:0;font-size:calc(var(--ui-vmin, 7.2px) * 1.9);}
.rec.re-standings .st td:first-child{display:flex;align-items:center;gap:calc(var(--ui-vmin, 7.2px) * 1.2);min-width:0;}
.rec.re-standings .re-ord-jp{display:inline-block;flex:none;width:calc(var(--ui-vmin, 7.2px) * 4);text-align:center;
  font-family:'Noto Serif JP',serif;font-weight:900;font-size:calc(var(--ui-vmin, 7.2px) * 3.6);line-height:1;color:var(--cream);}
.rec.re-standings .st tr:nth-child(1) .re-ord-jp{color:var(--gl);text-shadow:0 .3vmin 0 var(--gd);}
.rec.re-standings .re-ord{margin-right:0;flex:none;font-family:'Cinzel Decorative',serif;
  font-size:calc(var(--ui-vmin, 7.2px) * 1.1);letter-spacing:.15em;opacity:.75;}
.rec.re-standings .re-pname{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--cream);}
.rec.re-standings .st td:nth-child(2){text-align:right;font-variant-numeric:tabular-nums;color:rgba(245,237,216,.85);font-weight:400;}
.rec.re-standings .st td:last-child{width:auto;min-width:calc(var(--ui-vmin, 7.2px) * 9);}
.rec.re-standings .re-delta{font-family:'Noto Serif JP',serif;font-weight:900;font-size:calc(var(--ui-vmin, 7.2px) * 2.1);font-variant-numeric:tabular-nums;}
.rec.re-standings .re-delta-pos{color:var(--gl);text-shadow:0 .25vmin 0 var(--gd);}
.rec.re-standings .re-delta-neg{color:#f08a80;}
.rec.re-standings .re-tag{flex:none;margin-left:0;padding:.2em .65em;border-radius:3px;font-size:calc(var(--ui-vmin, 7.2px) * 1.2);}
.rec.re-standings .re-tag-win{background:#c8322a;border-color:#e9a49e;color:var(--cream);transform:rotate(-3deg);}

@media (prefers-reduced-motion:reduce){
  .rec.re-standings .st tr,.re-st-kanji,.re-st-romaji,.re-st-round{animation:none;opacity:1;}
}
.re-dora-row{display:flex;gap:8px;align-items:center;justify-content:center;flex-wrap:wrap;margin-bottom:6px;}
.re-dora-lbl{font-size:calc(var(--ui-vmin, 7.2px) * 1.25);color:var(--gold);letter-spacing:1px;}
.re-ura-lbl{color:#f88;margin-left:6px;}
.re-tile-row{display:flex;gap:3px;justify-content:center;flex-wrap:wrap;align-items:flex-end;}
.re-meld-sep{width:8px;flex-shrink:0;}
.re-kan-back{width:var(--tmd-w,28px);height:var(--tmd-h,38px);background:rgba(30,60,40,.8);border:1px solid rgba(201,168,76,.3);border-radius:3px;}
.re-td-delta{text-align:right;width:calc(var(--ui-vmin, 7.2px) * 7.5);}
.re-delta{font-size:calc(var(--ui-vmin, 7.2px) * 1.375);font-weight:700;letter-spacing:.5px;}
.re-badge-pos{color:#7ec87e;}.re-badge-neg{color:#e07070;}
.re-delta-pos{color:#7ec87e;}.re-delta-neg{color:#e07070;}
.re-delta-nil{color:rgba(245,237,216,.25);font-weight:400;}
@keyframes re-badge-float{0%{opacity:0;transform:translateY(0)}15%{opacity:1;transform:translateY(-4px)}70%{opacity:1;transform:translateY(-12px)}100%{opacity:0;transform:translateY(-20px)}}
.re-limit{display:inline-block;margin-left:8px;padding:calc(var(--ui-vmin, 7.2px) * .25) calc(var(--ui-vmin, 7.2px) * 1.25);border-radius:20px;font-size:calc(var(--ui-vmin, 7.2px) * 1.375);font-weight:700;letter-spacing:1.5px;text-transform:uppercase;vertical-align:middle;}
.lbl-mangan  {background:rgba(80,160,80,.18);border:1px solid rgba(100,200,100,.4);color:#90e090;}
.lbl-haneman {background:rgba(80,140,200,.18);border:1px solid rgba(100,180,255,.4);color:#90c8ff;}
.lbl-baiman  {background:rgba(160,80,200,.18);border:1px solid rgba(200,120,255,.4);color:#d090ff;}
.lbl-sanbaiman{background:rgba(200,100,60,.18);border:1px solid rgba(255,160,80,.4);color:#ffb070;}
.lbl-yakuman {background:rgba(201,168,76,.18);border:1px solid rgba(201,168,76,.6);color:var(--gold);box-shadow:0 0 10px rgba(201,168,76,.3);}
.lbl-double-yakuman{background:rgba(201,168,76,.28);border:1px solid rgba(201,168,76,.9);color:var(--gold);box-shadow:0 0 16px rgba(201,168,76,.55),0 0 32px rgba(201,168,76,.2);font-size:calc(var(--ui-vmin, 7.2px) * 1.5);letter-spacing:2px;}

/* ── Per-yakuman particle motif (see spawnYakumanMotif), used by the win
   cutscene's yakuman tier step. One <span> per
   particle, absolutely placed inside .yakuman-motif; JS seeds --sz/--dur/
   --delay/--rot and the per-particle vectors (--dx/--dy/--tilt), CSS runs
   the chosen keyframe via --motion. --tint is the particle's fill. ── */
.yakuman-motif{position:absolute;inset:0;z-index:1;overflow:hidden;}
.ym-particle{position:absolute;width:var(--sz,2vmin);height:var(--sz,2vmin);
  opacity:0;will-change:transform,opacity;
  animation:var(--motion,ym-fall) var(--dur,2.2s) ease-out var(--delay,0s) forwards;}
.ym-s-star{background:var(--tint,#ffd75a);
  clip-path:polygon(50% 0,61% 35%,98% 35%,68% 57%,79% 91%,50% 70%,21% 91%,32% 57%,2% 35%,39% 35%);
  filter:drop-shadow(0 0 6px var(--tint,#ffd75a));}
.ym-s-shard{background:linear-gradient(135deg,#fff,var(--tint,#b9c4ff));
  clip-path:polygon(50% 0,100% 42%,58% 100%,0 58%);
  filter:drop-shadow(0 0 8px var(--tint,#b9c4ff));}
.ym-s-crystal{background:linear-gradient(180deg,#fff,var(--tint,#cfd6f0));
  clip-path:polygon(50% 0,100% 50%,50% 100%,0 50%);
  filter:drop-shadow(0 0 10px var(--tint,#cfd6f0));}
.ym-s-leaf{background:var(--tint,#5ce65c);border-radius:0 100% 0 100%;
  filter:drop-shadow(0 0 5px var(--tint,#5ce65c));}
.ym-s-ember{border-radius:50%;
  background:radial-gradient(circle at 40% 40%,rgba(255,255,255,.5),var(--tint,#ff5a46));
  filter:drop-shadow(0 0 8px var(--tint,#ff5a46));}
.ym-s-orb{border-radius:50%;
  background:radial-gradient(circle at 40% 40%,#fff,var(--tint,#ffc86e) 70%,transparent);
  filter:drop-shadow(0 0 12px var(--tint,#ffc86e));}
.ym-s-coin{border-radius:50%;border:.3vmin solid rgba(255,255,255,.5);
  background:radial-gradient(circle at 35% 35%,#fff8d0,var(--tint,#ffd75a));
  filter:drop-shadow(0 0 8px var(--tint,#ffd75a));}
.ym-s-bolt{background:linear-gradient(180deg,#fff,var(--tint,#fff));
  clip-path:polygon(42% 0,76% 0,55% 40%,82% 40%,30% 100%,46% 54%,20% 54%);
  filter:drop-shadow(0 0 10px var(--tint,#9ff)) drop-shadow(0 0 4px #fff);}
.ym-s-streak{height:calc(var(--sz,14vmin) * .16);width:var(--sz,14vmin);border-radius:40%;
  background:linear-gradient(90deg,transparent,var(--tint,#66e6ff),transparent);
  filter:blur(.25vmin) drop-shadow(0 0 6px var(--tint,#66e6ff));}
@keyframes ym-fall{
  0%{opacity:0;transform:translate3d(0,0,0) rotate(0);}
  12%{opacity:1;}85%{opacity:1;}
  100%{opacity:0;transform:translate3d(var(--dx,0),115vh,0) rotate(var(--rot,360deg));}
}
@keyframes ym-rise{
  0%{opacity:0;transform:translate3d(0,0,0) rotate(0);}
  12%{opacity:1;}85%{opacity:1;}
  100%{opacity:0;transform:translate3d(var(--dx,0),-120vh,0) rotate(var(--rot,-360deg));}
}
@keyframes ym-radiate{
  0%{opacity:0;transform:translate3d(0,0,0) scale(.2) rotate(0);}
  16%{opacity:1;}78%{opacity:.9;}
  100%{opacity:0;transform:translate3d(var(--dx,0),var(--dy,0),0) scale(1) rotate(var(--rot,180deg));}
}
@keyframes ym-gale{
  0%{opacity:0;transform:translate3d(0,0,0) rotate(var(--tilt,-8deg));}
  14%{opacity:1;}85%{opacity:.85;}
  100%{opacity:0;transform:translate3d(128vw,var(--dy,0),0) rotate(var(--tilt,-8deg));}
}
@media (prefers-reduced-motion:reduce){
  .yakuman-motif{display:none;}
}
.yl{display:flex;flex-wrap:wrap;gap:5px;justify-content:center;margin-bottom:12px;}
.yc{background:rgba(201,168,76,.1);border:1px solid rgba(201,168,76,.3);border-radius:20px;padding:calc(var(--ui-vmin, 7.2px) * .375) calc(var(--ui-vmin, 7.2px) * 1.25);font-size:calc(var(--ui-vmin, 7.2px) * 1.25);color:var(--cream);}
.yh{color:var(--gl);font-weight:700;margin-left:4px;}.yj{color:rgba(245,237,216,.4);font-size:calc(var(--ui-vmin, 7.2px) * 1);margin-left:3px;}
.st{width:100%;border-collapse:collapse;margin:10px 0;}.st td{padding:calc(var(--ui-vmin, 7.2px) * .75) calc(var(--ui-vmin, 7.2px) * 1.5);font-size:calc(var(--ui-vmin, 7.2px) * 1.5);border-bottom:1px solid rgba(201,168,76,.1);text-align:left;}
.st td:last-child{text-align:right;font-weight:700;color:var(--gl);}.st tr.w td{color:var(--gl);font-size:calc(var(--ui-vmin, 7.2px) * 1.625);}
.st tr.l td{color:#e07070;font-size:calc(var(--ui-vmin, 7.2px) * 1.625);}
/* Game-over/bankruptcy final standings (see onGameOver in script.js) — the
   standings card (.re-standings) plus .re-final, laid out like the
   game-start roll call's strips (.gs-row): a brush placement numeral sized
   like the seat wind there, the player's unboxed tag (.pl-tag), and the
   score. Every strip is the same plain ink; first place's numeral and score
   are gold (as the dealer's 東 is on the roll call), and my own row has the
   gold underline (as .gs-me). The brush numeral alone carries the place, so
   the small "1st" label is hidden. */
.rec.re-final .st tr{grid-template-columns:minmax(0,1fr) auto;padding:calc(var(--ui-vmin, 7.2px) * 1) calc(var(--ui-vmin, 7.2px) * 3.5);}
.rec.re-final .st tr.re-row-me::after{content:'';position:absolute;left:4%;right:6%;bottom:0;height:2px;background:var(--gold);}
.rec.re-final .re-ord{display:none;}
.rec.re-final .st td:first-child{gap:calc(var(--ui-vmin, 7.2px) * 2);}
.rec.re-final .re-ord-jp{width:calc(var(--ui-vmin, 7.2px) * 4.4);font-size:calc(var(--ui-vmin, 7.2px) * 3.8);}
.rec.re-final .st .pl-tag{text-align:left;}
.rec.re-final .st td:nth-child(2){font-family:'Noto Serif JP',serif;font-weight:700;font-size:calc(var(--ui-vmin, 7.2px) * 2.2);}
.rec.re-final .st tr:nth-child(1) td:nth-child(2){color:var(--gl);text-shadow:0 .25vmin 0 var(--gd);}
/* Space between the last strip and the buttons (View Results on the final
   round's standings, Play Again / Leave Room on game over). */
.rec.re-standings .reb{margin-top:calc(var(--ui-vmin, 7.2px) * 3.2);}
/* Phones: the strips give their padding to the name so it isn't cut short. */
@media (max-width:520px){
  .rec.re-final .st tr{padding-inline:calc(var(--ui-vmin, 7.2px) * 1.5);column-gap:calc(var(--ui-vmin, 7.2px) * 1.2);}
  .rec.re-final .st td:first-child{gap:calc(var(--ui-vmin, 7.2px) * 1.2);}
}
.wt{display:flex;flex-direction:column;align-items:center;gap:4px;margin:10px 0;}
.re-tenpai{display:flex;flex-direction:column;gap:8px;margin:10px 0;}
.re-tenpai-row{display:flex;flex-direction:column;align-items:center;gap:4px;}
.re-tenpai-name{font-size:calc(var(--ui-vmin, 7.2px) * 1.375);color:var(--gold);letter-spacing:1px;}
/* Riichi-stick/honba pot carrying into next round — hidden (no text, no
   margin/border) when the round ended the game or nothing's on the table,
   set in onEnd() (script.js). */
/* Strip section of the standings card (.re-standings.re-strips, see
   END_CARD_HEADS / renderEndTiles): one ink strip per line above the rows —
   an exhaustive draw's tenpai hands (or 全員不聴), the chonbo offender, or
   the nagashi mangan seats — each a brush label (<b>) then the name, with an
   optional note or the hand's tiles under it. They slide in before the
   standings rows, which wait for them (their delays shift by .4s here). */
.rec.re-standings.re-strips .re-tenpai{display:flex;gap:calc(var(--ui-vmin, 7.2px) * .8);margin:0 0 calc(var(--ui-vmin, 7.2px) * 1.8);}
.re-strips .re-tenpai-row{position:relative;gap:calc(var(--ui-vmin, 7.2px) * .8);padding:calc(var(--ui-vmin, 7.2px) * 1.1) calc(var(--ui-vmin, 7.2px) * 3.5);
  background:linear-gradient(90deg,transparent,rgba(11,13,12,.92) 6%,rgba(11,13,12,.92) 94%,transparent);
  clip-path:polygon(2% 0,100% 0,98% 100%,0 100%);
  opacity:0;animation:re-st-row .45s cubic-bezier(.2,1.3,.4,1) calc(.35s + var(--i, 0) * .14s) forwards;}
.re-strips .re-tenpai-row .re-tile-row{align-self:stretch;}
.re-strips .re-tenpai-name{display:flex;align-items:baseline;gap:calc(var(--ui-vmin, 7.2px) * 1);
  font-family:'Cinzel Decorative',serif;font-weight:700;font-size:calc(var(--ui-vmin, 7.2px) * 1.7);letter-spacing:.08em;color:var(--cream);}
.re-strips .re-tenpai-name b{font-family:'Noto Serif JP',serif;font-weight:700;font-size:1.4em;letter-spacing:.02em;color:var(--gl);}
.re-strip-note{font-size:calc(var(--ui-vmin, 7.2px) * 1.35);letter-spacing:.08em;color:rgba(245,237,216,.62);text-align:center;}
/* Chonbo: the offender's strip is ruled and labelled in vermilion. */
.re-strips .re-strip-bad b{color:#f08a80;}
.re-strips .re-strip-bad::after{content:'';position:absolute;left:4%;right:6%;bottom:0;height:2px;background:#c8322a;}
.rec.re-strips .st tr:nth-child(1){animation-delay:.9s;}
.rec.re-strips .st tr:nth-child(2){animation-delay:1.02s;}
.rec.re-strips .st tr:nth-child(3){animation-delay:1.14s;}
.rec.re-strips .st tr:nth-child(4){animation-delay:1.26s;}
.rec.re-standings .re-tag-tenpai{background:rgba(201,168,76,.12);border-color:rgba(240,208,128,.6);color:var(--gl);transform:rotate(-3deg);}
.rec.re-standings .re-tag-chonbo{background:#3a0d0a;border-color:#c8322a;color:#f6c7c1;transform:rotate(-3deg);}
/* Phones: tags show only their kanji (和了 / 聴牌 / 流満 / 罰) so the name keeps its room. */
@media (max-width:520px){
  .rec.re-standings .re-tag{font-size:0;padding:.25em .5em;}
  .rec.re-standings .re-tag b{margin-right:0;font-size:calc(var(--ui-vmin, 7.2px) * 1.4);}
}
@media (prefers-reduced-motion:reduce){.re-strips .re-tenpai-row{animation:none;opacity:1;}}
.re-carryover{font-size:calc(var(--ui-vmin, 7.2px) * 1.375);color:var(--gold);letter-spacing:.5px;
  padding-top:8px;margin-top:4px;border-top:1px solid rgba(201,168,76,.15);}
.re-carryover:empty{display:none;padding:0;margin:0;border:none;}
/* ── Settings panel ── */
/* --ui-vmin: same clamped-scale-unit approach as #s-start/#s-lobby/#s-end
   (see their own comments) — this panel is reachable from every screen
   (see toggleSettings in script.js) so it's never a descendant of any of
   those, and everything below already reads var(--ui-vmin, 7.2px) but had
   nothing actually defining it, so it was permanently stuck at the 7.2px
   fallback (i.e. never actually scaling) regardless of screen size —
   which read as "too small" specifically on very large/4K displays. */
#settings-panel{--ui-vmin:clamp(6px,1vmin,10px);--pg:calc(var(--ui-vmin) * 6);position:fixed;top:0;right:0;bottom:0;
  width:calc(var(--ui-vmin) * 33.33);max-width:calc(100vw - 24px);
  /* Diagonal-stagger, drawer edition — the panel is flush against three
     screen edges, so only the left edge is treated: a staggered double
     gold rule (bold outer at x=0 cut top+bottom, finer inner a few px in
     cut through the middle). Drawn as background layers, not ::before, so
     it stays pinned while the settings sections scroll. Matches the
     .about-modal / #bstart frame language. */
  background:
    linear-gradient(180deg,transparent 0 var(--pg),#e8c25c var(--pg) calc(100% - var(--pg)),transparent) 0 0/2px 100% no-repeat,
    linear-gradient(180deg,#c9a84c 0 38%,transparent 38% 62%,#c9a84c 62%) calc(var(--ui-vmin) * .7) 0/1px 100% no-repeat,
    rgba(6,12,7,.97);
  border-left:0;
  z-index:200;transform:translateX(100%);transition:transform .25s ease;
  display:flex;flex-direction:column;padding:calc(var(--ui-vmin) * 2.778) calc(var(--ui-vmin) * 2.222);
  gap:calc(var(--ui-vmin) * 2.778);overflow-y:auto;}
#settings-backdrop{display:none;position:fixed;inset:0;z-index:199;}
.sp-title{font-family:'Cinzel Decorative',serif;font-size:calc(var(--ui-vmin, 7.2px) * 1.528);letter-spacing:3px;
  color:var(--gold);border-bottom:1px solid rgba(201,168,76,.2);padding-bottom:calc(var(--ui-vmin, 7.2px) * 1.389);}
.sp-section{display:flex;flex-direction:column;gap:12px;}
.sp-header{font-size:calc(var(--ui-vmin, 7.2px) * 1.389);letter-spacing:2px;color:rgba(245,237,216,.45);}
.sp-row{display:flex;align-items:center;justify-content:space-between;}
/* .sp-row's rows normally get their vertical spacing from the settings
   panel's .sp-section{gap:12px} wrapper — the Rules modal's .about-body
   uses bare .sp-row divs with no such wrapper, so give them their own
   breathing room here instead. */
.about-body .sp-row{padding:calc(var(--ui-vmin, 7.2px) * .556) 0;}
/* Highlights the current player's own row in the Rankings list. */
.sp-row-mine .sp-label{color:var(--gold);font-weight:700;}
/* Rankings name → player card modal (see openPlayerCard in script.js). No
   underline — cursor+hover colour alone is the clickability affordance. */
.sp-label-clickable{cursor:pointer;transition:color .15s;}
.sp-label-clickable:hover{color:var(--gold);}
.player-card-loading{text-align:center;color:rgba(245,237,216,.5);padding:calc(var(--ui-vmin, 7.2px) * 1.111) 0;}
/* Reuses .up-nameplate's card look wholesale (same border/background/tier
   shine as the User Profile hub's own nameplate) inside #player-card-modal
   — just cancels .about-title's fixed modal-header spacing/centring, which
   would otherwise fight the nameplate's own flex layout and margins. */
.player-card-nameplate{margin:0 0 calc(var(--ui-vmin, 7.2px) * 1.111) 0;text-align:left;}

/* In-game nameplate click popover (see openPlayerCardInGame/
   renderPlayerPopover in script.js) — a small anchored card, NOT a modal:
   no backdrop, no dimming, positioned via inline left/top next to whichever
   nameplate was clicked so the rest of the board stays visible/interactive
   mid-hand. Own --ui-vmin (same clamp() pattern as #settings-panel/
   .about-modal above) so .up-nameplate/.player-card-nameplate/.sp-row
   inside it — all of which read var(--ui-vmin, 7.2px) — scale down to fit a
   card this small rather than inheriting some ancestor's larger value. */
/* Glassy but restrained: a translucent tinted background + blur (the actual
   "glass" read) plus one soft inset highlight along the top edge only (a
   frosted pane catching light from above, not an all-over shine). No
   diagonal specular sweep like .up-nameplate::before uses elsewhere in this
   file — that's the "too much gloss" look this was asked to avoid, fine for
   a static profile-hub header but too busy for a small popover that pops
   in/out constantly mid-game. backdrop-filter falls back to the flat
   background alone on browsers without support (still fully readable). */
.player-popover{--ui-vmin:clamp(6px,1vmin,10px);position:fixed;
  width:calc(var(--ui-vmin) * 30.5);max-width:calc(100vw - 16px);
  background:linear-gradient(180deg,rgba(20,32,22,.72),rgba(10,22,12,.72));
  -webkit-backdrop-filter:blur(14px) saturate(140%);backdrop-filter:blur(14px) saturate(140%);
  border:1px solid rgba(201,168,76,.32);border-radius:2px;
  padding:calc(var(--ui-vmin) * 2) calc(var(--ui-vmin) * 1.8);
  box-shadow:0 8px 28px rgba(0,0,0,.55),inset 0 1px 0 rgba(255,255,255,.08);
  z-index:180;display:flex;flex-direction:column;gap:calc(var(--ui-vmin) * 1.4);}
.player-popover-stats{display:flex;flex-direction:column;gap:calc(var(--ui-vmin) * 0.8);
  font-size:calc(var(--ui-vmin) * 1.25);}
.player-popover-stats .sp-label{color:rgba(245,237,216,.6);}
/* Rankings' sort-stat picker (Points/Wins/Avg Place/etc. — see
   setLeaderboardSort in script.js) — a real tab strip (shared bottom
   border, active tab picked out by an underline) rather than a row of
   pill buttons, icon-only (title attr carries the label as a hover
   tooltip, same as the corner nav icons like .card-icon-btn) so all 7 fit
   in one row without wrapping even at the modal's narrowest width. */
.rank-sort-tabs{display:flex;border-bottom:1px solid rgba(201,168,76,.2);margin-bottom:calc(var(--ui-vmin, 7.2px) * .556);}
.rank-sort-tab{flex:1;display:flex;align-items:center;justify-content:center;
  padding:calc(var(--ui-vmin, 7.2px) * 1.111) 0;margin-bottom:-1px;
  background:none;border:none;border-bottom:2px solid transparent;
  color:rgba(245,237,216,.45);cursor:pointer;transition:all .2s;}
.rank-sort-tab svg{width:calc(var(--ui-vmin, 7.2px) * 2.222);height:calc(var(--ui-vmin, 7.2px) * 2.222);display:block;}
.rank-sort-tab:hover{color:var(--gl);}
.rank-sort-tab.active{color:var(--gold);border-bottom-color:var(--gold);}
/* Rankings' period picker (All Time/This Week/This Month — see
   setLeaderboardPeriod in script.js) — same real-tab-strip language as
   .rank-sort-tabs right above (shared bottom border, active tab picked out
   by an underline), just with the label always visible as plain text
   instead of an icon+tooltip, since only 3 tabs need to fit rather than 7. */
.rank-period-tabs{display:flex;border-bottom:1px solid rgba(201,168,76,.2);margin-bottom:calc(var(--ui-vmin, 7.2px) * 1.111);}
.rank-period-tab{flex:1;padding:calc(var(--ui-vmin, 7.2px) * 1.111) 0;margin-bottom:-1px;
  background:none;border:none;border-bottom:2px solid transparent;
  font-family:'Cinzel Decorative',serif;font-size:calc(var(--ui-vmin, 7.2px) * 1.389);
  color:rgba(245,237,216,.45);cursor:pointer;transition:all .2s;}
.rank-period-tab:hover{color:var(--gl);}
.rank-period-tab.active{color:var(--gold);border-bottom-color:var(--gold);}
/* Persistent label for whichever tab above is active (see
   updateRankSortHeader in script.js) — icon-only tabs otherwise have no
   always-visible indicator of what's actually selected, especially on
   touch devices where the tabs' own hover title never fires at all. */
.rank-sort-header{text-align:center;font-family:'Cinzel Decorative',serif;
  font-size:calc(var(--ui-vmin, 7.2px) * 1.528);letter-spacing:calc(var(--ui-vmin, 7.2px) * .208);
  color:var(--gold);margin-bottom:calc(var(--ui-vmin, 7.2px) * 1.389);}
.sp-label{font-size:calc(var(--ui-vmin, 7.2px) * 1.667);color:var(--cream);}
/* Placements mini bar-chart (see renderPlacementBar in script.js) —
   replaces the old "1st 1 · 2nd 0 · 3rd 0 · 4th 0" text line, which was
   hard to compare at a glance. One thin bar per rank, width = that rank's
   % of games played; brightness fades rank-over-rank via .place-bar-1..4
   (1st brightest gold, last dimmest) so "better placement" reads as
   "brighter bar" without a legend. */
.place-bars{margin:calc(var(--ui-vmin, 7.2px) * .556) 0;}
.place-bar-row{display:flex;align-items:center;gap:calc(var(--ui-vmin, 7.2px) * 1.111);
  padding:calc(var(--ui-vmin, 7.2px) * .278) 0;}
.place-bar-label{flex:0 0 calc(var(--ui-vmin, 7.2px) * 3.5);font-size:calc(var(--ui-vmin, 7.2px) * 1.389);color:rgba(245,237,216,.6);}
.place-bar-track{flex:1;height:calc(var(--ui-vmin, 7.2px) * 1.111);border-radius:999px;
  background:rgba(0,0,0,.35);overflow:hidden;}
.place-bar-fill{height:100%;border-radius:999px;transition:width .4s ease;}
.place-bar-1{background:rgba(201,168,76,1);}
.place-bar-2{background:rgba(201,168,76,.75);}
.place-bar-3{background:rgba(201,168,76,.5);}
.place-bar-4{background:rgba(201,168,76,.3);}
.place-bar-pct{flex:0 0 calc(var(--ui-vmin, 7.2px) * 8);text-align:right;
  font-size:calc(var(--ui-vmin, 7.2px) * 1.389);color:rgba(245,237,216,.7);}
/* Achievements panel (see renderAchievements in script.js) — one item per
   achievement, reusing .place-bar-track/.place-bar-fill above for its
   progress bar. Not-yet-earned ones are just dimmed via .achv-locked
   (opacity, no lock icon) — that treatment is deliberately reserved for
   the Title/Voice Theme pickers, which hide/disable instead. */
.achv-item{padding:calc(var(--ui-vmin, 7.2px) * 1.111) 0;
  border-bottom:1px solid rgba(201,168,76,.12);}
.achv-item:last-child{border-bottom:none;}
.achv-item.achv-locked{opacity:.45;}
.achv-label{font-size:calc(var(--ui-vmin, 7.2px) * 1.667);color:var(--cream);}
.achv-reward{font-size:calc(var(--ui-vmin, 7.2px) * 1.25);color:var(--gold);opacity:.8;
  margin:calc(var(--ui-vmin, 7.2px) * .278) 0 calc(var(--ui-vmin, 7.2px) * .417);}
/* Achievement-unlock toast (see index.html/showAchievementUnlock in
   script.js) — a corner banner, not a blocking modal: no backdrop, and
   pointer-events is off while hidden/off-screen so it never intercepts
   clicks to whatever's underneath (the round-end screen's View
   Results/Main Menu/Ready buttons, specifically — a full about-modal here
   used to sit right on top of them). Slides in from off-screen via
   transform rather than display toggling, so it can auto-dismiss with a
   visible animation instead of just vanishing. */
/* --ui-vmin: same clamped-scale-unit approach as #s-end/#settings-panel
   (see their own comments) — this toast is a standalone body-level element
   (see index.html), never a descendant of #s-lobby/#s-start/etc., so
   everything below was permanently stuck at the calc()'s 7.2px fallback
   regardless of screen size until this was added. */
.achv-toast{--ui-vmin:clamp(6px,1vmin,10px);position:fixed;top:calc(var(--ui-vmin) * 2);right:calc(var(--ui-vmin) * 2);
  z-index:350;width:min(320px,calc(100vw - 24px));
  background:#0a160c;border:1px solid rgba(201,168,76,.4);border-radius:8px;
  padding:calc(var(--ui-vmin, 7.2px) * 2.222);box-shadow:0 12px 32px rgba(0,0,0,.6);
  transform:translateX(calc(100% + 24px));transition:transform .4s ease;
  pointer-events:none;max-height:calc(100vh - 32px);overflow-y:auto;}
.achv-toast.show{transform:translateX(0);pointer-events:auto;}
/* Stack the corner toasts (achievement / room invite / daily bonus) when
   more than one is on screen at once, so a later one drops below the
   earlier instead of landing on top of it. Sibling order in index.html is
   achv-toast, invite-toast, coin-toast; `~` skips any that aren't `.show`,
   so a 2nd visible toast is nudged down one step and a 3rd two steps,
   regardless of which of the three they are. Approximate step height —
   these are all short (title + one or two lines). */
.achv-toast.show ~ .achv-toast.show{transform:translateX(0) translateY(calc(var(--ui-vmin) * 15));}
.achv-toast.show ~ .achv-toast.show ~ .achv-toast.show{transform:translateX(0) translateY(calc(var(--ui-vmin) * 30));}
/* Room Invite toast's Accept button (#invite-toast-accept) — the
   achievement toast has no equivalent, it's purely informational, so this
   only applies to the invite variant. */
#invite-toast-accept{margin-top:calc(var(--ui-vmin, 7.2px) * 1.389);}
.achv-toast-title{font-family:'Cinzel Decorative',serif;font-size:calc(var(--ui-vmin, 7.2px) * 1.806);
  color:var(--gold);margin-bottom:calc(var(--ui-vmin, 7.2px) * 1.389);padding-right:calc(var(--ui-vmin, 7.2px) * 2.778);}
/* Wordless on/off switch (replaces the old "ON"/"OFF" text pill) — a knob
   (::after) slides right when on, left when the .off class is added (see
   toggleMusic/toggleFx/toggleDiscardSfx/applyRuleButton in script.js,
   which now just toggle that class instead of writing textContent + two
   inline styles). Track stays the same green-tinted "on"/red-tinted "off"
   coding the old text buttons used, just via .off instead of inline style,
   so this needs no localization and reads at a glance without reading
   words. Buttons are now empty elements (see index.html) — the knob is the
   entire visual, nothing else to lay out. */
/* Recolored to reuse the same "selected/unselected" language as
   .mode-btn.active/.mode-btn (jade tint + jade border when on, dim
   near-black + faint gold border when off) instead of a stoplight red/
   green pair — matches the rest of the Janshin theme and reads as a
   quieter on/off cue than the old saturated colors. */
.sp-toggle{position:relative;width:calc(var(--ui-vmin, 7.2px) * 4.2);height:calc(var(--ui-vmin, 7.2px) * 2.15);
  padding:0;border-radius:999px;cursor:pointer;flex-shrink:0;
  background:rgba(26,107,69,.4);border:1px solid var(--jl);
  transition:background .2s,border-color .2s;}
.sp-toggle::after{content:'';position:absolute;top:calc(var(--ui-vmin, 7.2px) * .225);
  left:calc(var(--ui-vmin, 7.2px) * 2.3);width:calc(var(--ui-vmin, 7.2px) * 1.7);height:calc(var(--ui-vmin, 7.2px) * 1.7);
  border-radius:50%;background:rgba(245,237,216,.85);box-shadow:0 1px 3px rgba(0,0,0,.35);
  transition:left .2s,background .2s;}
.sp-toggle.off{background:rgba(0,0,0,.3);border-color:rgba(201,168,76,.25);}
.sp-toggle.off::after{left:calc(var(--ui-vmin, 7.2px) * .225);background:rgba(245,237,216,.55);}
.sp-vol{display:flex;flex-direction:column;gap:calc(var(--ui-vmin, 7.2px) * .833);}
.sp-vol-row{display:flex;justify-content:space-between;align-items:center;}
.sp-vol-label{font-size:calc(var(--ui-vmin, 7.2px) * 1.528);color:var(--gold);}
.sp-slider{width:100%;accent-color:var(--gold);cursor:pointer;}
.sp-theme-list{display:flex;flex-direction:column;gap:calc(var(--ui-vmin, 7.2px) * .833);}
/* Voice theme option buttons (see renderVoiceThemes in script.js) — base
   look lives here now instead of an inline style.cssText, so sizing scales
   with --ui-vmin like everything else in the modal; JS only ever toggles
   .locked (via a class, not inline px) plus the per-theme active/hover
   background swap already handled separately in applyVoiceTheme. */
.theme-btn{padding:calc(var(--ui-vmin, 7.2px) * .972) calc(var(--ui-vmin, 7.2px) * 1.667);border-radius:2px;
  font-size:calc(var(--ui-vmin, 7.2px) * 1.528);letter-spacing:calc(var(--ui-vmin, 7.2px) * .139);
  transition:all .2s;text-align:left;width:100%;
  background:rgba(42,58,42,.3);border:1px solid rgba(201,168,76,.2);
  color:var(--cream);font-family:'Cinzel Decorative',serif;cursor:pointer;}
.theme-btn.locked{opacity:.45;cursor:default;}
/* Game Music picker (see renderGameMusicList) reuses .theme-btn but is
   multi-select rather than pick-one, so it needs its own persistent
   "selected" look instead of applyVoiceTheme's single-active inline style. */
.theme-btn.selected{background:rgba(201,168,76,.25);border-color:var(--gold);}
.theme-btn.selected::before{content:'✓ ';color:var(--gold);}
/* Player Icon grid — User Profile modal only (see renderPlayerIcons in
   script.js). Each icon is a small hand-coded SVG mark, not an uploaded
   image — the app has no image-upload capability. */
.icon-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:calc(var(--ui-vmin, 7.2px) * 1.111);}
/* Choose an Existing Profile — its own window now (#choose-profile-modal
   in index.html), so this just fills that window's .about-body, which
   already scrolls on its own (see .about-body's comment) — no height cap
   needed here the way the old inline version needed one to avoid pushing
   the Log In card's own fields around underneath it. Still a compact
   variant of .profile-card though: icon and name laid out horizontally
   (like a plain list row) instead of icon-on-top/name-below, 2 columns
   (not .icon-grid's usual 3) so a horizontal card has enough width for
   the name. */
#lobby-profile-grid{grid-template-columns:repeat(2,1fr);}
#lobby-profile-grid .profile-card{flex-direction:row;justify-content:flex-start;
  padding:calc(var(--ui-vmin, 7.2px) * .833) calc(var(--ui-vmin, 7.2px) * 1.111);
  gap:calc(var(--ui-vmin, 7.2px) * .833);}
#lobby-profile-grid .profile-card svg{width:calc(var(--ui-vmin, 7.2px) * 2.778);height:calc(var(--ui-vmin, 7.2px) * 2.778);flex-shrink:0;}
#lobby-profile-grid .profile-card-name{font-size:calc(var(--ui-vmin, 7.2px) * 1.389);}
.icon-btn{background:rgba(42,58,42,.3);border:1px solid rgba(201,168,76,.2);border-radius:2px;
  padding:calc(var(--ui-vmin, 7.2px) * .833);cursor:pointer;transition:all .2s;display:flex;align-items:center;justify-content:center;}
.icon-btn svg{width:calc(var(--ui-vmin, 7.2px) * 5);height:calc(var(--ui-vmin, 7.2px) * 5);display:block;}
.icon-btn:hover{border-color:rgba(201,168,76,.5);}
.icon-btn.active{background:rgba(201,168,76,.2);border-color:var(--gold);}

/* ════════════════════════════════════════════════════════════
   START SCREEN — "Ink" (D4) on deep green, with the split-kanji glitch art
   Picked from the Spotlight start-menu mockups (D4 Ink Stroke · G1 Deep
   Green · R1 split kanji with glitch). Left: the 雀心 logo and a vertical
   menu, 開始 Start / 情報 About / 設定 Settings, Japanese first. The item
   under the pointer or keyboard focus gets a ragged ink-brush swash behind
   it; with neither, 開始 (.st-default) keeps it. Right: decorative art, see
   .st-art. Everything scales off --ui-vmin (6–10px; 8px at 1280×800, the
   size the mockup was drawn at, so "N units" = mockup px / 8).
   ════════════════════════════════════════════════════════════ */
#s-start{--ui-vmin:clamp(6px,1vmin,10px);overflow:hidden;
  background:radial-gradient(ellipse 55% 65% at 28% 0%,rgba(255,240,210,.08),transparent 70%),
    radial-gradient(ellipse 80% 90% at 40% 45%,#0e2b1d,#06160e 60%,#020805 100%);}

.st-main{position:absolute;z-index:2;left:calc(var(--ui-vmin) * 16.25);top:50%;translate:0 -54%;}
.st-logo{margin:0;font-family:'Noto Serif JP',serif;font-weight:900;font-size:calc(var(--ui-vmin) * 14);
  letter-spacing:calc(var(--ui-vmin) * 1.25);line-height:1.05;
  background:linear-gradient(135deg,#ffe680,#c9a84c,#8a6b20);-webkit-background-clip:text;background-clip:text;color:transparent;
  filter:drop-shadow(0 0 calc(var(--ui-vmin) * 2.5) rgba(201,168,76,.35));}
/* Start-press flash (enterLobby() adds .burst), same keyframes as the lobby logo. */
.st-logo.burst{animation:logoBurst .45s ease;}
.st-sub{display:flex;align-items:center;gap:calc(var(--ui-vmin) * 1.75);margin-top:calc(var(--ui-vmin) * .75);}
.st-sub span{font-family:'Cinzel Decorative',serif;font-size:calc(var(--ui-vmin) * 1.75);letter-spacing:.7em;color:rgba(201,168,76,.8);}
/* a short dry-brush stroke trailing off the name */
.st-sub i{width:calc(var(--ui-vmin) * 15);height:calc(var(--ui-vmin) * .375);
  background:linear-gradient(90deg,rgba(201,168,76,.7),rgba(201,168,76,0));clip-path:polygon(0 30%,100% 0,96% 100%,0 80%);}

.st-menu{display:flex;flex-direction:column;gap:calc(var(--ui-vmin) * .25);
  margin:calc(var(--ui-vmin) * 7.25) 0 0 calc(var(--ui-vmin) * -5);}
.st-mi{position:relative;isolation:isolate;display:flex;align-items:center;gap:calc(var(--ui-vmin) * 2);
  width:calc(var(--ui-vmin) * 52.5);height:calc(var(--ui-vmin) * 9);padding:0 0 0 calc(var(--ui-vmin) * 5);
  background:none;border:0;cursor:pointer;text-align:left;color:rgba(232,240,230,.5);transition:color .15s,transform .15s;}
.st-mi b{font-family:'Noto Serif JP',serif;font-weight:900;font-size:calc(var(--ui-vmin) * 3.5);letter-spacing:.18em;}
.st-mi small{font-family:'Cinzel Decorative',serif;font-weight:700;font-size:calc(var(--ui-vmin) * 1.375);letter-spacing:.3em;
  color:rgba(201,168,76,.45);transition:color .15s;}
.st-mi:focus-visible{outline:none;}
/* The ink swash: a near-black green brush mark, ragged at the ends. */
.st-mi::before{content:'';position:absolute;z-index:-1;
  left:calc(var(--ui-vmin) * 1.25);right:calc(var(--ui-vmin) * 2.5);top:calc(var(--ui-vmin) * 1.25);bottom:calc(var(--ui-vmin) * 1);
  background:linear-gradient(90deg,rgba(4,14,9,0),#04110a 10%,#030c07 62%,rgba(3,12,7,0));
  clip-path:polygon(2% 30%,6% 8%,30% 0,62% 6%,90% 2%,100% 22%,97% 60%,100% 88%,70% 100%,40% 94%,12% 100%,0 76%,4% 55%);
  opacity:0;transition:opacity .15s;}
.st-mi:hover,.st-mi:focus-visible,.st-menu:not(:hover):not(:has(:focus-visible)) .st-default{color:#fff6dc;}
.st-mi:hover small,.st-mi:focus-visible small,.st-menu:not(:hover):not(:has(:focus-visible)) .st-default small{color:#f0d080;}
.st-mi:hover::before,.st-mi:focus-visible::before,.st-menu:not(:hover):not(:has(:focus-visible)) .st-default::before{opacity:1;}
#bstart.pressed{transform:scale(.96);}

.st-foot{position:absolute;z-index:2;left:calc(var(--ui-vmin) * 16.25);bottom:calc(var(--ui-vmin) * 5);
  display:flex;gap:calc(var(--ui-vmin) * 2.25);font-size:calc(var(--ui-vmin) * 1.45);letter-spacing:.04em;color:rgba(232,240,230,.35);}
/* "v0.18.0 · What's new" — opens the release notes (toggleWhatsNew). */
.st-whatsnew{display:inline-flex;align-items:baseline;gap:calc(var(--ui-vmin) * .8);color:inherit;text-decoration:none;}
.st-whatsnew i{font-style:normal;}
.st-whatsnew b{font-weight:400;color:rgba(240,208,128,.85);transition:color .15s;}
.st-whatsnew:hover b,.st-whatsnew:focus-visible b{color:var(--gl);}
.st-whatsnew:focus-visible{outline:none;}

/* ── Right-hand art ──
   雀's left half (set high) and 心's right half (set low) meet at a
   hairline cut (--st-cut across the screen). Each half is an SVG 560×1220
   (viewBox units) sized off the screen height (--st-h), so it bleeds past
   the top / bottom / right edges at any size. Faint silver outline, faded
   toward the menu and the bottom; a left-side shadow keeps the menu clean.
   The glitch: jade + gold copies offset a few units, three bands per half
   masked out of the main drawing and redrawn shifted (.st-gj, which jump
   now and then), flickering bars (.st-gb) and scanlines. */
.st-art{position:absolute;inset:0;z-index:1;pointer-events:none;--st-cut:72.66%;--st-h:152.5vh;}
.st-half{position:absolute;display:block;height:var(--st-h);width:calc(var(--st-h) * .459);opacity:.34;overflow:hidden;}
.st-half-l{left:calc(var(--st-cut) - var(--st-h) * .459);top:calc(var(--st-h) * -.2213);
  -webkit-mask-image:linear-gradient(90deg,transparent 0,#000 45%),linear-gradient(180deg,transparent 0,#000 30%);-webkit-mask-composite:source-in;
  mask-image:linear-gradient(90deg,transparent 0,#000 45%),linear-gradient(180deg,transparent 0,#000 30%);mask-composite:intersect;}
.st-half-r{left:var(--st-cut);top:calc(var(--st-h) * -.0164);
  -webkit-mask-image:linear-gradient(180deg,#000 50%,transparent 85%);mask-image:linear-gradient(180deg,#000 50%,transparent 85%);}
.st-cut{position:absolute;left:var(--st-cut);top:0;bottom:0;width:1px;
  background:linear-gradient(180deg,rgba(220,226,232,0),rgba(220,226,232,.2) 30%,rgba(220,226,232,.2) 70%,rgba(220,226,232,0));}
.st-scan{position:absolute;left:40%;top:0;right:0;bottom:0;
  background:repeating-linear-gradient(180deg,rgba(255,255,255,.028) 0 1px,transparent 1px 4px);
  -webkit-mask-image:linear-gradient(90deg,transparent,#000 40%);mask-image:linear-gradient(90deg,transparent,#000 40%);}
.st-art::after{content:'';position:absolute;left:0;top:0;bottom:0;width:56%;background:linear-gradient(90deg,rgba(4,14,9,.55),rgba(4,14,9,0));}
.st-gj{animation:stJit 3.6s steps(1) infinite;}
.st-gj2{animation-duration:4.4s;animation-delay:-1.3s;}
.st-gj3{animation-duration:5.2s;animation-delay:-2.6s;}
.st-gb{animation:stFlick 2.8s steps(1) infinite;}
.st-gb2{animation-duration:3.7s;animation-delay:-1.1s;}
@keyframes stJit{0%{transform:translateX(0)}6%{transform:translateX(-26px)}8%{transform:translateX(18px)}10%{transform:translateX(-6px)}12%,100%{transform:translateX(0)}}
@keyframes stFlick{0%{opacity:.5}20%{opacity:0}23%{opacity:.6}60%{opacity:.15}62%{opacity:.55}100%{opacity:.5}}
@media (prefers-reduced-motion:reduce){.st-gj,.st-gb{animation:none;}}
/* Portrait phones: a half is wider than the whole screen here, so the art
   can't stay clear of the menu — it's drawn smaller, fainter, and under a
   full-width shade so the logo and menu still read first. The menu moves
   in from the edge. */
@media (orientation:portrait){
  .st-art{--st-cut:70%;--st-h:118vh;}
  .st-half{opacity:.22;}
  .st-art::after{width:100%;background:linear-gradient(90deg,rgba(4,14,9,.6),rgba(4,14,9,.15));}
  .st-main,.st-foot{left:9vw;}
}

/* Start -> lobby logo hand-off (flyLogoToLobby in script.js): one fixed copy
   per character, in the logo's gold, moved by the Web Animations API from
   the start logo's glyph boxes to the lobby logo's. transform-origin 0 0 so
   translate+scale maps glyph box onto glyph box; line-height:normal makes
   each copy's box the same as the measured glyph box. .logo-hold freezes
   the lobby logo's float while it's being measured and landed on. */
.logo-fly{position:fixed;inset:0;z-index:500;pointer-events:none;}
.logo-fly span{position:fixed;transform-origin:0 0;line-height:normal;will-change:transform;
  font-family:'Noto Serif JP',serif;font-weight:900;
  background:linear-gradient(135deg,#ffe680,#c9a84c,#8a6b20);-webkit-background-clip:text;background-clip:text;color:transparent;
  filter:drop-shadow(0 0 calc(var(--ui-vmin, 7.2px) * 2.5) rgba(201,168,76,.45));}
.logo.logo-hold{animation:none;}

/* Start -> lobby transition: press-pulse 開始 (via #bstart.pressed above),
   fade+scale the start screen out, then fade+scale the lobby in once
   show() has swapped which screen has display:none — see enterLobby(). */
#s-start.screen-out{animation:screenOut .27s ease forwards;}
#s-lobby.screen-in{animation:screenIn .315s ease .045s backwards;}
@keyframes screenOut{to{opacity:0;transform:scale(.96);}}
@keyframes screenIn{from{opacity:0;transform:scale(1.03);}to{opacity:1;transform:scale(1);}}
/* Rankings/Match Settings — icon-only buttons, a flex item within
   .card-header-row (see index.html) alongside the back button and header,
   rather than absolutely anchored to .card's own corner — that's what
   actually keeps them on the header's baseline instead of just sharing a
   "top" offset with .back-icon-btn (a text glyph box and an svg box don't
   naturally come out to the same height at the same font-size). Sits flush
   against .card's own (now-reduced) padding edge rather than clawing past
   it with a negative margin — see .card's padding comment. Rankings sits
   first in DOM order so it renders left of Match Settings. */
.card-icon-btns{flex-shrink:0;display:flex;gap:calc(var(--ui-vmin, 7.2px) * 1.111);}
.card-icon-btn{background:none;border:none;padding:0;color:#888;cursor:pointer;
  display:flex;align-items:center;justify-content:center;transition:color .2s;}
.card-icon-btn:hover{color:var(--gold);}
.card-icon-btn svg{width:calc(var(--ui-vmin, 7.2px) * 1.667);height:calc(var(--ui-vmin, 7.2px) * 1.667);display:block;}
/* Readout of the current Match Settings (game mode + player count) on the
   lobby card itself — those pickers now live inside the settings modal
   (see #rules-modal), so without this there'd be no way to tell what
   you're about to queue for without opening it. */
/* Profile nameplate — icon left, name right, with a reserved line under
   the name for a future title/grade (see #pnp-title, currently always
   empty). Click anywhere on it to open the profile picker, same action
   the old #iname text field had — see refreshProfileDisplay in script.js
   for what keeps its icon/name in sync. */
/* Plain display now, not a button — logging out lives in its own Logout
   link below (see .lobby-auth-link on #lobby-inputs' Logout <a>), so this
   is just the icon+name+title readout, no click affordance of its own. */
.profile-nameplate{display:flex;align-items:center;gap:calc(var(--ui-vmin, 7.2px) * 1.389);width:100%;
  background:rgba(0,0,0,.5);border:1px solid rgba(201,168,76,.2);border-radius:2px;
  padding:calc(var(--ui-vmin, 7.2px) * 1.111) calc(var(--ui-vmin, 7.2px) * 1.667);
  text-align:left;margin-bottom:0;}
.pnp-icon{flex-shrink:0;width:calc(var(--ui-vmin, 7.2px) * 4.444);height:calc(var(--ui-vmin, 7.2px) * 4.444);}
.pnp-icon svg{width:100%;height:100%;display:block;}
.pnp-info{display:flex;flex-direction:column;flex:1;min-width:0;}
.pnp-name{font-size:calc(var(--ui-vmin, 7.2px) * 1.944);color:var(--cream);font-family:'Cinzel Decorative',serif;
  letter-spacing:calc(var(--ui-vmin, 7.2px) * .069);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.pnp-title{font-size:calc(var(--ui-vmin, 7.2px) * 1.389);color:var(--gold);opacity:.9;letter-spacing:calc(var(--ui-vmin, 7.2px) * .139);min-height:calc(var(--ui-vmin, 7.2px) * 1.667);}
/* User Profile hub's header — same icon+name pairing as .profile-nameplate
   above, boxed the same way (rounded corners, gold border) but centered and
   non-interactive in place of .about-title's plain text (see #up-icon/
   #up-name in index.html), with a brushed-gold/glossy finish to read as a
   little metal nameplate rather than a flat panel: a dark gold-to-black
   gradient for the "brushed metal" base, a diagonal light streak (::before)
   for the glossy highlight, and inset+outer shadows for plate depth. */
/* margin-left:0 lines the plate's left edge up with the hub links below
   (.hub-link has no left inset either, both start flush at .about-body's
   edge); margin-right alone pulls the right edge in enough to clear the
   ✕ button's footprint (its right offset + icon size, see .about-close)
   so the plate's own edge never sits underneath it. */
.up-nameplate{display:flex;align-items:center;justify-content:left;gap:calc(var(--ui-vmin, 7.2px) * 1.389);
  margin-left:0;margin-right:calc(var(--ui-vmin, 7.2px) * 1.2);
  position:relative;overflow:hidden;padding:calc(var(--ui-vmin, 7.2px) * 1.389) calc(var(--ui-vmin, 7.2px) * 2.222);border-radius:2px;
  transition:background .3s,border-color .3s;
  /* Bronze — the default tier (under 10 combined wins, see
     updateNameplateTier in script.js). .tier-silver/.tier-gold/
     .tier-platinum below override both border and background together;
     kept as plain modifier classes (not CSS vars) since each tier needs a
     genuinely different multi-stop gradient, not just one swapped color. */
  border:1px solid rgba(180,110,60,.6);
  background:linear-gradient(145deg,#8a5a2e,#3a2410 40%,#1a0f06 55%,#3a2410 70%,#8a5a2e);
  box-shadow:inset 0 1px 0 rgba(255,210,170,.2),inset 0 -1px 2px rgba(0,0,0,.7),0 3px 8px rgba(0,0,0,.5);}
.up-nameplate.tier-silver{border-color:rgba(210,212,218,.6);
  background:linear-gradient(145deg,#d4d6dc,#6c6e76 40%,#2c2d31 55%,#6c6e76 70%,#d4d6dc);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.3),inset 0 -1px 2px rgba(0,0,0,.7),0 3px 8px rgba(0,0,0,.5);}
.up-nameplate.tier-gold{border-color:rgba(255,210,90,.7);
  background:linear-gradient(145deg,#f5d67c,#b8892a 40%,#5a3d0a 55%,#b8892a 70%,#f5d67c);
  box-shadow:inset 0 1px 0 rgba(255,245,200,.4),inset 0 -1px 2px rgba(0,0,0,.6),0 3px 8px rgba(0,0,0,.5);}
.up-nameplate.tier-platinum{border-color:rgba(200,232,240,.65);
  background:linear-gradient(145deg,#eaf6fa,#a9c9d2 40%,#54727a 55%,#a9c9d2 70%,#eaf6fa);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.45),inset 0 -1px 2px rgba(0,20,30,.6),0 3px 8px rgba(0,0,0,.5);}
/* Diamond — 200+ wins, the top tier. Brighter/more saturated blue-white
   than Platinum (pure white highlights vs Platinum's softer gray-teal),
   plus a SECOND glossy streak (::after, mirrored direction from the base
   ::before one every tier shares) so it visibly "sparkles" rather than
   just being Platinum-but-bluer. */
.up-nameplate.tier-diamond{border-color:rgba(190,235,255,.85);
  background:linear-gradient(145deg,#ffffff,#bfe8fb 25%,#7ec8e8 45%,#4a90c2 55%,#7ec8e8 70%,#bfe8fb 85%,#ffffff);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.6),inset 0 -1px 2px rgba(0,20,40,.55),0 0 12px rgba(140,210,240,.35),0 3px 8px rgba(0,0,0,.5);}
.up-nameplate.tier-diamond::after{content:'';position:absolute;top:0;right:-25%;width:18%;height:100%;
  background:linear-gradient(100deg,transparent,rgba(255,255,255,.5),transparent);
  transform:skewX(-25deg);}
.up-nameplate::before{content:'';position:absolute;top:0;left:-30%;width:35%;height:100%;
  background:linear-gradient(100deg,transparent,rgba(255,255,255,.22),transparent);
  transform:skewX(-25deg);}
.up-nameplate .pnp-icon{width:calc(var(--ui-vmin, 7.2px) * 6.5);height:calc(var(--ui-vmin, 7.2px) * 6.5);}
/* Click affordance for #up-icon (see index.html) — it opens Player Icon
   directly now instead of a separate hub link, so it needs to visibly read
   as clickable. The SVG's outer <rect stroke="var(--gold)" stroke-width="2">
   every playerIcons entry in script.js draws is thinned to 1.2 here
   (scoped to just this one usage, not every icon-btn/profile-card
   elsewhere that reuses the same SVGs) so the frame reads as delicate at
   rest; hovering doesn't thicken it back up — instead the whole icon gets
   a soft glow (drop-shadow, transitioned via blur radius 0 → 6px so it
   animates smoothly rather than popping in) plus the frame brightening to
   the lighter --gl gold, so the emphasis reads as "lit up" rather than
   "outlined harder". `svg rect` only ever matches that one frame rect —
   every other mark in playerIcons is a circle/line/text, never a second
   rect — so this can't accidentally restyle anything else. */
.up-icon-btn{cursor:pointer;}
.up-icon-btn svg{transition:filter .25s;filter:drop-shadow(0 0 0 var(--gl));}
.up-icon-btn svg rect{stroke-width:0.5;transition:stroke .25s;}
.up-icon-btn:hover svg{filter:drop-shadow(0 0 6px var(--gl));}
.up-icon-btn:hover svg rect{stroke:var(--gl);}
.up-nameplate .pnp-name{position:relative;text-shadow:0 1px 1px rgba(0,0,0,.6);}
/* #up-title (see index.html) — same text-shadow as .pnp-name above so it
   stays legible against the metallic background. */
.up-nameplate .pnp-title{position:relative;text-shadow:0 1px 1px rgba(0,0,0,.6);}
/* Click affordance for #up-title — opens Title. Same "brighten, don't
   frame" language as .up-icon-btn above: fades from --gold to the lighter
   --gl on hover rather than adding a ring/shadow around the text. */
.up-title-btn{cursor:pointer;transition:color .2s;}
.up-title-btn:hover{color:var(--gl);}
.about-backdrop{display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:300;}
/* Same clamped scale-unit approach as #s-lobby/#s-start/.topbar (see their
   own --ui-vmin comments) — defining it here means every descendant below
   (.about-title/.about-body/.about-section-title/.hub-link/.icon-grid/
   .profile-nameplate/etc, plus .ig/.bp/.mode-btn which already read this
   var with a static 7.2px fallback for when they're used with no such
   ancestor) scales together with viewport size instead of every dialog
   rendering at one fixed pixel size regardless of a 320px phone or a 4K
   display. width is clamp()-driven the same way (50 * the 6–12px clamp
   range = 300–600px) instead of a flat 360px, still bounded by max-width
   so it can never overflow a narrow phone screen.
   Driven by 1vw, not 1vmin — vmin resolves off whichever dimension is
   SMALLER, which on any ordinary landscape desktop window is the height,
   so a vmin-based panel completely ignores how wide the window actually
   is: a 1870×950 window and a 950×950 window produced the exact same
   (small) panel, which read as "a small box floating in a lot of empty
   background" on a normal wide monitor even though there was plenty of
   width to use. vw fixes that directly (a wider window now genuinely
   grows the panel), and behaves IDENTICALLY to the old vmin value on a
   portrait phone, where width already IS the smaller dimension (vw==vmin
   there) — so this only changes landscape/desktop behavior. A short
   LANDSCAPE window (a phone rotated sideways) can in principle push the
   vw-driven height past what's visible, but max-height/overflow-y below
   already exist to catch exactly that (scrolls, doesn't overflow).
   Content inside scales by the exact same factor as the panel (font-size/
   padding are all driven by this same --ui-vmin), so growing the panel
   reads as "the same design, bigger" rather than the box outgrowing its
   own contents. Border/border-radius/box-shadow stay plain px on purpose
   — hairline chrome like that doesn't need to track viewport size, only
   content/spacing does. */
/* height is fixed (not content-driven) so every panel in this family is
   the same box, whether it's a short hub list or a long stats readout —
   without this, switching between e.g. Voice Theme (short) and My Stats
   (tall) or the User Profile hub and any of its children snaps the box to
   a different size on every navigation, which reads as a jump rather than
   a transition. 48 units matches the tallest common panel's natural
   content height at the base 7.2px scale, so most panels just gain a
   little breathing room at the bottom rather than looking empty; anything
   that still overflows it (About's text, a long My Stats/Rankings list)
   scrolls via overflow-y, same as before. max-height still wins over this
   on short viewports, so it can't push the box off-screen. .leave-modal
   (the in-game "Leave Game?" confirm) opts back out to height:auto below —
   it's a standalone 2-button confirm, not a panel in this hub/child
   navigation, so forcing it to the same tall box would just look broken. */
/* Ceiling lowered from 18px to 12px — the old ceiling was reached by any
   window ≥1800px wide, which includes ordinary 1920×1080 (FHD) displays,
   not just 4K/ultrawide ones. At 18px the box hit 900×864px — on a FHD
   screen that's ~80% of the viewport height, reading as "the panel takes
   over the whole screen" instead of a centered dialog. 12px caps it at a
   more normal 600×576px on every desktop-class window from ~1200px wide
   up, while narrower/portrait viewports are unaffected (still driven down
   to the same 6px floor as before). */
.about-modal{--ui-vmin:clamp(6px,1vw,12px);
  --pg:calc(var(--ui-vmin) * 4.5);--pw:calc(var(--ui-vmin) * .15);
  display:none;flex-direction:column;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);
  width:calc(var(--ui-vmin) * 50);height:calc(var(--ui-vmin) * 48);
  max-width:calc(100vw - 24px);max-height:calc(100vh - 48px);overflow:visible;
  background:#0a160c;border:1px solid rgba(201,168,76,.12);
  border-radius:0;padding:calc(var(--ui-vmin) * 3.333);z-index:301;box-shadow:0 12px 48px rgba(0,0,0,.8);}
/* Diagonal-stagger gold frame — the same treatment as the START button
   (#bstart), at panel scale. The modal is a flex column with .about-body
   scrolling inside it (see below) so this ::before/::after stays pinned to
   the panel edge rather than scrolling away with the content. Outer rule
   (::before) is cut at top-left + bottom-right — the ✕ sits in that
   top-right gap — inner rule (::after) at the other two. --pg is the corner
   gap (scales with the panel via --ui-vmin), --pw the rule weight. */
.about-modal::before,.about-modal::after{content:"";position:absolute;pointer-events:none;z-index:2;
  background-repeat:no-repeat;}
.about-modal::before{inset:0;
  background-image:linear-gradient(115deg,#b5903f 0%,#ffe9b0 48%,#cfa74d 100%);
  -webkit-mask:
    linear-gradient(90deg,transparent 0 var(--pg),#000 var(--pg)) top/100% var(--pw) no-repeat,
    linear-gradient(90deg,#000 0 calc(100% - var(--pg)),transparent calc(100% - var(--pg))) bottom/100% var(--pw) no-repeat,
    linear-gradient(180deg,transparent 0 var(--pg),#000 var(--pg)) left/var(--pw) 100% no-repeat,
    linear-gradient(180deg,#000 0 calc(100% - var(--pg)),transparent calc(100% - var(--pg))) right/var(--pw) 100% no-repeat;
  mask:
    linear-gradient(90deg,transparent 0 var(--pg),#000 var(--pg)) top/100% var(--pw) no-repeat,
    linear-gradient(90deg,#000 0 calc(100% - var(--pg)),transparent calc(100% - var(--pg))) bottom/100% var(--pw) no-repeat,
    linear-gradient(180deg,transparent 0 var(--pg),#000 var(--pg)) left/var(--pw) 100% no-repeat,
    linear-gradient(180deg,#000 0 calc(100% - var(--pg)),transparent calc(100% - var(--pg))) right/var(--pw) 100% no-repeat;}
.about-modal::after{inset:calc(var(--ui-vmin) * .55);opacity:.7;
  background-image:linear-gradient(115deg,#a37f35 0%,#f4e0ac 48%,#bd9740 100%);
  -webkit-mask:
    linear-gradient(90deg,#000 0 calc(100% - var(--pg)),transparent calc(100% - var(--pg))) top/100% var(--pw) no-repeat,
    linear-gradient(90deg,transparent 0 var(--pg),#000 var(--pg)) bottom/100% var(--pw) no-repeat,
    linear-gradient(180deg,#000 0 calc(100% - var(--pg)),transparent calc(100% - var(--pg))) left/var(--pw) 100% no-repeat,
    linear-gradient(180deg,transparent 0 var(--pg),#000 var(--pg)) right/var(--pw) 100% no-repeat;
  mask:
    linear-gradient(90deg,#000 0 calc(100% - var(--pg)),transparent calc(100% - var(--pg))) top/100% var(--pw) no-repeat,
    linear-gradient(90deg,transparent 0 var(--pg),#000 var(--pg)) bottom/100% var(--pw) no-repeat,
    linear-gradient(180deg,#000 0 calc(100% - var(--pg)),transparent calc(100% - var(--pg))) left/var(--pw) 100% no-repeat,
    linear-gradient(180deg,transparent 0 var(--pg),#000 var(--pg)) right/var(--pw) 100% no-repeat;}
.about-backdrop.visible{display:block;}
.about-modal.visible{display:flex;}
.leave-modal{height:auto;}
/* Smaller corner gap on the compact confirm dialogs (Leave Game? / the
   generic yes/no) — at their short auto height the default gap would eat
   almost the whole vertical rule. */
.leave-modal{--pg:calc(var(--ui-vmin) * 2.5);}
/* Portrait phones (and phone-sized/portrait tablets) have far more spare
   vertical room than --ui-vmin's width-driven (1vw) value assumes — the
   height formula above never sees how tall the viewport actually is, only
   how wide. Add real vh-based room on top of it here so tall content (My
   Stats, Rankings, a long Play History) needs less internal scrolling — content
   sizing (font/padding) is untouched, still driven by the same --ui-vmin
   as everywhere else, so this only gives the BOX more room, it doesn't
   make anything inside it bigger (per the "keep content proportions"
   requirement this was built to). min(...) with the existing max-height
   still wins on a short/squat viewport, same safety net as before. */
@media (orientation:portrait) and (max-width:820px){
  .about-modal{height:min(calc(var(--ui-vmin) * 48 + 26vh),calc(100vh - 48px));}
  /* Re-assert .leave-modal's own height:auto opt-out (see its rule above) —
     without this, the same-specificity .about-modal rule right above wins
     on a portrait phone purely because it comes later in the file, quietly
     un-opting-out the one panel that deliberately isn't part of this tall
     fixed-box family. */
  .leave-modal{height:auto;}
}
.about-close{position:absolute;top:calc(var(--ui-vmin, 7.2px) * 1.389);right:calc(var(--ui-vmin, 7.2px) * 1.667);background:none;border:none;
  color:#888;font-size:calc(var(--ui-vmin, 7.2px) * 2.222);cursor:pointer;}
/* Mirrors .about-close exactly (same top offset AND font-size, so the two
   icons sit at the same y position) on the opposite corner — returns to
   the Developer Settings hub instead of exiting it (see backToDevPanel in
   script.js). Icon-only, same as .about-close's ✕. */
.about-back{position:absolute;top:calc(var(--ui-vmin, 7.2px) * 1.389);left:calc(var(--ui-vmin, 7.2px) * 1.667);background:none;border:none;
  color:#888;font-size:calc(var(--ui-vmin, 7.2px) * 2.222);cursor:pointer;}
.about-back:hover{color:var(--gold);}
.about-title{font-family:'Cinzel Decorative',serif;font-size:calc(var(--ui-vmin, 7.2px) * 1.944);letter-spacing:calc(var(--ui-vmin, 7.2px) * .278);
  color:var(--gold);margin-bottom:calc(var(--ui-vmin, 7.2px) * 1.944);text-align:center;}
/* flex:1 + own overflow so the body scrolls INSIDE the flex-column modal
   (see .about-modal) — this is what keeps the title, the ✕/← and the gold
   frame pinned while a long list scrolls. flex-basis:auto (not 0) so the
   short confirm dialogs, which are height:auto, still size to their text. */
.about-body{font-size:calc(var(--ui-vmin, 7.2px) * 1.667);line-height:1.6;color:rgba(245,237,216,.75);
  flex:1 1 auto;min-height:0;overflow-y:auto;}
.about-body p{margin:0 0 calc(var(--ui-vmin, 7.2px) * 1.389);}
.about-body p:last-child{margin-bottom:0;}
/* Inline links inside modal body text (Legal section, sign-up's Terms/
   Privacy checkbox) — otherwise unstyled <a> tags would fall back to the
   browser default blue, which nothing else in this gold/jade palette uses. */
.about-body a,.profile-remember-row a{color:var(--gold);}
.about-body a:hover,.profile-remember-row a:hover{text-decoration:none;}
.about-section{margin-top:calc(var(--ui-vmin, 7.2px) * 1.944);padding-top:calc(var(--ui-vmin, 7.2px) * 1.944);border-top:1px solid rgba(201,168,76,.15);}
.about-section-title{font-size:calc(var(--ui-vmin, 7.2px) * 1.389);letter-spacing:calc(var(--ui-vmin, 7.2px) * .278);color:rgba(201,168,76,.95);
  font-family:'Cinzel Decorative',serif;margin-bottom:calc(var(--ui-vmin, 7.2px) * .833);}
/* Yaku Achieved section header (see renderStatsBlock/toggleYakuSection in
   script.js) — clickable to expand/collapse the achieved+not-yet-achieved
   list below it. .yaku-caret flips ▸/▾ in JS; transition here just
   smooths the color change on hover, same hover language as .hub-link. */
.yaku-toggle{cursor:pointer;display:flex;align-items:center;gap:4px;transition:color .2s;}
.yaku-toggle:hover{color:var(--gold);}
/* Rows for yaku never achieved yet (count always 0) — dimmed so the
   achieved ones above them still read as the "real" data at a glance. */
.sp-row.yaku-not-achieved{opacity:.4;}
/* Hub-panel links (Developer Settings, User Profile) — same golden look
   .about-section-title had back when these were collapsible section
   headers, kept even though they're plain links now (each opens its own
   window instead of expanding inline). */
.hub-link{display:block;font-size:calc(var(--ui-vmin, 7.2px) * 1.528);letter-spacing:calc(var(--ui-vmin, 7.2px) * .278);color:rgba(201,168,76,.95);
  font-family:'Cinzel Decorative',serif;text-align:left;text-decoration:none;padding:calc(var(--ui-vmin, 7.2px) * 1.389) 0;
  cursor:pointer;}
.hub-link:hover{color:var(--gold);}
/* Log In card's Create an Account/Forgot Password/Continue as Guest links
   (see #lobby-login-fields in index.html), plus the game card's own
   Logout link below the nameplate — sized and set in the same font as
   "Remember me" (.profile-remember-row), same dim color as the start
   screen's About link (.about-link) with no hover color change (unlike
   .hub-link, used for actual navigation links elsewhere — About > Legal,
   Developer Settings' hub). inline-block rather than .hub-link's
   display:block so the clickable area is only ever the text itself —
   each link sits in its own wrapper div in index.html for the line break
   that display:block used to give for free, since two inline-block
   elements next to each other in markup would otherwise sit side by
   side. */
.lobby-auth-link{display:inline-block;font-size:calc(var(--ui-vmin, 7.2px) * 1.389);font-family:'Noto Sans JP',sans-serif;
  color:rgba(245,237,216,.4);text-decoration:none;cursor:pointer;padding:calc(var(--ui-vmin, 7.2px) * .694) 0;}
/* Logout's own top padding overridden to near-zero so it sits snug under
   the nameplate above it (see #profile-nameplate in index.html) — the
   bottom padding stays, for breathing room before Quick Match/Room List
   below it. */
.nameplate-logout{padding-top:calc(var(--ui-vmin, 7.2px) * .139);}
/* Continue as Guest sits above the identifier field, right-aligned (see
   #lobby-login-fields in index.html) — .lobby-auth-link is already
   inline-block, so text-align on this row is enough with no extra
   override needed on the link itself. */
.lobby-guest-row{text-align:left;}
/* Floating label — "Username"/"Password"/etc. live INSIDE the
   box like a placeholder until the field is focused or has a value, then
   shrink and move to sit at the box's own top edge (see .ig-float in
   index.html for the input+label markup this pairs with, and its comment
   on why each input needs a real, if blank, placeholder attribute).
   Scoped to .ig.ig-float specifically — plain .ig (Developer Settings,
   Change Username/Code, etc.) keeps its ordinary label-above-input
   layout unchanged. */
.ig.ig-float{position:relative;}
/* Two extra classes (.ig.ig-float, not just .ig-float) so this reliably
   outranks plain .ig input's own padding shorthand regardless of which
   rule happens to come first in the file — same specificity trick as
   .ig .pw-field input below. */
.ig.ig-float input{padding-top:calc(var(--ui-vmin, 7.2px) * 2.6);padding-bottom:calc(var(--ui-vmin, 7.2px) * .9);}
.ig-float label{position:absolute;left:calc(var(--ui-vmin, 7.2px) * 1.944);top:50%;transform:translateY(-50%);
  font-size:calc(var(--ui-vmin, 7.2px) * 2.222);color:rgba(245,237,216,.55);
  text-transform:none;letter-spacing:normal;margin-bottom:0;pointer-events:none;
  transition:top .15s ease,font-size .15s ease,color .15s ease;}
.ig-float input:focus+label,
.ig-float input:not(:placeholder-shown)+label{
  top:calc(var(--ui-vmin, 7.2px) * 1.15);transform:translateY(0);
  font-size:calc(var(--ui-vmin, 7.2px) * 1.25);color:var(--gold);
  text-transform:uppercase;letter-spacing:calc(var(--ui-vmin, 7.2px) * .417);}
/* Show/hide toggle inside a password field (see .pw-field/.pw-eye in
   index.html's Log In/Sign Up forms) — the eye button sits absolutely
   inside the same wrapper as the input, so .ig input's own 100%-width
   padding rule needs a right-side callout wide enough for the icon
   without the typed text running under it; higher specificity than plain
   ".ig input" (two classes instead of one) is what lets this win without
   !important. */
.pw-field{position:relative;}
.ig .pw-field input{padding-right:calc(var(--ui-vmin, 7.2px) * 5);}
.pw-eye{position:absolute;top:50%;right:calc(var(--ui-vmin, 7.2px) * 1.111);transform:translateY(-50%);
  background:none;border:none;padding:0;margin:0;color:rgba(245,237,216,.5);cursor:pointer;
  display:flex;align-items:center;line-height:0;}
.pw-eye:hover{color:var(--gold);}
.pw-eye svg{width:calc(var(--ui-vmin, 7.2px) * 2.222);height:calc(var(--ui-vmin, 7.2px) * 2.222);}
/* Profile picker cards — icon on top, name below, one per profile (see
   renderProfileList/selectProfileCard in script.js). Reuses .icon-grid's
   3-column layout but each cell needs a taller, name-labeled card instead
   of .icon-btn's icon-only square. */
.profile-card{position:relative;background:rgba(42,58,42,.3);border:1px solid rgba(201,168,76,.2);border-radius:2px;
  padding:calc(var(--ui-vmin, 7.2px) * 1.111) calc(var(--ui-vmin, 7.2px) * .556);cursor:pointer;transition:all .2s;display:flex;flex-direction:column;
  align-items:center;gap:calc(var(--ui-vmin, 7.2px) * .556);overflow:hidden;}
.profile-card svg{width:calc(var(--ui-vmin, 7.2px) * 5);height:calc(var(--ui-vmin, 7.2px) * 5);display:block;}
.profile-card-name{font-size:calc(var(--ui-vmin, 7.2px) * 1.389);color:var(--cream);font-family:'Cinzel Decorative',serif;
  letter-spacing:calc(var(--ui-vmin, 7.2px) * .069);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;}
.profile-card:hover{border-color:rgba(201,168,76,.5);}
.profile-card.active{background:rgba(201,168,76,.2);border-color:var(--gold);}
/* Online-status dot (see the Invite Players picker, renderInvitePlayers in
   script.js) — a small glowing green mark, top-right of the card, only
   present on profiles the server reported as online right now. Pulses
   gently rather than sitting static, so "online" reads as a live signal
   rather than just another icon. */
.profile-card-dot{position:absolute;top:calc(var(--ui-vmin, 7.2px) * .556);right:calc(var(--ui-vmin, 7.2px) * .556);
  width:calc(var(--ui-vmin, 7.2px) * .972);height:calc(var(--ui-vmin, 7.2px) * .972);border-radius:50%;
  background:#4eff9a;box-shadow:0 0 6px 2px rgba(78,255,154,.8);animation:profile-dot-pulse 1.8s ease-in-out infinite;}
@keyframes profile-dot-pulse{0%,100%{opacity:1;}50%{opacity:.5;}}
/* Offline (or just currently unreachable — see server/index.js's
   'get_online_profiles') profiles are still listed, just dimmed and
   non-interactive (renderInvitePlayers never attaches an onclick to
   these), rather than missing from the picker entirely. */
.profile-card-offline{opacity:.4;cursor:default;}
.profile-card-offline:hover{border-color:rgba(201,168,76,.2);}
/* Invite Players' staged picks (see #rules-invited-list in index.html,
   renderInvitedChips in script.js) — shown above "+ Create Room" once the
   picker window closes. */
.invite-chip-list{display:flex;flex-wrap:wrap;gap:calc(var(--ui-vmin, 7.2px) * .556);margin-top:calc(var(--ui-vmin, 7.2px) * .833);}
.invite-chip{display:inline-flex;align-items:center;gap:calc(var(--ui-vmin, 7.2px) * .417);
  background:rgba(201,168,76,.15);border:1px solid rgba(201,168,76,.35);border-radius:999px;
  padding:calc(var(--ui-vmin, 7.2px) * .278) calc(var(--ui-vmin, 7.2px) * .278) calc(var(--ui-vmin, 7.2px) * .278) calc(var(--ui-vmin, 7.2px) * .833);
  font-size:calc(var(--ui-vmin, 7.2px) * 1.25);color:var(--cream);}
.invite-chip-x{background:none;border:none;color:rgba(245,237,216,.6);cursor:pointer;
  font-size:calc(var(--ui-vmin, 7.2px) * 1.111);line-height:1;padding:calc(var(--ui-vmin, 7.2px) * .417);border-radius:50%;}
.invite-chip-x:hover{color:#ff8080;background:rgba(255,128,128,.15);}
#profile-code-section{margin-top:calc(var(--ui-vmin, 7.2px) * 2.222);}
.leave-modal .about-body{text-align:center;}
.leave-actions{display:flex;gap:calc(var(--ui-vmin, 7.2px) * 1.389);margin-top:calc(var(--ui-vmin, 7.2px) * 2.5);}
.leave-actions button{flex:1;padding:calc(var(--ui-vmin, 7.2px) * 1.528);border-radius:2px;font-size:calc(var(--ui-vmin, 7.2px) * 1.667);
  font-family:'Cinzel Decorative',serif;letter-spacing:calc(var(--ui-vmin, 7.2px) * .278);cursor:pointer;transition:all .25s;}
.leave-btn-stay{background:linear-gradient(135deg,var(--jade),var(--jd));border:1px solid var(--jl);color:var(--cream);}
.leave-btn-stay:hover{transform:translateY(-2px);box-shadow:0 8px 20px rgba(26,107,69,.5);}
.leave-btn-confirm{background:rgba(120,20,20,.85);border:1px solid rgba(200,60,60,.5);color:rgba(255,200,200,.9);}
.leave-btn-confirm:hover{background:rgba(160,30,30,.95);transform:translateY(-2px);}
/* font-size here relies on --ui-vmin from whichever ancestor this button
   is currently in — #s-start, #s-lobby, or .topbar (via .tb-right) — all
   three declare the same clamp(6px,1vmin,10px), so the icon reads as one
   consistent scale wherever it's shown. */
.settings-icon-btn{position:absolute;top:calc(var(--ui-vmin, 7.2px) * 1.667);right:calc(var(--ui-vmin, 7.2px) * 1.944);background:none;border:none;
  color:#888;font-size:calc(var(--ui-vmin, 7.2px) * 2.778);cursor:pointer;z-index:10;letter-spacing:-1px;}
/* Holds an inline SVG (the profile's chosen player icon, see
   renderUserIconButton) instead of a text glyph like its .settings-icon-btn
   sibling — sized to roughly the same footprint the ☰ glyph occupies.
   calc(var(--ui-vmin))-based like everything else here (not raw vmin) so it
   shares the same clamp(6px,1vmin,10px) floor/ceiling — a bare vmin value
   would shrink with no floor on a short/narrow viewport and grow with no
   ceiling on a huge one. */
.user-icon-btn{display:flex;align-items:center;justify-content:center;padding:0;}
.user-icon-btn svg{width:calc(var(--ui-vmin, 7.2px) * 6.111);height:calc(var(--ui-vmin, 7.2px) * 6.111);display:block;}

/* Unread-notification dot on the 人 user icon — a small red disc at its
   top-right corner, toggled by the .has-notif class (see updateNotifBadge
   in script.js). A dot, not a count: it just says "there's something in
   Notifications" (inside the User Profile hub). ::after so it survives
   refreshProfileDisplay()'s innerHTML rewrite of the button. */
.user-icon-btn.has-notif::after{content:"";position:absolute;
  top:calc(var(--ui-vmin, 7.2px) * .3);right:calc(var(--ui-vmin, 7.2px) * -.4);
  width:calc(var(--ui-vmin, 7.2px) * 1.6);height:calc(var(--ui-vmin, 7.2px) * 1.6);
  border-radius:50%;background:#d02323;box-shadow:0 0 calc(var(--ui-vmin, 7.2px) * .7) rgba(208,35,35,.8);}
/* Same dot, inline after the "Notifications" hub-link text, so once the hub
   is open you can see which row has news. */
.hub-badge{display:none;width:calc(var(--ui-vmin, 7.2px) * 1.1);height:calc(var(--ui-vmin, 7.2px) * 1.1);
  border-radius:50%;background:#d02323;vertical-align:middle;margin-left:calc(var(--ui-vmin, 7.2px) * .833);}
.hub-badge.show{display:inline-block;}

/* Lobby status cluster — the "Online Friends" button + coin balance, as one
   flex row anchored where the coin balance alone used to sit (see
   #coin-count's old `right`) and growing LEFTWARD, so a wide coin count
   pushes the button further from the 人 icon rather than overlapping it.
   Same `top` as .settings-icon-btn so button, numerals, 人 and ☰ all sit
   the same distance from the top edge. */
.lobby-status{position:absolute;top:calc(var(--ui-vmin, 7.2px) * 1.667);right:calc(var(--ui-vmin, 7.2px) * 12.7);
  display:flex;align-items:center;gap:calc(var(--ui-vmin, 7.2px) * 1.4);z-index:10;}
.lobby-status-btn{background:none;border:0;padding:0;cursor:pointer;color:#888;
  display:flex;align-items:center;justify-content:center;position:relative;transition:color .18s;}
.lobby-status-btn:hover{color:var(--gold, #c9a84c);}
.lobby-status-btn svg{width:calc(var(--ui-vmin, 7.2px) * 2.9);height:calc(var(--ui-vmin, 7.2px) * 2.9);display:block;}

/* Coin balance — pale-gold serif numerals + leaning coin-stack icon over a
   gradient-fade underline (no box; see coinUnderlineSvg / updateCoins in
   script.js). Sits inside .lobby-status now (was absolutely positioned
   itself). */
.coin-count{position:relative;
  display:flex;align-items:center;gap:calc(var(--ui-vmin, 7.2px) * .6);
  padding:0 calc(var(--ui-vmin, 7.2px) * .5) calc(var(--ui-vmin, 7.2px) * .55) 0;
  color:#e6dcc2;font-family:'Noto Serif JP',serif;font-weight:700;
  font-size:calc(var(--ui-vmin, 7.2px) * 1.85);line-height:1;white-space:nowrap;
  letter-spacing:calc(var(--ui-vmin, 7.2px) * .06);font-variant-numeric:tabular-nums;
  text-shadow:0 1px calc(var(--ui-vmin, 7.2px) * .28) rgba(0,0,0,.6);
  cursor:pointer;background:none;border:0;outline-offset:calc(var(--ui-vmin, 7.2px) * .5);
  transition:filter .18s;}
.coin-count:hover{filter:brightness(1.12);}
.coin-count:active{filter:brightness(.95);}
.coin-count>span{position:relative;z-index:1;}
.coin-count .coin-icon{display:flex;align-items:center;}
/* Gradient-fade underline — a mark, not a container. Starts just left of
   the coin icon and fades out past the numerals; preserveAspectRatio="none"
   stretches it to width. */
.coin-underline{position:absolute;left:calc(var(--ui-vmin, 7.2px) * -.4);
  width:calc(100% + var(--ui-vmin, 7.2px) * .7);
  bottom:calc(var(--ui-vmin, 7.2px) * -.55);height:calc(var(--ui-vmin, 7.2px) * 2.2);
  z-index:0;pointer-events:none;}
/* The SVG coin-stack — sized to the surrounding text, used in both the
   lobby count and the #s-end reward line. */
.coin-svg{width:1.65em;height:1.65em;display:inline-block;vertical-align:-.42em;flex-shrink:0;
  filter:drop-shadow(0 1px 1px rgba(0,0,0,.4));}

/* "Coins earned +N" line on the game-over results panel (#s-end) — same
   pale gold as the lobby count, no underline (one-off line, not a HUD). */
.coin-reward{text-align:center;color:#e6dcc2;font-family:'Noto Serif JP',serif;font-weight:700;
  font-size:calc(var(--ui-vmin, 7.2px) * 1.9);letter-spacing:calc(var(--ui-vmin, 7.2px) * .08);
  margin:calc(var(--ui-vmin, 7.2px) * 1.1) 0 calc(var(--ui-vmin, 7.2px) * .4);}
.coin-reward:empty{display:none;}
.coin-reward .coin-cap{opacity:.65;font-size:.82em;}

/* Store modal (opened from #coin-count) — see renderShopIcons/
   renderShopNameplates in script.js. */
/* Balance readout — top-left. top AND left both match .about-modal's own
   padding (see that rule) — the same inset .about-title's text box starts
   from vertically, and the same left inset .shop-section-label ("PLAYER
   ICONS") starts from horizontally, since neither .about-title nor
   .about-body carry any extra margin/padding of their own beyond the
   modal's. Lines up with both rather than .about-close's higher, further-in
   corner-notch position (the ✕ deliberately sits above/right of that, in
   the gold frame's corner cutout — this shouldn't copy it). Visible right
   on the Store itself rather than only in the topbar corner behind this
   modal. Same icon/number pairing as #coin-count (populated by the same
   updateCoins() call, see script.js). */
.shop-balance{position:absolute;top:calc(var(--ui-vmin, 7.2px) * 3.333);left:calc(var(--ui-vmin, 7.2px) * 3.333);
  display:flex;align-items:center;gap:calc(var(--ui-vmin, 7.2px) * .5);
  color:#e6dcc2;font-family:'Noto Serif JP',serif;font-weight:700;
  font-size:calc(var(--ui-vmin, 7.2px) * 1.5);}
.shop-balance .coin-icon{display:flex;align-items:center;}
/* Scoped under .about-body so it outranks the generic `.about-body p`
   margin (0 top), which otherwise left each heading flush against the
   previous section's cards. */
.about-body .shop-section-label{color:var(--gold);font-family:'Cinzel Decorative',serif;
  font-size:calc(var(--ui-vmin, 7.2px) * 1.4);letter-spacing:calc(var(--ui-vmin, 7.2px) * .14);
  text-transform:uppercase;margin:calc(var(--ui-vmin, 7.2px) * 4) 0 calc(var(--ui-vmin, 7.2px) * 1.111);}
.about-body .shop-section-label:first-child{margin-top:0;}
/* minmax(0,1fr), not plain 1fr: a 1fr track can't shrink below its widest
   card's content, which on a phone pushed the grid past .about-body and
   gave the whole Store a horizontal scroll. */
.shop-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:calc(var(--ui-vmin, 7.2px) * 1.111);}
/* Nameplate cards hold an .np-preview with a fixed rem size, so they can't
   squeeze into a narrow third: drop to 2 columns (or 1) once a column would
   be narrower than the preview. The 30% floor keeps it at 3 on desktop. */
#shop-nameplate-list{grid-template-columns:repeat(auto-fill,minmax(max(7.5rem,30%),1fr));}
.shop-item{display:flex;flex-direction:column;align-items:center;gap:calc(var(--ui-vmin, 7.2px) * .417);
  background:rgba(42,58,42,.3);border:1px solid rgba(201,168,76,.2);border-radius:2px;
  padding:calc(var(--ui-vmin, 7.2px) * .833);cursor:pointer;transition:all .2s;text-align:center;}
.shop-item:hover{border-color:rgba(201,168,76,.5);}
.shop-item svg{width:calc(var(--ui-vmin, 7.2px) * 4.5);height:calc(var(--ui-vmin, 7.2px) * 4.5);display:block;}
.shop-item .np-preview{transform:scale(.85);}
.shop-item-label{font-size:calc(var(--ui-vmin, 7.2px) * 1.25);color:var(--cream);}
.shop-item-tier{font-size:calc(var(--ui-vmin, 7.2px) * .972);text-transform:uppercase;letter-spacing:.05em;}
.shop-item-tier.common{color:rgba(245,237,216,.55);}
.shop-item-tier.uncommon{color:var(--jl);}
.shop-item-tier.rare{color:var(--gl);}
.shop-item-tier.legendary{color:#ffe066;text-shadow:0 0 4px rgba(255,224,102,.6);}
.shop-item-price{font-size:calc(var(--ui-vmin, 7.2px) * 1.111);color:var(--gold);}
/* .shop-equipped/.shop-owned: already yours — border reads "selected",
   price line shows state instead of a cost. .shop-buy: for sale, shown
   identically regardless of balance — the actual funds check only ever
   happens server-side (cosmetics.purchase) when the player commits from
   the preview's Buy button (see openShopPreview), so there's nothing
   trustworthy to grey out here even if we wanted to. */
.shop-item.shop-equipped{border-color:var(--gold);background:rgba(201,168,76,.2);}
.shop-item.shop-owned:hover{border-color:var(--gl);}

/* Nameplate skin picker (#nameplate-list) and the Shop's nameplate cards
   both preview a skin via .np-preview — a small, self-contained stand-in
   for the real in-game .np, since .np's own sizing is calibrated entirely
   off --table-size (only meaningful inside the game table, see that
   class's comment) and would render degenerate here. Only the .np-skin-*
   background/border/box-shadow rules are shared with the real thing (see
   those classes below) — every size/spacing value here is its own, fixed,
   rem-based scale. */
.nameplate-grid{display:flex;flex-direction:column;gap:calc(var(--ui-vmin, 7.2px) * .833);}
.nameplate-btn{background:rgba(42,58,42,.3);border:1px solid rgba(201,168,76,.2);border-radius:2px;
  padding:calc(var(--ui-vmin, 7.2px) * .556);cursor:pointer;transition:all .2s;display:flex;justify-content:center;}
.nameplate-btn:hover{border-color:rgba(201,168,76,.5);}
.nameplate-btn.active{border-color:var(--gold);}
/* Call Style picker (#callstyle-list) — same card language as .nameplate-btn,
   laid out as a row: swatch, name, then a ▶ that plays the style. */
.callstyle-btn{display:flex;align-items:center;gap:calc(var(--ui-vmin, 7.2px) * 1.111);
  background:rgba(42,58,42,.3);border:1px solid rgba(201,168,76,.2);border-radius:2px;
  padding:calc(var(--ui-vmin, 7.2px) * .556) calc(var(--ui-vmin, 7.2px) * 1.111);cursor:pointer;transition:all .2s;text-align:left;}
.callstyle-btn:hover{border-color:rgba(201,168,76,.5);}
.callstyle-btn.active{border-color:var(--gold);background:rgba(201,168,76,.2);}
.callstyle-label{flex:1;font-size:calc(var(--ui-vmin, 7.2px) * 1.389);color:var(--cream);}
.callstyle-play{display:grid;place-items:center;width:calc(var(--ui-vmin, 7.2px) * 3.333);height:calc(var(--ui-vmin, 7.2px) * 3.333);
  border:1px solid rgba(201,168,76,.45);border-radius:50%;color:var(--gold);font-size:calc(var(--ui-vmin, 7.2px) * 1.111);transition:all .2s;}
.callstyle-play:hover{background:rgba(201,168,76,.2);border-color:var(--gold);}
.np-preview{display:inline-flex;align-items:center;gap:.5rem;padding:.5rem .9rem;
  border-radius:1px;background:#1a1a1a;box-shadow:0 0 0 1px rgba(255,255,255,.08);}
.np-preview .npv-wind{width:1.6rem;height:1.6rem;border-radius:50%;flex-shrink:0;
  display:flex;align-items:center;justify-content:center;font-family:'Noto Serif JP',serif;
  font-weight:900;font-size:.85rem;background:rgba(201,168,76,.18);color:var(--gl);
  border:1px solid rgba(201,168,76,.45);}
.np-preview .npv-text{display:flex;flex-direction:column;line-height:1.2;}
.np-preview .npv-name{font-size:.85rem;font-weight:700;color:#fff;}
/* Title line — only present in the big Shop preview (see
   renderShopPreview), not the compact picker/grid thumbnails, which stay
   wind+name only. Mirrors .ptitle's own gold/Cinzel look. */
.np-preview .npv-title{font-size:.7rem;color:var(--gold);opacity:.8;font-family:'Cinzel Decorative',serif;}

/* Nameplate skins — purchasable reskins of .np (see server/cosmetics.js's
   CATALOG.nameplate and NAMEPLATE_CATALOG in script.js). Each one only ever
   overrides background/box-shadow/color-type declarations, mirroring
   .up-nameplate's own tier classes elsewhere in this file — never padding/
   gap/font-size/width/height, so .np's own calibrated sizing (see its own
   comment) stays untouched regardless of skin. box-shadow (not a real
   `border`) for the same reason .np's own default chrome uses it: a real
   border adds to the box's rendered size, box-shadow doesn't. Each rule
   also targets .npv-wind/.npv-name so the exact same class works for both
   the real in-game plate and the Shop/picker's .np-preview above —
   deliberately placed AFTER .np-preview's own base rule: both are single-
   class selectors, so with equal specificity the later one in the
   stylesheet wins: a skin declared any earlier would lose its background/
   box-shadow to .np-preview's plain #1a1a1a default the moment both
   classes land on the same element. .np.np-skin-*.active re-asserts just
   the background — .np.active's own background:#222 would otherwise stomp
   every skin at the one moment (a seat's turn) it matters most to still
   look like the skin. (The turn's pulsing gold frame was removed; the
   centre panel shows whose turn it is.) */
.np-skin-hai{background:linear-gradient(170deg,#fffdf4,#f0e8d0);box-shadow:0 0 0 1px var(--gold),0 0 0 3px #7a5c10,inset 0 1px 0 rgba(255,255,255,.5);}
.np-skin-hai .pname,.np-skin-hai .npv-name{color:#1a1a1a;}
.np-skin-hai .ptitle,.np-skin-hai .npv-title{color:#7a5c10;}
.np-skin-hai .wb,.np-skin-hai .npv-wind{background:rgba(0,0,0,.06);color:#7a5c10;border-color:rgba(122,92,16,.4);}
.np.np-skin-hai.active{background:linear-gradient(170deg,#fffdf4,#f0e8d0);}

.np-skin-andon{background:radial-gradient(ellipse at 50% 40%,#fff3d0,#f0c878 70%,#c99a4a);box-shadow:0 0 0 1px #8a6b20,0 0 10px rgba(240,200,120,.5);}
.np-skin-andon .pname,.np-skin-andon .npv-name{color:#3a2a10;}
.np-skin-andon .ptitle,.np-skin-andon .npv-title{color:#6b5220;}
.np-skin-andon .wb,.np-skin-andon .npv-wind{background:rgba(255,255,255,.3);color:#8a6b20;border-color:rgba(138,107,32,.4);}
.np.np-skin-andon.active{background:radial-gradient(ellipse at 50% 40%,#fff3d0,#f0c878 70%,#c99a4a);}

.np-skin-kinpaku{background:radial-gradient(ellipse at 30% 30%,#f5dc90,#d4af37 40%,#a8842a 70%,#f5dc90 100%);box-shadow:0 0 0 1px #6b5218;}
.np-skin-kinpaku .pname,.np-skin-kinpaku .npv-name{color:#3a2a08;}
/* Darker than the other skins' title colour, and opacity re-asserted to 1
   — the base .ptitle/.npv-title rules dim titles to 80% opacity, which on
   every other (darker) skin still reads fine but on this pale-gold
   background washed the title out to near-illegible: the 80% fade blends
   an already gold-toned color further into the gold behind it. */
.np-skin-kinpaku .ptitle,.np-skin-kinpaku .npv-title{color:#4a3208;opacity:1;}
.np-skin-kinpaku .wb,.np-skin-kinpaku .npv-wind{background:rgba(0,0,0,.12);color:#5c4712;border-color:rgba(92,71,18,.4);}
.np.np-skin-kinpaku.active{background:radial-gradient(ellipse at 30% 30%,#f5dc90,#d4af37 40%,#a8842a 70%,#f5dc90 100%);}

.np-skin-hyosatsu{background:linear-gradient(170deg,#5a1414,#2c0a0a);box-shadow:0 0 0 1px var(--gold),inset 0 0 8px rgba(0,0,0,.5);}
.np-skin-hyosatsu .pname,.np-skin-hyosatsu .npv-name{color:#f5edd8;}
.np-skin-hyosatsu .ptitle,.np-skin-hyosatsu .npv-title{color:#e0b8b8;}
.np-skin-hyosatsu .wb,.np-skin-hyosatsu .npv-wind{background:rgba(201,168,76,.2);color:var(--gl);border-color:var(--gold);}
.np.np-skin-hyosatsu.active{background:linear-gradient(170deg,#5a1414,#2c0a0a);}

.np-skin-tatamiberi{background:linear-gradient(170deg,#d4c896,#b8a870);box-shadow:0 0 0 1px #6b5220,0 0 0 3px #2a6b45;}
.np-skin-tatamiberi .pname,.np-skin-tatamiberi .npv-name{color:#2a2408;}
.np-skin-tatamiberi .ptitle,.np-skin-tatamiberi .npv-title{color:#4a4020;}
.np-skin-tatamiberi .wb,.np-skin-tatamiberi .npv-wind{background:rgba(0,0,0,.08);color:#2a6b45;border-color:rgba(42,107,69,.4);}
.np.np-skin-tatamiberi.active{background:linear-gradient(170deg,#d4c896,#b8a870);}

/* Shop item preview (opened by clicking any Shop card, see openShopPreview
   in script.js) — a bigger, standalone look at whichever item was
   clicked, shown the way it's actually seen in play: an icon preview
   reuses the real .up-nameplate/.pnp-* profile-plate markup (see that
   class elsewhere in this file), a nameplate-skin preview reuses
   .np-preview/.np-skin-* (scaled up here) with no icon. See
   renderShopPreview's own comment for why the two differ. */
.shop-preview-stage{display:flex;justify-content:center;padding:calc(var(--ui-vmin, 7.2px) * 3.5) 0 calc(var(--ui-vmin, 7.2px) * 2);}
.shop-preview-stage .np-preview{transform:scale(1.6);}
.shop-preview-meta{text-align:center;margin-bottom:calc(var(--ui-vmin, 7.2px) * 1.5);}
.shop-preview-meta .shop-item-tier{display:block;margin-top:calc(var(--ui-vmin, 7.2px) * .417);}
.shop-preview-meta .shop-item-price{display:block;margin-top:calc(var(--ui-vmin, 7.2px) * .417);
  font-size:calc(var(--ui-vmin, 7.2px) * 1.25);}
.shop-preview-status{text-align:center;color:var(--gl);font-size:calc(var(--ui-vmin, 7.2px) * 1.389);
  padding:calc(var(--ui-vmin, 7.2px) * 1.111) 0;}
/* Shown in place of the Buy button when the price is more than the
   player's current balance — see renderShopPreview. A plain statement, not
   an error: nothing was attempted, so nothing failed. */
.shop-preview-insufficient{text-align:center;color:rgba(224,128,128,.9);font-size:calc(var(--ui-vmin, 7.2px) * 1.25);
  padding:calc(var(--ui-vmin, 7.2px) * 1.111) 0;}

/* ── Friends panel & Notifications panel ── */
.friends-my-id{display:flex;align-items:center;gap:calc(var(--ui-vmin, 7.2px) * 1.111);flex-wrap:wrap;
  margin-bottom:calc(var(--ui-vmin, 7.2px) * 2);}
.friends-my-id-label{font-size:calc(var(--ui-vmin, 7.2px) * 1.389);letter-spacing:calc(var(--ui-vmin, 7.2px) * .417);
  color:var(--gold);text-transform:uppercase;opacity:.9;}
#friends-my-id{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:calc(var(--ui-vmin, 7.2px) * 1.944);
  color:var(--cream);background:rgba(0,0,0,.25);border:1px solid rgba(212,175,110,.3);border-radius:2px;
  padding:calc(var(--ui-vmin, 7.2px) * .556) calc(var(--ui-vmin, 7.2px) * 1.111);user-select:all;letter-spacing:calc(var(--ui-vmin, 7.2px) * .139);}
#friends-copy-id{background:none;border:1px solid rgba(212,175,110,.4);border-radius:2px;
  color:var(--gold);font-size:calc(var(--ui-vmin, 7.2px) * 1.25);cursor:pointer;
  padding:calc(var(--ui-vmin, 7.2px) * .556) calc(var(--ui-vmin, 7.2px) * 1.111);font-family:'Noto Sans JP',sans-serif;}
/* Friends modal's two-tab strip — friends list vs. add-by-ID search.
   Same underline-tab language as Rankings' .rank-sort-tabs/.rank-period-tabs
   (shared bottom border, active tab picked out by a gold underline). The
   add tab (.friends-tab-icon) is a magnifier only; its label is in
   title/aria-label. Both flex:1 so "Friends" and the icon split the width
   evenly. */
.friends-tabs{display:flex;border-bottom:1px solid rgba(201,168,76,.2);
  margin-bottom:calc(var(--ui-vmin, 7.2px) * 1.111);}
.friends-tab{flex:1;display:flex;align-items:center;justify-content:center;
  padding:calc(var(--ui-vmin, 7.2px) * 1.111) 0;margin-bottom:-1px;
  background:none;border:none;border-bottom:2px solid transparent;
  font-family:'Cinzel Decorative',serif;font-size:calc(var(--ui-vmin, 7.2px) * 1.389);
  color:rgba(245,237,216,.45);cursor:pointer;transition:all .2s;}
.friends-tab:hover{color:var(--gl);}
.friends-tab.active{color:var(--gold);border-bottom-color:var(--gold);}
/* The magnifier tab is label-less — let it shrink to its icon so the two
   text tabs ("Friends", "Recently Played") get the width instead of a
   three-way even split. */
.friends-tab-icon{flex:0 0 auto;padding-left:calc(var(--ui-vmin, 7.2px) * 2.4);padding-right:calc(var(--ui-vmin, 7.2px) * 2.4);}
.friends-tab-icon svg{width:calc(var(--ui-vmin, 7.2px) * 2.222);height:calc(var(--ui-vmin, 7.2px) * 2.222);display:block;}
/* Recently Played tab — the intro line, plus a muted "3d ago" after each
   name. .recent-when sits inside .friend-nm (a flex row) so it shares that
   row's baseline with the name. */
.friends-recent-hint{color:rgba(245,237,216,.5);font-size:calc(var(--ui-vmin, 7.2px) * 1.35);
  line-height:1.5;margin:calc(var(--ui-vmin, 7.2px) * .5) 0 calc(var(--ui-vmin, 7.2px) * 1.5);}
.recent-when{flex-shrink:0;color:rgba(245,237,216,.4);font-size:calc(var(--ui-vmin, 7.2px) * 1.15);
  font-family:'Noto Sans JP',sans-serif;font-weight:400;}
.friends-section{margin-top:calc(var(--ui-vmin, 7.2px) * 1.5);}
.friends-section-title,.friends-list-title{font-size:calc(var(--ui-vmin, 7.2px) * 1.389);letter-spacing:calc(var(--ui-vmin, 7.2px) * .417);
  color:var(--gold);text-transform:uppercase;opacity:.9;margin:calc(var(--ui-vmin, 7.2px) * 1.5) 0 calc(var(--ui-vmin, 7.2px) * .833);}
.friends-list-title{margin-top:calc(var(--ui-vmin, 7.2px) * 2);}
.friends-empty{color:rgba(245,237,216,.5);font-size:calc(var(--ui-vmin, 7.2px) * 1.528);}
.friend-row{display:flex;align-items:center;gap:calc(var(--ui-vmin, 7.2px) * 1.111);
  padding:calc(var(--ui-vmin, 7.2px) * .833) 0;border-bottom:1px solid rgba(212,175,110,.15);}
.friend-row-pending{opacity:.7;}
.friend-ic{flex-shrink:0;width:calc(var(--ui-vmin, 7.2px) * 3.333);height:calc(var(--ui-vmin, 7.2px) * 3.333);display:flex;align-items:center;justify-content:center;}
.friend-ic svg{width:100%;height:100%;display:block;}
.friend-nm{flex:1;min-width:0;color:var(--cream);font-size:calc(var(--ui-vmin, 7.2px) * 1.667);
  display:flex;align-items:center;gap:calc(var(--ui-vmin, 7.2px) * .833);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.friend-dot{width:calc(var(--ui-vmin, 7.2px) * 1.111);height:calc(var(--ui-vmin, 7.2px) * 1.111);border-radius:50%;
  background:#4caf50;box-shadow:0 0 calc(var(--ui-vmin, 7.2px) * .833) #4caf50;flex-shrink:0;}
.friend-dot-busy{background:#d4af6e;box-shadow:none;}
.friend-actions{display:flex;gap:calc(var(--ui-vmin, 7.2px) * .694);flex-shrink:0;}
.friend-actions button{background:none;border:1px solid rgba(212,175,110,.4);border-radius:2px;
  color:var(--gold);font-size:calc(var(--ui-vmin, 7.2px) * 1.25);cursor:pointer;font-family:'Noto Sans JP',sans-serif;
  padding:calc(var(--ui-vmin, 7.2px) * .417) calc(var(--ui-vmin, 7.2px) * 1);white-space:nowrap;}
.friend-actions .friend-remove{border-color:rgba(224,96,96,.4);color:#e06060;}
.friend-pending{font-size:calc(var(--ui-vmin, 7.2px) * 1.25);color:rgba(245,237,216,.5);align-self:center;}

/* Online Friends modal — each row is a multi-select toggle (no checkbox
   control, the whole row is the hit target, same as the Invite Players
   picker's cards). .online-pick is the little hollow ring on the right that
   fills in when the row is selected. */
.online-row-pick{cursor:pointer;transition:background .15s;}
.online-row-pick:hover{background:rgba(212,175,110,.06);}
.friend-row.selected{background:rgba(201,168,76,.12);}
.online-pick{width:calc(var(--ui-vmin, 7.2px) * 1.8);height:calc(var(--ui-vmin, 7.2px) * 1.8);border-radius:50%;
  border:1.5px solid rgba(212,175,110,.5);box-sizing:border-box;display:block;align-self:center;transition:all .15s;}
.friend-row.selected .online-pick{background:var(--gold, #c9a84c);border-color:var(--gold, #c9a84c);
  box-shadow:0 0 calc(var(--ui-vmin, 7.2px) * .7) rgba(201,168,76,.6);}
#online-invite-btn{margin-top:calc(var(--ui-vmin, 7.2px) * 2);}
#online-invite-btn:disabled{opacity:.45;cursor:default;}
.notif-row{padding:calc(var(--ui-vmin, 7.2px) * 1.111) 0;border-bottom:1px solid rgba(212,175,110,.15);
  display:flex;flex-direction:column;gap:calc(var(--ui-vmin, 7.2px) * .833);}
.notif-row-unread{position:relative;}
.notif-row-unread::before{content:"";position:absolute;left:calc(var(--ui-vmin, 7.2px) * -1.111);top:calc(var(--ui-vmin, 7.2px) * 1.667);
  width:calc(var(--ui-vmin, 7.2px) * .833);height:calc(var(--ui-vmin, 7.2px) * .833);border-radius:50%;background:#c02020;}
.notif-text{color:var(--cream);font-size:calc(var(--ui-vmin, 7.2px) * 1.667);}
.notif-actions{display:flex;gap:calc(var(--ui-vmin, 7.2px) * .833);}
.notif-actions button{background:none;border:1px solid rgba(212,175,110,.4);border-radius:2px;
  color:var(--gold);font-size:calc(var(--ui-vmin, 7.2px) * 1.389);cursor:pointer;font-family:'Noto Sans JP',sans-serif;
  padding:calc(var(--ui-vmin, 7.2px) * .556) calc(var(--ui-vmin, 7.2px) * 1.389);}
.notif-actions .notif-decline{border-color:rgba(224,96,96,.4);color:#e06060;}
/* The update row (renderNotifications): opens What's New. */
.notif-row-release{flex-direction:row;align-items:center;gap:calc(var(--ui-vmin, 7.2px) * 1.2);cursor:pointer;}
.notif-row-release .notif-text{flex:1;min-width:0;}
.notif-row-release .notif-text small{display:block;margin-top:calc(var(--ui-vmin, 7.2px) * .3);font-size:calc(var(--ui-vmin, 7.2px) * 1.25);color:rgba(245,237,216,.5);}
.notif-row-release .notif-go{flex:0 0 auto;color:var(--gl);font-size:calc(var(--ui-vmin, 7.2px) * 1.389);white-space:nowrap;}
.notif-row-release:hover .notif-go{text-decoration:underline;}
.notif-row-release:focus-visible{outline:2px solid var(--gl);outline-offset:2px;}

/* What's New (#whatsnew-modal): one block per version, its changes under
   New / Improved / Fixed headings. Older versions dimmed. */
.wn-rel+.wn-rel{margin-top:calc(var(--ui-vmin) * 2.4);padding-top:calc(var(--ui-vmin) * 2);border-top:1px solid rgba(255,255,255,.07);}
.wn-ver{display:flex;align-items:baseline;gap:calc(var(--ui-vmin) * 1);}
.wn-ver b{font-family:'Cinzel Decorative',serif;color:var(--gl);font-size:calc(var(--ui-vmin) * 1.8);letter-spacing:.06em;}
.wn-ver span{font-size:calc(var(--ui-vmin) * 1.2);color:rgba(245,237,216,.45);}
.wn-head{margin:calc(var(--ui-vmin) * 1.4) 0 calc(var(--ui-vmin) * .5);font-family:'Noto Sans JP',sans-serif;font-weight:700;
  font-size:calc(var(--ui-vmin) * 1.15);letter-spacing:.2em;text-transform:uppercase;color:rgba(245,237,216,.5);}
.about-body .wn-list{margin:0;padding-left:calc(var(--ui-vmin) * 2);display:flex;flex-direction:column;gap:calc(var(--ui-vmin) * .5);line-height:1.5;}
.wn-list li::marker{color:rgba(240,208,128,.6);}
.wn-old{opacity:.7;}

/* ＋ Add Friend inside the in-game nameplate popover */
.pp-addfriend{margin-top:calc(var(--ui-vmin, 7.2px) * 1.111);width:100%;
  background:none;border:1px solid rgba(212,175,110,.45);border-radius:2px;
  color:var(--gold);font-size:calc(var(--ui-vmin, 7.2px) * 1.528);cursor:pointer;font-family:'Noto Sans JP',sans-serif;
  padding:calc(var(--ui-vmin, 7.2px) * .833) 0;}
/* A flex item within .card-header-row (see index.html), alongside the
   header and Rankings/Match Settings, rather than absolutely anchored to
   .card's own top-left corner — .card-header-row's align-items:center is
   what lines up the ← glyph's vertical centre with the header text/icon
   svgs, not a shared "top" offset. Sits flush against .card's own
   (now-reduced) padding edge rather than clawing past it with a negative
   margin — see .card's padding comment. Only shown on #s-lobby (there's
   nowhere "back" from #s-start itself). */
.back-icon-btn{flex-shrink:0;display:flex;align-items:center;justify-content:center;
  background:none;border:none;color:#888;font-size:calc(var(--ui-vmin, 7.2px) * 2.778);cursor:pointer;}

/* ── Hand Advice ("Help") panel ── */
/* Transparent click-catcher, not a dimming overlay — clicking anywhere
   outside the panel closes it (same onclick wiring the .about-modal family
   uses) without darkening the board behind it, since this panel doesn't
   block the game the way a true modal does. */
.advice-backdrop-clear{display:none;position:fixed;inset:0;background:transparent;z-index:300;}
.advice-backdrop-clear.visible{display:block;}
/* Anchored so its bottom-left corner sits just above the ❓ help icon that
   opened it (see positionAdvicePanel in script.js, which sets left/top
   inline from the button's own getBoundingClientRect — the left/bottom
   here are only the pre-JS/fallback position) rather than screen-centered
   like .about-modal — a glassy floating panel popping up from the button
   that triggered it, same "frosted pane" language as .player-popover (the
   nameplate-click profile card): translucent tinted gradient + blur for
   the actual glass read, one soft inset highlight along the top edge only
   (not an all-over shine), backdrop-filter degrades to the flat background
   alone on browsers without support (still fully readable). Sized smaller
   than .about-modal on purpose (36 units, capped further by vw/vh below)
   — this is a compact popover next to the board, not a full panel. */
.advice-panel{--ui-vmin:clamp(6px,1vw,9px);
  display:none;position:fixed;left:20px;bottom:20px;
  width:calc(var(--ui-vmin) * 36);max-width:min(340px,calc(100vw - 24px));
  max-height:min(70vh,480px);overflow-y:auto;
  background:linear-gradient(180deg,rgba(20,32,22,.72),rgba(10,22,12,.72));
  -webkit-backdrop-filter:blur(14px) saturate(140%);backdrop-filter:blur(14px) saturate(140%);
  border:1px solid rgba(201,168,76,.32);border-radius:8px;
  padding:calc(var(--ui-vmin) * 2.778);
  box-shadow:0 8px 28px rgba(0,0,0,.55),inset 0 1px 0 rgba(255,255,255,.08);
  z-index:301;}
.advice-panel.visible{display:block;}
/* Rows/cards inside #advice-body — same --ui-vmin scale as the rest of the
   .about-modal family (inherited from that ancestor), not raw px. */
.advice-list{display:flex;flex-direction:column;gap:calc(var(--ui-vmin, 7.2px) * .556);}
.advice-row{background:rgba(201,168,76,.08);border:1px solid rgba(201,168,76,.2);border-radius:6px;
  padding:calc(var(--ui-vmin, 7.2px) * .833) calc(var(--ui-vmin, 7.2px) * 1.111);
  font-size:calc(var(--ui-vmin, 7.2px) * 1.389);color:rgba(245,237,216,.85);}
.advice-row-best{border-color:rgba(78,255,154,.5);background:rgba(78,255,154,.08);}
.advice-row-bad{border-color:rgba(255,120,120,.35);background:rgba(255,120,120,.06);opacity:.85;}
/* Sub-lines inside a "Should You Call?" row (see renderAdvice in
   script.js) — what taking that call would actually lead to, beneath the
   shanten-based recommend/reason line above it. */
.advice-call-yaku{display:flex;flex-wrap:wrap;align-items:center;gap:2px;
  margin-top:calc(var(--ui-vmin, 7.2px) * .417);font-size:calc(var(--ui-vmin, 7.2px) * 1.25);}
.advice-call-warning{margin-top:calc(var(--ui-vmin, 7.2px) * .417);color:#ff9a9a;
  font-size:calc(var(--ui-vmin, 7.2px) * 1.25);}
.advice-call-potential{margin-top:calc(var(--ui-vmin, 7.2px) * .417);display:flex;flex-direction:column;gap:2px;}
.advice-call-potential-item{font-size:calc(var(--ui-vmin, 7.2px) * 1.2);color:rgba(245,237,216,.65);}
/* "Gives up potential: X" line under a discard candidate row (see
   renderAdvice in script.js) — amber, same "be careful" language as
   .advice-row-warn, just scoped to inside a single row instead of its own. */
.advice-lost-yaku{margin-top:calc(var(--ui-vmin, 7.2px) * .417);font-size:calc(var(--ui-vmin, 7.2px) * 1.2);color:#ffcf80;}
.advice-wait{background:rgba(201,168,76,.08);border:1px solid rgba(201,168,76,.2);border-radius:6px;
  padding:calc(var(--ui-vmin, 7.2px) * .833) calc(var(--ui-vmin, 7.2px) * 1.111);margin-bottom:calc(var(--ui-vmin, 7.2px) * .556);}
.advice-wait-tile{display:flex;align-items:center;gap:calc(var(--ui-vmin, 7.2px) * .556);
  font-family:'Cinzel Decorative',serif;color:var(--gold);font-size:calc(var(--ui-vmin, 7.2px) * 1.528);}
.advice-wait-score{font-size:calc(var(--ui-vmin, 7.2px) * 1.25);color:rgba(245,237,216,.7);margin-top:2px;}
.advice-wait-yaku{display:flex;flex-wrap:wrap;gap:2px;margin-top:calc(var(--ui-vmin, 7.2px) * .417);}
/* Mini tile icon inline within a text row — used by Yaku Reference's
   tile-art cards (renderYakuRefCard/miniTileHTML in script.js). Vertical-
   align keeps it level with the surrounding text baseline instead of
   sitting high/low like a bare inline SVG normally does. */
.advice-mini-tile{display:inline-flex;vertical-align:middle;margin-right:calc(var(--ui-vmin, 7.2px) * .556);}

/* ── Jargon glossary popups ──
   Beginner-facing mahjong terms inside the Help panel (renderAdvice in
   script.js) are wrapped as .jargon-term spans (see jt()/jtYaku()/
   jargonizeText() there) — a dotted underline marks them clickable without
   shouting over the surrounding advice text. Clicking one opens
   #jargon-popup, positioned next to it (positionJargonPopup) the same
   anchor-below/flip-above/clamp-to-viewport way #player-popover anchors to
   a clicked nameplate. */
.jargon-term{cursor:pointer;border-bottom:1px dotted rgba(201,168,76,.75);
  -webkit-tap-highlight-color:transparent;}
.jargon-term:hover,.jargon-term:focus-visible{color:var(--gl);border-bottom-color:var(--gl);outline:none;}
/* Yaku pills (.yc) already read as a distinct, clickable-looking chip on
   their own — swap the dotted underline for a filled hover instead of
   layering both treatments on top of each other. */
.yc.jargon-term{border-bottom:none;}
.yc.jargon-term:hover,.yc.jargon-term:focus-visible{background:rgba(201,168,76,.22);color:var(--cream);}
/* Same frosted-glass recipe as .advice-panel/.player-popover, just sized
   for one short definition. z-index sits above the advice panel (301) that
   spawns it. */
/* No display:none here — same convention as .player-popover: this class's
   own default is visible (flex), and the element starts (and gets
   re-hidden) via the plain inline style="display:none" in index.html /
   closeJargonPopup, not a .visible modifier class. */
.jargon-popup{--ui-vmin:clamp(6px,1vw,9px);
  position:fixed;display:flex;flex-direction:column;
  width:calc(var(--ui-vmin) * 28);max-width:min(300px,calc(100vw - 24px));
  background:linear-gradient(180deg,rgba(20,32,22,.8),rgba(10,22,12,.8));
  -webkit-backdrop-filter:blur(16px) saturate(150%);backdrop-filter:blur(16px) saturate(150%);
  border:1px solid rgba(201,168,76,.4);border-radius:8px;
  padding:calc(var(--ui-vmin) * 1.8) calc(var(--ui-vmin) * 2);
  box-shadow:0 10px 32px rgba(0,0,0,.6),inset 0 1px 0 rgba(255,255,255,.1);
  z-index:400;}
.jargon-popup-head{display:flex;align-items:baseline;gap:calc(var(--ui-vmin) * .8);
  margin-bottom:calc(var(--ui-vmin) * 1);}
#jargon-popup-term{font-weight:700;color:var(--gl);font-size:calc(var(--ui-vmin) * 1.7);flex:1;}
.jargon-popup-close{background:none;border:none;color:rgba(245,237,216,.6);
  font-size:calc(var(--ui-vmin) * 1.6);cursor:pointer;line-height:1;padding:0;}
.jargon-popup-close:hover{color:var(--cream);}
.jargon-popup-desc{font-size:calc(var(--ui-vmin) * 1.3);line-height:1.5;color:rgba(245,237,216,.88);}
/* Beginner traps/opportunities row (furiten/kita/riichi/danger — see
   renderAdvice in script.js), amber for "be careful" — distinct from
   .advice-row-best's green "do this" and .advice-row-bad's red "don't". */
.advice-row-warn{border-color:rgba(255,200,100,.4);background:rgba(255,200,100,.08);}

/* ── Magnifier (see openMagnify/renderMagnifyHand in script.js) ── */
/* A genuinely DIMMING full-screen overlay (unlike .advice-panel's
   transparent click-catcher) — this one's meant to draw full attention
   onto the enlarged hand, not sit alongside the board like a popover.
   Centers its content both ways so the hand reads as the sole focus
   regardless of how many tiles it has to lay out. */
.magnify-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.8);
  z-index:400;align-items:center;justify-content:center;cursor:pointer;}
.magnify-overlay.visible{display:flex;}
/* Own cursor:default (overriding the overlay's pointer-shows-it's-clickable
   cursor above) since this specific area either does nothing on click
   (stopPropagation, see index.html) or discards a tile — neither reads as
   "click to dismiss" the way the dimmed background around it does.
   max-width here is a pre-JS/no-JS fallback only — renderMagnifyHand sets
   it inline to the table's actual pixel width every render, which is the
   real constraint ("only the width of the table", not just the viewport).
   transform-origin:center (not the box default) so the pan-out/zoom-in
   open/close animation (openMagnify/closeMagnify in script.js) scales
   from the content's own center, matching how the translate offset in
   that animation is computed (center-to-center against #hdb). */
.magnify-content{display:flex;flex-direction:column;align-items:center;
  max-width:96vw;cursor:default;transform-origin:center;}
/* 壱/弐 row-count toggle — pinned next to the ✕ close button, top-right
   (a sibling of #magnify-content, not a child — see index.html's comment)
   rather than flowing right below the tiles, specifically so it stays put
   when switching modes: #magnify-content is vertically centered by
   .magnify-overlay, and its height changes between 1 row and 2 rows of
   tiles, which used to visibly shift the toggle along with it every time
   it was part of that flow. Boxed in its own small panel now that it's
   floating independently over the dimmed backdrop rather than sitting
   directly under the tile group — a bare row of text there would've read
   as a stray floating label with nothing to anchor it. right:70px lines
   its right edge up just left of .magnify-close's own right:20px (20px
   button + ~30px clearance). Each button still picks itself out with its
   own underline when active, same tab-strip language as elsewhere. 壱/弐
   (the formal/"daiji" forms of 一/二, read "ichi"/"ni") fit the game's
   existing Japanese-first labeling (see .about-title, tile glyphs, etc). */
.magnify-mode-toggle{position:fixed;top:16px;right:70px;
  z-index:401;display:flex;gap:2px;cursor:default;
  background:rgba(20,32,22,.85);border:1px solid rgba(201,168,76,.3);
  border-radius:6px;padding:3px;box-shadow:0 4px 16px rgba(0,0,0,.5);}
.magnify-mode-btn{padding:4px 14px;background:none;border:none;border-radius:4px;
  border-bottom:2px solid transparent;font-family:'Cinzel Decorative',serif;
  font-size:15px;letter-spacing:1px;color:rgba(245,237,216,.45);cursor:pointer;transition:all .2s;}
.magnify-mode-btn:hover{color:var(--gl);}
.magnify-mode-btn.active{color:var(--gold);border-bottom-color:var(--gold);background:rgba(201,168,76,.12);}
/* Rows stack vertically in .magnify-hand (1 child row in 壱 mode, 2 in
   弐); each .magnify-row is its own horizontal strip — `tile SIZE is
   computed in script.js from the table's actual width divided across
   however many tiles the widest row holds, so both modes fill the table
   edge-to-edge rather than shrinking to fit an arbitrary flex-wrap. Both
   the row and column gap below are only a pre-JS fallback — script.js
   sets the real gap inline (as an exact px value that its own tile-width
   budget math already accounts for), since a mismatch between the two was
   exactly what pushed the outermost tiles past the table's edge before. */
/* padding-top reserves room for a selected tile's own lift (.tlg.sel's
   translateY(-1vmin)) — transforms don't affect layout/box size, so
   without this the lifted tile pokes above .magnify-hand's own top edge
   and overflow-y:auto (needed as a safety net for tall content) clips it,
   since a scrollport's static bounds sit exactly at the container's own
   padding-box, not wherever a transformed child visually ends up. */
/* overflow-x:hidden is NOT decorative — leaving it unset (default
   'visible') while overflow-y is 'auto' hits a CSS spec quirk where the
   'visible' axis silently computes to 'auto' too whenever the other axis
   isn't 'visible', which was producing an unwanted HORIZONTAL scrollbar
   the instant tile sizing rounded even 1px over. Tile width is already
   computed in script.js to fit with no overflow at all, so this is a hard
   guarantee, not a fallback. */
.magnify-hand{display:flex;flex-direction:column;gap:30px;padding-top:16px;
  max-width:96vw;max-height:78vh;overflow-y:auto;overflow-x:hidden;}
.magnify-row{display:flex;gap:3px;justify-content:center;align-items:flex-end;}
.magnify-close{position:fixed;top:20px;right:20px;background:none;border:none;
  color:rgba(245,237,216,.85);font-size:28px;line-height:1;cursor:pointer;z-index:401;
  transition:color .2s;}
.magnify-close:hover{color:var(--gold);}

/* ── Yaku Reference cards (see renderYakuRefCard in script.js) ── */
/* Each yaku gets its own card with real tile art showing the shape, not
   just a name/han pill — a beginner can see what the pattern actually
   looks like instead of having to picture it from the Japanese term. */
.yakuref-list{display:flex;flex-direction:column;gap:calc(var(--ui-vmin, 7.2px) * .833);}
.yakuref-card{background:rgba(201,168,76,.08);border:1px solid rgba(201,168,76,.2);border-radius:8px;
  padding:calc(var(--ui-vmin, 7.2px) * .972) calc(var(--ui-vmin, 7.2px) * 1.111);}
.yakuref-card-head{display:flex;flex-wrap:wrap;align-items:baseline;gap:calc(var(--ui-vmin, 7.2px) * .417);
  font-size:calc(var(--ui-vmin, 7.2px) * 1.389);color:var(--cream);margin-bottom:calc(var(--ui-vmin, 7.2px) * .694);}
/* nowrap (not wrap) — script.js's fitYakuRefTiles sizes every tile to
   actually fit the card's rendered width in one line, so wrapping should
   never be needed; nowrap+hidden here is a hard guarantee of that (a
   backstop against a stray rounding pixel), not a fallback layout. */
.yakuref-tiles{display:flex;flex-wrap:nowrap;overflow:hidden;gap:2px;margin-bottom:calc(var(--ui-vmin, 7.2px) * .694);}
/* fitYakuRefTiles only budgets the 2px gap, so drop .advice-mini-tile's own
   right margin here or a 14-tile hand overflows and the last tiles clip. */
.yakuref-tiles .advice-mini-tile{margin-right:0;}
/* The tiles that specifically MAKE the yaku (see `highlight` in
   YAKU_REFERENCE) render at full brightness; everything else dims/
   desaturates instead — that contrast alone reads as "this subset is the
   yaku, everything else is just filling out a legal 14-tile hand around
   it", with no extra outline/frame needed on the highlighted tiles
   themselves. Whole-hand yaku highlight every tile, so nothing dims. */
.yakuref-tile-dim{opacity:.4;filter:saturate(.5);}
/* Menzen (closed-hand) requirement badge — pushed to the row's right edge
   via margin-left:auto so it doesn't crowd the name/han/jp on the left.
   Three colors matching how much the open/closed distinction actually
   matters for that yaku (see YAKU_MENZEN_BADGE in script.js): red for a
   hard requirement, amber for "scores less open", green for "no penalty
   either way". */
.yakuref-menzen-badge{margin-left:auto;padding:calc(var(--ui-vmin, 7.2px) * .278) calc(var(--ui-vmin, 7.2px) * .694);
  border-radius:999px;font-size:calc(var(--ui-vmin, 7.2px) * 1.111);white-space:nowrap;}
.yakuref-menzen-required{background:rgba(255,120,120,.15);border:1px solid rgba(255,120,120,.4);color:#ff9a9a;}
.yakuref-menzen-bonus{background:rgba(255,200,100,.15);border:1px solid rgba(255,200,100,.4);color:#ffcf80;}
.yakuref-menzen-any{background:rgba(120,255,154,.15);border:1px solid rgba(120,255,154,.4);color:#8fffb0;}
/* Situational yaku (Riichi, Ippatsu, Tenhou, ...) have no tile shape at
   all — the description below stands in for the tile row entirely, so the
   card still tells you why there's nothing to draw rather than looking
   broken/empty. */
.yakuref-desc{font-size:calc(var(--ui-vmin, 7.2px) * 1.25);color:rgba(245,237,216,.65);line-height:1.5;}

/* ── Topbar inline elements ── */
.tb-right .settings-icon-btn{position:static;z-index:auto;color:var(--gold);
  width:calc(var(--ui-vmin) * 3.6);height:calc(var(--ui-vmin) * 3.6);display:flex;align-items:center;justify-content:center;
  border:1px solid rgba(201,168,76,.45);border-radius:2px;box-sizing:border-box;font-size:calc(var(--ui-vmin) * 2.4);letter-spacing:0;line-height:1;}
.tb-mode-label{align-self:center;font-size:calc(var(--ui-vmin) * 1.3);line-height:1.35;padding:0 calc(var(--ui-vmin) * .8);border:1px solid rgba(201,168,76,.5);letter-spacing:calc(var(--ui-vmin) * .14);}
.tb-mode-label:empty{display:none;}
.tb-version{font-size:calc(var(--ui-vmin) * 1.25);opacity:.65;margin-left:6px;letter-spacing:calc(var(--ui-vmin) * .139);font-family:var(--font-sans);}
.about-version{font-size:calc(var(--ui-vmin, 7.2px) * 1.389);opacity:.95;margin-left:calc(var(--ui-vmin, 7.2px) * .833);letter-spacing:calc(var(--ui-vmin, 7.2px) * .139);font-family:var(--font-sans);}
.tb-right{display:flex;align-items:center;gap:6px;}

/* ── Game loading overlay ── */
#game-loading{position:fixed;inset:0;z-index:200;background:rgba(0,0,0,.85);
  display:none;flex-direction:column;align-items:center;justify-content:center;gap:18px;}
.gl-kanji{font-family:'Noto Serif JP',serif;font-size:clamp(28px,6vmin,48px);
  font-weight:900;color:var(--gold);letter-spacing:4px;animation:pulse 1.2s ease-in-out infinite;}
.gl-dots{display:flex;gap:10px;}
/* clamp(px,vmin,px) like .gl-kanji above, not a flat size — otherwise this
   stops shrinking with the "配牌中" kanji above it once the window gets
   small, throwing the two out of proportion with each other. */
.gl-label{font-size:clamp(9px,2vmin,13px);letter-spacing:3px;color:rgba(245,237,216,.45);font-family:'Cinzel Decorative',serif;}

/* ── Centre score layers: each turned to face its seat (bottom = me) ── */
.ctr-score-top{transform:rotate(180deg);}
.ctr-score-left{transform:rotate(90deg);}
.ctr-score-right{transform:rotate(-90deg);}

/* ── Riichi sticks: centred in the seat's slot (.ctr-score::after, which
   sets the row geometry vars), inside that seat's rotated layer (so
   .rs-drop's +Y is the player's own side for every seat). ── */
.rs-bottom,.rs-top,.rs-left,.rs-right{display:none;position:absolute;z-index:16;translate:-50% 0;
  left:calc(50% + (var(--sw) + var(--gap)) / 2 * var(--cu));
  bottom:calc((var(--row-b) + var(--row-h) / 2 - var(--stw) * 5 / 44) * var(--cu));}
/* aspect-ratio matches the viewBox: the markup's width/height attributes
   (4vmin x 1vmin) would otherwise make the box 4:1 and letterbox the
   44:10 drawing inside it, sitting the stick off-centre in its slot. */
.rs-bottom svg,.rs-top svg,.rs-left svg,.rs-right svg{width:calc(var(--stw) * var(--cu));height:auto;aspect-ratio:44 / 10;display:block;}

/* ── Meld containers ── */
.ml-b{position:absolute;bottom:0;right:0;display:flex;flex-direction:row;align-items:flex-end;z-index:15;}
.ml-t{position:absolute;top:0;left:0;display:flex;flex-direction:row;align-items:flex-end;transform:rotate(180deg);transform-origin:center center;z-index:15;}
.ml-l{position:absolute;bottom:0;left:0;display:flex;flex-direction:row;align-items:flex-end;transform:rotate(90deg) translateX(-100%);transform-origin:bottom left;z-index:15;}
.ml-r{position:absolute;top:0;right:0;display:flex;flex-direction:row;align-items:flex-end;transform:rotate(-90deg) translateY(-100%);transform-origin:top right;z-index:15;}

/* ── Nameplates positions ── */
.np-wrap-b{position:absolute;bottom:calc(var(--tlg-h) + 3vmin);left:15%;display:flex;gap:1vmin;align-items:flex-end;}
.np-inner{display:flex;align-items:center;position:relative;}
.actbar{transform:translateY(-50%);display:flex;align-items:center;gap:clamp(1px,calc(var(--table-size) * 0.0025),4px);white-space:nowrap;
  filter:drop-shadow(0 .4vmin .7vmin rgba(0,0,0,.55));}
.np-wrap-t{position:absolute;top:calc(var(--tsm-h) + 3vmin);right:15%;transform:rotate(180deg);z-index:20;}
.np-wrap-l{position:absolute;top:15%;left:calc(var(--tsm-h) + 3vmin);transform:translateX(-50%) rotate(90deg);transform-origin:center bottom;z-index:20;}
.np-wrap-r{position:absolute;bottom:15%;right:calc(var(--tsm-h) + 3vmin);transform:translateX(50%) rotate(90deg);transform-origin:center top;z-index:20;}
.np-flip{transform:rotate(180deg); transform-origin:center center;}

/* ── Kita strip (sanma only) — banked North (pei-nuki) tiles. Each seat's
   strip is rotated to FACE that seat, like its discard pile / hand / melds
   (left +90°, right -90°), and every strip starts from the felt's CENTRE
   line and grows outward toward that seat's far corner — exactly like the
   bottom seat (which starts at left:50% and grows right). Starting at the
   centre means the strip can never reach the nameplate no matter how long
   the player's name is.
   It also FILLS the way that seat's discard pile does: tiles are appended
   oldest-first (see renderState) and the OLDEST tile is the fixed anchor —
   new tiles extend away from it and nothing already placed moves.
     - bottom seat: oldest at centre, grows right;
     - left seat: oldest at centre, grows down (its discards run top→bottom);
     - right seat: oldest at centre, grows up, so the newest is on top
       (its discards run bottom→top — the mirror of the left).
   The earlier version pinned the NEWEST tile and let every older tile jump
   to make room on each new kita, which read as the strip filling backwards
   ("right to left") for both opponents. Plain `flex-direction:row` with the
   near edge pinned (via `left`, and a `transform-origin` on that same edge,
   NOT the width-scaled translate the old rule used) keeps the oldest tile
   still. ── */
/* z-index below the nameplates (.np-wrap-*, z-index:20): starting at the
   centre keeps the strip well clear of the nameplate in normal play, but
   should a corner case ever bring them together the nameplate must stay
   readable on top. Still above the melds (.ml-*, z-index:15) — a side
   strip grown to 4 tiles can just reach a full 4-meld row. */
.kita-strip{position:absolute;display:flex;flex-direction:row;gap:0;align-items:flex-end;z-index:16;}
.kita-strip:empty{display:none;}
/* Bottom seat: anchored at the felt's horizontal CENTRE (left:50%) rather
   than tucked close to the nameplate — .pname has no max-width of its own
   (grows with the player's name up to the 20-char profile limit), so a
   closer anchor risks the two overlapping on a narrow screen with a long
   name. 50% still leaves the whole right half of the row for tiles to grow
   into. max-width hard-caps growth before it reaches the right player's
   nameplate corner regardless of tile count or screen size (4 tiles is the
   realistic case; this also covers a rarer 5th+); overflow-x auto (not
   hidden) keeps an actual overflow reachable by scrolling rather than
   silently clipped. */
.kita-b{bottom:calc(var(--tlg-h) + 3vmin);left:50%;max-width:38%;overflow-x:auto;overflow-y:hidden;}
/* While a kita tile is dropping in (.tile-drop starts it at 1.7x scale with a
   halo), that overflow clipping cut the enlarged tile off and even flashed a
   scrollbar, so it didn't read like the same drop a discard gets. Lift the
   clip for the drop's duration — the strip holds at most 4 tiles, so it has
   nothing scrolled to lose in practice. */
.kita-b:has(.tile-drop){overflow:visible;}
/* kita-t: the top seat never exists in sanma (.table.sanma .kita-t is
   display:none) and kita only ever renders in sanma, so this is dead —
   kept only so the element still has a sane position if that changes. */
.kita-t{top:calc(var(--tsm-h) + 3vmin);left:15%;transform:rotate(180deg);}
/* Left seat: oldest tile's top edge on the felt's vertical centre, strip
   grows straight DOWN toward the bottom-left corner (nameplate is up at
   top:15%, so it grows away from it). Rotate about the bottom-left corner;
   the -var(--tmd-h) in `top` cancels this rule's own one-tile render
   offset so the oldest tile lands exactly on 50%. */
.kita-l{top:calc(50% - var(--tmd-h));left:calc(var(--tsm-h) + 3vmin);transform:rotate(90deg);transform-origin:bottom left;}
/* Right seat: oldest tile's bottom edge on the felt's vertical centre,
   strip grows straight UP toward the top-right corner (nameplate is down
   at bottom:15%). Rotate about the top-left corner so it grows up from a
   fixed oldest tile; `left` (not `right`) so that oldest tile — the first
   flex child — is the pinned one, and -var(--tmd-h) keeps the rotated
   strip's far edge on the felt. */
.kita-r{top:50%;left:calc(100% - (var(--tsm-h) + 3vmin) - var(--tmd-h));transform:rotate(-90deg);transform-origin:top left;}
.kita-strip .mg{outline:1.5px solid var(--gold, #c9a84c);outline-offset:-1px;border-radius:2px;}

/* ── Dora panel ── */
/* --ui-vmin: same clamped-scale-unit approach as #s-start/#s-lobby, so
   padding/font-size/margins stay proportional to each other at any
   resolution instead of drifting apart. .dora-slot's own tile size isn't
   touched here — it already comes from --tlg-w/h, computed in JS as a
   fraction of the felt's own (already-consistent) size, not px/clamp. */
/* flex-direction:column lives here (not set inline from JS) specifically
   so the panel has the same shape the instant it's shown — several call
   sites toggle `style.display='flex'` (game_start's loading state,
   round_start, ...) and only renderState() used to also set flexDirection,
   so the panel briefly rendered its two rows (slots/sticks) SIDE BY SIDE
   during the loading overlay before snapping to the stacked layout once
   the first game_state arrived. */
#dora-panel{--ui-vmin:clamp(6px,1vmin,10px);
  position:fixed;top:44px;left:8px;z-index:80;pointer-events:none;
  background:rgba(8,25,12,.97);border:2px solid rgba(201,168,76,.5);border-radius:3px;
  padding:calc(var(--ui-vmin) * 1.944) calc(var(--ui-vmin) * 2.222) calc(var(--ui-vmin) * 2.222);
  display:none;flex-direction:column;
  max-height:calc(100vh - 54px);overflow-y:auto;
  box-shadow:0 4px 24px rgba(0,0,0,.8),inset 0 1px 0 rgba(201,168,76,.2);}
.dora-slots{display:flex;gap:6px;align-items:flex-end;}
/* Live riichi-stick/honba pot, shown alongside the dora indicators for the
   duration of the round (unlike .re-carryover on the round-end screen,
   which reports what carries into the NEXT round) — populated in
   renderState() from the same game_state fields. Always visible (counts
   start from 0), unlike .re-carryover which hides when there's nothing to
   report — this is a persistent live counter, not a one-off callout. */
.dora-sticks{display:flex;gap:calc(var(--ui-vmin) * 2.5);align-items:center;justify-content:center;
  margin-top:calc(var(--ui-vmin) * 1.111);padding-top:calc(var(--ui-vmin) * 1.111);
  border-top:1px solid rgba(201,168,76,.25);}
.stick-item{display:flex;align-items:center;gap:6px;}
/* A stick's own shape/gradient mirrors .tlg's tile styling (cream face,
   gold border) for visual consistency, kept nearly rectangular (a small
   corner radius, not a pill) to read as a physical wooden stick. The
   riichi stick's red centre dot mirrors the aka-dora red marker already
   drawn on tiles (see svgMan's `marker` in script.js) — same "red mark =
   special/valuable" convention. Real 100-point sticks have no red mark at
   all (see the honba::after 4x2 black-dot grid below), which is exactly
   why it's the visually "odd one out" of a real riichi stick set. */
.stick{position:relative;flex-shrink:0;width:44px;height:9px;border-radius:1.5px;
  background:linear-gradient(170deg,#fffdf4,#f0e8d0);border:1px solid #c8a84a;
  box-shadow:0 1px 2px rgba(0,0,0,.4);}
.stick-riichi::after{content:'';position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
  width:4px;height:4px;border-radius:50%;background:#c82020;}
/* 4 columns x 2 rows of small black dots — one real dot (top-left of the
   grid) plus 7 box-shadow copies at column/row offsets, a standard
   single-element "dot grid" trick (each shadow inherits the base dot's
   border-radius, so every copy renders as a circle too). Positioned
   relative to the stick's PADDING box (border excluded from an absolutely
   positioned child's containing block) — with box-sizing:border-box
   (global reset) .stick's 44x9px already includes its 1px border, so the
   real interior to fit dots in is 42x7px, not 44x9. Real 100-point sticks
   cluster the dots tightly in the middle third of the stick rather than
   spreading them across the full length — columns at 16/19/22/25 (pitch 3,
   centered in the 42px interior) and rows at 1.5/4.5 (pitch 3, centered
   in the 7px interior) reproduce that tight, centered grid. */
.stick-honba::after{content:'';position:absolute;top:1.5px;left:16px;width:1.5px;height:1.5px;border-radius:50%;
  background:#1a1a1a;
  box-shadow:3px 0 0 #1a1a1a,6px 0 0 #1a1a1a,9px 0 0 #1a1a1a,
             0 3px 0 #1a1a1a,3px 3px 0 #1a1a1a,6px 3px 0 #1a1a1a,9px 3px 0 #1a1a1a;}
.stick-count{font-family:'Cinzel Decorative',serif;font-size:calc(var(--ui-vmin) * 1.4);color:var(--gold);font-weight:700;}

/* ── Round end screen ── */
.reb-btn{max-width:calc(var(--ui-vmin, 7.2px) * 27.78);}
.ready-status{margin-top:8px;font-size:calc(var(--ui-vmin, 7.2px) * 1.375);color:rgba(245,237,216,.5);letter-spacing:1px;text-align:center;}
/* Game-over action row: Play Again (+ host-only Start New Game) on the
   left, Leave Room on the right — a plain .reb (no flex, see its own
   lack-of-rule comment further down) is fine for the single-button Ready/
   View Results states, but a left/right split needs an explicit flex row.
   Scoped to its own wrapper (.reb-split) rather than making .reb itself
   flex, so those single-button states keep their existing centered look
   instead of collapsing to the flex-row start edge. */
/* flex-wrap:wrap stays as a last-resort safety net, but .reb-left and
   Leave Room are no longer hard-pinned to a fixed size below it (see their
   own rules) — they were both flex-shrink:0 at a fixed --ui-vmin-based
   width, which stopped shrinking the moment --ui-vmin hit its 6px floor
   (see #s-end's own comment) even though .rec kept shrinking past that
   point via its 96vw cap. That mismatch — fixed content demand vs.
   shrinking available width — was exactly what forced Leave Room onto its
   own wrapped row on ordinary phone widths (~360-414px), not just on
   genuinely tiny screens. Both items now shrink together to stay on one
   row as long as there's any reasonable room at all; wrap only kicks in
   as the true last resort on extreme widths. */
.reb-split{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:flex-start;gap:clamp(6px, 3vw, 12px);width:100%;}
/* clamp() floor (165px) is "▶ Start New Game" 's real single-line minimum
   at this row's font-size (the longest label here) — below that,
   shrinking further wraps it onto 2 lines instead, which reads worse than
   the row-wrap this whole fix is meant to avoid. Above the floor,
   flex-shrink:1 lets this column compress alongside .reb-split's
   available width; flex-basis (the clamp's preferred value) still targets
   the original 30-unit width whenever there's room for it, and
   flex-grow:0 keeps it from ballooning past that on wide screens (a plain
   flex:1 here fought .reb-split's justify-content:space-between by eating
   the gap itself instead of leaving it as empty space). min-width matches
   the floor so flexbox's own content-based auto-minimum can't override
   the clamp. Only below ~340px content width (an already-legacy phone
   size, e.g. the 320px iPhone SE) does honoring this floor leave no room
   for Leave Room beside it — .reb-split's flex-wrap then takes over as
   the last-resort fallback exactly as it already did before this fix. */
.reb-left{display:flex;flex-direction:column;gap:10px;flex:0 1 clamp(176px, 38vw, calc(var(--ui-vmin, 7.2px) * 30));min-width:176px;}
#bplay-again-start{max-width:100%;}
/* Distinct colour per action so Play Again/Start New Game/Leave Room read
   apart at a glance instead of all sharing .bp's same jade pill. */
.reb-btn-green{background:linear-gradient(135deg,var(--jade),var(--jd));border:1px solid var(--jl);color:var(--cream);}
.reb-btn-green:hover{box-shadow:0 10px 28px rgba(26,107,69,.55);border-color:var(--gold);}
.reb-btn-gold{background:linear-gradient(135deg,var(--gold),var(--gd));border:1px solid var(--gl);color:#241a04;}
.reb-btn-gold:hover{box-shadow:0 10px 28px rgba(201,168,76,.5);border-color:var(--gl);}
/* width/flex overrides (not just colour): unlike the left-side buttons
   above, which fill .reb-left's column via .bp's own width:100%, this one
   sits alone as a direct .reb-split flex item and should size to its own
   label instead of stretching to match that column's width. Horizontal
   padding uses the same clamp(...,vw,...) shrink as .rec's own padding
   (see its comment) instead of a pure --ui-vmin value, so it keeps giving
   up room on narrow phone widths rather than freezing at the vmin floor —
   same fix as .reb-left, for the same "everything stayed fixed-size past
   the floor" reason. flex-shrink:1 (not 0) lets it compress alongside
   .reb-left rather than being the one item forcing the wrap. */
.reb-btn-red{width:auto;flex:0 1 auto;min-width:0;white-space:nowrap;
  padding-left:clamp(10px, 2.5vw, calc(var(--ui-vmin, 7.2px) * 2.5));padding-right:clamp(10px, 2.5vw, calc(var(--ui-vmin, 7.2px) * 2.5));
  background:linear-gradient(135deg,rgba(120,20,20,.9),rgba(60,10,10,.95));border:1px solid rgba(200,60,60,.6);color:rgba(255,214,214,.95);}
.reb-btn-red:hover{box-shadow:0 10px 28px rgba(160,30,30,.45);border-color:rgba(255,120,120,.8);}
/* Results-panel buttons in the cutscene theme (overrides the pills above):
   each is a slanted ink strip like the standings rows, ruled top and bottom
   by a hairline (--rb-rule), with a brush kanji label from data-jp ahead of
   the Cinzel text. Play Again / Start New Game / View Results are the gold-
   tinted ink of a winner's strip; Leave Room is plain ink ruled vermilion. */
#s-end .reb-btn{--rb-rule:var(--gl);--rb-bg:rgba(46,34,8,.96);--rb-bg2:rgba(20,16,6,.96);--rb-hover:rgba(84,62,14,.97);
  display:flex;align-items:center;justify-content:center;gap:calc(var(--ui-vmin, 7.2px) * 1);
  padding:calc(var(--ui-vmin, 7.2px) * 1.3) clamp(12px, 2.6vw, calc(var(--ui-vmin, 7.2px) * 2.8));
  border:none;border-radius:0;color:var(--gl);
  background:linear-gradient(90deg,transparent,var(--rb-bg) 7%,var(--rb-bg2) 93%,transparent);
  box-shadow:inset 0 1px 0 var(--rb-rule),inset 0 -1px 0 var(--rb-rule);
  clip-path:polygon(3% 0,100% 0,97% 100%,0 100%);
  font-family:'Cinzel Decorative',serif;font-weight:700;font-size:calc(var(--ui-vmin, 7.2px) * 1.45);letter-spacing:.1em;white-space:nowrap;
  transition:background .2s,color .2s,transform .2s;}
#s-end .reb-btn::before{content:attr(data-jp);flex:none;font-family:'Noto Serif JP',serif;font-weight:700;font-size:1.5em;line-height:1;letter-spacing:.02em;}
#s-end .reb-btn:hover{transform:translateY(-2px);box-shadow:inset 0 1px 0 var(--rb-rule),inset 0 -1px 0 var(--rb-rule);
  background:linear-gradient(90deg,transparent,var(--rb-hover) 7%,var(--rb-bg) 93%,transparent);color:#fff3c4;}
#s-end .reb-btn:disabled{opacity:.45;transform:none;}
#s-end .reb-btn-red{--rb-rule:#c8322a;--rb-bg:rgba(11,13,12,.94);--rb-bg2:rgba(11,13,12,.94);--rb-hover:rgba(58,12,10,.96);color:rgba(245,237,216,.85);}
#s-end .reb-btn-red::before{color:#f08a80;}
#s-end .reb-btn-red:hover{color:var(--cream);}
#s-end .reb-btn:focus-visible{outline:2px solid var(--gl);outline-offset:3px;}

/* ── Mode button sub-label ── */
.mode-sub{font-size:calc(var(--ui-vmin, 7.2px) * 1.25);letter-spacing:calc(var(--ui-vmin, 7.2px) * .139);opacity:.9;}

/* ── Lobby mode row ── */
.mode-row{display:flex;gap:8px;margin-top:4px;}

/* ── Play History (User Profile hub) ────────────────────────────────── */
/* Wider than the default .about-modal (round-detail cards need room for a
   full hand's worth of tiles on one line — see buildHistoryRoundCard's own
   fit-sizing pass in script.js, which caps tile size to whatever width
   actually ends up available here). */
#playhistory-modal{width:calc(var(--ui-vmin) * 70);}
.ph-game-row{border-bottom:1px solid rgba(201,168,76,.12);}
.ph-game-row:last-child{border-bottom:none;}
.ph-game-head{display:flex;align-items:center;gap:calc(var(--ui-vmin, 7.2px) * 1.111);
  padding:calc(var(--ui-vmin, 7.2px) * 1.111) 0;cursor:pointer;}
/* Field order top-to-bottom: game rule + date/time sharing one line, then
   every player's placement+username+points (own row highlighted) — see
   renderPlayHistory in script.js. */
.ph-game-info{flex:1;display:flex;flex-direction:column;gap:calc(var(--ui-vmin, 7.2px) * .417);min-width:0;}
.ph-game-topline{display:flex;justify-content:space-between;align-items:baseline;gap:calc(var(--ui-vmin, 7.2px) * .833);}
.ph-game-rule{flex:1;min-width:0;font-size:calc(var(--ui-vmin, 7.2px) * 1.25);letter-spacing:calc(var(--ui-vmin, 7.2px) * .139);
  color:rgba(201,168,76,.85);text-transform:uppercase;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
/* 2 columns × N rows (2 rows for yonma's 4 seats, 2+1 for sanma's 3) rather
   than one player per line — a 4-seat game standing on 4 stacked lines was
   the single biggest driver of this modal's scroll height. */
.ph-game-standings{display:grid;grid-template-columns:1fr 1fr;
  column-gap:calc(var(--ui-vmin, 7.2px) * .833);row-gap:calc(var(--ui-vmin, 7.2px) * .278);}
.ph-standing-row{display:flex;align-items:baseline;gap:calc(var(--ui-vmin, 7.2px) * .556);min-width:0;
  padding:calc(var(--ui-vmin, 7.2px) * .139) calc(var(--ui-vmin, 7.2px) * .556);border-radius:calc(var(--ui-vmin, 7.2px) * .417);}
/* The viewing profile's own seat in the standings — same gold-tinted
   highlight language as .sp-row-mine in Rankings, so "which row is me"
   reads the same way it does elsewhere in the app. */
.ph-standing-mine{background:rgba(201,168,76,.12);border:1px solid rgba(201,168,76,.3);}
.ph-standing-mine .ph-game-name{color:var(--gold);font-weight:700;}
.ph-game-place{font-family:'Cinzel Decorative',serif;font-size:calc(var(--ui-vmin, 7.2px) * 1.25);
  font-weight:700;color:rgba(245,237,216,.5);flex:0 0 calc(var(--ui-vmin, 7.2px) * 2.5);}
.ph-game-place.place-1{color:#f0d080;}
.ph-game-place.place-2{color:#c0c0c0;}
.ph-game-place.place-3{color:#cd7f32;}
.ph-game-name{font-size:calc(var(--ui-vmin, 7.2px) * 1.389);color:var(--cream);flex:1;min-width:0;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.ph-game-score{font-size:calc(var(--ui-vmin, 7.2px) * 1.389);font-weight:700;color:var(--gl);}
.ph-game-date{font-size:calc(var(--ui-vmin, 7.2px) * 1.111);color:rgba(245,237,216,.4);flex:0 0 auto;white-space:nowrap;}
.ph-game-chevron{flex:0 0 auto;background:none;border:none;padding:calc(var(--ui-vmin, 7.2px) * .556);
  font-size:calc(var(--ui-vmin, 7.2px) * 1.667);color:var(--gold);cursor:pointer;transition:transform .2s;line-height:1;}
.ph-game-row.open .ph-game-chevron{transform:rotate(180deg);}
.ph-game-detail{display:none;padding:0 0 calc(var(--ui-vmin, 7.2px) * 1.389);}
.ph-game-row.open .ph-game-detail{display:block;}
.ph-loading{font-size:calc(var(--ui-vmin, 7.2px) * 1.389);color:rgba(245,237,216,.5);}
.ph-round-card{background:rgba(0,0,0,.2);border:1px solid rgba(201,168,76,.15);border-radius:calc(var(--ui-vmin, 7.2px) * .694);
  padding:calc(var(--ui-vmin, 7.2px) * 1.111);margin-bottom:calc(var(--ui-vmin, 7.2px) * .833);}
.ph-round-card:last-child{margin-bottom:0;}
.ph-round-head{display:flex;justify-content:space-between;align-items:baseline;gap:calc(var(--ui-vmin, 7.2px) * .833);
  margin-bottom:calc(var(--ui-vmin, 7.2px) * .694);}
.ph-round-kyoku{font-family:'Cinzel Decorative',serif;font-size:calc(var(--ui-vmin, 7.2px) * 1.389);color:var(--gold);}
.ph-round-reason{font-size:calc(var(--ui-vmin, 7.2px) * 1.111);color:rgba(245,237,216,.5);}
.ph-round-info{font-size:calc(var(--ui-vmin, 7.2px) * 1.389);margin-bottom:calc(var(--ui-vmin, 7.2px) * .694);}
.ph-yaku-row{display:flex;flex-wrap:wrap;gap:calc(var(--ui-vmin, 7.2px) * .556);margin-bottom:calc(var(--ui-vmin, 7.2px) * .694);}
.ph-tenpai{display:flex;flex-direction:column;gap:calc(var(--ui-vmin, 7.2px) * .556);margin-bottom:calc(var(--ui-vmin, 7.2px) * .694);}
.ph-score-table{width:100%;border-collapse:collapse;margin-top:calc(var(--ui-vmin, 7.2px) * .694);}
.ph-score-table td{padding:calc(var(--ui-vmin, 7.2px) * .417) 0;font-size:calc(var(--ui-vmin, 7.2px) * 1.25);
  border-top:1px solid rgba(201,168,76,.08);}
.ph-score-table td:nth-child(2){text-align:right;font-weight:700;color:var(--cream);}
.ph-score-table td:nth-child(3){text-align:right;}
/* Dealer marker (see buildHistoryRoundCard in script.js) — a plain gold
   dot, superscript after the name (a real <sup>, so it rides the raised
   baseline for free instead of a manual vertical-align offset), same
   sizing convention as .profile-card-dot, just static/no glow since "who
   was dealer" is a fixed historical fact, not a live status worth calling
   extra attention to. */
.ph-dealer-dot{display:inline-block;width:calc(var(--ui-vmin, 7.2px) * .556);height:calc(var(--ui-vmin, 7.2px) * .556);
  border-radius:50%;background:var(--gold);margin-left:calc(var(--ui-vmin, 7.2px) * .417);}
.ph-score-table tr.w td:first-child{color:var(--gl);}
.ph-score-table tr.l td:first-child{color:#e07070;}

/* Developer Settings > Player Reports — one card per Contact-form report. */
.feedback-item{border:1px solid rgba(201,168,76,.2);background:rgba(0,0,0,.3);
  padding:calc(var(--ui-vmin, 7.2px) * 1.25);margin-bottom:calc(var(--ui-vmin, 7.2px) * 1.25);}
.feedback-meta{font-size:calc(var(--ui-vmin, 7.2px) * 1.389);color:rgba(201,168,76,.85);margin-bottom:calc(var(--ui-vmin, 7.2px) * .833);}
.feedback-cat{display:inline-block;padding:0 calc(var(--ui-vmin, 7.2px) * .694);margin-right:calc(var(--ui-vmin, 7.2px) * .556);
  border:1px solid currentColor;text-transform:uppercase;letter-spacing:calc(var(--ui-vmin, 7.2px) * .139);}
.feedback-cat-bug{color:#ff8080;}
.feedback-cat-account{color:#80c0ff;}
.feedback-msg{white-space:pre-wrap;word-break:break-word;color:var(--cream);font-size:calc(var(--ui-vmin, 7.2px) * 1.667);}
.feedback-ua{font-size:calc(var(--ui-vmin, 7.2px) * 1.111);opacity:.5;margin-top:calc(var(--ui-vmin, 7.2px) * .833);word-break:break-all;}
.feedback-dismiss{margin-top:calc(var(--ui-vmin, 7.2px) * .833);background:none;border:1px solid rgba(201,168,76,.4);color:var(--gold);
  font-size:calc(var(--ui-vmin, 7.2px) * 1.25);padding:calc(var(--ui-vmin, 7.2px) * .417) calc(var(--ui-vmin, 7.2px) * 1.111);cursor:pointer;}
.feedback-dismiss:hover{border-color:var(--gold);}

/* ── Win cutscene (see playWinCutscene / csFlashCall in
   script.js) — ink slash + brush-kanji stamp, hand flip-up, yaku, limit
   tier and point count-up, then the results panel. The display pieces
   (slash, kanji, points, tiles) use plain vmin like the call banners: a
   whole-screen moment sized to the screen. Small supporting text uses the
   clamped --ui-vmin so it stays legible on phones and never balloons on
   4K. --ym-* are set from YAKUMAN_THEMES on the yakuman tier step; the
   fallbacks here are _default's gold. ── */
.cs-layer{position:fixed;inset:0;z-index:216;pointer-events:none;overflow:hidden;
  --ui-vmin:clamp(6px,1vmin,10px);
  --ym-ink:var(--gl,#f0d080);--ym-glow:255,215,100;--ym-glow2:255,180,40;--ym-accent:255,140,0;
  --ym-scrim-a:rgba(70,45,0,.6);--ym-scrim-b:rgba(0,0,0,.93);}
.cs-layer.blocking{pointer-events:auto;}
.cs-layer.cs-shake{animation:cs-shake .42s ease-out;}
.cs-layer.cs-shake-big{animation:cs-shake-big .7s ease-out;}
@keyframes cs-shake{0%,100%{transform:none}20%{transform:translate(-1.2vmin,.6vmin)}40%{transform:translate(1vmin,-.8vmin)}60%{transform:translate(-.6vmin,.4vmin)}80%{transform:translate(.3vmin,-.2vmin)}}
@keyframes cs-shake-big{0%,100%{transform:none}10%{transform:translate(-2.2vmin,1.4vmin) rotate(-.4deg)}25%{transform:translate(2vmin,-1.6vmin) rotate(.4deg)}40%{transform:translate(-1.5vmin,1vmin)}55%{transform:translate(1.1vmin,-.7vmin)}70%{transform:translate(-.6vmin,.4vmin)}85%{transform:translate(.3vmin,-.2vmin)}}

/* The game's themed backdrop (--bg-theme, the title screen's look) with a
   faint gold hatch. Translucent while a call/riichi slash plays over the
   table (.on), fully opaque once a win's hand reveal starts (.heavy) —
   the cutscene and the standings card after it are their own screen, not
   a dim over the board (the solid base matches body, since --bg-theme's
   centre is only 85% opaque). A yakuman lays its theme's scrim over the top.
   Inset past the edges so the shake never shows the table at the border. */
.cs-dim{position:absolute;inset:-4vmin;opacity:0;transition:opacity .3s;
  background:repeating-linear-gradient(45deg,rgba(201,168,76,.025) 0 2px,transparent 2px 6px),var(--bg-theme);background-color:#060c07;}
.cs-dim.on{opacity:.6;}
.cs-dim.heavy{opacity:1;}
.cs-dim.ym{opacity:1;background:radial-gradient(circle,var(--ym-scrim-a),var(--ym-scrim-b) 70%),var(--bg-theme);background-color:#060c07;}

/* Slash + headline. Everything enters on CSS delays: slash wipe 0ms,
   kanji stamp 120ms, eyebrow 280ms, romaji 340ms, name 450ms, seal 600ms. */
.cs-call{position:absolute;inset:0;z-index:3;display:grid;place-items:center;}
/* At least tall enough for the call's small lines, whose --ui-vmin size has a
   floor (so they stay readable on phones) while the band itself scales with
   vmin. The band's curved edges leave only ~65% of its height clear in the
   middle, so this leaves the kanji, romaji and name a margin from them. */
.cs-slash{--cs-bh:max(42vmin,calc(var(--ui-vmin,7.2px) * 32));position:absolute;left:-6%;width:112%;top:50%;height:var(--cs-bh);margin-top:calc(var(--cs-bh) / -2);transform:rotate(-4deg);
  clip-path:inset(0 100% 0 0);animation:cs-wipe-in .3s cubic-bezier(.7,0,.2,1) forwards;}
.cs-call.out .cs-slash{animation:cs-wipe-out .28s ease-in forwards;}
/* Default call style: the jade band's gold hairlines (a yakuman tints them its theme colour) */
.cs-edge{fill:none;stroke:var(--gold,#c9a84c);}
.cs-tone-ym .cs-edge{stroke:rgb(var(--ym-accent));}
.cs-text{position:relative;display:flex;flex-direction:column;align-items:center;gap:.8vmin;transform:rotate(-4deg);transition:opacity .2s;}
.cs-call.out .cs-text{opacity:0;}
.cs-kanji{font-family:'Noto Serif JP',serif;font-weight:900;line-height:1;white-space:nowrap;letter-spacing:.06em;color:var(--cream,#f5edd8);
  opacity:0;animation:cs-stamp .38s cubic-bezier(.2,1.6,.4,1) .12s forwards;}
.cs-len1,.cs-len2{font-size:17vmin;}
.cs-len3{font-size:14vmin;}
.cs-len4{font-size:11.5vmin;}
.cs-len5,.cs-len6{font-size:8.6vmin;letter-spacing:.02em;}
.cs-tone-call .cs-len1,.cs-tone-call .cs-len2{font-size:14vmin;}
.cs-tone-call .cs-len4,.cs-tone-call .cs-len5{font-size:10vmin;}
.cs-tone-call .cs-kanji,.cs-tone-gold .cs-kanji{color:var(--gl,#f0d080);text-shadow:0 .5vmin 0 var(--gd,#8a6b20);}
.cs-tone-ym .cs-kanji{color:var(--ym-ink);
  text-shadow:0 0 3vmin rgba(var(--ym-glow),.9),0 0 7vmin rgba(var(--ym-glow2),.55),0 0 12vmin rgba(var(--ym-accent),.35);}
.cs-romaji{font-family:'Cinzel Decorative',serif;font-weight:700;font-size:calc(var(--ui-vmin,7.2px) * 2.6);
  letter-spacing:.55em;color:var(--gold,#c9a84c);text-align:center;opacity:0;animation:cs-rise .25s ease .34s forwards;}
.cs-tone-ym .cs-romaji{color:rgba(var(--ym-glow),.9);letter-spacing:.4em;}
/* Kept to the band's flat middle: its curved, tilted edges clip a line that
   runs near the screen's sides, so very long names (a double ron on a phone)
   end in an ellipsis instead. */
.cs-name{max-width:70vw;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:calc(var(--ui-vmin,7.2px) * 2.2);letter-spacing:.1em;color:rgba(245,237,216,.8);
  opacity:0;animation:cs-rise .25s ease .45s forwards;}
.cs-eyebrow{font-family:'Noto Serif JP',serif;font-weight:700;font-size:3.4vmin;line-height:1;letter-spacing:.35em;
  color:rgba(var(--ym-glow),.95);text-shadow:0 0 2vmin rgba(var(--ym-glow),.6);opacity:0;animation:cs-drop-in .25s ease .28s forwards;}
.cs-eyebrow.double{color:rgb(var(--ym-accent));letter-spacing:.15em;}
.cs-seal{position:absolute;right:-9vmin;top:-1vmin;width:8vmin;height:8vmin;display:grid;place-items:center;
  background:#c8322a;color:var(--cream,#f5edd8);border:.3vmin solid #e9a49e;border-radius:.9vmin;
  font-family:'Noto Serif JP',serif;font-weight:900;font-size:2.8vmin;line-height:1.05;writing-mode:vertical-rl;
  opacity:0;animation:cs-seal .32s cubic-bezier(.3,1.6,.5,1) .6s forwards;}
@keyframes cs-wipe-in{from{clip-path:inset(0 100% 0 0)}to{clip-path:inset(0 0 0 0)}}
@keyframes cs-wipe-out{from{clip-path:inset(0 0 0 0)}to{clip-path:inset(0 0 0 100%)}}
@keyframes cs-stamp{0%{opacity:0;transform:scale(2.6)}50%{opacity:1}100%{opacity:1;transform:scale(1)}}
@keyframes cs-rise{from{opacity:0;transform:translateY(1vmin)}to{opacity:1;transform:none}}
@keyframes cs-drop-in{from{opacity:0;transform:translateY(-1vmin)}to{opacity:1;transform:none}}
@keyframes cs-seal{0%{opacity:0;transform:scale(2.4) rotate(25deg)}100%{opacity:1;transform:rotate(9deg)}}

/* Hand reveal, yaku and points */
.cs-reveal{position:absolute;inset:0;z-index:1;display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:2.6vmin;padding:4vmin 4vw;opacity:0;transition:opacity .25s;}
.cs-reveal.on{opacity:1;}
.cs-who{font-family:'Cinzel Decorative',serif;font-size:calc(var(--ui-vmin,7.2px) * 2.4);letter-spacing:.3em;color:var(--gold,#c9a84c);}
.cs-hand{display:flex;align-items:flex-end;gap:.4vmin;}
.cs-flip{position:relative;flex:none;perspective:60vmin;}
.cs-flip-in{position:absolute;inset:0;transform-style:preserve-3d;transition:transform .42s cubic-bezier(.3,1.45,.5,1);}
.cs-flip.down .cs-flip-in{transform:rotateY(180deg);}
.cs-flip-in>.cs-face,.cs-flip-in>.cs-back{position:absolute;inset:0;width:100%;height:100%;backface-visibility:hidden;-webkit-backface-visibility:hidden;}
.cs-flip-in>.cs-back{transform:rotateY(180deg);}
.cs-win-tile{margin-left:1.6vmin;}
.cs-win-tile:not(.down) .cs-face{box-shadow:0 0 0 .35vmin var(--gl,#f0d080),0 0 2.4vmin rgba(240,208,128,.7);animation:cs-pulse 1.4s ease-in-out infinite;}
@keyframes cs-pulse{50%{box-shadow:0 0 0 .35vmin rgba(240,208,128,.35),0 0 1vmin rgba(240,208,128,.3)}}
.cs-melds{display:flex;align-items:flex-end;gap:.4vmin;margin-left:1.6vmin;opacity:0;transition:opacity .3s;}
.cs-melds.on{opacity:1;}
.cs-melds .re-kan-back{width:var(--cs-tw);height:var(--cs-th);}
.cs-dora{display:flex;align-items:center;gap:.6vmin;font-size:calc(var(--ui-vmin,7.2px) * 1.7);letter-spacing:.15em;color:rgba(245,237,216,.6);}
.cs-dora:empty{display:none;}
.cs-dora span{margin:0 .6vmin 0 1.4vmin;}
.cs-yaku{display:flex;flex-wrap:wrap;justify-content:center;gap:1vmin 1.4vmin;max-width:min(110vmin,92vw);}
.cs-y{display:flex;align-items:baseline;gap:1vmin;padding:.8vmin 1.6vmin;border:1px solid rgba(201,168,76,.35);border-radius:4px;
  background:rgba(10,24,16,.7);opacity:0;}
/* Each badge is pressed down onto the table like a light hanko, kept well
   short of the limit-tier stamp's size so that one still lands hardest.
   The clack plays at the 60% impact (CS_BADGE_IMPACT in script.js). */
.cs-y.in{animation:cs-y-press .3s cubic-bezier(.5,0,.6,1) both;}
@keyframes cs-y-press{0%{opacity:0;transform:scale(1.7) rotate(-5deg)}60%{opacity:1;transform:scale(.95) rotate(0)}100%{opacity:1;transform:none}}
@media (prefers-reduced-motion:reduce){.cs-y.in{animation:none;opacity:1;}}
.cs-yj{font-family:'Noto Serif JP',serif;font-weight:700;font-size:calc(var(--ui-vmin,7.2px) * 2.4);color:var(--cream,#f5edd8);}
.cs-yn{font-size:calc(var(--ui-vmin,7.2px) * 1.6);color:rgba(245,237,216,.6);}
.cs-yh{font-family:'Noto Serif JP',serif;font-weight:700;font-size:calc(var(--ui-vmin,7.2px) * 2);color:var(--gl,#f0d080);}
.cs-pts{display:flex;flex-direction:column;align-items:center;gap:.6vmin;opacity:0;transform:scale(.9);
  transition:opacity .25s,transform .4s cubic-bezier(.2,1.5,.4,1);}
.cs-pts.on{opacity:1;transform:none;}
/* margin-bottom clears the number's drop shadow and the comma's tail off
   the han line below. display:block because the number sits inside
   .cs-pts-num (for the stamp), where an inline <b> would drop the margin. */
.cs-pts b{display:block;margin-bottom:2.2vmin;font-family:'Noto Serif JP',serif;font-weight:900;font-size:9vmin;line-height:1;color:var(--gl,#f0d080);
  font-variant-numeric:tabular-nums;text-shadow:0 .5vmin 0 var(--gd,#8a6b20);}
.cs-pts span{font-size:calc(var(--ui-vmin,7.2px) * 1.9);letter-spacing:.2em;color:rgba(245,237,216,.65);}
/* Limit-tier hanko (see csBuildStamp / runWinCutscene in script.js) — a
   vermilion 白文 seal (cream characters cut out of a solid red face, like
   the START button's 始) pressed down beside the points once they've counted
   up. It sits off to the right of the number so it never pushes the layout.
   Each tier is bigger and lands harder (--st size; ring from haneman,
   splatter from baiman, double border from sanbaiman, gold rim and glow for
   yakuman). .go plays the press. The impact lands at 55% of cs-stamp-press, which
   CS_STAMP_IMPACT in script.js mirrors to time the shake and thud. */
.cs-pts-num{position:relative;}
.cs-stamp{--st:11vmin;--ink:#c8322a;position:absolute;left:100%;top:50%;width:var(--st);height:var(--st);
  margin:calc(var(--st) / -2) 0 0 calc(var(--st) * .3);pointer-events:none;}
.cs-stamp.t-haneman{--st:12.5vmin;}
.cs-stamp.t-baiman{--st:14vmin;}
.cs-stamp.t-sanbaiman{--st:15vmin;}
.cs-stamp.t-yakuman,.cs-stamp.t-double-yakuman{--st:17vmin;--ink:#b5231c;}
/* Small screens (the --ui-vmin floor binds below 600px): the han lines
   under the points can't shrink with the screen, so they run wider than
   the number and pass under the stamp beside it, which hangs below the
   number's baseline (it's centred on the number and taller than it). The
   number's own --st mirrors its stamp's size (via :has, so the space is
   there before the stamp lands and nothing jumps), and the gap under the
   number grows to clear the stamp's tilted bottom corner (~0.62 of its
   size below centre with the -8deg tilt and the face's padding), minus
   half the number's height, plus a margin. */
@media (max-width:600px),(max-height:600px){
  .cs-pts{gap:max(.6vmin,calc(var(--ui-vmin,7.2px) * .5));}
  .cs-pts-num:has(.cs-stamp){--st:11vmin;}
  .cs-pts-num:has(.cs-stamp.t-haneman){--st:12.5vmin;}
  .cs-pts-num:has(.cs-stamp.t-baiman){--st:14vmin;}
  .cs-pts-num:has(.cs-stamp.t-sanbaiman){--st:15vmin;}
  .cs-pts-num:has(.cs-stamp.t-yakuman){--st:17vmin;}
  /* counted a size up: its x2 tab hangs off the seal's lower corner */
  .cs-pts-num:has(.cs-stamp.t-double-yakuman){--st:24vmin;}
  .cs-pts-num:has(.cs-stamp) b{margin-bottom:max(2.2vmin,calc(var(--st) * .62 - 4.5vmin + 3vmin));}
}
.cs-stamp-ink{position:absolute;inset:0;width:100%;height:100%;overflow:visible;opacity:0;transform:rotate(-8deg);}
/* Fill set here, not as an svg attribute: var() in a presentation attribute
   isn't supported everywhere. */
.cs-stamp-face{fill:var(--ink);}
.cs-stamp-ink text{font-family:'Noto Serif JP',serif;font-weight:900;fill:var(--cream,#f5edd8);
  text-anchor:middle;dominant-baseline:central;}
.cs-stamp.t-yakuman.go,.cs-stamp.t-double-yakuman.go{
  filter:drop-shadow(0 0 2.4vmin rgba(var(--ym-glow),.55));}/* Double yakuman's ×2 tab on the seal's corner. */
.cs-stamp-x2{position:absolute;right:calc(var(--st) * -.16);bottom:calc(var(--st) * -.1);padding:.2em .45em;opacity:0;
  border-radius:.3em;background:var(--gl,#f0d080);color:#5a1010;font-family:'Noto Serif JP',serif;font-weight:900;font-style:normal;
  font-size:calc(var(--st) * .17);line-height:1;transform:rotate(-8deg);}
.cs-stamp.go .cs-stamp-ink{animation:cs-stamp-press .5s cubic-bezier(.5,0,.6,1) forwards;}
@keyframes cs-stamp-press{
  0%  {opacity:0;transform:translateY(-5vmin) scale(2.6) rotate(-24deg);}
  20% {opacity:1;}
  55% {opacity:1;transform:scale(.9) rotate(-7deg);animation-timing-function:cubic-bezier(.2,1.4,.4,1);}
  100%{opacity:1;transform:scale(1) rotate(-8deg);}
}
.cs-stamp.go .cs-stamp-x2{animation:cs-stamp-x2 .25s ease .55s forwards;}
@keyframes cs-stamp-x2{from{opacity:0;scale:1.6}to{opacity:1;scale:1}}
/* Impact: an ink ring spreading out from under the seal (haneman up). */
.cs-stamp-ring{position:absolute;inset:0;border:calc(var(--st) * .05) solid var(--ink);border-radius:calc(var(--st) * .12);opacity:0;transform:rotate(-8deg);}
.cs-stamp.go:not(.t-mangan) .cs-stamp-ring{animation:cs-stamp-ring .55s ease-out .27s forwards;}
@keyframes cs-stamp-ring{0%{opacity:.75;transform:rotate(-8deg) scale(1)}100%{opacity:0;transform:rotate(-8deg) scale(1.9)}}
/* Splatter: ink flecks thrown out on impact (baiman up). */
.cs-stamp-dot{position:absolute;left:50%;top:50%;width:var(--d);height:var(--d);margin:calc(var(--d) / -2);border-radius:50%;
  background:var(--ink);opacity:0;}
.cs-stamp.go:is(.t-baiman,.t-sanbaiman,.t-yakuman,.t-double-yakuman) .cs-stamp-dot{animation:cs-stamp-fleck .5s cubic-bezier(.1,.8,.3,1) .27s forwards;}
@keyframes cs-stamp-fleck{0%{opacity:1;transform:rotate(var(--a)) translateX(calc(var(--st) * .3))}
  70%{opacity:.9}100%{opacity:0;transform:rotate(var(--a)) translateX(var(--r))}}
@media (prefers-reduced-motion:reduce){
  .cs-stamp.go .cs-stamp-ink,.cs-stamp.go .cs-stamp-x2{animation:none;opacity:1;}
  .cs-stamp.go .cs-stamp-ring,.cs-stamp.go .cs-stamp-dot{animation:none;}
}
.cs-skip{position:absolute;right:2vmin;bottom:2vmin;z-index:4;font-size:calc(var(--ui-vmin,7.2px) * 1.6);letter-spacing:.15em;color:rgba(245,237,216,.5);}

/* Ron snatch (playRonSnatch in script.js): the ron tile glows in the
   discarder's pond, then its flying copy sits in a fixed layer just under
   #cs-layer (216) so the cutscene's dim covers it. */
.ron-snatch-layer{position:fixed;inset:0;z-index:215;pointer-events:none;}
.ron-snatch{position:absolute;margin:0;transform-origin:center center;box-shadow:0 .4vmin 1.2vmin rgba(0,0,0,.55);}
.tile.ron-hot{box-shadow:0 0 0 .35vmin var(--gl,#f0d080),0 0 2vmin rgba(240,208,128,.75);}

/* Riichi stick is put down on the table (onRiichi adds .rs-drop to the svg),
   moving in from below: each stick sits in its seat's .ctr-score row,
   which is already rotated toward that player, so +Y here is that player's
   own side — the stick comes from the declarer's hand for every seat. */
.rs-drop{animation:rs-drop .6s cubic-bezier(.3,1.5,.5,1);}
@keyframes rs-drop{0%{opacity:0;transform:translateY(14vmin) rotate(-25deg) scale(1.4)}60%{opacity:1}100%{opacity:1;transform:none}}

@media (prefers-reduced-motion:reduce){
  .cs-layer.cs-shake,.cs-layer.cs-shake-big,.rs-drop{animation:none;}
  .cs-flip-in{transition:none;}
}
/* Win cutscene: the stacked points lines (score/limit, payment, honba +
   sticks). */
.cs-who{display:flex;align-items:center;gap:calc(var(--ui-vmin,7.2px) * 1.2);}
.cs-pts span:empty{display:none;}
.cs-pts span+span{font-size:calc(var(--ui-vmin,7.2px) * 1.7);letter-spacing:.12em;color:rgba(245,237,216,.55);}
/* Win cutscene: the itemized dora rows (Dora/Ura Dora/Aka Dora/Kita) as
   their own vermilion-edged group, and the running han total that pops
   up as each yaku/dora row lands. */
.cs-y-dora{border-color:rgba(200,50,42,.6);background:rgba(40,10,8,.6);}
.cs-y-dora .cs-yh{color:#f0a090;}
.cs-han{display:flex;align-items:baseline;gap:calc(var(--ui-vmin,7.2px) * .8);opacity:0;transition:opacity .25s;
  font-size:calc(var(--ui-vmin,7.2px) * 1.8);letter-spacing:.12em;color:rgba(245,237,216,.65);}
.cs-han.on{opacity:1;}
.cs-han b{font-family:'Noto Serif JP',serif;font-weight:900;font-size:calc(var(--ui-vmin,7.2px) * 3.4);
  color:var(--gl,#f0d080);font-variant-numeric:tabular-nums;display:inline-block;}
.cs-han b.pop{animation:cs-han-pop .32s cubic-bezier(.2,1.6,.4,1);}
@keyframes cs-han-pop{0%{transform:scale(1.6)}100%{transform:scale(1)}}
@media (prefers-reduced-motion:reduce){.cs-han b.pop{animation:none;}}
/* Win cutscene: "Tap to continue" is a real button, the only way past the
   finished cutscene (the rest of the layer ignores taps). Padded out for a
   comfortable tap target on phones. The bar drains under it over
   CS_FINAL_HOLD (its duration is set inline), showing when the standings
   card will take over by itself. */
.cs-skip{display:flex;flex-direction:column;align-items:flex-end;gap:calc(var(--ui-vmin,7.2px) * .5);
  background:none;border:0;font-family:inherit;cursor:pointer;
  padding:calc(var(--ui-vmin,7.2px) * 1.2) calc(var(--ui-vmin,7.2px) * 1.5);margin:calc(var(--ui-vmin,7.2px) * -1.2) calc(var(--ui-vmin,7.2px) * -1.5);
  transition:color .2s;}
.cs-skip:hover{color:var(--gl,#f0d080);}
.cs-skip:focus-visible{outline:1px solid var(--gold,#c9a84c);outline-offset:2px;}
.cs-hold-bar{display:block;width:100%;height:2px;background:var(--gold,#c9a84c);transform-origin:left;
  animation:cs-hold-drain linear forwards;}
@keyframes cs-hold-drain{from{transform:scaleX(1)}to{transform:scaleX(0)}}
@media (prefers-reduced-motion:reduce){.cs-hold-bar{animation:none;opacity:.5;}}
/* Between-rounds Ready (see startRoundReadyCountdown in script.js): sits in
   the bottom-right corner like the win cutscene's "Tap to continue", with
   the same draining gold bar (cs-hold-drain) over ROUND_READY_MS. Once
   pressed (or auto-sent) it settles to "Waiting…" and the bar disappears;
   #ready-status above it shows how many players are ready. */
.re-next{position:fixed;right:calc(var(--ui-vmin, 7.2px) * 2.5);bottom:calc(var(--ui-vmin, 7.2px) * 2.5);z-index:5;
  display:flex;flex-direction:column;align-items:flex-end;gap:calc(var(--ui-vmin, 7.2px) * .6);}
.re-next[hidden]{display:none;}
.re-next .ready-status{margin:0;text-align:right;font-size:calc(var(--ui-vmin, 7.2px) * 1.3);}
.re-next-btn{background:rgba(11,13,12,.85);border:1px solid var(--gold);border-radius:2px;cursor:pointer;
  padding:calc(var(--ui-vmin, 7.2px) * .8) calc(var(--ui-vmin, 7.2px) * 2.4);
  font-family:'Cinzel Decorative',serif;font-weight:700;font-size:calc(var(--ui-vmin, 7.2px) * 1.7);
  letter-spacing:.3em;color:var(--gl);transition:background .2s,box-shadow .2s;}
.re-next-btn:hover:not(:disabled){background:rgba(201,168,76,.15);box-shadow:0 0 calc(var(--ui-vmin, 7.2px) * 2) rgba(201,168,76,.4);}
.re-next-btn:focus-visible{outline:2px solid var(--gl);outline-offset:2px;}
.re-next-btn:disabled{cursor:default;color:rgba(245,237,216,.55);border-color:rgba(201,168,76,.35);}
.re-next-bar{display:block;align-self:stretch;height:2px;background:var(--gold);transform-origin:left;
  animation:cs-hold-drain linear forwards;}
.re-next.waiting .re-next-bar{visibility:hidden;}
@media (prefers-reduced-motion:reduce){.re-next-bar{animation:none;opacity:.5;}}
/* ── Win/riichi call styles (see CS_STYLES / CALLSTYLE_CATALOG in
   script.js). The default jade band uses .cs-slash above; every store style
   plays on a 16:10 .cs-stage that fills as much of the screen as it can,
   sized in cqw so each style keeps the same proportions everywhere. Names
   and small lines take max(cqw, --ui-vmin) so they stay readable when the
   stage is only phone-width. Keyframes are csx-* to keep clear of the rest
   of the stylesheet. ── */
.cs-defs{position:absolute;width:0;height:0;overflow:hidden;}
.cs-layer.cs-preview{z-index:500;}
.cs-stage{position:absolute;left:50%;top:50%;width:min(100vw,160vh);aspect-ratio:16/10;transform:translate(-50%,-50%);
  container-type:inline-size;overflow:hidden;transition:opacity .3s;}
.cs-call.cs-styled.out .cs-stage{opacity:0;}
.cs-stage .stack,.cs-stage .col{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1.4cqw;}
.cs-stage .k{font-family:'Noto Serif JP',serif;font-weight:900;font-size:var(--kz,16cqw);line-height:1;letter-spacing:.06em;white-space:nowrap;color:var(--cream,#f5edd8);
  animation:csx-stamp .4s cubic-bezier(.2,1.6,.4,1) both;}
.cs-stage .r{font-family:'Cinzel Decorative',serif;font-weight:700;font-size:max(2.4cqw,calc(var(--ui-vmin,7.2px) * 1.6));letter-spacing:.5em;padding-left:.5em;
  color:var(--gold,#c9a84c);white-space:nowrap;animation:csx-rise .3s ease .35s both;}
.cs-stage .nm{max-width:90cqw;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:max(2cqw,calc(var(--ui-vmin,7.2px) * 1.8));letter-spacing:.1em;color:rgba(245,237,216,.85);text-shadow:0 .2cqw .6cqw rgba(0,0,0,.7);
  animation:csx-rise .3s ease .45s both;}
@keyframes csx-stamp{0%{opacity:0;transform:scale(2.4)}55%{opacity:1}100%{opacity:1;transform:none}}
@keyframes csx-rise{from{opacity:0;transform:translateY(1.2cqw)}to{opacity:1;transform:none}}
@keyframes csx-fade{from{opacity:0}to{opacity:1}}

/* Folding fan (sensu) — white lettering over gold/jade ribs */
.cs-st-sensu .fan{position:absolute;left:50%;bottom:8cqw;width:80cqw;transform:translateX(-50%);}
.cs-st-sensu .fan svg{display:block;width:100%;height:auto;overflow:visible;}
.cs-st-sensu .rib{transform-box:view-box;transform-origin:100px 104px;animation:csx-fan .6s cubic-bezier(.2,.9,.3,1) both;animation-delay:calc(var(--i) * .02s);}
.cs-st-sensu .stack{padding-top:14cqw;}
.cs-st-sensu .k{color:#fff;text-shadow:0 .4cqw 0 rgba(0,0,0,.6);animation-delay:.42s;}
.cs-st-sensu .r{animation-delay:.6s;}.cs-st-sensu .nm{animation-delay:.7s;}
/* the romaji and names sit over the gold/jade ribs, so lift them off it */
.cs-st-sensu .r{color:#fff3c4;}
.cs-st-sensu .r,.cs-st-sensu .nm{text-shadow:0 .2cqw .3cqw rgba(0,0,0,.95),0 0 1.4cqw rgba(0,0,0,.85);}
@keyframes csx-fan{from{transform:rotate(0deg);opacity:.2}}

/* Paper lantern (chōchin) */
.cs-st-lantern .col{gap:1.6cqw;}
.cs-st-lantern .hang{display:flex;flex-direction:column;align-items:center;transform-origin:50% 0;
  animation:csx-drop .5s cubic-bezier(.3,1.3,.5,1) both,csx-swing 1.6s ease-out .5s forwards;}
.cs-st-lantern .cord{width:.3cqw;height:4cqw;background:#2a2a2a;}
.cs-st-lantern .cap{position:relative;z-index:1;width:12cqw;height:2.4cqw;border-radius:.4cqw;background:linear-gradient(#2b2b2b,#111);}
.cs-st-lantern .body{width:22cqw;height:30cqw;margin:-.6cqw 0;border-radius:46% / 32%;display:grid;place-items:center;
  background:repeating-linear-gradient(0deg,rgba(60,0,0,.22) 0 .25cqw,transparent .25cqw 2.6cqw),radial-gradient(ellipse at 50% 45%,#ff7a4a,#c8322a 55%,#7a1712);
  box-shadow:0 0 6cqw rgba(255,110,60,.45);animation:csx-glow 1.6s ease-in-out .5s both;}
.cs-st-lantern .k{writing-mode:vertical-rl;color:#1a0d08;font-size:min(10cqw,calc(28cqw / var(--n,2)));animation:csx-fade .3s ease .3s both;}
.cs-st-lantern .r{animation-delay:.6s;}.cs-st-lantern .nm{animation-delay:.7s;}
@keyframes csx-drop{from{transform:translateY(-70cqw)}to{transform:none}}
@keyframes csx-swing{0%{transform:rotate(7deg)}25%{transform:rotate(-5deg)}50%{transform:rotate(3deg)}75%{transform:rotate(-1.5deg)}100%{transform:none}}
@keyframes csx-glow{0%,100%{box-shadow:0 0 6cqw rgba(255,110,60,.45)}50%{box-shadow:0 0 11cqw rgba(255,140,80,.75)}}

/* Noren curtain — one indigo panel per character */
.cs-st-noren{--pw:min(16cqw,calc(60cqw / var(--n,2)));}
.cs-st-noren .col{gap:0;}
.cs-st-noren .pole{position:relative;z-index:1;width:calc(var(--n,2) * var(--pw) + (var(--n,2) - 1) * .6cqw + 6cqw);height:1.8cqw;border-radius:.9cqw;background:linear-gradient(#6b4a2c,#3a2414);}
.cs-st-noren .panels{display:flex;gap:.6cqw;margin-bottom:2cqw;}
.cs-st-noren .pn{width:var(--pw);height:32cqw;transform-origin:50% 0;display:grid;place-items:center;
  background:linear-gradient(#22386b,#152449);box-shadow:inset 0 2.2cqw 0 #101b38,inset 0 -.6cqw 0 #101b38;
  animation:csx-curtain .55s cubic-bezier(.3,1.2,.5,1) both,csx-sway 1.4s ease-out forwards;
  animation-delay:calc(var(--i) * .1s),calc(var(--i) * .1s + .55s);}
.cs-st-noren .pn span{font-family:'Noto Serif JP',serif;font-weight:900;font-size:calc(var(--pw) * .62);color:var(--cream,#f5edd8);}
.cs-st-noren .r{animation-delay:calc(var(--n,2) * .1s + .45s);}
.cs-st-noren .nm{margin-top:1cqw;animation-delay:calc(var(--n,2) * .1s + .55s);}
@keyframes csx-curtain{from{transform:scaleY(0)}to{transform:none}}
@keyframes csx-sway{0%{transform:skewX(4deg)}40%{transform:skewX(-2.5deg)}70%{transform:skewX(1deg)}100%{transform:none}}

/* Point sticks (tenbō) */
.cs-st-tenbou .sticks{position:relative;width:70cqw;height:9cqw;}
.cs-st-tenbou .tb-stick{position:absolute;left:0;right:0;top:50%;height:3.2cqw;margin-top:-1.6cqw;border-radius:1.6cqw;
  background:linear-gradient(#fff,#e4ded0);box-shadow:0 .5cqw 0 #b9b2a0,0 1cqw 2cqw rgba(0,0,0,.5);}
.cs-st-tenbou .tb-stick::before{content:"";position:absolute;left:50%;top:50%;width:2cqw;height:2cqw;margin:-1cqw;border-radius:50%;background:#c8322a;
  box-shadow:-7cqw 0 0 -.4cqw #161616,7cqw 0 0 -.4cqw #161616,-12cqw 0 0 -.4cqw #161616,12cqw 0 0 -.4cqw #161616;}
.cs-st-tenbou .st1{transform:rotate(-8deg);animation:csx-stickL .45s cubic-bezier(.3,1.4,.5,1) both;}
.cs-st-tenbou .st2{transform:rotate(8deg);animation:csx-stickR .45s cubic-bezier(.3,1.4,.5,1) .08s both;}
.cs-st-tenbou .k{text-shadow:0 .5cqw 0 rgba(0,0,0,.6);animation-delay:.35s;}
.cs-st-tenbou .r{animation-delay:.55s;}.cs-st-tenbou .nm{animation-delay:.65s;}
@keyframes csx-stickL{from{transform:translateX(-120cqw) rotate(-30deg)}to{transform:rotate(-8deg)}}
@keyframes csx-stickR{from{transform:translateX(120cqw) rotate(30deg)}to{transform:rotate(8deg)}}

/* Fireworks (hanabi) */
.cs-st-hanabi .fws{position:absolute;inset:0;}
.cs-st-hanabi .fw{position:absolute;width:0;height:0;}
.cs-st-hanabi .trail{position:absolute;left:-.15cqw;top:0;width:.3cqw;height:14cqw;background:linear-gradient(#f0d080,transparent);opacity:0;
  animation:csx-trail .4s ease-out both;animation-delay:var(--d);}
.cs-st-hanabi .sp{position:absolute;left:-.5cqw;top:-.5cqw;width:1cqw;height:1cqw;border-radius:50%;background:var(--c);box-shadow:0 0 1.2cqw var(--c);opacity:0;
  animation:csx-pop 1.1s cubic-bezier(.1,.7,.3,1) both;animation-delay:calc(var(--d) + .35s);}
.cs-st-hanabi .stack{padding-top:16cqw;}
.cs-st-hanabi .k{color:var(--gl,#f0d080);text-shadow:0 .5cqw 0 var(--gd,#8a6b20),0 0 3cqw rgba(240,208,128,.4);animation-delay:.45s;}
.cs-st-hanabi .r{animation-delay:.7s;}.cs-st-hanabi .nm{animation-delay:.8s;}
@keyframes csx-trail{0%{opacity:0;transform:translateY(24cqw)}40%{opacity:1}100%{opacity:0;transform:none}}
@keyframes csx-pop{0%{opacity:1;transform:rotate(var(--a)) translateX(0)}70%{opacity:1}100%{opacity:0;transform:rotate(var(--a)) translateX(var(--R))}}

/* Cherry blossoms (sakura) */
.cs-st-sakura .petals{position:absolute;inset:0;overflow:hidden;}
.cs-st-sakura .pt{position:absolute;top:-8cqw;width:calc(2.2cqw * var(--s));height:calc(1.6cqw * var(--s));border-radius:150% 0 150% 0;
  background:linear-gradient(135deg,#ffe2ea,#f29bb2);opacity:0;animation:csx-petal var(--t) linear both;animation-delay:var(--d);}
.cs-st-sakura .k{text-shadow:0 0 3cqw rgba(242,155,178,.65),0 .4cqw 0 rgba(0,0,0,.4);animation:csx-bloom .8s ease-out .3s both;}
.cs-st-sakura .r{animation-delay:.75s;}.cs-st-sakura .nm{animation-delay:.85s;}
@keyframes csx-petal{0%{opacity:0;transform:translate(0,0) rotate(0)}10%{opacity:1}85%{opacity:1}100%{opacity:0;transform:translate(var(--dx),80cqw) rotate(var(--rot))}}
@keyframes csx-bloom{from{opacity:0;transform:scale(.85);filter:blur(1cqw)}to{opacity:1;transform:none;filter:none}}

/* Lightning strike (kaminari) */
.cs-st-kaminari .flash{position:absolute;inset:0;background:#fffbe6;opacity:0;animation:csx-flash .55s ease-out .18s both;}
.cs-st-kaminari .bolt{position:absolute;left:50%;top:-2cqw;width:26cqw;transform:translateX(-50%);overflow:visible;}
.cs-st-kaminari .bolt-path{fill:none;stroke:#fffbe6;stroke-width:3;stroke-linejoin:miter;stroke-dasharray:420;
  filter:drop-shadow(0 0 4px #9fdcff) drop-shadow(0 0 10px #f0d080);
  animation:csx-strike .22s linear both,csx-boltfade .5s ease .45s forwards;}
.cs-st-kaminari .stack{padding-top:12cqw;}
.cs-st-kaminari .k{color:var(--gl,#f0d080);text-shadow:0 0 2cqw rgba(159,220,255,.6),0 .5cqw 0 var(--gd,#8a6b20);
  animation:csx-fade .05s .24s both,csx-zap 1s steps(1) .3s both;}
.cs-st-kaminari .r{animation-delay:.5s;}.cs-st-kaminari .nm{animation-delay:.6s;}
@keyframes csx-strike{from{stroke-dashoffset:420;opacity:1}to{stroke-dashoffset:0;opacity:1}}
@keyframes csx-boltfade{from{opacity:1}to{opacity:0}}
@keyframes csx-flash{0%{opacity:0}10%{opacity:.85}30%{opacity:.1}45%{opacity:.6}100%{opacity:0}}
@keyframes csx-zap{0%,20%,40%{text-shadow:0 0 4cqw #bfe8ff,0 .5cqw 0 #8a6b20}10%,30%{text-shadow:none}100%{text-shadow:0 0 2cqw rgba(159,220,255,.6),0 .5cqw 0 #8a6b20}}

@media (prefers-reduced-motion:reduce){
  .cs-stage *{animation:none!important;}
}

/* Store: call style swatches (grid card + preview), each in its style's colours */
.cs-swatch{display:grid;place-items:center;width:calc(var(--ui-vmin, 7.2px) * 4.5);height:calc(var(--ui-vmin, 7.2px) * 4.5);
  border:1px solid rgba(201,168,76,.5);background:#0b1a12;}
.cs-swatch b{font-family:'Noto Serif JP',serif;font-weight:700;font-size:calc(var(--ui-vmin, 7.2px) * 1.7);line-height:1;color:var(--gl);white-space:nowrap;}
.cs-sw-default{background:linear-gradient(#14583c,#072117);border-color:var(--gold);}
.cs-sw-noren{background:linear-gradient(#22386b,#152449);}
.cs-sw-noren b{color:var(--cream);}
.cs-sw-lantern{background:radial-gradient(ellipse at 50% 45%,#ff7a4a,#c8322a 60%,#7a1712);}
.cs-sw-lantern b{color:#1a0d08;}
.cs-sw-tenbou{background:linear-gradient(#fff,#e4ded0);}
.cs-sw-tenbou b{color:#c8322a;}
.cs-sw-sakura{background:linear-gradient(135deg,#ffe2ea,#f29bb2);}
.cs-sw-sakura b{color:#7a1f3a;}
.cs-sw-sensu{background:repeating-conic-gradient(from -90deg at 50% 100%,#1a6b45 0 15deg,#c9a84c 15deg 30deg);}
.cs-sw-sensu b{color:#fff;text-shadow:0 1px 2px rgba(0,0,0,.7);}
.cs-sw-kaminari{background:radial-gradient(circle,#2a3550,#0b0d12);}
.cs-sw-kaminari b{color:#fffbe6;text-shadow:0 0 6px #9fdcff;}
.cs-sw-hanabi{background:radial-gradient(circle at 30% 30%,rgba(240,208,128,.5) 0 8%,transparent 9%),radial-gradient(circle at 72% 40%,rgba(255,122,90,.5) 0 7%,transparent 8%),#0b0d12;}
.cs-preview-card{display:flex;flex-direction:column;align-items:center;gap:calc(var(--ui-vmin, 7.2px) * 1.5);}
.cs-preview-card .cs-swatch{width:calc(var(--ui-vmin, 7.2px) * 9);height:calc(var(--ui-vmin, 7.2px) * 9);}
.cs-preview-card .cs-swatch b{font-size:calc(var(--ui-vmin, 7.2px) * 3.4);}

/* ════════════════════════════════════════════════════════════
   MENU THEME · D "Glass"
   Menus restyled to match the in-game chat bar (.chat-bar): frosted dark
   glass panels with a faint white hairline, small square-ish corners, a
   solid gold primary (like .chat-send) and the in-game action buttons'
   slant + ghost kanji (.ba) on every menu button. Kept as one override
   block so the whole look can be swapped for another direction later.
   Covers: .about-modal popups, the lobby .card, #settings-panel, and the
   buttons / pickers / toggles / fields inside them. The game-over
   buttons (.reb-btn) keep their own look.
   ════════════════════════════════════════════════════════════ */
:root{--glass-bg:rgba(10,18,13,.55);--glass-fx:blur(14px) saturate(1.2);--glass-edge:rgba(255,255,255,.09);
  --glass-tile:rgba(255,255,255,.05);--glass-tile-hi:rgba(255,255,255,.1);--glass-on:rgba(240,208,128,.15);--glass-on-edge:rgba(240,208,128,.55);}

/* ── Panels ── the stagger gold frame is dropped; the hairline is an inset shadow. */
.about-modal,.card{background:var(--glass-bg);-webkit-backdrop-filter:var(--glass-fx);backdrop-filter:var(--glass-fx);
  border:0;border-radius:calc(var(--ui-vmin, 7.2px) * .6);box-shadow:inset 0 0 0 1px var(--glass-edge),0 18px 50px rgba(0,0,0,.45);}
.about-modal::before,.about-modal::after,.card::before,.card::after{content:none;}
/* Popups are fully opaque (no see-through, no blur) so nothing behind them
   competes with their contents. To lift the box off the dimmed page: a
   surface a step lighter than the page, a thin gold-leaf top edge (the
   START button's gradient, like the in-game top bar's gold rule), a
   brighter hairline and a deep drop shadow. The gold edge is a background
   layer, so it stays put while .about-body scrolls. */
.about-modal{background:linear-gradient(115deg,#b5903f 0%,#ffe9b0 48%,#cfa74d 100%) top/100% 2px no-repeat,
    linear-gradient(180deg,rgba(255,255,255,.04),rgba(255,255,255,0) 45%),#161e19;
  -webkit-backdrop-filter:none;backdrop-filter:none;
  box-shadow:inset 0 0 0 1px rgba(255,255,255,.13),0 0 0 1px rgba(0,0,0,.6),0 24px 70px rgba(0,0,0,.8);}
/* Every popup is the lobby card's size: same scale unit (#s-lobby's
   clamp), same width formula (.card), so the popup and everything in it
   render at the card's scale. Height fits the content like the card does,
   capped to the screen; long content scrolls in .about-body. Overrides the
   fixed 48-unit height, the portrait-phone height and Play History's
   wider box. */
.about-modal{--ui-vmin:clamp(6px,1vmin,10px);width:min(calc(var(--ui-vmin) * 61.11),96vw);height:auto;
  max-width:96vw;max-height:calc(100vh - 48px);}
#playhistory-modal{width:min(calc(var(--ui-vmin) * 61.11),96vw);}
/* Never shorter than the lobby card in its profile / Quick Match / Room
   List state. That card measures 27.73 units + 46.4px (the px part is the
   card's own fixed spacing — header-row margin, gaps, borders), checked
   against the real card at --ui-vmin 6/8/9/10 to within 1px. The short
   confirm dialogs centre their contents in the taller box. */
.about-modal{min-height:min(calc(var(--ui-vmin) * 27.73 + 46.4px),calc(100vh - 48px));}
.leave-modal{justify-content:center;}
.leave-modal .about-body{flex:0 0 auto;}
/* Behind a popup: a heavy dim plus a soft blur, so the lobby card / logo
   behind it fade to a quiet haze instead of competing with the popup. */
.about-backdrop{background:rgba(0,0,0,.7);-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);}
.about-title{color:var(--gl);}
.about-section{border-top-color:rgba(255,255,255,.07);}
.about-section-title{font-family:'Noto Sans JP',sans-serif;font-weight:700;text-transform:uppercase;letter-spacing:.2em;color:rgba(245,237,216,.5);}
#settings-panel{background:linear-gradient(90deg,var(--glass-edge) 0 1px,transparent 1px),#0d1410;}
.sp-title{border-bottom-color:rgba(255,255,255,.08);}

/* ── ✕ / ← ── glass icon tiles, like the chat bar's buttons. */
.about-close,.about-back{display:flex;align-items:center;justify-content:center;padding:0;
  width:calc(var(--ui-vmin, 7.2px) * 3.6);height:calc(var(--ui-vmin, 7.2px) * 3.6);top:calc(var(--ui-vmin, 7.2px) * 1.2);
  border-radius:calc(var(--ui-vmin, 7.2px) * .4);background:rgba(255,255,255,.06);color:rgba(245,237,216,.75);
  font-size:calc(var(--ui-vmin, 7.2px) * 1.6);line-height:1;z-index:3;transition:background .15s,color .15s;}
.about-close{right:calc(var(--ui-vmin, 7.2px) * 1.2);}
.about-back{left:calc(var(--ui-vmin, 7.2px) * 1.2);}
.about-close:hover,.about-back:hover{background:rgba(255,255,255,.13);color:var(--gl);}

/* ── Pickers & tiles ── glass tile; the chosen one is lit gold. */
.mode-btn,.theme-btn,.icon-btn,.profile-card,.room-card{background:var(--glass-tile);border:0;border-radius:calc(var(--ui-vmin, 7.2px) * .4);
  box-shadow:inset 0 0 0 1px rgba(255,255,255,.06);}
.mode-btn{color:rgba(245,237,216,.7);}
.mode-btn:hover:not(.active),.icon-btn:hover,.profile-card:hover{background:var(--glass-tile-hi);color:var(--cream);}
.mode-btn.active,.theme-btn.selected,.icon-btn.active,.profile-card.active{background:var(--glass-on);box-shadow:inset 0 0 0 1px var(--glass-on-edge);color:#fff;}
.mode-btn.active .mode-sub{color:var(--gl);}
.profile-card-offline:hover{background:var(--glass-tile);}

/* ── Toggles ── rounded-square; on = gold with a dark knob (the chat send colours). */
.sp-toggle{border:0;border-radius:calc(var(--ui-vmin, 7.2px) * .4);background:var(--gl);}
.sp-toggle::after{border-radius:calc(var(--ui-vmin, 7.2px) * .25);background:#1a1206;box-shadow:none;}
.sp-toggle.off{background:rgba(255,255,255,.1);}
.sp-toggle.off::after{background:rgba(245,237,216,.5);}

/* ── Fields ── */
.ig input,.ig select,.ig textarea{background:rgba(255,255,255,.06);border:0;border-radius:calc(var(--ui-vmin, 7.2px) * .4);
  box-shadow:inset 0 0 0 1px rgba(255,255,255,.07);transition:background .2s,box-shadow .2s;}
.ig input:focus,.ig select:focus,.ig textarea:focus{background:rgba(255,255,255,.1);box-shadow:inset 0 0 0 1px rgba(240,208,128,.5);}
.ig select option{background:#141a16;color:var(--cream);}

/* ── Buttons ── the .ba slant: the button itself is the edge colour
   (--sl-bd), ::before (same slant, inset) the fill (--sl-bg), and ::after
   a ghost kanji from data-kj. clip-path clips box-shadow, so no glow. */
.bp:not(.reb-btn),.bs,.leave-actions button,.room-card-join{--sl-cut:calc(var(--ui-vmin, 7.2px) * 1.5);
  position:relative;isolation:isolate;overflow:hidden;border:0;border-radius:0;background:var(--sl-bd);color:var(--sl-fg);
  clip-path:polygon(var(--sl-cut) 0,100% 0,calc(100% - var(--sl-cut)) 100%,0 100%);transition:filter .2s;}
.bp:not(.reb-btn)::before,.bs::before,.leave-actions button::before,.room-card-join::before{content:'';position:absolute;z-index:-1;inset:1px 1.5px;
  background:var(--sl-bg);clip-path:polygon(var(--sl-cut) 0,100% 0,calc(100% - var(--sl-cut)) 100%,0 100%);}
.bp:not(.reb-btn)::after,.bs::after,.leave-actions button::after{content:attr(data-kj);position:absolute;z-index:-1;
  right:calc(var(--sl-cut) * .5);top:50%;translate:0 -50%;pointer-events:none;
  font-family:'Noto Serif JP',serif;font-weight:900;font-size:2em;letter-spacing:0;line-height:1;opacity:.14;}
/* The lobby's two half-width buttons are too narrow to centre the label
   clear of the kanji, so the label shifts left a little. */
.lobby-actions .bp[data-kj]{padding-right:calc(var(--ui-vmin) * 4);}
.bp:not(.reb-btn):hover,.bs:hover,.leave-actions button:hover,.room-card-join:hover{filter:brightness(1.15);transform:none;box-shadow:none;}
.bp:not(.reb-btn):focus-visible,.bs:focus-visible,.leave-actions button:focus-visible,.room-card-join:focus-visible{outline:none;filter:brightness(1.3);}
.bp:not(.reb-btn):disabled,.room-card-join:disabled{opacity:.4;filter:none;}
/* Primary: solid gold, dark text (= .chat-send). */
.bp:not(.reb-btn),.room-card-join{--sl-bg:var(--gl);--sl-bd:var(--gl);--sl-fg:#1a1206;font-weight:700;}
/* Secondary: glass. */
.bs,.bp.bp-glass,.leave-btn-stay{--sl-bg:rgba(30,36,32,.92);--sl-bd:rgba(255,255,255,.2);--sl-fg:rgba(245,237,216,.88);font-weight:400;}
.bs:hover{color:var(--sl-fg);}
/* Danger: red glass. */
.leave-btn-confirm,.bp.bp-danger{--sl-bg:rgba(150,24,24,.9);--sl-bd:rgba(255,120,120,.6);--sl-fg:#ffe2e2;}
/* Waiting room's Ready (#bready): gold primary; once pressed it's a green
   glass "✓ Ready" — done, not unavailable, so it isn't dimmed like other
   disabled buttons. */
.ready-btn.pressed,.ready-btn.pressed:disabled{--sl-bg:rgba(30,36,32,.92);--sl-bd:rgba(127,209,139,.6);--sl-fg:#8fe0a0;opacity:1;cursor:default;filter:none;}
.room-card-join{--sl-cut:calc(var(--ui-vmin, 7.2px) * 1);padding-left:calc(var(--ui-vmin, 7.2px) * 2.2);padding-right:calc(var(--ui-vmin, 7.2px) * 2.2);}

/* ════════════════════════════════════════════════════════════
   REPLAY VIEWER — #replay-screen (see openReplay in script.js).
   Full-screen on the popups' deep-green surface while loading; the
   replay itself plays on the real game table (.rp-tv, below). Grades
   use one colour per severity, set as --sev by a .sev-* class.
   ════════════════════════════════════════════════════════════ */
.rp-screen{--ui-vmin:clamp(6px,1vmin,10px);--u:var(--ui-vmin);display:none;position:fixed;inset:0;z-index:360;flex-direction:column;
  background:linear-gradient(115deg,#b5903f 0%,#ffe9b0 48%,#cfa74d 100%) top/100% 2px no-repeat,
    radial-gradient(120% 80% at 30% 0%,#1a2a20 0%,#0f1813 55%,#0a0f0c 100%);
  color:var(--cream);font-family:'Noto Sans JP',sans-serif;font-size:calc(var(--u) * 1.5);}
.rp-screen.visible{display:flex;}
.sev-best{--sev:#7fd18b;}.sev-good{--sev:#a9cf8e;}.sev-tip{--sev:#7fb6e0;}
.sev-inaccuracy{--sev:#e8c25c;}.sev-mistake{--sev:#f08a4b;}.sev-blunder{--sev:#e86060;}.sev-win{--sev:#f0d080;}

/* ── Header ── */
.rp-head{display:flex;align-items:center;gap:calc(var(--u) * 1.4);padding:calc(var(--u) * 1.2) calc(var(--u) * 1.6);
  border-bottom:1px solid rgba(201,168,76,.16);flex-wrap:wrap;}
.rp-icon-btn{flex:0 0 auto;display:flex;align-items:center;justify-content:center;width:calc(var(--u) * 3.8);height:calc(var(--u) * 3.8);
  border:0;border-radius:calc(var(--u) * .4);background:rgba(255,255,255,.06);color:rgba(245,237,216,.8);font-size:calc(var(--u) * 1.8);cursor:pointer;}
.rp-icon-btn:hover{background:rgba(255,255,255,.13);color:var(--gl);}
.rp-title-wrap{min-width:0;}
.rp-title{font-family:'Noto Serif JP',serif;font-weight:700;font-size:calc(var(--u) * 2.1);color:var(--gl);letter-spacing:.04em;white-space:nowrap;}
.rp-sub{font-size:calc(var(--u) * 1.2);color:rgba(245,237,216,.5);white-space:nowrap;}
.rp-rounds{margin-left:auto;display:flex;gap:calc(var(--u) * .5);overflow-x:auto;max-width:62%;padding-bottom:2px;scrollbar-width:thin;}
.rp-round-chip{position:relative;flex:0 0 auto;border:0;cursor:pointer;padding:calc(var(--u) * .6) calc(var(--u) * 1.1);
  border-radius:calc(var(--u) * .4);background:rgba(255,255,255,.05);box-shadow:inset 0 0 0 1px rgba(255,255,255,.06);
  color:rgba(245,237,216,.7);font-family:'Noto Serif JP',serif;font-weight:700;font-size:calc(var(--u) * 1.3);}
.rp-round-chip:hover{background:rgba(255,255,255,.1);color:var(--cream);}
.rp-round-chip.active{background:rgba(240,208,128,.15);box-shadow:inset 0 0 0 1px rgba(240,208,128,.55);color:#fff;}
.rp-chip-dot{position:absolute;top:calc(var(--u) * .3);right:calc(var(--u) * .3);width:calc(var(--u) * .6);height:calc(var(--u) * .6);border-radius:50%;background:#f08a4b;}

/* ── Main: table + review side panel ── */
.rp-main{flex:1;min-height:0;display:grid;grid-template-columns:minmax(0,1fr) minmax(calc(var(--u) * 34),calc(var(--u) * 46));
  gap:calc(var(--u) * 1.4);padding:calc(var(--u) * 1.4) calc(var(--u) * 1.6);}
.rp-table-wrap{display:flex;flex-direction:column;gap:calc(var(--u) * .9);min-width:0;min-height:0;}
.rp-table{flex:1;min-height:0;overflow-y:auto;display:flex;flex-direction:column;gap:calc(var(--u) * .8);padding-right:2px;}
.rp-empty{color:rgba(245,237,216,.6);padding:calc(var(--u) * 3);text-align:center;}

.rp-delta{font-weight:700;font-variant-numeric:tabular-nums;}
.rp-delta.pos{color:#8fe0a0;}.rp-delta.neg{color:#ef8a8a;}
/* Tile face: the .tmd look, with the gold edge drawn as inset shadows
   rather than borders so the SVG keeps its exact size and the You/Best
   labels (::after) can sit outside the box unclipped. */
.rp-t{position:relative;display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;border-radius:2px;
  background:linear-gradient(170deg,#fffdf4,#f8edd8);box-shadow:inset 0 0 0 1px #c8a84a,inset 0 -2px 0 #7a5c10;}
.rp-t svg{display:block;}
/* The player's own hand at a discard decision: their tile (red) vs the
   recommended one (green), each with a small label above it. */
.rp-mark-you,.rp-mark-best{translate:0 calc(var(--u) * -.6);}
.rp-mark-you{outline:2px solid #e86060;outline-offset:1px;}
.rp-mark-best{outline:2px solid #7fd18b;outline-offset:1px;}
.rp-mark-you::after,.rp-mark-best::after{position:absolute;left:50%;bottom:100%;translate:-50% calc(var(--u) * -.35);white-space:nowrap;
  font-size:calc(var(--u) * 1);font-weight:700;letter-spacing:.06em;padding:0 calc(var(--u) * .4);color:#111;}
.rp-mark-you::after{content:"You";background:#e86060;}
.rp-mark-best::after{content:"Best";background:#7fd18b;}

/* ── Side panel ── */
.rp-side{display:flex;flex-direction:column;min-height:0;min-width:0;background:#141a16;box-shadow:inset 0 0 0 1px rgba(255,255,255,.09);}
.rp-tabs{display:flex;border-bottom:1px solid rgba(255,255,255,.08);}
.rp-tab{flex:1;border:0;background:none;cursor:pointer;padding:calc(var(--u) * 1.1) calc(var(--u) * .6);color:rgba(245,237,216,.55);
  font-family:'Noto Sans JP',sans-serif;font-weight:700;font-size:calc(var(--u) * 1.2);letter-spacing:.14em;text-transform:uppercase;
  box-shadow:inset 0 -2px 0 transparent;}
.rp-tab:hover{color:var(--cream);}
.rp-tab.active{color:var(--gl);box-shadow:inset 0 -2px 0 var(--gl);}
.rp-panel{flex:1;min-height:0;overflow-y:auto;padding:calc(var(--u) * 1.4);display:flex;flex-direction:column;gap:calc(var(--u) * 1.2);}
.rp-panel[hidden]{display:none;}
.rp-event{font-size:calc(var(--u) * 1.45);line-height:1.6;color:rgba(245,237,216,.85);}
.rp-t-inline{vertical-align:middle;margin:0 1px;translate:0 -1px;}
.rp-hint{margin:0;font-size:calc(var(--u) * 1.25);line-height:1.6;color:rgba(245,237,216,.55);}
.rp-link{border:0;background:none;padding:0;cursor:pointer;color:var(--gl);font:inherit;text-decoration:underline;text-underline-offset:2px;}
.rp-review{padding:calc(var(--u) * 1.1) calc(var(--u) * 1.2);border-left:calc(var(--u) * .35) solid var(--sev);
  background:linear-gradient(90deg,color-mix(in srgb,var(--sev) 14%,transparent),transparent 80%);
  display:flex;flex-direction:column;gap:calc(var(--u) * .9);}
.rp-review-head{display:flex;align-items:center;gap:calc(var(--u) * .8);}
.rp-sev-badge{display:flex;align-items:center;justify-content:center;flex:0 0 auto;width:calc(var(--u) * 2.6);height:calc(var(--u) * 2.6);border-radius:50%;
  background:var(--sev);color:#111;font-weight:900;font-size:calc(var(--u) * 1.2);}
.rp-review-title{font-weight:700;color:var(--cream);}
.rp-sev-label{margin-left:auto;font-weight:700;letter-spacing:.12em;text-transform:uppercase;font-size:calc(var(--u) * 1.15);color:var(--sev);}
.rp-sev-desc{margin:calc(var(--u) * -.4) 0 0;font-size:calc(var(--u) * 1.15);line-height:1.5;color:var(--sev);opacity:.85;}
.rp-review-text{margin:0;line-height:1.65;color:rgba(245,237,216,.88);}
.rp-cands{width:100%;border-collapse:collapse;font-size:calc(var(--u) * 1.2);}
.rp-cands th{text-align:left;font-weight:400;color:rgba(245,237,216,.45);font-size:calc(var(--u) * 1.05);letter-spacing:.08em;text-transform:uppercase;
  padding:0 calc(var(--u) * .5) calc(var(--u) * .4);}
.rp-cands td{padding:calc(var(--u) * .45) calc(var(--u) * .5);border-top:1px solid rgba(255,255,255,.06);vertical-align:middle;color:rgba(245,237,216,.8);}
.rp-cands tr.is-best td{background:rgba(127,209,139,.1);}
.rp-cands tr.is-you:not(.is-best) td{background:rgba(232,96,96,.1);}
.rp-cand-tile{white-space:nowrap;}
.rp-tag{display:inline-block;margin-left:calc(var(--u) * .4);padding:0 calc(var(--u) * .4);font-size:calc(var(--u) * .95);font-weight:700;color:#111;vertical-align:middle;}
.rp-tag.best{background:#7fd18b;}.rp-tag.you{background:#e86060;}
.rp-accepts{font-variant-numeric:tabular-nums;}
.rp-ukt{display:inline-flex;gap:1px;vertical-align:middle;margin-left:calc(var(--u) * .3);flex-wrap:wrap;}
.rp-risk.hi{color:#ef8a8a;}.rp-risk.mid{color:#e8c25c;}.rp-risk.lo{color:#8fe0a0;}
.rp-round-sum{padding:calc(var(--u) * 1) calc(var(--u) * 1.2);background:rgba(255,255,255,.04);box-shadow:inset 0 0 0 1px rgba(255,255,255,.07);
  display:flex;flex-direction:column;gap:calc(var(--u) * .7);}
.rp-round-sum-head{display:flex;justify-content:space-between;font-weight:700;color:var(--cream);}
.rp-chips{display:flex;flex-wrap:wrap;gap:calc(var(--u) * .5);}
.rp-chip{display:inline-flex;align-items:center;gap:calc(var(--u) * .3);padding:calc(var(--u) * .2) calc(var(--u) * .7);font-size:calc(var(--u) * 1.1);
  white-space:nowrap;color:var(--sev);background:color-mix(in srgb,var(--sev) 12%,transparent);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--sev) 45%,transparent);}
.rp-ok{color:#8fe0a0;}
.rp-result-card{margin:0;}

/* ── Summary tab ── */
.rp-sum-top{display:flex;align-items:center;gap:calc(var(--u) * 1.6);}
.rp-ring{position:relative;flex:0 0 auto;width:calc(var(--u) * 9);height:calc(var(--u) * 9);border-radius:50%;
  background:conic-gradient(var(--gl) var(--deg),rgba(255,255,255,.08) 0);display:flex;flex-direction:column;align-items:center;justify-content:center;}
.rp-ring::before{content:"";position:absolute;inset:calc(var(--u) * .8);border-radius:50%;background:#141a16;}
.rp-ring-v,.rp-ring-k{position:relative;}
.rp-ring-v{font-family:'Cinzel Decorative',serif;font-weight:700;font-size:calc(var(--u) * 2.8);color:var(--gl);line-height:1;}
.rp-ring-k{font-size:calc(var(--u) * 1);letter-spacing:.16em;text-transform:uppercase;color:rgba(245,237,216,.5);}
.rp-verdict{font-family:'Cinzel Decorative',serif;font-weight:700;font-size:calc(var(--u) * 1.9);color:var(--cream);margin-bottom:calc(var(--u) * .4);}
.rp-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:calc(var(--u) * .6);}
.rp-stat{display:flex;flex-direction:column;align-items:center;padding:calc(var(--u) * .8) calc(var(--u) * .4);background:rgba(255,255,255,.04);box-shadow:inset 0 0 0 1px rgba(255,255,255,.06);}
.rp-stat-v{font-weight:700;font-size:calc(var(--u) * 1.7);color:var(--cream);font-variant-numeric:tabular-nums;}
.rp-stat-k{font-size:calc(var(--u) * 1);letter-spacing:.12em;text-transform:uppercase;color:rgba(245,237,216,.5);}
.rp-sec{margin-top:calc(var(--u) * .4);}
/* Grade legend under the tally: a chip and one line per grade. */
.rp-legend{font-size:calc(var(--u) * 1.2);color:rgba(245,237,216,.7);}
.rp-legend summary{cursor:pointer;width:max-content;color:var(--gl);text-decoration:underline;text-underline-offset:.2em;}
.rp-legend summary:focus-visible{outline:2px solid var(--gl);outline-offset:2px;}
.rp-legend dl{margin:calc(var(--u) * .8) 0 0;display:flex;flex-direction:column;gap:calc(var(--u) * .6);}
.rp-legend-row{display:grid;grid-template-columns:calc(var(--u) * 11) 1fr;align-items:baseline;gap:calc(var(--u) * .8);}
.rp-legend dt,.rp-legend dd{margin:0;}
.rp-legend dd{line-height:1.5;}
.rp-moments{display:flex;flex-direction:column;gap:calc(var(--u) * .4);}
.rp-moment{display:flex;flex-wrap:wrap;align-items:center;gap:calc(var(--u) * .3) calc(var(--u) * .8);text-align:left;border:0;cursor:pointer;
  padding:calc(var(--u) * .7) calc(var(--u) * .9);background:rgba(255,255,255,.04);color:var(--cream);font:inherit;font-size:calc(var(--u) * 1.25);}
.rp-moment:hover{background:rgba(255,255,255,.09);}
.rp-moment-label{font-weight:700;}
.rp-moment-txt{flex:1 1 100%;color:rgba(245,237,216,.65);line-height:1.6;}
.rp-round-table{width:100%;border-collapse:collapse;font-size:calc(var(--u) * 1.25);}
.rp-round-table tr{cursor:pointer;}
.rp-round-table tr:hover td{background:rgba(255,255,255,.05);}
.rp-round-table td{padding:calc(var(--u) * .55) calc(var(--u) * .5);border-top:1px solid rgba(255,255,255,.06);}
.rp-round-table td:first-child{font-family:'Noto Serif JP',serif;font-weight:700;color:var(--gold);white-space:nowrap;}
.rp-round-table td:nth-child(3),.rp-round-table td:nth-child(4){text-align:right;}
.rp-foot-note{margin:0;font-size:calc(var(--u) * 1.1);line-height:1.6;color:rgba(245,237,216,.4);}

/* ── Footer: timeline + transport ── */
.rp-foot{display:flex;flex-direction:column;gap:calc(var(--u) * .9);padding:calc(var(--u) * 1) calc(var(--u) * 1.6) calc(var(--u) * 1.3);
  border-top:1px solid rgba(201,168,76,.16);background:rgba(0,0,0,.25);}
.rp-timeline{cursor:pointer;padding:calc(var(--u) * .6) 0;}
.rp-track{position:relative;height:calc(var(--u) * .8);background:rgba(255,255,255,.08);}
.rp-fill{position:absolute;left:0;top:0;bottom:0;background:rgba(240,208,128,.28);}
.rp-mark{position:absolute;top:50%;width:calc(var(--u) * 1.3);height:calc(var(--u) * 1.3);translate:-50% -50%;border-radius:50%;
  border:0;padding:0;cursor:pointer;background:var(--sev);box-shadow:0 0 0 2px #0d1410;}
.rp-mark.sev-best,.rp-mark.sev-good{width:calc(var(--u) * .7);height:calc(var(--u) * .7);opacity:.6;}
.rp-mark:hover{scale:1.3;}
.rp-head-dot{position:absolute;top:calc(var(--u) * -.7);bottom:calc(var(--u) * -.7);width:2px;translate:-1px 0;background:var(--gl);pointer-events:none;}
.rp-controls{display:flex;align-items:center;justify-content:center;flex-wrap:wrap;gap:calc(var(--u) * .6);}
.rp-ctl{min-width:calc(var(--u) * 4.6);height:calc(var(--u) * 4);padding:0 calc(var(--u) * .9);border:0;cursor:pointer;
  border-radius:calc(var(--u) * .4);background:rgba(255,255,255,.06);box-shadow:inset 0 0 0 1px rgba(255,255,255,.08);
  color:rgba(245,237,216,.85);font-size:calc(var(--u) * 1.7);line-height:1;white-space:nowrap;}
.rp-ctl:hover{background:rgba(255,255,255,.13);color:var(--gl);}
.rp-ctl:focus-visible,.rp-tab:focus-visible,.rp-round-chip:focus-visible,.rp-icon-btn:focus-visible{outline:2px solid var(--gl);outline-offset:2px;}
.rp-play{background:var(--gl);color:#1a1206;box-shadow:none;min-width:calc(var(--u) * 6);font-size:calc(var(--u) * 1.5);}
.rp-play:hover{background:#ffe3a0;color:#1a1206;}
.rp-pos{min-width:calc(var(--u) * 11);text-align:center;font-size:calc(var(--u) * 1.2);color:rgba(245,237,216,.55);font-variant-numeric:tabular-nums;}
.rp-toggle{display:flex;align-items:center;gap:calc(var(--u) * .5);font-size:calc(var(--u) * 1.2);color:rgba(245,237,216,.7);cursor:pointer;}
.rp-toggle input{accent-color:var(--gl);width:calc(var(--u) * 1.6);height:calc(var(--u) * 1.6);}

/* ── Narrow / portrait: one column, the whole body scrolls ── */
@media (max-width:820px),(orientation:portrait){
  .rp-head{padding:calc(var(--u) * 1) calc(var(--u) * 1.2);gap:calc(var(--u) * 1);}
  .rp-rounds{order:3;margin-left:0;max-width:100%;flex:1 1 100%;}
  .rp-main{display:flex;flex-direction:column;overflow-y:auto;padding:calc(var(--u) * 1) calc(var(--u) * 1.2);}
  .rp-table-wrap,.rp-table{flex:0 0 auto;overflow:visible;}
  .rp-side{flex:0 0 auto;}
  .rp-panel{overflow:visible;}
  .rp-foot{padding:calc(var(--u) * .8) calc(var(--u) * 1.2) calc(var(--u) * 1);}
  .rp-ctl{min-width:calc(var(--u) * 4.2);}
  .rp-pos{min-width:0;}
}

/* ── Header: review-panel toggle ── */
.rp-side-btn{display:none;}
.rp-side-btn[aria-pressed="true"]{color:var(--gl);}

/* ── Table view (.rp-tv, body.rp-live) ──
   The replay on the real game table: #s-game shows underneath, raised
   above the menus, and the replay screen goes see-through except for its
   header (in the topbar's place), the review panel (docked right) and
   the transport bar (in the chat bar's place). .table gives up the
   panel's and bar's room, and --table-size/--felt-size are re-derived
   from what's left the same way :root derives them from the viewport —
   both redefined, since --felt-size is resolved where it's declared. */
body.rp-live{--rp-u:clamp(6px,1vmin,10px);--rp-foot-h:calc(var(--rp-u) * 9.6);--rp-sheet-h:0px;
  --rp-side-w:clamp(calc(var(--rp-u) * 34),27vw,calc(var(--rp-u) * 46));
  --table-size:min(calc((100vw - var(--rp-side-w)) * 1.1),calc((100vh - 36px - var(--rp-foot-h) - var(--rp-sheet-h)) * 1.1));
  --felt-size:calc(var(--table-size) * .86);}
body.rp-live.rp-side-off{--rp-side-w:0px;}
body.rp-live #s-game{z-index:350;pointer-events:none;background:#0a0f0c;}
body.rp-live #s-game .topbar,body.rp-live #chat-bar,body.rp-live #actbar,body.rp-live #game-loading{display:none!important;}
body.rp-live .table{right:var(--rp-side-w);bottom:calc(var(--rp-foot-h) + var(--rp-sheet-h));}
body.rp-live #dora-panel{z-index:355;}
/* Your discard (red) and the stronger one (green), lifted out of the hand. */
body.rp-live .tlg.rp-mark-you,body.rp-live .tlg.rp-mark-best{translate:none;transform:translateY(-1.4vmin);outline-width:.35vmin;outline-offset:0;z-index:25;}
body.rp-live .tlg.rp-mark-you{box-shadow:0 0 1.6vmin rgba(232,96,96,.75);}
body.rp-live .tlg.rp-mark-best{box-shadow:0 0 1.6vmin rgba(127,209,139,.75);}
/* Show all hands: each opponent's face-up row turned to face its owner
   (tops toward the table centre, like their melds) — the 180deg those
   rows carry for the face-down .tsm tile-tops is undone. */
#hdl.rp-open-row,#hdt.rp-open-row{transform:none;}
#hdr.rp-open-row{transform:rotate(180deg);}

.rp-screen.rp-tv{background:none;pointer-events:none;}
.rp-tv .rp-head,.rp-tv .rp-main,.rp-tv .rp-foot{pointer-events:auto;}
.rp-tv .rp-head{--u:clamp(6px,1vmin,7.5px);position:fixed;top:0;left:0;right:0;height:36px;box-sizing:border-box;flex-wrap:nowrap;
  padding:0 calc(var(--u) * 1.4);gap:calc(var(--u) * 1.2);border:0;
  background:linear-gradient(#0d110e,#050806);box-shadow:inset 0 -1px 0 rgba(201,168,76,.65);}
.rp-tv .rp-icon-btn{width:calc(var(--u) * 3.6);height:calc(var(--u) * 3.6);}
.rp-tv .rp-side-btn{display:flex;}
.rp-tv .rp-title{font-size:calc(var(--u) * 1.944);}
.rp-tv .rp-sub{display:none;}
.rp-tv .rp-rounds{flex:0 1 auto;min-width:0;max-width:none;}
.rp-dora{display:none;}
.rp-tv .rp-main{position:fixed;top:36px;right:0;bottom:var(--rp-foot-h);width:var(--rp-side-w);display:flex;flex-direction:column;gap:0;padding:0;
  background:rgba(13,17,14,.97);box-shadow:inset 1px 0 0 rgba(201,168,76,.4);}
.rp-tv .rp-table-wrap{display:none;}
.rp-tv .rp-side{flex:1;background:none;box-shadow:none;}
/* The game's dora panel, docked at the top of the column instead of
   floating over the table's corner (the table has moved left for the panel). */
.rp-tv .rp-dora{display:flex;justify-content:center;padding:calc(var(--u) * 1) calc(var(--u) * 1) 0;}
.rp-dora #dora-panel{position:static;max-height:none;box-shadow:none;}
/* Panel put away: just the dora card stays, top right. */
body.rp-side-off .rp-tv .rp-main{bottom:auto;width:auto;background:none;box-shadow:none;}
body.rp-side-off .rp-tv .rp-side{display:none;}
.rp-tv .rp-foot{position:fixed;left:0;right:0;bottom:0;height:var(--rp-foot-h);box-sizing:border-box;justify-content:center;border:0;
  background:linear-gradient(#0d110e,#050806);box-shadow:inset 0 1px 0 rgba(201,168,76,.65);}

/* Portrait: the review panel becomes a sheet between the table and the
   transport bar, taking whatever height a square table leaves over. */
@media (orientation:portrait){
  body.rp-live{--rp-side-w:0px;--rp-sheet-h:clamp(24vh,calc(100vh - 36px - var(--rp-foot-h) - 100vw),55vh);}
  body.rp-live.rp-side-off{--rp-sheet-h:0px;}
  .rp-tv .rp-title{display:none;}
  .rp-tv .rp-rounds{order:0;flex:1 1 auto;margin-left:0;}
  .rp-tv .rp-main{top:auto;left:0;width:auto;height:var(--rp-sheet-h);overflow:hidden;flex-direction:row;box-shadow:inset 0 1px 0 rgba(201,168,76,.4);}
  .rp-tv .rp-dora{padding:calc(var(--u) * 1) 0 calc(var(--u) * 1) calc(var(--u) * 1);align-items:flex-start;}
  .rp-tv .rp-side{flex:1 1 auto;min-height:0;min-width:0;}
  /* Panel put away: the dora card goes top left, above the table, as in a game. */
  body.rp-side-off .rp-tv .rp-main{top:36px;bottom:auto;right:auto;height:auto;}
  .rp-tv .rp-panel{overflow-y:auto;}
}

/* ── Play History entry points ── */
.ph-replay-btn{flex:0 0 auto;display:inline-flex;align-items:center;gap:calc(var(--ui-vmin, 7.2px) * .5);border:0;cursor:pointer;
  padding:calc(var(--ui-vmin, 7.2px) * .6) calc(var(--ui-vmin, 7.2px) * 1.6);font-family:'Noto Sans JP',sans-serif;font-weight:700;
  font-size:calc(var(--ui-vmin, 7.2px) * 1.2);color:#1a1206;background:var(--gl);
  clip-path:polygon(calc(var(--ui-vmin, 7.2px) * .9) 0,100% 0,calc(100% - var(--ui-vmin, 7.2px) * .9) 100%,0 100%);transition:filter .2s;}
.ph-replay-btn:hover{filter:brightness(1.12);}
.ph-replay-btn:focus-visible{outline:none;filter:brightness(1.3);}
.ph-replay-btn:disabled{cursor:default;background:rgba(255,255,255,.1);color:rgba(245,237,216,.35);filter:none;}
.ph-replay-ic{font-size:.8em;}
.ph-round-watch{border:0;background:rgba(240,208,128,.12);box-shadow:inset 0 0 0 1px rgba(240,208,128,.4);color:var(--gl);cursor:pointer;
  padding:calc(var(--ui-vmin, 7.2px) * .2) calc(var(--ui-vmin, 7.2px) * .8);font-size:calc(var(--ui-vmin, 7.2px) * 1.05);font-weight:700;white-space:nowrap;}
.ph-round-watch:hover{background:rgba(240,208,128,.22);}
