/**
 * 中国象棋模块样式。
 *
 * 只复用 ../global.css 的 CSS 变量和通用组件（.btn / .panel / .back-link 等），
 * 类名一律带 xq- 前缀（模块隔离要求）。
 *
 * 本模块用 DOM + CSS 渲染棋盘（不是 Canvas），所以**不需要监听 themechange** ——
 * 所有颜色都是 CSS 变量驱动的，切主题自动生效。
 * 不要从 conway-life-game 抄那段监听代码。
 */

/* === 模块级颜色 === */
/* 棋子与棋盘的颜色必须自己定义：global.css 里没有「棋盘木色」这类语义 */
:root {
  --xq-red: #c0392b;
  --xq-black: #2d3436;
  --xq-board-bg: #f0d9b5;
  --xq-board-line: #a08660;
  --xq-hl-last: rgba(9, 132, 227, .30);
  --xq-hl-target: rgba(0, 184, 148, .38);
  /* 提示（AI 建议的着法）。同一个琥珀色，圆点用半透明、棋子外圈用实色。
     从浅黄（#fdcb6e）换成偏深的琥珀：浅黄在米色棋盘上对比太弱 ——
     和「走子方外圈」踩过的是同一个坑（见 --xq-turn 的注释）。 */
  --xq-hint: #e8890c;
  --xq-hl-hint: rgba(232, 137, 12, .42);
  /* 开关的「关」态底色。不能用 --border（太浅，白色滑块在上面看不见） */
  --xq-switch-off: #ccd2d9;
  /* 走子方的高亮色。红黑两方**统一用同一个颜色**，不用各自的方色 ——
     「高亮」表达的是「现在轮到这边」，而「这是红方的子」棋子本身已经说了。
     顺带省掉「红黑各一套 + 深色主题再各一套」四份颜色。

     **为什么是紫：它要和棋盘底色比，不是和棋子比。**
     圈画在棋盘上（棋子外沿之外），而棋盘是暖米色 #f0d9b5。
     最初用的黄色和它同一个色系，50% 透明度合成出来是 #F2C65B ——
     还是「偏黄的米色」，对比天生就弱。紫色和米色是**色相对比**，一眼就能分开。

     同时它避开了所有已占用的颜色：
       红（棋子、被将军）、黑（棋子）、蓝（上一步）、青绿（可走位置）。 */
  --xq-turn: #7c3aed;
}
:root[data-theme="dark"] {
  /* 深色下棋盘不能纯黑，否则棋子边框看不清；用偏暖的深灰 */
  --xq-red: #ff6b6b;
  --xq-black: #e6e9ee;
  --xq-board-bg: #2b2a26;
  --xq-board-line: #6b6355;
  --xq-hl-last: rgba(77, 171, 247, .36);
  --xq-hl-target: rgba(0, 184, 148, .42);
  --xq-hint: #f0a02a;
  --xq-hl-hint: rgba(240, 160, 42, .45);
  --xq-switch-off: #3a414d;
}

/* === 玩法说明 === */
/* 入口是标题行末尾的一个问号图标，说明本体在标题行下面展开。
   原来入口是一整行可点的文字（「单机对弈 · 四个挡位 · …」）——
   那行字在手机上会折成两三行、把棋盘往下顶一截，而它换不来任何操作。 */
.xq-help-open {
  flex-shrink: 0;
  width: 26px;
  height: 26px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* global.css 的 .btn 带的是 9px 18px —— 圆图标得按自己的尺寸重设 */
  padding: 0;
  font-size: 14px;
  font-weight: 700;
  line-height: 1;
  border-radius: 50%;
}
/* 展开时点亮图标：说明块在下面，视线要能对上「是它打开的」 */
.xq-help-open[aria-expanded="true"] {
  color: var(--accent);
  border-color: var(--accent);
}

.xq-help-body {
  margin-top: 16px;
  padding: 14px 16px;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  font-size: 13px;
  color: var(--text-secondary);
  line-height: 1.7;
  text-align: left;
}
/* 说明块的显隐靠 el.hidden 控制，这里显式钉一条 ——
   别让以后加进来的 display 值把它顶掉（.xq-dialog-foot 踩过同一个坑） */
