/* =========================================================
   Color Card · 全局样式
   ========================================================= */

*,
*::before,
*::after {
  box-sizing: border-box;
}

:root {
  /* v1.3.4 · 拒绝浏览器强制深色（与 4 个 HTML 的 <meta name="color-scheme"> 成对）
     ─────────────────────────────────────────────────────────
     本项目的立身之本是「数值取色」：lab() 色块的颜色就是结论本身。
     浏览器自动深色（Chromium Auto Dark / 各厂商"夜间模式"）会对色块
     叠 filter 或反色 → **静默改变真实颜色**，用户还以为看到的是真色。
     `only light` 的 `only` 正是为「禁止 UA 覆盖配色」而设的关键字。
     同时它也让滚动条、表单控件等 UA 部件保持亮色渲染。
     ⚠️ 挡不住的：靠注入 filter 实现的厂商夜间模式（详见 docs/开发护栏.md 第 10 章）。 */
  color-scheme: only light;

  --bg: #f5f6fa;
  --bg-elev: #ffffff;
  --bg-soft: #f0f2f7;
  --line: #e2e5ec;
  --text: #1f2330;
  --text-muted: #6b7080;
  --text-faint: #9aa0ac;
  /* 主题色（v3.3.0：**中性深灰 #545454**，用户指定）
     ─────────────────────────────────────────────────────────
     ⚠️ 这里有一段必须记住的历史：v1.5.7~v3.2.2 的主色是 **#154b8e（深藏青）**，
        来源是 lab(32.13 8.9 -41.95) 的 D65 实测解释（当时 CSS lab() 按 D50 算，
        会得到 #31498e，所以是「手工解释」出来的）。v3.3.0 起换成中性灰。
     ⚠️ **换主色不影响任何颜色数据**：--primary 从头到尾只用于 UI 装饰
        （按钮 / 链接 / 焦点环 / 选中态 / 描边），本站的色块一律走 lab() 真实还原，
        从不经过 --primary。所以取色、比色、相似色匹配的结果一字未变。
     ⚠️ 中性灰的既有代价（已与用户确认，不要「顺手修」）：
        · 灰描边压在灰系色卡上，辨识度低于原来的蓝 ——
          灯箱 / 取色向导的选区框（canvas：白外描边 + 主色内描边）最明显；
        · --primary-soft（#ececec）与站内既有中性底 --bg-soft（#f0f2f7）非常接近，
          「淡主色底」这个视觉手段本身被削弱。这正是本版把子标签激活态
          从 5 个信号砍到 2 个信号的原因之一（见下方 .cat-rail .chip.active）。
     ⚠️ 对比度（对白底）：#545454 = 7.59:1（AAA 正文级）；hover #3d3d3d = 11.4:1。
        反白（白字压在 --primary 上）同样 7.59:1，达标。
     ⚠️ 改这三个值之前先读 docs/开发护栏.md 第 26 章与第 72 章。 */
  --primary: #545454;
  --primary-hover: #3d3d3d;
  --primary-soft: #ececec;
  /* dock 激活态强调色（v3.5.8：去掉灰底框，改由图标本身的渐变表达激活）
     ⚠️ 这是**唯一**一处 v3.3.0「去彩色化」之后的彩色回归，且**只作用于 dock 激活键**。
        它不参与任何色卡/比色计算（那些一律走 lab() 真实还原），所以取色与相似色结果不受影响。
     ⚠️ 两个端点值即整个渐变；要调色只改这两处。
     ⚠️ 渐变要能在「4 个线稿键（stroke）+ 1 个实心键（fill）」上都生效 ——
        五个图标一律用 `paint-order` 无关的 `stroke/fill: url(#…) currentColor`
        双值写法（后一个是兜底，见下面 .dock-btn.active svg 的注释）。 */
  --dock-accent-a: #5b6cff;
  --dock-accent-b: #8b5cf6;
  --danger: #e0445b;
  --success: #21a85a;
  --warn: #f3a93b;
  --radius-sm: 8px;
  --radius-md: 12px;
  --radius-lg: 16px;
  --shadow-sm: 0 1px 2px rgba(20, 23, 32, 0.05);
  --shadow-md: 0 6px 18px rgba(20, 23, 32, 0.06);
  --shadow-lg: 0 16px 40px rgba(20, 23, 32, 0.08);
  --top-h: clamp(56px, 16vw, 64px);   /* v1.5.0：手机端随视口等比缩放，见下方「手机端比例尺」 */

  /* ===== 手机端比例尺（v1.5.0）=============================================
     ⚠️ 手机端的**写死 px 尺寸必须随布局视口等比缩放**，否则会在窄视口上被
     「物理放大」。同机实测（用户提供的两张截图，同一台手机同一时刻）：
       · OPPO 把 device-width 算成 **348px**，微信算成 **403px**；
       · 于是一个写死的 14px 间距，在 OPPO 上渲染成 44 图像px、在微信上 38 图像px
         —— 差 **16%**。窄视口那头组件显得大、能放的信息少
         （用户原话：「微信的排版这些组件显示信息更多更协调」）。
       · 色卡格子是 `1fr`（随视口），它的物理尺寸两图几乎一样（306 vs 314 图像px）
         → 差异**全部**来自这些写死的 px。
     写法：`clamp(下限, 基准值/4 vw, 基准值)`
       · **以 400px 布局视口为基准** → vw ≥ 400px 时等于原设计值
         （**桌面、平板、大屏一字未改**），≤ 400px 时等比缩小。
       · 下限保住可点面积（dock 按钮 ≥40px、回到顶部 ≥38px；桌面端原本 50/44）。
     副作用（正是我们想要的）：水平间隙也随视口等比缩放，于是 dock 与
     「回到顶部」在**任何**宽度下都不会重叠 —— 见 docs/开发护栏.md 第 18/19 章。 */
  --m-padx: clamp(12px, 3.5vw, 14px);          /* 内容左右内衬（手机 14） */
  --m-pad-bottom: clamp(78px, 24vw, 96px);     /* 内容底部预留：给 dock 让位（手机 96） */
  --m-bar-gap: clamp(7px, 2.25vw, 9px);        /* 标签行内间距（手机 9） */
  --m-rail-gap: clamp(6px, 2vw, 8px);          /* 标签胶囊之间（手机 8） */
  --m-chip-py: clamp(6px, 2vw, 8px);           /* 标签胶囊内距（手机 8 / 14） */
  --m-chip-px: clamp(11px, 3.5vw, 14px);
  --m-chip-fs: clamp(12px, 3.25vw, 13px);      /* 标签胶囊字号（13） */
  --m-grid-r: clamp(13px, 4.5vw, 18px);        /* 网格行距（18） */
  --m-grid-c: clamp(10px, 3.5vw, 14px);        /* 网格列距（14） */
  --m-card-r: clamp(9px, 3vw, 12px);           /* 色块圆角（12） */
  --m-code-fs: clamp(11px, 3vw, 12px);         /* 色卡编号字号（12） */
  /* ⚠️ v3.2.0：dock 从 4 键加到 **5 键**（新增「设置」，见 js/dock.js），
     按钮边长**必须同步从 50 缩到 40** —— 否则：
       总宽 = 5×50+20 = 270px（原 4×50+20 = 220px）
       ⇒ 与右下角「回到顶部」按钮在 300~430px 视口下**重叠**（实测 400px 时间隙只剩 7px，
         最窄 300px 时 −10px 直接压住）。这条几何约束由门禁 16.20 逐视口实测把关。
     取 40px 的妙处：**胶囊总宽仍是 220px，与原来一字不差**
       （5×40+20 = 4×50+20 = 220），按钮变小但整条胶囊的位置与宽度完全没变。
     比例尺仍守既有约定「clamp(下限, 设计值/4 vw, 设计值)」：设计值 40 ⇒ 系数 10vw。 */
  --m-dock-btn: clamp(32px, 10vw, 40px);       /* dock 按钮边长（40 · v3.2.0 由 50 改） */
  --m-dock-icon: clamp(18px, 5vw, 20px);       /* dock 图标（20 · 随按钮同步缩） */
  --m-dock-bottom: clamp(16px, 5vw, 20px);     /* dock 底距（20） */
  --m-btt: clamp(38px, 11vw, 44px);            /* 回到顶部按钮（44） */
  --m-btt-icon: clamp(17px, 4.5vw, 18px);      /* 回到顶部图标（18） */
  --m-btt-right: clamp(12px, 3.5vw, 14px);     /* 回到顶部右距（14） */
  /* v1.5.10：首次访问欢迎层。⚠️ 与其余 --m-* 同约定 —— 基准 400px 视口，
     ≥400 时等于设计值（桌面零改动），窄视口等比缩小。新增本组变量时，
     必须**同时**登记进 verify-release 第 16.20 组的 DESIGN 表（否则系数写错没人报）。 */
  /* ⚠️ 这组的上限值 = 欢迎层的**桌面设计值**（v1.5.12 整体放大过一轮：
     标题 26→32、英文 20→25、图标本体 19→21、按钮 48→52、行距 18→22；
     图标底色块 34 未变）。改桌面值就必须同步改这里 + DESIGN 表。 */
  --m-welcome-title: clamp(26px, 8vw, 32px);       /* 欢迎层标题（32，Apple 大字） */
  --m-welcome-title-en: clamp(20px, 6.25vw, 25px); /* 英文另给一档（25）——理由见 .welcome-title */
  --m-welcome-icon: clamp(30px, 8.5vw, 34px);      /* 功能图标底色块（34） */
  --m-welcome-glyph: clamp(17px, 5.25vw, 21px);    /* 功能图标本体（21） */
  --m-welcome-btn: clamp(44px, 13vw, 52px);        /* 主按钮高（52，下限 44 = 可点面积底线） */
  --m-welcome-gap: clamp(14px, 5.5vw, 22px);       /* 功能行间距（22） */
  /* 灯箱主色区最小高度（改色图 / lab 色块共用），实际高度由 flex 弹性分配 */
  --lb-media-min: 120px;
}

html,
body {
  margin: 0;
  padding: 0;
  height: 100%;
}

/* v1.3.5 · 关掉「第二滚动端口」（仅限自带滚动容器的页面）=================
   ---------------------------------------------------------------
   背景：`.app{height:100vh}` 在移动端 = **large viewport**（地址栏收起时的高度），
   而 html/body 的 `height:100%` 解析为**当前可视高度**。地址栏显示时可视高度
   比 100vh 矮约 56px → `.app` 比 body 高 → html 出现约 56px 纵向溢出。
   因为这里没有 overflow:hidden，那 56px 就变成了**真实的第二个滚动端口**。

   于是全站有两个滚动层：html（根）与 .content。在 .content 内滚动时根滚动恒为 0，
   sticky 一切正常；**但滚到最底部继续拖时，滚动链会按规范上抛给 html** →
   整页被向上推走 → `.filter-row` 是相对 .content 的 scrollport 定位的，
   .content 自身被推离屏幕 → **标签栏脱浮且回拉后不平复位**。
   （症状只在「滑到底后回拉」出现，正是滚动链外溢的指纹。）

   所以把根滚动彻底关掉：滚动已由 .content / .admin-body 独占，根滚动只会惹祸。
   ---------------------------------------------------------------
   ⚠️ 必须按页面加类、**不能全站一刀切**：登录页（.auth-bg）本身没有内部滚动容器，
      靠文档滚动兜底；在横屏矮屏上若连它一起 hidden，登录表单会被裁掉且无法滚动。
      故只有 index / favorites / admin 三页的 <html> 带 `.app-root`。
   ⚠️ 当时这里写的是「那 56px 溢出虽然还在，但 .content-inner 有 96px 的下内边距
      缓冲，内容底部本来就不会被永久遮住」—— **这句是错的，v1.3.6 已证伪**：
      容器底边比可视底边低了「浏览器工具栏」那一截，而内边距是从**容器底边**往上
      量的 → 留给 dock 的那 96px 被这段隐藏高度吃掉 → 页码栏被 dock 盖住、划不上来。
      根治办法不是加大内边距（那只是个跟着浏览器变化的魔术数字），而是让 .app 的
      高度跟随可视视口（见 .app 的 `height: 100dvh`，v1.3.6）。 */
html.app-root,
html.app-root body {
  overflow: hidden;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
    "Hiragino Sans GB", "Microsoft YaHei", Roboto, Helvetica, Arial, sans-serif;
  color: var(--text);
  background: var(--bg);
  font-size: 14px;
  line-height: 1.5;
}

a {
  color: inherit;
  text-decoration: none;
}

button {
  font: inherit;
  color: inherit;
  cursor: pointer;
  border: none;
  background: none;
}

input,
select,
textarea {
  font: inherit;
  color: inherit;
}

::-webkit-scrollbar {
  width: 8px;
  height: 8px;
}
::-webkit-scrollbar-thumb {
  background: rgba(0, 0, 0, 0.18);
  border-radius: 8px;
}
::-webkit-scrollbar-thumb:hover {
  background: rgba(0, 0, 0, 0.28);
}

/* =========================================================
   通用 utilities
   ========================================================= */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 9px 16px;
  border-radius: 999px;
  font-weight: 500;
  font-size: 14px;
  background: var(--bg-soft);
  color: var(--text);
  transition: background 0.15s ease, transform 0.05s ease;
  border: 1px solid transparent;
}
.btn:hover {
  background: #e7eaf2;
}
.btn:active {
  transform: translateY(1px);
}
.btn.primary {
  background: var(--primary);
  color: #fff;
}
.btn.primary:hover {
  background: var(--primary-hover);
}
.btn.ghost {
  background: transparent;
  border: 1px solid var(--line);
}
.btn.ghost:hover {
  background: var(--bg-soft);
}
.btn.danger {
  background: #fde9eb;
  color: var(--danger);
}
.btn.danger:hover {
  background: #fbd5da;
}
.btn.sm {
  padding: 5px 12px;
  font-size: 12px;
}

.input,
.select,
.textarea {
  width: 100%;
  padding: 9px 12px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--line);
  background: #fff;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.input:focus,
.select:focus,
.textarea:focus {
  outline: none;
  border-color: var(--primary);
  box-shadow: 0 0 0 3px rgba(84, 84, 84, 0.15);
}
.textarea {
  min-height: 80px;
  resize: vertical;
}
/* 表格内联小号下拉（后台标签：相似色匹配默认标签） */
.select-sm {
  width: auto;
  min-width: 150px;
  max-width: 220px;
  padding: 5px 8px;
  font-size: 12px;
  border-radius: 8px;
}

.field {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-bottom: 14px;
}
.field label {
  font-size: 12px;
  font-weight: 500;
  color: var(--text-muted);
}

.muted {
  color: var(--text-muted);
}

.hidden {
  display: none !important;
}

/* =========================================================
   应用骨架
   ========================================================= */

.app {
  /* v1.3.6 · 高度必须跟随「可视视口」，不能用 100vh =========================
     `.app` 是整页骨架（.main / .content 都由它定高）。它比可视区高的那一截，
     就是「滚动容器底部被藏起来的一段」；而 .content-inner 的下内边距是从
     **容器底边**往上量的 → 这段被藏起来的高度会**吃掉**留给 dock 的内边距。
     后果（OPPO 自带浏览器实测）：色卡最后一行被底部 dock 压住，
     **页码栏被 dock 完全遮住、且怎么往上划都划不出来**。

     `100vh` 在移动端 = **large viewport**（浏览器工具栏收起时的高度），
     比当前可视高度高出工具栏那一截（约 56–90px，各浏览器不同，OPPO 约 70）。
     `100dvh` = **dynamic viewport**，就是当前可视视口高度 —— 与 dock 用的
     `position: fixed + bottom` 参照系完全一致，两者才对得上。

     写法与移动端灯箱（@media 内的 .lightbox）**同一套 vh→dvh 回退对**：
     不认识 dvh 的浏览器该行整条失效 → 退回上一行（＝改前行为，不会更坏）；
     认识 dvh 的取其值。项目里灯箱早已这么写且实测正常 → 本机浏览器支持 dvh。

     ⚠️ 不要再改回纯 100vh；也不要改成 100svh（= 最小可视高度，工具栏收起后
        会比屏幕矮，底部露出一条背景色空带）。 */
  height: 100vh;
  height: 100dvh;
  overflow: hidden;
}

/* 主体 */
.main {
  display: flex;
  flex-direction: column;
  height: 100%;
  min-height: 0;
  overflow: hidden;
}

/* 顶部 topbar（v1.16.0：主页/搜索/收藏夹移到底部 dock，顶栏仅留后台入口 + 语言切换）
   ⚠️ v3.2.0：**前台已不再使用 .topbar**（首页/收藏夹改为 .cat-sticky 吸顶块，
      语言与后台入口移入 dock 的设置菜单）。本组规则**保留**，因为
      后台 admin.html 仍在用 .topbar（.admin-shell > .topbar），删掉会连带打死后台。
      前台相关的新规则见下方「v3.2.0 吸顶块」一节。 */
.topbar {
  height: var(--top-h);
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 18px;
  padding: 0 24px;
  border-bottom: 1px solid var(--line);
  background: var(--bg-elev);
}
.topbar .icon-btn {
  width: 40px;
  height: 40px;
  border-radius: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-muted);
  transition: background 0.15s ease, color 0.15s ease;
}
.topbar .icon-btn:hover {
  background: var(--bg-soft);
  color: var(--text);
}
.topbar .icon-btn svg {
  width: 22px;
  height: 22px;
}

.search {
  flex: 1;
  position: relative;
  max-width: 540px;
}
.search input {
  width: 100%;
  padding: 10px 44px 10px 18px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--bg-soft);
  transition: background 0.15s ease, border-color 0.15s ease;
}
.search input:focus {
  outline: none;
  background: #fff;
  border-color: var(--primary);
  box-shadow: 0 0 0 3px rgba(84, 84, 84, 0.15);
}
.search .search-btn {
  position: absolute;
  right: 6px;
  top: 50%;
  transform: translateY(-50%);
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: var(--text);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
}
.search .search-btn:hover {
  background: #000;
}
.search .search-btn svg {
  width: 16px;
  height: 16px;
}

.topbar .spacer {
  flex: 1;
}
/* .btn 的 display:inline-flex 会覆盖 UA 的 [hidden]，需显式隐藏 */
.topbar .admin-entry[hidden] {
  display: none;
}

/* =========================================================
   顶栏左上角品牌位（v1.3.5）
   ---------------------------------------------------------
   由 site.js 在后台配置了 Logo / 站点名时才插入；未配置时不存在，
   顶栏布局与旧版逐字节一致。
   ⚠️ 靠左的实现：.topbar 本身是 `justify-content: flex-end`，
      给品牌位一个 `margin-right: auto` 让它吃掉全部剩余空间 ——
      既把品牌位顶到最左，又**不动右侧那组按钮原有的对齐方式**，
      比改成 flex-start 再给右侧加 spacer 的风险小得多
      （admin 顶栏自己已经有一个 .spacer，两套机制混用会打架）。
   ⚠️ 高度走 --brand-h 自定义属性（由 site.js 按后台设置写入），
      图片与文字两种形态共用同一个尺寸来源。 */
.topbar-brand {
  display: flex;
  align-items: center;
  flex: 0 0 auto;
  min-width: 0;
  margin-right: auto;
  text-decoration: none;
  color: var(--text);
}
.topbar-brand-logo {
  display: block;
  height: var(--brand-h, 32px);
  width: auto;
  /* 防止超宽 Logo 把右侧按钮挤走 */
  max-width: min(220px, 42vw);
  object-fit: contain;
}
.topbar-brand-name {
  font-weight: 600;
  /* 站点名的字号跟着 Logo 尺寸设置走，一个控件同时管两种形态 */
  font-size: clamp(14px, calc(var(--brand-h, 32px) * 0.46), 20px);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* 有品牌位时，后台那个小房子图标是冗余的 —— Logo / 站点名本身就是回首页的链接 */
.topbar[data-branded] .icon-btn[aria-label='home'] {
  display: none;
}

.lang-switch {
  display: inline-flex;
  align-items: center;
  background: var(--bg-soft);
  border-radius: 999px;
  padding: 4px;
  font-size: 13px;
}
.lang-switch button {
  padding: 5px 12px;
  border-radius: 999px;
  color: var(--text-muted);
  font-weight: 500;
}
.lang-switch button.active {
  background: #fff;
  color: var(--text);
  box-shadow: var(--shadow-sm);
}

/* =========================================================
   v3.2.0 · 方案C「吸顶合并」页头
   ---------------------------------------------------------
   结构：.cat-sticky
           ├── .pagehead  → 母标签胶囊（左） + 品牌位（右）
           └── #cat-wrap  → 子标签轨（.cat-rail 横向滚动）
   原顶栏（.topbar 那条白底横条）已取消；语言切换与后台入口移入 dock 设置菜单。

   ⚠️ 为什么吸顶块要包住**两行**而不是只吸子标签轨：
      只吸轨道的话，切母标签要去页头（会滚走）、切子标签在轨道（常驻），
      两个操作分处"会滚走"与"常驻"两种状态，翻页到一半想换组就得先滚回顶部。
      整块吸顶后「母 → 子」两级永远同时在视野里，这正是方案C 要解决的问题。

   ⚠️ 用 position:sticky 而不是 fixed：.content 是**全站唯一滚动端口**
      （见该容器的说明），sticky 天然以它为参照，不需要 JS 监听滚动重算位置；
      fixed 则要自己算 .content 的偏移与高度，还会在 iOS 橡皮筋时抖动。
   ⚠️ z-index 要高于色卡网格、低于 dock(45) 与灯箱/弹窗 —— 吸顶块不该盖住弹层。
   ========================================================= */
.cat-sticky {
  position: sticky;
  top: 0;
  z-index: 30;
  /* ⚠️ v3.3.0：**吸顶块本体就是那一行** —— 母标签（.pagehead）与子标签轨（#cat-wrap）
     并排，子标签轨跟在母标签**后方**。这正是 v1.3.3「钉左 + 右轨」的老布局，
     v3.2.0 曾把它拆成上下两行，本版并回来。
     ⚠️ 为什么不把 #cat-wrap 挪进 .pagehead 的 DOM：那要同时改
        index.html / favorites.html 两页，而且 pagehead.js 里有 `ph.innerHTML = ''`
        的重画，一不小心就把轨道内容冲掉。让**吸顶块自己**当 flex 容器，
        则两页 DOM、renderMasterTag()、renderCategories() **一行都不用动**。
     ⚠️ v3.2.2 起本块是**透明浮层**（见下方紧邻的 pointer-events 段），本版不变：
        不透明底 / 通栏下边线 / 横向常驻 backdrop-filter 都别加回来。 */
  display: flex;
  align-items: center;
  gap: var(--m-bar-gap, 9px);
  /* 10 上衬 + 38 胶囊 + 8 下衬 = 56px（拆成两行时是 104px，本版省掉一整行） */
  padding: 10px var(--m-padx, 14px) 8px;
  min-height: 52px;
  min-width: 0;
  background: none;
  border-bottom: 0;
  pointer-events: none;
}
/* 透明浮层上，只有**真正的控件**恢复可交互。
   ⚠️ 不要图省事写 `.cat-sticky * { pointer-events: auto }` —— 那等于把整块又变回
      可点击区域，空隙里的色卡立刻又被挡住，本次的 pointer-events 就白加了。
   ⚠️ 为什么单独列出 `.cat-rail`（而不是只恢复 `.chip`）：它是**横向滚动容器**，
      只在胶囊上恢复的话，"从轨道空白处起手横向滑动"会失效 ——
      而轨道里大部分面积本来就没有胶囊（只有两三枚时右侧全是空）。
      代价：轨道那一条（约 46px 高）仍是"吸顶外壳"，该条内的色卡点不到；
      本块自己的上下内衬（10px / 8px）没有盒子，是穿透的。
   ⚠️ v3.3.0：`.pagehead-brand` 已随 Logo 组件一起去掉，名单里不再有它。 */
.cat-sticky .cat-select,
.cat-sticky .cat-panel,
.cat-sticky .cat-rail {
  pointer-events: auto;
}

/* 页头：现在只是「那一行里的母标签格」，**自己不再是行容器** ——
   行距与上下内衬都由 .cat-sticky 承担（见上），这里留着会被重复计一次。 */
.pagehead {
  display: flex;
  align-items: center;
  /* v3.3.1：按内容定宽、**永不被压缩** —— 母标签名要始终完整显示。
     ⚠️ 原为 `flex: 0 1 auto; max-width: 46%`（可压缩 + 46% 上限）。上限本身
        在线上数据下并不触发（「全棉府绸」= 106px，上限 160px），但"可压缩"会：
        一旦某天真被压到，标签名就变成「全棉府…」。
     代价（明说）：母标签名极长时会挤占子标签轨 —— `#cat-wrap` 是 `min-width: 0`，
        最坏情况被挤到 0 宽。这是"名字必须完整"与"轨道至少露出几枚"之间的取舍，
        用户选了前者；子标签仍可从下拉面板进入。 */
  flex: 0 0 auto;
  min-width: 0;
}
.pagehead-master {
  flex: 0 0 auto;
  min-width: 0;
}
/* 母标签胶囊的外层装包（renderMasterTag 生成的 #cat-wrap-master）：同样不可压缩。
   ⚠️ 用后代选择器而不是改共享的 `.cat-wrap` —— 那个类子标签轨也在用。 */
.pagehead-master > .cat-wrap {
  flex: 0 0 auto;
}

/* 子标签轨所在的格：吃掉母标签剩下的全部宽度。
   ⚠️ v3.3.0 起与母标签**同一行**（原先独占一行）；左右内衬改由 .cat-sticky 统一给，
      这里 padding 归零 —— 留着会让轨道从两端的竖线上「缩进去」，两行对不齐。 */
#cat-wrap {
  flex: 1 1 auto;
  min-width: 0;
  padding: 0;
}
/* 轨道为空时的一句提示（当前组只有一个子标签 / 独立标签无兄弟时） */
.cat-rail-empty {
  font-size: 12.5px;
  color: var(--text-faint);
  padding: 6px 0;
}

/* 下拉面板里的「组标题」与子标签缩进样式定义在下方 .cat-item 之后
   （同优先级下靠后生效，必须排在 .cat-item 规则之后，否则 padding 覆盖不上去） */

/* =========================================================
   移动端：横向内衬与页头对齐成同一条竖线
   ========================================================= */
@media (max-width: 640px) {
  /* v3.3.0：行距与左右内衬都改由 .cat-sticky 承担（它才是那一行），
     这里只把竖向留白收小：8 上衬 + 38 胶囊 + 6 下衬 = 52px。
     ⚠️ 原 `.pagehead-master { max-width: 56% }` 已删 —— 上限现在挂在
        `.pagehead` 上统一取 46%（手机上限更紧对子标签轨更有利）。 */
  .cat-sticky {
    padding-top: 8px;
    padding-bottom: 6px;
  }
}

/* =========================================================
   内容区域
   ========================================================= */

