/* ============================================================
   作业墙 · 默认主题「白板风」的骨相
   骨相: 白板上只有文字。无卡片、无投影、无装饰。
   ——这行从 ADR-0012 起**只对默认主题成立**。下面每个 body.theme-* 块各有一套骨相，
   但主题只活在 token 层（颜色 / 圆角 / 材质 / 字重）：DOM 结构、网格、断点与
   展示态那组 --show-* 跨主题唯一，谁都不许按主题分叉。
   编辑模式: 科代表录入。
   展示模式: 学生观看，自动刷新。
   ============================================================ */

/* ---------- Reset & Base ---------- */
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }

/* ---------- SVG 图标全局样式（统一描边，继承文字颜色） ---------- */
svg {
  width: 1.25em;
  height: 1.25em;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  flex: none;
}
button svg { pointer-events: none; }

/* 焦点环全站统一（ADR-0009）：动作色 2px，只给键盘焦点（:focus-visible），
   点按不出现环。原来这里没有统一规则，只有两条各自为政的控件级覆写，
   其余按钮全凭浏览器默认环——而表单字段里那句 outline:none 干脆把环抹掉了。
   :where() 把特异性压到 0，个别控件需要微调时可直接在组件规则里覆盖。 */
:where(a, button, input, select, textarea, [tabindex]):focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: var(--sp-1);
}

/* 拉丁语境专用（spec §3.4）：只含数字与拉丁字符、不含汉字的文本——
   时钟 14:30、44px、100%、A−/A+ 四处。中英混排串（"次日 18:00"）一律不吃它，
   否则一串里两种字。OFL 1.1 授权，随字体分发，绿色版仍是本地文件。 */
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('fonts/ibm-plex-sans-600-latin.woff2') format('woff2');
}

:root {
  --bg: #ffffff;
  --surface: #ffffff;
  --ink: #111111;
  --ink-dim: #6f6f6f;
  --ink-faint: #b8b8b8;
  --line: #e8e8e8;

  /* 动作色（ADR-0009）：只出现在可被按的东西上——按钮、选中态、焦点环。
     科目名 / 正文 / 截止时间等内容区永远不用它。白底 8.61:1，实算见 spec §3.1。
     它与数学蓝只差 16°，这是刻意接受的：动作在控件、科目色在内容，不同屏同角色。 */
  --accent: #263cba;
  --accent-strong: #1c2c8a;   /* accent 底上的 hover/pressed */
  --on-accent: #ffffff;       /* accent 底上的前景 */
  --on-ink: #ffffff;          /* 墨底上的前景 */

  /* 语义色脱离科目色池：现状是「错误」直接借用语文红、「离线」借用化学橙，
     同一块颜色既表示一门课又表示一种状态。s8 重排腾出琥珀，正好转作 --warn。 */
  --success: #1d7a3f;
  --danger: #b42318;
  --warn: #b45309;

  /* 科目色池：白底对比度 ≥4:1（投影/白板后排可辨识）；由 JS 按科目序号引用 --c-sN。
     s1-s6 = 六主科，锁死（含暗色档），本轮一字未改。
     s7-s12 = 表外科目，色相取六主科之间最宽的空档，与最近主科 ≥20°（spec §5）。 */
  --c-s1: #cf3227;
  --c-s2: #2f6fd0;
  --c-s3: #c23a8b;
  --c-s4: #0d8574;
  --c-s5: #c2410c;
  --c-s6: #2f8a2e;
  --c-s7: #9a7913;
  --c-s8: #4d8811;
  --c-s9: #129147;
  --c-s10: #1484a3;
  --c-s11: #6b18bf;
  --c-s12: #b618bf;

  /* 原先这里还有六行 --c-yu: var(--c-s1) 之类的语义别名，主题化轮删掉了，理由记在
     门禁 §7(a)-7e 那条反向断言里：别名只在 html 上声明，var() 在 html 的
     computed-value time 就代完了，挂在 body 上的主题块追不动它们。 */

  /* 间距阶梯：4 的倍数，2px 一档留给图标级缝隙。
     吸附三规则——① 取最近档；② 等距时上取；③ 可点击元素的命中区只增不减。 */
  --sp-1: 2px;
  --sp-2: 4px;
  --sp-3: 8px;
  --sp-4: 12px;
  --sp-5: 16px;
  --sp-6: 24px;
  --sp-7: 32px;
  --sp-8: 48px;
  --sp-9: 64px;

  /* 圆角：30px 胶囊在 25-48px 高的元素上会被夹成半高，与 --r-pill 逐像素相同，
     所以这条收敛零视觉变化，只是清掉魔法数字。 */
  --r-sm: 8px;
  --r-md: 12px;
  --r-lg: 16px;
  --r-pill: 999px;
  --r-full: 50%;

  --font-latin: 'IBM Plex Sans', 'PingFang SC', 'Microsoft YaHei', system-ui, sans-serif;
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --dur-fast: 150ms;
}

html { height: 100%; }

body {
  /* 首位曾是 'Noto Sans SC'，但为守绿色版免联网已移除网络字体，
     教室机上永远取不到它——留着等于白写一级回落。 */
  font-family: 'PingFang SC', 'Microsoft YaHei', system-ui, sans-serif;
  background: var(--bg);
  color: var(--ink);
  min-height: 100%;
  font-size: 16px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

/* ============================================================
   顶部一行
   ============================================================ */
.topbar {
  position: sticky;
  top: 0;
  z-index: 100;
  background: var(--bg);
}

.topbar-inner {
  width: 100%;
  padding: var(--sp-5) var(--sp-8);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-6);
  flex-wrap: wrap;
}

.brand {
  font-size: clamp(26px, 3vw, 34px);
  font-weight: 700;
  letter-spacing: 0.06em;
  white-space: nowrap;
}

/* 日期导航：工具行里的一个组，独占一行的事交给 .tool-row */
.date-nav {
  display: flex;
  align-items: center;
  gap: var(--sp-1);
}
.date-btn {
  background: none;
  border: none;
  color: var(--ink-faint);
  padding: var(--sp-3);
  width: 44px;      /* 原 36×36，低于触控下限（016 遗留，spec §6） */
  height: 44px;
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease-out);
  display: flex;
  align-items: center;
  justify-content: center;
}
.date-btn svg { width: 20px; height: 20px; }
.date-btn:hover { color: var(--accent); }
.date-display {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  font-size: 14px;
  color: var(--ink-dim);
  letter-spacing: 0.05em;
  white-space: nowrap;
}
.today-btn {
  background: none;
  border: 1px solid var(--ink-faint);
  border-radius: var(--r-pill);
  color: var(--ink-dim);
  font-size: 12px;
  padding: var(--sp-2) var(--sp-5);
  min-height: 44px;   /* 原 54×25：靠 min-height 顶到下限，视觉尺寸不缩 */
  margin-left: var(--sp-3);
  cursor: pointer;
  transition: all var(--dur-fast) var(--ease-out);
}
.today-btn:hover { border-color: var(--accent); color: var(--accent); }

/* 操作 */
.top-actions {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
}
.mode-toggle {
  background: none;
  border: 1.5px solid var(--accent);
  border-radius: var(--r-pill);
  color: var(--accent);
  font-size: 14px;
  font-weight: 500;
  padding: var(--sp-3) var(--sp-6);
  height: 44px;
  cursor: pointer;
  transition: all var(--dur-fast) var(--ease-out);
}
.mode-toggle:hover, .mode-toggle.active { background: var(--accent); color: var(--on-accent); }

/* 主操作按钮 */
.add-btn {
  background: var(--accent);
  border: 1.5px solid var(--accent);
  border-radius: var(--r-pill);
  color: var(--on-accent);
  font-size: 16px;
  font-weight: 700;
  padding: 0 var(--sp-7);
  height: 44px;
  cursor: pointer;
  transition: all var(--dur-fast) var(--ease-out);
  display: flex;
  align-items: center;
  gap: var(--sp-3);
}
.add-btn svg { width: 18px; height: 18px; }
.add-btn:hover { background: var(--accent-strong); }

/* 收起顶栏按钮 */
.collapse-btn {
  background: none;
  border: 1.5px solid var(--ink-faint);
  border-radius: var(--r-pill);
  color: var(--ink-dim);
  width: 44px;
  height: 44px;
  cursor: pointer;
  transition: all var(--dur-fast) var(--ease-out);
  display: flex;
  align-items: center;
  justify-content: center;
}
.collapse-btn svg { width: 18px; height: 18px; }
.collapse-btn:hover { border-color: var(--accent); color: var(--accent); }

/* 顶栏收起态 */
body.topbar-hidden .topbar { display: none; }
body.topbar-hidden .main { padding-top: var(--sp-5); }

/* 展开条 */
.expand-bar {
  position: fixed;
  top: var(--sp-5);
  left: 50%;
  transform: translateX(-50%);
  z-index: 120;
  background: var(--accent);
  color: var(--on-accent);
  border: none;
  border-radius: var(--r-pill);
  font-size: 14px;
  font-weight: 600;
  padding: var(--sp-4) var(--sp-6);
  cursor: pointer;
  box-shadow: 0 4px 16px rgba(0,0,0,0.25);
  transition: all var(--dur-fast) var(--ease-out);
  display: flex;
  align-items: center;
  gap: var(--sp-3);
}
.expand-bar svg { width: 16px; height: 16px; }
.expand-bar:hover { background: var(--accent-strong); transform: translateX(-50%) translateY(2px); }
.expand-bar.hidden { display: none; }

/* ============================================================
   工具行：日期 · 字号 · 暗色 · 主题 · 备份（顶栏第二行，整组居中）
   —— ADR-0010：原先这条在顶栏之外自成一条 .manage-bar，
      科目筛选删除后它只剩两个小控件，不值一整行，于是并了进来。
   ============================================================ */
.tool-row {
  order: 3;
  width: 100%;
  margin-top: var(--sp-3);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-6);
  flex-wrap: wrap;
}

