/*
 * 雪山跑团 demo —— 主舞台六区布局与全局 CSS 变量。
 *
 * 六区（PRD 五「PC Web 主舞台呈现内容」）：顶部状态栏 / 左侧角色栏 /
 * 中央剧情区 / 预测与揭晓区 / 底部剧情时间轴 / 公屏聊天区（时间轴右侧第三列
 * 常驻）。用 CSS Grid 命名区域实现，三列：角色栏 | 剧情区 | 聊天区（聊天区
 * 纵向贯穿中间行与底部行，即「常驻」）；底部行前两列合并为时间轴。
 * 预测与揭晓区不占独立列，而是嵌在中央剧情区内部、紧跟当前回合正文之后
 * （二者同属「当前在看的这一回合」，共用剧情区那一条滚动条）。
 *
 * :root 里的变量是本任务建立的设计基础，供 T17-T19（本页精细渲染）与 T20
 * （web/admin.css，独立文件但明确要求复用这里的变量）共同使用；本任务本身只
 * 需要六区骨架 + 连接提示条，不做精细视觉设计（YAGNI，后续任务按需扩展）。
 */

/*
 * 色板：冷色基调（雪山夜色）+ 暖色强调（篝火/预测高亮）
 *
 * 本版整体在 CIELCh 里重排过一次（见 tests/js/color_audit.mjs，`node
 * tests/js/color_audit.mjs` 可复算全部数字）。起因是用户反馈浅米色预测卡
 * 「突兀」——它 L*97.8 压在 L*12 的面板上，与外壳的对比度高达 15.5:1，等于
 * 在夜色页面里点了一盏探照灯。
 *
 * 解法不是把卡改深色（那张浅卡是用户给参考图认可过的设计语言），而是
 * **两头收**：深壳整体抬一档（页面底 L*6.7→10.1、面板 L*12→16），浅卡整体
 * 压一档（L*97.8→84.9），落差 15.5:1 → 9.97:1（−36%），浅卡仍然是浅卡。
 *
 * 明度阶梯（L*）：页面底 10 / 面板 16 / 预测面板 20 / 序幕卡 23 / 正文卡 26 /
 * 回看态 32.5 / 浅预测卡 85。区分度由「明度阶梯 + 色相分族」双轴承担：
 * 冷蓝紫 h272（页面/面板）、靛蓝 h287（预测深态）、青石板 h195（正文卡）、
 * 暖米 h69（序幕卡/浅卡）。量化判据与实测值见 color_audit.mjs。
 *
 * 前景色 --color-text-dim / --color-accent / --color-success 一并抬亮：旧值在
 * 最亮的深色面（回看态卡）上只有 2.83 / 4.11 / 3.28，本来就不过 WCAG AA。
 * 全站 63 组文字组合现在 0 组不达标、最低 4.73（改前 27 组不达标，见 cff23d2）。
 *
 * 【改这里之前先看这条】色值不是文档，是被 `go test ./tests -run ColorAudit`
 * 真读进去校验的：color_audit.mjs 直接解析本文件，任何一项判据跌破都会红。
 * web/stage/graph.js 里那几个字面量副本也在校验范围内，改 token 记得同步。
 */
:root {
  --color-bg: #171c23;
  --color-panel: #232832;
  --color-panel-alt: #2a313c;
  --color-border: #343c49;
  --color-text: #e7e9ee;
  --color-text-dim: #bac1d0;
  --color-accent: #f2b574;
  --color-accent-dim: #785837;
  --color-danger: #f47471;
  --color-success: #83d299;
  --color-player: #59b1fc;
  --color-guest: #bac1d0;
  /* 切片视图对话区（tasks 7.5）：本人昵称的青色（观众灰白 / 玩家橙黄用
     既有 --color-guest / --color-accent）。深面板底上与 --color-success
     同一明度带，AA 可读。 */
  --color-mine: #6fd8d0;

  /* 地点卡的深色实底（顶条「图标+地名」与底部在场者格）。0.88 不透明度给了
   * 合成结果一个明度上界——底下纵使是纯白雪面，合成后也只到 rgb(39,45,55)。
   * 这条上界是 color_audit 里 locPlate 一组判据的依据，改它必须同步看那份报表。
   *
   * 这两条带如今压的是卡片自己的实底色 .xms-node-face，不再是 AI 贴图：贴图
   * 只铺卡片中段的贴图带（原因见 mapstage.js buildNode 里「取景窗必须是贴图
   * 带」那段——铺满整张卡会让取景被在场人数绑架）。因此实际合成比上面那条
   * 最坏估计更暗、也更稳定，判据只会更宽松。 */
  --color-loc-plate: rgba(10, 16, 28, 0.88);

  --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
    "Microsoft YaHei", sans-serif;
  --radius: 10px;
  --gap: 12px;
  --topbar-h: 52px;
}

* {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  height: 100%;
  background: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-sans);
  font-size: 14px;
}

/* ---------- 连接状态提示条（app.js 用 .visible 切换显示/隐藏） ---------- */

#conn-banner {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 1000;
  display: none;
  padding: 8px 16px;
  background: var(--color-danger);
  /* --color-danger 抬亮到能在深面板上当文字用（旧值 4.18 不过 AA）之后，
     白字压在它上面只剩 3.91——这条横幅改成深墨字，5.92 过 AA。 */
  color: #2f1a18;
  text-align: center;
  font-weight: 600;
  letter-spacing: 0.02em;
}

#conn-banner.visible {
  display: block;
}

/* ---------- 六区 Grid 布局 ----------
   【已下线（c-20260909-demo-b-0915 tasks 7.1）】以下直到「切片视图（slice
   view）」一节之前的大部分规则服务于旧文字流舞台视图（剧情区/预测揭晓区/
   时间轴/世界动态/Pixi 舞台层），index.html 已不再渲染那些容器，规则不可达。
   保留原文有两个原因：①配色门禁 tests/js/color_audit.mjs 的清单仍以这些
   选择器为解析锚点（token 体系延续）；②.predict-card / .predict-option /
   .predict-mask / .chat-* / .char-modal-* / .settlement-table 等样式族被切片
   视图原样复用（玩法面板=竞猜卡设计语言不变）。新视图的布局与增量规则
   集中在文件末尾「切片视图（slice view）」一节，同名选择器以末尾为准。 */

#app {
  display: grid;
  position: relative; /* 左栏拖宽手柄（absolute 脱离 Grid 流）的定位上下文 */
  /* 左栏列宽走 --char-w 变量：拖宽手柄（bindCharResize）只改这一个变量，
     列宽与手柄 left 两处联动；默认 220px 与拖宽前完全一致。 */
  grid-template-columns: var(--char-w, 220px) minmax(0, 1fr) var(--chat-w, 340px);
  /* 中间行用 minmax(0, 1fr) 而不是裸 1fr：1fr 的下限是 auto（内容高度），
     剧情区现在纵向串了正文 + 预测/揭晓两段，内容一长就会把整行顶高、挤掉
     底部时间轴；下限压到 0 才能让超出部分老老实实走剧情区自己的滚动条。 */
  /* 底部行 148px：正好容纳一行双层时间轴节点（回合号 + 标题）+ 横向滚动条，
     旧值 200px 是给药丸按钮留的，节点改大后反而剩一截空白。 */
  grid-template-rows: var(--topbar-h) minmax(0, 1fr) 148px;
  grid-template-areas:
    "topbar   topbar   topbar"
    "chars    story    chat"
    "timeline timeline chat";
  gap: var(--gap);
  height: 100vh;
  padding: var(--gap);
  min-width: 960px;
  overflow-x: auto;
}

.panel {
  background: var(--color-panel);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  overflow: auto;
  padding: var(--gap);
}

.panel-title {
  margin: 0 0 8px;
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-text-dim);
}

/* 通用「空态提示」（JS 动态渲染：暂无角色卡/等待运营开始/AI 推演中……） */
.empty-hint {
  color: var(--color-text-dim);
  font-style: italic;
  padding: 8px 0;
}

#topbar {
  grid-area: topbar;
  display: flex;
  align-items: center;
  padding: 0 var(--gap);
  white-space: nowrap;
  overflow-x: auto;
}

/* 左栏纵向两分区（用户反馈：角色卡一多世界动态被挤没）：panel 自身不再
   滚动（overflow:hidden 覆盖 .panel 的 auto），滚动下放到两个内容区——
   角色区 flex:1 占余量独立滚动，世界动态区钉底、max-height 封顶后内部
   滚动。padding 归零，分区头与内容区各管各的内衬（分区头要横贯整栏）。 */
#char-panel {
  grid-area: chars;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  padding: 0;
}
#char-body {
  flex: 1 1 auto;
  min-height: 0; /* flex 子项默认 min-height:auto 撑破容器，压回 0 才能滚动 */
  overflow-y: auto;
  padding: 10px 12px;
}

/* 分区头（角色 / 世界动态）：升级为明显的分区标题条——字号加大一档、左侧
   accent 橙竖条、独立底色横贯整栏（用户反馈：小字浮在同色背景上边界不明）。
   覆盖 .panel-title 的 uppercase 小灰字形态。 */
.pane-head {
  flex: none;
  margin: 0;
  padding: 9px 12px;
  font-size: 13.5px;
  letter-spacing: 0.04em;
  text-transform: none;
  color: var(--color-text);
  background: var(--color-panel-alt);
  border-left: 3px solid var(--color-accent);
  border-bottom: 1px solid var(--color-border);
}
/* 中央剧情区：当前回合正文 + 紧随其后的预测/揭晓面板，整列共用一条滚动条
   （沿用 .panel 的 overflow:auto）。刻意不拆成「正文自己滚 / 预测区固定」两个
   滚动区：那样长正文会被截在半句话上，而这里正文一次只渲染一个回合（见
   app.js 的 renderStoryPanel），高度可控，连续排版读起来更像一页完整的内容。 */
#story-panel {
  grid-area: story;
}
/* 嵌套在剧情区里的预测/揭晓面板：底色向靛蓝紫**明显**偏移提亮 + 亮边框 +
   左侧暗橙身份边条，和正文一眼分块。首版 #161b2a 与正文通道差 <5 肉眼不可辨
   （用户二次抱怨「底色为啥还是一样的」），此后所有底色都必须能量出分离度。
   本版 #262f50（L*20.1 / h287 靛蓝）vs 面板底 ΔE00 10.1、蓝通道差 30，
   vs 正文卡 ΔE00 22.6、通道差 21，均过判据。只动本面板，
   别的 .panel（顶栏/角色栏/时间轴/聊天）不受影响。
   这套深底只服务「非卡片」状态：等待开始/推演中/作废重生成/终局结算。作答态
   与揭晓态渲染的是浅米色卡片（.predict-card），见下面的 .light-card。 */
#predict-panel {
  position: relative; /* 承载 .predict-mask（paused 覆盖态）的定位上下文 */
  margin-top: var(--gap);
  overflow: visible; /* 覆盖 .panel 的 auto：高度随内容，滚动交给外层剧情区 */
  background: #262f50;
  border-color: #63688d;
  border-left: 3px solid var(--color-accent-dim);
}
/* 浅色卡片态（app.js renderPredictPanel 按渲染结果切类）：外层面板的底/框/
   内衬整个让位给卡片自己，避免「深框套浅卡」多出一圈边。分区标题「预测 /
   揭晓」仍在卡片之上，落在剧情区底色上，与别的分区标题同款。 */
#predict-panel.light-card {
  background: transparent;
  border: none;
  padding: 0;
}
#predict-panel.light-card .predict-mask {
  border-radius: 16px; /* 跟随卡片圆角，不再是面板的 --radius */
}

/* ---------- 待办态：本人此刻还有一票没投（app.js applyPredictAttention 切类）
 *
 * 【为什么要有】用户反馈「小屏上这块可能注意不到」。默认展开只解决了「打开
 * 没打开」，解决不了「打开了也不显眼」。
 *
 * 【为什么它必须会灭】投完、超时、暂停、观众身份——任意一条成立就立刻摘类。
 * 一个不会停的强调等于没有强调：看见它一直亮着的人，第二轮就学会无视它了。
 *
 * 【为什么强调挂在卡片而不是外层面板】作答态一定同时是 .light-card（渲染出
 * 了 .predict-card），那个态把外层面板的底与边整个让给了卡片自己——在外层
 * 加描边根本不会显示。所以描边挂在卡片上，接管它原有的那圈 1px 过渡环。 */
#predict-panel.needs-vote .predict-voting {
  box-shadow: 0 0 0 2px #e8a94a, 0 6px 18px rgba(0, 0, 0, 0.28);
}
/* 标题旁那颗呼吸点：折叠着的时候它就是唯一的可见信号（折叠态标题行上已经有
   「剩余 mm:ss」，两者配套读得通）。 */
#predict-panel.needs-vote .fold-title::after {
  content: "";
  display: inline-block;
  width: 8px;
  height: 8px;
  margin-left: 8px;
  vertical-align: middle;
  border-radius: 50%;
  background: #e8a94a;
  animation: predict-pulse 1.6s ease-in-out infinite;
}
@keyframes predict-pulse {
  0%,
  100% {
    opacity: 1;
    transform: scale(1);
  }
  50% {
    opacity: 0.3;
    transform: scale(0.7);
  }
}

/* ---------- 快捷浮标条：地图底部那条控制行上的两个入口
 *
 * 描边和呼吸点只能解决「不显眼」；小屏上真正的问题是地图下面那两块**根本不在
 * 首屏里**，给看不见的东西加描边毫无意义。
 *
 * 条的 left/top/max-width 由 app.js 的 placeJumpBar 写进行内样式：落点取地图底部
 * 控制行（.xms-log-btn / .xms-audio-bar 那一排）靠左的空位，再把整条夹进「中栏
 * 可视区 ∩ 视口」——那条控制行长在地图里、会跟着地图滚走，落点只说「想去哪」，
 * 夹取才保证「一定看得见」（见 placeJumpBar 注释里几次浏览器实测的数据）。
 * 这里只定形，不定位。
 *
 * 【这里绝不能有 transform】placeJumpBar 写进去的 left/top 就是条的**左上角**，
 * 夹取的算术也是按左上角 + offsetWidth/offsetHeight 做的。留一个 translate 会让
 * 实际位置整体偏移，夹取全部落空——浮标照样能跑出可视区，而且看不出是这里的错。
 * tests/js/predict_attention.mjs 直接读本文件钉了这一条。 */
.stage-jump-bar {
  position: fixed;
  z-index: 40;
  display: flex;
  /* 两颗横排。落点是地图底部那条控制行（.xms-dialogue 等三条文本带给它留的净空，
     现为 padding-bottom:52px）。竖排的两颗叠起来近 80px，会一路向上压回正文，
     所以必须横排；左右顺序＝两颗指向的那两块在中栏里的先后顺序（剧情正文在
     预测/揭晓之前）。
     【本条单行实测高 37px】它比同一行里的按钮（.xms-audio-btn 实测 23px）高
     14px，那条净空的尺寸就是按它定的（见 .xms-dialogue 上方的算式）。改这里的
     padding / font-size 就是在改那条净空的下限，请一并回去复核。
     【flex-wrap 是把双刃】挤不下时向上折行而不是撑破，但折成两行后实测高 81px
     ——静态净空再宽也装不下。这个形态由 app.js 的 placeJumpBar 在运行时接住：
     量到装不下就把整条抬到文本带上方，宁可占掉一行地图也不压正文。 */
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  /* 【width:max-content 不是可有可无的】fixed 定位元素的收缩宽度是按「从 left 到
     视口右边」算的。left 是 JS 写的一个可能相当靠右的点，于是可用宽度只剩几十
     像素，按钮文字会在里面断成三行——430px 视口实测条宽 158、条高 129。
     max-content 让宽度只由内容决定，与 left 落在哪里无关。
     【下面这条 max-width 现在只是兜底】正常路径上 placeJumpBar 会写一个行内
     max-width（这一格到音频控件之间的净空），行内值无条件盖过本条；所以那道
     「不许撑出屏幕」的保险在 JS 里也 Math.min 了一次同样的 100vw-24，别指望本条
     还在起作用。本条只在 JS 算不出正数宽度、把行内值清成空串时接手。 */
  width: max-content;
  max-width: calc(100vw - 24px); /* 极窄屏：宁可文字被截，也不让条撑出屏幕 */
  /* 折行时两颗宽度不等，容器的包围盒里有一块谁都不占的空白。容器留 auto 会让
     这块空白吃掉地图上的鼠标事件（小地图节点、地点漫游都在下面），所以事件只给
     按钮自己。这不替代 applyPredictAttention 里那句 bar.hidden——hidden 还负责
     把整条从无障碍树里摘掉、并停掉倒计时文案的更新。 */
  pointer-events: none;
}
/* 【display 写在这里就必须把 hidden 补回来】浏览器实测（1440×900，:8082 的
   d5f0fc7e 局，paused + 观众身份）：applyPredictAttention 明明把 predict-jump
   置成了 hidden=true，按钮却照样显示、照样呼吸——UA 样式表的 [hidden]{display:none}
   属于 user-agent 层，作者样式里任何一条 display（这里是 flex / inline-flex）
   都无条件盖过它，与选择器权重无关。结果是「提醒必须会灭」这条判据在 JS 里算得
   一丝不苟，在屏幕上一次都没生效过：暂停、投完、观众，浮标一直挂着。
   headless 门禁只看得到 el.hidden 的布尔值，测不出这一层，所以这条规则由
   tests/js/predict_attention.mjs 里那段读 CSS 的断言钉住。 */
.stage-jump-bar[hidden],
.stage-jump[hidden] {
  display: none;
}
.stage-jump {
  pointer-events: auto;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 8px 14px;
  white-space: nowrap;
  border-radius: 999px;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.45);
}
/* 待办款：琥珀 + 呼吸。它表达的是「有事没做，而且会过期」。
 *
 * 新落点（压在对白条 rgba(8,12,22,.96) 上）核过：底色合成后 (25.6,19.0,9.6)~
 * (26.8,20.2,10.8)，对白条底 (7.7,11.5,21.1)~(17.9,21.7,31.3)（分别按贴图纯黑/
 * 纯白两种最坏情况合成）——ΔE00 10.0~11.5（≥8 过），8bit 主导通道差 17.9~20.5，
 * 贴着那把尺的边。它不靠底色分离：琥珀描边 #e8a94a 对同一底的对比度 8.8~9.5，
 * 加上呼吸动效，认出它靠的是这两样。所以这颗**不动**——动了就把「待办」的语汇
 * 冲淡了。 */
.stage-jump-alert {
  border: 1px solid #e8a94a;
  background: rgba(28, 20, 8, 0.88);
  color: #ffd79a;
  animation: predict-pulse 1.6s ease-in-out infinite;
}
.stage-jump-alert:hover {
  background: rgba(48, 34, 12, 0.95);
}
/* 导航款：安静。它只是个入口，不表达任何紧迫性——两颗一起闪就等于都不闪。
 *
 * 【底色为什么必须提亮，2026-08-28】旧值 rgba(12,18,30,.86) 与新落点的底
 * （对白条 rgba(8,12,22,.96)）合成后 ΔE00 只有 1.4、主导通道差只有 5——两者在
 * 屏幕上是同一个颜色。用户反馈「浮标在哪没有看见」时它其实一直渲染着，只是
 * 深色叠深色。只搬位置不改配色，等于把一个看不见的东西挪到另一处继续看不见。
 *
 * 现值 rgba(43,58,86,.96) 合成后 (41.6,56.1,83.4)~(42.0,56.5,83.8)，对同一底
 * ΔE00 13.1~15.7、主导通道差 52.5~62.3（B 通道）——两条都过 color_audit.mjs 那把
 * 尺（ΔE00 ≥ 8 且通道差 ≥ 20）。文字 #e6ecf6 压在它上面对比度 9.8，远过 AA 4.5。
 * 近实底 .96 是故意的：这颗要在一张内容不可控的 AI 贴图上认得出来，透明度越高
 * 底下的画面越参与合成，判据就越不成立。
 *
 * 【它仍然比 .stage-jump-alert 安静】区别不在响度而在语汇：中性蓝灰 vs 琥珀、
 * 无动效 vs 呼吸、font-weight 500 vs 600。两颗一起抢眼等于都不抢眼。
 * tests/js/predict_attention.mjs 里有一条读本文件重算这两个数的断言——配色不该
 * 只有注释保护。 */
.stage-jump-quiet {
  border: 1px solid rgba(255, 255, 255, 0.28);
  background: rgba(43, 58, 86, 0.96);
  color: #e6ecf6;
  font-weight: 500;
}
/* hover 必须比常态更亮：旧的 hover 值（28,38,56）比现在的常态还暗，留着会让
   鼠标压上去时按钮反而「沉下去」。 */
.stage-jump-quiet:hover {
  background: rgba(62, 82, 118, 0.98);
  color: #ffffff;
}
/* 没什么可点的那一档（目前只有「本回合还没有正文」）。
 *
 * 浮标常驻之前，「没得看」是靠**把浮标藏了**表达的；常驻之后藏不掉，这句话得写
 * 出来，写出来就必须同时说明它点不动——一个点下去什么都不发生的按钮比没有按钮
 * 更糟，用户会以为是坏了。
 *
 * 【为什么不用 opacity 变淡】这颗浮标压在一张内容不可控的 AI 贴图上，底色分离度
 * 本来就是踩着尺过的（见上面 .stage-jump-quiet 的算式）；整体调透明会把那笔账
 * 重新变成不合格。所以底色一动不动，只换字色 + 虚线描边 + 光标——三样都不影响
 * 「它在不在」，只影响「它能不能按」。
 * 字色 #b9c4d6 压在同一块底（合成后 41.6,56.1,83.4 ~ 42.0,56.5,83.8）上对比度
 * 6.62~6.66，仍过 AA 4.5（常态字色 #e6ecf6 是 9.82~9.88，两者拉得开）。 */
.stage-jump[disabled] {
  cursor: default;
  color: #b9c4d6;
  border-style: dashed;
}
/* hover 不许再亮起来：亮起来就是在说「可以按」。选择器比 .stage-jump-quiet:hover
   多一个属性选择器，权重压得住，与规则先后无关。 */
.stage-jump[disabled]:hover {
  background: rgba(43, 58, 86, 0.96);
  color: #b9c4d6;
}
.predict-jump-time {
  font-variant-numeric: tabular-nums;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  color: #ffffff;
}

/* 动效只负责「引起注意」，信息本身由「在不在」承载——所以关掉动效不丢任何
   信息，静止的点与静止的浮标照样说明有事待办。 */
@media (prefers-reduced-motion: reduce) {
  #predict-panel.needs-vote .fold-title::after,
  .stage-jump-alert {
    animation: none;
  }
}
#timeline {
  grid-area: timeline;
}
#chat-panel {
  grid-area: chat;
  display: flex;
  flex-direction: column;
  overflow: hidden; /* 覆盖 .panel 的 overflow:auto，滚动交给 #chat-messages 自己 */
  position: relative; /* 收起态竖把手的定位上下文 */
}
#chat-panel .panel-title {
  flex: 0 0 auto;
}

/* ---------- 聊天抽屉（用户反馈：公屏聊天可折叠）：收起时右列缩成 34px
 * 竖把手，中央剧情区经 minmax(0,1fr) 自动扩占腾出的宽度。 ---------- */
.chat-title {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.chat-fold-btn {
  border: 1px solid var(--color-border);
  border-radius: 6px;
  background: var(--color-panel-alt);
  color: var(--color-text-dim);
  font-size: 12px;
  line-height: 1;
  padding: 3px 8px;
  cursor: pointer;
}
.chat-fold-btn:hover {
  color: var(--color-text);
}
#app.chat-collapsed {
  grid-template-columns: var(--char-w, 220px) minmax(0, 1fr) 34px;
}
#app.chat-collapsed #chat-panel {
  padding: 0;
}
/* 收起态：除竖把手外全部隐藏（标题/消息/输入区） */
#app.chat-collapsed #chat-panel > :not(.chat-unfold-handle) {
  display: none;
}
.chat-unfold-handle {
  position: absolute;
  inset: 0;
  width: 100%;
  writing-mode: vertical-rl;
  letter-spacing: 3px;
  font-size: 11px;
  font-weight: 600;
  color: var(--color-text);
  background: var(--color-panel-alt);
  border: none;
  cursor: pointer;
  padding: 12px 0;
}
.chat-unfold-handle:hover {
  color: var(--color-accent);
}
.chat-unfold-handle[hidden] {
  display: none; /* 展开态把手必须让位（作者样式会压过 UA 的 [hidden]，同 .xms-dialogue 教训） */
}

