/* ═══════════════════════════════════════════════════════════════
   视觉基调：教学文件
   ───────────────────────────────────────────────────────────────
   不是"应用"，是一份排版讲究的教学材料——纯白纸、近黑墨、发丝线做全部
   结构、一个主色。

   四条硬规矩（改这个文件前先读）：
     1. 零圆角。所有边角都是直角，一个 border-radius 都不写。
     2. 零阴影。层次靠 1px 发丝线，不靠投影——投影是"卡片感"，
        发丝线是"表格感"，后者才像教学材料。
     3. 颜色只有下面这几个 token，不许在下面写死颜色值。
     4. 零 emoji。图标一律用 Lucide 的 <i data-lucide="..."></i>。
     5. 状态必须上色。老师扫页面找的就是"哪些要动手"，纯文字状态等于没有。

   之前那版是暖纸底 + 墨绿 + 宋体 + 纸纹格线 + 五套色系 + 50 处圆角，
   信息密度高、决定太多，观感"vibe coded"。这版做的全是减法。
   ═══════════════════════════════════════════════════════════════ */

:root {
  /* 纸与墨 */
  --paper: #ffffff;
  --paper-2: #f7f8f8;      /* 极浅底：表头、嵌套块 */
  --ink: #16181c;          /* 正文/标题 */
  --ink-2: #5c636d;        /* 次要说明 */
  --ink-3: #949aa3;        /* 元信息、时间戳 */
  --rule: #e2e5e8;         /* 发丝线 */
  --rule-strong: #c7ccd2;  /* 需要强调的分隔 */

  /* ── 颜色为什么是这几个 ──────────────────────────────────────
     中国课堂里颜色本来就有约定，直接用它，不另发明：
       红  是老师的笔——批改、朱批。看到红就是"这里要老师动手"。
       蓝黑是学生的笔——作业本上写字的墨水。是"写下内容"的颜色。
     所以主色取墨蓝而不是红：红必须留给"需要处理/错了"，主色若也是红，
     满屏按钮就把真正要处理的东西淹掉了。两者各管各的，不打架。

     绿只当"确认"用（已通过/一致/已签），不做主题色。
     ——上一版拿黑板绿当主题色是从旧配色沿用的，那是惯性不是理由。 */

  /* 主色：墨蓝（写下动作——主按钮、当前模式、链接、焦点） */
  --accent: #1d4e89;
  --accent-2: #163c69;
  --accent-wash: #eef2f8;

  /* 朱红：老师要处理 / 错了（待处理、卡住、方向错、不符、未签） */
  --alert: #b3261e;
  --alert-wash: #fdf1f0;

  /* 淡绿：确认过了（已通过、一致、已签）——只做状态，不做主题 */
  --confirm: #1f6b45;
  --confirm-wash: #eef5f1;

  /* 字号阶梯：1.20 比例，只有这七档 */
  --t-xs: 11px;
  --t-sm: 12px;
  --t-base: 14px;
  --t-md: 16px;
  --t-lg: 19px;
  --t-xl: 23px;
  --t-2xl: 28px;

  /* 字距阶梯：**跟字号反向**。
     汉字是方块字、笔画密，小字号下 letter-spacing:0 会糊成一片；
     字号越大本身越有空气，再加字距就散了。西文正好相反，所以照搬
     西文那套"标题加大字距"的做法在中文里是错的——原来 h1 是 .02em、
     h2 是 .01em、正文 normal，三个值互不相干，也没有依据。

     另有一条：中文不要用 .06em 以上的字距做"标签感"，那是西文小型大写
     的手法，汉字拉开只会像没排好版。 */
  --ls-lg:  0;        /* 19px 以上：不加 */
  --ls-md:  .015em;   /* 16px 标题与强调 */
  --ls-base: .025em;  /* 14px 正文 */
  --ls-sm:  .04em;    /* 12px 及以下：字越小越要留白 */

  /* ── 间距：不只是刻度，是"关系有多近" ──────────────────────────
     光有七档数值不叫系统——得规定哪一档表示哪种关系，否则还是随手挑。
     由近及远，越相关的东西挨得越紧：

       --gap-inline  同一件事的组成部分（图标↔文字、标签↔值）
       --gap-tight   一组之内的相邻行（列表里一条和下一条）
       --gap-item    一个小节内部：标题↔内容、说明↔控件
       --gap-group   同一面板里两个**不同用途**的区块之间
       --gap-block   面板与面板之间
       --gap-page    页面级留白

     判断标准只有一条：**两样东西的间距，必须小于它们各自到别处的间距。**
     现在的毛病正是反过来——难点地图和各组列表贴成 0px，而关系更远的
     操作区反而只隔 12px，读的人分不出哪些是一伙的。 */
  --s-1: 4px;
  --s-2: 8px;
  --s-3: 12px;
  --s-4: 16px;
  --s-5: 24px;
  --s-6: 32px;
  --s-7: 48px;

  --gap-inline: var(--s-2);
  --gap-tight:  var(--s-3);
  --gap-item:   var(--s-4);
  --gap-group:  var(--s-6);
  --gap-block:  var(--s-7);
  --gap-page:   var(--s-7);

  --font: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei",
          -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif;
  --font-num: "SF Mono", "JetBrains Mono", ui-monospace, Menlo, monospace;
}

*, *::before, *::after { box-sizing: border-box; border-radius: 0 !important; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  font-family: var(--font);
  font-size: var(--t-base);
  line-height: 1.7;
  color: var(--ink);
  background: var(--paper);
  -webkit-font-smoothing: antialiased;
}

/* Lucide 图标：跟着文字走，不喧宾夺主 */
[data-lucide] { width: 1em; height: 1em; stroke-width: 1.75; vertical-align: -0.14em; }
.svg-icon { width: 1em; height: 1em; stroke-width: 1.75; vertical-align: -0.14em; flex: none; }

/* ── 页头 ─────────────────────────────────────────────────── */
header {
  padding: var(--s-3) var(--s-5);
  background: var(--paper);
  color: var(--ink);
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s-4);
  border-bottom: 1px solid var(--rule-strong);
  position: sticky; top: 0; z-index: 20;
}
header h1 {
  font-size: var(--t-md); margin: 0; font-weight: 600; letter-spacing: var(--ls-md);
  white-space: nowrap;
}
header .badge {
  font-size: var(--t-xs); color: var(--ink-2);
  border: 1px solid var(--rule); padding: 2px var(--s-2);
  white-space: nowrap; font-weight: 400;
}

.wrap { max-width: 1080px; margin: 0 auto; padding: var(--s-6) var(--s-5) var(--s-7); }

/* ── 首页 ─────────────────────────────────────────────────── */
.landing { max-width: 560px; }
/* 选择器曾经写的是 .landing a .sub，而 HTML 里的类名是 .role——
   两者从没匹配上，所以"姓名"和"这个角色能干什么"被挤成了一行。 */
.landing a {
  display: block;
  padding: var(--gap-item) var(--gap-item);
  text-decoration: none; color: var(--ink);
  border-bottom: 1px solid var(--rule);
  font-size: var(--t-md);
}
.landing a:first-of-type { border-top: 1px solid var(--rule); }
.landing a:hover { background: var(--accent-wash); color: var(--accent); }
.landing a .role {
  display: block; margin-top: var(--gap-inline);
  color: var(--ink-3); font-size: var(--t-sm); line-height: 1.6;
}

/* ── 面板：靠线分区，不靠卡片 ──────────────────────────────── */
.panel, .board {
  background: var(--paper);
  border: 1px solid var(--rule);
  margin-bottom: var(--s-6);
}
.panel-head, .board-head {
  padding: var(--s-3) var(--s-4);
  border-bottom: 1px solid var(--rule);
  background: var(--paper-2);
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s-3);
  font-weight: 600; font-size: var(--t-md);
}
.panel-head h2, .board-head h2 { margin: 0; font-size: var(--t-md); font-weight: 600; }
.panel-body, .board-body { padding: var(--s-4); }
.panel-body > :first-child, .board-body > :first-child { margin-top: 0; }
.panel-body > :last-child, .board-body > :last-child { margin-bottom: 0; }

.toolbar {
  display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap;
  margin-bottom: var(--s-3);
}
.spacer { flex: 1 1 auto; }

/* ── 锚点导航 ─────────────────────────────────────────────── */
/* 锚点导航贴在页头下面。**不要写死 top 值**——页头高度随内容变（手机上
   模式切换按钮会换行），写死 49px 会让导航和页头之间露出一条缝、或者被
   面板盖住。改用一个变量，由页头的实际高度决定。 */
.anchor-nav {
  display: flex; gap: var(--s-4);
  padding: var(--s-3) var(--s-5);
  border-bottom: 1px solid var(--rule);
  font-size: var(--t-sm);
  background: var(--paper);
  overflow-x: auto; scrollbar-width: none;
  position: sticky; top: var(--header-h, 52px); z-index: 19;
}
.anchor-nav::-webkit-scrollbar { display: none; }
.anchor-nav a { white-space: nowrap; }
.anchor-nav a { color: var(--ink-2); text-decoration: none; }
.anchor-nav a:hover { color: var(--accent); }