.tool-btn {
  background: none;
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  color: var(--ink-dim);
  font-size: 13px;
  padding: var(--sp-3) var(--sp-5);
  min-height: 44px;   /* 原 height:34px —— #darkmodeBtn 实测 43×34（016 遗留） */
  cursor: pointer;
  transition: all var(--dur-fast) var(--ease-out);
  display: flex;
  align-items: center;
  gap: var(--sp-3);
}
.tool-btn svg { width: 15px; height: 15px; }
.tool-btn.icon-only { padding: 0 var(--sp-4); min-width: 44px; justify-content: center; }
.tool-btn.icon-only svg { width: 17px; height: 17px; }
.tool-btn:hover { border-color: var(--accent); color: var(--accent); }
.tool-btn.active { border-color: var(--accent); color: var(--accent); font-weight: 600; }

.font-wrap {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  font-size: 13px;
  color: var(--ink-dim);
}
/* 主题/备份入口（ADR-0013）：每个入口在工具行只常驻一颗钮，选项装在它底下的弹层里。
   换成按钮的理由是宽度账不是观感——常驻段钮的宽度按名单长度付（实测二档那颗条约 176px，
   四档模拟约 300px），一颗钮无论几档都是 98~116px。弹层挂在入口下面，随顶栏一起隐藏，
   所以它不是 .rivet.md:16 禁的那"第三条工具条"。两个入口共用这套几何与投影。 */
.theme-entry { position: relative; }
.theme-pop {
  position: absolute;
  top: 100%;
  /* 右对齐而不是居中：备份入口靠近工具行右端，居中会让 shrink-to-fit 只拿到
     "从 50% 到容器右边"那一点可用宽度，四个字被挤成四行。右对齐 + max-content
     让弹层按内容自然展开，且在窄屏上不会越过视口右缘。 */
  right: 0;
  margin-top: var(--sp-2);
  z-index: 5;
  width: max-content;
  /* 菜单至少和入口一样宽（实测入口 98 / 菜单 88 时，两者看着不像同一个控件）。
     写在定位盒上而不是 .seg 上：百分比 min-width 要拿 .theme-entry 当参照才算得出，
     挂在 .seg 上时参照变成了这个 shrink-to-fit 盒自己，等于没写。 */
  min-width: 100%;
}
/* 本站没有全局 .hidden——每个浮层自己声明一条（.modal-overlay / .toast / .expand-bar 都是）。
   少这一条，弹层刷新就是一条常驻选项条，上面那句宽度账整笔作废。 */
.theme-pop.hidden { display: none; }
/* 投影落在 .seg 那个看得见的盒子上，不落在外层定位盒上（外层没有底色）。
   与 .modal / .toast / .expand-bar 同一手法。
   竖排是因为横排四档要 300px 宽；.seg 的分隔线本来是横排设计的 border-left，
   竖排下它会变成每项左边一条孤线，所以换成 border-top。 */
.theme-pop .seg {
  flex-direction: column;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
}
.theme-pop .seg-opt { white-space: nowrap; }
.theme-pop .seg-opt + .seg-opt { border-left: none; border-top: 1px solid var(--line); }
/* 段钮外观仍复用 .seg/.seg-opt，按钮由 ui.js 的 THEME_NAMES 生成，HTML 里不抄名单。
   命中区写死 48×44 而不是继承 .seg-opt 的 var(--sp-8)：门禁 §7(a)-6 用 parseFloat 取值，
   函数式的那一份读不出数——写死才是要验的那一条。实测（真页面，非源码推算）：
   弹层里每一项 96×48（二档）/ 114×48（四档模拟），与这条 CSS 值对得上。 */
.theme-seg .seg-opt {
  min-height: 48px;
  min-width: 44px;
}
/* 原状是 input 本体 90×3 —— 命中区只有三像素高。改成 input 高 44px、
   轨道由 ::-webkit-slider-runnable-track 画回 3px：视觉一模一样，手指能按到了。
   该伪元素是 blink/webkit 专有，与原实现一致（原来也只有 -webkit-slider-thumb）。 */
#fontSizeSlider {
  width: 90px;
  height: 44px;
  -webkit-appearance: none;
  appearance: none;
  background: transparent;
  cursor: pointer;
}
#fontSizeSlider::-webkit-slider-runnable-track {
  height: 3px;
  background: var(--ink-faint);
  border-radius: var(--r-pill);
}
#fontSizeSlider::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 15px;
  height: 15px;
  margin-top: -6px;   /* (轨道 3px - 滑块 15px) / 2，让滑块垂直居中于轨道 */
  border-radius: var(--r-full);
  background: var(--accent);
  cursor: pointer;
}
/* 拉丁语境：这里只会出现 "44px" 这类数字+拉丁 */
#fontSizeLabel { min-width: var(--sp-7); text-align: right; font-weight: 600; font-family: var(--font-latin); }

/* ============================================================
   主体 — 科目分组（一屏铺满）
   ============================================================ */
.main {
  width: 100%;
  padding: var(--sp-8);
  min-height: calc(100vh - 80px);
}

.subject-groups {
  width: 100%;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: var(--sp-8) var(--sp-9);
  align-items: start;
}

/* 科目组 */
.subject-group {
  display: flex;
  flex-direction: column;
  --subject-color: var(--ink-faint);
}

.subject-head {
  display: flex;
  align-items: baseline;
  gap: var(--sp-4);
  border-bottom: 2px solid var(--ink);
  padding-bottom: var(--sp-4);
  margin-bottom: var(--sp-5);
}
.subject-name {
  font-size: clamp(28px, 3.5vw, 44px);
  font-weight: 700;
  letter-spacing: 0.08em;
  color: var(--subject-color);
}

/* 作业行 */
.hw-list { list-style: none; display: flex; flex-direction: column; gap: var(--sp-2); }

.hw-row {
  display: flex;
  gap: var(--sp-6);
  padding: var(--sp-4) 0;
  border-bottom: 1px solid var(--line);
  cursor: default;
  transition: opacity var(--dur-fast) var(--ease-out);
}
.hw-row:last-child { border-bottom: none; }

.hw-row .bar {
  width: 6px;
  border-radius: var(--r-pill);
  flex: none;
  align-self: stretch;
  background: var(--subject-color);
}

.hw-row .body { flex: 1; min-width: 0; }

.hw-row .content {
  font-size: var(--card-font-size, 44px);
  font-weight: 400;
  line-height: 1.5;
  letter-spacing: 0.01em;
  word-break: break-word;
  white-space: pre-wrap;
}

.hw-row .deadline {
  margin-top: var(--sp-3);
  font-size: 20px;
  color: var(--ink-dim);
  display: flex;
  align-items: center;
  gap: var(--sp-3);
}
.hw-row .deadline svg {
  width: 1.1em;
  height: 1.1em;
}
.hw-row .deadline > span { font-size: 0.9em; }

/* 编辑态（.manage-on）：行内只有删除钮，改作业的手势是点整行 */
.hw-row .row-actions {
  display: none;
  align-items: center;
  gap: var(--sp-2);
  flex: none;
}
body.manage-on .hw-row .row-actions { display: flex; }
/* 整行即入口，光标得先说出来；掐掉连点两行时的选字残留 */
body.manage-on .hw-row { cursor: pointer; user-select: none; }

.row-action-btn {
  background: none;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  width: 44px;    /* 原 34px（触屏档 40px），仍低于触控下限 */
  height: 44px;
  cursor: pointer;
  color: var(--ink-dim);
  transition: all var(--dur-fast) var(--ease-out);
  display: flex;
  align-items: center;
  justify-content: center;
}
.row-action-btn svg { width: 17px; height: 17px; }
.row-action-btn:hover { border-color: var(--accent); color: var(--accent); background: rgba(0,0,0,0.04); }
/* 删除借用的是语义红，不再是语文红——同一块颜色不该既表示一门课又表示一种状态 */
.row-action-btn.del:hover { border-color: var(--danger); color: var(--danger); }

/* 空状态 */
.empty-line {
  text-align: center;
  padding: var(--sp-9) 0;
  color: var(--ink-faint);
  font-size: 17px;
  letter-spacing: 0.15em;
}

/* ============================================================
   模态框
   ============================================================ */
.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.4);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
  padding: var(--sp-8);
}
.modal-overlay.hidden { display: none; }

.modal {
  background: var(--surface);
  border: 2px solid var(--ink);
  border-radius: var(--r-lg);
  width: 100%;
  max-width: 560px;
  /* 软键盘弹起会压矮动态视口：模态跟着变矮，而不是把「保存」顶出屏幕 */
  max-height: calc(100dvh - 80px);
  display: flex;
  flex-direction: column;
  box-shadow: 0 20px 60px rgba(0,0,0,0.2);
}
/* 打开模态时焦点落在模态本身（tabindex=-1，见 ui.js openAddModal），因此那一圈会被画出来。
   这里**刻意不写** `.modal:focus { outline: none }`：`§7(a)-4` 明令全站唯一一条焦点环规则，
   任何自带 outline 的规则都判红（`outline: none` 这行曾经把表单字段的环整个抹掉，
   就该条断言就是为了钉住那次回潮）。为了"打开时不看见环"去关掉它，等于用一条视觉洁癖
   换掉"焦点此刻在哪"的唯一指示——环留着。 */
.modal-header {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--sp-7) var(--sp-7) 0;
}
.modal-header h2 { font-size: 22px; font-weight: 700; }
.modal-close {
  background: none;
  border: none;
  cursor: pointer;
  color: var(--ink-faint);
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.modal-close svg { width: 20px; height: 20px; }
.modal-close:hover { color: var(--accent); }

/* 横向内边距不在这里，在 .form-body 与 .form-actions 上各写一份（同值 var(--sp-7)）：
   .form-body 是裁切盒，焦点环画在它外面，padding 得落在盒子内侧才给得出地方。 */
#homeworkForm {
  padding: var(--sp-7) 0;
  display: flex;
  flex-direction: column;
  min-height: 0;
  flex: 1;
}
/* 模态里唯一的滚动区：字段多/视口矮时滚这里，「取消/保存」不跟着滚走。
   overflow-y 一旦不是 visible，overflow-x 的 visible 会被浏览器算成 auto——这个盒子因此在横向上也裁。
   全站焦点环画在元素边框盒**外面**（2px 实线 + 2px offset = 外沿 4px），字段又是 width:100%，
   于是环的左右两条竖边整条被切掉（改前读数：余量 0 / 需 4px）。padding 挪到这里，环就有 32px 可花，
   而字段的位置与宽度一字未动：盒子宽 556，内容区 556−32−32 = 原来那 492。 */