.xq-help-body[hidden] {
  display: none;
}
.xq-help-body table {
  border-collapse: collapse;
  margin-bottom: 12px;
}
.xq-help-body td {
  padding: 2px 12px 2px 0;
  vertical-align: top;
}
.xq-help-body p {
  margin-top: 10px;
}
.xq-help-body b {
  color: var(--text);
}
kbd {
  display: inline-block;
  padding: 1px 6px;
  font-family: var(--mono);
  font-size: 12px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 4px;
}

/* === 标题行：局面库入口 === */
.xq-title-row {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}

/* 唯一入口。同时承担两个职责：
     「局面库」= 这里能点开什么     （弱化）
     「标准开局」= 我现在在哪一局   （强调） */
.xq-picker-open {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  max-width: min(340px, 60vw);
  padding: 5px 12px;
  font-size: 13px;
}
.xq-picker-label {
  color: var(--text-secondary);
  transition: color .2s;
}
.xq-picker-sep {
  color: var(--text-secondary);
  opacity: .5;
}
.xq-picker-open b {
  min-width: 0;
  font-weight: 600;
  /* 自定义局面的名字可以有 40 字，不能把标题行撑破 */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* 小三角，明示「这能点开」 */
.xq-picker-open::after {
  content: '▾';
  font-size: 10px;
  color: var(--text-secondary);
  transition: color .2s;
}
.xq-picker-open:hover .xq-picker-label,
.xq-picker-open:hover::after {
  color: var(--accent);
}

/* === 布局 ===
   棋盘在第 1 行第 1 列、侧栏在第 1 行第 2 列、工具栏在第 2 行第 1 列。

   工具栏**必须**独立成一行，不能塞回棋盘列里 —— 否则第 1 行的高度会变成
   「棋盘 + 工具栏」，侧栏跟着变高，着法列表就伸到棋盘下沿之外了。
   这样摆之后第 1 行两列等高（align-items 默认就是 stretch），
   侧栏里的着法面板就能用 flex: 1 正好收在棋盘下方。 */
.xq-stage {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 300px;
  column-gap: 20px;
  row-gap: 14px;
}
.xq-board-col { grid-area: 1 / 1; min-width: 0; }
.xq-toolbar   { grid-area: 2 / 1; }
.xq-side      { grid-area: 1 / 2; }

/* 窄屏改回单列，顺序由 DOM 决定（棋盘 → 工具栏 → 侧栏） */
@media (max-width: 860px) {
  .xq-stage {
    grid-template-columns: 1fr;
  }
  .xq-board-col,
  .xq-toolbar,
  .xq-side {
    grid-area: auto;
  }
}

.xq-board-wrap {
  max-width: 560px;
  margin: 0 auto;
  /* 让棋子字号能跟着棋盘宽度走。inline-size 容器只约束宽度，
     不会像 size 那样影响 aspect-ratio 的推算 */
  container-type: inline-size;
}

/* === 棋盘上方的状态区 ===
   战斗状态（轮到谁走 / 将死 / 和棋）和棋谱说明（当前是哪一局、已走几步），
   从右侧「对局」面板搬到了棋盘正上方。

   搬家的理由：窄屏（≤860px）是单列布局，顺序是「棋盘 → 工具栏 → 侧栏」——
   留在侧栏里的话，这两行会被挤到整页最下面，正在下棋时看不见「轮到谁」，
   终局了也看不见「谁赢了」。放到棋盘正上方，什么布局都在视线里；
   桌面端这里也比隔着半屏的侧栏底部更靠近落子处。 */
.xq-board-status {
  /* 和棋盘左右对齐：.xq-board-wrap 就是 560px 居中 */
  max-width: 560px;
  margin: 0 auto 10px;
  font-size: 13px;
}
/* 状态正文和「思考中」并排 —— AI 思考时那两句说的是同一件事，不该各占一行 */
.xq-status-line {
  display: flex;
  align-items: baseline;
  gap: 8px;
  flex-wrap: wrap;
}
.xq-status {
  font-weight: 600;
}
.xq-status--over {
  color: var(--danger);
}
/* 状态里的方名按方着色 —— 扫一眼就知道轮到谁 */
.xq-status--red {
  color: var(--xq-red);
}
.xq-status--black {
  color: var(--xq-black);
}
/* 终局时整行统一走危险色，但**只有红方名让位，黑方名保留自己的颜色**。
   这个不对称是有原因的，不是漏写：
     - 红方名（--xq-red #c0392b）和终局色（--danger #d63031）本来就都是红的，
       让位等于没变化 ——「将死 · 红方胜」读起来仍然是一整句红字，不违和；
     - 黑方名（--xq-black #2d3436）和终局色差得远，让它随整行的话
       「将死 · 黑方胜 · 未达成目标」会成为一整句红字，看不出是谁赢的。
   深色主题下 --xq-red 与 --danger 恰好同值（#ff6b6b），两边表现一致即可。 */
.xq-status--over .xq-status--red {
  color: inherit;
}
/* 残局模式的棋谱说明（「xxx」· 谱载先手胜 · 已走 N 步） */
.xq-goal {
  margin-top: 4px;
  color: var(--accent);
}
/* AI 思考中。跟在状态正文后面，不另占一行 */
.xq-thinking {
  color: var(--accent);
}

/* === 棋盘 === */
/* 9×10 的格子是「交叉点」网格：每个格子的中心就是一个落子点。
   棋子绝对定位在棋盘容器里，用 left / top 百分比对到格心。
   棋盘背景天然自带半格边距（最外侧的线在 5.6% 和 94.4% 处），
   所以边线上的棋子不会被裁掉，不需要额外 padding。 */
.xq-board {
  position: relative;
  /* 建立层叠上下文：让下面那三档 z-index（棋盘线 0 / 格子 1 / 棋子 2）
     的作用范围就止于棋盘内部，不会漏出去盖住页面上的其他元素。 */
  isolation: isolate;
  display: grid;
  grid-template-columns: repeat(9, 1fr);
  grid-template-rows: repeat(10, 1fr);
  aspect-ratio: 9 / 10;
  width: 100%;
  background: var(--xq-board-bg);
  border: 1px solid var(--xq-board-line);
  border-radius: 8px;
  box-shadow: var(--shadow);
  transition: transform .3s;
}

/* 每个格子画自己的十字线。格子中心就是交叉点，
   所以横线横贯整格、竖线纵贯整格，拼起来正好是完整的棋盘格。
   z-index 让高亮盖住棋盘线 —— 九宫斜线是唯一会伸进格子内部的棋盘线。 */
.xq-cell {
  position: relative;
  z-index: 1;
  border: none;
  background: none;
  padding: 0;
  cursor: pointer;
  border-radius: 4px;
}
.xq-cell::before,
.xq-cell::after {
  content: '';
  position: absolute;
  background: var(--xq-board-line);
  pointer-events: none;
}
.xq-cell::before {
  left: 0;
  right: 0;
  top: 50%;
  height: 1px;
  margin-top: -.5px;
}
.xq-cell::after {
  top: 0;
  bottom: 0;
  left: 50%;
  width: 1px;
  margin-left: -.5px;
}

/* 河界：纵线在河的两岸断开（最外两条纵线不断，那是棋盘边界） */
.xq-cell--river-up::after {
  bottom: 50%;
}
.xq-cell--river-down::after {
  top: 50%;
}

/* 九宫斜线：两个伪元素绝对定位在棋盘容器上。
   注意绝对定位的伪元素不参与 grid 布局，所以不会挤掉格子。

   **必须显式写 z-index: 0，不能靠默认绘制顺序。**
   伪元素在绘制顺序里等同于子元素：::before 是最前一个、::after 是最后一个。
   而棋子是 draw() 时 appendChild 到棋盘上的，排在 ::after **前面** ——
   所以不加 z-index 的话，红方九宫的斜线（::after）会画在所有棋子之上，
   把帅和仕盖住；黑方九宫（::before）却正常地在棋子下面。
   同一个棋盘两半表现不一致，而且只在红方那半边看得出来。
   显式分层之后，「棋盘线 → 格子 → 棋子」的顺序就与 DOM 顺序无关了。 */
.xq-board::before,
.xq-board::after {
  content: '';
  position: absolute;
  z-index: 0;
  left: 38.889%;
  width: 22.222%;
  height: 20%;
  pointer-events: none;
  background-image:
    linear-gradient(to bottom right,
      transparent calc(50% - 1px), var(--xq-board-line) calc(50% - 1px) calc(50% + 1px), transparent calc(50% + 1px)),
    linear-gradient(to top right,
      transparent calc(50% - 1px), var(--xq-board-line) calc(50% - 1px) calc(50% + 1px), transparent calc(50% + 1px));
}
.xq-board::before { top: 5%; }      /* 黑方九宫（y = 0..2） */
.xq-board::after  { top: 75%; }     /* 红方九宫（y = 7..9） */

/* === 高亮 === */
/* 用带 alpha 的径向渐变画圆点，不覆盖格子自己的十字线 */
.xq-cell--last {
  background: radial-gradient(circle, var(--xq-hl-last) 0 30%, transparent 31%);
}
.xq-cell--target {
  background: radial-gradient(circle, var(--xq-hl-target) 0 20%, transparent 21%);
}
.xq-cell--hint {
  background: radial-gradient(circle, var(--xq-hl-hint) 0 34%, transparent 35%);
}
.xq-cell--selected {
  background: radial-gradient(circle, var(--xq-hl-last) 0 42%, transparent 43%);
}

/* === 棋子 === */
.xq-piece {
  position: absolute;
  z-index: 2;              /* 最高层：盖住棋盘线与格子高亮 */
  width: 10.222%;          /* 棋盘宽度的 1/9 再乘 .92 */
  aspect-ratio: 1;
  transform: translate(-50%, -50%);
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: var(--card-bg);
  border: 2px solid currentColor;
  box-shadow: var(--shadow);
  font-family: "KaiTi", "STKaiti", "SimSun", serif;
  font-weight: 700;
  font-size: 18px;         /* 不支持容器查询时的兜底 */
  font-size: 5.2cqw;
  line-height: 1;
  user-select: none;
  pointer-events: none;    /* 点击由底下的格子接管 */
  transition: left .18s ease-out, top .18s ease-out;
}
.xq-piece--red {
  color: var(--xq-red);
}
.xq-piece--black {
  color: var(--xq-black);
}
/* 上一步的落点：格子被棋子盖住了，所以圈画在棋子上 */
.xq-piece--last {
  box-shadow: 0 0 0 2px var(--accent), var(--shadow);
}
/* 被将军的将 / 帅：外圈加一道危险色（优先级高于「上一步」） */
.xq-piece--checked {
  box-shadow: 0 0 0 3px var(--danger), var(--shadow);
}

/* 轮到谁走：那一方的**全部**棋子外圈加一道高亮圈。
   只标将帅太不显眼 —— 满盘棋里找那一个小圈很费眼，所以标一整方。

   三个要点：

   1. 颜色是**统一的紫色**（`--xq-turn`），不是各自的方色。高亮表达的是
      「现在轮到这边」，而「这是红方的子」棋子本身已经说了
   2. **不透明，粗细取 2px**。这个效果的强弱主要由「粗细 × 不透明度」决定，颜色只是其次 ——
      一开始的 2px + 50% 透明度，即使换成好颜色也还是细。
      粗细的上限是几何决定的：相邻棋子中心距 62.2px、棋子半径 28.6px，
      所以圈的外沿半径不能超过 31.1px。`偏移 2 + 宽度 3` 已经到 33.6px，
      后排相邻棋子的圈会重叠 2.5px；收到 2px 后是 32.6px，重叠 1.5px
   3. 用 `outline` 而不是 `box-shadow` —— 后者已经被「上一步」和「被将军」占了
      （一个元素只能有一条 `box-shadow`），`outline` 不参与那套竞争，三者可以共存。
      另外 `outline` 在现代浏览器里会跟随 `border-radius`，所以画出来是圆的 */
.xq-piece--turn {
  outline: 2px solid var(--xq-turn);
  outline-offset: 2px;
}

/* 可吃子的目标：棋子外面一圈红环。
   **为什么不画「更大的红点」** —— 点画在格子上，而棋子不透明地盖住格子中心，
   所以「更大的点」在这里只能露出棋子外面很窄的一圈（受格子尺寸限制，最多约 2.5px）。
   直接画在棋子上没有这个限制，可以画得更粗更清楚。

   用 `outline` 而不是 `box-shadow`：后者被「上一步」占着。
   和「轮到我方」的紫圈**不会同时出现**（紫圈在我方子、红环在对方子），
   所以两者共用同一套几何参数也不冲突。
   3px 比紫圈粗一档 —— 吃子比「可以走」更值得提醒。 */
.xq-piece--capture {
  outline: 3px solid var(--danger);
  outline-offset: 2px;
}

/* 提示（AI 建议的着法）：**起点和终点**的棋子各套一道琥珀色外圈。
   和「可吃子」的红环同理 —— 格子上的圆点会被棋子盖住，所以画在棋子上。
   （起点一定也有子，所以两头都得这么画，不能只改终点。）

   **用 `::after` 而不是 `outline`**：`outline` 已经被「轮到我方」（紫圈）
   和「可吃子」（红环）占了，一个元素只有一条。而提示的**起点往往正是「轮到我方」的子** ——
   用 outline 会把它的紫圈顶掉，16 个紫圈里少 2 个，看起来像渲染出错。
   `::after` 可以套在紫圈外面，两者共存。

   偏移 6px 是为了让开紫圈（2px 偏移 + 2px 宽 = 4px）和红环（2px + 3px = 5px）。 */
.xq-piece--hint::after {
  content: '';
  position: absolute;
  inset: -6px;
  border-radius: 50%;
  border: 2px solid var(--xq-hint);
  pointer-events: none;
}

/* === 翻转 === */
/* 只影响显示：整个棋盘转 180°，棋子文字再转回来。
   因为格子和棋子都在棋盘内部，点击事件跟着 DOM 走，所以 cellAt 不需要任何翻转逻辑。 */
.xq-board-wrap.is-flipped .xq-board {
  transform: rotate(180deg);
}
.xq-board-wrap.is-flipped .xq-piece {
  transform: translate(-50%, -50%) rotate(180deg);
}

/* === 工具栏 === */
/* global.css 的 .btn 没有禁用态样式，这里补上。
   禁用态很重要：AI 思考期间按钮全部置灰，玩家才知道「不是坏了，是在等」。 */
.btn:disabled {
  opacity: .45;
  cursor: not-allowed;
}

.xq-toolbar {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  justify-content: center;
  /* 与棋盘之间的间距由 .xq-stage 的 row-gap 给，这里不要再加 margin */
}

/* 复制 FEN / 复制链接 点完会把文字短暂换成「已复制」。
   给个固定最小宽度 —— 工具栏是居中对齐的，按钮一缩一放会让整排左右抖一下。 */
.xq-copy-fen {
  min-width: 7em;
}

/* === 侧栏 === */
/* 侧栏和棋盘等高（第 1 行的 stretch 给的），所以「着法」面板可以用 flex: 1
   吃掉对局面板剩下的全部高度 —— 列表于是正好收在棋盘下沿 */
.xq-side {
  display: flex;
  flex-direction: column;
  gap: 16px;
  min-height: 0;
}
.xq-panel {
  padding: 16px 18px;
  margin-bottom: 0;
}
.xq-field {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-top: 10px;
  font-size: 13px;
}
.xq-field > span {
  color: var(--text-secondary);
}
.xq-select {
  flex: 1;
  min-width: 0;
  padding: 6px 8px;
  font-family: var(--font);
  font-size: 13px;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 8px;
}
.xq-select:focus {
  outline: none;
  border-color: var(--accent);
}

/* === 开关（双人对弈） === */
/* 用原生 checkbox + `appearance: none` 画成开关 —— **语义一点没变**：
   仍然是 checkbox，可 Tab、空格切换、读屏读得出「已选中 / 未选中」。
   不要用 <div> 挂 click 自己造，那样键盘和读屏都会丢。
   （旁边的 <label> 包着它，所以点文字也能切换。） */
.xq-switch {
  appearance: none;
  -webkit-appearance: none;
  position: relative;
  flex-shrink: 0;
  width: 38px;
  height: 22px;
  margin: 0;
  border: none;
  border-radius: 11px;
  background: var(--xq-switch-off);
  cursor: pointer;
  transition: background .2s;
}
.xq-switch::after {
  content: '';
  position: absolute;
  top: 3px;
  left: 3px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--card-bg);
  box-shadow: 0 1px 2px rgba(0, 0, 0, .3);
  transition: left .2s;
}
.xq-switch:checked {
  background: var(--accent);
}
.xq-switch:checked::after {
  left: 19px;   /* 38 - 3 - 16 */
}
.xq-switch:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.xq-switch:disabled {
  opacity: .45;
  cursor: not-allowed;
}
/* 状态行（.xq-status / .xq-goal / .xq-thinking）原本长在这儿，
   它们跟着 DOM 一起搬到了上面「棋盘上方的状态区」那一节，别在这里重复定义 */