/* ── 按钮：只有两种 ───────────────────────────────────────── */
.btn, .btn-ghost, button {
  font-family: inherit; font-size: var(--t-base); line-height: 1.4;
  padding: var(--s-2) var(--s-4);
  border: 1px solid var(--accent); background: var(--accent); color: #fff;
  cursor: pointer; white-space: nowrap;
  display: inline-flex; align-items: center; gap: var(--s-2);
  transition: background .12s, border-color .12s, color .12s;
}
.btn:hover, button:hover { background: var(--accent-2); border-color: var(--accent-2); }
.btn-ghost {
  background: var(--paper); color: var(--ink); border-color: var(--rule-strong);
}
.btn-ghost:hover { background: var(--paper-2); color: var(--accent); border-color: var(--accent); }
button:disabled, .btn:disabled, .btn-ghost:disabled { opacity: .4; cursor: default; }
button:disabled:hover { background: var(--accent); border-color: var(--accent); }
.btn-ghost:disabled:hover { background: var(--paper); color: var(--ink); border-color: var(--rule-strong); }

/* ── 表单：原生控件，统一外观 ──────────────────────────────────
   下拉刻意保留原生 <select>：师生都在手机上用，原生会拉起系统选择器，
   大、好按、无障碍免费。自绘下拉在手机上是降级。这里只统一外观。 */
input, select, textarea {
  font-family: inherit; font-size: var(--t-md); line-height: 1.5;
  color: var(--ink); background: var(--paper);
  border: 1px solid var(--rule-strong);
  padding: var(--s-2) var(--s-3);
  width: 100%;
}
/* 焦点态不用 box-shadow（硬规矩第 2 条），改成加粗边框；
   同时 padding 减 1px，免得整个控件在聚焦瞬间跳一下。 */
input:focus, select:focus, textarea:focus {
  outline: none; border: 2px solid var(--accent); padding: calc(var(--s-3) - 1px);
}
textarea { resize: vertical; }

select {
  appearance: none; -webkit-appearance: none;
  padding-right: var(--s-6);
  background-image:
    linear-gradient(45deg, transparent 50%, var(--ink-2) 50%),
    linear-gradient(135deg, var(--ink-2) 50%, transparent 50%);
  background-position: right 18px center, right 13px center;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}

.field { display: block; margin-bottom: var(--s-3); }
.field > span {
  display: block; font-size: var(--t-sm); color: var(--ink-2);
  margin-bottom: var(--s-1);
}

/* ── 学生页：题干 ─────────────────────────────────────────── */
/* align-items:start——grid 默认 stretch 会把两栏拉成等高，
   题干短的时候「题目」面板底下就空出一大片。 */
.layout { display: grid; gap: var(--gap-block); align-items: start; }
@media (min-width: 900px) { .layout { grid-template-columns: minmax(0,1fr) minmax(0,1fr); } }
/* 课上答疑的版面：**上下两块**。
   上块「这节课」——左题目、右我的对话；下块整幅给小组讨论。
   小组讨论是这节课最热闹的地方，横着铺开比挤在侧栏里好读得多。 */
@media (min-width: 900px) {
  body[data-mode="class"] .layout {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr);
    grid-template-areas:
      "problem chat"
      "group   group";
    align-items: start;
  }
  body[data-mode="class"] .ga-problem { grid-area: problem; }
  body[data-mode="class"] .ga-chat    { grid-area: chat; }
  body[data-mode="class"] .ga-group   { grid-area: group; }
}
/* 手机退成单列：先看题、再问助手、最后看同学在聊什么 */
@media (max-width: 899px) {
  body[data-mode="class"] .ga-problem { order: 1; }
  body[data-mode="class"] .ga-chat    { order: 2; }
  body[data-mode="class"] .ga-group   { order: 3; }

  /* 阶段②的主任务是同伴讨论。小屏上把讨论区排到第一位，学生不用
     先越过题目和私聊才能看到组员刚说了什么；其它课堂阶段仍保留
     「题目 → 私问 → 小组」的阅读顺序。 */
  body[data-mode="class"][data-phase="2"] .ga-group   { order: 1; }
  body[data-mode="class"][data-phase="2"] .ga-problem { order: 2; }
  body[data-mode="class"][data-phase="2"] .ga-chat    { order: 3; }
}

#stem { font-size: var(--t-md); line-height: 1.8; }
#stem p { margin: 0 0 var(--s-3); }
#figures img, .t-stem-img { max-width: 100%; display: block; margin-top: var(--s-3); border: 1px solid var(--rule); }

/* ── 对话流 ───────────────────────────────────────────────── */
.chat-panel { display: flex; flex-direction: column; }
/* 可滚区域一律用 .scroll-y，滚动条**常驻可见**。
   macOS 默认是浮层滚动条：不滚就完全看不见，内容被截断了也毫无提示——
   实测截图里群聊第一条"我先算一下…"上半截被切掉，看起来像渲染坏了。
   scrollbar-gutter: stable 让滚动条占位固定，内容不会在出现/消失时抖动。 */
#transcript, #agent-transcript, #class-visit-log, .group-chat-log, .scroll-y {
  overflow-y: auto;
  padding-right: 16px;          /* 给右侧那条指示条让位 */
}
#transcript { max-height: 56vh; }
.group-chat-log { max-height: 46vh; }

.msg, .gc-msg { margin-bottom: var(--s-4); }
.msg .who, .gc-msg .who {
  font-size: var(--t-xs); color: var(--ink-3); margin-bottom: var(--s-1);
  letter-spacing: var(--ls-sm);
}
.msg .bubble, .gc-msg .bubble {
  padding: var(--s-3);
  border: 1px solid var(--rule);
  line-height: 1.75;
}
.msg.student .bubble, .gc-msg.student .bubble {
  background: var(--accent-wash); border-color: var(--accent-wash);
}
.msg.tutor .bubble, .gc-msg.student_agent .bubble { background: var(--paper); }
.gc-msg.host .bubble { background: var(--paper-2); }
.gc-msg.teacher .bubble, .msg.tutor.teacher-reply .bubble {
  background: var(--paper); border-left: 3px solid var(--accent);
}
.gc-msg.system, .msg-note {
  text-align: center; font-size: var(--t-sm); color: var(--ink-3);
  padding: var(--s-2) 0;
}
.msg .meta {
  font-size: var(--t-xs); color: var(--ink-3); margin-top: var(--s-1);
  font-family: var(--font-num);
}
.sent-photo { max-width: 220px; display: block; margin-top: var(--s-2); border: 1px solid var(--rule); }

.composer, .group-chat-composer { display: flex; gap: var(--s-2); margin-top: var(--s-3); }
.composer input { flex: 1; }
.group-chat-composer { flex-direction: column; }
.group-chat-actions { display: flex; gap: var(--s-2); justify-content: flex-end; }
/* 拍照那个是 <label>（里面藏着 file input），不是 <button>——
   按钮样式只选了 button/.btn/.btn-ghost，label 拿不到边框，
   于是它跟旁边的麦克风按钮长得不一样。这里补齐。 */
.photo-btn {
  flex: none; width: 44px; height: 100%; min-height: 40px;
  display: inline-flex; align-items: center; justify-content: center;
  padding: var(--s-2); cursor: pointer;
  border: 1px solid var(--rule-strong);
  background: var(--paper); color: var(--ink-2);
  transition: background .12s, border-color .12s, color .12s;
}
.photo-btn:hover { background: var(--paper-2); color: var(--accent); border-color: var(--accent); }

.starter-prompts { display: flex; gap: var(--s-2); flex-wrap: wrap; margin-bottom: var(--s-3); }
.spinner { color: var(--ink-3); font-size: var(--t-sm); }

/* ── 整页自判 ─────────────────────────────────────────────── */
.selfcheck-bar {
  border: 1px solid var(--rule);
  padding: var(--s-4); margin-top: var(--s-5);
}
.sc-item { padding: var(--s-3) 0; border-bottom: 1px solid var(--rule); }
.sc-item:last-child { border-bottom: none; }
/* 自判结果的四种状态必须一眼可分（硬规矩第 5 条）。原来一律 ink-2 灰，
   学生得逐条读文字才知道哪几题要回头看。
   注意"通过"用 confirm 绿而不是主色——绿在这套配色里只表示"确认"。 */
.sc-status { font-size: var(--t-sm); color: var(--ink-2); }
.sc-status.pass     { color: var(--confirm); }
.sc-status.stuck    { color: var(--alert); font-weight: 600; }
.sc-status.mismatch { color: var(--alert); }
.sc-status.unclear  { color: var(--ink-3); }

