@font-face {
	font-family: 'Zpix';
	src: url('/syncxiangqi/fonts/zpix.woff2') format('woff2'),
	     url('/syncxiangqi/fonts/zpix.ttf') format('truetype');
	font-display: swap;
}

/*
 * ============ 主题色板：暖木 + 墨（向中国象棋的实物质感靠拢） ============
 *
 * 原配色的问题：棋子是「红 + 蓝」、棋盘是「深灰」、主按钮是「绿底黑字」——
 * 四个色相互不相干，各自都很显眼但凑不成一个体系。
 * 现在统一到一套暖色系：木（棋盘/卡片）+ 琥珀（强调/主按钮）+ 朱红（红方）+ 墨黑（黑方）。
 * 顺带修正了语义：中国象棋的两方本来就是「红方 / 黑方」，不是红/蓝。
 */
:root {
	--bg:           #100d0b;                 /* 页面底：暖黑（比纯黑有温度） */
	--panel:        rgba(42, 31, 22, 0.9);   /* 卡片 / 信息条：暖褐半透 */
	--panel-solid:  #1b1510;
	--line:         rgba(255, 231, 196, 0.24); /* 描边：暖白（弱） */
	--line-strong:  rgba(255, 231, 196, 0.48); /* 描边：暖白（强） */
	--text:         #f4e6d2;                 /* 正文：暖白（不刺眼） */
	--text-dim:     rgba(244, 230, 210, 0.66);
	--accent:       #e9bd76;                 /* 主强调：琥珀 / 黄铜 */
	--accent-deep:  #b98a4c;
	--vermilion:    #c8412f;                 /* 红方：朱红 */
	--vermilion-dk: #8d2317;
	--ink:          #2a2a31;                 /* 黑方：墨黑 */
	--ink-dk:       #141419;
	--wood:         #c9a475;                 /* 棋盘：木色 */
	--wood-dk:      #7a5730;
	--wood-line:    #6b4a26;                 /* 棋盘格线：墨褐 */
	--ok:           #a8c98f;                 /* 在线 / 正向：苔绿（低饱和） */
	--warn:         #e0896f;                 /* 警示：暖红 */

	/*
	 * ============ 像素化设计层 ============
	 *
	 * 字体是 Zpix 点阵（真·像素），但控件原来是「平滑渐变 + 大圆角 + 模糊阴影」——
	 * 一边是马赛克字、一边是矢量质感，风格打架。这里把控件也拉到同一档「分辨率」：
	 *
	 *   ① **圆角归零 / 阶梯化**：矩形控件一律 `border-radius: 0`，角是直角（无抗锯齿）；
	 *      真正需要「圆形」的只有棋子、头像、状态点这类东西。
	 *   ② **渐变阶梯化**：用**硬停靠**把渐变切成 4~5 条色带（`30% 44%` 这种写法），
	 *      肉眼能数出色带 → 就是像素画里的「逐阶上色」。
	 *   ③ **阴影去模糊**：`box-shadow` 的模糊半径一律 0（偏移 + 同色块），
	 *      替代原来的 `0 6px 18px rgba(0,0,0,.6)` 柔和投影。
	 *   ④ **高光/暗面用 0 模糊内阴影**造出「斜面」感（Retro 面板的经典做法）。
	 *
	 * ⚠ 改这里要连着看：控件尺寸都随 `--uu` 缩放，但**像素化是固定 px**（1/2/3px）——
	 *   这是故意的：像素网格就该是屏幕上的绝对尺度，不跟着舞台缩放。
	 */
	--brass:        linear-gradient(180deg,
	                  #f4d5a4 0 18%, #e8c283 18% 40%, #d7a95c 40% 62%, #c6923f 62% 84%, #b07c33 84% 100%);
	--brass-flat:   #d7a95c;                 /* 阶梯渐变的中间色（用作边框 / 基线） */
	--hair:         rgba(255, 248, 232, 0.85); /* 斜面高光（上方 / 左侧） */
	--hair-dk:      rgba(58, 32, 8, 0.55);     /* 斜面暗面（下方 / 右侧） */
	--px-drop-sm:   0 2px 0 rgba(0, 0, 0, 0.45);
	--px-drop-md:   0 3px 0 rgba(0, 0, 0, 0.5);
	--px-drop-lg:   0 5px 0 rgba(0, 0, 0, 0.55);
	--px-bevel:     inset 0 2px 0 var(--hair), inset 0 -2px 0 var(--hair-dk);

}

* {
	box-sizing: border-box;
	font-family: 'Zpix', monospace, sans-serif;
	-webkit-font-smoothing: none;
	-moz-osx-font-smoothing: grayscale;
	text-rendering: pixelated;
}

html, body {
	margin: 0;
	width: 100%;
	height: 100%;
	overflow: hidden;
	background: var(--bg);
}

/* ================= 舞台外壳 (登录后由 vDOM 生成) ================= */

.viewport {
	width: 100vw;
	height: 100vh;
	display: grid;
	place-items: center;
}

.stage {
	position: relative;
	aspect-ratio: 9 / 16;
	width: min(100vw, calc(100vh * 9 / 16));
	height: min(100vh, calc(100vw * 16 / 9));
	/*
	 * 舞台就是「屏幕」：抽屉之类的滑入元素要能被它裁掉，
	 * 否则在宽屏上关闭态的抽屉会飘在舞台右侧的黑边上。
	 */
	overflow: hidden;
	/* 1 个 --uu = 舞台短边的 1%，用于让卡片等控件随布局等比缩放 */
	--uu: min(1vw, 0.5625vh);
	/* 顶部信息条卡片高度 (昵称卡 / 房间号卡 / 内嵌的音频开关共用)
	 * ⚠ 这个「+5px」= 2×2px 边框 + 1px 余量（像素化后边框由 1px 加粗到 2px，
	 *   所以由原来的 +3px 跟着调大 —— 两张卡的实际高度必须保持一致，否则会错位） */
	--top-card-h: calc(var(--uu) * 7.6 + 5px);
}

/* ================= 登录面板 (登录前；登录后整个 <body> 交由 vDOM 接管) ================= */

#divLogin {
	position: fixed;
	inset: 0;
	display: flex;
	justify-content: center;
	align-items: center;
	padding: 16px;
	background: var(--bg);
	z-index: 10;
}

/* 标题 + 卡片作为一个整体在视口中居中（两者间距由 gap 控制） */
.login-stack {
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 26px;
}

/*
 * 五个 3D 艺术字「一起下象棋」。
 *
 * 立体感三件套：
 *   ① 外层 `.login-title3d` 给 perspective（透视）—— 挂在字身上不会作用于它自己；
 *   ② 每个字 rotateX/rotateY 摆出角度，并保留 preserve-3d；
 *   ③ text-shadow 用一列**递增偏移 + 递增加深**的颜色把字「挤出」厚度。
 *      像素字体（Zpix）笔画齐整，挤出后尤其像实体块，比模糊阴影好看得多。
 * 每个字错开一点动画相位 → 缓慢的波浪浮动，与等待室那枚自转棋子同一套“活的”语感。
 */
.login-title3d {
	display: flex;
	gap: 1px;
	perspective: 460px;
	transform-style: preserve-3d;
	user-select: none;
}

.login-title3d span {
	display: inline-block;
	font-size: clamp(26px, 9vw, 40px);
	font-weight: 700;
	line-height: 1;
	color: #f2cd92;
	/* 同 .xq-piece：抵消 Zpix 字形在前进盒内固有偏左，否则字距看着不匀 */
	--glyph-shift-x: 0.0925em;
	padding-left: calc(var(--glyph-shift-x) * 2);
	transform-style: preserve-3d;
	transform: rotateX(16deg) rotateY(-14deg);
	text-shadow:
		1px 1px 0 #d3a355,
		2px 2px 0 #c69447,
		3px 3px 0 #b7853d,
		4px 4px 0 #a57634,
		5px 5px 0 #92672c,
		6px 6px 0 #7e5825,
		7px 7px 0 #6a4a1f,
		11px 11px 0 rgba(0, 0, 0, 0.45);
	animation: xq-title3d 5.4s ease-in-out infinite;
}

.login-title3d span:nth-child(2) { animation-delay: -0.12s; }
.login-title3d span:nth-child(3) { animation-delay: -0.24s; }
.login-title3d span:nth-child(4) { animation-delay: -0.36s; }
.login-title3d span:nth-child(5) { animation-delay: -0.48s; }

@keyframes xq-title3d {
	0%, 100% { transform: rotateX(16deg) rotateY(-14deg) translateZ(0); }
	50%      { transform: rotateX(11deg) rotateY(-18deg) translateZ(7px); }
}

/* 登录失败提示：暖红小字。空内容时 :empty 直接不渲染，免得卡片多出一段间距 */
.login-error {
	width: 100%;
	font-size: 12px;
	line-height: 1.5;
	text-align: center;
	color: var(--warn);
	text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.7);
}

.login-error:empty { display: none; }