.form-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding-inline: var(--sp-7);
}
.form-group { margin-bottom: var(--sp-6); }
.form-group label {
  display: block;
  font-size: 13px;
  font-weight: 600;
  color: var(--ink-dim);
  margin-bottom: var(--sp-3);
  letter-spacing: 0.05em;
}
.form-group select,
.form-group textarea,
.form-group input {
  width: 100%;
  padding: var(--sp-4) var(--sp-5);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  font-family: inherit;
  font-size: 16px;
  color: var(--ink);
  background: var(--bg);
  transition: border-color var(--dur-fast) var(--ease-out);
}
/* 焦点环由文件顶部的全站 :focus-visible 规则负责（ADR-0009）。这里只补边框反馈：
   点按也要有回应，所以用 :focus 而不是 :focus-visible。
   原先这里是一句 `outline: none`，把字段唯一的焦点提示也抹掉了。 */
.form-group select:focus,
.form-group textarea:focus,
.form-group input:focus { border-color: var(--accent); }
.form-group textarea { resize: vertical; min-height: 90px; line-height: 1.6; }

/* 截止时间：段式二选（当天/次日）+ 时刻框并排。
   触控下用大号段钮而不是下拉——两档选择没必要再多一次弹层。 */
.deadline-row { display: flex; align-items: stretch; gap: var(--sp-4); }
.deadline-row input { flex: 1 1 auto; min-width: 0; }
.seg {
  flex: none;
  display: flex;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  overflow: hidden;
  background: var(--bg);
}
.seg-opt {
  font-family: inherit;
  font-size: 15px;
  font-weight: 600;
  color: var(--ink-dim);
  background: none;
  border: none;
  padding: 0 var(--sp-5);
  min-height: var(--sp-8);
  cursor: pointer;
}
.seg-opt + .seg-opt { border-left: 1px solid var(--line); }
.seg-opt.active { background: var(--accent); color: var(--on-accent); }
/* 这一族里全站那条环是画不出来的：.seg 的 overflow:hidden（把选项底色关进 --r-md 圆角）
   就是裁切盒，而那条环画在元素盒子**外面** 4px——三组按钮四边余量实测全 0，环整条不见
   （探针 .scratch/verify-focus-ring-clip.js）。修法走顶部那条规则自己留的口子
   （:where() 把特异性压到 0，「个别控件需要微调时可直接在组件规则里覆盖」）：把环推进
   按钮自己的盒子里，-4px 让 2px 环落在内侧 2~4px 处，裁切盒便管不到它。
   代价三笔，都量过：① 环贴着按钮内侧，与全站「环浮在外面」的观感不一致；
   ② 选中的那一格底色本身就是 --accent，同色环压同色底等于没画，得换成坐在这块底上的
   文字色 --on-accent——在册六档实测 5.19~8.61:1（最低是 github 亮档 #ffffff 压 #0969da），
   读数在探针的「可见性」腿里，不在这里手抄；
   ③ 环既然画进盒子，就落进了 .seg 那道圆角的作用范围：拐角处环会被自己的圆角啃掉一截
   笔尾，whiteboard（--r-md 12px）啃 2.5px、玻璃（16px）啃 5.25px、github（6px）不啃，
   每个受影响的选项 1~2 个角，逐角取像素复核过。 */
.seg-opt:focus-visible { outline-offset: calc(-2 * var(--sp-1)); }
.seg-opt.active:focus-visible { outline-color: var(--on-accent); }

/* 科目选择器（ADR-0011）：六科闭集的六个按钮。
   .seg 本为「当天/次日」那种两三项、永不换行的控件设计，分隔线用 border-left；
   六个按钮在窄屏（390px）一排放不下，换行后行首那项的左边框会变成一条孤线。
   所以这里把分隔线换成 1px→--sp-1 的 gap 透底色：换行与否都不多一条线。
   排成三列两行而不是"能塞几个塞几个"：形状与屏宽无关，验收只看一种形状。 */
.seg-subject {
  flex-wrap: wrap;
  gap: var(--sp-1);
  background: var(--line);
}
.seg-subject .seg-opt {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  flex: 1 1 30%;
  min-width: 0;
  background: var(--bg);
}
.seg-subject .seg-opt + .seg-opt { border-left: none; }
/* 已录标记用圆点不用文字：六个按钮各加两字角标会撑破 560px 的弹窗。
   点本身不承担识别职责——aria-label/title 里写了同一句话。 */
.seg-subject .seg-opt.recorded::after {
  content: '';
  width: var(--sp-3);
  height: var(--sp-3);
  border-radius: var(--r-full);
  background: currentColor;
  opacity: 0.55;
}
.form-actions {
  flex: none;
  display: flex;
  gap: var(--sp-4);
  justify-content: flex-end;
  margin-top: var(--sp-7);
  padding-inline: var(--sp-7);   /* 与 .form-body 同一份：#homeworkForm 只留上下内边距 */
}
.btn-cancel, .btn-submit {
  padding: var(--sp-4) var(--sp-6);
  border-radius: var(--r-pill);
  font-family: inherit;
  font-size: 15px;
  font-weight: 500;
  cursor: pointer;
  transition: all var(--dur-fast) var(--ease-out);
}
.btn-cancel { background: none; border: 1.5px solid var(--line); color: var(--ink-dim); }
.btn-cancel:hover { border-color: var(--accent); color: var(--accent); }
.btn-submit { background: var(--accent); border: 1.5px solid var(--accent); color: var(--on-accent); font-weight: 700; }
.btn-submit:hover { background: var(--accent-strong); }

/* ============================================================
   Toast
   ============================================================ */
.toast-container {
  position: fixed;
  bottom: var(--sp-7);
  right: var(--sp-7);
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  z-index: 2000;
}
.toast {
  background: var(--ink);
  color: var(--on-ink);
  padding: var(--sp-4) var(--sp-6);
  border-radius: var(--r-pill);
  font-size: 14px;
  box-shadow: 0 6px 20px rgba(0,0,0,0.2);
  animation: toastIn 0.25s var(--ease-out);
}
/* 成功/失败是语义，不是某门课：原来 .toast.error 直接借用语文红 --c-yu */
.toast.success { background: var(--success); }
.toast.error { background: var(--danger); }
@keyframes toastIn {
  from { transform: translateY(var(--sp-5)); opacity: 0; }
  to { transform: translateY(0); opacity: 1; }
}

/* ============================================================
   暗色模式
   ============================================================ */
body.dark-mode {
  --bg: #141414;
  --surface: #1c1c1c;
  --ink: #f0f0f0;
  --ink-dim: #a0a0a0;
  --ink-faint: #5a5a5a;
  --line: #2c2c2c;

  /* 暗底上动作色要提亮才够对比：对 #141414 为 8.25:1（实算见 spec §3.1）。
     hover 往更亮走——暗色里"加深"读起来像"变远"，不是"按下"。 */
  --accent: #99a8ff;
  --accent-strong: #b8c4ff;
  --on-accent: #141414;
  --on-ink: #141414;

  --success: #2d9e52;
  --danger: #ff6b5e;
  --warn: #ffab5c;

  /* s1-s6 六主科的暗色档同样锁死 */
  --c-s1: #ff7a6e;
  --c-s2: #7fb0ff;
  --c-s3: #ff8fc8;
  --c-s4: #3fd6c2;
  --c-s5: #ffab5c;
  --c-s6: #7fdc78;
  --c-s7: #ffe699;
  --c-s8: #ccff99;
  --c-s9: #99ffc3;
  --c-s10: #99e9ff;
  --c-s11: #cc99ff;
  --c-s12: #fa99ff;
}
body.dark-mode .topbar { background: var(--bg); }
body.dark-mode .modal { background: var(--surface); }
/* 原先这里有三条 .toast 覆写（#f0f0f0 / #2d9e52 / #ff6b5e）与两条按钮 hover
   覆写（#333 / #444）。主色归动作之后它们全部变成多余的——
   .toast 本就用 --ink/--on-ink，成功/失败本就用 --success/--danger，
   换一档 token 值就把暗色一起带过去了。删。 */

/* ============================================================
   主题：GitHub · 亮档
   ============================================================
   取值出处：@primer/primitives@11.10.0，dist/css/functional/themes/light.css
   与 dist/css/functional/size/radius.css——两份都已原样落盘
   .scratch/四主题体系/vendor/primer-11.10.0/（sha256 见同目录 PROVENANCE.json）。
   每行右侧标官方 token 名，这是 ADR-0012 决定 6 的版本戳纪律：同一个 token 名
   在别的版本里是别的值（暗档链接蓝旧版 #58a6ff、11.10.0 是 #4493f8），
   不写版本与 token 名就等于没写出处，下一个人抄错也自称照抄官方。
   官方圆角以 rem 计，此处按 16px 根字换算成 px：主题不许随字号滑块变（spec 故事 19）。
   科目色与 :root 逐字相同——决定 4 的「复用不重算」。落点是本档的 --bg（内容区的底由 body/.main
   上 --bg，--surface 只到顶栏与弹层为止）：白底上含表外 s7~s12 在内十二色最坏 4.06，全在 4:1 窗内；
   六主科在 --bg/--surface 两底上各为 4.38~5.18 / 4.11~4.86。
   这里有一个不能只当巧合记下来的数：--bg 若取 muted 的 #f6f8fa，s7 掉到 3.85、s9 掉到 3.82，
   出窗。官方 default/muted 的分工本来就是白作页底、灰作卡片，照抄它恰好把窗口守住了（ADR-0012 决定 4b）。
   块必须排在 body.dark-mode 之后：两者特异性同为 (0,1,1)，谁在后面谁说话。 */