/* ── 表格化的行：花名册/复盘/工单/各组 ─────────────────────── */
.roster-grid, .progress-list, .ticket-cols { display: grid; gap: var(--s-3); }
@media (min-width: 720px) {
  .roster-grid { grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); }
  /* 曾经是 1fr 1fr 的两列看板。已回复那一列即使收成 45px 的行，
     也还是永久占着一半版面——而那列里没有一条需要老师动手。
     现在待处理独占整块，已回复收进底部的 .done-col。 */
}
.roster-card, .qc-card, .report-row, .class-row, .guide-row, .ladder-row,
.profile-row, .faq-save-row {
  padding: var(--s-3) 0;
  border-bottom: 1px solid var(--rule);
  display: flex; align-items: center; gap: var(--s-3); flex-wrap: wrap;
}
.roster-card { display: block; border: 1px solid var(--rule); padding: var(--s-3); }
.roster-card .name { font-weight: 600; }
.roster-card .sid, .group-stat { font-size: var(--t-sm); color: var(--ink-3); }
.group-stat { margin-left: auto; font-family: var(--font-num); }
.report-row .count, .prog-chip { font-family: var(--font-num); font-size: var(--t-sm); }

.ticket-col-title {
  font-size: var(--t-sm); color: var(--ink-2); font-weight: 600;
  padding-bottom: var(--s-2); border-bottom: 1px solid var(--rule-strong);
  margin-bottom: var(--s-3);
}
.t-head { display: flex; align-items: baseline; gap: var(--s-2); font-weight: 600; }
.t-q { margin: var(--s-2) 0; }
.t-ctx, .t-stem { font-size: var(--t-sm); color: var(--ink-2); }
.t-stem-body { border-left: 2px solid var(--rule); padding-left: var(--s-3); margin-top: var(--s-2); }
.ticket-reply-form { margin-top: var(--s-3); }
.ticket-reply-actions { display: flex; gap: var(--s-2); margin-top: var(--s-2); }

/* ── 标签/状态 ────────────────────────────────────────────── */
.tag, .badge, .chip, .prog-chip, .unready-chip, .action-tag, .flag, .cat {
  display: inline-flex; align-items: center; gap: var(--s-1);
  font-size: var(--t-xs); padding: 1px var(--s-2);
  border: 1px solid var(--rule-strong); color: var(--ink-2);
  background: var(--paper); white-space: nowrap;
}
.chip-row { display: flex; flex-wrap: wrap; gap: var(--s-2); margin: var(--s-2) 0; }
.chip { cursor: pointer; padding: var(--s-1) var(--s-3); font-size: var(--t-sm); }
.chip:hover { border-color: var(--accent); color: var(--accent); }
.chip-on, .chip.active {
  background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 500;
}
/* 状态色统一在文件末尾定义（见「状态色」一节），这里不重复 */

/* ── 四模式切换 ───────────────────────────────────────────── */
.mode-switch { display: flex; border: 1px solid var(--rule-strong); }
.mode-switch-btn {
  background: var(--paper); color: var(--ink-2); border: none;
  padding: var(--s-2) var(--s-4); font-size: var(--t-sm);
}
.mode-switch-btn + .mode-switch-btn { border-left: 1px solid var(--rule-strong); }
.mode-switch-btn:hover { background: var(--paper-2); color: var(--accent); }
.mode-switch-btn.active { background: var(--accent); color: #fff; font-weight: 500; }
.mode-switch-btn.active:hover { background: var(--accent-2); }
body[data-mode="class"] .homework-only { display: none !important; }
body[data-mode="homework"] .class-only { display: none !important; }

/* ── 课堂横幅 ─────────────────────────────────────────────── */
.class-banner, .stu-class-banner {
  border: 1px solid var(--rule); border-left: 3px solid var(--accent);
  background: var(--paper-2);
  padding: var(--s-3) var(--s-4); margin-bottom: var(--s-4);
}
.class-banner div, .stu-class-banner b { display: block; }
.stu-class-banner b { font-size: var(--t-md); }
.class-phase { font-size: var(--t-sm); color: var(--accent); font-weight: 600; }
.class-actions { display: flex; gap: var(--s-2); flex-wrap: wrap; margin-top: var(--s-3); }
.group-chat { border: 1px solid var(--rule); padding: var(--s-4); margin-bottom: var(--s-4); }
.group-chat-head { padding-bottom: var(--s-2); border-bottom: 1px solid var(--rule); }
.class-visit-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s-2); margin: var(--s-4) 0 var(--s-2);
  padding-top: var(--s-3); border-top: 1px solid var(--rule-strong);
}

/* ── 浮层 ─────────────────────────────────────────────────── */
.overlay {
  position: fixed; inset: 0; background: rgba(22, 24, 28, .45);
  display: flex; align-items: center; justify-content: center;
  padding: var(--s-4); z-index: 50;
}
.overlay-card {
  background: var(--paper); border: 1px solid var(--rule-strong);
  padding: var(--s-5); max-width: 520px; width: 100%;
}
.overlay-actions { display: flex; gap: var(--s-2); justify-content: flex-end; margin-top: var(--s-4); }

/* ── 富文本 ───────────────────────────────────────────────── */
.rich-p { margin: 0 0 var(--s-2); }
.rich-p:last-child { margin-bottom: 0; }
.rich-list { margin: 0 0 var(--s-2); padding-left: 1.3em; }
.rich-list li { margin-bottom: var(--s-1); }
strong, b { font-weight: 600; }
code { font-family: var(--font-num); font-size: .92em; background: var(--paper-2); padding: 1px 4px; }

/* ── 通用 ─────────────────────────────────────────────────── */
.hint, .sub, .report-empty, .empty, .note, .meta {
  color: var(--ink-2); font-size: var(--t-sm); line-height: 1.7;
}
.report-empty, .empty { color: var(--ink-3); padding: var(--s-3) 0; }
h2, h3 { font-weight: 600; letter-spacing: var(--ls-md); }
h3 { font-size: var(--t-base); margin: var(--s-4) 0 var(--s-2); }
a { color: var(--accent); }
hr { border: none; border-top: 1px solid var(--rule); margin: var(--s-4) 0; }

/* ── 手机（师生的主要使用方式）───────────────────────────────
   输入类控件字号必须 ≥16px，否则 iOS 聚焦时会自动放大页面。 */
@media (max-width: 860px) {
  .wrap { padding: var(--s-4) var(--s-4) var(--s-7); }
  header { padding: var(--s-3) var(--s-4); }
  .anchor-nav { padding: var(--s-2) var(--s-4); gap: var(--s-3); overflow-x: auto; flex-wrap: nowrap; }
  .anchor-nav a { white-space: nowrap; }
  .layout { grid-template-columns: 1fr; }
  input, select, textarea { font-size: 16px; }
  #transcript { max-height: 50vh; }
  .panel-body, .board-body { padding: var(--s-3); }
  header h1 { font-size: var(--t-base); }
}

/* ── 收尾：把上一版残留的观感统一掉 ───────────────────────────── */

/* 表单控件跟着主色，不要系统蓝 */
input[type="checkbox"], input[type="radio"] {
  width: 15px; height: 15px; accent-color: var(--accent);
  vertical-align: -2px; margin-right: var(--s-2);
}
label:has(> input[type="checkbox"]) { display: inline-flex; align-items: center; }

/* 工单条目：给标题一点层次，别让姓名/题号/状态挤成一条 */
.t-head { justify-content: space-between; }
.t-head .badge { margin-left: auto; }
#tickets .board-body > div > div { margin-bottom: var(--s-5); }

/* 录音按钮别撑成大方块 */
.rec-btn { padding: var(--s-2) var(--s-3); }

/* 手机上锚点导航自己横向滚，但**不许把文档撑宽**——
   overflow-x:auto 的元素若父级没夹住，整页会跟着横向滚。 */
body, .wrap { overflow-x: hidden; }
.anchor-nav { max-width: 100%; }

/* KaTeX：公式过长时在自己的容器里横向滚，**不许顶宽页面**。
   踩过的坑：只写 .katex-display 不够——撑宽页面的是行内公式渲染出的
   隐藏 <math>（MathML 那份），它不受 .katex 的样式管，实测在手机上
   把 390px 的页面顶到 440px。所以直接把 MathML 那份藏掉（KaTeX 渲染的
   HTML 那份才是给人看的，MathML 只为无障碍/复制），并给 .katex 加滚动。 */
.katex-display { overflow-x: auto; overflow-y: hidden; padding: 2px 0; }
.katex { max-width: 100%; }
.katex-html { overflow-x: auto; overflow-y: hidden; }
.katex .katex-mathml {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap;
}

/* 页头右侧那条说明在窄屏会顶宽页头——让它可以换行、并让出空间 */
header .badge { white-space: normal; text-align: right; line-height: 1.4; }
@media (max-width: 520px) { header .badge { display: none; } }

/* 工单回复那一行：录音 / 勾选 / 回复 三块等高对齐，不要各长各的 */
.ticket-reply-actions { align-items: stretch; }
.ticket-reply-actions > * { display: flex; align-items: center; }
.ticket-reply-actions label { flex: 1; gap: var(--s-2); font-size: var(--t-sm); color: var(--ink-2); }
.ticket-reply-actions .rec-btn { justify-content: center; min-width: 88px; }

