/* 口语对话实战器 · 全站样式
   ============================================================
   结构约定（不变）
     本文件是全站唯一样式表，4 个页面共用一个 —— 不采用技能包建议的
     「CSS 内联到各页面」（那会让同一套样式存四份）。见 TECH_DESIGN.md §4.1。

   Day 12 重设计
     设计依据换成 Taste Skill V1，参数取值（技能基线 8 / 6 / 4）：
       DESIGN_VARIANCE  8 → 5   这是每天要用的任务入口，不是作品集。
                                用户带着「开口焦虑」来，不对称只留在留白与
                                比例上，不留在「内容位置不可预测」上。
       MOTION_INTENSITY 6 → 6   保留：错峰入场 + 触觉按压 + 弹簧曲线 + 持续微动。
       VISUAL_DENSITY   4 → 4   Daily App Mode，正常应用间距。

     技能与项目既有锁定的三处冲突，处理方式（不自行取舍，逐条说明）：
       ① 技能默认 React / Tailwind / Framer Motion
          → 本项目技术路线「乙」是原生 HTML/CSS/JS 无打包（TECH_DESIGN.md §3.3），
            不引入任何依赖。技能里关于动效的建议一律用纯 CSS 实现。
       ② 技能「Dashboard 禁用衬线体」
          → 本项目衬线体只用在**英文内容本身**（卡片英文说明、条目引文），
            不用在任何标题/标签/数字上。技能禁的是「UI 外壳用衬线」，
            这里是「内容是英文句子，用衬线才读得对」——不冲突，保留。
       ③ 技能「最多 1 个强调色」
          → 保留三色，但改成**各有分工、互不抢位**（见下方 --accent 的注释）。
            PRD §6.2 用颜色区分「偏题 / 逻辑错误 / 精彩句子」是产品语义，
            不能为了配色规则删掉。

     这一版改的是：字体栈、间距节奏、卡片结构、层级、动效、状态页；
     锁定的品牌色值与 PRD 口径全部不动。
   ============================================================ */

:root {
  /* ---- 配色（60-30-10）---- */
  /* 60% 主色：暖奶油底，避开纯白的冷硬 */
  --bg: #FDFBF6;
  --bg-tint: #FBF5EA;
  /* 30% 辅助色：白卡片 + 浅奶油区块 */
  --surface: #FFFFFF;
  --surface-soft: #FFF8EE;

  /* ---------- 10% 强调色（Day 12 重新分工）----------
     原来三个色到处串：编号是珊瑚、区块小标是珊瑚、悬停边框是蜜橘、
     星标渐变是蜜橘→珊瑚、卡点数字是珊瑚…… 结果是**没有一个颜色在说事**，
     页面看着吵。现在按职能分工，每个色只干一件事：

       --primary / --primary-dark  → 组 A（会议上）的**组色**
       --teal                      → 组 B（同事间）的组色
       --accent（珊瑚）            → 警示：偏题、卡点、失败、品牌点

     另一条：**鲜色只做装饰，深色才做文字**。
     依据是 Day 12 实测的对比度（白底 / 奶油底）：
       原写法                          实测     12px 文字要求 4.5
       --accent  #E8624A 当文字         3.35     ✗ 不达标
       --primary #D97706 当文字         3.19     ✗ 不达标
       白字压在蜜橘→珊瑚渐变按钮上      3.19~3.35 ✗ 不达标
     所以新增两个「-deep」令牌专供小字，鲜色值一个没动（继续做圆斑、
     竖条、圆点、大号数字等装饰）。 */
  --primary: #D97706;          /* 装饰：组 A 圆斑、竖条、边框、大号数字 */
  --primary-dark: #B45309;     /* 白底 5.02 · 奶油底 4.86 —— 可做正文级文字 */
  --primary-deep: #9A4708;     /* Day 12 新增：小字专用。白底 6.40 · 蜜橘浅底 5.70 */
  --primary-soft: #FDF0DC;

  --accent: #E8624A;           /* 装饰：品牌圆点、警示边框、渐变止点 */
  --accent-deep: #A83A23;      /* Day 12 新增：警示小字专用。白底 6.37 · 珊瑚浅底 5.60 */
  --accent-soft: #FDEDE8;

  --teal: #0F7A6B;             /* 组 B 组色。白底 5.23 · 深青浅底 4.61 —— 达标，不用改 */
  --teal-soft: #E4F4F0;

  /* 主按钮渐变端点（Day 12 新增两个令牌）。
     原 #D97706 → #E8624A 上压白字只有 3.19~3.35，16px 文字不达标；
     换成下面两档后两端都是 5.02 / 4.87，且色相仍是蜜橘→红橘，
     「多巴胺」的暖色观感保住，只是深了一档 —— 反而更像熟成的水果，不像荧光。 */
  --grad-from: #B45309;
  --grad-to: #C24A2E;
  --grad-from-hover: #9A4708;
  --grad-to-hover: #A83A23;

  --text: #2B2622;             /* 非纯黑（技能第八节禁用 #000000） */
  /* 三级文字色阶（深 → 中 → 浅）。
     --text-muted 于 Day 9 由 #9C9288 调深为 #736A5F：
     原值在 6 种底色上只有 2.68~3.05，低于正文 4.5:1；调深后为 4.67~5.31。
     Day 12 复核：4.89（奶油深底）~5.31，全达标，不动。 */
  --text-secondary: #6B6259;
  --text-muted: #736A5F;
  --border: #EBE3D6;
  --border-soft: #F2EADD;

  /* ---- 排版（Day 12 换栈）----
     换中文字体这件事原本挂在「待决」里，理由是「自托管几 MB 进仓库 / CDN 国内不稳」。
     Day 12 查了本机 C:\Windows\Fonts，发现**不需要下载**：
       Noto Sans SC（VF + 静态 Regular/Medium/Bold）、MiSans、HarmonyOS Sans SC
       都在本地已装。于是直接换栈，零下载、零网络依赖、零授权风险。
     为什么 Noto Sans SC 排第一：它是思源黑体的 Google 发行版，本机有真
     Regular/Medium/Bold 三档（MiSans 只有 Regular，标题会退化成合成粗体）。
     观感上它比原先实际落到的「微软雅黑」笔画更匀、拉丁字母更干净。
     代价（已记录）：换机器若没装这几款，会退到 PingFang / 雅黑 —— 中文仍正常，
     只是没了这一档质感。要彻底锁定就得自托管字体文件，那件事仍然没做。 */
  --font-sans: "Noto Sans SC", "MiSans", "HarmonyOS Sans SC", "PingFang SC",
    "Microsoft YaHei", "Hiragino Sans GB", sans-serif;
  /* 英文内容（卡片说明、条目引文）走衬线斜体，是「英语特色」的主要落点。
     只在装英文句子的地方用，不用在标题/标签/数字上。 */
  --font-en: Georgia, "Sitka Text", Cambria, "Times New Roman",
    "Songti SC", serif;
  /* 等宽只用在「技术小标」：区块编号、TOPIC N、轮次、计时。
     原先落到的 Consolas 太像代码编辑器；Cascadia Mono 笔画更整、更现代。 */
  --font-mono: "Cascadia Mono", "Cascadia Code", "Source Code Pro",
    "SF Mono", Consolas, "Courier New", monospace;

  --text-xs: 0.75rem;
  --text-sm: 0.875rem;
  --text-base: 1rem;
  --text-lg: 1.125rem;
  --text-xl: 1.25rem;
  --text-2xl: 1.5rem;
  /* Day 14 第 4 板块删掉了 --text-3xl：全站 0 处引用（实测 grep -c = 0），
     是一条死令牌。留着无害，但会让下一个读令牌表的人以为有个 30px 的档位可用，
     于是拿它去"加大标题"——那正好是项目注释里禁止的「靠字号喊话」。
     真正要更大时用 .topbar h1 的 clamp，不用新加令牌。
     字号分布实测（grep计数）：xs 23 / sm 21 / base 7 / lg 4 / xl 6 / 2xl 4。
     xs+sm 占八成是对的——小字是界面的主力，标题层只该占少数，
     层级差得开才叫层级，全挤在 12~14px 才是"层级糊了"。 */

  /* ---- 间距（4px 网格）---- */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.25rem;
  --space-6: 1.5rem;
  --space-8: 2rem;
  --space-10: 2.5rem;
  --space-12: 3rem;

  /* ---- 圆角与阴影 ----
     阴影一律带暖色底（rgba(107,84,56,…)），不用中性灰 ——
     压在奶油底上，中性灰阴影会显脏。技能第四条同款要求（阴影要染色）。 */
  --radius-sm: 0.25rem;
  --radius-md: 0.5rem;
  --radius-lg: 0.75rem;
  --radius-xl: 1rem;
  --radius-full: 9999px;

  --shadow-sm: 0 1px 2px rgba(107, 84, 56, 0.05);
  --shadow-md: 0 4px 10px -2px rgba(107, 84, 56, 0.09), 0 2px 4px -2px rgba(107, 84, 56, 0.06);
  --shadow-lg: 0 12px 24px -6px rgba(107, 84, 56, 0.13), 0 4px 8px -4px rgba(107, 84, 56, 0.07);

  /* ---- 动效 ----
     --ease-spring 是回弹曲线（技能要求交互元素走弹簧感，不要线性）。
     --ease-soft 是入场曲线：比 --ease-out 更缓，长距离位移用它。 */
  --fast: 150ms;
  --normal: 300ms;
  --ease-out: cubic-bezier(0, 0, 0.2, 1);
  --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
  --ease-soft: cubic-bezier(0.16, 1, 0.3, 1);

  /* 卡片错峰入场：第 N 张的延迟 = --i × --stagger（--i 由 nth-child 给，见下）。
     55ms × 最多 5 档 = 275ms 之内全部起步，整段动画 700ms 内收尾。 */
  --stagger: 55ms;

  /* ============================================================
     暗场 + 光球（Day 13 新增）
     ------------------------------------------------------------
     为什么需要一个暗场：这一版的核心形象是「AI 是一枚会脉动的光球」，
     而**光只有落在暗处才看得见**。奶油底上放一颗发光球，它顶多是个橙色圆圈。
     所以对话页的对话区（.stage）单独做成一块深暖棕的「舞台」——
     像书桌上的一盏灯，四周暗下来，光球成为房间里唯一在动的东西。

     为什么不是整站变深色（这是刻意的克制）：
       ① 主题列表是「挑东西」，要在明亮里快速扫读，暗色只会拖慢判断；
       ② 明暗对比本身就是冲击力 —— 从亮处点进对话页，画面沉下去，
          这个下坠感在告诉用户「现在开始认真说话了」；
       ③ 全站深色 + 发光是通用的 AI 套路，只在需要的地方暗，才有意图。

     颜色为什么是暖棕而不是黑：纯黑（#000）会把暖橘光压成冷调，
     整个屏幕像关机的手机。深暖棕是「关了灯的客厅」，木色还在。
     全部取值经 WCAG 相对对比度实测（背景用最深的 --night）：
       --night-text  #F5EDE2  14.68  正文与光球上方的引导语
       --night-2nd   #CDBBA6   9.13  次一级说明
       --night-muted #AB9985   6.19  12px 小字（阈值 4.5）
       --night-accent#E8A34E   7.94  暗场上的强调文字
     ============================================================ */
  --night: #241A13;              /* 舞台底色 */
  --night-raised: #33251A;       /* 舞台内需要抬升的块 */
  --night-edge: #3E2D20;         /* 舞台描边 */
  --night-text: #F5EDE2;
  --night-2nd: #CDBBA6;
  --night-muted: #AB9985;
  --night-accent: #E8A34E;

  /* 光球的三档配色。它不是「一个橙色的圆」——
     从左上角高光到右下角收暗是一个完整的球体，所以用径向渐变而不是纯色。
     暗底上的对比度（只需 3.0 即清晰可辨）：亮核 10.84 / 暖橙 8.01 / 珊瑚 5.09。 */
  --orb-hi: #FFE0AE;             /* 球面高光 */
  --orb-core: #FFC46B;           /* 受光面 */
  --orb-warm: #F2A03C;           /* 球体主色 */
  --orb-lo: #E8624A;             /* 背光面（收在珊瑚，与品牌色同源） */
  --orb-glow: rgba(242, 160, 60, 0.5);
  --orb-ring: rgba(255, 196, 107, 0.65);
}

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

/* hidden 属性兜底（Day 10 新增）。
   为什么需要：HTML 的 hidden 属性只等价于一条极低优先级的 display:none，
   任何显式写了 display 的规则都能把它盖掉 —— 例如 .rec-live{display:flex}
   会让 hidden 完全失效（Day 10 实测：未录音态仍然渲染出红点与「正在听……」）。
   加了这条之后，hidden 在任何元素上都可靠，组件不必各自防一遍。 */
[hidden] {
  display: none !important;
}

/* 键盘焦点描边（Day 9 新增）。
   出处：frontend-design 中文版第 235 行「:focus-visible 可见描边（2px + 2px 偏移）」。
   为什么用 :focus-visible 而不是 :focus：鼠标点击时不显示描边，只有键盘 Tab
   或程序化聚焦时才出现 —— 规范推荐做法，不给鼠标用户添噪。
   为什么用 --primary-dark：奶油底上 4.86:1，焦点指示要的是「一眼看见」。
   这里**不写 border-radius**：outline 会自动跟随元素自身圆角，写了反而会把
   胶囊按钮的描边压成小圆角。 */
:focus-visible {
  outline: 2px solid var(--primary-dark);
  outline-offset: 2px;
}

body {
  font-family: var(--font-sans);
  font-size: var(--text-base);
  line-height: 1.75;
  color: var(--text);
  background: var(--bg);
  min-height: 100vh;
  padding: var(--space-8) var(--space-6) var(--space-12);
  -webkit-font-smoothing: antialiased;
  /* 中英夹排时数字与标点不跳字宽 */
  font-variant-numeric: tabular-nums;
}

/* 页面顶部一层极淡的暖光，让整片底色不那么平。
   Day 12：高度 320 → 420px，渐变更缓 —— 拉开后上半屏有一层「晨光」，
   hero 与通知卡落在里面，视线自然从亮处往内容走。
   技能第五条要求：这类装饰滤镜只能挂在 fixed + pointer-events:none 的伪元素上，
   不能挂在滚动容器上（否则每帧重绘，手机端掉帧）。这条本来就合规。 */
body::before {
  content: "";
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 420px;
  background: linear-gradient(180deg, var(--bg-tint) 0%, rgba(251, 245, 234, 0) 100%);
  pointer-events: none;
  z-index: 0;
}

/* 容器宽度 1000 → 1080（Day 12）。
   为什么：主题卡三列是页面的主体，1000px 下每张只有约 305px，
   卡片英文说明两行就满、和「预计时长」挤在一起。1080px 下每张 333px，
   同时正文仍在 65ch 以内（.lede / .hint 各自限宽），不影响阅读。 */
.wrap {
  position: relative;
  z-index: 1;
  max-width: 1080px;
  margin: 0 auto;
}

/* ============================================================
   页头（hero）
   ------------------------------------------------------------
   技能第三条「反对居中 bias」：hero 一律左对齐或分屏，不做居中大标题。
   这一页本来就是左对齐，改动在**层级怎么交代**：
   技能第七条禁止「靠巨大字号喊话」，所以标题不再放大，
   改为「标题压重 + 说明限宽 + 一条发丝线收口」建立区块感。
   ============================================================ */

.topbar {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4);
  flex-wrap: wrap;
  margin-bottom: var(--space-3);
}

.topbar h1 {
  /* Day 13：30px 定值 → clamp 流式（28 ~ 38px）。
     为什么：30px 的页头标题与区块 2 的标题（24px）只差 6px，
     在 1440px 的宽屏上页头压不住场面，两级标题看着差不多大。
     改成流式后窄屏不溢出、宽屏撑得起页头，层级一眼分明。
     Day 14 第 3 板块：上限 38 → 44px，下限 1.75rem 不动。
     为什么不改下限：窄屏（390px）上 28px 已经够压场，再大必溢出。
     为什么上限敢加到 44：原注释担心的「喊话体」风险，
     真正的解法是把**信息量补足**（见下面 .lede 提亮 + h1 下方新增的品牌副题），
     而不是把标题空放大 —— 44px 配一句说清用途的话，是「有主语」，
     38px 配空标题才是「喊」。 */
  font-size: clamp(1.75rem, 3.8vw, 2.75rem);
  font-weight: 700;
  line-height: 1.2;
  /* 中文标题收紧字距会显得局促，只收一点点（-0.01em）；
     真正拉开层级的是下面的说明文字与发丝线，不是字号。 */
  letter-spacing: -0.015em;
  /* 标题折行时按语义均衡分配，避免「口语对话实 / 战器」这种断法 */
  text-wrap: balance;
}

