/* ===== 设计 token：简约风，一处改全局生效 ===== */
:root {
  /* 主题三态（跟随系统 / 浅色 / 深色）由 /ui/theme-boot.js 在首帧之前定好，
     结果就是这个属性：`<html data-theme="light|dark">`。**配色只认它**
     （以前这里是一段 `@media (prefers-color-scheme: dark)`，用户没法自己选深色）。 */
  color-scheme: light;
  --bg: #f5f6f8;
  --surface: #ffffff;
  --surface-2: #fafbfc;
  --text: #1b1f24;
  --muted: #6b7280;
  --line: #e6e8ec;
  --brand: #3b6ef6;
  --brand-soft: #eaf0ff;
  --danger: #ef4444;
  --ok: #10b981;
  --warn: #f59e0b;

  --c-course: #3b6ef6;
  --c-task: #f59e0b;
  --c-exam: #ef4444;
  --c-activity: #10b981;
  --c-personal: #8b5cf6;
  --c-other: #64748b;

  /*
   * 课程表的"一门课一个色"（相邻课程颜色不同，一眼分得开）。
   * 都是中等饱和度的实色，配白字有足够对比度。
   * 挑色时避开了语义色（红=考试、琥珀=任务），免得学生看到红色块以为要考试。
   */
  --cc-1: #2f6feb;
  --cc-2: #0e9f6e;
  --cc-3: #7c5cf0;
  --cc-4: #d97706;
  --cc-5: #0e8fa8;
  --cc-6: #db2777;
  --cc-7: #65a30d;
  --cc-8: #e11d48;
  --cc-9: #4f46e5;
  --cc-10: #b45309;
  --cc-11: #0891b2;
  --cc-12: #9333ea;

  --r-sm: 8px;
  --r-md: 12px;
  --r-lg: 16px;
  --shadow: 0 1px 2px rgba(16, 24, 40, .05), 0 8px 24px -12px rgba(16, 24, 40, .18);
  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB",
          "Microsoft YaHei", "Noto Sans SC", Roboto, sans-serif;
  --sidebar-w: 210px;
  /* 侧栏收起（只留图标）/ 展开（显示文字）的宽度。
     投放区要**盖住侧栏**，它的 left 偏移必须与展开宽度一致 ——
     两个值都从这里取，避免改了一处忘另一处。 */
  --sidebar-w-collapsed: 62px;
  --sidebar-w-expanded: 210px;
  --tabbar-h: 58px;
  /* 顶栏有多高 —— 给"钉在顶部的块"用（清单的分组头、课程表的表头）。
     ⚠️ 为什么是变量而不是各处写死一个数：顶栏高度跟着标题/副标题/字号（个性化里的 `--fs`）变，
        实测手机 58、平板与桌面 70 —— 写死就会在另一档偏 12px（表头被顶栏盖住或浮在下方露缝）。
     真实值由 `web/ui/app.js` 的 `trackTopbarHeight()` 量出来写回 `documentElement`；
     这里这个数是**兜底**：JS 没跑（或第一帧还没量）时也得有个对得上的值。 */
  --topbar-h: 58px;

  /* ===== 内容围栏（地基 G3）=====
     宽屏上正文不该一路拉到头（一行 100+ 字读起来很累），所以给"内容列"一条上限。
     用**内边距**而不是 `max-width` 实现，是因为这样：
       · 顶栏的底边线、横幅的底色仍然通铺整窗（`max-width` 会把它们一起夹窄，
         于是标题贴左、正文居中，两块对不齐 —— 这是最容易踩的坑）；
       · 内容列与顶栏标题、横幅用**同一条**公式，天然对齐。
     公式：可用宽度比 --content-max 宽出来的部分，两边各分一半；
     窄屏（可用宽度 ≤ 上限）时 `max(…)` 取回下界 14px，等于**没这个围栏**。
     下界为什么是 14px 而不是 20px：手机上正文用 14px 边距（屏幕小，20px 会显得挤），
     平板（800–1080px）如果退回 20px，跨过 800px 那一下文字会横向跳 6px（"端分离"线
     候选 #10 量的就是这个）。把下界做成 14px 之后，phone / 平板 / 桌面是**同一条连续
     公式**：≤1080px 都是 14px，再往上随宽度线性长到上限——没有任何门槛上的跳变。
     ⚠️ 气泡画布（`.app.view-bubble`）覆盖成 0，画布必须铺满 —— 见 layout.css 里那条规则
        和 tools/content-fence.test.mjs。 */
  --content-max: 1080px;
  --pad-inline: max(14px, calc((100% - var(--content-max)) / 2));

  /* ===== 字号（「个性化」页里的独立选项）=====
     三档由 /ui/theme-boot.js 写成 `<html data-font="small|medium|large">`，
     这里只给系数；**全仓每个 `font-size` 都写成 `calc(Npx * var(--fs))`** ——
     一处改、到处跟着变（别再写裸 px：写了就跟不了字号档）。
     ⚠️ canvas 里的字**读不到 CSS 变量**，它们走 `__ttTheme.fontScale()`
        （同一个系数表，别在这儿和 JS 里各写一份）。 */
  --fs: 1;
}

