/* 我的电影库 —— 样式（原生 CSS，零依赖、无构建、无外部字体）。
 *
 * 三条与设计文档直接相关的视觉决定：
 *   - 深色底：海报是唯一的主角，浅底会把它们压灰。
 *   - 墙上卡片的「为什么推荐」默认隐形，鼠标悬停才浮出来（spec §11：解释的
 *     存在感要低，用户要的是逛，不是读推荐报告）。
 *   - 星级控件是**半星**的：左半边 = x.5，右半边 = x（spec §6.2 的 0.5 粒度）。
 */

:root {
  --bg: #101216;
  --bg-soft: #171a20;
  --card: #1b1e25;
  --line: #2b3038;
  --text: #e7eaf0;
  --dim: #9aa3b2;
  --accent: #4c8dff;
  --gold: #ffc857;
  --warn: #ffb454;
  --danger: #ff6b6b;
  --ok: #4fd48a;
  /* ★ 四个答案的语义色（2026-10-18）★
   *
   * 之前「想看 / 不感兴趣」是两个长得一模一样的灰按钮，而它们是首页上
   * **唯一要做的事**。四个答案分成两组（没看过 ×2、看过 ×2），分组靠的是文字
   * 标签而不是颜色；颜色只负责让「我想看」与「我不想看」在一瞥之间分得开。
   *
   * 两条纪律：
   *   - **不靠颜色单独承载语义**：按钮文字本身就是完整的答案，颜色是加强 ——
   *     色弱用户与这块深色底都不会因此失去信息（与时间线那四种标记同一条规矩）；
   *   - 取低饱和的强调色，不用纯红纯绿：这一屏的主角是海报，按钮抢戏会让整页
   *     看起来像一张表单。
   */
  --want: #35d0a5;
  --nope: #e2707d;
  /* ★ 星级控件的尺寸（用户报告：星星太小，容易打错分）★
   *
   * 一颗星 40px 宽 ⇒ 每个半星的命中区 20px（左半边 = x.5，右半边 = x）。
   * 原来一颗星只有 18px、半星 9px —— 那不是「小一点」，那是一台产错分的机器：
   * 错点一下会往那份**无法重建**的评分表里写一个错分，而错分会进权重模型。
   *
   * 三个值放在这里而不是散在下面：字形宽度、悬停预览的偏移、星格宽度必须是
   * **同一个数**，否则悬停预览会和它要预览的那一半错位（预览是把同一个字形平移
   * 半个星格宽再裁给右半边按钮的）。 */
  --star-size: 40px;
  --star-height: 40px;
  --star-glyph-size: 32px;
}

* { box-sizing: border-box; }

/* ★ 全局：hidden 属性必须**永远**管用 ★
 *
 * 浏览器默认样式里有一条 `[hidden] { display: none }`，但它是**作者样式之前**
 * 的规则 —— 我们这个文件里任何一条 `display` 都能盖掉它。`.stars` 就撞在这上面：
 * `.stars { display: flex }` 让「看过 · 打分」展开的五星控件永远摊开着，
 * `box.hidden = !box.hidden` 切了个没有任何视觉效果的属性（spec §11 要求它
 * 默认收着）。三张视图的每一张卡片都中招，而所有测试都是绿的 —— 因为测试只拼
 * HTML 字符串，从不解析 CSS。
 *
 * 修法有两种，这里选**通用**的那种：
 *   - `[hidden] { display: none !important }` —— 任何元素被 hidden 都会消失，
 *     不管它自己的 display 写的是什么。下一个给控件加 display 的人不会再踩一次。
 *   - `.stars[hidden] { display: none }` —— 只修这一个元素，下一个 display
 *     会原样重演同一个 bug，而且不会有测试响。
 *
 * 放在这里（重置附近）而不是文件末尾：规则本身不依赖位置（有权重更高的
 * `!important`），但要让人一眼看到「这个东西是全局约定」，别在下面再写一条
 * 同名规则把它覆盖掉。
 *
 * 注意 `!important` 不是可有可无的：`[hidden]` 与 `.stars` 的权重相同（都是
 * 0-1-0），少了它就只能靠**先后顺序**取胜，而顺序是最容易在后续改动里被破坏
 * 的东西。tests/test_frontend.py 有一条测试把这个层叠结果真的算一遍。 */
[hidden] { display: none !important; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--text);
  font-family: "Microsoft YaHei", "PingFang SC", "Hiragino Sans GB", sans-serif;
  font-size: 15px;
  line-height: 1.6;
}