/* ---------- 左栏拖宽手柄（用户反馈：左栏要能左右拖动）：骑在左栏与中央区
 * 之间的 gap 上，8px 命中区居中；只覆盖中间内容行（顶栏之下、时间轴之上）。
 * 拖动期间 body.col-resizing 屏蔽舞台 pointer-events（canvas hover 防误触）
 * 并禁文本选中。 ---------- */
.col-resize-handle {
  position: absolute;
  top: calc(var(--topbar-h) + 2 * var(--gap));
  bottom: calc(148px + 2 * var(--gap));
  left: calc(var(--char-w, 220px) + 1.5 * var(--gap));
  width: 8px;
  margin-left: -4px;
  cursor: col-resize;
  border-radius: 4px;
  z-index: 30;
}
.col-resize-handle:hover,
body.col-resizing .col-resize-handle {
  background: rgba(242, 181, 116, 0.35);
}
body.col-resizing {
  cursor: col-resize;
  user-select: none;
}
body.col-resizing #stage-wrap {
  pointer-events: none;
}
/* 聊天栏拖宽手柄：同款模式镜像到右侧（从右缘按 --chat-w 定位）；聊天列跨
   中间行与底行，手柄一路到底部 padding。抽屉收起态（34px 把手）没有宽度
   可调，隐藏。 */
.col-resize-right {
  left: auto;
  right: calc(var(--chat-w, 340px) + 1.5 * var(--gap));
  margin-left: 0;
  margin-right: -4px;
  bottom: var(--gap);
}
#app.chat-collapsed .col-resize-right {
  display: none;
}

/* ---------- 剧情条目（T17：每回合一条，含标题 + Markdown 叙事） ---------- */

/* 正文卡片独立底色（用户三连反馈的收尾：正文/预测/页面底三块层次分明）：
   青石板 #1a4443（L*26 / h195），与预测区靛蓝（h287）、面板冷蓝紫（h272）
   三分——vs 面板 ΔE00 17.3、G 通道差 28；vs 预测面板 ΔE00 22.6、通道差 21，
   都在判据之上（详见 tests/js/color_audit.mjs）。白字 8.86 / 橙台词 5.96 /
   灰摘要 5.96，三类文字均过 WCAG AA。 */
.story-entry {
  margin: 0 0 16px;
  padding: 10px 12px 14px;
  background: #1a4443;
  border: 1px solid #365858;
  border-radius: 8px;
  border-left: 3px solid transparent;
  line-height: 1.6;
  transition: background-color 0.3s ease, border-left-color 0.3s ease;
}

/* （旧「末条去分隔线」规则随卡片化删除：正文现在是带四边框的独立卡片，
   去 border-bottom 会变成三边框破相。） */

/* 回看态（从时间轴显式点选了某个历史回合）：高亮 + 左侧强调条，与「实时跟随
   当前回合」的常态区分开；剧情区任一时刻只渲染一个回合，见 renderStoryPanel */
.story-entry.highlighted {
  /* 回看高亮：在青石板底上提亮一档（原 --color-panel-alt 灰底会把新底色顶回
     灰色相），左橙边条语义不变。#205454 是 L*32.5，也是全站最亮的深色面——
     --color-text-dim / --color-accent 的下限就是被它卡住的（4.73 / 4.74）。 */
  background: #205454;
  border-left-color: var(--color-accent);
}

.story-entry-head {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin-bottom: 6px;
}

.story-round-title {
  margin: 0;
  font-size: 15px;
  font-weight: 700;
  color: var(--color-text);
}

.story-round-badge {
  font-size: 11px;
  padding: 1px 6px;
  border-radius: 999px;
  border: 1px solid var(--color-border);
  color: var(--color-text-dim);
}
.story-round-badge.status-predicting {
  color: var(--color-accent);
  border-color: var(--color-accent-dim);
}
.story-choice-bridge {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 12px;
  padding-top: 10px;
  border-top: 1px dashed rgba(227, 151, 77, 0.42);
  color: var(--color-text);
  font-size: 13px;
  font-weight: 600;
}
.story-choice-marker {
  flex: 0 0 auto;
  padding: 1px 7px;
  border: 1px solid var(--color-accent-dim);
  border-radius: 999px;
  color: var(--color-accent);
  font-size: 11px;
  letter-spacing: 0.06em;
}
.story-post-choice {
  margin-top: 12px;
  padding: 11px 12px 2px;
  border-radius: 7px;
  background: rgba(8, 20, 24, 0.28);
  border: 1px solid rgba(255, 255, 255, 0.08);
}
.story-post-choice-label {
  margin-bottom: 7px;
  color: var(--color-accent);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.04em;
}
.story-round-badge.status-revealed {
  color: var(--color-success);
  border-color: var(--color-success);
}

.story-round-summary {
  margin: 0 0 8px;
  font-size: 12.5px;
  color: var(--color-text-dim);
}
.story-round-transition {
  margin: 0 0 10px;
  padding-left: 10px;
  border-left: 2px solid var(--color-accent-dim);
  color: var(--color-text-dim);
  font-size: 12.5px;
  line-height: 1.65;
}

/* ---------- 序幕卡片（开局之后、第一回合之前的独立阶段） ----------
 * 与回合卡片刻意区分：暖褐底 + 左侧橙条，一眼看得出「这不是回合」。
 * 它在剧情区最上方，第一回合到达即自动消失（renderStoryPanel 的窗口判定）。
 * 舞台形态下这张卡同样存在（默认折在「剧情文本」里），所以演出被跳过之后
 * 全文仍然读得到。 */
.prologue-entry {
  background: #443425;
  border-color: #5c4a3b;
  border-left-color: var(--color-accent);
}

.prologue-entry .story-round-badge.status-prologue {
  color: var(--color-accent);
  border-color: var(--color-accent-dim);
}

/* 段落之间给足留白：序幕是被逐段朗读的，视觉上也该一段一顿，不是一堵文字墙。 */
.prologue-body p {
  margin: 0 0 10px;
}
.prologue-body p:last-child {
  margin-bottom: 0;
}

.prologue-replay {
  margin-top: 10px;
  padding: 3px 10px;
  font-size: 12px;
  color: var(--color-accent);
  background: transparent;
  border: 1px solid var(--color-accent-dim);
  border-radius: 999px;
  cursor: pointer;
}
.prologue-replay:hover {
  background: rgba(0, 0, 0, 0.2);
}

/* ---------- 世界动态区块（左栏下部分区）：移动/状态/关系等变化在演出
 * 旁白条一闪而过后，这里是事后可查的常驻落点。三轮用户反馈从舞台右缘抽屉
 * 迁入左栏；四轮反馈钉在左栏底部**常驻可见**——不随角色区滚动被挤走，
 * 标题条永远露头，内容多时区内自己滚。上缘 2px 粗分隔与角色区明确分界。 ---------- */
.world-events {
  flex: none;
  display: flex;
  flex-direction: column;
  max-height: 40%;
  border-top: 2px solid var(--color-border);
}
/* 拖过高度后（bindEventsResize 给 #char-panel 挂 events-sized + --events-h）
   世界动态改为固定高，clamp 15%~70% 由 JS 保证；折叠态只剩标题条，固定高
   要让位回 auto。 */
#char-panel.events-sized .world-events {
  height: var(--events-h, 40%);
  max-height: 70%;
}
#char-panel.events-sized .world-events.collapsed {
  height: auto;
}

/* 两分区高度手柄：骑在角色区与世界动态之间，8px 命中带即视觉分隔带；
   margin-top:auto 承接钉底职责（原在 .world-events 上——手柄插进 DOM 序后
   由它把下方整组推到底）。 */
.row-resize-handle {
  flex: none;
  height: 8px;
  margin-top: auto;
  cursor: row-resize;
  border-radius: 4px;
}
.row-resize-handle:hover,
body.row-resizing .row-resize-handle {
  background: rgba(242, 181, 116, 0.35);
}
.row-resize-handle[hidden] {
  display: none; /* 作者样式不压 UA [hidden]（同 .xms-dialogue 教训），且隐藏时钉底交还 .world-events */
}
/* 手柄隐藏（折叠/无世界层）时 .world-events 自己钉底 */
.row-resize-handle[hidden] + .world-events {
  margin-top: auto;
}
body.row-resizing {
  cursor: row-resize;
  user-select: none;
}
body.row-resizing #stage-wrap {
  pointer-events: none;
}
.world-events-list {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  font-size: 12px;
  padding: 8px 12px;
}
.world-events.collapsed .world-events-list {
  display: none;
}
.world-events-empty {
  font-size: 12px;
  color: var(--color-text-dim);
  padding: 6px 0;
}

/* 左栏两个折叠头（角色 / 世界动态）与呈现形态无关，任何形态下都可折叠——
 * 覆盖 .fold-title 默认「箭头只在 body.stage-on 出现」的预测区专属规则。
 * .collapsed 时箭头右转指示「已收起」。 */
#char-fold,
#world-events-fold {
  cursor: pointer;
  user-select: none;
}
#char-fold .fold-arrow,
#world-events-fold .fold-arrow {
  display: inline-block;
}
#char-fold.collapsed .fold-arrow,
#world-events-fold.collapsed .fold-arrow {
  transform: rotate(-90deg);
}

.story-world-event {
  display: flex;
  gap: 7px;
  font-size: 12.5px;
  line-height: 1.75;
  color: var(--color-text-dim);
}
.story-world-event .ev-icon {
  flex: none;
}

/* ---------- Markdown 渲染产物（renderMarkdown 输出，narrative/real_action 专用） ---------- */

.md-body p {
  margin: 0 0 10px;
}
.md-body p:last-child {
  margin-bottom: 0;
}
.md-body h1,
.md-body h2,
.md-body h3 {
  margin: 12px 0 6px;
  font-weight: 700;
  line-height: 1.4;
}
.md-body h1 {
  font-size: 1.15em;
}
.md-body h2 {
  font-size: 1.08em;
}
.md-body h3 {
  font-size: 1.02em;
  color: var(--color-text-dim);
}
.md-body ul {
  margin: 6px 0 10px;
  padding-left: 22px;
}
.md-body li {
  margin-bottom: 4px;
}
.md-body strong {
  color: var(--color-accent);
}
.md-body strong em,
.md-body em strong {
  color: var(--color-accent);
}

/* ---------- 左侧角色栏（T18：焦点角色置顶放大 + 其余竖排小方块） ---------- */

#char-body {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.char-card {
  appearance: none;
  width: 100%;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: 10px;
  background: var(--color-panel-alt);
  color: var(--color-text);
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: border-color 120ms ease, background 120ms ease, transform 120ms ease;
}
.char-card:hover {
  border-color: var(--color-accent-dim);
  background: color-mix(in srgb, var(--color-panel-alt) 88%, var(--color-accent) 12%);
  transform: translateY(-1px);
}
.char-card:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

.char-card-focus {
  border-color: var(--color-accent);
  box-shadow: 0 0 0 1px var(--color-accent-dim);
  padding: 14px;
}
.char-card-focus .char-name {
  font-size: 16px;
  color: var(--color-accent);
}

.char-rest {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.char-card-small {
  padding: 6px 10px;
}
.char-card-small .char-name {
  font-size: 12.5px;
}
.char-card-heading {
  min-width: 0;
  flex: 1 1 auto;
}
.char-card-profession,
.char-card-meta {
  color: var(--color-text-dim);
  font-size: 11px;
  line-height: 1.45;
}
.char-card-profession {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}
.char-card-meta {
  margin-top: 7px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.char-card-action {
  margin-left: auto;
  flex: 0 0 auto;
  font-size: 11px;
  color: var(--color-accent);
  white-space: nowrap;
}

/* 角色头像（§26）：角色形象图片与「姓名首字」占位共用同一个盒子尺寸，保证角色形象
   前后就绪时卡片布局不跳动。 */
.char-head {
  display: flex;
  align-items: center;
  gap: 8px;
}

.char-avatar {
  flex: 0 0 auto;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  overflow: hidden;
  background: var(--color-panel);
  border: 1px solid var(--color-border);
  display: flex;
  align-items: center;
  justify-content: center;
}
.char-avatar-focus {
  width: 52px;
  height: 52px;
  border-color: var(--color-accent);
}
.char-avatar-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* 角色形象是竖版全身构图，圆形头像里居中偏上更容易框住脸 */
  object-position: center 20%;
  display: block;
}
.char-avatar-fallback {
  font-weight: 700;
  color: var(--color-text-dim);
  font-size: 14px;
}
.char-avatar-focus.char-avatar-fallback {
  font-size: 28px;
}

.char-name {
  font-weight: 700;
  margin-bottom: 4px;
}

/* ---------- 角色实时状态（用户反馈：每角色展示身体/体力/情绪/位置/装备/
 * 关系）：数据来自 world 权威投影。小卡收起态一行双迷你条速览；展开态
 * 「实时状态」网格块，垫在设定字段（职业/背景…）之前。迷你条复用舞台
 * HUD 的 .hud-bar 语义色（红=身体 绿=体力）。 ---------- */
.char-live-bars {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 5px;
}
.char-live {
  margin-top: 6px;
  padding: 6px 8px;
  background: var(--color-panel-alt);
  border-radius: 6px;
  font-size: 12px;
  line-height: 1.55;
}
.char-live-title {
  font-size: 11px;
  font-weight: 700;
  color: var(--color-accent);
  margin-bottom: 3px;
}
.char-live-row {
  color: var(--color-text);
  margin-top: 2px;
}
.char-live-label {
  color: var(--color-text-dim);
  margin-right: 6px;
}
.char-live-label::after {
  content: "：";
}
.char-live-bars-inline {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  margin-top: 0;
  vertical-align: middle;
}

/* 卡内「角色设定」长文子折叠头（默认收起——设定太长撑爆左栏是用户原话）。 */
/* ---------- 角色卡：贴着左侧角色区浮出的一张卡，**不是全屏 modal**。 ----------
   【2026-09-15 按 PRD 正文改；别当成回归改回去】PRD「五、前端需求 · 角色区」
   原文：「点击角色后，在**左侧角色区旁**展示角色卡」「同一时间仅展示一张角色卡；
   **点击其他角色时替换内容**」；原型（generated-slices-review-v1.html）对应
   .role-detail{position:absolute;z-index:30;width:380px}，贴着角色列表浮出。
   此前这里是 position:fixed;inset:0 的全屏遮罩（940 宽卡片居中 + rgba(3,7,14,.78)
   + blur(7px) 盖满整屏）。它不只是「样子不对」——遮罩把其他角色按钮整个吃掉了：
   在别的角色位置做 elementFromPoint 命中的是遮罩，点下去只会关掉当前卡，PRD
   那条「点击其他角色替换内容」在 UI 上根本不可达。
   现在没有遮罩层，所以「点卡外区域关闭」由 app.js 的 document 级 click 监听实现
   （同原型），卡内与角色按钮上 stopPropagation。 */
.char-modal[hidden] {
  display: none;
}
.char-modal {
  position: fixed;
  /* 贴左侧角色区右缘：角色列位于 padding(--gap) 之后、宽 240px，列间距 --gap。
     顶对齐角色区顶部（--topbar-h + 两个 --gap）。这两个数跟 #app.slice-app 的
     grid 定义走，改栅格时一起改。 */
  left: calc(var(--gap) * 2 + 240px);
  top: calc(var(--topbar-h) + var(--gap) * 2);
  z-index: 60;
  width: min(380px, calc(100vw - var(--gap) * 3 - 240px));
  max-height: calc(100vh - var(--topbar-h) - var(--gap) * 3);
  display: flex;
}
.char-modal-dialog {
  position: relative;
  width: 100%;
  max-height: 100%;
  overflow: auto;
  border: 1px solid var(--color-border);
  border-radius: 14px;
  background: linear-gradient(150deg, #17202d 0%, #101722 72%);
  color: var(--color-text);
  box-shadow: 0 22px 60px rgba(0, 0, 0, 0.5);
  outline: none;
}
.char-modal-close {
  position: sticky;
  z-index: 2;
  top: 12px;
  float: right;
  margin: 12px 12px -42px 0;
  width: 30px;
  height: 30px;
  border: 1px solid var(--color-border);
  border-radius: 50%;
  background: rgba(10, 16, 25, 0.9);
  color: var(--color-text);
  font-size: 26px;
  line-height: 1;
  cursor: pointer;
}
.char-modal-close:hover,
.char-modal-close:focus-visible {
  border-color: var(--color-accent);
  color: var(--color-accent);
  outline: none;
}
.char-modal-content {
  padding: 18px;
}
.char-modal-hero {
  display: flex;
  align-items: center;
  gap: 14px;
  min-height: 72px;
  padding: 0 38px 16px 0;
  border-bottom: 1px solid var(--color-border);
}
.char-modal-avatar.char-avatar-focus {
  width: 66px;
  height: 66px;
  box-shadow: 0 0 0 4px rgba(227, 151, 77, 0.1);
}
.char-modal-eyebrow {
  margin-bottom: 5px;
  color: var(--color-accent);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}
.char-modal-identity h2 {
  margin: 0;
  font-size: 21px;
  line-height: 1.2;
}
.char-modal-profession {
  margin-top: 6px;
  color: var(--color-text-dim);
  font-size: 13px;
}
/* 单列：卡宽 380（原型 .role-detail 同宽），两列塞不下。 */
.char-modal-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 12px;
  margin-top: 16px;
}
.char-modal-section {
  min-width: 0;
  padding: 14px;
  border: 1px solid var(--color-border);
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.025);
}
.char-modal-section h3 {
  margin: 0 0 13px;
  color: var(--color-accent);
  font-size: 14px;
  letter-spacing: 0.04em;
}
.char-modal-section-body {
  color: var(--color-text);
  font-size: 14px;
  line-height: 1.78;
}
.char-modal-section-body p {
  margin: 0 0 12px;
  white-space: pre-line;
}
.char-modal-section-body p:last-child {
  margin-bottom: 0;
}
.char-modal-section-body ul {
  margin: 0;
  padding-left: 20px;
}
.char-modal-section-body li {
  margin: 0 0 9px;
  padding-left: 3px;
}
.char-modal-section-body li:last-child {
  margin-bottom: 0;
}
.char-modal-span-2 {
  grid-column: 1 / -1;
}
.char-modal-live-section {
  background: rgba(227, 151, 77, 0.055);
}
.char-modal-secret {
  border-color: rgba(205, 87, 102, 0.45);
  background: rgba(205, 87, 102, 0.065);
}
.char-modal-secret h3 {
  color: #ef9aa6;
}
.char-secret-block {
  padding: 14px 0;
  border-top: 1px solid rgba(239, 154, 166, 0.16);
}
.char-secret-block:first-child {
  padding-top: 0;
  border-top: 0;
}
.char-secret-block:last-child {
  padding-bottom: 0;
}
.char-secret-block h4 {
  margin: 0 0 7px;
  color: #f4bbc3;
  font-size: 13px;
}
.char-modal-section .char-live {
  margin: 0;
  padding: 0;
  background: transparent;
  font-size: 13px;
  line-height: 1.7;
}
.char-modal-section .char-live-row {
  display: inline-flex;
  align-items: center;
  margin: 0 24px 7px 0;
}

/* 窄屏：角色列 240px + 卡 380px 已经超出视口，退回贴左边缘的一张全宽卡
   （仍不是全屏遮罩——不许再把其他角色按钮盖住）。 */
@media (max-width: 720px) {
  .char-modal {
    left: var(--gap);
    width: calc(100vw - var(--gap) * 2);
  }
  .char-modal-span-2 {
    grid-column: auto;
  }
}

/* ---------- Actor Agent 判断记录浮层 ---------- */
body.actor-modal-open {
  overflow: hidden;
}
.actor-modal[hidden] {
  display: none;
}
.actor-modal {
  position: fixed;
  inset: 0;
  z-index: 10020;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: rgba(2, 7, 15, 0.82);
  backdrop-filter: blur(7px);
}
.actor-modal-dialog {
  position: relative;
  width: min(1080px, 100%);
  max-height: calc(100vh - 48px);
  overflow: auto;
  border: 1px solid #34475f;
  border-radius: 18px;
  background: linear-gradient(160deg, #172334, #0e1622 76%);
  color: var(--color-text);
  box-shadow: 0 28px 90px rgba(0, 0, 0, 0.62);
  outline: none;
}
.actor-modal-content {
  padding: 30px;
}
.actor-modal-content > h2 {
  margin: 0;
  padding-right: 48px;
  font-size: 25px;
}
.actor-modal-note {
  margin: 8px 48px 20px 0;
  color: var(--color-text-dim);
  font-size: 12px;
  line-height: 1.6;
}
.actor-modal-close {
  position: sticky;
  z-index: 2;
  top: 14px;
  float: right;
  margin: 14px 14px -50px 0;
  width: 38px;
  height: 38px;
  border: 1px solid #40556f;
  border-radius: 50%;
  background: rgba(9, 16, 27, 0.92);
  color: var(--color-text);
  font-size: 26px;
  line-height: 1;
  cursor: pointer;
}
.actor-modal-close:hover,
.actor-modal-close:focus-visible {
  border-color: #65b8ed;
  color: #79c8f8;
  outline: none;
}
.actor-decision-open {
  align-self: flex-start;
  margin: 0 0 14px;
  padding: 7px 12px;
  border: 1px solid #5684a5;
  border-radius: 999px;
  background: #203b51;
  color: #d9efff;
  font: inherit;
  font-size: 12px;
  font-weight: 700;
  cursor: pointer;
}
.actor-decision-open:hover,
.actor-decision-open:focus-visible {
  background: #2a516e;
  outline: 2px solid rgba(101, 184, 237, 0.32);
  outline-offset: 2px;
}
/* 预测阶段置灰（2026-09-02：揭晓前不可看 Actor 判断，防泄底）。原因随按钮的
 * title 提示给出（app.js actorDecisionButton）。 */
.actor-decision-open:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}
.actor-decision-open:disabled:hover {
  background: #203b51;
  outline: none;
}
.actor-modal-tabs {
  display: flex;
  gap: 8px;
  overflow-x: auto;
  margin-bottom: 18px;
  padding-bottom: 3px;
}
.actor-modal-tab {
  flex: 0 0 auto;
  padding: 8px 14px;
  border: 1px solid #34475f;
  border-radius: 8px;
  background: #111c2a;
  color: var(--color-text-dim);
  font: inherit;
  cursor: pointer;
}
.actor-modal-tab.active {
  border-color: #65b8ed;
  background: #203b51;
  color: #eef8ff;
}
.actor-final-choice,
.actor-trace-section,
.actor-candidate {
  border: 1px solid #2d4058;
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.025);
}
.actor-final-choice {
  margin-bottom: 16px;
  padding: 18px;
  border-color: #4e7fa3;
  background: rgba(63, 139, 188, 0.09);
}
.actor-final-choice h3,
.actor-trace-section h3,
.actor-candidates-title {
  margin: 0 0 10px;
  color: #78c5f4;
  font-size: 14px;
}
.actor-final-choice p {
  margin: 6px 0;
  line-height: 1.68;
}
.actor-rationale {
  margin-top: 13px !important;
  padding-top: 12px;
  border-top: 1px solid rgba(120, 197, 244, 0.18);
  color: var(--color-text-dim);
}
.actor-appraisal-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px;
}
.actor-trace-section {
  padding: 16px;
}
.actor-trace-section ul {
  margin: 0;
  padding-left: 20px;
  font-size: 13px;
  line-height: 1.65;
}
.actor-trace-section li {
  margin-bottom: 7px;
}
.actor-motives {
  border-color: #635784;
  background: rgba(117, 91, 157, 0.08);
}
.actor-motives h3 {
  color: #c4a8eb;
}
.actor-trace-empty {
  margin: 0;
  color: var(--color-text-dim);
}
.actor-candidates-title {
  margin-top: 20px;
}
.actor-candidates {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px;
}
.actor-candidate {
  padding: 15px;
}
.actor-candidate.selected {
  border-color: #58b57b;
  background: rgba(64, 157, 99, 0.1);
  box-shadow: 0 0 0 1px rgba(88, 181, 123, 0.2);
}
.actor-candidate-head {
  margin-bottom: 8px;
  color: #94a9c0;
  font-size: 12px;
  font-weight: 700;
}
.actor-candidate.selected .actor-candidate-head {
  color: #7bd49b;
}
.actor-candidate p {
  margin: 6px 0;
  font-size: 13px;
  line-height: 1.62;
}
.actor-candidate-meta {
  color: var(--color-text-dim);
  font-size: 12px !important;
}
.actor-modal-error {
  padding: 15px;
  border: 1px solid rgba(220, 80, 95, 0.45);
  border-radius: 10px;
  background: rgba(220, 80, 95, 0.09);
  color: #ffb5be;
}

