/* ==========================================================================
   认知抗体 · 设计系统
   风格：Neo-Brutalism（新粗野主义）
   规则：3px 黑边 · 7px 硬阴影 · 零圆角 · 高饱和撞色 · 无渐变
   ========================================================================== */

:root {
  /* ---- 色板（严格限制在 4 色 + 黑 + 纸底，撞色才有力） ---- */
  --paper: #FFFDF5;
  --ink: #111111;
  --blue: #4D6BFF;
  --yellow: #FFE44D;
  --pink: #FF5C8A;
  --green: #29C46F;
  --muted: #F1F1F1;
  --white: #FFFFFF;

  /* ---- 描边与阴影 ---- */
  --bw: 3px;
  --bd: 3px solid var(--ink);
  --bd-thin: 2px solid var(--ink);
  --sh: 7px 7px 0 var(--ink);
  --sh-sm: 4px 4px 0 var(--ink);
  --sh-lg: 11px 11px 0 var(--ink);

  /* 粘性导航的实测高度，由 common.js 的 syncNavH() 写入。
     这里给的是首屏（JS 尚未执行）时的兜底值。 */
  --nav-h: 70px;

  /* 「回到顶部」按钮为了避开底部吸底条要抬高多少，由 common.js 实测写入
     （只有 train.html 有吸底条，其余页面它是 0）。 */
  --fab-lift: 0px;

  /* ---- 间距节奏 ---- */
  --s1: 4px;
  --s2: 8px;
  --s3: 12px;
  --s4: 16px;
  --s5: 22px;
  --s6: 30px;
  --s7: 42px;
  --s8: 60px;

  /* ---- 字号 ---- */
  --fs-xs: 11px;
  --fs-sm: 12.5px;
  --fs-base: 14px;
  --fs-md: 16px;
  --fs-lg: 20px;
  --fs-xl: 28px;
  --fs-2xl: 40px;
  --fs-3xl: 58px;
  --fs-4xl: 78px;

  /* ---- 字体 ---- */
  --ff: "Arial Black", "Microsoft YaHei", "PingFang SC", -apple-system, sans-serif;
  --ff-mono: "Consolas", "Courier New", monospace;

  /* ---- 动效 ---- */
  --t: 0.12s;
}

* { margin: 0; padding: 0; box-sizing: border-box; }

/* 导航是 sticky（sticky 会盖住滚动目标的顶部），所以用 scroll-padding-top 抵消它的高度。
   --nav-h 由 JS 实测写入（导航高度随视口变化，写死会在某个分辨率上差几个像素）。 */
html { scroll-behavior: smooth; scroll-padding-top: calc(var(--nav-h, 70px) + 16px); }

body {
  background: var(--paper);
  color: var(--ink);
  font-family: var(--ff);
  font-size: var(--fs-base);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  min-height: 100vh;
  background-image:
    linear-gradient(rgba(17,17,17,.035) 1px, transparent 1px),
    linear-gradient(90deg, rgba(17,17,17,.035) 1px, transparent 1px);
  background-size: 26px 26px;
}

a { color: inherit; text-decoration: none; }
button, input, textarea, select { font-family: inherit; font-size: inherit; color: inherit; }

::-webkit-scrollbar { width: 12px; height: 12px; }
::-webkit-scrollbar-track { background: var(--paper); border-left: var(--bd-thin); }
::-webkit-scrollbar-thumb { background: var(--ink); border: 2px solid var(--paper); }

/* ==========================================================================
   布局
   ========================================================================== */
.wrap { max-width: 1280px; margin: 0 auto; padding: 0 var(--s5); }
.wrap-sm { max-width: 880px; margin: 0 auto; padding: 0 var(--s5); }

/* 容器宽度固定 1280 —— 不再随屏幕放大。
   上一版试过 ≥1400px 放到 1520 / ≥1700px 放到 1760，是为了让选择卡"排成一排"；
   但卡片被摊到 570px 宽、文字只有两行，反而空得难看。
   现在改成：卡片组用固定列数（.g3 / .g4）自然换行，宽度上限保持不变。
   .wrap-sm（长文阅读区）仍为 880 —— 那是为阅读舒适度刻意限的。
   ⚠️ .dock-inner 的 1280 必须与这里同步，否则吸底条与主内容左右错位。 */

.row { display: flex; align-items: center; gap: var(--s3); }
.row-between { display: flex; align-items: center; justify-content: space-between; gap: var(--s3); }
.col { display: flex; flex-direction: column; gap: var(--s3); }
.grow { flex: 1; min-width: 0; }
/* ⚠️ 这套工具类必须**逐档写全**。
   曾经只写到 .mb5 / .mt6，而页面里用了 .mb6 / .mt7 —— 类名不存在不会报错，
   只是那条规则静默失效：网格的 30px 下边距没了，下一节标题正好贴在卡片底边上。
   照片上看着像"重叠"，实测量出来是 gap=0px。补全后一次修掉 6 个页面。
   新增档位时，务必回头把这里补齐。 */
.mt1 { margin-top: var(--s1); } .mt2 { margin-top: var(--s2); } .mt3 { margin-top: var(--s3); }
.mt4 { margin-top: var(--s4); } .mt5 { margin-top: var(--s5); } .mt6 { margin-top: var(--s6); }
.mt7 { margin-top: var(--s7); } .mt8 { margin-top: var(--s8); }
.mb1 { margin-bottom: var(--s1); } .mb2 { margin-bottom: var(--s2); } .mb3 { margin-bottom: var(--s3); }
.mb4 { margin-bottom: var(--s4); } .mb5 { margin-bottom: var(--s5); } .mb6 { margin-bottom: var(--s6); }
.mb7 { margin-bottom: var(--s7); } .mb8 { margin-bottom: var(--s8); }
.center { text-align: center; }
.hidden { display: none !important; }

/* ==========================================================================
   导航
   ========================================================================== */
.nav {
  background: var(--yellow);
  border-bottom: var(--bd);
  position: sticky; top: 0; z-index: 100;
  /* 滚动方向驱动的收起/展开（逻辑在 common.js 的 initNavAutoHide）。
     ⚠️ 为什么 translateY 能真的"腾出空间"、而不只是把导航挪走留一条空白：
     它是 sticky，**流内位置在页面最顶端**；而能触发收起的前提是 scrollY 已经超过
     导航高度 —— 也就是说流内那份占位早已滚出视口上方。此刻把它移出视口，
     视口里不会剩下任何空白，下面的内容自然拿到这一整条 60～70px。
     （若在页面顶部就收起，是会把背景露出来的 —— 所以 JS 里加了 y > navH 的前置条件。） */
  transition: transform .26s cubic-bezier(.2, .9, .3, 1);
  will-change: transform;
}
/* ⚠️ 状态类挂在 <html> 上，不挂在 .nav 自己身上：
   common.js 的 renderNav() 会执行 `el.className = 'nav'`（骨架态画一次、真实态再画一次），
   把 nav 身上的类清掉 —— 于是"已收起"的导航会在数据回来那一刻突然弹出来。
   挂在根节点就与导航的重绘彻底解耦了。 */
