/* ==========================================================================
   拼豆图纸生成器 · 样式
   --------------------------------------------------------------------------
   设计方向：「图纸台」

   拼豆图纸的本质是一张**工程图纸** —— 格子、编号、色号，照着它一格一格地拼。
   所以这个界面不该长得像后台管理系统，该长得像一张工作台：
   深墨的字、纸的底、等宽的数字，和唯一一种彩色 —— 豆子自己的颜色。

   三条自我约束：
   1. 所有数字（格子数、豆数、色号、尺寸）一律等宽 + tabular-nums。
      它们是这个工具真正的信息，不该和正文用同一套字形。
   2. 主色 #DD422F 取自真实 MARD 豆色 F13（朱砂红），不是随手调的橙。
      见 :root 里的注释。
   3. 动效只做「解释状态变化」，不做装饰。全部尊重 prefers-reduced-motion。

   --------------------------------------------------------------------------
   ⚠️ 本文件是**主应用（index.html）的组件库**，不是通用 UI 框架。
   里面有大量通用类名：.step / .stage / .field / .legend-item / .legend-list …

   独立页面（texture-lab.html / 以后的落地页）复用它时：
   - ✅ 可以放心用：设计令牌（:root 变量）、reset、基础排版、body 底色
   - ⛔ 不要复用组件类名 —— 必须给本页所有类名加页面前缀
     （texture-lab → lab-*）
   - 另外：应用外壳的 `body{height:100vh;overflow:hidden}` 已收进
     `body.app-shell`（见桌面端断点），所以独立页面不会被锁成一屏。

   2026-10-04 事故：付费体验页（paid.html，2026-10-07 已删）初版用了 .step 做
   自己的步骤区块，而本文件的 .step（第 ~548 行）是应用顶部的步骤条，带
   `display:inline-flex; align-items:center`。结果三个 <section> 被变成
   同一行的行内元素、互相叠压 —— 整页高度只剩 372px，照片卡被压到 112px 宽，
   Playwright 点不中，报「被 .ctrls 拦截指针事件」。
   同页还撞了 .field（多出 margin-top）、.stage（min-width:0）、
   .legend-item / .legend-list。
   ========================================================================== */

/* ==========================================================================
   1. 设计令牌
   ========================================================================== */

:root {
  /* --- 色：纸与墨 ---------------------------------------------------------
     底色偏暖（#FAF7F2 而非 #F7F7F7）。冷灰是 SaaS 的默认脸，
     暖纸是手工作坊的脸 —— 而且和暖色豆子放一起不打架。 */
  --paper:        #faf7f2;
  --paper-deep:   #f2ede4;
  --surface:      #ffffff;
  --surface-2:    #fbf9f6;
  --surface-sunk: #f4f0e9;

  --ink:          #16130e;
  --ink-2:        #3e372e;
  --ink-3:        #6b6156;
  --ink-4:        #9a8d7e;

  --line:         #e7e0d5;
  --line-2:       #d6ccbd;
  --line-3:       #bdb1a0;

  /* --- 色：主色 -----------------------------------------------------------
     朱砂红 = MARD 色卡 F13，rgb(221, 66, 47)。
     选它的理由：① 白底上对比度 4.29:1，做填充色够用；
     ② 色相落在 15° 附近，和暖纸底同族，不突兀；
     ③ 它是**真实存在的一种豆子**，用户买得到 —— 整个界面的主色
        可以真的拿在手里。这一点是别的产品抄不走的。 */
  --accent:       #dd422f;
  --accent-deep:  #b32e1d;   /* 白底上的文字用这一档（对比度 6.1:1，过 AA） */
  --accent-wash:  #fdeee9;
  --accent-line:  #f3c9be;

  /* --- 色：语义 ----------------------------------------------------------- */
  --ok:           #1f6b45;
  --ok-wash:      #e8f3ed;
  --ok-line:      #bcd9c9;
  --warn:         #8a5a10;
  --warn-wash:    #fdf2e0;
  --warn-line:    #ecd6ab;
  /* 「认不出」用的红。刻意不复用 --accent —— 主色是品牌色，
     混进语义会让人以为「红色 = 品牌」，以后改主色会连累这里。 */
  --bad:          #a33224;
  --bad-wash:     #fdecea;
  --bad-line:     #f0c4bd;

  /* --- 字体 --------------------------------------------------------------- */
  --font: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Source Han Sans SC",
          system-ui, -apple-system, "Segoe UI", sans-serif;
  --mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas,
          "Liberation Mono", "Courier New", monospace;

  /* --- 字阶 ---------------------------------------------------------------
     原来是 11/12.5/13/14/16 五个挤在一起的尺寸，等于没有层级。
     现在拉开：micro 给标签，base 给正文，2xl 给标题，数字另有 monospace。 */
  --fs-micro: 11px;
  --fs-xs:    12px;
  --fs-sm:    13px;
  --fs-base:  14px;
  --fs-md:    15px;
  --fs-lg:    17px;
  --fs-xl:    21px;
  --fs-2xl:   28px;

  --lh-tight: 1.25;
  --lh-snug:  1.45;
  --lh-base:  1.6;

  --tr-tight: -0.022em;   /* 大字号收紧字距，中文也吃这一套 */
  --tr-normal: 0;
  --tr-wide:  0.06em;     /* 小号全大写标签用 */

  /* --- 间距 --------------------------------------------------------------- */
  --s-1: 2px;   --s-2: 4px;   --s-3: 6px;   --s-4: 8px;
  --s-5: 10px;  --s-6: 12px;  --s-7: 14px;  --s-8: 16px;
  --s-9: 20px;  --s-10: 24px; --s-11: 32px; --s-12: 40px;

  /* --- 圆角 --------------------------------------------------------------- */
  --r-xs: 4px;
  --r-sm: 7px;
  --r-md: 11px;
  --r-lg: 16px;
  --r-full: 999px;

  /* --- 投影：全部带暖调，纯黑阴影在暖底上会发脏 --------------------------- */
  --sh-1: 0 1px 2px rgba(60, 45, 28, 0.05);
  --sh-2: 0 2px 6px -1px rgba(60, 45, 28, 0.07), 0 1px 2px rgba(60, 45, 28, 0.04);
  --sh-3: 0 8px 24px -6px rgba(60, 45, 28, 0.12), 0 2px 6px -2px rgba(60, 45, 28, 0.06);
  --sh-4: 0 24px 60px -12px rgba(40, 28, 16, 0.30), 0 4px 12px -4px rgba(40, 28, 16, 0.14);

  --sh-inset: inset 0 1px 2px rgba(60, 45, 28, 0.06);
  --sh-focus: 0 0 0 3px rgba(221, 66, 47, 0.22);

  /* --- 动效 ---------------------------------------------------------------
     三档就够：micro 给反馈，base 给状态切换，slow 给入场。
     缓动统一用 ease-out 家族 —— 界面里的东西应该是「被推出去」，不是「弹出来」。 */
  --dur-micro: 110ms;
  --dur-base:  190ms;
  --dur-slow:  300ms;
  --dur-enter: 380ms;

  --ease-out:  cubic-bezier(0.22, 0.61, 0.36, 1);
  --ease-inout: cubic-bezier(0.55, 0.06, 0.36, 1);
  --ease-spring: cubic-bezier(0.34, 1.4, 0.64, 1);

  /* --- 图纸台底纹 ---------------------------------------------------------
     两级网格 = 坐标纸。细格 16px 给「格子」的质感，粗格 80px 给方向感 ——
     真图纸就是这么画的。两条线性渐变叠出来，比 SVG 便宜，缩放也不会糊。 */
  --grid-line:       rgba(120, 100, 72, 0.085);
  --grid-line-major: rgba(120, 100, 72, 0.155);
  --grid-size: 16px;
  --grid-major: 80px;

  color-scheme: light;
}

/* --- 暗色主题：夜里的工作台 ------------------------------------------------
   不是把亮色反相。木料和纸在灯下变暗，是**变暖变深**，不是变蓝。
   所以暗色底是暖黑（#12100d）而不是 #0f1115 那种冷灰。
   主色也提亮一档 —— 朱砂在暗底上要往上走，否则糊成一团。 */
[data-theme="dark"] {
  --paper:        #12100d;
  --paper-deep:   #0b0906;
  --surface:      #1c1915;
  --surface-2:    #232019;
  --surface-sunk: #0e0c09;

  --ink:          #f3eee6;
  --ink-2:        #cbc2b5;
  --ink-3:        #978c7d;
  --ink-4:        #6d6455;

  --line:         #2e2820;
  --line-2:       #413931;
  --line-3:       #574c40;

  --accent:       #ef5a42;
  --accent-deep:  #ff7d66;
  --accent-wash:  #2b1a15;
  --accent-line:  #5c2c22;

  --ok:           #5fbe8c;
  --ok-wash:      #14231b;
  --ok-line:      #2c4a3a;
  --warn:         #dfa94a;
  --warn-wash:    #251c0e;
  --warn-line:    #4d3a18;
  --bad:          #f0836c;
  --bad-wash:     #2c1614;
  --bad-line:     #5e2b24;

  --sh-1: 0 1px 2px rgba(0, 0, 0, 0.5);
  --sh-2: 0 2px 6px -1px rgba(0, 0, 0, 0.55), 0 1px 2px rgba(0, 0, 0, 0.4);
  --sh-3: 0 8px 24px -6px rgba(0, 0, 0, 0.6), 0 2px 6px -2px rgba(0, 0, 0, 0.45);
  --sh-4: 0 24px 60px -12px rgba(0, 0, 0, 0.75), 0 4px 12px -4px rgba(0, 0, 0, 0.5);

  --sh-inset: inset 0 1px 2px rgba(0, 0, 0, 0.45);
  --sh-focus: 0 0 0 3px rgba(239, 90, 66, 0.3);

  --grid-line:       rgba(255, 235, 200, 0.055);
  --grid-line-major: rgba(255, 235, 200, 0.11);

  color-scheme: dark;
}

/* ==========================================================================
   2. 基础
   ========================================================================== */

* { box-sizing: border-box; }

/* 必须放在最前面：.empty-state 等用了 display:flex，
   会盖掉 [hidden] 自带的 display:none，导致「隐藏了但还在显示」 */
[hidden] { display: none !important; }

html, body { margin: 0; padding: 0; }

body {
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font);
  font-size: var(--fs-base);
  line-height: var(--lh-base);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

/* 主题切换要平滑，但不能让整页动画卡住 —— 只过渡颜色 */
body, .card, .topbar, .canvas-scroll, .print-area {
  transition: background-color var(--dur-base) var(--ease-out),
              border-color     var(--dur-base) var(--ease-out);
}

h1, h2, h3 {
  margin: 0;
  font-weight: 650;
  letter-spacing: var(--tr-tight);
  line-height: var(--lh-tight);
}

p { margin: 0; }

/* 所有数字用等宽 —— 这是这个工具的信息主体 */
.num,
.plan-summary,
.print-meta,
.legend-idx,
.legend-code,
.legend-count,
.ps-count,
.ps-code,
.ps-n,
.crop-info,
.zoom-level,
.status-text,
.range-head output,
.tb-v {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1, "zero" 1;
}

::selection { background: var(--accent-wash); color: var(--accent-deep); }

/* 输入光标也是界面的一部分。默认是纯黑，在暖纸底上是一根不属于这套配色的线；
   跟随主色之后，连「正在输入」这件事都在同一个视觉世界里（2026-10-22）。 */
:root { caret-color: var(--accent); }

/* 全局焦点环。用 :focus-visible 而不是 :focus —— 鼠标点击不该出现光环，
   键盘导航必须有。这一条是可用性评分里最容易拿分也最容易丢分的地方。 */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--r-xs);
}
:focus:not(:focus-visible) { outline: none; }

/* 自定义滚动条：默认那条灰色滚动条会在暖色版面里格外刺眼 */
* { scrollbar-width: thin; scrollbar-color: var(--line-2) transparent; }
*::-webkit-scrollbar { width: 11px; height: 11px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb {
  background: var(--line-2);
  border: 3px solid transparent;
  border-radius: var(--r-full);
  background-clip: content-box;
}
*::-webkit-scrollbar-thumb:hover { background: var(--line-3); background-clip: content-box; }
*::-webkit-scrollbar-corner { background: transparent; }

/* ==========================================================================
   3. 顶栏
   ========================================================================== */

.topbar {
  position: sticky;
  top: 0;
  z-index: 20;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-8);
  padding: var(--s-6) var(--s-10);
  background: color-mix(in srgb, var(--paper) 88%, transparent);
  backdrop-filter: blur(12px) saturate(1.4);
  -webkit-backdrop-filter: blur(12px) saturate(1.4);
  border-bottom: 1px solid var(--line);
}

.brand { display: flex; align-items: center; gap: var(--s-6); min-width: 0; }
a.brand { color: inherit; text-decoration: none; }

/* 品牌标：2×2 颗豆。右下那颗是朱砂 —— 差一颗就拼完的那颗。
   每颗豆带一点顶部高光，看起来像实物而不是色块。 */
/* ★ 品牌标（2026-10-24 v2）：改用 AI 生成的拼豆图（web/logo-128.png，透明底）。
   和 favicon.svg 同一套图形：四颗拼豆，3 奶油白 + 1 珊瑚红，中间有孔。
   ⚠️ 尺寸固定 36px（原 34 + 1px 边框 ×2）—— 改小会让顶栏变矮，
      预览画布尺寸跟着变，e2e「图纸完全复原」会失败（2026-10-24 踩过）。 */
.brand-mark {
  width: 36px;
  height: 36px;
  flex: none;
  display: block;
  object-fit: contain;
}

.brand-text { min-width: 0; }

.topbar h1 {
  font-size: var(--fs-md);
  font-weight: 680;
  letter-spacing: var(--tr-tight);
  /* ⚠️ 中文没有词边界 —— 少了这一行，窄屏上「拼豆图纸生成器」会被逐字竖排 */
  white-space: nowrap;
}

.tagline {
  /* 2026-10-24：改成品牌 slogan 后加深一档 —— 原来 --ink-3 太浅，几乎看不见 */
  margin-top: 2px;
  font-size: 12.5px;
  letter-spacing: .01em;
  color: var(--ink-2);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.topbar-actions {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  flex: none;
}

/* 状态胶囊：像一个仪表读数 */
.status-text {
  display: inline-flex;
  align-items: center;
  gap: var(--s-3);
  padding: 5px var(--s-6);
  border: 1px solid var(--line);
  border-radius: var(--r-full);
  background: var(--surface);
  font-family: var(--mono);
  /* ★ 2026-10-13：11 → 12px、--ink-3 → --ink-2、加粗。
     这是**唯一的全局反馈通道**（等待上传 / 已生成 / 导出成功 / 各种失败），
     原来却是全页最小的字、和主题切换按钮挤在同一行里，扫不到。 */
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--ink-2);
  letter-spacing: 0.01em;
  white-space: nowrap;
  transition: color var(--dur-base) var(--ease-out),
              border-color var(--dur-base) var(--ease-out);
}

/* 文案单独成一项：胶囊可以收窄，文字用省略号收尾而不是硬裁 */
.status-label { min-width: 0; }

.status-text::before {
  content: "";
  /* 5 → 7px：状态点要能一眼看出「现在是哪种状态」 */
  width: 7px;
  height: 7px;
  flex: none;
  border-radius: 50%;
  background: var(--line-3);
  transition: background var(--dur-base) var(--ease-out),
              box-shadow var(--dur-base) var(--ease-out);
}

/* 有图纸了，指示灯亮起 */
.status-text.is-live { color: var(--ink-2); border-color: var(--accent-line); }
.status-text.is-live::before {
  background: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-wash);
}

.icon-btn {
  width: 34px;
  height: 34px;
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--ink-3);
  cursor: pointer;
  transition: color var(--dur-micro) var(--ease-out),
              border-color var(--dur-micro) var(--ease-out),
              background var(--dur-micro) var(--ease-out),
              transform var(--dur-micro) var(--ease-out);
}

/* ⚠️ 文字/图标色用 --accent-deep（6.1:1），不用 --accent —— 后者压在
   --accent-wash 上只有 3.8:1，检测器会报 low-contrast。 */
.icon-btn:hover { color: var(--accent-deep); border-color: var(--accent-line); background: var(--accent-wash); }
.icon-btn:active { transform: scale(0.94); }
.icon-btn svg { width: 17px; height: 17px; display: block; }

/* 亮色下显示月亮（点了变暗），暗色下显示太阳 */
.theme-icon-sun { display: none; }
[data-theme="dark"] .theme-icon-sun { display: block; }
[data-theme="dark"] .theme-icon-moon { display: none; }

/* ==========================================================================
   4. 布局外壳
   ========================================================================== */

.layout {
  display: grid;
  /* ⚠️ 2026-10-13：固定 336px 改自适应。
     336px 在 1600px 视口下只占 21%，中文标签被挤折行
     （「人物 / 宠物（头像特写）」折成三行、还断在「特 / 写」中间）。
     clamp 让它在宽屏上长到 400px；超宽屏封顶，避免左栏被拉得空荡。 */
  grid-template-columns: clamp(336px, 25vw, 400px) minmax(0, 1fr);
  gap: var(--s-10);
  align-items: start;
  max-width: 1680px;
  margin: 0 auto;
  padding: var(--s-9) var(--s-10) var(--s-12);
}

@media (max-width: 1000px) {
  .layout { grid-template-columns: 1fr; }
}

/* ★ 2026-10-16 浩哥：空状态下右栏底部留一大片空白 → 「不协调、失重」。
   原因：左栏（3 张卡片 + 打印/导出按钮 + 底部说明）比右栏空状态框
   （`min-height: 520px`）高，差值全部堆在右栏底部，视觉重心偏左上。

   改法：**只在空状态时**让两栏等高、空状态框撑满剩余高度。
   用 `:has()` 限定 —— 图纸生成后右栏高度仍由内容决定，
   不会被左栏那张展开后约 1900px 高的付费面板拉长。 */