@media (max-width: 720px) {
  .actor-modal {
    align-items: stretch;
    padding: 8px;
  }
  .actor-modal-dialog {
    max-height: calc(100vh - 16px);
    border-radius: 12px;
  }
  .actor-modal-content {
    padding: 22px 14px;
  }
  .actor-appraisal-grid,
  .actor-candidates {
    grid-template-columns: 1fr;
  }
}

/* ---------- 预测与揭晓区（T17） ---------- */

.review-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  background: var(--color-panel-alt);
  border: 1px solid var(--color-border);
  border-radius: 6px;
  padding: 6px 10px;
  margin-bottom: 10px;
  font-size: 12.5px;
  color: var(--color-accent);
}

/* ---------- 预测/揭晓卡片（浅色焦点区） ----------
 *
 * 一张暖米色圆角大卡片压在深色舞台上：标题问句 + 右上角深棕倒计时药丸 +
 * 2×2 选项网格。这是全站唯一的浅底区域，所以卡片里**每一处**颜色都显式写死
 * ——:root 那套色板是给深色主题准备的（--color-text 是浅灰），继承进来就是
 * 浅底浅字。卡片自己的色令牌集中挂在 .predict-card 上，语义态（选中/正确/
 * 选错/超时）只换令牌不改结构，圆角、描边粗细、留白节奏四态一致。
 *
 * 本版整体压暗提暖（用户反馈「突兀」）。**设计语言不变**——仍然是浅米色
 * 大卡 + 2×2 选项网格 + 倒计时药丸，就是用户给参考图认可的那张；变的只有
 * 亮度与色温：卡底 L*97.8 → 84.9，与深色外壳的对比度 15.5:1 → 9.97:1。
 *
 * 「压暗提暖别走成发黄发脏」这条用数守：卡底色相 h74.9 → 71.9（**离开**黄
 * 相、朝橙相走了 3°，不是奔着黄去的），彩度 C*3.1 → 7.9（个位数，远低于
 * 「发黄」的 C*>20 区间）；发脏的典型是 L*60~75 配 C*15~25 的卡其带，本版
 * L*84.9 / C*7.9 不在那个带里。
 *
 * 顺带把这张卡本来就欠的账还了：--pc-ink-dim 旧值只有 3.9:1（低于 AA），
 * 白字压橙/绿药丸只有 3.36 / 3.39，禁用按钮 3.79。全卡最低现在 4.87。
 */
.predict-card {
  --pc-bg: #dfd2c6; /* 卡片底：暖米色（压暗一档，L*84.9） */
  --pc-surface: #ece4dc; /* 选项卡片 / 内嵌块底：比卡底高 L*6，比旧版更分得开 */
  --pc-line: #c7b6a8; /* 浅灰暖描边 */
  --pc-ink: #302621; /* 正文深色 */
  --pc-ink-dim: #64544b; /* 次要说明文字（旧 #8a7a70 只有 3.9:1，不过 AA） */
  --pc-accent: #b54c31; /* 选中橙：压到白字能过 AA 的那一档（3.36 → 5.19） */
  --pc-accent-soft: #f0d4c9;
  /* 压在 --pc-accent-soft 上的橙色**文字**色（2026-09-15 补，补的是一个缺口：
     --pc-ok / --pc-bad 早就各有一个 *-ink 变体，橙色一直没有）。--pc-accent 本身
     压在 --pc-accent-soft 上只有 3.70:1，够不上正文 AA 的 4.5——而选中态的主行
     是 14.5px 粗体，按 WCAG 还不算「大字」（要 18.66px 粗才算）。压到 #953c26
     后是 5.07:1，与 --pc-ok-ink(5.10) / --pc-bad-ink(5.16) 站在同一档上。 */
  --pc-accent-ink: #953c26;
  --pc-timer: #663426; /* 倒计时药丸深棕 */
  --pc-ok: #247c46; /* 揭晓：正确项（白字 3.39 → 5.19） */
  --pc-ok-soft: #cae3cf;
  --pc-ok-ink: #126736;
  --pc-bad: #b9423f; /* 揭晓：自己选错的那项 */
  --pc-bad-soft: #f6d2cd;
  --pc-bad-ink: #993533;

  background: var(--pc-bg);
  border-radius: 16px;
  padding: 18px 20px 20px;
  color: var(--pc-ink);
  /* 外圈过渡环：深壳(L*16) → 浅卡(L*85) 本来是一步 ΔL*69 的硬边，插一圈
     L*45 的暖褐把它拆成 29 + 40 两步，边缘不再是刀切的。用 box-shadow 而不是
     border，免得多出 1px 撑开布局。 */
  box-shadow: 0 0 0 1px #7c6756, 0 6px 18px rgba(0, 0, 0, 0.28);
  /* 选项网格要按「剧情区这一列有多宽」决定几列，而不是按窗口宽度——左栏/
     聊天栏都能拖宽、聊天还能整列收起，同一个窗口宽度下这一列可以差出几百
     像素。container query 正好问的就是容器自身宽度。 */
  container-type: inline-size;
}

/* 头一行：问句占余量，右侧钉一颗状态药丸（作答态倒计时 / 揭晓态徽章）。 */
.predict-card-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 14px;
  margin-bottom: 16px;
}
.predict-prompt {
  flex: 1 1 auto;
  min-width: 0;
}
.predict-focus-character {
  position: relative;
  z-index: 6; /* 暂停遮罩之上仍能看见本轮在预测谁 */
  display: inline-flex;
  align-items: center;
  margin-bottom: 7px;
  padding: 4px 9px;
  border-radius: 999px;
  background: #c9b9aa;
  color: #554239;
  font-size: 11.5px;
  font-weight: 800;
  letter-spacing: 0.02em;
}
.predict-question {
  font-size: 17.5px;
  font-weight: 700;
  line-height: 1.45;
  color: var(--pc-ink);
}

/* 倒计时药丸：深棕全圆底 + 白色等宽数字（tabular-nums 防止秒数跳动时药丸
   宽度抖动），内边距按参考稿放宽。 */
.predict-countdown {
  flex: none;
  padding: 7px 16px;
  border-radius: 999px;
  background: var(--pc-timer);
  color: #ffffff;
  font-size: 16px;
  font-weight: 700;
  line-height: 1.25;
  letter-spacing: 0.06em;
  font-variant-numeric: tabular-nums;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

/* 超时态：倒计时归零后 tickCountdown 给 .predict-voting 切 .expired（只切类，
   不重建 DOM）——药丸褪成灰棕，选项下方显出截止说明。选项本身不禁用：客户端
   与服务端的钟可能差一两秒，本地抢先锁死会白白吃掉玩家最后一次提交机会，
   真过期由服务端拒绝并走 .predict-error 提示。 */
.predict-voting.expired .predict-countdown {
  background: #76685f; /* 白字旧值只有 3.28，压到 5.36 */
}
.predict-expired-note {
  display: none;
  margin: 8px 0 0; /* 倒计时行改到面板底部后，这句提示跟着它走 */
  font-size: 12.5px;
  color: var(--pc-ink-dim);
}
.predict-voting.expired .predict-expired-note {
  display: block;
}
/* 已截止就别再说「点击后仍可改选」了，两句话打架。 */
.predict-voting.expired .predict-option-note {
  display: none;
}

/* 选项区：**一律单列竖排**（2026-09-15 需求方裁决，原话「这个选项变成 竖着的
   4 条」）。**别当成回归改回去。**

   此前是 2 列网格（`repeat(2, minmax(0, 1fr))`），4 项走 2×2、3 项由
   `[data-count="3"]` 特例改单列。那条特例存在的理由是：2 列放 3 项会空出一个
   格子，视觉上等于「有第四个选项但没内容」；让第 3 项跨两列又会让它的点击区域
   比另外两个大一倍。

   单列之后那条特例连同它要躲的两个坑一起消失了——**没有空格子可留，也没有跨列
   可跨**，所以 data-count 的分档规则整个删掉，不再需要 app.js 写那个属性来分档
   （属性本身还在，别的地方可能拿它做别的事，删规则不删属性）。

   min-height 也跟着去掉：那个 88px 是给**并排**卡片对齐用的等高留白，竖排之后
   它只会让短选项凭空多出一截空白。选项高度回到内容高度。**加副文案那次也别把
   它带回来**——两行的高度本来就参差（有的项没有副文案），拉齐等于给没副文案的
   那几项各补一截空白。

   【2026-09-15 第二改：选项间距 = 主副两行间距的数倍，这是「两行属于同一项」的
   唯一凭据】选项本身已经没有边框和底色（见 .predict-option），四项八行之间就只
   剩留白在分组：项与项之间 18px（本 gap，另加各自 8px 的上下内距），主行与副行
   之间 4px（.predict-option-detail 的 margin-top）。**两个数不能拉平**——拉平
   之后一道四选题在屏幕上就是八条并列的行，谁跟谁一组全靠猜。改任何一个数之前
   先看 tests/js/slice_quiz_option_parts.mjs 里那条比值断言。 */
.predict-options {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 18px;
  margin-bottom: 14px;
}
/* 【2026-09-15 改：选项不再是卡片，是纯文字块】需求方给的参考图里，未选中的选项
   没有边框、没有底色，只有文字和留白；此前那圈 `1px solid var(--pc-line)` +
   `var(--pc-surface)` 底的卡片形态**是被这次裁决替换掉的，别当成回归改回去**。

   底色与描边都撤掉之后，只有三种态还需要「一眼看出来」：选中、揭晓答对、揭晓
   答错。它们全部改用「淡底色 + 文字色」表达（见下面各自的规则），不再用描边——
   描边一旦回来，未选中态就又变回卡片了。 */
.predict-option {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: flex-start;
  width: 100%;
  text-align: left;
  background: transparent;
  border: none;
  border-radius: 10px;
  padding: 8px 10px;
  color: var(--pc-ink);
  font-family: inherit;
  cursor: pointer;
}
button.predict-option:hover:not(:disabled) {
  background: #efe7df;
}
.predict-option:disabled {
  cursor: default;
}
.predict-option-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  width: 100%;
}
/* 「A · 独自穿过雪桥」：字母 + 间隔点 + 正文，整体深色加粗，一个文本节点。
   这是选项的**主行**——上游 optionParts[i].label，「这个人做了什么」。 */
.predict-option-text {
  font-size: 14.5px;
  font-weight: 700;
  line-height: 1.55;
  color: var(--pc-ink);
}
/* 选项的**副行**（2026-09-15，上游新增 optionParts 之后加的）：
   optionParts[i].detail，「这个做法具体怎么发生」——一个动作、一句原话、或者
   它的代价。降一号字号（主行 14.5px → 12.5px）、常规字重、次级灰，与主行拉开
   层级但仍属同一项。

   margin-top 只有 4px，而选项之间的 .predict-options gap 是 18px：**这个数量级
   差就是「两行属于同一项」的全部依据**，两个数拉平了分组就没了（.predict-options
   的注释里同一条账记了两遍，改任一处都先看另一处）。

   【不复用 .predict-option-note】那个类是旧文字流玩法留下的死 CSS——app.js 里
   零引用，只有 tests/js/palette_preview.html 还在摆样。副文案另起一个语义明确的
   名字，免得两件事共用一条规则之后谁也不敢动。

   这里**故意不写 display**：副行只在 detail 非空时才由 app.js 创建（缺席就没有
   这个节点，不是造一个空节点再藏起来），因此不存在被置 hidden 的路径，也就不必
   配 [hidden] 伴生规则。 */
.predict-option-detail {
  margin-top: 4px;
  font-size: 12.5px;
  font-weight: 400;
  line-height: 1.5;
  color: var(--pc-ink-dim);
}
.predict-option-note {
  margin-top: 6px;
  font-size: 12px;
  font-weight: 400;
  line-height: 1.5;
  color: var(--pc-ink-dim);
}
.predict-option-outcome {
  margin-top: 7px;
  font-size: 12.5px;
  line-height: 1.55;
  color: #5f534b;
}
.predict-option-risks {
  margin-top: 4px;
  font-size: 12px;
  line-height: 1.5;
  color: #8a4c3e;
}
/* 三种语义态：选中 / 揭晓答对 / 揭晓答错。

   【2026-09-15 改：从「描边 + 同色外扩投影」换成「淡底色 + 文字色」】选项本身
   已经不是卡片了（见 .predict-option），没有描边可以变色——此前那套
   `border-color + box-shadow: 0 0 0 1px` 的写法整套作废，**别当成回归改回去**。

   换法的硬要求是：**答对答错必须仍然一眼可辨**。样式不好看是审美问题，答对答错
   分不清是玩法坏了。所以每一种态都压三条独立的线索，缺一条也还认得出来：
     ①整块的淡底色（--pc-ok-soft / --pc-bad-soft，两个色相隔得开）；
     ②主行文字色（--pc-ok-ink / --pc-bad-ink，灰度截图下仍有明暗差）；
     ③.predict-option-tag 那颗角标的显式文字（「正确答案」/「你的选择」，见下）。

   【选中态为什么不用参考图里的绿，2026-09-15】参考图的选中态是绿底绿字，但本页
   的绿**已经被占用**：--pc-ok 是「揭晓时这一项答对了」。选中态也用绿，玩家在
   揭晓之前每点一次选项都像是被告知猜对了，两个语义当场糊在一起。所以选中仍然走
   --pc-accent / --pc-accent-soft（橙），只把参考图里「淡底 + 强调色文字」的做法
   照搬过来。**照着参考图把它改回绿的之前先读这一段。**

   .wrong 排在 .selected 之后是有意的：自己选错的那一项两个类都挂着（app.js
   同时 add("selected") 和 add("wrong")），同权重下后来者胜——揭晓之后该看见的是
   「你选错了」，不是「你选了这个」。 */
.predict-option.selected {
  background: var(--pc-accent-soft);
}
.predict-option.selected .predict-option-text {
  color: var(--pc-accent-ink);
}
.predict-option.reveal-option {
  cursor: default;
}
.predict-option.correct {
  background: var(--pc-ok-soft);
}
.predict-option.correct .predict-option-text {
  color: var(--pc-ok-ink);
}
.predict-option.wrong {
  background: var(--pc-bad-soft);
}
.predict-option.wrong .predict-option-text {
  color: var(--pc-bad-ink);
}
/* 角标（正确答案 / 你的选择）：跟着语义色走，中性态是暖米底深棕字。 */
.predict-option-tag {
  flex: none;
  padding: 2px 9px;
  border-radius: 999px;
  background: #d2bdad;
  color: var(--pc-timer);
  font-size: 11px;
  font-weight: 700;
  line-height: 1.7;
  white-space: nowrap;
}
.predict-option.selected .predict-option-tag {
  background: var(--pc-accent);
  color: #ffffff;
}
.predict-option.correct .predict-option-tag {
  background: var(--pc-ok);
  color: #ffffff;
}
.predict-option.wrong .predict-option-tag {
  background: var(--pc-bad);
  color: #ffffff;
}
.reveal-option-meta {
  margin-top: 6px;
  font-size: 12px;
  line-height: 1.5;
  color: var(--pc-ink-dim);
}
.predict-option.correct .reveal-option-meta {
  color: var(--pc-ok-ink);
}
.predict-option.wrong .reveal-option-meta {
  color: var(--pc-bad-ink);
}

/* 【2026-09-15 随「选项一律单列竖排」的裁决一并删除】这里原本有一条
   `@container (max-width: 460px)`：窄列时把两列网格退回单列、并把 88px 等高
   卡片的 min-height 归零。它要解决的是「两列挤到 150px 宽会把选项文案折成三行」。

   基础规则已经改成单列、min-height 也已去掉之后，这条覆盖做的事和基础规则**逐字
   相同**，留着只会让下一个人以为还存在两套排布、改宽屏时忘了改窄屏。删掉。

   **如果哪天把两列改回来，这条也要一起回来**——那时窄列的坑会原样复现，
   而它当初的成立理由（container query 不加特异性、必须排在基础规则之后；
   浏览器不支持时不进规则、退化成与宽屏一致）一个字都没变。 */

.predict-submit-btn {
  width: 100%;
  padding: 12px;
  border-radius: 12px;
  border: none;
  background: var(--pc-accent);
  color: #ffffff;
  font-family: inherit;
  font-size: 14.5px;
  font-weight: 700;
  cursor: pointer;
}
.predict-submit-btn:hover:not(:disabled) {
  background: #a03f26;
}
.predict-submit-btn:disabled {
  background: #d0beb0;
  color: #55463e; /* 禁用也得读得出来：旧组合 #7d6d64/#ecdfd4 只有 3.79，本版 5.01 */
  cursor: default;
}
/* 已提交态（PRD 附录 2「提交按钮」：「已提交：显示「已提交」，选项不可修改」）。
   它也是 :disabled，但「已经办成了」和「还不能点」是两种意思，不该长一个样——
   借用「已揭晓」徽章那对已验过对比度的配色（var(--pc-ok) / #ffffff）。 */
.predict-submit-btn.submitted:disabled {
  background: var(--pc-ok);
  color: #ffffff;
}

.predict-error,
.chat-error {
  color: var(--color-danger);
  font-size: 12px;
  margin-top: 6px;
}
/* 卡片里的红字改用暖底上够沉的那一档（--color-danger 在米色上偏跳）。 */
.predict-card .predict-error {
  color: var(--pc-bad-ink);
}
/* 卡片里的通用空态提示（游客只读、已提交等待揭晓…）同样要脱离深色主题。 */
.predict-card .empty-hint {
  color: var(--pc-ink-dim);
  padding: 0;
}

.predict-submitted {
  margin-top: 14px;
  padding-top: 12px;
  border-top: 1px solid var(--pc-line);
  font-size: 12.5px;
  line-height: 1.6;
  color: var(--pc-ink);
}
.predict-submitted-label {
  color: var(--pc-ink-dim);
  margin-bottom: 4px;
}

/* 揭晓徽章：与倒计时药丸同一副几何（全圆、同内边距、同字重），只换成绿系。 */
.reveal-badge {
  flex: none;
  padding: 7px 16px;
  border-radius: 999px;
  background: var(--pc-ok);
  color: #ffffff;
  font-size: 13.5px;
  font-weight: 700;
  line-height: 1.25;
  letter-spacing: 0.04em;
  white-space: nowrap;
}
/* 揭晓卡的动作行：「查看 Actor 判断」按钮与停拍倒计时药丸并排（2026-09-02
 * 需求方指位）。按钮自带的 margin/align-self 是给直接挂在卡片上的场景（作答态
 * 仍那样用）预备的，进了本行由行统一管间距。 */
.reveal-actions-row {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0 0 14px;
}
.reveal-actions-row .actor-decision-open {
  margin: 0;
  align-self: auto;
}

/* 「N 秒后进入下一回合」（停拍倒计时）：与预测倒计时（.predict-countdown）同一
 * 分量的药丸——需求方点名要醒目，原 12.5px 灰字玩家注意不到。同色（--pc-timer）
 * 是刻意的：两颗药丸都是「倒计时」，玩家学一次就认得；区别由文案自己说清。
 * 秒数由服务端下发的绝对截止时刻现算（app.js revealHoldRemainingMs），每秒只改
 * 文本节点、归零置 hidden（tickCountdown）。 */
.reveal-hold-countdown {
  flex: none;
  padding: 7px 14px;
  border-radius: 999px;
  background: var(--pc-timer);
  color: #ffffff;
  font-size: 14px;
  font-weight: 700;
  line-height: 1.25;
  letter-spacing: 0.04em;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.reveal-hold-countdown[hidden] {
  display: none;
}
.reveal-real-action {
  margin-bottom: 14px;
  padding: 12px 14px;
  background: var(--pc-surface);
  border: 1px solid var(--pc-line);
  border-radius: 12px;
}
.reveal-real-label {
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: 0.06em;
  color: var(--pc-ink-dim);
  margin-bottom: 6px;
}
.reveal-real-action .md-body {
  font-size: 13.5px;
  line-height: 1.7;
  color: var(--pc-ink);
}
.reveal-real-method {
  margin-top: 7px;
  font-size: 12.5px;
  line-height: 1.65;
  color: var(--pc-ink-dim);
}
.reveal-result {
  margin-bottom: 14px;
  padding: 12px 14px;
  background: #f4eee8;
  border: 1px solid var(--pc-line);
  border-radius: 12px;
  color: var(--pc-ink);
}
/* Markdown 强调色：全局那档 --color-accent（暖黄）落在米色底上几乎糊掉，
   卡片内换成同色相更沉的一档。 */
.predict-card .md-body strong,
.predict-card .md-body strong em,
.predict-card .md-body em strong {
  color: #a03f2e;
}
.predict-card .md-body h3 {
  color: var(--pc-ink-dim);
}
.reveal-abstained {
  font-size: 12.5px;
  color: var(--pc-ink-dim);
  margin-bottom: 10px;
}
.reveal-scores {
  font-size: 12.5px;
  line-height: 1.9;
  color: var(--pc-ink);
}

/* 暂停覆盖态：蒙在预测/揭晓区之上，阻断投票交互，倒计时展示冻结值 */
.predict-mask {
  position: absolute;
  inset: 0;
  z-index: 5;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  text-align: center;
  padding: 12px;
  background: rgba(23, 28, 35, 0.82);
  backdrop-filter: blur(1px);
  border-radius: var(--radius);
}
.predict-mask-title {
  font-size: 18px;
  font-weight: 700;
  color: var(--color-accent);
}
.predict-mask-focus {
  padding: 4px 10px;
  border: 1px solid rgba(227, 151, 77, 0.4);
  border-radius: 999px;
  color: #f0c18e;
  font-size: 12px;
  font-weight: 800;
}
.predict-mask-sub {
  font-size: 13px;
  color: var(--color-text-dim);
}

/* ---------- 终局结算视图（T18：finished 时替代预测区内容） ---------- */

.settlement-view h3 {
  margin: 14px 0 8px;
  font-size: 13px;
  color: var(--color-text-dim);
}
.settlement-view h3:first-of-type {
  margin-top: 0;
}
.settlement-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 12px;
}
.settlement-table th,
.settlement-table td {
  border: 1px solid var(--color-border);
  padding: 5px 7px;
  text-align: center;
}
.settlement-table th {
  background: var(--color-panel-alt);
  color: var(--color-text-dim);
  font-weight: 600;
}
.matrix-table th:first-child,
.matrix-table td:first-child {
  text-align: left;
}
.matrix-table td.self-diag {
  color: var(--color-accent);
  font-weight: 700;
}
.settlement-note {
  margin-top: 12px;
  font-size: 12px;
  color: var(--color-text-dim);
}

/* ---------- 底部剧情时间轴（T18；用户反馈旧版药丸节点太小不突出，改为
   双层大节点：上层「回合 N」大字 + 状态徽标，下层回合标题） ---------- */

.timeline-body {
  display: flex;
  gap: 10px;
  overflow-x: auto;
  padding-bottom: 6px;
  /* 节点多时靠横向滚动而不是折行挤压（flex 默认 nowrap，这里显式声明意图） */
  flex-wrap: nowrap;
}

.timeline-node {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 5px;
  min-width: 168px;
  max-width: 240px;
  min-height: 64px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: 10px 14px;
  background: var(--color-panel-alt);
  color: var(--color-text);
  font-family: inherit;
  text-align: left;
  cursor: pointer;
  white-space: nowrap;
  transition: border-color 0.15s ease, background-color 0.15s ease;
}
button.timeline-node:hover {
  border-color: var(--color-accent-dim);
}

.timeline-node-head {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
}
.timeline-node-no {
  font-size: 15px;
  font-weight: 700;
  letter-spacing: 0.02em;
}
.timeline-node-state {
  margin-left: auto;
  font-size: 10.5px;
  padding: 1px 7px;
  border-radius: 999px;
  border: 1px solid var(--color-border);
  color: var(--color-text-dim);
}
.timeline-node-title {
  display: block;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  font-size: 12.5px;
  color: var(--color-text-dim);
}