/* === 局面分类页签 === */
/* 原来是下拉框。下拉框的问题是把「内置残局」和「自定义局面」混在同一个列表里，
   看不出边界；换成页签之后两者是并列的、随时能切。
   条数徽标让「我存过几条」一眼可见，不用点进去看。 */
.xq-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 2px;
  border-bottom: 1px solid var(--border);
}
.xq-tab {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 7px 12px;
  /* margin-bottom: -1px 让选中态的 2px 下边框压在容器的 1px 分隔线上，
     看起来像「页签接上了内容区」 */
  margin-bottom: -1px;
  border: none;
  border-bottom: 2px solid transparent;
  background: none;
  font-family: var(--font);
  font-size: 13px;
  color: var(--text-secondary);
  cursor: pointer;
  transition: color .2s, border-color .2s;
}
.xq-tab:hover {
  color: var(--text);
}
.xq-tab[aria-selected="true"] {
  color: var(--accent);
  border-bottom-color: var(--accent);
  font-weight: 600;
}
.xq-tab:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
  border-radius: 4px;
}
.xq-tab-count {
  font-size: 11px;
  font-weight: 400;
  color: var(--text-secondary);
  opacity: .7;
}
.xq-tab[aria-selected="true"] .xq-tab-count {
  color: var(--accent);
  opacity: .85;
}

