/* Tailwind CSS CDN */
@import url('https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css');

:root {
  --primary: #4F46E5;
  --primary-2: #6366F1;
  --primary-3: #8B5CF6;
  --bg: #F7F8FA;
  --bg-2: #EEF0F6;
  --surface: #FFFFFF;
  --text: #1F2430;
  --text-2: #5B6478;
  --ok: #10B981;
  --danger: #EF4444;
  --warn: #F59E0B;

  /* 学生画像：红黄绿信号色 */
  --light-red: #EF4444;
  --light-yellow: #F5A524;
  --light-green: #10B981;
  --light-red-soft: rgba(239, 68, 68, .12);
  --light-yellow-soft: rgba(245, 165, 36, .14);
  --light-green-soft: rgba(16, 185, 129, .12);

  --radius-lg: 20px;
  --radius-md: 14px;
  --radius-sm: 10px;
  --shadow-sm: 0 2px 8px rgba(31, 36, 48, .06);
  --shadow-md: 0 10px 30px rgba(79, 70, 229, .12);
  --shadow-lg: 0 18px 45px rgba(79, 70, 229, .18);
  --grad: linear-gradient(135deg, var(--primary) 0%, var(--primary-2) 55%, var(--primary-3) 100%);

  /* 彩虹霓虹色停：只用在「正在跑 / 正在为你服务 / 等待中」的**装饰流光与光环**上 ——
     它不是状态色（状态色仍是 橙=在跑 / 绿=完成 / 红=失败），别拿它去表达状态。
     放在 :root 是刻意的：步骤条（.steps-agent）与智能体状态灯（.slot-light.is-mine）
     共用**同一份**取色，抄成两份迟早走偏。 */
  --neon-band: linear-gradient(
    100deg,
    rgba(255, 45, 149, 0) 6%, rgba(255, 45, 149, .42) 17%,
    rgba(255, 193, 7, .40) 30%, rgba(52, 255, 158, .38) 44%,
    rgba(41, 216, 255, .42) 58%, rgba(168, 85, 247, .40) 72%,
    rgba(255, 45, 149, .42) 86%, rgba(255, 45, 149, 0) 96%
  );
  --neon-ring: #FF2D95, #FFC107, #34FF9E, #29D8FF, #A855F7, #FF2D95;
}

* { box-sizing: border-box; }

/* `[hidden]` 必须压得住组件自己的 display。
   浏览器默认的 `[hidden] { display: none }` 优先级极低，任何 `.xxx { display: flex }`
   都能把它盖掉 —— 那样 el.hidden = true 的元素**其实还留在页面上**（例如
   「任务结束后才浮现」的续聊输入框，一直可见，老师就会在任务跑到一半时点发送）。 */
[hidden] { display: none !important; }

body {
  margin: 0;
  min-height: 100vh;
  background:
    radial-gradient(1200px 500px at 12% -8%, rgba(139, 92, 246, .14), transparent 60%),
    radial-gradient(900px 420px at 92% 0%, rgba(99, 102, 241, .12), transparent 62%),
    var(--bg);
  color: var(--text);
  font-family: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", system-ui, -apple-system, sans-serif;
  font-size: 15px;
  line-height: 1.6;
}

h1, h2, h3 { margin: 0; font-weight: 600; }
h2 { font-size: 18px; }
.muted { color: var(--text-2); font-size: 13px; }

/* ---------- 顶栏 ---------- */
.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 18px 28px;
  background: rgba(255, 255, 255, .78);
  backdrop-filter: blur(14px);
  border-bottom: 1px solid rgba(31, 36, 48, .06);
  position: sticky;
  top: 0;
  z-index: 20;
}
.brand { display: flex; align-items: center; gap: 12px; }
.brand-mark {
  width: 40px; height: 40px;
  border-radius: 12px;
  background: var(--grad);
  color: #fff;
  display: grid; place-items: center;
  font-size: 20px; font-weight: 700;
  box-shadow: var(--shadow-md);
}
.brand-text { display: flex; flex-direction: column; line-height: 1.25; }
.brand-text strong { font-size: 17px; }
.brand-text small { color: var(--text-2); font-size: 12px; }

.ta-badge {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 14px;
  border-radius: 999px;
  background: var(--surface);
  box-shadow: var(--shadow-sm);
  font-size: 13px;
}
.ta-badge .dot {
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--warn);
  transition: background .25s ease;
}
.ta-badge.is-ok .dot { background: var(--ok); }

/* 顶栏右侧操作区（当前身份 + 退出登录；单人自用 autoLogin 时退出按钮由 app.js 隐藏） */
.topbar-actions { display: flex; align-items: center; gap: 10px; }
.btn-sm { padding: 8px 14px; font-size: 13px; margin-top: 0; }
.ta-logout { white-space: nowrap; }

/* ---------- Tab ---------- */
.tabs {
  display: flex; gap: 6px;
  padding: 14px 28px 0;
  border-bottom: 1px solid rgba(31, 36, 48, .07);
}
.tab {
  position: relative;
  appearance: none; border: 0; background: transparent;
  padding: 10px 18px 14px;
  font-size: 15px; font-weight: 500;
  color: var(--text-2);
  cursor: pointer;
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
  transition: color .2s ease, background .2s ease;
}
.tab:hover { color: var(--text); background: rgba(99, 102, 241, .06); }
.tab.is-active { color: var(--primary); }
.tab.is-active::after {
  content: ""; position: absolute; left: 18px; right: 18px; bottom: 0; height: 3px;
  border-radius: 3px; background: var(--grad);
}
.tab.is-locked { cursor: not-allowed; opacity: .62; }
.tab .tag {
  margin-left: 6px; font-size: 11px; font-weight: 400;
  padding: 2px 7px; border-radius: 999px;
  background: var(--bg-2); color: var(--text-2);
}

/* ---------- 主体 ---------- */
.main { padding: 22px 28px 96px; max-width: 1240px; margin: 0 auto; }
.panel { display: none; animation: fade-up .35s ease both; }
.panel.is-active { display: block; }

@keyframes fade-up {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: none; }
}