/* 已揭晓：常态收敛（大字保持可读，其余降权），与进行中/当前区分开 */
.timeline-node.revealed .timeline-node-no {
  color: var(--color-text-dim);
}
.timeline-node.revealed .timeline-node-state {
  color: var(--color-success);
  border-color: var(--color-success);
}

/* 进行中（predicting，全局至多一个）：暖色强调 */
.timeline-node.predicting {
  border-color: var(--color-accent-dim);
}
.timeline-node.predicting .timeline-node-no {
  color: var(--color-accent);
}
.timeline-node.predicting .timeline-node-state {
  color: var(--color-accent);
  border-color: var(--color-accent-dim);
}

/* 当前正在中栏展示的回合：明显高亮（实时跟随或回看选中都算） */
.timeline-node.active {
  border-color: var(--color-accent);
  background: rgba(224, 164, 88, 0.14);
  box-shadow: 0 0 0 1px var(--color-accent-dim);
}
.timeline-node.active .timeline-node-no {
  color: var(--color-accent);
}
.timeline-node.active .timeline-node-title {
  color: var(--color-text);
}

/* 序幕节点：时间轴最左端那一个。**不是回合 0**，所以刻意不长成回合的样子——
   没有状态徽标（序幕没有预测/揭晓这套生命周期），没有标题行，因而只有一行，
   宽度也收窄到回合节点的一半略多，读起来是「回合 1 之前的一小段引子」。
   左侧橙条与剧情区的序幕卡片（.prologue-entry）同一记号，两处一眼可对上。
   DOM 侧的取舍与理由见 app.js 的 buildTimelinePrologueNode。 */
.timeline-node-prologue {
  min-width: 104px;
  border-left: 3px solid var(--color-accent);
}
.timeline-node-prologue .timeline-node-head {
  gap: 7px;
}
.timeline-node-prologue .timeline-node-no {
  color: var(--color-accent);
}
/* ▶ 播放提示：节点的唯一动作暗示，比状态徽标位置更靠前（它说的是「点我会
   发生什么」，不是「它现在处于什么状态」）。 */
.timeline-node-cue {
  font-size: 11px;
  line-height: 1;
  color: var(--color-accent);
}

/* 幽灵节点：下一回合推演中的占位（虚线框 + 呼吸动画，不可点击） */
.timeline-node-ghost {
  border-style: dashed;
  border-color: var(--color-accent-dim);
  background: transparent;
  cursor: default;
  animation: advancing-breathe 2.2s ease-in-out infinite;
}
.timeline-node-ghost .timeline-node-no {
  color: var(--color-accent);
}
.timeline-node-ghost .timeline-node-state {
  color: var(--color-accent);
  border-color: var(--color-accent-dim);
}
/* gen_error 挂起：异常样式，静止不再呼吸（不是「正常在路上」的状态） */
.timeline-node-ghost.timeline-node-error {
  animation: none;
  border-color: var(--color-danger);
}
.timeline-node-ghost.timeline-node-error .timeline-node-no,
.timeline-node-ghost.timeline-node-error .timeline-node-state {
  color: var(--color-danger);
  border-color: var(--color-danger);
}

/* ---------- 「推演中」提示（剧情区当前回合正文下方；与幽灵节点同款动画） ---------- */

@keyframes advancing-breathe {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.45;
  }
}

.advancing-hint {
  display: flex;
  align-items: center;
  gap: 9px;
  margin-top: 14px;
  padding: 11px 14px;
  border: 1px dashed var(--color-accent-dim);
  border-radius: var(--radius);
  color: var(--color-accent);
  font-size: 13px;
  animation: advancing-breathe 2.2s ease-in-out infinite;
}
.advancing-dot {
  flex: 0 0 auto;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--color-accent);
}
.advancing-hint.advancing-error {
  animation: none;
  border-color: var(--color-danger);
  color: var(--color-danger);
}

/* ---------- 公屏聊天区（T19） ---------- */

.chat-messages {
  flex: 1 1 auto;
  overflow-y: auto;
  min-height: 0;
  padding: 4px 2px;
  margin-bottom: 8px;
}

.chat-msg {
  margin-bottom: 10px;
  line-height: 1.5;
}
.chat-msg-head {
  display: flex;
  align-items: baseline;
  gap: 6px;
  font-size: 11.5px;
  margin-bottom: 2px;
}
.chat-tag {
  font-size: 10px;
  padding: 1px 6px;
  border-radius: 999px;
  font-weight: 700;
}
.chat-tag-player {
  background: var(--color-player);
  color: #071523;
}
.chat-tag-guest {
  background: var(--color-guest);
  color: #12151b;
}
.chat-nick {
  font-weight: 700;
}
.chat-msg-player .chat-nick {
  color: var(--color-player);
}
.chat-msg-guest .chat-nick {
  color: var(--color-guest);
}
.chat-time {
  color: var(--color-text-dim);
  margin-left: auto;
}
.chat-body {
  font-size: 13px;
  word-break: break-word;
  white-space: pre-wrap;
}
.chat-msg-guest .chat-body {
  color: var(--color-text-dim);
}

.chat-input-area {
  flex: 0 0 auto;
}
.chat-input-form,
.chat-guest-form {
  display: flex;
  gap: 6px;
}
.chat-guest-form {
  flex-direction: column;
}
.chat-guest-hint {
  font-size: 11.5px;
  color: var(--color-text-dim);
}
.chat-input-form input,
.chat-guest-form input {
  flex: 1;
  background: var(--color-panel-alt);
  border: 1px solid var(--color-border);
  border-radius: 6px;
  padding: 7px 9px;
  color: var(--color-text);
  font-family: inherit;
  font-size: 13px;
}
.chat-input-form button,
.chat-guest-form button {
  border: none;
  border-radius: 6px;
  padding: 7px 14px;
  font-weight: 700;
  cursor: pointer;
  background: var(--color-accent);
  color: #1a1408;
}
.chat-input-form button:disabled,
.chat-guest-form button:disabled {
  opacity: 0.5;
  cursor: default;
}

/* ---------- 通用小按钮（时间轴“返回实时”等） ---------- */
button.small {
  border: 1px solid var(--color-border);
  background: transparent;
  color: var(--color-text);
  border-radius: 6px;
  padding: 3px 9px;
  font-size: 11.5px;
  cursor: pointer;
  font-family: inherit;
}
button.small:hover {
  border-color: var(--color-accent-dim);
}

/* ================= v2 舞台区（tech_design v2 §9.5，task 6.2） ================= */

.stage-wrap {
  position: relative;
  height: 340px;
  margin-bottom: 12px;
  border-radius: 8px;
  overflow: hidden;
  background: #101826;
}
.stage-canvas,
.stage-canvas canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
.stage-hud {
  position: absolute;
  inset: 0;
  pointer-events: none; /* HUD 子元素自己按需开启 pointer-events */
}
/* 回落字幕：speak/act 未命中时承载同一段文本，几何与字号跟着底部文本带走，
 * 否则同一段戏在两条路径上会呈现出两种大小。 */
.stage-caption {
  position: absolute;
  left: 6%;
  right: 6%;
  bottom: 16px;
  padding: 11px 16px 36px;
  border-radius: 9px;
  background: rgba(10, 16, 28, 0.9);
  color: #f0f4fa;
  font-size: 17px;
  line-height: 1.62;
  pointer-events: none;
}
.stage-caption .caption-speaker {
  color: #9fc1ef;
  font-weight: 600;
  margin-right: 8px;
}
.stage-controls {
  position: absolute;
  top: 10px;
  right: 10px;
  display: flex;
  gap: 8px;
}
/* 【回放横幅遮住演出控制键】用户反馈「回合回放时看不到『跳过本段』」——按钮
 * 一直都在（showSkip 是通用演出循环启动处调的，序幕与回合同走一条路），是被
 * 回放横幅盖住了：.stage-controls 钉在右上角 (10,10)，.stage-replay-banner 是
 * top:0;left:0;right:0 的**整条**顶栏，两者都没有显式 z-index，靠 DOM 序决定
 * 谁在上面，横幅正好压住右上角那两个按钮（线上截图里能看见橙条后面露出的
 * 「跳…本…」残字）。
 *
 * 【为什么不是给 .stage-controls 提 z-index】那只是把按钮浮到橙色横幅的文案
 * 上面，两层字叠在一起，比现在更难读。遮挡是布局冲突，就该用布局解决：横幅
 * 可见时把整组控件下移到横幅带之外，两者纵向错开，谁都不用盖谁。
 *
 * 【为什么是纯 CSS 而不是 JS 算像素】横幅由 [hidden] 属性切换，`:not([hidden])`
 * 直接就是「横幅可见」这个状态；后继兄弟选择器 `~` 要求横幅在 DOM 里排在控件
 * **之前**（index.html 已按此调整并留了注记）。没有任何 JS 状态需要同步，也就
 * 不存在「忘了清标志位」的那类残留。
 *
 * 下移量 48px = 横幅带高 38px（下面 min-height 钉死，与浏览器实测的 38.5px
 * 对齐）+ 10px 呼吸，与常态 top:10px 的口径一致。两个数字分散在两条规则里，靠
 * tests/js/stage_overlay.mjs 交叉校验「控件让开的高度 ≥ 横幅占住的高度」，改
 * 一半忘另一半会红。 */
#stage-replay-banner:not([hidden]) ~ .stage-controls {
  top: 48px;
}
.stage-btn {
  font-size: 12px;
  padding: 4px 10px;
  border: 1px solid rgba(255, 255, 255, 0.35);
  border-radius: 999px;
  background: rgba(10, 16, 28, 0.6);
  color: #e6ecf5;
  cursor: pointer;
}
.stage-btn:hover {
  background: rgba(10, 16, 28, 0.85);
}
/* [hidden] 必须显式压过下面的 display:flex——HTML hidden 属性的 UA 样式
   （display:none）优先级低于作者样式，缺这一条横幅会永远可见。 */
.stage-replay-banner[hidden],
.stage-controls [hidden] {
  display: none;
}
/* min-height + nowrap 一起把横幅的实际高度钉在 38px 这一档（浏览器实测：内部
 * .stage-btn 26.5px + 上下 padding 12px = 38.5px，min-height 与它对齐，声明的
 * 「带高」才不会比实际占的地方小）：上面那条下移规则让开的正是这个数，横幅若
 * 因文案折行长高就会重新盖住按钮（长地点名 + 窄舞台是真会撞的组合）。所以文案
 * 单行截断、按钮组不换行，高度不随内容浮动。 */
.stage-replay-banner {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  min-height: 38px;
  padding: 6px 12px;
  background: rgba(217, 119, 6, 0.92);
  color: #fff;
  font-size: 13px;
  display: flex;
  flex-wrap: nowrap;
  justify-content: space-between;
  align-items: center;
  gap: 10px;
}
/* 横幅文案（replay.js showBanner 建的那个 span）：可压缩 + 单行省略号。
 * min-width:0 是必须的——flex 子项默认 min-width:auto，不写这一条 nowrap 的
 * 长文案会把按钮组顶出横幅右缘而不是自己截断。 */