.layout:has(#emptyState:not([hidden])) { align-items: stretch; }
.layout:has(#emptyState:not([hidden])) .stage {
  display: flex;
  flex-direction: column;
}
.layout:has(#emptyState:not([hidden])) .empty-state { flex: 1; }

.panel {
  display: flex;
  flex-direction: column;
  gap: 0;
  min-height: 0;
}

/* 卡片滚动区。
 * 桌面端由媒体查询接管成 overflow-y:auto —— 打印按钮因此常驻，不用翻滚去找。
 * 手机上它只是普通容器，整页照常滚动。 */
.panel-scroll {
  display: flex;
  flex-direction: column;
  gap: var(--s-5);
  min-height: 0;
}

/* ⚠️ 2026-10-13：卡片**不许被压缩**。
   .panel-scroll 是 flex 列容器，卡片默认 flex-shrink:1 —— 内容总高超过容器时，
   flex 会把每张卡片按比例压扁，卡片里的内容就「漏」出卡片边界。
   实测：AI 通道下卡片 1 变高，「用 AI 生成像素图」按钮被压到卡片外面，
   只露出半行「先输」，看起来像被裁掉 —— 其实是 flex 压缩的锅。
   改成不收缩，超出部分交给滚动区正常滚动。 */
.panel-scroll > .card { flex: 0 0 auto; }

/* 左栏「下面还有内容」的提示（2026-10-13）——
   见 app.js 的 syncPanelScrollHint()。只在「确实有内容被切」且「还没滚到底」时出现；
   滚到底立刻撤掉，否则会遮住最后一张卡片的底边。 */
.panel-scroll.has-overflow:not(.at-bottom) {
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 52px), transparent);
  mask-image: linear-gradient(to bottom, #000 calc(100% - 52px), transparent);
}

/* 常驻底栏：打印按钮 + 隐私/色差提示。
 * 桌面端页面 footer 被隐藏（见第 16 节），文案挪到这里常驻 ——
 * 左栏怎么滚，按钮和提示都钉在底部。 */
.panel-foot {
  flex: none;
  display: flex;
  flex-direction: column;
  gap: var(--s-5);
  padding-top: var(--s-6);
  /* 分隔线要看得见 —— 它是「这里是常驻区、上面还能滚」的唯一提示。
     --line 太淡，在暖纸底上几乎消失。 */
  border-top: 1px solid var(--line-2);
  background: var(--paper);
}

/* 只给桌面端看；移动端用页面底部的 footer，这里不重复 */
.panel-note {
  display: none;
  margin: 0;
  /* ★ 2026-10-13：11 → 12px、--ink-3 → --ink-2。
     这行是「图片不会上传到任何服务器」—— 整个产品最重要的信任承诺，
     原来却是全页最小的字、最浅的颜色。它应该被看见。 */
  font-size: var(--fs-xs);
  color: var(--ink-2);
  line-height: var(--lh-snug);
}

/* ============================ 桌面端：应用外壳 ============================
 *
 * 为什么不让整页滚动（2026-10-03 浩哥反馈「向下滚动看不到右侧预览」）：
 *   实测左栏 6 张卡片高 1613px，右栏只有 1130px，视口 820px —— 左栏明显更高。
 *   整页滚动时，滚到左栏下半部分（颜色数 / 图纸显示 / 打印按钮），
 *   右侧预览早就被顶出屏幕了，等于**闭着眼睛调参**。
 *
 * 所以桌面端改成「外壳固定 + 两栏各自滚动」：左栏怎么滚，右侧预览都纹丝不动。
 *
 * 顺带删掉了 .panel 上原来的 position:sticky —— 它是死代码：
 *   左栏比自己那一格还高，格子高度 = 左栏高度，没有可移动空间，
 *   实测滚到底时 top 是 -906px 而不是 76px，等于没生效。
 *
 * 只有桌面端这么做。手机上单列，整页滚动才是对的（见下面的 640px 断点）。
 */
@media (min-width: 1001px) {
  /* ⚠️ 只作用于主应用（index.html 的 <body class="app-shell">）。
   *
   * 之前这里写的是裸 `body`，结果是：任何加载 style.css 的独立页面
   * （texture-lab.html / 以后的落地页）在 >1000px 视口下
   * 都会被当成应用外壳 —— body 被锁成 100vh + overflow:hidden + flex 列，
   * 整页无法滚动、内容被压扁成一条。
   * 2026-10-04 实测：付费体验页（paid.html，2026-10-07 已删）的 .pd 只剩 372px 高、
   * #photoPicks 被压到 112px 宽，点都点不中（Playwright 报「被 .ctrls 拦截指针事件」）。
   *
   * 独立页面没有 .topbar/.layout，本来就不该走外壳布局。 */
  body.app-shell {
    height: 100vh;
    overflow: hidden;
    display: flex;
    flex-direction: column;
  }

  .topbar { position: static; flex: none; }

  .layout {
    flex: 1;
    min-height: 0;
    width: 100%;
    align-items: stretch;   /* 两栏都撑满可用高度，才能各自滚动 */
  }

  /* 滚动发生在「卡片区」而不是整个左栏 —— 打印按钮才能常驻底部。
   * 之前整个 .panel 滚动，按钮作为最后一个子元素被顶在 1613px 处，
   * 必须滚到底才看得见（2026-10-03 浩哥反馈）。 */
  .panel-scroll,
  .stage {
    min-height: 0;
    overflow-y: auto;
    overflow-x: hidden;
    scrollbar-gutter: stable;   /* 预留滚动条宽度，出现/消失时版面不跳 */
  }

  .panel-scroll {
    flex: 1;
    padding-bottom: var(--s-5);   /* 滚到底时最后一张卡片不贴住底栏 */
  }

  /* 让底栏也留出和滚动区一样的滚动条槽位。
   * 否则卡片宽 336-滚动条、按钮宽 336，右边缘差一条滚动条 —— 肉眼可见的不齐。
   * 内容不会溢出，所以这层 auto 不会真的长出滚动条。 */
  .panel-foot {
    overflow-x: hidden;
    overflow-y: auto;
    scrollbar-gutter: stable;
  }

  .panel-note { display: block; }

  /* 桌面端不再需要横跨整页的页脚：文案已挪进左栏常驻底栏，
   * 省下的这一条高度（原 padding-bottom 32px）还给预览区。 */
  .footer { display: none; }
}

/* ==========================================================================
   5. 卡片与步骤
   ========================================================================== */

.card {
  position: relative;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: var(--s-7);
  box-shadow: var(--sh-1);
  transition: border-color var(--dur-base) var(--ease-out),
              box-shadow   var(--dur-base) var(--ease-out);
}

.card:hover { border-color: var(--line-2); }

/* 步骤号：方形的等宽数字，不是圆点。
   圆形徽章是消费级 App 的语汇，方形编号是图纸的语汇（图框编号）。 */
.card-title {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  margin-bottom: var(--s-5);
  font-size: var(--fs-base);
  font-weight: 650;
  letter-spacing: var(--tr-tight);
}

.step {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  flex: none;
  border-radius: var(--r-xs);
  background: var(--accent-wash);
  border: 1px solid var(--accent-line);
  color: var(--accent-deep);
  font-family: var(--mono);
  font-size: var(--fs-micro);
  font-weight: 700;
  line-height: 1;
}

/* 已完成的步骤：号变成实心 —— 给用户一个「走到哪了」的读数 */
.card.is-done .step {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}

/* 尚待输入的区域保留边框状态；不显示强制步骤徽标。 */
.card.is-active {
  border-color: var(--accent-line);
  box-shadow: 0 0 0 1px var(--accent-line), var(--sh-2);
}



/* `.card.is-optional`（「可选」灰徽标）已移除（2026-10-19）——
   它唯一的用途是标「裁剪可以跳过」，而裁剪已并进第 1 张卡片，不再是独立一步。 */

.card-hint {
  margin: calc(var(--s-2) * -1) 0 var(--s-5);
  font-size: var(--fs-xs);
  color: var(--ink-3);
  line-height: var(--lh-snug);
}

/* ==========================================================================
   6. 上传区
   ========================================================================== */

.dropzone {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-10) var(--s-8);
  border: 1.5px dashed var(--line-2);
  border-radius: var(--r-sm);
  background: var(--surface-2);
  cursor: pointer;
  text-align: center;
  transition: border-color var(--dur-base) var(--ease-out),
              background    var(--dur-base) var(--ease-out),
              transform     var(--dur-micro) var(--ease-out);
}

.dropzone:hover,
.dropzone:focus-visible {
  border-color: var(--accent);
  background: var(--accent-wash);
  outline: none;
}

.dropzone.is-over {
  border-color: var(--accent);
  border-style: solid;
  background: var(--accent-wash);
  transform: scale(0.988);
}

.dz-icon {
  width: 26px;
  height: 26px;
  color: var(--accent);
  margin-bottom: var(--s-2);
  transition: transform var(--dur-base) var(--ease-spring);
}

.dropzone:hover .dz-icon { transform: translateY(-2px); }

.dz-main { font-size: var(--fs-sm); font-weight: 600; color: var(--ink); }
/* ★ 2026-10-13：11 → 12px、--ink-3 → --ink-2。
   上传区副标题里写着「图片不会上传到任何服务器」—— 免费通道的核心承诺，
   不该比旁边的操作提示还弱。 */
.dz-sub  { font-size: var(--fs-xs); color: var(--ink-2); line-height: var(--lh-snug); }

kbd {
  display: inline-block;
  padding: 1px 5px;
  border: 1px solid var(--line-2);
  border-bottom-width: 2px;
  border-radius: var(--r-xs);
  background: var(--surface);
  font-family: var(--mono);
  font-size: 10.5px;
  color: var(--ink-2);
  line-height: 1.5;
}

/* 缩略图 */

.thumb-row {
  display: flex;
  gap: var(--s-6);
  align-items: center;
  margin-top: var(--s-6);
  padding-top: var(--s-6);
  border-top: 1px solid var(--line);
}

/* ★★ 2026-10-23 浩哥：「悬停在那张图时，需要出现放大按钮和阴影，
   让用户知道点一下就能放大。」→ 缩略图变成一个「可点的图」，不是一张静态图。 */
.thumb-wrap {
  position: relative;
  flex: none;
  width: 60px;
  height: 60px;
  border-radius: var(--r-sm);
}

.thumb {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
  border-radius: var(--r-sm);
  border: 1px solid var(--line);
  background: var(--surface-sunk);
  box-shadow: var(--sh-1);
  /* 阴影跟着 hover 长出来 —— 这是「浮起来可以点」的最便宜信号 */
  transition: box-shadow .16s var(--ease-out), transform .16s var(--ease-out);
}

.thumb-zoom {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  padding: 0;
  border: 0;
  border-radius: var(--r-sm);
  background: rgba(20, 16, 10, 0.44);
  color: #fff;
  opacity: 0;
  cursor: zoom-in;
  transition: opacity .16s var(--ease-out);
}
.thumb-zoom svg { width: 22px; height: 22px; }

.thumb-wrap:hover .thumb-zoom,
.thumb-zoom:focus-visible { opacity: 1; }
.thumb-wrap:hover .thumb {
  box-shadow: var(--sh-3);
  transform: translateY(-1px);
}
/* 触屏没有 hover —— 手机上一直显示这个提示，否则用户永远不知道能点 */
@media (hover: none) {
  .thumb-zoom { opacity: 1; background: rgba(20, 16, 10, 0.30); }
}

/* ---- 图片预览弹层 ---- */
.preview-modal {
  position: fixed;
  inset: 0;
  z-index: 70;
  display: grid;
  place-items: center;
  padding: var(--s-10);
  background: rgba(24, 19, 13, 0.84);
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
  animation: fade-in var(--dur-base) var(--ease-out);
}
.preview-modal[hidden] { display: none; }
/* 预览开着时锁住背后页面的滚动 */
body.preview-open { overflow: hidden; }

.preview-dialog {
  position: relative;
  display: grid;
  place-items: center;
  max-width: 100%;
  max-height: 100%;
}

.preview-dialog img {
  display: block;
  max-width: 100%;
  max-height: 86vh;
  border-radius: var(--r-md);
  background: var(--surface);
  box-shadow: 0 26px 70px rgba(0, 0, 0, 0.5);
}

.preview-close {
  position: absolute;
  top: -12px;
  right: -12px;
  width: 36px;
  height: 36px;
  border: 1px solid rgba(255, 255, 255, 0.28);
  background: rgba(20, 16, 10, 0.72);
  color: #fff;
  font-size: 22px;
  line-height: 1;
}
.preview-close:hover { background: rgba(20, 16, 10, 0.9); color: #fff; }

.thumb-meta { display: flex; flex-direction: column; gap: 1px; min-width: 0; }

.thumb-name {
  font-size: var(--fs-xs);
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.thumb-size {
  font-family: var(--mono);
  font-size: var(--fs-micro);
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}

.link-btn {
  align-self: flex-start;
  margin-top: var(--s-1);
  padding: 0;
  border: 0;
  background: none;
  color: var(--accent-deep);
  font-family: inherit;
  font-size: var(--fs-xs);
  font-weight: 600;
  cursor: pointer;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  transition: color var(--dur-micro) var(--ease-out);
}

.link-btn:hover { color: var(--accent-deep); }

/* --- 「裁剪 · 换一张图」并排（2026-10-19）---
   原来「裁剪」是第 2 张卡片，占一个步骤号，把「拼豆时长」挤到第 3 步。
   并进缩略图这一行之后：调这张图有两条路，都在这儿，不多占一步。 */
.thumb-actions {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  margin-top: var(--s-2);
}

/* ⚠️ `.link-btn` 默认是竖排里的按钮（align-self:flex-start + margin-top），
   放进横排必须归零，否则两个按钮会上下错开。
   ⚠️ 另外它原本只有 24×16 —— 鼠标勉强能点，手指基本点不中。
      这里先给鼠标撑到 28px，触屏在下面撑到 44px（WCAG 2.5.8）。 */
.thumb-actions .link-btn {
  display: inline-flex;
  align-items: center;
  align-self: center;
  margin-top: 0;
  min-height: 28px;
}

.thumb-sep { color: var(--line-3); font-size: var(--fs-xs); }

/* 只在**裁过**时出现（`hidden` 由 syncCropEntry 控制） */
.thumb-crop-state {
  margin-top: var(--s-1);
  font-size: var(--fs-micro);
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}

/* 触屏：「裁剪 / 换一张图」必须 ≥44px 才点得准。
   ⚠️ 只加高度和左右留白，**不改字号** —— 看起来还是文字链接，手感是按钮。 */
@media (max-width: 640px) {
  .thumb-actions { gap: var(--s-3); margin-top: 0; }
  .thumb-actions .link-btn { min-height: 44px; padding: 0 var(--s-2); }
}

/* --- 付费通道（图片来源选择 + 内置风格图示例）---
   浩哥 2026-10-05：「没有看到可以转图片风格的付费通道交互，是不是入口没放出来？」
   确实没放出来 —— 这一块就是入口。只在选中付费通道时才显示。 */

/* .chip-row 默认 3 列（时间档位用），通道只有 2 个，别留一格空的 */
.chan-row { grid-template-columns: repeat(2, 1fr); }

/* ⚠️ 2026-10-13：「画面重点」只有 2 个选项，却套了 .chip-row 的 3 列网格
   → 每个 chip 只拿到 1/3 宽，中文标签被挤成三行。改成 2 列。 */
#frameRow { grid-template-columns: repeat(2, 1fr); }

.paid-panel {
  margin-top: var(--s-6);
  padding: var(--s-7) var(--s-7) var(--s-8);
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
}

/* 引导句：**可执行的下一步指引**（看示例），不是分类标签 ——
   所以用正文尺寸（--fs-xs = 12px），不套 --fs-micro 的 11px 大写标签样式。
   移动端 mobile_test.py 的字号下限就是 12px，这里不能破例。 */
.paid-lead {
  margin: 0 0 var(--s-5);
  font-size: var(--fs-xs);
  color: var(--ink-3);
  font-weight: 600;
  line-height: var(--lh-base);
}
.paid-lead b { color: var(--ink-2); }

/* ---- 三段式前后对比（原照片 → AI 风格图 → 拼豆图纸） ----
 *
 * ⚠️ 缩略图**只负责讲清流程**，不负责「看效果」。
 *    左栏固定 336px，卡片 + 面板内边距扣完只剩 280px —— 三段平铺每段 ~76px。
 *    在这个尺寸上判断「做得好不好看」是不可能的，硬做大只会挤坏布局。
 *    真正的观感靠**点第 2 段载入右侧预览区**（那边 900px+，能一眼下判断）。
 *    —— 这是 2026-10-07 重做时定下的分工，不是偷懒。
 */
.sample-row {
  display: flex;
  flex-direction: column;
  gap: var(--s-6);
  margin-bottom: var(--s-7);
}

.pc-case {
  padding: var(--s-5);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--surface);
}

.pc-head {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  margin-bottom: var(--s-5);
}

.pc-name { font-size: var(--fs-xs); font-weight: 650; color: var(--ink-1); }

.pc-tag {
  padding: 1px var(--s-3);
  border-radius: var(--r-full);
  background: var(--accent-wash);
  color: var(--accent-deep);
  font-size: var(--fs-xs);
  font-weight: 650;
  line-height: 1.5;
}

.pc-preset {
  margin-left: auto;
  font-size: var(--fs-xs);
  font-weight: 650;
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}

.pc-flow {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  gap: var(--s-3);
  align-items: start;
  margin: 0;
  padding: 0;
  list-style: none;
}

.pc-stage {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s-3);
  min-width: 0;
}

/* 第 3 段（结果）跨满整行。理由见 app.js 的 buildSampleCase：
   珠子要 240px 以上才读得出，而并排时每段只有 ~76px。 */
.pc-big {
  grid-column: 1 / -1;
}
/* 「上排是输入、下排是结果」的方向提示 —— 原来靠第 2 个箭头，
   现在第 3 段换行了，改成一个朝下的箭头。 */
.pc-big::before {
  content: '↓';
  color: var(--ink-4);
  font-size: var(--fs-xs);
  line-height: 1;
}
/* 大图按自身比例显示，只受两个上限约束（宽不超面板、高不超 300px）。
   ⚠️ 不能用 .pc-thumb 默认的 aspect-ratio:1/1 + object-fit:cover ——
   人像那例是 29×37 的竖长图，会被裁掉下巴。 */
.pc-big .pc-thumb {
  width: auto;
  max-width: 100%;
  aspect-ratio: auto;
}
.pc-big .pc-thumb img {
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: 300px;
}
/* 切换按钮现在挂在大图下面（.pc-stage 是 column flex）——
   .pc-tex 自带的 margin-left:auto 是给并排布局用的，这里要取消，
   否则 align-items:center 会被它顶成右对齐。 */
.pc-big .pc-tex { margin-left: 0; }

.pc-thumb {
  display: block;
  width: 100%;
  aspect-ratio: 1 / 1;
  padding: 0;
  border: 1px solid var(--line-2);
  border-radius: var(--r-sm);
  background: var(--surface-sunk);
  overflow: hidden;
}