/* === 局面过滤框 === */
/* 页签与列表之间的一道输入框。它是**页签内**的过滤（不跨页签搜），
   跨页签的线索交给页签徽标 —— 有过滤词时徽标显示的是各页的命中数。
   内置库收成全谱 551 局之后，没有它就是一路滚。 */
.xq-search {
  display: flex;
  gap: 6px;
  margin-top: 12px;
}
.xq-search .xq-input {
  padding: 7px 10px;
}
.xq-search-clear {
  flex-shrink: 0;
  width: 34px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  color: var(--text-secondary);
  background: none;
  border: 1px solid var(--border);
  border-radius: 8px;
  cursor: pointer;
}
.xq-search-clear:hover {
  color: var(--text);
  background: var(--bg);
}
/* **必须显式再写一条。** UA 的 `[hidden] { display: none }` 会被上面那句
   `display: flex` 覆盖掉（作者样式优先于 UA 样式），于是 el.hidden = true 不起作用。
   和 .xq-dialog-foot[hidden] 是同一个坑。 */
.xq-search-clear[hidden] {
  display: none;
}

/* === 残局 / 自定义局面列表 === */
.xq-endgame-list {
  flex: 1;
  min-height: 120px;
  overflow-y: auto;
  margin: 12px 0;
  padding-right: 4px;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
/* 一行 = 主体按钮 +（自定义局面才有）删除按钮 */
.xq-endgame-row {
  display: flex;
  align-items: center;
  gap: 2px;
}
.xq-endgame {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  flex: 1;
  min-width: 0;
  padding: 6px 8px;
  border: 1px solid transparent;
  border-radius: 8px;
  background: none;
  font-family: var(--font);
  font-size: 13px;
  color: var(--text);
  text-align: left;
  cursor: pointer;
}
/* 每行末尾那两个小图标按钮：改名 / 删除。形状一样，只有 hover 色分好坏 */
.xq-endgame-edit,
.xq-endgame-del {
  flex-shrink: 0;
  width: 26px;
  height: 26px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  color: var(--text-secondary);
  background: none;
  border: none;
  border-radius: 6px;
  cursor: pointer;
}
.xq-endgame-edit:hover {
  color: var(--accent);
  background: var(--bg);
}
.xq-endgame-del:hover {
  color: var(--danger);
  background: var(--bg);
}
.xq-endgame:hover {
  background: var(--bg);
  border-color: var(--border);
}
.xq-endgame--current {
  background: var(--accent);
  color: var(--on-accent);
}
.xq-endgame--current .xq-endgame-meta {
  color: var(--on-accent);
  opacity: .8;
}
.xq-endgame--current:hover {
  background: var(--accent-hover);
  border-color: transparent;
}
.xq-endgame-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.xq-endgame-meta {
  flex-shrink: 0;
  font-size: 12px;
  color: var(--text-secondary);
}
.xq-endgame-exit {
  width: 100%;
}

/* === 局面库弹窗 === */
/* 用原生 <dialog> + showModal()：Esc 关闭、焦点陷阱、背景遮罩都是白送的，
   比手写遮罩层省掉一大半代码（也少一堆「忘了恢复 body 滚动」的坑）。 */
.xq-dialog {
  width: min(560px, calc(100vw - 32px));
  max-height: min(78vh, 700px);
  /* **必须显式写 margin: auto**，不能指望 UA 默认值。
     global.css 顶部的 `*, *::before, *::after { margin: 0 }` 重置
     把 `dialog { margin: auto }` 一起干掉了 —— 少了这一行弹窗就贴顶显示。
     （position: fixed + inset: 0 + margin: auto 才是原生 dialog 的居中方式。） */
  margin: auto;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--card-bg);
  color: var(--text);
  box-shadow: var(--shadow-hover);
  overflow: hidden;
}
.xq-dialog::backdrop {
  background: rgba(0, 0, 0, .45);
}
/* dialog 默认是 block，改成纵向 flex —— body 才能吃到剩余高度、列表才能滚 */
.xq-dialog[open] {
  display: flex;
  flex-direction: column;
}