html.nav-hidden .nav { transform: translateY(-100%); }
/* 同弹窗动画：这是交互反馈，reduce 下只缩短、不取消 —— 否则用户会觉得页面卡住了。
   ⚠️ 必须带 `!important`（见 .modal 那段的说明），否则会被
   `.reduce-motion * { transition-duration: .001ms !important }` 静默吃掉。 */
.reduce-motion .nav { transition-duration: .14s !important; }
.nav-inner {
  max-width: 1280px; margin: 0 auto; padding: var(--s3) var(--s5);
  display: flex; align-items: center; gap: var(--s5);
}
.nav-logo {
  display: flex; align-items: baseline; gap: var(--s2);
  padding: var(--s1) var(--s3);
  border: var(--bd-thin); background: var(--ink); color: var(--yellow);
  transform: rotate(-1.5deg);
  flex-shrink: 0;
}
.nav-logo b { font-size: var(--fs-md); letter-spacing: -.3px; }
.nav-logo span { font-size: 9px; letter-spacing: 1.4px; opacity: .75; }
.nav-links { display: flex; gap: var(--s1); flex-wrap: wrap; flex: 1; }
.nav-links a {
  padding: var(--s2) var(--s3);
  font-size: var(--fs-sm); font-weight: 900;
  border: var(--bd-thin); border-color: transparent;
  transition: var(--t);
  white-space: nowrap;
}
.nav-links a:hover { border-color: var(--ink); background: var(--white); }
.nav-links a.on { background: var(--ink); color: var(--yellow); border-color: var(--ink); }
.nav-user { display: flex; align-items: center; gap: var(--s2); flex-shrink: 0; }

/* 菜单键：与「登录 / 注册」等按钮保持同一套视觉（黑底黄字 + 硬阴影 + 位移动效），
   之前它是白底细边的独立样式，混在按钮里显得格格不入。 */
.nav-burger {
  display: none; align-items: center; gap: var(--s2);
  border: var(--bd); background: var(--ink); color: var(--yellow);
  padding: var(--s2) var(--s4);
  font-size: var(--fs-sm); font-weight: 900; letter-spacing: .8px;
  box-shadow: var(--sh-sm); cursor: pointer;
  transition: var(--t); white-space: nowrap; flex-shrink: 0;
}
.nav-burger:hover { transform: translate(-3px, -3px); box-shadow: var(--sh); }
.nav-burger:active { transform: translate(0, 0); box-shadow: 0 0 0 var(--ink); }
.nav-burger .nb-i { font-size: 13px; line-height: 1; }

/* --------------------------------------------------------------------------
   左侧抽屉菜单
   移动端不再用"下拉展开"（一展开导航就长高一截，页面跟着跳），
   改为从屏幕左缘滑出，背景加遮罩，向右滑或点遮罩都能收起。
   -------------------------------------------------------------------------- */
.nav-drawer {
  position: fixed; top: 0; bottom: 0; left: 0;
  width: min(80vw, 300px); z-index: 960;
  background: var(--paper); border-right: var(--bd);
  box-shadow: 6px 0 0 rgba(17, 17, 17, .07);
  transform: translateX(-104%);
  transition: transform .32s cubic-bezier(.22, .9, .28, 1);
  display: flex; flex-direction: column; overflow-y: auto;
  will-change: transform;
}
.nav-drawer.open { transform: none; }

.nav-drawer-mask {
  position: fixed; inset: 0; z-index: 950;
  background: rgba(17, 17, 17, .44);
  opacity: 0; pointer-events: none;
  transition: opacity .3s ease;
}
.nav-drawer-mask.on { opacity: 1; pointer-events: auto; }

.nd-hd {
  display: flex; align-items: center; justify-content: space-between;
  background: var(--ink); color: var(--yellow);
  padding: var(--s4); font-size: var(--fs-sm); font-weight: 900; letter-spacing: 1.4px;
}
.nd-close {
  border: var(--bd-thin); background: var(--yellow); color: var(--ink);
  /* 30px 见方太小，在右上角尤其难点中；抬到 44px 的触控下限 */
  width: 44px; height: 44px; font-weight: 900; cursor: pointer;
  display: grid; place-items: center; font-size: 16px;
}
.nd-links { display: flex; flex-direction: column; padding: var(--s4); gap: var(--s2); flex: 1; }
.nd-links a {
  border: var(--bd-thin); background: var(--white);
  padding: var(--s3) var(--s4); font-size: var(--fs-base); font-weight: 900;
  transition: var(--t);
}
.nd-links a:hover { background: var(--yellow); transform: translateX(3px); }
.nd-links a.on { background: var(--ink); color: var(--yellow); border-color: var(--ink); }
.nd-ft {
  padding: var(--s4); border-top: var(--bd-thin);
  font-size: var(--fs-xs); font-weight: 700; opacity: .5;
}

/* 抽屉打开时锁住背景滚动 */
body.drawer-open { overflow: hidden; }

@media (max-width: 900px) {
  /* 移动端：菜单键放到最左，页面导航收进左侧抽屉。
     不用"下拉展开"是因为一展开导航就长高一截，页面内容跟着往下跳。 */
  .nav-burger { display: inline-flex; }
  .nav-links { display: none; }
  .nav-inner { flex-wrap: nowrap; }
  .nav-user { margin-left: auto; }
  /* 手机顶栏不放 logo 标签（第 24 轮用户要求）：窄屏就 菜单键 + 用户区两件，
     站名在抽屉页脚里仍然可见。宽屏不受影响。 */
  .nav-logo { display: none; }
}

/* ==========================================================================
   卡片
   ========================================================================== */
.nb {
  border: var(--bd); background: var(--white); box-shadow: var(--sh);
}
.nb-flat { border: var(--bd); background: var(--white); }
.nb-hd {
  background: var(--ink); color: var(--white);
  padding: var(--s3) var(--s5);
  font-size: var(--fs-sm); font-weight: 900; letter-spacing: 1.4px;
  display: flex; justify-content: space-between; align-items: center; gap: var(--s3);
}
.nb-hd.yellow { background: var(--yellow); color: var(--ink); }
.nb-hd.blue { background: var(--blue); color: var(--white); }
.nb-hd.pink { background: var(--pink); color: var(--white); }
/* 卡片头右侧的附属小注（如「第 5 / 10 轮」）。
   用等宽字：轮次从 9→10 位数会变，比例字会让整个标题左右抖一下。
   字重比标题轻、字号更小，避免和标题抢视线。 */
.nb-hd-note {
  font-family: var(--ff-mono); font-size: var(--fs-xs); font-weight: 900;
  letter-spacing: 0; white-space: nowrap; opacity: .9; flex: 0 0 auto;
}
.nb-bd { padding: var(--s5); }
.nb-bd-sm { padding: var(--s4); }