.pc-thumb img { display: block; width: 100%; height: 100%; object-fit: cover; }

/* 第 2 段是**唯一可点**的一格 —— 它是付费通道的输入，点了就载进预览。
   给一点点「可点」的暗示，但不要做成按钮那么重。 */
.pc-thumb.is-pick {
  appearance: none;
  cursor: pointer;
  transition: border-color var(--dur-micro) var(--ease-out),
              box-shadow   var(--dur-micro) var(--ease-out);
}
.pc-thumb.is-pick:hover {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px var(--accent-wash);
}

/* 标签固定两行高：「AI 风格图」会折行、「原照片」不会 ——
   不固定的话三格底边参差，箭头也对不齐。 */
.pc-lab {
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--ink-2);
  text-align: center;
  line-height: 1.25;
  min-height: calc(var(--fs-xs) * 1.25 * 2);
}

/* 箭头要对齐**缩略图的垂直中心**（不含下方标签）。
   原来靠写死 padding-top: 28px（那时缩略图固定 ~76px）—— 现在缩略图宽度
   随面板宽度变，写死就对不上了。改成：align-self:center 让它居中于整行，
   再用 margin-bottom 把居中点上移 (gap + 标签高) / 2 —— 数学上恰好落在
   缩略图的中线上，且对任何宽度都成立。 */
.pc-arrow {
  align-self: center;
  margin-bottom: calc(var(--fs-xs) * 1.25 * 2 + var(--s-3));
  color: var(--ink-4);
  font-size: var(--fs-xs);
  line-height: 1;
}

.pc-stat {
  margin: var(--s-5) 0 var(--s-3);
  font-size: var(--fs-xs);
  font-weight: 650;
  color: var(--ink-2);
  line-height: var(--lh-snug);
  font-variant-numeric: tabular-nums;
}

.pc-note {
  margin: 0 0 var(--s-3);
  font-size: var(--fs-xs);
  color: var(--ink-3);
  line-height: var(--lh-snug);
}

/* 「为什么是这个档位」—— 这是给用户的**决策依据**，不是脚注，
   所以用一条竖线把它从上面的参数里拎出来。 */
.pc-why {
  margin: 0 0 var(--s-5);
  padding-left: var(--s-4);
  border-left: 2px solid var(--accent-line);
  font-size: var(--fs-xs);
  color: var(--ink-3);
  line-height: var(--lh-snug);
}

.pc-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-4);
}

.pc-load { font-size: var(--fs-xs); }

/* 图纸 / 实物 切换：买的人想看的就这两样 ——
   照着拼的那张图，和拼出来长什么样。 */
.pc-tex {
  display: flex;
  margin-left: auto;
  border: 1px solid var(--line-2);
  border-radius: var(--r-full);
  overflow: hidden;
}

.pc-tex button {
  appearance: none;
  border: 0;
  background: transparent;
  padding: 4px var(--s-5);
  font: inherit;
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--ink-3);
  cursor: pointer;
  transition: background var(--dur-micro) var(--ease-out);
}

.pc-tex button:hover { background: var(--surface-sunk); }
.pc-tex button[aria-pressed="true"] { background: var(--accent); color: #fff; }
.pc-tex button[aria-pressed="true"]:hover { background: var(--accent-deep); }

/* 说明文字块：两句分开，读起来才不糊成一团 */
.paid-note {
  margin: 0 0 var(--s-5);
  font-size: var(--fs-xs);
  line-height: var(--lh-base);
  color: var(--ink-3);
}
.paid-note:last-child { margin-bottom: 0; }
.paid-note b { color: var(--ink-2); }
.paid-note .link-btn { font-size: var(--fs-xs); }

/* ==========================================================================
   7. 芯片（时间档位 / 裁剪比例）
   ========================================================================== */

.chip-row {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--s-3);
}

/* Q 版拼搭档位：主标签 + 时间范围，选中后副文案跟主色 */
.q-time-field { display: block; }
.q-time-label {
  display: block;
  margin-bottom: var(--s-2);
  font-size: 13px;
  font-weight: 600;
  color: var(--ink-2);
}
.q-time-row .chip {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: 10px 4px;
  line-height: 1.3;
  white-space: normal;
  text-align: center;
}
.q-time-row .chip small {
  font-size: 11px;
  font-weight: 400;
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}
.q-time-row .chip[aria-pressed="true"] small { color: var(--accent-deep); }
/* Q 版的滑块是读数不是控件：去掉 40 小时的刻度（Q 版封顶两小时） */
#timeSliderField.is-q-readout .range-scale { display: none; }
#timeSliderField.is-q-readout #timeSlider { cursor: default; }
#timeSliderField.is-q-readout #timeSlider::-webkit-slider-thumb { opacity: .9; }
@media (max-width: 360px) {
  .q-time-row .chip { padding: 8px 2px; font-size: 12px; }
  .q-time-row .chip small { font-size: 10px; }
}

.chip {
  position: relative;
  padding: 9px var(--s-2);
  border: 1px solid var(--line-2);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--ink-2);
  font-family: inherit;
  font-size: var(--fs-sm);
  font-weight: 500;
  cursor: pointer;
  transition: border-color var(--dur-micro) var(--ease-out),
              background    var(--dur-micro) var(--ease-out),
              color         var(--dur-micro) var(--ease-out),
              box-shadow    var(--dur-micro) var(--ease-out),
              transform     var(--dur-micro) var(--ease-out);
}

.chip:hover { border-color: var(--accent); color: var(--accent-deep); background: var(--accent-wash); }
.chip:active { transform: scale(0.97); }

/* ★ 2026-10-23（浩哥）：「tab 切换你没必要用主色调的按钮……一个界面里面尽量
   只有一个主色调代表的按钮，知道现在是下一步。」
   → 选中态从「实底红」改成「浅底 + 主色字 + 主色描边」。
     实底红只留给**真正的动作按钮**（生成 / 打印），一屏最多一个。
     选中是「状态」，不是「下一步」，不该和主按钮抢注意力。 */
.chip[aria-pressed="true"],
.chip.is-on {
  background: var(--accent-wash);
  border-color: var(--accent);
  color: var(--accent-deep);
  font-weight: 650;
  box-shadow: inset 0 0 0 1px var(--accent);
}

.chip[aria-pressed="true"]:hover,
.chip.is-on:hover {
  background: var(--accent-wash);
  border-color: var(--accent-deep);
  color: var(--accent-deep);
  filter: brightness(0.98);
}

/* ⚠️ 暗色主题的 --accent-deep 是**提亮**过的红（#ff7d66）——
   白字压上去只有 3.38:1，比亮色还差。深色文字 7.7:1。
   （两个主题的 --accent-deep 语义都是「这个主题下最实的红」，所以复用同一个变量，
     只有文字色要跟着主题翻。） */
[data-theme="dark"] .chip[aria-pressed="true"],
[data-theme="dark"] .chip.is-on,
[data-theme="dark"] .chip[aria-pressed="true"]:hover,
[data-theme="dark"] .chip.is-on:hover { color: var(--accent-deep); }

/* 时间档位是数字，用等宽 */
#timePresets .chip { font-variant-numeric: tabular-nums; }

/* ==========================================================================
   8. 表单
   ========================================================================== */

.field { margin-top: var(--s-6); }

.field label {
  display: block;
  margin-bottom: var(--s-2);
  font-size: var(--fs-xs);
  font-weight: 550;
  color: var(--ink-2);
}

.field-note {
  margin-top: var(--s-2);
  /* 11 → 12（2026-10-13）：11px 中文在桌面端已属偏小、移动端更吃力。
     这是**说明文字**，用户靠它判断参数含义，不该是全页最小的字。 */
  font-size: var(--fs-xs);
  color: var(--ink-3);
  line-height: var(--lh-snug);
}

input[type="number"],
select {
  width: 100%;
  padding: 7px var(--s-4);
  border: 1px solid var(--line-2);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--ink);
  font-family: inherit;
  font-size: var(--fs-sm);
  font-variant-numeric: tabular-nums;
  transition: border-color var(--dur-micro) var(--ease-out),
              box-shadow    var(--dur-micro) var(--ease-out);
}

input[type="number"]:focus,
select:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: var(--sh-focus);
}

select {
  appearance: none;
  padding-right: 30px;
  background-image:
    linear-gradient(45deg, transparent 50%, currentColor 50%),
    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: right 13px center, right 8px center;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  cursor: pointer;
}

.range-field .range-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-4);
}

.range-head output {
  font-family: var(--mono);
  font-size: var(--fs-base);
  font-weight: 700;
  color: var(--accent-deep);
  letter-spacing: -0.01em;
}

/* 滑块：全自定义。默认那条浏览器滑块在暖色版面里像贴上去的。
   track 用渐变填充已选区间 —— 这一条让「现在选到哪」一眼可见。 */
input[type="range"] {
  appearance: none;
  -webkit-appearance: none;
  width: 100%;
  height: 18px;
  margin: var(--s-3) 0 0;
  background: transparent;
  cursor: pointer;
}

input[type="range"]::-webkit-slider-runnable-track {
  height: 4px;
  border-radius: var(--r-full);
  background: linear-gradient(to right, var(--accent) var(--fill, 0%), var(--line-2) var(--fill, 0%));
}

input[type="range"]::-webkit-slider-thumb {
  appearance: none;
  -webkit-appearance: none;
  width: 16px;
  height: 16px;
  margin-top: -6px;
  border-radius: 50%;
  background: var(--surface);
  border: 2px solid var(--accent);
  box-shadow: var(--sh-2);
  transition: transform var(--dur-micro) var(--ease-spring),
              box-shadow var(--dur-micro) var(--ease-out);
}

input[type="range"]:hover::-webkit-slider-thumb { transform: scale(1.14); }
input[type="range"]:active::-webkit-slider-thumb { transform: scale(1.02); box-shadow: var(--sh-focus); }

input[type="range"]::-moz-range-track {
  height: 4px;
  border-radius: var(--r-full);
  background: var(--line-2);
}
input[type="range"]::-moz-range-progress {
  height: 4px;
  border-radius: var(--r-full);
  background: var(--accent);
}
input[type="range"]::-moz-range-thumb {
  width: 13px;
  height: 13px;
  border-radius: 50%;
  background: var(--surface);
  border: 2px solid var(--accent);
  box-shadow: var(--sh-2);
}

.range-scale {
  display: flex;
  justify-content: space-between;
  margin-top: var(--s-1);
  font-size: var(--fs-xs);
  color: var(--ink-3);
}

/* 滑块下方的精度读数（2026-10-15）—— 「滑一下图纸变细」的量化反馈。
   min-height 让它在有 / 无值时高度一致，滑动时不会把下面内容顶得跳动。 */
.time-spec {
  margin: var(--s-3) 0 0;
  font-size: var(--fs-xs);
  color: var(--ink-2);
  font-variant-numeric: tabular-nums;
  min-height: 1.45em;
}

.checkbox {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  margin-top: var(--s-5);
  font-size: var(--fs-sm);
  color: var(--ink-2);
  cursor: pointer;
  user-select: none;
}

.checkbox input {
  appearance: none;
  -webkit-appearance: none;
  width: 17px;
  height: 17px;
  flex: none;
  margin: 0;
  border: 1.5px solid var(--line-2);
  border-radius: var(--r-xs);
  background: var(--surface);
  cursor: pointer;
  transition: background var(--dur-micro) var(--ease-out),
              border-color var(--dur-micro) var(--ease-out);
}

.checkbox input:hover { border-color: var(--accent); }

.checkbox input:checked {
  background: var(--accent);
  border-color: var(--accent);
  /* 对勾用 clip-path 画，不引外部图标 */
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='white' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round' d='M3.5 8.4l3 3 6-6.4'/%3E%3C/svg%3E");
  background-size: 13px;
  background-position: center;
  background-repeat: no-repeat;
}

.checkbox input:focus-visible { box-shadow: var(--sh-focus); outline: none; }

/* ---- 条件生效的子分组 ----
   一组控件只在某种模式下有意义时，整组禁用 + 一句说明，
   比留着让用户点了没反应好。禁用用 opacity + not-allowed，
   不隐藏 —— 用户得知道「这些选项存在，只是现在不适用」。 */
.subgroup {
  margin-top: var(--s-7);
  padding-top: var(--s-6);
  border-top: 1px dashed var(--line-2);
  transition: opacity var(--dur-base) var(--ease-out);
}

.subgroup.is-off { opacity: 0.42; }

.subgroup-cap {
  font-size: var(--fs-xs);
  color: var(--ink-3);
  letter-spacing: var(--tr-wide);
  margin-bottom: var(--s-2);
}

.subgroup.is-off .subgroup-cap { color: var(--ink-4); }

.subgroup .field:first-of-type { margin-top: var(--s-4); }

.subgroup select:disabled,
.subgroup input:disabled {
  cursor: not-allowed;
  background: var(--surface-sunk);
}

.subgroup.is-off .checkbox { cursor: not-allowed; }

.advanced {
  margin-top: var(--s-7);
  padding-top: var(--s-6);
  border-top: 1px solid var(--line);
}

.advanced summary {
  font-size: var(--fs-xs);
  font-weight: 550;
  color: var(--ink-3);
  cursor: pointer;
  user-select: none;
  list-style: none;
  display: flex;
  align-items: center;
  gap: var(--s-3);
  transition: color var(--dur-micro) var(--ease-out);
}

.advanced summary::-webkit-details-marker { display: none; }

.advanced summary::before {
  content: "";
  width: 0;
  height: 0;
  flex: none;
  border-left: 4.5px solid currentColor;
  border-top: 3.5px solid transparent;
  border-bottom: 3.5px solid transparent;
  transition: transform var(--dur-base) var(--ease-out);
}

.advanced[open] summary::before { transform: rotate(90deg); }
.advanced summary:hover { color: var(--accent-deep); }

/* ==========================================================================
   9. 按钮
   ========================================================================== */

.btn-primary {
  position: relative;
  width: 100%;
  padding: var(--s-6);
  border: 0;
  border-radius: var(--r-md);
  background: var(--accent);
  color: #fff;
  font-family: inherit;
  font-size: var(--fs-base);
  font-weight: 650;
  letter-spacing: 0.01em;
  cursor: pointer;
  box-shadow: 0 1px 2px rgba(150, 40, 25, 0.25);
  transition: background var(--dur-micro) var(--ease-out),
              box-shadow var(--dur-micro) var(--ease-out),
              transform  var(--dur-micro) var(--ease-out);
}

.btn-primary:hover:not(:disabled) {
  background: var(--accent-deep);
  box-shadow: 0 3px 10px -2px rgba(150, 40, 25, 0.4);
}

.btn-primary:active:not(:disabled) { transform: translateY(1px); box-shadow: none; }

/* ★ 2026-10-23：禁用的主按钮原来是「灰色实心」，看起来仍像个能点的按钮，
   和真正的下一步抢注意力。改成虚线空位 —— 一眼看出「还没到这一步」。 */
.btn-primary:disabled {
  background: transparent;
  color: var(--ink-3);
  box-shadow: none;
  cursor: not-allowed;
  border: 1px dashed var(--line-2);
}

.btn-ghost {
  flex: none;
  padding: 9px var(--s-7);
  border: 1px solid var(--line-2);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--ink-2);
  font-family: inherit;
  font-size: var(--fs-sm);
  font-weight: 550;
  cursor: pointer;
  transition: border-color var(--dur-micro) var(--ease-out),
              color         var(--dur-micro) var(--ease-out),
              background    var(--dur-micro) var(--ease-out),
              transform     var(--dur-micro) var(--ease-out);
}

.btn-ghost:hover:not(:disabled) {
  border-color: var(--accent);
  color: var(--accent-deep);
  background: var(--accent-wash);
}

.btn-ghost:active:not(:disabled) { transform: translateY(1px); }
/* ★ 2026-10-24：禁用的次要按钮原来给的是「浅灰实底 + 实线边框」——
   和真按钮长得太像。DESIGN.md §2 的判据是「灰实心看起来仍像能点的按钮」，
   同样适用于 ghost：改成虚线空位，和禁用的主按钮同一套语言。 */
.btn-ghost:disabled {
  color: var(--ink-3);
  cursor: not-allowed;
  background: transparent;
  border: 1px dashed var(--line-2);
}
.btn-ico { width: 16px; height: 16px; flex: none; }

/* ==========================================================================
   10. 舞台
   ========================================================================== */

.stage { min-width: 0; }

/* ---------------------- 右侧两栏：画布 + 侧栏（2026-10-13）----------------
   实测：竖向图纸（616×868）装进横向画布容器（1114×644）后，适应高度，
   画布两侧空出 657px；而「用色一览 / 色号对照表 / 采购清单」被压在画布下方
   （printArea 1583px vs stage 876px）—— 用户要滚 707px 才看得到第一眼色号。

   并排之后：空白被侧栏吃掉，色号表与画布同屏；窄屏自动回退单列，
   顺序仍是「画布 → 标题统计 → 色号表 → 采购清单」，不改变阅读动线。 */
.stage-body {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--s-9);
  align-items: start;
}
.stage-main { min-width: 0; }
.stage-side { min-width: 0; }

@media (min-width: 1180px) {
  .stage-body { grid-template-columns: minmax(0, 1fr) 316px; }
}

/* ---------------------- 中屏：768–1179（平板 / 窄笔记本）------------------
   2026-10-13 补。原来 1000px 一刀切：1024px 的平板直接拿到单列，
   色号对照表和采购清单在画布下方排成很长一条，纵向要滚很久。
   这里把两块并排（画布仍在上方 —— 宽度不够，塞不下第三栏），纵向长度减半。 */
@media (min-width: 768px) and (max-width: 1179px) {
  .stage-side {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--s-9);
    align-items: start;
  }
  .stage-side .print-head { grid-column: 1 / -1; }
  .stage-side .legend-list { grid-template-columns: 1fr; }
}

/* ---------------------------- 空状态 --------------------------------------
   原来是「一片白卡中间 4×4 小方块」。改成一整块图纸台：
   网格底纹 + 一个正在拼的菱形图案 + 明确的三步引导。
   空状态是用户看到的第一个画面，它该像邀请，不该像报错。 */

.empty-state {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--s-4);
  min-height: 520px;
  padding: var(--s-12) var(--s-10);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background-color: var(--surface);
  background-image:
    linear-gradient(var(--grid-line) 1px, transparent 1px),
    linear-gradient(90deg, var(--grid-line) 1px, transparent 1px),
    linear-gradient(var(--grid-line-major) 1px, transparent 1px),
    linear-gradient(90deg, var(--grid-line-major) 1px, transparent 1px);
  background-size: var(--grid-size) var(--grid-size),
                   var(--grid-size) var(--grid-size),
                   var(--grid-major) var(--grid-major),
                   var(--grid-major) var(--grid-major);
  box-shadow: var(--sh-inset);
  text-align: center;
  overflow: hidden;
}

