/* ════════════════════════════════════════════════════════════
   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;
  /* 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: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%);}
.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;}
#s-start{flex-direction:column;align-items:center;justify-content:center;gap:32px;}
.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,.35);border-radius:16px;
  padding:calc(var(--ui-vmin) * 2.5) calc(var(--ui-vmin) * 6.389);
  /* 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;}
.card::before{content:'';position:absolute;top:0;left:0;right:0;height:2px;background:linear-gradient(90deg,transparent,var(--gold),transparent);}
.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 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:8px;
  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:8px;
  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:10px;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:10px;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:10px;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:8px;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{width:100%;padding:calc(var(--ui-vmin) * 1.667);background:linear-gradient(135deg,#0a5a2a,#1a8840);border:2px solid #4eff9a;border-radius:10px;color:#c0ffd8;font-size:calc(var(--ui-vmin) * 1.944);font-family:'Cinzel Decorative',serif;letter-spacing:calc(var(--ui-vmin) * .417);cursor:pointer;transition:all .25s;margin-top:12px;position:relative;overflow:hidden;}
.ready-btn:hover{transform:translateY(-2px);box-shadow:0 0 20px rgba(78,255,154,.4);}
.ready-btn.pressed{opacity:.6;cursor:default;transform:none;}
.ready-btn.pressed::after{content:'✓ Ready!';position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:linear-gradient(135deg,#0a5a2a,#1a8840);font-size:calc(var(--ui-vmin) * 1.944);letter-spacing:calc(var(--ui-vmin) * .417);}

/* ════════════════════════════════════════════════════════════
   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;background:rgba(3,10,5,.97);border-bottom:1px solid rgba(201,168,76,.18);display:flex;align-items:center;justify-content:space-between;padding:0 10px;z-index:50;}
.tb-title{font-family:'Noto Serif JP',serif;font-size:calc(var(--ui-vmin) * 1.944);color:var(--gold);letter-spacing:calc(var(--ui-vmin) * .278);}
.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:#ffdd44;font-weight:700;animation:pulse 1.5s infinite;}
.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. The centre score panel's top strip
   (.ctr-score-top) is deliberately NOT included here: it's one of four
   ridge-bordered segments that visually close into a single rectangular
   frame (see .ctr/.ctr-score), not a per-player element on its own —
   hiding the whole strip leaves that frame open on one side. Its content
   (the score value and riichi-stick icon) IS hidden below, via
   visibility:hidden rather than display:none, so the frame segment stays
   in place but reads as empty 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 ── */
