:root{
  /* ============================================================
     ★ 2026-10-12 · 第三阶段 **design tokens 批次 1**（区 A `3s-tokens`）
     ------------------------------------------------------------
     值**一律照抄台账 D1–D10**（出处 `design-out/consult-168-answer.md`
     表 1/2/3/4），这一段里**没有执行侧自己拍的产品决定**。

     ⚠️ **兼容别名**（本批**故意**留的，不是漏删）：
       `--bg` · `--card` · `--sub` · `--warn` · `--shadow` —— 台账把它们标成
       "替换现有"，但它们**此刻仍有引用**（实测 `var(--bg)` 4 · `var(--card)` 20 ·
       `var(--sub)` 54 · `var(--warn)` 10 · `var(--shadow)` 22 = **110 处**），
       而 `tests/pwa.test.js` / `tests/a11y-mobile.test.js` 的断言**按名字读**它们。
       ⇒ 批 1 **只让新名字可用 + 把主操作色接过去**；"改 110 处引用"留到批次 2
         （那和"改 275 处硬编码色"是同一件事）。
       ⇒ 新代码**只用新名字**；旧名字不再新增用法。

     ⚠️ `--ok` → `--success` 已**按 D8 改写引用**；`--pet` 已**按 D9 删除**
        （实测 `var(--pet)` 0 次）；`--pet-h` / `--pet-eye` 是**另外**的变量、**保留**。
     ============================================================ */

  /* —— 颜色 15 个（D1 · D7 · 表 1）—— */
  --bg-base:#f4f7fe;       /* 页面底 */
  --bg-card:#ffffff;       /* 卡片底 */
  --ink:#1f2a44;           /* 正文 */
  --ink-sub:#62708d;       /* 次级文字 */
  --ink-muted:#9aa8c4;     /* 禁用 / 占位 */
  --action:#4a6b4f;        /* 主操作（苔绿）—— D1：`--accent`+`--accent2` 合并到这里 */
  --action-hover:#3d5a42;  /* 主操作悬停 */
  --success:#1f9d6b;       /* 成功态 —— **值取实际成功色**，不是旧 `--ok:#37c391`（D8）*/
  --warning:#b7791f;       /* 警告文字（实际色）*/
  --warning-bg:#fff4e6;    /* 警告底 */
  --error:#d9534f;         /* 危险 / 删除 */
  --accent-warm:#c8a96a;   /* 暖金点缀（描金：稀有 / 成就）*/
  --border:#dfe7f5;        /* 边框 / 分割 */
  --shadow-sm:0 2px 8px rgba(40,70,140,.08);   /* 悬浮提示 */
  --shadow-md:0 8px 24px rgba(40,70,140,.12);  /* 按钮悬停 */

  /* —— A2 收尾件新增的 3 个 token（2026-10-12）——
     ⚠️ **都不引入新色值**（J1a 的 79 种不变）：
        `--info` 用的是**已存在的** `--ink-sub`（与 `--ink-sub` 同值）—— D14 说"颜色种类都入 :root"，
        而 D12 只区分"成功/信息/警告/错误"四个**状态**，没要求第四种**色相**。
        硬塞一个新蓝色会把刚并下来的种类数又加回去（而且信息态与成功态在色相上本就该接近）。 */
  --info:var(--ink-sub);   /* 信息态（toast-info）—— **别名到已存在的 token**，不引入新色值 */
  --toast-bg:#ffffff;      /* **浮起面**的统一底（toast / 次级按钮 / 停用态）—— 与 --bg-card 同值
                              ⚠️ 同值两份是**有意的**：`--bg-card` 是"卡片"，`--toast-bg` 是"浮在内容之上的面"，
                                 将来可能分开取值；现在同值 ⇒ **不新增颜色种类**（J1a 的 79 不变） */
  --toast-shadow:var(--shadow-md);  /* **浮起面**的投影（复用 D5 的 md 档）*/
  --on-action:#ffffff;     /* **彩色按钮上的文字/图标**（对比色）——
                              ⚠️ 为什么另立一个名字而不是直接用 `#fff`：`design-tokens` 的 **J9**
                                 （"每个断点内不写裸色值"）判的是**从 360 档起点到文件尾**这一整段，
                                 而按钮/toast 段就在那之后 ⇒ 段里每出现一个裸 `#fff` 都会把 J9 判红。
                                 另立 token 也**不新增颜色种类**（值本来就是仓里已有的白）。 */

  /* —— 字号 6 档（D2 · 表 2）——
     ⚠️ 本批**只定义这 6 档**；把剩下的 15/17/19/20/22/30px 归并进来是批次 2 的事。 */
  --text-xs:11px; --text-sm:12px; --text-base:13px;
  --text-md:14px; --text-lg:16px; --text-xl:18px;

  /* —— 圆角 4 档（D3 · 表 3）——
     ⚠️ 本批**只定义**：把 132 处 `border-radius` 归并进这 4 档是批次 2 的事
        （J7 已按"取值集合不增长"钉住基线 21 值）。 */
  --radius-sm:8px; --radius-md:12px; --radius-lg:18px; --radius-full:999px;

  /* —— 间距 6 档 · 4px 网格（D4 · 表 3）——
     ⚠️ 本批只被新增的 360 档用（实测 `padding` 里 139/149 处含**非** 4px 网格值 ⇒
        "padding ≥60% 走 token"必须改数值，属视觉变化，归批次 2）。 */
  --space-1:4px; --space-2:8px; --space-3:12px;
  --space-4:16px; --space-6:24px; --space-8:32px;

  /* —— 阴影 3 档（D5 · 表 3）—— */
  --shadow-lg:0 18px 50px rgba(40,70,140,.14);
  --shadow:var(--shadow-lg);   /* 兼容别名（同上）*/

  /* —— 兼容别名 ——
     `--bg`/`--card`/`--warn` 的值**与旧定义逐字节相同** ⇒ 视觉零变化；
     `--accent` 是**唯一**一个值变了的 —— 它就是 D1"主操作色换成苔绿"的落点。 */
  --bg:#f4f7fe;
  --card:#ffffff;
  --warn:#ffb020;
  --sub:var(--ink-sub);   /* → --ink-sub（批次 2 改 54 处引用后删）*/
  --accent:var(--action);   /* ⚠️ 名字**不能删**：`tests/a11y-mobile.test.js:46` 与
       `tests/pwa.test.js` 两条断言都**按名字读**它，而 `a11y-mobile` 不在本区可动清单里。
       实测：改名前 `var(--accent)` 26 处 + `var(--accent2)` 12 处 = **38 处**，
       本批逐处改成了 `var(--action)` ⇒ 如今 `var(--accent)` 的引用数 = **0**。
       📌 副作用（如实记）：键盘焦点轮廓（本文件 `:focus-visible` 那条）也吃这一条
          ⇒ 焦点圈**由蓝转绿**；a11y 语义没变（仍是 3px 实线轮廓）。*/
  /* ⚠️ `--accent2` **已删**（T-A4 合并进 `--action`），原有 12 处引用逐处改掉：
       6 处把 `linear-gradient(…,var(--accent),var(--accent2))` 改成
         `linear-gradient(…,var(--action),var(--action-hover))`；
       另 6 处装饰（`border-left` / 文字色）改成 `var(--action)`。
       渐变**没有**按 `consult-168` ⑤.2 写的"`#4a6b4f` → `#5a7e5f`"取更亮的一头 ——
       因为 `#5a7e5f` **不在 D7 的 15 个 token 值里**，编一个值就是执行侧自己拍板；
       改用了台账里**真实存在**的 `--action-hover:#3d5a42`。那是设计侧"更亮"的意图，
       属取舍 ⇒ 记进报告交汇总侧，本批不自行加值。 */
  /* ============================================================
     跨端（手机 + 电脑）的两个基础量
     ------------------------------------------------------------
     用户说明的定位：「这个 app 是奔着**手机端和电脑端一起开发**的，
     目前是原型开发阶段」。所以这两个量在这里定一次、全站共用：

       --vh      手机浏览器的地址栏会**吃掉一块高度**。
                 CSS 的 `100vh` 在手机上算的是"地址栏**收起时**的高度"，
                 于是 `height:100vh` 的元素底部会被地址栏盖住 ——
                 全屏游戏界面的底栏、聊天输入框正好在那一块，
                 这是"手机上直接不能用"级别的坑。
                 修法：**先写 100vh 兜底，再用 100dvh 覆盖**
                 （dvh = 动态视口高度，支持它的浏览器会正确算）。
                 不支持 dvh 的老浏览器仍拿 100vh —— 不会更糟。

       --safe-*  刘海屏 / 小白条的安全区。`env()` 在不支持的浏览器里
                 取到 0，所以直接用不会出问题。
     ============================================================ */
  --vh:100vh;
  /* ★ 手机软键盘的高度（由 a11y.js 用 visualViewport 算出来写进来）。
     默认 0 —— 桌面上永远是 0，不会影响任何布局。
     ⚠️ 用它而不是 `100vh` 去算可用高度：键盘弹起时 visualViewport 会变小，
        而 window.innerHeight 不会，所以只有它能反映"真正的可视区"。 */
  --kb:0px;
  --safe-t:env(safe-area-inset-top,0px);
  --safe-b:env(safe-area-inset-bottom,0px);
  --safe-l:env(safe-area-inset-left,0px);
  --safe-r:env(safe-area-inset-right,0px);
  /* ============================================================
     v8 全局令牌（M1-13 / S20-A；值逐字取自 docs/design/v8/交互原型/style.css:1）
     ------------------------------------------------------------
     桌面断点（≥1024px）的 v8 外壳用这一组；移动端保留上面的浅色令牌
     （任务书只要求桌面 1024×640 / 1280×720 / 1440×900 的 v8 外壳）。
     命名加 `--v8-` 前缀，避免和既有 15 个语义 token 混成两份同义变量。
     ============================================================ */
  --v8-bg:#11191b;
  --v8-side:#121a1c;
  --v8-panel:#1b2527;
  --v8-panel2:#222e30;
  --v8-line:#ffffff12;
  --v8-text:#e9eee9;
  --v8-muted:#93a29e;
  --v8-mint:#b9d6bb;
  --v8-lilac:#c9bedf;
  --v8-gold:#e2c18c;
  --v8-ink:#243b31;
  --v8-radius:18px;
  --v8-text-base:14px;
  --v8-sidebar:208px;
  --v8-sidebar-min:180px;
  --v8-topbar:72px;
}
@supports (height:100dvh){
  :root{--vh:100dvh}
}
*{box-sizing:border-box}
html,body{height:100%}
body{margin:0;padding-left:var(--safe-l);padding-right:var(--safe-r);font-family:"Microsoft YaHei","PingFang SC","Noto Sans CJK SC",system-ui,sans-serif;color:var(--ink);
  background:radial-gradient(1200px 700px at 20% -10%, #dbe8ff 0%, transparent 60%),
             radial-gradient(900px 600px at 100% 0%, #dbe8ff 0%, transparent 55%), var(--bg);}
.app{max-width:520px;margin:0 auto;min-height:var(--vh);position:relative}
/* ★ 手机键盘弹起时把底部让出来（--kb 由 a11y.js 写）。
   只作用在 .app 上，不动其它层 —— 全屏游戏界面有自己的高度算法。 */
body.kb-open .app{padding-bottom:var(--kb)}
/* 键盘开着时输入区跟着抬起来，用户能看见自己在打什么 */
body.kb-open .composer{padding-bottom:8px}
.view{display:none}.view.active{display:block}
.hidden{display:none !important}
.tiny{font-size:var(--text-xs);color:var(--sub)}
/* CD-00：上传状态行。慢网/断开/失败都要**看得见**（不是只靠转圈），成功也留个绿色终点。 */
.upload-status{font-size:var(--text-xs)}
.upload-status[data-level="slow"]{color:var(--warning);font-weight:600}
.upload-status[data-level="offline"],.upload-status[data-level="failed"]{color:#c0392b;font-weight:600}
.upload-status[data-level="retrying"]{color:var(--warning);font-weight:600}
.upload-status[data-level="done"]{color:var(--success);font-weight:600}
.tiny.center{text-align:center;margin:8px 0 0}

label{display:block;font-size:var(--text-base);color:var(--sub);margin:10px 0 4px}
input,textarea,select{width:100%;margin-top:4px;padding:10px 12px;border:1.5px solid var(--border);border-radius:var(--radius-md);font:inherit;color:var(--ink);background:#fbfdff;outline:none}
input:focus,textarea:focus,select:focus{border-color:var(--action)}
textarea{resize:vertical}
button{font:inherit;cursor:pointer;border:none;border-radius:var(--radius-md);padding:11px 16px}
.primary{width:100%;margin-top:16px;background:linear-gradient(135deg,var(--action),var(--action-hover));color:#fff;font-weight:700}
.primary:active{transform:translateY(1px)}
.ghost{background:#eef2fb;color:var(--ink);padding:var(--space-2) var(--space-3);font-size:var(--text-sm)}
.row-btns{display:flex;gap:var(--space-2);margin-top:12px}
.row-btns .primary{width:auto;margin:0;flex:1}
/* 一行里带输入框时：输入框吃掉剩余宽度，按钮保持自己的尺寸不被压扁换行
   （input 全局是 width:100%，不管的话按钮会被挤成竖排两行） */
.row-btns>input{flex:1 1 auto;min-width:0;width:auto;margin:0}
.row-btns>button{flex:0 0 auto;white-space:nowrap}
/* ============================================================
   ★ 2026-10-11：**拾光页那几排 `.row-btns` 必须能换行**（手机档实测的真问题）
   ------------------------------------------------------------
   症状（手机档 390×844 的原始数字）：`documentElement.scrollWidth=548` vs `clientWidth=390`
   —— 溢出 158px。最宽的那一层就是这一排：`<input#sg-kw>` + 8 颗筛选按钮
   （全部/通关/元碎片/初到/凝出生灵/宠物/自己记/快照）**一条线排下去**，
   最右边那颗「快照」的右沿 = 548。
   🔴 后果不止"多点一下横向滚动"：Chrome 会**自动"缩小以适配"**（实测 inner 变成 548×1186，
      而布局视口 390×844）⇒ 连 `position:fixed` 的底栏都被摆到**布局视口之外**（y=1133..1186），
      六颗 tab 的中心逐个落空（那一刻量到的 `mobile_bottom_nav` 是"中心不在视口内"）。
   ⚠️ 为什么是 `.row-btns` 本身（第 61 行那条 `display:flex` **没写换行**）：
      它的设计前提是"两三个短按钮"（见 1005 行那一节对 `#jiezi-first-card` 的同类处理）。
      这一排是**1 个输入框 + 8 颗按钮**，远远超出"两三个"。
   ⚠️ 作用域只给拾光页：别的页的 `.row-btns` 布局一个字不动（避免为了这一处动全局）。
   ============================================================ */
#shiguang-root .row-btns{flex-wrap:wrap}
.card{background:var(--card);border-radius:var(--radius-lg);padding:var(--space-4);box-shadow:var(--shadow)}
.hint{color:var(--sub);font-size:var(--text-base)}
/* 说明文字里的重点：.hint 用的是次要色（--sub），光靠 <b> 的加粗在 13px 下
   几乎看不出来 —— 给它正文色（--ink），让"经历/感受/记忆/思维/信念/人格"
   这几个词真的被看见。（记忆页那句介绍就是靠这个读出因果链的。）
   ⚠️ 我第一版写的是 var(--fg)，而它**不存在** —— 这里可用的正文色是 --ink。 */
.hint b{color:var(--ink);font-weight:600}

/* ==============================================================
   无障碍：读屏专用 + 键盘焦点可见 + 尊重"减少动效"
   --------------------------------------------------------------
   补评估里列的欠账 #3。原来实测：0 个 focus-visible、0 个 sr-only。
   ============================================================== */

/* 只给读屏看的文字（视觉上不占位）——
   ⚠️ 不能用 display:none / visibility:hidden：那样读屏也读不到。
      必须用"裁成一个点"的老办法。 */
.sr-only, #a11y-live{
  position:absolute !important; width:1px; height:1px; padding:0; margin:-1px;
  overflow:hidden; clip:rect(0 0 0 0); clip-path:inset(50%); white-space:nowrap; border:0;
}
/* 跳转到主内容：键盘用户一进来就能跳过导航那一串按钮。
   默认不可见，聚焦时才出现 —— 这是标准做法。 */
.skip-link{
  position:absolute; left:8px; top:-44px; z-index:9999;
  padding:8px 14px; border-radius:0 0 10px 10px;
  background:var(--action); color:#fff; font-size:var(--text-base); text-decoration:none;
  transition:top .12s ease;
}
.skip-link:focus{ top:0 }

/* ★ 键盘焦点要看得见。
   用 :focus-visible 而不是 :focus —— 后者鼠标点击也会触发，
   于是"点一下按钮就闪一圈轮廓"，视觉上很吵。 */
:focus-visible{
  outline:3px solid var(--action);
  outline-offset:2px;
  border-radius:6px;
}
/* 深色按钮（主色底）上的焦点改用白色轮廓，否则看不见 */
.btn:focus-visible, .sbtn.ok:focus-visible, .tab.active:focus-visible{
  outline-color:#fff;
  box-shadow:0 0 0 5px rgba(91,140,255,.55);
}
/* 标题被程序聚焦（页面切换后）时不要画出难看的框 ——
   它是"落点"，不是可操作元素。 */
h1:focus, h2:focus, h3:focus{ outline:none }

/* ★ 尊重系统的"减少动效"。
   桌宠会走动、气泡会浮、进度条会动 —— 对前庭功能敏感的人这些动效会不适。
   ⚠️ 不是"关掉一切"：只把**动画时长压到几乎为零**，状态变化仍然发生。 */
@media (prefers-reduced-motion: reduce){
  *, *::before, *::after{
    animation-duration:.01ms !important;
    animation-iteration-count:1 !important;
    transition-duration:.01ms !important;
    scroll-behavior:auto !important;
  }
  /* 桌宠的"走动"是 JS 驱动的，靠这个变量让引擎自己停下（见 deskpet.js） */
  .pet{ transition:none !important }
}
/* 插件配置面板底部那句"图会发往哪" —— 这是用户决定要不要开"画面外发"的唯一依据，
   所以给它一点警示色，别混在说明文字里被划过去。 */
.cfg-endpoint{margin-top:8px;padding:7px 9px;border-radius:9px;
  background:rgba(184,134,11,.10);border-left:3px solid var(--warn);line-height:1.6}
.cfg-endpoint b{color:var(--ink);word-break:break-all}

.create-card{background:var(--card);border-radius:22px;padding:22px;box-shadow:var(--shadow);margin:16px}
.create-card h1{margin:.1em 0 .2em;font-size:22px}
.color-row{display:flex;gap:10px;margin-top:8px}
.swatch{width:30px;height:30px;border-radius:50%;cursor:pointer;border:3px solid transparent}
.swatch.on{border-color:var(--ink)}
.model-box{border:1.5px dashed var(--border);border-radius:14px;padding:10px 12px 14px;margin-top:14px}
legend{font-size:var(--text-sm);color:var(--sub);padding:0 6px}
.stage{display:flex;flex-direction:column;align-items:center;gap:14px;padding-top:40px}
.pet-wrap{position:relative;cursor:pointer}
.bubble{max-width:400px;background:var(--card);border-radius:16px;padding:var(--space-3) var(--space-4);box-shadow:var(--shadow);font-size:15px;line-height:1.6}
.bubble.big{font-size:17px}

/* 宠物 */
.pet-svg{width:100%;height:100%;overflow:visible;display:block}
#pet-g{transform-origin:100px 130px}
@keyframes breathe{0%,100%{transform:scale(1,1)}50%{transform:scale(1.03,0.97)}}
.state-idle #pet-g{animation:breathe 2.8s ease-in-out infinite}
@keyframes bounce{0%,100%{transform:translateY(0)}50%{transform:translateY(-8px)}}
.state-listen #pet-g{animation:bounce .7s ease-in-out infinite}
@keyframes tilt{0%,100%{transform:rotate(-3deg)}50%{transform:rotate(3deg)}}
.state-think #pet-g{animation:tilt 1.1s ease-in-out infinite}
@keyframes hop{0%,100%{transform:translateY(0) scale(1)}30%{transform:translateY(-14px) scale(1.05)}}
/* 🔴 点击反馈单列一个状态（2026-10 第 112 轮 · 设计侧 consult-57 判 (甲)+(乙)）
   ──────────────────────────────────────────────────────────────
   为什么拆：`.state-done` 曾被**四处**调用（poke / 聊天回复 / mountPet / switchForm）
   ⇒ 它上面挂 `hop` 会让**每次聊天回复都"弹两下"** —— 那是意外，不是设计。
   ⇒ 拆开后 `hop` **只**属于"点击回应"。
   ⚠️ 时长从 `.6s×2`（总 1200ms）改到 **`.2s×1`（总 200ms）** ——
      点击反馈的金标准是 ~100ms，200ms 是上限；1200ms 早已越出"反馈"区间。 */
.state-poke #pet-g{animation:hop .2s ease-out 1}
@keyframes walk{0%,100%{transform:translateY(0) rotate(-2deg)}50%{transform:translateY(-3px) rotate(2deg)}}
.state-walk #pet-g{animation:walk .5s ease-in-out infinite}
@keyframes doze{0%,100%{transform:rotate(-6deg) translateY(4px)}50%{transform:rotate(-6deg) translateY(8px)}}
.state-sleep #pet-g{animation:doze 3.6s ease-in-out infinite;opacity:.85}
.blink #eye-l,.blink #eye-r{transform:scaleY(.1);transform-origin:center}
#eye-l,#eye-r{transition:transform .08s}
.dots{position:absolute;top:2px;right:4px;display:none;gap:var(--space-1)}
.state-think .dots{display:flex}
.dots i{width:8px;height:8px;border-radius:50%;background:#b9c6e6;display:block;animation:dotup .9s infinite}
.dots i:nth-child(2){animation-delay:.15s}.dots i:nth-child(3){animation-delay:.3s}
@keyframes dotup{0%,100%{opacity:.3;transform:translateY(0)}50%{opacity:1;transform:translateY(-4px)}}

/* 外壳 */
.shell{display:flex;flex-direction:column;min-height:var(--vh)}
.shell:not(.hidden){animation:fade .22s ease}
.appbar{display:flex;justify-content:space-between;align-items:center;padding:14px 16px 10px}
.who{display:flex;align-items:center;gap:var(--space-2);font-weight:700}
.status{font-size:var(--text-sm);font-weight:400;color:var(--sub);background:#eef2fb;border-radius:var(--radius-full);padding:2px 10px}
.mood{font-size:var(--text-sm);font-weight:400;color:#7a5a4a;background:#fff3ec;border:1px solid #ffe2d3;border-radius:var(--radius-full);padding:2px 9px;cursor:help}
.mood.low{color:#7a5a6a;background:#f6eef6;border-color:#eadff0}
.acts{display:flex;gap:var(--space-2)}
/* ============================================================
   ★ 2026-10-11：这 84px 是**给最后一排控件留的命**（手机档 ② 号缺陷）
   ------------------------------------------------------------
   症状（手机档原始数字）：滚到底时最后一排控件**压在 fixed 底栏下面**、点不到 ——
     · 聊天页 `#btn-extract` 中心 [63,828] → 真触摸落到的却是 `button.tab「🏠芥子」`
     · 元碎片页卡片上那 4 颗按钮中心 y=814 → 同样落到 tab 上
     判据那边同一条：`mobile_page_usable` 的"可见控件的中心被谁盖住"逐条点名。
   根因**不是**"忘了写 padding"（padding 一直在），而是**这 84px 落在哪儿**：
   它只有在 `.pages` 这个盒子**长到装得下内容**时，才排在**内容的下面**；
   盒子一旦停在视口高、内容却从盒子里溢出去，这 84px 就落在**溢出内容之上**，谁也护不住。
   （本轮实测的触发原因：`public/index.html` 里多了一个结束标签 ⇒ 解析器把
     `<main class="pages">` 提前关掉、五页掉到 `.app` 底下 ⇒ 这个盒子里**根本没有内容**。
     那一处已由"芥子入口改切页"那件删掉；静态体检 `node tools/html-nesting-probe.js` 现在自洽。
     ⚠️ 我在这条修好之前写过一个过渡规则 `.app > .page{padding-bottom:84px}` ——
        结构修好之后它**恒不命中**（页面不再直接挂在 `.app` 下）⇒ 按"死样式不留"的规矩**已删掉**，
        此处如实记一笔，免得下一个人以为它凭空消失。）
   ⚠️ 84 = 底栏高（53～54px）+ 约 30px 余量；底栏自己没有安全区内边距，所以这 84 是唯一那道缓冲。
   ============================================================ */
.pages{flex:1;padding:0 16px 84px;display:grid;
  /* 🔴 列宽 = **容器宽**（不是"内容更宽的那个"）。
     默认的 `auto` 会让那一格被**两页里更宽的那个**撑开；转场期两页同时在版面里
     ⇒ 文档横向溢出 ⇒ Chrome"缩小以适配"且**不复位**（详见上面那段量到的数字）。
     `minmax(0,1fr)` 把最小值显式钉成 0 ⇒ 列宽只由**容器**决定，与里面放了什么无关。 */
  grid-template-columns:minmax(0,1fr);
  /* 🔴 第二道（**必须的** —— 只钉列宽不够，实测过）：
     列宽钉死之后，如果**某一页自己**就比视口宽（页里有个宽东西），那一页照样会
     横向溢出、把 `documentElement.scrollWidth` 顶出去 ⇒ Chrome 还是"缩小以适配"。
     实测（`tools/pageswitch-nav-zoom-probe.js` 的机制正对照，390 档）：
       · 只钉列宽：`#page-jz-dressup` 压成 466 ⇒ 峰值 `sw=482`（**溢出 92px**）
       · 加这一道之后：同前提 ⇒ 峰值 `sw=390`（**溢出 0**）
     ⇒ `overflow-x:clip` = **兜底一道**：这一格里的任何东西都不许把**文档**撑宽。
       ⚠️ 为什么是 `clip` 而不是 `hidden`：`hidden` 会把这个盒子变成**可滚动容器**
       （`overflow-y` 会跟着算成 `auto`）⇒ 底栏留白那 84px 与 `.page` 里那些绝对定位的
       摆件（`.jz-main-btn`）会多出一个"可以滚一下"的行为。`clip` **不产生滚动容器**，
       只是把画到外面的部分裁掉 —— 这正是这里要的。
       ⚠️ 它裁掉的只可能是**本来就该修的那种溢出**（本仓纪律：横向溢出算缺陷，
       `tools/sixpage-e2e.js` 手机档就拿它当判据）；稳定态实测 `scrollWidth===clientWidth`
       （390/390）⇒ 这一条**在稳定态什么都不裁**（不会把正常内容剪掉）。
       ⚠️ 只写 `overflow-x`，**不写** `overflow:clip` —— 竖向该滚还得滚。 */
  overflow-x:clip}
/* 🔴 **页面切换的淡出 + translateY**（2026-10 第 159 轮 · 设计侧 T2 批次① 点名的那件）
   ──────────────────────────────────────────────────────────────────────
   原来这里是**硬切**：`.page` 只有淡入、**没有淡出** ——
   切页时旧页「啪」地消失。而设计侧明确把「页面淡出 + translateY」列进了 T2 批次。
   ⚠️ **一个我自己踩的坑（同一类第 4 次）**：
      我原先把"旧版那条声明"**原样**抄进了本注释（连冒号都保留）——
      而清单工具是**按文本扫**的 ⇒ 它把注释里的那段**当成一条真动效**收进清单
      ⇒ 多出一条"待人工判" ⇒ 守卫**假红**。
      🔑 前三次：断言命中自己的注释 · 剥注释前先匹配 · 扫描器读注释。
      ⇒ **注释里不要写代码样式的文本**；要引用就**用文字描述**，不要照抄。
      （本段我改了两遍才对 —— 第一遍连"我抄了它"这句话里都还留着那个冒号写法。）

   🔑 做法：`.pages` 改 **grid**，两页**占同一格**（`grid-area:1/1`）
      ⇒ 旧页可以**留在原地淡出**、同时新页淡入 ⇒ **真正的交叉淡入淡出**，
        而且**不产生布局跳动**（若两页都在普通流里会上下堆叠 ⇒ 跳一下）。
   🔴🔴 **2026-10-11 补的那一条（真缺陷：切页之后整个页面被 Chrome 永久缩小）**
      ──────────────────────────────────────────────────────────────────
      别人量到的原始数字：切到 `#page-jz-dressup` 那 200ms 里
        `documentElement.scrollWidth` **482** vs 布局视口 **390** ⇒ 横向溢出
        ⇒ Chrome"**缩小以适配**"（`visualViewport.scale≈1.236`、`innerWidth` 390→482）
        ⇒ 页头那颗「‹ 回芥子」被排到 x=**412..482**（**在布局视口之外** ⇒ 手指点不到）。
      机制（已量清）：`.page{grid-area:1/1}` 把两页放进**同一格**，而 grid 的列宽默认是
        `auto` ⇒ 一格的行内尺寸取**两页里更宽的那个**；转场那 200ms 里
        旧页（`.leaving`）与新页**同时在版面里** ⇒ 格子被更宽的那页撑开 ⇒ 整份文档横向溢出。
      🔑 修法：**把列宽钉死成"容器宽"**（`minmax(0,1fr)`，见下面那条规则）——
        列宽**不再随内容（哪一页更宽）变化** ⇒ 转场期与稳定态**同一套几何** ⇒ 不会溢出。
        ⚠️ 为什么不是"给 `.leaving` 那页 `position:absolute`"：那要处理它的定位上下文
        （`.pages` 没有 `position:relative`），而 `.page` 里有绝对定位的摆件
        （`.jz-main-btn` 就靠 `#page-home.jz-house` 这个祖先定位）⇒ 改定位上下文
        会**顺带动别人已经量过的几何**。钉列宽是"一个声明、几何不变"的那条。
        ⚠️ **淡出动画一个字没动**（`.page.leaving` 的 `animation:fade-out` 照旧）——
        本条只是**不让它参与撑开格子**，它照样显示、照样淡出。
   ⚠️ `.leaving` 带 **`pointer-events:none`** —— 否则淡出中的"幽灵页"还能被点到
      （那是"看得见的已经不是它了、点下去还是它"那类最难查的 bug）。
   ⚠️ `.18s` / `.16s` 都是**反馈级**（≤200ms）：切页是**点一下就发生**的事，
      拖过 200ms 会让 tab 显得迟钝。 */
.page{grid-area:1/1;display:none;animation:fade .18s ease}
.page.active{display:block}
.page.leaving{display:block;pointer-events:none;animation:fade-out .16s ease forwards}
@keyframes fade{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
@keyframes fade-out{from{opacity:1;transform:none}to{opacity:0;transform:translateY(-4px)}}
.page-head{display:flex;justify-content:space-between;align-items:center;margin:4px 0 10px}
.page-head h2{font-size:17px;margin:0}

/* 芥子 · 桌宠那一层 —— **它不再是一间"屋子"**（2026-10-10）
   ──────────────────────────────────────────────────────────────
   所有者原话（附实机截图）：「**只留这个屋子**，**这个黄色的背景是最早的 1d 屋子，
   删掉啊**」。
   🔴 原来 `.scene` 就是那间 1D 屋子：`height:330px` + 黄橙渐变（`#fff1d8→#e9b57e`）
      + 窗 / 帘 / 阳光束 / 灯 —— 它和 3D 屋子**并排占版面**（同一屏里两间屋子），
      而**朏朏本人住在旧的那一间**（`#pet` 是 `.scene` 的孩子）
      ⇒ 她在 3D 屋子里看着像"贴上去的"：**两套坐标**，她根本不在那间屋子里。
   ⇒ 现在 `.scene` 只是一个**中性的宿主容器**：**没有背景、没有固定高度、不占版面**。
      · 屋子档（`#page-home.jz-house`）：由 `jiezi-view.js` 的 `placeRoomLayer()`
        把它摆成**盖在屋子画面上的透明浮层**（于是桌宠的 px 坐标 = 屋子画布的
        像素坐标 —— 她真的站在屋里，见下面「屋子档」那一节）；
      · 没有屋子可画时：它只是一块**透明**的地，高度用 `min-height`
        （她得有个落脚的地方，但它透明、也**远小于**原来那 330px）。
   ⚠️ 窗 / 帘 / 阳光束 / 灯 / 毯 / 盆栽六件（`.window` `.curtain` `.sunbeam`
      `.lamp` `.rug` `.plant`）**一并删掉**：它们是那间 1D 屋子的零件。
      全仓库扫过：只有 `.sunbeam` 在 `index.html` 里出现过一次（已删），
      其余五条**一条引用都没有**（死样式）。
   ⚠️ `#room-items` 是旧摆件层，`position:absolute;inset:0`（见下面它自己那条）
      ⇒ 宿主必须留 `position:relative` 当它的包含块。 */
.scene{position:relative;min-height:190px;margin-top:6px}
/* 朏朏站的**那条地带**（`#pet-stage`）：`deskpet.js` 把 `.pet-wrap` 的父节点当"场景"
   （`sceneWidth()` / `minX()` / `maxX()` 全按它算，`apply()` 往 `#pet` 写 px 的 `left`）
   ⇒ 这条地带的宽高**就是她的活动范围** —— 屋子档下把它摆到屋子的前场，
      她就在屋里溜达（而不是在屋子下面的另一套坐标里溜达）。
   默认（没有屋子可画）：贴在宿主下沿，高度 = 她落脚需要的 190px。 */
#pet-stage{position:absolute;left:0;right:0;bottom:0;height:190px}
.home-pet{position:absolute;bottom:20px;left:50%;width:150px;height:150px;transform:translateX(-50%);transition:left 2.4s ease-in-out}
.home-bubble{position:absolute;top:12px;left:50%;transform:translateX(-50%);max-width:78%;font-size:var(--text-base);padding:9px 13px}

/* 聊天 */
.chat-log{list-style:none;margin:6px 0 0;padding:0 2px;max-height:calc(var(--vh) - 220px);overflow:auto}
/* ============================================================
   ★ 2026-10-11：聊天页**整页装进"可用高度"**，余量由消息列表吸收（手机档 ② 的第二种形态）
   ------------------------------------------------------------
   症状（手机档 390×844 的原始数字，判据 `mobile_page_usable` 的"页顶"那一遍）：
     `button#btn-extract「✦ 从对话提炼 skill」` 中心 @[80,822] → **点到的是 `span「聊天」`**
     —— 它整条（y 806..838）落在 fixed 底栏（顶端 y=790）**下面**。
   为什么"底部留了 84px 还是压住"：留白只在**滚动到底**时管用；而这一遍是**页顶**，
     内容比"视口 - 顶栏 - 底栏"高 ⇒ 最后一排必然先出现在底栏底下。
   实测这一页的账（`tools/mobile-floor-probe.js --seed-chat`，发一条消息后量）：
     整页 782 = 立绘 190 + 关系条 30 + 关系说明 35 + 消息列表 141 + 提示 34 + 首日卡片 240
              + 输入行 47 + 提炼那排 32 + 各段间距
     可用高度 = 844 - 顶栏 56 - 底栏 54 = 734 ⇒ **超了 48px**。
   ⇒ 修法：把这一页**钉在可用高度以内**（`max-height`），并让**消息列表**做那个能屈能伸的
     （`flex:1 1 auto` + `min-height:0`，它本来就有 `overflow:auto`）——
     这正是聊天界面该有的形状：输入行与操作排在底栏之上**始终可见**，长消息在列表里自己滚。
   ⚠️ 为什么不是"把消息列表的 `max-height` 调小"：实测那一页的列表只有 141 高（没到它自己的
     上限 624）—— 撑高页面的是**立绘 + 首日卡片**那两块，调列表的帽子对这里**一点用都没有**
     （这就是"改了一个看着像原因的地方、数字一动不动"的形状，记一笔免得下一个人重走）。
   ⚠️ 118 = 顶栏 56 + 底栏 54 + 8 余量（数字都是量出来的；底栏自己做在 53～54）。
   🔴 **选择器必须带 `.active`**（`#page-chat.active`，不是 `#page-chat`）：
      这条是 id 选择器，而"没轮到的页不显示"是 `.page{display:none}`（**类**选择器）——
      id 天生压过类 ⇒ 写成 `#page-chat{display:flex}` 会让聊天页**永远显示**，
      于是它与当前那一页同处 `grid-area:1/1`、两层叠在一起：
      实测（本轮真踩了，判据当场抓出来）工坊页那排 `.segbtn「技能 / 玩法」` 的中心
      @[151,81] / @[240,81] 点到的是 **`img#chat-portrait-img`**（聊天页的立绘）
      ⇒ `workshop:mobile_page_usable` 在两档都变红（还连带 `mobile_touch`：那一次真触摸
        落到立绘上，段没切、`changed=false`）。⇒ 一个字之差，**布局只在这一页真的轮到时才接管**。
   ⚠️ 列方向的 flex **不会**撑宽页面（上面芥子那一节记过"改成 flex 之后宽度被撑到 1404"的坑：
     那是**行方向**、且子元素是一条不换行的长带；这里是列方向，横向最小内容宽度取各行的最大值）。
   ============================================================ */
#page-chat.active{display:flex;flex-direction:column;max-height:calc(var(--vh) - 118px)}
#page-chat .chat-log{flex:1 1 auto;min-height:0}
.chat-log li{margin:8px 0;display:flex}
.chat-log .me{justify-content:flex-end}
.chat-log .bub{max-width:76%;padding:10px 14px;border-radius:16px;font-size:var(--text-md);line-height:1.55;box-shadow:var(--shadow)}
.chat-log .me .bub{background:linear-gradient(135deg,var(--action),var(--action-hover));color:#fff;border-bottom-right-radius:5px}
.chat-log .fr .bub{background:var(--card);border-bottom-left-radius:5px}

/* ==============================================================
   微信式气泡：头像 + 气泡 + 时间
   --------------------------------------------------------------
   用户给的参考图就是这个形态。改它的理由：一眼看出谁在说、什么时候说的；
   纯文本列表看久了会分不清哪些是她说、哪些是系统提示。
   ★ 系统行（亲密度加减）单独样式、居中、灰字 —— **绝不冒充她说的话**，
     否则"亲密度 -30"会看起来像她在嫌你。
   ============================================================== */
.chat-log li{align-items:flex-start;gap:var(--space-2)}
.chat-log .ava{flex:0 0 34px;width:34px;height:34px;border-radius:9px;
  display:flex;align-items:center;justify-content:center;font-size:var(--text-base);
  background:rgba(127,127,127,.16);color:var(--sub);user-select:none}
.chat-log .ava.mine{background:linear-gradient(135deg,var(--action),var(--action-hover));color:#fff}
.chat-log .col{display:flex;flex-direction:column;gap:2px;min-width:0;max-width:78%}
.chat-log .me .col{align-items:flex-end}
.chat-log .col .bub{max-width:100%}
.chat-log .tm{font-size:var(--text-xs);color:var(--sub);padding:0 4px}
/* 图片气泡（预留位：她真的能发图片时走这里） */
.chat-log .bub-img{max-width:100%;max-height:220px;border-radius:14px;box-shadow:var(--shadow);display:block}
/* 图片角标：让用户能分辨"她自己画的"与"生图服务出的" ——
   两者观感差很多，不标出来用户会以为是同一种东西。 */
.chat-log .pic-by{font-size:var(--text-xs);color:var(--sub);padding:0 4px}
.chat-log .me .pic-by{align-self:flex-end}
/* 系统行 */
.chat-log li.sys{justify-content:center;margin:6px 0}
.chat-log li.sys>span{font-size:var(--text-xs);color:var(--sub);background:rgba(127,127,127,.12);
  padding:2px 10px;border-radius:9px}
.composer{display:flex;gap:var(--space-2);margin-top:12px}
.composer input{flex:1}
.composer .primary{width:auto;margin:0;padding:11px 20px}

/* 分段 / 子页 */
.seg{display:flex;gap:6px;background:#dfe7f5;border-radius:var(--radius-md);padding:var(--space-1);margin:4px 0 12px}
.segbtn{flex:1;background:none;border-radius:9px;padding:var(--space-2) var(--space-1);font-size:var(--text-base);color:var(--sub)}
.segbtn.active{background:#fff;color:var(--ink);font-weight:700;box-shadow:0 2px 8px rgba(40,70,140,.12)}
.seg.small{padding:3px;margin:0 0 10px}
.seg.small .segbtn{font-size:var(--text-sm);padding:6px 4px}
.skl{display:flex;flex-wrap:wrap;gap:var(--space-1);margin:4px 0 2px}
.skl i{font-style:normal;font-size:var(--text-xs);background:#eef2fb;color:#5b6b8c;border-radius:6px;padding:1px 6px}
.sech{font-size:var(--text-base);color:var(--sub);margin:14px 0 6px}
pre.md{background:#fbfdff;border:1px solid #dfe7f5;border-radius:10px;padding:8px 10px;font-size:var(--text-xs);line-height:1.5;white-space:pre-wrap;max-height:150px;overflow:auto;margin:6px 0 0;color:#3b4a66;font-family:inherit}
.sbtn.ok{background:#e6f8ef;color:var(--success)}
.sbtn[disabled]{opacity:.6;cursor:default}
.wshead{grid-column:1/-1;font-size:var(--text-sm);color:var(--sub);margin:8px 0 -2px;font-weight:700}
.mgr{display:flex;align-items:center;gap:var(--space-2);margin-top:6px}
.mgr .switch{width:40px;height:24px}
.mgr .switch::after{width:18px;height:18px}
.mgr .switch.on::after{left:19px}
.mgr .rm{background:#fdecec;color:var(--error);padding:6px 10px;font-size:var(--text-sm);border-radius:10px}
.sub{display:none}.sub.active{display:block}
.chk{display:flex;flex-wrap:wrap;gap:var(--space-2);margin-top:6px}
.chk label{display:flex;align-items:center;gap:6px;width:auto;margin:0;background:#f4f7fe;border-radius:10px;padding:7px 10px;font-size:var(--text-sm);color:var(--ink)}
.chk input{width:auto;margin:0}
/* 已卸载但仍被框架引用的插件：保留在列表里但明显区分开，
   避免用户以为它还在生效。不隐藏是因为隐藏=保存一次就静默抹掉依赖。 */
.chk label.off{background:#ffffff;color:#8a8a94;text-decoration:line-through}
.chk label.off input{cursor:not-allowed}

/* 游玩时的局面面板（狼人杀这类有裁判插件的框架）。
   目的：让玩家随时知道"现在是什么阶段、我是谁、能做什么"。 */
#play-board{margin:0 0 10px;padding:10px 12px;background:#f7f4ef;border:1px solid #dfe7f5}
.play-board-top{display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap;margin-bottom:6px}
.pb-phase{font-weight:700;font-size:var(--text-base);background:#4a3f35;color:#fff;border-radius:var(--radius-sm);padding:2px 9px}
#pb-me{color:#3d3428;margin-bottom:2px}
#pb-alive{color:#6b6156;margin-bottom:8px}
#pb-actions{gap:6px}
#pb-actions button{padding:6px 11px;font-size:var(--text-sm)}
#pb-actions .pb-abstain{opacity:.85}

/* 卡片网格 */
.grid{display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3);margin-top:12px}
.skill{background:var(--card);border-radius:16px;padding:14px;box-shadow:var(--shadow);display:flex;flex-direction:column;gap:6px}
.skill .ic{font-size:22px}
.skill .nm{font-weight:700;font-size:var(--text-md)}
.skill .ds{font-size:var(--text-sm);color:var(--sub);line-height:1.5;flex:1}
.skill .mt{font-size:var(--text-xs);color:var(--ink-muted)}
.skill .sbtn{margin-top:6px;padding:var(--space-2);font-size:var(--text-sm);background:#eef2fb;color:var(--ink);border-radius:10px}
.skill .sbtn.installed{background:#e6f8ef;color:var(--success)}
.skill .sbtn.play{background:linear-gradient(135deg,var(--action),var(--action-hover));color:#fff}

/* 技能管理 */
/* ★ SB-03：插件兼容面板（独立文件 plugin-compat-panel.js 渲染） */
.plugin-compat{margin:12px 0;padding:12px 14px;border-radius:14px;background:var(--card);box-shadow:var(--shadow)}
.plugin-compat .pc-list{list-style:none;margin:8px 0 0;padding:0}
.plugin-compat .pc-item{display:flex;flex-direction:column;gap:2px;padding:8px 10px;border-radius:10px;margin-bottom:6px;border-left:4px solid var(--line);background:rgba(0,0,0,.02)}
.plugin-compat .pc-item.err{border-left-color:var(--error)}
.plugin-compat .pc-item.warn{border-left-color:var(--warning)}
.plugin-compat .pc-item b{font-size:var(--text-md)}
.manage-list{list-style:none;margin:0;padding:0}
.manage-list li{display:flex;align-items:center;gap:10px;background:var(--card);border-radius:14px;padding:12px 14px;margin-bottom:10px;box-shadow:var(--shadow)}
.manage-list .nm{flex:1}
.manage-list .nm b{display:block;font-size:var(--text-md)}
.manage-list .nm span{font-size:var(--text-xs);color:var(--sub)}
.switch{width:44px;height:26px;border-radius:var(--radius-full);background:#cfd8ea;position:relative;cursor:pointer;flex:none}
.switch::after{content:"";position:absolute;top:3px;left:3px;width:20px;height:20px;border-radius:50%;background:#fff;transition:.18s}
.switch.on{background:var(--success)}.switch.on::after{left:21px}
.manage-list .rm{background:#fdecec;color:var(--error);padding:7px 10px;font-size:var(--text-sm)}
.ktag{font-size:var(--text-xs);border-radius:var(--radius-full);padding:3px 9px;background:#eef2fb;color:#5b6b8c;flex:none}
.ktag.ok{background:#e6f8ef;color:var(--success)}
.ktag.warn{background:#fff4e6;color:var(--warning)}

/* 记忆页 */
.mem-stats{display:flex;gap:10px;margin:10px 0}
.mstat{flex:1;background:var(--card);border-radius:14px;padding:var(--space-3);text-align:center;box-shadow:var(--shadow);font-size:var(--text-sm);color:var(--sub)}
.mstat b{display:block;font-size:20px;color:var(--ink)}
.mem-summary{background:var(--card);border-radius:14px;padding:12px 14px;font-size:var(--text-base);line-height:1.6;box-shadow:var(--shadow);white-space:pre-wrap;min-height:44px;color:#3b4a66}
.mem-facts{list-style:none;margin:0;padding:0}
.mem-facts li{display:flex;gap:10px;background:var(--card);border-radius:var(--radius-md);padding:10px 12px;margin-bottom:8px;box-shadow:var(--shadow);font-size:var(--text-base)}
.mem-facts li b{color:var(--action);flex:none;min-width:56px}
.mem-short{list-style:none;margin:0;padding:0}
.mem-short li{background:var(--card);border-radius:var(--radius-md);padding:9px 12px;margin-bottom:8px;font-size:var(--text-base);line-height:1.5;box-shadow:var(--shadow)}
.mem-short li.me{background:#f4f7fe}
.mem-short .who{display:inline-block;font-size:var(--text-xs);color:var(--sub);margin-right:6px}

/* 权限审批 */
.perm-list{list-style:none;margin:0;padding:0}
.perm-list li{display:flex;align-items:center;gap:var(--space-2);background:var(--card);border-radius:var(--radius-md);padding:10px 12px;margin-bottom:8px;box-shadow:var(--shadow)}
.perm-list .nm{flex:1;display:flex;flex-direction:column}
.perm-list .nm b{font-size:var(--text-base)}
.perm-list .nm span{font-size:var(--text-xs);color:var(--sub)}
.perm-list .sbtn{padding:6px 10px;font-size:var(--text-sm);background:#eef2fb;color:var(--ink);border-radius:9px}
.perm-list .sbtn.ok{background:#e6f8ef;color:var(--success)}

/* ══════════════════════════════════════════════════════════════════════════
   「花了多少」（报价单）· 2026-10 · 工具侧①
   --------------------------------------------------------------------------
   为什么样式必须在这里：`tools/check-css-classes.js` 查的就是"**加了类名但没写样式**" ——
   而这个毛病本仓库自己连犯两次（`.gap-greet` / `.play-busy`）。
   面板由 `public/quote-panel.js` 画，类名与这里的定义**一一对应**（判据会核）。
   配色沿用既有变量（`--card` / `--sub` / `--ink` / `--shadow`），不引新色。
   ⚠️ "不知道用量"那一行**故意用告警色**（`.quote-warn`）而不是灰色：
      灰色看起来像"备注"，而它是"这个数算不出来"的事实。
   ══════════════════════════════════════════════════════════════════════════ */
.quote-card{margin-top:16px}
.quote-total{font-size:var(--text-md);margin:6px 0 4px}
.quote-total b{font-size:19px}
.quote-total .quote-dim{font-weight:400}
.quote-dim{color:var(--sub);font-size:var(--text-xs)}
.quote-list{list-style:none;margin:0 0 8px;padding:0}
.quote-list li{display:flex;align-items:flex-start;justify-content:space-between;gap:10px;background:var(--card);border-radius:var(--radius-md);padding:9px 12px;margin-bottom:7px;box-shadow:var(--shadow)}
.quote-list li>span:first-child{display:flex;flex-direction:column;min-width:0}
.quote-list li>span:last-child{flex:none;white-space:nowrap;font-size:var(--text-base)}
.quote-list li b{font-size:var(--text-base)}
.quote-unknown{color:var(--warning);font-weight:600}
.quote-warn{color:var(--warning);background:#fff4e6;border-radius:10px;padding:8px 10px;margin:8px 0}
.quote-empty{color:var(--sub);margin:8px 0}
.quote-loading{color:var(--sub);margin:8px 0}
.quote-caveat{color:var(--sub);font-size:var(--text-xs);margin:6px 0 0;line-height:1.55}
.quote-basis{margin-top:12px;border-top:1px solid rgba(40,70,140,.12);padding-top:4px}
.quote-basis p{margin:4px 0}
.quote-src-list li a{font-size:var(--text-xs);color:var(--action);word-break:break-all;text-decoration:none}
.quote-src-list li a:hover{text-decoration:underline}
.quote-unknown-list li>span:first-child{color:var(--warning)}
.quote-card .sech{margin-top:12px}
.quote-card label{display:block;font-size:var(--text-sm);color:var(--sub);margin:8px 0 4px}
.quote-card input[type="text"]{width:100%;box-sizing:border-box}
/* 模型读出来的那一份（与本地那份**并列**摆着，不合并成一个数） */
.quote-ai{background:#f4f7fe;border-radius:var(--radius-md);padding:8px 10px;margin:8px 0}
.quote-ai p{margin:3px 0}
/* 「估的 vs 真的」对照（事后校准用的那一块） */
.quote-calib{background:#f7f4ef;border-radius:var(--radius-md);padding:8px 10px;margin:10px 0 0}
.quote-calib p{margin:3px 0}

/* 养宠物 / 空屋子 */
.home-bar .primary{width:auto;margin:0}
.adopt-grid{display:flex;flex-direction:column;gap:var(--space-3);margin:14px 0}
.adopt-card{display:flex;align-items:center;gap:var(--space-3);background:#f4f7fe;border:1.5px solid #f4f7fe;border-radius:16px;padding:14px;cursor:pointer;transition:.15s}
.adopt-card:hover{border-color:var(--action);background:#f4f7fe;transform:translateY(-1px)}
.adopt-ic{font-size:30px;flex:none}
.adopt-card b{font-size:15px}
.adopt-card .tiny{display:block;margin-top:2px}
.gen-perm{font-size:var(--text-sm);color:var(--warning);background:#fff4e6;border-radius:10px;padding:8px 10px;margin-top:8px;display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap}
.gen-perm .sbtn{padding:5px 10px;font-size:var(--text-sm);border-radius:var(--radius-sm);background:#e6f8ef;color:var(--success)}
.gen-test{display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap;margin-top:8px}
.gen-test .ghost{padding:7px 12px;font-size:var(--text-sm)}
.gen-test .tiny{color:var(--sub)}

/* 登录页/账号 */
.auth-sep{text-align:center;font-size:var(--text-sm);color:var(--sub);margin:12px 0 4px}
.auth-sec{margin-top:12px;border-top:1px dashed #dfe7f5;padding-top:6px}
.auth-line{display:flex;align-items:center;gap:var(--space-2);margin:8px 0}
.auth-tag{flex:none;width:92px;font-size:var(--text-sm);color:var(--sub)}
.auth-tag em{font-style:normal;font-size:10px;color:#fff;background:var(--action);border-radius:6px;padding:0 5px;margin-left:4px}
.auth-line .ghost{flex:1;background:#eef2fb}
.linkbtn{background:none;color:var(--action);font-size:var(--text-sm);padding:4px 0}
.s-account{margin-top:10px}
.recovery-code{font-family:ui-monospace,Consolas,monospace;font-size:15px;letter-spacing:1px;background:#f4f7fe;border:1px dashed #b9cdf3;border-radius:10px;padding:10px 12px;text-align:center;margin:6px 0}

/* 档案 */
.archive-list{list-style:none;margin:0;padding:0}
.archive-list li{border-bottom:1px solid #eef2fb;padding:10px 2px;font-size:var(--text-base);line-height:1.55}
.archive-list li .r{color:var(--sub);font-size:var(--text-xs);display:block;margin-top:2px}

/* 底部导航 */
.tabbar{position:fixed;bottom:0;left:0;right:0;max-width:520px;margin:0 auto;display:flex;background:rgba(255,255,255,.94);backdrop-filter:blur(8px);border-top:1px solid #f4f7fe;z-index:15}
.tab{flex:1;background:none;border-radius:0;padding:9px 2px 11px;font-size:19px;color:var(--ink-muted);line-height:1}
.tab span{display:block;font-size:var(--text-xs);margin-top:3px}
.tab.active{color:var(--action)}

/* ==============================================================
   管理页的四个子界面（已装插件 / 审核台 / 权限 / 插件内核）
   --------------------------------------------------------------
   ⚠️ 类名**不能**用 .tabs / .tab —— 那两个已经被底部导航占用
      （.tab 是 flex:1 + 19px 的大图标按钮）。复用会让底部导航的样式
      串到管理页，按钮被撑成四等分的大块。所以独立命名 mg-*。
   ============================================================== */
.mg-tabs{display:flex;gap:6px;flex-wrap:wrap;margin:0 0 14px;
  border-bottom:1px solid rgba(127,127,127,.20);padding-bottom:0}
.mg-tab{background:none;border:0;border-bottom:2px solid transparent;
  padding:8px 12px 9px;margin-bottom:-1px;font-size:var(--text-base);color:var(--sub);
  cursor:pointer;border-radius:8px 8px 0 0;display:flex;align-items:center;gap:6px}
.mg-tab:hover{background:rgba(127,127,127,.10);color:var(--ink)}
.mg-tab.on{color:var(--action);border-bottom-color:var(--action);font-weight:600}
/* 角标：这一页有多少东西（审核台那个还要醒目 —— 它是"待办"） */
.mg-tabn{display:none;min-width:17px;height:17px;line-height:17px;padding:0 5px;
  border-radius:9px;background:rgba(127,127,127,.20);font-size:var(--text-xs);
  font-weight:600;text-align:center;font-variant-numeric:tabular-nums}
.mg-tabn.show{display:inline-block}
.mg-tabn.warn{background:var(--warn);color:#fff}
.mg-pane.hidden{display:none}
/* ==============================================================
   记忆页：灵魂的因果循环 + 情感印记
   --------------------------------------------------------------
   这一页回答的是"她怎么长出来的"，所以最上面放那一圈循环，
   而不是先摆三个数字。
   情感印记刻意用**暖色**：它是唯一一层"有温度、且不允许被平均"的东西。
   ============================================================== */
.mem-loop{display:flex;flex-wrap:wrap;align-items:center;gap:var(--space-1);margin:2px 0 8px;
  padding:10px 12px;border-radius:var(--radius-md);background:rgba(127,127,127,.07)}
.ml-step{display:flex;flex-direction:column;align-items:center;gap:1px;
  min-width:62px;padding:5px 7px;border-radius:10px;background:rgba(127,127,127,.10)}
.ml-step.warm{background:rgba(255,150,120,.20)}
/* 「她真的在长」那段说明（soul-view.js）——
   它解释"经历"与"人格"为什么不是同一个数，以及她已经忘了多少。
   给一点左侧色条与弱化色：它是注解、不是主角，但**必须看得见**。 */
.ml-grow{margin-top:4px;padding-left:8px;border-left:2px solid rgba(255,150,120,.45);
  color:var(--sub);line-height:1.5;font-size:var(--text-xs)}
.ml-ic{font-size:15px;line-height:1.1}
.ml-lb{font-size:var(--text-xs);color:var(--sub)}
.ml-v{font-size:var(--text-sm)}
.ml-ar{color:var(--sub);font-size:var(--text-sm)}
.mem-self{font-size:var(--text-sm);line-height:1.6;margin-bottom:6px}

.mem-marks{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:7px}
.mem-marks li{display:flex;flex-direction:column;gap:2px;padding:8px 10px;border-radius:10px;
  background:rgba(255,150,120,.10);border-left:3px solid rgba(240,130,100,.75)}
.mk-feel{font-size:var(--text-xs);font-weight:600;color:#c8623f}
.mk-hit{outline:2px solid var(--accent);outline-offset:2px;background:rgba(68,170,119,.08)}
.mk-feel.i5{color:#a8271a}
.mk-feel.i4{color:#c8623f}
.mk-feel.i3{color:#b8860b}
.mk-tx{font-size:var(--text-base)}
.mk-mt{font-size:var(--text-xs);color:var(--sub)}
.mem-patterns{display:flex;flex-wrap:wrap;gap:6px}
.mp{font-size:var(--text-sm);padding:3px 9px;border-radius:9px;background:rgba(127,127,127,.14)}

/* 需求条：会被消耗，饿到 30 以下标出来 —— 她就"突然要人陪"得有据可查 */
.mem-needs{display:flex;flex-direction:column;gap:5px}
.nd{display:flex;align-items:center;gap:7px;font-size:var(--text-sm)}
.nd-lb{flex:0 0 62px;color:var(--sub)}
.nd-bar{flex:1;height:7px;border-radius:5px;background:rgba(127,127,127,.20);overflow:hidden}
.nd-bar>i{display:block;height:100%;background:rgba(90,150,210,.75)}
.nd.hungry .nd-bar>i{background:rgba(230,130,90,.9)}
.nd b{flex:0 0 28px;text-align:right;font-variant-numeric:tabular-nums}
.nd.hungry b{color:#c8623f}
/* 需求上的"随关系变化"标记：让用户看得出为什么同样一句关心涨得不一样。
   ★ 2026-10-11：字号 10px → **12px**（手机档 ⑤ 号缺陷）。
     原始数字：`mobile_font_floor` 判"正文 ≥12px"，记忆页 17 个正文样本里**最小 10px**，
     报的就是这一枚 `span.nd-tie「×1.0」`（倍率标注）。
     ⚠️ 口径说明（为什么它算正文、不能靠"归类成辅助文字"绕过去）：
       判据把三类排除在"正文"之外 —— 标签是 I/EM/SMALL/…、类名命中 tiny|hint|… 那一串、
       以及**颜色就是 app 自己的次要色 `--sub`**。这一枚三条都不沾（它是玫红色 #c1547a 的
       语义标记，不是次要说明）⇒ 它就该按正文的可读下限走。手机上 10px 是真的看不清。
     ⚠️ 只动字号，颜色/圆角/内边距一个没动 —— 它是"倍率"标记，视觉层级靠颜色给，不靠字号硬压。 */
.nd-tie{font-size:var(--text-sm);margin-left:4px;padding:0 4px;border-radius:6px;
  background:rgba(212,96,122,.16);color:#c1547a}

/* ==============================================================
   权限页：让用户能**看懂**再决定
   --------------------------------------------------------------
   用户面对一条 `device.camera` 时，能依据的只有这里的字 ——
   他不可能去读插件源码。所以"谁在申请 + 为什么"必须与按钮并排。
   设备权限（摄像头/麦克风）再额外标一次，因为那一条最敏感。
   ============================================================== */
#perm-list li{display:flex;flex-wrap:wrap;align-items:center;gap:6px}
#perm-list li .nm{flex:1 1 60%;display:flex;flex-direction:column;gap:1px}
#perm-list li .why{font-size:var(--text-xs);color:var(--sub);line-height:1.5}
#perm-list li .why.warn{color:#b8860b}
#perm-list li.dev-perm{border-left:3px solid var(--warn);padding-left:10px;
  background:rgba(255,176,32,.06);border-radius:var(--radius-sm)}
.mem-values,.mem-relations,.mem-bounds{font-size:var(--text-sm);line-height:1.6}
.mem-values,.mem-bounds{display:flex;flex-wrap:wrap;gap:6px;align-items:center}
.mem-relations{display:flex;flex-direction:column;gap:2px}

/* ==============================================================
   关系条 + 情感候选回答（对话框上方那两条）
   --------------------------------------------------------------
   状态**不给切换按钮**：用户要的是"像真人一样自然切换"。
   所以这里只显示"现在是什么状态"，不提供控制。
   ============================================================== */
.rel-bar{display:flex;align-items:center;gap:var(--space-2);padding:6px 10px;margin-bottom:6px;
  border-radius:11px;background:rgba(127,127,127,.08);font-size:var(--text-sm)}
.rel-state{font-weight:600;white-space:nowrap}
.rel-state.s-emotional{color:#d4607a}
.rel-state.s-work{color:#4a7fb5}
.rel-meter{flex:1;height:7px;border-radius:5px;background:rgba(127,127,127,.22);overflow:hidden}
.rel-meter>i{display:block;height:100%;width:2%;
  background:linear-gradient(90deg,#e8a0b4,#d4607a);transition:width .35s}
.rel-score{font-variant-numeric:tabular-nums;color:var(--sub)}
.rel-stage{padding:1px 8px;border-radius:var(--radius-sm);background:rgba(212,96,122,.16);color:#c1547a;
  white-space:nowrap}
/* 「她此刻为什么这样说话」（app.js 填）——
   为什么单独一条：这是把"亲密度影响她说什么"从**看不见**变成**看得见**的那一处。
   它是一行注解，不该抢关系条的风头，但必须读得清 —— 所以用副色、行高放开。
   ⚠️ 手机上它必须能换行（关系条本身是 flex 不换行的）。 */
.rel-why{font-size:var(--text-xs);line-height:1.5;color:var(--sub);
  margin:-2px 0 6px;padding:0 10px;word-break:break-word}
.rel-why:empty{display:none}

/* ==============================================================
   ★ 任务态（`#task-state`）—— **与关系态同屏并存、不合并**（2026-10 区 B1）
   --------------------------------------------------------------
   为什么单独一颗、且不改 `.rel-state`：两颗标签回答的是**两个问题** ——
     `.rel-state`（关系态）= 她此刻**怎么跟你说话**（闲聊 / 情感 / 工作）；
     `.task-state`（任务态）= 你要的那件事**在哪一步**（对话 / 干活中 / 等你决定）。
   她"在干活"与她"跟你关系如何"**可以同时是任意组合** ⇒ 合并就会盖掉其中一件事。
   ⚠️ 内容由 `public/task-state.js` 的 `render()` 填；这里只负责"长什么样"，
      一个字的文案都不写（文案在那边一份，别处不各写一套）。
   ⚠️ 三态配色**走 :root 的 token**（D14 口径：零新增硬编码色）：
      `--ink`（对话·正文色）· `--action`（干活中·主操作色）· `--warning`（等你决定·警示色）。
      这三个 token 都在 `:root` 里（实测各 1 处定义）⇒ 不引入新色值。
   ⚠️ **触控 ≥44px**（D28）：它虽然此刻不可点，但"等你决定"那一档将来要挂按钮，
      尺寸现在就得站住，否则以后加按钮会顺手缩小它。
      ⚠️ 更要紧的一条：**别把按钮做进 `.task-state` 里** ——
      【继续】/【取消】由区 B2 的分流渲染（两个按钮要接报价/授权/失败三条不同的链），
      本模块按契约 §三 只负责"显示态 + `why`"。
   ============================================================== */
.task-state{
  display:inline-flex;align-items:center;
  min-height:44px;                        /* D28：触控目标（详见上面那段） */
  max-width:100%;
  padding:4px var(--space-3);
  border-radius:var(--radius-sm);
  border:1px solid var(--border);
  background:var(--bg-card);
  font-size:var(--text-sm);line-height:1.6;
  white-space:normal;word-break:break-word;  /* `why` / `note` 是**玩家原话**，长句必须能断 */
  flex:none;                                 /* 不抢 `.rel-meter` 的伸缩空间 */
  margin-left:var(--space-1);                /* 与 `#rel-state` 拉开一点，读得出是两颗 */
}
.task-state.s-chat{color:var(--ink)}
.task-state.s-working{color:var(--action)}
.task-state.s-waiting{color:var(--warning);border-color:var(--warning)}
/* ⚠️ 这里**故意没有** `.task-state:empty{display:none}` 那种"空就藏起来"的写法：
   三态（含"对话"）**都必须看得见**（判据 J1：三态存在且可辨）——
   "没有任务"不是"不该显示"，而是"现在是对话态"。把它藏掉会让这一颗**时有时无**，
   于是"任务态到底有没有在工作"反而更难判。 */

/* 手机上任务态**折到关系条的第二行**（2026-10 · 照仓里既有的 `430px` 断点写法）。
   为什么是 430 而不是别的数：`style.css` 里既有的三档是 720 / 430 / 360
   （`:1057` / `:1316` / `:2061` / `:2151` / `:2178`），照既有写法走，**不自创断点**。
   🔴 为什么必须折：`#rel-bar` 本来是一行 flex（状态 + 亲密度条 + 分数 + 阶段），
      再加一颗带文字的任务态 ⇒ 390px 视口下 `.rel-meter` 会被挤到几乎看不见宽度。
      折行比缩字体好：缩字号会让"亲密度"这个主信息在手机上读不出来。 */
@media (max-width:430px){
  .rel-bar{flex-wrap:wrap}
  .task-state{flex:1 1 100%;margin-left:0;margin-top:2px}
}

/* 候选回答：每条是一个**取向**（不是死台词），点了填进输入框还能改。
   放在输入框上方 —— 与成熟恋爱向 app 的位置一致。 */
.rel-moves{display:flex;flex-direction:column;gap:6px;margin:0 0 6px}
.rel-move{display:flex;flex-direction:column;gap:1px;text-align:left;width:100%;
  padding:7px 10px;border-radius:10px;border:1px solid rgba(212,96,122,.35);
  background:rgba(212,96,122,.08);cursor:pointer;font:inherit}
.rel-move:hover{background:rgba(212,96,122,.16)}
.rel-move b{font-size:var(--text-sm);color:#c1547a;font-weight:600}
.rel-move span{font-size:var(--text-xs);color:var(--sub)}

/* 审核台里"这条要你处理"的行：左侧一条醒目竖线 */
#review-code-list li.review-todo,#review-perm-list li.review-todo{
  border-left:3px solid var(--warn);padding-left:10px}

/* ==============================================================
   开发台的「这一局会长成什么样」实时预览
   --------------------------------------------------------------
   把服务层早就算得出的东西摊在**生成之前**：玩法类型 / 会用哪套界面 /
   这一类通常要有哪些机制 / 可能还缺哪几项。
   放在输入框旁边而不是产物区 —— 用户要在打字的时候就看到反馈。
   ============================================================== */
.fpv{border:1px solid rgba(127,127,127,.22);border-radius:var(--radius-md);
  padding:9px 11px;margin:2px 0 10px;background:rgba(127,127,127,.05);
  display:flex;flex-direction:column;gap:3px}
.fpv-row{display:flex;gap:var(--space-2);align-items:baseline;font-size:var(--text-sm);line-height:1.5}
.fpv-row .k{color:var(--sub);flex:0 0 84px}
.fpv-row b{font-weight:600}
/* "可能还缺"那一条要显眼但不吓人：它是建议，不是错误 */
.fpv-miss{margin-top:3px;padding:5px 8px;border-radius:var(--radius-sm);font-size:var(--text-sm);
  background:rgba(255,176,32,.14);border-left:3px solid var(--warn)}
/* 机制清单：短名字 + 勾/叉的密集小标签。悬停看完整说明。
   为什么不用整句列表：整句会变成一大段重复文字，用户反而看不出缺了哪几套。 */
.fpv-chips{display:flex;flex-wrap:wrap;gap:var(--space-1);margin-top:2px}
.fpv-chip{font-size:var(--text-xs);padding:2px 7px;border-radius:9px;line-height:1.6;
  background:rgba(127,127,127,.14);color:var(--sub);cursor:help}
.fpv-chip.ok{background:rgba(55,195,145,.18);color:var(--success)}
/* "会交付什么"那一行里的两个状态色：真规则 vs 骨架 */
.fpv .ok{color:var(--success)}
.fpv .warn{color:var(--warn)}

/* 弹窗 */
.age-gate-overlay,.modal{position:fixed;inset:0;background:rgba(20,30,60,.35);display:flex;align-items:center;justify-content:center;z-index:30;padding:20px}
.modal-card{background:var(--card);border-radius:var(--radius-lg);padding:18px;width:100%;max-width:380px;box-shadow:var(--shadow)}
/* 设置内容可随功能增加；卡片始终留在视口内，由内部滚动到达全部操作。 */
#modal-settings>.modal-card{max-height:calc(100vh - 40px);max-height:calc(100dvh - 40px);overflow-y:auto;overscroll-behavior:contain}
.modal-card h3{margin:.1em 0 .4em}
#modal-pets>.modal-card{max-height:calc(100dvh - 40px);overflow-y:auto;overscroll-behavior:contain}
#pet-list{list-style:none;margin:12px 0;padding:0;display:grid;gap:8px}
#pet-list button{width:100%;text-align:left}
#modal-settings .modal-card{max-height:calc(100vh - 40px);max-height:calc(100dvh - 40px);overflow-y:auto}

/* 游玩元碎片 */
.overlay{position:fixed;inset:0;background:rgba(12,20,40,.5);backdrop-filter:blur(3px);z-index:40;display:flex;align-items:flex-end;justify-content:center}
.play-shell{background:var(--bg);width:100%;max-width:520px;height:92vh;border-radius:20px 20px 0 0;display:flex;flex-direction:column;padding:14px 16px 16px}
.play-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}
.play-head b{font-size:var(--text-lg)}
.play-head .hint{margin-left:8px}
.play-log{flex:1;overflow:auto;padding:6px 2px}
.play-log .row{margin:9px 0;display:flex}
.play-log .row.me{justify-content:flex-end}
.play-log .bub{max-width:82%;padding:9px 13px;border-radius:14px;font-size:var(--text-md);line-height:1.6;box-shadow:var(--shadow)}
.play-log .row.me .bub{background:linear-gradient(135deg,var(--action),var(--action-hover));color:#fff}
.play-log .row.fr .bub{background:var(--card)}
.play-log .row.sys .bub{background:#fff4e6;color:#8a6d3b;font-style:italic}

/* 芥子布置 */
#room-items{position:absolute;inset:0;z-index:2}
.ritem{position:absolute;transform:translate(-50%,-50%);font-size:30px;cursor:pointer;filter:drop-shadow(0 3px 4px rgba(0,0,0,.22));transition:transform .15s;user-select:none}
.ritem:hover{transform:translate(-50%,-50%) scale(1.12)}
.ritem .x{position:absolute;top:-8px;right:-9px;font-size:var(--text-xs);background:#ff6b6b;color:#fff;border-radius:50%;width:17px;height:17px;line-height:17px;text-align:center;display:none;font-style:normal}
.edit-mode .ritem{cursor:crosshair}
.edit-mode .ritem .x{display:block}
.edit-mode .scene{outline:3px dashed #7fa2e8;outline-offset:-3px}
.home-bar{display:flex;align-items:center;gap:10px;margin-top:10px;justify-content:center}
.palette{display:flex;flex-wrap:wrap;gap:var(--space-2);margin-top:10px;padding:10px;background:var(--card);border-radius:14px;box-shadow:var(--shadow)}
.pchip{background:#f4f7fe;border-radius:10px;padding:7px 9px;font-size:20px;cursor:pointer;border:2px solid transparent;text-align:center}
.pchip.on{border-color:var(--action);background:#f4f7fe}
.pchip small{display:block;font-size:10px;color:var(--sub);margin-top:1px}
/* MCP 面板的行（2026-10 第 96 轮）—— 复用 `.palette` 的容器，行内自己排版。
   ⚠️ 加它是因为 `tests/css-classes.test.js` 会抓"用了但没样式的类" ——
      那条守卫**抓到了我这次新增的 `.mcp-row`**（说明它有效）。 */
.mcp-row{flex:1 1 100%;padding:8px 10px;background:#f4f7fe;border-radius:10px;border:1px solid var(--line,var(--border))}
.mcp-row .sbtn{margin-top:4px}
/* 🔴 **危险工具的确认块**（第 127 轮）—— 设计上要"多花一点注意力"：
   · 用警示色 + 左边框，与普通行**明显不同**（防"无脑点过"）
   · 确认按钮**样式刻意与普通 `.sbtn` 不同**（实心警示色），
     不许它看起来像"随手点一下就行" */
.mcp-danger{margin-top:6px;padding:8px 10px;background:rgba(184,134,11,.10);
  border-left:3px solid var(--warn,#b8860b);border-radius:var(--radius-sm)}
.mcp-danger-yes{margin-top:6px;background:var(--warn,#b8860b);color:#fff;border:none;font-weight:600}
.mcp-danger-yes:hover{filter:brightness(1.08)}
/* 🔴 **数值条"变了"的反馈**（第 128 轮 · 设计侧 `consult-63` 的 ③）
   分级 = **反馈类** ⇒ ≤200ms · `ease-out` · **`iteration-count: 1`（不许循环）**。
   ⚠️ 循环会把"变了"变成"一直在动" —— 那是**抢视觉**，不是反馈。
   ⚠️ 用 `filter: brightness` 而不是 `box-shadow`：不改布局、不触发重排。 */
@keyframes moodFlash{
  0%,100%{filter:brightness(1)}
  35%{filter:brightness(1.55)}
}
#d-mood.mood-flash{animation:moodFlash .18s ease-out 1}
@media (prefers-reduced-motion: reduce){
  #d-mood.mood-flash{animation:none}
}



/* 形象选择 */
.avatar-box{border:1.5px dashed var(--border);border-radius:14px;padding:10px 12px 14px;margin-top:14px}
.av-panel{margin-top:4px}
.av-up{display:flex;gap:10px;align-items:center;margin-top:8px}
.av-drop{display:inline-flex;align-items:center;justify-content:center;width:78px;height:78px;flex:none;margin:0;border:1.5px dashed var(--action);border-radius:14px;background:#f4f7fe;color:var(--action);font-size:var(--text-sm);cursor:pointer;text-align:center;line-height:1.3}
.av-src-prev{width:78px;height:78px;border-radius:14px;background:#f4f7fe;display:flex;align-items:center;justify-content:center;color:var(--sub);font-size:var(--text-xs);overflow:hidden}
.av-src-prev img{width:100%;height:100%;object-fit:cover}
.adv{margin-top:10px;font-size:var(--text-sm);color:var(--sub)}
.adv summary{cursor:pointer}
.adv label{margin:8px 0 4px}
.av-lib{display:flex;flex-wrap:wrap;gap:10px;margin-top:8px}
.av-lib-item{width:64px;text-align:center;cursor:pointer;font-size:var(--text-xs);color:var(--sub);border:2px solid transparent;border-radius:var(--radius-md);padding:var(--space-1)}
.av-lib-item.on{border-color:var(--action);background:#f4f7fe}
.av-lib-thumb{width:48px;height:48px;margin:0 auto 2px}
.av-result{display:flex;gap:var(--space-3);align-items:center;margin-top:14px;padding-top:12px;border-top:1px dashed #dfe7f5}
.av-result-box{width:76px;height:76px;flex:none}
.av-result b{font-size:var(--text-md)}
/* 图片形象 */
/* 生图插件 */
.gen-box{margin-top:12px;padding:10px 12px 12px;border:1px solid #dfe7f5;border-radius:var(--radius-md);background:#fbfdff}
.gen-box>label:first-child{margin-top:2px}
.gen-cfg{margin-top:4px}
.cfg-field{display:block;margin:9px 0 2px}
.cfg-field .cfg-label{display:flex;align-items:center;gap:6px}
.cfg-field .ftype{font-style:normal;font-size:10px;color:var(--ink-muted);background:#eef2fb;border-radius:5px;padding:0 6px}
.cfg-field.cfg-bool{display:flex;align-items:center;gap:var(--space-2)}
.cfg-field.cfg-bool .cfg-label{order:2;margin:0}
.cfg-field.cfg-bool input{order:1;width:auto;margin:0}
.gen-fb{display:flex;align-items:center;gap:6px;font-size:var(--text-sm);color:var(--sub);margin-top:8px}
.gen-fb input{width:auto;margin:0}
.gen-list{margin:10px 0 4px}
.gen-item{display:flex;align-items:center;gap:var(--space-2);background:#fff;border:1px solid #dfe7f5;border-radius:var(--radius-md);padding:8px 10px;margin-bottom:8px}
.gen-item.on{border-color:var(--action);background:#f4f7fe}
.gen-item .gi-ic{font-size:20px}
.gen-item .gi-nm{flex:1;display:flex;flex-direction:column}
.gen-item .gi-nm b{font-size:var(--text-base)}
.gen-item .gi-nm i{font-style:normal;font-size:var(--text-xs);color:var(--sub)}
.gen-item .sbtn{padding:6px 9px;font-size:var(--text-sm);background:#eef2fb;color:var(--ink);border-radius:9px}
.gen-item .sbtn.installed{background:#e6f8ef;color:var(--success)}
.gen-item .rm{background:#fdecec;color:var(--error);padding:6px 8px;font-size:var(--text-sm);border-radius:9px}

.pet-img{width:100%;height:100%;object-fit:cover;border-radius:50%;display:block;background:#fff;box-shadow:0 8px 20px rgba(40,70,140,.22)}
/* 朏朏：插画头像加一圈白边与柔光，与房间背景分层 */
.pet-img-fufu{background:#f4f7fe;box-shadow:0 0 0 3px rgba(255,255,255,.9),0 10px 26px rgba(40,70,140,.3)}

/* ===== 桌宠（透明底立绘 + 自己走动/睡觉/互动） ===== */
.home-pet.deskpet{width:auto;height:auto;transition:none}      /* 位置由引擎逐帧驱动，别加缓动 */
.pet-sprite{display:block;height:var(--pet-h,130px);width:auto;transform-origin:50% 92%;
  filter:drop-shadow(0 6px 10px rgba(40,70,140,.28));-webkit-user-drag:none;user-select:none;pointer-events:none}
/* 注视鼠标时的侧身缓动（走路时这个 class 会被摘掉，避免拖慢逐帧步进） */
.home-pet.pet-looking{transition:transform .32s ease-out}
.home-pet[data-facing="left"] .pet-sprite{transform:scaleX(-1)}
.pet-shadow{position:absolute;left:50%;bottom:-2px;width:64%;height:14px;transform:translateX(-50%);
  background:radial-gradient(closest-side,rgba(44,54,84,.42),rgba(44,54,84,0));border-radius:50%;pointer-events:none}
.home-pet.pet-sleeping .pet-sprite{filter:drop-shadow(0 6px 10px rgba(40,70,140,.2)) brightness(.86) saturate(.9)}
.home-pet.pet-sleeping .pet-shadow{opacity:.6}
.home-pet.pet-sit .pet-sprite{transform:scaleY(.94) translateY(2px)}
.home-pet[data-facing="left"].pet-sit .pet-sprite{transform:scaleX(-1) scaleY(.94) translateY(2px)}
.pet-zzz{position:absolute;top:-6px;right:2px;font-size:15px;opacity:0;transition:opacity .4s;pointer-events:none}
.home-pet.pet-sleeping .pet-zzz{opacity:.95;animation:zzz 2.6s ease-in-out infinite}
@keyframes zzz{0%{transform:translateY(4px) scale(.9);opacity:.35}50%{transform:translateY(-6px) scale(1.05);opacity:1}100%{transform:translateY(-14px) scale(.95);opacity:0}}
.pet-react{position:absolute;right:-26px;top:-14px;width:52px;height:52px;opacity:0;transform:scale(.5);transition:opacity .22s,transform .22s;pointer-events:none}
.pet-react.on{opacity:1;transform:scale(1)}
.pet-react img{width:100%;height:100%;display:block;filter:drop-shadow(0 4px 8px rgba(40,70,140,.3))}
.home-pet.pet-dragging{cursor:grabbing}
.home-pet.deskpet{cursor:grab}
/* 桌宠状态动画（作用在立绘上） */
.deskpet.state-idle .pet-sprite{animation:breathe 3.1s ease-in-out infinite}
.deskpet.state-listen .pet-sprite{animation:bounce .7s ease-in-out infinite}
.deskpet.state-think .pet-sprite{animation:tilt 1.1s ease-in-out infinite}
.deskpet.state-poke .pet-sprite{animation:hop .2s ease-out 1}
.deskpet.state-sleep .pet-sprite{animation:doze 3.8s ease-in-out infinite}
.deskpet.state-walk .pet-sprite{animation:none}

/* 桌宠挂件层：前端插件（widget:true）挂到这里，浮在所有界面之上 */
.widget-layer{position:fixed;inset:0;pointer-events:none;z-index:70}
.plugin-frame.widget{position:fixed;border:0;background:transparent;pointer-events:auto;box-shadow:none}
.plugin-frame{border:0}

/* 常驻自检面板 */
#selftest-panel{position:fixed;right:8px;top:8px;z-index:99;background:#fff;border:1px solid var(--border);border-radius:var(--radius-md);box-shadow:var(--shadow);padding:10px 12px;max-width:280px;font-size:var(--text-sm);line-height:1.5}
#selftest-panel .st-head{font-weight:700;margin-bottom:6px}
#selftest-panel .st-row.ok{color:var(--success)}
#selftest-panel .st-row.bad{color:var(--error)}
#selftest-panel .st-row i{font-style:normal;color:var(--ink-muted)}
#selftest-panel .st-row.skip{color:var(--ink-muted)}
.state-idle .pet-img{animation:breathe 2.8s ease-in-out infinite}
.state-listen .pet-img{animation:bounce .7s ease-in-out infinite}
.state-think .pet-img{animation:tilt 1.1s ease-in-out infinite}
.state-done .pet-img{animation:hop .6s ease-in-out 2}
.state-walk .pet-img{animation:walk .5s ease-in-out infinite}
.state-sleep .pet-img{animation:doze 3.6s ease-in-out infinite;opacity:.85}

/* ===== 插件界面：左侧纵向子界面（社区 / 收藏）+ 右侧面板 =====
   注意 .app 是 max-width:520px 的窄屏布局，左栏必须够窄才不会挤坏卡片。 */
.plugin-shell{display:flex;gap:10px;align-items:flex-start;margin:2px 0 14px}
.plugin-nav{flex:0 0 74px;width:74px;display:flex;flex-direction:column;gap:6px;position:sticky;top:8px}
.pnav{position:relative;display:flex;flex-direction:column;align-items:center;gap:2px;
  background:#dfe7f5;color:var(--sub);border-radius:var(--radius-md);padding:10px 4px;font-size:var(--text-sm);line-height:1.25}
.pnav .pni{font-size:17px}
.pnav.active{background:#fff;color:var(--ink);font-weight:700;box-shadow:0 2px 8px rgba(40,70,140,.12)}
.pnav:active{transform:translateY(1px)}
.pbadge{position:absolute;top:4px;right:6px;min-width:16px;height:16px;padding:0 4px;border-radius:var(--radius-sm);
  background:var(--action);color:#fff;font-size:var(--text-xs);line-height:16px;text-align:center}
.pbadge:empty{display:none}
.plugin-panes{flex:1;min-width:0}
.ppane{display:none}
.ppane.active{display:block}
/* 面板内卡片沿用「内置演示插件」那套排版（.grid 双列 + .skill 卡片），
   只是左栏占掉 74px 后横向变窄，间距相应收紧一点 */
.ppane .grid{gap:var(--space-2);margin-top:10px}
.ppane .skill{padding:11px;border-radius:14px;gap:5px}
.ppane .skill .sbtn{padding:7px;font-size:var(--text-sm)}
.ppane .skill .sbtn+.sbtn{margin-top:0}
.ppane .page-head{margin:0 0 8px}
.ppane .page-head h2{font-size:var(--text-lg)}
.ppane .hint{font-size:var(--text-sm);margin:0 0 2px}

/* ══════════════════════════════════════════════════════════════
   框架游戏的「游戏界面」（全屏）
   --------------------------------------------------------------
   为什么：框架游戏要的是**自己的完整界面**，不是聊天页上方那块窄面板。
   开启时覆盖 app 主界面，退出后回到 app（只是隐藏这层遮罩，
   app 的 DOM 从未被替换，所以"重新载入"是天然成立的）。
   声明方式见 public/game-view.js —— 宿主只承载，不判断内容。
   ══════════════════════════════════════════════════════════════ */
/* 开了游戏界面：窄面板 → 全屏 */
#view-play.gv-on{align-items:stretch;justify-content:stretch;padding:0;background:var(--bg);backdrop-filter:none}
#view-play.gv-on .play-shell{max-width:none;width:100%;height:var(--vh);border-radius:0;padding:10px 14px 12px}
.play-stage{flex:1;min-height:0;display:flex;gap:14px}
.play-stage.hidden{display:none}
.gv-side{width:186px;flex:0 0 186px;display:flex;flex-direction:column;gap:6px;
  background:var(--card);border-radius:16px;padding:12px 10px;overflow:auto;box-shadow:var(--shadow)}
.gv-sidehead{display:flex;flex-direction:column;gap:2px;padding:2px 6px 8px;
  border-bottom:1px solid rgba(127,127,127,.18);margin-bottom:4px}
.gv-sidehead b{font-size:15px}
.gv-nav{display:flex;flex-direction:column;gap:var(--space-1)}
.gv-navbtn{display:flex;align-items:center;gap:var(--space-2);width:100%;text-align:left;
  background:transparent;border:0;border-radius:var(--radius-md);padding:9px 10px;cursor:pointer;
  color:inherit;font-size:var(--text-base)}
.gv-navbtn:hover{background:rgba(127,127,127,.12)}
.gv-navbtn.on{background:rgba(127,127,127,.20);font-weight:600}
.gv-navbtn .gv-ic{width:18px;text-align:center}
.gv-main{flex:1;min-width:0;overflow:auto;background:var(--card);
  border-radius:16px;padding:16px 18px;box-shadow:var(--shadow)}
.gv-main h3{margin:0 0 8px;font-size:15px}
.gv-main .gv-sec{margin-bottom:18px}
.gv-main .gv-sec:last-child{margin-bottom:0}
.gv-main .gv-kv{display:flex;flex-wrap:wrap;gap:6px 18px;font-size:var(--text-base)}
.gv-main .gv-kv span b{font-weight:600}
.gv-main .gv-bar{height:8px;border-radius:6px;background:rgba(127,127,127,.18);overflow:hidden;margin:6px 0 4px}
.gv-main .gv-bar>i{display:block;height:100%;background:currentColor;opacity:.55}
.gv-main .gv-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:10px}
.gv-main .gv-tile{border:1px solid rgba(127,127,127,.22);border-radius:var(--radius-md);padding:10px 12px;font-size:var(--text-base)}
.gv-main .gv-tile.on{border-color:currentColor;background:rgba(127,127,127,.10)}

/* ==============================================================
   活体内容面（3D / canvas / iframe 的落点）
   --------------------------------------------------------------
   主区平时是一块滚动区域（padding + overflow:auto）。但 3D 场景要的是
   **一块铺满、自己裁剪、自己滚动的画布** —— 交给它滚动条与内边距会很难看，
   也会让命中检测的坐标算错。
   所以挂载时给主区加 .gv-mounted：去掉内边距与滚动，让面铺满。
   ★ 这一层不认识"3D"：任何自己管资源的活体内容（canvas / iframe / 视频）
     都走同一个位。
   ============================================================== */
.gv-main.gv-mounted{overflow:hidden;padding:0;position:relative}
.gv-surface{position:absolute;inset:0;overflow:hidden}
.gv-main.gv-mounted .gv-surface{position:absolute;inset:0}
/* 场景里的可点对象：给个手势提示（声明方可以覆盖） */
.gv-surface [data-scene-pick]{cursor:pointer}

/* ---- 状态面板（左栏上半）—— 对齐成熟游戏界面：数值常驻可见 ----
   为什么必须有：玩家不该为了看"我还剩多少寿元"去翻到人物信息页。
   这是参考界面（Agent修仙）里最显眼的一块，不是装饰。 */
.gv-stats{display:flex;flex-direction:column;gap:2px;padding:8px 6px;
  border-bottom:1px solid rgba(127,127,127,.18);margin-bottom:6px}
.gv-stats.hidden{display:none}
.gv-stat{display:flex;align-items:baseline;justify-content:space-between;gap:var(--space-2);
  font-size:var(--text-sm);padding:3px 2px}
.gv-stat .k{color:var(--sub)}
.gv-stat b{font-size:var(--text-base);font-weight:600;font-variant-numeric:tabular-nums}
.gv-stat .gv-bar{grid-column:1/-1;width:100%;flex-basis:100%;height:5px;
  border-radius:4px;background:rgba(127,127,127,.18);overflow:hidden;margin:2px 0 4px}
.gv-stat .gv-bar>i{display:block;height:100%;background:var(--action);opacity:.75}
/* 状态行带进度条时换行：标签+数值一行，条占满下一行 */
.gv-stat:has(.gv-bar){flex-wrap:wrap}

/* ---- 顶栏工具条（每日签到 / 帮助 这类游戏级动作）---- */
.gv-tools{display:flex;align-items:center;gap:6px;margin-left:auto;margin-right:8px}
.gv-tools.hidden{display:none}
.gv-tool{font-size:var(--text-sm);padding:4px 10px}

/* 底部区（故事流 + 输入）：开游戏界面时压到底部、不再抢主区空间。
   ★ 比例对齐参考界面（Agent修仙）：故事流是**底部一小条**（约 22%），
     主区（场景/行动）占大头 —— 玩家看的是游戏，不是聊天记录。
     之前给到 38%，主区被挤得只剩几行，行动按钮都快贴到顶上了。 */
.play-foot{display:flex;flex-direction:column;gap:var(--space-2);min-height:0}
#view-play.gv-on .play-foot{flex:0 0 22%;max-height:22%;border-top:1px solid rgba(127,127,127,.18);padding-top:8px;margin-top:10px}
/* 窄面板模式（没有游戏界面的框架）：底部区就是原来的样子 */
#view-play:not(.gv-on) .play-foot{flex:1;min-height:0}
@media (max-width:720px){
  .play-stage{flex-direction:column}
  /* 窄屏：左栏变横条，状态面板跟着横排 —— 否则一屏看不到主区 */
  .gv-side{width:auto;flex:0 0 auto;flex-direction:row;align-items:center;overflow-x:auto;gap:10px}
  .gv-sidehead{display:none}
  .gv-stats{flex-direction:row;border-bottom:0;border-right:1px solid rgba(127,127,127,.18);
    margin-bottom:0;margin-right:6px;padding:0 8px 0 0;white-space:nowrap}
  .gv-stat{display:inline-flex;gap:var(--space-1);padding:0 4px}
  .gv-stat .gv-bar{display:none}
  .gv-nav{flex-direction:row}
  #view-play.gv-on .play-foot{flex:0 0 30%;max-height:30%}
}
/* 她记得的代价（因果闭环）：后悔/搁下/不后悔 三种，样式要能一眼分出来 */
.mem-lessons{display:flex;flex-direction:column;gap:5px;font-size:var(--text-sm)}
.ls{display:flex;flex-direction:column;gap:1px;padding:6px 9px;border-radius:9px;background:rgba(127,127,127,.10);border-left:3px solid rgba(127,127,127,.4)}
.ls b{font-size:var(--text-sm)}
.ls.regretted{border-left-color:rgba(200,98,63,.9);background:rgba(200,98,63,.10)}
.ls.costed{border-left-color:rgba(184,134,11,.8);background:rgba(184,134,11,.08)}
.ls.proud{border-left-color:rgba(90,150,210,.8);background:rgba(90,150,210,.08)}
.ls span{color:var(--sub)}


/* ===== 芥子 · 2.5D 舞台（分层 DOM：z-index 遮挡 + 近大远小 + 影子随深度）=====
   为什么是 DOM 不是 canvas：这个 app 没有画布层；DOM 天然给出遮挡/命中/拖拽，
   布局算在纯函数 layoutFor 里 —— 将来换渲染器只换消费方，契约不用动。 */
/* 🔴 2026-10：**舞台必须是定位容器**（`position: relative`）。
   为什么：`.iso-scaler` 是 `position: absolute`，而它的 `left/top` 由
   `QPetIsoScene.compose()` 按**"舞台尺寸"**算出来（居中：`(w − boxW·k)/2 − boxLeft·k`）。
   如果舞台**不是**定位容器，scaler 的包含块就会落到更外层的定位祖先上 ——
   那个盒子比舞台宽（实测 520 vs 488），于是 `left` 的语义与算法**对不上**，
   房间整体右漂（实测 ~92px，且 `layoutRect()` 的纯函数判据看不出来，
   因为它假定"包含块就是舞台"）。
   ⇒ 加这一行，让 CSS 的包含块与算法的假设**一致**。 */
.jiezi-stage { margin: 10px 0; position: relative; }
/* ============================================================
   ★★ T1②（`design-out/consult-255-answer.md` §三 T1 第②条）：**平面必须自己建一个栈上下文**
   ------------------------------------------------------------
   所有者原话：「**用户要拖动装饰只用两种方法：告诉大模型让他帮忙，
     或者手指拖拽交互（对应电脑鼠标交互）**」——
   而这条链在真浏览器探针里**够不着**，其中一条原因是 z 序：

   🔴 症状（`tools/jiezi-occupancy-probe.js` 用 `elementsFromPoint` 取的原始摞）：
      `layoutFor()` 给摆件的 z-index 是 `bandZ(layer) + round(y/20) − 100`
      （见 `public/jiezi-view.js`），而 `bandZ('object') = 0` ⇒
      **逻辑 y < 2000 的摆件拿到的是负数**。
      而 `.jiezi-plane` 原来是 `position: relative; z-index: auto` ——
      **它不建栈上下文** ⇒ 那些负 z-index 的摆件**逃到根栈上下文**里，
      被 `body` / `.shell` / `.pages` 各层背景盖住。
      探针那一摞的原文：`… SECTION.page → MAIN.pages → DIV.shell → DIV.app → BODY → SPAN.jiezi-glyph`
      —— 摆件排在 **BODY 之后**（= 画在所有背景后面）：**看不见，也点不到**。

   🔑 修法：**让平面自己成为栈上下文的根**（`isolation: isolate`），
      于是负 z-index 只在这个盒子里排 —— 摆件落在**平面自己的背景之上**。
   ⚠️ **不动 `bandZ()` 那套排序算法**：它是对的（图层带 + 层内按 y），
      错的只是"它没被关在一个盒子里"。
   ⚠️ 为什么是 `isolation` 而**不是** `z-index: 0`：两者都能建栈上下文，但
      `z-index: 0` 会**顺带**把这个平面变成一个"定位的、层级 0 的栈上下文"参与
      兄弟之间的排布；`isolation: isolate` **只建盒子、不改这个元素自己的层级** ——
      在"修一个遮挡 bug"这件事上，改动面越小越不会碰坏别的层
      （`.jz-ctl`(6) / `.jz-main-btn`(7) / `.home-bar`(14) 那些浮层照旧压在平面上，一个字没动）。
   ⚠️ 一个**已知且刻意留着**的副作用：`isolation` 会让平面成为
      `backdrop-filter` 的 **backdrop root** ⇒ `.jiezi-item` 那层 `blur(2px)`
      取样的背景**不再包含平面之外的那些层**。实测观感无差：平面自己的背景
      （`radial-gradient` + `linear-gradient`，不透明）本来就完整盖住它下面那些层。
   ============================================================ */
.jiezi-plane {
  position: relative; isolation: isolate; width: 100%; overflow: hidden; touch-action: none;
  border: 1px solid var(--border); border-radius: 14px;
  background: radial-gradient(120% 90% at 50% 8%, rgba(255,255,255,.6), rgba(0,0,0,.04)),
              linear-gradient(180deg, #f4f7fe, var(--border));
}
/* 🔴 2026-10 本轮：原来这里写着 `aspect-ratio: 16 / 9`，**已删**。
   它不是"没在起作用"（实测它撑出了 488×274.5 的高度），而是**错的假设**：
   等距房间的比例是 5657:3147 ≈ 1.8，而 16:9 是 1.78 —— 看着像，实差 230px。
   房间的实际盒子由 `QPetIsoScene.compose()` 的 `plane` 四个数给（见 jiezi-view.js）。
   🔴🔴 **这一句原来写错了，2026-10-11 改掉（错注释与错代码一样是 bug）**：
     原文是「非等距（`dim:"2d"` 与旧渲染）那条路本来就不靠这个属性定高 ——
     它里面是 `.jiezi-item` 绝对定位 + 平面自己 content 高度，**实测不受影响**」。
     前半句（不靠这个属性定高）是对的，**后半句「实测不受影响」是错的**：
     真浏览器探针（`tools/jiezi-occupancy-probe.js`，有头 + 真 GPU）实测 ——
     平面计算高度 **2px**（只有上下两条 1px 边框）、rect **488×2**、
     摆件"与平面相交的面积" **{w:62,h:0,area:0}**、`elementFromPoint` **命不中**。
     根因正是"绝对定位的子元素不撑父元素高度"：`.jiezi-item` 是
     `position: absolute`，而平面是 `overflow: hidden` ⇒ 摆件被**整段裁掉**。
     （那句话只有一个字的错处：把"按推理应该不影响"写成了"实测不受影响"。）
   ⇒ **扁平那一档的平面高度由下面那条 `:not(.iso):not(.iso3d)` 规则给**（T1①）。 */
.jiezi-plane[data-dim="2d"] { background: linear-gradient(180deg, #fbfdff, #eef2fb); }
/* ============================================================
   🔴 2026-10-10：**等距那一档的平面不许裁**（只有 `.iso` 这一档放开）
   ------------------------------------------------------------
   症状（真机截图 + 真浏览器量出来的，不是推的）：切到 2.5D 之后屋子只画出**右半边**
   —— 左半边的地板与左墙被切掉，房间整体贴在舞台右边，左边空一大块。
   根因（一句话）：`iso.js` 的**平面局部坐标**里，地板是从**负坐标**开始的
     （世界 (0,0) 的屏幕 y 是 0，而地板菱形的最左角在 x = −span·k；小屋实测 **−2828**），
   而 `compose()` 给 `.jiezi-plane` 的是"**包住整个房间**的盒子"（`left = −box.left`）。
   两者叠起来：地板内容落在缩放层的 `0 … 5657`，平面盒子却落在 `2828 … 8485`
   ⇒ 平面自己的 `overflow:hidden` 把**内容的前 2828px 整段裁掉**（正是左半边）。
   ⚠️ 这不是"布局算错"，而是"**盒子与内容各按各的原点**"：相对定位的元素，
      子元素跟着它一起走 —— 所以"把盒子往后挪"永远挪不出这个矛盾（挪多少，内容也挪多少）。
   ⇒ 这一档**去掉裁剪**：内容本来就被 `compose()` 缩进舞台里了（`k` 按横竖两个方向取小），
      不裁也不会溢到别的界面；而 2d / 旧渲染那条路**照旧裁**（`.jiezi-item` 是绝对定位，
      裁掉才是对的）。⚠️ 与 `tools/room-probe.js` 的位置判据不冲突：那个探针量的是
      `.iso-ground` 落在舞台哪里（45..515），它一直是**对的** —— 错的只是"看得见多少"。
   ============================================================ */
.jiezi-plane.iso { overflow: visible; }
/* ============================================================
   ★★ T1①（`design-out/consult-255-answer.md` §三 T1 第①条）：
      **扁平那一档（`dim:"2d"` / 旧渲染 / iso 取不到时的兜底）必须自己有一条高度**
   ------------------------------------------------------------
   🔴 症状（真浏览器探针实测，原始数）：平面计算高度 **2px**、rect **488×2**、
      摆件"与平面相交的面积" **0**、`elementFromPoint` 命不中
      ⇒ 玩家**按不动摆件**、手指/鼠标拖拽这条链在这一档完全断掉。
   🔴 根因：`.jiezi-plane` **没有高度**，而唯一那条高度规则
      （`#page-home.jz-house #jiezi-stage{height:calc(var(--vh) - 170px)}`，见下面）
      **只在屋子档（`jz-house`）生效** —— 那个类是"这一屏真的画出了一间屋子"
      （`applyHouse()`，只在 2.5D / 3D 两档加）。扁平这一档没有渲染器去给平面定高
      （2.5D 是 `placeIso()` 按 `compose().plane` 写、3D 是后端按 `stage.clientHeight` 写），
      而 `.jiezi-item` 是 `position: absolute`（**不撑父元素高度**）+ 平面 `overflow: hidden`
      ⇒ 平面只剩两条边框的高度，摆件被整段裁掉。
   🔑 修法：给**扁平这一档**的平面一条与屋子档**同一个数**的高度
      （`calc(var(--vh) - 170px)`，下界 `300px` 保证矮窗口里也够摆）。
   ⚠️ 为什么用**选择器**分档、而不是给所有平面都写高度：
      `compose()`（2.5D）与 3D 后端都按"舞台的尺寸"给平面定高 ——
      再写一条通用高度就成了**两个来源打架**（本仓库反复栽的形状）。
      `:not(.iso):not(.iso3d)` 与那两档**互斥** ⇒ 同一个元素上永远只有一条来源：
        · 2.5D  → `.iso`（`placeIso()` 写 inline 尺寸）
        · 3D    → `.iso3d`（下面那条 `height:100%`，舞台有确定高度）
        · 其余  → 这一条（扁平 / 旧渲染 / iso 脚本没加载的兜底）
   ⚠️ 也没有用 `aspect-ratio`：那条路本文件上面刚解释过（比例是**错的假设**，
      而且等距那档会被它反过来推出一个宽度、把整页撑宽）。
      这里要的是"和屋子一样高"，不是"一个猜出来的比例"。
   ⚠️ `:not()` 双写在选择器里是**故意**的（不是 `.jiezi-plane:not(.iso, .iso3d)`）：
      后者要 CSS Selectors L4，老 WebView 上会整条规则失效 ——
      而"整条规则失效"正是这一条要修的那个 bug 的形状（静默退回 2px）。
   ============================================================ */
#page-home #jiezi-stage .jiezi-plane:not(.iso):not(.iso3d) {
  height: calc(var(--vh) - 170px); min-height: 300px;
}
.jiezi-item {
  position: absolute; transform-origin: 50% 100%; cursor: grab;
  display: flex; flex-direction: column; gap: 2px; min-width: 64px; padding: 8px 10px;
  text-align: center; border: 1px solid rgba(0,0,0,.08); border-radius: 10px;
  background: rgba(255,255,255,.92);
}
.jiezi-item.dragging { cursor: grabbing; outline: 2px solid #4a6b4f; }
.jiezi-kind { font-size: 10px; opacity: .6; }
.jiezi-label { font-size:var(--text-sm); }
.jiezi-panels { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; margin: 6px 0; }
.jiezi-chip { font-size:var(--text-sm); padding: 2px 8px; border-radius:var(--radius-full); background: rgba(74,107,79,.12); }

/* 芥子摆件的字形（用主题强调色描边；字形表在 jiezi-view.js 里） */
.jiezi-glyph { font-size: 20px; line-height: 1.1; }
.jiezi-item { backdrop-filter: blur(2px); }

/* ================= 摆件"活起来"（对照市面养成类游戏） =================
   用户 2026-10：「宠物家园玩法的**视觉表现**功能是不是还能通过内置插件改进，
   而不是我一开始用的什么插件就从头用到尾」。
   实测：摆件是 emoji 字形（`GLYPH` 表，零素材、零授权问题 —— 这个选择是对的），
   但**全都不动**；而市面同类（旅行青蛙 / 猫咪后院）里物件是有小动作的。

   所以这里做**零素材的那一半**：按 `data-kind` 给不同摆件配不同的小动作。
   为什么按类型分而不是全都一起晃：全都有动作等于全都没有重点，
   而且"地毯在飘"这种事会让人一眼看出是假的。

   ⚠️ 动的是**字形**（`.jiezi-glyph`），不是整个卡片 ——
      卡片带着标签与数值，晃它会让人读不清。
   ⚠️ 全部尊重 `prefers-reduced-motion`（与 a11y.js 同一条约定）。 */
.jiezi-glyph { display: inline-block; transform-origin: 50% 100%; }

@keyframes jz-sway   { 0%,100% { transform: rotate(-2.5deg); } 50% { transform: rotate(2.5deg); } }
@keyframes jz-flicker{ 0%,100% { opacity: 1; } 45% { opacity: .72; } 55% { opacity: .95; } }
@keyframes jz-glow   { 0%,100% { filter: brightness(1); } 50% { filter: brightness(1.25); } }
@keyframes jz-spin   { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
@keyframes jz-breathe{ 0%,100% { transform: scale(1); } 50% { transform: scale(1.06); } }

/* 植物：被风轻轻吹 */
.jiezi-item[data-kind="plant"] .jiezi-glyph { animation: jz-sway 4.2s ease-in-out infinite; }
/* 灯：轻微闪烁（像真的灯，而不是贴纸） */
.jiezi-item[data-kind="lamp"] .jiezi-glyph { animation: jz-flicker 3.1s ease-in-out infinite; }
/* 屏幕：亮一点暗一点 */
.jiezi-item[data-kind="screen"] .jiezi-glyph { animation: jz-glow 5s ease-in-out infinite; }
/* 传送门：真的在转（它是"入口"，转起来才像） */
.jiezi-item[data-kind="portal"] .jiezi-glyph { animation: jz-spin 9s linear infinite; }
/* 坐垫：几乎不动，只是轻微起伏 —— 让人觉着"有东西在那儿" */
.jiezi-item[data-kind="cushion"] .jiezi-glyph { animation: jz-breathe 5.6s ease-in-out infinite; }
/* ⚠️ 地毯 / 柜子 / 台座 / 牌子**故意不动** ——
   它们是"地面与家具"，动起来会让整个屋子显得在漂。 */

/* 拖动时停掉动画：手上正在摆的东西不该自己动（会打架） */
.jiezi-item.dragging .jiezi-glyph { animation: none; }

@media (prefers-reduced-motion: reduce) {
  .jiezi-item .jiezi-glyph { animation: none !important; }
  .chat-portrait.speaking .chat-portrait-img { animation: none !important; }
}

/* 芥子：挂在物件头上的数值条 + 占位标记（市面 2.5D 的常见做法） */
.jiezi-meter { position: absolute; width: 64px; height: 10px; background: rgba(0,0,0,.10);
  border-radius:var(--radius-full); overflow: hidden; pointer-events: none; }
.jiezi-meter i { display: block; height: 100%; opacity: .85; }
.jiezi-meter b { position: absolute; left: 0; top: 10px; font-size: 10px; font-weight: 400;
  opacity: .75; white-space: nowrap; }
.jiezi-item[data-blocking]::after { content: "⊘"; position: absolute; right: -6px; top: -8px;
  font-size:var(--text-sm); opacity: .7; }

/* ================= 初见引导（lore-intro.js） =================
   为什么单独一节：这几张卡片是"新用户第一次打开时唯一告诉他这是什么"的东西。
   内容由 lore-intro.js 从 lore/canon.json 读出来填进去 —— 这里只管样子。 */
#meet-intro { width: min(560px, 92vw); margin: 14px auto 0; text-align: left; }
#meet-intro.hidden { display: none; }
#meet-intro h3 { margin: 0 0 10px; font-size: 15px; color: var(--ink); text-align: center; }
#meet-intro .intro-cards { display: flex; flex-direction: column; gap:var(--space-2); }
#meet-intro .intro-card { display: flex; gap: 10px; align-items: flex-start;
  background: var(--card); border-radius:var(--radius-md); padding: 10px 12px; box-shadow: var(--shadow); }
#meet-intro .intro-card .ic { font-size: 20px; line-height: 1.2; flex: 0 0 auto; }
#meet-intro .intro-card .tx { min-width: 0; }
#meet-intro .intro-card .tx b { display: block; font-size:var(--text-base); color: var(--ink); }
#meet-intro .intro-card .tx p { margin: 3px 0 0; font-size:var(--text-sm); line-height: 1.5; color: var(--sub); }
#meet-intro .intro-card .tx p.tiny { font-size:var(--text-xs); opacity: .85; }
#meet-intro > p.tiny { margin: 10px 0 0; font-size:var(--text-xs); color: var(--sub); text-align: center; }
#meet-intro code { background: rgba(91,140,255,.1); border-radius: 4px; padding: 0 3px; }

/* ================= 首次行动卡片（first-run.js） =================
   为什么单独一条：`.row-btns` 是 `flex` 且**不换行**（为两三个短按钮设计的），
   而这张卡片有三个带文字的按钮 —— 窄屏会挤成一行溢出。
   这个 app 的定位是"手机端和电脑端一起开发"，所以窄屏必须能换行。 */
#jiezi-first-card { margin: 12px 0; }
#jiezi-first-card.hidden { display: none; }
#jiezi-first-card .row-btns { flex-wrap: wrap; }
#jiezi-first-card .row-btns > button { flex: 1 1 auto; }

/* ================= 拾光 tab 的"有新经历"红点（first-run.js） =================
   为什么用一个小圆点、而不是数字角标：新经历可能一次来很多条，
   数字会变成 37、128 这种没有意义的量 —— 用户只需要知道"有新东西"。
   颜色用主题里已有的 `--warn`，不新造一个色。 */
.tab { position: relative; }
.tab-dot { position: absolute; top: 4px; right: 50%; margin-right: -14px;
  width: 8px; height: 8px; border-radius: 50%; background: var(--warn);
  box-shadow: 0 0 0 2px var(--bg); pointer-events: none; }

/* ================= 缺能力时的可点引导（first-run.js） =================
   为什么单独一节：这类提示是"用户卡住时唯一的出口"，要显眼但不喧宾夺主。
   用主题里已有的 `--warn` 做左色条与卡片底色，不新造颜色。 */
#model-need-hint.hidden { display: none; }
.action-hint { border-left: 4px solid var(--warn); margin-top: 10px; }
.action-hint p { margin: 0 0 4px; font-size:var(--text-base); color: var(--ink); }
.action-hint p.tiny { font-size:var(--text-sm); color: var(--sub); }

/* ================= 「这间屋子的规则由你定」（jiezi-view.js） =================
   🔴 2026-10 产品方向：芥子 = 玩家当天道的、一个小型元碎片，机制由他定。
   为什么单独给样式：这段是"玩家感知到自己是天道"的唯一落点 ——
   原来它藏在默认收起的 <details> 里，**等于没有**。 */
.jiezi-rules { margin-top: 8px; }
.jiezi-rules > summary { cursor: pointer; }
.jiezi-rules ul { margin: 6px 0 0; padding-left: 18px; }
.jiezi-rules li { margin: 2px 0; }
/* 邀请段：只有摆件、还没有规则时出现 —— 要显眼一点，
   否则读者会把它当旁注跳过，就还是"感知不到自己能定机制" */
.jiezi-invite { border-left: 3px solid var(--action); padding-left: 8px; margin-top: 8px; }

/* ================= 聊天页立绘（chat-portrait.js） =================
   为什么单独一节：这是 galgame 的"脸面" —— 没有它，聊天页就只有一堆气泡。
   素材是现成的（public/pets/ 的透明底 PNG），所以这里只管摆法与动作。
   ⚠️ 高度用**相对单位 + 上限**，不写死像素：这个 app 的定位是跨端，
      手机竖屏放不下 190px 的立绘，得让它自己缩。 */
.chat-portrait { display: flex; justify-content: center; align-items: flex-end;
  padding: 4px 0 0; min-height: 0; }
.chat-portrait.hidden { display: none; }
.chat-portrait .portrait-wrap { position: relative; line-height: 0; }
.chat-portrait-img { height: clamp(96px, 22vh, 190px); width: auto;
  object-fit: contain;
  /* 透明底立绘落在浅色卡面上会"发灰"，给一点自己的落影让它站住 */
  filter: drop-shadow(0 6px 10px rgba(40,70,140,.18)); }
/* 她开口时的一个很轻的动作 —— 纯 CSS，不引任何外部素材或动图 */
.chat-portrait.speaking .chat-portrait-img { animation: portrait-nod .5s ease-in-out; }
@keyframes portrait-nod {
  0%   { transform: translateY(0)    scale(1); }
  40%  { transform: translateY(-3px) scale(1.012); }
  100% { transform: translateY(0)    scale(1); }
}
/* 尊重"减少动效"（与 a11y.js 同一条约定） */
@media (prefers-reduced-motion: reduce) {
  .chat-portrait.speaking .chat-portrait-img { animation: none; }
}

/* ================= 好感事件（galgame 的里程碑） =================
   🔴 为什么单独一节：关系跨过一档是**整段关系里最值得记住的时刻**，
   而在此之前它完全静默 —— 关系条上的名字悄悄换了，玩家不会注意到，
   拾光里也不留痕迹。
   它必须一眼看得出"这是件事"；但同时**仍是一条系统行** ——
   不能做得像她说的话（否则就成了"她宣布我们关系变好了"，很出戏）。
   所以：加一个左色条与一点底色，但保持居中 / 灰字 / 不带头像。 */
.rel-milestone > span { border-left: 3px solid var(--action); padding-left: 8px;
  background: rgba(143,123,255,.08); border-radius: 4px; }

/* 「这个插件出过力」那一小行（manage-panel.js）——
   为什么单独一个类：原来我写的是 class="ok"，但 .ok 在本仓库只服务于
   .sbtn / .ktag，**不会给 <b> 上色** —— 那是个"没报错但不显眼"的假依赖。
   现在给一个语义明确的类，并沿用主题里已有的成功色。 */
.act-ok{color:var(--success)}
/* 「她注意到你多久没来了」那句问候（first-run.js 的 greetBack）——
   🔴 2026-10 核实出来的：那个函数一直给气泡加 `.gap-greet`，
   **但 CSS 里从来没有这个类的样式** —— 钩子加上了、视觉没接上。
   于是那句问候与 `homeSay()` 写的日常话**长得一模一样**，
   而它本来是这一页唯一"她在回应你离开"的信号。
   做法沿用 `.rel-milestone`：一条左色条 + 一点底色 ——
   看得出"这是件事"，但不喧宾夺主（它是一句问候，不是弹窗）。 */
.home-bubble.gap-greet{border-left:3px solid var(--action);padding-left:10px;
  background:rgba(143,123,255,.08)}
/* ============================================================
   ★★ 朏朏 · **向导立绘**（2026-10 · 所有者给设定图 · 设计侧 `consult-107` 规格书 Q3/Q5）
   ------------------------------------------------------------
   素材现在是**占位**（桌宠立绘 `fufu-junior-body.png`），最终图按规格书命名
   `fufu-junior-guide.png` / `fufu-mature-guide.png` —— **图一到就只换文件、本文件不动**。
   动画契约（规格书 Q3，可判）：
     · 呼吸：`scaleY` 1.0 ⇄ 1.02，周期 **3000ms**，`ease-in-out`，`infinite`
     · 入场：`opacity` 0→1，**500ms**，`ease-in`，只一次
     · **不做眨眼**（静态立绘眨眼要换图/骨骼，超出"立绘"范畴 —— 规格书明写）
   ⚠️ 位置照规格书 Q5：右上角 `top:80px; right:120px`、**0.6 缩放**、不挡屋子中心。
   ⚠️ 她**不接收点击**（`pointer-events:none`）：屋子里的互动已经绑在 `#pet` 与摆件上，
      向导立绘只负责"在场"，抢点击会让"点屋子"变得不可预期。 */
.guide-sprite{position:absolute;top:80px;right:120px;width:256px;height:256px;
  display:grid;place-items:end center;pointer-events:none;z-index:2;
  animation:guide-in .5s ease-in both}
.guide-sprite img{display:block;max-width:100%;max-height:100%;
  transform-origin:50% 92%;animation:guide-breathe 3s ease-in-out infinite;
  filter:drop-shadow(0 8px 14px rgba(40,70,140,.18))}
@keyframes guide-in{from{opacity:0}to{opacity:1}}
@keyframes guide-breathe{0%,100%{transform:scaleY(1)}50%{transform:scaleY(1.02)}}
/* ⚠️ 「减少动效」时**只停呼吸、保留入场**（入场是一次性的，不是持续动效） */
@media (prefers-reduced-motion:reduce){.guide-sprite img{animation:none}}
/* 窄屏（手机）：规格书 Q5 的位置按桌面算的 ⇒ 窄屏把她挪到右上更靠边、并缩小，别压住屋子 */
@media (max-width:430px){
  .guide-sprite{top:56px;right:14px;width:160px;height:160px}
}
/* 没有屋子可画时（2d 纯平面世界 / 一个摆件都没有 / 两档都画不出来）：
   上面那组数是**给 330px 那间 1D 屋子**算的 —— 屋子没了，那组数也就没了意义
   （她会挂在宿主外 146px，压住下面那排入口）。⇒ 按**这块透明地**的大小摆。
   ⚠️ 只动"摆在哪、多大"，她的动画契约（呼吸 / 入场 / `pointer-events:none`）一个字没动。 */
#page-home:not(.jz-house) .guide-sprite{top:0;right:8px;width:190px;height:190px}
/* 轮流发言时的进度行（app.js 的 runSpeechTurns）——
   🔴 2026-10：狼人杀发言阶段每个 NPC 一次真模型调用（5-10 秒是常态），
   而原来这段时间**什么都没有** —— 用户点完「发言」看到"什么都没发生"，
   然后突然刷出一堆 NPC 发言。那是最典型的"以为是 bug"。
   ⚠️ `.play-busy` 必须**看得出是"正在进行"而不是一条普通系统消息** ——
      否则用户照样会以为卡住了。（我上一轮刚记下"钩子挂好了没样式"这个毛病，
        这一轮差点又犯：类加上去了、CSS 没写。） */
.play-log .row.play-busy .bub{opacity:.72;font-style:italic}
.play-log .row.play-busy .bub::after{content:'…';animation:busy-dots 1.2s step-end infinite}
@keyframes busy-dots{0%{content:''}33%{content:'·'}66%{content:'··'}}
@media (prefers-reduced-motion: reduce){.play-log .row.play-busy .bub::after{animation:none;content:'…'}}
/* 「现在在这一章」（jiezi-view.js 的 renderCurrentChapter）——
   🔴 2026-10：它是主线面板最上面那块，**原来是裸 div** ——
   里面挤着 4~5 行 `.tiny`（世界 / 第几章 / 要做什么 / 怎么算过 / 过了会怎样），
   全糊成一片。而且其中一行还带着**内联样式**（`style="opacity:.85"`）——
   说明当初就想让它有层次，只是没走 CSS。
   ⚠️ 这段信息是"我现在该干什么"的落点，**它必须一眼看得清**，
      否则前面几轮"把主线说清楚"的努力就白做了。 */
.jz-here{margin:2px 0 6px;padding:6px 10px;border-left:3px solid var(--action);
  border-radius:6px;background:rgba(143,123,255,.06)}
.jz-here p{margin:1px 0}
/* 世界与章节那行是"我在哪"，给一点分量 */
.jz-here p:first-child{color:var(--ink);font-weight:500}
/* 目标那几行是"要做什么"，保持可读但不抢 */
.jz-here p+p{color:var(--sub)}
/* 「她说话」那个触发按钮（jiezi-view.js）——
   它是上一版新加的桥（她说完话 → 触发世界事件）的**唯一入口**：
   按钮原来只从 spec 里已有的 on 键生成，没有规则用它就没有按钮。
   ⚠️ 没接上规则时加 `.jz-her` —— 用**虚线边**表示"这里还空着、可以接"
      （与实线的普通触发键区分开，一眼看得出哪个还没接）。
   🎨 我上上轮连犯两次"加了类没写样式"（`.gap-greet` / `.play-busy`），
     现在是**先写样式再验证** —— 而且 `check-css-classes.js` 会挡。 */
.jiezi-panels .ghost.jz-her{border-style:dashed;opacity:.9}
.jz-her-note{margin:2px 0 4px;opacity:.85}
/* 「玩法部件」里 kind:"meter" 的数值显示（jiezi-view.js）——
   🔴 2026-10 核实出来的：部件原来只渲染成一个**只有标题的小标签**，
      而数值只有 `decor[].meter` 那条路能显示。
      于是玩家写 `{"add":{"体力":1}}` + 一个 meter 部件，规则跑得完全正确，
      **而他什么都看不见** → 他会以为"规则没生效"。
      **一个看不见自己机制在动的世界，玩家不会继续设计它。**
   ⚠️ 查不到值时显示「还没动过」而不是 0 ——
      "从没跑过"与"跑过但现在是 0"不该长得一样（与「跑过 N 次」同一个规矩）。 */
.jiezi-chip.jiezi-meter{display:inline-flex;align-items:center;gap:6px}
.jiezi-meter-bar{display:inline-block;width:44px;height:5px;border-radius:3px;
  background:rgba(0,0,0,.12);overflow:hidden;font-style:normal}
.jiezi-meter-bar > i{display:block;height:100%;width:0;border-radius:3px;
  background:linear-gradient(90deg,var(--action),var(--action-hover));transition:width .25s}
@media (prefers-reduced-motion: reduce){.jiezi-meter-bar > i{transition:none}}
/* 「这一局的结果」（index.html 的 #play-result）——
   🔴 2026-10 核实出来的：终局原来**只是一行日志**。
   引擎算得出 `win.over / winner / reason`，界面也 pushPlay 成「【终局】X 获胜（原因）」，
   但那是**一行会在日志里滚过去的系统字**，而全屏游戏界面的状态面板只显示「阶段：终局」。
   ⇒ 打完一局之后屏幕上**没有一个"结果"** —— 玩家的感受是"打完了，然后呢"，
     而这是重玩率的关键一刻。
   ⚠️ 放在 play-board **外面**：要同时覆盖游戏界面与旧窄面板两条路。
   ⚠️ 赢了/输了**用颜色分开**，但要看得出是同一件事（同一套形状、只换色）。 */
.play-result{display:flex;flex-direction:column;gap:2px;margin:6px 0;padding:9px 12px;
  border-radius:var(--radius-md);border-left:4px solid var(--action);
  background:rgba(143,123,255,.08);text-align:center}
.play-result b{font-size:15px}
.play-result.ok{border-left-color:var(--success);background:rgba(31,157,107,.10)}
.play-result.bad{border-left-color:#c25454;background:rgba(194,84,84,.10)}
/* 「朏朏的开场白」（index.html 的 #adopt-feifei）——
   ★ 2026-10（方向：docs/direction-2026-10.md「二」）：
     朏朏是**始终在场**的向导，与玩家的宠物**两个并存**
     —— 所以养宠物这一页她**先开口**，不管玩家养不养。
   🔴 为什么必须有样式：她原来是"设定文档里的一段字"，
      界面上没有任何"她在说话"的形状 —— 那就只是又一段说明文字。
      这里给它**对话的样子**（左边一条竖线 + 说话人名字），
      让玩家第一眼就知道"**有个东西在跟我说话**"。
   ⚠️ 样式不齐的话 check-css-classes 会挡（那个守卫抓过我两次）。 */
.feifei-say{display:flex;gap:var(--space-2);align-items:flex-start;margin:2px 0 10px;
  padding:9px 12px;border-radius:var(--radius-md);border-left:4px solid var(--action);
  background:rgba(143,123,255,.09)}
.feifei-say .fs-who{flex:0 0 auto;color:var(--action);font-size:var(--text-base)}
.feifei-say .fs-line{font-size:var(--text-base);line-height:1.6}
/* 「先不养」那张卡：虚线边 —— 与另两张一样能点，只是形状上说明"这也是一种选法" */
.adopt-card.later{border-style:dashed}
/* 「可用空间」（jiezi-view.js 拼出的 .jz-cap）——
   ★ 2026-10（方向：docs/direction-2026-10.md「三之二」）：
     用「空间」当限制而不是「等级」，为的是**可量化、可看见、可规划** ——
     那么界面就必须真的让它"看得见"。所以这一行给一点存在感（不是又一行灰字）。
   ⚠️ 满了要**一眼看出**（.full）：那时玩家要做的第一件事是扩或拆，
      而不是继续点按钮撞墙。 */
.jz-cap{border-left:3px solid var(--action);padding-left:8px}
.jz-cap.full{border-left-color:#c25454;color:#c25454}
/* 「官方」徽章（jiezi-view.js 的芥子列表）——
   ★ 2026-10（所有者：「**主线剧情是官方设定的，内置做好的**，不是玩家生成的」）：
     芥子列表里原来**看不出哪个是官方的** —— 「朏朏的小屋」和玩家自己造的
     看起来一模一样。这枚徽章把那件事说清楚。
   ⚠️ 要**低调**：它是**标记**不是卖点（描金那种点缀色，不是主色）。
      用主题里的描金（#C8A96A）那种分量 —— 看得出、不抢眼。 */
.jz-badge{display:inline-block;margin-right:4px;padding:0 5px;border-radius:6px;
  font-size:10px;line-height:15px;letter-spacing:.5px;
  background:rgba(200,169,106,.22);color:#8a6d2f;vertical-align:1px}
.jz-official{font-weight:600}
/* ============================================================
   芥子的**资源条**（jiezi-view.js 的 hudHtml）
   ------------------------------------------------------------
   ★ 2026-10 所有者给了一张市面游戏截图 +「**视觉表现是一个游戏的核心**，
     **你这做的完全就不像游戏**」+「**芥子无非就是玩家长时间驻留的元碎片**」。
   一个长时间待着的地方，第一眼要能回答「我现在有什么」。
   ⚠️ 横向排布、可换行（手机上不能挤成一行看不见）；
      数字用等宽感觉（大一点、粗一点），标签小一点 —— 一眼读数字，细看才知道是什么。
   ⚠️ 「满了」的那一格高亮：那是**该做点什么**的信号。
   ============================================================ */
.jz-hud{display:flex;flex-wrap:wrap;gap:6px;margin:6px 0 8px}
.jz-chip{display:inline-flex;align-items:baseline;gap:3px;padding:3px 9px;border-radius:9px;
  background:rgba(46,107,94,.10);border:1px solid rgba(46,107,94,.22);line-height:1.5}
.jz-chip b{font-size:var(--text-xs);font-weight:500;opacity:.72}
.jz-chip em{font-size:15px;font-style:normal;font-weight:700}
.jz-chip i{font-size:var(--text-xs);font-style:normal;opacity:.6}
.jz-chip.full{background:rgba(200,169,106,.20);border-color:rgba(200,169,106,.55)}
.jz-chip-qi{background:rgba(158,59,50,.09);border-color:rgba(158,59,50,.24)}
.jz-chip-space{background:rgba(27,27,31,.06);border-color:rgba(27,27,31,.16)}
/* ============================================================
   摆件右键菜单（jiezi-view.js 的 showMenu）
   ------------------------------------------------------------
   ★ 2026-10（设计侧第二十批第 3 批）：QQ 农场那条路径 ——
     **点那块地 → 弹出「种/收/查看」→ 点一下 → 成了**。
   ⚠️ 菜单挂在**舞台容器**里（不是 body），所以随页面一起走。
   ⚠️ 还没满足条件的项**灰掉并说清为什么** —— 玩家要看得见"还差什么"，
      不是一个点了没反应的按钮。
   ============================================================ */
.jiezi-item{cursor:pointer}
.jiezi-item.active{outline:2px solid rgba(200,169,106,.85);outline-offset:2px}
.jiezi-menu{position:absolute;right:10px;bottom:10px;z-index:1900;
  min-width:150px;max-width:260px;padding:6px;border-radius:var(--radius-md);
  background:rgba(255,255,255,.97);border:1px solid rgba(27,27,31,.16);
  box-shadow:0 10px 26px rgba(0,0,0,.20);text-align:left}
.jiezi-menu-title{display:block;padding:3px 7px 5px;font-size:var(--text-xs);opacity:.62;font-weight:500}
.jiezi-menu-item{display:block;width:100%;margin:2px 0;padding:7px 9px;border:0;border-radius:var(--radius-sm);
  background:rgba(46,107,94,.09);color:inherit;font-size:var(--text-base);text-align:left;cursor:pointer}
.jiezi-menu-item:hover{background:rgba(46,107,94,.18)}
.jiezi-menu-item.off{background:rgba(27,27,31,.05);cursor:not-allowed;opacity:.62}
.jiezi-menu-why{display:block;margin-top:2px;font-size:10px;font-style:normal;opacity:.75}
/* ============================================================
   芥子的**大厅入口卡**（jiezi-view.js 的 .jz-lobby / .jz-door）
   ------------------------------------------------------------
   ★ 2026-10 所有者：「**参考元碎片的游玩方式**，**芥子只放主线入口**，
     **点击进入游戏界面**不就行了」
   🔴 之前这一页堆着 **22 颗平铺按钮** —— 那是**工具栏**，不是大厅。
   ⇒ 每张卡 = 一个**去处**：名字 + 一句话说清进去能做什么 + 「进去 ›」
   ⚠️ 网格排布、手机上自动落成一列；卡片本身是 button（键盘也能用）。
   ⚠️ 「维护」那几颗（体检/撤销/导出/扩充）退到 `.jz-utils` 一行小按钮 ——
      它们不是"去处"。
   ============================================================ */
.jz-lobby{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));
  gap:var(--space-2);margin:8px 0}
.jz-door{display:flex;flex-direction:column;align-items:flex-start;gap:2px;
  padding:10px 12px;border:1px solid rgba(46,107,94,.24);border-radius:var(--radius-md);
  background:rgba(46,107,94,.07);color:inherit;text-align:left;cursor:pointer}
.jz-door:hover{background:rgba(46,107,94,.15);border-color:rgba(46,107,94,.45)}
.jz-door b{font-size:var(--text-md)}
.jz-door i{font-size:var(--text-xs);font-style:normal;opacity:.72;line-height:1.6}
.jz-door em{font-size:var(--text-xs);font-style:normal;margin-top:3px;opacity:.85;
  color:#2E6B5E;font-weight:600}
.jz-utils{opacity:.82}
/* ★ T3（`consult-256-answer.md` §三 风险表）：主线那张卡搬进悬浮球菜单之后，
   这一页要有一句**指路**（"首次进芥子页会空"那条风险的落点）。
   它是**说明**不是控件 —— 用 `.tiny` 那一族的长相，不抢门卡的注意力。 */
#jz-lobby-note{margin:var(--space-2,8px) 0 0;opacity:.78;line-height:1.7}
/* ============================================================
   芥子 · **进屋先看到屋子**（2026-10-10 · 所有者：「我不想进芥子界面还是这堆选项」）
   ------------------------------------------------------------
   ★★ 2026-10-11（**本轮改掉了下面这条设计**）：所有者原话 ——
      「**都要动**，而且**他应该是一个交互入口，点了他就切到整个功能对应的界面，
        而不是做成一个抽屉**，那样**打开会污染界面，变得太丑了**」
      ⇒ 编辑用的那些（域卡 / 规则列表 / 试着触发 / 对齐 / 部件 / spec 编辑）
        **不再"收进一个抽屉"** —— 它们搬进了**它自己那一屏** `#page-jz-dressup`
        （五藏山经同理，搬进 `#page-jz-mainline`）。
      🔑 `#page-home` 上现在**只剩一个类**，由 `jiezi-view.js` 的 `applyHouse()` 写：
        · `jz-house` —— 「这一屏**真的**画出了一间屋子」（3D 挂载成功 / 等距画出来了）
      ⚠️ 类名由**渲染结果**决定，不是由"打算画什么"决定 ——
         画不出来就不加 `jz-house`，一切照旧（不摆一个空架子）。
      ✅ "一次只有一页在版面里"这件事**不用 CSS 再管一遍**：`.page` 自己是
         `display:none` / `.active` 才是 `display:block`（见文件上面那节）
         ⇒ 进了子页，芥子那一页**根本没被排版**。
      ⚠️ 手机上 `--vh` 已经是 `100dvh`（见文件顶部那段），所以"占满可视区"在
         地址栏收放时也成立。
   ============================================================ */
/* ① 屋子排到最前 —— **靠挪 DOM，不靠 CSS 的 `order`**（本轮实测栽过一次，必须写清）：
      `order` 要求父元素是 flex/grid，而把 `#page-home` 改成 `display:flex` 之后，
      它作为 `.pages`（`display:grid`、auto 轨道）里的 item，**宽度会被内容撑开**：
      实测 488 → **1404px**（整页横向溢出，屋子画布跟着变成 1402 宽）。
      ⇒ 改由 `jiezi-view.js` 的 `applyHouse()` 把**芥子自己建的四个节点**
        （stage / drawer / panel / mainline）挪到页面最前，别的页面的排版一点不碰。
      ★ 2026-10-11：`drawer` / `panel` / `mainline` 三个**已经不在 `#page-home` 里了**
        （搬进各自那一屏）⇒ 这条"排到最前"现在实际只管 `#jiezi-stage` 一个；
        那三个由 `mountJzPages()` 搬到它们自己的页里（幂等）。
   ⚠️ 所以这里只剩"屋子多高"这一件事 —— **只有一条**（正下面是本轮删掉的那条）。 */
#page-home.jz-house #jiezi-stage{margin:2px 0 10px;
  height:calc(var(--vh) - 170px);min-height:300px}
/* ============================================================
   ★★ 2026-10-11：**删掉了那条"抽屉开着就把屋子压小"的规则**
   ------------------------------------------------------------
   删掉的那条（**这里只描述、不引原文** —— 那条判据是"那个数不许再出现"，
   引了它就会自己命中自己；本仓为这件事写过好几次注释，`tests/jiezi-view.test.js`
   那条档位文案判据的注释里记着同一条教训）：
     「`#page-home` 同时带 `jz-house` 与 `jz-drawer-open` 两个类时，
       `#jiezi-stage` 的高度被改写成一个**小得多的百分比**（约四成视高）」
   🔴 为什么删：所有者这一轮的原话 ——
     「**都要动**，而且**他应该是一个交互入口，点了他就切到整个功能对应的界面，
       而不是做成一个抽屉**，那样**打开会污染界面，变得太丑了**」
     那条规则**就是"污染界面"的可观察形式**：点一下「装扮 / 编辑这间屋子」，
     屋子当场从 830px 掉到 420px（`tools/jz-entry-probe.js` 量的原始数字）。
   🔑 现在两个入口走的是**切页**：`.page` 一次只有一页 `display:block`
     （见文件上面那节 `.page{grid-area:1/1;display:none}`）⇒ 芥子那一页
     **根本没被排版**，也就没有任何东西需要"让出下半屏"。
   ⚠️ 同时删掉的还有三条**只服务于"就地摊开"**的规则（同样只描述、不引原文）：
     · 「屋子档下、开合类名**不在**时，把编辑面与域卡面板设为不显示」
     · 「屋子档下、主线那个开合类名**不在**时，把主线块设为不显示」
     · 「开合类名**在**时，给编辑面块加一道上边距」
     它们的前提（"那两块在 `#page-home` 里、靠类名显隐"）已经不存在了 ——
     两块内容现在住在**它们自己那一页**里，"显不显示"由 `.page.active` 一句话说了算。
     ⚠️ 留着它们会更坏：那会让"隐藏"有两个出处（`display:none` 与 `.page` 的
        `display:none`），而其中一个永远不生效 —— 本仓库最忌的形状。
   ⚠️ `jz-drawer-open` / `jz-main-open` 这两个类名现在**再也没有人写**（`jiezi-view.js`
     的 `applyHouse` 恒传 `false/false`）。⇒ 上面那三条规则连同它们的类名一起清掉；
     `#page-home` 上仍留 `jz-house`（"这一屏真的画出了屋子"），那条一个字没动。 */
/* 🔴 3D 那一档的**平面高度**（本轮实测栽过一次，必须写清为什么）：
   定高的两半分别在两个地方 —— 舞台高度由上面那条给，而"平面 = 舞台"这一句
   **只有 `scene3d` 那个降级后端会写**（`jiezi-3d.js` 的 `wantH = stageH`）；
   **`three` 那一档不写**：它只量 `.jz3d-host`（`inset:0`，跟着平面走）。
   ⇒ 只给舞台定高、不给平面定高时，平面退回 `.jiezi-plane.iso3d` 的 `aspect-ratio:16/9`
      **兜底比例** —— 实测舞台 488×790、画布只有 486×273：屋子成了中间一条横带
      （图看着"画出来了"，但那不是"满屏的屋子"，正是所有者不要的那种）。
   `height:100%` 在这里能解析（舞台有确定高度），所以不需要 `!important`。
   ⚠️ **同时要把 `aspect-ratio` 撤掉**（写 `auto`）：它在下面那条兜底规则里是给
      "量不到真实高度"用的；留着它的话，"高度 = 100%（确定值）"会反过来按 16:9
      推出一个**宽度**（实测 790×16/9 ≈ 1404px）——屋子把整页撑宽、横向溢出。
      这正是"两条来源打架"的又一个实例。 */
#page-home.jz-house #jiezi-stage .jiezi-plane.iso3d{height:100%;aspect-ratio:auto}
/* ============================================================
   ★★ 2026-10-11：芥子的**两个功能页**（点入口之后切过去的那两屏）
   ------------------------------------------------------------
   所有者原话：「**他应该是一个交互入口，点了他就切到整个功能对应的界面，
     而不是做成一个抽屉**，那样**打开会污染界面，变得太丑了**」。
   🔑 这两屏里放的是**原来那两块节点本体**（`#jiezi-drawer` / `#jiezi-panel` /
      `#jiezi-mainline`），所以这里**只写"摆放"**（留白与卡片间距）——
      里面那些块的样式一条都不用重写（它们自带 `.card` / `.jiezi-panels` 那套）。
   ⚠️ 两块内容都是**整页宽**的卡片：给它们与页头之间的缝，与芥子页那条
      `#jiezi-mainline{margin-top:10px}` 同一条规矩（一眼分得开"标题"与"内容"）。
   ⚠️ 手机档：`.page` 是 `display:block` 的一整块，没有横向最小宽度 ⇒
      这一屏不会成为新的横滚来源（判据由 `tools/jz-entry-probe.js` 量
      `documentElement.scrollWidth` vs 布局视口）。 */
#page-jz-dressup #jiezi-drawer,
#page-jz-dressup #jiezi-panel,
#page-jz-mainline #jiezi-mainline{margin-top:10px}
/* ============================================================
   ★ 2026-10-11：这两屏里那几排 `.row-btns` **也要能换行**（手机档 ⑥ 号缺陷）
   ------------------------------------------------------------
   实测（手机档 390×844，改前既有、不是本轮引入；别人先量到、我用
   `tools/mobile-floor-probe.js --blame auto` 归到因）：
     · 装扮那一屏：`documentElement.scrollWidth=482` vs 布局视口 390 ⇒ **溢出 92px**
     · 主线那一屏：449 vs 390 ⇒ **溢出 59px**
     归因链（逐层隐藏/隔离量出来的，不是猜）：
       `#page-jz-dressup` → `#jiezi-panel` → `div.row-btns`（**434 宽**，flex 不换行）
       `#page-jz-mainline` → `#jiezi-mainline` → `div.row-btns`（**401 宽**）
     ⇒ 与拾光页（③）**同一个形状**：一排按钮排不下就整条撑出去。
   ⚠️ 为什么不把这句写进 `.row-btns` 自己（全局换行）：那个类被 20 多个模块用着，
      而它"不换行"是**写明的设计前提**（为两三个短按钮设计，见文件上面 `.row-btns` 那一节）；
      本仓既有做法就是**在哪一排太长时就地给它一条**（`#jiezi-first-card` 那条也是这么来的，
      而且 `tests/first-run.test.js` 把它钉成了判据）⇒ 沿用同一个形状，不动全局。
   ============================================================ */
#page-jz-dressup .row-btns,
#page-jz-mainline .row-btns{flex-wrap:wrap}
/* ============================================================
   ★ 2026-10-11：工坊页**非默认那几段**里的 `.row-btns` 要能换行（同一形状的第三处）
   ------------------------------------------------------------
   实测（手机档 390×844，`tools/mobile-floor-probe.js --pages workshop --open '.segbtn[data-sub=skilllib]' --blame auto`）：
     点「技能」那一段之后再量：`documentElement.scrollWidth=465` vs 布局视口 390 ⇒ **溢出 75px**
     （`inner` 被 Chrome 改成 465×1005 —— 又是"缩小以适配"）
     归因链（逐层隐藏量出来的）：`#sub-skilllib` → `div.plugin-shell` → 里面那几排 `.row-btns`（flex 不换行）
   🔴 **作用域只给"非默认那几段"**（`#sub-skilllib` / `#sub-frameworks` / `#sub-manage`），
      第一版写成 `#page-workshop .row-btns` —— 那会连**默认那一段**（`#sub-skills`）的排版一起挪：
      实测两颗 `button.sbtn「fork」` 的中心正好落进底栏那一条（@[130,819] / @[271,819] → 点到 tab）
      ⇒ 判据 `mobile_page_usable` 的"页顶那一遍"当场变红（判据量的就是默认那一段）。
      **判据量哪一段，修法就落在哪几段上**——这一条是本轮"自己把自己判红"的实例，记一笔。
   ⚠️ 为什么判据不会红、我却还是要修：横向溢出会**跨页继承**（Chrome 的"缩小以适配"不随换页复位）——
      判据里那句「视口被上一页的自动缩小以适配污染 ⇒ 重新加载一次再点导航」就是被它触发的。
   ============================================================ */
#sub-skilllib .row-btns,
#sub-frameworks .row-btns,
#sub-manage .row-btns{flex-wrap:wrap}
/* 「回芥子」那颗按钮：页头右侧，与页头里别的按钮同一种长相（`.ghost tiny`）——
   它走的是**同一套换页机制**（`[data-jzback]` → `window.showPage('home')`）。 */
.page-head [data-jzback]{flex:0 0 auto}
/* ⚠️ 这里**没有**给 `.jiezi-plane` 写高度，是故意的：
   屋子的高度由**舞台**决定（上面那条 `height`），两条渲染路都从舞台取尺寸 ——
   3D 是 `jiezi-3d.js` 按 `stage.clientHeight` 给平面定高，
   2.5D 是 `placeIso()` 按舞台的 `cw × ch` 算缩放。
   在这里再写一条平面高度 ⇒ 两个来源打架（本仓库反复栽的形状）。 */
/* ============================================================
   屋子档 · **那几块旧的常驻块不再挤压屋子**（2026-10-10）
   ------------------------------------------------------------
   所有者原话（附实机截图：最上面是对话气泡、中间是「🐾 养一只宠物 / 🔌 外部工具」，
   屋子在最下面）：「**都已经有新的屋子了，怎么还不删上面这个**」
   🔴 判断（与所有者那句话对齐）：这不是"删功能"，是"**别让它们占位**"——
      入口一个都没删、点了照旧能用，只是从"文档流里的一行"改成"**浮层**"。
   🔴 核实到的事实（两块的处境**不一样**，所以处理也不一样）：
      · `.home-bar`（那排入口按钮）—— 原来是**文档流里的一行**（`margin-top:10px`），
        真占纵向空间 ⇒ 这里改成**浮层**（与 `.tabbar` 同一种做法：fixed + 居中同宽）。
      · `#home-bubble`（对话气泡）—— **本来就是浮层**（`.home-bubble` 是
        `position:absolute`，挂在 `.scene` 里）⇒ 它**从来没有**把屋子往下推。
        它真正的问题是**常驻不散**（`homeSay` 写完就一直在那儿），
        那一条在 `first-run.js` 的 `watchHomeBubble()` + 下面的 `.jz-bubble-away` 里解决。
   ⚠️ 浮层定位在**底栏之上**（`bottom:52px`，`.tabbar` 高约 53px）——
      它是"页内浮层"：`#page-home` 不是 active 时整页 `display:none`，不会跑到别的页上。
   ⚠️ 浮层只占屋子**下沿**那一条，房间中心是空的（与 `#home-guide` 规格书里
      "不挡屋子中心"同一条精神）。 */
#page-home.jz-house .home-bar{position:fixed;bottom:52px;left:0;right:0;max-width:520px;
  margin:0 auto;z-index:14;flex-wrap:wrap;gap:6px;padding:5px 10px;
  border-radius:14px 14px 0 0;background:rgba(255,255,255,.72);
  backdrop-filter:blur(8px);box-shadow:0 -2px 10px rgba(20,30,50,.12)}
/* ⚠️ 2026-10-10 二次调整（评审：「底部那排入口仍占了一整行」）：
   上面那两条 `tiny` 说明文字会让工具条**折成两行**（实测高 ~76px，压住屋子下沿一大块）。
   ⇒ 屋子档把**那条一次性教程**（`#pet-hint`）收起来：工具条回到**一行**（~44px）。
   ⚠️ 它**没有删** —— DOM 里还在、非屋子档照旧显示；而且它说的那件事（她会在屋里溜达、
      点她打招呼、按住能拖）在**她本人在场**时是最容易自己发现的（她就在你眼前动）。
   ⚠️ `#room-hint`（"空屋子/已摆放 N 件"）**留着**：那是**状态**，不是教程。 */
#page-home.jz-house .home-bar #pet-hint{display:none}
/* 工具条里的面板（MCP）**向上开**：容器浮起来之后，面板不能还按"流里的一块"排 ——
   那会挤在这条窄带里。挪到工具条**上方**，高度自己收（超高就滚）。 */
#page-home.jz-house .home-bar .palette{position:absolute;left:0;right:0;bottom:calc(100% + 8px);
  margin-top:0;max-height:52vh;overflow:auto}
/* ============================================================
   ★★ 2026-10-10：`#scene` 从「一间屋子」降级成**盖在屋子画面上的透明浮层**
   ------------------------------------------------------------
   所有者原话：「**只留这个屋子**，这个黄色的背景是最早的 1d 屋子，删掉啊」
   + 「让朏朏真的在屋里」。
   🔴 为什么是"浮层"，**不是**把她的 DOM 挪进 3D 宿主（`.jz3d-host`）：
      宿主每次重渲染都被 `host.innerHTML = html` **重建**
      ⇒ 挪进去 = 每画一次屋子就把她连根拔掉（桌宠引擎的实例、她身上那些监听、
        `app.js` 里的 `$('#pet')` 全成孤儿，而且**不报错**）。
      ⇒ 她留在原地，**由宿主来就她**：量出屋子那一组的矩形，把宿主摆上去。
   🔴 浮层的矩形由 **JS** 写成屋子的矩形（`jiezi-view.js` 的 `placeRoomLayer()`）——
      于是桌宠引擎写的 px 坐标 = **屋子画布的像素坐标**：她在屋里走。
   ⚠️ `pointer-events:none`：浮层整块盖在画布上，不关掉它就没法转视角 / 点家具。
      只放开两处：她本人（`.home-pet`）与旧摆件（`.ritem`）—— 它们本来就要点得到；
      编辑态（`.edit-mode`，`app.js` 的「布置芥子」贴的类）把**整块**放开：
      那个功能本来就是"点屋子里的位置放东西"（`#scene` 的 click 在 `app.js` 里）。
   ⚠️ `z-index:1`：压住画布（auto/0），又被屋子那排控制条（`.jz-ctl` = 6）与
      漂浮球（7）压住 —— 数字全落在既有的那一梯里，**没有新加一层**。 */
#page-home.jz-house{position:relative}
#page-home.jz-house .scene{position:absolute;margin:0;min-height:0;
  background:none;border-radius:0;box-shadow:none;overflow:visible;
  pointer-events:none;z-index:1}
#page-home.jz-house .scene.edit-mode{pointer-events:auto}
#page-home.jz-house .scene .ritem{pointer-events:auto}
/* 她的位置由**引擎**写（`left` = 她那条地带里的 px），这里只定"站在哪一行"：
   屋子档下那条地带的下沿就是**屋子的前场地面**（矩形由 JS 算，见 placeRoomLayer）。 */
#page-home.jz-house .home-pet{bottom:0}
/* ⚠️ 她**不在场**时那颗空盒子**不许**吃点击：`#pet` 就算没挂立绘也占着 150×150
   （`.home-pet` 的宽高），而浮层整块是 `pointer-events:none` —— 只有她真在场
   （`jz-pet-in-room`）才把那一块放开。否则屋子前场上会有一片"看着什么都没有、
   点了没反应"的死区（在那儿转视角 / 点家具都会失灵）。 */
#page-home.jz-pet-in-room .home-pet{pointer-events:auto}
/* 她说话那颗气泡：挂在**她那条地带的顶上**（屋里、她头顶上方）。
   宽度由 JS 按屋子宽度给 ⇒ 屋子档不再用 max-width:78%（那是按旧宿主宽算的）。 */
#page-home.jz-house .home-bubble{bottom:calc(100% + 8px);top:auto;max-width:none}
/* ============================================================
   屋里只站**一位**朏朏：真身（桌宠本体）在场时，那两张**向导立绘**让位
   ------------------------------------------------------------
   🔴 为什么要有这一条：`#home-guide`（屋外那张 HTML 立绘）与画布里那张 billboard
      是"**没养宠物时**她是始终在场的向导"那件事的实现（规格书 consult-107）。
      而真身入场之后，同一只朏朏会在同一屏出现两次 —— 那是 bug，不是"向导 + 宠物"。
   ⚠️ 与既有的 `#page-home[data-jzdim="3d"] #home-guide{display:none}` 同一条分工原则
      （那条只管 3D 档）；这一条按"**她本人在不在**"决定，2.5D / 2d 档同样成立。
   ⚠️ 只是 `display:none`：元素、接线、`drawGuideSprite()` 一个都没删 ——
      没养宠物时，那两张立绘照旧是她。
   ⚠️ 那条类是 `jiezi-view.js` 按"`#pet` 里有没有立绘"贴的（`syncPetInRoom()`），
      而且挂了个观察器：**养了宠物当场就换**（不用等下一次重渲染）。 */
#page-home.jz-pet-in-room #home-guide{display:none}
/* 说话的出口**一句只说一遍**（3D 档有两颗气泡：画布里那颗镜像 + 屋里这颗 HTML）：
   · 真身不在场（画布里那张 billboard 在）：由**镜像**那颗说 —— 它每帧按她的世界坐标
     投影，转镜头/缩放时**跟着她的头顶走**（这是它存在的理由）；
   · 真身在场（billboard 让位）：镜像没得可锚 ⇒ 由**屋里这颗 HTML** 说 ——
     它挂在她的地带上，她走到哪、话就在哪顶上冒出来。 */
#page-home.jz-house[data-jzdim="3d"]:not(.jz-pet-in-room) #home-bubble{display:none}
#page-home.jz-pet-in-room .jz3d-say{display:none}
/* 说过的话**不常驻**：完整显示几秒 → 自己淡出。
   ⚠️ `first-run.js` 的 `watchHomeBubble()` 会在**每次有新话**时重新触发这段动画
      （只靠 CSS 不行：动画只跑一次，而 `homeSay` 是改 textContent，不是重建元素）。 */
.home-bubble.jz-bubble-away{animation:jz-bubble-away 9s ease forwards}
@keyframes jz-bubble-away{0%,68%{opacity:1}100%{opacity:0;visibility:hidden}}
/* 减少动效：**关掉淡出**（气泡回到"常驻"的旧行为）—— 宁可不淡，也不要"闪一下就没了" */
@media (prefers-reduced-motion:reduce){.home-bubble.jz-bubble-away{animation:none}}
/* 浮在屋子上的那一行控制（**抽屉关着也在**）：
   左 = 玩家的档位开关（2.5D / 3D），右 = 抽屉开合。
   ⚠️ 它不吃屋子的高度（绝对定位浮在上面）—— 那样"进屋只有屋子"才成立。
   ⚠️ 半透明 + 模糊：3D 屋子的底色深浅不一，纯色底会在某些角度看不清字。 */
/* ============================================================
   **屋里朏朏说的那句话**（2026-10-10 · 所有者：「这个气泡开场白就应该是屋子里朏朏说的」）
   ------------------------------------------------------------
   元素由 `jiezi-3d.js` 的 `buildGuide()` 建在 3D 宿主里，`left/top` **每帧**
   由她的世界坐标投影出来（`Vector3.project(cam)`）⇒ 它锚在**她头顶**，跟着镜头走。
   ⚠️ `pointer-events:none`：它浮在画布上 —— 挡住下面那块就没法点选家具了
      （同形状的坑本仓库在 3D 信息条上踩过一次）。
   ⚠️ `transform:translate(-50%,-100%)`：定位点 = 气泡**下沿中点**（所以是"从头顶冒出"）。
   ⚠️ `:empty` 不占位、看不见：她没说话时不该在画面里留一块白斑。
   ⚠️ 说过几秒淡出（`jz3d-say-away`），由 `buildGuide()` 在**每次新话**时重新计时 ——
      只靠 CSS 不行：动画在一个元素上只跑一次，而这颗气泡是复用的。 */
.jz3d-say{position:absolute;left:0;top:0;transform:translate(-50%,-100%);z-index:5;
  max-width:74%;padding:7px 11px;border-radius:14px;pointer-events:none;
  background:rgba(255,253,248,.94);box-shadow:0 2px 10px rgba(20,30,50,.18);
  font-size:var(--text-sm);line-height:1.55;color:#3A2E22;white-space:pre-wrap}
.jz3d-say:empty{display:none}
.jz3d-say.jz3d-say-away{animation:jz3d-say-away 9s ease forwards}
@keyframes jz3d-say-away{0%,68%{opacity:1}100%{opacity:0}}
@media (prefers-reduced-motion:reduce){.jz3d-say.jz3d-say-away{animation:none}}
/* ============================================================
   向导立绘的**分工**（别让屋里屋外各站一个朏朏）
   ------------------------------------------------------------
   · 3D 档：她在**屋里**（`jiezi-3d.js` 立的那一个 billboard）⇒ 屋外那张 `#home-guide` 收起来；
   · 2.5D / 2d 档：屋里没有她 ⇒ 屋外这张**照旧**（`first-run.js` 的 `drawGuideSprite()` 画的）。
   ⚠️ 用 `data-jzdim`（由 `jiezi-view.js` 的 `applyHouse` 写在 `#page-home` 上）——
      不新增类名，也不必让 CSS 去猜"这一屏画的是哪一档"。 */
#page-home[data-jzdim="3d"] #home-guide{display:none}
.jz-ctl{position:absolute;left:8px;right:8px;top:8px;z-index:6;display:flex;
  align-items:center;justify-content:space-between;gap:6px;pointer-events:none}
.jz-ctl > *{pointer-events:auto}
/* 🔴 2026-10-15 · 件 T3（反馈①「这些设置重复了」）：
   `.jz-dims` / `.jz-dim` / `.jz-dim.on` 三条**已删** ——
   它们只服务页内那一排 `[data-jzdim]` 档位按钮，而那一排按
   `consult-255-answer.md` §2.1（判丙"合并且挪走"）**整个删掉了**
   （判据：`dimsHtml` 的调用点 = 0；`.jz-ctl` 里数出来的 `[data-jzdim]` = 0）。
   三档的样式仍在（`.jz-screen` / `.jz-smode`，见本文件 T-B6 那一段）——
   它们现在唯一的落点是悬浮球菜单（`.jz-fab-menu .jz-screen`，见本文件末尾那一段）。
   ⚠️ 不留死样式："加了类没写样式"由 `tools/check-css-classes.js` 挡；
      反方向（写了样式没人用）同样是"下一个人以为它还有用"的来源。
   ⚠️ `.jz-ctl` 本身**留着**：两颗入口按钮（装扮 / 主线）就住在这行里，
      它的 absolute + z-index:6 是那两颗按钮的落点与层级（见 `putEntriesBack()`）。 */
/* ============================================================
   🔴 2026-10-15 · 件 T3（反馈③「布置芥子的这些统统没用」）：
      **编辑模式**（`.jz-editable`）与**拖动手柄**（`.jiezi-handle`）
   ------------------------------------------------------------
   裁定原文 `design-out/consult-255-answer.md` §2.3：
     「进入编辑模式：摆件**显示拖动手柄**（现在默认就有的那个）」
     「⚠️ **非编辑模式下摆件不可拖**（避免误拖）」
   🔑 这里只做**看得见**那一半（"现在能不能拖"的机器判据是
      `jiezi-view.js` 里 `wireDrag()` 那一句 `if (!editMode()) return;`，
      以及 `.jz-editable` 这个类 —— 两处读的是**同一个** `editMode()`）。
   ⚠️ 手柄**不是一个新控件**：它没有 id、不接任何事件，只是"这一件现在可以拖"的记号。
      非编辑模式下 `display:none`（不是"透明但仍可点"——那还是会给玩家错觉）。
   ⚠️ 光标也给一份提示：编辑模式 `grab` / 按住 `grabbing`；非编辑模式**不给** grab
      （给 `pointer` —— 因为点一下仍然会弹规则菜单，那是始终可用的那条路）。 */
.jiezi-handle{display:none}
.jz-editable .jiezi-item{cursor:grab;outline:1px dashed rgba(200,169,106,.75);outline-offset:1px}
.jz-editable .jiezi-item .jiezi-handle{display:inline-block;margin-right:3px;opacity:.85;
  font-size:11px;line-height:1;cursor:grab}
.jz-editable .jiezi-item.dragging{cursor:grabbing;outline-style:solid}
.jz-editable .jiezi-item.dragging .jiezi-handle{cursor:grabbing}
/* 非编辑模式：摆件**照旧可点**（弹规则菜单那条路一直可用），但**不是**可拖的样子 */
.jiezi-plane:not(.jz-editable) .jiezi-item{cursor:pointer}
/* 键盘焦点必须看得见（摆件现在进 Tab 序列了 —— 见 `.jiezi-item[tabindex]` 那条注释） */
.jiezi-item:focus-visible{outline:3px solid var(--action);outline-offset:2px}
/* 菜单里那一项「删除」：**破坏性** ⇒ 用危险色（与 D13/D77 那套 `.modal-danger` 同族） */
.jiezi-menu-item.jiezi-menu-del{color:var(--error)}
.jiezi-menu-item.jiezi-menu-del span{font-weight:700}
/* ============================================================
   屋子右上角那两颗"点开一个面板"的按钮（**同一套长相**：不抢画面、一眼看得出能点）：
     `#jz-drawer-toggle`（装扮 / 编辑这间屋子）· `#jz-main-toggle`（主线 · 五藏山经）
   ⚠️ 两块**共用一条声明** —— 长得不一样会让人以为是两类东西。
   ============================================================ */
.jz-drawer-btn, .jz-main-btn{padding:3px 10px!important;border-radius:var(--radius-full);
  background:rgba(255,255,255,.82);backdrop-filter:blur(6px);
  box-shadow:0 1px 6px rgba(20,30,50,.16)}
/* ============================================================
   ★★ 屋中的**漂浮球**：五藏山经做成一个 UI 入口（2026-10-10 · 所有者原话）
   ------------------------------------------------------------
   「**这个五藏山经做成一个 ui 入口，类似漂浮球，漂浮在屋中，点进去才展开**」。
   🔑 它**不是一个新控件** —— 还是原来那颗 `#jz-main-toggle`（id 一个字没改）。
   ★★ 2026-10-11（本轮）：**它没挪窝，改的是"点下去发生什么"**。
      所有者这一轮说：「**整个球在芥子空间里，屋子也在芥子空间内**」
        ⇒ 球**仍应在 3D 空间里**（那是"五藏山经的入口"），
          「**只是"点开之后"要切页而不是就地展开**」。
      所以这一节的**位置那半一个字没动**（仍是 `position:absolute` + 由
      `placeRoomLayer()` 按屋子内区左上角 `0.22×宽 / 0.22×高` 算出来的落点）；
      改的只有"点开之后"：现在是 `window.showPage('jz-mainline')`（**切页**），
      不再是"就地摊开 `#jiezi-mainline`"——那条 `display:none` 的规则已经删了
      （见上面那段"删掉了那条…"）。
   ⚠️ `[aria-expanded="true"]` 那个 `✕` 字形**删掉了**：这个属性现在**恒为 false**
      （它不再展开任何东西）⇒ 那条规则永远命中不了，留着就是死样式。
   ⚠️ 尺寸 56×56：移动端最小可点尺寸是 44×44（判据建议值），留出余量。
   ⚠️ 为什么把文字**藏起来**（`font-size:0`）而不是删掉：`applyHouse()` 每次都要重写
      `textContent`（在芥子页写"点它去那一屏"、在那一屏写"你正在这一屏"）——
      删字就等于把那半条接线拆了。字留着（读屏仍读得到），
      看得见的只有 `::before` 那个字形；**可读名字靠 `aria-label`**（写在 HTML 串里）。
   ⚠️ 漂浮动效是**装饰级**（不承载任何信息）⇒ `prefers-reduced-motion` 下关掉；
      关的是动画，位置、尺寸、可点性一点不变（**不是** `display:none`）。 */
#page-home.jz-house .jz-main-btn{position:absolute;z-index:7;
  width:56px;height:56px;min-width:56px;min-height:56px;padding:0!important;
  display:flex;align-items:center;justify-content:center;
  border-radius:50%;font-size:0;line-height:1;
  background:radial-gradient(circle at 34% 28%,#ffffff,rgba(255,252,246,.9) 58%,rgba(240,228,200,.94));
  box-shadow:0 4px 14px rgba(40,30,10,.3),0 0 0 2px rgba(200,169,106,.55) inset;
  animation:jz-ball-float 4.6s ease-in-out infinite}
#page-home.jz-house .jz-main-btn::before{content:'☰';font-size:22px;color:#6b5326}
@keyframes jz-ball-float{0%,100%{transform:translateY(0)}50%{transform:translateY(-4px)}}
@media (prefers-reduced-motion:reduce){#page-home.jz-house .jz-main-btn{animation:none}}
/* 档位出问题时那一行（**在画面上说**，不是只写 console）：屋子左上角、控制条下面 */
.jz-dim-note{position:absolute;left:8px;right:8px;top:42px;z-index:6;
  font-size:var(--text-xs);line-height:1.5;padding:6px 9px;border-radius:10px;
  background:rgba(255,244,230,.94);border:1px solid rgba(200,140,60,.45);color:#7a4a12}
/* 编辑面那块容器（`#jiezi-drawer`）：它自己**没有样式职责**（不加边框、不加背景）——
   里面那些块自带 `.card` / `.jiezi-panels` 的既有样式 ⇒ 保持与原样一模一样。
   ★ 2026-10-11：它现在住在 `#page-jz-dressup` 里（原来在 `#page-home` 里当"抽屉"）——
     这里一个字不用改：它本来就没有定位，跟着父节点走。 */
.jz-drawer{display:block}
.jz-what{margin:4px 0 8px;opacity:.9}
/* ============================================================
   等距房间（`iso-scene.js` 画出来的东西）
   ------------------------------------------------------------
   ★ 所有者 2026-10：「**用户打开 app，芥子界面就是这个小屋**……**做成这样**……
     **app 框架里的渲染引擎如果做不到，就换渲染**」（二之八）
   以前每个摆件是"带 emoji + 一行字的盒子"，平摊在平面上 —— 看不出屋子。
   现在：**地板（菱形网格）+ 墙 + 有厚度的家具（三面）**。
   ⚠️ 形状全部由 `iso.js` 算出来的 `clip-path` 给，CSS 只负责**颜色与层次**。
   ⚠️ 调色照 `xianxia` 主题（宣纸底 · 青绿 · 朱砂 · 描金）。
   ============================================================ */
.iso-stage{position:relative;overflow:hidden;
  background:radial-gradient(circle at 50% 28%,#fffdf8,#E4DCCC)}
.iso-plane{position:absolute;transform-origin:0 0}
/* 地板：网格在等距下自动变成菱形地砖 */
.iso-ground{position:absolute;
  background:
    repeating-linear-gradient(90deg,rgba(140,110,80,.20) 0 1px,transparent 1px 200px),
    repeating-linear-gradient(0deg,rgba(140,110,80,.20) 0 1px,transparent 1px 200px),
    linear-gradient(135deg,#CBA97C,#B08F63)}
/* 墙：靠后的两片（后墙亮、左墙暗 —— 光源从右上来的感觉） */
.iso-wall{position:absolute}
.iso-wall-back{background:linear-gradient(180deg,#f7f1e5,#e4dccc)}
.iso-wall-left{background:linear-gradient(180deg,#eee4d4,#d6c8af)}
/* 摆件：一个"对象"是它的影子 + 三面 + 名字牌 */
.iso-obj{position:absolute;left:0;top:0}
/* ★★ T1③（consult-255 §三 T1 第③条）：**2.5D 那一档的摆件也是可拖的** ——
   它一直是可点 DOM（带 `data-jz`），现在 `wireDrag()` 也认它（见 `jiezi-view.js`）。
   ⚠️ 这里只给**手感信号**（指针形状）：`cursor` 会被三面/名字牌继承 ⇒
      "能抓"这件事在鼠标下看得见（触屏没有 hover，靠的是拖起来动）。
   ⚠️ `.dragging` 与 `.jiezi-item.dragging` 同一个类名（一个状态一个类名，
      不许为第二档另造一个新名字 —— 那样"拖拽态"就有两个出处了）。 */
.iso-obj{cursor:grab}
.iso-obj.dragging{cursor:grabbing}
.iso-shadow{position:absolute;background:rgba(60,40,20,.13)}
.iso-flat{position:absolute;background:rgba(150,120,90,.30)}
.iso-face{position:absolute}
.iso-face-top{background:#F1E0C5}
.iso-face-left{background:#D9C4A1}
.iso-face-right{background:#C5AD87}
/* 名字牌：**要有**（否则认不出那是床还是桌子），但**小、不抢画面** */
.iso-name{position:absolute;transform:translate(-50%,-50%);
  font-size:var(--text-xs);line-height:1.2;color:#3A2E22;white-space:nowrap;
  background:rgba(255,252,246,.72);padding:1px 4px;border-radius:5px;pointer-events:none}
/* 等距场景的**缩放层**（`jiezi-view.js` 写进 DOM 之后加的）
   ⚠️ 缩放在这一层、不是在平面自己身上 —— 平面里的像素坐标与 `clip-path` 就不用重算。
   ⚠️ 不加这句的话，缩放会以元素中心为原点，整个房间会飘到画面外面。 */
.iso-scaler{position:absolute;left:0;top:0;transform-origin:0 0}

/* ===== 芥子的「3D 档」容器（`jiezi-view.js` 的挂载点）=====
   ★ 2026-10：给 `scene3d.js` / three.js 那一档用。
   🔴 为什么这里必须给**明确尺寸**（踩过两次，两次都表现为"画了但看不见"）：
      · `.jz3d-host` 用 `inset:0` 铺满平面 ⇒ 平面**必须自己**有高度，否则算出来是 0。
      · `.jiezi-plane` 在 3D 档下**没有**显式高度（`aspect-ratio` 已删；
        显式尺寸只有等距那条路走）⇒ 第一版我给宿主写 `height:100%`
        ⇒ 父元素高度 auto ⇒ **恒等于 0** ⇒ 画布拿 0 尺寸 ⇒ 一个对象都不建。
   ⚠️ 所以 CSS 给一个**比例兜底**，保证"量不到真实高度"时也不是 0；
      真实高度由 `jiezi-3d.js` 按舞台尺寸覆盖（只有它知道舞台多大）。 */
.jiezi-plane.iso3d { position: relative; aspect-ratio: 16 / 9; }
.jz3d-host { position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: hidden; }
.jz3d-host canvas { display: block; width: 100% !important; height: 100% !important; }

/* ===== 芥子 3D：**点中那件东西的名字**（`jiezi-3d-pick.js` 造）=====
   ★ 2026-10：设计侧 Q4 第 1 步「点选 + 高亮 + 显示物件信息」的第三项。
   🔴 为什么这个类必须有样式（不是"顺手补一个"）：
      本仓库有 `tests/css-classes.test.js` —— **用了类名却没写样式会被判红**，
      而它存在的理由正是"加类名"与"写样式"分成两步做时会漏（连犯两次）。
   ⚠️ 三条都是"踩过或差点踩"的：
      ① `pointer-events:none` —— 信息条压在画布上，挡住它下面那块就没法点了
         （症状很怪：只有某一块区域点不中）。它纯展示，不该接管事件。
      ② `position:absolute` + 不参与布局 —— 交互层是**可能被 `ResizeObserver` 那条链
         间接调到**的；信息条一旦影响父元素尺寸，就会"改布局 → 又触发观察器"，
         本仓库报过 `ResizeObserver loop completed with undelivered notifications`。
         绝对定位把它从布局里摘出去（与"推迟一帧"是两道独立的保险）。
      ③ 空的时候**不占位、看不见**（`:empty`）—— 没选中时不该在画面里留一块色斑；
         也不写 `visibility:hidden`（那看不见但**仍能被读屏念出来**）。
      文字用深色 + 半透明浅底（three 那档的背景是浅灰蓝 `0xdfe7f5`，深字才看得清）。 */
.jz-info { position: absolute; left: 50%; bottom: 10px; transform: translateX(-50%);
  max-width: 82%; padding: 3px 10px; border-radius:var(--radius-full);
  font-size:var(--text-sm); line-height: 1.5; color: #2a2114; text-align: center;
  background: rgba(255, 252, 246, .86); border: 1px solid rgba(120, 96, 48, .28);
  box-shadow: 0 1px 4px rgba(0, 0, 0, .18);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  pointer-events: none; z-index: 2; }
.jz-info:empty { display: none; }
/* 选中态：与外圈的边框一起给一个"就是它"的信号。
   ⚠️ 真正让**画面里那件东西**亮起来的是材质 `emissive`（3D 里没法用 CSS）；
      这里这一圈只是顺手让"名字"和"选中"两件事在视觉上对上。 */
.jz-info[data-jz-id]:not([data-jz-id=""]) { border-color: rgba(214, 150, 40, .85); }
/* 触屏（没有 hover 的指针）下把信息条抬高一点：手指点在画面上会挡住下缘那一块 */
@media (hover: none) { .jz-info { bottom: 16px; font-size:var(--text-base); } }

/* ══════════════════════════════════════════════════════════════════════
   ★★ 桌宠「生命感」A2/A3（2026-10 · 派工单 `design-out/dispatch-deskpet.md`）
   ──────────────────────────────────────────────────────────────────────
   设计侧判定（`design-out/consult-132-answer.md` Q1 = 甲「借表现手法，自己接」）：
   借 BongoCat 的"眼睛跟着鼠标 / 每个输入都有对应动作" + VPet 的"状态驱动的动画组"，
   **自己实现**（不引它们的代码）。行为全在 `public/deskpet.js`，这里只放**动作**。

   🔴 为什么这一块在**文件末尾**而不是紧挨着上面那段桌宠规则：
      它是**追加的覆盖层**（见下面第一条"接管"），放末尾才能靠"同权重、后写者赢"
      稳稳压住既有规则，而不必去改别人的那几行。合并时也不容易冲突。
   ══════════════════════════════════════════════════════════════════════ */

/* ---------- ① 接管待机呼吸：CSS 节拍器 → JS 微动 ----------
   🔴 上面 `.deskpet.state-idle .pet-sprite{animation:breathe 3.1s ease-in-out infinite}`
      是**一台匀速节拍器**：3.1 秒一个来回、永远一样。设计侧点名的核心缺口正是
      「待机微动 ≠ 木偶」⇒ 它现在由 `deskpet.js` 逐帧写 `transform` 接管
      （摇摆/呼吸的周期**每轮重抽** 3.4–6.4s / 2.9–4.3s，见那里的 `_micro`）。
   🔴 而且那条 `animation` 还顺带压掉了 `[data-facing="left"]{transform:scaleX(-1)}`
      —— CSS 动画的 `transform` 会盖住同元素的普通声明 ⇒ **她在待机时永远朝右**，
      "偶尔看向玩家"根本看不见。这一条关掉之后，转身（信息）与 `.pet-sit`（坐着）也一起活了。 */
.deskpet.state-idle .pet-sprite{animation:none}
/* 注视鼠标的 CSS 缓动同理作废：微动是逐帧写的，过渡只会把它拖糊（见上面 695 行那段）。 */
.home-pet.pet-looking{transition:none}

/* ---------- ② 眨眼（判据①的一部分：`DeskPet.blink()` 加 `.pet-blinking`） ----------
   周期**由引擎随机**（1.7–7.1s × 心情系数，另约 18% 概率紧接一次"连眨"）——
   不在这里写成固定 animation，否则又变回节拍器。
   🔴 **如实标注**：素材是一张 PNG（`public/pets/*.png`），**没有分层眼睑**
      ⇒ 这里做不到"眼皮真的合上"，做的是**"眯一下"**：立绘以"眼线"为原点
      在 120ms 内缩到 95.5% 并下沉 0.6px。要真正的眨眼得有**分层素材**（眼睛单独一层），
      那是素材侧的活，CSS 变不出来。
   ⚠️ 用 `scale` / `translate` 这两个**独立变换属性**（不是 `transform`）：
      否则会把 `[data-facing="left"]` 的翻转盖掉 —— 与上面 `.state-idle` 同一个坑。
      不认这两个属性的老浏览器**只是没有这一下**，不会错位、不会崩。
   ⚠️ `--pet-eye`：两种立绘的眼睛位置不同（幼年体是侧脸、单眼偏右上；成熟期是正面双眼），
      所以按 `data-stage` 分别标定 —— 这是**照着素材量出来的**，不是猜的。 */
.deskpet[data-stage="junior"]{--pet-eye:32%}
.deskpet[data-stage="mature"]{--pet-eye:36%}
.deskpet.pet-blinking .pet-sprite{animation:pet-blink .12s ease-in-out 1;transform-origin:50% var(--pet-eye,34%)}
@keyframes pet-blink{0%,100%{scale:1 1;translate:0 0}45%{scale:1 .955;translate:0 .6px}}

/* ---------- ③ 说话时的头动（判据④：气泡一显示就有对应动画类） ----------
   `.pet-talking` 由 `deskpet.js` 盯 `#home-bubble`（本 app 里**所有说话的公共出口**）
   加上，话说完（按时长）自动摘掉 ⇒ **状态驱动**：只在"正在说话"时有，不是闲着也在动。
   ⚠️ 周期 620ms：待机类动效的下限是 600ms（`tools/motion-inventory.js` 判据⑤），
      再快就从"说话"变成"抖"。 */
.deskpet.pet-talking .pet-sprite{animation:pet-talk .62s ease-in-out infinite}
@keyframes pet-talk{0%,100%{rotate:-1.1deg;translate:0 0}50%{rotate:1.1deg;translate:0 -1.4px}}

/* ---------- ④ 被摸/被戳：300ms 内看得见的形变（判据②） ----------
   `.pet-poked` 由 `DeskPet.pop()` 同步加上（引擎自己的反馈，不等宿主），260ms 一次。
   🔴 这里**故意用整段 `transform`**（而不是 ②③ 那种独立属性），并把
      `translateX(-50%)` 写进每一帧：被摸要有反应是**功能反馈**，
      不能因为浏览器不认新属性就没了 —— 功能不能挂在"新语法支持"上。 */
.deskpet.pet-poked{animation:pet-pop .26s ease-out 1}
@keyframes pet-pop{
  0%{transform:translateX(-50%) translateY(0) scale(1,1)}
  30%{transform:translateX(-50%) translateY(-4px) scale(1.06,.94)}
  60%{transform:translateX(-50%) translateY(0) scale(.98,1.03)}
  100%{transform:translateX(-50%) translateY(0) scale(1,1)}}

/* ---------- ⑤ 睡觉：`.sleeping` 是判据⑤点名的类名 ----------
   `deskpet.js` 进/出睡眠时**两个类一起加/一起摘**：`pet-sleeping`（既有钩子，
   `selftest.js` 与上面那几条规则在用）+ `sleeping`（设计侧判据⑤的原话）。
   两个都留着是为了"判据与实现不说两套话"，不是历史遗留。
   睡觉那一组动作（💤 上浮 + 呼吸起伏）就在上面 705–706 两行，这里只补别名。 */
.home-pet.sleeping .pet-sprite{filter:drop-shadow(0 6px 10px rgba(40,70,140,.2)) brightness(.86) saturate(.9)}
.home-pet.sleeping .pet-shadow{opacity:.6}
.home-pet.sleeping .pet-zzz{opacity:.95;animation:zzz 2.6s ease-in-out infinite}

/* ---------- ⑥ 让她**点得到**（顺带修的一处，不修则判据②在真机上根本走不到） ----------
   🔴 实测（`elementFromPoint` 在她矩形里打 8×10 网格）：**0 个点命中的是她**，
      最上层是 `#room-items`（`position:absolute;inset:0;z-index:2`，家具层）。
      而 `.home-pet`（她）**没有 z-index** ⇒ 家具层整块盖在她上面 ⇒
      **"双击摸摸头"在真机上根本点不到她** ⇒ 判据②（被摸 300ms 内有反应）永远走不到。
   ⚠️ 这**不是**本轮引入的：`git show HEAD:public/style.css` 里两条规则一直就是这样
      （`.home-pet{…}` 无 z-index · `#room-items{…;z-index:2}`）——是本轮**量出来**的存量问题。
   ⇒ 只补 `z-index:3`（不碰布局、不碰家具层自己的 z-index）：她站在这间屋子的**最上层**，
      点她 = 点角色（而不是点她脚下的地板）。
   ℹ️ 同一层的 `#home-guide`（向导立绘）也有同样的遮挡，但那是芥子 UI 的活（另一个工作区），
      这里**只动桌宠这一条**，不替别人做决定。 */
.home-pet{z-index:3}

/* ============================================================================
   集市审核面板（public/review-panel.js 的浮动窗）
   ----------------------------------------------------------------------------
   🔴 2026-10-10 从 review-panel.js **搬到这里**（原来由 JS 往 <head> 注入）。
   为什么搬：tests/css-classes.test.js 的信号是「类名在 JS/HTML 里出现、在 style.css 里没有定义」——
   它**故意**只认 style.css，因为那是样式唯一的家。审核面板那 18 个 rv-* 类当时只在 JS 里注入
   ⇒ 判据红（合法地红：真去翻 style.css 的人是找不到它们的）。
   ⇒ 两条路：进白名单（写明理由）或搬进来。**选后者** —— 那个工具的注释自己写着
     「四个都修在工具里了，**没有靠白名单掩盖**」，这条经验值得沿用。
   ⚠️ 作用域仍是 #qpet-review-win（面板是个浮动窗，不该污染全局）⇒ 规则一字未改，只换了住处。
   ⚠️ 搬完**必须**把 JS 里那段注入删掉，否则两处重复（将来改一处忘一处）。
   ============================================================================ */
#qpet-review-fab{position:fixed;right:14px;bottom:14px;z-index:9998;padding:var(--space-2) var(--space-3);border-radius:var(--radius-lg);
border:1px solid #d8cdbb;background:#fffdf8;color:#4a3f30;font:13px/1.2 system-ui,"Microsoft YaHei",sans-serif;cursor:pointer;box-shadow:0 2px 10px rgba(0,0,0,.12)}
/* ============================================================================
   ★★ 2026-10-11：入口**挂在顶栏那一行**时的样式（手机档 ① 号缺陷的修法）
   ----------------------------------------------------------------------------
   上面那条 `position:fixed;right:14px;bottom:14px` 在手机上**正好压住底栏第 6 颗 tab
   「记忆」的中心**（tab x 325..390 / y 792..845，中心 (358,818)；这个入口 x 311..376 / y 796..830）
   ⇒ 真触摸点「记忆」落到它身上（`touchFailed=true`），六页的 `mobile_bottom_nav` 全红。
   `review-panel.js` 的 `homeFab()` 在窄屏 / 触摸设备上把这个节点**挪进 `.appbar .acts`**，
   这里只负责"进去之后长什么样" —— 与旁边两颗 `.ghost`（设置 / 重造）同一套观感：
   参与布局、不再是浮层、也不可能压住任何人。
   ⚠️ 宽屏（桌面）那条浮动规则**一个字没动**：选择器要求它确实是 `.acts` 的孩子才命中。
   ⚠️ 字号/内边距比 `.ghost` 略小：360 档那一行的余量只有几十像素（实测），
      标签也已经缩短成「审核」（完整说明在 `title` 里）。
   ============================================================================ */
#shell .appbar .acts > #qpet-review-fab{position:static;padding:7px 10px;border-radius:var(--radius-md);
  border:0;background:#eef2fb;color:var(--ink);font-size:var(--text-sm);box-shadow:none}
#qpet-review-win{position:fixed;right:14px;bottom:58px;z-index:9999;width:min(560px,96vw);max-height:76vh;overflow:auto;
background:#fffdf8;color:#4a3f30;border:1px solid #d8cdbb;border-radius:var(--radius-md);box-shadow:0 8px 30px rgba(0,0,0,.2);
font:13px/1.55 system-ui,"Microsoft YaHei",sans-serif;padding:var(--space-3)}
#qpet-review-win .rv-note{margin:0 0 8px;padding:8px 10px;background:#f7f4ef;border-radius:var(--radius-sm)}
#qpet-review-win .rv-dim{color:#8b8073}
#qpet-review-win .rv-warn{color:#a34a1f}
#qpet-review-win .rv-ok{color:#2f7d43}
#qpet-review-win .rv-bad{color:#b23b3b}
#qpet-review-win .rv-tabs{display:flex;flex-wrap:wrap;gap:6px;margin:8px 0}
#qpet-review-win .rv-card{border:1px solid #dfe7f5;border-radius:10px;padding:9px 10px;margin:8px 0;background:#fff}
#qpet-review-win .rv-hd{display:flex;flex-wrap:wrap;gap:6px;align-items:baseline;margin-bottom:4px}
#qpet-review-win .rv-tag{font-size:var(--text-xs);padding:1px 6px;border-radius:9px;background:#f7f4ef;color:#6b5f4e}
#qpet-review-win .rv-tag-ok{background:#e6f8ef;color:#2f7d43}
#qpet-review-win .rv-tag-wait{background:#fff4e6;color:#8a6a12}
#qpet-review-win .rv-tag-bad{background:#fdecec;color:#b23b3b}
#qpet-review-win .rv-tiny{font-size:var(--text-sm);margin:2px 0}
#qpet-review-win .rv-list{margin:2px 0 2px 16px;padding:0;font-size:var(--text-sm)}
#qpet-review-win .rv-btns{display:flex;flex-wrap:wrap;gap:6px;margin-top:7px}
#qpet-review-win .rv-b{padding:5px 10px;border-radius:var(--radius-sm);border:1px solid #d8cdbb;background:#fff;color:#4a3f30;cursor:pointer;font-size:var(--text-sm)}
#qpet-review-win .rv-primary{background:#3d6b4f;border-color:#3d6b4f;color:#fff}
#qpet-review-win .rv-danger{background:#fdecec;border-color:#e0b6b6;color:#a33}

/* ══════════════════════════════════════════════════════════════════════════
   ★ 2026-10 市场（素材交易）· **只加新样式，不动上面任何一行**
   --------------------------------------------------------------------------
   为什么单独一段放在文件末尾（而不是插到相关规则旁边）：
     这一段是**另一个件**（市场子界面）新加的，而 `style.css` 同期还有别件在改
     （手机档那一件）。放在末尾 ⇒ 合并时两边不会有行级冲突，
     也保证"既有选择器一条都没被改过"这件事**肉眼可核**（diff 全是纯新增）。

   为什么类名都带 `mk-` 前缀：`tools/check-css-classes.js --strict` 要求
     "用了类名就必须有样式"；而短名（如 `.on` / `.sel`）会与既有类撞车。
     `mk-` = market，全仓只有市场这一块用。

   两条纪律：
     · `.mk-pane` 的显隐由 `market-trade.js` 切（默认只有集市那一格 active）
     · `.mk-switch` **必须**用 `data-mktab` 而不是 `data-sub` ——
       `app.js:showSub()` 会把 `#page-workshop` 下所有 `.segbtn` 按 `data-sub` 重画，
       没带 `data-sub` 的按钮会被**清掉 active**（这条是实测踩出来的）
   ══════════════════════════════════════════════════════════════════════════ */
/* ⚠️ 手机档实测逼出来的收口（**只作用于市场这一格，不动既有行**）：
   这一格挂在 `.sub` 里，而 `.sub` 在 390px 视口下会被上面那排**既有的** `.segbtn` 撑到 444px
   ⇒ 它的 100% 还是 444px、右缘被推到 460px（手机上就是"右边被裁掉一截"）。
   ⇒ 用**视口宽度**收口。⚠️ 这里**不能**用 `100vw`：手机模拟档里 100vw 与布局视口不等
      （实测 405 vs 390，差的就是那条经典滚动条）⇒ 由 `market-trade.js` 量准了写进
      `--mk-maxw`（`fitToViewport()`，带 ResizeObserver 跟随）。取不到那个变量时退回 100%。
   ⚠️ 这一屏更上一层那条横向溢出（`.seg` 撑宽 444px）属于【手机档】那条线，本件不动它。 */
.mk-switch{margin:0 0 10px;max-width:var(--mk-maxw,100%);min-width:0}
.mk-filter{font-size:var(--text-sm);padding:6px 10px}
.mk-pane{display:none;min-width:0;max-width:var(--mk-maxw,100%);overflow-x:clip}
.mk-pane.active{display:block}
.mk-card{display:flex;flex-direction:column;gap:5px;min-width:0}
.mk-card .nm{display:flex;align-items:center;justify-content:space-between;gap:6px;min-width:0}
.mk-card .nm b{min-width:0;overflow-wrap:anywhere}
.mk-card .tiny{overflow-wrap:anywhere}
.mk-card .nm i{font-style:normal;font-size:var(--text-xs);color:var(--sub);background:#eef2fb;border-radius:7px;padding:2px 7px;white-space:nowrap}
.mk-price{font-size:var(--text-base);color:var(--sub)}
.mk-price b{font-size:var(--text-lg);color:var(--ink)}
.mk-money{display:flex;flex-wrap:wrap;align-items:baseline;gap:var(--space-2);background:var(--card);border-radius:14px;padding:10px 12px;margin:6px 0 4px;box-shadow:var(--shadow)}
.mk-msg{margin:10px 0 0;padding:9px 12px;border-radius:var(--radius-md);font-size:var(--text-sm);background:#eef2fb;color:var(--ink);overflow-wrap:anywhere}
.mk-msg.ok{background:#e6f8ef;color:#2f6045}
.mk-msg.bad{background:#fdecec;color:#a33}
.mk-ledger{display:flex;flex-direction:column;gap:var(--space-1);margin-top:8px}
.mk-row{display:flex;align-items:baseline;gap:var(--space-2);font-size:var(--text-sm);padding:6px 10px;border-radius:10px;background:var(--card);box-shadow:var(--shadow)}
.mk-row .mk-when{color:var(--sub);font-size:var(--text-xs);white-space:nowrap;flex:0 0 auto}
.mk-row .mk-what{flex:1 1 auto;min-width:0;overflow-wrap:anywhere}
.mk-row .mk-amt{flex:0 0 auto;font-weight:700;font-variant-numeric:tabular-nums}
@media (max-width:430px){
  .mk-card .nm{flex-wrap:wrap}
  .mk-row{flex-wrap:wrap}
  .mk-row .mk-what{flex:1 1 100%}
}

/* === 第三阶段 区 B 追加（Wave 2）=== */
/* 本块只服务两件新东西：① 首开引导屏（P1）②「我的作品」（P4）。
   🔴 纪律：**一个既有行都没改**（本块是纯追加）；下面所有选择器都带自己的前缀
      （`.intro-*` / `.work-*` / `#my-works-host`），不会碰到别人的样式。
   🟡 取值口径：**优先用 token 变量**（台账 D7 那 15 个 / D3 圆角 / D4 间距 / D5 阴影）。
      区 A 这一轮正在改 `:root`，此刻 `--action` / `--ink-sub` / `--radius-*` / `--space-*`
      还**没落地** ⇒ 每个 `var()` 都写了**台账 D1/D3/D4/D7 的字面值**兜底，
      于是"区 A 落地前"与"落地后"是同一份 CSS，不需要再改一次。
      ⚠️ 待 token 化清单写在区 B 交件报告里（这些兜底值就是清单本身）。 */
#intro-overlay.intro-overlay{
  position:fixed;inset:0;z-index:60;                 /* 比 `.modal`(30) 与 `.overlay`(40) 都高 —— 它盖在芥子页上方 */
  display:flex;align-items:center;justify-content:center;
  padding:var(--space-4,16px);
  padding-bottom:calc(var(--space-4,16px) + var(--safe-b,0px));
  background:rgba(20,30,60,.55);backdrop-filter:blur(3px);
}
#intro-overlay .intro-card{
  width:100%;max-width:420px;max-height:88vh;overflow:auto;
  background:var(--bg-card,#FFFFFF);color:var(--ink,#1F2A44);
  border-radius:var(--radius-lg,18px);padding:var(--space-6,24px);
  box-shadow:var(--shadow-lg,0 18px 50px rgba(40,70,140,.14));
}
#intro-overlay .intro-title{
  margin:0 0 var(--space-2,8px);font-size:18px;line-height:1.6;font-weight:700;
}
#intro-overlay .intro-sub{margin:0 0 var(--space-4,16px);font-size:13px}
#intro-overlay .intro-examples{display:flex;flex-direction:column;gap:var(--space-3,12px)}
#intro-overlay .intro-example{
  display:grid;grid-template-columns:auto 1fr;gap:2px var(--space-3,12px);
  align-items:center;text-align:left;width:100%;min-height:44px;    /* D14/D28：触控目标 ≥44px */
  padding:var(--space-3,12px) var(--space-4,16px);
  background:#fbfdff;color:var(--ink,#1F2A44);
  border:1.5px solid var(--border,#dfe7f5);border-radius:var(--radius-md,12px);
  font:inherit;cursor:pointer;
}
#intro-overlay .intro-example:hover{border-color:var(--action,#4A6B4F)}
#intro-overlay .intro-example:focus-visible{outline:2px solid var(--action,#4A6B4F);outline-offset:2px}
#intro-overlay .intro-ic{grid-row:1/3;font-size:22px;line-height:1}
#intro-overlay .intro-example b{font-size:14px;font-weight:700}
#intro-overlay .intro-example i{font-style:normal;font-size:12px;color:var(--sub);line-height:1.5}
#intro-overlay .intro-foot{margin-top:var(--space-4,16px)}
#intro-overlay .intro-foot button{flex:1;min-height:44px}

#my-works-host{margin-top:var(--space-4,16px)}
/* ★★ 2026-10-15 · 件 T3（`consult-256-answer.md` §一.3 / §三）：
   这一块从芥子页搬到**元碎片页**，并改成**可折叠（默认收起，标题栏显示数量）**。
   折叠用的是**原生 `<details>` / `<summary>`** —— Tab 能到、Enter/Space 能开合、
   读屏会念"折叠/展开"，不需要 JS，也不需要自造一套折叠控件。
   ⚠️ 原来那两条 `.works .page-head` 的排版规则**保留**：`<details>` 里不再有
      `.page-head`（标题移进了 `<summary>`），但页面别处/老存档里可能还有 —— 删它
      属于"顺手清理"，本件不做（如实登记：这两条现在没有已知的命中者）。 */
#my-works-host .works-sum{display:flex;align-items:baseline;justify-content:space-between;
  gap:var(--space-2,8px);cursor:pointer;list-style:none}
#my-works-host .works-sum::-webkit-details-marker{display:none}
#my-works-host .works-sum::after{content:'▸';color:var(--ink-muted,#9AA8C4);font-size:12px}
#my-works-host .works[open] .works-sum::after{content:'▾'}
#my-works-host .works-sum:focus-visible{outline:3px solid var(--action);outline-offset:2px}
#my-works-host .works-sum b{font-size:14px}
#my-works-host .works .page-head{display:flex;align-items:baseline;justify-content:space-between;gap:var(--space-2,8px)}
#my-works-host .works .page-head h3{margin:0}
#my-works-host .work-empty{margin:var(--space-2,8px) 0 0}
#my-works-host .work-list{display:flex;flex-direction:column;gap:var(--space-2,8px);margin-top:var(--space-3,12px)}
#my-works-host .work-card{
  display:flex;align-items:center;gap:var(--space-3,12px);flex-wrap:wrap;
  padding:var(--space-3,12px);border-radius:var(--radius-md,12px);
  background:#fbfdff;border:1.5px solid var(--border,#dfe7f5);
}
#my-works-host .work-ic{font-size:20px;line-height:1;flex:0 0 auto}
#my-works-host .work-main{flex:1 1 140px;min-width:0;display:flex;flex-direction:column;gap:2px}
#my-works-host .work-name{font-size:14px;overflow-wrap:anywhere}
#my-works-host .work-meta{font-size:11px;color:var(--ink-muted,#9AA8C4)}
#my-works-host .work-acts{display:flex;gap:var(--space-2,8px);flex:0 0 auto;flex-wrap:wrap}
#my-works-host .work-acts .sbtn{min-height:36px}     /* 窄屏时下面那档把它抬到 44 */
/* `[data-goto-agent]`（D19 的显式创作入口）：和「查看全部 →」并排时不许被挤成竖排 */
#my-works-host .agent-goto{flex:1 1 auto;min-height:36px}
/* ★ T-B6「⚙ 画面」三档开关（`[data-dim-switch]`）——
   位置：芥子页**右上角那一行**（`.jz-ctl`，与既有的档位按钮同一处，不新增控件容器）。
   长相照 `.jz-dims`（同一套"浮在屋子上、不抢画面"的观感），只在末尾追加、
   不动第 1649-1657 那几条既有规则。 */
.jz-screen{display:flex;align-items:center;gap:4px;padding:3px 6px;border-radius:999px;
  background:rgba(255,255,255,.82);backdrop-filter:blur(6px);
  box-shadow:0 1px 6px rgba(20,30,50,.16)}
.jz-screen b{opacity:.7}
.jz-smode{padding:2px 9px!important;border-radius:999px}
.jz-smode.on{background:rgba(46,107,94,.22);border-color:rgba(46,107,94,.55);font-weight:700}
.jz-screen-note{opacity:.72;white-space:nowrap}
/* 低端设备上选「强制 3D」时的确认框：**复用既有 `.modal` / `.modal-card`**，
   `.modal-confirm` 只是那个框自己的名字（设计侧给的判据钩子）。
   ⇒ 这里只写"它是这套合一套弹窗里的一员"，不另起一套观感。
   理由：`tools/check-css-classes.js` 要求"加了类就得有样式"（不许只加类不写样式），
   而"观感全部由 `.modal` 提供"这件事**必须写在明处**，不能靠读代码猜。 */
.modal.modal-confirm .modal-card{max-width:360px}
.modal.modal-confirm h3{margin:.1em 0 .4em;font-size:14px;line-height:1.5;font-weight:700}
/* ★ 2026-10-12（区 E · T-E1）：**危险操作**的确认框 —— D13 的第四类（危险→`.modal-danger`）。
   它**不另起一套观感**（仍是 `.modal` + `.modal-card`），只把标题强调成危险色；
   红色"确定"按钮由 `.btn-danger` 提供（区 A2 的 5 类之一）⇒ 这里只做"标题是危险色"这一件事。 */
.modal.modal-danger .modal-card{border:1px solid var(--error)}
.modal.modal-danger h3{color:var(--error)}
@media (max-width:430px){
  #my-works-host .work-acts{flex:1 1 100%}
  #my-works-host .work-acts .sbtn{flex:1 1 auto;min-height:44px}
  #my-works-host .agent-goto{min-height:44px}
  /* 窄屏：那一行按钮（自动 / 强制 2D / 强制 3D）横着放不下 ⇒ 让「建议」那句换到下一行，
     但**三颗按钮本身不缩**（它们是触控目标，按 D28 那档抬到 44） */
  .jz-screen{flex-wrap:wrap;row-gap:4px}
  .jz-smode{min-height:44px}
  .jz-screen-note{flex:1 1 100%}
}
/* === 第三阶段 区 B 追加结束 === */

/* ══════════════════════════════════════════════════════════════════════════
   ★ 2026-10-12 · **新增 360px 断点**（D6 / D28 / T-A9 —— 现状 0 条，这是第一条）
   --------------------------------------------------------------------------
   为什么追加在**文件最末尾**：同上面市场那一段的理由 —— `style.css` 同期有多个区
   在改，追加在末尾 ⇒ **diff 全是纯新增**，"既有的 720 / 430 一条都没被碰过"
   这件事**肉眼可核**。

   🔴 **本档只做"紧凑化"**（D6/D28 原文）：减 padding / 缩文字。
      **不改任何布局逻辑** —— 不加 `flex-direction` / `display` / `order` 翻转，
      不改 `width` / 栅格，不隐藏任何东西。
   🔴 **720 / 430 两个断点的规则一条都没删**（实测基线：720 = 8 个规则块、
      430 = 1 + 3 = 4 个）—— 由 `tests/design-tokens.test.js` 的 J10 钉住。
   ⚠️ 触控目标不缩：`.row-btns>button` 只减**左右** padding，上下保持原值 ⇒
      高度不变（D28"触控目标 ≥44px"那条不被这一档破坏）。
   ══════════════════════════════════════════════════════════════════════════ */
@media (max-width:360px){
  /* —— 容器：padding 收一档（16 → 12）—— */
  .card{padding:var(--space-3)}
  .mk-money{padding:var(--space-2) var(--space-3);margin:var(--space-1) 0 var(--space-1)}
  .mk-msg{padding:var(--space-2) var(--space-3)}
  .mk-row{padding:var(--space-2) var(--space-3)}
  /* —— 按钮行：只减左右，不动上下（保触控目标高度）—— */
  .row-btns{gap:var(--space-1)}
  .row-btns>button{padding-left:var(--space-3);padding-right:var(--space-3)}
  /* —— 文字：13 → 12（走 token，本档不写裸值）—— */
  .hint{font-size:var(--text-sm)}
  .tiny{font-size:var(--text-sm)}
  #page-workshop .mk-msg,#page-workshop .mk-row{font-size:var(--text-sm)}
}

/* ══════════════════════════════════════════════════════════════════════════
   ★ 2026-10-12 J1a（第三阶段 · 区 A2 · tokens 批次 3）：硬编码色 **108 → 77 种**
   --------------------------------------------------------------------------
   口径出处：设计侧 `consult-193-answer.md`（中性 5 档：`#ffffff` / `#fbfdff` /
   `#f4f7fe` / `#eef2fb` / `#fff3ec`）+ 台账 **D61**（4 簇语义审计）。
   判据：`tests/design-tokens.test.js` 的 J1a（钉 `<= 80`，含变异自证）。

   归并规则（**可机械复核**，按"这个色当什么用"分，不按"离白多近"分）：
     · **surface（底/面）** ⇒ 按语义簇并档 —— 告警→`--warning-bg`（救活原本 0 引用的死 token）·
       错误→`#fdecec` · 成功→`#e6f8ef` · 暖纸面→`#f7f4ef` · 环境光→`#dbe8ff` ·
       蓝白→档 3 `#f4f7fe` / 档 2 `#fbfdff` · 中性·暖白→档 1 `#ffffff`
     · **border / divider（边框/分隔线）** ⇒ **一律并到语义 token `--border`**（`#dfe7f5`）
       —— 保留"边框看得出来"这件事，同时让 `--border` 成为唯一真源
     · **3D 墙面（明暗 = 光照）** ⇒ **只按坡度向代表收敛**（浅端→`#fffdf8` · 深端→`#e4dccc`），
       **不跨坡并**（墙的明暗就是立体感本身）
     · **❌ 不许并**（D61 明令）：`#cfd8ea`（`.switch` OFF 轨道，旋钮是 #fff）·
       `#f6eef6`/`#eadff0`（`.mood.low` 的底与边 —— 暖→紫的色相位移**就是**"心情低落"）

   ⚠️ `--bg` 与 `--bg-base` 都落到**档 3**：**这破了上面"兼容别名与旧定义逐字节相同"那句话**
      —— 因为被并掉的那个旧值已经不存在，别名与新定义**必须同改**，否则两处值分叉。
      （别名机制本身不变；变的只是被并掉的那个值。）
   ⚠️ **本注释里不写被并掉的具体色值**：`design-tokens` 的计数口径把**注释里的 hex 也算一种**
      ⇒ 在说明里回抄旧值会把刚并掉的种类**又数回来**（实测踩过：回抄了一次 ⇒ 种类数 +1）。
   ══════════════════════════════════════════════════════════════════════════ */


/* ═══ A2 收尾件：按钮 5 类 + 3 尺寸（D11）与 toast（D12）════════════════════════
   ★ 2026-10-12 · 第三阶段区 A2 的最后两项（派工单 J4/J5）。
   ----------------------------------------------------------------------------
   为什么现在才做：这两项与 **J1a（颜色归并）** 同在 `style.css` 上 ⇒ 先做会被 J1a 的
   大范围替换覆盖掉（顺序约束「每并一区换一次 CACHE」同理）。**J1a 落地后**才追加这一段。

   🔴 三条纪律：
     ① **只追加在文件末尾** —— 不插进既有段（区 B/区 A 都已在这文件末尾追加过，插进去会撞车）
     ② **与既有类并存** —— E 迁移调用侧之前，`.ghost`/`.primary`/`.sbtn` 必须照旧可用
        （D48：先换类名而样式没到 = 182 处用户面反馈集体没有视觉表现）
     ③ **零新增硬编码色** —— 全部走 :root 的 token（D14 的口径）⇒ J1a 的 79 种不变

   验收：`public/_widgetprobe.html?widgets=1`（**真浏览器**看得到 + 计算值可断言）
   ══════════════════════════════════════════════════════════════════════════ */

/* ── 按钮基座（5 类共用的形状/字号/可点性）──
   ⚠️ 触控目标 ≥44px 是 **360 档的硬要求**（D28/D6）⇒ 用 min-height 兜住，
      而不是只在窄屏加（否则桌面点起来也难受）。 */
.btn-primary,.btn-secondary,.btn-danger,.btn-link,.btn-icon{
  display:inline-flex;align-items:center;justify-content:center;gap:6px;
  min-height:44px;padding:0 var(--space-3);border-radius:var(--radius-md);
  border:1px solid transparent;background:none;cursor:pointer;
  font:inherit;font-size:var(--text-base);line-height:1.2;text-decoration:none;
  transition:filter .15s ease,transform .08s ease}
.btn-primary:active,.btn-secondary:active,.btn-danger:active,.btn-icon:active{transform:translateY(1px)}
.btn-primary:disabled,.btn-secondary:disabled,.btn-danger:disabled,.btn-link:disabled,.btn-icon:disabled{
  opacity:.55;cursor:not-allowed;transform:none}

/* 类 1：主操作（吃 --action；D11 把蓝紫渐变收成单色）*/
.btn-primary{background:linear-gradient(135deg,var(--action),var(--action-hover));color:var(--on-action);font-weight:700;border-color:transparent}
.btn-primary:hover{filter:brightness(1.06)}

/* 类 2：次要操作（--bg-card + --border；对应现在的 .ghost）
   ⚠️ 弹层/悬浮面里用 `--toast-bg`（同值 token，浮起面的统一底）*/
.btn-secondary{background:var(--bg-card);color:var(--ink);border-color:var(--border);box-shadow:var(--toast-shadow)}
.btn-secondary:hover{background:var(--bg-base);border-color:var(--info)}

/* 类 3：危险操作（--error）*/
.btn-danger{background:var(--error);color:var(--on-action);font-weight:600;border-color:transparent}
.btn-danger:hover{filter:brightness(1.06)}

/* 类 4：文字链接（--action、无背景）*/
.btn-link{background:none;color:var(--action);border-color:transparent;padding:0 var(--space-1);min-height:0;font-size:var(--text-sm);text-decoration:underline;text-underline-offset:2px}
.btn-link:hover{filter:brightness(1.1)}

/* 类 5：图标按钮（继承父级色；触控目标仍 ≥44px）*/
.btn-icon{background:none;color:inherit;border-color:transparent;padding:0;width:44px;min-width:44px}
.btn-icon:hover{background:var(--bg-base);color:var(--info)}

/* 次要态：**"已卸载/停用"那种弱化按钮**（对应现在的 `.chk label.off` 语义）——
   `--info` 在这里当"中性提示色"用（灰蓝），与 toast-info 同一个 token ⇒ 引用数够 J2(a) */
.btn-secondary.is-off{background:var(--toast-bg);color:var(--info);text-decoration:line-through}

/* ── 3 个尺寸档（D3 的圆角、D2 的字号、D4 的间距 —— 全部走 token）── */
.btn-sm{min-height:36px;padding:0 var(--space-2);font-size:var(--text-sm);border-radius:var(--radius-sm)}
.btn-md{min-height:44px;padding:0 var(--space-3);font-size:var(--text-base)}
.btn-lg{min-height:52px;padding:0 var(--space-6);font-size:var(--text-md)}
/* ⚠️ 这里**不另写 360 档**：D6 的 360 断点已经存在（style.css 里那一段），
   而且 `design-tokens` 的 J9 判据"**360 档内不写裸色值/裸字号**"是**从 360 档的起点一路查到文件尾**的
   ⇒ 在文件末尾再追加任何含 hex 的段落都会把那条判据判红（实测踩过：加了一段 360 档 ⇒ J9 与它的变异自证双双报红）。
   按钮的窄屏紧凑化由 `.btn-sm/.btn-md/.btn-lg` 三个尺寸档本身负责（触控目标仍 ≥44px，D28 不减）。 */

/* ══ toast（D12 定稿：顶部居中 · 最多 3 条 · 错误态带重试 · 不阻断）════════════
   🔴 **CSS 只负责"长什么样"，不负责"停多久"** —— 这条要写清楚，因为它被设计侧写错过：
      设计侧给过一条判据".toast-success.*animation-duration:s*3s"，
      而 **3s 是"停留时长"（由 JS 的 setTimeout 决定），不是动画时长**。
      照它写 CSS 会得到一个 **3 秒的淡入动画**（toast 会在 3 秒里慢慢变清晰 —— 观感是坏的），
      而且那条判据**永远匹配不到真行为**（CSS 里写个 3s 就过，JS 里删掉定时器它照样绿）。
      ⇒ 执行侧按层次实现：**JS 管时长（E 的 T-E1）· CSS 管外观与本段**。
      判据相应改为：① 四个状态类都有样式 ② 它们**互相可区分**（有各自的语义色载体）
      ③ **不许出现"靠 animation-duration 表达停留时长"的写法**（变异：写上一个 3s ⇒ 必红）。 */
.toast{
  position:fixed;top:var(--space-4);left:50%;transform:translateX(-50%);
  z-index:10000;max-width:min(520px,92vw);
  display:flex;align-items:center;gap:var(--space-2);
  padding:var(--space-2) var(--space-3);
  background:var(--toast-bg);color:var(--ink);
  border:1px solid var(--border);border-left-width:4px;border-radius:var(--radius-md);
  box-shadow:var(--toast-shadow);
  font-size:var(--text-sm);line-height:1.6;
  animation:toast-in .18s ease-out}
@keyframes toast-in{from{opacity:0;transform:translate(-50%,-6px)}to{opacity:1;transform:translate(-50%,0)}}
/* 逐条往下排（最多 3 条由 JS 保证；这里只管堆叠不重叠）*/
.toast:nth-of-type(2){top:calc(var(--space-4) + 56px)}
.toast:nth-of-type(3){top:calc(var(--space-4) + 112px)}
/* 四个状态：**左侧色条**是各自语义色的载体（背景统一用 --toast-bg ⇒ 不加新色值，
   也仍然"互相可区分" —— 小圆点 + 色条两处信号）*/
.toast::before{content:'';width:8px;height:8px;border-radius:var(--radius-full);flex:0 0 auto}
.toast-success{border-left-color:var(--success)}
.toast-success::before{background:var(--success)}
.toast-info{border-left-color:var(--info)}
.toast-info::before{background:var(--info)}
.toast-warning{border-left-color:var(--warning)}
.toast-warning::before{background:var(--warning)}
.toast-error{border-left-color:var(--error)}
.toast-error::before{background:var(--error)}
/* 错误态可带"重试"（结构见 consult-169 表 2）；这里只保证按钮不与文字挤在一起 */
.toast .btn-sm,.toast .btn-link{margin-left:auto;flex:0 0 auto}
/* 错误态不自动关 ⇒ 它需要一个"能关"的出口（× 或重试）；给出显式样式，避免 JS 拼裸字 */
.toast [data-toast-close]{background:none;border:none;color:var(--ink-muted);cursor:pointer;font-size:var(--text-md);line-height:1;padding:0 var(--space-1);min-height:0}
/* toast 里的次级按钮沿用同一套底/投影（token 复用的第三个落点）*/
.toast .btn-secondary{background:var(--toast-bg);box-shadow:var(--toast-shadow)}
/* reduce-motion：入场动画是装饰级 ⇒ 关掉（D38 的同族口径）*/
@media (prefers-reduced-motion:reduce){.toast{animation:none}}

/* ══════════════════════════════════════════════════════════════════════════
   ★★ 2026-10-15 · 件 T2（反馈②「悬浮球在中间」）：**屏幕边界那颗悬浮球**
   --------------------------------------------------------------------------
   依据：`design-out/consult-255-answer.md` §2.2（判 **甲** —— 屏幕边界 + 可拖 + 记住位置）。
   所有者原话：「**转给悬浮球 —— 我不是说要放到屏幕边界、做成一个类似 UI 交互的入口吗，
     怎么还在中间**」。

   🔴 它**不是**屋子里那颗 `#jz-main-toggle`（那颗是**玩法**入口，§2.2 明写保留）：
      两颗球是两件事，代码里也是两个节点 —— 详见 `jiezi-view.js` 的 `FAB_POS_KEY` 那段。

   位置口径（判据⑤）：**右下角 · 距边界 16px** —— 16 这个数由 JS 写进 `left/top`
   （`fabPos()`），**CSS 里不写死**：位置是"存的 + 视口"算出来的，
   写死一份就等于有第二份真相（本仓最忌的形状）。
   ⚠️ 纵向那 16px 会被 JS 抬到底栏（`.tabbar`，约 53px）**之上**：
      被底栏压住的球 = 点不到的入口（"能力还在、够不到"是本仓最忌的形状之一）。

   ⚠️ 浮空动效（`jz-fab-float`）是**装饰级**（不承载任何信息）⇒
      `prefers-reduced-motion` 下关掉；关的是动画，位置、尺寸、可点性一点不变。
   ⚠️ `touch-action:none` 是**触屏能拖**的前提：不写它，浏览器会把纵向拖动当成页面滚动
      并把 pointer 流**取消**掉（症状：桌面能拖、手机上拖不动 —— 本仓最忌的形状）。
   ══════════════════════════════════════════════════════════════════════════ */
.jz-fab{
  position:fixed;z-index:20;
  width:56px;height:56px;min-width:56px;min-height:56px;padding:0!important;
  display:flex;align-items:center;justify-content:center;
  border-radius:50%;cursor:grab;touch-action:none;
  font-size:22px;line-height:1;
  background:radial-gradient(circle at 34% 28%,#ffffff,rgba(255,252,246,.92) 58%,rgba(240,228,200,.95));
  box-shadow:0 4px 14px rgba(40,30,10,.3),0 0 0 2px rgba(200,169,106,.55) inset;
  animation:jz-fab-float 4.6s ease-in-out infinite}
@keyframes jz-fab-float{0%,100%{transform:translateY(0)}50%{transform:translateY(-3px)}}
.jz-fab-drag{cursor:grabbing;animation:none}
/* 不在芥子页时收起来（它挂在 `body` 上 ⇒ 拿不到 `.page{display:none}` 那免费的一层）。
   ⚠️ 用**独立的类**而不是 `.hidden`：菜单自己用 `.hidden` 表达"收起了"，
      两者语义不同（"这一页不该有它" vs "它现在收着"），叠在一个类上会互相盖。 */
.jz-fab-off{display:none!important}
@media (prefers-reduced-motion:reduce){.jz-fab{animation:none}}
/* 菜单：**就地展开**（不切页）。`position:fixed` 与球同一套坐标系 ——
   球是 fixed，菜单若用 absolute 就会以最近的定位祖先为参照（球一拖，菜单跑到别处）。 */
.jz-fab-menu{position:fixed;z-index:20;min-width:180px;max-width:min(320px,86vw);
  padding:10px 12px;border-radius:14px;
  background:rgba(255,255,255,.96);backdrop-filter:blur(6px);
  border:1px solid rgba(200,169,106,.45);
  box-shadow:0 6px 22px rgba(20,30,50,.22);
  font-size:var(--text-xs);line-height:1.5}
.jz-fab-row{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
.jz-fab-h{opacity:.7;flex:0 0 auto}
/* 菜单里那三档按钮：**复用** `.jz-smode` 那套长相（同一族控件长得一样），
   这里只把"按不下的宽度"放开（菜单比顶栏窄）。 */
.jz-fab-menu .jz-screen{padding:0;background:none;box-shadow:none;flex-wrap:wrap}
.jz-fab-menu .jz-screen-note{flex:1 1 100%}
.jz-fab-sep{height:1px;margin:8px 0;background:rgba(200,169,106,.45)}
/* 菜单里的**目的地**入口（★ T3 之后有三颗：📜 主线 / ✎ 装扮 / 🤖 让朏朏帮我改）——
   它们各自走仓里已有的那条路（`navTo(PAGE_MAIN)` / `toggleDrawer()` / `showPage('chat')`）。 */
.jz-fab-item{width:100%;justify-content:flex-start;min-height:36px;padding:4px 10px!important;
  border-radius:10px;text-align:left}
/* ★ T3：「让朏朏帮我改」那颗旁边那句**怎么用**（说一句就行 + 一个例子）。
   它是**说明**不是控件 —— 排在按钮下面一行（不让它挤按钮的宽度）。 */
.jz-fab-note{flex:1 1 100%;opacity:.72;padding-left:2px}
@media (max-width:430px){
  /* 手机档：菜单里的按钮按 D28 抬到 44（与 `.jz-smode` 那条同一个口径）。 */
  .jz-fab-item{min-height:44px}
}

/* ══════════════════════════════════════════════════════════════════════════
   ★ DIY 面板的「两区」与「生成物」那一区（consult-266-answer §1 判（甲）· 第 4–7 步）
   --------------------------------------------------------------------------
   为什么追加在**文件末尾**：本件只许往 style.css **末尾追加**（不许动既有行）。
   为什么需要这些类：
     · 裁定 §1 的补充约束要「生成物与手搓模板**分开列**（小节标题或视觉分隔）」⇒ `.diy-zone`
       是小标题、`.diy-zone-gen` 是那条**分隔线**（视觉分区的落点）。
     · 裁定 §4 风险 2 要「失败详情**折叠**展示，默认只显示第一条」⇒ `.diy-fold`。
     · 失败清单要一眼看出"哪一条 + 改法"⇒ `.diy-err` / `.diy-err-code` / `.diy-err-how`。
   ⚠️ 一个类名都不要漏：`tools/check-css-classes.js`（`tests/css-classes.test.js` 的守卫）
      会把"加了类名但没写样式"报红 —— 上面这批是**真的要有样式**的，不进白名单。
   ══════════════════════════════════════════════════════════════════════════ */
/* 小节标题（「手搓」/「生成物」）。样式对齐卡片标题那一档，不抢 <h2> 的层级。 */
.diy-zone{font-size:14px;line-height:1.5;margin:14px 0 6px;opacity:.92;font-weight:600}
/* 第二条分隔线 + 与上面那一区拉开距离（视觉分区的**实质**：不是只加个标题） */
.diy-zone-gen{margin-top:22px;padding-top:14px;border-top:1px solid rgba(200,169,106,.35)}
.diy-zone-gen .hint{margin-left:8px;font-weight:400}
/* 生成物列表：竖排（一张卡一行，别用 .grid 挤成两列 —— 卡片上要放四个按钮） */
.diy-gen-list{display:flex;flex-direction:column;gap:10px;margin-top:8px}
/* 成品卡片本体：外壳沿用既有的 `.skill`（与 DIY 里手搓的插件卡同一套观感 ——
   两区看起来是"同一个地方的两类东西"，不是两个界面）。这里只补一处区别。 */
.diy-card{border-left:3px solid rgba(200,169,106,.5)}
/* 失败清单的整体容器（一档里可能有多轮，每轮一个 `.diy-errlist`） */
.diy-errlist{margin-top:2px}
/* 【看详细错误】摊开的那块。默认 `hidden`（用户点了才填、才揭开）——
   它是"把折叠的全部摊开"的落点，不是又一份重复清单。 */
/* ⚠️ 2026-10-13（批 A 收口）：这里原来写 `border-radius:8px` —— 那是个**新取值**（全表只此 1 处），
   把 `border-radius` 的取值种类从 22 顶到 23 ⇒ 撞 `tests/design-tokens.test.js` 的 J7 上限。
   本仓那条判据的口径是"**新增样式不许扩取值种类**"（终局目标 ≤8 种），所以**不是**抬高基线，
   而是**用已有的值**：`10px` 全表 17 处、是本面板同族卡片在用的档 ⇒ 改成它，**种类不增**。 */
.diy-detail{margin-top:8px;padding:8px 10px;border-radius:10px;background:rgba(200,169,106,.08)}
/* 失败卡片与成品卡片同一套外壳，只换左边框的颜色（一眼分得出"要处理"） */
.diy-fail{border-left:3px solid rgba(214,120,90,.8)}
.diy-round{margin-top:6px}
.diy-roundhead{font-weight:600;opacity:.85}
/* 一条错误：`[code] 正文` + 下面那行「改法」（拿到 hint 才显示真内容） */
.diy-err{margin:4px 0 6px;padding-left:8px;border-left:2px solid rgba(200,169,106,.3)}
.diy-err-code{font-family:ui-monospace,Menlo,Consolas,monospace;font-size:11px;
  padding:0 4px;border-radius:4px;background:rgba(200,169,106,.16)}
.diy-err-how{opacity:.8;margin-top:2px}
/* 改法拿不到时**如实说**（不编一句）—— 给它一点视觉提示，免得被当正文读过去 */
.diy-nohow{opacity:.7;font-style:italic}
/* ★ 折叠：默认**不展开**（裁定 §4 风险 2：两轮各 8 条会把人淹掉）。
   用原生 `<details>`（`.diy-fold` 上没有 `open` 属性 = 默认收起）。 */
.diy-fold{margin-top:6px}
.diy-fold>summary{cursor:pointer;opacity:.8}
.diy-fold>summary:hover{opacity:1}
/* 卡片底部的按钮行：允许换行（手机上四颗按钮一行放不下） */
.diy-btns{flex-wrap:wrap;gap:6px}
/* ★ 第 7 步：上架预留位 —— **置灰** + 「本机可用」。
   置灰是 `disabled` 属性给的（不是只靠样式）⇒ 键盘与读屏也拿得到"不能用"这个事实。 */
.diy-pubrow{align-items:center;gap:8px;margin-top:2px}
.diy-pubrow button[disabled]{opacity:.5;cursor:not-allowed}
.diy-pubnote{opacity:.72}
.diy-pubflag{opacity:.6}
/* 「你当时说」那一句（失败后要"改需求重来"，得让用户看见自己当初说了什么） */
.diy-want{opacity:.8;margin-top:2px}
.diy-selftest{margin-top:2px}
/* Cookie 说明与选择固定在窗口内，窄屏可换行。 */
.cookie-consent{position:fixed;left:var(--space-4);right:var(--space-4);bottom:var(--space-4);z-index:1600;display:flex;align-items:center;flex-wrap:wrap;gap:var(--space-3);padding:var(--space-4);background:var(--bg-card);color:var(--ink);border:1px solid var(--border);border-radius:var(--radius-md);box-shadow:var(--shadow-md)}
.cookie-consent[hidden]{display:none}
.cookie-consent span{flex:1 1 220px}
.machine-b-panel{margin-block:var(--space-4);padding:var(--space-4);border:1px solid var(--border);border-radius:var(--radius-md)}
.machine-b-panel button{margin:var(--space-1)}
.machine-b-panel label{display:flex;align-items:center;gap:var(--space-2)}
.machine-b-panel label[hidden]{display:none}
.machine-b-panel input[type=checkbox]{width:auto;flex:none;margin:0}
.machine-b-panel label>span{flex:1;min-width:0}
.machine-b-panel select{min-width:0;flex:1}
.machine-b-panel progress{display:block;width:100%;accent-color:var(--action)}
.machine-b-panel figure{display:inline-block;margin:var(--space-1)}
.machine-b-panel canvas{width:120px;height:120px;max-width:100%}
.machine-b-panel [data-state=error]{color:var(--error)}
.app-project-workbench{display:grid;gap:12px;min-width:0;margin-block:12px}
.app-project-workbench textarea,.app-project-workbench input{box-sizing:border-box;max-width:100%;width:100%}
.app-project-workbench pre{white-space:pre-wrap;overflow-wrap:anywhere;max-height:420px;overflow:auto;margin:0}
.app-project-workbench [data-action=list]{display:flex;flex-wrap:wrap;gap:8px}
.app-project-workbench [data-action=surface]{overflow:auto;min-width:0}
.app-project-workbench button{white-space:normal;overflow-wrap:anywhere}
.project-cloud-workbench{display:grid;gap:10px;min-width:0;max-width:100%;margin-block:16px}
.project-cloud-workbench label{font-weight:600;overflow-wrap:anywhere}
.project-cloud-workbench input,.project-cloud-workbench textarea,.project-cloud-workbench select{box-sizing:border-box;width:100%;max-width:100%;min-width:0}
.project-cloud-workbench textarea{min-height:220px;max-height:420px;resize:vertical}
.project-cloud-workbench button{justify-self:start;max-width:100%;white-space:normal;overflow-wrap:anywhere}
.project-cloud-workbench p{margin:0;overflow-wrap:anywhere}

/* 旧资料工作台：摘要可换行，文件控件/按钮不撑破窄屏。 */
.legacy-project-workbench{display:grid;gap:var(--space-3);min-width:0;max-width:100%;margin-block:var(--space-4);overflow-wrap:anywhere}
.legacy-project-workbench>*{min-width:0;max-width:100%}
.legacy-project-workbench h4,.legacy-project-workbench p{margin:0}
.legacy-project-workbench label{display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap;min-width:0}
.legacy-project-workbench input[type=file]{min-width:0;max-width:100%}
.legacy-project-workbench pre{white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word;max-height:420px;overflow:auto;margin:0}
.legacy-project-workbench button{white-space:normal;overflow-wrap:anywhere;max-width:100%}
.legacy-project-workbench>div{display:grid;gap:var(--space-2);min-width:0}
.legacy-project-workbench>div>div{min-width:0;overflow-wrap:anywhere}

/* 统一工程预览是独立舞台：绝对定位房间需要显式高度与包含块。 */
#scene-project-workspace [data-scene-action="surface"] { position: relative; height: 420px; min-width: 0; overflow: hidden; touch-action: none; }
#scene-project-workspace [data-scene-action="surface"] .jiezi-plane:not(.iso):not(.iso3d) { height: 100%; }
.machine-b-section{padding-block:var(--space-4);border-top:1px solid var(--border)}
.machine-b-section:first-of-type{margin-top:var(--space-3)}
.machine-b-section h4{margin:0 0 var(--space-2);font-size:var(--text-md)}
.machine-b-section p{margin-block:var(--space-2);color:var(--ink-sub)}
.machine-b-actions{display:flex;align-items:center;flex-wrap:wrap;gap:var(--space-2);margin-block:var(--space-3)}
.machine-b-actions button{min-height:40px;margin:0}
.machine-b-panel [role=status]{overflow-wrap:anywhere}
.machine-b-file-record{padding-block:var(--space-3);overflow-wrap:anywhere}
.machine-b-file-record+.machine-b-file-record{border-top:1px solid var(--border)}
.machine-b-files-hold{display:flow-root;min-width:0;width:100%}
@media(max-width:480px){
  .machine-b-actions button{flex:1 1 140px}
  .machine-b-panel label{flex-wrap:wrap}
  .machine-b-panel select{flex-basis:100%}
}
/* 未确认的私人资料始终可见，避免设置弹层关闭后丢失保存提示。 */
#vault-save-alert { position: fixed; left: 1rem; bottom: 1rem; z-index: 9998; max-width: min(34rem, calc(100vw - 2rem)); padding: var(--space-3) var(--space-4); }
#vault-save-alert[hidden], #vault-save-alert button[hidden], #vault-status button[hidden] { display: none; }
#play-save-status { position: fixed; top: 1rem; left: 1rem; z-index: 9998; max-width: min(34rem, calc(100vw - 2rem)); max-height: 30vh; overflow: auto; padding: var(--space-2) var(--space-4); }
#play-save-status[hidden] { display: none; }
#vault-status { margin-block: 1rem; }
/* 正式创作的文档输入卡（formal-creation-page.js 的 mount）：选文件 → 预览全文 → 确认/取消/重试。 */
.formal-creation-input { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); margin-top: var(--space-3); padding: var(--space-3); border: 1.5px dashed var(--border); border-radius: var(--radius-md); }
.formal-creation-input > label, .formal-creation-input > textarea, .formal-creation-input > p { flex-basis: 100%; margin: 0; }
.formal-creation-input > textarea { min-height: 8rem; resize: vertical; }
.formal-creation-input [hidden] { display: none; }

/* 保险箱恢复码解锁（CD-04b · public/vault-unlock.js）：locked 时的唯一输入入口。
   为什么不复用 .recovery-code：那是"显示码"的样式，这里是"输入码"的对话框。
   2026-11-04：全部改用既有 token（--bg-card/--ink-sub/--error/--border/--shadow-lg/--radius-*/--text-*/--space-*），
   不新增硬编码 hex 与字号 —— design-tokens 的 J1/J1a/J5/J8 基线本来就没有余量。 */
.vault-unlock-mask { position: fixed; inset: 0; z-index: 10000; display: flex; align-items: center; justify-content: center; padding: var(--space-4); background: rgba(18, 24, 38, 0.55); }
.vault-unlock-card { width: min(28rem, calc(100vw - 2rem)); padding: var(--space-4); border-radius: var(--radius-sm); background: var(--bg-card); box-shadow: var(--shadow-lg); }
.vault-unlock-title { margin: 0 0 var(--space-2); font-size: var(--text-lg); color: var(--ink); }
.vault-unlock-hint { margin: 0 0 var(--space-3); font-size: var(--text-sm); line-height: 1.55; color: var(--ink-sub); }
.vault-unlock-input { width: 100%; padding: var(--space-2) var(--space-3); font-family: ui-monospace, Consolas, monospace; font-size: var(--text-md); letter-spacing: 1px; text-align: center; border: 1px solid var(--border); border-radius: var(--radius-sm); }
.vault-unlock-input:disabled { opacity: 0.6; }
.vault-unlock-error { min-height: 1.2rem; margin: var(--space-2) 0 0; font-size: var(--text-sm); color: var(--error); }
.vault-unlock-actions { display: flex; justify-content: flex-end; gap: var(--space-2); margin-top: var(--space-3); }
.vault-unlock-btn { padding: var(--space-2) var(--space-4); border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--bg-base); color: var(--ink); cursor: pointer; }
.vault-unlock-btn:disabled { opacity: 0.6; cursor: default; }
.vault-unlock-cancel { background: transparent; border-color: transparent; color: var(--ink-sub); }

/* 网页版（PWA · WEB-01）更新提示：新版 Service Worker 停在 waiting 时浮在底部。
   不自动消失、不自动换版；用户点「更新」才 skipWaiting + 刷新（public/pwa-update.js）。
   样式只用既有 token，不新增硬编码色值/字号。 */
.pwa-update { position: fixed; left: 50%; bottom: calc(var(--space-4) + env(safe-area-inset-bottom, 0px)); z-index: 10000; display: flex; align-items: center; gap: var(--space-2); max-width: calc(100vw - 2 * var(--space-4)); padding: var(--space-2) var(--space-3); border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--bg-card); box-shadow: var(--shadow-lg); transform: translateX(-50%); }
.pwa-update-text { flex: 1 1 auto; font-size: var(--text-md); color: var(--ink); }
.pwa-update .btn-sm { flex: 0 0 auto; }
@media (max-width: 480px) {
  .pwa-update { flex-wrap: wrap; justify-content: flex-end; }
  .pwa-update-text { flex-basis: 100%; }
}
