/* ============================================================
   Distance Online — 4P Dice Board Game
   Mobile-style glass HUD: the whole play surface is the 3D board,
   with floating bars, a party panel and a bottom action dock.
   ============================================================ */

:root {
  --bg:          #0d1226;
  --bg2:         #141b3d;
  --panel:       rgba(16, 20, 46, .78);
  --panel-solid: #1a2148;
  --panel2:      #232b56;
  --stroke:      rgba(150, 170, 255, .18);
  --stroke-hi:   rgba(170, 190, 255, .38);

  --accent:      #7c5cff;
  --accent2:     #ff5c8a;
  --accent3:     #38e8c0;
  --gold:        #ffc93c;
  --warn:        #ffb020;
  --good:        #46e08a;
  --bad:         #ff5f6d;
  --info:        #4bc8ff;

  --text:        #eef1ff;
  --muted:       #9aa6d8;

  --seat0: #7c5cff;
  --seat1: #ff5c8a;
  --seat2: #38e8c0;
  --seat3: #ffc93c;

  --radius: 16px;
  --shadow-lg: 0 24px 60px rgba(0, 0, 0, .55);
  --shadow-md: 0 10px 30px rgba(0, 0, 0, .40);
  --shadow-sm: 0 4px 14px rgba(0, 0, 0, .32);
  /* One gap value shared by the rail (Profile -> Status) and the Log card
     (Status -> Log), so the left column is evenly spaced. */
  --rail-gap: 4px;

  /* Layout metrics. These MUST have a base value here: the rail is positioned
     with `top: calc(var(--top-h) + 4px)`, and a custom property with no value
     makes the whole declaration invalid at computed-value time. The rail then
     falls back to `top: auto`, which — with `bottom: 8px` — pins it to the
     BOTTOM of the screen instead of the top-left corner. They used to be
     defined only inside the `max-height: 480px` media query, so every normal
     desktop viewport lost the top-left Profile.
     The same trap hit `--die`: `.dice3d { width: var(--die) }` computed to an
     invalid length, the die collapsed to 0x0, and the walk roll looked like it
     had no animation at all — the throw was running on an invisible cube. */
  --top-h: 34px;
  --rail-w: 244px;
  --bar-h: 96px;
  /* req 2 (round 20): the two animation stages are centred on the screen, side
     by side, so they no longer reserve a corner of the log. --portrait-size is
     the square each frame takes; it is clamped against the viewport height so a
     phone in landscape still fits both frames plus the gap. */
  --portrait-size: clamp(120px, 26vh, 210px);
  --dock-h: 0px;
  --die: 44px;
  --orb: 56px;
  /* req 4: the action pucks are the primary controls, so they are sized for a
     thumb rather than for a mouse. 60px is comfortably above the 44px minimum
     touch target and still leaves the arc inside the bottom-right corner. */
  --puck: 60px;
  --slot: 50px;
  --log-w-base: 300px;
  --log-min: 96px;
  /* req 1 (round 21): the Profile card is the same width as the Status card, so
     the left column has one clean edge. The Status card is content-sized, so
     there is no measured height to publish. */
  --profile-h: 120px;
}

* { box-sizing: border-box; }
/* The play surface is exactly the viewport: no page scroll, no rubber-banding.
   `100dvh` tracks the *visible* height, so the layout does not jump when iOS
   Safari shows or hides its URL bar (100vh keeps the tall value and pushed the
   HUD off-screen). `100vh` stays first as the fallback for older browsers. */
html, body {
  height: 100%;
  height: 100dvh;
  overflow: hidden;
  overscroll-behavior: none;
  /* No page-level scrollbar anywhere: the board owns its own pan/zoom. */
  scrollbar-width: none;
  -ms-overflow-style: none;
}
html::-webkit-scrollbar, body::-webkit-scrollbar { width: 0; height: 0; display: none; }
body {
  margin: 0;
  width: 100%;
  min-height: 100%;
  font-family: "Segoe UI", system-ui, -apple-system, "Noto Sans Thai", sans-serif;
  background: var(--bg);
  color: var(--text);
  -webkit-font-smoothing: antialiased;
  /* iOS: stop the whole page from being dragged/scrolled under the HUD. */
  position: fixed;
  inset: 0;
  touch-action: manipulation;
}
#app { width: 100%; height: 100%; min-height: 0; overflow: hidden; }
.hidden { display: none !important; }
.screen { display: none; height: 100%; height: 100dvh; overflow: hidden; }
.screen.active { display: block; }

button { font-family: inherit; cursor: pointer; border: none; color: inherit; }
button:disabled { cursor: not-allowed; }
input { font-family: inherit; }

/* ============================ LOBBY ============================ */
/* The lobby is a scroll container, not a fixed-height block: on a short
   landscape phone (375 px tall) the card is taller than the viewport, and
   without this the bottom half — including the START button — was unreachable.
   `margin: auto` centres the card when there IS room and lets it scroll from the
   top when there is not. */
