.assessment-pane{width:360px;flex-shrink:0;padding:22px;overflow-y:auto}.week-card>span,.muted{font-size:13px;color:#777587}.week-card summary{cursor:pointer;font-size:12px}.week-card form{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}.assessment-heading{display:flex;align-items:center;justify-content:space-between;gap:10px}.assessment-heading h2{font-size:17px;font-weight:700;letter-spacing:-.02em;color:#1c1c1e;margin:0}.assessment-dialog button,.week-card button{background:#1c1c1e;color:#fff;border:0;border-radius:10px;padding:8px 13px;font-size:13px}.assessment-dialog [data-cancel]{background:#f2f2f7;color:#1c1c1e}/* 考核卡：白面 + 1px hairline。原来那套"静态布光"（多层渐变 + inset 高光）整段去掉——
   白底上再叠白色渐变只会让卡片发灰；边界交给 hairline，体积交给一层极轻投影就够。
   依然不用 backdrop-filter：要给每个元素开合成层，右栏一列卡片滚动时逐帧重算，VPS 上纯属白烧 CPU。 */
.event-card{position:relative;padding:14px 15px;margin-top:10px;border-radius:16px;
  background:#fff;border:1px solid rgba(60,60,67,.08);
  box-shadow:0 1px 2px rgba(0,0,0,.04)}
.event-card h3{font-size:15px;font-weight:600;margin:8px 0 0;letter-spacing:-.01em;color:#1c1c1e;overflow-wrap:anywhere}
.event-card .assessment-heading{font-size:10px;font-weight:600;letter-spacing:.08em;color:#8e8e93}
.event-card .assessment-heading b{font-size:11px;font-weight:600;letter-spacing:0;color:#1c1c1e;background:#f2f2f7;padding:2px 7px;border-radius:6px}
.event-meta{display:flex;align-items:baseline;gap:7px;flex-wrap:wrap;font-size:11.5px;color:#8e8e93;margin-top:5px}
.event-meta a{color:#1c1c1e;text-decoration:none}
.event-meta a:hover{text-decoration:underline;text-decoration-color:rgba(60,60,67,.35)}
.event-scope{font-size:12px;line-height:1.55;color:#8e8e93;margin-top:7px;white-space:pre-wrap;overflow-wrap:anywhere}
.event-scope.is-empty{color:#c7c7cc}
.event-scope.is-folded>summary{display:inline-flex;align-items:center;gap:5px;font-size:11.5px;color:#8e8e93;cursor:pointer;list-style:none;padding:1px 0}
.event-scope.is-folded>summary::-webkit-details-marker{display:none}
.event-scope.is-folded>summary::before{content:"";width:0;height:0;border-left:4px solid currentColor;border-top:3.5px solid transparent;border-bottom:3.5px solid transparent;transition:transform .18s ease}
.event-scope.is-folded[open]>summary::before{transform:rotate(90deg)}
.event-scope.is-folded>summary:hover{color:#1c1c1e}
.event-scope.is-folded>summary:focus-visible{outline:2px solid #8e809f;outline-offset:2px;border-radius:4px}
.event-scope.is-folded>p{margin-top:5px;padding-left:10px;border-left:2px solid rgba(60,60,67,.16)}
.is-overdue{border-color:rgba(255,149,0,.35)}
.overdue-text{font-size:11.5px;color:#9c6237;margin-top:6px}.event-empty{font-size:13px;line-height:1.8;padding:24px 0;color:#888}.assessment-dialog{width:min(460px,94vw);max-height:90vh;border:1px solid #e5e1ec;border-radius:24px;padding:26px}.assessment-dialog::backdrop{background:#25203355}.assessment-dialog h2{font-size:21px;font-weight:700;margin-bottom:18px}.assessment-dialog label{display:block;font-size:13px;margin:12px 0}.assessment-dialog input,.assessment-dialog select,.assessment-dialog textarea,.week-card input{display:block;width:100%;border:1px solid #dcd6e6;border-radius:9px;padding:9px;background:white;margin-top:5px}.assessment-dialog [data-error]{color:#a74444;font-size:13px}.course-overview{height:100vh;overflow:auto}.overview-grid{display:grid;grid-template-columns:minmax(0,1fr) 330px;gap:48px;max-width:1240px;margin:0 auto;padding:40px 32px}.overview-grid h1{font-size:32px;font-weight:800;margin:8px 0 28px}.overview-grid h2{font-size:20px;font-weight:700;margin:26px 0 14px}.lecture-list>a{display:flex;justify-content:space-between;gap:20px;background:#fff;padding:20px;border:1px solid #e8e4ed;border-radius:14px;margin-bottom:10px}.lecture-list>a:hover{border-color:#a896c4}.lecture-list small{color:#807789;white-space:nowrap}.lecture-list>p{padding:20px;color:#888}
.lecture aside.quiz{border-left:6px solid #d2ccdf!important;position:relative}.lecture aside.quiz.is-correct{border:2px solid #74a88a!important;border-left-width:6px!important;background:#f0f8f2!important}.lecture aside.quiz.is-wrong{border:2px solid #cd8291!important;border-left-width:6px!important;background:#fff2f4!important}.lecture aside.quiz.is-shown,.lecture aside.quiz.is-recovering{border:2px solid #d4aa60!important;border-left-width:6px!important;background:#fff8ea!important}.q-status{display:block;width:fit-content;border-radius:8px;background:#eeeaf4;color:#766886;padding:5px 11px;margin:10px 0 16px;font-size:13px;font-weight:700}.is-correct>.q-status{background:#d7ecdd;color:#326446}.is-wrong>.q-status{background:#f2d9de;color:#9d4055}.is-recovering>.q-status,.is-shown>.q-status{background:#f2e4c3;color:#8b641e}.q-book{border:1px solid #b9abc9;border-radius:9px;padding:8px 12px;font-size:13px;color:#716198}.q-check:disabled{opacity:.55;cursor:default}.q-bar{flex-wrap:wrap}
@media(max-width:900px){.assessment-pane{width:290px;padding:20px}.overview-grid{grid-template-columns:1fr;gap:20px}}@media(max-width:640px){.home-columns{display:block;overflow-y:auto}.home-columns>main{overflow:visible}.assessment-pane,.assessment-pane:not(:focus-within){position:static;width:100%;max-height:none;padding:24px}.overview-grid{padding:22px}}
.lecture .quiz.is-recovering .q-verdict{display:block;color:#8b641e}.lecture .quiz.is-recovering .q-status{display:block}
/* Static glass lighting: no blur, animation, image, or additional font request. */
/* ------------------------------------------------------------ 周数卡 · Plan A「Cupertino」
   纯白实心面 + 1px hairline + 20px 圆角 + 一层极轻投影。
   渐变、色晕、inset 高光、紫色全部移除；整块只留一个彩色，就是日历上那颗系统红点。 */
.week-card{position:relative;isolation:isolate;padding:18px 18px 16px;margin-bottom:26px;border-radius:20px;
  background:#fff;border:1px solid rgba(60,60,67,.08);
  box-shadow:0 1px 2px rgba(0,0,0,.04),0 12px 30px -22px rgba(0,0,0,.35)}
.week-card .week-eyebrow{display:block;font-size:11px;font-weight:500;letter-spacing:.01em;color:#8e8e93;margin-bottom:16px}
.week-calendar-layout{display:grid;grid-template-columns:auto 1fr;gap:18px;align-items:center}
.week-display{align-self:center;padding-bottom:0}
.week-card h2[data-week]{display:flex;flex-direction:column;margin:0;line-height:1;color:#1c1c1e;font-family:-apple-system,BlinkMacSystemFont,"SF Pro Display","Helvetica Neue",sans-serif}
/* 大数字：实心深灰 + 600 字重 + 紧字距。
   原来那套「76px / 字重 300 / 渐变裁切文字 / text-shadow」是 Dribbble 语汇，不是 Apple——
   Apple 的大数字是一个像素的渐变都没有的。 */
.week-word{display:block;font-size:11px;font-weight:600;letter-spacing:.1em;text-transform:uppercase;color:#8e8e93}
.week-number{display:block;font-size:56px;font-weight:600;letter-spacing:-.05em;line-height:.95;margin:3px 0 0;color:#1c1c1e;font-variant-numeric:lining-nums tabular-nums}
.week-caption{display:block;font-size:9px;font-weight:500;letter-spacing:.16em;color:#c7c7cc;margin-top:6px}
.week-card h2.week-unset{font-size:20px;font-weight:600;line-height:1.5;margin-bottom:12px}
.mini-calendar{min-width:0;border-left:1px solid rgba(60,60,67,.1);padding-left:18px;font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","Helvetica Neue",sans-serif}
.calendar-month{font-size:11px;font-weight:600;color:#1c1c1e;margin-bottom:9px}
.calendar-month b{font-weight:600;color:#1c1c1e;margin-left:3px}
.calendar-grid{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));row-gap:1px;text-align:center;align-items:center;font-variant-numeric:tabular-nums}
.calendar-weekday{font-size:9px;font-weight:500;color:#c7c7cc;padding-bottom:6px}
.calendar-day{position:relative;display:grid;place-items:center;min-height:23px;font-size:11px;font-weight:500;color:#1c1c1e;border-radius:50%}
/* 今天 = 实心深灰圆（原来是从紫到紫的渐变球 + inset 高光） */
.calendar-day.is-today{background:#1c1c1e;color:#fff;font-weight:600}
/* 截止日 = 3px 系统红点。整块唯一的彩色，所以它才醒目。 */
.calendar-day.has-event:after{content:"";position:absolute;bottom:1px;width:3px;height:3px;border-radius:50%;background:#ff3b30}
.calendar-day.is-today.has-event:after{background:#fff}
.week-card>details{margin-top:16px;padding-top:12px;border-top:1px solid rgba(60,60,67,.1)}
.week-card summary{font-size:10px;color:#8e8e93;list-style-position:inside}
.week-card summary:hover{color:#1c1c1e}
.week-card summary:focus-visible{outline:2px solid #8e809f;outline-offset:4px}
.week-card form{font-size:12px}
/* 「＋」按钮：白底圆形 + hairline，跟卡片同一套语言。
   选择器只认 [data-add]——.assessment-heading 在「添加考核」弹窗的按钮行上也用着，
   写成 .assessment-heading button 会把弹窗那两个按钮一起吃掉。 */
.assessment-heading>button[data-add]{display:grid;place-items:center;width:30px;height:30px;padding:0;border-radius:50%;
  background:#fff;border:1px solid rgba(60,60,67,.12);color:#1c1c1e;box-shadow:0 1px 2px rgba(0,0,0,.05);
  transition:background .15s ease,transform .15s ease}
.assessment-heading>button[data-add]:hover{background:#f2f2f7;transform:scale(1.04)}
.assessment-heading>button[data-add] .ico{width:15px;height:15px}
@media(min-width:641px) and (max-width:900px){.assessment-pane{width:310px}}
.event-card.is-completed{border-color:rgba(60,60,67,.05);background:#fff;box-shadow:none}
.event-card.is-completed h3,.event-card.is-completed .event-meta,.event-card.is-completed .event-meta a,.event-card.is-completed .event-scope,.event-card.is-completed .event-scope>summary,.event-card.is-completed>p{text-decoration:line-through;text-decoration-thickness:1px;text-decoration-color:#d1d1d6;color:#c7c7cc}
.event-card.is-completed .assessment-heading{color:#c7c7cc}
.event-card.is-completed .assessment-heading b{color:#c7c7cc;background:#f7f7f9}
.event-controls{display:flex;gap:7px;margin-top:12px}
.event-controls button{display:grid;place-items:center;width:28px;height:28px;border-radius:50%;color:#8e8e93;background:#f2f2f7;border:0;transition:background .15s ease,color .15s ease}
.event-controls button:hover{background:#e5e5ea;color:#1c1c1e}
.event-controls button:focus-visible{outline:2px solid #8e809f;outline-offset:2px}
.event-controls .ico{width:14px;height:14px}
.event-controls button[aria-pressed=true]{color:#34c759;background:#e3f2e8}
@media(prefers-reduced-motion:reduce){.event-controls button{transition:none}.event-scope.is-folded>summary::before{transition:none}}
.lecture-entry{display:flex;align-items:center;background:#fff;border:1px solid #e8e4ed;border-radius:14px;margin-bottom:10px}.lecture-entry:hover{border-color:#a896c4}.lecture-entry>a{display:flex;align-items:center;justify-content:space-between;gap:20px;padding:20px;flex:1;min-width:0}.lecture-entry [data-lecture-title]{overflow-wrap:anywhere}.lecture-rename{display:grid;place-items:center;flex-shrink:0;margin-right:12px;width:32px;height:32px;border-radius:9px;color:#94879f}.lecture-rename:hover{background:#eee8f5;color:#67517e}.lecture-rename:focus-visible{outline:2px solid #8e809f;outline-offset:2px}.lecture-rename .ico{width:15px;height:15px}[data-rename-error]{color:#a74444;font-size:13px;margin:12px 0}
/* ---------------------------------------------------------------- 跨文档视图过渡
   点速览卡进页面时，那张卡会**长成**目标页上的那一块（项目页的大封面 / 课程总览的页头），
   其余部分交叉淡入——而不是"先放大一张封面、再硬切到新页面"。后者两件事之间没有关系，
   看着就是生硬，这正是原来那个做法的问题。

   要点：两端要有**同名**的 view-transition-name 才会做共享元素变形；名字对不上就退化成
   整页交叉淡入，也比硬切好。名字在同一个文档里必须唯一，所以卡片那侧是点击时由 JS
   临时写上去的（一页里有好几张卡）。

   浏览器不支持这条 at-rule 时完全无感，就是普通跳转——JS 那边会退回封面浮层那个方案。 */
@view-transition { navigation: auto; }

::view-transition-old(root),
::view-transition-new(root) { animation-duration: .42s; }

::view-transition-group(peek-morph) {
  /* 慢一点。.46s 时整个变形一闪就过去了，看不出"卡片长成页面"这件事；
     配长减速曲线，慢下来反而更稳。 */
  animation-duration: .7s;
  animation-timing-function: cubic-bezier(.22, 1, .36, 1);
}

/* 目标页上"被卡片长成"的那一块。项目页在 playground.css 里。
   ⚠️ 2026-09-12 起这一块的**比例和 morph 源头不再匹配**，是已知取舍：
   首页 `.peek-card` 1.50 / `.hcc-cover` 1.60，这块现在是 3.86（全幅封面 834×216）。
   旧版是 240×150（正好 1.60），比例对得上才"像同一张图放大"；现在跨文档转场的
   那 .7s 里快照会被拉宽。想彻底不拉就**删掉这一行**（退回 root 的 .42s 交叉淡入），
   代价是没有"卡片长成页面"的关联感。别改成给某个子元素挂名字——子元素同样是 3.86。 */
.course-overview .course-hero { view-transition-name: peek-morph; }

/* ---------------------------------------------------------------- 首页（两栏）
   考核日历从右栏并进左栏，右栏取消：三栏一挤，主栏那点宽度放不下速览 + 两列课程卡。
   "最近访问"整块删掉——它被上面新增的"速览"顶了，那是同一件事的更好版本。
   课程卡改成 Playground 项目卡那个路子：封面在上、信息在下，封面可换。 */

/* 侧栏宽度只有一个来源：`--home-side-w`。顶栏的右边界也读它（见 `.home-page #site-nav`），
   改宽度就改这一处，别在别处再写一个 360。

   **两栏之间没有竖线**（2026-09-11 用户定：改成"无边框留白"）。原来这里有一条
   `border-left:1px solid rgba(255,255,255,.7)`，撤掉之后 `.hub-aurora` 那层色晕从主栏
   一路贯到侧栏，中间不断；两块区域靠**留白**分开，不靠线。
   ⚠️ 别再往这儿加 `border-left/right`、`box-shadow` 或者伪元素分隔线——列本身要完全透明。
   （卡片上的边框和投影不受影响：那是卡片自己的，不是列的分隔。） */
.home-page{--home-side-w:360px}
.home-side{width:var(--home-side-w,360px);flex-shrink:0;overflow-y:auto;display:flex;flex-direction:column}
/* 侧栏顶部：一句问候。头像 / 昵称 /「今天已学」都撤了（2026-09-11 用户定）——
   顶栏药丸右侧已经有头像菜单和今天学了多久，侧栏再摆一遍是重复。
   条高锁 **60px**（= 12px 上边距 + 48px 药丸，2026-09-15 从 68 跟着药丸一起收），
   这样问候行的垂直中心和药丸对齐，下面的周数卡也正好落在主栏 Preview 那一行的水平线上。
   上内边距补 12px 就是为了那个对齐——光靠 align-items:center 会落在 30，
   比药丸中心 36 高 6px，并排看是歪的。box-sizing 是 preflight 给的 border-box，
   所以 min-height 含这 12px。
   （⚠️ 2026-09-13 侧栏整个撤了，这条现在没页面在用，留着是"哪天加回来"的备查。） */
.home-side-top{display:flex;align-items:center;min-height:60px;padding:12px 26px 0}
.home-side-hi{margin:0;font-size:18px;font-weight:700;line-height:1.2;letter-spacing:-.01em;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.home-side-cal{padding:26px}
.home-side-cal .week-card{margin-bottom:22px}

.home-main{flex:1;min-width:0;overflow-y:auto;padding:30px 34px 60px}
.home-main-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin:34px 0 20px}
.home-main-head h1{font-size:28px;font-weight:700;margin:0}

/* 学期筛选：小药丸。选中的实心黑，其余描边——和站里其它"选中"用同一套语言。 */
.home-main-title{display:flex;flex-direction:column;gap:11px;min-width:0}
/* 区块标题：**全系统字体**——中文 PingFang SC、拉丁 SF Pro，字重 500。
   三个标题共用这一条：「我的课程」「Projects」「习题库」
   （首页 / /courses / /exercises / 项目页，四个地方）。

   2026-09-11 之前挂的是 Instrument Serif（拉丁衬线）+ PingFang 700（中文）。
   因为 Instrument Serif 没有中文字形，中文一路落到系统字体，实际效果是
   **「Projects」是细衬线、「我的课程」是重黑体**——两个标题像两个来源。
   现在统一交给系统无衬线：中英同族，且和顶栏、卡片、按钮本来就是一套。
   500 比原来的 700 退后一档，标题不抢内容。

   不再需要 font-synthesis:none（那是为了压住 Instrument Serif 只有 400
   而拉丁被要求 700 时浏览器造的假粗体）；系统字体在 500 这一档不触发合成。 */
.home-main-title h1{font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Helvetica Neue',sans-serif;font-weight:500;font-size:34px;letter-spacing:.005em;line-height:1.15}
.sem-filter{display:flex;gap:6px;flex-wrap:wrap}
.sem-filter button{padding:4px 11px;border-radius:999px;border:1px solid var(--border-color);background:hsla(0,0%,100%,.9);font-size:11.5px;color:var(--text-muted);cursor:pointer;transition:color .15s,background-color .15s,border-color .15s}
.sem-filter button:hover{color:var(--text-main);border-color:#cfd6db}
.sem-filter button[aria-pressed="true"]{background:#1C1C1E;border-color:#1C1C1E;color:#fff}
.sem-empty{margin:0 0 18px;font-size:13px;color:var(--text-muted)}
/* 卡片上没写 display，[hidden] 本来就能生效；写死一条免得以后加布局把它顶掉 */
.home-course-card[hidden]{display:none}   /* 学期筛选靠它 */

/* ---- 速览：一行层叠卡 ----
   卡片做成横向长方形（3:2），文字压在封面上。之前是"封面 16:10 + 下面一段白底文字"，
   两者加起来 200×212，看着就是个方的。

   这里最容易做砸的一点：卡片是全幅图片，如果没有清楚的边界和投影，一排下来就只是
   "几张褪色的图"。所以三件事一起做——**描边**给出边界、**两层投影**（近处一圈接触阴影
   + 远处一层环境阴影）给出体积、**别把两侧卡淡得太狠**（原来 opacity 掉到 0.4，
   直接糊进背景，等于自己把边界擦掉了）。前一张的投影落在后一张身上，层级感就从这里来。

   （曾经在整行底下垫过一张渐变底卡想"凸显这一行"，效果不好，删了。） */

/* 标题条：**没有下划线**（2026-09-12 Allen：「先把 Preview 和课程卡片中间的横线去掉」）。
   原来有一条 `border-bottom:1px solid rgba(28,28,30,.1)`，去掉之后标题和卡片之间只靠留白分——
   和首页/项目页两栏"不许加竖线"是同一条取向（见 shell.md 第 20 条）。
   ⚠️ 别顺手把 `border-bottom` 加回来；要"分开"就调下面那两处留白，不要画线。 */
.peek-head{display:flex;align-items:baseline;justify-content:space-between;gap:14px;padding-bottom:6px}
/* 字号 20 → 26 → **30**（2026-09-12 Allen 两次提："Preview 字体调大一些" / "再放大一点"）。
   参照物：下面「我的课程」是 `.home-main-title h1` 的 34px / 字重 500；
   Preview 是 700，同字号会显得更重，所以停在 30。
   ⚠️ `line-height` 必须显式写：不写就吃 tw.css 里 h2 的 1.5，30px 会撑出 45px 的行盒，
   标题条白高一截、把卡片往下顶（标题条的留白是"看得见"的量，行盒一涨就白涨）。
   1.15 → 34.5px。 */
.peek-head h2{font-size:30px;font-weight:700;line-height:1.15;margin:0}
/* `.peek-hint`（右侧那句操作提示）2026-09-11 删了——模板里已经不再渲染这个元素，
   这条规则留着就是死规则。`.peek-head` 的 `justify-content:space-between` 保持不变：
   只有一个孩子时它跟 `flex-start` 等价，标题仍贴左。 */
/* Preview 主入口：桌面卡片 **460 × 249**，比例 1.85（一路 400×220/20:11 → 460×253/20:11
   → 460×207/20:9 → 现在这版，见 `.peek` 里的 `--peek-ratio`）。
   卡宽与居中 padding 共用变量，窄屏保持完整卡片可见。
   ⚠️ `padding-inline` 必须**正好半个卡宽**（`calc(50% - 卡宽/2)`），第 0 张才会在
   `scrollLeft = 0` 时居中；改卡宽不能只改一个。
   `padding-top` 38 → 26 → **18**（2026-09-12 Allen 两次提"往下更贴近这些课程卡片一点" /
   "跟下面课程卡片的距离再近一些"）。**标题到卡片的可见间距 = 本值 + `.peek-head` 的
   `padding-bottom`**，两处一起调才是那个数：现在 18 + 6 = **24px**（原来 26 + 8 = 34）。
   ⚠️ 这个值**不是**给"居中放大"留的余量——`transform-origin:center bottom` 的缩放只会把
   上边缘**往下**推（400 宽时 220×0.14 ≈ 31px），放大那张永远是最高的，缩小那张不可能溢出上边。
   真正的余量是卡片自己的投影（最多往上 2px）。所以往下压到 18 是安全的，别以为必须留 38。 */
/* ⚠️ 尺寸是**一组**，改卡宽必须同改下面那条 `margin-left`（见 shell.md 第 36 条）。
   一路 300 → 340 → 400 → **460**（2026-09-13 Allen：再适当放大一些）。
   `padding-inline` 读的就是这个变量，会自动保持"半个卡宽"；
   `home.js` 的衰减半径也是自算的，都不用跟着改。 */
.peek{--peek-card-width:460px;
  /* 卡片比例（2026-09-14 Allen 用稿子上的滑杆定的值）。
     一路 20/11（460×253）→ 我试的 20/9（460×207）→ **1.85（460×249）**。
     ⚠️ 别再写成分数形式：Allen 报的就是滑杆读数 `1.85`，写成 `37/20` 反而对不上。
     比例**只动高**：卡宽 / 重叠 −83 / 轨道 padding / `home.js` 的 `RANGE` 全是横向的，
     一个都不用跟着改（硬约定第 21 条管的是那一组）。
     ✅ **1.85 比 20/11 只矮 4px**，所以窄屏也安全、**不需要第二套比例**
     （20/9 那版就是因为太扁才在窄屏覆盖回 20/11，2026-09-14 已删掉那条覆盖）。
     ⚠️ 改完必量 `peek_geom.py` 的 `fade.ratio`：文字块探出 `.peek-cover::after`
     那层 74% 蒙版（ratio > 1）= 白字直接压在封面原图上。
     实测 1.85：桌面 460×**248** / 蒙版 184 / **0.44**，窄屏 300×**162** / 120 / **0.82**。 */
  --peek-ratio:1.85;
  /* 金属边框的两个旋钮（2026-09-14 Allen 用稿子滑杆定的值）。
     调参用 `preview/peek-card-metal.html`，那份带滑杆，别在这儿盲改。 */
  --peek-ring:1px;                    /* 环厚（1.5 → 1）*/
  --peek-flow:5s;                     /* 高光绕一圈的时长（9.5 → 5）*/
  /* ---- hover 抬起的缓动：**一条重阻尼弹簧**（2026-09-15 Allen 要的"阻尼感"）
     二阶系统单位阶跃响应，ζ=0.90 / ω=11.5，采样成 `linear()`（25 个点）。
     ⚠️ 和旧曲线 `cubic-bezier(.25,1.4,.4,1)` 的差别**不在过冲，在起步**：
        · 旧：起步斜率 = P1y/P1x = 1.4/0.25 = **5.60** —— 输入动 1px 输出先动 5.6px，
          鼠标一搭上去就是"啪"地弹开。Allen 的原话是"一 hover 到卡片就反应…有廉价感"；
        · 新：弹簧的 x'(0) = 0，**起步斜率 0.60**（慢 9 倍），前 4% 时间只走 2.5% 路程，
          然后才加速 —— 这就是"重"的来源。
     过冲：5.29% → **0%**（实测 25 个采样点里一个都没超过 1）。末点**钉成 1**，
     否则弹簧在窗口末尾还剩 0.24%，transition 结束那一下会瞬跳。
     ⚠️ 别换回 `cubic-bezier`：三次贝塞尔做不出"起步斜率为 0 又快速跟上"的形状，
     想要那种手感只能靠高次项，`linear()` 是唯一能直接写出来的形式。
     ⚠️ 改这两个数就重算一遍（脚本见 `.workbuddy-ai/verify/` 里的注释），别手改采样值。 */
  --peek-hover-ease:linear(0, 0.0249 4.17%, 0.0863 8.33%, 0.1686 12.5%, 0.2606 16.67%, 0.3547 20.83%, 0.4458 25%, 0.5307 29.17%, 0.6077 33.33%, 0.6758 37.5%, 0.7352 41.67%, 0.786 45.83%, 0.829 50%, 0.8648 54.17%, 0.8944 58.33%, 0.9185 62.5%, 0.938 66.67%, 0.9535 70.83%, 0.9657 75%, 0.9752 79.17%, 0.9826 83.33%, 0.9882 87.5%, 0.9924 91.67%, 0.9954 95.83%, 1 100%);
  /* ⚠️ **别把"流动"那条也做成变量**：它含 `var(--peek-angle)`，而 `--peek-angle` 是
     `inherits:false` 且**动画跑在 `::before` 上** —— 变量若声明在 `.peek` 这一层，
     `var()` 会在 `.peek` 处就解析成初始值 `0deg`，再继承下去，**动画直接死掉**。
     （原来这里还有个 `--peek-ring-static` = 原稿 `metal-buttons.html` 的
     `linear-gradient(68deg, …)`。2026-09-14「环只画在居中那张」之后它**没有任何使用点**，
     删了。色值要看就去 `preview/metal-buttons.html` 或 `git log -S peek-ring-static`。）*/
  }
.peek-track{display:flex;overflow-x:auto;overscroll-behavior-x:contain;scroll-snap-type:x mandatory;padding:18px max(14px,calc(50% - var(--peek-card-width)/2)) 48px;scrollbar-width:none;
  /* ⚠️ 两端渐隐（2026-09-12 Allen 报「左右两边出现阴影隔断」后加的）。
     根因不是投影形状，是**轨道自己的裁切边**：`overflow-x:auto` 会把 `overflow-y` 一起算成
     `auto`，于是内容按 padding box 裁。滚动后必然有卡片骑在轨道边上（实测
     `scrollLeft=656` 时卡片 1 的 rect 是 `[-56,…,314]`、卡片 3 是 `[600,…,970]`，
     轨道是 `[58,…,856]`），卡片和它的投影在 x=58 / 856 被**一刀切断**；
     轨道边到面板边还有 24px 净空，那道切口就露在面板的纯色底上 —— 就是"隔断"。
     渐隐让内容在切口之前就溶进面板底色。**宽度要比面板的横向 padding 略大**
     （面板 `padding-inline:24px`，这里 32px），否则还没淡完就被切断，白加。
     用 `mask-image` 而不是 `backdrop-filter`：遮罩是静态的，不逐帧重采样（同 preview 长条那套）。 */
  -webkit-mask-image:linear-gradient(to right,transparent 0,#000 32px,#000 calc(100% - 32px),transparent 100%);
  mask-image:linear-gradient(to right,transparent 0,#000 32px,#000 calc(100% - 32px),transparent 100%)}
.peek-track::-webkit-scrollbar{display:none}
.peek-track:focus-visible{outline:2px solid #8e809f;outline-offset:-2px;border-radius:20px}
/* ⚠️ 卡片**只做布局容器**：圆角、裁切、底色、投影全部交给内层 `.peek-inner`。
   原因就是"缩放的是内层"——留在卡片上的话，非居中的卡片会露出一圈**原尺寸的底 + 边框 + 投影**，
   而图已经缩到 86% 了，看着就是图片周围套了个白框（2026-09-13 Allen 报的"莫名其妙的白框"）。
   `transform` 的过渡只服务 `:hover`（见下面那条）：JS 写的 scale 在内层、不带过渡，
   滚动时逐帧跟随，加了过渡反而会拖影。 */
.peek-card{position:relative;flex:0 0 auto;width:var(--peek-card-width);aspect-ratio:var(--peek-ratio);
  scroll-snap-align:center;text-decoration:none;color:inherit;background:transparent;
  /* 这条是**离开**时用的：`.4s`，比进入短——缩回去拖太久会显得"粘手"。
     `transition` 取的是"当前状态"那条，所以进入那条（`.5s`）写在 `:hover` 里，
     进 / 出可以不一样长，这是 CSS 的既定行为，不是 hack。
     `filter` 也在这里过渡：模糊是**按 class 切**的（见下面 `:not(.is-active)`），
     不是逐帧写的 —— 逐帧写 filter 会把它从合成层拽回主线程，那一排就不丝滑了。 */
  transition:transform .4s var(--peek-hover-ease),filter .4s ease}
/* 内层：`home.js` 逐帧写 scale 的那一层（"越靠中间越大"）。
   圆角 / 裁切 / 底色 / 投影都在这儿，所以它们会跟着缩放一起动。
   ⚠️ `transform-origin` 是 **center**（不是 `center bottom`）—— 2026-09-15 Allen：
   「所有卡片的垂直中点处在同一高度」。原来定在**底边**，缩放时底边钉住、顶边往下缩，
   于是各卡底边对齐、**中点差 17.4px**（实测 0.86 那张 308.2 / 1.0 那张 290.8）。
   改成 center 之后所有中点都在 290.8，缩放上下对称。
   ⚠️ 别改回 `bottom`：那样"一排卡立在同一地面上"看着挺整齐，但一旦相邻两张的缩放不同，
   中点就错开，"中间那张更大"会被读成"中间那张更高"。 */
.peek-inner{position:absolute;inset:0;display:block;border-radius:16px;overflow:hidden;
  background:var(--bg-color);transform-origin:center;will-change:transform,opacity;
  box-shadow:0 2px 5px rgba(20,16,30,.13),0 14px 32px -2px rgba(20,16,30,.34);
  transition:box-shadow .4s ease}
/* ⚠️ **投影的 spread 不能再写大负数**（2026-09-12 Allen 报「阴影隔断」后改的）。
   原来第二层是 `0 16px 34px -16px`：`34/2 = 17` 的横向扩散被 `-16` 的 spread 吃掉，
   **横向只剩 1px 渐变**，于是投影变成"只有上下渐变、左右刀切"的一块矩形。
   实测（`clipboard-…-513397b0.png` 逐像素扫）：卡片正下方 `157 → 221` 平滑过渡 55px，
   但同一高度横向 `224`（面板底）在卡片左缘**一步跳到 `169` 再一路平到底**——
   那道竖直硬边就是 Allen 说的"隔断"。
   规则：**横向渐变宽度 ≈ blur/2 + spread**，想让它自然就得让这个值 ≳ 12px。
   现在 `32/2 - 2 = 14px`。改 blur 就要回头验这个数。

   ⚠️ **原来的 1px 边框已经不在这段代码里了 —— 这条是历史记录，不是现状。**
   原设计：`border:1px solid rgba(20,16,30,.14)` 压在卡片**自己的浅色底**
   （`--bg-color:#e8e8e8`）上 → 0.14 的深色压浅底正好是 `rgb(202,202,204)`，
   两张卡叠在一起时那条 1px **亮线**就是它（实测 x=520 处 `rgb(202,201,203)`）。
   当时的修法是给 `.peek-inner` 加 `background-clip:padding-box`，让边框改成压在
   **底下那张卡的图上**——叠在深色图上就看不见了，露在面板上仍留一条极淡的描边。
   **但这个修法从没落地**：`background-clip` 在仓库历史上任何一次提交里都没出现过，
   而 `border` 在"圆角 / 裁切 / 底色 / 投影搬到 `.peek-inner`"那一次一起丢了。
   完整记录见 `.workbuddy-ai/memory/shell.md` 第 36b 条。

   ✅ **2026-09-13：边框回来了，但换成了金属环**（见下面「金属边框」那一段）。
   **不是**加回 `border` —— 那会让外框尺寸变，动到那组数字。
   所以这条的历史结论只留一句有用的：**别再想着加 `border` + `background-clip`。** */
/* 重叠维持 **18%**（460 × 0.18 ≈ 83），卡距 = 460 − 83 = 377。
   ⚠️ 这个数是**手写**的，改卡宽忘了改它，重叠比就变了（卡距一变，
   `home.js` 的衰减半径也跟着变，梯度会整体变陡或变平）。 */
.peek-card+.peek-card{margin-left:-83px}
/* 居中那张（离正中最近的）抬一档，"选中"要看得出来。
   ⚠️ 投影在**内层**（见上面），所以这两条也得跟着写 `.peek-inner`——
   写在卡片上会变成一圈浮在图外面的阴影，和"白框"是同一个毛病。 */
.peek-card.is-active .peek-inner{box-shadow:0 3px 7px rgba(20,16,30,.15),0 18px 40px -2px rgba(20,16,30,.42)}
/* "选中了、正在进"的那一下：再加一圈描边。只动 box-shadow，不动 transform——
   transform 是 JS 逐帧写的（居中放大），CSS 里再改会跟它打架。 */
.peek-card.is-opening .peek-inner{box-shadow:0 0 0 2px #8e809f,0 18px 42px -2px rgba(20,16,30,.5)}

/* ============================================================================
   速览卡的**金属边框**（2026-09-13 定样式；**2026-09-14 Allen 改规则 + 调参**）。
   当前值：**环厚 1px / 流速 5s / 滚动中冻结 / 环只画在居中那张**。
   稿子：`preview/peek-card-metal.html`（带滑杆，调参用它）。

   ⚠️ **2026-09-14 的规则变更**（Allen：「以后只有在居中的有环，其他没有」）：
   以前是"**每张卡都有一圈**静态金属环，只有居中的那张会流动"，
   现在**两侧那几张完全不画环**（`display:none`），全屏只有居中那张有环、且在流动。
   连带作废两条：
     · `--peek-ring-static`（原稿那条 `linear-gradient(68deg, …)`）**没有使用点了，已删**；
     · 「hover 独占流动」（2026-09-13 晚那版）**整条撤掉** —— 它的前提是
       "别的卡也有环、只是静态"，现在别的卡根本没环，没有可"打回静态"的东西。
       悬停边卡时仍然会 `scale(1.055)`（那条在 `.peek-card:hover` 上），只是不出环。
   ⚠️ 哪天要恢复"每张都有静态环"：去掉下面的 `display:none`，并用
   `git log -S peek-ring-static` 取回那条渐变（`preview/metal-buttons.html` 里也有）。

   画法：`.peek-inner::before` 铺满卡片、给一圈 `padding` 当**环厚**，
   再用 mask 把中间挖空 —— 只剩一圈带渐变的环。

   ⚠️ **为什么不用 `border`**：卡宽 460 / 重叠 −83 / 轨道 padding / `home.js` 的 `RANGE`
   是**一组数字**（硬约定第 21 条），动外框尺寸就得四个一起改。`::before` 对现有盒子
   **零影响**，也不碰 `.peek-inner` 已有的 `background` / `box-shadow` / `overflow`。
   （`overflow:hidden` 裁的是 padding box，而 `::before` 的 `inset:0` 正好落在同一条边上，
   所以环的外缘不会被裁掉。）

   ⚠️ 环是**压在封面外缘之上**的（封面被吃掉 `--peek-ring` 那么多）。这是照原稿的：
   `preview/metal-buttons.html` 里那圈金属也在按钮**内部**（`inset:1.9%`），外面才是轮廓。

   渐变色值来自 Allen 给的原稿 `metal-buttons-flow.html` 的
   `conic-gradient(from var(--peek-angle), …)`，**色值一个没改**。 */

/* `@property` 要声明成 `<angle>` 才能插值。不声明的话浏览器把自定义属性当字符串，
   动画会在 0deg / 360deg 之间**跳变**，不是连续转。 */
@property --peek-angle { syntax: '<angle>'; initial-value: 0deg; inherits: false; }
@keyframes peek-reflect { from { --peek-angle: 0deg; } to { --peek-angle: 360deg; } }

/* ⚠️ `display:none` 而不是 `opacity:0`：不画的元素根本不进绘制列表，
   连 conic-gradient 的合成都省掉（这套渐变每帧要重画一次，是有代价的，见下面"滚动中冻结"）。 */
.peek-inner::before{
  display:none;
  content:"";position:absolute;inset:0;
  z-index:4;                          /* 压过 .peek-cover 和 .peek-info（z-index:2） */
  border-radius:inherit;
  padding:var(--peek-ring);
  pointer-events:none;
  background:conic-gradient(from var(--peek-angle),
    #bfc0cc 0deg, #9698ac 35deg, #42445e 66deg,
    #24253c 78deg, #8e746a 85deg, #ffe2a8 89deg,
    #fffff4 93deg, #e6f4ff 98deg, #96b3d5 105deg,
    #565f80 116deg, #bcbecb 144deg, #ececf1 176deg,
    #a8a9b7 208deg, #50516a 243deg, #29283e 257deg,
    #a9757d 265deg, #ffd895 270deg, #fff9e7 274deg,
    #f4fbff 279deg, #a1bedb 287deg, #676f8e 302deg,
    #bfc0cc 360deg);
  /* 中间挖空 → 只剩环：`content-box` 那块画在内容盒里，和"整块"做 xor/exclude，
     差值就是那圈 padding 的宽度。 */
  -webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);
  -webkit-mask-composite:xor;
  mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);
  mask-composite:exclude}
/* 环**只画在居中那张**上（`.is-active` = 离轨道正中最近那张，见 `home.js` 的 `paint()`）。
   全屏同一时刻**只有这一圈**，且在流动 —— 对应原稿那句"有选中的则选中的卡片边框流动"。
   ⚠️ 必须排在 `::before` 那条**之后**：`display` 同权重，靠"后来者赢"。
   ⚠️ 别顺手把 `:hover` 加回这个选择器：2026-09-14 Allen 明确"其他没有"。 */
.peek-card.is-active .peek-inner::before{
  display:block;
  animation:peek-reflect var(--peek-flow) linear infinite}
/* ⚠️ 必须排在设置 `animation` 的那条**之后**：`animation` 简写会把
   `animation-play-state` 重置成 `running`，写在前面等于没写。这里靠"同权重、后来者赢"。
   为什么冻：这套动画每帧重画一次 conic-gradient（渐变上不了合成层），而这一段滚动
   本来是纯 transform / opacity、完全跑在合成层上的 —— 不冻就把"丝滑"换掉了。
   `paused` 不产生重绘。类由 `home.js` 的 `markScrolling()` 加/去。 */
.peek.is-scrolling .peek-inner::before{animation-play-state:paused}
@media (prefers-reduced-motion: reduce){
  /* 无障碍设置优先：环**照旧画着**（conic 停在初始的 0deg 不动），
     不是退回静态金属环 —— 那条渐变 2026-09-14 已随"只有居中有环"一起删了。 */
  .peek-card.is-active .peek-inner::before{animation:none}
}

/* 点卡之后从卡片位置炸开的那层浮层（只在浏览器没有跨文档视图过渡时才用）：
   克隆封面，先摆成卡片当前的矩形，再改成平移 + 放大到盖满视口。只动 transform。
   z-index 要压过顶栏（20）和原页预览浮窗（60）。 */
/* 兜底浮层的圆角要和卡片对齐：起点是卡片那块圆角矩形，铺满视口时再收成直角
   （整屏还留圆角会露出底下的页面）。border-radius 不是合成层属性，但只有这一个元素、
   只有 620ms，代价可以接受。 */
.peek-burst{position:fixed;z-index:600;overflow:hidden;background:var(--bg-color);border-radius:16px;will-change:transform;transition:transform .62s cubic-bezier(.22,1,.36,1),border-radius .62s cubic-bezier(.22,1,.36,1)}
.peek-burst .peek-cover{position:absolute;inset:0;width:auto;height:auto;aspect-ratio:auto}
.peek-burst img{width:100%;height:100%;object-fit:cover;display:block}
/* 最后 160ms 淡到页面底色再跳。从一张照片硬切到白底页面同样生硬，
   淡一下至少让落点是"页面"而不是"另一张图"。 */
.peek-burst::after{content:"";position:absolute;inset:0;z-index:3;background:var(--bg-color);opacity:0;transition:opacity .16s ease;pointer-events:none}
.peek-burst.is-fading::after{opacity:1}

.peek-cover{position:absolute;inset:0;display:block;overflow:hidden;background:var(--bg-color)}
.peek-cover img{width:100%;height:100%;object-fit:cover;display:block}
/* 阴影蒙版：封面颜色什么都有，白字没这层读不出来。挂在 cover 上，
   兜底浮层克隆 cover 时会一起带过去。 */
.peek-cover::after{content:"";position:absolute;left:0;right:0;bottom:0;height:74%;background:linear-gradient(to top,rgba(12,12,16,.8),rgba(12,12,16,.44) 46%,rgba(12,12,16,0))}
.peek-blank{display:grid;place-items:center;width:100%;height:100%;font-size:13px;letter-spacing:.1em;color:#b6b6bd;background:linear-gradient(135deg,#f6f4f9,#eef2f7)}

/* 文字层。卡宽一路 300 → 340 → 400 → 460，字跟着放大（比盒子的比例保守一档）——
   盒子长了字不长的话，字会显得"飘"在封面里；跟着放满又会显得字在喊。
   四个字号 + 内缩（`left/right/bottom`）是**一组**，改一起改：
   `.peek-label` 10.5 / `.peek-code` 24 / `.peek-title` 16.5（副行 13.5）/ `.peek-meta` 11.5，
   内缩 17px。
   ⚠️ 加行 / 压扁卡片之后要回头验封面蒙版够不够：`.peek-cover::after` 现在是**底部 74%**，
   文字块（桌面 65px）+ 底边距 15px = 顶边落在离底 80px 处。实测：
   桌面 1.85 → 卡高 **248** / 蒙版 **184** → `fade.ratio` **0.44**；
   窄屏 1.85 → 卡高 **162** / 蒙版 **120** → **0.82**（都是 2026-09-14 量的）。
   再往上加行就要抬这个高度。 */
/* ⚠️ **用 grid 而不是顺序堆叠**（2026-09-13 Allen：「把那个 1 讲移到卡片的右下角，
   然后左边整体下移」）。三个作用一次到位：
   ① `title` 和 `meta` 是**同一行的两列**（`1fr auto`）→ meta 落在卡片右下角；
   ② 左边那块（标签 / 编号 / 课程名）于是**少了一行**，而 `.peek-info` 的 `bottom:15px`
      不动 → 整块**自然下沉**一个 meta 的高度（≈22px）。这就是"左边整体下移"，
      **不要再手动加 `padding-bottom` 去凑**；
   ③ 标题列是 `1fr` → 它**自动让出 meta 的宽度**，不会跑到 meta 底下。
      换成绝对定位（`position:absolute;right:0;bottom:0`）就得给标题留一笔固定右内边距，
      留多了浪费、留少了撞上（meta 宽度是变的："1 讲" 26px，"Notes · 1 份 HTML" 100px）。
   `align-items:end` 让 meta 与标题**底对齐**（不是居中）。
   项目卡没有 `.peek-code`，"code code" 那行是空的、高度 0，不用单独处理。 */
/* ---- 卡片文字字体栈：2026-09-15 Allen 选的「方案 D」（Helvetica Neue 优先）----
   一处定义、两处用：速览卡 `.peek-info` + 「我的课程」卡 `.hcc-overlay`。
   Allen 的原话是「采用字体方案 D，替换首页的所有[卡片]」—— 首页就这两处卡片文字，
   字体栈只留一个来源，别在两边各抄一份。
   ⚠️ **实测这台机器上 Helvetica Neue 只有 400 / 500 / Bold 三档**：
   `font-weight` 600 / 650 / 700 / 800 量出来宽度**完全一样**（83.05px），
   500 = 82.18、400 = 81.27（量法：临时 span + `getBoundingClientRect`）。
   所以方案里的 `font-weight:650` 实际落在 **Bold** 上 —— 和它要替换掉的 800 长得**一模一样**。
   要"细一档"得写 **500**（Medium），那才是真的比原来轻。
   ⚠️ 顺带：Helvetica Neue 比 Satoshi 窄（同串 81.27 vs 89.52），换过来之后
   编号那行的宽度会**缩**一截，`.peek-code` 的字距 / 字号不用跟着动。 */
:root{--font-card:"Helvetica Neue",Helvetica,"PingFang SC",Arial,"Microsoft YaHei",sans-serif}

/* `left` 17 → **22 → 26px**（2026-09-15 Allen 两轮：「稍微增大课程，课程号，课程名和卡片左边界的距离」，
   然后又「再稍微调大一点」）。⚠️ **只加左边**，右边还是 17px：课程卡的讲义数目已经挪到编号右边了，
   右下角那条只服务项目卡，右边内缩不用跟着变。
   ⚠️ **分类标签（`.peek-label`，就是「课程」/「Playground」）2026-09-15 已整个去掉** ——
   Allen：「『课程/project』这个分类就可以去掉了」。所以 grid 少一行，
   `grid-template-areas` 从三行变两行（课程卡那条另见下面）。
   ⚠️ 别为了补回那点高度去动 `bottom` —— 文字块**变矮了**是去掉标签的必然结果，
   蒙版余量实测反而更宽松（1440 下 91.5 → 更多）。 */
.peek-info{position:absolute;left:26px;right:17px;bottom:15px;z-index:2;color:#fff;
  font-family:var(--font-card);
  display:grid;grid-template-columns:1fr auto;column-gap:12px;align-items:end;
  grid-template-areas:"code code" "title meta"}
/* ⚠️ **`.peek-label` 已删**（2026-09-15 Allen：「『课程/project』这个分类就可以去掉了」）。
   原来那行是 `「课程」/「Playground」` 的小字大写标签，字号 10.5 / 字距 .14em / 700。
   `views._peek_items()` 里**还在生成 `label` 字段**（`label: "课程"` / `label: "Playground"`），
   模板已经不渲染了 —— 留着不算错，但下次动 `_peek_items` 时可以顺手删掉那个键。 */
/* 课程号是**主角**（2026-09-13 Allen：「主要需要凸显课程号，如 EE6402，课程名放在课程号的下面」）。
   编号是短 token（EE6402 六个字符），给到 24px 也只占 426px 文字区的一小截，敢往大里放。
   `tabular-nums` 让每张卡的数字等宽——一排卡扫下来，编号的位数是对齐的。
   ⚠️ **2026-09-15 起「我的课程」卡（`.hcc-*`）也统一成这一套了** ——
   以前那边是反着的（15px 小前缀 + 18px 大标题），Allen 说「把课程页的课程卡片做成跟 Preview 一样」，
   那条旧约定已作废。改这里时两边一起看。 */
.peek-code{grid-area:code;display:block;margin-top:4px;font-size:24px;font-weight:650;line-height:1.05;letter-spacing:-.01em;font-variant-numeric:tabular-nums;text-shadow:0 1px 3px rgba(0,0,0,.5)}
/* ⚠️ `font-weight` **800 → 650**（2026-09-15 Allen 的「选定方案」）。
   看着是"细了一档"，其实**这一版什么都没变** —— Helvetica Neue 没有 600 这一档，
   650 和 800 落在同一个 Bold 面上（实测宽度都是 83.05px，见 `--font-card` 的注释）。
   要真细一档就写 **500**（Medium，82.18px）。这里先照方案原样写 650。 */
/* `min-width:0`：grid 项默认 `min-width:auto`（= 最长单词的宽度），不加的话标题撑住不肯缩，
   长 meta 会把这一行顶宽、把卡片撑出 `.peek-info` 的右边界。 */
.peek-title{grid-area:title;min-width:0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;margin-top:5px;font-size:16.5px;font-weight:700;line-height:1.32;letter-spacing:-.015em;overflow-wrap:anywhere;text-shadow:0 1px 3px rgba(0,0,0,.5)}
/* 跟在编号后面的课程名 = 副行：字号降一档、字重降到 600，把视觉重心整个让给编号。
   项目卡没有 `.peek-code`，`.peek-title` 就保持上面那档 16.5px 大字当主行——
   用**相邻兄弟选择器**区分主/副行，不必给元素再挂一个"我是副行"的类
   （`{% if %}` 只增删元素、不留文本节点，`+` 不会被空行打断）。 */
.peek-code+.peek-title{margin-top:3px;font-size:13.5px;font-weight:600;line-height:1.3;letter-spacing:0;opacity:.94}
/* 卡片右下角（**只有项目卡走这条**，课程卡的 meta 已经挪到编号左边，见下面那条）。
   `margin-top` 必须清零——grid 的 `align-items:end` 对齐的是**外边框**，
   留着原来堆叠时那 5px 上边距会把整块往上顶 5px，和标题对不齐。
   `white-space:nowrap`：让它换行的话这一行会变高，左边的标题跟着被顶上去。
   `opacity` **.85 → .72**（2026-09-15 Allen：「颜色略淡」）。 */
.peek-meta{grid-area:meta;margin-top:0;white-space:nowrap;font-size:11.5px;opacity:.72;text-shadow:0 1px 2px rgba(0,0,0,.45)}

/* ---- 课程卡：讲义数目挪到编号**右边**（2026-09-15 Allen）----
   原布局是"编号独占一行、讲义数目在右下角和课程名同行"（09-13 定的那版，现在推翻）。
   改成 `[EE6402] [1 讲]` 同一行，紧挨着，底对齐。
   ⚠️ 列宽是 **`auto 1fr`**（编号 auto、讲义 1fr），不是 `1fr auto`：
   后者会把讲义数目推到**卡片最右边**，和编号之间空一大截 —— 那是"在右边"，
   不是"在编号右边"。`auto` 让编号那列正好等于编号自己的宽度，讲义就贴着它起头。
   顺带：编号的左缘 = 卡片左内缩，**不随讲义数目的宽度变**（比放左边那版更稳）。
   ⚠️ **`align-self:baseline`，不能靠容器的 `align-items:end`**：
   编号 `line-height:1.05`（24px 字 → 盒子 25.2px）、讲义数目 11.5px（盒子 ~13.2px），
   两者的**盒子下沿**对不上 —— 基线差 4.2px，`end` 对的是盒子底，
   看着就是讲义数目"掉下去"了。基线对齐才是 Allen 说的"字体底部对齐"。
   ⚠️ 两个元素给**一样的 `margin-top`**（4px，原来编号那条的值）：基线对齐会算上外边距，
   只给编号加的话两者会差 4px。
   ⚠️ **只改课程卡**（`[data-kind="course"]`）：项目卡的 meta 是"分类 · N 份 HTML"，
   不是"讲义数目"，而且它**没有编号** —— 挪上去会凭空多出一行、把文字块顶高、
   撞进封面蒙版（`peek_geom.py` 的 `fade.ratio` 会涨）。项目卡保持原样。 */
.peek-card[data-kind="course"] .peek-info{
  grid-template-columns:auto 1fr;
  grid-template-areas:"code meta" "title title"}
.peek-card[data-kind="course"] .peek-code,
.peek-card[data-kind="course"] .peek-meta{align-self:baseline;margin-top:4px}

/* ---- 课程卡：标题压在封面上 ----
   卡片下方原来那个「课程内容」展开按钮删掉了，讲次和资料回到课程页去看——
   首页这一块只回答"这门课进度怎么样"，不负责导航。 */
/* ⚠️ 列宽下限 **300 → 360**（2026-09-14 Allen：「再放大一下他们的尺寸」）。
   auto-fill 的放大靠"少放一列"生效：常见桌面宽度下从 4 列掉到 3 列，
   卡宽 ≈ 344 → 466，和速览卡的 460 正好一个量级。 */
.course-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(360px,1fr));gap:26px 22px}
.home-course-card{position:relative;min-width:0}
/* ⚠️ 比例 **16/10 → 1.85**（2026-09-14 Allen：「把课程页的这些卡面尺寸的宽高比
   也调成跟 preview 课程卡片一样的」）。**1.85 是速览卡的 `--peek-ratio`，两处必须同值**——
   它们共用同一条 `view-transition-name:peek-morph`（见下面「跨文档视图过渡」那节），
   比例不一样的话点卡那一瞬封面会先"跳"一下再长开。
   ⚠️ 这里**写死数字、不引 `--peek-ratio`**：那个变量定义在 `.peek` 上，
   `.hcc-cover` 不在 `.peek` 的子树里，`var()` 取不到（会退化成 `auto`）。
   要改就两处一起改（`.peek` 的 `--peek-ratio` + 这里）。
   ⚠️ 卡高会跟着掉（300 宽的列上 186 → 162），**底部的文字蒙版要重新验**：
   蒙版是 `::after` 的 78%，实测 1.85 下 overlay 顶边仍在蒙版内（见下面 `::after` 那条）。 */
.hcc-cover{display:block;position:relative;overflow:hidden;border-radius:18px;background:var(--bg-color);aspect-ratio:1.85;border:1px solid rgba(28,28,30,.06);isolation:isolate}
.hcc-cover img{width:100%;height:100%;object-fit:cover;display:block;transition:transform .62s cubic-bezier(.22,1,.36,1)}
.home-course-card:hover .hcc-cover img{transform:scale(1.04)}
.hcc-cover:focus-visible{outline:2px solid #8e809f;outline-offset:3px}
/* 阴影蒙版：从透明渐到黑。封面颜色什么都有（截图里一张是全白讲义的
   首页、一张是紫底标题页），没有这层，白字在浅色封面上直接读不出来。
   进度也搬进封面之后，要压住的内容多了两行，蒙版从 64% 提到 78%。 */
.hcc-cover::after{content:"";position:absolute;left:0;right:0;bottom:0;height:78%;z-index:1;pointer-events:none;background:linear-gradient(to top,rgba(12,12,16,.82),rgba(12,12,16,.46) 46%,rgba(12,12,16,0))}
/* 字体栈跟着速览卡走（`--font-card`，方案 D）—— 「首页的所有卡片文字用同一套字」。 */
.hcc-overlay{position:absolute;left:16px;right:16px;bottom:14px;z-index:2;color:#fff;
  font-family:var(--font-card)}
/* ---- 2026-09-15：整块文字**对齐速览卡**（Allen：「把课程页的课程卡片做成跟 Preview 一样的」）----
   以前是**反着**的：`.hcc-code` 是 15px / `.82` 的小前缀，`.hcc-title` 是 18px / 700 的大标题。
   现在和 `.peek-*` 同一套 —— 编号是主角、课程名是副行、讲义数目挨着编号。
   ⚠️ 两处**必须同改**（`.peek-code` / `.hcc-code` 等）：它们是"同一张卡的两个尺寸"，
   只改一边就会出现"首页的卡和课程页的卡长得不一样"。
   ⚠️ 编号从 15px 跳到 **24px**，文字块整体变高了 ~10px。`.hcc-cover::after` 是底部 78% 的蒙版，
   改完要复核 `overlay 顶边仍在蒙版内`（探针 `hcc_geom.py` 里有这条）。 */
.hcc-head{display:flex;align-items:baseline;gap:10px}
.hcc-code{display:block;font-size:24px;font-weight:650;line-height:1.05;letter-spacing:-.01em;
  font-variant-numeric:tabular-nums;text-shadow:0 1px 3px rgba(0,0,0,.5)}
/* 讲义数目：挨着编号右边、和它**基线对齐**（靠 `.hcc-head` 的 `align-items:baseline`）。
   颜色比编号淡一档 —— 和速览卡的 `.peek-meta` 同一个口径（`.72`）。 */
.hcc-count{font-size:11.5px;opacity:.72;white-space:nowrap;text-shadow:0 1px 2px rgba(0,0,0,.45)}
/* 课程名 = 副行：字号降一档、字重降到 600，把视觉重心整个让给编号
   （数值和速览卡的 `.peek-code+.peek-title` 完全一致）。
   ⚠️ 两行 clamp 保留：课程名可能很长（`REAL-TIME DSP DESIGN AND APPLICATIONS`）。 */
.hcc-title{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;
  margin-top:3px;font-size:13.5px;font-weight:600;line-height:1.3;letter-spacing:0;
  overflow-wrap:anywhere;opacity:.94;text-shadow:0 1px 3px rgba(0,0,0,.5)}
/* 没有封面时底下是浅色占位，白字反过来读不出来：蒙版撤掉、字改深色 */
.hcc-cover.is-blank{background:linear-gradient(135deg,#f6f4f9,#eef2f7)}
.hcc-cover.is-blank::after{display:none}
.hcc-cover.is-blank .hcc-overlay{color:#4a4a52}
.hcc-cover.is-blank .hcc-code{color:#9985ac;opacity:1;text-shadow:none}
.hcc-cover.is-blank .hcc-count{text-shadow:none}
.hcc-cover.is-blank .hcc-title{text-shadow:none}
.hcc-tools{position:absolute;top:10px;right:10px;z-index:3;display:flex;gap:5px;opacity:0;transform:translateY(-4px);transition:opacity .2s ease,transform .2s ease}
.home-course-card:hover .hcc-tools,.home-course-card:focus-within .hcc-tools{opacity:1;transform:translateY(0)}
.hcc-tools button{display:grid;place-items:center;width:30px;height:30px;border:0;border-radius:8px;background:rgba(255,255,255,.92);color:#4a4a50;cursor:pointer;box-shadow:0 1px 3px rgba(0,0,0,.14)}
.hcc-tools button:hover{background:#fff;color:#1c1c1e}
.hcc-tools button:focus-visible{outline:2px solid #8e809f;outline-offset:2px}
.hcc-tools .ico{width:14px;height:14px}
/* 进度整块搬进封面里了，所以这几个都换成"压在图上"的配色：
   字变白带投影、轨道变浅。浅色/深色封面都靠上面那层蒙版兜住。
   2026-09-15 Allen：「进度条和在读以及标注都放到同一行，也就是说进度条不在独占一整行」——
   这一行现在是 **flex 三件套**：`[进度 x/y · 在读 z]` + `[进度条]` + `[M 条标注]`，
   进度条 `flex:1` 吃掉中间那段空白。
   ⚠️ `justify-content:space-between` **必须去掉**：留着的话进度条那个 `flex:1` 会跟它打架
   （space-between 先把三项推开，再让中间那个撑满 —— 结果是两段文字被挤到卡片外）。
   ⚠️ 两边那两段文字要 `flex:none` + `nowrap`：不给的话它们会被进度条挤着换行，
   一行变两行，"同一行"就白改了。 */
.hcc-meta{display:flex;align-items:center;gap:10px;margin-top:11px;font-size:11px;opacity:.86;text-shadow:0 1px 2px rgba(0,0,0,.45)}
.hcc-meta>span:first-child,.hcc-meta>span:last-child{flex:none;white-space:nowrap}
.hcc-progress{position:relative;flex:1 1 auto;min-width:36px;display:block;height:4px;margin-top:0;border-radius:999px;background:rgba(255,255,255,.26);overflow:hidden}
.hcc-progress span{position:absolute;top:0;bottom:0;left:0;border-radius:999px}
.hcc-read{background:#EFD3DE}
.hcc-done{background:#D890B2}
/* 没有封面时底下是浅色占位、蒙版也撤了，白轨道会看不见——换回深色轨道和深色字 */
.hcc-cover.is-blank .hcc-meta{opacity:1;text-shadow:none}
.hcc-cover.is-blank .hcc-progress{background:rgba(28,28,30,.12)}

/* ---------------------------------------------------------------- 编辑资料对话框
   顶栏那个 dialog。背景遮罩用 ::backdrop 写在这儿，而不是靠 Tailwind 的
   `backdrop:bg-black/20` —— 那个类没被任何模板用到过，早被 purge 掉了。 */
#me-dialog::backdrop{background:rgba(0,0,0,.22)}
#me-dialog{box-shadow:0 2px 8px rgba(28,28,30,.08),0 40px 90px -40px rgba(28,28,30,.55)}

/* ---------------------------------------------------------------- 习题库
   按课程 / 项目分组统计习题与错题。刻意做成"表格"而不是卡片墙：
   这一页是拿来核对的（哪一讲题多、哪一讲错得多），不是拿来浏览的。 */
.ex-lead{margin:0 0 26px;font-size:13px;color:var(--text-muted);line-height:1.7}
.ex-lead b{color:var(--text-main);font-weight:700}
.ex-section{margin-bottom:34px}
.ex-section-head{font-size:13px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--text-muted);margin:0 0 12px}
.ex-card{border:1px solid rgba(28,28,30,.08);background:hsla(0,0%,100%,.72);border-radius:20px;padding:18px 20px;margin-bottom:12px}
.ex-card-head{display:flex;align-items:flex-start;justify-content:space-between;gap:18px;flex-wrap:wrap}
.ex-code{font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--text-muted)}
.ex-title{font-size:16px;font-weight:700;margin:3px 0 0}
.ex-stats{display:flex;gap:14px;flex-wrap:wrap;font-size:12px;color:var(--text-muted);white-space:nowrap}
.ex-stats b{color:var(--text-main);font-weight:700}
.ex-stats .is-wrong{color:#C0506E}
.ex-stats .is-solved{color:#4E8C63}
.ex-headrow,.ex-row{display:grid;grid-template-columns:minmax(0,1fr) 62px 62px;gap:10px;align-items:center}
.ex-headrow{margin:14px 0 4px;padding-bottom:6px;border-bottom:1px solid rgba(28,28,30,.08);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--text-muted)}
.ex-headrow span:not(:first-child),.ex-row-num{text-align:right}
.ex-row{padding:7px 0;font-size:13px;border-bottom:1px solid rgba(28,28,30,.045)}
.ex-row:last-child{border-bottom:0}
.ex-row-title{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ex-row-num{color:var(--text-muted);font-variant-numeric:tabular-nums}
.ex-row-num.is-wrong{color:#C0506E;font-weight:600}
/* ---- 习题集：分堆卡片 ----
   首页那套玻璃卡片的语言（半透明白 + 亮边 + 软投影 + 悬停抬起 3px），
   和课程卡、Preview 卡是同一族——习题集不是另一个站点。 */
.ex-decks{display:grid;grid-template-columns:repeat(auto-fill,minmax(258px,1fr));gap:12px}
.ex-deck{display:flex;flex-direction:column;border:1px solid hsla(0,0%,100%,.9);background:hsla(0,0%,100%,.72);border-radius:20px;padding:16px 18px 15px;text-decoration:none;color:inherit;box-shadow:0 1px 2px rgba(28,28,30,.04),0 10px 26px -18px rgba(28,28,30,.3);transition:transform .22s ease,box-shadow .22s ease,background-color .22s ease,border-color .22s ease}
.ex-deck:hover{transform:translateY(-3px);background:#fff;box-shadow:0 2px 4px rgba(28,28,30,.05),0 18px 36px -18px rgba(28,28,30,.26)}
.ex-deck:active{transform:translateY(0)}
.ex-deck-top{display:flex;align-items:center;justify-content:space-between;gap:10px}
.ex-deck-arrow{color:#C3C8CD;font-size:15px;line-height:1;transition:color .2s ease,transform .2s ease}
.ex-deck:hover .ex-deck-arrow{color:var(--text-main);transform:translateX(2px)}
.ex-deck .ex-title{margin:2px 0 0}
.ex-deck .ex-stats{margin-top:10px;gap:12px}
.ex-deck-bar{display:block;height:4px;border-radius:999px;background:#ECEAE6;overflow:hidden;margin-top:12px}
.ex-deck-bar>i{display:block;height:100%;width:0;border-radius:999px;background:#D890B2;transition:width .3s cubic-bezier(.32,.72,0,1)}
.ex-deck-foot{display:block;margin-top:7px;font-size:11px;color:var(--text-muted);font-variant-numeric:tabular-nums}
/* ---- 习题集：里层每一讲一行 ---- */
.ex-lines{display:flex;flex-direction:column;gap:10px}
.ex-line{border:1px solid hsla(0,0%,100%,.9);background:hsla(0,0%,100%,.72);border-radius:18px;padding:15px 18px 14px;box-shadow:0 1px 2px rgba(28,28,30,.04),0 10px 26px -20px rgba(28,28,30,.28);transition:background-color .2s ease,border-color .2s ease}
.ex-line:hover{background:hsla(0,0%,100%,.92)}
.ex-line-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;flex-wrap:wrap}
.ex-line-title{font-size:15px;font-weight:700;line-height:1.4;min-width:0}
.ex-line-bar{height:4px;border-radius:999px;background:#ECEAE6;overflow:hidden;margin-top:11px}
.ex-line-bar>i{display:block;height:100%;width:0;border-radius:999px;background:#D890B2;transition:width .3s cubic-bezier(.32,.72,0,1)}
.ex-line-foot{display:flex;align-items:center;justify-content:space-between;gap:14px;flex-wrap:wrap;margin-top:9px}
.ex-line-progress{font-size:11.5px;color:var(--text-muted);font-variant-numeric:tabular-nums}
.ex-line-actions{display:flex;align-items:center;gap:8px}
.ex-go{display:inline-flex;align-items:center;gap:6px;padding:6px 13px;border-radius:10px;background:#1C1C1E;color:#fff;font-size:12.5px;font-weight:600;text-decoration:none;transition:transform .2s ease,box-shadow .2s ease,background-color .2s ease}
.ex-go:hover{transform:translateY(-2px);box-shadow:0 8px 18px -10px rgba(28,28,30,.6)}
.ex-go:active{transform:translateY(0)}
.ex-go.is-off{background:transparent;border:1px dashed rgba(28,28,30,.16);color:var(--text-muted);font-weight:500}
.ex-go .ico{font-size:14px}
.ex-link{font-size:12.5px;color:var(--text-muted);text-decoration:none;padding:6px 10px;border-radius:10px;transition:color .15s,background-color .15s}
.ex-link:hover{color:var(--text-main);background:rgba(28,28,30,.05)}
.ex-back{display:inline-flex;align-items:center;gap:4px;font-size:12px;color:var(--text-muted);text-decoration:none;transition:color .15s}
.ex-back:hover{color:var(--text-main)}
.ex-lead-tight{margin:0;font-size:13px}
@media(max-width:640px){
  .ex-card{padding:15px 16px}
  .ex-headrow,.ex-row{grid-template-columns:minmax(0,1fr) 44px 44px;gap:6px}
  .ex-stats{gap:10px}
  .ex-decks{grid-template-columns:1fr}
  .ex-deck{padding:14px 15px}
  .ex-line{padding:13px 15px 12px}
  .ex-line-foot{align-items:flex-start}
  .ex-go{padding:6px 11px}
}

/* ---------------------------------------------------------------- 顶栏（悬浮胶囊）
   一条不贴边的圆角胶囊：左边站标、中间导航、右边头像。样式参考 Apple 的导航条。

   三栏用 grid 的 `1fr auto 1fr`。**不能用 flex + justify-between**：那样按两侧内容宽度
   分配空间，右边（头像 + 今天已学）比左边（一个站标）宽，中间那组就被挤得偏左。
   两个 1fr 平分剩余，中间才是相对整条居中。

   **`position:fixed`：胶囊浮在内容之上**，页面滚动时内容从它底下穿过去。
   这样"下滑收起"就只是把胶囊滑出视口，**内容一动不动**（纯 transform，不碰布局）；
   要是留在文档流里收起，内容会跟着上下跳 68px，滚一下抽一下。
   代价是每个滚动容器都要自己让出 68px，见下面那组 `padding-top`。

   胶囊本身还是"假毛玻璃"那一套（半透明白 + 亮边 + 软阴影），没有 backdrop-filter——
   它压在内容上、每页都在，开合成层不划算。 */
/* ⚠️ 这一条现在是**老版单胶囊**的形态，只有 project 页在用（`variant='classic'`）。
   三药丸版见下面 `#site-nav:not(.is-classic)`，两版共用 `_topbar.html` 那一份 markup。 */
#site-nav{position:fixed;top:0;left:0;right:0;z-index:30;
  margin:12px 20px 0;padding:8px 18px;min-height:56px;
  display:grid;grid-template-columns:1fr auto 1fr;align-items:center;
  border-radius:20px;
  background:hsla(0,0%,100%,.86);
  border:1px solid rgba(28,28,30,.05);
  box-shadow:0 1px 2px rgba(28,28,30,.05),0 14px 34px -18px rgba(28,28,30,.22)}

/* ---------------------------------------------------------------- 三药丸形态
   2026-09-15 Allen：「先把网站的首页的 menubar 换成这个样式（参考图），加上左右的两个
   小药丸按钮，做得扁一点、稍微长一点，左侧预留一个 3:1 的 logo 样式空间。」

   外壳**整条透明**，实心的是里面三枚独立药丸：
     左格  预留药丸（虚线描边，还没想好放什么）
     中格  主药丸 = 3:1 站标槽 + 首页 / 课程 / 习题集 / Projects
     右格  头像 + 用户名 ｜ 今日已学
   所以 `#site-nav` 自己的底、边、影、内边距全部归零，交给 `.nav-pill`。

   ⚠️ **外壳必须 `pointer-events:none`**：它现在是一整条透明的 fixed 元素（还横跨整幅视口），
   不关掉就会在内容上挖出一条看不见的点击禁区 —— 顶部 60px 之内所有链接都点不动。
   三枚药丸各自 `auto` 收回来（`.nav-pill` 那条）。

   ⚠️ 用 `:not(.is-classic)` 划范围，**不是**给 classic 另写一份回滚：这样老版胶囊那几条
   原封不动（含 `playground.css` 深色主题那组 `[data-ff-theme="dark"] … #site-nav` ——
   它们特异性 (0,2,1)，比 `#site-nav.is-classic` 的 (1,1,0) 低，写回滚会被反过来压死）。

   ⚠️ `position` / `display` / `grid-template-columns` **留在基础那条里不重复声明**：
   阅读页有一条 `#reader-nav #site-nav{position:relative}`，特异性 (2,0,0)，
   靠"基础那条别来抢"才盖得住。 */
#site-nav:not(.is-classic){
  padding:0;min-height:0;border-radius:0;
  background:transparent;border:0;box-shadow:none;
  pointer-events:none;
  /* ⚠️ 两侧那两列必须写成 `minmax(0, 1fr)`，**不能写裸的 `1fr`**：
     `1fr` 展开是 `minmax(auto, 1fr)`，而那个 `auto` 下限会取内容的 min-content ——
     于是列宽永远压不到内容以下，整条 grid 直接比容器宽、朝右边溢出
     （480px 实测：三列 90 + 335.5 + 50 = 475.5，容器只有 460，右药丸跑到 509.5）。
     写 `minmax(0, 1fr)` 下限就是 0，列能被压窄，`max-width:100%` 那层才接得住。
     ⚠️ 两列**必须等宽**（都用 1fr）：不等宽中格就不居中了。
     ⚠️ `column-gap` 是第二道保险 —— 万一哪一档算漏了，药丸之间也还剩 12px 的缝。 */
  grid-template-columns:minmax(0,1fr) auto minmax(0,1fr);
  column-gap:12px;
}

/* ---- 三枚药丸本体 ----
   几何（2026-09-15 定，一处改全都跟着动）：
     药丸高 **48px**（原单胶囊 56 —— Allen 要的"扁一点"），圆角 999px 跟着高度走；
     让位因此从 68px 收到 **60px**（12 + 48），见文件末尾那组 `padding-top`。
     "长一点"落在两处：导航项内边距 15 → **20px**（4 项共 +40px），
     外加中格左边那枚 3:1 站标槽（40px 方标 → 108px 宽，+68px）。

   底色用**纯白**，不是原来那层 `.86` 的半透明：页面底色统一成 `#FAFAFA` 之后，
   `.86` 的白算出来是 `#FCFCFC`，和底只差 2 级 —— 药丸会糊进底色里，只剩一圈描边。
   白 + 1px 亮边 + 软影才是参考图那个观感。
   ⚠️ 纯白 = **这一版没有"假毛玻璃"了**（内容从底下滚过去会被完全挡住）。这是有意的取舍：
   药丸小、彼此之间有空隙，本来就读得出是"浮在上面的按钮"，不需要靠半透明来证明它浮着。
   老版单胶囊（project 页）仍是 `.86` 半透明，没动。

   ⚠️⚠️ **这一整段（到 `.nav-pill-sep` 为止）必须带 `#site-nav:not(.is-classic)` 前缀。**
   模板是**一份 markup 两版形态**：`variant='classic'` 时右格那两枚包裹层上**照样挂着**
   `.nav-pill` / `.nav-pill--me` 这两个类名（左格 `.nav-back` 和中格的 `.nav-logo-slot`
   才是 Jinja 条件渲染的）。不划范围的话，project 页会在老胶囊**里面**再长出两枚
   48px 白药丸 —— 实测就是"Projects 首页量到 2 枚 `.nav-pill`"，页面直接散架。 */
#site-nav:not(.is-classic) .nav-pill{
  height:48px;border-radius:999px;background:#fff;border:1px solid rgba(28,28,30,.06);
  box-shadow:0 1px 2px rgba(28,28,30,.05),0 14px 30px -18px rgba(28,28,30,.24);
  pointer-events:auto}
/* `display:flex;align-items:center` **不带前缀**：两版都要。
   classic 版这里只是两枚"透明的横排容器"，药丸那层皮全在上面前缀里。 */
.nav-pill{display:flex;align-items:center;flex:none}

/* 药丸自己的 hover：抬起 1px + 投影加深 —— "这三枚是被碰得到的实物"。
   2026-09-15 Allen 要的 hover 交互感，分两层：**药丸抬起**（这一条）+
   **导航项之间滑片**（见下面的 `.nav-glider`）。只做后者的话，鼠标扫过整条顶栏时
   左右两枚毫无反应，会显得只有中格是活的。
   ⚠️ 只抬 **1px**，不是站内那套 2–3px：顶栏是 fixed 的 chrome，抬多了整条像在抖。
   ⚠️ 这三条原来都带 `:not(.nav-slot)`，把左格那枚**空占位**排除在外（当时它不是可点的东西，
   给反馈等于骗人"这儿能点"）。**2026-09-15 左格定成返回键，排除撤掉了** ——
   三枚药丸现在都是真按钮，反馈要一致。 */
#site-nav:not(.is-classic) .nav-pill{
  transition:transform .22s cubic-bezier(.22,1,.36,1),box-shadow .22s ease}
#site-nav:not(.is-classic) .nav-pill:hover{
  transform:translateY(-1px);
  box-shadow:0 1px 2px rgba(28,28,30,.05),0 18px 34px -18px rgba(28,28,30,.32)}
#site-nav:not(.is-classic) .nav-pill:active{
  transform:translateY(0) scale(.995)}

/* 左格：**返回键**（2026-09-15 Allen：「把左上角的预留 icon 做成返回键 参考这个设计和动画」）。
   常态就是一枚普通药丸（白底 + 软影，和另外两枚一样），只是里面装的是"← 返回"：
     · 文字 / 箭头色 = **未选中导航项那一档**（`--text-muted`，Allen 明确要求）；
     · hover **底色变灰**（就是导航项 hover 那层 `.045`）+ 文字转墨色；
     · hover 箭头**向左**滑 5px —— 参考稿是 `translateX(5px)`（箭头在右边、向右滑），
       返回要反着来，照抄参考值就成了"前进"；
     · 按下整枚 `scale(.95)`（参考稿的 `button:active`）。
   ⚠️ `:active` 那条特异性要压过上面 `.nav-pill:active`（(1,2,0)）——
   多带一个 `.nav-back` 就是 (1,3,0)，而且写在它后面，两头都占。

   ⚠️ 只存在于 pill 版（classic 的左格是站标）。 */
/* ⚠️ **每一枚药丸都要 `max-width:100%`**（这是"永不重叠"的那道结构保险，不是装饰）：
   grid 是 `1fr auto 1fr`，中格占 `auto`、两侧各占一个 `1fr`。中格一宽，两侧的 1fr 就被压到
   只剩几十像素 —— 而药丸是 `flex:none`，**不会被列宽裁掉，只会朝相邻那格身上长过去**。
   800px 实测：中格右缘 619.7 / 右格左缘 565，压了 55px。
   有了 `max-width:100%`，药丸最坏也只是被压窄，里面的用户名走省略号（见 `.nav-me-name`），
   结构上不可能再叠在一起。 */
#site-nav:not(.is-classic) .nav-back{padding:0 18px;gap:7px;max-width:100%;
  white-space:nowrap;
  font-size:13px;font-weight:500;color:var(--text-muted);
  text-decoration:none;
  transition:transform .22s cubic-bezier(.22,1,.36,1),box-shadow .22s ease,
             background-color .18s ease,color .18s ease}
#site-nav:not(.is-classic) .nav-back .ico{width:15px;height:15px;
  transition:transform .3s ease-in-out}
#site-nav:not(.is-classic) .nav-back:hover{background:rgba(17,17,19,.045);color:var(--text-main)}
#site-nav:not(.is-classic) .nav-back:hover .ico{transform:translateX(-5px)}
#site-nav:not(.is-classic) .nav-back:active{transform:scale(.95)}
#site-nav:not(.is-classic) .nav-back:focus-visible{outline:2px solid #8e809f;outline-offset:2px}

/* 中格：主导航。内边距 6px → 里面每件正好 36px 高（48 − 6×2），和右药丸的内件同高。 */
#site-nav:not(.is-classic) .nav-pill--main{padding:6px;gap:2px;position:relative}
/* classic 版：中格没有药丸皮，四项直接排在老胶囊中间，间距回到原来那个 30px
   （以前 `.site-nav-center{gap:30px}` 管的就是这几个链接，现在中间多插了一层 `.nav-pill--main`，
   那条 gap 落到了单孩子容器上，等于没有 —— 必须在这儿补回来，否则四项会挤成一坨）。 */
#site-nav.is-classic .nav-pill--main{gap:30px}

/* ---------------------------------------------------------------- 滑片（hover 交互）
   2026-09-15 Allen：「给这个 menubar 加一点 hover 时的交互感。」
   **不另造一套语言** —— 讲义标题条那枚药丸（`.seg > .glider`）早就是"浅灰滑片跟着鼠标走"，
   顶栏复用它，全站一套手感，也不会两个地方两种脾气。
   行为（`js/topbar.js` 驱动）：
     · 鼠标移到哪一项，滑片就滑到哪一项；
     · 鼠标离开中格 → 滑片回到**当前页**那一项（静止态也是它，等于"我在哪一页"的指示）；
     · Tab 进来（`:focus-visible`）也跟着走 —— 不然键盘用户看得到焦点、看不到"我在哪一格"。

   ⚠️ 和 `.seg` 那枚的**唯一区别**：那边格子宽度每帧都在变（标签正在展开），
   CSS transition 每次重设目标都会重新计时、越拖越慢，所以必须 JS 弹簧逐帧追；
   这里四项宽度是**死的**，CSS transition 就是对工具，还省一个 rAF 循环。
   曲线用同一根 `--sp-C`（ζ=0.78 的弹簧采样），手感因此和标题条那枚对齐。
   （`--sp-C` 定义在本文件靠后的 `:root` 里。CSS 变量按**使用时机**解析，
   和定义先后无关 —— 别为了"看着顺"把它挪上来。）

   ⚠️ `pointer-events:none` **不能删**：滑片铺满整格、压在链接上，不关掉会把
   点击和 hover 一起吞了（鼠标永远停在滑片上，`mouseover` 也找不到链接）。
   ⚠️ `top` / `height` **不在 transition 列表里**：由 JS 按目标项的实际几何写死，
   四项等高，所以永远不跳；写进 transition 反而会多出一次无意义的动画。 */
#site-nav:not(.is-classic) .nav-glider{
  position:absolute;top:6px;left:0;height:36px;width:0;
  border-radius:999px;background:rgba(17,17,19,.075);
  transform:translateX(0);opacity:0;pointer-events:none;
  transition:transform .3s var(--sp-C),width .3s var(--sp-C),opacity .16s ease;
  will-change:transform,width}
/* 链接要抬到滑片**上面**：滑片是 absolute、链接是 static，不抬的话浅灰会盖住文字
   （肉眼就是"这一项的字发灰"）。 */
#site-nav:not(.is-classic) .top-nav-link{position:relative;z-index:1}

/* 站标（CourseLah）。2026-09-15 Allen：「现在把他替换到 menubar 上面」——
   稿子是 `preview/home-menubar-courselah-final.html`，这一组照搬那版几何与缓动。

   形态：起始态一枚 **36×36 的标记**（黑斜方形 + 白 C），hover / 键盘聚焦时标记缩没、
   长字标从 0 展到 96。两段同时走，**总宽 36 → 96 一路只增不减**，不会先挤后松。
   驱动在 `js/brand.js`（逐帧写内联宽高），这里只放几何。

   ⚠️ **槽本身仍然是 108 × 36**（`topbar3_probe.py` 钉着它和"正好 3:1"），别动。
   两枚图的显示尺寸走 `--brand-mark` / `--brand-word` 两个变量：窄屏那档槽会缩到 84×28，
   标记必须跟着缩 —— 尺寸写死在 JS 里就会在窄屏把药丸撑破。
   ⚠️ `--brand-word` 调到 **0** 就等于"这一档不展开"（手机竖屏就是这样，见 479 那条），
   JS 读到 0 会整个停掉动画 —— 不然标记会缩到 0 又没东西顶上来，logo 凭空消失。
   ⚠️ 只有字标那层 `overflow:hidden`：它是从 0 宽"长"出来的，不裁会先溢到药丸外面；
   而**槽本身不能裁**，标记带着 `drop-shadow`，裁了影子会在底边被切一刀。
   ⚠️ 原来那条虚线 + 小字是占位记号，图到位就删了（`border` / `font-size` /
   `letter-spacing` / `color` 连同 hover 变色一起）。现在 hover 反馈**就是**这段展开动画。 */
#site-nav:not(.is-classic) .nav-logo-slot{
  --brand-mark:36px;--brand-word:96px;
  display:grid;place-items:center;
  width:108px;height:36px;flex:none;margin-right:6px;border-radius:8px}
#site-nav:not(.is-classic) .nav-brand-dock{
  width:100%;height:100%;display:flex;align-items:center;justify-content:center}
#site-nav:not(.is-classic) .nav-brand-mark{
  position:relative;flex:none;
  width:var(--brand-mark);height:var(--brand-mark);display:grid;place-items:center}
#site-nav:not(.is-classic) .nav-brand-tile{
  position:absolute;inset:0;width:100%;height:100%;object-fit:contain;
  filter:drop-shadow(0 2px 3px rgba(0,0,0,.13));will-change:opacity,transform}
#site-nav:not(.is-classic) .nav-brand-word{
  width:0;height:100%;display:grid;place-items:center;
  opacity:0;overflow:hidden;transform:translateX(-4px);
  will-change:width,opacity,transform}
#site-nav:not(.is-classic) .nav-brand-word img{
  display:block;width:var(--brand-word);height:auto;max-width:none;flex:none}
#site-nav:not(.is-classic) .nav-logo-slot:focus-visible{
  outline:2px solid #8e809f;outline-offset:2px}

/* 右格：头像 + 用户名。左 6px 是给头像的（它自己 36px 高），右 16px 给用户名收口。
   `max-width:100%` 见上面 `.nav-back` 那段（"永不重叠"的结构保险）。 */
#site-nav:not(.is-classic) .nav-pill--me{padding:6px 16px 6px 6px;gap:10px;max-width:100%}
/* classic 版：右格原来是 `#me-menu` / 分隔线 / 钟表 直接挂在 `.site-nav-side` 下、靠那条
   `gap:14px` 分开的。现在中间多了一层 `.nav-pill--me`，那条 gap 够不着了，在这儿补回来。 */
#site-nav.is-classic .nav-pill--me{gap:14px}

/* 用户名是 `#me-menu` 的**兄弟**，不是它的孩子 —— 所以下拉面板的 `right-0` 对齐到
   「头像 + 用户名」这一组的右缘，而不是只对着头像（Allen 要的正是这两样绑在一起）。
   ⚠️ **classic 版里藏掉它**：Allen 只说了首页那版右格是"头像 + 用户名"，
   project 页是独立设计、本来就没有昵称。要放出来就把下面那条 `display:none` 删掉。
   ⚠️ `min-width:0` + `flex:0 1 auto` 是**省略号能不能生效的前提**：flex item 的
   `min-width` 默认是 `auto`（= 内容宽），不归零就永远压不到省略号那一档，
   上面那条 `max-width:100%` 也就白写了。`max-width:12ch` 是宽屏时的上限，
   窄屏时由 `max-width:100%` 那一层先接管。 */
.nav-me-name{font-size:13.5px;font-weight:600;color:var(--text-main);white-space:nowrap;
  flex:0 1 auto;min-width:0;max-width:12ch;overflow:hidden;text-overflow:ellipsis}
#site-nav.is-classic .nav-me-name{display:none}
/* 昵称要能缩，头像 / 钟表 / 分隔线**不能**缩（缩了就变形，而且它们是固定尺寸的图标位）。 */
#site-nav:not(.is-classic) #me-menu{min-width:0}
#site-nav:not(.is-classic) #me-toggle,
#site-nav:not(.is-classic) .site-nav-clock{flex:none}

/* 分隔线：把「头像 + 用户名」和后面那段「今日已学」分开。
   取色同 `.seg-sep` 的理由：`--border-color`（#F0F0F0）在白底上对比度只有 1.13:1，
   实测就是看不见 —— 等于没画。`#E5E7EB` 是站内既有写法，白底上读得出来又不重。
   ⚠️ 只存在于 pill 版；classic 版藏掉，否则空 span 会在头像和钟表之间白吃一个 14px 的缝。 */
#site-nav:not(.is-classic) .nav-pill-sep{width:1px;height:20px;flex:none;background:#E5E7EB}
#site-nav.is-classic .nav-pill-sep{display:none}
/* classic 版钟表回到老行为：窄屏（<640px，原来那个 Tailwind `hidden sm:flex`）收起。 */
@media(max-width:639px){#site-nav.is-classic .site-nav-clock{display:none}}
/* 这里原来还有两条：`#site-nav` 上的 `transition:transform .34s`，
   以及 `#site-nav.is-hidden` 那条位移（下滑把胶囊滑出视口，上滑再滑回来）。
   2026-09-11 Allen 明确要求**不再隐藏**，两条一起删了——那个 transition 只服务这一处位移，
   留着没有任何东西会动它。
   ⚠️ 别把它当成"为了绕开 rAF 卡死无头验证"的取舍：那是**另一件事**，已经单独修好了
   （`nav-scroll.js` 现在同步判定，不挂 pending 帧）。这里是**产品决定**，跟工具链无关，
   不要再"顺手恢复"。
   真要恢复：把 base.html 里的 `js/nav-scroll.js` 加回来，再把上面两条加回来，
   另外记得补一条 `@media(prefers-reduced-motion:reduce)` 让 `#site-nav` 的 transition 归零
   （原来有，跟着一起被删了）。 */
/* 阅读页的顶栏塞在 `#reader-nav` 那套"鼠标贴顶才展开"的折叠机制里，不能改成悬浮 */
#reader-nav #site-nav{position:relative;transform:none;z-index:20}

/* 首页：顶栏只占主栏那一列。
   2026-09-11 左右对调之后主栏在左、侧栏在右，胶囊不该再横跨到侧栏头上。
   `left` 不动（视口 20px），只把 `right` 收成侧栏宽——胶囊在主栏里左右各内缩 20px，
   和它原来在视口里左右各内缩 20px 是同一套观感。
   ⚠️ 宽度只认 `--home-side-w` 这一个来源（下面 `.home-side` 也用它），别在这里再写死一个 360px。 */
.home-page #site-nav{right:var(--home-side-w,360px)}
/* 640px 以下整页变成单栏（`.home-columns{display:block}`），胶囊恢复通栏 */
@media(max-width:640px){.home-page #site-nav{right:0}}

/* ⚠️ **首页不显示左格那枚返回键**（2026-09-15 Allen：「把首页有的那个返回按钮给去掉」）。
   理由也是自洽的：首页就是这个站的家，`history.back()` 只会把人带出站外，
   而 `href` 兜底指向的又是首页本身 —— 点它要么跳走、要么原地不动，两种都不是"返回"。
   ⚠️ **只隐藏、不删元素**：别的页（`/courses`、`/exercises`、课程总览…）它仍然是个真按钮，
   `js/topbar.js` 里那段"有上一页才 `history.back()`、否则交给 href"的逻辑照样在跑。
   ⚠️ 判据是 `.home-page`（挂在 `index.html` 根 div 上），**不是**"路径等于 `/`"——
   全站只有首页那个根 div 带这个类，和上面那条让位规则同一个来源。
   ⚠️ 特异性 (1,3,0)，压得过 `.nav-back` 自己那条 (1,2,0)；≤639 那条也把它藏起来，
   两处都藏，所以窄屏的首页看不到它是"本来就该这样"。 */
.home-page #site-nav:not(.is-classic) .nav-back{display:none}

/* 「让位给悬浮胶囊」的 padding 统一放在**文件末尾**（见最后那一块）：
   这里写在中间的话，会被后面几条 @media 里的 `padding` 简写盖掉
   （`.home-main{padding:26px 24px 50px}` 就是一条），窄屏直接破功。 */

/* 两侧各占一栏。
   ⚠️ 中间和右侧的 `display` **现在必须由本文件出**：2026-09-15 三药丸版改完之后，
   模板上那两处 Tailwind 的 `hidden md:flex` / `hidden sm:flex` 已经删了 ——
   本文件排在 tw.css 之后，同优先级下会把它们压掉，小屏就藏不住。
   窄屏改成下面那组 @media 里按宽度收起「今日已学」/ 用户名 / 站标槽。

   `grid-column` 必须**显式**写死在中间和右侧：`display:none` 的元素**不是 grid item**，
   小屏把中间那列藏掉之后，右侧会自动落到第 2 列（auto 列）——头像就跑到正中间去了。
   （左侧不写，让它自动落到第 1 列。）

   ⚠️ 右侧**不能写 `justify-self:end`**（虽然看着最自然）：`justify-self` 的非 stretch 值会把
   grid item 的宽度定成 `fit-content`（= max-content），于是右格会**撑得比它那一列还宽、
   朝左边溢出**，直接压到中格身上（800px 实测压了 55px）。
   正确写法是 `justify-self:stretch`（默认值，不写就是它）+ `justify-content:flex-end`：
   容器老老实实占满自己那一列，内容靠右。列不够宽时 `.nav-pill--me` 的
   `max-width:100%` 才接得住。 */
.site-nav-side{display:flex;align-items:center;gap:14px;min-width:0}
.site-nav-end{grid-column:3;justify-content:flex-end}
.site-nav-center{grid-column:2;justify-self:center;display:flex;align-items:center;gap:30px;font-size:13.5px;font-weight:500}
.site-nav-clock{display:inline-flex;align-items:center;gap:6px;font-size:11.5px;color:var(--text-muted);white-space:nowrap}
.site-nav-clock .ico{font-size:13px}

.site-nav-logo{display:block;flex-shrink:0;border-radius:10px;transition:transform .18s ease}
.site-nav-logo img{display:block;width:34px;height:34px;border-radius:10px}
/* 自己传的 Logo：**不强行压成方**。字标是宽的一压就变形，所以只锁高度、宽度跟着走，
   上限 170px（再宽就把中间的导航挤了）。方图自然还是 34×34。 */
.site-nav-logo.is-custom{border-radius:0}
.site-nav-logo.is-custom img{width:auto;height:34px;max-width:170px;border-radius:0;object-fit:contain}
.site-nav-logo:hover{transform:scale(1.06)}
.site-nav-logo:active{transform:scale(.96)}
.site-nav-logo:focus-visible{outline:2px solid #8e809f;outline-offset:3px}

/* 头像按钮：就一个圆。
   ⚠️ 2026-09-15 昵称**又摆回头像旁边了**（Allen：「右边的 bar 改成头像和用户名」，
   见 `.nav-me-name`）—— 上面那句"不差这一处"是 09-11 的取舍，已经作废，别再照它删。
   头像自己 36px 高，正好等于药丸内高（48 − 6×2），所以它顶不到药丸的边。 */
#me-toggle{display:grid;place-items:center;width:36px;height:36px;padding:0;border:0;
  border-radius:999px;background:transparent;cursor:pointer;
  transition:transform .18s ease,background-color .18s ease}
#me-toggle:hover{background:rgba(28,28,30,.06);transform:scale(1.06)}
#me-toggle:active{transform:scale(.95)}
#me-toggle:focus-visible{outline:2px solid #8e809f;outline-offset:2px}

/* ---------------------------------------------------------------- 悬停动效
   一套统一的动作语言：抬起 2–3px + 投影加深，.2s 快速响应；按下回到原位——
   不回到原位的话点下去像没反应。只动 transform / box-shadow / border-color / color，
   没有一处触发布局，所以都不会让页面抖。 */

/* 顶栏导航项：一枚**圆角高亮块**（三药丸版）。
   2026-09-15 从"下划线从中间展开"换成"悬停垫一层浅灰"：药丸版里导航项自己就是一枚
   36px 高的圆角块，底下再压一条 2px 下划线会同时出现两层"我选中了"的语言。
   当前页 = 更实一点的浅灰 + 墨色字。
   ⚠️ **故意不动 `font-weight`**：加粗会让文字宽出 1–2px，切页时中格那枚药丸跟着变宽、
   后面三项一起挪一下。hover 时变宽无所谓（鼠标在那儿），**常驻的当前页不行**。

   ⚠️ 老版单胶囊（project 页）**仍是下划线那套**，写在下面 `.is-classic` 那几条里 ——
   那里导航项没有底，只能靠下划线定位。两套别合并。 */
.top-nav-link{display:flex;align-items:center;height:36px;padding:0 20px;
  border-radius:999px;color:var(--text-muted);text-decoration:none;white-space:nowrap;
  transition:color .18s ease,background-color .18s ease}
.top-nav-link:hover{color:var(--text-main)}
.top-nav-link.is-active{color:#1c1c1e}

/* ⚠️ 上面那两条**故意不写背景色**：高亮现在归滑片（`.nav-glider`）管 ——
   滑片滑到哪一项，哪一项就有浅灰底。要是这里也写一份，hover 时会出现**两层灰叠一起**
   （滑片的 .075 叠上逐项背景的 .045，颜色明显更深，看着像"这一项被选中了两次"）。

   下面这两条是**无 JS 的兜底**：滑片是脚本驱动的，脚本没跑（禁用 / 报错 / 还没执行）
   时不能连个 hover 反馈都没有。`topbar.js` 一启动就给 `#site-nav` 挂 `.glider-on`，
   这两条随即失效 —— 于是"有脚本"和"没脚本"各自都是对的。 */
#site-nav:not(.is-classic):not(.glider-on) .top-nav-link:hover{background:rgba(17,17,19,.045)}
#site-nav:not(.is-classic):not(.glider-on) .top-nav-link.is-active{background:rgba(17,17,19,.075)}

/* ---- classic：下划线版（project 页） ----
   ⚠️ `::after` 那两条**不能删**：`playground.css` 深色主题里还有
   `[data-ff-theme="dark"] .ff-pick-page .top-nav-link::after { background: var(--ff-ink) }`
   —— 那里底是近黑，墨色下划线等于没有，靠它换成浅色。 */
.is-classic .top-nav-link{display:inline-block;position:relative;height:auto;
  padding:0 0 4px;border-radius:0;background:transparent}
.is-classic .top-nav-link:hover{background:transparent;color:var(--text-main)}
.is-classic .top-nav-link.is-active{background:transparent;color:var(--text-main)}
.is-classic .top-nav-link::after{content:"";position:absolute;left:0;right:0;bottom:0;height:2px;
  border-radius:2px;background:#1c1c1e;transform:scaleX(0);transform-origin:center;
  transition:transform .22s cubic-bezier(.22,1,.36,1)}
.is-classic .top-nav-link:hover::after,
.is-classic .top-nav-link.is-active::after{transform:scaleX(1)}

/* 左栏顶部原来有两张统计卡（学习天数 / 完成讲次），连同它们的 hover 触感一起删了。
   这块现在只剩"头像 + 昵称 + 今天学了多久"一行。 */

/* 本周卡：白面没有 inset 高光可叠，只加深外层环境阴影 */
.home-side-cal .week-card{transition:transform .2s ease,box-shadow .2s ease}
.home-side-cal .week-card:hover{transform:translateY(-2px);box-shadow:0 1px 2px rgba(0,0,0,.04),0 18px 36px -22px rgba(0,0,0,.42)}

/* 考核卡 */
.home-side-cal .event-card{transition:transform .2s ease,box-shadow .2s ease,border-color .2s ease}
.home-side-cal .event-card:hover{transform:translateY(-2px);box-shadow:0 1px 2px rgba(0,0,0,.04),0 16px 30px -22px rgba(0,0,0,.4)}
/* 完成 / 删除那两个小按钮 */
.event-controls button{transition:background .15s ease,color .15s ease,transform .15s ease}
.event-controls button:hover{transform:scale(1.06)}
.event-controls button:active{transform:scale(.94)}

/* 速览卡 hover：**整张卡放大**（2026-09-13 Allen 要的）。
   以前这里只能动描边和投影，因为 transform 被 JS 逐帧占着、两层写同一个元素会打架；
   现在 JS 的居中缩放挪到了内层 `.peek-inner`，卡片自己空出来了。
   ⚠️ `z-index` 必须带 `!important`：JS 每帧往卡片写内联 `style.zIndex`（`100 - d*60`），
   内联样式只有 `!important` 压得住。不抬这一下，鼠标往左移到卡片上时它会被
   **右边那张**压住半个身子（`margin-left:-83px` 的负边距让右卡在上面）。 */
/* hover 抬起：**放大感弱了、阻尼重了**（2026-09-15 Allen：「这个反应太大了，有一种廉价感，
   我需要阻尼感更强一点，放大感再稍微弱一点」）。
   三个旋钮都动了，别只改一个：
     · 幅度 `1.055 → 1.028`（"放大感再稍微弱一点"）；
     · 缓动 `cubic-bezier(.25,1.4,.4,1)` → `--peek-hover-ease`（那条重阻尼弹簧，
       起步斜率 5.60 → 0.60、过冲 5.29% → 0%，见 `.peek` 里的注释）；
     · 时长 `.45s → .5s`（更长一点，"重"才站得住）。
   合起来：绝对过冲位移从 `0.055 × 5.29% = 0.29%` 掉到 **0**，起步速度慢 9 倍 ——
   "一碰就弹"的那个劲儿没了，剩下的是"被压下去、慢慢浮上来"。
   ⚠️ `z-index` 必须带 `!important`：JS 每帧往卡片写内联 `style.zIndex`（`100 - d*60`），
   内联样式只有 `!important` 压得住。不抬这一下，鼠标往左移到卡片上时它会被
   **右边那张**压住半个身子（`margin-left:-83px` 的负边距让右卡在上面）。 */
.peek-card:hover{transform:scale(1.028);z-index:300!important;
  transition:transform .5s var(--peek-hover-ease),filter .5s ease}
/* 投影加深写在**内层**（和 `.is-active` 同一个口径）：外层放大只让阴影跟着变大，
   "抬起来"那层意思得靠颜色变深。写在卡片上就会变成浮在图外面的一圈。
   ⚠️ 别跟着幅度一起缩太多：投影变深是"抬起来"的主要信号，1.028 那点放大量几乎看不出来，
   阴影要是也不动，hover 就彻底没反馈了。 */
.peek-card:hover .peek-inner{box-shadow:0 4px 10px rgba(20,16,30,.18),0 24px 50px -2px rgba(20,16,30,.48)}
/* 文字再轻轻提一下，和卡片的放大叠在一起更有"抬起来"的感觉。
   缓动 / 时长和卡片完全对齐（进入 .5s、离开 .4s），两处节奏不一致会像各动各的。
   ⚠️ 位移也从 -3px 收到 **-2px**：卡片只放大 2.8% 了，文字还提 3px 会显得"文字比卡片动得多"。 */
.peek-info{transition:transform .4s var(--peek-hover-ease)}
.peek-card:hover .peek-info{transform:translateY(-2px);
  transition:transform .5s var(--peek-hover-ease)}

/* 候选卡（不在正中的那些）加一点点模糊 —— "景深"。
   2026-09-15 Allen：「试着给后面的候选卡选加一点点模糊的效果」。
   ⚠️ **按 class 切、不逐帧写**：`filter` 不是合成层友好的属性，逐帧写会把它从
   合成层拽回主线程，这一排滚动就不丝滑了（那是这个组件最在意的事，见文件里多处注释）。
   现在只有"居中那张换了"时才动 class，模糊交给 CSS 的 `transition:filter` 自己过渡。
   ⚠️ 挂在外层 `.peek-card` 上、不是 `.peek-inner`：内层有自己的 `box-shadow`，
   挂在外层连投影一起糊，才是"整张卡退到景深后面"的样子。
   ⚠️ **1.5px 是量出来的，不是估的**。三个值在 1:1 逻辑像素下并排比过
   （`verify/peek3-blur-cmp.png`，用 `element.style.filter` 现场改的，不是三份稿子）：
     · `1px`   —— 封面看得出软了一点点，但**文字基本还是清晰的**，"景深"读不出来；
     · `1.5px` —— 封面明显发虚、文字仍然可读 ← **取这个**；
     · `2px`   —— `REAL-TIME DSP DESIGN AND APPLICATIONS` 那行已经难认了。
   可用区间就是 1–2px，两头都出界。要改就照上面那个法子重新并排比一遍。
   ⚠️ 居中那张**没有**这条规则，所以它天然是 `filter:none` —— 不用再写一条覆盖。 */
.peek-card:not(.is-active){filter:blur(1.5px)}

/* 课程卡：封面本来就会放大，这里让整张卡也抬起来 */
.home-course-card{transition:transform .22s ease}
.home-course-card:hover{transform:translateY(-3px)}
.hcc-cover{transition:border-color .2s ease}
.home-course-card:hover .hcc-cover{border-color:rgba(20,16,30,.2)}

/* 「管理课程」按钮 */
.home-main-head>a{transition:transform .2s ease,box-shadow .2s ease,background .2s ease}
.home-main-head>a:hover{transform:translateY(-2px)}
.home-main-head>a:active{transform:translateY(0)}

@media(prefers-reduced-motion:reduce){
  /* ⚠️ classic 那两条要**连 `.is-classic` 一起列**：它们的特异性 (0,2,1) 高于下面
     裸写的 `.top-nav-link` (0,1,1)，不列就压不住，project 页上过渡照跑。
     ⚠️ 三药丸那两条同理：`.nav-glider` / `.nav-pill` 都写成
     `#site-nav:not(.is-classic) xxx`（(1,2,0)），裸写压不住，滑片照样滑。
     ⚠️ **站标（`.nav-logo-slot`）不在这张表里** —— 它的展开动画是 `js/brand.js`
     逐帧写内联样式，一条 transition 都没有。那个文件自己判 `prefers-reduced-motion` 并直接跳到终态。 */
  .top-nav-link,.is-classic .top-nav-link,
  .is-classic .top-nav-link::after,#me-toggle,.site-nav-logo,
  #site-nav:not(.is-classic) .nav-glider,
  #site-nav:not(.is-classic) .nav-pill,
  /* ⚠️ 返回键那两条要**单独列**：`.nav-back` 自己那条 `transition` 是 (1,3,0)，
     上面 `.nav-pill` 那条 (1,2,0) 压不住它，不列的话箭头照样滑、底色照样渐变。 */
  #site-nav:not(.is-classic) .nav-back,
  #site-nav:not(.is-classic) .nav-back .ico,
  .home-side-cal .week-card,.home-side-cal .event-card,
  .event-controls button,.peek-card,.peek-info,.home-course-card,.hcc-cover,
  .home-main-head>a{transition:none}
  .home-side-cal .week-card:hover,
  .home-side-cal .event-card:hover,.home-course-card:hover,.home-main-head>a:hover,
  #me-toggle:hover,.site-nav-logo:hover,
  #site-nav:not(.is-classic) .nav-pill:hover,
  #site-nav:not(.is-classic) .nav-pill:active,
  #site-nav:not(.is-classic) .nav-back:hover .ico{transform:none}
  .peek-card:hover .peek-info{transform:none}
}

/* 窄视口：侧栏从 360px 掉到 300px，卡片内宽只剩 248px。
   周数那一列是 auto，会先吃掉 max-content，日历就只剩 127px（15px/格，两位数直接挨上）。
   把周数那列压窄、内边距收紧，把日历放回 140px —— 和改造前一个水平，不会更挤。 */
@media(max-width:1100px){
  /* 只改这一个变量：`.home-side` 的宽度和 `.home-page #site-nav` 的右边界都读它 */
  .home-page{--home-side-w:300px}.home-main{padding:26px 24px 50px}
  .week-card{padding:16px 15px 14px}
  .week-calendar-layout{gap:12px}
  .week-word{font-size:10px;letter-spacing:.08em}
  .week-number{font-size:46px}
  .week-caption{font-size:8px;letter-spacing:.1em}
  .mini-calendar{padding-left:12px}
  .calendar-day{font-size:10.5px;min-height:22px}
}
@media(max-width:900px){.course-grid{grid-template-columns:1fr}}
/* 窄屏那一档是**独立的一套数**（卡宽 min(300px,76vw) / 重叠 −54 / 轨道 padding 18…34），
   和桌面那组不是倍数关系，改一处别指望另一处跟着动。
   ✅ **比例只有一套 `--peek-ratio`（桌面窄屏同一个值）** —— 2026-09-14 定成 `1.85` 之后
   窄屏也安全（300×162，`fade.ratio` **0.82**），所以那条窄屏覆盖**删掉了**。
   ⚠️ **但"窄屏要不要单独一套比例"这件事得一直盯着**：卡越小，那块固定 px 的字
   占卡高的比例越大。2026-09-13 和 2026-09-14 各撞过一次（20/13、20/9 都因为
   窄屏 ratio 冲到 0.98 而被迫分档）。
   **留个记性**：**再加行 / 加字号 / 压扁卡片，先量 `peek_geom.py` 的 `fade.ratio`**，
   >1 就是文字探出了封面蒙版（白字直接压在封面原图上），那时才需要回来动比例或缩字号。 */
@media(max-width:640px){
  .peek{--peek-card-width:min(300px,76vw)}
  .peek-card+.peek-card{margin-left:-54px}
  .peek-track{padding:18px max(14px,calc(50% - var(--peek-card-width)/2)) 34px}
}
/* ⚠️ 这里 `.peek-card:hover` / `.peek-card:hover .peek-info` 两条**必须单列**：
   `:hover` 变体是 (0,2,0)，裸写的 `.peek-card`（(0,1,0)）压不住 ——
   `transition` 会留在那条 `.5s` 的弹簧上。虽然 `transform:none` 让 hover 前后一样、
   动不起来，但 `filter` 那条过渡还在跑（模糊淡入淡出 0.5s），
   而"减少动态效果"是不该有这段的。（同一个坑今天在顶栏上踩过两次。） */
@media(prefers-reduced-motion:reduce){.hcc-cover img,.hcc-tools{transition:none}.home-course-card:hover .hcc-cover img{transform:none}.peek-card,.peek-card:hover,.peek-info,.peek-card:hover .peek-info{transition:none}.peek-card:hover{transform:none}.peek-inner{will-change:auto}.peek-burst{transition:none}}
/* Quiet course library: system type, neutral surfaces and grouped rows. */
/* ------------------------------------------------------------ 课程总览（/c/<课程>/）
   2026-09-11 按用户给的设计稿重做：**封面当页头整块背景**（编号 / 课程名 / 统计 / 继续学习 /
   进度条都压在图上）、封面下面一条标签条、讲义和资源各一张白卡。

   ⚠️ 上一版（240px 缩略图 + 并排的课程名 + 两张大的错题本/笔记本卡）**整个撤掉**。
   上一版也是用户定的（"封面展示框和课程的名字还是放在一起，展示框不要太大了"），
   两份要求前后不一致，**以最新这份为准**——封面现在是页头本身，不是缩略图。
   那两张 `.course-action` 卡的去处：错题本 / 笔记本挪进标签条（还带着各自的数字）。

   和首页共用的那套语言**没变**：同样的 .hub-aurora 色晕底、
   同样的「半透明白卡 + 1px 白 hairline + 极轻投影」、同样的留白节奏。

   三条容易踩的：
   1. **别在这一页写 `background`（简写）**。`background` 会把 background-image 一起清成 none，
      等于把模板上 .hub-aurora 那层色晕整个盖掉——这一页以前就吃过这个亏，看着只是
      "比首页灰一点"，很容易当成设计选择。要改底色只用 background-color，或者干脆不写。
   2. **正文字体交回 Satoshi**，只有页面级标题（.course-title）走系统无衬线，
      和首页 .home-main-title h1 对齐。⚠️ 那条例外必须写在 `.course-overview .course-title` 上：
      input.css 的 `h1,h2,...,.display-font{font-family:Cabinet Grotesk}` 是**直接命中 h1 元素**的，
      父级上的 font-family 只是继承，压不过它。
   3. **封面上所有文字都是白的**，所以 .is-blank 的兜底必须是**深色**渐变——
      换成浅色的话，没封面的课直接看不见字。 */

.course-overview .overview-grid{max-width:1280px;grid-template-columns:minmax(0,1fr) 310px;gap:56px;padding:36px 40px 64px}
.course-library{min-width:0}
.course-back{display:inline-flex;align-items:center;gap:5px;font-size:13px;color:#83838b}
.course-back .ico{width:14px;height:14px}
.course-back:hover{color:#29292e}

/* ---- 封面卡 ----
   高度**写死**，不用 aspect-ratio：它是"页头"，不该随视口越宽长得越高——
   主栏最宽 834px，按 16/10 会变成 521px 高，把标签条和讲义全顶出首屏。
   图是 <img> 而不是 background-image：同一条 view-transition-name（peek-morph）
   要和首页课程卡那张 <img> 做共享元素变形，background 不参与变形。 */
.course-hero{position:relative;display:block;height:216px;border-radius:18px;overflow:hidden;
  isolation:isolate;background:#1b1d1c;
  box-shadow:0 1px 2px rgba(28,28,30,.06),0 18px 40px -22px rgba(28,28,30,.5)}
.course-hero-bg{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;z-index:0}
/* 两层蒙版，**缺一不可**：横向那道撑住左边三行字，纵向那道撑住底部的按钮和进度条。
   只留横向的话，右下角那条进度条会直接消失在图上（浅色封面尤其明显）。 */
.course-hero-scrim{position:absolute;inset:0;z-index:1;
  background:linear-gradient(90deg,rgba(8,10,9,.92) 0%,rgba(8,10,9,.72) 34%,rgba(8,10,9,.22) 66%,rgba(8,10,9,.34) 100%),
             linear-gradient(0deg,rgba(8,10,9,.55) 0%,rgba(8,10,9,0) 46%)}
.course-hero.is-blank{background:linear-gradient(135deg,#2b2f3a,#1b1d22 55%,#232833)}
.course-hero-body{position:relative;z-index:2;height:100%;display:flex;flex-direction:column;
  padding:24px 32px 20px}
.course-code{font-size:11px;font-weight:600;letter-spacing:.16em;text-transform:uppercase;
  color:rgba(255,255,255,.72);margin:0}
/* 课程名走系统无衬线 + 700（见文件头第 2 条）。
   `max-width` 是**故意的**：名字要在左侧那块深蒙版里换行，不然会一路铺到右边的亮区上。
   短名字够不到这个宽度，自然是一行。 */
/* 课程总览页的字体 —— 2026-09-15 Allen：「某门课程的主页面的字体…都按上述字体的要求调」，
   也就是跟着方案 D（`--font-card`，Helvetica Neue 优先）。
   ⚠️ **挂在 `.overview-grid` 上，不是 `.course-overview`**：顶栏 `{{ topbar('courses') }}`
   也在这个根 div 里面，挂根上会把顶栏那四项导航的字一起换掉，
   于是这一页的顶栏和别的页长得不一样。`.overview-grid` 是 `<main>`，正好圈住内容、不含顶栏。
   ⚠️ 下面这串要**显式列**，光靠继承压不过它们自己的规则：
     · `h1/h2/h3` —— `tools/css/input.css` 的 `h1,h2,…{font-family:Cabinet Grotesk}` 是
       **直接命中元素**的（(0,0,1)），父级继承赢不了它；
     · `.course-title` —— 自己写了一条 `-apple-system,…`（(0,2,0)），得 (0,3,0) 才压得住。
   ⚠️ 页面级的**展示字体**（`.display-font`、站标）**不在**这条里，仍是 Cabinet Grotesk。 */
.overview-grid,
.overview-grid h1,.overview-grid h2,.overview-grid h3,
.course-overview .overview-grid .course-title{font-family:var(--font-card)}
.course-overview .course-title{font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Helvetica Neue',sans-serif;
  font-size:clamp(24px,2.3vw,32px);font-weight:700;letter-spacing:-.02em;line-height:1.1;
  color:#fff;max-width:22em;margin:8px 0 0;overflow-wrap:anywhere;text-wrap:pretty;
  text-shadow:0 1px 10px rgba(0,0,0,.35)}
.course-meta{display:flex;align-items:center;flex-wrap:wrap;gap:7px;font-size:13px;
  color:rgba(255,255,255,.72);margin:10px 0 0}
.course-meta i{font-style:normal;color:rgba(255,255,255,.4)}
/* 按钮和进度条那一行贴到底：body 是 flex column，`margin-top:auto` 把它推下去。 */
.course-hero-foot{display:flex;align-items:center;justify-content:space-between;gap:16px;
  margin-top:auto;padding-top:14px}
.course-continue{display:inline-flex;align-items:center;gap:7px;flex-shrink:0;
  height:34px;padding:0 16px;border-radius:999px;background:#fff;color:#1c1c1e;
  font-size:13px;font-weight:600;text-decoration:none;
  box-shadow:0 1px 3px rgba(0,0,0,.24);transition:transform .16s ease,box-shadow .16s ease}
/* `play` 是 lucide 的实心三角（`fill:none` 在 sprite 上），不补 fill 就只剩一条折线。 */
.course-continue .ico{width:12px;height:12px;fill:currentColor}
.course-continue:hover{transform:translateY(-1px);box-shadow:0 3px 10px rgba(0,0,0,.3)}
.course-continue:focus-visible{outline:2px solid #fff;outline-offset:2px}
.course-progress{display:flex;align-items:center;gap:10px;min-width:0}
.cp-bar{position:relative;display:block;width:150px;height:3px;border-radius:999px;
  background:rgba(255,255,255,.28);overflow:hidden;flex-shrink:0}
.cp-bar>span{position:absolute;top:0;bottom:0;left:0;border-radius:999px}
/* **两层**：浅色 = 读过（含只读了没标完成的），实色 = 已标记完成。
   和首页 .hcc-read/.hcc-done、阅读页 progress 同一条口径。只画实色的话，
   读了一小时进度条还是空的，看着像坏了。 */
.cp-read{background:rgba(255,255,255,.5)}
.cp-done{background:#fff}
.cp-label{font-size:12px;color:rgba(255,255,255,.9);white-space:nowrap;
  text-shadow:0 1px 6px rgba(0,0,0,.4)}
@media(prefers-reduced-motion:reduce){.course-continue{transition:none}}

/* ---- 标签条 ----
   四个标签**全是本页内的面板切换，一个都不跳走**（2026-09-12 用户定）。
   ⚠️ 上一版是"讲义 / 资源是本页锚点，笔记本 / 错题本跳各自页面"（2026-09-11），
   用户的原话是「单击笔记本按钮直接跳转到了另一个页面，这样的逻辑交互不对」。
   两个详情页还在，入口换成了面板里的预览卡。**别再改回跳转。**

   元素从 <a> 换成了 <button>，所以这里要显式清掉按钮的默认底色和边框——
   漏了的话每个标签都会顶着一块系统灰。
   激活那项是"白药丸 + 底下一道短横"，不是纯下划线——药丸给出点击区，横线给出现在在哪。 */
.course-tabs{display:flex;align-items:center;gap:34px;margin:18px 0 4px;flex-wrap:wrap}
.course-tab{position:relative;display:inline-flex;align-items:center;gap:8px;
  height:40px;padding:0 16px;border:0;border-radius:999px;
  font-family:inherit;font-size:14px;font-weight:500;line-height:1;text-decoration:none;
  color:#6e6e73;background:none;cursor:pointer;transition:color .16s,background-color .16s}
.course-tab .ico{width:17px;height:17px;flex-shrink:0}
.course-tab:hover{color:#1c1c1e;background:hsla(0,0%,100%,.6)}
.course-tab.is-active{color:#1c1c1e;background:#fff;
  box-shadow:0 1px 2px rgba(28,28,30,.06),0 6px 16px -12px rgba(28,28,30,.4)}
.course-tab.is-active:after{content:"";position:absolute;left:16px;right:16px;bottom:-6px;
  height:2px;border-radius:2px;background:#1c1c1e}
.course-tab-count{display:inline-flex;align-items:center;justify-content:center;
  min-width:20px;height:19px;padding:0 6px;border-radius:999px;
  background:rgba(28,28,30,.06);font-size:11px;font-weight:600;color:#8e8e93}
.course-tab.is-active .course-tab-count{background:rgba(28,28,30,.08);color:#6e6e73}
.course-tab:focus-visible{outline:2px solid #8e809f;outline-offset:2px}
@media(prefers-reduced-motion:reduce){.course-tab{transition:none}}

/* ---- 区块白卡（讲义 / 资源共用） ----
   卡面沿用习题页 .ex-deck 那套半透明白 + 白 hairline：实心 #fff 压在这层色晕上像贴上去的白纸。 */
.course-card{margin-top:22px;background:hsla(0,0%,100%,.72);
  border:1px solid hsla(0,0%,100%,.9);border-radius:18px;overflow:hidden;
  box-shadow:0 1px 2px rgba(28,28,30,.04),0 10px 26px -18px rgba(28,28,30,.3)}
.course-card-head{display:flex;align-items:center;gap:9px;padding:16px 20px;
  border-bottom:1px solid rgba(28,28,30,.055)}
.course-card-head .ico{width:18px;height:18px;color:#1c1c1e;flex-shrink:0}
.course-overview .course-card-head h2{font-size:16px;font-weight:600;letter-spacing:-.02em;margin:0}
.course-card-n{margin-left:auto;font-size:12px;color:#a0a0a9}
.course-card-body{padding:16px 20px 20px}

/* ---- 讲义行 ----
   行高锁 52px（从设计稿量出来的）。分隔线**从行内文字起**（left:50px）而不是通栏——
   通栏那条线会把行首那枚圆环也切进去，看着像表格。 */
.course-overview .lecture-entry{position:relative;border:0;border-radius:0;margin:0;background:transparent}
.course-overview .lecture-entry+.lecture-entry:before{content:"";position:absolute;top:0;
  left:50px;right:0;height:1px;background:rgba(28,28,30,.055)}
.course-overview .lecture-entry>a{display:flex;align-items:center;gap:13px;
  min-height:52px;padding:0 6px 0 20px;justify-content:initial;text-decoration:none;color:inherit}
.course-overview .lecture-entry:hover{background:hsla(0,0%,100%,.6)}
/* 当前讲次（= 封面卡上「继续学习」指向的那一讲）。
   ⚠️ **这层浅绿底不是"进行中"的意思**——进行中可能有两行，只有这一行有底色。
   底色说的是"下一步从这儿接着读"。 */
.course-overview .lecture-entry.is-current{background:#f3f9f5}
.course-overview .lecture-entry.is-current:hover{background:#eef6f1}
/* 行首三态圆环。中间用 radial mask 挖空，而不是盖一个白圆：
   盖白圆的话，落在 .is-current 的浅绿底上会露出一圈白边。
   ⚠️ **2026-09-17 起"进行中"那圈弧是真进度**（之前写死 50%，纯装饰）：
   弧长 = 模板写的 `--ring-arc`，值来自 `views._lecture_progress`，**是加权的**——
   正文节均分 90% ＋ 末尾收尾块（audit / coverage / selftest / …）整体 10%。
   所以 `--ring-arc` **不等于**"已标记读完的小节 ÷ 可标记的小节数"，别拿 tooltip 的
   原始分数去反推它（6/12 节可以是 68%）。口径细节见那个函数的 docstring。
   变量缺省回落 50% —— 只有"这一讲没法追踪小节"（自包含讲义，`sections` 为 0）才会走到那一步。
   剩下那截轨道用**浅绿**而不是中性灰：一节都还没标时弧长就是 0、环是空的，
   靠底色和"未开始"的灰圈区分。**别给弧加最小值** —— 那是假进度，用户要的就是真数。 */
.lecture-ring{position:relative;display:grid;place-items:center;flex-shrink:0;
  width:19px;height:19px;border-radius:50%}
.lecture-ring .ico{width:11px;height:11px;color:#fff;stroke-width:3}
.lecture-ring[data-state=new]{box-shadow:inset 0 0 0 2px #dcdce2}
.lecture-ring[data-state=reading]{background:conic-gradient(#33a06a 0 var(--ring-arc,50%),rgba(51,160,106,.22) var(--ring-arc,50%) 100%);
  -webkit-mask:radial-gradient(farthest-side,transparent calc(100% - 2px),#000 calc(100% - 2px));
  mask:radial-gradient(farthest-side,transparent calc(100% - 2px),#000 calc(100% - 2px))}
.lecture-ring[data-state=done]{background:#33a06a}
.course-overview [data-lecture-title]{flex:1;min-width:0;font-size:14px;line-height:1.5;
  letter-spacing:-.012em;overflow-wrap:anywhere}
.course-overview .lecture-status{flex-shrink:0;font-size:12.5px;color:#a0a0a9}
.course-overview .lecture-status[data-state=done],
.course-overview .lecture-status[data-state=reading]{color:#33a06a}
.lecture-arrow{width:15px;height:15px;color:#c2c2c9;flex-shrink:0}
/* 讲义管理（2026-09-23）：卡头「导入 HTML」「编辑」；编辑态每行右侧「重命名」「删除」。
   常态下行尾不放任何管理按钮（原来那枚常驻铅笔撤了），编辑态才出现 —— 触屏上也是点「编辑」切换，不靠 hover。 */
.lecture-tools{display:flex;align-items:center;gap:6px;margin-left:10px;flex-shrink:0}
.lecture-tool{display:inline-flex;align-items:center;gap:5px;height:28px;padding:0 10px;border-radius:999px;
  border:1px solid rgba(28,28,30,.1);background:#fff;color:#55555c;font-size:12.5px;white-space:nowrap}
.lecture-tool .ico{width:13px;height:13px;color:currentColor}
.lecture-tool:hover{color:#1c1c1e;border-color:rgba(28,28,30,.22)}
.lecture-tool[aria-pressed=true]{background:#1c1c1e;border-color:#1c1c1e;color:#fff}
.lecture-tool:disabled{opacity:.55;cursor:progress}
.lecture-tool:focus-visible,.lecture-action:focus-visible{outline:2px solid #8e809f;outline-offset:2px}
.lecture-import-status{margin:0;padding:10px 20px;font-size:12.5px;color:#55555c;background:#f7f7f9;
  border-bottom:1px solid rgba(28,28,30,.055);overflow-wrap:anywhere}
.lecture-import-status.is-error{color:#a74444;background:#fbf3f3}
.lecture-actions{display:none;align-items:center;gap:6px;flex-shrink:0;margin-right:14px}
.course-card.is-editing .lecture-actions{display:flex}
.lecture-action{height:28px;padding:0 11px;border-radius:8px;border:1px solid rgba(28,28,30,.1);background:#fff;
  color:#3a3a40;font-size:12.5px;white-space:nowrap}
.lecture-action:hover{border-color:rgba(28,28,30,.25);color:#1c1c1e}
.lecture-action.is-danger{color:#b23b3b;border-color:rgba(178,59,59,.25)}
.lecture-action.is-danger:hover{background:#fbf1f1;border-color:rgba(178,59,59,.45)}
.lecture-action:disabled{opacity:.5;cursor:progress}
.course-empty-line{padding:18px 20px;font-size:13px;color:#9999a3;margin:0}

/* ---- 资源 ---- */
.resource-row{display:flex;align-items:center;gap:11px;min-height:44px;padding:0 10px;
  border-radius:10px;font-size:13.5px;color:inherit;text-decoration:none}
.resource-row .ico{width:15px;height:15px;color:#a0a0a9;flex-shrink:0}
.resource-row>span{flex:1;min-width:0;overflow-wrap:anywhere}
.resource-row small{flex-shrink:0;font-size:12px;color:#a0a0a9}
.resource-row:hover{background:hsla(0,0%,100%,.7)}
/* 空态是**虚线框**（设计稿那样），不是一行灰字——虚线框更像"这里可以放东西"。 */
.course-overview .resource-empty{display:flex;align-items:center;justify-content:center;gap:9px;
  min-height:96px;margin:0;font-size:13px;color:#a8a8b0;
  border:1px dashed rgba(28,28,30,.13);background:hsla(0,0%,100%,.4);border-radius:14px}
.resource-empty .ico{width:19px;height:19px}

/* ---- 标签面板 + 预览长条（笔记本 / 错题本） ----
   四个标签对应四个 `.course-panel`，靠 `hidden` 切换（`[hidden]{display:none!important}`
   在 input.css 里，别在这儿再写一遍 display:none——那样切回来时得记得改两处）。

   预览解决的是"别把详情页搬过来"：每讲**一条横铺的长条**，点条才进详情。

   ⚠️ **2026-09-12 用户定：不做框，做长条。** 上一版是 232px 的方格卡
   （`repeat(auto-fill,minmax(232px,1fr))` + 竖排摘录），窄卡里中文一行放不下几个字、
   两行就被裁成"…"，用户否掉了：「这个预览不要做成框，做成长条形，
   隐藏的部分用模糊显示」。**别再改回方格。**
   现在的结构：左侧 `.preview-head` 固定宽度（哪一讲 / 有几条），右侧 `.preview-strip`
   把摘录横向铺开，**铺不下的不硬裁**——右缘 150px 是一条"虚化带"。

   虚化带怎么做的（两层同一份内容，遮罩互补）：
     `.preview-flow`          正常层，遮罩从左到右渐隐；
     `.preview-flow.is-fog`   同一份摘录的副本，`filter:blur()` + 反向遮罩渐显。
   两层遮罩在 150px 区间里交叉，接缝处看不出边界。

   ⚠️ **为什么这里能用 `filter:blur()`，而全站别处都禁用 `backdrop-filter`**：
   `filter` 作用在**静态**图层上，内容不变就不重算；`backdrop-filter` 要每帧重新取
   背后的像素，滚动时逐帧重算（理由见本文件第 3 行和 input.css 第 41 行）。
   代价是摘录的 DOM 节点**存两份**——所以 `views._preview` 只给前 6 条，
   别把全量铺进来（6 条 × 最多十几组，两份也就百来个节点）。

   摘录**颜色一律中性灰**：这一页要的是"有质感、简约"，
   给每种 kind 配一个彩条会让整屏变成色卡（2026-09-12 用户对错题本提过同样的话）。 */
.course-panel.is-active{animation:course-panel-in .22s ease}
@keyframes course-panel-in{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
.course-panel-all{margin-left:auto;font-size:12.5px;color:#8e8e93;text-decoration:none}
.course-panel-all:hover{color:#1c1c1e}

.preview-list{display:flex;flex-direction:column;gap:10px}
.preview-card{display:flex;align-items:stretch;min-width:0;height:66px;
  padding-left:16px;border-radius:14px;text-decoration:none;color:inherit;
  background:hsla(0,0%,100%,.66);border:1px solid hsla(0,0%,100%,.9);
  box-shadow:0 1px 2px rgba(28,28,30,.03),0 8px 20px -16px rgba(28,28,30,.28);
  overflow:hidden;transition:background-color .16s,box-shadow .16s,transform .16s}
.preview-card:hover{background:hsla(0,0%,100%,.9);
  box-shadow:0 2px 4px rgba(28,28,30,.05),0 14px 30px -16px rgba(28,28,30,.3);
  transform:translateY(-1px)}
.preview-card:focus-visible{outline:2px solid #8e809f;outline-offset:2px}
/* 左侧讲次头：固定宽度，**不参与虚化**——"这是哪一讲"得一直读得清 */
.preview-head{flex:0 0 208px;min-width:0;display:flex;flex-direction:column;
  justify-content:center;gap:3px;padding-right:18px}
.preview-head-top{display:flex;align-items:center;gap:8px;min-width:0}
.preview-badge{flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;
  min-width:26px;height:18px;padding:0 6px;border-radius:6px;
  background:rgba(28,28,30,.06);color:#6e6e73;
  font-size:10px;font-weight:700;letter-spacing:.04em}
.preview-title{flex:1;min-width:0;font-size:13.5px;font-weight:600;letter-spacing:-.01em;
  color:#1c1c1e;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.preview-meta{font-size:11px;color:#a0a0a9;font-variant-numeric:tabular-nums;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.preview-meta.is-done{color:#74a88a}
/* 右侧摘录带。`overflow:hidden` 兜底——遮罩已经把尾巴收干净了，
   这层是防"遮罩不被支持时整条糊出去"。 */
.preview-strip{position:relative;flex:1;min-width:0;overflow:hidden;
  border-left:1px solid rgba(28,28,30,.07)}
.preview-flow{position:absolute;inset:0;display:flex;align-items:center}
/* 两条遮罩在 `100% - 176px` → `100% - 92px` 这 84px 里交叉：
   清晰层在 `-92px` 处**已经彻底没了**，所以最后 92px 是**纯模糊**、不是"半透明的清晰字"。
   第一版交叉区间给到 150px、模糊层 opacity .72，看着只是"渐隐"、不像"失焦"——
   用户要的是后者。⚠️ 想让它更明显就**同时**加宽纯模糊区、加大 blur，
   只调 blur 没用：字已经淡到看不见了，再糊也是看不见。 */
.preview-flow:not(.is-fog){
  -webkit-mask-image:linear-gradient(to right,#000 calc(100% - 176px),transparent calc(100% - 92px));
  mask-image:linear-gradient(to right,#000 calc(100% - 176px),transparent calc(100% - 92px))}
.preview-flow.is-fog{filter:blur(3.2px);opacity:.82;user-select:none;
  -webkit-mask-image:linear-gradient(to right,transparent calc(100% - 176px),#000 calc(100% - 92px));
  mask-image:linear-gradient(to right,transparent calc(100% - 176px),#000 calc(100% - 92px))}
/* `flex:0 0 196px` 是**故意不收缩**：就是要让它铺出去、由虚化带收尾。
   改成 `flex:1 1 auto` 会全部挤进一行，虚化带就没东西可模糊了。 */
.preview-line{flex:0 0 196px;min-width:0;display:flex;flex-direction:column;
  justify-content:center;gap:2px;padding:0 14px;
  border-left:1px solid rgba(28,28,30,.08)}
.preview-line:first-child{border-left:0}
.preview-text{font-size:12.5px;line-height:1.45;color:#4a4a52;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.preview-sub{font-size:11.5px;line-height:1.4;color:#9a9aa3;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
@media(max-width:760px){
  .preview-head{flex-basis:170px;padding-right:14px}
  .preview-line{flex-basis:168px;padding:0 12px}
  /* 窄屏上摘录带本身变短，虚化带按比例收一档（130 / 68），别把整条吃掉 */
  .preview-flow:not(.is-fog){
    -webkit-mask-image:linear-gradient(to right,#000 calc(100% - 130px),transparent calc(100% - 68px));
    mask-image:linear-gradient(to right,#000 calc(100% - 130px),transparent calc(100% - 68px))}
  .preview-flow.is-fog{
    -webkit-mask-image:linear-gradient(to right,transparent calc(100% - 130px),#000 calc(100% - 68px));
    mask-image:linear-gradient(to right,transparent calc(100% - 130px),#000 calc(100% - 68px))}
}
@media(max-width:480px){
  .preview-head{flex-basis:150px;padding-right:12px}
  .preview-line{flex-basis:150px;padding:0 10px}
}
@media(prefers-reduced-motion:reduce){
  .course-panel.is-active{animation:none}
  .preview-card{transition:none}
  .preview-card:hover{transform:none}
}

/* 右栏的上内边距 = **返回链高 + 它到封面卡的间隙**（20 + 3 ≈ 24），
   让 `.week-card` 的顶边和 `.course-hero` 的顶边齐平。
   ⚠️ 这个数是从左栏算出来的，不是拍脑袋定的：改 `.course-back` 的行高、
   或者改它和 hero 之间的 margin，**这个数要跟着改**。
   （验证：探针量 `heroTop - asideTop`，应当等于 24 而不是 0——
     因为 aside 的顶边在返回链那一行，week-card 才对齐 hero。） */
.course-overview .overview-grid>aside{padding-top:24px}
.course-overview .assessment-heading h2{font-size:16px;letter-spacing:-.02em}
.course-overview .event-card h3{font-size:16px}
@media(max-width:1000px){.course-overview .overview-grid{gap:28px;padding:28px 24px;grid-template-columns:minmax(0,1fr) 280px}.course-hero{height:200px}}
@media(max-width:760px){.course-overview .overview-grid{grid-template-columns:minmax(0,1fr);padding:24px 20px;gap:8px}.course-overview .overview-grid>aside{padding-top:24px}.course-hero{height:186px}.course-hero-body{padding:20px 22px 18px}.course-tabs{gap:8px}.course-tab{padding:0 12px}.lecture-arrow{display:none}.course-overview .lecture-status{font-size:11px}.course-overview .lecture-entry>a{padding-left:16px;gap:11px}.cp-bar{width:110px}}
@media(max-width:560px){.course-hero{height:auto;min-height:176px}.course-hero-foot{flex-direction:column;align-items:flex-start;gap:12px}.course-progress{width:100%}.cp-bar{flex:1;width:auto}.course-tabs{gap:4px}.course-tab{font-size:13px;height:36px}}
@media(max-width:480px){.course-hero-body{padding:18px 18px 16px}.course-card-head{padding:14px 16px}.course-card-body{padding:14px 16px 16px}.course-overview .lecture-entry>a{padding:0 4px 0 14px}.lecture-actions{margin-right:8px;gap:4px}.lecture-action{padding:0 8px}
  .lecture-tool{padding:0 9px}.lecture-tools{margin-left:6px}
  .course-card.is-editing .lecture-status,.course-card.is-editing .lecture-arrow{display:none}
  .course-card.is-editing .lecture-entry>a{padding-top:9px;padding-bottom:9px}}

/* Reader toolbar: keep named actions legible in focus mode. */
/* 标题条左侧这一组（返回 / 本讲大纲 / 导图 / 习题速览）名字常显。
   .dock-btn 的默认是"只留图标、悬停才展开文字"——那是给右侧那排次级动作定的；
   左侧这几枚是这一页的主要出口，图标猜不出意思，藏起来等于没有。
   ⚠️ 用 `.reader-tools .dock-btn` **整组**匹配，别再逐个 id 列：2026-09-14 把「习题速览」
   从药丸里挪进来时就差点漏掉——漏了的话那枚按钮在专注模式下只剩一个图标。 */
#title-bar .reader-tools .dock-btn{flex-shrink:0;padding:9px 11px}
#title-bar .reader-tools .dock-btn>span{max-width:none;opacity:1}
/* 左侧这一组包成一层：专注模式下标题条是 grid 三段式，
   得靠这个包裹层把「左 / 中 / 右」凑成正好三个直接子元素。
   gap 取 24px，和模板上 header 的 `gap-6` 一致，非专注模式外观不变。 */
#title-bar .reader-tools{display:flex;align-items:center;gap:24px;flex-shrink:0}
#title-bar .reader-heading{flex:1;min-width:0}
.focus-mode #title-bar{opacity:1}
.focus-mode #panel-dock{opacity:1;filter:none}
/* 专注模式：标题条改成 grid 三段式，标题真正落在整条的正中。
   原来靠 `flex:1` 让标题占"左组到右组之间"的剩余空间，但左侧只有 257px、右侧 459px，
   标题就被顶到偏左 100px 左右。只有两侧各 1fr 才会居中。

   ⚠️ 2026-09-14 修：中间那列原来是 `minmax(0,auto)`，而 `auto` 的**上限是 max-content** ——
   等于"标题有多长，整条就有多宽"，一旦超过容器，整条就撑破、左侧那组按钮被压到标题上。
   两个触发条件都实测过：
     · 标题长：真实讲次标题最长 **70 字**，1440 视口下只剩 30px 余量；
     · 窗口窄：连短标题都在 **~980px** 崩（1040 时还余 34px，960 时已经 -6px）。
   改成 `minmax(0,1fr)`：中间列的上限变成"三等分里的一份"，超了就交给 h1 自己的
   `truncate` 去省略。两侧仍是 `1fr`（= `minmax(auto,1fr)`），`auto` 下限保证按钮不被压扁。 */
.focus-mode #title-bar>.auto-bar-inner>header{
  display:grid;grid-template-columns:1fr minmax(0,1fr) 1fr;align-items:center;
  width:calc(100% - 24px);max-width:1440px;gap:14px;padding:10px 12px;border-radius:18px}
.focus-mode #title-bar .reader-tools{gap:14px;justify-self:start}
/* ⚠️ `justify-self:center` 是配套 `minmax(0,1fr)` 的，不能删：
   中列变成"三等分里的一份"之后，如果标题还按默认的 `stretch` 左对齐，
   短标题就会贴在那一份的左边（实测左移约 150px），等于把 09-13 那次"标题居中"白做了。
   改成 center 之后，标题块在自己那 1/3 里居中 —— 而那一份的中心**就是整条的中心**，
   所以短标题照样是正的；长标题撑满那一份、由 h1 的 `truncate` 收尾。
   非 grid（窄屏退回 flex）时这条被忽略，没有副作用。

   ⚠️ `max-width:100%` 是**安全阀**，也不能删：`justify-self:center` 下宽度走 fit-content，
   而 fit-content = `min(max-content, max(min-content, 可用宽))` —— 标题是 `nowrap` 的，
   它的 min-content 就等于 max-content，所以**可用宽那一项根本不起作用**，
   标题会按自己的内容宽居中、**两侧一起溢出网格列**。
   实测 980px 视口 + 药丸展开时中列只剩 ~85px，标题却铺到 159px，
   左边压到「习题速览」按钮上 19px（标题在 DOM 里靠后，画在按钮上面）。
   `max-width:100%` 按网格列宽夹住它，于是标题在最窄的列里也只会变成省略号，绝不会压到按钮。 */
.focus-mode #title-bar .reader-heading{justify-self:center;max-width:100%}
/* 时间条（`.focus-clock-bar` 的 52px 覆盖）2026-09-14 随时间条一起删了 */
.focus-mode #title-bar .reader-heading+div{gap:12px}

/* ============================================================
   共享标题条（`.course-bar`，原来给笔记本 / 错题本 / 刷题模式用）
   ------------------------------------------------------------
   2026-09-25 顶栏统一（V1）：最后一个用它的错题本也换成了阅读类页面那一版顶栏
   （`_topbar.html` 的 `doc_bar`），`_course_dock.html` 里的 `course_bar` 宏和这里那几条让位规则一起删了。
   错题本的卡片现在走下面那条通用的 `.page-scroll{padding-top:60px}` 让出药丸。 */

/* ============================================================
   标题条右侧那枚**药丸容器**（`#panel-dock.seg`）
   ------------------------------------------------------------
   2026-09-14 Allen 定稿：外形取「甲①」（padding 4px），展开动画取曲线 C。
   里面每一格都是「图标 + 悬停出标签」；鼠标移到哪一格，浅灰滑片就跟到哪一格。
   三格（笔记本 / 错题本 / 刷题模式）是**整页跳转**，AI 是开侧栏，所以 AI 前有分隔线。
   ============================================================ */
/* 弹簧参数提到 `:root`（2026-09-14 二改）：阅读页那两组 `reader-menu-expando` 也要用它，
   而它们在药丸**外面**、继承不到 `.seg` 上定义的变量。
   ⚠️ 只搬了 `--sp-*` 四条，`--seg-pad` 留在 `.seg` 里（那是药丸自己的内边距）。 */
:root{
  /* 展开那条：ζ=0.78 / ω=14 的弹簧响应采样成 CSS `linear()`（24 个点）。
     ζ=1 是临界阻尼（不过冲、但尾巴很长）；ζ<1 的那几 px 过冲就是"阻尼感"。
     收起那条**故意不是弹簧** —— 鼠标离开不该拖。 */
  --sp-C:linear(0, 0.0479 4.17%, 0.1601 8.33%, 0.3002 12.5%, 0.4446 16.67%, 0.5786 20.83%, 0.6948 25%, 0.7903 29.17%, 0.8651 33.33%, 0.9211 37.5%, 0.961 41.67%, 0.9881 45.83%, 1.0051 50%, 1.0146 54.17%, 1.019 58.33%, 1.0199 62.5%, 1.0187 66.67%, 1.0163 70.83%, 1.0135 75%, 1.0106 79.17%, 1.008 83.33%, 1.0057 87.5%, 1.0039 91.67%, 1.0025 95.83%, 1.0014 100%);
  --sp-out:cubic-bezier(.4,0,.2,1);
  --sp-dur:.58s;
  --sp-ease:var(--sp-C);
  /* 代码框（代码块 / 字符画引文、AI 侧栏的 ai-code、导图引用卡）共用的一套值。
     改这一组，三处一起走 —— 别各自再写一遍 rgba(60,72,66,.06)。
     ⚠️ 浅灰底、**不要黑底**（用户 2026-09-19 明确要求过）。 */
  --code-bg:rgba(60,72,66,.06);
  --code-fg:#2f3a34;
  --code-fg-dim:#7a827c;
  --code-radius:8px;
  --code-font:12.5px/1.65 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
}
.seg{
  --seg-pad:4px;
  position:relative;display:flex;align-items:center;gap:2px;flex-shrink:0;
  padding:var(--seg-pad);border-radius:99px;
  background:#fff;border:1px solid var(--border-color);
  box-shadow:0 0 1px 0 rgba(24,94,224,.15),0 6px 12px 0 rgba(24,94,224,.15);
}
/* 滑片：跟着鼠标走的那层浅灰底。位置/宽度由 reader-shell.js 逐帧写内联样式。
   ⚠️ `transition:none` 是**故意的**：容器展开时每格宽度每帧都在变，
   走 CSS transition 的话每次重设目标都会重新计时，越拖越慢。
   ⚠️ 别把 `transition` 删掉换成一条曲线 —— 会和 JS 弹簧打架。 */
.seg>.glider{
  position:absolute;top:var(--seg-pad);bottom:var(--seg-pad);left:0;width:0;
  border-radius:99px;background:#F1F1F0;transform:translateX(0);opacity:0;
  transition:none;
  will-change:transform,width;
}
.seg>*{position:relative;z-index:1}
/* 药丸里的一格：**去掉自己的边框/底色/投影**，外形由容器给；
   底色是滑片画的，所以 hover 只改文字颜色，不动 background（动了会盖住滑片）。 */
.seg .dock-btn{
  gap:0;border:0;background:none;box-shadow:none;border-radius:99px;
  height:30px;padding:6px 8px;color:var(--text-muted);
}
.seg .dock-btn:hover,
.seg .dock-btn[aria-expanded="true"]{
  color:var(--text-main);background:none;border:0;box-shadow:none;
}
/* **当前页那一格**：深色 + 一圈描边 + 一层浮起阴影。
   2026-09-14 Allen：「目前在的页面所对应的 button 也要展开，就像 ai 模式那里那样」
   → 「当前页面还要加个和 hover 效果一样的阴影框」。

   "和 hover 效果一样"指的是 `.dock-btn` 在药丸**外面**的 hover 外观（input.css 718-724）：
   描边从 `var(--border-color)` 变成 `#cfd6db`、再加一层 `0 10px 24px -12px` 的柔和投影。
   药丸里的格子平时把这三样都清了（底色交给滑片），**只有"当前页"这一格破例浮起来** ——
   它不是一个等着被点的入口，而是"你在这儿"。

   ⚠️ 描边用 `box-shadow: 0 0 0 1px` 画，**不是** `border`：格子高 30px 是算好的
   （`height:30px` + 药丸 `padding:4px` = 40px），加一条真 border 会把它撑到 32px、
   整枚药丸跟着长高。`0 0 0 1px` 画在盒子**外面**、不参与布局，视觉和 border 一样。
   ⚠️ `z-index:2`（`.seg>*` 那条给的是 1）：阴影是往外的，不抬一层会被**后面那些格子**
   （DOM 顺序在后、画在上面）切掉下半截。
   ⚠️ 底色仍然由滑片画（`background:none`），别改成 `background:#fff` ——
   滑片在格子**下面**，一填底色就把滑片盖没了。 */
.seg .dock-btn[data-seg-current]{
  color:var(--text-main);background:none;border:0;z-index:2;
  box-shadow:0 0 0 1px #cfd6db,
             0 1px 2px rgba(28,28,30,.06),
             0 10px 24px -12px rgba(28,28,30,.34);
}
.seg .dock-btn .ico{font-size:14px}
.seg .dock-badge{margin-left:5px}

/* ---------- 标签展开 ----------
   ⚠️ 这里换过三版，前两版都被"内容宽"夹住，记下来免得再踩：

   ① `max-width: 0 → 96px`（老写法，input.css 的 `.dock-btn > span`）：标签内容只有 52px，
      可见的展开只用到整条曲线的 **54%**。实测线性 500ms 在 311ms 就静止了。
   ② `grid-template-columns: 0fr → 1fr`：按内容宽插值，时序对了（同样 500ms → 509ms 走满），
      **但 `fr` 超过 1 之后会被内容宽夹平** —— 实测 C 只过冲 0.4px、D 只 1.8px。
      更糟的是：曲线一旦越过 1.0 就被夹住，尾巴直接丢光，
      D 反而在 **214ms** 就"到位"了（比 B 的 499ms 还快），跟"阻尼"完全相反。
   ③ **现在这版：标签宽度是 JS 量出来的 px 值**（`--w`，见 reader-shell.js 的 `measureLabels`）。
      宽度是自由变量，过冲时标签右侧多出几 px 空白 → 药丸会"鼓"一下再收回来，
      这才是真回弹。代价是宽度得量，字体加载完 / 窗口变化要重量。

   ⚠️ 展开和收起用两条曲线：进入 `:hover` 时浏览器取的是**新状态**那条 transition，
      所以弹簧写在 `:hover` 规则里，收起那条写在基础规则里。
   ⚠️ `var(--w, auto)` 的兜底故意是 `auto` 而不是一个 px 值：脚本没跑起来时
      `--w` 量不到，兜底写 px 会把「刷题模式」这种四字标签裁掉半个字。 */
.seg .lbl{
  /* ⚠️ `max-width:none` 是防回归用的，**真正的修在 input.css**：
     老规则 `.dock-btn > span:not(.dock-badge){max-width:0}` 会夹住这个标签
     （它也是 `.dock-btn` 的直接子 span），夹住之后 JS 量出来的宽度恒为 0、
     `--w` 一辈子是空的。那边已经加了 `:not(.lbl)` 把它排除。
     这里再写一条只能压住基础那条（0,2,0 > 0,1,1），压不住 `:hover` 那条（0,3,1）——
     所以别把 input.css 里那个 `:not(.lbl)` 删掉。 */
  max-width:none;
  flex:none;width:0;overflow:hidden;opacity:0;margin-left:0;
  /* 收起：走这条（鼠标离开不该拖） */
  transition:width .24s var(--sp-out),
             margin-left .24s var(--sp-out),
             opacity .14s ease;
}
.seg .lbl>span{white-space:nowrap;font-size:12px;font-weight:500}
/* 展开：走弹簧。两条**常显**的路子：
     · `[aria-expanded="true"]` —— AI 那格，侧栏开着时（`ai.js` 写这个属性）；
     · `[data-seg-current]` —— **当前页那一格**（2026-09-14 Allen：「目前在的页面所对应的
       button 也要展开，就像 ai 模式那里那样」）。它没有"开合"的概念，所以不用
       `aria-expanded`（那是给 `<button>` 的），改用 `data-seg-current` 这个纯标记。
   ⚠️ AI 那格**只在侧栏真开着时**展开（Allen 同一条消息里强调的："ai 栏没有悬浮的时候要收回"）
   —— 别为了"好看"把 AI 格也写成常显，那会让人以为侧栏开着。 */
.seg:hover .lbl,
.seg:focus-within .lbl,
.seg .dock-btn[aria-expanded="true"] .lbl,
.seg .dock-btn[data-seg-current] .lbl{
  width:var(--w, auto);opacity:1;margin-left:5px;
  transition:width var(--sp-dur) var(--sp-ease),
             margin-left var(--sp-dur) var(--sp-ease),
             opacity .2s ease;
}
/* 分隔线：把三格（整页跳转）和 AI（开侧栏）分开。
   ⚠️ 颜色**故意不用 `var(--border-color)`**（= #F0F0F0）：在纯白底上对比度只有 1.13:1，
   实测截图像素就是 (240,240,240)，**肉眼看不见** —— 等于没画，AI 还是像第四个 tab。
   药丸自己的描边也是这个色，但它旁边还有一圈 `rgba(24,94,224,.15)` 的投影撑形，
   分隔线没有那层，所以同样的色号在这儿就废了。
   取 `#E5E7EB`：和标题条上那条 `bg-gray-200` 的分隔线**同一个色号**（站内既有写法），
   实测像素 (229,231,235)，在白底上能读出来，又不至于在 40px 高的药丸里显得重。 */
.seg-sep{width:1px;height:18px;background:#E5E7EB;margin:0 4px;flex-shrink:0}
@media (prefers-reduced-motion: reduce){
  .seg .lbl{transition-duration:.01ms}
}
@media(max-width:900px){
 /* 窄屏放不下三段：退回 flex 换行，标题占满第二行 */
 .focus-mode #title-bar>.auto-bar-inner>header{display:flex;flex-wrap:wrap;width:calc(100% - 24px);gap:10px}
 .focus-mode #title-bar .reader-heading{flex-basis:calc(100% - 130px)}
 .focus-mode #title-bar .reader-heading+div{width:100%;justify-content:flex-end;gap:12px;flex-wrap:wrap}
 .focus-mode #panel-dock{flex-wrap:wrap}
}

/* 顶部导航：平时收着，鼠标贴到窗口最顶端那条细热区（#nav-hotzone）才展开，离开导航栏就收起。
   感应区不挂在标题栏上——标题栏近百像素高，挂在那儿等于"鼠标在上半屏随便一划就弹出来"。
   热区贴在文档最顶端，讲义在 iframe 里时它也在外层文档，指针事件一定收得到。
   时长分两组：基础规则是收起（快），.is-open 是展开（慢）。CSS 的 transition 取"当前状态"
   那一条，所以加 class 走展开曲线、去掉 class 走收起曲线，就是进场慢、出场快。 */
#reader-nav{display:grid;grid-template-rows:0fr;flex-shrink:0;
  transition:grid-template-rows .26s cubic-bezier(.4,0,1,1)}
#reader-nav.is-open{grid-template-rows:1fr;
  transition:grid-template-rows .38s cubic-bezier(.22,1,.36,1)}
.reader-nav-inner{overflow:hidden;min-height:0;opacity:0;transform:translateY(-12px);
  transition:opacity .18s ease,transform .26s cubic-bezier(.4,0,1,1)}
#reader-nav.is-open .reader-nav-inner{opacity:1;transform:translateY(0);
  transition:opacity .26s ease,transform .38s cubic-bezier(.22,1,.36,1)}
#nav-hotzone{position:absolute;top:0;left:0;right:0;height:14px;z-index:29}
/* 导航展开后热区让位：否则它会盖住导航顶上那 14px，把点击吞掉 */
#reader-nav.is-open~#nav-hotzone{pointer-events:none}
#reader-nav-toggle{position:absolute;top:0;left:0;width:100%;height:0;z-index:30;background:transparent;overflow:hidden}
#reader-nav-toggle:focus-visible{height:8px;background:#d8cedf;outline:2px solid #8c779d}
#course-root #title-bar{grid-template-rows:1fr;flex-shrink:0}
.focus-mode #reader-nav,.focus-mode #reader-nav-toggle,.focus-mode #nav-hotzone{display:none}
/* .is-open 的 transition 特异性更高，光写 #reader-nav 压不住它 */
@media(prefers-reduced-motion:reduce){
  #reader-nav,#reader-nav.is-open,.reader-nav-inner,#reader-nav.is-open .reader-nav-inner{transition:none}
}

/* ---------------------------------------------------------------- 顶栏让位（必须放最后）
   顶栏改成 `position:fixed` 浮在内容之上之后，**每个滚动容器都要自己让出顶栏占的高度**
   （= 12px 上边距 + 药丸高），否则页面顶部会被压住。
   2026-09-15 三药丸版把药丸从 56px 收到 **48px**，所以让位从 68px 变成 **60px**。

   ⚠️ **两套数并存，别统一**：三药丸版（`variant='pill'`，首页、Playground 首页
   和 Playground 项目页）是 60px；老版单胶囊（`variant='classic'`）**还是 68px**。
   它们共用 `.ff-scroll`，所以药丸版那两页要用页级类单独改回 60px。

   加在滚动容器**本身**、不是它的外层：这样内容是从药丸底下穿过去，而不是被一条空带子
   隔开——外层加 padding 的话那条带子不跟着滚，就露出空白。

   **必须放在文件最后**：后面那几条 @media 里有 `padding` 简写
   （`.home-main{padding:26px 24px 50px}`、`.course-overview .overview-grid{padding:28px 24px}`），
   媒体查询不加特异性、只比先后，写在中间就会被它们盖掉，窄屏直接破功。

   各页面的滚动容器（用无头 Chrome 量出来的，改布局前先重新量一遍）：
     /                          main.home-main
     /courses、/exercises …      main.home-main
     /c/<课程>/                  div.course-overview
     /playground/…                main.ff-scroll            ← 三药丸，60px
                                  （项目页 2026-09-17 起也走这一档；阅读页没有这个容器）
     /c/<课程>/notebook、/wrong  div.flex-1.overflow-y-auto（模板上带 .page-scroll，
                                 但在 .hub-shell 里 → 被 input.css 压成 0）
     /admin/…                   div.flex-1.overflow-y-auto（同上）
     /c/<课程>/?lecture=…       阅读页，顶栏没悬浮，不用让位 */
.home-side,.course-overview,.page-scroll{padding-top:60px}
.ff-scroll{padding-top:68px}      /* 兜底：还在用老版单胶囊的页面按这个让位（56 + 12）。
                                     2026-09-17 起只剩 /playground/notebook（全局笔记页）一页 */
.ff-pick-page .ff-scroll{padding-top:60px} /* Playground 首页复用主页三药丸（48 + 12） */
.ff-proj-page .ff-scroll{padding-top:60px} /* Playground 项目页 2026-09-17 也换成三药丸了（同上） */
.ff-nb .ff-scroll{padding-top:60px}        /* 全局笔记（/playground/notebook）2026-09-25 也换了，老版单胶囊从此没有页面在用 */
.home-main{padding-top:90px}      /* 它本来就有 30px 上内边距，叠加后观感和改之前一样 */

/* ⚠️ 首页的侧栏**不用让位**：`.home-page #site-nav` 的右边界读 `--home-side-w`，
   2026-09-13 侧栏撤掉之后那个变量是 0（见 calendar.css 末尾），顶栏本来就是通栏的。
   特异性 (1,1,0) 压过上面那条 (0,1,0)，不靠先后顺序。 */
.home-page .home-side{padding-top:0}

@media(max-width:1100px){
  /* 这条原来写的是 `padding:26px 24px 50px` 简写，会把上面的 90px 一起盖掉。
     这里只补 padding-top，另外三边沿用原来那组值。 */
  .home-main{padding:26px 24px 50px;padding-top:86px}
}
@media(max-width:640px){
  /* 窄屏首页整个 `.home-columns` 变成一个滚动容器（见上面那条 @media），
     让位要挪到它身上，里面两块恢复自己的上内边距。 */
  .home-columns{padding-top:60px}
  .home-side{padding-top:0}
  .home-main{padding-top:24px}
}

/* ---------------------------------------------------------------- 顶栏窄屏收起（三药丸版）
   一档一档地丢，丢的顺序是"最不要紧的先走"：
     ≤968px  丢掉「今日已学」（+ 它前面那条分隔线）—— 锦上添花，先走；导航内边距 20 → 14；
     ≤767px  3:1 站标槽 108 × 36 → **84 × 28**（仍是 3:1）；
     ≤700px  丢掉用户名；
     ≤639px  丢掉左格那枚返回键；classic 版（project 页）的钟表回到老行为；
     ≤479px  连站标槽也收起（只留标记不展开字标）。

   ⚠️ **这些数字不是随手取的，每一个都是"刚好放得下"的临界**（靠扫宽度实测出来的）：
     968  —— 968 往下右药丸会被列宽夹窄、用户名变成 "kokun…"（实测夹断带 **941–968**，
            按**小数**宽度量的：968 那档实际 67.83 / 自然 68.10，差 0.27px 也会出省略号）；
     700  —— 700 往下同样会夹断用户名（实测夹断带 640–660）；
     639  —— 左格那枚返回键在 500px 附近会被夹到文字漏出药丸；
     479  —— 站标槽那 90px 放不下四项导航。
   ⚠️⚠️ **这一档原来写的是 940，2026-09-15 挪到了 967** —— 因为**夹断带整个往上跑了**：
     这一档同时管两件事（钟表收起 + 导航内边距 20→14），所以它一档就是一个"台阶"：
     941 往上中格从 391.5px 跳到 **439.5px**（内边距回到 20），两侧 1fr 各被挤掉 24px。
     加上钟表本身 73.4px，**941–966 这一带右药丸装不下 [头像 + 用户名 + 分隔线 + 钟表]**，
     用户名就被省略成 "kokun…" / "kokund..."（截图确认过）。
     940 是**站标还是空占位**的时候量的（当时夹断带在 905–930）；站标填成真 logo 之后
     中格变宽、两侧变窄，带子跟着上移到 942–966。**改中格/站标的尺寸就要重新扫这一带。**
   **改动任何一项（药丸高、导航内边距、站标槽尺寸、收起断点）都会把这几条带子挪位置** ——
   跑一遍 `topbar3_probe.py`，它会把"药丸重叠 / 视口溢出 / 内容夹出药丸 / 用户名被夹断"
   四类症状全列出来。

   ⚠️ 约束式：`2 × max(左药丸, 右药丸) + 中药丸 + 2 × 12 ≤ 可用宽`。
   grid 是 `minmax(0,1fr) auto minmax(0,1fr)`，两侧**必须等宽**（否则中格不居中），
   所以取两侧里更宽的那个乘 2。
   ⚠️ 下面这些带 `#site-nav:not(.is-classic)` 的**前缀不能省**：裸写的话特异性只有 (0,1,0)，
   压不过上面那几条 (1,2,0)，窄屏直接不生效（实测踩过）。 */
@media(max-width:968px){
  #site-nav:not(.is-classic) .site-nav-clock,
  #site-nav:not(.is-classic) .nav-pill-sep{display:none}
  #site-nav:not(.is-classic) .nav-pill--me{padding:6px 10px 6px 6px}
  .top-nav-link{padding:0 14px}
}
@media(max-width:767px){
  /* 站标槽跟着窄屏缩到 84×28，**里面两枚图也要缩**，否则 36 的标记会顶破 28 高的槽。
     字标按比例给到 80 宽（原 96 × 84/108 ≈ 75，取 80 让它读得清一点）。 */
  #site-nav:not(.is-classic) .nav-logo-slot{
    width:84px;height:28px;--brand-mark:28px;--brand-word:80px}
  .top-nav-link{padding:0 10px}
}
@media(max-width:700px){
  #site-nav:not(.is-classic) .nav-me-name{display:none}
  #site-nav:not(.is-classic) .nav-pill--me{padding:6px}
}
@media(max-width:639px){
  #site-nav:not(.is-classic){margin:12px 10px 0}
  /* ⚠️ 左格那枚返回键在这一档收掉，**不是等到 ≤479**：它自带 80px 左右，
     两侧列一被压到 80px 以下，`max-width:100%` 就会把它夹窄、
     `white-space:nowrap` 的文字从药丸里漏出来。
     ⚠️ 2026-09-15 它从"空占位"变成**真按钮**之后**仍然留在这一档收掉** ——
     手机浏览器自己就有返回手势，顶栏这一格腾给站标 + 四项导航更划算。
     想让窄屏也留着的话，得同时重算下面几档的内边距（`.top-nav-link` 的 6/10/14）。 */
  #site-nav:not(.is-classic) .nav-back{display:none}
  /* classic 版钟表回到老行为：<640px（原来那个 Tailwind `hidden sm:flex`）收起。 */
  #site-nav.is-classic .site-nav-clock{display:none}
}
@media(max-width:479px){
  /* 手机竖屏：站标**只留标记，不展开**（没有 hover，长字标一展开就把药丸撑破）。
     做法是把 `--brand-word` 调到 0 —— JS 读到 0 会整个停掉动画，
     不然标记会缩到 0、又没东西顶上来，logo 凭空消失（这个坑在静态稿里看不见）。
     尺寸从 36 收到 26：375px 下标记 + 四项导航正好塞得下，靠探针的窄屏扫描钉住。
     ⚠️ 别把这档改回 `display:none`：Allen 的 logo 是**产品门面**，手机上不该整个没有。
     真要收，得连 `.top-nav-link` 的 padding 一起重算。 */
  #site-nav:not(.is-classic) .nav-logo-slot{width:26px;--brand-mark:26px;--brand-word:0px}
  /* 2026-09-18：五项导航（加了「足迹」）比当初按四项校准的宽度多一枚，
     375px 下光缩 top-nav-link 的内边距不够，中格 gap 也得跟着收，
     否则 me-toggle 会被顶出视口右边（跑 responsive_check.py 能看到）。 */
  .top-nav-link{padding:0 4px}
  #site-nav:not(.is-classic) .nav-pill--main{gap:0}
}

/* Neutral homepage：底色走全站统一的 `#FAFAFA`。
   这条规则真正的作用其实是**把 `.hub-aurora` 那四层极光渐变压掉**——
   `background` 简写会把 `background-image` 一起清空，所以这几页是纯色底。
   ⚠️ 别只删这一条：删了渐变会回来。别把它改成 `background-color`，同上。

   2026-09-15 Allen：全站页面底色统一为 `#FAFAFA`，原来这里是 `#e8e8e8`。
   顺带影响：`.peek-cover` / `.hcc-cover` / `.peek-burst` 都读 `var(--bg-color)`，
   在这几页也跟着从 `#e8e8e8` 变到 `#FAFAFA`（封面占位底色，本来就该跟页面走）。
   ⚠️ **`.home-preview-well` 那块凸起面板没跟着改**（还是 `#e0e0e0`）——
   它的配方要求"面板色贴近页面底色"，现在底变成 `#FAFAFA`，面板和底差了 26 级，
   会比原来显眼得多。见下面那条的注释。 */
.home-page.hub-aurora,
.hub-neutral-page.hub-aurora {
  --bg-color: #FAFAFA;
  background: #FAFAFA;
}
/* ⚠️ `padding-top` 18px 是**看着调出来的**，两头都别越界：
     · 太小 → 标题贴着面板上沿，"Preview" 像被切掉一截；
     · 太大 → Allen 说的"往上更贴近这个阴影框一点"就没做到。
   （2026-09-12 定的。改这里要连 `.peek-head h2` 的字号一起看——字号一变，可见边距跟着变。
   注：这条原来还受"面板顶部那条 inset 投影可见带约 19px"的限制；面板改成凸起式之后，
   那条约束不存在了，现在纯粹是观感取值。） */
.home-page .home-preview-well {
  padding: 18px 24px 0;
  border-radius: 30px;
  /* 凸起式 neumorphism（2026-09-12 Allen 给的配方：右下压深 + 左上提亮）。
     原来这里是**凹陷**的（`background:#dedee0` + 三条 `inset` 投影 + 一圈白色描边），
     现在反过来，面板从页面里"浮起来"。
     ⚠️ 下面三条都是**有意的删改**，别顺手加回来：
       · `border:1px solid rgba(255,255,255,.35)`——硬边白线会把柔和投影割断，
         neumorphism 靠的就是"没有轮廓线"；
       · 三条 `inset` 投影——和"凸起"方向相反，留着互相打架；
       · `background` 从 `#dedee0`（偏紫）换成 `#e0e0e0`（中性灰）——**面板色必须贴近
         页面底色 `#e8e8e8`，这个"凸起"才成立**，差得远了就变成一块贴上去的灰板子。
     ⚠️ 外阴影**不会被 `.home-main` 的 `overflow-y:auto` 裁掉**：那是滚动容器，
     `overflow-x` 会跟着变成 `auto` 并按 padding box 裁，但主栏左右内边距各 34px，
     阴影左右各伸 `15 + 30/2 = 30px`，装得下；窄屏 `.home-columns>main{overflow:visible}` 根本不裁。
     （窄屏主栏 padding 只有 24px，左边理论上切掉 6px——最外圈几乎透明，看不出来。
      真要动主栏的 padding，回头重算这一条。） */
  background: #e0e0e0;
  box-shadow: 15px 15px 30px #bebebe,
              -15px -15px 30px #ffffff;
}
@media (max-width: 640px) {
  .home-page .home-preview-well { padding: 16px 12px 0; border-radius: 24px; }
}

/* ============================================================
   课程安排面板（课程总览页的「课程安排」标签页，2026-09-13）
   ------------------------------------------------------------
   两块：每周上课时间（可增删）+ 期末考试时间。
   样式跟着 `.course-card` 那套走（白面 + hairline + 12px 圆角），
   只有行内布局是新的。前端见 js/course-schedule.js。 */
.class-list{display:flex;flex-direction:column;gap:6px;margin-bottom:14px}
.class-row{display:flex;align-items:center;gap:12px;padding:10px 12px;
  border:1px solid var(--border-color);border-radius:12px;background:#fff}
.class-day{font-size:13px;font-weight:600;color:#1c1c1e;min-width:36px}
.class-time{font-size:13px;color:#3a3a3c;font-variant-numeric:tabular-nums;min-width:98px}
.class-room{flex:1;min-width:0;font-size:12.5px;color:#8e8e93;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.class-room i{font-style:normal;color:#c7c7cc}
.class-row button{flex-shrink:0;width:28px;height:28px;display:grid;place-items:center;
  border:0;border-radius:50%;background:#f2f2f7;color:#8e8e93;cursor:pointer;
  transition:background .15s ease,color .15s ease}
.class-row button:hover{background:#e5e5ea;color:#1c1c1e}
.class-row .ico{width:14px;height:14px}

.class-add{display:flex;flex-wrap:wrap;align-items:center;gap:8px}
.class-add select,.class-add input{border:1px solid #dcd6e6;border-radius:9px;padding:8px 10px;
  font:inherit;font-size:13px;background:#fff;color:#1c1c1e}
.class-add select{min-width:80px}
.class-add input[name="room"]{flex:1;min-width:120px}
.class-add-dash{color:#c7c7cc}
.class-add button{background:#1c1c1e;color:#fff;border:0;border-radius:9px;
  padding:8px 14px;font-size:13px;cursor:pointer}
.class-add button:disabled{opacity:.5;cursor:default}
.class-empty{font-size:13px;line-height:1.7;color:#888;margin:0 0 12px}
[data-class-error]{font-size:12.5px;color:#a74444;margin:8px 0 0}

.exam-note{font-size:12.5px;line-height:1.6;color:#8e8e93;margin:0 0 10px}
.exam-row{display:flex;align-items:baseline;gap:10px;padding:10px 12px;margin-bottom:6px;
  border:1px solid var(--border-color);border-radius:12px;background:#fff}
.exam-when{font-size:13.5px;font-weight:600;color:#1c1c1e;font-variant-numeric:tabular-nums}
.exam-meta{font-size:12px;color:#8e8e93}
.exam-btn{margin-top:6px;background:#f2f2f7;color:#1c1c1e;border:0;border-radius:9px;
  padding:8px 14px;font-size:13px;cursor:pointer;transition:background .15s ease}
.exam-btn:hover{background:#e5e5ea}

/* ============================================================
   阅读页：文件名居中，操作从中间往两边展开
   ------------------------------------------------------------
   2026-09-14 二改（Allen）：「文件名需要居中，把讲义 / 笔记本 / 错题本 / 刷题模式 / AI
   都挪到标题的右边去，所以动画就变成了中间往两边展开，过渡要丝滑、有阻尼感」。

   结构 = 左组（阅读操作：返回 / 本讲大纲 / 导图 / 习题速览）· 文件名 · 右组（课程导航：
   讲义 / 笔记本 / 错题本 / 刷题模式 ｜ AI）。两组都从 `width: 0` 长到同一个
   `--reader-menu-open`，左组内容右对齐、右组左对齐 —— 于是两组一起从标题往外长，
   标题（`trigger`）自己**不参与动画**，全程钉在正中间。

   ⚠️⚠️ **两组必须共用一个宽度**。左组内容 ~308px、右组内容 ~368px，差 60px；
   要是各按自己的内容宽展开，标题会被推偏 (308-368)/2 = **30px**，而且整个展开过程一路在漂
   —— 那就不是"居中"了。
   但"共用宽度"会把差额变成**左端 60px 的空白**（右端只有 6px，一眼看得出两边不一样）。
   所以左组用 `flex:1` + `justify-content:space-between` **铺满自己那半格**：
   两端各 6px、完全对称，而且不依赖魔法数字（右组将来变宽，左组自动跟着铺开）。

   ⚠️ 过冲走 `--sp-C`（ζ=0.78 弹簧）。**能真过冲的前提是目标值是自由变量**：
   这里目标是 px，过冲多出来的那几 px 是空白，所以弹得出来；换成
   `max-width` / `Nfr` / `auto` 立刻被内容宽夹平（这条踩过两版，见 `.seg` 那段注释）。

   ⚠️ 窄视口靠 **flex-shrink 兜底**，不靠断点硬调：两个 expando 是 `flex:0 1 auto` +
   `min-width:0` + `overflow:hidden`，基宽相同 → 超宽时**两边等量收缩**、仍然对称，
   被切掉的是各自最外那一格（左：返回；右：AI）。药丸永远不会捅出视口。
   ⚠️ 所以别把 `flex:0 1 auto` 改回 `flex:none` —— 改回去窄视口就是硬溢出。

   2026-09-15 三改（Allen）：外观整体换成 tabs 稿 —— 拟物灰（#e0e0e0 + inset 阴影 +
   2px 灰边）→ **纯白底 + `rgba(24,94,224,.15)` 蓝影**（和 `.seg` 药丸同一副投影），
   悬停/当前格 = 浅蓝 `#e6eef9` + 蓝字 `#185ee0`，滑片同步换成浅蓝。
   **只换皮，结构与动画一字未动**：展开/收起仍是 --sp-C 弹簧（ζ=0.78，阻尼感），
   滑片仍是 seg-dock.js 的 JS 弹簧（逐帧积分，CSS transition 会和它打架，别加）。
   ============================================================ */
#course-root.focus-mode #title-bar{padding:10px 0 4px;background:transparent;overflow:visible}
#course-root.focus-mode #title-bar>.auto-bar-inner>header.reader-menu-shell{display:flex;justify-content:center;align-items:center;width:100%;max-width:none;margin:0;padding:0;border:0;border-radius:0;background:transparent;box-shadow:none}
#course-root.focus-mode #title-bar .reader-menu{--reader-menu-open:368px;display:flex;align-items:center;max-width:calc(100vw - 28px);padding:4px;border:0;border-radius:999px;background:#fff;box-shadow:0 0 1px 0 rgba(24,94,224,.15),0 6px 12px 0 rgba(24,94,224,.15);color:#1d1d1f}
/* 展开 / 收起两条曲线：弹簧写在 `:hover` 里，收起那条留在基础规则 —— 进入 `:hover` 时
   浏览器取的是**新状态**那条 transition，反了就变成"收起也弹"（鼠标离开不该拖）。 */
#course-root.focus-mode #title-bar .reader-menu-expando{display:flex;align-items:center;flex:0 1 auto;min-width:0;width:0;height:34px;overflow:hidden;opacity:0;pointer-events:none;transition:width .28s var(--sp-out),opacity .16s ease,transform .28s var(--sp-out)}
/* 左组贴标题（右对齐）、右组贴标题（左对齐）：展开时是"从标题往外露"，
   `translateX(±6px)` 再加一点点横向位移，两组一起往外推，比纯裁切更顺。 */
#course-root.focus-mode #title-bar .reader-menu-expando--left{justify-content:flex-end;transform:translateX(6px)}
#course-root.focus-mode #title-bar .reader-menu-expando--right{justify-content:flex-start;transform:translateX(-6px)}
#course-root.focus-mode #title-bar .reader-menu:hover .reader-menu-expando,#course-root.focus-mode #title-bar .reader-menu:focus-within .reader-menu-expando{width:var(--reader-menu-open);opacity:1;pointer-events:auto;transform:translateX(0);transition:width var(--sp-dur) var(--sp-ease),opacity .22s ease,transform var(--sp-dur) var(--sp-ease)}
/* ⚠️ `flex:none` 必须留：药丸撞到 `max-width:calc(100vw - 28px)` 时三个孩子一起被压，
   而 trigger 的默认 `flex-shrink:1` 会**先挤标题**（实测 860px 悬停态标题被截成省略号）。
   钉住它 → 收缩全落在两个 expando 身上，标题永远拿满自己的宽度。 */
.reader-menu-trigger{display:inline-flex;align-items:center;flex:none;min-width:0;height:38px;padding:0 15px;border:0;border-radius:999px;background:transparent;color:#1d1d1f;cursor:pointer;font:500 13px/1 inherit;transition:color .15s ease-in,background-color .15s ease-in}
/* 贴进来的 tabs 语言：可点的东西 hover 一律是「浅蓝底 #e6eef9 + 蓝字 #185ee0」，
   标题自己也不例外（它在触屏上就是展开菜单的按钮）。 */
.reader-menu-trigger:hover{color:#185ee0;background:#e6eef9}
.reader-menu-trigger:focus-visible{outline:2px solid #185ee0;outline-offset:3px}
.reader-menu-name{display:block;min-width:0;max-width:min(32vw,330px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* 左组：`flex:1` + `space-between` 把 4 个动作**均匀铺满**这半格。
   ⚠️ 这条是**对称的必需件**，别改回 `flex:none`：右组内容比左组宽 60px，
   左组要是按内容宽贴着标题放，左端就会空出 60px（右端只有 6px），一眼能看出两边不一样。
   铺满之后两端各 6px、左右完全对称，而且**不依赖魔法数字** ——
   哪天右组加了按钮、变宽了，左组自动跟着铺开。
   `gap:8px` 是**最小间距**（space-between 只在此基础上分配剩余空间）。 */
#course-root.focus-mode #title-bar .reader-menu .reader-tools{display:flex;align-items:center;gap:8px;flex:1 1 auto;min-width:0;justify-content:space-between;white-space:nowrap}
/* ⚠️ 动作按钮 `flex:none`：`.reader-tools` 是 `flex:1`，展开途中容器比内容窄，
   不加这条按钮会被压扁（inline-flex + nowrap 的文字会溢出按钮框，很难看）。
   保持原宽 → 溢出部分由 expando 的 `overflow:hidden` 裁掉。 */
#course-root.focus-mode #title-bar .reader-menu .reader-tools .dock-btn{flex:none;height:30px;padding:0 8px;border:0;border-radius:999px;background:transparent;box-shadow:none;color:#1d1d1f;font-size:12px;transition:color .15s ease-in,background-color .15s ease-in}
/* hover = 滑片底色（#e6eef9）+ 蓝字（#185ee0），和 tabs 稿一致；
   面板真开着的那格（本讲大纲 / 习题速览，aria-expanded=true）保持同一副高亮，
   相当于 tabs 里的 :checked。 */
#course-root.focus-mode #title-bar .reader-menu .reader-tools .dock-btn:hover,#course-root.focus-mode #title-bar .reader-menu .reader-tools .dock-btn[aria-expanded="true"]{transform:none;background:#e6eef9;box-shadow:none;color:#185ee0}
/* 原来独立浮在右侧的 .seg 现在是右组内部的一段，不再绘制第二个胶囊外壳。
   ⚠️ `gap:0` + `padding:6px 6px` 是**为对称让位**的：右组比左组宽，这两条各削一点，
   让 `--reader-menu-open` 能定得小一些（见文件头那段）。 */
#course-root.focus-mode #title-bar #panel-dock.seg{flex:none;gap:0;padding:0;border:0;background:transparent;box-shadow:none}
#course-root.focus-mode #title-bar #panel-dock.seg .dock-btn{height:30px;padding:6px 6px;color:#1d1d1f;transition:color .15s ease-in}
#course-root.focus-mode #title-bar #panel-dock.seg .lbl{width:var(--w,auto);opacity:1;margin-left:5px;transition:none}
/* 滑片从浅灰换成 tabs 稿的浅蓝 #e6eef9：底色交给滑片画，所以格子自己永远 transparent，
   hover / 当前页只改文字色（#185ee0）——动了 background 就把滑片盖住了。
   原来的 inset 灰圈 + 半透明白底（拟物时代的"当前页"标记）随主题一起撤掉。 */
#course-root.focus-mode #title-bar #panel-dock.seg>.glider{background:#e6eef9}
#course-root.focus-mode #title-bar #panel-dock.seg .dock-btn[data-seg-current]{box-shadow:none;background:none;color:#185ee0}
#course-root.focus-mode #title-bar #panel-dock.seg .dock-btn:hover,#course-root.focus-mode #title-bar #panel-dock.seg .dock-btn[aria-expanded="true"]{background:none;color:#185ee0}
/* 错题角标 = tabs 稿里的 .notification：平时浅蓝底蓝字，悬停/当前那格反成蓝底白字 */
#course-root.focus-mode #title-bar #panel-dock.seg .dock-badge{background:#e6eef9;color:#185ee0}
#course-root.focus-mode #title-bar #panel-dock.seg .dock-btn:hover .dock-badge,#course-root.focus-mode #title-bar #panel-dock.seg .dock-btn[data-seg-current] .dock-badge{background:#185ee0;color:#fff}
/* ---------- 窄视口：四级降档 ----------
   ⚠️ 宽度是**两组共用**的，而 `--reader-menu-open` 必须 ≥ **更宽那组**的内容宽，
   否则那一组会被 `overflow:hidden` 从外面切掉半个字。所以每一级都要同时：
   ① 降 `--reader-menu-open`；② 把**右组**（更宽那组）的内容也压到那个值以内。
   只藏左组标签是没用的 —— 宽度不由它决定。 */
@media(max-width:1000px){
  /* 344：右组 padding-inline 6→5（省 10）、`.lbl` 左边距 5→3（省 10）、
     分隔线外边距 4→2（省 4）。左组跟着铺满，不用动。 */
  #course-root.focus-mode #title-bar .reader-menu{--reader-menu-open:344px}
  #course-root.focus-mode #title-bar #panel-dock.seg .dock-btn{padding-inline:5px}
  #course-root.focus-mode #title-bar #panel-dock.seg .lbl{margin-left:3px}
  #course-root.focus-mode #title-bar #panel-dock.seg .seg-sep{margin-inline:2px}
  #course-root.focus-mode #title-bar .reader-menu .reader-tools{gap:6px}
  #course-root.focus-mode #title-bar .reader-menu .reader-tools .dock-btn{padding:0 6px}
}
@media(max-width:920px){
  /* 300：再收就把 **AI 那格的文字** 收掉（图标 + `title` 原生提示留着）。
     这一档右组内容 = 5 个图标 + 分隔线 ≈ 300。 */
  #course-root.focus-mode #title-bar .reader-menu{--reader-menu-open:300px}
  #course-root.focus-mode #title-bar #panel-dock.seg #ai-toggle .lbl{display:none}
}
@media(max-width:830px){
  /* 160：两组都收成纯图标。右组 ≈139、左组 ≈116，都放得下。 */
  #course-root.focus-mode #title-bar .reader-menu{--reader-menu-open:160px}
  #course-root.focus-mode #title-bar .reader-menu .reader-tools .dock-btn>span{display:none}
  #course-root.focus-mode #title-bar #panel-dock.seg .lbl{display:none}
}
@media(max-width:760px){
  .reader-menu-name{max-width:170px}
}
@media(prefers-reduced-motion:reduce){#course-root.focus-mode #title-bar .reader-menu-expando{transition:none}}

/* ============================================================ 讲义页顶栏（2026-09-25 起走 V1「三島」）
   2026-09-15 那版「课程页 → 讲义页」morph 药丸的整组规则（`#mainPill.nav-pill--reader` / `.cell` /
   `[data-reader-action]` / 固定定位的 `.site-nav-center` / 32px 返回键 …）已随 V1 删掉：
   阅读类页面的顶栏现在是 `_topbar.html` 的 `doc_bar` 宏，样式全在 `css/navbar-v1.css`。
   这里只留和顶栏形状无关、页面自己要的几条。

   ⚠️ `#reader-nav` / `#title-bar` 这两棵子树**只隐藏、不删**：`reader-shell.js` 靠
      `getElementById('title-bar')` / `'panel-dock'` / `'reader-nav'` 拿节点，删了那几段逻辑整个不跑。 */
#course-root.focus-mode #reader-nav-toggle,
#course-root.focus-mode #nav-hotzone,
#course-root.focus-mode #title-bar{display:none}
#course-root.focus-mode #reader-nav{display:none}

/* ---- 顶栏让位 ----
   顶栏浮在页面上，正文让出 72px（24 上边距 + 药丸高 48）；≤860 顶距收到 12，让位跟着收到 64（见文件末尾）。 */
@media(min-width:861px){
  #course-root main{padding-top:72px}
}

/* ============================================================ 收藏（★）
   2026-09-17：题卡上多一枚收藏按钮（js/quiz.js 的 `.q-star`），错题本多一档「收藏」
   （列表里那些条目由 views.wrong_book 把 quiz_wrong + quiz_star 合并出来）。

   ⚠️ 选择器**必须写到 `.lecture .quiz .q-star` 这一档（0,3,1）**。
      `lecture.css` 里那条 `& button` 编译出来是 `.lecture .quiz button`（0,2,1），
      而 `lecture.css` 是页面级样式、排在 `updates.css` **后面**（base.html 把
      `{% block head %}` 放在两个全局样式之后）。只写 `.q-star` 的话颜色、描边、
      内边距会被它整条吃掉 —— `updates.css` 里原有的 `.q-book` 就是这么变成死规则的
      （它只写了 (0,1,0)，一条都没生效，所以"加入错题本"看起来和"自评错误"一模一样）。
      改这里之前先想清楚新写的规则到底赢不赢。

   配色沿用站里已有的那档"芥末金"（`#d4aa60`，原本是习题"已查看/待订正"的底色）：
   收藏是**标注**，不是第四种作答状态，所以只占一个色相、不改几何。 */
.lecture .quiz .q-star{
  color:#7d6c47;border-color:rgba(190,160,90,.34);background:rgba(255,252,244,.62)}
.lecture .quiz .q-star:hover{
  color:#6b5a2f;border-color:rgba(190,160,90,.58);background:#fffdf7}
.lecture .quiz .q-star.on{
  color:#8a6a1f;border-color:rgba(212,170,96,.55);background:#fff8ea;font-weight:700}
.lecture .quiz .q-star.on:hover{background:#fff3d9}
.lecture .quiz .q-star:focus-visible{outline:2px solid rgba(212,170,96,.55);outline-offset:2px}
.lecture .quiz .q-star:disabled{opacity:.55;cursor:default}

/* 错题本卡片头部的 ★ 徽章：和 `.wrong-count` / `.wrong-solved` 同一套几何，
   只换颜色。形状不变是故意的——三个徽章并排，读者先看形状再看颜色。 */
.wrong-star{
  font-size:11px;font-weight:700;letter-spacing:.03em;
  padding:3px 9px;border-radius:999px;
  background:rgba(212,170,96,.18);color:#8a6a1f}

/* ⚠️ 这三个徽章和删除键是**常驻 DOM、只切 hidden** 的（wrong.js 的 paintBadges），
   所以 hidden 必须真的能藏住。`.wrong-drop` 自己写了 `display:flex`，作者样式
   压得住 UA 的 `[hidden]{display:none}` —— 不补这条，纯收藏条目的删除键会一直露着。 */
.wrong-badges > [hidden]{display:none}

/* 「收藏」那一档选中时给一点同色相的金，和另外三档（紫系）分开：
   它列的是另一类东西，不是错题的第四种状态。 */
.wrong-filter.on[data-f="star"]{
  color:#8a6a1f;
  box-shadow:0 1px 2px rgba(138,106,31,.08),0 6px 16px -12px rgba(138,106,31,.45)}

/* ---- 手机 / 窄屏的阅读页（≤860px）----
   顶栏窄屏那几档（收图标 / 手机「当前 ⌄」）2026-09-25 起在 `css/navbar-v1.css`，按顶栏自己的宽度判。
   这里只留两条页面自己的：
   1. 讲义页正文让位跟着顶距收到 64（12 + 48 + 4）。
   2. ≤767 AI 侧栏是盖满全屏的浮层：侧栏往下让出顶栏，顶栏右岛那枚 AI 就是收起键
      （不然手机上打开 AI 之后没有任何能点的地方把它关掉）。 */
@media(max-width:860px){
  #course-root main{padding-top:64px}
}
@media(max-width:767px){
  :is(#course-root.focus-mode,#ff-root.ff-reader).ai-open .ai-side{height:calc(100% - 80px);margin:68px 12px 12px}
}

/* ---- 视口高度：100vh → 100dvh（2026-09-17）----
   Allen 在 iPad 的 Chrome 上看讲义页：AI 侧栏底下的输入框整个看不到。
   原因：全站的整页容器都是 Tailwind 的 `h-screen`（= `height:100vh`，body 本身也是），
   而 iPad / 手机浏览器的 `100vh` 按"地址栏、工具栏都收起来"时的**最大**视口算，
   比实际能看到的区域高出一截 —— 页面底部（输入框、底部按钮）就落到浏览器工具栏后面去了，
   body 又是 `overflow-hidden`，滚也滚不出来。
   `100dvh` 跟着工具栏的实际显隐变，电脑浏览器上和 100vh 完全一样。
   不支持 dvh 的老浏览器仍按上面 tw.css 的 100vh 走。
   ⚠️ 新写整页高度时直接用 `h-screen` 就行（这条已经兜住）；自己写 `100vh` 的话记得配一条 `100dvh`。 */
@supports (height:100dvh){
  .h-screen{height:100dvh}
}

/* ---- 窄屏：标题条不许把整页撑宽（2026-09-17）----
   刷题页 / 错题本那条 `.course-bar #title-bar` 是单列 grid，grid 子项默认 `min-width:auto`，
   列宽会被里面那枚胶囊（不换行的按钮组）的最小内容宽度撑开 —— 375px 的手机上撑到 385px，整页左右晃。
   `min-width:0` 让列宽回到容器宽度，胶囊自己在 `width:calc(100% - 32px)` 里排。 */
#title-bar>.auto-bar-inner{min-width:0}


/* 笔记本 / 学习足迹里"代码块、字符画"的引文（2026-09-19）：阅读页里从 <pre> 划下来的高亮
   （anchor 带 pre），原样等宽显示，换行和对齐空格都保留；太宽就在框里横向滚（data-scroll-x）。
   用在 hub-util.js renderQuote（课程笔记本 / Playground 笔记本）、_nb_default.html、footprint.html。 */
pre.quote-pre{margin:0;max-width:100%;overflow-x:auto;white-space:pre;overflow-wrap:normal;
  font:var(--code-font);color:var(--code-fg);
  background:var(--code-bg);border-radius:var(--code-radius);padding:10px 12px;tab-size:4}

/* 长代码块引文的「展开 / 收起」。按钮由 `hub-util.js` 的 `Hub.clampPre()` 插在 pre 后面，
   三处代码框共用（课程 / Playground 笔记本、导图引用卡）。样式放在这儿 —— 这些页面
   各自只引自己那份 CSS，只有 updates.css 是每页都有的。 */
.hub-pre-more{display:block;margin:5px 0 0;padding:0;border:0;background:none;
  font:inherit;font-size:11px;line-height:1.4;color:var(--code-fg-dim);cursor:pointer}
.hub-pre-more:hover{color:var(--code-fg)}

/* 自测题英文版（2026-09-23）：题里中文包在 .q-zh、英文在并列的 .q-en（格式见 CONTENT_FORMAT.md
   「自测题的英文版」）。默认只显示中文；外层带 data-qlang="en" 时换成英文——目前只有刷题页有切换键。
   不用 display:revert：.meta 等元素有作者样式，revert 会连它们一起丢掉。 */
aside.quiz .q-en { display: none; }
[data-qlang="en"] aside.quiz div.q-en, [data-qlang="en"] aside.quiz p.q-en { display: block; }
[data-qlang="en"] aside.quiz span.q-en { display: inline; }
[data-qlang="en"] aside.quiz:has(.q-en) :is(.q-zh, .meta:not(.q-en)) { display: none; }
aside.quiz :is(.q-zh, .q-en) > :first-child { margin-top: 0; }
aside.quiz :is(.q-zh, .q-en) > :last-child { margin-bottom: 0; }