.content {
  flex: 1;
  min-height: 0;
  overflow: auto;
  /* v1.3.5 · 阻断滚动链外溢：滚到底部继续拖时不许把滚动上抛给视口。
     这是「标签栏滑到底再回拉就脱浮」的直接修复 —— 与 html.app-root 的
     overflow:hidden 是一对：那个拿掉溢出源，这个拦住传导路径（双保险，
     任一在旧内核上不被支持时另一个仍能兜住）。
     顺带禁掉在色卡列表底部误触发浏览器「下拉刷新」的老毛病。
     ⚠️ 只约束 y 轴：.cat-rail 的横向甩动另由 overscroll-behavior-x:contain 管。 */
  overscroll-behavior-y: contain;
  /* v1.16.7：内边距移到 .content-inner —— 顶栏现在是本容器的第一个子元素，
     需要通栏（背景 + 下边框铺满全宽），所以 .content 自身不能再吃左右内边距，
     同时顶栏会随内容一起滚出视口（不再常驻） */
  padding: 0;
}

/* 内容内衬：承接内边距（上 10 / 左右 28 / 下留 dock 高度 96 + 安全区）
   顶栏被排除在外，因此它是通栏的
   v1.16.8：顶距 22 → 10，配合标签行下距收紧，缩短「顶栏 → 色卡展示区」的空白带 */
.content-inner {
  padding: clamp(8px, 2.5vw, 10px) 28px calc(var(--m-pad-bottom) + env(safe-area-inset-bottom, 0px));
}

/* 标签栏行：滚动时吸附在内容区顶部悬浮（v1.16.6）
   左对齐（v1.16.8 撤销 v1.16.7 的水平居中回到原位；
   v1.16.9 又删掉了右侧数量徽标 → 本行只剩 .cat-wrap 一个子元素，
   `space-between` 此时等价于 flex-start，保留以兼容日后可能再加的第二个子元素）
   ---------------------------------------------------------
   · `.content` 才是滚动容器（body 不滚），它同时是最近滚动端口，
     而 `.filter-row` 是它的后代 → sticky 天然生效
   · `z-index:20` 必须显式给：`.card{position:relative}`、
     `.card:hover{transform}` 都会让卡片参与定位层绘制，
     不给 z-index 的话卡片会盖住吸附中的胶囊
   · 层级：卡片(0) < 本行(20) < .cat-panel(30) < dock(45)/回到顶部(40) < 灯箱(90)
   · `top:12px` 留一点呼吸位，胶囊与徽标都悬浮在滚动的色卡之上
   · 下距 12（v1.16.8：18 → 12，收紧到色卡展示区的空白；与 #card-grid 的
     margin-top 同级，二者发生外边距合并 → 实际间距取 12） */
.filter-row {
  position: sticky;
  top: 12px;
  z-index: 20;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  margin-bottom: 12px;
}

/* 顶栏：收藏夹入口（位于语言切换按钮左边） */
.fav-entry {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  height: 38px;
  padding: 0 14px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--bg-elev);
  color: var(--text);
  font-size: 13px;
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
  flex-shrink: 0;
  transition: all 0.15s ease;
}
.fav-entry:hover {
  border-color: #c5cad6;
}
.fav-entry svg {
  width: 16px;
  height: 16px;
}
.fav-entry.active {
  background: var(--primary-soft);
  border-color: var(--primary);
  color: var(--primary);
}
.fav-count {
  min-width: 20px;
  height: 20px;
  padding: 0 6px;
  border-radius: 999px;
  background: var(--primary-soft);
  color: var(--primary);
  font-size: 11px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.fav-count[hidden] {
  display: none;
}
.fav-entry.active .fav-count {
  background: #fff;
}

/* 管理后台入口：任何视口下都只显示图标
   （文字节点保留供 i18n / 无障碍使用，视觉上不展示）
   高度与收藏夹入口胶囊统一 38px，11px 侧内边距 → 近似圆形的图标胶囊 */
.admin-entry {
  height: 38px;
  padding: 0 11px;
}
.admin-entry .admin-label {
  display: none;
}
.admin-entry .admin-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  line-height: 0;
}
.admin-entry .admin-icon svg {
  width: 16px;
  height: 16px;
}

/* 移动端：收藏夹入口收起文字，只留图标 + 数量徽标
   同时收紧顶栏间距，把省下的宽度让给搜索框 */
@media (max-width: 640px) {
  .topbar {
    gap: clamp(8px, 2.5vw, 10px);
    padding: 0 var(--m-padx);
  }
  /* v1.3.5：窄屏给品牌位更紧的宽度上限，避免把语言切换挤出屏幕 */
  .topbar-brand-logo {
    max-width: 34vw;
  }
  .topbar-brand-name {
    max-width: 34vw;
  }
  .lang-switch button {
    padding: clamp(4px, 1.25vw, 5px) clamp(7px, 2.25vw, 9px);
  }
  .fav-entry {
    gap: 6px;
    padding: 0 11px;
  }
  .fav-entry .fav-label {
    display: none;
  }
}

/* 收藏夹页：标签 chip 内的收藏数量徽标 */
.chip-count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 20px;
  height: 20px;
  padding: 0 6px;
  margin-left: 8px;
  border-radius: 999px;
  background: var(--bg-soft);
  color: var(--text-muted);
  font-size: 11px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.chip.active .chip-count {
  background: var(--primary);
  color: #fff;
}
/* 0 条收藏的标签置灰（仍可点击） */
.chip-empty {
  color: var(--text-faint);
}
.chip-empty .chip-count {
  color: var(--text-faint);
}

/* 灯箱主图右下角的收藏按钮
   视觉与顶栏胶囊（.fav-entry / .chip）保持一致：
   默认 = 白底 + 细边框(--line) + 正文色；hover 只换边框色；
   已收藏 = 主色浅底 + 主色描边 + 主色图标（对应胶囊的选中态） */
.lightbox-fav {
  position: absolute;
  right: 12px;
  bottom: 12px;
  width: 40px;
  height: 40px;
  padding: 0;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--bg-elev);
  color: var(--text);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  z-index: 2;
  transition: border-color 0.15s ease, background 0.15s ease,
    color 0.15s ease;
}
.lightbox-fav svg {
  width: 20px;
  height: 20px;
}
/* 图标容器用 flex 居中，line-height 归零：
   否则 span 会因内联 SVG 的行盒基线产生约 2.5px 的垂直偏移（爱心看起来偏上） */
.lightbox-fav .i-off,
.lightbox-fav .i-on {
  display: none;
  align-items: center;
  justify-content: center;
  line-height: 0;
}
.lightbox-fav .i-off {
  display: flex;
}
.lightbox-fav:hover {
  border-color: #c5cad6;
}
.lightbox-fav.on {
  background: var(--primary-soft);
  border-color: var(--primary);
  color: var(--primary);
}
.lightbox-fav.on .i-off {
  display: none;
}
.lightbox-fav.on .i-on {
  display: flex;
}

/* 「回到顶部」：毛玻璃圆钮 + 主色箭头（v1.5.0 起回到"按钮"形态）--------------
   ---------------------------------------------------------
   形态：44px 圆形毛玻璃（桌面）／随视口等比缩到 38px（窄视口）。
         —— 与 dock 用**同一套玻璃参数**（半透明白 + blur(24px) saturate(180%)
         + 同款描边与投影），但**箭头用主色 `--primary`**；它比 dock 的灰图标
         更亮，所以整体比 dock 的 60px 矮一档，视觉重量不会压过 dock。
   落点：右距 `--m-btt-right`（桌面 24 / ≤640px 14，按比例缩）；
         **纵向与 dock 的中心线对齐**，表达式**由 dock 自己的变量算出来**：
           dock 中心距底 = --m-dock-bottom + (--m-dock-btn + 10px) / 2
           本按钮 bottom = 该中心 − --m-btt / 2
         ⚠️ 所以这里**不写死 28px**：dock 尺寸一改，这里自动跟 ——
            v1.4.6 / v1.4.7 反复踩的「按钮与 dock 对不齐」就是写死数字造成的。
   ⚠️ **为什么不再需要 v1.4.6 的「窄屏抬到 dock 上方」**：
      v1.5.0 把 dock 与按钮的尺寸都改成随视口等比缩放（见 :root 的「手机端比例尺」），
      于是水平间隙**也随视口等比缩放**：
        间隙 = vw − 右距 − 按钮宽 − (0.5×vw + dock宽/2) ≈ 0.5×vw − 0.42×vw ≈ 0.08 × vw
      实测（v3.2.0 · 5 键 dock，胶囊总宽仍是 220px）：
        400 → 32px｜348（用户那台 OPPO）→ 26.5px｜336 → 24px｜320 → 20px｜300 → 10px
      —— **恒为正**。故 v1.4.6 的 `@media (max-width:371px)` 抬升档**已删除**
      （连同它那段「窄屏要加大内容底部预留」的补丁，因为不再抬上去）。
   ⚠️ v3.2.0 的坑值得记一笔：dock 从 4 键加到 5 键时，**只改按钮数不改按钮边长**
      会让总宽 220→270px，上面那条"≈0.08×vw"的正间隙立刻变成负的
      （实测 400px 只剩 7px、300px 时 −10px）—— 正是这个注释里算出来的公式把它论证死的。
      所以本次是把按钮边长 50→40 与键数 4→5 **成对**改的，总宽纹丝不动。
   visibility 参与过渡：隐藏时不进入 tab 序列，也不拦截点击 */
.back-to-top {
  position: fixed;
  right: 24px;
  bottom: calc(
    var(--m-dock-bottom) + var(--m-dock-btn) / 2 + 5px - var(--m-btt) / 2 +
      env(safe-area-inset-bottom, 0px) + var(--dock-lift, 0px)
  );
  width: var(--m-btt);
  height: var(--m-btt);
  padding: 0;
  border-radius: 999px;
  border: 1px solid rgba(20, 23, 32, 0.07);
  background: rgba(255, 255, 255, 0.78);
  -webkit-backdrop-filter: blur(24px) saturate(180%);
  backdrop-filter: blur(24px) saturate(180%);
  color: var(--primary);
  display: flex;
  align-items: center;
  justify-content: center;
  line-height: 0;
  cursor: pointer;
  box-shadow: 0 6px 18px rgba(20, 23, 32, 0.1);
  /* 低于 dock(45)/灯箱(90)/弹窗(100)，避免盖住遮罩层 */
  z-index: 40;
  opacity: 0;
  visibility: hidden;
  transform: translateY(8px);
  transition: opacity 0.2s ease, transform 0.2s ease, visibility 0.2s ease,
    background 0.15s ease, border-color 0.15s ease;
}
.back-to-top.show {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
}
.back-to-top:hover {
  background: #fff;
  border-color: #c5cad6;
}
.back-to-top svg {
  width: var(--m-btt-icon);
  height: var(--m-btt-icon);
}
/* dock 展开搜索时变宽（343px）会压到本按钮 → 整片淡出并停止接收点击。
   选择器权重 (0,2,1) 高于 .back-to-top.show (0,2,0)，无需 !important */
html.dock-searching .back-to-top {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transform: translateY(8px);
}
/* 移动端：贴右边更近一点（沿用原落点；纵向由上面的「dock 中心线」表达式管） */
@media (max-width: 640px) {
  .back-to-top {
    right: var(--m-btt-right);
  }
}

/* =========================================================
   底部居中悬浮导航栏 dock（v1.16.0；v1.4.0 增至 4 键）
   玻璃胶囊 220×60：主页 · 搜索 · 取色 · 收藏夹
   - 搜索展开后宽度 343px（主页/取色/收藏夹暂时隐藏），主色聚焦光晕
   - z-index 45：高于回到顶部(40)，低于灯箱/弹窗(90)
   - --dock-lift 由 dock.js 依据 visualViewport 写入（iOS 键盘避让）
   ⚠️ 宽度由按钮数决定，公式：50n + 2×9(内边距) + 2×1(边框) = 50n + 20。
      （全局 box-sizing: border-box，所以内边距与边框要从 width 里扣掉。）
      n=3 → 170px（v1.3.6 及以前是 168，本就差 2px，胶囊两端会各挤出 1px）；
      n=4 → 220px。加/减按钮必须同步改这里，否则按钮会溢出胶囊圆角。
   ========================================================= */
.dock {
  position: fixed;
  left: 50%;
  transform: translateX(-50%);
  bottom: calc(var(--m-dock-bottom) + env(safe-area-inset-bottom, 0px) + var(--dock-lift, 0px));
  /* v3.13.18：键数由 --dock-cols 决定 —— AI 入口被权限关掉时收成 4 键，不留空位。
     ⚠️ 只改变量、不写宽度：.dock.searching 的固定宽度是同特异性后置规则，
        直接写 width 会把它一起盖掉。 */
  width: calc(var(--m-dock-btn) * var(--dock-cols, 5) + 20px);
  height: calc(var(--m-dock-btn) + 10px);
  padding: 5px 9px;
  display: flex;
  align-items: center;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.78);
  -webkit-backdrop-filter: blur(24px) saturate(180%);
  backdrop-filter: blur(24px) saturate(180%);
  border: 1px solid rgba(20, 23, 32, 0.07);
  box-shadow: 0 12px 32px rgba(20, 23, 32, 0.14);
  z-index: 45;
  transition: width 0.22s cubic-bezier(0.2, 0.7, 0.3, 1),
    box-shadow 0.18s ease, border-color 0.18s ease;
}
.dock-btns {
  display: flex;
  align-items: center;
  gap: 0;
}
/* v3.13.18：AI 入口隐藏后 dock 收成 4 键 */
.dock.dock--no-ai {
  --dock-cols: 4;
}
/* v3.13.28：首帧还不知道「AI 键该不该在」时，整条 dock 先不画。
   ⚠️⚠️ 为什么必须有这一条：allowed 要等 /api/ai/front 回来才知道，而那时
      dock 已经按 5 键（220px）画完了 ⇒ 4 键站点上会看到一次「先宽后窄」的
      收缩（用户报的「点图标后变宽再复原」就是它，width 还带 0.22s 过渡）。
      藏掉这一帧、等结论一次成形，就没有"变宽"可看了。
   ⚠️ 只在**无缓存**时挂这个类（见 js/dock.js）；有缓存时首帧键数就是对的，
      dock 立刻可见，不会有任何延迟。
   ⚠️ 不要给它加 transition / animation：任何过渡都会被读成"在动"，
      这里要的是"一次出现"。700ms 后由 JS 强制摘掉，防接口挂起时一直隐身。 */
.dock.dock--pending {
  opacity: 0;
  pointer-events: none;
}
/* v3.5.8：渐变定义载体。⚠️ 必须脱离布局 —— .dock-btns 是 flex、.dock 也是 flex，
   这个 0×0 的 svg 若不绝对定位会被当成一个 flex 项，把五个键挤窄。 */
.dock-defs {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
  pointer-events: none;
}
.dock.searching .dock-btns {
  display: none;
}
.dock-btn {
  position: relative;
  flex: 0 0 auto;
  width: var(--m-dock-btn);
  height: var(--m-dock-btn);
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 999px;
  color: var(--text-muted);
  transition: background 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
}
.dock-btn svg {
  width: var(--m-dock-icon);
  height: var(--m-dock-icon);
}
.dock-btn:hover {
  color: var(--text);
}
/* 选中态（v3.5.8）：**去掉灰底框**，改由图标自身的渐变表达激活。
   ── 为什么去掉底框：底框原是 40px = 按钮整格，四周只剩 10px，
      视觉上"顶到格子边"；且与胶囊玻璃叠加后层次偏重。
   ── 怎么着色：用 SVG 渐变描边。⚠️ 五个键里有 4 个是线稿（stroke）、
      1 个是实心（fill），所以**不能**只改 fill 或只改 stroke ——
      CSS 里同时给出两条（见下），各自命中对应属性。
   ⚠️⚠️ 渐变用 `url(#grad) currentColor` **双值**写法，不要只写 url()：
      SVG 里 url() 引用一旦解析不到，该属性会**直接失效** ⇒ 图标渲染成
      **完全空白**（实测：0 个可见像素）。加上 currentColor 兜底后，
      即使渐变没生效，图标仍以当前文字色显示（实测 400 像素照常）。
      ⚠️ 这条兜底不是理论风险：渐变 defs 是运行时注入的，任何注入失败
        （拼装顺序、DOM 被替换、旧缓存）都会命中。 */
.dock-btn.active {
  background: none;
  color: transparent;              /* 兜底色交给渐变；失败时由 currentColor 接管 */
  transition: color 0.15s ease;
}
.dock-btn.active svg [stroke] {
  stroke: url(#dock-grad) currentColor;
}
.dock-btn.active svg [fill] {
  fill: url(#dock-grad) currentColor;
}
/* 兜底：万一渐变 defs 没注入成功，图标仍要说得出话 ——
   这时用 --primary 而不是透明，至少是"变深的灰"（退回 v3.5.7 的观感） */
.dock-btn.active svg {
  color: var(--primary);
}
/* ⚠️ v3.13.6：收藏数量徽标的两条样式规则整组删除（用户点名「不用显示收藏数量」）——
   节点、刷新函数与订阅一并移除；不要再加回来（16.36 + 54m1/m2 守着，塞回必报红）。 */

/* ⚠️ v3.4.0：**「dock 设置菜单」整块样式已删除**（原为 v3.2.0 引入，
   含弹出面板、语言分段器、后台入口按钮、齿轮键展开态，约 100 行）。
   删除原因：那两个功能（中英切换 + 后台入口）在 v3.3.5 恢复的顶栏里已经有了，
   这里是重复入口；而 dock 第 5 键已换给「AI 生图」。
   ⇒ 若在别处看到 .dock-settings / .dock-lang / .dock-admin-entry 这类类名，
     说明有人把它加回来了 —— 先问清楚为什么需要第二个语言入口。
   ⚠️ 也不要因为「dock 上少了个齿轮」就去动 --m-dock-btn 或 .dock 的 width：
      键数仍是 5（本次是**换掉**一个键，不是减一个）。 */

/* 展开态：输入框铺满胶囊 */
.dock-search {
  display: none;
  flex: 1 1 auto;
  min-width: 0;
  align-items: center;
  gap: 4px;
}
.dock.searching .dock-search {
  display: flex;
}
.dock.searching {
  width: 343px;
  border-color: var(--primary);
  box-shadow: 0 0 0 3px rgba(84, 84, 84, 0.15),
    0 12px 32px rgba(20, 23, 32, 0.14);
}
.dock-search-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  line-height: 0;
  color: var(--text-faint);
}
.dock-search-icon svg {
  width: 18px;
  height: 18px;
}
.dock-search input {
  flex: 1 1 auto;
  min-width: 0;
  padding: 0 2px;
  border: none;
  background: transparent;
  outline: none;
  font-size: 14px;
  color: var(--text);
}
.dock-search input::placeholder {
  color: var(--text-faint);
}
.dock-search-clear {
  flex: 0 0 auto;
  width: 30px;
  height: 30px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 999px;
  color: var(--text-muted);
  line-height: 0;
  transition: background 0.15s ease, color 0.15s ease;
}
.dock-search-clear:hover {
  background: rgba(20, 23, 32, 0.07);
  color: var(--text);
}
.dock-search-clear svg {
  width: 15px;
  height: 15px;
}

/* 极窄屏：展开态收敛到视口内，避免 343px 溢出 */
@media (max-width: 380px) {
  .dock.searching {
    width: calc(100vw - 28px);
  }
}
/* 移动端输入字号提到 16px —— iOS Safari 对 <16px 的输入框聚焦时会自动放大页面 */
@media (max-width: 640px) {
  .dock-search input {
    font-size: 16px;
  }
}

/* 收藏夹空状态 */
.fav-empty {
  padding: 64px 20px;
  text-align: center;
}
/* 空态在可视区**垂直居中**（v1.5.18，用户："提示移到屏幕中间，现在不够协调"）。
   ⚠️ 全部**作用域在 `.content.is-empty` 之下**（该类由 app.js 按"是否真的没有卡"开关）：
      非空页面布局一字不变 —— 把这条弹性链放到全局，会让每个页面的滚动高度都变。
   ⚠️ 为什么挂 `.content` 而不是 `.card-grid`：只有滚动容器 `.content` 有确定高度，
      由它把弹性链一路传下去（.content-inner → .card-grid），空态才拿得到"剩余可视高度"。
      好处是**不需要任何 px 魔术数字**（旧写法只能写 `100dvh - 顶栏 - 筛选行 - …` 一长串，
      而那几项一改就悄悄错位）。 */
.content.is-empty {
  display: flex;
  flex-direction: column;
}
.content.is-empty .content-inner {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
}
/* align-content:center 在网格里＝把行整体垂直居中（不是 align-items，那是格内对齐） */
.content.is-empty .card-grid {
  flex: 1 1 auto;
  align-content: center;
}
.fav-empty-icon {
  width: 52px;
  height: 52px;
  margin: 0 auto 14px;
  border-radius: 999px;
  background: var(--primary-soft);
  color: var(--primary);
  display: flex;
  align-items: center;
  justify-content: center;
}
.fav-empty-icon svg {
  width: 24px;
  height: 24px;
}
.fav-empty-title {
  font-size: 15px;
  font-weight: 600;
  color: var(--text);
  margin-bottom: 8px;
}
.fav-empty-desc {
  font-size: 13px;
  color: var(--text-muted);
  margin-bottom: 18px;
}
.fav-empty .btn {
  display: inline-flex;
  align-items: center;
  text-decoration: none;
}

.chip-row {
  display: flex;
  flex: 1 1 auto;
  min-width: 0;
  flex-wrap: wrap;
  gap: 10px;
}
.chip {
  padding: 8px 18px;
  border-radius: 999px;
  background: #fff;
  border: 1px solid var(--line);
  color: var(--text);
  font-size: var(--m-chip-fs);
  font-weight: 500;
  transition: all 0.15s ease;
}
.chip:hover {
  border-color: #c5cad6;
}
.chip.active {
  background: var(--primary-soft);
  border-color: var(--primary);
  color: var(--primary);
}

/* =========================================================
   标签：可收起下拉胶囊（v1.16.0）
   收起态只显示当前标签名 + 箭头，点开在下方浮出面板；
   面板 absolute 定位（position:relative 落在 .cat-wrap 上），
   不撑开内容、不改变 .filter-row 高度。
   ========================================================= */
.cat-wrap {
  position: relative;
  min-width: 0;
  max-width: 100%;
  /* v3.2.0：母标签胶囊现在住在 .pagehead-master（页头右侧留品牌位），
     子标签轨那一个 #cat-wrap 是块级独占一行。flex 只对母标签那个 wrap 有意义。 */
  flex: 0 1 auto;
}
/* 收起态胶囊：尺寸照原样，视觉与 .dock 用**同一套毛玻璃**（v1.16.6）
   ⚠️ 底必须是半透明：胶囊吸附后会压住滚动的色卡，且这里同样不能依赖
      backdrop-filter（它在 sticky 祖先内、Safari 行为不一致）
      → 玻璃感由 `rgba(255,255,255,.78)` 承担，blur 只作增强 */
.cat-select {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: 38px;
  /* v3.3.1：`max-width: 100%` → `none`。父层（.cat-wrap / .pagehead-master /
     .pagehead）已经全部按内容定宽，这里再留一道上限只是给"文字被夹掉"留机会。 */
  max-width: none;
  padding: 0 14px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.78);
  -webkit-backdrop-filter: blur(24px) saturate(180%);
  backdrop-filter: blur(24px) saturate(180%);
  border: 1px solid rgba(20, 23, 32, 0.07);
  color: var(--text);
  font-size: 13px;
  font-weight: 600;
  box-shadow: 0 12px 32px rgba(20, 23, 32, 0.14);
  transition: border-color 0.15s ease, background 0.15s ease,
    color 0.15s ease, box-shadow 0.15s ease;
}
.cat-select:hover {
  border-color: #c5cad6;
}
.cat-select-label {
  /* v3.3.1：母标签名**始终完整显示** ⇒ 去掉 overflow/text-overflow 的省略号。
     ⚠️ 只删省略号是不够的：必须**同时**把这条链上所有"可压缩"的点改成不可压缩
        （.pagehead / .pagehead-master / .pagehead-master > .cat-wrap 的 flex: 0 0 auto，
         以及 .cat-select 的 max-width: none）。
        否则文字会**溢出胶囊**画到外面，比省略号更糟。 */
  min-width: 0;
  white-space: nowrap;
}
.cat-caret {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  line-height: 0;
  color: var(--text-muted);
  transition: transform 0.2s ease;
}
.cat-caret svg {
  width: 16px;
  height: 16px;
}
/* 展开态：沿用 .dock-btn.active 那套「主色半透明玻璃」，
   不再用不透明的 --primary-soft，保持整站玻璃语言一致 */
.cat-wrap.open .cat-select {
  background: rgba(84, 84, 84, 0.16);
  border-color: var(--primary);
  color: var(--primary);
}
.cat-wrap.open .cat-caret {
  transform: rotate(180deg);
  color: var(--primary);
}

/* 浮层面板：宽 220，圆角 18px；限高取到能整行放下条目，
   避免底部半截项与面板圆角叠在一起显得「重叠」 */
.cat-panel {
  position: absolute;
  top: calc(100% + 8px);
  /* v1.16.8：胶囊回左对齐 → 面板也回左对齐（与胶囊左缘齐平） */
  left: 0;
  /* v3.3.1：面板宽度改成"装得下最长的那条标签名"。
     ⚠️ 第一行是**兜底**：不认识 max-content 的浏览器仍按 220px 渲染（与旧版一致），
        认识的就取 max-content —— 与本项目 `height: 100vh; height: 100dvh`
        同一套"后一行覆盖前一行、不认识就整条失效"的写法。
     min-width 保住原观感：名字都短时面板不会缩成一小条。
     ⚠️ 面板里的 .cat-item-name / .cat-group-name 仍留着 ellipsis，那是**最后一道保险** ——
        只在 `max-width`（视口宽度）真的被顶满时才会用到。 */
  width: 220px;
  width: max-content;
  min-width: 220px;
  max-width: calc(100vw - 32px);
  display: flex;
  flex-direction: column;
  gap: 2px;
  max-height: min(260px, 42vh);
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 8px;
  border-radius: 18px;
  /* 与胶囊 / dock 同一套玻璃：半透明白 + 大半径模糊，
     面板浮在色卡之上时透出卡片色但文字仍清晰 */
  background: rgba(255, 255, 255, 0.78);
  -webkit-backdrop-filter: blur(24px) saturate(180%);
  backdrop-filter: blur(24px) saturate(180%);
  border: 1px solid rgba(20, 23, 32, 0.07);
  box-shadow: 0 12px 32px rgba(20, 23, 32, 0.14);
  z-index: 30;
  animation: pop 0.14s ease;
}
.cat-panel[hidden] {
  display: none;
}
.cat-item {
  width: 100%;
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 38px;
  padding: 10px 14px;
  border-radius: 12px;
  font-size: 13px;
  font-weight: 500;
  color: var(--text);
  text-align: left;
  transition: background 0.12s ease, color 0.12s ease;
}
.cat-item:hover {
  background: var(--bg-soft);
}
.cat-item-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.cat-item.active {
  background: var(--primary-soft);
  color: var(--primary);
  font-weight: 600;
}
/* 面板项里的数量徽标复用 .chip-count（收藏夹页用），去掉胶囊带来的左外边距 */
.cat-item .chip-count {
  margin-left: 0;
  flex: 0 0 auto;
}
.cat-item.active .chip-count {
  background: var(--primary);
  color: #fff;
}