/* 中央柔光，把视线收到图案上。要很淡 —— 重了会变成一块白斑，
   反而比网格本身更抢眼。 */
.empty-state::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(ellipse 78% 62% at 50% 44%,
              color-mix(in srgb, var(--surface) 62%, transparent) 0%,
              transparent 72%);
}

/* ⚠️ 2026-10-13 重做：换掉原来那套「纯红菱形 + 中心黑格」——
   红色 + 中心黑块读起来像**警告标记**，不像拼豆；而且只用一种颜色，
   看不出「这是一颗一颗豆子拼出来的」—— 而后者正是这个产品在做的事。
   现在：一颗用真实 MARD 豆色（三档同色相的红）拼的心，逐格落下。
   豆子做成圆形 + 内圈，就是拼豆的样子。结构由 app.js 的 buildEmptyArt() 生成。 */
.empty-art {
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: repeat(8, 26px);
  grid-template-rows: repeat(7, 26px);
  gap: 5px;
  margin-bottom: var(--s-7);
}

.empty-art i {
  border-radius: 50%;
  /* 内圈 = 豆孔；高光在左上，投影在右下 —— 一颗真的拼豆就是这样 */
  box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.45),
              inset 0 0 0 3.5px rgba(255, 255, 255, 0.28),
              inset 0 -1px 2px rgba(0, 0, 0, 0.18),
              0 1px 3px rgba(60, 45, 28, 0.16);
  animation: bead-drop var(--dur-enter) var(--ease-spring) backwards;
}

@keyframes bead-drop {
  from { opacity: 0; transform: scale(0.35) translateY(-6px); }
  to   { opacity: 1; transform: scale(1) translateY(0); }
}

.empty-state > * { position: relative; z-index: 1; }

.empty-state h2 {
  font-size: var(--fs-xl);
  font-weight: 680;
  letter-spacing: var(--tr-tight);
}

.empty-state p {
  max-width: 34ch;
  font-size: var(--fs-sm);
  color: var(--ink-3);
  line-height: var(--lh-snug);
}

/* 三步引导 */
.empty-steps {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--s-3) var(--s-5);
  margin-top: var(--s-6);
}

.empty-steps li {
  display: inline-flex;
  align-items: center;
  gap: var(--s-3);
  padding: 5px var(--s-5);
  border: 1px solid var(--line);
  border-radius: var(--r-full);
  background: color-mix(in srgb, var(--surface) 70%, transparent);
  font-size: var(--fs-xs);
  color: var(--ink-2);
  white-space: nowrap;
}

.empty-steps b {
  font-family: var(--mono);
  font-size: var(--fs-micro);
  font-weight: 700;
  color: var(--accent-deep);
}

.empty-steps ol, .empty-steps ul { list-style: none; margin: 0; padding: 0; }

/* ---------------------------- 图纸区 -------------------------------------- */

.print-area {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: var(--s-9);
  box-shadow: var(--sh-1);
}

.print-head { margin-bottom: var(--s-6); }

.print-head h2 {
  font-size: var(--fs-xl);
  font-weight: 680;
  letter-spacing: var(--tr-tight);
  margin-bottom: var(--s-5);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ---- 标题栏：工程图纸右下角那块元信息表 ----
   原来是一行「19×12 格 · 228 颗豆 · 7 个色号 · 约 30 分钟 · 色卡 MARD · 每格 38px」
   糊过去，读不出重点。现在拆成带标签的数据格：
   值用等宽加粗（视觉重量），标签用小号（不抢）。 */

.print-meta {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(84px, 1fr));
  gap: 1px;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--line);
  overflow: hidden;
}

.tb-cell {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--s-4) var(--s-5);
  background: var(--surface-2);
}

/* ★ 2026-10-13 分主次：6 项等重平铺时，用户的眼睛没有落点。
   主信息（格数 / 颗豆 / 耗时）保持原样；技术细节（色号数 / 色卡 / 每格像素）
   降一档字号与颜色，退到背景里。 */
.tb-cell.is-sub { background: var(--surface); }
.tb-cell.is-sub .tb-v {
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--ink-3);
}
.tb-cell.is-sub .tb-k { color: var(--ink-3); }

.tb-v {
  font-family: var(--mono);
  font-size: var(--fs-base);
  font-weight: 700;
  color: var(--ink);
  letter-spacing: -0.01em;
  line-height: 1.25;
}

.tb-k {
  font-size: var(--fs-micro);
  color: var(--ink-3);
  letter-spacing: var(--tr-wide);
  line-height: 1.3;
}

/* ---- 用色一览 ----
   之前是一条按用量比例分段的纯色带，前面只挂「用色」两个字。
   问题不是不好看，是**读不出信息** —— 不知道每段是什么色号、多少颗，
   也不知道拿它干什么用（用户原话：「看不出是干什么用的，对实际图纸的
   导出也没有太多的帮助」）。

   现在每个色号一颗豆：豆色缩略 + 色号 + 颗数，按用量从多到少排开。
   直接回答拼豆用户最实际的两个问题 ——「这张图主要是什么色」和
   「我要买哪几个色号、各多少颗」。缩略图用和预览同一套质感渲染，
   不会出现「图例和图纸对不上」的割裂感。

   位置仍在图纸标题栏下面（`no-print`）：打印页有自己排好版的
   色号对照表和采购清单，不需要这一块。 */

.palette-strip {
  margin-top: var(--s-6);
  padding-top: var(--s-6);
  border-top: 1px solid var(--line);
}

.palette-strip:empty { display: none; }

/* ★★ 2026-10-22：这一行原来会**把中文逐字拆开**。
   右栏只有 316px，「用色」被压到 23px → 竖排成「用 / 色」，「7 种」同样。
   中文没有词边界，`white-space: normal` 下浏览器会在任意两个汉字之间断行 ——
   凡是「一行放不下的短标签」都会变成竖排。修法：
     ① 每个短标签 `flex: none` + `nowrap`：不许被压；
     ② 容器 `flex-wrap: wrap`：真放不下就整块换行；
     ③ 说明文字 `flex: 1 0 100%` 独占第二行 —— 它本来就是次要信息。 */
.ps-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s-3) var(--s-4);
  margin-bottom: var(--s-5);
}

.ps-title,
.ps-count {
  flex: none;
  white-space: nowrap;
}

.ps-title {
  font-size: var(--fs-sm);
  font-weight: 650;
  color: var(--ink);
  letter-spacing: var(--tr-tight);
}

.ps-count {
  font-family: var(--mono);
  font-size: var(--fs-sm);
  font-weight: 700;
  color: var(--ink-2);
}

.ps-sub {
  order: 9;
  flex: 1 0 100%;
  margin-left: 0;
  font-size: var(--fs-micro);
  color: var(--ink-3);
  letter-spacing: var(--tr-wide);
}

.ps-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3);
}

.ps-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--s-3);
  padding: 3px var(--s-4) 3px 3px;
  border: 1px solid var(--line);
  border-radius: var(--r-full);
  background: var(--surface-2);
  cursor: default;
  transition: border-color var(--dur-micro) var(--ease-out),
              background    var(--dur-micro) var(--ease-out),
              transform     var(--dur-micro) var(--ease-out);
}

.ps-chip:hover {
  border-color: var(--line-3);
  background: var(--surface);
  transform: translateY(-1px);
}

.ps-sw { display: block; flex: none; }

.ps-code {
  font-family: var(--mono);
  font-size: var(--fs-xs);
  font-weight: 700;
  color: var(--ink);
  letter-spacing: -0.01em;
}

.ps-n {
  font-family: var(--mono);
  font-size: var(--fs-micro);
  color: var(--ink-3);
}

/* ==========================================================================
   11. 图纸台（画布视口）
   ========================================================================== */

.canvas-scroll {
  /* 定高（不是 max-height）—— 缩放要一个**确定的视口**去算「适应」比例，
     高度若由内容撑，就会出现「容器高度取决于画布、画布尺寸又取决于容器高度」
     的死循环。 */
  height: 62vh;
  display: flex;
  /* ⚠️ overflow: hidden —— **故意不要滚动条**（浩哥 2026-10-05）。
   *
   * 原来这里是 auto：鼠标在预览区上滚，滚的是「预览区内部」而用户以为在滚页面；
   * 放大后突然冒出来的滚动条还会挤掉画布宽度、视觉上也乱。
   *
   * 关键：hidden **不阻止程序设 scrollLeft/scrollTop**（元素仍是可滚动的，
   * 只是用户不能通过 UI 滚），所以 setZoom() 的锚点补偿逻辑一行都不用改，
   * 拖拽平移也只是改这两个值。
   *
   * 平移靠拖拽、缩放靠滚轮，见 app.js 的 bindZoomGestures()。 */
  overflow: hidden;
  border: 1px solid var(--line-2);
  border-radius: var(--r-lg);
  /* ★ 编辑态进出要有过渡（浩哥 2026-10-16：「编辑态和非编辑态之间的过渡丢失了」）。
     只过渡颜色和光晕，不动尺寸 —— 动尺寸会让画布抖动。 */
  transition: border-color .18s ease, box-shadow .18s ease;
  /* 图纸台：坐标纸底纹 + 内凹阴影，让画布「浮」在台面上 */
  background-color: var(--paper-deep);
  background-image:
    linear-gradient(var(--grid-line) 1px, transparent 1px),
    linear-gradient(90deg, var(--grid-line) 1px, transparent 1px),
    linear-gradient(var(--grid-line-major) 1px, transparent 1px),
    linear-gradient(90deg, var(--grid-line-major) 1px, transparent 1px);
  background-size: var(--grid-size) var(--grid-size),
                   var(--grid-size) var(--grid-size),
                   var(--grid-major) var(--grid-major),
                   var(--grid-major) var(--grid-major);
  box-shadow: var(--sh-inset);
  padding: var(--s-6);
  /* 手势**全部**交给 JS（自己实现拖拽平移 + 双指缩放），别让浏览器拿去滚页面
     或缩放整个页面。pan-x/pan-y 会让单指变成原生滚动，与拖拽平移打架。 */
  touch-action: none;
  /* 拖拽平移时不要顺手选中页面文字 */
  user-select: none;
  -webkit-user-select: none;
}

/* 桌面端（应用外壳）：预览视口按可用高度算 —— **目标是「画布底边落在首屏内」**。
 *
 * ⚠️ 必须写在上面基础规则**之后** —— 同特异性下后写的赢。
 *
 * 330 的来历（2026-10-07 重排后实测，视口 1000px）：
 *   画布上方 = 顶栏 105 + 图纸区上内边距 20 + 判断结论 82 + 间距 12
 *             + 预览工具条 52 + 间距 10 ≈ 281
 *   画布下方 = 说明行 27 + 图纸区下内边距 20 ≈ 47
 *   舞台底边在 981（视口 1000 里还有 19px 给了布局的内边距，够不着）
 *   → 画布高 ≤ 981 − 281 − 47 = 653，取整为 100vh − 330 = 670
 *     （多出的十几像素落在图纸区的下内边距里，看不见，也不会产生整页滚动条）
 *
 * 改之前这个常数是 330，那是**判断结论 / 用色一览还没加进来**时校准的 ——
 * 后来画布上方涨到 436px，1000px 视口下画布底边落到 1211px，
 * 浩哥必须往下滚才能看全图。2026-10-07 把标题 / 统计 / 用色一览
 * 下沉到画布之后，上方重新回到 ~281。
 *
 * 这条由 tools/viewbar_check.py 的「画布底边在首屏内」守住。 */
@media (min-width: 1001px) {
  .canvas-scroll {
    height: calc(100vh - 330px);
    min-height: 300px;
  }
}

#preview {
  display: block;
  /* margin:auto 而不是 justify-content:center ——
     前者在内容溢出时自动塌成 0，四个方向都能滚到；
     后者会把左上角的溢出部分变成**滚不到的死区**。 */
  margin: auto;
  /* ⚠️ 必须禁止 flex 收缩。
     .canvas-scroll 是 display:flex，而 flex 项默认 flex-shrink:1 ——
     放大后 width 虽然被 applyZoom() 设成 3672px，计算宽度却被压回容器宽，
     于是**横向永远出不来滚动条**：图纸放大后右侧被切掉，还平移不过去。
     （纵向不受影响 —— margin:auto 关掉了 align-items:stretch，
       高度按内容走，所以只有横向坏。2026-10-05 实测 rectW=1018 / styleW=3672。）
     这跟上面 margin:auto 的意图是配套的：能溢出，才谈得上滚到。 */
  flex-shrink: 0;
  /* 注意：image-rendering 由 applyZoom() 按当前倍数动态设置 ——
     放大用 pixelated（色块锐利），缩小用 auto（格线不锯齿）。 */
  image-rendering: auto;
  border-radius: var(--r-xs);
  /* 用 box-shadow 画外框而不是 border —— border 会改变元素盒子尺寸，
     而「适应」的滚动判定是拿逻辑尺寸跟视口比，多出来的 2px 会让
     「装得进视口」变成「装不进」，直接踩掉缩放测试。 */
  box-shadow: 0 0 0 1px var(--line-2), var(--sh-3);
}

/* 放大到超出视口时，光标提示「这里可以拖着平移」。
   is-panning 由 JS 在超过 3px 抖动阈值后加上 —— 用 :active 不行，
   鼠标刚按下、还没真的拖动，就已经是 active 了。 */
/* ★ 编辑态：选中格子后画布边框亮起 ——
   让「我正在改这张图」有明确的视觉边界（改前点完格子界面上毫无反应）。
   配合上面的 transition，进出编辑态是平滑的。 */
.canvas-scroll.is-editing {
  border-color: var(--accent);
  box-shadow: var(--sh-inset), 0 0 0 3px var(--accent-wash);
}

.canvas-scroll.is-zoomed { cursor: grab; }
.canvas-scroll.is-zoomed.is-panning { cursor: grabbing; }

/* ==========================================================================
   11b. 预览工具条（2026-10-07 重做）
   --------------------------------------------------------------------------
   一条工具条装下「看哪种样子」+「看多大」：分段控件在左，缩放控件在右。
   改之前是两条各自为政的条（样式一条、缩放一条），加起来 100px 高，
   还把画布往下顶了 —— 浩哥：「切换不同效果的按钮又很紧凑」。

   ⚠️ 选中态不用品牌红。红色是「打印」那个 CTA 的专属，这里抢了整页就没主次。
      用「浅凹槽 + 白滑块 + 细阴影」的分段控件（macOS 那套），
      克制、清楚、和缩放控件同一套语言。
   ========================================================================== */

.preview-bar {
  display: flex;
  align-items: center;
  /* ★ 2026-10-19：加 `flex-wrap`。原来靠 `#previewStyleNote`（flex:1 1 auto）吸收收缩量，
     说明删掉之后**缩放按钮成了唯一能缩的东西** —— 「适应」被挤到 33px 就竖排了。
     现在放不下就换行，绝不压缩控件。 */
  flex-wrap: wrap;
  gap: var(--s-7);
  margin-bottom: var(--s-5);
  padding: var(--s-4) var(--s-5);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--surface);
  box-shadow: var(--sh-1);
  /* 让下面那条 `@container` 能按「工具条自身宽度」判断，而不是按视口 ——
     工具条宽不宽取决于两栏布局，和视口宽度不是线性关系（实测 1200 只有 404px）。 */
  container-type: inline-size;
}

.seg-cap {
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--ink-3);   /* --ink-4 只有 3.2:1，这个标签是要读的 */
  letter-spacing: var(--tr-wide);
  white-space: nowrap;
}

.seg {
  display: inline-flex;
  gap: 2px;
  padding: 3px;
  border-radius: var(--r-full);
  background: var(--surface-sunk);
  /* 一圈极淡的内描边 —— 只靠底色差读不出「凹槽」，整个控件会散掉 */
  box-shadow: inset 0 0 0 1px var(--line), var(--sh-inset);
}

.seg .chip {
  border: 0;
  border-radius: var(--r-full);
  padding: 8px 18px;
  background: transparent;
  color: var(--ink-3);
  font-family: inherit;
  font-size: var(--fs-sm);
  font-weight: 550;
  line-height: 1.2;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color var(--dur-micro) var(--ease-out),
              color            var(--dur-micro) var(--ease-out),
              box-shadow       var(--dur-micro) var(--ease-out);
}

.seg .chip:hover:not([aria-pressed="true"]) { color: var(--ink-2); }

.seg .chip[aria-pressed="true"] {
  background: var(--surface);
  color: var(--ink);
  box-shadow: var(--sh-2);
}

/* ★ 2026-10-19：档位说明（`#previewStyleNote`）删了 —— 档位名本身已经说明是什么。
   工具条上那一格空出来，交给 `margin-left:auto` 把缩放控件推到右边。 */

/* 「熨烫后」这一档的翻面按钮 —— 只在那一档出现（见 syncFlipBtn）。
   和 .chip 用同一套语言，但更轻：它是档位**内部**的一个动作，不是第四档。 */
.flip-btn {
  appearance: none;
  flex: 0 0 auto;
  margin-left: var(--s-2);
  padding: 3px var(--s-5);
  border: 1px solid var(--line-2);
  border-radius: var(--r-full);
  background: transparent;
  font: inherit;
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--ink-3);
  white-space: nowrap;
  cursor: pointer;
  transition: border-color var(--dur-micro) var(--ease-out),
              color var(--dur-micro) var(--ease-out),
              background var(--dur-micro) var(--ease-out);
}
.flip-btn:hover { border-color: var(--accent); color: var(--ink-1); }
/* 停在未熨面时点亮 —— 和 .chip 的 aria-pressed 一个语言 */
.flip-btn[aria-pressed="true"] {
  border-color: var(--accent);
  background: var(--accent-wash);
  color: var(--accent-deep);
}
/* ⚠️ 必须显式写：.flip-btn 有 display 相关的布局意图，
   浏览器默认的 [hidden]{display:none} 会被后面的规则盖掉。 */
.flip-btn[hidden] { display: none; }

/* 翻面：画布转到侧面时换面，再转回来 —— 观感上就是「把成品翻了过来」。
   ⚠️ 用 0→90→0 而不是 0→180：180° 时画布是**镜像**的，
      动画结束重置 transform 会闪一帧（见 flipFusedFace）。 */