/* 局面库弹窗：**高度固定**。列表条数随页签和过滤词变（551 / 8 / 0 条…），
   高度再不固定的话弹窗会一会长一会短，连它居中的位置都跟着跳。
   定成 78vh / 700px 和上面的 max-height 同口径，只是从「最多这么高」改成「就这么高」；
   条数多少都由 .xq-endgame-list 自己滚（body flex: 1 + 列表 overflow-y: auto）。
   「保存 / 导入」那个弹窗是普通表单流，没有这个类，不受影响。 */
.xq-dialog--picker {
  height: min(78vh, 700px);
}
/* 重命名弹窗：就一行输入，用不着 560px 那么宽 */
.xq-dialog--rename {
  width: min(420px, calc(100vw - 32px));
}

.xq-dialog-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 16px 20px;
  border-bottom: 1px solid var(--border);
}
.xq-dialog-head h2 {
  font-size: 16px;
  font-weight: 600;
}
.xq-dialog-close {
  width: 28px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 14px;
  color: var(--text-secondary);
  background: none;
  border: none;
  border-radius: 6px;
  cursor: pointer;
}
.xq-dialog-close:hover {
  color: var(--text);
  background: var(--bg);
}

/* 底部不留 padding-bottom：列表的滚动条要能一直走到分隔线上 */
.xq-dialog-body {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  padding: 16px 20px 0;
}
.xq-dialog-foot {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 16px 20px;
  border-top: 1px solid var(--border);
}
/* **必须显式再写一条。** UA 的 `[hidden] { display: none }` 会被上面那句
   `display: flex` 覆盖掉（作者样式优先于 UA 样式），于是 el.hidden = true 不起作用。
   和 E8 那个 dialog margin 的坑是同一类：作者样式压掉了 UA 默认值。 */