/* =========================================================
   v3.2.1 · 折叠式面板：母标签列表（可点开）→ 次级子标签列表
   ---------------------------------------------------------
   结构：
     .cat-panel--tree
       ├── .cat-item                        ← 独立顶层标签（可直接选中）
       └── .cat-group-toggle  (button)      ← 母标签：点一下展开/收起，不选中任何东西
             └── .cat-sub-list  (div)         ← 次级菜单，放该组的子标签
                   └── .cat-item--child

   ⚠️ 必须定义在 .cat-item / .cat-panel 之后：同优先级（都是单类选择器）下
      「靠后生效」，写前面会被基础规则的 padding / max-height 盖掉，
      表现为「缩进没出来、面板还是只显示 260px」。
   ⚠️ 为什么母标签是 <button> 而不是 v3.2.0 那个不可点的标题 div：
      它现在**有功能**（展开次级列表），必须是可聚焦、可回车触发的按钮；
      而它不带 .cat-item 类 ⇒ 不会被"选项点击"那套绑定当成选中项
      （组没有卡可筛，选中它没有意义 —— 这条不变式没变）。
   ⚠️ aria 用**折叠面板（disclosure）**语义，不再是 listbox/option：
      listbox 只允许 option/group 作为子节点，塞按钮进去是无效 ARIA。
   ========================================================= */
.cat-group-toggle {
  width: 100%;
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 38px;
  padding: 10px 12px;
  border-radius: 12px;
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
  text-align: left;
  transition: background 0.15s ease, color 0.15s ease;
}
.cat-group-toggle:hover {
  background: var(--bg-soft);
}
/* 展开中的母标签：淡主色底，与下面那段子标签连成一组 */
.cat-group-toggle.open {
  background: var(--primary-soft);
  color: var(--primary);
}
.cat-group-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* 展开箭头靠右；收起时朝下、展开时翻转朝上 */
/* ⚠️ v3.3.5：`.cat-group-badge`（组名旁边那个写着「母标签」的胶囊）已**删除**，
   连同它的两条 CSS 规则。用户要求："母标签展开菜单里不用显示母标签字样标识"。
   理由：组开关已经用「带 ▾ 的可展开行 + 下面缩进的子标签」表达了「这是一组」，
   再挂一个结构名属于把数据模型暴露给用户。
   ⚠️ **不要加回来**；i18n 键 `categories.group` 仍保留在 locales 里（只是不再被渲染）。
   ⚠️ 后台「标签管理」表格里的 `.cat-badge--group`（母/子/分组徽标）**是另一回事**，
      那是管理界面、需要区分层级 —— 与本条无关，不要一起删。 */
.cat-group-caret {
  margin-left: auto;
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  line-height: 0;
  color: var(--text-muted);
  transition: transform 0.2s ease;
}
.cat-group-caret svg {
  width: 16px;
  height: 16px;
}
.cat-group-toggle.open .cat-group-caret {
  transform: rotate(180deg);
  color: var(--primary);
}
/* 次级菜单：左侧一条竖线 + 缩进，明确"这些属于上面那个母标签" */
.cat-sub-list {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin: 0 0 2px 14px;
  padding-left: 8px;
  border-left: 2px solid var(--line);
}
/* .cat-sub-list 是 flex 容器，需显式覆盖 UA 的 [hidden] */
.cat-sub-list[hidden] {
  display: none;
}
/* 子标签项：缩进由外层 .cat-sub-list 的竖线承担，自身只需收一点左内距 */
.cat-item--child {
  padding-left: 10px;
}
/* 两级面板比单级需要更多高度（母 + 子），放宽上限但仍不超一屏 */
.cat-panel--tree {
  max-height: min(62vh, 460px);
}
@media (max-width: 640px) {
  .cat-group-toggle {
    padding: 9px 10px;
  }
  .cat-sub-list {
    margin-left: 10px;
    padding-left: 6px;
  }
  .cat-item--child {
    padding-left: 8px;
  }
}

/* =========================================================
   标签栏：钉左 + 右轨横滚（v1.3.3 · 方案 C）
   ---------------------------------------------------------
   左块 `.cat-pin` 钉住「当前分类」，内部仍复用上面的 `.cat-select`
   玻璃胶囊与全量下拉面板（于是 closeCatPanel / bindCatOutside /
   `.cat-wrap.open` 那一整套逻辑与视觉零改动）；
   右轨 `.cat-rail` 横向滚动**其余**分类，胶囊沿用全站 `.chip` 语言
   （与顶栏 .fav-entry 同族、与后台筛选栏同款），
   形成「静态列表项用白底细边框、悬浮控件才用毛玻璃」的既有分工。
   轨道内不含当前分类 → 无激活项 → 不必把某项滚进视野。
   ========================================================= */
.cat-bar {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  min-width: 0;
}
/* 钉块自身不做定位 → 内部 .cat-panel 仍以 .cat-wrap 为包含块，
   `top: calc(100% + 8px)` 正好落在整行底缘下方。
   flex-shrink 必须为 0：分类名超长时靠 max-width 收口 + 标签省略号，
   而不是被右轨反过来先挤扁 */
.cat-pin {
  flex: 0 0 auto;
  min-width: 0;
  max-width: 46%;
}
.cat-divider {
  flex: 0 0 auto;
  width: 1px;
  height: 20px;
  background: var(--line);
}
.cat-divider[hidden] {
  display: none;
}

/* 右轨：全站唯一的横向滚动端口
   · overflow-x:auto 会按规范把 overflow-y 一并算成 auto，
     所以纵向必须保证不溢出（胶囊无 translateY、无外投影，hover 只换边框色）
   · 上下各 4px 内边距 + 等量负外边距：给键盘焦点环留出绘制空间，
     同时让本行外高仍等于钉块的 38px → 不改变 .filter-row 高度
   · overscroll-behavior-x:contain 防止横向甩动触发页面级返回手势 */
.cat-rail {
  display: flex;
  flex: 1 1 auto;
  min-width: 0;
  align-items: center;
  gap: 10px;
  flex-wrap: nowrap;
  overflow-x: auto;
  overflow-y: hidden;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
  padding: 4px 0;
  margin: -4px 0;
}
.cat-rail::-webkit-scrollbar {
  display: none;
}
/* =========================================================
   v3.3.1 · 轨道胶囊：**选中 / 未选中两套样式对调**
   ---------------------------------------------------------
   交换前：未选中 = 白底 + 细边框 + 正文色   选中 = 淡底 + 主色字
   交换后：未选中 = 淡底 + 主色字            **选中 = 白底 + 细边框 + 正文色**
   用户的要求是"选中激活的那个以白底显示" —— 白底在整行里最亮，天然读作"浮起 / 当前"。
   ⚠️ 两套都保留 **1px 边框**（未选中只是把 border-color 变透明）：
      交换不改变任何胶囊的尺寸 ⇒ 切换选中项时右侧不会整体位移。
   ⚠️⚠️ 这张底**来回改过三轮，当前定稿是「不透明」**，别再动它：
       v3.3.0 未选中=白底细边框 / 选中=`rgba(84,84,84,.08)`
       v3.3.1 两套对调（选中=白底），未选中=**不透明** `var(--primary-soft)`
       v3.3.2 未选中改半透明 `rgba(84,84,84,.08)` → **用户实测反馈「太透明了」**
       v3.3.3 改回**不透明** `var(--primary-soft)`（= 当前）
   ⚠️ **为什么半透明在这里走不通**（根因，别再试）：
       吸顶块是透明的（v3.2.2 起），胶囊底下直接就是彩色色卡 ⇒ 半透明底会
       让**每一枚**未选中胶囊都透出各自那块卡色，而文字是 `--primary`（中灰
       #545454）。8% 的灰几乎盖不住卡色，等于"文字直接压在卡上"——
       压在饱和的深色卡上时整条轨都读不清。
       不透明底则把卡色完全隔开，文字对比度恒定为 7.59:1（对 `--primary-soft`）。
   ⚠️ 若还要"轻一点"的观感，**只能调不透明底的明度**（例如再浅一档的灰），
      不能回到半透明 —— 半透明的问题不是"太淡"，是"透出卡色"。
   ⚠️ 与 `--primary-soft` 的关系：它就是那个淡灰（当前 #ececec，随主色派生）。
      改主色时它自动跟着变，**不需要**手工同步任何 rgba。
   ⚠️ 作用域必须限定在 .cat-rail 内：共享的 `.chip` / `.chip.active` 还被
      后台「色卡列表」的标签筛选栏用着，动基类会把它一起改掉。
   ========================================================= */
.cat-rail .chip {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  white-space: nowrap;
  /* ↓ 交换后的"未选中"样式（v3.3.3：底改回**不透明** —— 半透明实测太透，见上方注释） */
  background: var(--primary-soft);
  border-color: transparent;
  color: var(--primary);
}
/* 「0 条收藏」的置灰信号必须保住：`.cat-rail .chip` 的字色是主色，
   会把共享的 `.chip-empty`（灰字）按优先级盖掉 ⇒ 补一条 3 类的把它赢回来。 */
.cat-rail .chip.chip-empty {
  color: var(--text-faint);
}
/* 计数徽标要跟着反转（收藏夹页才显示数量）：
   未选中胶囊是浅灰底，徽标再用共享的 --bg-soft 就"浅灰压浅灰"等于看不见。
   规则：**徽标永远取与所在胶囊相反的底** —— 灰底上白徽标、白底上灰徽标。 */
.cat-rail .chip .chip-count {
  background: #fff;
}
.cat-rail .chip.active .chip-count {
  background: var(--primary-soft);
  color: var(--text-muted);
}
.cat-rail .chip:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 1px;
}
/* 轨道里的**当前项**（v3.2.1：钉在行首并高亮）。
   v3.3.1：与"未选中"对调后，选中项 = **白底 + 细边框 + 正文色**
   （即全站 `.chip` 的本色），也就是整行里最亮的那一枚。
   ⚠️ 三条都要显式写回来：共享的 `.chip.active`（2 个类）会给它
      `--primary-soft` 底 + 主色边框 + 主色字，而 `.cat-rail .chip`（2 个类）刚刚把
      未选中改成了同一套 —— 不写回来，选中项会和未选中长得一模一样。
      `.cat-rail .chip.active`（3 个类）优先级更高，不需要 !important。
   ⚠️ 边框保留 1px（只是换成 --line），所以交换前后胶囊尺寸完全一致。
   ⚠️ 不写 font-weight：`.chip` 本身就是 500。选中与否靠"底 + 边框 + 字色"三条区分，
      这是用户要的"不那么跳"的观感；要更明确可以再加一条下划线或 600。 */
.cat-rail .chip.active {
  background: #fff;
  border-color: var(--line);
  color: var(--text);
}

