:root {
  --gold: #e7c27a;
  --gold-soft: rgba(231, 194, 122, 0.55);
  --green: #8be7b3;
  --green-soft: rgba(139, 231, 179, 0.45);
  --ink: #f3ead8;
  --muted: #a7a18f;
  --felt-dark: #0c1b17;
  --wood: #3a2519;
  --line: rgba(231, 194, 122, 0.16);
}

* {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: #080a0b;
}

body {
  color: var(--ink);
  font: 13px/1.4 "Microsoft YaHei", "Segoe UI", system-ui, sans-serif;
  -webkit-font-smoothing: antialiased;
}

button {
  color: inherit;
  font: inherit;
}

[hidden] {
  display: none !important;
}

#viewport {
  position: fixed;
  inset: 0;
  overflow: hidden;
  background:
    radial-gradient(ellipse at 50% 45%, #141918 0%, #080a0b 68%),
    #080a0b;
}

#stage {
  --cw: 104px;
  --ch: 147px;
  --vis: 74px;
  --scale: 1;
  position: absolute;
  left: 50%;
  top: 50%;
  width: 1600px;
  height: 900px;
  overflow: hidden;
  border-radius: 26px;
  transform: translate(-50%, -50%) scale(var(--scale));
  transform-origin: center center;
  background:
    radial-gradient(ellipse at 50% 42%, rgba(95, 140, 113, 0.18) 0%, rgba(31, 58, 48, 0.08) 38%, transparent 70%),
    radial-gradient(ellipse at 50% 45%, #1d3b32 0%, #142a24 52%, #0b1915 100%);
  box-shadow:
    0 40px 110px rgba(0, 0, 0, 0.8),
    0 10px 24px rgba(0, 0, 0, 0.55);
}

/* 木围边 */
#stage::before {
  content: "";
  position: absolute;
  inset: 7px;
  z-index: 2;
  border: 16px solid var(--wood);
  border-radius: 25px;
  box-shadow:
    inset 0 0 0 2px rgba(255, 220, 160, 0.12),
    inset 0 0 0 3px rgba(0, 0, 0, 0.55),
    0 0 0 2px rgba(0, 0, 0, 0.6),
    0 0 0 4px rgba(160, 110, 62, 0.18);
  pointer-events: none;
}

#table-surface {
  position: absolute;
  inset: 25px;
  z-index: 1;
  border-radius: 17px;
  pointer-events: none;
  background:
    radial-gradient(ellipse at 50% 44%, rgba(164, 195, 156, 0.055) 0%, transparent 58%),
    repeating-linear-gradient(92deg, rgba(255, 255, 255, 0.012) 0 1px, transparent 1px 3px),
    repeating-linear-gradient(2deg, rgba(0, 0, 0, 0.08) 0 1px, transparent 1px 4px),
    linear-gradient(180deg, rgba(35, 66, 55, 0.32), rgba(6, 18, 15, 0.24));
  box-shadow:
    inset 0 0 0 1px rgba(231, 194, 122, 0.08),
    inset 0 0 90px rgba(0, 0, 0, 0.52);
}

#zone-layer,
#card-layer,
#hud-layer {
  position: absolute;
  inset: 0;
}

#zone-layer {
  z-index: 10;
  pointer-events: none;
}

#arrow-layer {
  position: absolute;
  inset: 0;
  z-index: 15;
  overflow: visible;
  pointer-events: none;
}

/* 不设 z-index：让单张牌自己的 z-index 和 HUD 比较 */
#card-layer {
  z-index: auto;
  pointer-events: none;
}

#hud-layer {
  z-index: 100;
  pointer-events: none;
}

/* ── 顶栏 ───────────────────────────────────────────── */

#topbar {
  position: absolute;
  top: 20px;
  left: 30px;
  right: 30px;
  height: 36px;
  z-index: 500;
  display: flex;
  align-items: center;
  gap: 18px;
  color: var(--muted);
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
}

.brand {
  display: flex;
  align-items: baseline;
  gap: 8px;
  flex: 0 0 auto;
}

.brand-title {
  color: var(--ink);
  font-family: "Noto Serif SC", "Songti SC", "SimSun", serif;
  font-size: 18px;
  letter-spacing: 2px;
}

.brand-sub {
  color: var(--gold-soft);
  /* 原来这里只有 9px，被游戏名压着；现在它自己就是左上角唯一的一行。 */
  font-size: 18px;
  letter-spacing: 1.6px;
  text-transform: uppercase;
}

/*
 * 第几轮 + 倒计时：夹在牌栈与回合阶段之间，横向居中。
 * 牌栈底在 y≈497、回合阶段在 528，这一条正好落在中间那 30px 里。
 */
.turn-strip {
  position: absolute;
  left: 50%;
  top: 499px;
  transform: translateX(-50%);
  z-index: 55;
  display: flex;
  align-items: center;
  gap: 16px;
  pointer-events: none;
  white-space: nowrap;
}

.turn-strip .round-label {
  color: var(--gold-soft);
  font-size: 19px;
  letter-spacing: 1.5px;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.85);
}

.turn-strip .countdown {
  width: 420px;
  height: 7px;
  margin: 0;
  border-radius: 4px;
}

/* 回合阶段：从右上角搬到牌栈与自己储蓄栏之间的空档，水平居中。
 * 牌栈底在 y≈497、储蓄栏最高一行在 y≈566 起，528 正好落在这一带中间。 */
.phase-rail {
  position: absolute;
  left: 50%;
  top: 528px;
  transform: translateX(-50%);
  /* 手牌放大到 2 倍时上沿会顶到这一带（底边 826 - 294 ≈ 532），
   * 所以这行提示必须压在牌下面，否则会盖住正在挑的那张牌。 */
  z-index: 55;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  max-width: 900px;
  color: var(--muted);
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
  pointer-events: none;
}

.phase-step {
  color: rgba(220, 214, 194, 0.4);
  font-size: 20px;
  letter-spacing: 0.8px;
  white-space: nowrap;
}

.phase-step.active {
  color: var(--gold);
  font-weight: 600;
  text-shadow: 0 0 12px rgba(231, 194, 122, 0.38);
}

/*
 * 被打断后的「回合中」。
 *
 * 金色是「轮到你了，慢慢来」；大红色是「这回合已经废了，只剩一张牌的机会」。
 * 不用 --danger：那个是砖红，跟打牌按钮的警示色同一个，混在阶段条里不够扎眼。
 * 阶段名不换——人确实还在回合中，只是这个回合中不一样了。
 */
.phase-step.halted {
  color: #f01e1e;
  text-shadow: 0 0 12px rgba(240, 30, 30, 0.55);
}

.phase-dot {
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: rgba(231, 194, 122, 0.22);
}

.top-right {
  display: flex;
  align-items: center;
  gap: 10px;
  flex: 0 0 auto;
  /* 阶段条搬走后，右侧这一组要自己贴到最右边。 */
  margin-left: auto;
}

.round-label,
.turn-label {
  font-size: 10px;
  color: rgba(220, 214, 194, 0.6);
  white-space: nowrap;
}

.turn-label {
  color: var(--green);
  text-shadow: 0 0 10px rgba(139, 231, 179, 0.28);
}

.icon-btn {
  width: 40px;
  height: 40px;
  padding: 0;
  border-radius: 50%;
  font-size: 18px;
  border: 1px solid rgba(231, 194, 122, 0.26);
  background: rgba(12, 25, 21, 0.45);
  color: rgba(231, 194, 122, 0.82);
  cursor: pointer;
  line-height: 1;
}

.icon-btn:hover {
  border-color: var(--gold);
  color: var(--gold);
}

/* ── 原型控制 ───────────────────────────────────────── */

.glass-panel {
  background:
    linear-gradient(180deg, rgba(55, 74, 63, 0.42), rgba(10, 23, 19, 0.66));
  border: 1px solid rgba(231, 194, 122, 0.2);
  box-shadow:
    inset 0 1px rgba(255, 255, 255, 0.06),
    0 12px 30px rgba(0, 0, 0, 0.35);
  backdrop-filter: blur(9px);
}

#proto-panel {
  position: static;
  z-index: auto;
  width: auto;
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 7px;
  padding-left: 14px;
  border-left: 1px solid rgba(231, 194, 122, 0.14);
}

.proto-kicker {
  color: var(--gold-soft);
  font-size: 9px;
  letter-spacing: 1.2px;
  margin: 0;
  white-space: nowrap;
}

.proto-row {
  display: flex;
  align-items: center;
  gap: 8px;
}

.proto-label {
  color: var(--muted);
  font-size: 10px;
}

.seat-buttons {
  display: flex;
  gap: 3px;
}

.seat-buttons button {
  width: 24px;
  height: 22px;
  padding: 0;
  border-radius: 5px;
  border: 1px solid rgba(231, 194, 122, 0.18);
  background: rgba(0, 0, 0, 0.2);
  color: var(--muted);
  font-size: 10px;
  cursor: pointer;
}

.seat-buttons button.active {
  color: #21170a;
  background: var(--gold);
  border-color: var(--gold);
}

.proto-reset {
  width: auto;
  margin: 0;
  padding: 3px 7px;
  border-radius: 5px;
  border: 1px solid rgba(231, 194, 122, 0.16);
  background: rgba(0, 0, 0, 0.16);
  color: var(--muted);
  font-size: 10px;
  white-space: nowrap;
  cursor: pointer;
}

