/* =========================================================
   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;
  /* 主题色（v1.5.7）：来自 lab(32.13 8.9 -41.95) 的 **D65 实测解释** = #154b8e
     ⚠️ CSS 的 lab() 按 D50 计算，同一组数字会得到 #31498e —— 这里**故意**用 D65，
        因为该值来自分光仪（D65/10°）。别按 CSS 语义去"修正"它。
     淡色点缀（soft / 描边 / 选中行底）是按新主色色度比例（0.534）同步降过色的，
     不是原来的亮蓝 —— 改动它们之前先读 docs/开发护栏.md 第 26 章。 */
  --primary: #154b8e;
  --primary-hover: #003d7e;
  --primary-soft: #ecf0f8;
  --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） */
  --m-dock-btn: clamp(40px, 12.5vw, 50px);     /* dock 按钮边长（50） */
  --m-dock-icon: clamp(20px, 5.5vw, 22px);     /* dock 图标（22） */
  --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(21, 75, 142, 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，顶栏仅留后台入口 + 语言切换） */
.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(21, 75, 142, 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);
}

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

.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
      实测：400 → 32px｜348（用户那台 OPPO）→ 26px｜336 → 24px｜320 → 20px｜300 → 8px
      —— **恒为正**。故 v1.4.6 的 `@media (max-width:371px)` 抬升档**已删除**
      （连同它那段「窄屏要加大内容底部预留」的补丁，因为不再抬上去）。
   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));
  width: calc(var(--m-dock-btn) * 4 + 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;
}
.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);
}
/* 选中态：主色系「毛玻璃片」，而不再是 --primary-soft 实色底。
   ⚠️ 玻璃感由「半透明主色底 + 顶部高光 + 主色内描边」承担，
   backdrop-filter 只作增强：dock 自身已是 backdrop root，
   Safari 对嵌套 backdrop-filter 处理不一致，缺失时退化为半透明片，
   视觉仍是玻璃而非实色 —— 所以底不能写回不透明色。 */
.dock-btn.active {
  background: rgba(21, 75, 142, 0.16);
  -webkit-backdrop-filter: blur(14px) saturate(180%);
  backdrop-filter: blur(14px) saturate(180%);
  color: var(--primary);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.85),
    inset 0 0 0 1px rgba(21, 75, 142, 0.18),
    0 2px 8px rgba(21, 75, 142, 0.12);
}
/* 收藏数量徽标：贴右上角，白描边与胶囊底分离 */
.dock-count {
  position: absolute;
  top: 4px;
  right: 4px;
  min-width: 17px;
  height: 17px;
  padding: 0 4px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 999px;
  border: 2px solid #fff;
  background: var(--primary);
  color: #fff;
  font-size: 10px;
  font-weight: 700;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
/* .dock-count 是 flex 容器，需显式覆盖 UA 的 [hidden] */
.dock-count[hidden] {
  display: none;
}

/* 展开态：输入框铺满胶囊 */
.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(21, 75, 142, 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%;
  /* v1.3.3：本行只有它一个子元素；占满整行后右轨才拿得到剩余宽度 */
  flex: 1 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;
  max-width: 100%;
  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 {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  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(21, 75, 142, 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;
  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;
}

/* =========================================================
   标签栏：钉左 + 右轨横滚（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;
}
/* 轨道胶囊：只在 .cat-rail 作用域内覆盖共享的 .chip（后台筛选栏不受影响） */
.cat-rail .chip {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  white-space: nowrap;
}
.cat-rail .chip:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 1px;
}

/* 两端渐隐：由 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;
}
/* 「个人颜色」卡片的小标记：在「全部」里跟普通色卡混排时，一眼认出是自己取的色。
   只是个小点（半透明深底 + 白环），不抢色块本身的颜色。 */
.card-mine .swatch::after {
  content: '';
  position: absolute;
  top: 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);
}

/* =========================================================
   分页器：与标签栏同源的胶囊风格
   ========================================================= */
.pager {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 28px;
  padding-bottom: 6px;
}
.pager[hidden] {
  display: none;
}
.pager-btn {
  min-width: 36px;
  height: 36px;
  padding: 0 11px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 999px;
  background: #fff;
  border: 1px solid var(--line);
  color: var(--text);
  font-size: 13px;
  font-weight: 500;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  transition: all 0.15s ease;
}
.pager-btn:hover:not(:disabled):not(.active) {
  border-color: #c5cad6;
}
.pager-btn:active:not(:disabled) {
  transform: translateY(1px);
}
.pager-btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}
.pager-btn.active {
  background: var(--primary-soft);
  border-color: var(--primary);
  color: var(--primary);
  font-weight: 600;
  box-shadow: var(--shadow-sm);
}
.pager-btn svg {
  width: 16px;
  height: 16px;
}
.pager-ellipsis {
  min-width: 22px;
  height: 36px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--text-faint);
  font-size: 13px;
  user-select: none;
}