/* 标题右侧一枚发光的小球，作为品牌记忆点。
   Day 13：从「一枚珊瑚圆点」改成**光球的缩小版**（同一套径向渐变 + 一圈柔光），
   理由是形象要自洽 —— 对话页中央那颗球是这个产品的脸，
   它在页头以 10px 的尺寸先出现一次，用户进对话页看到大球时才会认出「是它」。
   若这里仍是一枚扁平色点，大球就成了一个孤立的装饰。

   呼吸 3.2s 是全站唯一的持续微动（技能第四条要求 MOTION_INTENSITY > 5 时埋一枚），
   幅度刻意小：1 → 1.18 倍。它同时充当「AI 在线」的活体信号。 */
.topbar h1::after {  content: "";
  display: inline-block;
  width: 10px;
  height: 10px;
  border-radius: var(--radius-full);
  background: radial-gradient(
    circle at 34% 30%,
    var(--orb-hi) 0%,
    var(--orb-core) 30%,
    var(--orb-warm) 66%,
    var(--orb-lo) 100%
  );
  /* 两圈：近处一圈实边（球的轮廓），远处一圈散开（光溢出到空气里） */
  box-shadow:
    0 0 0 3px rgba(242, 160, 60, 0.16),
    0 0 12px rgba(242, 160, 60, 0.55);
  margin-left: 10px;
  vertical-align: 0.5rem;
  animation: brandBreath 3.2s var(--ease-out) infinite;
}

@keyframes brandBreath {
  0%, 100% { transform: scale(1);    opacity: 1; }
  50%      { transform: scale(1.18); opacity: 0.72; }
}

.navlink {
  display: inline-block;
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--primary-dark);
  text-decoration: none;
  /* Day 9：上下内边距 8px → 10px。原值实测点击区仅 43px，低于要求的 44×44px；
     10px 落在 4px 网格上，实测得 46px。 */
  padding: 10px var(--space-4);
  border-radius: var(--radius-full);
  background: var(--surface);
  border: 1px solid var(--border);
  transition: color var(--fast) var(--ease-out),
              border-color var(--fast) var(--ease-out),
              background var(--fast) var(--ease-out),
              transform var(--fast) var(--ease-out);
}

/* 悬停文字原用 --accent（白底 3.35，14px 不达标），改用 --accent-deep（6.37）。
   边框是装饰、只需 3:1，仍可用鲜珊瑚。 */
.navlink:hover {
  color: var(--accent-deep);
  border-color: var(--accent);
  background: var(--accent-soft);
  transform: translateY(-1px);
}

.navlink:active {
  transform: translateY(0) scale(0.98);
}

/* ---------- 面包屑（Day 13 新增） ----------
   位置：紧贴 .topbar 之下、.lede 之上。四页都有（话题链一致，见下）。

   为什么单独一套样式、不复用 .navlink：
     .navlink 是**药丸按钮**（10px 内边距、圆角胶囊、46px 高），
     那是「可点击的主要去处」的样子。面包屑是**位置指示**——
     它回答「我在哪」，不回答「点我去哪」。两者角色不同，
     套用同一套视觉会让页头多出三个抢戏的胶囊，把 h1 压下去。

   为什么是深青而不是蜜橘（视觉规范：三色分工）：
     蜜橘族是 A 组（会议上）、深青族是 B 组（同事间）。
     面包屑是**全站通用构件**，不属于任何一个组，
     所以走中性文字档（--text-muted / --primary-deep），
     不占用 A / B 任一组的身份色 —— 一旦占了，读者会误以为
     「我现在在 A 组那一侧」。 */

.breadcrumb {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: 0 0 var(--space-3);
  font-size: var(--text-sm);
  line-height: 1.6;
}

.breadcrumb a {
  color: var(--primary-deep);          /* 白底 6.40，14px 小字达标 */
  text-decoration: none;
  /* 触控目标：min-height 44px 是硬要求（regress.js 逐页逐断点量）。
     为什么不靠 padding 撑到 44：10px padding + 22.4px 行高 = 42.4px，
     实测 42px，**差这2px** —— 而 padding 从 10 调到 11 会脱离4px 网格。
     所以 padding 保持 10px（仍在网格上），另用 min-height 兜到 44px。
     负 margin 把padding 撑出来的高度收回，面包屑不会在页头下多占一行。 */
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 10px var(--space-2);
  margin: -10px calc(var(--space-2) * -1);
  border-radius: var(--radius-sm);
  transition: color var(--fast) var(--ease-out),
              background var(--fast) var(--ease-out);
}

.breadcrumb a:hover {
  color: var(--accent-deep);
  background: var(--accent-soft);
}

.breadcrumb a:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* 分隔符「›」：aria-hidden —— 读屏用户听到的是"主题列表 错误记录"，
   中间这个符号没有语义，读出来只会变成"尖括号"。*/
.breadcrumb .sep {
  color: var(--text-muted);
  /* 「›」在中文里的视觉重心偏上，基线对齐会显得浮 */
  transform: translateY(-1px);
}

/* 当前页：不是链接，是文字。
   为什么不给它加背景色块：加了色块，它看起来就"可点"，
   而它是这一层里唯一不能点的一页（点自己没意义）。 */
.breadcrumb [aria-current="page"] {
  color: var(--text-secondary);
  font-weight: 500;
  cursor: default;
}

/* 窄屏：面包屑允许折行，但不让它挤到 h1 下面显得像正文。
   flex-wrap 已在 .breadcrumb 上，这里只保证折行后行距不塌。 */
.breadcrumb {
  row-gap: var(--space-1);
}

/* 页头与主内容之间的一条发丝线（Day 12 新增）。
   为什么：原来标题区与三张卡之间只靠 margin 分隔，视觉上是一个连续的大块；
   加一条线之后页头自成一个「带」，下面的内容从线上「开始」。
   为什么挂在 .lede 上：HTML 里 .topbar 与 .lede 是兄弟节点，没有共同父级
   可以做浏览器，写在 .lede 的 border-bottom 上等于给整个页头收口，
   且不需要改 HTML 结构。 */
.lede {
  /* Day 14 第 3 板块：16px → 19px，--text-secondary → --text。
     为什么改这里而不是只改 h1：用户反馈是「不知道这个网站是干什么的」，
     而**回答这个问题的是这段说明，不是标题**——标题只有产品名，
     光把「口语对话实战器」放到 60px 也还是不知道能干什么。
     提亮 + 放大让它在首屏成为第二强的阅读落点（第一是 h1）。
     19px 是刻意停在「正文偏大」而不是「标题级」：
     它仍需与下面的 .card 标题（24px）拉开层级差。
     对比度：--text 2B2622 压奶油底 FDFBF6 = 14.6:1（原 --text-secondary 为 5.31）。 */
  color: var(--text);
  font-size: clamp(1.0625rem, 0.95rem + 0.6vw, 1.1875rem);
  font-weight: 500;
  line-height: 1.65;
  max-width: 62ch;                 /* 技能：正文行宽不超过 65ch */
  text-wrap: pretty;               /* 末行不留单字 */
  padding-bottom: var(--space-5);
  border-bottom: 1px solid var(--border);
  margin-bottom: var(--space-8);
}

/* 说明段里的语义重音（Day 14 第 4 板块）。
   「它不给你台阶」这句是整个产品的性格所在——AI 不迁就你，
   这是与普通练习类产品的分水岭，值一个视觉重音。
   不用珊瑚色：珊瑚在本项目被定为**警示色专管**（偏题、卡点、失败），
   用在这里会让「产品的个性」被读成「这里有警告」。
   改用墨色加重：强调靠重量，不靠颜色。 */
.lede strong {
  font-weight: 700;
  color: var(--text);
  /* 不加底色块：说明段里已经有一个 .eyebrow 徽章，再加一块会变成两块色块打架 */
}

/* ---------- 页头品牌副题（Day 14 第 3 板块新增） ----------
   加它是因为用户反馈「不知道这个网站是干什么的」。
   只放大 h1 解决不了：产品名「口语对话实战器」本身不回答「能干什么」，
   而 P2/P3/P4 的 h1（对话 / 会话结果 / 错误记录）是**页面名**，
   在那种页面里更需要一句「你在哪、接下来干什么」。

   【结构上的一个坑，改版时踩过】标签必须是 h1 的**兄弟节点、排在 h1 前面**，
   不能塞进 h1 里面。塞进去试过，窄屏（390px）实测 h1 高度被撑到 74px：
   h1 是个行盒，里面的块级子元素（标签 display:block）会参与它的行内布局，
   标签另起一行后 h1 多出一个行盒，标题本身就被挤到第二行去了。
   移出来之后，窄屏是「标签一行 + 标题一行」，各占各的高度，互不影响。

   为什么不用「标题下方的副标题」：那会把页头整体推高，
   把面包屑和 .lede 一起挤下去，而首屏高度在 390px 上本来就紧张。

   Day 14 第 4 板块：等宽 → **衬线体**（--font-en）。
   理由：这枚标签里装的是英语（"职场英语开口练习"是中文，但 P2 的是
   "AI 当场追问 · 第 1 步"、P3/P4 也含英文），而全站唯一被允许用衬线体的
   位置就是「装英文句子的地方」（见 main.css 里 .en 与各台词段的注释）。
   标签本来就是一句分类语，不是编号——**编号才配等宽**。
   顺带：等宽在这里还有个实际问题，「职场英语开口练习」10 个汉字用
   Cascadia Mono 会退到不支持中文的字形，字距忽宽忽窄，很难看。 */
.brandtag {
  display: block;
  width: fit-content;
  font-family: var(--font-en);
  font-size: var(--text-sm);
  font-weight: 600;
  /* 衬线体不需要额外拉字距（0.06em 是给等宽调的，
     衬线体本身有衬线做横向分隔，再加会显松散） */
  letter-spacing: 0.01em;
  color: var(--accent-deep);        /* A83A23 压奶油底 6.37:1 */
  background: var(--accent-soft);
  border: 1px solid rgba(232, 98, 74, 0.28);
  border-radius: var(--radius-full);
  /* padding 3px/4px → 2px/3px：实测标签高 47px，比旁边的 14px 小标高一倍多，
     视觉上像顶了个门帘。衬线体在 pill 里本身留白就多，2/3 够居中了。 */
  padding: 2px var(--space-3) 3px;
  /* 桌面：与标题同行，用 margin-bottom 压到标题基线附近；
     窄屏（≤480px）改为独立成行，见下方媒体查询。 */
  margin-bottom: var(--space-2);
  white-space: nowrap;
}

@media (max-width: 480px) {
  /* 窄屏（390px 实测）：标签 + 标题同排会折成三行、把 navlink 顶到第四行。
     改为独立成行当「分类标签」用，标题另起一行 —— 两行比三行清楚。
     这条竖线是分隔不是装饰：独立成行的标签若不给界线，
     它会和紧随其后的面包屑（「主题列表」）糊成一块，
     而这两者一个属于标题、一个属于导航，混一起读不出层级。 */
  .brandtag {
    margin-bottom: var(--space-3);
  }
  .brandtag::after {
    content: "";
    display: block;
    width: 100%;
    height: 1px;
    background: var(--border);
    margin-top: var(--space-3);
  }
}

/* 顶栏里的多个链接用竖线隔开 */
.topbar > div > .navlink + .navlink {
  margin-left: var(--space-2);
}

/* 英文引文统一走衬线体，是「英语特色」的主要落点 */
.en {
  font-family: var(--font-en);
  font-style: italic;
}

/* ============================================================
   通用块
   ============================================================ */

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-xl);
  padding: var(--space-6);
  margin-bottom: var(--space-4);
  box-shadow: var(--shadow-sm);
}

.card h2 {
  font-size: var(--text-lg);
  font-weight: 600;
  line-height: 1.45;
  margin-bottom: var(--space-1);
  text-wrap: balance;
}

/* 需要与上方内容拉开一段距离的卡片（Day 9 新增）。
   出处：records.html 第 48 行原来是行内 `style="margin-top:24px"`。 */
.card-spaced {
  margin-top: var(--space-6);
}

.card .hint {
  font-size: var(--text-sm);
  color: var(--text-muted);
  line-height: 1.7;
  max-width: 62ch;
  text-wrap: pretty;
}

/* 卡片里「标题区（h2 + .hint）」与「正文内容」之间的一道间距（Day 9 新增）。
   出处：result.html 三处行内 `style="margin-top:14px"` —— 14px 不在 4px 网格上，
   且同一意图写三遍。现在统一用兄弟选择器表达：任何紧跟在 .card .hint 后面的元素
   自动获得 16px 上边距，HTML 里不必再贴类名。 */
.card .hint + * {
  margin-top: var(--space-4);
}

/* 说明性条目列表（Day 9 新增）。
   出处：原来是 records.html 第 51 行的一串行内样式。
   处理口径：14px → --text-sm；#57606a → --text-muted；12px → --space-3；
   左侧 20px 缩进保留原值（它给的是列表标记留位，属列表自身排版）。
   命名用通用名 .spec-list，不绑死某个页面。 */
.spec-list {
  margin: var(--space-3) 0 0 var(--space-5);
  font-size: var(--text-sm);
  color: var(--text-muted);
  line-height: 1.7;
}

.spec-list li + li {
  margin-top: var(--space-1);
}

/* ============================================================
   分组标题
   ------------------------------------------------------------
   Day 12 改动：左侧色条之后补一条**渐隐发丝线**，一直拉到容器右边缘。
   为什么：原来标题后面是一片空白，组与组之间的分界靠「上面的卡片结束」
   猜出来；补线之后每组有了明确的起始边，页面被切成三段，扫描成本下降。
   ============================================================ */

.section-title {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  font-size: var(--text-xl);
  font-weight: 600;
  color: var(--text);
  margin: var(--space-10) 0 var(--space-5);
  text-wrap: balance;
}

.section-title::before {
  content: "";
  width: 5px;
  height: 22px;
  border-radius: var(--radius-full);
  flex: none;
  background: var(--primary);
}

/* 发丝线：撑满剩余宽度，向右渐隐，不与卡片边框抢存在感 */
.section-title::after {
  content: "";
  flex: 1 1 auto;
  min-width: var(--space-4);
  height: 1px;
  background: linear-gradient(90deg, var(--border) 0%, rgba(235, 227, 214, 0) 100%);
}

.section-title.group-a::before {
  background: linear-gradient(180deg, var(--primary) 0%, var(--accent) 100%);
}

.section-title.group-b::before {
  background: linear-gradient(180deg, var(--teal) 0%, #35A895 100%);
}

.section-title .count {
  font-weight: 400;
  font-size: var(--text-sm);
  color: var(--text-muted);
  flex: none;
}

/* ============================================================
   昵称区
   ------------------------------------------------------------
   技能第六条：表单标签必须在输入框上方。原先标签与输入框同排（.field 是横向
   flex），宽屏下没问题，但窄到 400px 时「昵称」会被挤到输入框左侧贴边，
   标签与控件的关系变模糊。
   Day 12：改成**纵向两行**（标签在上、输入框在下），桌面端靠栅格放到右列，
   与左列标题齐平 —— 结构上成了标准的「label 在上」表单块，任何宽度都不会歧义。
   ============================================================ */

.field {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.field label {
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-secondary);
}

.field input {
  width: 100%;
  min-width: 0;
  font: inherit;
  font-size: var(--text-base);
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--bg);
  color: var(--text);
  transition: border-color var(--fast) var(--ease-out),
              background var(--fast) var(--ease-out),
              box-shadow var(--fast) var(--ease-out);
}

.field input::placeholder {
  color: var(--text-muted);
}

.field input:focus {
  border-color: var(--primary);
  background: var(--surface);
  box-shadow: 0 0 0 3px rgba(217, 119, 6, 0.13);
}

/* 输入框原有的 `outline: none`（Day 9 移除）会把键盘焦点圈一并干掉。
   现在改由全局 :focus-visible 统一给描边，鼠标点击时只显示边框变色 + 柔光环。 */

/* ============================================================
   主题卡片
   ------------------------------------------------------------
   Day 12 重排卡片内部结构。原来卡片是「一条条竖着摞的块」，
   编号、名称、角色、英文说明、标记、星标逐行往下排，
   星标落在最后一行的左下方 —— 视觉上像被随手丢下的按钮。
   现在把卡片改成 5 行栅格，把**星标挪到顶行右侧**，
   与左上角的 TOPIC N 形成对角；底部一整行留给信息胶囊。
   ============================================================ */

.topic-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(290px, 1fr));
  gap: var(--space-4);
}

