/* ===== 视图样式 ===== */

/* --- 通用卡片 --- */
.card {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-lg); box-shadow: var(--shadow);
}

/* --- 空状态 --- */
.empty {
  display: grid; place-items: center; gap: 10px;
  padding: 56px 20px; text-align: center; color: var(--muted);
}
.empty .empty-ico { font-size: calc(34px * var(--fs)); opacity: .5; }
.empty h3 { color: var(--text); font-size: calc(15px * var(--fs)); font-weight: 600; }

/* ============ 月历 ============ */
.month-head {
  display: grid; grid-template-columns: repeat(7, 1fr);
  border-bottom: 1px solid var(--line);
  background: var(--surface-2);
  border-radius: var(--r-lg) var(--r-lg) 0 0;
}
.month-head span { padding: 8px 0; text-align: center; font-size: calc(12px * var(--fs)); color: var(--muted); }
.month-head span.weekend { color: color-mix(in srgb, var(--muted) 70%, var(--danger)); }

.month-grid { display: grid; grid-template-columns: repeat(7, 1fr); }
.month-cell {
  min-height: 108px; padding: 6px 7px 7px;
  border-right: 1px solid var(--line); border-bottom: 1px solid var(--line);
  cursor: pointer; position: relative; overflow: hidden;
  transition: background .12s;
}
.month-cell:nth-child(7n) { border-right: 0; }
.month-cell:hover { background: var(--surface-2); }
.month-cell.out { background: color-mix(in srgb, var(--bg) 60%, transparent); }
.month-cell.out .daynum { opacity: .38; }
.month-cell.today .daynum {
  background: var(--brand); color: #fff; border-radius: 999px;
  width: 22px; height: 22px; display: grid; place-items: center;
}
.month-cell .daynum { font-size: calc(12px * var(--fs)); font-weight: 600; margin-bottom: 4px; }
.month-cell .cell-head { display: flex; align-items: center; justify-content: space-between; }
.month-cell .cell-week { font-size: calc(10px * var(--fs)); color: var(--muted); }