:root[data-font="small"] { --fs: 0.9; }
:root[data-font="large"] { --fs: 1.15; }

:root[data-theme="dark"] {
  color-scheme: dark;
  --bg: #0f1115;
  --surface: #171a21;
  --surface-2: #1c2029;
  --text: #e8eaed;
  --muted: #9aa2af;
  --line: #262b34;
  --brand: #6b93ff;
  --brand-soft: #1b2440;
  --shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 12px 28px -16px rgba(0, 0, 0, .6);
  /*
   * 语义色里只有 --danger 需要换：它绝大多数场合是**前景文字**（`.btn-danger`、
   * 回收站的"晚完成"、错误提示…），#ef4444 压在 #171a21 上对比度只有 3.9:1，
   * 小字看着发闷；#f87171 是 6.0:1。--ok(#10b981, 5.4:1) 与 --warn(#f59e0b, 7.9:1)
   * 本来就够，而且它们常被当**底色**配白字用（`.check.on`），调亮反而更糟。
   * 课程色板 `--c-*` / `--cc-*` 也不用换：它们的用法几乎都是
   * `color-mix(in srgb, var(--c) 12~16%, var(--surface))`（本来就跟 surface 混），
   * 直接当底色的那几处配的是白字，调亮会让白字读不清。
   */
  --danger: #f87171;
}

* { box-sizing: border-box; }

html, body { height: 100%; }

body {
  margin: 0;
  font-family: var(--font);
  font-size: calc(14px * var(--fs));
  line-height: 1.5;
  color: var(--text);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
  -webkit-tap-highlight-color: transparent;
}

h1, h2, h3, h4, p { margin: 0; }
button { font: inherit; color: inherit; }
ul { margin: 0; padding: 0; list-style: none; }

::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-thumb { background: var(--line); border-radius: 999px; border: 3px solid transparent; background-clip: content-box; }
::-webkit-scrollbar-thumb:hover { background: var(--muted); background-clip: content-box; }

