/* 掼蛋本地联调界面 —— 纯 CSS，无依赖。
   视觉参照真实产品素材：牌桌墨绿、卡牌白底、
   点数+花色固定在左上角竖排（这样叠牌时才看得见）。 */

:root {
  --felt-1: #1a6b45;
  --felt-2: #0a3524;
  --ink: #eef5f1;
  --ink-dim: #9fb8ac;
  --gold: #f0c85c;
  /* 牌圈尺寸的兜底值；手牌实际用 JS 按 HAND_BASE_CARD_W/H 写进 #hand 的内联变量 */
  --card-w: 56px;
  --card-h: 80px;
  --radius: 8px;
}

* {
  box-sizing: border-box;
  -webkit-tap-highlight-color: transparent;
}

html,
body {
  margin: 0;
  height: 100%;
  overflow: hidden;
  font-family: system-ui, -apple-system, 'Segoe UI', 'Microsoft YaHei', sans-serif;
  color: var(--ink);
  background: radial-gradient(circle at 50% 35%, var(--felt-1), var(--felt-2) 72%);
  user-select: none;
}

/*
 * 高度与安全区。
 *
 * height: 100dvh —— dvh 跟着**可见区域**走。移动端工具栏收起/展开时不会留灰边；
 * 100% 在某些 webview 里是按较大的 layout viewport 解析的，就会露出一条空白。
 * 不支持 dvh 的浏览器会自动退回上面那条 height: 100%。
 *
 * padding 用 env(safe-area-inset-*) —— viewport meta 里已经写了 viewport-fit=cover，
 * env() 才会生效。普通标签页里这些值都是 0，对现有布局零影响；
 * 一旦「添加到主屏幕」（standalone，浏览器 UI 全没了），顶部状态栏/刘海和
 * 底部 Home 指示条就会压到内容上，必须靠这层 padding 让开。
 *
 * ⚠️ **底部的 Home 指示条（那条白色加粗横线）不预留**（产品明确要求）：
 *   · iOS 是把这条线**画在内容之上**的，它本身不遮挡可读信息
 *   · 预留出来反而把整条底栏往上顶，牌桌少一截
 * 所以底部写 0，内容一直铺到屏幕最下沿；左右（横屏时是刘海那一侧）照旧让开。
 */
body {
  height: 100dvh;
  padding: env(safe-area-inset-top) env(safe-area-inset-right) 0 env(safe-area-inset-left);
}

/*
 * 三段式：牌桌（吃掉剩余高度）/ 底部（手牌一行 + 按钮一行）。
 *
 * **顶栏那一行取消了** —— 高度全给牌桌，玩家3 的头像才能贴到应用最上沿。
 * 「本局打几」和「重开」改成浮在牌桌左上/右上角（.table-topbar）。
 */
#app {
  display: grid;
  grid-template-rows: 1fr auto;
  height: 100%;
  width: 100%;
}

/* ───────────── 浮在牌桌上的顶栏 ───────────── */

.table-topbar {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 2px 40px;
  font-size: 14px;
  /* 浮层：不吃布局高度，手指也能点到下面的牌 */
  pointer-events: none;
  z-index: 4;
}
.table-topbar .btn {
  pointer-events: auto;
}
.tt-left,
.tt-right {
  display: flex;
  align-items: center;
  gap: 6px;
}

.tb-group {
  display: flex;
  align-items: center;
  gap: 6px;
}
.tb-group.right {
  margin-left: auto;
  gap: 8px;
}

.tb-label {
  color: var(--ink-dim);
}
.tb-value {
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.tb-value.level {
  color: var(--gold);
  font-size: 17px;
}
.tb-value.me {
  color: #7fd1ff;
}

/* ───────────── 档位牌：我方 / 对方（样式照产品给的截图） ───────────── */

.level-board {
  display: flex;
  gap: 8px;
  align-items: stretch;
  pointer-events: none;
}
.level-board .lb-box {
  position: relative;
  min-width: 52px;
  padding: 3px 10px 4px;
  border-radius: 6px;
  border: 2px solid rgba(255, 255, 255, 0.28);
  background: linear-gradient(180deg, rgba(28, 58, 46, 0.94), rgba(18, 42, 33, 0.94));
  text-align: center;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.35);
  opacity: 0.82;
}
.level-board .lb-name {
  font-size: 13px;
  font-weight: 700;
  color: #e8f3ec;
  line-height: 1.15;
}
/* 名字下面那条分隔线（截图里也有） */
.level-board .lb-name::after {
  content: '';
  display: block;
  margin: 2px 0 1px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.35);
}
.level-board .lb-value {
  font-size: 20px;
  font-weight: 800;
  line-height: 1.05;
  color: #ffffff;
  font-variant-numeric: tabular-nums;
}
/* 当前这一局打的是哪一方：金边 + 金三角，另一块压暗 */
.level-board .lb-box.active {
  border-color: var(--gold);
  opacity: 1;
  box-shadow: 0 0 0 1px rgba(240, 200, 92, 0.55), 0 2px 8px rgba(0, 0, 0, 0.45);
}
.level-board .lb-box.active .lb-name {
  color: var(--gold);
}
.level-board .lb-box .lb-caret {
  display: none;
}
.level-board .lb-box.active .lb-caret {
  display: block;
  position: absolute;
  left: 50%;
  bottom: -9px;
  width: 0;
  height: 0;
  margin-left: -6px;
  border-left: 6px solid transparent;
  border-right: 6px solid transparent;
  border-top: 7px solid var(--gold);
}

.team {
  padding: 2px 40px; /* 第六次：产品改为 30px */
  border-radius: 999px;
  font-size: 12px;
  background: rgba(255, 255, 255, 0.08);
}
.team.t0 b {
  color: #ffd479;
}
.team.t1 b {
  color: #8fd0ff;
}

.tag {
  font-size: 11px;
  padding: 2px 7px;
  border-radius: 999px;
  background: rgba(240, 200, 92, 0.18);
  color: var(--gold);
}
/* 前端版本号：用来确认页面是不是最新版（不对就 Ctrl+F5） */
.tag.ver {
  font-family: ui-monospace, Consolas, monospace;
  background: rgba(255, 255, 255, 0.1);
  color: var(--ink-dim);
}

/* ───────────── 牌桌 ───────────── */

/*
 * 牌桌。
 *
 * **横向留白栏**：左右各 210px 给玩家4/玩家2 的头像 + 出的牌。
 *
 * **纵向没有留白了**：玩家3（上家）的头像"直接到顶"——
 * 用绝对定位挂在牌桌顶边，不再单独占一行。原来上边那一行会在
 * 头像上方留出一大块空白，看着像牌桌没铺满。玩家3 出的牌紧贴头像下方。
 *
 * 玩家1（自己）的头像和手牌在底部功能区，见 #bottom 那一节。
 */
#table {
  position: relative;
  display: grid;
  grid-template-columns: 210px 1fr 210px;
  grid-template-areas: 'left center right';
  /* 横向不留内边距：左右两家的头像要贴到**应用左右边 2px**，
     留了内边距就得把头像的偏移反过来扣，容易算错 */
  padding: 0 0 6px;
  min-height: 0;
  overflow: hidden;
  /*
   * 左右两家整体的纵向落点（负值 = 上移），**相对牌桌高度**。
   *
   * 定义在 #table 而不是 .pos-left/.pos-right 上：因为「我的出牌区」
   * （#center 里的 #myPlay）也要对到同一条线上（产品要求：
   * "玩家1的等待时间/出的牌上移到和玩家2、玩家3头像一条线"）。
   * 放在共同祖先上，改这一个值，左右两家和我的出牌区会一起动、不会错位。
   */
  --seat-drop: -20px;
}

/*
 * 玩家3：头像贴到**应用最上沿**，距离 2px（产品明确要求），
 * 并且略微偏右一点放（不居中）—— 居中的话会和牌桌中央的东西挤在一起，
 * 太靠左又会离出牌区太远。
 *
 * 头像 + 名字张数 横排成一行，出的牌排在**头像右侧**。
 *
 * left 的值调过两轮：33% → 40%（产品反馈"头像右移一点"）。
 * 注意这是**整组**（头像 + 出的牌）的中心，不是头像中心 ——
 * 牌在头像右侧，所以头像会落在整组中心的左边。想再右移只改这一个数。
 */
.seat.pos-top {
  position: absolute;
  top: 2px;
  left: 40%;
  transform: translateX(-50%);
  z-index: 3;
  flex-direction: row;
  align-items: flex-start;
  gap: 6px;
}
.seat.pos-top .seat-head {
  order: 1;
}
.seat.pos-top .seat-play {
  order: 2;
  margin-top: 6px;
}

/*
 * 座位容器与内部结构（三块各自独立摆放，不用 order 反复绕）：
 *
 *   .seat         flex 容器，方向按座位不同
 *     .seat-avatar  头像
 *     .seat-play    这家出的牌 / 「不出」
 *     .seat-meta    名字 + 张数 + 名次（小字）
 *
 * 摆牌位置（产品的明确要求，注意别再翻）：
 *   玩家3（上边）   → 牌在头像**下方**，名字张数在更下面
 *   玩家4（左边）   → 牌在头像**右边**
 *   玩家2（右边）   → 牌在头像**左边**
 *   玩家1（自己）   → 头像是底栏里独立的一列（见 #bottom），
 *                     摆在按钮右侧、贴底，不会被手牌压住
 *
 * 左右两家的名字/张数改成绝对定位挂在头像下方，
 * 否则它会参与横向排布、把牌的位置关系搞乱。
 */
.seat {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 0;
}
.pos-left {
  grid-area: left;
}
.pos-right {
  grid-area: right;
}

/* 玩家3 的具体排布见上面 .seat.pos-top 那一段（横排：头像在左、出的牌在右） */

/*
 * 玩家4（左边）和玩家2（右边）。
 *
 * 位置照素材：名字/张数在头像正下方，出的牌朝桌面中心排：
 *   玩家4（左边） → 牌在头像**右边**
 *   玩家2（右边） → 牌在头像**左边**
 *
 * **头像贴到应用左右边各 2px**（产品要求）：用绝对定位精确贴边，
 * 而不是靠 flex 居中 —— 居中时头像横向位置随列宽浮动，落不到 2px 上。
 * 纵向放在牌桌中线附近（top: 36%），照素材。
 */
.pos-left,
.pos-right {
  flex-direction: row;
  align-items: center;
  /*
   * 出牌区必须朝着**桌面中心**生长，绝不能长到头像底下。
   *
   * 头像已经绝对定位贴边了，所以"挨着头像内侧"这件事只能靠对齐方式保证：
   *   玩家4（左边）→ 出牌区靠**左**对齐（margin-left 让开头像），向右长 ✓
   *   玩家2（右边）→ 出牌区必须靠**右**对齐（margin-right 让开头像），向左长 ✓
   *
   * ⚠️ 这里原来两家都写了 flex-start。对右边那家来说，出牌区会从座位**左边缘**
   * 开始往右排 —— 牌一多就一直长到贴右边固定的头像底下，把头像整个盖住。
   * 产品反馈的"手机上玩家2的头像被牌遮挡"就是这个。见下面的 .pos-right 覆盖。
   */
  justify-content: flex-start;
  gap: 6px;
  /*
   * 左右两家整体下移量（负值 = 上移）。头像块和出牌区**共用同一个值**，
   * 这样"两者同一排"这个关系不会因为调这个值而被破坏。
   *
   * 调过三轮：0 → 16 → 0 → -20（产品反馈"头像上移"）。
   * 想再微调只改这一个数，头像和出牌区会一起动、不会错位。
   * （值本身统一在 #table 上定义，见那里的说明。）
   */
}
/* 玩家2（右边）：靠右对齐，出牌区向左生长，永远不会压到头像 */
.pos-right {
  justify-content: flex-end;
}
.pos-left .seat-head,
.pos-right .seat-head {
  position: absolute;
  /*
   * 头像块必须和出牌区**共用一条水平中线**。
   *
   * 出牌区是 flex 子项，被容器的 align-items: center 摆在 50%；
   * 头像块是绝对定位，所以这里要显式写 50%。
   *
   * 原来是 36%：实测头像块中心在 99px、出牌区中心在 138px，
   * **差了 39px** —— 看起来就是"牌挂在头像下面"，不是并排。
   * 再加上 --seat-drop 满足"整体下移一点"。
   */
  top: calc(50% + var(--seat-drop, 0px));
  transform: translateY(-50%);
}
.pos-left .seat-head {
  left: 31px; /* 第六次：产品改为 30px（4 网格 + 21 + 4.6 内缩 ≈ 29.6） */
}
.pos-right .seat-head {
  right: 31px; /* 第六次：产品改为 30px（同左） */
}
.pos-left .seat-meta {
  align-items: flex-start;
}
.pos-right .seat-meta {
  align-items: flex-end;
}
/*
 * 出的牌排在头像内侧（朝桌面中心那一侧）。
 * 头像宽 52px + 左边距 2px + 一点缝隙 = 64px 起排；
 * 玩家2 从右边算同样的距离。
 */
.pos-left .seat-play,
.pos-right .seat-play {
  /* 跟着头像块一起下移，保证两者始终同一排 */
  position: relative;
  top: var(--seat-drop, 0px);
}
.pos-left .seat-play {
  order: 1;
  margin-left: 64px;
}
.pos-right .seat-play {
  order: 2;
  margin-right: 64px;
}

.seat-meta {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1px;
}

/*
 * 头像 + 名字张数 这一组。
 * 左右两家要让"这一组"垂直居中，出的牌再排到它外侧，
 * 所以需要一个包裹元素把头像和文字绑在一起。
 */
.seat-head {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
}

/*
 * 头像 + 徽标的定位容器。
 *
 * 徽标必须相对**头像**定位，不能相对 .seat-head —— 后者是「头像 + 名字张数」
 * 整块，四个座位的宽度和 flex 方向都不一样（我这一家在横屏下是横排的），
 * 挂在 head 上徽标会落到文字角上（实测头像 x=10..44，徽标却跑到 x=79）。
 * 也不能放进 .avatar：那里有 overflow: hidden，徽标会被裁掉。
 */
.av-wrap {
  position: relative;
  display: inline-flex;
  flex: none;
}

/*
 * 剩余张数徽标（挂在头像角上的数字）。
 *
 * 样式照产品给的参考素材：**深蓝底 + 亮青描边的圆角方块 + 白色粗体数字**，
 * 挂在头像的**右上角**。
 *
 * 只在剩 ≤10 张时出现（判定见 remain.js / client.js 的 REMAIN_BADGE_MAX）：
 * 张数多的时候显示没意义，快打完时才需要盯着 —— 参考素材里也是这样，
 * 只有剩 6、7 张的那两家挂了数字，剩得多的没有。
 *
 * 两个座位要镜像到左上角（都是实测出来的，不是随手加的）：
 *   · 玩家2（右边）头像贴着屏幕右缘 → 挂右上角会出屏
 *   · 玩家1（我）横屏下是「头像 + 名字」横排 → 挂右上角会压住名字（截图可见）
 * 玩家3（上边）头像顶到 y=2 → top:-7 会跑到屏幕外，改成 top:0
 */
.seat-remain {
  position: absolute;
  top: -7px;
  right: -8px;
  min-width: 22px;
  height: 22px;
  padding: 0 4px;
  border-radius: 6px;
  background: linear-gradient(180deg, #2f66c4, #16336e);
  border: 2px solid #6fd0ff;
  color: #fff;
  font-size: 13px;
  font-weight: 800;
  line-height: 18px; /* 22 - 上下边框各 2 */
  text-align: center;
  font-variant-numeric: tabular-nums;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.5);
  z-index: 3;
}
.pos-right .seat-remain,
.pos-bottom .seat-remain {
  right: auto;
  left: -8px;
}
/* 上家的头像顶到 y=2，徽标再往上就出屏了 */
.pos-top .seat-remain {
  top: 0;
}

.seat .avatar {
  /*
   * 统一 **50 × 50**，四家（含我自己）一律这个尺寸，**不区分宽屏 / 窄屏**。
   *
   * 原来的取值是宽屏 52、窄屏 36、我自己的 44 —— 同一张牌桌上三种大小，
   * 看着不齐；而且"我的头像"比别家小一圈也没有理由。现在归一。
   */
  width: 50px;
  height: 50px;
  /* 圆角方形（照素材那种"卡片头"），不要正圆 */
  border-radius: 10px;
  display: grid;
  place-items: center;
  font-weight: 800;
  font-size: 22px;
  line-height: 1;
  background: rgba(255, 255, 255, 0.14);
  border: 2px solid transparent;
  transition: border-color 0.15s, box-shadow 0.15s;
  font-variant-numeric: tabular-nums;
  /* 头像图片：铺满整个方块；图片没加载出来时这里就是空的，
     底下那层背景色和文字（如果有）仍然看得到 */
  overflow: hidden;
}
.seat .avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  user-select: none;
  -webkit-user-drag: none;
}
/* 电脑座位：数字放大、底色更实，一眼能看出是几号电脑 */
.seat .avatar.npc {
  background: rgba(10, 40, 28, 0.55);
  border-color: rgba(255, 255, 255, 0.22);
  color: #d8f0e2;
}
.seat.team0 .avatar.npc {
  border-color: rgba(255, 212, 121, 0.45);
}
.seat.team1 .avatar.npc {
  border-color: rgba(143, 208, 255, 0.45);
}
.seat.turn .avatar {
  border-color: var(--gold);
  box-shadow: 0 0 0 4px rgba(240, 200, 92, 0.25);
  animation: pulse 1.1s ease-in-out infinite;
}
@keyframes pulse {
  50% {
    box-shadow: 0 0 0 10px rgba(240, 200, 92, 0.05);
  }
}
/*
 * 当前出牌方：头像加金色描边并呼吸闪烁。
 * 左右两家的头像在牌桌中部、上家的在顶部，都靠这个高亮来指出"轮到谁"。
 * 徽标只换成金边（不能换成金色底 —— 那是深色底白字，换了就读不清了）。
 */
.seat.turn .seat-remain {
  border-color: var(--gold);
  box-shadow: 0 0 0 2px rgba(240, 200, 92, 0.3), 0 2px 6px rgba(0, 0, 0, 0.5);
}
/* 名次/托管等小字在头像上方时不要被裁掉 */
.seat-head {
  overflow: visible;
}

.seat.team0 .avatar {
  background: rgba(255, 212, 121, 0.22);
}
.seat.team1 .avatar {
  background: rgba(143, 208, 255, 0.22);
}
.seat .sname {
  font-size: 12px;
  max-width: 128px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.seat .sname .bot {
  color: var(--ink-dim);
  font-size: 10px;
}
/*
 * 头像下面的那一行：以前是「剩 X 张」，现在换成**金豆**（产品要求）。
 * 样式照参考素材：深色胶囊 + 金色数字。
 *
 * ⚠️ 金豆系统**还没做**，服务端视图里没有这个字段，所以现在恒为 0（占位）。
 * 等规则定了，服务端在 seats 里补一个 beans 字段即可，这里不用改。
 */
.seat .sbeans {
  font-size: 10px;
  font-weight: 700;
  color: #ffe6a3;
  background: rgba(0, 0, 0, 0.42);
  border-radius: 999px;
  padding: 1px 8px;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.seat .srank {
  font-size: 11px;
  color: var(--gold);
  font-weight: 700;
}

/*
 * 名次标签（第一/第二/第三/第四）。
 *
 * 摆在**出牌区里、牌的左边**（DOM 里在 .seat-cards 之前，.seat-play 是 flex 行）。
 * 产品要求：出完牌之后名次显示在这里，不要缩在头像下面那行小字里。
 *
 * 金色底 + 深色字：和「金豆」那类信息区分开，一眼能看出是"走完了"的标记。
 * 牌被清掉之后它仍然留着（服务端下家一出牌就清上家的牌），
 * 所以这个标签必须能独立站着好看。
 */
.seat-rank {
  font-size: 12px;
  font-weight: 800;
  color: #3a2a05;
  background: linear-gradient(180deg, #ffe9a8, #f0c85c);
  border-radius: 6px;
  /* 还原 8px：之前被批量替换误伤成 40px（产品反馈"上游词条太长"） */
  padding: 2px 8px;
  white-space: nowrap;
  flex: none;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.45);
}
.seat.finished .avatar {
  opacity: 0.4;
}

/*
 * 中间：只留「轮到谁」的提示 + 我自己出的牌。
 *
 * 我自己出的牌放这里（不放自己面前）：自己面前那块已经被手牌占了，
 * 摆在中间才看得清自己刚出了什么。
 */
#center {
  grid-area: center;
  /*
   * ⚠️ 这里改成「绝对定位容器」了，不再用 flex 居中。
   *
   * 产品要求：**我的等待时间（倒计时圆环）和我出的牌，上移到和玩家2、玩家3
   * 头像同一条线**。原来这块是 flex 垂直居中的，圆环落在 y 158..212 ——
   * 比左右头像（114..150）低一截，而且**压在牌上**（手牌顶边 192）。
   *
   * 现在把提示文字和出牌区都按「左右头像的那条线」绝对定位：
   *   头像块的纵向中心 = 50% + --seat-drop（见 .pos-left .seat-head）
   *   头像**图片**高 36px、在头像块顶部，所以图片中心 = 那个值再往上 16.5px
   * 用同一个表达式，窗口高度或 --seat-drop 变化时两者始终对齐。
   */
  position: relative;
  min-height: 0;
}

/*
 * 我的倒计时 / 我出的牌：**中心与左右头像图片的中心对齐**。
 *
 * #myPlay 高 60（倒计时圆环高 54，正好居中），所以 top 要再减掉一半高度 30：
 *   50% + --seat-drop - 16.5 - 30 = 50% + --seat-drop - 46.5
 * 实测（视口 874×402）：top = 101.5，圆环 105..159、中心 132 ——
 * 和左右头像图片（114..150、中心 132）**完全同一条线**，
 * 而且离手牌顶边（166+）还有余量，不再压牌。
 *
 * ⚠️ `z-index: 20` 不能省。这里用了 `transform: translateX(-50%)` 做水平居中，
 * 而 **transform 会创建层叠上下文** —— 圆环自己的 z-index:20 就被关在
 * #myPlay 内部了；#myPlay 是 z-index:auto，会落到手牌（z-index 1~5）**下面**，
 * 于是手牌把倒计时盖住。实测症状：强制造出重叠后，
 * "圆环可见/隐藏"两张截图**一个像素都不差**（圆环压根没画上去）。
 * 给 #myPlay 自己一个 z-index，让它进到 #app 的层叠上下文里比。
 */
#myPlay {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  top: calc(50% + var(--seat-drop, 0px) - 46.5px);
  z-index: 20;
  height: 60px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-end;
}
#myPlay .cards {
  display: flex;
  align-items: flex-end;
}
#myPlay.hidden {
  display: none;
}

/* 提示文字：压在出牌区正上方（间距 10px，和原来的 gap 一致）。
 * 同样因为 transform 而自成层叠上下文，需要一个 z-index（见上）。 */
#centerHint {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  top: calc(50% + var(--seat-drop, 0px) - 74.5px);
  z-index: 21;
  white-space: nowrap;
}

/* 大号「不出」（参考素材里的金色大字） */
.pass-badge {
  font-size: 30px;
  font-weight: 800;
  color: var(--gold);
  letter-spacing: 3px;
  text-shadow: 0 3px 10px rgba(0, 0, 0, 0.6);
  animation: passIn 0.22s ease-out;
}
@keyframes passIn {
  from {
    transform: scale(0.7);
    opacity: 0;
  }
}

.hint {
  font-size: 14px;
  color: var(--gold);
  min-height: 18px;
  text-align: center;
}

/* ───────────── 各座位面前摆的牌 ───────────── */

/*
 * 为什么摆在各自座位前：四家的牌都堆到中央时，很难分辨哪手是谁出的。
 * 放在自己面前，位置本身就说明了归属。
 *
 * 牌统一**下方对齐**（align-items: flex-end），不要上方对齐 ——
 * 上对齐会让牌贴在头像顶部，跟头像之间留一大截空白，看着散。
 */
.seat-play {
  min-height: 44px;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  /* 名次标签和牌之间留一点缝（标签是 .seat-play 的直接子元素，牌在 .seat-cards 里） */
  gap: 5px;
}
.seat-cards {
  display: flex;
  align-items: flex-end;
  justify-content: center;
}
/*
 * 出牌区里摆的牌（座位旁那张、以及屏幕中间我出的牌）。
 *
 * 产品指定的最终尺寸：**宽 35、高 50**（35/50 = 0.7，和底牌 56/80 同一个比例）。
 * 字号不变（数字 16px、花色 15px），花色在数字**下面**且两者居中对齐。
 *
 * ⚠️ 三件事是**联动的**，改一个必须重算另两个：
 *   ① 数字距牌左边 2px（= 1px 边框 + .idx 的 left 1px，见 .card.played .idx）
 *   ② 右侧那张压过来时距数字右边也是 2px
 *   ③ 「10」的横向压缩比（见 .two）
 *
 * 换算：露出的那一条 = 2 + 「10」实宽 + 2 = 2 + 16.8 + 2 ≈ 21px
 *       → margin-left = -(35 - 21) = **-14px**（压盖 14/35 = 40%）
 * 牌从 38 缩到 35 之后，压盖比例会自动从 45% 变成 40% —— 因为露出的一条要
 * 保持 21px 才放得下「10」，这个宽度不随牌宽变。
 */
.seat-cards .card,
#myPlay .cards .card {
  position: relative;
  top: auto;
  width: 35px;
  height: 50px;
  /*
   * 出牌区的牌高，供大小王那条"行高 = 牌高 / 5"的公式用（见 .card.joker 段）。
   * 手牌那边的同一个变量由 applyHandLayout 写在 #hand 上；出牌区在这里声明，
   * 于是**同一份 JOKER 规则**在两个位置都能自动算出合适的字号 —— 这就是"一种方案"。
   */
  --card-h: 50px;
  /*
   * 压盖按**「A」的宽度**算（产品要求：字母距牌左边 2px，右边那张牌距字母右边也 2px）：
   *   露出的那一条 = 2 + 「A」实宽 11.1 + 2 ≈ 15px
   *   → margin-left = -(35 - 15) = -20px（压盖 20/35 = 57.1%）
   * 单字符里 A/K 最宽（都是 11.1px），按 A 定就够；「10」比它宽，会被右边那张压掉一点 ——
   * 产品明确说"10 盖住显示不全先不管"。
   */
  margin-left: -20px;
}
.seat-cards .card:first-child,
#myPlay .cards .card:first-child {
  margin-left: 0;
}
/* 右边的牌压住左边那张：z-index 递增（DOM 顺序本来也是后面的在上，显式写出来更稳） */
.seat-cards .card:nth-child(2),
#myPlay .cards .card:nth-child(2) {
  z-index: 2;
}
.seat-cards .card:nth-child(3),
#myPlay .cards .card:nth-child(3) {
  z-index: 3;
}
.seat-cards .card:nth-child(4),
#myPlay .cards .card:nth-child(4) {
  z-index: 4;
}
.seat-cards .card:nth-child(n + 5),
#myPlay .cards .card:nth-child(n + 5) {
  z-index: 5;
}
/*
 * 出牌区的角标字号。
 *
 * 产品先要求"和底牌一样大"，之后又单独指定了出牌区的值：
 *   · 数字 **16px**（底牌是 18px）
 *   · 花色 **15px**（底牌是 17px）
 * 所以这里不再共用底牌那两个变量，写成本区的固定值 ——
 * 共用一份的前提是"两边要一样"，现在产品明确要不一样，硬绑在一起反而会打架。
 *
 * 16px 字号下「10」的实测总宽约 20px（18px 时是 22.2px），
 * 正是产品给的目标值；`played-cards-check` 会断言这一条（18~21px）。
 *
 * 排列仍然是"数字在上、花色在下、两者居中对齐"。
 */
.seat-cards .card.played .idx .r,
#myPlay .cards .card.played .idx .r {
  font-size: 16px;
  line-height: 1;
  letter-spacing: 0;
}
/*
 * 出牌区的角标：**和手牌统一** —— 点数 12px 宽、花色 12px 宽、「10」16px 宽。
 *
 * 出牌区的字号是 16px（手牌 17px），所以两边的换算比不一样，但**画出来的宽度一样**。
 */
.card.played .idx .r.two,
#myPlay .cards .card.played .idx .r.two {
  /*
   * 出牌区的「10」：**14px 宽**（产品口径；原来 16px）。
   *
   * ⚠️ 必须**同时**给格子宽 + 缩放，两个数字都要对：
   *
   *   · 16px 字号下「10」自然宽约 19.75px → 压到 14px：scaleX = 14/19.75 ≈ 0.71；
   *   · `width: 14px` 让**参与布局的盒子**正好等于**画出来的宽度**。
   *
   * 为什么两条都必要（花色对不齐就是这个原因）：
   * `.idx` 是 `flex-direction: column; align-items: center`，**居中按的是布局盒**，
   * 而 transform 不改变布局盒。只写 scaleX 不写 width 时：
   * 盒子仍是 19.75px、画出来却是 16px —— 于是花色按 19.75px 居中，
   * 比「10」的视觉中心**偏右约 1.85px**（产品实测就是看到这个歪）。
   *
   * 加上 width 之后盒子 = 14px = 画出来的宽度，两边中心重合，花色自然对齐。
   * `transform-origin: left` 保证左边缘不动（右边缘才不会被右边那张压掉）。
   *
   * ⚠️ 量宽度时要用 `offsetWidth`（布局盒 = 14px），**不要**用
   * `getBoundingClientRect()` —— rect 是"缩放后的盒子"（14 × 0.71 ≈ 9.9px），
   * 既不是布局宽也不是墨迹宽，会误判。
   */
  width: 14px;
  transform: scaleX(0.71);
  transform-origin: left center;
  /*
   * ⚠️ 必须有 nowrap：`.idx` 是 flex 容器，子元素会被 blockify，于是 `width: 14px`
   * 变成一个**会折行的块宽** —— 「10」有 19.75px 自然宽，会被折成两行，
   * 而折行后最宽的那行仍是 14px，**宽度断言照样通过**，只有高度会翻倍。
   * （检查里补了一条"「10」的高度必须和单字符一样"来守它。）
   */
  white-space: nowrap;
}
/*
 * 出牌区的普通点数：**统一 12px 宽的格子**（和手牌同一条规则）。
 *
 * ⚠️ 这里不能靠 scaleX 凑：字形本身宽窄不同（「7」9.9px、「A」11.1px），
 * 乘同一个比例只会得到 10.7 / 12.0 这种参差的结果。
 * 真正"统一"的做法和手牌一样 —— 给一个 **12px 的固定格子**，
 * 每个字形在自己的格子里左对齐，于是所有点数的**起始位置**和**占用宽度**都一致。
 *
 * 格子宽不影响布局（.idx 是绝对定位、文字在格子里左对齐），
 * 所以"数字距牌左边 2px"照样成立（检查里有断言）。
 */
.seat-cards .card.played .idx .r:not(.two),
#myPlay .cards .card.played .idx .r:not(.two) {
  width: 12px;
}
.seat-cards .card.played .idx .s,
#myPlay .cards .card.played .idx .s {
  font-size: 15px;
  line-height: 1;
  /* 花色紧贴在数字下面（底牌是并排，这里是上下，所以要把行距收掉） */
  margin-top: -2px;
  /*
   * 出牌区花色的宽度：**10px**（产品最新口径，原来要求 12px）。
   *
   * 实测：15px 字号下花色字形只有 **10.26px** 宽（比数字窄不少），
   * 所以这里不是设格子宽，而是用 scaleX 精确调到 10px：
   *   scaleX = 10 / 10.26 ≈ 0.975，取 **0.97**（实测绘制宽 9.95px）
   *
   * 两个讲究：
   *   · 只改宽度、**不动字号** → 高度不变（和「10」的处理同一个思路）；
   *   · `transform` **不参与布局**，所以缩放不会把数字推走 ——
   *     "数字距牌左边 2px"那条几何照样成立（检查里有断言）。
   */
  transform: scaleX(0.97);
  transform-origin: center center;
}

/*
 * 牌型小牌子（照参考图）：竖排、蓝框、贴在最后一张牌的**左下角、数字下面**。
 *
 * ⚠️ 两个坑：
 *   · 不能从牌顶就开始：角标（数字 + 花色）也贴在左上角，牌子会把它盖住；
 *   · 牌缩小到 38×53 之后，角标占到 68% 的高度，牌子只剩底部约 20px ——
 *     两个字（四炸/顺子…）放得下，三个字（三带二/天王炸/同花顺）要缩一档，
 *     否则会顶出牌外被裁掉，所以有 `.long` 这个修饰类。
 */
.seat-cards .play-kind,
#myPlay .cards .play-kind {
  position: absolute;
  left: 1px;
  top: 64%;
  bottom: 2px;
  width: 13px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid rgba(96, 132, 214, 0.9);
  border-radius: 3px;
  background: rgba(255, 255, 255, 0.92);
  color: #40589c;
  font-size: 8px;
  font-weight: 800;
  line-height: 1;
  letter-spacing: 0;
  writing-mode: vertical-rl;
  text-orientation: upright;
  z-index: 9;
}
.seat-cards .play-kind.long,
#myPlay .cards .play-kind.long {
  font-size: 6.5px;
  letter-spacing: -0.2px;
}
/*
 * ⚠️ 这里原来有一条 `.seat-cards .card.joker .idx.joker-idx { font-size: 11px }`，
 * 是上一版"出牌区把 JOKER 降级成 JO"时定的补偿字号。它比新的
 * `.card.joker .idx.joker-idx`（0,3,0）更具体（0,4,0），会把
 * "行高 = 牌高 / 5、字号 = 行高 × 0.8"那条公式**整个盖掉** —— 已删除。
 * 现在出牌区的字号完全由 --card-h: 50px 代入同一个公式得到（8px / 行高 10px）。
 */

.seat-pass {
  font-size: 17px;
  font-weight: 800;
  color: var(--gold);
  letter-spacing: 2px;
  text-shadow: 0 2px 6px rgba(0, 0, 0, 0.5);
}

/*
 * 回合计时：一个圆环，圈里是剩余秒数，圈本身随进度收掉。
 *
 * 出牌区里显示，轮到谁就出现在谁的出牌区。电脑的圈小一点（2 秒，一闪而过），
 * 真人的圈大一点（30 秒，看得清）。最后 5 秒整圈变红。
 */
.turn-timer {
  position: relative;
  width: 54px;
  height: 54px;
  display: grid;
  place-items: center;
  flex-shrink: 0;
  /*
   * ⚠️ 必须在**最上层**，不能被牌遮挡（产品明确要求）。
   *
   * 为什么会遮：倒计时挂在 #center（我）或 .seat-play（其他人）里，这两处
   * 都**没有 z-index**（等价于 0）；而手牌卡片是 z-index 1~5，
   * 且 #handWrap 在 DOM 里排在 #table 之后 —— 手牌一高（一列叠满）就正好
   * 盖住中间那个圆环，轮到谁反而看不见了。
   *
   * 20 的取法：要高于手牌卡片（1~5）和 #actions（8），
   * 但要低于 toast（50）和诊断/引导层（900）。
   */
  z-index: 20;
}
.turn-timer svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  /* 从 12 点方向开始画 */
  transform: rotate(-90deg);
}
.turn-timer circle {
  fill: none;
  stroke-width: 4;
  stroke-linecap: round;
}
.turn-timer .track {
  stroke: rgba(255, 255, 255, 0.18);
}
.turn-timer .bar {
  stroke: var(--gold);
  transition: stroke-dashoffset 0.15s linear;
}
.turn-timer .n {
  position: relative;
  font-size: 19px;
  font-weight: 800;
  line-height: 1;
  color: var(--gold);
  font-variant-numeric: tabular-nums;
}
/*
 * 电脑（含托管）的圈略小一点做区分 —— 但**时限同样是 30 秒**，
 * 圈里也是两位数字，所以不能太小。
 */
.turn-timer.npc {
  width: 48px;
  height: 48px;
}
.turn-timer.npc .n {
  font-size: 17px;
}
/* 最后 5 秒：整圈变红 */
.turn-timer.urgent .bar {
  stroke: #ff6b6b;
}
.turn-timer.urgent .n {
  color: #ff8f8f;
}

/*
 * ──────────── 装饰层不吃点击 ────────────
 *
 * 产品反馈两条，同一个根因：
 *   · 「选底牌时被其他家出牌挡住，会选不上」
 *   · 「倒计时压着的那张牌选不到」
 *
 * 别人出的牌（`.seat-cards`）和倒计时圆环（`.turn-timer`）都是**画在上层**的
 * 纯展示元素，但它们照样参与命中测试 —— 手指落在那块区域时事件被它们接走，
 * 底下的手牌一张也收不到。
 * 倒计时尤其明显：上一轮为了不被牌挡住把它提到了 z-index 20，
 * 结果它连点击也一起挡了。
 *
 * 这几块**没有任何交互**（不能点、不能拖），所以统一下掉 pointer-events，
 * 让触摸**穿透**到下面的手牌。比调 z-index 干净得多 —— 视觉层级一动都不用动，
 * "倒计时要在最上层" 和 "牌要能点得到" 这两件事就不再打架了。
 */
.seat-play,
.seat-pass,
.seat-cards,
.turn-timer,
#center,
#myPlay {
  pointer-events: none;
}

/* ───────────── 牌 ───────────── */

/*
 * 牌面（比例照素材）：
 *
 *   手牌： 点数在左、花色在右，中间一个大花色
 *     ┌────────┐
 *     │ A♥     │
 *     │   ♥    │
 *     └────────┘
 *
 *   打出去的牌（.played）：点数在上、花色在下方，**没有中间大花色**
 *     ┌────┐
 *     │ A  │
 *     │ ♥  │
 *     └────┘
 *
 * 手牌的字号由 JS 按牌宽注入（--idx-rank / --idx-suit / --suit-big），
 * 比例是点数 31%、角花色 29%、中间大花色 46%。
 */
.card {
  position: absolute;
  top: 0;
  width: var(--card-w);
  height: var(--card-h);
  border-radius: var(--radius);
  background: linear-gradient(180deg, #ffffff, #f4f6f4);
  border: 1px solid #b9c2ba;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.38);
  color: #1b1b1b;
  font-weight: 700;
  transition: transform 0.11s ease, box-shadow 0.11s ease, border-color 0.11s ease;
  overflow: hidden;
}

/*
 * 手牌角上的「点数 + 花色」（row：点数在左、花色在右）。
 *
 * 产品给的是一组精确数值（几轮迭代后**手牌和出牌区统一**）：
 *
 *   普通点数（除大小王和 10）：点数宽 **12px**，花色宽 **12px**
 *   「10」                   ：点数宽 **16px**
 *
 * 布局上的配平（决定了花色能不能排成一列）：
 *   普通：4(距左) + 12(点数) + 3(间距) = 19
 *   「10」：1(距左) + 16(点数) + 2(间距) = 19     ← 一样
 *
 * ⚠️ 中间试过把「10」的点数格缩到 15px，那时必须把间距加到 3px 才能配回 19；
 * 现在产品定回 **16px**，间距也就跟着回到 **2px**（数值正好是最初那一组）。
 * 改任何一个都要重新配平，否则花色会参差不齐（检查里有"花色必须对齐"的断言）。
 *
 * 大小王不走这条路径（它们是 .joker-idx + .joker-art，见下面 .card.joker 段），
 * 所以这里怎么写都不会动到王。
 */
.card .idx {
  position: absolute;
  top: 2px;
  left: 3px;
  display: flex;
  flex-direction: row; /* 点数在左、花色在右 */
  align-items: baseline;
  gap: 3px;
  line-height: 1;
  z-index: 2;
}
/* 「10」：格子更宽、更贴左边、离花色更近 —— 见上面的配平说明 */
.card.ten .idx {
  left: 0;
  gap: 2px;
}
/*
 * ⚠️ 12px 的格子宽**只给手牌**（:not(.played)）：出牌区那边靠"每张露 15px"的版式定位，
 * 给 .r 加固定宽会被 scaleX 再乘一次 —— 实测出牌区「10」从 16px 变成 9.7px（12 × 0.81）。
 */
.card:not(.played) .idx .r {
  font-size: var(--idx-rank, 19px);
  letter-spacing: -0.5px;
  width: 12px;
}
/*
 * 手牌里的「10」：格子 **16px**。
 *
 * ⚠️ 17px 字号下「10」的**自然墨迹宽约 20.9px**，塞进 16px 的格子会溢出去压到花色。
 * 所以和出牌区用同一招：**横向压缩、不动字号**（高度不变）。
 * 压缩比 = 16 / 20.9 ≈ 0.766，取 0.77，压完正好 16px。
 */
.card:not(.played).ten .idx .r {
  width: 16px;
  transform: scaleX(0.77);
  transform-origin: left center;
}
/*
 * 手牌的花色：**统一 12px 宽**。
 *
 * 16px 字号下花色字形约 11px 宽，所以这里要**拉宽**一点：scaleX(12/11) ≈ 1.09。
 * 用 transform 的原因和出牌区一样：它不参与布局，拉宽不会把点数或列位置挤动。
 */
.card:not(.played) .idx .s {
  font-size: var(--idx-suit, 18px);
  transform: scaleX(1.09);
  transform-origin: center center;
}
.card.red {
  color: #cc2222;
}

/* 牌中间的大花色（只有手牌有） */
.card .suit-big {
  position: absolute;
  left: 50%;
  top: 54%;
  transform: translate(-50%, -50%);
  font-size: var(--suit-big, 28px);
  line-height: 1;
  opacity: 0.9;
  z-index: 1;
}

/*
 * 打出去的牌：**花色移到点数下方，并且和点数居中对齐**。
 *
 * 用 `align-items: center` 让两个子元素在**彼此之间**居中
 *（.idx 的宽度 = 最宽的那个子元素，也就是点数），
 * 于是「10」下面那个花色会落在数字的正中，而不是贴左边。
 *
 * ⚠️ `left` 是相对**内边距盒**算的，牌本身还有 1px 边框，所以：
 *     数字距牌左边 = 1px(边框) + left
 * 产品要求"数字距离左边牌边 2px" → `left: 1px`（原来是 3px，等于 4px，太远）。
 *
 * 产品另一条要求"右侧那张压过来时，距数字右边也是 2px"，是这样凑出来的：
 *   露出的那一条 21px（压盖 45%）
 *   − 数字距左 2px
 *   − 「10」的实宽 16.8px（最宽的点数，见 .two 的横向压缩）
 *   = 2.2px   ← 正是 2px 左右
 * 所以 `left: 1px` 和压盖 45% 是**一起**决定这个 2px 间距的，改一个要重算另一个。
 */
.card.played .idx {
  top: 2px;
  left: 1px;
  flex-direction: column;
  align-items: center;
  gap: 0;
}
.card.played .idx .r {
  line-height: 1;
}
.card.played .idx .s {
  margin-top: -1px;
  line-height: 1;
}
.card.played .suit-big {
  display: none;
}

/*
 * 大小王牌面 —— 左边**竖排 JOKER**（占牌高 80%）+ 中间**小丑图案**，**牌面纯白**。
 *
 * 配色只走「字 + 图案」，**不做任何背景填充**（产品明确要求：大王不要红色填充、底部纯白）：
 *   大王 = 红字 #cc2222 + 红图案 + 淡红描边
 *   小王 = 黑字 #1b1b1b + 黑图案 + 淡灰描边
 *
 * 所有尺寸都从 `--card-h`（这张牌的实测高度）算出来，所以**同一份规则**在手牌和
 * 出牌区都自动得到合适的值 —— 这就是"手牌和出牌区用同一种方案"：
 *
 *     字母块高 = 牌高 × 0.80      ← 只占八成，上下各留一成，字不再顶到底（也就不溢出）
 *     每行行高 = 字母块高 / 5
 *     字号     = 行高 × 0.80
 *     图案边长 = 牌高 × 0.52
 *
 *   手牌（--card-h = 80）：行高 12.8、字号 10.24、图案 41.6
 *   出牌区（--card-h = 50）：行高 8、字号 6.4、图案 26
 *
 * ⚠️ 换行**只能**由这里控制：client.js 里千万不要写 idx.style.whiteSpace，
 *    inline 样式优先级高于样式表，会把出牌区的规则顶掉（上一版踩过）。
 */
.card.joker {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  justify-items: center;
  /* 纯白牌面：显式写出来，免得以后有人给 .card 加底色时把王也一起染色 */
  background: #fff;
}
.card.joker .idx.joker-idx {
  grid-column: 1;
  grid-row: 1;
  writing-mode: horizontal-tb;
  text-orientation: mixed;
  white-space: pre-line;
  line-height: calc(var(--card-h, 80px) * 0.8 / 5);
  font-size: calc(var(--card-h, 80px) * 0.8 / 5 * 0.8);
  letter-spacing: 0;
  text-align: center;
  font-weight: 800;
  align-self: center;
}

/*
 * 中间的小丑图案
 *
 * `left: 3px`（配合 position: relative）是产品要求："中间图案和左边字母太靠在一起了，
 * 图案右移 3px，左侧文字不动"。用相对定位而不是 margin：
 * 这个元素是 `justify-self: center` 居中的，加 margin-left 只会把中心推右 **1.5px**（一半），
 * 相对定位才是实打实的 3px，而且完全不影响左边那列文字的布局。
 */
.card.joker .joker-art {
  grid-column: 2;
  grid-row: 1;
  justify-self: center;
  align-self: center;
  position: relative;
  left: 3px;
  width: calc(var(--card-h, 80px) * 0.52);
  height: calc(var(--card-h, 80px) * 0.52);
  fill: currentColor;
  pointer-events: none;
}

/*
 * 配色**只走字和图案**，牌面的边框保持默认（和其他牌完全一样）。
 * 之前给王加过淡红/淡灰描边，产品反馈"大王的牌边线怎么改了红色，用默认的和其他牌一样" —— 已去掉。
 */
.card.joker.big .idx.joker-idx,
.card.joker.big .joker-art {
  color: #cc2222;
}
.card.joker.small .idx.joker-idx,
.card.joker.small .joker-art {
  color: #1b1b1b;
}



/*
 * 手牌布局 —— 严格照素材的摆法：
 *   同点数一列（列内叠放，每张露一条点数），不同点数一列一列往右排。
 *
 * 列内的叠法（这一点改了好几轮才对）：
 *   · 第一张牌在**最下层**（z 最高）、整张可见，它的**底边**就是整叠的底线
 *   · 后面的牌依次**往上**错开一个步进，各露出上面一条（正好是点数信签）
 *   · 结果就是"底边对齐"，所有点数都能看清
 *
 * #hand 是定位容器；每一列 .hand-col 绝对定位（left/bottom 由 JS 算），
 * 列内的牌也是绝对定位（top 由 JS 的列内步进算）。
 *
 * #hand 自身也是绝对定位（见下面的手牌锚点一节），底边贴在分界线上方 2px；
 * JS 再用 top 做整体上移（牌多时往上排到窗口上方）。
 */
#hand .card {
  cursor: pointer;
  /*
   * 拖动连选要的几个设置：
   *   touch-action: none —— 手指按在牌上滑动时不要触发页面滚动/缩放，
   *                        否则拖动会被浏览器当成滚动手势抢走
   *   user-select: none —— 拖动时不要选中文字（那会弹出复制菜单）
   */
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}
/*
 * ⚠️ 这里原来有一条 `#hand .card.selectable:hover { border-color: #6fc7ff }`（悬停改描边），
 * **已删除**，原因有两个：
 *   1. 产品要求"选中牌的时候不要有边颜色改变，只要改填充" —— 触摸设备上
 *      `:hover` 点一下就**粘住**不走了，看起来就是"选中的牌边框变蓝了"，
 *      正是被报上来的现象（检查里量到的 rgb(111,199,255) 就是它）；
 *   2. 手机本来就没有悬停，这条规则只在调试时起作用，删掉没有任何损失。
 */
#hand .hand-col {
  position: absolute;
  bottom: 0;
}
/*
 * 选中的牌变灰 —— **不做位移**（产品要求：选中不要往前推）。
 * 用 greyscale + 压暗表示"这张已选中"，配一圈金色描边方便确认。
 * 注意描边和灰化都作用在整列上：同点数的牌是一叠，
 * 一起变灰才符合"选中了这个点数"的直觉。
 */
/*
 * 选中的牌：**只加深牌面的填充**。
 *
 * 产品要求（两轮迭代）：
 *   1. "选中填充变为灰色就行，字母、花色不用变"
 *   2. "不要有边颜色改变，只要改填充就行，填充颜色加深一点"
 *
 * 所以这里**不设 border-color、也不加金色外圈**（那两样都是"边颜色改变"）——
 * 边框和阴影全部走 .card 的默认值，和其他牌完全一致。
 * 之前用过 `filter: grayscale(1) brightness(0.82)`，那是把整张牌（连同点和花色）
 * 一起去色压暗，红桃会变灰 —— 已废弃。
 */
#hand .card.selected {
  background: linear-gradient(180deg, #c2c2c2, #a8a8a8);
}

/*
 * ⚠️ 这里原来有一条 `#hand .card.hinted { border-color:#4fc3f7; box-shadow: 0 0 0 2px rgba(79,195,247,.9) }`
 *    —— **「提示」选中牌时那条天蓝色描边 + 外圈就是它**（产品反馈：「点提示选牌不要把牌带上边线，
 *    和手动选牌一样」）。已删除，理由：
 *      · 「提示」在 client.js 里本来就是**直接选中**（`selected.clear()` + 把推荐的那几张加进
 *        `selected`，见 socket 的 `hint` 分支），也就是说它已经走的是**手动选牌那套 `.selected`**
 *        （只加深填充、不动边框）；
 *      · 上面那条 `.hinted` 是在那之上**又加**了一层蓝边 + 外发光 —— 于是同一手牌，
 *        「点提示选出来的」和「手动点出来的」长得不一样（一个带边线、一个不带）；
 *      · 所以正确做法是**删掉这条额外样式**，让两种选中**完全同源**（都是 `.selected`）。
 *    `.hinted` 这个类名仍然由 client.js 打在牌上（只作为"这几张是提示给的"的可观测标记，
 *    没有任何视觉），给自检脚本读 DOM 用 —— 谁要再给它加边线/描边，就是把这个 bug 加回来。
 *
 *    ⚠️ 本文件末尾（补丁区）还抄过一条一模一样的规则，两处**都已删除**；
 *       以后要加样式请只留一处，别在补丁区再抄一遍。
 */

/* ───────────── 底部操作 ───────────── */

/*
 * 底部功能区。
 *
 * 产品要求：
 *   1. **不要背景色**，和上面的牌桌连成一体（之前是深色块，像切开的两个区域）
 *   2. 功能区上方画**一条分界线**，手牌的下边线对齐在这条线上方 2px
 *   3. 牌的大小**固定**，牌多就往上排
 *
 * 所以这里手牌区只是一个"贴住分界线的锚点"，高度给个够用的固定值就行；
 * 手牌比它高时往上溢出（overflow: visible），不去改变牌的尺寸。
 */
/*
 * 底部：手牌 + 我出的牌 + 操作按钮 + 我的头像。
 *
 * 布局要点（照素材）：
 *   · **分界线在最下面**（--footer-bottom: 56px，就在按钮上方一点），
 *     不是画在整块区域的顶上 —— 手牌和头像都会**越过**这条线，
 *     看起来才像"牌摆在桌子上"，而不是被一条线截断
 *   · 手牌下边线贴在分界线上方 2px（JS 按分界线位置算 top）
 *   · 我的头像绝对定位在左下角、贴到应用下沿 2px
 *   · 操作按钮浮在右下角（absolute），但**左边要让开 --me-col 那一列**，
 *     否则会和我的头像撞上
 */
#bottom {
  /* 分界线距底边的距离：正好在按钮上方一点 */
  --footer-bottom: 56px;
  /* 最左留给我的头像的宽度 */
  --me-col: 96px;
  position: relative;
  /* 高度 = 手牌往上排的起点 + 分界线以下的余量 */
  height: 168px;
  display: block;
  padding: 0 14px;
  background: none;
  border-top: none;
  overflow: visible;
}

/* 分界线：画在底部按钮上方一点 */
#bottom::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: var(--footer-bottom);
  height: 1px;
  background: rgba(255, 255, 255, 0.16);
  pointer-events: none;
}

/* 我的头像：最左一列，**贴到应用下沿 2px**（产品明确要求），要越过分界线 */
#bottom .pos-bottom {
  position: absolute;
  left: 14px;
  bottom: 2px;
  z-index: 6;
}
/*
 * 我自己出的牌**不画在左下角**（那块已经被手牌占满），改画在牌桌中间，
 * 见 #myPlay。所以这里的出牌区平时隐藏。
 *
 * 但**两种例外**不能一起藏掉：
 *   · 倒计时：轮到我时圆环就挂在这个出牌区里，藏了就看不到自己的倒计时
 *   · 名次标签：我自己走完了要把「第一」显示出来（和其他三家一致）
 * 所以用两条 `:not(:has(...))` 把它们排除在外。
 */
#bottom .pos-bottom .seat-play:not(:has(.turn-timer)):not(:has(.seat-rank)),
#bottom .pos-bottom .seat-pass {
  display: none;
}

/*
 * 手牌锚点。
 *
 * 位置和高度都由 JS 算好后写成内联样式：
 *
 *     handTop = 分界线y - 2 - 手牌高度
 *
 * 也就是「手牌底边落在分界线上方 2px」，牌多时自动往上排。
 * 分界线现在画在底栏**下面**（按钮上方一点），所以手牌会越过原本
 * 那一整块区域 —— 这正是素材里的效果。
 */
#handWrap {
  position: relative;
  width: 100%;
  align-self: start;
  /* 牌多时往上溢出到牌桌上 */
  overflow: visible;
}

/*
 * 手牌本体：高度 = 牌高 + 叠放高度（JS 设），底部用内联 top 定位。
 * 宽度由 JS 按列数算，水平居中。
 */
#hand {
  position: absolute;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  height: var(--card-h);
}
/* 横向排不下时改为左对齐 + 横向滚动 */
#handWrap.scrollable #hand {
  left: 0;
  transform: none;
}

/*
 * 操作区（选牌提示 + 按钮）。
 *
 * 浮在底栏右下角（absolute），**不参与布局**：
 *   · 这样它不会把底栏撑高，手牌和头像可以自由越过它
 *   · 它是浮层，所以 z-index 要高，保证按钮永远点得到
 *   · `left: var(--me-col)` 让开最左那一列给我的头像，否则会撞上
 */
#actions {
  position: absolute;
  right: 40px; /* 四周统一 40px */
  bottom: 6px;
  left: var(--me-col);
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 6px;
  z-index: 8;
  pointer-events: none;
}
#actions .btn-row,
#actions .btn {
  pointer-events: auto;
}
/* 选牌提示做成一行，不要占两行 */
.sel {
  font-size: 12px;
  color: var(--ink-dim);
  max-width: 300px;
  min-width: 240px;
  text-align: right;
  height: 16px;
  line-height: 16px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/*
 * 没选牌时不显示任何提示（产品要求）。
 *
 * 用 display: none 而不是留空字符串：留空的话这个 div 仍然占着 16px 高，
 * 在 #actions 里会把按钮往左挤 —— 明明没内容却影响布局。
 */
.sel.empty {
  display: none;
}
.btn-row {
  display: flex;
  gap: 8px;
}

.btn {
  border: 1px solid rgba(255, 255, 255, 0.22);
  background: rgba(255, 255, 255, 0.1);
  color: var(--ink);
  font-size: 14px;
  font-weight: 600;
  padding: 9px 20px;
  border-radius: 9px;
  cursor: pointer;
  transition: filter 0.12s, transform 0.06s;
  white-space: nowrap;
}
.btn:hover:not(:disabled) {
  filter: brightness(1.18);
}
.btn:active:not(:disabled) {
  transform: translateY(1px);
}
.btn:disabled {
  opacity: 0.34;
  cursor: not-allowed;
}
.btn.primary {
  background: linear-gradient(180deg, #2f9e63, #1d7a49);
  border-color: #35b673;
}
.btn.accent {
  background: linear-gradient(180deg, #d8a83a, #b8862a);
  border-color: var(--gold);
  color: #241c05;
}
.btn.ghost {
  background: transparent;
}
.btn.small {
  padding: 4px 10px;
  font-size: 12px;
  border-radius: 7px;
}
.btn.off {
  opacity: 0.5;
}

.hidden {
  display: none !important;
}

/* ───────────── 大字横幅（出完牌的名次 / 接风）───────────── */

/*
 * 挂在 #app 里面（见 client.js 的 showBanner）—— 强制横屏是把 #app 整体转 90°，
 * 挂到 body 上会跟着屏幕方向歪过去。
 *
 * z-index 700：压住牌、倒计时、toast(50)，但让给设置面板(800)。
 * pointer-events: none —— 只是个动画，绝不能挡住点牌（这条踩过坑：
 * 倒计时圆环当初就是因为会吃点击，才不得不加 pointer-events: none）。
 */
.banner {
  position: fixed;
  left: 50%;
  top: 34%;
  z-index: 700;
  pointer-events: none;
  font-size: 40px;
  font-weight: 900;
  letter-spacing: 4px;
  white-space: nowrap;
  opacity: 0;
  transform: translate(-50%, -50%) scale(0.6);
  color: var(--gold);
  text-shadow: 0 4px 18px rgba(0, 0, 0, 0.8), 0 0 30px rgba(240, 200, 92, 0.55);
}

/* 名次：金色，砰地弹出来，然后**飞向那个玩家的头像**（见 client.js 的 showBanner） */
.banner.finish.show {
  animation: bannerPop 1.9s cubic-bezier(0.25, 0.9, 0.3, 1) forwards;
}
/*
 * 名次横幅的关键帧。
 *
 * 产品要求：**不要只在屏幕中间闪一下**（不醒目），要"从中间出现、缩小到各自
 * 玩家的头像旁边"。所以 0~46% 在中间"砰"地弹出来并停一下让人看清，
 * 之后缩小着飞到落点淡出。
 *
 * 落点用 `--fly-x/--fly-y` 注入（由 JS 按目标头像的实时位置算），
 * 所以**这两个变量必须在加 .show 之前就设好** —— 关键帧一开头就要用。
 * 注意：位移是**元素自身坐标系**里的，而强制横屏给 #app 加了 rotate(±90deg)，
 * JS 会把视觉位移反算回这个坐标系（见 flyOffsetTo）。
 */
@keyframes bannerPop {
  0% {
    opacity: 0;
    transform: translate(-50%, -50%) scale(0.5) rotate(-7deg);
  }
  16% {
    opacity: 1;
    transform: translate(-50%, -50%) scale(1.14) rotate(2deg);
  }
  30% {
    opacity: 1;
    transform: translate(-50%, -50%) scale(1) rotate(0deg);
  }
  /* 停一下：让人看清是谁、第几游 */
  46% {
    opacity: 1;
    transform: translate(-50%, -50%) scale(1) rotate(0deg);
  }
  100% {
    opacity: 0;
    transform: translate(calc(-50% + var(--fly-x, 0px)), calc(-50% + var(--fly-y, 0px))) scale(0.24)
      rotate(0deg);
  }
}

/*
 * 每局开始的「本局打X」—— 用**独立的 level kind**，不要复用 finish：
 * finish 那条横幅的检查是按"class 里没有 jiefeng 就当名次"来分类的，
 * 复用会让「本局打X」被当成名次横幅（实测把 finish-jiefeng-check 带崩过一次）。
 *
 * 节奏是产品定的：**中间停留 0.5 秒**，然后才缩小飞到档位牌上。
 * 关键是"停留"要从它**完全显示出来**之后算起 —— 所以弹入段压到前 20%（1.6s 里的 0.32s），
 * 20%→52% 保持不动（0.5s），52% 之后再飞。
 */
.banner.level {
  color: var(--gold);
  text-shadow: 0 4px 18px rgba(0, 0, 0, 0.8), 0 0 30px rgba(240, 200, 92, 0.55);
}
.banner.level.show {
  animation: levelPop 1.6s cubic-bezier(0.25, 0.9, 0.3, 1) forwards;
}
@keyframes levelPop {
  0% {
    opacity: 0;
    transform: translate(-50%, -50%) scale(0.5) rotate(-7deg);
  }
  12% {
    opacity: 1;
    transform: translate(-50%, -50%) scale(1.14) rotate(2deg);
  }
  20% {
    opacity: 1;
    transform: translate(-50%, -50%) scale(1) rotate(0deg);
  }
  /* ↓ 停留：产品要求 0.5s（1.6s × 32% ≈ 0.51s），这段时间里它就是停在屏幕中间的 */
  52% {
    opacity: 1;
    transform: translate(-50%, -50%) scale(1) rotate(0deg);
  }
  100% {
    opacity: 0;
    transform: translate(calc(-50% + var(--fly-x, 0px)), calc(-50% + var(--fly-y, 0px))) scale(0.24);
  }
}

/* 接风：青蓝色 + 从两侧扫入，和名次明显区分 */
.banner.jiefeng {
  color: #7ee0ff;
  text-shadow: 0 4px 18px rgba(0, 0, 0, 0.8), 0 0 34px rgba(126, 224, 255, 0.7);
}
.banner.jiefeng.show {
  animation: jiefengPop 1.9s cubic-bezier(0.2, 0.9, 0.2, 1) forwards;
}
/*
 * 接风的动画：**和名次同一套节奏** —— 中间弹出来 → 停一下 → 缩小着飞到落点淡出。
 *
 * ⚠️ 老版本的这段关键帧**从头到尾没用 `--fly-x/--fly-y`**，所以接风只在屏幕中间
 * 闪一下、不会飞（产品实测报上来的就是"接风也要动画，接风缩小到接风的头像上"）。
 * 落点由 JS 注入（playJiefeng 会把 seat 传下去），这里必须消费这两个变量。
 */
@keyframes jiefengPop {
  0% {
    opacity: 0;
    letter-spacing: 34px;
    transform: translate(-50%, -50%) scale(1.5);
  }
  22% {
    opacity: 1;
    letter-spacing: 6px;
    transform: translate(-50%, -50%) scale(1.06);
  }
  40% {
    letter-spacing: 4px;
    transform: translate(-50%, -50%) scale(1);
  }
  /* 停一下：让人看清是谁接风 */
  56% {
    opacity: 1;
    letter-spacing: 4px;
    transform: translate(-50%, -50%) scale(1);
  }
  100% {
    opacity: 0;
    letter-spacing: 4px;
    transform: translate(calc(-50% + var(--fly-x, 0px)), calc(-50% + var(--fly-y, 0px))) scale(0.24);
  }
}

/* 进贡确认：屏幕中央清晰停留约 1 秒，再淡出；期间前端锁住出牌倒计时与操作。 */
.banner.tribute {
  color: #ffe7a3;
  font-size: clamp(24px, 4.3vw, 42px);
  letter-spacing: 1px;
  padding: 14px 26px;
  border: 1px solid rgba(255, 224, 143, 0.6);
  border-radius: 16px;
  background: linear-gradient(135deg, rgba(12, 65, 47, 0.96), rgba(19, 92, 63, 0.96));
  box-shadow: 0 12px 38px rgba(0, 0, 0, 0.38), 0 0 24px rgba(240, 200, 92, 0.25);
}
.banner.tribute.show {
  animation: tributeConfirm 1.3s ease-out forwards;
}
@keyframes tributeConfirm {
  0% {
    opacity: 0;
    transform: translate(-50%, -50%) scale(0.88);
  }
  12% {
    opacity: 1;
    transform: translate(-50%, -50%) scale(1.04);
  }
  20%, 88% {
    opacity: 1;
    transform: translate(-50%, -50%) scale(1);
  }
  100% {
    opacity: 0;
    transform: translate(-50%, -50%) scale(0.98);
  }
}

/* 尊重"减少动效"偏好：不做缩放/位移，只留淡入淡出 */
@media (prefers-reduced-motion: reduce) {
  .banner.finish.show,
  .banner.level.show,
  .banner.jiefeng.show,
  .banner.tribute.show {
    animation: bannerFade 1.6s linear forwards;
  }
  @keyframes bannerFade {
    0% {
      opacity: 0;
    }
    15%,
    80% {
      opacity: 1;
    }
    100% {
      opacity: 0;
    }
  }
}

/* ───────────── toast ───────────── */

.toast {
  position: fixed;
  left: 50%;
  bottom: 30%;
  transform: translateX(-50%);
  background: rgba(200, 40, 40, 0.94);
  color: #fff;
  padding: 10px 20px;
  border-radius: 10px;
  font-size: 14px;
  font-weight: 600;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.45);
  z-index: 50;
  pointer-events: none;
  max-width: 80vw;
  text-align: center;
}
.toast.ok {
  background: rgba(30, 140, 85, 0.94);
}

/* ───────────── 窄屏（手机） ───────────── */
/*
 * 手机高度有限，上下两栏必须尽量薄，把高度让给中间的牌桌：
 *   · 顶栏只有「本局打几 + 重开」，天然就一行
 *   · 底栏 = 手牌一行 + 按钮一行；我的头像挪到按钮那一行的右边，
 *     不再单独占一列宽度（手机太窄，留一列头像会把手牌挤扁）
 */

/* ───────────── 手机横屏 ───────────── */
/*
 * 横屏手机（典型 844×390 / 780×360）和竖屏是**相反**的约束：
 *
 *   竖屏 390×844 → 缺宽度、多高度 → 手机样式把左右座位列收窄到 84px
 *   横屏 844×390 → 缺高度、多宽度 → 这里把底栏压矮，把宽度让给牌桌
 *
 * 上面那条断点对横屏原本是**落空的**（844 > 720），已经在断点列表里补上
 * `(orientation: landscape) and (max-height: 520px)`，让横屏手机先吃到
 * 手机样式，再由下面这一段针对横屏做二次收敛。
 *
 * 三个数值不是拍脑袋，是按实测反推的：
 *
 *   1. 27 张手牌会**横铺满整条底栏**（实测右边界 825 / 844px，仅剩 6px 余量），
 *      所以横向没有可让的余地，只能从纵向解决
 *   2. 手牌底边落在分界线（--footer-bottom）上方 2px
 *   3. 我的头像、倒计时、操作区都是底栏内的绝对定位元素 ——
 *      只要它们**整体待在分界线以下**，就绝不可能压住手牌
 *      （**头像例外**：产品明确说"头像可以越过分界线"，见下面的 avatar）
 *
 * 于是 --footer-bottom 由操作区反推。当前口径（产品最新要求）：
 *
 *     按钮高 38 + 按钮距底边 2 + 分界线距按钮顶 2  =  **42**
 *
 * 底栏总高原本取 124px（余下 70px 当手牌带起点）。后来产品对照参考素材反馈
 * 「状态栏太高、占牌桌太多」，压到 64px —— 因为手牌和分界线都锚在**底边**上，
 * 减掉的是底栏上方的空白，牌桌因此从 278px 涨到 338px（69% → 84%）。
 *
 * 分界线又下移过一次：54 → 42（产品要求"边线下移"）。
 * 注意手牌是**锚在分界线上**的，所以分界线一下移，手牌也跟着下移同样的距离
 *（`bottomBarDividerY()` = clientHeight - --footer-bottom），牌桌的可视区随之变大。
 */

/* ───────────── 强制横屏（竖屏手机也横着显示）───────────── */
/*
 * 产品口径：**不要「请横屏」提示，直接强行横过来。**
 *
 * 网页侧有两个手段，按可靠性排序：
 *
 *   1. screen.orientation.lock('landscape') —— 让操作系统真的转屏。
 *      但只在全屏 / 添加到主屏幕（standalone）状态下才被允许，所以在
 *      fullscreen.js 里：进入全屏后调一次，standalone 启动时再调一次。
 *
 *   2. CSS 旋转 —— 系统报竖屏时（用户开了「方向锁定」是最常见的情况），
 *      把整个应用转 90°。此时用户手里其实已经横着拿了，转一下正好对上。
 *
 * 这里是手段 2。
 *
 * 旋转后必须**正好铺满屏幕**。两套变换都按四角映射验算过：
 *   #app 先按 width=100dvh / height=100dvw 摆好（宽高互换），再：
 *     正向：transform-origin: 0 0;  rotate(90deg) translateY(-100%)
 *     翻转：transform-origin: 0 0;  translateY(100%) rotate(-90deg)
 *   两者都把四角映射到屏幕四角，不露边、不错位。
 *
 * 往哪个方向转？网页无从得知用户是往左还是往右转的手机，所以顶栏给一个
 * 「翻转」按钮（只在这一模式下出现）一键换向，选择记在 localStorage。
 * 偏好由 index.html 里一段内联脚本尽早恢复，避免先画错一帧再翻过去。
 *
 * 只对「窄屏 + 竖屏 + 触摸设备」生效。
 * `(pointer: coarse)` 是关键的一道闸：没有它，桌面上开一个窄高窗口（比如
 * 600×900）也会被莫名其妙转过来。触摸屏笔记本的主指针通常是 fine，不受影响。
 */
#btnFlip {
  display: none;
}


/* ───────────── 全屏（fullscreen.js）───────────── */

/*
 * 「全屏」按钮处于全屏状态时高亮一下，让用户知道再点一次是退出。
 * 按钮本身在 .table-topbar 里，用 .btn.ghost.small 的既有样式。
 */
.btn.ghost.on {
  background: rgba(240, 200, 92, 0.18);
  border-color: rgba(240, 200, 92, 0.6);
  color: var(--gold);
}

/* 危险操作（退出）—— 用红色边框区分，别和普通按钮混在一起 */
.btn.danger {
  background: rgba(190, 60, 60, 0.28);
  border-color: rgba(255, 120, 120, 0.55);
}

/* ───────────── 设置面板 ───────────── */

/*
 * 面板挂在 #app 里面（见 index.html 的说明），所以这里的定位是
 * **应用局部坐标** —— #app 被旋转 90° 时面板跟着转，方向正确。
 *
 * z-index 800：要压住 toast(50) 和手牌，但让给「全屏引导提示」(900)。
 */
.sheet {
  position: fixed;
  inset: 0;
  z-index: 800;
  background: rgba(0, 0, 0, 0.55);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 8px;
}

.sheet-box {
  width: min(680px, 100%);
  max-height: 100%;
  overflow-y: auto;
  background: linear-gradient(180deg, #14603f, #0b3a28);
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: 14px;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.55);
  /* 面板里要能点、能输入 —— 外层的 pointer-events 不能被别处影响 */
  pointer-events: auto;
}

.sheet-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 14px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.14);
  position: sticky;
  top: 0;
  background: #14603f;
  border-radius: 14px 14px 0 0;
}

.sheet-title {
  font-size: 15px;
  font-weight: 700;
  color: var(--gold);
}

.sheet-body {
  padding: 12px 14px 16px;
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.field-label {
  font-size: 12px;
  color: var(--ink-dim);
  margin-bottom: 6px;
}

.field-row {
  display: flex;
  align-items: center;
  gap: 8px;
}

.inp {
  flex: 1;
  min-width: 0;
  background: rgba(0, 0, 0, 0.28);
  border: 1px solid rgba(255, 255, 255, 0.2);
  border-radius: 9px;
  color: var(--ink);
  font-size: 14px;
  font-family: inherit;
  padding: 8px 12px;
}
.inp:focus {
  outline: none;
  border-color: rgba(240, 200, 92, 0.7);
}

/* 头像九宫格：20 张素材，一行放 10 个，两行正好。
 * 限宽是为了别把面板撑得要在小屏上滚动 —— 手机横屏可视高度只有 ~390px。 */
.avatar-grid {
  display: grid;
  grid-template-columns: repeat(10, 1fr);
  gap: 6px;
  max-width: 470px;
}

.avatar-grid .pick {
  width: 100%;
  aspect-ratio: 1;
  border-radius: 50%;
  overflow: hidden;
  border: 2px solid transparent;
  background: rgba(0, 0, 0, 0.25);
  padding: 0;
  cursor: pointer;
}
.avatar-grid .pick img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.avatar-grid .pick.on {
  border-color: var(--gold);
  box-shadow: 0 0 0 2px rgba(240, 200, 92, 0.35);
}

.tip {
  font-size: 12px;
  color: var(--ink-dim);
  margin-top: 6px;
  min-height: 16px;
}
.tip.warn {
  color: #ffb4b4;
}
.tip.ok {
  color: #9ff0bd;
}

/*
 * 全屏引导提示（由 fullscreen.js 动态创建）。
 *
 * 只在两种情况出现：
 *   · 拿不到 Fullscreen API —— 主要是 iPhone（iOS 只给 <video> 实现全屏）
 *   · 在微信内置浏览器里 —— 有 API 也基本会被忽略
 * 内容就是教用户走「添加到主屏幕」那条真正可行的路。点「知道了」后不再出现。
 */
#fsHint {
  position: fixed;
  left: 50%;
  transform: translateX(-50%);
  bottom: calc(12px + env(safe-area-inset-bottom));
  z-index: 900;
  display: flex;
  align-items: center;
  gap: 10px;
  max-width: calc(100vw - 24px);
  padding: 9px 10px 9px 14px;
  border-radius: 10px;
  background: rgba(6, 32, 21, 0.94);
  border: 1px solid rgba(255, 255, 255, 0.18);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
  color: var(--ink);
  font-size: 13px;
  line-height: 1.4;
  animation: fsHintIn 0.24s ease-out;
}
#fsHint button {
  flex-shrink: 0;
  padding: 7px 12px;
  border: 1px solid rgba(255, 255, 255, 0.24);
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.12);
  color: var(--ink);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
}
@keyframes fsHintIn {
  from {
    opacity: 0;
    transform: translateX(-50%) translateY(8px);
  }
  to {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
  }
}

/* ───────────── AI 托管遮罩 ─────────────
 *
 * 产品要求：被 AI 托管时屏幕「稍微变灰一点」，并提示「AI托管中，点击取消」。
 *
 * 为什么用遮罩而不是给 #app 加 filter: grayscale()：
 * filter 会让 #app 变成**包含块**，里面所有 position: fixed 的子元素
 *（横幅、倒计时、toast）都会改成相对它定位 —— 强制横屏那套坐标全乱。
 * 半透明遮罩没有这个副作用，而且顺便能接住"点击取消"。
 *
 * z-index 650：压住牌和座位，但**低于横幅(700)**（名次/接风还得看得见），
 * 也低于设置面板(800)（否则设置打不开）。
 */
/* ───────────── AI 托管遮罩 ─────────────
 *
 * 产品要求：被 AI 托管时屏幕「稍微变灰一点」并提示「AI托管中，点击取消」，
 * 后来又补了两条：**别用黑底**、要**能透视到下面的牌**。
 *
 * 所以底色从 rgba(20,26,22,.42)（偏黑）换成一层**很淡的青绿**（和牌桌同色系），
 * 透明度也压到 0.16 —— 牌面清楚可见，只是整体蒙了一层、并靠一圈内阴影收边。
 *
 * 定位沿用横幅那套：挂在 #app 里面（强制横屏是把 #app 整体转 90°，
 * 挂到 body 上会跟着屏幕方向歪过去）。z-index 650：压住牌和座位，
 * 但低于横幅(700)与设置面板(800)。
 */
/*
 * ══════════ 跟随 App 的横屏坐标系（vivo 安卓浏览器实测踩的坑）══════════
 *
 * 强制横屏是靠这个媒体查询把 #app 变成 position: fixed + rotate(90deg) 实现的
 *（见下面的 @media (orientation: portrait) ... 块）。
 *
 * 我原来给这个遮罩用 position: fixed —— 在部分安卓浏览器里，fixed 元素会**退回视口**
 * 定位（不去理会被 transform 的祖先），于是横屏的牌桌上出现一块**竖屏**的罩子，
 * 产品在 vivo 安卓浏览器上实测到了。
 *
 * 改成 position: absolute 就对了：
 *   · 旋转时  → 最近的定位祖先是 #app（它是 fixed），absolute 天然跟着它，
 *               框和旋转一起继承，正好是横屏那块区域；
 *   · 没旋转时 → #app 没有 position，absolute 退回初始包含块（视口），也对。
 * 两种情况都落在"横屏"上，所以不需要再写任何 orientation 相关的分支。
 */
/* ⚠️ 必须用 absolute 而不是 fixed —— 见上面"跟随 App 的横屏坐标系"的说明 */
.autoplay-mask {
  position: absolute;
  inset: 0;
  z-index: 650;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(46, 120, 92, 0.16);
  box-shadow: inset 0 0 120px rgba(0, 0, 0, 0.18);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.autoplay-mask.hidden {
  display: none;
}
.autoplay-mask span {
  padding: 10px 22px;
  border-radius: 999px;
  background: rgba(20, 60, 44, 0.72);
  border: 1px solid rgba(255, 255, 255, 0.34);
  color: #fff;
  font-size: 17px;
  font-weight: 700;
  letter-spacing: 1px;
  text-shadow: 0 2px 10px rgba(0, 0, 0, 0.6);
}
/* 掼蛋本地联调界面 —— 纯 CSS，无依赖。
   视觉参照真实产品素材：牌桌墨绿、卡牌白底、
   点数+花色固定在左上角竖排（这样叠牌时才看得见）。 */

:root {
  --felt-1: #1a6b45;
  --felt-2: #0a3524;
  --ink: #eef5f1;
  --ink-dim: #9fb8ac;
  --gold: #f0c85c;
  /* 牌圈尺寸的兜底值；手牌实际用 JS 按 HAND_BASE_CARD_W/H 写进 #hand 的内联变量 */
  --card-w: 56px;
  --card-h: 80px;
  --radius: 8px;
}

* {
  box-sizing: border-box;
  -webkit-tap-highlight-color: transparent;
}

html,
body {
  margin: 0;
  height: 100%;
  overflow: hidden;
  font-family: system-ui, -apple-system, 'Segoe UI', 'Microsoft YaHei', sans-serif;
  color: var(--ink);
  background: radial-gradient(circle at 50% 35%, var(--felt-1), var(--felt-2) 72%);
  user-select: none;
}

/*
 * 高度与安全区。
 *
 * height: 100dvh —— dvh 跟着**可见区域**走。移动端工具栏收起/展开时不会留灰边；
 * 100% 在某些 webview 里是按较大的 layout viewport 解析的，就会露出一条空白。
 * 不支持 dvh 的浏览器会自动退回上面那条 height: 100%。
 *
 * padding 用 env(safe-area-inset-*) —— viewport meta 里已经写了 viewport-fit=cover，
 * env() 才会生效。普通标签页里这些值都是 0，对现有布局零影响；
 * 一旦「添加到主屏幕」（standalone，浏览器 UI 全没了），顶部状态栏/刘海和
 * 底部 Home 指示条就会压到内容上，必须靠这层 padding 让开。
 *
 * ⚠️ **底部的 Home 指示条（那条白色加粗横线）不预留**（产品明确要求）：
 *   · iOS 是把这条线**画在内容之上**的，它本身不遮挡可读信息
 *   · 预留出来反而把整条底栏往上顶，牌桌少一截
 * 所以底部写 0，内容一直铺到屏幕最下沿；左右（横屏时是刘海那一侧）照旧让开。
 */
body {
  height: 100dvh;
  padding: env(safe-area-inset-top) env(safe-area-inset-right) 0 env(safe-area-inset-left);
}

/*
 * 三段式：牌桌（吃掉剩余高度）/ 底部（手牌一行 + 按钮一行）。
 *
 * **顶栏那一行取消了** —— 高度全给牌桌，玩家3 的头像才能贴到应用最上沿。
 * 「本局打几」和「重开」改成浮在牌桌左上/右上角（.table-topbar）。
 */
#app {
  display: grid;
  grid-template-rows: 1fr auto;
  height: 100%;
  width: 100%;
}

/* ───────────── 浮在牌桌上的顶栏 ───────────── */

.table-topbar {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 2px 40px;
  font-size: 12px;
  /* 浮层：不吃布局高度，手指也能点到下面的牌 */
  pointer-events: none;
  z-index: 4;
}
.table-topbar .btn {
  pointer-events: auto;
}
.tt-left,
.tt-right {
  display: flex;
  align-items: center;
  gap: 6px;
}

.tb-group {
  display: flex;
  align-items: center;
  gap: 6px;
}
.tb-group.right {
  margin-left: auto;
  gap: 8px;
}

.tb-label {
  color: var(--ink-dim);
}
.tb-value {
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.tb-value.level {
  color: var(--gold);
  font-size: 15px;
}
.tb-value.me {
  color: #7fd1ff;
}

/* ───────────── 档位牌：我方 / 对方（样式照产品给的截图） ───────────── */

.level-board {
  display: flex;
  gap: 8px;
  align-items: stretch;
  pointer-events: none;
}
.level-board .lb-box {
  position: relative;
  min-width: 52px;
  padding: 3px 10px 4px;
  border-radius: 6px;
  border: 2px solid rgba(255, 255, 255, 0.28);
  background: linear-gradient(180deg, rgba(28, 58, 46, 0.94), rgba(18, 42, 33, 0.94));
  text-align: center;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.35);
  opacity: 0.82;
}
.level-board .lb-name {
  font-size: 13px;
  font-weight: 700;
  color: #e8f3ec;
  line-height: 1.15;
}
/* 名字下面那条分隔线（截图里也有） */
.level-board .lb-name::after {
  content: '';
  display: block;
  margin: 2px 0 1px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.35);
}
.level-board .lb-value {
  font-size: 20px;
  font-weight: 800;
  line-height: 1.05;
  color: #ffffff;
  font-variant-numeric: tabular-nums;
}
/* 当前这一局打的是哪一方：金边 + 金三角，另一块压暗 */
.level-board .lb-box.active {
  border-color: var(--gold);
  opacity: 1;
  box-shadow: 0 0 0 1px rgba(240, 200, 92, 0.55), 0 2px 8px rgba(0, 0, 0, 0.45);
}
.level-board .lb-box.active .lb-name {
  color: var(--gold);
}
.level-board .lb-box .lb-caret {
  display: none;
}
.level-board .lb-box.active .lb-caret {
  display: block;
  position: absolute;
  left: 50%;
  bottom: -9px;
  width: 0;
  height: 0;
  margin-left: -6px;
  border-left: 6px solid transparent;
  border-right: 6px solid transparent;
  border-top: 7px solid var(--gold);
}

.team {
  padding: 2px 40px; /* 第六次：产品改为 30px */
  border-radius: 999px;
  font-size: 12px;
  background: rgba(255, 255, 255, 0.08);
}
.team.t0 b {
  color: #ffd479;
}
.team.t1 b {
  color: #8fd0ff;
}

.tag {
  font-size: 11px;
  padding: 2px 7px;
  border-radius: 999px;
  background: rgba(240, 200, 92, 0.18);
  color: var(--gold);
}
/* 前端版本号：用来确认页面是不是最新版（不对就 Ctrl+F5） */
.tag.ver {
  font-family: ui-monospace, Consolas, monospace;
  background: rgba(255, 255, 255, 0.1);
  color: var(--ink-dim);
}

/* ───────────── 牌桌 ───────────── */

/*
 * 牌桌。
 *
 * **横向留白栏**：左右各 210px 给玩家4/玩家2 的头像 + 出的牌。
 *
 * **纵向没有留白了**：玩家3（上家）的头像"直接到顶"——
 * 用绝对定位挂在牌桌顶边，不再单独占一行。原来上边那一行会在
 * 头像上方留出一大块空白，看着像牌桌没铺满。玩家3 出的牌紧贴头像下方。
 *
 * 玩家1（自己）的头像和手牌在底部功能区，见 #bottom 那一节。
 */
#table {
  position: relative;
  display: grid;
  grid-template-columns: 104px 1fr 104px;
  grid-template-areas: 'left center right';
  /* 左右各 4px：末尾 .seat.pos-left{left:0}/.seat.pos-right{right:0} 靠这 4px
     把侧边头像落到"距屏幕边 40px"。原来宽屏是 0，所以桌面上头像是 36px。 */
  padding: 0 4px;
  min-height: 0;
  overflow: hidden;
  /*
   * 左右两家整体的纵向落点（负值 = 上移），**相对牌桌高度**。
   *
   * 定义在 #table 而不是 .pos-left/.pos-right 上：因为「我的出牌区」
   * （#center 里的 #myPlay）也要对到同一条线上（产品要求：
   * "玩家1的等待时间/出的牌上移到和玩家2、玩家3头像一条线"）。
   * 放在共同祖先上，改这一个值，左右两家和我的出牌区会一起动、不会错位。
   */
  --seat-drop: -20px;
}

/*
 * 玩家3：头像贴到**应用最上沿**，距离 2px（产品明确要求），
 * 并且略微偏右一点放（不居中）—— 居中的话会和牌桌中央的东西挤在一起，
 * 太靠左又会离出牌区太远。
 *
 * 头像 + 名字张数 横排成一行，出的牌排在**头像右侧**。
 *
 * left 的值调过两轮：33% → 40%（产品反馈"头像右移一点"）。
 * 注意这是**整组**（头像 + 出的牌）的中心，不是头像中心 ——
 * 牌在头像右侧，所以头像会落在整组中心的左边。想再右移只改这一个数。
 */
.seat.pos-top {
  position: absolute;
  top: 2px;
  left: 40%;
  transform: translateX(-50%);
  z-index: 3;
  flex-direction: row;
  align-items: flex-start;
  gap: 6px;
}
.seat.pos-top .seat-head {
  order: 1;
}
.seat.pos-top .seat-play {
  order: 2;
  margin-top: 6px;
}

/*
 * 座位容器与内部结构（三块各自独立摆放，不用 order 反复绕）：
 *
 *   .seat         flex 容器，方向按座位不同
 *     .seat-avatar  头像
 *     .seat-play    这家出的牌 / 「不出」
 *     .seat-meta    名字 + 张数 + 名次（小字）
 *
 * 摆牌位置（产品的明确要求，注意别再翻）：
 *   玩家3（上边）   → 牌在头像**下方**，名字张数在更下面
 *   玩家4（左边）   → 牌在头像**右边**
 *   玩家2（右边）   → 牌在头像**左边**
 *   玩家1（自己）   → 头像是底栏里独立的一列（见 #bottom），
 *                     摆在按钮右侧、贴底，不会被手牌压住
 *
 * 左右两家的名字/张数改成绝对定位挂在头像下方，
 * 否则它会参与横向排布、把牌的位置关系搞乱。
 */
.seat {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 0;
}
.pos-left {
  grid-area: left;
}
.pos-right {
  grid-area: right;
}

/* 玩家3 的具体排布见上面 .seat.pos-top 那一段（横排：头像在左、出的牌在右） */

/*
 * 玩家4（左边）和玩家2（右边）。
 *
 * 位置照素材：名字/张数在头像正下方，出的牌朝桌面中心排：
 *   玩家4（左边） → 牌在头像**右边**
 *   玩家2（右边） → 牌在头像**左边**
 *
 * **头像贴到应用左右边各 2px**（产品要求）：用绝对定位精确贴边，
 * 而不是靠 flex 居中 —— 居中时头像横向位置随列宽浮动，落不到 2px 上。
 * 纵向放在牌桌中线附近（top: 36%），照素材。
 */
.pos-left,
.pos-right {
  flex-direction: row;
  align-items: center;
  /*
   * 出牌区必须朝着**桌面中心**生长，绝不能长到头像底下。
   *
   * 头像已经绝对定位贴边了，所以"挨着头像内侧"这件事只能靠对齐方式保证：
   *   玩家4（左边）→ 出牌区靠**左**对齐（margin-left 让开头像），向右长 ✓
   *   玩家2（右边）→ 出牌区必须靠**右**对齐（margin-right 让开头像），向左长 ✓
   *
   * ⚠️ 这里原来两家都写了 flex-start。对右边那家来说，出牌区会从座位**左边缘**
   * 开始往右排 —— 牌一多就一直长到贴右边固定的头像底下，把头像整个盖住。
   * 产品反馈的"手机上玩家2的头像被牌遮挡"就是这个。见下面的 .pos-right 覆盖。
   */
  justify-content: flex-start;
  gap: 6px;
  /*
   * 左右两家整体下移量（负值 = 上移）。头像块和出牌区**共用同一个值**，
   * 这样"两者同一排"这个关系不会因为调这个值而被破坏。
   *
   * 调过三轮：0 → 16 → 0 → -20（产品反馈"头像上移"）。
   * 想再微调只改这一个数，头像和出牌区会一起动、不会错位。
   * （值本身统一在 #table 上定义，见那里的说明。）
   */
}
/* 玩家2（右边）：靠右对齐，出牌区向左生长，永远不会压到头像 */
.pos-right {
  justify-content: flex-end;
}
.pos-left .seat-head,
.pos-right .seat-head {
  position: absolute;
  /*
   * 头像块必须和出牌区**共用一条水平中线**。
   *
   * 出牌区是 flex 子项，被容器的 align-items: center 摆在 50%；
   * 头像块是绝对定位，所以这里要显式写 50%。
   *
   * 原来是 36%：实测头像块中心在 99px、出牌区中心在 138px，
   * **差了 39px** —— 看起来就是"牌挂在头像下面"，不是并排。
   * 再加上 --seat-drop 满足"整体下移一点"。
   */
  top: calc(50% + var(--seat-drop, 0px));
  transform: translateY(-50%);
}
.pos-left .seat-head {
  left: 31px; /* 第六次：产品改为 30px（4 网格 + 21 + 4.6 内缩 ≈ 29.6） */
}
.pos-right .seat-head {
  right: 31px; /* 第六次：产品改为 30px（同左） */
}
.pos-left .seat-meta {
  align-items: flex-start;
}
.pos-right .seat-meta {
  align-items: flex-end;
}
/*
 * 出的牌排在头像内侧（朝桌面中心那一侧）。
 * 头像宽 52px + 左边距 2px + 一点缝隙 = 64px 起排；
 * 玩家2 从右边算同样的距离。
 */
.pos-left .seat-play,
.pos-right .seat-play {
  /* 跟着头像块一起下移，保证两者始终同一排 */
  position: relative;
  top: var(--seat-drop, 0px);
}
.pos-left .seat-play {
  order: 1;
  margin-left: 64px;
}
.pos-right .seat-play {
  order: 2;
  margin-right: 64px;
}

.seat-meta {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1px;
}

/*
 * 头像 + 名字张数 这一组。
 * 左右两家要让"这一组"垂直居中，出的牌再排到它外侧，
 * 所以需要一个包裹元素把头像和文字绑在一起。
 */
.seat-head {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
}

/*
 * 头像 + 徽标的定位容器。
 *
 * 徽标必须相对**头像**定位，不能相对 .seat-head —— 后者是「头像 + 名字张数」
 * 整块，四个座位的宽度和 flex 方向都不一样（我这一家在横屏下是横排的），
 * 挂在 head 上徽标会落到文字角上（实测头像 x=10..44，徽标却跑到 x=79）。
 * 也不能放进 .avatar：那里有 overflow: hidden，徽标会被裁掉。
 */
.av-wrap {
  position: relative;
  display: inline-flex;
  flex: none;
}

/*
 * 剩余张数徽标（挂在头像角上的数字）。
 *
 * 样式照产品给的参考素材：**深蓝底 + 亮青描边的圆角方块 + 白色粗体数字**，
 * 挂在头像的**右上角**。
 *
 * 只在剩 ≤10 张时出现（判定见 remain.js / client.js 的 REMAIN_BADGE_MAX）：
 * 张数多的时候显示没意义，快打完时才需要盯着 —— 参考素材里也是这样，
 * 只有剩 6、7 张的那两家挂了数字，剩得多的没有。
 *
 * 两个座位要镜像到左上角（都是实测出来的，不是随手加的）：
 *   · 玩家2（右边）头像贴着屏幕右缘 → 挂右上角会出屏
 *   · 玩家1（我）横屏下是「头像 + 名字」横排 → 挂右上角会压住名字（截图可见）
 * 玩家3（上边）头像顶到 y=2 → top:-7 会跑到屏幕外，改成 top:0
 */
.seat-remain {
  position: absolute;
  top: -7px;
  right: -8px;
  min-width: 22px;
  height: 22px;
  padding: 0 4px;
  border-radius: 6px;
  background: linear-gradient(180deg, #2f66c4, #16336e);
  border: 2px solid #6fd0ff;
  color: #fff;
  font-size: 13px;
  font-weight: 800;
  line-height: 18px; /* 22 - 上下边框各 2 */
  text-align: center;
  font-variant-numeric: tabular-nums;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.5);
  z-index: 3;
}
.pos-right .seat-remain,
.pos-bottom .seat-remain {
  right: auto;
  left: -8px;
}
/* 上家的头像顶到 y=2，徽标再往上就出屏了 */
.pos-top .seat-remain {
  top: 0;
}

.seat .avatar {
  /*
   * 统一 **50 × 50**，四家（含我自己）一律这个尺寸，**不区分宽屏 / 窄屏**。
   *
   * 原来的取值是宽屏 52、窄屏 36、我自己的 44 —— 同一张牌桌上三种大小，
   * 看着不齐；而且"我的头像"比别家小一圈也没有理由。现在归一。
   */
  width: 50px;
  height: 50px;
  /* 圆角方形（照素材那种"卡片头"），不要正圆 */
  border-radius: 10px;
  display: grid;
  place-items: center;
  font-weight: 800;
  font-size: 22px;
  line-height: 1;
  background: rgba(255, 255, 255, 0.14);
  border: 2px solid transparent;
  transition: border-color 0.15s, box-shadow 0.15s;
  font-variant-numeric: tabular-nums;
  /* 头像图片：铺满整个方块；图片没加载出来时这里就是空的，
     底下那层背景色和文字（如果有）仍然看得到 */
  overflow: hidden;
}
.seat .avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  user-select: none;
  -webkit-user-drag: none;
}
/* 电脑座位：数字放大、底色更实，一眼能看出是几号电脑 */
.seat .avatar.npc {
  background: rgba(10, 40, 28, 0.55);
  border-color: rgba(255, 255, 255, 0.22);
  color: #d8f0e2;
}
.seat.team0 .avatar.npc {
  border-color: rgba(255, 212, 121, 0.45);
}
.seat.team1 .avatar.npc {
  border-color: rgba(143, 208, 255, 0.45);
}
.seat.turn .avatar {
  border-color: var(--gold);
  box-shadow: 0 0 0 4px rgba(240, 200, 92, 0.25);
  animation: pulse 1.1s ease-in-out infinite;
}
@keyframes pulse {
  50% {
    box-shadow: 0 0 0 10px rgba(240, 200, 92, 0.05);
  }
}
/*
 * 当前出牌方：头像加金色描边并呼吸闪烁。
 * 左右两家的头像在牌桌中部、上家的在顶部，都靠这个高亮来指出"轮到谁"。
 * 徽标只换成金边（不能换成金色底 —— 那是深色底白字，换了就读不清了）。
 */
.seat.turn .seat-remain {
  border-color: var(--gold);
  box-shadow: 0 0 0 2px rgba(240, 200, 92, 0.3), 0 2px 6px rgba(0, 0, 0, 0.5);
}
/* 名次/托管等小字在头像上方时不要被裁掉 */
.seat-head {
  overflow: visible;
}

.seat.team0 .avatar {
  background: rgba(255, 212, 121, 0.22);
}
.seat.team1 .avatar {
  background: rgba(143, 208, 255, 0.22);
}
.seat .sname {
  font-size: 12px;
  max-width: 128px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.seat .sname .bot {
  color: var(--ink-dim);
  font-size: 10px;
}
/*
 * 头像下面的那一行：以前是「剩 X 张」，现在换成**金豆**（产品要求）。
 * 样式照参考素材：深色胶囊 + 金色数字。
 *
 * ⚠️ 金豆系统**还没做**，服务端视图里没有这个字段，所以现在恒为 0（占位）。
 * 等规则定了，服务端在 seats 里补一个 beans 字段即可，这里不用改。
 */
.seat .sbeans {
  font-size: 10px;
  font-weight: 700;
  color: #ffe6a3;
  background: rgba(0, 0, 0, 0.42);
  border-radius: 999px;
  padding: 1px 8px;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.seat .srank {
  font-size: 11px;
  color: var(--gold);
  font-weight: 700;
}

/*
 * 名次标签（第一/第二/第三/第四）。
 *
 * 摆在**出牌区里、牌的左边**（DOM 里在 .seat-cards 之前，.seat-play 是 flex 行）。
 * 产品要求：出完牌之后名次显示在这里，不要缩在头像下面那行小字里。
 *
 * 金色底 + 深色字：和「金豆」那类信息区分开，一眼能看出是"走完了"的标记。
 * 牌被清掉之后它仍然留着（服务端下家一出牌就清上家的牌），
 * 所以这个标签必须能独立站着好看。
 */
.seat-rank {
  font-size: 12px;
  font-weight: 800;
  color: #3a2a05;
  background: linear-gradient(180deg, #ffe9a8, #f0c85c);
  border-radius: 6px;
  /* 还原 8px：之前被批量替换误伤成 40px（产品反馈"上游词条太长"） */
  padding: 2px 8px;
  white-space: nowrap;
  flex: none;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.45);
}
.seat.finished .avatar {
  opacity: 0.4;
}

/*
 * 中间：只留「轮到谁」的提示 + 我自己出的牌。
 *
 * 我自己出的牌放这里（不放自己面前）：自己面前那块已经被手牌占了，
 * 摆在中间才看得清自己刚出了什么。
 */
#center {
  grid-area: center;
  /*
   * ⚠️ 这里改成「绝对定位容器」了，不再用 flex 居中。
   *
   * 产品要求：**我的等待时间（倒计时圆环）和我出的牌，上移到和玩家2、玩家3
   * 头像同一条线**。原来这块是 flex 垂直居中的，圆环落在 y 158..212 ——
   * 比左右头像（114..150）低一截，而且**压在牌上**（手牌顶边 192）。
   *
   * 现在把提示文字和出牌区都按「左右头像的那条线」绝对定位：
   *   头像块的纵向中心 = 50% + --seat-drop（见 .pos-left .seat-head）
   *   头像**图片**高 36px、在头像块顶部，所以图片中心 = 那个值再往上 16.5px
   * 用同一个表达式，窗口高度或 --seat-drop 变化时两者始终对齐。
   */
  position: relative;
  min-height: 0;
}

/*
 * 我的倒计时 / 我出的牌：**中心与左右头像图片的中心对齐**。
 *
 * #myPlay 高 60（倒计时圆环高 54，正好居中），所以 top 要再减掉一半高度 30：
 *   50% + --seat-drop - 16.5 - 30 = 50% + --seat-drop - 46.5
 * 实测（视口 874×402）：top = 101.5，圆环 105..159、中心 132 ——
 * 和左右头像图片（114..150、中心 132）**完全同一条线**，
 * 而且离手牌顶边（166+）还有余量，不再压牌。
 *
 * ⚠️ `z-index: 20` 不能省。这里用了 `transform: translateX(-50%)` 做水平居中，
 * 而 **transform 会创建层叠上下文** —— 圆环自己的 z-index:20 就被关在
 * #myPlay 内部了；#myPlay 是 z-index:auto，会落到手牌（z-index 1~5）**下面**，
 * 于是手牌把倒计时盖住。实测症状：强制造出重叠后，
 * "圆环可见/隐藏"两张截图**一个像素都不差**（圆环压根没画上去）。
 * 给 #myPlay 自己一个 z-index，让它进到 #app 的层叠上下文里比。
 */
#myPlay {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  top: calc(50% + var(--seat-drop, 0px) - 46.5px);
  z-index: 20;
  height: 60px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-end;
}
#myPlay .cards {
  display: flex;
  align-items: flex-end;
}
#myPlay.hidden {
  display: none;
}

/* 提示文字：压在出牌区正上方（间距 10px，和原来的 gap 一致）。
 * 同样因为 transform 而自成层叠上下文，需要一个 z-index（见上）。 */
#centerHint {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  top: calc(50% + var(--seat-drop, 0px) - 74.5px);
  z-index: 21;
  white-space: nowrap;
}

/* 大号「不出」（参考素材里的金色大字） */
.pass-badge {
  font-size: 24px;
  font-weight: 800;
  color: var(--gold);
  letter-spacing: 3px;
  text-shadow: 0 3px 10px rgba(0, 0, 0, 0.6);
  animation: passIn 0.22s ease-out;
}
@keyframes passIn {
  from {
    transform: scale(0.7);
    opacity: 0;
  }
}

.hint {
  font-size: 14px;
  color: var(--gold);
  min-height: 18px;
  text-align: center;
}

/* ───────────── 各座位面前摆的牌 ───────────── */

/*
 * 为什么摆在各自座位前：四家的牌都堆到中央时，很难分辨哪手是谁出的。
 * 放在自己面前，位置本身就说明了归属。
 *
 * 牌统一**下方对齐**（align-items: flex-end），不要上方对齐 ——
 * 上对齐会让牌贴在头像顶部，跟头像之间留一大截空白，看着散。
 */
.seat-play {
  min-height: 44px;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  /* 名次标签和牌之间留一点缝（标签是 .seat-play 的直接子元素，牌在 .seat-cards 里） */
  gap: 5px;
}
.seat-cards {
  display: flex;
  align-items: flex-end;
  justify-content: center;
}
/*
 * 出牌区里摆的牌（座位旁那张、以及屏幕中间我出的牌）。
 *
 * 产品指定的最终尺寸：**宽 35、高 50**（35/50 = 0.7，和底牌 56/80 同一个比例）。
 * 字号不变（数字 16px、花色 15px），花色在数字**下面**且两者居中对齐。
 *
 * ⚠️ 三件事是**联动的**，改一个必须重算另两个：
 *   ① 数字距牌左边 2px（= 1px 边框 + .idx 的 left 1px，见 .card.played .idx）
 *   ② 右侧那张压过来时距数字右边也是 2px
 *   ③ 「10」的横向压缩比（见 .two）
 *
 * 换算：露出的那一条 = 2 + 「10」实宽 + 2 = 2 + 16.8 + 2 ≈ 21px
 *       → margin-left = -(35 - 21) = **-14px**（压盖 14/35 = 40%）
 * 牌从 38 缩到 35 之后，压盖比例会自动从 45% 变成 40% —— 因为露出的一条要
 * 保持 21px 才放得下「10」，这个宽度不随牌宽变。
 */
.seat-cards .card,
#myPlay .cards .card {
  position: relative;
  top: auto;
  width: 35px;
  height: 50px;
  /*
   * 出牌区的牌高，供大小王那条"行高 = 牌高 / 5"的公式用（见 .card.joker 段）。
   * 手牌那边的同一个变量由 applyHandLayout 写在 #hand 上；出牌区在这里声明，
   * 于是**同一份 JOKER 规则**在两个位置都能自动算出合适的字号 —— 这就是"一种方案"。
   */
  --card-h: 50px;
  /*
   * 压盖按**「A」的宽度**算（产品要求：字母距牌左边 2px，右边那张牌距字母右边也 2px）：
   *   露出的那一条 = 2 + 「A」实宽 11.1 + 2 ≈ 15px
   *   → margin-left = -(35 - 15) = -20px（压盖 20/35 = 57.1%）
   * 单字符里 A/K 最宽（都是 11.1px），按 A 定就够；「10」比它宽，会被右边那张压掉一点 ——
   * 产品明确说"10 盖住显示不全先不管"。
   */
  margin-left: -20px;
}
.seat-cards .card:first-child,
#myPlay .cards .card:first-child {
  margin-left: 0;
}
/* 右边的牌压住左边那张：z-index 递增（DOM 顺序本来也是后面的在上，显式写出来更稳） */
.seat-cards .card:nth-child(2),
#myPlay .cards .card:nth-child(2) {
  z-index: 2;
}
.seat-cards .card:nth-child(3),
#myPlay .cards .card:nth-child(3) {
  z-index: 3;
}
.seat-cards .card:nth-child(4),
#myPlay .cards .card:nth-child(4) {
  z-index: 4;
}
.seat-cards .card:nth-child(n + 5),
#myPlay .cards .card:nth-child(n + 5) {
  z-index: 5;
}
/*
 * 出牌区的角标字号。
 *
 * 产品先要求"和底牌一样大"，之后又单独指定了出牌区的值：
 *   · 数字 **16px**（底牌是 18px）
 *   · 花色 **15px**（底牌是 17px）
 * 所以这里不再共用底牌那两个变量，写成本区的固定值 ——
 * 共用一份的前提是"两边要一样"，现在产品明确要不一样，硬绑在一起反而会打架。
 *
 * 16px 字号下「10」的实测总宽约 20px（18px 时是 22.2px），
 * 正是产品给的目标值；`played-cards-check` 会断言这一条（18~21px）。
 *
 * 排列仍然是"数字在上、花色在下、两者居中对齐"。
 */
.seat-cards .card.played .idx .r,
#myPlay .cards .card.played .idx .r {
  font-size: 16px;
  line-height: 1;
  letter-spacing: 0;
}
/*
 * 出牌区的角标：**和手牌统一** —— 点数 12px 宽、花色 12px 宽、「10」16px 宽。
 *
 * 出牌区的字号是 16px（手牌 17px），所以两边的换算比不一样，但**画出来的宽度一样**。
 */
.card.played .idx .r.two,
#myPlay .cards .card.played .idx .r.two {
  /*
   * 出牌区的「10」：**14px 宽**（产品口径；原来 16px）。
   *
   * ⚠️ 必须**同时**给格子宽 + 缩放，两个数字都要对：
   *
   *   · 16px 字号下「10」自然宽约 19.75px → 压到 14px：scaleX = 14/19.75 ≈ 0.71；
   *   · `width: 14px` 让**参与布局的盒子**正好等于**画出来的宽度**。
   *
   * 为什么两条都必要（花色对不齐就是这个原因）：
   * `.idx` 是 `flex-direction: column; align-items: center`，**居中按的是布局盒**，
   * 而 transform 不改变布局盒。只写 scaleX 不写 width 时：
   * 盒子仍是 19.75px、画出来却是 16px —— 于是花色按 19.75px 居中，
   * 比「10」的视觉中心**偏右约 1.85px**（产品实测就是看到这个歪）。
   *
   * 加上 width 之后盒子 = 14px = 画出来的宽度，两边中心重合，花色自然对齐。
   * `transform-origin: left` 保证左边缘不动（右边缘才不会被右边那张压掉）。
   *
   * ⚠️ 量宽度时要用 `offsetWidth`（布局盒 = 14px），**不要**用
   * `getBoundingClientRect()` —— rect 是"缩放后的盒子"（14 × 0.71 ≈ 9.9px），
   * 既不是布局宽也不是墨迹宽，会误判。
   */
  width: 14px;
  transform: scaleX(0.71);
  transform-origin: left center;
  /*
   * ⚠️ 必须有 nowrap：`.idx` 是 flex 容器，子元素会被 blockify，于是 `width: 14px`
   * 变成一个**会折行的块宽** —— 「10」有 19.75px 自然宽，会被折成两行，
   * 而折行后最宽的那行仍是 14px，**宽度断言照样通过**，只有高度会翻倍。
   * （检查里补了一条"「10」的高度必须和单字符一样"来守它。）
   */
  white-space: nowrap;
}
/*
 * 出牌区的普通点数：**统一 12px 宽的格子**（和手牌同一条规则）。
 *
 * ⚠️ 这里不能靠 scaleX 凑：字形本身宽窄不同（「7」9.9px、「A」11.1px），
 * 乘同一个比例只会得到 10.7 / 12.0 这种参差的结果。
 * 真正"统一"的做法和手牌一样 —— 给一个 **12px 的固定格子**，
 * 每个字形在自己的格子里左对齐，于是所有点数的**起始位置**和**占用宽度**都一致。
 *
 * 格子宽不影响布局（.idx 是绝对定位、文字在格子里左对齐），
 * 所以"数字距牌左边 2px"照样成立（检查里有断言）。
 */
.seat-cards .card.played .idx .r:not(.two),
#myPlay .cards .card.played .idx .r:not(.two) {
  width: 12px;
}
.seat-cards .card.played .idx .s,
#myPlay .cards .card.played .idx .s {
  font-size: 15px;
  line-height: 1;
  /* 花色紧贴在数字下面（底牌是并排，这里是上下，所以要把行距收掉） */
  margin-top: -2px;
  /*
   * 出牌区花色的宽度：**10px**（产品最新口径，原来要求 12px）。
   *
   * 实测：15px 字号下花色字形只有 **10.26px** 宽（比数字窄不少），
   * 所以这里不是设格子宽，而是用 scaleX 精确调到 10px：
   *   scaleX = 10 / 10.26 ≈ 0.975，取 **0.97**（实测绘制宽 9.95px）
   *
   * 两个讲究：
   *   · 只改宽度、**不动字号** → 高度不变（和「10」的处理同一个思路）；
   *   · `transform` **不参与布局**，所以缩放不会把数字推走 ——
   *     "数字距牌左边 2px"那条几何照样成立（检查里有断言）。
   */
  transform: scaleX(0.97);
  transform-origin: center center;
}

/*
 * 牌型小牌子（照参考图）：竖排、蓝框、贴在最后一张牌的**左下角、数字下面**。
 *
 * ⚠️ 两个坑：
 *   · 不能从牌顶就开始：角标（数字 + 花色）也贴在左上角，牌子会把它盖住；
 *   · 牌缩小到 38×53 之后，角标占到 68% 的高度，牌子只剩底部约 20px ——
 *     两个字（四炸/顺子…）放得下，三个字（三带二/天王炸/同花顺）要缩一档，
 *     否则会顶出牌外被裁掉，所以有 `.long` 这个修饰类。
 */
.seat-cards .play-kind,
#myPlay .cards .play-kind {
  position: absolute;
  left: 1px;
  top: 64%;
  bottom: 2px;
  width: 13px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid rgba(96, 132, 214, 0.9);
  border-radius: 3px;
  background: rgba(255, 255, 255, 0.92);
  color: #40589c;
  font-size: 8px;
  font-weight: 800;
  line-height: 1;
  letter-spacing: 0;
  writing-mode: vertical-rl;
  text-orientation: upright;
  z-index: 9;
}
.seat-cards .play-kind.long,
#myPlay .cards .play-kind.long {
  font-size: 6.5px;
  letter-spacing: -0.2px;
}
/*
 * ⚠️ 这里原来有一条 `.seat-cards .card.joker .idx.joker-idx { font-size: 11px }`，
 * 是上一版"出牌区把 JOKER 降级成 JO"时定的补偿字号。它比新的
 * `.card.joker .idx.joker-idx`（0,3,0）更具体（0,4,0），会把
 * "行高 = 牌高 / 5、字号 = 行高 × 0.8"那条公式**整个盖掉** —— 已删除。
 * 现在出牌区的字号完全由 --card-h: 50px 代入同一个公式得到（8px / 行高 10px）。
 */

.seat-pass {
  font-size: 17px;
  font-weight: 800;
  color: var(--gold);
  letter-spacing: 2px;
  text-shadow: 0 2px 6px rgba(0, 0, 0, 0.5);
}

/*
 * 回合计时：一个圆环，圈里是剩余秒数，圈本身随进度收掉。
 *
 * 出牌区里显示，轮到谁就出现在谁的出牌区。电脑的圈小一点（2 秒，一闪而过），
 * 真人的圈大一点（30 秒，看得清）。最后 5 秒整圈变红。
 */
.turn-timer {
  position: relative;
  width: 54px;
  height: 54px;
  display: grid;
  place-items: center;
  flex-shrink: 0;
  /*
   * ⚠️ 必须在**最上层**，不能被牌遮挡（产品明确要求）。
   *
   * 为什么会遮：倒计时挂在 #center（我）或 .seat-play（其他人）里，这两处
   * 都**没有 z-index**（等价于 0）；而手牌卡片是 z-index 1~5，
   * 且 #handWrap 在 DOM 里排在 #table 之后 —— 手牌一高（一列叠满）就正好
   * 盖住中间那个圆环，轮到谁反而看不见了。
   *
   * 20 的取法：要高于手牌卡片（1~5）和 #actions（8），
   * 但要低于 toast（50）和诊断/引导层（900）。
   */
  z-index: 20;
}
.turn-timer svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  /* 从 12 点方向开始画 */
  transform: rotate(-90deg);
}
.turn-timer circle {
  fill: none;
  stroke-width: 4;
  stroke-linecap: round;
}
.turn-timer .track {
  stroke: rgba(255, 255, 255, 0.18);
}
.turn-timer .bar {
  stroke: var(--gold);
  transition: stroke-dashoffset 0.15s linear;
}
.turn-timer .n {
  position: relative;
  font-size: 19px;
  font-weight: 800;
  line-height: 1;
  color: var(--gold);
  font-variant-numeric: tabular-nums;
}
/*
 * 电脑（含托管）的圈略小一点做区分 —— 但**时限同样是 30 秒**，
 * 圈里也是两位数字，所以不能太小。
 */
.turn-timer.npc {
  width: 48px;
  height: 48px;
}
.turn-timer.npc .n {
  font-size: 17px;
}
/* 最后 5 秒：整圈变红 */
.turn-timer.urgent .bar {
  stroke: #ff6b6b;
}
.turn-timer.urgent .n {
  color: #ff8f8f;
}

/*
 * ──────────── 装饰层不吃点击 ────────────
 *
 * 产品反馈两条，同一个根因：
 *   · 「选底牌时被其他家出牌挡住，会选不上」
 *   · 「倒计时压着的那张牌选不到」
 *
 * 别人出的牌（`.seat-cards`）和倒计时圆环（`.turn-timer`）都是**画在上层**的
 * 纯展示元素，但它们照样参与命中测试 —— 手指落在那块区域时事件被它们接走，
 * 底下的手牌一张也收不到。
 * 倒计时尤其明显：上一轮为了不被牌挡住把它提到了 z-index 20，
 * 结果它连点击也一起挡了。
 *
 * 这几块**没有任何交互**（不能点、不能拖），所以统一下掉 pointer-events，
 * 让触摸**穿透**到下面的手牌。比调 z-index 干净得多 —— 视觉层级一动都不用动，
 * "倒计时要在最上层" 和 "牌要能点得到" 这两件事就不再打架了。
 */
.seat-play,
.seat-pass,
.seat-cards,
.turn-timer,
#center,
#myPlay {
  pointer-events: none;
}

/* ───────────── 牌 ───────────── */

/*
 * 牌面（比例照素材）：
 *
 *   手牌： 点数在左、花色在右，中间一个大花色
 *     ┌────────┐
 *     │ A♥     │
 *     │   ♥    │
 *     └────────┘
 *
 *   打出去的牌（.played）：点数在上、花色在下方，**没有中间大花色**
 *     ┌────┐
 *     │ A  │
 *     │ ♥  │
 *     └────┘
 *
 * 手牌的字号由 JS 按牌宽注入（--idx-rank / --idx-suit / --suit-big），
 * 比例是点数 31%、角花色 29%、中间大花色 46%。
 */
.card {
  position: absolute;
  top: 0;
  width: var(--card-w);
  height: var(--card-h);
  border-radius: var(--radius);
  background: linear-gradient(180deg, #ffffff, #f4f6f4);
  border: 1px solid #b9c2ba;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.38);
  color: #1b1b1b;
  font-weight: 700;
  transition: transform 0.11s ease, box-shadow 0.11s ease, border-color 0.11s ease;
  overflow: hidden;
}

/*
 * 手牌角上的「点数 + 花色」（row：点数在左、花色在右）。
 *
 * 产品给的是一组精确数值（几轮迭代后**手牌和出牌区统一**）：
 *
 *   普通点数（除大小王和 10）：点数宽 **12px**，花色宽 **12px**
 *   「10」                   ：点数宽 **16px**
 *
 * 布局上的配平（决定了花色能不能排成一列）：
 *   普通：4(距左) + 12(点数) + 3(间距) = 19
 *   「10」：1(距左) + 16(点数) + 2(间距) = 19     ← 一样
 *
 * ⚠️ 中间试过把「10」的点数格缩到 15px，那时必须把间距加到 3px 才能配回 19；
 * 现在产品定回 **16px**，间距也就跟着回到 **2px**（数值正好是最初那一组）。
 * 改任何一个都要重新配平，否则花色会参差不齐（检查里有"花色必须对齐"的断言）。
 *
 * 大小王不走这条路径（它们是 .joker-idx + .joker-art，见下面 .card.joker 段），
 * 所以这里怎么写都不会动到王。
 */
.card .idx {
  position: absolute;
  top: 2px;
  left: 3px;
  display: flex;
  flex-direction: row; /* 点数在左、花色在右 */
  align-items: baseline;
  gap: 3px;
  line-height: 1;
  z-index: 2;
}
/* 「10」：格子更宽、更贴左边、离花色更近 —— 见上面的配平说明 */
.card.ten .idx {
  left: 0;
  gap: 2px;
}
/*
 * ⚠️ 12px 的格子宽**只给手牌**（:not(.played)）：出牌区那边靠"每张露 15px"的版式定位，
 * 给 .r 加固定宽会被 scaleX 再乘一次 —— 实测出牌区「10」从 16px 变成 9.7px（12 × 0.81）。
 */
.card:not(.played) .idx .r {
  font-size: var(--idx-rank, 19px);
  letter-spacing: -0.5px;
  width: 12px;
}
/*
 * 手牌里的「10」：格子 **16px**。
 *
 * ⚠️ 17px 字号下「10」的**自然墨迹宽约 20.9px**，塞进 16px 的格子会溢出去压到花色。
 * 所以和出牌区用同一招：**横向压缩、不动字号**（高度不变）。
 * 压缩比 = 16 / 20.9 ≈ 0.766，取 0.77，压完正好 16px。
 */
.card:not(.played).ten .idx .r {
  width: 16px;
  transform: scaleX(0.77);
  transform-origin: left center;
}
/*
 * 手牌的花色：**统一 12px 宽**。
 *
 * 16px 字号下花色字形约 11px 宽，所以这里要**拉宽**一点：scaleX(12/11) ≈ 1.09。
 * 用 transform 的原因和出牌区一样：它不参与布局，拉宽不会把点数或列位置挤动。
 */
.card:not(.played) .idx .s {
  font-size: var(--idx-suit, 18px);
  transform: scaleX(1.09);
  transform-origin: center center;
}
.card.red {
  color: #cc2222;
}

/* 牌中间的大花色（只有手牌有） */
.card .suit-big {
  position: absolute;
  left: 50%;
  top: 54%;
  transform: translate(-50%, -50%);
  font-size: var(--suit-big, 28px);
  line-height: 1;
  opacity: 0.9;
  z-index: 1;
}

/*
 * 打出去的牌：**花色移到点数下方，并且和点数居中对齐**。
 *
 * 用 `align-items: center` 让两个子元素在**彼此之间**居中
 *（.idx 的宽度 = 最宽的那个子元素，也就是点数），
 * 于是「10」下面那个花色会落在数字的正中，而不是贴左边。
 *
 * ⚠️ `left` 是相对**内边距盒**算的，牌本身还有 1px 边框，所以：
 *     数字距牌左边 = 1px(边框) + left
 * 产品要求"数字距离左边牌边 2px" → `left: 1px`（原来是 3px，等于 4px，太远）。
 *
 * 产品另一条要求"右侧那张压过来时，距数字右边也是 2px"，是这样凑出来的：
 *   露出的那一条 21px（压盖 45%）
 *   − 数字距左 2px
 *   − 「10」的实宽 16.8px（最宽的点数，见 .two 的横向压缩）
 *   = 2.2px   ← 正是 2px 左右
 * 所以 `left: 1px` 和压盖 45% 是**一起**决定这个 2px 间距的，改一个要重算另一个。
 */
.card.played .idx {
  top: 2px;
  left: 1px;
  flex-direction: column;
  align-items: center;
  gap: 0;
}
.card.played .idx .r {
  line-height: 1;
}
.card.played .idx .s {
  margin-top: -1px;
  line-height: 1;
}
.card.played .suit-big {
  display: none;
}

/*
 * 大小王牌面 —— 左边**竖排 JOKER**（占牌高 80%）+ 中间**小丑图案**，**牌面纯白**。
 *
 * 配色只走「字 + 图案」，**不做任何背景填充**（产品明确要求：大王不要红色填充、底部纯白）：
 *   大王 = 红字 #cc2222 + 红图案 + 淡红描边
 *   小王 = 黑字 #1b1b1b + 黑图案 + 淡灰描边
 *
 * 所有尺寸都从 `--card-h`（这张牌的实测高度）算出来，所以**同一份规则**在手牌和
 * 出牌区都自动得到合适的值 —— 这就是"手牌和出牌区用同一种方案"：
 *
 *     字母块高 = 牌高 × 0.80      ← 只占八成，上下各留一成，字不再顶到底（也就不溢出）
 *     每行行高 = 字母块高 / 5
 *     字号     = 行高 × 0.80
 *     图案边长 = 牌高 × 0.52
 *
 *   手牌（--card-h = 80）：行高 12.8、字号 10.24、图案 41.6
 *   出牌区（--card-h = 50）：行高 8、字号 6.4、图案 26
 *
 * ⚠️ 换行**只能**由这里控制：client.js 里千万不要写 idx.style.whiteSpace，
 *    inline 样式优先级高于样式表，会把出牌区的规则顶掉（上一版踩过）。
 */
.card.joker {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  justify-items: center;
  /* 纯白牌面：显式写出来，免得以后有人给 .card 加底色时把王也一起染色 */
  background: #fff;
}
.card.joker .idx.joker-idx {
  grid-column: 1;
  grid-row: 1;
  writing-mode: horizontal-tb;
  text-orientation: mixed;
  white-space: pre-line;
  line-height: calc(var(--card-h, 80px) * 0.8 / 5);
  font-size: calc(var(--card-h, 80px) * 0.8 / 5 * 0.8);
  letter-spacing: 0;
  text-align: center;
  font-weight: 800;
  align-self: center;
}

/*
 * 中间的小丑图案
 *
 * `left: 3px`（配合 position: relative）是产品要求："中间图案和左边字母太靠在一起了，
 * 图案右移 3px，左侧文字不动"。用相对定位而不是 margin：
 * 这个元素是 `justify-self: center` 居中的，加 margin-left 只会把中心推右 **1.5px**（一半），
 * 相对定位才是实打实的 3px，而且完全不影响左边那列文字的布局。
 */
.card.joker .joker-art {
  grid-column: 2;
  grid-row: 1;
  justify-self: center;
  align-self: center;
  position: relative;
  left: 3px;
  width: calc(var(--card-h, 80px) * 0.52);
  height: calc(var(--card-h, 80px) * 0.52);
  fill: currentColor;
  pointer-events: none;
}

/*
 * 配色**只走字和图案**，牌面的边框保持默认（和其他牌完全一样）。
 * 之前给王加过淡红/淡灰描边，产品反馈"大王的牌边线怎么改了红色，用默认的和其他牌一样" —— 已去掉。
 */
.card.joker.big .idx.joker-idx,
.card.joker.big .joker-art {
  color: #cc2222;
}
.card.joker.small .idx.joker-idx,
.card.joker.small .joker-art {
  color: #1b1b1b;
}



/*
 * 手牌布局 —— 严格照素材的摆法：
 *   同点数一列（列内叠放，每张露一条点数），不同点数一列一列往右排。
 *
 * 列内的叠法（这一点改了好几轮才对）：
 *   · 第一张牌在**最下层**（z 最高）、整张可见，它的**底边**就是整叠的底线
 *   · 后面的牌依次**往上**错开一个步进，各露出上面一条（正好是点数信签）
 *   · 结果就是"底边对齐"，所有点数都能看清
 *
 * #hand 是定位容器；每一列 .hand-col 绝对定位（left/bottom 由 JS 算），
 * 列内的牌也是绝对定位（top 由 JS 的列内步进算）。
 *
 * #hand 自身也是绝对定位（见下面的手牌锚点一节），底边贴在分界线上方 2px；
 * JS 再用 top 做整体上移（牌多时往上排到窗口上方）。
 */
#hand .card {
  cursor: pointer;
  /*
   * 拖动连选要的几个设置：
   *   touch-action: none —— 手指按在牌上滑动时不要触发页面滚动/缩放，
   *                        否则拖动会被浏览器当成滚动手势抢走
   *   user-select: none —— 拖动时不要选中文字（那会弹出复制菜单）
   */
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}
/*
 * ⚠️ 这里原来有一条 `#hand .card.selectable:hover { border-color: #6fc7ff }`（悬停改描边），
 * **已删除**，原因有两个：
 *   1. 产品要求"选中牌的时候不要有边颜色改变，只要改填充" —— 触摸设备上
 *      `:hover` 点一下就**粘住**不走了，看起来就是"选中的牌边框变蓝了"，
 *      正是被报上来的现象（检查里量到的 rgb(111,199,255) 就是它）；
 *   2. 手机本来就没有悬停，这条规则只在调试时起作用，删掉没有任何损失。
 */
#hand .hand-col {
  position: absolute;
  bottom: 0;
}
/*
 * 选中的牌变灰 —— **不做位移**（产品要求：选中不要往前推）。
 * 用 greyscale + 压暗表示"这张已选中"，配一圈金色描边方便确认。
 * 注意描边和灰化都作用在整列上：同点数的牌是一叠，
 * 一起变灰才符合"选中了这个点数"的直觉。
 */
/*
 * 选中的牌：**只加深牌面的填充**。
 *
 * 产品要求（两轮迭代）：
 *   1. "选中填充变为灰色就行，字母、花色不用变"
 *   2. "不要有边颜色改变，只要改填充就行，填充颜色加深一点"
 *
 * 所以这里**不设 border-color、也不加金色外圈**（那两样都是"边颜色改变"）——
 * 边框和阴影全部走 .card 的默认值，和其他牌完全一致。
 * 之前用过 `filter: grayscale(1) brightness(0.82)`，那是把整张牌（连同点和花色）
 * 一起去色压暗，红桃会变灰 —— 已废弃。
 */
#hand .card.selected {
  background: linear-gradient(180deg, #c2c2c2, #a8a8a8);
}

/*
 * ⚠️ 这里原来有一条 `#hand .card.hinted { border-color:#4fc3f7; box-shadow: 0 0 0 2px rgba(79,195,247,.9) }`
 *    —— **「提示」选中牌时那条天蓝色描边 + 外圈就是它**（产品反馈：「点提示选牌不要把牌带上边线，
 *    和手动选牌一样」）。已删除，理由：
 *      · 「提示」在 client.js 里本来就是**直接选中**（`selected.clear()` + 把推荐的那几张加进
 *        `selected`，见 socket 的 `hint` 分支），也就是说它已经走的是**手动选牌那套 `.selected`**
 *        （只加深填充、不动边框）；
 *      · 上面那条 `.hinted` 是在那之上**又加**了一层蓝边 + 外发光 —— 于是同一手牌，
 *        「点提示选出来的」和「手动点出来的」长得不一样（一个带边线、一个不带）；
 *      · 所以正确做法是**删掉这条额外样式**，让两种选中**完全同源**（都是 `.selected`）。
 *    `.hinted` 这个类名仍然由 client.js 打在牌上（只作为"这几张是提示给的"的可观测标记，
 *    没有任何视觉），给自检脚本读 DOM 用 —— 谁要再给它加边线/描边，就是把这个 bug 加回来。
 *
 *    ⚠️ 本文件末尾（补丁区）还抄过一条一模一样的规则，两处**都已删除**；
 *       以后要加样式请只留一处，别在补丁区再抄一遍。
 */

/* ───────────── 底部操作 ───────────── */

/*
 * 底部功能区。
 *
 * 产品要求：
 *   1. **不要背景色**，和上面的牌桌连成一体（之前是深色块，像切开的两个区域）
 *   2. 功能区上方画**一条分界线**，手牌的下边线对齐在这条线上方 2px
 *   3. 牌的大小**固定**，牌多就往上排
 *
 * 所以这里手牌区只是一个"贴住分界线的锚点"，高度给个够用的固定值就行；
 * 手牌比它高时往上溢出（overflow: visible），不去改变牌的尺寸。
 */
/*
 * 底部：手牌 + 我出的牌 + 操作按钮 + 我的头像。
 *
 * 布局要点（照素材）：
 *   · **分界线在最下面**（--footer-bottom: 56px，就在按钮上方一点），
 *     不是画在整块区域的顶上 —— 手牌和头像都会**越过**这条线，
 *     看起来才像"牌摆在桌子上"，而不是被一条线截断
 *   · 手牌下边线贴在分界线上方 2px（JS 按分界线位置算 top）
 *   · 我的头像绝对定位在左下角、贴到应用下沿 2px
 *   · 操作按钮浮在右下角（absolute），但**左边要让开 --me-col 那一列**，
 *     否则会和我的头像撞上
 */
#bottom {
  /* 分界线距底边的距离：正好在按钮上方一点 */
  --footer-bottom: 30px;
  /* 最左留给我的头像的宽度 */
  --me-col: 60px;
  position: relative;
  /* 高度 = 手牌往上排的起点 + 分界线以下的余量 */
  height: 64px;
  display: block;
  padding: 0 14px;
  background: none;
  border-top: none;
  overflow: visible;
}

/* 分界线：画在底部按钮上方一点 */
#bottom::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: var(--footer-bottom);
  height: 1px;
  background: rgba(255, 255, 255, 0.16);
  pointer-events: none;
}

/* 我的头像：最左一列，**贴到应用下沿 2px**（产品明确要求），要越过分界线 */
#bottom .pos-bottom {
  position: absolute;
  left: 8px;
  bottom: 2px;
  z-index: 6;
}
/*
 * 我自己出的牌**不画在左下角**（那块已经被手牌占满），改画在牌桌中间，
 * 见 #myPlay。所以这里的出牌区平时隐藏。
 *
 * 但**两种例外**不能一起藏掉：
 *   · 倒计时：轮到我时圆环就挂在这个出牌区里，藏了就看不到自己的倒计时
 *   · 名次标签：我自己走完了要把「第一」显示出来（和其他三家一致）
 * 所以用两条 `:not(:has(...))` 把它们排除在外。
 */
#bottom .pos-bottom .seat-play:not(:has(.turn-timer)):not(:has(.seat-rank)),
#bottom .pos-bottom .seat-pass {
  display: none;
}

/*
 * 手牌锚点。
 *
 * 位置和高度都由 JS 算好后写成内联样式：
 *
 *     handTop = 分界线y - 2 - 手牌高度
 *
 * 也就是「手牌底边落在分界线上方 2px」，牌多时自动往上排。
 * 分界线现在画在底栏**下面**（按钮上方一点），所以手牌会越过原本
 * 那一整块区域 —— 这正是素材里的效果。
 */
#handWrap {
  position: relative;
  width: 100%;
  align-self: start;
  /* 牌多时往上溢出到牌桌上 */
  overflow: visible;
}

/*
 * 手牌本体：高度 = 牌高 + 叠放高度（JS 设），底部用内联 top 定位。
 * 宽度由 JS 按列数算，水平居中。
 */
#hand {
  position: absolute;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  height: var(--card-h);
}
/* 横向排不下时改为左对齐 + 横向滚动 */
#handWrap.scrollable #hand {
  left: 0;
  transform: none;
}

/*
 * 操作区（选牌提示 + 按钮）。
 *
 * 浮在底栏右下角（absolute），**不参与布局**：
 *   · 这样它不会把底栏撑高，手牌和头像可以自由越过它
 *   · 它是浮层，所以 z-index 要高，保证按钮永远点得到
 *   · `left: var(--me-col)` 让开最左那一列给我的头像，否则会撞上
 */
#actions {
  position: absolute;
  right: 40px; /* 四周统一 40px */
  bottom: 6px;
  left: var(--me-col);
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 6px;
  z-index: 8;
  pointer-events: none;
}
#actions .btn-row,
#actions .btn {
  pointer-events: auto;
}
/* 选牌提示做成一行，不要占两行 */
.sel {
  font-size: 12px;
  color: var(--ink-dim);
  max-width: 38vw;
  text-align: right;
  height: 16px;
  line-height: 16px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/*
 * 没选牌时不显示任何提示（产品要求）。
 *
 * 用 display: none 而不是留空字符串：留空的话这个 div 仍然占着 16px 高，
 * 在 #actions 里会把按钮往左挤 —— 明明没内容却影响布局。
 */
.sel.empty {
  display: none;
}
.btn-row {
  display: flex;
  gap: 8px;
}

.btn {
  border: 1px solid rgba(255, 255, 255, 0.22);
  background: rgba(255, 255, 255, 0.1);
  color: var(--ink);
  font-size: 13px;
  font-weight: 600;
  padding: 9px 20px;
  border-radius: 9px;
  cursor: pointer;
  transition: filter 0.12s, transform 0.06s;
  white-space: nowrap;
}
.btn:hover:not(:disabled) {
  filter: brightness(1.18);
}
.btn:active:not(:disabled) {
  transform: translateY(1px);
}
.btn:disabled {
  opacity: 0.34;
  cursor: not-allowed;
}
.btn.primary {
  background: linear-gradient(180deg, #2f9e63, #1d7a49);
  border-color: #35b673;
}
.btn.accent {
  background: linear-gradient(180deg, #d8a83a, #b8862a);
  border-color: var(--gold);
  color: #241c05;
}
.btn.ghost {
  background: transparent;
}
.btn.small {
  padding: 4px 10px;
  font-size: 12px;
  border-radius: 7px;
}
.btn.off {
  opacity: 0.5;
}

.hidden {
  display: none !important;
}

/* ───────────── 大字横幅（出完牌的名次 / 接风）───────────── */

/*
 * 挂在 #app 里面（见 client.js 的 showBanner）—— 强制横屏是把 #app 整体转 90°，
 * 挂到 body 上会跟着屏幕方向歪过去。
 *
 * z-index 700：压住牌、倒计时、toast(50)，但让给设置面板(800)。
 * pointer-events: none —— 只是个动画，绝不能挡住点牌（这条踩过坑：
 * 倒计时圆环当初就是因为会吃点击，才不得不加 pointer-events: none）。
 */
.banner {
  position: fixed;
  left: 50%;
  top: 34%;
  z-index: 700;
  pointer-events: none;
  font-size: 40px;
  font-weight: 900;
  letter-spacing: 4px;
  white-space: nowrap;
  opacity: 0;
  transform: translate(-50%, -50%) scale(0.6);
  color: var(--gold);
  text-shadow: 0 4px 18px rgba(0, 0, 0, 0.8), 0 0 30px rgba(240, 200, 92, 0.55);
}

/* 名次：金色，砰地弹出来，然后**飞向那个玩家的头像**（见 client.js 的 showBanner） */
.banner.finish.show {
  animation: bannerPop 1.9s cubic-bezier(0.25, 0.9, 0.3, 1) forwards;
}
/*
 * 名次横幅的关键帧。
 *
 * 产品要求：**不要只在屏幕中间闪一下**（不醒目），要"从中间出现、缩小到各自
 * 玩家的头像旁边"。所以 0~46% 在中间"砰"地弹出来并停一下让人看清，
 * 之后缩小着飞到落点淡出。
 *
 * 落点用 `--fly-x/--fly-y` 注入（由 JS 按目标头像的实时位置算），
 * 所以**这两个变量必须在加 .show 之前就设好** —— 关键帧一开头就要用。
 * 注意：位移是**元素自身坐标系**里的，而强制横屏给 #app 加了 rotate(±90deg)，
 * JS 会把视觉位移反算回这个坐标系（见 flyOffsetTo）。
 */
@keyframes bannerPop {
  0% {
    opacity: 0;
    transform: translate(-50%, -50%) scale(0.5) rotate(-7deg);
  }
  16% {
    opacity: 1;
    transform: translate(-50%, -50%) scale(1.14) rotate(2deg);
  }
  30% {
    opacity: 1;
    transform: translate(-50%, -50%) scale(1) rotate(0deg);
  }
  /* 停一下：让人看清是谁、第几游 */
  46% {
    opacity: 1;
    transform: translate(-50%, -50%) scale(1) rotate(0deg);
  }
  100% {
    opacity: 0;
    transform: translate(calc(-50% + var(--fly-x, 0px)), calc(-50% + var(--fly-y, 0px))) scale(0.24)
      rotate(0deg);
  }
}

/*
 * 每局开始的「本局打X」—— 用**独立的 level kind**，不要复用 finish：
 * finish 那条横幅的检查是按"class 里没有 jiefeng 就当名次"来分类的，
 * 复用会让「本局打X」被当成名次横幅（实测把 finish-jiefeng-check 带崩过一次）。
 *
 * 节奏是产品定的：**中间停留 0.5 秒**，然后才缩小飞到档位牌上。
 * 关键是"停留"要从它**完全显示出来**之后算起 —— 所以弹入段压到前 20%（1.6s 里的 0.32s），
 * 20%→52% 保持不动（0.5s），52% 之后再飞。
 */
.banner.level {
  color: var(--gold);
  text-shadow: 0 4px 18px rgba(0, 0, 0, 0.8), 0 0 30px rgba(240, 200, 92, 0.55);
}
.banner.level.show {
  animation: levelPop 1.6s cubic-bezier(0.25, 0.9, 0.3, 1) forwards;
}
@keyframes levelPop {
  0% {
    opacity: 0;
    transform: translate(-50%, -50%) scale(0.5) rotate(-7deg);
  }
  12% {
    opacity: 1;
    transform: translate(-50%, -50%) scale(1.14) rotate(2deg);
  }
  20% {
    opacity: 1;
    transform: translate(-50%, -50%) scale(1) rotate(0deg);
  }
  /* ↓ 停留：产品要求 0.5s（1.6s × 32% ≈ 0.51s），这段时间里它就是停在屏幕中间的 */
  52% {
    opacity: 1;
    transform: translate(-50%, -50%) scale(1) rotate(0deg);
  }
  100% {
    opacity: 0;
    transform: translate(calc(-50% + var(--fly-x, 0px)), calc(-50% + var(--fly-y, 0px))) scale(0.24);
  }
}

/* 接风：青蓝色 + 从两侧扫入，和名次明显区分 */
.banner.jiefeng {
  color: #7ee0ff;
  text-shadow: 0 4px 18px rgba(0, 0, 0, 0.8), 0 0 34px rgba(126, 224, 255, 0.7);
}
.banner.jiefeng.show {
  animation: jiefengPop 1.9s cubic-bezier(0.2, 0.9, 0.2, 1) forwards;
}
/*
 * 接风的动画：**和名次同一套节奏** —— 中间弹出来 → 停一下 → 缩小着飞到落点淡出。
 *
 * ⚠️ 老版本的这段关键帧**从头到尾没用 `--fly-x/--fly-y`**，所以接风只在屏幕中间
 * 闪一下、不会飞（产品实测报上来的就是"接风也要动画，接风缩小到接风的头像上"）。
 * 落点由 JS 注入（playJiefeng 会把 seat 传下去），这里必须消费这两个变量。
 */
@keyframes jiefengPop {
  0% {
    opacity: 0;
    letter-spacing: 34px;
    transform: translate(-50%, -50%) scale(1.5);
  }
  22% {
    opacity: 1;
    letter-spacing: 6px;
    transform: translate(-50%, -50%) scale(1.06);
  }
  40% {
    letter-spacing: 4px;
    transform: translate(-50%, -50%) scale(1);
  }
  /* 停一下：让人看清是谁接风 */
  56% {
    opacity: 1;
    letter-spacing: 4px;
    transform: translate(-50%, -50%) scale(1);
  }
  100% {
    opacity: 0;
    letter-spacing: 4px;
    transform: translate(calc(-50% + var(--fly-x, 0px)), calc(-50% + var(--fly-y, 0px))) scale(0.24);
  }
}

/* 尊重"减少动效"偏好：不做缩放/位移，只留淡入淡出 */
@media (prefers-reduced-motion: reduce) {
  .banner.finish.show,
  .banner.level.show,
  .banner.jiefeng.show {
    animation: bannerFade 1.6s linear forwards;
  }
  @keyframes bannerFade {
    0% {
      opacity: 0;
    }
    15%,
    80% {
      opacity: 1;
    }
    100% {
      opacity: 0;
    }
  }
}

/* ───────────── toast ───────────── */

.toast {
  position: fixed;
  left: 50%;
  bottom: 30%;
  transform: translateX(-50%);
  background: rgba(200, 40, 40, 0.94);
  color: #fff;
  padding: 10px 20px;
  border-radius: 10px;
  font-size: 14px;
  font-weight: 600;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.45);
  z-index: 50;
  pointer-events: none;
  max-width: 80vw;
  text-align: center;
}
.toast.ok {
  background: rgba(30, 140, 85, 0.94);
}

/* ───────────── 窄屏（手机） ───────────── */
/*
 * 手机高度有限，上下两栏必须尽量薄，把高度让给中间的牌桌：
 *   · 顶栏只有「本局打几 + 重开」，天然就一行
 *   · 底栏 = 手牌一行 + 按钮一行；我的头像挪到按钮那一行的右边，
 *     不再单独占一列宽度（手机太窄，留一列头像会把手牌挤扁）
 */

/* ───────────── 手机横屏 ───────────── */
/*
 * 横屏手机（典型 844×390 / 780×360）和竖屏是**相反**的约束：
 *
 *   竖屏 390×844 → 缺宽度、多高度 → 手机样式把左右座位列收窄到 84px
 *   横屏 844×390 → 缺高度、多宽度 → 这里把底栏压矮，把宽度让给牌桌
 *
 * 上面那条断点对横屏原本是**落空的**（844 > 720），已经在断点列表里补上
 * `(orientation: landscape) and (max-height: 520px)`，让横屏手机先吃到
 * 手机样式，再由下面这一段针对横屏做二次收敛。
 *
 * 三个数值不是拍脑袋，是按实测反推的：
 *
 *   1. 27 张手牌会**横铺满整条底栏**（实测右边界 825 / 844px，仅剩 6px 余量），
 *      所以横向没有可让的余地，只能从纵向解决
 *   2. 手牌底边落在分界线（--footer-bottom）上方 2px
 *   3. 我的头像、倒计时、操作区都是底栏内的绝对定位元素 ——
 *      只要它们**整体待在分界线以下**，就绝不可能压住手牌
 *      （**头像例外**：产品明确说"头像可以越过分界线"，见下面的 avatar）
 *
 * 于是 --footer-bottom 由操作区反推。当前口径（产品最新要求）：
 *
 *     按钮高 38 + 按钮距底边 2 + 分界线距按钮顶 2  =  **42**
 *
 * 底栏总高原本取 124px（余下 70px 当手牌带起点）。后来产品对照参考素材反馈
 * 「状态栏太高、占牌桌太多」，压到 64px —— 因为手牌和分界线都锚在**底边**上，
 * 减掉的是底栏上方的空白，牌桌因此从 278px 涨到 338px（69% → 84%）。
 *
 * 分界线又下移过一次：54 → 42（产品要求"边线下移"）。
 * 注意手牌是**锚在分界线上**的，所以分界线一下移，手牌也跟着下移同样的距离
 *（`bottomBarDividerY()` = clientHeight - --footer-bottom），牌桌的可视区随之变大。
 */

/* ───────────── 强制横屏（竖屏手机也横着显示）───────────── */
/*
 * 手机始终使用横向游戏界面。能锁定系统方向时由浏览器负责旋转；
 * 锁定失败时，窄屏触摸设备由这里把 #app 按横屏尺寸旋转到视口中。
 * 不显示额外的“请横屏”遮罩，主页和房间也沿用同一套横向坐标系。
 */
#btnFlip {
  display: none;
}

@media (orientation: portrait) {
  #app {
    position: fixed;
    top: 0;
    left: 0;
    width: 100dvh;
    height: 100dvw;
    transform-origin: 0 0;
    transform: rotate(90deg) translateY(-100%);
    padding-left: env(safe-area-inset-top);
    padding-right: env(safe-area-inset-bottom);
    background: radial-gradient(circle at 50% 50%, var(--felt-1), var(--felt-2) 72%);
  }

  html.flip #app {
    transform: rotate(-90deg) translateX(-100%);
    padding-left: env(safe-area-inset-bottom);
    padding-right: env(safe-area-inset-top);
  }
}

/* ───────────── 全屏（fullscreen.js）───────────── */

/*
 * 「全屏」按钮处于全屏状态时高亮一下，让用户知道再点一次是退出。
 * 按钮本身在 .table-topbar 里，用 .btn.ghost.small 的既有样式。
 */
.btn.ghost.on {
  background: rgba(240, 200, 92, 0.18);
  border-color: rgba(240, 200, 92, 0.6);
  color: var(--gold);
}

/* 危险操作（退出）—— 用红色边框区分，别和普通按钮混在一起 */
.btn.danger {
  background: rgba(190, 60, 60, 0.28);
  border-color: rgba(255, 120, 120, 0.55);
}

/* ───────────── 设置面板 ───────────── */

/*
 * 面板挂在 #app 里面（见 index.html 的说明），所以这里的定位是
 * **应用局部坐标** —— #app 被旋转 90° 时面板跟着转，方向正确。
 *
 * z-index 800：要压住 toast(50) 和手牌，但让给「全屏引导提示」(900)。
 */
.sheet {
  position: fixed;
  inset: 0;
  z-index: 800;
  background: rgba(0, 0, 0, 0.55);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 8px;
}

.sheet-box {
  width: min(680px, 100%);
  max-height: 100%;
  overflow-y: auto;
  background: linear-gradient(180deg, #14603f, #0b3a28);
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: 14px;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.55);
  /* 面板里要能点、能输入 —— 外层的 pointer-events 不能被别处影响 */
  pointer-events: auto;
}

.sheet-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 14px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.14);
  position: sticky;
  top: 0;
  background: #14603f;
  border-radius: 14px 14px 0 0;
}

.sheet-title {
  font-size: 15px;
  font-weight: 700;
  color: var(--gold);
}

.sheet-body {
  padding: 12px 14px 16px;
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.field-label {
  font-size: 12px;
  color: var(--ink-dim);
  margin-bottom: 6px;
}

.field-row {
  display: flex;
  align-items: center;
  gap: 8px;
}

.inp {
  flex: 1;
  min-width: 0;
  background: rgba(0, 0, 0, 0.28);
  border: 1px solid rgba(255, 255, 255, 0.2);
  border-radius: 9px;
  color: var(--ink);
  font-size: 14px;
  font-family: inherit;
  padding: 8px 12px;
}
.inp:focus {
  outline: none;
  border-color: rgba(240, 200, 92, 0.7);
}

/* 头像九宫格：20 张素材，一行放 10 个，两行正好。
 * 限宽是为了别把面板撑得要在小屏上滚动 —— 手机横屏可视高度只有 ~390px。 */
.avatar-grid {
  display: grid;
  grid-template-columns: repeat(10, 1fr);
  gap: 6px;
  max-width: 470px;
}

.avatar-grid .pick {
  width: 100%;
  aspect-ratio: 1;
  border-radius: 50%;
  overflow: hidden;
  border: 2px solid transparent;
  background: rgba(0, 0, 0, 0.25);
  padding: 0;
  cursor: pointer;
}
.avatar-grid .pick img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.avatar-grid .pick.on {
  border-color: var(--gold);
  box-shadow: 0 0 0 2px rgba(240, 200, 92, 0.35);
}

.tip {
  font-size: 12px;
  color: var(--ink-dim);
  margin-top: 6px;
  min-height: 16px;
}
.tip.warn {
  color: #ffb4b4;
}
.tip.ok {
  color: #9ff0bd;
}

/*
 * 全屏引导提示（由 fullscreen.js 动态创建）。
 *
 * 只在两种情况出现：
 *   · 拿不到 Fullscreen API —— 主要是 iPhone（iOS 只给 <video> 实现全屏）
 *   · 在微信内置浏览器里 —— 有 API 也基本会被忽略
 * 内容就是教用户走「添加到主屏幕」那条真正可行的路。点「知道了」后不再出现。
 */
#fsHint {
  position: fixed;
  left: 50%;
  transform: translateX(-50%);
  bottom: calc(12px + env(safe-area-inset-bottom));
  z-index: 900;
  display: flex;
  align-items: center;
  gap: 10px;
  max-width: calc(100vw - 24px);
  padding: 9px 10px 9px 14px;
  border-radius: 10px;
  background: rgba(6, 32, 21, 0.94);
  border: 1px solid rgba(255, 255, 255, 0.18);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
  color: var(--ink);
  font-size: 13px;
  line-height: 1.4;
  animation: fsHintIn 0.24s ease-out;
}
#fsHint button {
  flex-shrink: 0;
  padding: 7px 12px;
  border: 1px solid rgba(255, 255, 255, 0.24);
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.12);
  color: var(--ink);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
}
@keyframes fsHintIn {
  from {
    opacity: 0;
    transform: translateX(-50%) translateY(8px);
  }
  to {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
  }
}

/* ───────────── AI 托管遮罩 ─────────────
 *
 * 产品要求：被 AI 托管时屏幕「稍微变灰一点」，并提示「AI托管中，点击取消」。
 *
 * 为什么用遮罩而不是给 #app 加 filter: grayscale()：
 * filter 会让 #app 变成**包含块**，里面所有 position: fixed 的子元素
 *（横幅、倒计时、toast）都会改成相对它定位 —— 强制横屏那套坐标全乱。
 * 半透明遮罩没有这个副作用，而且顺便能接住"点击取消"。
 *
 * z-index 650：压住牌和座位，但**低于横幅(700)**（名次/接风还得看得见），
 * 也低于设置面板(800)（否则设置打不开）。
 */
/* ⚠️ 必须用 absolute 而不是 fixed —— 见上面"跟随 App 的横屏坐标系"的说明 */
.autoplay-mask {
  position: absolute;
  inset: 0;
  z-index: 650;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(20, 26, 22, 0.42);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.autoplay-mask.hidden {
  display: none;
}
.autoplay-mask span {
  padding: 10px 22px;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.55);
  border: 1px solid rgba(255, 255, 255, 0.28);
  color: #fff;
  font-size: 17px;
  font-weight: 700;
  letter-spacing: 1px;
  text-shadow: 0 2px 10px rgba(0, 0, 0, 0.7);
}

/* ─────────── 中央提示：移到屏幕正中 + 字号加大 ───────────
 *
 * 产品要求：「本局结束，点下一局继续」太靠上了，直接给它到屏幕正中间，字体加大一点。
 *
 * 这条提示就是 #centerHint（还同时用于"与服务器断开""连接出错"）。
 * 文件里前面已经有两处 #centerHint 定义（其中一处在媒体查询里），
 * 所以这里用 #app #centerHint（更高优先级）统一覆盖，避免和它们打架。
 */
#app #centerHint {
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  font-size: 26px;
  font-weight: 800;
  letter-spacing: 1px;
  line-height: 1.35;
  text-align: center;
  text-shadow: 0 3px 14px rgba(0, 0, 0, 0.9), 0 0 26px rgba(0, 0, 0, 0.65);
}

/* ══════════ 托管遮罩：最终生效的一条 ══════════
 *
 * ⚠️ 这个类在文件里**定义了两遍**，前面那份是早期的黑色版（rgba(20,26,22,.42)），
 * 位置靠后所以一直盖住我改成青绿的那份 —— 产品在手机上看到"还是黑底"就是这个原因
 *（实测从生产取 computed style：bg = rgba(20, 26, 22, 0.42)）。
 *
 * 这里在**文件末尾**再写一条：同优先级、位置最后，必然生效，也就不需要 !important。
 * 要求来自产品：别用黑底、要能透视到下面的牌 —— 所以是很淡的青绿，alpha 只有 0.16。
 */
.autoplay-mask {
  background: rgba(46, 120, 92, 0.16);
  box-shadow: inset 0 0 120px rgba(0, 0, 0, 0.18);
}
.autoplay-mask span {
  background: rgba(20, 60, 44, 0.82);
  border-color: rgba(255, 255, 255, 0.34);
}

/* ══════════ 四周统一 40px（底部栏头像）══════════
 * 底部栏里"自己的头像"原来距左边 10px。这里是座位格的内边距推法：
 * 若该格是居中排布，padding 会被吸收一半 —— 所以改完必须用
 * .deploy/measure-edges2.mjs 实测，不许凭感觉认定。
 */
.pos-bottom {
  padding-left: 30px;
}

/* ══════════ 左右头像精确 40px（贴边对齐）══════════
 *
 * 产品问"为什么头像是 39.6px 不是 40px"。实测拆解（.deploy/probe-avatar-chain.mjs）：
 *     座位格离边 4px
 *   + .seat-head 的 left 31px
 *   + (head 宽 45.16px − 头像 36px) / 2 = 4.58px   ← 头像在 head 里**居中**让出来的
 *   = 39.58px
 *
 * 最后那 4.58px 不是误差，但它**随昵称长度变化**（head 宽是被昵称撑开的）——
 * 昵称越长，头像越往右，间距根本不固定。所以不能靠"把 31 调成 31.42"来凑，
 * 那样换个昵称又偏了。
 *
 * 正确做法：让 head 的内容贴边（左座靠左、右座靠右），头像成为最外侧元素，
 *   距边 = 座位格 4px + head 偏移 36px = 正好 40px，与昵称长度无关。
 *
 * ⚠️ 这两条写在文件末尾，用来覆盖前面各层的 left/right: 31px（同优先级、位置最后）。
 */
.pos-left .seat-head {
  align-items: flex-start;
  left: 36px;
}
.pos-right .seat-head {
  align-items: flex-end;
  right: 36px;
}

/* ══════════ 出牌区让开头像（产品反馈：牌压在头像上）══════════
 *
 * 出牌区的位置 = 座位格左边缘(距应用边 4px) + margin-left。
 * 头像推到 40px 之后，头像右边缘在 76px；而 margin-left 仍是 64px，
 * 出牌区从 68px 开始 —— **伸进头像下面 8px**，牌一出来就压住头像。
 *
 * 实测（.deploy/probe-play-overlap.mjs：往出牌区塞 3 张牌后量矩形重叠）：
 *   margin 64px → 左右各重叠 8px
 *   margin 80px → 重叠 0
 * 这里取 82px：在头像(76px)之外留 10px 间隙，也刚好越过昵称行的右边缘(85.2px)。
 *
 * ⚠️ 用 margin 而不是改出牌区宽度：出牌区必须朝桌面中心生长（见上面的注释），
 * margin 是"让开头像"的唯一手段。
 */
.pos-left .seat-play {
  margin-left: 91px;
}
.pos-right .seat-play {
  margin-right: 91px;
}

/* ══════════ 从屏幕边缘量（抵消安全区）══════════
 *
 * 产品反馈：苹果上量的是"从灵动岛（安全区）算起"，要的是"从屏幕边缘、也就是程序边缘算起"。
 *
 * 安全区让出的内边距有两层：
 *   ① body 的 padding（横屏时让开灵动岛那侧）
 *   ② 强制横屏旋转时 #app 自己的 padding（局部左右 = 屏幕上下）
 * 所以实际距边 = 40 + 安全区，而且左右不等（灵动岛侧 62、另一侧 34）。
 *
 * 下面把两侧安全区抽成变量，各处偏移把它减掉 —— 这样无论有没有刘海，
 * 量出来都是**从程序边缘**算的 40px。
 */
:root {
  --edge-l: env(safe-area-inset-left);
  --edge-r: env(safe-area-inset-right);
}
/* 竖屏手持 + 强制横屏旋转：app 的左右方向对应屏幕的上下 */
@media (orientation: portrait) {
  :root {
    --edge-l: env(safe-area-inset-top);
    --edge-r: env(safe-area-inset-bottom);
  }
  /* 点了「翻转」之后方向再反过来 */
  html.flip {
    --edge-l: env(safe-area-inset-top);
    --edge-r: env(safe-area-inset-bottom);
  }
}

/* 左右头像：减掉安全区（绝对定位，left/right 允许负数） */
.pos-left .seat-head {
  left: calc(max(0px, 40px - var(--edge-l)) - 4px);
}
.pos-right .seat-head {
  right: calc(max(0px, 40px - var(--edge-r)) - 4px);
}

/*
 * 顶栏：padding 不能为负，所以把盒子本身撑到程序边缘，内部留着 40px。
 * 负的 left/right 正好让它跨过安全区那个内边距区域。
 */
.table-topbar {
  left: calc(-1 * min(var(--edge-l), 40px));
  right: calc(-1 * min(var(--edge-r), 40px));
  padding: 2px 40px;
}

/* 底部栏按钮组：right 允许负数 */
#actions {
  right: calc(max(0px, 40px - var(--edge-r)));
}

/* 底部栏自己的头像：padding 不能为负，改用 margin 减安全区 */
.pos-bottom {
  padding-left: 0;
  margin-left: calc(max(0px, 40px - var(--edge-l)) - 10px);
}

/* ══════════ 取较大值（夹住安全区）══════════
 *
 * 口径：距边 = max(40px, 安全区)。
 *   · 没有灵动岛的那一侧 → 正好 40px
 *   · 有灵动岛的那一侧   → 贴着安全区边界（不会被岛压住、也不会被裁）
 *
 * 为什么不再"完全抵消安全区"：产品在苹果 16 Pro 上实测头像被裁掉一半 ——
 * 完全抵消依赖"变量值 == 实际内边距"，一旦两者不一致（按过「翻转」、
 * 旋转分支未生效等），负偏移就会把元素推到程序边界之外，被容器裁掉。
 * 夹住之后，任何情况下都不会出现负的可见位置。
 */

/* ══════════ 张数徽标重做（按产品截图复刻）══════════
 *
 * 产品反馈：vivo 浏览器上"上游"词条太长（另见 .seat-rank 的 padding 还原）；
 * 并给了参考截图 —— 徽标样式重做，位置是**头像旁边、出牌区最左侧**：
 *      [头像] [7] 不出
 * 所以徽标不再是"压在头像角上"的绝对定位，而是出牌区里的第一个普通项目。
 */
.seat-remain {
  position: static;
  flex: none;
  box-sizing: border-box;
  min-width: 26px;
  height: 26px;
  padding: 0 5px;
  border-radius: 7px;
  background: linear-gradient(180deg, #3f7fd8, #1d3f7d);
  border: 2px solid #8fd8ff;
  color: #fff;
  font-size: 15px;
  font-weight: 800;
  line-height: 22px; /* 26 - 上下边框各 2 */
  text-align: center;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.45);
}

/*
 * 右家（玩家2）：出牌区是靠右对齐、向左生长的，徽标排在最后才紧邻它自己的头像。
 * 左家/自己（玩家4、玩家1）排在最前，正好在头像与牌之间。
 */
.pos-right .seat-remain {
  order: 1;
}

/* ══════════ 座位信息区：名次 + 张数竖排一列（产品给的尺寸）══════════
 *
 *   [几游]   ← 宽 30，在张数徽标上方
 *   [12]     ← 30 高 × 21 宽，整列距头像 5px
 *   出牌区/倒计时在这个列旁边，间隔 5px（.seat-play 的 gap），互不遮挡
 */
.seat-side {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  width: 30px;          /* 名次宽 30，徽标 21 居中在这一列里 */
  flex: none;
  align-self: center;
  /* 距头像 5px：出牌区的 margin 已把整块推到头像外侧，这里补列与出牌区的间隔 */
  margin-right: 5px;
}

/* 名次标签：宽度固定 30（原来是 padding 撑开，宽窄不一） */
.seat-rank {
  width: 30px;
  box-sizing: border-box;
  padding: 2px 0;
  text-align: center;
  font-size: 12px;
  line-height: 1.2;
  border-radius: 6px;
}

/* 张数徽标：30 高 × 21 宽 */
.seat-remain {
  position: static;
  flex: none;
  box-sizing: border-box;
  width: 21px;
  height: 30px;
  min-width: 21px;
  padding: 0;
  border-radius: 6px;
  background: linear-gradient(180deg, #3f7fd8, #1d3f7d);
  border: 2px solid #8fd8ff;
  color: #fff;
  font-size: 13px;
  font-weight: 800;
  line-height: 26px; /* 30 - 上下边框各 2 */
  text-align: center;
  letter-spacing: -0.5px; /* 两位数（10）也塞得进 21 宽 */
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.45);
}

/* 右家：整列排到出牌区最右（紧邻它自己的头像），间隔 5px 改在左边 */
.pos-right .seat-side {
  order: 1;
  margin-right: 0;
  margin-left: 5px;
}

/* ══════════ 牌型水印（印章样式，照产品参考图）══════════
 *
 * 参考图是贴在最后一张牌左下角的红色小印章：竖排两个字、红框红字、微微倾斜，
 * 像盖上去的章。只有炸弹/天王炸/同花顺/钢板/木板/三带二/顺子会盖（见 STAMP_KINDS）。
 *
 * ⚠️ 覆盖前面几层的 .play-kind 定义（原来是个蓝色小牌子），所以写在文件末尾。
 */
.play-kind {
  position: absolute;
  left: 3px;
  bottom: 3px;
  box-sizing: border-box;
  min-width: 17px;
  padding: 2px 1px;
  border: 1.5px solid rgba(198, 40, 32, 0.92);
  border-radius: 3px;
  background: rgba(255, 255, 255, 0.62);
  color: rgba(198, 40, 32, 0.96);
  font-size: 11px;
  font-weight: 800;
  line-height: 1.02;
  text-align: center;
  letter-spacing: -0.5px;
  /* 竖排：两个字竖着落，和参考图一致 */
  writing-mode: vertical-rl;
  text-orientation: upright;
  transform: rotate(-7deg); /* 手盖章那种轻微歪斜 */
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
  pointer-events: none;
  z-index: 3;
}
/* 三字以上（天王炸 / 同花顺 / 三带二）：字号再收一点，免得竖排太高 */
.play-kind.long {
  font-size: 10px;
  letter-spacing: -1px;
}

/* ══════════ 昵称居中（头像仍贴边）══════════
 *
 * 产品要求：头像下的名称要居中。
 *
 * 背景：为了让头像精确落在距边 40px 的位置（当时头像在 head 里居中，
 * 而 head 的宽度是被昵称撑开的 —— 昵称越长头像越往右），我把 head 改成了
 * 靠左/靠右对齐。副作用就是昵称跟着贴边，不再居中。
 *
 * 现在两全：head 仍贴边（头像位置不变），但**昵称自己与头像同宽并居中** ——
 * 昵称的左边缘（右家是右边缘）就是头像的边缘，宽度取头像宽度，
 * 文字在 50px 里居中，看起来就是"头像下面居中"。
 *
 * 名字超过 50px 时省略号截断（和参考素材里的"玩家273..."一致）。
 */
.seat-head .sname {
  width: 50px;
  max-width: 50px;
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ══════════ 座位信息区：按产品要求固定 ══════════
 *
 * 目标（左家为例，右家镜像）：
 *
 *   头像(50)         几游(30×18)   ← 顶部与头像顶部齐平
 *                    徽标(21×30)   ← 底部与头像底部齐平
 *                    出牌区        ← 顶部与几游顶部齐平，距几游 5px
 *
 * 关键：这一列总高 18 + 2(间隔) + 30 = **50 = 头像高度**，
 * 所以只要整列与头像**同中心**（都由座位的 align-items: center 居中），
 * "几游顶部齐头像顶部 / 徽标底部齐头像底部"就自动成立，不用写死坐标。
 * 头像的定位规则一行都不改 —— 产品要求头像不能移动。
 */

/* 1) 几游：高 18、宽 30；文字垂直居中 */
.seat-rank {
  box-sizing: border-box;
  width: 30px;
  height: 18px;
  min-height: 18px;
  padding: 0;
  line-height: 18px;
  text-align: center;
  font-size: 12px;
}

/* 2) 整列：宽 30、间隔 2；左右两家分别靠头像那一侧对齐
      —— 靠边对齐后，21 宽的徽标才真正贴到"距离头像 5px"，而不是被居中推到 9.5px */
.seat-side {
  width: 30px;
  gap: 2px;
  justify-content: flex-start;
  flex: none;
  align-self: center;
}
.pos-left .seat-side,
.pos-bottom .seat-side {
  align-items: flex-start; /* 靠左：徽标左边缘 = 列左边缘 = 头像右侧 + 5px */
  margin-right: 5px;
  margin-left: 0;
}
.pos-right .seat-side {
  align-items: flex-end; /* 靠右：镜像 */
  margin-left: 5px;
  margin-right: 0;
}

/* 3) 徽标尺寸复核（21 × 30），不再有外边距 */
.seat-remain {
  width: 21px;
  min-width: 21px;
  height: 30px;
  margin: 0;
}

/* 4) 出牌区：顶部与几游顶部齐平 —— 出牌区内部改顶部对齐 */
.seat-play {
  align-items: flex-start;
}

/* 5) 上方玩家头像的处理见文件末尾（更保守的做法：不碰头像本身）*/

/* ══════════ 收尾：列顶与头像顶齐平 + 上方玩家不被挤动 ══════════
 *
 * 实测（.deploy/probe-seat-side.mjs）：名次 30×18 ✓、徽标 21×30 ✓、整列 50 高 ✓，
 * 但这一列的顶部在 123、头像顶部在 106.5 —— 差 16.5px。原因是头像块带了
 * --seat-drop 的位移，而出牌区只是 flex 居中，两者中心并不重合。
 *
 * 处理：出牌区**固定高度**（内容变化时顶部不移动）+ 上移 16.5px。
 * 固定高度这一步同时解决"倒计时一出现、整块上下窜"的问题。
 */
.pos-left .seat-play,
.pos-right .seat-play {
  height: 50px;
  min-height: 50px;
  top: calc(var(--seat-drop, 0px) - 16.5px);
}

/*
 * 上方玩家（玩家3）：上一版我把出牌区改成绝对居中，结果它和头像**叠在一起**、
 * 几游还跑到头像左侧（产品实测反馈）—— 已撤销，恢复它原来的流内布局。
 * "倒计时出现时头像会闪"另想办法，不能再动出牌区的定位。
 */

/* ══════════ 座位信息区：绝对定位版 ══════════
 *
 * 坐标全部写死，谁也不会把谁挤动（产品反馈：玩家3 等倒计时时头像会跳到左边）。
 * 头像的定位规则一行不动。
 *
 * 关键尺寸：头像 50 宽 50 高、右边缘在 90（左边距 40）；
 *          几游 30×18、徽标 21×30，列高 18 + 2 + 30 = 50（与头像等高）
 *          —— 所以列顶 = 头像顶、列底 = 头像底，天然对齐。
 *
 * 座位内部坐标：座位框起点在 4px（见网格内边距），所以"距头像 5px"要写 91
 *（4 + 91 = 95 = 头像右缘 90 + 5）。
 */
.seat-side {
  position: absolute;
  width: 30px;
  height: 50px;
  top: 0; /* 由出牌区锚定：出牌区顶 = 几游顶 = 头像顶（实测校正） */
  margin: 0;
  display: flex;
  flex-direction: column;
  /*
   * **列内顶部对齐**。
   *
   * 这里改过一次，两种口径都记录清楚，别再改回去：
   *
   *   · 几游(18) + 间隔(2) + 徽标(30) = 50 = 列高 → 两项同时存在时，
   *     顶部对齐与底部对齐**完全等价**（徽标下边都贴着头像下边）；
   *   · 只剩**徽标**时（还没名次）：flex-start 会让它顶在上面，下边差 20px ✗
   *   · 只剩**几游**时（本局结束、徽标消失）：flex-end 会把它压到列的底部，
   *     低于头像下边 ✗ —— 产品在 iPhone 16 Pro 上就是这么看到的。
   *
   * 结论：两种"只剩一个"的情形**要求相反**，但产品要的行为是：
   *   只剩几游 → 贴**头像上边**；只剩徽标 → 贴**头像下边**。
   * 所以默认顶部对齐，再用 :has() 只给"只有徽标"的情形补回底部对齐。
   */
  justify-content: flex-start;
}

/*
 * 列里**只有徽标**（还没有名次）→ 贴列底 = 头像下边。
 * :has() 判断"没有 .seat-rank 但有 .seat-remain"。
 */
.seat-side:not(:has(.seat-rank)):has(.seat-remain) {
  justify-content: flex-end;
  gap: 2px;
  z-index: 4;
  pointer-events: none;
}

/* 左家（玩家4）：列贴头像右侧 5px，内容靠左（徽标才真正贴着头像） */
.pos-left .seat-side {
  /* 相对出牌区恒定往回 30+5 —— 出牌区与列同源于头像位置，安全区一起走，所以这里不用再算 */
  left: -35px;
  right: auto;
  align-items: flex-start;
}

/*
 * 自己（下方玩家1）：底部栏的座位格与左右两家不同（头像不在 40 处），
 * 实测列离头像 41px —— 出牌区左缘要从 126 往回 36 才落到"头像右缘 + 5"。
 */
.pos-bottom .seat-side {
  left: -35px; /* 同左家：恒定值 */
  right: auto;
  align-items: flex-start;
}
.pos-bottom .seat-play {
  position: absolute;
  left: calc(max(0px, 40px - var(--edge-l)) + 86px);
  right: auto;
  /* 底部栏的基准高与左右家不同：实测出牌区顶 333.5、应为 350（头像顶）→ 用 -25 */
  top: calc(50% + var(--seat-drop, 0px) - 25px);
  margin: 0;
  justify-content: flex-start;
}

/* 右家（玩家2）：镜像 —— 列贴头像左侧 5px，内容靠右 */
.pos-right .seat-side {
  right: -35px; /* 同左：恒定值 */
  left: auto;
  align-items: flex-end;
}

/* 上方玩家（玩家3）：头像水平居中，列贴它右侧 5px（50% + 头像半径 25 + 5） */
.pos-top .seat-side {
  left: -35px; /* 同上：相对出牌区往回 30+5 */
  right: auto;
  align-items: flex-start;
}

/*
 * 出牌区 / 倒计时：距几游 5px。
 * 左家 / 上方玩家在几游**右侧**（列右缘 + 5 = 91 + 30 + 5 = 126 相对座位）
 * 右家（玩家2）在几游**左侧**（镜像 126）
 *
 * ⚠️ 出牌区也一并绝对定位：这样它变宽不会挤动头像与这一列。
 */
.pos-left .seat-play {
  position: absolute;
  /*
   * ⚠️ 必须跟随安全区：真机上头像被 max(40, 安全区) 推到 62，写死 126 就会拉开距离
   *（产品在 iPhone 上实测报过"左右两家距离不对"）。
   * 座位内坐标：列左 = max(0, 40 - 安全区) + 51；出牌区左 = 列左 + 30 + 5 = 上式 + 86。
   */
  left: calc(max(0px, 40px - var(--edge-l)) + 86px);
  right: auto;
  top: calc(50% + var(--seat-drop, 0px) - 41.5px); /* 实测校正：顶部与几游顶齐平 */
  margin: 0;
  justify-content: flex-start;
}

.pos-right .seat-play {
  position: absolute;
  right: calc(max(0px, 40px - var(--edge-r)) + 86px); /* 跟随安全区（镜像） */
  left: auto;
  top: calc(50% + var(--seat-drop, 0px) - 41.5px); /* 同上 */
  margin: 0;
  justify-content: flex-end;
}

/* 上方玩家：列在 50%+30，出牌区/倒计时跟在它右边 5px（50% + 30 + 30 + 5） */
.pos-top .seat-play {
  position: absolute;
  left: calc(50% + 65px);
  right: auto;
  /* 上方玩家的基准高与左右不同，实测再补 14px（原来列顶比头像顶高 14） */
  top: calc(50% + var(--seat-drop, 0px) - 27.5px);
  margin: 0;
  justify-content: flex-start;
}

/* ══════════ 自己（玩家1）的几游：单独定位 ══════════
 *
 * 产品要求：自己的几游**不受其他三家影响**，放在头像右侧、垂直对齐头像中线，
 * 不遮挡金豆与昵称（那两行在头像下面，所以贴中线右侧就不会碰到）。
 *
 * 底部栏这一格与其他三家不同：座位框的起点不在 4px（实测列跑到头像右侧 41px 处），
 * 所以这里给单独的偏移：
 *   left: -71px  → 相对出牌区往回 71px，落点 = 头像右缘 + 5px
 *   top:  16px   → 整列下移 16px，使几游（18 高）的中线对准头像中线（50 高）
 * 自己不再显示张数徽标，所以列里只有几游。
 */
.pos-bottom .seat-side {
  /*
   * 与其它三家**同一套偏移**：实测自己那格的头像同样在 0..50、出牌区同样在 86，
   * 所以 -35 落点就是"头像右缘 + 5"。
   *
   * ⚠️ 这里原来写 -71 / top 16（早期另一套底部栏布局留下的），在 iPhone 16 Pro 上
   * 实测几游左 15、头像右 50 —— 几游**压在头像上**了（产品截图反馈"跑到头像下边"）。
   * top 也改成 0：几游顶与**头像上边**齐平（产品要求）。
   */
  left: calc(64px + max(0px, var(--edge-l) - 40px) - max(0px, 40px - var(--edge-l)));
  right: auto;
  top: -10px;
  align-items: flex-start;
}

/* ══════════ 出牌区印章：竖排 + 蓝色 + 右下角 ══════════
 *
 * 产品要求：
 *   · 不要横的，要**竖的**（和手牌一致）
 *   · 贴在**最后一张牌**上、靠**右下角**一点
 *   · 字号和手牌一样，颜色用**蓝**（出牌区原来就是这个蓝）
 *
 * 说明：出牌区这一处先前是被**早期那版 .play-kind 定义**（蓝色、横排）盖住的，
 * 所以看起来是横的、也是蓝的。这里显式写清楚，避免再被层叠影响。
 */
.seat-cards .play-kind,
#myPlay .cards .play-kind {
  writing-mode: vertical-rl;      /* 竖排 */
  text-orientation: upright;
  left: auto;
  right: 3px;                     /* 靠右下角 */
  bottom: 3px;
  top: auto;
  min-width: 17px;
  padding: 2px 1px;
  border: 1.5px solid rgba(56, 122, 214, 0.95);   /* 蓝框 */
  border-radius: 3px;
  background: rgba(255, 255, 255, 0.62);
  color: rgba(38, 104, 200, 0.98);                /* 蓝字 */
  font-size: 11px;                                /* 与手牌一致 */
  font-weight: 800;
  line-height: 1.02;
  text-align: center;
  letter-spacing: -0.5px;
  transform: rotate(7deg);                        /* 右下角，歪斜方向反过来 */
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
  pointer-events: none;
  z-index: 3;
}
.seat-cards .play-kind.long,
#myPlay .cards .play-kind.long {
  font-size: 10px;
  letter-spacing: -1px;
}
/* ══════════ 记牌器 ══════════
 *
 * 产品要求（照另一款掼蛋的记牌器复刻）：
 *   · 从右往左 2 → A → 小王 → 大王（左起就是 大王 小王 A K Q J 10 9 8 7 6 5 4 3 2）
 *   · 不按花色统计（同花顺不涉及花色）
 *   · 级牌不单独记
 *
 * 位置放在右上角按钮下方（横屏时那块是空的）。
 */
#counter {
  position: absolute;
  top: calc(max(0px, 40px - var(--edge-t, 0px)) + 6px); /* 实测：顶部按钮行底边在 40+1，这里 6 = 距它 5px */
  right: calc(max(0px, 40px - var(--edge-r))); /* 实测：按钮行右边缘就是"距屏幕 40px"，对齐它 */
  display: flex;
  gap: 1px;
  z-index: 7;
  pointer-events: none;
  background: rgba(0, 0, 0, 0.28);
  border-radius: 6px;
  padding: 2px 4px;
}
#counter.hidden { display: none; }

.ctr-cell {
  width: 16px; /* 列宽收窄（原 20） */
  display: flex;
  flex-direction: column;
  align-items: center;
  line-height: 1.05;
}

.ctr-rank {
  font-size: 11px;
  font-weight: 700;
  color: rgba(255, 255, 255, 0.86);
}

.ctr-num {
  font-size: 12px;
  font-weight: 800;
  color: #ffe37a;
}

.ctr-cell.zero .ctr-rank,
.ctr-cell.zero .ctr-num {
  color: rgba(255, 255, 255, 0.28);
}
/* ══════════ 设置：两级菜单 ══════════ */
.set-page {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.set-page.hidden { display: none; }

.set-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  min-height: 40px;
  padding: 8px 12px;
  font-size: 15px;
  color: #eaf5ee;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 8px;
  cursor: pointer;
}

.set-row:hover { background: rgba(255, 255, 255, 0.12); }

.set-val {
  font-weight: 700;
  color: #9fe0b6;
}

.set-arrow {
  font-size: 18px;
  color: rgba(255, 255, 255, 0.5);
}

.avatar-now {
  width: 50px;
  height: 50px;
  border-radius: 10px;
  object-fit: cover;
  border: 2px solid rgba(255, 255, 255, 0.25);
}

/* ══════════ 设置：滑块开关 / 收窄 / 分组 ══════════ */

/* 产品要求：面板不要那么长，刚好够显示就行 */
#settings .sheet-box {
  width: auto;
  max-width: 300px;
}

/* 二级页里的分组标题（不同类型分不同组） */
.set-group {
  margin: 6px 0 2px;
  font-size: 12px;
  font-weight: 700;
  color: rgba(255, 255, 255, 0.55);
  letter-spacing: 1px;
}

/* 带滑块的行不是按钮，但仍然用同一套外观 */
.set-row.static { cursor: default; }
.set-row.static:hover { background: rgba(255, 255, 255, 0.06); }

/* 滑块 */
.sw {
  position: relative;
  flex: none;
  width: 46px;
  height: 26px;
  padding: 0;
  border-radius: 13px;
  border: 1px solid rgba(255, 255, 255, 0.22);
  background: rgba(255, 255, 255, 0.14);
  transition: background 0.16s ease;
  cursor: pointer;
}

.sw-knob {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: #f2f6f3;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
  transition: left 0.16s ease;
}

.sw.on {
  background: #35b56a;
  border-color: #35b56a;
}

.sw.on .sw-knob { left: 22px; }
/* ══════════ 设置面板靠右 + 顶部声音按钮隐藏 ══════════ */

/* 产品要求：设置面板靠右，不要摆在中间 */
#settings {
  justify-content: flex-end;
  align-items: center;
  padding-right: calc(max(0px, 40px - var(--edge-r)));
}

/* 产品要求：顶部「声音」按钮删掉 —— 设置里已经有声音开关了。
   保留元素（声音开关的状态判定读它），只是不显示。 */
#btnSound { display: none; }

/* ══════════ 自己的几游：贴头像右侧 5px、与头像上边对齐 ══════════
 *
 * 产品要求：
 *   · 距头像 5px（横向）
 *   · 与自己头像的**上边**对齐
 *   · 昵称与金豆相应**下移**，给几游让出位置
 *
 * 实测（无安全区）：头像 40..90、出牌区左缘 126 → -31 正好落在 95 = 头像右缘 + 5。
 * 顶部用 0：出牌区顶与头像顶齐平（见 .seat-side 的说明）。
 */
.pos-bottom .seat-side {
  left: -71px;
  right: auto;
  top: 0;
  align-items: flex-start;
}

/* 进贡/还贡牌：左右座位显示在头像上方，牌左边与头像左边对齐。 */
.tribute-marker {
  display: flex;
  flex: 0 0 auto;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  width: max-content;
  max-width: 40px;
  z-index: 8;
}
.tribute-card {
  display: flex;
  align-items: center;
  justify-content: space-between;
  box-sizing: border-box;
  width: 30px;
  min-width: 30px;
  max-width: 30px;
  height: 18px;
  padding: 0 1px;
  overflow: hidden;
  border: 1px solid rgba(255, 255, 255, 0.86);
  border-radius: 4px;
  background: linear-gradient(180deg, #fff 0%, #edf3ef 100%);
  color: #17241e;
  font-size: 12px;
  font-weight: 900;
  line-height: 1;
  text-overflow: ellipsis;
  white-space: nowrap;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
}
.tribute-card.is-red { color: #c32932; }
.tribute-card.is-placeholder {
  justify-content: center;
  border-color: rgba(255, 218, 133, 0.88);
  background: linear-gradient(180deg, #fff0bd, #f6c35c);
  color: #654012;
}
.tribute-card.is-refused {
  justify-content: center;
  border-color: rgba(255, 218, 133, 0.94);
  background: linear-gradient(180deg, #ffe7a0, #d5a632);
  color: #533507;
}
.tribute-card.is-small-joker {
  justify-content: center;
  color: #17241e;
}
.tribute-card.is-big-joker {
  justify-content: center;
  color: #c32932;
}
.tribute-rank {
  font-size: 12px;
  font-weight: 1000;
}
.tribute-suit {
  font-size: 13px;
  line-height: 1;
}
.tribute-kind {
  display: grid;
  flex: 0 0 20px;
  place-items: center;
  box-sizing: border-box;
  width: 20px;
  height: 18px;
  border: 1px solid rgba(255, 255, 255, 0.82);
  border-radius: 5px;
  color: #fff;
  font-size: 11px;
  font-weight: 1000;
  line-height: 1;
  white-space: nowrap;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.48);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.32);
}
.tribute-marker.is-giving .tribute-kind {
  background: linear-gradient(180deg, #35bb86, #13865c);
}
.tribute-marker.is-returning .tribute-kind {
  background: linear-gradient(180deg, #ffcf5a, #df8e16);
  color: #412800;
  text-shadow: none;
}

/* 左右玩家与自己：牌面右边缘对齐头像右边缘，进/还标识在左并从头像左边缘起。 */
.pos-left .tribute-marker,
.pos-right .tribute-marker,
#bottom .pos-bottom .tribute-marker {
  position: absolute;
  top: -20px;
  z-index: 9;
  flex-direction: row-reverse;
  align-items: center;
  gap: 0;
  width: 50px;
  max-width: none;
  height: 18px;
}
.pos-left .tribute-marker,
#bottom .pos-bottom .tribute-marker,
.pos-right .tribute-marker { right: 0; left: auto; }

/* 上方玩家：进/还标识也在牌左边，牌面右边缘与头像左边缘对齐。 */
.pos-top .tribute-marker {
  position: absolute;
  top: 16px;
  left: -50px;
  z-index: 9;
  display: flex;
  flex-direction: row-reverse;
  align-items: center;
  gap: 0;
  width: 50px;
  max-width: none;
  height: 18px;
}

/* 昵称与金豆下移，避免和几游挤在一起 */
#bottom .pos-bottom .sname,
#bottom .pos-bottom .seat-meta {
  margin-top: 12px;
}
/* ══════════ 名次色阶（A 方案：奖牌色） ══════════
 *
 * 产品选定：上游金 / 二游银 / 三游铜 / 下游暗灰。
 *
 * 只上色，**不动尺寸与位置** —— .seat-rank 的 30×18、以及各自那格的偏移，
 * 都是按真机实测调好的（见上面几段注释），改这里会连带把它们弄乱。
 *
 * 特意避开蓝与红：那两个颜色已经被"出牌区印章 / 手牌印章"占用。
 */
.seat-rank.rank-1 {
  background: #f0c85c; /* 金 */
  color: #3a2a05;
  border-color: #ffe9a8;
}

.seat-rank.rank-2 {
  background: #cfd8e3; /* 银 */
  color: #2b333b;
  border-color: #eef3f8;
}

.seat-rank.rank-3 {
  background: #cd8b53; /* 铜 */
  color: #2e1a08;
  border-color: #e8b98c;
}

.seat-rank.rank-4 {
  background: #6b7a72; /* 暗灰：下游 */
  color: #e8efe9;
  border-color: #8b9a91;
  opacity: 0.9;
}

/* ══════════ 自己那格：昵称与金豆距离头像 5px ══════════
 *
 * 产品要求：自己头像旁的昵称与金豆，距离头像 5px。
 * 实测原来间隙是 4px（头像 40..90，文字从 94 开始）→ 这里设成 5px。
 *
 * 头像与文字是同一个 .seat-head 里的 flex 子项，间隙由 head 的 gap 决定；
 * 这个选择器比 .seat-head 更具体，所以能稳定覆盖它的 gap，
 * 而且不会影响另外三家（它们各自有更具体的规则）。
 */
#bottom .pos-bottom .seat-head {
  gap: 5px;
}

/* ══════════ 出牌动作按钮：屏幕中央复刻（产品参照图） ══════════
 *
 * 产品要求：完整复刻参照图的**样式与位置** —— 不要在右下角，
 * 右下角只留「整理」。所以这组按钮固定浮在屏幕中央（略高于中线，
 * 正好在出牌区上方），样式按参照图做成胶囊：
 *   提示 / 不出 → 蓝底白字      出牌 → 橙底深色字      要不起 → 一个宽蓝按钮
 */
#actionCenter {
  position: fixed;
  left: 50%;
  top: 46%;
  transform: translate(-50%, -50%);
  display: flex;
  align-items: center;
  gap: 14px;
  z-index: 40;           /* 高于牌桌、低于弹层 */
  pointer-events: none;  /* 只让按钮本身可点，避免挡住看牌 */
}
#actionCenter .btn {
  pointer-events: auto;
  /* 照参照图：更大、更饱满，蓝底带高光边（产品图里的按钮比原先大一圈） */
  border: 2px solid rgba(255, 255, 255, 0.55);
  border-radius: 999px;
  padding: 16px 46px;
  font-size: 21px;
  font-weight: 800;
  letter-spacing: 2px;
  color: #fff;
  background: linear-gradient(180deg, #8fc4ff 0%, #3f86f0 55%, #2f6fd8 100%);
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.38), inset 0 2px 0 rgba(255, 255, 255, 0.45);
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.25);
}
/* 出牌：橙底深字（参照图就是这样，不是白字）
   ⚠️ 「还贡」**共用同一条规则**（产品口径 2026-10-09「要和出牌按钮一样的**配色和样式**」）：
      挂同一个 `primary` 类不够（`.btn.primary` 是蓝的 ✗）、只对齐 4 项颜色也不够（还有 border/阴影/字号… ✗）
      ⇒ 这里把**选择器并列**，两条按钮从此共享**同一份声明**，任何一项都不可能再各自漂移 ✓ */
#actionCenter #btnPlay,
#actionCenter #btnReturn {
  background: linear-gradient(180deg, #ffd98a 0%, #ffab2e 55%, #f08c00 100%);
  color: #fff;
  text-shadow: 0 1px 2px rgba(120, 60, 0, 0.45);
}
/* 要不起：一个更宽的蓝按钮 */
#actionCenter #btnNoBeat {
  padding: 16px 70px;
}
#actionCenter .btn:disabled {
  /* 只压一点亮度，**不要去色** —— 参照图里出牌没选牌时仍是橙色，
     加 grayscale 会变成灰橄榄色，很脏（截图验收时发现的） */
  opacity: 0.72;
  filter: brightness(0.92);
}
#actionCenter .btn.hidden {
  display: none;
}

/* 底栏按钮行：现在只剩「整理」（以及还贡/下一局），靠右排 */
#actions .btn-row {
  justify-content: flex-end;
}

/* ══════════ 倒计时搬进按钮行 · 与按钮同高（产品参照图） ══════════
 *
 * 参照图里倒计时圆圈**夹在按钮中间**（提示 | ⑲ | 出牌；有「不出」时 不出 | ⑱ | 提示 | 出牌），
 * 而不是浮在按钮上方。这里用固定定位把它挪到按钮行中间：
 *   · 圆环本身仍由座位渲染器生成（不动结构，风险最低）
 *   · 直径做成 58px = 与放大后的按钮等高（产品要求「同按钮一样高」）
 *   · 位置取屏幕中线略偏右 —— 正好落在「提示」和「出牌」之间
 */
#bottom .pos-bottom .turn-timer,
#bottom .pos-bottom .seat-play .turn-timer,
body .turn-timer {
  position: fixed !important;
  left: 50% !important;
  top: 46% !important;
  transform: translate(-50%, -50%) !important;
  width: 58px !important;
  height: 58px !important;
  margin: 0 !important;
  z-index: 41 !important;      /* 盖在按钮行之上一点，但不超过弹层 */
}
.turn-timer svg {
  width: 58px !important;
  height: 58px !important;
}
.turn-timer .n {
  font-size: 20px !important;
  font-weight: 800 !important;
}

/* ⚠️ 产品要求：**不选牌时「出牌」也要是亮色的** —— 所以禁用态不再压暗。
   「还贡」同样共用（它在"还没选牌"时是禁用的 ✓ 与出牌的可点/不可点逻辑一致 ✓） */
#actionCenter #btnPlay:disabled,
#actionCenter #btnReturn:disabled {
  opacity: 1;
  filter: none;
}
/* 其余按钮禁用时也只轻微压暗，保持可辨 */
#actionCenter .btn:disabled {
  opacity: 0.85;
  filter: brightness(0.95);
}

/* 「下一局」在中间时也按同样的胶囊样式 */
#actionCenter #btnNext {
  background: linear-gradient(180deg, #ffd98a 0%, #ffab2e 55%, #f08c00 100%);
  color: #fff;
  text-shadow: 0 1px 2px rgba(120, 60, 0, 0.45);
  padding: 16px 58px;
}

/* ══════════ 我的倒计时圆环：作为按钮行的一项（最小必需，4 条） ══════════
 *
 * ⚠️ 只用 `.turn-timer.in-row` 限定，且**只写这几条**。
 *    上一版我在这里写了 transform:none / z-index / flex:0 0 58px 一堆 !important，
 *    结果整行被撑成整屏宽、按钮被挤出屏幕（本地验证拦住了，没上线上）。
 *    本地诊断证明：仅"插入行内"是无害的 —— 行宽不变、提示/出牌坐标不变。
 * 直径 58px = 与放大后的按钮等高（产品要求「同按钮一样高」）。
 */
#actionCenter .turn-timer.in-row {
  position: static;
  width: 58px;
  height: 58px;
  margin: 0;
  flex: 0 0 auto;
}

/* ══════════ 按钮布局按产品规格（尺寸 / 与头像上边对齐 / 以倒计时圆为锚） ══════════
 *
 * 规格：不出 60×30 ｜ 提示 60×30 ｜ 出牌 60×30 ｜ 要不起 90×30 ｜ 圆直径 30
 * 上边 = 左右头像的上边。实测三种屏幕下头像 top 都是 calc(50% - 94px)，故写成同一公式。
 * 横向以倒计时圆为锚：出牌=圆心右 10px，提示=圆心左 10px，不出=提示左 10px，要不起=圆心右 10px。
 * 圆心在屏幕中点 → 圆 left:50% + margin-left:-15px（30 宽的一半）。
 *
 * ⚠️ 容器不能带 transform：会把子元素的 fixed 劫持成"相对容器"（踩过这个坑）。
 */
#actionCenter {
  position: static !important;
  transform: none !important;
  left: auto !important;
  top: auto !important;
  display: block !important;
  pointer-events: none;
}
#actionCenter .btn {
  /*
   * ⚠️ 全部用 !important 钉死：实测发现高度被别处的样式撑到 38px（规格要 30）——
   *    本项目 style.css 是层层叠加的历史文件，只写普通声明压不住更具体/更靠后的规则。
   *    border 一律去掉，描边改用 box-shadow（不占尺寸）。
   */
  position: fixed !important;
  box-sizing: border-box !important;
  top: calc(50% - 94px) !important;
  width: 60px !important;
  height: 30px !important;
  min-height: 0 !important;
  max-height: 30px !important;
  padding: 0 !important;
  border: none !important;
  border-radius: 999px !important;
  font-size: 15px !important;
  line-height: 30px !important;
  font-weight: 800 !important;
  letter-spacing: 1px !important;
  pointer-events: auto;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.4) !important;
}
/* 出牌：圆心右边 10px（圆心在 50%，半径 15 → 25px） */
#actionCenter #btnPlay { left: calc(50% + 25px); right: auto; }
/* 要不起：同样在圆心右边 10px，但宽 90 */
#actionCenter #btnNoBeat { left: calc(50% + 25px); right: auto; width: 90px; }
/* 提示：圆左边 10px —— 用 right 定位，宽度变化也不影响贴边 */
#actionCenter #btnHint { right: calc(50% + 25px); left: auto; }
/* 不出：提示左边再 10px（提示宽 60 → 60+10+10 = 80，再 +15 半径 = 95） */
#actionCenter #btnPass { right: calc(50% + 95px); left: auto; }
/* 下一局（局末）：居中显示在同一高度 */
#actionCenter #btnNext {
  left: 50%;
  right: auto;
  transform: translateX(-50%);
  width: auto;
  padding: 0 28px;
  height: 34px;
}

/* 倒计时圆：直径 30，圆心在屏幕中点，上边与按钮一致 */
#actionCenter .turn-timer.in-row {
  position: fixed !important;
  left: 50% !important;
  top: calc(50% - 94px) !important;
  margin: 0 0 0 -15px !important;
  width: 30px !important;
  height: 30px !important;
  transform: none !important;
  z-index: auto !important;
}
#actionCenter .turn-timer.in-row svg { width: 30px; height: 30px; }
#actionCenter .turn-timer.in-row .n { font-size: 13px; font-weight: 800; }

/* ══════════ 按钮/圆环尺寸 v2（产品新规格 + 参照图） ══════════
 *
 * 两字按钮 80×40（不出/提示/出牌）｜ 三字按钮 120×40（要不起）｜ 圆直径 40
 * 间距仍 10px；圆 40 宽 → margin-left:-20px，圆心仍是屏幕中点。
 * 圆环样式按参照图复刻：**金环 + 白底 + 红色数字**，剩余时间走绿色弧。
 * 上边仍 = 左右头像上边 = calc(50% - 94px)（三种屏幕实测值）。
 */
#actionCenter .btn {
  width: 80px !important;
  height: 40px !important;
  max-height: 40px !important;
  font-size: 17px !important;
  line-height: 40px !important;
}
#actionCenter #btnPlay { left: calc(50% + 30px) !important; right: auto !important; }
#actionCenter #btnNoBeat { left: calc(50% + 30px) !important; right: auto !important; width: 120px !important; }
#actionCenter #btnHint { right: calc(50% + 30px) !important; left: auto !important; }
#actionCenter #btnPass { right: calc(50% + 120px) !important; left: auto !important; }

/*
 * 倒计时圆：直径 40
 *
 * 产品口径（参照图）：**最外圈是橙色，里面是白色，不要进度条**。
 * ⚠️ 上一版我把白底画满整个 40px，橙圈又画在中间，结果看起来是「白 → 橙 → 白」——
 *    正是产品说"你弄最外是白"的那个问题。
 * 这一版：
 *   · 橙圈半径顶到最外（r=22.5 + stroke 5 → 外沿正好 25 = 圆的边缘）
 *   · 白底留在里面（数字的底色）
 *   · 进度弧（绿的那段）**直接不显示** —— 产品不要进度条
 */
#actionCenter .turn-timer.in-row {
  width: 40px !important;
  height: 40px !important;
  margin: 0 0 0 -20px !important;
  background: #fff !important;      /* 里面的白色 */
  border-radius: 50% !important;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.35) !important;
  overflow: hidden !important;      /* 橙圈超出边缘的部分裁掉，保证最外就是橙色 */
}
#actionCenter .turn-timer.in-row svg { width: 40px !important; height: 40px !important; }
/* 橙圈：贴到最外沿（viewBox 50 → r=22.5 时外沿=25=半径） */
#actionCenter .turn-timer.in-row circle.track {
  stroke: #f08c00 !important;
  stroke-width: 5 !important;
  r: 22.5px !important;
}
/* 进度弧不要（产品明确说不要进度条） */
#actionCenter .turn-timer.in-row circle.bar {
  display: none !important;
  stroke: transparent !important;
}
/* 数字：红色、加粗 */
#actionCenter .turn-timer.in-row .n {
  color: #d92b2b !important;
  font-size: 17px !important;
  font-weight: 900 !important;
}

/* ══════════ 按钮布局 v3：圆为锚，右边提示/出牌，左边不出，**间距 5px** ══════════
 *
 * 产品口径：提示放倒计时右边、出牌也放倒计时右边（提示再往右 5px），
 *          不出放倒计时左边，三者与圆都是 **5px** 间隔。圆心仍在屏幕中点。
 * 圆半径 20、按钮宽 80，所以：
 *   提示左 = 圆心 + 25 ｜ 出牌左 = 圆心 + 110 ｜ 不出右 = 圆心 − 25
 */
#actionCenter #btnHint { left: calc(50% + 25px) !important; right: auto !important; }
#actionCenter #btnPlay { left: calc(50% + 110px) !important; right: auto !important; }
#actionCenter #btnPass { right: calc(50% + 25px) !important; left: auto !important; }
#actionCenter #btnNoBeat { left: calc(50% + 25px) !important; right: auto !important; width: 120px !important; }

/* ══════════ 其他三家的倒计时圆环（同款 + 贴徽标） ══════════
 *
 * 产品口径：
 *   · 三家圆环换成和"我"一样的款式：最外橙圈 + 内白 + 红字 + **无进度弧**
 *   · 放在"还剩几张"徽标（.seat-remain）**右侧 10px**；玩家2（右侧那家）放**左侧 10px**
 *   · 圆的**底部**与徽标**底部**对齐（align-self: flex-end）
 *
 * 圆环与徽标都在同一个 .seat-play（flex 行）里，所以用 order + margin 排位即可；
 * 右侧那家的 .seat-side 有 order:2（见更早的注释），圆环用 order:1 排到它左边。
 */
/* 1) 款式统一（含 npc） */
.turn-timer.npc {
  background: #fff !important;
  border-radius: 50% !important;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.35) !important;
  overflow: hidden !important;
}
.turn-timer.npc circle.track {
  stroke: #f08c00 !important;
  stroke-width: 5 !important;
  r: 22.5px !important;
}
.turn-timer.npc circle.bar { display: none !important; }
.turn-timer.npc .n {
  color: #d92b2b !important;
  font-weight: 900 !important;
}

/* 2) 位置：默认（左/上/下三家）徽标右侧 10px，底部对齐 */
#bottom .pos-bottom .turn-timer.npc,
.seat.pos-left .turn-timer.npc,
.seat.pos-top .turn-timer.npc {
  order: 2 !important;
  margin: 0 0 0 5px !important;   /* 距徽标 10px → 5px（产品口径） */
  align-self: flex-end !important;
}
/* 3) 玩家2（右侧那家）：放徽标左侧 10px，底部对齐 */
.seat.pos-right .turn-timer.npc {
  order: 1 !important;
  margin: 0 5px 0 0 !important;   /* 距徽标 10px → 5px（产品口径） */
  align-self: flex-end !important;
}

/* ══════════ 布局按"几个按钮在"自动切换 + 三家圆环 40 ══════════
 *
 * 产品口径：**三个按钮都在时**才用"提示在圆右边"的排法；
 *          只有提示+出牌时，**提示仍在圆的左边**。
 * 用 :has() 看「不出」是否可见来切换（.hidden 即 display:none）。
 */
/* 默认（领牌：只有提示+出牌）→ 提示在圆左、出牌在圆右，各 5px */
#actionCenter #btnHint { right: calc(50% + 25px) !important; left: auto !important; }
#actionCenter #btnPlay { left: calc(50% + 25px) !important; right: auto !important; }
/* 跟牌（三个都在）→ 不出在圆左；提示在圆右；出牌在提示右 */
#actionCenter:has(#btnPass:not(.hidden)) #btnHint { left: calc(50% + 25px) !important; right: auto !important; }
#actionCenter:has(#btnPass:not(.hidden)) #btnPlay { left: calc(50% + 110px) !important; right: auto !important; }
#actionCenter:has(#btnPass:not(.hidden)) #btnPass { right: calc(50% + 25px) !important; left: auto !important; }

/* 其他三家圆环：直径也 40（原来 npc 是小圈，产品要求统一） */
.turn-timer.npc {
  width: 40px !important;
  height: 40px !important;
}
.turn-timer.npc svg { width: 40px !important; height: 40px !important; }
.turn-timer.npc .n { font-size: 17px !important; }

/* ══════════ 三家圆环：把三家从"固定居中"里救出来 ══════════
 *
 * ⚠️ 真因（实测查出）：我给"我自己"写的 `.turn-timer.in-row{position:fixed;left:50%...}`
 *    那条规则**也命中三家的圆环** —— 实测 pos-right 的圈 x=417 = 屏幕正中，
 *    而它本该在右侧那家旁边。所以先把三家改回座位内部排版。
 *
 * 位置：贴"还剩几张"徽标 10px（上家/对家在右，下家在左），底部对齐
 *      （产品口径：对齐徽标底部 = 头像底部；这里先贴出牌行底，下一轮用实测把差值补到 0）
 */
.turn-timer.npc {
  position: static !important;
  left: auto !important;
  top: auto !important;
  transform: none !important;
  margin-top: 0 !important;
}

/* 上家（左）+ 对家（上）：徽标右侧 10px */
.seat.pos-left .turn-timer.npc,
.seat.pos-top .turn-timer.npc {
  order: 2 !important;
  margin: 0 0 0 5px !important;   /* 距徽标 10px → 5px（产品口径） */
  align-self: flex-end !important;
}
/* 下家（右）：徽标左侧 10px */
.seat.pos-right .turn-timer.npc {
  order: 1 !important;
  margin: 0 5px 0 0 !important;   /* 距徽标 10px → 5px（产品口径） */
  align-self: flex-end !important;
}

/* ══════════ 分界线距底 40px + 「整理」按钮实体化（产品口径） ══════════
 *
 * ① 分界线就是 #bottom::before，位置由 --footer-bottom 控制（原来是 56px，产品要 40px）。
 *    ⚠️ 同一变量在文件里被多处定义，这里用更靠后的选择器覆盖，确保生效。
 * ② 「整理」原来只有 .ghost（半透明），现在做成**实体**按钮：橙金渐变、白字，
 *    尺寸按产品要求 **30×55**（宽 30、高 55；文字竖排两行）。
 *    如果产品其实想要 55×30（宽 55、高 30），改这两行即可。
 */
#bottom,
html body #bottom {
  --footer-bottom: 40px !important;
}

#actionCenter ~ * #btnTidy,
#actions #btnTidy,
#btnTidy {
  /* 实体化：与「出牌」同一套橙金渐变 */
  background: linear-gradient(180deg, #ffd98a 0%, #ffab2e 55%, #f08c00 100%) !important;
  color: #fff !important;
  text-shadow: 0 1px 2px rgba(120, 60, 0, 0.45) !important;
  border: none !important;
  border-radius: 8px !important;
  box-shadow: 0 3px 10px rgba(0, 0, 0, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.4) !important;

  /* 尺寸 30×55（宽 30、高 55）：两个字竖排 */
  box-sizing: border-box !important;
  width: 30px !important;
  height: 55px !important;
  min-width: 0 !important;
  min-height: 0 !important;
  padding: 0 !important;
  font-size: 14px !important;
  font-weight: 800 !important;
  line-height: 1.15 !important;
  letter-spacing: 0 !important;
  white-space: normal !important;
  writing-mode: vertical-rl !important;   /* 竖排，两个字上下叠 */
  text-orientation: upright !important;
}

/* ══════════ 三家圆环：五层一次性统一（盒子/SVG/底环/进度弧/数字） ══════════
 *
 * 产品截图：橙圈和白盘**错位**（一个偏左上、一个偏右下）。
 * 原因：上一版只改了外盒，里面 SVG 与数字还带旧样式。
 * 现在把"我"那套构造完整套到三家身上，五层全部钉死，内外必然同心。
 */
.turn-timer.npc {
  position: static !important;
  box-sizing: border-box !important;
  width: 40px !important;
  height: 40px !important;
  min-width: 40px !important;
  min-height: 40px !important;
  padding: 0 !important;
  background: #fff !important;
  border: none !important;
  border-radius: 50% !important;
  overflow: hidden !important;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.35) !important;
  display: block !important;
}
.turn-timer.npc svg {
  display: block !important;
  width: 40px !important;
  height: 40px !important;
  margin: 0 !important;
  position: static !important;
}
/* 底环：橙，贴到最外沿 */
.turn-timer.npc circle.track {
  fill: none !important;
  stroke: #f08c00 !important;
  stroke-width: 5 !important;
  r: 22.5px !important;
}
/* 进度弧不要 */
.turn-timer.npc circle.bar { display: none !important; }
/* 数字：绝对居中（不依赖旧定位），红色 */
.turn-timer.npc .n {
  position: absolute !important;
  inset: 0 !important;
  display: flex !important;
  align-items: center !important;
  justify-content: center !important;
  margin: 0 !important;
  padding: 0 !important;
  color: #d92b2b !important;
  font-size: 17px !important;
  font-weight: 900 !important;
  line-height: 1 !important;
  transform: none !important;
}
/* 数字要能被绝对定位参照 → 盒子必须是定位上下文 */
.turn-timer.npc { position: relative !important; }

/* ══════════ 分界线距底 30px + 「整理」横排 36×26 距底 2px ══════════
 *
 * ① 分界线仍是 #bottom::before，距离变量 40px → **30px**
 * ② 整理按钮改回**横排**（上一版我按 30×55 做成了竖排，产品要横排）
 * ③ 尺寸 **宽 36 × 高 26**，并**距屏幕底部 2px**
 *    #bottom 是 position:relative，所以用 absolute + bottom:2px 精确贴底。
 *    横向沿用底栏内边距（14px），与原来大致同一位置。
 */
#bottom,
html body #bottom {
  --footer-bottom: 30px !important;
}

#btnTidy,
#actions #btnTidy {
  /* fixed 才是相对**屏幕**贴底：absolute 是相对底栏，实测会差 2px（产品要恰好 2px） */
  position: fixed !important;
  bottom: 2px !important;
  right: 14px !important;
  z-index: 35 !important;

  box-sizing: border-box !important;
  width: 36px !important;
  height: 26px !important;
  min-width: 0 !important;
  min-height: 0 !important;
  max-height: 26px !important;
  padding: 0 !important;

  /* 横排 */
  writing-mode: horizontal-tb !important;
  text-orientation: mixed !important;
  white-space: nowrap !important;
  line-height: 26px !important;
  font-size: 13px !important;
  font-weight: 800 !important;
  letter-spacing: 0 !important;
  text-align: center !important;
}

/* ══════════ 右侧一律距程序边 40px（产品早先就定的规格） ══════════
 *
 * ⚠️ 我之前只把这条加在了「记牌器」上，漏了「整理」和「顶部按钮」——产品这次指出来了。
 * 公式：right: calc(max(0px, 40px - var(--edge-r)))
 *   --edge-r = 安全区右侧 inset（苹果岛/圆角）；没有安全区时就等于距屏幕边 40px。
 * 所以这是"距**程序可用区的右边** 40px"，两种机器上都对。
 */
/* ① 整理按钮（右下角） */
#btnTidy,
#actions #btnTidy {
  right: calc(14px + max(0px, 26px - var(--edge-r, 0px))) !important;
}
/* ② 记牌器 */
#counter {
  right: calc(max(0px, 40px - var(--edge-r, 0px))) !important;
}
/* ③ 顶部按钮那一排（退出/全屏/设置/重开）—— 容器 id 不确定，常见几种都写上 */
#topBar, #topbar, #topRight, #top-right, #top, .top-bar, .topbar,
#topBtnRow, #topButtons, .top-actions, #app > .top, #header, header {
  right: calc(max(0px, 40px - var(--edge-r, 0px))) !important;
  left: auto !important;
}

/* ══════════ 两侧统一距程序边 40px（产品口径：左右都要） ══════════
 *
 * 左：left : calc(max(0px, 40px - var(--edge-l)))
 * 右：right: calc(max(0px, 40px - var(--edge-r)))
 * 安全区是 insets，所以要减掉——这样苹果岛/圆角机型与普通机型**都是距可用区 40px**。
 *
 * ⚠️ 之前只加在记牌器上，产品两次指出漏项；这次把两侧同类元素一次找齐。
 */
/* ── 左侧 ── */
.seat.pos-left,
#bottom .pos-bottom,
#handWrap,
#teams,
.teams,
#score,
#scoreBox,
.score-box {
  left: calc(max(0px, 40px - var(--edge-l, 0px))) !important;
}
/* ── 右侧 ── */
.seat.pos-right,
#topBar, #topbar, #topRight, #top-right, .top-bar, .topbar, header,
#counter,
#btnTidy {
  right: calc(max(0px, 40px - var(--edge-r, 0px))) !important;
}
/* 整理仍贴屏幕底 2px（上一轮定的），只改横向 */
#btnTidy {
  left: auto !important;
}

/* ══════════ 距屏幕物理边 40px（修正：不做安全区减法） ══════════
 *
 * ⚠️ 上一版写成 calc(max(0px, 40px - var(--edge-r)))，本意是"距可用区 40px"，
 *    但 iPhone 横屏安全区约 59px → max(0, -19) = 0 → 记牌器/整理**贴到屏幕最边上**，
 *    正是产品截图里看到的问题。
 * 产品口径：**距屏幕边 40px**。直接写 40px（相对视口），所有机型一致。
 * `!important` 覆盖掉上一版的安全区公式。
 */
/* 左侧 */
.seat.pos-left,
#teams, .teams, #score, #scoreBox, .score-box,
#handWrap {
  left: 40px !important;
}
/* 右侧 */
.seat.pos-right,
#topBar, #topbar, #topRight, #top-right, .top-bar, .topbar, header,
#counter,
#btnTidy {
  right: 40px !important;
}
/* 整理仍贴屏幕底 2px —— 只改横向，纵向保持 */
#btnTidy {
  left: auto !important;
  bottom: 2px !important;
}

/* ══════════ 两个侧边座位：只用一个方向定位，精确 40px ══════════
 *
 * 自检面板实测（视口 849x450）：左家 左=44 ✗、右家 右=43 ✗（应为 40）。
 * 原因：座位原本用 `right` 定位（左家算出 right:-40px），我上一版只写了 `left`，
 *       left/right 同时存在会过约束，最终结果不是我要的 40px。
 * 现在两个座位都改成"只留一个方向"，另一个显式 auto。
 */
/*
 * ⚠️ 为什么是 36px 而不是 40px：
 *   探针实测：座位是相对**祖先 MAIN 的 padding 盒**定位的，
 *   而 MAIN 有 `padding: 0 4px`（左右各 4px）。
 *   于是 left:40px 实际渲染出来是 44px（右家同理 44px）——这正是产品看到的偏差。
 *   所以这里写 40 - 4 = 36px，渲染结果正好是距屏幕边 40px。
 *   若哪天 MAIN 的 padding 变了，这个数要跟着改（下面量过才上线）。
 */
/*
 * ⚠️ 关键：产品要的是**头像**距屏幕边 40px，不是座位外壳。
 *   探针实测：座位盒左边缘在 44px，而座位**里面的头像**又在内部缩了 36px
 *   （头像左边缘实测 80px）——所以光把座位调到 40 还不够，头像看着仍然靠内。
 *   这里把座位再往外挪 36px（4px），头像就落在 4 + 36 = 40px。
 *   座位往外挪会让"出牌区"整体也往中心方向让 36px，符合"出牌区朝桌面中心生长"的既有设计。
 *   （若 MAIN 的 padding 或座位内部缩进变了，这个数要跟着改。）
 */
.seat.pos-left {
  left: 0px !important;   /* 渲染成 4px（MAIN 的 padding 4px）→ 头像 = 4 + 36 = 40 ✓ */
  right: auto !important;
}
.seat.pos-right {
  right: 0px !important;  /* 同上：渲染成 4px → 头像距右 40 ✓ */
  left: auto !important;
}

/* ══════════ 底部座位（我自己的头像）：与上家头像对齐，距左边 40px ══════════
 *
 * 实测：头像在座位内部缩进 0px，所以座位 left 写 40px，
 * 渲染时 MAIN 的 4px padding 会再加，最终头像正好落在距屏幕左边 40px。
 * （改前头像实测 70px，产品要求 40px。）
 */
/*
 * ⚠️ 为什么是 10px：实测座位 left 属性写 40px 时，渲染出来的左边缘是 70px
 *   （座位之外还有 30px 的来源，和左家座位遇到的情况同类）。
 *   头像本身在座位内不再内缩，所以座位 left 写 40 - (70-40) = 10px，
 *   渲染后头像正好落在距屏幕左边 40px，与上家头像同一条竖线。
 */

/* ══════════ 底部座位（我自己的头像）：与上家头像对齐，距左边 40px ══════════
 *
 * 实测：头像在座位内部缩进 0px，所以座位 left 写 40px，
 * 渲染时 MAIN 的 4px padding 会再加，最终头像正好落在距屏幕左边 40px。
 * （改前头像实测 70px，产品要求 40px。）
 */

/* ══════════ 底部座位（我自己的头像）：与上下家头像同一条竖线，距左边 40px ══════════
 *
 * 实测：座位 left 属性写 40px 时，渲染出来的左边缘是 70px（座位之外还有 30px 来源），
 * 头像在座位内不再内缩。所以属性写 40 - (70-40) = 10px，
 * 渲染后头像正好落在距屏幕左边 40px。
 */

/* ══════════ 底部座位里"我自己的头像"：距左边 40px ══════════
 *
 * 实测（?diag=1 面板 + 探针）：底部座位的横向位置不受 left 控制
 * （写 10px 也不动，写 40px 渲染仍是 70px），所以上一版改座位是错的——
 * 座位一动，里面的「几游」标签就盖到头像上（产品实测反馈）。
 *
 * 这一版只动**头像本身**，而且用负 margin 而不是定位：
 * margin 会让后面的兄弟元素（几游标签 / 昵称）**一起左移**，
 * 于是"标签距头像 5px"的关系原样保留，同时头像落到 40px。
 * 70 - 40 = 30，故 -30px。
 */
.seat.pos-bottom .avatar {
  margin-left: -30px !important;
}

/* ══════════════════════════════════════════════════════════════════════════
 * 【尺寸统一化】从已删除的窄屏 / 横屏断点里搬出来的规则（与"尺寸分档"无关）
 * ══════════════════════════════════════════════════════════════════════════
 *
 * 产品口径：这个项目**只面向手机（横屏）**，取值只留手机上那一套，
 * 不许再出现"按屏幕尺寸给不同取值"的代码。
 *
 * 因此删掉了两个会改尺寸的媒体查询：
 *   @media (max-width:720px), (orientation:landscape) and (max-height:520px)
 *   @media (orientation:landscape) and (max-height:520px), (orientation:portrait) and (max-width:720px) and (pointer:coarse)
 * （连同上份重复 CSS 里那三个永远不生效的同类断点一起删了），
 * 统一后的值直接写进了它们各自选择器的**基础规则**里，不再靠断点切换：
 *   #table   grid-template-columns: 104px 1fr 104px ; padding: 0 4px
 *   #bottom  height: 64px ; --me-col: 60px ; --footer-bottom: 30px
 *   #actions flex-direction: row ; align-items: center
 *   .sel     max-width: 38vw
 *   底栏头像 50 × 50（.seat .avatar 基础规则本来就是 50，无需改）
 *
 * 下面这几条是断点里**与尺寸无关**、但只写在断点里会丢功能的规则 ——
 * 搬到这里（文件最后，同优先级必胜），任何尺寸下都成立。
 */

/*
 * 顶栏那排小按钮：原来只在窄屏断点里，宽屏会回落到 .btn 的默认值，
 * 于是同一排按钮在两种尺寸下不一样大。现在统一。
 */
.table-topbar .btn.ghost.small {
  padding: 4px 8px;
  font-size: 12px;
}

/*
 * 底栏里"我"那一格：头像与昵称 / 金豆**横排**（头像在左、文字在右）。
 * 窄屏基础规则是竖排，横屏断点才改横排 —— 现在统一成横排。
 *
 * ⚠️ 只写"排布方向"，不碰左右偏移：left/right 与 gap 由上面补丁区的
 *    .pos-left / .pos-right / .pos-bottom 那几段控制（产品已验收）。
 */
#bottom .pos-bottom .seat-head {
  flex-direction: row;
  align-items: center;
  gap: 4px;
}
/* 竖排时"张数"居中即可；改横排后要顶部对齐，否则文字会跟着头像上下飘 */
#bottom .pos-bottom .seat-meta {
  align-items: flex-start;
}
/* ══════════ 旋转模式下安全区改取 top/bottom（修"苹果不对"的真因） ══════════
 *
 * 竖屏时应用被 `transform: rotate(90deg)` 旋转（顺时针）→ 应用内 +x 轴朝物理下方，
 * 于是应用里的"左"= 物理**上**边、"右"= 物理**下**边。
 * 竖屏的刘海在物理上边，所以要取 safe-area-inset-top/bottom，
 * 取 left/right 会得到 0（原来就是这样，所以苹果的边缘距离不对；电脑不旋转、vivo 安全区≈0，都不受影响）。
 *
 * 触发条件与既有的旋转规则一致（竖屏 + 窄 + 粗指针）。
 */
@media (orientation: portrait) {
  html {
    --edge-off-l: max(0px, var(--edge-gap) - env(safe-area-inset-top, 0px));
    --edge-off-r: max(0px, var(--edge-gap) - env(safe-area-inset-bottom, 0px));
  }
}

/* ══════════ 通用边距（探针实测版） ══════════
 *
 * 产品口径：距边 = max(40px, 安全区)；不写死任何具体安全区数值。
 *
 * ⚠️ 关键：这个变量**必须先定义**。上一版定义被驳回后回滚删掉，
 *    只留下引用 → 所有引用声明失效、right 回退 auto → 元素乱跑（连错 5 次的真因）。
 *
 * 三类元素的基准不同，所以写法不同（都由探针现场试写验证过）：
 *   ① 整理（fixed，相对视口）→ right: max(gap, 安全区)
 *   ② 记牌器（absolute，相对视口）→ right: max(gap, 安全区)
 *   ③ 我的头像（在 body 的安全区内边距盒里）→ 负 margin 要随安全区收回：
 *        -30px（无安全区）→ 0（安全区 ≥ 40）
 *        公式：-1 * min(30px, max(0px, gap - 安全区))
 *   左右两家座位已经正确（40 / 安全区），本次不动 ✗
 */
html {
  --edge-gap: 40px;   /* ← 唯一常数：只改这一处即可调整"两侧距边" */
}

/* ① 整理 */
#btnTidy,
#actions #btnTidy {
  right: max(var(--edge-gap), env(safe-area-inset-right, 0px)) !important;
  left: auto !important;
}

/* ② 记牌器 */
#counter {
  right: max(var(--edge-gap), env(safe-area-inset-right, 0px)) !important;
}

/* ③ 我自己的头像 */
.seat.pos-bottom .avatar {
  /*
   * 由真机两点实测反推（不是猜）：
   *   本地安全区 0  → 需要 margin −30px 才能距左 40 ✓
   *   苹果安全区 62 → 需要 margin +10px 才能距左 62 ✓（原来算成 0 → 落到 52 ✗）
   * 两点解出：margin = 10px − max(0px, gap − 安全区)
   *   安全区 0  → 10 − 40 = −30 ✓
   *   安全区 62 → 10 − 0  = +10 ✓
   */
  margin-left: calc(10px - max(0px, var(--edge-gap) - env(safe-area-inset-left, 0px))) !important;
}

/* ══════════ 贴边工具类（以后新增贴边元素统一用这两个） ══════════
 *
 * 用法（相对视口定位的元素，position: fixed 或 absolute）：
 *     <button id="newBtn" class="edge-right">新按钮</button>
 *     <div id="newPanel" class="edge-left">…</div>
 *
 * 效果：距屏幕边 = max(40px, 安全区) —— 电脑/安卓 40 ✓ 刘海机取安全区 ✓
 *       安全区数值由设备自己提供（env()），**代码里没有写死任何机型值** ✓
 *       想统一调"距边多少"，只改 html 里的 --edge-gap 一处 ✓
 *
 * ⚠️ 两种坐标系，别用错：
 *   ① 相对**视口**定位（fixed / absolute，且祖先没有安全区内边距）
 *        → 用本工具类（.edge-right / .edge-left）✓
 *   ② 元素在 **body 的安全区内边距盒里**（普通流式元素，位置由 body padding 决定）
 *        → 不要用工具类 ✗ 要用"减法形式"（因为安全区已经把它推进来了）：
 *          right: max(0px, var(--edge-gap) - env(safe-area-inset-right, 0px));
 *       现有例子：左右两家座位用 left/right: 0 直接借 body 的 padding ✓
 *                我自己的头像用 margin-left: calc(10px - max(0px, 40px - 安全区)) ✓
 *
 * 判断方法：本地（安全区 0）量出来是 40px 就对了 ✓
 */
.edge-right {
  right: max(var(--edge-gap), env(safe-area-inset-right, 0px)) !important;
}
.edge-left {
  left: max(var(--edge-gap), env(safe-area-inset-left, 0px)) !important;
}

/* ══════════════════════════════════════════════════════════════════════════
 * 主页（三个模式入口：经典 / 开房间 / 疯狂炸）—— 产品点名要的第一个功能
 *
 * ⚠️ 为什么这一整段必须加在**文件最末尾**：
 *   本项目 style.css 是历史层层叠加的文件（已逐行 diff 验证）：
 *     ① 1–2070      第一份，**已经失效**
 *     ② 2071–4130   第二份，逐字节等于第一份的前半段（:root / #app / body /
 *                   .btn / .autoplay-mask 都在这里），**目前生效的是这一份**
 *     ③ 4131–5773   补丁区，一条条覆盖前面
 *   所以任何新样式都必须写在③之后（本段），否则会被后面那些规则按"同优先级
 *   后来居上"顶掉 —— 这个坑文件里已经记了好几次（见 #actionCenter 那段注释）。
 *
 * ⚠️ 为什么用 position: absolute 而不是 fixed：
 *   和 .autoplay-mask 同一个理由（见上面「跟随 App 的横屏坐标系」那段实测记录）：
 *   强制横屏是把 #app 整体 rotate(90deg) 实现的，部分安卓浏览器会把 fixed 子元素
 *   退回视口定位，于是在横屏牌桌上浮出一块**竖屏**的罩子（vivo 上产品实测报过）。
 *   #homeLayer 挂在 #app 里面，absolute 天然跟着 #app 的坐标系走。
 *   注意此时 #app 通常是 static，absolute 会退回初始包含块（= 视口），结果一样对。
 *
 * ⚠️ z-index 760 的取法（不是随手写的）：
 *   · 必须**高于** #autoplayMask(650)，否则灰罩子会盖在主页上
 *   · 必须**低于** #settings(800)，否则设置面板打不开 —— 而设置里的「返回主界面」
 *     正是回到主页的入口，被挡住就回不去了
 *   · 高于 .toast(50)、横幅 .banner(700)：主页在的时候这些都是牌局的东西
 *     （"进局后才会有"的提示），不该漏到主页上
 *
 * 手机上只考虑**横屏**（产品口径）：整体按 vh 定尺寸，vh 小的时候字自动变小，
 * 三个模块 + 标题在常见的 360~430 高横屏里放得下；不给按钮写死高度，
 * 真遇到极矮的屏也只会更紧凑，不会把某个模块挤出屏幕。
 * 零外部资源：没有字体、没有图片、没有 icon 文件，图标就是 ♠♥♣ 三个字符。
 * ══════════════════════════════════════════════════════════════════════════ */

/*
 * position: relative 放在前面，是给"#app 恰好成了定位元素"那一种情况兜底：
 * 强制横屏那段媒体查询会给 #app 加 position: fixed + 安全区 padding，
 * 此时**下面这层相对定位优先**，让 #homeLayer 的 inset:0 对齐 #app 的 **padding 盒**，
 * 左右就把刘海 / Home 指示条让开了 —— 和牌桌自己借 body padding 让开同一个道理。
 * 没旋转时 #app 是 static，这条不生效，absolute 退回初始包含块（视口），也对。
 */
.home-layer {
  position: relative;
  position: absolute;
  inset: 0;
  z-index: 760;
  display: flex;
  align-items: center;
  justify-content: center;
  /*
   * 背景：和 body / 强制横屏那套用**同一组桌面底色变量**（--felt-1/--felt-2），
   * 不自己发明颜色 —— 这样主页和牌桌是同一个"桌布"，切换时不会色差跳变。
   * 这层必须**不透明**：它是盖住牌桌的，透出去就穿帮了。
   */
  background: radial-gradient(circle at 50% 32%, var(--felt-1), var(--felt-2) 74%);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  overflow: hidden;
  animation: home-in 0.18s ease-out;
}
/*
 * 主页隐藏后必须**彻底不吃点击**。
 * .hidden 本身已经是 display:none !important（见前面的定义），天生就收不到点击；
 * 这里再显式补一条 pointer-events:none，把"万一 display 被别的规则顶回来"的余地也堵上。
 * （主页层是覆盖全屏的浮层，只要它还参与命中测试，牌桌上的按钮就全点不到了。）
 */
.home-layer.hidden {
  display: none !important;
  pointer-events: none;
}
/*
 * 主页状态不只依赖覆盖层遮住牌桌：移动浏览器在旋屏/视口变化时偶尔会把
 * fixed 的底栏牌区绘制到覆盖层之外。主页仍保留原牌局状态，但不绘制游戏层，
 * 再进游戏时由 hideHome 恢复显示。
 */
#app.home-screen > #table,
#app.home-screen > #bottom {
  visibility: hidden !important;
  pointer-events: none !important;
}
@keyframes home-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

/* 内容列：标题 + 三个模块 + （可能弹出来的）开发中提示 */
.home-box {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(8px, 2.4vh, 22px);
  padding: 0 clamp(12px, 4vw, 40px);
  max-height: 100%;
}

/* ── 标题 ── */
.home-logo {
  display: flex;
  flex-direction: column;
  align-items: center;
  line-height: 1.05;
}
.home-logo-cn {
  font-size: clamp(24px, 7.6vh, 52px);
  font-weight: 900;
  letter-spacing: clamp(4px, 1.6vw, 14px);
  /* 金色渐变字：和 .btn.accent 同一族颜色（#d8a83a → #b8862a）再加一个亮顶 */
  background: linear-gradient(180deg, #ffe9a8 0%, #f0c85c 45%, #c9922b 100%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  /* 渐变文字在部分浏览器里会失去阴影，所以垫一层淡淡的光晕保证在任何底色上都可读 */
  filter: drop-shadow(0 2px 10px rgba(0, 0, 0, 0.45));
}
.home-logo-sub {
  font-size: clamp(9px, 2.1vh, 14px);
  font-weight: 700;
  letter-spacing: clamp(3px, 1.2vw, 8px);
  color: rgba(255, 255, 255, 0.42);
  margin-top: 0.35em;
}

/* ── 三个模块 ── */
.home-modes {
  display: flex;
  gap: clamp(8px, 1.8vw, 20px);
  align-items: stretch;
  justify-content: center;
  /*
   * flex-wrap 是**兜底**：正常手机横屏三个模块一行放得下，但极窄的屏
   *（比如横屏后可用宽只有 480 的小机）宁可折成两行，也不能把「疯狂炸」
   * 挤到屏幕外面 —— 那样最后一个模块就点不到了。
   */
  flex-wrap: wrap;
  max-width: 100%;
}
.home-mode {
  /*
   * 卡片外形沿用现有 UI 语言：深绿卡片 + 白粗体 + 圆角，
   * 和 .set-row（设置里的行按钮）是同一套观感，玩家不会觉得是两个应用。
   */
  display: flex;
  align-items: center;
  gap: clamp(6px, 1.2vw, 14px);
  min-width: clamp(96px, 20vw, 190px);
  padding: clamp(10px, 2.4vh, 22px) clamp(10px, 2vw, 24px);
  border: 2px solid rgba(255, 255, 255, 0.16);
  border-radius: clamp(12px, 2vh, 20px);
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0.04));
  color: #fff;
  font-family: inherit;
  text-align: left;
  cursor: pointer;
  transition: filter 0.12s, transform 0.08s, border-color 0.12s;
  -webkit-tap-highlight-color: transparent;
}
.home-mode:hover:not(:disabled) {
  filter: brightness(1.16);
  border-color: rgba(240, 200, 92, 0.5);
}
.home-mode:active:not(:disabled) {
  transform: translateY(2px);
}
.home-mode .hm-ico {
  font-size: clamp(18px, 4.4vh, 34px);
  line-height: 1;
  color: var(--gold);
  text-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
}
.home-mode .hm-txt {
  display: flex;
  flex-direction: column;
  gap: 0.15em;
}
.home-mode .hm-name {
  font-size: clamp(15px, 3.4vh, 26px);
  font-weight: 800;
  letter-spacing: 2px;
  white-space: nowrap;
}
.home-mode .hm-desc {
  font-size: clamp(9px, 1.9vh, 13px);
  font-weight: 600;
  color: rgba(255, 255, 255, 0.62);
  white-space: nowrap;
}
/*
 * 「经典」= 主推的那个（就是现在的玩法），做成**橙金渐变实心胶囊**，
 * 和「出牌」按钮同一种质感（#ffd98a → #ffab2e → #f08c00 + 白色粗体），
 * 让玩家一眼知道"点这个就开打"。
 */
#homeClassic {
  background: linear-gradient(180deg, #ffd98a 0%, #ffab2e 55%, #f08c00 100%);
  border-color: rgba(255, 255, 255, 0.6);
  box-shadow: 0 8px 22px rgba(0, 0, 0, 0.38), inset 0 2px 0 rgba(255, 255, 255, 0.5);
}
#homeClassic .hm-ico,
#homeClassic .hm-desc {
  /* 橙金底上白字要够重才压得住，深棕反而更清楚（和 #btnPlay 一致的做法） */
  color: #7a3d00;
  text-shadow: none;
}
#homeClassic .hm-name {
  color: #fff;
  text-shadow: 0 1px 2px rgba(120, 60, 0, 0.45);
}

/* ── 「开房间」开发中提示卡：弹在主页**内部** ──
 * 不用全局 #toast 的原因：它的 z-index 只有 50，比主页层(760)低，会被主页挡住看不见。
 */
.home-notice {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(4px, 1.2vh, 10px);
  max-width: min(560px, 84vw);
  padding: clamp(10px, 2.6vh, 22px) clamp(14px, 3vw, 30px);
  border: 2px solid rgba(240, 200, 92, 0.45);
  border-radius: clamp(12px, 2vh, 18px);
  background: rgba(0, 0, 0, 0.45);
  box-shadow: 0 10px 28px rgba(0, 0, 0, 0.45);
  animation: home-in 0.16s ease-out;
}
.home-notice.hidden {
  display: none !important;
}
.home-notice .hn-title {
  font-size: clamp(14px, 3.2vh, 22px);
  font-weight: 800;
  letter-spacing: 1px;
  color: var(--gold);
}
.home-notice .hn-text {
  font-size: clamp(10px, 2.1vh, 14px);
  font-weight: 600;
  line-height: 1.5;
  text-align: center;
  color: rgba(255, 255, 255, 0.86);
}
.home-notice .hn-back {
  margin-top: clamp(2px, 0.8vh, 8px);
  padding: clamp(6px, 1.4vh, 12px) clamp(16px, 3vw, 28px);
  font-size: clamp(12px, 2.4vh, 17px);
  border-radius: 999px;
  font-weight: 800;
}

/* ══════════════════════════════════════════════════════════════════════════
 * 开房间 —— 房间界面（建房 / 邀请 / 准备 / 开始 / 离开）
 *
 * ⚠️ 为什么这一整段又必须加在**文件最末尾**：
 *   和上面「主页」那段同一个理由，这里照抄它的结论（本项目 style.css 是历史
 *   层层叠加的文件，已逐行 diff 验证）：
 *     ① 1–2070      第一份，**已经失效**
 *     ② 2071–4168   第二份，**目前生效的基础**（:root / #app / body / .btn …）
 *     ③ 4169–6012   补丁区，一条条覆盖前面
 *   房间层是主页层的兄弟，凡是要和 `.home-layer` / `.btn.accent` 抢同一条属性的
 *   写法（position / z-index / 背景），**只有写在文件最后才说得上话** ——
 *   同优先级"后来居上"，这个坑文件里已经记了好几次（见 #actionCenter 那段）。
 *
 * ⚠️ 为什么是 position: absolute 而不是 fixed：
 *   和 #homeLayer / #autoplayMask 完全同款的理由（见上面「跟随 App 的横屏坐标系」
 *   那段实测记录）：强制横屏是把 #app 整体 `rotate(90deg)` 实现的，部分安卓浏览器
 *   会把 fixed 子元素退回**视口**定位，于是在横屏牌桌上浮出一块**竖屏**的罩子
 *   （vivo 上产品实测报过）。#roomLayer 挂在 #app 里面，absolute 天然跟着 #app
 *   的坐标系走。
 *
 * ⚠️ z-index 770 = 比主页层(760)高一档、比设置面板(800)低：
 *   · 高于主页层 —— 从 /r/<房号> 进来时房间界面要盖在主页上面（同 z-index 时
 *     后出现的 #roomLayer 本来也赢，但把数写清楚，以后加层不会踩到）；
 *   · 低于设置面板 —— 设置里的「返回主界面」是回主页的入口，不能被挡住。
 *
 * ⚠️ 默认路径（经典 / 疯狂炸）一个像素都不受影响：这一层默认带 hidden
 *   （display:none !important），不走「开房间」这条路就永远不参与布局。
 *
 * 手机上只考虑**横屏**（产品口径）：尺寸全部按 vh 走 clamp，横屏常见的
 * 360~430 高时字和卡片自动变小、内容放得下；不给按钮写死高度，屏再矮也只会更紧凑。
 * 零外部资源 + 沿用现有变量：背景走 --felt-1/--felt-2（和主页/牌桌同一块桌布，
 * 切换时不色差跳变），主按钮沿用 #btnPlay 那套橙金渐变（#ffd98a→#ffab2e→#f08c00）。
 * ══════════════════════════════════════════════════════════════════════════ */

/*
 * position: relative 放前面，给"#app 恰好成了定位元素"那种情况兜底：
 * 强制横屏那段媒体查询会给 #app 加 position: fixed + 安全区 padding，
 * 此时**下面这层相对定位优先**，让 inset:0 对齐 #app 的 padding 盒，
 * 左右就把刘海 / Home 指示条让开了（和 .home-layer 一字不差的处理）。
 */
.room-layer {
  position: relative;
  position: absolute;
  inset: 0;
  z-index: 770;
  display: flex;
  align-items: center;
  justify-content: center;
  /* 必须**不透明**：它是盖住牌桌/主页的，透出去就穿帮了 */
  background: radial-gradient(circle at 50% 32%, var(--felt-1), var(--felt-2) 74%);
  -webkit-tap-highlight-color: transparent;
  overflow: hidden;
  animation: home-in 0.18s ease-out; /* 复用主页那段关键帧，切换观感一致 */
}
/*
 * 收起来时**彻底不吃点击**：.hidden 本身就是 display:none !important，
 * 这里再补一条 pointer-events:none，把"万一 display 被别的规则顶回来"的余地堵上
 * （房间层是全屏浮层，只要还参与命中测试，底下的牌桌按钮就全点不到了）。
 */
.room-layer.hidden {
  display: none !important;
  pointer-events: none;
}

/* 内容列：标题行 + （大厅 | 房内两块之一） */
.room-box {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(6px, 1.8vh, 18px);
  width: 100%;
  max-height: 100%;
  padding: clamp(8px, 2vh, 20px) clamp(12px, 4vw, 40px);
  /* 极矮的横屏（比如 320 高）宁可让内容自己滚，也不能把「开始游戏」挤出屏幕外 */
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}

.room-head {
  display: flex;
  align-items: center;
  gap: clamp(6px, 1.4vw, 16px);
  width: 100%;
}
.room-head .room-title {
  font-size: clamp(13px, 2.8vh, 20px);
  font-weight: 900;
  letter-spacing: 2px;
  color: var(--gold);
}
/* 右侧的状态摘要：3/4 人 · 你是房主 · 还有 1 人未准备 */
.room-head .room-hint {
  margin-left: auto;
  font-size: clamp(9px, 1.9vh, 13px);
  font-weight: 600;
  color: rgba(255, 255, 255, 0.62);
  white-space: nowrap;
}

/* ── 两块互斥的内容：大厅（建房/输房号）与房内 ── */
.room-lobby,
.room-in {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(6px, 1.8vh, 18px);
  width: 100%;
}
.room-lobby.hidden,
.room-in.hidden {
  display: none !important;
}

/* 「创建房间」：主推动作，沿用 #btnPlay / #homeClassic 那套橙金渐变实心胶囊 */
.room-layer .room-big {
  min-width: clamp(150px, 30vw, 280px);
  padding: clamp(10px, 2.4vh, 20px) clamp(24px, 6vw, 60px);
  border: 2px solid rgba(255, 255, 255, 0.6);
  border-radius: 999px;
  background: linear-gradient(180deg, #ffd98a 0%, #ffab2e 55%, #f08c00 100%);
  box-shadow: 0 8px 22px rgba(0, 0, 0, 0.38), inset 0 2px 0 rgba(255, 255, 255, 0.5);
  color: #fff;
  font-size: clamp(15px, 3.4vh, 24px);
  font-weight: 900;
  letter-spacing: 2px;
  text-shadow: 0 1px 2px rgba(120, 60, 0, 0.45);
}
.room-layer .room-big:disabled {
  opacity: 0.72;
  filter: brightness(0.92);
}

/* 输房号加入：数字键盘（inputmode=numeric 在 index.html 上）+ 大号居中数字 */
.room-join {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(6px, 1.4vw, 16px);
  flex-wrap: wrap;
  max-width: 100%;
}
.room-layer .room-code-inp {
  flex: 0 0 auto;
  width: clamp(120px, 22vw, 230px);
  padding: clamp(6px, 1.4vh, 12px) clamp(8px, 1.5vw, 16px);
  background: rgba(0, 0, 0, 0.35);
  border-radius: clamp(10px, 1.8vh, 14px);
  font-size: clamp(20px, 5vh, 34px);
  font-weight: 900;
  letter-spacing: clamp(2px, 0.8vw, 6px);
  text-align: center;
  font-variant-numeric: tabular-nums;
}
/*
 * 占位符（"6 位房号"）跟着输入框的大字号走会喧宾夺主 ——
 * 实测截图里空输入框比「创建房间」还抢眼。压小、压淡、去掉大字距。
 */
.room-layer .room-code-inp::placeholder {
  font-size: 0.5em;
  font-weight: 700;
  letter-spacing: 1px;
  color: rgba(255, 255, 255, 0.42);
}
.room-join .btn {
  padding: clamp(7px, 1.7vh, 14px) clamp(16px, 3.4vw, 34px);
  border-radius: 999px;
  font-size: clamp(12px, 2.6vh, 18px);
  font-weight: 800;
}
.room-tip {
  font-size: clamp(9px, 1.8vh, 13px);
  font-weight: 600;
  color: rgba(255, 255, 255, 0.55);
  text-align: center;
}

/* ── 房内：房号（大字号，方便念给朋友）+ 复制邀请链接 ── */
.room-code-row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(6px, 1.4vw, 18px);
  flex-wrap: wrap;
}
.room-code-cap {
  font-size: clamp(10px, 2vh, 14px);
  font-weight: 700;
  letter-spacing: 2px;
  color: rgba(255, 255, 255, 0.55);
}
/* 房号用和主页标题同款的金色渐变字：一眼看清、隔着桌子也念得出来 */
.room-code-text {
  font-size: clamp(26px, 8.4vh, 54px);
  font-weight: 900;
  letter-spacing: clamp(3px, 1.4vw, 12px);
  font-variant-numeric: tabular-nums;
  background: linear-gradient(180deg, #ffe9a8 0%, #f0c85c 45%, #c9922b 100%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  filter: drop-shadow(0 2px 10px rgba(0, 0, 0, 0.45));
}
.room-code-row .btn {
  padding: clamp(6px, 1.5vh, 12px) clamp(12px, 2.6vw, 24px);
  border-radius: 999px;
  font-size: clamp(11px, 2.3vh, 16px);
  font-weight: 800;
}
/*
 * ⚠️ 「邀请链接」那行网址的样式**已随元素一起删掉**（那条规则只挂在被删掉的那个 div 上）。
 *    产品口径：界面上不再留那行 `http://…/r/<房号>`，链接只走「复制邀请链接」；
 *    不在界面上常驻显示邀请网址。
 */

/* ── 4 个位置 ── */
.room-seats {
  display: flex;
  align-items: stretch;
  justify-content: center;
  gap: clamp(6px, 1.4vw, 16px);
  flex-wrap: wrap;
  max-width: min(880px, 94vw);
}
.room-seat {
  display: flex;
  flex: 1 1 0;
  flex-direction: column;
  align-items: center;
  gap: clamp(2px, 0.7vh, 6px);
  min-width: clamp(74px, 15vw, 130px);
  max-width: clamp(96px, 19vw, 170px);
  padding: clamp(6px, 1.6vh, 14px) clamp(6px, 1vw, 12px);
  border: 2px solid rgba(255, 255, 255, 0.16);
  border-radius: clamp(10px, 1.8vh, 16px);
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0.04));
}
/* 我那一格：金边，一眼找到自己 */
.room-seat.self {
  border-color: rgba(240, 200, 92, 0.78);
  box-shadow: inset 0 0 0 2px rgba(240, 200, 92, 0.18), 0 6px 18px rgba(0, 0, 0, 0.32);
}
/* 空位：虚框 + 压暗，和"有人"一眼可分 */
.room-seat.empty {
  border-style: dashed;
  opacity: 0.55;
}
.room-seat .rs-pos {
  font-size: clamp(9px, 1.8vh, 12px);
  font-weight: 700;
  letter-spacing: 1px;
  color: rgba(255, 255, 255, 0.5);
}
.room-seat .rs-name {
  max-width: 100%;
  overflow: hidden;
  font-size: clamp(12px, 2.6vh, 19px);
  font-weight: 800;
  color: #fff;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.room-seat.empty .rs-name {
  color: rgba(255, 255, 255, 0.45);
}
.room-seat .rs-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 3px;
  justify-content: center;
}
.rs-tag {
  padding: 1px 6px;
  border: 1px solid rgba(255, 255, 255, 0.22);
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.22);
  color: rgba(255, 255, 255, 0.78);
  font-size: clamp(8px, 1.6vh, 11px);
  font-weight: 700;
  white-space: nowrap;
}
.rs-tag.me {
  border-color: rgba(255, 255, 255, 0.5);
  background: linear-gradient(180deg, #ffd98a, #f0b02e);
  color: #241c05;
}
.rs-tag.host {
  border-color: rgba(255, 255, 255, 0.45);
  background: linear-gradient(180deg, #ffe9a8, #e0b93f);
  color: #241c05;
}
.rs-tag.human {
  border-color: rgba(120, 190, 255, 0.45);
  background: rgba(40, 90, 150, 0.45);
  color: #dbeeff;
}
.rs-tag.bot {
  border-color: rgba(170, 160, 255, 0.45);
  background: rgba(70, 60, 160, 0.42);
  color: #e8e4ff;
}
.rs-tag.ready {
  border-color: rgba(90, 220, 150, 0.6);
  background: rgba(35, 140, 92, 0.62);
  color: #dcf7e8;
}

/* ── 三个动作按钮 ── */
.room-actions {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(6px, 1.4vw, 16px);
  flex-wrap: wrap;
}
.room-actions .btn {
  padding: clamp(7px, 1.7vh, 14px) clamp(14px, 3vw, 30px);
  border-radius: 999px;
  font-size: clamp(12px, 2.5vh, 17px);
  font-weight: 800;
}
/* 「开始游戏」= 这一屏的主推动作 → 和 #btnPlay 同一个橙金渐变 */
#roomStartBtn {
  border-color: rgba(255, 255, 255, 0.6);
  background: linear-gradient(180deg, #ffd98a 0%, #ffab2e 55%, #f08c00 100%);
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.34), inset 0 2px 0 rgba(255, 255, 255, 0.45);
  color: #fff;
  text-shadow: 0 1px 2px rgba(120, 60, 0, 0.45);
}
/*
 * 点不了的时候（不是房主 = "等房主开始" / 已经开局 = "已开局"）。
 *
 * ⚠️ 这里和 #actionCenter 的 .btn:disabled 处理**不一样**，是刻意的：
 *   牌桌上"出牌"没选牌时仍要保持橙色（参照图要求，去色会变脏橄榄），
 *   而这个房间按钮点不了就是点不了，**必须一眼看出它不生效** ——
 *   实测截图里"等房主开始"只压暗一点，看上去和可点的金色按钮几乎一样，
 *   访客会一直点它。所以这里换成中性描边 + 半透明底，把橙色整个撤掉。
 */
#roomStartBtn:disabled {
  border-color: rgba(255, 255, 255, 0.18);
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.13), rgba(255, 255, 255, 0.05));
  box-shadow: none;
  color: rgba(255, 255, 255, 0.6);
  text-shadow: none;
}
/* 其余动作按钮（准备 / 离开）不可点时只压暗，保留底色 */
.room-actions .btn:disabled {
  opacity: 0.55;
}
/* 「已准备」状态：按钮转成绿色实心（和 .btn.primary 同一族色） */
.room-actions .btn.on {
  border-color: #35b673;
  background: linear-gradient(180deg, #2f9e63, #1d7a49);
  color: #fff;
}

/* ══════════════════════════════════════════════════════════════════════════
 * 房间界面 · 重做版（选对家 / 加电脑 / 四方位排布）
 *
 * ⚠️ 为什么又追加到**文件最末尾**（而不是改上面那段 .room-* ）：
 *   这份样式表的结构是"后面覆盖前面"（见本文件顶部与 6010 行附近那段说明）：
 *     1–2070 失效 ｜ 2071–4168 生效基础 ｜ 4169+ 补丁区，**越靠后越生效**
 *   房间界面要改的都是"和已有规则抢同一条属性"的东西（.room-seat 的边框/背景、
 *   .room-actions 的排布），只有写在最后才说得上话。改中间那段等于和后面几十条
 *   补丁赌博。**新样式一律追加到文件末尾**，这条规矩在 §6010 已经写过一次了。
 *
 * ⚠️⚠️ **本段（"重做版"：按屏幕槽位摆 2×2 网格 + 每卡竖排四行）已经被文件**最末尾**
 *   的「第二批：A队/B队 分组 + 座位卡压成一行」整体覆盖** —— 现在生效的是文末那一段：
 *   座位按**队伍**分两列（A队 = 座位 0&2、B队 = 1&3），每张卡一行。
 *   本段留作历史记录（里面关于"槽位 / 方位口径"的说明仍然成立，只是不再决定排布）。
 *
 * ⚠️ 布局是给**手机横屏**做的（项目有竖屏强转横屏的逻辑，见 fullscreen.js）：
 *   6 个实测尺寸 568×320 / 640×360 / 736×414 / 844×390 / 874×402 / 932×430
 *   全部不许溢出。所以：
 *     · 纵向一切用 vh + clamp，屏越矮字越小，绝不写死 px 高度；
 *     · .room-box 保持可滚动（最矮的 568×320 是唯一会触发滚动的尺寸，见量测报告）；
 *     · 座位格子用**内容自适应**（不设 aspect-ratio），矮屏时卡片自己压扁。
 *
 * ⚠️ 方位口径和牌桌的 seatSlot() **完全一致**（这是本片最容易写错的地方）：
 *     槽位 0 = 屏幕下方 = **我**
 *     槽位 1 = 屏幕右边 = 我的**下家**（敌队）
 *     槽位 2 = 屏幕上方 = **对家**（我队）
 *     槽位 3 = 屏幕左边 = 我的**上家**（敌队）
 *   所以"我"和"对家"永远在**对角**上（掼蛋的固定座位关系），
 *   而 1/3 两格是另外两家。格子按**槽位**（不是绝对座位号）落在网格里：
 *     第 1 行 = 左列 槽位2(对家) / 右列 槽位1(下家)
 *     第 2 行 = 左列 槽位3(上家) / 右列 槽位0(我)
 *   同队用**同一种颜色**（金 = 我方，蓝 = 对方），一眼能看出是哪两个人一队。
 *
 * ⚠️ 开局之后（.room-seat.locked）座位不可点：client.js 连监听都不挂，
 *   这里再把鼠标指针收回来，免得"看起来能点、点了没反应"。
 * ══════════════════════════════════════════════════════════════════════════ */

/* ── 一、房内那一块的骨架：顶部房号 / 中部座位 / 底部按钮 ── */
.room-in {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(3px, 0.85vh, 9px);
  width: 100%;
  min-height: 0;
  /* 撑满 .room-box 的剩余高度 → 后面的 .room-actions 用 margin-top:auto 钉到底部 */
  flex: 1 1 auto;
  justify-content: flex-start;
}
.room-in.hidden {
  display: none;
}
/*
 * 大厅那一块（还没进房）保持竖向紧凑，别被上面那条 flex 拉伸：
 * 创建房间 / 输房号加入 / 一句提示，三样居中堆在中间最顺眼。
 */
.room-lobby {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(6px, 1.6vh, 16px);
  width: 100%;
}
.room-lobby.hidden {
  display: none;
}
/* .room-box 允许被撑满：房内那块才有"顶部/中部/底部"三段式 */
.room-box {
  align-items: stretch;
}
/* 标题行不参与拉伸，高度按内容 */
.room-head {
  flex: 0 0 auto;
}

/* 房号行：房号大字 + 「复制邀请链接」并排；屏太窄时允许换行 */
.room-code-row {
  gap: clamp(6px, 1.4vw, 14px);
  flex-wrap: wrap;
  justify-content: center;
}
/*
 * 房号：**这一屏最大的字**（产品口径：它是"念给朋友"的核心元素，必须最醒目）。
 *
 * ⚠️ 这里踩过一次坑，写下来免得再犯：
 *   本补丁区**前面**（§6228）已经有 `.room-code-text { font-size: clamp(26px, 8.4vh, 54px) }`
 *   —— 那条本来是对的（874×402 下 8.4vh ≈ 33.8px）。但我这份"重做版"又写了一条
 *   `clamp(16px, 4.6vh, 30px)` 把它**盖掉了**，线上量出来房号只有 18.5px
 *   （18.492 ÷ 402 = 4.6vh，正好等于我写的那条）。补丁区"后来居上"在这里帮了倒忙。
 *   ⇒ 现在只保留**一条**口径：`clamp(26px, 8.4vh, 54px)`，和前面那条一致。
 *     实测：568×320 → 26.9px ｜ 844×390 → 32.8px ｜ 874×402 → 33.8px ｜ 932×430 → 36.1px。
 */
.room-code-text {
  font-size: clamp(26px, 8.4vh, 54px);
  font-weight: 900;
  letter-spacing: 0.08em; /* 字距按产品口径拉开一点（等宽数字 + 拉开 = 好念） */
  color: #ffe9a8; /* 兜底纯金：万一 background-clip:text 不被支持也不是"透明"的 */
}
/* 「房号」那个小标签：≥12px（原来 10px 太小） */
.room-code-cap {
  font-size: clamp(12px, 2.4vh, 16px);
}
/* 「复制邀请链接」按钮文字：≥13px */
.room-code-row .btn {
  font-size: clamp(13px, 2.7vh, 18px);
}
/* ⚠️ 邀请链接不在界面常驻显示，只通过按钮复制。 */
/* 状态摘要那一行（"1/4 人 · 你是房主 · …"）：≥11px（原来 9px） */
.room-head .room-hint {
  font-size: clamp(11px, 2.2vh, 14px);
  /*
   * 允许换行（原来写死 nowrap）：摘要现在多了"我的对家：XXX"和"点座位可换位"两句，
   * nowrap 会把它整段推到屏幕外、被 .room-layer 的 overflow:hidden 裁掉 ——
   * 等于玩家根本看不到"我的对家是谁"，那正是本片要传达的信息。
   */
  white-space: normal;
  line-height: 1.25;
  text-align: right;
  max-width: 62%;
}

/* ── 二、4 个座位：按**真实方位**落在 2×2 网格里 ── */
.room-seats {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  grid-auto-rows: min-content;
  gap: clamp(3px, 0.8vh, 9px) clamp(6px, 1.2vw, 14px);
  width: min(660px, 96vw);
  max-width: 100%;
  margin: 0 auto;
}
/* 槽位 → 格子（口径见文件头那段；行列都是"按槽位"排的，不是按绝对座位号） */
.room-seat.slot-top {
  grid-area: 1 / 1 / 2 / 2; /* 对家 */
}
.room-seat.slot-right {
  grid-area: 1 / 2 / 2 / 3; /* 下家 */
}
.room-seat.slot-left {
  grid-area: 2 / 1 / 3 / 2; /* 上家 */
}
.room-seat.slot-bottom {
  grid-area: 2 / 2 / 3 / 3; /* 我 */
}

/* 卡片本身：横向排（左=座位号+昵称标签 ｜ 右下=换位提示），比竖排矮一半 */
.room-seat {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: clamp(1px, 0.4vh, 4px);
  min-width: 0;
  max-width: none;
  padding: 3px clamp(6px, 1vw, 12px);
  border: 2px solid rgba(255, 255, 255, 0.16);
  border-radius: clamp(8px, 1.4vh, 14px);
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.10), rgba(255, 255, 255, 0.03));
}

/*
 * 两队两色（**本片的核心视觉**）：同队 = 同色。
 *   · 金（我方）：我 + 对家 —— 用的就是主按钮那套橙金（#ffd98a/#ffab2e 家族）；
 *   · 蓝（对方）：下家 + 上家。
 * 底色只给一点点（0.16/0.10），免得把昵称和白标签的对比度压下去。
 */
.room-seat.team-mine {
  border-color: rgba(240, 200, 92, 0.66);
  background: linear-gradient(180deg, rgba(240, 200, 92, 0.16), rgba(240, 200, 92, 0.05));
}
.room-seat.team-foe {
  border-color: rgba(120, 180, 255, 0.42);
  background: linear-gradient(180deg, rgba(90, 150, 235, 0.13), rgba(90, 150, 235, 0.04));
}
/* 我那一格：更亮的金边 + 外发光（满桌 4 张卡里第一眼要找到自己） */
.room-seat.self {
  border-color: #ffd98a;
  box-shadow: inset 0 0 0 2px rgba(255, 217, 138, 0.22), 0 6px 18px rgba(0, 0, 0, 0.34);
}
/* 对家那一格：加粗描边 + 左上角一个金三角角标（不看文字也知道那是队友） */
.room-seat.partner {
  position: relative;
  border-color: rgba(255, 217, 138, 0.95);
}
.room-seat.partner::after {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  border-top: 12px solid rgba(255, 217, 138, 0.95);
  border-right: 12px solid transparent;
  border-top-left-radius: 8px;
  pointer-events: none;
}
/* 空位：虚线 + 压暗（和"有人"一眼可分；保持 dashed 是原有口径） */
.room-seat.empty {
  border-style: dashed;
  opacity: 0.62;
}
/* 可点换位：手掌指针 + 按下反馈（手机上手指按下去要看得见） */
.room-seat.can-pick {
  cursor: pointer;
  transition: transform 0.1s ease, box-shadow 0.1s ease;
}
.room-seat.can-pick:active {
  transform: scale(0.985);
  box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.35) inset;
}
/* 开局后锁死：指针收回来，明确它不再是个按钮 */
.room-seat.locked {
  cursor: default;
}

/* 座位号 + 相对方位（「座位2 对家」）：方位那一小截用主题色区分 */
.room-seat .rs-pos {
  display: flex;
  align-items: baseline;
  gap: 5px;
  font-size: clamp(11px, 2.2vh, 14px); /* 原来 8px：手机上基本看不清 */
  letter-spacing: 0.5px;
}
.room-seat .rs-pos .rs-where {
  padding: 0 5px;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.3);
  font-size: clamp(11px, 2.2vh, 14px); /* 「我 / 对家 / 下家 / 上家」≥11px */
  font-weight: 800;
  color: rgba(255, 255, 255, 0.92);
}
.room-seat.team-mine .rs-pos .rs-where {
  background: rgba(150, 105, 10, 0.55);
  color: #ffeec2;
}
.room-seat.team-foe .rs-pos .rs-where {
  background: rgba(30, 70, 130, 0.6);
  color: #d8e9ff;
}
/* 昵称：一行的宽度有限，长名字截断（原名不改，只是显示） */
.room-seat .rs-name {
  max-width: 100%;
  font-size: clamp(13px, 2.8vh, 19px); /* ≥13px（原来 11px） */
  font-weight: 800;
}
.room-seat .rs-tags {
  gap: 3px;
  justify-content: flex-start;
}
/*
 * 座位状态标签（我 / 对家 / 房主 / 真人 / 电脑 / 已准备 / 未准备 / 等人）：≥11px。
 * ⚠️ 基础规则在 §6312 是 `clamp(8px, 1.6vh, 11px)`（8px），这里按产品口径抬到 11px。
 *    选择器带上 `.room-seat` 前缀，确保只在房间界面生效、碰不到牌桌。
 */
.room-seat .rs-tag {
  font-size: clamp(11px, 2.3vh, 14px);
  padding: 0 7px; /* 纵向 0：标签靠 line-height 撑高，省下的都是矮屏最缺的纵向空间 */
}
/* 换位提示：次要信息，但**也不能小于 11px**；矮屏才整条藏掉（见文末媒体查询） */
.room-seat .rs-pick {
  font-size: clamp(11px, 2.2vh, 14px);
  font-weight: 700;
  color: rgba(255, 255, 255, 0.62);
}
/* 「对家」这个标签：金底深字，和「我」同一个家族 */
.rs-tag.partner {
  border-color: rgba(255, 217, 138, 0.85);
  background: rgba(160, 115, 15, 0.72);
  color: #fff3d4;
}

/* ── 三、底部动作按钮：钉在这一屏的最下面（margin-top:auto 把空隙吃掉） ── */
.room-actions {
  margin-top: auto;
  padding-top: clamp(1px, 0.5vh, 6px);
}
/* 底部主按钮（准备 / 开始游戏 / 离开房间）：文字也按"按钮 ≥13px"的口径抬上来 */
.room-actions .btn {
  font-size: clamp(13px, 2.7vh, 18px);
}

/*
 * ── 五、矮屏（高度 ≤ 370px）怎么省高度 ──
 *
 * 背景：产品给的 6 个尺寸里，568×320 与 640×360 是最紧的两档（320 那档改前只剩 14px 余量）。
 * 把字号抬到"≥11px / 房号 ≥26px"之后，必须把这两档重新排一次，否则
 * 「开始游戏」「离开房间」会被挤出屏幕（真机上就是**点不到开始**，硬故障）。
 *
 * ⚠️ 省高度的手段**按优先级**如下，一条都不许越界：
 *   ① 藏**次要文字**（换位提示「点这里坐过去」—— 座位卡本身还是可点的，功能不丢）；
 *   ② 缩**间距与内边距**（gap / padding / 行距）；
 *   ③ **绝不缩字号**（≥11px 是产品下限）、**绝不缩按钮的可点高度**（≥28px）。
 *   之前有人在这一档写过 `font-size:10px !important` 和 `min-height:22px !important`，
 *   两条都与产品口径直接冲突，已经删掉 —— 别再加回来。
 */
@media (max-height: 370px) {
  /* ① 次要文字：换位提示整条收掉（座位卡仍然可点，`rs-pick` 只是说明文字） */
  .room-seat .rs-pick {
    display: none;
  }
  /* ② 间距 / 内边距收紧 */
  .room-in {
    gap: 3px;
  }
  .room-seats {
    gap: 3px 8px;
  }
  .room-seat {
    padding: 3px 7px;
    gap: 1px;
    border-radius: 8px;
  }
  .room-seat .rs-tags {
    gap: 2px;
  }
  .room-head {
    gap: 6px;
  }
  .room-actions {
    padding-top: 0;
  }
  /*
   * ③ 座位卡改成**横向流式**：座位号 / 昵称 / 标签 / 按钮尽量挤在同一行。
   *    竖排四行在 320 高的屏幕上要吃掉 ~75px×2 行，横排只要 ~34px×2 行 ——
   *    这是矮屏最大的一笔节省，而且**一个字都不用缩小**。
   */
  .room-seat {
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    column-gap: 6px;
  }
  .room-seat .rs-name {
    min-width: 0;
    max-width: 46%;
  }
  .room-seat .rs-tags {
    flex: 1 1 auto;
    justify-content: flex-start;
  }
  /* 房号仍是这一屏最大的字，矮屏按比例收一档（568×320 → 7.5vh = 24px ≥ 20px 口径） */
  .room-code-text {
    font-size: clamp(20px, 7.5vh, 34px);
    letter-spacing: 0.06em;
  }
}
/*
 * ── 四、「加电脑 / 移除」的样式**整块删除** ──
 *
 * 原来这里有两颗房主专用的小按钮（空位「+ 加电脑」、人机那格「移除」）：
 *   .room-seat .rs-ops / .room-seat .rs-op / .room-seat .rs-op.danger
 *   （含 min-height:28px 那条"可点高度下限"）
 * 产品确认：**空位在开局时本来就会自动补人机**，不需要手动加，所以
 * 「元素 + 文案 + 这份样式」一起删干净。服务端 roomAddBot / roomRemoveBot
 * 两条分支原样保留（无害、也便于以后），只是前端不再发。
 *
 * ⚠️ 删掉的只有"已经不可能匹配到任何元素"的选择器，所以不可能引起布局回归；
 *    但按本项目纪律，改完样式**仍然实测了 6 档尺寸 + 字号**（见交付说明）。
 */

/* ══════════════════════════════════════════════════════════════════════════
 * 房间界面 · 第二批：**A队 / B队 分组** + 座位卡压成一行
 *
 * 为什么又追加到**文件最末尾**：这份样式表是"后面覆盖前面"（1–2070 失效 ｜
 * 2071–4168 生效基础 ｜ 4169+ 补丁区，越靠后越生效，见 §6010 / §6400 两处说明）。
 * 本批要覆盖的正是**紧挨着上面的那份"重做版"**里的 .room-seat（竖排四行）与
 * .room-seats（按屏幕槽位摆 2×2）—— 只有写在最后才说得上话。
 *
 * 布局（产品口径，和服务端 roomstore.teamOf 同一套口径）：
 *   ┌─ A队（你在这队）────┬─ B队 ────────────┐
 *   │ 我的对家：小明      │                   │
 *   │ [1号 我   我·房主]  │ [2号 玩家B  真人] │
 *   │ [3号 空·等人  空]   │ [4号 玩家D  电脑] │
 *   └────────────────────┴───────────────────┘
 *          点某一格就能换位：空位坐过去 · 真人交换 · 电脑让位
 *
 *   · **A队 = 座位 0 与 2**（左列，升序），**B队 = 座位 1 与 3**（右列）；
 *   · 每队上面一个**队标题**；我所在的队加金边高亮 + 「你在这队」；
 *   · 每张卡**只有一行**：座位号 + 昵称 + **一个**状态徽标
 *     （「点这里坐过去」这类提示收成整块下面那一条共用提示行）；
 *   · 我这一队的标题下多一行「我的对家：XXX」（对家位空着就说"空位（等人）"）。
 *
 * ⚠️ 字号一条都不许缩（产品下限）：昵称 ≥13px、状态/角色标签 ≥11px、队标题 ≥12px、
 *    按钮文字 ≥13px、按钮可点高 ≥28px、房号 **不动**（clamp(26px,8.4vh,54px)，
 *    874×402 实测 33.77px）。座位卡同时也是一整块点击靶子，所以给它 min-height:30px
 *    （矮屏 28px）——压的是**卡片高度**，不是字。
 * ══════════════════════════════════════════════════════════════════════════ */

/* ── 一、座位区：两列 = 两队；底部那条提示行横跨两列 ── */
.room-seats {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  align-items: start;
  gap: clamp(4px, 1vh, 10px) clamp(6px, 1.6vw, 18px);
  width: min(660px, 96vw);
  max-width: 100%;
  margin: 0 auto;
}
.room-team {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  padding: 2px clamp(4px, 0.8vw, 8px) 3px;
  border: 1px solid rgba(255, 255, 255, 0.10);
  border-radius: clamp(8px, 1.4vh, 12px);
  background: rgba(0, 0, 0, 0.14);
}
.room-team.team-a {
  grid-column: 1 / 2; /* A队 = 座位 0 & 2（左列） */
}
.room-team.team-b {
  grid-column: 2 / 3; /* B队 = 座位 1 & 3（右列） */
}
/*
 * 我所在的队：金边 + 金色底 + 内描边。
 * 「一眼看出来」靠三件一起：这一圈金边、标题变金、标题旁那枚「你在这队」。
 */
.room-team.mine {
  border-color: rgba(255, 217, 138, 0.9);
  background: linear-gradient(180deg, rgba(240, 200, 92, 0.16), rgba(240, 200, 92, 0.04));
  box-shadow: inset 0 0 0 1px rgba(255, 217, 138, 0.18), 0 4px 14px rgba(0, 0, 0, 0.26);
}

/* ── 二、队标题（A队 / B队）+「你在这队」+ 我这队的「我的对家：X」 ── */
.room-team-head {
  display: flex;
  align-items: baseline;
  gap: 6px;
  min-width: 0;
  font-size: clamp(12px, 2.4vh, 15px); /* ≥12px：比卡里的字段醒目，但不抢房号的风头 */
  font-weight: 900;
  letter-spacing: 0.04em;
  color: rgba(255, 255, 255, 0.92);
  white-space: nowrap;
}
.room-team.mine .room-team-head {
  color: #ffe9a8;
}
.room-team-tag {
  padding: 0 6px;
  border: 1px solid rgba(255, 217, 138, 0.65);
  border-radius: 999px;
  background: rgba(150, 105, 10, 0.62);
  color: #fff3d4;
  font-size: clamp(11px, 2.2vh, 13px); /* ≥11px */
  font-weight: 800;
}
.room-team-note {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: rgba(255, 233, 168, 0.92);
  font-size: clamp(11px, 2.2vh, 13px); /* ≥11px */
  font-weight: 700;
  line-height: 1.2;
}

/* ── 三、队内的两格：竖着排（一队上下各一格） ── */
.room-team-seats {
  display: flex;
  flex-direction: column;
  gap: clamp(2px, 0.5vh, 5px);
  min-width: 0;
}

/* ── 四、座位卡：**一行**装完「座位号 + 昵称 + 一个状态徽标」 ── */
.room-seat {
  display: flex;
  flex-direction: row; /* 覆盖上面那条 column：原来竖排四行，现在一行 */
  flex-wrap: nowrap;
  align-items: center;
  gap: 6px;
  min-width: 0;
  padding: 3px clamp(5px, 0.9vw, 10px);
  border-width: 2px;
  border-radius: clamp(7px, 1.2vh, 11px);
  /*
   * 整张卡就是"点座位换位"的靶子（不是文字行），所以给它一个可点高度下限。
   * ⚠️ 这一条压的是**卡片高度**：字号一个字都没缩（见下面各条）。
   */
  min-height: 30px;
}
/* ① 座位号：小圆片（绝对座位号，服务端口径 —— 报位置时说"几号"） */
.room-seat .rs-pos {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  padding: 0 6px;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.28);
  color: rgba(255, 255, 255, 0.88);
  font-size: clamp(11px, 2.2vh, 13px); /* ≥11px */
  font-weight: 800;
  letter-spacing: 0;
  white-space: nowrap;
}
/* ② 昵称：占满中间那一段，太长就省略号（不改玩家原名，只是显示） */
.room-seat .rs-name {
  flex: 1 1 auto;
  min-width: 0;
  max-width: none;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: clamp(13px, 2.6vh, 17px); /* ≥13px（产品下限 11px，这里更宽裕） */
  font-weight: 800;
}
/* ③ 状态徽标：一卡就一个，钉在右边 */
.room-seat .rs-tags {
  flex: 0 0 auto;
  display: inline-flex;
  flex-wrap: nowrap;
  align-items: center;
  gap: 3px;
  margin-left: auto;
  justify-content: flex-end;
}
.room-seat .rs-tag {
  display: inline-flex;
  align-items: center;
  padding: 0 8px;
  font-size: clamp(11px, 2.2vh, 13px); /* ≥11px（基础规则里那条 8px 被这里盖掉） */
  white-space: nowrap;
}
/*
 * 准备状态小圆点：只给"需要准备的人"（真人且非房主）那一格画 —— 由 client.js 的
 * roomSeatCard 打上 .is-ready / .is-idle（房主不需要准备，所以房主那格不画，见那里的注释）。
 * 用它而不是再加一行字，是为了不把卡片变高：绿 = 已准备、橙 = 未准备。
 */
.room-seat.is-ready .rs-tag::before,
.room-seat.is-idle .rs-tag::before {
  content: '';
  display: inline-block;
  width: 6px;
  height: 6px;
  margin-right: 5px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.35);
}
.room-seat.is-ready .rs-tag::before {
  background: #7fe093;
  box-shadow: 0 0 4px rgba(127, 224, 147, 0.85);
}
.room-seat.is-idle .rs-tag::before {
  background: #f0a83c;
}

/* ── 五、换位提示：整块下面**一条**（原来每张卡各写一句「点这里坐过去」） ── */
.room-seat-hint {
  grid-column: 1 / -1;
  text-align: center;
  color: rgba(255, 255, 255, 0.68);
  font-size: clamp(11px, 2.2vh, 13px); /* ≥11px */
  line-height: 1.2;
}
.room-seat-hint:empty {
  display: none; /* 大厅状态（还没进房）不留空行 */
}

/*
 * ── 六、矮屏档（max-height:370px）再收一档 ──
 *
 * ⚠️ 本批的规则写在文件最后，**会盖掉上面那条矮屏媒体查询**（同权重、后来居上），
 *    所以这里必须把矮屏要的"更紧"再声明一遍；口径和那条一样：
 *    **只缩间距/内边距，绝不缩字号、绝不把可点高度压到 28px 以下**。
 *    实测 568×320（这一档最紧）底部仍有充裕余量，见交付说明里的量测输出。
 */
@media (max-height: 370px) {
  .room-seats {
    gap: 3px 8px;
  }
  .room-team {
    padding: 1px 4px 2px;
  }
  .room-team-head {
    font-size: 12px;
  }
  .room-team-note {
    font-size: 11px;
  }
  .room-team-seats {
    gap: 2px;
  }
  .room-seat {
    min-height: 28px; /* 可点高度下限，不再往下压 */
    padding: 2px 5px;
    gap: 5px;
  }
  .room-seat .rs-pos {
    padding: 0 5px;
    font-size: 11px;
  }
  .room-seat .rs-name {
    font-size: 13px;
  }
  .room-seat .rs-tag {
    padding: 0 6px;
    font-size: 11px;
  }
  .room-seat-hint {
    font-size: 11px;
  }
}

/* ══════════════════════════════════════════════════════════════════════════
 * 房间界面 · 第三批：**换位要对方同意**（弹层 + 等待条）
 *
 * 产品口径（2026-10-08）：点到**有人的座位**不再立刻交换，而是问对方同不同意。
 * 两份 UI，分别给两边看：
 *   · 被请求者 → `#roomSwap`：房间层**内部**的同意 / 拒绝弹层
 *     （⚠️ 不用浏览器原生 confirm()，产品明确否掉；也不用 position:fixed，
 *      强制横屏 rotate(90deg) 在部分安卓上会让 fixed 退回视口定位）；
 *   · 请求者   → `#roomSwapWait`：座位区下面一条常驻的「等待对方同意…」。
 *
 * 为什么又追加到**文件最末尾**：这份样式表是"后面覆盖前面"（见 §6010 / §6400 /
 * §6765 三处说明），新规则一律写在最后才说得上话。
 *
 * 字号口径（和房间界面其它部分一致，一条都不许缩）：
 *   弹层标题 ≥13px ｜ 对方昵称 ≥15px ｜ 说明 / 等待条 ≥11px ｜ 按钮文字 ≥13px、
 *   可点高 ≥28px。弹层本身**不占房间内容的高度**（absolute 浮在上面），
 *   所以 568×320 那一档也不会因为多出这一层而把按钮挤出屏幕。
 * ══════════════════════════════════════════════════════════════════════════ */

/* ── 一、请求者：常驻的「等待对方同意…」（对方回话时由 client.js 收起） ── */
.room-swap-wait {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  max-width: min(660px, 96vw);
  padding: clamp(2px, 0.6vh, 6px) clamp(8px, 1.6vw, 16px);
  border: 1px solid rgba(255, 217, 138, 0.55);
  border-radius: 999px;
  background: rgba(150, 105, 10, 0.42);
  color: #fff3d4;
  font-size: clamp(11px, 2.2vh, 14px); /* ≥11px */
  font-weight: 800;
  line-height: 1.25;
  text-align: center;
}
.room-swap-wait.hidden {
  display: none !important;
}
/* 等待中的呼吸点：一眼看出"这件事还没结束"，不是一条静止的死文字 */
.room-swap-wait::before {
  content: '';
  flex: 0 0 auto;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: #ffd98a;
  animation: room-swap-pulse 1s ease-in-out infinite;
}
@keyframes room-swap-pulse {
  0%, 100% { opacity: 0.35; }
  50% { opacity: 1; }
}

/* ── 二、被请求者：同意 / 拒绝弹层（盖在房间层内部，absolute 不是 fixed） ── */
.room-swap {
  position: absolute;
  inset: 0;
  z-index: 6;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: clamp(6px, 1.6vh, 18px) clamp(10px, 3vw, 32px);
  background: rgba(0, 0, 0, 0.58);
  -webkit-tap-highlight-color: transparent;
}
.room-swap.hidden {
  display: none !important;
}
.room-swap-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(4px, 1vh, 10px);
  width: min(420px, 92%);
  padding: clamp(8px, 2vh, 20px) clamp(10px, 2.4vw, 26px);
  border: 2px solid rgba(255, 217, 138, 0.85);
  border-radius: clamp(10px, 2vh, 18px);
  background: linear-gradient(180deg, rgba(28, 62, 44, 0.98), rgba(12, 32, 24, 0.98));
  box-shadow: 0 14px 36px rgba(0, 0, 0, 0.5), inset 0 0 0 1px rgba(255, 255, 255, 0.08);
}
.room-swap-title {
  font-size: clamp(13px, 2.8vh, 19px); /* ≥13px */
  font-weight: 900;
  letter-spacing: 0.08em;
  color: #ffe9a8;
}
.room-swap-who {
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: 6px;
  max-width: 100%;
  font-size: clamp(11px, 2.2vh, 14px); /* ≥11px */
  color: rgba(255, 255, 255, 0.86);
  font-weight: 700;
  text-align: center;
}
.room-swap-from {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 58%;
  color: #fff;
  font-size: clamp(15px, 3.2vh, 22px); /* 对方昵称：弹层里最醒目的那一行（≥15px） */
  font-weight: 900;
}
.room-swap-desc {
  font-size: clamp(11px, 2.2vh, 14px); /* ≥11px */
  color: rgba(255, 255, 255, 0.66);
  line-height: 1.3;
  text-align: center;
}
.room-swap-btns {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(8px, 2vw, 18px);
  margin-top: clamp(2px, 0.6vh, 6px);
}
.room-swap-btns .btn {
  min-height: 28px; /* 可点高度下限（产品口径） */
  padding: clamp(5px, 1.2vh, 11px) clamp(14px, 3vw, 28px);
  border-radius: 999px;
  font-size: clamp(13px, 2.6vh, 18px); /* ≥13px */
  font-weight: 800;
}
/* 矮屏（≤370px）：只收间距 / 内边距，字号与可点高度一个都不动 */
@media (max-height: 370px) {
  .room-swap-card {
    gap: 3px;
    padding: 7px 12px;
  }
  .room-swap-btns {
    gap: 8px;
    margin-top: 0;
  }
  .room-swap-btns .btn {
    padding: 4px 14px;
  }
}

/* ══════════════════════════════════════════════════════════════════════════
 * 房间界面 · 第四批：**座位卡显示头像**（产品口径：不只昵称）
 *
 * 结构（client.js 的 roomAvatarEl + roomSeatCard）：
 *   <span class="rs-avatar">首字 <img class="rs-avatar-img" src="/avatars/x.png"></span>
 *
 * 三条口径：
 *   ① 头像是**圆角方形**（不是正圆）：和牌桌上那套头像的观感一致，
 *      而且方形/圆角方形在小尺寸下比正圆更容易看清素材；
 *   ② **兜底文字垫在底下**（`.rs-avatar` 直接写了首字），img 用 absolute 盖上去，
 *      加载失败时 client.js 把 img remove 掉 → 露出首字，**永不裂图**；
 *   ③ 空位 / 人机没有图片：空位显示「空」+ 虚框（沿用 .room-seat.empty 的压暗），
 *      人机显示「电」（它到底有没有素材由服务端那份头像解析器决定，见 roomAvatarFor）。
 *
 * ⚠️ 尺寸这里只给一个**能用的中间值**：这一批要的是"头像真的显示出来"，
 *    第三片（整体重做）会把座位卡改成**正方形**、头像占主要面积，那批的规则
 *    写在更后面、会盖掉这里的尺寸 —— 这符合本文件"后面覆盖前面"的既有纪律。
 * ══════════════════════════════════════════════════════════════════════════ */

.room-seat .rs-avatar {
  position: relative; /* img 的定位基准（absolute; inset:0 盖在首字上） */
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: clamp(28px, 8.4vh, 44px);
  height: clamp(28px, 8.4vh, 44px);
  overflow: hidden;
  border: 1px solid rgba(255, 255, 255, 0.28);
  border-radius: clamp(6px, 1.2vh, 11px);
  background: linear-gradient(180deg, rgba(0, 0, 0, 0.34), rgba(0, 0, 0, 0.18));
  color: rgba(255, 255, 255, 0.9);
  /* 兜底首字：**不能小于 11px**（产品下限），而且要在方块里居中 */
  font-size: clamp(11px, 2.4vh, 15px);
  font-weight: 900;
  line-height: 1;
  user-select: none;
}
/* 图片盖住兜底文字；object-fit:cover 保证任意比例的素材都不变形、不留白边 */
.room-seat .rs-avatar-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* 空位：头像框也虚化，和整张卡的 dashed 边框是同一套语言 */
.room-seat.empty .rs-avatar {
  border-style: dashed;
  background: rgba(0, 0, 0, 0.22);
  color: rgba(255, 255, 255, 0.55);
}
/* 我这一格的头像：金边（和卡片的高亮呼应，一眼找到自己） */
.room-seat.self .rs-avatar {
  border-color: rgba(255, 217, 138, 0.95);
  box-shadow: 0 0 0 1px rgba(255, 217, 138, 0.25);
}
/* 矮屏（≤370px）：方块和兜底字号都**只收到下限为止** */
@media (max-height: 370px) {
  .room-seat .rs-avatar {
    width: 28px;
    height: 28px;
    border-radius: 7px;
  }
  .room-seat .rs-avatar {
    font-size: 11px;
  }
}


/* ══════════════════════════════════════════════════════════════════════════
 * 房间界面 · 第五批：**整体重做**（信息条 + 两个队伍面板 + **正方形**座位卡）
 *
 * 产品口径（2026-10-08）：
 *   · 顶部一条**房间信息条**：房号（一屏最大的字）+ 复制邀请链接 + 状态摘要；
 *   · 中部**两个队伍面板并排**：A队 = 座位 0&2（左）、B队 = 座位 1&3（右），
 *     每个面板有标题栏（我所在的队加金边 + 「你在这队」），面板里两个**正方形**座位卡；
 *   · 座位卡**宽 = 高**（`aspect-ratio: 1/1`）；**头像是主体**、昵称在下方、
 *     座位号压在左上角、状态徽标压在右下角；
 *   · 底部一行主按钮：准备 / 开始游戏 / 离开房间；
 *   · **层次分明**：面板有描边 + 底色，卡片之间有间距，我方整列金边高亮。
 *
 * ⚠️ 为什么又追加到**文件最末尾**：这份样式表是"后面覆盖前面"
 *    （1–2070 失效 ｜ 2071–4168 生效基础 ｜ 4169+ 补丁区，越靠后越生效，
 *      见 §6010 / §6400 / §6765 / §7050 四处说明）。
 *    本批要覆盖的是前面**所有** .room-seat / .room-seats / .room-in 的排布规则，
 *    只有写在最后才说得上话。选择器统一带 `#roomLayer` 前缀（提高优先级），
 *    这样连矮屏档里那几条 `.room-seat { padding… gap… }` 也一并压住，**不需要 !important**。
 *
 * ⚠️ 上一版（并行改动）在文件末尾塞过一段 `--rs-seat-size: min(74px,15vh)` +
 *    一堆 `!important` 的方卡补丁 —— 它能把卡片变方，但边长只有 60px（874×402）/ 48px（568×320），
 *    头像被 `img{max-width:60%}` 又压了一层，内容挤成一团。这一段**被本批整体取代**：
 *    边长改成由**可用高度**推出的 `--rs-side`（874×402 → ~100px ｜ 568×320 → ~77px），
 *    头像按边长等比放大到 40%（不缩字号、不缩可点高度）。
 *
 * 字号口径（产品下限，一条都不许缩）：
 *   房号 clamp(26px, 8.4vh, 54px)（矮屏档 20px/7.5vh 照旧）｜ 昵称 ≥13 ｜ 队标题 ≥12
 *   副行 / 座位号 / 徽标 / 摘要 ≥11 ｜ 按钮文字 ≥13 且**可点高 ≥28px**。
 *   正方形座位卡本身就是大靶子（≥52px），所以"可点高 ≥28px"自然满足。
 * ══════════════════════════════════════════════════════════════════════════ */

/* ── 一、顶部**房间信息条**：房号 + 复制邀请链接（第一行）、状态摘要（第二行） ── */
#roomLayer .room-info {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(1px, 0.4vh, 4px);
  width: min(720px, 96vw);
  max-width: 100%;
  padding: clamp(3px, 0.9vh, 8px) clamp(6px, 1.6vw, 14px);
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: clamp(8px, 1.6vh, 14px);
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.02));
}
#roomLayer .room-info-main {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(6px, 1.4vw, 14px);
  flex-wrap: wrap;
  width: 100%;
}
/*
 * 房号：**这一屏最大的字**（口径和前面几条一字不差）。
 * ⚠️ 这里必须把矮屏那一档的 clamp 也一起重声明 —— 本段在文件最后，
 *    否则会把 §6732 那条 `clamp(20px, 7.5vh, 34px)` 盖掉（房号在 568×320 上会变大到 26.9px，
 *    挤压下面的正方形）。两档数值就是原来那两个，一个字没改。
 */
#roomLayer .room-code-text {
  font-size: clamp(26px, 8.4vh, 54px);
  letter-spacing: 0.08em;
}
#roomLayer .room-code-cap {
  font-size: clamp(12px, 2.4vh, 16px);
}
#roomLayer .room-info-main .btn {
  min-height: 30px; /* 可点高度下限（产品口径 ≥28px，这里给 30） */
  padding: clamp(5px, 1.3vh, 11px) clamp(12px, 2.6vw, 22px);
  border-radius: 999px;
  font-size: clamp(13px, 2.7vh, 18px); /* ≥13px */
  font-weight: 800;
}
/* 状态摘要：≥11px；允许换行（宽屏一行、窄屏两行，绝不横向溢出） */
#roomLayer .room-info .room-hint {
  max-width: 100%;
  font-size: clamp(11px, 2.2vh, 14px);
  font-weight: 700;
  line-height: 1.25;
  color: rgba(255, 255, 255, 0.74);
  text-align: center;
  white-space: normal;
  word-break: break-word;
}

/* ── 二、座位区：两个队伍面板并排；`--rs-side` 是正方形座位卡的边长 ── */
#roomLayer .room-seats {
  /*
   * 边长：**由可用高度推**（每列两个正方形竖着排，所以高度是硬约束）。
   * 25vh 在六档上都留了余量（实测见交付说明）：
   *   568×320 → 80 ｜ 640×360 → 90 ｜ 736×414 → 103.5 ｜ 844×390 → 97.5 ｜ 874×402 → 100.5 ｜ 932×430 → 107.5
   * 宽度由 `min(100%, var(--rs-side))` 再兜一道（面板再窄也不会横向溢出）。
   */
  --rs-side: clamp(56px, 25vh, 124px);
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  align-items: stretch;
  gap: clamp(4px, 1vh, 10px) clamp(6px, 1.6vw, 18px);
  width: min(660px, 96vw);
  max-width: 100%;
  margin: 0 auto;
}
#roomLayer .room-team {
  display: flex;
  flex-direction: column;
  gap: clamp(1px, 0.35vh, 4px);
  min-width: 0;
  padding: clamp(3px, 0.9vh, 8px);
  border: 1px solid rgba(255, 255, 255, 0.13);
  border-radius: clamp(8px, 1.4vh, 14px);
  background: rgba(0, 0, 0, 0.16);
}
#roomLayer .room-team.team-a {
  grid-column: 1 / 2; /* A队 = 座位 0 & 2（左列，升序） */
}
#roomLayer .room-team.team-b {
  grid-column: 2 / 3; /* B队 = 座位 1 & 3（右列） */
}
/*
 * 我所在的队：金边 + 金色底 + 内描边 + 外发光（一眼看出"我和谁一队"）。
 * 和标题变金、标题旁那枚「你在这队」一起构成三重视觉线索。
 */
#roomLayer .room-team.mine {
  border-color: rgba(255, 217, 138, 0.9);
  background: linear-gradient(180deg, rgba(240, 200, 92, 0.16), rgba(240, 200, 92, 0.04));
  box-shadow: inset 0 0 0 1px rgba(255, 217, 138, 0.18), 0 4px 14px rgba(0, 0, 0, 0.28);
}
/* 队标题栏：下面一条细线把它和卡片分开（层次） */
#roomLayer .room-team-head {
  display: flex;
  align-items: baseline;
  gap: 6px;
  min-width: 0;
  padding-bottom: 1px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
  font-size: clamp(12px, 2.4vh, 15px); /* ≥12px */
  font-weight: 900;
  letter-spacing: 0.04em;
  color: rgba(255, 255, 255, 0.92);
  white-space: nowrap;
}
#roomLayer .room-team.mine .room-team-head {
  border-bottom-color: rgba(255, 217, 138, 0.45);
  color: #ffe9a8;
}
#roomLayer .room-team-tag {
  margin-left: auto;
  padding: 0 6px;
  border: 1px solid rgba(255, 217, 138, 0.65);
  border-radius: 999px;
  background: rgba(150, 105, 10, 0.62);
  color: #fff3d4;
  font-size: clamp(11px, 2.2vh, 13px); /* ≥11px */
  font-weight: 800;
  white-space: nowrap;
}
#roomLayer .room-team-note {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: rgba(255, 233, 168, 0.92);
  font-size: clamp(11px, 2.2vh, 13px); /* ≥11px */
  font-weight: 700;
  line-height: 1.2;
}
/* 面板里的两格：竖排（一队上下各一格）+ 水平居中 */
#roomLayer .room-team-seats {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(3px, 0.8vh, 8px);
  min-width: 0;
}

/* ── 三、**正方形座位卡**（产品口径：宽 = 高）──
 *
 * 卡内布局（三段式，头像是主体）：
 *   ┌──────────────┐
 *   │[1号]         │ ← 座位号小圆片（左上角，≥11px）
 *   │   (头像)      │ ← 主要元素：宽 = 边长的 40%
 *   │   昵称        │ ← 头像下方，≥13px，过长省略号
 *   │        [我·房主]│ ← 状态徽标（右下角，≥11px；真人另带准备状态圆点）
 *   └──────────────┘
 *
 * ⚠️ 高度必须**恰好等于宽度**，所以：
 *   · 明确写 `width: min(100%, var(--rs-side))` + `aspect-ratio: 1/1`；
 *   · 把矮屏档里那条 `min-height: 28px` 压成 0（它虽然比边长小、不会真的撑破，
 *     但留着会让"宽=高"这条判据在极矮屏上出现 1~2px 的偏差）；
 *   · 卡内一律用 `box-sizing: border-box` + `overflow: hidden`，
 *     内容再多也只会被裁掉，**不会把正方形撑变形**（内容已经按边长等比排好）。
 */
#roomLayer .room-seat {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  box-sizing: border-box;
  width: min(100%, var(--rs-side));
  height: auto;
  aspect-ratio: 1 / 1; /* ★ 正方形：浏览器按宽度定高度 */
  min-width: 0;
  min-height: 0;
  max-width: 100%;
  overflow: hidden;
  /* 上下留出角标的带子（座位号 / 徽标压在角上），左右只留 4px */
  padding: calc(var(--rs-side) * 0.16) 4px;
  border-width: 2px;
  border-radius: clamp(10px, 1.8vh, 16px);
}
/* 卡内间距压到最小：正方形的每一像素都要留给头像和昵称 */
#roomLayer .room-seat {
  gap: clamp(1px, 0.3vh, 3px);
}

/* 头像：**主体元素**，边长按方卡等比（40%）；再大就装不下昵称了 */
#roomLayer .room-seat .rs-avatar {
  width: calc(var(--rs-side) * 0.4);
  height: calc(var(--rs-side) * 0.4);
  min-width: 0;
  border-radius: clamp(8px, 1.6vh, 14px);
  font-size: clamp(11px, 2.4vh, 16px); /* 兜底首字 ≥11px */
}
#roomLayer .room-seat .rs-avatar-img {
  width: 100%;
  height: 100%;
  max-width: none;
  max-height: none;
  object-fit: cover;
}
/* 昵称：头像下方，一行，长了省略号（不改玩家原名，只是显示） */
#roomLayer .room-seat .rs-name {
  flex: 0 0 auto;
  max-width: 100%;
  font-size: clamp(13px, 2.6vh, 17px); /* ≥13px */
  font-weight: 800;
  line-height: 1.1;
  text-align: center;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* 座位号小圆片：压在**左上角**（报位置时说"几号"，所以要一直在） */
#roomLayer .room-seat .rs-pos {
  position: absolute;
  top: 3px;
  left: 4px;
  padding: 0 5px;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.36);
  color: rgba(255, 255, 255, 0.9);
  font-size: clamp(11px, 2.2vh, 13px); /* ≥11px */
  font-weight: 800;
  line-height: 1.25;
  white-space: nowrap;
}
/* 状态徽标：压在**右下角**（我 / 我·房主 / 房主 / 真人 / 电脑 / 空） */
#roomLayer .room-seat .rs-tags {
  position: absolute;
  right: 3px;
  bottom: 3px;
  left: auto;
  display: inline-flex;
  flex-wrap: nowrap;
  align-items: center;
  justify-content: flex-end;
  gap: 3px;
  margin-left: 0;
  max-width: calc(100% - 6px);
}
#roomLayer .room-seat .rs-tag {
  max-width: 100%;
  padding: 0 5px;
  overflow: hidden;
  text-overflow: ellipsis;
  font-size: clamp(11px, 2.2vh, 13px); /* ≥11px */
  line-height: 1.25;
  white-space: nowrap;
}
/* 准备状态小圆点（真人且非房主）：绿=已准备、橙=未准备（口径同前几批，一个字没改） */
#roomLayer .room-seat.is-ready .rs-tag::before,
#roomLayer .room-seat.is-idle .rs-tag::before {
  width: 6px;
  height: 6px;
  margin-right: 4px;
}

/* ── 四、底部主按钮：钉在这一屏最下面（margin-top:auto 吃掉多余空隙） ── */
#roomLayer .room-actions {
  margin-top: auto;
  padding-top: clamp(1px, 0.5vh, 6px);
}
#roomLayer .room-actions .btn {
  min-height: 32px; /* 可点高度下限（产品口径 ≥28px） */
  padding: clamp(6px, 1.5vh, 12px) clamp(12px, 2.6vw, 26px);
  border-radius: 999px;
  font-size: clamp(13px, 2.7vh, 18px); /* ≥13px */
  font-weight: 800;
}

/* ── 五、大厅（还没进房）：也做成一块面板，和房内的层次一致 ── */
#roomLayer .room-lobby {
  gap: clamp(6px, 1.6vh, 16px);
  width: min(520px, 96vw);
  max-width: 100%;
  padding: clamp(8px, 2.2vh, 20px) clamp(10px, 3vw, 24px);
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: clamp(10px, 2vh, 18px);
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.07), rgba(0, 0, 0, 0.12));
}
#roomLayer .room-tip {
  font-size: clamp(11px, 2.2vh, 14px); /* ≥11px（原来 9px） */
  line-height: 1.3;
}

/*
 * ── 六、矮屏档（max-height: 370px）──
 * ⚠️ 本段在文件最后，会把前面那条矮屏媒体查询里"同属性"的声明盖掉，
 *    所以**必须把矮屏要的口径在这里再声明一遍**：
 *    只收**间距 / 内边距 / 正方形边长**，**绝不缩字号、绝不把可点高度压到 28px 以下**。
 *    边长按 24vh（568×320 → 76.8px）+ 58px 下限：这一档最紧，实测仍有余量（见交付说明）。
 */
@media (max-height: 370px) {
  /*
   * 最紧的一档（568×320）：先把**所有能省的间距**省掉，再把正方形边长按 24vh 收一档。
   * 实测：只省间距不够（底部两个按钮会出屏 3px），所以这里同时把 `--rs-side`
   * 从 25vh 收到 24vh（320 → 76.8px）——**字号一个都没缩**。
   */
  #roomLayer .room-box {
    padding: 5px 10px;
  }
  #roomLayer .room-in {
    gap: 2px;
  }
  #roomLayer .room-info {
    padding: 2px 8px;
    gap: 0;
  }
  #roomLayer .room-code-text {
    font-size: clamp(20px, 7.5vh, 34px); /* 矮屏房号口径：和原来那条一模一样 */
    letter-spacing: 0.06em;
  }
  #roomLayer .room-seats {
    --rs-side: clamp(56px, 24vh, 108px);
    gap: 2px 8px;
  }
  #roomLayer .room-team {
    padding: 1px 4px 2px;
  }
  #roomLayer .room-team-head {
    font-size: 12px;
    padding-bottom: 0;
  }
  #roomLayer .room-team-note {
    font-size: 11px;
    line-height: 1.1;
  }
  #roomLayer .room-team-tag {
    font-size: 11px;
  }
  #roomLayer .room-team-seats {
    gap: 2px;
  }
  #roomLayer .room-seat {
    padding: calc(var(--rs-side) * 0.17) 4px;
    gap: 1px;
  }
  #roomLayer .room-seat .rs-name {
    font-size: 13px;
  }
  #roomLayer .room-seat .rs-pos,
  #roomLayer .room-seat .rs-tag {
    font-size: 11px;
  }
  #roomLayer .room-actions {
    padding-top: 0;
  }
  #roomLayer .room-actions .btn {
    min-height: 29px; /* 可点高度下限 28px，这里留 1px 余量 */
    padding: 3px 12px;
    font-size: 13px;
  }
}


/* ⚠️ 这里原来有一段**并行改动**加的「修正」补丁（`--rs-seat-size: min(62px,12.5vh) !important`
   + `@media (max-height:380px)` 把方卡再压到 35px）。它针对的是**更早那版** 48~60px 的小方卡
   （那版确实在 568×320 把底部按钮挤出了屏幕 3px）。但本批已经把边长改成由可用高度推出的
   `--rs-side`（568×320 → **76.8px**），并且**只靠收间距 + 一档 24vh** 就把那一档修好了：
   实测六档 **出屏按钮 全为 []、底部余量 7/16/8/3/5/10 全部 > 0、方形 true**（见交付说明）。
   所以那段 `!important` 补丁被删除 —— 留着它会把方卡压到 35px，反而与"头像为主体"冲突。
   ⚠️ 这段样式**只允许一条线改**：并行追加 `!important` 会让量测出来的数字解释不清。 */

/* ── 七、换位等待条改成**浮层**：绝不参与纵向布局 ──
 *
 * 为什么必须浮起来：它是 `#roomIn` 里的一个普通流内元素，显示时会**多占 ~22px**。
 * 而 568×320 那一档（方卡 76.8px）底部余量只有 **7px** ——
 * 一旦"等待对方同意…"出现，底部两个按钮就会被挤出屏幕（真正点不到开始/离开）。
 * 改成 absolute（相对 `#roomIn`）之后它浮在信息条下面，**一个像素都不参与布局**，
 * 于是"等待中"和"平时"的几何完全一致。样式仍然沿用第一批那套金色胶囊。
 */
#roomLayer .room-in {
  position: relative; /* 等待条的定位基准（absolute 相对它） */
}
#roomLayer .room-swap-wait {
  position: absolute;
  left: 50%;
  top: clamp(2px, 0.8vh, 8px);
  z-index: 5;
  transform: translateX(-50%);
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35);
}

/* ══════════════════════════════════════════════════════════════════════════
 * 房间界面 · 第八批：**水平居中**（产品原话：「创房间界面的元素没有居中太靠左了」）
 *
 * 为什么会偏左（两条，各自都要修）：
 *   ① `.room-box` 在更早的补丁里被改成 `align-items: stretch`（那是为了让 #roomIn
 *      撑满高度、底部按钮能被 margin-top:auto 钉住）。stretch 的交叉轴起点是**左**，
 *      于是大厅那块定宽的卡片（`.room-lobby`：width: min(520px, 96vw)）就贴在左边了；
 *   ② `.room-head`（‹ 返回 + 标题）是一条 `width:100%` 的 flex 行，默认 `flex-start`
 *      —— 整组靠左，看上去就是"没居中"。
 *
 * 修法（**只加对齐，不动任何尺寸/字号**）：
 *   · `.room-head` 改成 `justify-content: center`（返回 + 标题作为一组居中）；
 *   · 三块定宽容器（大厅卡片 / 顶部信息条 / 座位棋盘）都加 `margin-inline: auto`
 *     —— 这样不管父级是 stretch 还是 center，它们都**左右等距**；
 *   · 宽度公式一个字没改（`min(520px,96vw)` / `min(720px,96vw)` / `min(660px,96vw)`），
 *     所以六档的"无横向滚动 / 无层溢出 / 无出屏按钮"不会因为这一批受影响。
 *
 * ⚠️ 为什么不用 `align-items: center` 去改 `.room-box`：那会让 `#roomIn` 的宽度
 *    收缩到内容宽（它靠 width:100% 撑满），底部按钮的 `margin-top:auto` 与
 *    `overflow-y` 的关系都可能跟着变 —— 风险比"给三块加 auto 外边距"大得多。
 * ══════════════════════════════════════════════════════════════════════════ */
#roomLayer .room-head {
  justify-content: center;
}
#roomLayer .room-lobby,
#roomLayer .room-info,
#roomLayer .room-seats {
  margin-inline: auto;
}
/* 大厅里的三行内容也各自居中（输入框 + 按钮是一组，提示文字单行居中） */
#roomLayer .room-lobby {
  align-items: center;
  text-align: center;
}

/* ══════════════════════════════════════════════════════════════════════════
 * 牌桌 · 第九批：**「下一局」永远在最上层**（产品截图反馈）
 *
 * 现象：牌局结束时，出牌区的牌（截图里一张 8♦）压住了「下一局」按钮。
 *
 * ── 根因（层叠上下文陷阱，不是"少写了个 z-index"）──
 *   「下一局」= `#btnNext`，它在 `#actionCenter` 里，而 `#actionCenter` 在 `#actions` 里：
 *
 *     #actions          position:absolute; z-index:8     ← **自成层叠上下文**
 *       └ #actionCenter position:fixed;    z-index:40    ← 40 只在这个上下文里有效
 *           └ #btnNext
 *
 *   `#actionCenter` 上的 `z-index:40` 看起来很高，但它被**关在** `#actions` 的
 *   层叠上下文里 —— 对外真正参与比较的是 `#actions` 的 **8**。
 *   而出牌区那两层在**页面级**上下文里比它高：
 *     · `#myPlay`（我出的牌，屏幕中央）→ z-index **20** ✗ 压住了按钮
 *     · `#centerHint`（阶段提示）      → z-index **21**
 *   所以只给 `#btnNext` 自己加 z-index 是**没用的**（它连 #actionCenter 都出不去），
 *   必须抬高它们共同的祖先 `#actions`。
 *
 * ── 修法（只动层叠顺序，不动任何尺寸/位置/字号）──
 *   `#actions` 的 z-index：8 → **35**。
 *   · 35 > 出牌区所有层（`#myPlay` 20、`#centerHint` 21、座位出牌区 ≤5、名词牌 ≤9）
 *     → 「下一局」「整理」和四个出牌按钮都不会再被牌盖住；
 *   · 35 < 倒计时圆环（`body .turn-timer` 41，它是 `!important` 的定位浮层）
 *     → 圆环的层级关系**一个像素都没变**；
 *   · 35 < toast(50) < 名次横幅(700) < 弹层 → 这些照旧盖在上面。
 *
 * ⚠️ 为什么**不**去把出牌区调低：那些 z-index 是"牌一张压一张"的堆叠顺序
 *   （`.seat-cards .card:nth-child(n)` 递增 2/3/4/5）以及 `#myPlay` 相对手牌的
 *   层叠关系，动它们会连带改变"牌飞出去/压在一起"的观感（上一轮踩过）。
 *   抬按钮这一侧只影响"谁盖谁"，不影响任何几何。
 * ══════════════════════════════════════════════════════════════════════════ */
#actions {
  z-index: 35;
}

/* ══════════════════════════════════════════════════════════════════════════
 * 房间界面 · 第十批：**「未准备」角标** + **「提醒准备」胶囊**（产品口径 2026-10-08）
 *
 * ── 一、「未准备」角标（`.rs-flag`）──
 *   只给**真人、非房主、未准备**的那一格加（由 client.js 的 roomSeatCard 决定）。
 *   位置：**头像上沿居中**（`left:50%` + `translateX(-50%)`），压着头像的顶边。
 *   ⚠️ 两个关键约束：
 *     ① `position:absolute` —— **不占文档流** ✗（六档的方形边长/底部余量/居中一分不变）；
 *     ② **不与另外两个角标重叠**：座位号在左上角（`.rs-pos`）、状态徽标在右下角（`.rs-tags`）。
 *        实测（568×320 最紧的一档）：座位号那条带子是 y 3~16.75、左右 x 4~36；
 *        头像中心 x=38.4、宽约 30.7；头像是"内容垂直居中"排下来的，顶边 y≈15.5。
 *        所以角标取 `top:18px`（**已避过座位号那条带子**下沿 16.75），
 *        `translateX(-50%)` 后横向落在 x≈18~59 —— 与座位号在**纵向**已错开 → 矩形不重叠 ✓；
 *        右下角的徽标在 y≈60 以下，更是互不相干 ✓。
 *        屏幕再大时卡片更宽、头像更靠中间，横向也自动错开 ✓。
 *   外观：橙底白字小胶囊（"未准备"= 需要动作）、≥11px、轻描边/阴影，不抢头像。
 *   准备之后：`.off` → `opacity:0` 走 **200ms** 过渡淡出（client.js 把旧角标移回新卡再打 .off，
 *   所以这段过渡是真能看到动画的，不是 DOM 重建后的"凭空消失"）。
 *
 * ── 二、「提醒准备」胶囊（`#roomNudgeBtn`）──
 *   · **只有房主可见**（非房主 `display:none` —— 不是置灰）；
 *   · 位置：信息条**右上角**，`position:absolute` **不占文档流**（摘要行是整行居中的长句，
 *     塞进流里会把行高顶高 → 六档底部余量就变了 ✗）；
 *   · 还有未准备的真人 → 可点；全员准备好 → 置灰；冷却中 → 置灰 + 倒计时（判定在服务端）；
 *   · 字号 12px（≥11px）、可点高 **28px**（≥28px 口径）。
 * ══════════════════════════════════════════════════════════════════════════ */

/* ── 一、未准备角标 ── */
#roomLayer .room-seat .rs-flag {
  position: absolute; /* ← 不占流：几何一分不变 */
  left: 50%;
  top: 18px;
  z-index: 2; /* 只压在头像（同层）之上即可，不参与牌桌层级 */
  transform: translateX(-50%);
  display: inline-flex;
  align-items: center;
  padding: 0 6px;
  border: 1px solid rgba(255, 255, 255, 0.55);
  border-radius: 999px;
  background: linear-gradient(180deg, #ffab2e, #ef7d16);
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.32);
  color: #fff;
  font-size: clamp(11px, 2.2vh, 13px); /* ≥11px */
  font-weight: 800;
  line-height: 1.3;
  white-space: nowrap;
  pointer-events: none; /* 角标不吃点击：座位卡自己才是"换位"的靶子 */
  opacity: 0;
  transition: opacity 0.2s ease; /* 准备之后：200ms 淡出（.off 见下） */
}
#roomLayer .room-seat .rs-flag.on {
  opacity: 1;
}
#roomLayer .room-seat .rs-flag.off {
  opacity: 0;
}
/* 被提醒准备时闪一下（轻量、不改变任何布局） */
@keyframes room-flag-pulse {
  0%, 100% { transform: translateX(-50%) scale(1); }
  50% { transform: translateX(-50%) scale(1.18); }
}
#roomLayer .room-seat .rs-flag.pulse {
  animation: room-flag-pulse 0.42s ease-in-out 2;
}

/* ── 二、提醒准备胶囊（房主专用） ── */
#roomLayer .room-info {
  position: relative; /* 提醒胶囊的定位基准（这一条不改任何布局） */
}
#roomLayer .room-info .nudge {
  position: absolute; /* ← 不占流：摘要行高度不变，六档底部余量不变 */
  right: clamp(4px, 0.8vw, 10px);
  top: clamp(1px, 0.4vh, 4px);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 28px; /* 可点高度下限 */
  padding: 0 clamp(8px, 1.6vw, 14px);
  border: 1px solid rgba(255, 217, 138, 0.7);
  border-radius: 999px;
  background: linear-gradient(180deg, rgba(255, 217, 138, 0.96), rgba(240, 140, 0, 0.96));
  box-shadow: 0 3px 10px rgba(0, 0, 0, 0.3);
  color: #43290a;
  font-size: clamp(12px, 2.3vh, 15px); /* ≥11px（按钮口径 13px 的下限附近，这里取 12~15） */
  font-weight: 800;
  line-height: 1;
  white-space: nowrap;
}
#roomLayer .room-info .nudge:disabled {
  border-color: rgba(255, 255, 255, 0.22);
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.16), rgba(255, 255, 255, 0.07));
  color: rgba(255, 255, 255, 0.62);
  box-shadow: none;
}
/* 非房主 / 开局后：整个不显示（不是置灰）—— 由 client.js 加 .hidden */
#roomLayer .room-info .nudge.hidden {
  display: none !important;
}
/* 矮屏（≤370px）：只把内边距和字号收到下限，可点高度仍是 28px */
@media (max-height: 370px) {
  #roomLayer .room-info .nudge {
    min-height: 28px;
    padding: 0 8px;
    font-size: 12px;
  }
  #roomLayer .room-seat .rs-flag {
    top: 17px;
    padding: 0 5px;
    font-size: 11px;
  }
}

/* ══════════════════════════════════════════════════════════════════════════
 * 牌桌 · 第十一批：**局间准备层**（产品口径 2026-10-08「一局打完要有准备界面」）
 *
 * 只在"这一局结束 + 这一桌有 ≥2 个真人"时出现（服务端发 `roundWait`，见 server.ts）；
 * 纯单人（默认路径）**永远不弹**，保持原来的「下一局」手感。
 *
 * ⚠️ 两条硬口径：
 *   ① `position:absolute; inset:0`（**不是 fixed** —— 强制横屏 rotate(90deg) 在部分安卓上
 *      会把 fixed 退回视口定位，这是本项目反复踩过的坑，房间层也用 absolute）；
 *   ② `z-index:650`：**高于牌桌/手牌/按钮（≤41）**，但**低于名次横幅(700)、设置面板(800)** ——
 *      这一局的名次横幅（上游/二游/下游）正好在这时候飞，不能被这层挡住。
 *
 * 字号沿用既有下限：标题 ≥13px、昵称/状态 ≥11px、按钮 ≥13px 且可点高 ≥28px。
 * ══════════════════════════════════════════════════════════════════════════ */
.round-layer {
  position: absolute;
  inset: 0;
  z-index: 650;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: clamp(6px, 1.6vh, 18px) clamp(10px, 3vw, 32px);
  background: rgba(4, 20, 14, 0.82);
  -webkit-tap-highlight-color: transparent;
  animation: home-in 0.18s ease-out;
}
.round-layer.hidden {
  display: none !important;
  pointer-events: none;
}
.round-box {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(4px, 1vh, 10px);
  width: min(520px, 94vw);
  max-height: 100%;
  padding: clamp(8px, 2vh, 18px) clamp(10px, 2.4vw, 24px);
  border: 2px solid rgba(255, 217, 138, 0.7);
  border-radius: clamp(10px, 2vh, 18px);
  background: linear-gradient(180deg, rgba(28, 62, 44, 0.98), rgba(12, 32, 24, 0.98));
  box-shadow: 0 14px 36px rgba(0, 0, 0, 0.5), inset 0 0 0 1px rgba(255, 255, 255, 0.08);
  overflow: hidden; /* 内容再多也只裁，不把整层撑出屏幕 */
}
.round-title {
  font-size: clamp(13px, 2.8vh, 19px); /* ≥13px */
  font-weight: 900;
  letter-spacing: 0.08em;
  color: #ffe9a8;
}
.round-settle {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: rgba(255, 255, 255, 0.72);
  font-size: clamp(11px, 2.2vh, 14px); /* ≥11px */
}
.round-list {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: clamp(4px, 1vh, 10px);
  width: 100%;
}
.round-person {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  max-width: 100%;
  padding: clamp(3px, 0.8vh, 7px) clamp(8px, 1.6vw, 14px);
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.22);
}
.round-person.ready {
  border-color: rgba(127, 224, 147, 0.7);
  background: rgba(31, 122, 73, 0.32);
}
.round-name {
  max-width: 40vw;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: #fff;
  font-size: clamp(11px, 2.2vh, 14px); /* ≥11px */
  font-weight: 700;
}
.round-tag {
  color: rgba(255, 255, 255, 0.82);
  font-size: clamp(11px, 2.2vh, 14px); /* ≥11px */
  font-weight: 800;
  white-space: nowrap;
}
.round-person.ready .round-tag {
  color: #b8f5c6;
}
.round-hint {
  max-width: 100%;
  color: rgba(255, 233, 168, 0.9);
  font-size: clamp(11px, 2.2vh, 14px); /* ≥11px */
  font-weight: 700;
  line-height: 1.3;
  text-align: center;
}
.round-ready {
  min-height: 30px; /* 可点高下限 ≥28px */
  padding: clamp(6px, 1.4vh, 12px) clamp(18px, 4vw, 40px);
  border-radius: 999px;
  font-size: clamp(13px, 2.6vh, 18px); /* ≥13px */
  font-weight: 800;
}
.round-ready:disabled {
  opacity: 0.65;
  filter: brightness(0.95);
}
/* 矮屏（≤370px）：只收间距/内边距，字号与可点高度一个都不动 */
@media (max-height: 370px) {
  .round-box {
    gap: 3px;
    padding: 7px 12px;
  }
  .round-list {
    gap: 4px;
  }
  .round-person {
    padding: 2px 8px;
  }
}

/* ══════════════════════════════════════════════════════════════════════════
 * 牌桌 · 第十二批：**「你已暂离，点这里回到牌桌」可点**（产品口径 ①）
 *
 * 背景：中间那行提示（`#centerHint`）原来在"没座位"时一律说「座位在另一台设备上」✗，
 * 而真正最常见的情况是**自己的连接断了/被托管后正在重连**（同一台设备！）。
 * 现在文案分三种（见 client.js 的 hintText），其中"暂离"这一种**可以点**：
 * 点一下就按同一个 clientId 重新认回自己的座位（roomJoin/join），
 * **不是**去抢别人的座位（同 clientId 才认得回来）。
 *
 * ⚠️ 这里只加"可点"样式与手感，**不动**字号/位置；其余状态（比如「要不起，自动过牌…」）
 *    不带 .clickable，仍然不吃点击。
 * ══════════════════════════════════════════════════════════════════════════ */
#centerHint.clickable {
  pointer-events: auto;
  cursor: pointer;
  padding: 4px 14px;
  border: 1px solid rgba(255, 217, 138, 0.6);
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.42);
  text-decoration: underline;
  text-underline-offset: 3px;
}
#centerHint.clickable:active {
  transform: scale(0.98);
}

/* ══════════════════════════════════════════════════════════════════════════
 * 牌桌 · 第十三批（产品口径 2026-10-09）
 *
 * ── B：还贡按钮搬到**倒计时圆环右边 +5px**，尺寸与「出牌」按钮**完全一样（80×40）** ──
 *
 * 实测依据（874×402，`.deploy/measure-return-and-rank.mjs`）：
 *   `#actionCenter .turn-timer.in-row` rect = x 417 / w 40 → **right = 457**，
 *   而按钮行的圆心是 `50%`（=437）、圆环右沿 = 50% + 20px → **圆环右沿 + 5px = 50% + 25px**，
 *   与既有的 `#btnHint`（提示）**同一列** ✓。
 *   为什么要放这一列：还贡阶段只有「还贡」一颗按钮（提示/出牌/不出/要不起全部隐藏，
 *   见 client.js 的 renderActions），所以**不会和任何按钮挤在一起**；
 *   而且圆环此刻正挂在同一行（`.in-row`），按钮就长在圆环右边，视线是顺的 ✓。
 *   ⚠️ 圆环本身（`r:22.5px` / `#f08c00` / 层级 41 / `margin-left:-20px`）**一个像素都没动**。
 *
 * 尺寸：`80×40` —— 与「出牌」按钮同一口径（原来是 auto 宽 × 39 高，实测 68×39 ✗）。
 * ⚠️ 选择器要点：`#btnReturn` 现在在 **`#actionCenter` 里面**（和倒计时圆环、提示/出牌同一行，
 *   见 index.html 的说明）。第一版把它留在 `.btn-row` 里，`left: calc(50% + 25px)`
 *   解析出来是 472（= 圆环右沿 + 15px ✗，差 10px）—— 因为那个定位容器的 50% 不等于屏幕中心。
 *   搬进 `#actionCenter` 后与 `#btnHint`/`#btnNoBeat` 用**同一个锚点**，
 *   `left: calc(50% + 25px)` 就是"圆环右沿(50%+20) + 5px" ✓（量测复核过）。
 * ────────────────────────────────────────────────────────────────────────── */
#actionCenter #btnReturn {
  /*
   * ⚠️ 用 **fixed**（和倒计时圆环同一种定位 —— 圆环本身就是 `body .turn-timer{position:fixed}`）：
   *    产品口径「与圆环**上边**对齐」，而圆环是 fixed 浮层；按钮若留在按钮行的流式/absolute
   *    定位里，垂直方向对不上（上一轮实测：按钮 top=294、圆环 top=107 ✗，差 187px ✗）。
   *    left/top 由 client.js 的 alignReturnButton() 按**实测 rect** 写 inline
   *    （先把 left/top 归零量一次，再按"目标 − 当前"补差 ⇒ 任何屏幕尺寸都精确 ✓）。
   *
   * ⚠️⚠️ 这里**只写"和出牌不一样"的那些**（定位与尺寸）✓ ——
   *    配色/边框/圆角/内高光/字号/字重/字距/内边距/各状态：
   *    **一律共用 `#actionCenter #btnPlay, #actionCenter #btnReturn {...}` 那条规则** ✓
   *    （产品第二轮口径：「要和出牌按钮一样的**配色和样式**」✗ 只对齐 4 项颜色不够 ✗）
   *    ⇒ 两条按钮的 computed style 除"位置/尺寸"外**逐属性全等**（有全量 diff 探针把关 ✓）。
   */
  position: fixed !important;
  right: auto !important;
  bottom: auto !important;
  /*
   * ⚠️ 只留 `width`，**不要写 `min-width`** ——
   *    全量 diff 探针抓到过：出牌的 `min-width` 算出 0px，而这里写 80px 会多出一项差异 ✗
   *    （产品要的是"逐属性一致"✗ 能少一项就少一项 ✓；`width:80px` 已经保证尺寸 ✓）
   */
  width: 80px !important;
  height: 40px !important;
}

/* ══════════════════════════════════════════════════════════════════════════
 * 牌桌 · 第十四批：删掉「轮到谁」的两处**表现**（产品口径 2026-10-09）
 *
 * 产品原话：
 *   · 「到谁出牌，怎么还剩几张的样式会变成金边，这个**不要变**，**默认的就行**」
 *   · 「到谁出牌，谁的头像闪，这个逻辑**也删除**」
 *
 * ⚠️ 只删**表现** ✗，**不删状态** ✓：
 *   `.turn` 这个类（"轮到谁"的判断）还在、还挂在座位上、判断逻辑一行没动 ✓
 *   —— 倒计时圆环靠它显示（产品明确要求保留 ✓），所以千万别动 `.turn` 本身 ✓。
 *
 * ① 剩余张数（`.seat-remain`）恢复**默认样式**：
 *    原来 `.seat.turn .seat-remain` 会覆盖成金边 + 金色光圈 ✗（规则在 base 区 533 行
 *    与补丁区 2611 行各一份，后者生效 ✗）。这里把这两项**还原成默认值**
 *    （默认就是上面那条 `.seat-remain` 规则：蓝边 `#8fd8ff` + 普通投影 ✓）。
 *    ⚠️ 注意别把"轮到谁"之外的用途删了：这两项只用于 `.seat.turn`，
 *       蓝底白字的张数徽标在**所有**状态下都照常显示 ✓（自己/队友的区分走的是别的规则 ✓）。
 *
 * ② 头像不闪：`animation` 去掉（原来是 `animation: pulse 1.1s ease-in-out infinite` ✗，
 *    规则同样两份：518/2596 行）。**只删动画** ✓ —— 头像的金色描边与光圈是静态的，
 *    产品说的是"闪"✗ 并且明确"别的头像样式（尺寸/圆角/描边）不要删"✓ 所以留着 ✓。
 *    ⇒ 上面那两个 `@keyframes pulse`（518 区/2596 区）从此**没有任何选择器引用** ✓（成了死代码）。
 * ══════════════════════════════════════════════════════════════════════════ */
.seat.turn .seat-remain {
  border-color: #8fd8ff; /* 默认蓝边（原来是 var(--gold) 金边 ✗） */
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.45); /* 默认投影（原来是金色光圈 ✗） */
}
.seat.turn .avatar {
  animation: none; /* 原来是 animation: pulse 1.1s ease-in-out infinite ✗ */
}

/* ══════════════════════════════════════════════════════════════════════════
 * 牌桌 · 第十六批：**「要不起」宽度 120 → 60**（产品口径 2026-10-09 主动修改 ✓）
 *
 * 产品原话：「要不起的尺寸改一下宽度改为 60」（原来 120×40 ✗）。
 *   · 尺寸：**60×40** ✓（高度不变 ✓；`padding` 归零 —— 否则 `.btn` 的 16px 46px 内边距
 *     会把 60 宽撑爆 ✗）
 *   · 位置口径**不变** ✓：离中线 +25px（`#actionCenter #btnNoBeat { left: calc(50% + 25px) }` ✓
 *     由它自己那条规则提供 ✓）｜ 上边 = 中线 −94px ✓（按钮行的既有口径 ✓）
 *   · 另外三个按钮（不出/提示/出牌 80×40）**一个字节没动** ✓
 *   · 与「还贡」的关系：两者**互斥阶段**出现（要不起在出牌阶段、还贡在还贡阶段 ✓），
 *     隐藏的那个是 `display:none`（`.hidden` ✓）**不吃点击** ✓ —— 有 elementFromPoint 判据把关 ✓。
 * ══════════════════════════════════════════════════════════════════════════ */
#actionCenter #btnNoBeat {
  width: 80px !important;
  min-width: 0 !important;
  padding: 0 !important;
}
/* ══════════════════════════════════════════════════════════════════════════
 * 牌桌 · 第十七批：**「还贡」最外圈那圈白**（产品实测 2026-10-09）
 *
 * 现象：产品说「还贡的图标怎么有一圈白色的白圈最外边，我说了和出牌的按钮一样」✗
 * 排查结论（代码级 ✓）：**不是我们 CSS 画的** ✗ ——
 *   全仓 `:focus` / `outline` 规则只有两条，都是输入框的（`.inp:focus{outline:none}` ✓），
 *   **按钮上一条都没有** ✓ ⇒ 那圈白是**浏览器默认的聚焦指示环**（UA 的 `:focus-visible{outline:auto}` ✓）：
 *   产品**刚点/刚按过还贡**，它就成了页面里被聚焦的元素 ✗，浏览器就给它描了一圈 ✗
 *   （出牌平时是"没被点过"的状态 ✓ 所以对比之下像是"还贡多了一圈" ✓）。
 *
 * 改法（最小 ✓ 只动还贡 ✓ **不碰出牌** ✓）：
 *   把还贡的聚焦指示环与 iOS 点按高亮都关掉 ✓ —— 它是**点按型按钮**，
 *   聚焦态视觉已经由 `.btn.primary` 的橙金底色表达 ✓ 不需要系统那圈白 ✓。
 *   我把**出牌按钮一个字没动** ✓（它是基准 ✓）；若产品要求两颗都关 ✗ 说一声我再加 ✓
 *   （那样会改变出牌被聚焦时的外观 ✗ 所以我没擅自动它 ✓）。
 * ══════════════════════════════════════════════════════════════════════════ */
#actionCenter #btnReturn:focus,
#actionCenter #btnReturn:focus-visible,
#actionCenter #btnReturn:active {
  outline: none !important;
  outline-offset: 0 !important;
  -webkit-tap-highlight-color: transparent !important;
  -webkit-focus-ring-color: transparent !important;
}
#actionCenter #btnReturn {
  -webkit-tap-highlight-color: transparent !important;
}
/* ══════════════════════════════════════════════════════════════════════════
 * 牌桌 · 第十八批：**「整理」默认隐藏 + 设置里可打开**（产品口径 2026-10-09）
 *
 * ⚠️ 用 `visibility:hidden + pointer-events:none` ✗**不是** `display:none` ✗：
 *    · `display:none` 会让 `#btnTidy` 的 rect 归零 ✗ ⇒ `?diag=1` 的冻结体检测不到尺寸 ✗ 会变红 ✗
 *    · `visibility:hidden` **保留布局盒** ⇒ 体检量到的仍是既验口径 **36×26 @ 距底 2 / 距右 40** ✓
 *    · 同时 `pointer-events:none` 保证它点不到 ✓（"隐藏"就该点不到 ✓）
 * 打开（body 上没有 .hide-tidy）时**完全不生效** ✓ ⇒ 与历史样式逐项一致 ✓
 * ══════════════════════════════════════════════════════════════════════════ */
body.hide-tidy #btnTidy {
  visibility: hidden !important;
  pointer-events: none !important;
}
/* ══════════════════════════════════════════════════════════════════════════
 * 全局 · 第十九批：**禁止长按选中/放大镜/复制菜单**（产品口径 2026-10-09）
 *
 * 产品原话：「现在长按会有像网页一样的选择，不要这个逻辑」✗
 *   · 对**游戏界面**（`body` + `#app`）关掉文字选择与 iOS 长按呼出（`-webkit-touch-callout`）✓
 *   · ⚠️ **例外**：输入框（昵称 / 房号 / 设置里的输入）**必须还能选** ✗ ——
 *     否则用户改不了昵称、输不了房号 ✗✓（这里给 `input, textarea` 显式恢复 `user-select: text` ✓）
 *   · `user-select:none` **不影响点击** ✓（要不起/还贡/出牌照样可点 ✓ 有真机判据把关 ✓）
 *   · 不写 `!important` 到 input 那一侧 ✓，靠后写的规则覆盖 ✓
 * ══════════════════════════════════════════════════════════════════════════ */
body,
#app {
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none; /* iOS 长按弹出菜单/放大镜 ✓ */
}
/* 输入框恢复可选（不然昵称/房号改不了 ✗） */
#app input,
#app textarea,
input,
textarea {
  -webkit-user-select: text;
  user-select: text;
  -webkit-touch-callout: default;
}
/* ══════════════════════════════════════════════════════════════════════════
 * 牌桌 · 第二十批：**结算两颗按钮**（产品口径 2026-10-09 第 ② 条）
 *
 * 「返回」：      宽 **80** ✓ 高 **40** ✓ 蓝色 ✓ 右边缘 = 圆环左边缘 − **5px** ✓
 * 「继续」：      宽 **80** ✓ 与「出牌」**逐属性一致** ✓（选择器并列共享声明 ✓ 见上面改过的两条 ✓）
 *                 左边缘 = 圆环右边缘 + **5px** ✓ 上边缘 = 圆环上边缘 + **0px** ✓
 *   · 位置由 client.js 的 paintFinishButtons() 每帧按圆环**实测 rect** 写 inline ✓（不写死坐标 ✗）
 *   · `position:fixed`（与圆环同一套定位 ✓ 与还贡一致 ✓）
 *   · 只在 `phase==='finished'` 出现 ✓（其它阶段 `.hidden{display:none}` ✓ 不留残影 ✗）
 *   · 蓝色显式写，且与「不出」共用同一渐变；不另加白色外描边 ✓
 *   · 按钮聚焦时关闭浏览器默认白色焦点圈，避免误看成白边 ✓
 * ══════════════════════════════════════════════════════════════════════════ */
#actionCenter #btnBackHome {
  position: fixed !important;
  right: auto !important;
  bottom: auto !important;
  width: 80px !important;
  height: 40px !important;
  padding: 0 !important;
  border: none !important;
  border-radius: 999px !important;
  background: linear-gradient(180deg, #8fc4ff 0%, #3f86f0 55%, #2f6fd8 100%) !important;
  color: #fff !important;
  font-size: 17px !important;
  font-weight: 800 !important;
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.38), inset 0 2px 0 rgba(255, 255, 255, 0.45) !important;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.25) !important;
}
#actionCenter #btnBackHome:focus,
#actionCenter #btnBackHome:focus-visible,
#actionCenter #btnBackHome:active {
  outline: none !important;
  outline-offset: 0 !important;
  -webkit-tap-highlight-color: transparent !important;
  -webkit-focus-ring-color: transparent !important;
}
#actionCenter #btnBackHome {
  -webkit-tap-highlight-color: transparent !important;
}
#actionCenter #btnContinue {
  position: fixed !important;
  right: auto !important;
  bottom: auto !important;
  width: 80px !important;
  height: 40px !important;
  padding: 0 !important;
}
/* ⚠️ 「继续」与「出牌」「还贡」**并列共享同一条声明**（产品口径：颜色/样式逐属性一致 ✓ 不抄一遍 ✗）
   —— 这里用"并列选择器"新增一条，等于把三者绑在同一份声明上 ✓（后写覆盖 ✓ 与既有那条同值 ✓） */
#actionCenter #btnPlay,
#actionCenter #btnReturn,
#actionCenter #btnContinue {
  background: linear-gradient(180deg, #ffd98a 0%, #ffab2e 55%, #f08c00 100%) !important;
  color: #fff !important;
  text-shadow: 0 1px 2px rgba(120, 60, 0, 0.45) !important;
}
#actionCenter #btnPlay:disabled,
#actionCenter #btnReturn:disabled,
#actionCenter #btnContinue:disabled {
  opacity: 1 !important;
  filter: none !important;
}

/* ══════════ 结算按钮：复用出牌区倒计时锚点，左右各留 5px ══════════ */
#actionCenter.finish-layout {
  /* 与正常出牌区使用同一个 50% / 46% 定位锚点；按钮和占位都以它为基准。 */
  position: fixed !important;
  left: 50% !important;
  top: 46% !important;
  right: auto !important;
  bottom: auto !important;
  transform: translate(-50%, -50%) !important;
  display: block !important;
  width: 0 !important;
  height: 0 !important;
}

#actionCenter.finish-layout #finishCenterSlot {
  /* 透明保留与倒计时完全相同的 40×40 中间位置，不参与点击。 */
  position: fixed !important;
  left: 50% !important;
  top: calc(50% - 94px) !important;
  width: 40px !important;
  height: 40px !important;
  margin: 0 0 0 -20px !important;
  display: block !important;
  visibility: hidden !important;
  pointer-events: none !important;
}

#actionCenter.finish-layout #btnBackHome,
#actionCenter.finish-layout #btnNext,
#actionCenter.finish-layout #btnContinue {
  position: fixed !important;
  top: calc(50% - 94px) !important;
  right: auto !important;
  bottom: auto !important;
  width: 80px !important;
  height: 40px !important;
  min-width: 80px !important;
  padding: 0 !important;
  transform: none !important;
}

#actionCenter.finish-layout #btnBackHome {
  /* 中间位左边：80px 按钮 + 5px 间距 + 40px 倒计时占位 */
  left: calc(50% - 105px) !important;
}

#actionCenter.finish-layout #btnNext,
#actionCenter.finish-layout #btnContinue {
  /* 中间位右边：倒计时占位 + 5px 间距 */
  left: calc(50% + 25px) !important;
}

/* ══════════ 页面统一左右安全边距：至少 40px，刘海/安全岛更大时取安全区 ══════════ */
html {
  --page-edge-left: max(var(--edge-gap), env(safe-area-inset-left, 0px));
  --page-edge-right: max(var(--edge-gap), env(safe-area-inset-right, 0px));
}

#homeLayer .home-box {
  width: min(880px, calc(100% - var(--page-edge-left) - var(--page-edge-right)));
}

#roomLayer .room-box {
  width: 100%;
  padding-inline: var(--page-edge-left) var(--page-edge-right);
}

/* ══════════ 房间：左右两队各占一列，队内两个正方形座位卡 ══════════ */
#roomLayer .room-team-seats {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

#roomLayer .room-seat {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  grid-template-columns: none;
  grid-template-rows: none;
  width: 100%;
  height: auto;
  aspect-ratio: 1 / 1;
  min-width: 0;
  padding: clamp(9px, 1.4vh, 14px);
  gap: clamp(4px, 0.8vh, 7px);
  text-align: center;
}

#roomLayer .room-seat .rs-avatar {
  flex: 0 0 auto;
  width: clamp(40px, 7vh, 62px);
  height: clamp(40px, 7vh, 62px);
  margin-top: clamp(7px, 1.2vh, 12px);
  border-radius: 16px;
}

#roomLayer .room-seat .rs-pos {
  position: absolute;
  top: 7px;
  left: 9px;
  grid-column: auto;
  grid-row: auto;
  align-self: auto;
  padding: 2px 5px;
  border-radius: 7px;
  background: rgba(0, 0, 0, 0.18);
  font-size: 11px;
}

#roomLayer .room-seat .rs-name {
  flex: 0 0 auto;
  width: 100%;
  grid-column: auto;
  grid-row: auto;
  align-self: auto;
  text-align: center;
}

#roomLayer .room-seat .rs-tags {
  right: 7px;
  bottom: 7px;
  max-width: calc(100% - 14px);
}

/* 未准备固定在头像正上方，不占座位卡布局空间。 */
#roomLayer .room-seat .rs-flag {
  top: 4px;
  left: 50%;
  transform: translateX(-50%);
}

#roomLayer .room-actions {
  justify-content: flex-end;
}

#roomLayer #roomLeaveBtn {
  order: 2;
}

#roomLayer #roomStartBtn {
  order: 3;
}

#roomLayer #roomReadyBtn {
  order: 1;
}

/* 身份由座位底部的「我」标识承担，队伍与头像边框保持中性颜色。 */
#roomLayer #roomSeats .room-team.mine {
  border-color: var(--room-line);
  background: linear-gradient(145deg, rgba(255, 255, 255, 0.075), rgba(0, 0, 0, 0.16));
  box-shadow: 0 12px 28px rgba(0, 0, 0, 0.18);
  animation: none;
  transition: none;
}

#roomLayer #roomSeats .room-team.mine .room-team-head {
  border-bottom-color: rgba(255, 255, 255, 0.13);
  color: rgba(255, 255, 255, 0.92);
}

#roomLayer #roomSeats .room-seat.self .rs-avatar {
  border-color: rgba(255, 255, 255, 0.25);
  box-shadow: none;
}

/* 房间页按主界面的控件位置复用纯图标入口：全屏左、设置右。 */
#roomLayer > .settings-icon-trigger {
  position: absolute;
  top: max(16px, env(safe-area-inset-top, 0px));
  right: max(var(--edge-gap, 40px), env(safe-area-inset-right, 0px));
}

#roomLayer > .fullscreen-trigger {
  right: calc(max(var(--edge-gap, 40px), env(safe-area-inset-right, 0px)) + 32px);
}

@media (orientation: portrait) {
  #roomLayer > .settings-icon-trigger {
    right: max(var(--edge-gap, 40px), env(safe-area-inset-bottom, 0px));
  }

  #roomLayer > .fullscreen-trigger {
    right: calc(max(var(--edge-gap, 40px), env(safe-area-inset-bottom, 0px)) + 32px);
  }
}

/* 创建房间：房号、复制按钮和摘要同排，摘要紧跟复制按钮；房号不再套外框。 */
#roomLayer .room-info {
  display: flex;
  flex-direction: row;
  align-items: center;
  width: 100%;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
  box-shadow: none;
}

#roomLayer .room-info-main {
  justify-content: flex-start;
  flex-wrap: wrap;
  width: 100%;
  min-width: 0;
  gap: 10px;
}

#roomLayer .room-info .room-hint {
  flex: 0 1 auto;
  grid-column: auto;
  grid-row: auto;
  max-width: min(42vw, 440px);
  margin: 0;
  color: var(--room-muted);
  font-size: clamp(11px, 1.8vh, 14px);
  line-height: 1.35;
  text-align: left;
}

/* 两队面板同高对齐；移除队友提示后，两列座位自然从同一高度开始。 */
#roomLayer .room-seats {
  align-items: stretch;
}

#roomLayer .room-team {
  align-self: stretch;
  height: 100%;
  box-sizing: border-box;
}

#roomLayer .room-team-seats {
  align-items: stretch;
}

/* 放大头像，但给正方形座位卡留足昵称与状态空间。 */
#roomLayer .room-seat .rs-avatar {
  width: clamp(48px, 9vh, 72px);
  height: clamp(48px, 9vh, 72px);
  margin-top: 0;
}

/* 房主：返回左、开始右；队友：返回左、准备右，不显示无效的等待按钮。 */
#roomLayer .room-actions {
  justify-content: space-between;
}

#roomLayer #roomLeaveBtn {
  order: 1;
  margin-right: auto;
}

#roomLayer #roomReadyBtn,
#roomLayer #roomStartBtn {
  order: 2;
  margin-left: auto;
}

/* 无边框设置图标：主页与牌桌共用同一入口样式。 */
.settings-icon-trigger {
  position: relative;
  z-index: 3;
  display: inline-grid;
  place-items: center;
  flex: 0 0 auto;
  width: 32px;
  height: 32px;
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
  color: rgba(238, 245, 241, 0.92);
  cursor: pointer;
}

.settings-icon-trigger svg {
  display: block;
  width: 15px;
  height: 15px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
  pointer-events: none;
}

.settings-icon-trigger:hover,
.settings-icon-trigger:focus-visible {
  color: var(--gold);
  outline: none;
}

#homeLayer > .settings-icon-trigger {
  position: absolute;
  top: max(16px, env(safe-area-inset-top, 0px));
  right: max(var(--edge-gap, 40px), env(safe-area-inset-right, 0px));
}

#homeLayer > .fullscreen-trigger {
  right: calc(max(var(--edge-gap, 40px), env(safe-area-inset-right, 0px)) + 32px);
}

/* 贡牌提示固定在操作区下方，状态驱动显示，直到还牌完成才撤下。 */
#actionCenter #tributePrompt {
  position: fixed;
  z-index: 42;
  top: calc(46% + 30px);
  left: 50%;
  transform: translateX(-50%);
  width: max-content;
  max-width: min(88vw, 520px);
  padding: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
  box-shadow: none;
  color: #ffe39b;
  font-size: clamp(12px, 1.7vw, 16px);
  font-weight: 700;
  line-height: 1.35;
  text-align: center;
  white-space: normal;
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.8);
  pointer-events: none;
}

#actionCenter #tributePrompt.hidden {
  display: none;
}

@media (orientation: portrait) {
  #homeLayer > .settings-icon-trigger {
    right: max(var(--edge-gap, 40px), env(safe-area-inset-bottom, 0px));
  }

  #homeLayer > .fullscreen-trigger {
    right: calc(max(var(--edge-gap, 40px), env(safe-area-inset-bottom, 0px)) + 32px);
  }
}

/* ══════════ 每局结算：暖金斜切奖章 + 象牙白双队成绩板 ══════════ */
.match-result-layer {
  position: absolute;
  inset: 0;
  z-index: 690;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: clamp(8px, 2vh, 18px);
  padding: clamp(8px, 3vh, 22px) max(40px, env(safe-area-inset-left)) clamp(8px, 3vh, 22px) max(40px, env(safe-area-inset-right));
  background: radial-gradient(ellipse at 50% 50%, rgba(15, 66, 43, 0.92), rgba(2, 18, 12, 0.96));
  -webkit-tap-highlight-color: transparent;
}
.match-result-layer.hidden { display: none !important; }
.match-result-card {
  display: flex;
  position: relative;
  width: min(1040px, 100%);
  min-height: min(55vh, 340px);
  overflow: hidden;
  border: 2px solid #f9d67c;
  border-radius: clamp(12px, 2.5vh, 24px);
  background: #fff4d8;
  box-shadow: 0 22px 60px rgba(0, 0, 0, 0.53), 0 0 0 5px rgba(251, 196, 72, 0.1), inset 0 0 0 2px rgba(255, 255, 255, 0.65);
}
.match-result-card::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  border: 1px solid rgba(255, 255, 255, 0.6);
  border-radius: inherit;
}
.match-result-hero {
  position: relative;
  z-index: 1;
  display: flex;
  flex: 0 0 35%;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: clamp(3px, 1vh, 8px);
  margin-right: -5%;
  padding: 12px 9% 12px 4%;
  clip-path: polygon(0 0, 100% 0, 85% 100%, 0 100%);
  color: #fff9d9;
  background: radial-gradient(circle at 15% 22%, #ffeaa1, transparent 45%), linear-gradient(150deg, #ffd75b 0%, #f5a21a 43%, #db7105 100%);
  text-shadow: 0 3px 8px rgba(116, 50, 0, 0.28);
}
.match-result-hero::before {
  content: '';
  position: absolute;
  width: 42vh;
  height: 42vh;
  border: 1px solid rgba(255, 255, 230, 0.25);
  border-radius: 50%;
  box-shadow: 0 0 0 14px rgba(255, 255, 235, 0.07), 0 0 0 32px rgba(255, 255, 235, 0.04);
}
.match-result-hero.loss { background: radial-gradient(circle at 15% 20%, #bfd6d0, transparent 46%), linear-gradient(145deg, #8faea4, #486a60 62%, #29493f); }
.match-result-chinese, .match-result-english { position: relative; }
.match-result-chinese { font-size: clamp(38px, 12vh, 100px); font-weight: 1000; letter-spacing: 0.04em; line-height: 1; white-space: nowrap; }
.match-result-english { font-size: clamp(20px, 5.3vh, 48px); font-weight: 1000; letter-spacing: 0.09em; line-height: 1; }
.match-result-details {
  display: flex;
  flex: 1 1 auto;
  min-width: 0;
  flex-direction: column;
  justify-content: center;
  padding: clamp(5px, 1.4vh, 14px) clamp(12px, 3vw, 32px) clamp(5px, 1.4vh, 14px) clamp(26px, 5.8vw, 66px);
  color: #62431c;
  background:
    radial-gradient(ellipse at 90% 8%, rgba(243, 181, 68, 0.22), transparent 45%),
    repeating-linear-gradient(-35deg, transparent 0 24px, rgba(171, 119, 45, 0.025) 25px 26px),
    linear-gradient(120deg, #fff8e6, #ffebc0);
}
.match-result-kicker { color: #ad6e1f; font-size: clamp(11px, 2.4vh, 16px); font-weight: 850; letter-spacing: 0.16em; }
.match-result-details h2 { margin: 4px 0 4px; color: #743f0a; font-size: clamp(20px, 5.3vh, 36px); line-height: 1.1; }
.match-result-summary { margin: 0 0 clamp(6px, 1.7vh, 14px); color: #a5671d; font-size: clamp(12px, 2.5vh, 17px); font-weight: 750; }
.match-result-teams { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: clamp(7px, 1.5vw, 18px); }
.match-result-team { min-width: 0; padding: clamp(6px, 1.5vh, 13px); border: 1px solid rgba(139, 98, 41, 0.19); border-radius: 12px; background: rgba(255, 255, 255, 0.55); }
.match-result-team.is-winner { border-color: rgba(221, 151, 33, 0.64); background: rgba(255, 250, 230, 0.85); box-shadow: inset 0 0 0 1px rgba(255, 232, 161, 0.65); }
.match-result-team-heading { display: flex; align-items: center; justify-content: space-between; gap: 4px; margin-bottom: 3px; }
.match-result-team-label { color: #ad6917; font-size: clamp(11px, 2.1vh, 15px); font-weight: 900; white-space: nowrap; }
.match-result-level { flex: 0 0 auto; color: #865725; font-size: clamp(10px, 1.9vh, 13px); font-weight: 800; }
.match-result-player { display: flex; align-items: center; gap: 6px; min-width: 0; padding: 3px 0; color: #62431c; font-size: clamp(11px, 2.3vh, 16px); font-weight: 700; }
.match-result-avatar { display: grid; flex: 0 0 clamp(40px, 9vh, 54px); place-items: center; width: clamp(40px, 9vh, 54px); height: clamp(40px, 9vh, 54px); overflow: hidden; border: 1px solid #e7ba68; border-radius: 11px; background: #eadab2; }
.match-result-avatar img { width: 100%; height: 100%; object-fit: cover; }
.match-result-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.match-result-name em { margin-left: 3px; color: #e18411; font-size: 0.82em; font-style: normal; }
.match-result-player b { flex: 0 0 auto; color: #a95320; white-space: nowrap; }
.match-result-wait { max-width: min(1040px, 100%); color: #ffdda0; font-size: clamp(11px, 2.1vh, 15px); font-weight: 700; text-align: center; }
.match-result-wait.hidden { display: none; }
.match-result-actions { display: flex; justify-content: center; gap: clamp(12px, 3vw, 28px); }
.match-result-actions .btn { min-width: clamp(130px, 19vw, 220px); min-height: clamp(40px, 8vh, 62px); padding: 0 20px; border-radius: 999px; color: white; font-size: clamp(14px, 2.6vh, 22px); font-weight: 900; box-shadow: 0 5px 0 rgba(0, 0, 0, 0.2), 0 10px 22px rgba(0, 0, 0, 0.22); }
.match-result-actions .btn:disabled { opacity: 0.7; cursor: default; }
.match-result-back { border: 2px solid rgba(255,255,255,.72); background: linear-gradient(180deg, #65adf4, #3475d6); }
.match-result-continue { border: 2px solid rgba(255,239,190,.9); background: linear-gradient(180deg, #ffc94e, #ec8707); }
@media (max-height: 420px) {
  .match-result-layer { gap: 7px; padding-top: 5px; padding-bottom: 5px; }
  .match-result-card { min-height: 54vh; }
  .match-result-details { padding-top: 4px; padding-bottom: 4px; }
  .match-result-details h2 { margin-bottom: 1px; }
  .match-result-actions .btn { min-height: 34px; }
}

/* ══════════════════════════════════════════════════════════════════════════
 * 主页 · 视觉重构
 *
 * 主页保留原有三个入口的 id 与点击逻辑，只调整信息层级：经典是主入口，
 * 房间/疯狂炸作为次级入口，并用轻量的品牌区和玩法摘要把首页从牌桌背景里
 * 区分出来。所有尺寸都用 clamp，兼顾手机竖屏、手机横屏与桌面窗口。
 * ══════════════════════════════════════════════════════════════════════════ */
.home-layer {
  isolation: isolate;
  background:
    radial-gradient(circle at 50% 18%, rgba(54, 145, 96, 0.5), transparent 38%),
    radial-gradient(circle at 50% 100%, rgba(5, 28, 20, 0.5), transparent 58%),
    linear-gradient(135deg, #0d4a32 0%, #082d21 52%, #062319 100%);
}
.home-layer::before,
.home-layer::after {
  content: '';
  position: absolute;
  pointer-events: none;
  z-index: -1;
}
.home-layer::before {
  inset: 0;
  opacity: 0.22;
  background-image:
    linear-gradient(135deg, transparent 48%, rgba(255, 255, 255, 0.11) 49%, transparent 50%),
    linear-gradient(45deg, transparent 48%, rgba(255, 255, 255, 0.07) 49%, transparent 50%);
  background-size: 42px 42px;
  mask-image: linear-gradient(to bottom, transparent, #000 18%, #000 78%, transparent);
}
.home-layer::after {
  width: min(72vw, 640px);
  height: min(72vw, 640px);
  left: 50%;
  top: 50%;
  border: 1px solid rgba(240, 200, 92, 0.14);
  border-radius: 50%;
  transform: translate(-50%, -50%);
  box-shadow: 0 0 0 28px rgba(240, 200, 92, 0.025), 0 0 0 56px rgba(240, 200, 92, 0.018);
}
.home-box {
  position: relative;
  width: min(880px, calc(100% - 32px));
  max-height: 100%;
  padding: clamp(18px, 5vh, 46px) 0 clamp(14px, 3vh, 28px);
  gap: clamp(14px, 3vh, 26px);
}
.home-intro {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: clamp(7px, 1.2vh, 12px);
  width: 100%;
}
.home-kicker {
  display: flex;
  align-items: center;
  gap: 9px;
  color: rgba(255, 255, 255, 0.58);
  font-size: clamp(10px, 1.8vh, 13px);
  font-weight: 700;
  letter-spacing: 2px;
}
.home-kicker span {
  display: block;
  width: 24px;
  height: 1px;
  background: rgba(240, 200, 92, 0.72);
}
.home-logo {
  flex-direction: row;
  align-items: center;
  gap: clamp(10px, 1.8vw, 18px);
  line-height: 1;
}
.home-logo-mark {
  display: grid;
  place-items: center;
  width: clamp(46px, 8vh, 64px);
  height: clamp(46px, 8vh, 64px);
  border: 1px solid rgba(255, 232, 164, 0.68);
  border-radius: 18px;
  background: linear-gradient(145deg, #ffda78, #bd7b1e);
  color: #173726;
  font-size: clamp(25px, 5vh, 38px);
  font-weight: 950;
  box-shadow: 0 8px 22px rgba(0, 0, 0, 0.28), inset 0 1px 0 rgba(255, 255, 255, 0.7);
  transform: rotate(-5deg);
}
.home-logo-copy {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 7px;
}
.home-logo-cn {
  font-size: clamp(34px, 7vh, 58px);
  letter-spacing: clamp(3px, 0.8vw, 8px);
  line-height: 0.9;
}
.home-logo-sub {
  margin-top: 0;
  color: rgba(255, 255, 255, 0.5);
  font-size: clamp(9px, 1.6vh, 12px);
  letter-spacing: clamp(3px, 0.7vw, 7px);
}
.home-tagline {
  margin: 0 0 0 clamp(56px, 8vh, 82px);
  color: rgba(255, 255, 255, 0.78);
  font-size: clamp(13px, 2.2vh, 17px);
  font-weight: 600;
  letter-spacing: 2px;
}
.home-mode-layout {
  display: grid;
  grid-template-columns: minmax(0, 1.12fr) minmax(0, 0.88fr);
  gap: clamp(10px, 1.5vw, 16px);
  width: 100%;
}
.home-mode {
  min-width: 0;
  margin: 0;
  border: 1px solid rgba(255, 255, 255, 0.16);
  border-radius: 20px;
  color: #fff;
  text-align: left;
  font-family: inherit;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: transform 0.16s ease, border-color 0.16s ease, filter 0.16s ease, box-shadow 0.16s ease;
}
.home-mode:hover:not(:disabled) {
  filter: brightness(1.08);
  border-color: rgba(255, 225, 143, 0.72);
  transform: translateY(-2px);
}
.home-mode:active:not(:disabled) {
  transform: translateY(1px) scale(0.99);
}
.home-mode-feature {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  min-height: clamp(190px, 28vh, 258px);
  padding: clamp(18px, 3vh, 28px);
  border-color: rgba(255, 232, 164, 0.72);
  background: linear-gradient(145deg, #f6c85f 0%, #e89b2e 52%, #b7621f 100%);
  box-shadow: 0 14px 30px rgba(0, 0, 0, 0.28), inset 0 1px 0 rgba(255, 255, 255, 0.7);
}
.hm-feature-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.hm-badge {
  padding: 5px 9px;
  border: 1px solid rgba(98, 44, 8, 0.22);
  border-radius: 999px;
  background: rgba(255, 247, 214, 0.26);
  color: #6e3608;
  font-size: clamp(9px, 1.6vh, 12px);
  font-weight: 800;
  letter-spacing: 1px;
}
.home-mode .hm-ico {
  line-height: 1;
  text-shadow: none;
}
.home-mode-feature .hm-ico {
  color: #6a3205;
  font-size: clamp(30px, 6vh, 48px);
}
.home-mode .hm-txt {
  display: flex;
  flex-direction: column;
  gap: 7px;
}
.home-mode .hm-name {
  color: #fff;
  font-size: clamp(20px, 3.5vh, 29px);
  font-weight: 900;
  letter-spacing: 2px;
  white-space: nowrap;
}
.home-mode .hm-desc {
  color: rgba(255, 255, 255, 0.78);
  font-size: clamp(11px, 1.9vh, 14px);
  font-weight: 600;
  white-space: normal;
}
.home-mode-feature .hm-name {
  text-shadow: 0 1px 2px rgba(110, 54, 8, 0.38);
}
.home-mode-feature .hm-desc {
  color: #6e3608;
}
.hm-cta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  color: #6e3608;
  font-size: clamp(12px, 2vh, 15px);
  font-weight: 900;
  letter-spacing: 1px;
}
.hm-cta span {
  font-size: 25px;
  line-height: 0.7;
}
.home-mode-side {
  display: grid;
  grid-template-rows: 1fr 1fr;
  gap: clamp(10px, 1.5vw, 16px);
}
.home-mode-secondary {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 14px;
  padding: clamp(14px, 2.5vh, 22px) clamp(16px, 2.5vw, 26px);
  background: linear-gradient(145deg, rgba(33, 91, 66, 0.92), rgba(11, 49, 35, 0.94));
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.11), 0 10px 24px rgba(0, 0, 0, 0.18);
}
.home-mode-secondary .hm-ico {
  color: var(--gold);
  font-size: clamp(24px, 4.4vh, 36px);
}
.home-mode-secondary .hm-name {
  font-size: clamp(16px, 2.8vh, 22px);
}
.home-mode-secondary .hm-desc {
  color: rgba(255, 255, 255, 0.58);
  font-size: clamp(10px, 1.7vh, 13px);
}
.hm-arrow {
  color: rgba(255, 255, 255, 0.5);
  font-size: 30px;
  font-weight: 300;
}
.home-footnote {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(10px, 2vw, 22px);
  color: rgba(255, 255, 255, 0.48);
  font-size: clamp(10px, 1.7vh, 13px);
  letter-spacing: 1px;
}
.home-footnote b {
  color: var(--gold);
  font-size: 1.12em;
}
.home-footnote i {
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: rgba(240, 200, 92, 0.65);
}

@media (max-width: 620px) {
  .home-box {
    width: min(390px, calc(100% - 28px));
    padding-top: clamp(14px, 4vh, 28px);
  }
  .home-mode-layout {
    grid-template-columns: 1fr;
  }
  .home-mode-feature {
    min-height: clamp(154px, 25vh, 200px);
  }
  .home-mode-side {
    grid-template-rows: 1fr 1fr;
  }
  .home-mode-secondary {
    min-height: 72px;
  }
}
@media (max-height: 560px) {
  .home-box {
    gap: 10px;
    padding-top: 10px;
    padding-bottom: 8px;
  }
  .home-intro {
    gap: 5px;
  }
  .home-tagline {
    display: none;
  }
  .home-mode-feature {
    min-height: 145px;
    padding: 15px 18px;
  }
  .home-mode-secondary {
    min-height: 64px;
    padding: 10px 14px;
  }
}

/* ══════════════════════════════════════════════════════════════════════════
 * 房间界面 · 牌局大厅视觉重构
 *
 * 只改视觉层，不改 DOM id / 事件 / 房间协议：
 *   · 未入房：新建房间与加入房间并列，像一个真正的大厅入口；
 *   · 已入房：房号信息、队伍、座位、操作分成四个清晰层级；
 *   · 座位卡改为横向信息卡，减少大块空白，更适合手机横屏；
 *   · 所有规则都带 #roomLayer 前缀，覆盖前面的历史补丁。
 * ══════════════════════════════════════════════════════════════════════════ */

#roomLayer {
  --room-panel: rgba(8, 42, 29, 0.82);
  --room-panel-soft: rgba(255, 255, 255, 0.065);
  --room-line: rgba(255, 255, 255, 0.14);
  --room-muted: rgba(234, 246, 239, 0.62);
  background:
    radial-gradient(circle at 16% 12%, rgba(69, 153, 102, 0.3), transparent 34%),
    radial-gradient(circle at 86% 86%, rgba(240, 200, 92, 0.08), transparent 30%),
    linear-gradient(135deg, #0d4a32 0%, #082d20 58%, #061f16 100%);
}

#roomLayer::before,
#roomLayer::after {
  position: absolute;
  z-index: 0;
  display: block;
  border: 1px solid rgba(240, 200, 92, 0.11);
  border-radius: 50%;
  content: '';
  pointer-events: none;
}

#roomLayer::before {
  width: clamp(220px, 38vw, 520px);
  height: clamp(220px, 38vw, 520px);
  top: -22%;
  right: -9%;
  box-shadow: 0 0 0 24px rgba(240, 200, 92, 0.025), 0 0 0 48px rgba(240, 200, 92, 0.018);
}

#roomLayer::after {
  width: clamp(140px, 24vw, 300px);
  height: clamp(140px, 24vw, 300px);
  bottom: -18%;
  left: -7%;
}

#roomLayer .room-box {
  position: relative;
  z-index: 1;
  align-items: stretch;
  gap: clamp(10px, 2vh, 20px);
  width: min(1080px, 100%);
  padding: clamp(18px, 4vh, 44px) clamp(18px, 5vw, 68px);
}

#roomLayer .room-head {
  align-items: center;
  justify-content: space-between;
  width: min(860px, 100%);
  margin-inline: auto;
  padding-bottom: clamp(10px, 1.8vh, 18px);
  border-bottom: 1px solid rgba(255, 255, 255, 0.12);
}

#roomLayer .room-head .room-title {
  position: relative;
  color: #ffe49a;
  font-size: clamp(22px, 4.6vh, 36px);
  letter-spacing: 0.08em;
  line-height: 1;
}

#roomLayer .room-head .room-title::after {
  display: block;
  margin-top: 7px;
  color: rgba(234, 246, 239, 0.52);
  content: '邀请好友，一起开局';
  font-size: clamp(10px, 1.6vh, 13px);
  font-weight: 600;
  letter-spacing: 0.12em;
}

#roomLayer #roomBack {
  order: -1;
  min-width: 76px;
  min-height: 36px;
  padding: 7px 14px;
  border: 1px solid rgba(255, 255, 255, 0.2);
  border-radius: 12px;
  background: rgba(0, 0, 0, 0.16);
  color: rgba(255, 255, 255, 0.78);
  font-size: 14px;
  font-weight: 800;
}

#roomLayer #roomBack:hover {
  border-color: rgba(255, 228, 154, 0.65);
  background: rgba(240, 200, 92, 0.12);
  color: #ffe49a;
}

/* ── 未进房：两个入口卡片 ── */
#roomLayer .room-lobby {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  grid-template-areas:
    'create join'
    'tip tip';
  align-items: stretch;
  gap: clamp(12px, 2vw, 20px);
  width: min(860px, 100%);
  margin: clamp(4px, 1vh, 12px) auto 0;
  padding: 0;
  text-align: left;
}

#roomLayer .room-lobby::before {
  position: absolute;
  content: none;
}

#roomLayer .room-lobby .room-big,
#roomLayer .room-lobby .room-join {
  min-height: clamp(145px, 25vh, 225px);
  border: 1px solid var(--room-line);
  border-radius: 22px;
  box-shadow: 0 16px 34px rgba(0, 0, 0, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.11);
}

#roomLayer .room-lobby .room-big {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: flex-end;
  grid-area: create;
  width: 100%;
  min-width: 0;
  padding: clamp(18px, 3vh, 30px);
  border-color: rgba(255, 229, 154, 0.82);
  border-radius: 22px;
  background:
    radial-gradient(circle at 86% 18%, rgba(255, 247, 204, 0.3), transparent 28%),
    linear-gradient(145deg, #e5a33e 0%, #bd6b25 55%, #83401f 100%);
  color: #fff8e3;
  font-size: clamp(20px, 3.8vh, 30px);
  letter-spacing: 0.08em;
  text-align: left;
  text-shadow: 0 2px 3px rgba(88, 42, 11, 0.4);
}

#roomLayer .room-lobby .room-big::before {
  align-self: flex-end;
  width: clamp(42px, 7vh, 62px);
  height: clamp(42px, 7vh, 62px);
  margin-bottom: auto;
  border: 1px solid rgba(255, 247, 204, 0.45);
  border-radius: 17px;
  background: rgba(255, 247, 204, 0.17);
  color: #fff0b7;
  content: '♣';
  font-size: clamp(28px, 5vh, 42px);
  line-height: clamp(42px, 7vh, 62px);
  text-align: center;
  text-shadow: none;
}

#roomLayer .room-lobby .room-big::after {
  margin-top: 7px;
  color: rgba(255, 248, 227, 0.72);
  content: '创建一桌新的掼蛋牌局';
  font-size: clamp(11px, 1.8vh, 14px);
  font-weight: 600;
  letter-spacing: 0.03em;
  text-shadow: none;
}

#roomLayer .room-lobby .room-big:disabled {
  filter: saturate(0.65);
}

#roomLayer .room-lobby .room-join {
  display: grid;
  grid-area: join;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-rows: auto 1fr;
  align-items: center;
  gap: 16px 10px;
  width: 100%;
  padding: clamp(18px, 3vh, 30px);
  border-color: rgba(255, 255, 255, 0.16);
  background: linear-gradient(145deg, rgba(38, 91, 69, 0.76), rgba(10, 39, 29, 0.9));
}

#roomLayer .room-lobby .room-join::before {
  grid-column: 1 / -1;
  color: #f1f7ef;
  content: '加入已有房间';
  font-size: clamp(18px, 3.2vh, 26px);
  font-weight: 900;
  letter-spacing: 0.04em;
}

#roomLayer .room-lobby .room-join::after {
  position: absolute;
  content: none;
}

#roomLayer .room-lobby .room-code-inp {
  width: 100%;
  min-width: 0;
  padding: 12px 10px;
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: 13px;
  background: rgba(0, 0, 0, 0.24);
  color: #fff4ce;
  font-size: clamp(18px, 3.4vh, 28px);
  letter-spacing: 0.15em;
}

#roomLayer .room-lobby .room-join .btn {
  min-height: 48px;
  padding: 10px 20px;
  border-radius: 13px;
  background: rgba(240, 200, 92, 0.14);
  border-color: rgba(240, 200, 92, 0.56);
  color: #ffe9a8;
  font-size: 16px;
  font-weight: 900;
}

#roomLayer .room-lobby .room-join .btn:hover {
  background: rgba(240, 200, 92, 0.24);
}

#roomLayer .room-lobby .room-tip {
  grid-area: tip;
  padding: 2px 10px 0;
  color: rgba(234, 246, 239, 0.56);
  font-size: clamp(11px, 1.8vh, 14px);
  letter-spacing: 0.03em;
}

/* ── 已进房：房号卡 / 队伍 / 操作栏 ── */
#roomLayer .room-in {
  align-items: stretch;
  gap: clamp(10px, 1.8vh, 18px);
  width: min(980px, 100%);
  margin-inline: auto;
}

#roomLayer .room-info {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-rows: auto auto;
  align-items: center;
  gap: 4px clamp(14px, 2vw, 28px);
  width: 100%;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
  box-shadow: none;
}

#roomLayer .room-info-main {
  justify-content: flex-start;
  width: auto;
  min-width: 0;
  gap: 10px;
}

#roomLayer .room-code-cap {
  color: rgba(255, 233, 168, 0.68);
  font-size: 13px;
  letter-spacing: 0.12em;
}

#roomLayer .room-code-text {
  font-size: clamp(30px, 7vh, 52px);
  letter-spacing: 0.12em;
  line-height: 0.95;
}

#roomLayer .room-info-main .btn {
  min-height: 38px;
  padding: 8px 15px;
  border: 1px solid rgba(255, 233, 168, 0.5);
  border-radius: 11px;
  background: rgba(240, 200, 92, 0.16);
  color: #ffe9a8;
  font-size: 13px;
  font-weight: 900;
}

#roomLayer .room-info .room-hint {
  grid-column: 2;
  grid-row: 1 / span 2;
  max-width: 240px;
  color: var(--room-muted);
  font-size: clamp(11px, 1.8vh, 14px);
  line-height: 1.45;
  text-align: right;
}

#roomLayer .room-info .nudge {
  right: 18px;
  top: auto;
  bottom: 14px;
}

#roomLayer .room-seats {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(12px, 2vw, 22px);
  width: 100%;
  max-width: none;
  margin: 0;
}

#roomLayer .room-team {
  gap: 10px;
  min-width: 0;
  padding: clamp(12px, 2vh, 18px);
  border: 1px solid var(--room-line);
  border-radius: 20px;
  background: linear-gradient(145deg, rgba(255, 255, 255, 0.075), rgba(0, 0, 0, 0.16));
  box-shadow: 0 10px 25px rgba(0, 0, 0, 0.14);
}

#roomLayer .room-team.mine {
  border-color: rgba(255, 224, 141, 0.72);
  background: linear-gradient(145deg, rgba(240, 200, 92, 0.17), rgba(69, 87, 43, 0.16));
  box-shadow: 0 12px 28px rgba(0, 0, 0, 0.18), inset 0 0 0 1px rgba(255, 224, 141, 0.11);
}

#roomLayer .room-team-head {
  min-height: 28px;
  padding: 0 2px 8px;
  border-bottom-color: rgba(255, 255, 255, 0.13);
  font-size: clamp(15px, 2.5vh, 20px);
}

#roomLayer .room-team-tag {
  padding: 3px 8px;
  background: rgba(240, 200, 92, 0.2);
  font-size: 11px;
}

#roomLayer .room-team-note {
  padding-left: 2px;
  color: rgba(255, 233, 168, 0.76);
  font-size: 12px;
}

#roomLayer .room-team-seats {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  align-items: stretch;
  gap: 8px;
}

#roomLayer .room-seat {
  display: grid;
  grid-template-columns: clamp(38px, 5.4vh, 54px) minmax(0, 1fr);
  grid-template-rows: 1fr 1fr;
  align-items: center;
  justify-items: start;
  width: 100%;
  height: clamp(78px, 14vh, 108px);
  min-height: 0;
  padding: 10px 9px;
  border: 1px solid rgba(255, 255, 255, 0.15);
  border-radius: 15px;
  background: rgba(3, 26, 18, 0.35);
  text-align: left;
  transition: transform 0.16s ease, border-color 0.16s ease, background 0.16s ease;
}

#roomLayer .room-seat.can-pick:hover {
  border-color: rgba(240, 200, 92, 0.72);
  background: rgba(240, 200, 92, 0.1);
  transform: translateY(-2px);
}

#roomLayer .room-seat .rs-avatar {
  grid-column: 1;
  grid-row: 1 / span 2;
  width: clamp(38px, 5.4vh, 54px);
  height: clamp(38px, 5.4vh, 54px);
  border-radius: 13px;
  border-color: rgba(255, 255, 255, 0.25);
  font-size: 16px;
}

#roomLayer .room-seat .rs-pos {
  position: static;
  grid-column: 2;
  grid-row: 1;
  align-self: end;
  padding: 0;
  background: transparent;
  color: rgba(234, 246, 239, 0.46);
  font-size: 11px;
}

#roomLayer .room-seat .rs-name {
  grid-column: 2;
  grid-row: 2;
  align-self: start;
  min-width: 0;
  max-width: 100%;
  color: #f5fbf7;
  font-size: clamp(13px, 2.1vh, 17px);
  line-height: 1.15;
}

#roomLayer .room-seat .rs-tags {
  right: 8px;
  bottom: 7px;
  max-width: calc(100% - 17px);
}

#roomLayer .room-seat .rs-tag {
  padding: 2px 6px;
  font-size: 10px;
}

#roomLayer .room-seat.self {
  border-color: rgba(255, 224, 141, 0.96);
  background: linear-gradient(145deg, rgba(240, 200, 92, 0.21), rgba(75, 70, 29, 0.23));
  box-shadow: 0 0 0 2px rgba(240, 200, 92, 0.09), 0 8px 18px rgba(0, 0, 0, 0.16);
}

#roomLayer .room-seat.empty {
  border-color: rgba(255, 255, 255, 0.14);
  background: rgba(0, 0, 0, 0.14);
}

#roomLayer .room-actions {
  display: flex;
  justify-content: center;
  gap: 10px;
  width: 100%;
  margin-top: 0;
  padding: 2px 0 0;
}

#roomLayer .room-actions .btn {
  min-width: 128px;
  min-height: 44px;
  padding: 9px 20px;
  border-radius: 13px;
  font-size: 14px;
  font-weight: 900;
}

#roomLayer #roomStartBtn {
  min-width: 168px;
}

#roomLayer #roomLeaveBtn {
  border-color: rgba(255, 255, 255, 0.2);
  background: rgba(255, 255, 255, 0.06);
  color: rgba(255, 255, 255, 0.76);
  box-shadow: none;
}

#roomLayer #roomReadyBtn.on {
  border-color: rgba(75, 218, 144, 0.76);
  background: linear-gradient(180deg, #2aaf70, #16734a);
}

/* 换位请求仍保持原有交互，只把弹层视觉统一到新房间卡片。 */
#roomLayer .room-swap {
  background: rgba(2, 16, 10, 0.7);
  backdrop-filter: blur(7px);
}

#roomLayer .room-swap-card {
  width: min(430px, 92%);
  padding: 24px;
  border: 1px solid rgba(255, 224, 141, 0.72);
  border-radius: 22px;
  background: linear-gradient(145deg, #194d37, #09261a);
  box-shadow: 0 22px 55px rgba(0, 0, 0, 0.42), inset 0 1px 0 rgba(255, 255, 255, 0.1);
}

@media (max-width: 720px) {
  #roomLayer .room-box {
    padding-inline: 14px;
  }

  #roomLayer .room-lobby {
    grid-template-columns: 1fr;
    grid-template-areas: 'create' 'join' 'tip';
  }

  #roomLayer .room-lobby .room-big,
  #roomLayer .room-lobby .room-join {
    min-height: 128px;
  }

  #roomLayer .room-seats {
    gap: 8px;
  }

  #roomLayer .room-team {
    padding: 9px;
  }

  #roomLayer .room-team-seats {
    grid-template-columns: 1fr;
  }

  #roomLayer .room-seat {
    height: 76px;
  }
}

@media (max-height: 560px) {
  #roomLayer .room-box {
    gap: 9px;
    padding-top: 10px;
    padding-bottom: 8px;
  }

  #roomLayer .room-head {
    padding-bottom: 8px;
  }

  #roomLayer .room-head .room-title::after {
    display: none;
  }

  #roomLayer .room-lobby {
    gap: 10px;
  }

  #roomLayer .room-lobby .room-big,
  #roomLayer .room-lobby .room-join {
    min-height: 108px;
    padding: 14px;
  }

  #roomLayer .room-info {
    padding: 8px 14px;
  }

  #roomLayer .room-code-text {
    font-size: 30px;
  }

  #roomLayer .room-team {
    gap: 6px;
    padding: 8px;
  }

  #roomLayer .room-team-head {
    min-height: 23px;
    padding-bottom: 4px;
  }

  #roomLayer .room-team-note {
    font-size: 11px;
  }

  #roomLayer .room-seat {
    height: 68px;
    padding: 7px;
  }

  #roomLayer .room-seat .rs-avatar {
    width: 34px;
    height: 34px;
  }

#roomLayer .room-actions .btn {
    min-height: 36px;
    padding-block: 6px;
  }
}

/* ══════════ 统一全屏入口：视频播放器式四角图标 ══════════ */
.fullscreen-trigger {
  position: relative;
  z-index: 3;
  display: inline-grid;
  place-items: center;
  flex: 0 0 auto;
  width: 32px;
  height: 32px;
  margin: 0;
  padding: 0 !important;
  border: 0 !important;
  border-radius: 0 !important;
  background: transparent !important;
  color: rgba(238, 245, 241, 0.92);
  cursor: pointer;
  transition: background 0.14s ease, border-color 0.14s ease, color 0.14s ease, transform 0.06s ease;
}

.fullscreen-trigger svg {
  width: 15px;
  height: 15px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2.15;
  stroke-linecap: round;
  stroke-linejoin: round;
  pointer-events: none;
}

.fullscreen-trigger:hover:not(:disabled),
.fullscreen-trigger:focus-visible {
  color: var(--gold);
  outline: none;
}

.fullscreen-trigger:active:not(:disabled) {
  transform: translateY(1px) scale(0.96);
}

.fullscreen-trigger.on {
  color: var(--gold);
}

.fullscreen-trigger .sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

#homeLayer > .fullscreen-trigger,
#roomLayer > .fullscreen-trigger {
  position: absolute;
  top: max(16px, env(safe-area-inset-top, 0px));
  right: max(var(--edge-gap, 40px), env(safe-area-inset-right, 0px));
}

#roomLayer > .fullscreen-trigger {
  top: max(16px, env(safe-area-inset-top, 0px));
  right: calc(max(var(--edge-gap, 40px), env(safe-area-inset-right, 0px)) + 32px);
}

#app .table-topbar #btnFullscreen {
  width: 32px;
  height: 32px;
  /* .table-topbar 整层为了让牌面可点而设 pointer-events:none；
     游戏里的全屏按钮不是 .btn，必须显式恢复点击，否则主页能点、牌桌不能点。 */
  pointer-events: auto;
}

#app .table-topbar #btnSettings {
  /* 与全屏按钮一样，必须覆盖顶栏容器的 pointer-events: none。 */
  pointer-events: auto;
}

/* 竖屏手持时 #app 已经旋转，安全区方向也要跟着旋转后的坐标系取值。 */
@media (orientation: portrait) {
  #homeLayer > .fullscreen-trigger,
  #roomLayer > .fullscreen-trigger {
    top: max(16px, env(safe-area-inset-top, 0px));
    right: calc(max(var(--edge-gap, 40px), env(safe-area-inset-bottom, 0px)) + 32px);
  }
}

/* 主页右上角并排控件：fullscreen 的 right 值比设置多 40px，因此全屏固定在左侧。 */
#homeLayer > .fullscreen-trigger {
  right: calc(max(var(--edge-gap, 40px), env(safe-area-inset-right, 0px)) + 32px);
}

@media (orientation: portrait) {
  #homeLayer > .fullscreen-trigger {
    right: calc(max(var(--edge-gap, 40px), env(safe-area-inset-bottom, 0px)) + 32px);
  }
}

/* ══════════ 最终手机横屏覆盖：安全边距、房间方卡与操作顺序 ══════════ */
html {
  --page-edge-left: max(var(--edge-gap), env(safe-area-inset-left, 0px));
  --page-edge-right: max(var(--edge-gap), env(safe-area-inset-right, 0px));
}

#homeLayer .home-box {
  width: min(880px, calc(100% - var(--page-edge-left) - var(--page-edge-right)));
}

#roomLayer .room-box {
  width: 100%;
  padding-inline: var(--page-edge-left) var(--page-edge-right);
}

#roomLayer .room-head {
  position: relative;
  justify-content: center;
}

#roomLayer #roomBack {
  position: absolute;
  left: 0;
}

#roomLayer .room-team-seats {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

#roomLayer .room-seat {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  grid-template-columns: none;
  grid-template-rows: none;
  width: 100%;
  height: auto;
  aspect-ratio: 1 / 1;
  min-width: 0;
  padding: clamp(9px, 1.4vh, 14px);
  gap: clamp(4px, 0.8vh, 7px);
  text-align: center;
}

#roomLayer .room-seat .rs-avatar {
  flex: 0 0 auto;
  width: clamp(40px, 7vh, 62px);
  height: clamp(40px, 7vh, 62px);
  margin-top: clamp(7px, 1.2vh, 12px);
  border-radius: 16px;
}

#roomLayer .room-seat .rs-pos {
  position: absolute;
  top: 7px;
  left: 9px;
  grid-column: auto;
  grid-row: auto;
  align-self: auto;
  padding: 2px 5px;
  border-radius: 7px;
  background: rgba(0, 0, 0, 0.18);
  font-size: 11px;
}

#roomLayer .room-seat .rs-name {
  flex: 0 0 auto;
  width: 100%;
  grid-column: auto;
  grid-row: auto;
  align-self: auto;
  text-align: center;
}

#roomLayer .room-seat .rs-tags {
  right: 7px;
  bottom: 7px;
  max-width: calc(100% - 14px);
}

#roomLayer .room-seat .rs-flag {
  top: 4px;
  left: 50%;
  transform: translateX(-50%);
}

#roomLayer .room-actions {
  justify-content: flex-end;
  gap: 10px;
}

#roomLayer #roomLeaveBtn {
  order: 1;
  margin: 0;
}

#roomLayer #roomNudgeBtn {
  position: static;
  inset: auto;
  order: 2;
  min-height: 38px;
  margin: 0;
  padding: 8px 15px;
  border: 1px solid rgba(255, 217, 138, 0.7);
  border-radius: 12px;
  background: linear-gradient(180deg, rgba(255, 217, 138, 0.96), rgba(240, 140, 0, 0.96));
  box-shadow: 0 3px 10px rgba(0, 0, 0, 0.3);
  color: #43290a;
  font-size: clamp(12px, 2.3vh, 15px);
  font-weight: 800;
}

#roomLayer #roomStartBtn {
  order: 3;
  margin: 0;
}

#roomLayer #roomReadyBtn {
  order: 3;
  margin: 0;
}

#roomLayer .room-seat .rs-avatar.has-user-menu {
  cursor: pointer;
}

#roomLayer .room-seat .rs-avatar.has-user-menu:focus-visible {
  outline: 2px solid #ffd98a;
  outline-offset: 3px;
}

#roomLayer .room-user-menu {
  position: absolute;
  z-index: 20;
  inset: 0;
  display: grid;
  place-items: center;
  padding: 18px;
  background: rgba(2, 16, 10, 0.72);
  backdrop-filter: blur(8px);
}

#roomLayer .room-user-menu.hidden {
  display: none;
}

#roomLayer .room-user-card {
  width: min(360px, 94vw);
  padding: 20px;
  border: 1px solid rgba(255, 224, 141, 0.62);
  border-radius: 20px;
  background: linear-gradient(145deg, #194d37, #09261a);
  box-shadow: 0 22px 55px rgba(0, 0, 0, 0.42), inset 0 1px 0 rgba(255, 255, 255, 0.1);
  color: #fff7dc;
}

#roomLayer .room-user-heading {
  margin-bottom: 14px;
  color: #ffdb83;
  font-size: 17px;
  font-weight: 800;
  text-align: center;
}

#roomLayer .room-user-summary,
#roomLayer .room-user-profile-summary {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  margin-bottom: 14px;
}

#roomLayer .room-user-summary > div {
  display: grid;
  gap: 4px;
}

#roomLayer .room-user-summary strong,
#roomLayer .room-user-profile-summary strong {
  font-size: 18px;
}

#roomLayer .room-user-summary span:not(.room-user-avatar) {
  color: rgba(255, 255, 255, 0.7);
  font-size: 13px;
}

#roomLayer .room-user-avatar {
  position: relative;
  display: grid;
  width: 48px;
  height: 48px;
  flex: none;
  overflow: hidden;
  place-items: center;
  border: 2px solid rgba(255, 217, 138, 0.8);
  border-radius: 14px;
  background: #23664b;
  color: #ffe8a5;
  font-size: 22px;
  font-weight: 800;
}

#roomLayer .room-user-avatar.large {
  width: 68px;
  height: 68px;
}

#roomLayer .room-user-avatar img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

#roomLayer .room-user-option,
#roomLayer .room-user-close {
  display: block;
  width: 100%;
  min-height: 42px;
  margin-top: 9px;
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.08);
  color: #fff7dc;
  font: inherit;
  font-weight: 700;
  cursor: pointer;
}

#roomLayer .room-user-option:active,
#roomLayer .room-user-close:active {
  background: rgba(255, 255, 255, 0.16);
}

#roomLayer .room-user-option.danger {
  border-color: rgba(255, 116, 102, 0.36);
  color: #ffb1a8;
}

#roomLayer .room-user-close {
  color: rgba(255, 255, 255, 0.7);
  font-weight: 600;
}

#roomLayer .room-user-profile-details {
  display: grid;
  gap: 8px;
  margin: 8px 0 16px;
  color: rgba(255, 255, 255, 0.78);
  line-height: 1.5;
  text-align: center;
  white-space: pre-line;
}