body.theme-github {
  --bg: #ffffff;            /* --bgColor-default */
  --surface: #f6f8fa;       /* --bgColor-muted */
  --ink: #1f2328;           /* --fgColor-default */
  --ink-dim: #59636e;       /* --fgColor-muted */
  --ink-faint: #818b98;     /* --fgColor-disabled */
  --line: #d1d9e0;          /* --borderColor-default */

  /* 动作色仍是蓝：本档的 accent 家族官方就是这一个值
     （--fgColor-accent 与 --bgColor-accent-emphasis 在 light 档相等）。
     偏差登记：GitHub 自己的主按钮填充是绿的（--button-primary-bgColor-rest
     → --bgColor-success-emphasis），本站的 --accent 一身兼三职（焦点环 /
     选中态 / 提交钮），拆开就没有一条动作色了——留蓝，记在本块里当明账。 */
  --accent: #0969da;            /* --bgColor-accent-emphasis */
  --accent-strong: #0550ae;     /* --reactionButton-selected-fgColor-hover（选中态再压暗一档） */
  --on-accent: #ffffff;         /* --fgColor-onEmphasis */
  --on-ink: #ffffff;            /* --fgColor-onEmphasis */

  /* 语义色取官方 emphasis 档：本站这三个 token 都作填充底用（toast 底、删除钮 hover），
     配 --on-ink/--on-accent 的白字，白底上 4.52~5.36（底账 §4 的 Class C 判法）。 */
  --success: #1f883d;       /* --bgColor-success-emphasis */
  --danger: #cf222e;        /* --bgColor-danger-emphasis */
  --warn: #9a6700;          /* --bgColor-attention-emphasis */

  /* s1~s6 与 :root 逐字相同（承重层，FROZEN 钉死）；s7~s12 同样复用，
     成套完整声明不留回落（spec 故事 3、5）。 */
  --c-s1: #cf3227;
  --c-s2: #2f6fd0;
  --c-s3: #c23a8b;
  --c-s4: #0d8574;
  --c-s5: #c2410c;
  --c-s6: #2f8a2e;
  --c-s7: #9a7913;
  --c-s8: #4d8811;
  --c-s9: #129147;
  --c-s10: #1484a3;
  --c-s11: #6b18bf;
  --c-s12: #b618bf;

  /* 形状族：GitHub 的控件一律 6px，没有本站白板风那种胶囊。
     --r-pill 在本档被改写成 6px 就是「把胶囊拉回方角」这一步，
     它同时管着提交钮、取消钮、toast 与离线角标的两端。 */
  --r-sm: 3px;              /* --borderRadius-small (0.1875rem) */
  --r-md: 6px;              /* --borderRadius-medium (0.375rem) */
  --r-lg: 12px;             /* --borderRadius-large (0.75rem) */
  --r-pill: 6px;            /* --borderRadius-medium：GitHub 无胶囊档 */
  --r-full: 50%;            /* 圆点还是圆点，与主题无关 */

  /* 材质三条本档不用，但仍要声明——成套完整声明（门禁 §7(a)-7d）不留回落缺口。
     α=1、blur=0px 就是"这一档的浮层是实面"，它和"没写"不等价：没写的那一档
     会去读上一条块的值，GitHub 的方角卡片于是浮在玻璃的半透白底上。 */
  --glass-blur: 0px;
  --glass-alpha: 1;
  --floor-alpha: 1;
}
body.theme-github .topbar { background: var(--bg); }
body.theme-github .modal { background: var(--surface); }

/* ============================================================
   主题：GitHub · 暗档
   ============================================================
   取值出处：@primer/primitives@11.10.0，dist/css/functional/themes/dark.css
   （原样落盘，sha256 见同目录 PROVENANCE.json）。行尾标官方 token 名，与亮档那块同一套写法。
   底色/文字/边框逐值照抄，六色与 body.dark-mode 逐字相同（spec 故事 7「复用不重算」）：
   这块的 --bg 窗口是 7.44~11.18，--surface 上是 6.81~10.23（底账 §2 暗档那两行，门禁 §7(a)-8b 钉住）。

   动作色与语义色走 foreground 梯，不走 emphasis 梯——本档唯一一处需要留下理由的取舍。
   本站每族只有两个槽：一个填充底，一个"压在底上的字"（--on-accent / --on-ink）。
   暗档的 --on-ink 必须是深色，因为 .toast 的底就是 --ink=#f0f6fc；而 --accent 除了作填充，
   还在 .btn-cancel:hover / .row-action-btn:hover 里直接当文字色，--danger 还在删除钮上当图标色。
   把官方 emphasis 梯摆进这两个角色，四格实测全部落在 4.5 以下：
     作文字/图标（对 --bg / 对 --surface）#1f6feb 4.08/3.74 · #238636 4.09/3.74 ·
       #da3633 4.11/3.76 · #9e6a03 4.07/3.72
     作填充底、上面压深色字               4.08 / 4.09 / 4.11 / 4.07
   它配白字是过的（4.61~4.65），可本档没有白字那个槽；而 hover 档官方只剩更深的 #0d419d，
   压到页底上只剩 2.04——控件在 hover 时几乎化进背景，与 body.dark-mode 那条「暗色里加深＝变远」相反。
   foreground 梯两头都在窗内：作文字/图标 对 --bg 5.65~7.50、对 --surface 5.16~6.86；
   作填充底、上面压深色字（--on-accent/--on-ink=#0d1117）5.65~7.50。
   且 --accent-strong=#4493f8 比 --accent=#388bfd 更亮，hover 方向与白板暗档一致。
   白板暗档早就做过同一种取舍（#99a8ff / #ff6b5e / #ffab5c 全是提亮档配深色字），
   本块只是按 GitHub 的官方梯把同一个决定重做一遍——没有一个 hex 是为了凑数改的，
   每个值都能在那份 dark.css 里按 token 名找到（ADR-0012 决定 6 逐字照抄仍然成立）。
   代价写在这里：本档的按钮蓝不是 GitHub 的 emphasis 蓝，而是它的 outline/link 蓝，
   观感归工单 007 的浏览器读数由人裁一次。

   --ink-faint=#656c76 对 --bg 只有 3.57：这一族**四档全不达标**（白板亮 1.98 / 白板暗 2.67 /
   GitHub 亮 3.45 / GitHub 暗 3.57，各对着本块自己的 --bg 实算），是同一处待裁项而不是本档新引入的洞。
   官方 token 名 --fgColor-disabled，GitHub 自己只把它用在禁用态。不在这里偷偷换成达标的假值。 */
body.theme-github.dark-mode {
  --bg: #0d1117;            /* --bgColor-default */
  --surface: #151b23;       /* --bgColor-muted */
  --ink: #f0f6fc;           /* --fgColor-default */
  --ink-dim: #9198a1;       /* --fgColor-muted */
  --ink-faint: #656c76;     /* --fgColor-disabled */
  --line: #3d444d;          /* --borderColor-default */

  /* foreground 梯，理由见上方整段。--on-accent/--on-ink 取 #0d1117（官方 --bgColor-default）：
     色块上的字只能引用本块里的 token，而官方暗档唯一的"压字槽" --fgColor-onEmphasis 是白，
     白字配这梯五个浅色块是 2.52~3.35。 */
  --accent: #388bfd;            /* --button-outline-fgColor-rest */
  --accent-strong: #4493f8;     /* --fgColor-accent（hover 往更亮走） */
  --on-accent: #0d1117;         /* --bgColor-default */
  --on-ink: #0d1117;            /* --bgColor-default */

  --success: #3fb950;       /* --fgColor-success */
  --danger: #f85149;        /* --fgColor-danger */
  --warn: #d29922;          /* --fgColor-attention */

  /* 十二色与 body.dark-mode 逐字相同（s1~s6 是承重层，§7(a)-5c 钉死）。 */
  --c-s1: #ff7a6e;
  --c-s2: #7fb0ff;
  --c-s3: #ff8fc8;
  --c-s4: #3fd6c2;
  --c-s5: #ffab5c;
  --c-s6: #7fdc78;
  --c-s7: #ffe699;
  --c-s8: #ccff99;
  --c-s9: #99ffc3;
  --c-s10: #99e9ff;
  --c-s11: #cc99ff;
  --c-s12: #fa99ff;

  /* 圆角跨档不变：GitHub 的半径阶梯只有一份 size/radius.css，亮暗共用。 */
  --r-sm: 3px;              /* --borderRadius-small (0.1875rem) */
  --r-md: 6px;              /* --borderRadius-medium (0.375rem) */
  --r-lg: 12px;             /* --borderRadius-large (0.75rem) */
  --r-pill: 6px;            /* --borderRadius-medium：GitHub 无胶囊档 */
  --r-full: 50%;

  /* 与亮档那块同一句"本档无材质"，理由见那边的注释（成套完整声明不留回落缺口）。 */
  --glass-blur: 0px;
  --glass-alpha: 1;
  --floor-alpha: 1;
}
/* .topbar / .modal 那两条不重复写：它们读的是 var(--bg)/var(--surface)，
   值已经被上面这个特异性更高的块换掉了（(0,2,1) 压过 body.theme-github 的 (0,1,1)）。 */

/* ============================================================
   主题：液态玻璃 · 亮档
   ============================================================
   版本戳（ADR-0012 决定 6 同一套规矩，本档没有外来官方值也要写明来源）：
   · 六色一个都不新算——s1~s6 与 `:root`（`style.css:69-74` 那组锁死值）逐字相同，
     s7~s12 同理。判据是门禁 §7(a)-5c 之外的另一条：值相等，不是"各自达标"。
   · --bg 的窗口出处：.scratch/四主题体系/contrast-baseline.md §3「实施后」栏，
     本档六色对 --bg=#f6f8fb 的最坏读数 4.12（s6），最亮 4.87（s5），全在 4:1 窗内。
     窗口由门禁 §7(a)-8b 逐块实算钉住，改暗一档当场判红。
   · 语义三色（success/danger/warn）逐字沿用白板那组：玻璃只换"材质与形状"，
     没有自己的一套对错色。写在这里是因为它们**看起来**像自选值。
   · 三条材质 token（--glass-blur/--glass-alpha/--floor-alpha）是本规格自订字段，
     不来自任何包，所以这块里一行"官方 token 名"标注都不许出现——
     test/token-provenance.test.js 的反向断言（T2 的 strays）正是抓这个。
   · 本块排在 body.dark-mode 之后（门禁 §7(a)-7b）：同特异性 (0,1,1) 靠源码顺序决胜。
     玻璃暗档是 005 的 body.theme-glass.dark-mode（(0,2,1)），在那之前"玻璃 + 暗色开关"
     拿到的是这一档的亮底——与 002/003 之间 GitHub 的同一过渡期同形。 */