@media (max-width: 640px) {
  .pager {
    gap: 5px;
    margin-top: 22px;
  }
  .pager-btn {
    min-width: 32px;
    height: 32px;
    padding: 0 9px;
    font-size: 12px;
  }
  .pager-ellipsis {
    min-width: 18px;
    height: 32px;
  }
}

/* =========================================================
   弹层：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;
}


/* ===== 「个人颜色」虚拟标签（标签管理页里单独一块，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);
}
/* 色号：叠加在色块底部，不占额外纵向空间。
   v2.0.4：改为「毛玻璃 + 白底 + 黑字」（原为深色毛玻璃 + 白字）。
   ⚠️ 白底在**浅色/白色色块**上会和背景融成一片 → box-shadow 那两层
      （1px 深色描边环 + 下方投影）是**把一个胶囊"立"起来**的唯一手段，
      不是装饰。删掉它们，浅色色块上的色号就等于看不见。 */
.similar-code {
  position: absolute;
  left: 50%;
  bottom: 5px;
  transform: translateX(-50%);
  max-width: calc(100% - 8px);
  padding: 2px 7px;
  border-radius: 999px;
  font-size: 10px;
  font-weight: 600;
  line-height: 1.4;
  color: #1d1d1f;
  background: rgba(255, 255, 255, 0.72);
  backdrop-filter: blur(8px) saturate(160%);
  -webkit-backdrop-filter: blur(8px) saturate(160%);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.18);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  pointer-events: none;
}
.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 {
    bottom: 4px;
    font-size: 10px;
    padding: 1px 6px;
  }
}
/* 移动端：灯箱近似全屏，单列纵向排列，主色区弹性 + 六色固定同屏 */
@media (max-width: 640px) {
  .lightbox-mask {
    padding: 0;
    align-items: stretch;
  }
  .lightbox {
    max-width: 100%;
    height: 100vh;
    height: 100dvh;
    max-height: 100vh;
    max-height: 100dvh;
    border-radius: 0;
    animation: none;
  }
  .lightbox-head {
    padding: 12px 14px;
    background: #fff;
  }
  .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 {
    bottom: 4px;
    font-size: 10px;
    padding: 1px 6px;
  }
}
/* 短屏（iPhone SE 等 ≤700px 高）：进一步压缩相似色模块，主色区优先 */
@media (max-width: 640px) and (max-height: 700px) {
  .lightbox-head {
    padding: 10px 12px;
  }
  .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 {
    bottom: 3px;
    font-size: 9px;
    padding: 1px 5px;
  }
}

.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);
}

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

.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='recolor'] {
  order: 3;
  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: 4;
  --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: 5;
  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: 6;
  --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");
}
.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'] {
    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;
  }
}

/* ---- 步骤条（第 2 / 3 步显示；第 1 步 wrap 为空 → :empty 收起）---- */
.picker-steps-wrap {
  flex: 0 0 auto;
  padding: 0 18px;
}
.picker-steps-wrap:empty {
  display: none;
}
.picker-steps {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 0;
  border-bottom: 1px solid rgba(0, 0, 0, 0.06);
}
.picker-step {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  font-weight: 500;
  color: var(--text-faint);
  white-space: nowrap;
  transition: color 0.15s ease;
}
.picker-step-n {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: var(--bg-soft);
  color: var(--text-muted);
  font-size: 11px;
  font-weight: 700;
  flex: 0 0 auto;
  transition: background 0.15s ease, color 0.15s ease;
}
.picker-step.active {
  color: var(--text);
  font-weight: 600;
}
.picker-step.active .picker-step-n {
  background: var(--primary);
  color: #fff;
}
.picker-step.done .picker-step-n {
  background: var(--primary-soft);
  color: var(--primary);
}
/* 步骤之间的连线（渲染时插在相邻两步之间，自动吃掉剩余宽度） */
.picker-step-line {
  flex: 1 1 auto;
  min-width: 8px;
  height: 1px;
  background: var(--line);
}

/* ---- 第 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);
}
.picker-drop-privacy {
  margin-top: 4px;
  max-width: 360px;
  font-size: 11px;
  line-height: 1.55;
  color: var(--text-faint);
}

/* ---- 第 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(21, 75, 142, 0.15);
}
/* 状态行：固定最小高度，避免「空 → 有文案」时整块表单跳动 */
.picker-status {
  min-height: 18px;
  margin-top: 8px;
  font-size: 12px;
  line-height: 1.5;
  color: var(--text-muted);
}
.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;
}

/* 窄屏：步骤条缩一档，保证三个步骤名 + 连线同屏放得下 */
@media (max-width: 400px) {
  .picker-step {
    font-size: 11px;
    gap: 4px;
  }
  .picker-step-n {
    width: 18px;
    height: 18px;
    font-size: 10px;
  }
  .picker-steps {
    gap: 6px;
  }
}

/* ---- 小屏：与改色灯箱完全同款的「满屏平铺」----
   ⚠️ 这段必须写在 `.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(21,75,142,.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; }
.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; }