@keyframes pd-flip-face {
  0%   { transform: perspective(1200px) rotateY(0deg); }
  50%  { transform: perspective(1200px) rotateY(90deg); }
  100% { transform: perspective(1200px) rotateY(0deg); }
}
.canvas-scroll.is-flipping > canvas {
  animation: pd-flip-face 340ms var(--ease-out);
  backface-visibility: hidden;
}
/* 系统设了「减少动态效果」就别转 —— 换面本身照常发生 */
@media (prefers-reduced-motion: reduce) {
  .canvas-scroll.is-flipping > canvas { animation: none; }
}

/* 画布下方一行：只放操作提示，右对齐 —— 读起来像脚注，不抢画布 */
.preview-note {
  display: flex;
  justify-content: flex-end;
  margin: var(--s-5) 0 0;
  font-size: var(--fs-xs);
  line-height: 1.5;
  /* ⚠️ 2026-10-13 对比度修复：--ink-4 只有 3.24:1（亮）/ 3.01:1（暗），
     远低于 AA 的 4.5:1。这行是**唯一的操作提示**（滚轮缩放/拖拽平移），
     看不见等于这个功能不存在。改用 --ink-3，5.87:1 / 5.52:1。
     同一天再把 11px 提到 12px —— 唯一的操作提示不该是全页最小的字。 */
  color: var(--ink-3);
  min-height: 1.5em;
}

.zoom-hint { text-align: right; }

/* ==========================================================================
   12. 缩放控件（现在装在 .preview-bar 里）
   ========================================================================== */

/* 工具条挤不下一行时，把「预览样式」这个**冗余标签**让出去（只视觉隐藏，读屏仍读得到 ——
   它是 `#previewStyle` 的 aria-labelledby）。
   这样 1400px 这类常见笔记本宽度还能保持一行，而不是靠压缩按钮换来的「适应」竖排。
   ⚠️ 阈值 640px 是工具条**自身**的宽度，不是视口宽度。 */
@container (max-width: 640px) {
  .preview-bar .seg-cap {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
}

.zoom-controls {
  display: flex;
  align-items: center;
  gap: var(--s-1);
  margin-left: auto;
  /* 整组不参与收缩 —— 放不下就让 `.preview-bar` 换行 */
  flex: none;
  padding: 3px;
  /* 和左边分段控件同一套「凹槽」语言，一条工具条才不散 */
  border-radius: var(--r-full);
  background: var(--surface-sunk);
  box-shadow: inset 0 0 0 1px var(--line), var(--sh-inset);
}

.zoom-btn {
  min-width: 30px;
  height: 28px;
  /* ★ 2026-10-19：不许压缩、不许换行。
     改前是 `flex: 0 1 auto` + `min-width: 30px` → 工具条窄的时候按钮被压到 33px，
     「适应」两个字竖排（浩哥报的 bug）。文字永远一行，宽度由内容决定。 */
  flex: none;
  white-space: nowrap;
  padding: 0 var(--s-4);
  border: 0;
  border-radius: var(--r-xs);
  background: transparent;
  color: var(--ink-2);
  font-family: inherit;
  font-size: var(--fs-md);
  line-height: 1;
  cursor: pointer;
  transition: background var(--dur-micro) var(--ease-out),
              color      var(--dur-micro) var(--ease-out);
}

.zoom-btn:hover:not(:disabled) { background: var(--surface); color: var(--accent-deep); box-shadow: var(--sh-1); }
.zoom-btn:disabled { color: var(--ink-4); cursor: not-allowed; }

.zoom-btn-text { font-size: var(--fs-xs); font-weight: 550; padding: 0 var(--s-5); white-space: nowrap; }

.zoom-btn-text[aria-pressed="true"] {
  /* ⚠️ 用「白底 + 细阴影」，不用品牌红（2026-10-07）。
     红色是「打印 / 另存为 PDF」那个 CTA 的专属 —— 工具条上再红一个，
     视线就被这个最不重要的控件抢走了，整页也没了主次。
     和左边分段控件的选中态用同一套语言，一条工具条读起来才是一个整体。 */
  background: var(--surface);
  color: var(--ink);
  font-weight: 650;
  box-shadow: var(--sh-1);
}
.zoom-btn-text[aria-pressed="true"]:hover { background: var(--surface); color: var(--accent-deep); }

.zoom-level {
  min-width: 52px;
  text-align: center;
  font-family: var(--mono);
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--ink-2);
}

/* ==========================================================================
   13. 色号对照表
   --------------------------------------------------------------------------
   ★ 2026-10-19：「采购清单」整块删除（`.shopping-*` 全部样式一起删）。
     浩哥：「采购清单里的"每包 500 颗"是一个假设……所以我觉得这个采购清单没有什么价值，可以去掉。」
     色号 + 颗数本来就在这张对照表里，采购清单是重复信息 + 一个不可信的包数。
   ========================================================================== */

.legend-block {
  margin-top: var(--s-9);
  padding-top: var(--s-7);
  /* 与 `.panel-foot` 用同一档线 —— 同样是「这一段结束了」的分隔，
     一个用 --line-2 一个用 --line，看起来像两套规则。 */
  border-top: 1px solid var(--line-2);
}

.legend-block h3 {
  font-size: var(--fs-base);
  font-weight: 650;
  margin-bottom: var(--s-5);
}

.legend-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: 2px var(--s-7);
}

.legend-item {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  padding: 4px var(--s-3);
  margin: 0 calc(var(--s-3) * -1);
  border-radius: var(--r-xs);
  font-size: var(--fs-xs);
  transition: background var(--dur-micro) var(--ease-out);
}

.legend-item:hover { background: var(--surface-2); }

/* 色块做成豆子：圆角 + 顶部高光，一眼看出是实物色 */
.legend-sw {
  width: 18px;
  height: 18px;
  flex: none;
  border-radius: 50%;
  border: 1px solid rgba(0, 0, 0, 0.16);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.3),
              0 1px 2px rgba(0, 0, 0, 0.1);
}

.legend-idx {
  font-family: var(--mono);
  font-size: var(--fs-micro);
  /* 用 ink-3 而不是 ink-4 —— ink-4 在白底上只有 3.24:1，不到 AA 的 4.5。
     这是对比度审计里唯一挂掉的一处，见 tools/contrast_audit.py。 */
  color: var(--ink-3);
  min-width: 16px;
}

.legend-code {
  font-family: var(--mono);
  font-size: var(--fs-xs);
  font-weight: 700;
  color: var(--ink);
  letter-spacing: -0.01em;
}

.legend-name { color: var(--ink-3); }

.legend-count {
  margin-left: auto;
  font-family: var(--mono);
  font-size: var(--fs-micro);
  color: var(--ink-3);
}

/* ==========================================================================
   14. 计划摘要
   ========================================================================== */

.plan-summary {
  margin-top: var(--s-6);
  padding: var(--s-5) var(--s-6);
  border: 1px solid var(--accent-line);
  border-radius: var(--r-sm);
  background: var(--accent-wash);
  color: var(--ink-2);
  font-size: var(--fs-xs);
  line-height: 1.6;
}

.plan-summary strong {
  font-family: var(--mono);
  font-weight: 700;
  color: var(--accent-deep);
  font-size: var(--fs-sm);
}

/* 「另需熨烫定型 …」—— 与颗数无关的固定开销，弱化但仍要看得见。
   2026-10-05 加：档位语义 = 纯拼搭时间，定型时间必须单独交代，
   否则「10 分钟档」会让用户以为 10 分钟就能拿到成品。 */
.plan-summary .plan-finish {
  display: inline-block;
  margin-top: var(--s-2);
  color: var(--ink-3);
  font-size: var(--fs-xs);
}

/* 「已去背景」徽标 —— 让用户知道颗数比网格格数少是**有意为之**，
   不是算错了（2026-10-05「只拼主体形状」，见 core/subject.js）。 */
.plan-summary .plan-cut {
  display: inline-block;
  margin-left: var(--s-2);
  padding: 1px var(--s-2);
  border-radius: 999px;
  background: var(--ok-wash);
  border: 1px solid var(--ok-line);
  color: var(--ink-2);
  font-size: var(--fs-xs);
  vertical-align: 1px;
  white-space: nowrap;
}

/* ★★ 2026-10-19：可辨识度判定面板整块删除（`.verdict` / `.vd-*` 全部规则）。
   浩哥：「这些块的内容没什么用，可以都删掉，不需要放在那里，反而阻碍增加了高度」。
   判定模型 core/recognize.js 已无使用者，一起下线（可从 git 6551544 恢复）。 */

/* ==========================================================================
   15. 裁剪入口与弹层
   --------------------------------------------------------------------------
   ★ 2026-10-19：「裁剪」不再是独立卡片，它并进了第 1 张卡片（选择图片）的
     缩略图那一行，和「换一张图」并排 —— 调这张图的两条路。
     所以这里没有 `.crop-entry` 了，样式在 `.thumb-actions` 那边。
   ========================================================================== */

.crop-modal {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--s-9);
  background: rgba(28, 22, 16, 0.6);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
  animation: fade-in var(--dur-base) var(--ease-out);
}

@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes dialog-in {
  from { opacity: 0; transform: translateY(10px) scale(0.985); }
  to   { opacity: 1; transform: none; }
}

body.crop-open { overflow: hidden; }

.crop-dialog {
  display: flex;
  flex-direction: column;
  width: min(800px, 100%);
  /* ⚠️ 必须是**定高**，不能只给 max-height 靠内容撑。
     否则会死循环：.crop-body 的高度取决于画布，画布尺寸又由 JS 按
     .crop-body 的高度算 —— 首次打开时画布只有默认的 300×150，
     量出来就是错的，图会缩成一小块。 */
  height: min(88vh, 800px);
  max-height: calc(100vh - 40px);
  border: 1px solid var(--line-2);
  border-radius: var(--r-lg);
  background: var(--surface);
  box-shadow: var(--sh-4);
  overflow: hidden;
  animation: dialog-in var(--dur-slow) var(--ease-out);
}

.crop-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--s-6) var(--s-7);
  border-bottom: 1px solid var(--line);
}

.crop-head h3 { font-size: var(--fs-md); font-weight: 650; }

.crop-head .icon-btn {
  width: 32px;
  height: 32px;
  border: 0;
  background: none;
  font-size: 21px;
  line-height: 1;
}
.crop-head .icon-btn:hover { background: var(--accent-wash); color: var(--accent-deep); }

.crop-body {
  flex: 1;
  min-height: 0;
  /* ★ 2026-10-21：必须裁。手机下画布尺寸是打开时量一次的，
     字体/换行让 `.crop-controls` 变高时，`.crop-body` 被压缩，
     但画布不会跟着缩 —— 会溢出去**盖住「比例 / 缩放」，把 footer 顶出屏幕**
     （手机套件抓到「应用裁剪」点不到）。裁掉 + JS 用 ResizeObserver 重算。 */
  overflow: hidden;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: var(--s-8);
  background-color: var(--paper-deep);
  /* ⚠️ 2026-10-24 去掉网格底纹。
     原来这里铺了一层「拼豆钉板」网格 —— 取景框铺满整图时看不到，没问题。
     但支持自由比例后框能缩到任意大小，框外会大面积露出网格，
     和取景框外那层压暗叠在一起，看起来像「画没对齐」。
     底色保留（--paper-deep），只是不再画格子。 */
}

#cropCanvas {
  display: block;
  border-radius: var(--r-sm);
  box-shadow: var(--sh-3);
  cursor: grab;
  /* 必须的：否则手指拖动会被浏览器当成滚动页面，pointermove 直接断掉 */
  touch-action: none;
}

#cropCanvas.is-dragging { cursor: grabbing; }

.crop-tip { margin-top: var(--s-6); font-size: var(--fs-xs); color: var(--ink-3); }

.crop-controls { padding: var(--s-7) var(--s-7) var(--s-1); }

.crop-field { margin-bottom: var(--s-6); }

.crop-label {
  display: block;
  margin-bottom: var(--s-3);
  font-size: var(--fs-xs);
  font-weight: 550;
  color: var(--ink-2);
}

.chip-row-sm { grid-template-columns: repeat(6, 1fr); }
.chip-row-sm .chip { padding: 7px var(--s-1); font-size: var(--fs-xs); }

.crop-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-6);
  padding: var(--s-6) var(--s-7);
  border-top: 1px solid var(--line);
  background: var(--surface-2);
}

.crop-info {
  font-family: var(--mono);
  font-size: var(--fs-xs);
  color: var(--ink-3);
}

.crop-actions { display: flex; gap: var(--s-5); }
.crop-actions .btn-primary { width: auto; padding: var(--s-5) var(--s-9); font-size: var(--fs-sm); }

/* ==========================================================================
   15b. 创作记录（本地留存，不用注册）
   --------------------------------------------------------------------------
   和裁剪弹层共用同一套遮罩/焦点陷阱/滚动锁，所以外观也保持一致：
   同样的遮罩、同样的圆角与阴影、同样的 head/body/foot 三段。
   ========================================================================== */

.hist-modal {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--s-9);
  background: rgba(28, 22, 16, 0.6);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
  animation: fade-in var(--dur-base) var(--ease-out);
}

body.hist-open { overflow: hidden; }

.hist-dialog {
  display: flex;
  flex-direction: column;
  width: min(760px, 100%);
  max-height: min(86vh, 760px);
  border: 1px solid var(--line-2);
  border-radius: var(--r-lg);
  background: var(--surface);
  box-shadow: var(--sh-4);
  overflow: hidden;
}

.hist-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--s-6);
  padding: var(--s-6) var(--s-7);
  border-bottom: 1px solid var(--line);
}

.hist-head h3 { font-size: var(--fs-md); font-weight: 650; }

.hist-note {
  margin: var(--s-2) 0 0;
  font-size: var(--fs-xs);
  color: var(--ink-3);
  line-height: var(--lh-snug);
}

.hist-head .icon-btn {
  flex: none;
  width: 32px;
  height: 32px;
  border: 0;
  background: none;
  font-size: 21px;
  line-height: 1;
}
.hist-head .icon-btn:hover { background: var(--accent-wash); color: var(--accent-deep); }

.hist-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: var(--s-7);
  background: var(--paper-deep);
}

.hist-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: var(--s-6);
  margin: 0;
  padding: 0;
  list-style: none;
}

.hist-card {
  position: relative;
  border: 1px solid var(--line-2);
  border-radius: var(--r-md);
  background: var(--surface);
  overflow: hidden;
}

.hist-card.is-current { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-wash); }

/* 缩略图：点击 = 打开这条记录。用 button 而不是 div，键盘能 Tab 到 */
.hist-thumb {
  display: block;
  width: 100%;
  aspect-ratio: 1 / 1;
  padding: 0;
  border: 0;
  background: var(--paper);
  cursor: pointer;
}
.hist-thumb img { width: 100%; height: 100%; object-fit: contain; display: block; }
.hist-thumb:hover { background: var(--accent-wash); }

.hist-meta {
  padding: var(--s-4) var(--s-5) var(--s-5);
  border-top: 1px solid var(--line);
}

.hist-when {
  display: block;
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--ink-1);
}

.hist-spec {
  display: block;
  margin-top: 2px;
  font-size: 11px;
  color: var(--ink-3);
  line-height: var(--lh-snug);
}

.hist-del {
  position: absolute;
  top: var(--s-3);
  right: var(--s-3);
  width: 26px;
  height: 26px;
  border: 0;
  border-radius: 50%;
  background: rgba(28, 22, 16, 0.55);
  color: #fff;
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease-out);
}
.hist-card:hover .hist-del,
.hist-del:focus-visible { opacity: 1; }
.hist-del:hover { background: var(--bad); }

.hist-empty {
  padding: var(--s-11) var(--s-8);
  text-align: center;
}
.hist-empty-title { font-size: var(--fs-sm); font-weight: 650; color: var(--ink-1); }
.hist-empty-sub {
  margin: var(--s-4) auto 0;
  max-width: 30em;
  font-size: var(--fs-xs);
  color: var(--ink-3);
  line-height: var(--lh-snug);
}

.hist-foot {
  padding: var(--s-5) var(--s-7) var(--s-6);
  border-top: 1px solid var(--line);
}

/* ⚠️ 免责说明必须**看得见**，所以用警告色、不用 ink-3 那种灰。
   用户以为存云端了、清浏览器才发现没了 —— 那是我们没说明白。 */
.hist-disclaimer {
  margin: 0;
  font-size: var(--fs-xs);
  color: var(--warn);
  line-height: var(--lh-snug);
}

.hist-foot-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-5);
  margin-top: var(--s-5);
}

.hist-usage { font-size: var(--fs-xs); color: var(--ink-3); }
.hist-foot-row .btn-ghost { width: auto; padding: var(--s-4) var(--s-7); font-size: var(--fs-xs); }

/* 顶栏那颗「创作记录」—— 比卡片里的 ghost 按钮矮一档，和状态栏同一行放得下 */
.topbar-actions .btn-ghost {
  padding: 6px var(--s-6);
  font-size: var(--fs-xs);
}

/* 顶栏那颗计数小圆点 */
.hist-count {
  display: inline-block;
  margin-left: var(--s-3);
  padding: 1px 6px;
  border-radius: 999px;
  background: var(--accent-wash);
  color: var(--accent-deep);   /* 3.8:1 → 6.1:1，小字号必须过 AA */
  font-size: 11px;
  font-weight: 650;
}

@media (max-width: 640px) {
  .hist-modal { padding: var(--s-5); align-items: flex-end; }
  .hist-dialog { max-height: 92vh; }
  .hist-list { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: var(--s-5); }
  /* 触屏没有 hover —— 删除按钮必须常显，否则点不到 */
  .hist-del { opacity: 1; width: 30px; height: 30px; }
}

/* ==========================================================================
   16. 页脚
   ========================================================================== */

.footer {
  max-width: 1680px;
  margin: 0 auto;
  padding: 0 var(--s-10) var(--s-11);
  /* 移动端才显示的隐私/色差说明 —— 与桌面端 .panel-note 同一份承诺，同等待遇 */
  font-size: var(--fs-xs);
  color: var(--ink-2);
  line-height: var(--lh-base);
}

.footer-sep { margin: 0 var(--s-3); color: var(--line-3); }

/* ==========================================================================
   17. 入场动效
   --------------------------------------------------------------------------
   只做「错峰浮现」，不做位移过大的编排 —— 这是个工具，用户是来干活的，
   不是来看片头的。每张卡片延迟 22ms，6 张卡片 130ms 内全部启动，
   加 380ms 时长 = 510ms 内整页安静下来。
   ========================================================================== */