/* 可点击卡片：悬停位移 */
.nb-link { transition: var(--t); cursor: pointer; display: block; }
.nb-link:hover { transform: translate(-4px, -4px); box-shadow: var(--sh-lg); }
.nb-link:active { transform: translate(0, 0); box-shadow: var(--sh-sm); }

/* ==========================================================================
   按钮
   ========================================================================== */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s2);
  border: var(--bd); background: var(--white);
  padding: var(--s3) var(--s5);
  font-size: var(--fs-sm); font-weight: 900; letter-spacing: .8px;
  box-shadow: var(--sh-sm); cursor: pointer;
  transition: var(--t); white-space: nowrap;
}
.btn:hover { transform: translate(-3px, -3px); box-shadow: var(--sh); }
.btn:active { transform: translate(0, 0); box-shadow: 0 0 0 var(--ink); }
/* 禁用态降到 .4 太淡：现在「开始接种」在等待时靠这颗按钮显示
   「AI 正在扮演对方…（6s）」，降到 .4 会看不清到底在说什么。
   改成 .62 —— 仍明显区别于可点态，但状态文案读得清。 */
.btn:disabled { opacity: .62; cursor: not-allowed; transform: none; box-shadow: var(--sh-sm); }
.btn-primary { background: var(--blue); color: var(--white); }
.btn-yellow { background: var(--yellow); }
.btn-pink { background: var(--pink); color: var(--white); }
.btn-green { background: var(--green); color: var(--white); }
.btn-ink { background: var(--ink); color: var(--yellow); }
.btn-lg { padding: var(--s4) var(--s6); font-size: var(--fs-md); }
.btn-sm { padding: var(--s2) var(--s3); font-size: var(--fs-xs); box-shadow: 3px 3px 0 var(--ink); }
.btn-block { width: 100%; }

/* ==========================================================================
   标签 / 徽章
   ========================================================================== */
.tag {
  display: inline-block; border: var(--bd-thin);
  padding: var(--s1) var(--s3);
  font-size: var(--fs-xs); font-weight: 900; letter-spacing: .8px;
  background: var(--white);
  /* ⚠️ 必须显式写 color。这里只给了白底却不给字色时，标签会**继承父级 color**：
     一旦它落在带 `color:#fff` 的深色卡片里（例如被选中的难度卡），
     就成了白底白字，标签内容直接看不见。 */
  color: var(--ink);
}
.tag-yellow { background: var(--yellow); }
.tag-blue { background: var(--blue); color: var(--white); }
.tag-pink { background: var(--pink); color: var(--white); }
.tag-green { background: var(--green); color: var(--white); }
.tag-ink { background: var(--ink); color: var(--yellow); }
.tag-muted { background: var(--muted); }

.stamp {
  border: var(--bd); background: var(--pink); color: var(--white);
  padding: var(--s2) var(--s4); font-size: var(--fs-sm); font-weight: 900;
  transform: rotate(-2deg); display: inline-block;
}
.stamp-blue { background: var(--blue); }
.stamp-yellow { background: var(--yellow); color: var(--ink); }
.stamp-green { background: var(--green); }

/* ==========================================================================
   进度条
   ========================================================================== */
.track { border: var(--bd-thin); height: 16px; background: var(--white); overflow: hidden; }
.track > i { display: block; height: 100%; background: var(--blue); transition: width .5s cubic-bezier(.2,.9,.3,1); }
.track.thick { height: 26px; }
.track.thin { height: 10px; }

/* ==========================================================================
   表单
   ========================================================================== */
.field { margin-bottom: var(--s4); }
.field label {
  display: block; font-size: var(--fs-xs); font-weight: 900;
  letter-spacing: 1.2px; margin-bottom: var(--s2);
}
.input, .textarea, .select {
  width: 100%; border: var(--bd); background: var(--white);
  padding: var(--s3) var(--s4); font-size: var(--fs-base); font-weight: 700;
  box-shadow: inset 3px 3px 0 rgba(17,17,17,.06);
  outline: none; transition: var(--t);
}
.input:focus, .textarea:focus, .select:focus { background: var(--yellow); }
.textarea { resize: vertical; min-height: 88px; line-height: 1.6; font-weight: 600; }
.checkline { display: flex; align-items: center; gap: var(--s2); font-size: var(--fs-sm); font-weight: 700; cursor: pointer; }
.checkline input { width: 18px; height: 18px; accent-color: var(--blue); }

/* ==========================================================================
   消息气泡（演练用）
   ========================================================================== */
.msg { margin-bottom: var(--s5); animation: pop .22s ease-out; }
.msg .who {
  font-size: var(--fs-xs); font-weight: 900; letter-spacing: 1px;
  margin-bottom: var(--s2); display: flex; align-items: center; gap: var(--s2);
}
.bub {
  border: var(--bd); background: var(--muted);
  padding: var(--s4) var(--s5);
  font-size: var(--fs-base); font-weight: 600; line-height: 1.75;
  /* 百分比上限只在窄屏有意义：聊天列在 1280 容器下约 880px，
     88% 就是 774px —— 一句行骗话会摊成 80 多字一行。
     加一个绝对上限，把每行压回 45 字左右的可读区间；min() 保证窄屏仍按 88% 走。 */
  max-width: min(88%, 760px);
}
.bub.me { background: var(--blue); color: var(--white); margin-left: auto; }
.bub.scam { background: var(--white); }
.bub.coach { background: var(--yellow); }

@keyframes pop {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: translateY(0); }
}

.typing { display: inline-flex; gap: 5px; padding: var(--s3) var(--s4); border: var(--bd); background: var(--white); }
.typing i { width: 8px; height: 8px; background: var(--ink); display: block; animation: blink 1.1s infinite; }
.typing i:nth-child(2) { animation-delay: .18s; }
.typing i:nth-child(3) { animation-delay: .36s; }
@keyframes blink { 0%,60%,100% { opacity: .22; } 30% { opacity: 1; } }

/* 手法提示条 */
.tacticbar {
  display: flex; align-items: center; gap: var(--s2);
  border: var(--bd); background: var(--yellow);
  padding: var(--s2) var(--s4); margin: var(--s2) 0 var(--s5);
  font-size: var(--fs-sm); font-weight: 900;
  animation: pop .22s ease-out;
}
.tacticbar.pink { background: var(--pink); color: var(--white); }
.tacticbar.green { background: var(--green); color: var(--white); }

/* 选项 */
.opt {
  border: var(--bd); background: var(--white);
  padding: var(--s4) var(--s5); margin-bottom: var(--s3);
  font-size: var(--fs-base); font-weight: 700;
  cursor: pointer; display: flex; gap: var(--s4);
  transition: var(--t); animation: pop .22s ease-out;
}
.opt:hover { background: var(--blue); color: var(--white); transform: translate(-3px,-3px); box-shadow: var(--sh-sm); }
.opt i { font-style: normal; font-weight: 900; }
.opt.sel { background: var(--blue); color: var(--white); }
.opt.right { background: var(--green); color: var(--white); }
.opt.wrong { background: var(--pink); color: var(--white); }
.opt.dim { opacity: .55; }