.wood-table{
  width:var(--table-size);
  height:var(--table-size);
  background:radial-gradient(ellipse at 40% 30%,#462727,#241006 50%,#1a0a04);
  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) ── */
.felt{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
  width:var(--felt-size);
  height:var(--felt-size);
  background:radial-gradient(ellipse at 40% 35%,#27774a,#155230 60%,#0e3b24);
  border:none;
  box-shadow:inset 0 0 60px rgba(0,0,0,.4);}

/* ── Centre info panel (round/wind/scores) ── */
.ctr{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
  z-index:1;pointer-events:none;
  background:#1a1a1a;
  width:min(26vmin,26%);height:min(26vmin,26%);
  display:flex;flex-direction:column;align-items:center;justify-content:center;
  padding:0;overflow:hidden;
  border-radius:1px;}
/* Score strip rows — double line separator using border + box-shadow.
   `solid`, not `ridge` — ridge fakes a bevel by splitting the border into
   algorithmically lightened/darkened halves, which is barely visible at
   a sub-2px width on a phone (reads as a flat line, the intended look)
   but becomes an obviously two-toned, visibly LIGHTER band once this
   vmin-scaled width grows past a few px on a large/4K screen. Same color
   and width, just without the fake-3D split, so it reads as the same
   flat rgb(72,72,72) at every screen size instead of correct-looking on
   mobile and washed-out on desktop. */
.ctr-score{width:100%;height:20%;text-align:center;
  border: solid 0.4vmin rgb(72, 72, 72);background:#1a1a1a;}
.ctr-mid{display:flex;align-items:center;justify-content:center;
  width:calc(min(26vmin,26%) - 2 * min(5vmin,38px));
  height:calc(min(26vmin,26%) - 2 * min(5vmin,38px));
  flex-shrink:0;position:relative;box-sizing:border-box;}
/* Centre info */
.ctr-score-val{display:flex;font-size:2.1vmin;font-weight:700;align-items:center;
  color:rgba(240,208,128,.9);letter-spacing:0;justify-content:center;padding-top:2%}
.ctr-info{display:flex;flex-direction:column;align-items:center;flex-shrink:0;}
.cw{font-family:'Noto Serif JP',serif;font-weight:900;
  color:#f0d080;line-height:1;position:relative;z-index:1;
  text-shadow:0 0 14px rgba(201,168,76,.6),0 0 4px rgba(0,0,0,.8);
  display:flex;flex-direction:row;align-items:baseline;gap:3px;}
#cw-wind{font-size:clamp(28px,5vmin,5vmin);}
#cw-kyoku{font-size:clamp(12px,2vmin,2vmin);opacity:0.75;letter-spacing:1px;}
.ci{font-size:clamp(7px,1.5vmin,15px);letter-spacing:2px;color:rgba(245,237,216,.35);
  margin-top:2px;position:relative;z-index:1;}
.cwl{font-size:clamp(9px,1.8vmin,15px);color:#2a9e67;font-weight:700;
  margin-top:1px;position:relative;z-index:1;}
/* ── Dora indicator strip ── */
.dora-slot{width:var(--tlg-w,46px);height:var(--tlg-h,64px);border-radius:5px;border:1px dashed rgba(201,168,76,.25);
  background:rgba(0,0,0,.3);display:flex;align-items:center;justify-content:center;}
.dora-slot.filled{border:none;background:none;padding:0;}
/* 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:3px;
  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
   ════════════════════════════════════════════════════════════ */
.dz{position:absolute;display:flex;flex-direction:column;;pointer-events:none;}
.dz-row{display:flex;flex-direction:row;}

/* H = min(13vmin,105px) = panel half-size */

/* H = min(13vmin,105px) — panel half-size. */

/* BOTTOM: top-left of zone at panel bottom-left. No rotation. */
.dzb{top:calc(50% + calc(min(26vmin,26%) / 2));left:calc(50% - calc(min(26vmin,26%) / 2));}

/* 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% - calc(min(26vmin,26%) / 2));
  left:calc(50% + calc(min(26vmin,26%) / 2));
  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% - calc(min(26vmin,26%) / 2));
  top:calc(50% - calc(min(26vmin,26%) / 2));
  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% + calc(min(26vmin,26%) / 2));
  left:calc(50% + calc(min(26vmin,26%) / 2));
  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. */
  padding:clamp(4px,calc(var(--table-size) * 0.007273),13px) clamp(10px,calc(var(--table-size) * 0.018182),32px) clamp(4px,calc(var(--table-size) * 0.007273),13px) clamp(4px,calc(var(--table-size) * 0.007273),13px);display:flex;align-items:center;gap:clamp(5px,calc(var(--table-size) * 0.01455),12px);
  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);}
.np.active{background:#222;border-color:#ffdd44;
  box-shadow:0 0 0 2px rgba(255,221,68,.3),0 0 16px rgba(255,221,68,.5);animation:glow 1.6s ease-in-out infinite;}
@keyframes glow{0%,100%{box-shadow:0 0 0 2px rgba(255,221,68,.3),0 0 14px rgba(255,221,68,.4);}50%{box-shadow:0 0 0 2px rgba(255,221,68,.55),0 0 28px rgba(255,221,68,.7);}}
/* wb/pname/ptitle/btag/tenpai/furiten-badge 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/tenpai/furiten-badge 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;
  font-size:clamp(17px,calc(var(--table-size) * 0.02727),48px);width:clamp(22px,calc(var(--table-size) * 0.03636),64px);height:clamp(22px,calc(var(--table-size) * 0.03636),64px);}
/* 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 lowered 13px→11px — still comfortably legible (this is a
   short name/label, read at a glance, not body copy) and was the single
   biggest remaining contributor to the plate staying oversized on an
   actual phone even after the padding/icon floors above came down. */
.pname{font-size:clamp(11px,calc(var(--table-size) * 0.016364),29px);font-weight:700;color:#fff;
  max-width:calc(var(--felt-size) * 0.17);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. */
.ptitle{font-size:clamp(9px,calc(var(--table-size) * 0.010909),19px);color:var(--gold);opacity:.8;font-family:'Cinzel Decorative',serif;
  max-width:calc(var(--felt-size) * 0.17);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
/* 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{padding:clamp(3px,calc(var(--table-size) * 0.003636),6px);border-radius:clamp(4px,calc(var(--table-size) * 0.004545),8px);background:linear-gradient(135deg,#6a0f0f,#c02020);color:#fff;font-size:clamp(9px,calc(var(--table-size) * 0.010909),19px);letter-spacing:1px;font-family:'Cinzel Decorative',serif;
  position:absolute;bottom:calc(var(--tlg-h) + 8vmin);left:15vmin;z-index:30;}
.furiten-badge{padding:clamp(3px,calc(var(--table-size) * 0.003636),6px);border-radius:clamp(4px,calc(var(--table-size) * 0.004545),8px);background:linear-gradient(135deg,#444,#222);color:#f88;font-size:clamp(9px,calc(var(--table-size) * 0.010909),19px);letter-spacing:1px;font-family:'Cinzel Decorative',serif;
  position:absolute;bottom:calc(var(--tlg-h) + 8vmin);left:calc(21vmin);z-index:31;}

/* ════════════════════════════════════════════════════════════
   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;}
.tlg.sel{transform:translateY(-1vmin);box-shadow:0 0 0 0.2vmin #00ffaa,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;}
.tsm{
  display:inline-flex;
  width:var(--tsm-w,16px);height:var(--tsm-h,22px);border-radius:0.4vmin;
  background:linear-gradient(145deg,#1a5c70,#0e3a48);
  border:0.1vmin solid #3a90aa;
  flex-shrink:0;position:relative;}
.tsm::before{content:'';position:absolute;inset:2px;border:0.1vmin solid rgba(255,255,255,.12);border-radius:0.3vmin;}
.tsm.rot{width:var(--tsm-h,22px);height:var(--tsm-w,16px);}
.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;}

/* ════════════════════════════════════════════════════════════
   ACTION BAR & BUTTONS
   ════════════════════════════════════════════════════════════ */
.actbar{display:flex;align-items:center;}
/* Floating action overlay — appears above the hand */
.act-overlay{display:flex;gap:1vmin;align-items:center;
  z-index:40;pointer-events:none;flex-wrap:nowrap;flex-shrink:0;}
.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%;}
.ba{padding:0 clamp(10px,1vmin,20px);height:clamp(28px,4vmin,64px);border:none;border-radius:clamp(4px,0.6vmin,10px);
  font-family:'Cinzel Decorative',serif;font-size:clamp(11px,1.6vmin,26px);
  letter-spacing:0.1vmin;cursor:pointer;transition:all .15s;font-weight:700;white-space:nowrap;
  flex-shrink:0;box-shadow:0 0.4vmin 1.4vmin rgba(0,0,0,.6);}
.ba:hover{filter:brightness(1.2);transform:translateY(--0.3vmin);box-shadow:0 0.6vmin 2vmin rgba(0,0,0,.7);}
.ba:active{transform:translateY(0);}
.ba:disabled{opacity:.3;cursor:default;transform:none;filter:none;}
.hint{display:none;}
.bdis{background:#8a1818;color:#ffd8d8;border:0.2vmin solid #d44;}
.btsu{background:#8a6500;color:#fff8d0;border:0.2vmin solid #f0d060;font-weight:900;}
.brii{background:#1e1e8a;color:#d0d0ff;border:0.2vmin solid #77f;}
.bron{background:#aa1010;color:#fff;border:0.2vmin solid #f66;box-shadow:0 0 1vmin rgba(220,50,50,.6);font-weight:900;}
.bchi{background:#145a14;color:#c0ffc0;border:0.2vmin solid #4d4;}
.bpon{background:#6a3800;color:#ffe0b0;border:0.2vmin solid #ca0;}
.bkan{background:linear-gradient(135deg,#3a0a5a,#6a1a9a);color:#e8d0ff;border:0.2vmin solid #a060d0;}
/* Matches .call-banner[data-type="kita"]'s accent colours, same pairing
   convention as .bpon/.bchi/.bkan/.brii all matching their own banners. */
.bkita{background:linear-gradient(135deg,#0a3a45,#0a6a80);color:#b0f0ff;border:0.2vmin solid #20b0c0;}
.bpas{background:rgba(40,40,40,.95);color:rgba(245,237,216,.7);border:0.2vmin solid rgba(255,255,255,.2);}
/* Chonbo (rules.chonbo) "attempt" declare — deliberately muted/dashed
   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{background:rgba(90,30,10,.55);color:#ffb080;border:0.2vmin dashed rgba(255,128,64,.6);opacity:.8;}
.bchonbo:hover{opacity:1;}
.declare-picker-confirm{background:linear-gradient(135deg,#5a1a0a,#9a3a1a);color:#ffd0b0;border:0.2vmin solid #ff8040;}

/* ════════════════════════════════════════════════════════════
   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. */
.chat-bar{--ui-vmin:clamp(6px,1vmin,10px);
  position:fixed;bottom:20px;right:20px;
  display:none;flex-direction:row;align-items:center;gap:5px;
  background:rgba(3,12,6,.95);border:1px solid rgba(201,168,76,.25);
  border-radius:10px;padding:calc(var(--ui-vmin) * .694) calc(var(--ui-vmin) * 1.111);z-index:65;
  box-shadow:0 4px 16px rgba(0,0,0,.6);}
.chat-input{width:calc(var(--ui-vmin) * 22.22);padding:calc(var(--ui-vmin) * .694) calc(var(--ui-vmin) * 1.111);background:rgba(0,0,0,.45);
  border:1px solid rgba(201,168,76,.2);border-radius:7px;
  color:var(--cream);font-size:calc(var(--ui-vmin) * 1.667);outline:none;font-family:'Noto Sans JP',sans-serif;}
.chat-input:focus{border-color:var(--jl);}
/* Message colour swatch + popup palette */
.chat-color-btn{width:calc(var(--ui-vmin) * 3.611);height:calc(var(--ui-vmin) * 3.611);
  border-radius:50%;border:2px solid rgba(201,168,76,.4);background:#fff;
  cursor:pointer;flex-shrink:0;padding:0;transition:border-color .2s,transform .15s;}
.chat-color-btn:hover{border-color:var(--gl);transform:scale(1.08);}
.chat-color-btn.open{border-color:var(--gl);}
.chat-color-picker{position:fixed;bottom:70px;right:20px;z-index:66;display:none;
  grid-template-columns:repeat(4,1fr);gap:8px;padding:10px;
  background:rgba(3,12,6,.97);border:1px solid rgba(201,168,76,.3);border-radius:10px;
  box-shadow:0 8px 24px rgba(0,0,0,.7);}
.chat-color-picker.open{display:grid;}
.chat-color-swatch{width:24px;height:24px;border-radius:50%;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-btn{width:calc(var(--ui-vmin) * 3.611);height:calc(var(--ui-vmin) * 3.611);
  border-radius:50%;border:2px solid rgba(201,168,76,.4);background:rgba(0,0,0,.35);
  cursor:pointer;flex-shrink:0;padding:0;font-size:calc(var(--ui-vmin) * 2);line-height:1;
  transition:border-color .2s,transform .15s;}
.chat-emoji-btn:hover{border-color:var(--gl);transform:scale(1.08);}
.chat-emoji-btn.open{border-color:var(--gl);}
.chat-emoji-picker .emoji-swatch{width:32px;height:32px;border-radius:50%;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(201,168,76,.15);}
.chat-send{padding:0 calc(var(--ui-vmin) * 1.389);height:calc(var(--ui-vmin) * 3.611);background:rgba(26,107,69,.8);
  border:1px solid var(--jl);border-radius:7px;color:var(--cream);
  font-size:calc(var(--ui-vmin) * 1.528);cursor:pointer;font-family:'Noto Sans JP',sans-serif;white-space:nowrap;flex-shrink:0;}
.chat-send:hover{background:rgba(42,158,103,.9);}
/* History button */
.chat-hist-btn{position:relative;padding:0 calc(var(--ui-vmin) * 1.389);height:calc(var(--ui-vmin) * 3.611);
  background:none;border:none;border-left:1px solid rgba(201,168,76,.2);
  color:rgba(201,168,76,.6);font-size:calc(var(--ui-vmin) * 1.528);letter-spacing:calc(var(--ui-vmin) * .139);cursor:pointer;
  font-family:'Noto Sans JP',sans-serif;white-space:nowrap;flex-shrink:0;
  transition:color .2s;}
.chat-hist-btn:hover{color:var(--gl);}
.chat-hist-btn.open{color:var(--gl);}
.chat-hist-btn .badge{position:absolute;top:-5px;right:2px;
  background:#c02020;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:7px;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);position:fixed;bottom:106px;right:12px;
  width:clamp(220px,35vw,300px);max-height:260px;
  background:rgba(5,14,8,.97);border:1px solid rgba(201,168,76,.3);
  border-radius:12px;z-index:74;display:none;flex-direction:column;
  overflow:hidden;box-shadow:0 8px 32px rgba(0,0,0,.8);
  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);background:rgba(26,107,69,.25);
  border-bottom:1px solid rgba(201,168,76,.2);font-size:calc(var(--ui-vmin) * 1.389);letter-spacing:2px;
  color:var(--gold);font-family:'Cinzel Decorative',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,.05);
  border-radius:6px;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:10px;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{position:fixed;z-index:400;display:flex;gap:6px;
  padding:calc(var(--ui-vmin) * .833) calc(var(--ui-vmin) * 1.111);
  background:rgba(5,14,8,.97);border:1px solid rgba(201,168,76,.3);border-radius:12px;
  box-shadow:0 8px 24px rgba(0,0,0,.7);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{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(5,14,8,.97);border:1px solid rgba(201,168,76,.3);border-radius:10px;
  box-shadow:0 8px 24px rgba(0,0,0,.7);animation:slideUpChat .15s ease;}
.msg-react-picker button{font-size:1.3rem;line-height:1;background:none;border:2px solid transparent;
  border-radius:50%;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);}

/* 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;}
.chat-msg{position:absolute;left:100%;white-space:nowrap;color:#fff;
  font-family:'MS Gothic','ＭＳ ゴシック',monospace;font-weight:700;
  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 call banner (.cb-layer:205, pon/
   chii/kan/riichi), round-start card (.rs-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)}}

/* ── Call banner (pon/kan/chi/riichi) — slides in from the bottom-left and settles across the felt ── */
.cb-layer{position:fixed;inset:0;z-index:205;pointer-events:none;
  display:flex;align-items:center;justify-content:center;overflow:hidden;}
.call-banner{
  --accent:var(--gl);--accent-strong:var(--gold);--accent-glow:rgba(201,168,76,.4);
  /* A rotated rectangle can only ever land ONE diagonal pair of corners on a
     fixed-width bounding box — the other pair is inset by H*sin(angle) no
     matter the width. To get all four corners flush with the table, the
     banner is a parallelogram instead: left/right edges stay vertical (so
     both left corners and both right corners sit exactly on the table's
     edges by construction) and the tilt comes from slanted top/bottom edges,
     offset by W*tan(4deg) so the slant reads as an equivalent ~4deg lean. */
  /* --table-size (110vmin) is deliberately allowed to overflow the
     viewport by up to 10% — fine for .wood-table, a background element
     that's meant to bleed off-screen, but not for a banner that needs to
     stay fully on-screen and readable. Cap it to the table size or 100%
     of the viewport WIDTH, whichever is smaller — not viewport height
     too: --table-size is also used as .wood-table's height (a square),
     but here it only ever drives the banner's width, so a vh-based cap
     would bind on perfectly ordinary landscape screens where the
     banner's actual (much shorter) height was never at risk of
     overflowing. On a narrow/portrait screen (e.g. a phone, where width
     is the smaller dimension) --table-size itself already exceeds the
     viewport width — that's fine for .wood-table, which is allowed to
     bleed off both edges, but the banner should use the full width
     right up to the screen edge rather than leaving extra margin, so
     it reads closer to the table's own (bleeding) size instead of
     looking small next to it. Every other measurement (slant, font
     sizes, min-height) derives from this SAME variable as a fixed ratio
     of it, so whichever constraint ends up binding, everything scales
     down together instead of one dimension getting capped while others
     keep scaling off the uncapped --table-size. */
  --cb-size:min(var(--table-size), 100vw);
  --cb-slant:calc(var(--cb-size) * tan(4deg));
  --cb-poly:polygon(0% var(--cb-slant), 100% 0, 100% calc(100% - var(--cb-slant)), 0% 100%);
  /* .cb-layer is a flex container, and flex items shrink to fit it by
     default (min-width:auto) — flex-shrink:0 makes sure the banner always
     renders at exactly --cb-size rather than being squeezed further. */
  position:relative;width:var(--cb-size);flex-shrink:0;
  /* Same ratio to --cb-size as the label font below (18/110), so height
     and label size are always locked together regardless of which
     constraint --cb-size is currently hitting. */
  min-height:calc(var(--cb-size) * 0.16364);
  display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;
  /* A CSS `border` draws a rectangle, which then gets chopped by clip-path —
     along the slanted edges it shows up as a jagged rectangular fragment
     instead of a line that follows the diagonal. Instead, the accent colour
     fills the whole outer polygon (acting as the border) and an inset
     pseudo-element, clipped to the same polygon shape, sits on top with the
     real background — leaving a clean accent-coloured ring that hugs the
     slant on every edge. */
  background:var(--accent-strong);border-radius:0;
  /* Vertical padding must be >= the slant, otherwise the diagonal corners
     cut into the label/name near the left and right edges instead of just
     trimming blank padding. */
  padding:calc(var(--cb-slant) + 10px) 22px;
  box-shadow:0 12px 34px rgba(0,0,0,.65),0 0 40px var(--accent-glow);
  clip-path:var(--cb-poly);
  animation:cb-slide 2.24s cubic-bezier(.22,.9,.3,1) forwards;
}
.call-banner::before{content:'';position:absolute;inset:2px;
  background:linear-gradient(155deg,rgba(18,18,18,.96),rgba(4,4,4,.98));
  clip-path:var(--cb-poly);z-index:0;}
.call-banner .cb-content{position:relative;z-index:1;
  display:flex;flex-direction:column;align-items:center;gap:2px;
  /* Same angle as --cb-slant (W*tan(4deg)) so the text leans with the
     banner's slanted top/bottom edges instead of sitting dead level. */
  transform:rotate(-4deg);}
.call-banner .cb-label{font-family:'Noto Serif JP',serif;font-weight:900;
  /* calc() off --cb-size (not plain vmin) so the label shrinks in lockstep
     whenever --cb-size hits its 92vw/92vh cap, instead of continuing to
     scale with vmin past the point the box itself has stopped growing. */
  font-size:calc(var(--cb-size) * 0.16364);letter-spacing:.08em;color:var(--accent);
  /* Fixed (not 1em) on purpose — .cb-label-long below shrinks font-size but
     deliberately does NOT touch line-height, so its line box (and therefore
     the whole banner's flex-sized height, since .call-banner has no fixed
     height of its own) stays identical to every other call type. Without
     this, a 1em line-height would shrink right along with the smaller font
     and the double-riichi banner would end up visibly shorter than a pon/
     kan/chi/riichi banner. */
  line-height:calc(var(--cb-size) * 0.16364);}
/* ダブルリーチ is twice the character count of every other call-banner
   label — same scaling basis as .cb-label above, just half the ratio so it
   fits the same banner width on one line instead of wrapping (see the
   cb-label-long toggle in callBanner, script.js). Only font-size shrinks;
   line-height is inherited unchanged from .cb-label above so the banner's
   height matches every other call type. */
.call-banner .cb-label.cb-label-long{font-size:calc(var(--cb-size) * 0.082);}
.call-banner .cb-name{font-family:'Cinzel Decorative',serif;
  font-size:calc(var(--cb-size) * 0.01455);letter-spacing:2px;color:rgba(245,237,216,.7);}
.call-banner .cb-name b{color:var(--accent-strong);}
.call-banner[data-type="pon"]{--accent:#ffe0b0;--accent-strong:#ca0;--accent-glow:rgba(202,170,0,.4);}
.call-banner[data-type="kan"]{--accent:#e8d0ff;--accent-strong:#a060d0;--accent-glow:rgba(160,96,208,.4);}
.call-banner[data-type="chi"]{--accent:#c0ffc0;--accent-strong:#4d4;--accent-glow:rgba(68,221,68,.35);}
.call-banner[data-type="riichi"]{--accent:#d0d0ff;--accent-strong:#77f;--accent-glow:rgba(119,119,255,.4);}
.call-banner[data-type="kita"]{--accent:#b0f0ff;--accent-strong:#20b0c0;--accent-glow:rgba(32,176,192,.4);}
@keyframes cb-slide{
  0%{opacity:0;transform:translate(-58vw,42vh) scale(.55);}
  20%{opacity:.90;}
  32%{transform:translate(0,0) scale(1);}
  84%{opacity:.90;transform:translate(0,0) scale(1);}
  100%{opacity:0;transform:translate(0,-1.2vh) scale(.97);}
}
@media (prefers-reduced-motion:reduce){
  .call-banner{animation:none;opacity:.85;transform:none;}
}

/* ── 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;}
.rs-scrim{position:absolute;inset:0;z-index:0;background:rgba(0,0,0,.85);
  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:6px;
  /* Plain vmin (no clamp min/max) — same fix as the call banner: a px
     cap on padding/font-size would stop scaling before the others do at
     large or small resolutions and throw them out of proportion with
     each other. */
  padding:6vmin 9vmin;
  animation:rs-hold 3.2s ease forwards;}
.rs-wind{font-family:'Noto Serif JP',serif;font-weight:900;color:var(--gl);
  font-size:16vmin;line-height:1;
  text-shadow:0 0 30px rgba(201,168,76,.55),0 0 60px rgba(201,168,76,.25);
  opacity:0;animation:rs-wind-in 1s cubic-bezier(.2,.8,.3,1) forwards;}
.rs-sub{font-family:'Cinzel Decorative',serif;letter-spacing:.12em;color:var(--cream);
  font-size:2.4vmin;display:flex;align-items:center;gap:10px;
  opacity:0;animation:rs-fade-up .5s ease forwards;animation-delay:.55s;}
.rs-sub i{color:var(--gold);font-style:normal;opacity:.7;}
.rs-start{font-family:'Cinzel Decorative',serif;letter-spacing:.4em;text-transform:uppercase;
  font-size:1.6vmin;color:rgba(245,237,216,.5);
  /* Starts once .rs-sub's own .55s-delay + .5s fade has finished, so the
     round info reads first and 開始 follows as a distinct beat rather than
     fading in alongside it. */
  opacity:0;animation:rs-fade-up .5s ease forwards;animation-delay:1.15s;}
@keyframes rs-wind-in{
  0%{opacity:0;filter:blur(10px);transform:scale(1.3);}
  60%{opacity:1;filter:blur(0);}
  100%{opacity:1;transform:scale(1);}
}
@keyframes rs-fade-up{from{opacity:0;transform:translateY(6px);}to{opacity:1;transform:translateY(0);}}
@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-wind,.rs-sub,.rs-start,.rs-scrim{animation:none;opacity:1;filter:none;transform:none;}
}

/* ════════════════════════════════════════════════════════════
   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;}
.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-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;}
.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);}
.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). */
.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);position:fixed;top:0;right:0;bottom:0;
  width:calc(var(--ui-vmin) * 33.33);max-width:calc(100vw - 24px);
  background:rgba(6,12,7,.97);border-left:1px solid rgba(201,168,76,.2);
  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;}
/* 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:4px;
  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);}
.icon-btn{background:rgba(42,58,42,.3);border:1px solid rgba(201,168,76,.2);border-radius:8px;
  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 button ── */
/* Raw vmin works for the logo (large coefficients stay legible even at
   tiny vmin), but these are small text — 1.4vmin on a 320px phone is a
   4.5px caption. Route them through one clamped scale unit instead: it
   still tracks vmin (and everything below stays proportional to
   everything else, since they all multiply the SAME variable — no
   independent caps to decouple), but never drops below a readable floor
   or balloons past a sensible ceiling on very large screens. Scoped to
   #s-start (inherited by its descendants) rather than :root, so it
   doesn't affect anything outside the start screen. */
#s-start{--ui-vmin:clamp(6px,1vmin,10px);}
#bstart{padding:calc(var(--ui-vmin) * 2.2) calc(var(--ui-vmin) * 6.7);
  font-family:'Cinzel Decorative',serif;
  font-size:calc(var(--ui-vmin) * 1.9);letter-spacing:calc(var(--ui-vmin) * .56);
  background:linear-gradient(135deg,rgba(26,107,69,.5),rgba(13,61,40,.5));
  border:1px solid var(--jl);border-radius:4px;color:var(--cream);cursor:pointer;
  animation:pulse 1.5s ease-in-out infinite;transition:all .2s;}
#bstart.pressed{transform:scale(.9);}

/* Start -> lobby transition: press-pulse the button (via .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);}}
.start-copy{font-size:calc(var(--ui-vmin) * 1.4);color:rgba(245,237,216,.3);
  letter-spacing:calc(var(--ui-vmin) * .28);}
.about-link{font-size:calc(var(--ui-vmin) * 1.4);color:rgba(245,237,216,.4);
  letter-spacing:calc(var(--ui-vmin) * .14);
  text-decoration:underline;margin-top:6px;cursor:pointer;transition:color .2s;}
.about-link:hover{color:var(--gold);}
/* 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. */
.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:8px;
  padding:calc(var(--ui-vmin, 7.2px) * 1.111) calc(var(--ui-vmin, 7.2px) * 1.667);cursor:pointer;transition:border-color .2s;position:relative;
  text-align:left;margin-bottom:calc(var(--ui-vmin, 7.2px) * 2.222);}
.profile-nameplate:hover{border-color:rgba(201,168,76,.5);}
.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);}
.pnp-change{position:absolute;top:calc(var(--ui-vmin, 7.2px) * 1.111);right:calc(var(--ui-vmin, 7.2px) * 1.667);font-size:calc(var(--ui-vmin, 7.2px) * 1.25);color:rgba(245,237,216,.35);
  letter-spacing:calc(var(--ui-vmin, 7.2px) * .139);text-transform:uppercase;opacity:0;transition:opacity .2s;}
.profile-nameplate:hover .pnp-change{opacity:1;}
/* 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);
  display:none;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-y:auto;
  background:#0a160c;border:1px solid rgba(201,168,76,.3);
  border-radius:8px;padding:calc(var(--ui-vmin) * 3.333);z-index:301;box-shadow:0 12px 48px rgba(0,0,0,.8);}
.about-backdrop.visible,.about-modal.visible{display:block;}
.leave-modal{height:auto;}
/* 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;}
.about-body{font-size:calc(var(--ui-vmin, 7.2px) * 1.667);line-height:1.6;color:rgba(245,237,216,.75);}
.about-body p{margin:0 0 calc(var(--ui-vmin, 7.2px) * 1.389);}
.about-body p:last-child{margin-bottom:0;}
.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);}
/* 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:8px;
  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:10px;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;}
/* 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;}

/* ── Topbar inline elements ── */
.tb-right .settings-icon-btn{position:static;z-index:auto;}
.tb-mode-label{font-size:calc(var(--ui-vmin) * 1.389);opacity:.8;margin-left:6px;}
.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 positions ── */
.ctr-score-top{position:absolute;top:0;transform:rotate(180deg);z-index:1;}
.ctr-score-left{position:absolute;left:-40%;transform:rotate(90deg);z-index:4;clip-path:inset(0 0 0 20%);}
.ctr-score-right{position:absolute;right:-40%;transform:rotate(-90deg);z-index:2;}
.ctr-score-bottom{position:absolute;bottom:0;z-index:3;}
.ctr-mid-rel{position:absolute;}

/* ── Riichi stick positions ── */
/* top:50% + translateY(-50%) instead of a bottom:0.3vmin anchor — the old
   anchor left the stick offset from its row's centre (.ctr-score, the
   same box for all four sides; rotate(90/-90deg) on .ctr-score-left/
   -right just remaps which screen axis "vertical" ends up on, so the
   same bottom-anchor offset showed up as a Y gap for top/bottom seats
   and an X gap for left/right seats). Percentage-based positioning is
   resolution-independent by construction — no vmin/px value to fall out
   of proportion at another screen size. (Pixel-matching this to
   .ctr-score-val's own padding-shifted centre made it sit noticeably
   high in practice, so this centers on the row's own box instead.) */
.rs-bottom{display:none;position:absolute;top:50%;right:1.45vmin;transform:translateY(-50%);z-index:16;}
.rs-top{display:none;position:absolute;top:50%;right:1.45vmin;transform:translateY(-50%);z-index:16;}
.rs-left{display:none;position:absolute;top:50%;right:1.45vmin;transform:translateY(-50%);z-index:16;}
.rs-right{display:none;position:absolute;top:50%;right:1.45vmin;transform:translateY(-50%);z-index:16;}

/* ── Riichi stick size ── */
.rs-bottom svg{width:6vmin;height:auto;display:block;}
.rs-top svg{width:6vmin;height:auto;display:block;}
.rs-left svg{width:6vmin;height:auto;display:block;}
.rs-right svg{width:6vmin;height:auto;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:1vmin;white-space:nowrap;}
.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 tiles, in line with each
   seat's own nameplate row. Rotation follows the tile-facing convention
   used by ml-l/ml-r (left:+90deg, right:-90deg), not the nameplate rows
   (which both use +90deg since text orientation doesn't mirror). ── */
.kita-strip{position:absolute;display:flex;flex-direction:row-reverse;gap:0;align-items:flex-end;z-index:20;}
.kita-strip:empty{display:none;}
/* Own (bottom) seat reads left-to-right like everything else on this row —
   opposite of the other three seats, which is why it needs its own
   flex-direction override: stability requires the FIXED anchor edge to be
   whichever side the OLDEST tile renders on (see .kita-strip's shared
   row-reverse comment above), and for a left-to-right reading order the
   oldest tile is the LEFTMOST one, so the anchor has to be `left`, not
   `right` like the other three.
   Anchored at the felt's horizontal CENTER (left:50%) rather than some
   percentage tucked close to the nameplate — the nameplate (.pname) has no
   max-width of its own (grows freely with the player's name, up to the
   20-char limit the profile name input enforces), so any fixed anchor close
   to it risks the two overlapping on a narrow screen with a long name. 50%
   needs the nameplate to reach clear across half the felt's width to reach
   it, which no realistic name does, while still leaving the whole right
   half of the row for tiles to grow into before the corner shared with the
   right player's nameplate (see the max-width note below). */
.kita-b{bottom:calc(var(--tlg-h) + 3vmin);left:50%;flex-direction:row;
  /* 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 was sized for (see conversation), but this also protects
     against a rarer 5th+ tile in one game rather than just assuming 4 is a
     hard limit. overflow-x auto (not hidden) so an actual overflow is
     still reachable by scrolling rather than silently clipped/invisible. */
  max-width:38%;overflow-x:auto;overflow-y:hidden;}
.kita-t{top:calc(var(--tsm-h) + 3vmin);left:15%;transform:rotate(180deg);}
.kita-l{bottom:15%;left:calc(var(--tsm-h) + 3vmin);transform:rotate(90deg) translateX(-100%);transform-origin:bottom left;}
.kita-r{top:15%;right:calc(var(--tsm-h) + 3vmin);transform:rotate(-90deg) translateY(-100%);transform-origin:top right;}
.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:12px;
  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);}

/* ── 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;}