@keyframes card-in {
  from { opacity: 0; transform: translateY(7px); }
  to   { opacity: 1; transform: none; }
}

.panel-scroll > .card {
  animation: card-in var(--dur-enter) var(--ease-out) backwards;
  animation-delay: calc(var(--i, 0) * 22ms);
}

.panel-scroll > .card:nth-child(1) { --i: 0; }
.panel-scroll > .card:nth-child(2) { --i: 1; }
.panel-scroll > .card:nth-child(3) { --i: 2; }
.panel-scroll > .card:nth-child(4) { --i: 3; }
.panel-scroll > .card:nth-child(5) { --i: 4; }
.panel-scroll > .card:nth-child(6) { --i: 5; }

.panel-foot > .btn-primary {
  animation: card-in var(--dur-enter) var(--ease-out) backwards;
  animation-delay: 130ms;
}

/* 图纸生成：画布淡入。不用逐格拼装 —— 大图有上万个格子，
   逐格动画会真的拖慢工具，得不偿失。淡入 + 轻微上浮已经够「显影」的感觉。 */
@keyframes canvas-reveal {
  from { opacity: 0; transform: translateY(6px) scale(0.995); }
  to   { opacity: 1; transform: none; }
}

.print-area.is-fresh #preview {
  animation: canvas-reveal var(--dur-slow) var(--ease-out);
}

/* 尊重系统的「减少动态效果」。这不是可选项 —— 前庭功能障碍的用户
   会因为界面动效真的头晕恶心。 */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ==========================================================================
   18. 移动端
   --------------------------------------------------------------------------
   拼豆用户在小红书（手机），链接就是在手机上点开的 —— 手机是**主场景**，
   不是「降级版」。所以这里按「手指点得准、眼睛看得清」的标准来，
   而不是「能挤下就行」。
   ========================================================================== */

.only-mobile { display: none; }

@media (max-width: 640px) {
  .layout { padding: var(--s-6) var(--s-6) var(--s-11); gap: var(--s-6); }

  .only-desktop { display: none; }
  .only-mobile { display: inline; }

  .topbar {
    padding: var(--s-5) var(--s-6);
    gap: var(--s-5);
    /* 手机上顶栏下面就是内容，88% 的半透明会透出字来，显得脏 */
    background: color-mix(in srgb, var(--paper) 97%, transparent);
    /* ★ 2026-10-22：390px 装不下「品牌 + 状态 + 两个按钮」一行 ——
       标题被压到 72px 宽，中文逐字竖排成「拼豆/图纸/生成/器」。
       改成两行：第一行品牌，第二行状态靠左、按钮靠右。 */
    flex-wrap: wrap;
    row-gap: var(--s-4);
  }
  .brand { flex: 1 1 100%; }
  .topbar-actions { flex: 1 1 100%; justify-content: space-between; }
  .tagline { display: none; }
  .brand-mark { width: 30px; height: 30px; }

  /* 触控目标：44px 是手指点得准的下限（Apple HIG 与 Material 同一口径） */
  .chip { min-height: 46px; font-size: var(--fs-base); }
  .btn-primary { min-height: 52px; font-size: var(--fs-md); }
  /* ★ 2026-10-23：底部「打印 / 另存为 PDF」和「导出 PNG」都是 `.btn-ghost`，
     不是 `.btn-primary` —— 上面那条管不到它们，实测只有 37px。
     手指点不准等于功能不存在。 */
  .panel-foot .btn-ghost { min-height: 46px; font-size: var(--fs-base); }
  .advanced summary { min-height: 44px; }
  .icon-btn { width: 40px; height: 40px; }

  /* 输入框用 16px：低于 16px 时 iOS 聚焦会自动放大整个页面 */
  input[type="number"],
  select { min-height: 46px; font-size: 16px; }

  /* 复选框本身只有 17px，放大到 22px；整行撑到 44px，点哪都算 */
  .checkbox { min-height: 44px; font-size: var(--fs-base); }
  .checkbox input { width: 22px; height: 22px; }

  /* 滑块：加大拖动命中区域，不然手机上拖不动 */
  input[type="range"] { height: 44px; }

  /* 正文不小于 12px —— 手机上 11px 是真的看不清 */
  .step { font-size: var(--fs-xs); }
  .field-note { font-size: var(--fs-xs); }
  .range-scale { font-size: var(--fs-xs); }
  kbd { font-size: var(--fs-xs); }
  .legend-idx { font-size: var(--fs-xs); }
  .legend-item { font-size: var(--fs-sm); }

  /* 用色一览：手机上「悬停看…」是空话（没有悬停），整条提示撤掉；
     色号和颗数抬到 12px 起，否则和 11px 的标签糊成一片。 */
  .ps-sub { display: none; }
  .ps-code,
  .ps-n { font-size: var(--fs-xs); }
  .plan-summary { font-size: var(--fs-sm); }
  .dz-sub { font-size: var(--fs-xs); }
  .tb-k { font-size: var(--fs-xs); }
  .thumb-size { font-size: var(--fs-xs); }
  .crop-state { font-size: var(--fs-sm); }
  /* 预览工具条：手指上要够大（44px），且要能折行 —— 390px 宽装不下
     「标签 + 三档 + 缩放」一整行。档位说明独占一行（不许省略号）。 */
  .preview-bar { flex-wrap: wrap; gap: var(--s-4); padding: var(--s-4); }
  .seg { flex: 1 1 auto; }
  .seg .chip { padding: 9px 14px; font-size: var(--fs-sm); min-height: 44px; flex: 1 1 auto; }
  /* 翻面按钮：和旁边三档 chip 同一口径撑到 44px —— 桌面端它是紧凑胶囊
     （~25px），手指点不中。间距交给 .preview-bar 的 gap，不要自己再加左边距。 */
  .flip-btn { min-height: 44px; padding: 9px var(--s-6); font-size: var(--fs-sm); margin-left: 0; }
  .zoom-controls { margin-left: 0; }
  .zoom-btn { min-width: 44px; height: 44px; font-size: 19px; }
  .zoom-btn-text { font-size: var(--fs-base); padding: 0 var(--s-6); }
  .zoom-level { font-size: var(--fs-base); min-width: 50px; }

  .preview-note { justify-content: flex-start; }
  .zoom-hint { font-size: var(--fs-xs); text-align: left; }

  /* 上传区 */
  .dropzone { padding: var(--s-10) var(--s-7); }
  .dz-main { font-size: var(--fs-md); }

  /* 付费通道三段对比。
     手机上左栏是全宽，缩略图自然涨到 ~86px，够看；
     但两个**可点控件**必须撑到 44px —— 「载入」按钮和「图纸/实物」切换
     桌面端只有 ~26px，手指点不中。 */
  .paid-lead, .paid-note { font-size: var(--fs-xs); }
  .pc-name, .pc-tag, .pc-preset, .pc-lab,
  .pc-stat, .pc-note, .pc-why { font-size: var(--fs-xs); }
  .pc-load { min-height: 44px; font-size: var(--fs-base); padding: 9px var(--s-8); }
  .pc-tex { width: 100%; margin-left: 0; }
  .pc-tex button { flex: 1; min-height: 44px; font-size: var(--fs-base); }

  /* 卡片：手机上内边距收一点，但保持呼吸 */
  .card { padding: var(--s-6); }
  .print-area { padding: var(--s-6); border-radius: var(--r-md); }

  /* 空状态 */
  .empty-state { min-height: 420px; padding: var(--s-11) var(--s-7); }
  .empty-art { grid-template-columns: repeat(5, 28px); grid-template-rows: repeat(5, 28px); }
  .empty-state h2 { font-size: var(--fs-lg); }
  .empty-steps { flex-direction: column; align-items: center; }

  /* 标题栏：手机上两列，别挤成一行小字 */
  .print-meta { grid-template-columns: repeat(2, 1fr); }
  .print-head h2 { font-size: var(--fs-lg); }

  /* 图纸：默认「适应」把整张图缩进视口，放大后可拖动看每格色号 */
  .canvas-scroll { height: 58vh; -webkit-overflow-scrolling: touch; }

  /* 裁剪入口按钮：桌面端 37px 够用，手指上不够（2026-10-02 移动端测试实测） */
  .crop-entry .btn-ghost { min-height: 46px; font-size: var(--fs-base); padding: 9px var(--s-8); }

  /* 裁剪弹层铺满整屏 —— 和相册裁剪的手感一致 */
  .crop-modal { padding: 0; }
  /* ★★ 2026-10-21：手机上编辑条**贴底固定**。
     理由有两条，都是实测出来的：
       ① 它上面是画布，高度一变画布就跳 —— 手机上点第二下会点到别的格子；
       ② 390px 宽放不下一行（坐标 168 + 色块条 + 全部色号 + 4 个按钮），
          一旦撑出横向滚动，整个页面被浏览器缩放，连裁剪弹层的高度都算错
          （手机套件里「应用裁剪」点不到，就是这么来的）。
     贴底固定：不占布局、不推画布、也不会把页面撑宽。 */
  .edit-bar {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 55;
    height: auto;
    min-height: 74px;
    margin: 0;
    padding: 8px 12px calc(8px + env(safe-area-inset-bottom, 0px));
    border-radius: 0;
    border-left: 0;
    border-right: 0;
    border-bottom: 0;
    box-shadow: 0 -3px 14px rgba(28, 22, 16, 0.14);
  }
  /* 别让固定条压住页面最后一段内容 */
  body:has(#editBar:not([hidden])) { padding-bottom: 92px; }
  .edit-row { gap: 6px; }
  .edit-title { display: none; }      /* 空间不够，图标和坐标已经说明问题 */
  .edit-pos { width: 118px; }
  .edit-hint { display: none; }       /* 手机上按钮本身就是提示 */

  .crop-dialog {
    width: 100%;
    height: 100%;
    max-width: none;
    max-height: none;
    border-radius: 0;
    border: 0;
  }
  .crop-head { padding: var(--s-5) var(--s-6); }
  .crop-head .icon-btn { width: 44px; height: 44px; font-size: 25px; }
  .crop-body { padding: var(--s-6); }
  .crop-tip { font-size: var(--fs-xs); }
  .crop-controls { padding: var(--s-6) var(--s-6) var(--s-1); }
  .chip-row-sm .chip { min-height: 44px; font-size: var(--fs-sm); }
  .crop-foot {
    flex-direction: column;
    align-items: stretch;
    gap: var(--s-5);
    padding: var(--s-5) var(--s-6);
  }
  .crop-info { font-size: var(--fs-sm); text-align: center; }
  .crop-actions { display: grid; grid-template-columns: 1fr 1fr; }
  .crop-actions .btn-ghost,
  .crop-actions .btn-primary { width: 100%; min-height: 48px; font-size: var(--fs-md); }

  .legend-list { grid-template-columns: 1fr; }

  /* 手机上不做错峰入场 —— 小屏滚动时动效更容易让人不适 */
  .panel-scroll > .card,
  .panel-foot > .btn-primary { animation: none; }
}

/* ==========================================================================
   19. 打印分页
   ========================================================================== */

/* 屏幕上不显示；打印时由 app.js 填充并替换掉预览画布 */
.print-pages { display: none; }

.print-page { page-break-after: always; }
.print-page:last-child { page-break-after: auto; }

.print-page-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--s-6);
  padding-bottom: var(--s-3);
  margin-bottom: var(--s-4);
  border-bottom: 1px solid #999;
  font-size: 10pt;
}