body.theme-glass {
  /* 信息永远坐在实底上：--bg 是**内容区那一层**，不是半透层。科目色、正文、截止时间
     全部落在它上面，所以它们的读数是一个确定的数（Class A），不随背衬亮度漂。 */
  --bg: #f6f8fb;
  --surface: #ffffff;       /* 玻璃的"白"：浮层 tint 的源，被下面那些 color-mix 按 α 抽薄 */
  --ink: #1b1f27;
  --ink-dim: #55606f;       /* 对本档 --bg 6.00:1；压在 0.92 的白玻璃上 5.46（身后最暗）~6.39（身后白卡） */
  --ink-faint: #8e97a6;     /* 2.77:1 —— 这一族五档全不达标，是同一处待裁项，不是本档新引入的洞 */
  --line: #d9e0ea;

  --accent: #2158c9;        /* 白字压在它上面 6.36:1；抽到 0.92 作浮条填充、压在最亮背衬上仍 5.39:1 */
  --accent-strong: #17408f; /* 亮档 hover 往更暗走，与白板同方向 */
  --on-accent: #ffffff;
  --on-ink: #ffffff;

  --success: #1d7a3f;
  --danger: #b42318;
  --warn: #b45309;

  /* s1~s6 与 :root 逐字相同（承重层，§7(a)-5 钉死）；成套完整声明不留回落。 */
  --c-s1: #cf3227;
  --c-s2: #2f6fd0;
  --c-s3: #c23a8b;
  --c-s4: #0d8574;
  --c-s5: #c2410c;
  --c-s6: #2f8a2e;
  --c-s7: #9a7913;
  --c-s8: #4d8811;
  --c-s9: #129147;
  --c-s10: #1484a3;
  --c-s11: #6b18bf;
  --c-s12: #b618bf;

  /* 形状族：玻璃是圆的，且圆得很随意——白板那档的 8/12/16 是给"纸"的，玻璃给的是"液滴"。
     --r-pill 保持 999px：提交钮两端收圆这件事与主题无关，它是胶囊不是圆角矩形。 */
  --r-sm: 10px;
  --r-md: 16px;
  --r-lg: 24px;
  --r-pill: 999px;
  --r-full: 50%;

  /* 材质。0.92 不是审美数：底账 §3 那条 α 判据量的是"浮层上的文字"（白板取值 0.88→3.82、0.92→4.20），
     本档落地后同一判据换到玻璃自己的取值上重跑了一遍（底账 §3〔004 实施后〕(3) 整张阶梯）——
     本档最紧的一处是 .toast.success 的白字：0.88→4.25 掉出门槛、0.92→4.60 过。
     门禁 §7(a)-9c 把这个下限钉成一族规则（暗档 0.88）。--floor-alpha 是**没有 blur 可依托时**
     的实面 α（@supports 退路分支读它）——同一片白，模糊替它挡住了身后穿过的字，去掉模糊就得靠不透明度。 */
  --glass-blur: 20px;
  --glass-alpha: 0.92;
  --floor-alpha: 0.97;
}

/* ============================================================
   主题：液态玻璃 · 暗档（spec 故事 1 的第八块，也是最后一块）
   ============================================================
   与亮档那一块**只差取值，不差结构**：下面 `body.theme-glass .topbar/.modal/...` 那几条材质规则
   一条都不为暗档重写——它们读的是 var(--surface)/var(--glass-alpha)/var(--glass-blur)，
   本块把这三个字换掉，材质就跟着换档。这是 ADR-0013"主题只能活在 token 层"在本档的落点。
   版本戳：
   · 十二色逐字复用 `body.dark-mode`（`style.css` 暗档那块的 `--c-s1..--c-s12`），一个新 hex 都不算。
     判据是值相等（门禁 §7(a)-9d 的暗档分支），不是"各自达标"。
   · --bg 的窗口出处：底账 §3 暗档表——`#171b21`→6.80、`#1c2128`→6.37、`#232830`→5.83，
     本块落在同一窗口内，实际读数由 005 自己重跑（不许引用 004 亮档的数），见底账 §3〔005 实施后〕。
     本块自己那组的最坏读数 `--c-s1` 7.13，窗口边界实测在 `#32415a`(4.05) 与 `#3a4658`(3.76) 之间——
     比底表那三格宽得多，因为暗档底够暗、十二格一起过线（004 亮档那条"后排六色出窗"在本档不成立）。
   · 框架色（bg、surface、ink 三档、line、accent 两档、on- 两档、语义三色）是本档自订的一套冷调暗色，
     **不来自任何官方包**——所以这块里同样一行"官方 token 名"标注都不许出现（token-provenance T2 的 strays 抓这个）。
   · 特异性 `body.theme-glass.dark-mode` (0,2,1) 压过 `body.dark-mode` 与 `body.theme-glass` 两个 (0,1,1)，
     也与它们同一顺序无关；但 `.topbar/.modal` 那几条材质规则的**规则层**顺序仍由 §7(a)-7b 钉（见亮档那块头注）。 */
body.theme-glass.dark-mode {
  --bg: #12161d;
  --surface: #1b2230;       /* 暗玻璃的"白"是相对页底抬高的一档，抽薄后仍要能盖住身后 */
  --ink: #e9eef7;
  --ink-dim: #a3b0c2;
  --ink-faint: #62708a;     /* 对本档 --bg 3.63:1（对 --surface 3.19、对 --line 2.51），六档全不达 4.5，待裁见底账 §4〔005〕。
       "它是官方禁用态色、可豁免"这句在本站不成立，但要按读者分开看：.date-btn 与 .modal-close 里是 SVG 箭头和 ×
       （svg 基规则 stroke:currentColor，所以是非文本图形，3:1 判 → 过），描边与滑轨同理；
       真正只有 4.5 这一条路可走的是 .empty-line 空状态提示那一行字（3.63 不过），外加 .subject-group 无科目色时的回落色。
       所以待裁的不是"禁用态能多弱"，是"空状态那一行字改档还是这一族六档一起抬"——本票不改值去冒充达标。 */
  --line: #2a3444;

  /* 暗档动作色走"提亮 + 深色压字"那一侧：浅色块上放白字必然掉到 3 以下，
     GitHub 暗档同一处也是这么解的（它的 --on-accent 取自家 --bgColor-default）。 */
  --accent: #6ba6ff;
  --accent-strong: #93c2ff; /* 暗色里 hover 往更亮走——"加深"读起来是变远，不是按下 */
  --on-accent: #12161d;
  --on-ink: #12161d;

  --success: #4cc47c;
  --danger: #ff8579;
  --warn: #e6b45c;

  /* 十二色与 body.dark-mode 逐字相同（s1~s6 是承重层，§7(a)-5 钉死）。 */
  --c-s1: #ff7a6e;
  --c-s2: #7fb0ff;
  --c-s3: #ff8fc8;
  --c-s4: #3fd6c2;
  --c-s5: #ffab5c;
  --c-s6: #7fdc78;
  --c-s7: #ffe699;
  --c-s8: #ccff99;
  --c-s9: #99ffc3;
  --c-s10: #99e9ff;
  --c-s11: #cc99ff;
  --c-s12: #fa99ff;

  /* 圆角与材质形状跟亮档同族：一科一档的"骨相"活在本主题的 token 层，暗档不换骨。 */
  --r-sm: 10px;
  --r-md: 16px;
  --r-lg: 24px;
  --r-pill: 999px;
  --r-full: 50%;

  /* α 下限 0.88 有两个测量，别把它们当一个：底账 §3 底表那条"真边界 0.82~0.85"是拿**白板那组** `--ink-dim`
     量的；本块自己的配对（`--ink-dim` 压在抽薄的本档 `--surface` 上）实测边界更低一档——0.80 只有 4.00、
     0.84 到 4.54，所以真边界在 0.80~0.84 之间，而 0.88 这一档读到 5.15。两个数都指向同一句：0.88 有余量（底账 §3〔005〕(3)）。
     暗档比亮档更敢透：浅字压在深色玻璃上，背衬变暗是"整体变暗"，不像亮档那样被白卡顶穿。
     blur 半径 24px 比亮档大 4px——深色下模糊颗粒更可见，糊得更多反而更像"磨砂"而不是"脏"。 */
  --glass-blur: 24px;
  --glass-alpha: 0.88;
  --floor-alpha: 0.94;
}

/* 材质只落在这几类浮层上（spec 故事 10 的名单：顶栏、弹层、浮条、toast，加 ADR-0013 的主题弹层）。
   写法是 color-mix(…, transparent) 而不是 rgba(255,255,255,.92)：三通道读本档的 token，
   α 读 --glass-alpha，这一行不重复任何一个数字——底账 §3 那张 α 阶梯表量的就是"同一色抽薄"，
   把通道抄成字面量的话，改 --surface 的人与改 α 的人就各自在算一半。
   顺带一条降级：color-mix 认不出的浏览器（Chromium <111 / Safari <16.2）整条丢弃，
   背景落回 .topbar/.modal 原有的实底 var(--bg)/var(--surface)——不是"坏掉"，是退回实面。
   门禁 §7(a)-9a 反向钉这条边界（内容节点上不许出现 backdrop-filter，也不许引用材质 token），
   §7(a)-9b 钉"科目色不落半透层"——那才是亮档六色能整套复用的结构前提。 */
body.theme-glass .topbar {
  background: color-mix(in srgb, var(--surface) calc(var(--glass-alpha) * 100%), transparent);
  backdrop-filter: blur(var(--glass-blur));
  -webkit-backdrop-filter: blur(var(--glass-blur));
}
body.theme-glass .modal {
  background: color-mix(in srgb, var(--surface) calc(var(--glass-alpha) * 100%), transparent);
  backdrop-filter: blur(var(--glass-blur));
  -webkit-backdrop-filter: blur(var(--glass-blur));
}
/* 主题选项弹层：本站第一个真弹层，也是第一个"浮层上带文字"的落点。
   它的选项吃 --ink-dim，正是 α 下限那条判据管的对象——本档 --ink-dim 压在 0.92 的白玻璃上
   最坏 5.46（背衬集合按保守界取：身后整片是墨色；真实场景是墨色笔画穿过模糊层，读数落在 5.46~6.39 之间），
   比底账 §3 拿白板 --ink-dim 在同一条判据下量到的 4.20 更宽。
   下限不跟着放松：那条 0.92 是族规（五档共用一个 α 地板），不是本档的实测结论——
   本档真正咬住 0.92 的地方在下面那条 toast 注释里（0.88 时语义填充跌到 4.25）。 */