/* ---------- 步骤条 ---------- */
.steps {
  display: flex; align-items: center; gap: 10px;
  list-style: none; margin: 0 0 20px; padding: 0;
}
.step {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 16px; border-radius: 999px;
  background: var(--surface); box-shadow: var(--shadow-sm);
  color: var(--text-2); font-size: 14px;
  transition: all .28s ease;
}
.step-no {
  width: 22px; height: 22px; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--bg-2); color: var(--text-2); font-size: 12px; font-weight: 600;
}
.step.is-current {
  color: var(--primary);
  box-shadow: var(--shadow-md);
  animation: pulse 2.2s ease-in-out infinite;
}
.step.is-current .step-no { background: var(--grad); color: #fff; }
.step.is-done { color: var(--ok); }
.step.is-done .step-no { background: var(--ok); color: #fff; }
.step + .step::before {
  content: ""; width: 26px; height: 2px; margin-right: 6px;
  background: var(--bg-2); border-radius: 2px;
}
@keyframes pulse {
  0%, 100% { box-shadow: var(--shadow-md); }
  50% { box-shadow: 0 0 0 6px rgba(99, 102, 241, .12); }
}

/* ---------- 布局 ---------- */
.layout { display: grid; grid-template-columns: 5fr 7fr; gap: 20px; align-items: start; }
.card {
  background: var(--surface);
  border-radius: var(--radius-lg);
  padding: 20px;
  box-shadow: var(--shadow-sm);
  border: 1px solid rgba(31, 36, 48, .05);
}
.card-head { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 12px; }
.state-line { font-size: 13px; color: var(--text-2); min-height: 20px; }
.state-line.is-error { color: var(--danger); }

/* ---------- 作业提交情况（云端口径，每次实时拉） ----------
   为什么单独一套类名：它既不是"班级选择"也不是"执行流"，是一块只读的统计视图，
   样式上要能一眼区分"总览（每个作业一行）"与"展开后的逐人名单"。 */
.hw-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-bottom: 10px; }
.hw-bar .field-label { margin: 0; }
.hw-select {
  font: inherit; font-size: 13px; color: var(--text);
  padding: 6px 10px; border-radius: var(--radius-sm);
  border: 1.5px solid rgba(31, 36, 48, .1); background: var(--surface);
  max-width: min(320px, 100%);
}
.hw-select:focus-visible { outline: 2px solid rgba(99, 102, 241, .35); outline-offset: 1px; }
.hw-list { display: flex; flex-direction: column; gap: 8px; margin-top: 10px; }
.hw-item {
  border: 1px solid rgba(31, 36, 48, .07); border-radius: var(--radius-sm);
  background: var(--bg); overflow: hidden;
}
.hw-head {
  display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between;
  gap: 6px 12px; width: 100%; padding: 10px 12px;
  font: inherit; text-align: left; background: none; border: 0; cursor: pointer;
}
.hw-head:hover { background: #F2F4FB; }
.hw-head:focus-visible { outline: 2px solid rgba(99, 102, 241, .35); outline-offset: -2px; }
.hw-title { font-size: 13.5px; font-weight: 500; }
.hw-stats { font-size: 12.5px; color: var(--text-2); }
.hw-stats .is-missing { color: var(--danger); font-weight: 600; }
.hw-roster { padding: 10px 12px 12px; border-top: 1px dashed rgba(31, 36, 48, .1); }
.hw-group + .hw-group { margin-top: 10px; }
.hw-group-head { font-size: 12px; color: var(--text-2); margin-bottom: 6px; }
.hw-group-head.is-missing { color: var(--danger); }
.hw-names { display: flex; flex-wrap: wrap; gap: 6px; }
.hw-name {
  font-size: 12.5px; padding: 2px 9px; border-radius: 999px;
  background: var(--surface); border: 1px solid rgba(31, 36, 48, .08);
}
.hw-name.is-missing { border-color: rgba(239, 68, 68, .35); background: var(--light-red-soft); }

/* 「显示全部（含未开放入口）」开关：默认不勾，且只在"两类作业都有"时才由 JS 显示出来 */
.hw-toggle {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 12.5px; color: var(--text-2); cursor: pointer; user-select: none;
}
.hw-toggle input { margin: 0; accent-color: var(--primary); }
.hw-toggle:hover { color: var(--text); }

/* 分组小标题：只在「显示全部」时出现，把"能交的"与"仅可查看的"分开，别让老师混着看 */
.hw-section-title { margin-top: 4px; font-size: 12px; color: var(--text-2); }

/* 没有提交入口的作业：**整行压暗、去掉可点击的暗示**（点了也交不了，装成能点更糟）。
   用中性色而不是红色：它不是"出错了"，只是"本系统没有这个作业的入口"。 */
.hw-item.is-readonly { border-style: dashed; }
.hw-item.is-readonly .hw-head { cursor: default; }
.hw-item.is-readonly .hw-head:hover { background: none; }
.hw-item.is-readonly .hw-title { color: var(--text-2); font-weight: 400; }
.hw-pill {
  display: inline-block; margin-left: 8px; padding: 1px 7px; vertical-align: 1px;
  font-size: 11px; color: var(--text-2); border-radius: 999px;
  background: var(--surface); border: 1px solid rgba(31, 36, 48, .1);
}
.hw-readonly-note { padding: 0 12px 10px; font-size: 12px; color: var(--text-2); }

/* ---------- 班级 ---------- */
.class-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 12px; }
.class-item {
  position: relative;
  text-align: left;
  padding: 14px 16px;
  border-radius: var(--radius-md);
  border: 1.5px solid rgba(31, 36, 48, .08);
  background: var(--surface);
  cursor: pointer;
  transition: transform .22s ease, box-shadow .22s ease, border-color .22s ease;
}
.class-item:hover { transform: translateY(-3px); box-shadow: var(--shadow-md); }
.class-item strong { display: block; font-size: 15px; font-weight: 600; }
.class-item span { font-size: 12px; color: var(--text-2); }
.class-item.is-selected {
  border-color: transparent;
  background:
    linear-gradient(#fff, #fff) padding-box,
    var(--grad) border-box;
  box-shadow: var(--shadow-lg);
}
.class-item.is-selected::after {
  content: "✓"; position: absolute; top: 10px; right: 12px;
  color: var(--primary); font-weight: 700;
}

/* ---------- 课次 ---------- */
.lesson-block { margin-top: 22px; animation: fade-up .3s ease both; }
.lesson-list { display: flex; flex-direction: column; gap: 8px; max-height: 340px; overflow: auto; }
.lesson-item {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 14px; border-radius: var(--radius-md);
  border: 1px solid rgba(31, 36, 48, .07);
  background: var(--surface); cursor: pointer;
  transition: background .2s ease, transform .2s ease;
}
.lesson-item:hover { background: #F5F6FF; transform: translateX(3px); }
.lesson-item.is-selected { background: #F1F2FF; box-shadow: inset 4px 0 0 var(--primary); }
.lesson-index {
  width: 28px; height: 28px; flex: none;
  border-radius: 8px; display: grid; place-items: center;
  background: var(--bg-2); color: var(--text-2); font-size: 12px; font-weight: 600;
}
.lesson-item.is-selected .lesson-index { background: var(--grad); color: #fff; }
.lesson-main { flex: 1; min-width: 0; }
.lesson-main strong { display: block; font-size: 14px; font-weight: 500; }
.lesson-main span { font-size: 12px; color: var(--text-2); }
.lesson-date {
  font-size: 12px; padding: 3px 9px; border-radius: 999px;
  background: var(--bg-2); color: var(--text-2); white-space: nowrap;
}
.lesson-date.is-soon { background: rgba(16, 185, 129, .12); color: var(--ok); }

/* ---------- 上传 ---------- */
.dropzone {
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px;
  padding: 34px 20px;
  border: 2px dashed rgba(99, 102, 241, .35);
  border-radius: var(--radius-lg);
  background: linear-gradient(180deg, rgba(99, 102, 241, .04), rgba(255, 255, 255, 0));
  cursor: pointer; text-align: center;
  transition: border-color .2s ease, background .2s ease, transform .2s ease;
}
.dropzone:hover, .dropzone.is-over { border-color: var(--primary-2); background: rgba(99, 102, 241, .08); }
.dz-icon {
  width: 46px; height: 46px; border-radius: 14px;
  background: var(--grad); color: #fff; font-size: 24px;
  display: grid; place-items: center; box-shadow: var(--shadow-md);
}
.dz-title { font-size: 15px; font-weight: 500; }
.dz-sub { font-size: 12px; color: var(--text-2); }

/* 待上传照片清单：一次可选多张（2026-10-26 起），一行一张缩略图 + 移除 */
.preview {
  display: flex; flex-direction: column; align-items: stretch;
  gap: 10px; margin-top: 16px;
}
.preview-list { display: flex; flex-direction: column; gap: 8px; max-height: 260px; overflow-y: auto; }
.preview-item {
  display: flex; align-items: center; gap: 12px;
  padding: 8px 10px; border-radius: var(--radius-md);
  background: var(--bg-2); border: 1px solid rgba(31, 36, 48, .06);
}
.preview img {
  width: 56px; height: 56px; object-fit: cover; flex: 0 0 auto;
  border-radius: var(--radius-md); box-shadow: var(--shadow-sm);
}
.preview-meta { flex: 1; min-width: 0; }
.preview-meta strong { display: block; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.preview-item .btn { flex: 0 0 auto; }
/* 「全部移除」贴着左下角，不被 flex 拉成整行宽 */
.preview > .btn { align-self: flex-start; }

.btn {
  appearance: none; border: 0; cursor: pointer;
  border-radius: var(--radius-md);
  padding: 12px 20px; font-size: 15px; font-weight: 500;
  transition: transform .18s ease, box-shadow .18s ease, opacity .18s ease;
}
.btn:disabled { opacity: .5; cursor: not-allowed; }
.btn-primary {
  width: 100%; margin-top: 16px;
  background: var(--grad); color: #fff;
  box-shadow: var(--shadow-md);
}
.btn-primary:not(:disabled):hover { transform: translateY(-2px); box-shadow: var(--shadow-lg); }
.btn-ghost { background: var(--bg-2); color: var(--text); padding: 10px 16px; font-size: 14px; }
.btn-ghost:not(:disabled):hover { background: #E4E7F2; }

.btn.is-loading { position: relative; color: transparent; }
.btn.is-loading::after {
  content: ""; position: absolute; inset: 0; margin: auto;
  width: 18px; height: 18px; border-radius: 50%;
  border: 2px solid rgba(255, 255, 255, .45); border-top-color: #fff;
  animation: spin .8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

.progress { display: flex; align-items: center; gap: 10px; margin-top: 14px; }
.progress-bar { flex: 1; height: 8px; border-radius: 999px; background: var(--bg-2); overflow: hidden; }
.progress-bar i { display: block; height: 100%; width: 0; border-radius: 999px; background: var(--grad); transition: width .2s ease; }

.alert {
  margin-top: 14px; padding: 12px 14px;
  border-radius: var(--radius-md);
  background: rgba(239, 68, 68, .08);
  border: 1px solid rgba(239, 68, 68, .22);
  color: var(--danger); font-size: 13px;
}

.result { margin-top: 18px; animation: pop .32s ease both; }
@keyframes pop {
  from { opacity: 0; transform: scale(.96); }
  to { opacity: 1; transform: none; }
}
.result-head { display: flex; align-items: center; gap: 8px; font-weight: 600; color: var(--ok); }
.result.is-warn .result-head { color: #8A5A08; }
.result.is-fail .result-head { color: var(--danger); }
.result-badge {
  width: 22px; height: 22px; border-radius: 50%; flex: none;
  background: var(--ok); color: #fff; display: grid; place-items: center; font-size: 13px;
}
.result.is-warn .result-badge { background: #B45309; }
.result.is-fail .result-badge { background: var(--danger); }

/* 提交报告：图和表**各一行**（哪一步成了、存到哪、有没有取代旧批次）。
   `is-stale` 是"这份报告之后表格又改过"那句 —— 与其余几种共用同一套配色。 */
.result-lines { display: grid; gap: 6px; margin-top: 10px; }
.result-line {
  display: flex; align-items: flex-start; gap: 8px;
  padding: 9px 12px; border-radius: var(--radius-sm);
  background: var(--bg-2); color: var(--text-2);
  font-size: 12.5px; line-height: 1.55; word-break: break-all;
}
.result-line b { flex: none; font-weight: 700; }
.result-line.is-ok { background: rgba(16, 185, 129, .09); color: #0B7A5B; }
.result-line.is-warn { background: rgba(245, 158, 11, .16); color: #92400E; }
.result-line.is-fail { background: rgba(239, 68, 68, .09); color: #B91C1C; }

.result img {
  width: 100%; max-height: 260px; object-fit: cover;
  border-radius: var(--radius-md); margin: 12px 0; box-shadow: var(--shadow-sm);
}
.link-row { display: flex; gap: 8px; }
.link-row input {
  flex: 1; min-width: 0;
  padding: 10px 12px; border-radius: var(--radius-sm);
  border: 1px solid rgba(31, 36, 48, .12);
  font-size: 12px; color: var(--text-2); background: #FBFCFE;
}

/* ---------- 占位页 ---------- */
.placeholder-card {
  background: var(--surface); border-radius: var(--radius-lg);
  padding: 60px 20px; text-align: center;
  box-shadow: var(--shadow-sm); border: 1px solid rgba(31, 36, 48, .05);
}
.ph-icon { font-size: 40px; margin-bottom: 10px; }

/* ---------- 底部状态栏 ---------- */
.statusbar {
  position: fixed; left: 0; right: 0; bottom: 0;
  display: flex; gap: 18px; align-items: center;
  padding: 10px 28px;
  background: rgba(255, 255, 255, .9);
  backdrop-filter: blur(12px);
  border-top: 1px solid rgba(31, 36, 48, .07);
  font-size: 12px; color: var(--text-2);
  z-index: 15;
}

/* ==================================================================
   作业 tab：Codex 自动化编排（样式沿用上面的设计 token，只增不改）
   ================================================================== */

.mt-lg { margin-top: 22px; }
.stack { display: flex; flex-direction: column; gap: 20px; }

/* ---------- 表单 ---------- */
.field { margin-bottom: 18px; }
.field-label { display: block; font-size: 13px; color: var(--text-2); margin-bottom: 8px; }
.field input {
  width: 100%; padding: 10px 12px; font-size: 14px;
  border-radius: var(--radius-sm);
  border: 1px solid rgba(31, 36, 48, .12);
  background: #FBFCFE; color: var(--text);
}
.field input:focus { outline: 2px solid rgba(99, 102, 241, .25); border-color: var(--primary-2); }

/* ---------- 作业页的子 tab：**下划线式**，与主 tab 栏"延伸联动"（2026-10-12） ----------
   为什么不再是"胶囊 + 渐变底"：老师要的是"由作业 tab 栏延伸到子 tab"的观感 —— 所以子 tab 改用
   与主 tab（`.tab.is-active::after`）**同一套下划线语言**：同样 3px 厚、同样圆角、同一条 `var(--grad)`，
   栏体再**贴住**主 tab 栏下沿（负 margin 吃掉 `.main` 的上内边距）—— 两块看起来就是同一根线延伸下来的。
   选中项之间的切换由 `#hwSubtabInd` 滑块滑过去（只动 transform / width ⇒ 不触发重排，才够顺）。 */
.hw-subtabs {
  position: relative;
  display: flex; align-items: flex-end; gap: 0;
  margin: -22px 0 14px;               /* 贴到主 tab 栏下沿（`.main` 的上内边距是 22px） */
  border-bottom: 1px solid rgba(31, 36, 48, .07);
}
.hw-subtab {
  appearance: none; border: 0; cursor: pointer;
  display: inline-flex; align-items: center; gap: 6px;
  padding: 10px 2px 12px; margin-right: 22px;
  font-size: 14.5px; font-weight: 500; color: var(--text-2); background: transparent;
  transition: color .2s ease;
}
.hw-subtab:last-of-type { margin-right: 0; }
.hw-subtab:hover { color: var(--text); }
.hw-subtab.is-active { color: var(--primary); }
/* 滑动下划线：位置与宽度由 syncSubtabIndicator() 量出来写进 transform / width。
   ⚠️ 别改成 `left`（那会每帧触发重排，滑动就不顺了）。 */
.hw-subtab-ind {
  position: absolute; left: 0; bottom: -1px;
  height: 3px; width: 48px; border-radius: 3px;
  background: var(--grad);
  transform: translateX(0);
  transition: transform .28s cubic-bezier(.22, .61, .36, 1), width .28s cubic-bezier(.22, .61, .36, 1);
  pointer-events: none;
}

/* 角标：只在「提交作业」里发生了值得你知道的事时出现（执行中 / 待确认 / 已完成 / 失败）。
   颜色沿用全站既有语义：橙 = 正在办，绿 = 办成了，红 = 没办成。
   ⚠️ 它只是**提示**：不切 tab、不改任何行为（老师 2026-10-12 明确要"页面别自作主张"）。 */
.hw-subtab-badge {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 1px 7px; border-radius: 999px;
  font-size: 11.5px; line-height: 1.5;
  background: rgba(245, 158, 11, .18); color: #92400E;
}
.hw-subtab-badge::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
.hw-subtab-badge.is-ok { background: rgba(16, 185, 129, .16); color: #047857; }
.hw-subtab-badge.is-bad { background: rgba(220, 38, 38, .14); color: #B91C1C; }
/* 中性灰：老师自己停的 / 被服务重启打断的 —— **不是失败**，别用红色吓人 */
.hw-subtab-badge.is-off { background: rgba(31, 36, 48, .10); color: var(--text-2); }

/* pane：未选中的整块**不参与布局**，切回来"淡入 + 轻微上浮"（只动 opacity/translate ⇒ 平滑）。
   刻意**不做交叉淡入**：那要把两块都绝对定位，会引入布局抖动，收益不抵风险。 */
.hw-pane { display: none; }
.hw-pane.is-active { display: block; animation: fade-up .26s cubic-bezier(.22, .61, .36, 1) both; }

/* 环境检查的**页内位**：只在"检查中"（或还没检查完）时有内容，查完它会被搬去顶栏 */
.health-slot:not(:empty) { margin-bottom: 14px; }

@media (max-width: 768px) {
  .hw-subtab { flex: 1; justify-content: center; margin-right: 0; }
}

/* ---------- 环境检查的「两态停靠」（2026-10-12）：检查中在作业页、查完缩进顶栏 ----------
   老师原话："检查完毕了之后，这个窗口就可以有一个缩小，到一个屏幕上不重要的地方，比如说右上角
   用户区，用户当前，就是提示当前登录用户区的左边"。所以查完把**整张卡**搬进顶栏 `.topbar-health`
   （身份徽章左边）并加 `.is-docked` 收成"胶囊 + 状态灯"；检查中它停在作业页的子 tab 下方。
   ⚠️ **同一份节点两处停靠**（搬运在 app.js 的 dockAgentHealth()），别复制第二份摘要/另一枚胶囊。 */
.topbar-health { display: flex; align-items: center; gap: 10px; }

/* 停靠态：卡壳全脱掉（内边距/底色/阴影/边框），只剩胶囊；标题与那行小字也收起来 —— 详情本来就在浮层里 */
.agent-health.is-docked { padding: 0; background: transparent; box-shadow: none; border: 0; }
.agent-health.is-docked .card-head,
.agent-health.is-docked .health-hint { display: none; }
/* 浮层右对齐：顶栏贴着屏幕右边，浮层再往右展开就出屏了；那个小箭头也跟着挪到右侧 */
.agent-health.is-docked .health-popover { left: auto; right: 0; }
.agent-health.is-docked .health-popover::before { left: auto; right: 22px; }
/* 状态灯：停靠态**只留圆点**（"智能体 1：现在空闲"这种长句放不进顶栏），原文进 tooltip/浮层 */
.agent-health.is-docked .slot-light i { display: none; }
.agent-health.is-docked .slot-light { padding: 0 8px; }

/* 连接卡现在住在环境浮层里：从"一整张卡"降级成浮层里的一块（不再有卡片阴影与加大间距） */
.health-popover .beaver-card {
  margin: 0 0 10px; padding: 10px 12px;
  background: var(--bg); box-shadow: none; border: 1px solid rgba(31, 36, 48, .06);
}
.health-popover .beaver-card h2 { font-size: 15px; }
.health-popover .beaver-card .field { margin-bottom: 10px; }
.health-popover .beaver-card .field-label { margin-bottom: 6px; font-size: 12.5px; }

/* ---------- 环境检查（默认只有一个小胶囊 + 一行小字，悬停/点击才看 6 条检查） ---------- */
.agent-health { padding: 14px 20px; }
.health-line {
  display: flex; align-items: center; gap: 10px;
  min-width: 0;
}
.health-bar {
  position: relative;
  display: inline-flex; align-items: center;
  height: 26px; padding: 0 12px; flex: none;
  border-radius: 999px;
  border: 1px solid transparent;
  background: var(--bg-2); color: var(--text-2);
  font-size: 12.5px; font-weight: 500; line-height: 1;
  cursor: pointer; user-select: none;
  transition: background .2s ease, color .2s ease, box-shadow .2s ease;
}
.health-bar:hover { box-shadow: var(--shadow-sm); }
.health-bar:focus-visible { outline: 2px solid rgba(99, 102, 241, .3); outline-offset: 2px; }
.health-bar-text { position: relative; z-index: 1; white-space: nowrap; }

/* 胶囊后面那行小字：正在检查哪一项 / 结果汇总；显示不下就截断 */
.health-hint {
  flex: 1; min-width: 0;
  font-size: 12.5px; color: var(--text-2);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.health-dots { letter-spacing: 1px; }

/* 智能体状态灯（在「环境检查」那一行的右侧）：空闲绿 / 忙碌橙 / 出现问题红。
   数量由后端工位数决定（2~4 盏都成立，样式不写死）；正在服务当前老师的那一盏
   带一圈环绕跑马灯 —— 不显示姓名，别人在用就不点名。 */
.slot-lights { display: inline-flex; align-items: center; gap: 8px; flex: none; }
.slot-light {
  position: relative; display: inline-flex; align-items: center; gap: 5px;
  height: 24px; padding: 0 10px; border-radius: 999px;
  font-size: 12px; line-height: 1; white-space: nowrap;
  background: var(--bg-2); color: var(--text-2);
}
.slot-light::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
.slot-light i { font-style: normal; opacity: .85; }
.slot-light.is-idle { background: rgba(16, 185, 129, .12); color: #047857; }
.slot-light.is-busy { background: rgba(245, 158, 11, .16); color: #92400E; }
.slot-light.is-problem { background: rgba(220, 38, 38, .12); color: #B91C1C; }
.slot-light.is-unknown { background: var(--bg-2); color: var(--text-2); }
/* 「正在为你服务」：绕灯一圈的**彩虹霓虹光环**（与步骤条数字外圈同一套取色与节奏）。
   ⚠️ 为什么不能像步骤条那样直接 `transform: rotate`（实测 2026-10-07 踩过）：
   步骤条那枚数字是**圆形**，转整体不变形；而这里是**胶囊形（非圆形）**，一转整体就把
   "环的形状"也转走 —— 转到 90° 附近它会变成一条竖着的高胶囊，从胶囊下方支棱出去。
   所以这里**几何全程不动**（inset/圆角/双层 mask 都不动），只转**渐变的起始角**：
   `--slot-angle` 用 @property 注册成 <angle> 才会插值；mask 出来的 2px 圆角环
   始终贴在胶囊轮廓上，只有霓虹在沿着它走。
   旧浏览器不认识 @property 时不插值 —— 靠 conic-gradient 里的 `var(--slot-angle, 0deg)`
   兜底成"静态霓虹环"：环还在、还能看出"正在为你服务"，只是不转。 */
@property --slot-angle { syntax: '<angle>'; initial-value: 0deg; inherits: false; }
.slot-light.is-mine::after {
  content: ""; position: absolute; inset: -2px; border-radius: inherit; pointer-events: none;
  padding: 2px;
  background: conic-gradient(from var(--slot-angle, 0deg), var(--neon-ring));
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
  animation: slot-halo 1.8s linear infinite;         /* 与步骤条 step-halo 同周期 */
}
/* 只动"渐变起始角"：环的形状在任意相位都恒定（这正是修复点） */
@keyframes slot-halo { to { --slot-angle: 360deg; } }
.slot-queue { font-size: 12px; color: var(--text-2); }

/* 检测中：橙色胶囊 + 内部一层来回流动的流光（流光层自带圆角，不能用 overflow:hidden——会把浮层一起裁掉） */
.health-bar.is-checking {
  background: rgba(245, 158, 11, .14);
  border-color: rgba(245, 158, 11, .34);
  color: #92400E;
}
.health-bar.is-checking::after {
  content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  border-radius: inherit;
  background: linear-gradient(100deg, transparent 32%, rgba(255, 255, 255, .8) 50%, transparent 68%);
  background-size: 220% 100%;
  animation: health-sweep 1.4s linear infinite;
}
@keyframes health-sweep {
  from { background-position: 180% 0; }
  to { background-position: -80% 0; }
}

/* 检测完：只靠胶囊颜色表达状态，文字仍是 2–4 字的短词 */
.health-bar.is-ok {
  background: rgba(16, 185, 129, .12);
  border-color: rgba(16, 185, 129, .3);
  color: #047857;
}
.health-bar.is-warn {
  background: rgba(245, 158, 11, .16);
  border-color: rgba(245, 158, 11, .34);
  color: #92400E;
}
/* 「待连接」：没凭证什么都干不了 —— 用红色与"有点小毛病"区分开（老师 2026-10-12 选的口径） */
.health-bar.is-bad {
  background: rgba(220, 38, 38, .12);
  border-color: rgba(220, 38, 38, .32);
  color: #B91C1C;
}

/* 浮层：向胶囊下方展开，不遮挡下方按钮 */
.health-popover {
  position: absolute; left: 0; top: calc(100% + 8px);
  width: min(420px, calc(100vw - 56px));
  padding: 14px 16px;
  border-radius: var(--radius-md);
  background: var(--surface);
  border: 1px solid rgba(31, 36, 48, .06);
  box-shadow: var(--shadow-md);
  opacity: 0; visibility: hidden; transform: translateY(-4px);
  transition: opacity .16s ease, transform .16s ease, visibility .16s ease;
  z-index: 30;
}
.health-popover::before {
  content: ""; position: absolute; top: -5px; left: 22px;
  width: 10px; height: 10px; border-radius: 2px;
  background: var(--surface);
  border-left: 1px solid rgba(31, 36, 48, .06);
  border-top: 1px solid rgba(31, 36, 48, .06);
  transform: rotate(45deg);
}
.health-bar:hover .health-popover,
.health-bar:focus-within .health-popover,
.health-bar.is-open .health-popover { opacity: 1; visibility: visible; transform: none; }

.health-popover-head { font-size: 12px; color: var(--text-2); margin-bottom: 8px; }
.health-grid { display: flex; flex-direction: column; gap: 6px; }
.health-item {
  display: grid; grid-template-columns: 68px 1fr;
  align-items: baseline; gap: 2px 10px;
  padding: 7px 10px;
  border-radius: var(--radius-sm);
  background: var(--bg);
  box-shadow: inset 3px 0 0 var(--ok);
}
.health-item.is-warn { box-shadow: inset 3px 0 0 var(--warn); }
/* 检测中的占位行：色条是灰的，结论处呼吸，表示"正在查" */
.health-item.is-pending { box-shadow: inset 3px 0 0 var(--bg-2); }
.health-item.is-pending .health-value {
  color: var(--text-2); font-weight: 400;
  animation: health-pending 1.6s ease-in-out infinite;
}
@keyframes health-pending {
  0%, 100% { opacity: .45; }
  50% { opacity: 1; }
}
.health-name { font-size: 12px; color: var(--text-2); }
.health-value { font-size: 13.5px; font-weight: 500; }
.health-fix { grid-column: 2; font-size: 12px; color: var(--warn); }
.health-tech {
  margin-top: 12px; padding-top: 10px;
  border-top: 1px dashed rgba(31, 36, 48, .1);
  font-size: 11.5px; line-height: 1.7; color: var(--text-2);
  word-break: break-all;
}
/* 「其余 N 项（点开查看）」：块级小字按钮。用 button 而不是 div —— 键盘能聚焦、能回车触发。
   不做新色板，沿用 --text-2 / --text 的灰阶。 */
.health-more {
  display: block; width: 100%; margin-top: 10px; padding: 2px 0;
  font: inherit; font-size: 11.5px; color: var(--text-2);
  text-align: left; background: none; border: 0; cursor: pointer;
}
.health-more::before {
  content: '▸'; display: inline-block; width: 1em; margin-right: 2px;
  transition: transform .16s ease;
}
.health-more[aria-expanded="true"]::before { transform: rotate(90deg); }
.health-more:hover { color: var(--text); text-decoration: underline; }
.health-more:focus-visible { outline: 2px solid rgba(99, 102, 241, .35); outline-offset: 2px; }
/* 折叠区：与 .health-tech 同字号行高；顶部虚线由按钮代替，这里只轻微内缩，长路径仍能断行 */
.health-tech-more {
  margin-top: 4px; padding-top: 0; padding-left: 10px;
  border-top: 0;
}

/* ---------- 连接 AI Beaver（凭证入口：对应「肥肥袋鼠」的 Token 授权弹窗） ---------- */
.beaver-connected {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 12px 14px; border-radius: var(--radius-md);
  background: rgba(16, 185, 129, .08);
  border: 1px solid rgba(16, 185, 129, .22);
}
.beaver-ok {
  width: 20px; height: 20px; flex: none; border-radius: 50%;
  background: var(--ok); color: #fff;
  display: grid; place-items: center; font-size: 12px;
}
.beaver-who { flex: 1; min-width: 0; font-size: 14px; }
.beaver-note { margin: 14px 0 0; font-size: 12px; line-height: 1.75; color: var(--text-2); }
.beaver-note strong { color: var(--text); }
/* 单人自用（.env 给了默认身份）时的告警条：所有访问者都算同一个人，必须说出来 */
.beaver-note.is-warn {
  margin-top: 12px; padding: 10px 12px;
  border-radius: var(--radius-md);
  background: rgba(245, 158, 11, .1);
  border: 1px solid rgba(245, 158, 11, .25);
  color: var(--text);
}

/* 环境检查里「可以点」的那一行（点它打开上面的凭证卡） */
.health-item.is-actionable { cursor: pointer; }
.health-item.is-actionable:hover { background: #F2F4FB; }
.health-item.is-actionable:focus-visible { outline: 2px solid rgba(99, 102, 241, .35); }
.health-act { grid-column: 2; font-size: 12px; color: var(--primary-2); text-decoration: underline; }

/* 常显主项「身份认证」：老师唯一要自己动手的地方（交一次凭证），比异常行更醒目一点 */
.health-item.is-primary {
  padding: 9px 10px;
  background: var(--surface);
  border: 1px solid rgba(31, 36, 48, .07);
  box-shadow: inset 3px 0 0 var(--ok), 0 1px 2px rgba(31, 36, 48, .04);
}
.health-item.is-primary.is-warn { box-shadow: inset 3px 0 0 var(--warn), 0 1px 2px rgba(31, 36, 48, .04); }
.health-item.is-primary .health-value { font-size: 14px; font-weight: 600; }
/* 「需要处理」小标题：只有真有异常时才由 JS 渲染出来（没问题就整块不出现） */
.health-group-title { margin-top: 8px; font-size: 12px; color: var(--text-2); }
/* 正常项收束成一行灰字（不再各占一行"正常/已连接"）：淡绿左边条表示"这些是好的" */
.health-summary {
  margin-top: 8px; padding: 3px 0 3px 10px;
  font-size: 12px; color: var(--text-2);
  border-left: 3px solid rgba(16, 185, 129, .45);
}

/* ---------- 步骤条（作业页：1–6 恒为一行，随容器缩放，绝不折成两排） ---------- */
.steps-agent {
  flex-wrap: nowrap; gap: clamp(4px, .7vw, 9px);
  /* 彩虹霓虹色停（--neon-band / --neon-ring）已上提到 :root —— 状态灯的环绕光环
     与这里用的是**同一份**取色，别在各自作用域再抄一遍（抄了就会两边走偏）。 */
}
.steps-agent .step {
  position: relative;
  /* 宽度先按内容分（长名字的步骤不会被压到省略号），空间不够时再一起缩 */
  flex: 1 1 auto; min-width: 0; justify-content: center;
  padding: clamp(6px, .7vw, 8px) clamp(6px, .8vw, 14px);
  font-size: clamp(12px, 1.05vw, 14px);
}
.steps-agent .step > span:last-child {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.steps-agent .step + .step::before { width: clamp(4px, .8vw, 18px); margin-right: 0; }
.steps-agent .step-no { flex: none; }

/* ===== 静默步骤的失败告警：栏尾一枚小红胶囊（平时不占位） =====
   静默 = 平时不占位，**不是**瞒着老师：扫描入口 / 选定作业一旦失败，就在栏尾以
   「! 扫描入口失败」露出来（胶囊本身仍不显示 —— 2026-10-12 老师逐条裁定选的就是这个形态：
   静默项出事放**告警**，不放回步骤位）。悬停看原因、点击展开/收起详情。
   `flex: none`：不许被 `.steps-agent .step { flex: 1 1 auto }` 拉宽，也不参与那四枚的等分。 */
.steps-agent .step-alert {
  position: relative; flex: none; display: flex; align-items: center; gap: 6px;
}
.step-alert-chip {
  display: inline-flex; align-items: center;
  padding: clamp(6px, .7vw, 8px) clamp(8px, .85vw, 13px);
  border-radius: 999px; font: inherit; font-size: clamp(12px, 1.05vw, 14px);
  color: var(--danger); cursor: pointer; white-space: nowrap;
  background: var(--surface);
  border: 1.5px solid rgba(239, 68, 68, .45);   /* 与「失败」胶囊同一套红，不另造一种告警色 */
}
.step-alert-chip:hover { background: rgba(239, 68, 68, .07); }
/* 详情**绝对定位**（浮在栏下方）：不改变那四枚胶囊的位置，也不会把栏撑成两排 */
.step-alert-detail {
  position: absolute; z-index: 3; top: calc(100% + 6px); right: 0;
  max-width: min(420px, 60vw); padding: 8px 11px; border-radius: 10px;
  background: var(--surface); box-shadow: var(--shadow-md);
  border: 1px solid rgba(239, 68, 68, .35);
  font-size: 12px; line-height: 1.5; color: var(--text-2);
  white-space: pre-wrap; word-break: break-word;
}

/* ===== 正在跑 / 等待中：胶囊内一层**彩虹霓虹流光** + 数字外圈彩虹光环 =====
   为什么在橙色之上再叠彩虹：项目既定的色彩语义是「橙 = 正在跑」（见 AGENTS 第六轮），
   彩虹只作**装饰流光**叠在上面 —— 既不丢语义，又让老师一眼看出"它在动"。
   只动 `background-position`（流光）与 `transform: rotate`（光环），都不触发重排；
   周期 ≥2.4s、不闪烁，避免视觉疲劳。 */
.steps-agent .step.is-current,
.steps-agent .step.is-waiting { overflow: hidden; }        /* 只裁胶囊内的流光，别加到 .step-no 上 */

.steps-agent .step.is-current::after,
.steps-agent .step.is-waiting::after {
  content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background: var(--neon-band);
  background-size: 300% 100%;
  animation: step-neon 2.6s linear infinite;
}
.steps-agent .step.is-current > *,
.steps-agent .step.is-waiting > * { position: relative; z-index: 1; }

.steps-agent .step.is-current .step-no,
.steps-agent .step.is-waiting .step-no { position: relative; }
.steps-agent .step.is-current .step-no::after,
.steps-agent .step.is-waiting .step-no::after {
  content: ""; position: absolute; inset: -3px; border-radius: 50%;
  background: conic-gradient(from 0deg, var(--neon-ring));
  -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 2px), #000 calc(100% - 2px));
  mask: radial-gradient(farthest-side, transparent calc(100% - 2px), #000 calc(100% - 2px));
  animation: step-halo 1.8s linear infinite;
}
@keyframes step-neon {
  from { background-position: 200% 0; }
  to { background-position: -100% 0; }
}
@keyframes step-halo { to { transform: rotate(360deg); } }

/* 失败：只收一边红走线，不加背景块，避免比「当前步骤」更抢眼 */
.steps-agent .step.is-failed {
  color: var(--danger);
  box-shadow: inset 0 0 0 1.5px rgba(239, 68, 68, .35);
}
.steps-agent .step.is-failed .step-no { background: var(--danger); color: #fff; }

/* 作业页状态色：**进行中用橙色、完成用绿色**（老师一眼区分「在跑」与「已跑完」） */
@keyframes pulse-warn {
  0%, 100% { box-shadow: var(--shadow-sm); }
  50% { box-shadow: 0 0 0 6px rgba(245, 158, 11, .16); }
}
/* —— 正在跑（最强）：橙色主体 + 彩虹流动 + 呼吸发光 —— */
.steps-agent .step.is-current {
  color: var(--warn);
  animation: pulse-warn 2.2s ease-in-out infinite;
}
.steps-agent .step.is-current .step-no { background: var(--warn); color: #fff; }

/* —— 等待中（同一套动效的弱化版）：更淡、更慢、不发光 ——
   它表达的是"任务还在推进，只是此刻没在动手"，**不是**已完成（已完成是绿的）。 */
.steps-agent .step.is-waiting { color: var(--text-2); box-shadow: var(--shadow-sm); }
.steps-agent .step.is-waiting::after { opacity: .38; animation-duration: 5s; }
.steps-agent .step.is-waiting .step-no::after { opacity: .45; animation-duration: 3.4s; }

/* —— 完成（绿）：**必须声明在 is-waiting 之后** ——
   两者特指度相同（0,3,0），谁赢看源码顺序；万一某枚同时带上了 is-waiting（兜底动效留下的），
   排后面的这条绿色仍然说了算。实测 2026-10-07：没有这条时，"已完成"会被 is-waiting 压成灰色
   （`.step.is-done` 只有 0,2,0，压不过 0,3,0 的 waiting）。 */
/* ⚠️ `:not(.is-failed)`：**失败优先于完成** —— 某一步这一轮最后是失败的（红），
   哪怕它早些时候完成过，也不许被染成绿（判断顺序上 JS 也会跳过它，这里是第二道）。 */
.steps-agent .step.is-done:not(.is-failed) { color: var(--ok); box-shadow: var(--shadow-sm); }
.steps-agent .step.is-done:not(.is-failed) .step-no { background: var(--ok); color: #fff; }
/* 已完成就不再流动 / 不转光环：动效只表达"正在跑"，完成态是静止的绿 */
.steps-agent .step.is-done::after,
.steps-agent .step.is-done .step-no::after { animation: none; opacity: 0; }

/* —— 卡住：动效必须停下（还在转 = 骗老师说"在推进"），换成静止的告警描边 —— */
.steps-agent .step.is-stalled {
  color: var(--warn);
  box-shadow: inset 0 0 0 1.5px rgba(245, 158, 11, .55);
}
.steps-agent .step.is-stalled .step-no { background: var(--warn); color: #fff; }
.steps-agent .step.is-stalled::after,
.steps-agent .step.is-stalled .step-no::after { animation: none; opacity: .22; }
.plan-box {
  margin-top: 14px; padding: 12px 14px;
  border-radius: var(--radius-md);
  background: linear-gradient(180deg, rgba(99, 102, 241, .07), rgba(255, 255, 255, 0));
  border: 1px dashed rgba(99, 102, 241, .28);
  font-size: 13.5px; color: var(--text); white-space: pre-wrap;
}
.plan-box .plan-title { font-size: 12px; color: var(--primary); font-weight: 600; margin-bottom: 6px; }

/* ---------- 班级卡（紧凑） ---------- */
.class-grid-sm .class-item { padding: 10px 12px; }
.class-grid-sm .class-item strong { font-size: 14px; }
.select-hint { font-size: 12px; color: var(--text-2); }

/* ---------- 标准提交：① 作业入口单选 / ② 班级挂包（2026-10-04） ---------- */

/* 小节标题行：标题在左、动作（如「重新扫描」）在右 */
.field-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }

/* ① 入口单选：一行一个入口，点一下选中；再点已选中的那个 = 取消 */
.entry-list { display: flex; flex-direction: column; gap: 6px; }
.entry-item {
  display: flex; align-items: center; gap: 8px; width: 100%;
  padding: 8px 10px; border-radius: var(--radius-sm);
  font: inherit; text-align: left; cursor: pointer;
  background: var(--surface); border: 1px solid rgba(31, 36, 48, .1);
  transition: border-color .16s ease, background .16s ease, box-shadow .16s ease;
}
.entry-item:hover { border-color: var(--primary-2); background: #F7F8FF; }
.entry-item.is-selected {
  border-color: var(--primary); background: #F5F6FF;
  box-shadow: inset 3px 0 0 var(--primary);
}
.entry-dot {
  flex: none; width: 12px; height: 12px; border-radius: 50%;
  background: var(--surface); border: 1px solid rgba(31, 36, 48, .28);
}
.entry-item.is-selected .entry-dot {
  border-color: var(--primary); box-shadow: inset 0 0 0 3px var(--primary);
}
.entry-name { flex: 1; font-size: 13.5px; color: var(--text); }
.entry-id { flex: none; font-size: 11.5px; color: var(--text-2); }

/* 未就绪的入口：折叠起来 —— 看得到名字（好去跟运营反馈），但选不了 */
.entry-rest { margin-top: 8px; }
.entry-rest > summary { font-size: 12px; color: var(--text-2); cursor: pointer; list-style: none; }
.entry-rest > summary::-webkit-details-marker { display: none; }
.entry-rest > summary::before { content: "▸ "; }
.entry-rest[open] > summary::before { content: "▾ "; }
.entry-rest-body {
  margin-top: 6px; padding: 8px 10px;
  border-radius: var(--radius-sm); background: var(--bg);
}
.entry-rest-item { display: flex; justify-content: space-between; gap: 10px; padding: 3px 0; font-size: 12.5px; }
.entry-rest-item .entry-name { color: var(--text-2); }
.entry-rest-item .entry-state { flex: none; color: var(--warn); }
.entry-rest-note { margin-top: 6px; font-size: 12px; color: var(--text-2); }

/* ③ 任务要求：默认收起的折叠区（与同卡「未就绪入口」同一套折叠观感 —— 一张卡上不造第二种折叠语言）。
   位置在技能按键**下方**（技能是"要顺手多做点什么"，任务是"有没有额外要交代的"）。
   收起是为了"多数时候用不上、别占地方"；但**填了就必须说出来**：`.is-filled` 由 syncNoteFold() 挂上，
   标题变正文色写"已填写" —— 内容藏在折叠里、老师却不知道这一单带了什么要求，那才是问题。 */
.note-fold { margin-top: 0; }
.note-fold > summary { font-size: 13px; color: var(--text-2); cursor: pointer; list-style: none; }
.note-fold > summary::-webkit-details-marker { display: none; }
.note-fold > summary::before { content: "▸ "; }
.note-fold[open] > summary::before { content: "▾ "; }
.note-fold.is-filled > summary { color: var(--text); }
.note-fold[open] > summary { margin-bottom: 8px; }

/* 额外技能：默认可见的一组**按键**（未选中置灰 / 选中变亮，可多选），摆在「③ 任务要求」折叠的**上方**。
   视觉直接沿用本项目既有的"可选按键"语言（学生画像灯筛选的 .pf-chip.is-active）——
   同一套东西不造第二种样子：灰底灰字=没选，品牌渐变底+白字=已选。
   清单由 app.js 的 EXTRA_SKILLS 驱动，**只放后端真能兑现的项**；项数不写死（多了自动换行）。 */
.skill-list { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 10px; }
.skill-btn {
  appearance: none; cursor: pointer;
  border: 1px solid rgba(31, 36, 48, .1); background: var(--surface);
  border-radius: 999px; padding: 6px 14px; font-size: 13px; color: var(--text-2);
  transition: all .18s ease;
}
.skill-btn:hover { border-color: var(--primary-2); color: var(--text); }
/* 选中：变亮（与 .pf-chip.is-active 同款）。只改底色/字色/边框，不做位移 —— 点一下不该"跳" */
.skill-btn.is-on { background: var(--grad); color: #fff; border-color: transparent; }
.skill-btn:focus-visible { outline: 2px solid rgba(99, 102, 241, .35); outline-offset: 2px; }

/* ② 班级挂包：整张卡就是"挑选入口"，右上角小 × 是移除；回形针图标 = 这个班已挂包 */
.class-package { padding: 0; cursor: default; }
.class-package .class-pick {
  /* 右边多留一点：右上角那枚小 × 是绝对定位的，别让长班名压到它下面 */
  display: block; width: 100%; padding: 10px 24px 10px 12px; border-radius: inherit;
  font: inherit; text-align: left; cursor: pointer; background: none; border: 0;
}
.class-line { display: flex; align-items: center; gap: 6px; }
.class-package .class-pick strong { font-size: 14px; font-weight: 600; }
.class-pkg {
  display: block; margin-top: 3px; font-size: 12px; color: var(--text-2);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.class-pin { flex: none; width: 14px; height: 14px; color: var(--primary); }
.class-package.is-loaded { border-color: rgba(79, 70, 229, .5); box-shadow: inset 3px 0 0 var(--primary); }
.class-package.is-loaded .class-pkg { color: #4338CA; }
.class-package.is-failed { border-color: rgba(245, 158, 11, .55); }
.class-package.is-failed .class-pin { color: var(--warn); }
.class-package.is-failed .class-pkg { color: var(--warn); }
.class-remove {
  position: absolute; top: 4px; right: 6px; width: 20px; height: 20px; padding: 0;
  font: inherit; font-size: 14px; line-height: 1; color: var(--text-2);
  background: none; border: 0; border-radius: 6px; cursor: pointer;
}
.class-remove:hover { color: var(--danger); background: rgba(239, 68, 68, .12); }

/* 「正在获取作业提交入口」前面那枚转圈：与执行流里那个转圈同一套画法（环形 + 主色渐变） */
.entry-loading { display: flex; align-items: center; gap: 7px; }
.loading-ring {
  flex: none; width: 13px; height: 13px; border-radius: 50%;
  background: conic-gradient(from 0deg, rgba(99, 102, 241, 0), var(--primary-2), var(--primary-3));
  -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 2px), #000 calc(100% - 2px));
  mask: radial-gradient(farthest-side, transparent calc(100% - 2px), #000 calc(100% - 2px));
  animation: proc-spin 1s linear infinite;
}

/* ---------- 拖拽区（紧凑） ---------- */
.dropzone-sm { padding: 20px 16px; }
.dropzone-sm .dz-icon { width: 38px; height: 38px; font-size: 20px; border-radius: 12px; }
.dropzone-sm .dz-title { font-size: 14px; }

/* ---------- 执行流：内嵌滚动面板（高度固定，内容在里面翻） ----------
   为什么不是「随内容长高的列表」：老师要能往上翻之前的信息（例如早先扫出来的
   作业入口名字），面板高度一变、页面跟着滚，就很难意识到"这里面可以翻"。 */
.stream-panel { position: relative; }
.stream {
  display: flex; flex-direction: column; gap: 8px;
  height: clamp(320px, 52vh, 520px);
  overflow-y: auto; overflow-x: hidden;
  overscroll-behavior: contain;      /* 滚到头不在整页上"漏"出去 */
  scrollbar-gutter: stable;          /* 常驻轨道：内容不因滚动条出现而左右跳 */
  padding: 12px 14px;
  border: 1px solid rgba(31, 36, 48, .07);
  border-radius: var(--radius-md);
  background: linear-gradient(180deg, rgba(31, 36, 48, .022), rgba(31, 36, 48, .007));
  box-shadow: inset 0 1px 2px rgba(31, 36, 48, .03);
}
.stream::-webkit-scrollbar { width: 8px; }
.stream::-webkit-scrollbar-track { background: transparent; }
.stream::-webkit-scrollbar-thumb { background: rgba(31, 36, 48, .14); border-radius: 999px; }
.stream::-webkit-scrollbar-thumb:hover { background: rgba(31, 36, 48, .26); }
/* 上下渐隐：只有该方向还有内容时才出现，提示"可以继续翻" */
.stream-fade {
  position: absolute; left: 1px; right: 10px; height: 20px;
  pointer-events: none; opacity: 0; transition: opacity .18s ease;
}
.stream-fade.is-top {
  top: 1px; border-radius: var(--radius-md) var(--radius-md) 0 0;
  background: linear-gradient(180deg, rgba(255, 255, 255, .95), rgba(255, 255, 255, 0));
}
.stream-fade.is-bottom {
  bottom: 1px; border-radius: 0 0 var(--radius-md) var(--radius-md);
  background: linear-gradient(0deg, rgba(255, 255, 255, .95), rgba(255, 255, 255, 0));
}
.stream-panel.can-scroll-up .stream-fade.is-top,
.stream-panel.can-scroll-down .stream-fade.is-bottom { opacity: 1; }
/* 「回到最新」：离开底部才淡入；有新消息时点一个小圆点提示，不打断阅读 */
.stream-jump {
  position: absolute; right: 18px; bottom: 14px;
  display: inline-flex; align-items: center; gap: 6px;
  padding: 6px 12px; font-size: 12.5px; color: var(--text);
  background: rgba(255, 255, 255, .94);
  border: 1px solid rgba(31, 36, 48, .12); border-radius: 999px;
  box-shadow: 0 6px 18px -8px rgba(31, 36, 48, .35);
  backdrop-filter: blur(6px);
  cursor: pointer; opacity: .96;
  transition: transform .16s ease, box-shadow .16s ease, opacity .16s ease;
}
.stream-jump:hover { transform: translateY(-1px); box-shadow: 0 10px 22px -10px rgba(31, 36, 48, .45); opacity: 1; }
.stream-jump::after {
  content: ''; width: 6px; height: 6px; border-radius: 50%;
  background: var(--primary-2); opacity: 0; transition: opacity .16s ease;
}
.stream-jump.is-new::after { opacity: 1; }
/* 把 .zip 拖进对话区时的提示层：整卡高亮 + 面板上浮一句话说清松手会发生什么 */
.card.is-over { border-color: rgba(99, 102, 241, .45); box-shadow: 0 0 0 3px rgba(99, 102, 241, .10); }
.drop-hint {
  position: absolute; inset: 0; display: grid; place-items: center;
  border-radius: var(--radius-md);
  border: 1.5px dashed rgba(99, 102, 241, .55);
  background: rgba(99, 102, 241, .10);
  font-size: 13px; color: var(--primary); text-align: center; padding: 0 16px;
  pointer-events: none; animation: fade-up .18s ease both;
}

/* ---------- 附件：本轮要交给助手的作业包（拖进来 / 点「＋附件」） ---------- */
.composer { margin-top: 12px; }
.attach-row { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 8px; }
.attach-chip {
  display: inline-flex; align-items: center; gap: 6px; max-width: 100%;
  padding: 5px 8px 5px 10px; border-radius: 999px;
  font-size: 12.5px; color: var(--text);
  background: rgba(99, 102, 241, .08);
  border: 1px solid rgba(99, 102, 241, .22);
}
.attach-chip .attach-dot { flex: none; width: 6px; height: 6px; border-radius: 50%; background: var(--primary-2); }
.attach-chip.is-uploading .attach-dot { background: var(--warn); }
.attach-chip.is-ready .attach-dot { background: var(--ok); }
.attach-chip.is-failed { background: rgba(239, 68, 68, .08); border-color: rgba(239, 68, 68, .28); }
.attach-chip.is-failed .attach-dot { background: var(--danger); }
.attach-chip .attach-name { max-width: 240px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.attach-chip .attach-size,
.attach-chip .attach-state { color: var(--text-2); font-size: 11.5px; }
.attach-chip .attach-state.is-warn { color: var(--warn); }
.attach-chip .attach-state.is-danger { color: var(--danger); }
.attach-chip .attach-remove {
  border: 0; background: transparent; color: var(--text-2);
  font-size: 14px; line-height: 1; padding: 0 2px; cursor: pointer;
}
.attach-chip .attach-remove:hover { color: var(--danger); }
/* 气泡里的附件名：只读展示（没有移除按钮） */
.attach-chip.is-plain { padding: 4px 10px; background: rgba(99, 102, 241, .10); }
.attach-btn { flex: none; }
.composer-hint .attach-note { color: var(--warn); }
/* 气泡底部的附件行 */
.bubble-attach { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
/* 「标准提交」的包挂在班级上：胶囊前面写上「班级 → 包名（大小）」，一眼看出哪个包是哪个班的 */
.bubble-attach .attach-owner { font-weight: 600; color: var(--text); }
.bubble-attach .attach-arrow { color: var(--text-2); opacity: .7; }

.stream-item {
  display: flex; gap: 10px; align-items: flex-start;
  padding: 10px 12px; border-radius: var(--radius-md);
  border: 1px solid rgba(31, 36, 48, .07); background: var(--surface);
  animation: fade-up .28s ease both;
}
.stream-item .icon {
  flex: none; width: 22px; height: 22px; border-radius: 50%;
  display: grid; place-items: center; font-size: 12px; color: #fff;
  background: var(--bg-2); margin-top: 1px;
}
.stream-item .body { flex: 1; min-width: 0; }
.stream-item .title { font-size: 13.5px; font-weight: 500; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.stream-item .desc { font-size: 12.5px; color: var(--text-2); margin-top: 4px; white-space: pre-wrap; }
.stream-item .at { font-size: 11.5px; color: var(--text-2); font-variant-numeric: tabular-nums; }
/* 追溯 ID 小签：AI 回复的右上角，灰、克制、点一下复制（`<runId>-<seq>`）。
   为什么用行内 `margin-left: auto` 而不是绝对定位：绝对定位在窄屏会压住正文，
   行内右端对齐在换行/缩放时都不会盖字。 */
.stream-item .trace-chip {
  margin-left: auto; display: inline-flex; align-items: center; gap: 4px;
  padding: 1px 6px; border-radius: 999px;
  font-family: ui-monospace, SFMono-Regular, Consolas, 'Courier New', monospace;
  font-size: 10.5px; font-weight: 400; letter-spacing: .2px;
  color: var(--text-2); opacity: .62;
  background: rgba(91, 100, 120, .09);
  cursor: pointer; user-select: none; white-space: nowrap;
}
.stream-item .trace-chip:hover { opacity: 1; background: rgba(91, 100, 120, .16); }
.stream-item .trace-chip .trace-copy { fill: none; stroke: currentColor; stroke-width: 1.4; }
.stream-item.is-ok .icon { background: var(--ok); }
.stream-item.is-run .icon { background: var(--grad); }
.stream-item.is-warn { background: rgba(245, 158, 11, .07); }
.stream-item.is-warn .icon { background: var(--warn); }
.stream-item.is-error { background: rgba(239, 68, 68, .07); }
.stream-item.is-error .icon { background: var(--danger); }
.stream-item.is-think { background: rgba(99, 102, 241, .05); }
.stream-item.is-think .icon { background: var(--primary-2); }
/* 老师说的话：稍微靠右（不贴边），浅紫底 + 渐变头像，与靠左的助手气泡区分来源 */
.stream-item.is-user {
  margin-left: auto; margin-right: 6px; max-width: 78%;
  background: linear-gradient(180deg, rgba(99, 102, 241, .10), rgba(99, 102, 241, .04));
  border-color: rgba(99, 102, 241, .28);
}
.stream-item.is-user .icon { background: var(--grad); font-size: 13px; }
.stream-item.is-user .title { color: var(--primary); }
.stream-item.is-agent .icon { background: var(--grad); }
.stream-item .data-block { margin-top: 10px; overflow-x: auto; }

/* 对话内联卡片：关键结果（入口清单 / 判班依据 / 预检明细 / 报告摘要）直接说进对话 */
.inline-card {
  margin-top: 8px; padding: 12px 14px;
  border: 1px solid rgba(31, 36, 48, .06);
  border-radius: var(--radius-md);
  background: var(--surface);
  box-shadow: var(--shadow-sm);
}
.inline-card .data-table { margin-top: 4px; }
.inline-card .mini-grid { margin-bottom: 0; }
.inline-card .muted { font-size: 12.5px; margin-top: 6px; }

/* 存活探测小窗口：出现在「可能卡住」卡里，每 2 秒刷新一次（转圈 + 三色点 + 数字）。
   转圈只在「确实还在动」时转 —— 判成真卡住就把动效停下，与既有纪律一致：
   还在转就是骗老师以为在推进。 */
.probe-box {
  margin-top: 10px; padding: 10px 12px;
  border: 1px dashed rgba(31, 36, 48, .16);
  border-radius: var(--radius-sm);
  background: var(--bg);
}
.probe-head { display: flex; align-items: center; gap: 8px; font-size: 12.5px; color: var(--text-2); }
.probe-head b { color: var(--text); font-size: 13px; }
.probe-checked { margin-left: auto; font-variant-numeric: tabular-nums; }
.probe-spinner {
  width: 13px; height: 13px; flex: none; border-radius: 50%;
  border: 2px solid rgba(31, 36, 48, .18); border-top-color: var(--primary);
  animation: probe-spin .9s linear infinite;
}
@keyframes probe-spin { to { transform: rotate(360deg); } }
.probe-box:not(.is-live) .probe-spinner {
  animation: none; border-top-color: rgba(31, 36, 48, .25);
}
.probe-verdict { display: flex; gap: 8px; align-items: flex-start; margin-top: 8px; font-size: 13px; line-height: 1.5; }
.probe-dot { width: 8px; height: 8px; flex: none; margin-top: 5px; border-radius: 50%; background: var(--text-2); }
.probe-dot.is-ok { background: var(--ok); }
.probe-dot.is-warn { background: var(--warn); }
.probe-dot.is-bad { background: var(--danger); }
.probe-dot.is-muted { background: rgba(31, 36, 48, .25); }
.probe-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(118px, 1fr)); gap: 6px 10px; margin-top: 8px; }
.probe-grid .k { display: block; font-size: 11.5px; color: var(--text-2); }
.probe-grid b { font-size: 13.5px; font-variant-numeric: tabular-nums; }
.probe-note { margin-top: 8px; font-size: 11.5px; color: var(--text-2); }

/* 提交进度表：逐班提交时"哪个班交完了"要能一眼看出 —— 交完的那行打勾并划掉 */
.submit-board tr.is-done td:not(:last-child) {
  text-decoration: line-through;
  color: var(--text-2);
}
.submit-board td:first-child { white-space: nowrap; }

/* 逐班耗时体检（提交结果上方那一小块）：让"这次是不是特别慢"有可对照的数字 */
.class-stats { display: grid; gap: 4px; margin: 4px 0 8px; }
.class-stat { display: flex; gap: 10px; align-items: baseline; font-size: 12.5px; }
.class-stat b { flex: none; min-width: 120px; }
.class-stat span { color: var(--text-2); font-variant-numeric: tabular-nums; }

/* 报告里的小标题：h4 没有全局重置，不写样式会顶着浏览器默认的大字号把报告拉长。
   只作用于报告容器（`#agentReport`），不去动别处可能出现的 h4。 */
#agentReport h4 { margin: 14px 0 6px; font-size: 13.5px; font-weight: 600; color: var(--text-2); }

/* 报错详情：小字折叠 + 一键复制（原文可长可短，绝不能把结论挤走，也不能复制不了） */
.err-detail { margin-top: 6px; font-size: 11.5px; color: var(--text-2); }
.err-detail > summary { cursor: pointer; padding: 2px 0; color: var(--text-2); list-style: none; }
.err-detail > summary::-webkit-details-marker { display: none; }
.err-detail > summary::before { content: '▸ '; }
.err-detail[open] > summary::before { content: '▾ '; }
.err-detail > summary:hover { color: var(--text); text-decoration: underline; }
.err-text {
  margin: 4px 0 0; padding: 8px 10px; max-height: 260px; overflow: auto;
  background: var(--bg-2); border-radius: 8px;
  font: 11.5px/1.55 ui-monospace, SFMono-Regular, Consolas, monospace;
  white-space: pre-wrap; word-break: break-word;
  user-select: text; -webkit-user-select: text;   /* 必须可选中：老师要能手动复制 */
  color: var(--text);
}
.err-actions { display: flex; gap: 10px; align-items: center; margin-top: 6px; flex-wrap: wrap; }
.err-path { font-size: 11.5px; color: var(--text-2); word-break: break-all; }

/* 本轮逐人明细里的「成功」折叠区：66 人也不糊住报告，想看再点开 */
.round-detail { margin-top: 6px; }
.round-detail > summary {
  cursor: pointer; font-size: 13px; color: var(--primary); padding: 4px 0;
  list-style: none;
}
.round-detail > summary::-webkit-details-marker { display: none; }
.round-detail > summary::before { content: '▸ '; }
.round-detail[open] > summary::before { content: '▾ '; }

/* 对话内联确认卡：紧跟在触发它的助手消息下方，替换原来的页面底部区块 */
.stream-item.is-confirm {
  background: rgba(245, 158, 11, .07);
  border-color: rgba(245, 158, 11, .30);
}
.stream-item.is-confirm .icon { background: var(--warn); }
.confirm-note {
  width: 100%; margin-top: 10px;
  padding: 9px 12px;
  border: 1.5px solid rgba(31, 36, 48, .12);
  border-radius: var(--radius-md);
  background: #FBFCFE; color: var(--text); font-size: 13px;
}
.confirm-note:focus { outline: 2px solid rgba(245, 158, 11, .25); border-color: var(--warn); }
.confirm-note:disabled { background: var(--bg-2); color: var(--text-2); cursor: not-allowed; }

/* ---------- 过程缩略条：结论突出，过程收起来（运行中转圈表示 Loading） ---------- */
.stream-process {
  margin: 8px 0 10px;
  border: 1px dashed rgba(31, 36, 48, .12);
  border-radius: var(--radius-md);
  background: rgba(31, 36, 48, .015);
}
.stream-process > summary {
  display: flex; align-items: center; gap: 8px;
  padding: 7px 12px;
  cursor: pointer; list-style: none;
  font-size: 12.5px; color: var(--text-2);
}
.stream-process > summary::-webkit-details-marker { display: none; }
.stream-process > summary:hover { color: var(--text); }
.stream-process .proc-text { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* 环形转圈：只用现有主色渐变，运行中才转 */
.stream-process .proc-spinner {
  flex: none; width: 13px; height: 13px; border-radius: 50%;
  background: conic-gradient(from 0deg, rgba(99, 102, 241, 0), var(--primary-2), var(--primary-3));
  -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 2px), #000 calc(100% - 2px));
  mask: radial-gradient(farthest-side, transparent calc(100% - 2px), #000 calc(100% - 2px));
}
.stream-process.is-running .proc-spinner { animation: proc-spin 1s linear infinite; }
.stream-process:not(.is-running) .proc-spinner {
  background: var(--ok);
  -webkit-mask: none; mask: none;
  opacity: .55;
}
@keyframes proc-spin { to { transform: rotate(360deg); } }
/* 右侧箭头：收起朝下、展开朝上 */
.stream-process .proc-caret {
  flex: none; width: 6px; height: 6px;
  border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor;
  transform: rotate(45deg) translate(-1px, -1px);
  transition: transform .2s ease;
}
.stream-process[open] > summary .proc-caret { transform: rotate(225deg) translate(-1px, -1px); }
.stream-process-body {
  padding: 2px 12px 10px;
  display: flex; flex-direction: column; gap: 8px;
}
.stream-process-body .stream-item { background: var(--surface); }

/* 对话输入行：**常驻**（不再靠显隐表达状态）。任务进行中置灰，同排露出「停止这个任务」。
   上边距为 0：外层 `.composer` 统一管间距（附件胶囊行也在里面） */
.follow-up { display: flex; gap: 10px; margin-top: 0; }
.follow-up input {
  flex: 1; min-width: 0;
  padding: 10px 14px;
  border: 1.5px solid rgba(31, 36, 48, .12);
  border-radius: var(--radius-md);
  background: #FBFCFE; color: var(--text);
}
.follow-up input:focus { outline: 2px solid rgba(99, 102, 241, .25); border-color: var(--primary-2); }
.follow-up .btn { flex: none; }
/* 不可用态：浅灰底 + 次要文字色 + 禁用光标，一眼看出「现在不能发」 */
.follow-up input:disabled {
  background: var(--bg-2); color: var(--text-2); cursor: not-allowed;
  border-color: rgba(31, 36, 48, .08);
}
.follow-up .btn:disabled { opacity: .55; cursor: not-allowed; }
/* 输入行下方一行状态说明（进行中 / 上传中 / 可直接对话） */
.composer-hint { margin-top: 8px; font-size: 12.5px; color: var(--text-2); }
.stream-separator {
  text-align: center; font-size: 11.5px; color: var(--text-2);
  margin: 8px 0; letter-spacing: 1px;
}

/* 工具数据小表 */
.data-table { width: 100%; border-collapse: collapse; font-size: 12.5px; min-width: 560px; }
.data-table th {
  background: var(--bg-2); color: var(--text-2); text-align: left;
  padding: 7px 9px; font-weight: 600; white-space: nowrap;
}
.data-table td { padding: 7px 9px; border-top: 1px solid rgba(31, 36, 48, .06); vertical-align: top; }
.data-table .num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
/* 网址列（个人网站作业：作品本体就是网址）：长了要能截断，别把表格撑破 */
.data-table .cell-url { max-width: 240px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.data-table .cell-url a { color: var(--primary); text-decoration: none; }
.data-table .cell-url a:hover { text-decoration: underline; }
.data-table tr.is-danger { background: rgba(239, 68, 68, .07); box-shadow: inset 3px 0 0 var(--danger); }
.data-table tr.is-muted td { color: var(--text-2); }

/* 提交进度表的**分组表头**（正在上传 / 待上传 / 已完成 / 未预检）：
   一行跨整表、弱化成"分区标题"而不是数据行 —— 与数据行必须一眼分得开，
   否则老师会把"正在上传（1 个班）"读成一个班的名字。 */
.data-table tr.board-group td {
  background: var(--bg-2); color: var(--text-2); font-weight: 600;
  padding: 5px 9px; border-top: 1px solid rgba(31, 36, 48, .08);
}

/* 「需要处理的待办」卡：它回答的是"谁没交上来、我该做什么" —— 必须比过程行重，
   但不能红成报错（大多数待办不是失败，是"学生交的东西我们处理不了"）。
   左侧那道竖线沿用坑位告警的视觉语言（inset 3px），老师一眼能分出"这是一件要办的事"。 */
.inline-card.action-board {
  border-left: 3px solid var(--warn, #d97706);
  background: rgba(217, 119, 6, .06);
}
.inline-card.action-board .data-table { margin-top: 6px; }

/* 执行流头部的「显示内部日志」：默认态是一枚安静的幽灵按钮，展开后高亮，
   表示"你现在看到的是全部原始事件"。 */
.stream-diag { margin-left: auto; }
.stream-diag.is-on {
  border-color: var(--warn, #d97706);
  color: var(--warn, #d97706);
}

/* 摘要小格 */
.mini-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 8px; }
.mini-item { padding: 9px 11px; border-radius: var(--radius-sm); background: var(--bg); border: 1px solid rgba(31, 36, 48, .05); }
.mini-item .k { font-size: 11.5px; color: var(--text-2); }
.mini-item b { font-size: 17px; font-variant-numeric: tabular-nums; }

/* 未交名单 chips */
.chips { display: flex; flex-wrap: wrap; gap: 6px; }
.chip {
  font-size: 12px; padding: 3px 9px; border-radius: 999px;
  background: var(--bg-2); color: var(--text-2); white-space: nowrap;
}
.chip.is-warn { background: rgba(245, 158, 11, .16); color: #92400E; }

/* ---------- 确认卡 ---------- */
.confirm-card {
  padding: 14px 16px; border-radius: var(--radius-md);
  background: rgba(245, 158, 11, .08);
  border: 1px solid rgba(245, 158, 11, .3);
  animation: pop .3s ease both;
}
.confirm-card .confirm-title { display: flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 600; }
.confirm-card .confirm-detail { font-size: 13px; color: var(--text-2); margin-top: 8px; white-space: pre-wrap; }
.confirm-options { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
.confirm-options .btn { margin-top: 0; width: auto; }
.confirm-options .btn-danger { background: var(--danger); color: #fff; }
.confirm-card.is-resolved { background: var(--bg); border-color: rgba(31, 36, 48, .08); }
/* 历史里的确认卡：只做回看（按钮已撤掉），压低存在感，不跟当前待办抢注意力 */
.confirm-card.is-history {
  background: var(--bg); border: 1px solid rgba(31, 36, 48, .06);
  animation: none; opacity: .92;
}
/* 识别卡**两栏**：左边「本次识别结果 + 依据」，右边可选列表（作业入口 / 班级）。
   窄屏自动堆叠成上下两段，不出现横向滚动。 */
.confirm-body { display: grid; grid-template-columns: minmax(0, 1fr) minmax(200px, 300px); gap: 14px; }
.confirm-summary, .confirm-choices { min-width: 0; }
.confirm-group + .confirm-group { margin-top: 10px; }
.confirm-group-title { font-size: 12.5px; font-weight: 500; color: var(--text); margin-bottom: 6px; }
.confirm-group-title .muted { font-weight: 400; margin-left: 4px; }
/* 右栏是"一列清单"：纵向排列，别挤成一排按钮 */
.confirm-choices .confirm-options { flex-direction: column; align-items: stretch; margin-top: 0; }
.confirm-choices .confirm-option { text-align: left; }
@media (max-width: 720px) {
  .confirm-body { grid-template-columns: minmax(0, 1fr); }
}
/* 识别卡：名录/候选按钮是**单选**（点一下只是选中），选中态要一眼看得出 */
.confirm-options .confirm-option { margin-top: 0; width: auto; }
.confirm-options .confirm-option.is-selected {
  background: rgba(99, 102, 241, .10);
  border-color: var(--primary-2);
  color: var(--primary);
  box-shadow: inset 0 0 0 1px rgba(99, 102, 241, .35);
}
/* 「确认提交」/「取消」：主次分明，且不跟上面的名录按钮挤成一片 */
.confirm-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 12px; }
.confirm-actions .btn { margin-top: 0; width: auto; }
/* 「发送补充说明」：只在备注有内容时出现，紧贴备注框下方 */
.confirm-note-send { margin-top: 8px; align-self: flex-start; width: auto; }
.confirm-card.is-revising { border-color: rgba(245, 158, 11, .45); }

/* ---------- 报告 ---------- */
.report-hero {
  display: flex; align-items: baseline; gap: 14px; flex-wrap: wrap;
  padding: 18px 22px; border-radius: var(--radius-md);
  background: linear-gradient(135deg, rgba(99, 102, 241, .10), rgba(139, 92, 246, .10));
  margin-bottom: 20px;
}
.report-hero .label { font-size: 13px; color: var(--text-2); }
.report-hero .big { font-size: 42px; font-weight: 800; line-height: 1; }
.report-hero .frac { font-size: 13px; color: var(--text-2); }
.bar-row { margin-bottom: 16px; }
.bar-row .head { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
.bar-row .cls { font-size: 14px; font-weight: 600; }
.bar-row .nums { font-size: 13px; color: var(--text-2); font-variant-numeric: tabular-nums; }
.bar-track { display: flex; height: 16px; border-radius: 999px; overflow: hidden; background: var(--bg-2); margin: 8px 0 6px; }
.bar-done { background: var(--grad); }
.bar-rest { background: var(--bg-2); }
.bar-row .foot { font-size: 12.5px; color: var(--text-2); }
.pill {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 3px 10px; border-radius: 999px; font-size: 12px;
  background: var(--bg-2); color: var(--text-2); white-space: nowrap;
}
.pill-ok { background: rgba(16, 185, 129, .12); color: #047857; }
.pill-warn { background: rgba(245, 158, 11, .16); color: #92400E; }
.pill-danger { background: rgba(239, 68, 68, .10); color: var(--danger); }
.pill-primary { background: rgba(99, 102, 241, .12); color: var(--primary); }
/* 「跳过」= 已交但这次没触发平台重新抓取截图：用中性色与「成功」区分开，
   既不吓人（不是失败），也不让人以为平台又跑了一遍。 */
.pill-skip { background: rgba(100, 116, 139, .14); color: #475569; }

/* ---------- 响应式 ---------- */
@media (max-width: 1024px) {
  .layout { grid-template-columns: 1fr; }
}
@media (max-width: 768px) {
  .topbar, .tabs, .main, .statusbar { padding-left: 16px; padding-right: 16px; }
  /* 顶栏现在要放三样：环境胶囊 + 状态灯 + 身份徽章 ⇒ 窄屏必须腾地方，
     否则身份徽章会被挤出屏幕（实测 420px 下溢出 ~31px）。
     腾法是"收品牌文字、截身份名、收紧胶囊内边距"，**不砍状态文字** ——
     「检查完成 / N 项待处理 / 待连接」正是老师要瞟的那几个字。 */
  .brand-text { display: none; }
  .topbar-actions { gap: 8px; }
  .ta-badge { padding: 7px 10px; }
  .ta-badge #taName { max-width: 96px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .agent-health.is-docked .health-bar { padding: 0 9px; font-size: 12px; }
  .steps { gap: 4px; overflow: auto; }
  .step { padding: 6px 12px; font-size: 13px; }
  .step + .step::before { width: 12px; }
  .class-grid { grid-template-columns: 1fr; }
  .btn { min-height: 44px; }
  .statusbar { gap: 10px; font-size: 11px; }

  /* 窄屏只留数字序号，当前步骤额外显示名字：六步仍是一行，不换行 */
  .steps-agent { gap: 3px; overflow-x: auto; }
  .steps-agent .step { padding: 5px 8px; font-size: 12px; }
  .steps-agent .step > span:last-child { display: none; }
  .steps-agent .step.is-current > span:last-child { display: inline; }
  .steps-agent .step.is-current { flex-grow: 2.4; }
  .steps-agent .step + .step::before { width: 6px; }

  /* 对话输入行：窄屏让输入框独占一行，按钮落到下一行，避免挤成一团 */
  .follow-up { flex-wrap: wrap; }
  .follow-up input { flex: 1 1 100%; }

  /* 过程缩略条：窄屏收紧内边距，摘要行仍是一行（长文案省略） */
  .stream-process > summary { padding: 7px 10px; }
  .stream-process-body { padding: 2px 10px 10px; }
}

@media (prefers-reduced-motion: reduce) {
  /* 作业页子 tab：滑块瞬时到位、内容不淡入 —— 位置仍然正确，只是不"滑" */
  .hw-subtab-ind { transition: none; }
  .hw-pane.is-active { animation: none; }
  /* 动效停下，但**绝不静默**：静态的彩虹霓虹流光与光环都还在 ——
     老师仍然一眼看得出"正在跑的是哪一步、下一步在等待"。 */
  .steps-agent .step.is-current,
  .steps-agent .step.is-waiting { animation: none; }
  .steps-agent .step.is-current::after { animation: none; opacity: .55; }
  /* 等待中：不动的时候改用**虚线描边**区分，别和"进行中"混成一种 */
  .steps-agent .step.is-waiting {
    animation: none;
    box-shadow: inset 0 0 0 1.5px rgba(31, 36, 48, .16);
  }
  .steps-agent .step.is-waiting::after { animation: none; opacity: .3; }
  .steps-agent .step.is-current .step-no::after,
  .steps-agent .step.is-waiting .step-no::after { animation: none; }
  .steps-agent .step.is-waiting .step-no::after { opacity: .35; }
  .steps-agent .step.is-stalled::after,
  .steps-agent .step.is-stalled .step-no::after { animation: none; opacity: .22; }
  .health-bar.is-checking::after { animation: none; opacity: .35; }
  .health-item.is-pending .health-value { animation: none; }
  .health-more::before { transition: none; }
  /* 过程缩略条：转圈退化为静态指示 */
  .stream-process.is-running .proc-spinner { animation: none; opacity: .75; }
  .stream-process .proc-caret { transition: none; }
  /* ① 区获取入口时的转圈：同样退化为静态指示 */
  .loading-ring { animation: none; opacity: .6; }
  /* 智能体状态灯：「正在为你服务」的霓虹环停下 —— 静止成"形状固定的彩虹环"，
     不做透明度衰减：它同时承载"这一盏正在为你干活"的信息，不该被弱化掉。 */
  .slot-light.is-mine::after { animation: none; }
  /* 课次表识别中那条流光：停下但**留着**（它承载"正在识别"这一个状态）；
     校对表的入场动画同样去掉。降级规则一律收在这一处，不另立一套。 */
  .roster-status.is-busy .roster-status-flow { animation: none; opacity: .35; }
  .roster-review { animation: none; }
  /* 登录门：错误条不再抖动、卡片不再浮入，骨架环退化为静态指示（仍能看出"在检查"） */
  .login-card, .login-alert.is-shake, .login-skeleton::before,
  .btn.is-loading::after { animation: none; }
  .login-submit:not(:disabled):hover { transform: none; }
}

/* ---------- 学生画像 ---------- */

.pf-toolbar {
  background: var(--surface);
  border-radius: var(--radius-lg);
  padding: 18px 20px;
  box-shadow: var(--shadow-sm);
  border: 1px solid rgba(31, 36, 48, .05);
}
.pf-toolbar-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
.pf-classbar { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.pf-select { display: flex; align-items: center; gap: 10px; flex: 1; min-width: 260px; max-width: 620px; }
.pf-select-label { flex: none; font-size: 13px; color: var(--text-2); }
.pf-select select {
  flex: 1; min-width: 0;
  appearance: none; cursor: pointer;
  padding: 11px 38px 11px 16px;
  border-radius: var(--radius-md);
  border: 1.5px solid rgba(31, 36, 48, .1);
  background:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5 6 6.5 11 1.5' fill='none' stroke='%235B6478' stroke-width='1.6' stroke-linecap='round'/%3E%3C/svg%3E")
    no-repeat right 14px center / 12px 8px,
    var(--surface);
  font-size: 15px; font-weight: 600; color: var(--text);
  transition: border-color .2s ease, box-shadow .2s ease;
}
.pf-select select:hover { border-color: var(--primary-2); }
.pf-select select:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 4px rgba(99, 102, 241, .14); }
.pf-class-hint { font-size: 12px; color: var(--text-2); }

.pf-pane { display: none; }
.pf-pane.is-active { display: block; animation: fade-up .3s ease both; }

.pf-stats {
  display: flex; flex-wrap: wrap; gap: 20px; align-items: center;
  background: var(--surface); border-radius: var(--radius-lg);
  padding: 15px 20px; margin-bottom: 14px;
  box-shadow: var(--shadow-sm); border: 1px solid rgba(31, 36, 48, .05);
}
.pf-stat { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--text-2); }
.pf-stat strong { font-size: 19px; font-weight: 600; color: var(--text); }
.pf-dot { width: 10px; height: 10px; border-radius: 50%; flex: none; }
.pf-dot.is-red { background: var(--light-red); }
.pf-dot.is-yellow { background: var(--light-yellow); }
.pf-dot.is-green { background: var(--light-green); }

.pf-filters { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 14px; }
.pf-chip {
  appearance: none; cursor: pointer;
  border: 1px solid rgba(31, 36, 48, .1); background: var(--surface);
  border-radius: 999px; padding: 6px 14px; font-size: 13px; color: var(--text-2);
  transition: all .18s ease;
}
.pf-chip:hover { border-color: var(--primary-2); color: var(--text); }
.pf-chip.is-active { background: var(--grad); color: #fff; border-color: transparent; }

.pf-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 14px; }
.pf-card {
  position: relative; overflow: hidden;
  background: var(--surface);
  border-radius: var(--radius-md);
  padding: 15px 16px 12px 20px;
  box-shadow: var(--shadow-sm);
  border: 1px solid rgba(31, 36, 48, .06);
  transition: transform .22s ease, box-shadow .22s ease;
  animation: fade-up .3s ease both;
}
.pf-card::before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 4px; background: var(--light-color, var(--bg-2)); }
.pf-card:hover { transform: translateY(-4px); box-shadow: var(--shadow-md); }
.pf-card.is-red { --light-color: var(--light-red); --light-color-soft: var(--light-red-soft); }
.pf-card.is-yellow { --light-color: var(--light-yellow); --light-color-soft: var(--light-yellow-soft); }
.pf-card.is-green { --light-color: var(--light-green); --light-color-soft: var(--light-green-soft); }

.pf-card-top { display: flex; align-items: center; gap: 10px; }
.pf-name { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.pf-name strong { font-size: 16px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pf-name span { font-size: 11px; color: var(--text-2); font-family: ui-monospace, Menlo, Consolas, monospace; }
.pf-lamp {
  width: 18px; height: 18px; border-radius: 50%; flex: none;
  background: var(--light-color);
  box-shadow: 0 0 0 4px var(--light-color-soft, rgba(31, 36, 48, .06));
  animation: lamp-pulse 2.4s ease-in-out infinite;
}
.pf-card.is-red .pf-lamp { animation-duration: 1.3s; }
@keyframes lamp-pulse {
  0%, 100% { transform: scale(1); opacity: 1; }
  50% { transform: scale(1.14); opacity: .8; }
}

.pf-tagline { display: flex; gap: 6px; flex-wrap: wrap; margin: 9px 0 10px; }
.pf-pill { padding: 2px 9px; border-radius: 999px; background: var(--bg-2); font-size: 11px; color: var(--text-2); }
.pf-pill.is-cluster { background: rgba(139, 92, 246, .12); color: var(--primary-3); }
.pf-pill.is-pending { background: var(--light-yellow-soft); color: #B4740C; }

.pf-more {
  appearance: none; border: 0; cursor: pointer; width: 100%;
  padding: 8px; border-radius: var(--radius-sm);
  background: var(--bg-2); color: var(--text-2); font-size: 12px;
  transition: background .2s ease, color .2s ease;
}
.pf-more:hover { background: #E4E7F2; color: var(--primary); }

.pf-empty { text-align: center; padding: 46px 20px; color: var(--text-2); }
.pf-empty p { margin: 6px 0; }

/* 导出名单的下拉菜单：点「导出名单 ▾」弹出「导出 CSV / 导出 PDF」 */
.pf-dropdown { position: relative; }
.pf-dropdown-menu {
  position: absolute; top: calc(100% + 6px); right: 0; z-index: 20;
  display: flex; flex-direction: column; gap: 2px;
  min-width: 172px; padding: 6px; border-radius: var(--radius-md);
  background: var(--surface); border: 1px solid rgba(31, 36, 48, .08);
  box-shadow: 0 10px 28px rgba(31, 36, 48, .16);
  animation: fade-up .16s ease both;
}
/* `display:flex` 会盖掉 `hidden`，得显式关掉 */
.pf-dropdown-menu[hidden] { display: none; }
.pf-dropdown-item {
  display: flex; flex-direction: column; align-items: flex-start; gap: 1px;
  border: 0; background: none; cursor: pointer; text-align: left;
  padding: 8px 12px; border-radius: var(--radius-sm);
  font-size: 13px; color: var(--text); font-family: inherit;
  transition: background .15s ease, color .15s ease;
}
.pf-dropdown-item:hover { background: rgba(99, 102, 241, .08); color: var(--primary); }
.pf-dropdown-item i { font-style: normal; font-size: 11px; color: var(--text-2); }

.pf-drawer { position: fixed; inset: 0; z-index: 40; }
.pf-drawer-mask { position: absolute; inset: 0; background: rgba(31, 36, 48, .32); backdrop-filter: blur(2px); animation: fade-in .2s ease both; }
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
.pf-drawer-panel {
  position: absolute; top: 0; right: 0; bottom: 0; width: min(470px, 93vw);
  background: var(--surface); box-shadow: var(--shadow-lg);
  padding: 26px 24px; overflow: auto;
  animation: slide-in .28s cubic-bezier(.22, .8, .3, 1) both;
}
@keyframes slide-in { from { transform: translateX(26px); opacity: 0; } to { transform: none; opacity: 1; } }
.pf-drawer-close {
  position: absolute; top: 14px; right: 16px;
  appearance: none; border: 0; cursor: pointer;
  width: 30px; height: 30px; border-radius: 50%;
  background: var(--bg-2); color: var(--text-2); font-size: 17px; line-height: 1;
}
.pf-drawer-close:hover { background: #E4E7F2; color: var(--text); }

.pf-hero { display: flex; align-items: center; gap: 12px; margin-bottom: 4px; }
.pf-hero-lamp { width: 26px; height: 26px; border-radius: 50%; flex: none; box-shadow: 0 0 0 6px rgba(31, 36, 48, .05); }
.pf-hero h3 { font-size: 20px; }
.pf-hero-sub { color: var(--text-2); font-size: 12px; margin-bottom: 4px; }

.pf-section { margin-top: 22px; }
.pf-section > h4 { font-size: 13px; font-weight: 600; color: var(--text-2); letter-spacing: .3px; margin-bottom: 10px; }

/* ---------- 卡片与抽屉的按钮行 ---------- */

.pf-do-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 11px; }
.pf-card .pf-do-actions { margin-top: 10px; }
.pf-card .pf-more { flex: 1; width: auto; }

/* ---------- 抽屉：只剩 四项指标 + 学生情况记录 ---------- */

.pf-section.is-first { margin-top: 16px; }

.alert.is-ok { background: rgba(16, 185, 129, .08); border-color: rgba(16, 185, 129, .22); color: #0B7A5B; }


.pf-modal-hint strong { color: var(--primary); }
.alert.is-error { background: rgba(239, 68, 68, .08); border-color: rgba(239, 68, 68, .22); color: var(--danger); }

@media (max-width: 768px) {
  .pf-grid { grid-template-columns: repeat(auto-fill, minmax(158px, 1fr)); gap: 10px; }
  .pf-drawer-panel { width: 100%; }
}

/* ==================================================================
   课次记录：选课次 → 填电子表格 → 课次表照片留档 → 回传
   照片**不再识别**（标记由老师亲手填），所以这一块只有"填"与"回传"两件事。
   与「环境检查」那条胶囊共用同一套动效语言：流光层自带 `border-radius: inherit`，
   父级**不许**加 overflow: hidden（会把子级浮层一起裁掉）。
   ================================================================== */

/* ---------- 留档区：两个子 tab（照片 / CSV） ----------
   2026-10-16 新增 CSV 通道。两枚胶囊等分，选中态沿用卡片那套靛蓝渐变。
   ⚠️ 两个面板**共用下面那一颗提交按钮** —— 换 tab 只换面板与文案，不新增第二个提交动作。 */
.roster-subtabs {
  display: flex; gap: 4px; margin: 22px 0 0;
  padding: 4px; border-radius: 14px; background: var(--bg-2);
}
.roster-subtab {
  flex: 1; appearance: none; border: 0; cursor: pointer;
  padding: 9px 12px; border-radius: 11px;
  background: transparent; color: var(--text-2);
  font: inherit; font-size: 13px; font-weight: 600;
  transition: background .2s ease, color .2s ease, transform .15s ease, box-shadow .2s ease;
}
.roster-subtab:hover { color: var(--text); background: rgba(31, 36, 48, .05); transform: translateY(-1px); }
.roster-subtab.is-active { background: var(--grad); color: #fff; box-shadow: var(--shadow-sm); }
.roster-subtab.is-active:hover { transform: none; }

/* 两个面板：切换时淡入上浮（隐藏态由全局 `[hidden] { display: none !important }` 兜住） */
.roster-panel { padding-top: 4px; }
.roster-panel.is-active { animation: fade-up .28s ease both; }

/* CSV 选中后的文件行：没有缩略图，左侧一枚文字徽标（照片那半用的是 `.preview`） */
.csv-file {
  display: flex; align-items: center; gap: 10px; margin-top: 14px;
  padding: 10px 12px; border-radius: var(--radius-md);
  background: var(--bg-2); border: 1px solid rgba(31, 36, 48, .06);
}
.csv-badge {
  flex: 0 0 auto; padding: 4px 8px; border-radius: 8px;
  background: rgba(99, 102, 241, .14); color: var(--primary);
  font-size: 11.5px; font-weight: 700; letter-spacing: .04em;
}
.csv-name {
  flex: 1; min-width: 0; font-size: 13px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.csv-file .btn { margin-top: 0; }

/* CSV 解析报告：浅靛蓝底 + 左侧一道竖条，逐行一句话。
   颜色只作强调 —— **每一行都自带文字标签**（"已回填 4 格" / "没有回填：…"）。 */
.csv-report {
  margin-top: 12px; padding: 12px 14px;
  border-left: 3px solid var(--primary-2);
  border-radius: var(--radius-md);
  background: rgba(99, 102, 241, .06);
}
.csv-report-line { font-size: 12.5px; line-height: 1.55; color: var(--text); }
.csv-report-line + .csv-report-line { margin-top: 4px; }
.csv-report-line.is-ok { color: #047857; }
.csv-report-line.is-warn { color: #92400E; }
.csv-report-line.is-fail { color: #B91C1C; }
.csv-report-line.is-muted { color: var(--text-2); font-size: 12px; }

/* 课次疑似选错：**红色气泡**（2026-10-26）。
   原先这条提示是报告最末一行灰字（`.is-muted`），老师压根没看见 —— 2026-10-24 一份
   10-24 的课次表被交到了 10-31 那节课上，17 行标记整批挂错课次。
   这里升成气泡：红底 + 红边 + ⚠，排在整个报告最上面，并弹入一次抓眼睛；
   整块报告的左侧竖条同步转红（`.has-alert`），保证一屏之内能看出「这次有问题」。 */
.csv-report.has-alert {
  border-left-color: var(--danger);
  background: rgba(239, 68, 68, .05);
}
.csv-report-alert {
  display: flex; align-items: flex-start; gap: 8px;
  margin-bottom: 10px; padding: 9px 12px;
  border-radius: 12px;
  border: 1px solid rgba(239, 68, 68, .45);
  background: rgba(239, 68, 68, .12);
  color: #991B1B; font-size: 12.5px; font-weight: 600; line-height: 1.55;
  animation: csv-alert-pop .32s cubic-bezier(.22, .8, .3, 1) both;
}
.csv-report-alert-icon { flex: 0 0 auto; font-size: 14px; line-height: 1.35; }
.csv-report-alert-text { flex: 1; min-width: 0; }
@keyframes csv-alert-pop {
  from { transform: scale(.97); opacity: 0; }
  to { transform: none; opacity: 1; }
}

/* 照片那一半的小标题：把卡片分成「填表」与「留档」上下两段 */
.roster-photo-head {
  display: flex; align-items: baseline; justify-content: space-between;
  margin: 22px 0 10px; padding-top: 16px;
  border-top: 1px solid rgba(31, 36, 48, .08);
}
.roster-panel .roster-photo-head { margin-top: 14px; }
.roster-photo-head h3 { margin: 0; font-size: 14px; font-weight: 600; }

.roster-hint { margin-top: 10px; }

/* 状态条：一条窄胶囊 + 内部来回流动的流光（只在"正在忙"时转） */
.roster-status {
  position: relative;
  display: flex; align-items: center;
  margin-top: 12px; padding: 9px 14px;
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  background: var(--bg-2); color: var(--text-2);
  font-size: 13px; line-height: 1.4;
  overflow: visible;
}
.roster-status-text { position: relative; z-index: 1; }
.roster-status-flow {
  position: absolute; inset: 0; z-index: 0; pointer-events: none;
  border-radius: inherit; opacity: 0;
  background: linear-gradient(100deg, transparent 32%, rgba(255, 255, 255, .85) 50%, transparent 68%);
  background-size: 220% 100%;
}
.roster-status.is-busy {
  background: rgba(245, 158, 11, .14);
  border-color: rgba(245, 158, 11, .34);
  color: #92400E;
}
.roster-status.is-busy .roster-status-flow {
  opacity: 1;
  animation: health-sweep 1.4s linear infinite;
}
.roster-status[data-kind="ok"] {
  background: rgba(16, 185, 129, .12);
  border-color: rgba(16, 185, 129, .3);
  color: #047857;
}
.roster-status[data-kind="fail"] {
  background: rgba(239, 68, 68, .08);
  border-color: rgba(239, 68, 68, .24);
  color: #B91C1C;
}

/* ---------- 学生画像 · 本周待办（页面最顶部，只出数字，不列名单） ---------- */

.pf-todo { margin-bottom: 18px; }
.pf-todo .card-head { margin-bottom: 10px; }
.pf-todo-bar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; }
/* `pre-line`：说明文字分三行（口径 / 两条行动 / 名单入口），文本里的 \n 要真的换行 */
.pf-todo-hint { flex: 1 1 220px; font-size: 12px; color: var(--text-2); line-height: 1.7; white-space: pre-line; }
.pf-todo-bar .btn { margin-top: 0; width: auto; }

/* 三个数字：需关注总人数 / 需上台 / 需关注作业 */
.pf-todo-stats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }
.pf-todo-stat {
  display: flex; flex-direction: column; gap: 2px;
  padding: 12px 14px; border-radius: var(--radius-md);
  background: var(--bg); border: 1px solid rgba(31, 36, 48, .07);
}
.pf-todo-stat strong { font-size: 26px; font-weight: 600; letter-spacing: -.5px; line-height: 1.15; }
.pf-todo-stat span { font-size: 12px; color: var(--text-2); }
.pf-todo-stat.is-stage { border-color: rgba(245, 165, 36, .38); }
.pf-todo-stat.is-stage strong { color: #B4740C; }
.pf-todo-stat.is-hw { border-color: rgba(99, 102, 241, .3); }
.pf-todo-stat.is-hw strong { color: var(--primary); }

.pf-todo-line { margin-top: 10px; font-size: 12.5px; color: var(--text-2); }

/* 查看名单：下拉展开 / 收起。**只读** ——
   「完成情况」只是个标识（已完成 / 待完成），由老师上传的回执识别后更新，页面上没有勾选框。 */
.pf-todo-list { margin-top: 12px; }
.pf-todo-list > summary {
  display: inline-flex; align-items: center; gap: 5px; cursor: pointer; list-style: none;
  font-size: 12.5px; color: var(--primary);
  padding: 5px 13px 5px 9px; border-radius: 999px;
  border: 1px solid rgba(99, 102, 241, .22); background: rgba(99, 102, 241, .06);
  transition: border-color .15s ease;
}
.pf-todo-list > summary::-webkit-details-marker { display: none; }
.pf-todo-list > summary::before { content: "▸"; font-size: 11px; }
.pf-todo-list[open] > summary::before { content: "▾"; }
.pf-todo-list > summary:hover { border-color: var(--primary-2); }
.pf-todo-list-count { color: var(--text-2); font-weight: 400; }

.pf-todo-list-wrap { overflow-x: auto; margin-top: 10px; }
.pf-todo-list-table {
  width: 100%; border-collapse: separate; border-spacing: 0;
  font-size: 12.5px; min-width: 520px;
}
.pf-todo-list-table th {
  text-align: left; font-weight: 600; font-size: 12px; color: var(--text-2);
  padding: 7px 10px; background: var(--bg-2); white-space: nowrap;
  border-bottom: 1px solid rgba(31, 36, 48, .08);
}
.pf-todo-list-table td { padding: 7px 10px; border-bottom: 1px solid rgba(31, 36, 48, .06); }
.pf-todo-list-table tr:last-child td { border-bottom: 0; }
.pf-todo-list-table tbody tr:hover td { background: rgba(99, 102, 241, .04); }
.pf-todo-list-table tr.is-done td { color: var(--text-2); }
.pf-todo-uid { font-family: ui-monospace, Menlo, Consolas, monospace; color: var(--text-2); }
.pf-todo-status {
  display: inline-block; padding: 1px 9px; border-radius: 999px; white-space: nowrap;
  font-size: 11.5px; background: var(--light-yellow-soft); color: #B4740C;
}
.pf-todo-status.is-done { background: rgba(16, 185, 129, .12); color: #0B7A5B; }

/* ---------- 上传回执的小框（选文件 → 上传并识别 → 就地看结果） ---------- */

.pf-modal { position: fixed; inset: 0; z-index: 50; display: flex; align-items: center; justify-content: center; padding: 20px; }
.pf-modal-mask { position: absolute; inset: 0; background: rgba(31, 36, 48, .34); backdrop-filter: blur(2px); animation: fade-in .2s ease both; }
.pf-modal-panel {
  position: relative; width: min(520px, 100%); max-height: 86vh; overflow: auto;
  padding: 22px 24px 24px; border-radius: var(--radius-lg);
  background: var(--surface); box-shadow: var(--shadow-lg);
  animation: fade-up .24s ease both;
}
.pf-modal-close {
  position: absolute; right: 14px; top: 14px; border: 0; cursor: pointer;
  width: 30px; height: 30px; border-radius: 50%;
  background: var(--bg-2); color: var(--text-2); font-size: 17px; line-height: 1;
}
.pf-modal-close:hover { background: #E4E7F2; color: var(--text); }
.pf-modal-panel h3 { font-size: 17px; margin: 0 0 8px; }
.pf-modal-hint { font-size: 12.5px; color: var(--text-2); line-height: 1.75; margin: 0 0 16px; }
.pf-receipt-em { color: var(--primary); font-weight: 600; }

.pf-file-pick {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap; cursor: pointer;
  padding: 12px 14px; border-radius: var(--radius-md);
  border: 1px dashed rgba(31, 36, 48, .2); background: var(--bg);
}
.pf-file-pick:hover { border-color: var(--primary-2); }
.pf-file-btn {
  padding: 7px 14px; border-radius: var(--radius-sm); font-size: 12.5px;
  background: var(--surface); border: 1px solid rgba(31, 36, 48, .14); color: var(--text);
}
.pf-file-name { font-size: 12.5px; color: var(--text-2); word-break: break-all; }

.pf-modal-panel > .btn-primary { width: 100%; margin-top: 14px; }
.pf-modal-panel > .btn-primary:disabled { opacity: .5; cursor: not-allowed; }

.pf-receipt-result {
  margin-top: 16px; padding: 12px 14px; border-radius: var(--radius-md);
  font-size: 13px; line-height: 1.85; background: var(--bg);
  border: 1px solid rgba(31, 36, 48, .08); color: var(--text);
}
.pf-receipt-result strong { display: block; margin-bottom: 4px; }
.pf-receipt-result.is-ok { background: rgba(16, 185, 129, .08); border-color: rgba(16, 185, 129, .24); }
.pf-receipt-result.is-error { background: rgba(239, 68, 68, .06); border-color: rgba(239, 68, 68, .22); }
.pf-receipt-skip { margin: 8px 0 0; padding-left: 18px; font-size: 12px; color: var(--text-2); }
.pf-receipt-skip li { margin: 2px 0; }
.pf-receipt-tip { margin-top: 8px; font-size: 12px; color: var(--text-2); }

@media (max-width: 560px) {
  .pf-modal-panel { padding: 18px; }
  .pf-dropdown-menu { right: auto; left: 0; }
}

/* ---------- 学生画像 · 详情：四项指标 + 备注流 ---------- */

.pf-ind-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
.pf-ind {
  display: flex; flex-direction: column; gap: 3px;
  padding: 12px 14px; border-radius: var(--radius-md);
  background: var(--bg); border: 1px solid rgba(31, 36, 48, .07);
}
.pf-ind-label { font-size: 12px; color: var(--text-2); display: flex; align-items: baseline; gap: 5px; }
.pf-ind-label i { font-style: normal; font-size: 10.5px; color: var(--primary-3); }
.pf-ind-value { font-size: 21px; font-weight: 600; color: var(--text); letter-spacing: -.3px; }
.pf-ind-avg { font-size: 11.5px; color: var(--text-2); }

.pf-note-form { display: flex; gap: 10px; align-items: flex-start; }
.pf-note-form textarea {
  flex: 1; resize: vertical; min-height: 46px; padding: 9px 11px; font-size: 13px;
  font-family: inherit; line-height: 1.6; color: var(--text);
  border: 1px solid rgba(31, 36, 48, .12); border-radius: var(--radius-sm); background: var(--bg);
  transition: border-color .15s ease, box-shadow .15s ease;
}
.pf-note-form textarea:focus { outline: none; border-color: var(--primary-2); box-shadow: 0 0 0 3px rgba(99, 102, 241, .12); }
.pf-note-form .btn { margin-top: 0; width: auto; }
.pf-note-hint { font-size: 12px; color: #0B7A5B; margin-top: 8px; min-height: 16px; }
.pf-note-hint.is-error { color: var(--danger); }
.pf-notes { display: flex; flex-direction: column; gap: 10px; margin-top: 6px; }
.pf-note-item {
  padding: 10px 12px; border-radius: var(--radius-md); background: var(--bg);
  border: 1px solid rgba(31, 36, 48, .07);
}
.pf-note-item.is-mine { background: rgba(99, 102, 241, .05); border-color: rgba(99, 102, 241, .16); }
.pf-note-meta { display: flex; align-items: center; gap: 10px; font-size: 11.5px; color: var(--text-2); margin-bottom: 5px; }
.pf-note-body { font-size: 13px; color: var(--text); line-height: 1.7; white-space: pre-wrap; word-break: break-word; }
.pf-note-del {
  margin-left: auto; border: 0; background: none; padding: 0; cursor: pointer;
  font-size: 11.5px; color: var(--text-2); font-family: inherit;
}
.pf-note-del:hover { color: var(--danger); }

@media (max-width: 560px) {
  .pf-ind-grid { grid-template-columns: 1fr; }
  .pf-todo-stats { grid-template-columns: 1fr; }
  .pf-note-form { flex-direction: column; }
  .pf-note-form .btn { width: 100%; }
}

/* 「需要注意的事」里临时追加的行：回传前置自检的结论要更显眼一点（红/黄） */
.roster-notes .is-error { color: #B91C1C; font-weight: 600; }
.roster-notes .is-warn { color: #8A5A08; }

.roster-review { margin-top: 18px; animation: fade-up .32s ease both; }

.roster-title { display: flex; flex-direction: column; gap: 2px; margin-bottom: 10px; }
.roster-title strong { font-size: 14px; word-break: break-all; }
.roster-title .muted { font-size: 12.5px; }

/* 需要注意的事：uid 问题会直接决定回传会不会被整批拒，所以整块是"要读"的橙色 */
.roster-notes {
  display: grid; gap: 4px;
  margin-bottom: 12px; padding: 10px 12px;
  border-radius: var(--radius-sm);
  background: var(--light-yellow-soft); color: #8A5A08;
  font-size: 12.5px; line-height: 1.5;
}

/* 表格：横向独立滚动 + 首列吸附（列数随课次变化，窄屏必然要滚） */
.roster-table-wrap {
  max-height: 460px; overflow: auto;
  border: 1px solid rgba(31, 36, 48, .08); border-radius: var(--radius-md);
  background: var(--surface);
}
.roster-table { width: 100%; border-collapse: separate; border-spacing: 0; font-size: 13px; }
.roster-table th {
  position: sticky; top: 0; z-index: 3;
  padding: 9px 10px; text-align: left; white-space: nowrap;
  background: var(--bg-2); color: var(--text-2);
  font-size: 12px; font-weight: 600;
  border-bottom: 1px solid rgba(31, 36, 48, .08);
}
.roster-table td {
  padding: 7px 10px;
  border-bottom: 1px solid rgba(31, 36, 48, .06);
  vertical-align: middle;
}
.roster-table tr:last-child td { border-bottom: 0; }
.roster-table tbody tr:hover td { background: rgba(99, 102, 241, .04); }

.roster-col-id { position: sticky; left: 0; z-index: 2; background: var(--surface); min-width: 168px; }
.roster-table th.roster-col-id { z-index: 4; background: var(--bg-2); }
.roster-name { display: block; font-size: 13px; line-height: 1.3; }
/* 学号：**只读**（名单由平台给，页面上没有"改学号"这条路径），所以是一行小字而不是输入框 */
.roster-uid {
  display: block; margin-top: 3px;
  color: var(--text-2);
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 12.5px; letter-spacing: .3px;
}

.roster-col-checkpoint { width: 76px; text-align: center; }
.roster-col-note { min-width: 180px; }

/* 低上台提醒（名字前的 ▲）：与纸面表同一套语言 —— **加粗**（黑白打印也辨得出），不用颜色。
   它只是提示，勾不勾都收；这里不标红、不做任何"必须处理"的暗示。 */
.roster-table tr.is-alerted .roster-name { font-weight: 700; }

.roster-check {
  width: 20px; height: 20px; margin: 0; cursor: pointer;
  accent-color: var(--primary);
}
.roster-text {
  width: 100%; padding: 5px 8px;
  border-radius: 8px; border: 1px solid rgba(31, 36, 48, .12);
  background: var(--bg); color: var(--text); font-size: 13px;
  font-family: inherit;
}
.roster-text:focus {
  outline: none; border-color: var(--primary-2);
  box-shadow: 0 0 0 2px rgba(99, 102, 241, .18); background: var(--surface);
}

/* 表头上那颗小灰点 = 这一列有平台登记的 key（悬停给出 key 名） */
.roster-key-dot {
  display: inline-block; width: 5px; height: 5px; margin-left: 5px;
  border-radius: 50%; background: var(--primary-2); opacity: .55; vertical-align: middle;
}

/* 老师改过的格子：左侧一道靛蓝细边（比整格变色安静，也不与行级告警打架） */
.roster-table td.is-edited { box-shadow: inset 3px 0 0 var(--primary-2); }

/* CSV 回填过的格子：极淡靛蓝底 + 右上角一枚小圆点（`title` 上有文字说明）。
   与 `.is-edited` **可叠加**：值来自 CSV、老师后来又改过，两个标记同时在。
   三种状态一眼分得清：空 / CSV 填的 / 老师改的。 */
.roster-table td.is-from-csv { position: relative; background: rgba(99, 102, 241, .055); }
.roster-table td.is-from-csv::after {
  content: ''; position: absolute; top: 4px; right: 4px;
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--primary-2); opacity: .7;
}

.roster-foot {
  display: flex; align-items: center; flex-wrap: wrap; gap: 8px;
  margin-top: 14px;
}
.roster-summary { flex: 1; min-width: 180px; font-size: 12.5px; color: var(--text-2); }
.roster-foot .btn { margin-top: 0; }

/* 唯一的提交按钮（传图 + 回传一起做）：整行宽，它是这张卡唯一的终点动作 */
.roster-submit { width: 100%; margin-top: 14px; }

/* 校验指出的问题行（本地兜底 / 平台 422 的逐行明细）：红底 + 一行说明 */
.roster-table tr.is-rejected td { background: rgba(239, 68, 68, .075); }
.roster-problem { display: block; margin-top: 4px; font-size: 11.5px; line-height: 1.4; color: #B91C1C; }

@media (max-width: 720px) {
  .roster-col-id { min-width: 132px; }
  .roster-foot .btn { flex: 1 1 auto; width: 100%; }
  .roster-summary { flex-basis: 100%; }
  /* 窄屏：两枚子 tab 仍是一行两枚（不换行），只把内边距收紧 */
  .roster-subtab { padding: 8px 6px; font-size: 12.5px; }
  .dropzone { padding: 24px 16px; }
  .csv-file { flex-wrap: wrap; }
  .csv-report { padding: 10px 12px; }
}

/* ======================================================================
   登录门（公网部署：TA ID + MCP Token）
   ----------------------------------------------------------------------
   这一层是**未授权用户唯一可见的东西**，所以它自己必须站得住：
   · 不透明背景（不是半透明遮罩）—— 底下的班级/学生卡片一个字都不该漏出来；
   · `is-gated` 由 app.js 挂在 body 上，顶栏/页签/主区同时 visibility:hidden：
     看不见，也不会被 Tab 键摸到（`display:none` 会丢布局，visibility 只夺焦点）；
   · 表单态与「正在检查登录状态」骨架态二选一，两者都用 `hidden` 切换。
   ====================================================================== */

body.is-gated > header,
body.is-gated > nav,
body.is-gated > main { visibility: hidden; }

.login-gate {
  position: fixed;
  inset: 0;
  z-index: 9999;
  display: grid;
  place-items: center;
  padding: 24px;
  overflow: auto;
  background:
    radial-gradient(1100px 520px at 12% -6%, rgba(139, 92, 246, .20), transparent 62%),
    radial-gradient(820px 420px at 92% 4%, rgba(99, 102, 241, .16), transparent 64%),
    linear-gradient(180deg, var(--bg) 0%, var(--bg-2) 100%);
}

.login-card {
  position: relative;
  width: min(420px, 100%);
  padding: 32px 28px 20px;
  border-radius: var(--radius-lg);
  background: var(--surface);
  border: 1px solid rgba(79, 70, 229, .10);
  box-shadow: var(--shadow-lg);
  animation: login-rise .3s ease-out both;
}

/* 卡片顶部一层极轻的紫色光晕：让白卡不显得"贴在灰底上" */
.login-card::before {
  content: "";
  position: absolute;
  inset: 0 0 auto;
  height: 132px;
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  background: radial-gradient(260px 130px at 50% 0, rgba(99, 102, 241, .16), transparent 72%);
  pointer-events: none;
}

.login-brand {
  position: relative;
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 26px;
}

.login-skeleton {
  position: relative;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 0 26px;
  color: var(--text-2);
  font-size: 14px;
}
.login-skeleton::before {
  content: "";
  width: 14px; height: 14px;
  border-radius: 50%;
  border: 2px solid rgba(79, 70, 229, .18);
  border-top-color: var(--primary);
  animation: login-spin .8s linear infinite;
}

.login-head h1 {
  margin: 0 0 6px;
  font-size: 28px;
  font-weight: 600;
  letter-spacing: -.01em;
  color: var(--text);
}
.login-sub {
  margin: 0 0 22px;
  font-size: 14px;
  color: var(--text-2);
}
.login-sub b { color: var(--text); font-weight: 600; }

.login-field { display: block; margin-bottom: 14px; }
.login-label {
  display: block;
  margin-bottom: 6px;
  font-size: 12.5px;
  font-weight: 600;
  letter-spacing: .02em;
  color: var(--text-2);
}

.login-field input {
  width: 100%;
  height: 44px;
  padding: 0 14px;
  font-size: 14px;
  color: var(--text);
  background: #FBFBFD;
  border: 1px solid #E3E6EF;
  border-radius: 12px;
  outline: none;
  transition: border-color .16s ease, box-shadow .16s ease, background .16s ease;
}
.login-field input::placeholder { color: #A6ADBF; }
.login-field input:focus {
  background: #fff;
  border-color: var(--primary);
  box-shadow: 0 0 0 4px rgba(79, 70, 229, .12);
}
.login-field input:disabled { opacity: .6; cursor: not-allowed; }

.login-input-wrap { position: relative; display: block; }
.login-input-wrap input { padding-right: 68px; }
.login-eye {
  position: absolute;
  top: 50%;
  right: 6px;
  transform: translateY(-50%);
  padding: 6px 10px;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: var(--primary);
  font-size: 12.5px;
  font-weight: 500;
  cursor: pointer;
  transition: background .16s ease, color .16s ease;
}
.login-eye:hover { background: rgba(79, 70, 229, .08); }
.login-eye:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }

.login-alert {
  margin: 0 0 12px;
  padding: 10px 12px;
  border-radius: 10px;
  border-left: 3px solid var(--danger);
  background: #FEF2F2;
  color: #B91C1C;
  font-size: 13px;
  line-height: 1.5;
}
.login-alert.is-shake { animation: login-shake .2s ease-in-out 1; }

/* 主按钮：与 .btn-primary 同一套渐变/阴影语言，但整行宽、且没有 --reload 时代那 16px 上边距 */
.login-submit {
  width: 100%;
  margin-top: 6px;
  padding: 12px 16px;
  border-radius: 12px;
  background: var(--grad);
  color: #fff;
  font-size: 15px;
  font-weight: 500;
  box-shadow: var(--shadow-md);
}
.login-submit:not(:disabled):hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow-lg);
}
.login-submit:not(:disabled):active { transform: translateY(0); }

.login-hint {
  position: relative;
  margin: 16px 0 0;
  font-size: 12.5px;
  line-height: 1.6;
  color: var(--text-2);
}
.login-foot {
  position: relative;
  margin: 18px 0 0;
  padding-top: 12px;
  border-top: 1px solid rgba(31, 36, 48, .06);
  font-size: 11.5px;
  color: #9AA2B4;
  text-align: center;
}

@keyframes login-rise {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: none; }
}
@keyframes login-shake {
  0%, 100% { transform: translateX(0); }
  30% { transform: translateX(-4px); }
  65% { transform: translateX(4px); }
}
@keyframes login-spin { to { transform: rotate(360deg); } }

@media (max-width: 640px) {
  .login-gate {
    padding: 16px;
    align-items: start;
    padding-top: 10vh;
  }
  .login-card { padding: 26px 20px 16px; }
  .login-head h1 { font-size: 24px; }
  .login-brand { margin-bottom: 20px; }
  /* 顶栏在窄屏会收掉品牌文字（腾地方给身份徽章），登录卡里没有这个取舍 —— 品牌要完整 */
  .login-brand .brand-text { display: flex; }
  .login-foot { display: none; }
}