.xq-dialog-foot[hidden] {
  display: none;
}

.xq-save {
  display: flex;
  gap: 8px;
}
.xq-input {
  flex: 1;
  min-width: 0;
  padding: 8px 10px;
  font-family: var(--font);
  font-size: 13px;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 8px;
}
.xq-input:focus {
  outline: none;
  border-color: var(--accent);
}
.xq-save .btn {
  flex-shrink: 0;
  padding: 8px 14px;
  font-size: 13px;
}

/* === 「保存 / 导入局面」弹窗 === */
/* 和局面库那个弹窗不同：这里是普通表单流，不需要「body 吃掉剩余高度、列表自己滚」，
   而是整块可滚。所以把 flex: 1 / min-height: 0 那套收回来。 */
.xq-dialog-body--form {
  flex: none;
  padding: 16px 20px;
  gap: 18px;
  overflow-y: auto;
}

.xq-io-section {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.xq-io-section h3 {
  font-size: 14px;
  font-weight: 600;
}
.xq-io-hint {
  font-size: 12px;
  line-height: 1.5;
  color: var(--text-secondary);
}
.xq-io-btn {
  align-self: flex-start;
  padding: 6px 12px;
  font-size: 13px;
}
/* 置灰态：「载入到棋盘」在框里就是当前局面时没有意义（见 main.js 的 syncLoadFenBtn）。
   写在这里而不是 global.css —— `.btn` 是全站共用的，改它会影响别的模块。
   第二条要把 `.btn-ghost:hover` 抢回来，否则鼠标一上去又变强调色，看着像能点。 */
.xq-io-btn:disabled {
  opacity: .45;
  cursor: not-allowed;
}
.xq-io-btn:disabled:hover {
  border-color: var(--border);
  color: var(--text);
}
/* 「载入到棋盘」/「存为自定义局面」—— 同一个输入、两个去处，并排摆开。
   flex-wrap 是为了窄屏：挤不下时换行，而不是把字压扁。 */
.xq-io-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

/* 标题行里的「保存 / 导入」按钮，和局面库那个并排 */
.xq-io-open {
  padding: 5px 12px;
  font-size: 13px;
}

/* 弹窗里的提示行（存成功 / 校验失败的原因）。两个弹窗共用 */
.xq-dialog-msg {
  font-size: 13px;
  color: var(--text-secondary);
}
.xq-dialog-msg--error {
  color: var(--danger);
}
.xq-thinking::after {
  content: '';
  display: inline-block;
  width: 6px;
  height: 6px;
  margin-left: 6px;
  border-radius: 50%;
  background: currentColor;
  animation: xq-pulse 1s ease-in-out infinite;
}
@keyframes xq-pulse {
  0%, 100% { opacity: .25; }
  50% { opacity: 1; }
}

/* === 着法列表 === */
/* 面板吃掉侧栏剩余高度、列表再吃掉面板剩余高度 —— 两层 flex 传递之后，
   列表的下沿就落在棋盘下沿上（侧栏与棋盘等高，见 .xq-stage 的注释）。 */
.xq-panel--moves {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
}
/* 标题行：h2 左边，复制按钮右边。h2 自己的 margin-bottom 留着当这一行的下边距 ——
   去掉它的话（比如把 margin 挪到 .xq-moves-head 上）会和 `.panel h2` 打架，
   以后改主题时容易两边都以为对方管着间距。 */
.xq-moves-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
}
/* 「复制」是个次要动作（和列表同属一个面板，不该比「悔棋 / 重开」那些工具栏按钮显眼），
   做成无边框的文字按钮，hover 才浮出色块 —— 同局面库行尾的 ✎ / ✕。
   min-width 是为了「复制」变成「已复制」时右侧不抽动。 */