#lobby.screen.active, #auth.screen.active {
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  overflow-x: hidden;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
  padding: max(12px, env(safe-area-inset-top)) max(12px, env(safe-area-inset-right))
           max(12px, env(safe-area-inset-bottom)) max(12px, env(safe-area-inset-left));
}
.lobby-bg {
  position: fixed; inset: 0; z-index: 0;
  background:
    radial-gradient(1200px 700px at 18% 12%, rgba(124, 92, 255, .30), transparent 62%),
    radial-gradient(1000px 620px at 84% 78%, rgba(56, 232, 192, .18), transparent 60%),
    radial-gradient(800px 500px at 50% 110%, rgba(255, 92, 138, .18), transparent 62%),
    linear-gradient(160deg, #0d1226 0%, #141b3d 55%, #0b1024 100%);
}
.lobby-bg::after {
  content: ""; position: absolute; inset: 0;
  background-image:
    linear-gradient(rgba(255,255,255,.035) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255,255,255,.035) 1px, transparent 1px);
  background-size: 46px 46px;
  mask-image: radial-gradient(circle at 50% 45%, #000 25%, transparent 78%);
}
.lobby-card {
  position: relative; z-index: 1;
  /* Fluid width: fills a phone, caps on a desktop, never overflows. */
  width: 100%;
  max-width: min(440px, 100%);
  margin: auto;
  padding: clamp(18px, 4.5vw, 34px) clamp(16px, 4vw, 32px) clamp(16px, 4vw, 30px);
  background: var(--panel);
  backdrop-filter: blur(16px);
  border: 1px solid var(--stroke);
  border-radius: 22px;
  box-shadow: var(--shadow-lg);
  text-align: center;
  animation: cardIn .45s cubic-bezier(.2,.9,.3,1.2);
}
/* The login card is the same card with a little more room for the tabs. */
.auth-card {
  position: relative; z-index: 1;
  width: 100%;
  max-width: min(420px, 100%);
  margin: auto;
  padding: clamp(18px, 4.5vw, 32px) clamp(16px, 4vw, 30px) clamp(16px, 4vw, 28px);
  background: var(--panel);
  backdrop-filter: blur(16px);
  border: 1px solid var(--stroke);
  border-radius: 22px;
  box-shadow: var(--shadow-lg);
  text-align: center;
  animation: cardIn .45s cubic-bezier(.2,.9,.3,1.2);
  /* A short landscape phone must still reach the guest button. */
  max-height: calc(100dvh - 24px);
  overflow-y: auto;
  overscroll-behavior: contain;
}
@keyframes cardIn { from { opacity: 0; transform: translateY(18px) scale(.97); } }

/* ---- Login tabs ---- */
.auth-tabs {
  display: flex; gap: 6px; margin: 0 0 clamp(12px, 3vw, 18px);
  padding: 4px; border-radius: 14px;
  background: rgba(8, 11, 26, .55);
  border: 1px solid var(--stroke);
}
.auth-tab {
  flex: 1 1 0; min-width: 0;
  padding: 9px 6px; border: 0; border-radius: 10px;
  background: transparent; color: var(--muted);
  font: inherit; font-size: clamp(11px, 2.8vw, 13px); font-weight: 800;
  cursor: pointer; transition: background .16s, color .16s;
}
.auth-tab:hover { color: #fff; }
.auth-tab.is-on {
  background: linear-gradient(135deg, var(--accent), var(--accent2));
  color: #0b0f22;
  box-shadow: 0 4px 14px rgba(124, 92, 255, .35);
}
.auth-note {
  margin: 10px 0 0; color: var(--muted);
  font-size: clamp(10px, 2.4vw, 11px); line-height: 1.5;
}

/* ---- Who is signed in (lobby header) ---- */
.whoami {
  display: flex; align-items: center; gap: 8px;
  margin: 0 0 clamp(10px, 2.4vw, 14px);
  padding: 8px 10px;
  background: rgba(8, 11, 26, .55);
  border: 1px solid var(--stroke);
  border-radius: 12px;
  text-align: left;
}
.whoami-ico { font-size: 15px; line-height: 1; }
.whoami-name {
  flex: 1 1 auto; min-width: 0;
  font-size: clamp(12px, 3vw, 14px); font-weight: 800; color: #fff;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.whoami-tag {
  flex: 0 0 auto;
  padding: 2px 8px; border-radius: 999px;
  font-size: 9px; font-weight: 900; letter-spacing: .6px;
  background: rgba(255, 201, 60, .18);
  border: 1px solid rgba(255, 201, 60, .5);
  color: #ffe6a3;
}
.whoami-tag.is-member {
  background: rgba(56, 232, 192, .18);
  border-color: rgba(56, 232, 192, .55);
  color: #a8f7e4;
}

.lobby-badge {
  display: inline-block; padding: 4px 14px; margin-bottom: clamp(8px, 2vw, 14px);
  font-size: 11px; font-weight: 800; letter-spacing: 2px;
  color: #0d1226;
  background: linear-gradient(135deg, var(--gold), #ffe08a);
  border-radius: 999px;
  box-shadow: 0 4px 16px rgba(255, 201, 60, .35);
}
.lobby-title {
  margin: 0 0 4px;
  /* Scales with the viewport so the title never forces the card wider than a
     small phone. */
  font-size: clamp(26px, 7vw, 40px);
  font-weight: 900; letter-spacing: 1px;
  background: linear-gradient(135deg, #fff 10%, var(--accent) 55%, var(--accent2) 95%);
  -webkit-background-clip: text; background-clip: text; color: transparent;
  text-shadow: 0 0 34px rgba(124, 92, 255, .35);
}
.lobby-title span { font-size: clamp(15px, 4vw, 24px); vertical-align: super; opacity: .85; }
.lobby-sub { margin: 0 0 clamp(12px, 3vw, 22px); color: var(--muted); font-size: clamp(11px, 2.6vw, 13px); }

.field { text-align: left; margin-bottom: clamp(9px, 2.2vw, 14px); }
.field label {
  display: block; margin-bottom: 6px;
  font-size: 11px; font-weight: 700; letter-spacing: 1px; color: var(--muted);
}
.field-row { display: flex; gap: 8px; align-items: stretch; }
.field-row input { flex: 1 1 auto; min-width: 0; }
input {
  width: 100%; padding: 12px 14px;
  background: rgba(8, 11, 26, .7);
  border: 1px solid var(--stroke);
  border-radius: 12px;
  color: var(--text); font-size: 15px;
  outline: none;
  transition: border-color .18s, box-shadow .18s;
}
input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(124, 92, 255, .22); }
input::placeholder { color: rgba(154, 166, 216, .6); }
/* A member's display name comes from their account and cannot be edited. */
input.is-locked {
  background: rgba(8, 11, 26, .35);
  color: var(--muted);
  cursor: not-allowed;
}

.btn {
  padding: 11px 20px; border-radius: 12px;
  font-size: 14px; font-weight: 700; letter-spacing: .3px;
  transition: transform .12s, filter .18s, box-shadow .18s;
  white-space: nowrap;
}
.btn:hover:not(:disabled) { filter: brightness(1.12); transform: translateY(-1px); }
.btn:active:not(:disabled) { transform: translateY(1px); }
.btn-primary {
  background: linear-gradient(135deg, var(--accent), #9d7cff);
  box-shadow: 0 8px 22px rgba(124, 92, 255, .42);
}
.btn-secondary {
  background: rgba(255,255,255,.08);
  border: 1px solid var(--stroke-hi);
  box-shadow: var(--shadow-sm);
}
.btn-lg { width: 100%; padding: 14px; font-size: 16px; }
.btn-sm { padding: 6px 12px; font-size: 12px; border-radius: 9px; }
.btn-ghost {
  background: rgba(255,255,255,.05);
  border: 1px solid var(--stroke);
  color: var(--muted);
}
.btn-ghost:hover { color: var(--text); border-color: var(--stroke-hi); }

/* ---- Quick Match queue ---- */
.queue-panel {
  margin-top: clamp(8px, 2vw, 12px);
  padding: clamp(10px, 2.4vw, 14px);
  background: rgba(8, 11, 26, .6);
  border: 1px solid var(--stroke-hi);
  border-radius: 14px;
  text-align: left;
}
.queue-head { display: flex; align-items: center; gap: 9px; font-size: 13px; font-weight: 700; }
.queue-spinner {
  width: 13px; height: 13px; flex: 0 0 auto;
  border: 2px solid rgba(120, 200, 255, .28);
  border-top-color: #6fd3ff;
  border-radius: 50%;
  animation: queue-spin .8s linear infinite;
}
@keyframes queue-spin { to { transform: rotate(360deg); } }
.queue-bar {
  margin: 9px 0 5px; height: 6px; border-radius: 99px;
  background: rgba(255,255,255,.09); overflow: hidden;
}
.queue-bar > i {
  display: block; height: 100%; width: 0%;
  border-radius: 99px;
  background: linear-gradient(90deg, #38e8c0, #6fd3ff);
  transition: width .3s ease;
}
.queue-meta { font-size: 11px; color: var(--muted); letter-spacing: .5px; }
.queue-list { list-style: none; margin: 8px 0 10px; padding: 0; display: flex; flex-wrap: wrap; gap: 6px; }
.queue-list li {
  font-size: 11px; padding: 3px 9px; border-radius: 99px;
  background: rgba(111, 211, 255, .14);
  border: 1px solid rgba(111, 211, 255, .35);
  color: #cfeaff;
}

/* ---- Room browser ---- */
.browser {
  margin-top: clamp(10px, 2.6vw, 16px);
  padding: clamp(9px, 2.2vw, 13px);
  background: rgba(8, 11, 26, .5);
  border: 1px solid var(--stroke);
  border-radius: 14px;
  text-align: left;
}
.browser-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.browser-title { font-size: 11px; letter-spacing: 1.6px; font-weight: 700; color: var(--muted); }
.room-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; max-height: 168px; overflow-y: auto; }
.room-list li.room-empty { font-size: 12px; color: var(--muted); padding: 6px 2px; }
.room-row {
  display: flex; align-items: center; gap: 8px;
  padding: 7px 9px; border-radius: 10px;
  background: rgba(255,255,255,.045);
  border: 1px solid var(--stroke);
  cursor: pointer;
  transition: border-color .15s, background .15s;
}
.room-row:hover { background: rgba(111,211,255,.1); border-color: rgba(111,211,255,.5); }
.room-row .rr-code { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 12px; color: var(--gold); }
.room-row .rr-host { flex: 1 1 auto; min-width: 0; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.room-row .rr-seats { font-size: 11px; color: var(--muted); flex: 0 0 auto; }
.room-row .rr-tag {
  font-size: 9px; letter-spacing: .6px; padding: 2px 6px; border-radius: 99px;
  background: rgba(56,232,192,.18); border: 1px solid rgba(56,232,192,.45); color: #9ff5e0;
  flex: 0 0 auto;
}

.divider {
  display: flex; align-items: center; gap: 12px;
  margin: clamp(10px, 2.6vw, 18px) 0; color: var(--muted); font-size: 11px; letter-spacing: 2px;
}
.divider::before, .divider::after {
  content: ""; flex: 1; height: 1px;
  background: linear-gradient(90deg, transparent, var(--stroke-hi), transparent);
}

.msg { min-height: 20px; margin-top: 10px; color: var(--warn); font-size: 13px; font-weight: 600; }

.room-info {
  margin-top: clamp(10px, 2.6vw, 18px); padding: clamp(9px, 2.4vw, 14px);
  background: rgba(8, 11, 26, .55);
  border: 1px solid var(--stroke);
  border-radius: 14px;
  text-align: left;
}
.room-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
.room-label { font-size: 11px; letter-spacing: 2px; color: var(--muted); font-weight: 700; }
.room-stage {
  display: flex; align-items: center; justify-content: space-between;
  margin-bottom: 10px; padding: 6px 9px;
  background: rgba(124, 92, 255, .12);
  border: 1px solid rgba(124, 92, 255, .28);
  border-radius: 9px;
}
.room-stage-label { font-size: 11px; letter-spacing: 2px; color: var(--muted); font-weight: 700; }
.room-stage-name { font-size: 13px; font-weight: 700; color: var(--text); }

/* Stage picker: five fixed backdrops, one row that wraps on a narrow card. */
.scene-picker { display: flex; flex-wrap: wrap; gap: 6px; }
.scene-btn {
  /* Seven stages: four per row leaves a tidy 4 + 3 grid. */
  flex: 1 1 calc(25% - 6px); min-width: 72px;
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  padding: 7px 4px; cursor: pointer;
  background: rgba(8, 11, 26, .55);
  border: 1px solid var(--stroke);
  border-radius: 10px;
  color: var(--muted);
  font: inherit; font-size: 11px; font-weight: 700;
  transition: border-color .15s, background .15s, color .15s, transform .1s;
}
.scene-btn:hover:not(:disabled) { border-color: rgba(124, 92, 255, .55); color: var(--text); }
.scene-btn:active:not(:disabled) { transform: scale(.97); }
.scene-btn.is-on {
  background: rgba(124, 92, 255, .22);
  border-color: var(--violet);
  color: var(--text);
  box-shadow: 0 0 14px rgba(124, 92, 255, .35);
}
.scene-btn:disabled { opacity: .45; cursor: not-allowed; }
.scene-ico { font-size: 17px; line-height: 1; }
.scene-name { white-space: nowrap; }

/* Adventure mode: a row of stage cards, each showing its reward and whether it
   is unlocked. A locked stage is dimmed and not clickable. */
.adv-block, .shop-block {
  margin: 10px 0 0;
  padding: 10px;
  background: rgba(8, 11, 26, .45);
  border: 1px solid var(--stroke);
  border-radius: 12px;
}
.adv-head, .shop-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 8px; margin-bottom: 8px;
}
.adv-title, .shop-title { font-size: 12px; font-weight: 800; letter-spacing: .5px; color: var(--text); }
.adv-money {
  font-size: 12px; font-weight: 800; color: var(--gold);
  text-shadow: 0 0 12px rgba(255, 201, 60, .4);
}
.shop-price { font-size: 11px; font-weight: 700; color: var(--muted); }
.adv-stages { display: flex; flex-wrap: wrap; gap: 6px; }
.adv-stage {
  flex: 1 1 calc(33.333% - 6px); min-width: 92px;
  display: flex; flex-direction: column; align-items: flex-start; gap: 2px;
  padding: 7px 8px; cursor: pointer; text-align: left;
  background: rgba(8, 11, 26, .55);
  border: 1px solid var(--stroke);
  border-radius: 10px;
  color: var(--muted);
  font: inherit; font-size: 11px; font-weight: 700;
  transition: border-color .15s, background .15s, color .15s, transform .1s;
}
.adv-stage:hover:not(:disabled) { border-color: rgba(124, 92, 255, .55); color: var(--text); }
.adv-stage:active:not(:disabled) { transform: scale(.97); }
.adv-stage:disabled { opacity: .4; cursor: not-allowed; }
.adv-stage.is-cleared { border-color: rgba(80, 220, 140, .55); }
.adv-stage-num { font-size: 10px; letter-spacing: 1px; color: var(--muted); }
.adv-stage-name { font-size: 12px; color: var(--text); }
.adv-stage-meta { font-size: 10px; color: var(--gold); }
.adv-stage.is-cleared .adv-stage-meta { color: #6ee7a8; }
.adv-note {
  margin-top: 8px; padding: 6px 8px;
  font-size: 11px; line-height: 1.4; color: #ffd479;
  background: rgba(255, 180, 60, .1);
  border: 1px solid rgba(255, 180, 60, .3);
  border-radius: 8px;
}
.shop-classes { display: flex; flex-wrap: wrap; gap: 6px; }
.shop-class {
  flex: 1 1 calc(33.333% - 6px); min-width: 92px;
  display: flex; flex-direction: column; align-items: flex-start; gap: 2px;
  padding: 7px 8px; cursor: pointer; text-align: left;
  background: rgba(8, 11, 26, .55);
  border: 1px solid var(--stroke);
  border-radius: 10px;
  color: var(--muted);
  font: inherit; font-size: 11px; font-weight: 700;
  transition: border-color .15s, background .15s, color .15s, transform .1s;
}
.shop-class:hover:not(:disabled) { border-color: rgba(124, 92, 255, .55); color: var(--text); }
.shop-class:active:not(:disabled) { transform: scale(.97); }
.shop-class:disabled { cursor: default; }
.shop-class.is-owned {
  border-color: rgba(80, 220, 140, .55);
  background: rgba(80, 220, 140, .1);
  color: var(--text);
}
.shop-class-name { font-size: 12px; color: var(--text); }
.shop-class-tag { font-size: 10px; color: var(--muted); }
.shop-class.is-owned .shop-class-tag { color: #6ee7a8; }
.room-code {
  font-family: ui-monospace, Menlo, Consolas, monospace;
  font-size: 18px; font-weight: 800; color: var(--gold);
  text-shadow: 0 0 14px rgba(255, 201, 60, .45);
}
#playerList { list-style: none; padding: 0; margin: 0 0 12px; }
#playerList li {
  display: flex; align-items: center; gap: 8px;
  padding: 7px 0; font-size: 13px;
  border-bottom: 1px solid rgba(140,160,255,.10);
}
#playerList li:last-child { border-bottom: none; }
#playerList li::before {
  content: ""; width: 10px; height: 10px; border-radius: 50%;
  background: var(--seat0); box-shadow: 0 0 8px currentColor;
}
#playerList li:nth-child(2)::before { background: var(--seat1); }
#playerList li:nth-child(3)::before { background: var(--seat2); }
#playerList li:nth-child(4)::before { background: var(--seat3); }
#startBtn { width: 100%; }

/* ============================ GAME SHELL ============================
   The 3D board fills the entire viewport; every UI block floats above
   it like a mobile game HUD. */
.game-shell {
  position: relative;
  height: 100%;
  height: 100dvh;
  width: 100%;
  min-width: 100%;
  background:
    radial-gradient(1100px 640px at 50% 8%, rgba(124, 92, 255, .20), transparent 62%),
    radial-gradient(900px 560px at 12% 88%, rgba(56, 232, 192, .12), transparent 60%),
    linear-gradient(170deg, #0d1226, #101634 60%, #0a0e20);
}

/* ---------------- Floating top strip ----------------
   Three INDEPENDENT groups (left / centre / right) laid over the board. The
   strip itself paints NOTHING — no background, border, radius or shadow — so
   the buttons inside it read as loose objects floating above the screen
   (req 9). Each group is sized to its own content, so the menu on the right is
   exactly as wide as its buttons and never a full-width bar (req 8). */
.top-bar {
  position: absolute; top: 8px; left: 8px; right: 8px;
  z-index: 60;
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 10px; padding: 0;
  background: none;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  border: none;
  border-radius: 0;
  box-shadow: none;
  pointer-events: none;
}
.top-left, .top-right { display: flex; align-items: center; gap: 6px; flex: 0 0 auto; min-width: 0; }
/* req 1 (round 16): the ROOM NUMBER and the STAGE NAME own the top-left corner.
   They used to be pushed right of the rail so the Profile card could sit at 0/0,
   which left the corner empty and the two most useful labels floating in the
   middle of the strip. The chips are now flush at the corner and the rail starts
   BELOW them (see --top-h / .player-rail top). */
.top-left { margin-left: 0; }
.top-right { justify-content: flex-end; }
/* req 1 (round 28): the announcement must sit at the TRUE centre of the screen.
   A flex `1 1 auto` middle column is only centred between the two side groups,
   so a wide menu on the right pushed the pill left of centre. The centre group is
   therefore taken out of the flex flow and pinned to the viewport's middle. */
.top-center {
  position: absolute;
  left: 50%;
  top: 0;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  max-width: min(56vw, 560px);
  pointer-events: none;
}

/* Event message: the running encounter / combat narration, top-centre. It is a
   single compact pill so it never grows into a bar. The template draws it with
   an image beside the words, so every announcement carries a round icon badge
   that is tinted by the kind of event (turn / combat / trap / loot / level). */
.event-msg {
  display: flex; align-items: center; gap: 8px;
  max-width: min(52vw, 520px);
  padding: 5px 14px 5px 6px;
  border-radius: 999px;
  font-size: 10.5px; line-height: 1.4; font-weight: 700;
  text-align: left;
  color: #eaf3ff;
  background: linear-gradient(180deg, rgba(22, 28, 58, .94), rgba(10, 14, 32, .94));
  border: 1px solid var(--stroke-hi);
  box-shadow: 0 10px 26px rgba(0,0,0,.5), inset 0 1px 0 rgba(255,255,255,.08);
  pointer-events: none;
  animation: popIn .3s cubic-bezier(.2,.9,.3,1.35);
}
/* req 2 (round 28): the dice-duel line sits DIRECTLY UNDER the announcement, in
   the same centred column, so the roll and the outcome read as one block. */
.duel-line {
  display: flex; align-items: center; gap: 7px;
  max-width: min(52vw, 520px);
  padding: 4px 12px 4px 5px;
  border-radius: 999px;
  font-size: 10px; line-height: 1.35; font-weight: 800;
  color: #eaf3ff;
  background: linear-gradient(180deg, rgba(30, 22, 52, .95), rgba(14, 10, 30, .95));
  border: 1px solid rgba(255, 209, 102, .45);
  box-shadow: 0 8px 22px rgba(0,0,0,.5), inset 0 1px 0 rgba(255,255,255,.08);
  pointer-events: none;
  animation: popIn .3s cubic-bezier(.2,.9,.3,1.35);
}
.duel-line.hidden { display: none; }
.duel-line .dl-icon {
  flex: 0 0 auto;
  width: 22px; height: 22px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 50%;
  font-size: 12px; line-height: 1;
  background: radial-gradient(circle at 50% 30%, #ffd166, #b06a00);
  border: 1px solid rgba(255,255,255,.3);
  box-shadow: 0 0 0 3px rgba(255,209,102,.18), inset 0 1px 0 rgba(255,255,255,.35);
}
.duel-line .dl-text { flex: 1 1 auto; min-width: 0; }
.duel-line b { color: var(--gold); }
.event-msg b { color: var(--gold); }
.event-msg.hidden { display: none; }
/* The icon badge: a lit disc with a soft halo in the event's own colour. */
.event-msg .em-icon {
  flex: 0 0 auto;
  width: 26px; height: 26px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 50%;
  font-size: 13px; line-height: 1;
  color: #fff;
  background: radial-gradient(circle at 50% 30%, var(--em-hi, #7c5cff), var(--em-lo, #4b32c3));
  border: 1px solid rgba(255,255,255,.28);
  box-shadow: 0 0 0 3px var(--em-glow, rgba(124,92,255,.18)),
              0 4px 12px rgba(0,0,0,.45),
              inset 0 1px 0 rgba(255,255,255,.3);
  text-shadow: 0 1px 3px rgba(0,0,0,.5);
}
.event-msg .em-text {
  flex: 1 1 auto; min-width: 0;
  /* Two lines maximum: the pill must never grow into a paragraph. */
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
/* Per-kind tints. The glyph itself is chosen in showEventMessage(). */
.event-msg.k-turn   { --em-hi: #6fd3ff; --em-lo: #1f6fd0; --em-glow: rgba(111,211,255,.20); }
.event-msg.k-combat { --em-hi: #ff8fa3; --em-lo: #c3123a; --em-glow: rgba(255,92,138,.22); }
.event-msg.k-trap   { --em-hi: #ffc93c; --em-lo: #b06a00; --em-glow: rgba(255,201,60,.22); }
.event-msg.k-game   { --em-hi: #ffd166; --em-lo: #c98a00; --em-glow: rgba(255,209,102,.22); }
.event-msg.k-class  { --em-hi: #a58cff; --em-lo: #4b32c3; --em-glow: rgba(165,140,255,.22); }
.event-msg.k-heal   { --em-hi: #6ef0b0; --em-lo: #128a52; --em-glow: rgba(110,240,176,.22); }
.event-msg.k-level  { --em-hi: #ffe9a8; --em-lo: #d08a00; --em-glow: rgba(255,233,168,.24); }
.event-msg.k-info   { --em-hi: #9fb4e8; --em-lo: #3a4a86; --em-glow: rgba(159,180,232,.18); }

.chip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 12px; border-radius: 999px;
  font-size: 11.5px; font-weight: 700;
  background: rgba(255,255,255,.07);
  border: 1px solid var(--stroke);
  white-space: nowrap;
  pointer-events: auto;
}
/* The menu buttons float over the board, so a translucent fill disappears
   against a bright tile. Every interactive chip gets a solid, dark surface with
   a lit rim so it always reads as a button (req 2). */
.chip-sound, .chip-surrender, .chip-leave, .chip-menu, .chip-fs {
  background: linear-gradient(180deg, rgba(46, 58, 108, .96), rgba(20, 26, 54, .96));
  border: 1px solid rgba(150, 175, 255, .55);
  color: #e8eeff;
  box-shadow: 0 4px 12px rgba(0,0,0,.45), inset 0 1px 0 rgba(255,255,255,.14);
  text-shadow: 0 1px 2px rgba(0,0,0,.6);
}
.chip-sound:hover, .chip-leave:hover, .chip-menu:hover, .chip-fs:hover {
  background: linear-gradient(180deg, rgba(62, 78, 140, .98), rgba(30, 38, 76, .98));
  border-color: rgba(190, 210, 255, .85);
  color: #fff;
}
/* The fullscreen toggle is a square icon button, so it must not inherit the
   text padding the word chips use. */
.chip-fs { display: inline-flex; align-items: center; justify-content: center; padding: 4px 7px; }
.chip-fs .chip-ico { width: 15px; height: 15px; display: block; }
/* req 6: the log toggle is an icon button like the fullscreen one. Its inline
   SVG carries no width/height of its own, so without this rule it laid out at
   0x0 and the button showed an empty pill with no glyph at all. */
.chip-menu { display: inline-flex; align-items: center; justify-content: center; padding: 4px 7px; }
.chip-menu .chip-ico { width: 15px; height: 15px; display: block; }
.chip-fs.is-on {
  background: linear-gradient(180deg, rgba(56, 232, 192, .9), rgba(20, 120, 100, .95));
  border-color: rgba(120, 255, 220, .9);
  color: #04231d;
}
.chip-room { font-family: ui-monospace, Menlo, Consolas, monospace; color: var(--gold); }
.chip-scene {
  background: linear-gradient(135deg, rgba(56,232,192,.28), rgba(124,92,255,.26));
  border-color: rgba(56,232,192,.5);
  color: #d9fff2;
}
.chip-surrender {
  cursor: pointer;
  /* req 1: the surrender chip shares the mute button's surface so the top-right
     row reads as one set of controls; only the GLYPH carries the meaning (a
     white flag). */
  background: linear-gradient(180deg, rgba(46, 58, 108, .96), rgba(20, 26, 54, .96));
  border: 1px solid rgba(150, 175, 255, .55);
  color: #e8eeff;
  font-family: inherit;
}
.chip-surrender:hover {
  background: linear-gradient(180deg, rgba(62, 78, 140, .98), rgba(30, 38, 76, .98));
  border-color: rgba(190, 210, 255, .85);
  color: #fff;
}
/* The white flag is an inline SVG so it renders identically everywhere (the
   emoji is missing or monochrome on many platforms). */
.chip-surrender .chip-ico {
  width: 13px; height: 13px; flex: 0 0 auto;
  color: #ffffff;
}
.chip-surrender.is-armed {
  background: linear-gradient(135deg, #ff5f6d, #ff8f98);
  border-color: transparent;
  color: #2a0509;
  font-weight: 900;
  animation: armedPulse .7s ease-in-out infinite alternate;
}
.chip-surrender.is-armed .chip-ico { color: #2a0509; }
@keyframes armedPulse { to { filter: brightness(1.18); } }

/* Leave the room / back to the lobby */
.chip-leave {
  cursor: pointer;
  /* req 2: same surface as the mute button, with a RED door glyph. */
  background: linear-gradient(180deg, rgba(46, 58, 108, .96), rgba(20, 26, 54, .96));
  border: 1px solid rgba(150, 175, 255, .55);
  color: #e8eeff;
  font-family: inherit;
}
.chip-leave:hover {
  background: linear-gradient(180deg, rgba(62, 78, 140, .98), rgba(30, 38, 76, .98));
  border-color: rgba(190, 210, 255, .85);
  color: #fff;
}
/* The door is drawn as an inline SVG tinted red, so the colour is exact on every
   platform instead of depending on the emoji font. */
.chip-leave .chip-ico {
  width: 13px; height: 13px; flex: 0 0 auto;
  color: #ff4d5e;
}
/* Upgrade + log buttons in the top bar */
.chip-upgrade, .chip-log {
  cursor: pointer; font-family: inherit;
  background: rgba(56, 232, 192, .16);
  border-color: rgba(56, 232, 192, .5);
  color: #b8ffe9;
}
/* Drawer + log-sheet toggles only exist on the phone layout. */
.chip-log { display: none; }
.chip-upgrade:hover, .chip-log:hover { background: rgba(56, 232, 192, .3); color: #fff; }
.chip-upgrade.has-points {
  background: linear-gradient(135deg, #2fd39b, #52efc0);
  border-color: transparent; color: #06251b; font-weight: 900;
  animation: armedPulse .8s ease-in-out infinite alternate;
}
.chip-upgrade .up-badge {
  display: inline-block; margin-left: 5px;
  min-width: 16px; padding: 0 4px;
  border-radius: 999px; font-size: 9.5px; font-weight: 900;
  background: #06251b; color: #52efc0;
}

/* Floating CRITICAL! tag for critical hits */
.crit-float {
  position: absolute; z-index: 12;
  transform: translate(-50%, -50%);
  pointer-events: none;
  font-size: 15px; font-weight: 900; letter-spacing: 1.2px;
  color: #ffe066;
  text-shadow: 0 0 12px rgba(255, 201, 60, .95), 0 2px 4px rgba(0,0,0,.85);
  animation: critPop 1.15s cubic-bezier(.2,.9,.3,1.3) forwards;
}
@keyframes critPop {
  0% { opacity: 0; transform: translate(-50%, -30%) scale(.6); }
  18% { opacity: 1; transform: translate(-50%, -60%) scale(1.18); }
  70% { opacity: 1; transform: translate(-50%, -110%) scale(1); }
  100% { opacity: 0; transform: translate(-50%, -150%) scale(.92); }
}
.dmg-popup.crit { font-size: 18px; font-weight: 900; }
.battle-log .blocked { color: #9fb4e8; }

/* Trap tag floating above a trap tile */
.hud-label.trap-tag {
  display: flex; align-items: center; gap: 4px;
  padding: 2px 7px;
  border-radius: 999px;
  font-size: 9.5px; font-weight: 900;
  color: #fff;
  background: rgba(8, 11, 26, .86);
  border: 1px solid var(--trap-color, #ff8f3c);
  box-shadow: 0 0 12px color-mix(in srgb, var(--trap-color, #ff8f3c) 60%, transparent);
  pointer-events: none;
  white-space: nowrap;
}
.hud-label.trap-tag .trap-own { color: var(--trap-color, #ff8f3c); }

/* ---------- Turn countdown ----------
   The countdown lives INSIDE the bottom-centre turn dock, directly above the
   ทอย / จบตา slot. It is a static flex row (not a fixed overlay) so it can never
   drift over the board: the track fills the row and the seconds sit beside it. */
.turn-timer-bar {
  position: relative;
  display: flex; align-items: center; gap: 5px;
  width: 100%; min-width: 96px;
  pointer-events: none;
}
/* The track is the outer pill; the fill is the <i> nested inside .tt-track. */
.turn-timer-bar .tt-track {
  flex: 1 1 auto; min-width: 0;
  height: 7px;
  border-radius: 999px;
  background: rgba(8, 11, 26, .78);
  border: 1px solid var(--stroke);
  overflow: hidden;
}
.turn-timer-bar .tt-track > i {
  display: block; height: 100%; width: 100%;
  border-radius: 999px;
  background: linear-gradient(90deg, #38e8c0, #7cffdc);
  transition: width .2s linear, background .3s;
}
/* The seconds readout sits beside the track, never on top of it. */
.turn-timer-bar > span {
  flex: 0 0 auto;
  font-size: 9.5px; font-weight: 900; color: #eaf3ff;
  min-width: 26px; padding: 1px 5px;
  border-radius: 999px;
  background: rgba(8, 11, 26, .9);
  border: 1px solid var(--stroke);
  text-align: center;
  letter-spacing: .3px;
}
.turn-timer-bar.is-urgent .tt-track > i { background: linear-gradient(90deg, #ff5f6d, #ff9aa3); }
.turn-timer-bar.is-urgent > span { color: #ffb3ba; }
.turn-timer-bar.is-mine .tt-track { border-color: rgba(255, 201, 60, .55); }
.chip-turn .dot {
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--good); box-shadow: 0 0 10px var(--good);
  animation: pulseDot 1.5s ease-in-out infinite;
}
@keyframes pulseDot { 50% { opacity: .35; transform: scale(.8); } }
.chip-phase {
  background: linear-gradient(135deg, rgba(124,92,255,.34), rgba(255,92,138,.30));
  border-color: var(--stroke-hi);
  letter-spacing: 1.4px; font-size: 10.5px;
}
.status-pill {
  padding: 6px 16px; border-radius: 999px;
  font-size: 12.5px; font-weight: 700;
  background: rgba(255,255,255,.07);
  border: 1px solid var(--stroke);
  box-shadow: var(--shadow-sm);
  white-space: nowrap;
  text-align: center;
}

/* ---------------- Stage (full-screen board area) ---------------- */
.stage {
  position: fixed; inset: 0;
  overflow: hidden;
  padding: 0;
}
/* Soft vignette so the HUD reads like a mobile game full-screen. */
.stage::after {
  content: ""; position: absolute; inset: 0; z-index: 5;
  pointer-events: none;
  background:
    radial-gradient(120% 90% at 50% 45%, transparent 55%, rgba(4, 6, 18, .42) 100%);
}
.board {
  position: absolute; inset: 0;
  background: #070b18;
}
/* The board canvas must always fill its container */
.board > canvas {
  position: absolute; inset: 0;
  width: 100% !important; height: 100% !important; display: block;
  z-index: 1;
}

/* Live HUD nameplate above every token: name + HP + SP bars */
.hud-label {
  position: absolute; z-index: 9;
  transform: translate(-50%, -50%);
  pointer-events: auto;
  cursor: pointer;
  text-align: center;
  font-weight: 800;
  font-size: 10.5px;
  color: #fff;
  text-shadow: 0 1px 4px rgba(0,0,0,.9), 0 0 8px rgba(0,0,0,.6);
  white-space: nowrap;
  /* The transition smooths a token WALKING from tile to tile. While the player
     spins or zooms the camera the label has to track the projection exactly, so
     the transition is switched off (see body.is-cam-moving below) — otherwise
     every nameplate trails the board by up to 0.4s and the whole HUD looks like
     it is sliding behind the model. */
  transition: left .4s cubic-bezier(.2,.9,.3,1), top .4s cubic-bezier(.2,.9,.3,1);
  padding: 2px 6px 3px;
  border-radius: 9px;
  background: rgba(8, 11, 26, .62);
  border: 1px solid rgba(255,255,255,.10);
}
/* Set by the render loop for as long as the camera is moving. */
body.is-cam-moving .hud-label { transition: none; }
.hud-label .hud-name { opacity: .96; }
.hud-label.is-self { border-color: rgba(124, 92, 255, .75); }
.hud-label .hud-bars {
  display: flex; flex-direction: column; gap: 2px; margin-top: 2px;
}
.hud-label .hud-bar {
  position: relative;
  width: 46px; height: 4px; margin: 0 auto;
  border-radius: 3px;
  background: rgba(0,0,0,.62);
  overflow: hidden;
  box-shadow: inset 0 0 0 1px rgba(255,255,255,.10);
}
.hud-label .hud-bar > i {
  position: absolute; inset: 0 auto 0 0;
  display: block; height: 100%;
  border-radius: 3px;
  transition: width .35s ease;
}
.hud-label .hud-bar.hp > i { background: linear-gradient(90deg, #ff5f6d, #ff9aa3); }
.hud-label .hud-bar.sp > i { background: linear-gradient(90deg, #7c5cff, #a58cff); }
.hud-label.dead { filter: grayscale(.9) opacity(.6); }

/* Monster level badge + HP bar floating above each monster */
.hud-label.monster {
  z-index: 10;
  display: flex; flex-direction: column; align-items: center; gap: 3px;
}
.hud-label.monster .hud-lv {
  padding: 1px 8px;
  border-radius: 999px;
  font-size: 10px; font-weight: 900; letter-spacing: .5px;
  color: #fff;
  background: rgba(8, 11, 26, .82);
  border: 1px solid rgba(255, 255, 255, .28);
  box-shadow: 0 2px 8px rgba(0,0,0,.55);
}
.hud-label.monster .hud-bar {
  position: relative;
  width: 52px; height: 6px; margin: 0 auto;
  border-radius: 4px;
  background: rgba(0,0,0,.72);
  overflow: hidden;
  box-shadow: inset 0 0 0 1px rgba(255,255,255,.22), 0 2px 6px rgba(0,0,0,.5);
}
.hud-label.monster .hud-bar > i {
  position: absolute; inset: 0 auto 0 0;
  display: block; height: 100%;
  border-radius: 4px;
  background: linear-gradient(90deg, #ff3d5f, #ff8f6b);
  transition: width .35s ease;
}
.hud-label.monster[data-tier="low"] .hud-lv { border-color: #63e6a0; color: #b6ffd8; }
.hud-label.monster[data-tier="mid"] .hud-lv { border-color: #ffa53c; color: #ffd9a3; }
.hud-label.monster[data-tier="high"] .hud-lv { border-color: #ff4d6d; color: #ffc0cb; }

/* ---------- Focused profile (a card in the left column) ---------- */
/* req 3 (round 20): the opponent / monster profile is a card in the left-hand
   column, directly under the Status card, so the column reads as one stack:
   Profile → Status → opponent. */
.focus-profile {
  /* req 3 (round 20): the opponent's profile is a CARD IN THE LEFT COLUMN, not a
     floating panel. It sits directly under the Status card (or under the Profile
     card when the Status card is hidden), so the left edge reads as one stack:
     Profile → Status → opponent. It used to be positioned beside the rail, which
     put it over the middle of the board. */
  position: relative;
  top: auto; left: auto;
  flex: 0 0 auto;
  width: 100%;
  min-width: 0; max-width: none;
  padding: 8px 10px 9px;
  background: linear-gradient(160deg, rgba(30, 26, 62, .94), rgba(13, 17, 38, .94));
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  border: 1px solid var(--stroke-hi);
  border-radius: 14px;
  box-shadow: 0 12px 30px rgba(0,0,0,.5), inset 0 1px 0 rgba(255,255,255,.08);
  animation: focusIn .28s cubic-bezier(.2,.9,.3,1.3);
  pointer-events: auto;
  /* The rail clips its children, so the card must be able to shrink instead of
     pushing the column past the bottom of the screen. */
  /* req 6 (round 25): on a short screen the card was taller than the room left
     under the Status card, and the rail's `overflow: hidden` simply cut the
     bottom off — the last stat row and the skill chips were unreachable. It now
     shrinks (flex 0 1 auto + min-height 0) and scrolls its own content, so every
     row can be reached on a phone. */
  flex: 0 1 auto;
  min-height: 0;
  max-height: 100%;
  overflow-y: auto;
  overflow-x: hidden;
  /* req 6 (round 25): the scrollbar must be VISIBLE, not an overlay that only
     appears while dragging — the player has to see that there is more below. */
  scrollbar-width: thin;
  scrollbar-color: rgba(150,170,255,.55) rgba(255,255,255,.06);
  overscroll-behavior: contain;
}
.focus-profile::-webkit-scrollbar { width: 7px; }
.focus-profile::-webkit-scrollbar-track {
  background: rgba(255,255,255,.06); border-radius: 4px;
}
.focus-profile::-webkit-scrollbar-thumb {
  background: rgba(150,170,255,.55); border-radius: 4px;
}
.focus-profile::-webkit-scrollbar-thumb:hover { background: rgba(150,170,255,.8); }
/* req 3 (round 20): the card matches the width of whatever it sits under. Both
   the Profile and the Status card are the full rail width now (req 1, round 21),
   so the opponent card is too — the left column has one clean edge. */
@keyframes focusIn {
  from { opacity: 0; transform: translateY(-12px) scale(.96); }
}
.fp-head { display: flex; align-items: center; gap: 9px; margin-bottom: 7px; }
.fp-badge {
  width: 34px; height: 34px; border-radius: 10px; flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center;
  font-size: 12px; font-weight: 900; color: #0d1226;
  background: linear-gradient(135deg, #ffe08a, var(--gold));
  box-shadow: 0 4px 14px rgba(255,201,60,.35);
}
/* req 5: the focused profile badge carries the class weapon puck. */
.fp-badge .icon3d { width: 28px; height: 28px; }
.fp-badge.monster { background: linear-gradient(135deg, #ff8fa3, var(--bad)); color: #2a0509; }
.fp-name { font-size: 14px; font-weight: 900; line-height: 1.15; }
.fp-sub { font-size: 10px; color: var(--muted); font-weight: 700; }
.fp-close {
  margin-left: auto; width: 22px; height: 22px; border-radius: 7px;
  background: rgba(255,255,255,.08); font-size: 11px; line-height: 1;
  flex: 0 0 auto;
}
.fp-close:hover { background: rgba(255,95,109,.3); }
.fp-bar-row { display: flex; align-items: center; gap: 7px; margin: 3px 0; }
.fp-bar-row .lbl { width: 20px; font-size: 9px; font-weight: 900; color: var(--muted); }
.fp-bar-row .val { font-size: 10px; font-weight: 800; min-width: 58px; }
.fp-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 4px; margin-top: 7px; }
.fp-stats div {
  background: rgba(0,0,0,.32); border-radius: 6px;
  padding: 3px 2px; text-align: center;
  font-size: 8.5px; font-weight: 800; color: var(--muted);
}
.fp-stats b { display: block; color: var(--text); font-size: 10px; }
.fp-skills { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 7px; }
/* One puck per skill: the 3D icon plus its level, nothing else. */
.fp-skill {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 2px 7px 2px 3px; border-radius: 999px;
  background: rgba(255,255,255,.06);
  border: 1px solid rgba(160,180,255,.18);
}
.fp-skill .sk-icon, .fp-skill .icon3d { width: 18px; height: 18px; }
.fp-skill b { font-size: 9.5px; font-weight: 900; color: var(--gold); letter-spacing: .3px; }
.fp-skill.off { opacity: .42; }
.fp-skill.off b { color: var(--muted); }

/* Skill hover tooltip: what it does now + what the next level would add. */
.skill-tip {
  position: fixed; z-index: 90;
  width: 240px; max-width: 42vw;
  padding: 9px 11px 10px;
  background: rgba(16, 20, 44, .97);
  border: 1px solid rgba(150, 170, 255, .35);
  border-radius: 12px;
  box-shadow: 0 14px 34px rgba(0,0,0,.55);
  pointer-events: none;
  font-size: 10.5px; color: var(--text);
}
.skill-tip.hidden { display: none; }
.skill-tip .st-head {
  display: flex; align-items: center; gap: 6px;
  font-size: 11.5px; font-weight: 900; margin-bottom: 5px;
}
.skill-tip .st-head .icon3d { width: 20px; height: 20px; }
.skill-tip .st-head b {
  margin-left: auto; font-size: 9.5px; color: #06251b;
  background: linear-gradient(135deg, #2fd39b, #52efc0);
  padding: 1px 7px; border-radius: 999px;
}
.skill-tip .st-row { display: flex; justify-content: space-between; font-size: 10px; color: var(--muted); }
.skill-tip .st-row b { color: var(--gold); }
.skill-tip .st-now { margin-top: 6px; color: #d7e2ff; line-height: 1.45; }
.skill-tip .st-next {
  margin-top: 5px; padding-top: 5px;
  border-top: 1px dashed rgba(150,170,255,.28);
  color: #b8ffe9; line-height: 1.45;
}

/* While the 3D chooser is open, dim the DOM nameplates so the cards read clearly. */
body.choosing .hud-label { opacity: .18; pointer-events: none; }

/* ---------- Target-selection hint ---------- */
.target-hint {
  position: absolute; left: 50%; bottom: calc(var(--dock-h, 0px) + 22px);
  transform: translateX(-50%);
  z-index: 66;
  padding: 8px 18px;
  border-radius: 999px;
  font-size: 12px; font-weight: 800;
  color: #0d1226;
  background: linear-gradient(135deg, #ffe08a, var(--gold));
  box-shadow: 0 10px 28px rgba(255,201,60,.42);
  animation: hintPulse 1.5s ease-in-out infinite;
  pointer-events: none;
  white-space: nowrap;
}
@keyframes hintPulse {
  50% { box-shadow: 0 10px 34px rgba(255,201,60,.72); }
}
/* req 2 (round 19): the target picker. A list of every legal target, nearest
   first, so an attack never depends on hitting a small figure on the board.
   It sits above the bottom bar and is scrollable, because a ranged class can
   have three targets and a phone has very little height to spare. */
.target-picker {
  position: fixed;
  /* req 1 (round 24): the picker occupies the SAME SLOT as the Status card —
     the second card of the left rail, directly under the Profile card — and
     overlays it while it is open. The player asked for the target list to
     appear exactly where the Status card lives, so the two never fight over
     the left column and the list is never somewhere new.
     left / top / width / max-height are written by positionTargetPicker() from
     the live rail geometry, because the rail's top depends on the measured top
     strip (--top-h) and the Profile card's height is content-sized. */
  left: 0;
  top: 0;
  right: auto;
  bottom: auto;
  margin-inline: 0;
  /* Above the rail (z-index 60) so it covers the Status card it replaces. */
  z-index: 168;
  width: var(--rail-w, 244px);
  max-height: 60vh;
  display: flex;
  flex-direction: column;
  border-radius: 14px;
  background: linear-gradient(180deg, rgba(24,32,64,.98), rgba(12,16,36,.98));
  border: 1px solid rgba(255,201,60,.42);
  box-shadow: 0 18px 44px rgba(0,0,0,.6), 0 0 0 1px rgba(255,255,255,.05) inset;
  overflow: hidden;
}
.target-picker.hidden { display: none; }
.tp-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 8px; padding: 6px 8px 5px;
  border-bottom: 1px solid rgba(255,255,255,.10);
}
.tp-title {
  font-size: 11px; font-weight: 800; color: var(--gold);
  /* One line only: the panel is as wide as the Profile card, so a wrapped title
     would eat the height the first target card needs. */
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.tp-close {
  width: 22px; height: 22px; flex: 0 0 auto;
  border-radius: 7px; border: 1px solid rgba(255,255,255,.18);
  background: rgba(255,255,255,.06); color: #dfe6ff;
  font-size: 11px; line-height: 1; cursor: pointer;
}
.tp-close:hover { background: rgba(255,255,255,.14); }
.tp-list { overflow-y: auto; padding: 5px; display: flex; flex-direction: column; gap: 5px; }
/* req 1 (round 22): each candidate is a PROFILE CARD, reusing the opponent
   profile's own classes (.fp-head / .fp-badge / .fp-bar-row / .fp-stats) so the
   two read identically. The card is a button, so it needs the reset below. */
.tp-card {
  display: block;
  width: 100%;
  padding: 6px 8px 7px;
  border-radius: 10px;
  text-align: left;
  font-family: inherit;
  color: var(--text);
  background: linear-gradient(160deg, rgba(30, 26, 62, .96), rgba(13, 17, 38, .96));
  border: 1px solid var(--stroke-hi);
  border-left: 3px solid var(--tp-accent, var(--accent));
  cursor: pointer;
  transition: transform .12s, border-color .18s, filter .18s;
}
.tp-card:hover, .tp-card:focus-visible {
  transform: translateY(-1px);
  filter: brightness(1.14);
  border-color: rgba(255, 201, 60, .8);
  outline: none;
}
.tp-card.is-near { border-color: rgba(126, 224, 160, .55); }
.tp-card .tp-id { min-width: 0; flex: 1 1 auto; }
.tp-card .fp-head { gap: 6px; margin-bottom: 4px; }
.tp-card .fp-badge { width: 24px; height: 24px; border-radius: 8px; }
.tp-card .fp-name { font-size: 11.5px; }
.tp-card .fp-sub { font-size: 8.5px; }
.tp-card .fp-bar-row { gap: 5px; margin: 2px 0; }
.tp-card .fp-bar-row .lbl { font-size: 7.5px; }
.tp-card .fp-bar-row .val { font-size: 8px; min-width: 30px; }
.tp-card .fp-bar-row .bar { height: 4px; }
.tp-card .fp-stats { margin-top: 4px; grid-template-columns: repeat(3, 1fr); gap: 2px; }
.tp-card .fp-stats div { font-size: 7px; padding: 1px; border-radius: 4px; }
.tp-card .fp-stats b { font-size: 8px; }
.tp-bot {
  font-size: 8px; font-weight: 800; vertical-align: middle;
  padding: 1px 4px; border-radius: 999px;
  background: rgba(255,255,255,.14); color: var(--muted);
}
/* The distance badge, so "which one can I actually reach" is readable without
   counting tiles. */
.tp-dist {
  flex: 0 0 auto; margin-left: auto;
  padding: 1px 6px; border-radius: 999px;
  font-size: 8px; font-weight: 900;
  background: rgba(255,255,255,.10); color: var(--muted);
}
.tp-dist.is-near { background: rgba(126, 224, 160, .22); color: #7ee0a0; }
/* While picking, valid targets glow so it is obvious what can be clicked. */
.hud-label.targetable {
  outline: 2px solid var(--gold);
  outline-offset: 2px;
  animation: targetablePulse 1.1s ease-in-out infinite;
}
@keyframes targetablePulse {
  50% { outline-color: rgba(255,201,60,.25); }
}


/* ---------------- Floating party panel (left edge) ---------------- */
.player-rail {
  position: absolute; left: 12px; top: 76px;
  bottom: calc(var(--dock-h, 0px) + 18px);
  width: 244px; z-index: 50;
  /* The gap between Profile and Status is half of what it used to be (8px ->
     4px), and the same 4px is used between Status and the Log below, so the
     three left-hand regions read as one evenly spaced column. */
  display: flex; flex-direction: column; gap: var(--rail-gap, 4px);
  pointer-events: none;
  /* The rail itself never scrolls: each panel below scrolls on its own so the
     profile, status and skill lists never scroll away from each other. */
  overflow: hidden;
  padding-right: 2px;
}
/* One scrollable panel inside the rail. flex-basis 0 + min-height 0 lets the
   four panels share the height predictably. */
.rail-panel {
  pointer-events: auto;
  display: flex; flex-direction: column;
  flex: 1 1 0; min-height: 0;
}
.rail-panel > .hub-body,
.rail-panel > .pc-scroll {
  flex: 1 1 auto; min-height: 0;
  overflow-y: auto; overflow-x: hidden;
  scrollbar-width: thin;
}
/* The profile card holds identity + vitals and scrolls on its own too. */
#selfProfile { flex: 0 1 auto; max-height: 42%; overflow-y: auto; scrollbar-width: thin; }
#selfProfile::-webkit-scrollbar { width: 5px; }
#selfProfile::-webkit-scrollbar-thumb { background: rgba(150,170,255,.34); border-radius: 3px; }
#statusCard  { flex: 0 0 auto; min-height: 0; }
/* req 5 (round 18) and req 2 (round 21): the Status card must never show a
   scrollbar. It is content-sized now — three rows of two stats — so there is
   nothing to scroll and nothing to divide up. */
#statusCard > .hub-body { overflow: hidden; }
#statusCard #statusList { height: auto; }
#statusCard .up-stats { height: auto; }
#skillCard   { flex: 1.6 1 0; min-height: 96px; }
#itemCard    { flex: 1 1 0; min-height: 64px; max-height: 26%; }
.rail-panel > .hub-body::-webkit-scrollbar { width: 5px; }
.rail-panel > .hub-body::-webkit-scrollbar-thumb {
  background: rgba(150,170,255,.34); border-radius: 3px;
}
/* The old rail-card sizing is gone: each panel now uses .rail-panel (own
   scrollbar). Kept as a no-op base so existing markup still renders. */
.rail-card {
  pointer-events: auto;
  min-height: 0;
}
/* Inside the rail every card manages its own scroll (see .rail-panel). */
.hub-spacer { pointer-events: none; }
.hub-tag {
  float: right;
  padding: 1px 7px; border-radius: 999px;
  font-size: 9px; font-weight: 900; letter-spacing: 0;
  background: linear-gradient(135deg, #2fd39b, #52efc0);
  color: #06251b;
}
.hub-tag.hidden { display: none; }

/* ---- Buff / ward badges after the class name (req 2) ---- */
.pc-buffs {
  display: inline-flex; flex-wrap: wrap; gap: 3px;
  margin-left: 5px; vertical-align: middle;
}
.pc-buff {
  display: inline-flex; align-items: center; gap: 2px;
  padding: 1px 5px; border-radius: 999px;
  font-size: 8px; font-weight: 900; letter-spacing: .2px;
  background: linear-gradient(135deg, rgba(56,232,192,.28), rgba(56,232,192,.14));
  border: 1px solid rgba(56,232,192,.55);
  color: #a8f7e4;
  white-space: nowrap;
}
.pc-buff b { font-size: 8px; opacity: .85; margin-left: 1px; }
.pc-buff-ico { font-style: normal; font-size: 8px; line-height: 1; }
/* A ward comes from an item, so it reads gold rather than green. */
.pc-buff.is-ward {
  background: linear-gradient(135deg, rgba(255,201,60,.26), rgba(255,201,60,.12));
  border-color: rgba(255,201,60,.6);
  color: #ffe6a3;
}

/* An encounter is waiting for this player's choice: pulse the panel so the
   Fight / Flee buttons are impossible to miss (จบตา is disabled by design). */
.hub-card--skills.needs-choice {
  border-color: var(--gold);
  box-shadow: 0 0 0 2px rgba(255, 201, 60, .28), 0 12px 30px rgba(255, 201, 60, .22);
  animation: needsChoicePulse 1.4s ease-in-out infinite alternate;
}
@keyframes needsChoicePulse {
  to { box-shadow: 0 0 0 5px rgba(255, 201, 60, .10), 0 14px 34px rgba(255, 201, 60, .34); }
}

/* ---------------- Status upgrade rows (inside the profile card) ------- */
.up-head-row {
  display: flex; align-items: center; justify-content: space-between;
  margin: 7px 0 4px;
  padding-top: 6px;
  border-top: 1px solid rgba(140,160,255,.18);
  font-size: 9px; font-weight: 900; letter-spacing: .8px;
  color: var(--muted); text-transform: uppercase;
}
.sp-pill {
  padding: 1px 7px; border-radius: 999px;
  font-size: 9px; font-weight: 900; letter-spacing: 0;
  background: rgba(255,255,255,.09); color: var(--muted);
}
.sp-pill.has { background: linear-gradient(135deg, #2fd39b, #52efc0); color: #06251b; }
.up-stats { display: grid; grid-template-columns: 1fr 1fr; gap: 4px; }
.up-stat-row {
  display: grid;
  grid-template-columns: 30px 1fr auto 22px;
  align-items: center; gap: 4px;
  padding: 1px 2px;
  min-width: 0;
}
/* req 3 (round 18): the Status card's open/close button lives in the Profile
   card's header, so the two cards are controlled from one place. */
.pc-status-toggle {
  position: relative;
  margin-left: auto; flex: 0 0 auto;
  width: 22px; height: 22px; padding: 0;
  display: flex; align-items: center; justify-content: center;
  border-radius: 7px;
  background: rgba(255,255,255,.08);
  border: 1px solid var(--stroke);
  color: var(--muted);
}
.pc-status-toggle svg { width: 13px; height: 13px; }
.pc-status-toggle:hover { background: rgba(255,255,255,.16); color: var(--text); }
.pc-status-toggle.is-open {
  background: linear-gradient(160deg, #2fd39b, #14a37a);
  border-color: transparent; color: #04231a;
}
.pc-status-toggle .pst-dot {
  position: absolute; top: -5px; right: -5px;
  min-width: 13px; height: 13px; padding: 0 3px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 999px;
  font-size: 8px; font-weight: 900; line-height: 1;
  color: #04231a;
  background: linear-gradient(160deg, #ffe08a, var(--gold));
  box-shadow: 0 2px 6px rgba(0,0,0,.5);
}
.up-stat-row .us-label { font-size: 9.5px; font-weight: 900; color: var(--text); }
.up-stat-row .us-val  { font-size: 9.5px; font-weight: 800; color: var(--gold); text-align: right; }
.up-stat-row .us-hint { font-size: 8.5px; color: #7f8cb4; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.up-stat-row .us-plus {
  width: 20px; height: 20px; padding: 0; border-radius: 6px;
  font-size: 13px; font-weight: 900; line-height: 1;
  background: linear-gradient(160deg, #2fd39b, #14a37a);
  color: #04231a; border: none; cursor: pointer;
}
.up-stat-row .us-plus:disabled { opacity: .25; cursor: default; }
.corner-card.has-points { border-color: var(--accent3, #38e8c0); }

/* ---------------- Skill rows (left rail) ---------------- */
.skill-row {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 5px;
  align-items: stretch;
}
.skill-row > button:first-child { min-width: 0; }
.sk-up {
  flex: 0 0 auto;
  min-width: 34px; padding: 0 7px;
  border-radius: 10px;
  font-family: inherit; font-size: 10.5px; font-weight: 900;
  background: rgba(255,255,255,.08);
  border: 1px solid var(--stroke);
  color: var(--muted);
  cursor: pointer;
}
.sk-up.can {
  background: linear-gradient(160deg, #2fd39b, #14a37a);
  border-color: transparent; color: #04231a;
}
.sk-up:disabled { opacity: .3; cursor: default; }

.corner-card {
  position: relative;
  width: 100%;
  flex: 0 0 auto;
  padding: 9px 12px;
  background: var(--panel);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border: 1px solid var(--stroke);
  border-radius: 14px;
  box-shadow: var(--shadow-md);
  font-size: 10.5px;
  pointer-events: auto;
  transition: border-color .25s, box-shadow .25s;
}
.corner-card:empty { display: none; }
.corner-card.is-self {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent), 0 12px 34px rgba(124, 92, 255, .38);
}
.corner-card.is-active {
  border-color: var(--gold);
  box-shadow: 0 0 0 1px var(--gold), 0 12px 34px rgba(255, 201, 60, .28);
}

.pc-name {
  display: flex; align-items: center; gap: 6px;
  font-weight: 800; font-size: 12px; margin-bottom: 2px;
}
.pc-name .badge {
  margin-left: auto; padding: 1px 7px; border-radius: 999px;
  font-size: 9px; font-weight: 800; letter-spacing: 1px;
  background: rgba(255,255,255,.12); color: var(--muted);
}
.pc-meta { color: var(--muted); font-size: 9.5px; margin-bottom: 4px; }

/* ---------- decorated profile header (req 6) ----------
   The template's "Profile" region is not plain text: it reads as a game card.
   A class crest sits on the left, the name and class name stack beside it, and
   the LV / Lap figures are drawn as small gold and cyan plaques instead of a
   grey "Lv 3 · Lap 2" string. */
.pc-head {
  display: flex; align-items: center; gap: 7px;
  margin-bottom: 4px;
}
.pc-crest {
  flex: 0 0 auto;
  width: 30px; height: 30px;
  border-radius: 9px;
  display: flex; align-items: center; justify-content: center;
  font-size: 15px; line-height: 1;
  background: linear-gradient(150deg, rgba(255,255,255,.22), rgba(255,255,255,.04));
  border: 1px solid var(--stroke-hi);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.22), 0 4px 12px rgba(0,0,0,.4);
}
/* req 5: the crest is a weapon puck, so it fills the badge instead of sitting
   in it as a text glyph. */
.pc-crest .icon3d { width: 26px; height: 26px; }
.pc-head-text { flex: 1 1 auto; min-width: 0; }
.pc-head-text .pc-name { margin-bottom: 1px; }
.pc-class {
  font-size: 8.5px; font-weight: 800; letter-spacing: .4px;
  color: #b8c6ff; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
}
/* LV / Lap plaques: compact, coloured, and readable at 8px. */
.pc-plaques { display: flex; align-items: center; gap: 4px; margin-top: 3px; }
.pc-plaque {
  display: inline-flex; align-items: center; gap: 3px;
  padding: 1px 6px; border-radius: 999px;
  font-size: 7.5px; font-weight: 900; letter-spacing: .3px;
  white-space: nowrap;
}
.pc-plaque .pl-lbl { opacity: .78; font-size: 7px; }
.pc-plaque .pl-val { font-size: 9.5px; font-weight: 900; }
.pc-plaque.lv {
  background: linear-gradient(135deg, rgba(255,201,60,.28), rgba(255,201,60,.12));
  border: 1px solid rgba(255,201,60,.55);
  color: #ffe9a8;
}
.pc-plaque.lap {
  background: linear-gradient(135deg, rgba(56,232,192,.24), rgba(56,232,192,.10));
  border: 1px solid rgba(56,232,192,.5);
  color: #b8ffe9;
}
.pc-plaque.seat {
  background: rgba(255,255,255,.08);
  border: 1px solid var(--stroke);
  color: var(--muted);
}
.pc-row { display: flex; align-items: center; gap: 6px; margin: 3px 0; }
.pc-row .lbl { width: 20px; font-size: 9px; font-weight: 800; color: var(--muted); }
.pc-row .val { font-size: 10.5px; font-weight: 700; min-width: 46px; }
.bar {
  flex: 1; height: 7px; border-radius: 4px;
  background: rgba(0,0,0,.45); overflow: hidden;
  border: 1px solid rgba(255,255,255,.07);
}
.bar > div {
  height: 100%; border-radius: 4px;
  background: linear-gradient(90deg, var(--good), #7cf5b4);
  transition: width .35s cubic-bezier(.2,.9,.3,1);
}
.bar.sp > div { background: linear-gradient(90deg, var(--accent), #a58cff); }
.bar.exp > div { background: linear-gradient(90deg, #f5c451, #ffe9a8); }
/* GAME_SPEC §3.4: the EXP bar sits under the level line in the profile card. */
.pc-exp { display: flex; align-items: center; gap: 6px; margin: 3px 0; }
.pc-exp .lbl { width: 20px; font-size: 9px; font-weight: 800; color: var(--muted); }
.pc-exp .val { font-size: 10.5px; font-weight: 700; min-width: 46px; }
.pc-exp.is-max .val { color: #f5c451; }
.player-rail .pc-exp { margin: 2px 0; gap: 4px; }
.player-rail .pc-exp .lbl { width: 16px; font-size: 7.5px; }
.player-rail .pc-exp .val { font-size: 8.5px; min-width: 34px; }
.pc-stats {
  /* req 4 (round 25): all five stats on ONE row, each with its own icon. The
     grid used to be 4 columns, so CRIT wrapped onto a second row and the block
     read as a broken grid on a wide screen. */
  display: grid; grid-template-columns: repeat(5, 1fr); gap: 3px; margin-top: 5px;
}
.pc-stat {
  padding: 3px 2px; border-radius: 5px; text-align: center;
  background: rgba(0,0,0,.32); font-size: 8.5px; font-weight: 700;
  color: var(--muted);
  display: flex; flex-direction: column; align-items: center; gap: 1px;
}
/* The stat's own glyph, sized to the chip. */
.pc-stat .icon3d { --ico: 15px; width: 15px; height: 15px; }
.pc-stat .pcs-lbl { font-size: 7.5px; letter-spacing: .3px; line-height: 1; }
.pc-stat b { display: block; color: var(--text); font-size: 9.5px; line-height: 1.1; }
/* req 6: a stat that a buff is currently raising shows its TOTAL, with the
   unbuffed base struck through beside it. The accent colour marks it as
   temporary, so a player can tell a buffed 30% from a trained 30%. */
.pc-stat.is-buffed {
  background: rgba(56,232,192,.16);
  box-shadow: inset 0 0 0 1px rgba(56,232,192,.45);
}
.pc-stat.is-buffed b { color: var(--accent3, #38e8c0); }
.pc-stat.is-buffed s {
  display: block; font-size: 7.5px; font-weight: 700;
  color: #7f8cb4; text-decoration-thickness: 1px;
}
.pc-dead {
  margin-top: 5px; padding: 2px 0; border-radius: 6px; text-align: center;
  background: rgba(255,95,109,.18); color: var(--bad);
  font-weight: 800; font-size: 9.5px; letter-spacing: 1px;
}

/* ---------------- Right action column ---------------- */
.action-hub {
  position: absolute; right: 12px; top: 74px;
  bottom: calc(var(--dock-h, 0px) + 14px);
  width: 264px; z-index: 55;
  display: grid;
  /* dice · orbs · battle/log (fills the rest) */
  grid-template-rows: auto auto minmax(0, 1fr);
  gap: 8px;
  pointer-events: none;
  min-height: 0;
}

/* Dice + result */
.hub-dice {
  pointer-events: auto;
  display: flex; align-items: center; gap: 12px;
  padding: 10px 14px;
  background: linear-gradient(160deg, rgba(38, 30, 74, .82), rgba(18, 22, 48, .82));
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border: 1px solid rgba(255, 201, 60, .30);
  border-radius: 16px;
  box-shadow: 0 10px 28px rgba(0,0,0,.42), inset 0 1px 0 rgba(255,255,255,.10);
}
.dice3d {
  width: 54px; height: 54px;
  perspective: 340px;
  position: relative;
  flex: 0 0 auto;
}
.cube {
  width: 54px; height: 54px; position: relative;
  transform-style: preserve-3d;
  /* Start from an explicit transform so the very first throw also animates. */
  transform: rotateX(0deg) rotateY(0deg) rotateZ(0deg);
  /* NO CSS transition: the throw is driven frame-by-frame from JS (gravity,
     bounce, spin, settle), so a transition here would fight every write. */
  will-change: transform;
}
.face {
  --pip: #1a2040;
  position: absolute; width: 54px; height: 54px;
  border-radius: 11px;
  background-color: #f6f8ff;
  background-repeat: no-repeat;
  background-size: 26% 26%;
  background-image: radial-gradient(circle at center, var(--pip) 0 58%, transparent 60%);
  box-shadow:
    inset 0 0 0 1px rgba(255,255,255,.95),
    inset 0 -8px 14px rgba(96, 116, 186, .22),
    inset 0 8px 12px rgba(255,255,255,.75);
  backface-visibility: hidden;
}
/* Pip layouts */
.f1 { background-position: 50% 50%; }
.f2 { background-position: 27% 27%, 73% 73%;
      background-image:
        radial-gradient(circle at center, var(--pip) 0 58%, transparent 60%),
        radial-gradient(circle at center, var(--pip) 0 58%, transparent 60%); }
.f3 { background-position: 25% 25%, 50% 50%, 75% 75%;
      background-image:
        radial-gradient(circle at center, var(--pip) 0 58%, transparent 60%),
        radial-gradient(circle at center, var(--pip) 0 58%, transparent 60%),
        radial-gradient(circle at center, var(--pip) 0 58%, transparent 60%); }
.f4 { background-position: 27% 27%, 73% 27%, 27% 73%, 73% 73%;
      background-image:
        radial-gradient(circle at center, var(--pip) 0 58%, transparent 60%),
        radial-gradient(circle at center, var(--pip) 0 58%, transparent 60%),
        radial-gradient(circle at center, var(--pip) 0 58%, transparent 60%),
        radial-gradient(circle at center, var(--pip) 0 58%, transparent 60%); }
.f5 { background-position: 27% 27%, 73% 27%, 50% 50%, 27% 73%, 73% 73%;
      background-image:
        radial-gradient(circle at center, var(--pip) 0 58%, transparent 60%),
        radial-gradient(circle at center, var(--pip) 0 58%, transparent 60%),
        radial-gradient(circle at center, var(--pip) 0 58%, transparent 60%),
        radial-gradient(circle at center, var(--pip) 0 58%, transparent 60%),
        radial-gradient(circle at center, var(--pip) 0 58%, transparent 60%); }
.f6 { background-position: 27% 24%, 27% 50%, 27% 76%, 73% 24%, 73% 50%, 73% 76%;
      background-image:
        radial-gradient(circle at center, var(--pip) 0 58%, transparent 60%),
        radial-gradient(circle at center, var(--pip) 0 58%, transparent 60%),
        radial-gradient(circle at center, var(--pip) 0 58%, transparent 60%),
        radial-gradient(circle at center, var(--pip) 0 58%, transparent 60%),
        radial-gradient(circle at center, var(--pip) 0 58%, transparent 60%),
        radial-gradient(circle at center, var(--pip) 0 58%, transparent 60%); }
/* Face placement (opposite faces total 7) */
.f1 { transform: translateZ(27px); }
.f6 { transform: rotateX(180deg) translateZ(27px); }
.f2 { transform: rotateY(90deg) translateZ(27px); }
.f5 { transform: rotateY(-90deg) translateZ(27px); }
.f3 { transform: rotateX(-90deg) translateZ(27px); }
.f4 { transform: rotateX(90deg) translateZ(27px); }

/* soft shadow under the die */
.dice-shadow {
  position: absolute; left: 50%; bottom: -9px;
  width: 44px; height: 10px; transform: translateX(-50%);
  background: radial-gradient(ellipse at center, rgba(0,0,0,.55), transparent 70%);
  pointer-events: none;
}
/* The glow must sit on the perspective WRAPPER, never on .cube. A `filter` on an
   element with `transform-style: preserve-3d` forces it to `flat`, which
   collapses the six faces into one stacked plane — the die then shows only the
   first face (.f1, one pip) for the whole throw. */
.hub-dice.is-rolling .dice3d { filter: drop-shadow(0 6px 12px rgba(255,201,60,.35)); }
/* The shadow is driven from JS during the throw (it spreads as the die rises),
   so only the resting opacity is set here. */
.hub-dice.is-rolling .dice-shadow { opacity: .5; }
/* NOTE: there is deliberately no bounce keyframe on .dice3d any more. The throw
   is a real rigid-body simulation written to the cube's transform every frame,
   and a CSS animation on the parent would fight it (and hide the lift). */
@keyframes dicePulse { from { opacity: .5; } to { opacity: 1; } }
.hub-dice.is-rolling .dice-readout small { animation: dicePulse .45s ease-in-out infinite alternate; }

.dice-readout {
  flex: 1;
  font-size: 12px; font-weight: 800; color: var(--muted);
  letter-spacing: .4px;
  text-align: center;
  min-height: 18px;
  transition: color .2s;
}
.dice-readout b {
  display: block;
  font-size: 26px; line-height: 1.05;
  color: var(--gold);
  text-shadow: 0 0 18px rgba(255, 201, 60, .55);
}
.dice-readout small { font-size: 10px; font-weight: 700; opacity: .8; letter-spacing: 1px; }
.dice-readout.has-result { color: var(--muted); }

/* Roll / end turn orbs (they live in the bottom-centre turn dock) */
.orb {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  width: 84px; height: 84px; border-radius: 50%;
  font-weight: 800; color: #fff;
  border: 3px solid rgba(255,255,255,.35);
  transition: transform .14s cubic-bezier(.2,.9,.3,1.4), filter .2s, opacity .2s, box-shadow .2s;
}
.orb-glyph { font-size: 28px; line-height: 1; filter: drop-shadow(0 3px 7px rgba(0,0,0,.5)); }
.orb-text { font-size: 11px; letter-spacing: 1.5px; margin-top: 3px; }
.orb-roll {
  background: radial-gradient(circle at 34% 26%, #ffb3d2, var(--accent2) 50%, #c62a5c 100%);
  box-shadow: 0 14px 34px rgba(255, 92, 138, .58), inset 0 -9px 20px rgba(0,0,0,.30);
}
.orb-end {
  width: 70px; height: 70px;
  background: radial-gradient(circle at 34% 26%, #b3a6ff, var(--accent) 50%, #4a2ec9 100%);
  box-shadow: 0 12px 28px rgba(124, 92, 255, .52), inset 0 -7px 18px rgba(0,0,0,.30);
}
.orb:hover:not(:disabled) { transform: scale(1.08) translateY(-3px); filter: brightness(1.12); }
.orb:active:not(:disabled) { transform: scale(.96); }
.orb:disabled { opacity: .45; filter: grayscale(.55) brightness(.75); }
.orb.is-pulse { animation: orbPulse 1.7s ease-in-out infinite; }
@keyframes orbPulse {
  50% { box-shadow: 0 16px 46px rgba(255, 92, 138, .78), inset 0 -9px 20px rgba(0,0,0,.30), 0 0 0 10px rgba(255,92,138,.12); }
}

/* Skill / item cards in the action column */
.hub-card {
  pointer-events: auto;
  display: flex; flex-direction: column; min-height: 0;
  padding: 8px 10px 10px;
  background: rgba(14, 18, 42, .70);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border: 1px solid var(--stroke);
  border-radius: 14px;
  box-shadow: var(--shadow-sm);
  overflow: hidden;
}
/* Categories are compact and scroll internally: no percentage heights,
   because a percentage max-height against an auto grid row is unreliable. */
.hub-card--skills { max-height: min(360px, 42vh); }
/* Inside the left rail the cards stretch with the rail instead of the old
   fixed heights that were tuned for the right column. */
.player-rail .hub-card--skills,
.player-rail .hub-card--items { max-height: none; }
.hub-spacer { pointer-events: none; }
.hub-title {
  flex: 0 0 auto;
  font-size: 9.5px; font-weight: 800; letter-spacing: 1.4px;
  color: var(--muted); text-transform: uppercase;
  margin-bottom: 6px;
}
.hub-body { flex: 1 1 auto; overflow-y: auto; overflow-x: hidden; min-height: 0; }
.hub-body::-webkit-scrollbar { width: 5px; }
.hub-body::-webkit-scrollbar-thumb { background: rgba(150,170,255,.3); border-radius: 3px; }

/* ---------------- Battle HUD ---------------- */
/* The battle box is always visible at the bottom of the action column: while a
   fight runs it carries the fight header + HP bar, and underneath it always
   holds the full game log. */
.battle-hud {
  pointer-events: auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
}
.battle-hud-inner {
  display: flex; flex-direction: column; gap: 6px;
  min-height: 0;
  flex: 1 1 auto;
  padding: 9px 11px;
  background: rgba(20, 24, 50, .80);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border: 1px solid var(--stroke-hi);
  border-radius: 14px;
  box-shadow: 0 8px 24px rgba(0,0,0,.40);
}
.battle-hud.is-fighting .battle-hud-inner {
  background: rgba(36, 16, 30, .86);
  border-color: rgba(255, 122, 150, .45);
}
/* req 3 (round 22): the log has no header. The card is the history, so the
   inner panel is a single rounded surface with the lines running edge to edge. */
.battle-close {
  width: 22px; height: 22px; border-radius: 7px;
  background: rgba(255,255,255,.08); font-size: 11px; line-height: 1;
}
.battle-close:hover { background: rgba(255,95,109,.25); }
.battle-bar { height: 8px; flex: 0 0 auto; }
.battle-bar > div { background: linear-gradient(90deg, var(--bad), #ff9aa3); }
/* The log fills the rest of the box and scrolls on its own. */
.battle-log {
  flex: 1 1 auto;
  min-height: 70px;
  max-height: none;
  overflow-y: auto;
  overscroll-behavior: contain;
  font-size: 10.5px; line-height: 1.5;
  display: flex; flex-direction: column; gap: 2px;
  padding-right: 2px;
}
.battle-log::-webkit-scrollbar { width: 5px; }
.battle-log::-webkit-scrollbar-thumb { background: rgba(150,170,255,.3); border-radius: 3px; }
.battle-log .log-line { color: #d7e2ff; word-break: break-word; }
.battle-log .log-line .log-time { color: #6d7ba6; font-size: 9.5px; margin-right: 5px; }
.battle-log .log-line.k-combat { color: #ffc7cf; }
.battle-log .log-line.k-trap { color: #ffd9a3; }
.battle-log .log-line.k-item { color: #c9f3ff; }
.battle-log .log-line.k-turn { color: #9fb4e8; }
.battle-log .log-line.k-stat { color: #b8ffe9; }
.battle-log .log-line.k-death { color: #ff9aa3; font-weight: 800; }
.battle-log .log-line.k-game { color: #ffe9a8; font-weight: 800; }
.battle-log .log-empty { color: var(--muted); font-size: 10.5px; }
.damage-floats { position: relative; height: 22px; flex: 0 0 auto; }
.dmg-popup {
  position: absolute; font-weight: 800; font-size: 17px;
  animation: floatUp .85s forwards; pointer-events: none;
}
@keyframes floatUp { from { opacity: 1; transform: translateY(0); } to { opacity: 0; transform: translateY(-34px); } }

/* ---------------- Bottom bar (retired) ----------------
   Stat points are now spent inside the profile card and skills inside the skill
   list, so the bottom dock no longer exists. `--dock-h` stays for layout. */
.hint { color: var(--muted); font-size: 11px; }

/* Skills live in the left rail: stack them as full-width buttons. */
.hub-skills {
  display: flex; flex-direction: column; gap: 6px;
  align-items: stretch;
}
.hub-skills button:not(.sk-up) {
  width: 100%;
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  padding: 8px 10px; border-radius: 10px;
  font-size: 11px; font-weight: 700;
  text-align: left;
  color: var(--text);
  background: rgba(24, 31, 66, .88);
  border: 1px solid var(--stroke-hi);
  box-shadow: var(--shadow-sm);
  transition: transform .12s, filter .18s, opacity .18s;
}
.hub-skills button:not(.sk-up):hover:not(:disabled) { transform: translateY(-1px); filter: brightness(1.18); }
.hub-skills button:not(.sk-up):disabled { opacity: .38; }
.hub-skills button .sk-lv {
  flex: 0 0 auto;
  padding: 1px 7px; border-radius: 999px;
  font-size: 9px; font-weight: 900;
  background: rgba(0,0,0,.35);
  color: var(--gold);
}
.hub-skills button:not(.sk-up).is-primary {
  background: linear-gradient(135deg, rgba(124,92,255,.9), rgba(157,124,255,.9));
  border-color: transparent;
  box-shadow: 0 6px 18px rgba(124, 92, 255, .45);
}
.hub-skills button:not(.sk-up).is-danger {
  background: linear-gradient(135deg, rgba(255,92,138,.92), rgba(255,133,173,.92));
  border-color: transparent;
  box-shadow: 0 6px 18px rgba(255, 92, 138, .40);
}
.hub-skills button:not(.sk-up).is-good {
  background: linear-gradient(135deg, #2fd39b, #52efc0);
  border-color: transparent; color: #06251b;
  box-shadow: 0 6px 18px rgba(56, 232, 192, .38);
}
.hub-skills button:not(.sk-up).is-good .sk-lv { background: rgba(255,255,255,.45); color: #06251b; }
/* Locked skills: dim, dashed, and only clickable when a stat point is free. */
.hub-skills button:not(.sk-up).is-locked {
  background: rgba(255,255,255,.05);
  border: 1px dashed rgba(255, 201, 60, .55);
  color: #ffd977;
  box-shadow: none;
  opacity: .95;
}
.hub-skills button:not(.sk-up).is-locked:hover:not(:disabled) {
  background: rgba(255, 201, 60, .18);
  border-color: var(--gold);
}

/* Encounter / initiative note shown in place of the skill buttons */
.init-note {
  width: 100%;
  padding: 8px 10px; margin-bottom: 2px;
  text-align: center; font-size: 11px; line-height: 1.55;
  color: var(--text);
  background: rgba(24, 31, 66, .95);
  border: 1px solid var(--gold);
  border-radius: 10px;
  box-shadow: 0 8px 26px rgba(255, 201, 60, .22);
  animation: popIn .35s cubic-bezier(.2,.9,.3,1.35);
}
.init-note b { color: var(--gold); font-size: 14px; }
/* Compact variant used during a fight: two short lines instead of a banner. */
.init-note.compact {
  padding: 5px 8px; font-size: 9.5px; line-height: 1.45;
  text-align: left; box-shadow: none;
}
.init-note.compact b { font-size: 11px; }
.hub-skills .sk-name { flex: 1 1 auto; min-width: 0; }
.hub-skills button:not(.sk-up) .sk-icon { flex: 0 0 auto; }

/* ---------- 3D icon puck (skills + items) ---------- */
.icon3d {
  --tint: #8fa8d8;
  position: relative;
  display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px;
  flex: 0 0 auto;
  border-radius: 50%;
  /* The puck is a DARK disc tinted by the skill's colour, so a bright glyph
     always reads against it. The old version was mostly white, which washed
     out every light-coloured icon. */
  background:
    radial-gradient(circle at 34% 22%, rgba(255,255,255,.55) 0 9%, rgba(255,255,255,.14) 30%, rgba(255,255,255,0) 56%),
    radial-gradient(circle at 50% 118%, color-mix(in srgb, var(--tint) 55%, transparent) 0 30%, transparent 62%),
    linear-gradient(158deg, color-mix(in srgb, var(--tint) 42%, #0a0e1e) 0%, #0a0e1e 118%);
  box-shadow:
    inset 0 1px 1px rgba(255,255,255,.45),
    inset 0 -4px 8px rgba(0,0,0,.7),
    0 3px 7px rgba(0,0,0,.55);
  transform: translateZ(0);
}
.icon3d::before {
  /* Specular ring + a soft bounce light from below = rounded 3D puck. */
  content: "";
  position: absolute; inset: 0;
  border-radius: 50%;
  background: radial-gradient(circle at 50% 118%, rgba(255,255,255,.45) 0 26%, transparent 58%);
  pointer-events: none;
}
.icon3d::after {
  content: "";
  position: absolute; inset: 1px;
  border-radius: 50%;
  border: 1px solid rgba(255,255,255,.42);
  pointer-events: none;
}
.icon3d-face {
  position: relative; z-index: 1;
  width: 76%; height: 76%;
  display: flex; align-items: center; justify-content: center;
  /* A dark halo under the glyph keeps it legible on any puck tint. */
  filter: drop-shadow(0 1px 1.5px rgba(0,0,0,.85)) drop-shadow(0 0 2px rgba(0,0,0,.5));
}
.icon3d-face svg { width: 100%; height: 100%; display: block; }
/* Bigger puck for the item cards. */
.item-card .icon3d { width: 34px; height: 34px; }
.hub-skills button:hover:not(:disabled) .icon3d,
.item-card:hover:not(:disabled) .icon3d {
  transform: translateY(-1px) scale(1.06);
  transition: transform .14s;
}

/* Encounter choice buttons carry icons too. */
.hub-skills button:not(.sk-up).has-icon {
  justify-content: flex-start; gap: 10px;
}
.hub-skills button:not(.sk-up).has-icon .sk-name { flex: 1 1 auto; font-size: 12px; }

/* The player's own dice roll when a fight / flee needs deciding. */
.encounter-roll {
  width: 100%;
  display: flex; align-items: center; justify-content: center; gap: 10px;
  padding: 12px 10px; border-radius: 14px;
  font-family: inherit; font-size: 14px; font-weight: 900; letter-spacing: .4px;
  color: #22130a;
  background: linear-gradient(160deg, #ffe9a8 0%, #ffc93c 46%, #f59f00 100%);
  border: 1px solid #ffe08a;
  box-shadow:
    inset 0 2px 0 rgba(255,255,255,.85),
    inset 0 -3px 8px rgba(150,80,0,.45),
    0 10px 24px rgba(255, 201, 60, .45),
    0 0 0 3px rgba(255, 201, 60, .14);
  cursor: pointer;
  animation: rollPulse 1.1s ease-in-out infinite alternate;
}
.encounter-roll .er-die {
  font-size: 20px;
  filter: drop-shadow(0 2px 3px rgba(0,0,0,.35));
  animation: dieWiggle 1.1s ease-in-out infinite;
}
@keyframes rollPulse { to { filter: brightness(1.1); box-shadow: inset 0 2px 0 rgba(255,255,255,.9), 0 14px 30px rgba(255,201,60,.6); } }
@keyframes dieWiggle { 50% { transform: rotate(-14deg) translateY(-1px); } }
.encounter-roll:disabled { opacity: .55; animation: none; cursor: default; }


.init-who { font-weight: 800; color: var(--good); letter-spacing: .5px; }
@keyframes popIn { from { opacity: 0; transform: scale(.9); } }
.hub-skills button:not(.sk-up).is-locked:disabled { opacity: .45; }

/* Items in the narrow right column: stacked icon cards. */
.item-list { display: flex; flex-direction: column; gap: 6px; }
.item-list button {
  width: 100%;
  display: flex; align-items: center; gap: 9px;
  text-align: left;
  padding: 6px 9px; border-radius: 10px; font-size: 11px; font-weight: 700;
  color: var(--text);
  background: rgba(255,255,255,.07); border: 1px solid var(--stroke);
  transition: background .15s, border-color .15s, transform .12s;
}
.item-list button:hover:not(:disabled) { background: rgba(255,201,60,.18); border-color: var(--gold); transform: translateY(-1px); }
.item-list button:disabled { opacity: .4; }
.item-list .item-name { flex: 1 1 auto; min-width: 0; }
.item-list .none { color: var(--muted); font-size: 11px; }

/* Legacy stat-grid classes (the bottom dock is retired). */

/* ---------------- Target picker ---------------- */
#targetPicker {
  position: fixed; inset: 0; z-index: 1200;
  display: flex; align-items: center; justify-content: center;
  background: rgba(5, 8, 20, .70); backdrop-filter: blur(6px);
}
#targetPicker > div {
  min-width: 260px; padding: 20px 24px;
  background: var(--panel-solid);
  border: 1px solid var(--stroke-hi);
  border-radius: 18px;
  box-shadow: var(--shadow-lg);
  text-align: center;
}
#targetPicker button {
  display: block; width: 100%;
  margin-top: 8px; padding: 10px 14px; border-radius: 11px;
  font-size: 13px; font-weight: 700;
  background: rgba(255,255,255,.08); border: 1px solid var(--stroke);
}
#targetPicker button:hover { background: rgba(124,92,255,.30); border-color: var(--accent); }

/* ---------------- Liveness banner ---------------- */
/* There is no centred toast stack: every transient message is rendered on the
   single top line (#eventMsg). */
.conn-banner {
  position: fixed; left: 50%; top: 12px; transform: translateX(-50%);
  z-index: 1600;
  padding: 8px 16px; border-radius: 999px;
  font-size: 12px; font-weight: 800;
  background: rgba(180, 40, 60, .95); color: #fff;
  border: 1px solid rgba(255, 255, 255, .35);
  box-shadow: 0 10px 26px rgba(0, 0, 0, .45);
}
.conn-banner.hidden { display: none; }

/* ---------------- Game over ---------------- */
#gameOverBanner {
  position: fixed; inset: 0; z-index: 1500;
  display: flex; align-items: center; justify-content: center;
  background: rgba(5, 8, 20, .84); backdrop-filter: blur(8px);
  animation: fadeIn .4s;
}
@keyframes fadeIn { from { opacity: 0; } }
#gameOverBanner > div {
  padding: 36px 52px; text-align: center;
  background: linear-gradient(150deg, #232b56, #1a2148);
  border: 1px solid var(--gold);
  border-radius: 22px;
  box-shadow: 0 30px 80px rgba(0,0,0,.7), 0 0 60px rgba(255,201,60,.18);
  animation: cardIn .5s cubic-bezier(.2,.9,.3,1.25);
}
#gameOverBanner .go-title {
  font-size: 38px; font-weight: 900; letter-spacing: 4px;
  color: var(--gold);
  text-shadow: 0 0 30px rgba(255, 201, 60, .55);
  margin-bottom: 8px;
}
#gameOverBanner .go-winner { font-size: 22px; font-weight: 800; color: var(--good); }
#gameOverBanner .go-why { margin: 6px 0 20px; color: var(--muted); font-size: 13px; }
#gameOverBanner .go-payout {
  margin: -8px 0 18px;
  font-size: 18px; font-weight: 800; color: var(--gold);
  text-shadow: 0 0 18px rgba(255, 201, 60, .5);
}
#gameOverBanner .go-actions { display: flex; gap: 12px; justify-content: center; }
#gameOverBanner .go-actions .btn { min-width: 130px; }

/* ---------------- Roll prompt (retired) ----------------
   The die and the ทอย button used to float in the middle of the screen. They now
   live in the bottom-centre turn dock (#turnDock), so there is no overlay to
   style: the dock itself is hidden while it is not your turn. */

/* ---------- left rail: Profile (top-left) then Status below it ----------
   Template regions "Profile" and "Status": both are compact cards pinned to the
   top-left corner. Neither scrolls (req 4 / req 5) — the profile is a summary
   and the five status rows always fit, so a scrollbar would only waste width.
   The rail is a plain transparent column: the cards themselves are the only
   visible surfaces. */
.player-rail {
  /* req 1 (round 16): the room number and the stage name own the top-left
     corner, so the rail starts BELOW the top strip instead of at 0/0. --top-h
     is the strip's height and is measured at runtime (syncDockHeight), so the
     rail follows the chips on every screen size. */
  position: fixed; left: 0; top: var(--top-h, 34px);
  /* The rail runs all the way to the bottom of the screen. It must NOT stop
     above the bottom bar: the bar is transparent and only its RIGHT half holds
     the skill arc, so the left column is free all the way down. Stopping at
     --bar-h left a dead strip of empty screen under the Status card. */
  bottom: 8px;
  width: var(--rail-w);
  display: flex; flex-direction: column; gap: 6px;
  z-index: 60;
  pointer-events: none;
  /* req 9 (round 18): the rail now also holds the skill-cast stage, so it must
     clip rather than spill past the bottom of the screen. */
  overflow: hidden;
}
.player-rail .rail-panel { pointer-events: auto; }
/* req 1 (round 21): the Profile card is now the SAME width as the Status card,
   so the left column has one clean edge instead of a narrow card above a wide
   one. The Status card is content-sized (three rows of two stats) rather than
   being derived from the Profile's height, so neither card has to be measured
   to lay the other one out. */
#selfProfile {
  flex: 0 0 auto; max-height: none; overflow: visible;
  width: 100%;
}
#statusCard  {
  flex: 0 0 auto; overflow: hidden;
  width: 100%;
  height: auto;
  min-height: 0;
}
/* req 1 (round 20): the narration strip was removed. The two centred animation
   stages now show what the player did, so a second text line under the Status
   card was redundant. The .anim-line rules are kept because the same lines are
   still pushed into the battle log. */
.anim-line {
  display: flex; align-items: center; gap: 6px;
  padding: 5px 9px;
  border-radius: 10px;
  font-size: 9.5px; font-weight: 800; line-height: 1.25;
  color: #eaf3ff;
  background: linear-gradient(180deg, rgba(26, 33, 72, .96), rgba(12, 16, 36, .96));
  border: 1px solid var(--stroke-hi);
  border-left: 3px solid var(--accent);
  box-shadow: 0 8px 20px rgba(0,0,0,.45);
  animation: animLineIn .28s cubic-bezier(.2,.9,.3,1.3);
  overflow: hidden;
}
.anim-line.is-out { animation: animLineOut .3s ease forwards; }
.anim-line .al-ico {
  flex: 0 0 auto; width: 18px; height: 18px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 50%; font-size: 10px;
  background: radial-gradient(circle at 50% 30%, var(--al-hi, #7c5cff), var(--al-lo, #4b32c3));
  border: 1px solid rgba(255,255,255,.26);
}
.anim-line .al-text { flex: 1 1 auto; min-width: 0; }
.anim-line.k-skill { --al-hi: #a58cff; --al-lo: #4b32c3; border-left-color: #a58cff; }
.anim-line.k-combat { --al-hi: #ff8fa3; --al-lo: #c3123a; border-left-color: #ff8fa3; }
.anim-line.k-lap   { --al-hi: #ffd166; --al-lo: #c98a00; border-left-color: #ffd166; }
.anim-line.k-level { --al-hi: #6ef0b0; --al-lo: #128a52; border-left-color: #6ef0b0; }
@keyframes animLineIn  { from { opacity: 0; transform: translateX(-14px); } }
@keyframes animLineOut { to { opacity: 0; transform: translateX(-14px); } }

/* ---------------------------------------------------------------------------
   req 2 (round 20): the two portrait stages, CENTRED on the screen.
   The 3D canvas is full-screen, so a stage is just a framed window onto it: the
   renderer draws the model into the stage's rect with a scissor test, which
   costs no extra WebGL context and no extra draw of the board.
   Left  = the caster's ATTACK clip.
   Right = the target's HIT clip.
   --------------------------------------------------------------------------- */
/* The pair is one centred flex row, so the two frames are always the same size
   and always sit together in the middle of the screen. It is pointer-transparent
   and only as large as its content, so it never blocks the board. */
.portrait-pair {
  position: fixed;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  z-index: 150;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  pointer-events: none;
}
/* The pair is always laid out; each stage hides itself with its own opacity and
   `display: none` while it is not playing. Toggling the PAIR's display with
   :has() was tried and is wrong: a stage that receives `is-on` while its
   ancestor is still display:none never starts its transition, so it stayed at
   the pre-transition scale(.94) and the two frames came out different sizes. */
.portrait-stage {
  position: relative;
  /* Above the log card (145): the stage is the event, the log is the history. */
  z-index: 150;
  pointer-events: none;
  border-radius: 14px;
  border: 2px solid rgba(150, 175, 255, .55);
  box-shadow: 0 10px 30px rgba(0,0,0,.55);
  /* req 1 / req 2 (round 26): the backdrop is no longer a fixed colour. Each
     stage paints its own artwork into the stage's canvas (see stageBackdrop),
     so this is only the colour behind the canvas before the first frame. */
  background: #1a1f30;
  overflow: hidden;
  opacity: 0;
  transform: scale(.94);
  transition: opacity .18s ease, transform .18s ease;
}
.portrait-stage.is-on { opacity: 1; transform: none; }
.portrait-stage.hidden { display: none; }
/* Both frames are the same square, sized from the viewport so they fit a phone
   in landscape as well as a desktop. */
.portrait-atk,
.portrait-hit {
  width: var(--portrait-size, 190px);
  height: var(--portrait-size, 190px);
  flex: 0 0 auto;
}
.portrait-atk { border-color: rgba(255, 122, 150, .6); }
.portrait-hit { border-color: rgba(255, 201, 60, .6); }
/* The stage's own canvas: the model is rendered into it, so it must fill the
   frame and sit under the label. */
.portrait-stage .ps-canvas {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  display: block;
  border-radius: 12px;
}
.portrait-stage .ps-label {
  position: absolute; left: 0; right: 0; bottom: 0;
  padding: 4px 8px;
  font-size: 9px; font-weight: 900; letter-spacing: .02em;
  text-align: center;
  color: #eaf3ff;
  background: linear-gradient(180deg, transparent, rgba(6,9,22,.92));
  text-shadow: 0 1px 3px #000;
}
.portrait-atk .ps-label { color: #ffd7de; }
.portrait-hit .ps-label { color: #ffe9a8; }
/* A short flash when the clip starts, so the stage reads as an event. */
.portrait-stage.is-on::after {
  content: '';
  position: absolute; inset: 0;
  background: radial-gradient(circle at 50% 45%, rgba(255,255,255,.28), transparent 62%);
  animation: psFlash .42s ease-out forwards;
}
@keyframes psFlash { from { opacity: 1; } to { opacity: 0; } }
@media (max-height: 480px) {
  /* req 2 (round 20): the stages are centred, so a short screen only shrinks
     them — there is no corner to fight the log over any more. */
  .portrait-stage .ps-label { font-size: 7.5px; padding: 3px 5px; }
}
/* When the viewport is too short for both cards at full size, the Status card
   gives up the difference instead of spilling over the bottom bar. Its rows are
   compact enough that the five stats still fit. */
#statusCard .hub-body { overflow: hidden; }
#selfProfile::-webkit-scrollbar,
#statusCard .hub-body::-webkit-scrollbar { width: 0; height: 0; }

/* The profile card is a summary, not a stat sheet: shrink everything. */
.player-rail .corner-card { padding: 6px 8px; border-radius: 11px; font-size: 9.5px; }
.player-rail .pc-name { font-size: 10.5px; margin-bottom: 1px; }
.player-rail .pc-name .badge { font-size: 7.5px; padding: 0 5px; }
.player-rail .pc-meta { font-size: 8px; margin-bottom: 3px; }
.player-rail .pc-row { margin: 2px 0; gap: 4px; }
.player-rail .pc-row .lbl { width: 16px; font-size: 7.5px; }
.player-rail .pc-row .val { font-size: 8.5px; min-width: 34px; }
.player-rail .bar { height: 5px; }
.player-rail .pc-stats { grid-template-columns: repeat(5, 1fr); gap: 2px; margin-top: 3px; }
.player-rail .pc-stat { font-size: 7px; padding: 2px 1px; border-radius: 4px; }
.player-rail .pc-stat b { font-size: 8px; }
.player-rail .hub-card { padding: 6px 8px 7px; border-radius: 11px; }
.player-rail .hub-title { font-size: 8px; letter-spacing: 1px; margin-bottom: 4px; }
.player-rail .hub-tag { font-size: 7.5px; padding: 0 5px; }

/* ---------- status rows: same visual language as the skill pucks ---------- */
/* req 2 (round 21): the five stats are laid out as THREE rows of TWO columns
   (the last cell is empty), so the card is half as tall as a five-row list and
   the "+" buttons are twice as wide apart — which is what makes them easy to hit
   on a phone. The rows are separated by a real gap so a thumb aiming at one "+"
   cannot land on its neighbour. */
.up-stats {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 6px;
}
.up-stat-row {
  display: grid;
  grid-template-columns: 22px 1fr auto 20px;
  align-items: center; gap: 5px;
  padding: 4px 5px;
  border-radius: 8px;
  background: rgba(24, 31, 66, .72);
  border: 1px solid var(--stroke);
  min-width: 0;
}
.up-stat-row .us-icon { width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; }
.up-stat-row .us-icon .icon3d { width: 20px; height: 20px; }
.up-stat-row .us-icon .icon3d-face svg { width: 13px; height: 13px; }
.up-stat-row .us-label { font-size: 8.5px; font-weight: 900; color: var(--text); }
.up-stat-row .us-val  { font-size: 8.5px; font-weight: 800; color: var(--gold); text-align: right; }
.up-stat-row .us-hint { display: none; }
.up-stat-row .us-plus {
  width: 18px; height: 18px; padding: 0; border-radius: 50%;
  font-size: 12px; font-weight: 900; line-height: 1;
  background: linear-gradient(160deg, #2fd39b, #14a37a);
  color: #04231a; border: none; cursor: pointer;
}
.up-stat-row .us-plus:disabled { opacity: .22; cursor: default; }
.up-stat-row.has-points { border-color: rgba(47, 211, 155, .55); }

/* ---------- bottom bar ----------
   Three INDEPENDENT floating regions, matching the template:

     [ log (bottom-left) ]  [ interaction slot (centre) ]  [ items | skill ]

   The bar itself paints NOTHING (req 9): no background, border or shadow, so
   each region floats on its own above the board. The log is a fixed-width card
   on the left edge, the interaction slot is absolutely centred on the SCREEN,
   and the right cluster is a fixed-width row holding the item category slots
   immediately LEFT of the basic attack puck. Everything scales with the viewport
   so the whole cluster still fits a 667x375 iPhone SE. */
.bottom-bar {
  position: fixed; left: 0; right: 0; bottom: 0;
  /* Height follows the content; syncDockHeight() measures it and publishes
     --bar-h so the board and the log card line up with it exactly. There is
     deliberately NO min-height: var(--bar-h) here — that made the bar ratchet
     upwards (it could grow but never shrink when the viewport got smaller),
     because the measured height fed back into its own floor. */
  height: auto;
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: 6px;
  /* Bottom padding leaves room for the labels under the pucks and slots. */
  padding: 4px 8px calc(16px + env(safe-area-inset-bottom));
  background: none;
  border: none;
  box-shadow: none;
  z-index: 150;
  pointer-events: none;
}
.bottom-bar > * { pointer-events: auto; }

/* ---------- interaction slot: bottom centre ----------
   Template region "User Interaction". ทอย and จบตา share ONE slot and swap by
   server phase; the countdown sits directly above them. The fight-or-flee
   choice takes over this SAME slot when a monster is met (req 2), so the player
   never has to hunt for it. The dock is absolutely centred on the SCREEN (not on
   the space left over by the right cluster), so the turn controls always land in
   the middle of the bottom edge. Its visible content is narrow, so it never
   reaches the log card or the skill arc. The whole dock is hidden while no turn
   is running, so the centre of the bar stays clear for the board. */
.turn-dock {
  position: absolute;
  left: 50%; transform: translateX(-50%);
  bottom: calc(16px + env(safe-area-inset-bottom));
  display: flex; flex-direction: column;
  align-items: center; justify-content: flex-end;
  gap: 4px;
  pointer-events: none;
  z-index: 2;
}
.turn-dock > * { pointer-events: auto; }
/* The countdown is decoration only: it must never swallow a board click. */
.turn-dock .turn-timer-bar { pointer-events: none; }
/* updateTurnButtons() toggles this when it is not the local player's turn. */
.turn-dock.is-hidden { display: none; }
.turn-slot {
  display: flex; align-items: center; justify-content: center;
  gap: 7px;
  min-height: var(--orb);
}
/* The die + readout sit beside the shared ทอย / จบตา button. The die stays up
   for the whole turn so the rolled number can still be read while the token
   walks; only the BUTTON swaps between ทอย and จบตา. */
.turn-slot-cell {
  display: flex; align-items: center; justify-content: center;
  gap: 7px;
}
.turn-slot-cell .hub-dice { padding: 3px 7px; gap: 5px; border-radius: 12px; }
/* The die block is decoration: it must never intercept a tap meant for the
   button beside it. */
.turn-slot-cell .hub-dice { pointer-events: none; }
/* The countdown is a static row inside the dock, never a floating overlay. */
.turn-dock .turn-timer-bar { width: min(210px, 34vw); }

/* ---------- fight-or-flee choice (centre interaction slot) ----------
   When a monster is met the choice replaces the dice slot in the SAME centre
   position (req 2). Two big pucks sit side by side so the decision is a single
   tap and impossible to miss. */
.interaction-choice {
  display: flex; align-items: center; justify-content: center;
  gap: 10px;
  pointer-events: auto;
}
.interaction-choice.hidden { display: none; }
.interaction-choice .ic-btn {
  position: relative;
  width: calc(var(--orb) * 1.16); height: calc(var(--orb) * 1.16);
  padding: 0; border-radius: 50%;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 1px;
  font-family: inherit; font-size: 9px; font-weight: 900;
  color: #fff; cursor: pointer;
  border: 2px solid transparent;
  transition: transform .12s, filter .18s;
}
.interaction-choice .ic-btn:hover:not(:disabled) { transform: translateY(-2px); filter: brightness(1.16); }
.interaction-choice .ic-btn:disabled { opacity: .45; cursor: default; }
.interaction-choice .ic-btn .ic-glyph { font-size: calc(var(--orb) * 0.42); line-height: 1; }
.interaction-choice .ic-fight {
  background: radial-gradient(circle at 50% 28%, #ff8fa3, #c3123a);
  box-shadow: 0 9px 22px rgba(255, 92, 138, .45), inset 0 1px 0 rgba(255,255,255,.22);
  animation: usableAura 2.1s ease-in-out infinite;
}
.interaction-choice .ic-flee {
  background: radial-gradient(circle at 50% 28%, #a58cff, #4b32c3);
  box-shadow: 0 9px 22px rgba(124, 92, 255, .45), inset 0 1px 0 rgba(255,255,255,.22);
  animation: usableAura 2.1s ease-in-out infinite;
}
/* The roll / back buttons under the choice. The roll is a proper round dice orb
   (req 5) — the same lit puck as ทอย in the turn dock, not a flat rectangle —
   and the switch button is a solid, readable pill (req 6). */
.interaction-choice .ic-wide {
  width: auto; height: auto;
  padding: 7px 14px; border-radius: 12px;
  flex-direction: row; gap: 6px;
  font-size: 10.5px;
}
/* req 5: the encounter roll is the SAME round dice orb as the turn dock's ทอย,
   so the two read as one control wherever it appears. */
.interaction-choice .ic-roll {
  width: calc(var(--orb) * 1.16); height: calc(var(--orb) * 1.16);
  padding: 0; border-radius: 50%;
  flex-direction: column; gap: 1px;
  font-size: 9px; letter-spacing: .3px;
  background: radial-gradient(circle at 34% 26%, #ffb3d2, var(--accent2) 50%, #c62a5c 100%);
  border: 2px solid rgba(255,255,255,.35);
  box-shadow: 0 12px 30px rgba(255, 92, 138, .55), inset 0 -8px 18px rgba(0,0,0,.30);
  animation: orbPulse 1.7s ease-in-out infinite;
}
.interaction-choice .ic-roll .ic-glyph {
  font-size: calc(var(--orb) * 0.42);
  filter: drop-shadow(0 3px 7px rgba(0,0,0,.5));
}
/* req 6: the switch button used to be a translucent ghost that was almost
   unreadable over the board. It is now a solid slate pill with a bright rim. */
.interaction-choice .ic-back {
  background: linear-gradient(180deg, #3a4a86, #232c58);
  border: 1.5px solid rgba(160, 185, 255, .75);
  color: #eaf1ff;
  font-weight: 800;
  box-shadow: 0 8px 20px rgba(0,0,0,.5), inset 0 1px 0 rgba(255,255,255,.16);
  text-shadow: 0 1px 3px rgba(0,0,0,.6);
}
.interaction-choice .ic-back:hover:not(:disabled) {
  background: linear-gradient(180deg, #4a5da4, #2c3768);
  border-color: rgba(190, 210, 255, .95);
}
/* The choice card carries the encounter narration above the pucks. */
.interaction-choice .ic-note {
  max-width: min(52vw, 420px);
  padding: 6px 12px; border-radius: 12px;
  font-size: 10.5px; line-height: 1.45; font-weight: 700;
  text-align: center; color: #eaf3ff;
  background: rgba(12, 16, 36, .9);
  border: 1px solid var(--gold);
  box-shadow: 0 8px 22px rgba(255, 201, 60, .22);
}
.interaction-choice .ic-note b { color: var(--gold); }
/* The choice stacks: narration on top, the pucks in a row beneath it. */
.interaction-choice { flex-direction: column; gap: 6px; }
.interaction-choice .ic-row {
  display: flex; align-items: center; justify-content: center;
  gap: 10px; flex-wrap: wrap;
}
/* While the choice owns the centre slot, the dice + ทอย / จบตา step aside. */
.turn-dock.has-choice .turn-slot,
.turn-dock.has-choice .turn-timer-bar { display: none; }
/* req 1: during a monster fight there is no button to press — จบตา is disabled
   until someone dies or the timer runs out — so the slot is dropped and only the
   countdown is drawn. The timer must stay visible: it is now the only way the
   turn can end without a death. */
.turn-dock.is-fight .turn-slot { display: none; }

/* ---------- encounter duel dice (player vs monster) ----------
   The fight-or-flee roll is settled by rolling against the monster, and the
   result is shown as a CENTRED card: the player's die on the left, the
   monster's on the right, VS between them and the outcome underneath.

   It is a top-level overlay, NOT a child of the turn dock. The fight-or-flee
   choice hides .turn-slot, so a die inside the dock was invisible at exactly
   the moment it was needed — that is why the roll looked like it had no
   animation. The backdrop is a light dim rather than a blackout, so the board
   stays readable underneath. */
.duel-dice {
  position: fixed; inset: 0; z-index: 880;
  display: flex; align-items: center; justify-content: center;
  background: rgba(6, 9, 22, .42);
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
  pointer-events: none;
}
.duel-dice.hidden { display: none; }
.duel-dice .dd-card {
  min-width: 300px;
  padding: 16px 24px 18px;
  border-radius: 20px;
  background: linear-gradient(155deg, rgba(38, 46, 88, .97), rgba(20, 25, 52, .97));
  border: 1px solid rgba(255, 201, 60, .45);
  box-shadow: 0 26px 70px rgba(0, 0, 0, .65), 0 0 50px rgba(255, 201, 60, .14);
  text-align: center;
  /* The entrance animates the TRANSFORM only. An opacity-based entrance could
     leave the card invisible if the animation restarted mid-flight. */
  animation: duelIn .3s cubic-bezier(.2, .9, .3, 1.25);
}
@keyframes duelIn { from { transform: translateY(10px) scale(.94); } }
.duel-dice .dd-title {
  font-size: 11px; font-weight: 900; letter-spacing: 2.2px;
  color: var(--gold); margin-bottom: 12px; text-transform: uppercase;
}
.duel-dice .dd-row {
  display: flex; align-items: center; justify-content: center; gap: 16px;
}
.duel-dice .dd-side {
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  width: 104px;
}
.duel-dice .dd-name {
  font-size: 11px; font-weight: 800; color: #e8eeff;
  max-width: 104px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.duel-dice .dd-val {
  font-size: 20px; font-weight: 900; color: var(--gold);
  line-height: 1.1; min-height: 24px;
}
.duel-dice .dd-vs {
  font-size: 11px; font-weight: 900; letter-spacing: 1.4px;
  color: rgba(255, 255, 255, .55);
}
.duel-dice .dd-outcome {
  margin-top: 12px; min-height: 16px;
  font-size: 11px; font-weight: 800; line-height: 1.4;
  color: #eaf3ff;
}
.duel-dice .dd-outcome.good { color: var(--good); }
.duel-dice .dd-outcome.bad  { color: var(--bad); }
/* The monster's die is tinted red so the two are never confused. */
.duel-dice .cube.is-enemy .face {
  --pip: #3a1220;
  background-color: #ffd9e2;
  box-shadow:
    inset 0 0 0 1px rgba(255, 220, 230, .95),
    inset 0 -10px 16px rgba(180, 60, 90, .22),
    inset 0 10px 14px rgba(255, 255, 255, .8);
}
/* The face offsets must be scoped through .dice3d: the generic `.dice3d .f1`
   rule further down the file has the same specificity and would otherwise win,
   collapsing all six faces onto one plane (the die then looks like a flat
   blob). */
.duel-dice .dice3d { width: 64px; height: 64px; perspective: 400px; }
.duel-dice .dice3d .cube, .duel-dice .dice3d .face { width: 64px; height: 64px; }
.duel-dice .dice3d .face { border-radius: 13px; }
.duel-dice .dice3d .f1 { transform: translateZ(32px); }
.duel-dice .dice3d .f6 { transform: rotateX(180deg) translateZ(32px); }
.duel-dice .dice3d .f2 { transform: rotateY(90deg) translateZ(32px); }
.duel-dice .dice3d .f5 { transform: rotateY(-90deg) translateZ(32px); }
.duel-dice .dice3d .f3 { transform: rotateX(-90deg) translateZ(32px); }
.duel-dice .dice3d .f4 { transform: rotateX(90deg) translateZ(32px); }
.duel-dice .dice-shadow { bottom: -10px; width: 52px; height: 11px; }

/* ---------- right cluster: items then the skill arc ----------
   The item category slots sit immediately to the LEFT of the basic attack puck,
   which is pinned to the bottom-right corner of the skill arc. */
.right-cluster {
  flex: 0 0 auto;
  /* margin-left:auto keeps the cluster pinned to the right edge even when the
     turn dock is hidden (display:none), which would otherwise leave it as the
     only flex child and push it to the left. */
  margin-left: auto;
  display: flex; align-items: flex-end; gap: 6px;
}
.item-dock {
  flex: 0 0 auto;
  display: flex; align-items: flex-end; justify-content: flex-end;
  /* The bag badge is absolutely positioned so it adds NO width to the right
     cluster: widening the cluster would push the log card into its "below the
     arc" fallback and collapse it on a short landscape screen. */
  position: relative;
}
/* Bag capacity badge (GAME_SPEC §4.6). Floats above the three category slots
   and turns amber once the bag is full, so the player knows a chest will be
   left behind before walking onto it. */
.bag-tag {
  position: absolute;
  right: 0;
  bottom: calc(100% + 2px);
  padding: 1px 6px; border-radius: 999px;
  font-size: 9px; font-weight: 900; letter-spacing: 0;
  background: rgba(14, 18, 42, .78);
  border: 1px solid var(--stroke);
  color: #cfd9ff;
  white-space: nowrap;
  pointer-events: none;
  text-shadow: 0 1px 3px #000;
}
.bag-tag.hidden { display: none; }
.bag-tag.is-full {
  background: linear-gradient(135deg, #ffd166, #ff9f1c);
  border-color: rgba(255, 201, 60, .8);
  color: #2a1a00;
}
#itemCard {
  background: none; border: none; box-shadow: none; padding: 0;
  /* .hub-card's frosted backdrop would paint a visible rectangle behind the
     loose slots, so it must be cleared along with the background. */
  backdrop-filter: none; -webkit-backdrop-filter: none;
  overflow: visible; flex: 0 0 auto;
  /* The old rail sizing (min-height 64px / max-height 26%) must not apply. */
  min-height: 0; max-height: none;
}
#itemCard .hub-body { overflow: visible; }
.item-list {
  display: flex; flex-direction: row; gap: 4px;
  align-items: flex-end; justify-content: center;
}
.item-list button,
.item-slot-empty {
  width: var(--slot);
  height: var(--slot);
  border-radius: 50%;
  flex: 0 0 auto;
}
.item-slot-empty {
  border: 2px dashed rgba(150, 175, 255, .45);
  background: rgba(30, 40, 82, .55);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.08);
}
.item-list button {
  position: relative; padding: 0;
  display: flex; align-items: center; justify-content: center;
  background: radial-gradient(circle at 50% 30%, rgba(58, 70, 130, .95), rgba(16, 20, 44, .95));
  border: 2px solid rgba(255, 201, 60, .45);
  box-shadow: 0 6px 16px rgba(0,0,0,.45), inset 0 1px 0 rgba(255,255,255,.14);
  cursor: pointer;
  transition: transform .12s, filter .18s;
}
.item-list button:hover:not(:disabled) { transform: translateY(-2px); filter: brightness(1.15); }
.item-list button:disabled { opacity: .38; }
.item-list button .icon3d {
  width: calc(var(--slot) * 0.62); height: calc(var(--slot) * 0.62);
}
.item-list .item-name {
  position: absolute; bottom: -11px; left: 50%; transform: translateX(-50%);
  font-size: 7px; font-weight: 800; color: #cfd9ff; white-space: nowrap;
  text-shadow: 0 1px 3px #000;
}
/* Category slots carry a count badge in the corner. */
.item-cat .item-count {
  position: absolute; top: -4px; right: -4px;
  min-width: 16px; height: 16px; padding: 0 4px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 999px;
  font-size: 8px; font-weight: 900;
  background: linear-gradient(135deg, #ffd166, #ff9f1c);
  color: #2a1a00;
  box-shadow: 0 2px 6px rgba(0,0,0,.55);
}
.item-cat.is-idle { opacity: .55; }
.item-cat.is-idle .item-count { background: rgba(255,255,255,.22); color: #cfd9ff; }

/* ---------- item picker ----------
   Opens directly ABOVE the item slot that was pressed and lists EVERY item in
   the chosen category as a grid of icon tiles (req 4), so the player picks by
   picture instead of reading a list of names. positionItemPicker() sets the
   left/top inline so the panel is anchored to the pressed slot and clamped to
   the viewport on a small phone. */
.item-picker {
  position: fixed;
  left: 0; top: 0;
  width: min(360px, 82vw);
  max-height: min(52vh, 320px);
  display: flex; flex-direction: column;
  /* req 2 (round 22): no header, so the card is a single rounded panel of tiles
     with a gold rim and a soft inner glow — it reads as a tray of items rather
     than a dialog with a title bar. */
  padding: 4px;
  background:
    radial-gradient(120% 90% at 50% 0%, rgba(255, 201, 60, .10), transparent 62%),
    linear-gradient(180deg, rgba(28, 34, 70, .98), rgba(11, 15, 34, .98));
  border: 1px solid rgba(255, 201, 60, .42);
  border-radius: 16px;
  box-shadow:
    0 18px 44px rgba(0,0,0,.62),
    0 0 0 1px rgba(255,255,255,.05) inset,
    0 0 22px rgba(255, 201, 60, .12);
  /* Above the centred roll prompt (700): opening the picker is an explicit
     action, so it must never be hidden behind the dice card. */
  z-index: 760;
  overflow: hidden;
}
.item-picker.hidden { display: none; }
/* A wrapping grid of square tiles: every item in the category is visible at
   once, which is the whole point of the picker. */
.ip-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(64px, 1fr));
  gap: 7px;
  padding: 6px;
  overflow-y: auto;
  overscroll-behavior: contain;
}
.ip-item {
  position: relative;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 3px;
  min-height: 64px; padding: 6px 4px;
  border-radius: 11px;
  background: radial-gradient(circle at 50% 26%, rgba(64, 78, 142, .95), rgba(20, 26, 54, .95));
  border: 1.5px solid rgba(140, 160, 255, .45);
  color: #e8eeff; font-family: inherit; font-size: 8.5px; font-weight: 800;
  text-align: center; cursor: pointer;
  transition: transform .12s, filter .18s, border-color .18s;
}
.ip-item:hover:not(:disabled) {
  transform: translateY(-2px); filter: brightness(1.18);
  border-color: rgba(255, 201, 60, .85);
}
.ip-item:disabled { opacity: .38; cursor: not-allowed; filter: grayscale(.5); }
.ip-item .icon3d { width: 30px; height: 30px; flex: 0 0 auto; }
.ip-item .ip-name {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; line-height: 1.2;
}
/* The effect line ("HP +10") under the name, so two potions of the same family
   are still distinguishable at a glance. */
.ip-item .ip-eff {
  font-size: 7.5px; font-weight: 700; color: var(--gold); opacity: .9;
}
.ip-empty {
  grid-column: 1 / -1;
  padding: 14px 10px; text-align: center;
  font-size: 11px; color: var(--muted);
}
.item-list .none {
  font-size: 9px; color: var(--muted);
  padding: 6px 12px; border-radius: 999px;
  background: rgba(14, 18, 42, .6); border: 1px dashed var(--stroke);
}

/* The skill arc, bottom-right corner of the right cluster. */
.skill-dock {
  display: flex; align-items: flex-end; gap: 6px;
}
#skillCard {
  position: relative;
  background: none; border: none; box-shadow: none; padding: 0;
  /* See #itemCard: no frosted panel behind the skill arc. */
  backdrop-filter: none; -webkit-backdrop-filter: none;
  overflow: visible; flex: 0 0 auto;
  /* The old rail sizing (min-height 96px) must not apply. */
  min-height: 0; max-height: none;
}
#skillCard .hub-body { overflow: visible; }
/* The skill-point counter floats above the wheel. */
.skill-pts {
  position: absolute; right: 0; bottom: calc(100% + 2px);
  float: none; font-size: 8px; padding: 1px 6px;
}
/* The skill cluster is an ARC: the basic attack sits in the bottom-right
   corner and the three class skills fan out up and to the left around it,
   like the reference MOBA layout. */
.hub-skills {
  position: relative;
  display: block;
  /* Room for the quarter-arc fan (radius 2.05 puck) plus the enlarged corner
     puck (1.7 puck) and the learn / upgrade badges that stick out above it. */
  width: calc(var(--puck) * 3.75);
  height: calc(var(--puck) * 3.75);
  padding-top: 7px;
}
.skill-row { position: relative; display: block; }
/* The basic attack: pinned to the bottom-right corner of the cluster and drawn
   LARGER than the class pucks so the corner reads as the primary action. */
.skill-row--basic {
  position: absolute;
  right: 0; bottom: 0;
  z-index: 3;
}
/* The three class skills fan out along an arc centred on the corner puck.
   --arc-i (0,1,2) picks the position. The offsets are precomputed rather than
   using CSS cos()/sin(), which older mobile browsers do not support. */
.skill-row--arc {
  position: absolute;
  right: 0; bottom: 0;
  z-index: 2;
}
/* A quarter arc from straight-up to straight-left, in EVEN 30° steps
   (90° / 60° / 30° from horizontal). Adjacent pucks are 30 degrees apart, so the
   chord between them is 2R·sin(15°) = 0.518R.
   req 3: the gap between the fan and the enlarged corner puck is HALVED, so the
   radius drops from 2.55 to 2.05 puck. The chord between neighbours is then
   1.06 puck — still wider than one puck, so the class skills keep a visible gap
   between each other while sitting much closer to the basic attack.
   Every offset is negative in both axes, so the fan always grows UP and LEFT
   from the corner puck and can never spill below the bottom bar. */
.skill-row--arc[style*="--arc-i: 0"] {
  transform: translate(0, calc(var(--puck) * -2.05));
}
.skill-row--arc[style*="--arc-i: 1"] {
  transform: translate(calc(var(--puck) * -1.025), calc(var(--puck) * -1.775));
}
.skill-row--arc[style*="--arc-i: 2"] {
  transform: translate(calc(var(--puck) * -1.775), calc(var(--puck) * -1.025));
}
.hub-skills button[data-skill]:not(.sk-up) {
  position: relative;
  width: var(--puck);
  height: var(--puck);
  padding: 0;
  display: flex; align-items: center; justify-content: center;
  border-radius: 50%;
  background: radial-gradient(circle at 50% 28%, rgba(70, 84, 156, .96), rgba(14, 18, 42, .96));
  border: 2px solid rgba(140, 160, 255, .55);
  box-shadow: 0 7px 18px rgba(0,0,0,.5), inset 0 1px 0 rgba(255,255,255,.16);
  overflow: visible;
  transition: transform .12s, filter .18s, opacity .18s;
}
.hub-skills button[data-skill]:not(.sk-up) .sk-icon { display: flex; }
.hub-skills button[data-skill]:not(.sk-up) .icon3d {
  width: calc(var(--puck) * 0.56); height: calc(var(--puck) * 0.56);
}
.hub-skills button[data-skill]:not(.sk-up) .sk-name {
  /* req 3: the label sits BELOW the puck with a real gap. It used to be pinned
     at bottom: -12px, which put it 12px under the puck's edge but only ~2px
     under the glyph inside it, so on a phone the icon and the name touched. */
  position: absolute; bottom: -18px; left: 50%; transform: translateX(-50%);
  font-size: 7px; font-weight: 800; color: #cfd9ff; white-space: nowrap;
  text-shadow: 0 1px 3px #000; text-align: center;
}
.hub-skills button[data-skill]:not(.sk-up) .sk-lv {
  position: absolute; top: -6px; right: -6px;
  min-width: 15px; height: 15px; padding: 0 3px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 999px;
  font-size: 7.5px; font-weight: 900;
  background: linear-gradient(135deg, #ffd166, #ff9f1c);
  color: #2a1a00;
  box-shadow: 0 2px 6px rgba(0,0,0,.5);
}
.hub-skills button[data-skill]:not(.sk-up).is-danger { border-color: rgba(255, 122, 150, .75); }
.hub-skills button[data-skill]:not(.sk-up).is-primary { border-color: rgba(124, 92, 255, .8); }
.hub-skills button[data-skill]:not(.sk-up).is-good { border-color: rgba(61, 220, 132, .8); }
.hub-skills button[data-skill]:not(.sk-up).is-locked { border-color: rgba(140, 160, 255, .28); }
.hub-skills button[data-skill]:not(.sk-up):hover:not(:disabled) { transform: translateY(-3px); filter: brightness(1.18); }
.hub-skills button[data-skill]:not(.sk-up):active:not(:disabled) { transform: scale(.95); }
.hub-skills button[data-skill]:not(.sk-up):disabled { opacity: .42; filter: grayscale(.5); }
.hub-skills button[data-skill]:not(.sk-up).is-pulse { animation: orbPulse 1.7s ease-in-out infinite; }

/* The basic attack is the primary action: it is drawn ~70% larger than the
   class pucks and sits in the corner, so the eye lands on it first (req 3: it
   was 1.45 and still read as just another puck). */
.skill-row--basic button[data-skill]:not(.sk-up) {
  width: calc(var(--puck) * 1.7);
  height: calc(var(--puck) * 1.7);
  border-width: 2.5px;
  border-color: rgba(255, 122, 150, .9);
  box-shadow: 0 9px 22px rgba(255, 92, 138, .42), inset 0 1px 0 rgba(255,255,255,.2);
}
.skill-row--basic button[data-skill]:not(.sk-up) .icon3d {
  width: calc(var(--puck) * 0.92); height: calc(var(--puck) * 0.92);
}
.skill-row--basic button[data-skill]:not(.sk-up) .sk-name { font-size: 8px; bottom: -13px; }

/* A skill that has been upgraded past Lv1 gets a gold rim so the investment is
   visible at a glance on the wheel. */
.hub-skills button[data-skill]:not(.sk-up).is-upgraded {
  border-color: rgba(255, 201, 60, .92);
  box-shadow: 0 7px 20px rgba(255, 201, 60, .34), inset 0 1px 0 rgba(255,255,255,.2);
}
.hub-skills button[data-skill]:not(.sk-up).is-upgraded .sk-lv {
  background: linear-gradient(135deg, #ffe9a8, #ffc93c);
  color: #2a1a00;
}
/* A skill that can be cast RIGHT NOW breathes a soft aura, so the player can
   tell at a glance which pucks are live without reading the tooltip. */
.hub-skills button[data-skill]:not(.sk-up).is-usable {
  animation: usableAura 2.1s ease-in-out infinite;
}
@keyframes usableAura {
  0%, 100% { box-shadow: 0 7px 18px rgba(0,0,0,.5), inset 0 1px 0 rgba(255,255,255,.16),
                          0 0 0 0 rgba(56, 232, 192, 0); }
  50%      { box-shadow: 0 7px 20px rgba(0,0,0,.5), inset 0 1px 0 rgba(255,255,255,.16),
                          0 0 0 4px rgba(56, 232, 192, .22); }
}
/* An upgraded skill keeps its gold rim while it breathes. */
.hub-skills button[data-skill]:not(.sk-up).is-upgraded.is-usable {
  animation: usableAuraGold 2.1s ease-in-out infinite;
}
@keyframes usableAuraGold {
  0%, 100% { box-shadow: 0 7px 20px rgba(255, 201, 60, .34), inset 0 1px 0 rgba(255,255,255,.2),
                          0 0 0 0 rgba(255, 201, 60, 0); }
  50%      { box-shadow: 0 7px 22px rgba(255, 201, 60, .42), inset 0 1px 0 rgba(255,255,255,.2),
                          0 0 0 4px rgba(255, 201, 60, .26); }
}

/* The learn / upgrade "+" badge on the puck's corner. */
.sk-up {
  position: absolute; top: -6px; left: -6px;
  width: 19px; height: 19px; min-width: 0; padding: 0;
  display: flex; align-items: center; justify-content: center;
  border-radius: 50%;
  font-family: inherit; font-size: 13px; font-weight: 900; line-height: 1;
  background: rgba(255,255,255,.14);
  border: 1px solid var(--stroke-hi);
  color: var(--muted);
  cursor: pointer;
  z-index: 2;
}
.sk-up.can {
  background: linear-gradient(160deg, #2fd39b, #14a37a);
  border-color: transparent; color: #04231a;
  box-shadow: 0 0 0 2px rgba(47, 211, 155, .28);
  animation: needsChoicePulse 1.4s ease-in-out infinite alternate;
}
.sk-up:disabled { opacity: .3; cursor: default; }

/* Encounter / combat notes float above the bottom bar, centred, instead of
   pushing the pucks around or being clipped by the bar. */
.skill-note {
  position: fixed; left: 50%; transform: translateX(-50%);
  bottom: calc(var(--bar-h) + 8px);
  width: min(78vw, 460px);
  display: flex; flex-direction: column; gap: 5px;
  align-items: stretch;
  pointer-events: auto;
  z-index: 160;
}
.skill-note:empty { display: none; }
.skill-note .init-note {
  margin: 0;
  padding: 6px 10px; border-radius: 10px;
  font-size: 10px; line-height: 1.45;
  background: rgba(14, 18, 42, .92);
  border: 1px solid var(--stroke-hi);
  box-shadow: 0 8px 22px rgba(0,0,0,.5);
}
.skill-note .encounter-roll,
.skill-note .is-primary {
  padding: 7px 12px; border-radius: 10px;
  font-size: 11px; font-weight: 800;
  background: linear-gradient(160deg, #7c5cff, #4b32c3);
  color: #fff; border: none; cursor: pointer;
}
.skill-note .encounter-roll { display: flex; align-items: center; gap: 6px; justify-content: center; }
.skill-note .er-die { font-size: 15px; }

/* Dice + turn orbs keep their look but shrink to the bar. The die and the two
   orbs live in the bottom-centre turn dock (see .turn-dock). */
.hub-dice { padding: 4px 6px; gap: 4px; border-radius: 12px; }
/* The readout is just the number: the label is dropped so the dice block stays
   narrow enough to leave the centre of the bar to the item slots. */
.dice-readout small { display: none; }
.dice-readout { min-width: 18px; }
.dice3d { width: var(--die); height: var(--die); perspective: 260px; }
.dice3d .cube, .dice3d .face { width: 100%; height: 100%; }
.dice3d .f1 { transform: translateZ(calc(var(--die) / 2)); }
.dice3d .f6 { transform: rotateX(180deg) translateZ(calc(var(--die) / 2)); }
.dice3d .f2 { transform: rotateY(90deg) translateZ(calc(var(--die) / 2)); }
.dice3d .f5 { transform: rotateY(-90deg) translateZ(calc(var(--die) / 2)); }
.dice3d .f3 { transform: rotateX(-90deg) translateZ(calc(var(--die) / 2)); }
.dice3d .f4 { transform: rotateX(90deg) translateZ(calc(var(--die) / 2)); }
.dice-readout b { font-size: clamp(13px, 2.4vw, 18px); }
.dice-readout small { font-size: 7px; }
.orb, .orb-end { width: var(--orb); height: var(--orb); border-width: 2px; }
.orb-glyph { font-size: clamp(15px, 2.6vw, 20px); }
.orb-text { font-size: 8px; letter-spacing: .3px; }
/* ---------- game log: TOP-RIGHT, below the menu strip, toggled by ☰ ----------
   Template region "Log". It used to be a permanent card in the bottom-left
   corner; it now hangs from the top-right, directly under the menu buttons, and
   the ☰ button in that strip opens and closes it. Being top-anchored means its
   height is bounded by the viewport below the strip, and it never competes with
   the bottom bar for space. */
.battle-hud {
  position: fixed;
  right: 8px;
  /* req 2 (round 20): the animation stages moved to the centre of the screen, so
     the log no longer has to give up the top-right corner. It starts directly
     under the menu strip and can use the full height down to the skill arc. */
  top: calc(var(--top-h) + 14px);
  bottom: auto;
  left: auto;
  flex: 0 0 auto;
  width: var(--log-w);
  /* The card grows downward from the strip and stops above the bottom bar, so it
     can never cover the interaction slot or the skill arc. */
  height: var(--log-h);
  /* req 1 (round 17): the card stops above the SKILL ARC, not above the bottom
     bar. --log-h is measured from the arc's real top edge (syncLogHeight), and
     this max-height is the same bound expressed as a fallback for the frame
     before the first measurement. */
  max-height: calc(100dvh - var(--top-h) - var(--bar-h) - 40px - env(safe-area-inset-bottom));
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border-radius: 12px;
  z-index: 145;
  /* Closed by default: the ☰ button reveals it. */
  opacity: 0;
  transform: translateY(-8px) scale(.98);
  pointer-events: none;
  transition: opacity .16s ease, transform .16s ease;
}
.battle-hud.is-open {
  opacity: 1;
  transform: none;
  pointer-events: auto;
}
.battle-hud-inner { padding: 6px 9px 8px; border-radius: 12px; }
.battle-log { max-height: none; font-size: 9.5px; }

/* ---------- board + overlays ---------- */
/* The board is FULL SCREEN: the rail, the bottom bar and the log all float on
   top of it, so the 3D view gets every pixel. */
.board { inset: 0; }
.top-bar { top: 4px; left: 6px; right: 6px; padding: 0; gap: 6px; }
.top-left { display: flex; }
/* req 1 (round 28): the centre group is absolutely positioned (see the base
   rule), so this override must not put it back into the flex flow. */
.top-center { min-width: 0; }
.status-pill { font-size: 9.5px; padding: 3px 8px; max-width: none; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.top-right { gap: 4px; flex-wrap: nowrap; flex: 0 0 auto; }
.chip { font-size: 10px; padding: 4px 7px; border-radius: 9px; min-height: 28px; }
.chip-phase, .chip-turn { display: none; }
.chip-word { display: none; }
.chip-menu { display: inline-flex; align-items: center; justify-content: center; font-size: 14px; padding: 2px 8px; }
/* The countdown is part of the bottom-centre interaction slot, so it needs no
   fixed positioning of its own; only its width is tuned here. */
.turn-dock .turn-timer-bar { width: min(210px, 34vw); }
body.phone-overlay .turn-dock { opacity: 0; visibility: hidden; }
/* req 7 (round 18): the focused profile (an opponent or the monster you are
   fighting) sits to the RIGHT of your own Profile card, not centred over the
   board. The base rule already positions it there; this block only tunes the
   type for a small screen. */
.focus-profile {
  max-width: min(46vw, 340px);
  font-size: 9.5px;
}
.focus-profile .fp-head { gap: 6px; }
.focus-profile .fp-badge { width: 26px; height: 26px; font-size: 12px; }
.focus-profile .fp-name { font-size: 12px; }
.focus-profile .fp-sub { font-size: 8.5px; }
.focus-profile .fp-stats { grid-template-columns: repeat(3, 1fr); gap: 3px; }
.focus-profile .fp-stats div { font-size: 7.5px; padding: 2px 1px; }
.focus-profile .fp-stats b { font-size: 9px; }
.skill-tip { width: min(46vw, 280px); font-size: 10.5px; }
.target-hint { bottom: calc(var(--bar-h) + 12px); font-size: 11px; }

/* ---------- small landscape phones (iPhone SE 667x375 and friends) -------
   The tightest target: 667x375. The log card, the interaction slot and the
   skill arc must all fit on one row without overlapping, so every region is
   scaled down and the log card is narrowed.
   The breakpoint is 480px tall, not 430px: a 956x440 phone (iPhone 16 Pro Max
   landscape) is just as short, and at full size the left rail could not fit
   above the log card — the Status rows were clipped. */
@media (max-height: 480px) {
  :root {
    --rail-w: 198px; --bar-h: 84px; --top-h: 34px;
    /* req 2 (round 20): the stages are centred, so a short screen only has to
       shrink them — they no longer compete with the log for the top-right. */
    --portrait-size: clamp(96px, 30vh, 150px);
    /* The log is deliberately short here: the left rail needs the room above it
       for the profile + the five status rows. --log-h is measured at runtime. */
    --log-w-base: 260px; --log-min: 88px;
    /* req 4: the class pucks were 34px here — below the 44px minimum touch
       target, so on a phone they were hard to hit. They are now 44px, the same
       floor the orb already used, and the arc keeps its shape because every
       offset is derived from --puck. */
    --puck: 44px; --slot: 36px; --orb: 48px; --die: 26px;
  }
  /* The arc needs vertical room; on a 375 px-tall phone it is tightened. */
  .hub-skills { width: calc(var(--puck) * 3.1); height: calc(var(--puck) * 3.35); }
  /* The rail must fit between the top strip and the log card — about 197 px on
     a 667x375 iPhone SE — so the profile and the five status rows are packed
     tighter here. Nothing is dropped: every stat stays readable and tappable. */
  .player-rail { gap: 4px; }
  .player-rail .corner-card { padding: 4px 6px; border-radius: 9px; }
  .player-rail .hub-card { padding: 4px 6px 5px; border-radius: 9px; }
  .player-rail .hub-title { font-size: 7px; margin-bottom: 2px; }
  .player-rail .pc-head { gap: 4px; }
  .player-rail .pc-crest { width: 20px; height: 20px; font-size: 11px; }
  .player-rail .pc-name { font-size: 9.5px; }
  .player-rail .pc-class { font-size: 7.5px; }
  .player-rail .pc-plaques { gap: 3px; margin: 2px 0; }
  .player-rail .pc-plaque { font-size: 7px; padding: 0 4px; }
  .player-rail .pc-plaque .pl-val { font-size: 8px; }
  .player-rail .pc-exp { margin: 1px 0; }
  .player-rail .pc-row { margin: 1px 0; }
  .player-rail .bar { height: 4px; }
  /* The five stat chips are dropped from the PROFILE here: the status card sits
     directly below and lists the same five values with their "+" buttons, so
     repeating them would only cost the vertical room the rail does not have.
     The profile keeps identity + vitals; the status card keeps the stats. */
  .player-rail .pc-stats { display: none; }
  /* req 2 (round 21): three rows of two stats. The card is content-sized now, so
     the rows only have to be tall enough to tap — they no longer have to divide a
     measured card height between five of them.
     req 6 (round 18) still holds: the icon column must be at least as wide as the
     puck inside it, or the puck spills over the label. The column stays 16px and
     the "+" gives up the width instead. */
  .player-rail .up-stats { gap: 3px; }
  .player-rail .up-stat-row {
    grid-template-columns: 16px 1fr auto 15px; gap: 2px; padding: 2px 2px;
    min-height: 17px;
  }
  /* req 6 (round 18): the icon column was 14px while the puck inside it was
     still 20px (the base rule), so the puck spilled over the label. The puck is
     now sized to the column, and the column is wide enough for it. */
  .player-rail .us-icon { width: 16px; height: 16px; display: flex; align-items: center; justify-content: center; }
  .player-rail .us-icon .icon3d { width: 14px; height: 14px; }
  .player-rail .us-icon .icon3d-face svg { width: 9px; height: 9px; }
  .player-rail .us-label { font-size: 7.5px; }
  .player-rail .us-val { font-size: 8px; }
  .player-rail .us-plus { width: 17px; height: 17px; font-size: 9px; }
  /* req 5 (round 18): the Status card never scrolls. The rows are flex tracks
     that shrink to the card, so there is nothing to scroll. */
  #statusCard .hub-body { overflow: hidden; }
  .dice-readout b { font-size: 15px; }
  /* The interaction slot is already a single row, so it only needs tighter gaps. */
  .turn-slot { gap: 5px; }
  .turn-slot-cell { gap: 5px; }
  .turn-dock .turn-timer-bar { width: min(150px, 30vw); }
  .hub-dice { padding: 2px 5px; }
  .battle-log { font-size: 9px; }
  .interaction-choice .ic-btn { width: calc(var(--orb) * 1.1); height: calc(var(--orb) * 1.1); }
  /* The wide pill must keep its auto width on short screens too, otherwise the
     square rule above collapses it and the label wraps into a 51px box. */
  .interaction-choice .ic-wide { width: auto; height: auto; padding: 6px 11px; }
  .interaction-choice .ic-note { font-size: 9.5px; padding: 4px 9px; max-width: 44vw; }
}

/* ---------- wide screens: a little more breathing room ---------- */
@media (min-width: 1100px) {
  /* --puck / --slot drive the arc offsets too, so they must be set here rather
     than on the buttons directly. */
  :root { --rail-w: 294px; --bar-h: 112px; --puck: 64px; --slot: 56px; --log-w-base: 340px; --portrait-size: clamp(150px, 26vh, 230px); }
  .orb { width: 62px; height: 62px; }
  .orb-end { width: 62px; height: 62px; }
}

/* ---------- portrait: the game is landscape-only ---------- */
.rotate-hint { display: none; }
@media (orientation: portrait) and (max-width: 1100px) {
  .rotate-hint {
    position: fixed; inset: 0; z-index: 900;
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: 14px; padding: 24px; text-align: center;
    background: radial-gradient(circle at 50% 40%, #1a2050, #070b18 70%);
    color: #e8eeff;
  }
  .rotate-hint .rh-icon {
    font-size: 54px; line-height: 1;
    animation: rotatePhone 2.4s ease-in-out infinite;
  }
  .rotate-hint .rh-title { font-size: 17px; font-weight: 900; letter-spacing: .5px; }
  .rotate-hint .rh-sub { font-size: 12px; color: #9fb0dd; max-width: 260px; line-height: 1.6; }
  #game .game-shell { visibility: hidden; }
}
@keyframes rotatePhone {
  0%, 40% { transform: rotate(0deg); }
  60%, 100% { transform: rotate(-90deg); }
}

/* ===========================================================================
   Turn panel + round counter — RETIRED
   ---------------------------------------------------------------------------
   The dice, ทอย and จบตา live in the bottom-centre interaction slot (#turnDock)
   and the round counter was removed with the Log button (req 7): the lap count
   now lives inside the player profile card. The rules below are kept as no-ops
   so any stale markup still renders harmlessly.
   =========================================================================== */
.turn-panel { display: none; }
.round-chip { display: none; }

/* ---------- smaller nameplates (req 11) ---------- */
.hud-label {
  font-size: 8.5px;
  padding: 1px 4px 2px;
  border-radius: 7px;
}
.hud-label .hud-bars { gap: 1px; margin-top: 1px; }
.hud-label .hud-bar { width: 32px; height: 3px; }
.hud-label.monster { gap: 2px; }
.hud-label.monster .hud-lv {
  padding: 0 5px;
  font-size: 7.5px;
  letter-spacing: .3px;
}
.hud-label.monster .hud-bar { width: 34px; height: 4px; }

/* ---------- item dock: three category slots, left of the basic attack -------
   The bag is grouped into three category slots (ยา / บัพ / LV Up), so the dock
   never needs to scroll: it is a fixed-width row that sits immediately LEFT of
   the basic attack puck inside #rightCluster. */
.item-list {
  display: flex; flex-direction: row; gap: 4px;
  align-items: flex-end; justify-content: flex-end;
  /* No bottom padding: the slots' bottom edge must land on the same line as the
     basic attack puck (req 2). The name labels hang below the slots and are
     absorbed by the bottom bar's own padding. */
  padding: 2px 2px 0;
}
.item-dock { justify-content: flex-end; }
/* ===========================================================================
   ROUND 30 — the reference-UI redesign
   ---------------------------------------------------------------------------
   The login, home, adventure, shop and rooms screens are rebuilt to follow the
   supplied reference art: a bright cartoon palette, chunky rounded cards with a
   thick light rim, gold-outlined buttons and a warm brown backdrop. The in-game
   HUD keeps its own dark glass theme (the board has to stay readable), so every
   rule below is scoped to the lobby-side screens.
   =========================================================================== */

:root {
  /* Reference palette: warm browns, cream cards, gold trim, one green CTA. */
  --ui-brown-0: #2a1608;
  --ui-brown-1: #4a2a12;
  --ui-brown-2: #6b3f1c;
  --ui-brown-3: #8a5527;
  --ui-cream-0: #fdf3dd;
  --ui-cream-1: #f6e6c4;
  --ui-cream-2: #e8d2a6;
  --ui-gold-0:  #ffd75e;
  --ui-gold-1:  #f0a92c;
  --ui-gold-2:  #b9741a;
  --ui-green-0: #7ee06a;
  --ui-green-1: #3fae3a;
  --ui-green-2: #237a24;
  --ui-blue-0:  #6fd0ff;
  --ui-blue-1:  #2b8fe0;
  --ui-blue-2:  #1a5fa8;
  --ui-red-0:   #ff7a6a;
  --ui-red-1:   #d63a2c;
  --ui-ink:     #3a2410;
  --ui-ink-soft:#6b4a2a;
  --ui-shadow:  0 6px 0 rgba(0, 0, 0, .28), 0 14px 26px rgba(0, 0, 0, .35);
  --ui-rim:     inset 0 2px 0 rgba(255, 255, 255, .75), inset 0 -3px 0 rgba(0, 0, 0, .16);
}

/* ---- Shared backdrop for every lobby-side screen ---- */
.home-bg, .auth-bg {
  position: fixed; inset: 0; z-index: 0;
  background:
    radial-gradient(900px 520px at 50% -10%, rgba(255, 214, 130, .30), transparent 62%),
    radial-gradient(700px 460px at 12% 100%, rgba(255, 150, 70, .18), transparent 60%),
    linear-gradient(180deg, #7a4a1e 0%, #5a3315 42%, #3a1f0c 100%);
}
.home-bg::after, .auth-bg::after {
  content: ""; position: absolute; inset: 0;
  background-image:
    radial-gradient(circle at 20% 30%, rgba(255,255,255,.05) 0 2px, transparent 3px),
    radial-gradient(circle at 70% 60%, rgba(255,255,255,.04) 0 2px, transparent 3px);
  background-size: 120px 120px, 90px 90px;
  opacity: .8;
}
/* The login backdrop is the bright sky of the reference art. */
.auth-bg {
  background:
    radial-gradient(1000px 620px at 50% 0%, #bfe9ff 0%, transparent 60%),
    radial-gradient(800px 520px at 15% 90%, #9fe0b0 0%, transparent 62%),
    linear-gradient(180deg, #7fd0f5 0%, #a8e2f7 45%, #cdeefb 100%);
}
.auth-bg::after { display: none; }

/* ============================ LOGIN ============================ */
#auth.screen.active {
  display: block;
  overflow-y: auto;
  overflow-x: hidden;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
  padding: max(10px, env(safe-area-inset-top)) max(10px, env(safe-area-inset-right))
           max(10px, env(safe-area-inset-bottom)) max(10px, env(safe-area-inset-left));
}

.auth-brand {
  position: relative; z-index: 2;
  display: inline-flex; align-items: baseline; gap: 1px;
  margin: 6px 0 0 10px;
  font-size: clamp(22px, 4.4vw, 34px); font-weight: 900; letter-spacing: -.5px;
  color: #fff;
  text-shadow: 0 3px 0 #1a5fa8, 0 6px 14px rgba(0, 0, 0, .3);
}
.auth-brand .brand-di { color: #ffe14d; text-shadow: 0 3px 0 #b9741a, 0 6px 14px rgba(0,0,0,.3); }
.auth-brand .brand-ce { color: #ffe14d; text-shadow: 0 3px 0 #b9741a, 0 6px 14px rgba(0,0,0,.3); }
.auth-brand .brand-dice { font-size: .8em; margin-left: 6px; }

.auth-status {
  position: absolute; z-index: 2;
  top: max(14px, env(safe-area-inset-top)); right: max(14px, env(safe-area-inset-right));
  display: inline-flex; align-items: center; gap: 7px;
  padding: 7px 16px; border-radius: 999px;
  background: #fff; color: #2b6b3a;
  font-size: clamp(10px, 2.2vw, 12px); font-weight: 800;
  box-shadow: 0 4px 12px rgba(0, 0, 0, .18);
}
.as-dot { width: 9px; height: 9px; border-radius: 50%; background: #35c759; box-shadow: 0 0 8px #35c759; }

.auth-card {
  position: relative; z-index: 2;
  width: 100%; max-width: min(430px, 100%);
  margin: clamp(10px, 3vh, 34px) auto 0;
  padding: clamp(16px, 3.4vw, 26px) clamp(14px, 3.2vw, 24px) clamp(14px, 3vw, 22px);
  background: linear-gradient(180deg, #4fb6f0 0%, #2f8fdc 100%);
  border: 4px solid #fff;
  border-radius: 26px;
  box-shadow: 0 10px 0 rgba(20, 80, 140, .45), 0 22px 44px rgba(0, 0, 0, .3);
  text-align: left;
  max-height: calc(100dvh - 40px);
  overflow-y: auto;
  overscroll-behavior: contain;
  animation: cardIn .4s cubic-bezier(.2,.9,.3,1.2);
}
.auth-card-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px; margin-bottom: 12px;
  padding-bottom: 10px;
  border-bottom: 2px solid rgba(255, 255, 255, .45);
}
.auth-title {
  margin: 0; font-size: clamp(17px, 3.6vw, 23px); font-weight: 900; color: #fff;
  text-shadow: 0 2px 0 rgba(0, 0, 0, .22);
}
.auth-title span { font-size: .72em; letter-spacing: 2px; opacity: .9; }
.auth-logo {
  flex: 0 0 auto;
  padding: 6px 12px; border-radius: 14px;
  background: #fff;
  font-size: clamp(11px, 2.4vw, 14px); font-weight: 900; letter-spacing: -.3px;
  color: #1a5fa8;
  box-shadow: 0 3px 0 rgba(0, 0, 0, .18);
}
.auth-logo .brand-di, .auth-logo .brand-ce { color: #f0a92c; }

/* White rounded fields, exactly like the reference. */
#auth .field { margin-bottom: 10px; }
#auth .field label {
  margin-bottom: 5px;
  font-size: clamp(11px, 2.4vw, 13px); font-weight: 800; letter-spacing: 0;
  color: #fff; text-shadow: 0 1px 0 rgba(0, 0, 0, .2);
}
#auth input {
  padding: 12px 16px;
  background: #fff;
  border: 3px solid #cfe9fb;
  border-radius: 999px;
  color: #23405c; font-size: clamp(13px, 2.8vw, 15px); font-weight: 700;
  box-shadow: inset 0 2px 5px rgba(0, 0, 0, .08);
}
#auth input:focus { border-color: #ffe14d; box-shadow: 0 0 0 3px rgba(255, 225, 77, .45); }
#auth input::placeholder { color: #9db6c9; font-weight: 600; }

.auth-remember {
  display: flex; align-items: flex-start; gap: 8px;
  margin: 2px 0 10px;
  font-size: clamp(10px, 2.2vw, 11.5px); font-weight: 700; line-height: 1.35;
  color: #eaf7ff;
}
.auth-remember input {
  flex: 0 0 auto; width: 18px; height: 18px; margin: 0;
  accent-color: #ffe14d;
}

.auth-tabs {
  display: flex; gap: 6px; margin: 0 0 12px;
  padding: 4px; border-radius: 999px;
  background: rgba(10, 60, 110, .35);
  border: 2px solid rgba(255, 255, 255, .35);
}
.auth-tab {
  flex: 1 1 0; min-width: 0;
  padding: 8px 6px; border: 0; border-radius: 999px;
  background: transparent; color: #d6efff;
  font: inherit; font-size: clamp(11px, 2.6vw, 13px); font-weight: 900;
  cursor: pointer; transition: background .16s, color .16s;
}
.auth-tab.is-on {
  background: linear-gradient(180deg, #ffe98a, #f5b731);
  color: #6b3f0a;
  box-shadow: 0 3px 0 #b9741a;
}

/* The one green call to action. */
.btn-go, .btn-guest {
  width: 100%;
  padding: 13px 18px;
  border-radius: 999px;
  font-size: clamp(14px, 3vw, 17px); font-weight: 900; letter-spacing: .4px;
  color: #fff;
  text-shadow: 0 2px 0 rgba(0, 0, 0, .25);
  border: 3px solid #fff;
  transition: transform .1s, filter .16s;
}
.btn-go {
  background: linear-gradient(180deg, var(--ui-green-0) 0%, var(--ui-green-1) 100%);
  box-shadow: 0 5px 0 var(--ui-green-2), 0 12px 22px rgba(0, 0, 0, .28);
}
.btn-guest {
  background: linear-gradient(180deg, #9fb4c9 0%, #6d8299 100%);
  box-shadow: 0 5px 0 #4a5c70, 0 12px 22px rgba(0, 0, 0, .28);
}
.btn-go:hover:not(:disabled), .btn-guest:hover:not(:disabled) { filter: brightness(1.07); }
.btn-go:active:not(:disabled), .btn-guest:active:not(:disabled) { transform: translateY(3px); box-shadow: 0 2px 0 rgba(0,0,0,.3); }

.auth-or {
  display: flex; align-items: center; gap: 10px;
  margin: 12px 0 10px;
  color: #eaf7ff; font-size: 11px; font-weight: 800; letter-spacing: 2px;
}
.auth-or::before, .auth-or::after {
  content: ""; flex: 1; height: 2px; border-radius: 2px;
  background: rgba(255, 255, 255, .4);
}
#auth .auth-note {
  margin: 10px 0 0; color: #e2f4ff;
  font-size: clamp(10px, 2.2vw, 11px); line-height: 1.5; font-weight: 600;
}
#auth .msg { color: #ffe14d; font-weight: 800; text-shadow: 0 1px 0 rgba(0,0,0,.25); }

.auth-lang {
  position: relative; z-index: 2;
  display: inline-flex; align-items: center; gap: 7px;
  margin: 14px 0 6px 10px;
  padding: 8px 20px; border-radius: 999px;
  background: #fff; color: #23405c;
  font-size: clamp(11px, 2.4vw, 13px); font-weight: 800;
  box-shadow: 0 4px 12px rgba(0, 0, 0, .18);
}
.al-ico { font-size: 1.1em; }

/* ============================ HOME ============================ */
#lobby.screen.active {
  display: block;
  overflow: hidden;
  padding: 0;
}

.home-top {
  position: absolute; z-index: 3;
  top: max(8px, env(safe-area-inset-top));
  left: max(10px, env(safe-area-inset-left));
  right: max(10px, env(safe-area-inset-right));
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  pointer-events: none;
}
.home-top > * { pointer-events: auto; }

.home-id {
  display: flex; align-items: center; gap: 9px;
  padding: 5px 14px 5px 5px;
  background: linear-gradient(180deg, rgba(60, 32, 12, .92), rgba(38, 20, 8, .92));
  border: 2px solid rgba(255, 215, 94, .55);
  border-radius: 999px;
  box-shadow: 0 5px 0 rgba(0, 0, 0, .3), 0 12px 22px rgba(0, 0, 0, .3);
  max-width: min(52vw, 340px);
}
.home-avatar {
  flex: 0 0 auto;
  width: clamp(30px, 5.4vw, 42px); height: clamp(30px, 5.4vw, 42px);
  display: grid; place-items: center;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, #ffe9a8, #e0a63a);
  border: 2px solid #fff;
  font-size: clamp(15px, 2.8vw, 20px);
  box-shadow: 0 2px 6px rgba(0, 0, 0, .35);
}
.home-id-text { min-width: 0; }
.home-name {
  font-size: clamp(12px, 2.6vw, 15px); font-weight: 900; color: #fff;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  text-shadow: 0 2px 0 rgba(0, 0, 0, .35);
}
.home-role {
  font-size: clamp(9px, 2vw, 11px); font-weight: 700; color: #ffd75e;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.home-id .whoami-tag {
  flex: 0 0 auto;
  padding: 2px 8px; border-radius: 999px;
  font-size: 9px; font-weight: 900; letter-spacing: .6px;
  background: rgba(255, 215, 94, .2);
  border: 1px solid rgba(255, 215, 94, .6);
  color: #ffe6a3;
}
.home-id .whoami-tag.is-member {
  background: rgba(126, 224, 106, .2);
  border-color: rgba(126, 224, 106, .6);
  color: #c8f7bb;
}

.home-wallet { display: flex; align-items: center; gap: 8px; }
.wallet-pill {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 6px 16px 6px 8px;
  border-radius: 999px;
  background: linear-gradient(180deg, rgba(60, 32, 12, .92), rgba(38, 20, 8, .92));
  border: 2px solid rgba(255, 215, 94, .55);
  color: #ffe6a3;
  font-size: clamp(12px, 2.6vw, 15px); font-weight: 900;
  box-shadow: 0 5px 0 rgba(0, 0, 0, .3);
}
.wallet-ico {
  display: grid; place-items: center;
  width: clamp(20px, 3.6vw, 26px); height: clamp(20px, 3.6vw, 26px);
  border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, #ffe9a8, #e0a63a);
  font-size: clamp(11px, 2.2vw, 14px);
}
.home-icon-btn {
  width: clamp(30px, 5vw, 38px); height: clamp(30px, 5vw, 38px);
  display: grid; place-items: center;
  border-radius: 50%;
  background: linear-gradient(180deg, rgba(60, 32, 12, .92), rgba(38, 20, 8, .92));
  border: 2px solid rgba(255, 215, 94, .55);
  color: #ffe6a3; font-size: clamp(13px, 2.6vw, 16px);
  box-shadow: 0 4px 0 rgba(0, 0, 0, .3);
  transition: transform .1s, filter .16s;
}
.home-icon-btn:hover { filter: brightness(1.15); }
.home-icon-btn:active { transform: translateY(2px); }

/* Left menu column. */
.home-menu {
  position: absolute; z-index: 3;
  left: max(10px, env(safe-area-inset-left));
  top: 50%; transform: translateY(-50%);
  display: flex; flex-direction: column; gap: 8px;
}
.home-menu-item {
  display: flex; align-items: center; gap: 9px;
  padding: 8px 16px 8px 8px;
  border-radius: 999px;
  background: linear-gradient(180deg, rgba(60, 32, 12, .9), rgba(38, 20, 8, .9));
  border: 2px solid rgba(255, 215, 94, .45);
  color: #ffe6a3;
  font: inherit; font-size: clamp(11px, 2.4vw, 13px); font-weight: 900;
  box-shadow: 0 4px 0 rgba(0, 0, 0, .3);
  transition: transform .1s, filter .16s, border-color .16s;
  white-space: nowrap;
}
.home-menu-item:hover { filter: brightness(1.15); border-color: rgba(255, 215, 94, .85); }
.home-menu-item:active { transform: translateY(2px); }
.hm-ico {
  display: grid; place-items: center;
  width: clamp(24px, 4.2vw, 30px); height: clamp(24px, 4.2vw, 30px);
  border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, #ffe9a8, #e0a63a);
  font-size: clamp(12px, 2.4vw, 15px);
}

/* Right column: the mode cards. */
.home-modes {
  position: absolute; z-index: 3;
  right: max(10px, env(safe-area-inset-right));
  top: 50%; transform: translateY(-50%);
  display: flex; flex-direction: column; gap: 9px;
  width: min(38vw, 330px);
}
.mode-card {
  display: flex; flex-direction: column; align-items: flex-start; gap: 2px;
  padding: 10px 14px;
  border-radius: 16px;
  background: linear-gradient(180deg, rgba(70, 38, 14, .94), rgba(40, 21, 8, .94));
  border: 2px solid rgba(255, 215, 94, .5);
  color: #fff; text-align: left;
  font: inherit;
  box-shadow: 0 5px 0 rgba(0, 0, 0, .32), 0 12px 22px rgba(0, 0, 0, .28);
  transition: transform .1s, filter .16s, border-color .16s;
}
.mode-card:hover { filter: brightness(1.12); border-color: rgba(255, 215, 94, .9); }
.mode-card:active { transform: translateY(2px); }
.mode-title { font-size: clamp(12px, 2.6vw, 15px); font-weight: 900; text-shadow: 0 2px 0 rgba(0,0,0,.35); }
.mode-sub { font-size: clamp(9px, 2vw, 11px); font-weight: 700; color: #e8cfa4; }
.mode-meta {
  margin-top: 3px;
  padding: 2px 9px; border-radius: 999px;
  background: rgba(255, 215, 94, .16);
  border: 1px solid rgba(255, 215, 94, .45);
  font-size: clamp(9px, 1.9vw, 10.5px); font-weight: 800; color: #ffe6a3;
}

/* The big START GAME puck. */
.start-puck {
  position: absolute; z-index: 3;
  right: max(14px, env(safe-area-inset-right));
  bottom: max(14px, env(safe-area-inset-bottom));
  width: clamp(84px, 15vw, 132px); height: clamp(84px, 15vw, 132px);
  display: grid; place-content: center; gap: 0;
  border-radius: 50%;
  background: radial-gradient(circle at 38% 30%, #ff9a7a 0%, #e8402c 55%, #a81f14 100%);
  border: 4px solid #ffd75e;
  color: #fff;
  font: inherit; font-weight: 900; line-height: 1.02;
  text-shadow: 0 2px 0 rgba(0, 0, 0, .4);
  box-shadow: 0 8px 0 #7d140c, 0 18px 30px rgba(0, 0, 0, .45), inset 0 3px 0 rgba(255,255,255,.4);
  transition: transform .1s, filter .16s;
}
.start-puck .sp-line { font-size: clamp(12px, 2.4vw, 18px); letter-spacing: .5px; }
.start-puck:hover { filter: brightness(1.08); }
.start-puck:active { transform: translateY(4px); box-shadow: 0 4px 0 #7d140c, 0 10px 18px rgba(0,0,0,.4); }

/* Queue panel + room panel float over the home screen. */
.home-queue {
  position: absolute; z-index: 4;
  left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: min(88vw, 380px);
  margin: 0;
  background: linear-gradient(180deg, rgba(70, 38, 14, .97), rgba(40, 21, 8, .97));
  border: 2px solid rgba(255, 215, 94, .6);
  border-radius: 18px;
  box-shadow: 0 10px 0 rgba(0, 0, 0, .3), 0 22px 44px rgba(0, 0, 0, .45);
  color: #ffe6a3;
}
.home-queue .queue-head { color: #fff; }
.home-queue .queue-meta { color: #e8cfa4; }
.home-queue .queue-list li {
  background: rgba(255, 215, 94, .14);
  border-color: rgba(255, 215, 94, .4);
  color: #ffe6a3;
}
.home-queue .btn-ghost { color: #ffe6a3; border-color: rgba(255, 215, 94, .45); }

.home-msg {
  position: absolute; z-index: 4;
  left: 50%; bottom: max(12px, env(safe-area-inset-bottom));
  transform: translateX(-50%);
  margin: 0; max-width: min(70vw, 520px); text-align: center;
  color: #ffe14d; font-weight: 800;
  text-shadow: 0 2px 0 rgba(0, 0, 0, .4);
}

.home-room {
  position: absolute; z-index: 4;
  left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: min(90vw, 400px); max-height: 76vh; overflow-y: auto;
  margin: 0;
  background: linear-gradient(180deg, rgba(70, 38, 14, .97), rgba(40, 21, 8, .97));
  border: 2px solid rgba(255, 215, 94, .6);
  border-radius: 18px;
  box-shadow: 0 10px 0 rgba(0, 0, 0, .3), 0 22px 44px rgba(0, 0, 0, .45);
  color: #fff;
}
.home-room .room-label, .home-room .room-stage-label { color: #e8cfa4; }
.home-room .room-stage {
  background: rgba(255, 215, 94, .12);
  border-color: rgba(255, 215, 94, .4);
}
.home-room .room-stage-name { color: #ffe6a3; }
.home-room #playerList li { border-bottom-color: rgba(255, 215, 94, .18); color: #fff; }

/* ============================ SUB SCREENS ============================ */
#adventure.screen.active, #shop.screen.active, #rooms.screen.active {
  display: flex; flex-direction: column;
  overflow: hidden;
  padding: 0;
}

.sub-top {
  position: relative; z-index: 3;
  flex: 0 0 auto;
  display: flex; align-items: center; gap: 12px;
  padding: max(8px, env(safe-area-inset-top)) max(12px, env(safe-area-inset-right)) 8px max(12px, env(safe-area-inset-left));
  background: linear-gradient(180deg, rgba(30, 16, 6, .92), rgba(30, 16, 6, 0));
}
.back-btn {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 7px 18px 7px 8px;
  border-radius: 999px;
  background: linear-gradient(180deg, #ffd75e, #e0a63a);
  border: 2px solid #fff;
  color: #5a3310;
  font: inherit; font-size: clamp(11px, 2.4vw, 13px); font-weight: 900;
  box-shadow: 0 4px 0 #a8701a, 0 10px 18px rgba(0, 0, 0, .3);
  transition: transform .1s, filter .16s;
}
.back-btn:hover { filter: brightness(1.08); }
.back-btn:active { transform: translateY(2px); }
.back-ico {
  display: grid; place-items: center;
  width: clamp(22px, 4vw, 28px); height: clamp(22px, 4vw, 28px);
  border-radius: 50%;
  background: rgba(255, 255, 255, .55);
  font-size: clamp(12px, 2.2vw, 14px);
}
.sub-title {
  flex: 1 1 auto; min-width: 0; margin: 0;
  font-size: clamp(14px, 3vw, 20px); font-weight: 900; color: #fff;
  text-shadow: 0 2px 0 rgba(0, 0, 0, .4);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ---- Adventure: one tall card per stage ---- */
.adv-scroll {
  position: relative; z-index: 2;
  flex: 1 1 auto; min-height: 0;
  overflow-y: auto; overflow-x: hidden;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
  padding: 4px max(12px, env(safe-area-inset-right)) max(14px, env(safe-area-inset-bottom)) max(12px, env(safe-area-inset-left));
}
.adv-cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
  gap: 10px;
  align-items: stretch;
}
.adv-card {
  display: flex; flex-direction: column;
  min-height: 0;
  border-radius: 16px;
  overflow: hidden;
  background: linear-gradient(180deg, #3a2410 0%, #24140a 100%);
  border: 3px solid #6b4a2a;
  box-shadow: 0 6px 0 rgba(0, 0, 0, .35), 0 14px 26px rgba(0, 0, 0, .35);
  color: #fff; text-align: left;
  font: inherit;
  transition: transform .1s, border-color .16s, filter .16s;
}
.adv-card:hover:not(:disabled) { border-color: var(--ui-gold-0); filter: brightness(1.06); }
.adv-card:active:not(:disabled) { transform: translateY(2px); }
.adv-card:disabled { filter: grayscale(.55) brightness(.72); cursor: not-allowed; }
.adv-card.is-cleared { border-color: var(--ui-green-1); }
.adv-card-art {
  position: relative;
  height: clamp(74px, 15vh, 118px);
  display: grid; place-items: center;
  font-size: clamp(30px, 7vw, 46px);
  background:
    radial-gradient(circle at 50% 40%, rgba(255, 215, 94, .28), transparent 62%),
    linear-gradient(180deg, #5a3a1c, #2a1608);
  border-bottom: 2px solid rgba(255, 215, 94, .3);
}
.adv-card-art .aca-scene {
  position: absolute; inset: 0;
  opacity: .5;
  background-size: cover; background-position: center;
}
.adv-card-art .aca-ico { position: relative; z-index: 1; filter: drop-shadow(0 3px 6px rgba(0,0,0,.5)); }
.adv-card-body { flex: 1 1 auto; display: flex; flex-direction: column; gap: 3px; padding: 8px 10px 10px; }
.adv-card-name { font-size: clamp(12px, 2.6vw, 14px); font-weight: 900; }
.adv-card-desc { font-size: clamp(9px, 2vw, 11px); font-weight: 600; color: #d9c3a0; line-height: 1.35; }
.adv-card-meta {
  margin-top: auto;
  display: flex; align-items: center; justify-content: space-between; gap: 6px;
  font-size: clamp(9px, 2vw, 11px); font-weight: 800; color: var(--ui-gold-0);
}
.adv-card.is-cleared .adv-card-meta { color: #9ff0a0; }
.adv-card-go {
  margin-top: 6px;
  padding: 7px 10px;
  border-radius: 999px;
  background: linear-gradient(180deg, var(--ui-green-0), var(--ui-green-1));
  border: 2px solid #fff;
  color: #fff; font-size: clamp(11px, 2.4vw, 13px); font-weight: 900;
  text-align: center;
  text-shadow: 0 2px 0 rgba(0, 0, 0, .25);
  box-shadow: 0 3px 0 var(--ui-green-2);
}
.adv-card:disabled .adv-card-go {
  background: linear-gradient(180deg, #7a6a58, #4a3c2e);
  box-shadow: 0 3px 0 #2a2018;
}
#adventure .adv-note {
  margin-top: 10px;
  background: rgba(255, 180, 60, .14);
  border-color: rgba(255, 180, 60, .45);
  color: #ffd479;
}

/* ---- Shop ---- */
.shop-body {
  position: relative; z-index: 2;
  flex: 1 1 auto; min-height: 0;
  display: flex; gap: 10px;
  padding: 4px max(12px, env(safe-area-inset-right)) max(14px, env(safe-area-inset-bottom)) max(12px, env(safe-area-inset-left));
}
.shop-main {
  flex: 1 1 auto; min-width: 0;
  display: flex; flex-direction: column; gap: 8px;
  padding: 10px;
  border-radius: 18px;
  background: linear-gradient(180deg, rgba(253, 243, 221, .96), rgba(232, 210, 166, .96));
  border: 3px solid #fff;
  box-shadow: 0 8px 0 rgba(0, 0, 0, .3), 0 18px 32px rgba(0, 0, 0, .35);
}
.shop-tabs { display: flex; gap: 6px; flex-wrap: wrap; }
.shop-tab {
  padding: 7px 16px;
  border-radius: 10px;
  background: linear-gradient(180deg, #8a5527, #5a3315);
  border: 2px solid #b9741a;
  color: #ffe6a3;
  font: inherit; font-size: clamp(10px, 2.2vw, 12px); font-weight: 900;
  box-shadow: 0 3px 0 rgba(0, 0, 0, .3);
}
.shop-tab.is-on {
  background: linear-gradient(180deg, #ffd75e, #e0a63a);
  border-color: #fff;
  color: #5a3310;
}
.shop-tab:disabled { opacity: .55; cursor: not-allowed; }
.shop-grid-wrap {
  flex: 1 1 auto; min-height: 0;
  overflow-y: auto; overflow-x: hidden;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
}
.shop-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  gap: 8px;
}
.shop-item {
  display: flex; align-items: center; gap: 9px;
  padding: 8px;
  border-radius: 12px;
  background: linear-gradient(180deg, #fffaf0, #f0e2c6);
  border: 2px solid #c9a86e;
  color: var(--ui-ink); text-align: left;
  font: inherit;
  box-shadow: 0 3px 0 rgba(0, 0, 0, .18);
  transition: transform .1s, border-color .16s, filter .16s;
}
.shop-item:hover:not(:disabled) { border-color: var(--ui-gold-1); filter: brightness(1.03); }
.shop-item:active:not(:disabled) { transform: translateY(2px); }
.shop-item.is-owned { border-color: var(--ui-green-1); background: linear-gradient(180deg, #f0fff0, #d8f2d8); }
.shop-item:disabled { cursor: default; }
.shop-item-ico {
  flex: 0 0 auto;
  width: clamp(34px, 6vw, 44px); height: clamp(34px, 6vw, 44px);
  display: grid; place-items: center;
  border-radius: 10px;
  background: radial-gradient(circle at 35% 30%, #ffe9a8, #d99a2c);
  border: 2px solid #fff;
  font-size: clamp(16px, 3.2vw, 21px);
  box-shadow: 0 2px 5px rgba(0, 0, 0, .2);
}
.shop-item-body { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.shop-item-name { font-size: clamp(11px, 2.4vw, 13px); font-weight: 900; }
.shop-item-tag { font-size: clamp(9px, 2vw, 11px); font-weight: 800; color: #8a5527; }
.shop-item.is-owned .shop-item-tag { color: var(--ui-green-2); }
.shop-item-buy {
  flex: 0 0 auto;
  padding: 6px 12px;
  border-radius: 999px;
  background: linear-gradient(180deg, var(--ui-green-0), var(--ui-green-1));
  border: 2px solid #fff;
  color: #fff; font-size: clamp(10px, 2.2vw, 12px); font-weight: 900;
  text-shadow: 0 1px 0 rgba(0, 0, 0, .25);
  box-shadow: 0 3px 0 var(--ui-green-2);
}
.shop-item.is-owned .shop-item-buy {
  background: linear-gradient(180deg, #b9c9b9, #8a9a8a);
  box-shadow: 0 3px 0 #5a6a5a;
}
.shop-foot {
  flex: 0 0 auto;
  display: flex; align-items: center; justify-content: flex-end;
  padding-top: 6px;
  border-top: 2px solid rgba(138, 85, 39, .25);
}
.shop-foot .shop-price { color: #6b4a2a; font-weight: 900; }

.shop-side {
  flex: 0 0 auto;
  width: min(30vw, 250px);
  display: flex; flex-direction: column; gap: 8px;
  padding: 10px;
  border-radius: 18px;
  background: linear-gradient(180deg, #f7b96a, #e08a3a);
  border: 3px solid #fff;
  box-shadow: 0 8px 0 rgba(0, 0, 0, .3), 0 18px 32px rgba(0, 0, 0, .35);
}
.shop-side-title {
  font-size: clamp(11px, 2.4vw, 13px); font-weight: 900; color: #5a3310;
  text-align: center;
}
.shop-side-body {
  flex: 1 1 auto; min-height: 0;
  display: grid; place-items: center;
  padding: 10px;
  border-radius: 12px;
  background: rgba(255, 255, 255, .35);
  border: 2px dashed rgba(90, 51, 16, .35);
}
.shop-side-empty {
  font-size: clamp(10px, 2.2vw, 12px); font-weight: 800; color: #6b4a2a;
  text-align: center; line-height: 1.4;
}
.shop-side-card { display: flex; flex-direction: column; align-items: center; gap: 6px; text-align: center; }
.shop-side-ico {
  width: clamp(48px, 9vw, 68px); height: clamp(48px, 9vw, 68px);
  display: grid; place-items: center;
  border-radius: 16px;
  background: radial-gradient(circle at 35% 30%, #fff3c4, #d99a2c);
  border: 3px solid #fff;
  font-size: clamp(24px, 5vw, 34px);
  box-shadow: 0 4px 10px rgba(0, 0, 0, .25);
}
.shop-side-name { font-size: clamp(12px, 2.6vw, 15px); font-weight: 900; color: #3a2410; }
.shop-side-desc { font-size: clamp(9px, 2vw, 11px); font-weight: 700; color: #5a3310; line-height: 1.4; }

/* ---- Rooms ---- */
.rooms-body {
  position: relative; z-index: 2;
  flex: 1 1 auto; min-height: 0;
  display: flex; gap: 10px;
  padding: 4px max(12px, env(safe-area-inset-right)) max(14px, env(safe-area-inset-bottom)) max(12px, env(safe-area-inset-left));
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
}
.rooms-left, .rooms-right {
  flex: 1 1 0; min-width: 0;
  display: flex; flex-direction: column; gap: 8px;
  padding: 12px;
  border-radius: 18px;
  background: linear-gradient(180deg, rgba(253, 243, 221, .96), rgba(232, 210, 166, .96));
  border: 3px solid #fff;
  box-shadow: 0 8px 0 rgba(0, 0, 0, .3), 0 18px 32px rgba(0, 0, 0, .35);
  color: var(--ui-ink);
}
.rooms-left .field label, .rooms-right .field label { color: #6b4a2a; }
.rooms-left input, .rooms-right input {
  background: #fff;
  border: 2px solid #c9a86e;
  border-radius: 12px;
  color: var(--ui-ink); font-weight: 700;
}
.rooms-left input:focus, .rooms-right input:focus {
  border-color: var(--ui-gold-1);
  box-shadow: 0 0 0 3px rgba(240, 169, 44, .3);
}
.rooms-left .btn-primary {
  background: linear-gradient(180deg, var(--ui-green-0), var(--ui-green-1));
  border: 2px solid #fff;
  color: #fff; font-weight: 900;
  box-shadow: 0 4px 0 var(--ui-green-2);
}
.rooms-left .btn-secondary {
  background: linear-gradient(180deg, #ffd75e, #e0a63a);
  border: 2px solid #fff;
  color: #5a3310; font-weight: 900;
  box-shadow: 0 4px 0 #a8701a;
}
.rooms-left .browser {
  background: rgba(255, 255, 255, .55);
  border: 2px solid #c9a86e;
  border-radius: 12px;
}
.rooms-left .browser-title { color: #6b4a2a; }
.rooms-left .room-row {
  background: #fff;
  border: 2px solid #dcc79a;
  color: var(--ui-ink);
}
.rooms-left .room-row:hover { background: #fff8e6; border-color: var(--ui-gold-1); }
.rooms-left .room-row .rr-code { color: #b9741a; }
.rooms-left .room-row .rr-seats { color: #8a5527; }
.rooms-left .room-list li.room-empty { color: #8a5527; }
.rooms-right .scene-btn {
  background: #fff;
  border: 2px solid #dcc79a;
  color: #6b4a2a;
}
.rooms-right .scene-btn:hover:not(:disabled) { border-color: var(--ui-gold-1); color: var(--ui-ink); }
.rooms-right .scene-btn.is-on {
  background: linear-gradient(180deg, #fff3c4, #ffe08a);
  border-color: var(--ui-gold-1);
  color: #5a3310;
  box-shadow: 0 0 0 3px rgba(240, 169, 44, .3);
}

/* ---- Narrow screens: stack the columns, shrink the chrome ---- */
@media (max-width: 760px) {
  .home-menu { gap: 6px; }
  .home-menu-item { padding: 6px 12px 6px 6px; }
  .home-modes { width: min(46vw, 260px); gap: 6px; }
  .mode-card { padding: 8px 11px; }
  .start-puck { width: clamp(70px, 17vw, 96px); height: clamp(70px, 17vw, 96px); }
  .shop-body, .rooms-body { flex-direction: column; }
  .shop-side { width: auto; flex: 0 0 auto; }
  .shop-side-body { min-height: 70px; }
  .rooms-left, .rooms-right { flex: 0 0 auto; }
  .adv-cards { grid-template-columns: repeat(auto-fill, minmax(132px, 1fr)); }
}
@media (max-height: 460px) {
  .home-menu-item .hm-label { display: none; }
  .home-menu-item { padding: 6px; }
  .mode-sub { display: none; }
  .adv-card-art { height: 56px; }
}

/* The home card carries the editable display name inline (a guest picks it here;
   a member sees their locked account name). */
.home-name-input {
  width: 100%; max-width: 100%;
  padding: 0; margin: 0;
  background: transparent; border: 0; border-radius: 0;
  color: #fff; font-size: clamp(12px, 2.6vw, 15px); font-weight: 900;
  text-shadow: 0 2px 0 rgba(0, 0, 0, .35);
  outline: none;
}
.home-name-input::placeholder { color: rgba(255, 255, 255, .55); }
.home-name-input:focus { box-shadow: none; }
.home-name-input.is-locked { color: #ffe6a3; cursor: not-allowed; }

/* Create / join controls on the home screen, bottom-left. */
.home-join {
  position: absolute; z-index: 3;
  left: max(10px, env(safe-area-inset-left));
  bottom: max(12px, env(safe-area-inset-bottom));
  display: flex; align-items: center; gap: 6px;
  max-width: min(56vw, 460px);
}
.home-join input {
  width: clamp(84px, 14vw, 130px);
  padding: 8px 12px;
  background: rgba(38, 20, 8, .9);
  border: 2px solid rgba(255, 215, 94, .5);
  border-radius: 999px;
  color: #ffe6a3; font-size: clamp(10px, 2.2vw, 12px); font-weight: 800;
}
.home-join input::placeholder { color: rgba(255, 230, 163, .5); }
.home-join .btn {
  padding: 8px 14px;
  border-radius: 999px;
  font-size: clamp(10px, 2.2vw, 12px); font-weight: 900;
  border: 2px solid #fff;
}
.home-join .btn-primary {
  background: linear-gradient(180deg, var(--ui-green-0), var(--ui-green-1));
  color: #fff; box-shadow: 0 4px 0 var(--ui-green-2);
}
.home-join .btn-secondary {
  background: linear-gradient(180deg, #ffd75e, #e0a63a);
  color: #5a3310; box-shadow: 0 4px 0 #a8701a;
}

/* Centre showcase on the home screen: the game mark over a lit dice pedestal. */
.home-hero {
  position: absolute; z-index: 2;
  left: 50%; top: 50%; transform: translate(-50%, -50%);
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  pointer-events: none;
  text-align: center;
}
.hero-mark {
  font-size: clamp(30px, 6.4vw, 62px); font-weight: 900; letter-spacing: -1px;
  color: #fff;
  text-shadow: 0 4px 0 #8a5527, 0 10px 24px rgba(0, 0, 0, .45);
}
.hero-mark .brand-di, .hero-mark .brand-ce {
  color: #ffe14d;
  text-shadow: 0 4px 0 #b9741a, 0 10px 24px rgba(0, 0, 0, .45);
}
.hero-sub {
  font-size: clamp(10px, 2.2vw, 14px); font-weight: 800; letter-spacing: 2px;
  color: #ffe6a3; text-transform: uppercase;
  text-shadow: 0 2px 0 rgba(0, 0, 0, .4);
}
.hero-stage {
  position: relative;
  width: clamp(110px, 20vw, 190px); height: clamp(110px, 20vw, 190px);
  margin-top: clamp(6px, 1.6vh, 16px);
  display: grid; place-items: center;
}
.hero-pedestal {
  position: absolute; bottom: 4%; left: 50%; transform: translateX(-50%);
  width: 92%; height: 26%;
  border-radius: 50%;
  background: radial-gradient(ellipse at 50% 40%, rgba(255, 215, 94, .55), rgba(255, 150, 60, .12) 62%, transparent 78%);
  filter: blur(2px);
}
.hero-die {
  position: relative; z-index: 1;
  width: 58%; height: 58%;
  border-radius: 22%;
  background: linear-gradient(150deg, #fffdf5 0%, #ffe9b0 55%, #e8b95c 100%);
  border: 3px solid #fff;
  box-shadow: 0 10px 0 rgba(120, 70, 20, .55), 0 20px 34px rgba(0, 0, 0, .45), inset 0 3px 0 rgba(255,255,255,.9);
  transform: rotate(-8deg);
  animation: heroDie 4.2s ease-in-out infinite;
}
@keyframes heroDie {
  0%, 100% { transform: rotate(-8deg) translateY(0); }
  50%      { transform: rotate(-4deg) translateY(-6px); }
}
.hd-face {
  position: absolute;
  width: 15%; height: 15%;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, #6b4a2a, #2a1608);
  box-shadow: inset 0 1px 2px rgba(255,255,255,.35);
}
.hd-1 { top: 18%; left: 18%; }
.hd-2 { top: 18%; left: 42.5%; }
.hd-3 { top: 18%; right: 18%; }
.hd-4 { bottom: 18%; left: 18%; }
.hd-5 { bottom: 18%; left: 42.5%; }
.hd-6 { bottom: 18%; right: 18%; }
.hero-tagline {
  margin-top: clamp(4px, 1.2vh, 12px);
  font-size: clamp(9px, 2vw, 12px); font-weight: 800; color: #e8cfa4;
  text-shadow: 0 2px 0 rgba(0, 0, 0, .4);
}
@media (max-width: 760px) {
  .hero-mark { font-size: clamp(22px, 7vw, 34px); }
  .hero-stage { width: clamp(80px, 22vw, 120px); height: clamp(80px, 22vw, 120px); }
  .hero-tagline { display: none; }
}

/* Narrow landscape: the hero must not run under the mode cards. */
@media (max-width: 900px) {
  .home-hero { max-width: 46vw; }
  .hero-mark { font-size: clamp(18px, 4.2vw, 30px); }
  .hero-sub { font-size: clamp(8px, 1.6vw, 10px); letter-spacing: 1px; }
  .hero-stage { width: clamp(58px, 11vw, 92px); height: clamp(58px, 11vw, 92px); margin-top: 4px; }
  .hero-tagline { display: none; }
  .home-modes { width: min(40vw, 300px); }
}