/* 输入行三块等高 */
.composer { align-items: stretch; }
.composer input { min-height: 40px; }

/* ── 状态色：一眼看出哪些要动手 ─────────────────────────────────
   工单看板上「待处理」和「已回复」原来长得一模一样，老师得逐条读文字
   才知道哪些要处理——状态是这个页面最该被扫到的信息，必须上色。 */
.st-todo {
  color: var(--alert); border-color: var(--alert); background: var(--alert-wash);
  font-weight: 600;
}
.st-done { color: var(--ink-3); border-color: var(--rule); background: var(--paper); }

/* 分栏标题：待处理那一列的标题用红并加粗左边线，扫一眼就知道该看哪边 */
.st-todo-title {
  color: var(--alert); border-bottom-color: var(--alert);
}
.st-done-title { color: var(--ink-3); }

/* 已回复的卡片怎么"退下去"：见文末 .ticket-card 一节——用左边线变灰 +
   浅底，不用透明度。两种手段叠着会让文字发糊，反而更难读。 */

/* 其余状态词沿用同一套语义 */
.ok, .sign-yes { color: var(--confirm); border-color: var(--confirm); background: var(--confirm-wash); }
.stuck, .mismatch, .flag, .sign-no, .pending {
  color: var(--alert); border-color: var(--alert); background: var(--alert-wash);
}

/* 花名册的进度小标：已通过=确认绿，卡住=朱红，进行中=中性 */
.prog-chip.done  { color: var(--confirm); border-color: var(--confirm); background: var(--confirm-wash); }
.prog-chip.stuck { color: var(--alert); border-color: var(--alert); background: var(--alert-wash); }
.prog-chip.going { color: var(--ink-2); }

/* 卡点复盘的人次：数字本身就是"要不要管"的信号，给它颜色 */
.report-row .count { color: var(--alert); font-weight: 600; }
.report-row .head { display: flex; justify-content: space-between; gap: var(--s-3); width: 100%; }

/* 「还没开始」是中性事实，不是问题，别用红 */
.roster-card .current { color: var(--ink-3); font-size: var(--t-sm); }

/* ── 间距系统的落地 ─────────────────────────────────────────────
   用 flex + gap 把"区块之间"的距离交给容器统一管，不再让每个子元素
   各自写 margin-bottom——那正是间距失控的根源（谁都写一点，加起来没规律）。 */

/* 面板内容：直接子块之间是"不同用途的区块"，用 group 档 */
.panel-body, .board-body { display: flex; flex-direction: column; gap: var(--gap-group); }
.panel-body > *, .board-body > * { margin: 0; }

/* 课堂面板：横幅 / 地图 / 各组 / 操作，四个不同用途的区块 */
#class-live { display: flex; flex-direction: column; gap: var(--gap-group); }
#class-idle { display: flex; flex-direction: column; gap: var(--gap-item); }

/* 一个区块内部：标题和它的内容属于同一件事，贴紧 */
#class-map, #class-asking, #class-groups { display: flex; flex-direction: column; gap: var(--gap-tight); }
#class-map .hint:first-child { margin-bottom: calc(var(--gap-tight) * -1 + var(--s-1)); }

/* 列表里相邻的行：同一组，用 tight；行内部的元素用 inline */
.class-row, .report-row, .guide-row, .ladder-row, .profile-row {
  padding: var(--gap-tight) 0; gap: var(--gap-inline);
}
.class-row:first-child, .report-row:first-child { padding-top: 0; }
.class-row:last-child, .report-row:last-child { border-bottom: none; padding-bottom: 0; }

/* 操作区自成一块，和上面的内容拉开到 group 档，并加一条线划界 */
.class-actions, .ticket-reply-actions {
  gap: var(--gap-inline); padding-top: var(--gap-item);
  border-top: 1px solid var(--rule);
}

/* 说明文字紧跟它解释的东西，不要浮在中间 */
.panel-body > .hint:first-child, .board-body > .hint:first-child { margin-bottom: 0; }

/* 工单：一张卡片内部各部分是同一件事，用 item 档；卡片之间用 group 档 */
.ticket-card { display: flex; flex-direction: column; gap: var(--gap-inline); }
#ticket-board .ticket-cols > div { display: flex; flex-direction: column; gap: var(--gap-group); }
.ticket-col-title { margin-bottom: 0; }

/* 对话：一条消息里"谁说的"和"说了什么"是一件事，条与条之间才拉开 */
.msg, .gc-msg { display: flex; flex-direction: column; gap: var(--s-1); margin-bottom: var(--gap-item); }

/* 面板之间 */
.panel, .board { margin-bottom: var(--gap-block); }
.wrap { padding: var(--gap-group) var(--s-5) var(--gap-page); }

/* 交给容器 gap 之后，子元素自己就不能再带 margin——两者会叠加。
   实测：横幅的 margin-bottom:16px 撞上容器的 32px gap，变成 48px，
   比别的区块间距都大，读起来像"横幅和下面无关"。 */
#class-live > *, #class-idle > *,
.panel-body > *, .board-body > *,
#class-map > *, #class-groups > *, #class-asking > * { margin-bottom: 0; }

.class-banner, .stu-class-banner, .group-chat { margin-bottom: 0; }
.toolbar { margin-bottom: 0; }

/* ── 工单卡片：每一条是一张独立的卡 ───────────────────────────────
   原来只给了 flex 和透明度，没有容器——一列里几条工单糊在一起，
   读的人要靠空行猜哪句属于哪条。 */
.ticket-card {
  border: 1px solid var(--rule);
  border-left: 3px solid var(--alert);      /* 待处理：左侧一道红，扫一眼就找得到 */
  padding: var(--gap-item);
  background: var(--paper);
}
.ticket-card.answered {
  border-left-color: var(--rule-strong);     /* 已回复：左侧退成灰 */
  background: var(--paper-2);
}

/* 卡内层级：问题是主，元信息是次 */
.ticket-card .t-q { font-size: var(--t-md); line-height: 1.75; }

/* 老师自己的回复：和学生的问题区分开，用主色的左边线 + 浅底 */
.ticket-card .t-ctx {
  border-left: 2px solid var(--accent);
  background: var(--accent-wash);
  padding: var(--s-2) var(--s-3);
  color: var(--ink);
}

/* ── 可点的东西必须看起来可点 ─────────────────────────────────
   <summary> 默认继承正文样式，「查看题干」看上去就是一句话。
   给它颜色、指针、以及一个会转的三角。 */
summary {
  cursor: pointer; list-style: none;
  color: var(--accent); font-size: var(--t-sm);
  display: inline-flex; align-items: center; gap: var(--s-1);
  padding: var(--s-1) 0;
  user-select: none;
}
summary::-webkit-details-marker { display: none; }
summary::before {
  content: ""; width: 0; height: 0;
  border-left: 4px solid currentColor;
  border-top: 3.5px solid transparent;
  border-bottom: 3.5px solid transparent;
  transition: transform .15s;
}
details[open] > summary::before { transform: rotate(90deg); }
summary:hover { color: var(--accent-2); text-decoration: underline; }
details[open] > summary { margin-bottom: var(--s-2); }

/* ── 触摸设备：把点击区撑到手指够得着 ─────────────────────────────
   实测生产站点（iPhone 13 视口）量出来的：锚点导航 20px、复选框 15px、
   <summary> 28px。Apple 建议 44pt、Material 建议 48dp——20px 是"看得见
   但按不准"，对手机上的初一学生尤其糟。

   用 pointer:coarse 而不是宽度断点：判据是"用手指还是用鼠标"，跟屏幕
   多宽无关。桌面端的信息密度完全不受影响。 */
@media (pointer: coarse) {
  .anchor-nav a {
    padding-top: var(--s-3); padding-bottom: var(--s-3);
    display: inline-block;
  }
  summary { padding: var(--s-3) 0; }
  /* 原生复选框在手机上只有 15px 高，且不受 padding 影响，只能直接放大 */
  input[type="checkbox"] { width: 24px; height: 24px; }
  /* 复选框旁边的文字也要能点——手指按标签比按那个小方块容易得多 */
  label:has(> input[type="checkbox"]) {
    display: inline-flex; align-items: center; min-height: 44px;
  }
  /* min-height 对行内元素无效。**这一条踩过**：只写 min-height:44px，
     实测量出来还是 20px，因为 .selfcheck-upload-btn 是 display:inline。 */
  .selfcheck-upload-btn, .photo-btn {
    display: inline-flex; align-items: center; min-height: 44px;
  }
  .landing a { padding-top: var(--s-5); padding-bottom: var(--s-5); }
}


/* ── [hidden] 必须真的隐藏 ──────────────────────────────────────
   HTML 的 hidden 属性由浏览器默认样式实现成 `[hidden]{display:none}`，
   优先级极低——作者样式里任何一条给该元素设 display 的规则都能压过它。

   **线上真崩过**：`#class-idle { display: flex }` 让开课前的"选哪节课 +
   开始上课"在课堂进行中仍然渲染，压在实时面板上面，看起来就是元素重叠。
   全站有十来处用 hidden 控制显隐，逐个排查不如在这里一次封死。 */