.stage-replay-banner .replay-label {
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
/* 横幅右侧按钮组（重新演绎 / 回到实时）：横幅的 space-between 只分
   「文案 | 按钮组」两份，组内按钮自己排 */
.stage-replay-banner .replay-actions {
  display: flex;
  gap: 8px;
  align-items: center;
  flex: 0 0 auto; /* 按钮组不被文案挤扁：要压缩的是文案（.replay-label） */
}
/* 演绎提示（replay.js 控制）：历史回合演出重放前后的「第X回合 开始/结束
   演绎」，舞台中央偏上短暂驻留。pointer-events:none 不挡地图交互；z-index
   居于抽屉（15）之下、天气/旁白等基础覆盖层之上，与其余覆盖层无位置交叠。
   左 padding 比右多 4px：抵消末字符 letter-spacing 留在右侧的空档。 */
@keyframes stage-notice-in {
  from {
    opacity: 0;
    transform: translate(-50%, -50%) scale(0.94);
  }
  to {
    opacity: 1;
    transform: translate(-50%, -50%) scale(1);
  }
}
.stage-perform-notice {
  position: absolute;
  top: 42%;
  left: 50%;
  transform: translate(-50%, -50%);
  padding: 12px 28px 12px 32px;
  border-radius: 24px;
  background: rgba(8, 12, 22, 0.92);
  color: #fff;
  font-size: 17px;
  letter-spacing: 4px;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
  pointer-events: none;
  z-index: 14;
  animation: stage-notice-in 240ms ease-out;
}
.stage-perform-notice[hidden] {
  display: none;
}
.mode-toggle-btn {
  margin-left: 12px;
  font-size: 12px;
  padding: 2px 10px;
  border: 1px solid #c9d4e3;
  border-radius: 999px;
  background: #fff;
  cursor: pointer;
}

/* 基本模式对局的呈现形态**静态标签**（不是控件）：占位与 .mode-toggle-btn 对齐，
 * 但去掉边框、底色与手型指针——这三样是「可以点」的全部视觉信号，留着任何一样
 * 都会招来一次点击。default 光标是刻意写的，不是冗余：顶栏整体没有设定光标时
 * 浏览器给的是 text 光标，读起来像可编辑。 */
.mode-static-label {
  margin-left: 12px;
  font-size: 12px;
  padding: 2px 0;
  color: var(--color-text-dim);
  cursor: default;
}

/* ================= HUD 覆盖层（tech_design v2 §9.5，task 6.5） =================
 * #stage-hud 铺满舞台、pointer-events:none（不挡立牌点击，见上面 .stage-hud
 * 定义）。四块各占一角/一侧，互不重叠：
 *   左上 小地图 hud-minimap      右上 时间牌 hud-clock
 *   左下 角色徽章 hud-badges     右侧中部 线索板 hud-clues
 * 只有真正要交互的两块（小地图节点、线索板）打开 pointer-events:auto——
 * 时间牌/角色徽章条纯展示，留 none 不抢立牌层的点击。
 */

.hud-minimap {
  position: absolute;
  top: 10px;
  left: 10px;
  width: 190px;
  padding: 6px;
  background: rgba(10, 16, 28, 0.72);
  border: 1px solid var(--color-border);
  border-radius: 8px;
  pointer-events: auto;
}
.hud-minimap-svg {
  width: 100%;
  height: 120px;
  display: block;
}
.hud-map-line {
  stroke: rgba(255, 255, 255, 0.28);
  stroke-width: 1.2;
}
.hud-map-node {
  cursor: pointer;
}
.hud-map-dot {
  fill: #46587c;
  stroke: rgba(255, 255, 255, 0.5);
  stroke-width: 1;
}
.hud-map-node-active .hud-map-dot {
  fill: var(--color-accent);
  stroke: #fff;
  stroke-width: 2;
}
.hud-map-label {
  fill: var(--color-text);
  font-size: 8px;
  font-family: var(--font-sans);
}
.hud-map-count-bg {
  fill: var(--color-danger);
}
.hud-map-count-text {
  fill: #fff;
  font-size: 7px;
  font-family: var(--font-sans);
}

.hud-clock {
  position: absolute;
  /* 顶部**中央**、放大一档（用户反馈：时间和天气要放显眼位置并突出）。
     top:36px 躲开回放横幅（top:0 高约 30px，回放时不互叠）；其下 72px 是
     特写地点名牌，再往下整个上半区已无常驻文本（旁白条已迁回底部文本带）；
     左右两角的 stage-controls / 小地图互不相干。暖色描边+微光让它从雪山底上
     浮出，可读性三件套口径与底部条一致（近实底+白字+投影）。 */
  top: 36px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 18px;
  background: rgba(8, 13, 24, 0.88);
  border: 1px solid rgba(242, 181, 116, 0.55);
  border-radius: 999px;
  font-size: 17px;
  font-weight: 700;
  letter-spacing: 1px;
  color: #fff;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
  box-shadow: 0 0 16px rgba(242, 181, 116, 0.22), 0 2px 10px rgba(0, 0, 0, 0.45);
  white-space: nowrap;
  z-index: 7; /* 高于视口内容/立绘（6），低于抽屉/横幅层 */
}
.hud-clock-icon {
  font-size: 19px;
  line-height: 1;
}

/* 状态条图例（用户反馈红绿条不自解释）：徽章区首行小字，与条同色。 */
.hud-badge-legend {
  display: flex;
  gap: 10px;
  font-size: 10px;
  opacity: 0.9;
  padding: 1px 6px;
  /* 底图蒙层减淡（0.62→0.25）后图例小字直接坐在亮底图上会发虚——补与
   * HUD 徽章同款的深色底牌，可读性归局部承担而不是靠全图压暗。 */
  background: rgba(10, 15, 26, 0.72);
  border-radius: 6px;
}
.hud-legend-body {
  color: var(--color-danger);
}
.hud-legend-stamina {
  color: var(--color-success);
}
.hud-badges {
  position: absolute;
  left: 10px;
  /* bottom:10 与既有 .stage-caption（8%~92% 宽、bottom:14px、近不透明）在
     底部区域重叠，说话/旁白期间字幕条会把徽章条盖满（fix round 1，见
     task-65-review.md Important #2，任务书明确要求核查的点）。改锚到小地图
     下方竖排：.hud-minimap 是 top:10 + padding 6*2 + svg 高 120 + 边框 2，
     底边约在 144px，这里留 6px 间距从 150px 开始——无论字幕条几行、多高，
     都在舞台上半区，结构上不会再和底部字幕条抢地方。 */
  top: 150px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  max-width: 190px;
}
.hud-badge-row {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 3px 8px;
  background: rgba(10, 16, 28, 0.72);
  border: 1px solid var(--color-border);
  border-radius: 6px;
  font-size: 11px;
  color: var(--color-text);
}
.hud-badge-name {
  min-width: 40px;
  white-space: nowrap;
}
.hud-bar {
  width: 40px;
  height: 5px;
  border-radius: 3px;
  background: rgba(255, 255, 255, 0.15);
  overflow: hidden;
  flex: none;
}
.hud-bar-fill {
  height: 100%;
}
.hud-bar-body .hud-bar-fill {
  background: var(--color-danger);
}
.hud-bar-stamina .hud-bar-fill {
  background: var(--color-success);
}
.hud-badge-item {
  color: var(--color-accent);
  font-size: 10.5px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.hud-clues {
  position: absolute;
  right: 10px;
  top: 50%;
  transform: translateY(-50%);
  pointer-events: auto;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 6px;
}
.hud-clue-badge {
  font-family: inherit;
  font-size: 12px;
  padding: 4px 10px;
  border: 1px solid var(--color-border);
  border-radius: 999px;
  background: rgba(10, 16, 28, 0.72);
  color: var(--color-text);
  cursor: pointer;
}
.hud-clue-badge:hover {
  border-color: var(--color-accent-dim);
}
.hud-clue-list {
  max-width: 200px;
  max-height: 200px;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.hud-clue-card {
  padding: 6px 9px;
  background: rgba(10, 16, 28, 0.88);
  border: 1px solid var(--color-border);
  border-radius: 6px;
  cursor: pointer;
}
.hud-clue-card:hover {
  border-color: var(--color-accent-dim);
}
.hud-clue-title {
  font-size: 12px;
  color: var(--color-text);
}
.hud-clue-round {
  font-size: 10.5px;
  color: var(--color-text-dim);
  margin-top: 2px;
}

/* 终局关系图谱的样式块（.graph-canvas / .graph-scrubber*）已随图谱入口一起移除
 * （2026-09-02，见 stage/graph.js 头注释）。下面的详情浮层与图谱无关，仍在服役。 */

/* ================= 详情浮层（tech_design v2 §9.5，task 6.8） =================
 * #stage-wrap 内右侧滑入的小面板：立牌点击/地图节点点击/关系边点击三处触发，
 * 共用同一套组件（graph.js 的 showDetail），因此统一挂在舞台区而不是各自在
 * 触发点旁边弹出——交互心智更一致，也不需要为每个触发点单独定位。
 * z-index 20 盖过舞台内其余覆盖层（stage-controls/stage-hud 等都没有显式
 * z-index，默认按 DOM 序，本面板又总是最后插入，正常情况下天然在最上层；
 * 这里再显式声明一次只是不想让「谁先插入」变成隐性前提）。
 */

.detail-panel {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: 260px;
  max-width: 80%;
  display: flex;
  flex-direction: column;
  background: rgba(10, 16, 28, 0.92);
  border-left: 1px solid var(--color-border);
  z-index: 20;
  animation: detail-panel-slide-in 200ms ease-out;
}
@keyframes detail-panel-slide-in {
  from {
    transform: translateX(100%);
  }
  to {
    transform: translateX(0);
  }
}
.detail-panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 10px 12px;
  border-bottom: 1px solid var(--color-border);
  flex: none;
}
.detail-panel-title {
  font-size: 13px;
  font-weight: 700;
  color: var(--color-text);
}
.detail-panel-close {
  font-family: inherit;
  font-size: 16px;
  line-height: 1;
  padding: 0 4px;
  border: none;
  background: none;
  color: var(--color-text-dim);
  cursor: pointer;
}
.detail-panel-close:hover {
  color: var(--color-text);
}
.detail-panel-body {
  flex: 1 1 auto;
  /* min-height:0 覆盖 flex 子项默认的 min-height:auto——没有它，内容较长时
     .detail-panel-fixed 的 max-height 限制不住这个子项，滚动条不出现，
     面板会顶穿 max-height 直接溢出（fix round 1 新增 fixed 变体才会真正
     暴露这个问题：.detail-panel 原本靠 top:0/bottom:0 撑满固定高度，凑巧
     没触发；fixed 变体只设 max-height，问题才显性）。 */
  min-height: 0;
  overflow-y: auto;
  padding: 10px 12px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.detail-row {
  display: flex;
  flex-direction: column;
  gap: 2px;
  font-size: 12px;
}
.detail-row-label {
  color: var(--color-text-dim);
}
.detail-row-value {
  color: var(--color-text);
  word-break: break-word;
}

/* ---------- 详情浮层 · fixed 挂载变体（fix round 1，评审 Important-1） ----------
 * #stage-wrap 隐藏时（文字形态/WebGL 降级）showDetail 改挂 document.body，
 * 叠加这个类覆盖定位/尺寸/层级；.detail-panel 的其余规则（背景/flex 布局/
 * 头部/关闭按钮/行样式）原样复用，靠 CSS 层叠顺序（本规则在文件更后面）
 * 覆盖冲突属性，不用整份重抄。z-index 盖过 #conn-banner（1000）。
 */
.detail-panel-fixed {
  position: fixed;
  top: 64px;
  right: 16px;
  bottom: auto;
  max-height: calc(100vh - 96px);
  border-left: 1px solid var(--color-border);
  border-radius: var(--radius);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
  z-index: 1001;
  animation: detail-panel-fixed-slide-in 200ms ease-out;
}
@keyframes detail-panel-fixed-slide-in {
  from {
    opacity: 0;
    transform: translateY(-8px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* ================= 世界地图弹层（tech_design v2 §24 第 8 行） =================
 *
 * XSMap 的全屏弹层：挂 body（与全文弹层同理，不受 #stage-wrap 裁剪/层叠影
 * 响），z-index 压在全文弹层（9999，perform.js 内联）之下——两者理论上不共
 * 存，但真叠起来时「正在读的全文」优先级理应更高。
 */

#xsmap-overlay {
  position: fixed;
  inset: 0;
  background: rgba(6, 10, 18, 0.72);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 9990;
}
.xsmap-panel {
  background: var(--color-panel);
  border: 1px solid var(--color-border);
  border-radius: 10px;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
  width: min(94vw, 1080px);
  max-height: 88vh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.xsmap-header {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 14px;
  border-bottom: 1px solid var(--color-border);
}
.xsmap-title {
  font-size: 14px;
  font-weight: 600;
  color: var(--color-text);
}
.xsmap-clockline {
  font-size: 12px;
  color: var(--color-text-dim);
}
.xsmap-close {
  margin-left: auto;
  background: transparent;
  border: none;
  color: var(--color-text-dim);
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
  padding: 2px 6px;
}
.xsmap-close:hover {
  color: var(--color-text);
}
.xsmap-body {
  display: flex;
  min-height: 0; /* 允许子项在 max-height 约束下收缩出滚动 */
}
.xsmap-svg-wrap {
  flex: 1;
  min-width: 0;
  background-color: #101826; /* 纯色兜底；AI 氛围底图由 XSMap 以 background-image 铺上 */
  background-size: cover;
  background-position: center;
}
.xsmap-svg {
  width: 100%;
  height: 100%;
  min-height: 320px;
  display: block;
}
.xsmap-aside {
  width: 240px;
  flex-shrink: 0;
  border-left: 1px solid var(--color-border);
  padding: 12px 14px;
  overflow-y: auto;
  font-size: 12.5px;
}
.xsmap-hint {
  color: var(--color-text-dim);
}
.xsmap-aside-title {
  font-size: 14px;
  font-weight: 600;
  color: var(--color-accent);
  margin-bottom: 8px;
}
.xsmap-row {
  margin-bottom: 8px;
  line-height: 1.5;
}
.xsmap-row-key {
  display: block;
  color: var(--color-text-dim);
  font-size: 11px;
}
.xsmap-row-val {
  color: var(--color-text);
  word-break: break-word;
}
/* 窄屏：详情面板落到地图下方，不跟地图挤一行 */
@media (max-width: 640px) {
  .xsmap-body {
    flex-direction: column;
  }
  .xsmap-aside {
    width: auto;
    border-left: none;
    border-top: 1px solid var(--color-border);
    max-height: 30vh;
  }
}

/* SVG 内元素 */
/* 连接线双层描边（终审 M-3）：压图蒙层从 0.62 降到 0.3 之后，单层半透明白线
 * 在底图亮部会直接消失。深色底线 + 亮线压顶，与 .xms-edge-under/.xms-edge
 * （地图主画面）同一套做法，亮底暗底通吃。 */
.xsmap-edge-under {
  stroke: rgba(6, 10, 18, 0.66);
  stroke-width: 4.5;
  stroke-dasharray: 6 5;
}
.xsmap-edge {
  stroke: rgba(255, 255, 255, 0.55);
  stroke-width: 2;
  stroke-dasharray: 6 5; /* 虚线小径感，和小地图的实线区分出「地图」气质 */
}
.xsmap-node {
  cursor: pointer;
}
.xsmap-node-ring {
  fill: var(--color-panel-alt);
  stroke: rgba(255, 255, 255, 0.45);
  stroke-width: 1.5;
}
.xsmap-node-active .xsmap-node-ring {
  stroke: var(--color-accent);
  stroke-width: 3;
}
.xsmap-node-selected .xsmap-node-ring {
  stroke: #fff;
  stroke-width: 3;
}
/* 地名/出发点标签：paint-order 深色文字描边（终审 M-3，与 .xms-node-start-tag
 * 同一套做法）——蒙层降到 0.3 后 SVG 裸文字在底图亮部会消失，描边比矩形底牌
 * 轻量且自适应文字宽度，亮暗底通吃。
 * （本视图的地名仍是浮在底图上的裸文字，所以照旧用描边；主画面 mapstage 的
 * 地名已经搬进地点卡顶条的深色实底，那边不再需要描边。） */
.xsmap-label {
  fill: var(--color-text);
  font-size: 13px;
  font-family: var(--font-sans);
  paint-order: stroke;
  stroke: rgba(6, 10, 18, 0.85);
  stroke-width: 3px;
  stroke-linejoin: round;
}
.xsmap-start-tag {
  fill: var(--color-accent);
  font-size: 10px;
  font-family: var(--font-sans);
  paint-order: stroke;
  stroke: rgba(6, 10, 18, 0.85);
  stroke-width: 3px;
  stroke-linejoin: round;
}
.xsmap-chip circle {
  fill: #3a3f4c;
}
.xsmap-chip-char circle {
  fill: #274a71;
  stroke: var(--color-player);
  stroke-width: 1;
}
.xsmap-chip-npc circle {
  fill: #3a3f4c;
  stroke: rgba(255, 255, 255, 0.35);
  stroke-width: 1;
}
.xsmap-chip-more circle {
  fill: #3a3f4c;
  stroke: rgba(255, 255, 255, 0.35);
  stroke-width: 1;
}
.xsmap-chip text {
  fill: #e7e9ee;
  font-size: 9px;
  font-family: var(--font-sans);
}

/* HUD 小地图右上角的「展开大地图」按钮：容器 pointer-events:auto 已开 */
.hud-map-expand {
  position: absolute;
  top: 4px;
  right: 4px;
  width: 20px;
  height: 20px;
  padding: 0;
  line-height: 1;
  font-size: 12px;
  background: rgba(255, 255, 255, 0.08);
  border: 1px solid var(--color-border);
  border-radius: 4px;
  color: var(--color-text-dim);
  cursor: pointer;
}
.hud-map-expand:hover {
  color: var(--color-text);
  border-color: var(--color-accent-dim);
}

/* ===== 地图主画面（§24 第 12 行） =====
 *
 * mapstage.js（XSMapStage）的容器与内部图层。三条形态切换规则是本区块与
 * app.js（task C，按 renderStage 挂/摘 .mode-map 类）之间的唯一接口：演绎
 * 形态下场景舞台 Canvas 和冗余的 HUD 小地图都让位；非演绎形态下 #mapstage
 * 整体隐藏。三种呈现形态（map/stage/text）互斥，靠这三条规则加 wrap.hidden
 * 共同保证，任何一条改错都会导致两套画面叠在一起。
 */
.stage-wrap.mode-map .hud-minimap {
  display: none;
}
.stage-wrap.mode-map #stage-canvas {
  display: none;
}
.stage-wrap:not(.mode-map) #mapstage {
  display: none;
}

#mapstage {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none; /* 地图主画面纯展示，不挡 HUD/字幕条等覆盖层的点击 */
}
/* 视口层：滚轮缩放/拖拽平移只动它的 transform（mapstage.js
 * bindViewportControls），布局坐标系不感知缩放。origin 固定左上角，
 * 光标锚定的数学在 JS 里做。 */
.xms-viewport {
  position: absolute;
  inset: 0;
  transform-origin: 0 0;
  /* 容器 #mapstage 是 pointer-events:none（不挡 HUD），滚轮缩放/拖拽的监听
   * 却绑在容器上——hit-test 必须有子元素显式 auto 事件才会冒泡进来，否则
   * 真实鼠标下缩放漫游整个收不到事件（此前冒烟用合成 dispatchEvent 绕过了
   * hit-test 没暴露）。viewport 铺满且在舞台最底层，上层各 auto 元素（线索
   * 板/抽屉把手/按钮）先于它命中，开 auto 不抢任何既有交互。 */
  pointer-events: auto;
  transition: filter 0.45s ease; /* 特写进出时后景压暗/还原的过渡 */
}

/* 对话特写（§24 第 31 行）：对质段地图退后景、立绘对话接管。全类切换 +
 * transition，进出平滑；transform（缩放/平移运镜）不受 filter 影响。 */
.xms-closeup .xms-viewport {
  filter: brightness(0.45) blur(2px);
}
/* 特写背景板：说话人所在地点贴图 cover 拉满。首版 blur 8px + brightness
 * 0.5 把贴图糊成一团深色雾、地点认不出（线上用户截图实证）；改极轻景深
 * blur 1px（先调 2px 用户仍要更清晰，地点细节尽量保真）+ brightness 0.7，
 * 可读性改由 ::after 的
 * vignette 暗角承担——中央地点清晰，四缘渐暗托住立绘/对白条/在场者头像
 * （它们都贴边缘布局，正好落在暗区上；对白条自身另有近实底三件套兜底）。
 * scale 1.01 补偿 blur 边缘透色。仅特写态且贴图就绪（bg-on）时淡入；
 * 贴图缺失/坏链摘 bg-on，露出的正是压暗模糊的地图——回落不破版。 */
.xms-closeup-bg {
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: center;
  opacity: 0;
  transform: scale(1.01);
  filter: blur(1px) brightness(0.7); /* 2→1px：用户看过 2px 后要更清晰的地点 */
  transition: opacity 0.45s ease;
  pointer-events: none;
}
/* vignette 暗角：伪元素随父层一起 blur/淡入（渐变对 2px blur 无感）。 */
.xms-closeup-bg::after {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(
    ellipse at center,
    rgba(6, 10, 18, 0) 38%,
    rgba(6, 10, 18, 0.42) 68%,
    rgba(6, 10, 18, 0.85) 100%
  );
}
.xms-closeup .xms-closeup-bg.xms-closeup-bg-on {
  opacity: 1;
}
/* 特写地点名牌：「📍 地点名」药丸，顶部居中时间牌（top 36px 高约 33px）
 * 正下方同轴——时间与地点同为场景元信息，纵向信息栈；比时间牌小一号。
 * 其下不再有第三层：旁白条已迁回底部文本带，上半区只剩这两块元信息。
 * 显隐双条件：特写态 + 名字取到（loc-on），淡入淡出纯 CSS。 */
.xms-closeup-loc {
  position: absolute;
  top: 72px;
  left: 50%;
  transform: translateX(-50%);
  padding: 2px 12px;
  border-radius: 999px;
  background: rgba(8, 13, 24, 0.88);
  border: 1px solid rgba(255, 255, 255, 0.2);
  color: #fff;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.7);
  font-size: 11.5px;
  white-space: nowrap;
  max-width: 56%;
  overflow: hidden;
  text-overflow: ellipsis;
  opacity: 0;
  transition: opacity 0.35s ease;
  pointer-events: none;
  z-index: 7; /* 与时间牌同层 */
}
.xms-closeup .xms-closeup-loc.xms-closeup-loc-on {
  opacity: 1;
}
/* 在场者小头像排：特写态沿下缘（对白条上方）居中，装饰性站位。
 *
 * 底边不写常数，跟着底部文本带的实测位置走：--xms-band-top 由 mapstage.js 的
 * syncBandTop() 写在 #mapstage 上，含义是「当前可见那条带的上缘距舞台下缘多少
 * 像素」（= 带的 bottom + 带高，一次读数拿全，不用区分普通态 16px 还是特写态
 * 20px）。10px 是两者之间的留白。
 *
 * 为什么必须动态测：此处原本是 bottom:84px，定于底部文本带字号放大之前。放大后
 * 单行对白条就有 85px 高（11 + 38 头像 + 36 padding），加特写态的 bottom:20px，
 * 上缘已经在 105px——84 < 105，整排头像落进带内部，又因 z-index 5 < 带的 6 被
 * 压住，于是「圆头像只露上半截、角色名完全看不见」（用户截图实证；实测单行
 * 105px、三行对白 122px、行动条三元组独占首行 123px、760px 窄窗 149px）。
 * 换一个更大的常数只是把同一个 bug 推迟到更多行文本 / 更窄窗口时复现。
 * 兜底值 105px = 单行对白条的上缘，是 JS 缺席时最接近的静态近似。 */
.xms-closeup-cast {
  position: absolute;
  bottom: calc(var(--xms-band-top, 105px) + 10px);
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: flex-start;
  gap: 12px;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.3s ease;
  z-index: 5;
}
.xms-closeup .xms-closeup-cast {
  opacity: 1;
}
.xms-cast-member {
  width: 48px;
  text-align: center;
}
.xms-cast-avatar {
  width: 36px;
  height: 36px;
  margin: 0 auto;
  border-radius: 50%;
  border: 2px solid rgba(255, 255, 255, 0.55);
  background-color: var(--color-panel-alt);
  background-size: cover;
  background-position: center 22%; /* 竖版肖像露脸，同 token */
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 14px;
  font-weight: 600;
  color: var(--color-text);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
}
.xms-cast-name {
  margin-top: 3px;
  font-size: 10px;
  line-height: 1.3;
  padding: 0 5px;
  border-radius: 4px;
  color: #e7e9ee;
  background: rgba(8, 12, 20, 0.68);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.xms-cast-more {
  font-size: 12px;
  color: var(--color-text-dim);
}
.xms-closeup .xms-portrait-on {
  opacity: 0.62; /* 陪衬侧在特写里略提亮——此刻立绘就是主角 */
}
.xms-closeup .xms-portrait-left.xms-portrait-on {
  transform: translate(14px, 0) scale(0.97);
}
.xms-closeup .xms-portrait-right.xms-portrait-on {
  transform: translate(-14px, 0) scale(0.97);
}
.xms-closeup .xms-portrait-left.xms-portrait-active {
  opacity: 1;
  transform: translate(22px, 0) scale(1.08);
  filter: none;
}
.xms-closeup .xms-portrait-right.xms-portrait-active {
  opacity: 1;
  transform: translate(-22px, 0) scale(1.08);
  filter: none;
}
/* 特写态下文本带整体展宽下沉一档。三者共用同一条规则——它们在普通态就同位，
 * 只给其中之一加特写偏移会让段落切换时整条跳动。 */
.xms-closeup .xms-dialogue,
.xms-closeup .xms-action,
.xms-closeup .xms-narration {
  left: 5%;
  right: 5%;
  bottom: 20px;
}
#mapstage.xms-zoomed {
  cursor: grab;
}
#mapstage.xms-grabbing {
  cursor: grabbing;
}
.xms-zoom-hint {
  position: absolute;
  right: 10px;
  bottom: 8px;
  font-size: 10px;
  color: var(--color-text-dim);
  opacity: 0.65;
  pointer-events: none;
}
.xms-backdrop {
  position: absolute;
  inset: 0;
  background-color: #101826; /* 纯色兜底；AI 全景底图由 renderBackdrop 以 background-image 铺上 */
  background-size: cover;
  background-position: center;
}
/* 天气/夜色的**染色**层（粒子不在这里，见 mapstage.js renderWeather 的注释）。
 * 挂在底图自己身上而不是 .xms-weather 上：后者在 DOM 里排在 .xms-viewport
 * 之后、z-index 同为 auto，染色会盖在节点/连线/头像之上把信息层一起蒙灰。
 * 两个伪元素而不是一个：雾夜可以同时成立，一个 background 装不下两层。 */
.xms-backdrop::before,
.xms-backdrop::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.4s ease;
}
.xms-backdrop::before {
  background: rgba(6, 10, 20, 0.32); /* 夜 */
}
.xms-backdrop::after {
  background: rgba(255, 255, 255, 0.16); /* 雾 */
}
.xms-backdrop.xms-tint-night::before {
  opacity: 1;
}
.xms-backdrop.xms-tint-fog::after {
  opacity: 1;
}
.xms-svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}
/* 连接线双层画法（蒙层归零后亮底图保读）：under 层粗深色描边 + 上层细亮
 * 虚线，两层同 dasharray 同起点、虚段自然对齐。上层从蒙层时代的 0.24 提亮
 * 到 0.55——原值是在 62% 深蒙层上调的，蒙层没了要自己撑对比。 */
.xms-edge-under {
  stroke: rgba(6, 10, 18, 0.66);
  stroke-width: 4.5;
  stroke-dasharray: 6 5;
}
.xms-edge {
  stroke: rgba(255, 255, 255, 0.55);
  stroke-width: 2;
  stroke-dasharray: 6 5;
}
/* 地点卡的深色打底描边：与 .xms-edge-under 同一条画法（同几何、更粗的深色
 * 描边先画，身份描边后画压在上面，只留外侧一圈深线）。没有它时，普通地点的
 * rgba(255,255,255,.45) 白描边在雪面上等于不存在，当前场景的 --color-accent
 * 橙描边对雪面也只有 1.6 : 1 —— 与 token 蓝环是同一个病：蒙层归零后没人给
 * 它们做局部衬托。fill:none，内侧那半圈被卡面（.xms-node-face 与贴图）盖掉。 */
.xms-node-ring-under {
  fill: none;
  stroke: rgba(6, 10, 18, 0.85);
  stroke-width: 5;
}
/* 身份描边压在卡面之上，因此必须 fill:none——旧版它兼任卡面底色（fill:
 * --color-panel-alt），那时它画在贴图之前；现在卡面内容裁进 clipPath 单独成
 * 一组，底色交给 .xms-node-face，这里只剩描边职责。 */
.xms-node-ring {
  fill: none;
  stroke: rgba(255, 255, 255, 0.45);
  stroke-width: 1.5;
}
.xms-node-active .xms-node-ring {
  stroke: var(--color-accent);
  stroke-width: 3;
}
/* 卡面底色：整张卡的底，顶条与在场者格两条深色带就压在它上面（贴图只铺中段
 * 的贴图带）；贴图缺席（还没生成完/坏链）时它同时兜住那一格，避免露出底图。 */
.xms-node-face {
  fill: var(--color-panel-alt);
}
/* 顶条（图标+地名）与在场者格：地点卡上两条深色实带。
 *
 * 【它们是这次「区分度不够」的解法本身】用户反馈地点图/人像/背景分不开。人像
 * 与地名此前直接压在 AI 实景底图上，而底图是白昼雪山这种大面积高亮画面，前景
 * 元素只能靠各自的描边硬撑（3459b41 实测：token 蓝环对雪面 1.42:1，落到山体灰
 * 上 1.05:1）。改成「人像坐在我们自己的深色实底上」之后，对比度不再取决于那张
 * 生成图长什么样——0.88 不透明度给了合成结果一个明度上界：即使底下是纯白，
 * 合成后也只到 rgb(39,45,55)，白字/头像相对它的对比度是能算出下限的（这条下限
 * 由 tests/js/color_audit.mjs 的 locPlate 清单项按最坏情况把关）。
 *
 * 【0.12 的透色如今透的是 .xms-node-face，不是贴图】原意是让贴图从带底下透
 * 出一点、人像因此读作「在地点图里面」。那条画法与「取景不随在场人数变」互斥
 * （见 mapstage.js buildNode 的整段说明），已改成贴图只铺中段贴图带。容器关系
 * 由卡片本身表达——图与人在同一张卡的上下两格里，不再靠带底下那点透光。
 * 判据不用改：实际底色比最坏估计（纯白）暗得多，对比度只增不减。 */
.xms-node-head,
.xms-node-cast {
  fill: var(--color-loc-plate);
}
.xms-node-icon-main {
  font-size: 22px;
  text-anchor: middle;
  dominant-baseline: central;
}
/* 地名：现在坐在顶条的深色实底上，不再需要 paint-order 描边去对抗亮底图
 * （描边是给「浮在底图上的裸文字」用的补偿手段，有了自足底就该撤掉——留着
 * 只会把 11px 的小字糊粗一圈）。图标作为文字前缀与地名同一行。 */
.xms-node-label {
  fill: var(--color-text);
  font-size: 11px;
  font-family: var(--font-sans);
  text-anchor: middle;
  dominant-baseline: central;
}
/* 「出发点」角标落在贴图带左上，那里没有深色带兜底，描边照旧保留。 */
.xms-node-start-tag {
  fill: var(--color-accent);
  font-size: 10px;
  font-family: var(--font-sans);
  text-anchor: start;
  paint-order: stroke;
  stroke: rgba(6, 10, 18, 0.85);
  stroke-width: 3px;
  stroke-linejoin: round;
}
.xms-item-badge circle {
  fill: rgba(8, 13, 24, 0.82);
  stroke: var(--color-border);
  stroke-width: 1;
}
.xms-item-badge text {
  fill: var(--color-text);
  font-size: 9px;
  text-anchor: middle;
  dominant-baseline: central;
}

/* token 层：HTML 绝对定位叠在 SVG 之上，left/top 与 svg viewBox 同一坐标系
 * （viewBox 数值即容器实际像素尺寸，1:1 不缩放）。token 定位用 left/top +
 * 负 margin 居中，不占用 transform——scale（pulse）留给 transform 单独用，
 * 两者互不冲突，anime.js 补间 left/top 时也不会撞上 pulse 的 scale。 */
.xms-tokens {
  position: absolute;
  inset: 0;
}
/* 【token 的自足化保读】f0cf511 把底图蒙层归零时逐项审计浮层元素，token 被
 * 判为「自带底」放行——但它那层底（--color-panel-alt）被 .xms-token-avatar
 * 100%×100% 的头像整个盖住，对底图**一个像素都不露**。于是蒙层没了之后，
 * token 与亮底图之间只剩两样东西：2px 的身份描边，和一团 6px 模糊的落影。
 *
 * Playwright 实测（1536x1024 白昼雪山底图，读合成后的屏幕像素）：
 *   描边 #59b1fc 相对紧邻底图 1.42 : 1，落到山体灰上只有 1.05 : 1
 *   —— 蓝环与底图近乎等亮度，眼睛拿不到任何边界信号。
 * 这就是「发虚」的数：UI 边界的通行下限是 3:1，这里连 1.5 都不到。而那团
 * 0 2px 6px rgba(0,0,0,.4) 的落影在亮底上不是「浮起来」而是一圈灰雾，它把
 * 本来就只有 34px 的轮廓又晕开 6px，是「发灰」的来源。
 *
 * 改法与 .xms-edge-under（粗深描边打底）、.xms-node-start-tag（paint-order 深色
 * 文字描边）完全同一条思路，只是换成 box-shadow 的 spread 环：紧贴身份描边
 * 外缘补一圈 1px 硬深色线（对雪面约 14:1，对深底图也仍比底图更深），落影同时
 * 收窄到 0 1px 3px —— 保留「浮起来」的暗示，不再糊边。
 * 深色线在外、身份色在内，两者都不被对方吃掉，亮暗底通吃。 */
.xms-token {
  position: absolute;
  width: 34px; /* 26→34：用户反馈头像认不出人（§24 第 21 行） */
  height: 34px;
  margin-left: -17px;
  margin-top: -17px;
  border-radius: 50%;
  border: 2px solid var(--color-guest);
  background: var(--color-panel-alt);
  box-shadow: 0 0 0 1px rgba(6, 10, 18, 0.92), 0 1px 3px rgba(0, 0, 0, 0.5);
  transition: opacity 0.2s ease;
}
.xms-token-char {
  border-color: var(--color-player);
}
.xms-token-npc {
  border-color: var(--color-guest);
}
.xms-token-avatar {
  width: 100%;
  height: 100%;
  border-radius: 50%;
  background-size: cover;
  /* 肖像图是 512x768 竖版、脸在上部：cover 进正方形后纵向溢出，取 y=22%
   * 让圆里露的是脸而不是胸口（用户反馈认不出人的另一半原因）。 */
  background-position: center 22%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 14px;
  font-weight: 600;
  color: var(--color-text);
  overflow: hidden;
  /* 内圈发丝线：头像照片直接顶在身份描边内缘时，浅色的画面（雪地/天空背景的
   * 立绘）会把 2px 描边从内侧吃掉一截，环看着更细也更糊。一条 1px 的内阴影
   * 把「照片」和「描边」切开，环恢复满宽。 */
  box-shadow: inset 0 0 0 1px rgba(6, 10, 18, 0.4);
}
/* 常显姓名标签：挂 token 正下方，现在落在地点卡的在场者格（.xms-node-cast
 * 的深色实底）上。
 *
 * max-width 从 68px 收到 36px：格宽只有 CELL_W=38（mapstage.js），旧的 68px
 * 会让相邻两人的名字横着叠在一起——那正是「把人装进格子」之后必须跟着改的
 * 尺寸。9.5px + padding 3px 下三个汉字约 34.5px 正好落在格内，更长的名字走
 * 省略号；两字名（最常见）绰绰有余。
 *
 * 自带的半透明底牌保留：说话者压暗态、以及在场者格外（卡片被挤出画面、格位
 * 落到贴图带上）的那点明度波动，都由它再兜一层，名字不跟着底图变脸。 */
.xms-token-name {
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  margin-top: 2px;
  width: max-content;
  max-width: 36px;
  font-size: 9.5px;
  line-height: 1.25;
  padding: 0 3px;
  border-radius: 4px;
  color: #e7e9ee;
  background: rgba(8, 12, 20, 0.68);
  text-align: center;
  pointer-events: none;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.xms-token-equip-dot {
  position: absolute;
  right: -1px;
  bottom: -1px;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--color-accent);
  border: 1.5px solid var(--color-bg);
}
/* 说话/闪烁态整条覆盖 box-shadow，因此上面那圈 1px 深色硬线必须在每一处
 * 原样重列——漏一处，token 一进入该状态就退回「亮底图上没有边界」。
 * 深色线恒排第一（spread 最小），其余环从 1px 起跳往外叠。 */