.topic {
  /* 5 行栅格：no / name / role / summary / (meta + star) */
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  /* 第 4 行（英文说明）拿 1fr：卡片被同行其他卡片拉高时，
     多出来的空间全给说明行，底部信息栏始终贴着卡片下沿对齐。 */
  grid-template-rows: auto auto auto 1fr auto;
  grid-template-areas:
    "no    no"
    "name  name"
    "role  role"
    "sum   sum"
    "meta  star";

  position: relative;
  background: var(--surface);
  /* 左侧 3px 是「悬停指示条」的预留位。
     为什么预留在常驻状态而不是悬停时才加宽：悬停才加宽会让内容横移 3px，
     看着像卡片被推动了一下。常驻留白、只换颜色，就没有位移。 */
  border: 1px solid var(--border);
  border-left: 3px solid transparent;
  border-radius: var(--radius-lg);
  padding: var(--space-6);
  text-decoration: none;
  color: inherit;
  overflow: hidden;
  transition: border-color var(--normal) var(--ease-out),
              box-shadow var(--normal) var(--ease-out),
              transform var(--normal) var(--ease-out);

  /* 错峰入场（技能第四条「不要瞬间挂载整个列表」）。
     延迟量 --i 由下面的 nth-child 规则给，不从 HTML/JS 传 ——
     这样卡片组件（topic-card.js）一个字都不用改。
     animation-fill-mode: backwards 保证延迟期间停在起始态（透明），
     否则会先闪一帧完整卡片再跳回去重放。 */
  animation: cardRise 420ms var(--ease-soft) backwards;
  animation-delay: calc(var(--i, 0) * var(--stagger));
}

@keyframes cardRise {
  from { opacity: 0; transform: translateY(12px); }
  to   { opacity: 1; transform: none; }
}

/* 第 N 张卡的序号。上限压到 5：组 A 5 张、组 B 3 张，
   最大延迟 5 × 55ms = 275ms，整组动画 700ms 内收尾，不拖。 */
.topic-list > .topic:nth-child(1) { --i: 0; }
.topic-list > .topic:nth-child(2) { --i: 1; }
.topic-list > .topic:nth-child(3) { --i: 2; }
.topic-list > .topic:nth-child(4) { --i: 3; }
.topic-list > .topic:nth-child(5) { --i: 4; }
.topic-list > .topic:nth-child(n + 6) { --i: 5; }

/* 卡片右上角一枚暖色光斑，悬停时向外扩 —— 「多巴胺」的主要来源。
   Day 13：由**实心圆**改成**径向渐隐**。
   理由：实心圆无论多淡，都是一个有硬边的色块（放大截图能看见边缘）；
   而这一版全站的主题是「光」，光没有边。
   改成渐隐后悬停放大 1.3 倍时，读者看到的是「这片光变亮了」，
   而不是「这个圆变大了」——同一个动作，读出来的意思完全不同。
   尺寸与位置随之调（108 → 150px、-46 → -56px），
   让圆心仍落在卡片外侧、只把一段弧渗进卡内，绝不压到英文说明那一行。 */
.topic::before {
  content: "";
  position: absolute;
  top: -66px;
  right: -66px;
  width: 180px;
  height: 180px;
  border-radius: var(--radius-full);
  background: radial-gradient(
    circle,
    rgba(253, 240, 220, 1) 0%,
    rgba(253, 240, 220, 0.55) 44%,
    rgba(253, 240, 220, 0) 74%
  );
  transition: transform var(--normal) var(--ease-out), opacity var(--normal) var(--ease-out);
}

.topic-list.group-b .topic::before {
  background: radial-gradient(
    circle,
    rgba(228, 244, 240, 1) 0%,
    rgba(228, 244, 240, 0.55) 44%,
    rgba(228, 244, 240, 0) 74%
  );
}

/* 悬停：上浮 + 左侧组色指示条亮起 + 一层**暖色**光晕。
   第三项是 Day 13 加的：原先只有中性灰阴影，卡片浮起来了但没「亮」起来。
   补一层同色相的柔光后，悬停的观感从「被抬起来」变成「被点亮」——
   这正是用户在挑主题时想要的那一下：选中感。 */
.topic:hover {
  border-color: var(--border);
  border-left-color: var(--primary);
  box-shadow:
    var(--shadow-lg),
    0 16px 34px -22px rgba(217, 119, 6, 0.55);
  transform: translateY(-4px);
}

.topic-list.group-b .topic:hover {
  border-left-color: var(--teal);
  box-shadow:
    var(--shadow-lg),
    0 16px 34px -22px rgba(15, 122, 107, 0.5);
}

/* 按下时回落到 -1px 并轻微收紧，模拟「压下去」的触感。
   技能第五条要求 :active 必须有物理反馈，否则点击手感发空。 */
.topic:active {
  transform: translateY(-1px) scale(0.992);
  box-shadow: var(--shadow-md);
  transition-duration: 75ms;
}

.topic:hover::before {
  transform: scale(1.3);
  opacity: 1;
}

.topic > * {
  position: relative;
  z-index: 1;
}

.topic .no {
  grid-area: no;
  /* 等宽编号，与顶行右侧星标同处一行 */
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.08em;
  /* 原用 --accent（白底 3.35，12px 不达标）→ 改用组色的深档。
     组 A 蜜橘深档 6.40、组 B 深青 5.23，都远超 4.5。 */
  color: var(--primary-deep);
  margin-bottom: var(--space-3);
}

.topic-list.group-b .topic .no {
  color: var(--teal);
}

.topic .name {
  grid-area: name;
  font-size: var(--text-lg);
  font-weight: 600;
  line-height: 1.45;
  margin-bottom: var(--space-2);
  overflow-wrap: break-word;
}

/* 职场角色（Day 10 加练新增）：告诉用户这一轮扮演谁。
   刻意不做成 .tag 那样的胶囊底色 —— 它紧挨着主题名，再上一块底色会
   和名称抢注意力；改用「小一号 + 主色文字 + 左侧短竖线」轻量区分。 */
.topic .role {
  grid-area: role;
  font-size: var(--text-xs);
  color: var(--primary-dark);
  line-height: 1.5;
  margin-bottom: var(--space-3);
  padding-left: var(--space-3);
  /* 竖线是装饰，仍可用鲜色；文字用的是深档（白底 5.02） */
  border-left: 2px solid var(--primary);
}

.topic-list.group-b .topic .role {
  color: var(--teal);
  border-left-color: var(--teal);
}

/* 英文说明走衬线体 + 斜体，是卡片上最醒目的英文信息 */
.topic .summary {
  grid-area: sum;
  font-family: var(--font-en);
  font-size: var(--text-sm);
  font-style: italic;
  line-height: 1.65;
  color: var(--text-secondary);
  margin-bottom: var(--space-4);
  min-height: 46px;
  overflow-wrap: break-word;
  text-wrap: pretty;
}

.topic .meta {
  grid-area: meta;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;   /* 多一枚标记时允许换行，窄卡片不横向溢出 */
  font-size: var(--text-xs);
  color: var(--text-muted);
  align-self: end;
}

/* 星标按钮所在格：右下角，与左上角的 TOPIC N 成对角 */
.topic > .ic-wrap {
  grid-area: star;
  justify-self: end;
  align-self: end;
}

.tag {
  display: inline-block;
  padding: 2px var(--space-3);
  border-radius: var(--radius-full);
  background: var(--primary-soft);
  /* 原用 --primary-dark：蜜橘浅底上 4.47，差 0.03 不达标 → 换深档 5.70 */
  color: var(--primary-deep);
  font-size: var(--text-xs);
  font-weight: 500;
}

.topic-list.group-b .tag {
  background: var(--teal-soft);
  color: var(--teal);
}

/* ---------- 主题卡片上的「上次在这里卡过」标记（PRD.md §6.3） ---------- */

.topic .stuck {
  display: inline-block;
  padding: 2px var(--space-3);
  border-radius: var(--radius-full);
  background: var(--accent-soft);
  /* 原用 --accent：珊瑚浅底上 2.94，12px 严重不达标 → 换深档 5.60 */
  color: var(--accent-deep);
  font-size: var(--text-xs);
  font-weight: 500;
}

/* ============================================================
   对话页（Day 10 起）
   ============================================================ */

.dialogue-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  flex-wrap: wrap;
}

.dialogue-head .topic-name {
  font-size: var(--text-xl);
  font-weight: 600;
  line-height: 1.4;
  text-wrap: balance;
}

/* 对话页顶部的职场角色（Day 10 加练新增）。
   与主题卡上的 .topic .role 同一视觉语言（主色文字 + 左侧短竖线），
   但这里是通栏卡片内部，字号沿用 --text-sm 便于与主题名形成层级。 */
.dialogue-head .topic-role {
  margin-top: var(--space-2);
  font-size: var(--text-sm);
  color: var(--primary-dark);
  padding-left: var(--space-3);
  border-left: 2px solid var(--primary);
}

.counter {
  display: flex;
  gap: var(--space-5);
  font-size: var(--text-xs);
  color: var(--text-muted);
  text-align: right;
}

/* 时长数字单独占一行，放在「对话时长」标签下方。
   注意：这条只该命中时长那个 <b>。轮次里的 <b id="turn-count"> 处于
   「第 1 轮」短语中间，若也被 display:block 会拆成「第 / 1 / 轮」三行
   （Day 10 实测踩到）。所以轮次里的 b 加 .counter-inline 显式压回行内。 */
.counter b {
  display: block;
  font-size: var(--text-2xl);
  font-weight: 700;
  color: var(--accent);
  line-height: 1.3;
  font-variant-numeric: tabular-nums;
}

.counter b.counter-inline {
  display: inline;
  font-size: inherit;
  line-height: inherit;
}

/* 轮次短语不换行：「第 1 轮」必须整体成行 */
.counter-item {
  white-space: nowrap;
}

/* ============================================================
   对话舞台 + 光球（Day 13 重做）
   ------------------------------------------------------------
   原来这里是一个虚线框，写着「这里会显示你说的话和 AI 的回应」——
   它诚实地说明了「还没接上」，但代价是：用户点进对话页，
   看到的是整页里最没有生命的一块。而对话页恰恰是这个产品的主场。

   现在把它做成「舞台」：一块暗场，中央一枚会呼吸的光球 ——
   光球就是 AI 在这张桌子上的样子。它不该是头像、不该是「AI」两个字，
   因为用户对着一个名字说话是紧张的，对着一枚安静发光的东西说话才放松。
   这就是「陪伴感」的落点：屏幕上有个东西在，而且它在等你。

   三态与光球联动（由 dialogue.html 的脚本写 data-mood）：
     rest    未开始    慢呼吸（4.6s），光晕饱满，像在安静地等你开口
     listen  你在说    球收小、变亮、光晕内收（1.7s 快而浅）——
                       像人在认真听时会不自觉地屏住呼吸
     hold    已暂停    呼吸放到 6.4s、饱和度降到 0.5、轨道弧停转
                       像把灯调暗了一点，等你回来

   无障碍怎么处理（这条是刻意的）：
     **状态全部用「尺寸 / 亮度 / 饱和度」表达，动画只是加强**。
     所以 prefers-reduced-motion 把动画时长压到 0 之后，
     三个状态的静态外观依然互不相同 —— 不依赖动画也读得懂。
     光球本身 aria-hidden，状态由上面的文字（.orb-line）传达。
   ============================================================ */

.stage {
  position: relative;
  margin-top: var(--space-4);
  padding: var(--space-10) var(--space-6);
  border: 1px solid var(--night-edge);
  border-radius: var(--radius-xl);
  background: var(--night);
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--space-6);
  overflow: hidden;
  isolation: isolate;
  /* 阴影用舞台自己的深色，而不是通用的暖灰 ——
     这样它看起来是「屏幕上凹进去的一块」，
     而不是「一张卡片碰巧是黑的」。 */
  box-shadow:
    0 26px 50px -28px rgba(36, 26, 19, 0.65),
    0 2px 6px -2px rgba(36, 26, 19, 0.18);
}

/* 舞台里的一层暖光：从光球所在的位置（居中偏上）向外扩散，
   再在底部补一点珊瑚色的反射光。为什么要在背景上做：
   球体自身的 box-shadow 只能照亮它周围一小圈，
   真正让整个暗场「有光」的是这层底色渐变。 */
.stage::before {
  content: "";
  position: absolute;
  inset: 0;
  background:
    radial-gradient(58% 44% at 50% 30%, rgba(242, 160, 60, 0.17) 0%, rgba(242, 160, 60, 0) 72%),
    radial-gradient(120% 70% at 50% 118%, rgba(232, 98, 74, 0.13) 0%, rgba(232, 98, 74, 0) 70%);
  pointer-events: none;
  z-index: 0;
}

/* ---------- 光球本体 ----------
   四层从外到内：光晕 / 涟漪 / 轨道弧 / 核心。
   为什么分四层而不是一个元素堆 box-shadow：
   它们的运动节奏不同（光晕 4.6s、涟漪 7.4s、轨道 16s），
   分开才能各走各的周期，叠加起来才不会看出「在循环」。 */

.orb {
  position: relative;
  z-index: 1;
  /* Day 13 复核（看截图才发现）：初版上限 148px，1440px 下实测 149px ——
     暗场块有 1080px 宽、360px 高，一颗 149px 的球放在里面显得空，
     四周的光晕铺不满，像一张没拍满的照片。上限提到 176px 后，
     球与暗场的比例才对（手机端仍由下限 104px 兜住，见 640px 媒体查询）。
     这条纯靠量数字发现不了 —— 量出来的 149 完全「合法」，是构图问题。 */
  width: clamp(104px, 26vw, 176px);
  aspect-ratio: 1 / 1;
  flex: none;
  display: grid;
  place-items: center;

  /* 状态变量：一处定义、四层共用，切换状态时只改这几个数。
     默认值是 rest 态。 */
  --orb-scale: 1;
  --orb-bright: 1;
  --orb-sat: 1;
  --orb-halo: 1;
  --orb-speed: 4.6s;

  animation: orbBreathe var(--orb-speed) ease-in-out infinite;
}

/* 光晕：最外圈大面积暖色，跟着呼吸一起涨落。
   脉动感主要来自这一层 —— 核心的缩放幅度刻意只有 4.5%，
   挪动太多会像在弹跳，不像在呼吸。 */
.orb-halo {
  position: absolute;
  inset: -36%;
  border-radius: 50%;
  background: radial-gradient(
    circle,
    rgba(242, 160, 60, 0.45) 0%,
    rgba(242, 160, 60, 0.17) 40%,
    rgba(242, 160, 60, 0) 72%
  );
  opacity: var(--orb-halo);
  animation: orbHaloPulse calc(var(--orb-speed) * 1.15) ease-in-out infinite;
  transition: opacity 520ms var(--ease-soft);
  will-change: transform;
}

/* 涟漪：一圈从球面向外散开、扩散即淡出的细环。
   它是「这一刻有反应」的信号 —— 从开始说话到 AI 有回应，
   中间不必等文字，这圈环一直在替系统说「我在」。
   基础 opacity 是给 prefers-reduced-motion 用的：动画停掉之后，
   环仍以 0.4 的透明度留在原地，成了一道静态的光环，而不是消失或突变成实线。 */
.orb-ripple {
  position: absolute;
  inset: -8%;
  border-radius: 50%;
  border: 1px solid var(--orb-ring);
  opacity: 0.4;
  animation: orbRipple calc(var(--orb-speed) * 1.6) cubic-bezier(0.2, 0.6, 0.2, 1) infinite;
  will-change: transform, opacity;
}

/* 轨道弧：一圈不闭合的细光弧，16s 转一圈。
   用 conic-gradient 画出「一段弧」，再靠 mask 把圆心掏空成环。
   为什么用弧而不是整圈：整圈太像一个加载动画，弧才有「在运转」的活的观感。 */