.xq-moves-copy {
  flex-shrink: 0;
  min-width: 4em;
  padding: 3px 8px;
  font-family: var(--font);
  font-size: 12px;
  color: var(--text-secondary);
  background: none;
  border: none;
  border-radius: 6px;
  cursor: pointer;
}
.xq-moves-copy:hover {
  color: var(--accent);
  background: var(--bg);
}
/* 一步都没走时没东西可复制（见 main.js 的 updateButtons）。第二条要把 hover 抢回来，
   否则鼠标一上去又变强调色，看着像能点 —— 同 .xq-io-btn:disabled:hover */
.xq-moves-copy:disabled {
  opacity: .45;
  cursor: not-allowed;
}
.xq-moves-copy:disabled:hover {
  color: var(--text-secondary);
  background: none;
}
.xq-move-list {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  font-family: var(--mono);
  font-size: 13px;
}
/* 窄屏是单列、侧栏高度由内容决定，flex: 1 会让列表塌成 0 —— 换回固定上限 */
@media (max-width: 860px) {
  .xq-panel--moves {
    flex: none;
  }
  .xq-move-list {
    flex: none;
    max-height: 300px;
  }
}
.xq-move-row {
  display: grid;
  grid-template-columns: 2.4em 1fr 1fr;
  gap: 4px;
  align-items: center;
}
.xq-move-row:nth-child(odd) {
  background: var(--bg);
}
.xq-move-no {
  color: var(--text-secondary);
  text-align: right;
  padding-right: 4px;
}
.xq-move {
  padding: 3px 6px;
  border-radius: 6px;
  cursor: pointer;
  white-space: nowrap;
}
.xq-move:hover {
  background: var(--card-bg);
  color: var(--accent);
}
.xq-move--current {
  background: var(--accent);
  color: var(--on-accent);
  font-weight: 600;
}
.xq-move--current:hover {
  background: var(--accent-hover);
  color: var(--on-accent);
}
/* 「还没有走棋」「还没有自定义局面」这类空状态提示 */
.xq-empty {
  color: var(--text-secondary);
  font-size: 13px;
}