.login-card {
	width: min(320px, 100%);
	padding: 22px 20px;
	border: 3px solid var(--line-strong);
	border-radius: 0;
	background: var(--panel-solid);
	box-shadow: var(--px-drop-lg), var(--px-bevel);
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 14px;
}

#divLogin input {
	width: 100%;
	height: 40px;
	padding: 0 12px;
	font-size: 14px;
	color: var(--text);
	background: #2a2118;
	border: 3px solid var(--line);
	border-radius: 0;
	box-shadow: inset 0 3px 0 rgba(0, 0, 0, 0.35); /* 凹入：上方暗、无模糊 */
	outline: none;
	transition: border-color 0.2s, background 0.2s;
}

#divLogin input::placeholder { color: rgba(244, 230, 210, 0.4); }

#divLogin input:focus {
	background: rgba(255, 231, 196, 0.11);
	border-color: var(--accent);
}

/*
 * 登录卡的两个按钮：创建房间 = 黄铜实心（主操作，永远可用）；「加入」= 描边（次），
 * 因为它是**有条件的** —— 随机加入要有公开房间、指定加入要填对四位房间号。
 * 「加入随机 / 指定房间」一个按钮两种行为：看上面那串房间号填没填（见客户端 readJoinGameId）。
 */
#btnCreateRoom,
#btnJoinRoom {
	width: 100%;
	height: 40px;
	padding: 0 16px;
	font-size: 14px;
	font-weight: 700;
	border-radius: 0;
	cursor: pointer;
	transition: filter 0.2s, background 0.2s, border-color 0.2s;
}

/* 次要入口（加入房间）：描边式 */
#btnJoinRoom {
	color: var(--text);
	background: #2a2118;
	border: 3px solid var(--line-strong);
	box-shadow: var(--px-drop-sm), var(--px-bevel);
}

#btnJoinRoom:hover {
	filter: brightness(1.2);
	border-color: var(--accent);
}

/* 主要入口（创建房间）：黄铜阶梯渐变实心 */
#btnCreateRoom {
	color: #3a2410;
	background: var(--brass);
	border: 3px solid rgba(255, 240, 214, 0.75);
	box-shadow: var(--px-drop-md), var(--px-bevel);
}

#btnCreateRoom:hover { filter: brightness(1.1); }

/* ================= 顶部信息条 (昵称卡 + 房间号卡) (vDOM 实体) ================= */

.top-bar {
	position: absolute;
	top: 14px;
	/* 左右都贴边：这样音频开关栏才能用 margin-left:auto 真正推到右上角 */
	left: 14px;
	right: 14px;
	display: flex;
	align-items: stretch;
	gap: 6px;
	z-index: 2;
}

/* ================= 信息卡 (昵称卡 / 房间号卡 / 房间名单卡) (vDOM 实体) ================= */

.info-card {
	padding: 6px 10px;
	border: 2px solid var(--line-strong);
	border-radius: 0;
	background: var(--panel-solid);
	color: var(--text);
	font-size: 11px;
	font-weight: 700;
	line-height: 1.5;
	box-shadow: var(--px-drop-md), var(--px-bevel);
	user-select: none;
}

/*
 * 昵称卡 / 房间号卡：字号与内边距随舞台短边 (--uu) 缩放。
 * 尺寸刻意克制 —— 这一行上方还挤着回合条 / 对手信息条 / 陈列栏 / 棋盘，
 * 顶部条每高 1px 都会往下压整条链；边框也用 1px（比名单卡细）。
 */
.player-info-card,
.room-no-card {
	font-size: calc(var(--uu, 2.86px) * 3.4);
	padding: calc(var(--uu, 2.86px) * 1.3) calc(var(--uu, 2.86px) * 2.4);
	border-width: 2px;
	border-radius: 0;
	line-height: 1.3;
	height: var(--top-card-h, 34px);
}

/* 玩家信息卡：常驻，顶部信息条内展示昵称 (过长省略) */
.player-info-card {
	flex: 0 1 auto;
	min-width: 0;
	max-width: 60%;
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
}

/* 房间号小卡片：常驻，位于昵称卡右侧 */
.room-no-card {
	flex: 0 0 auto;
	white-space: nowrap;
}

/* 房间名单卡：居中展示 (卡片本身不能设 overflow，避免裁掉其子元素) */
.room-info-card {
	position: absolute;
	top: 50%;
	left: 50%;
	transform: translate(-50%, -50%);
	width: min(280px, 84%);
	padding: 14px;
	font-size: 14px;
	line-height: 1.7;
	text-align: center;
	white-space: pre-line;
	z-index: 2;
}

/* ================= 房间名单 (滚动容器 + 成员条目) (vDOM 实体) ================= */

/*
 * 名单滚动容器：最多完整展示 6 名成员，超出则出现纵向滚动条。
 * 高度 = 6 × 条目 3.03em ≈ 18.2em (用 em 随卡片字号缩放)
 */
.roster-list {
	max-height: 18.2em;
	overflow-y: auto;
	scrollbar-width: thin;
	scrollbar-color: rgba(255, 255, 255, 0.3) transparent;
}

.roster-list::-webkit-scrollbar { width: 6px; }
.roster-list::-webkit-scrollbar-track { background: transparent; }
.roster-list::-webkit-scrollbar-thumb {
	background: rgba(255, 255, 255, 0.3);
	border-radius: 0;
}

/* 房间名单条目：半身剪影头像 + 昵称 */
.player-entry {
	display: flex;
	align-items: center;
	gap: 0.55em;
	margin-top: 0.55em;
	padding: 0.4em 0.7em;
	border-radius: 0;
	background: rgba(255, 255, 255, 0.08);
	box-shadow: inset 0 2px 0 rgba(255, 255, 255, 0.08), inset 0 -2px 0 rgba(0, 0, 0, 0.25);
	font-size: 0.92em;
	line-height: 1.2;
	text-align: left;
}

/* 半身剪影头像：圆形底 + 头部圆点 + 肩部半椭圆 (纯 CSS 矢量绘制，不依赖图片) */
.player-entry::before {
	content: "";
	flex: 0 0 auto;
	width: 1.9em;
	height: 1.9em;
	border-radius: 50%;
	background-color: rgba(255, 255, 255, 0.14);
	background-image:
		radial-gradient(circle at 50% 35%, currentColor 0 20%, transparent 21%),
		radial-gradient(ellipse 42% 32% at 50% 100%, currentColor 0 99%, transparent 100%);
	background-repeat: no-repeat;
	color: #cfe6ff;
}

/* 房主：仅保留右侧「房主」文字备注 */
.player-entry.is-owner::after {
	content: "房主";
	margin-left: auto;
	font-size: 0.8em;
	color: #ffd700;
}