.orb-orbit {
  position: absolute;
  inset: -16%;
  border-radius: 50%;
  background: conic-gradient(
    from 0deg,
    rgba(255, 196, 107, 0) 0deg,
    rgba(255, 196, 107, 0) 196deg,
    rgba(255, 196, 107, 0.5) 296deg,
    rgba(255, 196, 107, 0.92) 348deg,
    rgba(255, 196, 107, 0) 360deg
  );
  -webkit-mask: radial-gradient(closest-side, transparent calc(100% - 2px), #000 calc(100% - 2px));
  mask: radial-gradient(closest-side, transparent calc(100% - 2px), #000 calc(100% - 2px));
  opacity: 0.75;
  animation: orbSpin 16s linear infinite;
  transition: opacity 520ms var(--ease-soft);
}

/* 核心：一颗有体积的球，不是一个圆片。
   高光偏左上（34% 30%）、背光收在右下角的珊瑚 —— 与整站光源方向一致。
   box-shadow 的第四、五条是 inset：一条压暗右下（模拟球体自身的转折），
   一条提亮左上（模拟环境光又一次打在受光面），有这两条才有「球」的体积感。 */
.orb-core {
  position: relative;
  width: 72%;
  height: 72%;
  border-radius: 50%;
  background: radial-gradient(
    circle at 34% 30%,
    var(--orb-hi) 0%,
    var(--orb-core) 26%,
    var(--orb-warm) 60%,
    var(--orb-lo) 100%
  );
  box-shadow:
    0 0 22px var(--orb-glow),
    0 0 64px rgba(232, 98, 74, 0.32),
    inset -7px -9px 20px rgba(168, 58, 35, 0.3),
    inset 5px 7px 16px rgba(255, 232, 190, 0.42);
  transform: scale(var(--orb-scale));
  filter: brightness(var(--orb-bright)) saturate(var(--orb-sat));
  transition:
    transform 560ms var(--ease-soft),
    filter 560ms var(--ease-soft);
}

/* ---------- 三态 ---------- */

/* 你在说：球往回收、提亮，光晕和轨道一起暗下去 ——
   「安静下来听」。这是三个状态里最明显的一个变化。 */
.stage[data-mood="listen"] .orb {
  --orb-scale: 0.82;
  --orb-bright: 1.16;
  --orb-sat: 1.06;
  --orb-halo: 0.46;
  --orb-speed: 1.7s;
}

.stage[data-mood="listen"] .orb-orbit {
  opacity: 0.9;
  animation-duration: 6s;
}

/* 已暂停：把灯调暗，但不是关掉 —— 用户随时会回来，不该给他一块黑屏。
   Day 13 复核（看截图才发现，改了两轮）：
     初版 brightness(0.62) + saturate(0.5) → 球变成一颗灰白色的石头。
     原因有两层：① filter 作用于整个渲染结果，包括外圈那两道 box-shadow 发光，
     所以「压暗」把发光一起压没了，球就失去了光源的属性；
     ② 受光面本来就是很浅的 #FFE0AE，再乘 0.62 直接掉进灰区。
     第二版改成 0.76 / 0.78 —— 暖了一些，但**降饱和会把高光推向白**，
     球看着像个鸡蛋，「暖」还是没有回来。
     定版：亮度只压到 0.82，饱和度几乎不动（0.95）。
     结论是「歇一下」这件事不该由褪色来表达 —— 它交给另外三处：
     光晕收到 0.3、轨道弧停转、呼吸放到 6.4s。
     少用一层手段，状态反而更清楚（这也是色觉障碍用户更能读懂的版本）。 */
.stage[data-mood="hold"] .orb {
  --orb-scale: 0.94;
  --orb-bright: 0.82;
  --orb-sat: 0.95;
  --orb-halo: 0.3;
  --orb-speed: 6.4s;
}

.stage[data-mood="hold"] .orb-orbit {
  opacity: 0.22;
  animation-play-state: paused;
}

@keyframes orbBreathe {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.045); }
}

@keyframes orbHaloPulse {
  0%, 100% { transform: scale(0.9); }
  50%      { transform: scale(1.22); }
}

@keyframes orbRipple {
  0%   { transform: scale(0.74); opacity: 0.45; }
  100% { transform: scale(1.42); opacity: 0; }
}

@keyframes orbSpin {
  to { transform: rotate(360deg); }
}

/* ---------- 光球上方那行字 ----------
   它是这个产品里唯一一处「AI 直接对用户说话」。
   口径（对齐 PRD.md §4.1 H1「怕被评判」）：
   **只说陪伴和鼓励，不承诺功能、不评价用户**。
   所以是「我在这儿」「慢慢说」，不是「我已准备好识别你的语音」。 */
.orb-line {
  position: relative;
  z-index: 1;
  font-size: var(--text-lg);
  font-weight: 500;
  line-height: 1.6;
  color: var(--night-text);
  max-width: 26ch;
  text-wrap: balance;
}

/* 这里原来还有一条 .stage-note（写着「语音录音与 AI 回应尚未接入」）。
   Day 14 · F6 第 3 步做完对话记录区之后，那半句已经不成立 ——
   你说的话和 AI 的回应现在真的会出现在页面上。
   那句话没有被删掉，而是挪到记录区底部，换成了**现在仍然为真**的事实：
   AI 的回应来自本地文案池，不是大模型现场生成的。
   为什么坚持在页面上留一句实话：这块暗场会让人以为「功能已经全接好了」，
   对用户说实话比看起来完整更重要。 */

.actions {
  display: flex;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin-top: var(--space-4);
}

/* ---------- 语音状态区（Day 10 新增，P2 对话页） ----------
   三态由 [data-state] 驱动：idle / recording / paused。
   文案与显隐由 dialogue.js 的 VIEW 表统一给，这里只管长相。 */

.recorder {
  margin-top: var(--space-4);
  padding: var(--space-4) var(--space-5);
  border: 1.5px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface);
  display: flex;
  align-items: center;
  gap: var(--space-4);
  flex-wrap: wrap;
}

/* 左侧状态标签：底色随状态变，一眼能看出在不在录 */
.rec-label {
  flex: none;
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.02em;
  padding: var(--space-1) var(--space-3);
  border-radius: 999px;
  background: var(--surface-soft);
  color: var(--text-muted);
  border: 1px solid var(--border);
}

/* 录音中/暂停的标签是「白底反转」（浅底 + 深色文字），
   所以文字改用深档：原 --accent 压在 --accent-soft 上是 2.94，不达标。 */
.recorder[data-state="recording"] .rec-label {
  background: var(--accent-soft);
  color: var(--accent-deep);
  border-color: transparent;
}

.recorder[data-state="paused"] .rec-label {
  background: var(--primary-soft);
  color: var(--primary-deep);
  border-color: transparent;
}

/* 中间：脉冲点 + 状态文字 + 计时 */
.rec-live {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

/* 录音中的红点：呼吸式脉冲，暗示「正在听」。
   已在全站 prefers-reduced-motion 里统一关掉动效。 */
.rec-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--accent);
  flex: none;
}

.recorder[data-state="recording"] .rec-dot {
  animation: recPulse 1.4s ease-in-out infinite;
}

.recorder[data-state="paused"] .rec-dot {
  background: var(--primary);
  opacity: 0.5;
}

@keyframes recPulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50%      { opacity: 0.35; transform: scale(0.8); }
}

.rec-text {
  font-weight: 500;
  /* ★★ Day 25：锁定宽度 + 不换行（抖动修复的第 3 道保险）。
     这行字会在「等你开口……」（6 字）和「正在听……（说完停一下就好）」
     （14 字）之间切换，长度差一倍多。它和右边的 .rec-hint 同在
     .recorder 这一行 flex 里 —— 宽度一变，右边那行说明文字就被
     推来推去，看起来「一直在跳」。
     光靠 JS 侧「只在翻转时改」只能让跳变得稀疏，锁定宽度才能让它彻底不跳。
     15em 是按最长那条（14 字）留的余量。 */
  display: inline-block;
  min-width: 15em;
  white-space: nowrap;
}

/* 计时数字：等宽数字，跳秒时不会左右抖。
   Day 12：字号 18px → 20px。原因是可访问性 ——
   18px 粗体刚好差一点够不到 WCAG「大字号」门槛（需 ≥18.66px 粗体），
   于是必须满足小字的 4.5:1，而珊瑚色在这个尺寸只有 3.35。
   提到 20px 后按大字号算（需 3.0），珊瑚色 3.35 达标，
   同时计时数字在状态条里更醒目 —— 尺寸与合规一起解决。 */
.rec-timer {
  font-family: var(--font-mono);
  font-size: var(--text-xl);
  font-weight: 700;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}

.recorder[data-state="paused"] .rec-timer {
  color: var(--primary-dark);
}

.rec-hint {
  flex: 1 1 260px;
  min-width: 0;
  font-size: var(--text-xs);
  color: var(--text-muted);
  line-height: 1.6;
}

/* 窄屏：状态区改成竖排，标签与计时各占一行，避免挤成一条 */
@media (max-width: 640px) {
  .recorder {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-2);
  }

  .rec-hint {
    flex: none;
  }
}

/* ============================================================
   对话记录 + 提交发言（Day 14 · F6 第 3 步）
   ------------------------------------------------------------
   为什么现在才有：PRD §7.3 给 P2 列的必现信息里本来就有
   「用户转写区、AI 回应区」，但页面一直空着 —— Day 13 又把那个
   占位虚线框换成了光球舞台，记录区就更没地方放了。这一步补上，
   **两种模式共用同一套结构**（8 个职场主题与自由对话）。

   配色上做的一件事：**AI 的话放暗色气泡，用户的话放暖色气泡**。
   暗色气泡与那块舞台同族（--night-raised / --night-text），于是
   「在暗处发光的那颗球」与「它说的话」看起来来自同一个地方；
   用户的话用奶油族的暖浅底，与页面其余部分一家。
   实测对比度：AI 正文 12.74 · 用户正文 13.31 · 12px 署名 6.89 / 5.70。

   语言口径（PRD §6.6、TECH_DESIGN §5.2.1 约定 4）：
   **AI 的台词是英语，中文只出现在页面说明文字里**。
   所以降级说明「这一句用的是预设台词」走.tr-note，不做成气泡 ——
   不让用户以为这句话是 AI 用中文说的。
   ============================================================ */

.transcript {
  margin-top: var(--space-4);
  padding: var(--space-5);
  border: 1.5px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface);
}

.tr-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin-bottom: var(--space-4);
}

.tr-title {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--text-secondary);
}

/* 右侧那一小行计数走等宽：与「第 N 轮」「计时」一样是技术小标，
   三者同族，用户一眼能分出"这是数字，不是正文"。 */
.tr-count {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--text-muted);
}

/* 滚动区：对话长了不把整页顶开，输入框始终留在视线里。
   上限 340px ≈ 四五个来回，再长就该收起来了。
   右侧留一点内边距：气泡贴边时它的圆角不会被裁掉。 */
.tr-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  max-height: 340px;
  overflow-y: auto;
  padding-right: var(--space-1);
}

.tr-empty {
  font-size: var(--text-sm);
  line-height: 1.7;
  color: var(--text-muted);
}

/* ★ 2026-10-27：「对方在想」等待行。
   只在 /api/chat 在途时出现（dialogue.html 的 showThinking / hideThinking）。
   和 AI 台词**刻意不同形**：没有气泡、没有边框、不可点 ——
   它表达的是「还没有内容」，不是一句话；做成卡片会被误认成台词。
   省略号用 ::after 动出来，而不是写进文本，
   这样骨架屏式的呼吸感只花一条动画，也不用往 DOM 里塞三个点。 */
.tr-think {
  font-size: var(--text-sm);
  line-height: 1.7;
  color: var(--text-muted);
  padding: 2px 0 6px;
  align-self: flex-start;
}

.tr-think::after {
  content: '…';
  display: inline-block;
  animation: thinkPulse 1200ms var(--ease-soft) infinite;
}

@keyframes thinkPulse {
  0%, 100% { opacity: 0.25; }
  50%      { opacity: 1; }
}

.tr-row {
  display: flex;
  flex-direction: column;
  align-items: flex-end;        /* 用户的话靠右 */
  animation: bubbleIn 260ms var(--ease-soft) backwards;
}

.tr-row.is-ai {
  align-items: flex-start;      /* AI 的话靠左 —— 一来一往看得出方向 */
}

.tr-bubble {
  max-width: 78%;
  padding: var(--space-3) var(--space-4);
  border: 1px solid transparent;
  border-radius: var(--radius-lg);
  background: var(--primary-soft);
  color: var(--text);
}

.tr-row.is-ai .tr-bubble {
  background: var(--night-raised);
  border-color: var(--night-edge);
  color: var(--night-text);
}

.tr-who {
  display: block;
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.02em;
  margin-bottom: var(--space-1);
  color: var(--primary-deep);   /* 5.70 压在 primary-soft 上 */
}

.tr-row.is-ai .tr-who {
  color: var(--night-accent);   /* 6.89 压在 night-raised 上 */
}

/* Day 18 · 降级说明行：AI 那句改用预设台词时，紧跟其后的一行小字。
   ---------------------------------------------------------------
   为什么它是「说明」而不是「错误」：
     它表示的是**临时状态**（接口暂时读不到），不是出了什么错。
     所以刻意不做成警示色 —— 珊瑚色在本项目里专管偏题/卡点/失败
     （§视觉规范的三色分工），拿它表示「正在降级」会污染这条约定。

   为什么用小字而不是醒目提示：
     用户不需要为它做任何事，放太显眼只会打断练习节奏。
     但**必须写出来** —— 用户若把固定台词当成模型的即时回应，
     会误以为「AI 就这个水平」，那比报错更伤（诚实性要求，
     与记录页的 #rec-mock 同源）。

   对比度：text-secondary 压在页面暖奶油底上 5.31:1，达标（≥4.5）。 */
.tr-note {
  display: block;
  font-size: var(--text-xs);
  line-height: 1.6;
  color: var(--text-secondary);   /* 5.31:1 达标 */
  padding: var(--space-1) 0 var(--space-2);
  padding-left: var(--space-3);    /* 与气泡文字左缘对齐 */
  border-left: 2px solid var(--border-soft);   /* 令牌实际名（--border-subtle 不存在） */
}

.tr-text {
  font-size: var(--text-base);
  line-height: 1.75;
  /* 长英文单词 / 长串不撑破气泡 */
  word-break: break-word;
}

/* 英文台词走衬线斜体（全站规矩：--font-en 只用在装英文句子的地方）。
   加 .en 这个判据，是因为本期 8 个职场主题的追问句还是中文的
   （followUps 本来就是中文描述），中文落进 --font-en 会退到宋体 + 伪斜体，
   很难看。由脚本按「整行是否 ASCII」决定加不加 —— 将来 8 主题补上
   英文追问句，它会自动套上斜体，这里一个字不用改。

   Day 14 第 4 板块：**开口引号**。加它有两个理由，都不是装饰：
     ① 口语特色。AI 的台词是「说出来的」，不是「写出来的」——
        引号是「有人在说话」这个事实的视觉标记，斜体只是「这是外语」。
        只有斜体时，英文气泡和用户气泡长得太像，都在讲英语，
        加上引号才能一眼分清「这是对面那个人说的」。
     ② 台词库里本来就有口语的不完整句（"We're a bit behind."、
        "I see."），它们脱离对话被单独看时很像写作错误；
        引号给出「这是在模仿人说话」的框架，读起来就对了。
   用中文直角引号「」而不是英文 " "：AI 台词一律英语（PRD §6.6），
   但这个页面是中文界面，引号跟着界面语言走更统一；
   且直角引号在中文排版里不会被斜体的倾斜带歪。 */
.tr-text.en {
  font-family: var(--font-en);
  font-style: italic;
}

/* ★ Day 27：AI 台词下面那行中文（**只显示，不朗读**）。
   为什么用小字 + --text-muted：它是辅助，不是主体 ——
   用户先读英文（那才是练习目标），卡住了才往下扫一眼；
   一行中文不该和英文抢视线。
   为什么用 --text-xs 而不是 --text-sm：中文比英文短，同样的字号会显得更"重"，
   小一号才能维持"英文为主、中文为辅"的层次。
   ★ 对比度：--text-muted 在 --bg-tint / 白底上满足 4.5（与 .tr-who 同一档）。 */
.tr-zh {
  margin-top: 2px;
  font-size: var(--text-xs);
  line-height: 1.6;
  color: var(--text-muted);
}

/* ============================================================
   2026-10-26 · AI 台词翻卡（正面英文 / 背面中文）
   ------------------------------------------------------------
   为什么做成卡片而不是「点一下在下面展开中文」：
     中文一直挂着，人会先扫中文再读英文，这个产品就没意义了。
     藏起来才逼人先读英文，读不懂再主动翻。
   为什么背面还留着英文（压成小字）：
     纯替换等于读完中文就把原句弄丢了，而卡片的价值正是「对照」。

   ★ 三个实现要点，改之前先读：
     ① 覆盖全局 `button` 样式必须用 `.tr-row.is-ai button.tr-card` 这一串 ——
        全局的 `button:hover:not(:disabled)` 是 (0,2,1)，只写 `.tr-card` 压不住，
        表现是悬停时卡片被涂成 --surface-soft 浅色（深底上直接花掉）。
     ② 两面用 grid 叠在同一格里（grid-area: 1/1），容器尺寸自动取较高的那一面
        → 翻面时卡片的宽高都不变，按行布局不会抖。
        3D 转到 90° 时可视宽度趋近 0，宽度若由内容撑开，整段对话会左右跳；
        这正是套一层固定网格的原因。
     ③ perspective 放在 .tr-card（父）、preserve-3d 放在 .tr-flip，
        缺一个的话两个面会变成平铺拼接而不是翻转。
   ★ 动效时长 360ms；系统开启「减少动态效果」时由文件末尾的全局规则
     把 transition/animation 压到 0.01ms，翻面变成瞬时切换，无需另写一份。
   ============================================================ */