.pill {
  display: flex; align-items: center; gap: 5px;
  padding: 2px 6px; margin-bottom: 3px;
  border-radius: 5px; font-size: calc(11.5px * var(--fs)); line-height: 1.45;
  background: color-mix(in srgb, var(--c) 12%, transparent);
  color: color-mix(in srgb, var(--c) 78%, var(--text));
  border-left: 2.5px solid var(--c);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.pill .pill-time { font-variant-numeric: tabular-nums; opacity: .8; flex: none; }
.pill .pill-title { overflow: hidden; text-overflow: ellipsis; }
.pill.done { opacity: .45; text-decoration: line-through; }
.more { font-size: calc(11px * var(--fs)); color: var(--muted); padding-left: 4px; }

@media (max-width: 799px) {
  /* 手机档的月历格：414px 下每格只有 54.8px 宽，原来 74px 高 ⇒ 一屏只看得见三四行、
     每条日程还只剩一个色条（时间被 `display:none` 掉了）。用户 10-13 说"手机版排版要
     更用心"，这里就是最明显的一处：格子加高到 96px，把**时间**放回来（几点上课是
     月历上第一要紧的信息），周次也一起回来 —— 但日子+周次+农历三样挤不进 54.8px，
     所以有农历的那天让周次让位（农历更"像日历"，也不占额外的字）。 */
  .month-cell { min-height: 96px; padding: 5px; }
  .pill { font-size: calc(10.5px * var(--fs)); padding: 1px 4px; border-left-width: 2px; }
  /* `.pill-time` 本来在手机档被藏起来（`display:none`）—— 现在留着：
     `flex: none` 保住"几点"，标题那半靠 `text-overflow` 截断，读起来仍是"几点 + 什么事"。 */
  .pill .pill-time { opacity: .75; }
  /* 三样挤不进一行时让一样让位（格子 53px 宽，`W42` + 农历两样都要就得 ~40px）。
     口径：**周一**显示周次（`W42`，学生记"第几周"就是按周一划的，日历也这么做），
     其余天显示农历（它能让月历"像个日历"）。`:has()` 在 Edge/Safari 都支持；
     没有农历的日子（很少）两样都不冲突，照常都显示。 */
  .month-cell:has(.cell-lunar) .cell-week { display: none; }
  .month-cell.is-mon:has(.cell-lunar) .cell-week { display: inline; }
  .month-cell.is-mon:has(.cell-lunar) .cell-lunar { display: none; }
}

/* ============ 清单 ============ */
.list { display: flex; flex-direction: column; gap: 14px; }
.list-group { display: flex; flex-direction: column; gap: 8px; }
.list-group-head {
  display: flex; align-items: baseline; gap: 8px; padding: 2px 4px;
  /* 钉在顶栏**下面**（不是视口顶）：原来写死 58，而平板/桌面顶栏实测 70 ⇒ 分组头有 12px
     被顶栏盖住。现在跟顶栏的真实高度（`--topbar-h`，app.js 量的）走。 */
  position: sticky; top: var(--topbar-h, 58px); background: var(--bg); z-index: 2;
}
.list-group-head h3 { font-size: calc(13px * var(--fs)); font-weight: 650; }
.list-group-head .count { font-size: calc(12px * var(--fs)); color: var(--muted); }

.row {
  display: flex; align-items: center; gap: 12px;
  padding: 11px 14px; border: 1px solid var(--line); border-radius: var(--r-md);
  background: var(--surface); cursor: pointer;
}
.row:hover { border-color: color-mix(in srgb, var(--brand) 35%, var(--line)); }
.row .bar { width: 3px; align-self: stretch; border-radius: 3px; background: var(--c); flex: none; }
.row .row-main { flex: 1; min-width: 0; }
.row .row-title { font-weight: 600; display: flex; align-items: center; gap: 6px; }
.row .row-meta { font-size: calc(12px * var(--fs)); color: var(--muted); display: flex; gap: 8px; flex-wrap: wrap; margin-top: 2px; }
.row .row-time { font-variant-numeric: tabular-nums; font-size: calc(13px * var(--fs)); color: var(--muted); flex: none; }
.row.done { opacity: .55; }
.row.done .row-title { text-decoration: line-through; }

.check {
  width: 22px; height: 22px; flex: none; border-radius: 50%;
  border: 1.5px solid var(--line); background: var(--surface);
  display: grid; place-items: center; cursor: pointer; font-size: calc(12px * var(--fs)); color: transparent;
}
.check:hover { border-color: var(--ok); }
.check.on { background: var(--ok); border-color: var(--ok); color: #fff; }

/* 触控目标：那个打勾圆点视觉仍是 22px，可点区域补到 **34×44**（手指比鼠标粗得多）。
   为什么宽度停在 34：圆点左右各 12px 是行内间隙（`.row { gap: 12px }`，左到 `.bar`、右到 `.row-main`），
   两边各留 6px 余量 —— 再往右扩就会把"点标题打开编辑"的那片吃掉（真浏览器量过间距）。
   高度能补满 44 是因为行高 66.5px，上下各有 22px 空地。 */
.check { position: relative; }
.check::after { content: ''; position: absolute; inset: -11px -6px; }

/* ============ 课程表 ============ */
.course-toolbar {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding: 10px 12px; margin-bottom: 12px;
}
.week-strip { display: flex; gap: 5px; overflow-x: auto; padding-bottom: 2px; }
.week-pill {
  min-width: 38px; padding: 5px 8px; border-radius: 999px; cursor: pointer;
  border: 1px solid var(--line); background: var(--surface); font-size: calc(12px * var(--fs)); flex: none;
}
.week-pill[aria-pressed="true"] { background: var(--brand); border-color: var(--brand); color: #fff; }

/* 「删除课程」对话框里的课程行（web/ui/views/course.js 的 openCourseManager）。
   一行一门课、右边一个删除按钮 —— 整行**不做**点击（删除是慎重的动作，
   所以不套用 .row 的 cursor/hover，免得看着像"点哪儿都能删"）。 */
.course-del-list { display: flex; flex-direction: column; gap: 8px; margin-top: 10px; }
.course-del-row {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 12px; border: 1px solid var(--line); border-radius: var(--r-md);
  background: var(--surface-2);
}
.course-del-row .cdr-main { flex: 1; min-width: 0; }
.course-del-row .cdr-title { font-weight: 600; }
.course-del-row .cdr-meta { font-size: calc(12px * var(--fs)); color: var(--muted); margin-top: 2px; }

/*
 * ⚠️ 这里**不能**写 `overflow: hidden`。
 *    sticky 在"被 overflow 裁剪的祖先"里会失效（官方规范：sticky 的约束矩形
 *    被最近的可滚动祖先裁剪）。原来为了圆角写了 overflow: hidden，
 *    结果表头和节次列怎么都贴不住（实测滚动后表头跑到 -629）。
 *    圆角改由四个角的格子各自负责（见下面的 border-radius 规则）。
 */
.course-table { display: grid; border: 1px solid var(--line); border-radius: var(--r-lg); background: var(--surface); }

/*
 * 窄屏横向滑动。
 *
 * 课程表是 **7 天 + 节次列**，一共 8 列（2026-10-14 前是 9 列：还多一个"周次"列）。实测每列宽度：
 *   可用 800px → 每列约 105px（平板上勉强）
 *   可用 600px → 每列约 72px（挤）
 *   可用 360px → 每列约 37px（**完全没法用**，一个汉字都塞不下）
 *
 * 所以给表格一个 `min-width`，外面套一个横向滚动容器：
 * 宽屏完全不受影响（表格撑满、不出现滚动条），窄屏手指左右滑。
 * 这比"把 7 列压到 30px"有用得多。
 *
 * ⚠️ 滚动条只在真正需要时出现 —— 靠 `min-width` 而不是 `width`。
 * ⚠️ 容器**只**设 overflow-x。设成 overflow:hidden / auto（两个方向）会让
 *    sticky 表头失效（见下面 .course-table 那条注释里的实测）。
 * ⚠️⚠️ 2026-10-14 实测把这条又往前推了一步：`overflow-y: visible` **会被规范强制算成 auto**
 *    （原话："if one is specified as visible and the other is scroll/auto/hidden,
 *    then visible is set to auto"）—— 实测 computed `overflow-y` 就是 `auto`。
 *    后果：这个容器**同时成了竖向滚动容器**，于是页面滚动时表头只能在这个盒子内部粘，
 *    而盒子自己又不竖着滚 ⇒ "把日期表头钉在顶栏下面"这件事**从来没有生效过**
 *    （实测页面滚 739px 后表头格子视口 top=177，顶栏 66；`.ct-head` 上的 sticky 更是空的，
 *    因为它带 `ct-row` ⇒ `display: contents` 没有盒子）。
 *    试过 `overflow-y: clip` 想绕开这条强制：实测 computed 变成 `hidden`，依旧是滚动容器，
 *    表头还是粘不住 ⇒ **CSS 层面做不到"页面级"的表头 sticky**（要做得把表头拆到
 *    这个滚动盒子**外面**、再同步 scrollLeft —— 属于改 DOM 的事，没做）。
 *    退而求其次（本轮做法）：表头钉在**这个盒子自己的顶边**上（`top: 0`），
 *    并在手机档给盒子一个 `max-height` ⇒ 盒子内部竖着滚、表头就一直看得见。
 *    高度是 auto 的盒子上 `visible` 不会被裁到任何东西，所以这条对宽屏没有任何副作用。
 */
.course-table-scroll {
  overflow-x: auto;
  overflow-y: visible;
  -webkit-overflow-scrolling: touch;
  /* 让滚动区域不要贴着圆角，视觉上还是整块 */
  border-radius: var(--r-lg);
}
/*
 * 手机：表格在自己的盒子里竖着滚（页面级的表头 sticky 做不到，见上），
 * 表头钉在盒子顶边 ⇒ 一屏只看得到三四节课时，也知道自己在看星期几。
 * ⚠️ 用 `dvh` 并留出顶栏 + 工具条 + 底部标签栏的位置；`--topbar-h` 是 JS 量的真实值。
 */
@media (max-width: 799px) {
  .course-table-scroll { max-height: calc(100dvh - var(--topbar-h, 58px) - 150px); }
}
.course-table { min-width: 720px; }
/* 670px 以下（手机横屏/竖屏）每列再宽一点，滑起来能看清课名 */
@media (max-width: 669px) {
  .course-table { min-width: 640px; }
}
/*
 * ⚠️ 用 `display: contents` 让"行"不产生自己的网格 ——
 *    所有格子和课程块都是**同一个网格**的子元素。
 *    这样课程块才能用 grid-row: span N 跨多行（第 1-2 节连成一块），
 *    而不是被迫只画在一行里（用户报"1-2 节 8:00~9:35 为什么显示一节课"）。
 */
.ct-row { display: contents; }
.ct-slot, .ct-cell { min-height: 62px; }

/*
 * 课程块 —— 整块上色的色块模式：
 *   · 整块上色（不是白底加一条色线），面积大、一眼看到
 *   · 课程名粗体；教师/地点/时间各占一行，前面带小图标
 *   · 时间范围放最后一行
 * 颜色由 JS 通过 --c 给（一门课一个色相），这样相邻课程不容易混淆。
 */
.ct-block {
  margin: 3px; z-index: 1; overflow: hidden;
  border-radius: 8px; padding: 7px 8px; font-size: calc(11.5px * var(--fs)); line-height: 1.4;
  /* 实心浅色底：比 color-mix 15% 更明显 */
  background: var(--c);
  color: #fff;
  box-shadow: 0 1px 2px rgb(0 0 0 / 0.08);
  cursor: pointer;
  display: flex; flex-direction: column; gap: 1px;
  transition: filter .12s;
}
.ct-block:hover { filter: brightness(1.06); }
.ct-block b {
  display: block; font-size: calc(12.5px * var(--fs)); font-weight: 650; line-height: 1.3;
  /* 名字最多两行，超出省略 —— 长课名（"习近平新时代中国特色社会主义思想概论"）不能撑破格子 */
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
/* 教师 / 地点 / 时间：统一弱化一档，用半透明白保持可读 */
.ct-block span {
  display: block; font-size: calc(11px * var(--fs));
  color: rgb(255 255 255 / 0.92);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* 时间范围：紧跟课程名下方（这是最该一眼看到的信息）。
 * 不用 margin-top:auto 把它推到底 —— 跨 2~3 行的块会中间空一大截，很难看。 */
.ct-block .ct-time {
  font-size: calc(11px * var(--fs)); font-weight: 600;
  color: rgb(255 255 255 / 0.95);
  font-variant-numeric: tabular-nums;
}
/* 网格模板放在 .course-table 上（行不再自成网格，见 .ct-row 的 display:contents）
 * ⚠️ 2026-10-14：原来是 `58px 64px repeat(7, …)` —— 那个 58px 是左侧「周次」列，
 *    按作者手写清单图二下第 8 条删掉了（见 web/ui/views/course.js 表头那段注释）。
 *    ⇒ 现在 8 列：节次 + 周一…周日。 */
.course-table { grid-template-columns: 64px repeat(7, minmax(0, 1fr)); }
.ct-head > div:nth-child(1) { background: var(--surface-2); }
.ct-head { background: var(--surface-2); border-bottom: 1px solid var(--line); }
.ct-head > div { padding: 8px 4px; text-align: center; font-size: calc(12px * var(--fs)); color: var(--muted); border-right: 1px solid var(--line); }
.ct-head > div:last-child { border-right: 0; }
.ct-slot {
  border-right: 1px solid var(--line); border-bottom: 1px solid var(--line);
  padding: 6px 4px; text-align: center; font-size: calc(11px * var(--fs)); color: var(--muted);
}
.ct-slot b { display: block; font-size: calc(13px * var(--fs)); color: var(--text); }

/*
 * ⚠️ 表格很高（一学期 11 节 × 6 天，实测 2400px+），滚下去之后
 *    **看不到这是星期几、第几节** —— 这正是用户说的"至少我不要滚动"。
 *
 * 解决：把"日期表头"和"节次列"钉住。
 *   · 表头 sticky top —— 页面滚动时它贴在**顶栏下面**（`--topbar-h`，app.js 量的真实高度）
 *   · 左侧那一列（节次）sticky left —— 横向滚动（手机）时它贴在左边
 *     （2026-10-14 前是"节次 + 周次"两列，周次列已按作者清单删掉）
 * 需要不透明背景，否则下面的内容会透出来。
 *
 * ⚠️⚠️ 2026-10-14 实测踩到的坑：`position: sticky` **不能写在 `.ct-head` 上**。
 *    它同时带着 `ct-row` 类 ⇒ `display: contents`（见下面 `.ct-row` 那条），
 *    **自己根本没有盒子** —— 写在上面的 `sticky` / `top` / 背景 / 边框**全是空的**：
 *    实测 `getBoundingClientRect()` 恒为 `{0,0,0,0}`、把它 `style.top` 改成 120px 也纹丝不动，
 *    而表头格子（真盒子）在页面滚 221px 后从 `t=851` 老老实实滚到 `t=630`
 *    ⇒ "钉住表头"这件事**从来没有生效过**，只是没人量过。
 *    所以粘的是**表头格子**（它们是网格项、有盒子），见下面那条。
 */
.ct-head > div {
  /* ⚠️ `top: 0` 不是 `var(--topbar-h)`：表头粘的是**自己那个滚动盒子**的顶边
     （`.course-table-scroll` 横竖都是滚动容器），不是视口 —— 页面级 sticky 做不到，理由见上。 */
  position: sticky; top: 0; z-index: 3;
  background: var(--surface-2);
}
/* ⚠️ 2026-10-14：原来这里是 `.ct-weeks, .ct-slot`（左侧两列）。周次列按作者清单删掉后
 *    只剩"节次"一列要钉 —— 选择器里也不再留 `.ct-weeks`（那是死规则，
 *    `tools/course-table-sticky.test.mjs` 第 ⑤ 项有一条反向断言盯着它不许回来）。 */
.ct-slot { position: sticky; z-index: 2; background: var(--surface-2); left: 0; }
/* 左上角那一格（"节次"）要同时"横竖都粘"，层级最高（否则横滑时会盖住左边的节次列） */
.ct-head > div:nth-child(1) { z-index: 4; left: 0; }

/* 圆角：原来靠 .course-table 的 overflow:hidden 实现，现在由四个角的格子负责 */
.ct-head > div:first-child { border-top-left-radius: var(--r-lg); }
.ct-head > div:last-child { border-top-right-radius: var(--r-lg); }
.ct-row:last-child > .ct-slot { border-bottom-left-radius: var(--r-lg); }
.ct-row:last-child > *:last-child { border-bottom-right-radius: var(--r-lg); }

.ct-cell {
  border-right: 1px solid var(--line); border-bottom: 1px solid var(--line);
  min-height: 62px; padding: 5px; position: relative;
}
.ct-cell:last-child { border-right: 0; }
.ct-course {
  border-radius: 6px; padding: 5px 6px; font-size: calc(11.5px * var(--fs)); line-height: 1.35; height: 100%;
  background: color-mix(in srgb, var(--c) 15%, var(--surface));
  border-left: 3px solid var(--c); cursor: pointer;
}
.ct-course b { display: block; font-size: calc(12px * var(--fs)); }
.ct-course span { color: var(--muted); display: block; }

/* ============ 导入 / 设置 ============ */
.panel { padding: 18px 20px; display: flex; flex-direction: column; gap: 14px; }
.panel h2 { font-size: calc(15px * var(--fs)); font-weight: 650; }
.panel h3 { font-size: calc(13px * var(--fs)); font-weight: 600; color: var(--muted); }
.field-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; }
.switch-row {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 11px 0; border-bottom: 1px solid var(--line);
}
.switch-row:last-child { border-bottom: 0; }

/*
 * 气泡面板里的"显示课程"开关：`.switch-row` 是给设置页写的
 * （11px 竖内边距 + 底边框），放进说明面板太松，这里收紧。
 * 注意全局 `label { display:block }` 会带来额外的下标偏移，
 * 所以用 align-items: center 拉平。
 */
.bubble-course-toggle {
  padding: 6px 0; border-bottom: 0;
  font-size: calc(12.5px * var(--fs)); color: var(--text); cursor: pointer;
}
.bubble-course-toggle input { margin: 0; cursor: pointer; }
.switch-row .sr-text b { display: block; font-size: calc(13.5px * var(--fs)); font-weight: 600; }
.switch-row .sr-text small { color: var(--muted); font-size: calc(12px * var(--fs)); }

.switch { position: relative; width: 46px; height: 26px; flex: none; }
.switch input { position: absolute; opacity: 0; width: 100%; height: 100%; margin: 0; cursor: pointer; }
.switch .track {
  position: absolute; inset: 0; border-radius: 999px; background: var(--line);
  transition: background .18s; pointer-events: none;
}
.switch .track::after {
  content: ''; position: absolute; top: 3px; left: 3px; width: 20px; height: 20px;
  border-radius: 50%; background: #fff; box-shadow: 0 1px 3px rgba(0, 0, 0, .25);
  transition: transform .18s;
}
.switch input:checked + .track { background: var(--brand); }
.switch input:checked + .track::after { transform: translateX(20px); }

.import-report { border: 1px solid var(--line); border-radius: var(--r-md); padding: 12px 14px; background: var(--surface-2); font-size: calc(13px * var(--fs)); }
.import-report ul { margin-top: 6px; display: flex; flex-direction: column; gap: 4px; }
.import-report li { color: var(--warn); font-size: calc(12px * var(--fs)); }
textarea.code { font-family: ui-monospace, Consolas, "Cascadia Mono", monospace; font-size: calc(12.5px * var(--fs)); min-height: 220px; }

/* ============ 气泡浮动视图（主界面）============
   设计目标：气泡区占满整屏，操作台收进"第二级"（点按钮才弹出）。
   这样手机上能用的高度从约 370px 提升到约 700px，气泡才铺得开、
   不会被挤成一团、也不会被边缘裁掉。 */

.bubble-wrap { display: flex; flex-direction: column; flex: 1; min-height: 0; }

/**
 * ⚠️⚠️ 气泡区**禁止选中文本、禁止 iOS 长按菜单** —— 这不是讲究，是必须的。
 *
 * 为什么（用户实测报的）：
 *   "长按 2.5 秒戳破泡泡"是本应用的核心手势之一，而
 *   **iOS 上长按默认是"选中文本 / 弹拷贝菜单"** —— 两者是同一个手势，必然打架。
 *   被抢走后的表现：长按带着偏移 → 整个气泡区被选中 → 焦点跑掉 →
 *   **之后再怎么点都点不动泡泡**（得先点别处才能恢复）。
 *
 * 注意：`.bubble-canvas` 上已有的 `touch-action: none` **挡不住这个** ——
 *   它管的是滚动/缩放/双击放大，管不着文字选择。必须用 user-select。
 *
 * `-webkit-touch-callout: none` 是 iOS 专有的那条，它才是关掉"拷贝/查找"菜单的开关；
 * 只写 user-select 在 iOS 上仍可能弹菜单。
 *
 * 输入控件要**恢复**可选（气泡面板里有复选框），否则触屏上勾选框都不好点。
 */
.bubble-wrap,
.bubble-stage,
.bubble-canvas {
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}
.bubble-wrap input,
.bubble-wrap textarea {
  -webkit-user-select: text;
  user-select: text;
}

/**
 * ⚠️⚠️ `touch-action` 必须**逐层写**（不能只写 `.bubble-canvas`）。
 *
 * 为什么（用户报的"长按 2.5 秒无响应"这条链上的一个真隐患）：
 *   `touch-action` **不继承**，浏览器判定一次触摸时看的是
 *   "目标元素到它最近的可滚动祖先"这条链上的**交集**：
 *     · 只要链上**任何一层**允许 pan/zoom（默认就是 auto），
 *       iOS 就可能把这次触摸判成"想滚动/缩放"，
 *     · 于是长按到一半收到 `pointercancel` —— 指针序列被掐断，
 *       2.5 秒的计时器**永远等不到 pointerup 也不会走完**，
 *     · 而且 `pointermove` 也可能根本收不到（判成滚动手势后事件被系统吃掉）。
 *   画布上原来只写了一句 `touch-action: none` —— 它挡得住"画布自己这一层"，
 *   挡不住祖先链上任何一层的默认 auto。
 *
 * 所以这里把**整条祖先链**都钉成 none：
 *   .app → .main → .content(view-host) → .bubble-wrap → .bubble-stage → .bubble-canvas
 * 副作用是这一整块区域不再接受滚动/双指缩放 —— 这正是我们要的：
 * 气泡区是**全屏可拖动的工作台**，它上面本来就没有可滚动的内容
 * （面板 `.bubble-panel` 自己会滚动，见下面的 `touch-action: auto` 恢复）。
 *
 * `user-select` / `-webkit-touch-callout` 同理逐层写：iOS 上"选中文本/弹拷贝菜单"
 * 和"长按"是**同一个手势**，只要有一层漏了就可能把长按抢走（抢走之后焦点跑掉，
 * 表现是"之后再怎么点都点不动泡泡"）。
 */
.app.view-bubble,
.app.view-bubble .main,
.app.view-bubble .content,
.bubble-wrap,
.bubble-stage,
.bubble-canvas {
  touch-action: none;
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}
/* 面板里是有可滚动/可选中内容的（设置项、说明文字）—— 在这一层恢复，
   否则用户没法滚动说明、也没法选中里面的文字。画布层不受影响。 */
.bubble-panel {
  touch-action: auto;
}
.bubble-panel,
.bubble-panel input,
.bubble-panel textarea,
.bubble-panel select {
  -webkit-user-select: text;
  user-select: text;
}

/**
 * 画布容器：**铺满右侧**（用户要求）。
 *
 * 之前是 `height: calc(100dvh - 210px)` 这种"估着减"的写法，右侧永远留一条边。
 * 现在改成靠 flex 链条撑满：content(flex:1) → bubble-wrap(flex:1) → stage(flex:1)，
 * 配合 layout.css 里 `.app.view-bubble .content` 去掉内边距，就能真正贴边。
 * 还留一个 min-height 兜底，避免窗口极矮时被压成一条线。
 */
.bubble-stage {
  position: relative;
  flex: 1;
  min-height: 240px;
  border-radius: var(--r-lg);
  border: 1px solid var(--line);
  overflow: hidden;
  background:
    radial-gradient(120% 90% at 50% 0%, color-mix(in srgb, var(--brand) 7%, transparent), transparent 70%),
    var(--surface);
}
.bubble-canvas { display: block; position: absolute; inset: 0; cursor: grab; touch-action: none; }
.bubble-canvas.grabbing { cursor: grabbing; }

/**
 * 单击背景的即时反馈涟漪（见 bubble.js 里 tapRipple 的说明）。
 *
 * 关键：`pointer-events: none` —— 它盖在画布上，绝不能把后续的手势吃掉
 * （否则"为了让用户看到反应"的装饰会把真正的点击挡住，变成更糟的 bug）。
 * 默认 opacity: 0，只有加上 .on 才播一次动画，播完自己回到 0。
 */
.bubble-tap-ripple {
  position: absolute;
  width: 34px;
  height: 34px;
  margin: -17px 0 0 -17px;
  border-radius: 50%;
  border: 2px solid color-mix(in srgb, var(--brand) 70%, transparent);
  background: color-mix(in srgb, var(--brand) 16%, transparent);
  opacity: 0;
  pointer-events: none;
  z-index: 1;
}
.bubble-tap-ripple.on { animation: bubble-tap-ripple 420ms ease-out; }
@keyframes bubble-tap-ripple {
  0% { opacity: .85; transform: scale(.45); }
  100% { opacity: 0; transform: scale(1.5); }
}

/**
 * 长按被**系统打断**时的一句话（平时 hidden）。
 *
 * ⚠️ 为什么必须有：被系统打断（第二根手指、系统长按菜单、切到后台）时，
 *   用户看到的和"完全没按到"**一模一样** —— 都是什么都不发生。
 *   平板上没有控制台，界面自己说话是唯一的诊断手段。
 *   `pointer-events: none` 是硬要求：它是**提示**，绝不能把后续手势吃掉
 *   （否则"为了解释为什么没反应"反而制造了新的没反应）。
 */
/* ⚠️ 这里的次级文字色原本写的是 `var(--text-dim, #64748b)` 和 `var(--muted)` ——
   这两个 token **全仓从没定义过**（10-13 逐个核对 token 时发现）：带兜底的还看得出颜色，
   没兜底的 `color: var(--muted)` 整条声明直接失效（文字静默退回继承色）。
   统一改用 `--muted`：已定义、深色模式下也有自己的值。 */
.bubble-hold-hint {
  position: absolute;
  left: 50%;
  bottom: 14px;
  transform: translateX(-50%);
  max-width: min(86%, 420px);
  padding: 6px 12px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--surface) 88%, #000 12%);
  border: 1px solid var(--line);
  color: var(--muted);
  font-size: calc(12.5px * var(--fs));
  line-height: 1.35;
  text-align: center;
  pointer-events: none;
  z-index: 3;
}

/**
 * 手势诊断角标（默认关闭；`?diag=1` 或「?」面板里的开关打开）。
 * 见 `web/ui/bubble-diag.js` 文件头：平板上没有控制台，长按没反应时
 * "原始事件到底有没有到"只能让屏幕自己说出来。
 *
 * ⚠️⚠️ `pointer-events: none` 是**硬要求**：它是诊断器，绝不能把长按手势吃掉 ——
 *    否则"为了看清为什么没反应"反而制造了一个新的没反应，那是最糟的结果。
 *    这条和 `.bubble-tap-ripple` / `.bubble-hold-hint` 是同一条规矩。
 * ⚠️ 位置放**左下角**：那里既没有 HUD（左上）、也没有"被系统打断"的提示条（底部居中）、
 *    也没有操作面板（底部整条，但它是 hidden 的）。角标不许盖住任何一个操作入口。
 * ⚠️ `z-index: 4`：低于操作面板(5)，高于提示条(3)和信息条(2) —— 面板一开就该盖住角标。
 */
.bubble-diag {
  position: absolute;
  left: 8px;
  bottom: 8px;
  z-index: 4;
  max-width: calc(100% - 16px);
  padding: 4px 8px;
  border-radius: 8px;
  background: color-mix(in srgb, var(--surface) 92%, #000 8%);
  border: 1px solid var(--line);
  color: var(--muted);
  font: calc(11px * var(--fs))/1.45 ui-monospace, Consolas, "Cascadia Mono", monospace;
  /* 那一行会很长（touch 通道有数时多一格）—— 让它自己换行，绝不撑破画布 */
  white-space: pre-wrap;
  word-break: break-all;
  pointer-events: none;
}
/* ⚠️ 这一行不能省：本项目已经在 `.bubble-hud-chip` / `.bubble-inside-hint` 上
   踩过"display 盖过 [hidden] → 留下一个空盒子"的坑（见上面那两条注释）。 */
.bubble-diag[hidden] { display: none; }

/* 进入气泡后：画布背景变成"容器颜色"的柔光，表示我们在它里面（套娃）。
   --bubble-inside-color 由 bubble.js 按容器档位写入。 */
.bubble-stage.bubble-inside {
  border-color: color-mix(in srgb, var(--bubble-inside-color, var(--brand)) 45%, var(--line));
  background:
    radial-gradient(90% 70% at 50% 12%,
      color-mix(in srgb, var(--bubble-inside-color, var(--brand)) 26%, transparent),
      transparent 72%),
    radial-gradient(120% 100% at 50% 110%,
      color-mix(in srgb, var(--bubble-inside-color, var(--brand)) 14%, transparent),
      transparent 70%),
    color-mix(in srgb, var(--bubble-inside-color, var(--brand)) 6%, var(--surface));
  transition: background 420ms ease, border-color 420ms ease;
}
/* 进入动画：先"涨大 + 虚化"一下，像是穿过泡壁进去的 */
@keyframes bubble-enter-pop {
  0% { transform: scale(1.06); filter: blur(6px); opacity: .55; }
  100% { transform: scale(1); filter: blur(0); opacity: 1; }
}
/* ===== 月历：左侧日历 + 右侧"选中那天的详细列表" =====
   窄屏（手机）时右侧面板落到下面，不然日历会被挤得没法看。 */
.month-layout { display: grid; grid-template-columns: minmax(0, 1fr) 300px; gap: 12px; align-items: start; }
@media (max-width: 1080px) { .month-layout { grid-template-columns: 1fr; } }
.month-side { max-height: 72vh; display: flex; flex-direction: column; }
@media (max-width: 1080px) { .month-side { max-height: none; } }

/* 平板档（800–1080px，与侧栏那条 `@media` 同一个区间）：把"这天有什么"提到月历**上面**并钉住。
   为什么：800–1080 这一档是平板（11" 竖屏 800 / 平板竖屏 820 / 10.1" 横屏 960 / 11" 横屏 1024），
   单列排下来月历自己就有 660–810px 高（格高 108），详情卡被压到页面最下面 ——
   实测 820px 上详情卡顶边 ~1174、1000px 上 1170，**点一天要往下滚 1150px 才看到当天日程**，
   而"点一天看那天的安排"正是月历最主要的动作。
   做法：这一档把详情卡排到月历**前面**（`order: -1`）并钉在顶栏下面（`top: var(--topbar-h)`，
   那个值是 JS 量出来的）—— 往下翻上半月/下半月时它一直看得见，点哪天都不用回头找。
   代价：月历第一行往下让出详情卡的高度，所以这里给详情卡 `max-height: 38vh`；
   再多它自己内部滚（`.month-detail-list` 本来就是 `overflow-y: auto`）。

   ⚠️ 为什么这一档要把 `.month-layout` 从 grid 换成 flex（真踩过）：
     在 grid 里每个格子的**包含块是它自己那一格**（grid area）⇒ 单列堆叠时详情卡在第 1 行、
     月历在第 2 行，详情卡 sticky 只能在第 1 行的高度里挪，**根本钉不住**；
     换成 `flex-direction: column` 之后，子项的包含块是**整个 flex 容器**
     ⇒ 详情卡能在"月历那么高"的范围里一路钉下去。
     桌面档（>1080px）是两列、两个卡片同高，不需要它钉住，所以那边一个字没动。

   手机档（≤799px）也没动：一屏里格子只有 96px 高，详情紧跟在月历后面才顺手
   （看下面的 `.month-cell` 手机档那几条）。 */
@media (min-width: 800px) and (max-width: 1080px) {
  .month-layout { display: flex; flex-direction: column; gap: 12px; }
  .month-layout .month-side {
    order: -1;
    position: sticky;
    top: var(--topbar-h, 58px);
    z-index: 2;                 /* 压在月历格子上方：滚动时不会被格子盖住 */
    max-height: 38vh;           /* 详情再多也不许占掉整屏 */
  }
}

.month-detail { display: flex; flex-direction: column; gap: 8px; min-height: 0; }
.month-detail-head {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  padding-bottom: 8px; border-bottom: 1px solid var(--line);
}
.month-detail-head b { font-size: calc(15px * var(--fs)); font-weight: 650; }
.month-detail-head .tiny { display: block; color: var(--muted); font-size: calc(11.5px * var(--fs)); }
.month-detail-list { display: flex; flex-direction: column; gap: 6px; overflow-y: auto; min-height: 0; }
.month-detail-item {
  display: flex; gap: 9px; align-items: flex-start;
  padding: 8px 9px; border-radius: var(--r-sm); cursor: pointer;
  border: 1px solid var(--line);
  border-left: 3px solid var(--c, var(--brand));
  background: var(--surface);
}
.month-detail-item:hover { background: var(--surface-2); }
.mdi-time { display: flex; flex-direction: column; align-items: flex-end; min-width: 42px; flex: none; }
.mdi-time b { font-size: calc(13px * var(--fs)); font-weight: 650; }
.mdi-time span { font-size: calc(10.5px * var(--fs)); color: var(--muted); }
.mdi-body { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.mdi-title { font-size: calc(13.5px * var(--fs)); font-weight: 600; word-break: break-word; }
.mdi-notes {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 200px;
}
.mdi-done { font-size: calc(10.5px * var(--fs)); color: var(--muted); flex: none; }
.month-detail-empty { color: var(--muted); padding: 12px 2px; line-height: 1.5; }

/* 日历格子里"当前查看"的那天 */
.month-cell.selected { box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--brand) 55%, transparent); }

.bubble-stage.bubble-inside .bubble-canvas,
.bubble-stage.bubble-enter .bubble-canvas {
  animation: bubble-enter-pop 420ms cubic-bezier(.22,.9,.3,1) both;
}

/* 母气泡过期时，里面也是紫色调（子泡泡会跟着一起变紫） */
.bubble-stage.bubble-inside.bubble-inside-overdue {
  /* 兜底不能漏：这个变量由 `web/ui/views/bubble.js:518` 写入，缺了兜底的话
     `color-mix(… var(--bubble-inside-color) …)` 整条声明失效（边框色静默退回上一条规则）。 */
  border-color: color-mix(in srgb, var(--bubble-inside-color, var(--brand)) 65%, var(--line));
  border-style: dashed;
}

/* 进了容器但里面还是空的：明说"点背景加东西"，
   否则一片空画布会让人以为进错了或卡住了 */
.bubble-inside-hint {
  position: absolute; inset: 0; z-index: 2;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 6px; text-align: center; pointer-events: none; padding: 16px;
  color: color-mix(in srgb, var(--bubble-inside-color, var(--text)) 55%, var(--muted));
}
.bubble-inside-hint[hidden] { display: none; }
.bubble-inside-hint strong { font-size: calc(17px * var(--fs)); font-weight: 650; }
.bubble-inside-hint span { font-size: calc(13px * var(--fs)); opacity: .85; }
.bubble-inside-hint-key {
  margin-top: 6px; font-size: calc(12px * var(--fs)) !important;
  padding: 5px 11px; border-radius: 999px;
  border: 1px dashed color-mix(in srgb, var(--bubble-inside-color, var(--line)) 50%, var(--line));
  background: color-mix(in srgb, var(--surface) 70%, transparent);
}

/* 浮在画布上的极简信息条：只显示"点了哪个气泡"，不占版面 */
.bubble-hud {
  position: absolute; left: 10px; top: 10px; right: 10px;
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  pointer-events: none; z-index: 3;
}
.bubble-hud > * { pointer-events: auto; }
.bubble-hud-chip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 10px; border-radius: 999px; font-size: calc(12.5px * var(--fs));
  /* 不透明度偏高：气泡会从下面经过，太透会看不清 HUD */
  background: color-mix(in srgb, var(--surface) 94%, transparent);
  border: 1px solid var(--line); backdrop-filter: blur(10px);
  box-shadow: 0 1px 4px rgba(16, 24, 40, .12);
}
.bubble-hud-chip b { font-weight: 650; }
.bubble-hud-chip.muted { color: var(--muted); }
/**
 * ⚠️ 这一行不能省：`.bubble-hud-chip` 上是 `display: inline-flex`，
 * 它会**盖过** `[hidden]` 自带的 `display: none` —— 于是"藏起来"的那格
 * 仍然画成一个**空的白色小胶囊**（用户截图里左上角那个就是它）。
 * 这个坑本项目在 `.bubble-inside-hint` 上已经踩过一次。
 */
.bubble-hud-chip[hidden] { display: none; }

/* 第二级：操作面板（点按钮才出现，浮在画布下方或弹出） */
.bubble-panel {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 5;
  display: flex; flex-direction: column; gap: 10px;
  padding: 14px 14px calc(14px + env(safe-area-inset-bottom));
  background: color-mix(in srgb, var(--surface) 96%, transparent);
  backdrop-filter: blur(14px);
  border-top: 1px solid var(--line);
  border-radius: var(--r-lg) var(--r-lg) 0 0;
  box-shadow: 0 -8px 30px -12px rgba(0, 0, 0, .25);
  max-height: 62%;
  overflow-y: auto;
  animation: bubblePanelUp .18s ease-out;
}
.bubble-panel[hidden] { display: none; }
@keyframes bubblePanelUp { from { transform: translateY(14px); opacity: .5; } }

/* 说明面板（点 HUD 上的 ? 才出现）——内容偏"读"，所以版式比原操作面板宽松些 */
.bubble-help { gap: 10px; }
.bubble-panel-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.bubble-panel-row .spacer { flex: 1; }
.bubble-tool-label { font-size: calc(12px * var(--fs)); color: var(--muted); }
/* 「时间范围」旁边那个可自填天数的输入框（用户要的"除了 3/7/14/30 还能自调"）。
   宽度只留三位数，和分段按钮排在一行不挤。 */
.bubble-horizon-input {
  width: 62px; padding: 4px 6px; font-size: calc(12.5px * var(--fs));
  border: 1px solid var(--line); border-radius: var(--r-sm);
  background: var(--surface); color: var(--text);
}
/* 节日背景图那一行：下拉 + 两个按钮 + 状态文字，窄屏要能换行（平板竖屏也在意） */
.bubble-art-row { flex-wrap: wrap; gap: 6px 8px; }
.bubble-art-select {
  padding: 4px 6px; font-size: calc(12.5px * var(--fs)); max-width: 46vw;
  border: 1px solid var(--line); border-radius: var(--r-sm);
  background: var(--surface); color: var(--text);
}
.bubble-art-status { font-size: calc(12px * var(--fs)); color: var(--muted); }
.bubble-art-preview { border-radius: 50%; flex: none; }
.bubble-help-head { display: flex; flex-direction: column; gap: 2px; }
.bubble-help-head strong { font-size: calc(14px * var(--fs)); font-weight: 650; }
.bubble-help-title {
  font-size: calc(12px * var(--fs)); font-weight: 600; color: var(--muted);
  margin-top: 4px; padding-top: 8px; border-top: 1px solid var(--line);
}
.bubble-help-grid { display: grid; grid-template-columns: auto 1fr; gap: 6px 12px; align-items: baseline; }
@media (max-width: 560px) { .bubble-help-grid { grid-template-columns: 1fr; gap: 2px 0; } }
.bubble-help-line { display: contents; }
@media (max-width: 560px) { .bubble-help-line { display: block; margin-bottom: 6px; } }
.bubble-help-key {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: calc(12.5px * var(--fs)); font-weight: 600; white-space: nowrap;
}
.bubble-help-key i {
  width: 12px; height: 12px; border-radius: 4px; display: inline-block; flex: none;
  border: 1px solid color-mix(in srgb, var(--line) 70%, transparent);
}
.bubble-help-desc { font-size: calc(12.5px * var(--fs)); color: var(--muted); line-height: 1.45; }

/* 自定义课程对话框的字段（课程名/代码/地点/老师…） */
.custom-field { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
.custom-field > span:first-child { font-size: calc(12.5px * var(--fs)); font-weight: 600; color: var(--muted); }
.custom-field .tiny { font-size: calc(11.5px * var(--fs)); color: var(--muted); }
/* 输入框占满自己的那一格；但"从…到…"这一行要横向排，别各占一整行 */
.custom-field > input, .custom-field > select { width: 100%; }
.custom-field .deadline-units { flex-wrap: wrap; }
.custom-field .deadline-units > select { flex: 1 1 180px; min-width: 0; width: auto; }
/* 星期几的单选：复用 .chip 样式，选中的加重 */
.chip-row { display: flex; gap: 6px; flex-wrap: wrap; }
.chip-row .chip[aria-pressed="true"] {
  border-width: 2px; font-weight: 650;
  border-color: color-mix(in srgb, var(--brand) 60%, var(--line));
  background: var(--brand-soft);
}
pre.code-block {
  margin: 0; padding: 8px 10px; border-radius: var(--r-sm);
  background: var(--surface-2, color-mix(in srgb, var(--line) 25%, transparent));
  font-family: ui-monospace, Consolas, monospace; font-size: calc(12.5px * var(--fs));
  overflow-x: auto;
}

/* 颜色（事情多大）选择：一个圆点 + 一个词 */
.bubble-level-row { display: flex; gap: 6px; flex-wrap: wrap; }
.level-chip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 11px; border-radius: 999px; font-size: calc(12.5px * var(--fs));
}
.level-chip i { width: 12px; height: 12px; border-radius: 50%; display: inline-block; }
.level-chip[aria-pressed="true"] { border-color: currentColor; font-weight: 650; }
.level-chip[disabled] { opacity: .38; cursor: not-allowed; }

/* ---- 截止时间输入 ---- */
.deadline-field { display: flex; flex-direction: column; gap: 8px; }
.deadline-units { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 6px; }
.deadline-unit { display: flex; align-items: center; gap: 4px; }
.deadline-unit .tiny { color: var(--muted); }
.deadline-preview {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding: 6px 8px; border-radius: var(--r-sm, 10px);
  background: color-mix(in srgb, var(--brand) 7%, transparent);
}
.deadline-preview .chip { font-size: calc(12.5px * var(--fs)); }

/* ---- 编辑器表单：常用区 + 「高级设置」折叠块（2026-10-14）----
   用户口径：「一定要功能不变的情况下使界面尽量简洁，把不常调的参数藏到"高级设置"里，
   但是想调也可以直接点开」。这里只负责"藏起来"的样式，字段本身一个都没动。 */
.ed-form { display: flex; flex-direction: column; gap: 14px; }
.ed-auto {
  display: flex; align-items: center; gap: 8px; margin-bottom: 8px;
  font-size: calc(13px * var(--fs)); color: var(--text);
}
.ed-reminder-note { margin: 6px 0 0; color: var(--muted); }

.ed-adv {
  border: 1px solid var(--line); border-radius: var(--r-md);
  background: var(--surface-2, var(--surface));
}
.ed-adv-head {
  display: flex; align-items: center; gap: 8px;
  padding: 10px 12px; cursor: pointer; list-style: none;
}
.ed-adv-head::-webkit-details-marker { display: none; }
.ed-adv-title { font-size: calc(13.5px * var(--fs)); font-weight: 650; }
.ed-adv-hint { flex: 1; min-width: 0; font-size: calc(12px * var(--fs)); color: var(--muted); }
.ed-adv-caret { color: var(--muted); font-size: calc(12px * var(--fs)); transition: transform .15s ease; }
.ed-adv[open] .ed-adv-caret { transform: rotate(180deg); }
.ed-adv[open] .ed-adv-head { border-bottom: 1px solid var(--line); }
.ed-adv-body { padding: 12px; display: flex; flex-direction: column; gap: 14px; }

/* 「截止日期 *」那一行和它下面那两枚「确切日期 / 模糊期限」并成一行 —— 手机上省一整行（约 34px）。
   做法是 `display: contents` 把 `.deadline-field` 这层壳"化掉"，让它的孩子直接落进两列网格：
   第一行 = label | 切换 chip，之后每一块（日期框 / 距离框 / 勾选框 / 还剩多少）都横跨两列。
   ⚠️ 别改成"把 chip 挪进 label 那一行"的 DOM 改法：那个 picker 的结构是测试的把手
      （`tools/future-bubble.test.mjs:307` 按 `data-role="ed-due"` 找输入框），动结构得跟着改一套用例。 */
.ed-due { display: grid; grid-template-columns: auto 1fr; gap: 8px 10px; align-items: center; }
.ed-due > .deadline-field { display: contents; }
.ed-due > .deadline-field > * { grid-column: 1 / -1; }
.ed-due > .deadline-field > .seg { grid-column: 2; }
.ed-due .seg { justify-self: start; }

.bubble-legend {
  display: flex; gap: 16px; flex-wrap: wrap;
  padding: 10px 12px; border: 1px solid var(--line); border-radius: var(--r-md);
  background: var(--surface-2, var(--surface));
}
.bubble-legend-block { display: flex; flex-direction: column; gap: 6px; }

/* 气泡调试信息（仅在 ?debug=1 时插入）*/
.bubble-debug {
  position: absolute; left: 4px; right: 4px; bottom: 4px;
  font: calc(11px * var(--fs))/1.35 ui-monospace, Consolas, monospace;
  color: color-mix(in srgb, var(--text) 75%, transparent);
  background: color-mix(in srgb, var(--surface) 84%, transparent);
  padding: 4px 6px; border-radius: 6px; pointer-events: none;
  white-space: normal; word-break: break-all;
}

/* 设置页里的二维码 */
.settings-qr {
  width: min(200px, 55vw); padding: 8px; background: #fff;
  border: 1px solid var(--line); border-radius: 12px;
}
.settings-qr svg { width: 100%; height: auto; display: block; }

/* 大小滑动条（气泡工具栏 / 事件编辑器共用） */
.bubble-size-row { gap: 10px; }
.bubble-pick-label {
  font-size: calc(12.5px * var(--fs)); font-weight: 600; max-width: 180px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.bubble-slider {
  flex: 1; min-width: 160px; max-width: 420px;
  height: 26px; padding: 0; min-height: 26px;
  background: transparent; border: 0; box-shadow: none;
  -webkit-appearance: none; appearance: none; cursor: pointer;
}
.bubble-slider:disabled { opacity: .45; cursor: not-allowed; }
.bubble-slider::-webkit-slider-runnable-track {
  height: 8px; border-radius: 999px;
  background: linear-gradient(90deg, #38bdf8, #22c55e 42%, #f5b301 72%, #ef4444);
}
.bubble-slider::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 22px; height: 22px; margin-top: -7px; border-radius: 50%;
  background: var(--surface); border: 3px solid var(--text);
  box-shadow: 0 2px 6px rgba(0, 0, 0, .3);
}
.bubble-slider::-moz-range-track {
  height: 8px; border-radius: 999px;
  background: linear-gradient(90deg, #38bdf8, #22c55e 42%, #f5b301 72%, #ef4444);
}
.bubble-slider::-moz-range-thumb {
  width: 20px; height: 20px; border-radius: 50%;
  background: var(--surface); border: 3px solid var(--text);
}
.bubble-size-value {
  min-width: 34px; text-align: center; font-size: calc(13px * var(--fs)); font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.bubble-presets { display: flex; gap: 6px; flex-wrap: wrap; }
.bubble-presets button { cursor: pointer; }
.bubble-legend-intensity { font-size: calc(9px * var(--fs)); letter-spacing: -2px; opacity: .75; }

/* 大小滑动条（事件编辑器内） */
.magnitude-field { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.magnitude-field .bubble-slider { max-width: none; }

/* 紧急度分档说明 */
.tier-list { display: flex; flex-direction: column; gap: 8px; }
.tier-row { display: flex; align-items: center; gap: 10px; font-size: calc(13px * var(--fs)); }
.tier-row i { width: 14px; height: 14px; border-radius: 50%; flex: none; }
.tier-row b { min-width: 42px; }
.tier-row small { color: var(--muted); }

.bubble-legend-title { font-size: calc(11.5px * var(--fs)); color: var(--muted); }
.bubble-legend-ramp { display: flex; gap: 10px; flex-wrap: wrap; }
.bubble-legend-item { display: flex; align-items: center; gap: 5px; font-size: calc(12px * var(--fs)); }
.bubble-legend-item i { width: 13px; height: 13px; border-radius: 4px; display: inline-block; }
.bubble-size-demo { display: flex; align-items: center; gap: 14px; }
.bubble-size-item { display: flex; align-items: center; gap: 5px; font-size: calc(12px * var(--fs)); }
.bubble-size-item i {
  display: inline-block; border-radius: 50%;
  background: color-mix(in srgb, var(--muted) 35%, transparent);
  border: 1px solid color-mix(in srgb, var(--muted) 45%, transparent);
}

/* 量级选择器（事件编辑器内） */
.importance-picker { display: flex; gap: 6px; flex-wrap: wrap; }
.importance-picker button {
  display: flex; align-items: center; gap: 6px;
  padding: 6px 10px; border-radius: 999px; cursor: pointer;
  border: 1px solid var(--line); background: var(--surface); font-size: calc(12.5px * var(--fs));
}
.importance-picker button i { width: 12px; height: 12px; border-radius: 4px; display: inline-block; }
.importance-picker button[aria-pressed="true"] { border-width: 2px; font-weight: 600; }

@media (max-width: 799px) {
  /* 手机上让气泡区尽量占满：只剩压扁后的顶栏和底部标签栏 */
  .bubble-stage { height: calc(100dvh - var(--tabbar-h) - 150px); min-height: 320px; }
  .bubble-legend { gap: 12px; padding: 8px 10px; }
  .bubble-panel { max-height: 72%; }
}

/* ============ 弹窗 ============ */
.modal-host { position: fixed; inset: 0; z-index: 60; }
.modal-host[hidden] { display: none; }
.modal-mask { position: absolute; inset: 0; background: rgba(15, 17, 21, .42); backdrop-filter: blur(2px); }
.modal {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: min(560px, calc(100vw - 32px)); max-height: min(84vh, 820px);
  display: flex; flex-direction: column;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-lg); box-shadow: 0 24px 60px -20px rgba(0, 0, 0, .4);
  overflow: hidden;
  animation: pop .16s ease-out;
}
@keyframes pop { from { opacity: 0; transform: translate(-50%, -48%) scale(.98); } }
.modal-head {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 14px 18px; border-bottom: 1px solid var(--line);
}
/* 抓手条（下滑关闭的落手处）：宽屏是居中卡片，不画出来。
   22px 是"手指按得住"的高度，里面那根 40×4 的小横杠才是它的视觉本体；
   `touch-action: none` 保证往下拖的时候不被浏览器的滚动手势抢走。
   （手势本身在 web/ui/modal.js 里，只在窄屏装。） */
.modal-grab {
  display: none; position: relative; flex: none;
  height: 22px; cursor: grab; touch-action: none;
}
.modal-grab::before {
  content: ''; position: absolute; left: 50%; top: 8px; transform: translateX(-50%);
  width: 40px; height: 4px; border-radius: 2px;
  background: var(--muted); opacity: .4;
}
.modal-grab:active { cursor: grabbing; }
.modal-head h2 { font-size: calc(15px * var(--fs)); font-weight: 650; }
.modal-body { padding: 16px 18px; overflow: auto; display: flex; flex-direction: column; gap: 14px; }

/* 编辑器顶部那条"新建 / 修改"横幅（用户 10-12 报的 bug ①）：
   他遇到的怪事是"改一颗泡泡，保存后又多出一颗" —— 因为这个弹窗当时
   除了标题和按钮的两个字之差，看不出自己到底在新建还是在改。
   现在：新建=绿色、修改=橙色，各配一句后果（"会多出一颗" / "不会新增"）。
   ⚠️ 只做视觉与文案 —— 用户明确要求先别动保存那条路径。 */
.ed-mode-banner {
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  padding: 9px 12px; border-radius: var(--r-md);
  border: 1px solid transparent; font-size: calc(13px * var(--fs));
}
.ed-mode-banner strong { font-size: calc(13px * var(--fs)); flex: none; }
.ed-mode-banner.is-new { background: #e8f6ec; border-color: #b7e0c2; color: #17603a; }
.ed-mode-banner.is-edit { background: #fff3e2; border-color: #f0d3a8; color: #7a4a10; }
.ed-mode-note { margin: -4px 0 0; }

/* 主题三态：配色只认 <html data-theme>（由 /ui/theme-boot.js 在首帧前定好），
   不再用 `@media (prefers-color-scheme: dark)` —— 那样用户就没法手动选深色。 */
:root[data-theme="dark"] .ed-mode-banner.is-new { background: #14301f; border-color: #2c5f3d; color: #a8e6bf; }
:root[data-theme="dark"] .ed-mode-banner.is-edit { background: #35260f; border-color: #6b5220; color: #f0cd93; }
.modal-foot {
  display: flex; align-items: center; gap: 8px;
  padding: 12px 18px; border-top: 1px solid var(--line); background: var(--surface-2);
}
.modal-foot .spacer { flex: 1; }

@media (max-width: 799px) {
  .modal {
    left: 0; right: 0; bottom: 0; top: auto; transform: none;
    width: 100%; max-height: 90vh; border-radius: var(--r-lg) var(--r-lg) 0 0;
    animation: sheet .2s ease-out;
  }
  @keyframes sheet { from { transform: translateY(12px); opacity: .6; } }
  .modal-body { padding-bottom: calc(16px + env(safe-area-inset-bottom)); }
  /* 抽屉形态才画抓手条；标题栏也一起交出滚动控制权（拖它等于拖抽屉） */
  .modal-grab { display: block; }
  .modal-head { touch-action: none; }
}

/* 手机上的「新建/修改日程」要**一眼看完**（2026-10-14 用户原话：界面太复杂、对用户不友好，
   要"功能不变、界面尽量简洁"）。真浏览器实测（`build/` 下那个拍图脚本，路径故意不写：
   目录名是渠道代号，写进来公开版导出会判硬失败）：
   收起状态的正文在 494×725 上是 709px、可见区 596px —— 差的那 113px 就是"还得往下拨一下"。
   这里只抠间距（正文 14→10、表单 14→11），加上 `.ed-due` 并掉的那一行，正好一屏看完、不用滚。
   ⚠️ 必须放在这条 base 规则**之后**：媒体查询不加权重，同权重下比的是先后。 */
@media (max-width: 560px) {
  .modal-body { gap: 9px; }
  .ed-form { gap: 8px; }
  .ed-form label { margin-bottom: 4px; }
  .ed-due > label { margin-bottom: 0; }
  .deadline-field { gap: 6px; }
  .ed-auto { margin-bottom: 4px; }
  .deadline-preview { padding: 4px 8px; }
  .ed-adv-head { padding: 9px 12px; }
}

/* ============ Toast ============ */
.toast-host {
  position: fixed; z-index: 80;
  right: 18px; bottom: 18px; display: flex; flex-direction: column; gap: 8px;
  max-width: min(380px, calc(100vw - 36px));
  pointer-events: none;
}
.toast {
  pointer-events: auto;
  display: flex; gap: 10px; align-items: flex-start;
  padding: 11px 14px; border-radius: var(--r-md);
  background: var(--surface); border: 1px solid var(--line);
  box-shadow: 0 10px 30px -12px rgba(0, 0, 0, .35);
  animation: slide .2s ease-out;
}
@keyframes slide { from { opacity: 0; transform: translateY(8px); } }
.toast.alert { border-left: 3px solid var(--brand); }
.toast.ok { border-left: 3px solid var(--ok); }
.toast.err { border-left: 3px solid var(--danger); }
.toast b { display: block; font-size: calc(13.5px * var(--fs)); }
.toast small { color: var(--muted); font-size: calc(12px * var(--fs)); }
.toast .t-close { border: 0; background: transparent; cursor: pointer; color: var(--muted); padding: 0 2px; }

@media (max-width: 799px) {
  .toast-host { left: 12px; right: 12px; bottom: calc(var(--tabbar-h) + 12px + env(safe-area-inset-bottom)); max-width: none; }
}

/* ===========================================================================
   回收气泡站（单独标签页）
   ---------------------------------------------------------------------------
   用户要求："气泡破裂后保存为气泡站中的历史，此区域泡泡不再浮动，
             被嵌入界面即可，且泡泡保留外观"、"破裂泡泡大小统一但要保存其破裂时剩余的时间"
   =========================================================================== */

.recycle-wrap { display: flex; flex-direction: column; gap: 14px; }
.recycle-head { display: flex; flex-direction: column; gap: 4px; }
.recycle-head h3 { font-size: calc(14px * var(--fs)); font-weight: 650; }
.recycle-head .tiny { color: var(--muted); }

/* ---- 布局 ①：列表式（按破裂时间排布，从左到右、从上到下）---- */
/* 用 auto-fill 网格：列数随宽度自适应，"从左到右、从上到下"正好是 DOM 顺序 */
.recycle-grid {
  display: grid; gap: 16px 14px;
  grid-template-columns: repeat(auto-fill, minmax(104px, 1fr));
  align-items: start; justify-items: center;
  border: 1px solid var(--line); border-radius: var(--r-md);
  background: var(--surface); padding: 16px 14px;
}

/* ---- 布局 ②：螺旋式 ---- */
.recycle-spiral { position: relative; overflow: hidden; border-radius: var(--r-md); }
/* 层是绝对定位 + 自身 transform 居中缩放；不要用 inset:0（那会把它撑满、丢掉固有尺寸）*/
.recycle-spiral-layer { position: absolute; }
.recycle-spiral-hint {
  position: absolute; left: 12px; bottom: 8px;
  font-size: calc(11.5px * var(--fs)); color: var(--muted); pointer-events: none;
}

/* ---- 方块本体（两种布局共用 → 保证"保留外观 + 大小统一"）----
 *
 * ⚠️ 形状是**圆角方块**，不是圆泡泡 —— 我第一版做成了 `border-radius: 50%`，
 *    但用户画的草图里方块是方的（方块里写编号）。已按图改回方块。
 *    名字仍用 recycle-block（"泡泡"这个叫法在界面文案里还在用）。
 */
.recycle-item { display: flex; flex-direction: column; align-items: center; gap: 3px; position: relative; }
.recycle-block {
  /* --c 由 JS 按等级给色；宽高由 JS 统一给（保证"大小统一"）*/
  border-radius: 10px;
  /* 角标（×N）相对方块定位，所以必须是 relative；
     overflow 不能是 hidden，否则角标会被裁掉 —— 名称的省略交给 -webkit-line-clamp。 */
  position: relative;
  background: linear-gradient(160deg, color-mix(in srgb, var(--c) 86%, #fff), var(--c) 62%);
  border: 2px solid color-mix(in srgb, var(--c) 60%, #000);
  box-shadow: 0 2px 6px rgb(0 0 0 / .14);
  display: grid; place-items: center; padding: 6px 7px;
  cursor: pointer; transition: transform .12s;
}
.recycle-block:hover { transform: scale(1.05); }
.recycle-block-name {
  font-size: calc(11px * var(--fs)); line-height: 1.25; text-align: center; color: #fff;
  font-weight: 650; text-shadow: 0 1px 2px rgb(0 0 0 / .35);
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
  overflow: hidden; word-break: break-word;
}
/* 事件合并的角标：这条事件一共被戳破几次 */
.recycle-block-count {
  position: absolute; right: -5px; top: -6px;
  background: var(--surface); color: var(--text);
  border: 1px solid var(--line); border-radius: 9px;
  font-size: calc(10px * var(--fs)); font-weight: 700; padding: 0 5px; line-height: 15px;
}
/* 每颗单独还原 */
.recycle-restore {
  border: 1px solid var(--line); border-radius: 8px; background: var(--surface-2);
  color: var(--muted); font-size: calc(12px * var(--fs)); padding: 1px 7px; cursor: pointer;
}
.recycle-restore:hover { color: var(--brand); border-color: var(--brand); }
/* 破裂时剩余时间：负号 = 提前完成，正号 = 拖延（用户指定的记号）*/
.recycle-badge {
  font-size: calc(11px * var(--fs)); font-weight: 650; font-variant-numeric: tabular-nums;
  color: var(--muted);
}
.recycle-badge.early { color: var(--ok); }        /* 提前完成：好事 */
.recycle-badge.late  { color: var(--warn); }      /* 拖延：提醒一下 */
.recycle-when { font-size: calc(10.5px * var(--fs)); color: var(--muted); font-variant-numeric: tabular-nums; }
/* ===== 月历上的节日与农历（用户要求"月历里载入节日"）=====
   ⚠️ 节日用**专用红**，和气泡区那颗节日泡泡是同一个颜色（core/holidays.js 的 FESTIVAL_COLORS）*/
.month-cell .cell-lunar { font-size: calc(10px * var(--fs)); color: var(--muted); margin-left: auto; }
.month-cell .cell-festival {
  font-size: calc(11px * var(--fs)); font-weight: 600; color: #e03a3a;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.month-cell.has-festival { box-shadow: inset 0 0 0 1px color-mix(in srgb, #e03a3a 35%, transparent); }
.month-cell.has-festival .daynum { color: #e03a3a; }

/* ---------------------------------------------------------------------------
   「闹钟」板块（web/ui/views/alarms.js）

   ⚠️ 只在 iOS 壳里出现（门控在视图里），但样式放在这儿是**无害**的：
      类名都带 `al-` 前缀，别的视图不会撞上；而且 CSS 不能按设备条件加载
      （Service Worker 预缓存的是整份 views.css）。
   --------------------------------------------------------------------------- */
.alarms-page { display: flex; flex-direction: column; gap: 18px; padding: 4px 2px 40px; }

/* 宽度围栏：桌面上这一页原来铺满 1400px，一行说明横跨半个屏幕、
   控件却挤在左边一小撮。手机上（<720px）它就是满宽，等于没有。
   `margin-inline: auto` 让窄屏时居中、宽屏时也不贴左。 */
.al-page {
  width: 100%; max-width: 720px; margin-inline: auto;
  display: flex; flex-direction: column; gap: 14px;
}

/* 页头：左边说现状、右边是唯一的主操作（＋ 新建闹钟） */
.al-actions {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  justify-content: space-between;
}
.al-actions-info { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.al-actions-count { font-size: calc(17px * var(--fs)); font-weight: 650; }
.al-actions-hint { margin: 0; }
.al-new { flex: none; padding: 10px 16px; font-size: calc(15px * var(--fs)); }

/* 可折叠块（计时器 / 铃声与诊断）：平时收起来，首屏留给"我建了哪些闹钟" */
.al-fold {
  border: 1px solid var(--line); border-radius: var(--r-md);
  background: var(--surface); overflow: hidden;
}
.al-fold-head {
  display: flex; align-items: baseline; gap: 8px;
  padding: 12px 16px; cursor: pointer; list-style: none;
}
.al-fold-head::-webkit-details-marker { display: none; }
.al-fold-title { font-size: calc(14px * var(--fs)); font-weight: 650; }
.al-fold-hint { font-size: calc(12px * var(--fs)); color: var(--muted); flex: 1; min-width: 0; }
.al-fold-caret { color: var(--muted); font-size: calc(12px * var(--fs)); transition: transform .15s ease; }
.al-fold[open] .al-fold-caret { transform: rotate(180deg); }
.al-fold[open] .al-fold-head { border-bottom: 1px solid var(--line); }
.al-fold-body { padding: 12px 16px 14px; display: flex; flex-direction: column; gap: 10px; }
/* 折叠块里面的 section 不再画自己的边框 —— 否则卡里套卡，手机上看着像三层抽屉 */
.al-fold-body .al-block {
  border: none; background: transparent; padding: 0; border-radius: 0;
}

/* 正在"改某一条"时，表单左边竖一条品牌色 —— 配合标题（新建闹钟 / 修改闹钟），
   让"我现在是在新建还是在改"不用读文字就能看出来。 */
.al-block.is-editing { border-left: 3px solid var(--brand); }

/* 表单里的几栏：够宽就并排，不够就各占一行（`auto-fit` 自己决定）。
   最后一栏（铃声）永远整行 —— 它的选项文字最长，挤成 1/3 会被截断。 */
.al-fields {
  display: grid; gap: 10px;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
}
.al-fields > .al-field:last-child { grid-column: 1 / -1; }

.al-block {
  border: 1px solid var(--line); border-radius: var(--r-md);
  background: var(--surface); padding: 14px 16px;
  display: flex; flex-direction: column; gap: 10px;
}
.al-block-head h3 { font-size: calc(14px * var(--fs)); font-weight: 650; margin: 0 0 2px; }
.al-block-head .tiny { margin: 0; }

.al-row { display: flex; align-items: flex-end; gap: 10px; flex-wrap: wrap; }
.al-field { display: flex; flex-direction: column; gap: 4px; }
.al-field.al-grow { flex: 1; min-width: 160px; }
.al-field.al-center { align-items: center; }
.al-lbl { font-size: calc(12px * var(--fs)); color: var(--muted); }
.al-hint { font-size: calc(12px * var(--fs)); color: var(--muted); align-self: center; }
.al-colon { font-size: calc(18px * var(--fs)); color: var(--muted); align-self: center; }

.al-input {
  padding: 8px 10px; border: 1px solid var(--line); border-radius: var(--r-sm);
  background: var(--bg); color: inherit; font-size: calc(14px * var(--fs)); min-width: 0;
}
.al-input-sm { width: 72px; text-align: center; font-variant-numeric: tabular-nums; }

.al-quick { display: flex; gap: 6px; flex-wrap: wrap; }
.al-weekdays { display: flex; gap: 6px; flex-wrap: wrap; }

/* ---------------------------------------------------------------------------
   时间滚轮（闹钟页的「时刻」）—— 外层滚动列 + 中央可打字的格子
   ⚠️ 行高必须与 JS 里的 `ROW_H`（`web/ui/timewheel.js` 的 36）一致，而且要能整除
      容器高度：36 × 5 = 180。不是整数倍的话，在 DPR=3 的手机上吸附完会亚像素抖动。
   --------------------------------------------------------------------------- */
.tw { display: flex; flex-direction: column; align-items: center; gap: 2px; }
.tw-cols {
  position: relative; display: flex; align-items: center; gap: 2px;
  padding: 6px 14px; border: 1px solid var(--line); border-radius: var(--r-md);
  background: var(--surface);
}
/* 中央选择条：只是视觉锚点，绝不吃点击（否则点不到中央那格） */
.tw-band {
  position: absolute; left: 8px; right: 8px; top: 50%; height: 36px;
  transform: translateY(-50%); border-radius: 10px;
  background: var(--brand-soft); opacity: .9; pointer-events: none;
}
.tw-col {
  /* 上下淡出（iOS 滚筒的样子）。定义在这里而不是 :root —— 它是这个零件的私事。
     ⚠️ `--tw-fade` 以前**从没定义过**，于是 `mask-image: var(--tw-fade)` 整条声明
     静默失效、淡出从来没生效（10-13 逐个核对 token 时发现）。 */
  --tw-fade: linear-gradient(180deg, transparent 0, #000 26%, #000 74%, transparent 100%);
  height: 180px; width: 64px; overflow-y: scroll; scroll-snap-type: y mandatory;
  overscroll-behavior: contain; text-align: center; position: relative;
  scrollbar-width: none; -webkit-overflow-scrolling: touch;
  -webkit-mask-image: var(--tw-fade); mask-image: var(--tw-fade);
}
.tw-col::-webkit-scrollbar { display: none; }
.tw-col:focus-visible { outline: 2px solid var(--brand); outline-offset: 4px; border-radius: 8px; }
.tw-pad { height: 72px; }                 /* (180 - 36) / 2 = 72：首尾留空，任意一行都能停到中央 */
.tw-item {
  height: 36px; line-height: 36px; font-size: calc(24px * var(--fs)); cursor: pointer; user-select: none;
  font-variant-numeric: tabular-nums; color: var(--muted);
  scroll-snap-align: center; scroll-snap-stop: always;
  transition: color .12s ease, font-size .12s ease;
}
.tw-item.is-sel { color: var(--text); font-weight: 600; }
/* 中央那格的上下邻居压暗一点：眼睛先落到中间，不用靠那条选择条才看得出选的是哪个。
   （`:has()` 在现代浏览器都有；不支持也只是回到原来的样子，不影响选择。） */
.tw-item.is-sel + .tw-item,
.tw-item:has(+ .tw-item.is-sel) { opacity: .45; }
.tw-item:hover { color: var(--text); }
.tw-sep { font-size: calc(24px * var(--fs)); font-weight: 600; color: var(--muted); }
/* 编辑态：锁住这一列，中央原地换成输入框（键盘弹出时不会把滚轮顶得乱跳） */
.tw-col.is-editing { overflow: hidden; }
.tw-edit {
  width: 100%; height: 36px; font-size: calc(24px * var(--fs)); font-weight: 600; text-align: center;
  font-variant-numeric: tabular-nums; color: var(--text);
  border: none; border-bottom: 2px solid var(--brand); border-radius: 0 0 6px 6px;
  background: transparent; padding: 0;
}
.tw-edit:focus { outline: none; }
.tw-hint { margin: 0; text-align: center; }
@media (prefers-reduced-motion: reduce) {
  .tw-item { transition: none; }
  .tw-col { scroll-behavior: auto; }
}

/* 计时器大字：一整块里最显眼的东西（用户瞄一眼就要知道还剩多久） */
.al-timer-big {
  font-size: calc(52px * var(--fs)); font-weight: 700; letter-spacing: 2px;
  font-variant-numeric: tabular-nums; line-height: 1.1; text-align: center;
  padding: 6px 0 0;
}
.al-timer-sub { text-align: center; font-size: calc(13px * var(--fs)); color: var(--muted); margin-bottom: 4px; }

.al-list { display: flex; flex-direction: column; gap: 8px; }
.al-item {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 12px; border: 1px solid var(--line); border-radius: var(--r-sm);
  background: var(--bg);
}
/* 关掉的那条要**一眼看出来是关的**（否则开着的和关着的长得一样） */
.al-item.is-off { opacity: .55; }
.al-item-main { flex: 1; min-width: 0; cursor: pointer; }
.al-item-title { font-weight: 600; font-size: calc(15px * var(--fs)); }
.al-item-sub { font-size: calc(12px * var(--fs)); color: var(--muted); margin-top: 2px; word-break: break-all; }

.al-switch {
  flex: none; width: 46px; height: 28px; border-radius: 999px;
  border: 1px solid var(--line); background: var(--surface); color: var(--muted);
  font-size: calc(12px * var(--fs)); cursor: pointer;
}
.al-switch.on {
  background: var(--brand); border-color: var(--brand); color: #fff; font-weight: 650;
}

.al-sounds { display: flex; flex-direction: column; gap: 8px; }
.al-sound {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 12px; border: 1px solid var(--line); border-radius: var(--r-sm);
  background: var(--bg);
}
.al-sound-main { flex: 1; min-width: 0; }

/* 诊断区：等宽字体 + 可横向滚动（id 很长，不能换行换得看不懂） */
.al-diag {
  margin: 0; padding: 10px 12px; border: 1px solid var(--line); border-radius: var(--r-sm);
  background: var(--bg); font-size: calc(12px * var(--fs)); line-height: 1.6;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  white-space: pre-wrap; word-break: break-all; overflow-x: auto;
}
.al-foot { color: var(--muted); }

/* ============================================================
 * 「模拟选课」
 *
 * 版面：宽屏"左列表 + 右模拟课表"；窄屏**把模拟课表提到列表上面** ——
 * 手机上排到一长串课程底下，等于没有。
 * 配色只用 tokens（`--cc-*` 那一套是课程色，和课表/气泡同一个口径）。
 * ⚠️ 这一块只给私有树里那一页用：公开精简版里那一页**整份都不在**，所以这份
 *    样式在副本里就是一段没人用的死规则（跟别的被砍模块一个待遇，故意留着）。
 *    ⚠️ 注释里**不要写出那个文件名**：文件名在导出器的泄漏词表里，写进来就判硬失败
 *    （2026-10-13 真被咬过一次）。要指路就写"侧栏那一页"。
 * ============================================================ */
.me {
  width: 100%; max-width: 1080px; margin-inline: auto;
  display: flex; flex-direction: column; gap: 12px;
}
.me-main { display: flex; flex-direction: column; gap: 12px; }
.me-main > * { min-width: 0; }

/* 宽屏两栏：筛选条 + 列表在左，模拟课表在右（跟着滚，不跑掉） */
@media (min-width: 900px) {
  .me-main {
    display: grid; grid-template-columns: minmax(0, 1fr) 320px;
    column-gap: 14px; row-gap: 12px; align-items: start;
  }
  .me-main > :first-child { grid-column: 1; grid-row: 1; }
  .me-list { grid-column: 1; grid-row: 2; }
  .me-side { grid-column: 2; grid-row: 1 / span 2; position: sticky; top: 10px; }
}

/*
 * 窄屏（<900px）：只有一列，顺序就按 DOM 走 —— 而 DOM 是"筛选条 → 列表 → 模拟课表"，
 * 于是模拟课表会被一长串课程顶到最底下（2026-10-13 真浏览器量出来：平板 820、手机 414
 * 都是 `sideTop > listTop`）。上面那句"把模拟课表提到列表上面"当时只是注释，CSS 没做 ——
 * 所以这里显式排一次序：筛选条 → **模拟课表** → 列表。
 * 为什么是这个顺序：选完一门课要马上看见"撞没撞"和已选几门，排在 60 条课程底下等于没有。
 */
@media (max-width: 899px) {
  .me-bar { order: 0; }
  .me-side { order: 1; }
  .me-list { order: 2; }

  /*
   * 候选 #8（2026-10-14）：这张卡**跟着走**。
   * 为什么：课程列表在手机上 18265px 高（整页 20061），卡片原来跟着页面滚 ——
   * 往下滚 3 屏它已经在 `-1407`，滚到底 `-17868`。而"选了几门 / 撞没撞"恰好是
   * 选课过程中**最需要随时看到**的两个数（撞了要马上换班）。
   * 做法：钉在顶栏下面（`--topbar-h` 是 JS 量出来的真实顶栏高，别写死），
   * 高度上限 46vh —— 不然它会吃掉大半屏；超出的部分在**卡片内部**滚。
   * ⚠️ `z-index: 2` 必须**低于**标签栏那条 30（`web/css/layout.css` 里），
   *    否则手机上这张卡会盖住底部导航。
   * ⚠️ `.me-side` 的父级 `.me-main` 在这一档是 flex 列（≥900px 才变 grid）——
   *    留在 grid 里 sticky 会静默失效（详见月历那一处的注释）。
   */
  .me-side {
    position: sticky; top: calc(var(--topbar-h, 58px) + 4px); z-index: 2;
    max-height: 46vh;
  }
  .me-side-card { max-height: 100%; overflow-y: auto; }
}

/* 学期档：一张一张小卡片，选中的那档描品牌色 */
.me-tabs { display: flex; flex-wrap: wrap; gap: 8px; }
.me-tab {
  display: flex; flex-direction: column; align-items: flex-start; gap: 2px;
  padding: 8px 12px; text-align: left; cursor: pointer;
  border: 1px solid var(--line); border-radius: var(--r-md); background: var(--surface);
}
.me-tab.is-on { border-color: var(--brand); background: var(--brand-soft); }
.me-tab-name { font-size: calc(14px * var(--fs)); font-weight: 600; }
.me-tab-rank { font-size: calc(11px * var(--fs)); color: var(--muted); }
.me-tab.is-on .me-tab-rank { color: var(--brand); }
/* 「今年这一次」用虚线边：一眼看出它跟那两档"已有的"不是一回事 */
.me-tab-next { border-style: dashed; }

.me-bar { display: flex; flex-direction: column; gap: 8px; }
.me-bar-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.me-search {
  flex: 1 1 200px; min-width: 0; padding: 8px 10px;
  border: 1px solid var(--line); border-radius: var(--r-sm);
  background: var(--surface); color: var(--text); font-size: calc(14px * var(--fs));
}
.me-select {
  padding: 8px 10px; max-width: 100%;
  border: 1px solid var(--line); border-radius: var(--r-sm);
  background: var(--surface); color: var(--text); font-size: calc(14px * var(--fs));
}
.me-chip {
  padding: 5px 10px; cursor: pointer;
  border: 1px solid var(--line); border-radius: 999px;
  background: var(--surface); color: var(--text); font-size: calc(12px * var(--fs));
}
.me-chip.is-on { background: var(--brand); border-color: var(--brand); color: #fff; }
.me-days { gap: 6px; }
.me-sort { margin-left: auto; }
/* 手机上把星期那一排收窄一点：8 枚 chip（每天 + 周一…周日）挤进一行，省下半屏 */
@media (max-width: 480px) {
  .me-days { gap: 4px; }
  .me-days .me-chip { padding: 5px 7px; }
}

/* 课程卡 → 教学班 → 上课时间 */
.me-list { display: flex; flex-direction: column; gap: 8px; }
.me-course {
  display: flex; flex-direction: column; gap: 6px;
  padding: 10px 12px; border: 1px solid var(--line);
  border-radius: var(--r-md); background: var(--surface);
}
.me-course-head { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.me-course-name { font-size: calc(15px * var(--fs)); font-weight: 650; }
.me-credit { font-size: calc(12px * var(--fs)); color: var(--brand); }
.me-dim { font-size: calc(12px * var(--fs)); color: var(--muted); }
.me-class {
  display: flex; gap: 10px; align-items: flex-start;
  padding: 6px 8px; border-radius: var(--r-sm); background: var(--bg);
}
.me-class.is-on { background: var(--brand-soft); }
.me-class-main {
  flex: 1 1 40%; min-width: 0;
  display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap;
  font-size: calc(13px * var(--fs));
}
.me-class-code { font-size: calc(11px * var(--fs)); color: var(--muted); font-variant-numeric: tabular-nums; }
.me-times {
  flex: 1 1 45%; min-width: 0;
  display: flex; flex-direction: column; gap: 1px;
  font-size: calc(12px * var(--fs));
}
.me-pick { flex: none; }
.me-pick.is-on { border-color: var(--brand); color: var(--brand); }
.me-more-link {
  align-self: flex-start; padding: 2px 0; cursor: pointer;
  border: 0; background: none; color: var(--brand); font-size: calc(12px * var(--fs));
}
.me-more { align-self: center; }
.me-count { margin: 0; }
.me-src { margin: 0; color: var(--muted); }

/* 右栏：已选 + 冲突 + 迷你课表 */
.me-side-card { display: flex; flex-direction: column; gap: 8px; }
.me-side-card h3 { margin: 0; font-size: calc(14px * var(--fs)); }
.me-picks { display: flex; flex-direction: column; gap: 4px; max-height: 190px; overflow: auto; }
.me-pick-row { display: flex; align-items: baseline; gap: 6px; min-width: 0; }
.me-pick-name { flex: none; font-size: calc(12px * var(--fs)); font-weight: 650; }
.me-pick-row .me-dim { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.me-warn { margin: 0; color: var(--danger); font-size: calc(12px * var(--fs)); }
.me-side-foot { display: flex; align-items: center; gap: 8px; justify-content: space-between; flex-wrap: wrap; }

/* 迷你课表：7 列 × 11 节。一格放不下课名就省略号，鼠标悬停看全名（title） */
.me-grid { display: grid; grid-template-columns: 26px repeat(7, minmax(0, 1fr)); gap: 2px; }
.me-grid-head, .me-grid-sec { font-size: calc(10px * var(--fs)); color: var(--muted); text-align: center; }
.me-grid-cell {
  height: 18px; border-radius: 3px; background: var(--bg);
  font-size: calc(9px * var(--fs)); line-height: 18px; text-align: center;
  overflow: hidden; white-space: nowrap;
}
.me-grid-cell.is-on { color: #fff; font-weight: 600; }

@media (max-width: 720px) {
  .me-class { flex-direction: column; gap: 6px; }
  .me-pick { align-self: flex-start; }
  .me-grid-cell { height: 16px; line-height: 16px; font-size: calc(8px * var(--fs)); }
  .me-sort { margin-left: 0; }
}

/* ---- ③ 时间冲突（课表页常驻提示）----
   和"学期没校准"那张卡一样要显眼，但不能长得像报错：暖色描边 + 正常的卡片底色，
   因为冲突是"课表本身有矛盾"，不是程序出问题了。 */
.conflict-card { border-left: 3px solid var(--warn); }

/* ---- ⑥ 导入撤销条 ----
   低调的补救入口，不是警告：一行字 + 一个按钮，按钮在右。 */
.import-undo-card { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.import-undo-card p.tiny { flex: 1 1 240px; margin: 0; }