.xms-token-speaking {
  box-shadow: 0 0 0 1px rgba(6, 10, 18, 0.92), 0 0 0 4px rgba(224, 164, 88, 0.85),
    0 1px 4px rgba(0, 0, 0, 0.5);
  animation: xms-speak-pulse 900ms ease-in-out infinite;
  z-index: 20; /* 说话者浮到密集人堆之上 */
}
/* 【非说话者的压暗】原值 opacity 0.55 —— 也就是让 45% 的底图直接从头像里
 * 透出来。演出期几乎每一拍都有说话人，于是「大部分时间、大部分头像都是半
 * 透明的」，这是「发虚」的第二个来源，而且是占比最大的那个：实测压暗态下
 * 头像本体相对底图只剩 1.41 : 1（未压暗时 1.60~2.21 : 1），蓝环从 #59b1fc
 * 洗成 #92c5f1。
 *
 * 说话者的辨识本来就由脉冲橙环 + z-index 20 单独承担，压暗只需要「退一档」，
 * 不需要「变透明」。改成保持不透明、走降饱和 + 轻压暗：主次关系照旧，但
 * 头像始终是一个实心不透底的物件，那圈 1px 深色硬线也就不会跟着淡掉。 */
.xms-token-dimmed {
  opacity: 0.92;
  filter: saturate(0.5) brightness(0.82);
}
.xms-token-flash {
  box-shadow: 0 0 0 1px rgba(6, 10, 18, 0.92), 0 0 0 6px rgba(255, 255, 255, 0.55),
    0 0 18px 6px rgba(255, 255, 255, 0.45);
}
@keyframes xms-speak-pulse {
  0%,
  100% {
    box-shadow: 0 0 0 1px rgba(6, 10, 18, 0.92), 0 0 0 4px rgba(224, 164, 88, 0.55),
      0 1px 4px rgba(0, 0, 0, 0.5);
  }
  50% {
    box-shadow: 0 0 0 1px rgba(6, 10, 18, 0.92), 0 0 0 6px rgba(224, 164, 88, 0.95),
      0 1px 4px rgba(0, 0, 0, 0.5);
  }
}
.xms-fly-dot {
  position: absolute;
  width: 10px;
  height: 10px;
  margin-left: -5px;
  margin-top: -5px;
  border-radius: 50%;
  background: #ffd76a;
  box-shadow: 0 0 6px rgba(255, 215, 106, 0.8);
}

/* 视觉小说立绘（§24 第 28 行）：说话人 512x768 竖版肖像在舞台下部两侧上位，
 * 当前说话者全亮、另一侧压暗缩小；层整体 pointer-events:none 不挡地图交互，
 * 在 .xms-viewport 外不随镜头缩放，z-index 5 压在对白条（6）之下——对白框
 * 盖住立绘下缘是 VN 惯例。空槽 opacity 0 恒不可见。 */
.xms-portraits {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 5;
}
.xms-portrait {
  position: absolute;
  bottom: 0;
  height: 62%;
  aspect-ratio: 512 / 768;
  /* 窄舞台护栏：宽随高按比例反推，窄容器里一侧立绘能占到半个舞台把地图
   * 全盖住（冒烟窄窗实证）；钳到 24% 后 height 不变、超出部分由 cover +
   * top center 取景裁剪（脸保住，不变形）。 */
  max-width: 24%;
  background-size: cover;
  background-position: top center; /* 竖版肖像脸在上部：贴顶取景 */
  opacity: 0;
  transform: translateY(14px) scale(0.94);
  filter: brightness(0.55) saturate(0.85);
  transition: opacity 0.28s ease, transform 0.3s ease, filter 0.3s ease;
  /* 下缘渐隐融入舞台，不出现生硬的裁切线 */
  -webkit-mask-image: linear-gradient(to bottom, #000 76%, transparent 100%);
  mask-image: linear-gradient(to bottom, #000 76%, transparent 100%);
}
.xms-portrait-left {
  left: 1.5%;
}
.xms-portrait-right {
  right: 1.5%;
}
.xms-portrait-on {
  /* 陪衬态（对方说话/旁白/事件阶段）压得比典型 VN 更低——立绘叠在地图上，
   * 太显眼会跟世界层抢注意力（冒烟截图实证 0.92 视觉过重）。 */
  opacity: 0.5;
  transform: translateY(0) scale(0.94);
}
.xms-portrait-on.xms-portrait-active {
  opacity: 1;
  transform: translateY(0) scale(1);
  filter: none;
}

/* 对话流记录（§24 第 28 行）：右下「记录 ≡」常驻小按钮 + 滚动面板。台词/
 * 旁白全程累积（上限 100 滚动淘汰），解决「一句话闪过没看清」。按钮压在
 * zoom-hint 提示小字上方；面板 z-index 14 低于世界动态抽屉（15），两者同开
 * 时抽屉优先。 */
.xms-log-btn {
  position: absolute;
  right: 10px;
  bottom: 26px;
  z-index: 7;
  pointer-events: auto;
  font-family: inherit;
  font-size: 11px;
  padding: 3px 9px;
  border: 1px solid var(--color-border);
  border-radius: 999px;
  background: rgba(10, 16, 28, 0.78);
  color: var(--color-text);
  cursor: pointer;
}
.xms-log-btn:hover,
.xms-log-btn-on {
  border-color: var(--color-accent-dim);
  color: #fff;
}

/* 音频按钮排（§24 第 41/53 行）：🗣 配音 / ♪ 曲目 / 🔊 声音 统一进一个
 * flex 容器，容器一次 absolute 定位在记录按钮左侧——按钮自身 static，
 * 增减按钮由 flex 排开天然不撞位（此前各按钮各自 absolute 硬编码 right
 * 坐标，新增 ♪ 漏配坐标即与 🔊 同位叠压，用户截图实证）。右缘锚定
 * right:92px，flex 内容向左增长，不侵入右侧记录按钮（10~90px 区域）。 */
.xms-audio-bar {
  position: absolute;
  right: 92px;
  bottom: 26px;
  z-index: 7;
  display: flex;
  gap: 8px;
  pointer-events: auto;
}
.xms-audio-btn {
  font-family: inherit;
  font-size: 11px;
  padding: 3px 9px;
  border: 1px solid var(--color-border);
  border-radius: 999px;
  background: rgba(10, 16, 28, 0.78);
  color: var(--color-text);
  cursor: pointer;
  white-space: nowrap;
}
.xms-audio-btn:hover,
.xms-audio-btn-on {
  border-color: var(--color-accent-dim);
  color: #fff;
}
/* 分轨/曲目按钮总开关关闭时置 hidden——显式补 [hidden] 规则（§24 第 27 行
 * 同款坑：作者样式可能压过 UA 的 display:none，flex 子项统一显式最稳）。 */
.xms-audio-btn[hidden] {
  display: none;
}
/* 「🔊 点击开启声音」：与舞台上的「待办」浮标 .stage-jump-alert 同一套语汇
 * （琥珀描边 + 深琥珀底 + 呼吸），因为它说的是同一件事——**有事没做，不做就
 * 一直没有**。此前它只复用 .xms-audio-btn 的中性样式，混在「🗣 配音 / ♪ / 🔊
 * 声音」那一排里毫无分别，而它是那一排里唯一一颗「不点就整场无声」的按钮。
 *
 * 【呼吸只动 opacity，不套用 predict-pulse】那条关键帧带 scale(0.7)，用在浮标
 * 那种大药丸上没问题，用在这颗 11px 的小按钮上文字会缩到读不出来。语汇一致靠
 * 的是颜色与节奏（同样 1.6s ease-in-out），不是逐字段照抄。
 *
 * 【不要在这里写 display】上面那条 .xms-audio-btn[hidden] 是它平时的隐藏依据，
 * 本规则一旦带 display 就会把它压掉——没被拒过的时候这颗按钮会一直挂着。 */
.xms-audio-unlock {
  border-color: #e8a94a;
  background: rgba(28, 20, 8, 0.92);
  color: #ffd79a;
  font-weight: 600;
  animation: xms-unlock-pulse 1.6s ease-in-out infinite;
}
.xms-audio-unlock:hover {
  background: rgba(48, 34, 12, 0.96);
  border-color: #f0bd6a;
  color: #ffe6bd;
}
@keyframes xms-unlock-pulse {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.55;
  }
}
/* 动效只负责引起注意，信息由「在不在」承载——关掉动效不丢任何信息（与
 * .stage-jump-alert / #predict-panel.needs-vote 的那条同款）。 */
@media (prefers-reduced-motion: reduce) {
  .xms-audio-unlock {
    animation: none;
  }
}
.xms-log-panel[hidden] {
  display: none; /* display:flex 会压过 UA [hidden]，必须显式（§24 第 27 行同款坑） */
}
.xms-log-panel {
  position: absolute;
  right: 10px;
  bottom: 52px;
  width: min(320px, 46%);
  max-height: 58%;
  z-index: 14;
  pointer-events: auto;
  display: flex;
  flex-direction: column;
  background: rgba(8, 12, 22, 0.94);
  border: 1px solid var(--color-border);
  border-radius: 8px;
  overflow: hidden;
}
.xms-log-head {
  flex: none;
  padding: 6px 12px;
  font-size: 11px;
  font-weight: 600;
  color: var(--color-text-dim);
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.xms-log-list {
  overflow-y: auto;
  padding: 6px 12px 8px;
}
.xms-log-line {
  font-size: 12.5px;
  line-height: 1.55;
  color: #dfe6f0;
  padding: 3px 0;
  border-bottom: 1px dashed rgba(255, 255, 255, 0.07);
}
.xms-log-line:last-child {
  border-bottom: none;
}
.xms-log-name {
  color: var(--color-accent);
  font-weight: 600;
  margin-right: 2px;
}
.xms-log-narr {
  font-style: italic;
  color: #aab6c8;
}

/* 底部对白条（§24 第 22 行）：对白从头顶气泡改为视觉小说式固定条——
 * 「头像 + 名字 + 台词」贴地图底缘，与旁白条同位互斥，零遮挡；「谁在说」由
 * token 高亮脉冲 + 条内名字双重指示。不随视口缩放（在 .xms-viewport 外）。 */
/* [hidden] 必须显式压过下面的 display:flex（同 .stage-replay-banner 处的
 * 注记）——漏掉这条时 hideDialogue() 的 hidden=true 形同虚设：对白条挂着
 * 上一句台词永不消失（线上截图实证「切新回合残留上一回合台词」），还以
 * z-index:6 同位盖住旁白条（z auto），旁白从此「看不见」。 */
.xms-dialogue[hidden] {
  display: none;
}
/* 底部文本带：对白/行动/旁白三者共用的几何锚点（用户反馈「三者都放下方、
 * 字体大一点」）。三者同位互斥、身份靠排版区分，因此左右下缘与圆角必须逐字
 * 一致——差一个像素，段落切换时整条就会跳动。改动这三个值请同步改另外两处。
 *
 * padding-bottom 52px 是给底部控制行让出的净空：不留这道空当，末行文字会从
 * 控件底下穿过去（字号放大后尤其明显）。控件落在这段留白里，读起来像 VN 惯例的
 * 「文本框底部控制行」。
 *
 * 【52 是按最高的那个占用者定的，不是按按钮定的，2026-08-28】这条空当原先是 36px，
 * 按 .xms-log-btn / .xms-audio-bar（bottom:26px，实测高 23px）的尺寸定的。后来
 * .stage-jump-bar 那两颗浮标也搬进这一行，而它实测高 37px——比按钮高 14px，空当
 * 却只剩 3px 富余，于是底对齐让浮标向上长进正文里。1440×900 实测：正文内容盒底边
 * 524、浮标顶边 513，压住 11px（用户截图实证）。
 * 一条空当由谁定尺寸，取决于**站在里面最高的那个**，不是最先站进来的那个。
 * 52 的下限是算出来的：正文最低点 = 带下缘 − P，浮标顶边 = 舞台下缘 − 26 − 37，
 * 而带下缘 = 舞台下缘 − 16 ⇒ 要求 P ≥ 37 + (26 − 16) = 47。取 52 留 5px 余量，
 * 实测 clearance：36→−11 / 44→−3 / 48→+1 / 52→+5。
 * 代价是本带长高 16px（实测 102.6 → 118.6）、同样吃掉 16px 地图，自觉付。
 *
 * 改控件**或浮标条**的高度，都要回来复核这个值；app.js 的 placeJumpBar 里另有一道
 * 运行时夹取，负责在这个静态值不够用时（例如浮标折成两行）把浮标抬走而不是压字。
 *
 * 本带上方的在场者小头像排（.xms-closeup-cast）不用手工同步：它的 bottom 走
 * mapstage.js 实测出来的 --xms-band-top，这里的几何怎么改它都自动让开。 */
.xms-dialogue {
  position: absolute;
  left: 6%;
  right: 6%;
  bottom: 16px;
  padding: 11px 16px 52px;
  border-radius: 9px;
  /* 同 .xms-narration 的可读性三件套（0.96 近实底/纯白/投影），理由见彼处。 */
  background: rgba(8, 12, 22, 0.96);
  color: #fff;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
  font-size: 17px;
  line-height: 1.62;
  display: flex;
  align-items: center;
  gap: 11px;
  z-index: 6;
}
.xms-dialogue-avatar {
  flex: none;
  width: 38px;
  height: 38px;
  border-radius: 50%;
  border: 2px solid var(--color-player);
  background-color: var(--color-panel-alt);
  background-size: cover;
  background-position: center 22%; /* 同 token：竖版肖像取上部露脸 */
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 15px;
  font-weight: 600;
}
.xms-dialogue-name {
  flex: none;
  color: var(--color-accent);
  font-weight: 700;
}
.xms-dialogue-text {
  flex: 1;
  min-width: 0;
}

/* ---- 地图为主视觉（§24 第 17 行）：仅 map 形态加高舞台区——场景舞台的
 * Pixi 画布按既有 340px 初始化，动它要重建渲染器，收益低。地图布局按
 * applyWorld 时的容器实际尺寸计算，加高即自动摊开节点。 ---- */
.stage-wrap.mode-map {
  height: min(64vh, 720px);
  min-height: 420px;
}

/* ---- 文字剧情/预测折叠：只在舞台可见（body.stage-on，app.js 挂）时生效，
 * 文字形态下折叠头隐藏、内容恒展开。折叠态由 .folded 类表达。 ---- */
.fold-head {
  display: none;
  width: 100%;
  text-align: left;
  padding: 8px 10px;
  margin-bottom: 8px;
  border: 1px solid var(--color-border);
  border-radius: 6px;
  background: var(--color-panel-alt);
  color: var(--color-text-dim);
  font-size: 13px;
  cursor: pointer;
}
body.stage-on .fold-head {
  display: block;
}
.fold-arrow {
  display: inline-block;
  transition: transform 0.15s ease;
}
body.stage-on .folded-ctl .fold-arrow {
  transform: rotate(-90deg);
}
body.stage-on #story-list.folded,
body.stage-on #predict-body.folded {
  display: none;
}
.fold-title .fold-arrow {
  display: none; /* 文字形态：标题不可折叠，箭头不出现 */
}
body.stage-on .fold-title {
  cursor: pointer;
  user-select: none;
}
body.stage-on .fold-title .fold-arrow {
  display: inline-block;
}
.fold-hint {
  font-weight: normal;
  font-size: 12px;
  color: #e8a94a;
  margin-left: 8px;
}

/* 旁白条：回到底部文本带，与对白条、动作条同位（用户反馈——演出时观众视线
 * 始终在下方立绘与对白上，旁白独自待在顶部要来回移动视线）。此前的「上部居中
 * 窄卡片」方案就此作废。
 *
 * 同位之后身份全靠排版扛：无头像、无名牌、无左强调竖线，居中斜体加「◆」装饰
 * 符，内缩的横向留白让文字列比另外两者窄一圈——它是画外音，视觉重量最轻。
 * 底色比对白条略沉（0.93 对 0.96）让它再退半步，但仍守住可读性三件套（近实底
 * + 白字 + 投影，用户实测反馈过的教训，不得回退）。
 * 左右下缘与圆角同 .xms-dialogue，理由见彼处。 */
.xms-narration {
  position: absolute;
  left: 6%;
  right: 6%;
  bottom: 16px;
  padding: 11px 15% 52px;
  border-radius: 9px;
  background: rgba(8, 12, 22, 0.93);
  color: #f2f3f6;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
  font-size: 16.5px;
  font-style: italic;
  line-height: 1.68;
  text-align: center;
  z-index: 6;
}
.xms-narration[hidden] {
  display: none;
}

/* 动作条：行动段（segment.type==="action"）的第三套文本表面。此前行动与旁白
 * 共用旁白条，观众分不清「某人做了什么」和「叙述者在描述什么」。三者现在同处
 * 底部文本带、一眼可辨——对白带头像圆与名牌，行动带左强调竖线与「A 动作 B」
 * 首行，旁白居中斜体无名牌。互斥纪律照旧：同一时刻只显示其一。
 * 可读性三件套（近实底 + 白字 + 投影）同前两者，不得回退。
 * 左右下缘与圆角同 .xms-dialogue，理由见彼处。 */
.xms-action[hidden] {
  /* 必须显式压过下面的 display:flex——漏掉这条时 hideAction() 形同虚设，
   * 动作条会挂着上一段残留与对白/旁白同屏并存（同 .xms-dialogue 教训）。 */
  display: none;
}
.xms-action {
  position: absolute;
  left: 6%;
  right: 6%;
  bottom: 16px;
  padding: 11px 16px 52px 13px;
  border-radius: 9px;
  border-left: 3px solid var(--color-accent);
  background: rgba(8, 12, 22, 0.96);
  color: #fff;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
  font-size: 16.5px;
  line-height: 1.62;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 3px;
  z-index: 6;
}
/* 三元组首行：「<符号> A <动作词> B」独占一行，正文另起一行（用户反馈「A 动作
 * B 展现可以再突出一些」）。此前四个成分挤在同一基线上，正文一长，结构就被
 * 冲散了。三元组缺席时本行整体 hidden（见 applyActionMeta），动作条退回单行
 * 正文——那是解析层降级后最常见的形态，必须无缝而不是留一道空隙。 */
.xms-action-head[hidden] {
  display: none;
}
.xms-action-head {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 7px;
  max-width: 100%;
}
.xms-action-name[hidden] {
  display: none;
}
.xms-action-name {
  flex: none;
  color: var(--act-accent, var(--color-accent));
  font-weight: 700;
  letter-spacing: 0.02em;
}
.xms-action-name::before {
  content: var(--act-mark, "▸ ");
  font-weight: normal;
}
/* 正文降一档字重与亮度，把首行的强调色三元组衬出来——同一条里「谁对谁做了
 * 什么」是结构，正文是展开，两者不该抢同一个视觉层级。 */
.xms-action-text {
  min-width: 0;
  color: #eceef2;
  font-size: 16px;
}

/* 行动三元组的两个附加成分：动作词与受动者，构成「<符号> A <动作词> B　正文」。
 * Director 没给三元组时它们 hidden，动作条退回「名字 + 正文」——那是最常见的
 * 形态（模型漏填、解析层降级都落在这里），必须无缝而不是看着像出了错。 */
.xms-action-verb[hidden],
.xms-action-target[hidden] {
  display: none;
}
/* 动作词做成胶囊：它是三元组的谓语，此前 12px + 半透明，被两侧的人名压得
 * 几乎看不见。底色用 color-mix 调出该动作分组的淡色块——不支持 color-mix 的
 * 浏览器落到前一条中性底色，胶囊仍在、只是不随分组换色，属无副作用降级。 */
.xms-action-verb {
  flex: none;
  color: var(--act-accent, var(--color-accent));
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 0.06em;
  padding: 1px 10px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.1);
  background: color-mix(in srgb, var(--act-accent, #e8a94a) 20%, transparent);
  text-shadow: none; /* 有底色托着，再叠投影会糊 */
}
.xms-action-target {
  flex: none;
  color: #f0ece2;
  font-weight: 700;
}
/* 受动者是物品/地点时已由 JS 加了〈〉【】饰记，这里再降一档权重与人物拉开
 * ——同样一行字里，「谁」应该比「什么东西」更抓眼。 */
.xms-action-target[data-kind="item"],
.xms-action-target[data-kind="loc"] {
  color: #cdc7b8;
  font-weight: 500;
}
/* 动作词与受动者之间的指向感：有受动者时才画箭头，不及物动作不画。箭头是
 * 「A → B」这层关系的唯一视觉线索，此前 opacity 0.7 太弱，这里给足权重。 */
.xms-action-target::before {
  content: "→ ";
  color: var(--act-accent, var(--color-accent));
  font-weight: normal;
  opacity: 0.9;
}

/* 十一个行动动词各自的符号与强调色。符号用通用 Unicode 而非 emoji——emoji
 * 在不同系统上字形与基线差异过大，窄条上一眼就乱。
 * 配色只分三组而不是十一色：对抗（暖红）/ 交互（琥珀，即默认）/ 感知（青），
 * 让观众扫一眼就知道「这是打起来了」还是「这是在找东西」，而不必逐个记符号。 */
.xms-action[data-act="give"] {
  --act-mark: "⇢ ";
}
.xms-action[data-act="take"] {
  --act-mark: "⇠ ";
}
.xms-action[data-act="use"] {
  --act-mark: "◈ ";
}
.xms-action[data-act="touch"] {
  --act-mark: "◌ ";
}
.xms-action[data-act="attack"] {
  --act-mark: "✦ ";
  --act-accent: #e0785f;
}
.xms-action[data-act="block"] {
  --act-mark: "⊓ ";
  --act-accent: #e0785f;
}
.xms-action[data-act="retreat"] {
  --act-mark: "← ";
  --act-accent: #e0785f;
}
.xms-action[data-act="approach"] {
  --act-mark: "→ ";
  --act-accent: #6fb3c9;
}
.xms-action[data-act="search"] {
  --act-mark: "⌕ ";
  --act-accent: #6fb3c9;
}
.xms-action[data-act="observe"] {
  --act-mark: "◉ ";
  --act-accent: #6fb3c9;
}
.xms-action[data-act="gesture"] {
  --act-mark: "≈ ";
  --act-accent: #6fb3c9;
}
/* 左强调竖线随动作分组换色，是最外围也最先被余光捕捉的信号。 */
.xms-action[data-act] {
  border-left-color: var(--act-accent, var(--color-accent));
}

/* 行动段的立绘动作脉冲：280ms 上挑回弹，幅度克制（7px + 3% 缩放）——给
 * 「这个人在动」一点体感，不抢台词的戏。
 * 只改独立变换属性（translate/scale），刻意不碰 transform：立绘的 transform
 * 承载状态位移（普通态 translateY(0) scale(0.94)、特写态 translate(±22px,0)
 * scale(1.08)），用 animation 改 transform 会在动画期间整个覆盖它，特写里的
 * 立绘会先跳回中位再弹。独立属性与 transform 相乘而非互斥，可以叠加。 */
@keyframes xms-portrait-act {
  0% {
    translate: 0 0;
    scale: 1;
  }
  38% {
    translate: 0 -7px;
    scale: 1.03;
  }
  100% {
    translate: 0 0;
    scale: 1;
  }
}
.xms-portrait-act {
  animation: xms-portrait-act 280ms ease-out;
}
/* 「◆」是旁白在底部文本带里的身份标记——同位之后，它和居中斜体一起承担
 * 「这是画外音不是某人在说」的全部指示，字号随正文一并放大。 */
.xms-narration-text::before {
  content: "◆ ";
  color: #9fc1ef;
  font-style: normal;
  font-size: 12px;
}
.xms-narration-text::after {
  content: " ◆";
  color: #9fc1ef;
  font-style: normal;
  font-size: 12px;
}

/* 天气粒子层：snow/storm 用纯 CSS 动画粒子（≤40 个）。粒子的水平位置/延迟/
 * 时长/偏移全部由索引派生（mapstage.js 里 i*质数%范围），不掺 Math.random，
 * 保证同一份世界投影每次画出来的粒子分布一致。
 * fog/夜晚的**染色**不在这一层——它盖在 .xms-viewport 之上，染色画在这里会
 * 把节点/连线/头像一起蒙灰。染色见 .xms-backdrop::before/::after，粒子留在
 * 最上层（雪该落在人前面）。 */
.xms-weather {
  position: absolute;
  inset: 0;
  overflow: hidden;
}
.xms-snow {
  position: absolute;
  top: -6%;
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.85);
  animation-name: xms-snow-fall;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
.xms-snow-storm {
  width: 3px;
  height: 11px;
  border-radius: 2px;
  background: rgba(220, 230, 245, 0.75);
  animation-name: xms-snow-fall-storm;
}
@keyframes xms-snow-fall {
  from {
    transform: translateY(0);
  }
  to {
    transform: translateY(380px);
  }
}
@keyframes xms-snow-fall-storm {
  from {
    transform: translate(0, 0) rotate(14deg);
  }
  to {
    transform: translate(var(--xms-drift, -20px), 380px) rotate(14deg);
  }
}

/* 版本角标（v1/v2 双实例并行期间标明当前实例）：固定右下角，不遮挡交互 */
.ver-badge-fixed {
  position: fixed;
  right: 10px;
  bottom: 10px;
  z-index: 50;
  padding: 2px 9px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.5px;
  border: 1px solid var(--color-border);
  background: var(--color-panel-alt);
  color: var(--color-text-dim);
  pointer-events: none;
}

/* ---------------- BGM 曲目选择（§24 第 52 行） ---------------- */

.xms-music-panel {
  position: absolute;
  right: 10px;
  bottom: 54px;
  z-index: 8;
  background: rgba(8, 12, 22, 0.96);
  border: 1px solid rgba(160, 190, 230, 0.25);
  border-radius: 8px;
  padding: 8px;
}
.xms-music-panel[hidden] {
  display: none;
}
.xms-music-select {
  background: #131b2a;
  color: #eef2f8;
  border: 1px solid rgba(160, 190, 230, 0.3);
  border-radius: 6px;
  font-size: 12px;
  padding: 4px 6px;
  max-width: 280px;
}
/* auto 方案「正在播放」行：select 下方一行小字 */
.xms-music-now {
  margin-top: 6px;
  font-size: 11px;
  color: #9fb4d4;
  max-width: 280px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.xms-music-now[hidden] {
  display: none; /* 纪律：新元素显式补 [hidden]，防作者样式压 UA 规则 */
}
/* 署名区：默认折叠的一行小字，CC-BY 曲目的界面署名落点 */
.xms-music-credit-toggle {
  margin-top: 6px;
  background: none;
  border: none;
  padding: 0;
  color: #7f92ae;
  font-size: 11px;
  cursor: pointer;
}
.xms-music-credit-toggle:hover {
  color: #9fb4d4;
}
.xms-music-credits {
  margin-top: 4px;
  font-size: 11px;
  line-height: 1.5;
  color: #7f92ae;
  max-width: 280px;
}
.xms-music-credits[hidden] {
  display: none;
}

/* 角色卡之后的20轮剧情蓝图审阅区。长Prompt只在运营主动加载后进入DOM。 */
.story-blueprint-card .kv { margin: 12px 0; }
.story-blueprint-detail { margin-top: 18px; }
.blueprint-act-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
  margin-bottom: 16px;
}
.blueprint-act,
.blueprint-round,
.blueprint-ending,
.blueprint-trace,
.blueprint-raw {
  border: 1px solid #d9dee8;
  border-radius: 10px;
  background: #fff;
}
.blueprint-act { padding: 12px; }
.blueprint-act p { margin: 6px 0 0; }
.blueprint-round,
.blueprint-ending,
.blueprint-trace,
.blueprint-raw { margin: 8px 0; padding: 10px 12px; }
.blueprint-round > summary,
.blueprint-ending > summary,
.blueprint-trace > summary,
.blueprint-raw > summary { cursor: pointer; }
.blueprint-decision {
  margin: 9px 0;
  padding: 10px 12px;
  border-left: 3px solid #5a8dee;
  background: rgba(90, 141, 238, 0.07);
}
.blueprint-decision p { margin: 5px 0; }
.blueprint-trace pre,
.blueprint-raw pre {
  max-height: 520px;
  overflow: auto;
  white-space: pre-wrap;
  word-break: break-word;
  font-size: 12px;
}
@media (max-width: 800px) {
  .blueprint-act-grid { grid-template-columns: 1fr; }
}

/* ============================================================
   切片视图（slice view）—— c-20260909-demo-b-0915 tasks 7.1-7.9
   specs/slice-presentation：1920×1080 四区布局（顶部房间状态栏 / 左侧角色区 /
   中间命运切片 / 右侧剧情列表）。本节整体追加在文件末尾：同名选择器
   （#app / .chat-* 等）按层叠以本节为准，旧视图规则不再可达（见文件上方
   「六区 Grid 布局」一节的下线说明）。
   ============================================================ */

/* 四区 Grid：基准画布 1920×1080，小窗等比压缩不出页面级横向滚动。
   （--color-mine 定义在文件顶部唯一的 :root 块里：配色门禁按「:root 只有
   一条规则」解析，不在这里另起第二个 :root。） */
#app.slice-app {
  display: grid;
  position: relative;
  grid-template-columns: 240px minmax(0, 1fr) 300px;
  grid-template-rows: var(--topbar-h) minmax(0, 1fr);
  grid-template-areas:
    "topbar topbar topbar"
    "chars  main   story";
  gap: var(--gap);
  height: 100vh;
  padding: var(--gap);
  min-width: 0;
  overflow: hidden; /* 中间区不得出现页面级横向滚动 */
}

/* ---------- 顶部状态栏（tasks 7.2） ---------- */

#app.slice-app #topbar {
  display: flex;
  align-items: center;
  gap: 18px;
  overflow: hidden;
}
.room-name {
  font-size: 16px;
  font-weight: 800;
  color: var(--color-text);
  white-space: nowrap;
}
.slice-progress {
  font-size: 13.5px;
  font-weight: 700;
  color: var(--color-accent);
  white-space: nowrap;
}
.topbar-me {
  margin-left: auto;
  color: var(--color-text-dim);
  font-size: 12.5px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ---------- BGM 曲目选择（2026-09-15「能关闭、能选曲目」） ---------- */

.bgm {
  position: relative;
  flex: none;
  margin-left: 12px;
}
.bgm-toggle {
  padding: 3px 11px;
  border-radius: 999px;
  background: transparent;
  border: 1px solid var(--color-border);
  color: var(--color-text-dim);
  font-family: inherit;
  font-size: 12px;
  max-width: 190px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  cursor: pointer;
}
.bgm-toggle:hover {
  color: var(--color-text);
  border-color: var(--color-accent-dim);
}
/* 在播时按钮自己就是状态灯——不点开也看得出有没有在放。 */
.bgm-toggle.bgm-on {
  color: var(--color-accent);
  border-color: var(--color-accent-dim);
}
/* 【为什么是 fixed 而不是 absolute】#topbar 有 overflow-x: auto，absolute 的
   下拉面板会被它裁掉（只露出状态栏那 52px 高的一条）。fixed 只被带 transform/
   filter 的祖先裁剪，而 #app / #topbar 都没有——坐标按 #app 的 padding
   （var(--gap)）+ #topbar 的 padding（var(--gap)）算出来，跟着变量走。 */
.bgm-panel {
  position: fixed;
  top: calc(var(--topbar-h) + var(--gap) + 6px);
  right: calc(var(--gap) * 2);
  z-index: 60;
  display: flex;
  flex-direction: column;
  gap: 8px;
  width: 260px;
  max-height: 60vh;
  padding: 10px;
  background: var(--color-panel);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  box-shadow: 0 10px 28px rgba(0, 0, 0, 0.45);
}
/* 【写了 display 就必须补 [hidden]】作者样式的 display 无条件盖过 UA 的
   [hidden]{display:none}——本仓反复踩的坑（.stage-jump / .slice-aux /
   .quiz-hint / .settle-page）。这四个都会被 JS 置 hidden，四条一个不能少。 */
.bgm-panel[hidden],
.bgm-empty[hidden],
.bgm-note[hidden],
.bgm-stop[hidden] {
  display: none;
}
/* 曲目列表：子元素只能是曲目项（空态/提示/「关闭音乐」都在容器外）。 */
.bgm-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
  overflow-y: auto;
}
.bgm-item {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 6px 9px;
  border-radius: 7px;
  background: transparent;
  border: 1px solid transparent;
  color: var(--color-text-dim);
  font-family: inherit;
  font-size: 12.5px;
  text-align: left;
  cursor: pointer;
}
.bgm-item:hover {
  background: var(--color-panel-alt);
  color: var(--color-text);
}
.bgm-item.active {
  border-color: var(--color-accent-dim);
  color: var(--color-accent);
}
.bgm-item-name {
  font-weight: 700;
}
.bgm-item-credit {
  font-size: 10.5px;
  opacity: 0.65;
}
.bgm-empty,
.bgm-note {
  padding: 4px 2px;
  color: var(--color-text-dim);
  font-size: 12px;
  line-height: 1.5;
}
.bgm-stop {
  padding: 5px 10px;
  border-radius: 7px;
  background: transparent;
  border: 1px solid var(--color-border);
  color: var(--color-text-dim);
  font-family: inherit;
  font-size: 12px;
  cursor: pointer;
}
.bgm-stop:hover:not(:disabled) {
  color: var(--color-text);
  border-color: var(--color-accent-dim);
}
.bgm-stop:disabled {
  opacity: 0.4;
  cursor: default;
}

