/* ==========================================================================
   NZEP 资料库 · assets/games/sandbox.css —— 「沙盘占点」的样式
   --------------------------------------------------------------------------
   约定（与棋牌/卡兹那两份一致，别另发明一套）：
     · 颜色 / 间距 / 字体一律用 nzep.css 里的 CSS 变量（--text-2、--border-faint…）；
     · 大厅/房间的通用零件（g-btn / g-input / g-tag / g-note / g-rooms / g-log）
       直接用 games.css 里的，**不覆盖**它们；
     · 本文件只写 sb- 前缀自己的东西：地图画布、节点、单位、手牌、技能、编辑器。

   两条硬约束（页面上会真的量，见 tools/test-sandbox-ui.js 的手机适配那一段）：
     ① **不许横向滚动**：地图放大后只在 .sb-stage 里裁切（overflow:hidden），
        手牌那一排自己 overflow-x:auto（它的宽度是被父级限住的，不会撑破页面）。
     ② **所有可点元素 ≥44px**：手机上（≤767px）统一抬到 44px；地图里的节点/单位
        在**手机的最小缩放**下也要 ≥44px 屏幕像素（画布坐标 → 见 sandbox-page.js 的
        K_MIN_MOBILE：62px 画布 = 44.6px 屏幕）。

   ★ 2026-10-06 热修（用户报「沙盘太劣质小了 / 还能随便拖到 / 电脑端点都点不到 / 点的显示太大」）：
     · 画布＝背景图的**自然像素**（official-map.jpg 1889×879），不再硬编码成 760px 的小盒子；
       舞台铺满正文栏，桌面端一屏能看整张图，手机端有"可用下限"（不再是把桌面缩小的迷你版）；
     · 点位是**小圆点**：白＝空点 / 蓝＝我方 / 红＝敌方（CSS 变量 --sb-c-*），
       右上角徽标＝点内单位数（敌方点位就是敌军数），命中区 96×96 画布像素（手机 ≥44px 屏幕）；
     · 点位详情**点开才看**（.sb-detail）：名称 / 归属 / 单位清单 / 产出 / 经验 / 连线，
       选中我方单位时它同时是"移动 / 出手 / 放技能"的入口；点空白或「关闭」收起；
     · 连线 SVG 与特效层都 pointer-events:none（纯装饰，绝不吃掉点击）；
     · 缩放到 125% / 70% 之后命中区跟着一起缩放（点哪儿就是哪儿）。
   ========================================================================== */

/* ---------------------------------------------------------------- 外壳 */
/* ★ 2026-10-07 整屏游戏页（用户要求：沙盘大厅不要资料主页那套导航）
   这一页现在与卡兹 /kards、拼点 /clash 同一套外壳：body.st-body（100vh，不滚动）+
   一行 .st-bar + 铺满的 .st-main（见 subsite.css）。沙盘自己的约束：
     · #view 是 .st-main 里唯一的弹性子元素 —— 牌桌视图要**正好一屏**（整页无纵向滚动条），
       大厅 / 地图工坊比一屏长，就让 .st-main 自己滚（subsite.css 已经是 overflow:auto）；
     · 手机软件内置版（appmode.js）里视口被钉死，所以一屏布局在手机上也成立。
   注意：.st-main 在 ≤767px 时 subsite.css 会调小内边距，两种尺寸都不受影响。 */
.st-main > #view { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
.sb-wrap { min-width: 0; max-width: 100%; }
.sb-wrap h1.g-title { margin: .2rem 0 .1rem; }
.sb-wrap .g-title .g-tag { font-size: .7rem; vertical-align: middle; margin-left: .4rem; }
.sb-grow { flex: 1 1 auto; }
.sb-row { display: flex; gap: .45rem; flex-wrap: wrap; align-items: center; }
.sb-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: .7rem; }
.sb-sec h2 { margin-bottom: .5rem; }
.sb-field { display: flex; flex-direction: column; gap: .2rem; margin: 0 0 .5rem; min-width: 0; }
.sb-field > label { font-size: .76rem; font-weight: 700; color: var(--text-2); }
.sb-login-note { margin: 0 0 .7rem; }
.sb-hide { display: none; }

/* 手机上"所有可点元素 ≥44px"：g-btn / g-input / w-btn 在别处是 40px，
   在沙盘页里统一抬一档（活动页那种密集文字按钮不在这一页，所以可以整体抬）。 */
.sb-wrap .g-btn, .sb-wrap .w-btn, .sb-wrap .g-input,
.sb-wrap select.g-input, .sb-wrap input.g-input, .sb-wrap textarea.g-input { min-height: 44px; }
.sb-wrap a.g-btn, .sb-wrap a.w-btn, .sb-wrap button.g-btn, .sb-wrap button.w-btn { display: inline-flex; align-items: center; justify-content: center; gap: .3rem; text-align: center; }
.sb-wrap .g-btn.big { min-width: 44px; }
.sb-wrap .g-btn.ghost { padding-left: .6rem; padding-right: .6rem; }
/* 房间头部那一排按钮在窄屏会挤：允许换行。
   ★ 2026-10-09 手机端「结束回合这些按钮被压得极小、几乎点不到」的真正原因就在下面这一行：
   `.sb-head` 是 `.sb-room`（flex 列，高度被"整页一屏"约束住）里的弹性子项，默认 `flex:0 1 auto`，
   而它自己带 `overflow-x:auto` → 自动最小尺寸被算成 0 → 整条头部被压到 **18px** 高，
   里面 44px 的按钮被 `overflow-y:hidden` 裁成一条缝（实测见 tools/test-sandbox-ui.js 的控件清单）。
   所以头部**绝不许参与收缩**：`flex:0 0 auto` + 给一个下限高度。其余玩法不共用这个类名。 */
.sb-head { flex-wrap: wrap; flex: 0 0 auto; min-height: 52px; }