/* ==========================================================================
   表格 / 榜单
   ========================================================================== */
.nb-table { width: 100%; border-collapse: collapse; }
.nb-table th {
  background: var(--ink); color: var(--white);
  font-size: var(--fs-xs); letter-spacing: 1.2px;
  padding: var(--s3) var(--s4); text-align: left; font-weight: 900;
}
.nb-table td { padding: var(--s3) var(--s4); border-bottom: var(--bd-thin); font-weight: 700; font-size: var(--fs-sm); }
.nb-table tr:last-child td { border-bottom: none; }
.nb-table tr:nth-child(even) td { background: rgba(17,17,17,.028); }
.nb-table tr.me td { background: var(--yellow); }
.nb-table tr:hover td { background: rgba(77,107,255,.09); }
.rank-no {
  display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; border: var(--bd-thin); font-weight: 900; font-size: var(--fs-sm);
  background: var(--white);
}
.rank-no.r1 { background: var(--yellow); }
.rank-no.r2 { background: #D8DCE6; }
.rank-no.r3 { background: #E8C9A0; }

/* ==========================================================================
   指标条（画像）
   ========================================================================== */
.metric { margin-bottom: var(--s4); }
.metric .r {
  display: flex; justify-content: space-between; align-items: baseline;
  font-size: var(--fs-sm); font-weight: 900; margin-bottom: var(--s2);
}
.metric .r b { font-size: var(--fs-md); }
.metric .r small { font-weight: 700; font-size: var(--fs-xs); opacity: .7; letter-spacing: .6px; }
/* 维度色块：颜色交给这个小方块（和下方的进度条），数字一律用墨黑。
   原先数字直接上维度色，实测「权威服从抵御」那抹黄 (#FFE44D) 压在白底上
   对比度约 1.3:1 —— 数值得几乎看不见。颜色只是编码，不该牺牲可读性。 */
.dimdot {
  display: inline-block; width: 8px; height: 8px;
  border: 2px solid var(--ink); margin-right: 6px;
  vertical-align: -1px; background: var(--yellow);
}

/* 大分数 */
.bigscore {
  border: var(--bd); background: var(--blue); color: var(--white);
  padding: var(--s5); text-align: center;
}
.bigscore b { display: block; font-size: var(--fs-4xl); line-height: .92; letter-spacing: -3px; }
.bigscore span { font-size: var(--fs-xs); font-weight: 900; letter-spacing: 1.6px; }

/* 警示块 */
.alert {
  border: var(--bd); background: var(--pink); color: var(--white);
  padding: var(--s4) var(--s5); font-size: var(--fs-sm); font-weight: 700; line-height: 1.7;
}
.alert.yellow { background: var(--yellow); color: var(--ink); }
.alert.blue { background: var(--blue); }
.alert.green { background: var(--green); }
.alert.muted { background: var(--muted); color: var(--ink); }

/* ==========================================================================
   首屏
   ========================================================================== */
.hero { padding: var(--s8) 0 var(--s7); }
.hero h1 {
  font-size: var(--fs-3xl); line-height: 1.02; letter-spacing: -2px; margin-bottom: var(--s4);
}
.hero h1 em { font-style: normal; background: var(--yellow); padding: 0 var(--s2); border: var(--bd); display: inline-block; transform: rotate(-1deg); }
.hero p.lead { font-size: var(--fs-md); font-weight: 700; max-width: 720px; line-height: 1.75; }

.kicker {
  display: inline-block; border: var(--bd); background: var(--ink); color: var(--yellow);
  padding: var(--s1) var(--s3); font-size: var(--fs-xs); letter-spacing: 2px; font-weight: 900;
  margin-bottom: var(--s4);
}

/* 网格
   ⚠️ 这里必须写 minmax(0,1fr) 而不是 1fr。
   1fr 的隐含最小值是 auto（等于内容的 min-content 宽度），
   一旦某列里放了固有宽度很大的东西（例如 viewBox 980 宽的 SVG 架构图），
   该列就会被顶宽、撑破视口 —— 手机上表现为整页被缩小或横向滚动。
   minmax(0,1fr) 把最小值显式压到 0，列宽才真正由容器决定。
   下面的 min-width:0 是同一件事在子项上的兜底（flex/grid 子项默认 min-width:auto）。 */
.grid { display: grid; gap: var(--s5); }
.grid > * { min-width: 0; }
.g2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.g3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.g4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.g-main { grid-template-columns: minmax(0, 1fr) 360px; }
.g-side { grid-template-columns: 300px minmax(0, 1fr); }

/* 正文里的图：一律按容器宽度自适应，不靠固有像素宽度 */
.prose svg, .nb-bd svg, .nb-bd-sm svg { display: block; max-width: 100%; height: auto; }

/* ==========================================================================
   选择器：分节标题 / 选中态 / 步骤指示
   ========================================================================== */

/* 分节标题。上间距交给上一块元素的下边距（.mb6 等），这里只管与下方内容的关系。
   ⚠️ 不要在标题上再写 scroll-margin-top —— 它和 html 的 scroll-padding-top 是**相加**的，
      实测会把目标多推 83px（两者都是 83px，叠成 166px）。全局那一处就够了。 */
.sec-h {
  font-size: var(--fs-md); letter-spacing: -.3px;
  margin-bottom: var(--s3);
}
.sec-sub {
  font-size: var(--fs-xs); font-weight: 700; opacity: .75;
  line-height: 1.75; margin-bottom: var(--s3); max-width: 780px;
}

/* 选中态一律用 class，不写内联 style ——
   内联样式会压过媒体查询，响应式规则就再也改不动它了。 */
.nb-link.sel { background: var(--yellow); }
.nb-link.sel-strong { background: var(--blue); color: var(--white); }
.nb-link.sel-strong .tag { background: var(--yellow); color: var(--ink); }

/* 「随机接种」卡（.nb-rand）：场景列表里的第一项。
   ⚠️ **这里故意不给它写任何配色。** 它的表头颜色跟随当前分组
   （train.html 的 renderScenarioGrid 里给表头挂 `pink`，非常规组才挂）。

   为什么：本网格里颜色是带语义的 —— 黑 = 常规场景，粉 = 非常规场景。
   上一版给它单独刷了粉色，看着像"突出推荐"，实际是个信息冲突：
   在「常规」tab 下它成了满屏唯一一张粉表头，被读成"这又是一张非常规场景卡"。
   而只在常规组去掉粉、非常规组保留，又会反过来 —— 非常规 tab 下它是黑底，
   一样是满屏唯一一个。所以只能跟随分组。
   它的身份靠卡头的「推荐」徽章 + 「随机接种」标题表达，不占用颜色语义。
   （.nb-rand 这个类留作语义钩子，探针 tools/grid-shot.js 靠它认出这张卡。） */

/* 三步流程胶囊条（.picksteps / .pstep / .parrow）已在第 53 轮删除。
   原因：那三颗胶囊长得像按钮、点下去却没有反应 —— 用户把整页读成
   "点这三下就完事了"，完全不知道下面还有三组卡片要选（实测反馈）。
   现在序号与问题交给每一节自己（见 train.html 的 .sec-hd / .sec-no / .sec-hint）。 */

@media (max-width: 1080px) {
  .g-main, .g-side { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 900px) {
  .g3, .g4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  :root { --fs-3xl: 40px; --fs-2xl: 30px; --fs-4xl: 54px; }
}
@media (max-width: 620px) {
  .g2, .g3, .g4 { grid-template-columns: minmax(0, 1fr); }
  .wrap, .wrap-sm { padding: 0 var(--s4); }
  :root { --fs-3xl: 32px; --fs-2xl: 26px; --fs-xl: 22px; --sh: 5px 5px 0 var(--ink); }
}

/* ==========================================================================
   数字与等宽
   ========================================================================== */
.num { font-family: var(--ff-mono); font-weight: 700; letter-spacing: -.5px; }
.mono { font-family: var(--ff-mono); }

/* 分隔线 */
.hr { border: none; border-top: var(--bd); margin: var(--s6) 0; }

/* 步骤条 */
.steps { display: flex; flex-wrap: wrap; gap: var(--s2); }
.steps .st {
  display: flex; align-items: center; gap: var(--s2);
  border: var(--bd-thin); background: var(--white);
  padding: var(--s2) var(--s3); font-size: var(--fs-xs); font-weight: 900;
}
.steps .st b { background: var(--ink); color: var(--yellow); width: 18px; height: 18px; display: grid; place-items: center; font-size: 10px; }
.steps .st.on { background: var(--yellow); }

/* 页脚 */
.footer {
  border-top: var(--bd); margin-top: var(--s8);
  padding: var(--s6) 0; background: var(--ink); color: var(--white);
}
.footer .wrap { display: flex; justify-content: space-between; gap: var(--s5); flex-wrap: wrap; }
.footer small { font-size: var(--fs-xs); opacity: .65; letter-spacing: .6px; font-weight: 700; }
.footer a:hover { color: var(--yellow); }

/* 加载遮罩 */
.loading {
  position: fixed; inset: 0; background: rgba(255,253,245,.94);
  display: grid; place-items: center; z-index: 900;
}
.loading .box { border: var(--bd); background: var(--white); box-shadow: var(--sh-lg); padding: var(--s6) var(--s7); text-align: center; }
.loading .box b { display: block; font-size: var(--fs-lg); margin-bottom: var(--s2); }
.loading .box small { font-size: var(--fs-sm); font-weight: 700; opacity: .7; }

/* 圆形分数 */
.donut { display: block; }
.donut text { font-family: var(--ff); font-weight: 900; }

/* ==========================================================================
   加载态：顶部扫光进度条 + 预填充骨架屏
   为什么要做：boot() 原来串行等待 /api/meta 才渲染导航与页面主体，
   网络一慢，上下两条栏位和整个页面就是一片空白 —— 用户不知道是在加载还是坏了。
   现在：结构立刻出现（骨架占位），数据到达后原地替换。
   ========================================================================== */

/* 顶部扫光：任何 API 请求在飞就会亮起（由 api() 统一计数控制） */
.loadbar {
  position: fixed; top: 0; left: 0; right: 0; height: 3px;
  z-index: 9999; pointer-events: none;
  opacity: 0; transition: opacity .22s ease;
  background: rgba(17, 17, 17, .07);
}
.loadbar.on { opacity: 1; }
.loadbar > i {
  display: block; height: 100%; width: 60%;
  background: linear-gradient(90deg, transparent, var(--blue) 22%, var(--pink) 52%, var(--yellow) 78%, transparent);
  transform: translateX(-110%);
  animation: sweep 1.15s cubic-bezier(.45, .05, .35, 1) infinite;
}
@keyframes sweep {
  0%   { transform: translateX(-110%); }
  100% { transform: translateX(210%); }
}

/* 骨架块：一条从右向左流动的高光，看着就是"正在来" */
.sk {
  display: block; border-radius: 3px;
  background: rgba(17, 17, 17, .07);
  background-image: linear-gradient(90deg, rgba(17,17,17,0) 0%, rgba(17,17,17,.13) 50%, rgba(17,17,17,0) 100%);
  background-size: 42% 100%; background-repeat: no-repeat;
  background-position: -42% 0;
  animation: shimmer 1.35s cubic-bezier(.4, 0, .2, 1) infinite;
}
@keyframes shimmer {
  0%   { background-position: -42% 0; }
  100% { background-position: 142% 0; }
}
.sk-line { height: 12px; margin-bottom: 8px; }
.sk-line.w40 { width: 40%; } .sk-line.w60 { width: 60%; }
.sk-line.w80 { width: 80%; } .sk-line.w100 { width: 100%; }
.sk-pill { height: 24px; width: 84px; display: inline-block; }
.sk-title { height: 16px; width: 50%; margin-bottom: 10px; }

/* 骨架卡片：占住真实卡片的位置与高度，避免数据到达时页面跳动 */
.sk-card {
  border: var(--bd); background: var(--white);
  padding: var(--s4) var(--s5); min-height: 92px;
  box-shadow: var(--sh-sm);
}
.sk-card .sk { height: 13px; }
.sk-card .sk + .sk { margin-top: 9px; }

/* 图片：未加载出来时先占位，加载完淡入 */
img { background: rgba(17, 17, 17, .06); }
img.img-in { animation: imgIn .38s ease both; }
@keyframes imgIn { from { opacity: 0; } to { opacity: 1; } }

/* ==========================================================================
   交互动效
   只用 transform / opacity（走合成层，不触发重排），手机上才不卡。
   ========================================================================== */

/* 页面进入：整体轻微上浮淡入 */
body.page-enter > main,
body.page-enter > .hero { animation: pageIn .34s cubic-bezier(.22, .9, .28, 1) both; }
@keyframes pageIn {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}

/* 列表项依次入场（JS 加 .stagger，用 --i 控制延迟） */
.stagger { opacity: 0; }
.stagger.in { animation: itemIn .36s cubic-bezier(.22, .9, .28, 1) both; animation-delay: calc(var(--i, 0) * 42ms); }
@keyframes itemIn {
  from { opacity: 0; transform: translateY(12px); }
  to   { opacity: 1; transform: none; }
}

/* 滚动进入视口时淡入（JS 加 .reveal-on-scroll + .in） */
.reveal-on-scroll { opacity: 0; transform: translateY(14px); }
.reveal-on-scroll.in {
  opacity: 1; transform: none;
  transition: opacity .42s cubic-bezier(.22,.9,.28,1), transform .42s cubic-bezier(.22,.9,.28,1);
}

/* 按下反馈：所有可点元素都有即时回弹，手感才有"回应" */
.btn, .nb-link, .opt, .qopt, .tag, .st, .nav-links a {
  -webkit-tap-highlight-color: transparent;
}
.press { animation: pressPop .17s ease; }
@keyframes pressPop {
  0%   { transform: translate(0, 0); }
  55%  { transform: translate(1px, 1px); }
  100% { transform: translate(0, 0); }
}

/* 数字滚动（分数变化时） */
.num-tick { animation: numTick .3s ease; }
@keyframes numTick { 0% { transform: translateY(4px); opacity: .4; } 100% { transform: none; opacity: 1; } }

/* 统计数字滚动：等宽数字。
   位数在滚动过程中会变（9→10、99→100），没有这一条，
   右对齐的统计块会随着数字宽度变化左右抖 —— 抖动比不做动画更难受。 */
.count-num { font-variant-numeric: tabular-nums; font-feature-settings: "tnum" 1; }

/* 环形图弧长生长。
   终值本来就写在 stroke-dashoffset 属性里（JS 挂了也是正确图形），
   JS 负责"打回起点、下一帧放回"，补间就靠这条 transition。 */
.dn-arc { transition: stroke-dashoffset 1.05s cubic-bezier(.22, .9, .28, 1); }

/* 雷达图：多边形以自身中心放大淡入。
   ⚠️ transform-box: fill-box 是关键 —— SVG 元素的 transform-origin 默认落在
   用户坐标系的 (0,0)（也就是画布左上角），不改成 fill-box 的话，
   图形会从左上角"飞"进来，而不是从中心长开。 */
.rd-poly, .rd-dot { transform-box: fill-box; transform-origin: center; }
.rd-poly { animation: rdIn .72s cubic-bezier(.22, .9, .28, 1) both; }
@keyframes rdIn {
  from { opacity: 0; transform: scale(.66); }
  to   { opacity: 1; transform: none; }
}
/* 网格与轴线只淡入 —— 跟着一起放大会让人晕 */
.rd-grid { animation: rdFade .5s ease both; }
@keyframes rdFade { from { opacity: 0; } to { opacity: 1; } }
/* 顶点：等图形站定后错峰蹦出来 */
.rd-dot { animation: rdDot .34s cubic-bezier(.22, .9, .28, 1) both; animation-delay: calc(var(--i, 0) * 90ms + 300ms); }
@keyframes rdDot {
  from { opacity: 0; transform: scale(.35); }
  to   { opacity: 1; transform: none; }
}

/* 折线图描线：dasharray 取一个远大于路径长度的值，offset 从该值回到 0
   就是"把线画出来"。取 4000 是因为本项目的图最多几百像素长，
   够用、且不必去量真实路径长度（getTotalLength 要在 DOM 里才拿得到）。 */
.ln-path { stroke-dasharray: 4000; stroke-dashoffset: 4000; animation: lnDraw 1.15s cubic-bezier(.3, .8, .3, 1) both; }
@keyframes lnDraw { to { stroke-dashoffset: 0; } }
.ln-dot { animation: rdFade .3s ease both; animation-delay: calc(var(--i, 0) * 70ms + 700ms); }

/* 减少动态效果。
   ⚠️ 这里刻意**不写成 @media (prefers-reduced-motion: reduce)**，而是挂在
   <html> 的 .reduce-motion 类上。类由 common.js 依据同一个媒体查询挂上去，
   默认行为与直接写 @media 完全一致；差别只在多出一个开关 ——
   演示/答辩时如果演示机把系统动画关了（Windows 默认可能就被"最佳性能"优化关掉，
   本项目的开发机就是这种情况），把 common.js 里的 FORCE_MOTION 改成 true
   就全部回来了，不必去改整台机器的系统设置。
   直接写 @media 的话，一点办法都没有。 */
.reduce-motion *,
.reduce-motion *::before,
.reduce-motion *::after {
  animation-duration: .001ms !important;
  animation-delay: 0s !important;          /* 延迟也要归零，否则只是"一次蹦出来" */
  animation-iteration-count: 1 !important;
  transition-duration: .001ms !important;
}
.reduce-motion .stagger,
.reduce-motion .reveal-on-scroll { opacity: 1 !important; transform: none !important; }

/* ==========================================================================
   移动端适配
   原则：手机版不是"缩小版桌面"，而是
     ① 双栏拆成单栏（内容顺序 = 阅读顺序）
     ② 固定元素不挡内容、并给 iPhone 底部横条留出安全区
     ③ 点击目标放大、正文降到 14px 以下会难读的地方不再压字号
     ④ 手机上没有 hover，去掉悬停位移，避免误触后的"抖动"
   ========================================================================== */
@media (max-width: 900px) {
  .grid { gap: var(--s4); }
  .g-main, .g-side { grid-template-columns: minmax(0, 1fr); }
  .nb-hd {
    padding: var(--s3) var(--s4);
    font-size: var(--fs-xs); letter-spacing: 1px;
    flex-wrap: wrap; row-gap: var(--s2);
  }
  .nb-bd, .nb-bd-sm { padding: var(--s4); }
  .hero { padding: var(--s6) 0 var(--s5); }
  .hr { margin: var(--s5) 0; }
}

@media (max-width: 620px) {
  :root {
    --bw: 2px;
    --bd: 2px solid var(--ink);
    --bd-thin: 1.5px solid var(--ink);
    --sh: 4px 4px 0 var(--ink);
    --sh-sm: 3px 3px 0 var(--ink);
    --sh-lg: 6px 6px 0 var(--ink);
  }
  body { background-size: 22px 22px; }

  /* ---- 导航：手机上一行放下 ----
     窄屏换行的主因是 logo 里那行 9px 英文副标题（约 116px 宽），
     加上「登录/注册」和「菜单」后总宽超过 390px 视口，菜单键必被挤到第二行。
     这里：隐藏副标题 + 去掉 logo 倾斜 + 压紧按钮内边距，三者合起来省出约 130px。

     触控目标：iOS HIG 建议的最小可点高度是 44pt。菜单键 / logo / 登录注册
     三件套原本都是 36px，尺寸一致但偏矮。这里**三个一起**抬到 44px ——
     必须同改，否则刚统一的视觉又会被尺寸差拉回参差。
     实测导航总高仍在 66px 以内（断言阈值），不会把首屏吃掉。 */
  .nav-inner { padding: var(--s2) var(--s4); gap: var(--s2); }
  .nav-burger { padding: var(--s2) var(--s3); font-size: var(--fs-xs); min-height: 44px; box-shadow: 2px 2px 0 var(--ink); }
  /* logo 原本按基线对齐；单行文字装在 44px 高的盒子里必须改成居中，否则字会浮在顶部 */
  .nav-logo { padding: var(--s1) var(--s2); transform: none; min-height: 44px; align-items: center; }
  .nav-logo b { font-size: var(--fs-sm); letter-spacing: 0; }
  .nav-logo span { display: none; }
  .nav-user { margin-left: auto; min-width: 0; }
  .nav-user .btn { padding: var(--s2) var(--s3); font-size: var(--fs-xs); white-space: nowrap; min-height: 44px; }
  /* 右上角「昵称 · Lv.x」那块黑标。
     ⚠️ 原来是 `max-width: 88px` + overflow:hidden + text-overflow:ellipsis，
     但**省略号从来没生效过** —— a.tag 是 inline-flex 容器（见上面 a.tag 那条），
     而 text-overflow 只对"块容器里的行内内容"起作用：这里的文字成了匿名 flex 项，
     既不缩也不出省略号，直接顶到边框上被 overflow 裁掉半截。
     症状（用户原话）："你看右上角这个字看着不像在居中" ——
     字的左留白 18px、右留白 0，看着当然不居中。修法是**把文字包进 .nu-txt**，
     由它来承担省略号；上限也放宽，常见昵称能完整显示。 */
  .nav-user .tag { max-width: min(140px, 40vw); }
  .nav-user .tag .nu-txt { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }

  /* ---- 卡片：去掉 hover 位移，保留按下反馈 ---- */
  .nb-link:hover { transform: none; box-shadow: var(--sh); }
  .nb-link:active { transform: translate(2px, 2px); box-shadow: var(--sh-sm); }
  .stamp { font-size: var(--fs-xs); padding: var(--s2) var(--s3); }
  /* 10px 在部分安卓机上会糊成一团；11px（--fs-xs）是全站字号下限。 */
  .kicker { font-size: 11px; letter-spacing: 1.2px; }
  .hero h1 { letter-spacing: -1px; }
  .hero p.lead { font-size: var(--fs-base); }

  /* ---- 表单：iOS 在聚焦小于 16px 的输入框时会自动放大整页 ---- */
  .input, .textarea, .select { font-size: 16px; }
  .btn { padding: var(--s3) var(--s4); }
  .btn-lg { width: 100%; padding: var(--s4) var(--s5); font-size: var(--fs-base); }
  /* 小按钮（如训练页的「常规场景 / 非常规场景」切换）原本 43px，差 1px 不达标 */
  .btn-sm { min-height: 44px; display: inline-flex; align-items: center; }
  .checkline { font-size: var(--fs-base); }

  /* 可点击的标签（首页的「查看排行榜 →」「去试一场 →」）原本只有 28px 高。
     它们长得像徽章、实际是 CTA，撑到 44px 反而更符合它的功能定位。
     注意只选 `a.tag` —— 纯展示用的徽章不该被撑大。 */
  a.tag { min-height: 44px; display: inline-flex; align-items: center; padding-inline: var(--s4); }

  /* ---- 正文里的行内链接：命中区撑到 44px，视觉位置不变 ----
     实测提示块里的「登录 / 注册」只有 54×15，拇指基本点不中。
     行内元素不能靠 padding 撑高（会顶开行距），所以用等量负 margin 抵消，
     只有命中区变大，排版一个像素都不动。 */
  .alert a, .prose a, .dock-meta a, .checkline a {
    display: inline-block;
    padding: 15px var(--s2);
    margin: -15px calc(var(--s2) * -1);
  }

  /* ---- 大分数别撑破卡片 ---- */
  .bigscore { padding: var(--s4); }
  .bigscore b { font-size: var(--fs-3xl); letter-spacing: -2px; }

  /* ---- 表格：允许横向滚动，不要把页面撑宽 ---- */
  .nb-table { display: block; overflow-x: auto; white-space: nowrap; -webkit-overflow-scrolling: touch; }
  .nb-table th, .nb-table td { padding: var(--s2) var(--s3); font-size: var(--fs-xs); }

  /* ---- 对话气泡与提示块 ---- */
  .bub { max-width: 100%; padding: var(--s3) var(--s4); }
  .alert { padding: var(--s3) var(--s4); font-size: var(--fs-sm); }
  .tacticbar { flex-wrap: wrap; row-gap: var(--s1); padding: var(--s2) var(--s3); font-size: var(--fs-xs); }

  /* ---- 页脚改为上下排列，避免右侧文字被挤成一条 ---- */
  .footer .wrap { flex-direction: column; }
  .footer .wrap > div:last-child { text-align: left !important; }
  .loading .box { padding: var(--s5); }
  .steps .st { font-size: 10px; }
}

/* ==========================================================================
   回到顶部悬浮按钮（.fab）
   由 common.js 的 initBackTop() 统一注入，9 个页面共用一份 ——
   不往每个 html 里各写一遍，就少一处"某个页面忘了加"的机会。
   外观沿用 .btn-ink：黑底黄箭头，和蓝色主按钮（开始接种）不会混淆。

   ---- 三条约束，改的时候别破坏 ----
   ① **只在滚下去之后出现**（scrollY > 400）。首屏就挂一个悬浮球会把
      首页那排统计数字的视线切掉一半。
   ② **必须给底部吸底条让位**。train.html 的 .dock 是 `position: sticky; bottom: 0`，
      按钮压在它上面正好遮住「开始接种」。让位距离由 common.js 实测写进
      `--fab-lift`（dock 在窄屏会折成两行，高度写死一定会错位）。
   ③ **平滑滚动要尊重 reduce-motion**。本机系统动画是关的，直接写 smooth
      会表现为"咻地滑过去"；reduce 分支下改成立即跳转（见 initBackTop）。
   ========================================================================== */
.fab {
  position: fixed;
  right: var(--s5);
  bottom: calc(var(--s5) + var(--fab-lift, 0px) + min(env(safe-area-inset-bottom, 0px), 20px));
  z-index: 195;                    /* 低于 .dock(200)，高于 .nav(100) 与正文 */
  width: 52px; height: 52px;
  display: grid; place-items: center;
  border: var(--bd); background: var(--ink); color: var(--yellow);
  box-shadow: var(--sh-sm); cursor: pointer;
  /* 藏起来用 opacity + visibility，不用 display —— display 不能过渡，
     会出现"啪"地蹦出来。transform 那 10px 是位移类动效，
     reduce-motion 下由过渡时长归零自动抹平。 */
  opacity: 0; visibility: hidden; transform: translateY(10px);
  transition: opacity var(--t), transform var(--t), visibility var(--t), bottom .2s;
}
.fab.on { opacity: 1; visibility: visible; transform: translateY(0); }
/* 序：.fab.on 之后写 :hover / :active —— 三者特异性相同(0,2,0)，靠先后决胜负 */
.fab:hover { transform: translate(-3px, -3px); box-shadow: var(--sh); }
.fab:active { transform: translate(0, 0); box-shadow: 0 0 0 var(--ink); }
.fab:focus-visible { outline: 3px solid var(--blue); outline-offset: 3px; }
.fab svg { display: block; }

/* 手机：缩小并贴近右边缘。52px 在 390px 宽的屏上能吃掉正文一角；
   46px 仍是稳妥的点击目标（≥44px）。 */
@media (max-width: 620px) {
  .fab { right: var(--s3); width: 46px; height: 46px; }
}

/* --------------------------------------------------------------------------
   自定义弹窗（.modal）—— 不用系统 alert / confirm
   --------------------------------------------------------------------------
   系统弹窗有三个没法接受的地方：
     ① 样式由浏览器与操作系统决定，套不上本项目的黑边 / 硬阴影 / 零圆角，
        在一个视觉风格很强的作品里会显得像"没做完"；
     ② 各家浏览器的弹窗长得不一样，截图与答辩演示无法保证一致；
     ③ 浏览器允许用户在被连续弹窗打扰时勾选"阻止此页面创建更多对话框"，
        一旦被勾，后面所有提示彻底失效 —— 训练流程会断。
   所以全部自绘，且在 reduced-motion 下只保留淡入、不做位移动画。
   -------------------------------------------------------------------------- */
.modal-mask {
  position: fixed; inset: 0; z-index: 800;   /* 高于 dock(200) 与 fab(195)，低于 toast(9999) */
  background: rgba(17, 17, 17, .62);
  display: flex; padding: var(--s5);
  overflow-y: auto;
  opacity: 0; visibility: hidden;
  transition: opacity .22s ease, visibility .22s;
}
.modal-mask.on { opacity: 1; visibility: visible; }

/* 模态期间：背景不滚 + 收起悬浮「回到顶部」。
   只给遮罩压 z-index 是不够的 —— 悬浮球是 fixed 定位、贴着视口右下角，
   即使点不到，从遮罩里透出一角亮黄也显得脏。干脆收掉。 */
body.modal-open { overflow: hidden; }
body.modal-open .fab { display: none; }

/* ⚠️ margin:auto 是关键：在 flex 容器里它同时做到"居中"与"内容超高时顶部不被裁"。
   只写 align-items:center 的话，弹窗一旦比视口高，上沿会被切掉且滚不上去。 */
.modal {
  margin: auto;
  width: min(560px, 100%);
  background: var(--white);
  border: var(--bd); box-shadow: 8px 8px 0 var(--ink);
  display: flex; flex-direction: column;
}
/* ⚠️⚠️ 入场动画必须挂在 **.modal-mask.on** 上，不能直接挂在 .modal 上。
   弹窗是写死在各页 HTML 里的**静态节点**（train.html 的 #briefMask），
   页面加载时它就已经在 DOM 里了；把 animation 直接写在 .modal 上，
   动画会在**页面加载那一刻**就播完（那时遮罩还是 opacity:0 / visibility:hidden），
   等用户真的点开弹窗，反而一动不动 —— 看起来就是"弹窗直接出现"。
   改成挂 .on 之后，加类的那一瞬间动画规则才被应用到元素上，动画才真正开始播。 */
.modal-mask.on .modal { animation: modal-in .34s cubic-bezier(.22, 1.15, .36, 1) both; }
/* 从下方滑入 + 轻微放大，收尾带一点过冲（cubic-bezier 的 y >1 段）。
   粗野风格的方块配一点"落下来"的份量，比纯淡入更贴合这套设计语言。 */
@keyframes modal-in {
  from { transform: translateY(28px) scale(.94); opacity: 0; }
  to { transform: none; opacity: 1; }
}
/* ⚠️ 弹窗的入场动画**不参与动效降级**，只缩短时长。
   本项目定的策略是"数值/尺寸类照播、位移类受系统设置约束"，这是**一个例外，
   而且是有意为之**：本机 Windows 关掉了系统动画（浏览器恒报
   prefers-reduced-motion: reduce），原先这里写的是 `animation: none` ——
   用户看到的就是弹窗"啪"地凭空出现，反馈是"这个弹窗需要有动画，不能直接出现"。
   判据是：**它是交互反馈本身，不是装饰**。模态的出现如果没有过渡，
   会让人以为是页面闪烁/加载出错。同类已有先例：数字滚动在 reduce 下也照播。
   时长压到 .2s，既留住了"东西落下来"的因果感，也不会让人等。
   ⚠️ 必须带 `!important`：下面 `减少动态效果` 那一段有一条
   `.reduce-motion * { animation-duration: .001ms !important }` 的全局兜底，
   不带 !important 的话这条会被它**静默吃掉**（实测量到的是 1e-06s，
   也就是"动画存在但时长为零"—— 症状和没有动画一模一样，从代码上看还挺对）。
   实测判据：`node tools/motion-check.js`。 */
.reduce-motion .modal-mask.on .modal { animation-duration: .2s !important; }

.modal-hd {
  flex: 0 0 auto;
  display: flex; align-items: center; justify-content: space-between; gap: var(--s3);
  background: var(--ink); color: var(--yellow);
  padding: var(--s4) var(--s5);
  border-bottom: var(--bd);
}
.modal-hd .mh-t { flex: 1 1 auto; min-width: 0; font-size: var(--fs-lg); font-weight: 900; letter-spacing: .5px; }
.modal-hd .mh-s { flex: 0 0 auto; font-size: var(--fs-xs); font-weight: 800; opacity: .8; white-space: nowrap; }

/* 标题栏右上的「取消」。做成方形描边按钮而不是一个裸的 ✕ 字符：
   后者在各平台字体里大小与基线都不一样，落在方块里会显得歪。 */
.modal-hd .mh-x {
  flex: 0 0 auto;
  width: 30px; height: 30px; padding: 0;
  display: grid; place-items: center;
  border: 2px solid var(--yellow); background: transparent; color: var(--yellow);
  cursor: pointer;
  transition: background .16s ease, color .16s ease;
}
.modal-hd .mh-x:hover { background: var(--yellow); color: var(--ink); }
.modal-hd .mh-x:focus-visible { outline: 2px solid var(--yellow); outline-offset: 2px; }
.modal-hd .mh-x svg { display: block; }
/* 窄屏收掉副标题，把宽度让给标题与取消键 —— 它会换行，但换行也比三个元素互挤好 */
@media (max-width: 460px) { .modal-hd .mh-s { display: none; } }
.modal-bd { flex: 1 1 auto; overflow-y: auto; padding: var(--s5); }
.modal-ft { flex: 0 0 auto; padding: 0 var(--s5) var(--s5); }

/* ---- 身份简报（弹窗内的内容） ---- */
.brief-meta { display: flex; flex-wrap: wrap; gap: var(--s3); margin-bottom: var(--s5); }
.brief-meta span {
  border: 2px solid var(--ink); background: var(--paper);
  padding: 4px var(--s3); font-size: var(--fs-xs); font-weight: 800;
}
.brief-who {
  font-size: var(--fs-lg); font-weight: 900; line-height: 1.55;
  border-left: 6px solid var(--yellow); padding-left: var(--s4);
}
.brief-detail { margin-top: var(--s4); font-size: var(--fs-sm); font-weight: 700; line-height: 1.95; }
/* 加载态：三条骨架条 + 一行实话。
   不画假进度条 —— 它有"快好了"的暗示，而这一轮实测就是 8～10 秒。 */
.brief-skel { height: 14px; background: var(--paper); border: 2px solid var(--ink); margin-bottom: var(--s3); }
.brief-skel.k2 { width: 78%; }
.brief-skel.k3 { width: 56%; }

/* 打印 */
@media print {
  .nav, .footer, .btn, .no-print { display: none !important; }
  body { background: var(--white); background-image: none; }
  .nb, .nb-flat { box-shadow: none; }
  .wrap { max-width: 100%; }
}