/* 头像配色按条目轮换 */
.player-entry:nth-child(6n + 2)::before { color: #ffd479; }
.player-entry:nth-child(6n + 3)::before { color: #9be59b; }
.player-entry:nth-child(6n + 4)::before { color: #ff9fb0; }
.player-entry:nth-child(6n + 5)::before { color: #c9a7ff; }
.player-entry:nth-child(6n + 6)::before { color: #8ef0e0; }

/* ================= 等待室：可旋转的 3D 木质棋子 (vDOM 实体) ================= */

/*
 * 等待室的视觉主体：一枚纯 CSS 3D 的木质棋子（不依赖任何图片资源）。
 *
 * 结构（服务端下发，见 buildWaitingPieceNodes）：
 *   .waiting-piece                外层：定位 + perspective + 圆心锚点
 *     └ .waiting-piece-stage      可旋转层：客户端拖拽时写内联 rotateX/rotateY
 *         ├ .wp-face.wp-bottom    底面（法线朝下）
 *         ├ .wp-side ×24          侧面（每片 rotateY(i·15°) translateZ(r)，角度由服务端下发）
 *         └ .wp-face.wp-top       顶面（带「帅」字，压在侧面上沿之上）
 *
 * 分工：
 *   - **几何全在这里**（`--wp-d/r/h` 三个自定义属性），服务端只给每片侧面的角度，
 *     所以调大调小只改这三个值，vDOM 与摘要都不受影响；
 *   - 旋转是**纯客户端**交互，不进 vDOM（避免把一次拖拽变成一次网络往返）。
 *
 * ⚠ 三个必须点（漏了就不像 3D）：
 *   ① `.waiting-piece-stage` 必须 `transform-style: preserve-3d`，否则子元素被压扁回 2D；
 *   ② `perspective` 必须在外层 —— 挂在 stage 自己身上**不会**作用于它的子元素；
 *   ③ 侧面宽度要略大于弦长（= 2r·sin(π/24) ≈ 0.13d），留一点重叠以消除片间缝。
 */
.waiting-piece {
	position: absolute;
	top: 30%;
	left: 50%;
	transform: translate(-50%, -50%);
	width: calc(var(--uu, 2.86px) * 40);
	height: calc(var(--uu, 2.86px) * 40);
	perspective: calc(var(--uu, 2.86px) * 210);
	z-index: 2;
	cursor: grab;
	user-select: none;
	touch-action: none; /* 触屏拖动只旋转棋子，不滚页面 */
	/* 棋子几何：直径 / 半径 / 厚度 */
	--wp-d: calc(var(--uu, 2.86px) * 30);
	--wp-r: calc(var(--wp-d) / 2);
	--wp-h: calc(var(--uu, 2.86px) * 7);
}

.waiting-piece-stage {
	position: absolute;
	inset: 0;
	transform-style: preserve-3d;
	/* 初始姿态：略俯视 + 偏航，让顶面与侧面厚度都看得见（与客户端常量一致） */
	transform: rotateX(-18deg) rotateY(-24deg);
}

/*
 * ⚠ 这里**不能**写 `transition: transform`：空闲自转由 rAF 每帧写内联 transform，
 *   叠加 CSS 过渡会让每次写入都开一段新过渡 → 动画被拖成「永远追不上目标」的糊状。
 *   平滑完全由 JS 负责（松手后俯仰按指数收拢回摆动轨迹）。
 * 拖拽中只换光标，表示已接管。
 */
.waiting-piece-stage.is-dragging {
	cursor: grabbing;
}

.wp-face,
.wp-side {
	position: absolute;
}

/* 上下两面：圆形盘面，绕 X 轴放平到水平面 */
.wp-face {
	left: 50%;
	top: 50%;
	width: var(--wp-d);
	height: var(--wp-d);
	margin-left: calc(var(--wp-d) / -2);
	margin-top: calc(var(--wp-d) / -2);
	border-radius: 50%;
	display: flex;
	align-items: center;
	justify-content: center;
	line-height: 1;
}

/* 顶面：木纹 + 内圈凹槽（两道 inset 阴影模拟雕刻），中间是棋子字 —— 阶梯圆而非平滑渐变 */
.wp-top {
	transform: translateY(calc(var(--wp-h) / -2)) rotateX(90deg);
	background: radial-gradient(circle at 38% 30%,
		#f4dcbd 0 26%, #e0b98f 26% 52%, #c9915a 52% 78%, #a9723c 78% 100%);
	box-shadow:
		inset 0 0 0 calc(var(--wp-d) * 0.045) rgba(122, 74, 32, 0.5),
		inset 0 0 0 calc(var(--wp-d) * 0.075) rgba(255, 232, 200, 0.35),
		inset 0 0 0 calc(var(--wp-d) * 0.12) rgba(90, 50, 14, 0.45);
	font-size: calc(var(--wp-d) * 0.46);
	font-weight: 700;
	color: #8c2f14;
	text-shadow: 0 1px 0 rgba(255, 240, 220, 0.55);
	/*
	 * ⚠ 同 `.xq-piece`：Zpix 的汉字字形在**前进盒内固有偏左**（墨心偏左 ≈0.0925em），
	 *   而 flex 的 `justify-content: center` 居中的是前进盒 —— 不修就会看到「帅」偏左。
	 *   本项目的其它棋子/文字都有这一行，**这里当初漏了**（用户报「等待室 3D 棋子字符有点偏左」）。
	 *   只加左侧内边距 = 2×偏移量，使内容盒中心右移一个偏移量；用 em，随字号缩放。
	 */
	--glyph-shift-x: 0.0925em;
	padding-left: calc(var(--glyph-shift-x) * 2);
}

/* 底面：正常看不见，转到下方时避免“透空”，用深色收边 */
.wp-bottom {
	transform: translateY(calc(var(--wp-h) / 2)) rotateX(-90deg);
	background: radial-gradient(circle at 50% 50%, #6b3f1e 0 40%, #46260e 40% 74%, #2c1908 74% 100%);
}

/* 侧面单片：竖直窄条，绕 Y 轴排成一圈（角度与 translateZ 由服务端内联下发） */
.wp-side {
	left: 50%;
	top: 50%;
	width: calc(var(--wp-d) * 0.142); /* ≈ 弦长 0.13d，略宽以重叠消缝 */
	height: var(--wp-h);
	margin-left: calc(var(--wp-d) * -0.071);
	margin-top: calc(var(--wp-h) / -2);
	background: linear-gradient(to bottom, #c08a55 0 22%, #96603a 22% 48%, #6d3f1e 48% 76%, #4a2711 76% 100%);
}

/* ================= 等待室状态文案 (vDOM 实体) ================= */

/*
 * 「等待另一名玩家」/「等待房主开始游戏」—— 放在名单卡下方，带轻微呼吸闪烁。
 * 由服务端按「角色 + 人数」决定是否下发（房主满员时不下发，他手上有开始按钮）。
 */
.waiting-status {
	position: absolute;
	top: 66%;
	left: 50%;
	transform: translateX(-50%);
	width: 84%;
	text-align: center;
	font-size: calc(var(--uu, 2.86px) * 3.4);
	letter-spacing: 1px;
	color: var(--text-dim);
	text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.85);
	user-select: none;
	z-index: 2;
	animation: xq-waiting-pulse 2.4s steps(2, end) infinite;
}

@keyframes xq-waiting-pulse {
	0%, 100% { opacity: 0.6; }
	50%      { opacity: 1; }
}

/* ================= 游戏操作按钮 (房主「开始游戏」) (vDOM 实体) ================= */

/*
 * 挂在舞台底部的操作按钮。定位用 absolute + bottom 百分比，
 * 尺寸/字号随舞台短边 --uu 缩放，与卡片体系保持一致。
 * 仅在等待室满员 2 人且未开局时，由服务端下发给房主。
 */
.game-btn {
	position: absolute;
	left: 50%;
	bottom: 12%;
	transform: translateX(-50%);
	display: inline-flex;
	align-items: center;
	justify-content: center;
	min-width: 46%;
	padding: calc(var(--uu, 2.86px) * 3.2) calc(var(--uu, 2.86px) * 6);
	font-size: calc(var(--uu, 2.86px) * 5);
	font-weight: 700;
	letter-spacing: 2px;
	color: #3a2410;
	background: var(--brass);
	border: 3px solid rgba(255, 240, 214, 0.75);
	border-radius: 0;
	box-shadow: var(--px-drop-md), var(--px-bevel);
	cursor: pointer;
	user-select: none;
	z-index: 3;
	transition: filter 0.15s ease, transform 0.15s ease;
}

.game-btn:hover {
	filter: brightness(1.1);
}

.game-btn:active {
	transform: translateX(-50%) scale(0.96);
}

/* ================= 等待室：「公开房间」开关 (vDOM 实体) ================= */

/*
 * 仅房主、仅等待室可见：控制房间能否被登录卡的「随机加入 / 创建房间」命中。
 * 语义与音效 / 音乐开关一致 —— 文案直接描述**当前状态**：
 *   「公开房间」（is-on，黄铜高亮）/「私密房间」（默认面板色），点一下切换。
 * 位置放在名单卡下方、等待文案（66%）之下，且避开对局中的「开始游戏」按钮（bottom 12%）。
 * ⚠ 不要写成 .action-btn：它的 :active 会覆盖这里的 translateX(-50%) 水平居中。
 */
.room-public-btn {
	position: absolute;
	top: 74%;
	left: 50%;
	transform: translateX(-50%);
	padding: calc(var(--uu, 2.86px) * 1.6) calc(var(--uu, 2.86px) * 4);
	font-size: calc(var(--uu, 2.86px) * 3);
	font-weight: 700;
	letter-spacing: 1px;
	color: var(--text-dim);
	background: var(--panel-solid);
	border: 2px solid var(--line-strong);
	border-radius: 0;
	box-shadow: var(--px-drop-sm), var(--px-bevel);
	cursor: pointer;
	user-select: none;
	z-index: 3;
	transition: transform 0.12s ease, filter 0.12s ease, color 0.2s, border-color 0.2s, background 0.2s;
}

.room-public-btn:hover { filter: brightness(1.1); }
.room-public-btn:active { transform: translateX(-50%) scale(0.96); }

/* 公开中：黄铜高亮（与主按钮同色系，一眼看出「房间已放出去」） */
.room-public-btn.is-on {
	color: #3a2410;
	background: var(--brass);
	border-color: rgba(255, 240, 214, 0.75);
	box-shadow: var(--px-drop-sm), var(--px-bevel);
}

/* ================= 等待室：机器人按钮（添加/移除 + 难度）(vDOM 实体) ================= */

/*
 * 仅房主、仅等待室可见。两枚胶囊同一行，叠在左下角「改名」FAB 的正上方：
 *   底边：改名 3% → 本行 7%（本行高约 3.5% → 到 10.5%），而「开始游戏」在 bottom 12%
 *   （横向 27%~73%）—— 纵向上不搭界，横向上也错开。
 * 机器人现在是**服务端的座位**（桩驾驶员），点一下是发一个普通事件给服务端，
 * 不再像之前那样由客户端另开连接。
 */
.bot-btn,
.bot-level-btn {
	position: absolute;
	bottom: 7%;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	padding: calc(var(--uu, 2.86px) * 1.4) calc(var(--uu, 2.86px) * 2.6);
	font-size: calc(var(--uu, 2.86px) * 2.8);
	font-weight: 700;
	letter-spacing: 1px;
	color: var(--accent);
	background: var(--panel-solid);
	border: 2px solid rgba(233, 189, 118, 0.7);
	border-radius: 0;
	box-shadow: var(--px-drop-sm), var(--px-bevel);
	cursor: pointer;
	user-select: none;
	z-index: 6;
	transition: filter 0.15s ease, color 0.2s, border-color 0.2s, background 0.2s;
}

.bot-btn { left: 4%; }
/* 难度胶囊跟在它右边（两块文案都是 5 个字，宽度相当，27% 不会相撞） */
.bot-level-btn { left: 28%; }

.bot-btn:hover,
.bot-level-btn:hover { filter: brightness(1.15); }

/* 房里已有机器人（文案变「移除机器人」）：黄铜高亮，与其它「已开启」的开关一致 */
.bot-btn.is-on {
	color: #3a2410;
	background: var(--brass);
	border-color: rgba(255, 240, 214, 0.75);
	box-shadow: var(--px-drop-sm), var(--px-bevel);
}

/* 困难档：难度胶囊也用黄铜高亮，一眼看出对方在「看你的布局」 */
.bot-level-btn[data-level="hard"] {
	color: #3a2410;
	background: var(--brass);
	border-color: rgba(255, 240, 214, 0.75);
}

/* ================= 等待室：「改名」按钮 + 改名面板 (vDOM 实体) ================= */

/*
 * 「改名」按钮：舞台**左下角**（与右下角的「玩法」FAB 对称）。
 * ⚠ 刻意不放顶部信息条：那里已有昵称卡 / 房间号卡 / 音频栏三件，再加一件会把
 *   信息条挤爆 —— 实测昵称卡被截成「昵称：…」、音频按钮被折成两行。
 * 尺寸与玩法 FAB 完全同档，两个角上的胶囊看起来是一对工具按钮。
 */
.rename-btn {
	position: absolute;
	left: 4%;
	bottom: 3%;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	padding: calc(var(--uu, 2.86px) * 1.4) calc(var(--uu, 2.86px) * 2.6);
	font-size: calc(var(--uu, 2.86px) * 2.8);
	font-weight: 700;
	letter-spacing: 1px;
	color: var(--accent);
	background: var(--panel-solid);
	border: 2px solid rgba(233, 189, 118, 0.7);
	border-radius: 0;
	box-shadow: var(--px-drop-sm), var(--px-bevel);
	cursor: pointer;
	user-select: none;
	z-index: 6;
	transition: filter 0.15s ease;
}

.rename-btn:hover { filter: brightness(1.15); }

/*
 * 改名面板：悬于舞台正中的小对话框（标签 / 输入框 / 错误行 —— 均占整行，下面两个按钮并排）。
 * ⚠ 默认 `display: none`，展开态由**客户端**直接加 `is-open`（与玩法抽屉同一套做法）；
 *   服务端下发的容器类名里**永远**不带 `is-open`，否则每次开合都会改变摘要。
 * ⚠ 不用 `.action-btn` 的通用尺寸：那两个按钮在面板里要各占一半宽，见下方覆盖。
 *
 * ⚠ **宽度上限必须跟着 `--uu` 长**，不能是裸的绝对像素：
 *   面板内部的 padding / gap / 字号全都随 `--uu` 缩放，若宽度死卡在 268px，
 *   舞台一大就会出现「内容比容器宽」—— 实测 900×1600（--uu = 9px）时
 *   两个按钮各只能分到 82px，而「确定」两个字连内边距要 119px
 *   → 文字被迫换成两行、面板被撑到 450px 高（同一根因的另一症状）。
 *   `max(268px, 46uu)` = 「手机屏保持原有尺寸，大屏按比例长」：
 *   --uu 就是舞台宽的 1%，所以 46uu ≈ 舞台宽的 46%，与内部间距等比。
 *   ⚠ 不能再加硬上限（如 460px）：那样在超大分辨率下又会回到「内部比容器宽」。
 */
.rename-panel {
	position: absolute;
	top: 50%;
	left: 50%;
	transform: translate(-50%, -50%);
	display: none;
	flex-wrap: wrap;
	justify-content: center;
	align-items: center;
	gap: calc(var(--uu, 2.86px) * 2.2);
	width: min(76%, max(268px, calc(var(--uu, 2.86px) * 46)));
	padding: calc(var(--uu, 2.86px) * 4) calc(var(--uu, 2.86px) * 4.5);
	border: 3px solid rgba(233, 189, 118, 0.85);
	border-radius: 0;
	background: var(--panel-solid);
	box-shadow: var(--px-drop-lg), 0 0 0 6px rgba(0, 0, 0, 0.3), var(--px-bevel);
	z-index: 8;
}

.rename-panel.is-open { display: flex; }

/*
 * 面板里的三个整行元素（标签 / 输入框 / 错误行）。
 *
 * ⚠ `min-width: 0` 不能省 —— 它修的是「昵称输入框在某些分辨率下超出边界」：
 *   flex 子项的 `min-width` 默认是 `auto`，会被解析成**内容的最小尺寸**；对
 *   `<input>` 来说就是那个由 `size`（默认 20 个字符）算出的**固有宽度**，而它会
 *   随 `font-size`（= `--uu × 3.4`）一起变大。面板宽度却被 `min(76%, 268px)`
 *   **按绝对值卡在 268px**，于是舞台一大、输入框的下限就超过面板内容宽，而 flex
 *   不允许子项缩到下限之下 → 输入框被 `justify-content: center` 往两侧顶出去：
 *     实测 1920×1080 超出 19px/侧、900×1600 超出 94px/侧（小屏反而正常，
 *     因为 --uu 小、固有宽度也小，两者「恰好都小」而看不出问题）。
 *   标签与错误行同样是 flex 子项，一并写上是防御性的（长串 ASCII 文本同样撑不破）。
 *   ⚠ 面板里的两个 `.action-btn` 早就写了 `min-width: 0`（见下），当初只是漏了输入框。
 */
.rename-label,
.rename-input,
.rename-error {
	flex-basis: 100%;
	min-width: 0;
}

.rename-label {
	font-size: calc(var(--uu, 2.86px) * 3);
	color: var(--text-dim);
	text-align: center;
}

.rename-input {
	height: calc(var(--uu, 2.86px) * 11);
	padding: 0 calc(var(--uu, 2.86px) * 2.4);
	font-size: calc(var(--uu, 2.86px) * 3.4);
	color: var(--text);
	background: #2a2118;
	border: 2px solid var(--line);
	border-radius: 0;
	box-shadow: inset 0 3px 0 rgba(0, 0, 0, 0.35);
	outline: none;
	text-align: center;
	transition: border-color 0.2s, background 0.2s;
}

.rename-input:focus {
	background: rgba(255, 231, 196, 0.11);
	border-color: var(--accent);
}

/* 错误行：空内容由 :empty 直接隐藏，免得面板多出一段空白 */
.rename-error {
	font-size: calc(var(--uu, 2.86px) * 2.6);
	line-height: 1.4;
	color: var(--warn);
	text-align: center;
}

.rename-error:empty { display: none; }

/* 面板里的两个按钮：并排各占一半 */
.rename-panel .action-btn {
	min-width: 0;
	flex: 1;
	padding: calc(var(--uu, 2.86px) * 1.8) calc(var(--uu, 2.86px) * 2.4);
	font-size: calc(var(--uu, 2.86px) * 3);
	border-radius: 0;
}

/* ================= 「已开始游戏」提示条 (vDOM 实体) ================= */

/* ================= 中国象棋棋盘 (vDOM 实体) ================= */

/*
 * 棋盘 = 9 列 × 10 行交叉点（**木色底**，格线用墨褐 —— 向实木棋盘靠拢，
 * 与等待室那枚木质棋子同一套质感；原来深灰底 + 亮红/蓝棋子才是配色不协调的根源）。
 * 格线 / 河界缺口 / 九宫斜线全部由内联 SVG 背景绘制：
 * viewBox 为 0 0 100 100 且 preserveAspectRatio="none"，
 * 因此 SVG 百分比坐标与棋子 left/top 百分比同一个参照系，必然对齐。
 *
 * 注意：棋盘不得设 border / padding —— 否则绝对定位子元素（棋子）的百分比
 * 参照系（padding box）会与背景缩放参照系错位，棋子会整体偏移。
 * 外框用 box-shadow 画（不占布局）。
 */
.xq-board {
	position: absolute;
	top: 50%;
	left: 50%;
	transform: translate(-50%, -50%);
	width: 92%;
	aspect-ratio: 8 / 9;
	border-radius: 0;
	background-color: var(--wood);
	background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' preserveAspectRatio='none' shape-rendering='crispEdges'%3E%3Cpath d='M6.5 6.5H93.5M6.5 16.167H93.5M6.5 25.833H93.5M6.5 35.5H93.5M6.5 45.167H93.5M6.5 54.833H93.5M6.5 64.5H93.5M6.5 74.167H93.5M6.5 83.833H93.5M6.5 93.5H93.5M6.5 6.5V93.5M93.5 6.5V93.5M17.375 6.5V45.167M17.375 54.833V93.5M28.25 6.5V45.167M28.25 54.833V93.5M39.125 6.5V45.167M39.125 54.833V93.5M50 6.5V45.167M50 54.833V93.5M60.875 6.5V45.167M60.875 54.833V93.5M71.75 6.5V45.167M71.75 54.833V93.5M82.625 6.5V45.167M82.625 54.833V93.5M39.125 74.167L60.875 93.5M60.875 74.167L39.125 93.5M39.125 6.5L60.875 25.833M60.875 6.5L39.125 25.833' fill='none' stroke='%236b4a26' stroke-width='0.8'/%3E%3Cpath d='M6.5 6.5H93.5V93.5H6.5Z' fill='none' stroke='%236b4a26' stroke-width='1.4'/%3E%3C/svg%3E");
	background-size: 100% 100%;
	background-repeat: no-repeat;
	/* 木纹暗角用**硬内描边**（原来是 12uu 模糊的面光 —— 模糊影与点阵字风格冲突） */
	box-shadow: inset 0 0 0 3px rgba(88, 56, 20, 0.35), inset 0 0 0 6px rgba(255, 236, 205, 0.1), 0 5px 0 rgba(0, 0, 0, 0.45), 0 0 0 4px var(--wood-dk);
	z-index: 1;
	touch-action: none;
}

/* ================= 棋子 (vDOM 实体，可拖放) ================= */

/*
 * 棋子直径 ≈ 一个交叉点格宽（棋盘宽度的 10.2%）。
 * left/top 由服务端下发（棋盘内百分比），transform 只负责把圆心对齐到交叉点。
 * 允许棋子重叠：不做占位避让。
 */
.xq-piece {
	position: absolute;
	width: 10.2%;
	aspect-ratio: 1;
	transform: translate(-50%, -50%);
	display: flex;
	align-items: center;
	justify-content: center;
	/*
	 * 字形居中修正：
	 * Zpix 是像素字体，实测其汉字字形在**前进盒（advance box）内固有偏左** ——
	 * 前进宽 24.13px，而墨迹只占 -1..21（左侧 −1 / 右侧 +3.13），墨心比前进盒中心
	 * 偏左 2.06px ≈ 0.0925em。而 flex 的 `justify-content: center` 居中的是
	 * **前进盒**（不是墨迹），所以字看上去整体偏左。
	 * 修法：只加左侧内边距（= 2×偏移量），使**内容盒中心**右移一个偏移量。
	 * 用 em 表达，随 font-size（即随舞台短边 --uu）等比缩放，托盘小圆片同规则复用。
	 */
	--glyph-shift-x: 0.0925em;
	padding-left: calc(var(--glyph-shift-x) * 2);
	border-radius: 50%;
	font-size: calc(var(--uu, 2.86px) * 5.6);
	font-weight: 700;
	line-height: 1;
	user-select: none;
	touch-action: none;
	cursor: grab;
	z-index: 2;
	/* 斜面（上亮下暗）+ 硬投影：两道 0 模糊内阴影 + 一道 0 模糊外阴影 */
	box-shadow: inset 0 3px 0 rgba(255, 255, 255, 0.22), inset 0 -3px 0 rgba(0, 0, 0, 0.28), 0 3px 0 rgba(0, 0, 0, 0.4);
	/*
	 * 推演时棋子靠 left/top 过渡完成「飞向落点」的动画。
	 * 服务端 XQ_RESOLVE_MOVE_MS 必须 >= 这里的时长，否则会在动画未完时就开始移除棋子。
	 */
	transition: left 0.5s ease, top 0.5s ease;
}

/* 红方：朱红底 + 暖白字。球面用**硬停靠的 5 条色带**（高光 → 深暗）而非平滑渐变 */
.xq-piece.side-red {
	color: #fff1e0;
	border: 2px solid var(--vermilion-dk);
	background: radial-gradient(circle at 34% 28%,
		#ea6b57 0 13%, #d9533f 13% 32%, #c8412f 32% 55%, #ab3423 55% 79%, #8d2317 79% 100%);
	text-shadow: 1px 1px 0 rgba(60, 12, 6, 0.75);
}

/* 黑方：墨黑底 + 象牙字（中国象棋本就是「红方 / 黑方」两方，不是红/蓝） */
.xq-piece.side-black {
	color: #f0e3cf;
	border: 2px solid #0b0b0f;
	background: radial-gradient(circle at 34% 28%,
		#5d5d6b 0 13%, #47474f 13% 32%, #34343c 32% 55%, #232328 55% 79%, #141419 79% 100%);
	text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.8);
}

/*
 * 对方棋子：字色改成**琥珀金**（自己的棋子保持暖白字），一眼分清敌我。
 * ⚠ 「对方」是**按视角**的：棋盘按「己方在下」翻转，所以同一枚棋子对房主是「对方」、
 *   对客人就是「自己」—— 类名由**服务端按每个玩家的阵营**下发（`XQ_ENEMY_CLASS`），
 *   CSS 自己判断不了。
 * ⚠ 必须带 `.side-red` / `.side-black`（3 个类）而不是只写 `.xq-piece.is-enemy`：
 *   后者与 `.xq-piece.side-red` 同为「2 个类」的权重，**谁后声明谁生效** ——
 *   将来把本规则往上挪一格就会静默失效（本项目踩过同类坑）。
 */
.xq-piece.side-red.is-enemy,
.xq-piece.side-black.is-enemy {
	color: var(--accent);
	text-shadow: 1px 1px 0 rgba(40, 16, 4, 0.9);
}

/* 上一回合移动并存活的棋子：彩色闪烁边框（只动 border-color/box-shadow，不碰 transform） */
@keyframes xq-moved-glow {
	0%   { border-color: var(--accent); box-shadow: 0 0 0 2px rgba(233, 189, 118, 0.9), 0 0 0 4px rgba(233, 189, 118, 0.32); }
	50%  { border-color: #ff9d4d; box-shadow: 0 0 0 2px rgba(255, 157, 77, 0.9), 0 0 0 4px rgba(255, 157, 77, 0.32); }
	100% { border-color: var(--accent); box-shadow: 0 0 0 2px rgba(233, 189, 118, 0.9), 0 0 0 4px rgba(233, 189, 118, 0.32); }
}

/* 注意：这条要放在 .is-dragging / .is-rejected 之前，
   否则同级选择器下后声明的动画会盖掉拖拽投影与被拒闪烁。
   `steps(4)` 而不是 linear：颜色在 4 档之间**跳变**，与点阵字同一档观感。 */
.xq-piece.is-moved {
	animation: xq-moved-glow 2.4s steps(4, end) infinite;
}

/*
 * 被将军：**不做任何棋子标记**（用户要求）。
 * 曾经这里有一个「将的红色脉冲边框」与「攻击者红角标」，均已删除 ——
 * 将军只靠棋盘上沿弹一下的「将！」（`.xq-check-warn`）告知。
 */

/* 拖拽中：提升层级并加重投影（拖拽预览必须即时跟随指针，故关闭过渡） */
.xq-piece.is-dragging {
	transition: none;
	animation: none;
	z-index: 5;
	cursor: grabbing;
	box-shadow: 0 6px 0 rgba(0, 0, 0, 0.5);
}

/* 走子被服务端拒绝：只闪一下红光，位置保持不变（不动 transform，避免破坏居中） */
.xq-piece.is-rejected {
	animation: xq-reject 0.4s ease;
}

@keyframes xq-reject {
	0%, 100% { box-shadow: 0 3px 0 rgba(0, 0, 0, 0.4); }
	30%, 70% { box-shadow: 0 0 0 4px rgba(255, 90, 90, 0.95); }
}

/* ================= 移动请求箭头 (vDOM 实体) ================= */

/*
 * 箭头以「棋盘宽度的 1%」为统一单位：left/top = 起点交叉点，width = 长度，
 * transform: rotate(…) = 方向；transform-origin 由 CSS 固定在起点（元素左上，高度 3px 时 50% = 线中心）。
 *
 * ⚠ 线本体必须用**元素盒**（height + background）而不是 height:0 + border-top：
 * 否则 ::after 的 top:50% 参照的是 0 高盒子，箭头三角会离线中心偏半个线宽。
 * margin-top: -1.5px 把线盒上移半个线宽，使【线中心】正好落在交叉点上。
 *
 * 层级：z-index 3 = **在棋子之上**（棋子 2、拖拽中的棋子 5）。
 * 若箭头在棋子之下，落点被棋子占着时（例如想吃掉那枚棋子）箭头末端会被完全盖住，
 * 看不到指向哪里。
 *
 * pointer-events: none 仍然保留：带箭头的棋子本回合已被锁定（不能再拖），
 * 所以盖不盖住都无影响；设为 none 只是把「箭头永不干扰交互」变成硬保证。
 */
.xq-arrow {
	position: absolute;
	height: 3px;
	margin-top: -1.5px;
	background: currentColor;
	border-radius: 0;
	transform-origin: 0 50%;
	pointer-events: none;
	opacity: 0.9;
	z-index: 3;
	filter: drop-shadow(2px 2px 0 rgba(0, 0, 0, 0.5));
}

/*
 * 箭头三角：top:50% 即线中心；往左拉 65%（三角宽 17px）使**尖端**正好落在线段末端（目标交叉点）。
 */
.xq-arrow::after {
	content: "";
	position: absolute;
	left: 100%;
	top: 50%;
	transform: translate(-65%, -50%);
	border: 6px solid transparent;
	border-left: 11px solid currentColor;
}

.xq-arrow.side-red { color: var(--vermilion); }
.xq-arrow.side-black { color: var(--ink); }

/* ================= 吃子陈列区 (棋盘上/下各一栏) (vDOM 实体) ================= */
/*
 * 棋盘占舞台中部（约 20.9% ~ 79.1%）。
 * 「对方棋子」一栏贴棋盘上沿向上生长（己方在下的镜像）；
 * 「己方棋子」一栏贴棋盘下沿向下生长。
 * 条目尺寸刻意控制为宽度的 4.2% —— 92% 宽的一行能放得下全部 16 枚，不会换行压到棋盘。
 */
.xq-tray {
	position: absolute;
	left: 50%;
	transform: translateX(-50%);
	width: 92%;
	display: flex;
	flex-wrap: wrap;
	gap: calc(var(--uu, 2.86px) * 0.7);
	z-index: 2;
}

/* 对方棋子：棋盘上方（与「己方棋子」一栏相对棋盘对称，间距同为 1.19%） */
.xq-tray.enemy {
	bottom: 80.3%;
	align-content: flex-end;
}

/* 己方棋子：棋盘下方 */
.xq-tray.own {
	top: 80.3%;
	align-content: flex-start;
}

.xq-tray-piece {
	width: calc(var(--uu, 2.86px) * 4.2);
	height: calc(var(--uu, 2.86px) * 4.2);
	display: flex;
	align-items: center;
	justify-content: center;
	/* 同 .xq-piece：抵消 Zpix 字形在前进盒内固有偏左（用 em，自动随字号缩放） */
	--glyph-shift-x: 0.0925em;
	padding-left: calc(var(--glyph-shift-x) * 2);
	border-radius: 50%;
	font-size: calc(var(--uu, 2.86px) * 2.5);
	font-weight: 700;
	line-height: 1;
	user-select: none;
	/* 边框常驻（透明）——高亮时只换颜色，避免尺寸跳动 */
	border: 1px solid transparent;
}

.xq-tray-piece.side-red {
    color: #fff1e0;
    border-color: var(--vermilion-dk);
    background: #b53826;
}

.xq-tray-piece.side-black {
    color: #f0e3cf;
    border-color: #0b0b0f;
    background: #2f2f38;
}

/*
 * 对方被吃掉的棋子（上方那一栏）：与棋盘上的对方棋子同一口径 —— 琥珀字。
 * 这里靠**容器类** `.enemy` 判定（与棋盘不同，陈列栏的容器本身就是按视角分的）。
 * ⚠ 同样带上 `.side-*`（3 段选择器）避免与上面的同权重规则拼顺序。
 */
.xq-tray.enemy .xq-tray-piece.side-red,
.xq-tray.enemy .xq-tray-piece.side-black {
	color: var(--accent);
}

/* 上一回合刚吃到的棋子：琥珀色高亮边框（底色沿用红 / 黑阵营色，不再用蓝） */
.xq-tray-piece.is-recent {
	border-color: #ffd479;
	box-shadow: 0 0 0 2px rgba(255, 212, 121, 0.9), 0 0 0 4px rgba(255, 212, 121, 0.35);
}

/* ================= 棋盘下方的滚动小提示 (vDOM 实体) ================= */

/*
 * 位于棋盘下方空档：己方陈列栏最高到 ≈82.4%，底部操作条从 ≈89.8% 开始。
 * 结构：.move-hint（容器，只露一行 / overflow 裁剪）
 *         └ .move-hint-track（上滑轨道）
 *             └ .move-hint-line ×9（8 条文案 + 首条副本，用于无缝循环）
 * 文案就是玩法抽屉的条目说明（服务端 `XQ_ROUND_TIPS = XQ_HELP_ITEMS.map(desc)`）。
 */
.move-hint {
	position: absolute;
	top: 83.6%;
	left: 50%;
	transform: translateX(-50%);
	width: 92%;
	height: calc(var(--uu, 2.86px) * 3.4);
	overflow: hidden;
	text-align: center;
	user-select: none;
	z-index: 2;
}

.move-hint-track {
	animation: xq-hint-scroll 32s linear infinite;
}

.move-hint-line {
	height: calc(var(--uu, 2.86px) * 3.4);
	line-height: calc(var(--uu, 2.86px) * 3.4);
	/*
	 * 字号 2.4uu：比段落正文小一档 —— 这一行要装下抽屉里最长的一条说明（≈28 字，
	 * 行宽 ≈366px）。字号每上调一档，可容纳的字数就少一截，超了会被 ellipsis 截掉。
	 */
	font-size: calc(var(--uu, 2.86px) * 2.4);
	letter-spacing: 1px;
	color: var(--text-dim);
	text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.8);
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
}

/*
 * 轨道共 9 行（8 条文案 + 首条副本）→ 每 1/9 ≈ 11.111% 正好一行高。
 * 每步「停 8.889% + 过渡 2.222%」依次展示 8 条；末尾那行是首条副本，回到 0 时无缝衔接。
 * 时长 32s：每个段落 ≈ 8×4s，一条看 4 秒。
 *
 * ⚠ 文案条数（= `XQ_HELP_ITEMS.length` + 1）一变，这里的行数 / 百分比 / 时长必须一起重算，
 *   且**保持「每行一个 @keyframes 段落」的写法** —— 自测第 16 条按这个结构校验
 *   （行数、停靠点个数 2L-1、每步位移恰好 = 一行高）。
 */
@keyframes xq-hint-scroll {
	0%, 8.889%       { transform: translateY(0); }
	11.111%, 20%     { transform: translateY(-11.111%); }
	22.222%, 31.111% { transform: translateY(-22.222%); }
	33.333%, 42.222% { transform: translateY(-33.333%); }
	44.444%, 53.333% { transform: translateY(-44.444%); }
	55.556%, 64.444% { transform: translateY(-55.556%); }
	66.667%, 75.556% { transform: translateY(-66.667%); }
	77.778%, 86.667% { transform: translateY(-77.778%); }
	100%             { transform: translateY(-88.889%); }
}

/* ================= 玩法说明抽屉 (vDOM 实体) ================= */

/*
 * 右下角常驻的「玩法」触发按钮。
 * 位置避开底部操作条（居中，约 27%~70% 宽）与滚动提示（83.6%），不与之相撞。
 */
.help-fab {
	position: absolute;
	right: 4%;
	bottom: 3%;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	padding: calc(var(--uu, 2.86px) * 1.4) calc(var(--uu, 2.86px) * 2.6);
	font-size: calc(var(--uu, 2.86px) * 2.8);
	font-weight: 700;
	letter-spacing: 1px;
	color: var(--accent);
	background: var(--panel-solid);
	border: 2px solid rgba(233, 189, 118, 0.7);
	border-radius: 0;
	box-shadow: var(--px-drop-sm), var(--px-bevel);
	cursor: pointer;
	user-select: none;
	z-index: 6;
	transition: filter 0.15s ease;
}

.help-fab:hover { filter: brightness(1.2); }

/*
 * 玩法说明抽屉：从右侧滑入。
 * ⚠ 展开/收起由**客户端**直接切类名（`.help-drawer.is-open`），**不走服务端** ——
 *   它是纯本地 UI 状态（不改规则、对手看不见），走动作帧既白费一次往返、
 *   又会让每次开合都改变摘要。见 syncxiangqiapp.js 的「玩法说明抽屉的展开状态」小节。
 * 关闭态靠 `translateX(102%)` 移出，由 `.stage` 的 `overflow: hidden` 裁掉，
 * 否则在宽屏上会飘在舞台右侧的黑边上。
 */
.help-drawer {
	position: absolute;
	top: 0;
	right: 0;
	width: 80%;
	height: 100%;
	padding: calc(var(--uu, 2.86px) * 7) calc(var(--uu, 2.86px) * 4) calc(var(--uu, 2.86px) * 5);
	overflow-y: auto;
	overflow-x: hidden;
	background: var(--panel-solid);
	border-left: 3px solid var(--line-strong);
	box-shadow: -8px 0 0 rgba(0, 0, 0, 0.6);
	transform: translateX(102%);
	transition: transform 0.36s cubic-bezier(0.22, 1, 0.36, 1);
	z-index: 9;
	scrollbar-width: thin;
	scrollbar-color: rgba(233, 189, 118, 0.4) transparent;
}

.help-drawer.is-open { transform: translateX(0); }

.help-drawer::-webkit-scrollbar { width: 5px; }
.help-drawer::-webkit-scrollbar-track { background: transparent; }
.help-drawer::-webkit-scrollbar-thumb {
	background: rgba(233, 189, 118, 0.4);
	border-radius: 0;
}

.help-title {
	font-size: calc(var(--uu, 2.86px) * 4);
	font-weight: 700;
	letter-spacing: 2px;
	color: var(--accent);
	margin-bottom: calc(var(--uu, 2.86px) * 4);
}

.help-close {
	position: absolute;
	top: calc(var(--uu, 2.86px) * 3);
	right: calc(var(--uu, 2.86px) * 3);
	width: calc(var(--uu, 2.86px) * 6);
	height: calc(var(--uu, 2.86px) * 6);
	display: flex;
	align-items: center;
	justify-content: center;
	padding: 0;
	font-size: calc(var(--uu, 2.86px) * 4);
	line-height: 1;
	color: var(--text-dim);
	background: #2a2118;
	border: 2px solid var(--line);
	border-radius: 0;
	cursor: pointer;
	user-select: none;
}

.help-close:hover { color: var(--text); border-color: var(--line-strong); }

/* 条目标题：琥珀色，和上一条说明拉开距离 */
.help-item-title {
	font-size: calc(var(--uu, 2.86px) * 3.2);
	font-weight: 700;
	color: var(--accent);
	margin-top: calc(var(--uu, 2.86px) * 3.4);
}

.help-item-title::before {
	content: "▸ ";
	color: var(--accent-deep);
}

/* 条目说明：暖白暗淡，行高放宽便于阅读 */
.help-item-desc {
	margin-top: calc(var(--uu, 2.86px) * 1.2);
	font-size: calc(var(--uu, 2.86px) * 2.7);
	line-height: 1.65;
	color: var(--text-dim);
}

/* ================= 回合信息条 (回合数 / 倒计时 / 就绪状态) ================= */

/*
 * 位于棋盘上方。垂直预算（9:16 舞台上共约 20.9%，即 ≈148px）：
 *   顶部信息条 2.0%~6.7% → 回合条 8.1%~12.7% → 对手信息条 14.2%~15.8%
 *   → 对方被吃棋子陈列栏 17.3%~19.7% → 棋盘 20.9% 起。
 * 四段间隙实测均约 10px（缩小顶部卡片后释放的空间均分到这里），四者互不重叠；
 * 改动任一处的高度/位置都要重算这条链。
 */
.round-bar {
	position: absolute;
	/* 10.8%（原 10.4%）：像素化后顶部卡片与回合条的边框都加粗了 2px，
	   会把这两行之间的缝隙挤掉 ≈3px —— 往下让一点把间距还原回 ~9px */
	top: 10.8%;
	left: 50%;
	transform: translate(-50%, -50%);
	display: flex;
	align-items: center;
	gap: calc(var(--uu, 2.86px) * 2.4);
	/* 垂直内边距压到 1.0uu：上方还要给「对手信息条」腾出独立一行 */
	padding: calc(var(--uu, 2.86px) * 1) calc(var(--uu, 2.86px) * 3);
	border: 2px solid var(--line-strong);
	border-radius: 0;
	background: var(--panel-solid);
	box-shadow: var(--px-drop-sm), var(--px-bevel);
	font-size: calc(var(--uu, 2.86px) * 3.2);
	white-space: nowrap;
	user-select: none;
	z-index: 3;
}

.round-info { color: var(--accent); font-weight: 700; }
.round-timer { color: var(--text); font-weight: 700; letter-spacing: 1px; }
.round-status { color: var(--text-dim); }

/* ================= 对手信息条 (棋盘上方：对手昵称 + 在线状态) ================= */

/*
 * 位置：方块上方的第二行（回合条下方、陈列栏上方），三段间隙均约 10px。
 * 字号刻意压到 2.5uu（比回合条小），避免在窄舞台上挤到棋盘。
 */
.opponent-bar {
	position: absolute;
	/* 15.3%（原 15.0%）：同上，跟随回合条下移，保证三段缝隙都在 8~10px */
	top: 15.3%;
	left: 50%;
	transform: translate(-50%, -50%);
	display: flex;
	align-items: center;
	gap: calc(var(--uu, 2.86px) * 1.6);
	font-size: calc(var(--uu, 2.86px) * 2.5);
	line-height: 1.2;
	white-space: nowrap;
	user-select: none;
	z-index: 2;
}

.opponent-name { color: var(--text-dim); }

.opponent-status {
	display: flex;
	align-items: center;
	gap: calc(var(--uu, 2.86px) * 0.8);
	color: var(--ok);
}

/* 状态小方块：在线绿色 / 离线红色（颜色随 currentColor 走；方格而不是圆点，与像素风一致） */
.opponent-status::before {
	content: "";
	width: calc(var(--uu, 2.86px) * 1.4);
	height: calc(var(--uu, 2.86px) * 1.4);
	border-radius: 0;
	background: currentColor;
}

.opponent-status.is-offline { color: var(--warn); }

/*
 * 回合条内的紧凑按钮（房主专用「结束游戏」）。
 * 只作用于回合条内部，不影响底部操作条上的撤销/就绪。
 */
.round-bar .action-btn {
	min-width: 0;
	padding: calc(var(--uu, 2.86px) * 1.1) calc(var(--uu, 2.86px) * 2.4);
	font-size: calc(var(--uu, 2.86px) * 2.8);
	border-width: 2px;
	border-radius: 0;
}

/* 结束游戏：警示色（暖红字红框），与开始/就绪的黄铜色区分开 */
.round-bar .action-btn.end-game-btn {
	color: var(--warn);
	border-color: rgba(224, 137, 111, 0.85);
	background: rgba(120, 42, 26, 0.35);
}

.round-bar .action-btn.end-game-btn:hover {
	color: #2b0d05;
	background: var(--warn);
}

/*
 * 求和：琥珀色（与结束游戏的红、开始/就绪的绿区分开）。
 * 刻意挂在回合条上（屏幕顶部），**不放底部操作条** —— 它是「一局一次、不可撤销」
 * 的动作，放在「撤销 / 就绪」旁边极易在点就绪时误触。
 */
.round-bar .action-btn.draw-game-btn {
	color: #ffd479;
	border-color: rgba(255, 212, 121, 0.8);
	background: rgba(120, 90, 20, 0.32);
}

.round-bar .action-btn.draw-game-btn:hover {
	color: #2a1e03;
	background: #ffd479;
}

/* ================= 音频开关栏 (音效 / 音乐) (vDOM 实体) ================= */

/*
 * 单独一行，放在滚动提示与底部操作条之间（提示底 ≈85.6%，操作条顶 ≈90.2%）。
 * 不挤占回合条宽度 —— 回合条上已经有回合数/倒计时/就绪/结束游戏。
 */
.audio-bar {
	/* 靠 margin-left:auto 推到顶部信息条最右侧（不再绝对定位独占一行） */
	margin-left: auto;
	display: flex;
	align-items: center;
	gap: calc(var(--uu, 2.86px) * 1.4);
	z-index: 3;
}

.audio-bar .action-btn {
	min-width: 0;
	padding: calc(var(--uu, 2.86px) * 0.9) calc(var(--uu, 2.86px) * 2.2);
	font-size: calc(var(--uu, 2.86px) * 2.6);
	border-width: 1px;
	border-radius: 0;
}

/* ================= 回合操作条 (撤销 / 就绪) ================= */

.action-bar {
	position: absolute;
	left: 50%;
	bottom: 4.5%;
	transform: translateX(-50%);
	display: flex;
	gap: calc(var(--uu, 2.86px) * 3);
	z-index: 3;
}

.action-btn {
	min-width: calc(var(--uu, 2.86px) * 20);
	padding: calc(var(--uu, 2.86px) * 2.4) calc(var(--uu, 2.86px) * 4);
	font-size: calc(var(--uu, 2.86px) * 3.6);
	font-weight: 700;
	letter-spacing: 1px;
	color: var(--text);
	background: var(--panel-solid);
	border: 3px solid var(--line-strong);
	border-radius: 0;
	box-shadow: var(--px-drop-sm), var(--px-bevel);
	cursor: pointer;
	user-select: none;
	transition: transform 0.12s ease, filter 0.12s ease;
}

.action-btn:hover { filter: brightness(1.15); }
.action-btn:active { transform: translate(1px, 2px); box-shadow: none; }

/*
 * 已就绪后：撤销 / 就绪 按钮灰掉且不可点击。
 * 禁用态由服务端下发（类名 + data-disabled），客户端只读 UI 属性；
 * 服务端同时会拒绝此时的 planMove / undoMove（外观之外还有规则兜底）。
 */
.action-btn.is-disabled,
.action-btn.is-disabled:hover,
.action-btn.is-disabled:active {
	opacity: 0.4;
	filter: grayscale(1);
	cursor: not-allowed;
	pointer-events: none;
	transform: none;
	box-shadow: none;
}

.action-btn.primary {
	color: #3a2410;
	background: var(--brass);
	border-color: rgba(255, 240, 214, 0.75);
	box-shadow: var(--px-drop-sm), var(--px-bevel);
}

/* ================= 和局确认框 (对方求和) (vDOM 实体) ================= */

/*
 * 对方发起求和时，悬于棋盘正中央的确认框：文案 +「同意和局 / 拒绝」。
 * 只下发给响应方（发起方通过操作条上置灰的「已求和」按钮获知进度）。
 * 文案独占一行（flex-basis:100%），两个按钮另起一行并排。
 */
.draw-offer-box {
	position: absolute;
	top: 50%;
	left: 50%;
	transform: translate(-50%, -50%);
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	align-items: center;
	gap: calc(var(--uu, 2.86px) * 2.2);
	min-width: 54%;
	padding: calc(var(--uu, 2.86px) * 4) calc(var(--uu, 2.86px) * 5);
	border: 3px solid rgba(233, 189, 118, 0.85);
	border-radius: 0;
	background: var(--panel-solid);
	box-shadow: var(--px-drop-lg), 0 0 0 6px rgba(0, 0, 0, 0.3), var(--px-bevel);
	font-size: calc(var(--uu, 2.86px) * 3.4);
	white-space: nowrap;
	user-select: none;
	z-index: 7;
	animation: xq-result-pop 0.22s ease both;
}

.draw-offer-text {
	flex-basis: 100%;
	text-align: center;
	color: var(--accent);
	font-weight: 700;
}

.draw-offer-box .action-btn {
	min-width: calc(var(--uu, 2.86px) * 14);
	padding: calc(var(--uu, 2.86px) * 1.6) calc(var(--uu, 2.86px) * 3);
	font-size: calc(var(--uu, 2.86px) * 3);
	border-radius: 0;
}

/* ================= 胜负和结果覆盖层 (棋盘正中央) (vDOM 实体) ================= */

/*
 * 对局结束（一方将帅阵亡 / 双方将帅同归于尽 / 双方和局）后，覆盖在棋盘正中央的大字
 * 「胜 / 负 / 和」—— 按各玩家视角给出（胜=自己赢，负=自己输，和=和局）。
 * 它是 #xq_board 的子节点，故定位用棋盘自身尺寸的百分比；z-index 高于棋子/箭头。
 * 此时游戏已停止：除房主的「结束游戏」外不再接受任何交互（服务端同样拒绝）。
 */
.xq-result {
	position: absolute;
	top: 50%;
	left: 50%;
	transform: translate(-50%, -50%);
	display: flex;
	align-items: center;
	justify-content: center;
	width: 36%;
	aspect-ratio: 1;
	border-radius: 0;
	background: rgba(20, 15, 10, 0.94);
	border: 4px solid var(--line-strong);
	/* 双层硬边框 → 像一枚「印章」，比原来的圆形光晕更贴像素风 */
	box-shadow: 0 0 0 5px rgba(0, 0, 0, 0.5), 0 8px 0 rgba(0, 0, 0, 0.6), inset 0 0 0 4px rgba(0, 0, 0, 0.45);
	/* 同 .xq-piece：抵消 Zpix 字形在前进盒内固有偏左（用 em，自动随字号缩放） */
	--glyph-shift-x: 0.0925em;
	padding-left: calc(var(--glyph-shift-x) * 2);
	font-size: calc(var(--uu, 2.86px) * 17);
	font-weight: 700;
	line-height: 1;
	user-select: none;
	pointer-events: none;
	z-index: 6;
	animation: xq-result-pop 0.35s steps(3, end) both;
}

@keyframes xq-result-pop {
	0%   { transform: translate(-50%, -50%) scale(0.4); opacity: 0; }
	100% { transform: translate(-50%, -50%) scale(1); opacity: 1; }
}

.xq-result.is-win  { color: var(--accent);  border-color: rgba(233, 189, 118, 0.9);  text-shadow: 4px 4px 0 rgba(0, 0, 0, 0.55); }
.xq-result.is-lose { color: var(--warn);    border-color: rgba(224, 137, 111, 0.9);  text-shadow: 4px 4px 0 rgba(0, 0, 0, 0.55); }
.xq-result.is-draw { color: var(--ok);      border-color: rgba(168, 201, 143, 0.9);  text-shadow: 4px 4px 0 rgba(0, 0, 0, 0.55); }

/* ================= 「将军 / 绝杀」警告条 (棋盘内顶部) (vDOM 实体) ================= */

/*
 * 本作没有「将军→必须应将」规则（将帅阵亡即定胜负），所以「被将军」是一个**警告**：
 * 对方这一回合真想动手，就能吃到你的将；「绝杀」更进一步 —— 将走开 / 吃掉攻击者 /
 * 挡·拆炮架·蹩马腿 三种救法**全都不成立**。
 *
 * ⚠ 判据必须**同时**满足两个条件：① 有敌子按几何走法真能打到将；
 *   ② **那枚敌子本回合没有僵直**（上回合动过的子本回合动不了，够得着也不算威胁）。
 *   两者都在共享内核 `XQRules.xqCheckInfo` 里判，服务端只负责渲染。
 *
 * 位置：贴在**棋盘上沿之上**（棋盘上沿在舞台 20.9%，见回合条那节的垂直预算表）。
 * ⚠ 有意做成**舞台的子节点**而不是棋盘的子节点：挂在棋盘内顶部会压住对方底线那排
 *   （将/士/象），而那正是玩家要盯的地方。代价是会盖住上方那条「对方被吃棋子陈列栏」
 *   （通常只有一两行）—— 两害相权取其轻。
 * z-index 4：高于陈列栏(2)与棋盘(1)，低于玩法抽屉(9)，所以重叠时也画在上层。
 * 只发给**自己要挨打**的那一方（判定在服务端 `refreshXqCheckForPlayer`）。
 *
 * ⚠ **一次性**（用户要求）：弹出 → 硬闪 → **自动渐隐**，不是常驻横幅。
 *   渐隐完全由 CSS 承担（末态 `opacity: 0` + `forwards`），所以**服务端不需要定时器**：
 *   渐隐后元素仍留在 vDOM 里（不可见 + `pointer-events: none`，无害），
 *   等威胁解除 / 回合切换时由服务端移除；要「再弹一次」时服务端**移除再重建**该节点
 *   （新元素才会重播动画）—— 由 `refreshXqCheckForPlayer` 按状态签名决定。
 *   这样服务端零定时器、零额外状态；常驻的提示是**将身上的红色脉冲边框**。
 */
.xq-check-warn {
	/* 硬闪的两个颜色（当初为「将军/绝杀」两种风格而引入变量，现在就一套，保留写法方便调） */
	--warn-bg: #c8412f;
	--warn-bg-hi: #ff7a5e;
	position: absolute;
	bottom: 79.6%;
	left: 50%;
	transform: translateX(-50%);
	padding: 2px 12px;
	font-size: calc(var(--uu, 2.86px) * 4.2);
	font-weight: 700;
	letter-spacing: 2px;
	color: #fff1e0;
	background-color: var(--warn-bg);
	border: 3px solid #ffd9cc;
	box-shadow: 0 4px 0 rgba(0, 0, 0, 0.5);
	white-space: nowrap;
	pointer-events: none;
	user-select: none;
	z-index: 4;
	/*
	 * 两条动画各管**不同属性**，所以可以共存：
	 *   xq-check-toast —— opacity/transform：弹出 + 上浮渐隐（末态保持透明）
	 *   xq-check-blink —— background-color 的硬闪（steps，像素风），只闪开头三下
	 * ⚠ 不要把它们合成一条：两条动画动同一个属性时，后者会把前者完全覆盖。
	 */
	animation:
		xq-check-toast 2.4s linear forwards,
		xq-check-blink 0.7s steps(2, end) 3;
}

/* 硬闪：两档跳变（像素风的「闪烁」不该是渐变） */
@keyframes xq-check-blink {
	0%, 100% { background-color: var(--warn-bg); }
	50%      { background-color: var(--warn-bg-hi); }
}

/* 弹出 → 两下硬闪 → 上浮渐隐（`forwards` 停在透明，服务端随后才移除节点） */
@keyframes xq-check-toast {
	0%   { opacity: 0; transform: translateX(-50%) translateY(8px) scale(0.7); }
	8%   { opacity: 1; transform: translateX(-50%) translateY(0) scale(1.14); }
	16%  { opacity: 1; transform: translateX(-50%) translateY(0) scale(1); }
	56%  { opacity: 1; }
	62%  { opacity: 0.45; }
	68%  { opacity: 1; }
	74%  { opacity: 0.45; }
	80%  { opacity: 1; }
	100% { opacity: 0; transform: translateX(-50%) translateY(-12px) scale(1); }
}

/*
 * 无障碍：开启「减弱动态效果」时只保留渐隐（去掉位移与硬闪）。
 * ⚠ 这一条不能省 —— 动画是「把提示条隐掉」的唯一手段，若整段动画被去掉，
 *   提示条会**永远留在屏幕上**。
 */
@media (prefers-reduced-motion: reduce) {
	.xq-check-warn {
		animation: xq-check-fade-out 2.4s linear forwards;
	}
}

@keyframes xq-check-fade-out {
	0%, 70% { opacity: 1; }
	100%    { opacity: 0; }
}