/* ---------------------------------------------------------------- 大厅 */
.sb-start {
  border: 1px solid var(--border-faint, #eaecf0); background: #fff;
  padding: .6rem .7rem; display: flex; flex-direction: column; gap: .45rem; min-width: 0;
}
.sb-start > b { font-family: var(--serif); font-size: .98rem; }
.sb-start .g-tip { margin: 0; }
.sb-room-row { flex-wrap: wrap; }
.sb-room-row .sb-grow { flex: 1 1 1rem; }
.sb-map-row { flex-wrap: wrap; }
.sb-map-row .sb-mapid { min-width: 3.2rem; }
.sb-match { margin: .7rem 0 0; }
/* 队列栏里的人数进度（已到 X/N 人）：三人图必须一眼看到"还差几个人到齐" */
.sb-match-need { margin: 0; font-weight: 700; }
.sb-match-need b { font-family: var(--mono, monospace); }

/* ---------------------------------------------------------------- 等候室（房间还没开局）
   用户的抱怨是「开房一直卡在正在读取这一局…」—— 那一条**只留给真正在读已开局的对局**，
   等人时必须是一间看得见座位、人数、按钮的等候室。 */
.sb-wait { display: flex; flex-direction: column; gap: .55rem; }
.sb-wait-top {
  display: flex; gap: .5rem; flex-wrap: wrap; align-items: center;
  border: 1px solid var(--border-faint, #eaecf0); background: var(--bg-subtle, #f8f9fa);
  padding: .5rem .6rem; font-size: .86rem;
}
.sb-wait-top .sb-count { font-weight: 700; color: var(--text-strong, #000); }
.sb-wait-top .sb-count.ready { color: #0f6b57; }
.sb-seats { display: flex; flex-direction: column; gap: .35rem; }
.sb-seat {
  display: flex; align-items: center; gap: .5rem; min-height: 44px;
  border: 1px solid var(--border-faint, #eaecf0); background: #fff; padding: .45rem .6rem; font-size: .88rem;
}
.sb-seat .sb-seatno { font-family: var(--mono, monospace); color: var(--text-3); min-width: 4.4rem; }
.sb-seat .sb-seatname { font-weight: 700; }
.sb-seat.empty { border-style: dashed; background: var(--bg-subtle, #f8f9fa); }
.sb-seat.empty .sb-seatname { color: var(--text-3); font-weight: 400; }
.sb-seat .g-tag { margin-left: auto; }
.sb-wait-acts { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; }
.sb-wait-note { font-size: .78rem; color: var(--text-2); line-height: 1.7; }
.sb-spin {
  width: 16px; height: 16px; flex: 0 0 auto; border-radius: 50%;
  border: 2px solid var(--border-soft, #c8ccd1); border-top-color: #3366cc;
  animation: sb-spin 1s linear infinite;
}
@keyframes sb-spin { to { transform: rotate(360deg); } }
.sb-loading { display: flex; align-items: center; gap: .55rem; }
.sb-load-err { border: 2px solid #b32424; background: #fff6f6; padding: .55rem .65rem; font-size: .86rem; line-height: 1.7; }

/* ---------------------------------------------------------------- 房间头部与统计 */
/* 牌桌＝**正好一屏**：头部与战况条各占一条，剩下的全给地图区（.sb-body）。
   为什么要锁高度：用户要"电脑端地图大些" + "整页无纵向滚动条"（100vh 内放下），
   所以房间容器用 height:100% 跟着 .st-main 走，内部面板自己滚。 */
.sb-room {
  flex: 1 1 auto; min-height: 0; height: 100%;
  display: flex; flex-direction: column; gap: .4rem; overflow: hidden;
}
.sb-stats {
  display: flex; gap: .4rem; flex-wrap: wrap; align-items: center;
  border: 1px solid var(--border-faint, #eaecf0); background: var(--bg-subtle, #f8f9fa);
  padding: .35rem .5rem; font-size: .82rem;
}
.sb-stat { display: inline-flex; align-items: center; gap: .3rem; padding: .15rem .45rem; border: 1px solid var(--border-faint, #eaecf0); background: #fff; }
.sb-stat.on { border-color: #14866d; box-shadow: inset 0 -2px 0 0 #14866d; font-weight: 700; }
/* 三人图：**每个座位一行**（自己那一行加粗一点，别人的也在，别让用户猜"第三个人多少积分"）。
   窄屏（≤767px / ≤430px）上这些行会自动换行、绝不横向溢出。 */
.sb-stats { flex-wrap: wrap; }
.sb-stat.sb-stat-seat { flex-wrap: wrap; max-width: 100%; min-width: 0; }
.sb-stat.sb-stat-seat.sb-me { border-color: #2f6fed; }
.sb-stat.sb-stat-players { background: #fff8ec; border-color: #d9a441; }
.sb-stat .g-tag, .dhead .g-tag { margin-left: .3rem; }
.sb-stat b { color: var(--text-strong, #000); }
.sb-dot { width: .6rem; height: .6rem; border-radius: 50%; display: inline-block; background: #c8ccd1; }
.sb-hint { margin: 0; min-height: 2.5rem; }
/* 空提示条：留出同样的高度但看不见（放牌之后这里冒出一行字，地图区不许因此变矮） */
.sb-hint:empty { border-color: transparent; background: none; }
.sb-selinfo {
  border-left: 4px solid #3366cc; background: #eaf3ff; padding: .45rem .6rem;
  font-size: .82rem; line-height: 1.7; overflow-wrap: anywhere;
}
.sb-spec-banner { margin: .2rem 0; }

/* ---------------------------------------------------------------- 地图区（地图是主角）
   ★ 2026-10-07 用户要求「电脑端地图大些」。布局：
     .sb-body（地图区）＝ 房间容器里**剩下的全部高度**（≈ 100vh − 顶条 − 房间头部 − 战况条）；
     .sb-stage 绝对定位铺满整个地图区（宽 100% × 高 100%）—— 不再是被上下两块面板夹住的小盒子；
     手牌 / 技能做成 .sb-hud **底部停靠条**（浮在舞台下沿，可一键收起）；
     战况 / 规则 / 日志进 .sb-drawer 右侧抽屉（默认收起，展开时占一列）。
   相机（sandbox-page.js 的 stageBox()）只认"没被停靠条遮住的那块"，
   所以默认视角下地图**不会**躲到停靠条底下（见 hudInsetH）。 */
.sb-body {
  flex: 1 1 auto; min-height: 0; position: relative; overflow: hidden;
  display: flex; gap: .45rem; align-items: stretch;
}
.sb-main { position: relative; min-width: 0; min-height: 0; flex: 1 1 auto; }
.sb-stage {
  position: absolute; left: 0; top: 0; right: 0; bottom: 0;
  overflow: hidden; touch-action: none;
  border: 1px solid var(--border, #a2a9b1); background: #1b2027;
  background-image: linear-gradient(45deg, rgba(255,255,255,.045) 25%, transparent 25%, transparent 75%, rgba(255,255,255,.045) 75%),
    linear-gradient(45deg, rgba(255,255,255,.045) 25%, transparent 25%, transparent 75%, rgba(255,255,255,.045) 75%);
  background-size: 22px 22px, 22px 22px; background-position: 0 0, 11px 11px;
  cursor: grab;
}
.sb-stage:active { cursor: grabbing; }
/* 适应屏幕（锁定态）：**不显示"可拖动"光标** —— 用户报的"为什么地图还能拖动"
   有一半是这只手在暗示"拖我"。锁定时拖拽在 JS 里也是直接 return（位移 0px）。 */
.sb-stage.sb-locked, .sb-stage.sb-locked:active { cursor: default; }
.sb-stage { overscroll-behavior: contain; }
/* ★ 2026-10-05 用户报「地图编辑器鼠标光标到图上就下滑不动了」：
   `.sb-stage{overscroll-behavior:contain}` 是为了"手机上别把整页带着一起滚"，
   但编辑器舞台（.sb-edstage）也带着这个类 —— 光标停在图上时滚轮被这个元素吃掉、
   不再链式传给 .st-main，于是整页滚不动（编辑器本来就不需要在图上平移/缩放）。
   编辑器恢复默认：滚轮照常滚页面，触摸也从图上开始就能滚（放置节点是"点"，不是"拖"）。 */
.sb-edstage { overscroll-behavior: auto; touch-action: auto; }
.sb-canvas {
  position: relative; width: 1889px;
  transform-origin: 0 0; will-change: transform;
}
.sb-canvas.sb-noimg { width: 1889px; }
.sb-mapimg { display: block; width: 100%; height: auto; user-select: none; -webkit-user-drag: none; }
.sb-imgph {
  height: 879px; display: flex; align-items: center; justify-content: center;
  color: #d7dbe0; font-size: 24px; background: #2a2f38; text-align: center; padding: 2rem;
}
.sb-overlay { position: absolute; left: 0; top: 0; right: 0; bottom: 0; }
/* 连线层只是装饰：**必须** pointer-events:none，不然它会盖住点位、点哪儿都没反应 */
.sb-links { position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none; }
/* 连线：平时是灰白虚线（画布上看得清），能走/能打时变成橙色实线（见 sb-link-hit） */
.sb-link { stroke: #9aa5b1; stroke-width: 3; stroke-dasharray: 10 8; opacity: .85; }
.sb-link-hit { stroke: #ff9d2e; stroke-width: 6; stroke-dasharray: none; opacity: 1; }
.sb-nodes { position: absolute; left: 0; top: 0; right: 0; bottom: 0; }

/* 底部停靠条（手牌 / 技能 / 图例 / 选择提示）：浮在舞台下沿，
   深色半透明 —— 它**不占**地图区的高度（相机会把地图让到它上面去）。
   收起（.off）之后只剩标题行，地图可见区更大。
   ★ 2026-10-08：桌面上它离底边留出 3.35rem —— 那一溜是缩放控件
   （−/＋/适应屏幕/回到中央）的地盘。旧版停靠条一直铺到底，把缩放条整个盖住
   （elementFromPoint 命中的是手牌区的文字），于是**桌面上那几个按钮一个都点不动**、
   用户只能靠 ctrl+滚轮缩放。 */
.sb-hud {
  position: absolute; left: 0; right: 0; bottom: 3.35rem; z-index: 7;
  background: linear-gradient(180deg, rgba(20, 23, 28, .82), rgba(16, 18, 22, .96));
  border-top: 1px solid rgba(255, 255, 255, .18);
  padding: .3rem .45rem .4rem; display: flex; flex-direction: column; gap: .25rem;
  /* ★ 2026-10-08：42% → 36%。相机给停靠条"让位"的算法有个 45% 的上限
     （停靠条不能把可见区压成一条缝），42% + 底部那 3.35rem 缩放条会超过上限，
     于是地图下沿有一截被停靠条压住。36% 之后 53.6px + 停靠条 ≤ 45% 舞台，
     相机能把整个让位区都算进去 —— 默认视角下地图整张可见，缩放条也不再被盖。 */
  max-height: 36%; overflow: auto; touch-action: pan-y;
}
.sb-hud.off { max-height: 2.9rem; overflow: hidden; }
/* ★ 2026-10-12：这里原来只写了 `.sb-cols`（带 s）—— 那是**大众厅**那套网格的类名，
   手牌这一块真正的类名是 `.sb-col sb-hudcol`，所以这条规则从来没生效过：
   手机端点「收起手牌」只藏掉了选择提示，手牌与技能照旧铺在下面（"收了跟没收一样"）。
   补上 `.sb-col`（顺带保留 .sb-cols，万一别处有）之后，收起才真的只剩标题行 + 摘要。 */
.sb-hud.off .sb-cols, .sb-hud.off .sb-col, .sb-hud.off .sb-selinfo { display: none; }
.sb-hud .sb-legend { color: #cfd4da; }
.sb-hud .sb-legend .sb-swatch { box-shadow: 0 0 0 1px rgba(0, 0, 0, .5); }
.sb-hudhead { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; }
.sb-hudhead .sb-legend { flex: 1 1 auto; }
.sb-hud-toggle {
  flex: 0 0 auto; background: #fff !important; color: #202122 !important;
  min-height: 44px; padding-left: .7rem; padding-right: .7rem;
}
.sb-hud .sb-selinfo { border-left-color: #7fb069; background: rgba(255, 255, 255, .94); }
.sb-hud .sb-handhead, .sb-hud .sb-skillhead { color: #f3f4f6; }
.sb-hud .sb-handhead .g-tag, .sb-hud .sb-skillhead .tiny { color: #cfd4da; }
.sb-hud .sb-hand { min-height: 0; padding-bottom: .2rem; }
/* ★ 停靠条里的卡面**比大厅矮一档**：停靠条越高，地图可见区越小 ——
   而"适应屏幕"是按可见区算的（锁定态必须整图可见），所以停靠条的高度**直接**决定地图多大。
   108px 的卡面仍然写着费用 / 名称 / 攻防，认牌够用（要看全文点开卡面详情）。 */
.sb-hud .sb-card { min-height: 108px; box-shadow: 0 2px 8px rgba(0, 0, 0, .5); }
.sb-hud .sb-hand { max-height: 120px; }
.sb-hud .sb-cname { margin-top: .2rem; }
.sb-hud .sb-ctext { font-size: .62rem; }
.sb-hud .sb-skills .g-btn { background: #fff; }
/* 停靠条收起的按钮行（展开/收起都点得到） */
.sb-hud.off .sb-hudhead { padding-bottom: .1rem; }
/* ★ 2026-10-12 收起之后那一行摘要（手牌几张 / 技能几个 / 两笔钱多少，见 sandbox-page.js 的 hudMini）：
   只在收起时露出来 —— 收起不等于"什么都看不见"；电脑端缩成窄轨时它也不摆（那一列只有一颗按钮）。 */
.sb-hudmini { display: none; font-size: .78rem; color: var(--text-2); line-height: 1.6; overflow-wrap: anywhere; }
.sb-hud.off .sb-hudmini { display: block; }
/* 三人局「侦察」选一家：技能下面一排 44px 的座位按钮 */
.sb-seatpick { display: flex; gap: .35rem; flex-wrap: wrap; align-items: center; margin-top: .25rem; }
.sb-seatpick .sb-seatpick-b { min-height: 44px; }
.sb-seatpick .g-tip { flex: 1 1 12rem; }
.sb-stat.sb-stat-seat.sb-out { opacity: .6; border-style: dashed; }

/* 右侧抽屉（战况 / 规则 / 动作日志）：默认收起（地图是主角），展开时占 22rem 一列。
   .off 时宽 0 —— 舞台随之变宽，相机重新夹紧（setDock → clampCam）。 */
.sb-drawer {
  width: min(22rem, 30vw); min-width: 0; overflow: auto; touch-action: pan-y;
  border: 1px solid var(--border-faint, #eaecf0); background: #fff; padding: .5rem .55rem;
  display: flex; flex-direction: column; gap: .4rem; font-size: .84rem;
}
.sb-drawer.off { display: none; }
.sb-drawer .sb-side-head { display: flex; align-items: center; gap: .45rem; }
.sb-drawer .sb-drawer-x { margin-left: auto; }
.sb-drawer .sb-log { max-height: none; flex: 1 1 auto; }
.sb-drawer .sb-rules dl { grid-template-columns: 4.6rem 1fr; }

/* 飘字 / 特效层：铺在舞台（不是画布）上，所以数字始终是屏幕大小，不会跟着缩放变小。
   同样 pointer-events:none —— 它是纯装饰，绝不能吃掉点击。 */
.sb-fx { position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: hidden; pointer-events: none; z-index: 6; }
.sb-dmg {
  position: absolute; transform: translate(-50%, -50%);
  font: 700 1.05rem/1 var(--sans); color: #fff; background: rgba(179, 36, 36, .94);
  border: 1px solid rgba(255, 255, 255, .5); border-radius: 999px; padding: .12rem .5rem;
  box-shadow: 0 2px 8px rgba(0, 0, 0, .45); white-space: nowrap;
  animation: sb-dmg-fly 1.15s cubic-bezier(.2, .7, .3, 1) forwards;
}
.sb-dmg.dead { background: rgba(20, 20, 20, .94); }
.sb-dmg.cap { background: rgba(15, 107, 87, .95); }
@keyframes sb-dmg-fly {
  0% { opacity: 0; transform: translate(-50%, -20%) scale(.6); }
  18% { opacity: 1; transform: translate(-50%, -70%) scale(1.14); }
  100% { opacity: 0; transform: translate(-50%, -200%) scale(1); }
}

/* ---------------------------------------------------------------- 点位标记
   用户 2026-10-06 的原话：「点的显示不要这么大，默认白色是空点，红色是被敌人占领的，
   点的右上角显示点内敌军数量，点开点才能查看详情」。
   所以：
     · 视觉是**小圆点**（44px 画布 ≈ 24px 屏幕），不给点位挂常驻大标签；
     · 命中区仍是 96×96 画布像素的透明方块（手机最小缩放 0.72 → 69px ≥44px 手指目标）；
     · 颜色：白＝空点（中立）/ 蓝＝我方 / 红＝敌方（变量见 .sb-wrap 的 --sb-c-*）；
     · 右上角徽标＝点内单位数（敌方点位就是"点内敌军数量"）。 */
.sb-wrap { --sb-c-neutral: #ffffff; --sb-c-mine: #2f6fed; --sb-c-foe: #cf2b2b; --sb-c-foe2: #7b3fd4; --sb-c-ink: #22262b; }
/* 三人图（2026-10-07）：**每个座位一个颜色**，红＝敌方 A、紫＝敌方 B，一眼分得开。
   两人图完全不变（座位 1 还是红）。查看视角决定"哪个座位是蓝"（服务端 view 只给一份，
   观战看 0 号位）—— 颜色类由 sandbox-page.js 的 seatIdent() 按座位号贴到节点/小方点/图例上。 */
.sb-wrap { --sb-c-seat0: var(--sb-c-mine); --sb-c-seat1: var(--sb-c-foe); --sb-c-seat2: var(--sb-c-foe2); }
.sb-node.sb-side2 { --sb-c: var(--sb-c-seat2); }
.sb-dot.seat0 { background: var(--sb-c-seat0); }
.sb-dot.seat1 { background: var(--sb-c-seat1); }
.sb-dot.seat2 { background: var(--sb-c-seat2); }
.sb-dots i.seat0 { background: var(--sb-c-seat0); }
.sb-dots i.seat1 { background: var(--sb-c-seat1); }
.sb-dots i.seat2 { background: var(--sb-c-seat2); }
.sb-swatch.seat0 { background: var(--sb-c-seat0); border-color: var(--sb-c-seat0); }
.sb-swatch.seat1 { background: var(--sb-c-seat1); border-color: var(--sb-c-seat1); }
.sb-swatch.seat2 { background: var(--sb-c-seat2); border-color: var(--sb-c-seat2); }
.sb-node {
  position: absolute; transform: translate(-50%, -50%);
  /* ★ 2026-10-08 手机端"点不准"的根因：命中区以前就是这个**画布尺寸**的盒子
     （96/108px），跟着地图一起缩放 —— 缩到 0.36× 时屏幕上只剩 38px，手指点不准。
     现在盒子不小于 --sb-hit（＝ HIT_PX / k，画布像素，由 applyCam 写）：乘上缩放之后
     屏幕边长恒 ≥44px；而 --sb-hit 又被"相邻点位不重叠"的上限（hitMaxCanvas）顶着，
     所以放大的时候它自然退回 96/108px 的常规大小，绝不会盖住旁边的点。
     圆点/徽标的定位都基于这个盒子，所以命中区一变它们也跟着居中，不会错位。 */
  width: max(96px, var(--sb-hit, 96px)); height: max(96px, var(--sb-hit, 96px));
  padding: 0; border: 0; background: none;
  display: block; cursor: pointer; font-family: var(--sans); text-align: left;
}
.sb-pin {
  position: absolute; left: 50%; top: 50%; width: 46px; height: 46px; transform: translate(-50%, -50%);
  border-radius: 50%; border: 5px solid var(--sb-c); background: var(--sb-c);
  box-shadow: 0 2px 7px rgba(0, 0, 0, .55), inset 0 0 0 3px rgba(255, 255, 255, .55);
  transition: transform .12s ease, box-shadow .12s ease;
}
.sb-node.sb-side0 { --sb-c: var(--sb-c-mine); }
.sb-node.sb-side1 { --sb-c: var(--sb-c-foe); }
.sb-node.sb-neutral { --sb-c: var(--sb-c-neutral); }
/* 总部：中间一个皇冠（点位本身还是那个小圆点） */
.sb-crown {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  font-size: 26px; font-style: normal; color: #7a4b00; text-shadow: 0 1px 0 #fff; pointer-events: none;
}
.sb-node.sb-side1 .sb-crown { color: #fff; text-shadow: 0 1px 3px rgba(0, 0, 0, .6); }
/* 右上角数量徽标：点内有几个单位（敌方点位＝敌军数量，颜色跟着归属走） */
.sb-badge {
  position: absolute; right: 6px; top: 6px; min-width: 30px; height: 30px; padding: 0 5px;
  display: flex; align-items: center; justify-content: center;
  font-family: var(--mono, monospace); font-size: 20px; font-weight: 700; line-height: 1;
  color: #fff; background: var(--sb-c); border: 2px solid #fff; border-radius: 15px;
  box-shadow: 0 1px 4px rgba(0, 0, 0, .5);
}
.sb-node.sb-neutral .sb-badge { color: var(--sb-c-ink); border-color: #8a9099; background: #fff; }
.sb-node.sb-empty .sb-badge { display: none; }
.sb-node.sb-full .sb-badge { border-color: #b32424; box-shadow: 0 0 0 3px rgba(179, 36, 36, .35); }
/* 点内单位的小方点（纯视觉，不可点）：蓝＝我方，红＝敌方 */
.sb-dots {
  position: absolute; left: 50%; bottom: 8px; transform: translateX(-50%);
  display: flex; gap: 3px; pointer-events: none;
}
.sb-dots i { width: 9px; height: 9px; border-radius: 2px; border: 1px solid rgba(255, 255, 255, .8); display: block; }
.sb-dots i.mine { background: var(--sb-c-mine); }
.sb-dots i.foe { background: var(--sb-c-foe); }
/* 四种状态：可走＝绿环 / 可打＝红环 / 选中＝橙环 / 满员＝徽标加圈 */
.sb-node.sb-can .sb-pin { box-shadow: 0 0 0 5px rgba(20, 134, 109, .85), 0 2px 7px rgba(0, 0, 0, .55); }
.sb-node.sb-can.sb-can-attack .sb-pin { box-shadow: 0 0 0 5px rgba(179, 36, 36, .85), 0 2px 7px rgba(0, 0, 0, .55); }
.sb-node.sb-can.sb-can-move .sb-pin { box-shadow: 0 0 0 5px rgba(20, 134, 109, .85), 0 2px 7px rgba(0, 0, 0, .55); }
.sb-node.sb-off .sb-pin { opacity: .55; }
.sb-node.sel .sb-pin { box-shadow: 0 0 0 7px rgba(255, 157, 46, .95), 0 2px 7px rgba(0, 0, 0, .55); transform: translate(-50%, -50%) scale(1.18); }
.sb-node.sb-nearest .sb-pin { box-shadow: 0 0 0 7px rgba(255, 157, 46, .55), 0 2px 7px rgba(0, 0, 0, .55); }
.sb-node.sb-enemy-here .sb-pin { border-style: double; }
.sb-node.sb-capture .sb-pin { animation: sb-capture .95s ease-out; }
@keyframes sb-capture {
  0% { box-shadow: 0 0 0 0 rgba(20, 134, 109, .9), 0 2px 7px rgba(0, 0, 0, .55); }
  100% { box-shadow: 0 0 0 46px rgba(20, 134, 109, 0), 0 2px 7px rgba(0, 0, 0, .55); }
}
.sb-node.sb-hurt .sb-pin { animation: sb-shake .42s ease-in-out; }
@keyframes sb-shake {
  0%, 100% { transform: translate(-50%, -50%); }
  25% { transform: translate(calc(-50% - 5px), -50%); }
  75% { transform: translate(calc(-50% + 5px), -50%); }
}

/* ---------------------------------------------------------------- 点位详情面板
   用户要求：点位默认**不再**常驻大标签，点开某个点才看详情（名称 / 归属 / 单位清单 /
   产出 / 经验 / 连线），选中我方单位时这一块同时是"出手 / 移动 / 放技能"的入口。
   桌面右上角、手机底部抽屉；面板自己可滚动（touch-action: pan-y）。 */
.sb-detail {
  position: absolute; right: .5rem; top: .5rem; z-index: 8;
  width: min(23rem, calc(100% - 1rem)); max-height: calc(100% - 1rem); overflow: auto;
  background: rgba(255, 255, 255, .97); border: 2px solid var(--border, #a2a9b1); border-radius: 6px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, .4); padding: .55rem .6rem; font-size: .84rem;
  touch-action: pan-y; cursor: auto;
}
.sb-detail-head { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; }
.sb-detail-head b { font-family: var(--serif); font-size: 1rem; }
.sb-detail-head .g-btn { margin-left: auto; min-height: 44px; }
.sb-detail-body { margin-top: .4rem; display: flex; flex-direction: column; gap: .35rem; }
.sb-detail-row { display: flex; gap: .4rem; flex-wrap: wrap; font-size: .78rem; color: var(--text-2); line-height: 1.7; }
.sb-swatch { display: inline-block; width: .8rem; height: .8rem; border-radius: 50%; border: 2px solid #8a9099; vertical-align: -.1rem; }
.sb-swatch.mine { background: #2f6fed; border-color: #2f6fed; }
.sb-swatch.foe { background: #cf2b2b; border-color: #cf2b2b; }
.sb-swatch.neutral { background: #fff; }
/* 单位行（面板里，是**可以点的**：选中它 = 出手/移动的第一步） */
.sb-urow {
  display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; min-height: 44px;
  border: 1px solid var(--border-soft, #c8ccd1); background: #fff; border-radius: 4px;
  padding: .3rem .4rem; cursor: pointer; font-family: inherit; text-align: left;
}
.sb-urow.mine { background: #eef4ff; border-color: #2f6fed; }
.sb-urow.foe { background: #fdf0f0; border-color: #cf2b2b; }
.sb-urow.sel { outline: 3px solid #ff9d2e; }
.sb-urow .sb-uname { font-size: .84rem; font-weight: 700; }
.sb-urow .sb-ustats { font-size: .78rem; margin-left: auto; }
.sb-urow .sb-uap { font-size: .72rem; color: var(--text-3); flex: 1 1 100%; }
.sb-target {
  display: block; width: 100%; min-height: 44px; margin-top: .2rem; text-align: left;
  border: 1px solid var(--border-soft, #c8ccd1); background: #f7fbf9; border-radius: 4px;
  padding: .3rem .45rem; font-family: inherit; font-size: .78rem; color: var(--text, #202122); cursor: pointer;
}
.sb-target:hover:not(:disabled) { background: #eaf7f1; border-color: #14866d; }
.sb-target:disabled { opacity: .55; cursor: not-allowed; background: #fff; }
.sb-detail .sb-close { min-height: 44px; }

/* 图例：白＝空点 / 蓝＝我方 / 红＝敌方（用户要求牌桌里有一行说明） */
.sb-legend {
  display: flex; gap: .7rem; flex-wrap: wrap; align-items: center;
  font-size: .76rem; color: var(--text-2); padding: .25rem .1rem 0;
}
.sb-legend .sb-swatch { width: .7rem; height: .7rem; }
.sb-legend .sb-legend-3p { color: var(--text-3); }
/* 「2 人图 / 3 人图」标签：地图池 / 等候室 / 队列栏 / 匹配按钮都用它 */
.sb-playertag { white-space: nowrap; }
.g-btn .sb-playertag { margin-left: .25rem; }

/* ---------------------------------------------------------------- 人物列表（2026-10-11 用户要求）
   「加入人物列表」：牌桌左上角一块**只读**的紧凑名单（sandbox-page.js 的 rosterHtml）——
   每个座位一行：座位号 / 昵称 / 你·对手 / 是否出局 / 总部名 / 行动积分 / 图上部队数，轮到谁高亮。
   为什么是浮层 + pointer-events:none：
     · 它**不能**占地图区的高度（整页一屏、地图舞台 ≥70% 视口是硬约束），所以只能浮在舞台里；
     · 右上角是点位详情（.sb-detail）、右下角是缩放控件（.sb-zoom）、下沿是手牌停靠条（.sb-hud）——
       只有左上角是空的（电脑上官方两张图的左上角没有点位；手机上地图被放大、个别点位可能从
       面板下面经过 —— 面板不吃点击，点得到，嫌挡就点「收起」）；
     · 整块 pointer-events:none ⇒ **一个点击都不吃**：点位、缩放条、手牌、外壳上那条常驻队列条
       （.sb-pinbar）与一键到顶/到底的浮钮（.sb-scroll）elementFromPoint 命中的都还是它们自己；
       只有「收起 / 展开」那颗按钮 pointer-events:auto（它 ≥44px，见下面）。
   观战看同一份名单（观战是一种视角，不是一种权限）。 */
.sb-roster {
  position: absolute; left: .4rem; top: .4rem; z-index: 9;
  width: min(15.5rem, calc(100% - .8rem)); max-width: calc(100% - .8rem);
  background: rgba(18, 21, 26, .86); color: #f3f4f6;
  border: 1px solid rgba(255, 255, 255, .22); border-radius: 5px;
  padding: .25rem .35rem .35rem; font-size: .72rem; line-height: 1.45;
  pointer-events: none;                 /* 见上面：绝不吃点击（点位 / 缩放条 / 外壳浮钮） */
}
.sb-roster.off .sb-roster-list { display: none; }
.sb-roster-head { display: flex; align-items: center; gap: .35rem; flex-wrap: wrap; }
.sb-roster-head b { font-family: var(--serif); font-size: .78rem; }
.sb-roster-head .tiny { color: #c9cfd6; flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }
/* 收起/展开：整块里**唯一**可点的东西（≥44px 点按区，且只有它 pointer-events:auto）。 */
.sb-roster-toggle {
  pointer-events: auto; margin-left: auto;
  min-height: 44px; min-width: 44px; padding: 0 .5rem !important;
  background: #fff !important; color: #202122 !important; font-size: .74rem;
}
.sb-roster-list { list-style: none; margin: .2rem 0 0; padding: 0; display: flex; flex-direction: column; gap: .15rem; }
.sb-rrow {
  display: flex; align-items: center; gap: .3rem; flex-wrap: wrap;
  border-left: 3px solid transparent; padding: .12rem .2rem; border-radius: 2px;
}
.sb-rrow .sb-dot { width: .5rem; height: .5rem; flex: 0 0 auto; }
.sb-rrow .sb-rno { font-family: var(--mono, monospace); color: #c9cfd6; }
.sb-rrow .sb-rname { font-weight: 700; overflow-wrap: anywhere; }
.sb-rrow .sb-rline { flex: 1 1 100%; color: #c9cfd6; font-size: .68rem; overflow-wrap: anywhere; }
.sb-rrow .sb-rline b { color: #fff; }
.sb-rrow .g-tag { font-size: .62rem; padding: 0 .25rem; }
.sb-rrow.sb-rme { border-left-color: #2f6fed; background: rgba(47, 111, 237, .18); }
/* 出局（总部被占 / 认输退出）：整行压暗 + 划掉名字，一眼就知道这一家不在了 */
.sb-rrow.sb-rout { opacity: .62; }
.sb-rrow.sb-rout .sb-rname, .sb-rrow.sb-rout .sb-rline { text-decoration: line-through; }
.sb-rrow.sb-rout .sb-routtag { text-decoration: none; }
/* 轮到谁：整行描边高亮（"当前回合的那一家"） */
.sb-rrow.sb-rturn { border-left-color: #14866d; box-shadow: inset 0 0 0 1px #14866d; }
.sb-rrow.sb-rturn .sb-rline b { color: #9fe0c8; }
/* 手机（≤767px）：位置与桌面一样（左上角），但再收一档 —— 舞台只有 320–470px 高，
   这块不能把地图压住：字号 .64rem、行距更紧、宽度最多 66%（一行一条信息，别绕成两行）。
   底色比桌面透一点：万一点位在它下面，透过去还看得见（命中测试完全不受影响：整块 pointer-events:none）。
   面板自身**绝不横向溢出**（.sb-stage 本来就 overflow:hidden，行内文字一律 overflow-wrap）。 */
@media (max-width: 767px) {
  .sb-roster { width: min(15rem, 66%); font-size: .64rem; background: rgba(18, 21, 26, .76); padding: .2rem .3rem .3rem; }
  .sb-roster-head b { font-size: .7rem; }
  .sb-rrow .sb-rline { font-size: .6rem; }
  .sb-rrow .g-tag { font-size: .58rem; }
  .sb-roster-toggle { min-height: 44px; min-width: 44px; padding: 0 .4rem !important; font-size: .7rem; }
}

/* 缩放控件：软件里浏览器缩放被关掉了（user-scalable=no），所以必须给按钮 */
.sb-zoom {
  position: absolute; right: .4rem; bottom: .4rem; display: flex; gap: .3rem; align-items: center;
  background: rgba(25,25,25,.82); padding: .25rem .3rem; border-radius: 3px; flex-wrap: wrap;
  max-width: calc(100% - .8rem);
}
.sb-zoom .g-btn { background: #fff; min-width: 44px; }
.sb-zoom-val { color: #fff; font-family: var(--mono, monospace); font-size: .74rem; min-width: 2.6rem; text-align: center; }
.sb-mapnote { font-size: .72rem; color: var(--text-3); line-height: 1.7; }

/* ---------------------------------------------------------------- 手牌 / 技能 / 日志 */
.sb-cols { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 22rem); gap: .7rem; align-items: start; }
.sb-col { min-width: 0; display: flex; flex-direction: column; gap: .35rem; }
.sb-handhead, .sb-skillhead, .sb-side-head { display: flex; align-items: center; gap: .45rem; flex-wrap: wrap; font-size: .86rem; }
.sb-handhead .g-tag { margin-left: auto; }
.sb-skillhead .tiny { margin-left: auto; }
.sb-side-head .g-btn { margin-left: auto; }
.sb-hand {
  display: flex; gap: .45rem; overflow-x: auto; overflow-y: hidden;
  padding: .2rem .1rem .5rem; min-height: 168px; -webkit-overflow-scrolling: touch;
}
/* 卡面：形状照着 KARDS 来（左上费用圆 / 名称 / 简介 / 左下攻击右下防御），
   但不引 kards.css —— 那一份是卡兹页面专用的四十多 KB，沙盘只要这个形状。 */
.sb-card {
  flex: 0 0 auto; width: 116px; min-height: 150px; position: relative;
  display: flex; flex-direction: column; gap: .2rem; padding: .3rem .3rem .25rem;
  border: 1px solid var(--border, #a2a9b1); border-top: 4px solid var(--c-red, #b32424);
  background: #fff; border-radius: 3px; cursor: pointer; font-family: inherit; text-align: left;
  box-shadow: 0 1px 3px rgba(0,0,0,.16);
}
.sb-card.ranged { border-top-color: var(--c-purple, #6b4ba1); }
.sb-card.sel { outline: 3px solid #ff9d2e; transform: translateY(-3px); }
.sb-card.sb-cant { opacity: .5; filter: saturate(.5); }
.sb-cost {
  position: absolute; left: -4px; top: -4px; width: 24px; height: 24px; border-radius: 50%;
  background: #3366cc; color: #fff; font-weight: 700; font-size: .8rem;
  display: flex; align-items: center; justify-content: center; box-shadow: 0 1px 2px rgba(0,0,0,.3);
}
.sb-cost.no { background: #8a8f96; }
.sb-cardrg { position: absolute; right: .2rem; top: .15rem; font-size: .6rem; color: var(--c-purple, #6b4ba1); font-weight: 700; }
.sb-cname { font-size: .8rem; font-weight: 700; margin-top: .35rem; overflow-wrap: anywhere; }
.sb-ctext { flex: 1 1 auto; font-size: .66rem; color: var(--text-2); line-height: 1.6; overflow: hidden; }
.sb-cstats { display: flex; justify-content: space-between; font-size: .84rem; font-weight: 700; }
.sb-skills { display: flex; gap: .4rem; flex-wrap: wrap; }
.sb-skill { flex-direction: column; align-items: flex-start !important; gap: .1rem; text-align: left; min-width: 44px; }
.sb-skill.sel { outline: 2px solid #ff9d2e; }
.sb-skill.sb-cant { opacity: .5; }
/* ★ 2026-10-11 货币口径（用户拍板「占点得的经验是用来购买技能的」）：
   .sb-skillcost 写的是**技能费（经验）** —— 用绿色系（= 占点赚来的那份钱），与卡片左上角那枚
   蓝底行动积分圆章 .sb-cost（= 打牌花的钱）在颜色上分开，一眼看得出是两种货币。 */
.sb-skillcost { color: #14866d; font-family: var(--mono, monospace); font-size: .72rem; font-weight: 700; }
.sb-skilltip { font-size: .62rem; color: var(--text-3); }
.sb-hqline { font-size: .78rem; color: var(--text-2); line-height: 1.7; }
.sb-hqline .tiny { display: block; color: var(--text-3); }
.sb-log { max-height: 16rem; overflow: auto; }
.sb-recon { margin: .2rem 0; }
.sb-reconlist { display: flex; gap: .3rem; flex-wrap: wrap; margin-top: .25rem; }
.sb-reconcard { border: 1px solid var(--border-soft, #c8ccd1); background: #fff; padding: .1rem .3rem; font-size: .72rem; }
.sb-reconcard i { font-style: normal; color: #3366cc; font-family: var(--mono, monospace); }
.sb-log > div { display: flex; gap: .3rem; flex-wrap: wrap; }
.sb-log-me { background: #f1f8f5; }
.sb-log-foe { background: #fdf7f7; }
.sb-log-r { color: var(--text-3); font-family: var(--mono, monospace); }
.sb-log-w { font-weight: 700; }
.sb-log-t { flex: 1 1 8rem; overflow-wrap: anywhere; }
/* 本机动作（自己点出去的那一手）：先在日志里出现，收到服务端战报后换成引擎那句中文 */
.sb-log-local { border-left: 3px solid #3366cc; padding-left: .3rem; }
.sb-log-local .sb-log-t { font-weight: 700; }
.sb-log-local.pending .sb-log-t { font-weight: 400; color: var(--text-2); }
.sb-log-head { display: flex; align-items: center; gap: .4rem; font-size: .82rem; }
.sb-log-head .tiny { margin-left: auto; color: var(--text-3); }

/* 胜利横幅 */
.sb-banner {
  display: flex; gap: .6rem; align-items: center; flex-wrap: wrap;
  border: 2px solid #14866d; background: #f1f8f5; padding: .5rem .6rem; font-size: .9rem;
}
.sb-banner.lose { border-color: #b32424; background: #fff5f5; }
.sb-banner b { font-family: var(--serif); font-size: 1.1rem; color: #0f6b57; }
.sb-banner.lose b { color: #8f1c1c; }
.sb-banner .g-btn { margin-left: auto; }

/* 规则面板 */
.sb-rules dl { display: grid; grid-template-columns: 5.2rem 1fr; gap: .25rem .7rem; margin: 0; }
.sb-rules dt { font-weight: 700; color: var(--text-2); }
.sb-rules dd { margin: 0; line-height: 1.85; }

/* ---------------------------------------------------------------- 全屏（沉浸模式）
   照着 kards 的做法：能 requestFullscreen 就真全屏，不能（iOS/无头）就给 <html> 挂
   sb-immersive，把顶栏 / 侧栏 / 页脚收起来，只留地图。 */
html.sb-immersive .w-topbar, html.sb-immersive .w-side, html.sb-immersive .w-footer,
html.sb-immersive .w-announce, html.sb-immersive .w-scrim { display: none !important; }
html.sb-immersive .w-layout { display: block; max-width: none; }
html.sb-immersive .w-main { max-width: none !important; padding: 0 !important; margin: 0; }
html.sb-immersive .sb-stage { height: calc(100vh - 3.4rem); height: calc(100dvh - 3.4rem); }

/* ---------------------------------------------------------------- 地图工坊 */
.sb-maps .sb-editor { border: 1px solid var(--border-faint, #eaecf0); background: #fff; padding: .6rem .7rem; }
.sb-check { display: inline-flex; align-items: center; gap: .35rem; font-size: .82rem; min-height: 44px; }
.sb-modes { display: flex; gap: .4rem; flex-wrap: wrap; align-items: center; margin: .5rem 0; }
/* ★ 2026-10-08 热修（用户报「编辑地图页地图渲染有问题」）—— **根因就在下面这一段**：
   外壳改版后 `<body class="st-body g-body sb-body">` 也带上了 `sb-body`，
   而 .sb-body{position:relative}（本来是给房间那个 #sbBody 用的）于是让 <body> 成了**定位祖先**；
   编辑器舞台继承房间那套 .sb-stage{position:absolute; inset:0}，
   就**脱离了文档流**钉在页面左上角（实测 1280×900 下 0,0 → 1280×486），
   压在地图池列表上、编辑器那一节只剩一个空洞 —— 桌面端一进来就是"地图跑到别处去了"。
   修法：编辑器舞台是**文档流里的一个盒子**（房间那套绝对定位只对牌桌成立，这里必须显式覆盖），
   高度让画布（＝背景图的真实比例）自己撑开 —— 舞台于是**正好等于背景图**，
   一格不多一格不少（"点的哪儿就是哪儿"靠的就是这个）；
   没图时由占位块 .sb-imgph 的 min-height 兜底（见下面那条）。 */
.sb-edstage {
  position: relative; inset: auto; width: 100%;
  height: auto; min-height: 0;
}

/* ------------------------------------------------ 上传背景地图（2026-10-07 重做）
   用户报的「上传不了 / 太复杂」：现在就是**一个**大控件 —— 点它、把图片拖进来、
   或者直接把图片拖到下面的地图上，三条路等价。选完立刻本地预览 + 进度 + 中文报错。 */
.sb-upload { margin: .5rem 0; }
.sb-drop {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: .25rem; text-align: center; padding: .9rem .8rem; min-height: 108px;
  border: 3px dashed #9aa5b1; border-radius: 10px; background: #f8fafc; cursor: pointer;
  transition: background .12s, border-color .12s;
}
.sb-drop:hover, .sb-drop:focus-visible { border-color: #2b6cb0; background: #eef5ff; }
/* 拖动经过：整块变蓝（连着下面的地图舞台一起高亮，用户知道"松手就传这儿"） */
.sb-drop.sb-drop-on, .sb-edstage.sb-drop-on { border-color: #2b6cb0; background: #dceaff; }
.sb-edstage.sb-drop-on { outline: 4px dashed #2b6cb0; outline-offset: -6px; }
.sb-drop-t { font-size: .95rem; font-weight: 700; color: var(--text, #202122); }
.sb-drop-h { font-size: .8rem; color: var(--text-2, #54595d); }
.sb-drop-h2 { font-size: .74rem; color: var(--text-3, #72777d); }
.sb-drop-btns { margin-top: .35rem; }
/* 进度条：只在真的在传的时候出现（「上传中… 42%」） */
.sb-prog { height: 10px; margin: .45rem 0 .15rem; border-radius: 6px; background: #e6e8eb; overflow: hidden; }
.sb-prog > i { display: block; height: 100%; width: 0; background: #2b6cb0; transition: width .15s; }
.sb-progtext { font-size: .78rem; color: var(--text-2, #54595d); min-height: 1.1rem; }
.sb-progtext:empty { min-height: 0; }
.sb-dropnote { font-size: .8rem; line-height: 1.6; margin: .35rem 0 0; padding: .35rem .5rem; border-radius: 6px; }
.sb-dropnote.ok { background: #eaf7ed; color: #16650f; }
.sb-dropnote.bad { background: #fdecea; color: #8a1c12; font-weight: 600; }
/* 本地预览的小缩略图：证明"选中的就是这张"（还没上传就看得见） */
.sb-thumb { display: block; max-width: 100%; max-height: 96px; margin: .4rem 0 0; border: 1px solid var(--border-faint, #eaecf0); border-radius: 6px; }
/* 「松手就上传这张图片」——只在拖拽经过地图时才浮出来 */
.sb-drop-hint {
  position: absolute; left: 50%; top: .6rem; transform: translateX(-50%);
  padding: .3rem .7rem; border-radius: 999px; background: rgba(43, 108, 176, .95); color: #fff;
  font-size: .82rem; opacity: 0; pointer-events: none; transition: opacity .12s;
}
.sb-drop-hint.on { opacity: 1; }
/* 编辑器用的是**自己的一套尺度**：点位是"卡片"（要显示名字 / AP / 经验），
   和地图上的小圆点是两种东西。
   ★ 2026-10-08 画布宽度从写死的 1000px 改成**跟着舞台走**（100%）：写死 1000px 时，
   手机上舞台只有 346px 宽（`overflow:hidden`），整张图被裁掉右边 2/3 ——
   看得见的只有左边 1/3，右边那些点根本点不到（实测：画布 1000×465 / 舞台 346×371）。
   现在画布＝舞台宽、高＝宽÷背景图真实比例 → 整张图永远装得下，点和图一一对应
   （坐标是百分比，见 sandbox-page.js 的 edPointAt）。 */
.sb-edstage .sb-canvas, .sb-edstage .sb-canvas.sb-noimg { width: 100%; }
/* 还没选背景图时的占位块：跟着舞台宽，但别撑成 879px 的高盒子（CSS 里 .sb-imgph 的默认高） */
.sb-edstage .sb-imgph { height: auto; min-height: clamp(240px, 40vh, 460px); }
.sb-ednode {
  width: auto; height: auto; min-width: 132px; min-height: 44px; padding: 5px 7px;
  display: flex; flex-direction: column; gap: 3px; border: 3px dashed #6b7280; border-radius: 8px;
  background: rgba(255, 255, 255, .95); box-shadow: 0 2px 6px rgba(0, 0, 0, .4);
  color: var(--text, #202122); cursor: pointer; overflow-wrap: anywhere;
}
.sb-ednode.sb-hq { border-width: 5px; }
.sb-ednode.sel { outline: 4px solid #ff9d2e; }
.sb-ednode .sb-node-head { display: flex; align-items: center; gap: 4px; font-size: 18px; line-height: 1.25; }
.sb-ednode .sb-node-name { font-weight: 700; }
.sb-ednode .sb-node-meta { display: flex; gap: 8px; flex-wrap: wrap; font-size: 15px; color: var(--text-3); }
.sb-ednode .sb-own { font-weight: 700; }
.sb-ednode .sb-crown { position: static; transform: none; font-size: 18px; color: #7a4b00; }
.sb-edlist { margin-top: .5rem; }
.sb-edrow { flex-wrap: wrap; gap: .35rem; }
.sb-edrow .sb-edname { flex: 1 1 8rem; max-width: 14rem; }
.sb-edfield { display: inline-flex; flex-direction: column; gap: .1rem; }
.sb-edfield > label { font-size: .62rem; color: var(--text-3); }
.sb-ednum { width: 4.2rem; }

/* ---------------------------------------------------------------- 手机（≤767px）
   这一档是被**真的量过**的：390×844 的视口下，页面不许横向滚动，所有可点元素 ≥44px。
   地图这一块手机上**不**把整张 1889px 的图缩进 390px（那就是"迷你版桌面"），
   而是保留可用下限（K_MIN_MOBILE）：一屏看一部分，**放大之后**才允许单指拖动。
   布局上手机把底部停靠条与右侧抽屉改成顺序排布（不再浮在地图上）。 */
@media (max-width: 767px) {
  .sb-stats { font-size: .76rem; }
  .sb-stat { padding: .18rem .35rem; }
  /* 三人图的战况条：三行座位在手机上也排得下（每行独占一行、允许换行，页面不横向溢出） */
  .sb-stat.sb-stat-seat { flex: 1 1 100%; }
  .sb-stat.sb-stat-seat .tiny { flex: 0 1 auto; }
  .sb-legend { gap: .5rem; }
  .sb-banner { font-size: .84rem; }
  .sb-banner .sb-banner-who { flex: 1 1 100%; }
  .sb-grid { grid-template-columns: 1fr; }
  /* ★ 2026-10-08：房间头部那一排按钮（房号/地图名/标签/战况/规则/复制/全屏/音效/结束回合/认输/离开）
     在 360–390px 上换行成 4–5 行、吃掉 200px 左右，把地图与缩放条一起挤出首屏
     （实测 360×640 上缩放条直接跑到视口外）。手机上改成**一行横滑**：
     高度从 ~200px 降到 ~52px，按钮仍然 ≥44px，页面本身不横向溢出。
     ★ 2026-10-09 返工（用户报「手机端结束回合这些按钮都被压缩的极小，几乎点不到」）：
     横滑这条修法只解决了一半 —— 整条头部仍然被 flex 压成 18px 高（见上面 .sb-head 的说明），
     而且 953px 的内容排在 360–390px 的窗口里，**「结束回合」在 x=759**：用户得先横滑才找得到，
     而他看到的只是一条 18px 的缝。这一版：① 头部不收缩；② 顶在 .st-main 顶沿（sticky top）；
     ③ **操作按钮排到信息前面**、主操作「结束回合 / 退出观战」排第一个，
     所以不横滑也一眼看得见、点得到（见下面 data-sb-primary 的说明）。 */
  .sb-head {
    flex-wrap: nowrap; overflow-x: auto; overflow-y: hidden; gap: .35rem;
    -webkit-overflow-scrolling: touch;
    touch-action: pan-x;                     /* 手指横滑这条工具栏（不是缩放页面） */
    position: sticky; top: 0; z-index: 6;    /* 纵向滚到地图/手牌时它还在（`.st-bar` 下面的顶沿） */
    min-height: 52px;
    scrollbar-width: none;
  }
  .sb-head::-webkit-scrollbar { display: none; }
  .sb-head .sb-grow { display: none; }              /* 占位弹簧在横滑行里没意义 */
  .sb-head > span, .sb-head > .g-tag, .sb-head .g-btn, .sb-head > b {
    flex: 0 0 auto; min-width: max-content; white-space: nowrap;
  }
  /* 操作按钮（data-sb-act）排到房号/地图名/标签**前面**：手机上"能出手"比"看房号"要紧。
     主操作（data-sb-primary：对局中是「结束回合」、观战中是「退出观战」）排在这一排**第一个**，
     所以它初始一定在视口里、一眼看得见（不横滑也点得到）。
     ⚠️ 不要给它 position:sticky —— 试过：钉在左沿的按钮会**盖住**横滑过来的下一颗
     （实测 390px 上 sticky 的「结束回合」压住「认输」的中心点，elementFromPoint 命中的是它，
     那颗按钮就点不动了）。宁可靠横滑，也不许任何一颗按钮被另一颗压住。 */
  .sb-head > .g-btn[data-sb-act] { order: -2; }
  .sb-head > .g-btn[data-sb-primary] { order: -3; }
  /* 手机：地图区不锁一屏（面板排在地图下面，页面可纵向滚动）。 */
  .sb-room { height: auto; overflow: visible; }
  /* ★ 2026-10-08：三个 flex 项都改成"不收缩"。旧版它们带着 flex:1 1 auto / min-height:0，
     结果父级（.sb-room/.sb-body＝一屏高）把 .sb-stage 从 clamp() 算出的 405px
     **压回 min-height:300px** —— 手机上的地图区只剩 300px 高（844 高的屏），
     缩放条又盖掉下半截，能点的点全被挤没了。地图区按 clamp() 的真实高度走，
     超出的部分交给 .st-main 滚动（手机本来就可以纵向滚，见头注释）。 */
  .sb-body { display: flex; flex-direction: column; gap: .45rem; overflow: visible; flex: 0 0 auto; }
  .sb-main { display: flex; flex-direction: column; min-height: 0; flex: 0 0 auto; }
  .sb-stage {
    position: relative; inset: auto; width: 100%; flex: 0 0 auto;
    height: clamp(320px, 52vh, 470px); min-height: 320px;
    overscroll-behavior: contain;              /* 手机上别把整页带着一起滚 */
  }
  .sb-hud {
    /* ★ 2026-10-09 手机端"按钮点不到"的**另一半**（头一条是 flex 压扁，见上面 .sb-head）：
       音乐浮动入口（player.js 在**没有底栏**的页面上往左下角塞一个 fixed 44×44 音符、
       z-index:60）正好压在房间左下角。实测 390×844：技能按钮中心点 elementFromPoint
       命中的是 BUTTON#mpEntry.mp-entry-float，缩放条上的「−」也一样。
       这一页**游戏自己的控件优先**：停靠条与缩放条抬到 z-index:61（都在 60 之上）。
       停靠条在手机上是文档流里的盒子，`position: relative` 不改布局，只是让它能参与层叠
       —— ⚠️ 必须同时 `inset: auto`：桌面那条规则留着 `left/right/bottom: 3.35rem`，
       relative 定位下 bottom 会**真的把它往上抬 54px** 盖住地图（实测相机 y 从 77 变 50、
       落点被它压住）。static 时这些 inset 是空操作，relative 就不是了。
       缩放条本来就是 absolute，加 z-index 同样不动布局。浮动入口在没被这两块盖住的地方
       照旧点得到。只作用在沙盘这一页（.sb-* 类名不与其他玩法共用）。 */
    position: relative; inset: auto; z-index: 61;
    max-height: none; background: #fff; border-top: 1px solid var(--border-faint, #eaecf0);
    padding: .4rem .2rem .5rem;
  }
  .sb-hud.off { max-height: none; }
  .sb-hud .sb-legend, .sb-hud .sb-handhead, .sb-hud .sb-skillhead { color: var(--text-2); }
  .sb-hud .sb-selinfo { background: #eaf3ff; }
  .sb-drawer { width: auto; max-height: none; }
  /* 编辑器舞台在手机上也是**文档流里的盒子**：高度跟着背景图比例走（见上面 .sb-edstage 那段），
     所以这里只留一个占位下限，不再写死高度（写死就会把图裁掉）。 */
  .sb-edstage { height: auto; min-height: 0; }
  /* 手机上点位再抬一档：圆点大一点好认（命中区＝盒子，见 .sb-node 的 max() 那段） */
  .sb-node { width: max(108px, var(--sb-hit, 108px)); height: max(108px, var(--sb-hit, 108px)); }
  .sb-pin { width: 50px; height: 50px; }
  .sb-badge { min-width: 34px; height: 34px; font-size: 23px; }
  .sb-urow { min-height: 48px; }
  .sb-target { min-height: 48px; }
  /* 详情面板在手机上变成底部抽屉（留出缩放条的位置） */
  .sb-detail { left: .4rem; right: .4rem; top: auto; bottom: 3.6rem; width: auto; max-height: 60%; }
  .sb-card { width: 108px; min-height: 146px; }
  .sb-hand { min-height: 162px; }
  /* ★ 2026-10-08 手机端"点不准/够不着"的返工：缩放条以前会换行成**三行**
     （「适应屏幕」被强制占一整行），在 300px 高的舞台上盖掉下半截，
     压在它下面的点位就永远点不到。现在压成**一行**（约 54px 高）：
     按钮各自 ≥44px、整行可横向滚动，盖住的地图面积从 ~145px 降到 ~54px，
     而且图比舞台大时用户可以单指把点位从它底下拖出来。 */
  .sb-zoom {
    left: .4rem; right: .4rem; justify-content: flex-start;
    flex-wrap: nowrap; overflow-x: auto; overflow-y: hidden; gap: .25rem;
    z-index: 61;                               /* 压过音乐浮动入口（z-index:60），见 .sb-hud 那段说明 */
  }
  .sb-zoom .g-btn { flex: 0 0 auto; min-height: 44px; padding-left: .4rem; padding-right: .4rem; }
  .sb-zoom .sb-zoom-val { min-width: 2.2rem; }
  .sb-rules dl { grid-template-columns: 4.2rem 1fr; }
  .sb-edrow .sb-edname { max-width: none; }
  .sb-seat { flex-wrap: wrap; }
}

/* 窄到 420px 以下（安卓小屏）：费用圆和卡面再收一点，别把三张牌挤成两张半 */
@media (max-width: 420px) {
  .sb-card { width: 102px; }
  .sb-stats .tiny { display: none; }
  /* 小屏上「2 人图/3 人图」标签与座位行再收一点：360px 也不许横向溢出 */
  .sb-stat.sb-stat-seat { font-size: .72rem; padding: .15rem .3rem; }
  .sb-playertag { font-size: .64rem; padding: .05rem .25rem; }
  .sb-seat .sb-seatno { min-width: 3.6rem; }
}

/* ------------------------------------------------- ① 常驻队列条 + ② 一键到顶/到底（手机）
   ★ 2026-10-10 用户报「手机端沙盘占点快速匹配要往下滑才能找到匹配情况，所以有时候不知道
   自己匹配上了」。两条都做在**外壳**上（sandbox.html 里 body.st-body 的直接子元素）：
     · .sb-pinbar —— 排队中/没能排上时的常驻条。它排在 .st-main **后面**、flex:0 0 auto，
                     所以一直贴在屏幕最下沿、滚动时不动，而且**一个像素都不盖住内容**
                     （内容区自己变矮）。这一点很关键：如果做成 fixed/absolute 浮层，
                     它总要在某个滚动位置压住某颗按钮 —— 而这一页"每个控件都点得到"
                     是有专门断言的（tools/test-sandbox-ui.js ⑦/⑫），压住就会被量出来。
                     配对成功那 3.2 秒它改成 `position: fixed` 的浮层（见 .sb-pinbar-ok）：
                     那一下必须**在任何视图里**都看得见，而 pointer-events:none 保证它不吃点击。
                     ⚠️ 类名是 sb-pinbar，**不是** sb-pin —— `.sb-pin` 是地图上那个点位小圆点
                     （见上面 .sb-pin / .sb-node .sb-pin），复用会把点位一起 display:none 掉。
     · .sb-scroll —— 一键到顶/到底的浮窗（两颗圆钮，各 46×46 ≥44px）。它钉在常驻条**上面**
                     （bottom = --sb-shell-bottom + .6rem，那个变量由 JS 按常驻条的实高写入），
                     只在这一页真的能滚的视图里出现（牌桌上有地图点位，不摆）。
   桌面（>767px）两条都不显示：那一档队列栏本来就在大厅里看得见，浮窗也没必要。 */
.sb-pinbar { display: none; }
.sb-scroll { display: none; }
.sb-matchbanner { display: none; }

@media (max-width: 767px) {
  .sb-pinbar:not([hidden]) {
    flex: 0 0 auto; display: flex; align-items: center; gap: .45rem;
    width: 100%; min-height: 52px; padding: .3rem .6rem;
    padding-bottom: calc(.3rem + env(safe-area-inset-bottom, 0px));
    background: var(--bg-subtle, #f8f9fa); border-top: 2px solid #3366cc;
    font-size: .82rem; line-height: 1.45;
  }
  .sb-pinbar-line { flex: 1 1 auto; min-width: 0; }
  .sb-pinbar-line b { font-family: var(--mono, monospace); }
  .sb-pinbar .g-btn { flex: 0 0 auto; min-height: 44px; min-width: 44px; padding-left: .55rem; padding-right: .55rem; }
  /* 没能排上（服务端一直不认）：换成警示色，原因与出路都留在这条上 */
  .sb-pinbar.sb-pinbar-bad { border-top-color: #b32424; background: #fff6f6; }
  /* ★ 配对成功的那 3.2 秒：同一个元素改成**浮层**（任何视图里都看得见），
     但 pointer-events:none —— 它绝不参与命中测试，压不住任何东西。 */
  .sb-pinbar.sb-pinbar-ok {
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 80;
    pointer-events: none; border-top-color: #0f6b57; background: #0f6b57; color: #fff; font-weight: 700;
    box-shadow: 0 -6px 18px rgba(0, 0, 0, .22);
  }
  .sb-pinbar.sb-pinbar-ok .sb-pinbar-line b { color: #fff; }

  .sb-scroll:not([hidden]) {
    display: flex; flex-direction: column; gap: .4rem;
    position: fixed; right: .55rem; z-index: 64;
    bottom: calc(var(--sb-shell-bottom, 0px) + .6rem);
  }
  .sb-scroll .g-btn {
    width: 46px; height: 46px; min-width: 46px; min-height: 46px; padding: 0;
    border-radius: 50%; font-size: 1.05rem; line-height: 1;
    background: #fff; border: 1px solid var(--border-soft, #c8ccd1);
    box-shadow: 0 2px 8px rgba(0, 0, 0, .18);
  }

  .sb-matchbanner:not([hidden]) {
    display: block; position: fixed; left: 0; right: 0; z-index: 80;
    pointer-events: none; text-align: center;
    padding: .8rem .7rem; background: #0f6b57; color: #fff;
    font-weight: 700; font-size: .92rem;
    box-shadow: 0 6px 18px rgba(0, 0, 0, .18);
  }
}

/* 减少动效：选中态的位移与飘字不动 */
@media (prefers-reduced-motion: reduce) {
  .sb-card.sel { transform: none; }
  .sb-dmg { animation-duration: .01s; }
  .sb-node.sb-capture, .sb-node.sb-hurt { animation: none; }
}

/* ==========================================================================
   ★ 2026-10-12 用户：「沙盘手牌技能展开放地图右侧吧（电脑端）」+「手机端再优化下」
   --------------------------------------------------------------------------
   结构（paintRoom 现画的，与上面那条"底部停靠条"是同一条 DOM 的两种摆法）：
     .sb-body › .sb-main（地图：.sb-stage / 缩放条）＋ .sb-side（一列）
                                                       ├ .sb-hud   手牌 / 技能 / 图例 / 提示
                                                       └ .sb-drawer 战况 / 规则 / 动作日志
     · ≥1024px：.sb-side 是**地图右侧的一列**（宽 clamp(19rem,24vw,23rem)），
       .sb-hud 占上半段（自己纵向滚，手牌在窄列里还是**一排横滑**：只有 ~290px 宽，换行会把技能顶出去），
       .sb-drawer 占下半段（默认收起，最多 48% 高，自己滚）；
       收起（.sb-side-off，由 setHud 写在包裹层上）→ 整列缩成 4rem 窄轨 + 一颗竖排按钮，
       地图立刻拿回整个宽度（相机不让位：hudInsetH 现在先看横向是否重叠）。
     · ≤767px：.sb-side 是 display:block，手牌与抽屉仍然是"排在地图下面的两块"（文件末尾那一段）。
     · 768–1023px：**不改**，还是上面那条浮在舞台下沿的底部停靠条（默认收起按钮一样能用）。
   为什么手牌与战况**同列上下**、不各占一列：1280px 上两列一起要吃掉 44rem，地图只剩 500 多像素，
   而用户明确要过「电脑端地图大些」。同列之后"开战况"不再改地图宽度 —— 地图也不会跳一下。
   为什么舞台必须 ≥70% 视口高：那条断言（tools/test-sandbox-ui.js ④ / tools/check-sandbox.js ⑤b）
   量的是**地图可视区**；旧版相机为了躲开底部停靠条要让出 45%，地图只能缩到 0.35×（668×311）。
   现在手牌列在舞台旁边、提示条也搬进那一列，舞台从 592px（66%）涨到 639px（71%）。
   ========================================================================== */
@media (min-width: 1024px) {
  /* ---- 右侧那一列：与 .sb-main（地图）并列，两者一起铺满 .sb-body ---- */
  .sb-side {
    flex: 0 0 auto; min-width: 0; display: flex; flex-direction: column; gap: .45rem;
    width: clamp(19rem, 24vw, 23rem);
  }
  .sb-side-off { width: 4rem; }                   /* 收起：只剩一条窄轨（按钮竖排） */

  /* ---- 上半段：手牌 / 技能 —— 和战况抽屉同一种"白卡片"，不再是深色停靠条 ----
     ⚠️ 选择器一律带 `.sb-side` 前缀（＝两个类）：基础那条 `.sb-hud.off { max-height: 2.9rem }`
     的权重是两个类，光写 `.sb-hud` 压不住它 —— 收起时那一列会被压成 46px 高的一条。 */
  .sb-side .sb-hud,
  .sb-side .sb-hud.off {
    position: static; inset: auto; left: auto; right: auto; top: auto; bottom: auto;
    flex: 1 1 auto; min-height: 0; max-height: none; overflow: auto; touch-action: pan-y;
    background: #fff; border: 1px solid var(--border-faint, #eaecf0); border-radius: 4px;
    padding: .45rem .5rem; gap: .3rem;
  }
  .sb-side .sb-hud .sb-legend { color: var(--text-2); font-size: .72rem; gap: .45rem; }
  .sb-side .sb-hud .sb-legend .sb-swatch { box-shadow: none; }
  .sb-side .sb-hud .sb-legend .sb-legend-3p { color: var(--text-3); }
  /* 电脑端右侧那一列的高度要留给手牌：那句"点一下点位看详情"（点一下就知道的事）不摆 */
  .sb-side .sb-hud .sb-legend-how { display: none; }
  .sb-side .sb-hud .sb-handhead, .sb-side .sb-hud .sb-skillhead { color: var(--text-strong, #202122); }
  .sb-side .sb-hud .sb-handhead .g-tag, .sb-side .sb-hud .sb-skillhead .tiny { color: var(--text-3); }
  .sb-side .sb-hud .sb-selinfo { border-left-color: #3366cc; background: #eaf3ff; }
  .sb-side .sb-hud .sb-card { min-height: 132px; box-shadow: 0 1px 3px rgba(0, 0, 0, .16); }
  /* 手牌在列里**还是一排横滑**（与大厅 / 手机端同一套手感）：列只有 ~290px 宽，
     3 张 116px 的牌排不下 —— 换行会把整列顶高 130px（技能被挤到看不见），
     而横滑只占**一行**的高度，卡面还能保持 132px（费用 / 名称 / 攻防 / 简介都在）。
     列本身纵向可滚（overflow:auto），所以再多的手牌也不会顶破这一屏。
     132px 这个数是量出来的：132×5 个技能（3 行 51px）+ 图例 + 提示 + 选择提示 + 标题行
     正好落在 1280×900 那一列 639px 里（旧版 150px 的卡面会让最后一个技能差 10px 露不出来）。 */
  .sb-side .sb-hud .sb-hand { min-height: 0; max-height: none; overflow-x: auto; overflow-y: hidden; padding-bottom: .25rem; }
  .sb-side .sb-hud .sb-skills .g-btn { background: none; }
  /* 技能按钮在列里压紧一档：名字 / 花费 / 选目标三行都在（信息一个不少），
     只是行距不再留白 —— 5 个技能从 242px 降到 ~160px，整列于是不用滚就能看全。 */
  .sb-side .sb-hud .sb-skill { gap: 0; padding-top: .1rem; padding-bottom: .1rem; }
  .sb-side .sb-hud .sb-skill .sb-skillcost, .sb-side .sb-hud .sb-skill .sb-skilltip { line-height: 1.25; }
  /* 开合按钮与战况抽屉那颗「收起 ▸」同一档：跟图例同一行（不再独占一行）——
     列里每一行都要留给手牌与技能。手机端它仍然是独占一行的大按钮（见下面 ≤767px 那一段）。 */
  .sb-side .sb-hud-toggle { width: auto; }
  /* ---- 下半段：战况 / 规则 / 动作日志（与手牌同列，最多占这一列的 48%） ---- */
  .sb-side .sb-drawer { width: auto; max-height: 48%; }
  /* ---- 收起：整列缩成窄轨，只留一颗竖排按钮 ---- */
  .sb-side-off .sb-hud { padding: .3rem .25rem; }
  .sb-side-off .sb-hud .sb-hudhead { flex-direction: column; align-items: stretch; }
  .sb-side-off .sb-hud .sb-legend,
  .sb-side-off .sb-hud .sb-hint,
  .sb-side-off .sb-hud .sb-selinfo,
  .sb-side-off .sb-hud .sb-col,
  .sb-side-off .sb-hud .sb-hudmini { display: none; }
  .sb-side-off .sb-hud .sb-hud-toggle {
    writing-mode: vertical-rl; text-orientation: mixed; letter-spacing: .12em;
    width: 100%; min-width: 44px; min-height: 6rem; height: auto; padding: .6rem 0 !important;
  }
}

/* ------------------------------------------------ 768–1023px（还是"手牌浮在舞台下沿"那一档）
   ★ 2026-10-12：手牌那一块搬进 .sb-side 之后，它的**定位基准**从 .sb-main 变成了 .sb-body
   （手牌已经不在地图那一列里了；.sb-main 是 position:relative，.sb-body 也是）。
   这一档还是"底部停靠条 + 右侧抽屉"，所以要专门补两件事（实测 980×800）：
     · 包裹层不参与布局（display:contents）—— 抽屉照旧是 .sb-body 的弹性子项、自己推着地图；
       不然一个宽度 0 的包裹层还会白吃掉 .sb-body 的一个 flex gap（7px）。
     · 抽屉展开时手牌条要让开抽屉的宽度（`right` 与 .sb-drawer 的宽度用同一个表达式）：
       实测抽屉展开时手牌条是 945px 宽而地图只有 644px，右边那 294px 整段压在抽屉底下 ——
       牌一多，最后几张就永远滑不出来。这一条用了 :has()：不支持的老浏览器只是维持原样
       （抽屉不透明，盖住那一段），功能不受影响。
   ⚠️ 范围写死在 768–1023：≥1024 是右侧一列（手牌 position:static，right 本来无效）、
   ≤767 手牌是 position:relative（给它 right 会真的把它推走）。 */
@media (min-width: 768px) and (max-width: 1023px) {
  .sb-side { display: contents; }
  .sb-body:has(.sb-drawer:not(.off)) .sb-hud { right: calc(min(22rem, 30vw) + .45rem); }
}

/* ------------------------------------------------ 手机（≤767px）再优化一档
   手牌 / 技能这一块手机端**本来就是排在舞台下面**（不吃地图、不吃命中区），
   这一版只做三件"少占高度、看得见、点得到"的事（数字见 content/_sandbox.md）：
     ① 两条"怎么读图"的长说明（.sb-legend-tip）收起来：旧版图例要占 3–4 行（78–106px），
        把"手牌 / 技能"顶到很下面；只留颜色图例一行，"怎么读图"交给点位详情面板现场解释；
     ② 开合那颗按钮**独占一行、拉满宽度**（≥44px）—— 一个拇指够得着、文案直接写"展开 / 收起"；
     ③ 收起时露出那一行摘要（.sb-hudmini），不展开也知道手里几张牌、几个技能、两笔钱多少。 */
@media (max-width: 767px) {
  .sb-legend-tip { display: none; }
  .sb-hud .sb-legend { font-size: .74rem; padding-top: .1rem; }
  .sb-hud .sb-skillhead .tiny { display: none; }        /* "两笔钱分开算"那句桌面留着，手机上省一行 */
  .sb-hud-toggle { flex: 1 1 100%; }                    /* 独占一行：图例一行、按钮一行 */
  .sb-hudmini { font-size: .74rem; }
}