.print-page-head .pp-title { font-weight: 700; }
.print-page-head .pp-range { color: #444; font-variant-numeric: tabular-nums; }

.print-page canvas { display: block; max-width: 100%; height: auto; }

/* ==========================================================================
   20. 打印样式 —— 只保留图纸区
   ========================================================================== */

@page { size: A4; margin: 12mm; }

@media print {
  .no-print { display: none !important; }

  /* ⚠️ 必须把桌面端的「应用外壳」整个拆掉。
     否则 body 的 height:100vh + overflow:hidden 会把内容裁到只剩一屏，
     打印出来只有第一页 —— 而且是残缺的一页。 */
  html, body {
    background: #fff;
    color: #000;
    font-size: 11pt;
    height: auto;
    overflow: visible;
    display: block;
  }

  /* 打印一律用白底黑字，不受暗色主题影响 */
  .layout {
    display: block;
    max-width: none;
    padding: 0;
    gap: 0;
    height: auto;
  }

  .panel,
  .stage {
    overflow: visible;
    min-height: 0;
  }

  .stage { display: block; }

  .canvas-scroll { height: auto; max-height: none; }

  .print-area {
    display: block !important;
    border: 0;
    border-radius: 0;
    padding: 0;
    box-shadow: none;
    background: #fff;
  }

  .print-head {
    border-bottom: 1px solid #999;
    margin-bottom: 10px;
  }

  .print-head h2 { font-size: 15pt; }

  .print-meta {
    display: block;
    border: 0;
    background: none;
    font-size: 9pt;
    color: #444;
  }
  .tb-cell { display: inline; padding: 0; background: none; }
  .tb-cell::after { content: " · "; color: #999; }
  .tb-cell:last-child::after { content: ""; }
  .tb-v { font-size: 9pt; color: #000; }
  .tb-k { font-size: 9pt; color: #444; }

  /* 屏幕预览画布隐藏 —— 打印改用按 A4 排版的高分辨率分页 */
  .canvas-scroll { display: none !important; }

  .print-pages { display: block; }

  .print-page {
    page-break-inside: avoid;
    margin-bottom: 0;
  }

  .legend-block {
    page-break-inside: avoid;
    margin-top: 14px;
    padding-top: 10px;
    border-top: 1px solid #bbb;
  }

  .legend-block h3 { font-size: 11pt; }

  .legend-list {
    grid-template-columns: repeat(3, 1fr);
    font-size: 9pt;
  }

  .legend-item { font-size: 9pt; }

  /* ⚠️ 屏幕版的两处「好看」在打印里会越界，必须拆掉：
   *
   * 1) 色块上的 box-shadow —— 阴影的**绘制边界**比布局盒子大，
   *    Chrome 打印时会把带阴影的元素光栅化成一张位图，位图边界就是绘制边界，
   *    于是左边多出约 3px，整份 PDF 被判越出左边距。
   *    实测：越界 0.32pt，被 tools/print_test.py 的 PDF 边界检查抓到。
   * 2) 图例项的负外边距（悬停高亮铺满用）—— 同理把第一列顶出左边距。
   *
   * 教训：打印输出不要依赖阴影、模糊、负外边距这类「超出盒子」的效果。 */
  .legend-item { padding: 0; margin: 0; }

  /* ⚠️⚠️ 色块的填充是 **CSS 背景色**，而 Chrome 按 Ctrl+P 时
     「背景图形」**默认不勾** → 背景色被剥掉，色块只剩一个空心圈。
     白色的色块本来就和纸一样，所以症状看起来是「只有非白色的颜色印不出来」
     （浩哥 2026-10-07 报的 bug）。
     `print-color-adjust: exact` = 明确告诉浏览器「这个背景承载信息，必须印」。
     实测（tools/_probe_print_bg.py，print_background=False 模拟真实 Ctrl+P）：
     判据色像素 **211 → 1311**，与强制打开背景时的 1311 完全一致。
     ⚠️ **只能精准加在承载信息的元素上，绝不要全局加** ——
        全局加会把暗色主题的深色底也真印出来（一整块黑）。
     ⚠️ 这条同时由 tools/print_test.py 的 `swatch_probe()` 守着，
        那一节用的是 `print_background=False`（用户真实路径）。 */
  .legend-sw {
    box-shadow: none;
    border-color: rgba(0, 0, 0, 0.35);
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
  }
}

/* ================= AI 通道（2026-10-12 新增） ================= */
.card-row { display: flex; gap: 8px; align-items: stretch; }
/* ⚠️ 移动端触控目标必须 ≥44px（mobile_test.py 有断言）。9px 内边距只有 42px，
   实测被拦下来了 —— 提到 12px。 */
.card-row .btn-ghost { padding-top: 12px; padding-bottom: 12px; }
.card-input {
  flex: 1 1 auto; min-width: 0;
  padding: 12px;
  border: 1px solid var(--line-2);
  border-radius: 10px;
  background: var(--surface, #fff);
  color: inherit;
  font: inherit;
  letter-spacing: .06em;
  text-transform: uppercase;
}
.card-input:focus { outline: 2px solid var(--accent); outline-offset: 1px; }

.ai-steps { margin-top: 12px; display: grid; gap: 8px; }
.ai-step { display: flex; align-items: center; gap: 9px; font-size: 13px; color: var(--ink-3); }
.ai-step i { width: 9px; height: 9px; border-radius: 50%; background: var(--line-2); flex: 0 0 auto; }
.ai-step[data-state="run"] { color: var(--accent-deep); font-weight: 600; }
.ai-step[data-state="run"] i { background: var(--accent); animation: aiPulse 1s ease-in-out infinite; }
.ai-step[data-state="ok"] i { background: #2f9e44; }
.ai-step[data-state="err"] { color: var(--bad); }
.ai-step[data-state="err"] i { background: var(--bad); }
@keyframes aiPulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }

/* ================= 图纸编辑器（2026-10-12 新增） =================
 *
 * ★★ 2026-10-21 搬到**画布正上方**，改成一条固定横条。
 *   浩哥：「这个说明它在颜料的下方，一旦我选择的颜料比较多，它就会因为位置
 *   一直往下挤，导致看不到……对于编辑的操作，尽量保持固定位置」。
 *   原来它在 `.stage-side` 里、排在「用色」**下面** —— 用色越多，它越靠下，
 *   用户的鼠标得跟着跑。现在它的上边只由 `.preview-bar` 决定，
 *   和右栏内容高度**完全无关**。
 *
 *   `flex-wrap` 是兜底：宽度不够就换行，绝不压缩控件（同 `.preview-bar` 的教训）。
 */
.edit-bar {
  /* ★★ 固定高度 74px = 上内边距 0 + 行1 26 + 间距 6 + 行2 26 + 下 0（靠 justify-content 居中）
     ⚠️ 这个高度**不许随内容变**：它下面就是画布，高度一变画布就跳，
        用户点第二下会点到别的格子（2026-10-21 e2e 抓到：再点同一格选中了隔壁）。
     ⚠️ 两个 `.edit-row` 都是 `nowrap`：坐标 / 色号文字变长只会挤色块条，不会换行。 */
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 6px;
  height: 74px;
  box-sizing: border-box;
  margin-bottom: var(--s-5);
  padding: 0 var(--s-5);
  border: 1px solid var(--line-2);
  border-radius: var(--r-md);
  background: var(--accent-wash, #faf7f0);
}
/* ⚠️ `display:flex` 会盖掉 `[hidden]` 的 `display:none` —— 必须显式补回来 */
.edit-bar[hidden] { display: none; }

.edit-row {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  flex-wrap: nowrap;
  min-width: 0;
}

.edit-title {
  flex: none;
  font-size: var(--fs-xs);
  font-weight: 700;
  color: var(--ink);
  white-space: nowrap;
}
/* 固定宽度：空态也占位 —— 「第 3 行 · 第 5 列 · C12」比空串宽得多，
   不固定就会把后面的色块条推来推去。 */
.edit-pos {
  flex: none;
  width: 138px;
  font-size: var(--fs-xs);
  font-weight: 650;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  font-variant-numeric: tabular-nums;
}
.edit-cur { display: none; }   /* 已并入 #editPos（保留占位给老测试读 textContent） */

/* 这张图用到的颜色 —— 就在操作行里，点一下 = 拿起画笔。
   横向滚动：颜色再多也不换行、不撑高，横条高度恒定。 */
.edit-swatch-label {
  flex: none;
  padding: 2px 0;
  border: 0;
  background: transparent;
  font-family: inherit;
  font-size: var(--fs-micro);
  color: var(--ink-3);
  white-space: nowrap;
  user-select: none;
  text-align: left;
  cursor: pointer;
}
.edit-swatch-label:hover { color: var(--accent-deep); text-decoration: underline; text-underline-offset: 3px; }
.edit-swatches {
  display: flex;
  align-items: center;
  gap: 3px;
  flex: 1 1 auto;
  min-width: 60px;
  padding: 1px 0;
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: thin;
  /* 末尾一点点渐隐 = 「右边还有颜色，可以滚」的暗示。
     ⚠️ 只渐隐 14px：再多会让最后一个色块看起来是禁用的。 */
  -webkit-mask-image: linear-gradient(90deg, #000 0, #000 calc(100% - 14px), transparent 100%);
  mask-image: linear-gradient(90deg, #000 0, #000 calc(100% - 14px), transparent 100%);
}
/* 画笔状态的文字版本：界面上由 `.edit-swatch-label` 的「画笔 H19」和色块勾选表达，
   这个 span 只给自动化测试读 textContent（同 `#editCur` 的做法）。 */
.edit-brush { display: none; }
/* 拿起画笔后，这块从「点颜色」变成状态读数：色点 + 色号。
   位置不动、宽度只多 20 来 px，比再塞一颗色块省地方；
   色块条横向滚动时，当前涂的是什么也不会丢。 */
.edit-swatch-label.is-brush {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  color: var(--accent-deep);
  font-weight: 700;
  text-decoration: none;
}
/* 画笔状态本身就是「放下」的按钮：× 是触屏也能看见的退出信号，
   不用再猜「再点一次同一个色块」才能放下笔。 */
.edit-swatch-label.is-brush::after {
  content: "×";
  margin-left: 2px;
  color: var(--ink-3);
  font-weight: 700;
  line-height: 1;
}
.edit-swatch-label.is-brush::before {
  content: "";
  width: 8px;
  height: 8px;
  flex: none;
  border-radius: 50%;
  border: 1px solid rgba(0, 0, 0, 0.16);
  background: var(--brush-sw, var(--accent));
}
.edit-swatch-label.is-eraser::before {
  border-color: var(--ink-3);
  background-color: var(--surface);
  background-image: linear-gradient(135deg,
    transparent 44%, var(--ink-3) 44%, var(--ink-3) 56%, transparent 56%);
}
.es-chip {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  height: 26px;
  padding: 0 6px 0 3px;
  border: 1px solid var(--line-2);
  border-radius: var(--r-full);
  background: var(--surface);
  color: var(--ink-2);
  font-family: var(--mono);
  font-size: var(--fs-micro);
  cursor: pointer;
  white-space: nowrap;
  transition: border-color var(--dur-micro) var(--ease-out),
              background   var(--dur-micro) var(--ease-out),
              color        var(--dur-micro) var(--ease-out),
              transform    var(--dur-micro) var(--ease-out);
}
.es-chip:hover { background: var(--surface); border-color: var(--accent-line); color: var(--accent-deep); }
.es-chip:active { transform: translateY(1px); }
/* 当前画笔：和用色一览里的高亮同一套语言 */
.es-chip.is-brush {
  border-color: var(--accent);
  background: var(--surface);
  color: var(--accent-deep);
  box-shadow: 0 0 0 2px var(--accent-wash);
}
.es-chip:focus-visible { outline: 2px solid var(--accent-deep); outline-offset: 2px; }
.es-sw {
  position: relative;
  width: 16px;
  height: 16px;
  flex: none;
  border-radius: 50%;
  border: 1px solid rgba(0, 0, 0, 0.16);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.3);
}
/* 选中的颜色打勾 —— 色相本身不足以说明「这支笔已经拿在手上」，
   白勾加深色描边，在浅色和深色豆子上都读得出来。 */
.es-chip.is-brush .es-sw::after {
  content: "";
  position: absolute;
  left: 4px;
  top: 1px;
  width: 5px;
  height: 8px;
  border: solid #fff;
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
  filter: drop-shadow(0 0 1px rgba(0, 0, 0, .9));
}

.edit-more {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 26px;
  padding: 0 var(--s-4);
  border: 1px solid var(--line-2);
  border-radius: var(--r-full);
  background: var(--surface);
  color: var(--ink-2);
  font-family: inherit;
  font-size: var(--fs-xs);
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
}
.edit-more:hover { border-color: var(--accent); color: var(--accent-deep); }

/* 工具排在颜色之前、固定在工具架左上：选择 / 橡皮 / 撤销 / 重置。
   不分组、不加小标题 —— 四个同等重量的按钮本身就是一层。 */
.edit-tools {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  flex: none;
  min-width: 0;
}
.edit-tools .btn-ghost {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  height: 26px;
  padding: 0 var(--s-4);
  font-size: var(--fs-xs);
}
/* ★ 2026-10-25：编辑的两个出口放在缩放控件右侧（画布上方，贴底广告盖不到）。
   高度和缩放按钮同一档，不把预览工具条撑高；保存仍是唯一的实心主按钮。 */
.preview-bar .preview-exit {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  width: auto;
  height: 34px;
  min-height: 34px;
  margin: 0;
  padding: 0 12px;
  border-radius: var(--r-full);
  font-size: var(--fs-xs);
  font-weight: 650;
  white-space: nowrap;
  box-shadow: none;
}
.preview-bar .preview-save { border: 0; font-weight: 700; }
.preview-bar .preview-exit .btn-ico { width: 14px; height: 14px; }
.preview-bar .preview-cancel { margin-left: 2px; }
/* ⚠️ 上面给了 display:inline-flex，会盖掉 [hidden] 的 display:none —— 必须补回来，
   否则没进编辑时两个出口也会显出来。 */
.preview-bar .preview-exit[hidden] { display: none; }
.edit-hint {
  margin-left: auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--fs-micro);
  color: var(--ink-3);
}
.edit-bar .btn-ghost:disabled { opacity: .45; cursor: not-allowed; }
.edit-bar .btn-ghost[aria-pressed="true"] {
  border-color: var(--accent);
  color: var(--accent-deep);
  background: var(--surface);
}

/* 「全部色号」入口 —— 挂在用色一览的标题行右边 */
.ps-all {
  margin-left: auto;
  padding: 2px var(--s-5);
  border: 1px solid var(--line-2);
  border-radius: var(--r-full);
  background: var(--surface);
  color: var(--ink-2);
  font-family: inherit;
  font-size: var(--fs-xs);
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
}
.ps-all:hover { border-color: var(--accent); color: var(--accent-deep); }

/* ==========================================================================
   13b. 全部色号（整个色卡的选色器）
   --------------------------------------------------------------------------
   复用「创作记录」抽屉的遮罩 / 焦点陷阱 / 三段结构（`.hist-*`），
   只补自己的搜索行与色卡网格。
   ========================================================================== */

.color-dialog { width: min(720px, 100%); }

.color-search-row {
  display: flex;
  align-items: center;
  gap: var(--s-5);
  padding: var(--s-5) var(--s-7) 0;
}

.color-search {
  flex: 1;
  min-width: 0;
  padding: 8px var(--s-6);
  border: 1px solid var(--line-2);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--ink);
  font-family: inherit;
  font-size: var(--fs-sm);
}
.color-search:focus { outline: none; border-color: var(--accent); box-shadow: var(--sh-focus); }

.color-count { flex: none; font-size: var(--fs-xs); color: var(--ink-3); }

.color-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(58px, 1fr));
  gap: var(--s-3);
}

.color-cell {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  padding: 4px 2px;
  border: 1px solid transparent;
  border-radius: var(--r-sm);
  background: none;
  cursor: pointer;
}
.color-cell:hover { border-color: var(--line-2); background: var(--surface); }
.color-cell.is-brush { border-color: var(--accent); background: var(--accent-wash); }

/* 色块本体：不用 canvas 质感 —— 291 个 canvas 太重，选色只要颜色准 */
.color-sw {
  width: 30px;
  height: 30px;
  border-radius: 6px;
  border: 1px solid rgba(0, 0, 0, 0.18);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.35);
}

.color-code {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--ink-2);
  line-height: 1.2;
}

@media (max-width: 640px) {
  .color-grid { grid-template-columns: repeat(auto-fill, minmax(50px, 1fr)); }
  .color-sw { width: 26px; height: 26px; }
}

/* ── 风格选择 + 效果对比（2026-10-22）──────────────────────────────── */
/* 只有两个风格，别留空列 */
.style-row { grid-template-columns: repeat(2, 1fr); }
/* 「画面重点」也只有 2 个 —— 不指定就会按 3 列排，第二个被挤成两行（2026-10-22 修） */
#frameRow { grid-template-columns: repeat(2, 1fr); }

/* ⚠️ 这个类在上面 864 行还有一份定义（历史遗留的重复）。
   这里覆盖了尺寸和颜色，所以「改上面那份不生效」—— 别在 864 行改。
   color 必须是 --accent-deep：--accent 在白底上只有 4.29:1，过不了 AA（2026-10-24）。 */
.link-btn {
  margin-left: 6px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--accent-deep);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.link-btn:hover { opacity: .72; }

.style-dialog { width: min(900px, 94vw); max-width: 900px; }

.style-compare {
  display: grid;
  gap: var(--s-4);
  /* 2026-10-23：左「原照片」右「Q 版 → 图纸」。
     ⚠️ 用 auto 而不是固定 150px —— 没有原照片时（photo 缺失）整行会退化成单列，
        固定 150px 会把内容全挤进那一列，图变成小方块。 */
  grid-template-columns: auto 1fr;
  align-items: start;
}
.style-orig {
  display: grid;
  gap: 6px;
  justify-items: center;
  font-size: 12px;
  color: var(--ink-dim, #8a8a92);
}
.style-orig img {
  width: 150px;
  border-radius: 10px;
  border: 1px solid var(--line);
  display: block;
}
.style-cols {
  display: grid;
  gap: var(--s-4);
  /* 自适应：现在只有一列（Q 版），固定两列会各占一半、图被挤小 */
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
}
.style-col {
  display: grid;
  gap: 6px;
  padding: var(--s-3);
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--surface-2, #f7f7f5);
}
.style-col-head { font-weight: 600; font-size: 14px; }
.style-col-note {
  font-size: 12px;
  color: var(--ink-dim, #8a8a92);
  line-height: 1.5;
}
.style-shots {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 6px;
}
.style-shots figure { margin: 0; display: grid; gap: 3px; }
.style-shots img {
  width: 100%;
  border-radius: 6px;
  border: 1px solid var(--line);
  display: block;
}
.style-shots figcaption {
  font-size: 11px;
  color: var(--ink-dim, #8a8a92);
  text-align: center;
}
.style-col-foot { font-size: 12px; }

@media (max-width: 720px) {
  .style-compare { grid-template-columns: 1fr; }
  .style-orig { justify-items: start; }
  .style-orig img { width: 110px; }
}


/* 等待提示（2026-10-23）：告诉用户要等多久、能不能离开 */
.ai-wait {
  margin: 4px 0 0;
  font-size: 12.5px;
  line-height: 1.6;
  color: var(--ink-3);
}
.ai-wait b { color: var(--accent-deep); }

/* 卡密购买入口（2026-10-23）：跟在输入框下面，不抢主按钮的注意力 */
.buy-link {
  display: inline-block;
  margin-left: 2px;
  color: var(--accent-deep);
  text-decoration: none;
  border-bottom: 1px dashed currentColor;
  white-space: nowrap;
}
.buy-link:hover { color: var(--accent); }

/* ==========================================================================
   终点动作（2026-10-23）
   浩哥：「一个界面里面尽量只有一个主色调代表的按钮，知道现在是下一步。」
   → 「生成」是下一步，占主色实底；「打印」是终点，用深色描边强调，不抢主色。
   ========================================================================== */
/* btn-emph 已于 2026-10-23 删除：打印和导出 PNG 是同一层级，统一用 ghost。 */

/* ==========================================================================
   布局呼吸（2026-10-23）
   浩哥：「按钮与文案之间的间距对齐这种布局，布局要求不要全挤在一坨了。」
   ========================================================================== */
/* label 行：左标题 + 右动作，基线对齐 */
.label-row {
  display: flex;
  align-items: baseline;
  gap: 10px;
  margin-bottom: var(--s-2);
}
.label-row label { margin-bottom: 0; }
/* 「看看效果」：标题删掉之后它得自己靠右，否则贴在最左边、和下面两个通道按钮错位 */
#btnStyleInfo { margin-left: auto; }

/* 三步说明和下面拉开，别贴着卡密 */
.paid-lead { margin: 0 0 var(--s-5); }

/* 卡密下面两段小字之间留一点空，别糊成一段 */
#cardNote { margin-top: var(--s-2); }
#buyNote  { margin-top: 8px; }

/* 付费面板内部：每一块之间统一 18px，不要靠各自的 margin 拼出来 */
.paid-panel > .field { margin-top: 22px; }
.paid-panel > .field:first-child { margin-top: 0; }
.paid-panel > #btnAI { margin-top: 24px; }

/* 「查看效果」挂在通道说明后面（2026-10-23）
   浩哥：「一张示例的效果应该是在『转Q版』里面才会出现的，而不是出现在图片来源的旁边。」
   → 紧跟说明那句话，2px 间距，看起来是那句话的补充，不是独立一块。 */
.chan-demo { margin-top: 2px; }
.chan-demo .link-btn { font-size: inherit; }

/* 「查看效果」跟在说明的句号后面（2026-10-23）
   浩哥：「查看效果这四个字放在那句文案的句号后面，这样可以省一行的高度。」
   → 同一个 <p> 里，文字在 span，按钮紧跟其后，行内不换行。 */
.chan-line { line-height: 1.7; }
.chan-demo-btn {
  margin-left: 4px;
  font-size: inherit;
  vertical-align: baseline;
}
/* ⚠️ 兜底：.link-btn 若被别处设了 display，[hidden] 会失效 */
.chan-demo-btn[hidden] { display: none !important; }

/* ==========================================================================
   Q 版图那一块（2026-10-23）
   浩哥：「Q版屏这里应该有两个图片的显示：一个是原始上传的图片，另一个是转换风格之后的图片。」
   上方 #thumbRow = 原图；下方 #aiRow = Q 版图（生成后才出现）。
   ========================================================================== */
.ai-row { margin-top: 14px; }

.ai-badge {
  display: inline-block;
  align-self: flex-start;
  padding: 1px 8px;
  border-radius: 999px;
  background: var(--accent-wash);
  color: var(--accent-deep);
  font-size: 11px;
  font-weight: 650;
  letter-spacing: .02em;
}

/* 页脚站内导航（2026-10-23）：图纸库 / 分类 / 色号表 */
.footer-links {
  margin: 8px 0 0;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  justify-content: center;
}
.footer-links a {
  color: inherit;
  text-decoration: none;
  border-bottom: 1px solid currentColor;
  opacity: .85;
}
.footer-links a:hover { opacity: 1; }

/* Release 0.10: status and editing tools fit the viewport in restored states. */
.topbar-actions { min-width: 0; }
.status-text { min-width: 0; max-width: 32rem; overflow: hidden; text-overflow: ellipsis; }
.brand-purpose { font-size: .72em; font-weight: 600; }
.edit-bar { min-width: 0; max-width: 100%; }
@media (max-width: 640px) {
  .topbar-actions { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 44px; width: 100%; gap: 8px; }
  .topbar-actions .status-text { grid-column: 1 / -1; width: 100%; max-width: 100%; box-sizing: border-box; white-space: normal; overflow-wrap: anywhere; }
  .topbar-actions .btn-ghost, .topbar-actions .icon-btn { min-height: 44px; justify-content: center; }
  .topbar h1 { white-space: normal; line-height: 1.35; }
  .brand-purpose { display: inline; }
  .edit-row { width: 100%; max-width: 100%; box-sizing: border-box; gap: 4px; }
  .edit-pos { flex: 0 1 94px; width: 94px; min-width: 0; }
  .edit-swatches { min-width: 0; flex-basis: 0; }
  .edit-more { padding-inline: 5px; }
  .edit-hint { flex: 1 1 0; }
  .panel-note { overflow-wrap: anywhere; }
}
.release-links { display: flex; flex-wrap: wrap; gap: 4px 12px; font-size: 12px; line-height: 1.6; }
.release-links a { color: var(--ink-3); text-underline-offset: 3px; }
.panel-foot .release-links { width: 100%; }
@media (max-width:640px) { .release-links a { display:inline-flex;align-items:center;min-height:44px; } }
@media print { .release-links { display:none; } }
.legend-title-row { display:flex;flex-wrap:wrap;align-items:center;gap:12px;justify-content:space-between; }
.legend-title-row h3 { margin:0; }
.legend-title-row .btn-ghost { min-height:44px;font-size:12px; }
.panel-help { flex-basis:100%;font-size:12px;color:var(--ink-3); }
.panel-help summary { cursor:pointer;min-height:24px; }
@media(max-width:640px) { .panel-help summary { min-height:44px;display:flex;align-items:center; } }
@media print { .panel-help {display:none;} }

/* 0.10.1 — a workbench with distinct browse and edit states. */
body:not(.has-pattern) .panel-foot > button { display:none; }
.chan-row .is-unavailable:not([aria-pressed="true"]) { color:var(--ink-3); border-color:var(--line); background:transparent; }
.chan-row .chip { white-space:normal; }
.mobile-flow { display:none; }
.result-exports { display:none; }
.edit-slot { height:78px; position:relative; margin-top:12px; }
.edit-slot .edit-bar { margin:0; }
.edit-intro { color:var(--ink-3); font-size:14px; padding:14px 0; margin:0; }
#btnEdit[aria-pressed="true"] { border-color:var(--accent); color:var(--accent-deep); background:var(--accent-wash); }
/* 进入编辑是这一屏的次主操作：比缩放按钮多一层强调，但不抢打印的主按钮。 */
.app-shell #btnEdit {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  border-color: var(--accent-line);
  color: var(--accent-deep);
  background: var(--surface);
}
.app-shell #btnEdit:hover:not(:disabled) { border-color: var(--accent); background: var(--accent-wash); }
.print-meta { grid-template-columns:repeat(3,minmax(0,1fr)); }
.print-meta .tb-k { font-size:12px; }
.card-hint,.field-note,.range-scale,.panel-note,.preview-note { font-size:13px; line-height:1.65; }
.card { box-shadow:none; }
#canvasScroll { scroll-margin-top:170px; }
@media(max-width:640px) {
 .topbar-actions { position:relative; }
 .mobile-flow:not([hidden]) { display:flex; grid-column:1 / -1; gap:8px; }
 .mobile-flow button { flex:1; min-height:44px; }
 .topbar .status-text { display:none; }
 .topbar { padding:8px 12px; }
 .brand-mark { width:24px; height:24px; }
 .topbar-actions { margin-top:6px; }
 .card-hint,.field-note,.range-scale,.panel-note,.preview-note { font-size:14px; }
 .edit-slot { height:0; margin:0; }
 .edit-intro { display:none; }
 .edit-bar { padding:8px 12px calc(8px + env(safe-area-inset-bottom,0px)); }
 .edit-row { min-height:44px; }
 .edit-row-main { gap:8px; }
 .edit-pos { display:none; }
 .edit-swatches { min-width:0; flex:1; overflow-x:auto; }
 .edit-bar button,.edit-bar .es-chip,.edit-bar .btn-ghost,.edit-bar .edit-more { min-width:44px; min-height:44px; height:44px; padding:8px; font-size:13px; flex-shrink:0; }
 /* 320px 一行放不下「四个带字工具」：手机上工具收成图标
    （选择/橡皮/撤销/重置都是通用图形，title 与 aria-label 保留全名），
    出口在顶栏，工具行只负责改图。全部色号同理只留调色盘图标。 */
 .edit-bar .edit-tool-label,.edit-bar .edit-more-label,.edit-bar .edit-more-arrow { display:none; }
 .edit-bar .edit-tool,.edit-bar .edit-more { width:44px; padding:0; justify-content:center; gap:0; }
 /* 手机上四颗工具是等宽格子（见 workbench.css 的 ≤900 规则），这里不再限宽。 */
 /* 手机上工具行没有出口了，提示也不再挤在 44px 的图标之间 —— 操作名在 aria-label 里。 */
 .edit-bar .edit-hint { display:none; }
 /* 手机和缩放按钮统一触控尺寸；两个出口跟在缩放组后面，不单独占一行。 */
 .preview-bar .preview-exit { height:44px; min-height:44px; padding:0 10px; }
 .has-pattern .panel-foot > #btnPrint,.has-pattern .panel-foot > #btnExport { display:none; }
 .result-exports { display:grid; gap:10px; padding-top:24px; }
 .result-exports button { min-height:44px; }
 .result-exports a { padding:10px 0; color:var(--ink-3); font-size:14px; }
 .preview-bar { scroll-margin-top:160px; }
 .print-area { scroll-margin-top:160px; }
}
@media print { .mobile-flow,.edit-slot,.result-exports { display:none !important; } }
.chan-row .chip { width:auto; min-width:110px; white-space:nowrap; display:inline-flex; align-items:center; gap:8px; }
.chan-row .chip small { font-size:11px; font-weight:400; }
@media(max-width:640px) {
 .edit-slot .edit-bar { height:calc(116px + env(safe-area-inset-bottom,0px)); justify-content:flex-start; padding-top:8px; padding-bottom:calc(8px + env(safe-area-inset-bottom,0px)); }
 .edit-swatches { min-height:46px; }
 body:has(#editBar:not([hidden])) { padding-bottom:calc(132px + env(safe-area-inset-bottom,0px)); }
}
.estimate-note { font-size:13px; line-height:1.6; margin:10px 0 0; color:var(--ink-3); }

/* ==========================================================================
   0.13.0 · 图纸交互增强：聚焦 / 拼搭进度 / 合并 / 分享 / 轻提示
   --------------------------------------------------------------------------
   这一组控件与「图纸台」共用同一套纸面语言：细线、暖白、mono 数字，
   强调色只落在当前动作上。所有浮层（toast / 弹窗）都不进入打印与导出。
   ========================================================================== */

.sr-only {
  position: absolute !important;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ---- 备豆清单：行现在本身是按钮（聚焦），右侧数字多了完成数 ---- */

.legend-item {
  width: 100%;
  min-height: 44px;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
  appearance: none;
  -webkit-appearance: none;
}

.legend-item.is-focus {
  background: var(--accent-wash);
  box-shadow: inset 0 0 0 1px var(--accent-line);
}
.legend-item.is-focus .legend-code { color: var(--accent-deep); }
.legend-item.is-focus:hover { background: var(--accent-wash); }

.legend-meta {
  margin-left: auto;
  display: inline-flex;
  align-items: baseline;
  gap: 5px;
  white-space: nowrap;
}
.legend-count { margin-left: 0; }

.legend-progress {
  font-family: var(--mono);
  font-size: var(--fs-micro);
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}
.legend-progress.is-complete { color: var(--ok); }

.legend-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  flex-wrap: wrap;
  gap: 6px;
}

.build-entry {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 36px;
  padding: 6px 12px;
  border: 1px solid var(--accent-line);
  border-radius: var(--r-full);
  background: var(--surface);
  color: var(--accent-deep);
  font-family: inherit;
  font-size: var(--fs-xs);
  font-weight: 650;
  white-space: nowrap;
  cursor: pointer;
  transition: background var(--dur-micro) var(--ease-out),
              border-color var(--dur-micro) var(--ease-out);
}
.build-entry:hover:not(:disabled) { border-color: var(--accent); background: var(--accent-wash); }
.build-entry.is-building { border-color: var(--accent); background: var(--accent); color: #fff; }
.build-entry.is-building:hover { background: var(--accent-deep); border-color: var(--accent-deep); }
.build-entry:disabled { opacity: .45; cursor: not-allowed; }

/* ---- 色号聚焦条 ---- */

.focus-bar {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 7px 8px 7px 12px;
  border: 1px solid var(--accent-line);
  border-radius: 12px;
  background: var(--accent-wash);
  color: var(--ink-2);
}
.focus-bar[hidden] { display: none; }

.focus-swatch {
  width: 20px;
  height: 20px;
  flex: none;
  border-radius: 50%;
  border: 1px solid rgba(0, 0, 0, .18);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .3);
}

.focus-text {
  min-width: 0;
  font-size: var(--fs-xs);
  font-weight: 650;
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.focus-actions { display: flex; align-items: center; gap: 4px; margin-left: auto; flex: none; }

.focus-merge {
  min-height: 36px;
  padding: 6px 12px;
  border: 1px solid var(--accent-line);
  border-radius: var(--r-full);
  background: var(--surface);
  color: var(--accent-deep);
  font-family: inherit;
  font-size: var(--fs-xs);
  font-weight: 650;
  white-space: nowrap;
  cursor: pointer;
}
.focus-merge:hover:not(:disabled) { border-color: var(--accent); background: var(--surface); }
.focus-merge:disabled { opacity: .45; cursor: not-allowed; }

.focus-clear {
  width: 36px;
  height: 36px;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--ink-2);
  font-size: 21px;
  line-height: 1;
  cursor: pointer;
}
.focus-clear:hover { background: var(--surface); color: var(--accent-deep); }

/* ---- 拼搭条：在内容流里，不覆盖画布 ---- */

.build-bar {
  display: flex;
  flex-direction: column;
  gap: 10px;
  width: 100%;
  padding: 12px 12px 10px;
  border: 1px solid var(--line-2);
  border-radius: 12px;
  background: var(--surface);
  box-shadow: var(--sh-1);
}
.build-bar[hidden] { display: none; }

.build-top { display: flex; align-items: center; gap: 12px; min-width: 0; }
.build-progress { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 8px; }

.build-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px 12px;
  flex-wrap: wrap;
}
.build-progress-text {
  font-size: var(--fs-sm);
  font-weight: 650;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.build-remain {
  font-family: var(--mono);
  font-size: var(--fs-micro);
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}

.build-track {
  position: relative;
  height: 8px;
  border-radius: var(--r-full);
  background: var(--surface-sunk);
  box-shadow: inset 0 0 0 1px var(--line);
  overflow: hidden;
}
.build-fill {
  display: block;
  height: 100%;
  width: 0;
  border-radius: inherit;
  background: linear-gradient(90deg, var(--accent), var(--accent-deep));
  transition: width var(--dur-base) var(--ease-out);
}

.build-exit {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: 44px;
  padding: 8px 12px;
  border: 1px solid var(--line-2);
  border-radius: var(--r-full);
  background: var(--surface);
  color: var(--ink-2);
  font-family: inherit;
  font-size: var(--fs-xs);
  font-weight: 650;
  cursor: pointer;
}
.build-exit:hover { border-color: var(--accent-line); color: var(--accent-deep); background: var(--accent-wash); }
.build-ico { width: 16px; height: 16px; flex: none; }

.build-controls {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  min-width: 0;
}

.build-current {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  max-width: 100%;
  padding: 8px 12px;
  border: 1px solid var(--line-2);
  border-radius: var(--r-full);
  background: var(--surface-2);
  color: var(--ink);
  font-family: inherit;
  font-size: var(--fs-xs);
  font-weight: 650;
  cursor: pointer;
}
.build-current:hover { border-color: var(--accent-line); }
.build-current-swatch {
  width: 18px; height: 18px; flex: none;
  border-radius: 50%;
  border: 1px solid rgba(0, 0, 0, .18);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .3);
}
.build-current-text {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.build-current-chevron {
  width: 14px; height: 14px; flex: none;
  color: var(--ink-3);
  transition: transform var(--dur-micro) var(--ease-out);
}
.build-current[aria-expanded="true"] .build-current-chevron { transform: rotate(180deg); }

.build-swatches {
  display: flex;
  flex-wrap: nowrap;
  gap: 6px;
  width: 100%;
  padding: 2px 0;
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: thin;
  scrollbar-color: var(--line-3) transparent;
  overscroll-behavior-x: contain;
}
.build-swatches[hidden] { display: none; }

.build-sw {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 40px;
  padding: 6px 10px;
  border: 1px solid var(--line);
  border-radius: var(--r-full);
  background: var(--surface);
  color: var(--ink-2);
  font-family: var(--mono);
  font-size: 11px;
  cursor: pointer;
  white-space: nowrap;
}
.build-sw:hover { border-color: var(--accent-line); }
.build-sw[aria-pressed="true"] {
  border-color: var(--accent);
  color: var(--ink);
  box-shadow: inset 0 0 0 1px var(--accent), 0 0 0 2px var(--accent-wash);
}
.build-sw-dot {
  width: 14px; height: 14px; flex: none;
  border-radius: 50%;
  border: 1px solid rgba(0, 0, 0, .18);
}
.build-sw-remain { color: var(--ink-3); font-size: 10px; font-variant-numeric: tabular-nums; }

.build-actions { display: flex; align-items: center; gap: 4px; margin-left: auto; flex-wrap: wrap; }

.build-action {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: 44px;
  padding: 8px 10px;
  border: 1px solid transparent;
  border-radius: var(--r-sm);
  background: transparent;
  color: var(--ink-2);
  font-family: inherit;
  font-size: var(--fs-xs);
  font-weight: 650;
  cursor: pointer;
}
.build-action:hover:not(:disabled) { background: var(--surface-2); border-color: var(--line); }
.build-action:disabled { opacity: .42; cursor: not-allowed; }
.build-share {
  border-color: var(--accent-line);
  background: var(--accent-wash);
  color: var(--accent-deep);
}
.build-share:hover:not(:disabled) { background: var(--accent-wash); border-color: var(--accent); }
.build-share[hidden] { display: none; }

/* 桌面：条形与画布列同宽，不横跨右侧材料栏 */
@media (min-width: 901px) {
  .focus-bar, .build-bar { margin-right: calc(clamp(220px, 20vw, 280px) + 20px); }
}
@media (min-width: 901px) and (max-width: 1150px) {
  .focus-bar, .build-bar { margin-right: calc(240px + 12px); }
}
@media (max-width: 900px) {
  .focus-bar, .build-bar { margin-right: 0; }
  /* ★ 2026-10-25：手机上备豆清单在图纸下方。若聚焦条还留在画布上方，
     用户滚到清单点一行时根本看不见它，也无法清除。
     聚焦期间把它固定在视口底部 —— 行滚出可视区，条仍然在。 */
  .focus-bar:not([hidden]) {
    position: fixed;
    left: 12px;
    right: 12px;
    bottom: max(12px, env(safe-area-inset-bottom));
    z-index: 70;
    width: auto;
    margin: 0;
    box-shadow: var(--sh-2, 0 8px 24px rgba(0, 0, 0, .16));
  }
  .build-actions { width: 100%; margin-left: 0; justify-content: space-between; }
  .legend-actions { width: 100%; justify-content: space-between; }
  .build-entry { min-height: 44px; }
  .focus-merge { min-height: 44px; }
  .focus-clear { width: 44px; height: 44px; }
}

@media (max-width: 400px) {
  .build-action {
    width: 44px;
    min-width: 44px;
    height: 44px;
    padding: 0;
    gap: 0;
  }
  .build-action-label {
    position: absolute;
    width: 1px; height: 1px;
    margin: -1px; padding: 0; border: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
  }
  .build-exit { width: 44px; padding: 0; }
  .build-exit-label {
    position: absolute;
    width: 1px; height: 1px;
    margin: -1px; padding: 0; border: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
  }
  .build-head { gap: 2px 8px; }
  .build-remain { font-size: 10px; }
  /* 320px：聚焦条必须完整显示「只看 H2 · 9,729 颗」。
     收紧内边距与间距比省略颗数好 —— 颗数正是这一条存在的理由。 */
  .focus-bar { gap: 8px; padding-left: 10px; padding-right: 6px; }
}

/* ---- 颜色合并确认 ---- */

.confirm-dialog { width: min(460px, 100%); }
.confirm-foot { display: flex; justify-content: flex-end; gap: 8px; }
.confirm-foot .btn-ghost,
.confirm-foot .btn-primary { min-height: 44px; padding: 10px 18px; }

.confirm-merge {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 10px 0 8px;
  padding: 10px 12px;
  border: 1px solid var(--line-2);
  border-radius: 10px;
  background: var(--surface-2);
}
.confirm-color {
  display: flex;
  align-items: center;
  gap: 7px;
  min-width: 0;
  font-size: var(--fs-xs);
  color: var(--ink-2);
}
.confirm-color i {
  width: 20px; height: 20px; flex: none;
  border-radius: 50%;
  border: 1px solid rgba(0, 0, 0, .18);
}
.confirm-color b { font-family: var(--mono); color: var(--ink); }
.confirm-color em {
  font-style: normal;
  font-family: var(--mono);
  font-size: 11px;
  color: var(--ink-3);
  white-space: nowrap;
}
.confirm-arrow { flex: none; color: var(--ink-3); font-size: 17px; line-height: 1; }
.confirm-note { margin: 0; font-size: var(--fs-xs); color: var(--ink-3); line-height: 1.6; }

/* ---- 轻提示 ---- */

.toast {
  position: fixed;
  z-index: 70;
  left: 50%;
  bottom: 24px;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 8px;
  width: max-content;
  max-width: min(520px, calc(100vw - 24px));
  min-height: 44px;
  padding: 6px 6px 6px 16px;
  border-radius: var(--r-full);
  background: #1c1915;
  color: #f3eee6;
  box-shadow: var(--sh-4);
  font-size: var(--fs-xs);
  animation: toast-in var(--dur-base) var(--ease-out);
}
.toast[hidden] { display: none; }
.toast-text { min-width: 0; line-height: 1.45; }
.toast-action {
  flex: none;
  min-height: 32px;
  padding: 4px 12px;
  border: 0;
  border-radius: var(--r-full);
  background: var(--accent);
  color: #fff;
  font-family: inherit;
  font-size: var(--fs-xs);
  font-weight: 700;
  cursor: pointer;
}
.toast-action:hover { background: var(--accent-deep); }
.toast-action[hidden] { display: none; }
.toast-close {
  flex: none;
  width: 32px;
  height: 32px;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: #f3eee6;
  opacity: .65;
  font-size: 19px;
  line-height: 1;
  cursor: pointer;
}
.toast-close:hover { opacity: 1; background: rgba(255, 255, 255, .12); }

@keyframes toast-in {
  from { opacity: 0; transform: translate(-50%, 8px); }
  to   { opacity: 1; transform: translate(-50%, 0); }
}

@media (max-width: 640px) {
  .toast { bottom: calc(84px + env(safe-area-inset-bottom, 0px)); }
  body:has(#editBar:not([hidden])) .toast {
    bottom: calc(128px + env(safe-area-inset-bottom, 0px));
  }
}

/* ---- 创作记录里的进度标记 ---- */

.hist-build {
  display: inline-block;
  margin-top: 6px;
  padding: 2px 7px;
  border-radius: var(--r-full);
  background: var(--accent-wash);
  color: var(--accent-deep);
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 650;
  font-variant-numeric: tabular-nums;
}
.hist-build.is-done { background: var(--ok-wash); color: var(--ok); }

/* ---- 左侧时间卡：每颗豆耗时 ---- */

#secPerBeadOut::after { content: " 秒"; }

/* ---- 减少动态 ---- */

@media (prefers-reduced-motion: reduce) {
  .build-fill,
  .build-current-chevron { transition: none; }
  .toast { animation: none; }
}

/* ---- 打印：屏幕上的状态与浮层一律不上纸 ---- */

@media print {
  .legend-item {
    width: auto;
    min-height: 0;
    border: 0;
    background: transparent;
    box-shadow: none;
    appearance: none;
  }
  .legend-progress,
  .legend-actions,
  .focus-bar,
  .build-bar,
  .toast,
  .confirm-dialog { display: none !important; }
  .toast { position: static; }
  .toast[hidden] { display: none !important; }
}