/* ── 课堂模式：让人知道该看哪里 ─────────────────────────────────
   问题不是"间距不够"，是层次反了：开课前的说明书占着最显眼的位置，
   而"几个人开始了、谁卡住了"这种上课时唯一要看的信号，用的是 .hint
   ——全页最小最灰、跟脚注同级。

   所以做三件事：说明书移进 idle（见 teacher.html）；现场读数升级成
   live-stat；每块加标题，让老师扫一眼就知道每块是什么。 */

#class-live { display: flex; flex-direction: column; gap: var(--gap-block); }

/* 当前阶段是整个面板的锚点——上课时老师最先确认的就是"现在到哪了" */
.class-banner {
  border-left: 3px solid var(--accent);
  background: var(--accent-wash);
  padding: var(--gap-item) var(--gap-group);
}
.class-banner b { font-size: var(--t-lg); display: block; }
.class-banner .sub { color: var(--ink-2); margin-top: var(--gap-inline); }

.live-sec { display: flex; flex-direction: column; gap: var(--gap-item); }
.live-sec-t {
  font-size: var(--t-sm); color: var(--ink-3);
  letter-spacing: var(--ls-md);
  padding-bottom: var(--gap-inline);
  border-bottom: 1px solid var(--rule);
}

/* 现场读数：数字要大到能扫到，限定词缩到最小 */
.live-stat { display: flex; align-items: baseline; gap: var(--gap-inline); }
.live-stat b { font-size: var(--t-xl); color: var(--accent); }
.live-stat span { font-size: var(--t-md); color: var(--ink); }
.live-stat i { font-style: normal; font-size: var(--t-sm); color: var(--ink-3); }

/* 操作区跟信息区之间要有明确的分界——它们是两种不同的东西 */
.class-actions {
  border-top: 1px solid var(--rule-strong);
  padding-top: var(--gap-group);
  display: flex; align-items: flex-end; gap: var(--gap-group);
}

/* ── 已回复的工单：收起来 ────────────────────────────────────────
   回完的工单老师不需要再读。铺成和待处理一样大的卡片，等于让"做完的事"
   和"该做的事"抢同样多的注意力——一列十几张卡，扫一眼根本找不到今天
   该动手的是哪几条。收成一行，要复查再点开。 */
.ticket-done > summary {
  display: flex; align-items: baseline; gap: var(--gap-inline);
  padding: var(--gap-tight) 0;
  border-bottom: 1px solid var(--rule);
  color: var(--ink-2);
}
.ticket-done > summary::before { border-left-color: var(--ink-3); }
.ticket-done > summary:hover { color: var(--ink); text-decoration: none; }
/* 姓名和题号不许换行——被 flex 压缩后"张明"会断成两行，行高翻倍，
   收起的意义就没了。只让问题原文那一段收缩并截断。 */
.ticket-done > summary b {
  color: var(--ink); font-weight: 600;
  white-space: nowrap; flex: 0 0 auto;
}
.ticket-done .tk-num { font-size: var(--t-sm); color: var(--ink-3); white-space: nowrap; flex: 0 0 auto; }
/* 问题原文占剩下的空间，单行截断——它只是帮你认出是哪一条 */
.ticket-done .tk-q {
  font-size: var(--t-sm); color: var(--ink-3);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  min-width: 0; flex: 1 1 auto;
}
.ticket-done[open] > summary { border-bottom: none; }
.ticket-done[open] .ticket-card { margin-bottom: var(--gap-item); }


/* ── 已回复：整列收进一条 ───────────────────────────────────────
   看板该回答"今天要做什么"。做完的工单留一个入口就够了，
   不该跟待处理平分版面。 */
.done-col { margin-top: var(--gap-block); border-top: 1px solid var(--rule-strong); }
.done-col > summary {
  padding: var(--gap-item) 0;
  color: var(--ink-2); font-size: var(--t-md);
}
.done-col > summary::before { border-left-color: var(--ink-3); }
.done-col-n {
  font-size: var(--t-sm); color: var(--ink-3);
  border: 1px solid var(--rule); padding: 0 var(--s-2); margin-left: var(--s-2);
}
.done-col-list { display: flex; flex-direction: column; padding-bottom: var(--gap-item); }

/* 回复框：默认一行，聚焦时长开。
   回复是这张卡的主动作，藏在一次点击后面是错的；但摊开三行又让每张卡
   涨到 300px，七条待处理要滚两千多像素。默认一行、focus 展开两头都占。
   一行太长既难读也难瞄准，所以限宽 62ch。 */
.ticket-reply-form textarea {
  max-width: 62ch;
  min-height: 2.6em;
  transition: min-height .12s ease;
}
.ticket-reply-form textarea:focus,
.ticket-reply-form textarea:not(:placeholder-shown) { min-height: 6.5em; }

/* 操作行在没动笔之前压低存在感——录音和"存入 FAQ"都是回复的附属选项 */
.ticket-reply-actions { margin-top: var(--gap-tight); }


/* ── 课堂模式：字号层级与 hover 修正 ───────────────────────────
   两个问题：
   1) 选中的阶段按钮是白字蓝底，而 .chip:hover 把字色改成主色——
      **深蓝字压在深蓝底上，hover 一下字就没了**。
   2) 小节标题用的是 .sub（跟 .hint 同一组样式，比正文还小），
      所以"标题比正文小"。标题统一走 .live-sec-t。 */
.chip-on:hover, .chip.active:hover {
  color: #fff; border-color: var(--accent-2); background: var(--accent-2);
}

/* 小节标题必须**大于**正文。原来用 --t-sm（12px），而正文是 14px——
   标题比正文还小，加上跟说明句同样大小，等于没有层级。
   现在 16px/600：标题 16 > 正文 14 > 说明句 13 > 元信息 12。 */
.live-sec-t {
  font-size: var(--t-md); font-weight: 600;
  color: var(--ink); letter-spacing: 0;
  padding-bottom: var(--gap-inline);
}
/* 说明句：一句话讲清这块是什么，比正文略小但明显大于元信息 */
.live-sec > .hint {
  margin: 0; max-width: 76ch; line-height: 1.7;
  font-size: var(--t-base);
}
.live-sec > .hint b { color: var(--ink); font-weight: 600; }

/* 阶段按钮本身放大一点——它是上课时要反复点的东西，不是标签 */
#class-lesson-phase-btns .chip { font-size: var(--t-md); padding: var(--s-3) var(--s-4); }


/* ── 开课前的表单：别让控件通栏 ─────────────────────────────────
   实测出来的三个毛病：
   · 标签到控件只有 4px——等于粘在一起，读起来是一坨而不是"标签+控件"
   · 下拉框和「开始上课」都是 1030px 通栏，用来装一个课程名和四个字
   · 每个字段等距堆叠，没有分组，扫下来分不清哪些是一组

   控件宽度该由内容定，不该由容器定。 */
#class-idle { gap: var(--gap-group); }
#class-idle .field { margin-bottom: 0; }
#class-idle .field > span {
  display: block;
  margin-bottom: var(--gap-inline);   /* 标签和它的控件贴近，但不粘死 */
  font-size: var(--t-sm); color: var(--ink-2);
}
#class-idle select { max-width: 46ch; }
#class-open { align-self: flex-start; min-width: 9em; }
/* 开课按钮跟上面的选择项之间要有分界：那是"填完了，开始"的动作 */
#class-open {
  margin-top: var(--gap-tight);
}

/* ── 上课中：状态压成一条，别占满屏 ─────────────────────────────
   上课时老师是扫一眼，不是读文档。原来每块都是「标题＋两行说明＋内容」
   的等距长条，一屏一千多像素只承载"阶段、0/3、两个组名"。
   说明句收成一行、字号降下来；块间距从 48px 收到 32px。 */
#class-live { gap: var(--gap-group); }
.live-sec > .hint {
  font-size: var(--t-sm); color: var(--ink-3);
  line-height: 1.6; max-width: 68ch;
}
.live-sec-t { padding-bottom: 0; }
.live-sec { gap: var(--gap-tight); }

/* 阶段横幅收成一行：它是状态，不是内容 */
.class-banner { padding: var(--gap-tight) var(--gap-item); }
.class-banner b { font-size: var(--t-md); }

/* 各组一行一个组，信息靠右对齐，左边是组名——扫的时候只看左列 */
.class-row { padding: var(--gap-tight) 0; }


/* ── 字距：按字号套用，不逐处硬写 ───────────────────────────────
   之前正文一律 letter-spacing:normal（即 0），只有 h1/h2 各自写了一个
   不同的小数——没有系统。中文密排在 14px 以下尤其吃亏，加一点字距
   可读性提升明显；而 19px 以上的大字加了反而松散。 */
body { letter-spacing: var(--ls-base); }
.hint, .sub, .note, .meta, .badge, .tag,
.field > span, .t-stem, .live-stat i { letter-spacing: var(--ls-sm); }
h1, .live-stat b, .class-banner b { letter-spacing: var(--ls-lg); }