body.theme-glass .theme-pop .seg {
  background: color-mix(in srgb, var(--surface) calc(var(--glass-alpha) * 100%), transparent);
  backdrop-filter: blur(var(--glass-blur));
  -webkit-backdrop-filter: blur(var(--glass-blur));
}
/* 浮条与 toast 的填充本来就是动作色/墨色——抽薄之后白字读数仍然在 4.5 以上，
   但**只对最亮那一档背衬成立**（success 抽到 0.92 压在最亮背衬——白卡——上 4.60，是全档最紧的一处；
   同一处 0.88 只有 4.25，掉出门槛）。所以 0.92 这条下限在本档不是"族规顺带"：
   它上面写着的那句"下限不跟着放松"在本档有一处自己的约束点，就在这一行。
   这不是"顺手也能做"：它是底账 §3 那条 α 判据换到深色填充上的同一次复算（整张阶梯见底账 §3〔004 实施后〕，
   脚本 .scratch/四主题体系/alpha-ladder-glass.js）。 */
body.theme-glass .expand-bar {
  background: color-mix(in srgb, var(--accent) calc(var(--glass-alpha) * 100%), transparent);
  backdrop-filter: blur(var(--glass-blur));
  -webkit-backdrop-filter: blur(var(--glass-blur));
}
body.theme-glass .toast {
  background: color-mix(in srgb, var(--ink) calc(var(--glass-alpha) * 100%), transparent);
  backdrop-filter: blur(var(--glass-blur));
  -webkit-backdrop-filter: blur(var(--glass-blur));
}
body.theme-glass .toast.success {
  background: color-mix(in srgb, var(--success) calc(var(--glass-alpha) * 100%), transparent);
}
body.theme-glass .toast.error {
  background: color-mix(in srgb, var(--danger) calc(var(--glass-alpha) * 100%), transparent);
}
/* hover 档不跟着抽薄（.expand-bar:hover / .toast 的语义变体除外，它们没有 hover）：
   本档 --accent-strong 是深色实心，浮层在 hover 时"变实"就是它的按下反馈。
   少写这一条覆写，就少往 ACCENT_ALLOWED 里塞一个选择器——那条清单每多一行都要一句理由。 */

/* 能力退路（spec 故事 25）：不支持 backdrop-filter 时，玻璃退成"同一族 α、去掉模糊"的实面浮层。
   α 从 --glass-alpha 换到 --floor-alpha（0.92→0.97）就是那一步：模糊本来替浮层挡住身后穿过的字，
   没有模糊就得靠不透明度挡。**内容区一行都不读这两条 α**，所以支持与否，内容区读数一字不变。
   全站此前零条 @supports（只有 @media），这条是第一条——门禁的解析器会递归进 @supports，
   里面的选择器照样被 §7(a)-9a/9b 判，不存在"藏在退路里就没人看"。 */
@supports not (backdrop-filter: blur(1px)) {
  body.theme-glass .topbar,
  body.theme-glass .modal,
  body.theme-glass .theme-pop .seg {
    background: color-mix(in srgb, var(--surface) calc(var(--floor-alpha) * 100%), transparent);
  }
  body.theme-glass .expand-bar {
    background: color-mix(in srgb, var(--accent) calc(var(--floor-alpha) * 100%), transparent);
  }
  body.theme-glass .toast {
    background: color-mix(in srgb, var(--ink) calc(var(--floor-alpha) * 100%), transparent);
  }
  body.theme-glass .toast.success {
    background: color-mix(in srgb, var(--success) calc(var(--floor-alpha) * 100%), transparent);
  }
  body.theme-glass .toast.error {
    background: color-mix(in srgb, var(--danger) calc(var(--floor-alpha) * 100%), transparent);
  }
}

/* ============================================================
   主题：Discord · 亮档
   ============================================================
   取材口径（2026-09-25 用户裁决，工单 003 那张等了两轮的裁决票）：三个锚点是**裁进来的值**——
   深灰 `#1e1f22` 作暗档底、blurple `#5865f2` 作动作色、亮档"白卡浮在 `#ebedef` 上"。
   它们**不是照抄官方**：Discord 不发布 design-token 产物，它的 `--base-*` 一层从来只活在客户端包里
   （底账 §5 三条取证路全堵：官网与 CDN 连不通、npm 没有颜色包、GitHub 侧未鉴权命中的全是零星标镜像）。
   所以"Discord 的官方值"这个概念不成立，本档的身份是"我们按裁决采用的一组取值"——
   代价与记账方式见 ADR-0014，这里落实它的纪律：**这两块里一行"官方 token 名"标注都不许出现**
   （test/token-provenance.test.js 的 T2 strays 反向断言抓的就是"标了官方名却无源可查"）。
   其余取值（ink 三档、line、圆角）是本档自订的一套，与玻璃档同一身份。

   版本戳：六色一个都不新算——s1~s6 与 `:root` 那组锁死值逐字相同，s7~s12 同理
   （判据是值相等，门禁 §7(a)-9d 的亮档分支；spec 故事 7 的"复用不重算"到这一档才真的被裁决过一遍）。

   底色这一格是本档唯一一处硬撞门禁的地方，也是本档形状的由来：
   `#ebedef` 上前排六色实测 s4 3.87、s6 3.73（后排 s7~s10 更差，3.46~3.69）——**§7(a)-8b 的 4:1 窗装不下它**。
   同一组六色压在白卡上 4.38~5.18、十二色 4.06~8.37 全过（最坏是 s9 的 4.06，与白板亮档同一格），
   于是裁决选了"内容刷白卡、灰桌只留页面底"这一条路（另两条的价格：把桌提亮到 `#f4f5f7` 会丢掉裁下来的那个值，
   白桌灰卡会把裁下来的那句话反过来写）。**代价两笔，都是这次改动自己欠的**：
   ① §7(a)-8b 的判据基准从"块自己的 `--bg`"改成"科目色实际落的刷底层"（见那条断言的注释与守卫）；
   ② 投影到教室墙上的这一档，形状是"灰桌六张白卡"，不再是白板风那句"墙上只有文字"。观感归工单 007 由人裁。
   面板感只用色差与圆角：白卡对灰桌只有 1.17:1，描边那条 `--line` 对白卡 1.35 更是画不出来——
   所以不补边框、不加投影，边界靠 `--r-lg` 与 `.subject-groups` 原有的 gap 说清楚（spec §8 不做布局分叉）。 */
body.theme-discord {
  --bg: #ebedef;            /* 裁决锚点：灰桌。只作页面底，内容不落在它上面 */
  --surface: #ffffff;       /* 裁决锚点：白卡。科目色、正文、顶栏、弹层都落在这层 */
  --ink: #232428;           /* 灰桌 13.21 / 白卡 15.50 */
  --ink-dim: #4a4e58;       /* 7.10 / 8.33 */
  --ink-faint: #6b7280;     /* 白卡 4.83 / 灰桌 4.12。压在白卡上时它是这一族八档里第一个过 4.5 的
       ——管的那处正是 .empty-line 空状态那一行字（玻璃暗档注释点名的唯一一条"只有 4.5 可走"的落点）。
       灰桌那一格 4.12 仍不达：桌面本身不放字，所以本档没有因此新增落点。 */
  --line: #dbdee1;          /* 对白卡 1.35：装饰线，不承担可辨识边界 */

  /* blurple 就是本档的动作色，这是裁决的另一半。它作填充压白字 4.61（门槛之上），
     作文字/描边落在白卡上 4.61、落在灰桌上 3.93——所以"内容刷白卡"这条不只救六色的读数，
     它同时是 --accent 在本档能站在 4.5 之上的前提。焦点环落灰桌那一格走 3:1 的图形门槛，3.93 过。 */
  --accent: #5865f2;
  --accent-strong: #4752c4; /* hover 往更暗走，与白板/GitHub 亮档同方向；白字压在上面 6.42 */
  --on-accent: #ffffff;
  --on-ink: #ffffff;

  /* 语义三色逐字沿用白板那组（玻璃两档同一先例）：本站的"对/错/警示"没有 Discord 版本，
     也不需要——它们在白卡上 5.02~6.57，与白板亮档同一批读数。 */
  --success: #1d7a3f;
  --danger: #b42318;
  --warn: #b45309;

  /* s1~s6 与 :root 逐字相同（承重层，§7(a)-5 钉死）；s7~s12 同样复用，成套完整声明不留回落。 */
  --c-s1: #cf3227;
  --c-s2: #2f6fd0;
  --c-s3: #c23a8b;
  --c-s4: #0d8574;
  --c-s5: #c2410c;
  --c-s6: #2f8a2e;
  --c-s7: #9a7913;
  --c-s8: #4d8811;
  --c-s9: #129147;
  --c-s10: #1484a3;
  --c-s11: #6b18bf;
  --c-s12: #b618bf;

  /* 本档的半径阶梯只有两档：4 给小件，8 给面板。--r-pill 被拉回 8px 就是"本站没有胶囊"这一步
     ——提交钮/取消钮/toast/今日按钮全部变圆角矩形，与 GitHub 那块把 pill 写成 6px 是同一手法。 */
  --r-sm: 4px;
  --r-md: 8px;
  --r-lg: 8px;
  --r-pill: 8px;
  --r-full: 50%;

  /* 材质三条本档不用，但仍要声明（成套完整声明，§7(a)-7d）：α=1、blur=0px 是"这一档的浮层是实面"，
     与"没写"不等价——没写的那一档会去读上一块的值，白卡于是浮在玻璃的半透白底上。 */
  --glass-blur: 0px;
  --glass-alpha: 1;
  --floor-alpha: 1;
}