.tr-row.is-ai button.tr-card,
.tr-row.is-ai button.tr-card:hover:not(:disabled),
.tr-row.is-ai button.tr-card:active:not(:disabled) {
  position: relative;
  max-width: 78%;
  padding: 0;
  border: 0;
  background: none;
  transform: none;
  text-align: left;
  font-weight: 400;
  perspective: 900px;
  /* 全局 button 有 `transition: all`，留着会让悬停位移作用到按钮本身、
     与内部 3D 翻转叠加出重影。翻卡的所有过渡都在子元素上各自声明。 */
  transition: none;
  -webkit-tap-highlight-color: transparent;
}

/* 高光：翻面时扫一道弱光。用 background-position 移动而不是平移元素 ——
   平移会溢出圆角，而 .tr-flip 不能加 overflow:hidden（会压平 3D 上下文）。 */
.tr-card::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  border-radius: var(--radius-lg);
  background: linear-gradient(105deg, transparent 42%, rgba(255, 240, 220, 0.22) 50%, transparent 58%);
  background-size: 220% 100%;
  background-position: 130% 0;
  opacity: 0;
}

.tr-card.is-flipping::before {
  animation: cardSheen 380ms ease-out;
}

@keyframes cardSheen {
  0%   { opacity: 1; background-position: 130% 0; }
  100% { opacity: 0; background-position: -30% 0; }
}

.tr-flip {
  display: grid;
  position: relative;
  transform-style: preserve-3d;
  transition: transform 200ms var(--ease-soft);
}

/* 悬停整卡轻抬 2px：只做位移，不动尺寸 —— 位移不会影响 grid 的高度计算 */
.tr-card:hover .tr-flip {
  transform: translateY(-2px);
}

.tr-face {
  grid-area: 1 / 1;
  position: relative;
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--night-edge);
  border-radius: var(--radius-lg);
  background: var(--night-raised);
  color: var(--night-text);
  overflow: hidden;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  transition: transform 360ms var(--ease-soft), border-color 200ms var(--ease-soft);
}

.tr-front { transform: rotateY(0deg); }
.tr-back  { transform: rotateY(180deg); }

.tr-card[aria-pressed="true"] .tr-front { transform: rotateY(-180deg); }
.tr-card[aria-pressed="true"] .tr-back  { transform: rotateY(0deg); }

.tr-card:hover .tr-face {
  border-color: #6B5240;
}

/* 右下折角：静置暗棕暗示「可以翻」，悬停变蜜橘 */
.tr-corner {
  position: absolute;
  right: 0;
  bottom: 0;
  width: 16px;
  height: 16px;
  background: #6B5240;
  clip-path: polygon(100% 0, 100% 100%, 0 100%);
  transition: background-color 200ms var(--ease-soft);
}

.tr-card:hover .tr-corner {
  background: var(--primary);
}

/* 「点一下看中文」：右内边距留给折角，别让字压在三角上 */
.tr-afford {
  display: block;
  margin-top: var(--space-2);
  padding-right: 18px;
  font-size: var(--text-xs);
  line-height: 1.5;
  color: var(--night-muted);
  transition: color 200ms var(--ease-soft);
}

.tr-card:hover .tr-afford {
  color: var(--night-accent);
}

/* 背面：左侧蜜橘竖条 = 「这一面是中文」的物理暗示 */
.tr-back {
  padding-left: calc(var(--space-4) + 6px);
}

.tr-back::before {
  content: "";
  position: absolute;
  left: 7px;
  top: 14px;
  bottom: 14px;
  width: 3px;
  border-radius: 2px;
  background: var(--primary);
}

/* 背面顶上的小字英文：保留对照，但不许抢中文的视线 */
.tr-back-en {
  display: block;
  font-family: var(--font-en);
  font-style: italic;
  font-size: var(--text-xs);
  line-height: 1.6;
  color: var(--night-muted);
}

/* 背面中文：这里要**盖掉**上面 .tr-zh 的「附注小字」设定 ——
   同一行中文，在附注位置是配角，翻到背面它就是主角。 */
.tr-back .tr-zh {
  display: block;
  margin-top: var(--space-2);
  padding-right: 34px;          /* 让开右下角的 EN 键 */
  font-size: var(--text-base);
  line-height: 1.75;
  color: var(--night-text);
}

/* 缺译文（模型没按 {en,zh} 返回，或走了降级台词）。
   用次一级色 + 不加粗：它是说明，不是这句台词的内容。 */
.tr-back .tr-zh-missing {
  color: var(--night-2nd);
  font-size: var(--text-sm);
}

/* 右下 EN 回翻键：不是真按钮（整张卡都是按钮），只是提示还能翻回去 */
.tr-en-key {
  position: absolute;
  right: 12px;
  bottom: 10px;
  padding: 1px 5px;
  border: 1px solid var(--night-edge);
  border-radius: 5px;
  font-size: 11px;
  letter-spacing: 0.06em;
  color: var(--night-muted);
  transition: color 200ms var(--ease-soft), border-color 200ms var(--ease-soft);
}

.tr-card:hover .tr-en-key {
  color: var(--night-accent);
  border-color: var(--night-accent);
}

.tr-text.en::before,
.tr-text.en::after {
  /* 只在英文台词两侧各放一个，不做「连续引号」——
     引号太多就成了引号墙，反而更吵。 */
  color: var(--night-accent);
  opacity: 0.55;
  font-style: normal;
}
.tr-text.en::before { content: "\300C"; margin-right: 1px; }
.tr-text.en::after  { content: "\300D"; margin-left: 1px; }

/* 降级说明。**刻意不做成气泡**：它是页面在说实话，
   不是 AI 说的话（语言口径见本节开头）。
   ★ Day 19：这段样式原本还服务于一条常驻的静态说明
   「本轮 AI 的回应来自本地文案池（大模型接口尚未接入）」，
   那条已随/api/chat 打通而删除。现在只剩 noteDegraded()
   在真降级时动态追加的用法，所以 border-top 保留
   —— 它仍出现在记录区末尾，与上一句气泡分隔。 */
.tr-note {
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--border-soft);
  font-size: var(--text-xs);
  line-height: 1.7;
  color: var(--text-muted);
}

@keyframes bubbleIn {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}

/* ---------- 提交发言（本期用文本框代替开口，PRD §6.6 边界与约定）----------
   接入语音识别后，这个框的内容来源改成转写结果，结构不变
   （PRD §8.2 E3「转写文字允许用户编辑」就是同一个位置）。 */

.composer {
  margin-top: var(--space-4);
  display: flex;
  align-items: flex-end;
  gap: var(--space-3);
  /* 允许换行：排队提示要独占一行（下面用 flex-basis: 100% 强制） */
  flex-wrap: wrap;
}

/* 隐私告知（Day 23 第七次修订 · 语音改走腾讯云）。
   ★ 为什么用警示色：这不是错误，也不是警告，而是**用户有权知道的事实**——
     录音离开设备。用 --text-muted 的灰字会被当成脚注，
     而这件事必须被看见。
   ★ 但也不能用珊瑚（那是珊瑚=警示的族色，PRD 视觉规范里专管失败/偏题）——
     所以用 --primary-deep（蜜橘深档），它是「说明」而非「出事了」。 */
.privacy-note {
  margin: var(--space-3) 0 0;
  padding: var(--space-3) var(--space-4);
  font-size: var(--text-sm);
  line-height: 1.6;
  color: var(--primary-deep);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
}

.privacy-note b {
  font-weight: 500;      /* 全站只用400/500 两个字重 */
}

/* 音量条（Day 23 第七次修订）。
   ★ 为什么需要它：识别换成服务端之后，浏览器不再返回中间结果，
     所以「正在听：xxx」那种实时文字没有了。用户判断
     「麦克风到底有没有在收我的声音」只剩这一个依据 ——
     没有它，用户只能凭猜，而猜错一次就放弃了。

   为什么用 --primary（蜜橘）而不是绿色或蓝色：
     它是「A 组（会议上）」的族色，而这个录音条属于通用控件；
     用珊瑚会误读成警示（那是降级提示才用的）。 */
.rec-meter {
  /* ★★★ Day 25 修（「那块一直闪」的根因）：这是一条**轨道**，宽度必须固定。
     改之前 JS 每帧把它的 width 写成百分比（`el.recMeter.style.width = pct+'%'`）——
     于是它自己每帧伸缩，而 .recorder 是 flex-wrap 行，它的宽度一变，
     右边 .rec-hint 就被推来推去（音量再大点还会把整行顶得换行）。
     flex: 0 1 140px = 基准 140px、空间不够时可缩，但**绝不随音量变**。 */
  flex: 0 1 140px;
  min-width: 0;
  height: 6px;                     /* 细条：它只是辅助信息，不该抢戏 */
  margin-top: var(--space-2);
  border-radius: 3px;
  background: var(--border);
  overflow: hidden;
  position: relative;
}

/* 真正随音量变化的那一段。
   ★ 为什么用 ::after 而不直接改 .rec-meter 的 width：
     父元素要留住轨道底色（让用户看得出「总共多宽」），
     子元素只负责「现在到哪」。这是两种不同的信息，不能合并。
   ★★ Day 25：宽度改由自定义属性 --voice 驱动。
     自定义属性会**继承**，所以 JS 只要把 --voice 写在 .rec-meter 上，
     这个 ::after 就能读到 —— 这是「父元素定尺寸、子元素表进度」的正解。
     （改之前这里写死 width:0，而 JS 去改父元素自己的 width，
     两边错位 → 填充段从来没显示过，用户看到的是轨道自己在伸缩。） */
.rec-meter::after {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;               /* 用 inset 简写，不必写四条 */
  width: var(--voice, 0%);         /* 由 JS 实时改 --voice */
  background: var(--primary);
  border-radius: 3px;
  opacity: 0.25;                   /* 没声音时淡一点，别像坏了 */
  transition: width 80ms linear;   /* ★ 80ms：跟得上说话，又不抖 */
}

/* 语音提示行（Day 23）。
   为什么用 --primary-deep（蜜橘深档）而不是 --accent-deep：
   这条提示的性质是「现在需要你做一件事」（授权麦克风 / 没听清 / 换用打字），
   偏「指引」；而 .queue-note 说的是「你的话已经存下了」，
   偏「状态告知」。两者都 不是错误，用珊瑚（警示）都过重。
   ★ 但一旦降级为「用打字」，这件事是**需要用户改习惯**的，
     所以那一条由 JS 换成更长的说明文字，颜色仍保持可读（非错误态）。 */
.rec-note {
  margin: var(--space-2) 0 0;
  font-size: var(--text-sm);
  line-height: 1.6;
  color: var(--primary-deep);
}

/* 「跳过朗读」按钮（Day 23）。
   为什么不用 .link（那个样式是纯文字链）：
   它是一个**当场要按的动作**，需要看得见、点得准，
   文字链的点击区太小（且已达44px 触控下限附近）。
   但它也不该抢主操作「开始说话」的视觉重量 —— 用次级按钮样式。 */
#btn-skip-speech {
  min-height: 44px;              /* 触控目标下限（PRD §4.4） */
}
   为什么用 --accent-deep 而不是 --text-muted：
   它不是一句中性说明，是「你的话已经进去了、AI 正在回上一句」
   的**状态告知** —— 用 tertiary 的灰字会被当成不重要的注释，
   而这恰好是同伴反馈里最缺的那点确定性（「我说了，它收到了吗」）。
   珊瑚族是警示色，这里不是警示，所以走 accent 档而不是 coral。 */
/* 排队提示（Day 23）。
   为什么用 --accent-deep 而不是 --text-muted：
   它不是一句中性说明，是「你的话已经进去了、AI 正在回上一句」
   的**状态告知** —— 用 tertiary 的灰字会被当成不重要的注释，
   而这恰好是同伴反馈里最缺的那点确定性（「我说了，它收到了吗」）。
   珊瑚族是警示色，这里不是警示，所以走 accent 档而不是 coral。 */
.queue-note {
  flex: 0 0 100%;          /* 独占一行，不挤到输入框旁边 */
  margin: 0;
  font-size: var(--text-sm);
  line-height: 1.6;
  color: var(--accent-deep);
}

.say-input {
  flex: 1 1 auto;
  min-width: 0;
  font: inherit;
  font-size: var(--text-base);
  line-height: 1.7;
  padding: var(--space-3) var(--space-4);
  border: 1.5px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface);
  color: var(--text);
  /* 允许用户自己拉高：说长一点时能一眼看到整句，不必在小框里翻 */
  resize: vertical;
  min-height: 58px;
  max-height: 180px;
  transition: border-color var(--fast) var(--ease-out),
              box-shadow var(--fast) var(--ease-out);
}

.say-input::placeholder {
  color: var(--text-muted);
}

/* 与 .field input:focus 同一套「边框变色 + 柔光环」。
   这里不写 outline: none —— 键盘焦点圈由全站 :focus-visible 统一给。 */
.say-input:focus {
  border-color: var(--primary);
  box-shadow: 0 0 0 3px rgba(217, 119, 6, 0.13);
}

@media (max-width: 640px) {
  /* 窄屏：输入框与按钮竖排，按钮铺满一行 —— 它是这一态的主操作 */
  .composer {
    flex-direction: column;
    align-items: stretch;
  }

  /* 气泡占宽放宽：窄屏上再留 22% 空白，一句英文要折成三行 */
  .tr-bubble {
    max-width: 88%;
  }

  /* ★ 翻卡也必须跟着放宽：上面那条卡片规则是 `.tr-row.is-ai button.tr-card`
     （0,3,1），比这里的 `.tr-bubble`（0,1,0）高 —— 不显式覆盖的话，
     手机上卡片仍卡在 78%，比用户气泡窄一截。 */
  .tr-row.is-ai button.tr-card,
  .tr-row.is-ai button.tr-card:hover:not(:disabled),
  .tr-row.is-ai button.tr-card:active:not(:disabled) {
    max-width: 88%;
  }
}

/* ============================================================
   按钮
   ============================================================ */

button {
  font: inherit;
  font-size: var(--text-base);
  font-weight: 500;
  padding: var(--space-3) var(--space-6);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface);
  color: var(--text);
  cursor: pointer;
  transition: all var(--fast) var(--ease-out);
}

button:hover:not(:disabled) {
  border-color: var(--primary);
  background: var(--surface-soft);
  transform: translateY(-1px);
}

button:active:not(:disabled) {
  transform: translateY(0) scale(0.98);
}

button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* 主按钮：蜜橘→红橘渐变，白字。
   Day 12 换端点：原 #D97706 → #E8624A 上压白字只有 3.19~3.35，
   16px 正文级文字不达标；换成 #B45309 → #C24A2E 后两端为 5.02 / 4.87，
   均达标，色相仍是暖橘，观感只是深了一档。

   Day 13：补一层**染色投影** + 加宽左右内边距。
   为什么投影要染色（不用中性灰）：按钮在奶油底上，「按下去会发光」这一点
   只靠渐变是做不到的；投影染成按钮自己的深色后，
   它在视觉上就成了一个「亮着的物件」，而不是一块贴纸。
   加宽内边距是为了拉开主次 —— 一个页面只该有一个主按钮，
   那就必须让人一眼看出哪个是主。 */
button.primary {
  /* 同样是「兜底底色 + 渐变」两条写。原因见 .ic-btn.is-fav 的注释：
     微信内置浏览器不渲染「渐变 + var()」的 background 简写时，
     整条 background 作废会回退到白底，而白字仍生效 → 文字消失。
     主按钮是全站都在用的，漏了它比收藏按钮更严重。 */
  background-color: var(--grad-from);
  background-image: linear-gradient(135deg, var(--grad-from) 0%, var(--grad-to) 100%);
  border-color: transparent;
  color: #FFFFFF;
  font-weight: 600;
  padding: var(--space-3) var(--space-8);
  box-shadow:
    0 7px 18px -9px rgba(168, 58, 35, 0.75),
    var(--shadow-sm);
}

button.primary:hover:not(:disabled) {
  background-color: var(--grad-from-hover);
  background-image: linear-gradient(135deg, var(--grad-from-hover) 0%, var(--grad-to-hover) 100%);
  box-shadow:
    0 12px 26px -9px rgba(168, 58, 35, 0.8),
    var(--shadow-md);
}

button.link {
  border: none;
  background: none;
  color: var(--primary-dark);
  padding: var(--space-3) 0;
  font-weight: 600;
}

button.link:hover:not(:disabled) {
  background: none;
  color: var(--accent-deep);
  transform: none;
}