/* 行距：中文段落比西文更需要，尤其是这些一整段的说明句 */
.hint, .sub { line-height: 1.75; }


/* 面板标题不许换行。手机上「题目」被挤成"题/目"两行，因为它和旁边那行
   很长的说明句在同一个 flex 行里被压缩了。标题固定宽度，让说明去换行。 */
.panel-head h2, .board-head h2 { white-space: nowrap; flex: 0 0 auto; }
.panel-head .hint, .board-head .hint { min-width: 0; }

/* ═══ 界面反馈原语（配合 ui.js）═══════════════════════════════ */

/* ── 头像 ───────────────────────────────────────────────────────
   方的不是圆的：零圆角是这套设计的硬规矩，头像不破例。
   六种色调都是低饱和的，**刻意避开红**——红在这个界面里是"需要处理"，
   一个人的头像不该长得像一条待办。 */
:root {
  --av-0: #3f5f8a; --av-1: #4a6f5d; --av-2: #6b5a86;
  --av-3: #8a6a3f; --av-4: #40707a; --av-5: #6a6f7d;
  --av-6: #7a4f5e; --av-7: #4d6b3c;
  /* 最后两个留给固定角色：主持用墨蓝（跟主色同族，它是系统的声音），
     老师用最深的墨——老师说话在群聊里最有分量。 */
  --av-8: var(--accent); --av-9: var(--ink);
}
.avatar {
  display: inline-flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; flex: 0 0 auto;
  font-size: var(--t-sm); font-weight: 600; line-height: 1;
  color: #fff; letter-spacing: 0; user-select: none;
}
.avatar.small { width: 22px; height: 22px; font-size: var(--t-xs); }
.avatar.tone-0 { background: var(--av-0); } .avatar.tone-1 { background: var(--av-1); }
.avatar.tone-2 { background: var(--av-2); } .avatar.tone-3 { background: var(--av-3); }
.avatar.tone-4 { background: var(--av-4); } .avatar.tone-5 { background: var(--av-5); }
.avatar.tone-6 { background: var(--av-6); } .avatar.tone-7 { background: var(--av-7); }
.avatar.tone-8 { background: var(--av-8); } .avatar.tone-9 { background: var(--av-9); }

/* 助手的角标：右下角一个小缺角。颜色负责区分"是谁"，角标负责区分
   "人还是助手"——两个维度分开，不互相挤占。 */
.avatar.is-agent { position: relative; }
.avatar.is-agent::after {
  content: ""; position: absolute; right: 0; bottom: 0;
  border: 5px solid transparent;
  border-right-color: var(--paper); border-bottom-color: var(--paper);
}
.avatar.is-agent.small::after { border-width: 4px; }
/* 离线：不是变灰那么简单——降透明度会让白字看不清，改成褪色+描边 */
.avatar.offline { background: var(--paper-2); color: var(--ink-3);
                  box-shadow: inset 0 0 0 1px var(--rule-strong); }

/* ── 按钮忙碌态 ─────────────────────────────────────────────── */
.is-busy { position: relative; opacity: .78; cursor: progress; }
.is-busy::after {
  content: ""; position: absolute; right: 8px; top: 50%;
  width: 10px; height: 10px; margin-top: -5px;
  border: 2px solid currentColor; border-right-color: transparent;
  animation: spin .7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* ── 轻提示 ─────────────────────────────────────────────────── */
#toast-box {
  position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%);
  z-index: 200; display: flex; flex-direction: column; gap: var(--s-2);
  align-items: center; pointer-events: none;
}
.toast {
  background: var(--ink); color: var(--paper);
  padding: var(--s-3) var(--s-5); font-size: var(--t-sm);
  border-left: 3px solid var(--confirm);
  max-width: min(88vw, 460px);
  animation: toast-in .18s ease;
}
.toast.err { border-left-color: var(--alert); }
.toast.out { opacity: 0; transition: opacity .25s; }
@keyframes toast-in { from { opacity: 0; transform: translateY(6px); } }

/* ── 骨架占位 ───────────────────────────────────────────────── */
.skel {
  height: 14px; margin: var(--s-3) 0;
  background: linear-gradient(90deg, var(--paper-2) 25%, var(--rule) 50%, var(--paper-2) 75%);
  background-size: 200% 100%;
  animation: shimmer 1.3s linear infinite;
}
@keyframes shimmer { to { background-position: -200% 0; } }

/* ── 聊天室感 ───────────────────────────────────────────────── */
.gc-head-room {
  display: flex; align-items: center; gap: var(--gap-inline);
  flex-wrap: wrap; padding-bottom: var(--gap-tight);
  border-bottom: 1px solid var(--rule);
}
.gc-members { display: flex; gap: 4px; margin-left: var(--s-2); }
.gc-online { font-size: var(--t-sm); color: var(--ink-3); }
.gc-online b { color: var(--confirm); font-weight: 600; }

/* 消息行带头像 */
.gc-msg { display: grid; grid-template-columns: 28px 1fr; gap: var(--gap-inline); }
.gc-msg.system { grid-template-columns: 1fr; }
.gc-msg .who { grid-column: 2; }
.gc-msg .bubble { grid-column: 2; }
.gc-msg .avatar { grid-row: 1 / span 2; }

/* 正在输入 / 助手正在想 */
.gc-typing {
  display: flex; align-items: center; gap: var(--gap-inline);
  color: var(--ink-3); font-size: var(--t-sm);
  padding: var(--s-2) 0; min-height: 30px;
}
.dots i {
  display: inline-block; width: 4px; height: 4px; background: currentColor;
  margin-right: 3px; animation: blink 1.2s infinite;
}
.dots i:nth-child(2) { animation-delay: .2s; }
.dots i:nth-child(3) { animation-delay: .4s; }
@keyframes blink { 0%, 60%, 100% { opacity: .25; } 30% { opacity: 1; } }

/* 填空位：一条看得见的横线。原来题干里的空位（连续下划线或连续空格）
   渲染出来就是一片空白，学生不知道那儿是要填东西的。 */
.blank {
  display: inline-block; min-width: 4.5em; height: 1em;
  border-bottom: 1.5px solid var(--ink-2);
  margin: 0 .25em; vertical-align: -.15em;
}

/* 返回主页：演示时要在三个学生和教师端之间来回切，
   没有这个入口只能手改地址栏。 */
.home-link {
  display: inline-flex; align-items: center; gap: 2px;
  color: var(--ink-3); text-decoration: none;
  font-size: var(--t-sm); padding: var(--s-1) var(--s-2) var(--s-1) 0;
  margin-right: var(--s-2); flex: 0 0 auto; white-space: nowrap;
}
.home-link:hover { color: var(--accent); }
.home-link svg { width: 14px; height: 14px; }


/* 群聊进了栅格之后：自己撑满格子的高度，内部滚动，不再顶开别人 */
.group-chat { display: flex; flex-direction: column; min-height: 0; }
.group-chat-log { flex: 1 1 auto; min-height: 120px; }
@media (min-width: 900px) {
  /* 减去页头、课堂横幅和上下留白——上一版只减了 96px，结果整块 751px、
     占屏 83%，比没改之前还高。这些数是量出来的：横幅约 76px，
     .wrap 上下各 24px，再留一点余量。 */
  .group-chat, .chat-panel { max-height: calc(100vh - var(--header-h, 56px) - 190px); }
  .group-chat-log { max-height: none; }
}


/* 放在文件最末：`[hidden]{display:none}` 的优先级本来就低，写在前面会被
   后面任何一条设了 display 的规则盖掉。已经踩过两次——第一次是
   `#class-idle{display:flex}` 让开课前的面板压在实时面板上，第二次是
   `.wrap.layout{display:grid}` 让学生端两个模式视图同时显示。
   `!important` 加上位置最末，两道保险。 */
[hidden] { display: none !important; }


/* ── 滚动指示条 ─────────────────────────────────────────────────
   **不用 ::-webkit-scrollbar**：实测在这套环境里它完全不生效——
   把 width/track/thumb 都写上，量出来那一列像素仍是纯白 (255,255,255)，
   而且 overflow-y:scroll 和 scrollbar-gutter:stable 两种写法都一样。
   我第一次是"看图觉得有"就下了结论，其实一直没画出来。

   所以改成自己画：容器 position:relative，用一个绝对定位的 div 当滑块，
   由 JS 按 scrollTop 更新位置。它不依赖任何浏览器实现，一定看得见。 */
.scroll-host { position: relative; }
.scroll-rail {
  position: absolute; top: 0; right: 0; width: 10px; height: 100%;
  background: var(--paper-2); border-left: 1px solid var(--rule-strong);
  pointer-events: none;
}
.scroll-thumb {
  position: absolute; right: 2px; width: 6px; min-height: 28px;
  background: var(--ink-3);
  pointer-events: none;
}


/* 小组讨论的大标题。原来只有一行"A 组 · 第1题"的小字，跟旁边的
   「题目」「对话」两个面板不对等——那两个都有 h2，这块却没有，
   看起来像附属物而不是这个阶段的主场。 */