/* ---------- 左侧角色区（tasks 7.3） ---------- */

.char-item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  margin-bottom: 8px;
  padding: 8px 10px;
  background: var(--color-panel-alt);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  color: var(--color-text);
  font-family: inherit;
  font-size: 13.5px;
  text-align: left;
  cursor: pointer;
}
.char-item:hover {
  border-color: var(--color-accent-dim);
}
/* 当前玩家：区别描边/强调色。 */
.char-item-me {
  border-color: var(--color-accent);
  box-shadow: 0 0 0 1px var(--color-accent);
}
.char-item-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: 700;
}
.char-item .char-avatar {
  width: 40px;
  height: 40px;
  flex: none;
}
.char-avatar-big {
  width: 96px;
  height: 96px;
}
/* 角色列表上的状态 Tag（仅本人可见——card_modules 整块只对本人下发）。
   【2026-09-15 改名】原来的类名里带 health。它展示的是**模组第一个信息模块**
   的当前状态，模组换一个就不再是「健康」了，那个名字会误导下一个人。 */
.char-state-tag {
  flex: none;
  padding: 2px 8px;
  border-radius: 999px;
  background: var(--color-accent-dim);
  color: #ffe9d2;
  font-size: 11px;
  font-weight: 700;
  white-space: nowrap;
}

/* 角色卡里的模组信息模块：一节一个模块（节标题 + 状态药丸 + 说明）。节名与状态
   文案全部来自模组配置，这里只管排版。
   .char-module-private（「仅自己可见」标签）按 2026-09-15 需求方裁决默认不渲染，
   样式**刻意留着**——那条裁决说的是「暂时」，回退只改 app.js 一个常量。
   别当成死代码清掉。 */
.char-modules {
  display: flex;
  flex-direction: column;
  gap: 12px;
  margin-top: 16px;
}
.char-module-label {
  display: flex;
  align-items: center;
  gap: 7px;
  color: var(--color-text-dim);
  font-size: 12px;
}
.char-module-private {
  padding: 2px 6px;
  border-radius: 5px;
  background: var(--color-panel-alt);
  color: var(--color-text-dim);
  font-size: 10px;
  font-weight: 700;
}
.char-module-tag {
  display: inline-block;
  width: max-content;
  margin-top: 8px;
  padding: 4px 11px;
  border-radius: 999px;
  background: var(--color-accent-dim);
  color: #ffe9d2;
  font-size: 12.5px;
  font-weight: 800;
}
.char-module-desc {
  margin-top: 7px;
  color: var(--color-text-dim);
  font-size: 12px;
  line-height: 1.55;
}

/* 角色卡浮层内的「物品」入口与列表（0916 暂行）。 */
.char-items-entry {
  display: block;
  width: 100%;
  padding: 8px 12px;
  background: var(--color-panel-alt);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  color: var(--color-text);
  font-family: inherit;
  font-size: 13px;
  font-weight: 700;
  text-align: left;
  cursor: pointer;
}
.char-items-entry:hover {
  border-color: var(--color-accent-dim);
}
.char-items-list {
  margin-top: 8px;
}
.char-item-row {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 6px 4px;
  border-bottom: 1px solid var(--color-border);
}
.char-item-row:last-child {
  border-bottom: none;
}
.char-item-icon {
  flex: none;
}
/* 运营在模组里配的物品图标（card.items[].icon_url）。定死尺寸而不是由图片自己
   撑开：上传的是运营自备的图，长宽比不可控，不定死的话一张竖图就能把整行顶高、
   把下面的物品挤出卡外。object-fit:cover 保证不同比例都填满这个方格。 */
.char-item-icon-img {
  width: 20px;
  height: 20px;
  border-radius: 4px;
  object-fit: cover;
}
.char-item-title {
  font-weight: 700;
  white-space: nowrap;
}
.char-item-desc {
  color: var(--color-text-dim);
  font-size: 12.5px;
}
.char-item-stats {
  margin-top: 8px;
  color: var(--color-text-dim);
  font-size: 12px;
}

/* ---------- 中间命运切片区（tasks 7.4） ---------- */

#slice-main {
  grid-area: main;
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  overflow-y: auto; /* 结局态下方要承接结算，纵向可滚；横向永不滚 */
  overflow-x: hidden;
}

/* 16:9 切片画布：宽度吃满中栏，高度随比例；纵向放不下时由 max-width 钳住
   （以可用高度反推），margin auto 居中，保证 16:9 完整展示不裁切。 */
.slice-stage {
  position: relative;
  flex: none;
  width: 100%;
  max-width: calc((100vh - var(--topbar-h) - 120px) * 16 / 9);
  aspect-ratio: 16 / 9;
  margin: 0 auto;
  background: #10141b;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  overflow: hidden;
}
.slice-image {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain; /* 完整展示，不裁切 */
  /* 点击图片不触发任何操作：不绑监听即可，无需 pointer-events 特技 */
}
.slice-empty {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--color-text-dim);
  font-size: 15px;
}
/* 【display 写在这里就必须把 hidden 补回来】同 .slice-aux 那条（见下方「辅助
   文字框」一节的长注释）：作者样式的任何一条 display 都无条件盖过 UA 样式表的
   [hidden]{display:none}。.slice-empty / .quiz-hint / .story-count 这三处是写
   .slice-aux 那条教训时的漏网——JS 早就在置 hidden，CSS 这边却一直没跟上，于是
   「等待开局…」在有图时仍在 DOM 流里（被图盖住，但屏幕阅读器照读，图加载失败
   就直接透出来），感叹号玩法提示更是从第一帧起常驻不灭。
   headless 门禁看不见层叠（假 DOM 没有 CSS 引擎，getComputedStyle 测不出来），
   所以这三条由各自用例直接读 style.css 源文本钉住。 */
.slice-empty[hidden] {
  display: none;
}
/* 左上角切片序号角标。 */
.slice-seq-badge {
  position: absolute;
  top: 10px;
  left: 10px;
  z-index: 4;
  padding: 4px 12px;
  border-radius: 999px;
  background: rgba(10, 16, 28, 0.82);
  color: var(--color-text);
  font-size: 13px;
  font-weight: 800;
  letter-spacing: 0.04em;
}

/* 辅助文字框：半透明背景覆盖切片底部。展开态标题+正文+「收起」；收起态仅
   标题栏+「展开」。 */
/* 【高度上限按页分档（2026-09-15，需求方「让看得人体验更好一些」）】
   两页的体量实测差一个数量级（线上 fate_slices：aux_text 中位 97 字 / 最长 339，
   feedback_text 中位 504 / 最长 620，且第 2 页常有 8 段）。所以上限**按页**给，
   不是按内容猜——判据是 ui.auxPage 这个本来就有的页号，不涉及任何对正文的分析。
   · 第 1 页（问题前·剧情铺垫）：46%。切片刚出现、玩家正在看画，PRD 正文写的是
     「图片比例为 16:9，在中间区域完整展示」「以画面为主、文字为辅」，这时候不该
     让文字盖住画。实测这一页绝大多数根本撑不到上限。
   · 第 2 页（问题后·剧情反馈）：64%。它只在倒计时结束、答案揭晓之后才解锁——
     那张画已经看了三分钟，此刻正文才是要读的东西。
   算术留在这儿好让下一个人重新权衡。**字号或行距一动就要回来重算这一整段**——
   它的每一个输入项都挂在那两个值上（2026-09-15 第二轮漏算过一次，评审逮出来了：
   那次只重算了 .slice-aux-body p 的段距比值，这里的结论却还停在旧数上）。

   基准：1920×1080 下切片舞台高 735px、正文可用宽 1257px；全角字宽 = 字号。

   · 第 1 页：19px / 1.85 ⇒ 66 字/行、行高 35.15px。线上最长 339 字 ≈ 6 行
     ≈ 211px + 段距，46%≈338px **仍然一屏装得下**，这一档不用动。
   · 第 2 页：19px / 1.95 ⇒ 66 字/行、行高 37.05px、段距 20.9px。线上最长那条
     620 字 8 段 ≈ 10 行正文 + 7 个段距 ≈ 370 + 146 = **516px**，而 64%≈470px
     ——**已经装不下了，要滚约 1.1 屏**。（17px/1.75 那一版是 470px 对 470px，
     正好一屏，所以当时写的是「基本一屏读完」。）
   这一档**有意不跟着提**：再往上就压过画面一半以上，而第 2 页仍然覆盖在切片上。
   滚 1.1 屏是可接受的代价，滚动条本身就是「下面还有」的提示（见 .slice-aux-body）。
   要改就改下面那一个百分数，并把这段算术一起更新。 */
.slice-aux {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 5;
  background: rgba(10, 14, 20, 0.82);
  color: var(--color-text);
  --slice-aux-max-h: 46%;
  max-height: var(--slice-aux-max-h);
  display: flex;
  flex-direction: column;
}
/* .aux-long 由 renderSliceAux 按页号挂（第 2 页），理由见上。 */
.slice-aux.aux-long {
  --slice-aux-max-h: 64%;
}
.slice-aux-bar {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 8px 14px;
}
/* 【display 写在这里就必须把 hidden 补回来】作者样式里的任何一条 display 都
   无条件盖过 UA 样式表的 [hidden]{display:none}（同 .stage-jump 那次线上教训）。
   .slice-aux 未完成切片时置 hidden、.slice-aux-pager 结局张置 hidden，都靠这条
   才真的看不见；headless 门禁只看得到 el.hidden 的布尔值，所以由
   tests/js/slice_aux_pages.mjs 里读 CSS 的断言钉住。 */
.slice-aux[hidden],
.slice-aux-pager[hidden] {
  display: none;
}
.slice-aux-head {
  display: flex;
  align-items: baseline;
  gap: 8px;
  min-width: 0;
}
.slice-aux-title {
  font-size: 14.5px;
  font-weight: 800;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* 阶段标签「回答前 / 回答后」：跟在页标题后的小胶囊。 */
.slice-aux-stage {
  flex: none;
  padding: 1px 8px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.1);
  color: var(--color-text-dim);
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: 0.04em;
  white-space: nowrap;
}
.slice-aux-nav {
  flex: none;
  display: flex;
  align-items: center;
  gap: 10px;
}
/* 翻页「‹ n / 2 ›」：结局张（只有一页）整块 hidden。 */
.slice-aux-pager {
  display: inline-flex;
  align-items: center;
  gap: 4px;
}
.slice-aux-pagebtn {
  flex: none;
  width: 22px;
  height: 22px;
  padding: 0;
  border-radius: 999px;
  background: transparent;
  border: 1px solid var(--color-border);
  color: var(--color-text-dim);
  font-family: inherit;
  font-size: 13px;
  line-height: 1;
  cursor: pointer;
}
.slice-aux-pagebtn:hover:not(:disabled) {
  color: var(--color-text);
  border-color: var(--color-accent-dim);
}
/* 揭晓前第 2 页锁定 ⇒ 右翻置灰不可点（disabled 是唯一权威，样式只跟随）。 */
.slice-aux-pagebtn:disabled {
  opacity: 0.35;
  cursor: default;
}
.slice-aux-page {
  min-width: 34px;
  text-align: center;
  color: var(--color-text-dim);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}
.slice-aux-toggle {
  flex: none;
  padding: 3px 12px;
  border-radius: 999px;
  background: transparent;
  border: 1px solid var(--color-border);
  color: var(--color-text-dim);
  font-family: inherit;
  font-size: 12px;
  cursor: pointer;
}
.slice-aux-toggle:hover {
  color: var(--color-text);
  border-color: var(--color-accent-dim);
}
/* 【正文可读性（2026-09-15，需求方原话：「这个文字的展示 是不是可以调整一下，
   让看得人 体验更好一些」）】
   真正的病根是**行长**，不是字号。此前正文宽度 = 切片舞台宽 − 28px，1920 宽屏上
   一行 117 个汉字（实测算），而中文舒适行长是 30–45 字。行一长，眼睛从行尾回到
   下一行行首就容易串行，第 2 页（剧情反馈，常有 8 段）读起来是一堵墙。
   三处改动，按收益排序：
     1. max-width: 36em —— 36 字一行（em 跟着 font-size 走，改字号不用重算）。
        **左对齐不居中**：标题栏的页标题也是左起，正文跟它对齐才有一条共同的
        视觉基准线；居中的话正文会从标题底下平移出去。
     2. font-size 13.5 → 15px —— 这是长文，不是标签。
     3. 段间距 > 行距 —— 见下面 .slice-aux-body p，让「段」成为视觉单元。
   line-height 1.7 原样不动，本来就合适。 */
.slice-aux-body {
  overflow-y: auto;
  padding: 0 18px 12px;
  /* 【2026-09-15 需求方裁决：正文铺满整个面板，不做窄栏】原话：「这个文字 大一点，
     另外为啥不覆盖所在窗口？？」——**别当成回归改回去**。
     此前这里是 `max-width: calc(36em + 28px)`，把正文收成 36 字/行的窄栏（那个
     +28px 也不是凑的：全局 box-sizing 是 border-box，不把左右内衬加回来，padding
     会从 36em 里吃掉近 2 个字，浏览器实测只剩 29 字/行）。收窄的理由是中文舒适
     行长 30–45 字，而铺满整宽时 1920 屏上一行 100 多字，眼睛从行尾回到行首容易
     串行。
     那条理由本身没被推翻——被推翻的是**用留白去换它**：半透明面板铺满整宽，正文
     却只占左边三分之一，右边一大片空着，看起来像渲染坏了而不像排版。
     代价照直写，而且是**浏览器实测**的数，不是估的：1920×1080 下切片舞台宽
     1306px，正文可用宽 1257px，17px 的全角字正好 17px 宽 ⇒ **73 字/行**
     （此前这里估的「100 字上下」是按更早那版更宽的舞台推的，实测没那么糟，
     但仍在舒适区 30–45 字之外）。真要两全，正确的方向是**分栏**
     （column-count，每栏 ~36 字、整体仍铺满），而不是把窄栏原样加回来。
     分栏是**形态变化，需求方要拍板**，所以这里只留方向不实现。 */
  /* 【2026-09-15 第二轮：只放大字号与行距，**不动宽度**】需求方当天先问「怎么让
     UI 更友好一些」，在「加大字号行高 / 居中留白 / 分栏」三个方向里选了第一个。
     上面那条「铺满整宽」的裁决因此**继续有效**，别拿这次当借口把 max-width 加回来。

     为什么加大字号也能缓解行长：1920 下正文可用宽 1257px，全角字宽=字号，
     17px ⇒ 73 字/行，19px ⇒ **66 字/行**。离舒适区 30–45 字仍有距离，但这是在
     「不收窄」这个约束下唯一还能动的杠杆——真要根治只能分栏或留白，那两条当天
     都被否了。行距同步 1.75→1.85：行越长，回扫越依赖行距把上下行分开。 */
  font-size: 19px;
  line-height: 1.85;
  /* 滚动条是这里唯一的「下面还有」提示：有溢出才出现，没溢出零痕迹。
     （渐变遮罩做不到这点——CSS 判断不了溢出，没内容可滚时会留一道假阴影。） */
  scrollbar-width: thin;
  scrollbar-color: rgba(255, 255, 255, 0.3) transparent;
}
/* 第 2 页「问题后 · 剧情反馈」再松一档行距：它实测是第 1 页体量的 5 倍上下
   （线上中位 504 字 vs 97 字、常有 8 段），是**要连读好几分钟**的长文，而第 1 页
   是看画时扫一眼的铺垫。.aux-long 按定义就是「当前在第 2 页」（app.js 的
   `aux.classList.toggle("aux-long", page === 2)`，判据只有页号），所以拿它当页号
   用是准确的——但那个类名说的是高度档位，将来若有人让第 1 页也变长，**要另起一个
   按页号的类，别让这条跟着高度走**。 */