/* ============================================================
   主题：Discord · 暗档
   ============================================================
   与亮档那块**只差取值，不差结构**：下面两条 `.subject-group` / `.topbar` 覆写一条都不为暗档重写，
   它们读的是 var(--surface)，本块把那个字换掉，卡片就从白卡变成深灰面板。
   裁决锚点 `#1e1f22` 落在这里的 `--bg`；身份与亮档同一句——裁进来的值，不是官方值，
   所以这块同样一行官方 token 名标注都不写（T2 strays）。
   版本戳：十二色逐字复用 `body.dark-mode`，一个新 hex 都不算（§7(a)-9d 暗档分支）。

   本档不需要为达标做任何事：暗档六色压在 `#1e1f22` 上 6.48~9.74、压在面板 `#2b2d31` 上 5.43~8.15，
   连后排 s7~s12 都在 6.28 以上（八档里"十二色两底全过"是暗档的共同形状，亮档只有这一档做到）。
   `--ink-faint` 在本档两底 5.87 / 4.92 —— 那一族八档里第一次在两处背衬都过 4.5，
   玻璃暗档注释记的那处"待裁"到本档自然消解，但**不跟着放松别的档**：它是取值凑出来的，不是判据变了。

   动作色是本档唯一一处需要留下理由的取舍：blurple 进不了暗档的**文字/描边**角色。
   实测它对页底 3.58、对面板 2.99——后者连 3:1 的图形门槛都只在线上一颤。
   而本站的 --accent 不只作填充：`.tool-btn:hover` / `.btn-cancel:hover` / `.row-action-btn:hover`
   直接把它当文字色，全站那条焦点环把它当描边色。它作填充压白字是过的（4.61），可本档没有白字那个槽
   （浅色块配白字反而是 2.52 那一类，与 GitHub 暗档撞上的是同一件事）。
   于是取提亮档 #a8b3ff / #c8cffd 配深色字，与白板暗档（#99a8ff）、GitHub 暗档（#388bfd/#4493f8）同一手法。
   代价写在这里，不糊过去：**暗档认不出 blurple**。色相角实测 #5865f2 234.9°、本档 #a8b3ff 232.4°、
   白板暗档 #99a8ff 231.2°——差 2.5° 与 1.2°，是同一族淡紫蓝里的明度/饱和差，不是两个颜色。
   这一格归工单 007 由人裁观感；如果裁"必须认得出 blurple"，出路是把那三处文字/描边落点拆开改色，
   那是五处组件规则与一次 ACCENT_ALLOWED 扩表，不是换一个 hex。 */
body.theme-discord.dark-mode {
  --bg: #1e1f22;            /* 裁决锚点：Discord 深灰 */
  --surface: #2b2d31;       /* 面板比页底抬一档，卡片轮廓对页底 1.19:1——同样只靠色差 */
  --ink: #f2f3f5;           /* 14.84 / 12.42 */
  --ink-dim: #b5bac1;       /* 8.44 / 7.07 */
  --ink-faint: #949ba4;     /* 5.87 / 4.92，见上方那段"第一次两底都过" */
  --line: #3f4147;

  --accent: #a8b3ff;        /* 作文字压在面板上 6.93 */
  --accent-strong: #c8cffd; /* 暗色里 hover 往更亮走，与白板暗档/GitHub 暗档同方向；9.06 */
  --on-accent: #1e1f22;     /* 色块上的字只能引用本块 token，取本档 --bg（GitHub 暗档同一解法）：8.28 */
  --on-ink: #1e1f22;

  --success: #2d9e52;       /* 逐字沿用白板暗档那组（玻璃暗档同一先例）：压深色字 4.81~5.90 */
  --danger: #ff6b5e;
  --warn: #ffab5c;

  /* 十二色与 body.dark-mode 逐字相同（s1~s6 是承重层，§7(a)-5 钉死）。 */
  --c-s1: #ff7a6e;
  --c-s2: #7fb0ff;
  --c-s3: #ff8fc8;
  --c-s4: #3fd6c2;
  --c-s5: #ffab5c;
  --c-s6: #7fdc78;
  --c-s7: #ffe699;
  --c-s8: #ccff99;
  --c-s9: #99ffc3;
  --c-s10: #99e9ff;
  --c-s11: #cc99ff;
  --c-s12: #fa99ff;

  /* 圆角与材质跨档不变：亮档那块的注释说清了它们各自的理由，暗档不换骨。 */
  --r-sm: 4px;
  --r-md: 8px;
  --r-lg: 8px;
  --r-pill: 8px;
  --r-full: 50%;

  --glass-blur: 0px;
  --glass-alpha: 1;
  --floor-alpha: 1;
}

/* 裁决的那句"内容刷白卡，灰桌只留页面底"落在这两条规则上——它们是本档的形状，不是装饰。
   为什么必须有机检钉着：§7(a)-8b 的判据基准正是"科目色实际落在哪一层"，删掉第一条就会把
   六张卡刷回灰桌，读数从 4.38~5.18 掉到 3.73~4.41——**而 CSS 里一个 hex 都没动**。
   §7(a)-8b 现在会跟着判红（它现推落底层），但那条红报的是颜色对，不是"覆写没了"，
   所以另立 §7(a)-8c 直接盯这两条规则本身。
   .topbar 那一条是同一次裁决的另一半：顶栏里的 .date-btn/.modal-close 吃 --ink-faint，
   在灰桌是 4.12、在白卡是 4.83，而 --accent 作 hover 文字同理是 3.93→4.61。
   两条都读 var(--surface)，所以暗档不必重写第二份（上面那块换字，这里跟着换）。
   展示态也吃这两条：投影墙上就是六张面板——这正是代价②，归 007 由人裁。
   padding 这一项在展示态有一处**看不见的连带**：fitShowDisplay 量的是 .subject-group 自己的
   scrollHeight 对 clientHeight（ui.js:628），全局 border-box（本文件 :12）下这 16px 上下内边距
   把内容盒压掉 32px，于是这一档的降级阶梯可能比另外三档早 32px 触发——`--show-*` 一字未动，
   动的是它前面那点空间。会不会真的位移要运行时读数（007），这里只把算术记在改动的地方。 */
body.theme-discord .subject-group {
  background: var(--surface);
  border-radius: var(--r-lg);
  padding: var(--sp-5);
}
body.theme-discord .topbar { background: var(--surface); }

/* ============================================================
   响应式
   ============================================================ */