.proto-reset:hover {
  color: var(--ink);
  border-color: var(--gold-soft);
}

/* ── 栏位底板 ───────────────────────────────────────── */

.shelf {
  position: absolute;
  border-radius: 9px;
  border: 1px solid rgba(231, 194, 122, 0.12);
  background:
    linear-gradient(180deg, rgba(255, 255, 255, 0.035), rgba(0, 0, 0, 0.08));
  box-shadow:
    inset 0 1px rgba(255, 255, 255, 0.045),
    inset 0 -10px 18px rgba(0, 0, 0, 0.22);
}

.shelf::after {
  content: "";
  position: absolute;
  left: 9px;
  right: 9px;
  bottom: 3px;
  height: 1px;
  background: linear-gradient(90deg, transparent, rgba(231, 194, 122, 0.12), transparent);
}

/*
 * 栏位默认**不接事件**：`#zone-layer` 是 `pointer-events: none`（否则这一层会
 * 盖住落在栏位上的牌），而 `pointer-events` 是继承属性，`.shelf` 跟着变成 none。
 *
 * 只有「点起了自己的装备、等着点目标区」时，`syncDropTargets` 会给目标栏位挂上
 * `.is-drop-target` —— 那才是它唯一需要响应点击的时刻。
 *
 * 少了这一条，标准版的「点装备、再点储蓄栏」点上去一点反应都没有：明储的游戏
 * 逻辑、`storeMoveOf`、命令全都在，唯独没东西接那一下点击。
 */
.shelf.is-drop-target {
  pointer-events: auto;
}

/* 栏位名：只留中间这一处（侧边的竖排标签已经去掉）。
 * 字与字之间的空隙交给 label 里的空格，这里只补一点点。 */
.shelf-tag {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  color: rgba(231, 194, 122, 0.14);
  font-family: "Noto Serif SC", "Songti SC", "SimSun", serif;
  font-size: 64px;
  line-height: 1;
  letter-spacing: 2px;
  /* pre：保留标签里的连续空格（nowrap 会把它折叠成一个）。 */
  white-space: pre;
  pointer-events: none;
}

/* ── 玩家 HUD ───────────────────────────────────────── */

/* 全知观战：座位能点开手牌。只加手型光标，不加边框——
   那会把牌桌弄成「一排按钮」，而观众只是偶尔想翻一下。
   `pointer-events` 必须显式打开：它继承自 `#hud-layer` 的 none。 */
#stage.is-omniscient .seat-hud {
  cursor: pointer;
  pointer-events: auto;
}

.seat-hud {
  position: absolute;
  display: flex;
  align-items: center;
  gap: 8px;
  color: var(--ink);
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.85);
}

/*
 * 可选目标时，整块座位自己也能点。
 *
 * `pointer-events` 是继承属性：`#hud-layer` 设了 none，底下所有子元素（角色图、
 * 生命珠、名字）就一起点不到了，`wrap` 上挂的 click 永远收不到事件——选目标
 * 实际只剩 `.seat-hit` 一块热区在撑。这里在「可选」这一档把它打开。
 */
.seat-hud.targetable {
  pointer-events: auto;
  cursor: pointer;
}

/* 自己的角色图也是「点我」的入口：选目标 / 回答带 seat 的选择时亮金边。 */
.seat-hud.targetable .role-card {
  border-color: rgba(231, 194, 122, 0.95);
  box-shadow:
    0 0 0 2px rgba(231, 194, 122, 0.3),
    0 4px 9px rgba(0, 0, 0, 0.55);
}

.seat-hud.centered {
  transform: translateX(-50%);
}

.seat-hud.right-hud {
  flex-direction: row-reverse;
  text-align: right;
}

.hand-icon-wrap {
  position: relative;
  width: 68px;
  height: 96px;
  flex: 0 0 auto;
}

/*
 * 角色图：占的正是原来「手牌示意」那一格（对手的牌背图标）。
 *
 * 素材是 `assets/roles/<id>.webp`——六张「妹妹」立绘，自带边框，尺寸按 2:3
 * 生成，与这一格一致，`cover` 不会切到脸。（原来是阵营色 + 人形剪影的占位，
 * 现在换成真图，几何没动。）
 *
 * 图层和**栏位**一个档（栏框 300 / 栏位牌 320）：它和栏位是同一种东西——
 * 普通摆在桌面上的立绘，不是正在操作的手牌，也不是要抢在最前面的界面 chrome。
 * 所以既不该压着栏位牌，也没必要被抬到生命珠那一层去。318 这个具体数字是
 * 被回合色带逼出来的：色带是 310/315（背景提示），立绘必须压在它上面——
 * 不然「轮到你」的呼吸光会从角色图脸上横着扫过去。
 * 图没加载出来时下面还有一层深色牌形，不至于空一块。
 */