/* ============================================================
   结果页
   ============================================================ */

.field-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: var(--space-3);
}

.field-grid .cell {
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-lg);
  padding: var(--space-4);
  background: var(--surface-soft);
  transition: border-color var(--fast) var(--ease-out);
}

.field-grid .cell:hover {
  border-color: var(--primary);
}

.field-grid .label {
  font-size: var(--text-xs);
  color: var(--text-muted);
  margin-bottom: var(--space-1);
}

.field-grid .value {
  font-size: var(--text-2xl);
  font-weight: 700;
  color: var(--text);
  line-height: 1.3;
  font-variant-numeric: tabular-nums;
}

/* 第二个字段（对话时长）用暖色，避免四个数字一样重。
   24px 粗体属 WCAG 大字号（≥18.66px 粗体），阈值 3.0，珊瑚色 3.35 达标。 */
.field-grid .cell:nth-child(2) .value {
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}

/* ============================================================
   记录页 / 空状态
   ============================================================ */

.empty {
  border: 1.5px dashed var(--border);
  border-radius: var(--radius-xl);
  padding: var(--space-12) var(--space-6);
  text-align: center;
  background: var(--surface-soft);
}

.empty .big {
  font-size: var(--text-lg);
  font-weight: 600;
  color: var(--text);
  margin-bottom: var(--space-2);
  text-wrap: balance;
}

.empty .small {
  font-size: var(--text-sm);
  color: var(--text-muted);
  line-height: 1.8;
  max-width: 52ch;      /* 空态说明居中对齐，行宽必须收窄，否则读起来要横扫 */
  margin-inline: auto;
  text-wrap: pretty;
}

/* 实线框版本的空态（Day 9 新增）。
   出处：result.html 两处行内 `border-style:solid`。
   .empty 默认是虚线框（表示「这条还没接上」），但结果页里的两个空态是
   「判断跑完了、结果就是空」—— 那是确定的结论，用实线才符合语义
   （口径见 PRD.md §9.2 B10）。 */
.empty-solid {
  border-style: solid;
}

/* ---------- 页脚 ---------- */

footer.page-foot {
  margin-top: var(--space-12);
  padding-top: var(--space-4);
  border-top: 1px solid var(--border);
  font-size: var(--text-xs);
  color: var(--text-muted);
}

/* ============================================================
   页面主布局（区块 1 / 2 / 3）
   ------------------------------------------------------------
   Day 12 改动：断点由 900px 降到 740px。
   原先「平板区间 640~900 两不靠」是记在待决项里的问题：
   .layout 本身在两种宽度下都是单列（900px 那条只改了 gap），
   真正只在 900px 以上生效的只有「区块 1 内部拆两列」，
   于是 640~900 这段区间里，区块 1 是通栏宽卡却把内容全挤在左边，
   右半边一大片空白。降到 740px 后，只有 641~739 这一小段是堆叠态，
   且这两个宽度本身就是手机横放的大小，堆叠本来就对。
   ============================================================ */

.layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-5);
}

/* 每一栏都允许被压缩，防止内容把栅格撑宽、在手机上出现横向滚动 */
.layout > * {
  min-width: 0;
}

.layout .card {
  margin-bottom: 0;                        /* 栏内间距交给 gap 管 */
}

/* 分组容器只用于归类、不生成盒子：四个区块直接参与外层栅格，
   顺序由 order 排成 1 → 2 → 3 → 4，和区块小标上的编号一致。
   （注意：order 影响视觉顺序，所以区块编号必须与视觉顺序一致 ——
     改顺序就等于要改编号，那属于改产品信息，不是改版式。
     Day 14 把自由对话提到 02 时，01 / 03 / 04 的编号就是跟着这张表一起改的。） */
.col-side {
  display: contents;
}

.blk-identity { order: 1; }
.blk-free     { order: 2; }   /* 自由对话：Day 14 由「列表末尾」提到「主题之上」，见 §自由对话入口 */
.blk-topics   { order: 3; }
.blk-overview { order: 4; }

/* ---------- 区块小标 ----------
   Day 14 第 3 板块改版：原为「12px 等宽小字 + 珊瑚深档」，在 390px 手机上
   一行灰字扫过去根本看不见，实测反馈是「点进去不知道该做些什么」。
   改成**实心徽章**：底色块 + 深档字 + 700 字重。
   为什么不用「把字号堆大」解决：
     ① .topbar h1 的注释里已定「上限 38px，再往上就是营销页的喊话体」——
        同理把小标推到 20px 就成了三块 shouting，谁都不再是重点，等于没重点；
     ② 真正的信息是「这块是干什么的」，不是「这块编号多大多鲜艳」。
   形状上选**圆角实心块**而不是下划线/箭头：块有面积，在手机上一眼能扫到；
   描边式（只有边线）在奶油底上对比不足，正是原来看不清的原因之一。
   字号 12 → 14px 是配套：块变醒目后，原字号在块里会显得空且局促。

   Day 14 第 4 板块：**字体从等宽改为正文字体**。
   这是全站最重的一处 AI 味来源——等宽字体（Cascadia Mono）带的是
   「代码 / 终端 / 开发者工具」语义，而这是个给英语学习者用的对话工具。
   一屏七处等宽（TOPIC 编号、轮次、计时、小标…）会把整页带成开发者面板。
   改法不是「删掉等宽」：下面 .topic .no（TOPIC 1）、.rec-timer（计时）、
   .tr-count（第 N 轮）仍保留等宽——**数字要对齐，那里等宽是对的**。
   只把「给人读的短句」换回正文字体，靠字重与底色区分层级。 */
.eyebrow {
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  font-weight: 700;
  /* 字距 0.1em → 0.04em：字号变大后 0.1em 会拉得太散，
     「03」和「选一个主题」之间出现明显断裂，读起来不像一个整体 */
  letter-spacing: 0.04em;
  /* 鲜色只做装饰，小字一律用 -deep 档（项目硬规矩）：
     --primary-deep 9A4708 压 --primary-soft 底 = 6.40:1，远超 4.5 */
  color: var(--primary-deep);
  /* 徽章底色用浅一档的蜜橘族，和卡片白底拉出层次但不抢戏 */
  background: var(--primary-soft);
  /* width: fit-content 是必须的：01 那块是通栏卡片，.eyebrow 是它的直接子元素，
     不加这一条 inline-block 会被父级的块级布局拉满整行宽，
     徽章就变成一条横幅 —— 与 02/03 的小块形态不一致，看着像另一类东西。 */
  width: fit-content;
  display: block;
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-sm);
  /* 徽章比原来高 9px，间距相应收 4px，净增 5px，不至于把下面的内容推远 */
  margin-bottom: var(--space-2);
  border-left: 3px solid var(--primary);
}

/* 区块 4 走深青：与它的三条统计同一色相，区块内不超过两种颜色 */
.blk-overview .eyebrow {
  color: var(--teal);
  background: var(--teal-soft);
  border-left-color: var(--teal);
}

/* ---------- 区块 01｜称呼区：收起说明行（Day 14，与自由对话那一节对调主次） ----------
   用户要求把「自由对话」与「称呼区」的视觉占比对调。两块都是通栏，
   所以「面积」的差别 100% 是高度（1440px 实测 176 vs 129）—— 对调即换高度。
   撑高称呼区的是输入框那一列（实测 103px：标签 25 + 间距 8 + 输入框 54 + 上间距 16），
   它一个字不能动（那是用户真正要打字的地方）；能省的只有这句说明行（实测 24px 桌面 / 48px 手机）。
   只收这一行就够了，内边距不用再动 —— 实测收起后 1440px → 151、390px → 215，
   正落在自由对话那一节对调前的两个高度（176 / 263）上；再收内边距反而会偏。
   文案仍留在 HTML 里（只是不显示），要放回来删掉下面这一条即可。 */
.blk-identity > .hint {
  display: none;
}

/* 区块 2 是主内容，自己不是卡片，头部单独排 */
.blk-topics > .blk-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin-bottom: var(--space-5);
}

/* 区块 2 的标题是页面上第二重的文字（次于页头 h1）。
   Day 12：20px → 24px。原来它和卡片标题（18px）只差 2px，
   整页看起来「所有标题差不多大」；拉开到 24px 后，
   视线能立刻分出「区块 → 卡片」两级。 */
.blk-topics > .blk-head h2 {
  font-size: var(--text-2xl);
  font-weight: 700;
  line-height: 1.35;
  letter-spacing: -0.01em;
  text-wrap: balance;
}

.blk-topics .blk-note {
  font-size: var(--text-xs);
  color: var(--text-muted);
}

/* 区块 2 里的第一个分组标题不要再顶出一段间距 */
.topics-body > .section-title:first-child {
  margin-top: 0;
}

/* ============================================================
   区块 02｜自由对话入口（Day 14 · F6）
   ------------------------------------------------------------
   它不是「第 9 个主题」，是一个**去处** —— 所以形状与主题卡故意不同：
   一条暗场横条，配色与对话页那块舞台同一套（--night 一族），
   左侧一颗缩小版光球（同一个 .orb）。用户点进去看到的，
   就是这颗在等他的球。形状不同这件事本身也在说：
   这里不是「再练一个场景」，是「换个方式说说话」。

   为什么用暗色：这页其余部分全是奶油底，一条暗横条天然读作「另一个地方」；
   而且光球在亮底上几乎看不见 —— 它的光本来是为暗处设计的（对话页那条理由）。

   Day 14 修订（用户拍板）：位置由「8 个主题之后的列表末尾」提到
   「8 个主题之上」，编号 02，自成一节（.blk-free）。
   这条假设的版式落点（原定在 8 个主题之后）已随本次修订，
   见 PRD.md §4.1 H4「H4 在版式上的落点」。

   随之作废的两件事（原来那条 .free-entry 规则就是为它们写的，现已删除）：
     ① 列表末尾那道发丝分隔线 —— 它不再是列表里的一项，没有「上面一组」可分隔；
     ② 入场错峰等到第 6 拍 —— 它现在排在第一张主题卡之前，改 1 拍。

   这一节自己不画盒子：等宽小标与横条直接落在奶油底上，与区块 03「选一个主题」
   同构（那节也是「小标在外面 + 内容在下」，不套卡片）。小标到横条的距离沿用
   全局 .eyebrow 的 12px，与 01 / 04 一致，不为它单开一条规则。

   高度（Day 14 修订）：这一节连同小标要做到约 176px（1440px 实测），
   正好接住称呼区对调前的高度 —— 两块的主次由此对调。
   横条本身原有 129px，加一个小标（21 + 12）是 162，还差 14，
   所以下面 .free-link 的上下内边距由 20px 提到 28px。
   实测结果见交付说明；这不是「看着差不多」，是两处高度都量过。
   ============================================================ */

/* Day 14 第 5 板块：横条由**暗底改浅底**（用户要求「从深色改浅一些，可以突出，
   但不要过于突出，不然用户注意不到其他板块」）。

   为什么原来那么深不叫「突出」而叫「抢戏」：#241A13 的明度只有 8%，
   而整页底色 #FDFBF6 是 99% —— 两者对比如同一张纸上贴了块黑胶布。
   视线会被它先吸走，再也回不到「选一个主题」那8 张卡上。
   统计口径上，这页三个区块的「注意力预算」应该按内容重要性分，
   而 8 个主题才是产品主体，自由对话是**出口**，不该占最重的视觉块。

   改成浅底后怎么还能「突出」——靠三处，不是靠色深：
     ① 一道 4px 的蜜橘左边框：颜色最饱和处只有这一条线，视线扫过去会被它引一下；
     ② 底色用 --primary-soft（比白卡片深一档），它仍是全页唯一「有色」的整块；
     ③ 光球那颗深色底座：整条横条里唯一的深色，焦点被收窄到球上（见 .free-orb）。
   三处叠加的注意力总量 ≈ 原来一块黑胶布的 1/3，但「看得出这里是个入口」保住了。 */
.free-link {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--space-5);
  /* 上下 28px（原 20px）：这一节现在是页面上的区块 02，要与称呼区对调主次，
     光靠一个小标补不上 14px 的差额，所以这里放开一点。算式写在上面那段说明里。 */
  padding: 28px var(--space-6);
  border: 1px solid var(--border);
  /* 左边框 4px：与 .eyebrow 同一根蜜橘色，是全站唯一「饱和色只出现在细线上」的地方。
     之所以不用珊瑚：珊瑚是警示色专管（偏题、卡点、失败），这里不是警示。 */
  border-left: 4px solid var(--primary);
  border-radius: var(--radius-xl);
  /* 浅底：比 .card 的纯白深一档，于是它仍是这一页唯一「有色」的整块，
     但色相与 8 张主题卡的白底同属暖奶油族，不会在页面上割出一块异色。 */
  background: var(--primary-soft);
  color: var(--text);
  text-decoration: none;
  overflow: hidden;
  isolation: isolate;
  /* 阴影从「0 20px 38px -24px rgba(36,26,19,.62)」大幅调浅：
     原来的浓阴影是暗底带来的「悬浮」错觉，底色变浅后还留着重影会显得脏。 */
  box-shadow:
    0 10px 20px -14px rgba(107, 84, 56, 0.34),
    0 2px 5px -2px rgba(107, 84, 56, 0.1);
  transition:
    transform var(--normal) var(--ease-soft),
    box-shadow var(--normal) var(--ease-out);

  /* 入场错峰：原来排在第 6 拍（它在 8 张主题卡之后），搬成区块 02 后改第 1 拍 ——
     顺序即层级，它现在比第一张主题卡还早落位。落位方式与主题卡同一套 cardRise。 */
  animation: cardRise 420ms var(--ease-soft) backwards;
  animation-delay: calc(1 * var(--stagger));
}

/* 光球那一侧铺一层暖光（浅底之后强度要一起降）：
   原来是 rgba(242,160,60,.22) 打在一块近黑上，才显得有光源。
   底色变浅后同样的强度会变成「一块发黄的白斑」，反而脏。
   降到 .10 并把扩散收窄，只留下极淡的一层暖意。 */
.free-link::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background: radial-gradient(
    38% 120% at 5% 50%,
    rgba(242, 160, 60, 0.1) 0%,
    rgba(242, 160, 60, 0) 68%
  );
}

/* 内容压在光之上（与 .stage 内部的关系一致） */
.free-link > * {
  position: relative;
  z-index: 1;
}

/* 悬停沿用主题卡那套「被点亮」而不是「被抬起来」：
   一小段上浮 + 外圈一圈极淡的暖色描边。
   Day 14 第 5 板块：阴影 rgba(36,26,19,.7) 与外圈 rgba(255,196,107,.16)
   都是按暗底调的 —— 底色变浅后，浓黑阴影会在横条下方拖一块脏影，
   亮米色描边则等于没有描边。改成与 .card:hover 同族的蜜橘描边。 */
.free-link:hover {
  transform: translateY(-3px);
  box-shadow:
    0 14px 26px -14px rgba(107, 84, 56, 0.4),
    0 0 0 1px rgba(217, 119, 6, 0.24);
}

.free-link:active {
  transform: translateY(-1px);
}

/* ---------- 缩小版光球 ----------
   与对话页那颗是同一个组件、同样四层，只改尺寸。
   core 的 box-shadow 是绝对 px，不按比例收小就会糊成一团光晕，
   所以这里把三层发光与两条 inset 一并收窄。

   尺寸：60px 写在 .orb.free-orb 那一层（不是 .free-orb），
   保持「缩放版」的身份；下面 .free-orb 只负责底座与居中。 */
.orb.free-orb {
  width: 60px;
}

.free-orb .orb-halo  { inset: -34%; }
.free-orb .orb-ripple { inset: -10%; }
.free-orb .orb-orbit { inset: -18%; }

.free-orb .orb-core {
  box-shadow:
    0 0 10px var(--orb-glow),
    0 0 26px rgba(232, 98, 74, 0.3),
    inset -3px -4px 9px rgba(168, 58, 35, 0.3),
    inset 2px 3px 7px rgba(255, 232, 190, 0.42);
}

/* Day 14 第 5 板块：横条由暗底改浅底后，**光球必须有自己的一块深色底座**。
   球体的光（--orb-hi / --orb-core / --orb-glow）是为暗处设计的，
   直接放到奶油底上会淡成一小团黄，看不出是颗球 —— 而这颗球是
   「自由对话」整块的形象落点（它与对话页中央那颗是同一个组件，
   用户靠它认出「这是同一套东西」）。
   所以不是「把球调亮」，而是给它一个深色圆底：
   深底 + 亮球 = 球还是球，且那块深色比整条暗底小得多，
   重量落在「一个焦点」上而不是「一整块黑」。

   ⚠️ 这里**不能写height**（第一版踩了，写了 height:60px）：
   基础 .orb 靠 `aspect-ratio: 1/1` 决定高度（main.css .orb 段），
   一旦给它写死height，宽高比就脱离 1:1；
   再叠加 flex 压缩后实测成49×62 的**扁椭圆**（球看着歪了）。
   正确做法：只给背景与阴影，尺寸继续由 .orb 的 aspect-ratio 管，
   窄屏由下面媒体查询里的 48px 统一收小。 */