.gc-title {
  font-size: var(--t-md); font-weight: 600; margin: 0 0 var(--gap-tight);
  padding-bottom: var(--gap-tight); border-bottom: 1px solid var(--rule-strong);
}

/* 等待气泡：跟真回复区分开——它不是内容，是一个状态。
   淡一点、斜一点，配三个跳动的点。 */
.msg.pending .bubble.think {
  display: flex; align-items: center; gap: var(--s-2);
  color: var(--ink-3); font-style: italic;
  background: var(--paper-2); border-color: var(--rule);
}
.msg.pending .bubble.think .dots i { background: var(--ink-3); }


/* 没上课时"课上答疑"那一页的说明 */
.idle-note {
  border: 1px solid var(--rule); border-left: 3px solid var(--accent);
  padding: var(--gap-item); background: var(--paper-2);
}
.idle-note .sub { margin-top: var(--s-2); }
.idle-note a { color: var(--accent); }
/* 课上答疑**永远可用**：上课时跟着老师那节课，没上课就是复习这节课的
   内容。所以不再整块隐藏版面——原来那条规则把整个 .layout 藏了起来，
   等于告诉学生"下课了就别问了"，而课时语料一直都在。 */

/* 工单里 AI 那段对话摘录：完整显示、保留换行。
   原来前端硬截到 80 字，而老师正是要看 AI 之前完整说了什么，
   才知道该补哪一句、别重复。 */
.t-ctx-ai { white-space: pre-wrap; }


/* 教学系统助理的对话：原来既没有 overflow 也没有高度上限——它一直往下
   长，代码里那句 scrollTop = scrollHeight 自然什么也做不了。
   给个上限让它自己滚，面板才不会被越推越长。 */
#agent-transcript { max-height: 46vh; min-height: 120px; }
/* 老师走进小组时看到的群聊日志，同样要有上限——一节课下来几十条，
   不封顶的话教师端会被它推得很长。 */
#class-visit-log { max-height: 46vh; min-height: 120px; }

/* 工具条里的输入与下拉：撑满剩余宽度，按钮不被压扁。
   这几个控件原来靠内联 style 撑（flex:1;min-width:0），而那串内联样式
   里的 border 用的是 var(--line)——那个变量在改版时删掉了，于是边框成了
   无效值，输入框看起来根本没有边，谁也看不出那儿能打字。
   内联样式清掉之后统一在这里定，顺带回到全局的控件外观（零圆角、发丝线）。 */
.toolbar > input, .toolbar > select { flex: 1 1 auto; min-width: 0; }
.toolbar > button { flex: 0 0 auto; width: auto; }

/* 教学指引上方的原题：老师要对着题写，不能全凭记忆。
   压低存在感——它是参照物，不是主角。 */
.guide-stem {
  border-left: 3px solid var(--rule-strong);
  padding: var(--gap-tight) var(--gap-item);
  margin-bottom: var(--gap-item);
  background: var(--paper-2);
  font-size: var(--t-sm); color: var(--ink-2); line-height: 1.75;
}
.guide-stem:empty { display: none; }
.guide-stem-t { font-weight: 600; color: var(--ink); margin-bottom: var(--s-2); }

/* 「当前生效的设定」里每条右侧的删除 */
#memory-view li { display: flex; align-items: baseline; gap: var(--gap-inline); }
#memory-view li > span { flex: 1 1 auto; min-width: 0; }
.mem-del { flex: 0 0 auto; font-size: var(--t-xs); padding: 2px 8px; }
.pace-row { margin-top: var(--gap-tight); }

/* 可交互图板（[[图]] 标记渲染出来的） */
.diagram-box { margin: 0.75rem 0; }
.diagram-board {
  width: 100%; max-width: 520px; aspect-ratio: 4 / 3;
  border: 1px solid var(--rule); background: var(--paper);
}
.diagram-loading {
  font-size: var(--t-xs); letter-spacing: 0.08em; color: var(--rule-strong);
  padding: 0.75rem 0;
}
.diagram-caption {
  font-size: var(--t-xs); letter-spacing: 0.06em; color: var(--rule-strong);
  padding: 0.4rem 0 0; max-width: 520px;
}

/* 指引的适用范围标记：全程 vs 只在某一步。两者对 AI 的行为差别很大，
   必须一眼可辨，不能只靠读文字。 */
.guide-scope {
  margin-left: .5rem;
  font-size: .78rem;
  color: var(--muted);
  white-space: nowrap;
}


/* ── 判作业 ─────────────────────────────────────────────────────────
   沿用这一页的视觉语言，不另起一套：发丝线分区、零圆角、状态必须上色。
   颜色语义照旧——红 = 老师要动手（没认出来、错、拿不准、结果过期）；
   绿 = 确认过了（对、已改判）；墨蓝只给可点的东西和进行中的状态。 */

/* 批改开始后会滚到第 3 步；页头和锚点导航都是 sticky，不留余量标题会被盖住 */
#sec-grading, .gr-step { scroll-margin-top: calc(var(--header-h, 52px) + 56px); }

.gr-step, #grading-assign-body { display: flex; flex-direction: column; gap: var(--gap-item); }
.gr-result { display: flex; flex-direction: column; gap: var(--gap-group); }
#grading-summary, #grading-review, #grading-students { display: flex; flex-direction: column; gap: var(--gap-tight); }
.gr-step > *, #grading-assign-body > *, .gr-result > *,
#grading-summary > *, #grading-review > *, #grading-students > * { margin: 0; }

/* 步骤标题：跟课堂面板的小节标题同一档（16px/600 + 发丝线），编号是方框不是圆 */
.gr-step-t {
  display: flex; align-items: center; gap: var(--gap-inline);
  font-size: var(--t-md); font-weight: 600; letter-spacing: var(--ls-md);
  padding-bottom: var(--gap-inline); border-bottom: 1px solid var(--rule);
}
.gr-step-n {
  display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; flex: none;
  border: 1px solid var(--ink);
  font-family: var(--font-num); font-size: var(--t-sm); letter-spacing: 0;
}
/* 还没走到的步骤：标题和编号一起退成灰，扫一眼就知道自己在第几步 */
.gr-step:has(> .hint[id$="-empty"]:not([hidden])) .gr-step-t { color: var(--ink-3); }
.gr-step:has(> .hint[id$="-empty"]:not([hidden])) .gr-step-n { border-color: var(--rule-strong); }

/* 选照片是 <label>（里面藏着 file input），禁用要靠类名，label 没有 disabled */
.gr-pick { cursor: pointer; }
.gr-pick.is-disabled { opacity: .4; cursor: default; pointer-events: none; }