.role-card {
  --camp: #26382e;
  position: relative;
  z-index: 318;
  display: block;
  width: 64px;
  height: 90px;
  border-radius: 6px;
  object-fit: cover;
  overflow: hidden;
  background:
    radial-gradient(120% 78% at 50% 0%, rgba(255, 255, 255, 0.16) 0%, rgba(0, 0, 0, 0) 62%),
    linear-gradient(180deg, color-mix(in srgb, var(--camp) 46%, #0b1512) 0%, #0a1411 74%);
  border: 1px solid rgba(231, 194, 122, 0.3);
  box-shadow: 0 4px 9px rgba(0, 0, 0, 0.55);
}

/* 自己的角色图：挂在生命珠**下面**，比对手那格略窄一点，免得压到左下开关。
 * 留出的间距要比对手那边松，否则紧贴珠子的下沿，看着像压在珠子上。 */
.role-card.own-role {
  width: 58px;
  height: 82px;
  margin-top: 14px;
}

/*
 * 手牌数：一块**牌背做底**的牌形底板，贴在屏幕四边。
 *
 * 尺寸是上一版的两倍（46×30 → 92×60，竖放 60×92）：它是远景里唯一能
 * 一眼读出「那人还剩几张」的东西，太小就白放了。
 */
.hand-count {
  position: absolute;
  display: grid;
  place-items: center;
  transform: translate(-50%, -50%);
  border-radius: 8px;
  border: 1px solid rgba(231, 194, 122, 0.46);
  /* 牌背图打底，压一层暗罩保证数字读得出来 */
  background-color: #0b1512;
  background-image: linear-gradient(rgba(4, 12, 9, 0.44), rgba(4, 12, 9, 0.66)),
    url('assets/cards/back.webp');
  background-size: cover, cover;
  background-position: center, center;
  box-shadow:
    0 4px 10px rgba(0, 0, 0, 0.6),
    inset 0 0 0 1px rgba(0, 0, 0, 0.35);
  color: #fff6dd;
  font-family: "Noto Serif SC", "Songti SC", "SimSun", serif;
  font-size: 36px;
  line-height: 1;
  text-shadow:
    0 2px 6px rgba(0, 0, 0, 0.95),
    0 0 12px rgba(0, 0, 0, 0.7);
  pointer-events: none;
  z-index: 545;
}

/* 对侧（上/下）座位：贴上下边，横放 */
.hand-count.h {
  width: 92px;
  height: 60px;
}

/* 左右座位：贴左右边，竖放 */
.hand-count.v {
  width: 60px;
  height: 92px;
}

/*
 * 这一块在「要对他动手」的时候是可点的。
 *
 * `#hud-layer` 是 `pointer-events: none`（整层不接事件），所以底板默认也点不到。
 * 但强盗逻辑、透视这类「点他拿一张手牌」的牌，玩家想点的就是那个数字——
 * 它贴在屏幕边上，跟座位热区根本不重合。于是在可选目标时单独打开。
 */
.hand-count.is-target {
  pointer-events: auto;
  cursor: pointer;
  border-color: rgba(231, 194, 122, 0.95);
  box-shadow:
    0 0 0 2px rgba(231, 194, 122, 0.28),
    0 6px 16px rgba(0, 0, 0, 0.62);
}

.hand-count.is-target:hover {
  border-color: var(--gold);
  box-shadow:
    0 0 0 3px rgba(231, 194, 122, 0.42),
    0 0 22px rgba(231, 194, 122, 0.35),
    0 6px 16px rgba(0, 0, 0, 0.62);
}

.seat-meta {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

/* 对侧玩家：手牌图标 / 名字 / 生命珠竖着放在三栏左侧。 */
.seat-hud:is(.top-side-hud, .bottom-side-hud) {
  flex-direction: column;
  align-items: center;
  gap: 4px;
  width: 76px;
}

:is(.top-side-hud, .bottom-side-hud) .seat-meta {
  gap: 0;
}

:is(.top-side-hud, .bottom-side-hud) .seat-name {
  font-size: 10px;
  line-height: 1.2;
}

:is(.top-side-hud, .bottom-side-hud) .hand-icon-wrap {
  width: 68px;
  height: 96px;
}

/* 手牌数已经搬去栏位中线，HUD 里只留角色图，尺寸与原来那格一致 */
:is(.top-side-hud, .bottom-side-hud) .role-card {
  width: 64px;
  height: 90px;
}

:is(.top-side-hud, .bottom-side-hud) .life.small {
  width: 44px;
  height: 44px;
  padding: 4px;
  transform: scale(1.3);
}

:is(.top-side-hud, .bottom-side-hud) .life.small .life-inner {
  font-size: 18px;
}

.seat-name {
  color: rgba(246, 238, 218, 0.92);
  font-size: 12px;
  font-weight: 600;
  white-space: nowrap;
  /* 抬一点点：生命珠放大时不会压住名字。不占布局，只挪视觉。 */
  transform: translateY(-3px);
}

.seat-hud.active .seat-name {
  color: var(--gold);
}

/* 「轮到谁」不再靠生命珠放大 + 描边：整套呼吸动画搬到栏位外侧的阵营色带上。
 * 好处是行动提示和队伍颜色落在同一个位置上，不用在两处之间来回看。 */

/* ── 阵营色带：颜色就是队伍 ─────────────────────────────── */

.camp-band {
  position: absolute;
  border-radius: 3px;
  background: var(--camp, var(--muted));
  opacity: 0.58;
  pointer-events: none;
  transition: opacity 180ms ease;
}

/* 六队各一个色，与大厅里选阵营的按钮同色 */
:is(.camp-band)[data-camp='A'] { --camp: #c9563f; }
:is(.camp-band)[data-camp='B'] { --camp: #4a7fb5; }
:is(.camp-band)[data-camp='C'] { --camp: #c8a13a; }
:is(.camp-band)[data-camp='D'] { --camp: #6b9c55; }
:is(.camp-band)[data-camp='E'] { --camp: #8b62b0; }
:is(.camp-band)[data-camp='F'] { --camp: #b8733f; }

/* 双阵营（奇偶座）用对抗色：奇数座蓝、偶数座红。
 *
 * 两人局就是「我 / 对手」：玩家总坐 1 号座（奇数）＝蓝，对手 2 号座（偶数）＝红。
 * 以前这里是中性灰，敌我在桌上一眼分不出来；蓝红是牌桌最熟的对抗语义。
 *
 * 色值直接取大厅 B / A 队的颜色，全站还是同一套色板（这里只是把奇偶两阵营
 * 落到已有的两个色上，不新造颜色）。 */
:is(.camp-band)[data-camp='odd'] { --camp: #4a7fb5; }
:is(.camp-band)[data-camp='even'] { --camp: #c9563f; }

/* 粗细不跟着舞台一起缩。
 *
 * 舞台是 1600×900 的定尺画布，手机横屏时整体缩到 0.5 倍上下：5px 的色带只剩
 * 两三个屏幕像素，呼吸起来像一根抖动的天线，远看就是「没有光带」。
 * 这里按 --scale 反向补偿（变量在 `#stage` 上，色带是它的后代），保证视觉上
 * 不少于 4 个屏幕像素；桌面缩放到 1 倍以上时 max() 取 5px，维持原样。
 *
 * 只夹最小尺寸，不改 `height` / `width` 本身——布局算好的长度仍是 JS 给的。 */
.camp-band.top,
.camp-band.own {
  min-height: max(5px, calc(4px / var(--scale)));
}

.camp-band.left,
.camp-band.right {
  min-width: max(5px, calc(4px / var(--scale)));
}

/* 轮到他了：整条色带呼吸。
 *
 * 它是全桌唯一的「轮到谁」指示，远看必须一眼扫到，所以亮度分三层：
 *   · **亮芯**——底色往白里提，像一根点亮的灯管，而不是涂了队色的胶带；
 *   · **外发光**——三层 drop-shadow，近处紧、中间亮、远处散，桌沿外也发亮；
 *   · **谷底**——最暗也留 0.62，不会有一段时间看起来像没亮。
 *
 * 发光全是静态的（渐变 + filter），每帧只动 `opacity`，光栅化一次交给合成器。
 * 以前给 `box-shadow` 打关键帧，等于每帧重画三层大半径模糊，一开就卡。
 *
 * 亮芯和外层发光要用 `color-mix()`，而它到 Chrome 111 才有。**不能**像原来那样
 * 在同一个属性里写两条声明当回退：后一条里的 `color-mix()` 一旦浏览器不认识，
 * 整条属性会在计算期变成无效、回落**初始值**（背景透明、`filter: none`），
 * 前一条纯队色的根本接不住。手机上整条呼吸光带就是这么消失的——手机上浏览器
 * 内核老（实测 Chrome 109），桌面的新，所以只有移动设备犯病。
 *
 * 兜底版放外面，增强版塞进 `@supports`，两边不会互相污染。 */
.camp-band.active {
  background: var(--camp, var(--muted));
  animation: campBreathe 1.9s ease-in-out infinite;
  filter: drop-shadow(0 0 6px var(--camp)) drop-shadow(0 0 18px var(--camp));
  /* 只比三栏（`.shelf` 300）高一点点：色带是背景提示，手牌 / 牌栈 / 弹丸
   * 都该盖过它。同时建一个层叠上下文，免得外发光被相邻栏位的背景切掉。 */
  z-index: 310;
}

@supports (color: color-mix(in srgb, red, blue)) {
  .camp-band.active {
    background: linear-gradient(
      to bottom,
      color-mix(in srgb, var(--camp) 40%, #fff) 0%,
      color-mix(in srgb, var(--camp) 72%, #fff) 46%,
      var(--camp) 100%
    );
    filter:
      drop-shadow(0 0 4px color-mix(in srgb, var(--camp) 55%, #fff))
      drop-shadow(0 0 13px var(--camp))
      drop-shadow(0 0 30px var(--camp));
  }
}

.camp-band.dead {
  opacity: 0.14;
  animation: none;
}

@keyframes campBreathe {
  0%,
  100% {
    opacity: 0.62;
  }
  50% {
    opacity: 1;
  }
}

/* 弹道（`.proj` / `.proj-beam` / `.proj-guide` / `.proj-hit`）在 `bolt.css`：
 * 牌桌和动效实验室共用同一套，样式只维护一份。 */

.hand-icon.targeted {
  border-color: var(--green);
  box-shadow:
    0 0 0 2px rgba(139, 231, 179, 0.45),
    0 0 22px rgba(139, 231, 179, 0.38),
    0 4px 9px rgba(0, 0, 0, 0.55);
  animation: targetPulse 1.8s ease-in-out infinite;
}

@keyframes targetPulse {
  0%,
  100% {
    box-shadow:
      0 0 0 2px rgba(139, 231, 179, 0.35),
      0 0 18px rgba(139, 231, 179, 0.28),
      0 4px 9px rgba(0, 0, 0, 0.55);
  }
  50% {
    box-shadow:
      0 0 0 3px rgba(139, 231, 179, 0.55),
      0 0 28px rgba(139, 231, 179, 0.45),
      0 4px 9px rgba(0, 0, 0, 0.55);
  }
}

/* 生命珠 */
.life {
  position: absolute;
  width: 86px;
  height: 86px;
  border-radius: 50%;
  padding: 5px;
  background: conic-gradient(
    var(--life, var(--green)) 0 var(--pct, 75%),
    rgba(139, 231, 179, 0.10) var(--pct, 75%) 100%
  );
  box-shadow:
    0 0 0 1px rgba(139, 231, 179, 0.24),
    0 10px 24px rgba(0, 0, 0, 0.5),
    inset 0 0 14px rgba(0, 0, 0, 0.35);
}

.life.inline {
  position: relative;
  left: auto;
  top: auto;
  flex: 0 0 auto;
}

.life-inner {
  width: 100%;
  height: 100%;
  border-radius: 50%;
  display: grid;
  place-items: center;
  border: 1px solid rgba(190, 255, 215, 0.22);
  background:
    radial-gradient(circle at 38% 28%, rgba(255, 255, 255, 0.12), transparent 32%),
    radial-gradient(circle at 50% 55%, rgba(16, 45, 34, 0.25), rgba(7, 20, 16, 0.58));
  backdrop-filter: blur(7px) saturate(1.1);
  box-shadow:
    inset 0 1px rgba(255, 255, 255, 0.16),
    inset 0 -8px 16px rgba(0, 0, 0, 0.35);
  color: var(--life, var(--green));
  font-family: "Noto Serif SC", "Songti SC", "SimSun", serif;
  font-size: 32px;
  line-height: 1;
  /* 不用 `color-mix()` 的写法：老内核上它会让整条 `text-shadow` 变 none，
   * 生命珠的光就没了（详见 `.camp-band.active` 上面那段）。这里先给一层
   * 不透明的柔光，认识 `color-mix()` 的浏览器再换成淡的。 */
  text-shadow: 0 0 12px currentColor;
}

@supports (color: color-mix(in srgb, red, blue)) {
  .life {
    text-shadow: 0 0 14px color-mix(in srgb, var(--life, var(--green)) 35%, transparent);
  }
}

.life.small {
  width: 56px;
  height: 56px;
  padding: 4px;
  /* 回合外的珠子也放大一点：只缩放视觉，不占布局、不挤名字和暗储。
   * 缩放原点同样放在上沿，名字不会被压住。 */
  transform: scale(1.22);
  transform-origin: 50% 0%;
}

.life.small .life-inner {
  font-size: 22px;
}

.own-label {
  position: absolute;
  color: rgba(231, 194, 122, 0.5);
  font-size: 10px;
  letter-spacing: 2px;
  text-shadow: 0 1px 3px #000;
}

/* ── 牌堆 / 弃牌堆 ─────────────────────────────────── */

.pile {
  position: absolute;
  width: 104px;
  height: 147px;
  /* hud-layer 整体是 pointer-events: none，子元素不显式打开就点不到——
   * 牌堆 / 弃牌堆的点击以前只有程序化 click 测得出，真实鼠标是点空的。 */
  pointer-events: auto;
  cursor: pointer;
}

.pile-layer {
  position: absolute;
  inset: 0;
  border-radius: 5px;
  overflow: hidden;
  border: 1px solid rgba(231, 194, 122, 0.16);
  box-shadow: 0 7px 14px rgba(0, 0, 0, 0.5);
  background: #0b1110;
}

.pile-layer img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* 牌堆 / 弃牌堆空的时候就是一个虚线牌框 */
.pile.is-empty {
  border: 1.5px dashed rgba(231, 194, 122, 0.34);
  border-radius: 7px;
  background: rgba(8, 14, 12, 0.34);
}

.pile.is-empty .pile-label {
  opacity: 0.42;
}

/* 伤害飘字与全屏暗角（只在本人掉血时出现） */
.dmg-float {
  position: fixed;
  z-index: 900;
  pointer-events: none;
  font: 800 34px/1 "Microsoft YaHei", "Segoe UI", sans-serif;
  color: #ff8f7d;
  text-shadow: 0 0 20px rgba(255, 60, 40, 0.8), 0 2px 8px rgba(0, 0, 0, 0.9);
  transform: translate(-50%, -50%);
}

.dmg-float.heal {
  color: #8be7b3;
  text-shadow: 0 0 20px rgba(80, 230, 150, 0.8), 0 2px 8px rgba(0, 0, 0, 0.9);
}

/* 被抵挡：血量一点没变，用中性色说「这一下被挡掉了」，不要再喊红 */
.dmg-float.blocked {
  color: #e7c27a;
  font-size: 24px;
  text-shadow: 0 0 18px rgba(231, 194, 122, 0.7), 0 2px 8px rgba(0, 0, 0, 0.9);
}

.dmg-vignette {
  position: fixed;
  inset: 0;
  z-index: 890;
  pointer-events: none;
  opacity: 0;
  background: radial-gradient(
    ellipse at 50% 50%,
    transparent 38%,
    rgba(150, 18, 14, 0.42) 78%,
    rgba(110, 10, 8, 0.62) 100%
  );
}

/* 出局标记：盖在座位下方，等座位褪色到三分之一才浮出来 */
.seat-out {
  position: absolute;
  left: 50%;
  top: 100%;
  transform: translate(-50%, 6px) rotate(-6deg);
  padding: 2px 9px;
  border: 1px solid rgba(255, 120, 100, 0.6);
  border-radius: 5px;
  background: rgba(40, 8, 6, 0.65);
  color: #ff9c8b;
  font-size: 12px;
  letter-spacing: 3px;
  pointer-events: none;
  white-space: nowrap;
}

.pile-label,
.pile-count {
  position: absolute;
  z-index: 30;
  color: var(--gold);
  text-shadow: 0 1px 3px #000;
}

.pile-label {
  left: 7px;
  top: 6px;
  font-size: 27px;
  letter-spacing: 1px;
  opacity: 0.75;
}

.pile-count {
  right: 7px;
  bottom: 5px;
  font-family: "Noto Serif SC", "Songti SC", "SimSun", serif;
  font-size: 40px;
  line-height: 1;
}

.pile-count small {
  font-family: "Microsoft YaHei", sans-serif;
  font-size: 18px;
  color: rgba(231, 194, 122, 0.55);
}

.pile.discard .pile-layer:nth-child(odd) {
  transform: rotate(1.4deg) translate(3px, -2px);
}

/* ── 牌 ─────────────────────────────────────────────── */

.card {
  position: absolute;
  width: var(--cw);
  height: var(--ch);
  z-index: 60;
  border-radius: 5px;
  background: #050706;
  transform-origin: center center;
  transform:
    translate3d(calc(var(--x, 0px) + var(--hx, 0px)), calc(var(--y, 0px) + var(--hy, 0px)), 0)
    rotate(var(--rot, 0deg))
    scale(var(--s, 1));
  transition:
    transform 150ms cubic-bezier(0.2, 0.7, 0.2, 1),
    clip-path 150ms ease,
    box-shadow 150ms ease,
    filter 180ms ease,
    opacity 180ms ease;
  box-shadow: 0 6px 13px rgba(0, 0, 0, 0.55);
  pointer-events: auto;
  outline: none;
}
.card img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 4px;
  user-select: none;
  -webkit-user-drag: none;
}

.zone-card {
  clip-path: inset(0 0 49.44% 0);
  cursor: pointer;
}

.zone-card:hover,
.zone-card:focus-visible {
  --s: 2;
  /* 写成 none 而不是 inset(0)：整张浮起时要把「垫在下面的素材牌」也放出来，
     它向左溢出牌框，inset(0) 正好把它裁掉。 */
  clip-path: none;
  z-index: 300;
  box-shadow:
    0 26px 48px rgba(0, 0, 0, 0.72),
    0 0 0 2px rgba(231, 194, 122, 0.9),
    0 0 24px rgba(231, 194, 122, 0.16);
}

/*
 * 自己区域的栏位牌点开查看：**底边固定，往上长**。
 *
 * 这几张只露上半张，位置又贴着舞台下沿（装备栏底边 893，舞台高 900）。
 * 以牌中心为原点放大 2 倍，下半张会伸到 966——整块掉出屏幕。
 * 把原点挪到牌底之后，牌底停在原来那条线上，多出来的部分全往桌面上方展开，
 * 露出过的上半张也就落在放大后牌面的上半段。
 *
 * 对侧/左右座位不能这么改：他们的栏位靠近舞台顶边，向上长反而会顶出去。
 */
.zone-card.own-zone:hover,
.zone-card.own-zone:focus-visible,
.zone-card.own-zone.is-doomed-zone {
  transform-origin: 50% 100%;
}

/*
 * 弃牌选择里选中的储蓄栏牌（明储的正面、暗储的牌背都算）。
 *
 * 和手牌选的 `is-doomed` 是同一件事，但那套「抬起来 + 金边」在这里不好使：
 * `.zone-card` 带着 `clip-path` 只露上半张，外阴影和 `drop-shadow` 会跟着被裁掉。
 * 所以选中时直接**放开裁切、整张浮起来**（hover 也是这么做的，不会压坏行），
 * 再加一圈金边——「这张我选了」一眼可见，不用盯着一道细线猜。
 */
.zone-card.is-doomed-zone {
  --s: 1.45;
  clip-path: inset(0);
  z-index: 460;
  filter: drop-shadow(0 0 9px rgba(231, 194, 122, 0.95));
  box-shadow:
    0 0 0 2px rgba(231, 194, 122, 0.95),
    0 18px 30px rgba(0, 0, 0, 0.66);
}

/*
 * 点起来、等着点目标区域的那张装备。
 *
 * 复古版是「点装备 → 点弃牌堆」，标准版是「点装备 → 点储蓄栏」，两版都只有
 * 这一个去处。第一步要让人看清「我点的是哪一张」，所以和上面选中储蓄栏那张牌
 * 同一套处理：放开裁切整张浮起 + 金边（`.zone-card` 的 `clip-path` 会把外阴影
 * 一起裁掉）。第二步由 `.is-drop-target` 把目标区点亮，见下面那一段。
 */
.zone-card.is-drop {
  --s: 1.45;
  clip-path: inset(0);
  z-index: 460;
  filter: drop-shadow(0 0 9px rgba(231, 194, 122, 0.95));
  box-shadow:
    0 0 0 2px rgba(231, 194, 122, 0.95),
    0 18px 30px rgba(0, 0, 0, 0.66);
}

/*
 * 「点装备 → 点目标区」的第二步：目标区自己亮起来。
 *
 * 复古版亮**弃牌堆**，标准版亮**储蓄栏**——两版都只有这一个去处，所以底栏不再
 * 摆「丢弃装备 / 明储」那一排常驻小按钮。以前标准版点装备会提示「再点弃牌堆」，
 * 顺着点下去装备就进了废牌堆；其实标准版该去的是明储。
 *
 * 用 `outline` 而不是 `border`：`.pile` 的边框已经被「空牌堆」占着，
 * `.shelf::after` 下沿也压着一条细线，outline 不跟这两处抢版面。
 */
.shelf.is-drop-target,
.pile.is-drop-target {
  outline: 2px solid rgba(231, 194, 122, 0.9);
  outline-offset: 6px;
  border-radius: 9px;
  animation: drop-target-glow 1.1s ease-in-out infinite;
}

/* 储蓄栏中间那个「明储」大字，这时候该看得见了。 */
.shelf.is-drop-target .shelf-tag {
  color: rgba(231, 194, 122, 0.5);
  transition: color 160ms;
}

@keyframes drop-target-glow {
  0%,
  100% {
    outline-color: rgba(231, 194, 122, 0.5);
    filter: drop-shadow(0 0 0 rgba(231, 194, 122, 0));
  }
  50% {
    outline-color: rgba(231, 194, 122, 1);
    filter: drop-shadow(0 0 14px rgba(231, 194, 122, 0.45));
  }
}

.card:focus-visible {
  box-shadow:
    0 0 0 2px var(--gold),
    0 18px 34px rgba(0, 0, 0, 0.7);
}

/* 侧席不再旋转；悬停和其它栏位一样放大到 2 倍，向桌心让一点避免贴边。 */
.side-card.left:hover,
.side-card.left:focus-visible {
  --hx: 45px;
  --hy: -10px;
  transform-origin: center top;
}

.side-card.right:hover,
.side-card.right:focus-visible {
  --hx: -45px;
  --hy: -10px;
  transform-origin: center top;
}

.stack-card {
  z-index: 70;
  /* 牌栈里每张牌都能点开看（`is-peek`），所以给个「可以点」的手型。 */
  cursor: pointer;
  /* 手机上双击牌栈别被浏览器当成「放大页面」——那会和「点一下放大」抢手势。 */
  touch-action: manipulation;
}

.stack-card.is-top {
  z-index: 90;
  box-shadow:
    0 12px 24px rgba(0, 0, 0, 0.65),
    0 0 0 1px rgba(231, 194, 122, 0.28);
}

/*
 * 点开的牌栈牌：原地放大到 2 倍，把牌面读清楚。
 *
 * 牌栈是一叠只错开 0.9 张牌宽的横排，不放大只看得到最上面那张，下面几张
 * 露出的是一条边。这里和手牌的「选中」是两套东西：**不发命令、不改变能打
 * 哪张**，纯查看。再点一下、点别处、按 Esc，或者这张牌离开牌栈就收起
 * （状态在 table.js 的 `stackPeek`，类名由 `cardClasses` 每次重算）。
 *
 * 缩放中心仍是 default 的 center center，所以放大后的牌以原位为中心向外长，
 * 不改变布局；z-index 压过其它栈牌与栈顶（500 / 560），但低于动作条（600），
 * 免得盖住要点的按钮。
 */
.stack-card.is-peek {
  --s: 2;
  z-index: 570;
  box-shadow:
    0 26px 48px rgba(0, 0, 0, 0.72),
    0 0 0 2px rgba(231, 194, 122, 0.9),
    0 0 24px rgba(231, 194, 122, 0.16);
}

/* ── 强化牌底下垫的素材 ───────────────────────────────────
 *
 * 它不是桌上一张独立的牌：合体期间视图只给 `defId`、没有实例 id，所以它是主牌
 * `.card` 的**子元素**（`table.js` 的 `syncUnderFaces` 挂上去），不参与布局、
 * 不接事件、也不算进「牌栈有几张」。拆出来之后才轮到它自己当 `.card`。
 *
 * 层叠：`.card` 有 `transform`、自成层叠上下文，内部顺序是「父背景 → 负 z-index
 * 子元素 → 牌面」。素材放在 0 这一档（牌面 `face-base` 是 1、描述层 2），于是
 * 牌面盖住它，只有向左错开出来的那一条露在牌外。
 *
 * 因为挂在牌上，主牌挪位、点开查看放大 2 倍、离场飞走都会带着它，不用另外跟。
 */
.under-stack {
  --under-step: 24px;
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}

.under-face {
  position: absolute;
  inset: 0;
  border-radius: 4px;
  overflow: hidden;
  /* 没卡图的素材也要有牌形：一张深色底比「什么都没有」诚实。 */
  background: #0b1411;
  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.5);
  /* 一张往左错开一档；`--u` 由 JS 按张数写（最底下那张最大，所以最靠左）。 */
  translate: calc(var(--u, 1) * var(--under-step) * -1) 0;
  /* 点开查看时整张让出来，这一下是滑过去的，不是跳过去的。 */
  transition: translate 160ms ease-out;
  /* 刚垫进来时从主牌底下滑出来：比角标闪一下更像「多了张牌」。 */
  animation: under-in 240ms ease-out;
}

/*
 * 点开查看时，素材牌整张让出来。
 *
 * 平时只露一道（够认出牌名）；点开时（栏位 hover / 牌栈点开查看）挪到主牌
 * 左边一整张牌宽的位置，两张牌并排摆着，素材牌面就完整了。
 *
 * 偏移**不除**放大倍数：素材牌是主牌的子元素，主牌放大 2 倍时它自己也放大
 * 2 倍，两张牌的中心距照样被放大 2 倍——正好和放大后的牌宽对上。除一次反而
 * 只挪半张，露出半张来。张数多于一张时按 `--u` 继续往外排。
 */
.zone-card:hover .under-face,
.zone-card:focus-visible .under-face,
.stack-card.is-peek .under-face {
  --under-step: calc(var(--cw) + 8px);
}

@keyframes under-in {
  from {
    translate: 0 0;
    opacity: 0.35;
  }
}

.stack-card.invalid {
  filter: grayscale(1) brightness(0.58) contrast(0.78);
  opacity: 0.55;
}

/*
 * 能打出的牌：**牌顶上方**一条光带在呼吸，牌面一点不染色。
 *
 * 位置在牌外（`top: -52px`），最亮的一行紧贴牌顶，往上渐暗——
 * 光是从牌顶边缘往外散出去的，不是往牌里渗的。
 *
 * 为什么不是绕四周的一圈：手牌压在舞台下沿（牌顶 826、牌高 147、舞台 900），
 * 露出来的只有上半截，左右两侧还常被相邻的牌压住（手牌间距只有 0.7 张牌宽）。
 * 而且牌面不透明，任何「中心最亮」的径向渐变，最亮的圆心都永远藏在牌底下，
 * 露在外面的只剩衰减后的尾巴（实测约 0.1），看着像根本没开灯。
 *
 * 渐变是**静态**的，每帧只改 `opacity`——光栅化一次，之后交给合成器。
 * 之前「光效一起来就卡」是给 `box-shadow` 打关键帧，每帧要重画三层大半径模糊。
 */
.card.is-playable:not(.is-enhance)::after,
.card.is-interrupt:not(.is-enhance)::after {
  content: "";
  position: absolute;
  top: -46px;
  left: 0;
  right: 0;
  height: 46px;
  z-index: 1;
  border-radius: 5px 5px 0 0;
  /*
   * 光色默认是**冷白**，不是绿色，也不是青。
   *
   * 两个坑都踩过：
   *   · 原来的薄荷绿和深绿桌布同一色相，手机上一缩就看不见了；
   *   · 改成饱和的青之后，桌面上又变成「狗荧光」——颜色太电。
   *
   * 所以默认值留在几乎不带色相的冷白：靠**明度差**而不是色相差去和桌布分开。
   * 颜色（`--aura-playable-rgb`）和峰顶（`--aura-playable-peak`）都能从
   * `anim-config.js` 的 `aura` 段改，见 `aura.js`；这里只留兜底值。
   */
  background: linear-gradient(
    to top,
    rgba(var(--aura-playable-rgb, 225, 245, 255), 0.92) 0%,
    rgba(var(--aura-playable-rgb, 225, 245, 255), 0.64) 18%,
    rgba(var(--aura-playable-rgb, 225, 245, 255), 0.33) 45%,
    rgba(var(--aura-playable-rgb, 225, 245, 255), 0.12) 72%,
    rgba(var(--aura-playable-rgb, 225, 245, 255), 0.04) 88%,
    rgba(var(--aura-playable-rgb, 225, 245, 255), 0) 100%
  );
  /* 左右两侧也收一下，否则是一块切得很齐的矩形，不像光 */
  -webkit-mask-image: linear-gradient(to right, transparent 0%, #000 13%, #000 87%, transparent 100%);
  mask-image: linear-gradient(to right, transparent 0%, #000 13%, #000 87%, transparent 100%);
  pointer-events: none;
  /* 周期与谷底都走 CSS 变量：牌桌和实验室读同一套（见 anim-config.js 的 aura 段） */
  opacity: var(--aura-playable-low, 0.26);
  /*
   * 这里**不能**再加 `will-change: opacity` 或 `transform: translateZ(0)`。
   *
   * 牌本身（父元素 `.card`）已经用 `translate3d` 建过合成层了，伪元素再套一层
   * 就变成「合成层里的合成层」：部分手机浏览器（iOS Safari、几个国产内核）
   * 不再为它重绘——动画数值在跑，屏幕上是死的，表现就是「手机上没有呼吸光」。
   * 性能上也不需要：光带只是面积很小的一条，每帧只改透明度。
   */
  animation: playableAura var(--aura-playable-dur, 2.6s) ease-in-out infinite;
}

/* 现在能打断 / 无效的牌：同样的顶边光带，换成橙色。 */
.card.is-interrupt:not(.is-enhance)::after {
  animation-name: interruptAura;
  animation-duration: var(--aura-interrupt-dur, 2.4s);
  opacity: var(--aura-interrupt-low, 0.34);
  background: linear-gradient(
    to top,
    rgba(var(--aura-interrupt-rgb, 255, 138, 40), 0.93) 0%,
    rgba(var(--aura-interrupt-rgb, 255, 138, 40), 0.66) 18%,
    rgba(var(--aura-interrupt-rgb, 255, 138, 40), 0.36) 45%,
    rgba(var(--aura-interrupt-rgb, 255, 138, 40), 0.13) 72%,
    rgba(var(--aura-interrupt-rgb, 255, 138, 40), 0.04) 88%,
    rgba(var(--aura-interrupt-rgb, 255, 138, 40), 0) 100%
  );
}

/*
 * 呼吸的谷底不从 0 起：0 会让这张牌有一段时间看起来像没亮。
 * 光带只有顶上一条，面积小，谷底就得比原来的一圈光再抬一点才稳得住。
 */
@keyframes playableAura {
  0%,
  100% {
    opacity: var(--aura-playable-low, 0.26);
  }
  50% {
    /* 峰顶也从配置来：调低它就是"更含蓄"的一套呼吸，不是另一个开关。 */
    opacity: var(--aura-playable-peak, 1);
  }
}

/*
 * 打断 / 无效响应的牌**不许灭到底**：这圈光也在说「这张现在能响应」，
 * 灭到 0 就有一段时间看起来像不能打。最暗留三成，剩下的靠明暗变化表达呼吸。
 */
@keyframes interruptAura {
  0%,
  100% {
    opacity: var(--aura-interrupt-low, 0.34);
  }
  50% {
    opacity: var(--aura-interrupt-peak, 1);
  }
}

/*
 * 触屏增强：手机上的牌只有桌面六成宽，顶边那道光带跟着缩到二十来像素，
 * 加上渐变本身就是往下衰减的，看起来就像根本没亮。这里把光带抬高、加高，
 * 谷底也抬到六成多——最暗的时候也一眼能看见。桌面维持原样：
 * 大牌上那道光已经够明显，再亮就过曝了。
 *
 * 用媒体查询而不是 JS 判断：这是纯样式，没必要等 `table.js` 加载完。
 */
@media (hover: none), (pointer: coarse) {
  .card.is-playable:not(.is-enhance)::after,
  .card.is-interrupt:not(.is-enhance)::after {
    top: -72px;
    height: 72px;
    --aura-playable-low: 0.72;
    --aura-interrupt-low: 0.78;
  }

  /* 「能打」那档整条光带换成一版**衰减更慢**的冷白：桌面那条只有顶上两成是亮的，
   * 缩到手机上就剩一根细线。打断牌的橙色渐变定义在别处，这里不能一起覆盖，
   * 否则两条光带会变成同一个颜色。 */
  .card.is-playable:not(.is-enhance)::after {
    background: linear-gradient(
      to top,
      rgba(var(--aura-playable-rgb, 225, 245, 255), 1) 0%,
      rgba(var(--aura-playable-rgb, 225, 245, 255), 0.85) 20%,
      rgba(var(--aura-playable-rgb, 225, 245, 255), 0.55) 48%,
      rgba(var(--aura-playable-rgb, 225, 245, 255), 0.3) 74%,
      rgba(var(--aura-playable-rgb, 225, 245, 255), 0.12) 90%,
      rgba(var(--aura-playable-rgb, 225, 245, 255), 0) 100%
    );
  }
}

.hand-card {
  z-index: 250;
  cursor: pointer;
  /* 触屏上双击默认会被浏览器当成「放大页面」，那正好和双击出牌抢同一组手势。
   * manipulation 只禁掉双击缩放和点击延迟，滚动、拖拽都不受影响。 */
  touch-action: manipulation;
  /*
   * 手牌的放大与上移是一整套动作（整排展开、选中抬起、素材压金边），
   * 比别的卡片给得长一点才看得出「抬起来」的过程；150ms 看起来像瞬跳。
   */
  transition:
    transform 210ms cubic-bezier(0.2, 0.75, 0.25, 1),
    box-shadow 180ms ease,
    filter 180ms ease,
    opacity 180ms ease;
}

/*
 * 悬停整排手牌：整副放大到 2 倍、整排上移，牌底贴住桌沿。
 *
 * 位置（x / y）由 table.js 按 2 倍重新算，这里只负责缩放。
 * 缩放中心用默认的 center center —— 布局公式按它推导：
 * 视觉左边缘 = --x - (s-1)*W/2，视觉底边 = --y + (s+1)*H/2。
 */
.hand-card.hand-open {
  --s: var(--hand-open-scale, 2);
}

.hand-card.is-hovered {
  z-index: 280;
  box-shadow:
    0 20px 34px rgba(0, 0, 0, 0.7),
    0 0 0 2px rgba(231, 194, 122, 0.9);
}

.hand-card.is-selected {
  z-index: 270;
  --s: 1.5;
  transform-origin: center bottom;
  box-shadow:
    0 20px 36px rgba(0, 0, 0, 0.72),
    0 0 0 2px rgba(139, 231, 179, 0.9),
    0 0 22px rgba(139, 231, 179, 0.18);
}

/* 展开时选中的牌也保持 2 倍，只由布局再抬一点。 */
.hand-card.hand-open.is-selected {
  --s: var(--hand-open-scale, 2);
  transform-origin: center center;
}

/* 选择请求（丢弃 / 获取等）里选中的手牌：和置换选中一样抬起来。 */
.hand-card.is-picked {
  z-index: 270;
  --s: 1.4;
  transform-origin: center bottom;
}

.hand-card.hand-open.is-picked {
  --s: var(--hand-open-scale, 2);
  transform-origin: center center;
}

/* 点选中的强化素材：整张抬起来，顶上再压一道金边，和「已选中」区分开。 */
.hand-card.is-enhance {
  z-index: 272;
  --s: 1.4;
  transform-origin: center bottom;
  box-shadow:
    0 20px 36px rgba(0, 0, 0, 0.72),
    0 0 0 3px rgba(231, 194, 122, 0.95),
    0 0 26px rgba(231, 194, 122, 0.45);
}

.hand-card.hand-open.is-enhance {
  --s: var(--hand-open-scale, 2);
  transform-origin: center center;
}

/*
 * 选中要弃掉的手牌：和「强化素材」同一套抬升 + 金边。
 * 两个选择不会同时出现（一个在出牌、一个在回答弃牌），所以共用这套视觉。
 */
.hand-card.is-doomed {
  z-index: 272;
  --s: 1.4;
  transform-origin: center bottom;
  box-shadow:
    0 20px 36px rgba(0, 0, 0, 0.72),
    0 0 0 3px rgba(231, 194, 122, 0.95),
    0 0 26px rgba(231, 194, 122, 0.45);
}

.hand-card.hand-open.is-doomed {
  --s: var(--hand-open-scale, 2);
  transform-origin: center center;
}

/*
 * 左下角的三个开关，竖着叠：强化预览 / 展开手牌 / 切换档位（2.0× ⇄ 2.8×）。
 *
 * 尺寸直接复用 `.bar-btn`——和右下角「结束回合」一模一样大（28px 字、16px 内边距），
 * 所以这里只补定位、排列和开合状态。贴舞台的**左下角**，和右下角那排对称。
 *
 * 竖排而不是横排：横排三颗要占掉近半条底边，展开手牌时正好压在最左边那张上；
 * 「强化预览」放最上面，它管的是牌面，另外两颗管的是摆牌。
 */
.hand-buttons {
  position: absolute;
  left: 14px;
  bottom: 14px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 8px;
  /* hud-layer 整体是 pointer-events: none，不显式打开就点不到 */
  pointer-events: auto;
  /* 手牌 545–548：按钮要压在牌上面，不然展开后被最左边那张挡住 */
  z-index: 550;
}

.hand-toggle,
.hand-zoom,
.hand-enh {
  white-space: nowrap;
}

/* 档位那颗窄一点：只写「2.0×」，不是一句话 */
.hand-zoom {
  padding: 16px 20px;
}

/* 开着的时候只把描边点亮，底色仍是 `.bar-btn` 原来那个深色。 */
.hand-toggle.is-on,
.hand-zoom.is-on,
.hand-enh.is-on {
  border-color: var(--gold);
  color: var(--ink);
}

/* ── 箭头 ───────────────────────────────────────────── */

.arrow-line {
  fill: none;
  stroke: rgba(236, 207, 150, 0.54);
  stroke-width: 8;
  stroke-linecap: round;
  stroke-dasharray: 10 14;
  filter: drop-shadow(0 0 5px rgba(236, 207, 150, 0.25));
  animation: dashFlow 2.8s linear infinite;
}

@keyframes dashFlow {
  to {
    stroke-dashoffset: -48;
  }
}

.arrow-head {
  fill: rgba(236, 207, 150, 0.62);
}

.target-ring {
  fill: none;
  stroke: rgba(139, 231, 179, 0.5);
  stroke-width: 3;
  stroke-dasharray: 5 8;
}

/* ── 动作盘 ─────────────────────────────────────────── */

.action-tray {
  position: absolute;
  left: 1218px;
  top: 770px;
  width: 364px;
  height: 92px;
  z-index: 400;
  padding: 10px 12px;
  border-radius: 12px;
  pointer-events: auto;
}

.action-title {
  color: var(--gold-soft);
  font-size: 9px;
  letter-spacing: 1.4px;
  margin-bottom: 3px;
}

.action-selected {
  color: var(--ink);
  font-size: 13px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.action-hint {
  color: rgba(220, 214, 194, 0.45);
  font-size: 10px;
  margin-top: 2px;
}

.action-buttons {
  display: flex;
  gap: 6px;
  margin-top: 7px;
}

.action-btn {
  padding: 4px 9px;
  border-radius: 6px;
  border: 1px solid rgba(231, 194, 122, 0.2);
  background: rgba(0, 0, 0, 0.18);
  color: var(--muted);
  font-size: 10px;
  cursor: pointer;
}

.action-btn.primary {
  border-color: var(--gold);
  background: linear-gradient(180deg, #e9c77d, #c99b4d);
  color: #25190a;
  font-weight: 600;
}

.action-btn.primary:disabled {
  opacity: 0.38;
  cursor: default;
}

.action-btn:hover:not(:disabled) {
  border-color: var(--gold);
  color: var(--ink);
}

.action-btn.primary:hover:not(:disabled) {
  color: #1a1005;
}

/* ── 设置弹层 ───────────────────────────────────────── */

.modal {
  position: absolute;
  inset: 0;
  z-index: 1000;
  display: grid;
  place-items: center;
  background: rgba(2, 6, 5, 0.62);
  backdrop-filter: blur(3px);
}

/*
 * 设置弹层：一颗 ⚙ 打开的那扇小窗，只有三个去向。
 *
 * 尺寸按原来的两倍做（380→760、字号 20→34），因为这里面全是「按错了就回不来」
 * 的按钮，宁可占屏幕中央一大块。三个按钮竖直排、等宽、文字居中——
 * 横向一字排开时「自刎」和「回到牌桌」挨得太近，容易点错。
 */
.modal-card {
  width: 760px;
  padding: 40px 44px;
  border-radius: 22px;
}

.modal-card h2 {
  margin: 0 0 26px;
  color: var(--gold);
  font-family: "Noto Serif SC", "Songti SC", "SimSun", serif;
  font-size: 34px;
  font-weight: 600;
  letter-spacing: 4px;
}

/* 音效开关：和下面那排大按钮同宽，但更轻——它是可选项，不是动作。 */
.sfx-settings {
  display: flex;
  flex-direction: column;
  gap: 14px;
  margin-bottom: 22px;
  padding: 16px 20px;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: rgba(0, 0, 0, 0.14);
}

.sfx-settings label {
  display: flex;
  align-items: center;
  gap: 14px;
  color: var(--muted);
  font-size: 17px;
  cursor: pointer;
}

.sfx-settings .sfx-toggle input {
  width: 18px;
  height: 18px;
  accent-color: var(--gold);
  cursor: pointer;
}

.sfx-settings input[type='range'] {
  flex: 1;
  min-width: 0;
  accent-color: var(--gold);
  cursor: pointer;
}

.modal-actions {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 14px;
}

.modal-actions button {
  padding: 16px 24px;
  border-radius: 12px;
  border: 1px solid rgba(231, 194, 122, 0.2);
  background: rgba(0, 0, 0, 0.18);
  color: var(--muted);
  font-size: 20px;
  text-align: center;
  cursor: pointer;
}

.modal-actions button.primary {
  border-color: var(--gold);
  background: var(--gold);
  color: #21170a;
  font-weight: 600;
}

.modal-actions button.danger {
  border-color: rgba(201, 90, 90, 0.5);
  color: #e49a9a;
}

.modal-actions button:hover {
  color: var(--ink);
}

.modal-actions button.primary:hover {
  color: #160e04;
}

/* ── 二次确认：设置先收起来，同一套卡片再窄一档 ─────────── */

/*
 * 结构完全复用 `.modal` / `.modal-card` / `.modal-actions`：玻璃底、金标题、
 * 竖排按钮都和设置窗一模一样。它只是「同一扇窗里接下来的那一句话」，
 * 而不是系统弹窗——点「自刎 / 退出对局」时先收起设置，确认完再决定去哪。
 */
.modal.confirm-layer {
  z-index: 1040;
}

.modal.confirm-layer .modal-card {
  width: 520px;
  padding: 32px 36px;
}

.confirm-text {
  margin: 0 0 26px;
  color: #d9d1c1;
  font-size: 17px;
  line-height: 1.75;
}

/* ── 弃牌台：弃牌堆上方的一条确认 ─────────────────────── */

/*
 * 这里刻意**不做全屏遮罩**。要不要弃这张牌，得看着手牌和桌面判断，
 * 把牌桌糊掉反而更难决定；所以它只占弃牌堆正上方那一小块，
 * 牌就摆在那儿（等于先放进弃牌堆让你看清楚），确认按钮挨着牌。
 *
 * 按钮的手感和底部「结束回合」那一排一致：金色实底 = 就这么办。
 */
#discard-tray {
  position: absolute;
  /* 正对弃牌堆的中心（1084 + 104/2），宽度按内容长，再居中回来 */
  left: 1136px;
  top: 118px;
  transform: translateX(-50%);
  width: max-content;
  max-width: 300px;
  z-index: 560;
  padding: 9px 11px 10px;
  border-radius: 12px;
  display: flex;
  flex-direction: column;
  gap: 7px;
  pointer-events: auto;
}

.tray-title {
  color: var(--gold);
  font-family: "Noto Serif SC", "Songti SC", "SimSun", serif;
  font-size: 12px;
  line-height: 1.4;
  letter-spacing: 0.6px;
}

.tray-cards {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  max-height: 230px;
  overflow: auto;
  padding: 1px;
}

/* 一张小牌：有素材就画牌面，没有素材就画名字块 */
.tray-card {
  position: relative;
  width: 78px;
  height: 110px;
  border-radius: 5px;
  overflow: hidden;
  flex: 0 0 auto;
  background: #050706;
  box-shadow:
    0 8px 18px rgba(0, 0, 0, 0.6),
    0 0 0 1px rgba(231, 194, 122, 0.22);
}

/* 托盘里的牌面：和挑牌窗口同一套小牌，只是再小一档（铺满 78×110 的格子）。 */
.tray-card .mini-face {
  width: 78px;
  height: 110px;
}

.tray-card.no-art {
  display: grid;
  place-items: center;
  padding: 6px;
  background: linear-gradient(160deg, #1d2b24, #0d1713);
}

.tray-name {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 14px 4px 4px;
  background: linear-gradient(180deg, transparent, rgba(0, 0, 0, 0.85));
  color: var(--ink);
  font-size: 10px;
  line-height: 1.25;
  text-align: center;
}

.tray-card.no-art .tray-name {
  position: static;
  padding: 0;
  background: none;
  font-size: 12px;
}

.tray-hint {
  color: var(--muted);
  font-size: 10px;
  line-height: 1.45;
}

.tray-actions {
  display: flex;
  justify-content: flex-end;
  gap: 6px;
}

.tray-actions button {
  padding: 8px 13px;
  border-radius: 9px;
  border: 1px solid rgba(231, 194, 122, 0.28);
  background: rgba(8, 20, 16, 0.78);
  color: rgba(243, 234, 216, 0.94);
  font-size: 13px;
  line-height: 1.1;
  cursor: pointer;
}

.tray-actions button:hover {
  border-color: var(--gold);
  color: var(--ink);
}

.tray-actions button.primary {
  border-color: var(--gold);
  background: linear-gradient(180deg, #e9c77d, #c99b4d);
  color: #25190a;
  font-weight: 600;
}

.tray-actions button.primary:hover {
  color: #160e04;
}

.tray-actions button:disabled {
  opacity: 0.42;
  cursor: default;
}

.tray-actions button:disabled:hover {
  border-color: rgba(231, 194, 122, 0.28);
  color: rgba(243, 234, 216, 0.94);
}

/* ── 托盘里的牌：点一下原地放大 ─────────────────────────── */

/*
 * 弃牌台（`#discard-tray`）里的牌只有 78×110，牌名和规则挤在小牌面里，
 * 一眼认不出是哪张——而它说的是「这张已经没了」，看错的代价比别处都大。
 * 点一下原地放大 2 倍（和牌栈的 `.is-peek` 同一套手感），再点一下、点别处、
 * 按 Esc 收起；状态在 table.js 的 `trayPeek`（`.seen-panel` 那边是 `seenPeek`，
 * 样式在 style.css）。
 *
 * `transform` 不参与排版，放大不会把邻居挤走。麻烦在溢出：`.tray-cards` 平时
 * 是 `overflow: auto`（牌多了要滚），放大那一半会被裁掉，所以放大期间 JS 会给
 * 它挂 `has-peek`，把溢出临时放出来——只有这一个容器是「看一张」的临时态。
 */
.tray-card {
  cursor: pointer;
  /* 手机上双击牌别被当成「放大页面」——那会和「点一下放大」抢手势。 */
  touch-action: manipulation;
  transition: transform 120ms ease-out;
}

.tray-card.is-peek {
  z-index: 6;
  transform: scale(2);
  box-shadow:
    0 18px 36px rgba(0, 0, 0, 0.75),
    0 0 0 2px rgba(231, 194, 122, 0.9);
}

.tray-cards.has-peek {
  overflow: visible;
}


/* ── 提示 ───────────────────────────────────────────── */

.toast {
  position: absolute;
  left: 50%;
  bottom: 24px;
  z-index: 1200;
  transform: translateX(-50%);
  padding: 8px 14px;
  border-radius: 9px;
  border: 1px solid rgba(231, 194, 122, 0.25);
  background: rgba(9, 21, 17, 0.88);
  color: var(--ink);
  font-size: 11px;
  box-shadow: 0 10px 26px rgba(0, 0, 0, 0.55);
  backdrop-filter: blur(8px);
}

#portrait-hint {
  display: none;
}

@media (orientation: portrait) and (max-width: 980px) {
  #portrait-hint {
    position: absolute;
    inset: 0;
    z-index: 2000;
    display: grid;
    place-items: center;
    background: rgba(4, 9, 7, 0.92);
    color: var(--gold);
    font-family: "Noto Serif SC", "Songti SC", "SimSun", serif;
    font-size: 20px;
    letter-spacing: 4px;
  }
}

@media (prefers-reduced-motion: reduce) {
  /*
   * 这里原来写的是「动画与过渡全关，改用常亮光」。
   *
   * 问题在于：**整套「现在能打哪张牌」的提示就是靠呼吸传达的**，关掉之后
   * 屏幕上看不出任何差别；悬停放大、选中的抬起也一起变成瞬移。
   * 系统开着「减少动画效果」的人（Windows 的辅助功能里一关就是全局）等于
   * 一直在玩一个没有动效的版本，而且不知道是被关掉了。
   *
   * 现在的策略：只压「突然的大幅运动」，周期性淡入淡出留着、周期放慢；
   * 悬停/选中的位移与缩放也保留，只把时长收短。
   */
  .card.is-playable:not(.is-enhance)::after,
  .card.is-interrupt:not(.is-enhance)::after {
    animation-duration: 3.4s;
  }

  .camp-band.active {
    animation-duration: 3.4s;
  }

  .hand-icon.targeted {
    animation-duration: 2.6s;
  }

  .arrow-line {
    animation-duration: 4.2s;
  }

  .card {
    transition-duration: 90ms;
  }
}

/* ── 图层顺序（从上到下）────────────────────────────────────
 *
 * 这一块是 z-index 的**唯一真源**：上面各处规则里的 z 值都是历史遗留，
 * 由这里统一覆盖。要调层序只改这一段，别再翻上面。
 *
 *   点开查看的栈牌 → 飞行中的牌 → 牌栈顶 → 生命珠 → 弹丸 → 牌栈其他牌 → 手牌
 *     → 栏位牌 → 角色图 → 回合色带 → 栏框 → 牌堆与废牌堆 → 阶段与读条 → 桌面
 *
 * 前提条件（少一条整套就散）：
 *   · `#hud-layer` / `#zone-layer` 必须 `z-index: auto`——它们原本各是一个
 *     层叠上下文，生命珠和牌堆会被打包成同一层，弹丸就没法插到两者中间；
 *   · `#card-layer` 本来就是 auto，牌自己带 z，直接参与比较；
 *   · 弹丸单独一层（`#bolt-layer`，table.js 里建），夹在生命珠和牌栈之间。
 */

#zone-layer,
#hud-layer {
  z-index: auto;
}

/* 底子 */
#table-surface {
  z-index: 10;
}
#stage::before {
  z-index: 20;
}

/* 阶段与读条：压在所有牌下面，手牌放大到 2 倍时也不会挡住正在挑的那张 */
.turn-strip,
.phase-rail {
  z-index: 240;
}

/* 牌堆 / 废牌堆：座位那一层的兄弟，但比栏位牌低 */
.pile {
  z-index: 280;
}

/* 栏位 */
.shelf {
  z-index: 300;
}
.zone-card {
  z-index: 320;
}
/* 暗储和明储排在同一栏、同一种牌：层级也必须一样。
 * （style.css 里那条 `card-back: 55` 已去掉——那是把牌背当背景层的老值，
 * 留着会让暗储被同栏明储压住，悬停也抬不起来。） */
.zone-card.card-back {
  z-index: 320;
}
.zone-card:hover,
.zone-card:focus-visible {
  z-index: 440;
}
/*
 * 自己区域的栏位牌点开查看：抬到手牌（545–548）之上。
 *
 * 玩家点进去就是要读牌面，这时候被自己那排手牌压掉下面一截没有道理。
 * 但仍低于动作条（600）/ 弃牌盘（590）——那两块是要点的，任何时候都得在最上面。
 */
.zone-card.own-zone:hover,
.zone-card.own-zone:focus-visible {
  z-index: 556;
}
#arrow-layer {
  z-index: 340;
}

/* 手牌：整排压在生命珠（540）之上。
 *
 * 展开到 2.8 倍时最左边那几张会伸到座位那边去，压住生命珠看着像被削掉一块，
 * 所以手牌这一档整体抬到座位之上——牌是正在操作的东西，该在前面。
 * 弹丸（520）、牌栈（500）也随之落到手牌之下，这点是有意的：
 * 手牌永远在最外层，不会被谁盖住。
 *
 * 手牌内部按「已经做过的操作 > 指针临时指着的那张」排：
 * 挑牌（弃牌 / 置换）时鼠标从选中的牌划到下一张，若让 hover 压过已选中的那张，
 * 看起来就是「选中的牌掉到了邻居下面」——选中是结果，不该被指针位置盖掉。
 * 长按看全文、拖拽中属于「正在单独操作这一张」，比选中更靠上。
 */
.hand-card {
  z-index: 545;
}
.hand-card.is-hovered {
  z-index: 546;
}
.hand-card.is-selected,
.hand-card.is-picked {
  z-index: 547;
}
.hand-card.is-enhance,
.hand-card.is-doomed {
  z-index: 548;
}
.hand-card.is-held {
  z-index: 549;
}
.card.is-dragging {
  z-index: 550;
}

/* 牌栈：普通帧 500，正在结算的那张 560（比生命珠还高）；
 * 点开查看的那张 570——盖住栈里其它牌和栈顶，但仍低于动作条（600）。 */
.stack-card {
  z-index: 500;
}
.stack-card.is-top {
  z-index: 560;
}
.stack-card.is-peek {
  z-index: 570;
}

/* 弹丸：从栈顶那张牌**底下**射出去，越过栈里其他牌，钻进生命珠底下 */
#bolt-layer {
  z-index: 520;
}

/* 回合色带：仅比三栏（`.shelf` 300）高——它是背景提示，不是前景。
 * 生命珠仍然压在弹丸之上；角色图（318）压在它上面：呼吸光是「桌上的氛围」，
 * 亮起来的时候不该从角色立绘脸上横着扫过去。 */
.camp-band {
  z-index: 310;
}
.camp-band.active {
  z-index: 315;
}
/*
 * 座位 HUD **不再是层叠上下文**：角色图要和栏位摆在同一层（见 `.role-card`
 * 的 z-index），而父级一旦带 z-index，子元素就再也出不了这一层。
 * 所以拆开写：生命珠、名字各自压到 540 那一档，角色图自己留在 310。
 *
 * 生命珠必须在 540（弹丸 520 要钻到它**底下**），名字跟着珠子走，
 * 否则侧座那 36px 与栏位重叠的地方，名字会被栏位牌（320）盖掉。
 */
.seat-hud {
  z-index: auto;
}
.seat-hud .seat-name {
  z-index: 539;
}
.seat-hud .life {
  z-index: 540;
}

/* 界面 chrome。
 *
 * 动作条和弃牌盘必须压在手牌（545–548）之上：手牌抬到生命珠之上以后，
 * 2.8 倍展开会一路铺到右下角，原来 450 的层级会让「结束回合」被整排牌埋掉。
 * 这两块是**要点的东西**，任何时候都该在最上面。 */
#action-bar {
  z-index: 600;
}
#discard-tray {
  z-index: 590;
}
#topbar {
  z-index: 800;
}

/* ── 实验牌局面板 ──────────────────────────────────────
 *
 * 挂在 body 上（大厅和牌桌都要能开），所以 `.modal` 的 absolute 得改成 fixed，
 * 否则会相对 #stage 定位、躲进 hidden 的 #viewport 里。
 *
 * z-index 必须压过大厅（`#screens` 是 1500）：大厅里点「实验：指定起手」时，
 * 面板原来只有 1200，开在大厅背后——看起来就是「点了没反应」。
 * 内容比设置弹层碎，窗口收窄一点、按钮横排。 */
#lab-modal {
  position: fixed;
  z-index: 1600;
}
#lab-modal .modal-card {
  width: 560px;
  padding: 28px 32px;
}
#lab-modal h2 {
  margin-bottom: 12px;
  font-size: 26px;
  letter-spacing: 3px;
}
.lab-hint {
  margin: 0;
  font-size: 13px;
  line-height: 1.6;
}
.lab-hint code {
  padding: 1px 5px;
  border-radius: 4px;
  background: rgba(0, 0, 0, 0.28);
  color: var(--gold);
  font-size: 12px;
}
.lab-rows {
  display: grid;
  gap: 9px;
  margin: 18px 0 6px;
}
.lab-row {
  display: grid;
  grid-template-columns: 7.5em 1fr;
  align-items: center;
  gap: 12px;
}
.lab-name {
  color: var(--muted);
  font-size: 14px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.lab-input {
  padding: 9px 11px;
  border-radius: 8px;
  border: 1px solid rgba(231, 194, 122, 0.24);
  background: rgba(0, 0, 0, 0.26);
  color: var(--text);
  font-family: inherit;
  font-size: 15px;
  /* 牌序号是等宽的，多张牌一眼能数清 */
  font-variant-numeric: tabular-nums;
}
.lab-input:focus {
  outline: none;
  border-color: var(--gold);
}
#lab-modal .modal-actions {
  flex-direction: row;
  margin-top: 16px;
}
#lab-modal .modal-actions button {
  flex: 1;
  padding: 12px 16px;
  font-size: 16px;
}
#lab-error {
  min-height: 1.2em;
  font-size: 13px;
}

/* 大厅里的入口：不抢「开始对局」的视觉重量。 */
.lab-open {
  margin-top: 12px;
  width: 100%;
}