.slice-aux.aux-long .slice-aux-body {
  line-height: 1.95;
}

/* 「回答后」标签在第 2 页用强调色：这一页的内容**揭晓之后才解锁**，和第 1 页不是
   同一种东西——第 1 页任何时候都能看，第 2 页是本回合的结果。两页的标签此前一模
   一样（都是灰底灰字），玩家翻过去看不出「这是刚刚才能看的东西」。
   前景用深墨而不是白字：底色是橙色强调色，白字在它上面对比度不达标。 */
.slice-aux.aux-long .slice-aux-stage {
  background: var(--color-accent);
  color: #2f1a05; /* 同 .quiz-handle / .quiz-hint 的橙底深墨，AA 达标 */
}

/* 段距压过 .md-body p 的 10px（同特指度，靠本文件里的先后顺序取胜）。
   用 em 给是为了跟着字号走：字号从 15px 调到 17px 之后这里一个字都不用改。
   19px/1.85 下 1.1em ≈ 20.9px：段间基线距 35.15+20.9=56.05px，段内 35.15px，
   约 1.59 倍——「明显大于行距」才能让人一眼看出这是新的一段。（第 2 页行距 1.95
   时是 37.05+20.9=57.95 比 37.05，约 1.56 倍，同样成立。）
   **行距再往上调要回来算这个比值**：行距涨而段距不涨，段落边界就糊了。 */
.slice-aux-body p {
  margin: 0 0 1.1em;
}
.slice-aux-body p:last-child {
  margin-bottom: 0;
}
.slice-aux-body::-webkit-scrollbar {
  width: 8px;
}
.slice-aux-body::-webkit-scrollbar-track {
  background: transparent;
}
.slice-aux-body::-webkit-scrollbar-thumb {
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.3);
}
/* 切片自身的标题：两页形态下标题栏让给页标题，切片 title 落到第 1 页首行。 */
.slice-aux-slice-title {
  margin-bottom: 2px;
  font-size: 14.5px;
  font-weight: 800;
  line-height: 1.4;
}
.slice-aux.collapsed .slice-aux-body {
  display: none;
}

/* ---------- 推进等待 / 停拍倒计时（tasks 7.8） ---------- */

.slice-waiting {
  position: absolute;
  top: 10px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 6;
  padding: 6px 16px;
  border-radius: 999px;
  background: rgba(10, 16, 28, 0.86);
  color: var(--color-text);
  font-size: 13px;
  font-weight: 700;
  white-space: nowrap;
}
.slice-waiting-loading::before {
  content: "";
  display: inline-block;
  width: 10px;
  height: 10px;
  margin-right: 8px;
  border-radius: 50%;
  border: 2px solid var(--color-accent);
  border-top-color: transparent;
  animation: slice-spin 0.9s linear infinite;
  vertical-align: -1px;
}
@keyframes slice-spin {
  to { transform: rotate(360deg); }
}

/* ---------- 玩法提示与面板（tasks 7.6） ---------- */

/* 感叹号玩法提示：切片区域右上角，呼吸动效提醒（不在状态栏重复）。 */
.quiz-hint {
  position: absolute;
  top: 10px;
  right: 10px;
  z-index: 7;
  width: 34px;
  height: 34px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: var(--color-accent);
  color: #2f1a05;
  font-size: 17px;
  font-weight: 900;
  /* 【2026-09-15 评审补：它从 <div> 改成了 <button>】面板不再自动弹出之后，这颗
     感叹号比右缘那条竖把手显眼得多，玩家第一反应会去点它——点了没反应是纯粹的
     失望。改成 button 是为了**键盘可达**（div 加 onclick 对 Tab 与读屏都是死的），
     随之必须重置浏览器给 button 的默认边框/内衬/字体，否则它会变成一颗系统按钮。 */
  border: none;
  padding: 0;
  font-family: inherit;
  cursor: pointer;
  animation: quiz-hint-pulse 1.6s ease-in-out 8;
}
/* 伴生 [hidden]（理由见 .slice-empty 处的长注释）。缺了它，applyQuizVisibility()
   把 el.hidden 置 true 也没用：橙色感叹号从页面第一帧起就挂在切片右上角，回看
   历史切片时在、揭晓之后还在，「玩法要来了」这个信号彻底失效。 */
.quiz-hint[hidden] {
  display: none;
}
@keyframes quiz-hint-pulse {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.12); }
}

/* 半透明遮罩 + 居中覆盖面板。收起向右折叠、展开从右向左展开由动画表达；
   hidden 是开合的唯一权威（JS 切）。 */
.quiz-mask {
  position: absolute;
  inset: 0;
  z-index: 8;
  background: rgba(8, 11, 16, 0.55);
}
.quiz-panel {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 9;
  width: min(680px, 82%);
  max-height: 86%;
  overflow-y: auto;
  animation: quiz-unfold 0.22s ease-out;
}
@keyframes quiz-unfold {
  from { transform: translate(-38%, -50%) scaleX(0.9); opacity: 0.35; }
  to { transform: translate(-50%, -50%) scaleX(1); opacity: 1; }
}
.quiz-panel[hidden],
.quiz-mask[hidden] {
  display: none;
}

/* 「展开玩法」把手：面板收起后贴在切片右缘（收起向右折叠的落点）。 */
.quiz-handle {
  position: absolute;
  top: 50%;
  right: 0;
  transform: translateY(-50%);
  z-index: 7;
  padding: 14px 8px;
  border-radius: 8px 0 0 8px;
  background: var(--color-accent);
  border: none;
  color: #2f1a05;
  font-family: inherit;
  font-size: 13px;
  font-weight: 800;
  writing-mode: vertical-rl;
  letter-spacing: 0.2em;
  cursor: pointer;
}
.quiz-handle[hidden] {
  display: none;
}

/* 把手的召唤动效（2026-09-15）：玩法面板按需求方裁决不再自动弹出
   （「不要自动弹，影响阅读了，做一个提示 让用户知道这里要操作即可」），这颗把手
   成了**唯一**的进入方式。静静贴在右缘太容易被当成装饰，所以在「本回合玩法已就绪、
   玩家还没打开过」时呼吸提醒，打开过一次就摘掉这个类安静下来（JS 侧
   applyQuizVisibility 切）。

   动的是 box-shadow 不是 transform：.quiz-handle 用 transform: translateY(-50%)
   做垂直居中，在这里再写一个 transform 关键帧会把它整颗顶走（覆盖而不是叠加）。
   要改成缩放动效的话，得把居中换成别的方式，别只加关键帧。 */
.quiz-handle-attn {
  /* 【有限次而不是 infinite，2026-09-15 评审补】上面那句「提示的职责是把人领到
     门口，不是一直在耳边喊」原先配的是 infinite——玩家不点就永远闪，而这一整轮
     裁决的起因正是「影响阅读了」。8 次约 13 秒后自行安静：把手仍在、仍可点、
     文案仍是「展开玩法」，一个信息都不丢，只是不再动。 */
  animation: quiz-handle-attn-pulse 1.6s ease-in-out 8;
}
@keyframes quiz-handle-attn-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(233, 162, 92, 0.55); }
  50% { box-shadow: -2px 0 14px 4px rgba(233, 162, 92, 0.55); }
}
/* 尊重系统「减少动态效果」：前庭敏感的玩家把动画关掉后，把手仍要看得出是可点的，
   所以降级成一圈常亮描边而不是什么都不给。 */
@media (prefers-reduced-motion: reduce) {
  .quiz-handle-attn {
    animation: none;
    box-shadow: -2px 0 10px 2px rgba(233, 162, 92, 0.5);
  }
  /* ❗ 一起收掉（2026-09-15 评审补）：它走的是 transform: scale() 的缩放脉冲，对
     前庭敏感的影响比把手那圈 glow 更大，而本轮改动把它从「附属提示」提升成了核心
     信号。它本身就是橙底深墨的实心圆，不动也完全看得见，所以停掉不补偿任何东西。 */
  .quiz-hint {
    animation: none;
  }
}

/* 面板头右侧：倒计时药丸 / 「已揭晓」徽章 + 「收起」。 */
.quiz-head-right {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: none;
}
.quiz-revealed-badge {
  padding: 5px 14px;
  border-radius: 999px;
  background: var(--pc-ok);
  color: #ffffff;
  font-size: 13px;
  font-weight: 800;
}
.quiz-fold-btn {
  padding: 5px 12px;
  border-radius: 999px;
  background: transparent;
  border: 1px solid var(--pc-line);
  color: var(--pc-ink-dim);
  font-family: inherit;
  font-size: 12.5px;
  font-weight: 700;
  cursor: pointer;
}
.quiz-fold-btn:hover {
  color: var(--pc-ink);
  border-color: var(--pc-ink-dim);
}

/* 卡头第一行：切片编号 + 题型（PRD 附录 2 通用字段①②，原型 .kicker）。
   【2026-09-15 按新版 UI 原型拆两行；别当成回归改回去】此前卡头只有一行，
   「第 N 张切片 · 切片标题」挤在这里、题型是旁边另一个胶囊。原型是 kicker
   小字大字距 + 标题衬线大字两行；取舍（留真实信息、只换排版）写在 app.js
   buildQuizView 卡头那段注释里。
   letter-spacing 从 0.08em 提到 0.16em 对齐原型——这一行现在只剩短词，
   字距拉开才像「标签行」而不像被压扁的标题。 */
.quiz-kicker {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
  color: var(--pc-accent);
  font-size: 12.5px;
  font-weight: 800;
  letter-spacing: 0.16em;
}
.quiz-kicker-seq {
  flex: none;
}
/* 卡头第二行：切片标题（原型 .modal-title，衬线大字）。
   衬线是原型定的调性（"Songti SC","STSong",serif）。字体栈末尾必须留通用族
   serif：Songti SC 只有 macOS 有，Windows/Android 上前两个都取不到，不兜底就会
   掉回默认无衬线，这一行的调性就没了。 */
.quiz-modal-title {
  margin-top: 8px;
  margin-bottom: 6px;
  font-family: "Songti SC", "STSong", "SimSun", serif;
  font-size: 25px;
  font-weight: 700;
  line-height: 1.25;
  color: var(--pc-ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* 面板**底部**的选项倒计时一行：标签 + 进度条 + 剩余秒数（PRD「通过进度条
   倒计时的形式在底部展示」，原型 .count-row）。位置改动的理由写在 app.js
   appendQuizCountRow 的注释里。 */
.quiz-count-row {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-top: 20px;
}
.quiz-count-label {
  flex: none;
  font-size: 12.5px;
  font-weight: 700;
  color: var(--pc-ink-dim);
}
/* 倒计时进度条（quiz_open_ms → deadline_ms，两个服务端绝对时刻之差）。 */
.quiz-countdown-track {
  flex: 1 1 auto;
  height: 6px;
  margin: 0;
  border-radius: 999px;
  background: rgba(48, 38, 33, 0.18);
  overflow: hidden;
}
.quiz-countdown-bar {
  height: 100%;
  border-radius: 999px;
  background: var(--pc-accent);
  transition: width 0.9s linear;
}

/* 题目完整展示：超出一行自动换行不截断。 */
.quiz-question-text {
  margin-bottom: 12px;
  white-space: pre-wrap;
  word-break: break-word;
}
.quiz-multi-count {
  margin: -6px 0 12px;
  font-size: 12.5px;
  font-weight: 700;
  color: var(--pc-ink-dim);
}
.quiz-locked-note {
  margin: 8px 0 0;
  font-size: 12.5px;
  color: var(--pc-ink-dim);
}

/* 连续题：分段进度 + 第 N/共 M + 上一题/下一题/提交全部。 */
.quiz-seq-progress {
  display: flex;
  gap: 6px;
  margin-bottom: 8px;
}
.quiz-seq-seg {
  flex: 1 1 auto;
  height: 6px;
  border-radius: 999px;
  background: rgba(48, 38, 33, 0.18);
}
.quiz-seq-seg.done {
  background: var(--pc-ok);
}
.quiz-seq-seg.cur {
  outline: 2px solid var(--pc-accent);
  outline-offset: 1px;
}
.quiz-seq-label {
  margin-bottom: 10px;
  font-size: 12.5px;
  font-weight: 800;
  color: var(--pc-ink-dim);
}
.quiz-seq-nav {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 4px;
}
.quiz-seq-btn {
  padding: 8px 18px;
  border-radius: 999px;
  background: var(--pc-surface);
  border: 1px solid var(--pc-line);
  color: var(--pc-ink);
  font-family: inherit;
  font-size: 13px;
  font-weight: 700;
  cursor: pointer;
}
.quiz-seq-btn:disabled {
  opacity: 0.5;
  cursor: default;
}

/* 提交即公开区（vote_board）：按提交时序排列 + 「我」标识。 */
/* 「已投」昵称排在**每个选项自己**下面，用一条上边框和选项正文分开（原型
   .answer-voters）。
   【2026-09-15 按 PRD + 新版原型改位置；别当成回归改回去】此前是一整块独立的
   .quiz-voteboard（标题「已提交作答 · 提交即公开」+ 「A」「B」圆形选项字母 +
   多题时的「第 N 题」小标题），挂在选项列表之外。那套二次编号是用来把昵称指回
   某个选项的；昵称贴进选项本体之后，指向的东西就在正上方，整套编号连同标题一起
   没了。理由与 PRD 出处写在 app.js buildOptionVoters 的注释里。 */
.quiz-option-voters {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 10px;
  padding-top: 9px;
  border-top: 1px solid var(--pc-line);
}
.quiz-voter-label {
  flex: none;
  font-size: 11px;
  font-weight: 700;
  color: var(--pc-ink-dim);
}
/* 「提交即公开」的说明：只在**还没有人提交**时出现（有人提交之后，选项下面的
   「已投」自己就在讲这件事）。玩家得在按下提交之前就知道昵称会被公开——独立
   公开板时代这句话由它的空态承载，块没了就得有人接着说。 */
.quiz-publish-tip {
  margin-top: 12px;
  font-size: 12px;
  font-weight: 700;
  color: var(--pc-ink-dim);
}
.vb-name {
  padding: 2px 10px;
  border-radius: 999px;
  background: var(--pc-surface);
  border: 1px solid var(--pc-line);
  color: var(--pc-ink);
  font-size: 12px;
  font-weight: 700;
}
.vb-name.vb-me {
  border-color: var(--pc-accent);
  box-shadow: 0 0 0 1px var(--pc-accent);
}
.vb-me-badge {
  margin-left: 5px;
  padding: 0 6px;
  border-radius: 999px;
  background: var(--pc-accent);
  color: #fff;
  font-size: 10.5px;
  font-weight: 800;
}
.quiz-round-score {
  margin-top: 10px;
  font-size: 13px;
  font-weight: 700;
  color: var(--pc-ok-ink);
}

/* ---------- 对话历史浮层与输入（tasks 7.5） ---------- */

.chat-overlay {
  position: absolute;
  left: 12px;
  /* 让开辅助文字框：--slice-aux-h 由 renderSliceAux 每次渲染时写成它的实际
     高度（展开/收起/翻页都会变）。此前这里是写死的 bottom:12px，而辅助文字框
     贴着切片底边向上长——默认展开态下两者必然重叠，聊天消息与「问题前 · 剧情
     铺垫」标题栏互相压字，糊成一团。而「新切片默认展开」是每张切片的初始状态，
     等于每次切换切片都会看到花字。 */
  bottom: calc(var(--slice-aux-h, 0px) + 12px);
  z-index: 6;
  /* 【2026-09-15 需求方裁决：弹幕再宽一点】原话「这个弹幕稍微宽一点」。
     此前是 min(320px, 34%)，长一点的昵称加内容就换行。
     430px 是新版 UI 原型 .chat-history 的宽度；40% 这道闸让窄屏上它不至于吃掉
     半张画。 */
  width: min(420px, 40%);
  /* 【2026-09-15 修「弹幕与辅助文字互相压字」的第 2 个根因】
     此前这里是裸的 `max-height: 42%`。它和上面那条 bottom 各自成立、合起来算不过账：
     辅助文字框第 2 页是 64%（.aux-long），42% + 64% = 106% > 100%，于是**即便
     --slice-aux-h 是新鲜的**，弹幕也必然从切片舞台顶边溢出去——1920×1080 实测
     弹幕 top=34 而舞台 top=89，最旧的 55px 消息被 .slice-stage 的 overflow:hidden
     直接吃掉（不是压字，是没了，更难发现）。
     改成「先算还剩多少地方，再和 42% 取小」：100% 是舞台高（绝对定位的百分比
     max-height 按定位祖先的 padding box 解析），减掉辅助文字框、减掉与它之间那
     12px、再减掉顶部给序号角标留的净空。放不下时由 .chat-messages 的
     overflow-y:auto 滚动——最新消息在底部，被挤掉的是最旧的那几条，这正是聊天
     记录该有的取舍。
     z-index 的账也要记在这儿：弹幕 6 > 辅助文字框 5，所以两者一旦重叠就是**弹幕
     的字盖在辅助文字的字上**，糊成一团——这就是需求方截图里那个样子。 */
  --chat-top-reserve: 49px; /* 序号角标 top:10px + 高 27px + 12px 呼吸 */
  max-height: min(42%, calc(100% - var(--slice-aux-h, 0px) - 12px - var(--chat-top-reserve)));
  display: flex;
  flex-direction: column;
  /* 两条 transition 写成两行的话后一条会整个盖掉前一条（同属性简写覆盖），
     此前 bottom 那条就是这么被 opacity 那条吃掉的——合成一条声明。
     bottom 要跟着辅助文字框的高度变化平滑移动，否则翻页/收起时会跳一下。 */
  transition: bottom 0.2s ease, opacity 0.2s ease;
}
/* 玩法面板打开：对话历史降低透明度（滚动暂停在 renderChatPanel 里做）。 */
body.quiz-open .chat-overlay {
  opacity: 0.35;
}
.chat-overlay .chat-messages {
  overflow-y: auto;
  min-height: 0;
  padding: 8px 10px;
  background: rgba(10, 14, 20, 0.55);
  border-radius: var(--radius);
}
/* 新配色规则（同特异度靠后覆盖旧的 .chat-msg-player .chat-nick 规则）：
   观众灰白 / 玩家橙黄 / 本人青 + 「我的」。 */
.chat-msg .chat-nick-player {
  color: var(--color-accent);
}
.chat-msg .chat-nick-guest {
  color: var(--color-guest);
}
.chat-msg .chat-nick-me {
  color: var(--color-mine);
}
.chat-mine-badge {
  margin: 0 4px;
  padding: 0 6px;
  border-radius: 999px;
  background: var(--color-mine);
  color: #10241f;
  font-size: 10.5px;
  font-weight: 800;
}
.chat-msg .chat-nick {
  margin-right: 6px;
}

#slice-main .chat-input-area {
  flex: none;
  margin-top: var(--gap);
}
.chat-count {
  align-self: center;
  flex: none;
  font-size: 12px;
  font-weight: 700;
  color: var(--color-text-dim);
}
.chat-count.over {
  color: var(--color-danger);
}

/* ---------- 右侧剧情列表（tasks 7.7） ---------- */

.story-list .story-item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  margin-bottom: 8px;
  padding: 9px 12px;
  background: var(--color-panel-alt);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  color: var(--color-text);
  font-family: inherit;
  font-size: 13px;
  text-align: left;
  cursor: pointer;
}
.story-list .story-item:hover {
  border-color: var(--color-accent-dim);
}
/* 两套高亮（2026-09-15 需求方裁决「按原型改」）：
     .active  = 此刻中区展示的那张（回看时落在历史张上）——满描边 + 亮徽标；
     .current = 直播中的当前张——回看期间仍看得出「现在演到哪了」，用一道暗
                描边表示，且只在它不是 .active 时才画（否则被满描边盖住）。
   把这两个态合成一套的后果是：一进回看，列表上唯一的高亮就跑到历史张身上，
   直播进度在界面上彻底消失。 */
.story-list .story-item.active {
  border-color: var(--color-accent);
  box-shadow: 0 0 0 1px var(--color-accent);
}
.story-list .story-item.current:not(.active) {
  border-color: var(--color-accent-dim);
}
/* 状态字用与 .active 同一个亮色：--color-accent-dim(#785837) 当 11px 正文色
   在 --color-panel-alt 上对比度不够，两态的区别交给描边和文案自己说。 */
.story-list .story-item.current:not(.active) .story-item-hint {
  color: var(--color-accent);
}
/* 方块数字徽标 01/02/03（PRD 原型 .round-no）。 */
.story-item-seq {
  flex: none;
  width: 32px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 9px;
  background: var(--color-panel);
  border: 1px solid var(--color-border);
  color: var(--color-text-dim);
  font-size: 13px;
  font-weight: 800;
  letter-spacing: 0.02em;
}
.story-list .story-item.active .story-item-seq {
  background: var(--color-accent);
  border-color: var(--color-accent);
  color: #2f1a05; /* 同 .quiz-handle / .quiz-hint 的橙底深墨，AA 达标 */
}
/* 「已生成切片 共 N 张」：列表顶部的一行计数（PRD 原型 slice-8）。 */
.story-count {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  padding: 2px 4px 8px;
  font-size: 12px;
  color: var(--color-text-dim);
}
/* 伴生 [hidden]（理由见 .slice-empty 处的长注释）。一张切片都没有时
   renderStoryCount 置 hidden，缺这条就会空着一行占位。 */
.story-count[hidden] {
  display: none;
}
.story-count-num {
  color: var(--color-text);
}
/* 列表项正文改为两行：标题 + 「点击回看 / 当前展示」提示。 */
.story-item-body {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
  overflow: hidden;
}
.story-item-title {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.story-item-hint {
  font-size: 11px;
  color: var(--color-text-dim);
}
.story-item.active .story-item-hint {
  color: var(--color-accent);
}
.story-item-loading {
  cursor: default;
  color: var(--color-text-dim);
}
.story-item-loading .story-item-seq {
  color: var(--color-text-dim);
}
.story-item-loading .story-item-title::after {
  content: "";
  display: inline-block;
  width: 9px;
  height: 9px;
  margin-left: 8px;
  border-radius: 50%;
  border: 2px solid var(--color-text-dim);
  border-top-color: transparent;
  animation: slice-spin 0.9s linear infinite;
  vertical-align: -1px;
}

/* ---------- 结局模块（tasks 7.9）+ 终局结算页（2026-09-15 裁决） ---------- */

/* 结算页：finished 时替换切片舞台占住中区。宽度与舞台对齐（同一条
   16:9 max-width），换页时左右不跳。 */
.settle-page {
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: calc((100vh - var(--topbar-h) - 120px) * 16 / 9);
  max-height: calc(100vh - var(--topbar-h) - 120px);
  margin: 0 auto;
  padding: 16px 18px 18px;
  background: var(--color-panel);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
}
/* 【写了 display 就必须把 [hidden] 补回来】作者样式里的任何一条 display 都无条件
   盖过 UA 样式表的 [hidden]{display:none}（同 .stage-jump / .slice-aux 那几次
   线上教训）。结算页平时就是 hidden 的，漏了这条它会从第一帧起常驻。 */
.settle-page[hidden] {
  display: none;
}
.settle-page-bar {
  flex: none;
  display: flex;
  justify-content: flex-end;
  margin-bottom: 10px;
}
/* 结算页内容区独立滚动：结算表可能比中区高，页面本身不许跟着长。 */
.settle-page-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
}
.settle-back,
.settle-enter {
  padding: 5px 14px;
  border-radius: 999px;
  background: transparent;
  border: 1px solid var(--color-border);
  color: var(--color-text-dim);
  font-family: inherit;
  font-size: 12.5px;
  cursor: pointer;
}
.settle-back:hover,
.settle-enter:hover {
  color: var(--color-text);
  border-color: var(--color-accent-dim);
}

/* 结局切片下方的「回结算页」入口条。 */
.ending-area {
  margin: var(--gap) auto 0;
  width: 100%;
  max-width: calc((100vh - var(--topbar-h) - 120px) * 16 / 9);
  text-align: center;
}