/* 一张照片一格。132px 起：手机上两列，桌面一行六七张 */
.gr-tiles { display: grid; gap: var(--s-3); grid-template-columns: repeat(auto-fill, minmax(132px, 1fr)); }
.gr-tiles:empty { display: none; }
.gr-tile {
  display: flex; flex-direction: column; gap: var(--s-2); min-width: 0;
  border: 1px solid var(--rule); padding: var(--s-2); background: var(--paper);
}
.gr-tile.is-failed, .gr-tile.is-todo { border-left: 3px solid var(--alert); }
/* contain 不是 cover：老师要看的正是卷头那一角，裁掉就白看了 */
.gr-thumb {
  display: block; width: 100%; aspect-ratio: 3 / 4; object-fit: contain;
  background: var(--paper-2); border: 1px solid var(--rule);
}
.gr-file {
  font-size: var(--t-xs); color: var(--ink-3); letter-spacing: var(--ls-sm);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.gr-reason { font-size: var(--t-xs); color: var(--ink-2); line-height: 1.6; letter-spacing: var(--ls-sm); }
.gr-via { align-self: flex-start; }
/* 格子里的下拉框：右侧留给箭头的空位从 32px 收到 24px，名字才放得下。
   聚焦态照全局的做法把边框加粗、内边距减 1px，免得控件跳一下 */
.gr-tile select {
  padding: var(--s-1) var(--s-5) var(--s-1) var(--s-2);
  background-position: right 12px center, right 7px center;
}
.gr-tile select:focus {
  padding: calc(var(--s-1) - 1px) calc(var(--s-5) - 1px) calc(var(--s-1) - 1px) calc(var(--s-2) - 1px);
}
/* 字号只在桌面收小：手机上输入控件必须 ≥16px，否则 iOS 聚焦时会放大页面 */
@media (min-width: 861px) { .gr-tile select { font-size: var(--t-base); } }
.gr-mini { align-self: flex-start; padding: 2px var(--s-3); font-size: var(--t-sm); }

/* 按学生分组 */
.gr-groups { display: flex; flex-direction: column; gap: var(--gap-tight); }
.gr-group { border: 1px solid var(--rule); }
.gr-group-head {
  display: flex; align-items: baseline; gap: var(--gap-inline); flex-wrap: wrap;
  padding: var(--s-2) var(--s-3); background: var(--paper-2);
}
.gr-group-head:not(:last-child) { border-bottom: 1px solid var(--rule); }
.gr-group > .gr-tiles { padding: var(--s-3); }
.gr-count-note { margin-left: auto; font-size: var(--t-sm); color: var(--ink-3); }
.gr-sub { font-size: var(--t-sm); color: var(--ink-3); letter-spacing: var(--ls-sm); }
/* 「没认出来」排第一；里面有照片时整组上朱红——这是这一步唯一必须动手的地方。
   空的时候退成灰，免得一片红让人以为出了问题。 */
.gr-unknown.has-todo { border-color: var(--alert); border-left-width: 3px; }
.gr-unknown.has-todo > .gr-group-head { background: var(--alert-wash); border-bottom-color: var(--alert); }
.gr-unknown.has-todo > .gr-group-head b,
.gr-unknown.has-todo .gr-count-note { color: var(--alert); font-weight: 600; }
.gr-unknown:not(.has-todo) > .gr-group-head b { color: var(--ink-3); font-weight: 400; }
.gr-actions {
  display: flex; align-items: center; gap: var(--gap-inline); flex-wrap: wrap;
  padding-top: var(--gap-item); border-top: 1px solid var(--rule);
}

/* 批改进度：进行中的状态，用主色而不是红——这时候还不需要老师做什么 */
.gr-progress {
  display: flex; align-items: center; gap: var(--gap-inline); flex-wrap: wrap;
  padding: var(--s-3) var(--s-4); font-size: var(--t-sm); color: var(--ink-2);
  border: 1px solid var(--rule); border-left: 3px solid var(--accent); background: var(--accent-wash);
}
.gr-progress-n { font-family: var(--font-num); color: var(--accent); font-weight: 600; }
.gr-progress .dots i { background: var(--accent); }

/* 结果过期：红框，因为它要老师点一下重新批改 */
.gr-stale {
  display: flex; flex-direction: column; align-items: flex-start; gap: var(--s-2);
  padding: var(--s-3) var(--s-4); color: var(--alert);
  border: 1px solid var(--alert); border-left-width: 3px; background: var(--alert-wash);
}
.gr-stale-note { font-size: var(--t-sm); color: var(--alert); padding: var(--s-2) 0; }

/* 结果区的小节标题：比步骤标题低一档 */
.gr-sec-t {
  display: flex; align-items: center; gap: var(--gap-inline);
  font-size: var(--t-base); font-weight: 600;
  padding-bottom: var(--s-2); border-bottom: 1px solid var(--rule-strong);
}
.gr-sub-t { font-size: var(--t-sm); font-weight: 600; color: var(--ink-2); padding-bottom: var(--s-1); }

/* 汇总表：题 × 结论。在自己的容器里横向滚，手机上不顶宽页面 */
.gr-table-wrap { overflow-x: auto; border: 1px solid var(--rule); }
.gr-table { width: 100%; border-collapse: collapse; font-size: var(--t-sm); }
.gr-table th, .gr-table td {
  padding: var(--s-1) var(--s-2); text-align: center; white-space: nowrap;
  border-bottom: 1px solid var(--rule);
}
.gr-table thead th { background: var(--paper-2); font-weight: 600; border-bottom-color: var(--rule-strong); }
.gr-table tbody th { text-align: left; font-weight: 500; color: var(--ink); }
.gr-table tbody tr:last-child > * { border-bottom: none; }
/* 数字本身是按钮（点开是哪几位学生），但长得像表格里的数，不像一排蓝按钮 */
.gr-count {
  justify-content: center; min-width: 2.6em; padding: 1px var(--s-2);
  font-family: var(--font-num); font-size: var(--t-sm);
  background: transparent; border: 1px solid transparent; color: var(--ink);
}
.gr-count:hover { background: var(--paper-2); border-color: var(--rule-strong); }
.gr-count.is-open, .gr-count.is-open:hover { background: var(--accent-wash); border-color: var(--accent); }
.gr-zero { color: var(--rule-strong); }
.gr-names td { text-align: left; white-space: normal; background: var(--paper-2); line-height: 1.7; }

.gr-stuck { display: flex; flex-direction: column; }
.gr-stuck-row {
  display: flex; align-items: baseline; gap: var(--gap-inline); flex-wrap: wrap;
  padding: var(--s-1) 0; border-bottom: 1px solid var(--rule);
}
.gr-stuck-row:last-child { border-bottom: none; }
.gr-stuck-n { font-family: var(--font-num); font-size: var(--t-sm); font-weight: 600; color: var(--alert); }

/* 待复核：一条一张卡，左边线红=等你看，绿=已改判 */
.gr-review { display: flex; flex-direction: column; gap: var(--gap-tight); }
.gr-review-item {
  display: flex; flex-direction: column; gap: var(--s-2);
  padding: var(--s-3) var(--s-4);
  border: 1px solid var(--rule); border-left: 3px solid var(--alert);
}
.gr-review-item.is-done { border-left-color: var(--confirm); background: var(--paper-2); }
.gr-review-head { display: flex; align-items: baseline; gap: var(--gap-inline); flex-wrap: wrap; }
.gr-answer {
  display: flex; align-items: baseline; gap: var(--gap-inline);
  border-left: 2px solid var(--rule-strong); padding-left: var(--s-3);
}
.gr-answer-k { flex: none; font-size: var(--t-xs); color: var(--ink-3); letter-spacing: var(--ls-sm); }
.gr-answer-v { min-width: 0; overflow-x: auto; font-size: var(--t-md); }
.gr-muted { color: var(--ink-3); font-size: var(--t-sm); }
.gr-parts { margin: 0; padding-left: 1.2em; font-size: var(--t-sm); }
.gr-parts li { margin-bottom: var(--s-1); }
.gr-why { color: var(--ink-2); }
.gr-why .rich-p { margin: 0; }
.gr-override { display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap; }
.gr-override .is-chosen, .gr-override .is-chosen:hover {
  color: var(--confirm); border-color: var(--confirm); background: var(--confirm-wash); font-weight: 600;
}
.gr-problem-body {
  display: flex; flex-direction: column; gap: var(--s-1);
  padding: var(--s-2) var(--s-3); font-size: var(--t-sm);
  border-left: 3px solid var(--rule-strong); background: var(--paper-2);
}
.gr-problem-k { font-weight: 600; color: var(--ink); margin-top: var(--s-1); }

/* 逐个学生：一人一行，点开是逐题结论 */
.gr-student-list { display: flex; flex-direction: column; }
.gr-student { border-bottom: 1px solid var(--rule); }
.gr-student > summary {
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--gap-inline);
  width: 100%; padding: var(--s-2) 0; color: var(--ink); font-size: var(--t-base);
}
.gr-student > summary:hover { color: var(--accent); text-decoration: none; }
.gr-student.is-stale > summary b { color: var(--ink-3); }
.gr-chip {
  display: inline-flex; align-items: center; padding: 0 var(--s-2);
  font-size: var(--t-xs); white-space: nowrap; border: 1px solid var(--rule-strong);
}
.gr-student-body { display: flex; flex-direction: column; padding: 0 0 var(--s-3) var(--s-4); }
.gr-student-empty { padding: var(--s-2) 0; font-size: var(--t-sm); color: var(--ink-3); border-bottom: 1px solid var(--rule); }
.gr-row {
  display: grid; grid-template-columns: 4.5em minmax(8em, 16em) minmax(0, 1fr) auto;
  gap: var(--gap-inline); align-items: baseline;
  padding: var(--s-1) 0; border-bottom: 1px solid var(--rule); font-size: var(--t-sm);
}
.gr-row:last-child { border-bottom: none; }
.gr-num { color: var(--ink-2); white-space: nowrap; }
.gr-label { display: inline-flex; align-items: baseline; flex-wrap: wrap; gap: var(--s-1); }
.gr-flag { font-weight: 400; }
.gr-row-ans { min-width: 0; overflow-x: auto; color: var(--ink); }
.gr-row-ans .rich-p { margin: 0; }
.gr-row-more > summary { padding: 0; }
.gr-row-more[open] { grid-column: 1 / -1; }
@media (max-width: 640px) {
  .gr-row { grid-template-columns: 4.5em minmax(0, 1fr); }
  .gr-row-ans, .gr-row-more { grid-column: 2; }
}

/* 结论的颜色（放在最后，压过 .tag / .gr-count 自带的中性色） */
.v-ok { color: var(--confirm); }
.v-partial, .v-wrong { color: var(--alert); }
.v-unsure, .v-manual { color: var(--alert); font-weight: 600; }
.v-blank { color: var(--ink-3); }
.tag.v-ok, .gr-chip.v-ok { border-color: var(--confirm); background: var(--confirm-wash); }
.tag.v-partial, .tag.v-wrong, .gr-chip.v-partial, .gr-chip.v-wrong { border-color: var(--alert); }
.tag.v-unsure, .tag.v-manual, .gr-chip.v-unsure, .gr-chip.v-manual { border-color: var(--alert); background: var(--alert-wash); }
.gr-chip.v-blank { border-color: var(--rule); }

@media (pointer: coarse) {
  .gr-count, .gr-mini { min-height: 40px; }
}