@media (max-width: 1024px) {
  .subject-groups { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 720px) {
  .subject-groups { grid-template-columns: minmax(0, 1fr); }
  .topbar-inner { padding: var(--sp-5) var(--sp-5); }
  .main { padding: var(--sp-6) var(--sp-5) var(--sp-9); }
  .hw-row { gap: var(--sp-4); }
  .modal-overlay { padding: var(--sp-5); }
}

/* 触屏设备：只写"要比默认更大"的那几项。
   原来这里有一条 .row-action-btn { width:40px; height:40px }——基础尺寸已按
   spec §6 顶到 44px，留着它反而会在触屏上把按钮缩回去，违反"命中区只增不减"。 */
@media (hover: none) {
  .modal-close { width: 48px; height: 48px; }
  .btn-cancel, .btn-submit { min-height: 48px; padding: var(--sp-5) var(--sp-7); }
}

/* ============================================================
   展示模式（学生只读视图）
   白板铺满：顶栏（含工具行）、展开条、行内操作全部隐藏
   ============================================================ */
body.view-show .topbar { display: none; }
body.view-show .expand-bar { display: none; }

/* 展示态右上角簇（时钟 + 退出展示）
   position:fixed 不参与布局，因此不会干扰 fitShowDisplay 的字号测量。 */
.show-hud {
  position: fixed;
  top: var(--sp-6);
  right: var(--sp-6);
  z-index: 960;
  display: none;
  align-items: center;
  gap: var(--sp-4);
}
body.view-show .show-hud { display: flex; }

/* 展示模式时钟（精确到分）
   字重 600 而非 700：打包进来的 IBM Plex Sans 只有 600 一档，
   写 700 会让浏览器自己合成粗体，字形会糊掉。 */
.show-clock {
  display: flex;
  flex: none;
  align-items: center;
  /* 底色读 var(--surface)，不写 rgba(255,255,255,.85)：写死白的那几年里，暗档靠一条
     body.dark-mode 覆写找补，而主题体系之后"档"不止亮暗两种（主题 × 明暗），一条 dark-mode
     覆写只能盖住一个主题的灰。每档各自说满外观族是 ADR-0012 的规矩（门禁 §7(a)-7d）。
     先实底再 color-mix：不认 color-mix() 的引擎留着实底，认的才拿到八成透明。 */
  background: var(--surface);
  background: color-mix(in srgb, var(--surface) 85%, transparent);
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  padding: var(--sp-3) var(--sp-6);
  font-family: var(--font-latin);
  font-size: clamp(20px, 1.7vw, 32px);
  font-weight: 600;
  letter-spacing: 0.08em;
  color: var(--ink);
  font-variant-numeric: tabular-nums; /* 等宽数字，避免分钟跳变时宽度抖动 */
  line-height: 1.2;
  /* 始终高亮显示（不做 hover 显影），保证后排也能看清当前时间 */
  opacity: 1;
}

/* 展示模式控制组（右下角：字号调节 + 退出按钮）
   四角离屏边的距离统一用 --sp-6，和右上角 .show-hud 同尺。
   原先这里靠一对**互相抵消**的规则把它"摆"在展示态：
     `.show-ctrl.hidden { display: none }` + `body.view-show .show-ctrl.hidden { display: flex }`
   后者特异性更高，`.hidden` 因此从未生效；更要紧的是这两条只写了"展示态要显示"，
   没有一条写"编辑态不显示"——而 index.html 上那个恒挂着的 `hidden` 让人误以为已经关掉了。
   实测（真浏览器 A/B）：删掉那对规则并摘掉 `hidden` 之后，**编辑态**右下角会多出一组
   「A− 100% A+」。现在把它写成一条正面表达： */
.show-ctrl {
  position: fixed;
  bottom: var(--sp-6);
  right: var(--sp-6);
  z-index: 950;
  display: flex;
  align-items: center;
  gap: var(--sp-4);
}
/* 展示态专属件：**非展示态一律正面关掉**，不许只写"展示态要显示"。
   `.show-hud` / `.expand-bar` 就是这条纪律的正例（前者 body.view-show 打开、后者 view-show 里关），
   而 .show-ctrl 原先只有"展示态显示"那半句 + 一个从未生效的 `.hidden`，
   于是编辑态右下角会漏出一整组「A− 100% A+」（真浏览器 A/B 实测）。 */
body:not(.view-show) .show-ctrl { display: none; }

/* 展示模式字号调节 */
.show-font {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  /* 与 .show-clock 同一条规矩：底色读 var(--surface)，不写死白（写死白在暗档是一块白岛，
     那岛上又画着本档的浅色焦点环——实测环对底 1.63~2.43:1，探针 .scratch/verify-focus-ring-page.js）。 */
  background: var(--surface);
  background: color-mix(in srgb, var(--surface) 85%, transparent);
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  padding: var(--sp-2) var(--sp-4);
  opacity: 0.3;
  transition: opacity var(--dur-fast) var(--ease-out);
}
/* 焦点也要显影，不能只给 hover 留门：一体机上没有 hover（同一处道理见上面 .exit-show-btn 的注释，
   那颗旧铅笔图标就是"0.25 透明、只在 :hover 才显影，等于没有出口"），键盘 Tab 走进这组时
   整组还压在 opacity:.3 上——环是画出来了，连它底下的底一起被压成三成，等于没画。 */
.show-font:hover, .show-font:focus-within { opacity: 0.95; }
.show-font button {
  background: none;
  border: none;
  cursor: pointer;
  width: 44px;   /* 工单 016：一体机上这颗 30×30 是最后一处不达标的命中区 */
  height: 44px;
  border-radius: var(--r-full);
  color: var(--ink);
  font-family: var(--font-latin);
  font-size: 15px;
  font-weight: 600;
  display: flex;
  align-items: center;
  justify-content: center;
}
.show-font button:hover { background: rgba(0,0,0,0.08); }
.show-font-val {
  font-size: 12px;
  color: var(--ink-dim);
  font-family: var(--font-latin);
  min-width: var(--sp-7);
  text-align: center;
  font-weight: 600;
}

/* 展示态出口：带文字的高对比胶囊。
   旧版是右下角一颗 0.25 透明度、只在 :hover 才显影的铅笔图标——
   触控一体机上既没有 hover、也没有 tooltip，等于没有出口。 */
.exit-show-btn {
  display: flex;
  flex: none;
  white-space: nowrap; /* 四字出口不许被挤成竖排 */
  align-items: center;
  justify-content: center;
  min-height: 48px; /* 一体机触控目标，手指即鼠标 */
  padding: 0 var(--sp-6);
  border: none;
  border-radius: var(--r-pill);
  background: var(--accent);
  color: var(--on-accent);
  font-family: inherit;
  font-size: clamp(16px, 1.4vw, 24px);
  font-weight: 700;
  letter-spacing: 0.12em;
  cursor: pointer;
  opacity: 1; /* 出口不做半透明，也不靠 hover 显影 */
  box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
.exit-show-btn:active { transform: scale(0.97); }
body.view-show .show-font { display: flex; }

/* 投影降级的第一档就是这些间距/行高：装不下时先由 fitShowDisplay 挂上
   .show-dense 把它们压小，字号尽量不动（后排可读性优先于疏朗）。
   集中成变量是为了让「压缩了什么」一眼可见，不散落在各条规则里。 */
body.view-show {
  --show-gap-v: 32px;
  --show-gap-h: 56px;
  --show-row-pad: 12px;
  --show-head-gap: 16px;
  --show-deadline-gap: 20px;
  --show-line-h: 1.35;
  /* 科名下沿到第一条作业的距离：与 --show-head-gap 同属一格之首的几何，dense 不压它。
     它同样是阶梯的输入，别吸附到间距刻度上。 */
  --show-head-pad: 6px;
  /* 墙的外边距（承重）：它决定一格的可容宽度，也就是阶梯从哪一档开始触发。
     .show-dense 不压它——压缩外边距换到的那点空间，抵不过字号跳档的观感损失。
     40/48 与大屏档的 56/72 是实测留住的数字，别顺手吸附到间距刻度上。 */
  --show-main-pad-v: 40px;
  --show-main-pad-h: 48px;
}
body.view-show.show-dense {
  --show-gap-v: 12px;
  --show-gap-h: 26px;
  --show-row-pad: 4px;
  --show-head-gap: 6px;
  --show-deadline-gap: 4px;
  --show-line-h: 1.12;
}

body.view-show .main {
  padding: var(--show-main-pad-v) var(--show-main-pad-h);
  height: 100vh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
body.view-show .subject-groups {
  /* minmax(0,1fr)：列/行可收缩到内容最小尺寸以下，长内容不再撑破视口 */
  grid-template-columns: repeat(3, minmax(0, 1fr));
  /* 列数固定 3，行数随科目数走（--show-rows 由 fitShowDisplay 按 ⌈科目数/3⌉ 写在 :root 上）。
     写死 2 行时 8 个科目会被挤扁，而不是自然铺成 3 行。 */
  grid-template-rows: repeat(var(--show-rows, 2), minmax(0, 1fr));
  grid-auto-rows: minmax(0, 1fr);
  height: 100%;
  gap: var(--show-gap-v) var(--show-gap-h);
  align-items: stretch;
}
body.view-show .subject-group {
  height: 100%;
  min-height: 0;
  overflow: hidden;
}
body.view-show .hw-list {
  flex: 1;
  min-height: 0;
  /* safe center：放得下时垂直居中；放不下时退化为顶部对齐，避免顶部被裁 */
  justify-content: safe center;
  overflow: hidden;
}
body.view-show .subject-head { padding-bottom: var(--show-head-pad); margin-bottom: var(--show-head-gap); }
body.view-show .hw-row { padding: var(--show-row-pad) 0; }

/* 整日为空时这一句是放给全班看的，编辑态的 17px 灰字后排读不到。
   展示态的 .subject-groups 是网格，不跨栏的话文案只会落在第一格里。
   颜色用 --ink-dim（暗色下 #a0a0a0），--ink-faint 在两种主题下都不够对比度。 */
body.view-show .empty-line {
  grid-column: 1 / -1;
  align-self: center;
  padding: 0;
  font-size: clamp(34px, 3.2vw + 1.4vh, 84px);
  letter-spacing: 0.18em;
  color: var(--ink-dim);
}

body.view-show .hw-empty {
  list-style: none;
  display: flex;
  align-items: center;
  gap: var(--sp-6);
  /* 字号与内边距都跟着阶梯的两档走。空格子是墙上信息量最低的一块，若它免疫于
     dense 与 fit-scale，九格「未布置」就能把整面墙顶进滚动兜底
     （工单 025 实测：固定 118px vs 同排正文行 42px，阶梯见底也吸收不掉）。
     默认档下仍是 36px——工单 024 量过的后排读数不变。 */
  padding: var(--show-row-pad) 0;
  font-size: calc(36px * var(--show-scale, 1) * var(--fit-scale, 1));
  letter-spacing: 0.12em;
  border-bottom: none;
}
body.view-show .hw-empty::before {
  content: '';
  width: var(--sp-3);
  border-radius: var(--r-pill);
  align-self: stretch;
  background: var(--subject-color, var(--ink-faint));
  opacity: 0.65;
}
body.view-show .hw-empty .empty-text {
  flex: 1;
  text-align: center;
  color: var(--ink-dim);
}
body.view-show .subject-head {
  border-bottom: 2px solid var(--subject-color, var(--ink));
}
body.view-show .hw-row .row-actions { display: none !important; }
body.view-show .hw-row {
  cursor: default;
}
body.view-show .hw-row .content {
  /* 基础字号随视口放大（后排可读）；--fit-scale 由 JS 依据实际内容量收缩，保证不裁切 */
  font-size: calc(clamp(32px, 3.4vw + 1.2vh, 92px) * var(--show-scale, 1) * var(--fit-scale, 1));
  line-height: var(--show-line-h);
  word-break: normal;
  overflow-wrap: break-word;
}
body.view-show .hw-row .deadline {
  font-size: calc(38px * var(--show-scale, 1) * var(--fit-scale, 1));
  margin-top: var(--show-deadline-gap);
  color: var(--ink);
  font-weight: 700;
}
body.view-show .hw-row .deadline svg {
  width: 0.75em;
  height: 0.75em;
  margin-right: var(--sp-1);
}
body.view-show .subject-name {
  font-size: calc(clamp(38px, 3vw + 1vh, 76px) * var(--show-scale, 1) * var(--fit-scale, 1));
}

/* 展示模式默认不滚动：压间距、缩字号两档阶梯负责把内容收进一屏。
   滚动只在阶梯见底后出现（见下方 show-scroll-fallback）——默认就放开等于把阶梯的眼睛
   挖掉：它靠的正是 1fr 行轨 + overflow:hidden 造出的 scroll>client，
   工单 018 实测默认几何下连基线六格都溢出 491px(1366×768) / 575px(1920×1080)。 */
body.view-show { overflow: hidden; }
body.view-show ::-webkit-scrollbar { display: none; }

/* 兜底档（ADR-0007）：压到字号下限仍装不下，整面墙交给纵向滚动。
   只放开 .main 那一屏高的钉——工单 018 逐步放开实测：单独放开 body 的 overflow、
   再连 .main 的 overflow 一起放开，可滚空间都还是 0px；把 height 换成 auto 才出现
   644px / 902px。行轨的 minmax(0,1fr) 与格子上的 overflow:hidden 一律不动。
   代价照 ADR-0007 认：同一行轨里最长那格会把同排另两格一起撑高，出现大片空白。 */
body.view-show.show-scroll-fallback { overflow: visible; }
body.view-show.show-scroll-fallback .main { height: auto; min-height: 100vh; }

/* 希沃一体机触屏优化：展示模式一屏铺满（兜底档下 .main 自行展开，padding 不变） */
@media (min-width: 1600px) {
  body.view-show { --show-main-pad-v: 56px; --show-main-pad-h: 72px; }
}

/* ============================================================
   无障碍：尊重系统的「减弱动态效果」设置
   展示模式是长时间挂在教室投影/一体机上的，持续动画对部分人不适，
   在低端一体机上也容易掉帧。开启该系统设置后，所有过渡与动画即刻完成
   （保留 0.01ms 而非 0 是为了不破坏依赖 transitionend/animationend 的逻辑）。
   ============================================================ */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}