.free-orb {
  position: relative;
  flex: none;
  /* 底座比球芯大一圈：球芯是 .orb-core 的 72%，剩下 28% 是留给光晕的边。
     第一版底座与球芯同宽（100% vs 72%），光晕溢出时会被底座边缘切断，
     球看着像「顶到井壁」；加宽到 116% 后四周留出一圈余量，
     光晕能散开，圆和底座的关系变成「球放在碟子里」而不是「球卡在洞里」。 */
  width: 116%;
  border-radius: var(--radius-full);
  background: radial-gradient(
    circle at 50% 42%,
    #4A3524 0%,      /* 比 --night 亮一档，是「有球的洞」而不是「一个洞」 */
    #2C1F16 58%,
    #241A13 100%
  );
  box-shadow:
    0 0 0 1px rgba(62, 45, 32, 0.9),
    0 3px 10px -3px rgba(36, 26, 19, 0.5);
  /* 球芯在底座里居中：球芯是绝对定位的 72%，不给绝对定位基准会贴左上角 */
  display: flex;
  align-items: center;
  justify-content: center;
}
/* 四层球体要压在底座之上且居中：
   它们是 position:absolute 的兄弟节点，不给z-index 会被后绘制的底座盖住。 */
.free-orb .orb-core,
.free-orb .orb-halo,
.free-orb .orb-ripple,
.free-orb .orb-orbit {
  z-index: 1;
}

/* ---------- 文字 ----------
   Day 14 第 5 板块：横条改浅底后，卡内文字**全部从暗场令牌换回亮场令牌**。
   这是这次改动里最不能漏的一步 —— 若只改背景不改字色，
   --night-text（#F5EDE2 浅米色）压在 --primary-soft（#FDF0DC 浅蜜橘）上，
   对比度约 1.1:1，整条横条会变成「白底白字」，比原来更糟。
   取值与深档都写在括号里，按WCAG 4.5（小字）/ 3.0（大字）核过。 */
.free-body {
  /* flex-basis 用 0 而不是 auto：auto 会拿「整句话不折行」的宽度去要空间，
     窄屏上光球就被挤到单独一行，横条凭空高出一大截（390px 实测 275px → 修后约 200px）。
     用 0 起步让它只吃「剩下的空间」，光球与文字始终在同一行，
     真正需要换行的只有右侧的时长与按钮（它自带 flex-basis:100%）。 */
  flex: 1 1 0;
  min-width: 0;
}

.free-kicker {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.1em;
  color: var(--primary-deep);   /* 5.70 压 primary-soft（原 --night-accent 只在暗底达标） */
  margin-bottom: var(--space-2);
}

.free-name {
  display: block;
  font-size: var(--text-xl);
  font-weight: 700;
  line-height: 1.3;
  color: var(--text);            /* 14.4:1，最深的一档，标题必须最重 */
  margin-bottom: var(--space-2);
}

.free-sum {
  display: block;
  font-size: var(--text-sm);
  line-height: 1.7;
  color: var(--text-secondary);   /* 5.35 压 primary-soft，14px 达标 */
  /* 不设 max-width：这句只有 33 个字，被限宽（试过 44ch）会折成
     「…AI 会接着 / 聊，不打断、不评判。」—— 第二行只剩孤零零一截。
     整句落在一行时横条更矮，也就不那么抢主题卡的眼。
     text-wrap: pretty 兜住将来文案变长的情况：宁可让上一行多装一点，
     也不要在末尾留一个孤字。 */
  text-wrap: pretty;
}

/* 右侧：时长 + 行动。竖排、右对齐，与横条左端的球形成两端锚点。 */
.free-meta {
  flex: none;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--space-3);
}

.free-dur {
  font-size: var(--text-xs);
  color: var(--text-secondary);    /* 5.35 压 primary-soft，12px 也达标 */
}

.free-go {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--primary-deep);      /* 5.70 压 primary-soft */
  /* 描边由「亮米色半透明」改为「蜜橘实色」：浅底上 rgba(255,196,107,.36)
     几乎看不见（亮色描边压浅底 = 没有描边），按钮会像个没边界的字串。 */
  border: 1px solid rgba(154, 71, 8, 0.42);
  border-radius: var(--radius-full);
  padding: 8px 18px;
  white-space: nowrap;
  transition:
    background var(--fast) var(--ease-out),
    border-color var(--fast) var(--ease-out);
}

/* 悬停时整条横条亮起来的同时，把「开始聊」也点亮一档 ——
   它是这条横条唯一可点的动作，值得跟着一起响应。 */
.free-link:hover .free-go {
  background: rgba(217, 119, 6, 0.14);
  border-color: var(--primary);
}

/* ============================================================
   四种状态：加载中 / 成功 / 空 / 错误
   ------------------------------------------------------------
   技能第五条要求三态都要「被认真设计过」。这一页本来就有，
   Day 12 只做对齐：骨架屏尺寸与新卡片结构对齐（顶行小条 + 两行正文 + 底部一条），
   这样从加载切到成功时画面不跳。
   ============================================================ */

.skeleton-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(290px, 1fr));
  gap: var(--space-4);
}

.skeleton-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: var(--space-6);
  /* 与 .topic 的最小高度对齐，切换时不产生跳动 */
  min-height: 210px;
}

.skeleton-card .bar {
  height: 12px;
  border-radius: var(--radius-full);
  background: linear-gradient(
    90deg,
    var(--border-soft) 0%,
    var(--bg-tint) 50%,
    var(--border-soft) 100%
  );
  background-size: 200% 100%;
  animation: shimmer 1.4s linear infinite;
}

.skeleton-card .bar + .bar {
  margin-top: var(--space-3);
}

/* 第一条是「TOPIC N」那一行，短一点更像真实卡片的节奏 */
.skeleton-card .bar:first-child {
  height: 9px;
  margin-bottom: var(--space-5);
}

.skeleton-card .bar.w40 { width: 40%; }
.skeleton-card .bar.w70 { width: 70%; }
.skeleton-card .bar.w100 { width: 100%; }

@keyframes shimmer {
  from { background-position: 200% 0; }
  to   { background-position: -200% 0; }
}

.state-hint,
.mock-note {
  margin-top: var(--space-4);
  font-size: var(--text-sm);
  color: var(--text-muted);
  max-width: 62ch;
}

/* 错误态：复用 .empty 的框，只换强调色；不给报错堆栈（TECH_DESIGN.md §8.2 E9） */
.state-error {
  border-style: solid;
  border-color: var(--accent);
  background: var(--accent-soft);
}

/* 原用 --accent：珊瑚浅底上 3.17，18px 半粗不达大字号门槛 → 换深档 5.60 */
.state-error .big {
  color: var(--accent-deep);
}

/* ---------- 区块 3：卡点概览 ---------- */

.stat-list {
  list-style: none;
  margin-top: var(--space-4);
}

.stat-list li {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--border-soft);
}

.stat-list li:last-child {
  border-bottom: none;
  padding-bottom: var(--space-2);
}

.stat-list .k {
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

.stat-list .v {
  font-size: var(--text-xl);
  font-weight: 700;
  line-height: 1.2;
  color: var(--text);
  text-align: right;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}

/* 第二个数字（累计问题条目）用暖色，避免三个数字一样重。
   20px 粗体属大字号，阈值 3.0，珊瑚 3.35 达标。 */
.stat-list li:nth-child(2) .v {
  color: var(--accent);
}

/* 数值下面的小字拆分（偏题 · 逻辑错误），口径见 PRD.md §8.3 */
.stat-list .sub {
  display: block;
  font-size: var(--text-xs);
  font-weight: 400;
  color: var(--text-muted);
  white-space: nowrap;
}

/* 概览区底部的「查看错误记录」入口。
   Day 12：原来 display:block 会让这枚胶囊拉满整张卡的宽度（1080px 宽的一根横条），
   在「一个次要入口」上给出了比主操作还重的分量；且胶囊内只有 9 个字，
   左右各留 500px 空白，看着像一条分隔带而不是按钮。
   改成 fit-content + 居中，恢复成正常胶囊大小。 */
.navlink.block {
  display: block;
  width: fit-content;
  margin: var(--space-5) auto 0;
  text-align: center;
}

/* ============================================================
   宽度 ≥740px：区块 1 内部拆两列、区块 3 三条统计横排
   ============================================================ */

@media (min-width: 740px) {
  /* ---------- 区块 1｜通栏后的内部两列 ---------- */

  /* 左列放「小标 + 标题 + 说明」，右列放输入框。
     右列宽度用 minmax(300px, 400px)：300 是输入框不折行的下限，
     400 是上限，超过就会把输入框拉成一条过长、眼睛要横扫的横条。
     align-items: start 让两列各自从顶部开始，不互相拉伸。 */
  .blk-identity {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(300px, 400px);
    grid-template-areas:
      "eyebrow   field"
      "title     field"
      "hint      hint";
    column-gap: var(--space-8);
    align-items: start;
  }

  .blk-identity > .eyebrow { grid-area: eyebrow; }
  .blk-identity > h2       { grid-area: title; }
  .blk-identity > .hint    { grid-area: hint; }
  .blk-identity > .field   { grid-area: field; }

  /* 通栏区块 1 的说明行放开限宽。
     为什么：.card .hint 全局限在 62ch（约 31 个汉字），在这个通栏宽卡里
     会让说明折成两行、第二行只剩「称呼。」两个字挂在左边，
     而右半边近 600px 全是空的 —— 一眼看上去像排版没对齐。
     放开后整句落在同一行（约 620px），卡片矮一行、右侧也不再留洞。
     只在 ≥740px 生效：窄屏卡片本来就窄，两行折行是正常的。 */
  .blk-identity > .hint {
    max-width: none;
  }

  /* 右列（输入框）与左列内容区顶边对齐：「昵称」标签与「01 · 开始之前」
     这条小标同一水平线起，右列整体不往下沉。
     用 align-self: start 交给栅格排，不写死 margin —— 窄列里标题折行时
     右列会自然跟着走，不会错位。 */
  .blk-identity > .field {
    align-self: start;
  }

  /* 栅格里 .hint 下方若接了别的内容，靠 .card .hint + * 的 16px 统一处理 */

  /* ---------- 区块 3｜三条统计横排 ---------- */

  /* 通栏宽卡里让三条统计横着排，避免右半边拖出一大片空白。
     注意这里**没有用卡片盒子**：三条之间只用一道 2px 浅色竖线分隔，
     数据直接落在空白里。技能第四条对数据区的要求 ——
     能靠留白和细线说清层级时，不要套盒子。 */
  .blk-overview .stat-list {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--space-4);
    margin-top: var(--space-5);
  }

  .blk-overview .stat-list li {
    display: block;
    padding: 0 0 0 var(--space-4);
    border-bottom: none;
    border-left: 2px solid var(--border-soft);
  }

  .blk-overview .stat-list li:last-child {
    padding-bottom: 0;
  }

  /* 每条统计改成上下两行（标签在上、数值在下）：
     三列本来就窄，横着放会把「5 条」拆成两行，竖排后三列的数值能对在同一条基线上 */
  .blk-overview .stat-list .k {
    display: block;
  }

  .blk-overview .stat-list .v {
    display: block;
    text-align: left;
    margin-top: var(--space-1);
  }

  /* 横排后每条只剩三分之一宽，「偏题 3 · 逻辑错误 2」允许折行，否则会横向溢出 */
  .blk-overview .stat-list .sub {
    white-space: normal;
  }
}

/* ============================================================
   Day 11：会反馈的按钮（交互反馈的表现层）
   ------------------------------------------------------------
   这一节是**表现层的唯一落点**，对应的状态机在
   frontend/js/components/interact.js（组件头有完整说明）。
   两层只通过 3 个 class + 2 个 data 属性连接：
     .ic-btn / .ic-hint / .is-fav / data-state / aria-pressed

   所以要换一套反馈风格，**只改这一节**，JS 一个字不用动。

   三个问题各自的答案（Day 12 复核后不变）：
     「我点到了吗」→ .ic-btn 的按压缩放（瞬态）
     「它在忙吗」  → [data-state="busy"] 变灰 + 禁用（防错）
     「成了吗」    → .is-fav 变色变字 + 一次弹跳（**持久状态，最强的一环**）
   ============================================================ */

/* 包装层：按钮 + 下方提示行，纵向排列 */
.ic-wrap {
  position: relative;
  z-index: 2;              /* 压在主题卡左上角那枚色斑之上 */
  display: inline-flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-2);
}

.ic-btn {
  /* 光圈用绝对定位铺满按钮，所以本体要成为定位上下文 */
  position: relative;
  /* 触控目标下限 44×44px —— 与 .navlink 同一条规矩（见下面 min-height 的说明）。
     用 inline-flex 而不是靠 padding 撑：加高之后文字要**垂直居中**，
     光加 padding-block 会让文字视觉偏上。 */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font: inherit;
  font-size: var(--text-sm);
  font-weight: 500;
  line-height: 1;
  padding: var(--space-2) var(--space-4);
  border: 1px solid var(--border);
  border-radius: var(--radius-full);
  background: var(--surface);
  color: var(--text-secondary);
  cursor: pointer;
  /* 尺寸固定：文字从「☆ 收藏」变「★ 已收藏」宽度会变，不锁住的话
     整行 meta 会跟着抖一下 —— 那看起来像布局坏了，不像反馈。 */
  /* 高度也固定，而且是 44px：原先由内边距自然撑出 30~32px，
     低于触控目标下限（项目里这条规矩原先只落在 .navlink 一处，
     所以这个按钮从没被验过 —— 见笔记「44px 触控目标」）。
     副作用已知并接受：主题卡因此长高约 14px、结果页与记录页长高约 12px。 */
  min-height: 44px;
  min-width: 7.5em;
  text-align: center;
  transition: background var(--fast) var(--ease-out),
              color var(--fast) var(--ease-out),
              border-color var(--fast) var(--ease-out),
              transform var(--fast) var(--ease-out),
              box-shadow var(--fast) var(--ease-out);
}

/* ---------- 光圈（按压 / 成功共用的一枚会扩散的环） ----------
   为什么用伪元素而不是新加一个 DOM 节点：这是纯装饰，加节点会污染
   「按钮 = 一个 button 元素」这个简单结构，也会让 interact.js 的
   状态逻辑多一份牵挂。伪元素只活在样式层，JS 完全不知道它存在。
   平时 visibility:hidden（不占位、不响应点击），只在对应状态下才显形。 */
.ic-btn::before {
  content: "";
  position: absolute;
  inset: -1px;                       /* 盖住按钮自己的 1px 边框 */
  border-radius: inherit;
  border: 2px solid var(--primary);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;              /* 绝不吞掉按钮的点击 */
}

.ic-btn:hover:not(:disabled) {
  border-color: var(--primary);
  color: var(--primary-dark);
  transform: translateY(-1px);
}

/* 按下的那一瞬：缩一下 + 一圈环向外扩。这是回答「我点到了吗」的动作，
   时长刻意比别的过渡更短（--fast 的一半），否则手感发黏。
   环用 300ms 扩散、比缩放的 75ms 长，这样「按下」这件事有个可见的余韵。 */
.ic-btn:active:not(:disabled) {
  transform: scale(0.94);
  transition-duration: 75ms;
}

.ic-btn:active:not(:disabled)::before {
  animation: ic-ring-out 300ms var(--ease-out);
}

/* ---------- 已收藏 / 已星标（持久状态，最强的一环） ---------- */

.ic-btn.is-fav {
  /* 兜底底色：**必须写background-color，而不是只靠上面的渐变**。
     Day 15 实测：微信内置浏览器（X5 内核）不渲染
     `linear-gradient(135deg, var(--x) 0%, var(--y) 100%)` 这条background 简写，
     于是 `background` 整条作废 → 回退到 .ic-btn 基础的 `background: var(--surface)`（白底），
     而 `color: #FFFFFF` 仍然生效 → **白字压白底，「★ 已收藏」四个字直接消失**
     （实测对比度 1.12:1，等于看不见）。
     写成两条之后：支持渐变的浏览器照旧拿渐变，不支持的也有深色底兜着，
     **白字在任何情况下都读得出来**（兜底值白字对比度 5.02:1）。 */
  background-color: var(--grad-from);
  background-image: linear-gradient(135deg, var(--grad-from) 0%, var(--grad-to) 100%);
  border-color: transparent;
  color: #FFFFFF;
  box-shadow: var(--shadow-sm);
  /* 双段弹跳：让状态变化被「看见」，而不只是「发现」。
     用 --ease-spring（回弹曲线）而不是默认缓动，弹跳感更明确。
     0.46s 刻意长于普通过渡 —— 这是整个交互里最该被记住的一下。 */
  animation: ic-pop 460ms var(--ease-spring);
}

