/* ============================================================
   允厘（v0.11.85 改名，原名见 HANDOFF.md）— 共享主题
   warm cream + orange brand
   ============================================================ */

:root {
  /* v0.10.17 「纸感」再调一档：这次动的是**明度差**，不是色相。
     v0.10.8 把底色提亮成纸白之后，底色 #f6f3ec 和卡片 #fffdf9 只差一个色阶
     （对比度 ≈1.11），卡片全靠一根 1px 描边撑着 —— 结果就是「一片米白 + 几根线」，
     整页发平。现在把底色压深到「桌面/牛皮纸」，卡片提到接近纯白（≈1.21），
     层次才立得住；边框、浅色块跟着一起下沉，否则它们在新的底色上会「浮起来」。
     ⚠️ 改 `--vibe-background` 必须**同时改 17 个页面的 <meta name="theme-color">**
        （见 AGENTS §八 换肤清单第 1 条），否则手机地址栏还是旧色，页面和新底色对不上。 */
  --vibe-background: #efe9dd;   /* 桌面：比纸白深一档的牛皮纸色（原 #f6f3ec） */
  --vibe-foreground: #211d1a;
  --vibe-card: #fffefb;         /* 卡片：接近纯白，浮在底色上（原 #fffdf9） */
  --vibe-card-foreground: #211d1a;
  --vibe-popover: #fffefb;      /* 弹层跟卡片同色 */
  --vibe-popover-foreground: #211d1a;
  --vibe-muted: #e8e1d2;        /* 浅底块（原 #efeae1，跟着底色一起下沉） */
  --vibe-muted-foreground: #5c5650;
  --vibe-primary: #f7663c;
  --vibe-primary-foreground: #fff7f3;
  --vibe-secondary: #4a413a;
  --vibe-secondary-foreground: #ffffff;
  --vibe-accent: #f1481e;
  --vibe-accent-foreground: #fff7f3;
  --vibe-destructive: #ef4444;
  --vibe-destructive-foreground: #ffffff;
  --vibe-border: #ded5c4;       /* 纸上的淡描边（原 #e4ded2，底色深了它得跟着深） */
  --vibe-input: #ded5c4;
  --vibe-ring: #f7663c;

  /* ---------- 预混色（v0.11.19）----------
     为什么要有这一组：卡片底色/描边原来是拿 color-mix() **现算**的，但 color-mix 需要
     Chromium 111+，而华为的「闪应用 / 快应用」容器内核远低于这个版本 —— 在那种容器里
     整条声明会被**直接丢弃**，卡片就变成「没底色、没边框」的一堆文字（用户截图原话：
     「框框都没了」）。把混好的结果固化成变量，老内核照常能用，新内核看到的效果分毫不差。
     ⚠️ 这些值是**按卡片底色 --vibe-card 为底**算出来的（半透明色叠在卡片上的效果），
        改 --vibe-card / --vibe-background / --vibe-border 时这几个要跟着重算。 */
  --vibe-card-sub: #f7f3eb;     /* 次卡底色 = mix(background 52%, card) */
  --vibe-line-soft: #ede7dd;    /* 卡片描边 = border 55% 叠在 card 上 */
  --vibe-line-faint: #f2eee5;   /* 次卡描边 = border 40% 叠在 card 上 */
  --vibe-brand-line: #ffd8ca;   /* 主视觉描边 = brand-200 70% 叠在 card 上 */
  --vibe-brand-line-soft: #ffe6dc; /* 空态描边 = brand-200 45% 叠在 card 上 */
  --vibe-brand-wash-55: #fff1ea;   /* 空态光晕 = brand-100 55% 叠在 card 上 */

  --vibe-brand-50: #fff4ee;
  --vibe-brand-100: #ffe6dc;
  --vibe-brand-200: #ffc8b5;
  --vibe-brand-300: #ffab8f;
  --vibe-brand-400: #ff845f;
  --vibe-brand-500: #f7663c;
  --vibe-brand-600: #f1481e;
  --vibe-brand-700: #d63a14;
  --vibe-brand-800: #ad2f13;
  --vibe-brand-900: #8a2914;

  --vibe-success: #3ccf8e;
  --vibe-warning: #f4b366;
  --vibe-info: #b3a6ff;

  /* ---------- 「当文字用」的深色版（v0.11.87，对比度修复）----------
     ⚠️ 起因：设计审查发现全站**把主色/警示色/成功色直接当文字色**用（提示语、可点链接、
        缺考角标、偏科科目…），而这三个色是给**色块/图标**配的亮色调，白底上对比度远远不够
        （按 WCAG 相对亮度公式实算，白卡 #fffefb 上）：
          --vibe-primary  #f7663c → 3.01:1   ✗ 正文要 4.5:1
          --vibe-warning  #f4b366 → 1.81:1   ✗ 最严重（那个「缺」字几乎看不见）
          --vibe-success  #3ccf8e → 1.98:1   ✗
        深色版（下面三个）实算：
          --vibe-brand-700 #d63a14 → 4.65:1  ✓
          --vibe-warning-ink #8a5a12 → 5.86:1 ✓
          --vibe-success-ink #1e7a52 → 5.26:1 ✓
     ⚠️ 规矩：**底色/边框继续用亮的那三个（--vibe-primary/-warning/-success），
        文字一律用 -ink 这三个**。深色主题另配（见 .dark 块）。 */
  --vibe-primary-ink: var(--vibe-brand-700);
  --vibe-warning-ink: #8a5a12;
  --vibe-success-ink: #1e7a52;
  /* ⚠️ 还有一个漏网的：--vibe-destructive #ef4444 当文字只有 3.73:1（「删除」按钮、负向差值
     那个「-3.2」），同样不达标；#b91c1c 是 6.41:1 ✓（下面 att2.css 的 badge 一直在用这个值）。 */
  --vibe-destructive-ink: #b91c1c;
  /* v0.15.65：--vibe-info #b3a6ff 一直是"定义了但没人用"（信息提示都借主色橙在顶），
     这里补上它的文字深色版（#4f46e5 在白卡 #fffefb 上 6.29:1 ✓），口径同上面三个。
     ⇒ **「信息」语义用它，别再借主色**（协同考勤域的 .a2-note.is-info / .a2-badge.is-info）。 */
  --vibe-info-ink: #4f46e5;

  /* ★ v0.15.75/76：**图表用色 = 品牌色**。
     来历（三层弯路，记下来别再来一遍）：
       ① v0.15.61 / v0.15.65 我把「数据图形不许用主色」一路执行成了「一律改墨色 / 灰」——
          折线变成一根黑线、进度条变成一根灰条；
       ② 用户 2026-09-23 否掉黑色，我**又自作主张**换成靛蓝 #4f46e5；
       ③ 用户再次否掉：「**颜色蓝色好丑 就用品牌色吧 不行吗 为什么要改**」。
     ⇒ 定稿：图里的数据图形**直接用主色**，别再换色相。
     留这两支令牌只是给"图里的颜色"一个**统一入口** —— 以后要调改这两行就行，
     不用去 `academic.js` 里翻 13 处。
     ⚠️ 值写成 `var(--vibe-primary)` 而不是写死色值：主色本来就跟着主题变，
        所以**深色主题不用另配**（这条以前写过一次、删掉过，别再补回来）。 */
  --vibe-chart-line: var(--vibe-primary);
  --vibe-chart-bar: var(--vibe-primary);

  --vibe-radius: 18px;
  --vibe-radius-sm: 12px;
  /* ★ v0.18.20：**补一个一直缺的档**。`--vibe-radius-md` 早就被 14 处引用了，但从来没定义过：
     带 fallback 的 7 处（写成 `var(--vibe-radius-md, 14px)`）看不出问题，
     另外 7 处**没写 fallback** ⇒ `border-radius` 整条**静默失效 = 直角**
     （管理页两张卡、家长页卡片、今日页拖拽占位符、协同考勤浮层）。
     值取 **14px**：正是那 7 处 fallback 里手写的数 ⇒ 它们零变化，另外 7 处恢复设计意图。 */
  --vibe-radius-md: 14px;
  --vibe-radius-xs: 8px;
  --vibe-radius-lg: 24px;
  --vibe-radius-full: 9999px;

  --vibe-font-sans: "Geist", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  /* ★ v0.18.17 中文衬线令牌（照 WorkBuddy 那套「衬线只上大字号」的规矩建）
     ⚠️ 允厘全站**没有引任何 webfont**（public/ 下 0 个字体文件、index.html 也没有字体
        链接），而首屏是特意用 `preload + onload` 做到「不挡首帧」的 —— 一个中文衬线
        的子集动辄几 MB，引进来等于把那个成果毁掉。所以这里只用**系统自带**的楷/宋。
     ⚠️ 楷排在宋前面：`.os-line`（今日一签）从 v0.9 起就是这个次序、688 条签文已在
        线上。这里只是把它提成令牌，**渲染结果跟以前一模一样**，目的就是别再多一份写法。
     ⚠️ 只挂 ≥20px、而且**只挂「文气该在那儿」的地方**。
        目前全站唯一一处是 `.today-md`（页头「几号」，v0.18.31 起 1.75rem = 28px）。
        ⚠️ `.now-stat-num`（今日页四个数字，同为 22px）**试过、撤掉了** ——
        楷/宋没有真 bold，`font-weight: 700` 是合成粗体、渲染比黑体轻一档，
        而那一格要的是「一眼看清」。理由与对照截图记在 `.now-stat-num` 那条注释里。 */
  --vibe-font-serif-cjk: "Kaiti SC", KaiTi, STKaiti, "Songti SC", SimSun, ui-serif, Georgia, serif;
  --vibe-font-serif: "Playfair Display", var(--vibe-font-serif-cjk);
  --vibe-font-mono: "Roboto Mono", ui-monospace, monospace;

  /* 投影改成暖色系（原来是偏冷的 rgba(33,29,26,...)）：纸白底上冷灰投影会发脏，
     换成暖棕后读起来像「纸叠纸」而不是「卡片贴在玻璃上」 */
  --vibe-shadow-xs: 0 1px 1px rgba(60, 50, 40, 0.03);
  --vibe-shadow-sm: 0 1px 2px rgba(60, 50, 40, 0.05);
  --vibe-shadow-md: 0 1px 2px rgba(60, 50, 40, 0.05), 0 10px 30px -18px rgba(60, 50, 40, 0.22);
  --vibe-shadow-lg: 0 2px 4px rgba(60, 50, 40, 0.05), 0 18px 40px -20px rgba(60, 50, 40, 0.3);

  /* ---------- 侧栏（v0.10.19 起是深色）----------
     为什么单独开一组令牌而不是复用 --vibe-card 那套：侧栏是**深底浅字**，
     跟内容区（浅底深字）是两套完全相反的关系，共用一套变量只会到处写 !important 打架。
     颜色选暖黑棕而不是纯灰黑：底色和卡片都是暖调（纸白/牛皮纸），
     侧栏用冷灰黑会像「贴了一块别的产品进来」。 */
  --vibe-nav-bg: #2b2521;
  --vibe-nav-fg: #f3ede3;
  --vibe-nav-muted: #a89d8e;
  --vibe-nav-line: rgba(255, 255, 255, 0.085);
  --vibe-nav-hover: rgba(255, 255, 255, 0.065);
  --vibe-nav-active-bg: rgba(247, 102, 60, 0.18);
  --vibe-nav-active-fg: #ffb195;

  color-scheme: light;
}

.dark {
  /* 同一条口径：底色再压深一点，卡片才浮得起来（v0.10.17） */
  --vibe-background: #181410;
  --vibe-foreground: #f2ede7;
  --vibe-card: #302923;
  --vibe-card-foreground: #f2ede7;
  --vibe-popover: #3a332d;
  --vibe-popover-foreground: #f2ede7;
  --vibe-muted: #241f1a;
  --vibe-muted-foreground: #b4aba2;
  --vibe-primary: #ff845f;
  --vibe-primary-foreground: #211d1a;
  --vibe-secondary: #4a413a;
  --vibe-secondary-foreground: #ffffff;
  --vibe-accent: #f7663c;
  --vibe-accent-foreground: #fff7f3;
  --vibe-destructive: #ef4444;
  --vibe-destructive-foreground: #ffffff;
  --vibe-border: #4a413a;
  --vibe-input: #3a332d;
  --vibe-ring: #f7663c;
  /* 预混色（深色一套；口径见 :root 里那组注释）—— 按深色 --vibe-card(#302923) 为底算的 */
  --vibe-card-sub: #241e19;
  --vibe-line-soft: #3e3630;
  --vibe-line-faint: #3a332c;
  --vibe-brand-line: #c19889;
  --vibe-brand-line-soft: #8d7165;
  --vibe-brand-wash-55: #a29189;
  /* 深色主题下侧栏要比页面底色**亮**一点才分得开（页面是 #181410） */
  --vibe-nav-bg: #221d19;
  --vibe-nav-fg: #f3ede3;
  --vibe-nav-muted: #a89d8e;
  --vibe-nav-line: rgba(255, 255, 255, 0.085);
  --vibe-nav-hover: rgba(255, 255, 255, 0.065);
  --vibe-nav-active-bg: rgba(255, 132, 95, 0.2);
  --vibe-nav-active-fg: #ffb195;
  color-scheme: dark;
  /* 「当文字用」的深色版（v0.11.87）—— ⚠️ 深色底上**反过来**：要用**亮**的那三个当文字
     （深底 #181410 上：--vibe-primary 7.61:1 ✓），所以这里把 ink 指回亮色调，
     这样上层样式只认 -ink、两套主题都对。 */
  --vibe-primary-ink: var(--vibe-brand-400);
  --vibe-warning-ink: var(--vibe-warning);
  --vibe-success-ink: var(--vibe-success);
  --vibe-destructive-ink: #f87171;
  --vibe-info-ink: var(--vibe-info);
}

* { border-color: var(--vibe-border); }
html { font-family: var(--vibe-font-sans); }
body {
  background-color: var(--vibe-background);
  color: var(--vibe-foreground);
  /* 触控优化：消除移动端双击缩放延迟与点按高亮残留 */
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  /* 数据类界面：数字统一等宽，统计值变化时不会左右抖动 */
  font-variant-numeric: tabular-nums;
}

/* ===== 纸感底纹（v0.10.8）=====
   「像纸」靠的不只是底色 —— 光换底色还是像屏幕。这里再叠一层 6% 的极淡噪点：
     · position:fixed + 全屏 + pointer-events:none → 浮在所有内容之上，但不吃任何点击；
     · mix-blend-mode:multiply → 噪点跟着底色走，而不是糊一层灰。
   ⚠️ 深色主题下关掉（multiply 在深底上会发脏）；打印也关掉（别把噪点印到纸上）。
   ⚠️ 混合模式在低端安卓上是有成本的 —— 好在它是**静态**的（不随滚动重绘），只走合成层。
      以后别往这里加动画，那样手机上会掉帧。
   ⚠️ 它必须挂在 body::after 上：挂 body 的 background 会被卡片全部盖住，就没有「纸」的感觉了。 */
body::after {
  content: '';
  position: fixed; top: 0; right: 0; bottom: 0; left: 0; pointer-events: none; z-index: 99999;
  opacity: 0.06;
  mix-blend-mode: multiply;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
}
.dark body::after { display: none; }
@media print { body::after { display: none !important; } }

/* ============================================================
   设计底座（v0.9.17）
   ① 中文字面栈：原来 font-sans 里没有中文字体，Windows 上会落到系统默认
      （雅黑/宋体混杂），字号看着忽大忽小；这里显式指定，字面更统一。
   ② 动效令牌：统一一个缓动 + 三档时长，替换各页面手写的 0.15s/0.2s。
   ③ 键盘焦点环 + 选中色 + 细滚动条 —— 细节决定「精致」还是「粗糙」。
   ============================================================ */
:root {
  --vibe-font-cjk: "PingFang SC", "HarmonyOS Sans SC", "Source Han Sans SC",
    "Noto Sans CJK SC", "Microsoft YaHei UI", "Microsoft YaHei", sans-serif;
  --vibe-dur-fast: 120ms;
  --vibe-dur: 180ms;
  --vibe-dur-slow: 320ms;
  --vibe-ease: cubic-bezier(0.2, 0, 0, 1);
  --vibe-ease-out: cubic-bezier(0.16, 1, 0.3, 1);
}

/* ============================================================
   设计令牌（v0.9.18「底座加强」第 2 批）
   ① 字号层级 7 档 —— 收编全站实测高频值（0.8125rem 出现 130 次居首）
   ② 间距节奏 5 档 —— 原来的 0.35 / 0.4 / 0.45rem 三个近似值收敛到 0.5rem
   ③ 字重 3 档 —— 消掉离群的 800（全区仅 6 处）
   ④ 圆角补齐 —— 6px / 4px 两个高频硬编码值纳管，不再散落
   ⚠️ 本段全部是【新增】，不改动任何现存规则；旧硬编码值仍然有效，
      逐页替换时才生效，且只做「数值完全相等」的等值替换 → 零视觉变化。
   ============================================================ */
:root {
  /* ① 字号层级 */
  --vibe-fs-2xs: 0.625rem;    /* 10px 微标签（版本号、角标） */
  --vibe-fs-xs: 0.6875rem;    /* 11px 辅助文字（时间、计数、说明） */
  --vibe-fs-sm: 0.75rem;      /* 12px 次要正文 */
  --vibe-fs-base: 0.8125rem;  /* 13px 正文（全站最高频） */
  --vibe-fs-md: 0.875rem;     /* 14px 卡片 / 区块标题 */
  --vibe-fs-lg: 1rem;         /* 16px 弹窗标题、大正文 */
  --vibe-fs-xl: 1.25rem;      /* 20px 页头标题 */

  /* ② 间距节奏 */
  --vibe-gap-xs: 0.25rem;
  --vibe-gap-sm: 0.5rem;
  --vibe-gap-md: 0.75rem;
  --vibe-gap-lg: 1rem;
  --vibe-gap-xl: 1.5rem;

  /* ③ 字重 */
  --vibe-fw-normal: 500;
  --vibe-fw-medium: 600;
  --vibe-fw-bold: 700;

  /* ④ 圆角补齐（原 --vibe-radius* 五档之外的两个高频值） */
  --vibe-radius-2xs: 6px;     /* 高频小圆角（原硬编码 6px，59 处） */
  --vibe-radius-3xs: 4px;     /* 微型圆角（原硬编码 4px，10 处） */
}
html {
  font-family: "Geist", var(--vibe-font-cjk), ui-sans-serif, system-ui, -apple-system,
    BlinkMacSystemFont, "Segoe UI", sans-serif;
}

/* 键盘可达性：只在键盘操作时显形，鼠标点击不出现，避免「到处都是框」 */
:focus-visible {
  outline: 2px solid var(--vibe-ring);
  outline-offset: 2px;
  border-radius: var(--vibe-radius-xs);
}
::selection { background: color-mix(in srgb, var(--vibe-primary) 26%, transparent); }

/* 桌面端细滚动条：系统默认的粗滚动条是「看着乱」的一大来源 */
@media (hover: hover) and (pointer: fine) {
  * { scrollbar-width: thin; scrollbar-color: color-mix(in srgb, var(--vibe-foreground) 24%, transparent) transparent; }
  ::-webkit-scrollbar { width: 10px; height: 10px; }
  ::-webkit-scrollbar-track { background: transparent; }
  ::-webkit-scrollbar-thumb {
    background: color-mix(in srgb, var(--vibe-foreground) 20%, transparent);
    border: 3px solid transparent; border-radius: 999px; background-clip: content-box;
  }
  ::-webkit-scrollbar-thumb:hover { background: color-mix(in srgb, var(--vibe-foreground) 34%, transparent); background-clip: content-box; }
}

/* 尊重系统的「减少动态效果」 */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* ---------- 语义化 token 兜底（Tailwind 生成前/失败时保证视觉一致） ---------- */
.bg-background { background-color: var(--vibe-background); }
.text-background { color: var(--vibe-background); }
.border-background { border-color: var(--vibe-background); }
.ring-background { --tw-ring-color: var(--vibe-background); }
.bg-foreground { background-color: var(--vibe-foreground); }
.text-foreground { color: var(--vibe-foreground); }
.border-foreground { border-color: var(--vibe-foreground); }
.ring-foreground { --tw-ring-color: var(--vibe-foreground); }
.bg-card { background-color: var(--vibe-card); }
.text-card { color: var(--vibe-card); }
.border-card { border-color: var(--vibe-card); }
.ring-card { --tw-ring-color: var(--vibe-card); }
.bg-card-foreground { background-color: var(--vibe-card-foreground); }
.text-card-foreground { color: var(--vibe-card-foreground); }
.border-card-foreground { border-color: var(--vibe-card-foreground); }
.ring-card-foreground { --tw-ring-color: var(--vibe-card-foreground); }
.bg-popover { background-color: var(--vibe-popover); }
.text-popover { color: var(--vibe-popover); }
.border-popover { border-color: var(--vibe-popover); }
.ring-popover { --tw-ring-color: var(--vibe-popover); }
.bg-popover-foreground { background-color: var(--vibe-popover-foreground); }
.text-popover-foreground { color: var(--vibe-popover-foreground); }
.border-popover-foreground { border-color: var(--vibe-popover-foreground); }
.ring-popover-foreground { --tw-ring-color: var(--vibe-popover-foreground); }
.bg-muted { background-color: var(--vibe-muted); }
.text-muted { color: var(--vibe-muted); }
.border-muted { border-color: var(--vibe-muted); }
.ring-muted { --tw-ring-color: var(--vibe-muted); }
.bg-muted-foreground { background-color: var(--vibe-muted-foreground); }
.text-muted-foreground { color: var(--vibe-muted-foreground); }
.border-muted-foreground { border-color: var(--vibe-muted-foreground); }
.ring-muted-foreground { --tw-ring-color: var(--vibe-muted-foreground); }
.bg-primary { background-color: var(--vibe-primary); }
.text-primary { color: var(--vibe-primary); }
.border-primary { border-color: var(--vibe-primary); }
.ring-primary { --tw-ring-color: var(--vibe-primary); }
.bg-primary-foreground { background-color: var(--vibe-primary-foreground); }
.text-primary-foreground { color: var(--vibe-primary-foreground); }
.border-primary-foreground { border-color: var(--vibe-primary-foreground); }
.ring-primary-foreground { --tw-ring-color: var(--vibe-primary-foreground); }
/* ★ v0.18.20：**Tailwind 产物里没有 `.bg-transparent`** —— 它在全项目只用过 3 次
   （grades.html 的三个 `<select>`），而按需生成的规则没把它生成出来。
   症状：那三个下拉框保留了**浏览器默认底色**，本来是要「融进外层那个 `bg-*` 按钮里」，
   肉眼只在"按钮底色 + 下拉框"这种组合下看得出来（同一个坑见 §三 的 `grid-cols-2`）。
   ⇒ 按 §三 的规矩「结构/工具类一律写在 theme.css，不依赖构建」，这里补 shim（跟下面那串 `.bg-*` 同口径）。 */
.bg-transparent { background-color: transparent; }
.bg-secondary { background-color: var(--vibe-secondary); }
.text-secondary { color: var(--vibe-secondary); }
.border-secondary { border-color: var(--vibe-secondary); }
.ring-secondary { --tw-ring-color: var(--vibe-secondary); }
.bg-secondary-foreground { background-color: var(--vibe-secondary-foreground); }
.text-secondary-foreground { color: var(--vibe-secondary-foreground); }
.border-secondary-foreground { border-color: var(--vibe-secondary-foreground); }
.ring-secondary-foreground { --tw-ring-color: var(--vibe-secondary-foreground); }
.bg-accent { background-color: var(--vibe-accent); }
.text-accent { color: var(--vibe-accent); }
.border-accent { border-color: var(--vibe-accent); }
.ring-accent { --tw-ring-color: var(--vibe-accent); }
.bg-accent-foreground { background-color: var(--vibe-accent-foreground); }
.text-accent-foreground { color: var(--vibe-accent-foreground); }
.border-accent-foreground { border-color: var(--vibe-accent-foreground); }
.ring-accent-foreground { --tw-ring-color: var(--vibe-accent-foreground); }
.bg-destructive { background-color: var(--vibe-destructive); }
.text-destructive { color: var(--vibe-destructive); }
.border-destructive { border-color: var(--vibe-destructive); }
.ring-destructive { --tw-ring-color: var(--vibe-destructive); }
.bg-destructive-foreground { background-color: var(--vibe-destructive-foreground); }
.text-destructive-foreground { color: var(--vibe-destructive-foreground); }
.border-destructive-foreground { border-color: var(--vibe-destructive-foreground); }
.ring-destructive-foreground { --tw-ring-color: var(--vibe-destructive-foreground); }
.bg-border { background-color: var(--vibe-border); }
.text-border { color: var(--vibe-border); }
.border-border { border-color: var(--vibe-border); }
.ring-border { --tw-ring-color: var(--vibe-border); }
.bg-input { background-color: var(--vibe-input); }
.text-input { color: var(--vibe-input); }
.border-input { border-color: var(--vibe-input); }
.ring-input { --tw-ring-color: var(--vibe-input); }
.bg-ring { background-color: var(--vibe-ring); }
.text-ring { color: var(--vibe-ring); }
.border-ring { border-color: var(--vibe-ring); }
.ring-ring { --tw-ring-color: var(--vibe-ring); }

/* 品牌色 / 状态色兜底 */
.bg-brand-50 { background-color: var(--vibe-brand-50); }
.text-brand-50 { color: var(--vibe-brand-50); }
.border-brand-50 { border-color: var(--vibe-brand-50); }
.bg-brand-100 { background-color: var(--vibe-brand-100); }
.text-brand-100 { color: var(--vibe-brand-100); }
.border-brand-100 { border-color: var(--vibe-brand-100); }
.bg-brand-200 { background-color: var(--vibe-brand-200); }
.text-brand-200 { color: var(--vibe-brand-200); }
.border-brand-200 { border-color: var(--vibe-brand-200); }
.bg-brand-500 { background-color: var(--vibe-brand-500); }
.text-brand-500 { color: var(--vibe-brand-500); }
.border-brand-500 { border-color: var(--vibe-brand-500); }
.bg-brand-600 { background-color: var(--vibe-brand-600); }
.text-brand-600 { color: var(--vibe-brand-600); }
.border-brand-600 { border-color: var(--vibe-brand-600); }
.bg-brand-700 { background-color: var(--vibe-brand-700); }
.text-brand-700 { color: var(--vibe-brand-700); }
.border-brand-700 { border-color: var(--vibe-brand-700); }
.bg-success { background-color: var(--vibe-success); }
.text-success { color: var(--vibe-success); }
.border-success { border-color: var(--vibe-success); }
.bg-warning { background-color: var(--vibe-warning); }
.text-warning { color: var(--vibe-warning); }
.border-warning { border-color: var(--vibe-warning); }
.bg-info { background-color: var(--vibe-info); }
.text-info { color: var(--vibe-info); }
.border-info { border-color: var(--vibe-info); }

/* ---------- 通用工具 ---------- */
.no-scrollbar::-webkit-scrollbar { display: none; }
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }

[data-icon] {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  -webkit-mask-size: contain;
  mask-size: contain;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
  background-color: currentColor;
}

/* ---------- 通用组件：Toast ---------- */
.toast {
  position: fixed; bottom: 1.5rem; left: 50%; transform: translateX(-50%);
  padding: 0.75rem 1.5rem; border-radius: 999px; font-size: var(--vibe-fs-md);
  font-weight: 500; z-index: 200; box-shadow: var(--vibe-shadow-md);
  transition: opacity 0.3s; opacity: 0; pointer-events: none;
  max-width: calc(100vw - 2rem); text-align: center;
}
.toast.show { opacity: 1; }
.toast.success { background: var(--vibe-foreground); color: var(--vibe-background); }
.toast.error { background: var(--vibe-destructive); color: #fff; }

/* ---------- 通用组件：模态框 ---------- */
.modal-overlay {
  /* ⚠️⚠️ 这里**必须是 top/right/bottom/left 长写法**，不能写 `inset: 0`：
     `inset` 要 Chrome 87+，而教室大屏那台机器是 **Chrome 86** ⇒ 简写被丢弃、弹层失去定位，
     确认弹窗会画到屏幕外（2026-10-16 用户报"退出点了没反应"，根因就是这个）。
     全项目凡是要铺满视口的 fixed/absolute，都别用 `inset`。 */
  position: fixed; top: 0; right: 0; bottom: 0; left: 0; background: rgba(33, 29, 26, 0.5);
  /* ★ 背板模糊（用户 2026-10-07："其他地方模糊"）。原来只有个别弹窗自己加，
     现在统一加在**通用壳**上 —— 全站六个弹窗一起生效，不用各改各的。
     ⚠️ 10px 是刻意的：再重会让低配教室机在滚动时掉帧；本仓库别处已有 blur(24px) 的先例（侧栏那段）。
     ⚠️ backdrop-filter 只影响"背后画什么"，弹窗内部照旧清楚（.modal-box 自己是不透明底）。 */
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  display: flex; align-items: center; justify-content: center;
  z-index: 100; padding: 1rem;
}
.modal-overlay.hidden { display: none; }
.modal-box {
  /* ★ v0.23.16：右上角那颗「叉」的**钉位**与弹窗的**同心圆角**都从这三个变量算出来。
     放成变量而不是写死两套数：手机上尺寸/内边距一变，圆角能**自动跟着重算**（不用手写第二组魔法数）。 */
  --bzr-modal-pad: 1.5rem;   /* 弹窗内边距。× 是相对**内边距盒**定位的 ⇒ 算"真正的角"时要减掉它 */
  --bzr-x-gap: 14px;         /* × 离右上角两条边的距离（**同时也是两条弧之间的间距**） */
  --bzr-x-size: 40px;        /* × 的直径 */
  background: var(--vibe-card); border-radius: var(--vibe-radius);
  border: 1px solid var(--vibe-border); box-shadow: var(--vibe-shadow-lg);
  width: 100%; max-width: 640px; max-height: 85vh; overflow-y: auto;
  padding: var(--bzr-modal-pad);
  position: relative;
}
/* ★ v0.18.110：**弹窗滚下去也关得掉**。
   用户 2026-09-30 原话：「我点了一下…往上滑往下滑之后，它右上角或者右下角它就没有这个确认保存
   这些东西，真的不是很方便。像是那个作业订正那个地方就有，**右上角那个关闭一直不在**，
   我得点到最上面去才能关闭。」
   根因：`.modal-box` 是**整块滚动**（`max-height:85vh; overflow-y:auto`）——
   内容一长，头部那颗「关闭 ×」就跟着内容一起滚走了。
   做法：把**头部那一行**（含关闭 × 的那一行）钉在弹窗顶部。
   ⚠️⚠️ 为什么不是把 sticky 加在 × 上（第一版就是这么写的，**当场没生效** —— 走查量到它跟着滚走，
      `xTop:-1973`）：那行 × 的**父元素是"标题 + ×"那一行**，高度只有 30 来 px，
      **sticky 的活动范围被父元素限死**，父元素一滚出视口它就没地方可粘了。
      粘在**行**上时，它的父正是滚动容器（`.modal-box`）本身 ⇒ 活动范围是整个滚动内容 ⇒ 一直粘得住。
   ⚠️ 也试过 `absolute`（相对 `.modal-box`）—— **一样没用**：滚动容器里的 absolute 元素会随内容滚动。
   ⚠️ `.modal-box` 的 `padding-top` 得让给这一行（不然它粘住时上方会漏出一条 1.5rem 的内容缝），
      行自己补回同样的上内边距 ⇒ 视觉和原来一模一样。
   ⚠️ 用 `:has()` 认"这一行里有 ×"—— 个别老浏览器不认，那种情况只是**不粘**（＝和以前一样），不会更糟。 */
.modal-box > :first-child:has(.modal-close) {
  position: sticky; top: 0; z-index: 3;
  background: var(--vibe-card);
  padding-top: var(--bzr-modal-pad);
}
.modal-box:has(> :first-child .modal-close) { padding-top: 0; }

/* ★★ 弹窗的角要和右上角那颗圆**同心**：`外圆角 = 1px 边框 + 间隔 + 圆半径`。
   ⚠️⚠️ 推导别丢（用户 2026-10-07 为这事来回三轮）：**正圆不能像矩形那样"把内圆角改小"去迁就外框** ——
      圆的半径是**给定**的（40/44 是触摸目标，不能缩），所以只能反过来**把弹窗的角放大**去迁就这个圆。
      网上那套「内圆角 = 外圆角 − 内边距」在这里**方向是反的**，别照抄。
   ⚠️ 只加给「头行里确实有一颗 .modal-x」的弹窗：没有那颗圆的弹窗不需要这么圆（也就没有对称收益），
      少动一处少一处观感风险。`:has()` 不被支持时只是**圆角回到 18px**，不会更糟。 */
.modal-box:has(> :first-child .modal-x) {
  border-radius: calc(1px + var(--bzr-x-gap) + var(--bzr-x-size) / 2);
}

@media (max-width: 767px) {
  /* 手机上：内边距收到 1rem、触摸目标抬到 44 —— 圆角由上面那条 calc **自动重算**（1+14+22 = 37） */
  .modal-box { --bzr-modal-pad: 1rem; --bzr-x-size: 44px; margin: 0.5rem; border-radius: var(--vibe-radius-sm); }
}

/* ---------- 弹窗右上角那颗「叉」（v0.23.16 统一）----------
   ⚠️⚠️ 为什么是**新类 `.modal-x`**，而不是直接给 `.modal-close` 加样式：
     `.modal-close` 在本仓库是「**这个按钮会关弹窗**」的标记 —— **「取消 / 完成」那些文字按钮也带它**
     （duty / homework / grades / students 里都有），而且 `.modal-box > :first-child:has(.modal-close)`
      这条**粘性标题**规则也靠它判定。
     所以给 `.modal-close` 加"圆形小钮"样式，会把全站的「取消」都变成 34×34 的圆钮 —— 千万别那么改。
   ⇒ 统一后全站的叉 = `class="modal-close modal-x"`（modal-close 留着给粘性标题和关窗逻辑用），
     原来那串行内 `padding:0.25rem;border-radius:…` 已一次摘掉（39 处 / 10 个文件）。
   ★ v0.23.16 换外观（用户 2026-10-07 在预览页上挑定的「① 浮起毛玻璃 + 14px 间隔」）：
     不再是"透明底、hover 才现形"，而是**常驻一个圆**：中性暖灰细边 + 半透明底 + 背后模糊 + 两层柔投影
     ⇒ 一眼看出它是"浮在卡片上面多出一层"，也解决了"白底上白圈＝看不出边界"。
     ⚠️ 描边**不许用品牌橙**（用户原话「不要用红色的…太丑了」）—— 用中性暖灰，色差只有 21（橙是 211）。 */
.modal-x {
  box-sizing: border-box;   /* ★ 必须有：下面加了 1px 边框，不写它这颗就不是 40/44 而是 42/46 */
  display: grid; place-items: center;
  width: var(--bzr-x-size, 40px); height: var(--bzr-x-size, 40px); padding: 0;
  border: 1px solid rgba(74, 63, 53, .17);
  border-radius: var(--vibe-radius-full);
  background: rgba(255, 253, 250, .72);
  -webkit-backdrop-filter: blur(9px) saturate(1.08);
  backdrop-filter: blur(9px) saturate(1.08);
  color: var(--vibe-muted-foreground); cursor: pointer;
  box-shadow: 0 1px 0 rgba(255, 255, 255, .95) inset,
              0 2px 5px rgba(60, 50, 40, .11),
              0 10px 26px -12px rgba(60, 50, 40, .42);
  transition: background-color .16s ease, color .16s ease, border-color .16s ease,
              box-shadow .18s ease, transform .16s ease;
}
.modal-x:hover {
  border-color: rgba(74, 63, 53, .30); background: rgba(255, 251, 248, .9);
  color: var(--vibe-foreground); transform: translateY(-1px);
  box-shadow: 0 1px 0 rgba(255, 255, 255, .95) inset,
              0 3px 8px rgba(60, 50, 40, .13),
              0 14px 30px -12px rgba(60, 50, 40, .48);
}
.modal-x:active { transform: scale(.94); }
.modal-x:focus-visible { outline: 2px solid var(--vibe-ring); outline-offset: 2px; }
.modal-x svg, .modal-x i { width: 18px; height: 18px; transition: transform .22s cubic-bezier(.16,1,.3,1); }
.modal-x:hover svg, .modal-x:hover i { transform: rotate(90deg); }

/* ★ 钉在弹窗**真正的**右上角（不再是"头行里靠右排"）：
   头行是 `position:sticky` ⇒ 它天然就是这颗 absolute 的**包含块**，所以坐标得相对它算；
   而头行的右边缘落在弹窗内边距**之内**（差 `--bzr-modal-pad`）⇒ 用一个**负的 right** 把它推回弹窗边上。
   ⇒ 只有挂在**粘性头行**下面，它才既"贴着角"又"滚下去还在"。
   ⚠️ 别把这颗 × 自己写成 `position:sticky`：它的粘性范围被父级（只有一行高）限死，一滚就粘不住
      （v0.18.110 踩过，走查量到 `xTop:-1973`）。 */
.modal-box > :first-child:has(.modal-x) .modal-x {
  position: absolute; top: var(--bzr-x-gap); right: calc(var(--bzr-x-gap) - var(--bzr-modal-pad));
}
/* ★ 头行给标题**留出那颗圆的位置**（算出来的：间隔 + 直径 + 8px 余量 − 头行已有的右内边距）
   —— 用户 2026-10-07：「东西不要跟那个圈重合了」。 */
.modal-box > :first-child:has(.modal-x) {
  padding-right: calc(var(--bzr-x-gap) + var(--bzr-x-size) + 8px - var(--bzr-modal-pad));
}

/* ★ 「全出血」头行：自己用负 margin 抵消掉了弹窗内边距那种（见 hw-collect 的 `.hc-g-top`）。
   它的**右边缘已经就是弹窗真正的边** ⇒ 钉位不用再减内边距，留位也不用减。
   ⚠️ 必须靠页面上的 `modal-head-bleed` 标记来认：CSS 读不出"这一行是不是负 margin 出血了"。
   ⚠️ 这两条要写在上面那两条**之后**：特异性打平（都是 0,4,0），靠**顺序**赢。 */
.modal-box > .modal-head-bleed:has(.modal-x) .modal-x { right: var(--bzr-x-gap); }
.modal-box > .modal-head-bleed:has(.modal-x) { padding-right: calc(var(--bzr-x-gap) + var(--bzr-x-size) + 8px); }

@media (prefers-reduced-motion: reduce) {
  .modal-x, .modal-x svg, .modal-x i { transition: none; }
  .modal-x:hover, .modal-x:active { transform: none; }
  .modal-x:hover svg, .modal-x:hover i { transform: none; }
}

/* ---------- 弹窗入场动效（v0.9.19 提到全局）----------
   以前只有 duty / seating 两页各写一份，其余 5 个页面弹窗是「硬切」。
   animation 在 display:none → 显示 时会自动重放，所以只要挂在类上即可，
   JS 侧不需要为「重放动效」做任何事。 */
@keyframes bzr-modal-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes bzr-modal-pop {
  0% { opacity: 0; transform: scale(0.92); }
  60% { opacity: 1; transform: scale(1.02); }
  100% { opacity: 1; transform: scale(1); }
}
.modal-overlay { animation: bzr-modal-fade var(--vibe-dur) var(--vibe-ease-out); }
.modal-box { animation: bzr-modal-pop var(--vibe-dur-slow) var(--vibe-ease-out); }

/* ---------- 顶部进度条（v0.9.19）----------
   纯感知反馈：切页 / 请求时让用户知道「点上了，正在办」。
   两条闸门防噪音（延迟显示 + 最短显示时长）在 common.js 里控制，CSS 只管长什么样。 */
.bzr-progress {
  position: fixed; top: 0; left: 0; right: 0; height: 2px;
  z-index: 99999; pointer-events: none;
  opacity: 0; transition: opacity var(--vibe-dur-fast) var(--vibe-ease);
}
.bzr-progress.is-on { opacity: 1; }
.bzr-progress i {
  display: block; height: 100%; width: 100%;
  transform: scaleX(0); transform-origin: left center;
  /* ★ v0.15.61 颜色减法：顶部加载条从"品牌橙渐层 + 橙色外发光"改成中性。
     理由：它是**系统在忙**的状态，不是"当前在哪"、也不是"该按哪个" ——
     橙色的三个正当用途（主操作 / 当前项 / 真在提醒）它一个都不占，
     却是全站最靠上、每次都闪的那条，最容易被误当成"有事情发生"。 */
  background: linear-gradient(90deg,
      color-mix(in srgb, var(--vibe-muted-foreground) 55%, transparent),
      var(--vibe-muted-foreground));
  box-shadow: none;
  /* 爬升用长缓动（走到 90% 就停住等），完成时 JS 会临时换成短缓动冲到 100% */
  transition: transform 1400ms var(--vibe-ease-out);
}

/* ---------- 通用组件：按钮（v0.9.18 统一）----------
   三个类都是「开箱即用」的完整按钮（自带内边距/圆角/字号），可以叠加也能单独用：
     .btn            主按钮（实心主色）
     .btn-ghost      次要按钮（透明 + 描边）
     .btn-secondary  次级按钮（实心淡灰底，多用于「取消」）
     .btn-grow       占满剩余宽度（模态框底部主按钮用，原来是各页 .btn-primary 的 flex:1）
   ⚠️ 为什么三个类共用一段基础声明，而不是把 ghost/secondary 做成纯修饰类：
      历史上 students / seating / duty 三页的次要按钮是**单独一个类**在用的
      （class="btn-ghost modal-close"，没有 .btn）。若只给修饰类，那些按钮会丢掉
      内边距与圆角，渲染成 32×24 的直角小按钮 —— 本地实测踩过这个坑，故如此写。
   ⚠️ 同时修掉一个真实缺陷：common.js 的安装引导弹窗输出 <button class="btn">，
      但 .btn 原先只在 3 个页面内联定义，导致其余 10 个页面（含登录页）那个按钮是裸样式。 */
.btn, .btn-ghost, .btn-secondary {
  display: inline-flex; align-items: center; justify-content: center; gap: 0.4rem;
  padding: 0.5rem 1rem; border-radius: var(--vibe-radius-xs); border: none;
  font-size: var(--vibe-fs-base); font-weight: var(--vibe-fw-medium); cursor: pointer;
  font-family: var(--vibe-font-sans);
  transition: opacity var(--vibe-dur-fast) var(--vibe-ease), background-color var(--vibe-dur-fast) var(--vibe-ease);
}
.btn i, .btn svg, .btn-ghost i, .btn-ghost svg, .btn-secondary i, .btn-secondary svg {
  width: 1rem; height: 1rem; flex-shrink: 0;
}
/* ⚠️ v0.11.87：填充按钮的底色从 --vibe-primary(#f7663c) 换成 --vibe-brand-800(#ad2f13)。
     原因：暖白字 #fff7f3 压在 #f7663c 上只有 **2.87:1**（13px 正文要 4.5:1），
     而这是「确认并入库」「新建考试」这类**主操作**的配色。
     ⚠️ 我一开始选了品牌色阶里的 --vibe-brand-700(#d63a14)，**实算只有 4.44:1 —— 还差一点点**，
        差点交上去一个"以为是修好了其实没达标"的值；再深一档 brand-800 是 **6.22:1** ✓。
        所以凡是要在上面写字的实心块，都用 brand-800。
     橙色本身继续用在色块、图标、进度条这些不需要读字的地方。 */
  .btn { background: var(--vibe-brand-800); color: var(--vibe-primary-foreground); }
  .btn:hover { background: var(--vibe-brand-900); opacity: 1; }
  /* 破坏性操作的实心按钮（删除/撤回这类）—— 底色用 -ink 深红：白字压上去 6.46:1 ✓
     （亮红 #ef4444 只有 3.76:1，不够）。配合 common.js 的 confirmDialog({danger:true}) 用。 */
  .btn.is-danger { background: var(--vibe-destructive-ink); color: #fff; }
  .btn.is-danger:hover { background: color-mix(in srgb, var(--vibe-destructive-ink) 84%, #000); opacity: 1; }
/* 侧栏左上角那组 logo 现在是可点的（v0.11.91：点一下看「关于允厘」）——
   给它一个 hover 提示与键盘焦点环（它由 common.js 的 initAbout() 注入 role/tabindex）。 */
.logo-about { border-radius: var(--vibe-radius-xs); transition: background-color var(--vibe-dur-fast) var(--vibe-ease); }
.logo-about:hover { background: var(--vibe-muted); }
.logo-about:focus-visible { outline: 2px solid var(--vibe-primary); outline-offset: 3px; }
.btn:disabled { opacity: 0.6; cursor: not-allowed; }
.btn-grow { flex: 1; }
.btn-ghost { background: transparent; border: 1px solid var(--vibe-border); color: var(--vibe-foreground); }
.btn-ghost:hover { background: var(--vibe-muted); }
.btn-secondary { background: var(--vibe-muted); color: var(--vibe-foreground); }
.btn-secondary:hover { background: color-mix(in srgb, var(--vibe-muted) 72%, var(--vibe-border)); }

/* ---------- 「选文件 / 拍照」按钮（v0.15.51）----------
   ⚠️ **为什么不写成「点 button → JS 去 .click() 那个 input」**：
      用户 2026-09-22 报「小米上用 Edge、小米浏览器点『📷 拍照』『🖼 相册』**完全没反应**」，
      两个浏览器一模一样 ⇒ 不是浏览器问题，是那套写法的锅：
      程序化点击一个 `hidden`（display:none）的 `<input type=file>`，**部分安卓浏览器会直接吞掉**。
   ⇒ 改成**把透明的 input 直接铺在按钮上**：用户点下去就是"真人点在真的 file input 上"，
      不经过 JS、不经过 label，也不需要那个 input "看得见"。
   ⚠️ 看得见的那个按钮是里面包着的 `<span class="btn btn-ghost">`（样式照旧），
      **不要**再给它加 `hidden` / `display:none`，否则整块点不到。 */
.file-btn { position: relative; display: inline-flex; }
.file-btn > input[type="file"] {
  position: absolute; top: 0; right: 0; bottom: 0; left: 0; width: 100%; height: 100%;
  opacity: 0; cursor: pointer; font-size: 0;
}
/* 键盘用户：焦点落在里面那个 input 上时，把焦点环画到可见的按钮上 */
.file-btn:focus-within > .btn,
.file-btn:focus-within > .btn-ghost { outline: 2px solid var(--vibe-primary); outline-offset: 2px; }

/* ---------- 「提交中」自动反馈（v0.9.19 · ★ v0.18.18 收敛成「一份名单」）----------
   全站有 21 处手写 `btn.disabled = true`，但只有 3 处改了文案 ——
   用户点了「保存」看不出在转。这里用 CSS 兜住：按钮一旦 disabled 就自动长出 spinner，
   不用去改那 20 多处调用点（将来新写的按钮也自动享受）。
   ⚠️ 例外：`[data-static]` 表示「永久禁用」（如设置页的「已在桌面应用中」、座位表「没东西可撤销」），
      那种不该转圈。

   ★ v0.18.18 把**两套手写名单合并成一条**。原来「要转圈」和「data-static 不转」各写一份，
     注释里还挂着「加新类时记得同步加进这一组，否则又会漏」—— 而 v0.15.56 真漏过一次
     （座位表的「撤销 / 锁定」永久禁用时一直挂着小转圈，看着像卡在加载）。
     现在把例外折进选择器本身（`:not([data-static])`）⇒ **全站只有下面这一份名单**，
     以后加新的可转圈按钮只改这一处，不可能再"漏同步"。
   ⚠️ 类清单必须套 `:where()`：它权重为 0，合并后的权重与合并前**逐字相等**
      （`.btn:disabled::before` 仍是 0,2,1）—— 否则这条会突然压过某些页面上更靠后的规则。
   ⚠️ 原「`content: none` 例外」那条整段删掉是安全的：全站给按钮写 `::before` 的只有这里，
      HTML 内联的 `::before` 全是给 `td`（表格转卡片时的列名）和 `.preview-tag` / 讲台用的。 */
   /* ★ v0.18.73：前面加了 `:where(html.is-net-busy)` —— **转圈只表示"正在跟服务器说话"**。
     ⚠️⚠️ **这段注释必须有自己的一对注释符（开头 + 收尾）** —— 我第一版就是漏了**开头那个**：
        上一段注释在 591 行就收尾了，于是 592~598 行变成**裸的 CSS 垃圾文本**，
        浏览器的错误恢复**把紧跟其后的整条转圈规则一起吞了** ⇒ 表现是"规则明明写在文件里、
        grep 得到、服务器也发得出来，**但它就是不生效**"（我为此怀疑过缓存、怀疑过选择器写法、
        怀疑过 Service Worker，量了半天才想到是注释没收尾）。见 check-css.mjs 新增的那条守卫。
     ⚠️ 顺带一条规矩：**写注释时别把注释符本身写进正文**（不管是 CSS 还是 JS）——
        收了尾就提前结束、漏了开头就变垃圾，两种都是静默的。
     起因（用户 2026-09-29）：「中间那个、下面那个按钮**一直在转一直在转**，我都不知道它在干什么」：
     小助手弹窗里那颗「就这么办」**天生 disabled**（要等你听完才可点）⇒ 一打开就在转，可它什么都没干。
     ⚠️ 改成"disabled **且** 有请求在飞"才转 —— 这一次改就把**全站**那些"因为条件不满足而禁用"
        的按钮（问答选项、没东西可撤销、没选文件……）一次性都治了，不用逐个打补丁（那种一定会漏）。
     ⚠️ `html.is-net-busy` 这个类由 `common.js` 的 `progressStart/progressDone` 维护（请求计数器，可重入）。
     ⚠️ 前缀也套 `:where()`（权重 0）⇒ 这条规则的权重**逐字不变**，不会突然压过页面上更靠后的规则。 */
:where(html.is-net-busy) :where(.btn, .btn-ghost, .btn-secondary, .gbtn, .tbtn, .btn-ghost-btn, .empty-btn, .reset-btn):disabled:where(:not([data-static]))::before,
/* 今日页这两处按钮是用「元素选择器」定义的（没有类名），单独列出来 */
:where(html.is-net-busy) .kd-add button:disabled:where(:not([data-static]))::before,
:where(html.is-net-busy) .todo-add button:disabled:where(:not([data-static]))::before {
  content: ''; width: 0.85rem; height: 0.85rem; flex-shrink: 0;
  border-radius: 999px; border: 2px solid currentColor; border-top-color: transparent;
  animation: bzr-spin 0.7s linear infinite;
}
/* 上面这些按钮里只有 .reset-btn 不是 flex 容器，spinner 需要自己留出间距 */
.reset-btn:disabled::before { margin-right: 0.3rem; }

/* ---------- 工具栏里的「分组分割线」（v0.15.56）----------
   用途：座位表 / 值日表的工具按钮变多之后，一长排平铺看不出"哪几个是一类"。
   在组与组之间插一个 `<span class="tsep">` 即可（**它自己也要带 `tool-extra`**，
   这样手机上「更多」收起时它跟着一起藏）。
   ⚠️ 不用 border-left 画在按钮上 —— 那样按钮一变宽（长短文案切换）线就跟着晃。 */
.tsep {
  display: inline-flex; align-self: center;
  width: 1px; height: 1.15rem; flex: 0 0 1px;
  background: var(--vibe-border);
  margin: 0 0.2rem;
}

/* ---------- 「● 未保存」（v0.15.57，设置页）----------
   设置页每张卡各自一个保存按钮，改了不点就走了 ⇒ 白改。这个标记挂在**保存按钮左边**
   （离"该按哪儿"最近），点过保存就消失。逻辑在 settings.js 末尾（事件代理，不逐个输入框挂钩子）。 */
.unsaved-tag {
  display: inline-flex; align-items: center; white-space: nowrap;
  margin-right: 0.55rem;
  font-size: var(--vibe-fs-xs); font-weight: 600;
  color: var(--vibe-brand-600);
}

/* ---------- 「三步上手」空态（v0.15.57，今日页）----------
   新账号一进来首屏大半是空的（0节 / 0条 / 0人）⇒ 按"该做的顺序"给三个入口，每步一句话说清为什么。
   ⚠️ 显隐逻辑没动：还是"这个账号还没有班级"时才出现（dashboard.js），满数据的账号一个字都不变。 */
.starter-steps { list-style: none; margin: 1rem 0 0; padding: 0; width: 100%; text-align: left; }
.starter-steps > li {
  display: flex; align-items: center; justify-content: space-between; gap: 0.75rem;
  padding: 0.7rem 0; border-top: 1px solid var(--vibe-border);
}
.starter-steps > li:first-child { border-top: 0; }
.starter-step-main { display: flex; align-items: flex-start; gap: 0.6rem; min-width: 0; }
.starter-step-n {
  flex: 0 0 auto; width: 1.35rem; height: 1.35rem; border-radius: 999px;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--vibe-brand-50); color: var(--vibe-brand-800);
  font-size: var(--vibe-fs-xs); font-weight: 700;
}
.starter-step-t { font-size: var(--vibe-fs-base); font-weight: 600; color: var(--vibe-foreground); }
.starter-step-s { margin-top: 0.15rem; font-size: var(--vibe-fs-xs); color: var(--vibe-muted-foreground); line-height: 1.6; }
/* 窄屏：按钮换到下一行，别把说明挤成一条竖线 */
@media (max-width: 520px) {
  .starter-steps > li { flex-direction: column; align-items: stretch; gap: 0.5rem; }
  .starter-steps .empty-btn { align-self: flex-start; }
}

/* ---------- 设置页的折叠块（v0.15.58）----------
   用户：「设置里东西是不是太多了 太长了的能不能给他隐藏一下 点了再展开」。
   这页 11 块里，把 7 块「配一次就不动」的折起来（学期与周次 / 校历 / 图片识别 / 我的名字 /
   我的任课 / 安装到桌面 / 我的身份），另 4 块（个人资料 / 默认班级 / 今日课表提醒 / 账号）常开。
   用**原生 `<details>`**：零 JS、键盘可用、点了才展开，无障碍也是白拿的。
   ⚠️ `.set-fold-inner` 是里面原来那张卡 —— 它自带行内 `box-shadow`，所以这里必须 `!important` 压掉，
      否则会出现"双层卡片边框"（行内样式优先级永远高于类）。 */
.set-fold {
  background: var(--vibe-card); border: 1px solid var(--vibe-border);
  border-radius: 0.5rem; box-shadow: var(--vibe-shadow-sm); overflow: hidden;
}
.set-fold-h {
  display: flex; align-items: center; gap: 0.5rem;
  padding: 0.95rem 1.25rem; cursor: pointer; list-style: none;
  font-size: var(--vibe-fs-sm); font-weight: 600; color: var(--vibe-foreground);
}
.set-fold-h::-webkit-details-marker { display: none; }
.set-fold-h:hover { background: var(--vibe-muted); }
.set-fold-t { flex: 0 0 auto; }
.set-fold-s {
  flex: 1 1 auto; min-width: 0;
  font-size: var(--vibe-fs-xs); font-weight: 400; color: var(--vibe-muted-foreground);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.set-fold-chev { width: 1rem; height: 1rem; flex: 0 0 auto; color: var(--vibe-muted-foreground); transition: transform 0.18s ease; }
.set-fold[open] .set-fold-chev { transform: rotate(180deg); }
.set-fold-inner {
  background: transparent !important;
  border: 0 !important;
  box-shadow: none !important;
  padding: 0 1.25rem 1.1rem !important;
}
/* 手机：让标题自己占一行（那句说明折到下一行），别把标题挤成省略号 */
@media (max-width: 560px) {
  .set-fold-h { flex-wrap: wrap; }
  .set-fold-s { flex-basis: 100%; padding-left: 1.5rem; }
  .set-fold-chev { order: 3; }
}

/* ---------- 通用组件：输入框（v0.9.18 统一）----------
   ⚠️ 焦点态用 border + box-shadow 表达，所以保留 `outline: none`；
      这是有可见焦点指示的，不违反 §三「不要用 outline:none 不补焦点态」。 */
.form-input {
  width: 100%; padding: 0.5rem 0.75rem; font-size: var(--vibe-fs-base);
  border-radius: var(--vibe-radius-xs); border: 1px solid var(--vibe-input);
  background: var(--vibe-background); color: var(--vibe-foreground);
  outline: none; box-sizing: border-box; font-family: var(--vibe-font-sans);
}
.form-input:focus { border-color: var(--vibe-primary); box-shadow: 0 0 0 2px var(--vibe-brand-100); }
.form-input::placeholder { color: var(--vibe-muted-foreground); }

/* ---------- ★ v0.24.3：手机端"点一下输入框，整页啪地放大"（用户 2026-10-08 撞上） ----------
   那是 **iOS Safari 的一条老规矩**：输入框字号 < 16px 时，聚焦会自动把**整页**放大。
   全站正文是 13px（`--vibe-fs-base`）⇒ 每一个输入框都是"点一下就放大"。
   这里在手机端把表单控件抬到 16px，那条自动放大就不再触发。
   ⚠️ 为什么不用 viewport 的 `user-scalable=no` / `maximum-scale=1` 来"禁止缩放"：
      ① **苹果从 iOS 10 起直接忽略这两条**（为了无障碍），写了在 iPhone 上等于没写；
      ② 该治的是"误触放大"，不是"用户想放大" —— 眼神不好的老师想放大看看字，是好事。
   ⚠️ 选择器必须盖过 `.form-input` / `.sa-input` 这些把字号写死 13px 的规则：
      它们都是 0-1-0，写成 `input:not(...)` 是 0-1-1 ⇒ 能压住（别改成只写 `input`，那样压不住）。 */
@media (max-width: 767px) {
  input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="file"]),
  select,
  textarea { font-size: 16px; }
}

/* ---------- PWA 安装引导（横幅 + 步骤弹窗） ---------- */
.bzr-install-banner {
  position: fixed; left: 50%; transform: translateX(-50%); bottom: 16px; z-index: 9999;
  display: flex; align-items: center; gap: 0.6rem;
  max-width: calc(100vw - 24px);
  padding: 0.55rem 0.5rem 0.55rem 0.9rem;
  border-radius: 14px; border: 1px solid var(--vibe-border);
  background: var(--vibe-card); color: var(--vibe-foreground);
  box-shadow: var(--vibe-shadow-lg); font-size: var(--vibe-fs-base);
  animation: bzr-banner-in 0.25s ease-out;
}
@keyframes bzr-banner-in {
  from { opacity: 0; transform: translate(-50%, 8px); }
  to { opacity: 1; transform: translate(-50%, 0); }
}
.bzr-ib-text { min-width: 0; }
.bzr-ib-title { font-weight: 700; line-height: 1.25; }
.bzr-ib-desc {
  font-size: var(--vibe-fs-xs); color: var(--vibe-muted-foreground); line-height: 1.35;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.bzr-ib-go {
  flex-shrink: 0; padding: 0.4rem 0.8rem; border-radius: 9px; border: none;
  background: var(--vibe-primary); color: var(--vibe-primary-foreground);
  font-size: var(--vibe-fs-base); font-weight: 600; cursor: pointer; white-space: nowrap;
  font-family: var(--vibe-font-sans);
}
.bzr-ib-close {
  flex-shrink: 0; padding: 0.3rem; border: none; border-radius: 7px; background: none;
  color: var(--vibe-muted-foreground); cursor: pointer; line-height: 0;
}
.bzr-ib-close svg { width: 14px; height: 14px; }
.bzr-ib-close:hover { background: var(--vibe-muted); color: var(--vibe-foreground); }

/* 移动端：抬到悬浮导航条之上，避免横幅跟底栏叠在一起
   只在「带侧栏壳层」的页面抬升；登录页没有底栏，保持常规贴底
   ⚠️ 这个值跟着底栏高度走。v0.9.21 底栏改为「悬浮圆角条」后：
      条高 ≈ 56px 内容 + 2×(0.25rem padding + 0.25rem margin) + 1px 边框 ≈ 73px，
      离底 = max(安全区 + 10px, 24px)，所以条顶 = max(安全区 + 83px, 97px)。
      这里取「条顶 + 8px 间隙」，同样用 max() 兜住「安全区为 0」的机型。
      改底栏高度时必须同步回看这里。 */
@media (max-width: 767px) {
  .bzr-install-banner {
    width: calc(100vw - 1.5rem); max-width: none;
  }
  body:has(aside) .bzr-install-banner {
    bottom: 6.6rem;   /* 兜底：不支持 max() 时也压在悬浮条之上 */
    bottom: max(calc(env(safe-area-inset-bottom, 0px) + 5.75rem), 6.6rem);
  }
}

.bzr-install-steps { margin: 0; padding-left: 1.3rem; }
.bzr-install-steps li { font-size: var(--vibe-fs-base); line-height: 1.75; color: var(--vibe-foreground); }
.bzr-install-steps li::marker { color: var(--vibe-primary); font-weight: 700; }
.bzr-install-note {
  margin: 0.85rem 0 0; padding: 0.55rem 0.7rem; border-radius: var(--vibe-radius-xs);
  background: var(--vibe-muted); color: var(--vibe-muted-foreground);
  font-size: var(--vibe-fs-xs); line-height: 1.6;
}

/* ---------- 登录页常驻安装引导条（v0.9.9）----------
   只在「手机/平板 + 不能一键安装」时出现；关掉用与横幅同一套标记 */
.pwa-hint {
  display: flex; align-items: flex-start; gap: 0.55rem;
  padding: 0.65rem 0.75rem; margin-bottom: 0.75rem;
  border-radius: var(--vibe-radius-sm);
  border: 1px solid color-mix(in srgb, var(--vibe-brand-200) 60%, transparent);
  background: var(--vibe-brand-50);
}
.pwa-hint-icon { flex-shrink: 0; line-height: 0; color: var(--vibe-brand-600); }
.pwa-hint-icon svg { width: 1rem; height: 1rem; display: block; }
.pwa-hint-main { flex: 1; min-width: 0; }
.pwa-hint-title { display: block; font-size: var(--vibe-fs-base); font-weight: 600; color: var(--vibe-brand-700); line-height: 1.5; }
.pwa-hint-body { display: block; margin-top: 0.15rem; font-size: var(--vibe-fs-sm); color: var(--vibe-muted-foreground); line-height: 1.65; }
.pwa-hint-close {
  flex-shrink: 0; padding: 0.15rem; border: none; background: none; cursor: pointer;
  color: var(--vibe-muted-foreground); line-height: 0; border-radius: var(--vibe-radius-xs);
}
.pwa-hint-close:hover { background: var(--vibe-brand-100); color: var(--vibe-foreground); }
.pwa-hint-close svg { width: 14px; height: 14px; display: block; }

@media print {
  .bzr-install-banner { display: none !important; }
}

/* ---------- 今日页（v0.9.8 建；v0.10.1 视觉改版）----------
   结构：页头（日期 / 班级 / 周次）→ 班级上下文细条 → 重要日期
        → 「此刻」主视觉（整宽）→ [今日课表 | 值日 · 待办]
   视觉口径：
     · 全页只保留一个重高亮 ——「正在上」/「我正在上」（主色）；其余靠字号与灰度分层
     · 两列不强制等高（align-items:start）—— 否则矮的那列会被拉出一大片死白
     · 空态一律走「一行式」(.empty-line)，不再用大虚线框占掉半屏 */

/* ===== 页头（⚠️ 叫 today-page-head 而不是 today-head —— 后者已经是卡片标题栏的类名） =====
   v0.10.5 起页头不只是「显示」，它同时是班级的入口：
   · 背景从品牌色最浅处向下过渡到卡片底色（不再是一块死白），把「几号 / 哪个班」托起来，
     往下滚的时候和内容自然分层；
   · ★ v0.18.31 起：**「几号」是这一行唯一的重音（1.75rem = 28px 中文衬线）**，
     旁边只留一个校历小标（「中秋节 · 放假」这种）；
     ⚠️ **「星期几」已去掉**（用户 2026-09-27 点的）—— 下面日期条上今天那格本来就写着「周日 27」
        并高亮着，同一个信息不该在页头出现两遍；去掉后让出来的宽度正好给「几号」加大。
     ⚠️ **班级筹码挪到下面「学期行」**（同一个入口，只是换了行）—— 见 dashboard.html 里那段注释；
   · 班级筹码是个按钮，点开就是班级切换面板（原来那条班级横条已删）。 */
.today-page-head {
  display: flex; flex-wrap: wrap; align-items: flex-start; justify-content: space-between;
  gap: 0.5rem;                    /* 桌面刻度 8：页头内部（head-main ↔ 铃铛） */
  background-image: linear-gradient(180deg,
      color-mix(in srgb, var(--vibe-brand-50) 75%, var(--vibe-card)) 0%,
      var(--vibe-card) 82%);
}
.today-head-main { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 0.25rem; }
.today-date-row { display: flex; align-items: center; flex-wrap: wrap; gap: 0.5rem; min-width: 0; }
.today-title { display: flex; align-items: baseline; gap: 0.5rem; margin: 0; letter-spacing: -0.01em; }
/* ★ v0.18.17：页头「几号」挂中文衬线。
   ★ v0.18.31：**22px → 28px**（用户 2026-09-27：「日期是不是可以大一点」）——
     它旁边原来挤着「星期几」和班级筹码，现在两个都让开了（星期几删掉、筹码挪到学期行），
     这一行只剩它和校历小标 ⇒ 正好把它做成页头的重音。
   ⚠️ 校历小标 `.today-cal` 只有 fs-xs，**不挂**衬线 —— 中文衬线在小字号下会糊，
      这是 WorkBuddy 那边验证过的规矩（v0.18.17 立的）；全站挂衬线的地方始终只有「几号」这一类大字号。
   参考它：顶栏日期也是它全站唯一走手写楷体的地方。 */
.today-md { font-size: 1.75rem; font-weight: 700; color: var(--vibe-foreground); font-family: var(--vibe-font-serif-cjk); }
/* ★ v0.15.3 校历：那天是调休上班 / 放假时，日期旁边挂一个小标 ——
   否则用户看到"周日却排了一整天的课"会以为系统错了（用户 2026-09-19 问的就是这件事）。
   调休 = 品牌橙（那天真的要上课，要显眼）；放假 = 素色（只是个说明）。 */
.today-cal {
  display: inline-flex; align-items: center;
  padding: 0.08rem 0.42rem;
  border-radius: var(--vibe-radius-full);
  font-size: var(--vibe-fs-xs); font-weight: 600; line-height: 1.6;
  color: var(--vibe-brand-700);
  background: var(--vibe-brand-50);
  border: 1px solid var(--vibe-border);
  white-space: nowrap;
}
.today-cal.is-holiday { color: var(--vibe-muted-foreground); background: transparent; }

/* ===== ★ v0.15.4 作业订正登记表：页面里那份"印出来长什么样"的预览 =====
   预览必须**和导出的 .doc 长得一样**（一条一天的格子 + 每天的裁剪虚线），
   否则用户要印出来才发现不对。比例按屏幕缩放，不追求 1:1 尺寸。 */
.pv-blk { position: relative; border-top: 1px dashed var(--vibe-foreground); padding: 0.6rem 0 0.35rem; }
.pv-blk:first-of-type { border-top: none; }
/* ★ v0.15.9：每条裁剪线两头挂个 ✂ —— 和 .doc 里那条裁剪线一致（用户说"看不出该从哪剪"） */
.pv-blk::before, .pv-blk::after {
  content: '✂'; position: absolute; top: -0.58rem; font-size: 0.6rem;
  color: var(--vibe-muted-foreground); line-height: 1;
}
.pv-blk::before { left: 0; }
.pv-blk::after { right: 0; }
.pv-blk:first-of-type::before, .pv-blk:first-of-type::after { content: none; }
.pv-bh { font-size: var(--vibe-fs-sm); font-weight: 600; margin: 0 0 0.3rem; line-height: 1.5; }
/* ★ v0.15.29 预览的页眉：左边说明 + 右边一个 ■ 字符（跟格子里那个小方块一样大）。
   ⚠️ 不用"黑底格子"当示范 —— 手机 WPS 不渲染单元格底纹（用户在手机上看不到）。 */
table.pv-hd { border-collapse: collapse; width: 100%; margin-bottom: 0.35rem; }
table.pv-hd td.pv-ht {
  border: none; padding: 0 0.5rem 0 0; vertical-align: middle;
  font-size: var(--vibe-fs-xs); color: var(--vibe-muted-foreground); line-height: 1.6;
}
table.pv-hd td.pv-sw { width: 9mm; font-size: 11pt; text-align: center; line-height: 1; }
/* 格子里那个「小方块」＝□/○ 字符，字号决定大小（和 .doc 里 td.bx 的 11pt 一致） */
table.pv-grid td.bx { font-size: 11pt; text-align: center; line-height: 1; }
/* ★ v0.15.30 预览的「列标题行」+ 组间粗竖线（和打印一致） */
table.pv-grid td.hh {
  height: 1rem; font-size: var(--vibe-fs-xs); text-align: center;
  color: var(--vibe-muted-foreground); font-weight: 600;
}
table.pv-grid td.no { border-left: 2px solid var(--vibe-foreground); }
.pv-tip { font-size: var(--vibe-fs-xs); font-weight: 400; color: var(--vibe-muted-foreground); margin-left: 0.5rem; }
table.pv-grid { border-collapse: collapse; width: 100%; table-layout: fixed; }
/* 每格 = 学号 + 两个方框（订 / 评）—— **和印出来用同一套画法：方框就是单元格本身**。
   （以前预览用"空 `<i>` + border"、打印用字符 □，两边都跟 WPS 的脾气斗过：
     2026-09-19 先是 WPS 把空元素的边框丢掉、再是它给两个字符算出不一样大的字号。
     ⇒ 现在统一成"真单元格"，预览和打印就再也不会长得不一样了。） */
table.pv-grid td {
  border: 1px solid var(--vibe-border); height: 1.5rem; vertical-align: middle;
  text-align: left; font-size: var(--vibe-fs-xs); font-weight: 600;
  color: var(--vibe-foreground); padding: 0;
}
table.pv-grid td.no { width: 1.6rem; padding-left: 0.25rem; }
table.pv-grid td.bx { width: 1.1rem; }
table.pv-grid td.pad { border: none; }
/* ★ v0.15.32：组间空隙（横）与行间空隙（竖）—— 预览也要跟印出来一致（不然老师看到的和打出来的不一样） */
table.pv-grid td.gap { width: 0.35rem; border: none; }
table.pv-grid tr.rg td { height: 0.3rem; border: none; padding: 0; }
/* ★ v0.15.35：定位角标（预览里也要有，和印出来一致） */
table.pv-grid td.pv-mk { border: none; width: 0.9rem; text-align: center; font-size: var(--vibe-fs-xs); }
.pv-mk { font-size: var(--vibe-fs-xs); margin: 0 0.3rem; }
/* 小组号角标（和印出来的一致：只在每组第一个人前面） */
.pv-gp { font-size: 0.55rem; color: var(--vibe-muted-foreground); margin-right: 0.15rem; }
.pv-idx { font-size: var(--vibe-fs-2xs); color: var(--vibe-muted-foreground); margin: 0.25rem 0 0; line-height: 1.4; }

/* ===== ★ v0.15.14 荣誉榜（可截图 / 导出图片发群）=====
   ⚠️ 舞台是**固定宽 px + 高度跟着内容长**：屏幕上用 `transform: scale()` 只按宽度缩放适配，
   导出 PNG 时把 transform 复位 —— 这样导出的图永远是设计宽度 @2x，不受屏幕大小影响。
   ★ v0.15.43 按用户要求**取消长宽比**（「荣誉榜人都放不下了…不限制长宽比例」）：
   原来是死高（16:9 = 1120×630 / 4:3 = 960×720），47 个人的班光"已完成订正"就 6 行，
   最后一行会被裁掉 ⇒ 现在宽度固定 1120、**高度由内容决定**（`min-height` 兜底，小班不塌）。
   ★ v0.15.15 按用户要求改风格：「礼花柔和一点、背景红一点、更符合家长审美、正式一点」：
   底色从蓝黑换成**绛红 / 中国红 + 一团暖金光晕**（不用亮红 —— 刺眼、显廉价），
   外加一圈**描金细边**；礼花改成**香槟金/暖白/浅玫瑰**三色、慢速飘落、**飘到底自己淡出**、只轻微翻转（不狂转）。 */
.hw-stage {
  position: relative; overflow: hidden; box-sizing: border-box;
  width: 1120px; min-height: 630px; height: auto; transform-origin: top left;
  padding: 34px 44px; border-radius: 18px; color: #fff;
  background:
    radial-gradient(115% 95% at 50% -12%, rgba(255, 216, 164, 0.20) 0%, rgba(255, 216, 164, 0) 62%),
    radial-gradient(140% 125% at 10% 2%, #a8231b 0%, #7f160f 46%, #5a0e0a 100%);
  font-family: "PingFang SC", "Microsoft YaHei", "Hiragino Sans GB", sans-serif;
}
/* 描金细边：一眼"正式"（纯装饰，不吃点击） */
.hw-stage::after {
  content: ''; position: absolute; top: 13px; right: 13px; bottom: 13px; left: 13px; pointer-events: none;
  border: 1px solid rgba(242, 209, 148, 0.30); border-radius: 12px;
}
/* ⚠️ v0.15.43：`.hw-stage.is-43`（4:3 那个死尺寸）已删 —— 现在高度跟着内容长，长宽比没有意义了 */
.hw-hd { display: flex; align-items: baseline; gap: 16px; margin-bottom: 14px; flex-wrap: wrap; }
.hw-hd h2 { margin: 0; font-size: 40px; letter-spacing: 4px; color: #fdf3e2; text-shadow: 0 1px 2px rgba(90, 14, 10, 0.55); }
.hw-hd .sub { font-size: 18px; color: rgba(255, 240, 222, 0.92); }
/* 标题下面一条金色渐隐细线（"喜报"那个味道）—— 用渐变底而不是渐变文字，
   因为 html2canvas 对 `background-clip: text` 支持不稳，导出会变白板。 */
.hw-rule { height: 2px; margin: 0 0 16px; background: linear-gradient(90deg, rgba(242, 209, 148, 0.9) 0%, rgba(242, 209, 148, 0.18) 55%, rgba(242, 209, 148, 0) 100%); }
.hw-sec { margin-bottom: 14px; }
.hw-sec h3 { margin: 0 0 10px; font-size: 21px; font-weight: 700; color: #f2d39a; }
.hw-chips { display: flex; flex-wrap: wrap; gap: 11px; }
.hw-chip {
  display: inline-flex; align-items: baseline; gap: 7px;
  padding: 8px 16px; border-radius: 999px; font-size: 24px; font-weight: 700;
  background: rgba(255, 255, 255, 0.15); border: 1px solid rgba(255, 224, 178, 0.5);
  color: #fff8f0;
}
.hw-chip.gold {
  background: linear-gradient(135deg, rgba(255, 230, 175, 0.96), rgba(236, 193, 118, 0.90));
  border-color: rgba(255, 240, 205, 0.85); color: #6b1408;
  box-shadow: 0 2px 8px rgba(80, 10, 6, 0.28);
}
.hw-chip .no { font-size: var(--vibe-fs-md); font-weight: 400; opacity: 0.7; }
.hw-chips.compact .hw-chip { font-size: 20.5px; padding: 6px 13px; }
.hw-empty { font-size: 19px; color: rgba(255, 240, 222, 0.8); }
/* ★ v0.15.47：「按住说话」按钮 —— 按住开始听、松开停。
   起因（用户 2026-09-22）：「我按了一下就开了，再摁一下没关还是开着」
   ⇒ 老版是"点一下开、再点一下关"，实际没关掉；现在按住说、松手停，并且"正在听"有明确动效。
   ⚠️ v0.15.48：类名从 `.hw-mic` 改成 **`.bzr-mic`** —— 因为 🤖 小助手也要用同一个按钮
      （逻辑抽在 `assets/js/voice-input.js`，**一份代码多处用**），叫 hw- 会误导后来的人。
   ⚠️ `touch-action: none` 是**必须的**：不写的话按住时页面会跟着手指滑，pointerup 就飘了。
   ⚠️ 不用 `transition: all`（全站规矩，只列要动的那几个属性）；
      `prefers-reduced-motion` 下把动效关掉、只留一圈静态描边表示"在听"。 */
.bzr-mic {
  position: relative; display: inline-flex; align-items: center; gap: 0.35rem;
  padding: 0.42rem 0.9rem; border-radius: 999px;
  border: 1px solid var(--vibe-border); background: var(--vibe-card); color: var(--vibe-foreground);
  font-size: var(--vibe-fs-sm); font-weight: 700; cursor: pointer;
  touch-action: none; user-select: none; -webkit-user-select: none; -webkit-touch-callout: none;
  transition: background-color .15s ease, border-color .15s ease, color .15s ease, transform .08s ease;
}
.bzr-mic:active { transform: scale(0.97); }
.bzr-mic:focus-visible { outline: 2px solid var(--vibe-brand-600); outline-offset: 2px; }
.bzr-mic .bzr-mic-ico { display: inline-flex; width: 1.05rem; height: 1.05rem; }
.bzr-mic .bzr-mic-ico svg { width: 100%; height: 100%; }
.bzr-mic.is-on { background: var(--vibe-destructive); border-color: var(--vibe-destructive); color: #fff; }
.bzr-mic.is-on .bzr-mic-ico { animation: bzrMicPulse 1s ease-in-out infinite; }
.bzr-mic-ring {
  position: absolute; top: -4px; right: -4px; bottom: -4px; left: -4px; border-radius: 999px;
  border: 2px solid var(--vibe-destructive); opacity: 0; pointer-events: none;
}
.bzr-mic.is-on .bzr-mic-ring { animation: bzrMicRing 1.15s ease-out infinite; }
/* 迷你版（**只有图标、没有文字**）—— 用在「待办」卡那一行里（v0.15.50）。
   ⚠️ 里面**不放** `.bzr-mic-txt`，所以 voice-input.js 那套"按住说话 / 松开结束"的文字切换对它无效；
      它的反馈靠：**变红 + 外圈扩散 + 松开后的 toast**（那个模块对 txt 为空是安全的，取不到就不改）。 */
.bzr-mic.is-mini { padding: 0.45rem; border-radius: 999px; }
.bzr-mic.is-mini .bzr-mic-ico { width: 1.15rem; height: 1.15rem; }
/* ★ v0.15.63：迷你版默认**只显图标**（桌面空间紧，够用）；
   手机上改成带文字「按住说」，见文件末尾的手机块 —— 起因是用户截图反馈「语音按钮那小一个」。 */
.bzr-mic.is-mini .bzr-mic-txt { display: none; }
@keyframes bzrMicPulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.28); } }
@keyframes bzrMicRing { 0% { opacity: .65; transform: scale(.92); } 100% { opacity: 0; transform: scale(1.32); } }
@media (prefers-reduced-motion: reduce) {
  .bzr-mic.is-on .bzr-mic-ico, .bzr-mic.is-on .bzr-mic-ring { animation: none; }
  .bzr-mic.is-on .bzr-mic-ring { opacity: .5; }
}
/* ---------- 🤖 小助手（v0.15.48）的「待确认清单」----------
   每一行就是一件要办的事：能办的带勾选框、办不了的（名字对不上 / 有歧义 / 只读查询）灰底 + 说明。
   ⚠️ 只读那条（她念数据给你听）单独一档颜色 —— 它**不是**"待你确认的操作"，别和勾选框混在一起。 */
.as-item {
  display: flex; align-items: flex-start; gap: 0.45rem;
  padding: 0.45rem 0.6rem; margin-bottom: 0.35rem;
  border: 1px solid var(--vibe-border); border-radius: var(--vibe-radius-2xs);
  background: var(--vibe-card); color: var(--vibe-foreground);
  font-size: var(--vibe-fs-sm); line-height: 1.6; cursor: pointer;
}
.as-item input[type="checkbox"] { margin-top: 0.2rem; flex: none; }
.as-item.is-bad { cursor: default; color: var(--vibe-muted-foreground); background: var(--vibe-muted); }
.as-item.is-read {
  cursor: default; border-style: dashed;
  background: color-mix(in srgb, var(--vibe-brand-600) 7%, transparent);
}

/* ⚠️ v0.15.43：页脚原来是"绝对定位贴底"（bottom:20px）—— 高度跟着内容长以后，
   人多时名单会长到页脚下面、被压住 ⇒ 改成**跟着内容往下走**（margin-top）。 */
.hw-foot {
  margin-top: 20px;
  display: flex; justify-content: space-between; gap: 12px;
  font-size: 15px; color: rgba(255, 236, 214, 0.8);
}
/* 礼花：**洒金**效果（像红纸上的金箔），几乎不动，只轻轻下坠 + 微转
   ⚠️⚠️ 三条一起才是对的（v0.15.15 走查逮到过"导出的图上没有礼花"）：
       ① 位置用 **inline 的 `top`/`left` 静态散布**，**不要靠动画的 transform 决定位置** ——
          html2canvas 是把 DOM 克隆到 iframe 里重画的，**动画在克隆里会从头开始**，
          如果礼花靠 transform 从画面外飘进来，导出图抓到的就是"全在画面外"= 一张没有礼花的图；
       ② 动画只做**小幅位移**（`--dx` / `--dy` 各自几十 px）+ 轻微旋转 → 帧帧都在画面里；
       ③ 动画曲线**不留全透明帧**（0.34~0.7 之间波动）→ 截图随手一抓都有礼花。
   ⚠️ 礼花是"洒金底"，必须垫在文字下面：`.hw-fwbox` 在 DOM 里排在 `#hw-honor-body` 前面。 */
.hw-fwbox { position: absolute; top: 0; right: 0; bottom: 0; left: 0; overflow: hidden; pointer-events: none; }
.hw-fw {
  position: absolute; width: 8px; height: 12px; border-radius: 3px;
  opacity: 0.5; animation-name: hwFall; animation-timing-function: linear; animation-iteration-count: infinite;
}
@keyframes hwFall {
  0% { transform: translate3d(0, 0, 0) rotate(0deg); opacity: 0.34; }
  14% { opacity: 0.7; }
  72% { opacity: 0.55; }
  100% { transform: translate3d(var(--dx, 0px), var(--dy, 46px), 0) rotate(var(--rot, 70deg)); opacity: 0.4; }
}
@media (prefers-reduced-motion: reduce) { .hw-fw { animation: none; opacity: 0.5; } }
/* 页脚上面一条极淡的金线：给"下半部留白"一点收口（人少的班不至于显得空） */
.hw-foot-rule { height: 1px; margin-top: 22px; background: linear-gradient(90deg, rgba(242, 209, 148, 0) 0%, rgba(242, 209, 148, 0.28) 50%, rgba(242, 209, 148, 0) 100%); }

/* ===== 班级筹码 = 班级切换入口（v0.10.5）===== */
.today-class-wrap { position: relative; }
.today-class-chip {
  display: inline-flex; align-items: center; gap: 0.3rem;
  padding: 0.22rem 0.5rem 0.22rem 0.62rem;
  border-radius: var(--vibe-radius-full);
  font: inherit; font-size: var(--vibe-fs-sm); font-weight: 600; line-height: 1.5;
  color: var(--vibe-brand-700);
  background: linear-gradient(135deg,
      color-mix(in srgb, var(--vibe-brand-100) 85%, var(--vibe-card)) 0%,
      color-mix(in srgb, var(--vibe-brand-50) 92%, var(--vibe-card)) 100%);
  border: 1px solid color-mix(in srgb, var(--vibe-brand-200) 75%, transparent);
  box-shadow: var(--vibe-shadow-xs);
  cursor: pointer; white-space: nowrap;
  transition: box-shadow 150ms var(--vibe-ease), background-color 150ms var(--vibe-ease), transform 120ms var(--vibe-ease);
}
.today-class-chip:hover { box-shadow: var(--vibe-shadow-sm); }
.today-class-chip:active { transform: scale(0.97); }
.today-class-chip[aria-expanded="true"] { box-shadow: var(--vibe-shadow-sm); }
/* 图标是 lucide 生成的（<i> 会被换成 <svg>，类名可能没带过来），所以两种都认 */
.today-class-chip .chip-caret,
.today-class-chip > svg {
  width: 0.85rem; height: 0.85rem; opacity: 0.65; flex-shrink: 0;
  transition: transform 160ms var(--vibe-ease);
}
.today-class-chip[aria-expanded="true"] .chip-caret,
.today-class-chip[aria-expanded="true"] > svg { transform: rotate(180deg); }
/* 只有一个班：没有「切换」可言，把箭头的暗示收掉 */
.today-class-chip.is-single .chip-caret,
.today-class-chip.is-single > svg { display: none; }

/* 班级切换面板：绝对定位挂在筹码下（会盖到内容区上，见 dashboard.js 的注释） */
.class-menu {
  position: absolute; z-index: 60; top: calc(100% + 0.4rem); left: 0;
  min-width: 12.5rem;
  max-width: min(18rem, calc(100vw - 2rem));
  max-height: 60vh; overflow-y: auto;
  padding: 0.3rem;
  background: var(--vibe-card);
  border: 1px solid var(--vibe-border);
  border-radius: var(--vibe-radius-sm);
  box-shadow: var(--vibe-shadow-lg);
  animation: bzr-menu-in 160ms var(--vibe-ease-out);
}
@keyframes bzr-menu-in { from { opacity: 0; transform: translateY(-6px); } }
.class-menu-item {
  display: flex; align-items: center; gap: 0.5rem; width: 100%;
  padding: 0.45rem 0.6rem;
  border: none; background: none; text-align: left; cursor: pointer;
  border-radius: var(--vibe-radius-xs);
  font: inherit; font-size: var(--vibe-fs-base); color: var(--vibe-foreground);
  transition: background-color 130ms var(--vibe-ease);
}
.class-menu-item:hover { background: var(--vibe-muted); }
.class-menu-item.is-current {
  color: var(--vibe-brand-700); font-weight: 600;
  background: color-mix(in srgb, var(--vibe-brand-50) 85%, transparent);
}
.class-menu-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.class-menu-check, .class-menu-item > svg {
  width: 0.95rem; height: 0.95rem; color: var(--vibe-brand-600); flex-shrink: 0;
}
/* （v0.10.9 删掉了 .class-menu-foot —— 班级切换面板里塞「座位表」入口既不合逻辑又占地方，
   座位表的入口本来就在「班级 → 座位表」。面板现在只有班级列表。） */

.today-term-row {
  display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;
  min-height: 1.5rem;
}
.today-term {
  margin: 0; font-size: var(--vibe-fs-sm); color: var(--vibe-muted-foreground); line-height: 1.5;
  display: flex; align-items: center; gap: 0.35rem; flex-wrap: wrap;
}
.today-term-set {
  font-size: var(--vibe-fs-xs); color: var(--vibe-muted-foreground); text-decoration: none;
  border-bottom: 1px dashed color-mix(in srgb, var(--vibe-border) 100%, transparent);
}
/* ★ v0.15.61 颜色减法：**链接统一"中性 + hover 才上色"**。
   原来这一类"正文里的文字链接"常年是橙的（`brand-600`）—— 一页里出现几个，
   就变成"到处都是可点的橙字"，而橙色本该留给主操作。悬停时才上色，既安静又还认得出。 */
.today-term-set:hover { color: var(--vibe-brand-600); }
.today-head-actions { display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0; }

/* ===== 页头图标按钮（v0.10.35）
   ⚠️ 「编辑首页」铅笔与「通知」铃铛必须共用**同一个盒子**，否则必然错位：
      v0.10.34 实测（390px）铅笔盒 40×36、铃铛盒 20×20 → 中心 y 差 8px；
      而且在桌面宽度下即使两个盒子一样高，铃铛外面那层 div 是默认 block，
      inline-block 的按钮按基线摆放，div 比按钮多出约 3px → 铃铛整体被摁下去。
   所以这里把尺寸 / display / 对齐 / padding / line-height 全部锁死一致，
   里面的 svg 也统一 1.1rem 且 display:block（inline 基线会再偷走几像素）。
   ⚠️ 别再靠 padding / margin 微调去「凑」对齐 —— 一改尺寸又会散。 */
.head-icon-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 2.25rem; height: 2.25rem;        /* 桌面 36×36 */
  padding: 0; margin: 0; flex-shrink: 0;
  line-height: 1; box-sizing: border-box;
  border: none; background: none; cursor: pointer;
  color: var(--vibe-muted-foreground);
  transition: color 150ms var(--vibe-ease);
}
.head-icon-btn:hover { color: var(--vibe-foreground); }
.head-icon-inner { position: relative; display: flex; align-items: center; justify-content: center; }
.head-icon-btn > svg, .head-icon-inner > svg {
  display: block; width: 1.1rem; height: 1.1rem; flex-shrink: 0; stroke-width: 2;
}
/* ★ v0.18.73：**带文字的页头按钮** —— 纯图标用户认不出来。
   用户 2026-09-29：「在主页那个里面…中间搞了这么多，我压根都看不到下面居然还有一个按钮」
   （手机主页 15 张卡、4236px 高，小助手卡排第 6，「跟她说一句」要往下滑才看得见；
   而页头那颗**机器人图标**他根本没认出来是它）。
   ⇒ 给页头这颗**加上文字**，它一直在首屏 ⇒ "找不到"这件事就没了。
   ⚠️ 不能直接往 `.head-icon-btn` 里塞文字：那个类**宽高写死 36×36**，塞进去会被压扁/溢出。
      ⇒ 单开一个变体，宽度跟着内容走、高度和图标按钮对齐（都是 2.25rem）。 */
.head-text-btn {
  display: inline-flex; align-items: center; gap: 0.35rem;
  height: 2.25rem; padding: 0 0.6rem; margin: 0; flex-shrink: 0;
  border: none; background: none; cursor: pointer; box-sizing: border-box;
  font-size: var(--vibe-fs-sm); font-family: var(--vibe-font-sans);
  color: var(--vibe-muted-foreground); border-radius: var(--vibe-radius-sm);
  transition: color 150ms var(--vibe-ease), background 150ms var(--vibe-ease);
}
.head-text-btn:hover { color: var(--vibe-foreground); background: var(--vibe-muted); }
.head-text-btn > svg { display: block; width: 1.1rem; height: 1.1rem; flex-shrink: 0; stroke-width: 2; }
/* ⚠️ 铃铛外面那层 div（#notif-wrap）必须是 flex：留成默认 block 就会引入上面说的基线偏移 */
#notif-wrap { display: flex; align-items: center; justify-content: center; }
@media (max-width: 1023px) { .head-icon-btn { width: 2.5rem; height: 2.5rem; } }  /* 手机 40×40 */
/* 铅笔与铃铛之间的竖分隔线：固定高度、自身居中；左右间距由 .today-head-actions 的 gap 保证对称。
   只在铅笔可见的宽度（≤1023px，见 dashboard.html）露出来，桌面端铅笔隐藏时它也跟着藏。 */
.today-head-sep {
  display: none; width: 1px; height: 1.1rem; flex-shrink: 0;
  align-self: center; background: var(--vibe-border);
}

/* ===== 页头「一周」条 = 日期切换器（v0.10.4 建 · v0.10.6 起可点）=====
   · 点某天 → 预览那天的课表 / 我的课 / 值日（selected = 实心品牌色的那格）
   · 横向拖动 → 手指滑 / 桌面鼠标按住拖（v0.10.9 去掉了两端的 ‹ › 按钮）
   · 今天永远是「浅品牌底 + 主色数字」，跟选中态区分开：一个是「我在哪」，一个是「我在看哪天」
   放在页头（桌面上与「大日期」同一行）—— 一直看得见，不用滚动去找。 */
.today-week-bar {
  flex: 1;                       /* 桌面：与「日期」挤在同一行，把日期条推到中间 */
  display: flex; align-items: center; gap: 0.5rem;
  margin-top: 0;
  min-width: 0;
}
/* 日期条（v0.10.9）：横向滚动的条，不再是一屏 7 格 + 两个箭头按钮。
   · 一次渲染 21 天（今天 -3 ~ 今天 +17），手指左右拖 / 桌面鼠标按住拖都能滑
   · scroll-snap 让手指松开后自动吸附到格子，不会停在半格上
   · touch-action: pan-x —— **只接横向手势**，纵向留给页面滚动，否则手机上滑不动页面
   · 隐藏滚动条（有齿状滚动条就露馅了） */
.today-week {
  flex: 1;
  display: flex; gap: 0.375rem;
  min-width: 0;
  overflow-x: auto; overflow-y: hidden;
  scroll-snap-type: x proximity;
  scroll-padding-inline: 2px;
  -webkit-overflow-scrolling: touch;
  touch-action: pan-x;
  user-select: none; -webkit-user-select: none;   /* 拖动时别把字选蓝 */
  padding: 2px 2px 5px;                           /* 给选中态的阴影留地方（不然被 overflow 裁掉） */
  /* 右端渐隐（v0.10.20）：日期条是要横向滚的，最右边那格必然被裁掉半格 ——
     硬裁看着像「页面坏了」，加一层渐隐就变成「还有，往右滑」。
     用 mask 而不是盖一个渐变块：mask 不占布局、也不会挡住底下的点击。 */
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 2.5rem), transparent);
  mask-image: linear-gradient(to right, #000 calc(100% - 2.5rem), transparent);
  scrollbar-width: none;
}
.today-week::-webkit-scrollbar { display: none; }
.week-cell {
  flex: 0 0 auto;
  width: 3.25rem;               /* 手机 52px（原 41px 太挤） */
  scroll-snap-align: center;
  display: flex; flex-direction: column; align-items: center; gap: 0.08rem;
  min-height: 2.9rem;           /* 有/无事件点的高度一致，整行不抖 */
  padding: 0.34rem 0.1rem 0.3rem;
  border: 1px solid transparent;
  border-radius: 14px;
  background: color-mix(in srgb, var(--vibe-muted) 85%, var(--vibe-card));
  border-color: color-mix(in srgb, var(--vibe-border) 55%, transparent);
  font: inherit; color: inherit; text-align: center; cursor: pointer;
  transition: background-color 150ms var(--vibe-ease), box-shadow 150ms var(--vibe-ease),
              transform 120ms var(--vibe-ease), border-color 150ms var(--vibe-ease);
}
/* hover 抬起来：底色趋白 + 描边收紧 + 一道浅影，跟前面几个「可点的东西」同一套反馈 */
.week-cell:hover {
  background: var(--vibe-card);
  border-color: color-mix(in srgb, var(--vibe-border) 90%, transparent);
  box-shadow: var(--vibe-shadow-sm);
  transform: translateY(-1px);
}
.week-wd { font-size: var(--vibe-fs-xs); color: var(--vibe-muted-foreground); line-height: 1.1; }
.week-dn { font-size: 0.9375rem; font-weight: 700; color: var(--vibe-foreground); line-height: 1.15; }
.week-dots { display: flex; align-items: center; justify-content: center; gap: 2px; height: 5px; }
.week-dot { width: 5px; height: 5px; border-radius: 999px; }
/* 今天：浅品牌底 + 主色数字，一眼找到「我在哪」 */
.week-cell.is-today { background: color-mix(in srgb, var(--vibe-brand-50) 92%, var(--vibe-muted)); }
.week-cell.is-today .week-dn { color: var(--vibe-primary); }
/* 有考试/家长会/活动的那天：品牌色描边（打点见 JS，不写标题） */
.week-cell.has-ev {
  border-color: color-mix(in srgb, var(--vibe-brand-200) 70%, transparent);
  background: var(--vibe-brand-50);
}
/* 正在看的那天（最后写，压住上面两种状态） */
.week-cell.is-sel {
  background: linear-gradient(140deg, var(--vibe-brand-500) 0%, var(--vibe-brand-600) 100%);
  border-color: transparent;
  box-shadow: 0 4px 12px color-mix(in srgb, var(--vibe-primary) 26%, transparent);
}
.week-cell.is-sel .week-wd,
.week-cell.is-sel .week-dn { color: #fff; }
/* 实心底上的彩色事件点会看不清，给它一圈白边 */
.week-cell.is-sel .week-dot { box-shadow: 0 0 0 1.5px color-mix(in srgb, #fff 85%, transparent); }
.week-cell.is-sel:hover { background: linear-gradient(140deg, var(--vibe-brand-500) 0%, var(--vibe-brand-600) 100%); }
/* 桌面端：格子放宽到 68px（一屏能看到约 14 天，够点；再宽就是空耗横向空间） */
@media (min-width: 768px) {
  .week-cell { width: 4.25rem; }
}

/* ===== 「正在预览别的日期」的两个标记（v0.10.6）===== */
.preview-tag {
  display: inline-flex; align-items: center; gap: 0.3rem;
  padding: 0.12rem 0.5rem;
  border-radius: 999px;
  font-size: var(--vibe-fs-xs); font-weight: 600;
  color: var(--vibe-primary);
  background: color-mix(in srgb, var(--vibe-primary) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--vibe-primary) 28%, transparent);
  white-space: nowrap;
}
.preview-tag::before {
  content: ''; width: 5px; height: 5px; border-radius: 999px; background: currentColor; flex-shrink: 0;
}
.today-back {
  padding: 0.22rem 0.6rem;
  border-radius: 999px;
  font: inherit; font-size: var(--vibe-fs-sm); font-weight: 600;
  color: var(--vibe-foreground);
  background: color-mix(in srgb, var(--vibe-card) 85%, transparent);
  border: 1px solid var(--vibe-border);
  cursor: pointer; white-space: nowrap;
  transition: box-shadow 150ms var(--vibe-ease), background-color 150ms var(--vibe-ease),
              transform 120ms var(--vibe-ease);
}
.today-back:hover { box-shadow: var(--vibe-shadow-sm); }
.today-back:active { transform: scale(0.97); }
/* 预览中：页头的渐变整体偏品牌色一档 —— 扫一眼就知道「这不是今天」 */
.today-page-head.is-preview {
  background-image: linear-gradient(180deg,
      color-mix(in srgb, var(--vibe-brand-100) 82%, var(--vibe-card)) 0%,
      var(--vibe-card) 90%);
}

/* ===== 铃铛轻摇（v0.10.4）=====
   有未读时才摇，而且每 5 秒只摇一下（幅度 ≤10°）—— 是「提醒」，不是「抖动」。
   点开面板或鼠标悬停时立刻停，不跟正在读通知的人抢注意力。 */
@keyframes bzr-bell-ring {
  0%, 74%, 100% { transform: rotate(0deg); }
  77% { transform: rotate(10deg); }
  81% { transform: rotate(-9deg); }
  85% { transform: rotate(6deg); }
  89% { transform: rotate(-4deg); }
  93% { transform: rotate(2deg); }
}
#notif-btn.is-ringing svg {
  transform-origin: 50% 14%;
  animation: bzr-bell-ring 5s var(--vibe-ease) 800ms infinite;
}
#notif-btn.is-ringing:hover svg,
#notif-wrap:has(#notif-panel:not(.hidden)) #notif-btn.is-ringing svg { animation: none; }

/* ===== 今日页内容流（块间距基座，v0.10.7 建 · v0.10.9 真修）=====
   块与块之间的间距**只由 row-gap 决定**（桌面 16 / 手机 12）。
   ⚠️⚠️ **Tailwind v4 的 `space-y-*` 生成的是 `margin-bottom`，不是 `margin-top`！**
      v0.10.7 只清了 margin-top → 没用，真实间距变成 `row-gap + 24px`（以为 12px，实测 36px，
      白改了一版）。所以这里 `> *` 两个方向都要清。
   ⚠️ 别删 `!important`：它要压过 Tailwind 的 `.space-y-6 > :not([hidden]) ~ :not([hidden])`。
   ⚠️ 改完必须**量元素之间的真实 rect 距离**，只读 `rowGap` 的 CSS 值会被骗（v0.10.7 的教训）。 */
.today-stack {
  display: flex; flex-direction: column;
  row-gap: 1rem;                 /* 桌面刻度 16 */
}
.today-stack > * { margin-top: 0 !important; margin-bottom: 0 !important; }

/* ===== 栅格：手机一列 / ≥1024 三列等宽 =====
   v0.10.22 改成「列容器 + 瀑布流」：.today-grid 只负责摆三列，
   每列（.today-col）是一个独立的纵向堆叠 —— 卡片在列内自己往下排，列与列互不影响。
   ⚠️ 为什么不做 12 列等格网格（用户提过「一格一格」）：卡片内容量差太多
      （课表 470 高、值日 120 高），等格会在矮的那格底下留一大片白，比参差更难看；
      瀑布流没有行内空洞，而且天然支持「列内上下移」这种排序。
   第三列里的信息在窄屏上已由「顶部倒计时条 + 主视觉筹码」覆盖，摆出来就是重复，
   所以在 <1024px 整列隐藏（见下面 .today-col-3）。
   v0.10.24：1024~1279 那档「第三列折成一行两张卡横排」已删除，栅格从 1024 起就是三列等宽
   （理由见下面 .today-col-3 那段）—— **只留一个断点，不再有横排特例。** */
.today-grid {
  display: grid; gap: 1rem;
  /* ⚠️⚠️ 这里在 v0.10.17 改成了 stretch，v0.10.20 又改回 start —— 记一下为什么，
     免得下次有人再「顺手对齐一下」：
       · v0.10.17 看到的是「三列底部参差」，就改成了等高。但当时页面真正难看的原因是
         **底色和卡片只差一个色阶**（对比度 1.11），跟参差没关系 —— 那一版把两件事混在一起了。
       · 等高之后，行高 = 最高那一列，其余列多出来的高度**全变成卡片内部的死白**：
         课表卡底下空 190px、待办卡中间空 180px。等于把空白从「页面级」搬进了「卡片里」，
         更像没做完。后来的月历又把行高从 524 顶到 733，死白变成 250px，更明显。
       · 结论：**卡片内部一大块白，比三列底部参差难看得多**。现代看板（GitHub / Linear）
         本来就是参差的。参差不是问题，内容撑不满才是 —— 那要用「加内容」解决，不是拉伸。
     现在按内容高度，每张卡都刚好包住自己的内容。 */
  align-items: start;
  grid-template-columns: 1fr;
}
/* 列容器：纵向堆叠，卡片自己往下排 */
.today-col { display: grid; gap: 1rem; align-content: start; min-width: 0; }
/* 被用户在「设置 → 首页布局」里关掉的卡片 / 整列（v0.10.22）
   ⚠️ 用 !important：.today-card 自己写着 display:flex、.today-col 写着 display:grid，
      普通选择器压不过它们，卡片不会真的消失。 */
.today-card.is-off, .today-col.is-off { display: none !important; }
/* v0.11.0：这张卡**没数据**时整卡不出现（不摆一张空卡）。
   和 .is-off 分开两个类是有意的：.is-off 是「用户自己在布局里关掉的」，
   .is-nodata 是「系统判断现在没内容可显示」—— 混用一个类的话，
   数据回来后代码就会把用户手动关掉的卡也一起放出来。 */
.today-card.is-nodata { display: none !important; }

/* 成绩预警卡（v0.11.0）的行列表：warn 用红色强调，其余低调。
   刻意不用表格：一列数字 + 短句在窄列里换行很丑，grid 三栏（姓名/标签/说明）更稳。 */
.ga-head { display: flex; align-items: baseline; justify-content: space-between; gap: .5rem; margin-bottom: .35rem; font-size: var(--vibe-fs-xs); color: var(--vibe-muted-foreground); }
.ga-count { flex: none; }
.ga-row { display: grid; grid-template-columns: auto auto minmax(0, 1fr); align-items: baseline; gap: .4rem; padding: .32rem .05rem; border-bottom: 1px dashed color-mix(in srgb, var(--vibe-border) 80%, transparent); font-size: var(--vibe-fs-sm); }
.ga-row:last-of-type { border-bottom: none; }
.ga-name { font-weight: 600; color: var(--vibe-foreground); white-space: nowrap; }
.ga-row.is-warn .ga-name { color: var(--vibe-destructive); }
.ga-tag { padding: 0 .3rem; border-radius: var(--vibe-radius-2xs); background: var(--vibe-muted); color: var(--vibe-muted-foreground); font-size: var(--vibe-fs-2xs); white-space: nowrap; }
.ga-row.is-warn .ga-tag { background: color-mix(in srgb, var(--vibe-destructive) 14%, transparent); color: var(--vibe-destructive); }
.ga-detail { min-width: 0; color: var(--vibe-muted-foreground); }
.ga-more { margin-top: .35rem; font-size: var(--vibe-fs-xs); color: var(--vibe-muted-foreground); }
/* ★ v0.18.46：作业提醒卡里那两行小标题（「要关注」/「要表扬」）——
   一张卡里有两类人，不给小标题老师会以为表扬的也是问题。 */
.ga-sub { margin: .45rem 0 .1rem; font-size: var(--vibe-fs-xs); font-weight: 700; color: var(--vibe-muted-foreground); }
/* ★ v0.18.23：AI 学情提醒那块（放在「成绩预警」卡里、规则行**上面**）。
   为什么在上：规则行答"谁、哪科、掉了多少"，AI 那段答"先做什么" —— 要动手的那句该先被看到。
   ⚠️ 只用本项目真有的令牌（v0.18.22 我误用了别处的 `tag-purple`/`--vibe-lavender`，
      结果角标一直是没样式的裸文字 —— 这类"名字从别处串来"的错，⑥ 号守卫已能自动抓）。 */
.ga-ai {
  margin-bottom: .5rem; padding: .55rem .65rem;
  border-radius: var(--vibe-radius-xs);
  background: color-mix(in srgb, var(--vibe-brand-600) 7%, transparent);
  border: 1px solid color-mix(in srgb, var(--vibe-brand-600) 18%, transparent);
}
.ga-ai-h { display: flex; align-items: center; gap: .35rem; margin-bottom: .3rem; }
.ga-ai-badge {
  flex: none; padding: 0 .3rem; border-radius: var(--vibe-radius-2xs);
  background: var(--vibe-brand-600); color: #fff;
  font-size: var(--vibe-fs-2xs); font-weight: 700; letter-spacing: .04em;
}
.ga-ai-t { font-size: var(--vibe-fs-xs); font-weight: 600; color: var(--vibe-brand-600); }
.ga-ai-body { font-size: var(--vibe-fs-sm); line-height: 1.7; color: var(--vibe-foreground); }
.ga-ai-tail { margin-top: .2rem; font-size: var(--vibe-fs-2xs); color: var(--vibe-muted-foreground); }

/* ===== 知识点胶囊（★ v0.18.26）=====
   用户 2026-09-26：「这个知识点光从列出来的话…太乱太杂了，能不能一个知识点就搞一个框框、
   一个圆圈一样的东西给它框起来，加个颜色好看一点，看着也清晰一点」。
   ⇒ 凡是**成串**出现考点的地方（连成一行的表格、堆成一段的列表、家长页那几行），
   一律换成这个胶囊：一个词一个圆角框，一眼看得出"这个词到哪儿结束"。
   ⚠️ 四个变体别混用（混了就看不出层级）：
      · 默认        = 知识点 / 考点
      · `.is-chapter` = 章 / 板块（底更深、字更重）—— 让人一眼分出"大块"和"小点"
      · `.is-off`    = 没标 / 库外的词（灰）
      · `.is-lost`   = 带着丢分的（暖色，跟"要补"同一档）
   ⚠️ 颜色全走令牌（守卫 ④ 会盯着 `--vibe-*`），色值一律不写死。 */
.kp-chip {
  display: inline-block; padding: 0.02rem 0.45rem; margin: 0 0.15rem 0.15rem 0;
  border-radius: 999px;
  border: 1px solid color-mix(in srgb, var(--vibe-brand-600) 26%, transparent);
  background: color-mix(in srgb, var(--vibe-brand-600) 8%, transparent);
  color: var(--vibe-brand-800);
  font-size: var(--vibe-fs-xs); line-height: 1.7; white-space: nowrap; vertical-align: 1px;
}
.kp-chip.is-chapter {
  border-color: color-mix(in srgb, var(--vibe-brand-600) 42%, transparent);
  background: color-mix(in srgb, var(--vibe-brand-600) 15%, transparent);
  font-weight: 600;
}
.kp-chip.is-off { border-color: var(--vibe-border); background: var(--vibe-muted); color: var(--vibe-muted-foreground); }
.kp-chip.is-lost {
  border-color: color-mix(in srgb, var(--vibe-destructive) 26%, transparent);
  background: color-mix(in srgb, var(--vibe-destructive) 8%, transparent);
  color: var(--vibe-destructive);
}
.kp-chip b { font-weight: 700; }

/* ===== 板块色（★ v0.18.30）=====
   用户 2026-09-27：「所有的字都是同一个颜色，会不会不是很清晰？比如一个知识点一个颜色…
   一个大的知识点、比如一个章节或者几何、数与式，就一个颜色」。
   ⇒ 采纳米的一半：**按「板块」（课标四大领域）上色，不按「章」** ——
      章有 32 个，32 个色相在色轮上分不完，相邻章必然"看着一个色"，结果既没分出来又更花；
      板块只有 4 类，是人眼一次能记住的数量。
   ⚠️⚠️ **四个通道各管一件事，别互相抢**（这是这一版最要紧的设计约束）：
      · **色相 = 板块**（这一块是"哪一领域"）—— 就用下面这四条；
      · **深浅/层级 = 章 vs 知识点**（`.is-chapter` 底更深）；
      · **虚线 = 状态**（`.is-off` 未标/库外、`is-pending` 待审）；
      · **丢分 = 红色加粗描边 + 那个数字**（`.is-lost`，**不再用填充色相** ——
        否则"红底"和"板块蓝底"会在同一个框上打架，用户就看不出哪个信息更重要了）。
   ⚠️ 颜色值只写在这儿（令牌），后面的规则一律 `var()`；四个都挑的是**在暖纸底上分得开、
      又不跟品牌橙红（#f1481e 系）撞**的色相。 */
:root {
  --vibe-kp-algebra: #2f6fd0;   /* 数与代数 —— 蓝 */
  --vibe-kp-geo:     #1f8a5c;   /* 图形与几何 —— 绿 */
  --vibe-kp-stat:    #7a5bd0;   /* 统计与概率 —— 紫 */
  --vibe-kp-prac:    #0f8f96;   /* 综合与实践 —— 青 */
}
/* 带 `data-d` 的胶囊按板块上色。⚠️ **未标/库外的词不给 `data-d`**（它们本来就没归类）⇒
   不会和上面那几条既有规则打架。 */
.kp-chip[data-d="数与代数"] { --kp-hue: var(--vibe-kp-algebra); }
.kp-chip[data-d="图形与几何"] { --kp-hue: var(--vibe-kp-geo); }
.kp-chip[data-d="统计与概率"] { --kp-hue: var(--vibe-kp-stat); }
.kp-chip[data-d="综合与实践"] { --kp-hue: var(--vibe-kp-prac); }
.kp-chip[data-d] {
  border-color: color-mix(in srgb, var(--kp-hue) 62%, transparent);
  background: color-mix(in srgb, var(--kp-hue) 20%, transparent);
  color: var(--vibe-foreground);
}
/* `.is-lost`（带丢分）在有板块色时：**底还是板块色、改用红色加粗描边**（两个通道并排说两件事） */
.kp-chip.is-lost[data-d] {
  border-width: 2px;
  border-color: color-mix(in srgb, var(--vibe-destructive) 58%, transparent);
  color: var(--vibe-foreground);
}

/* ===== 首页卡片拖拽（v0.10.23）=====
   抓手由 dashboard.js 动态插进每张卡的标题栏（不然要改 5 处 HTML，加新卡还会漏）。
   ⚠️ 常显（淡），不是「悬停才出现」：用户反馈过「不能移动」——
      如果抓手要悬停才显形，他们根本不会知道这卡能拖。发现性 > 视觉极简。 */
.drag-handle {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.4rem; height: 1.4rem; flex-shrink: 0;
  border-radius: var(--vibe-radius-2xs);
  color: var(--vibe-muted-foreground);
  opacity: 0.3; cursor: grab;
  /* ⚠️ 必须写：不写的话触屏上按住抓手拖动会变成「滚动页面」，pointermove 收不到 */
  touch-action: none;
  transition: opacity 0.15s ease, color 0.15s ease, background 0.15s ease;
}
.today-card:hover .drag-handle { opacity: 0.6; }
.drag-handle:hover { opacity: 1; color: var(--vibe-brand-600); background: var(--vibe-muted); }
.drag-handle:active { cursor: grabbing; }
/* v0.10.24：标题栏整条也能发起拖拽（JS 里同时放行了 .today-head）。
   抓手只有 22px，用户下意识是去拖标题 —— 光标也要跟着变，不然看不出来能拖。
   里面的「课表 ›」「管理 ›」是链接/按钮，保持 pointer。 */
@media (min-width: 1024px) {
  .today-head { cursor: grab; }
  .today-head:active { cursor: grabbing; }
  .today-head .today-more { cursor: pointer; }
}
/* 触屏没有 hover，再亮一点 */
@media (hover: none) { .drag-handle { opacity: 0.45; } }
/* ⚠️ <1024 不显示抓手：那个宽度下是单列堆叠，拖来拖去没有意义
   （手机上想调顺序走「设置 → 首页布局」，↑↓ 按钮在任何宽度都能用）。
   dashboard.js 里也有一道同样的 1024 判断，**两处必须一起改**。
   v0.10.24：门槛从 1280 降到 1024 —— 见下面 .today-grid 那段注释。 */
@media (max-width: 1023px) { .drag-handle { display: none !important; } }

/* 正在拖的那张：脱离文档流、跟随指针。
   ⚠️ pointer-events: none 是必须的 —— 卡片跟着指针走，如果它还能被命中，
      elementFromPoint 永远返回它自己，就永远算不出「该插到哪儿」。 */
.today-card.is-dragging {
  position: fixed !important; z-index: 80; margin: 0 !important;
  box-shadow: var(--vibe-shadow-lg, 0 12px 28px rgba(0, 0, 0, 0.16));
  transform: rotate(0.5deg);
  pointer-events: none;
  transition: none !important;
  animation: none !important;
}
/* 占位符：既顶住原来那块位置（不然布局会塌），又充当「松手落在这儿」的提示 */
.drag-ph {
  border: 1.5px dashed color-mix(in srgb, var(--vibe-brand-500) 55%, transparent);
  background: color-mix(in srgb, var(--vibe-brand-500) 5%, transparent);
  border-radius: var(--vibe-radius-md);
  transition: none;
}
/* 拖拽中，把「因为被清空而收起来」的列临时放出来当落点。
   ⚠️ 不放出来那列就**永久废掉**了：列一空就被打上 .is-off → display:none，
      而 colAt() 又会跳过 .is-off 的列 —— 被拖走的那张卡再也回不去，
      用户只能去「设置 → 首页布局 → 恢复默认」。v0.10.24 实测踩到过。 */
body.layout-dragging .today-col.is-off {
  display: grid !important;
  min-height: 6rem;
  border: 1.5px dashed color-mix(in srgb, var(--vibe-brand-500) 40%, transparent);
  border-radius: var(--vibe-radius-md);
}
.today-col-3 { display: none; }
/* ===== ≥1024 一律「三列等宽」=====
   v0.10.24 把 1024~1279 那段「第三列折成一行两张卡横排」的写法删掉了。为什么：
     ① **它看着就是「没对齐」**：col-3 是 grid-column: 1/-1，得等前两列里**高的那列**排完
        才开始 —— 于是 1200px 下「课表」很高、「待办」很矮，待办底下空出一大块，
        值日还被压在课表下面，整屏右半边是个洞。用户原话「也没有对齐」就是这个。
     ② **它让拖拽没法在 1024~1279 用**：横排那一列不存在「上下插入位置」，
        所以 v0.10.23 只能把抓手整段隐藏 —— 用户就表达成「不能移动」。
     ③ 而 1024px 下三列各 ~234px 其实**够用**（课表卡最窄只要 230 左右，
        时间 + 课名 + 老师三列还排得下；1280 下本来就是 320 在跑）。
   一句话：**同一套栅格从头用到尾，横排特例是万恶之源。** */
@media (min-width: 1024px) {
  .today-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .today-col-3 { display: grid; }
  /* ⚠️ 只要第三列出现，就把「顶部重要日期条」和「主视觉里的我的课筹码」收掉：
     它们和第三列里的「近期安排 / 我的课」是同一份信息，同时出现就是重复。
     反过来，<1024px 时第三列整列隐藏，这两处就是唯一的来源 —— 必须留着。
     一句话：**同一份信息在任一宽度下只出现一次。** */
  .key-date { display: none !important; }
  .now-hero-my-lessons { display: none; }
  /* v0.10.20 补：主视觉那行「共 N 节」和卡标题重复了。
     <1024 时它是对的（主视觉是唯一能看到「共几节」的地方）；
     一旦第三列出现，「下周一课表 共 9 节 / 下周一我的课 共 9 节」就在同一屏里写着同一件事 ——
     同一份信息在任一宽度下只出现一次，这条口径这里以前漏了。 */
  .now-hero-count { display: none; }
}
/* ===== 卡片高度档（v0.10.25）=====
   用户提的「定几个长度，固定就那么几种」。先说清它**不解决什么**：
   量出来的真实高度是 值日 182 / 近期安排 313 / 待办 379 / 我的课 404 —— 差 2.2 倍，
   **它们本来就不是一簇**，所以定档不会让三列底部对齐（要底部对齐只能上行列网格，
   v0.10.17 试过 stretch、v0.10.20 回退了，理由见上面 .today-grid 那段）。
   定档真正买到的是**版式稳定**：待办从 3 条涨到 12 条、值日岗位变多、某天 10 节课，
   卡片不会自己长高把整列顶长。超出的内容在 `.card-fill` 里滚动。
   取值 = 现状 + 余量，保证现在这批数据不触发滚动。
   ⚠️ 课表卡（485px）**不参与**：它是这一页的主内容，按内容走。 */
:root {
  --card-h1: 14rem;   /* 224 ← 今日值日(182) / 今天谁过生日 */
  --card-h2: 21rem;   /* 336 ← 近期安排(313) */
  --card-h3: 27rem;   /* 432 ← 待办(379) / 我的课(404) */
  /* ★ v0.15.49：**卡片可选「大 / 小」**（第一个用上的是「待办」卡）。
     用户 2026-09-22：「待办这个功能还挺好的 但是在手机上占地面积太大了 能不能选择它是大是小」
     —— 27rem(432px) 在手机上等于半个屏（实测占整屏 51%）。
     小档 = 15rem(240px)：标题 + 两行 + 「还有 N 条」那行 + 添加框，实测占整屏 28%。
     ⚠️ 取 15rem 不是拍的：12.5rem 那版把「还有 N 条 · 点卡片右上「大」看全部」切成半行了
        （自测里现在有一条"量最后一行的下沿有没有超出卡片"的断言，就是逮这个的）。
     ⚠️ 仍然走"定档"这一套（**不是随内容长**），理由见上面那段：版式稳定比省那几像素重要。
     ⚠️ 选择**只记在这台设备上**（localStorage，dashboard.js 的 bzr_card_sm_todos）——
        跟"手机改了不影响电脑"的既有口径一致（他自己就是手机要小、电脑可以留着大）。 */
  --card-h-sm: 15rem;
}
.today-card.h1 { height: var(--card-h1); }
.today-card.h2 { height: var(--card-h2); }
.today-card.h3 { height: var(--card-h3); }
/* ⚠️ 放在三条高度规则**后面**（同优先级、后者胜）—— 别挪到前面去 */
.today-card.is-sm { height: var(--card-h-sm); }
/* overflow 收在卡内 —— 不写的话滚动内容会从圆角处切出来变直角 */
.today-card.h1, .today-card.h2, .today-card.h3 { overflow: hidden; }
.today-card.is-sm { overflow: hidden; }
/* 卡里「会变长的那块」自己撑满剩余高度。矮卡多出来的余量落在这里，
   读起来是留白（列表本来就该有呼吸感），不是卡片底下的一块死白。
   ⚠️ 必须 min-height: 0 —— flex 子项默认 min-height: auto，不加它 overflow 不生效。 */
.today-card.h1 > .card-fill,
.today-card.h2 > .card-fill,
.today-card.h3 > .card-fill,
.today-card.is-sm > .card-fill { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
/* 卡片标题栏右边那个「大 / 小」小按钮（v0.15.49）—— 视觉上跟 .today-more 那一档走 */
.today-size {
  font-size: var(--vibe-fs-xs); color: var(--vibe-muted-foreground);
  background: none; border: 1px solid var(--vibe-border); border-radius: var(--vibe-radius-2xs);
  padding: 0.05rem 0.4rem; cursor: pointer; white-space: nowrap;
}
.today-size:hover { color: var(--vibe-brand-600); border-color: var(--vibe-brand-600); }
.today-size:focus-visible { outline: 2px solid var(--vibe-brand-600); outline-offset: 2px; }

/* ===== 课堂时钟（v0.10.25 建 · v0.10.28 加秒）===== */
.ck-time {
  /* flex + baseline：秒数跟大时间**共基线**，不会各站各的 */
  display: flex; align-items: baseline; gap: 0.12rem;
  font-size: 2.25rem; font-weight: 700; line-height: 1.1; letter-spacing: -0.02em;
  color: var(--vibe-foreground); font-variant-numeric: tabular-nums;
}
/* 秒：小一号 + 弱一档 —— 它是「时钟在走」的证据，不该抢大时间的注意力。
   两段都开了 tabular-nums（等宽数字），所以秒数跳的时候宽度不变、不会左右抖。 */
.ck-sec {
  font-size: 0.56em; font-weight: 600; letter-spacing: 0;
  color: var(--vibe-muted-foreground);
}
.ck-state {
  margin-top: 0.5rem; display: flex; flex-direction: column; gap: 0.2rem;
  font-size: var(--vibe-fs-sm); color: var(--vibe-muted-foreground);
}
.ck-state b { font-weight: 600; color: var(--vibe-foreground); }
.ck-state.is-now b { color: var(--vibe-primary); }
.ck-label { font-style: normal; margin-left: 0.35rem; font-size: var(--vibe-fs-xs); color: var(--vibe-muted-foreground); }
.ck-left { font-size: var(--vibe-fs-xs); }

/* ===== 今天谁过生日（v0.10.25）===== */
.bd-today { display: flex; align-items: baseline; gap: 0.45rem; padding: 0.1rem 0; }
.bd-tag {
  flex-shrink: 0; padding: 0.05rem 0.35rem; border-radius: 999px;
  font-size: var(--vibe-fs-2xs); font-weight: 600;
  background: var(--vibe-primary); color: var(--vibe-primary-foreground);
}
.bd-name { font-size: var(--vibe-fs-md); font-weight: 600; color: var(--vibe-foreground); }
.bd-age { font-size: var(--vibe-fs-xs); color: var(--vibe-muted-foreground); }
.bd-none { font-size: var(--vibe-fs-sm); color: var(--vibe-muted-foreground); }
.bd-next { margin-top: 0.2rem; font-size: var(--vibe-fs-sm); color: var(--vibe-muted-foreground); }
.bd-next b { color: var(--vibe-foreground); }
.bd-hd { margin-top: 0.7rem; font-size: var(--vibe-fs-xs); color: var(--vibe-muted-foreground); letter-spacing: 0.02em; }
.bd-row { display: flex; align-items: baseline; gap: 0.5rem; padding: 0.15rem 0; font-size: var(--vibe-fs-sm); }
.bd-d { flex-shrink: 0; font-size: var(--vibe-fs-xs); color: var(--vibe-muted-foreground); font-variant-numeric: tabular-nums; }
/* 姓名可能很长：让它省略号，别把「几天后」挤掉 */
.bd-n { flex: 1; min-width: 0; color: var(--vibe-foreground); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bd-m { flex-shrink: 0; font-size: var(--vibe-fs-xs); color: var(--vibe-muted-foreground); }

/* ===== 学期进度（v0.10.25）===== */
/* ★ v0.15.77：那个大百分比也改用**品牌色**（用户 2026-09-23：「学期进度也改成品牌色」）——
   它和下面那根进度条是**同一份数据**，一个橙、一个近黑，看着像两个来源。
   ⚠️ 页脚那行小字（共 N 周 / 还剩 N 天）**保持中性**：那是次要说明、不是主角，别一起改橙。 */
.tm-big { font-size: 1.5rem; font-weight: 700; letter-spacing: -0.01em; color: var(--vibe-primary); }
.tm-bar {
  margin: 0.6rem 0 0.45rem; height: 0.5rem; border-radius: 999px; overflow: hidden;
  background: color-mix(in srgb, var(--vibe-border) 60%, transparent);
}
/* ★ v0.15.61 颜色减法：进度条是**数据**，不是"当前/主操作" ⇒ 改中性（下面两处同理）
   ★ v0.15.75：中性改过头了 ——「数据」不等于「没有颜色」，灰条看着像坏了。
     改用图表专用色（用户反馈「学期进度也太丑了」）。 */
.tm-bar > span { display: block; height: 100%; border-radius: 999px; background: var(--vibe-chart-bar); }
.tm-foot { display: flex; justify-content: space-between; gap: 0.5rem; font-size: var(--vibe-fs-xs); color: var(--vibe-muted-foreground); }

/* ===== 本周积分榜（v0.10.28）===== */
.pt-row { display: flex; align-items: center; gap: 0.5rem; padding: 0.2rem 0; font-size: var(--vibe-fs-sm); }
.pt-rank {
  flex-shrink: 0; width: 1.3rem; height: 1.3rem; border-radius: 999px;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: var(--vibe-fs-2xs); font-weight: 700;
  background: var(--vibe-muted); color: var(--vibe-muted-foreground);
}
/* 前三名给点分量：金 / 银 / 铜。
   ⚠️ **故意不取调色板令牌** —— 奖牌色是约定俗成的语义色，跟着品牌色走反而看不懂。
      三个底色都是浅色，所以文字固定用深色，深色主题下底浅字深，照样读得清。 */
.pt-rank.r1 { background: #f2c14e; color: #4a3a12; }
.pt-rank.r2 { background: #ccd2d9; color: #333a41; }
.pt-rank.r3 { background: #dda87c; color: #4a3222; }
/* 姓名可能很长：省略号，别把分数挤掉 */
.pt-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--vibe-foreground); }
.pt-total { flex-shrink: 0; font-weight: 700; font-variant-numeric: tabular-nums; color: var(--vibe-brand-600); }
/* 扣分用灰的：品牌色是「奖励」的语义，负数还染成那个颜色会读反 */
.pt-total.is-minus { color: var(--vibe-muted-foreground); }

/* ★ v0.18.80：这里的 `.fd-bal` / `.fd-foot`（班费余额那张卡的两个样式）**已随班费整条下架**。
   用户 2026-09-30：「班费这个直接删掉吧，我不要班费了……这个不适合在我们这展示」。
   ⚠️ 只删了这两条样式（`check:all` 会验"用了却没定义的类名"，所以删得干净是安全的）；
      服务端 `class_funds` 里那份**数据没删**，要恢复就是把 nav.mjs 那一行加回来 + 放开那三个接口。 */

/* ===== 常用入口（v0.10.25）=====
   纯静态快捷方式（HTML 里写死），不需要接口。4 列 × 2 行。 */
.sc-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0.4rem; }
.sc-item {
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 0.3rem;
  padding: 0.55rem 0.25rem; border-radius: var(--vibe-radius-xs);
  border: 1px solid color-mix(in srgb, var(--vibe-border) 60%, transparent);
  background: var(--vibe-background);
  color: var(--vibe-muted-foreground); text-decoration: none;
  font-size: var(--vibe-fs-xs); line-height: 1;
  transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease;
}
.sc-item svg { width: 1rem; height: 1rem; }
.sc-item:hover { color: var(--vibe-brand-600); border-color: var(--vibe-brand-300); background: var(--vibe-card); }
/* 卡窄的时候（1024 下三列只有 235px）4 列会太挤，退成 3 列 */
@media (max-width: 1279px) {
  .sc-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* ===== 今日一签（v0.11.92）=====
   抽签规则与签文表都在 assets/js/one-sign.js（含"有签等、但最低只到中签"这条的由来）。
   这里只管长相：一张「签纸」+ 未抽时的签筒。
   ⚠️ 底色/描边一律用现成令牌（--vibe-card / --vibe-line-soft / --vibe-muted 那一组），
      不要现算 color-mix —— 华为闪应用容器的内核会整条丢弃（见 --vibe-card 上的说明）。
   ⚠️ 当文字的颜色用 -ink 令牌（--vibe-primary-ink）：亮色 #f7663c 当文字不达标
      （v0.11.87 的对比度修复，白卡上只有 3.01:1），-ink 版 4.65:1 ✓ 且深色主题自动翻转。 */
.one-sign-body { display: flex; flex-direction: column; gap: 0.5rem; }

/* 未抽：虚线签筒（整块可点） */
.os-draw {
  flex: 1 1 auto; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 0.5rem;
  min-height: 7.5rem; padding: 1rem;
  border: 1px dashed var(--vibe-border); border-radius: var(--vibe-radius-sm);
  background: var(--vibe-background); color: var(--vibe-muted-foreground);
  font: inherit; font-size: var(--vibe-fs-sm); cursor: pointer;
  transition: border-color var(--vibe-dur-fast) var(--vibe-ease), color var(--vibe-dur-fast) var(--vibe-ease);
}
.os-draw:hover { border-color: var(--vibe-brand-300); color: var(--vibe-brand-800); }
.os-draw:focus-visible { outline: 2px solid var(--vibe-primary); outline-offset: 2px; }
/* 那个「签」字：一小块木牌 */
.os-draw-tube {
  width: 1.9rem; height: 2.6rem; display: flex; align-items: center; justify-content: center;
  border: 1px solid var(--vibe-brand-line); border-radius: 3px;
  background: var(--vibe-card); color: var(--vibe-primary-ink);
  font-size: 0.95rem; font-weight: 700; line-height: 1;
}

/* 签纸 */
.os-stage { perspective: 700px; }
.os-slip {
  display: flex; flex-direction: column; gap: 0.15rem;
  padding: 0.7rem 0.8rem 0.8rem;
  border: 1px solid var(--vibe-line-soft); border-radius: var(--vibe-radius-sm);
  background: var(--vibe-card);
}
/* 上上签：描边换成暖色，一眼看得出来 */
.os-slip.is-ss { border-color: var(--vibe-brand-line); }
.os-top { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; }
.os-lv {
  padding: 0.05rem 0.4rem; border-radius: var(--vibe-radius-full);
  border: 1px solid var(--vibe-line-soft);
  font-size: var(--vibe-fs-xs); font-weight: 700; letter-spacing: 0.04em;
  color: var(--vibe-muted-foreground);
}
.os-slip.is-ss .os-lv { border-color: var(--vibe-brand-line); color: var(--vibe-primary-ink); }
.os-no { font-size: var(--vibe-fs-xs); color: var(--vibe-muted-foreground); }
/* 原句：签文用衬线（宋/楷），跟纸感的界面是一路的。
   ★ v0.18.17：字体栈提到 `--vibe-font-serif-cjk` 令牌（原来这里硬写一份），
   渲染结果一字未变；「别扩散到别处」这条规矩同步放宽为「别扩散到 <20px 的地方」。 */
.os-line {
  margin: 0.15rem 0 0; font-size: var(--vibe-fs-lg); font-weight: 600; line-height: 1.65; letter-spacing: 0.02em;
  font-family: var(--vibe-font-serif-cjk);
  color: var(--vibe-foreground);
}
.os-src { margin: 0; font-size: var(--vibe-fs-xs); color: var(--vibe-muted-foreground); }
.os-gloss { margin: 0.3rem 0 0; font-size: var(--vibe-fs-sm); line-height: 1.7; color: var(--vibe-muted-foreground); }
.os-act { margin: 0.2rem 0 0; font-size: var(--vibe-fs-sm); line-height: 1.7; color: var(--vibe-foreground); }
.os-act b { margin-right: 0.35rem; font-weight: 600; color: var(--vibe-primary-ink); }

.os-foot { display: flex; align-items: center; gap: 0.5rem; }
.os-again {
  padding: 0.3rem 0.7rem; border-radius: var(--vibe-radius-full);
  border: 1px solid var(--vibe-border); background: var(--vibe-card);
  color: var(--vibe-primary-ink); font: inherit; font-size: var(--vibe-fs-sm); font-weight: 500;
  cursor: pointer; transition: background-color var(--vibe-dur-fast) var(--vibe-ease);
}
.os-again:hover { background: var(--vibe-muted); }
.os-again:focus-visible { outline: 2px solid var(--vibe-primary); outline-offset: 2px; }
.os-count { font-size: var(--vibe-fs-xs); color: var(--vibe-muted-foreground); }
/* 本轮进度那一行（★ v0.18.12「抽过的不再抽」）—— 比 os-count 还轻，压在整张卡的最下面 */
.os-round { margin: 0; font-size: var(--vibe-fs-xs); line-height: 1.6; color: var(--vibe-muted-foreground); }

/* 翻牌：只在**这次刚抽出来**时放（切页回来看旧签不重播） */
@keyframes bzr-os-flip {
  from { transform: rotateY(86deg); opacity: 0; }
  to   { transform: rotateY(0deg);  opacity: 1; }
}
.os-slip.is-in { transform-origin: left center; animation: bzr-os-flip 380ms var(--vibe-ease-out); }
@media (prefers-reduced-motion: reduce) { .os-slip.is-in { animation: none; } }

/* ===== 首页编辑模式（v0.10.26 建 · v0.10.27 入口搬到卡片区底部的粘底条）=====
   口径：**拖动不用进模式（可逆、误触代价≈0），「移除 / 添加」要进模式**（破坏性操作）。
   以前收一张卡要「离开今日页 → 进设置 → 找到那一行 → 取消勾选」；
   现在入口就在卡片区底边（`.home-edit-bar`，sticky 常驻），点一下就地展开成工具条。 */
/* 平时只露这一个入口（右对齐）。它是「一个 pill」，不是一条 bar ——
   不编辑的时候不该占一块版面。 */
.home-edit-open {
  display: inline-flex; align-items: center; gap: 0.35rem;
  padding: 0.35rem 0.75rem; border-radius: var(--vibe-radius-full);
  border: 1px solid var(--vibe-border); background: var(--vibe-card);
  color: var(--vibe-muted-foreground); font-size: var(--vibe-fs-xs); font-weight: 500;
  cursor: pointer; white-space: nowrap;
  box-shadow: var(--vibe-shadow-sm);
  transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease;
}
.home-edit-open:hover { color: var(--vibe-brand-600); border-color: var(--vibe-brand-300); }
.home-edit-open svg { width: 0.875rem; height: 0.875rem; }
body.home-editing .home-edit-open { display: none; }   /* 编辑中：入口换成下面的面板，不再露 pill */

/* 展开后的工具条（进编辑模式时**就地**替换那个 pill） */
.home-edit-panel[hidden] { display: none !important; }
.home-edit-panel { flex: 1; min-width: 0; }
.home-edit-panel .home-edit-hint { margin: 0 0 0.65rem; }

/* 编辑模式下的卡片描一圈虚线：不然用户不知道自己在模式里，会以为样式坏了 */
body.home-editing .today-card {
  outline: 1.5px dashed color-mix(in srgb, var(--vibe-brand-500) 45%, transparent);
  outline-offset: 2px;
}
/* ⚠️ 编辑模式下要把「xx ›」链接收起来：点一下就跳走了，改到一半的界面没了。
     第 75 条里拖拽也是同一个理由要放行这些链接 —— 那是在正常模式，这里是编辑模式。 */
body.home-editing .today-more { display: none !important; }
/* ⚠️ v0.15.49：编辑模式下这个「大/小」也一起藏 —— 它管的是"看多大"，不是"放在哪"，
   编辑模式本来就有一堆抓手和 ✕，再多一个小按钮只会让人分不清哪个是哪个。 */
body.home-editing .today-size { display: none !important; }
/* 空列 / 第三列在编辑模式都要露出来，否则长在上面的卡没法管理。
   ⚠️ 第三列在 <1024 本来是整列藏掉的（信息跟主视觉重复），但它的「常用入口 / 学期进度」
      在手机上没有替代品 —— 至少编辑时要能看见、能拖出来。 */
body.home-editing .today-col.is-off,
body.home-editing .today-col-3 {
  display: grid !important;
  min-height: 4rem;
  border: 1.5px dashed color-mix(in srgb, var(--vibe-brand-500) 30%, transparent);
  border-radius: var(--vibe-radius-md);
}

/* 卡片上的「移除」/ 上下移：平时不显示，编辑模式才出来 */
.card-remove, .card-move {
  display: none; align-items: center; justify-content: center; flex-shrink: 0;
  border: 1px solid var(--vibe-border); border-radius: var(--vibe-radius-2xs);
  background: var(--vibe-card); color: var(--vibe-muted-foreground);
  cursor: pointer; line-height: 1; font-size: var(--vibe-fs-base);
  transition: color 0.15s ease, border-color 0.15s ease;
}
body.home-editing .card-remove { display: inline-flex; }
/* ⚠️ 命中区不能等于图标（AGENTS 第 42 条）：1.5rem ≈ 24px */
.card-remove { width: 1.5rem; height: 1.5rem; }
.card-remove:hover { color: var(--vibe-destructive, #c0392b); border-color: color-mix(in srgb, currentColor 40%, transparent); }
.card-move { width: 1.9rem; height: 1.9rem; }
.card-move:hover:not(:disabled) { color: var(--vibe-brand-600); border-color: var(--vibe-brand-300); }
.card-move:disabled { opacity: 0.3; cursor: not-allowed; }
/* ↑↓ 只在「拖不动」的窄屏放出来 —— 宽屏能直接拖，再排两个箭头是噪音。
   ⚠️ 这个 1024 必须和 dashboard.js 里那道判断一致（同 v0.10.24 第 80 条那个坑）。 */
@media (max-width: 1023px) {
  body.home-editing .card-move { display: inline-flex; }
}

/* ===== 编辑首页 · 粘底条（v0.10.27）=====
   平时：右端一个「✎ 编辑首页」pill。编辑中：**就地展开**成工具条（含「完成」）。
   ⚠️ 用 sticky 而不是普通地放在流末尾 —— 否则要滚到最底下才看得见，
      而「找不到」这件事用户已经抱怨过两次。
   ⚠️ 负 margin 把父级 `.p-6` 的 24px 抵消掉，做成通栏；**下面的 padding 也一起抵掉**，
      不然它贴住滚动容器底边之后、滚到最底会「跳一下」（最后 24px 是父级的 padding）。
   ⚠️ 底色必须**不透明**：卡片是从它后面滚过去的。 */
.home-edit-bar {
  position: sticky; bottom: 0; z-index: 30;
  display: flex; justify-content: flex-end;
  margin: 1rem -1.5rem -1.5rem;
  padding: 1.2rem 1.5rem 0.9rem;
  /* 平时用「渐隐」而不是硬边：卡片滚到这儿淡出，不会切出一条线 */
  background: linear-gradient(to bottom, transparent, var(--vibe-background) 46%);
}
body.home-editing .home-edit-bar {
  padding-top: 0.8rem;
  background: var(--vibe-background);
  border-top: 1px solid color-mix(in srgb, var(--vibe-brand-500) 40%, transparent);
  box-shadow: 0 -8px 20px -16px rgba(0, 0, 0, 0.25);
}
.home-edit-hint { margin: 0 0 0.7rem; font-size: var(--vibe-fs-xs); color: var(--vibe-muted-foreground); line-height: 1.6; }
.home-edit-actions { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.home-add-tile {
  display: inline-flex; align-items: center; gap: 0.4rem;
  padding: 0.5rem 0.9rem; border-radius: var(--vibe-radius-xs);
  border: 1px dashed color-mix(in srgb, var(--vibe-brand-500) 55%, transparent);
  background: var(--vibe-card); color: var(--vibe-brand-600);
  font-size: var(--vibe-fs-sm); font-weight: 600; cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease;
}
.home-add-tile svg { width: 1rem; height: 1rem; }
.home-add-tile:hover { background: var(--vibe-muted); border-style: solid; }

/* 「添加组件」面板里的每一行 */
.aw-row {
  display: flex; align-items: center; gap: 0.6rem;
  padding: 0.5rem 0.6rem; border-radius: var(--vibe-radius-2xs);
  border: 1px solid var(--vibe-border); background: var(--vibe-background);
}
.aw-main { flex: 1; min-width: 0; }
.aw-name { font-size: var(--vibe-fs-base); font-weight: 500; color: var(--vibe-foreground); }
.aw-desc { font-size: var(--vibe-fs-xs); color: var(--vibe-muted-foreground); margin-top: 0.1rem; }
.aw-add {
  flex-shrink: 0; padding: 0.3rem 0.7rem; border-radius: var(--vibe-radius-2xs);
  border: 1px solid var(--vibe-brand-300); background: var(--vibe-card);
  color: var(--vibe-brand-600); font-size: var(--vibe-fs-sm); font-weight: 500; cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
}
.aw-add:hover { background: var(--vibe-primary); border-color: var(--vibe-primary); color: var(--vibe-primary-foreground); }
.aw-empty { padding: 0.6rem 0; font-size: var(--vibe-fs-sm); color: var(--vibe-muted-foreground); }

/* 超宽屏给整页一个上限，否则三列会被拉到两千多像素、每行都读得很累。
   v0.10.17：上限从 1560 提到 1680（再给 ≥2048 一档 1880）——
   实测 2560 宽的屏上 1560 只占 67%，两侧空出很大一块，看着像没铺满。
   1280~1599 之间完全不变。 */
@media (min-width: 1600px) {
  .today-stack { max-width: 1680px; margin-inline: auto; }
}
@media (min-width: 2048px) {
  .today-stack { max-width: 1880px; }
}

/* ===== 卡片分主次（v0.10.3 建 · v0.10.17 重做为一套「三层纸」）=====
   原来两级卡片的差别只在「有没有投影」，底色几乎一样（62% 混完跟背景只差一点点），
   所以摆在深一档的新底色上，次卡看着像「没画完」。现在明确成三层：
     背景（桌面）→ 次卡（凹进去的浅色纸）→ 主卡（最白的纸 + 真实投影）
   三层的明度各自拉开，扫一眼就有前后关系，不靠描边。
   .now-hero 是最高一级，不参与这套。 */
.today-card {
  background: var(--vibe-card);
  /* ⚠️ 描边用预混色变量，**不要改回 color-mix()** —— 华为闪应用/快应用容器的内核不支持它，
     整条声明会被丢弃，卡片就变成「没底色、没边框」的一堆文字（v0.11.19 修的就是这个）。 */
  border: 1px solid var(--vibe-line-soft);
  border-radius: var(--vibe-radius);
  box-shadow: var(--vibe-shadow-sm);
  padding: 1rem;                  /* 桌面刻度 16，四边相等 */
  display: flex; flex-direction: column;
}
/* 主卡（v0.10.28 废止）----------
   原来「今日课表」用 .is-primary：最白的纸 + 真实投影。那是 v0.10.17 定的 ——
   当时卡片只有 5 张、课表是页面上唯一的主角。
   现在卡片涨到 9 张，**「唯一主角」这个前提已经不成立了**，它只会显得
   「莫名其妙比别人亮一格」（用户原话：「课表的组件颜色感觉和其他的不太一样」）。
   页面的主角交给顶部那块「此刻」主视觉（它本来就是最醒目的），9 张工具卡统一用次卡。
   ⚠️ 规则先留着（不改 HTML 的兜底），但**不要再用到新卡片上**。 */
.today-card.is-primary {
  border-color: transparent;
  box-shadow: var(--vibe-shadow-md);
}
/* 次卡：凹进去 —— 往背景色方向压，比主卡暗、比桌面亮，形成中间那一层 */
.today-card.is-secondary {
  background: var(--vibe-card-sub);      /* 预混色，别改回 color-mix()（见 .today-card 上的说明） */
  border-color: var(--vibe-line-faint);
  box-shadow: none;
}
/* 卡片标题的图标底座（v0.10.17）：图标本身是 brand-600（内联 color），
   这里用 currentColor 反推出一个同色系的浅底，等于给每张卡钉了一个色彩锚点 ——
   原来五张卡的标题都是一排灰字，整页没有视觉落点。 */
.today-head h3 svg {
  width: 1rem; height: 1rem; padding: 0.3rem; box-sizing: content-box;
  border-radius: 9px;
  background: color-mix(in srgb, currentColor 13%, transparent);
  flex-shrink: 0;
}
.today-head {
  /* ⚠️ 不能写 space-between。标题栏的子元素个数是会变的：
     拖拽抓手是 JS 动态插进来的，而最后那个「课表 › / 设置 ›」不是每张卡都有。
       3 个子元素（抓手 + 标题 + 链接）→ space-between 把标题挤到中间；
       **2 个**（抓手 + 标题，如「待办」「课堂时钟」「常用入口」）→ 标题被甩到最右边。
     v0.10.22 的待办卡就是这么悄悄歪的，v0.10.23 插了抓手之后才显形。
     改成「靠左排 + 标题吃掉剩余空间」，链接自然被推到右边 —— 与子元素个数无关。 */
  display: flex; align-items: center; justify-content: flex-start;
  gap: 0.5rem; padding-bottom: 0.5rem; margin-bottom: 0.5rem;
  border-bottom: 1px solid color-mix(in srgb, var(--vibe-border) 50%, transparent);
}
.today-head h3 {
  margin: 0 auto 0 0;                /* 右边留 auto：把后面的「xx ›」顶到最右 */
  font-size: var(--vibe-fs-md); font-weight: 600; color: var(--vibe-foreground);
  display: flex; align-items: center; gap: 0.5rem; min-width: 0;
}
.today-head-sub { font-size: var(--vibe-fs-xs); font-weight: 400; color: var(--vibe-muted-foreground); }
.today-more { font-size: var(--vibe-fs-xs); color: var(--vibe-muted-foreground); text-decoration: none; white-space: nowrap; }
.today-more:hover { color: var(--vibe-brand-600); }
/* 一行式空态（v0.9.18 由 .today-empty 提升为全站通用）：
   用于小栏目 / 小格子 / 弹窗内的列表 —— 只占一行：图标 + 一句话 +（可选）链接。
   主内容区请改用 .empty-block（大空态，带行动按钮）。 */
.empty-line {
  margin: 0; font-size: var(--vibe-fs-base); color: var(--vibe-muted-foreground); line-height: 1.7;
  display: flex; align-items: center; gap: 0.45rem;
}
.empty-line i, .empty-line svg { flex-shrink: 0; color: var(--vibe-border); }
/* ★ v0.15.61 颜色减法：空态里的文字链接 → 中性 + hover 上色（跟 .today-term-set 一个口径） */
.empty-line a { color: var(--vibe-foreground); text-decoration: underline; text-underline-offset: 2px;
  text-decoration-color: color-mix(in srgb, var(--vibe-border) 100%, transparent); }
.empty-line a:hover { color: var(--vibe-brand-600); }
/* 文字整块（见 dashboard.js `emptyHtml` 的注释）：
   `.empty-line` 是 flex，不把文字包成**一项**的话，里面的每个文本节点和内联标签
   都会变成独立 flex 项，句子会被横着拆成几列。 */
.empty-line-text { min-width: 0; }
/* 卡片里当主内容用时给点上下留白（如后台列表的「暂无数据」） */
.empty-line.is-pad { padding: 1.25rem 0; justify-content: center; }

/* 页头两行标题：第一行「9月12日 星期六」+ 班级筹码，第二行「秋季学期 · 第 3 周」 */
.today-title {
  margin: 0; font-size: var(--vibe-fs-xl); font-weight: 700; line-height: 1.25;
  color: var(--vibe-foreground); letter-spacing: -0.01em;
}
.today-subtitle { margin: 0.1rem 0 0; font-size: var(--vibe-fs-sm); color: var(--vibe-muted-foreground); line-height: 1.5; }

/* ===== 「此刻」主视觉（v0.10.1 新增，整宽）=====
   整页最该被一眼看到的东西：我正在上什么 / 下一节是什么。
   主色只在这里和课表的「正在上」行出现。 */
.now-hero {
  position: relative;
  overflow: hidden;
  border-radius: var(--vibe-radius);
  border: 1px solid var(--vibe-brand-line);   /* 预混色，别改回 color-mix()（说明见 .today-card 那段） */
  /* 光晕浓度 80% → 100%（v0.11.17）：用户觉得卡片右上角那层暖光「太淡了」，主视觉这一档开满。
     ⚠️ 这是**全局**的 —— 「正在上」「我的下一节」以及没设「我教的科目」时的兜底版本共用这条规则，
        改它等于所有主视觉一起变亮（有意为之：它们本来就是同一个主视觉，不该有的亮有的暗）。 */
  background:
    radial-gradient(120% 170% at 100% 0%, var(--vibe-brand-100) 0%, transparent 62%),
    var(--vibe-card);
  padding: 1.15rem 1.25rem;       /* 比卡片略厚一点 —— 整宽横条太薄会显得单薄 */
  box-shadow: var(--vibe-shadow-sm);
  /* v0.10.17：主视觉从「一条只有一行字的空横条」改成横向布局 ——
     左＝此刻/下一节，右＝今天的四个数字（.now-stats 由 dashboard.js 注入）。
     两个子元素里 now-stats 是可选的：没有它的时候 now-hero-left 照样占满整宽。 */
  display: flex; align-items: center; gap: 1.25rem;
}
.now-hero-left { flex: 1 1 auto; min-width: 0; }

/* ---- 「今天有考勤」薄提示条（本批）----
   位置：主视觉 .now-hero 下面、卡片区上面。只回答「今天有例会」这件事：
   活动名 + 已点进度 + 「去点名」入口。attendanceToday 为 null 时不显示。
   ⚠️ 不写具体时间 —— attendance_events 只有起止日期、没有时间字段，编一个时间就是假信息。 */
.att-today-bar {
  display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap;
  padding: 0.55rem 0.85rem;
  border-radius: var(--vibe-radius-sm);
  border: 1px solid color-mix(in srgb, var(--vibe-brand-300) 55%, var(--vibe-border));
  background: color-mix(in srgb, var(--vibe-brand-500) 8%, var(--vibe-card));
  font-size: var(--vibe-fs-sm); color: var(--vibe-foreground);
}
.att-today-bar[hidden] { display: none; }
.att-today-bar .att-today-ic { display: inline-flex; flex-shrink: 0; color: var(--vibe-brand-600); }
.att-today-bar .att-today-ic i, .att-today-bar .att-today-ic svg { width: 1rem; height: 1rem; }
.att-today-bar .att-today-txt { min-width: 0; }
.att-today-bar .att-today-txt b { font-weight: var(--vibe-fw-medium); }
.att-today-bar .sp { flex: 1 1 auto; }
.att-today-bar .att-today-go {
  display: inline-flex; align-items: center; gap: 0.25rem; flex-shrink: 0;
  padding: 0.3rem 0.7rem; border-radius: var(--vibe-radius-full);
  background: var(--vibe-primary); color: var(--vibe-primary-foreground);
  font-size: var(--vibe-fs-sm); font-weight: var(--vibe-fw-medium); text-decoration: none;
}
.att-today-bar .att-today-go:hover { background: var(--vibe-brand-600); }
/* 全部点完：换成「已完成」的绿调（同一套令牌，深浅主题都成立） */
.att-today-bar.is-done {
  border-color: color-mix(in srgb, var(--vibe-success) 55%, var(--vibe-border));
  background: color-mix(in srgb, var(--vibe-success) 10%, var(--vibe-card));
}
.att-today-bar.is-done .att-today-ic { color: var(--vibe-success); }
.att-today-bar.is-done .att-today-go {
  background: transparent; color: var(--vibe-foreground); border: 1px solid var(--vibe-border);
}
@media (max-width: 767px) {
  /* 手机上一行放不下就折两行（去掉撑开的空隙），不横向溢出 */
  .att-today-bar { gap: 0.4rem; padding: 0.5rem 0.7rem; }
  .att-today-bar .sp { display: none; }
  .att-today-bar .att-today-txt { flex: 1 1 100%; }
}
/* 认领课表引导条（v0.10.40）：直接复用上面 .att-today-bar 的壳，只多一个「不再提示」的 × */
.att-today-bar .att-today-x {
  display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0;
  width: 1.5rem; height: 1.5rem; padding: 0; border: none; background: transparent;
  color: var(--vibe-muted-foreground); cursor: pointer; border-radius: var(--vibe-radius-2xs);
}
.att-today-bar .att-today-x:hover { background: var(--vibe-muted); color: var(--vibe-foreground); }
.att-today-bar .att-today-x svg { width: 0.9rem; height: 0.9rem; }

/* ---- 主视觉顶部的问候行（v0.10.18）----
   参照「高校教师工作台」那张图的做法：横幅开头先对老师说一句话。
   我们的主体信息（课名）必须比问候大，所以问候只占一行小字，压在 kicker 上面。 */
.now-greet {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: 0.15rem 0.4rem;
  margin-bottom: 0.35rem;
}
.now-greet-hi {
  font-size: var(--vibe-fs-md); font-weight: 700;
  color: var(--vibe-foreground); letter-spacing: -0.01em;
}
.now-greet-tip { font-size: var(--vibe-fs-sm); color: var(--vibe-muted-foreground); }
/* 「就是今天」「还有 3 天」这类要紧的事，用主色提一下，别和寒暄混在一起 */
.now-greet-tip.is-urgent { color: var(--vibe-brand-700); font-weight: 600; }

/* ---- 主视觉右侧的四个数字（v0.10.17）----
   整宽横条上最怕「左边一行字、右边四个数字、中间一大片空」，
   所以让 .now-stats 自己长（flex-grow）并靠右停住（margin-left:auto），
   四个格子在里面均分 —— 空出来的部分就被四个格子摊掉了，不会聚成一块。 */
.now-stats {
  flex: 1 1 auto; max-width: 68rem; margin-left: auto;
  display: flex; align-items: stretch; justify-content: space-evenly;
}
.now-stat {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 0.05rem; flex: 1 1 0; min-width: 5rem; padding: 0 0.6rem;
  border-left: 1px dashed color-mix(in srgb, var(--vibe-border) 90%, transparent);
}
.now-stat:first-child { border-left: 0; }
.now-stat-icon {
  display: flex; align-items: center; justify-content: center;
  width: 1.75rem; height: 1.75rem; border-radius: 999px; margin-bottom: 0.2rem;
  background: var(--vibe-muted); color: var(--vibe-muted-foreground);
}
.now-stat-icon svg { width: 1rem; height: 1rem; }
/* 数字保持墨色 —— 四个彩色数字会吵；颜色只出现在图标圈上。
   ★ v0.18.17 **试过给这一格也挂中文衬线，最后撤掉了**：
     楷/宋没有真 bold 字面，`font-weight: 700` 是**合成粗体**，实际渲染出来比黑体轻一档
     （走查里留了张并排样本 `_tmp_v1817_statnum.png`，左楷右黑，一看就分得出来）。
     而这一格的用意本来就是「一眼看清的读数」，不是装饰 ⇒ 保持黑体。
     页头那个 `.today-md` 不一样：它是「几号」的日期条，文气正是要的效果，那边留着。 */
.now-stat-num {
  font-size: 1.375rem; font-weight: 700; line-height: 1.05;
  color: var(--vibe-foreground); font-variant-numeric: tabular-nums;
}
.now-stat-num small {
  font-size: 0.68em; font-weight: 600; margin-left: 0.1rem;
  color: var(--vibe-muted-foreground);
}
.now-stat-label {
  font-size: var(--vibe-fs-2xs); color: var(--vibe-muted-foreground);
  white-space: nowrap; letter-spacing: 0.01em;
}
/* 第四行小字（v0.10.19）：数字之外的补充信息（第一节几点 / 谁值日 / 哪场考试）。
   比 label 再淡一档，避免和 label 抢；放不下就省略号，绝不换行撑高整格。 */
.now-stat-sub {
  font-size: var(--vibe-fs-2xs);
  color: color-mix(in srgb, var(--vibe-muted-foreground) 72%, transparent);
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.now-stat-sub.is-urgent { color: var(--vibe-brand-600); font-weight: 600; }
/* 四个各自的色：只染图标圈，一深一浅成对 */
.now-stat.is-lesson .now-stat-icon { background: #e7eefb; color: #2f6fd0; }
.now-stat.is-todo   .now-stat-icon { background: var(--vibe-brand-100); color: var(--vibe-brand-600); }
.now-stat.is-duty   .now-stat-icon { background: #e2f4ea; color: #1f9d63; }
.now-stat.is-exam   .now-stat-icon { background: #fbf0d5; color: #b07d0a; }
/* 窄屏整块收掉（v0.10.19 建 · v0.10.20 断点从 767 提到 1023）----------
   ⚠️ 原来写的是 ≤767，理由是「手机上塞不下」。但 768~1023 这一段更糟：
      侧栏在 ≥768 已经是桌面宽度（240px），内容区只剩 500 出头，
      主视觉要同时装「我的课筹码 + 四个数字」→ 全被压成竖排换行
      （实测 768px：课名、时间、每个筹码都竖着断行，看着像坏了）。
      诡异的是**横向溢出量仍然是 0** —— 因为它选择了换行而不是溢出，
      所以「只看溢出」的体检扫不出来，必须看图。
   现在断点跟第三列对齐（都是 ≥1024）：一个断点管三件事 ——
      出现第三列 / 收起主视觉里的我的课筹码 / 显示数字概览。语义统一，也不会再互相挤。 */
@media (max-width: 1023px) {
  .now-stats { display: none; }
}
.now-hero.is-idle {
  /* v0.11.16：空态**不再把光晕抹掉**，只把它调淡。
     v0.11.17：浓度 42% → 55%（用户反馈 42% 太淡，几乎看不见，空态跟有课的差别又丢了）。
     现在两档是：**有课 100% / 课上完了 55%**，差得开、又都还在同一个暖色调里。
     原来写的是 background: var(--vibe-card) —— 等于把暖橘渐变整条干掉，底色又留在
     全站最亮的 #fffefb，而下面 9 张卡都是压暗一档的 .is-secondary（#f7f3eb）：
     一对比，这张「空卡」反而最白最扎眼（用户原话：「课上完了怎么就卡片变白了」）。
     ⚠️ 两条别走回头路：
        (a) 位置（右上角 100% 0%）**不要改**：这张卡一天要切好几次（下一节 / 正在上 / 上完了），
            光晕一搬家页面重心就跟着跳；靠浓淡区分状态比靠位置稳。
        (b) **底色不要拉平成普通卡片**（曾讨论过 `#f7f3eb`）—— 那等于把「主视觉」的另一半身份
            交出去（三层明度：底 → 卡片 → 主视觉），页面会变平；用户比较过三版后选的是这一版。 */
  background:
    radial-gradient(120% 170% at 100% 0%, var(--vibe-brand-wash-55) 0%, transparent 62%),
    var(--vibe-card);
  border-color: var(--vibe-brand-line-soft);
  padding: 0.75rem 1rem 0.85rem;   /* 空态不是重点，压扁一点别占掉一屏 */
}
.now-hero.is-idle .now-hero-subject { font-size: 1.0625rem; }
.now-hero.is-idle .now-hero-main { margin-top: 0.1rem; }
.now-kicker {
  font-size: var(--vibe-fs-2xs); font-weight: 700; letter-spacing: 0.12em;
  /* ★ v0.15.61 颜色减法：这是主视觉上的一行**说明标签**（"我的下一节"），不是按钮也不是提醒。
     原来是 brand-700 橙 —— 大小写标签 + 橙色，看着像可以点。改成中性；
     「今天上课上完了」那种空闲态的弱化版仍然靠下面那条 is-idle 规则。 */
  color: var(--vibe-foreground);
}
.now-hero.is-idle .now-kicker { color: var(--vibe-muted-foreground); }
.now-hero-main { display: flex; align-items: baseline; gap: 0.6rem; flex-wrap: wrap; margin-top: 0.25rem; }
/* 从 1.5rem 提到 1.75rem（v0.10.17）：整宽横条上原来的字号太秀气，
   左边一行小字压不住 1800px 的宽度；放大后左右两端的视觉重量才对得上。 */
.now-hero-subject { font-size: 1.75rem; font-weight: 700; line-height: 1.2; color: var(--vibe-foreground); letter-spacing: -0.01em; }
/* ★ v0.15.61 颜色减法：上课时间（"10:25 – 11:05"）是**数据**，主角是上面的科目名。
   原来它是橙的、比科目还抢眼 ⇒ 改成中性（保留 600 字重和等宽数字的清晰度）。 */
.now-hero-time { font-size: var(--vibe-fs-md); font-weight: 600; color: var(--vibe-foreground); font-variant-numeric: tabular-nums; }
.now-hero-meta { margin-top: 0.5rem; font-size: var(--vibe-fs-sm); color: var(--vibe-muted-foreground); line-height: 1.6; }
.now-hero-meta b { color: var(--vibe-foreground); font-weight: 600; }
.now-hero-next {
  margin-top: 0.55rem; padding-top: 0.55rem;
  border-top: 1px dashed color-mix(in srgb, var(--vibe-brand-200) 70%, transparent);
  font-size: var(--vibe-fs-sm); color: var(--vibe-muted-foreground); line-height: 1.6;
}
.now-hero-next b { color: var(--vibe-brand-700); font-weight: 600; }
/* 当前这节的进度条 ——「还有多久下课」一眼可见 */
.now-hero-progress {
  margin-top: 0.6rem; height: 4px; border-radius: 999px; overflow: hidden;
  /* ★ v0.15.61：槽底也跟着中性 —— 条改成中性之后，浅橙槽 + 灰条会显得像"坏了" */
  background: color-mix(in srgb, var(--vibe-border) 60%, transparent);
}
/* ★ v0.15.61 颜色减法：同上 —— 这一节上到哪了是"数据"
   ★ v0.15.75：同上改成图表专用色（灰度条＝看着像坏了） */
.now-hero-progress > span { display: block; height: 100%; border-radius: 999px; background: var(--vibe-chart-bar); }
.now-hero-caption { margin-top: 0.3rem; font-size: var(--vibe-fs-xs); color: var(--vibe-muted-foreground); }
/* 「今天我的课」横排小卡（主视觉的副信息，比原来竖排列表省一半高度） */
.now-hero-lessons { display: flex; gap: 0.4rem; flex-wrap: wrap; margin-top: 0.35rem; }
.now-chip {
  display: inline-flex; align-items: baseline; gap: 0.32rem;
  padding: 0.22rem 0.55rem; border-radius: 999px;
  border: 1px solid color-mix(in srgb, var(--vibe-border) 90%, transparent);
  background: color-mix(in srgb, var(--vibe-card) 70%, transparent);
  font-size: var(--vibe-fs-xs); color: var(--vibe-muted-foreground);
  font-variant-numeric: tabular-nums;
}
.now-chip b { font-size: var(--vibe-fs-base); font-weight: 600; color: var(--vibe-foreground); font-family: var(--vibe-font-sans); }
.now-chip.is-now { border-color: var(--vibe-primary); background: color-mix(in srgb, var(--vibe-primary) 10%, transparent); }
.now-chip.is-now b, .now-chip.is-now { color: var(--vibe-primary); }
.now-chip.is-past { opacity: 0.55; }
/* ★ v0.15.61 颜色减法：同上 —— 文字链接中性 + hover 上色（它旁边还有 .empty-btn 这种真按钮） */
.now-hero-empty-link { font-size: var(--vibe-fs-xs); color: var(--vibe-muted-foreground); text-decoration: none; }
.now-hero-empty-link:hover { color: var(--vibe-brand-600); text-decoration: underline; }

/* 今日页 · 移动端：窄屏最容易变「又长又空」，这里统一收紧
   ⚠️ 页头的 flex 方向**不再由各页自己的内联样式改成 column** ——
      那样会把通知铃铛挤到第二行左边（曾经的样子）。铃铛必须留在第一行右侧。 */
@media (max-width: 767px) {
  /* 手机上块间距统一到刻度 12（窄屏本来排得密，再密就分不清块了） */
  .today-stack { row-gap: 0.75rem !important; }
  /* ⚠️ 底栏留白别再额外加在这（v0.10.9 试过又撤了）：
     手机端 dashboard.html 的内联样式把 html/body 改成了 height:auto，所以这里是
     **整页文档滚动**（不是 .page-body 内嵌滚动）→ `body:has(aside) main { padding-bottom }`
     那条已经生效（≈128px，扣掉底栏 96px 还剩 30 多 px 呼吸）。
     再给 .today-stack 加一份就是双重留白，滑到底会空出一大截。 */
  .today-page-head { flex-direction: row; align-items: flex-start; flex-wrap: wrap; gap: 0.5rem; }
  .today-page-head .today-head-main { flex: 1; gap: 0.375rem; }
  /* ★ v0.15.50 起因：**日期条被挤没了**（用户 2026-09-22：「日期都挤的快没了」）。
     根因：页头是 `[主块 flex:1][图标 flex-shrink:0]` 一行两列，而
     `.today-week-bar { flex: 1 0 100% }` 的"整条换行"是在**主块内部**换行 ——
     主块多窄，日期条就多窄。图标从 2 个变 3 个（v0.15.49 加了 🤖）之后主块只剩 ~230px，
     一行只看得见 4 天。
     v0.15.50 的解法是"把图标整排抬到上面一行"，日期条确实回到 6~7 天，但页头凭空高了一排。
     ⇒ **v0.15.59 换成"图标浮到右上角"**，既拿回整宽、又不多占一排。见下面那一段。 */
  /* ★ v0.15.59：**图标不再单独占一行**（用户 2026-09-23：「顶上三个东西单独占了一行
     是不是太浪费了 想想办法呐」）。
     原来是 `order: -1; flex: 1 1 100%` 把图标整排抬到日期之上 —— 那是 v0.15.50 为了让
     日期条拿回整宽用的**权宜**做法（当时日期条被关在主块里，只有图标让位主块才会变宽）。
     代价就是页头凭空多一排：实测 184px。
     ⚠️ 关键认知（量过才敢删那两行）：**图标独占一行不是"塞不下"，是被我们抬上去的**
        —— 删「星期几」只省 33px，那排照样在。
     现在改成：图标绝对定位钉在页头右上角，主块吃满整宽 ⇒ 图标回到日期那一行右侧；
     日期条的 `flex: 1 0 100%` 现在相对**页头**换行（不再相对被压窄的主块），所以**照样整宽**。
     实测：页头 184 → **136px**，日期条一天没少（431px 下 403px），桌面（≥1024）一个字没动。 */
  .today-page-head { position: relative; }
  .today-head-actions {
    position: absolute; top: 0.5rem; right: 0; left: auto;
    order: 0; flex: 0 0 auto; justify-content: flex-end;
  }
  .today-head-main { flex: 1 1 100%; }
  /* ⚠️ v0.18.31：「让出右上角那排浮动图标」的那几条**不写在这里** —— 它们只该在
     「图标真的浮在右上角」的宽度上生效（380~767px，见下面那个新块）。
     因为 ≤379px 有一条安全网（再往下几行）会把图标退回"独占一行"，那一档根本不需要让位，
     写了反而白扣 136px，把校历小标无辜挤到第二行。 */
  /* 更窄的屏（≲360，以及校历小标很长时）：日期那一行本身可能比"让位后"的宽度还宽
     —— 让它自己换行，别硬挤到图标底下（≤379 那档虽然没有浮动图标，换行也无害）。 */
  .today-title { flex-wrap: wrap; }
  /* 一周条手机上整条换行到页头第二行（date-row 里塞不下日期+班级+刻度+铃铛） */
  .today-week-bar { flex: 1 0 100%; margin-top: 0.25rem; }
  /* 手机上把日期条与学期行再收紧一点：格子已经 52px 够大，不必再垫高 */
  .today-week { gap: 0.375rem; padding: 1px 2px 4px; }
  /* ⚠️ v0.15.50 试过把这里的 52px 缩到 48px（想多塞一天），**又改回来了**：
     日期条拿到整宽（362px）之后，52px 一行的容量本来就是 6 个（floor((362-4)/(52+6))=6）——
     缩格子一天也没多塞，反而把"日期"本身变小了（用户抱怨的正是"挤"）。别为了凑数字再动它。 */
  .week-cell { width: 3.25rem; padding: 0.26rem 0.1rem 0.22rem; min-height: 2.5rem; border-radius: 12px; }
  .week-dn { font-size: 0.9375rem; }
  .today-date-row { gap: 0.375rem; flex-wrap: wrap; }
  .today-term-row { gap: 0.375rem; margin-top: 0; min-height: 1.125rem; }
  /* ⚠️ v0.18.31：「星期几」和「手机上把日期收一档（fs-xl）」这两条都删了 ——
     星期几不再渲染（见 dashboard.js），日期也不用再为"一行塞下三样东西"缩小了。 */
  .today-class-chip { padding: 0.18rem 0.45rem 0.18rem 0.55rem; }
  /* 面板贴着筹码左边，手机上宽度按视口收（.class-menu 的 max-width 已兜底） */
  .class-menu { min-width: 11rem; }
  .today-subtitle, .today-term { font-size: var(--vibe-fs-xs); }
  /* v0.10.35：改成 align-items:center（原来是 flex-start + padding-top 微调，
     铅笔和铃铛盒高不同时那样必然错位）。间距靠 gap，左右对称。 */
  .today-head-actions { align-items: center; gap: 0.375rem; }
  /* 主视觉 / 卡片 / 重要日期条：内边距统一到刻度 12 */
  .now-hero { padding: 0.75rem; border-radius: var(--vibe-radius-sm); }
  .now-hero-subject { font-size: var(--vibe-fs-xl); }
  .now-hero-time { font-size: var(--vibe-fs-base); }
  .now-hero-lessons { gap: 0.375rem; }
  .now-chip { padding: 0.2rem 0.45rem; }
  .today-grid { gap: 0.75rem; }
  .today-card { padding: 0.75rem; }
  .key-date { padding: 0.625rem 0.75rem; }
}

/* ⚠️ v0.15.59 安全网：**≤379px 退回"图标独占一行"的旧样子**。
   实测（`_tmp_v1559_polish.mjs`，量的是**图标左沿 − 日期行内容右沿**，≥0 才没压上）：
     · 390px 余 **14px** / 414px 余 38px / 431px 余 55px  ⇒ 新做法（图标浮到日期那一行）很宽裕
     · 375px 余 **-1px**（刚好擦上）/ 360px 更差、320px 差 44px  ⇒ 会压住**班级筹码**（切班入口）
   ⚠️ 阈值取 **379** 而不是 374：375 那一档实测只差 1px，是"贴着"的状态 ——
      与其留一个擦边，不如让 375 及以下**整整齐齐地走回退**（多一排，但绝不重叠）。
      390 / 414 / 431 是现在的主流机型，都走新做法。
   ⚠️ 这条**只会让极端窄屏保持原样**，不会把任何东西变得更糟 —— 所以它永远是安全的。
   ⚠️ ★ v0.18.31：被压的对象变了 —— 以前是**班级筹码**（切班入口），现在筹码挪到学期行了，
      这一档挡的是**校历小标 / 日期**。规矩没变：宁可多一排，绝不压字。 */
@media (max-width: 379px) {
  .today-page-head { position: static; }
  .today-head-actions { position: static; order: -1; flex: 1 1 100%; }
}

/* ★ v0.18.31：**只有"图标浮在右上角"的那一档（380~767）才需要让出右上角。**
   根因：图标是 `position: absolute; right: 0`（见上面 .today-head-actions），而主块
   `flex: 1 1 100%` 占整宽 ⇒ 日期行一路往右排，第一行的内容就钻到图标底下。
   实测 390px（用户 2026-09-27 手机截图报的「班级名被压住」）：
     班级筹码 213→322、图标区 244→390 ⇒ 横向压住 **78px**，班级名被铅笔图标盖掉中间那个字。
   ⇒ 两条一起用：
     ① `.today-date-row` 让出右上角那块 —— 放不下就把校历小标挤到第二行，**绝不压字**；
     ② `.today-week-bar` 用 `width` 把让出去的宽度补回来（**一格不少，实测仍 362px / 6 格**）。
   ⚠️ 那个 8.5rem 就是"右上角那排图标占多宽"：实测 3 个图标 = 146px，比页头右内边距（14px）多出
      132px ⇒ 取 136px 留几像素余量。**以后加/删页头图标、或改 .head-icon-btn 尺寸，这个数要跟着改。**
   ⚠️ 日期条**不能用负 margin** 去抵消这个 padding（试过，不行）：负 margin 会把它自己的
      `flex-basis: 100%` 缩成"占位只剩 86px"，于是它跟隔壁挤回同一行去了（实测只有 247.5px）。 */
@media (min-width: 380px) and (max-width: 767px) {
  .today-page-head { --head-icons-w: 8.5rem; }
  .today-date-row { padding-right: var(--head-icons-w); }
  .today-week-bar { flex: 1 0 auto; width: calc(100% + var(--head-icons-w)); }
}

/* ===== ★ v0.15.62 手机触控尺寸：**要点的东西最小 44×44** =====
   起因（用户 2026-09-23）：「待办的语音输入按钮是不是太小了」「小助手的按钮在左边 我右手按不方便」。
   我把手机上每个可点元素连尺寸带位置量了一遍（`_tmp_v1562_touch.mjs`），实测：
     · 待办 🎤 **35×35**、「添加」53×**33**、待办删除 ✕ **27×27**、放大/缩小 **37×20**
     · 作业订正那 5 个按钮**高只有 36~38**（`#hw-make` / `#hw-review` / `#hw-honor-btn` …）
     · 一堆「xx ›」文字链接只有 **17px 高**
   44 是 Apple HIG 的触控最小值（Material 更严，48）。下面分两种做法：
     ① **按钮**：直接 `min-height: 2.75rem`（本来就矮，该长高就长高）
     ② **小图标按钮 / 纯文字链接**：**视觉一个像素都不变**，只把命中区撑大 ——
        用一层透明 `::after` 覆盖层（`max(100%, 2.75rem)`），居中盖在原按钮上。
        ⚠️ 这里**不用**「padding + 等量负外边距」那招：那几个元素有边框/底色，
           padding 会把边框和 hover 底色一起撑大（视觉就变了）；覆盖层不会。
        ⚠️ 覆盖层里 `pointer-events` 不用改 —— 它是子元素，点它等于点父按钮，事件照常冒泡。
   ⚠️ **两个明确的例外**（是量出来的，不是漏了）：
     · **页头那三颗图标保持 40×40**：加到 44 会让那排宽 12px，而 390px 屏上
       它离班级筹码只剩 **14px** 余量（v0.15.59 实测）—— 会挤掉日期条，得不偿失。
     · **日历格子 `.kd-cal-day`（48×27）不动**：密集网格型目标通行做法允许小一点，
       抬到 44 会让 6 行日历多出 100px。 */
@media (max-width: 767px) {
  /* ① 按钮：直接长高 */
  .btn, .btn-ghost, .btn-secondary, .tbtn, .gbtn, .empty-btn, .reset-btn,
  .todo-add .todo-add-btn, .modal-close:not(.modal-x), .a2-btn { min-height: 2.75rem; }   /* ⚠️ :not(.modal-x)：那颗圆钮的 44 由 .modal-x 自己管（见上面），别在这里拉成 34×44 的椭圆 */
  /* ② 小图标按钮 / 文字链接：视觉不变，命中区撑到 44 */
  .bzr-mic.is-mini { width: 2.75rem; height: 2.75rem; padding: 0; }
  /* ⚠️ `.kd-cal-nav` 的宽高**不能写在这** —— 它的基础规则在文件后半段（2385 行附近），
     同权重后写者赢（这条我第一版就踩了：改了没生效，量出来还是 24×24）。
     挪到**文件末尾**的「v0.15.62 补」那一块。 */
  .todo-del, .today-size, .today-more { position: relative; }
  .todo-del::after, .today-size::after, .today-more::after {
    content: ''; position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
    width: max(100%, 2.75rem); height: max(100%, 2.75rem);
  }

  /* ★ v0.15.62：**待办卡底部从"一行挤三个"改成两行** ——
     原来是 `[输入框 flex:1][🎤 35][添加 53×33]` 挤一行，手机上一行三个控件本来就难点。
     现在：输入框独占一行；下一行是 [🎤 44×44] + [添加 占满剩余]，两个都够大、都在右下。 */
  .todo-add { flex-wrap: wrap; }
  /* ⚠️ `.todo-add input { flex: 1 1 100% }` 也不能写在这 —— 基础规则 `.todo-add input { flex: 1 }`
     在 2475 行附近，同权重后写者赢（第一版就是这条让输入框没独占一行、三个控件还挤在一块）。
     挪到文件末尾那一块。 */
  .todo-add .todo-add-btn { flex: 1 1 auto; }
}

/* ★ v0.15.62 小助手卡上那个「跟她说一句」：**靠右**。
   用户原话：「小助手的按钮在左边 我右手按是不是不方便」—— 实测它中心在 x=22%（卡片是整宽的，
   按钮左对齐贴在左边）。右边留给右手拇指。 */
.as-cta { display: flex; justify-content: flex-end; }

/* 重要日期倒计时条 */
.key-date {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: 0.5rem 0.75rem;
  padding: 0.75rem 1rem;          /* 左边 16 与卡片内容对齐（精细项 5） */
  border-radius: var(--vibe-radius-sm);
  border: 1px solid color-mix(in srgb, var(--vibe-brand-200) 55%, transparent);
  background: linear-gradient(120deg, var(--vibe-brand-50) 0%,
              color-mix(in srgb, var(--vibe-brand-100) 50%, var(--vibe-card)) 100%);
}
.key-date-main { display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; flex: 1; min-width: 0; }
.key-date-label { flex-shrink: 0; font-size: var(--vibe-fs-xs); font-weight: 600; letter-spacing: 0.04em; color: var(--vibe-brand-700); }
.key-date-title { font-size: 0.9375rem; font-weight: 700; color: var(--vibe-foreground); }
.key-date-date { font-size: var(--vibe-fs-xs); color: var(--vibe-muted-foreground); }
/* 倒计时天数（v0.10.9）：默认退到次要色 —— 原来它是全条最抢眼的（大号橙粗体），
   但「还有 26 天」这件事本身不该比标题还醒目。7 天内 / 就是今天 才转成强调色。 */
.key-date-days {
  flex-shrink: 0; font-size: var(--vibe-fs-sm); font-weight: var(--vibe-fw-medium);
  color: var(--vibe-muted-foreground); white-space: nowrap;
}
.key-date-days.is-soon { color: var(--vibe-brand-700); font-weight: 700; }
.key-date-days.is-today { color: var(--vibe-primary); font-weight: 700; }
.key-date-btn {
  flex-shrink: 0; padding: 0.22rem 0.6rem; font-size: var(--vibe-fs-xs); font-weight: 600;
  border-radius: var(--vibe-radius-xs); cursor: pointer; font-family: var(--vibe-font-sans);
  border: 1px solid color-mix(in srgb, var(--vibe-brand-300) 70%, transparent);
  background: var(--vibe-card); color: var(--vibe-brand-700);
}
.key-date-btn:hover { background: var(--vibe-brand-50); }
/* 没设日期时压成一行细条 —— 原来是个大方块，信息量为零却吃掉半屏 */
.key-date.is-empty {
  padding: 0.4rem 0.7rem;
  gap: 0.4rem;
  border-style: dashed;
  border-color: color-mix(in srgb, var(--vibe-border) 90%, transparent);
  background: transparent;
}
.key-date.is-empty .key-date-label { color: var(--vibe-muted-foreground); }
.key-date.is-empty .key-date-title { font-weight: 500; color: var(--vibe-muted-foreground); font-size: var(--vibe-fs-base); }
.key-date.is-empty .key-date-btn {
  border-color: var(--vibe-border); color: var(--vibe-muted-foreground); background: transparent;
}
.key-date.is-empty .key-date-btn:hover { background: var(--vibe-muted); color: var(--vibe-foreground); }

/* 大空态（v0.9.18 由「今日页」私有提升为全站通用）：
   主内容区没有数据时用 —— 虚线框 + 图标 + 主标题 + 副说明 + 行动按钮。
   结构：<div class="empty-block">
           <i data-lucide="…"></i>
           <div class="empty-title">这里还没有 X</div>
           <div class="empty-sub">为什么空 / 该怎么办</div>
           <div class="empty-actions"><a class="empty-btn" href="…">去做 X</a></div>
         </div>
   ⚠️ 小栏目 / 弹窗列表不要用这个，用 .empty-line（塞大虚线框反而更显空）。 */
.empty-block {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 0.45rem; text-align: center;
  padding: 1.75rem 1rem; margin: 0.35rem 0;
  border: 1px dashed color-mix(in srgb, var(--vibe-border) 95%, transparent);
  border-radius: var(--vibe-radius-sm);
  background: color-mix(in srgb, var(--vibe-muted) 32%, transparent);
}
.empty-block > i, .empty-block > svg { width: 1.5rem; height: 1.5rem; color: var(--vibe-brand-300); }
.empty-title { font-size: var(--vibe-fs-md); font-weight: var(--vibe-fw-medium); color: var(--vibe-foreground); }
.empty-sub { font-size: var(--vibe-fs-sm); color: var(--vibe-muted-foreground); line-height: 1.6; max-width: 34em; }
/* 行动区：可以放一个也可以放两个按钮（如「添加学生」+「批量导入」） */
.empty-actions { display: flex; flex-wrap: wrap; gap: var(--vibe-gap-sm); justify-content: center; margin-top: 0.3rem; }
.empty-btn {
  display: inline-flex; align-items: center; gap: 0.3rem;
  padding: 0.35rem 0.85rem;
  font-size: var(--vibe-fs-base); font-weight: var(--vibe-fw-medium); text-decoration: none;
  border-radius: var(--vibe-radius-xs);
  background: var(--vibe-brand-50); color: var(--vibe-brand-700);
  border: 1px solid color-mix(in srgb, var(--vibe-brand-200) 70%, transparent);
}
.empty-btn:hover { background: var(--vibe-brand-100); }
.empty-btn i, .empty-btn svg { width: 0.9rem; height: 0.9rem; flex-shrink: 0; }
/* 次要按钮（配对出现时的第二个动作） */
.empty-btn.is-ghost {
  background: transparent; color: var(--vibe-muted-foreground);
  border-color: var(--vibe-border);
}
.empty-btn.is-ghost:hover { background: var(--vibe-muted); color: var(--vibe-foreground); }
/* 按钮型（不是链接，例如点开「添加学生」弹窗）—— 需要 JS 绑定，用 button 标签 */
button.empty-btn { cursor: pointer; font-family: var(--vibe-font-sans); }

/* ===== 后台类宽表格页：窄屏横向滚动（v0.10.10）=====
   背景：「用户管理」「邀请码管理」这类页面本质就是「一张宽表格」，列宽由内容决定 ——
   数据一多，表格就把 .card / .container 一起撑破，**整个页面横向溢出**
   （表现：标题被切掉一截、列被挤成两行、整页能左右滑）。实测 admin-users 溢出 92px。
   这里让表格只在**自己的容器里**横滑，页面宽度永远不超过视口。
   ⚠️ 新增同类后台表格页时，只给表格外层容器加一个 `.table-scroll` 就行。
   ⚠️ 别改成 `overflow: hidden` —— 那是「看不见」而不是「能滑」，列会被直接裁掉。
   ⚠️ 只在 ≤1023 生效：桌面端表格放得下，加了反而多一条无用的滚动条。 */
@media (max-width: 1023px) {
  .table-scroll {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior-x: contain;      /* 横滑到头别把整页带着一起动 */
  }
  /* 680 是实测出来的：邀请码表 7 列，640 时「备注 / 使用情况」还会硬换一次行
     （行高 145 → 680 时 103），再多也没收益，就定这。 */
  .table-scroll > table { min-width: 680px; }
  .table-scroll::-webkit-scrollbar { height: 6px; }
  .table-scroll::-webkit-scrollbar-thumb { background: var(--vibe-border); border-radius: 999px; }
}

/* 重要日期管理弹窗 */
.kd-row {
  display: flex; align-items: center; gap: 0.5rem;
  padding: 0.45rem 0.6rem; border-radius: var(--vibe-radius-xs);
  border: 1px solid color-mix(in srgb, var(--vibe-border) 70%, transparent);
}
.kd-kind {
  flex-shrink: 0; font-size: var(--vibe-fs-2xs); font-weight: 600; padding: 0.1rem 0.4rem;
  border-radius: 999px; background: var(--vibe-muted); color: var(--vibe-muted-foreground);
}
.kd-row-title { flex: 1; min-width: 0; font-size: var(--vibe-fs-base); color: var(--vibe-foreground); word-break: break-word; }
.kd-row-date { flex-shrink: 0; font-size: var(--vibe-fs-xs); color: var(--vibe-muted-foreground); font-variant-numeric: tabular-nums; }
.kd-row-del {
  flex-shrink: 0; padding: 0.15rem; border: none; background: none; cursor: pointer;
  color: var(--vibe-muted-foreground); line-height: 0; border-radius: var(--vibe-radius-xs);
}
.kd-row-del:hover { color: var(--vibe-destructive); background: var(--vibe-muted); }
.kd-add {
  display: grid; gap: 0.45rem; margin-top: 1rem; padding-top: 1rem;
  border-top: 1px solid color-mix(in srgb, var(--vibe-border) 70%, transparent);
}
.kd-add input, .kd-add select {
  width: 100%; box-sizing: border-box;
  padding: 0.42rem 0.6rem; font-size: var(--vibe-fs-base);
  border-radius: var(--vibe-radius-xs); border: 1px solid var(--vibe-input);
  background: var(--vibe-background); color: var(--vibe-foreground);
  font-family: var(--vibe-font-sans);
}
.kd-add input:focus, .kd-add select:focus {
  outline: 2px solid var(--vibe-ring, rgba(241, 72, 30, 0.4)); outline-offset: 1px; border-color: transparent;
}
.kd-add button {
  padding: 0.45rem 0.9rem; font-size: var(--vibe-fs-base); font-weight: 600;
  border: none; border-radius: var(--vibe-radius-xs); cursor: pointer;
  background: var(--vibe-primary); color: var(--vibe-primary-foreground);
  font-family: var(--vibe-font-sans);
}
.kd-add button:hover { opacity: 0.9; }
.kd-add button:disabled { opacity: 0.6; cursor: not-allowed; }

/* 课表时间轴：分段标题 + 圆点（不再每行画满宽横线） */
.tt-seg { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 0.2rem 0.25rem; }
.tt-seg:first-child { padding-top: 0.25rem; }
.tt-seg-name { font-size: var(--vibe-fs-xs); font-weight: 600; color: var(--vibe-muted-foreground); letter-spacing: 0.02em; flex-shrink: 0; }
.tt-seg-line { flex: 1; height: 1px; background: color-mix(in srgb, var(--vibe-border) 55%, transparent); }

.tt-row {
  display: grid; grid-template-columns: 0.7rem 3rem minmax(0, 1fr);
  gap: 0.55rem; align-items: center;
  padding: 0.32rem 0.4rem; border-radius: var(--vibe-radius-xs);
}
.tt-dot { width: 6px; height: 6px; border-radius: 999px; background: var(--vibe-border); justify-self: center; }
.tt-time { font-size: var(--vibe-fs-xs); color: var(--vibe-muted-foreground); font-variant-numeric: tabular-nums; }
.tt-body { display: flex; align-items: baseline; gap: 0.5rem; min-width: 0; }
.tt-label { font-size: var(--vibe-fs-xs); color: var(--vibe-muted-foreground); flex-shrink: 0; width: 3.6rem; }
.tt-name { font-size: var(--vibe-fs-base); font-weight: 600; color: var(--vibe-foreground); }
.tt-meta { font-size: var(--vibe-fs-xs); color: var(--vibe-muted-foreground); }
.tt-badge { font-size: var(--vibe-fs-2xs); font-weight: 600; padding: 0.05rem 0.4rem; border-radius: 999px; }

/* 公共行（升旗 / 午休 / 大课间）：空心圆点弱化。
   ⚠️ 必须写在 is-past / is-now / is-next 之前 —— 同权重下后写的赢，
   否则「正在上」的公共行文字会被这条压成灰色（曾经踩过）。 */
.tt-row.is-common .tt-name { font-weight: 500; color: var(--vibe-muted-foreground); }
.tt-row.is-common .tt-dot { background: transparent; box-shadow: inset 0 0 0 1.5px var(--vibe-border); }

/* 已上完：只把文字压淡（整行透明会让页面发灰发脏） */
.tt-row.is-past .tt-name { font-weight: 500; color: var(--vibe-muted-foreground); }
.tt-row.is-past .tt-dot { background: color-mix(in srgb, var(--vibe-border) 65%, transparent); }

/* 正在上：唯一的重高亮（主色左竖条 + 淡底 + 实心徽标） */
.tt-row.is-now {
  background: color-mix(in srgb, var(--vibe-primary) 8%, transparent);
  box-shadow: inset 3px 0 0 var(--vibe-primary);
}
.tt-row.is-now .tt-name { color: var(--vibe-primary); }
.tt-row.is-now .tt-dot { background: var(--vibe-primary); }
.tt-row.is-now .tt-badge { background: var(--vibe-primary); color: var(--vibe-primary-foreground); }

/* 下一节：只有徽标 + 主色文字，不铺底色（避免两个橙块打架） */
.tt-row.is-next .tt-name { color: var(--vibe-brand-600); }
.tt-row.is-next .tt-dot { background: var(--vibe-brand-600); }
.tt-row.is-next .tt-badge { background: var(--vibe-brand-50); color: var(--vibe-brand-700); }

/* ---------- 右列：今日值日 + 待办（「此刻」已上移为整宽主视觉，见 .now-hero） ---------- */

/* 设置页「我的任课」科目筹码 */
.subj-chip {
  padding: 0.28rem 0.7rem; font-size: var(--vibe-fs-base); font-weight: 500; cursor: pointer;
  border-radius: 999px; font-family: var(--vibe-font-sans);
  border: 1px solid var(--vibe-border);
  background: var(--vibe-background); color: var(--vibe-muted-foreground);
  transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.subj-chip:hover { border-color: var(--vibe-brand-300); color: var(--vibe-foreground); }
.subj-chip.is-on {
  background: var(--vibe-brand-50); color: var(--vibe-brand-700);
  border-color: var(--vibe-brand-300); font-weight: 600;
}

/* ⚠️ v0.10.22：原来的 .side-sec / .side-head / .today-side 一整套样式**已删掉**。
   那套是给「待办 + 值日 挤在同一张卡里」用的（卡内分节、节间画分隔线）。
   现在它们各自成卡、统一用 .today-head 那套卡片标题，分节概念不存在了。
   如果哪天又想把两张卡并回一张，git 里能翻到这一版之前的内容。 */

/* 值日（v0.10.3 从「一行一岗位」改成筹码流）：
   8 个岗位一行一个要占 350px，手机上把待办挤到屏幕外；改成会换行的筹码后
   高度砍掉一半多，而且「岗位 + 姓名」还是一对，读起来不打结。 */
.duty-chips { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.duty-chip {
  display: inline-flex; align-items: baseline; gap: 0.5rem;
  padding: 0.26rem 0.6rem; border-radius: 999px;
  border: 1px solid color-mix(in srgb, var(--vibe-border) 62%, transparent);
  background: color-mix(in srgb, var(--vibe-background) 62%, transparent);
}
.duty-pos { font-size: var(--vibe-fs-xs); color: var(--vibe-muted-foreground); }
.duty-who { font-size: var(--vibe-fs-base); font-weight: var(--vibe-fw-medium); color: var(--vibe-foreground); }

/* 科目色小方点（v0.10.3）：主视觉筹码、课表行、右侧「我的课」共用。
   只做点缀，文字一律保持深色 —— 彩色文字在浅底上读着累。 */
.subj-dot {
  width: 7px; height: 7px; border-radius: 2px; flex-shrink: 0;
  background: var(--vibe-border); display: inline-block;
  align-self: center;   /* 行内与文字按基线对齐时，小方块会浮上去，这里压回中线 */
}

/* 右侧「近期安排」列表 */
.upcoming-row { display: flex; align-items: baseline; gap: 0.5rem; padding: 0.5rem 0; }
.upcoming-row + .upcoming-row { border-top: 1px solid color-mix(in srgb, var(--vibe-border) 50%, transparent); }
.upcoming-kind {
  flex-shrink: 0; font-size: var(--vibe-fs-2xs); font-weight: var(--vibe-fw-medium);
  padding: 0.1rem 0.4rem; border-radius: 999px;
  background: color-mix(in srgb, var(--vibe-muted-foreground) 14%, transparent);
  color: var(--vibe-muted-foreground);
}
.upcoming-title { font-size: var(--vibe-fs-base); font-weight: var(--vibe-fw-medium); color: var(--vibe-foreground); min-width: 0; }
.upcoming-when { margin-left: auto; flex-shrink: 0; font-size: var(--vibe-fs-xs); color: var(--vibe-muted-foreground); white-space: nowrap; }
/* 正在预览的那天正好有安排：左边一条主色竖条 + 浅底，不用自己去找（v0.10.6） */
.upcoming-row.is-view {
  padding-left: 0.45rem; padding-right: 0.45rem;
  border-radius: var(--vibe-radius-2xs);
  background: color-mix(in srgb, var(--vibe-brand-50) 85%, transparent);
  box-shadow: inset 3px 0 0 var(--vibe-primary);
}
.upcoming-when.is-soon { color: var(--vibe-brand-700); font-weight: var(--vibe-fw-medium); }

/* ---------- 「近期安排」里的月历（v0.10.19）----------
   一行 7 格、格子 27px 高：六周刚好 ~212px，放在右列（320~460px 宽）里不挤。
   有安排的日子在数字下面点一个小圆点 —— 这是「这周有几天有事」的唯一线索，
   下面那三行文字负责说「是什么事」，两边分工。 */
.kd-cal {
  padding-bottom: 0.5rem; margin-bottom: 0.15rem;
  border-bottom: 1px solid color-mix(in srgb, var(--vibe-border) 50%, transparent);
}
.kd-cal-head { display: flex; align-items: center; justify-content: space-between; gap: 0.35rem; margin-bottom: 0.25rem; }
.kd-cal-title {
  font-size: var(--vibe-fs-sm); font-weight: 600;
  color: var(--vibe-foreground); font-variant-numeric: tabular-nums;
}
.kd-cal-nav {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.5rem; height: 1.5rem; flex-shrink: 0;
  border: none; background: none; cursor: pointer;
  border-radius: var(--vibe-radius-2xs); color: var(--vibe-muted-foreground);
  transition: background-color 150ms var(--vibe-ease), color 150ms var(--vibe-ease);
}
.kd-cal-nav:hover { background: var(--vibe-muted); color: var(--vibe-foreground); }
.kd-cal-nav svg { width: 0.95rem; height: 0.95rem; }
.kd-cal-week, .kd-cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); }
.kd-cal-wd {
  text-align: center; font-size: var(--vibe-fs-2xs);
  color: var(--vibe-muted-foreground); padding-bottom: 0.15rem;
}
.kd-cal-grid { row-gap: 2px; }
.kd-cal-day {
  position: relative;
  display: flex; align-items: center; justify-content: center;
  height: 1.7rem; padding: 0; border: none; background: none;
  font: inherit; font-size: var(--vibe-fs-xs);
  color: var(--vibe-foreground); font-variant-numeric: tabular-nums; cursor: pointer;
  border-radius: var(--vibe-radius-2xs);
  transition: background-color 150ms var(--vibe-ease), color 150ms var(--vibe-ease);
}
.kd-cal-day:hover { background: var(--vibe-muted); }
.kd-cal-day.is-blank { cursor: default; }
.kd-cal-day.has-ev::after {
  content: ''; position: absolute; left: 50%; transform: translateX(-50%);
  bottom: 1px; width: 4px; height: 4px; border-radius: 999px; background: var(--vibe-primary);
}
/* 今天 / 正在看的那天：前者只染色（不抢），后者做实心主色块（它是导航的一部分）。
   ⚠️ is-sel 必须写在 :hover 之后 —— 两者特异度相同，靠顺序压住 hover 的底色。 */
.kd-cal-day.is-today { color: var(--vibe-primary); font-weight: 700; }
.kd-cal-day.is-sel {
  background: linear-gradient(140deg, var(--vibe-brand-500), var(--vibe-brand-600));
  color: #fff; font-weight: 700;
  box-shadow: 0 1px 2px rgba(60, 50, 40, 0.18);
}
.kd-cal-day.is-sel.has-ev::after { background: #fff; }

/* 右侧「我的课」列表（宽屏才有；窄屏由主视觉筹码代替） */
.my-lesson { position: relative; display: flex; align-items: baseline; gap: 0.5rem; padding: 0.5rem 0 0.5rem 0.8rem; }
.my-lesson + .my-lesson { border-top: 1px solid color-mix(in srgb, var(--vibe-border) 50%, transparent); }
/* 时间轴竖条（v0.10.19）：科目色由 JS 内联写进 style，这里只管位置与形状 */
.my-lesson-bar { position: absolute; left: 0; top: 0.55rem; bottom: 0.55rem; width: 3px; border-radius: 3px; }
/* 正在上：给竖条一圈主色光晕，和「下一节/已上过」区分开 */
.my-lesson.is-now .my-lesson-bar { box-shadow: 0 0 0 3px color-mix(in srgb, var(--vibe-primary) 16%, transparent); }
.my-lesson.is-past .my-lesson-bar { opacity: 0.4; }
.my-lesson-time { flex-shrink: 0; width: 2.9rem; font-size: var(--vibe-fs-xs); color: var(--vibe-muted-foreground); font-variant-numeric: tabular-nums; }
.my-lesson-subject { font-size: var(--vibe-fs-base); font-weight: var(--vibe-fw-medium); color: var(--vibe-foreground); }
.my-lesson-cls { margin-left: auto; font-size: var(--vibe-fs-xs); color: var(--vibe-muted-foreground); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 46%; }
.my-lesson.is-past .my-lesson-subject { color: var(--vibe-muted-foreground); font-weight: 500; }
.my-lesson.is-now .my-lesson-subject, .my-lesson.is-now .my-lesson-time { color: var(--vibe-primary); }

/* 「更多 ›」用按钮实现时（如「近期安排 · 管理」）要抹掉按钮默认样式 */
.today-more-btn {
  border: none; background: none; padding: 0; cursor: pointer;
  font-family: var(--vibe-font-sans); line-height: 1.5;
}

/* 待办（右列内单列） */
/* ⚠️⚠️ v0.18.88：**`align-content: start` 不能少** —— 用户 2026-09-30 附截图：
   「再优化一下这个图二 还能再优化一下 **不好看**」。
   那个"不好看"就是**一行待办躺在卡片上半截、下面一大片空白**：
   `.todo-list` 是 grid，而 grid 容器的 `align-content` 默认是 `stretch`
   ⇒ 行高是 auto 时，**多出来的高度会平摊给每一行**（只有一条待办时就被拉得极高）。
   ⇒ 钉死成 `start`：行有多高就多高，多出来的空间留在下面（下面还有"今天完成"和录入区接着）。 */
.todo-list { display: grid; align-content: start; }
/* ★ v0.18.88：勾完 / 撤销之后 JS 会改 scrollTop 把那一行滚进视野 ——
   平滑**交给 CSS**：实测 `scrollTo({behavior:'smooth'})` 在这个 grid 容器里压根不动。 */
.todo-list { scroll-behavior: smooth; }
.todo-row { display: flex; align-items: flex-start; gap: 0.5rem; padding: 0.5rem 0; }
.todo-row + .todo-row { border-top: 1px solid color-mix(in srgb, var(--vibe-border) 50%, transparent); }
/* 待办的勾选框（v0.10.14）：原来 0.9rem＝14px，手机上点不中。
   装进一个带内边距的 label（见 dashboard.js 的 renderTodo），可点区域约 30×32。 */
.todo-check {
  display: inline-flex; align-items: center; justify-content: center;
  flex-shrink: 0; padding: 0.4rem 0.5rem 0.4rem 0.15rem; margin: -0.4rem 0;
  cursor: pointer; line-height: 0;
}
.todo-row input[type="checkbox"] {
  margin: 0; width: 1.05rem; height: 1.05rem; flex-shrink: 0;
  accent-color: var(--vibe-primary); cursor: pointer;
}
.todo-main { flex: 1; min-width: 0; }
/* ★ v0.18.87：标题从 `<div>` 改成 `<button>`（**点一下就改内容**，见 dashboard.js 的 todoEditTitle）。
   ⇒ 这些是浏览器给 button 的默认样式，一条都不能省（少了就是"居中、灰色、带边框"的按钮样）。 */
.todo-title {
  display: block; width: 100%; text-align: left; padding: 0; margin: 0;
  border: none; background: none; cursor: text;
  font-family: var(--vibe-font-sans); font-size: var(--vibe-fs-base); font-weight: 400;
  color: var(--vibe-foreground); line-height: 1.5; word-break: break-word;
}
.todo-title:hover { color: var(--vibe-primary); }
/* 就地改标题时换上的输入框（跟"改提醒时间"那颗药丸一个套路：不弹窗、就地换） */
.todo-title-edit {
  display: block; width: 100%; box-sizing: border-box;
  padding: 0.2rem 0.4rem; font-size: var(--vibe-fs-base);
  border: 1px solid var(--vibe-primary); border-radius: var(--vibe-radius-xs);
  background: var(--vibe-background); color: var(--vibe-foreground); font-family: var(--vibe-font-sans);
  animation: todo-in var(--vibe-dur) var(--vibe-ease-out);
}
.todo-due { font-size: var(--vibe-fs-xs); color: var(--vibe-muted-foreground); line-height: 1.5; }
.todo-due.is-overdue { color: var(--vibe-destructive); }
.todo-del {
  flex-shrink: 0; padding: 0.45rem; border: none; background: none; cursor: pointer;   /* v0.10.14：0.15rem→0.45rem（18px→约 27px，手机上才点得中） */
  color: var(--vibe-muted-foreground); line-height: 0; border-radius: var(--vibe-radius-xs);
}
.todo-del:hover { color: var(--vibe-destructive); background: var(--vibe-muted); }
/* ★ v0.18.88：**"按下去要有反应"** —— 用户 2026-09-30：「按钮选择时间什么的 **确认感差**」。
   这几颗原来只有 hover（手机上根本看不到 hover）⇒ 手指按下去那一下**一点反馈都没有**。
   ⚠️ 只给"待办这一摊"的按钮加，**别顺手写 `.btn:active`** —— `.btn` 是全站的，会波及每个页面。 */
.todo-add .todo-add-btn, #ov-when-ok, #ov-when-no, .todo-del {
  transition: transform var(--vibe-dur-fast) var(--vibe-ease-out),
    background-color var(--vibe-dur-fast) var(--vibe-ease),
    opacity var(--vibe-dur-fast) var(--vibe-ease);
}
.todo-add .todo-add-btn:active { transform: scale(0.97); }
#ov-when-ok:active, #ov-when-no:active { transform: scale(0.97); }
.todo-del:active { transform: scale(0.88); }
/* 「要提醒你吗」弹窗里那三颗**快捷芯片** ——
   ⚠️ 原来它们的外观是**内联样式**，而内联优先级最高 ⇒ `:active` 根本改不动（这就是"确认感差"的根因）。
      ⇒ 挪到类里，顺带补上 hover / active。 */
.todo-quick {
  font-size: var(--vibe-fs-xs); padding: 0.3rem 0.6rem; cursor: pointer;
  border: 1px solid var(--vibe-border); border-radius: 999px;
  background: var(--vibe-card); color: var(--vibe-foreground); font-family: var(--vibe-font-sans);
  transition: transform var(--vibe-dur-fast) var(--vibe-ease-out),
    background-color var(--vibe-dur-fast) var(--vibe-ease),
    border-color var(--vibe-dur-fast) var(--vibe-ease),
    color var(--vibe-dur-fast) var(--vibe-ease);
}
.todo-quick:hover { border-color: var(--vibe-primary); color: var(--vibe-primary); }
.todo-quick:active { transform: scale(0.95); background: var(--vibe-muted); }
/* ★★ v0.18.88：**选中的那颗要亮着** —— 用户 2026-09-30：
   「这边我点了一个小时什么的 **一点确认感都没有**」。
   原来点完只是把下面的时间框悄悄改了值，芯片本身**毫无变化** ⇒ 他不知道自己点中没有。
   ⚠️ 只在"他点了芯片"时亮；他**自己手改时间框**时要把这个亮取消（见 dashboard.js）。 */
.todo-quick.is-on {
  border-color: var(--vibe-primary); color: var(--vibe-primary);
  background: color-mix(in srgb, var(--vibe-primary) 12%, transparent);
  font-weight: 600;
}
/* ★ v0.18.87：**待办的四段小动效** —— 用户 2026-09-30：
   「待办这个动画有没有？再给我多加一点，搞得好看一点。这个待办的功能很重要」。
   ⚠️⚠️ 三条纪律（别改坏）：
     ① **动效一律挂在"事件那一刻"的类上**（.is-checking / .is-unchecking / .is-removing / .is-new），
        **绝不许把 animation 挂到 .todo-row 这个基础类上** —— 那样每次 reloadOverview 重画都会重播一遍，
        勾一条会看到整列都重新入场一次（列表一直在闪）。
        （⚠️ 这条注释**故意不写那个字面写法** —— 写了的话，别人 grep 这个模式时会被注释骗到，
          我自己这一版的走查就差点被自己注释里的那串骗白红一条。）
     ② 时长/缓动**一律用令牌**（--vibe-dur / --vibe-dur-slow / --vibe-ease-out），别写死毫秒。
     ③ 全局顶部已有 `prefers-reduced-motion: reduce` 兜底（把所有 transition 压成 0.01ms），这里不用再写。 */
.todo-row { transition: opacity var(--vibe-dur-slow) var(--vibe-ease-out), transform var(--vibe-dur-slow) var(--vibe-ease-out); }
/* 划掉：**用 text-decoration-color 的过渡**，不是"啪地冒出来"——
   （原来就是一条静态的 text-decoration，没有任何过渡。）
   ⚠️ 起点（未完成）就必须挂着透明的 line-through，否则"从 none 变 line-through"那一下的颜色起点不可靠。 */
.todo-title {
  text-decoration-line: line-through; text-decoration-thickness: 1.5px;
  text-decoration-color: transparent;
  transition: text-decoration-color var(--vibe-dur-slow) var(--vibe-ease-out);
}
.todo-row.is-checking .todo-title, .todo-row.is-done .todo-title { text-decoration-color: currentColor; }
/* 勾上那一瞬：整行退到背景里一半 + 勾选框弹一下（他勾的是"这件事做完了"，得有个"收进去"的手感） */
.todo-row.is-checking { opacity: 0.62; }
.todo-row.is-checking .todo-check { animation: todo-pop var(--vibe-dur-slow) var(--vibe-ease-out); }
@keyframes todo-pop { 45% { transform: scale(1.32); } }
/* 撤销：划线渐隐 + 整行从"已完成"的淡出状态回到正常（它本来 opacity 是 .72） */
.todo-row.is-done.is-unchecking { opacity: 1; }
.todo-row.is-unchecking .todo-title { text-decoration-color: transparent; }
.todo-row.is-unchecking .todo-check { animation: todo-pop var(--vibe-dur-slow) var(--vibe-ease-out); }
/* 删除：淡出 + 往右挪一点（方向跟"删掉/扔掉"一致），播完再让服务端真删 + 重画 */
.todo-row.is-removing { opacity: 0; transform: translateX(14px); }
/* 新加的那一条：从上方轻轻落进来（**只给它**，见 dashboard.js 的 markNewTodoRows） */
.todo-row.is-new { animation: todo-in var(--vibe-dur-slow) var(--vibe-ease-out); }
@keyframes todo-in { from { opacity: 0; transform: translateY(-8px); } }
/* 点提醒药丸就地把输入框换上来 —— 也给一下淡入，不然是一次硬切 */
.todo-remind-edit { animation: todo-in var(--vibe-dur) var(--vibe-ease-out); }

/* ★ v0.18.71：**办完的别直接消失，留在下面划掉** ——
   用户 2026-09-29：「待办里已经办完的事情是不是可以不直接消失，而是不通知不提醒、被划掉」。
   （原来勾完这条就从列表里没了：看不到"今天干了啥"，误勾了也没法撤销。）
   ⚠️ 划掉是**只读的观感**：勾选框还在（可以撤销），删除键也还在。
   ⚠️ v0.18.87：这儿原来那条 `text-decoration: line-through` 挪到上面 `.todo-title` 去了
      （为了能过渡）—— 别再往这儿加回去，加了就是"瞬间出现 + 过渡"两套打架。 */
.todo-row.is-done { opacity: 0.72; }
.todo-row.is-done .todo-due { opacity: 0.9; }
/* 「今天完成 N 条」那道小分隔（不是 group 标题的排场，就是一条浅线 + 一行小字） */
.todo-sep {
  display: flex; align-items: center; gap: 0.5rem;
  margin: 0.35rem 0 0; font-size: var(--vibe-fs-xs); color: var(--vibe-muted-foreground);
}
.todo-sep::before, .todo-sep::after {
  content: ''; flex: 1; height: 1px;
  background: color-mix(in srgb, var(--vibe-border) 70%, transparent);
}
/* ★ v0.18.91：原来这儿是 `.todo-undo / .todo-undo-txt / .todo-undo-btn`
   （v0.18.90 加的"删完在最上面留一条撤销"）—— 用户 2026-09-30「我二次确认后 就不用撤销了吧 太麻烦了」
   ⇒ **整条撤掉**，样式一并删干净（别留死样式）。
   ⚠️ "删之前先问一句"**保留**，那是他要的那一次确认。 */

/* 通知铃铛的可点区域（v0.10.14 建 · v0.10.35 撤掉伪元素）
   v0.10.14 时铃铛盒子只有 20×20，靠 `::after { inset: -0.4rem }` 把命中区撑到约 33px。
   v0.10.35 起铃铛改用与铅笔同一套 .head-icon-btn 盒子（手机 40×40 / 桌面 36×36），
   盒子本身就是合格的命中区 —— 再留那个伪元素会撑到 53px，反而会和旁边的铅笔抢点击，故删除。 */
.todo-add { display: flex; align-items: center; gap: 0.4rem; margin-top: 0.75rem; }
.todo-add input {
  flex: 1; min-width: 0; padding: 0.42rem 0.6rem; font-size: var(--vibe-fs-base);
  border-radius: var(--vibe-radius-xs); border: 1px solid var(--vibe-input);
  background: var(--vibe-background); color: var(--vibe-foreground);
  font-family: var(--vibe-font-sans);
}
.todo-add input:focus { outline: 2px solid var(--vibe-ring, rgba(241, 72, 30, 0.4)); outline-offset: 1px; border-color: transparent; }
/* ⚠️ v0.15.50：这一行原来写的是 `.todo-add button` —— 泛选到**这一行里的每个按钮**。
   现在这一行多了一个「按住说话」的麦克风（`.bzr-mic.is-mini`），泛选会把它染成橙色实心按钮
   （选择器优先级 0,1,1 > .bzr-mic 的 0,1,0）。所以收窄成 `.todo-add-btn`，别改回 `button`。 */
.todo-add .todo-add-btn {
  flex-shrink: 0; padding: 0.42rem 0.85rem; font-size: var(--vibe-fs-base); font-weight: 600;
  border: none; border-radius: var(--vibe-radius-xs); cursor: pointer;
  background: var(--vibe-primary); color: var(--vibe-primary-foreground);
  font-family: var(--vibe-font-sans);
}
.todo-add .todo-add-btn:hover { opacity: 0.9; }
.todo-add .todo-add-btn:disabled { opacity: 0.6; cursor: not-allowed; }

/* ---------- 待办提醒（★ v0.15.83）----------
   老师 2026-09-23：「每条待办都要能通知」。三块：
     · `.todo-add .todo-bell` = 录入区那个开关（点一下才露出下面那行）
     · `.todo-remind-bar`     = 录入区的「提醒我 ___」
     · `.todo-remind`         = 待办行上的小药丸（点它就地改）
   ⚠️ 提醒行**默认收着**：待办卡本来就小（老师专门为它做过"放大/缩小"），
      常驻一行会把卡片撑高 —— 而"设提醒"不是每条待办都要做的事。 */
.todo-add .todo-bell {
  flex-shrink: 0; padding: 0.42rem 0.5rem; line-height: 0;
  border: 1px solid var(--vibe-input); border-radius: var(--vibe-radius-xs);
  background: var(--vibe-background); color: var(--vibe-muted-foreground); cursor: pointer;
}
.todo-add .todo-bell:hover { color: var(--vibe-primary); border-color: var(--vibe-primary); }
.todo-add .todo-bell.is-on { color: var(--vibe-primary); border-color: var(--vibe-primary); background: color-mix(in srgb, var(--vibe-primary) 10%, transparent); }
.todo-remind-bar {
  display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap;
  margin-top: 0.45rem; font-size: var(--vibe-fs-xs); color: var(--vibe-muted-foreground);
}
/* ⚠️ `display:flex` 会盖掉 `hidden` 自带的 `display:none`（同一个元素上 display 只认一条），
   所以必须显式再写一条 —— 不写的话「点开关收起」会失效。 */
.todo-remind-bar[hidden] { display: none; }
.todo-remind-bar input {
  flex: 1 1 10rem; min-width: 0; padding: 0.3rem 0.45rem;
  font-size: var(--vibe-fs-xs); border-radius: var(--vibe-radius-xs);
  border: 1px solid var(--vibe-input);
  background: var(--vibe-background); color: var(--vibe-foreground);
  font-family: var(--vibe-font-sans);
}
.todo-remind-bar button {
  flex-shrink: 0; padding: 0.3rem 0.5rem; font-size: var(--vibe-fs-xs);
  border: 1px solid var(--vibe-border); border-radius: var(--vibe-radius-xs);
  background: var(--vibe-background); color: var(--vibe-muted-foreground);
  cursor: pointer; font-family: var(--vibe-font-sans);
}
.todo-remind {
  display: inline-block; margin-top: 0.2rem; padding: 0.05rem 0.4rem;
  font-size: var(--vibe-fs-xs); line-height: 1.6;
  border: 1px solid var(--vibe-border); border-radius: 999px;
  background: var(--vibe-muted); color: var(--vibe-muted-foreground);
  cursor: pointer; font-family: var(--vibe-font-sans);
}
.todo-remind:hover { color: var(--vibe-primary); border-color: var(--vibe-primary); }
/* 提醒时刻已经过去了（含已逾期）—— 压暗，别和"待会要响的"抢注意力 */
.todo-remind.is-past { opacity: 0.55; }
/* 行内改提醒时间的输入框（点药丸换出来的） */
.todo-remind-edit {
  display: block; margin-top: 0.25rem; padding: 0.2rem 0.35rem;
  font-size: var(--vibe-fs-xs); border-radius: 6px;
  border: 1px solid var(--vibe-primary);
  background: var(--vibe-background); color: var(--vibe-foreground);
  font-family: var(--vibe-font-sans); max-width: 13rem;
}

/* ---------- 表单错误态（登录页） ---------- */
.field-error input { border-color: var(--vibe-destructive) !important; }
.field-error-msg { display: none; font-size: var(--vibe-fs-sm); margin-top: 0.35rem; color: var(--vibe-destructive); }
.field-error + .field-error-msg, .field-error-msg.show { display: block; }

@keyframes bzr-shake {
  0%, 100% { transform: translateX(0); }
  20%, 60% { transform: translateX(-5px); }
  40%, 80% { transform: translateX(5px); }
}
.shake { animation: bzr-shake 0.35s ease; }

@keyframes bzr-spin { to { transform: rotate(360deg); } }
.spinner {
  width: 1rem; height: 1rem; border-radius: 999px;
  border: 2px solid currentColor; border-top-color: transparent;
  animation: bzr-spin 0.7s linear infinite;
}

/* ---------- 统一加载态（v0.9.18）----------
   替代散落各页的「加载中…」「读取中…」纯文本。
   默认居中成块（主内容区用）；.is-line 贴左一行（小栏目 / 弹窗列表用）。 */
.loading {
  display: flex; align-items: center; justify-content: center; gap: 0.5rem;
  padding: 1.5rem 1rem; margin: 0;
  font-size: var(--vibe-fs-base); color: var(--vibe-muted-foreground);
}
.loading .spinner { width: 0.9rem; height: 0.9rem; color: var(--vibe-brand-400); }
.loading.is-line { padding: 0.5rem 0; justify-content: flex-start; }

/* ---------- 骨架屏（v0.9.18）----------
   关键列表页（学生 / 成绩）首次加载用：比转圈更能预告「内容马上来、大概长这样」。
   用法：<div class="skeleton-row"><span class="skeleton skeleton-bar" style="width:6rem"></span>…</div> */
.skeleton {
  display: block;
  background: linear-gradient(90deg,
    color-mix(in srgb, var(--vibe-muted) 78%, transparent) 25%,
    color-mix(in srgb, var(--vibe-border) 60%, transparent) 37%,
    color-mix(in srgb, var(--vibe-muted) 78%, transparent) 63%);
  background-size: 400% 100%;
  animation: bzr-skeleton 1.4s ease infinite;
  border-radius: var(--vibe-radius-2xs);
}
@keyframes bzr-skeleton {
  0% { background-position: 100% 50%; }
  100% { background-position: 0 50%; }
}
.skeleton-row {
  display: flex; align-items: center; gap: 0.6rem;
  padding: 0.5rem 0.15rem;
}
.skeleton-row + .skeleton-row { border-top: 1px solid color-mix(in srgb, var(--vibe-border) 40%, transparent); }
.skeleton-bar { height: 0.75rem; }
.skeleton-avatar { width: 1.75rem; height: 1.75rem; border-radius: 999px; flex-shrink: 0; }
/* 骨架卡片：网格布局的列表页用（放进已有的网格容器里会自动分列）。
   结构：<div class="skeleton-card"><span class="skeleton skeleton-avatar"></span>
           <div class="sk-main"><span class="skeleton skeleton-bar"></span>…</div></div> */
.skeleton-card {
  display: flex; align-items: flex-start; gap: 0.85rem;
  background: var(--vibe-card);
  border: 1px solid var(--vibe-border);
  border-radius: var(--vibe-radius-sm);
  padding: 1rem 1.1rem;
}
.sk-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.45rem; }
@media (prefers-reduced-motion: reduce) {
  .skeleton { animation: none !important; background-position: 0 50%; }
}

/* ---------- 侧边栏固定在视口内（仅桌面端）----------
   长页面滚动时，侧栏不再被内容拉长，底部「收起侧边栏」始终可见 */
@media (min-width: 768px) {
  aside {
    position: sticky;
    top: 0;
    align-self: flex-start;
    height: 100vh;
  }
}

/* ---------- 侧边栏折叠（仅桌面端） ---------- */
@media (min-width: 768px) {
  html.sidebar-collapsed aside {
    width: 56px !important;
    transition: width 0.2s ease;
    overflow: hidden;
  }
  /* logo 区：文字隐藏、图标居中 */
  html.sidebar-collapsed aside > div:first-child {
    padding: 0.6rem 0.25rem !important;
    display: flex; justify-content: center;
  }
  html.sidebar-collapsed aside > div:first-child .logo-word { display: none !important; }
  /* 导航区：纯图标居中 */
  html.sidebar-collapsed aside nav { padding-left: 0.25rem !important; padding-right: 0.25rem !important; }
  html.sidebar-collapsed aside nav a,
  html.sidebar-collapsed aside nav > div {
    justify-content: center !important;
    padding-left: 0.25rem !important; padding-right: 0.25rem !important;
  }
  html.sidebar-collapsed aside nav a span,
  html.sidebar-collapsed aside nav > div span { display: none !important; }
  /* 用户区：只保留头像居中（v0.10.21 修）----------
     ⚠️ 原来这里只写了 `p, button { display: none }`，但结构里还有两样东西没管：
       ① 那个「设置」齿轮是个 <a>（漏在规则外）—— 已从 13 个页面的 HTML 里删掉；
       ② **文字块本身是个 div**，里面的 p 藏了它还在，而且挂着 flex-1 继续占位。
     算一下就知道为什么会挤：折叠后侧栏 56px、用户区可用 48px，
     里面要装 px-3(24) + gap-3(12) + 头像 32 + 齿轮 16 = 84px —— 装不下，
     头像的 flex-shrink 默认是 1，于是它被压成 16px 宽的**竖条**（就是那个橙色竖胶囊）。
     所以：文字块整块收掉 + 清掉内边距和 gap + 头像禁止收缩。 */
  html.sidebar-collapsed aside .sidebar-user { padding-left: 0.25rem !important; padding-right: 0.25rem !important; }
  html.sidebar-collapsed aside .sidebar-user > div {
    justify-content: center !important;
    gap: 0 !important;
    padding-left: 0 !important; padding-right: 0 !important;
  }
  /* :not(#user-avatar) —— 头像自己也是个 div，不能一起藏掉 */
  html.sidebar-collapsed aside .sidebar-user > div > div:not(#user-avatar) { display: none !important; }
  html.sidebar-collapsed aside .sidebar-user #user-avatar { flex-shrink: 0; }
  /* 折叠按钮：居中、隐藏文字、图标翻转 */
  html.sidebar-collapsed aside .sidebar-collapse button { justify-content: center !important; }
  html.sidebar-collapsed aside .sidebar-collapse button span { display: none !important; }
  html.sidebar-collapsed #sidebar-toggle svg { transform: rotate(180deg); transition: transform 0.2s ease; }
  /* 分组标题（v0.10.19）：折叠后那条窄轨里塞不下「学生 / 班级 / 协同 / 我的」这几行字，
     会挤成一团。上面的 `nav > div span` 挡不住它 —— 那几行是 div 里的**纯文本**，没有 span。
     折叠态改成一条细分隔线：分组关系不丢，也不占高度。
     ★ v0.18.17：这里必须把吸顶和负外边距都撤掉 —— 桌面态的 `.nav-group-title` 现在
       是 sticky + 左右负外边距（见下面 `@media (min-width: 768px)` 里那条）。
       ⚠️ 不撤负外边距的话，这条 1px 分隔线会往左溢出窄轨（-0.75rem 抵消再被
          `padding: 0.5rem` 顶出来 ⇒ 起点在 nav 边框外）。 */
  html.sidebar-collapsed aside .sidebar-nav .nav-group-title {
    font-size: 0 !important;
    padding: 0.45rem 0.5rem !important;
    position: static !important;
    margin: 0 !important;
  }
  html.sidebar-collapsed aside .sidebar-nav .nav-group-title::after {
    content: ''; display: block; height: 1px; background: var(--vibe-nav-line);
  }
}
@media (max-width: 767px) {
  #sidebar-toggle { display: none; }
}

/* ---------- 侧边栏导航：分组标题 / 移动端短标签 ---------- */
aside nav .nav-group-title {
  font-size: var(--vibe-fs-xs);
  color: var(--vibe-muted-foreground);
  padding: 0.5rem 0.75rem 0.25rem;
}
/* 移动端底栏用的 2 字短标签，桌面端不显示 */
aside nav .nav-label-mobile { display: none; }

/* ---------- 桌面侧栏：深色底 + 当前页高亮（v0.10.17 建高亮 · v0.10.19 改深色）----------
   为什么改深色：上一版把**内容区底色**压深之后，侧栏还是白的（bg-card），
   两者明度仍然接近 —— 等于只做了半套。深色侧栏一次性把「导航 / 内容」分开，
   而且让整页有重量（参照「高校教师工作台」那张图的做法，但换成暖黑棕，不抄它的墨绿）。

   ⚠️ 侧栏的 HTML 是**14 个页面各一份**（每页一段 <aside>），所以这里一律用
      `aside ...` 作用域 + !important 覆盖 Tailwind 的 bg-card / text-muted-foreground，
      改一处 14 页同时生效 —— 不要去动那 14 份 HTML。
   ⚠️ 颜色全部走 --vibe-nav-*（见文件头的令牌），换色只改令牌。
   ⚠️ 当前项左侧那条主色竖条用 ::before **绝对定位、不加内边距** ——
      加了文字会被往右推，跟其它导航项对不齐。 */
@media (min-width: 768px) {
  aside {
    background: var(--vibe-nav-bg) !important;
    border-right: 1px solid var(--vibe-nav-line) !important;
    box-shadow: none !important;
  }
  /* 分区线（logo 下 / 用户区上 / 折叠按钮上）*/
  aside > div:first-child,
  aside .sidebar-user,
  aside .sidebar-collapse { border-color: var(--vibe-nav-line) !important; }

  /* Logo 区 */
  aside .logo-name { color: var(--vibe-nav-fg) !important; }
  aside .logo-name .text-primary { color: var(--vibe-brand-400) !important; }
  aside .logo-sub { color: var(--vibe-nav-muted) !important; }

  /* ★ v0.18.17：吸顶要贴到「真正的顶端」，得先把 nav 的 `py-4` 上间距挪走。
     ⚠️ 为什么非挪不可：sticky 的盒子**不许越出包含块**（= nav 的内容盒），
        而 nav 有 `padding-top: 1rem` ⇒ 分组标题最多只能停在 y = 16px 处，
        它上面永远留一条 16px 的缝，滚过去的导航项会从那道缝里露出半截字。
        （这不是推测：第一版就是这么写的，截图里能直接看到一条被切掉的行。）
     解法：`padding-top` 归零 —— 内容盒顶端 == nav 顶端 ⇒ 标题能停到 y = 0，把缝盖住；
     那 16px 的间距改由**第一个导航项自己的 `margin-top`** 承担，视觉位置与改动前一致。
     ⚠️ 可以只写 `> :first-child`：Tailwind 的 `space-y-1` 是
        `:not([hidden]) ~ :not([hidden])`，它本来就不给第一个兄弟加 margin，正好空着。
     ⚠️ 只放桌面段：手机上 nav 是底栏（分组标题 display:none），动它只会把底栏顶偏。 */
  aside .sidebar-nav { padding-top: 0 !important; }
  aside .sidebar-nav > :first-child { margin-top: 1rem !important; }

  /* 分组标题
     ★ v0.18.17 吸顶（照 WorkBuddy 的 `.nav-group-label` 那招）：
     侧栏现在 14+ 项、自己 `overflow-y-auto` 滚，滚下去就不知道自己在哪一组了，
     所以让分组标题贴在滚动容器顶端。
     ⚠️ 底色**必须**用 --vibe-nav-bg：深色侧栏是上面 `aside{background:…}` 给的，
        这里若写成 --vibe-card，深底上会贴出一块浅补丁。
     ⚠️ 侧栏的 HTML 是 14 个页面各一份、`px-3`（0.75rem）挂在 `<nav>` 上，
        不能在 HTML 里删；所以标题要**左右各负 0.75rem 抵消掉 nav 的内边距、
        再用 1.5rem 内边距补回来** —— 否则滚上来的导航项会从标题左右两侧露出来。
        文字位置与改动前完全一致（0.75 + 0.75 = 1.5）。
     ⚠️ z-index:2 得有：`.nav-item` 是 `position:relative`，不给层级的话
        同层级里后出现的项会盖在标题上。
     ⚠️ 折叠态那条 `html.sidebar-collapsed …` 里已把 sticky + 负边距撤掉。 */
  aside nav .nav-group-title {
    color: color-mix(in srgb, var(--vibe-nav-muted) 72%, transparent) !important;
    position: sticky; top: 0; z-index: 2;
    background: var(--vibe-nav-bg);
    margin-left: -0.75rem; margin-right: -0.75rem;
    padding-left: 1.5rem; padding-right: 1.5rem;
  }

  /* 导航项：默认 / 悬停 / 当前 */
  aside .sidebar-nav .nav-item { position: relative; color: var(--vibe-nav-muted) !important; }
  aside .sidebar-nav .nav-item:hover {
    background: var(--vibe-nav-hover) !important;
    color: var(--vibe-nav-fg) !important;
  }
  aside .sidebar-nav .nav-item.nav-current {
    background: var(--vibe-nav-active-bg) !important;
    color: var(--vibe-nav-active-fg) !important;
    font-weight: 600;
  }
  aside .sidebar-nav .nav-item.nav-current::before {
    content: ''; position: absolute; left: 0; top: 50%; transform: translateY(-50%);
    width: 3px; height: 1.15rem; border-radius: 0 3px 3px 0;
    background: var(--vibe-brand-400);
  }
  /* 侧栏里的「管理类」项（邀请码 / 用户 / 学校）也用同一套灰，别因为 hidden 类漏色 */
  aside .sidebar-nav .nav-item.hidden { color: var(--vibe-nav-muted) !important; }

  /* 用户区 + 折叠按钮 */
  aside .sidebar-user #user-name { color: var(--vibe-nav-fg) !important; }
  aside .sidebar-user p.text-xs { color: var(--vibe-nav-muted) !important; }
  aside .sidebar-user a { color: var(--vibe-nav-muted) !important; }
  aside .sidebar-user a:hover { color: var(--vibe-nav-fg) !important; }
  aside .sidebar-collapse button { color: var(--vibe-nav-muted) !important; }
  aside .sidebar-collapse button:hover {
    background: var(--vibe-nav-hover) !important;
    color: var(--vibe-nav-fg) !important;
  }
}

/* 「教务」的入口卡片页 `edu.html` **只在手机上**出现在导航里（v0.12.5）——
   手机底栏点「教务」→ 一排卡片（成绩分析 / 排课表 / 教师名册）→ 再点进去，
   跟「班级」那一格的做法完全一致（用户 2026-09-17：「**或者能不能和班级一样需要再点一下**」）。
   ⚠️ 宽屏不需要它：桌面侧栏本来就把那三项直接列出来了，再多一个「教务」入口是同义反复。 */
@media (min-width: 768px) {
  aside .sidebar-nav #nav-edu-hub { display: none !important; }
}

/* ---------- 移动端导航：侧栏转为「悬浮圆角条」（≤767px）----------
   与桌面端共用同一份 aside/nav，仅定位与排布不同。
   v0.9.21：按用户提供的三张参考图（微信 / 高德 / Apple Music 的 iOS 26 浮层导航）
   从「贴底满宽」改回「悬浮圆角条」，并解决两个硬要求：
     ① 「千万不能小」—— 图标 27px（原 20px）、tab 内容区 56px、标签 11px
     ② 「不能和系统最底部横条打架」—— bottom 用 max() 兜底：
        iPhone 有安全区（34px）→ 离底 34+10=44px；Android 安全区为 0 →
        强制留在 24px，绝不会贴到手势条上。老浏览器不支持 max() 时回落到 1.5rem。
   浮层形态：左右各留 12px、圆角 2rem、半透明材质 + 24px 模糊、向下软投影。
   ⚠️ 改这里的高度要同步回看两处：本块末尾的 main 底部留白、以及上方 PWA 横幅的 bottom。 */
@media (max-width: 767px) {
  aside {
    position: fixed !important;
    left: 0.75rem !important;
    right: 0.75rem !important;
    bottom: 1.5rem !important;   /* 兜底：不支持 max() 时也离底 24px */
    /* 关键：安全区为 0 的机型（多数安卓）也强制留 24px，避免和系统手势条叠在一起 */
    bottom: max(calc(env(safe-area-inset-bottom, 0px) + 0.625rem), 1.5rem) !important;
    top: auto !important;
    width: auto !important;
    height: auto !important;
    min-height: 0 !important;
    flex-direction: row !important;
    align-items: stretch !important;
    gap: 0 !important;
    padding: 0.25rem !important;
    border: 1px solid var(--vibe-nav-line) !important;
    border-radius: 2rem !important;
    /* v0.10.19：跟随桌面侧栏一起改深色 —— 同一个 <aside>，两边不一致会像两个 App。
       仍然保留半透明 + 模糊（深色玻璃），底下的内容会透一点出来，浮层感更强。 */
    background: color-mix(in srgb, var(--vibe-nav-bg) 92%, transparent) !important;
    -webkit-backdrop-filter: blur(24px) saturate(1.7);
    backdrop-filter: blur(24px) saturate(1.7);
    box-shadow: 0 12px 32px rgba(30, 22, 16, 0.32), 0 2px 8px rgba(30, 22, 16, 0.16) !important;
    overflow: hidden;
    z-index: 60;
  }
  /* 底栏不显示 logo 区 / 用户区 / 折叠按钮 / 分组标题 */
  aside > div:first-child,
  aside .sidebar-user,
  aside .sidebar-collapse,
  aside .sidebar-nav .nav-group-title { display: none !important; }

  aside .sidebar-nav {
    display: flex !important;
    flex-direction: row !important;
    justify-content: space-around !important;
    align-items: stretch !important;
    flex: 1 !important;
    width: 100% !important;
    padding: 0 !important;
    gap: 0 !important;
    overflow: visible !important;
  }
  /* v0.16.0 起底栏只放 4 个主域入口（今日 / 我的班 / 协同 / 我的，教务按权限多一格）；
     座位表/值日表/课表/积分/班费/作业订正/邀请码/用户管理都只在桌面侧栏。 */
  aside .sidebar-nav .nav-item:not(.nav-tab) { display: none !important; }
  /* ★ v0.15.68：**`.hidden` 必须能压过上一条 `.nav-tab` 的 `display:flex !important`**。
     原来压不过 ⇒ "没权限的页签"（协同考勤 / 教职工名册在 HTML 里就带 `hidden`）
     在手机底栏会**先亮一下**，等 /api/auth/me 回来才被 JS `remove()` 掉 ——
     用户看到的就是「点一下导航栏闪一下，本来没有考勤的还先出现一下再消失」。
     用 `.nav-tab.hidden`（两个类）把优先级抬到 `.nav-tab`（一个类）之上：两边都是 !important 时高的赢。 */
  aside .sidebar-nav .nav-tab.hidden { display: none !important; }
  aside .sidebar-nav .nav-tab {
    display: flex !important;
    flex: 1 1 0 !important;
    flex-direction: column !important;
    align-items: center !important;
    justify-content: center !important;
    gap: 4px !important;
    margin: 0.25rem 0.15rem !important;   /* 留出选中药丸的边距，别贴到圆角 */
    min-height: 56px !important;          /* 内容区够高，指针/拇指都好吃到 */
    padding: 0.25rem 0.15rem !important;
    border-radius: 1rem !important;
    background: transparent !important;
    font-size: var(--vibe-fs-xs) !important;   /* 11px，比 iOS 的 10pt 略大，小屏也能看清 */
    font-weight: 500 !important;
    line-height: 1.2 !important;
    white-space: nowrap !important;
    color: var(--vibe-nav-muted) !important;   /* 深色底上的未选中态（v0.10.19） */
    -webkit-tap-highlight-color: transparent;   /* 去掉系统默认的灰块，换我们自己的按下反馈 */
    transition: background-color var(--vibe-dur-fast) var(--vibe-ease),
                color var(--vibe-dur-fast) var(--vibe-ease),
                transform var(--vibe-dur-fast) var(--vibe-ease);
  }
  /* 「千万不能小」：图标 27px（桌面端 18px、上一版 20px → 25px → 现在 27px） */
  aside .sidebar-nav .nav-tab i,
  aside .sidebar-nav .nav-tab svg { width: 27px !important; height: 27px !important; }
  aside .sidebar-nav .nav-tab .nav-label { display: none !important; }
  aside .sidebar-nav .nav-tab .nav-label-mobile { display: inline !important; }
  /* 当前页高亮：亮主色文字 + 加粗 + 半透明主色药丸底（对齐参考图里的选中态）
     ⚠️ 深色底上要用 brand-400（#ff845f）而不是 brand-500 —— 后者在暖黑棕上偏暗、糊。 */
  aside .sidebar-nav .nav-tab.nav-current,
  aside .sidebar-nav .nav-tab.nav-tab-active-mobile {
    color: var(--vibe-nav-active-fg) !important;
    background: var(--vibe-nav-active-bg) !important;
    font-weight: 600 !important;
  }

  /* ↓↓↓ 按下反馈（v0.9.22）↓↓↓
     这是「跳转真正发生之前」唯一能给人的即时反馈，所以做得明确：
     整格缩小 + 药丸底色变深 + 图标反向放大（按下时更"实"）。
     ⚠️ 这两条必须放在上面「当前页高亮」之后 —— 选择器特异度相同，靠顺序取胜。 */
  aside .sidebar-nav .nav-tab:active {
    transform: scale(0.92) !important;
    background: color-mix(in srgb, var(--vibe-brand-400) 26%, transparent) !important;
    transition-duration: 60ms !important;   /* 按下要跟手，不能有延迟感 */
  }
  aside .sidebar-nav .nav-tab:active i,
  aside .sidebar-nav .nav-tab:active svg {
    transform: scale(1.18) !important;
    transition: transform 60ms ease-out;
  }

  /* 内容区底部留白：让开悬浮条（条高 ≈72px + 离底 24~44px + 一点呼吸） */
  body:has(aside) main {
    padding-bottom: 7rem !important;
    padding-bottom: max(calc(env(safe-area-inset-bottom, 0px) + 6rem), 7rem) !important;
  }
}

/* ---------- 页面切换过渡 ----------
   v0.9.22 建 · v0.10.2 重做成「离开 + 到达」两半 · v0.10.4 首选交给浏览器原生
   本项目的导航是**整页跳转**（每页一个独立 HTML）。硬切的根源不只是「没动画」，
   而是「旧页已经消失、新页的 HTML 还在路上」的那段空白 —— 靠 CSS 动画盖不住。
   所以现在分两档：

   ① 首选 · 跨文档视图过渡（浏览器原生：Chrome/Edge 126+ / Safari 18.2+ / Firefox 139+）
      `@view-transition { navigation: auto }` 让浏览器**一直显示旧页画面**，直到新页
      首帧就绪，再整体交叉淡化 —— 中间那段空白彻底不存在，动效也跑在合成线程上。
      · 侧栏 / 手机底栏额外点名 bzr-shell：两页之间它只有「高亮项」不同，
        跟着一起淡会出现两个高亮的重影，所以让它直接以新页的样子呈现（下面两条）。
      · ⚠️ 走这一档时 common.js **不再**自己先把内容淡掉 —— 否则浏览器拿到的旧页快照
        是「已经淡没了的内容」，等于白等一轮。新页自带的入场动画也被 .bzr-vt 关掉，
        避免「交叉淡化 + 上浮」两层动效打架（.bzr-vt 由 pagereveal 打上）。
   ② 兜底 · 自己做的「离开 + 到达」（不支持视图过渡的浏览器 / 系统开了减少动态效果）
      · 离开 → common.js 给 <html> 加 .bzr-leaving，内容淡出并轻微上移
      · 到达 → 新页内容壳淡入 + 从下往上 10px 浮起（下面的 bzr-page-in）

   两档共同的作用对象 =「内容壳」：桌面侧栏 / 手机底栏都**留在原地不动**，只有内容在换。
   这样读起来像「换了一页内容」，而不是「整块被掀掉」。
     · 7 个页面：内容壳就是 <main>（底栏与 main 同级）
     · 今日 / 我的：底栏嵌在 main 里，内容壳是 main 内部那个不含底栏的 div（.page-body）
   ⚠️ 将来把这两页的外壳统一了（AGENTS §八 的「外壳漂移」待办），
      第二组选择器 `body:has(aside) .page-body` 就可以删掉。
   ⚠️ 只动内容壳、绝不动 main，还有一个原因：给 main 加 transform 会让它临时成为
      fixed 后代的包含块 —— v0.9.23 就是因为动了 main，导致底栏 position:fixed
      以 main 而非视口为参照而瞬间跑偏（用户看到「点今日/我的会闪一下」）。
      现在 main 完全不参与动画，这个坑自然不存在了。
   ⚠️ toast / 弹窗 / 进度条 / 安装横幅 都在 </main> 之后或由 JS 挂到 body —— 不受影响。 */

/* ===== ① 浏览器原生跨文档视图过渡（不认这个 at-rule 的浏览器整段忽略，自动落到 ②） ===== */
@view-transition { navigation: auto; }
aside { view-transition-name: bzr-shell; }
/* 侧栏 / 底栏：不参与淡化，直接呈现新页快照（位置由 group 的恒等变换保持原地） */
::view-transition-old(bzr-shell) { animation: none; opacity: 0; }
::view-transition-new(bzr-shell) { animation: none; opacity: 1; }
/* 这次到达由浏览器交淡化，新页别再叠一遍入场动画 */
html.bzr-vt body:has(aside) main:not(:has(aside)),
html.bzr-vt body:has(aside) .page-body { animation: none; }

/* ===== ② 兜底：自己做的「离开 + 到达」 ===== */
@keyframes bzr-page-in {
  from { opacity: 0; transform: translateY(10px); }
}
/* ⚠️⚠️ 这个动画**别写回 scale(0.88) + cubic-bezier(0.34,1.56,0.64,1)**（v0.13.2 改）：
   那是"缩 12% 再弹过头 1.19%"的橡皮筋效果，而它挂在 `.nav-current` 上 ——
   **每翻一次页都会来一下**（点当前那格也会重播），用户 2026-09-18 的原话是
   「点一下今日就会这个鬼畜地弹一下，这个很怪」「底下那几个图标都会那个弹一下、闪一下，
   就很怪，很不舒服」。实测原始数值：当前格 61.6px → 54.21（−12.0%）→ 过冲 62.33（+1.19%）→ 61.6。
   现在只留一个"轻轻落下"的暗示：缩 4%、**不过冲**、160ms。
   如果你觉得连这个都多余，把下面那条 media query 整块删掉即可（不要只删 keyframes）。 */
@keyframes bzr-tab-pop {
  from { transform: scale(0.96); }
}
/* 默认只动页面外壳，不自动接管里面的新按钮、卡片。
   有自己入场状态机的页面用 data-bzr-enter-owner 显式声明所有权，通用入场就不再叠加；
   transition 仍保留给统一的 .bzr-leaving 离场使用。 */
body:has(aside) main:not(:has(aside)):not([data-bzr-enter-owner]),
body:has(aside) .page-body:not([data-bzr-enter-owner]) {
  animation: bzr-page-in 260ms var(--vibe-ease-out);
}
body:has(aside) main:not(:has(aside)),
body:has(aside) .page-body {
  transition: opacity 140ms var(--vibe-ease), transform 140ms var(--vibe-ease);
}
/* 离开中：内容淡出 + 轻微上移；底栏、侧栏不动 */
.bzr-leaving main:not(:has(aside)),
.bzr-leaving .page-body {
  animation: none;
  opacity: 0;
  transform: translateY(-8px);
}
@media (max-width: 767px) {
  /* 选中页签轻轻落一下 —— 读起来像「选中状态挪到了这里」。
     ⚠️ 参数改动理由见上面 @keyframes bzr-tab-pop 的注释（原来的橡皮筋回弹已去掉）。 */
  aside .sidebar-nav .nav-tab.nav-current,
  aside .sidebar-nav .nav-tab.nav-tab-active-mobile {
    animation: bzr-tab-pop 160ms ease-out;
  }
}
@media (prefers-reduced-motion: reduce) {
  body:has(aside) main:not(:has(aside)),
  body:has(aside) .page-body,
  .bzr-leaving main:not(:has(aside)),
  .bzr-leaving .page-body,
  aside .sidebar-nav .nav-tab.nav-current,
  aside .sidebar-nav .nav-tab.nav-tab-active-mobile { animation: none !important; }
  body:has(aside) main:not(:has(aside)),
  body:has(aside) .page-body { transition: none !important; }
  .bzr-leaving main:not(:has(aside)),
  .bzr-leaving .page-body { opacity: 1 !important; transform: none !important; }
  aside .sidebar-nav .nav-tab:active,
  aside .sidebar-nav .nav-tab:active i,
  aside .sidebar-nav .nav-tab:active svg { transform: none !important; }
  #notif-btn.is-ringing svg { animation: none !important; }
  .class-menu { animation: none !important; }
  /* 系统开了「减少动态效果」就整档关掉浏览器原生过渡（含交叉淡化） */
  @view-transition { navigation: none; }
}

/* ---------- 版本标签（logo 旁） ---------- */
.version-tag {
  display: inline-block;
  margin-left: 0.5rem;
  padding: 0.1rem 0.5rem;
  border-radius: 999px;
  font-size: var(--vibe-fs-2xs);
  font-weight: 600;
  line-height: 1.5;
  vertical-align: middle;
  background: var(--vibe-muted);
  color: var(--vibe-muted-foreground);
  letter-spacing: 0.01em;
}

/* ---------- 考勤统计筛选栏 ---------- */
#stats-filters {
  font-family: var(--vibe-font-sans);
}
#stats-filters .filter-pill {
  font-family: var(--vibe-font-sans);
}
#stats-filters input[type="date"] {
  font-family: var(--vibe-font-sans);
}
#stats-filters input[type="date"]::-webkit-calendar-picker-indicator {
  cursor: pointer;
  opacity: 0.6;
}

/* ---------- 趋势图 ---------- */
#stats-trend svg {
  width: 100%;
  height: auto;
}
#stats-trend polyline {
  transition: none;
}

/* ---------- 教师点击提示 ---------- */
[onclick*="showTeacherDetail"] {
  text-decoration: underline;
  text-decoration-style: dotted;
  text-underline-offset: 2px;
  text-decoration-color: var(--vibe-primary);
  transition: color 0.15s;
}
[onclick*="showTeacherDetail"]:hover {
  color: var(--vibe-accent) !important;
}

/* ---------- 教师详情弹窗内表格 ---------- */
#teacher-detail-body table {
  font-family: var(--vibe-font-sans);
}
#teacher-detail-body th {
  font-family: var(--vibe-font-sans);
  background: var(--vibe-card);
}

/* ---------- 统计到岗率进度条 ---------- */
.stats-rate-bar {
  display: inline-block;
  width: 36px;
  height: 5px;
  background: var(--vibe-muted);
  border-radius: 2px;
  vertical-align: middle;
  margin-right: 4px;
  overflow: hidden;
}

/* ---------- 无障碍：尊重系统"减少动态效果"偏好 ---------- */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}

/* ===== ★ v0.15.62 补：手机触控尺寸里**会被后面基础规则盖掉的两条**，放这里稳压 =====
   ⚠️ 教训：这一版的触控尺寸块写在文件中间（2140 行附近），其中两条**没生效** ——
     `theme.css` 里 CSS 是"同权重后写者赢"，而它们的基础规则都在**后面**：
       · `.kd-cal-nav` 的宽高在 2385 行附近  ⇒ 改出来还是 24×24
       · `.todo-add input` 的 `flex: 1` 在 2475 行附近 ⇒ 输入框没独占一行、三个控件还挤一块
   所以这两条必须放在**文件末尾**。
   ⚠️ 以后往 theme.css 加"覆盖型"规则，先 grep 一下要覆盖的选择器在不在它后面；
     在的话就挪到文件末尾（或者直接改那条基础规则，别留两份）。
   （其余触控规则 —— `.btn` 的 min-height、`.bzr-mic.is-mini` 的尺寸、几个 `::after` 命中区 ——
     实测都生效了：它们要么在文件里没有同名基础规则，要么基础规则不涉及这些属性。） */
@media (max-width: 767px) {
  .kd-cal-nav { width: 2.75rem; height: 2.75rem; }
  .todo-add input { flex: 1 1 100%; }
}

/* ===== ★ v0.15.63 补：待办那一行，「🎤」和「添加」**等宽**了 =====
   用户 2026-09-23 截图反馈：「语音按钮那小一个」。
   病根在上一版：我把「添加」撑成了 `flex: 1 1 auto`（占满剩余），而 🎤 还是 44×44 的小圆圈 ——
   并排放在一起，一个大红块 + 一个小圆圈，看着像没做完。
   现在两个都 `flex: 1 1 0`（各占一半），并且 🎤 **带上文字「按住说」**：
     · 视觉上两边重量相等，不偏；
     · 顺手解决"光一个麦克风图标，没人知道要**按住**"（这是长按控件，不是点一下）。
   ⚠️ 放文件末尾的原因见上面那条（`.bzr-mic.is-mini` / `.todo-add .todo-add-btn` 的基础规则都在前面）。 */
@media (max-width: 767px) {
  .todo-add .bzr-mic.is-mini {
    /* ⚠️ `box-sizing: border-box` 不能少：`.bzr-mic` 默认是 content-box（1px 边框 + 左右内边距），
       而 `flex-basis: 0` 对 content-box 说的是"内容宽 0" ⇒ 它的外框天生比无边框的「添加」大一截，
       两个 `flex: 1 1 0` 分下来会差 6px（实测 162 vs 168，就是这 6px 的锅）。 */
    box-sizing: border-box;
    flex: 1 1 0; width: auto; min-width: 0; height: 2.75rem;
    padding: 0 0.6rem; justify-content: center;
  }
  .todo-add .bzr-mic.is-mini .bzr-mic-txt { display: inline; }
  /* ⚠️ 实测：两边都 `flex: 1 1 0` 之后仍然差 6px（162 vs 168）。
     我把 `box-sizing` / `min-width` / `flex-grow` / `flex-basis` **都对过了，四处完全一样**，
     没定位到那 6px 的来源（不值当继续查：肉眼看不出来，而用户抱怨的是当初 44 vs 200 那种量级）。
     `box-sizing: border-box` 和 `min-width: 0` 留着没错（flex 里本来就该显式写）。
     哪天要写"绝对等宽"的并排按钮，别用 `flex: 1 1 0` 猜，直接 `width: calc((100% - gap) / 2)`。 */
  .todo-add .todo-add-btn { box-sizing: border-box; flex: 1 1 0; min-width: 0; }
}

/* ===== ★ v0.18.93 手机触控尺寸「补漏」：把 v0.15.62 当年没覆盖到的小目标补齐 =====
   起因：用户 2026-09-30 收了一份《AI 设计守则》（项目根目录 DESIGN.md），§5 要求
   「按钮 / 可点区域 ≥ 44×44px（移动端）」。v0.15.62 已经做过一轮，但只覆盖了
   `.btn` 家族 + 待办那几颗；这一版把**当年漏掉的那些类**补齐。
   做法**完全照 v0.15.62 那两条口径**（不发明新招）：
     ① 该长高的（成排的 tab / chip / 次级按钮）：直接 `min-height: 2.75rem`；
     ② 小图标 / 纯文字链：**视觉一个像素都不变**，只加一层透明 `::after` 把命中区撑到 44。
   ⚠️ **只撑竖直方向的 44 —— 横向一律不撑**（往外撑宽最容易抢左右邻居的点击，v0.10.35 栽过）。
   ⚠️ 写在**文件末尾**：这些选择器的基础规则都在前面，同权重后写者赢（v0.15.62 的教训）。
   ⚠️ **两个仍然不改的例外**（是量出来的，不是漏做的）：
     · `.kd-cal-day`（48×27）：密集网格型目标通行做法允许小一点，抬到 44 会让 6 行日历多出 100px；
     · 原生 `select` / `input`（`.form-control` / `.sa-input` 等，38px 高）：高度由行内表单布局决定，
       抬起来会带着整行（乃至录分那种密表）一起变；浏览器对原生控件本来就有系统级命中判定。
       ⇒ 这条已写进 AGENTS 的设计守则例外清单。
   ⚠️ **页头那三颗 `.head-icon-btn` 这次给了覆盖层**：v0.15.62 把它们列为"保持 40×40"的例外
      （加到 44 会让那排宽 12px、挤掉日期条）—— 那是**改盒子尺寸**的后果；
      而覆盖层是 `position: absolute` 的、**完全不占布局**，所以既不破那条实测结论、
      又把命中区给到了 44。 */
@media (max-width: 767px) {
  /* ① 该长高的 */
  .hdr-link { display: inline-flex; align-items: center; min-height: 2.75rem; }
  .page-act { min-height: 2.75rem; }
  /* ⚠️ `.class-chip` **不能用 ::after 撑** —— 6 个页面（duty/grades/points/seating/students/timetable）
     各自定义了 `.class-chip.active::after { height: 2.5px }`（那是"选中下划线"），而**页面内联样式在
     同权重下赢过 theme.css** ⇒ 我的覆盖层会被它盖掉、还会跟它打架。
     它本来 95×42，长高 2px 就够（这是实测出来的，别改成 ::after）。 */
  .class-chip { min-height: 2.75rem; }
  /* 独立的表单控件：只给**表单页专用**的那两个类 + 待办输入框。
     ⚠️ **故意不含 `.form-control`** —— admin-users 的权限矩阵里有 **49 个 `.form-control` 只有 18px 高**，
        统一抬会把那张密集表撑坏。⇒ 原生控件整体豁免，已登记进 AGENTS 的设计守则例外清单。 */
  .form-input, .sa-input, #ov-todo-input { min-height: 2.75rem; }
  /* 滑块（大屏设置里的「字号」）—— 轨道整条就是命中区，抬到 44 不影响外观 */
  input[type="range"] { min-height: 2.75rem; }

  /* ② 视觉不变、只把命中区撑到 44（竖直方向） */
  .today-class-chip, .today-term-set, .head-icon-btn, .head-text-btn, .key-date-btn,
  .att-today-go, .att-today-x, .set-cat, .g-tab, .tt-day-tab, .mode-pill,
  .btn-ghost-btn, .sa-btn, .sa-mini, .chip-act, .class-add-btn, .sa-chk { position: relative; }
  .today-class-chip::after, .today-term-set::after, .head-icon-btn::after, .head-text-btn::after,
  .key-date-btn::after, .att-today-go::after, .att-today-x::after, .set-cat::after,
  .g-tab::after, .tt-day-tab::after, .mode-pill::after, .btn-ghost-btn::after,
  .sa-btn::after, .sa-mini::after, .chip-act::after, .class-add-btn::after, .sa-chk::after {
    content: ''; position: absolute; left: 0; right: 0; top: 50%; transform: translateY(-50%);
    height: max(100%, 2.75rem);
  }
  /* ★ 两颗"方按钮"宽度也不够 44 ⇒ 横向也撑（它们的左侧/上下有余量，实测不抢点击）：
       · `.head-icon-btn`：三个一排、间距 6px，各往外 2px ⇒ 只吃掉 4px；
       · `.chip-act`：它在大屏设置那个 `.class-chip`（127×61）里面，四周都是空的。
     ⚠️ **横向撑是"看邻居"的活，不能成批套** —— 我先给「不再提示」那颗 ✕ 也横撑，
        验收脚本立刻报出它**抢了隔壁「去认领」的点击**（v0.10.35 栽的同一个坑）。 */
  .head-icon-btn::after, .chip-act::after {
    left: 50%; right: auto; transform: translate(-50%, -50%);
    width: max(100%, 2.75rem);
  }
  /* ⚠️ **`.empty-line a`（空态里的文字链）故意没加** —— 实测它们**成组上下紧挨**
     （「去设置 ›」/「看本班课表 ›」），撑到 44 会互相压住、**抢对方的点击**。
     这正是 v0.10.35 栽过的那个坑，所以宁可留着：它们旁边就有一句说明，同一入口在页头也有按钮。 */
}

/* ---------- 📷 扫码弹层（★ v0.18.106 · 公共模块 `scan-qr.js` 专用）----------
   ⚠️ 原来是"两半"：结构在 `screen-admin.js`、样式在 `class.html` 的 `<style>` 里。
      抽公共模块时**整块搬到这儿**（theme.css 是全站共用表、每页都引）——
      以后**别再往某个页面的 `<style>` 里写 `.bzr-scan`**，那样别的页面打开就没样式了。 */
.bzr-scan { position: fixed; top: 0; right: 0; bottom: 0; left: 0; z-index: 60; background: rgba(0, 0, 0, 0.55);
  display: flex; align-items: center; justify-content: center; padding: 1rem; box-sizing: border-box; }
.bzr-scan-in { width: min(26rem, 100%); background: var(--vibe-card); border: 1px solid var(--vibe-border);
  border-radius: var(--vibe-radius-sm); padding: 1rem 1.1rem; }
.bzr-scan-t { font-size: var(--vibe-fs-md); font-weight: 600; color: var(--vibe-foreground); }
.bzr-scan-in video { width: 100%; aspect-ratio: 1 / 1; margin-top: 0.6rem;
  border-radius: var(--vibe-radius-sm); background: #000; object-fit: cover; }
.bzr-scan-tip { margin-top: 0.55rem; font-size: var(--vibe-fs-sm); line-height: 1.7; color: var(--vibe-muted-foreground); }
.bzr-scan-actions { margin-top: 0.9rem; display: flex; gap: 0.5rem; flex-wrap: wrap; }
.bzr-scan-btn { font-family: inherit; font-size: var(--vibe-fs-sm); cursor: pointer;
  padding: 0.45rem 1rem; border-radius: var(--vibe-radius-sm);
  border: 1px solid var(--vibe-border); background: var(--vibe-card); color: var(--vibe-foreground);
  display: inline-flex; align-items: center; position: relative; }
/* 「拍照 / 选图」是这条路上最要紧的一颗（苹果就靠它）⇒ 给它主色 */
.bzr-scan-btn-p { border-color: var(--vibe-primary); color: var(--vibe-primary); font-weight: 600; }
/* 真正的 file input **透明铺满整颗按钮**（不是 display:none）—— 小米上撞过"点按钮完全没反应"
   （见 AGENTS：透明 input 铺在按钮上是这个项目的既定做法，照抄作业订正那套 `.file-btn`）。 */
.bzr-scan-btn input[type="file"] { position: absolute; top: 0; right: 0; bottom: 0; left: 0; width: 100%; height: 100%;
  opacity: 0; cursor: pointer; }

/* @legacy-fallback:start */
/* ============================================================================
   ★★★ 老内核兜底块（**机器生成，别手改**）—— v0.20.8
   重新生成：`node scripts/gen-legacy-fallback.mjs`（改动了主题令牌或新增 color-mix 用法后跑一次）

   起因：全站有 189 处 color-mix()，而 **color-mix 要 Chromium 111+**；
   教室里有 **Windows 7** 的机器，而 **Chrome 109 是最后一个支持 Win7 的版本**。
   老内核会把带 color-mix 的那条声明**整个丢弃**：边框变没、淡底变透明、
   选中态没有底色（用户以前在闪应用容器上踩过一次，原话「框框都没了」）。

   做法：**不去改那 189 处**（改错了在 Win10 / iPad 上根本看不出来 ——
   后写的兜底会被 color-mix 那条盖掉），而是把「朴素等价颜色」集中写在这里，用 @supports 挡住：
     · 现代内核：条件成立 ⇒ `not (…)` 为假 ⇒ **整段不生效，零影响**；
     · 老内核：条件不成立 ⇒ 整段生效，把丢掉的颜色补回来。
   ⚠️ 兜底色一律写成 `var(--vibe-*)` ⇒ **深色主题自动跟着走**，不需要两套。
   ⚠️ 换算口径：
        · border / outline / color / box-shadow 等 → 取 mix 的第一个颜色、**去掉百分比**
          （「55% 的淡线」降级成「实色细线」——线还在，只是略重）；
        · background* → 有二色取第二色；只有「X P%, transparent」取 --vibe-muted
          （避免一块 7% 的淡底降级成实心大红）；
        · 跳过：@keyframes 2 处、@media 5 处、@layer/@supports 3 处
          —— 数量少、都不关键，而拆错了会生成非法 CSS，整条作废。
   ============================================================================ */
@supports not (color: color-mix(in srgb, red, blue)) {
  .apb.is-ok {
    border-color: var(--vibe-success);
  }
  .apb.is-wait {
    border-color: var(--vibe-warning);
  }
  button.danger {
    border: 1px solid var(--vibe-destructive);
  }
  button.danger:hover {
    background-color: var(--vibe-muted);
  }
  .pill.on {
    background: var(--vibe-muted);
  }
  .pill.admin {
    background: var(--vibe-muted);
  }
  .a2-btn.is-danger {
    border-color: var(--vibe-destructive);
  }
  .a2-btn.is-danger:hover {
    background: var(--vibe-muted);
  }
  .a2-chip.is-on {
    background: var(--vibe-muted);
  }
  .a2-badge.is-on {
    background: var(--vibe-muted);
  }
  .a2-badge.is-off {
    background: var(--vibe-muted);
  }
  .a2-badge.is-warn {
    background: var(--vibe-muted);
  }
  .a2-badge.is-danger {
    background: var(--vibe-muted);
  }
  .a2-badge.is-info {
    background: var(--vibe-muted);
  }
  .a2-row .ev-role {
    background: var(--vibe-muted);
  }
  .a2-anno-tx[contenteditable="true"] {
    background: var(--vibe-muted);
  }
  .a2-heat.is-best rect {
    fill: var(--vibe-success);
  }
  .a2-heat.is-good rect {
    fill: var(--vibe-success);
  }
  .a2-heat.is-mid rect {
    fill: var(--vibe-warning);
  }
  .a2-heat.is-low rect {
    fill: var(--vibe-destructive);
  }
  .a2-heat.is-worst rect {
    fill: var(--vibe-destructive);
  }
  ::selection {
    background: var(--vibe-muted);
  }
  .btn.is-danger:hover {
    background: #000;
  }
  .btn-secondary:hover {
    background: var(--vibe-border);
  }
  .pwa-hint {
    border: 1px solid var(--vibe-brand-200);
  }
  .as-item.is-read {
    background: var(--vibe-muted);
  }
  .today-class-chip {
    background: linear-gradient(135deg, var(--vibe-card) 0%, var(--vibe-card) 100%);
    border: 1px solid var(--vibe-brand-200);
  }
  .class-menu-item.is-current {
    background: var(--vibe-muted);
  }
  .today-term-set {
    border-bottom: 1px dashed var(--vibe-border);
  }
  .week-cell {
    background: var(--vibe-card);
    border-color: var(--vibe-border);
  }
  .week-cell:hover {
    border-color: var(--vibe-border);
  }
  .week-cell.is-today {
    background: var(--vibe-muted);
  }
  .week-cell.has-ev {
    border-color: var(--vibe-brand-200);
  }
  .week-cell.is-sel {
    box-shadow: 0 4px 12px var(--vibe-primary);
  }
  .week-cell.is-sel .week-dot {
    box-shadow: 0 0 0 1.5px #fff;
  }
  .preview-tag {
    background: var(--vibe-muted);
    border: 1px solid var(--vibe-primary);
  }
  .today-back {
    background: var(--vibe-muted);
  }
  .today-page-head.is-preview {
    background-image: linear-gradient(180deg, var(--vibe-card) 0%, var(--vibe-card) 90%);
  }
  .ga-row {
    border-bottom: 1px dashed var(--vibe-border);
  }
  .ga-row.is-warn .ga-tag {
    background: var(--vibe-muted);
  }
  .ga-ai {
    background: var(--vibe-muted);
    border: 1px solid var(--vibe-brand-600);
  }
  .kp-chip {
    border: 1px solid var(--vibe-brand-600);
    background: var(--vibe-muted);
  }
  .kp-chip.is-chapter {
    border-color: var(--vibe-brand-600);
    background: var(--vibe-muted);
  }
  .kp-chip.is-lost {
    border-color: var(--vibe-destructive);
    background: var(--vibe-muted);
  }
  .kp-chip[data-d] {
    border-color: var(--kp-hue);
    background: var(--vibe-muted);
  }
  .kp-chip.is-lost[data-d] {
    border-color: var(--vibe-destructive);
  }
  .drag-ph {
    border: 1.5px dashed var(--vibe-brand-500);
    background: var(--vibe-muted);
  }
  body.layout-dragging .today-col.is-off {
    border: 1.5px dashed var(--vibe-brand-500);
  }
  .tm-bar {
    background: var(--vibe-muted);
  }
  .sc-item {
    border: 1px solid var(--vibe-border);
  }
  body.home-editing .today-card {
    outline: 1.5px dashed var(--vibe-brand-500);
  }
  body.home-editing .today-col.is-off, body.home-editing .today-col-3 {
    border: 1.5px dashed var(--vibe-brand-500);
  }
  .card-remove:hover {
    border-color: currentColor;
  }
  body.home-editing .home-edit-bar {
    border-top: 1px solid var(--vibe-brand-500);
  }
  .home-add-tile {
    border: 1px dashed var(--vibe-brand-500);
  }
  .today-head h3 svg {
    background: var(--vibe-muted);
  }
  .today-head {
    border-bottom: 1px solid var(--vibe-border);
  }
  .empty-line a {
    text-decoration-color: var(--vibe-border);
  }
  .att-today-bar {
    border: 1px solid var(--vibe-brand-300);
    background: var(--vibe-card);
  }
  .att-today-bar.is-done {
    border-color: var(--vibe-success);
    background: var(--vibe-card);
  }
  .now-stat {
    border-left: 1px dashed var(--vibe-border);
  }
  .now-stat-sub {
    color: var(--vibe-muted-foreground);
  }
  .now-hero-next {
    border-top: 1px dashed var(--vibe-brand-200);
  }
  .now-chip {
    border: 1px solid var(--vibe-border);
    background: var(--vibe-muted);
  }
  .now-chip.is-now {
    background: var(--vibe-muted);
  }
  .key-date {
    border: 1px solid var(--vibe-brand-200);
    background: linear-gradient(120deg, var(--vibe-brand-50) 0%, var(--vibe-card) 100%);
  }
  .key-date-btn {
    border: 1px solid var(--vibe-brand-300);
  }
  .key-date.is-empty {
    border-color: var(--vibe-border);
  }
  .empty-block {
    border: 1px dashed var(--vibe-border);
    background: var(--vibe-muted);
  }
  .empty-btn {
    border: 1px solid var(--vibe-brand-200);
  }
  .kd-row {
    border: 1px solid var(--vibe-border);
  }
  .kd-add {
    border-top: 1px solid var(--vibe-border);
  }
  .tt-seg-line {
    background: var(--vibe-muted);
  }
  .tt-row.is-past .tt-dot {
    background: var(--vibe-muted);
  }
  .tt-row.is-now {
    background: var(--vibe-muted);
  }
  .duty-chip {
    border: 1px solid var(--vibe-border);
    background: var(--vibe-muted);
  }
  .upcoming-row + .upcoming-row {
    border-top: 1px solid var(--vibe-border);
  }
  .upcoming-kind {
    background: var(--vibe-muted);
  }
  .upcoming-row.is-view {
    background: var(--vibe-muted);
  }
  .kd-cal {
    border-bottom: 1px solid var(--vibe-border);
  }
  .my-lesson + .my-lesson {
    border-top: 1px solid var(--vibe-border);
  }
  .my-lesson.is-now .my-lesson-bar {
    box-shadow: 0 0 0 3px var(--vibe-primary);
  }
  .todo-row + .todo-row {
    border-top: 1px solid var(--vibe-border);
  }
  .todo-quick.is-on {
    background: var(--vibe-muted);
  }
  .todo-sep::before, .todo-sep::after {
    background: var(--vibe-muted);
  }
  .todo-add .todo-bell.is-on {
    background: var(--vibe-muted);
  }
  .skeleton {
    background: linear-gradient(90deg, var(--vibe-muted) 25%, var(--vibe-muted) 37%, var(--vibe-muted) 63%);
  }
  .skeleton-row + .skeleton-row {
    border-top: 1px solid var(--vibe-border);
  }
  .aseat-teacher.is-selected {
    background: var(--vibe-muted);
  }
  .aseat-selbar .a2-btn.is-on {
    background: var(--vibe-muted);
  }
  .seat.is-sel {
    background: var(--vibe-muted);
  }
  .ev-done.is-all {
    border-color: var(--vibe-success);
    background: var(--vibe-card);
  }
  .ev-done.is-miss {
    border-color: var(--vibe-destructive);
    background: var(--vibe-card);
  }
  .ev-done.is-all .ev-done-badge {
    background: var(--vibe-muted);
    border: 1px solid var(--vibe-success);
  }
  .ev-done.is-miss .ev-done-badge {
    background: var(--vibe-muted);
    border: 1px solid var(--vibe-destructive);
  }
  .ev-grp-card .a2-row .a2-st:not(.is-on) {
    background: var(--vibe-muted);
  }
  .ev-grp-sub.is-done {
    background: var(--vibe-muted);
  }
  .ev-done-name {
    background: var(--vibe-muted);
    border: 1px solid var(--vibe-destructive);
  }
  .ev-done-name:hover {
    background: var(--vibe-muted);
  }
  .ev-period.is-on {
    background: var(--vibe-muted);
  }
  .a2-matrix .mx-chip {
    background: var(--vibe-muted);
    border: 1px solid var(--mx);
  }
  .ev-streak {
    background: var(--vibe-muted);
    border: 1px solid var(--vibe-destructive);
  }
  .seat.is-alt, .seat-blank.is-alt, .seat-row-label.is-alt {
    background: var(--vibe-muted);
  }
  .seat-call.is-marked {
    background: var(--vibe-muted);
  }
  .seat-call.is-bad {
    background: var(--vibe-muted);
  }
  .seat-call.is-late {
    background: var(--vibe-muted);
  }
  .a2-st.is-bad {
    box-shadow: 0 0 0 2px var(--seat-st, var(--vibe-destructive));
  }
  .ev-signed-badge {
    background: var(--vibe-muted);
    border: 1px solid var(--vibe-success);
  }
  .score-input.is-zero {
    border-color: var(--vibe-warning);
    background: var(--vibe-muted);
  }
  #entry-wrap.is-absent-mode {
    outline: 1px dashed var(--vibe-primary);
  }
  .sig-ai {
    background: var(--vibe-muted);
  }
  .hw-card.is-sub .hw-ic {
    background: var(--vibe-muted);
  }
  details.hw-help > div {
    background: var(--vibe-muted);
  }
  .invite-banner.is-bad {
    background: var(--vibe-muted);
  }
  .ex-grid tbody tr:hover td {
    background: var(--vibe-muted);
  }
  .ex-grid td.is-hovercol, .ex-grid th.is-hovercol {
    background: var(--vibe-muted);
  }
  .ex-grid tbody tr:hover td.is-hovercol {
    background: var(--vibe-muted);
  }
  .ex-grid tbody tr:hover td:first-child {
    background: var(--vibe-card);
  }
  .ex-grid tbody tr:hover td.is-hovercol:first-child {
    background: var(--vibe-card);
  }
  .p-pill.g {
    border-color: var(--vibe-warning, #b45309);
  }
  .p-pill.n {
    border-color: var(--vibe-info, #0f766e);
  }
  .p-pill.t {
    border-color: var(--vibe-brand-600);
  }
  .p-pill.ok {
    border-color: var(--vibe-success-ink, #1e7a52);
  }
  .p-pill.no {
    border-color: var(--vibe-destructive);
  }
  .gs-row mark {
    background: var(--vibe-muted);
  }
  .plan-flag {
    background: var(--vibe-muted);
  }
  .plan-flag.is-agent {
    background: var(--vibe-muted);
  }
  .edit-item.is-danger:hover {
    background: var(--vibe-muted);
  }
  table.tt thead th {
    background: var(--vibe-card);
    border-bottom: 2px solid var(--vibe-foreground);
  }
  .tt-period-time.tt-time-same {
    color: var(--vibe-muted-foreground);
  }
  .tt-only-days {
    background: var(--vibe-muted);
  }
  .tt-cell:hover {
    background: var(--vibe-muted);
  }
  .tt-meta {
    color: var(--vibe-foreground);
  }
  .tt-note {
    color: var(--vibe-muted-foreground);
  }
  .tt-empty {
    color: var(--vibe-muted-foreground);
  }
  .tt-cell.is-override {
    box-shadow: inset 0 0 0 2px var(--vibe-warning);
  }
  table.tt th.tt-today .col-copy-btn {
    color: var(--vibe-primary-foreground);
  }
  table.tt th.tt-today .col-copy-btn:hover {
    background: var(--vibe-muted);
  }
  td.tt-common:hover {
    background: var(--vibe-muted);
  }
  .tt-group-tag {
    color: var(--vibe-foreground);
  }
  .tt-group-line {
    background: var(--vibe-muted);
  }
  .col-copy-btn {
    color: var(--vibe-muted-foreground);
  }
  .tt-fade {
    background: linear-gradient(to right, transparent, var(--vibe-muted));
  }
  .tt-stat-block + .tt-stat-block {
    border-top: 1px solid var(--vibe-border);
  }
  .tt-stat-label {
    color: var(--vibe-foreground);
  }
  .period-row .pr-btn.danger {
    border-color: var(--vibe-destructive);
  }
  .pr-day.on {
    background: var(--vibe-muted);
  }
  .claim-gbar button.is-on {
    background: var(--vibe-muted);
    border-color: var(--vibe-primary);
  }
  .mob-seg {
    color: var(--vibe-foreground);
    border-bottom: 1px solid var(--vibe-border);
  }
  .mob-row {
    border-bottom: 1px solid var(--vibe-border);
  }
  .mob-row:active {
    background: var(--vibe-muted);
  }
  .mob-empty {
    color: var(--vibe-muted-foreground);
  }
}
/* @legacy-fallback:end */

/* 今日页：补交闭环，窄屏操作按钮保留可点区域。 */
.morning-follow-row { display:flex; gap:0.75rem; align-items:center; justify-content:space-between; margin:0.4rem 0; }
.morning-follow-row span { min-width:0; overflow-wrap:anywhere; }
.morning-follow-row button { min-height:44px; flex-shrink:0; }
.morning-follow-list { display:block; width:100%; min-height:9rem; margin-top:0.75rem; font:inherit; color:var(--vibe-foreground); background:var(--vibe-card); border:1px solid var(--vibe-border); border-radius:var(--vibe-radius); padding:0.5rem; }
.morning-follow-list[hidden] { display:none; }

.morning-receipt { display:flex; flex-wrap:wrap; gap:0.5rem; align-items:center; padding:0.5rem 0; border-bottom:1px solid var(--vibe-border); }

/* 每日班务点评：复用全站令牌；事实列表和名单独立滚动。 */
.dr-box { margin-block: 1rem; padding: 1rem; border: 1px solid var(--vibe-border); border-radius: var(--vibe-radius); background: var(--vibe-card); color: var(--vibe-foreground); font-size: var(--vibe-fs-base); }
.dr-head, .dr-footer { display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:0.75rem; }
.dr-body { display:flex; flex-direction:column; gap:0.75rem; margin-top:0.75rem; }
.dr-sub, .dr-list small { color:var(--vibe-muted-foreground); font-size:var(--vibe-fs-sm); }
.dr-error { color:var(--vibe-destructive-ink); }
.dr-btn { min-height:44px; padding:0.5rem 0.75rem; border:1px solid var(--vibe-border); border-radius:var(--vibe-radius-xs); background:var(--vibe-card); color:var(--vibe-foreground); cursor:pointer; font:inherit; text-align:center; }
.dr-btn:hover { background:var(--vibe-muted); }
.dr-btn:focus-visible { outline:2px solid var(--vibe-ring); outline-offset:2px; }
.dr-btn:active { transform:translateY(1px); }
.dr-btn:disabled { opacity:0.5; cursor:default; }
.dr-btn.is-selected { background:var(--vibe-card-sub); color:var(--vibe-primary-ink); border-color:var(--vibe-primary); }
.dr-options, .dr-facts { display:flex; flex-wrap:wrap; gap:0.5rem; }
.dr-facts > .dr-btn { flex:1 1 44%; }
.dr-roster { display:grid; grid-template-columns:repeat(auto-fill,minmax(6rem,1fr)); gap:0.5rem; max-height:26vh; overflow:auto; padding:0.25rem; }
.dr-list { margin:0; padding:0; list-style:none; max-height:20vh; overflow:auto; }
.dr-list li { display:flex; justify-content:space-between; align-items:center; gap:0.75rem; padding:0.75rem 0; border-bottom:1px solid var(--vibe-border); overflow-wrap:anywhere; }
.dr-list small { display:block; }
.dr-footer { padding-block:0.75rem; border-top:1px solid var(--vibe-border); }
.dr-review { margin-top:0.75rem; padding:0.75rem; background:var(--vibe-card-sub); border-radius:var(--vibe-radius-xs); }
.dr-review summary, .dr-body summary { min-height:44px; cursor:pointer; padding-block:0.5rem; }
.dr-review .dr-list { max-height:40vh; }
.dr-pin { font-size:var(--vibe-fs-lg); letter-spacing:0.5rem; text-align:center; }
.dr-keys { display:grid; grid-template-columns:repeat(3,1fr); gap:0.5rem; max-width:20rem; align-self:center; width:100%; }

.dr-body { max-height:70vh; }
.dr-content { overflow-y:auto; min-height:0; }
.dr-footer { flex-shrink:0; background:var(--vibe-card); }

.b-pane .dr-box { font-size:var(--f-md); }
.b-pane .dr-head > b { font-size:var(--f-lg); }
.b-pane .dr-sub, .b-pane .dr-list small { font-size:var(--f-xs); }
.b-pane .dr-roster { grid-template-columns:repeat(auto-fill,minmax(9rem,1fr)); }

/* ================= 每日班务点评 · 大屏三栏（2026-10-17） =================
   只作用于 `.dr-box.dr-expanded`；老师端今日卡仍是原来的紧凑样式。
   颜色刻意沿用预览里确认的柔和紫/薄荷/浅橙，不跟页面其它橙色主色混。 */
.dr-box.dr-expanded {
  --dr-violet:#7B61FF; --dr-violet-ink:#5A44D8; --dr-violet-soft:#F0ECFF;
  --dr-mint:#42C58A; --dr-mint-ink:#1E7B50; --dr-mint-soft:#E7F9F0;
  --dr-peach:#F08A5D; --dr-peach-ink:#A64B24; --dr-peach-soft:#FFF1E9;
  --dr-ink:#17181C; --dr-muted:#727583; --dr-line:#E7E9F0; --dr-card:#FFFFFF;
  margin:0; padding:0; border:0; background:#F2F3F7; color:var(--dr-ink);
  height:100%; min-height:0; display:flex; flex-direction:column; gap:8px;
  border-radius:var(--r-card,20px); overflow:hidden;
}
#cr-main:has(.dr-box.dr-expanded) { overflow:hidden; }
#cr-main:has(.dr-box.dr-expanded) #cr-class-wrap { display:flex; flex-direction:column; flex:1; min-height:0; }
#cr-main:has(.dr-box.dr-expanded) .b-body { flex:1; min-height:0; display:flex; padding:0.35rem 0.8rem 0.8rem; }
#pane-duty:has(.dr-box.dr-expanded) { flex:1; min-height:0; display:flex; flex-direction:column; }
#pane-duty:has(.dr-box.dr-expanded) #duty-readonly,
#pane-duty:has(.dr-box.dr-expanded) #duty-edit,
#pane-duty:has(.dr-box.dr-expanded) #duty-tools { display:none; }

.dr-box.dr-expanded .dr-top { display:flex; align-items:center; justify-content:space-between; gap:10px; flex-wrap:wrap;
  padding:10px 14px; background:var(--dr-card); border:1px solid var(--dr-line); border-radius:16px; flex:none; }
.dr-top-title { display:flex; align-items:baseline; gap:10px; flex-wrap:wrap; min-width:0; }
.dr-top-title b { font-size:var(--f-md); font-weight:800; letter-spacing:.01em; }
.dr-top-actions { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
.dr-top-actions .dr-btn { min-height:38px; padding:0 11px; border-radius:10px; }
.dr-topbtn { min-height:38px; padding:0 13px; border-radius:999px; background:var(--dr-ink-soft,#EEF0F4);
  color:var(--dr-muted); font-size:var(--f-xs); font-weight:800; }
.dr-topbtn.on { background:var(--dr-ink); color:#fff; }
.dr-recorder { font-size:var(--f-xs); color:var(--dr-muted); font-weight:700; }
.dr-recorder b { color:var(--dr-ink); font-weight:800; }
.dr-box.dr-expanded .dr-body { max-height:none; flex:1; min-height:0; display:flex; flex-direction:column; gap:8px; margin-top:0; }
.dr-box.dr-expanded .dr-error { margin:0; padding:8px 12px; border-radius:10px; background:var(--dr-peach-soft);
  color:var(--dr-peach-ink); font-size:var(--f-xs); font-weight:700; }

.dr-gate { flex:1; min-height:0; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:10px;
  padding:18px; background:var(--dr-card); border:1px solid var(--dr-line); border-radius:18px; }
.dr-gate > p { margin:0; color:var(--dr-muted); font-size:var(--f-sm); font-weight:700; }
.dr-gate-hint { color:var(--dr-muted); font-size:var(--f-xs); font-weight:700; margin-top:-4px; }
.dr-gate.dr-who { align-items:stretch; justify-content:flex-start; overflow:auto; }
.dr-gate.dr-who > b { font-size:var(--f-md); font-weight:800; }
.dr-gate .dr-roster { max-height:none; flex:1; min-height:0; }
.dr-gate .dr-keys { flex:none; }
/* ★ 2026-10-18 晚：**班务总结 / 日常汇报共用的密码卡** ——
   没解锁时整页只留这一张（居中、小一点），记录内容 / 提交数 / 历史一概不露。 */
.dr-box.dr-locked { display:flex; align-items:center; justify-content:center; padding:24px;
  background:var(--v2-bg, #F2F3F7); border:0; box-shadow:none; }
.dr-gate-card { flex:0 0 auto; min-height:0; width:min(400px, 92%); padding:24px 26px; }
.dr-gate-card .dr-pin { font-size:clamp(22px, 1.6vw, 30px); }
.dr-gate-card .dr-keys { width:100%; max-width:18rem; }
.dr-gate-err { color:#A4551A; font-size:var(--f-xs); font-weight:700; }

.dr-stats { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:8px; flex:none; }
.dr-stat { padding:8px 12px; background:var(--dr-card); border:1px solid var(--dr-line); border-radius:14px;
  display:flex; align-items:baseline; gap:8px; flex-wrap:wrap; min-width:0; }
.dr-stat span { font-size:var(--f-xs); color:var(--dr-muted); font-weight:800; }
.dr-stat b { font-size:var(--f-lg); line-height:1; font-weight:800; font-variant-numeric:tabular-nums; }
.dr-stat small { font-size:var(--f-xs); color:var(--dr-muted); font-weight:700; }
.dr-stat.mint b { color:var(--dr-mint-ink); }
.dr-stat.peach b { color:var(--dr-peach-ink); }

.dr-layout { display:grid; grid-template-columns:minmax(0,.95fr) minmax(0,1.05fr) minmax(0,1.15fr);
  gap:8px; flex:1; min-height:0; }
.dr-panel { background:var(--dr-card); border:1px solid var(--dr-line); border-radius:16px;
  display:flex; flex-direction:column; min-height:0; overflow:hidden; padding:8px 10px 10px; }
.dr-panel-hd { display:flex; align-items:center; justify-content:space-between; gap:8px; margin-bottom:7px; flex:none; }
.dr-panel-hd b { font-size:var(--f-sm); font-weight:800; }
.dr-pill { font-size:var(--f-xs); color:var(--dr-muted); background:#EEF0F4; border-radius:999px;
  padding:2px 8px; font-weight:800; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:55%; }

.dr-stage-tabs { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:5px; margin-bottom:7px; flex:none; }
.dr-stage { min-height:42px; border-radius:10px; background:#EEF0F4; color:var(--dr-muted);
  font-size:var(--f-xs); font-weight:800; }
.dr-stage.on { background:var(--dr-ink); color:#fff; }
.dr-fact-grid { display:grid; grid-template-columns:minmax(0,1fr); gap:6px; flex:1; min-height:0;
  grid-auto-rows:minmax(58px,1fr); overflow:auto; }
.dr-fact { display:flex; align-items:center; gap:7px; padding:7px 8px; border:1.5px solid transparent;
  border-radius:12px; text-align:left; font-size:var(--f-xs); font-weight:800; line-height:1.3; min-width:0; }
.dr-fact .dr-fact-label { min-width:0; flex:1; }
.dr-fact .dr-fact-label small { display:block; margin-top:1px; font-size:var(--f-xs); font-weight:700; opacity:.72; }
.dr-fact .dr-fact-mark { flex:none; opacity:.6; font-size:var(--f-sm); font-weight:800; }
.dr-fact.pos { background:var(--dr-mint-soft); border-color:#D4F2E2; color:var(--dr-mint-ink); }
.dr-fact.neg { background:var(--dr-peach-soft); border-color:#FFDFCE; color:var(--dr-peach-ink); }
.dr-fact.on { border-color:var(--dr-violet); box-shadow:0 0 0 3px rgba(123,97,255,.13); }
.dr-fact.on .dr-fact-mark { opacity:1; }
.dr-fact:active { transform:scale(.99); }

.dr-improve-block { margin-top:7px; flex:none; }
.dr-improve-hd { display:flex; align-items:center; justify-content:space-between; gap:6px;
  font-size:var(--f-xs); color:var(--dr-muted); font-weight:800; margin-bottom:4px; }
.dr-mini { min-height:32px; padding:0 9px; border-radius:999px; background:#EEF0F4; color:var(--dr-muted);
  font-size:var(--f-xs); font-weight:800; }
.dr-mini.on { background:var(--dr-ink); color:#fff; }
.dr-improve-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:5px; }
.dr-improve { min-height:38px; padding:0 8px; border-radius:10px; background:#FAFAFC; border:1px solid #F0F1F5;
  color:var(--dr-muted); font-size:var(--f-xs); font-weight:800; text-align:left; }
.dr-improve.on { background:var(--dr-violet-soft); border-color:#DDD4FF; color:var(--dr-violet-ink); }
.dr-hint { margin:6px 0 0; color:var(--dr-muted); font-size:var(--f-xs); line-height:1.45; font-weight:600; flex:none; }

.dr-current-fact { display:flex; align-items:center; gap:7px; min-height:42px; padding:7px 9px; margin-bottom:7px;
  border-radius:11px; background:var(--dr-violet-soft); border:1px solid #DDD4FF; color:var(--dr-violet-ink);
  font-size:var(--f-xs); font-weight:800; flex:none; }
.dr-current-fact.empty { background:#EEF0F4; border-color:var(--dr-line); color:var(--dr-muted); }
.dr-tag { flex:none; font-size:var(--f-xs); font-weight:800; border-radius:999px; padding:2px 7px; white-space:nowrap; }
.dr-tag.pos { background:var(--dr-mint-soft); color:var(--dr-mint-ink); }
.dr-tag.neg { background:var(--dr-peach-soft); color:var(--dr-peach-ink); }
.dr-roster-grid { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:5px; flex:1; min-height:0;
  overflow:auto; align-content:start; padding:1px; }
.dr-stu { min-height:48px; padding:0 8px; border:1.5px solid var(--dr-line); border-radius:11px;
  background:#fff; font-size:var(--f-xs); font-weight:700; text-align:left; min-width:0; overflow:hidden;
  text-overflow:ellipsis; white-space:nowrap; }
.dr-stu.on { border-color:var(--dr-violet); background:var(--dr-violet-soft); color:var(--dr-violet-ink);
  box-shadow:0 0 0 3px rgba(123,97,255,.12); }
.dr-stu:disabled { opacity:.45; cursor:not-allowed; }
.dr-picker-foot { padding-top:7px; border-top:1px solid #F0F1F5; margin-top:6px; flex:none;
  font-size:var(--f-xs); color:var(--dr-muted); font-weight:700; }
.dr-picker-foot b { color:var(--dr-ink); font-weight:800; }

.dr-tabs { display:grid; grid-template-columns:1fr 1fr; gap:5px; margin-bottom:7px; flex:none; }
.dr-dtab { min-height:40px; border-radius:10px; background:#EEF0F4; color:var(--dr-muted);
  font-size:var(--f-xs); font-weight:800; }
.dr-dtab.on { background:var(--dr-ink); color:#fff; }
.dr-dtab span { display:inline-block; margin-left:4px; border-radius:999px; padding:0 6px;
  background:rgba(255,255,255,.22); font-size:var(--f-xs); font-weight:800; }
.dr-dtab:not(.on) span { background:#E3E6ED; color:var(--dr-muted); }
.dr-tabpanel { display:flex; flex-direction:column; flex:1; min-height:0; }
.dr-tray-list { list-style:none; margin:0; padding:0; overflow:auto; flex:1; min-height:0;
  display:flex; flex-direction:column; gap:5px; }
.dr-tray-item { display:flex; align-items:center; gap:7px; padding:6px 7px; border-radius:10px;
  background:#FAFAFC; border:1px solid #F0F1F5; min-width:0; }
.dr-avatar { flex:none; width:30px; height:30px; border-radius:9px; background:var(--dr-violet-soft);
  color:var(--dr-violet-ink); display:grid; place-items:center; font-size:var(--f-xs); font-weight:800; }
.dr-tray-main { min-width:0; flex:1; }
.dr-tray-main b { display:block; font-size:var(--f-xs); font-weight:800; line-height:1.2;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.dr-tray-main small { display:block; font-size:var(--f-xs); color:var(--dr-muted); font-weight:600;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.dr-remove { flex:none; min-height:30px; padding:0 7px; border-radius:999px; background:#EEF0F4;
  color:var(--dr-muted); font-size:var(--f-xs); font-weight:800; }
.dr-improve-chip { margin-top:6px; padding:6px 8px; border-radius:10px; background:var(--dr-violet-soft);
  border:1px solid #DDD4FF; color:var(--dr-violet-ink); font-size:var(--f-xs); font-weight:700;
  display:flex; align-items:center; gap:6px; flex:none; }
.dr-improve-chip span { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.dr-tray-foot { display:flex; align-items:stretch; gap:6px; padding-top:7px; margin-top:6px;
  border-top:1px solid #F0F1F5; flex:none; }
.dr-clear { min-height:52px; padding:0 10px; border-radius:12px; background:#EEF0F4; color:var(--dr-muted);
  font-size:var(--f-xs); font-weight:800; flex:none; }
.dr-submit { display:flex; flex-direction:column; align-items:center; justify-content:center; gap:1px;
  min-height:52px; border-radius:12px; background:var(--dr-violet); color:#fff; font-size:var(--f-sm);
  font-weight:800; flex:1; box-shadow:0 8px 20px rgba(123,97,255,.22); padding:4px 8px; line-height:1.25; }
.dr-submit small { font-size:var(--f-xs); font-weight:700; opacity:.85; }
.dr-submit:disabled { background:#D9DCE5; color:#8A8E9A; box-shadow:none; cursor:not-allowed; }
.dr-clear:disabled { opacity:.5; cursor:default; }

.dr-batch-list { overflow:auto; flex:1; min-height:0; display:flex; flex-direction:column; gap:5px; }
.dr-batch { padding:6px 7px; border-radius:10px; background:#FAFAFC; border:1px solid #F0F1F5; min-width:0; }
.dr-batch-head { display:flex; align-items:center; justify-content:space-between; gap:6px;
  font-size:var(--f-xs); font-weight:800; }
.dr-batch-head b { font-weight:800; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.dr-batch-head span { flex:none; color:var(--dr-muted); font-weight:700; }
.dr-batch-improve { margin-top:3px; font-size:var(--f-xs); color:var(--dr-violet-ink); font-weight:700; }
.dr-batch-items { list-style:none; margin:4px 0 0; padding:0; display:flex; flex-direction:column; gap:2px; }
.dr-batch-items li { display:flex; align-items:center; gap:5px; font-size:var(--f-xs); color:var(--dr-muted);
  font-weight:600; min-width:0; }
.dr-batch-items li b { color:var(--dr-ink); font-weight:800; flex:none; }
.dr-batch-items li span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; flex:1; }
.dr-batch-items li i { flex:none; font-style:normal; font-size:var(--f-xs); font-weight:800; }
.dr-batch-items li i.pos { color:var(--dr-mint-ink); }
.dr-batch-items li i.neg { color:var(--dr-peach-ink); }
.dr-empty { display:flex; flex-direction:column; align-items:center; justify-content:center; gap:3px;
  min-height:120px; flex:1; color:var(--dr-muted); font-size:var(--f-xs); font-weight:700; text-align:center; padding:10px; }
.dr-empty b { font-size:var(--f-sm); color:var(--dr-ink); font-weight:800; }

/* ========== 2026-10-18 班务总结改版：两栏 + 底部提交条 ========== */
.dr-layout2 { display:grid; grid-template-columns:minmax(0,1.05fr) minmax(0,.95fr); gap:8px; flex:1; min-height:0; }
.dr-fact-grid2 { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:6px;
  flex:1; min-height:0; overflow:auto; align-content:stretch; grid-auto-rows:minmax(58px,1fr);
  padding-right:2px; }
.dr-fact2 { display:flex; align-items:center; gap:8px; min-height:58px; padding:9px 10px;
  border-radius:13px; border:1.5px solid var(--dr-line); background:var(--dr-card); color:var(--dr-ink);
  text-align:left; font-family:inherit; cursor:pointer; }
.dr-fact2 .dr-fact2-t { flex:1; min-width:0; font-size:var(--f-sm); font-weight:800; line-height:1.25; }
.dr-fact2 .dr-fact2-t i { display:block; margin-top:2px; font-style:normal; font-size:var(--f-xs);
  font-weight:800; color:var(--dr-muted); }
.dr-fact2 .dr-fact2-m { flex:none; font-size:var(--f-sm); font-weight:900; color:#C3C6D1; }
.dr-fact2.pos { border-color:#DDF3E8; background:#F8FDFA; }
.dr-fact2.pos .dr-fact2-t { color:var(--dr-mint-ink); }
.dr-fact2.on { border-color:var(--dr-violet); background:var(--dr-violet-soft);
  box-shadow:0 0 0 3px rgba(123,97,255,.12); }
.dr-fact2.on .dr-fact2-t { color:var(--dr-ink); }
.dr-fact2.on .dr-fact2-m { color:var(--dr-violet); }
.dr-fact2.add { grid-column:1 / -1; justify-content:center; border-style:dashed;
  border-color:#DDD4FF; background:transparent; color:var(--dr-violet-ink);
  font-size:var(--f-sm); font-weight:900; }

.dr-current2 { display:flex; align-items:center; gap:8px; flex:none; min-height:44px; margin-top:7px;
  padding:8px 10px; border-radius:12px; background:#F5F6FA; font-size:var(--f-xs);
  color:var(--dr-muted); font-weight:700; }
.dr-current2 b { color:var(--dr-ink); font-size:var(--f-sm); }
.dr-current2.empty { background:#EEF0F4; }
.dr-del-fact { margin-left:auto; flex:none; min-height:30px; padding:0 9px; border-radius:999px;
  background:#EEF0F4; color:var(--dr-peach-ink); font-family:inherit; font-size:var(--f-xs);
  font-weight:800; cursor:pointer; }
/* ★ 2026-10-19：自己加的模板可以删；「只记这一次」只是临时一条，不进模板 */
.dr-once-tag { flex:none; margin-left:8px; padding:2px 9px; border-radius:999px; background:#EEF0F4;
  color:var(--dr-muted); font-size:var(--f-xs); font-weight:800; }
.dr-dlg-mine-h { display:flex; align-items:center; gap:8px; margin-top:14px;
  color:var(--dr-muted); font-size:var(--f-xs); font-weight:800; }
.dr-dlg-mine-h span { color:#9AA0AC; font-weight:700; }
.dr-dlg-mine { max-height:120px; overflow:auto; margin-top:6px; display:flex; flex-direction:column; gap:6px; }
.dr-mine-row { display:flex; align-items:center; gap:8px; padding:5px 8px; border-radius:10px; background:#F5F6F9; }
.dr-mine-row > span { flex:1; min-width:0; color:var(--dr-ink); font-size:var(--f-xs); font-weight:700;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.dr-mine-row > span em { font-style:normal; color:var(--dr-muted); margin-right:6px; }
.dr-mine-row .dr-del-fact { margin-left:0; }

.dr-traybar { flex:none; display:flex; align-items:center; gap:8px; min-height:52px;
  padding:8px 10px; border-radius:12px; background:var(--dr-card); border:1px solid var(--dr-line); }
.dr-traybar > b { flex:none; font-size:var(--f-xs); color:var(--dr-muted); font-weight:800; }
.dr-traychips { flex:1; min-width:0; display:flex; align-items:center; gap:6px; overflow-x:auto; padding-bottom:2px; }
.dr-traychip { flex:none; display:flex; align-items:center; gap:6px; height:34px; padding:0 6px 0 11px;
  border-radius:999px; background:var(--dr-violet-soft); color:var(--dr-violet-ink);
  font-size:var(--f-xs); font-weight:800; white-space:nowrap; }
.dr-traychip em { font-style:normal; color:var(--dr-muted); font-weight:700; max-width:18ch;
  overflow:hidden; text-overflow:ellipsis; }
.dr-traychip button { flex:none; width:24px; height:24px; border-radius:50%; background:#fff;
  color:var(--dr-violet-ink); font-family:inherit; font-size:14px; line-height:1; font-weight:900; cursor:pointer; }

.dr-bottom { display:flex; align-items:center; gap:8px; flex:none; padding-top:8px; }
.dr-bottom-sum { font-size:var(--f-xs); color:var(--dr-muted); font-weight:800; }
.dr-bottom-sum b { color:var(--dr-ink); font-size:var(--f-md); margin:0 2px; }
.dr-bottom-sum i { font-style:normal; color:var(--dr-mint-ink); }
.dr-bottom .dr-clear { margin-left:auto; min-height:48px; padding:0 16px; }
.dr-submit2 { min-height:48px; padding:0 22px; border-radius:13px; background:var(--dr-violet);
  color:#fff; font-family:inherit; font-size:var(--f-sm); font-weight:900; cursor:pointer;
  box-shadow:0 10px 22px rgba(123,97,255,.28); }
.dr-submit2:disabled { background:#D9DCE5; color:#8A8E9A; box-shadow:none; cursor:not-allowed; }
/* 「存进模板」是次要选项（主按钮是「只记这一次」） */
.dr-submit2.is-ghost { background:#EEF0F4; color:var(--dr-muted); box-shadow:none; }

.dr-done { min-height:38px; padding:0 13px; border-radius:999px; background:#EEF0F4;
  color:var(--dr-muted); font-family:inherit; font-size:var(--f-xs); font-weight:800; cursor:pointer; }
.dr-done b { color:var(--dr-ink); font-size:var(--f-sm); }
.dr-done.on { background:var(--dr-ink); color:#fff; }
.dr-done.on b { color:#fff; }

.dr-pre-note { margin-top:7px; flex:none; padding:8px 10px; border-radius:11px;
  background:var(--dr-mint-soft); border:1px solid #C9EEDA; color:var(--dr-mint-ink);
  font-size:var(--f-xs); font-weight:800; }
.dr-reviews-view { flex:1; min-height:0; display:flex; flex-direction:column; }

/* 自己加一条：打字只在弹层里出现 */
.dr-modal { position:fixed; inset:0; z-index:130; display:none; align-items:flex-start;
  justify-content:center; padding:8vh 16px 24px; overflow:auto; background:rgba(18,20,26,.42); }
.dr-modal.on { display:flex; }
.dr-dlg { width:min(560px,94vw); padding:20px 22px 18px; border-radius:22px; background:var(--dr-card);
  box-shadow:0 30px 70px rgba(0,0,0,.28); display:flex; flex-direction:column; gap:12px; }
.dr-dlg-h { display:flex; align-items:center; }
.dr-dlg-h b { font-size:var(--f-md); font-weight:900; }
.dr-dlg-x { margin-left:auto; font-size:22px; line-height:1; color:var(--dr-muted); cursor:pointer; }
.dr-dlg-label { color:var(--dr-muted); font-size:var(--f-xs); font-weight:800; }
.dr-dlg-chips { display:flex; flex-wrap:wrap; gap:7px; }
.dr-chip { min-height:36px; padding:0 14px; border-radius:999px; border:1.5px solid var(--dr-line);
  background:var(--dr-card); color:var(--dr-ink); font-family:inherit; font-size:var(--f-xs);
  font-weight:800; cursor:pointer; }
.dr-chip.on { background:var(--dr-violet-soft); border-color:var(--dr-violet); color:var(--dr-violet-ink); }
.dr-ta { width:100%; min-height:96px; resize:none; padding:12px 14px; border-radius:14px;
  border:1.5px solid var(--dr-line); background:#F7F8FB; color:var(--dr-ink); font-family:inherit;
  font-size:var(--f-sm); font-weight:700; outline:none; }
.dr-ta:focus { border-color:var(--dr-violet); background:var(--dr-card); }
.dr-dlg-f { display:flex; align-items:center; gap:8px; }
.dr-dlg-note { flex:1; color:var(--dr-muted); font-size:var(--f-xs); font-weight:700; line-height:1.45; }

@media (max-width:820px) {
  .dr-layout2 { grid-template-columns:minmax(0,1fr); }
  .dr-fact-grid2 { grid-template-columns:minmax(0,1fr); }
}

@media (max-width:1200px) {
  .dr-layout { grid-template-columns:minmax(0,1fr) minmax(0,1fr); }
  .dr-submit-col { grid-column:1 / -1; min-height:260px; }
  .dr-stats { grid-template-columns:repeat(2,minmax(0,1fr)); }
}
@media (max-width:820px) {
  .dr-layout { grid-template-columns:minmax(0,1fr); }
  .dr-submit-col { grid-column:auto; }
  #cr-main:has(.dr-box.dr-expanded) .b-body { padding:0.25rem 0.4rem 0.6rem; }
}


/* Leave linkage: neutral exception, teacher management in existing modal. */
.b-morning-status.is-leave{color:var(--vibe-muted-foreground);background:var(--vibe-muted);opacity:1;cursor:default;}
.lv-approved-head{font-size:var(--vibe-fs-base);margin:16px 0 8px;}
.lv-approved-row{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 16px;border-top:1px solid var(--vibe-border);}
.lv-approved-row>span{min-width:0;overflow-wrap:anywhere;}
.lv-approved-row small{display:block;color:var(--vibe-muted-foreground);font-size:var(--vibe-fs-sm);}
.lv-approved-row>div{display:flex;gap:8px;flex-wrap:wrap;}
.lv-approved-row button{min-height:44px;min-width:44px;}
@media(max-width:480px){.lv-approved-row{align-items:flex-start;flex-direction:column;}.lv-approved-row>div{width:100%;}}

#leave-list{max-height:52vh;overflow-y:auto;overscroll-behavior:contain;}

/* 今日卡片滚动进场：仅独立位移，不覆盖卡片既有 transform / hover / 拖拽。
   ★ v0.24.10：起位 30px → 36px（与 dashboard.js 里那段 WAAPI 的 from 保持同一个数，改一处要改两处）。 */
.today-card.today-reveal-pending { opacity:0; translate:0 36px; pointer-events:none; }
body.home-editing .today-card.today-reveal-pending,
body.layout-dragging .today-card.today-reveal-pending { opacity:1; translate:none; pointer-events:auto; }
@media (prefers-reduced-motion: reduce) {
  .today-card.today-reveal-pending { opacity:1; translate:none; pointer-events:auto; }
}
@media print {
  .today-card.today-reveal-pending { opacity:1; translate:none; }
}

/* ============================================================
   工作站 · 现代个人空间（2026-10-23，用户确认预览）
   仅 html.workspace-skin 生效。公开首页、登录、家长端与大屏播放
   不带此标记。保持现有字号、权限、导航结构和业务状态色。
   ============================================================ */
:root.workspace-skin {
  --vibe-background: #f1f4f8;
  --vibe-foreground: #28313e;
  --vibe-card: #ffffff;
  --vibe-card-foreground: #28313e;
  --vibe-popover: #ffffff;
  --vibe-popover-foreground: #28313e;
  --vibe-muted: #e9eef5;
  --vibe-muted-foreground: #606e80;
  --vibe-border: #dce3ed;
  --vibe-input: #dce3ed;
  --vibe-primary: #5b70ac;
  --vibe-primary-foreground: #ffffff;
  --vibe-primary-ink: #4b609b;
  --vibe-secondary: #e9eef5;
  --vibe-secondary-foreground: #28313e;
  --vibe-accent: #52669f;
  --vibe-accent-foreground: #ffffff;
  --vibe-ring: #5b70ac;
  --vibe-card-sub: #f7f9fc;
  --vibe-line-soft: #e4e9f1;
  --vibe-line-faint: #eaf0f6;
  --vibe-brand-50: #f2f5fb;
  --vibe-brand-100: #e9eef9;
  --vibe-brand-200: #d4dff2;
  --vibe-brand-300: #b6c7e7;
  --vibe-brand-400: #899fcf;
  --vibe-brand-500: #687fba;
  --vibe-brand-600: #5b70ac;
  --vibe-brand-700: #4b609b;
  --vibe-brand-800: #4c619b;
  --vibe-brand-900: #3f5285;
  --vibe-brand-line: #dce3f1;
  --vibe-brand-line-soft: #e5eaf5;
  --vibe-brand-wash-55: #f4f6fc;
  --vibe-nav-bg: #ffffff;
  --vibe-nav-fg: #28313e;
  --vibe-nav-muted: #606e80;
  --vibe-nav-line: #e4e9f1;
  --vibe-nav-hover: #f2f5fa;
  --vibe-nav-active-bg: #e9eef9;
  --vibe-nav-active-fg: #4b609b;
  --vibe-shadow-xs: 0 1px 2px rgba(38, 56, 84, .025);
  --vibe-shadow-sm: 0 2px 9px rgba(38, 56, 84, .035);
  --vibe-shadow-md: 0 3px 8px rgba(38, 56, 84, .035), 0 14px 35px -20px rgba(38, 56, 84, .2);
  --vibe-shadow-lg: 0 5px 16px rgba(38, 56, 84, .05), 0 22px 50px -24px rgba(38, 56, 84, .25);
  --vibe-workspace-glow: rgba(203, 229, 243, .6);
  --vibe-workspace-blush: rgba(237, 223, 237, .38);
  --vibe-workspace-hero: #fafaff;
}
:root.workspace-skin.dark {
  --vibe-background: #1b2330;
  --vibe-foreground: #edf2fb;
  --vibe-card: #273243;
  --vibe-card-foreground: #edf2fb;
  --vibe-popover: #2d394c;
  --vibe-popover-foreground: #edf2fb;
  --vibe-muted: #323f53;
  --vibe-muted-foreground: #b7c4d7;
  --vibe-border: #435168;
  --vibe-input: #435168;
  --vibe-primary: #b4c9ff;
  --vibe-primary-foreground: #18263e;
  --vibe-primary-ink: #bdd0ff;
  --vibe-secondary: #323f53;
  --vibe-secondary-foreground: #edf2fb;
  --vibe-accent: #b4c9ff;
  --vibe-accent-foreground: #18263e;
  --vibe-ring: #b4c9ff;
  --vibe-card-sub: #2b3749;
  --vibe-line-soft: #3c4a60;
  --vibe-line-faint: #364358;
  --vibe-brand-50: #2d3b54;
  --vibe-brand-100: #364969;
  --vibe-brand-200: #475d85;
  --vibe-brand-300: #647eaf;
  --vibe-brand-400: #a0b8ec;
  --vibe-brand-500: #a9c0f4;
  --vibe-brand-600: #b4c9ff;
  --vibe-brand-700: #bdd0ff;
  --vibe-brand-800: #b4c9ff;
  --vibe-brand-900: #d0dfff;
  --vibe-brand-line: #4a5b7b;
  --vibe-brand-line-soft: #384b69;
  --vibe-brand-wash-55: #2d3b54;
  --vibe-nav-bg: #232d3d;
  --vibe-nav-fg: #edf2fb;
  --vibe-nav-muted: #b7c4d7;
  --vibe-nav-line: #3c4a60;
  --vibe-nav-hover: #2b3749;
  --vibe-nav-active-bg: #364969;
  --vibe-nav-active-fg: #c4d5ff;
  --vibe-shadow-xs: 0 1px 2px rgba(0, 0, 0, .08);
  --vibe-shadow-sm: 0 2px 9px rgba(0, 0, 0, .1);
  --vibe-shadow-md: 0 3px 8px rgba(0, 0, 0, .1), 0 14px 35px -20px rgba(0, 0, 0, .4);
  --vibe-shadow-lg: 0 5px 16px rgba(0, 0, 0, .15), 0 22px 50px -24px rgba(0, 0, 0, .5);
  --vibe-workspace-glow: rgba(57, 86, 118, .35);
  --vibe-workspace-blush: rgba(78, 61, 93, .25);
  --vibe-workspace-hero: #2a3549;
}
.workspace-skin body {
  background-image: radial-gradient(ellipse at 20% 0%, var(--vibe-workspace-glow), transparent 60%),
                    radial-gradient(ellipse at 100% 15%, var(--vibe-workspace-blush), transparent 60%);
  background-repeat: no-repeat;
  background-size: 100% 50rem;
}
.workspace-skin body::after { display: none; }
.workspace-skin .logo-mark {
  background: var(--vibe-foreground) !important;
  box-shadow: none;
}
.workspace-skin .logo-mark-text { color: var(--vibe-card) !important; }
.workspace-skin aside .logo-name .text-primary { color: var(--vibe-nav-fg) !important; }
.workspace-skin aside .sidebar-nav .nav-item { border-radius: var(--vibe-radius-sm); }
.workspace-skin aside nav .nav-group-title { color: var(--vibe-nav-muted) !important; }
.workspace-skin aside .sidebar-nav .nav-item.nav-current::before { display: none; }
.workspace-skin .today-page-head {
  background: var(--vibe-card);
  background-image: none;
  border-bottom-color: var(--vibe-line-soft);
}
.workspace-skin .today-page-head.is-preview { background: var(--vibe-brand-50); }
.workspace-skin .week-cell { background: var(--vibe-card); border-color: var(--vibe-line-soft); }
.workspace-skin .week-cell.is-today { background: var(--vibe-brand-100); }
.workspace-skin .week-cell.is-sel,
.workspace-skin .week-cell.is-sel:hover {
  background: var(--vibe-brand-800);
  color: var(--vibe-primary-foreground);
  border-color: var(--vibe-brand-800);
  box-shadow: var(--vibe-shadow-sm);
}
.workspace-skin .today-class-chip { background: var(--vibe-card); border-color: var(--vibe-border); color: var(--vibe-foreground); }
.workspace-skin .today-md { font-family: var(--vibe-font-sans); }
.workspace-skin .today-card.is-secondary {
  background: var(--vibe-card);
  border-color: var(--vibe-line-soft);
  box-shadow: var(--vibe-shadow-sm);
}
.workspace-skin .today-head { border-bottom-color: var(--vibe-line-soft); }
.workspace-skin .today-head h3 svg {
  color: var(--vibe-primary-ink) !important;
  background: var(--vibe-brand-50);
}
.workspace-skin .now-hero,
.workspace-skin .now-hero.is-idle {
  background: var(--vibe-workspace-hero);
  border-color: var(--vibe-brand-line);
  box-shadow: var(--vibe-shadow-md);
}
/* 复用现有助手与课程两个节点：只调整视觉先后，不重建卡片或改时段判断。 */
.workspace-skin .ta-hero-shell.now-hero { display: flex; flex-direction: column; align-items: stretch; padding: 0; gap: 0; }
.workspace-skin .ta-hero-shell > #today-assistant-strip {
  order: -1;
  margin: 0;
  padding: 12px 20px;
  border-bottom: 1px solid var(--vibe-brand-line);
}
.workspace-skin .ta-hero-shell > #ov-now.now-hero {
  padding: 20px;
  border: 0;
  box-shadow: none;
  background: transparent;
}
.workspace-skin .ta-hero-shell .now-stat-icon { background: var(--vibe-brand-100); color: var(--vibe-primary-ink); }
.workspace-skin .tb-card .tb-glyph {
  width: 36px;
  height: 36px;
  border-radius: var(--vibe-radius-sm);
  color: var(--vibe-primary-ink);
  background: var(--vibe-brand-100);
}
.workspace-skin .tb-card .tb-glyph svg { width: 18px; height: 18px; }
.workspace-skin .hub-grid { max-width: none; }
.workspace-skin .hub-group { box-shadow: var(--vibe-shadow-sm); }
.workspace-skin .hub-items .hub-card .hub-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--vibe-brand-100);
  color: var(--vibe-primary-ink);
  width: 40px;
  height: 40px;
  border-radius: var(--vibe-radius-sm);
}
.workspace-skin .hub-items .hub-icon svg { display: block; flex-shrink: 0; width: 20px !important; height: 20px !important; }
.workspace-skin .hub-items .hub-card:hover { background: var(--vibe-brand-50); }
@media (min-width: 768px) {
  .workspace-skin .hub-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: start; gap: 16px; }
  .workspace-skin .hub-group { display: block; padding: 20px; margin: 0; }
  .workspace-skin .hub-group-title { padding: 0; margin-bottom: 16px; }
  .workspace-skin .hub-note { display: block; margin: 4px 0 0; }
  .workspace-skin .hub-items { gap: 8px; }
  .workspace-skin .hub-items .hub-card { flex-direction: column; justify-content: flex-start; text-align: center; gap: 8px; padding: 12px 4px; }
  .workspace-skin .hub-short { display: none; }
}
@media (min-width: 768px) and (max-width: 1099px) {
  .workspace-skin .hub-grid { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 767px) {
  .workspace-skin aside { background: var(--vibe-card) !important; border-color: var(--vibe-border) !important; }
  .workspace-skin aside .sidebar-nav .nav-tab.nav-current,
  .workspace-skin aside .sidebar-nav .nav-tab.nav-tab-active-mobile {
    background: var(--vibe-brand-100);
    color: var(--vibe-primary-ink) !important;
    box-shadow: none;
  }
  .workspace-skin .ta-hero-shell > #today-assistant-strip { padding: 12px; }
  .workspace-skin .ta-hero-shell > #ov-now.now-hero { padding: 16px 12px; }
  .workspace-skin .hub-group { box-shadow: none; }
  .workspace-skin .hub-items { box-shadow: var(--vibe-shadow-sm); padding: 8px; }
  .workspace-skin .hub-items .hub-card { gap: 8px; padding: 12px 4px; }
}
@media print {
  .workspace-skin body { background-image: none; }
  .workspace-skin .today-card, .workspace-skin .now-hero, .workspace-skin .hub-group { box-shadow: none; }
}

/* 工作站可选皮肤 · 2026-10-24：浅色保持原版；柔紫/炭黑按个人选择。 */

:root.workspace-skin{
 --vibe-skin-tone-1:#f2f3f7;
 --vibe-skin-tone-2:rgba(153,133,220,.17);
 --vibe-skin-tone-3:rgba(191,179,231,.09);
 --vibe-skin-tone-4:rgba(175,157,229,.14);
 --vibe-skin-tone-5:rgba(205,195,237,.07);
 --vibe-skin-tone-6:#fafafcdb;
 --vibe-skin-tone-7:#e7e9f08c;
 --vibe-skin-tone-8:#24242d;
 --vibe-skin-tone-9:#7b61ff;
 --vibe-skin-tone-10:#436b921c;
 --vibe-skin-tone-11:#fff;
 --vibe-skin-tone-12:#fcfcfeea;
 --vibe-skin-tone-13:#f0ecffcc;
 --vibe-skin-tone-14:rgba(255,255,255,.86);
 --vibe-skin-tone-15:#e5ebf0;
 --vibe-skin-tone-16:#24203307;
 --vibe-skin-tone-17:#dedce863;
 --vibe-skin-tone-18:#f0ecffb0;
 --vibe-skin-tone-19:#7c70a1;
 --vibe-skin-tone-20:rgba(255,255,255,.82);
 --vibe-skin-tone-21:#ffffffc4;
 --vibe-skin-tone-22:#24203309;
 --vibe-skin-tone-23:rgba(242,239,251,.24);
 --vibe-skin-tone-24:#e2ddef70;
 --vibe-skin-tone-25:#9682d3;
 --vibe-skin-tone-26:#5a44d8;
 --vibe-skin-tone-27:#ffffff8f;
 --vibe-skin-tone-28:#e3dfed;
 --vibe-skin-tone-29:#23232c;
 --vibe-skin-tone-30:#9a8cba;
 --vibe-skin-tone-31:#343140;
 --vibe-skin-tone-32:#e1dceb80;
 --vibe-skin-tone-33:#f0ecffb3;
 --vibe-skin-tone-34:#e3ddef80;
 --vibe-skin-tone-35:#8675b6;
 --vibe-skin-tone-36:#353043;
 --vibe-skin-tone-37:#9b94ac;
 --vibe-skin-tone-38:#ffffff45;
 --vibe-skin-tone-39:#fbfaffbf;
 --vibe-skin-tone-40:#24203310;
 --vibe-skin-tone-41:#fafdff;
}
/* Local visual study only. Actual application markup and data renderers retained. */
:root.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) {
 --vibe-background:#f5f5f3;--vibe-foreground:#26282d;--vibe-card:#fff;--vibe-card-foreground:#26282d;--vibe-popover:#fff;--vibe-popover-foreground:#26282d;
 --vibe-muted:#eeeFEB;--vibe-muted-foreground:#686d75;--vibe-border:#e1e2e4;--vibe-input:#d8dade;--vibe-secondary:#f1f1ef;--vibe-secondary-foreground:#303238;
 --vibe-primary:#9a6137;--vibe-primary-foreground:#fff;--vibe-primary-ink:#88542d;--vibe-accent:#9a6137;--vibe-accent-foreground:#fff;--vibe-ring:#b37a4d;
 --vibe-card-sub:#f8f8f6;--vibe-line-soft:#e9e9e7;--vibe-line-faint:#f0f0ee;
 --vibe-brand-50:#faf5ee;--vibe-brand-100:#f3e9dc;--vibe-brand-200:#ead5bc;--vibe-brand-300:#d9b58b;--vibe-brand-400:#c39261;--vibe-brand-500:#b07944;--vibe-brand-600:#9a6137;--vibe-brand-700:#88542d;--vibe-brand-800:#9a6137;--vibe-brand-900:#704021;
 --vibe-brand-line:#e7d9ca;--vibe-brand-line-soft:#f0e6db;--vibe-brand-wash-55:#faf5ee;
 --vibe-nav-bg:#f9f9f7;--vibe-nav-fg:#303238;--vibe-nav-muted:#737780;--vibe-nav-line:#e9e9e7;--vibe-nav-hover:#f0f0ed;--vibe-nav-active-bg:#e9e9e5;--vibe-nav-active-fg:#222429;
 --vibe-shadow-xs:0 1px 2px #24262d03;--vibe-shadow-sm:0 2px 4px #24262d03;--vibe-shadow-md:0 3px 12px #24262d05;--vibe-shadow-lg:0 15px 40px #24262d12;
 --vibe-workspace-hero:#fff;--vibe-skin-accent:#9a6137;--vibe-skin-wash:#faf5ee;--vibe-skin-hero-line:#dedfdc;
 --vibe-radius-lg:16px;--vibe-radius-md:12px;--vibe-radius-sm:9px;
 color-scheme:light;
}
:root.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]).dark {
 --vibe-background:#111214;--vibe-foreground:#eeeef1;--vibe-card:#1b1d20;--vibe-card-foreground:#eeeef1;--vibe-popover:#24262b;--vibe-popover-foreground:#eeeef1;
 --vibe-muted:#282a2e;--vibe-muted-foreground:#a6a9b2;--vibe-border:#34363c;--vibe-input:#3c3f46;--vibe-secondary:#282a2e;--vibe-secondary-foreground:#eeeef1;
 --vibe-primary:#e2b28a;--vibe-primary-foreground:#251d16;--vibe-primary-ink:#e7ba95;--vibe-accent:#e2b28a;--vibe-accent-foreground:#251d16;--vibe-ring:#e2b28a;
 --vibe-card-sub:#212327;--vibe-line-soft:#303238;--vibe-line-faint:#282a30;
 --vibe-brand-50:#2b2723;--vibe-brand-100:#393028;--vibe-brand-200:#514030;--vibe-brand-300:#876444;--vibe-brand-400:#c49368;--vibe-brand-500:#d5a37a;--vibe-brand-600:#e2b28a;--vibe-brand-700:#e7ba95;--vibe-brand-800:#e2b28a;--vibe-brand-900:#f0ceaf;
 --vibe-brand-line:#514337;--vibe-brand-line-soft:#3c332b;--vibe-brand-wash-55:#2b2723;
 --vibe-nav-bg:#17181b;--vibe-nav-fg:#e0e1e5;--vibe-nav-muted:#999da7;--vibe-nav-line:#2c2e33;--vibe-nav-hover:#23252a;--vibe-nav-active-bg:#2d2f35;--vibe-nav-active-fg:#fff;
 --vibe-shadow-xs:none;--vibe-shadow-sm:none;--vibe-shadow-md:none;--vibe-shadow-lg:0 15px 40px #0005;
 --vibe-workspace-hero:#1f2125;--vibe-skin-accent:#e2b28a;--vibe-skin-wash:#2b2723;--vibe-skin-hero-line:#3d3f46;
 color-scheme:dark;
}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) body {background:var(--vibe-background);background-image:none!important;}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .today-page-head {background:var(--vibe-background);border-bottom:1px solid var(--vibe-line-soft);}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .today-md {font-weight:700;letter-spacing:-.7px;}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .today-class-chip {border-radius:7px;}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .week-cell {border-radius:9px;background:transparent;border-color:transparent;}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .week-cell:hover {background:var(--vibe-muted);}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .week-cell.is-today {background:var(--vibe-card);border-color:var(--vibe-border);}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .week-cell.is-sel,.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .week-cell.is-sel:hover {background:var(--vibe-foreground);border-color:var(--vibe-foreground);color:var(--vibe-background);box-shadow:none;}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) aside {border-right:1px solid var(--vibe-nav-line);}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) aside .sidebar-nav .nav-item {margin-top:2px;margin-bottom:2px;min-height:34px;}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) aside .sidebar-nav .nav-item.nav-current {font-weight:650;}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) aside .sidebar-nav .nav-item svg {width:16px;height:16px;stroke-width:1.65;}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .today-grid {gap:16px;}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .today-col {gap:16px;}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .today-card,.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .today-card.is-secondary {border-radius:14px;background:var(--vibe-card);border:1px solid var(--vibe-border);box-shadow:var(--vibe-shadow-sm);padding:16px;}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .today-head {padding-bottom:11px;margin-bottom:12px;border-bottom:1px solid var(--vibe-line-soft);}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .today-head h3 {font-size:13px;letter-spacing:0;font-weight:650;gap:8px;}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .today-head h3 svg {width:17px;height:17px;padding:0;background:none!important;border-radius:0;color:var(--vibe-muted-foreground)!important;stroke-width:1.65;}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .today-head>a {font-size:11px;color:var(--vibe-muted-foreground);}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .now-hero,.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .now-hero.is-idle {border-radius:16px;background:var(--vibe-workspace-hero);border:1px solid var(--vibe-skin-hero-line);box-shadow:var(--vibe-shadow-md);}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .ta-hero-shell > #today-assistant-strip {padding:13px 20px;border-bottom:1px solid var(--vibe-line-soft);background:transparent;}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) #today-assistant-strip .ta-head>strong {color:var(--vibe-skin-accent);font-size:12px;font-weight:650;}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) #today-assistant-strip .ta-title {font-size:13px;font-weight:550;color:var(--vibe-foreground);}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) #today-assistant-strip .ta-sub {color:var(--vibe-muted-foreground);font-size:11px;}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) #today-assistant-strip .btn {border-radius:8px;min-height:31px;box-shadow:none;}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .ta-hero-shell > #ov-now.now-hero {padding:23px 22px 25px;}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .now-greet-hi {font-size:12px;color:var(--vibe-muted-foreground);font-weight:400;}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .now-kicker {color:var(--vibe-muted-foreground);margin-top:8px;}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .now-hero-subject {font-size:28px!important;font-weight:700!important;letter-spacing:-.7px;line-height:1.35;}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .now-hero-time {font-size:13px;color:var(--vibe-muted-foreground);font-variant-numeric:tabular-nums;font-weight:450;}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .now-stat {padding-left:20px;padding-right:20px;border-color:var(--vibe-line-soft);}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .ta-hero-shell .now-stat-icon {background:none!important;color:var(--vibe-muted-foreground);width:22px;height:22px;margin-bottom:4px;}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .now-stat-num {font-size:25px;font-weight:650;font-variant-numeric:tabular-nums;letter-spacing:-.8px;}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .now-stat-label {color:var(--vibe-foreground);font-size:11px;}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .now-stat-sub {color:var(--vibe-muted-foreground);font-size:10px;}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .btn {border-radius:8px;box-shadow:none;transition:background-color .15s,border-color .15s;}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .btn-ghost,.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .btn-secondary {color:var(--vibe-foreground);background:var(--vibe-card-sub);border-color:var(--vibe-border);}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) button:focus-visible,.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) a:focus-visible {outline:2px solid var(--vibe-ring);outline-offset:3px;}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .tb-card .tb-glyph {width:32px;height:32px;background:var(--vibe-card-sub);color:var(--vibe-muted-foreground);border:1px solid var(--vibe-line-soft);border-radius:9px;}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .tb-card .tb-line.has-pending .tb-glyph {background:var(--vibe-skin-wash);color:var(--vibe-skin-accent);}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .tb-line-label {font-size:12px;font-weight:600;}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .tb-line-text {font-size:11px;}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .tb-line {border-radius:9px;}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .todo-row {padding-top:10px;padding-bottom:10px;line-height:1.55;border-bottom:1px solid var(--vibe-line-faint);}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) input:not([type=checkbox]),.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) textarea {background-color:var(--vibe-card-sub);border-color:var(--vibe-input);color:var(--vibe-foreground);border-radius:8px;}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .todo-add-btn {background:var(--vibe-foreground);color:var(--vibe-card);border-radius:8px;}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .today-card[data-widget=clock] .clock-time {font-family:var(--vibe-font-sans);font-size:42px;font-weight:600;font-variant-numeric:tabular-nums;letter-spacing:-1.5px;}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .hub-group {background:var(--vibe-card);border:1px solid var(--vibe-border);}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .hub-items .hub-card .hub-icon {background:var(--vibe-card-sub);color:var(--vibe-muted-foreground);}
@media(max-width:767px){
 .workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .today-grid,.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .today-col{gap:12px;}
 .workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .today-card,.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .today-card.is-secondary{padding:14px;border-radius:12px;}
 .workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .ta-hero-shell > #today-assistant-strip{padding:12px 14px;}
 .workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .ta-hero-shell > #ov-now.now-hero{padding:18px 14px;}
 .workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .now-hero-subject{font-size:23px!important;}
 .workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .now-stat{padding-left:8px;padding-right:8px;}
 .workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .now-stat-num{font-size:22px;}
}
@media(prefers-reduced-motion:reduce){.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .btn{transition:none;}}


/* 沿用班级大屏 v2 默认柔紫令牌。浅底与柔紫强调；低强度动态、真实alpha透色。 */
:root.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]):not(.dark){
 --vibe-background:#f2f3f7;--vibe-foreground:#17181c;--vibe-card:#fafdff;--vibe-card-foreground:#17181c;
 --vibe-muted:#ededf4;--vibe-muted-foreground:#727583;--vibe-border:#e7e9f0;--vibe-input:#dadde7;
 --vibe-primary:#7b61ff;--vibe-primary-foreground:#fff;--vibe-primary-ink:#5a44d8;--vibe-accent:#7b61ff;--vibe-accent-foreground:#fff;--vibe-ring:#7b61ff;
 --vibe-secondary:#f0ecff;--vibe-secondary-foreground:#343440;--vibe-card-sub:#f6f5fa;--vibe-line-soft:#e7e9f0;--vibe-line-faint:#eeeef4;
 --vibe-brand-50:#f7f5ff;--vibe-brand-100:#f0ecff;--vibe-brand-200:#e3dcff;--vibe-brand-300:#cfc3ff;--vibe-brand-400:#b5a3ff;--vibe-brand-500:#9680fa;--vibe-brand-600:#7b61ff;--vibe-brand-700:#5a44d8;--vibe-brand-800:#7b61ff;--vibe-brand-900:#4735ae;
 --vibe-brand-line:#e0d9f5;--vibe-brand-line-soft:#ece7f8;--vibe-brand-wash-55:#f7f5ff;
 --vibe-nav-bg:#f9f9fc;--vibe-nav-fg:#343440;--vibe-nav-muted:#727583;--vibe-nav-line:#e7e9f0;--vibe-nav-hover:#f2f0f9;--vibe-nav-active-bg:#f0ecff;--vibe-nav-active-fg:#5a44d8;
 --vibe-shadow-sm:0 4px 18px #24203307;--vibe-shadow-md:0 8px 28px #2420330b;
 --vibe-skin-accent:#7b61ff;--vibe-skin-wash:#f0ecff;--vibe-skin-hero-line:#ffffffc4;
}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .week-cell.is-sel .week-wd,.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .week-cell.is-sel .week-dn{color:var(--vibe-background);}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]) .workspace-atmosphere{display:none;pointer-events:none;}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]):not(.dark) body{isolation:isolate;background:var(--vibe-skin-tone-1)!important;}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]):not(.dark) .workspace-atmosphere{display:block;position:fixed;inset:0;z-index:-1;overflow:hidden;contain:strict;background:var(--vibe-skin-tone-1);}
.workspace-atmosphere i{position:absolute;display:block;width:110vw;height:110vh;left:-25vw;top:-30vh;border-radius:50%;background:radial-gradient(ellipse,var(--vibe-skin-tone-2) 0%,var(--vibe-skin-tone-3) 35%,transparent 70%);animation:workspace-glow-drift 26s ease-in-out infinite alternate;}
.workspace-atmosphere i+i{left:30vw;top:15vh;opacity:.7;background:radial-gradient(ellipse,var(--vibe-skin-tone-4),var(--vibe-skin-tone-5) 40%,transparent 70%);animation:workspace-glow-return 34s ease-in-out infinite alternate;}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]):not(.dark) .today-page-head{background:var(--vibe-skin-tone-6);border-color:var(--vibe-skin-tone-7);}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]):not(.dark) .today-md{color:var(--vibe-skin-tone-8);}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]):not(.dark) .week-cell.is-sel,.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]):not(.dark) .week-cell.is-sel:hover{background:var(--vibe-skin-tone-9);border-color:transparent;box-shadow:0 3px 10px var(--vibe-skin-tone-10);}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]):not(.dark) .week-cell.is-sel .week-wd,.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]):not(.dark) .week-cell.is-sel .week-dn{color:var(--vibe-skin-tone-11);}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]):not(.dark) aside{background:var(--vibe-skin-tone-12)!important;}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]):not(.dark) aside .sidebar-nav .nav-current{background:var(--vibe-skin-tone-13)!important;}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]):not(.dark) .today-card,.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]):not(.dark) .today-card.is-secondary{background:var(--vibe-skin-tone-14);border:1px solid var(--vibe-skin-tone-15);border-radius:17px;box-shadow:0 4px 18px var(--vibe-skin-tone-16);}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]):not(.dark) .today-card[data-widget]{background:var(--vibe-skin-tone-14);}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]):not(.dark) .today-head{border-bottom-color:var(--vibe-skin-tone-17);}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]):not(.dark) .today-head h3 svg{width:22px;height:22px;padding:3px;border-radius:7px;background:var(--vibe-skin-tone-18)!important;color:var(--vibe-skin-tone-19)!important;}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]):not(.dark) .ta-hero-shell.now-hero{position:relative;isolation:isolate;overflow:hidden;background:var(--vibe-skin-tone-20);border:1px solid var(--vibe-skin-tone-21);border-radius:19px;box-shadow:0 7px 24px var(--vibe-skin-tone-22);}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]):not(.dark) .ta-hero-shell > #today-assistant-strip{background:var(--vibe-skin-tone-23);border-bottom-color:var(--vibe-skin-tone-24);}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]):not(.dark) #today-assistant-strip .ta-head>strong{display:inline-flex;align-items:center;gap:6px;}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]):not(.dark) #today-assistant-strip .ta-head>strong::before{content:"";width:5px;height:5px;border-radius:50%;background:var(--vibe-skin-tone-25);}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]):not(.dark) #today-assistant-strip .ta-main>.btn{background:var(--vibe-skin-tone-26);color:var(--vibe-skin-tone-11);border-color:var(--vibe-skin-tone-26);}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]):not(.dark) #today-assistant-strip .btn-ghost{background:var(--vibe-skin-tone-27);border-color:var(--vibe-skin-tone-28);}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]):not(.dark) .now-hero-subject{color:var(--vibe-skin-tone-29);}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]):not(.dark) .ta-hero-shell .now-stat-icon{background:none!important;color:var(--vibe-skin-tone-30);}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]):not(.dark) .now-stat-num{color:var(--vibe-skin-tone-31);}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]):not(.dark) .now-stat{border-color:var(--vibe-skin-tone-32);}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]):not(.dark) .tb-card .tb-glyph{background:var(--vibe-skin-tone-33);border-color:var(--vibe-skin-tone-34);color:var(--vibe-skin-tone-35);}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]):not(.dark) .ck-time{font-weight:650;letter-spacing:-1.3px;color:var(--vibe-skin-tone-36);}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]):not(.dark) .ck-sec{color:var(--vibe-skin-tone-37);font-size:.45em;}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]):not(.dark) .tb-previous-button{background:var(--vibe-skin-tone-38);}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]):not(.dark) input:not([type=checkbox]),.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]):not(.dark) textarea{background:var(--vibe-skin-tone-39);}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]):not(.dark) .today-card,.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]):not(.dark) .btn,.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]):not(.dark) .tb-glyph,.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]):not(.dark) .week-cell{transition:box-shadow .2s ease,border-color .2s ease,background-color .2s ease,transform .2s ease;}
.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"])[data-workspace-motion=off] .workspace-atmosphere i{animation-play-state:paused;}
@keyframes workspace-glow-drift{from{transform:translate3d(-10vw,-6vh,0) scale(.95)}to{transform:translate3d(35vw,28vh,0) scale(1.12)}}
@keyframes workspace-glow-return{from{transform:translate3d(10vw,4vh,0)}to{transform:translate3d(-40vw,-28vh,0)}}
@media(hover:hover) and (pointer:fine){.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]):not(.dark) .today-card:hover{box-shadow:0 8px 24px var(--vibe-skin-tone-40);border-color:var(--vibe-skin-tone-41);}.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]):not(.dark) .btn:hover{transform:translateY(-1px);}}
@media(max-width:767px){.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]):not(.dark) .today-card,.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]):not(.dark) .today-card.is-secondary{border-radius:14px;}.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]):not(.dark) .ta-hero-shell.now-hero{border-radius:16px;}.workspace-atmosphere i{width:140vw;height:90vh;animation-duration:36s;}.workspace-atmosphere i+i{display:none;}}
@media(prefers-reduced-motion:reduce){.workspace-atmosphere i,.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]):not(.dark) .ta-hero-shell{animation:none;}.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]):not(.dark) .today-card,.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]):not(.dark) .btn,.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]):not(.dark) .tb-glyph,.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]):not(.dark) .week-cell{transition:none;}.workspace-skin:is([data-workspace-skin="violet"],[data-workspace-skin="charcoal"]):not(.dark) .btn:hover{transform:none;}}

.workspace-skin .workspace-atmosphere[hidden]{display:none!important;}
.workspace-skin[data-workspace-skin=classic] .workspace-atmosphere{display:none;}
.workspace-skin[data-workspace-skin=violet] .bg-card{background-color:var(--vibe-skin-tone-card,rgba(255,255,255,.86));}
:root.workspace-skin{--vibe-skin-tone-card:rgba(255,255,255,.86);--vibe-swatch-classic:#5b70ac;--vibe-swatch-violet:#7b61ff;--vibe-swatch-charcoal:#e2b28a;--vibe-swatch-dark:#1b1d20;--vibe-swatch-dark-sub:#2d2f35;--vibe-swatch-light:#fff;--vibe-swatch-pale:#f1f4f8;}
.workspace-skin .workspace-options{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;padding:0;margin:16px 0;border:0;min-width:0;}
.workspace-skin .workspace-option{display:flex;flex-direction:column;gap:7px;min-width:0;padding:9px;border:1px solid var(--vibe-border);border-radius:11px;cursor:pointer;background:var(--vibe-card);}
.workspace-skin .workspace-option:has(input:checked){border-color:var(--vibe-primary);box-shadow:0 0 0 1px var(--vibe-primary);}
.workspace-skin .workspace-option:focus-within{outline:2px solid var(--vibe-ring);outline-offset:3px;}
.workspace-skin .workspace-mini{height:64px;display:flex;gap:5px;padding:7px;border-radius:6px;background:var(--vibe-swatch-pale);border:1px solid var(--vibe-border);}
.workspace-skin .workspace-mini-nav{width:18%;border-radius:3px;background:var(--vibe-swatch-light);}
.workspace-skin .workspace-mini-main{flex:1;display:grid;grid-template-columns:1fr 1fr;gap:4px;}
.workspace-skin .workspace-mini-main>span{background:var(--vibe-swatch-light);border-radius:3px;}
.workspace-skin .workspace-swatch-classic{--vibe-swatch-accent:var(--vibe-swatch-classic);}
.workspace-skin .workspace-mini-main>span:first-child{grid-column:1/-1;border-top:3px solid var(--vibe-swatch-accent,var(--vibe-swatch-classic));}
.workspace-skin .workspace-swatch-violet .workspace-mini-main>span:first-child{border-top-color:var(--vibe-swatch-violet);}
.workspace-skin .workspace-swatch-charcoal .workspace-mini{background:var(--vibe-swatch-dark);}
.workspace-skin .workspace-swatch-charcoal .workspace-mini-nav,.workspace-skin .workspace-swatch-charcoal .workspace-mini-main>span{background:var(--vibe-swatch-dark-sub);}
.workspace-skin .workspace-swatch-charcoal .workspace-mini-main>span:first-child{border-top-color:var(--vibe-swatch-charcoal);}
.workspace-skin .workspace-choice-name{display:flex;align-items:center;gap:6px;min-height:27px;}
.workspace-skin .workspace-choice-name strong{font-size:13px;font-weight:600;}
.workspace-skin .workspace-option small,.workspace-skin .workspace-appearance-hint{font-size:12px;color:var(--vibe-muted-foreground);line-height:1.65;margin:0;}
.workspace-skin input[name=workspace-skin],.workspace-skin #workspace-motion{accent-color:var(--vibe-primary);width:15px;height:15px;flex-shrink:0;}
.workspace-skin .workspace-motion-label{display:flex;gap:10px;align-items:flex-start;padding:12px 0;min-height:44px;cursor:pointer;}
.workspace-skin .workspace-motion-label small{display:block;font-size:11px;color:var(--vibe-muted-foreground);margin-top:4px;}
@media(max-width:400px){.workspace-skin .workspace-options{gap:7px;}.workspace-skin .workspace-option{padding:7px;}.workspace-skin .workspace-option small{font-size:11px;}.workspace-skin .workspace-mini{padding:5px;height:54px;}}

/* 浅色保留截图的配色与布局，仅在开关打开时增加低强度流动。 */
:root.workspace-skin {
  --vibe-classic-glow-one: rgba(143,174,205,.15);
  --vibe-classic-glow-one-edge: rgba(181,202,223,.07);
  --vibe-classic-glow-two: rgba(175,197,220,.12);
  --vibe-classic-glow-two-edge: rgba(202,216,232,.06);
  --vibe-classic-clear-card: rgba(255,255,255,.95);
}
.workspace-skin[data-workspace-skin=classic] body { isolation:isolate; }
.workspace-skin[data-workspace-skin=classic][data-workspace-motion=on] body { background-image:none!important; }
.workspace-skin[data-workspace-skin=classic][data-workspace-motion=on] .workspace-atmosphere {
  display:block;position:fixed;inset:0;z-index:-1;overflow:hidden;contain:strict;pointer-events:none;background:transparent;
}
.workspace-skin[data-workspace-skin=classic] .workspace-atmosphere i {
  background:radial-gradient(ellipse,var(--vibe-classic-glow-one),var(--vibe-classic-glow-one-edge) 35%,transparent 70%);
}
.workspace-skin[data-workspace-skin=classic] .workspace-atmosphere i+i {
  background:radial-gradient(ellipse,var(--vibe-classic-glow-two),var(--vibe-classic-glow-two-edge) 40%,transparent 70%);
}
.workspace-skin[data-workspace-skin=classic][data-workspace-motion=on] .today-card { background:var(--vibe-classic-clear-card); }
.workspace-skin[data-workspace-motion=off] .workspace-atmosphere i { animation-play-state:paused; }

/* 局部排版：不覆盖卡片的高度、列归属和排序。 */
.workspace-skin .today-grid,.workspace-skin .today-col { gap:16px; }
.workspace-skin .today-head { gap:8px; }
.workspace-skin .today-head h3 { min-width:0; }
@media(max-width:767px) {
  .workspace-skin .today-grid,.workspace-skin .today-col { gap:12px; }
  .workspace-skin #today-assistant-strip .ta-head .btn,
  .workspace-skin #today-assistant-strip .ta-main>.btn { min-height:44px; }
}

/* 三套皮肤的状态颜色：文字按钮与数据语义分开，危险操作不跟品牌色走。 */
:root.workspace-skin {
  --vibe-danger-fill:#b91c1c;
  --vibe-danger-fill-hover:#991b1b;
  --vibe-danger-surface:#fff1f2;
  --vibe-state-selected-bg:var(--vibe-brand-50);
  --vibe-state-selected-ink:var(--vibe-primary-ink);
}
:root.workspace-skin[data-workspace-skin=violet]:not(.dark) {
  /* 保留装饰主色，承载小字号白字的实心按钮再深一档。 */
  --vibe-brand-800:#6e52e5;
  --vibe-muted-foreground:#696d7a;
  --vibe-nav-muted:#696d7a;
}
:root.workspace-skin[data-workspace-skin=charcoal] { --vibe-danger-surface:#352326; }
.workspace-skin .btn-ghost:not(:disabled):not(.is-danger):hover,
.workspace-skin .btn-secondary:not(:disabled):not(.is-danger):hover { background:var(--vibe-muted);color:var(--vibe-foreground); }
.workspace-skin .btn-ghost:not(:disabled):not(.is-danger):active,
.workspace-skin .btn-secondary:not(:disabled):not(.is-danger):active { background:var(--vibe-border); }
.workspace-skin #today-assistant-strip .btn-ghost:not(:disabled):hover { background:var(--vibe-muted); }
.workspace-skin #today-assistant-strip .btn-ghost:not(:disabled):active { background:var(--vibe-border); }
.workspace-skin .btn.is-danger { background:var(--vibe-danger-fill);color:var(--vibe-destructive-foreground); }
.workspace-skin .btn.is-danger:not(:disabled):hover,
.workspace-skin .btn.is-danger:not(:disabled):active { background:var(--vibe-danger-fill-hover); }
.workspace-skin .a2-btn.is-danger:not(:disabled):hover { background:var(--vibe-danger-surface);color:var(--vibe-destructive-ink); }
.workspace-skin .a2-chip.is-on,
.workspace-skin .acad-page #an-cohort-chips .a2-chip.is-on,
.workspace-skin .acad-page .acad-subjects .a2-chip.is-on,
.workspace-skin .acad-page .acad-report-nav>.a2-btn[aria-pressed=true],
.workspace-skin .acad-page .acad-student-subjects .a2-btn[aria-pressed=true] {
  background:var(--vibe-state-selected-bg);color:var(--vibe-state-selected-ink);border-color:var(--vibe-brand-200);
}
.workspace-skin .hub-card:active,
.workspace-skin .tb-line[aria-expanded=true] { background:var(--vibe-state-selected-bg); }
.workspace-skin .a2-card.is-link:active,
.workspace-skin .a2-wcard:active { background:var(--vibe-state-selected-bg);border-color:var(--vibe-primary); }
.workspace-skin .wc-chart-fit svg>path[opacity="0.5"] { opacity:.85; }
.workspace-skin .btn:disabled:not(.btn-ghost):not(.btn-secondary):not(.is-danger),
.workspace-skin .a2-btn.is-primary:disabled { background:var(--vibe-brand-800); }
:root.workspace-skin[data-workspace-skin=violet] .week-cell.is-sel,
:root.workspace-skin[data-workspace-skin=violet] .week-cell.is-sel:hover,
.workspace-skin[data-workspace-skin=violet] .set-cat.is-on,
.workspace-skin[data-workspace-skin=violet] .bg-brand-500.text-white { background:var(--vibe-brand-800); }

/* 独立模块的实心操作也使用同一文字底色；不覆盖大屏画面示意图。 */
.workspace-skin .ft-app button.ft-primary,
.workspace-skin .af-app button.af-primary,
.workspace-skin .cm-btn-main,
.workspace-skin .sa2-save,
.workspace-skin .sa2-main-act,
.workspace-skin .sa2-send,
.workspace-skin .sa2-look-btn,
.workspace-skin .af-steps .is-active em {
  background:var(--vibe-brand-800);border-color:var(--vibe-brand-800);color:var(--vibe-primary-foreground);
}
.workspace-skin .ft-app button.ft-primary:not(:disabled):hover,
.workspace-skin .af-app button.af-primary:not(:disabled):hover,
.workspace-skin .cm-btn-main:not(:disabled):hover,
.workspace-skin .sa2-save:not(:disabled):hover,
.workspace-skin .sa2-main-act:not(:disabled):hover,
.workspace-skin .sa2-send:not(:disabled):hover,
.workspace-skin .sa2-look-btn:not(:disabled):hover { background:var(--vibe-brand-900);border-color:var(--vibe-brand-900); }
.workspace-skin .a2-fab { background:var(--vibe-brand-800);color:var(--vibe-primary-foreground);box-shadow:var(--vibe-shadow-md); }
.workspace-skin .sa2-main-act { box-shadow:var(--vibe-shadow-md); }
.workspace-skin .sa2-main-act small { opacity:1; }

/* 今日主卡：磨砂玻璃 + 窗外透光（2026-10-24 用户从 6 个方案里选定「6 窗光」，并要求动起来）
   ① 卡身半透（62%）+ backdrop-filter：页面里那层暖光会真的从卡后透上来，而不是画在卡面上；
      卡顶 1.5px 漏光 + 左右各 1px 边缘高光 + 底部内阴影 = 玻璃的厚度感。
   ② 卡底铺一层纸纹（feTurbulence，透明度烘在 SVG 里，不靠 mix-blend-mode）；
      卡面 ::before 是两团右上角的暖光（窗外那道），::after 是每 14 秒一次的斜向扫光。
   ③ 动效只动光、不动卡：光团慢速漂移 17s（background-position）+ 明暗呼吸（opacity），
      扫光 14s 一轮只亮 3 秒；卡外页面暖光在背后漂（26s/34s）。
      ★ 0.24.167 的教训：scale() 作用在铺满卡片的渐变层上，光斑边界会在按钮区来回扫，
        用户读成「右上角那一块也在缩放」。所以这里一律只用 opacity / background-position，
        不对卡身、文字、按钮做任何 transform。
   ⚠️ Win7 的 Chrome 109 不支持 color-mix，这里全部用 rgba，别改回去。 */
:root.workspace-skin {
  --vibe-workspace-hero-halo-spec:rgba(255,255,255,.95);
  --vibe-hero-glass-bg:rgba(255,253,248,.62);
  --vibe-hero-glass-line:rgba(255,255,255,.72);
  --vibe-hero-glass-top:rgba(255,255,255,.98);
  --vibe-hero-glass-side:rgba(255,255,255,.46);
  --vibe-hero-glass-shade:rgba(120,80,50,.22);
  --vibe-hero-glass-drop:rgba(120,80,50,.50);
  --vibe-hero-glass-mist:rgba(255,249,238,.80);
  --vibe-hero-glass-sheen:rgba(255,255,255,.88);
  --vibe-hero-window-core:rgba(255,170,106,.58);
  --vibe-hero-window-soft:rgba(255,214,180,.58);
  --vibe-hero-window-halo:rgba(255,166,102,.60);
  --vibe-hero-grain:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncA type='linear' slope='.14'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)'/%3E%3C/svg%3E");
}
/* 柔紫：冷底，窗光用淡紫而不是暖橙，否则在 #fafdff 上会串成脏黄。 */
:root.workspace-skin[data-workspace-skin=violet]:not(.dark) {
  --vibe-hero-glass-bg:rgba(255,255,255,.60);
  --vibe-hero-glass-mist:rgba(255,255,255,.82);
  --vibe-hero-glass-shade:rgba(90,90,140,.18);
  --vibe-hero-glass-drop:rgba(90,80,140,.44);
  --vibe-hero-window-core:rgba(141,120,255,.26);
  --vibe-hero-window-soft:rgba(196,188,255,.38);
  --vibe-hero-window-halo:rgba(141,120,255,.34);
}
/* 经典是冷灰蓝底，硬塞暖橙会串色：窗光改成偏冷的天光，和 brand 蓝同一族。 */
:root.workspace-skin[data-workspace-skin=classic] {
  --vibe-hero-window-core:rgba(190,208,230,.55);
  --vibe-hero-window-soft:rgba(236,243,252,.60);
  --vibe-hero-window-halo:rgba(168,192,222,.45);
}
:root.workspace-skin[data-workspace-skin=charcoal] {
  --vibe-workspace-hero-halo-spec:rgba(255,255,255,.16);
  --vibe-hero-glass-bg:rgba(38,34,30,.62);
  --vibe-hero-glass-line:rgba(255,255,255,.10);
  --vibe-hero-glass-top:rgba(255,255,255,.16);
  --vibe-hero-glass-side:rgba(255,255,255,.06);
  --vibe-hero-glass-shade:rgba(0,0,0,.36);
  --vibe-hero-glass-drop:rgba(0,0,0,.55);
  --vibe-hero-glass-mist:rgba(255,236,214,.14);
  --vibe-hero-glass-sheen:rgba(255,236,214,.20);
  --vibe-hero-window-core:rgba(226,178,138,.20);
  --vibe-hero-window-soft:rgba(226,178,138,.12);
  --vibe-hero-window-halo:rgba(226,178,138,.22);
  --vibe-hero-grain:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncA type='linear' slope='.06'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)'/%3E%3C/svg%3E");
}
.workspace-skin #today-assistant-hero {
  position:relative;
  isolation:isolate;
  overflow:hidden;
  background-color:var(--vibe-hero-glass-bg);
  background-image:
    var(--vibe-hero-grain),
    linear-gradient(197deg,var(--vibe-hero-glass-mist) 0%,rgba(255,249,238,.14) 16%,rgba(255,249,238,0) 32%),
    radial-gradient(26% 34% at 92% -2%,var(--vibe-workspace-hero-halo-spec) 0%,rgba(255,255,255,0) 100%);
  background-repeat:repeat,no-repeat,no-repeat;
  background-size:120px 120px,auto,auto;
  border:1px solid var(--vibe-hero-glass-line);
  -webkit-backdrop-filter:blur(18px) saturate(1.12);
  backdrop-filter:blur(18px) saturate(1.12);
  box-shadow:
    inset 0 1.5px 0 var(--vibe-hero-glass-top),
    inset 1px 0 0 var(--vibe-hero-glass-side),
    inset -1px 0 0 var(--vibe-hero-glass-side),
    inset 0 -30px 44px -36px var(--vibe-hero-glass-shade),
    0 -16px 56px -26px var(--vibe-hero-window-halo),
    0 1px 1px rgba(120,80,50,.05),
    0 30px 60px -36px var(--vibe-hero-glass-drop);
}
/* 窗外透光：右上角两团虚化暖光（相当于方案里的两块 blur 光源），慢速上下漂移 + 明暗呼吸。
   只动 background-position / opacity，不对卡身、文字、按钮做任何 transform —— 0.24.167 的教训。 */
.workspace-skin #today-assistant-hero::before {
  content:"";position:absolute;inset:0;z-index:1;pointer-events:none;border-radius:inherit;
  background-image:
    radial-gradient(54% 150% at 80% -42%,var(--vibe-hero-window-core) 0%,transparent 72%),
    radial-gradient(34% 110% at 104% -8%,var(--vibe-hero-window-soft) 0%,transparent 76%);
  background-repeat:no-repeat;
  opacity:.9;
}
.workspace-skin[data-workspace-motion=on] #today-assistant-hero::before {
  animation:workspace-hero-window 17s ease-in-out infinite;
}
@keyframes workspace-hero-window {
  0%,100% { background-position:0 0,0 0;opacity:.86; }
  50% { background-position:2% 7%,-3% 4%;opacity:1; }
}
/* 斜向扫光：每 14 秒过一次，只亮 3 秒，其余时间完全静止（频繁闪会显廉价）。 */
.workspace-skin #today-assistant-hero::after {
  content:"";position:absolute;inset:0;z-index:1;pointer-events:none;border-radius:inherit;
  background-image:linear-gradient(104deg,transparent 44%,var(--vibe-hero-glass-sheen) 50%,transparent 56%);
  background-size:90% 140%;
  background-repeat:no-repeat;
  background-position:125% 0;
  opacity:0;
}
.workspace-skin[data-workspace-motion=on] #today-assistant-hero::after {
  animation:workspace-hero-sheen 14s cubic-bezier(.4,0,.3,1) infinite;
}
@keyframes workspace-hero-sheen {
  0%,54% { background-position:125% 0;opacity:0; }
  57% { opacity:.55; }
  78% { background-position:-25% 0;opacity:0; }
  100% { background-position:-25% 0;opacity:0; }
}
.workspace-skin #today-assistant-hero > #today-assistant-strip,
.workspace-skin #today-assistant-hero > #ov-now { position:relative;z-index:2;background:transparent; }
@media (prefers-reduced-motion: reduce) {
  .workspace-skin #today-assistant-hero::before,
  .workspace-skin #today-assistant-hero::after { animation:none; }
  .workspace-skin #today-assistant-hero::after { opacity:0; }
}

/* 暖橙：恢复旧版暖纸与深棕导航，保留现有卡片分组、数据与光晕。 */
:root.workspace-skin[data-workspace-skin=warm]:not(.dark) {
  --vibe-background:#efe9dd;--vibe-foreground:#211d1a;
  --vibe-card:#fffefb;--vibe-card-foreground:#211d1a;--vibe-popover:#fffefb;--vibe-popover-foreground:#211d1a;
  --vibe-muted:#e8e1d2;--vibe-muted-foreground:#5c5650;--vibe-border:#ded5c4;--vibe-input:#ded5c4;
  --vibe-primary:#f7663c;--vibe-primary-foreground:#fff7f3;--vibe-primary-ink:var(--vibe-brand-800);
  --vibe-secondary:#4a413a;--vibe-secondary-foreground:#fff;--vibe-accent:#f1481e;--vibe-accent-foreground:#fff7f3;--vibe-ring:#f7663c;
  --vibe-card-sub:#f7f3eb;--vibe-line-soft:#ede7dd;--vibe-line-faint:#f2eee5;
  --vibe-brand-50:#fff4ee;--vibe-brand-100:#ffe6dc;--vibe-brand-200:#ffc8b5;--vibe-brand-300:#ffab8f;--vibe-brand-400:#ff845f;
  --vibe-brand-500:#f7663c;--vibe-brand-600:#f1481e;--vibe-brand-700:#d63a14;--vibe-brand-800:#ad2f13;--vibe-brand-900:#8a2914;
  --vibe-brand-line:#ffd8ca;--vibe-brand-line-soft:#ffe6dc;--vibe-brand-wash-55:#fff1ea;
  --vibe-nav-bg:#2b2521;--vibe-nav-fg:#f3ede3;--vibe-nav-muted:#c1b7aa;--vibe-nav-line:rgba(255,255,255,.085);
  --vibe-nav-hover:rgba(255,255,255,.065);--vibe-nav-active-bg:#f7663c;--vibe-nav-active-fg:#211d1a;
  --vibe-shadow-xs:0 1px 1px rgba(60,50,40,.03);--vibe-shadow-sm:0 1px 2px rgba(60,50,40,.05);
  --vibe-shadow-md:0 1px 2px rgba(60,50,40,.05),0 10px 30px -18px rgba(60,50,40,.22);
  --vibe-shadow-lg:0 2px 4px rgba(60,50,40,.05),0 18px 40px -20px rgba(60,50,40,.3);
  --vibe-workspace-glow:rgba(247,102,60,.06);--vibe-workspace-blush:rgba(247,102,60,.025);--vibe-workspace-hero:var(--vibe-card);
}
.workspace-skin[data-workspace-skin=warm] body {isolation:isolate;background-image:none;}
.workspace-skin[data-workspace-skin=warm] .logo-mark {background:var(--vibe-brand-800)!important;}
.workspace-skin[data-workspace-skin=warm] .set-cat.is-on,
.workspace-skin[data-workspace-skin=warm] .todo-add .todo-add-btn,
.workspace-skin[data-workspace-skin=warm] .kd-cal-day.is-sel,
.workspace-skin[data-workspace-skin=warm] .bg-brand-500.text-white {background:var(--vibe-brand-800);color:var(--vibe-primary-foreground);}
.workspace-skin[data-workspace-skin=warm] .workspace-atmosphere {display:none;}
.workspace-skin[data-workspace-skin=warm][data-workspace-motion=on] .workspace-atmosphere {
  display:block;position:fixed;inset:0;z-index:-1;overflow:hidden;contain:strict;pointer-events:none;
}
.workspace-skin[data-workspace-skin=warm] .workspace-atmosphere i {background:radial-gradient(ellipse,var(--vibe-workspace-glow),transparent 70%);}
.workspace-skin[data-workspace-skin=warm] .workspace-atmosphere i+i {background:radial-gradient(ellipse,var(--vibe-workspace-blush),transparent 70%);}
.workspace-skin .workspace-options {grid-template-columns:repeat(4,minmax(0,1fr));}
.workspace-skin .workspace-swatch-warm {--vibe-swatch-warm-paper:#efe9dd;--vibe-swatch-warm-card:#fffefb;--vibe-swatch-warm-nav:#2b2521;--vibe-swatch-accent:#f7663c;}
.workspace-skin .workspace-swatch-warm .workspace-mini {background:var(--vibe-swatch-warm-paper);}
.workspace-skin .workspace-swatch-warm .workspace-mini-nav {background:var(--vibe-swatch-warm-nav);}
.workspace-skin .workspace-swatch-warm .workspace-mini-main>span {background:var(--vibe-swatch-warm-card);}
@media(max-width:560px) {.workspace-skin .workspace-options {grid-template-columns:repeat(2,minmax(0,1fr));}}
@media(max-width:767px) {
  .workspace-skin[data-workspace-skin=warm] .today-card {background:var(--vibe-card);border-color:var(--vibe-line-soft);box-shadow:var(--vibe-shadow-xs);}
  .workspace-skin[data-workspace-skin=warm] .today-head h3 {font-size:14px;font-weight:600;}
  .workspace-skin[data-workspace-skin=warm] .today-head h3 svg {background:transparent;color:var(--vibe-muted-foreground)!important;padding:0;}
  .workspace-skin[data-workspace-skin=warm] .tb-card .tb-glyph {background:transparent;color:var(--vibe-muted-foreground);}
  .workspace-skin[data-workspace-skin=warm] .tb-card .has-pending .tb-glyph {background:var(--vibe-brand-50);color:var(--vibe-primary-ink);}
  .workspace-skin[data-workspace-skin=warm] .hub-group {border:0;background:transparent;box-shadow:none;}
  .workspace-skin[data-workspace-skin=warm] .hub-group-title {font-size:14px;font-weight:600;color:var(--vibe-foreground);margin:0 2px 8px;}
  .workspace-skin[data-workspace-skin=warm] .hub-items {background:var(--vibe-card);border-color:var(--vibe-line-soft);box-shadow:var(--vibe-shadow-xs);}
  .workspace-skin[data-workspace-skin=warm] .hub-items .hub-card {gap:4px;padding:8px 2px;min-height:76px;}
  .workspace-skin[data-workspace-skin=warm] .hub-items .hub-card .hub-icon {background:transparent;color:var(--vibe-primary-ink);width:32px;height:32px;}
  .workspace-skin[data-workspace-skin=warm] .hub-card:is([href="students.html"],[href="homework.html"],[href="hw-collect.html"]) .hub-icon {background:var(--vibe-brand-50);}
  .workspace-skin[data-workspace-skin=warm] .hub-title {font-size:14px;font-weight:500;}
  .workspace-skin[data-workspace-skin=warm] aside {background:var(--vibe-nav-bg)!important;border-color:var(--vibe-nav-line)!important;}
  .workspace-skin[data-workspace-skin=warm] aside .sidebar-nav .nav-tab {color:var(--vibe-nav-muted)!important;}
  .workspace-skin[data-workspace-skin=warm] aside .sidebar-nav .nav-tab.nav-current,
  .workspace-skin[data-workspace-skin=warm] aside .sidebar-nav .nav-tab.nav-tab-active-mobile {background:var(--vibe-nav-active-bg)!important;color:var(--vibe-nav-active-fg)!important;}
}