a { color: var(--accent); text-decoration: none; }
code { background: #0c0e12; border: 1px solid var(--line); border-radius: 4px; padding: 1px 6px; }

/* ---------------------------------------------------------------- 顶栏 */

.topbar {
  position: sticky;
  top: 0;
  z-index: 20;
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 10px 18px;
  background: rgba(16, 18, 22, 0.92);
  border-bottom: 1px solid var(--line);
  backdrop-filter: blur(6px);
}

.brand { font-size: 17px; font-weight: 700; letter-spacing: 1px; }

.nav { display: flex; gap: 6px; }

.nav-btn {
  padding: 6px 14px;
  background: transparent;
  color: var(--dim);
  border: 1px solid transparent;
  border-radius: 999px;
  cursor: pointer;
  font-size: 14px;
  font-family: inherit;
}
.nav-btn:hover { color: var(--text); background: var(--bg-soft); }
.nav-btn.active { color: var(--text); background: var(--bg-soft); border-color: var(--line); }

.topbar-right { margin-left: auto; display: flex; align-items: center; gap: 8px; }

.badge {
  font-size: 12px;
  padding: 4px 10px;
  border-radius: 999px;
  border: 1px solid var(--line);
  color: var(--dim);
  white-space: nowrap;
}
.badge-ok { color: var(--ok); border-color: #245c3f; }
.badge-warn { color: var(--warn); border-color: #5c4524; }

/* ★ 徽章现在是一颗按钮（2026-10-19 用户反馈：「不能点进去，由我自己去排」）★
 *
 * 它原来是一个 <span>：状态读数，点它什么都不发生 —— 在用户眼里就是「按钮坏了」。
 * 现在点它直接进夯拉榜。视觉上仍然是一个徽章（不抢顶栏的注意力），但要给出
 * 三个「我可以点」的信号：鼠标手型、悬停变色、末尾一个小箭头。
 *
 * `font-family: inherit` 不是装饰：<button> **不继承**字体（与 .starrow 同一条
 * 坑），少了它这颗徽章会变成系统默认的无衬线体，在一行中文里非常突兀。
 */
button.badge {
  font-family: inherit;
  background: transparent;
  cursor: pointer;
}
button.badge::after { content: " ›"; color: var(--dim); }
button.badge:hover {
  color: var(--text);
  background: var(--bg-soft);
  border-color: #3d4451;
}
button.badge:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ---------------------------------------------------------------- 按钮 */

.btn {
  display: inline-block;
  padding: 5px 11px;
  background: var(--bg-soft);
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: 6px;
  cursor: pointer;
  font-size: 13px;
  font-family: inherit;
  line-height: 1.4;
}
.btn:hover { border-color: #3d4451; background: #1e222a; }
.btn-quiet { color: var(--dim); }
.btn-primary { background: var(--accent); border-color: var(--accent); color: #06101f; font-weight: 700; }
.btn:disabled { opacity: 0.45; cursor: not-allowed; }

/* ---------------------------------------------------------------- 横幅 */

.banners { padding: 0 16px; }

.banner {
  margin: 10px 0 0;
  padding: 10px 14px;
  border: 1px solid var(--line);
  border-left-width: 4px;
  border-radius: 8px;
  background: var(--bg-soft);
  font-size: 14px;
}
.banner p { margin: 4px 0 0; color: var(--dim); }
.banner strong { color: var(--text); }
.banner-warn { border-left-color: var(--warn); }
.banner-info { border-left-color: var(--accent); }
.banner-ok { border-left-color: var(--ok); }
.banner-setup { border-left-color: var(--accent); }
/* spec §27：库文件自检没通过 —— 这是全站唯一一个「你的数据可能坏了」的提示，
   用 --danger（而不是 --warn）与「离线 / 池子不完整」那几条明确区分开。 */
.banner-danger { border-left-color: var(--danger); }
.banner-danger strong { color: var(--danger); }
.banner .hint { margin-top: 8px; }
.banner .hint code { color: var(--gold); font-size: 13px; }

.config-form { display: flex; flex-wrap: wrap; gap: 10px; align-items: flex-end; margin-top: 10px; }
.config-form label { display: flex; flex-direction: column; font-size: 12px; color: var(--dim); gap: 4px; }
.config-form input {
  width: 340px;
  padding: 6px 10px;
  background: #0c0e12;
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: 6px;
  font-family: inherit;
  font-size: 14px;
}
.form-error {
  margin-top: 10px;
  padding: 8px 12px;
  border: 1px solid #5c2b2b;
  border-radius: 6px;
  background: #241417;
  color: #ffb3b3;
  font-size: 13px;
  white-space: pre-wrap;
}

/* ---------------------------------------------------------------- 视图容器 */

.app, #app { padding: 14px 16px; }

.view-head {
  display: flex;
  align-items: baseline;
  gap: 14px;
  flex-wrap: wrap;
  margin-bottom: 10px;
}
.view-head h2 { margin: 0; font-size: 18px; }
.view-head .count { color: var(--dim); font-size: 13px; }
.empty { color: var(--dim); padding: 40px 0; text-align: center; }
.dim { color: var(--dim); font-size: 13px; }

/* ---------------------------------------------------------------- tab */

.tabs { display: flex; gap: 6px; }
.tab {
  padding: 6px 16px;
  background: transparent;
  color: var(--dim);
  border: 1px solid var(--line);
  border-radius: 999px;
  cursor: pointer;
  font-size: 14px;
  font-family: inherit;
}
.tab:hover { color: var(--text); }
.tab.active { color: #06101f; background: var(--accent); border-color: var(--accent); font-weight: 700; }

/* ---------------------------------------------------------------- 海报墙网格 */

/* ★ 列宽下限从 178px 提到 232px（用户报告 1 的连带后果，量过才改的）★
 *
 * 一张卡片的内容宽度 = 列宽 - 卡片左右边框 2px - `.card-body` 的左右内边距 18px。
 * 178px 的列 ⇒ 内容只有 158px，而放大后的星级控件**自己就要 200px**
 * （5 颗 × 40px）。卡片是 `overflow: hidden` 的，装不下不会撑破页面 ——
 * 它会安静地把第 5 颗星裁掉，而那是最糟的一种坏法：看得到、点不到。
 * 232px 的列 ⇒ 内容 212px，控件（200px）加得上，还剩 12px 余量。
 * 「每页 10 部」让每行少几张卡片，本来也正合用户「别太挤」的意图。 */
.wall-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(232px, 1fr));
  gap: 14px;
}

/* 折叠区用的是**同一个卡片组件**（也带星级控件），所以下限必须一样 */
.wall-grid-compact { grid-template-columns: repeat(auto-fill, minmax(232px, 1fr)); }

.low-group {
  margin-top: 22px;
  border-top: 1px dashed var(--line);
  padding-top: 12px;
}
.low-group > summary {
  cursor: pointer;
  color: var(--warn);
  font-size: 14px;
  padding: 6px 0;
}
.low-note { color: var(--dim); font-size: 13px; margin: 6px 0 14px; }

/* ---------------------------------------------------------------- 卡片（唯一组件） */

.card {
  display: flex;
  flex-direction: column;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 10px;
  overflow: hidden;
  transition: border-color 0.12s ease;
}
.card:hover { border-color: #3d4451; }

/* ★ 用户请求：「有些电影它很特殊，是我会想反复观看的」★
 *
 * 这些片必须**不打开任何东西**就能在海报墙上一眼认出来 —— 所以有两处标记：
 * 一个角标（说清楚是什么），一条金色描边（在整面墙的灰边框里一眼跳出来）。
 * 只给角标的话，缩略图一多就沉进画面里了；只给描边的话，用户不知道金边是什么意思。
 * 两者都用 --gold：它在这个界面里已经是「你特别在意的东西」的颜色
 * （星级填充、贡献值、大众评分徽章）。 */
.card-rewatch { border-color: var(--gold); }
.card-rewatch:hover { border-color: var(--gold); }

.poster-wrap { position: relative; background: #0c0e12; }
.poster { display: block; width: 100%; aspect-ratio: 2 / 3; object-fit: cover; }

.badge-score {
  position: absolute;
  left: 6px;
  top: 6px;
  padding: 2px 7px;
  border-radius: 5px;
  background: rgba(8, 10, 14, 0.82);
  color: var(--gold);
  font-size: 12px;
  letter-spacing: 0.3px;
}

/* 「会反复观看」的角标。左上角已经被契合度占了，所以它放右上角 ——
   两个角标是两件不同的事，绝不能互相盖住。反色（金底深字）是刻意的：
   整面墙上只有这些片有它，而它必须比旁边那圈灰边框响得多。 */
.badge-rewatch {
  position: absolute;
  right: 6px;
  top: 6px;
  padding: 2px 7px;
  border-radius: 5px;
  background: rgba(255, 200, 87, 0.94);
  color: #14171c;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.3px;
}

/* `.card-body` 必须**长满**卡片剩下的高度（`flex: 1 1 auto`），这不是为了好看：
 * 卡片流与海报墙的卡片是网格项，同一行的卡片高度由**内容最高的那张**决定，其余的
 * 被拉伸到同一高度。少了这一条，拉伸出来的空白会留在卡片底部、`card-body` 自己
 * 只有内容那么高，于是 `margin-top: auto` 无处可推 —— 同一行里「片名一行」的卡片
 * 四个按钮会比「片名两行」的卡片高 19.17px（CDP 实测），一页 10 张扫过去就是锯齿。
 * 长出这一条之后，同一行的按钮 y 完全一致。 */
.card-body { flex: 1 1 auto; padding: 8px 9px 10px; display: flex; flex-direction: column; gap: 4px; }
.card-title { margin: 0; font-size: 14px; line-height: 1.35; }
.card-meta { margin: 0; color: var(--dim); font-size: 12px; }
/* ★ 用户请求：简介要上卡片，但不能把卡片撑成一堵墙 ★
 *
 * 简介来自 TMDB，长度不受控（这个库里最长的一条 130 多字）。三件事一起做才算
 * 「封顶」：
 *   1. **按行截**（-webkit-line-clamp），末行带省略号 —— 不是按字符切：那样会
 *      切在词中间，看起来像内容丢了；
 *   2. 盒子高度**钉死**在声明的行数上（height = 行高 × 3 = 4.35em）。少了它，
 *      「有简介」与「没有简介（只有一行『（TMDB 没有提供简介）』）」的卡片会差
 *      一行的高度，一行行看下去就是参差不齐；
 *   3. 长单词强制断开（overflow-wrap）：一条长长的英文原片名不该把格子撑破。
 * 3 行是「说清楚这是什么片」与「一页 10 张不要变成一堵墙」之间的取舍点。 */
.card-overview {
  margin: 2px 0 0;
  color: var(--dim);
  font-size: 12px;
  line-height: 1.45;
  height: 4.35em;              /* = 3 行 × 1.45 行高（跟着 font-size 走） */
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
  text-overflow: ellipsis;
  overflow-wrap: anywhere;
}

/* ★ 用户请求：推荐理由要**写出来**，不是悬停才浮出来 ★
 *
 * 这里原来是三条规则（spec §11 旧版「解释的存在感要低」）：
 *
 *     .card-wall .reasons { opacity: 0; max-height: 0; overflow: hidden; ... }
 *     .card-wall:hover .reasons,
 *     .card-wall:focus-within .reasons { opacity: 1; max-height: 8em; }
 *
 * 用户这一轮明确推翻了那个设计（「还要附带你推荐给我的理由是什么」），所以它们
 * 是被**整条删掉**的，而不是把 opacity 改成 1 留在原地：两套行为并存的样式，
 * 下一个人分不清哪一套在生效，改一处就漏一处。
 *
 * 取而代之的只有一件事：默认就看得见。理由的**条数**由 app.js 的 reasonsHtml()
 * 决定（signal 模式给前 3 条；insufficient 模式一条都不给，那时没有可解释的口味），
 * 样式这边不再参与「显示几条」这个决定。 */

/* 「数据不够，仅供参考」：confidence === "low" 的片在墙上列理由时必然带这一行。
   逐字用的是折叠区标题（`.low-group > summary`）那句 —— 同一个意思不写两遍
   不同的字，而且 --warn 在这个界面里就是「这条信息要打个折扣」的颜色。 */
.reasons-low { color: var(--warn); font-size: 12px; }

.reasons { list-style: none; margin: 2px 0 0; padding: 0; font-size: 12px; color: var(--dim); }
.reasons li { display: flex; justify-content: space-between; gap: 8px; }
.why-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.why-value { color: var(--gold); font-variant-numeric: tabular-nums; }
.reasons-status { color: var(--ok); font-size: 12px; }

.actions { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 6px; }
/* 卡片里的动作按钮**钉在卡片底部**：同一行里有的片名一行、有的两行、理由条数也
   可能不同（signal 模式给 3 条、没命中特征时是 1 行说明），不钉住的话四个按钮会
   停在各不相同的 y 上，一页 10 张扫过去就是锯齿。`.card-body` 是纵向 flex，
   `margin-top: auto` 吃掉多出来的空间；卡片没被拉伸时它正好是 0，间距由 gap 给。
   （只作用于卡片：详情页的 `.actions` 在块级容器里，auto 本来就是 0。） */
.card-body .actions { margin-top: auto; }
.actions .btn { padding: 3px 7px; font-size: 12px; }

/* ---------------------------------------------------------------- 星级（半星）
 *
 * ★ 用户报告：星星太小，容易打错分 ★
 *
 * 结构与几何的关系（改之前先读懂，这几行是配套的）：
 *
 *   1. 星格 `.star` 宽 = --star-size（40px），两个 `.star-hit` 各占 50%（20px）。
 *      命中区就是屏幕上看得见的那一半 —— 「点哪儿」和「填哪儿」是同一个盒子。
 *   2. 字形在星格里**居中**（width 撑满星格 + text-align: center）。于是
 *      「按星格中线裁一半」正好是「这个字的一半」：不管字体把这个 ★ 画得多宽，
 *      居中之后中线都落在字的中间。原来字形是左对齐的，一放大就会出现
 *      「半星看着比半颗多」—— 而填充状态是用户唯一的反馈。
 *   3. 填充（.star-on）和悬停预览（.star-preview）都是**同一个字形**的副本，
 *      只靠外层的宽度裁剪：full = 100%，half = 50%。所以「显示的状态」和
 *      「数据的值」不可能各说各话。
 *   4. 悬停预览在**半个星的按钮里面**（见 app.js 的 starsHtml），靠按钮自己的
 *      overflow: hidden 裁掉另一半。原来那条 `.star:hover .star-glyph` 是挂在
 *      整颗星上的，鼠标落在左半边也会把整颗星涂金 —— 3.5 星的预览长得和 3 星
 *      一模一样，用户点下去才知道点错了。这条已经删掉，不许再加回来。
 */

.stars { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-top: 6px; }
.starrow { display: inline-flex; }

.star {
  position: relative;
  display: inline-block;
  width: var(--star-size);
  height: var(--star-height);
}

/* 字形：整行共用一个定位方式 —— 左对齐到星格，宽度撑满星格并居中。
   宽度必须写死成星格宽（不是 100%）：`.star-on` / `.star-preview` 自己是窄的，
   里面的字形要是按它们的宽度居中，就会跟着裁剪框一起往左跑 —— 那就成了
   「填一半」的同时把字也挪半个位，看起来像脱节的两个字。 */
.star-glyph {
  position: absolute;
  left: 0;
  top: 0;
  width: var(--star-size);
  text-align: center;
  font-size: var(--star-glyph-size);
  line-height: var(--star-height);
  color: #3a4049;
}

/* 已选中的填充：宽度 0 / 50% / 100%，把金色字形裁出来。
 *
 * ★ height 这一行是必须的，别删 ★
 * 里面的 `.star-glyph` 是 absolute 定位的（脱离文档流），所以这个盒子**没有任何
 * 在流内容**：不写 height 的话它的高度是 0，配合 `overflow: hidden` 就是
 * 「金色字形被裁得一点不剩」—— 实测（CDP 读 getBoundingClientRect）：
 * `.star-on` 的 height 是 0，而它子字形的 height 是 40。
 * 也就是说**已经选中的评分根本没画出来**，用户点完星星看不到自己打了几分 ——
 * 这是放大星级之前就存在的旧缺陷，本轮一并修掉：填充状态必须看得见。 */
.star-on {
  position: absolute;
  left: 0;
  top: 0;
  width: 0;
  height: 100%;
  overflow: hidden;
  white-space: nowrap;
}
.star-on .star-glyph { color: var(--gold); }
.star.full .star-on { width: 100%; }
.star.half .star-on { width: 50%; }

/* 命中区：真实按钮，正好半个星格宽，撑满星格高 */
.star-hit {
  position: absolute;
  top: 0;
  width: 50%;
  height: 100%;
  padding: 0;
  border: 0;
  background: transparent;
  cursor: pointer;
  overflow: hidden;         /* ← 裁剪悬停预览，见 .star-preview */
  /* ★ 这一行不是装饰 ★ `font-family` 在 `<button>` 上是**不继承**的：
     浏览器 UA 样式给按钮配了自己的字体（Arial），于是悬停预览里那个 ★ 和
     填充（`.star-on`）里的 ★ 是**两个不同的字形** —— Arial 的 ★ 更宽。
     实测（CDP 数金色像素，窗口 2px 一列，一颗星的右半边）：
     已选填充 93 px、悬停预览把它变成 153 px —— 同一颗星、同一个颜色，
     鼠标一放上去就胖了一圈，而「预览」的全部意义就是**和点下去之后长得一样**。
     预览只有一半时会看不出来；累积填充之后它旁边就是四颗真的填充，对不齐一眼就看得见。 */
  font-family: inherit;
}
.star-hit.left { left: 0; }
.star-hit.right { right: 0; }

/* 悬停 / 键盘聚焦时的预览：一个整宽的金色字形，被按钮裁成这一半。
   右半边按钮里的字形要**左移半个星格**，这样露出来的才是字的右半 ——
   不移动的话两个按钮都会显示左半边（右按钮看起来会像「少一点」的星）。 */
.star-preview {
  display: none;
  position: absolute;
  left: 0;
  top: 0;
  width: var(--star-size);
  height: 100%;
  /* 预览是装饰，不接鼠标：命中区必须**就是**那个按钮本身。不写这一条的话，
     悬停时鼠标底下的元素变成预览里的字形（点击照样会冒泡到按钮，功能不错，
     但「鼠标底下是谁」变得含糊 —— 也让 hover 的验收没法直接读出来）。 */
  pointer-events: none;
}
.star-hit.right .star-preview { left: calc(-1 * var(--star-size) / 2); }
.star-hit .star-preview .star-glyph { color: var(--gold); }
.star-hit:hover .star-preview,
.star-hit:focus-visible .star-preview { display: block; }

/* 悬停预览是**累积**的：鼠标停在哪一半，从第一颗星到那儿全都亮起来。
 *
 * ★ 用户报告 3 ★「评分的时候鼠标移动到某颗星星，应该那颗星星前面都要亮。」
 *
 * 上面那条单个半格的预览只回答了「鼠标在哪」，没回答「我正在选几分」：停在
 * 第四颗星的左半边时，屏幕上只有一小条金色，3.5 和 4 的区别全靠用户在脑子里
 * 补 —— 这正是「星星太小、容易打错分」的另一面。预览要把**将要提交的那个值**
 * 整个画出来。
 *
 * 为什么这里非 `:has()` 不可：两个半星按钮在 `.star` **里面**，所以「悬停那颗
 * 按钮」的兄弟是同一个星里的预览块，不是别的星 —— `~` 从按钮出发够不到任何一颗
 * 星。`:has()` 是唯一能回答「这颗星里有被悬停的按钮吗」的东西（Edge 105+ 起支持；
 * 本项目没有构建步骤，样式表就是发给浏览器的那一份），而 `~` 只负责「后面」这个
 * 方向。两者各管一半，正好是经典写法：**先全部点亮，再清掉后面的**。
 *
 *   1. 鼠标落在任何一半上 → 五颗星先全部填满；
 *   2. 悬停那颗**之后**的星清回 0 宽（`.star:has(.star-hit:hover) ~ .star`）；
 *   3. 悬停那一颗如果是左半边，收回到 50%。
 *
 * 少一条都不行：没有第 1 条，已存 0 分的面板上第一颗星之前什么都不亮；没有第 2 条，
 * 已存 5 分时把鼠标移到第 1 颗上，「改成 0.5 分」的预览还是五颗全亮；没有第 3 条，
 * 被悬停的左半边会被第 1 条填成**整颗** —— 那就是上一轮修掉的那个 bug 的孪生兄弟
 * （上一轮修的是 `.star:hover .star-glyph`，3.5 的预览和 3 长得一模一样，症状相同）。
 *
 * 第 2 条刻意带上 `.starrow:has(...)` 这层前缀：不加的话它和第 1 条**权重相同**
 * （都是 0-5-0），只能靠「谁写在后面」取胜 —— 那种正确性下一个人一移动行序就没了。
 * 加了之后是 0-8-0 > 0-5-0，靠权重说话；第 3 条 0-9-0 又是三者里最大的。
 *
 * 三条全部挂在 `:hover` 上：鼠标离开控件（或面板收起来）时一条都不命中，
 * 屏幕上自动回到 `.star.full` / `.star.half` 画出的**已存评分** ——
 * 不需要任何 JS 去收拾预览，也不可能留下一个「卡住的预览」。
 * 键盘聚焦（`:focus-visible`）走的仍是上面那条单个半格的预览，本轮不动它：
 * 两套累积规则同时挂 hover 与 focus 会打架（焦点在第 5 颗、鼠标在第 1 颗时
 * 两条都命中，屏幕上就是个谁也不认识的值）。
 */
.starrow:has(.star-hit:hover) .star .star-on {
  width: 100%;
}
.starrow:has(.star-hit:hover) .star:has(.star-hit:hover) ~ .star .star-on {
  width: 0;
}
.starrow:has(.star-hit.left:hover) .star:has(.star-hit.left:hover) .star-on {
  width: 50%;
}

.stars-value { color: var(--dim); font-size: 12px; }

/* 「会反复观看」开关。
 *
 * ★ 命中区是刻意选的：上一轮用户报告星星太小、容易点错，星星因此放大到
 * 每颗 40px。这个控件是同一轮里新加的，没有理由比修好的星星更难按 ——
 * `min-height: var(--star-size)` 就是「和一颗星一样高」，而不是新编一个数。
 * 宽度由 padding 撑开（中文标签本身就不短），不去写死 px。
 *
 * 它和 `.starrow` 是 `.stars` 里的两个并列 flex 项：**两个独立按钮**，
 * 点其中一个不会碰到另一个。勾它不需要先打分，打分也不会把它清掉。 */
.btn-rewatch {
  min-height: var(--star-size);
  padding: 0 12px;
  font-size: 13px;
  line-height: 1.2;
}
/* 已勾上：文字自己已经说了（☑ + 「已标记」），颜色只是加强。
   反过来不成立 —— 只靠颜色区分状态，色弱用户和这片深色底都会让它失效。 */
.btn-rewatch[aria-pressed="true"] {
  border-color: var(--gold);
  color: var(--gold);
  background: rgba(255, 200, 87, 0.14);
}

/* ---------------------------------------------------------------- 翻页条 */

.pager {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 0 0 14px;
}
.pager-pos { color: var(--dim); font-size: 13px; font-variant-numeric: tabular-nums; }

/* ---------------------------------------------------------------- 首页：一次一部
 *
 * ★ 用户请求（2026-10-17）★「首页展示 10 部电影还是太多了，直接就展示一部电影，
 * 也不要翻页，只要用户对着一部去评价：看过评分 / 看过会反复看 / 没看过想看 /
 * 没看过不感兴趣。」
 *
 * 这一块只有三层：大图 + 信息、一行小字说「为什么是它」、四个答案。
 * 它的对跖点是 `.card`（一屏 10 张要扫的紧凑卡片）—— 两者**故意不共享盒模型**：
 * 卡片要在窄格子里塞下简介和 3 条理由，首页这一屏只有一部，空间可以全部铺开。
 */

.hero {
  display: flex;
  flex-direction: column;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 12px;
  overflow: hidden;
  /* 960px 而不是铺满：一次只有一部片，铺到 1180 会让正文右边空出半屏，
     整个卡片看起来「没装满」。960 - 300(海报) - 22(间距) - 36(内边距) ≈ 600px
     的正文宽度，正好是简介（72ch）与理由都能读满、又不留死白的那个区间。 */
  max-width: 960px;
  margin: 0 auto;
}
/* 标了「会反复观看」的那一部：与卡片用同一条金色描边（同一件事的同一种说法） */
.hero.card-rewatch { border-color: var(--gold); }

.hero-main {
  display: flex;
  gap: 20px;
  padding: 14px;
  align-items: flex-start;
}
/* 大图用 w500（与详情页同一个尺寸）：这一屏只有一张图，没有理由再给它缩略尺寸；
   宽 268px 与详情页 300px 只差一点，切过去图不跳。
   2026-10-19：从 300 → 268，为的是让 1366×768 的笔记本不用滚轮就能看到四个答案
   （海报高度 ≈ 宽 × 1.5，省下的 48px 是这一屏最大的一块）。 */
.hero-poster { flex: 0 0 268px; width: 268px; }
.hero-poster .poster { border-radius: 8px; }

.hero-info { flex: 1; min-width: 0; }
.hero-title { margin: 0 0 4px; font-size: 22px; line-height: 1.25; }
.hero-info .card-meta { font-size: 13px; }
.hero-info .dim { margin: 3px 0 0; }
/* 简介在这一屏**不截成 3 行**：一屏只有一部片，读者想知道它讲什么。
   但仍然封一个上限（7 行）—— 一条超长简介把四个答案顶出屏幕，是「首页」
   最不能出的问题：答案必须看得见。（2026-10-19：8 → 7 行，配合整屏压缩。） */
.hero-overview {
  margin: 8px 0 0;
  color: var(--dim);
  font-size: 14px;
  line-height: 1.6;
  max-width: 72ch;
  display: -webkit-box;
  -webkit-line-clamp: 7;
  -webkit-box-orient: vertical;
  overflow: hidden;
  overflow-wrap: anywhere;
}
.hero-score { margin: 8px 0 0; color: var(--dim); font-size: 12px; }
.hero-info .reasons { margin-top: 6px; font-size: 13px; }
   最不能出的问题：答案必须看得见。 */
.hero-overview {
  margin: 10px 0 0;
  color: var(--dim);
  font-size: 14px;
  line-height: 1.65;
  max-width: 72ch;
  display: -webkit-box;
  -webkit-line-clamp: 8;
  -webkit-box-orient: vertical;
  overflow: hidden;
  overflow-wrap: anywhere;
}
.hero-score { margin: 8px 0 0; color: var(--dim); font-size: 12px; }
.hero-info .reasons { margin-top: 6px; font-size: 13px; }
.hero-info .reasons-status { margin-top: 6px; }
/* 理由在这一屏**不封宽度**（卡片上封 46ch 是为了在窄格子里不折行）：
   这一屏的正文有 600px，理由条目的「标签 + 数值」本来就短，让它用满就行。 */

/* ---- 四个答案 ---- */

.hero-choose {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 20px;
  padding: 10px 14px 12px;
  border-top: 1px solid var(--line);
  background: var(--bg-soft);
}
.hero-answer { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
/* 分组标签（没看过 / 看过）是这一行的语义骨架，不是装饰：
   四个答案分成两组，用户就不用自己猜「想看」算不算看过。 */
.hero-answer-label {
  color: var(--dim);
  font-size: 12px;
  letter-spacing: 0.5px;
  padding-right: 2px;
}
.hero-answer-seen .stars { margin-top: 0; }
.hero-answer .hero-btn { padding: 7px 15px; font-size: 14px; }
/* 跳过 / 详情靠右：它们是次要动作，不该和四个答案抢位置 */
.hero-answer-quiet { margin-left: auto; }

/* ---- 「看过 · 打分」那一行（spec §29，2026-10-19）----
 *
 * 用户第二次拍板：「还是把打分放首页会方便点」。与墙上那张卡片的差别只有一条：
 * **不折叠**（那里点一下才展开，是为了在 10 张一屏的墙里省空间、也防误点；
 * 这一屏只有一部片，展开就是它的默认形态）。
 *
 * 独占一行（`flex-basis: 100%`）而不是塞进「看过」那组：五颗星 5×var(--star-size)
 * = 200px，加上星级读数与「会反复观看」按钮，在 1024 宽下与那三个按钮挤在一行
 * 必然折行 —— 与其让它自己乱折，不如固定成这一块区域的第二行。
 *
 * 高度账（spec §11.4 量过）：这一行约 58px。1366×768 上原来四个答案底部在
 * 646px（余 122px），加完仍有约 64px 余量，**不需要滚轮**这一条没有被破掉。
 */
.hero-rate {
  display: flex;
  flex: 1 0 100%;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 12px;
  padding-top: 2px;
}
.hero-rate .stars { margin-top: 0; }

@media (max-width: 900px) {
  .hero-main { flex-direction: column; }
  .hero-poster { flex: none; width: 100%; max-width: 300px; }
  .hero-answer-quiet { margin-left: 0; }
}

/* ---- 推荐屏 / 标记屏（spec §25，2026-10-19）---- */

/* 「已看过 12 部 · 到 25 部时打分更值」那一行。
   它是这一屏的**进度说明**，不是警告：底色用普通的 soft，左缘一道细线把
   「这是一段说明文字」说清楚 —— 用 warn 色会让人以为哪里出错了。 */
.ladder {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 10px;
  margin: 0 0 12px;
  padding: 8px 12px;
  border-left: 3px solid var(--accent);
  background: var(--bg-soft);
  border-radius: 6px;
  font-size: 13px;
  color: var(--dim);
}
.ladder strong { color: var(--text); font-weight: 600; }
/* 台阶那一行里如果还出现按钮（目前没有），别让它把整段话挤成两行 */
.ladder .btn { padding: 4px 10px; font-size: 12px; }

/* 标记屏上那个问题（「你看过这部吗？」）。它比两个答案更显眼一点：
   这一步只有两个选项，而它们是**用户的第一个判断**，不该被当成配角。 */
.mark-question .hero-answer-label {
  color: var(--text);
  font-size: 14px;
  font-weight: 600;
  letter-spacing: 0;
}
/* 「没看过」这一步的两个答案：想看是绿的（正向），不感兴趣是灰的（拒绝）——
   与卡片上那两颗按钮同一套配色，见 .hero-btn[data-act="want"] / [data-act="nope"]。 */

/* 海报墙「看过」页签上的「只看没打分的」开关。
   两个状态**字形不同**（☐ / ☑）而不只是颜色：色弱用户与这块深色底都会让
   「开没开」变成要试才知道的问题 —— 与「会反复观看」开关同一条纪律。
   （spec §31 删掉了顶栏的「待打分」入口之后，这里是找「看过但没打星」的唯一入口；
   它仍然值得存在：标记屏答「看过」时会写下这种行。） */
.unrated-toggle {
  margin: 0 0 10px;
  font-size: 13px;
}
.unrated-toggle.on {
  color: var(--gold);
  border-color: var(--gold);
}

/* 没有 TMDB 钥匙时那张折叠起来的可选项（spec §25.5）。
   有片库时它不该是首屏的第一块东西：不填也能用，那就别挡路。 */
.setup-optional {
  font-size: 13px;
}
.setup-optional > summary {
  cursor: pointer;
  padding: 2px 0;
}
.setup-optional > summary::marker { color: var(--dim); }
.setup-optional[open] > summary { margin-bottom: 8px; }
.setup-optional p { margin: 0 0 8px; color: var(--dim); }


/* 旧卡片流网格（.flow-grid）与「当前焦点卡片」（.card-flow.focused）已经删掉：
 * 首页改成「一次一部」（.hero）之后，`app.js` 里再没有任何路径会生成这两个类。
 * 留着它们等于在样式表里承诺一个不存在的功能 —— 与 `.card-flow.past` 同一条
 * 先例：死规则删掉，不占位。 */

.keys-hint { color: var(--dim); font-size: 13px; }
.keys-hint kbd {
  display: inline-block;
  padding: 1px 6px;
  margin: 0 1px;
  border: 1px solid var(--line);
  border-bottom-width: 2px;
  border-radius: 4px;
  background: #0c0e12;
  font-family: inherit;
  font-size: 12px;
  color: var(--text);
}

/* ---------------------------------------------------------------- 短屏再压一档
 *
 * 2026-10-19：用户请求「页面上下高度缩小一点点，让电脑不需要滚轮翻下来」。
 * 台式机上 1440×900 已经够用，但 1366×768 的笔记本（还有 16:10 的 1280×800）
 * 在「离线横幅 + 一部片」同时在场时会把四个答案顶到折叠线以下 —— 而四个答案
 * 就是这一屏唯一要做的事，看不到就等于没法用。
 *
 * 所以按**视口高度**再压一档：海报小一点、简介少两行、内边距再收 2px。
 * 只动空间，不动字号与可点区域（星格仍然是 40px，答案按钮仍然是 7px/15px），
 * 也只在「屏幕确实矮」的时候生效 —— 大屏上看不出区别。
 */
@media (max-height: 820px) and (min-width: 901px) {
  .hero-main { padding: 12px; gap: 16px; }
  .hero-poster { flex: 0 0 226px; width: 226px; }
  .hero-overview { -webkit-line-clamp: 5; line-height: 1.55; }
  .hero-title { font-size: 20px; }
  .hero-info .reasons { margin-top: 4px; }
  .hero-choose { padding: 8px 12px 10px; gap: 6px 18px; }
  .banner { padding: 8px 12px; font-size: 13px; }
  .banner p { margin: 3px 0 0; }
  .view-head { margin-bottom: 8px; }
}

/* ---------------------------------------------------------------- 详情 */

.detail { display: flex; flex-direction: column; gap: 18px; align-items: flex-start; }
.detail-main { display: flex; gap: 20px; align-items: flex-start; flex-wrap: wrap; width: 100%; }
.detail-poster {
  width: 300px;
  border-radius: 10px;
  border: 1px solid var(--line);
  background: #0c0e12;
}
.detail-info { flex: 1; min-width: 300px; }
.detail-info h2 { margin: 0 0 6px; font-size: 22px; }
.overview { color: var(--dim); font-size: 14px; max-width: 70ch; }

.score-card {
  width: 100%;
  padding: 14px 16px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--bg-soft);
}
.score-card h3 { margin: 0 0 8px; font-size: 15px; }
.score-line { font-size: 14px; color: var(--dim); }
.score-line b { color: var(--text); font-variant-numeric: tabular-nums; }

.contrib { width: 100%; }
.contrib h3 { margin: 0 0 8px; font-size: 15px; }
.contrib-table { width: 100%; max-width: 760px; border-collapse: collapse; font-size: 13px; }
.contrib-table th, .contrib-table td {
  text-align: left;
  padding: 6px 10px;
  border-bottom: 1px solid var(--line);
}
.contrib-table th { color: var(--dim); font-weight: 400; font-size: 12px; }
.contrib-table td.num { text-align: right; font-variant-numeric: tabular-nums; }
.contrib-table tr.neg td.num { color: var(--danger); }
.contrib-table tr.pos td.num { color: var(--gold); }

/* ---------------------------------------------------------------- 提示气泡 */

.toast {
  position: fixed;
  left: 50%;
  bottom: 26px;
  transform: translateX(-50%);
  padding: 9px 18px;
  border-radius: 999px;
  background: #23272f;
  border: 1px solid var(--line);
  color: var(--text);
  font-size: 13px;
  z-index: 40;
  max-width: 80vw;
}
.toast.bad { border-color: #5c2b2b; color: #ffb3b3; }

/* ---------------------------------------------------------------- P2：筛选 / 搜索 / 画像 / 历史 / 权重
 *
 * 这一整块只做一件事：把**数据**画成能一眼比大小的形状，而不引入任何图表库。
 * 全站零依赖（无框架、无 CDN、无外部字体）是项目的第一条视觉约束，画像页也不
 * 例外 —— 一张横条图的全部实现就是「一个 div 的宽度百分比」。
 *
 * 颜色分工沿用已有的语义，不新增一套：
 *   --accent  可点的东西、主按钮、条形图本身
 *   --gold    分数与「你特别在意的东西」（星级、契合度、权重值）
 *   --dim     次要说明
 *   --warn/--danger  需要打折看的信息、负权重
 */

/* 顶栏现在有 6 个入口：允许换行，别让它们在窄屏上叠成一坨 */
.topbar { flex-wrap: wrap; row-gap: 8px; }
.nav { flex-wrap: wrap; }

/* ---- 筛选栏（海报墙、权重面板共用这个横排容器）---- */

.filters {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin: 0 0 12px;
}
/* 计数靠右：它是这一行的**结论**（筛选后多少部），不该混在控件里被当成又一个按钮 */
.filters .count { color: var(--dim); font-size: 13px; margin-left: auto; }

.filter-select,
.filter-q,
.search-input,
.w-input {
  background: #0c0e12;
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: 6px;
  font-family: inherit;
  font-size: 13px;
  padding: 6px 9px;
  min-height: 32px;
}
.filter-select:focus,
.filter-q:focus,
.search-input:focus,
.w-input:focus {
  border-color: var(--accent);
  /* 只留边框变色是不够的：深色底上 1px 的边框色变化几乎看不出来，而键盘用户
     全靠它判断「现在在哪」。所以 `outline: none` 之后必须**补一个真正的焦点环**
     （见下面那条 :focus-visible）—— 否则这四个控件是全站唯一没有可见焦点的东西，
     而其他控件走的是浏览器默认的 outline: auto。
     分两条写是标准做法：`:focus` 抹掉默认描边（含鼠标点击时），`:focus-visible`
     只给键盘焦点补环 —— 鼠标点一下不该留一圈蓝边。 */
  outline: none;
}
.filter-select:focus-visible,
.filter-q:focus-visible,
.search-input:focus-visible,
.w-input:focus-visible {
  /* 与 `.card-flow.focused` 用的是同一个蓝色光晕，视觉语言保持一致 */
  box-shadow: 0 0 0 2px rgba(76, 141, 255, 0.35);
}
.filter-q { min-width: 210px; }
.filter-select { cursor: pointer; }

/* 候选池「补齐」小按钮：与「共 1906 部」同一行的安静动作。
   它挂着 .btn-quiet，这里只把它压小一点、别抢 view-head 的视线。 */
.pool-chip { padding: 1px 9px; font-size: 12px; }

/* ---- 搜索 ---- */

.search-box { display: flex; gap: 8px; margin: 0 0 16px; flex-wrap: wrap; }
.search-input { flex: 1 1 320px; max-width: 560px; font-size: 15px; padding: 9px 12px; }

/* 远端结果：一行一部，左侧一张小海报。
   这里**刻意不用**卡片网格 —— 远端结果的动作只有「加进库」一个，
   而一套缩略图网格会让人以为这些片已经能在墙上点着打分了。 */
.remote-block { margin-top: 26px; border-top: 1px dashed var(--line); padding-top: 16px; }
.remote-block h3 { margin: 0 0 8px; font-size: 15px; }
.remote-list {
  list-style: none;
  margin: 12px 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.remote-item {
  display: flex;
  gap: 12px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 10px;
}
.remote-poster { flex: 0 0 72px; width: 72px; }
.remote-poster .poster { border-radius: 6px; }
.remote-info { flex: 1; min-width: 0; }
.remote-info h4 { margin: 0 0 4px; font-size: 14px; }
.remote-info .actions { margin-top: 6px; }
.remote-overview {
  margin: 0;
  color: var(--dim);
  font-size: 12px;
  line-height: 1.5;
  /* 远端简介不封顶的话，一条 300 字的简介能把整屏占满 —— 这里要的是
     「是不是这部」，不是读完它的故事。 */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  overflow-wrap: anywhere;
}

/* ---- 画像：概览数字 ---- */

.stat-row { display: flex; flex-wrap: wrap; gap: 10px; margin: 0 0 18px; }
.stat-card {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 132px;
  padding: 10px 16px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 10px;
}
.stat-label { color: var(--dim); font-size: 12px; }
.stat-value {
  font-size: 22px;
  font-weight: 700;
  line-height: 1.2;
  color: var(--gold);
  font-variant-numeric: tabular-nums;
}
.stat-note { color: var(--dim); font-size: 11px; }

/* ---- 面板（画像 / 历史 / 权重共用的白底盒）---- */

.panel {
  padding: 14px 16px;
  margin: 0 0 16px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 10px;
}
.panel h3 { margin: 0 0 6px; font-size: 15px; }
.panel > .dim { margin: 0 0 10px; line-height: 1.6; }
.panel-title { margin: 0 0 6px; font-size: 15px; }
.panel-note { margin: 0 0 10px; color: var(--dim); font-size: 13px; line-height: 1.7; }
/* 两栏并排；窄屏自动落成单栏（auto-fit 而不是写死的两栏） */
.panel-pair {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: 0 16px;
}

/* ---- 横条图 ---- */

.bars {
  list-style: none;
  margin: 4px 0 0;
  padding: 0;
  /* ★ 网格挂在 <ul> 上、<li> 用 display: contents ★
   *
   * 直觉写法是「每个 li 自己当网格」，但那样**每一行都会独立算列宽**：
   * 数值列是 `auto`，于是「0 部」那行的数值列只有 22px，而「31 部 · 4.50 ·
   * 大众 7.7」那行要 100px —— 结果每一行的条形图起点一样、终点却各不相同，
   * 一屏看下去就是一堆参差不齐的横条，而横条图存在的全部意义就是**对齐比较**。
   * 网格上移一级之后所有行共用同一组列宽；li 用 display: contents 退出盒子
   * 生成，它的三个子元素直接落进父网格。 */
  display: grid;
  grid-template-columns: minmax(84px, 148px) 1fr auto;
  gap: 6px 10px;
  align-items: center;
}
.bar-row { display: contents; }
.bar-label {
  font-size: 13px;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.bar-track {
  height: 12px;
  background: #0c0e12;
  border: 1px solid var(--line);
  border-radius: 999px;
  overflow: hidden;
}
.bar-fill {
  display: block;
  height: 100%;
  border-radius: 999px;
  background: var(--accent);
}
.bar-value {
  color: var(--dim);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* ---- 片单条（高分 / 低分 / 会反复观看）---- */

.film-strip { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 8px; }
.film-link {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 6px 10px;
  background: var(--bg-soft);
  border: 1px solid var(--line);
  border-radius: 8px;
  color: var(--text);
  font-family: inherit;
  font-size: 13px;
  text-align: left;
  cursor: pointer;
}
.film-link:hover { border-color: var(--accent); }
.film-title { line-height: 1.3; }
.film-meta { color: var(--dim); font-size: 11px; }

/* ---- 评分历史时间线 ---- */

.timeline { list-style: none; margin: 4px 0 0; padding: 0; }
.tl-row {
  display: grid;
  grid-template-columns: 52px 1fr auto auto;
  gap: 10px;
  align-items: center;
  padding: 6px 0;
  border-bottom: 1px solid rgba(43, 48, 56, 0.65);
}
.tl-row:last-child { border-bottom: 0; }
.tl-time { color: var(--dim); font-size: 12px; font-variant-numeric: tabular-nums; }
/* 片名是个**按钮**（点开详情），所以必须把按钮的默认外观全部清掉：
   它在这行里要长得像链接，而不是像旁边那些动作按钮。 */
.tl-title {
  background: none;
  border: 0;
  padding: 0;
  color: var(--text);
  font-family: inherit;
  font-size: 14px;
  text-align: left;
  cursor: pointer;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.tl-title:hover { color: var(--accent); text-decoration: underline; }
.tl-year { color: var(--dim); font-size: 12px; }
.tl-marks { display: flex; gap: 6px; align-items: center; justify-content: flex-end; flex-wrap: wrap; }
.tl-mark {
  font-size: 12px;
  padding: 1px 7px;
  border: 1px solid var(--line);
  border-radius: 999px;
  color: var(--dim);
  white-space: nowrap;
}
/* 四种标记四种颜色，但**文字本身已经不同**（不靠颜色区分状态） */
.tl-score { color: var(--gold); border-color: #5c4b24; }
.tl-rewatch { color: var(--gold); border-color: #5c4b24; background: rgba(255, 200, 87, 0.12); }
.tl-want { color: var(--accent); border-color: #2b3f66; }
.tl-seen { color: var(--ok); border-color: #245c3f; }
.tl-nope { color: var(--dim); }

/* ---- 权重面板 ---- */

.w-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.w-table th,
.w-table td {
  text-align: left;
  padding: 6px 10px;
  border-bottom: 1px solid var(--line);
  vertical-align: middle;
}
.w-table th { color: var(--dim); font-weight: 400; font-size: 12px; }
.w-table td.num,
.w-table th.num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
/* 「其中核心层」那一列（2026-10-19）：它是「会反复观看」那批片的分量。
   用低饱和我方色标出来，不抢「当前权重」那一列（真正决定排序的还是它）。 */
.w-table td.w-core { color: var(--accent); font-variant-numeric: tabular-nums; }
.w-table td.w-core:empty { color: var(--dim); }
/* 手改过的那一行：数值换成金色 —— 用**颜色 + 「恢复自动」按钮的有无**两个信号
   一起说「这一项是你改的」，任一失效都还有另一个。 */
.w-row.w-manual .w-current { color: var(--gold); }
.w-row.w-manual td { background: rgba(255, 200, 87, 0.05); }
.w-kind {
  display: inline-block;
  margin-right: 6px;
  padding: 0 5px;
  border: 1px solid var(--line);
  border-radius: 4px;
  color: var(--dim);
  font-size: 11px;
}
.w-ops { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
.w-input { width: 88px; text-align: right; font-variant-numeric: tabular-nums; }

@media (max-width: 860px) {
  .filters .count { margin-left: 0; }
  /* 列宽定义在 .bars 上（见上面那段说明），所以窄屏也要改在 .bars 上 */
  .bars { grid-template-columns: minmax(64px, 104px) 1fr auto; }
  .tl-row { grid-template-columns: 46px 1fr auto; }
  .tl-year { display: none; }
}

@media (max-width: 720px) {
  .config-form input { width: 100%; }
  .detail-poster { width: 100%; }
}

/* ================================================================ 2026-10-18
 *
 * 这一轮加的东西：四个答案的语义配色、撤销按钮、详情页的演职员 / 关键词 /
 * 同导演、人筛选的说明、还原备份的横幅。全部沿用上面那套变量与节奏，
 * 没有引入新的颜色家族 —— 一个 15px 字号、8px 圆角的深色界面里，
 * 「多一种颜色」比「少一种颜色」危险得多。
 */

/* ---- 首页四个答案：语义配色 ---- */

/* 「没看过 · 想看」= 想要的东西，青绿色（与 --want 同一族） */
.hero-btn[data-act="want"] {
  border-color: rgba(53, 208, 165, 0.40);
  color: #7ef0cd;
}
.hero-btn[data-act="want"]:hover {
  background: rgba(53, 208, 165, 0.14);
  border-color: var(--want);
  color: #a6f7de;
}
/* 「没看过 · 不感兴趣」= 明确不要，低饱和的红（不是报错的 --danger） */
.hero-btn[data-act="nope"] {
  border-color: rgba(226, 112, 125, 0.36);
  color: #f0aab1;
}
.hero-btn[data-act="nope"]:hover {
  background: rgba(226, 112, 125, 0.13);
  border-color: var(--nope);
  color: #ffc3c9;
}
/* 「看过」那一组用金色（与星级、与「会反复观看」同一个色：都是「我确认过」） */
.hero-answer-seen .hero-answer-label { color: var(--gold); opacity: 0.85; }
.hero-answer:not(.hero-answer-seen) .hero-answer-label { color: var(--dim); }

/* 首页那张大卡片：一道极淡的顶部光，把海报与正文从纯色块里托起来一点点。
   opacity 只有 0.10 —— 目标是「不那么平」，不是「发亮」。 */
.hero {
  background-image: radial-gradient(120% 90% at 14% -30%,
                                    rgba(76, 141, 255, 0.10), transparent 62%);
}

/* ---- 撤销：贴在提示里的那颗按钮 ---- */

/* 提示条原来是纯文字（textContent），带撤销时才变成 innerHTML + 一颗按钮。
   它必须**看起来像可点的东西**，否则「撤销」两个字只是一段文字。 */
.toast-action {
  margin-left: 12px;
  padding: 2px 10px;
  background: var(--accent);
  color: #06101f;
  border: 0;
  border-radius: 999px;
  font-family: inherit;
  font-size: 12px;
  font-weight: 700;
  cursor: pointer;
}
.toast-action:hover { background: #6ba0ff; }
.toast-action:focus-visible { outline: 2px solid var(--text); outline-offset: 2px; }

/* ---- 详情：演职员 / 关键词 / 同导演 ---- */

.credits, .keyword-block, .related { width: 100%; }
.credits h3, .keyword-block h3, .related h3 {
  margin: 0 0 8px;
  font-size: 15px;
}
.credit-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin: 0 0 6px;
}
/* 「导演 / 主演」这两列标签固定宽度：两行的名字要从同一条竖线开始，
   否则一眼看过去分不清哪个名字属于哪一栏。 */
.credit-label {
  flex: 0 0 44px;
  color: var(--dim);
  font-size: 12px;
  letter-spacing: 0.5px;
}

/* 人名是个**可点的小块**：点下去就是「这个人的其它片」。
   做成 chip 而不是链接，是因为它在语义上是一个筛选项，不是一次跳转。 */
.person-chip {
  padding: 2px 10px;
  background: var(--bg-soft);
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: 999px;
  font-family: inherit;
  font-size: 13px;
  line-height: 1.6;
  cursor: pointer;
}
.person-chip:hover {
  border-color: var(--accent);
  color: #bcd4ff;
  background: rgba(76, 141, 255, 0.10);
}
.person-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* 关键词只是说明（没有对应的筛选维度），所以**不做成按钮** ——
   一个点下去没有任何反应的按钮，比一段普通文字更让人困惑。 */
.keyword-row { display: flex; flex-wrap: wrap; gap: 6px; margin: 0; }
.keyword-tag {
  padding: 1px 9px;
  border: 1px dashed var(--line);
  border-radius: 999px;
  color: var(--dim);
  font-size: 12px;
}

/* 同导演那一栏的卡片网格：与海报墙同一个 .wall-grid，只是多一条上边距，
   让它和上面「为什么推荐」那张表分开。 */
.related-grid { margin-top: 10px; }

/* ---- 筛选栏里那句「当前条件：…」 ---- */

/* 导演 / 演员没有下拉框（几千个人列不出来），所以条件生效时必须**说出来**，
   否则用户看到的是「三个下拉都是全部，可墙上只有 3 部片」——那就是筛选器坏了
   的样子。这句摘要由服务端算（wall.filter_summary），前端只负责排版。 */
.filter-note {
  color: var(--gold);
  font-size: 13px;
  padding: 3px 10px;
  border: 1px dashed #5c4b24;
  border-radius: 999px;
  background: rgba(255, 200, 87, 0.07);
}

/* ---- 还原备份的横幅 ---- */

/* 还原是一条「你已经动了我的库」的路：它不能长得像一条随手可以忽略的提示，
   所以用 --ok / --info 的左边框，同时保留横幅的普通排版（不另起一套卡片）。 */
.banners .banner p .btn { margin-top: 4px; }

/* ================================================================ 夯拉榜（spec §21）
 *
 * 五档横排成五行，每行左边是档位名、右边是可以往里拖的座位。视觉上照搬了网上
 * 那张 tier list 的骨架：**档位名占一列、影片铺满剩下的**，因为用户认的就是
 * 这个形状 —— 换个样子就得重新学一遍。
 *
 * 颜色只用三处：档位名的左边条、选中卡片的光晕、放置区的高亮。档位间的区别
 * **主要靠文字**（夯 / 顶级 / 人上人 / NPC / 拉完了），颜色只是让五行一眼分得开，
 * 与时间线那四种标记同一条规矩：不靠颜色单独承载语义。
 */

.tier-board {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin: 4px 0 14px;
}

.tier-row {
  display: grid;
  grid-template-columns: 148px 1fr;
  gap: 10px;
  padding: 10px;
  background: var(--bg-soft);
  border: 1px solid var(--line);
  border-radius: 10px;
}
/* 档位名那一格：左边一条 4px 的色条是这一档的身份（同时也是放置目标） */
.tier-label {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 6px 10px;
  border-left: 4px solid var(--line);
  border-radius: 6px;
  background: rgba(255, 255, 255, 0.02);
  cursor: pointer;
}
.tier-name { font-size: 16px; letter-spacing: 1px; }
.tier-note { color: var(--dim); font-size: 11px; line-height: 1.4; }
.tier-count { color: var(--dim); font-size: 11px; font-variant-numeric: tabular-nums; }

/* 五档的色条：从暖到冷，和「从夯到拉」的方向一致 */
.tier-row[data-tier="hang"] .tier-label { border-left-color: var(--gold); }
.tier-row[data-tier="top"] .tier-label { border-left-color: #ff9d5c; }
.tier-row[data-tier="above"] .tier-label { border-left-color: var(--ok); }
.tier-row[data-tier="npc"] .tier-label { border-left-color: #7b8494; }
.tier-row[data-tier="bad"] .tier-label { border-left-color: var(--nope); }
.tier-unrated .tier-label { border-left-style: dashed; }

/* 座位区：空的时候是一条虚线，看得见「这里可以放」 */
.tier-drop {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  min-height: 62px;
  padding: 4px;
  border: 1px dashed transparent;
  border-radius: 8px;
}
.tier-drop:hover { border-color: var(--line); }
.tier-empty {
  align-self: center;
  color: var(--dim);
  font-size: 12px;
  opacity: 0.7;
}

/* 「还有 N 部 · 展开这一档」：占地很小、语气很轻 —— 它是这一行的**页脚**，
   不是第二个主按钮。整行高度约等于一张卡片的 1/3。 */
.tier-more {
  display: flex;
  align-items: center;
  gap: 8px;
  align-self: flex-end;
  padding: 0 2px 2px;
}
.tier-rest { color: var(--dim); font-size: 12px; }
.tier-more .btn { font-size: 12px; padding: 3px 9px; }

/* ================================================================ 想看清单的五档（spec §23）
 *
 * 与夯拉榜的档位形状一致（一行一档、左边一条色条、右上角一个条数），但**不叫**
 * 夯/顶级/人上人/NPC/拉完了 —— 那五个词在这个应用里的含义是「你给它的评分」，
 * 而这里的片你还没看过。所以档位名改成贴合度的说法（最合口味 … 不太合口味），
 * 语义上不再打架。
 */

.band-note { font-size: 12px; margin: 0 0 8px; }

.band-block {
  margin: 0 0 14px;
  padding: 8px 10px 10px;
  border: 1px solid var(--line);
  border-left-width: 4px;
  border-radius: 10px;
  background: var(--bg-soft);
}
/* 五档的色条：从暖到冷，和「越靠前越合口味」的方向一致 */
.band-block[data-band="best"] { border-left-color: var(--gold); }
.band-block[data-band="strong"] { border-left-color: #ff9d5c; }
.band-block[data-band="fair"] { border-left-color: var(--ok); }
.band-block[data-band="plain"] { border-left-color: #7b8494; }
.band-block[data-band="weak"] { border-left-color: var(--nope); }

.band-head {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 10px;
  margin: 0 0 8px;
}
.band-name { font-size: 15px; letter-spacing: 1px; }
.band-count { color: var(--dim); font-size: 12px; font-variant-numeric: tabular-nums; }
.band-range { color: var(--dim); font-size: 12px; font-variant-numeric: tabular-nums; }
.band-empty { color: var(--dim); font-size: 12px; margin: 0; opacity: 0.7; }
.sort-note { font-size: 12px; margin: 0 0 8px; }

/* 每档的卡片比海报墙上的**小一档**（180px 列 vs 232px）：
 * 一档通常只有 2–6 部，用大卡片会在一行里留下三四个空格，整屏看着像没填满；
 * 小一档之后一行能并排 6–7 部，五档加起来也不再是五屏那么长。
 * 卡片的全部信息（简介 3 行、理由 3 条、四个动作）一个都没少。 */
.band-block .wall-grid {
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: 10px;
}
.band-block .wall-grid .card-title { font-size: 14px; }

/* 一张卡片：海报在上，片名与星级在下。96px 宽 —— 一行大约能放下 9-12 张，
   于是「一整档」通常在一屏之内，拖动时不用滚动。 */
.rank-tile {
  display: flex;
  flex-direction: column;
  gap: 3px;
  width: 96px;
  padding: 0;
  background: transparent;
  border: 0;
  color: var(--text);
  font-family: inherit;
  text-align: left;
  cursor: grab;
}
.rank-shot {
  position: relative;
  display: block;
  width: 96px;
  height: 144px;
  border-radius: 6px;
  overflow: hidden;
  background: #0c0e12;
  border: 1px solid var(--line);
}
.rank-poster { width: 100%; height: 100%; object-fit: cover; display: block; }
/* 档内名次：只在真的排过序时出现（见 rankTileHtml 的说明） */
.rank-badge {
  position: absolute;
  top: 3px;
  left: 3px;
  min-width: 18px;
  padding: 0 4px;
  border-radius: 999px;
  background: rgba(8, 10, 14, 0.82);
  color: var(--gold);
  font-size: 11px;
  line-height: 18px;
  text-align: center;
  font-variant-numeric: tabular-nums;
}
.rank-rewatch {
  position: absolute;
  top: 3px;
  right: 3px;
  color: var(--gold);
  font-size: 11px;
  line-height: 18px;
  text-shadow: 0 0 4px #000;
}
.rank-title {
  font-size: 12px;
  line-height: 1.3;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  overflow-wrap: anywhere;
}
.rank-meta { color: var(--dim); font-size: 11px; font-variant-numeric: tabular-nums; }

/* 选中：画一圈光晕 + 抬起一点，让「当前在操作哪一张」没有歧义 */
.rank-tile.is-selected .rank-shot {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px rgba(76, 141, 255, 0.55), 0 6px 18px rgba(0, 0, 0, 0.45);
}
.rank-tile:focus-visible .rank-shot { outline: 2px solid var(--accent); outline-offset: 2px; }
.rank-tile:hover .rank-shot { border-color: #3d4451; }
/* 标了「会反复观看」的那一张：金色描边，与墙上的卡片同一个说法 */
.rank-tile.card-rewatch .rank-shot { border-color: var(--gold); }

/* 「放到哪一档」按钮条：选中一张卡片之后出现在榜的顶部 */
.rank-picker {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin: 0 0 12px;
  padding: 8px 12px;
  border: 1px solid #2b3f66;
  border-radius: 10px;
  background: rgba(76, 141, 255, 0.08);
}
.rank-picker-title { color: var(--text); font-size: 13px; margin-right: 4px; }
.rank-pick.is-current {
  border-color: var(--accent);
  color: #bcd4ff;
  background: rgba(76, 141, 255, 0.16);
}

.tier-footnote { font-size: 12px; }

@media (max-width: 760px) {
  .tier-row { grid-template-columns: 1fr; }
  .tier-label { flex-direction: row; align-items: baseline; gap: 8px; }
  .tier-label .tier-note { flex: 1; }
}

/* ---- 画像页：夯拉对照表 ---- */

.ranking-panel .contrast-title {
  margin: 14px 0 6px;
  font-size: 13px;
  color: var(--dim);
  font-weight: 400;
  letter-spacing: 0.5px;
}
.contrast-table { width: 100%; max-width: 720px; border-collapse: collapse; font-size: 13px; }
.contrast-table th,
.contrast-table td {
  text-align: left;
  padding: 5px 10px;
  border-bottom: 1px solid var(--line);
}
.contrast-table th { color: var(--dim); font-weight: 400; font-size: 12px; }
.contrast-table td.num,
.contrast-table th.num { text-align: right; font-variant-numeric: tabular-nums; }
/* 正差 = 这一维在你「夯」的那批里更常见；负差 = 只在「拉」的那批里出现。
   颜色只是加强，数值本身带着符号（与贡献明细那张表同一套说法）。 */
.contrast-table tr.pos td.num { color: var(--ok); }
.contrast-table tr.neg td.num { color: var(--nope); }

/* ---------------------------------------------------------------- 账号 / 登录
 * （多用户，2026-10-19）
 *
 * 这一屏是**唯一**在未登录状态下能看到的界面，所以它得自己站住：没有导航、
 * 没有卡片、没有横幅，只有一个居中的框。三件事在视觉上必须清楚到不需要解释：
 *   1. 现在是在登录还是第一次建号（标题一行就说完）；
 *   2. 输入框在哪（标签在上、框够大、聚焦有蓝边 —— 键盘 Tab 过去看得见）；
 *   3. 出错时错在哪（红字在按钮**上方**，眼睛从输入框往下走的第一站）。
 */

.auth { display: flex; justify-content: center; padding: 32px 16px; }
.auth-card {
  width: 100%;
  max-width: 460px;
  padding: 26px 28px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 12px;
}
.auth-title { margin: 0 0 10px; font-size: 20px; }
.auth-note { margin: 0 0 18px; color: var(--dim); font-size: 13px; line-height: 1.8; }
.auth-note b { color: var(--text); font-weight: 600; }
.auth-form { display: flex; flex-direction: column; gap: 12px; }
.auth-error {
  margin: 0 0 12px;
  padding: 8px 10px;
  border: 1px solid #6b2c2c;
  border-radius: 6px;
  background: #2a1717;
  color: var(--danger);
  font-size: 13px;
}
.auth-submit { padding: 9px 14px; font-size: 14px; margin-top: 4px; }
.auth-foot { margin: 16px 0 0; color: var(--dim); font-size: 12px; line-height: 1.7; }
.auth-foot code,
.panel-note code {
  padding: 1px 5px;
  background: var(--bg-soft);
  border: 1px solid var(--line);
  border-radius: 4px;
  font-size: 11px;
}

/* 一排「标签 + 输入框」。label 包着 input，所以点标签也能聚焦（触屏尤其要紧）。 */
.field { display: flex; flex-direction: column; gap: 5px; font-size: 12px; color: var(--dim); }
.field input {
  padding: 8px 10px;
  background: var(--bg-soft);
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: 6px;
  font-size: 14px;
  font-family: inherit;
}
.field input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* 顶栏的身份 chip：名字 + 「· 主账号」，点它进账号页 */
.user-chip {
  font-family: inherit;
  font-size: 13px;
  padding: 5px 12px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: transparent;
  color: var(--text);
  cursor: pointer;
  white-space: nowrap;
  max-width: 220px;
  overflow: hidden;
  text-overflow: ellipsis;
}
.user-chip:hover { background: var(--bg-soft); border-color: #3d4451; }
.user-chip.active { background: var(--bg-soft); border-color: var(--accent); }
.user-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ---- 账号页 ---- */
.account { max-width: 820px; }
.view-title { margin: 0 0 6px; font-size: 20px; }
.view-sub { margin: 0 0 18px; color: var(--dim); font-size: 13px; line-height: 1.8; }
.view-sub b { color: var(--text); }
.inline-form { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 10px; margin: 10px 0 0; }
.inline-form .field { min-width: 160px; }
.row-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.form-ok { margin: 0 0 10px; color: var(--ok); font-size: 13px; }
.form-error { margin: 0 0 10px; color: var(--danger); font-size: 13px; }

.accounts { width: 100%; border-collapse: collapse; font-size: 13px; margin: 4px 0 0; }
.accounts th,
.accounts td {
  text-align: left;
  padding: 7px 10px;
  border-bottom: 1px solid var(--line);
  vertical-align: middle;
}
.accounts th { color: var(--dim); font-weight: 400; font-size: 12px; }
.accounts .mono { font-variant-numeric: tabular-nums; color: var(--dim); font-size: 12px; }
.accounts .tag {
  display: inline-block;
  margin-left: 6px;
  padding: 0 6px;
  border-radius: 999px;
  border: 1px solid #245c3f;
  color: var(--ok);
  font-size: 11px;
}
.reset-form { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.reset-form input {
  padding: 5px 8px;
  background: var(--bg-soft);
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: 6px;
  font-size: 13px;
  font-family: inherit;
  width: 170px;
}