/* 成功时，同一枚环从按钮边缘**向外炸开并淡出**。
   与按压环的区别：按压环是「描边扩散、留白不填」，成功环是
   「实心外晕、扩散即消失」，视觉分量更重 —— 呼应它是「成了」的信号。
   用 ::after（不是 ::before），好在两个环可以同时在场上，互不打架。 */
.ic-btn.is-fav::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: radial-gradient(circle, var(--accent) 0%, transparent 70%);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  animation: ic-burst 520ms var(--ease-out);
}

.ic-btn.is-fav:hover:not(:disabled) {
  color: #FFFFFF;
  box-shadow: var(--shadow-md);
}

@keyframes ic-pop {
  0%   { transform: scale(1); }
  30%  { transform: scale(1.18); }
  55%  { transform: scale(0.97); }   /* 回弹一下，像东西落回原位 */
  75%  { transform: scale(1.06); }
  100% { transform: scale(1); }
}

/* 按压环：只扩散描边，透明度先升后降，最后完全消失 */
@keyframes ic-ring-out {
  0%   { opacity: 0.75; transform: scale(0.96); visibility: visible; }
  100% { opacity: 0;    transform: scale(1.35); visibility: hidden; }
}

/* 成功环：实心外晕扩散即淡出，比按压环更亮、扩得更开 */
@keyframes ic-burst {
  0%   { opacity: 0.55; transform: scale(0.6); visibility: visible; }
  60%  { opacity: 0.25; transform: scale(1.5); }
  100% { opacity: 0;    transform: scale(1.8); visibility: hidden; }
}

/* 说明：原本还想让按钮里的 ★ 单独转一下，但**纯 CSS 做不到** ——
   ★ 和文字是同一个文本节点，无法只旋它一半；要拆成独立元素就得改
   interact.js 去包裹图标，那就破坏了两层分离（改样式要动 JS）。
   权衡后放弃，把「成功」的分量压在弹跳 + 光圈上（已经足够醒目）。 */

/* ---------- 处理中：不可重复点击 ---------- */

.ic-btn[data-state="busy"] {
  opacity: 0.6;
  cursor: progress;
  /* 还在用 cursor: not-allowed 的地方是 button:disabled 的通用规则；
     这里覆盖成 progress，语义更准 —— 不是「不能点」，是「正在忙」。 */
  transform: none;
}

/* ---------- 失败提示 ---------- */

.ic-hint {
  font-size: var(--text-xs);
  line-height: 1.5;
  /* 原用 --accent：白底 3.35，12px 不达标 → 换深档 6.37。
     报错文字是最不能读不清的一类，这里必须够深。 */
  color: var(--accent-deep);
  padding-left: var(--space-1);
  max-width: 20em;
}

/* 卡片里放这个按钮时，与上方内容留一道间距（覆盖 .card .hint + * 的 16px）。
   Day 12：星标挪进卡片右下角的独立栅格格，间距改由栅格负责，
   这条只留给「非卡片容器」里包着按钮的情况，值收成 0 避免重复留白。 */
.topic .ic-wrap {
  margin-top: 0;
}

/* ---------- 卡片内失败提示：浮起来，不参与排版 ----------
   为什么必须脱离文档流：.topic 是 5 行栅格，放提示的 star 格是 auto 宽。
   提示若留在流里，它 20em 的最大宽度会把这一格从 84px 撑到 240px，
   把左边那排信息胶囊（「上次在这里卡过 / T1 / 预计 3–5 分钟」）挤成竖条，
   整行卡片跟着长高。改成绝对定位后它完全不参与栅格测量，
   出现与消失都不让卡片动一下。 */
.topic .ic-hint {
  position: absolute;
  right: 0;
  bottom: calc(100% + var(--space-2));
  width: max-content;
  max-width: 15em;            /* 15em × 12px = 180px，不超出卡片内宽 */
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-md);
  background: var(--surface);
  box-shadow: var(--shadow-md);
  text-align: left;
  z-index: 3;
}

/* 主题卡上的星标按钮：卡片本身是 <a>，点按钮不该顺带跳转
   （见 topics.js 的 stopPropagation）。 */
.topic .ic-btn {
  font-size: var(--text-xs);
  min-width: 7em;
}

/* ---------- 被星标（置顶）的主题卡 ---------- */

/* 左侧一道主色竖条 + 更实的阴影：让「它被标了」在不读文字时也能看见。
   Day 12：竖条与悬停指示条同处左边缘，所以置顶态把悬停指示条一并点亮相呼应；
   置顶时长条收在上下留白之间，悬停指示条是通高的 —— 两者叠在一起也不打架。 */
.topic.is-pinned {
  border-color: var(--primary);
  border-left-color: var(--primary);
  box-shadow: var(--shadow-md);
}

.topic.is-pinned::after {
  content: "";
  position: absolute;
  left: 0;
  top: var(--space-6);
  bottom: var(--space-6);
  width: 3px;
  border-radius: var(--radius-full);
  background: linear-gradient(180deg, var(--primary) 0%, var(--accent) 100%);
}

.topic-list.group-b .topic.is-pinned {
  border-color: var(--teal);
  border-left-color: var(--teal);
}

.topic-list.group-b .topic.is-pinned::after {
  background: linear-gradient(180deg, var(--teal) 0%, #35A895 100%);
}

/* ---------- 收藏条目（P3 结果页 / P4 记录页） ---------- */

.item-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.item {
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-lg);
  padding: var(--space-4);
  background: var(--surface-soft);
  transition: border-color var(--fast) var(--ease-out),
              background var(--fast) var(--ease-out);
}

/* 已收藏的条目整块变暖：反馈不止落在按钮上，条目本身也跟着变 */
.item:has(.ic-btn.is-fav) {
  border-color: var(--primary);
  background: var(--primary-soft);
}

.item-head {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
  margin-bottom: var(--space-2);
}

/* 类型胶囊三色。偏题=珊瑚（要提醒的问题）、逻辑错误=蜜橘（问题更重、要纠正）、
   精彩句子=深青（**不是问题，是亮点** —— 用另一个色相，一眼能分开）。
   这三处都是 12px 小字压在各自的浅底上，Day 12 全部改用深档：
   珊瑚原 2.94、蜜橘原 4.47，都不达标；换档后 5.60 / 5.70 / 4.61。 */
.item-tag-offtopic {
  background: var(--accent-soft);
  color: var(--accent-deep);
}

.item-tag-logic {
  background: var(--primary-soft);
  color: var(--primary-deep);
}

.item-tag-good {
  background: var(--teal-soft);
  color: var(--teal);
}

.item-turn {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--text-muted);
}

/* 引文是整页最长的英文字符串，1100px 通栏会拉出一行过长的英文；
   限宽到 68ch（英文比中文窄，ch 单位对英文才准）。 */
.item-quote {
  font-family: var(--font-en);
  font-style: italic;
  font-size: var(--text-base);
  color: var(--text);
  line-height: 1.7;
  max-width: 68ch;
  margin-bottom: var(--space-2);
  text-wrap: pretty;
}

.item-note {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  line-height: 1.7;
  max-width: 68ch;
}

.item-note b {
  color: var(--text);
  font-weight: 600;
}

/* 逻辑错误条目才有「纠正」这一格；偏题条目没有（PRD.md §6.2 的刻意区别） */
.item-fix {
  margin-top: var(--space-2);
  padding-left: var(--space-3);
  border-left: 2px solid var(--primary);
  font-size: var(--text-sm);
  color: var(--text-secondary);
  line-height: 1.7;
  max-width: 68ch;
}

.item-foot {
  margin-top: var(--space-3);
  /* Day 22：条目底部现在有两个按钮（收藏 + 删除）。
     为什么用 flex 而不是靠 br 换行：.ic-btn 有 `min-width: 7.5em`，
     块级排版下两个按钮各占一整行，卡片会凭空长高一截 ——
     而它们在概念上是并列的两个动作（对这一条做这个 / 做那个），
     并排更符合这个关系，也省纵向空间。
     gap 用 space-2：两个 .ic-btn 之间有 1px 边框，
     贴太近会让两条边框看起来像一条双线。 */
  display: flex;
  flex-wrap: wrap;              /* 窄屏时换行，不硬挤 */
  align-items: center;
  gap: var(--space-2);
}

/* ---------- 删除按钮（Day 22）----------
   ★ 为什么单独一条而不是直接用 .ic-btn：删除是**不可逆**动作，
     它得跟同一张卡上可撤销的「收藏」在视觉上分得开。
     同一张卡上两个长得一样的按钮摆在一起，用户容易把「删除」当第二个收藏点下去。

   用珊瑚色的哪一档：文字用 --accent-deep（小字专用，≥4.5:1），
   边框与 hover 底色才用鲜色 --accent —— 遵守项目那两条硬规矩
   （① 鲜色只做装饰 ② 小字一律用 -deep 档），见 .tag-offtopic 的同类做法。 */
.item-del {
  color: var(--accent-deep);
  border-color: color-mix(in srgb, var(--accent) 40%, var(--border));
}

.item-del:hover:not(:disabled) {
  background: color-mix(in srgb, var(--accent) 8%, var(--surface));
  border-color: var(--accent);
}

/* 处理中（点击后请求在飞）：与 .ic-btn 的 busy 态一致地压暗一档。
   ★ 为什么不靠 opacity：淡到 0.6 后文字对比度会掉到 4.5:1 以下，
     而这条禁令是项目级的（见 .impeccable.md 视觉规范第三条）。 */
.item-del:disabled {
  color: var(--text-muted);
  border-color: var(--border);
  background: var(--surface);
  cursor: not-allowed;
}

/* 记录页每条下方的归属信息（所属主题 · 时间）。
   PRD.md §7.3 P4 要求每条能看到「所属主题与时间」—— 记录页跨会话，
   不标归属用户会不知道这条是哪次说的。结果页不需要它（那一页的条目
   全部属于同一次会话，标了是冗余）。 */
.item-src {
  font-size: var(--text-xs);
  color: var(--text-muted);
  padding: var(--space-2) var(--space-4) 0;
}

/* 假数据提示条：说明这一段的条目来自本地文件，不是真练出来的（Day 11 临时） */
.mock-bar {
  margin-top: var(--space-4);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  background: var(--bg-tint);
  font-size: var(--text-xs);
  color: var(--text-muted);
  line-height: 1.6;
}

/* Day 17 新增：降级态——「后端读不到真实记录，正在用本地假条目」。
   ------------------------------------------------------------
   为什么是珊瑚色左边框而不是把整块染成警示底色：
     这一条不是「操作失败要用户去补救」，而是**状态说明**——
     页面完全可用，只是数据源是临时的。用珊瑚左边框（4px，与 .free-link
     同一个处理手法）说「这里有个需要注意的点」，视觉上不喧宾夺主。

   为什么仍然用 -deep 档文字（不换深色底）：
     底色还是 --bg-tint（浅奶油），文字沿用 --text-muted，
     对比度与原来一致，不引入新的可读性问题。
     **鲜色只做装饰、小字一律用 -deep 档**这条硬规矩没被破坏——
     珊瑚只出现在 4px 边框上，不承载任何文字。 */
.mock-bar-warn {
  border-left: 4px solid var(--accent);
}

/* ============================================================
   P4 筛选条（Day 12新增）
   ------------------------------------------------------------
   口径见 PRD.md §7.4：一个输入框同时筛「收藏区」与「全部条目」两块。

   为什么放成「两区块上方通栏」而不是各区块一个：
     收藏区与全部条目是**同一份数据的两种排法**（都出自records.html 的
     flatten()），一个输入框两处同步变才符合「搜整个页面」的预期。
     两个框不仅多一个控件，还会出现「两个框结果不一样」的困惑。

   为什么复用 .field 而不是新写一套：
     .field 已经是「标签在上、输入框在下」的纵向表单块（见上文昵称区），
     标签与控件的关系在任何宽度下都不歧义。这里只多给一个筛选结果计数行。

   颜色：全部用现有令牌，**不新增任何颜色**。计数行用 --text-muted
   （4.67~5.31，已达标，见 :root 注释）—— 它是解释性文字不是正文。

   触控目标：输入框高度由 .field input 的 padding 决定，44px 要求由下面
   显式 min-height 保证（.ic-btn 已有同类修法，见 checks.md §6）。
   ============================================================ */

.filter-bar {
  margin-bottom: var(--space-6);
  padding: var(--space-4) var(--space-5);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-lg);
  background: var(--surface-soft);
}

/* 计数行：筛完显示「显示 N /共 M 条」，让用户知道「没找到」与「本来就没有」
   是两回事。role=status 让读屏软件在筛选结果变化时播报这一行。
   min-height 预留一行高度：数字从 2 位变3 位时整块不会跳。 */
.filter-count {
  margin-top: var(--space-2);
  font-size: var(--text-xs);
  color: var(--text-muted);
  min-height: 1.6em;
  font-variant-numeric: tabular-nums;
}

.filter-bar .field input {
  /* ≥44px 触控目标：与 .ic-btn 同一类修法，用 min-height 而不是加 padding-block，
     否则文字会视觉偏上（checks.md §6 记着这个坑）。 */
  min-height: 44px;
}

/* 小屏：筛选条收紧内边距，输入框占满整宽（≤640px 单列，layout.md §4） */
@media (max-width: 640px) {
  .filter-bar {
    padding: var(--space-4);
  }
}

/* ============================================================
   小屏适配（≤640px）
   Day 12 复核：三档宽度各自都有明确形态，不再有「两不靠」区间 ——
     ≤640px   手机：单列、纵向堆叠、说明不留最小高度
     641~739  平板竖持 / 手机横放：仍是单列，区块 1 不拆两列（宽度不够）
     ≥740px   桌面：区块 1 拆两列、区块 3 统计横排、主题卡自动 2~3 列
   ============================================================ */

@media (max-width: 640px) {
  body {
    padding: var(--space-6) var(--space-4) var(--space-10);
  }

  /* Day 14 第 3 板块：原为 var(--text-2xl)（24px 定值）。
     它会把 .topbar h1 里的 clamp 覆盖掉 —— 媒体查询在文件更靠后、
     同特异度下后写的赢，所以 390px 上标题一直被钉死在 24px，
     上面那句「clamp 上限敢加到 44」的注释在窄屏上根本没兑现过。
     窄屏给 26px：比 24px 明显压得住场，又留得住中文标题在 390px 内的
     单行可能（26px × 7 字 ≈ 182px，加标签 136px 仍需换行，故标签在窄屏另起一行）。 */
  .topbar h1 {
    font-size: 1.625rem;
    line-height: 1.3;
  }

  /* 光球舞台：小屏上左右内边距收窄（.orb 本身靠 clamp 已缩到下限 104px），
     引导语降一档字号 —— 18px 的「慢慢说，不着急」在 390px 上会折成两行，
     压着不小的球体显得挤。 */
  .stage {
    padding: var(--space-8) var(--space-4);
    gap: var(--space-5);
  }

  .orb-line {
    font-size: var(--text-base);
  }

  .topic-list {
    grid-template-columns: 1fr;
  }

  /* 单列时卡片不再需要等高的最小说明行高 */
  .topic .summary {
    min-height: 0;
  }

  .counter {
    gap: var(--space-4);
  }

  .skeleton-grid {
    grid-template-columns: 1fr;
  }

  .blk-topics > .blk-head {
    align-items: flex-start;
  }

  .blk-topics > .blk-head h2 {
    font-size: var(--text-xl);
  }

  /* 自由对话入口：390px 上一行放不下「球 + 文字 + 时长与按钮」三截，
     让球与文字占第一行，时长与按钮换到第二行铺满、左右分开。
     不给它单独一套版式：换行后仍是同一条横条，只是排布收了一层。 */
  .free-link {
    flex-wrap: wrap;
    gap: var(--space-4);
  }

  .orb.free-orb {
    width: 48px;
  }

  .free-meta {
    flex: 1 1 100%;
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
  }

  /* 窄屏上星标按钮与信息胶囊挤在一行会换行，让它按内容宽排更好点 */
  .topic .ic-btn {
    min-width: 0;
  }
}

/* 动效降级：用户在系统里关掉了动效，就把所有过渡与动画压到近零。
   注意这只是「时长归零」，动画的**终态**仍然会被应用 ——
   卡片入场动画的终态是 opacity:1，所以内容不会消失。 */
@media (prefers-reduced-motion: reduce) {
  * {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
  }
}