/* 两端渐隐：由 JS 按实际滚动位置写 data-fade（滚到端即撤掉，避免糊边） */
.cat-rail[data-fade~='start'] {
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 18px);
  mask-image: linear-gradient(90deg, transparent 0, #000 18px);
}
.cat-rail[data-fade~='end'] {
  -webkit-mask-image: linear-gradient(270deg, transparent 0, #000 18px);
  mask-image: linear-gradient(270deg, transparent 0, #000 18px);
}
.cat-rail[data-fade~='start'][data-fade~='end'] {
  -webkit-mask-image: linear-gradient(
    90deg,
    transparent 0,
    #000 18px,
    #000 calc(100% - 18px),
    transparent 100%
  );
  mask-image: linear-gradient(
    90deg,
    transparent 0,
    #000 18px,
    #000 calc(100% - 18px),
    transparent 100%
  );
}

/* 移动端：横向内衬 28 → 14，与顶栏（padding: 0 14px）对齐成同一条竖线，
   同时把省下的宽度让给右轨，多露出一枚分类胶囊 */
@media (max-width: 640px) {
  .content-inner {
    padding-left: var(--m-padx);
    padding-right: var(--m-padx);
  }
  .cat-bar {
    gap: var(--m-bar-gap);
  }
  .cat-rail {
    gap: var(--m-rail-gap);
  }
  .cat-rail .chip {
    padding: var(--m-chip-py) var(--m-chip-px);
  }
  .cat-pin {
    max-width: 44%;
  }
}

.card-grid {
  display: grid;
  grid-template-columns: repeat(8, minmax(0, 1fr));
  gap: var(--m-grid-r) var(--m-grid-c);
}

@media (max-width: 1280px) {
  .card-grid {
    grid-template-columns: repeat(6, minmax(0, 1fr));
  }
}
@media (max-width: 960px) {
  .card-grid {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}
@media (max-width: 640px) {
  .card-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

.card {
  position: relative;
  border-radius: var(--m-card-r);
  cursor: pointer;
  transition: transform 0.15s ease;
}
.card:hover {
  transform: translateY(-2px);
}
.card .swatch {
  width: 100%;
  aspect-ratio: 1 / 1;
  border-radius: var(--m-card-r);
  background: var(--bg-soft);
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  position: relative;
  overflow: hidden;
  border: 1px solid rgba(0, 0, 0, 0.04);
}
.card .swatch img.lazy {
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity 0.4s ease;
}
.card .swatch img.lazy.loaded {
  opacity: 1;
}
.card .code {
  margin-top: clamp(4px, 1.5vw, 6px);
  font-size: var(--m-code-fs);
  color: var(--text-muted);
  letter-spacing: 0.4px;
  font-weight: 500;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* 「个人颜色」卡片的小标记：在「全部」里跟普通色卡混排时，一眼认出是自己取的色。
   只是个小点（半透明深底 + 白环），不抢色块本身的颜色。
   v3.13.5：移到**右下角**（用户点名）—— 原在右上与收藏夹管理模式的勾选圈
   （.pick-dot，也在右上）重叠；右下角是卡片空位（16.35 守，54l3 证伪）。 */
.card-mine .swatch::after {
  content: '';
  position: absolute;
  bottom: 6px;
  right: 6px;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: rgba(20, 23, 32, 0.55);
  box-shadow: 0 0 0 1.5px rgba(255, 255, 255, 0.9);
  pointer-events: none;
}

.empty {
  text-align: center;
  padding: 60px 0;
  color: var(--text-faint);
}

/* =========================================================
   色域页码（v3.13.10）
   ---------------------------------------------------------
   收起态只有三个控件：上一页 / 页码触发器 / 下一页。
   触发器 = 当前页 + 整页色域渐变 + 总页数 + 竖向虚线 + 展开箭头；
   点它在**上方**浮出面板（无遮罩），每行 = 页码 + 留白虚线 + 整页渐变。

   ⚠️ 几何全部走 --pg-* 变量：三档断点只改变量、不改结构，
      避免"某个断点漏改一条规则"这类只在手机上才暴露的错位。
   ⚠️ 数字对齐是算出来的，不是调出来的（见各条注释的公式）：
      · 触发器页码：左内距 = gap = 6 ⇒ 空白 6+36+6，数字正中最稳；
      · 总页数：渐变右缘 → 6 + 单元 30 + 6 → 虚线，左右各 21；
      · 行页码：单元宽 36、虚线左距 0 ⇒ 空白就是单元本身。
   ========================================================= */
.pager {
  position: relative;
  display: flex;
  justify-content: center;
  /* v3.13.12：内容不足一屏（标签下颜色很少）时，页码靠 margin-top:auto
     压到内容区底部、dock 上方；内容够长时自然跟在卡片后面 —— 见下面
     .content--pager 那条弹性链。 */
  margin: auto 0 0;
  padding-bottom: 6px;
  --pg-control: 42px;            /* 上一页 / 下一页 圆钮 */
  --pg-trigger-h: 56px;
  --pg-trigger-radius: 16px;
  --pg-cell: 30px;               /* 总页单元 = 展开单元（两者必须等宽） */
  --pg-page-w: 36px;             /* 触发器里的当前页码单元 */
  --pg-row-page-w: 36px;         /* 面板行里的页码单元 */
  --pg-grad-w: 100px;
  --pg-grad-h: 36px;
  --pg-dash-h: 22px;
  --pg-dash-margin: 6px;
  --pg-panel-w: 335px;           /* = 整排宽，面板与组件严格等宽 */
  --pg-panel-pad: 8px;
  --pg-row-h: 48px;
  --pg-row-gap: 5px;
  --pg-scroll-h: 289px;          /* 5 整行 + 第 6 行半行 */

  /* 颜色一律取自全站 token（--text / --text-muted / --line / --primary） */
  --pg-line: var(--line);
  --pg-line-strong: #c5cad6;
  --pg-row-line: #e9ebf0;
  --pg-dash: #d8dce5;
  /* 选中行：描边比主色浅一档（2026-10-18 用户点名"再浅一点"），页码仍用主色保持辨识 */
  --pg-active-line: #8f929b;
  --pg-active-ink: var(--primary);
  --pg-hover-bg: #fcfcfd;
}
.pager[hidden] {
  display: none;
}

/* v3.13.12：页码的「贴底」弹性链。
   .content 变列 → 顶栏占自然高度，.content-inner 吃满剩余高度；
   .pager 的 margin-top:auto 再把页码推到内容区底部（dock 上方 96px 内衬之上）。
   内容够长时这条链自动失效（内容把容器撑高），页码仍跟在最后一行卡片后面。
   ⚠️ 28px 最小间距写在 .card-grid 的 margin-bottom 上，不能写成 .pager 的
      padding-top —— 面板锚点是 .pager 的 border box，上内距会把浮层面板推高 28px。
   ⚠️ 只给带页码的两页（index / favorites）加 .content--pager，account / ai 不受影响。 */
.content--pager {
  display: flex;
  flex-direction: column;
}
/* ⚠️ 滚动容器里的 flex 子项默认 flex-shrink:1 —— 内容一超屏，浏览器会**压缩**
   顶栏和吸顶标签栏（实测顶栏 64 → 41），表现就是"顶栏变窄/变矮了"。
   除内容段外一律禁止收缩。 */
.content--pager > * {
  flex: 0 0 auto;
}
.content--pager > .content-inner {
  flex: 1 0 auto;   /* 只许长高，不许缩短 */
}
.content-inner--pager {
  display: flex;
  flex-direction: column;
}
.content-inner--pager .card-grid {
  flex: 0 0 auto;
  margin-bottom: 28px;
}


.pg-pager {
  display: flex;
  align-items: center;
  gap: 8px;
}

.pg-nav {
  width: var(--pg-control);
  height: var(--pg-control);
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border-radius: 50%;
  border: 1px solid var(--pg-line);
  background: #fff;
  color: var(--text);
  box-shadow: var(--shadow-sm);
  transition: border-color 0.15s ease, background 0.15s ease, opacity 0.15s ease;
}
.pg-nav:hover:not(:disabled) {
  border-color: var(--pg-line-strong);
  background: var(--pg-hover-bg);
}
.pg-nav:disabled {
  opacity: 0.28;
  cursor: not-allowed;
  box-shadow: none;
}
.pg-nav svg {
  width: 14px;
  height: 14px;
}

/* 整颗触发器就是热区：页码 / 渐变 / 总页 / 虚线 / 箭头都只是内部布局 */
.pg-trigger {
  height: var(--pg-trigger-h);
  display: flex;
  align-items: center;
  gap: 6px;
  /* 右内距 6 = 左内距，且 (42-30)/2 = 6 ⇒ 箭头中心与下一页图标中心同一条竖线 */
  padding: 6px;
  border-radius: var(--pg-trigger-radius);
  border: 1px solid var(--pg-line);
  background: #fff;
  box-shadow: var(--shadow-sm);
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.pg-trigger:hover {
  border-color: var(--pg-line-strong);
}
.pg-trigger[aria-expanded="true"] {
  border-color: var(--pg-line-strong);
  box-shadow: var(--shadow-md);
}

.pg-page {
  width: var(--pg-page-w);
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-muted);
  font-size: 13px;
  font-weight: 600;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

.pg-grad {
  display: block;
  border-radius: 8px;
  background-color: var(--bg-soft);
  box-shadow: inset 0 0 0 1px rgba(20, 23, 32, 0.06);
}
.pg-grad--trigger {
  width: var(--pg-grad-w);
  height: var(--pg-grad-h);
  flex: 0 0 auto;
}

.pg-right {
  display: flex;
  align-items: center;
  height: 100%;
  flex: 0 0 auto;
}
.pg-total {
  width: var(--pg-cell);
  height: var(--pg-cell);
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-muted);
  font-size: 13px;
  font-weight: 600;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
.pg-rule {
  width: 1px;
  height: var(--pg-dash-h);
  flex: 0 0 auto;
  margin: 0 var(--pg-dash-margin);
  background-image: repeating-linear-gradient(
    to bottom,
    var(--pg-dash) 0 2px,
    transparent 2px 6px
  );
}
.pg-expand {
  width: var(--pg-cell);
  height: var(--pg-cell);
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  color: var(--text);
}
.pg-expand svg {
  width: 14px;
  height: 14px;
  transition: transform 0.2s ease;
}
.pg-trigger[aria-expanded="true"] .pg-expand svg {
  transform: rotate(180deg);
}

/* ---------------------------- 面板 ---------------------------- */
.pg-panel {
  position: absolute;
  left: 50%;
  bottom: calc(100% + 13px);
  width: var(--pg-panel-w);
  padding: var(--pg-panel-pad);
  background: #fff;
  border: 1px solid var(--pg-line);
  border-radius: 20px;
  box-shadow: var(--shadow-lg);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transform: translate(-50%, 10px) scale(0.985);
  transform-origin: 50% 100%;
  transition: opacity 0.17s ease, transform 0.22s cubic-bezier(0.22, 0.78, 0.26, 1),
    visibility 0s linear 0.22s;
  z-index: 2;
}
/* 面板底部小三角：指向触发器 */
.pg-panel::after {
  content: "";
  position: absolute;
  left: 50%;
  bottom: -6px;
  width: 11px;
  height: 11px;
  background: #fff;
  border-right: 1px solid var(--pg-line);
  border-bottom: 1px solid var(--pg-line);
  transform: translateX(-50%) rotate(45deg);
  border-bottom-right-radius: 2px;
}
.pager.is-open .pg-panel {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transform: translate(-50%, 0) scale(1);
  transition: opacity 0.17s ease, transform 0.22s cubic-bezier(0.22, 0.78, 0.26, 1),
    visibility 0s;
}

.pg-scroll {
  position: relative;          /* 行 offsetTop 才能相对滚动区算（ensureVisible） */
  width: 100%;
  border-radius: 14px;
  overflow: hidden;
}
.pg-scroll.is-scrollable {
  height: var(--pg-scroll-h);
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
.pg-scroll.is-scrollable::-webkit-scrollbar {
  display: none;
}
.pg-list {
  display: flex;
  flex-direction: column;
  gap: var(--pg-row-gap);
  width: 100%;
}

.pg-row {
  width: 100%;
  height: var(--pg-row-h);
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  overflow: hidden;
  padding: 0;
  border: 1px solid var(--pg-row-line);
  border-radius: 14px;
  background: #fff;
  text-align: left;
  transition: border-color 0.15s ease, background 0.15s ease;
}
.pg-row:hover {
  border-color: #d4d8e1;
  background: #fbfbfd;
}
.pg-row.is-active {
  border: 1.5px solid var(--pg-active-line);
}
.pg-row-page {
  width: var(--pg-row-page-w);
  height: 100%;
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text);
  font-size: 13px;
  font-weight: 600;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
.pg-row.is-active .pg-row-page {
  font-weight: 700;
  color: var(--pg-active-ink);
}
.pg-row-rule {
  width: 1px;
  height: 24px;
  flex: 0 0 auto;
  margin: 0 6px 0 0;
  background-image: repeating-linear-gradient(
    to bottom,
    var(--pg-dash) 0 2px,
    transparent 2px 6px
  );
}
.pg-row-grad-wrap {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  padding: 0 5px 0 0;
}
.pg-row-grad {
  width: 100%;
  height: 36px;
  border-radius: 8px;
  background-color: var(--bg-soft);
  box-shadow: inset 0 0 0 1px rgba(20, 23, 32, 0.06);
}

/* 第 6 行只露半行：底部白色渐隐，滚到底后消失 */
.pg-fade {
  position: absolute;
  left: var(--pg-panel-pad);
  right: var(--pg-panel-pad);
  bottom: var(--pg-panel-pad);
  height: 14px;
  border-radius: 0 0 13px 13px;
  background: linear-gradient(to bottom, rgba(255, 255, 255, 0), #fff 96%);
  pointer-events: none;
  z-index: 3;
  transition: opacity 0.16s ease;
}
.pg-fade.is-hidden {
  opacity: 0;
}

/* 键盘焦点环与全站同口径 */
.pager button:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 3px;
}
/* v3.13.12：指针打开时**不画**焦点环。
   脚本 focus() 在部分 WebKit / 触摸场景下会被判成 :focus-visible，2px 环
   + 3px offset 超出面板后被裁掉一截 —— 就是用户看到的"第一格下方一道黑线"。
   键盘一按（onHostKeydown）立刻移除这个类，Tab / 方向键仍有正常焦点环。 */
.pager.is-pointer-focus button:focus {
  outline: none;
}

/* ============================ 响应式 ============================ */
@media (max-width: 640px) {
  .pager {
    --pg-control: 44px;
    --pg-trigger-h: 52px;
    --pg-trigger-radius: 14px;
    --pg-cell: 34px;
    --pg-page-w: 30px;
    --pg-row-page-w: 32px;
    --pg-grad-w: 84px;
    --pg-grad-h: 34px;
    --pg-dash-h: 20px;
    --pg-dash-margin: 5px;
    --pg-panel-pad: 7px;
    --pg-row-h: 46px;
    --pg-row-gap: 4px;
    --pg-scroll-h: 273px;
  }
  .pg-pager { gap: 7px; }
  .pg-trigger { gap: 5px; padding: 5px; }
  .pg-total { font-size: 12.5px; }
  .pg-panel {
    width: min(366px, calc(100vw - 24px));
    border-radius: 18px;
  }
  .pg-row { border-radius: 13px; }
  .pg-row-page { font-size: 12.5px; }
  .pg-row-rule { height: 22px; margin-right: 5px; }
  .pg-row-grad-wrap { padding-right: 4px; }
  .pg-row-grad { height: 34px; }
}

@media (max-width: 380px) {
  .pager {
    --pg-control: 44px;
    --pg-cell: 36px;
    --pg-page-w: 28px;
    --pg-row-page-w: 30px;
    --pg-grad-w: 76px;
    --pg-dash-margin: 4px;
    --pg-panel-pad: 6px;
  }
  .pg-pager { gap: 5px; }
  .pg-trigger { gap: 4px; padding: 4px; }
}

/* 320 也要完整可用：再收一档，否则整排会超出内容区（实测 301 > 296） */
@media (max-width: 344px) {
  .pager {
    --pg-control: 40px;
    --pg-trigger-h: 50px;
    --pg-trigger-radius: 13px;
    --pg-cell: 34px;
    --pg-page-w: 26px;
    --pg-row-page-w: 28px;
    --pg-grad-w: 60px;
    --pg-grad-h: 32px;
    --pg-dash-h: 18px;
    --pg-dash-margin: 3px;
  }
  .pg-pager { gap: 4px; }
  .pg-trigger { gap: 3px; padding: 3px; }
  .pg-total { font-size: 12px; }
  .pg-panel { border-radius: 16px; }
  .pg-row-grad { height: 30px; }
}

/* =========================================================
   弹层：Modal
   ========================================================= */

@keyframes pop {
  from {
    transform: translateY(-4px) scale(0.98);
    opacity: 0;
  }
  to {
    transform: translateY(0) scale(1);
    opacity: 1;
  }
}

.modal-mask {
  position: fixed;
  inset: 0;
  background: rgba(20, 23, 32, 0.4);
  z-index: 90;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
  animation: fade 0.15s ease;
}
@keyframes fade {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}
.modal {
  background: #fff;
  border-radius: var(--radius-lg);
  width: 100%;
  max-width: 460px;
  box-shadow: var(--shadow-lg);
  animation: pop 0.18s ease;
}
.modal .modal-head {
  padding: 18px 22px 6px;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.modal .modal-head h3 {
  margin: 0;
  font-size: 16px;
  font-weight: 600;
}
.modal .modal-head .close {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-muted);
  font-size: 18px;
}
.modal .modal-head .close:hover {
  background: var(--bg-soft);
}
.modal .modal-body {
  padding: 14px 22px;
  max-height: 70vh;
  overflow: auto;
}
.modal .modal-foot {
  padding: 12px 22px 18px;
  display: flex;
  justify-content: flex-end;
  gap: 8px;
}

/* =========================================================
   灯箱：TrueColor™ 改色图 + 色彩采样 + 相似色匹配
   同一滚动容器，纵向可滑动
   ========================================================= */
.lightbox {
  background: #fff;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  width: 100%;
  max-width: 860px;
  /* 固定高度：改色图 / 色彩采样两态共用同一容器高度，切换不塌陷、不跳动 */
  /* ⚠️ 与 .app 同源问题（v1.3.6）：`100vh` 是 **large viewport**，安卓浏览器
     工具栏展开时可视高度比它矮一截 → 灯箱底部（相似色 / 按钮行）被压在工具栏
     后面且划不上来。故写 vh + dvh 回退对：不认 dvh 的老浏览器整条失效，
     自动退回上一行的 vh；认得的用真实可视高度。 */
  height: calc(100vh - 32px);
  height: calc(100dvh - 32px);
  /* v1.5.18：**高度封顶 900px**。大屏（4K / 27"）上这个 dialog 会跟着视口长到
     1400px 以上，而主图区是弹性项 → 被拉到近千像素高，照片按 contain 缩在中间一小块、
     上下全是留白；相似色却仍只是底部一条细带（用户："相似色和主色显示分配不合理、
     取色与灯箱模块太窄"）。封顶后 4K 上的高度 ≈ 13" 笔记本的 868px，观感直接对齐。
     ⚠️ 只封 max-height：短屏照旧走 calc(100dvh - 32px)，手机上另有全屏规则覆盖。 */
  max-height: min(900px, calc(100vh - 32px));
  max-height: min(900px, calc(100dvh - 32px));
  display: flex;
  flex-direction: column;
  animation: pop 0.18s ease;
  overflow: hidden;
}
.lightbox-head {
  flex: 0 0 auto;
  padding: 14px 18px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  border-bottom: 1px solid rgba(0, 0, 0, 0.06);
}
/* 大屏加宽（v1.5.18）：固定 860px 在 1440 / 1920 / 2560 宽的逻辑视口里只占
   60% / 45% / 34% —— 越宽的屏上，弹窗越像"屏幕正中一根窄条"。
   加宽同时也让相似色色块跟着变大（6 列均分，宽度即色块宽）：
   860→1040 时色块 130→160，860→1180 时 130→183。
   ⚠️ 断点刻意从 **1600px** 起：13" 笔记本（1280 / 1440）都落在断点之下
      → 用户明确说"13 寸上的布局体验比较合理"，那两档必须**一字不变**。
   ⚠️ 只有 max-width 在变，高度由上面的 min(900px, …) 管，两者配合才是大屏观感的解。
   ⚠️ 取色窗**另有一条更窄的加宽规则**（`.picker`，写在它自己那条 base 之后）——
      不要以为这里有就够了。 */
@media (min-width: 1600px) {
  .lightbox {
    max-width: 1040px;
  }
}
@media (min-width: 2100px) {
  .lightbox {
    max-width: 1180px;
  }
}
.lightbox-title {
  font-size: 15px;
  font-weight: 600;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.lightbox-head .close {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-muted);
  flex-shrink: 0;
}
/* 关闭图标尺寸：**显式**给 SVG 定值。
   ⚠️ 三处灯箱（色卡灯箱 / 个人颜色灯箱 / 取色向导）共用本规则；只要图标是同一条
   路径（app.js 的 ICONS.close 与 picker.js 的 ICONS.close 逐字节一致），叉号大小
   就必然相同 —— 比各自靠 font-size 排文本字符可靠得多。
   ⚠️ 别再退回文本 `×` + font-size：18px 的字符视觉只有 ≈12.6px，
   而这里 32px 的 SVG 视觉 ≈16px（叉号路径在 24 网格里只占中间 12 单位），
   「收藏页的叉叉比取色的小一圈」就是从这个差别来的。 */
.lightbox-head .close svg {
  width: 32px;
  height: 32px;
  display: block;
}
.lightbox-head .close:hover {
  background: var(--bg-soft);
}
/* 头部右侧操作区：TrueColor™ 开关 + 关闭按钮 */
.lightbox-actions {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-shrink: 0;
}
/* ===== TrueColor™ 开关 ===== */
/* 外层仅作排布容器：文案不可点击，只有 .tc-switch-toggle（开关本体）可点 */
.tc-switch {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  user-select: none;
}
/* TrueColor™ 开关文案：字号与灯箱编号、相似色匹配统一（15px） */
.tc-switch-label {
  font-size: 15px;
  font-weight: 500;
  color: var(--text-muted);
  letter-spacing: 0.2px;
  white-space: nowrap;
  cursor: default;
  pointer-events: none;
}
.tc-switch-toggle {
  display: inline-flex;
  align-items: center;
  cursor: pointer;
}
.tc-switch-input {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
  pointer-events: none;
}
.tc-switch-track {
  position: relative;
  flex-shrink: 0;
  width: 40px;
  height: 22px;
  border-radius: 999px;
  background: #d4d8e1;
  transition: background 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.tc-switch-thumb {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 1px 3px rgba(20, 23, 32, 0.25);
  transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.tc-switch-input:checked + .tc-switch-track {
  background: var(--primary);
}
.tc-switch-input:checked + .tc-switch-track .tc-switch-thumb {
  transform: translateX(18px);
}
.tc-switch-input:focus-visible + .tc-switch-track {
  box-shadow: 0 0 0 3px var(--primary-soft);
}
/* 文案为静态展示：不可点击，也不随鼠标悬停变色 */
/* 内容区：主色区弹性吸收剩余高度，相似色区固定不压缩 —— 移动端同屏看完不滚动 */
.lightbox-body {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  padding: 16px;
  /* 仅在可用高度低于主色区 min-height 的极矮屏上才出现滚动 */
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}
.lightbox-panel {
  flex: 1 1 auto;
  /* 媒体区最小高度，防止主色区被压扁 */
  min-height: var(--lb-media-min);
  display: flex;
  flex-direction: column;
  min-width: 0;
  /* 收藏按钮绝对定位的参照（主图右下角） */
  position: relative;
}
/* ⚠️ v2.0.1 灯箱主图「压住相似色」的兜底裁剪：
   `.recolor-slot` 里的 <img> 曾经按内容撑高（见下方 1780 那条的注释），
   槽位比面板高 → 溢出的主图**在相似色色块下面接着画**（后画者在上）——
   症状是「相似色标题看不见（深色照片上的深色字）+ 色块像贴在主图上」。
   真正修因果的是 1780 把图片改成绝对定位；这条是"无论谁再撑高都出不去"的硬保险。
   ⚠️ 用 :not(.picker-panel) 排除取色向导：那边复用 .lightbox-panel 的类名，
      面板里是步骤区（拖放 / 框选 / 结果），裁掉溢出可能切到它自己的东西。 */
.lightbox-panel:not(.picker-panel) {
  overflow: hidden;
}
/* 主色区媒体：改色图与 lab 色块填满弹性区域，切换不跳动 */
.lightbox-color {
  flex: 1 1 auto;
  width: 100%;
  min-height: var(--lb-media-min);
  border-radius: 12px;
  border: 1px solid rgba(0, 0, 0, 0.06);
}
.lightbox-img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  border-radius: 12px;
  border: 1px solid rgba(0, 0, 0, 0.06);
  background: var(--bg-soft);
}
/* ===== 取色结果页的主图区：并列双图 → 窄屏改上下布局 =====
   ⚠️ v1.5.6：这组类原先属于「个人颜色」灯箱（v1.4.1 建立），而**那盏灯箱已删除**
      （收藏里的取色颜色改用常规色卡灯箱、不再显示框选图）——
      现在只有一个使用方：**取色向导的结果页**。所以别因为"灯箱没了"就删掉它们。
   左/上 = 刚框选的照片 + 框选位置（由 app.js 的 paintShot() 用 canvas 画死）
   右/下 = 框出来的平均色（TrueColor™ 开启时这一格显示改色图）
   ⚠️ 照片格不用「<img> + 绝对定位选框」：img 在容器里是 contain 适配，
      实际显示框与容器不一致（上下留边），百分比定位必然错位；
      canvas 是替换元素，max-width/max-height:100% 就能保比例内接，
      几何完全确定、不依赖各浏览器对百分比高度的解析差异。 */
.mine-pair {
  flex: 1 1 auto;
  min-height: var(--lb-media-min);
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
  /* ⚠️ 定位祖先（v1.5.16）：结果页的「收藏」键挂在它下面，而不是挂在改色图那一格里 ——
     因为改色失败时 app.js 会对那一格做 `innerHTML = 提示`，放进去的键会被一起抹掉。
     键距右/下各 12px，落在右格（手机上＝下面那格）的右下角。 */
  position: relative;
}
/* 结果页拿不到缩略图时（极端情况）→ 右图铺满整行，不留空槽 */
.mine-pair.single {
  grid-template-columns: 1fr;
}
.mine-half {
  min-width: 0;
  /* ⚠️ 必须是定位祖先：结果页的两个功能键（收藏 / 返回框选，v1.5.16）按它定位。
     少了这一行，两个键都会以 .picker-panel 为参照 → 一起叠到右格右下角（重合）。
     它同时是取色图那一格的定位/居中容器（v2.0.0 起那一格是 .mine-color 纯色块，
     原本的 .mine-recolor 已随服务端改色引擎删除），故不影响既有布局。 */
  position: relative;
  border-radius: 12px;
  border: 1px solid rgba(0, 0, 0, 0.06);
  overflow: hidden;
  background: var(--bg-soft);
}
.mine-photo {
  display: flex;
  align-items: center;
  justify-content: center;
}
.mine-shot {
  display: block;
  max-width: 100%;
  max-height: 100%;
}
/* TrueColor™ 开启时，「平均色」那一格显示改色图（v1.4.4）
   ⚠️ 刻意**不复用 .recolor-slot**：那个类带 min-height:var(--lb-media-min)，
      一旦跟着 .mine-half 进入 .mine-pair 的网格格，窄屏上下布局的每一格都会
      被那 120px 托住、两格不再等高，所以那边用独立的 .mine-color。
   ⚠️⚠️ v2.0.1 修的灯箱布局 bug **根因就在这里**：上面这段注释原先**漏了收尾的
      注释结束符**（星号 + 斜杠），后果不是"注释没写完"，而是
      **紧跟着的 4 组规则整块被吞进注释里**：
        .recolor-slot / .recolor-slot .lightbox-img / .recolor-loading,.recolor-error
        / .recolor-error
      也就是说，这些规则**在线上从来没有生效过**。其中 `.recolor-slot` 失去
      `flex:1 1 auto` + `min-height` 之后，它的"自动最小尺寸"退回**内容最小尺寸**：
      槽位里那张静态定位的 <img> 按固有宽高比撑出 828×622，槽位便拒绝收缩到
      面板分给它的 420 → 溢出部分画到 `.similar` 底下（相似色标题被深色照片吞掉、
      色块像贴在照片上）。**这是本次灯箱 bug 的真正根因，不是 object-fit 的问题。**
   ⚠️ 为什么一直没被发现：`css-parse-check` 对"未闭合注释"**不报错** —— CSS 注释里
      允许出现任意字符（包括 `{` `}` 和整条规则），解析器只当它不存在，
      于是检查**静默通过**，代价是规则数悄悄少了 4 组（445 vs 449；另加下面这组
      兜底规则后为 450）。同类纪律见《开发护栏》第 14 章：检查器对"被跳过的
      内容"必须另有断言。⚠️ 本条真正的守卫是 tools/checks/07-s16-ui.mjs 的
      16.22 组 —— 它断言这几条规则**真实存在**（跑在剥注释文本上，被吞掉必报红），
      而"规则数守恒"那类断言在这里**没用**（注释内外的 `{` 会一起变少，仍然配平）。 */
.recolor-slot {
  flex: 1 1 auto;
  width: 100%;
  min-height: var(--lb-media-min);
  border-radius: 12px;
  border: 1px solid rgba(0, 0, 0, 0.06);
  background: var(--bg-soft);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  /* ⚠️ v2.0.1：绝对定位主图的参照盒（下方那条 inset:0 靠它定位） */
  position: relative;
}
/* TrueColor 主图：**绝对定位填满槽位**，不是「static + width/height:100%」。
   ⚠️ v2.0.1 修的就是这一条（灯箱相机图的"布局 bug"根因）：
      静态定位的 <img> 在 `height:100%` 解析不出来时（父链上是 flex 弹性高，
      百分比高度形成循环 → 退回 auto），会按**固有宽高比**撑出 828×622 的盒子，
      于是槽位的内容高 = 622（> 面板实际分到的 420）→ 槽位溢出面板、
      溢出的部分画到了 `.similar` 底下 —— 表现为「相似色标题被照片盖掉、
      色块像贴在照片上、照片底部被切」。
      改成 inset:0 后：① 图片盒 = 槽位盒（绝对定位的包含块尺寸必然确定，
      与百分比高度是否可解析无关）；② 绝对定位子元素**不参与**父元素的内容尺寸计算
      → 槽位的 flex-basis 归零 → 槽位只能拿 flex 分给它的高度 → 循环依赖根除。
   ⚠️ object-fit: contain 必须留着：盒子比例与照片比例不一致时靠它等比内接（不拉伸）。 */
.recolor-slot .lightbox-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  border: 0;
  border-radius: 0;
  background: transparent;
}
.recolor-loading,
.recolor-error {
  font-size: 13px;
  color: var(--text-muted);
}
.recolor-error {
  color: #c0392b;
}
/* TrueColor™ 不可用时的回落提示：容器底色已是 lab() 真实色，提示压在上面 */
.recolor-fallback-note {
  padding: 3px 10px;
  border-radius: 99px;
  font-size: 12.5px;
  color: #fff;
  background: rgba(0, 0, 0, 0.42);
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
}
/* 鉴权取图失败时的可见提示（v1.5.19）：
   ⚠️ 破图与「上传失败」长得一模一样 —— 必须给一句明确的话，否则用户会把 401
/* 后台：标题栏操作按钮组 */
.head-actions {
  display: flex;
  align-items: center;
  gap: 8px;
}

/* ===== 后台：色卡批量操作栏 ===== */
/* 后台色卡列表：按标签筛选（复用前台 .chip 胶囊语言，尺寸收小） */
.admin-filter {
  margin-bottom: 12px;
  gap: 8px;
}
.admin-filter:empty {
  display: none;
}
.admin-filter .chip {
  padding: 5px 14px;
  font-size: 12px;
}

/* 批量选择模式：默认折叠复选框列，点「批量选择」后才展开 */
#panel-cards .col-check {
  display: none;
}
#panel-cards.bulk-mode .col-check {
  display: table-cell;
}

.bulk-bar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 12px;
  padding: 9px 14px;
  border-radius: var(--radius-md);
  background: var(--primary-soft);
  border: 1px solid #d9e0f0;
}
.bulk-bar[hidden] {
  display: none;
}
.bulk-bar .spacer {
  flex: 1;
}
.bulk-all {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  font-weight: 500;
  color: var(--primary);
  cursor: pointer;
  user-select: none;
}
.bulk-all input[type='checkbox'],
.table input[type='checkbox'] {
  width: 16px;
  height: 16px;
  margin: 0;
  accent-color: var(--primary);
  cursor: pointer;
  vertical-align: middle;
}
/* 已选中的行做浅色高亮，便于核对 */
.table tr.row-selected > td {
  background: #f6f8fc;
}
/* 后台：CSV 导入弹窗 */
.csv-toolbar {
  display: flex;
  gap: 8px;
  margin-bottom: 10px;
}
.csv-text {
  width: 100%;
  min-height: 170px;
  resize: vertical;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12px;
  line-height: 1.6;
  white-space: pre;
  overflow: auto;
}
.csv-modes {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  margin-top: 12px;
}
.csv-modes .radio {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 13px;
  color: var(--text-muted);
  cursor: pointer;
}
.csv-report {
  margin-top: 12px;
  font-size: 13px;
  line-height: 1.7;
}
.csv-report.ok {
  color: #1a7f37;
}
.csv-report.warn {
  color: #9a6700;
}
.csv-report.error {
  color: #c0392b;
}
.csv-errors {
  margin: 6px 0 0;
  padding-left: 18px;
  color: #c0392b;
  font-size: 12px;
  max-height: 130px;
  overflow: auto;
}
.csv-errors li {
  margin: 2px 0;
}

/* 后台：分类种子色单元格 */
.cat-default {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  cursor: pointer;
  user-select: none;
  white-space: nowrap;
}
.cat-default input[type='checkbox'] {
  width: 16px;
  height: 16px;
  accent-color: var(--primary);
  cursor: pointer;
}
.cat-default-text {
  font-size: 11px;
  font-weight: 600;
  color: var(--primary);
  background: var(--primary-soft);
  border: 1px solid #d9e0f0;
  border-radius: 999px;
  padding: 1px 8px;
}
.cat-default-text:empty {
  display: none;
}

/* ===== 标签层级（v3.2.0）：母标签 / 子标签 ===== */

/* 子标签行：左侧一道主色细线 + 轻微底色，缩进感来自 .cat-name-indent 的 └ 字符
   ⚠️ 刻意**不用纯 padding 缩进整行** —— 那会把 ID / 开关等列一起推右，
   在窄屏下把「删除」按钮挤出可视区（后台表格是 overflow:auto，被挤出去就是点不到）。
   只缩进名称单元格里的文字，其余列与母标签行严格对齐。 */
.cat-row-child td {
  background: rgba(84, 84, 84, 0.028);
}
.cat-row-child .cat-name-cell {
  border-left: 2px solid rgba(84, 84, 84, 0.28);
  padding-left: 10px;
}
.cat-name-indent {
  color: var(--text-faint);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
.cat-name-cell {
  white-space: nowrap;
}
/* 标签名后的小徽标：母 / 子 / 分组容器 */
.cat-badge {
  display: inline-block;
  margin-left: 6px;
  padding: 1px 7px;
  border-radius: 999px;
  font-size: 10.5px;
  font-weight: 600;
  line-height: 1.6;
  vertical-align: 1px;
}
.cat-badge--group {
  color: var(--primary);
  background: var(--primary-soft);
  border: 1px solid #d9e0f0;
}
.cat-badge--child {
  color: #6b7080;
  background: #f0f2f7;
  border: 1px solid var(--line);
}
.cat-badge--sealed {
  color: #8a5b12;
  background: #fdf6ec;
  border: 1px solid #f3dfbd;
}
/* 直接挂在本标签下的色卡数（一眼看出「这个标签能不能当母标签」） */
.cat-cardcount {
  display: inline-block;
  margin-left: 6px;
  font-size: 11px;
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
}
.cat-cardcount::before {
  content: '· ';
}
/* v3.13.0：标签的「模板 · 排序」注脚（名称格第二行，一眼看清呈现配置） */
.cat-meta {
  margin-top: 2px;
  font-size: 10.5px;
  color: var(--text-faint);
}
/* 拖动落点：插入到行前/后 */
.cat-row-top.drag-over td,
.cat-row-child.drag-over td {
  box-shadow: inset 0 0 0 9999px rgba(84, 84, 84, 0.07);
}
.cat-row-top.drag-over:not(.dragging) td:first-child,
.cat-row-child.drag-over:not(.dragging) td:first-child {
  box-shadow: inset 0 3px 0 -1px var(--primary);
}
/* 拖动落点：**收为子标签**（居中区域）—— 整行高亮 + 主色描边，
   与「插入到前后」的细线落点视觉上必须能一眼分开，否则用户不知道松手后会变成哪种 */
.drag-into td {
  background: var(--primary-soft) !important;
  box-shadow: inset 0 0 0 2px rgba(84, 84, 84, 0.42);
}
.drag-into .cat-name-cell::after {
  content: ' ⤵';
  color: var(--primary);
  font-weight: 600;
}

/* 弹窗里的「标签类型」单选（母标签 / 子标签） */
.cat-kind-row {
  display: flex;
  gap: 16px;
  align-items: center;
  padding-top: 2px;
}
.cat-kind {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: 13px;
  cursor: pointer;
  user-select: none;
}
.cat-kind input[type='radio'] {
  width: 15px;
  height: 15px;
  accent-color: var(--primary);
  cursor: pointer;
}
.cat-kind input[type='radio']:disabled + span {
  color: var(--text-faint);
}
.field-hint {
  margin: 6px 0 0;
  font-size: 12px;
  line-height: 1.6;
  color: var(--text-muted);
}


/* ===== 「个人颜色」虚拟标签（标签管理页里单独一块，v1.4.5）=====
   ⚠️ 与上面的分类表**在视觉上就要分家**：加一道分隔线 + 独立底色，
   让「它和上面那些能拖、能删的分类不是一类东西」一眼可见。
   不放进 .table 里也刻意为之 —— 那会继承表格的斑马纹与行悬停，
   看起来就像第 N 个分类。 */
.cat-personal-sec {
  margin-top: 22px;
  padding: 16px 18px 18px;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--bg-soft);
}
.cat-personal-head {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 6px;
}
.cat-personal-head h4 {
  margin: 0;
  font-size: 15px;
  font-weight: 600;
}
/* 「不可删除」标记：把它做成一个中性小胶囊，而不是红色警告 —— 这是设计约束，不是错误 */
.cat-personal-tag {
  font-size: 11px;
  line-height: 1;
  padding: 5px 8px;
  border-radius: 99px;
  background: rgba(0, 0, 0, 0.05);
  color: var(--text-muted);
  white-space: nowrap;
}
.cat-personal-hint {
  margin: 0 0 14px;
  font-size: 12px;
  line-height: 1.6;
  color: var(--text-muted);
  max-width: 720px;
}
.cat-personal-row {
  display: flex;
  align-items: flex-end;
  gap: 12px;
  flex-wrap: wrap;
}
.cat-personal-row .field {
  flex: 1 1 220px;
  min-width: 180px;
}
/* 「个人颜色」虚拟标签的渲染配置区（v2.0.0 起只剩「相似色匹配默认标签」一项）：
   原先这里并排放「改色源图 / 种子色 / 相似色默认标签」三块，直接复用分类表的
   .cat-src / .cat-seed 两个组件（连上传、清除按钮的样式都一致）。那两块已随
   服务端改色引擎删除，对应的组件样式也一并清掉了 —— 现在只剩一个下拉。
   ⚠️ 用 grid + auto-fit 而不是横向滚动：这块是表单不是表格，窄屏折行比让人左右拖更合理
   （分类表可以横拖是因为它每列都要对齐着看）。 */
.cat-personal-assets {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 0 20px;
  margin-top: 14px;
}
.cat-personal-assets .field {
  margin-bottom: 10px;
}
.cat-personal-note {
  margin-top: 8px;
  font-size: 12px;
  color: var(--text-faint);
}
/* 种子色取色弹窗 */
.seed-picker {
  position: relative;
  width: 100%;
  max-height: 46vh;
  overflow: auto;
  border-radius: 12px;
  border: 1px solid var(--line);
  background: var(--bg-soft);
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 160px;
}
.seed-picker canvas {
  display: block;
  max-width: 100%;
  cursor: crosshair;
}
.seed-picker .seed-fallback {
  padding: 24px;
  font-size: 13px;
  color: var(--text-muted);
}
.seed-cursor {
  position: absolute;
  width: 14px;
  height: 14px;
  margin: -7px 0 0 -7px;
  border-radius: 50%;
  border: 2px solid #fff;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.4);
  pointer-events: none;
}
.seed-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 12px;
}
.seed-preview {
  width: 36px;
  height: 36px;
  min-width: 36px;
  border-radius: 8px;
  border: 1px solid var(--line);
  background: rgb(128 128 128);
}
.seed-in {
  width: 82px;
}
.lightbox-meta {
  margin-top: 8px;
  font-size: 12px;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  text-align: center;
}

/* ===== 相似色匹配（固定于底部，六格永不压缩） ===== */
.similar {
  flex: 0 0 auto;
  border-top: 1px solid rgba(0, 0, 0, 0.06);
  margin-top: 14px;
  padding-top: 14px;
  /* ⚠️ v2.0.1：同层里**必须压在主图之上**。主图区（.lightbox-panel）在 DOM 里靠前，
     一旦它再出现溢出（见 .lightbox-panel:not(.picker-panel) 的注释），
     后画的相似色才是应该"赢"的一方 —— 否则标题/色块会被深色照片吞掉。
     这两行是硬保险，不依赖上面对溢出的修复是否一直有效。 */
  position: relative;
  z-index: 1;
}
.similar-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 12px;
}
/* 相似色匹配标题：字号与灯箱编号、TrueColor™ 文案统一（15px） */
.similar-title {
  font-size: 15px;
  font-weight: 600;
  color: var(--text);
  letter-spacing: 0.2px;
}
.similar-cats {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
/* 标签下拉选择框 */
.similar-select {
  appearance: none;
  -webkit-appearance: none;
  min-width: 132px;
  max-width: 190px;
  padding: 6px 30px 6px 12px;
  border-radius: 999px;
  background-color: #fff;
  /* 右侧箭头（内联 SVG，避免额外请求） */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236b7080' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 10px center;
  background-size: 14px 14px;
  border: 1px solid var(--line);
  color: var(--text);
  font-size: 12px;
  font-weight: 500;
  line-height: 1.4;
  cursor: pointer;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.similar-select:hover {
  border-color: #c5cad6;
}
.similar-select:focus {
  outline: none;
  border-color: var(--primary);
  box-shadow: 0 0 0 3px var(--primary-soft);
}
.similar-select option {
  color: var(--text);
  background: #fff;
}
.similar-grid {
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  gap: 10px;
}
.similar-item {
  position: relative;
  display: block;
  padding: 0;
  background: none;
  border: 0;
  cursor: pointer;
}
.similar-swatch {
  display: block;
  position: relative;
  width: 100%;
  aspect-ratio: 1 / 1;
  border-radius: 10px;
  border: 1px solid rgba(0, 0, 0, 0.07);
  box-shadow: var(--shadow-sm);
  background-color: var(--bg-soft);
  background-size: cover;
  background-position: center;
  transition: transform 0.15s ease, box-shadow 0.15s ease;
}
/* TrueColor™ 模式：启动中的骨架占位 */
.similar-swatch.is-tc:not(.loaded) {
  background-image: linear-gradient(
    100deg,
    #eef1f6 30%,
    #f7f9fc 50%,
    #eef1f6 70%
  );
  background-size: 220% 100%;
  animation: swatchShimmer 1.1s linear infinite;
}
/* 改色图取不到：由 JS 写入 lab() 真实颜色，这里只停掉骨架动效。
   ⚠️⚠️ v2.1.9（用户要求）：**不要再画那圈红色描边**。
      旧规则是 `box-shadow: inset 0 0 0 1px rgba(220,38,38,.4)` —— 相似色区 6 个色块里
      只要有 2 个没导素材，视觉上就是「2 个红框 + 4 个灰框」，用户读到的是
      「这两个颜色有问题」（本次报障的截图正是这个形态），而它们其实完全正常，
      只是那两张卡还没导素材。**「没有素材」不该长得像「出错了」。**
      ⇒ 框线交回 .similar-swatch 自己那条正常规则（1px rgba(0,0,0,.07)）。
   ⚠️ 仍然**绝不能**统一涂成一个颜色（旧版 #f3d9d9）—— 6 个块全同色会被误判成
      「相似色老是匹配到同一组颜色」，而匹配列表其实完全正确。
   ⚠️ v2.1.9 起渲染期就会判掉无素材的卡（见 app.js 的 similarSwatchHtml），
      所以这个类组合**在正常路径上不再出现**；留它只为兜住「以为有素材、fetch 却失败」。 */
.similar-swatch.is-tc.failed {
  background-image: none;
}
@keyframes swatchShimmer {
  from {
    background-position: 120% 0;
  }
  to {
    background-position: -120% 0;
  }
}
.similar-item:hover .similar-swatch {
  transform: translateY(-2px);
  box-shadow: var(--shadow-md);
}
/* 色号：**置于色块下方**（v3.13.2 用户点名：名称放图下、不遮挡图片框）。
   历史：v2.0.4~v3.13.1 曾是「毛玻璃白底胶囊、绝对定位叠在色块底部」——
   该形态会遮挡色块底部约 20% 高度，且浅色块上要靠描边+投影把它"立起来"
   （视觉噪音）；现改为图下素字：轻灰小字、零装饰、不占色块面积。
   ⚠️ 本类（.similar-code）是灯箱与取色结果页**共用**的一份 CSS（见护栏 49.2）；
      「全站只许一条非 hover 的 .similar-code 规则带 color」有硬断言（16.28）。
   ⚠️ 悬停变深色的规则紧跟其后（.similar-item:hover .similar-code，
      按规则体判定时它以 `:hover` 前缀与基础规则区分）。 */
.similar-code {
  display: block;
  margin-top: 7px;
  text-align: center;
  font-size: 11.5px;
  font-weight: 500;
  color: var(--text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* 悬停：色号由灰变深（整卡可点，这是交互反馈的一半；另一半是色块上浮） */
.similar-item:hover .similar-code {
  color: var(--text);
}
.similar-hint {
  font-size: 12px;
  color: var(--text-muted);
  padding: 6px 0;
}
.similar-hint.error {
  color: #c0392b;
}

/* ===== 灯箱响应式 ===== */
/* 中等屏幕：相似色转 3 列两行 */
@media (max-width: 900px) {
  .similar-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}
/* 平板 / 窄窗（641~900px）：保持 3 列两行的结构不变，仅压扁色块比例 + 紧凑间距。
   该区间 3 列正方形色块单格可达 200~270px，两行相似区高达 470~600px，
   主色区被压到最小 120px（占比仅 18%~46%），故改 2:1 横图回收纵向空间
   （实测主色占比提升到 51%~63%，结构仍为 3 列两行） */
@media (min-width: 641px) and (max-width: 900px) {
  .similar {
    margin-top: 10px;
    padding-top: 10px;
  }
  .similar-head {
    margin-bottom: 8px;
  }
  .similar-grid {
    gap: 8px;
  }
  .similar-swatch {
    aspect-ratio: 2 / 1;
    border-radius: 8px;
  }
  .similar-code {
    margin-top: 5px;
    font-size: 10.5px;
  }
}
/* 移动端：灯箱近似全屏，单列纵向排列，主色区弹性 + 六色固定同屏 */
@media (max-width: 640px) {
  .lightbox-mask {
    padding: 6px 0 0;
    align-items: stretch;
  }
  /* v3.13.5：留缝磨砂 —— **只给「留缝那条 6px」做**（用户：无需看清下方页面）。
     ⚠️ 刻意**不**给整个 mask 加 backdrop-filter：全屏大面积模糊在手机上会掉帧
        （与 .welcome-mask 注释同一条原则；小条面积 = 零负担）。16.29 有反向断言，
        54l4 把模糊挪到全屏 mask 上必报红。
     ⚠️ -webkit- 必须与标准属性成对（iOS Safari 只认前缀版；16.29 钉住）。
     ⚠️ pointer-events:none 是**行为件**：点顶缝空白要落到 mask 上（关窗），
        条子若吃指针，「点缝关窗」会静默失效。 */
  .lightbox-mask::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    height: 6px;
    pointer-events: none;
    -webkit-backdrop-filter: blur(8px);
    backdrop-filter: blur(8px);
  }
  /* v3.13.5「抽屉形态」= 留缝 **6** + 顶缝**磨砂（6px 小条）** + 上方两角圆弧 20。
     （v3.13.4 删把手；v3.13.5 留缝 12 → 6（用户：「太浪费」）+ 磨砂做成
      `.lightbox-mask::before` 小条（用户：「无需看清下方页面」）——让位用的
      36px/34px 头部 padding 已在 v3.13.4 清零，不再回来；明细见 16.29。）
     ⚠️ 留缝 / 圆弧 / 磨砂 / 高度扣减的退化都是「看起来改了、其实没改」的静默型：
        证伪变体 54j1（留缝）/ 54j2（圆弧）/ 54l1（磨砂）/ 54l2（高度配对）/
        54l4（磨砂挪到全屏 mask）；「把手不得回来」由反转后的 54j3 守着。
     ⚠️ 本注释**必须留在 .lightbox-mask 规则之后**：16.14 / 16.29 的切段锚点是
        `@media (max-width: 640px) {` 与 `  .lightbox-mask {` 的**紧邻两行**，
        注释夹在中间会让整段判定静默打偏（v3.13.3 实测踩到，已列为反面样例）。 */
  .lightbox {
    max-width: 100%;
    height: 100vh;
    height: 100dvh;
    max-height: 100vh;
    max-height: 100dvh;
    border-radius: 0;
    animation: none;
  }
  /* 抽屉外壳只认「带 .lightbox-mask 的两扇窗」：色卡灯箱与取色窗（picker.js 的
     mask 同时带 lightbox-mask，见 16.13）。
     ⚠️ AI 生图页的历史图灯箱（ai.js，mask 只有 .modal-mask）刻意**不**套抽屉 ——
        它没有下拉语义，多一个把手反而是假可供性；它的移动端形态与旧版一字不变。 */
  .lightbox-mask .lightbox {
    max-width: 100%;
    height: calc(100vh - 6px);
    height: calc(100dvh - 6px);
    max-height: calc(100vh - 6px);
    max-height: calc(100dvh - 6px);
    border-radius: 20px 20px 0 0;
  }
  .lightbox-head {
    padding: 12px 14px;
    background: #fff;
  }
  /* v3.13.4：头部让位规则与把手伪元素**整段删除**（用户点名，见上）。
     ⚠️ 不许再给这两扇窗的头部加回任何「让位 / 抓手」——16.29 是负向断言，
        54j3 会把它塞回去验证「必报红」。 */
  .lightbox-body {
    padding: 12px 14px calc(14px + env(safe-area-inset-bottom));
  }
  /* 窄屏下开关文案保留（用户要求显示当前模式名），标题过长时截断
     字号不再单独收窄：与灯箱编号、相似色匹配统一 15px */
  .lightbox-title {
    flex: 0 1 auto;
    min-width: 0;
  }
  .lightbox-actions {
    flex: 0 0 auto;
  }
  /* ★ 个人颜色的两图（框选图 / 平均色图）在窄屏改**上下布局**（v1.4.4）：
     左右并列时每张只剩约 150px 宽，照片与平均色都被压成竖条；
     改成上下后两张都能拿到整幅宽度（框选图在上、平均色图在下）。
     两格等高平分（1fr 1fr）；无照片的老条目只有一格，别给它留出空的第二行。
     ⚠️ 不要靠给 .mine-pair 加 min-height 来「撑开」——
        高度来自 flex:1 1 auto 的弹性分配，写死值会在不同屏高下变成魔术数字。 */
  .mine-pair {
    grid-template-columns: 1fr;
    grid-template-rows: 1fr 1fr;
  }
  .mine-pair.single {
    grid-template-rows: 1fr;
  }
  /* 相似色模块收窄：缩略图改横向 3:2 + 紧凑间距，主色区占据主要篇幅 */
  .similar {
    margin-top: 10px;
    padding-top: 10px;
  }
  .similar-head {
    margin-bottom: 8px;
  }
  .similar-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 6px;
  }
  .similar-swatch {
    aspect-ratio: 3 / 2;
    border-radius: 8px;
  }
  .similar-code {
    margin-top: 5px;
    font-size: 10.5px;
  }
}
/* 短屏（iPhone SE 等 ≤700px 高）：进一步压缩相似色模块，主色区优先 */
@media (max-width: 640px) and (max-height: 700px) {
  .lightbox-head {
    padding: 10px 12px;
  }
  /* v3.13.4：短屏同样无把手（原 34px 让位档随把手一起删除，见上）。
     通用那条 .lightbox-head 保留给 AI 历史灯箱（不套抽屉，见上）。 */
  .lightbox-body {
    padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
  }
  .similar {
    margin-top: 8px;
    padding-top: 8px;
  }
  .similar-head {
    margin-bottom: 6px;
  }
  .similar-grid {
    gap: 5px;
  }
  .similar-swatch {
    aspect-ratio: 2 / 1;
  }
  .similar-code {
    margin-top: 4px;
    font-size: 10px;
  }
}

.toast {
  position: fixed;
  top: 80px;
  left: 50%;
  transform: translateX(-50%);
  background: rgba(20, 23, 32, 0.92);
  color: #fff;
  padding: 9px 16px;
  border-radius: 999px;
  font-size: 13px;
  z-index: 100;
  box-shadow: var(--shadow-lg);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s ease, transform 0.2s ease;
}
.toast.show {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}
/* v3.9.0：错误样式的 toast（UI.toast(msg,'bad')）—— 深色胶囊换成红底 */
.toast.bad {
  background: rgba(224, 68, 91, 0.95);
}

/* =========================================================
   登录 / 后台
   ========================================================= */

.auth-bg {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background: linear-gradient(135deg, #eef1f8 0%, #f7f8fc 100%);
  padding: 24px;
}
.auth-card {
  background: #fff;
  border-radius: var(--radius-lg);
  padding: 32px;
  width: 100%;
  max-width: 380px;
  box-shadow: var(--shadow-lg);
}
.auth-card h2 {
  margin: 0 0 6px;
  font-size: 22px;
}
.auth-card .sub {
  color: var(--text-muted);
  margin-bottom: 22px;
  font-size: 13px;
}
.auth-card .foot {
  margin-top: 14px;
  text-align: center;
  font-size: 13px;
  color: var(--text-muted);
}
.auth-card .foot a {
  color: var(--primary);
  font-weight: 500;
}

.admin-shell {
  display: grid;
  grid-template-rows: var(--top-h) 1fr;
  /* v1.3.6 · 与 .app 同源，同样不能锁 100vh。
     后台没有底部 dock，但工具栏那一截仍然会把 .admin-body 的 60px 下内边距
     吃掉 → 最后一个面板（如「保存」按钮）贴到屏幕底边甚至被工具栏盖住。
     回退对写法与 .app / 移动端灯箱一致，理由详见 .app 处注释。 */
  height: 100vh;
  height: 100dvh;
}
.admin-shell > .topbar {
  border-bottom: 1px solid var(--line);
}
.admin-body {
  overflow: auto;
  /* v1.3.5 · 后台与前台同源：.admin-shell 也是 height:100vh + 内部滚动容器，
     同样会产生 56px 根溢出。除 html.app-root 的 overflow:hidden 外，
     这里再阻断一次滚动链（理由见 .content 处注释）。 */
  overscroll-behavior-y: contain;
  /* v3.1.1 · 方案A 后台骨架：左侧导航 + 右侧内容。
     ⚠️ 本容器仍是**唯一**的滚动口 —— admin.js 的 goToPage() 会
        `scrollTo('.admin-body')` 把列表拉回顶部，别把 overflow 挪到内层。
        所以 padding 归 0，内衬由 .admin-side / .admin-main 各自带：
        否则侧边栏会被 padding 顶出一道缝、贴不到容器边缘。 */
  display: grid;
  grid-template-columns: 236px minmax(0, 1fr);
  padding: 0;
}

/* 侧边栏：sticky 贴住滚动容器顶部（.admin-body 才是滚动口）。
   ⚠️ grid item 默认 align-self:stretch 会被拉到内容总高 → sticky 失效，
      必须显式 align-self:start + 固定高度。 */
.admin-side {
  position: sticky;
  top: 0;
  align-self: start;
  height: calc(100vh - var(--top-h));
  height: calc(100dvh - var(--top-h));
  overflow-y: auto;
  padding: 18px 12px 22px;
  background: var(--bg-elev);
  border-right: 1px solid var(--line);
}
.admin-main {
  min-width: 0;
  padding: 22px 28px 60px;
}

/* =========================================================
   后台 · 侧边栏导航（v3.1.1 · 方案A）
   ---------------------------------------------------------
   ⚠️⚠️ 与 JS 的契约 —— 动这里之前先读，改坏的症状是
   「某个导航项点了没反应」且**控制台不报任何错**：
     · `.tabs` 必须存在且**只有一个** —— 4 个 admin-*.js 的 injectTab()
       都往它 appendChild 自己的按钮（`if (!tabs || $('#tab-x')) return;`）；
     · 导航项必须仍是 `.tabs` 的**后代 `<button>`** —— admin.js 的
       bindTabs() 用 `$$('.tabs button')` 统一管 active 态；其余 4 个面板
       各靠 document **捕获阶段**的 `closest('.tabs button')` 切自己那块
       （目标面板写死成各自的 id，见 admin-cache.js 那段长注释）。
   ⇒ 所以本次把「顶部横排 tab」改成「左侧竖排导航」**只动 CSS**：
     结构上只是给 .tabs 换了个父容器，上述选择器一个字没变，
     **没有任何 JS 需要改**。
   ========================================================= */
.tabs {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin: 0;
}
.tabs button {
  /* 视觉顺序按方案A分组排（管理 / 呈现 / 运维）。
     ⚠️ 只用 `order` 改**视觉**顺序，绝不改 DOM 顺序 —— DOM 顺序由脚本
        加载顺序决定，改它就得动 4 个 admin-*.js 的 injectTab()。 */
  order: 99;
  display: flex;
  align-items: center;
  gap: 10px;
  height: 40px;
  padding: 0 12px;
  border-radius: 10px;
  font-size: 13.5px;
  font-weight: 500;
  color: var(--text-muted);
  text-align: left;
  white-space: nowrap;
  transition: background 0.15s ease, color 0.15s ease;
}
/* 导航图标：mask + currentColor 上色 ⇒ hover / active 换字色时图标自动跟随。
   ⚠️ 之所以用伪元素而不是往按钮里塞 <svg>：那 4 个按钮是 admin-*.js 用
      createElement + textContent 建的，要塞图标就得改 4 个 JS 文件。 */
.tabs button::before {
  content: '';
  flex: 0 0 17px;
  width: 17px;
  height: 17px;
  background: currentColor;
  -webkit-mask: var(--tab-ico) center / contain no-repeat;
  mask: var(--tab-ico) center / contain no-repeat;
}
/* --- 各导航项：order = 视觉位次；margin-top = 组间留白 --- */
.tabs button[data-tab='cards'] {
  order: 1;
  --tab-ico: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='3' width='7.5' height='7.5' rx='1.6'/%3E%3Crect x='13.5' y='3' width='7.5' height='7.5' rx='1.6'/%3E%3Crect x='13.5' y='13.5' width='7.5' height='7.5' rx='1.6'/%3E%3Crect x='3' y='13.5' width='7.5' height='7.5' rx='1.6'/%3E%3C/svg%3E");
}
.tabs button[data-tab='categories'] {
  order: 2;
  --tab-ico: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20.6 13.4l-7.2 7.2a2 2 0 0 1-2.8 0L2 12V2h10l8.6 8.6a2 2 0 0 1 0 2.8z'/%3E%3Ccircle cx='7' cy='7' r='1.2'/%3E%3C/svg%3E");
}
.tabs button[data-tab='ai'] {
  order: 3;
  --tab-ico: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10.4 3.6 12.1 8.3 16.8 10 12.1 11.7 10.4 16.4 8.7 11.7 4 10 8.7 8.3Z'/%3E%3Cpath d='M17.6 13.9 18.4 16.1 20.6 16.9 18.4 17.7 17.6 19.9 16.8 17.7 14.6 16.9 16.8 16.1Z'/%3E%3C/svg%3E");
}
/* ⚠️ v3.4.0：新加导航项时**必须同时给出 order 与 --tab-ico**。
   `order` 的默认值是 **0**，比现有所有项（1..8）都小 ⇒ 漏写 order 的后果
   不是「掉到列表末尾」，而是**跑到第一位**（最上面），而且看起来不像 bug。
   --tab-ico 是内联 SVG（stroke=black，靠 CSS 的 filter 反色），
   漏写会渲染成一个实心黑方块（本站曾经的故障形态）。
   ⚠️ v3.8.0：新增「邮件发信」项 ⇒ order 的取值范围扩到 **1..8**。 */
.tabs button[data-tab='recolor'] {
  order: 4;
  margin-top: 18px;
  --tab-ico: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 2.7l5.7 5.7a8 8 0 1 1-11.4 0z'/%3E%3C/svg%3E");
}
.tabs button[data-tab='site'] {
  order: 5;
  --tab-ico: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='3.2'/%3E%3Cpath d='M12 2.5v3M12 18.5v3M4.6 4.6l2.1 2.1M17.3 17.3l2.1 2.1M2.5 12h3M18.5 12h3M4.6 19.4l2.1-2.1M17.3 6.7l2.1-2.1'/%3E%3C/svg%3E");
}
.tabs button[data-tab='cache'] {
  order: 6;
  margin-top: 18px;
  --tab-ico: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'%3E%3Cellipse cx='12' cy='5.5' rx='8.5' ry='3.2'/%3E%3Cpath d='M3.5 5.5v13c0 1.8 3.8 3.2 8.5 3.2s8.5-1.4 8.5-3.2v-13'/%3E%3Cpath d='M3.5 12c0 1.8 3.8 3.2 8.5 3.2s8.5-1.4 8.5-3.2'/%3E%3C/svg%3E");
}
.tabs button[data-tab='update'] {
  order: 7;
  --tab-ico: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M21 12a9 9 0 0 1-15.5 6.2M3 12a9 9 0 0 1 15.5-6.2'/%3E%3Cpath d='M21 3v6h-6M3 21v-6h6'/%3E%3C/svg%3E");
}
/* v3.8.0：「邮件发信」——新的一组（与上面「系统」组之间留白） */
.tabs button[data-tab='mail'] {
  order: 8;
  margin-top: 18px;
  --tab-ico: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='2.5' y='4.5' width='19' height='15' rx='2.4'/%3E%3Cpath d='M3.2 6.2 12 12.6l8.8-6.4'/%3E%3C/svg%3E");
}

/* v3.9.0：「用户管理」——与「邮件发信」同组（紧接其后） */
.tabs button[data-tab='users'] {
  order: 9;
  --tab-ico: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='9' cy='8' r='3.4'/%3E%3Cpath d='M2.8 19.4a6.2 6.2 0 0 1 12.4 0'/%3E%3Cpath d='M16 5.4a3.4 3.4 0 0 1 0 5.2M17.8 13.6a6.2 6.2 0 0 1 3.4 5.8'/%3E%3C/svg%3E");
}
.tabs button:hover {
  background: var(--bg-soft);
  color: var(--text);
}
.tabs button.active {
  background: var(--primary);
  color: #fff;
  font-weight: 600;
}
.tabs button.active:hover {
  background: var(--primary-hover);
  color: #fff;
}

/* 面板标题行（原来的内联 style 收敛到这里，见 admin.html 的 .panel-head） */
.panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 12px;
}
.panel-head h3 {
  margin: 0;
}

/* 窄屏（≤900px）：侧边栏塌成顶部横向导航 —— 后台的主要使用场景是桌面，
   但手机上同样要能用（原横排 tab 在窄屏是换行/横滚，这里保持可横滚）。 */
@media (max-width: 900px) {
  .admin-body {
    grid-template-columns: minmax(0, 1fr);
  }
  .admin-side {
    position: static;
    height: auto;
    overflow-y: visible;
    padding: 10px 12px;
    border-right: none;
    border-bottom: 1px solid var(--line);
  }
  .tabs {
    flex-direction: row;
    flex-wrap: wrap;
  }
  .tabs button {
    /* ⚠️ 不在这里改 order：`[data-tab]` 的选择器特异性更高，写了也不生效。
       横排时仍按方案A的 1..6 位次排（与竖排一致，只是换了个方向）。 */
    height: 34px;
    padding: 0 12px;
    font-size: 13px;
  }
  .tabs button[data-tab='recolor'],
  .tabs button[data-tab='cache'],
  .tabs button[data-tab='mail'],
  .tabs button[data-tab='users'] {
    margin-top: 0;
  }
  .admin-main {
    padding: 16px 14px 48px;
  }
}

.section {
  background: var(--bg-elev);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: 18px 20px;
  margin-bottom: 18px;
}
.section h3 {
  margin: 0 0 14px;
  font-size: 15px;
  font-weight: 600;
}

.table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
}
.table th,
.table td {
  text-align: left;
  padding: 10px 12px;
  border-bottom: 1px solid var(--line);
}
.table th {
  font-weight: 600;
  color: var(--text-muted);
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.4px;
  background: var(--bg-soft);
}
.table tr:last-child td {
  border-bottom: none;
}
.table .row-actions {
  display: flex;
  gap: 6px;
  justify-content: flex-end;
}

/* 后台标签：拖动排序 */
.drag-cell {
  width: 52px;
  padding-right: 0 !important;
}
.drag-handle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border-radius: 7px;
  color: var(--text-faint);
  cursor: grab;
  user-select: none;
  transition: background 0.15s ease, color 0.15s ease;
}
.drag-handle:hover {
  background: var(--bg-soft);
  color: var(--text-muted);
}
.drag-handle:active {
  cursor: grabbing;
}
.drag-handle svg {
  width: 15px;
  height: 15px;
}
.table tr.dragging {
  opacity: 0.45;
}
.table tr.drag-over > td {
  box-shadow: inset 0 2px 0 0 var(--primary);
}
.color-dot {
  display: inline-block;
  width: 22px;
  height: 22px;
  border-radius: 6px;
  vertical-align: middle;
  border: 1px solid rgba(0, 0, 0, 0.06);
  margin-right: 8px;
}
.image-thumb {
  width: 36px;
  height: 36px;
  border-radius: 6px;
  background-size: cover;
  background-position: center;
  border: 1px solid var(--line);
}

.lab-picker {
  display: flex;
  align-items: center;
  gap: 8px;
}
.lab-preview {
  width: 36px;
  height: 36px;
  min-width: 36px;
  border-radius: 8px;
  border: 1px solid var(--line);
  background: lab(50 0 0);
}
.lab-in {
  width: 88px;
}

.preview-card {
  width: 90px;
  height: 90px;
  border-radius: 12px;
  background: var(--bg-soft);
  background-size: cover;
  background-position: center;
  border: 1px solid var(--line);
}

/* =========================================================
   取色向导（v1.4.1 · 方案 C 三步向导）
   ---------------------------------------------------------
   形态：复用灯箱外壳与**同一套响应式逻辑**（.modal-mask + .lightbox-mask + .lightbox）。
     1 选图  → .picker-drop（拖放 / 选择文件）
                ⚠️ v1.5.6 起「最近取色」模块已移除（用户要求），本步只剩拖放区
     2 框选  → .picker-stage（拖出矩形 → 实时平均色 + 放大镜）+ 动作行里的小正方预览框
     3 结果  → .mine-pair（框选图 + 取色图）+ 相似色匹配模组 + 收藏

   ⚠️ 结果页的主图**刻意不自己写一套尺寸**：`.mine-pair / .mine-half / .mine-shot`
      全部复用文件上半部那一组规则。那组规则原本属于「个人颜色」灯箱，
      而**那盏灯箱已在 v1.5.6 删除**（收藏里的取色颜色改用常规色卡灯箱）——
      现在它们**只服务取色结果页**，别因为"灯箱没了"就顺手删掉。
      另写一套的下场是两处观感慢慢分叉，而分叉不会有任何报错。

   ⚠️ 四条不能改的约束（改了功能就坏）：
   ① `.picker-stage` 必须 `touch-action: none`。否则触摸设备上浏览器会把
      拖动判成「滚动页面 / 双指缩放」，pointermove 被中途 cancel →
      框选拖到一半就断（PC 上完全看不出来）。
   ② `.picker-sel` 必须 `pointer-events: none`，只有 `.picker-handle` 是
      `auto`。选框会盖住它自己那块图像区域，若它能吃事件，用户就再也没法
      在已选区域内重新开一个新选区（详见 picker.js 文件头 ⑥）。
   ③ `.picker-sel` 的「选区外压暗」用 `box-shadow: 0 0 0 9999px`，**依赖**
      `.picker-stage { overflow: hidden }` 把它裁在舞台内。两者是一对，
      去 overflow 会让整页变暗。
   ④ `.picker-mask` 必须同时带 `.lightbox-mask` 类（见 picker.js open()）：
      小屏「满屏平铺」的规则挂在 `.lightbox-mask` / `.lightbox` 上，
      只写 `.picker-mask` 会被 16px 内边距与圆角顶回来。
   ========================================================= */

/* 遮罩层级：高于灯箱(90)与 dock(45)。
   取色与灯箱不会同时存在（picker.js 点相似色时先 close 再开灯箱），
   这里只是防御性取值，避免未来某处同时打开时层级错乱。 */
.picker-mask {
  z-index: 95;
}
/* 向导比普通灯箱窄：内容更聚焦，桌面上不至于把选框拉得过宽。
   position:relative → 命名弹窗（absolute inset:0）的参照，并被 shell 的
   overflow:hidden 裁住。 */
.picker {
  max-width: 620px;
  position: relative;
}
/* 大屏加宽取色窗（v1.5.18）。
   ⚠️ 必须写在这条 base **之后**：同名同特异性（0,1,0），媒体查询不加特异性 →
      写在前面会被上面这行 620px 直接盖掉，而且**不报任何错**。
      （06-s16-wizard 里那条断言钉的就是这个顺序纪律。）
   为什么必须加宽：620px 在 2560 宽的逻辑视口里只占 24%；而 v1.5.17 起相似色由
   6 列均分 → 色块只剩 (620−32−50)/6 ≈ 90px（用户："太窄、显示内容不协调"）。
   加宽到 1000 后色块回到 ~153px，且仍**窄于灯箱**（1180），保留"向导更聚焦"的取向。 */
@media (min-width: 1600px) {
  .picker {
    max-width: 880px;
  }
}
@media (min-width: 2100px) {
  .picker {
    max-width: 1000px;
  }
}

/* v3.13.4：步骤条（原 wrap / steps / step / step-n / step-line 一整组，
   连同窄屏压缩档）按用户要求**整体删除** —— 太占地方、无功能。
   ⚠️ 不许以任何形态恢复：16.31 是负向断言，54k1 把节点塞回去验证「必报红」。 */

/* ---- 第 1 步：选图 ----
   ⚠️ `flex: 1 1 auto` 是关键：最近取色那一块压缩后让出来的纵向空间**自动**归它
      （不需要给两边写死高度）。min-height 只是「空间再紧也不许比这更矮」的下限，
      v1.5.2 由 240 → 300（用户要求放大拖图区）。 */
.picker-drop {
  flex: 1 1 auto;
  min-height: 300px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 26px 20px;
  border-radius: var(--radius-lg);
  border: 2px dashed var(--line);
  background: var(--bg-soft);
  text-align: center;
  transition: border-color 0.15s ease, background 0.15s ease;
}
.picker-drop.over {
  border-color: var(--primary);
  background: var(--primary-soft);
}
.picker-drop-ic {
  line-height: 0;
  color: var(--primary);
}
.picker-drop-ic svg {
  width: 42px;
  height: 42px;
}
.picker-drop-title {
  font-size: 16px;
  font-weight: 600;
}
.picker-drop-or {
  font-size: 12px;
  color: var(--text-faint);
}
.picker-pick {
  margin-top: 2px;
}
.picker-drop-note {
  font-size: 12px;
  color: var(--text-muted);
}
/* ---- 第 2 步：框选舞台 ---- */
.picker-stage {
  position: relative;
  flex: 1 1 auto;
  min-height: 210px;
  border-radius: 12px;
  overflow: hidden; /* ⚠️ 与选区压暗阴影成对，见文件头 ③ */
  touch-action: none; /* ⚠️ 见文件头 ①：不加则触摸端拖不动框选 */
  cursor: crosshair;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
  /* 棋盘格底：透明 PNG 与浅色图也能看清边界 */
  background-color: #f0f2f7;
  background-image: linear-gradient(45deg, #e4e8ef 25%, transparent 25%),
    linear-gradient(-45deg, #e4e8ef 25%, transparent 25%),
    linear-gradient(45deg, transparent 75%, #e4e8ef 75%),
    linear-gradient(-45deg, transparent 75%, #e4e8ef 75%);
  background-size: 16px 16px;
  background-position: 0 0, 0 8px, 8px -8px, -8px 0;
}
.picker-img {
  position: absolute;
  left: 0;
  top: 0;
  display: block;
  max-width: none;
  pointer-events: none;
  user-select: none;
  -webkit-user-drag: none;
}
.picker-sel {
  position: absolute;
  z-index: 2;
  border: 1.5px solid var(--primary);
  border-radius: 4px;
  /* 第一层：白色细描边（把选框与浅色图像分开）；
     第二层：超大 spread 把选区**之外**整片压暗（画在选区边框之外，故框内不暗） */
  box-shadow: 0 0 0 1.5px rgba(255, 255, 255, 0.95),
    0 0 0 9999px rgba(20, 23, 32, 0.38);
  pointer-events: none; /* ⚠️ 见文件头 ②：只有把手可交互 */
}
.picker-sel[hidden] {
  display: none;
}
.picker-handle {
  position: absolute;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: #fff;
  border: 2px solid var(--primary);
  box-shadow: 0 1px 4px rgba(20, 23, 32, 0.3);
  pointer-events: auto; /* ⚠️ 全舞台唯一能吃 pointerdown 的选框部件 */
  touch-action: none;
}
.picker-handle[data-h="nw"] {
  left: -9px;
  top: -9px;
  cursor: nwse-resize;
}
.picker-handle[data-h="ne"] {
  right: -9px;
  top: -9px;
  cursor: nesw-resize;
}
.picker-handle[data-h="sw"] {
  left: -9px;
  bottom: -9px;
  cursor: nesw-resize;
}
.picker-handle[data-h="se"] {
  right: -9px;
  bottom: -9px;
  cursor: nwse-resize;
}
/* 触摸设备：把手放大到手指够得着（18px 是鼠标尺度，不是手指尺度） */
@media (pointer: coarse) {
  .picker-handle {
    width: 24px;
    height: 24px;
  }
  .picker-handle[data-h="nw"],
  .picker-handle[data-h="ne"] {
    top: -12px;
  }
  .picker-handle[data-h="sw"],
  .picker-handle[data-h="se"] {
    bottom: -12px;
  }
  .picker-handle[data-h="nw"],
  .picker-handle[data-h="sw"] {
    left: -12px;
  }
  .picker-handle[data-h="ne"],
  .picker-handle[data-h="se"] {
    right: -12px;
  }
}
/* 尺寸标签放在**选框内**左上角（而不是浮在框外）：
   浮在框外时，靠边的选区会把标签顶到舞台外被 overflow 裁掉。 */
.picker-sel-size {
  position: absolute;
  left: 4px;
  top: 4px;
  padding: 2px 7px;
  border-radius: 999px;
  background: rgba(20, 23, 32, 0.72);
  color: #fff;
  font-size: 10px;
  font-weight: 600;
  line-height: 1.5;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  pointer-events: none;
}
/* 放大镜：不插值放大（imageSmoothingEnabled=false + pixelated），能直接看到像素格 */
.picker-mag {
  position: absolute;
  z-index: 3;
  width: 96px;
  height: 96px;
  border-radius: 12px;
  border: 2px solid #fff;
  box-shadow: 0 6px 20px rgba(20, 23, 32, 0.28);
  image-rendering: pixelated;
  pointer-events: none;
}
.picker-mag[hidden] {
  display: none;
}
.picker-hint {
  position: absolute;
  z-index: 2;
  left: 50%;
  bottom: 10px;
  transform: translateX(-50%);
  padding: 5px 12px;
  border-radius: 999px;
  background: rgba(20, 23, 32, 0.6);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  color: #fff;
  font-size: 12px;
  white-space: nowrap;
  pointer-events: none;
  transition: opacity 0.2s ease;
}
/* 已经框过一次 → 提示退场（由 JS 在 paintSelection 里切换 .has-sel） */
.picker-stage.has-sel .picker-hint {
  opacity: 0;
}

/* ---- 第 2 步：小正方形取色预览框（紧贴「全图取色」右侧）----
   v1.4.1 用它取代了原先独立的读数区（色块 + L/a/b + hex·尺寸）。
   没框选时留棋盘格空槽，让人看得出「这里应该是颜色」而不是漏渲染。
   平均色的数值挂在 data-lab 上（见 picker.js 文件头 ⑦）——UI 不显示，但有据可查。 */
.picker-sw {
  flex: 0 0 auto;
  width: 34px;
  height: 34px;
  border-radius: 9px;
  border: 1px solid rgba(0, 0, 0, 0.12);
  box-shadow: var(--shadow-sm);
  background-color: #f0f2f7;
  background-image: linear-gradient(45deg, #e4e8ef 25%, transparent 25%),
    linear-gradient(-45deg, #e4e8ef 25%, transparent 25%),
    linear-gradient(45deg, transparent 75%, #e4e8ef 75%),
    linear-gradient(-45deg, transparent 75%, #e4e8ef 75%);
  background-size: 10px 10px;
  background-position: 0 0, 0 5px, 5px -5px, -5px 0;
}
/* 有平均色 → 棋盘格收起（内联 background 已覆盖），只留一圈描边 */
.picker-sw.on {
  background-image: none;
}

/* ---- 动作行（上一步 / 下一步 / 全部图片）---- */
.picker-acts {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 12px;
}
.picker-acts-gap {
  flex: 1 1 auto;
}

/* ---- 第 3 步：结果 ----
   v1.4.1 按要求把「L/a/b 三行 + 采样小字 + 平均色 hex」全部删掉；
   v1.5.2 起主图换成与「个人颜色」灯箱同款的 .mine-pair（框选图 + 取色图），
   于是这里只剩一条约束：主图容器的最小高度维持旧结果页的 132px
   （不缩水，也不会把下面的相似色区顶出首屏）。
   ⚠️ 网格与格子（.mine-pair / .mine-half / .mine-shot）刻意
      复用灯箱那一组规则，不要在这里重写一套 —— 两套尺寸早晚分叉且不报错。 */
.picker-panel {
  min-height: 132px;
}

/* 结果页的两个功能键（v1.5.16，见 picker.js 文件头 ⑱）：
   二者都复用灯箱的 .lightbox-fav（40×40 圆钮 / 距右下 12px / 白底细描边 / z-index 2），
   定位祖先是 .mine-half —— 收藏挂取色图格、返回框选挂框选图格。
   ⚠️ 只有「没有框选图可依附」这一条极端路径（编码器不可用 → .mine-pair.single）才用
      at-left：把返回框选改挂到取色图格的**左下角**，否则第 3 步没有回到框选的路。 */
.picker-edit.at-left {
  right: auto;
  left: 12px;
}

/* 头部 TrueColor™ 开关的槽位（v1.5.2）：
   ⚠️ 必须有 display:flex —— 槽位是 .lightbox-actions（flex）里的一个 span，
      再往里套一层后 .tc-switch（inline-flex）会退化成匿名行盒，与关闭键的
      垂直居中差几个像素；flex 化后与灯箱里「开关直接当 flex item」完全同构。
   ⚠️ 槽位为空时宽度为 0（只吃掉 .lightbox-actions 的一个 gap），
      关闭键右侧位置与第 1/2 步完全一致，不会左右跳。 */
.picker-tc-slot {
  display: flex;
  align-items: center;
}

/* 取色窗里的相似色。
   ⚠️ v1.5.17（方案 C）：这里**刻意不再改网格列数** —— 取色窗与改色灯箱共用
      `.similar-grid` 那一套分档（>900px 六列一行 / ≤900px 三列两行）。
      曾经有过两条私有覆盖（<520 三列 + ≥520 六列），后果是窄屏档色块只有
      109×55、色号胶囊还压掉 23px（可见色彩面积仅 3462px²），而灯箱在同一个
      宽度下是 227×113。同构之后窄屏档 ×5.9，两个界面也不会再分叉。
      ⚠️ 若将来确实要给某一档单独调列数：必须写在 `.picker-similar` 作用域内、
        并**同时**改灯箱那份 —— 否则两边又会不一致（这正是当初那个 bug 的形状）。
        但**优先选择是不加覆盖**：分档逻辑只有一份，就永远不会分叉。 */
.picker-similar {
  flex: 0 0 auto;
}

/* 编号输入：与全站 .input 同款（这里只补「自己占满一行」） */
.picker-code {
  flex: 1 1 auto;
  min-width: 0;
  width: 100%;
  padding: 9px 12px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--line);
  background: #fff;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.picker-code:focus {
  outline: none;
  border-color: var(--primary);
  box-shadow: 0 0 0 3px rgba(84, 84, 84, 0.15);
}
/* 状态行：v3.13.4 起「安静优先」——空编号 / 编号可用都不写文案（用户点名：
   空态那行太占地方）；只有重名（bad）/ 同色（warn）才出现。
   ⚠️ :empty 收起，连占位高度都不留（否则空态仍占一行）—— 16.32 守，54k2/k3 证伪。 */
.picker-status {
  min-height: 18px;
  margin-top: 8px;
  font-size: 12px;
  line-height: 1.5;
  color: var(--text-muted);
}
.picker-status:empty {
  display: none;
}
.picker-status.good {
  color: var(--success);
}
/* --warn(#f3a93b) 在白色卡面上偏浅、对比不足 → 取深一档琥珀色 */
.picker-status.warn {
  color: #b8791d;
}
.picker-status.bad {
  color: var(--danger);
}

/* ---- 命名弹窗（收藏的唯一入口，见 picker.js 文件头 ⑩）----
   挂在 .picker 外壳内：absolute inset:0 → 被外壳的 overflow:hidden 裁住，
   不会溢出到遮罩外面，也不会把外壳撑高。 */
.picker-name {
  position: absolute;
  inset: 0;
  z-index: 20;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 18px;
  background: rgba(20, 23, 32, 0.38);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  animation: fade 0.12s ease;
}
.picker-name-card {
  width: 100%;
  max-width: 340px;
  padding: 16px;
  border-radius: var(--radius-lg);
  background: #fff;
  box-shadow: var(--shadow-lg);
  animation: pop 0.16s ease;
}
.picker-name-title {
  margin-bottom: 10px;
  font-size: 15px;
  font-weight: 600;
}

/* ---- 小屏：与改色灯箱完全同款的「满屏平铺」----
   ⚠️ 这段必须写在 `.picker { max-width: 620px }` **之后**：
     两者特异性相同（都是单类），后面的赢；写在前面会被 620px 顶回来。 */
@media (max-width: 640px) {
  .picker {
    max-width: 100%;
  }
  .picker-name {
    padding: 12px;
  }
}

/* ===== 首屏防闪：语言未就绪时先藏住待翻译元素（v1.5.9）=====
   i18n.js 在 <head> 里判定访客语言；若判定结果与 HTML 里写死的占位语言（中文）
   不一致，就给 <html> 打 data-i18n-pending —— 把这一批「马上要被翻译替换」的元素
   先藏起来，等翻译就位（I18N.init 完成，或 PENDING_TIMEOUT 兜底）再放开。
   不做这一步的话，英文访客会先看到中文、再跳成英文（首屏闪烁）。

   ⚠️ 用 visibility 而不是 display：
      · visibility:hidden **保留布局盒子** → 放开时只重绘、不触发重排；
      · display:none 会让整页重排，代价高一个数量级。
   ⚠️ 中文访客不会被隐藏：i18n.js 只在「判定语言 ≠ 占位语言」时才打标记。
   ⚠️ 标记一旦被移除，下面两条规则**整条不匹配**（html 上没有那个属性）→
      稳态匹配成本为 0。
   ⚠️ 刻意**不含** [data-i18n-placeholder]（全站仅 2 处）：placeholder 是 input 的
      属性，CSS 没法只藏它；藏整个 input 会让输入框直接消失，比"placeholder 闪一下"
      严重得多。同理不含 [data-i18n-aria]（全站 0 处，且它只改无障碍名、视觉上不可见）。 */
html[data-i18n-pending] [data-i18n],
html[data-i18n-pending] [data-i18n-html] {
  visibility: hidden;
}

/* ===== 首次访问欢迎层（v1.5.10）=====
   ⚠️ 骨架**照抄 .modal-mask**（position:fixed / inset:0 / rgba(20,23,32,0.4) 三件套），
      只把层级单独定为 **96**：
        · 高于 .picker-mask(95) → 首次访问时它是最顶层的阻塞层；
        · 低于 .toast(100)      → 提示消息仍能浮在最上面。
   ⚠️ 遮罩**刻意不加 backdrop-filter**：全屏大面积模糊在手机上会掉帧，与本次
      「保障使用流畅度」的目标相悖（.dock 的毛玻璃只作用在一小块面积上）。
      纯色半透明已足够表达层级。
   ⚠️ **只有一份 DOM**：宽屏是居中悬浮窗、窄屏是底部抽屉，差异全在下面那个
      @media (max-width:640px) 里（沿用本文件「桌面为基准、移动端覆盖」的写法）。
      不复制结构 = 读屏不会读到两份、i18n 挂载点不会重复。 */
.welcome-mask {
  position: fixed;
  inset: 0;
  z-index: 96;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
  background: rgba(20, 23, 32, 0.4);
  opacity: 1;
  transition: opacity 0.22s ease;
}
/* 关闭时的淡出（JS 在 transitionend 或 260ms 兜底后移除节点） */
.welcome-mask.is-out {
  opacity: 0;
}

/* 宽屏：竖屏悬浮窗（290 宽，高由内容决定 ≈344） */
.welcome-card {
  box-sizing: border-box;
  /* v1.5.12：290 → 368px、内距 22 → 28px（用户：宽屏悬浮窗太小、不够大气）。
     ⚠️ 仍是**竖屏**比例（宽 368 / 高约 420），只是整体放大，没有被拉成横版。
     368 是设计观感值：1280 屏上占 29%、1920 上占 19%，再大就有"弹窗感"。
     改小之前先读护栏第 28 章 —— 它不是随手定的数。 */
  width: 368px;
  max-width: 100%;
  padding: 28px;
  /* 圆角按比例放大（16 → 20）：大卡片配原来那个半径会显得生硬。
     ⚠️ 不直接改 --radius-lg —— 它是全站共用令牌，动它会波及别的组件。 */
  border-radius: 20px;
  background: var(--bg-elev);
  border: 0.5px solid rgba(20, 23, 32, 0.08);
  /* 对话框容器本身不显示焦点框（它是 tabindex="-1" 的容器，不是可点元素）；
     ⚠️ 只对容器取消，绝不能对 button 取消 —— 键盘用户要靠它看焦点 */
  outline: none;
}
/* 标题：**Apple 大字排版**——行断点写在语言包里（`<br>`），字号靠这条放大。
   ⚠️ 英文另给一档（`html[lang="en"]`）：同一句话英文比中文长 ~1.8 倍，
      用同一个字号会在 246px 的行宽里折成 3~4 行。这不是"英文小一号"，
      而是两种书写系统在**同等视觉重量**下本来就需要不同 px（CJK 占满 em 框）。
      `html[lang]` 由 i18n.js 在 init 时写入，所以切语言后会立刻生效。 */
.welcome-title {
  margin: 0;
  font-size: 32px;
  line-height: 1.25;
  font-weight: 500;
  /* 字号越大越要收紧字距，否则大标题会显得松散（Apple 大字排版的固定手法） */
  letter-spacing: -0.3px;
  color: var(--text);
}
html[lang='en'] .welcome-title {
  font-size: 25px;
}
.welcome-list {
  display: flex;
  flex-direction: column;
  gap: 22px;
  margin: 24px 0 0;
  padding: 0;
  list-style: none;
}
.welcome-item {
  display: flex;
  align-items: center;
  gap: 12px;
}
.welcome-ic {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border-radius: 11px;
  background: var(--primary-soft);
  color: var(--primary);
}
.welcome-ic svg {
  display: block;
  width: 21px;
  height: 21px;
}
/* 叠层色卡图标的「前卡」填成底色 → 盖住后卡左下角，才形成"叠"的观感。
   ⚠️ 不能写在 SVG 的 presentation 属性里（那儿写 var() 不生效）。 */
.welcome-ic .ic-front {
  fill: var(--primary-soft);
}
.welcome-txt {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0;
}
.welcome-item-title {
  font-size: 15px;
  line-height: 1.4;
  font-weight: 500;
  color: var(--text);
}
.welcome-item-sub {
  /* ⚠️ 只到 12px，不再往上：368px 卡片的文字区是 266px，英文最长那条
     （`High-grade fabrics, every category`）在 13px 时约 245px，再放大就要折行。 */
  font-size: 12px;
  line-height: 1.5;
  color: var(--text-muted);
}
.welcome-start {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 52px;
  margin-top: 28px;
  border: 0;
  border-radius: 15px;
  background: var(--primary);
  color: #fff;
  font-family: inherit;
  font-size: 15px;
  font-weight: 500;
  cursor: pointer;
  transition: background-color 0.18s ease;
}
.welcome-start:hover {
  background: var(--primary-hover);
}

@media (max-width: 640px) {
  /* 窄屏：贴底抽屉。遮罩改成「从底边往上排」，于是卡片自动满宽靠底。 */
  .welcome-mask {
    align-items: flex-end;
    padding: 0;
  }
  .welcome-card {
    width: 100%;
    max-width: none;
    padding: 22px 18px calc(22px + env(safe-area-inset-bottom, 0px));
    border: 0;
    border-top: 0.5px solid rgba(20, 23, 32, 0.08);
    border-radius: 22px 22px 0 0;
  }
  .welcome-title {
    font-size: var(--m-welcome-title);
    line-height: 1.25;
  }
  /* ⚠️ 必须写在媒体查询**内部**：与上面的 `html[lang='en']` 特异性相同，
     靠"后出现"取胜（媒体查询在文件靠后）。放到外面会被基础规则盖掉。 */
  html[lang='en'] .welcome-title {
    font-size: var(--m-welcome-title-en);
  }
  .welcome-list {
    gap: var(--m-welcome-gap);
    margin-top: 20px;
  }
  .welcome-item {
    gap: 12px;
  }
  .welcome-ic {
    width: var(--m-welcome-icon);
    height: var(--m-welcome-icon);
    border-radius: 10px;
  }
  .welcome-ic svg {
    width: var(--m-welcome-glyph);
    height: var(--m-welcome-glyph);
  }
  .welcome-start {
    height: var(--m-welcome-btn);
    font-size: 15px;
  }
}

/* ===== 首次访问 · 操作指引教练层（v1.5.11，挖洞手法 v1.5.13 换过）=====
   ⚠️⚠️ 压暗用 **SVG `fill-rule="evenodd"`，不是 `box-shadow: 0 0 0 9999px`**。
      原手法（与第 16 章「拖框选」同一套）在**单洞**时很省事，但它**数学上做不到多洞**：
      每个洞的 `box-shadow` 都会盖住另一个洞的"洞内"。
      第 2 步要同时圈「顶部可收起的分类下拉」与「底部 dock 的搜索+取色」——
      两者相距 550px，**取并集**会得到一个覆盖几乎全屏的大框（把色卡网格全圈进去）。
      所以改成：把压暗画成一个**带孔的填充** —— 一个外框子路径 + 每个洞一个子路径，
      `fill-rule: evenodd` 自动挖空。圆角由 coach.js 自己拼 arc（见 roundRectPath）。
   ⚠️ 层级 97：高于欢迎层(96)，低于 toast(100)。
   ⚠️ 所有几何由 coach.js 用 getBoundingClientRect() 现算：
      · 洞的坐标 → 内联 left/top/width/height（写在 .coach-hole 上，只负责描边）
      · 压暗的形状 → SVG 的 viewBox 与 <path d>
      这里只给外观，**不要在这里写死位置**。 */
.coach-mask {
  position: fixed;
  inset: 0;
  z-index: 97;
  overflow: hidden;          /* 不再"与 box-shadow 成对"，但对任何意外溢出仍是兜底 */
}
/* 压暗层（带孔的填充）。
   ⚠️ pointer-events: none —— 点击要穿透到 .coach-mask 的 onTap（洞内点击 = 下一步），
      否则整块遮罩吃掉点击。 */
.coach-dim {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}
.coach-dim path {
  fill: rgba(20, 23, 32, 0.55);
}
/* 洞的白色描边。⚠️ 只画描边，**不再负责压暗**（压暗已交给 .coach-dim）。
   ⚠️ 这里的 12px 必须与 coach.js 的 HOLE_R 一致 —— 洞是 SVG 挖的、描边是 DOM 画的，
      两个值一旦不一致，描边就会与洞错位（看着像描边浮在暗区里）。有断言钉住。 */
.coach-hole {
  position: absolute;
  border-radius: 12px;
  box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.92);
}
.coach-tip {
  position: absolute;
  box-sizing: border-box;
  max-width: calc(100vw - 24px);
  /* v2.1.9：内距重新配平（上 18 / 左右 20 / 下 16）——
     进度行删掉后标题成了第一行，上内距必须比原来(16)略大，
     否则 20px 的标题会顶到圆角上，看着"贴边"。 */
  padding: 18px 20px 16px;
  border-radius: 18px;
  background: var(--bg-elev);
  border: 0.5px solid rgba(20, 23, 32, 0.08);
}
/* ⚠️ v2.1.9 删除了 `.coach-count`（「第 x 步 / 共 x 步」进度行）。
   理由：4 步很短，显示进度只会让首访者去读"还剩几步"，而不是读这一步要做什么。
   它的 i18n 键也一并删除（已无引用者）—— 键名刻意不写在这里，见 coach.js 里的同条纪律。 */
.coach-tip-title {
  margin: 0;
  /* v2.1.9：15px → 20px。气泡里只有「标题 + 一句说明 + 一个按钮」三层，
     标题是唯一的信息锚点，按 Apple 的大标题口径放大、收紧字距。 */
  font-size: 20px;
  line-height: 1.28;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--text);
}
.coach-tip-sub {
  margin: 8px 0 0;
  font-size: 13px;
  line-height: 1.55;
  color: var(--text-muted);
}
.coach-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  margin-top: 18px;
}
/* ⚠️ v2.1.9 删除了 `.coach-skip`（「跳过」按钮）。
   理由：它的唯一作用是退出，而**点遮罩任意处已经能推进到下一步**，
   走完 4 步只要 4 次点击 —— 多一个次要出口只会让主按钮失焦。
   `.coach-next` 从"次要按钮组"里独立出来（不再有共享的 height/padding 规则）。 */
.coach-next {
  height: 36px;
  padding: 0 20px;
  border-radius: 999px;
  border: 0;
  font-family: inherit;
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
  background: var(--primary);
  color: #fff;
  transition: background-color 0.18s ease;
}
.coach-next:hover {
  background: var(--primary-hover);
}

/* 窄屏：气泡内距与按钮高度略放大（贴合触屏）。
   ⚠️ 这几个值刻意**不**进 --m-* 比例尺：它们是可读性/可点面积的**下限**，
      再缩会伤可用性；真正需要随视口缩放的是欢迎层的 hero 大标题。 */
@media (max-width: 640px) {
  .coach-tip {
    padding: 16px 18px 14px;
    border-radius: 18px;
  }
  .coach-next {
    height: 38px;
  }
}

/* =========================================================
   后台 · 面板卡片样式（v3.1.1 · 方案A 代码整理）
   ---------------------------------------------------------
   upd-* / site-* / rc-* / ck-* 四组样式原先各自内联写在 admin.html 的
   四个 <section> 里（4 段 <style>）。v3.1.1 把它们集中到这里：
   **选择器与取值一字未改**，只是换了存放的位置。

   为什么等价：内联 <style> 里的选择器本来就是**全局**生效的，搬到这里
   层叠关系不变（四组之间的相对顺序照旧，且它们之间无同名选择器）。
   已核对 style.css 此前不存在 upd-/site-/rc-/ck- 四个前缀的规则。
   收益：admin.html 只留结构，4 段样式可被其它页面复用。 */

.upd-grid { display:flex; flex-wrap:wrap; gap:12px; margin-bottom:18px; }
.upd-stat { flex:1 1 190px; background:#fff; border:1px solid var(--line); border-radius:12px; padding:12px 14px; }
.upd-stat .k { font-size:12px; color:var(--text-muted); margin-bottom:4px; }
.upd-stat .v { font-size:15px; font-weight:600; }
.upd-drop { border:1.5px dashed var(--line); border-radius:12px; padding:22px 18px; text-align:center; background:#fafbfc; }
.upd-drop.on { border-color:var(--primary); background:var(--primary-soft,#eef5ff); }
.upd-drop p { margin:8px 0 0; color:var(--text-muted); font-size:13px; }
.upd-bar { height:6px; border-radius:99px; background:#eceef2; overflow:hidden; margin-top:14px; display:none; }
.upd-bar.on { display:block; }
.upd-bar > i { display:block; height:100%; width:0; background:var(--primary); transition:width .18s ease; }
.upd-preview { margin-top:16px; padding:14px 16px; border:1px solid var(--line); border-radius:12px; display:none; }
.upd-preview.on { display:block; }
.upd-preview .row { display:flex; justify-content:space-between; gap:12px; padding:4px 0; font-size:14px; }
.upd-preview .row .k { color:var(--text-muted); }
.upd-ver { font-weight:700; }
.upd-log { margin-top:14px; border:1px solid var(--line); border-radius:12px; max-height:280px; overflow:auto; display:none; }
.upd-log.on { display:block; }
.upd-log .item { display:flex; gap:10px; padding:9px 14px; border-bottom:1px solid #f3f4f6; font-size:13.5px; }
.upd-log .item:last-child { border-bottom:none; }
.upd-log .ic { flex:0 0 16px; font-weight:700; }
.upd-log .ic.ok { color:#059669; }
.upd-log .ic.bad { color:#dc2626; }
.upd-log .nm { flex:0 0 100px; color:var(--text-muted); }
.upd-log .ms { flex:1; }
.upd-actions { margin-top:16px; display:flex; gap:10px; align-items:center; }
.upd-note { font-size:12.5px; color:var(--text-muted); margin-top:10px; line-height:1.7; }
.upd-badge { display:inline-block; padding:1px 8px; border-radius:99px; font-size:12px; font-weight:600; }
.upd-badge.ok { background:#ecfdf5; color:#065f46; }
.upd-badge.rb { background:#fff7ed; color:#9a3412; }
.upd-mono { font-family:ui-monospace,SFMono-Regular,Menlo,monospace; font-size:12.5px; }

.site-grid { display:flex; flex-wrap:wrap; gap:16px; align-items:flex-start; }
.site-col { flex:1 1 320px; min-width:0; background:#fff; border:1px solid var(--line); border-radius:12px; padding:16px; }
.site-col h4 { margin:0 0 4px; font-size:14px; }
.site-sub { margin:0 0 14px; font-size:12.5px; color:var(--text-muted); line-height:1.6; }
.site-field { margin-bottom:12px; }
.site-label { display:block; font-size:12.5px; color:var(--text-muted); margin-bottom:6px; }
.site-input { width:100%; box-sizing:border-box; padding:9px 12px; border:1px solid var(--line); border-radius:10px; font-size:13.5px; background:#fff; color:var(--text); font-family:inherit; }
.site-input:focus { outline:none; border-color:var(--primary); box-shadow:0 0 0 3px rgba(84,84,84,.15); }
.site-row { display:flex; align-items:center; gap:10px; flex-wrap:wrap; }
.site-range { flex:1 1 120px; min-width:100px; }
.site-num { width:72px; padding:7px 9px; border:1px solid var(--line); border-radius:9px; font-size:13px; text-align:center; font-family:inherit; }
.site-preview { margin-top:10px; padding:10px 12px; border:1px dashed var(--line); border-radius:10px; background:#fafbfd; display:flex; align-items:center; justify-content:center; min-height:60px; }
.site-preview img { max-width:100%; }
.site-preview .empty { font-size:12.5px; color:var(--text-muted); }
.site-fav { width:40px; height:40px; object-fit:contain; }
.site-msg { margin-top:14px; padding:10px 12px; border-radius:10px; font-size:13.5px; background:#f8fafc; color:var(--text-muted); display:none; }
.site-msg.show { display:block; }
.site-msg.ok { background:#ecfdf5; color:#065f46; }
.site-msg.bad { background:#fef2f2; color:#991b1b; }
.site-hint { margin:8px 0 0; font-size:12px; color:var(--text-muted); line-height:1.65; }
.site-file { display:none; }
.site-actions { display:flex; gap:8px; flex-wrap:wrap; margin-top:4px; }

.rc-grid { display:flex; flex-wrap:wrap; gap:16px; align-items:flex-start; }
.rc-card { flex:1 1 340px; min-width:0; background:#fff; border:1px solid var(--line); border-radius:12px; padding:16px; }
.rc-card.wide { flex:1 1 100%; }
.rc-head { display:flex; align-items:center; justify-content:space-between; gap:10px; margin-bottom:12px; flex-wrap:wrap; }
.rc-head h4 { margin:0; font-size:14px; }
.rc-sub { margin:0 0 12px; font-size:12.5px; color:var(--text-muted); line-height:1.6; }
.rc-kv { display:grid; grid-template-columns:auto 1fr; gap:6px 12px; font-size:12.5px; }
.rc-kv dt { color:var(--text-muted); white-space:nowrap; }
.rc-kv dd { margin:0; font-family:ui-monospace,Menlo,Consolas,monospace; word-break:break-all; }
.rc-field { margin-bottom:12px; }
/* v3.7.2：后台「启用的模型」每一项 = 勾选行 + 展示名输入行（两块一组）。
   `.rc-row` 自身没有外边距，不补这条的话多个模型会**挤成一坨**，
   看不出"哪个输入框属于哪个模型"。 */
.ai-model-row { margin-bottom:12px; }
.rc-label { display:block; font-size:12.5px; color:var(--text-muted); margin-bottom:6px; }
.rc-row { display:flex; align-items:center; gap:10px; flex-wrap:wrap; }
.rc-range { flex:1 1 120px; min-width:100px; }
.rc-num { width:76px; padding:7px 9px; border:1px solid var(--line); border-radius:9px; font-size:13px; text-align:center; font-family:inherit; }
.rc-hint { margin:8px 0 0; font-size:12px; color:var(--text-muted); line-height:1.65; }
.rc-badge { display:inline-block; padding:3px 9px; border-radius:999px; font-size:12px; background:#f1f5f9; color:var(--text-muted); }
.rc-badge.ok { background:#ecfdf5; color:#065f46; }
.rc-badge.bad { background:#fef2f2; color:#991b1b; }
.rc-badge.busy { background:#eff6ff; color:#1d4ed8; }
.rc-bar { height:8px; border-radius:999px; background:#eef2f7; overflow:hidden; margin-top:8px; }
.rc-bar > i { display:block; height:100%; width:0; background:var(--primary); transition:width .25s ease; }
.rc-table { width:100%; border-collapse:collapse; font-size:12.5px; }
.rc-table th { text-align:left; font-weight:600; color:var(--text-muted); padding:8px 8px; border-bottom:1px solid var(--line); white-space:nowrap; }
.rc-table td { padding:8px 8px; border-bottom:1px solid var(--line); vertical-align:middle; }
.rc-table tr:last-child td { border-bottom:none; }
.rc-pick { width:104px; height:78px; object-fit:cover; border-radius:8px; border:1px solid var(--line); cursor:crosshair; display:block; }
.rc-swatch { display:inline-block; width:16px; height:16px; border-radius:4px; border:1px solid var(--line); vertical-align:-3px; margin-right:6px; }
.rc-mono { font-family:ui-monospace,Menlo,Consolas,monospace; }
.rc-seed { width:92px; padding:6px 8px; border:1px solid var(--line); border-radius:8px; font-size:12.5px; font-family:ui-monospace,Menlo,Consolas,monospace; text-align:center; }
.rc-ops { display:flex; gap:6px; flex-wrap:wrap; }
.rc-msg { margin-top:14px; padding:10px 12px; border-radius:10px; font-size:13px; background:#f8fafc; color:var(--text-muted); display:none; }
.rc-msg.show { display:block; }
.rc-msg.ok { background:#ecfdf5; color:#065f46; }
.rc-msg.bad { background:#fef2f2; color:#991b1b; }
.rc-file { display:none; }
/* 预热进度盒（v3.0.2）：入口是表格每行的「预热」按钮，这里只放进度与控制。
   ⚠️ 进度条复用上面的 .rc-bar（全站缓存页也用它），别再定义一个同款。 */
.rc-warm { margin-top:14px; padding:12px 14px; border:1px solid var(--line); border-radius:10px; background:#fafbfd; }
.rc-warm[hidden] { display:none; }
.rc-warm-head { display:flex; align-items:center; justify-content:space-between; gap:10px; flex-wrap:wrap; margin-bottom:8px; }
.rc-warm-title { font-size:13px; font-weight:600; }
.rc-warm-stat { margin-top:8px; font-size:12.5px; color:var(--text-muted); line-height:1.7; }
.rc-warm-err { margin-top:4px; font-size:12px; color:#991b1b; line-height:1.7; word-break:break-all; }
.rc-table tr.rc-warming td { background:#eff6ff; }
/* 预热期间行内操作一律禁用：清缓存 / 换底图 / 再点一次预热都会与
   正在跑的那一批抢同一份缓存文件。禁用态要看得出来，否则像"点了没反应"。 */
.rc-table button[data-op]:disabled { opacity:.45; cursor:not-allowed; }

.ck-grid { display:flex; flex-wrap:wrap; gap:16px; align-items:flex-start; }
.ck-card { flex:1 1 340px; min-width:0; background:#fff; border:1px solid var(--line); border-radius:12px; padding:16px; }
.ck-card.wide { flex:1 1 100%; }
.ck-head { display:flex; align-items:center; justify-content:space-between; gap:10px; margin-bottom:12px; flex-wrap:wrap; }
.ck-head h4 { margin:0; font-size:14px; }
.ck-kv { display:grid; grid-template-columns:auto 1fr; gap:6px 12px; font-size:12.5px; }
.ck-kv dt { color:var(--text-muted); white-space:nowrap; }
.ck-kv dd { margin:0; font-family:ui-monospace,Menlo,Consolas,monospace; word-break:break-all; }
.ck-table { width:100%; border-collapse:collapse; font-size:12.5px; }
.ck-table th { text-align:left; font-weight:600; color:var(--text-muted); padding:8px 8px; border-bottom:1px solid var(--line); white-space:nowrap; }
.ck-table td { padding:8px 8px; border-bottom:1px solid var(--line); vertical-align:middle; }
.ck-table tr:last-child td { border-bottom:none; }
.ck-mono { font-family:ui-monospace,Menlo,Consolas,monospace; }
.ck-tag { display:inline-block; padding:2px 8px; border-radius:999px; font-size:11.5px; }
.ck-tag.asset { background:#fef3c7; color:#92400e; }
.ck-tag.regen { background:#ecfdf5; color:#065f46; }
.ck-hint { margin:8px 0 0; font-size:12px; color:var(--text-muted); line-height:1.65; }
/* 容量超限时的水位提示必须与正常水位**一眼分得开**（否则"自动清理过一次"没人会注意到）。
   v3.0.2：这一条原先叫 .imp-cache-warn，随「导入数据」页删除后没补回来 ——
   结果是 quotaHint() 仍在拼 ck-over 类名，而样式表里没有它 ⇒ 超限与正常长得一模一样。 */
.ck-hint.ck-over { color:#991b1b; }
.ck-sub { margin:0 0 12px; font-size:12.5px; color:var(--text-muted); line-height:1.6; }
.ck-bar { height:8px; border-radius:999px; background:#eef2f7; overflow:hidden; margin-top:8px; }
.ck-bar > i { display:block; height:100%; width:0; background:var(--primary); transition:width .25s ease; }
.ck-msg { margin-top:14px; padding:10px 12px; border-radius:10px; font-size:13px; background:#f8fafc; color:var(--text-muted); display:none; }
.ck-msg.show { display:block; }
.ck-msg.ok { background:#ecfdf5; color:#065f46; }
.ck-msg.bad { background:#fef2f2; color:#991b1b; }

/* =========================================================
   AI 生图页（v3.4.0 · ai.html + js/ai.js）
   ---------------------------------------------------------
   ⚠️ 本段是**追加在文件末尾**的独立一节，不动上面任何一行。
   ⚠️ 全部复用既有令牌与类（--primary / --primary-soft / --bg / --bg-elev /
      --line / --text* / --radius-* / --shadow-* / --m-*），
      不新引外部资源（无字体、无图片），占位一律走渐变或内联 SVG。
   ⚠️ 类名统一 `ai-` 前缀，避免与色卡列表 / 后台的通用类（.chip / .card /
      .modal …）互相污染：通用类只**借用**（例如按钮直接写 class="chip"），
      需要改外观时只在 `.ai-` 作用域里覆盖。
   ========================================================= */

/* ---- 版心：桌面单列居中 max-width 660，手机全宽 ---- */
.ai-wrap { max-width: 660px; margin: 0 auto; }
.ai-title { margin: 8px 0 2px; font-size: 22px; font-weight: 700; letter-spacing: .2px; }
.ai-sub { margin: 0 0 16px; font-size: 13px; color: var(--text-muted); line-height: 1.6; }

/* ---- 输入卡 ---- */
.ai-card {
  position: relative;
  background: var(--bg-elev);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  padding: 12px;
  transition: border-color .15s ease, box-shadow .15s ease;
}
/* 拖入参考图时：主色描边 + 4px 光晕（拖到卡内任意位置都算） */
.ai-card.dragover {
  border-color: var(--primary);
  box-shadow: 0 0 0 4px rgba(84, 84, 84, .14);
}

/* ---- 参考图入口（版本 1 · 缩略图行） ---- */
.ai-refs { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }
.ai-refs:not(:empty) { margin-bottom: 8px; }
/* 空态那一枚虚线方块（没有、也不再有「添加参考图（可选）」文字按钮） */
.ai-ref-add {
  width: 56px;
  height: 56px;
  flex: 0 0 auto;
  border-radius: var(--radius-md);
  border: 1.5px dashed var(--line);
  background: transparent;
  color: var(--text-faint);
  font-size: 22px;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: border-color .15s ease, color .15s ease, background .15s ease;
}
.ai-ref-add:hover { border-color: var(--primary); color: var(--primary); background: var(--primary-soft); }
.ai-ref-add:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.ai-ref {
  position: relative;
  width: 56px;
  height: 56px;
  flex: 0 0 auto;
  border-radius: var(--radius-md);
  border: 1px solid var(--line);
  background-color: var(--bg-soft);
  background-size: cover;
  background-position: center;
}
/* × 删除键**默认可见**（手机没有 hover） */
.ai-ref-x {
  position: absolute;
  top: -7px;
  right: -7px;
  width: 21px;
  height: 21px;
  border-radius: 50%;
  background: rgba(20, 23, 32, .74);
  color: #fff;
  font-size: 13px;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
}
.ai-ref-x:hover { background: rgba(20, 23, 32, .92); }

/* ---- 描述词输入（无边框、自动增高，最少 2 行） ---- */
.ai-textarea {
  display: block;
  width: 100%;
  min-height: 44px;         /* 约 2 行 */
  max-height: 220px;
  padding: 6px 2px;
  border: none;
  outline: none;
  resize: none;
  background: transparent;
  font-size: 15px;
  line-height: 1.6;
  color: var(--text);
  overflow-y: auto;
}
.ai-textarea::placeholder { color: var(--text-faint); }

/* ---- 卡底一行：左 = 当前模型名（只读徽标），右 = 计数 + 发送键 ----
   v3.7.2：原来这里是四枚参数胶囊（模型 / 比例 / 张数 / 更多参数）+ 横向滚动 + 两端渐隐，
          用户要求**整体删除**，改为直接显示当前调用的模型名。
   ⚠️ 刻意做成"一眼看出不是按钮"：无 hover 高亮、无指针变化、底色是浅灰不是白。
      做成可点的样子会让人以为还能改参数 —— 而参数现在只由后台决定。 */
.ai-foot { display: flex; align-items: flex-end; justify-content: space-between; gap: 8px; margin-top: 8px; }
.ai-model {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  flex: 0 1 auto;
  min-width: 0;
  padding: 5px 11px 5px 9px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--bg-soft);
  font-size: 12.5px;
  line-height: 1.5;
  cursor: default;
}
/* 小圆点 = 状态指示（与卡片上的品牌色呼应），外圈那层光晕用 primary-soft */
.ai-model-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  flex: 0 0 auto;
  background: var(--primary);
  box-shadow: 0 0 0 3px var(--primary-soft);
}
.ai-model-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--text);
  font-weight: 500;
}
/* 一个可用模型都没有时：虚线边 + 灰点，明确表示"这里没东西"而不是"加载中" */
.ai-model.off { background: transparent; border-style: dashed; }
.ai-model.off .ai-model-dot { background: var(--text-faint); box-shadow: none; }
.ai-model.off .ai-model-name { color: var(--text-faint); font-weight: 400; }
.ai-send-wrap { display: flex; align-items: center; gap: 10px; flex: 0 0 auto; }
.ai-count { font-size: 12px; color: var(--text-faint); white-space: nowrap; }
.ai-send {
  width: 38px;
  height: 38px;
  flex: 0 0 auto;
  border-radius: 50%;
  background: var(--primary);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background .15s ease;
}
.ai-send svg { width: 18px; height: 18px; }
.ai-send:hover:not(.off) { background: var(--primary-hover); }
/* 禁用 / 空输入外观；`.off` 与 :disabled 同时给，键盘与视觉都到位 */
.ai-send.off,
.ai-send:disabled { background: var(--primary-soft); color: var(--text-faint); cursor: not-allowed; }
.ai-send:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.ai-spin { display: none; width: 16px; height: 16px; border-radius: 50%; border: 2px solid rgba(255, 255, 255, .45); border-top-color: #fff; animation: ai-spin .7s linear infinite; }
.ai-send.busy svg { display: none; }
.ai-send.busy .ai-spin { display: block; }
@keyframes ai-spin { to { transform: rotate(360deg); } }

/* ---- 过程状态行（v3.7.3：从原「本次结果」里挪出来，成为唯一的进行中反馈）----
   ⚠️★ 原「本次结果」栏（.ai-results / .ai-results-head / .ai-grid / .ai-tile*）
      已按用户要求**整体删除** —— 出图直接进「生成历史」，最新那条标「最新结果」。
      所以这条状态行绝不能再删：出图要 20~300 秒，没有它用户分不清
      "在上传 / 已送达上游 / 卡住了"。 */
.ai-progress {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 14px;
}
.ai-progress[hidden] { display: none; }
.ai-meta { font-size: 12px; font-weight: 400; color: var(--text-faint); }
.ai-stage { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; font-size: 12.5px; color: var(--text-muted); }
.ai-stage[hidden] { display: none; }
.ai-stage.bad { color: var(--danger); }
/* 骨架微光：历史列表里"正在生成"那张卡用（原来只有结果网格用） */
@keyframes ai-shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }

/* ---- 一行浅底提示 ---- */
.ai-tip { margin-top: 18px; padding: 10px 12px; border-radius: var(--radius-md); background: var(--bg-soft); color: var(--text-muted); font-size: 12.5px; line-height: 1.6; }

/* ---- 未登录 / 未启用 提示卡 ---- */
.ai-notice { padding: 22px 18px; border: 1px dashed var(--line); border-radius: var(--radius-lg); background: var(--bg-elev); color: var(--text-muted); font-size: 13.5px; line-height: 1.8; text-align: center; }
.ai-notice-btn { margin-top: 14px; margin-left: 8px; }

/* ---- 生成历史（v3.7.2 立；v3.7.3 起成为**唯一的图片区**）----
   ⚠️★ v3.7.3：原「本次结果」网格已删除 ⇒ 这里是页面上**唯一**显示出图的地方，
      所以它必须自己承担原来那套视觉：3 列网格、1px 边框 + --radius-md 圆角 + --bg-soft 底。
      （注释里"与本次结果保持一致"这句已作废 —— 没有"本次结果"那块了。） */
.ai-hist-wrap { margin-top: 26px; }
.ai-hist-wrap[hidden] { display: none; }
.ai-hist-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; margin-bottom: 10px; font-size: 13px; font-weight: 600; color: var(--text-muted); }
.ai-hist-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
.ai-hist {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  background: var(--bg-elev);
  overflow: hidden;
  /* v3.7.3：「最新结果」角标以卡片为参照绝对定位 */
  position: relative;
}
.ai-hist-thumb {
  display: flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: 1 / 1;
  background: var(--bg-soft);
  border-bottom: 1px solid var(--line);
}
.ai-hist-thumb img { display: block; width: 100%; height: 100%; object-fit: cover; }
.ai-hist-none { font-size: 12px; color: var(--text-faint); }
/* 提示词两行截断：不定长文本不截会让卡片高低不齐，网格里很难看 */
.ai-hist-text {
  margin: 8px 10px 0;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--text);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.ai-hist-meta { margin: 4px 10px 0; font-size: 11.5px; color: var(--text-faint); }
/* margin-top:auto 把操作行压到卡底 —— 卡片等高时按钮才会对齐成一条线 */
.ai-hist-ops { display: flex; gap: 6px; padding: 9px 10px 10px; margin-top: auto; }
.ai-hist-btn {
  flex: 1 1 0;
  min-width: 0;
  padding: 5px 0;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: #fff;
  color: var(--text-muted);
  font-size: 12px;
  line-height: 1.4;
  transition: border-color .15s ease, color .15s ease, background .15s ease;
}
.ai-hist-btn:hover:not(:disabled) { border-color: var(--primary); color: var(--primary); background: var(--primary-soft); }
.ai-hist-btn:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
/* 「删除」是破坏性动作：hover 用 danger 色，与另两枚一眼可分 */
.ai-hist-btn.bad:hover:not(:disabled) { border-color: var(--danger); color: var(--danger); background: #fef2f2; }
/* 没有结果图时「下载/续改」置灰（不是隐藏 —— 按钮位置稳定才不会被误以为功能没了） */
.ai-hist-btn:disabled { opacity: .45; cursor: not-allowed; }
.ai-hist-empty { grid-column: 1 / -1; margin: 0; padding: 20px 0; text-align: center; font-size: 12.5px; color: var(--text-faint); }
.ai-hist-more { display: block; margin: 12px auto 0; }
.ai-hist-more[hidden] { display: none; }

/* ---- 手机端 ---- */
@media (max-width: 640px) {
  /* 历史网格 3 列 → 2 列（原「结果网格」已随本次结果栏一起删除，只剩这一处） */
  .ai-hist-grid { grid-template-columns: repeat(2, 1fr); }
  .ai-title { font-size: 20px; }
  /* 历史卡在窄屏上按钮会挤 ⇒ 允许换行，不做三等分 */
  .ai-hist-ops { flex-wrap: wrap; }
  .ai-hist-btn { flex: 1 1 auto; min-width: 56px; padding: 5px 10px; }
}


/* =========================================================
   v3.5.3：AI 生图「过程状态条」+ 计时
   ─────────────────────────────────────────────────────────
   要解决的问题：出图要 20~40 秒，而原先全程只有一句静态文案，
   用户无法区分「正在上传参考图」/「已送达上游、正在出图」/「卡住了」，
   也不能确认参考图与提示词是否真的被上游收下 ⇒ 只能干等（"无效等待"）。
   现在三件事分开表达：
     · 状态点颜色 = 阶段（提交中=呼吸蓝 / 已送达=脉冲紫 / 失败=红）
     · 文案       = 具体在做什么（由 ai.js 填）
     · 秒表       = 独立元素、每 100ms 刷新（与文案互不打断）
   ⚠️ 状态点用 [data-phase]，由 js/ai.js 的 markPhase() 写在 #ai-stage 上。
   ========================================================= */
.ai-progress .ai-stage { margin-bottom: 0; flex: 1 1 auto; min-width: 0; }
.ai-progress .ai-stage::before {
  content: ''; flex: 0 0 auto; width: 7px; height: 7px; border-radius: 50%;
  background: var(--text-faint); box-shadow: 0 0 0 0 rgba(84, 84, 84, .35);
}
/* 提交中：呼吸（慢速淡入淡出，低性能开销，不用 filter/backdrop-filter） */
.ai-stage[data-phase='submit']::before { background: #4a90d9; animation: ai-dot-breathe 1.6s ease-in-out infinite; }
/* 已送达上游、等出图：脉冲光圈（这是最需要"还在动"的一段，等待最长） */
.ai-stage[data-phase='waiting']::before { background: #7b5cd6; animation: ai-dot-pulse 1.8s ease-out infinite; }
.ai-stage.bad::before { background: var(--danger); animation: none; }
/* 失败态不要再显示"在动" */
.ai-stage { display: flex; align-items: center; gap: 8px; }
@keyframes ai-dot-breathe { 0%, 100% { opacity: .35; } 50% { opacity: 1; } }
@keyframes ai-dot-pulse {
  0%   { box-shadow: 0 0 0 0 rgba(123, 92, 214, .45); }
  70%  { box-shadow: 0 0 0 7px rgba(123, 92, 214, 0); }
  100% { box-shadow: 0 0 0 0 rgba(123, 92, 214, 0); }
}
/* 秒表：等宽数字，跳秒时宽度不抖 */
.ai-timer {
  flex: 0 0 auto; font-size: 12.5px; color: var(--text-muted);
  font-variant-numeric: tabular-nums; font-feature-settings: 'tnum';
  letter-spacing: .2px; white-space: nowrap;
}
.ai-timer[hidden] { display: none; }
@media (prefers-reduced-motion: reduce) {
  .ai-stage[data-phase]::before { animation: none; box-shadow: none; }
}

/* v3.5.4 立、v3.7.3 改挂到历史缩略图上：取图失败时显示**原因**
   （原来只有「图片加载失败」五个字，用户报障时无从排查是 404 / 401 / 断网） */
.ai-hist-thumb.bad {
  flex-direction: column;   /* 两行：标题 + 原因 */
  gap: 2px;
  font-size: 12px;
  color: var(--text-faint);
  padding: 8px;
}
.ai-hist-why { font-size: 11px; color: var(--text-faint); word-break: break-all; text-align: center; }

/* =========================================================
   v3.7.3：AI 生图页 —— 出图落卡（骨架 / 最新角标 / 取图失败）
   ⚠️★ 「本次结果」栏删除后，历史列表成了页面上**唯一**的图片区，
      它必须自己承担原来那套视觉（3 列网格 / 细边框 / 圆角 / 浅底）。
   ========================================================= */
/* 生成中的骨架卡：尺寸与真实卡完全一致 ⇒ 出图后替换不跳动 */
.ai-hist-thumb.skeleton {
  background: linear-gradient(100deg, #eef0f5 30%, #f7f8fb 50%, #eef0f5 70%);
  background-size: 200% 100%;
  animation: ai-shimmer 1.2s linear infinite;
}
.ai-hist.pending .ai-hist-meta { color: var(--text-faint); }

/* 「最新结果」角标（v3.7.3：原「本次结果」栏删除后由它承担标识职责）
   ⚠️ 用绝对定位压在缩略图左上角，不占卡片布局高度 ——
      否则"有角标"和"没角标"的卡片会差一行，网格里高低不齐。 */
.ai-hist-badge {
  position: absolute;
  top: 8px;
  left: 8px;
  z-index: 2;
  padding: 2px 8px;
  border-radius: 999px;
  background: rgba(20, 23, 32, .82);
  color: #fff;
  font-size: 11px;
  line-height: 1.6;
  letter-spacing: .2px;
  pointer-events: none;   /* 别挡住缩略图的点击（点了要开灯箱） */
}
/* 最新那条整体描一圈主色：一眼看出"这就是刚才那张" */
.ai-hist.is-new {
  border-color: var(--primary);
  box-shadow: 0 0 0 3px var(--primary-soft);
}
/* 刚出、还没被服务端记录替换的临时卡（done 之后会重建整个列表） */
.ai-hist.fresh .ai-hist-meta { color: var(--primary); }

/* 缩略图可点（开灯箱）—— 给出手势提示，但**不要**做成按钮的样子 */
.ai-hist-thumb { cursor: zoom-in; }
.ai-ref { cursor: zoom-in; }

/* =========================================================
   v3.7.3：图片预览灯箱
   ⚠️★ 只补「它是一张简单预览、不是色卡详情」的**差异**部分：
      DOM 完全复用全站 `.modal-mask / .lightbox / .lightbox-head / .lightbox-title /
      .lightbox-head .close / .lightbox-body / .lightbox-panel / .lightbox-img`
      （见 js/ai.js 的 openLightbox）—— 遮罩色、圆角、阴影、关闭键尺寸因此与
      首页色卡灯箱天然一致。
      不要为了"看起来更简单"另起一套遮罩：同一件事两套外观，
      用户会以为它们是两个不同的功能。
   ⚠️ 基础 .lightbox 是**固定高度**（calc(100dvh-32px)，封顶 900px）——
      那是色卡详情页"改色图 / 色彩采样两态共用高度不跳动"的约束，与简单预览无关，
      所以这里改回自适应高度。
   ========================================================= */
.ai-lightbox {
  height: auto;
  max-height: 88vh;
  max-width: min(920px, 94vw);
}
/* 标题可能是整段描述词 ⇒ 允许两行（基类是 nowrap + 省略号） */
.ai-lightbox .lightbox-title {
  white-space: normal;
  overflow-wrap: anywhere;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.ai-lightbox .lightbox-body { padding: 12px; }
.ai-lightbox-panel {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg-soft);
  border-radius: 12px;
}
/* 基类的 .lightbox-img 是 width/height:100%（填满色卡槽位），预览要按内容自适应 */
.ai-lightbox-img {
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: min(74vh, 760px);
  object-fit: contain;
}

/* ══════════════════════════════════════════════════════════
   ★★★ v3.9.0 全局兜底：`[hidden]` 必须真的隐藏
   ----------------------------------------------------------
   作者样式里的任何 `display` 都会**压过**浏览器对 `hidden` 的默认
   `[hidden]{display:none}`（UA 样式优先级更低）—— `hidden` 属性写了等于没写。
   本项目为这个坑付出过三次代价（`.topbar .admin-entry` / `.btn` / `.seg`
   —— v3.9.0 实测：忘记密码页的分段控件"隐藏"后还挂在弹窗顶上）。
   与其逐个类名补 `[hidden]` 规则（全文件已补了 15+ 处、还会继续漏），
   不如在这里**一次性兜底**：`hidden` 的语义就是"藏"，不存在合法的反例。
   ⚠️ 不要在别处再写 `[hidden] { display: <非 none> }` 之类的例外。
   ══════════════════════════════════════════════════════════ */
[hidden] {
  display: none !important;
}

/* =========================================================
   v3.8.0 · 前台用户体系（登录/注册弹窗 · 顶栏用户位 · 个人中心）
   ---------------------------------------------------------
   ⚠️ 全部新增类名（`user-btn` / `auth-*` / `code-cell` / `acct-*`），
      改前已确认与既有样式零冲突（全仓 grep 命中 0）。
   ⚠️ 凡是 flex 容器且用 `hidden` 属性显隐的，都要显式写 `[hidden]` 覆盖 ——
      `.xxx { display:flex }` 会**压过** UA 的 `[hidden]{display:none}`
      （作者样式优先于 UA 样式），不写就是"点了关闭遮罩还在"。
      本文件里既有代码已踩过同一个坑（见 .topbar .admin-entry[hidden] 等）。
   ========================================================= */

/* ---------- 顶栏用户位 ---------- */
.topbar .topbar-user {
  display: flex;
  align-items: center;
}
.topbar .user-btn.is-out {
  height: 38px;
}
.topbar .icon-btn.user-btn {
  padding: 0;
}
.topbar .user-icon {
  display: flex;
}
.topbar .user-icon svg {
  width: 22px;
  height: 22px;
  display: block;
}

/* 会话未判定前的占位：保留布局、不可见
   ⚠️ 用 visibility 而不是 display:none —— 后者会让异步渲染前的顶栏
      完全没有这个节点，门禁的 jsdom 套件同步读 DOM 时会得到 0（假红）。 */
.topbar .topbar-user.is-pending {
  visibility: hidden;
}

/* ---------- 登录 / 注册弹窗 ---------- */
.auth-mask {
  z-index: 95;
}
.auth-mask[hidden] {
  display: none !important;
}
.auth-modal .auth-seg {
  margin-bottom: 16px;
}

/* ══════════ v3.8.1 修复组（弹窗 UI 逐项对齐设计稿）══════════
   ⚠️ 背景（v3.8.0 实测踩坑）：弹窗用到 `.seg` / `.btn.block` / `.hint`
   三个类，但**它们从未被定义过** —— 作者样式缺失时元素会退化成裸盒子，
   界面看着"能用但不像设计稿"（胶囊分段没了、按钮不全宽、提示没弱化）。
   这里的规格**逐项抄自设计稿**（outputs/色卡站-用户系统设计方案-2026-10-10.html）。 */

/* 分段控件：方案 A 的核心（登录/注册胶囊切换） */
.seg {
  display: inline-flex;
  background: var(--bg-soft);
  border-radius: 999px;
  padding: 4px;
  width: 100%;
  font-size: 13.5px;
}
.seg button {
  flex: 1;
  padding: 7px 12px;
  border-radius: 999px;
  color: var(--text-muted);
  font-weight: 500;
  background: transparent;
  border: 0;
  cursor: pointer;
  font-family: inherit;
}
.seg button.active {
  background: #fff;
  color: var(--text);
  box-shadow: var(--shadow-sm);
}

/* 全宽按钮（设计稿 A-1/A-2/A-3 的主按钮） */
.btn.block {
  width: 100%;
}

/* 辅助文字（设计稿规格：11.5px / --text-faint） */
.hint {
  font-size: 11.5px;
  color: var(--text-faint);
}

/* ⚠️★★ 幽灵按钮防御：`.btn` 是 `display:inline-flex`（作者样式），
   会**压过**浏览器对 `hidden` 属性的默认 `display:none`（UA 样式）。
   不给这条兜底，凡是"写了 hidden 的 .btn"都会照常显示 ——
   v3.8.0 实测：登录弹窗里冒出了幽灵「← 返回」和一个空白胶囊按钮。
   全站统一兜住（`.btn` + hidden 只可能是"要藏"）。 */
.btn[hidden] {
  display: none !important;
}

/* 弹窗内按钮的禁用态：发送中 / 倒计时中必须看得出被禁用 */
.auth-modal .btn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

/* 登录/注册/忘密弹窗的脚部：**横排按钮行**（v3.12.0 用户要求：返回与主按钮
   平行一行，不叠按钮、不占空间；此前是纵向 column）。
   ⚠️ 站内既有的 `.modal .modal-foot` 是横向 flex-end（后台弹窗在用），
      这里覆盖为「主按钮占剩余、返回随行、重发/忘记整行」的排布。 */
.auth-modal .modal-foot {
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  padding: 6px 22px 18px;
}
/* 兜底：页脚按钮一律自适应宽（按权重压过 .btn.block 的 width:100%） */
.auth-modal .modal-foot .btn {
  flex: 0 0 auto;
  width: auto;
}
/* 主按钮：占剩余宽度 + 设计稿的 padding:11px 规格 */
.auth-modal .modal-foot .btn.primary {
  flex: 1 1 0;
  min-width: 0;
  padding: 11px;
}
/* 重发 / 「忘记密码？」：各自整行 */
.auth-modal .modal-foot .btn.ghost {
  flex: 1 1 100%;
}
/* 「← 返回」：与主按钮**同一行**的自适应次要按钮（正常描边 —— v3.12.0
   起它必须是"看得见的按钮"；旧版在忘密下被做成文字链接，用户点名要改） */
.auth-modal .modal-foot .btn.ghost.auth-back {
  flex: 0 0 auto;
  color: var(--text-muted);
}
/* 弹窗头部右侧组：语言胶囊 + 关闭键（v3.12.0 语言入口统一位置） */
.auth-head-right {
  display: flex;
  align-items: center;
  gap: 10px;
}
.auth-head-right .lang-switch {
  font-size: 12px;
}
.auth-head-right .lang-switch button {
  padding: 4px 10px;
}
.auth-forgot-tip {
  margin: 2px 0 0;
  line-height: 1.6;
}
.auth-code-title {
  margin: 0 0 2px;
  font-size: 15px;
  line-height: 1.6;
  text-align: center;
}
.auth-code-to {
  margin: 0;
  text-align: center;
}
.auth-code-row {
  justify-content: center;
  margin: 14px 0 0;
}
.auth-code-exp {
  margin: 6px 0 0;
  text-align: center;
}
.auth-err {
  margin: 10px 0 0;
}
.auth-err2 {
  margin: 8px 0 0;
  text-align: center;
}
.auth-lang {
  /* v3.12.0：语言胶囊容器（现在位于弹窗头部关闭键左侧）——
     不再需要页脚的居中与上边距 */
  display: flex;
  align-items: center;
}

/* ---------- 验证码 6 格 ---------- */
.code-row {
  display: flex;
  gap: 8px;
}
.code-cell {
  width: 48px;
  height: 56px;
  padding: 0;
  text-align: center;
  font-family: inherit;
  font-size: 20px;
  font-weight: 500;
  color: var(--text);
  background: #fff;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.code-cell:focus {
  outline: none;
  border-color: var(--primary);
  box-shadow: 0 0 0 3px rgba(84, 84, 84, 0.15);
}
.code-cell.is-filled {
  border-color: var(--primary);
}
.code-cell.is-bad {
  border-color: var(--danger);
  background: #fff5f6;
  color: var(--danger);
}

/* ---------- 个人中心 ---------- */
.account-page {
  max-width: 860px;
  margin: 0 auto;
  padding: 18px 0 40px;
}
.acct-loading {
  padding: 44px 0;
  text-align: center;
  color: var(--text-faint);
  font-size: 13px;
}
.acct-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 0;
  border-bottom: 1px solid var(--line);
}
.acct-row:last-child {
  border-bottom: 0;
}
.acct-k {
  font-size: 13px;
  color: var(--text-muted);
}
.acct-v {
  font-size: 13.5px;
  font-weight: 500;
  text-align: right;
  word-break: break-all;
}
.acct-codewrap {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}
.acct-code-row .code-cell {
  width: 44px;
  height: 52px;
}
.err.is-ok {
  color: var(--success);
}

/* v3.9.0（用户要求）：个人页的内容卡片不做 hover 抬起。
   ⚠️ v3.10.0：一页流的组卡改用 .acct-group（不再挂 .card —— 那套 cursor/transition
      属于色卡卡片，信息组不需要），本规则**保留为防御**：以后若有人给本页元素
      误加 .card，也不会抬起。 */

.account-page .card:hover {
  transform: none;
}

/* ---------- 手机端 ---------- */
@media (max-width: 640px) {
  .code-cell {
    width: 44px;
    height: 52px;
  }
}

/* ---------- 后台「邮件发信」面板（v3.8.0） ---------- */
.mail-switch {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13.5px;
  cursor: pointer;
}
.mail-preview-wrap {
  margin-top: 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  overflow: hidden;
  background: #fff;
}
.mail-preview-subject {
  padding: 10px 14px;
  border-bottom: 1px solid var(--line);
  background: var(--bg-soft);
  font-size: 12.5px;
  color: var(--text-muted);
  word-break: break-all;
}
.mail-preview-frame {
  display: block;
  width: 100%;
  height: 320px;
  border: 0;
  background: #fff;
}
.mail-log {
  font-size: 12.5px;
  color: var(--text-muted);
}
.mail-log-row {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 6px 0;
  border-bottom: 1px solid var(--line);
}
.mail-log-row:last-child {
  border-bottom: 0;
}
.mail-log-ok {
  color: var(--success);
}
.mail-log-bad {
  color: var(--danger);
}
.mail-log-t {
  margin-left: auto;
  color: var(--text-faint);
  white-space: nowrap;
}

/* =========================================================
   v3.13.29 · 后台分页条（发信记录 / 生成历史共用）
   ---------------------------------------------------------
   ⚠️ 与前台 .pager（色域页码浮层）**不是一套东西**，别为了"统一"合并：
      前台那套带面板锚点、28px 底部预留和 .content--pager 弹性链，
      搬进后台会把面板顶起来，还会在窄屏上跟 dock 抢位置。
   ⚠️ 容器常驻、单页时 hidden（由 JS 设 hidden）—— 翻页时高度不跳；
      若改成"有数据才插入 DOM"，每翻一页都会重建节点与监听。
   ⚠️ 单页时 hidden 靠 [hidden] 全局规则 + 这里的两条兜底，两者都在：
      只靠 flex 的 display:flex 会盖掉 UA 的 [hidden]{display:none}。
   ========================================================= */
.adm-pager {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  margin-top: 14px;
  font-size: 12.5px;
  color: var(--text-muted);
}
.adm-pager[hidden] {
  display: none;
}
.adm-pager-btn {
  width: 30px;
  height: 30px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--line);
  background: var(--bg-elev);
  border-radius: var(--radius-sm);
  color: var(--text);
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease;
}
.adm-pager-btn:hover:not(:disabled) {
  background: var(--bg-soft);
  border-color: #d4d8e1;
}
.adm-pager-btn:disabled {
  opacity: 0.4;
  cursor: default;
}
.adm-pager-info {
  min-width: 74px;
  text-align: center;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}
.adm-pager-total {
  color: var(--text-faint);
  font-size: 12px;
}

/* =========================================================
   v3.9.0 · 收藏夹工具栏 / 批量管理
   ---------------------------------------------------------
   ⚠️ `.fav-bar` 是 flex + hidden 显隐 —— 必须显式写 [hidden] 覆盖
      （作者样式的 display 会压过 UA 的 [hidden]{display:none}，
       与 .btn[hidden] 是同一个坑，见 v3.8.1 的注释）。
   ========================================================= */
.fav-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 12px 0 0;
  min-height: 32px;
}
.fav-bar[hidden] {
  display: none !important;
}
.fav-bar-info {
  margin-right: auto;
  font-size: 13px;
  color: var(--text-muted);
}

/* =========================================================
   v3.13.19 · 收藏夹批量管理：底部浮动操作条（方案 B）
   ---------------------------------------------------------
   ⚠️ 位置由 dock 的几何推导：dock 底距 + dock 高 + 8px 间隙。
      不许把 52 / 8 写死在 JS 里 —— --m-dock-btn 是 clamp()，
      写死会在窄屏上错位（与回到顶部的公式同源）。
   ⚠️ z-index 44：低于 dock(45)，万一贴住也由 dock 在上。
   ========================================================= */
.fav-action {
  position: fixed;
  left: 50%;
  transform: translateX(-50%);
  bottom: calc(var(--m-dock-bottom) + env(safe-area-inset-bottom, 0px) + var(--dock-lift, 0px) + var(--m-dock-btn) + 10px + 8px);
  z-index: 44;
  width: min(340px, calc(100vw - 40px));
  height: 52px;
  padding: 0 6px 0 16px;
  border-radius: 999px;
  background: var(--bg-elev);
  border: 1px solid var(--line);
  box-shadow: 0 10px 28px rgba(20, 23, 32, 0.12);
  display: flex;
  align-items: center;
  gap: 8px;
}
.fav-action[hidden] {
  display: none !important;
}
/* v3.13.21：全选从纯文字改成胶囊按钮 —— 与右侧「删除 / 完成」同一套语汇
   v3.13.26 方案 A：改成白底 1px 描边幽灵 —— 让「完成」成为唯一实底主操作，
   视觉重心自然落到右下；浅灰底会与删除的浅红底抢同一层级。 */
.fav-action-all {
  margin-right: auto;
  height: 32px;
  padding: 0 14px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--bg-elev);
  font: inherit;
  font-size: 12.5px;
  color: var(--primary);
  cursor: pointer;
}
/* v3.13.21：不再淡化未选卡 —— 用户给的参考图是「只给选中项描边 + 打钩」，
   未选项保持原色（淡化会让人以为它们被禁用） */
/* 悬浮条会盖住最后一行：管理态给网格留出它的高度 */
.fav-select .card-grid {
  padding-bottom: 76px;
}

/* 勾选圈：仅收藏夹管理模式（.fav-select）下显示。
   v3.13.4：**个人颜色（.card-mine）也参与批量** —— 原先把它们排除在勾选圈外，
   用户报「无法选择和管理收藏的个人颜色」；现一视同仁（16.33 + favorites-order Case 7）。 */
.card .pick-dot {
  position: absolute;
  top: 8px;
  right: 8px;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.94);
  border: 1.5px solid var(--line);
  box-shadow: var(--shadow-sm);
  display: none;
  align-items: center;
  justify-content: center;
  transition: background 0.15s ease, border-color 0.15s ease;
}
/* v3.13.22：管理态每张卡都显示空心圈 —— 这是「可以点」的暗示；
   勾只在选中后出现（见下面的 ::after） */
.fav-select .card .pick-dot {
  display: flex;
}
.fav-select .card {
  cursor: pointer;
}
/* 未选中：圈里不画东西（空心） */
.card .pick-dot::after {
  content: none;
}
/* v3.13.22：选中才画白色对勾（用户点名：要打钩，不要白点） */
.card.is-picked .pick-dot::after {
  content: '';
  width: 10px;
  height: 5px;
  border-left: 1.8px solid #fff;
  border-bottom: 1.8px solid #fff;
  border-radius: 0;
  background: none;
  transform: rotate(-45deg) translate(1px, -2px);
}
.card.is-picked .pick-dot {
  background: var(--primary);
  border-color: var(--primary);
  /* v3.13.25：勾加一圈白，压在白框上才有层次（深卡 / 浅卡都成立） */
  box-shadow: 0 0 0 2px #fff, 0 2px 6px rgba(20, 23, 32, 0.16);
}
/* v3.13.21：勾的颜色由 ::after 的边框决定，这里不再填白点 */
/* 选中态：v3.13.25 方案 D —— 白框浮起（用户点名不要深色描边）
   ⚠️ 三层各司其职：swatch 自带的 1px 浅边设透明（否则白框外会多一道灰线）；
      3px 纯白外框把卡片从 #f5f6fa 上"抠"出来；投影只做抬升，不参与识别。
   ⚠️ .card 没有 overflow:hidden，外框才能溢出到网格间隙里（10px 间距足够）。
   实证：.card{position:relative} 且 .swatch 才 overflow:hidden。 */
.fav-select .card.is-picked .swatch {
  border-color: transparent;
  box-shadow: 0 0 0 3px #fff, 0 6px 16px rgba(20, 23, 32, 0.14);
}

/* v3.13.24：收藏夹首屏骨架（数据到货前的占位，防止空态闪一下） */
.skel-card {
  display: flex;
  flex-direction: column;
}
.skel-card .swatch {
  width: 100%;
  aspect-ratio: 1 / 1;
  border-radius: var(--m-card-r);
  background: var(--bg-soft);
}
.skel-card .code {
  width: 46%;
  height: 10px;
  margin-top: 8px;
  border-radius: 4px;
  background: var(--bg-soft);
}

/* 批量删除确认层的正文 */
.fav-del-text {
  margin: 0;
  font-size: 13.5px;
  line-height: 1.7;
  color: var(--text);
}

/* ---------- 用户管理表格（v3.9.0） ---------- */
.users-table-wrap {
  margin-top: 6px;
  background: var(--bg-elev);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  overflow: hidden;
}
.users-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13.5px;
}
.users-table th,
.users-table td {
  text-align: left;
  padding: 11px 14px;
  border-bottom: 1px solid var(--line);
  vertical-align: middle;
}
.users-table thead th {
  font-size: 12px;
  font-weight: 600;
  color: var(--text-muted);
  background: var(--bg-soft);
}
.users-table tbody tr:last-child td {
  border-bottom: 0;
}
.users-th-ops {
  width: 150px;
}
.users-email {
  word-break: break-all;
}
.users-me {
  color: var(--text-faint);
  font-size: 12px;
}
.users-badge {
  display: inline-block;
  padding: 2px 9px;
  border-radius: 999px;
  font-size: 11.5px;
  font-weight: 500;
  background: var(--primary-soft);
  color: var(--primary);
}
/* v3.13.17：普通用户也显示（中性灰，不与管理员抢视觉权重） */
.users-badge.is-user {
  background: var(--bg-soft);
  color: var(--text-muted);
}
.users-ops {
  display: flex;
  gap: 6px;
  justify-content: flex-end;
}
.users-row-form .field {
  margin-bottom: 12px;
}


/* =========================================================
   v3.10.0 · UI 重设计（设计稿已确认，2026-10-10；v3.12.1 修订）
   ---------------------------------------------------------
   ① 忘记密码弹窗：v3.12.1 起**取消品牌双栏**（用户选定「方案 A · 纯净同构」）——
      与登录/注册完全同构：460px 单栏、头部同款、验证码区居中、
      「有效期 · 重新发送」居中一行、返回与主按钮同行。
   ② 个人中心页 → 「方案 B · 一页流」：
      取消 tab，三组设置纵向排布 + 「修改密码」渐进披露 + 退出登录独立行。
   ⚠️ 显隐一律走 hidden 属性 —— 全站已有 `[hidden]{display:none!important}`
      兜底（v3.9.0 加），本段不必逐个补。
   ⚠️ 格子为「填充式」（灰底 + 透明占位边框），尺寸与旧描边式一致
      （全局 box-sizing: border-box）。
   ========================================================= */

/* ---------- ① 忘密弹窗：单栏（与登录/注册同构） ---------- */
.auth-body {
  min-width: 0;
}

/* v3.12.1：忘密表单区随单栏统一回**居中** —— v3.10.0 双栏时期的四条
   `.is-forgot` 左对齐覆盖（title/to/row/err2）已删，与注册第 2 步完全一致。 */

/* fg2 辅助行：**居中一行**「验证码 10 分钟内有效 · 重新发送（22s）」
   （v3.12.1：随品牌双栏一并取消左右分栏；重发仍是行内文字链接、不叠按钮） */
.auth-fg-aux {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0;
  margin-top: 7px;
  min-height: 18px;
}
.auth-modal.is-forgot .auth-code-exp {
  margin: 0;
}
/* 「·」分隔符由有效期文案的 ::after 提供（只在 fg2 屏出现） */
.auth-modal.is-forgot .auth-code-exp::after {
  content: '·';
  margin: 0 7px;
  color: #cfd4de;
}
.auth-modal.is-forgot .auth-fg-aux .btn {
  width: auto;
  height: auto;
  min-height: 0;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--text-muted);
  font-size: 12px;
  font-weight: 500;
}
.auth-modal.is-forgot .auth-fg-aux .btn:disabled {
  color: var(--text-faint);
}

/* v3.12.0：「返回上页」不再是"主按钮下方居中文字链接"——改为与主按钮
   平行同排的描边次要按钮（由 .auth-modal .modal-foot 的横排规则统一接管）；
   v3.12.1：文本去掉「←」箭头前缀（用户点名，纯文字按钮）。 */

/* ---------- 填充式验证码格（v3.10.0 全站统一：注册 / 忘密 / 个人中心） ---------- */
/* ⚠️ 边框用 1px 透明（占位、保持盒尺寸稳定）—— 别再写 1.5px：
   1x 屏会被 Chromium snap 成 1px、2x 屏真渲染 1.5px，跨设备不一致。 */
.code-cell {
  background: var(--bg-soft);
  border: 1px solid transparent;
}
.code-cell:focus {
  background: #fff;
  border-color: var(--primary);
  box-shadow: 0 0 0 3px rgba(84, 84, 84, 0.15);
}
.code-cell.is-filled {
  border-color: var(--primary);
}

/* ---------- ② 个人中心：一页流 ---------- */
.account-page {
  max-width: 640px;   /* 一页流 = 窄列（v3.10.0 自 860 收窄） */
}
.acct-head {
  padding: 10px 2px 0;
}
.acct-title {
  margin: 0;
  font-size: 22px;
  font-weight: 700;
  letter-spacing: -0.01em;
}
.acct-sub {
  margin: 5px 0 0;
  font-size: 13px;
  color: var(--text-muted);
  word-break: break-all;
}
.acct-gtitle {
  margin: 22px 4px 8px;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--text-muted);
  letter-spacing: 0.02em;
}
.acct-group {
  background: var(--bg-elev);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  padding: 4px 18px;
}
.acct-opt {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  width: 100%;
  padding: 13px 0;
  border: 0;
  border-bottom: 1px solid var(--line);
  background: transparent;
  color: var(--text);
  font-family: inherit;
  font-size: 13.5px;
  font-weight: 400;
  text-align: left;
  cursor: pointer;
}
.acct-opt:last-child {
  border-bottom: 0;
}
.acct-opt.is-on {
  font-weight: 600;
}
.acct-opt-link {
  text-decoration: none;
}
.acct-chk {
  width: 15px;
  height: 15px;
  stroke: var(--primary);
  fill: none;
  stroke-width: 2.4;
  stroke-linecap: round;
  stroke-linejoin: round;
  flex: 0 0 auto;
}
.acct-chv {
  width: 15px;
  height: 15px;
  stroke: #c4c8d2;
  fill: none;
  stroke-width: 2.2;
  stroke-linecap: round;
  stroke-linejoin: round;
  flex: 0 0 auto;
  transition: transform 0.18s ease;
}
.acct-opt-acc.is-open .acct-chv {
  transform: rotate(90deg);
}
.acct-acc-body {
  padding: 2px 0 16px;
  border-top: 1px solid var(--line);
}
.acct-acc-body .field {
  margin-top: 14px;
}
.acct-acc-body .field:first-child {
  margin-top: 16px;
}
.acct-acc-body #acct-submit {
  margin-top: 16px;
}
/* v3.12.1（用户要求）：返回网站 + 退出登录 —— **平行并列同一行**。
   等宽两枚（flex:1）、同一套 .btn 语汇：返回=描边 ghost（弱）、退出=浅红 danger。
   取代旧版「全宽透明行（acct-out）+ 页脚小胶囊（acct-foot）」的两段式布局。 */
.acct-actions {
  display: flex;
  gap: 10px;
  margin-top: 18px;
}
.acct-actions .btn {
  flex: 1 1 0;
  min-width: 0;
  padding: 10px 0;
  font-size: 13.5px;
}
.acct-backsite {
  color: var(--text-muted);
  text-decoration: none;
}
.acct-v.is-ok {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  color: var(--success);
}
.acct-v.is-ok .acct-chk {
  stroke: var(--success);
  width: 13px;
  height: 13px;
}
.acct-guest {
  margin-top: 16px;
  padding: 20px 18px;
  display: flex;
  flex-direction: column;
  gap: 14px;
  align-items: flex-start;
}
.acct-guest-text {
  margin: 0;
  font-size: 13.5px;
  color: var(--text-muted);
}