/* ===== 通用小件 ===== */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  padding: 8px 14px; min-height: 36px;
  border: 1px solid var(--line); border-radius: var(--r-sm);
  background: var(--surface); cursor: pointer;
  transition: background .15s, border-color .15s, transform .06s;
  white-space: nowrap;
}
.btn:hover { background: var(--surface-2); }
.btn:active { transform: scale(.98); }
.btn-primary { background: var(--brand); border-color: var(--brand); color: #fff; }
.btn-primary:hover { filter: brightness(1.06); background: var(--brand); }
.btn-ghost { background: transparent; border-color: transparent; }
.btn-danger { color: var(--danger); border-color: var(--line); }
.btn-danger:hover { background: rgba(239, 68, 68, .08); }
.btn-block { width: 100%; }
.btn-sm { min-height: 30px; padding: 4px 10px; font-size: calc(13px * var(--fs)); }

.icon-btn {
  width: 34px; height: 34px; display: inline-grid; place-items: center;
  border: 1px solid var(--line); border-radius: var(--r-sm);
  background: var(--surface); cursor: pointer; font-size: calc(15px * var(--fs));
}
.icon-btn:hover { background: var(--surface-2); }

.chip {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 2px 8px; border-radius: 999px;
  background: var(--surface-2); border: 1px solid var(--line);
  font-size: calc(12px * var(--fs)); color: var(--muted);
}
.tiny { font-size: calc(12px * var(--fs)); color: var(--muted); }
.muted { color: var(--muted); }
.only-desk { display: inline; }
.only-mobile { display: none; }

input, select, textarea {
  font: inherit; color: inherit; width: 100%;
  padding: 9px 11px; min-height: 38px;
  border: 1px solid var(--line); border-radius: var(--r-sm);
  background: var(--surface); outline: none;
  transition: border-color .15s, box-shadow .15s;
}

/*
 * ⚠️ `hidden` 属性必须真的能隐藏元素。
 *
 * 浏览器的内置样式是 `[hidden] { display: none }`，但它的**优先级极低** ——
 * 任何带 `display` 的类规则都会盖掉它，比如 `.bubble-panel { display: flex }`。
 * 后果：JS 里设了 `el.hidden = true`，元素却照样显示（本该折叠的区块，
 * 结果一进页面全展开，一屏全展开 —— 用户报"导入界面好奇怪"）。
 *
 * 项目里已经有 4 处零散补丁（.bubble-inside-hint / .bubble-panel /
 * .modal-host / .bubble-dropzone 各自写了 `[hidden] { display: none }`）。
 * 与其一个个补，不如在这里统一修掉，以后新增组件不会再中招。
 * 用 !important 是刻意的：hidden 表达的是"不要显示"，任何 display 都该让路。
 */
[hidden] { display: none !important; }

/*
 * ⚠️ 复选框/单选框必须**排除**在上面的规则之外。
 *
 * 上面那条是给文本输入框写的（撑满一行、38px 高、有内边距），
 * 但选择器 `input` 把 `<input type="checkbox">` 也命中了 ——
 * 后果是复选框被撑成 100% 宽（实测 262px）并把同一行的文字挤成 0 宽，
 * 于是课程名变成"军/事/理/论"一字一行（用户报"导入界面好奇怪"就是这个）。
 *
 * 以前只在个别组件里局部补救（`.chip-row input { flex: none }`），
 * 但 `flex: none` 挡不住 `width: 100%`。所以在这里统一修掉，
 * 任何地方用复选框都不会再中招。
 *
 * 注意还要给 align-self: flex-start —— 父级 flex 容器若是 stretch，
 * 复选框会被拉高变形。
 */
input[type='checkbox'], input[type='radio'] {
  width: auto;
  min-width: 0;
  min-height: 0;
  height: auto;
  padding: 0;
  margin: 0;
  border: 0;
  border-radius: 0;
  background: none;
  flex: none;
  align-self: flex-start;
  accent-color: var(--brand);
}

input:focus, select:focus, textarea:focus {
  border-color: var(--brand);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand) 18%, transparent);
}
textarea { min-height: 72px; resize: vertical; }
label { display: block; font-size: calc(12px * var(--fs)); color: var(--muted); margin-bottom: 5px; }

/* 类型色 */
.t-course { --c: var(--c-course); }
.t-task { --c: var(--c-task); }
.t-exam { --c: var(--c-exam); }
.t-activity { --c: var(--c-activity); }
.t-personal { --c: var(--c-personal); }
.t-other { --c: var(--c-other); }

@media (max-width: 799px) {
  .only-desk { display: none; }
  .only-mobile { display: inline-grid; }
}
