/* ============ 页面 token：明暗三态 + 主题色轴 ============ */
:root {
  --shell-bg: #efede6;
  --frame: #1e1c17;
  --bg: #f7f5ef;
  --bg-2: #ede9de;
  --bg-3: #e8e3d4;
  --bg-4: #f3f0e7;
  --bg-5: #fcfbf7;
  --surface: #ffffff;
  --surface-2: #fbf9f3;
  --surface-3: #f4f1e8;
  --warm: #e6e0d2;
  --warm-strong: #dad3c2;
  --warm-soft: #efeadd;
  --ink: #26241f;
  --ink-2: #8b8577;
  --ink-3: #3a352c;
  --ph: #b0ab9e;
  --line: #e9e5da;
  --line-2: #ddd8cb;
  --line-3: #e0dbce;
  /* 地图上那些圆盘的暖橙（阶段节点 `.journey-node-circle` 与子节点 `.journey-kid-disc`；
     子节点那条注释本来就写着"和阶段节点同一套填色"）。2026-10-01 起**页内圆形图标钮的高亮**
     也用它——用户要求"这 3 个按钮高亮背景色对齐地图页节点"。 */
  --node: #cf8749;
  --line-4: #f0ece2;
  /* 门/窗在画布上的颜色（用户 2026-10-05 第八十三条：「门和窗使用区别于墙体的不同颜色，
     这样容易看清楚」）。⚠ 别再退回 `#3F7C5C`：那是**房间底色与内墙**那一族的绿 ✗，
     门窗混在里面根本分不出来（用户报的就是这个 ✓）。紫与绿/蓝/橙都分得开，
     而且"选中"那颗橙（`--node`）压在它上面照样一眼看出来 ✓。 */
  /* 门窗（门扇/门窗符号/窗那对双线/D1·W1 那行小字）的色号。⚠ **一个变量说了算** ✓。
     用户 2026-10-06 第一百零三次：「加粗未选中的门窗线条，颜色换一下（紫色不太明显）」
     ⇒ 从 `#6b5b95`（紫 ✗ 压在米色底图上确实不显）换成蓝：先在靛蓝 `#2f6db5` 上停了一版，
     用户要**浅一档** ⇒ `#5b9bd5`（天蓝 ✓ 在米色底图上够亮、与"选中"那支暖橙 `--node` ✓
     一眼分得开，也不跟房名那支深绿 `#1F4D35` ✓ 撞 ✓）。 */
  --cf-opening: #5b9bd5;
  /* 面板/表里几种控件共用的一个高度（对齐「原图」开关）。⚠ 2026-10-05 从 `.cf-edit` 提到 `:root`：
     门窗编辑表**不在** `#cf-edit` 里面（它是另一张 Sheet），可里面也用着它
     （`.cf-dim-input` 那颗「洞口净宽」与新的多档开关）——变量挂在 `.cf-edit` 上时那几处**取不到值**，
     `min-height: var(--cf-ctl-h)` 整条作废（盒子掉回文字那么高，一直没人报，因为只是矮一截）。 */
  --cf-ctl-h: 32px;
  /* **叶知秋那一族（头像 ＋ 气泡）的几何**：一个数只说一遍 —— `.guide`（头像的落点与大小 ✓）、
     `.guide-bubble`（气泡离头像多远 ✓）与 `.cf-folder`（内容区下沿要**让开**它 ✓）三处共用。
     用户 2026-10-16：「减小内容区高度，下沿应避让设计师气泡（内容按 2 行算）」✓。 */
  --guide-face: 64px; /* 小态头像的边长（`.guide` 里那颗 `--face` 同值 ✓） */
  --guide-bottom: 24px; /* 头像离屏幕底 */
  --guide-gap: 14px; /* 气泡与头像之间那道缝 */
  /* 气泡**按 2 行算**的高度（用户点名 ✓）：字 13px × 行距 1.55 × 2 ＝ 40.3 ＋ 上下内边距 10×2
     ＋ 上下描边 1.5×2 ≈ 63（实测 2 行时 62 ✓）。⚠ 不是"把气泡截成 2 行"✗ —— 说的是**预留**多少 ✓。 */
  --guide-bubble-2l: 63px;
  --accent: #3f7c5c;
  --accent-soft: #e3ede6;
  --accent-strong: #2e6b4d;
  --amber: #c07b3a;
  --blue: #2e6b8f;
  --danger: #e0483a;
  --bubble: rgba(255, 255, 255, 0.82);
  --bubble-edge: rgba(255, 255, 255, 0.9);
  --bubble-soft: rgba(255, 255, 255, 0.58);
  --bubble-me: rgba(58, 53, 44, 0.58);
  --bubble-me-ink: #f4efe6;
  --stream-1: rgba(255, 255, 255, 0.5);
  --stream-2: rgba(255, 255, 255, 0.22);
  --stream-3: rgba(255, 255, 255, 0);
  --mask: rgba(28, 26, 20, 0.45);
  --intro-fallback: #c9e7f4;
}
/* ⚠ 主题色轴（`data-accent="ink"` / `"amber"` 两组覆盖）2026-10-10 **撤了**：
   用户「去掉主题色」——品牌色就是松绿一种。`--accent` 与它那三档 `--warm*` 都回到
   `:root` 的默认值 ✓（挑过墨色/暖橙的人，下一次打开就是这个默认色）。 */
/* 深色（跟随系统） */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --shell-bg: #171614;
    --frame: #0e0d0b;
    --bg: #1a1917;
    --bg-2: #211f1c;
    --bg-3: #262320;
    --bg-4: #232019;
    --bg-5: #2c2925;
    --surface: #242220;
    --surface-2: #2b2926;
    --surface-3: #302d29;
    --warm: #3e3a31;
    --warm-strong: #4a463b;
    --warm-soft: #37342b;
    --ink: #edeae2;
    --ink-2: #a29b8c;
    --ink-3: #d9d3c4;
    --ph: #6e6a5e;
    --line: #3a362e;
    --line-2: #454139;
    --line-3: #3f3b32;
    --line-4: #35322b;
    --accent: #7cb491;
    --accent-soft: #24362b;
    --accent-strong: #9cc5ac;
    --amber: #d9a05c;
    --blue: #7fa7c4;
    --danger: #e0483a;
    /* ⚠ 暖橙在**深色下要提亮**（用户 2026-10-06：「深色下也覆写一下」）。它不只画在画布上
       ——页内的图标钮高亮、分段开关的滑块、地图那几个圆盘都用它 ✓，压在深底上原色偏闷 ✗。
       借的就是深色那支 `--amber`（`#d9a05c` ✓ 同一族 ✓，不再新造一个色号 ✓）。
       ⚠ 代价明说：它在**画布上**（选中墙、拖放高亮、幽灵）压在米色的底图上，比原色淡一点 ✗ ——
       但那几笔都是"粗线/大图标"（7px 线、34px 幽灵 ✓），看得清 ✓。 */
    --node: #d9a05c;
    --bubble: rgba(36, 34, 32, 0.85);
    --bubble-edge: rgba(60, 56, 50, 0.9);
    --bubble-soft: rgba(36, 34, 32, 0.55);
    --bubble-me: rgba(226, 220, 206, 0.62);
    --bubble-me-ink: #26241f;
    --stream-1: rgba(26, 25, 23, 0.5);
    --stream-2: rgba(26, 25, 23, 0.22);
    --stream-3: rgba(26, 25, 23, 0);
    --mask: rgba(0, 0, 0, 0.6);
    --intro-fallback: #2a3a42;
  }
}
/* 深色（手动） */
:root[data-theme="dark"] {
  --shell-bg: #171614;
  --frame: #0e0d0b;
  --bg: #1a1917;
  --bg-2: #211f1c;
  --bg-3: #262320;
  --bg-4: #232019;
  --bg-5: #2c2925;
  --surface: #242220;
  --surface-2: #2b2926;
  --surface-3: #302d29;
  --warm: #3e3a31;
  --warm-strong: #4a463b;
  --warm-soft: #37342b;
  --ink: #edeae2;
  --ink-2: #a29b8c;
  --ink-3: #d9d3c4;
  --ph: #6e6a5e;
  --line: #3a362e;
  --line-2: #454139;
  --line-3: #3f3b32;
  --line-4: #35322b;
  --accent: #7cb491;
  --accent-soft: #24362b;
  --accent-strong: #9cc5ac;
  --amber: #d9a05c;
  --blue: #7fa7c4;
  --danger: #6e4a43;
  /* 与"跟随系统"那一块同一个值、同一个理由（用户要求手动深色也一样 ✓）。 */
  --node: #d9a05c;
  --bubble: rgba(36, 34, 32, 0.85);
  --bubble-edge: rgba(60, 56, 50, 0.9);
  --bubble-soft: rgba(36, 34, 32, 0.55);
  --bubble-me: rgba(226, 220, 206, 0.62);
  --bubble-me-ink: #26241f;
  --stream-1: rgba(26, 25, 23, 0.5);
  --stream-2: rgba(26, 25, 23, 0.22);
  --stream-3: rgba(26, 25, 23, 0);
  --mask: rgba(0, 0, 0, 0.6);
  --intro-fallback: #2a3a42;
}

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}
/* **全局不许选文本**（用户 2026-09-29）。这个应用里"拖动"**本来就是操作**——转视角、拖房间、
   翻图纸——手指或鼠标一划就带出一片蓝色选中，是纯噪音。
   ⚠ `user-select` 是**继承**属性，所以写在 `body` 上就够；但也正因为是继承，**两处必须显式放行**：
   ① **输入框与可编辑区** —— 不放行的话，聊天输入框里的字选不中（实测里双击选不到词）；
   ② **调试面板** —— 那是开发者要看、要拷的东西。它只在 `?debug=1` 下存在，用户看不到，
      所以不会把"误选"带回来。
   要交给用户的文字（户型文件链接）走**剪贴板 API**，本来就不靠手工划选。
   ⚠ 判据是"真的选不中"，不是这条声明在不在——见 `tests/browser/ui-consistency.spec.js`：
   那边真的双击一下再读 `window.getSelection()`。 */
body { user-select:none; -webkit-user-select:none; }
/* 能点的都是小手，按不动的都是"禁止"——**这一条只写一次**。
   从前它散在几十条按钮规则里各写一遍，谁新写一颗忘了带就少一处：用户 2026-10-02 报的
   「添加新家 Sheet 上鼠标指向确定按钮没有变成小手」就是 `.opening-actions button` 漏了。
   ⚠ 个别按钮有意用别的光标（拖拽把手 `grab`、占位行 `[data-disabled]` 的 `default`／
   `not-allowed`）：它们都是**属性/伪类**选择器，specificity 比这两条高，照旧赢。 */
button:not(:disabled) { cursor:pointer; }
button:disabled { cursor:not-allowed; }
input, textarea, [contenteditable], #debug-panel { user-select:text; -webkit-user-select:text; }
/* 全屏页面（.page）排除在外：它们要能对 display 做过渡，而**带 !important 的声明
   按规范不参与过渡**——包着 !important 就没法把隐藏推迟到滑出动画结束。见下面 .page 那段。 */
[hidden]:not(.page) {
  display: none !important;
}
html {
  -webkit-text-size-adjust: 100%;
}
body {
  background: var(--shell-bg);
  color: var(--ink);
  font-family:
    -apple-system, BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB",
    "Microsoft YaHei", sans-serif;
  font-size: 15px;
  line-height: 1.6;
  min-height: 100vh;
}
.num {
  font-variant-numeric: tabular-nums;
}

/* ============ 页面壳 ============ */
.shell {
  max-width: 1120px;
  margin: 0 auto;
  padding: 40px 24px 56px;
}
.stage-wrap {
  display: flex;
  justify-content: center;
}

/* ============ 应用容器（桌面预览列 / 移动端全屏） ============ */
.app {
  position: relative;
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 430px;
  height: min(860px, calc(100vh - 140px));
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 14px;
  overflow: clip;
  box-shadow: 0 18px 50px -20px rgba(0, 0, 0, 0.35);
}
.app * {
  -webkit-tap-highlight-color: transparent;
}

/* ============ 通用组件 ============ */
.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 14px;
  padding: 14px;
}
.section-label {
  font-size: 12px;
  font-weight: 600;
  color: var(--ink-2);
  letter-spacing: 0.08em;
  margin: 18px 0 8px;
}
.result-tip {
  font-size: 11.5px;
  color: var(--ink-2);
  background: var(--surface-3);
  border-radius: 10px;
  padding: 8px 12px;
  margin-top: 10px;
}
.std-row {
  display: flex;
  justify-content: space-between;
  font-size: 12.5px;
  padding: 7px 0;
  color: var(--ink);
  border-bottom: 1px dashed var(--line);
}
.std-row:last-child {
  border-bottom: none;
}
.std-row b {
  font-weight: 600;
}
/* ============ 按钮配色：一条规则，七类 ============
   **判断只有一句**：点下去，事情是不是**就定了**？
     · 定了（提交 / 确认 / 保存）→ 强调色实心（--accent + 白字）
     · 只是再走一步（开一张表、开一个流程）→ 米色（--warm）或描边行
   两类同色的话，用户在同一个弹层里只能靠读文字分辨；而"米色 = 再走一步、实心 = 落定"
   这个差别是免费的。（曾经有一个 .primary-btn 是这套里的"实心"那颗，但没有任何调用方，
   2026-09-23 删掉——真正在用的是 .opening-actions button。）

   提交 / 确认  .opening-actions button（默认实心强调色）、.design-preview button.primary
                —— 确定 / 保存 / 确认并保存需求 / 整理需求卡 / 保存尺寸 / 保存修改 / 应用修改
   入口         .house-add（米色）、.entry-command 与 .assistant-commands button（描边行）
                —— 添加新家 / 编辑 / 户型图页那两个去处 / 叶知秋面板那几行
   次要（并存） .house-add-quiet、.opening-actions .secondary
                —— 另开一套新房装它 / 先看户型 / 门窗表单里的删除
   破坏性       .house-add-danger（红）——**只在不可逆时** / 删除一套房（它没有撤销）
   还不能用     .add-upload、[data-disabled]（虚线）—— 上传户型图
   提示里的出路  .budget-blocked button（白底 + 强调色描边与文字）—— 标注户型尺寸
   浮动圆钮     .cf-fab 一族（右下角那组）——位置决定形态，不参与上面这套

   两条容易踩的：danger / quiet / primary 三个变体**必须排在 .house-add 之后**（同权重下
   靠后的赢，写在前面会被那层底色盖掉，实测）；**红只给不可逆的破坏**——户型图页删门窗是有
   撤销的，所以它穿"次要"那颗，不标红。 */

/* ============ 设计入口（行动导向） ============ */
.suggest-row + .suggest-row {
  border-top: 1px solid var(--line-4);
}
.suggest-empty {
  padding: 12px 14px;
  font-size: 12px;
  color: var(--ink-2);
}
/* **搜索结果那一列**（2026-10-18：「在当前页面而不是新 tab 展示搜索结果」✓、「注明来源」✓）：
   一条＝标题 ＋ 摘要（两行封顶）＋ 来源；整条可点，点开原页面 ✓。
   ⚠ 摘要是**别人的话** —— 只当线索 ✓，两行就够，别让一屏装不下第三条 ✓。 */
/* 加载态：转圈复用画布那一幕的 keyframes（`cf-recognize-spin` ✓，一个动画两处用 ✓）。 */
.suggest-loading {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 12px 14px;
  font-size: 12px;
  color: var(--ink-2);
}
.suggest-ring {
  width: 16px;
  height: 16px;
  box-sizing: border-box;
  border: 2px solid var(--line-2);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: cf-recognize-spin 0.9s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
  .suggest-ring { animation: none; }
}
.estate-results {
  margin: 0;
  padding: 0;
  list-style: none;
}
.estate-result {
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: 10px 14px;
  color: inherit;
  text-decoration: none;
  border-top: 1px solid var(--line-2);
}
.estate-result:first-child { border-top: 0; }
/* 「图标 ＋ 标题」一行（图标是搜索接口自带的站点图标 ✓ —— 不给裂图留位置 ✓）。 */
.estate-result-head {
  display: flex;
  align-items: center;
  gap: 6px;
}
.estate-result-icon {
  flex: 0 0 auto;
  width: 16px;
  height: 16px;
  border-radius: 4px;
}
.estate-result-icon.is-off { display: none; }
.estate-result-title {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
}
.estate-result-snip {
  font-size: 12px;
  line-height: 1.5;
  color: var(--ink-2);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.estate-result-src {
  font-size: 11px;
  color: var(--ph);
}
.estate-result-note {
  margin: 0;
  padding: 10px 14px;
  font-size: 11.5px;
  line-height: 1.6;
  color: var(--ink-2);
  border-top: 1px solid var(--line-2);
}
.estate-result-more {
  display: block;
  padding: 10px 14px;
  font-size: 12.5px;
  color: var(--accent);
}
/* ⚠⚠ **两行用同一个网格**（用户 2026-10-19：「几个下拉菜单和输入框以及搜索按钮的布局不太美观」✓）：
   原来两行各自是 flex ✗ ⇒ 第一行有搜索键、第二行没有 ⇒ **右边缘一长一短** ✗（实测 320 vs 366）。
   现在列宽固定成"下拉一格 ＋ 其余一格" ⇒ 两行的输入框**同起同止** ✓。
   ⚠ 搜索键**收进输入框里**（绝对定位在那一行的右端 ✓）：既让两行齐平 ✓，也是搜索框该有的样子 ✓
   —— 一颗方块键孤零零挂在输入框外面，就是原来那副"不美观" ✓。 */
.estate-search {
  display: grid;
  grid-template-columns: calc(4em + 48px) minmax(0, 1fr);
  align-items: center;
  gap: 8px;
  position: relative;
}
/* 弹层里的**第二行**（几室几厅 ＋ 面积 ✓，用户 2026-10-19）：同一个网格 ✓，只多一条上间距 ✓。 */
.estate-search-extra { margin-top: 8px; }
.estate-search input {
  width: 100%;
  min-width: 0;
  border: 1px solid var(--line-2);
  border-radius: 10px;
  padding: 8px 12px;
  font: inherit;
  font-size: 13px;
  background: var(--surface);
  color: var(--ink);
}
.estate-search input::placeholder {
  color: var(--ph);
}
.estate-search input:focus-visible {
  outline: 1px solid var(--warm-strong);
}
/* ============ 右下角那列浮动圆钮 ============
   用户 2026-09-30：地图页的设置、户型图页的 ✅、效果图页的 ✅ 从**标题栏右上角**搬到**右下角**，
   与 FAB、漫游、光照预览排成一列，统一成原来 FAB 那颗圆钮的样子。
   位置公式**只有这里一份**：各颗只声明自己站第几层（--fab-step 是「一颗 + 一道缝」）。
   ⚠ 它们不能留在 .page-bar 里——那层有 backdrop-filter，会给绝对定位换个参照物。 */
.fab-float {
  position: absolute;
  right: var(--fab-right);
  z-index: 8;
  box-sizing: border-box;
  width: 40px;
  height: 40px;
  padding: 0;
  display: grid;
  place-items: center;
  border: 2px solid var(--accent);
  border-radius: 50%;
  background: var(--surface);
  color: var(--accent);
  cursor: pointer;
  box-shadow: 0 3px 10px rgba(0, 0, 0, 0.18);
}
.fab-float svg {
  display: block;
  width: 20px;
  height: 20px;
}
.fab-float[aria-pressed="true"] {
  background: var(--accent);
  color: #fff;
  border-color: var(--accent);
}
.fab-float:disabled {
  border-color: var(--line-2);
  color: var(--ink-2);
  cursor: not-allowed;
}
.fab-float.layer-1 { bottom: var(--fab-bottom); }
/* ⚠ 这里从前是贴齿轮的一张 210px 小卡（`position:absolute; bottom:80px`）。用户 2026-09-30：
   齿轮搬进标题栏、改叫「设置」，点开的是**一张 Sheet**——壳归 `#sheet`（见 `.sheet` 那一族），
   所以定位、宽度、边框、投影、底色全撤掉，这里只剩分组与行距。 */
.settings-group { margin-bottom:18px; }
.settings-group:last-child { margin-bottom:0; }
/* 账号那一行：**常见的长相**——圆形头像 + 一行名字（用户 2026-09-30：做成 Profile 那种样子、
   放在第一行；**不要背景、不要 ›、也不要副标题**，所以它不是一张卡片，就是一行）。
   ⚠ 它仍是**占位**：按不动（`data-disabled`）。副标题一去，"还没上线"就只剩两处落点——
   整行淡一档（下面那条 `--ink-2`）与点一下回的那句实话（见 settings.js）。 */
.settings-account { width:100%; box-sizing:border-box; display:flex; align-items:center; gap:12px; padding:6px 2px; border:0; background:none; color:var(--ink); font:inherit; text-align:left; cursor:pointer; }
.settings-account[data-disabled] { cursor:not-allowed; color:var(--ink-2); }
.settings-account[data-disabled] .settings-avatar { color:var(--line-2); }
/* 设置里可以拨的那一行（光照预览）：一行文字 + 右边一枚开关。
   状态只在 `aria-pressed` 上——画法与读屏读的是同一个属性。 */
/* 设置里那几行（外观 / 光照预览 / 设计师辅助 / 隐私声明）**等高、等距** ——
   用户 2026-10-11：「除我的账号外，其他几个设置项应等高等距」。实测过：两行的那种
   （标题＋小字）自然高 64px，一行的那种（外观那行只有标题＋分段开关）只有 52px，
   摆在一起就是"一行高一行矮" ✗ ⇒ 给一个同样的下沿 64px（内容仍垂直居中）。
   等距靠**同组**（组内相邻两行的上下内边距加起来就是它们之间的空档，天然一致 ✓）——
   分成两组时，组间那 18px 与组内这 20px 对不齐，最后一行会显得更松 ✗。 */
.settings-row { width:100%; box-sizing:border-box; display:flex; align-items:center; gap:12px; min-height:64px; padding:10px 2px; border:0; background:none; color:var(--ink); font:inherit; text-align:left; cursor:pointer; }
.settings-row-text { display:flex; flex-direction:column; gap:2px; min-width:0; }
.settings-row-text strong { font-size:15px; font-weight:650; }
.settings-row-text small { font-size:11.5px; color:var(--ink-2); }
.settings-switch { flex:none; margin-left:auto; position:relative; width:42px; height:26px; border-radius:999px; background:var(--line-2); transition:background .2s; }
.settings-switch::after { content:""; position:absolute; top:3px; left:3px; width:20px; height:20px; border-radius:50%; background:#fff; box-shadow:0 1px 3px rgba(0,0,0,.2); transition:transform .2s; }
/* 那枚「›」：与开关同族的位置（右边那一格），只是它说的是"点开还有一页" */
.settings-arrow { flex:none; margin-left:auto; color:var(--ink-2); font-size:18px; line-height:1; }
.settings-row[aria-pressed="true"] .settings-switch { background:var(--accent); }
.settings-row[aria-pressed="true"] .settings-switch::after { transform:translateX(16px); }
.settings-avatar { flex:none; width:44px; height:44px; display:grid; place-items:center; border-radius:50%; background:var(--surface-2); color:var(--ink-2); }
.settings-avatar svg { display:block; width:24px; height:24px; }
.settings-account-text { display:flex; flex-direction:column; gap:2px; min-width:0; }
.settings-account-text strong { font-size:15px; font-weight:650; }
.settings-account[data-disabled] .settings-account-text strong { color:var(--ink-2); }
.settings-account-text small { font-size:11.5px; color:var(--ink-2); }

/* ⚠ `.theme-title` / `.theme-opts`（外观与主题色那两排药丸按钮）随主题色一起去掉了
   （2026-10-10）——外观改用**分段开关**（`.cf-seg`，与门型、墙体虚实同一套），
   主题色那排整行撤掉。这里不再留空壳规则：一个类名没有消费者就该消失 ✓。 */
/* 设置在**这一行里坐着别的东西**（分段开关）时的两处差异：整行按不动（它不是一个按钮），
   控件推到右边（与开关那颗 `.settings-switch` 同一个位次）。 */
.settings-row.is-static { cursor: default; }
.settings-row .cf-seg { margin-left: auto; }
/* ⚠ 外观那三档比门型/墙体那两处**字长**（「跟随系统」四个字，那两处都是两个字），
   而 `.cf-seg button` 的 `flex: 1 1 0` 让轨道按各段的 `min-width: 3.6em`（≈47px）算宽度
   ⇒ 四个字塞不进 47px，滑块里的字**竖着折成两行**（实测截图看到的）。
   这一处把最小宽度按最长那一档给足：4em 字宽（4 字 × 13px）＋左右各 12px 内边距。
   ⚠ 哪天有哪一档的名字超过四个字，这里要跟着改（`nowrap` 会让它溢出来而不是折行——
   溢出来一眼看得见，折行看着像"设计成这样"，所以宁可溢）。 */
#theme-mode button { min-width: calc(4em + 24px); white-space: nowrap; }

/* 隐私声明那张 Sheet 的正文（2026-10-10 用户：「字号偏大」）。
   ⚠ 它是**成篇要读的字**，不是控件标签：整段从 `body` 那 15px 掉到 13px（与验收项
   `.craft-check-text` 同一档），行距放宽到 1.7 —— 字号小一档、行距大一档，读起来才不挤。
   小节标题只比正文大半档（13.5px）＋ 一条上间距：它们是**分段标记**，不该抢戏。 */
.privacy { font-size: 13px; line-height: 1.7; color: var(--ink); }
.privacy-title { font-size: 13.5px; font-weight: 650; margin: 20px 0 6px; }
.privacy-title:first-child { margin-top: 0; }
.privacy-list { margin: 0; padding-left: 18px; }
.privacy-list li + li { margin-top: 6px; }
.privacy .material-note { margin-top: 20px; }
.guide {
  position: absolute;
  left: 8px;
  bottom: var(--guide-bottom);
  width: min(50vw, 220px);
  z-index: 9;
  transform: translateZ(0); /* 独立合成层：与确认页动画隔离，避免 GIF 闪烁 */
  touch-action: none;
  cursor: grab;
  user-select: none;
  -webkit-user-select: none;
  /* 小态那颗头像的边长 = 小态形象的宽度（**同一个数，两处引用一处定义**）。
     放大那一下格子宽度在动，头像**不跟**着长（见 `.guide-mascot .avatar-head`）。
     ⚠ 值在 `:root` 的 `--guide-face`（内容区下沿要按它让位 ✓）。 */
  --face: var(--guide-face);
  transition:
    width 0.35s cubic-bezier(0.2, 0.7, 0.3, 1),
    left 0.5s cubic-bezier(0.2, 0.7, 0.3, 1);
}
.guide.dragging {
  transition: none;
}
/* 缩小：**只剩一个头像**（用户 2026-10-09）。从前是"整身缩到 64px"——人只有二十来像素高，
   看不出是谁。用**聊天窗那颗的样式**：圆形 ＋ 静态图 `assets/avatar.png`
   （与消息里的 `.msg-avatar` 同一张、同一套取景，见 `.guide-mascot .avatar-head`）。 */
.guide.small {
  width: var(--face);
}
.guide.dragging {
  cursor: grabbing;
}
.guide-mascot {
  position: relative;
  width: 100%;
  aspect-ratio: 2 / 3;
  /* 换态那一段的时长（两张图交叉淡入淡出、以及底下那层占位什么时候露面）。
     一个数管三处，改就一起改。 */
  --cross: 0.4s;
}
/* 小态：方盒（圆头像要在方形盒里才圆得正） */
.guide.small .guide-mascot {
  aspect-ratio: 1 / 1;
}
.guide-mascot img {
  /* ⚠ 两张图**叠着放**（绝对定位）：都在场、只是透明与否 ⇒ 留在文档流里的话，格子会被
     它们上下摞成两倍高（实测小态 64 → 128 ✓）。格子的高度本来就由 aspect-ratio 定 ✓。 */
  position: absolute;
  top: 0;
  left: 0;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  -webkit-user-drag: none;
  user-drag: none;
  user-select: none;
  -webkit-user-select: none;
  /* ⚠ 换态那一下（下面那两条 `filter`）走的是 **`filter: opacity()`，不是 `opacity`** ——
     整身那张的 `opacity` 归"模糊占位淡入"（`.avatar-img.loaded`）管；两件事共用一个旋钮，
     换态就会把那套淡入抹掉。两张图**都在场**（叠着放），换态是"一个淡出、另一个淡入"，
     不是换个 `display`。
     三段**同时**走（用户 2026-10-09：「能不能同时执行这 3 段动画」✓ —— 先做过分阶段的版本，
     他把节奏定成同时）：格子缩/放（.35s，`.guide` 那条）＋ 两张图交叉淡（`--cross` = .4s），
     都在 t=0 起跑 ✓。 */
  transition:
    opacity 0.8s ease,
    transform 0.35s cubic-bezier(0.2, 0.7, 0.3, 1),
    filter var(--cross, 0.4s) ease;
}
/* 小态那个头像：**圆形**（用户 2026-10-09 点名要聊天窗那颗的样式），取景照抄 `.msg-avatar`
   （方形盒里 cover、`50% 15%` 把脸取出来 —— 那张图 265×345、头在上三分之一，居中的话裁窗
   会落到肩膀）。
   ⚠ 外面那层 `span` 是**给自己撑一个方盒**用的：`<img>` 是替换元素，`aspect-ratio` 里那个
   `auto` 认的是它自己的固有比例（265:345）⇒ 直接写在图上，放大那一下（格子先变成 2:3）
   它也跟着变竖，`border-radius: 50%` 于是画出个**竖椭圆** ✗（实测：放大过程中脸是长的 ✓）。
   方盒里它永远是正圆 ⇒ 换态时看到的是"圆头像淡出、再淡入全身" ✓。
   ⚠ 贴**底**不贴顶：格子从 2:3 切回方形时（放大那一下）高度会从 96 掉回 64，贴顶的话
   这颗头会跟着往上跳 32px ✗；贴底则一动不动，就在小态那个位置上淡出 ✓。 */
.guide-mascot .avatar-head {
  position: absolute;
  bottom: 0;
  left: 0;
  /* ⚠ 边长**不跟格子走**：格子放大那一下宽度在动（64 → 195），跟着走的话这颗头会一路
     胀大 ✗（用户 2026-10-09：「为什么头像也会跟着放大，很明显」）⇒ 卡在小态的边长上，
     它就守在原处、原大小淡出 ✓（缩小那一下反之：它淡入时格子已经缩到同一个大小 ✓）。 */
  width: min(100%, var(--face, 64px));
  aspect-ratio: 1 / 1;
  border-radius: 50%;
  overflow: hidden;
  filter: opacity(0);
  transition: filter var(--cross, 0.4s) ease;
}
.guide-mascot .avatar-head img {
  object-position: 50% 15%;
}
.guide-mascot .avatar-img {
  filter: opacity(1);
}
.guide.small .guide-mascot .avatar-img {
  filter: opacity(0);
}
.guide.small .guide-mascot .avatar-head {
  filter: opacity(1);
}
/* 停靠右侧：形象水平镜像，朝向屏幕内侧（翻转由方向感知动画驱动） */
.guide.mirror .guide-mascot img {
  transform: scaleX(-1);
}
.guide.mirror .guide-mascot .avatar-ph {
  transform: scaleX(-1) scale(1.05);
}
/* 小态底下**什么都不垫**（用户 2026-10-09：「去掉圆形背景」）。
   头像那张图是**抠像**（四角透明），背后那层模糊占位会整圈糊出来（先去掉了模糊 ✓），
   换成一枚纯色圆盘之后又成了"一颗有底色的圆"（再去掉 ✓）—— 现在就是**一个圆形的头像**
   浮在页面上 ✓。跨线淡出时它淡到没有了，那正是这套提示的意思。
   ⚠ 用 `visibility` 不用 `display`：`display:none` 的元素"变成显示"时**不跑过渡** ✗
   （下面那条延迟就落空了）。
   ⚠ 要压在 `.guide.mirror …` 那条后面：同分，靠文档顺序赢（也无所谓，反正不显示了）。 */
.guide.small .guide-mascot .avatar-ph {
  opacity: 0;
  visibility: hidden;
  transition: none; /* 缩小：立刻藏（那条延迟只管"露面"，不管"收起来"） */
}
/* 大态那层模糊占位：**等这一段交叉淡完再露**（`--cross`）。
   不这样，放大那 .4s 里那层糊会从半透明的全身底下透出来（实测：一块糊 ✗）——
   而那本来只是"整身图还没加载完"时的垫底，图在了就不该再冒头。
   ⚠ 形象初始就是小态 ⇒ 这条延迟不会把"冷启动时那层占位"挡掉 ✓。 */
.guide-mascot .avatar-ph {
  opacity: 1;
  visibility: visible;
  transition:
    opacity 0s linear var(--cross, 0.4s),
    visibility 0s linear var(--cross, 0.4s);
}
/* 拖动提示：跨线淡出、回来淡入；松手才真正换向。
   ⚠ 类挂在**这一格**上、不挂在图上：格子里有两张图（小态头像 / 大态整身），一条选择器
   一次盖住两张 —— 挂单张的话，另一种状态拖起来毫无反馈。 */
.guide-mascot.fading img {
  opacity: 0;
  transition-duration: 0.18s;
}
.guide-bubble {
  position: absolute;
  /* 气泡是纯说明，不该吃点击：它可以很宽（2026-10-09 起长话会铺满整个屏幕宽），会压到
     户型图页右下的 FAB 上，于是点「撤销」先被气泡接走。抬 z-index 没用——#confirm 自己是
     z-7 的层叠上下文，整棵子树都在 #guide(9) 之下；让点击穿过去才是对的。
     与地图那颗 .journey-bubble 同一套做法。 */
  pointer-events: none;
  /* 横向落位由 `placeGuideBubble()` 现算（居中于形象的头、再夹进屏幕内边距）——
     这个 `left` 只是没跑 JS 时的样子。 */
  left: 25%;
  bottom: calc(100% + var(--guide-gap));
  width: max-content;
  /* 多宽：话长了就往两边长，长到屏幕的两条内边距为止（用户 2026-10-09：「拉大宽度撑满屏幕」）。
     从前封在 220–258px 的窄栏里 ⇒ 一段两百字的话要摞十几行、高到盖住户型图；现在它铺开成
     一条横带，同样的字只占几行。`--bubble-max` 由 `syncGuideBubble()` 按 .app 的宽度写。 */
  max-width: var(--bubble-max, calc(100vw - 20px));
  /* 封顶：话说长了不许一路往上顶到户型图上（用户 2026-10-03 报的「内容较多时会遮挡页面内容」）。
     用户 2026-10-09 又收了一档（30vh/260px ⇒ 25vh/220px）——加宽之后，最坏情况（服务端上限
     两百字）本来就落在这个高度里，封顶只兜那种真的更长的。超出的部分在正文里滚。
     ⚠ 只有**装不下**的时候才打开指针（.is-long，见下）——其余时候整块穿透，那条"气泡不吃点击"
     的老规矩照旧。 */
  max-height: min(25vh, 220px);
  display: flex;
  background: var(--bubble);
  border: 1.5px solid var(--bubble-edge);
  border-radius: 16px;
  padding: 10px 13px;
  font-size: 13px;
  line-height: 1.55;
  color: var(--ink);
  box-shadow: 0 3px 12px rgba(0, 0, 0, 0.08);
  text-align: left;
  z-index: 3;
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
}
/* 正文：气泡封顶之后由它滚。`min-height:0` 是 flex 子项能滚的前提（默认 min-height:auto
   会把它撑到内容高度，于是滚动条永远不出现）。 */
.guide-bubble-text {
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
}
/* 装不下的时候才接收指针（能滚），其余时候整块穿透。@see styles.css 顶上那条注释 */
.guide-bubble.is-long {
  pointer-events: auto;
}
/* ✕：**只有"说的话"才配它**（`.is-message`，由 typeGuideText 挂）——设计页那行
   「当前讨论：主卧」是状态，收起来也马上会回来，配个 ✕ 是骗人。
   与 `.journey-bubble button` 同一套：气泡整块穿透，但按钮自己要能点。 */
.guide-bubble-close {
  display: none;
  position: absolute;
  top: 2px;
  right: 2px;
  width: 26px;
  height: 26px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--ink-soft, #83785e);
  font-size: 12px;
  line-height: 1;
  cursor: pointer;
}
.guide-bubble.is-message .guide-bubble-close {
  display: block;
}
/* 叉号由 CSS 画：按钮里**不放字**——`#guide-bubble` 的 textContent 得保持就是"说的话"
   （好几条用例在断它为空/含某句）。可读的名字在按钮的 aria-label 上。 */
.guide-bubble-close::before {
  content: "✕";
}
.guide-bubble.is-message .guide-bubble-text {
  padding-right: 14px;
}
.guide-bubble button {
  pointer-events: auto;
}
.guide-bubble::after {
  content: "";
  position: absolute;
  /* 尾巴的**尖**指着头顶：`--guide-tail` 是那个尖的横坐标，由 `placeGuideBubble()` 现算
     （减掉的 8px 是三角自己的半边宽）。从前是 `left: 14px` 一个定值——气泡只有 220px 宽、
     又总贴在头正上方时够用；加宽成一条横带之后，头在哪就得算到哪。 */
  left: calc(var(--guide-tail, 22px) - 8px);
  bottom: -8px;
  border: 8px solid transparent;
  border-top-color: var(--bubble);
  border-bottom: 0;
}
/* ⚠ 形象停靠右侧（`.flip`）从前另有一套 left/right 25% 的摆法，撤了：气泡的横向位置现在
   全由 `placeGuideBubble()` 算（居中于头、夹进屏幕），尾巴跟着头走，不需要按侧翻。 */
/* ============ 引导页：叶知秋开场对话 ============ */
:root {
  --intro-thumb: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAArCAIAAADt+9+sAAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAGKADAAQAAAABAAAAKwAAAAAEtLf7AAAG1UlEQVRIDV1WW28jSRXu6qq227fEsXNbZsMyEwaxuyCNQFwkVuK2QuKNZ/4WP4I3EA8I8bS88MBKK5jdHdiZzMTJ2CZ2nHRst/veXcV3TrWzw5bS5dPn8p1L1Tkd8ft/lcIVjuMYfrALeqNljFOT/COYAV3NfEiJcEkHIiVhV+sx1pYmoDdpQuZ3x5H8C6k0ZMKOhZIuGVgVCsquLURtumVbBYrUhod9q89AWz0Sb2kijINELIcQOSm8k2O8WyyrL5wa6N6JywZWCiYWYdkQkNRX8t2mAlDFha7TJ0OW2Si+jIXgeX0l1S0PmkqhRvxOOx5Wtfp4s0HV5vjh6rL6l4drC6IoW17kFQ+jEm/LBxbzmIEybSXWKbRqINLbpnpvf08A494TGVCQNYfQ2YP1pJSsHb4RBMjt4riwsQntFtei3DNB4EKSmP7eTMHiMICVWixCsWrg2mMBwSVRIJA2xcyLCGhbWrN/a8lMq0YqeGVQypdJOrW8qi1J2RZCUN1tFtYzaYDJUuuUaMZCTq501GHHCXNRaZNVTsUh1NERKufMiNYHMSwMC4F11BGeSzzVbTi9htNSYpk588ggOgRSx4Ks+YzIMbcLmW8vJ1AGvjjumKhwktJROw0KuOJCNCQlDmObDuV/X1oeFxwHbWj1loI/M4ucUovSGDFeVigT2wvAIeHblOL6vzRtvdiBTc2txxAQXTJHr21y0oKYsagAiN1zKZo3Q6Ps8ECK0rq8C+FJkUejMg88/4GS0oU1/JeVhiVoTocR+aYAkv4IgkaplMJmALXl/KPw5pPpfO2ofeXmU+0Y1zSarUNgAdHCkXeqFf3hwZSQji7yNFlHeZ47Xs8T8/ju2fhqFWfmcN+ou+kfpvPQqK9/7/u/bjYGlRFlZXAbKDRCoBzp9lfp1WR6vVjmwttptTbxi27jv8vgbhUmXztojSeX6vz1/NU4kV4Shusf/fi3RSV2e+1ux0sKU2qUyQghdJlOp9MSwbpKKK/XCsvkwi3GTVn2e96nz5NCC/X8otykvtab/q54PhoHGznsNd5//NZ+v4OAKE3jJJqKiBBdoXe8ySo4a7qb2SyaB3m4wUeo+fCBVC3f73XEwV51HYxenxde90ncOIqS0lfxapPA/mi4IwVCE1IUunia6Xh2u4ky4Qs/TvJ2uyNdcTGtlDSRNt6LURKsSqXOT05PD4fDeHVxc9tbx0VZmvld9ujBQDqLeHO52/KE7u21h+8MTZW9KgLZa8gwTKQxanqVOkpL5bf89ODw5GjY7KlPX7x87Xrf3Dt40pRJVQZJ7ulsdHW1GDZ23morEV0H4yBMoqN9ebOO01QXjpYf/OJD5eIqF3lp8rJ457iKNjejSbxYzAf9w8HOenQ1zZPZzfXLyTyczZc//+Anu/3h0+eXn3x+PjgaNpp9p/JwN9VvPvxpS8l2x8d9/vNfPpq+nDjdblm6cZItl6+8xu5yne2q+eIui9Z5P41wrY6PD/r7x0Kc+c7xtx+djs7P8sooT77sd999eHISRomTRlHlDg8O2lna7UlXdue3pumq23Urzfa7ftDQOTWP4+R5UWq90zdpdb2IF+s4UX/7+PJwbyFEc9gfogeK9DreNPe6stkeGCdsqFaU5q4rHr2t0o1/+6rCbViHmyxP3/5GZ3L3xd//k0WZzEsXE3ITrJJ/fPbxD97/YbPhRessmi10/0Ejzlsd/Z1vnR4PhlfzZ5fTRRBEna5zuzobz8Z3+etf/eyXRnvfPVXUwWiAP/3xd9PZ3e2qfO/h47N/f5Eb9/S9J732ru+3cI8fnhwcDTD1qjjOL6efPX3210VAUwz9Pex3A3Sr2j0cFNeBFkUw+ufnF+hEjNnL8WQwHJ4+ftcY01ACN83Fon8QnFZT+m58twzW63AdxmWVh3EeJmWaZ6UucXNFuZ5UaHoaaQIjBVc5y4qC+hZDCxxRVQZTAXA9X4NTZGmyiTEA0NtoRB4VPCCqcEq/1OpYIIlAo/IrGo2a1tJIxy6tqxJ3virLoip49BRFqaDEGFabSWpUi0sITHLLEg996+CEPM/jYQdbcMk9PpAYodt/HiwOooBJ/QATg49iZFe8kSfkRJFj40iEImCo8hs2nmRWg1FZ105rMqnRLWlRiEbSSA3vjF5LOeQ6NIrFOmQhNGtlmxDXgDbwERHV01YCYkMHiB2XARcN/UBrWzJrA0yCqyr60pOYnGhlDwKW7JuSgAi3xxqDj6hY2yZt1alRiMkHQYb4ZLMmqfKnDHr2gKyUNLiIqK31BMZWmWSUClRhrlwpKRHyRDt4dCKYrPQRYy2qB/O0RqhIGzsdLPRd3GJOzZj/Af4C/AhOcNoEAAAAAElFTkSuQmCC");
}
:root {
  --avatar-thumb: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAkCAYAAACTz/ouAAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAGKADAAQAAAABAAAAJAAAAACG5CAqAAAE4UlEQVRIDa2WbUxbVRjHz73nvrS9LaUtpby/bHOAjWRKkBijjsUPSxxZ2ES3ZBoXo4mJ7IvRfTBGsiy6+AVjliwzhilkhpRE3aJZEBd0ukVhbGzYARuvk0GhQIH2tr1v53iuJgtlvbSMnS/3Ps895/8753mee86BII32RWMj78jLrnrm+Z1zfX19KI0h97vA+28GL01NTfTvV3u/jMakk8HpmcqaqsqJv4fv3DPo/oA7JcAGgCsqSc2ZNpu1prKsfGlZrM/LzTs3MjGx8IBaEgedxJfgMrndIUlRbwsmFjS+sQ/VeLe4lHj0lYRO6xgpAR0dHZqFYa5VbSsAYlikdj9XDQo9Wc51NBM+pQTovUdGRz/NtJm/cVotIMvtPkNT3NcJKusYaQFu9na7dtbtj2gIgfE7twdfO3JkdB3NhE9UgpXEWBwbqDSbTFcoGgqKuILk0BwdYYTh0GJ4/45du/1JhiS4mAQriSFLcrHNahNWwhHE8QKwlniRHdJlFDOz7/hHHx4oLizYGg5HBm703+w63dZ2da1ESgBSFUnVNKDIEmAYCGRFARDzGNPM0e3F2Yz38TI+IsUPjk9ORon4xgEA0nFVVbEkSUAHxeUVwHJmLC3eEyoqnsA0a0ZwKUg/WeDoXzt73U6ZZFlT1agoAlmWKQYpQKAk4IJRYEFxxLM8yHC6aTkmj7/ozRlOBkgZInE+NEY7nYsOC+dC4QU8LyJwfXYJBILzwCIE4m63C3b9ce3EiebmQDJAyhV4a18KTIyM7p2aXfCznlLqn9ACmBYxEGk7HZiZ/eCzU21P3Rgaak8mrvtSrkDv5HI6RnOKirIYzkQNDg9hR/4OwFs9QOQ4d2dn07qlmnIFOsCZk/u+I9PuWY7EUO7WakASC+LxGAAY1HefaTLpfYxaSoC/u9vKcVyd/kfOL8eIuBUgTANMGnFti7E5W4zEdX9KgLe2NjIyOXe8Z2AMT82FgVmwY95kJkMxpSBqZmRRntoUQB/cMxSAt+6uEEEG6CuBNA0Q2ZdUVbPH4zbLpgGC2VzPspCEHJPwIAAZFmiahhVFyaRpNX9TAN/530oZlnmB7EkYk1lrGgGQFZCGyZ8NEcIlmwJosdghjjdl6DOmKAqTJ6BInDiO1RMNKJYpf2iAz9dlRxgfVlUF6GeBhjRKUWRKrx+WhAkjDMihXvnQgLgivUxKtJRsdohMmoSICOoJJnmgyFNfDbnDeP1+P2cEMSxTn88HicBbDGT0jQ7TRFAPCYSQVJCGddB/SUeo7PLlgZINAxoaGsh88ceapi7/r0uUSSNR1yNPC4JAkzCFSdh+lSRZMQIY7kV6Qsmgznbfz8ckWWLNZvs7gIbFPFCQIi4HZyLaXwwHvzr63tvnjcQ35G9t/7Hi0tmWnsj1X/DKlR9a0x1smIO1Aq8f2DNY5qRDgl0AfIY97ftp2oDFrtN2TrB6yAF0iZSRY+0EjOy0AD6MoWTJ/5xhWHKAasdIFeX6fA0p77U6NC0A39LiCa5ED1KEoATnBkm9enYVvPqY0axX+9MCTIr9C5PTgTeXJLR9AsE9KqYKrWZ73Woho/e0lnnhQo9Wf+jwLRYiJa4iN/mri6xmvvrZp8u/PXvuon4ferTtz5/a997tvRjo7/r+3UervErtu5OfuDpbT2WvciV9/Rc68CKHQskBXAAAAABJRU5ErkJggg==");
}
.intro {
  position: absolute;
  inset: 0;
  z-index: 30;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 24px 20px 56px;
  background: var(--intro-fallback);
  overflow: hidden;
  transition: opacity 0.45s ease;
}
/* 渐进式背景（LQIP/blur-up）：先显示微小模糊占位，原图加载完成后淡入替换 */
.intro-bg,
.intro-bg-full {
  position: absolute;
  inset: 0;
  background: center / cover no-repeat;
  pointer-events: none;
}
.intro-bg {
  background-image: var(--intro-thumb);
  filter: blur(14px);
  transform: scale(1.06);
}
.intro-bg-full {
  background-image: url("assets/community.webp");
  opacity: 0;
  transition: opacity 0.5s ease;
}
.intro-bg-full.loaded {
  opacity: 1;
}
/* 设计师形象 blur-up：模糊占位先显示，GIF 加载完成后淡入 */
.avatar-ph {
  position: absolute;
  inset: 0;
  background: var(--avatar-thumb) center / cover no-repeat;
  filter: blur(8px);
  transform: scale(1.05);
  pointer-events: none;
}
.avatar-img {
  position: relative;
  opacity: 0;
  transition: opacity 0.5s ease;
}
.avatar-img.loaded {
  opacity: 1;
}
.intro-avatar,
.guide-mascot {
  position: relative;
}
.intro.fade-out {
  opacity: 0;
  pointer-events: none;
}

.intro-stream {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 52%;
  z-index: 2;
  overflow-y: auto;
  box-sizing: border-box;
  padding: calc(env(safe-area-inset-top, 0px) + 46px) 14px 12px;
  background: linear-gradient(
    180deg,
    var(--stream-1) 0%,
    var(--stream-2) 60%,
    var(--stream-3) 100%
  );
  scrollbar-width: none;
}
.intro-stream::-webkit-scrollbar {
  display: none;
}
.intro-msg {
  display: flex;
  gap: 8px;
  margin-bottom: 12px;
  animation: msgIn 0.3s ease;
}
.intro-msg.ye {
  justify-content: flex-start;
}
.intro-msg.me {
  justify-content: flex-end;
}
.intro-msg-bubble {
  max-width: 74%;
  padding: 9px 13px;
  font-size: 14.5px;
  line-height: 1.6;
  border-radius: 14px;
  transition: opacity 0.5s ease;
}
.intro-msg.ye .intro-msg-bubble {
  background: var(--bubble-soft);
  color: var(--ink);
  border-top-left-radius: 4px;
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
}
.intro-msg.me .intro-msg-bubble {
  background: var(--bubble-me);
  color: var(--bubble-me-ink);
  border-top-right-radius: 4px;
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
}
.intro-msg.old .intro-msg-bubble {
  opacity: 0.42;
}
@keyframes msgIn {
  from {
    opacity: 0;
    transform: translateY(6px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}
.intro-avatar {
  position: absolute;
  left: 8px;
  bottom: 24px;
  z-index: 3;
  width: min(50vw, 220px);
  aspect-ratio: 2 / 3;
}
.intro-avatar img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* 「跳过」常驻在右上角：这段对话是自动播的，不想看的人随时有出口。
   以前这里是「轻触回应」的提示加脉冲动效——那套交互问的是「你愿不愿意听」，
   而这是一段介绍：愿意听的人被点烦，不愿意听的人早就在找出口（见 onboarding.js）。 */

/* ============ 户型图识别 ============ */
#recognize {
  position: absolute;
  inset: 0;
  z-index: 7;
  background: var(--bg);
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 40px 22px 20px;
}
.rec-plan {
  position: relative;
  width: 100%;
  max-width: 316px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 14px;
  padding: 10px;
  overflow: hidden;
}
.rec-plan svg {
  display: block;
  width: 100%;
  height: auto;
}
.rec-img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 4px;
}
.rec-scan {
  position: absolute;
  left: 10px;
  right: 10px;
  top: 0;
  height: 3px;
  border-radius: 999px;
  background: linear-gradient(
    90deg,
    transparent,
    var(--accent) 15%,
    var(--accent) 85%,
    transparent
  );
  box-shadow: 0 0 14px color-mix(in srgb, var(--accent) 55%, transparent);
  animation: scanmove 1.7s ease-in-out infinite alternate;
}
@keyframes scanmove {
  from {
    top: 8px;
  }
  to {
    top: calc(100% - 12px);
  }
}

/* ============ 识别结果确认 ============ */
#confirm {
  position: absolute;
  inset: 0;
  z-index: 7;
  background: var(--bg);
  display: flex;
  flex-direction: column;
  padding: 40px 20px 18px;
  isolation: isolate;
  /* 内容区右侧那一列签的两个数（2026-10-11 加）。
     ⚠ 挂在**页面上**、不是挂在 `.cf-folder` 上：操作面板（`.cf-edit`）是文件夹的**兄弟**，
     它也要按这两个数算自己的宽度（"面板与纸同宽同边"那条）—— 挂在文件夹上它取不到值 ✗
     （同一个坑 `--cf-ctl-h` 踩过一次，见 `:root` 里那条注释）。 */
  --cf-tab-w: 33px; /* 那一列的总宽 */
  --cf-tab-overlap: 4px; /* 整列往纸上压多少（`.cf-tabs` 的负外边距） */
}
.cf-toggles {
  margin-top: 10px;
  display: flex;
  gap: 8px;
}
#cf-g-overlay {
  opacity: 0.9;
}
.cf-wall {
  stroke: #2e6b4d;
  stroke-width: 3.5;
  stroke-linecap: round;
  /* ⚠⚠ **不填充**（用户 2026-10-08：「调节为曲墙后，扇形背景应与其他区域背景使用同一种颜色」✓）：
     直墙是 `<line>`（填不了 ✓），但**弧是 `<polyline>`** ⇒ SVG 默认 `fill: black` ✗
     ⇒ 弦与弧之间那块被涂成**黑色扇形** ✓（实测：`polyline.cf-wall.cf-edge` 的 `fill` 是
     `rgb(0,0,0)` ✓）。⇒ 墙一律 `fill: none` ✓（对线无害 ✓，对折线是必须 ✓）。 */
  fill: none;
}
/* **墙的命中带**（用户 2026-10-08：「斜墙不太好选中」✓）：每一段墙（含覆盖边 ✓）底下垫一条
   **透明但吃指针**的粗线 —— 墙线本身只有 3.5～4.5 单位宽（屏幕上 ≈1.4px ✗），手指压不住；
   垫线 14 单位（≈5.4px ✓）与端点的胖靶子、门窗那条 `.cf-hit` 同一个量级 ✓。
   ⚠ 它**一直透明**：选中/悬停那两套 class（`.is-hover` / `.is-sel`）挂在 `.cf-wall` /
   `.cf-outline` 上，垫线**不挂**它们 ✓ —— 挂上去就会跟着变粗变橙、把墙画胖一圈 ✗。
   ⚠ `pointer-events: stroke` ＝ 只有画出来的那一条粗线吃事件（线的**内部**不吃 ✓），
   与 `.cf-hit` 同一手 ✓。 */
.cf-hitline {
  stroke: transparent;
  stroke-width: 14;
  stroke-linecap: round;
  fill: none;
  pointer-events: stroke;
}
.cf-outline {
  stroke: #2e6b8f;
  stroke-width: 4.5;
  stroke-linecap: round;
  /* ⚠ 与 `.cf-wall` 同一条（见那里的注释 ✓）：**墙线一律不填充** —— 斜墙/曲墙画的是
     `<polyline>`，SVG 默认 `fill: black` ⇒ 弦与弧之间那块会被涂黑 ✗。
     ⚠⚠ 这条**必须三支都写**（`.cf-wall` / `.cf-outline` / `.cf-edge` ✗）：边一旦按身份
     改走 `.cf-outline`（2026-10-08 ✓），漏掉的那一支黑扇形就回来了 ✓（实测：用户当天报的
     "曲墙区域又变成黑色了" 正是这个 ✓）。 */
  fill: none;
}
.cf-edge {
  fill: none;
}

/* 房名：**一间房一个、落在房间中心**（用户 2026-10-05 第七十八条）——居中对齐（位置由 JS
   按面积重心算 ✓）。
   ⚠⚠ **2026-10-13 起它吃指针了**（用户：「去掉房间操作区的 chip，改成点击区域名直接编辑」✓）：
   操作区那颗芯片（名字 ＋ ✎）整个撤了 ⇒ **选中一间房与改名都搬到这颗名字上** ——
   点一下＝选中它、再点一下＝就地改名（用户当天的三条路里挑的 ✓，见 `calibration.js`
   的 `tapRoomName` ✓）。从前这里写的是 `pointer-events: none`（那时改名走芯片上那颗 ✎，
   落在名字上的 `cursor: pointer` 是个谎 ✗）✓。
   ⚠ 字号：12 → 15（第八十三条）→ **18**（第一百零三次：「区域 tab 内容区的区域名字号再调大些」✓）。
   画布是 900 的 viewBox、屏幕上再乘 ~0.387 ⇒ 18 个用户单位 ≈ 7px。⚠ 别把字号写回标记的
   `font-size` 属性上（属性最弱、改起来还得进 JS ✗）。 */
.cf-name {
  text-anchor: middle;
  font-size: 18px;
  font-weight: 700;
  fill: #1f4d35;
  /* ⚠ 默认**不吃指针**（SVG 的 text 默认是 visiblePainted ⇒ 不写这一行它就是吃的 ✗ ——
     只在「房间」那一组改成 auto，见下面那条 ✓）。 */
  pointer-events: none;
  /* ⚠ 命中面积**靠这圈白边撑大**（`paint-order: stroke` ⇒ 描边画在字的**底下** ✓）：
     7px 高的字，光靠笔画本身手指压不住 ✗。这圈 6px 的白边同时干三件事 ——
     ① 当靶子（与墙上那条透明命中带同一个道理 ✓）、② 压在照片上也读得清 ✓、
     ③ 选中时它天然就是一枚"标签底" ✓。 */
  paint-order: stroke;
  stroke: rgba(255, 255, 255, 0.92);
  stroke-width: 6px;
  stroke-linejoin: round;
}
/* 选中的那一间：房名变橙（与画布上的焦点格、目标格同一个 `--node` ✓）。
   ⚠ 芯片撤了之后，**"现在往哪间划"全靠它说** ✓ —— 只靠 ＋/🗑 那两颗按钮变个底色，
   在看图的时候太轻了 ✗。 */
.cf-name.is-sel {
  fill: var(--node);
}
/* ⚠⚠ **只有「房间」那一组吃指针**（2026-10-13 实测撞出来的 ✗）：房名画在各层之上，
   而它在别的组里只是**标签** —— 一旦吃指针，那几组里压在同一位置的靶子就全被它挡住了：
   量房那一组点房间＝跳到那一间（`.cf-walk-hit` ✓）、墙体那一组点墙（`.cf-hitline` ✓）、
   门窗那一组点门窗（`.cf-hit` ✓）—— 而房名**正好落在房间重心上**，实测就是
   「点次卧跳到它的第一段」那条用例当场红 ✓（`pointer-events: none` 的老规矩本来在挡着这件事 ✓）。 */
#cf-plan[data-cal-group="zone"] .cf-name {
  pointer-events: auto;
  cursor: pointer;
}
/* 就地改名那颗输入框：**浮在画布上**（2026-10-13 从操作区搬来的 ✓）。
   ⚠ 定位＝`absolute`，位置由 JS 按**点下去那一下**在画布里的位置给（`startRename` ✓）——
   不跟着 SVG 的缩放/平移走：改名是一瞬间的事，用户这时候不会去捏合 ✓。
   ⚠ 长相照那颗芯片（药丸、白底、深字）：它替换掉的就是画布上那颗房名 ✓。 */
.cf-name-input {
  position: absolute;
  z-index: 4;
  box-sizing: border-box;
  min-width: 4ch;
  max-width: 60%;
  padding: 4px 10px;
  border: 1.5px solid var(--node);
  border-radius: 999px;
  background: var(--surface);
  color: var(--ink);
  font-size: 13px;
  font-weight: 700;
  text-align: center;
  outline: none;
  transform: translate(-50%, -50%);
}
/* 门窗的笔画颜色：**一个变量说了算**（`--cf-opening` ✓）。⚠ 只给颜色——线宽仍按每条路径
   自己的描边（门扇 2.2 / 引线 1.2 ✓），"选中"那套装在 `.cf-item.is-sel path` 上照旧盖得过来 ✓。 */
.cf-open-mark {
  fill: none;
  stroke: var(--cf-opening);
}
/* ⚠ 命名表（`.name-grid` / `.name-chip`）2026-10-04 用户第四次裁撤时**整张表删了**
   （「拆分和合并都不需要了」＋「点房名弹表那条路去掉」）——改名只剩芯片上那颗 ✎。
   这两条规则于是没有用户了，一并撤掉。 */
#cf-svg.cal-on .cf-outline {
  stroke-width: 6;
}
/* ⚠ **房间不再可拖**（四条拖动路径 2026-10-04 全撤了），所以 `cursor: move`（四向箭头，
   看上去就是个十字）与那条 hover 加粗一并撤掉——用户第三十五次问的"墙体内容区为什么
   还会展示十字鼠标"就是它：光标停在房间里（不是墙上）时，露出的正是这个十字。 */

/* ⚠ 墙**不再可拖**（2026-10-04 用户确认：区域的形状与大小改由「区域」tab 精确标）——
   所以那套 `ns/ew-resize`（"这条墙能拖"）撤掉，改成"**点一下选中它**"。
   ⚠ 悬停**不许再用 CSS 的 `:hover`**（用户 2026-10-04 第三十五次问的："指向时加粗的是那一段，
   点击后却选中好几段，连垂直的相邻段都算"）：`:hover` 只认光标底下那一段，而当时选中的单位是
   **两间之间那条边界**。现在两边都由 JS 按**同一个段键**点亮（`.is-hover` / `.is-sel`，
   见 `paintWallHover` / `paintWallSel`）——悬停就是"这一下会选中什么"的预览。
   ⚠ 第三十六次把单位收成"**段**"之后（共线的几段算一段、垂直的各算各的），这一条不变。 */
/* ⚠⚠ **四支笔的高亮**（2026-10-13，用户：「点砌墙则高亮所有开放墙体…点拆墙则高亮所有实墙」✓）：
   **纯 CSS** —— 墙元素上本来就带着 `cf-open`（开放边界 ✓）与 `cf-edge`（画出来的那条 ✓），
   JS 只把"拿的是哪支笔"写到 `#cf-plan[data-wall-brush]` 上 ✓（见 `renderWallBar` ✓）。
   逐支对齐"**点得动的那些**"：砌墙＝开放着的 ✓、拆墙＝实墙（**含画出来的那条** ✓ 它也是
   `.cf-wall` / `.cf-outline`，只是不带 `cf-open` ✓）、调节＝画出来的那条 ✓、
   画一道墙＝不画（它自己分两步问落点，落点在整张图上 ✓）。
   ⚠⚠ 特异性要**压过**上面那套悬停/选中（都是 (1,2,0) ⇒ 这里 (2,2,0) ✓）：
   笔拿在手里的时候，"哪些点得动"比"鼠标底下那一段"更要紧 ✓。
   ⚠ 橙色 `--node` 是这一页的"正在弄的东西"（选中那套也用 ✓）——它在这儿说的是
   "**点得动**"，不是"选中了"：点一下就走，没有"选中的那段墙"这件状态了 ✓。 */
#cf-plan[data-wall-brush="build"] #cf-svg .cf-open {
  stroke: var(--node);
  stroke-width: 5;
  /* 线粗了，虚线也得疏一档（与 `.cf-open.is-hover` 同一条理由 ✓）。 */
  stroke-dasharray: 10 8;
}
#cf-plan[data-wall-brush="break"] #cf-svg .cf-wall:not(.cf-open),
#cf-plan[data-wall-brush="break"] #cf-svg .cf-outline:not(.cf-open),
#cf-plan[data-wall-brush="adjust"] #cf-svg .cf-edge {
  stroke: var(--node);
  stroke-width: 6;
}
/* ⚠ 墙上的小手**只在拿了笔的时候**（2026-10-13 改准）：没拿笔时点墙什么都不做 ✗
   （用户：「未选中任何工具时…只能双指捏合缩放、单指按下拖动」✓）⇒ 小手是句谎 ✗。
   ⚠ 与下面门窗那条的分工不变：这一条只管「墙体」那一组 ✓（`data-cal-group` ✓）。 */
#cf-plan[data-cal-group="wall"][data-wall-brush] #cf-svg.cal-on .cf-wall,
#cf-plan[data-cal-group="wall"][data-wall-brush] #cf-svg.cal-on .cf-outline,
#cf-plan[data-cal-group="wall"][data-wall-brush] #cf-svg.cal-on .cf-hitline {
  cursor: pointer;
}
/* 门窗自己**要**是小手（同一条的后半句：「悬停在门/窗上时应展示为小手」）——靶子是那三块：
   沿墙的命中带（`.cf-hit`）、洞口中心那颗圆（`.cf-item > circle` ✓ 直接子元素，避开
   `.cf-marks` 里那些画出来的图形与手柄 ✓）、以及整扇门那块扇形（`.cf-hit-area` ✓）。 */
/* ⚠ 与墙那条**对称**：门窗只在「门窗」那一组里可点（用户第一百零三次的补充：「墙体 tab
   内容区禁止选中门窗、且悬停指标不要用小手」）⇒ 小手也只挂在那一边 ✓。
   ⚠⚠ **2026-10-13 起还要拿着「调节」或「拆门窗」那支笔**：放门/放窗那三支底下点门窗什么都不做（那一手是"定落点" ✓），
   空着手更是只认看 ✓ ⇒ 那时候摆一双小手是句谎 ✗（与墙体那一组同一条规矩 ✓）。 */
#cf-plan[data-cal-group="opening"][data-opening-brush="adjust"] #cf-svg.cal-on .cf-hit,
#cf-plan[data-cal-group="opening"][data-opening-brush="adjust"] #cf-svg.cal-on .cf-item > circle,
#cf-plan[data-cal-group="opening"][data-opening-brush="adjust"] #cf-svg.cal-on .cf-hit-area,
#cf-plan[data-cal-group="opening"][data-opening-brush="del"] #cf-svg.cal-on .cf-hit,
#cf-plan[data-cal-group="opening"][data-opening-brush="del"] #cf-svg.cal-on .cf-item > circle,
#cf-plan[data-cal-group="opening"][data-opening-brush="del"] #cf-svg.cal-on .cf-hit-area {
  cursor: pointer;
}
/* 拿着「放门 / 放窗」时，**墙上是小手**（那儿点得动 ✓ —— 这一支的落点就是墙 ✓）。
   ⚠ 与"调节"那条正好互补：一个指着墙、一个指着门窗 ✓。 */
#cf-plan[data-cal-group="opening"][data-opening-brush="door"] #cf-svg.cal-on .cf-wall,
#cf-plan[data-cal-group="opening"][data-opening-brush="door"] #cf-svg.cal-on .cf-outline,
#cf-plan[data-cal-group="opening"][data-opening-brush="window"] #cf-svg.cal-on .cf-wall,
#cf-plan[data-cal-group="opening"][data-opening-brush="window"] #cf-svg.cal-on .cf-outline {
  cursor: pointer;
}
/* 开着的墙（`.cf-open`）**一律是细虚线**——⚠ 得压过上面那条给外墙加粗的
   `#cf-svg.cal-on .cf-outline { stroke-width: 6 }`（(1,2,0) > `.cf-open` 的 (0,1,0)）。
   而"开着的外墙"现在也**可以被选中**了：从前外墙一开就丢了 `.cf-outline` 类，
   于是它再也点不着、恢复不了（顺手修掉的老 bug）。 */
#cf-svg.cal-on .cf-outline.cf-open {
  stroke: #9a9486;
  stroke-width: 2.5;
  stroke-dasharray: 6 5;
}
/* 悬停与选中**都要压过上面那两条**（(1,3,0) > (1,2,0)）——不然外墙的"粗一档"看不见。 */
#cf-svg.cal-on .cf-wall.is-hover,
#cf-svg.cal-on .cf-outline.is-hover {
  stroke-width: 6;
}
#cf-svg.cal-on .cf-wall.is-sel,
#cf-svg.cal-on .cf-outline.is-sel {
  stroke: var(--node);
  stroke-width: 7;
}
/* ⚠ **虚墙高亮时要换一套疏的虚线**（用户 2026-10-05：「虚墙高亮的虚线太密，增大间隙」）：
   `6 5` 那套是配 2.5 线宽的（细线上一段 6 的空 5 看着正好），可高亮一加粗到 6~7，
   同样的 6/5 几乎连成实线 ✗。疏密得**跟着线宽走**——SVG 的 `stroke-dasharray` 没有相对
   单位，所以按档手写：线段与间隙都取线宽的 1.3~1.6 倍，才是"一段一段"的样子。 */
#cf-svg.cal-on .cf-open.is-hover {
  stroke-dasharray: 9 8;
}
#cf-svg.cal-on .cf-open.is-sel {
  stroke-dasharray: 11 11;
}
.cf-rb {
  stroke: #3f7c5c;
  stroke-width: 1.5;
  stroke-dasharray: 4 3;
  pointer-events: none;
}
.cf-open {
  stroke: #9a9486;
  stroke-width: 2.5;
  stroke-dasharray: 6 5;
}
/* ⚠ **画出来的门/窗符号不吃指针**（2026-10-05，`plan-page` 那条 hover 用例当场抓到的）：
   门符号现在是**按真实尺寸**画的（门扇长＝洞口净宽 ⇒ 一扇 900 的门在图上就有 70 多个单位长，
   是原先那个固定 20 单位的示意图的三四倍）⇒ 它会盖住旁边那段墙，指针落在符号上就再也
   悬停/点不到墙 ✗（实测：`#cf-svg.cal-on .cf-wall.is-hover` 一条都不亮 ✗）。
   符号是**看的**，靶子有两个：沿墙那条 `.cf-hit`（洞口那一段）与洞口中心那颗圆 ✓。
   ⚠ 手柄要放行：飘窗进深那颗**就画在 marks 里面**（`windowMarkSVG`）✓ 校准页里要靠它拖进深 ✓。 */
.cf-item .cf-marks {
  pointer-events: none;
}
/* ⚠ **只剩飘窗进深那一颗** ✓ —— 窗的"改宽度"手柄 2026-10-09 撤了 ✗（用户：「本来门窗也没有
   这个功能，而且调也调不准，就在编辑 Sheet 里改就行」✓）：净宽只在编辑表里改 ✓。
   ⇒ `.cf-handle` 那三条规则一并删掉 ✓（不然就是留着一段谁也点不到的死样式 ✗）。 */
.cf-item .cf-marks .cf-depth-handle {
  pointer-events: auto;
}
/* 转方向那颗按钮（用户 2026-10-06 第九十二次）：长在扇形那段弧的中点上，**只在门被选中时**
   才画。⚠ 色彩与"能上手操作"那一族一致（`#c07b3a` ✓ —— 飘窗进深那颗也是它），只是它**只点
   不拖** ⇒ 光标是 pointer ✓。 */
.cf-turn {
  display: none; /* 只在选中的那扇门上露出来（见下一条）——选中态是**换 class**，不是重画 ✓ */
  cursor: pointer;
}
.cf-item.is-sel .cf-turn {
  display: block;
}
/* ⚠ 线宽跟着按钮的**屏幕尺寸**走（见 `doorTurnButton` 那段注释）：画布 348px ÷ 900 的 viewBox
   ⇒ 一个单位只有 0.387px，所以"1px 的线"＝2.6 个单位起 ✓（1.2 那种细线在这儿是发丝，
   放大之后才看得出来 ✗）。 */
.cf-turn circle {
  fill: #fff;
  stroke: #c07b3a;
  stroke-width: 2.4;
}
/* ⚠⚠ 这一条从前**从来没生效过**（用户第一百零二次第五条：「旋转按钮内部 icon 线条太粗了，
   且看不清箭头」）：`.cf-item.is-sel path`（0,2,1）盖过了它（0,1,1）⇒ 弧一直是按"选中那套"
   画的 —— **粗 3.5**、色是 `--node` ✗；箭头那两笔在 3.5 上糊成一团，当然读不出来 ✓。
   修法是把那几条**收窄到 `.cf-marks` 里**（它们本来就只该管画出来的门弧/窗格 ✓），
   这里于是拿回了自己的 1.2 ✓。 */
.cf-turn path {
  fill: none;
  stroke: #c07b3a;
  stroke-width: 2.4;
  stroke-linecap: round;
}
/* 箭头改成**实心三角**（同上第五条）：两根小短笔画的箭头在这个尺寸上读不出来 ✗。
   ⚠ 它得盖过上面那条 `fill: none`（0,2,0 ＞ 0,1,1 ✓）。 */
.cf-turn .cf-turn-head {
  fill: #c07b3a;
  stroke: none;
}
/* **整扇门都能点**（用户第一百零二次第四条）：扇形那**一块面**也接指针 ——
   `fill: transparent` 是"有填充、但不画出来"，`pointer-events: all` 让这块面按几何判命中
   （`fill: none` 是**点不到**里面的 ✗）。⚠ 它接的是**房间里**那一块，洞口那一段墙本来就被
   `cutDoorGaps` 切掉了 ⇒ 依旧不抢墙的点击 ✓。 */
.cf-hit-area {
  fill: transparent;
  stroke: none;
  pointer-events: all;
}
.cf-tag {
  font-size: 10px;
  font-weight: 700;
  fill: var(--cf-opening); /* D1 / W1 那行小字跟着门窗走（与 `--cf-opening` 一处源 ✓） */
}
/* 量房包络线：它标的是"量哪两个方向"，不是户型的一部分，所以不接指针事件——
   房间、墙、门窗都还要能点。描白边是为了压在户型图上也读得清。 */
.cf-dim,
/* 逐间实测的那几个数（他自己填的）。和包络线一样**不接指针**——图上那一下是去拖房间、
   拖墙的，标注不该抢走它。 */
.cf-dim-room {
  pointer-events: none;
}
/* 逐间的数比包络线小一号、浅一档：它是"你说的"，与"图上的"要分得开 */
.cf-room-dim {
  font-size: 11px;
  font-weight: 600;
  fill: #8a6b3f;
}
.cf-dim line {
  stroke: #c07b3a;
  stroke-width: 2;
  stroke-dasharray: 7 4;
}
.cf-dim-text {
  font-size: 12px;
  font-weight: 700;
  fill: #a8621f;
  text-anchor: middle;
  paint-order: stroke;
  stroke: #fff;
  stroke-width: 3.5px;
  stroke-linejoin: round;
}
.cf-item.cf-fix .cf-tag {
  fill: #c07b3a;
}
/* 空的不立着：纸是撑满的那一块（`height:100%`），空的时候照样是一大块白板，
   看着像一张没关掉的弹层。:empty 比逐个 toggle hidden 稳。 */
.cf-plan:empty {
  display: none;
}
.cf-plan {
  background: var(--surface);
  border: 1px solid var(--line);
  /* 四角同半径。⚠ 2026-10-11 一度只圆左边（右边贴着有底的签条、直角才接得上）——
     签条那层底撤掉之后，右边留着直角就是"没画完的卡片" ✗ ⇒ 收回四角 ✓。 */
  border-radius: 14px;
  padding: 0;
  overflow: hidden;
  position: relative;
  /* ⚠ 这里是 `height:100%` 而**不再是 `aspect-ratio:1/1`**（用户 2026-10-11）：
     纸的高度由这一列的剩余空间给（`.cf-folder` 的 `flex:1` ✓ —— 底下还让开叶知秋那颗气泡，
     见 `.cf-folder` 的 `margin-bottom` ✓）。
     ⚠ 纸比宽长 ⇒ 900×900 的方 viewBox 按**短边**缩放、两头留白（换算见 `cfPlanPoint` ✓）
     —— 2026-10-16 起开这一页会给图对一次焦（`calibration.js` 的 `fitPlanWidth` ✓：
     把图上那一团放大到**撑满纸的宽** ✓），所以肉眼看到的不再是"图小在当中" ✓。
     ⚠ 别把 `aspect-ratio` 加回来：那会让纸重新变方、底下又空出一大块桌面 ✗。 */
  height: 100%;
  box-sizing: border-box;
}
.cf-plan svg {
  display: block;
  width: 100%;
  height: 100%;
  touch-action: none;
  /* ⚠ **长按要留给画布自己**（拖动框选/滑目标格时，别让系统弹出长按菜单把这一手打断 ✓ ——
     2026-10-13 之前那条"长按 = 掏出涂抹的笔"已经删了，但这条**照旧要留** ✓）：
     不然按住不放时会弹出"拷贝/存储图片"那种菜单或 iOS 的 callout，正涂到一半被打断。
     （`contextmenu` 那一支在 calibration.js 里挡——CSS 挡不住桌面右键菜单。） */
  -webkit-touch-callout: none;
  user-select: none;
  -webkit-user-select: none;
  transform-origin: 0 0;
}
/* 门窗的**命中带**（用户 2026-10-05 第七十四条：「门在图上感觉比墙还难选中」）：
   一条**只吃事件、不画东西**的粗线。⚠ `stroke: none` 时默认的 `pointer-events: visiblePainted`
   是**点不着**的 ⇒ 必须显式写 `pointer-events: stroke`（只认这层笔画，不管画没画）✓。
   44 个单位：900 的视图在 390px 宽的屏上 ≈ 19px，手指够得着 ✓（随缩放一起变大 ✓）。 */
.cf-hit {
  stroke: none;
  pointer-events: stroke;
  stroke-width: 44;
  /* ⚠ 别改成 `round`：圆头让两头各多伸半个线宽（22 个单位），压到旁边的墙上去了 ✗ */
  stroke-linecap: butt;
}
/* 门窗**选中**时画布上要看得出来——这是"难选中"的另一半：墙有 `.is-hover`/`.is-sel`，
   门窗从前**一点反馈都没有** ⇒ 点没点中全靠顶上那一行芯片，手上没底 ✗。
   ⚠ **悬停那一条 2026-10-05 撤了**（用户第八十一条：「门取消选中后，当鼠标指向门，门还是会
   高亮（但没加粗）并跟着移动」）：它与"选中"只差一线条粗细 ⇒ 指针一扫过去满屏都在亮 ✗，
   而那时候**什么都还没选中** ✗ —— 用户读到的就是"看起来选中了、其实没有" ✓。
   墙那边留着悬停是有实在用途的（悬停预览"这一下会选中哪一段"，而且它与点击**同一个段键** ✓）；
   门窗这边悬停不预览任何东西 ⇒ 只留**选中**这一种反馈 ✓（触屏本来也只有它 ✓）。
   ⚠ 颜色写在 CSS 里能盖过元素上的 `stroke="#3F7C5C"`：表现属性（presentation attribute）
   的优先级低于任何 CSS 规则 ✓（不用去改那些内联的 stroke ✓）。 */
.cf-item.is-sel .cf-marks path,
/* ⚠ 弧上的窗画的是 **polyline**（不是 path ✓）⇒ 不带上它，选中就没有任何反馈 ✗
   （用户 2026-10-09：「选中曲墙上的窗后，窗没有高亮」✓ —— 新图形没进老样式 ✓）。 */
.cf-item.is-sel .cf-marks polyline,
.cf-item.is-sel .cf-tag {
  fill: var(--node);
}
/* ⚠⚠ **拿着「调节」那支笔时，所有门窗都"亮"起来**（2026-10-13 ✓）—— 与另两页的语言一致：
   拿起来时图上亮起"这支笔点得动的那些" ✓。另两支笔（放门/放窗）**不画**：那时候能落的地方是
   **所有墙**，答案"全都行"的时候满屏高亮只是噪音 ✗（与「画一道墙」不画高亮同一个理由 ✓）。
   ⚠⚠ 这里的"亮"**只加粗、不换色**（第一版照搬"选中"那套写成橙色 ✗，`plan-page`／`openings`
   两条用例当场红 ✓）—— 门窗的笔画色 `--cf-opening`（蓝）是**符号自己的话**（"这是一扇门/窗" ✓），
   与墙那种"身份色"（内墙绿、外墙蓝）不是一回事 ✗：墙被高亮换色只是认不出哪条是哪条 ✓，
   门窗被换色是**把符号本身涂掉了** ✗。⇒ 高亮＝**粗一档**（2.4/3 → 4 ✓，一眼看得出 ✓），
   颜色照旧归 `--cf-opening` ✓；"选中那一扇"仍走 `.is-sel`（橙色 ✓，那是"就是它"✓）。 */
#cf-plan[data-opening-brush="adjust"] #cf-svg .cf-item .cf-marks path,
#cf-plan[data-opening-brush="adjust"] #cf-svg .cf-item .cf-marks polyline {
  stroke-width: 4;
}
/* **「拆门窗」那一支**（2026-10-13）：亮点的是**同一批**（所有门窗 ✓ —— 点得动的那些 ✓），
   但长相与「调节」分开 ✗：这一支是**虚线** ✓ —— "会没掉"在这个工程里一直是虚线
   （橡皮的 `.is-erase` ✓、框选预览 ✓、目标格 ✓ 同一套语言 ✓）⇒ 两支笔在图上分得开 ✓。 */
#cf-plan[data-opening-brush="del"] #cf-svg .cf-item .cf-marks path,
#cf-plan[data-opening-brush="del"] #cf-svg .cf-item .cf-marks polyline {
  stroke-width: 4;
  stroke-dasharray: 6 4;
}

.cf-item .cf-marks path,
.cf-item .cf-marks polyline {
  transition: stroke-width 0.1s;
}
.cf-item.cf-guess .cf-marks path,
.cf-item.cf-guess .cf-marks polyline {
  stroke-dasharray: 5 4;
}
.cf-item.cf-fix .cf-marks path,
.cf-item.cf-fix .cf-marks polyline {
  stroke: #c07b3a;
}
.cf-item.cf-active .cf-marks path,
.cf-item.cf-active .cf-marks polyline {
  stroke-width: 4;
}
/* 选中的那扇门窗（用户 2026-10-05）：与墙的选中**同一套语言**——换成 `--node` ＋ 粗一档。
   ⚠ 门窗的图形是 path（门弧/窗格），所以颜色改的是 `.cf-item path`，不是 `<g>` 自己。 */
.cf-item.is-sel .cf-marks path,
/* ⚠ 弧上的窗画的是 **polyline**（不是 path ✓）⇒ 不带上它，选中就没有任何反馈 ✗
   （用户 2026-10-09：「选中曲墙上的窗后，窗没有高亮」✓ —— 新图形没进老样式 ✓）。 */
.cf-item.is-sel .cf-marks polyline {
  stroke: var(--node);
  stroke-width: 3.5;
}
.cal-fab-wrap {
  position: absolute;
  right: 20px;
  bottom: 140px;
  z-index: 8;
}
.cal-fab {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: 2px solid var(--accent);
  background: var(--surface);
  color: var(--accent);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  box-shadow: 0 3px 10px rgba(0, 0, 0, 0.18);
  transition:
    background 0.2s ease,
    color 0.2s ease;
}
.cal-fab.active {
  background: var(--accent);
  color: #fff;
}
.cal-fab svg {
  width: 19px;
  height: 19px;
  display: block;
}
.cal-fab-wrap .cal-item {
  position: absolute;
  top: 4px;
  right: 4px;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: var(--surface);
  border: 2px solid var(--accent);
  color: var(--accent);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  font: inherit;
  font-size: 11px;
  opacity: 0;
  transform: scale(0.4);
  pointer-events: none;
  transition:
    opacity 0.2s ease,
    transform 0.22s cubic-bezier(0.3, 0.9, 0.4, 1.2);
}
.cal-fab-wrap.open .cal-item {
  opacity: 1;
  pointer-events: auto;
}
/* 扇面加宽：从左上（约 70°）扇形展开到左下方（约 -17°），避免接近竖直 */
.cal-fab-wrap.open .cal-item[data-group="zone"] {
  transform: translate(-30px, -84px);
}
.cal-fab-wrap.open .cal-item[data-group="wall"] {
  transform: translate(-66px, -60px);
}
.cal-fab-wrap.open .cal-item[data-group="win"] {
  transform: translate(-84px, -20px);
}
.cal-fab-wrap.open .cal-undo {
  transform: translate(-78px, 24px);
}
.cal-panel {
  position: absolute;
  bottom: 52px;
  right: 0;
  z-index: 9;
  width: 224px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 14px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
  padding: 12px 14px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.cal-panel-title {
  font-size: 12px;
  font-weight: 600;
  color: var(--ink);
}
.cal-panel-tools {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
}
/* （2026-10-13 删掉两条 `.cal-tool` / `.cal-tool.active`：界面上一个 `.cal-tool` 都没有了
   —— 那一族药丸按钮被 `.cf-edit-btn` / `.icon-btn` 替掉之后，这两条一次都没生效过 ✗。
   连同 `calibration.js` 里按 `data-tg` 切它们的那个循环一起删 ✓（两处是同一天死的 ✓）。） */
.cf-fab {
  position: absolute;
  right: 20px;
  bottom: 84px;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: 2px solid var(--accent);
  background: var(--surface);
  color: var(--accent);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  box-shadow: 0 3px 10px rgba(0, 0, 0, 0.18);
}
.cf-fab svg {
  width: 20px;
  height: 20px;
  display: block;
}
.cf-fab:focus-visible {
  outline: 2px solid var(--accent-strong);
  outline-offset: 2px;
}

/* ============ 3D 预览 ============ */
#design-chat {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
  z-index: 5;
  background: var(--bg-2);
}
.d3-full {
  position: relative;
  width: 100%;
  height: 100%;
}
#design-chat .d3-view {
  position: absolute;
  inset: 0;
  height: auto;
  border-radius: 0;
  overflow: hidden;
  background: linear-gradient(
    180deg,
    var(--bg-5) 0%,
    var(--bg-4) 55%,
    var(--bg-3) 100%
  );
  touch-action: none;
  cursor: grab;
}
.d3-view canvas {
  display: block;
}
.d3-view:active {
  cursor: grabbing;
}
.design-preview {
  position: absolute;
  z-index: 27;
  left: 14px;
  right: 14px;
  color: var(--ink);
  background: color-mix(in srgb, var(--surface) 94%, transparent);
  border: 1px solid var(--line-2);
  box-shadow: 0 6px 22px rgba(0, 0, 0, 0.14);
  backdrop-filter: blur(10px);
}

.design-preview button {
  border: 1px solid var(--line-2);
  color: var(--ink-2);
  background: var(--surface);
  border-radius: 999px;
  padding: 6px 10px;
  font: inherit;
  cursor: pointer;
}
.design-preview {
  top: 84px;
  border-radius: 15px;
  padding: 10px 12px;
}
.design-preview-head {
  display: flex;
  gap: 8px;
  align-items: baseline;
  font-size: 12px;
  line-height: 1.45;
}
.design-preview-head span {
  color: var(--ink-2);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.design-preview-actions {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 8px;
  overflow-x: auto;
  scrollbar-width: none;
}
.design-preview-actions::-webkit-scrollbar {
  display: none;
}
.preview-toggle {
  display: flex;
  flex: 0 0 auto;
  padding: 2px;
  border-radius: 999px;
  background: var(--bg-3);
}
.preview-toggle button {
  border: 0;
  background: transparent;
}
.preview-toggle button.active,
.design-preview button.primary {
  color: #fff;
  border-color: var(--accent);
  background: var(--accent);
}
.design-preview-actions > button {
  flex: 0 0 auto;
}
.d3-stylebar {
  position: absolute;
  top: 14px;
  left: 0;
  right: 0;
  display: flex;
  justify-content: center;
  gap: 8px;
  flex-wrap: wrap;
  padding: 0 16px;
  z-index: 6;
}
.style-chip {
  cursor: pointer;
  border: 1px solid var(--line-3);
  background: var(--surface);
  border-radius: 999px;
  padding: 6px 14px;
  font: inherit;
  font-size: 12.5px;
  color: var(--ink-2);
}
.style-chip.active {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
  font-weight: 600;
}
.style-chip:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.mascot-dot {
  position: absolute;
  top: 2px;
  right: -2px;
  width: 13px;
  height: 13px;
  border-radius: 50%;
  background: var(--danger);
  border: 2px solid #fff;
}

/* ============ 半屏聊天 ============ */
.sheet-mask {
  position: absolute;
  inset: 0;
  background: var(--mask);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.25s;
  z-index: 20;
}
.sheet-mask.show {
  opacity: 1;
  pointer-events: auto;
}
.chat-overlay {
  position: absolute;
  left: 8px;
  right: 8px;
  bottom: 8px;
  height: 56%;
  z-index: 26;
  background: var(--bg);
  border-radius: 18px;
  display: flex;
  flex-direction: column;
  transform: translateY(calc(100% + 16px));
  transition:
    transform 0.28s cubic-bezier(0.2, 0.7, 0.3, 1),
    box-shadow 0.28s ease;
  overflow: hidden;
}
.chat-overlay.show {
  transform: translateY(0);
}
.co-head {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 10px;
  padding: 12px 16px 10px;
  background: var(--surface);
  border-bottom: 1px solid var(--line);
}
.co-close {
  border: 2px solid var(--accent);
  background: var(--surface);
  color: var(--accent);
  width: 32px;
  height: 32px;
  border-radius: 50%;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  font-family: inherit;
}
.co-close svg {
  width: 18px;
  height: 18px;
  display: block;
}
.co-close:focus-visible {
  outline: 2px solid var(--accent);
}
.co-body {
  flex: 1 1 auto;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  padding-top: 10px;
}
.chat-box {
  flex: 1 1 auto;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 4px 16px 10px;
  scrollbar-width: none;
}
.chat-box::-webkit-scrollbar {
  display: none;
}
.bubble {
  max-width: 84%;
  border-radius: 14px;
  padding: 9px 12px;
  font-size: 12.8px;
  line-height: 1.55;
}
.msg-row {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  margin-bottom: 10px;
}
.msg-row.ai {
  justify-content: flex-start;
}
.msg-avatar {
  width: 30px;
  height: 30px;
  border-radius: 50%;
  object-fit: cover;
  object-position: 50% 15%;
  flex: 0 0 auto;
  background: var(--surface-2);
}
.msg-row .bubble {
  max-width: calc(84% - 38px);
}
.bubble.ai {
  align-self: flex-start;
  background: var(--surface);
  border: 1px solid var(--line);
  color: var(--ink);
  border-top-left-radius: 4px;
}
.bubble.user {
  align-self: flex-end;
  background: var(--accent);
  color: #fff;
  border-top-right-radius: 4px;
}
/* 对话里的面板入口（用户 2026-09-30）：与快捷回复**长得不一样**——那一行是"我刚说的这句
   可以怎么回"，随每轮换；这两颗是"随时能把那两张表调出来"，常驻。 */
.chat-tools {
  flex: 0 0 auto;
  display: flex;
  gap: 8px;
  padding: 0 16px 8px;
}
.chat-tool {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  box-sizing: border-box;
  min-height: 30px;
  padding: 0 12px;
  border: 1px solid var(--line-2);
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--ink-2);
  font: inherit;
  font-size: 12.5px;
  cursor: pointer;
}
.chat-tool svg {
  display: block;
  width: 15px;
  height: 15px;
}
.quick-replies {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  padding: 0 16px 6px;
}
.quick-reply {
  border: 1px solid var(--accent-soft);
  background: var(--accent-soft);
  color: var(--accent);
  font-size: 11.5px;
  border-radius: 999px;
  padding: 5px 11px;
  cursor: pointer;
  font-family: inherit;
}
.quick-reply:focus-visible {
  outline: 2px solid var(--accent);
}
.chat-input-row {
  display: flex;
  gap: 8px;
  padding: 10px 12px 16px;
  background: var(--surface);
  border-top: 1px solid var(--line);
}
.chat-input-row input {
  flex: 1;
  min-width: 0;
  border: 1px solid var(--line-2);
  border-radius: 12px;
  padding: 9px 12px;
  font: inherit;
  font-size: 12.8px;
  background: var(--surface);
  color: var(--ink);
}
.chat-input-row input::placeholder {
  color: var(--ph);
}
.chat-input-row input:focus-visible {
  outline: 2px solid var(--accent);
}
.chat-send {
  border: none;
  background: var(--accent);
  color: #fff;
  border-radius: 12px;
  padding: 0 16px;
  font: inherit;
  font-size: 13px;
  cursor: pointer;
}
.chat-send:focus-visible {
  outline: 3px solid #a9c9b5;
  outline-offset: 2px;
}
.prod-card {
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 8px 10px;
  margin-top: 8px;
  background: var(--surface-2);
}
.prod-head {
  font-size: 11px;
  color: var(--ink-2);
  margin-bottom: 4px;
}
.prod-row {
  display: flex;
  justify-content: space-between;
  font-size: 11.8px;
  color: var(--ink);
  padding: 3px 0;
}
.prod-row b {
  font-weight: 600;
  color: var(--amber);
}
.add-list-btn {
  margin-top: 8px;
  border: 1px solid var(--accent);
  background: var(--surface);
  color: var(--accent);
  border-radius: 999px;
  font-size: 11.5px;
  padding: 4px 14px;
  cursor: pointer;
  font-family: inherit;
}
.add-list-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* ============ 弹层与提示 ============ */
.sheet {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 28;
  background: var(--bg);
  border-radius: 20px 20px 0 0;
  transform: translateY(102%);
  transition: transform 0.28s cubic-bezier(0.2, 0.7, 0.3, 1);
  max-height: 78%;
  overflow-y: auto;
  padding: 8px 16px 22px;
  scrollbar-width: none;
}
.sheet::-webkit-scrollbar {
  display: none;
}
.sheet.show {
  transform: translateY(0);
}
.sheet-grip {
  width: 40px;
  height: 4px;
  border-radius: 999px;
  background: var(--line-2);
  margin: 0 auto 10px;
}
.sheet-close {
  position: absolute;
  right: 16px;
  top: 14px;
  border: none;
  background: var(--surface-3);
  color: var(--ink-2);
  width: 26px;
  height: 26px;
  border-radius: 50%;
  cursor: pointer;
  font-size: 13px;
  line-height: 26px;
}
.sheet-close:focus-visible {
  outline: 2px solid var(--accent);
}
.sheet-title {
  font-family: "Noto Serif SC", serif;
  font-weight: 700;
  font-size: 17px;
  color: var(--ink);
  padding-right: 34px;
}
.toast {
  position: absolute;
  bottom: 46px;
  left: 50%;
  transform: translateX(-50%) translateY(8px);
  background: rgba(38, 36, 31, 0.92);
  color: #fff;
  font-size: 12.5px;
  border-radius: 999px;
  padding: 7px 16px;
  opacity: 0;
  pointer-events: none;
  transition: all 0.25s;
  z-index: 30;
  /* ⚠⚠ 2026-10-12：原来是 `white-space: nowrap` ✗ —— 它是**单行药丸**的设计 ✓，
     可一旦哪句通报长了（用户报的「Toast 内容过长未自动换行，导致显示不全」✗），
     就整句溢出、看不见 ✗。改成**允许多行 ＋ 封顶宽度**：短句仍是原来那颗药丸 ✓，
     长句自己折行、居中 ✓（`border-radius: 999px` 两行时是一颗圆头胶囊 ✓，不难看 ✓）。
     ⚠ 话本身也别太长 —— toast 是"喊一声"，不是"讲一段" ✓。 */
  white-space: normal;
  /* ⚠⚠ `width: max-content` **必须写**（实测踩到，和页脚那条是同一个坑 ✗）：
     这颗是 `position:absolute; left:50%` ⇒ 它的**收缩到合适**宽度只从"中间到右边缘"
     那么多（实测 195px ✗）⇒ 长句被折成五行的窄柱 ✗。按内容撑开、再由 `max-width` 封顶，
     才是"短句一颗药丸、长句两行居中" ✓。 */
  width: max-content;
  max-width: min(88%, 420px);
  box-sizing: border-box;
  text-align: center;
}
.toast.show {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}

/* ============ 移动端：全屏 app，无外框 ============ */
@media (max-width: 860px) {
  .shell {
    padding: 0;
  }
  .app {
    max-width: none;
    width: 100%;
    height: 100vh;
    height: 100dvh;
    border: none;
    border-radius: 0;
    box-shadow: none;
  }
  .chat-input-row {
    padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 16px);
  }
}

/* Snackbar（M3）：近满宽、单行、4dp 圆角、可带一个动作。两种内容共用它——
   常驻的（地面素材加载失败，带「重试」）与瞬时的（点了一个还进不去的节点，带「去定图」），
   见 ui/snackbar.js。加载中不在这里——那是被动信息，由叶知秋自己说（renderDesignerBubble）。
   底部 120px 避开右下角 FAB（68px）和左下角叶知秋（约 120px 高）；
   46px 那条轨道留给 .toast——它更轻、不带动作，两条轨道各司其职。 */
.snackbar {
  position: absolute;
  bottom: 120px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 31; /* 高于 .toast 的 30 */
  display: flex;
  align-items: center;
  gap: 8px;
  box-sizing: border-box;
  width: calc(100% - 16px); /* 两侧各留 8px */
  max-width: 640px; /* M3 大屏建议上限 */
  min-height: 48px; /* M3 单行高度 */
  padding: 8px 8px 8px 16px; /* 起始 16，末端留给动作按钮 8 */
  border-radius: 4px; /* M3 snackbar 圆角 */
  background: rgba(38, 36, 31, 0.94);
  color: #fff;
  font-size: 14px; /* M3 body-medium */
  line-height: 20px;
  box-shadow: 0 4px 12px #0002;
  /* 元素靠 hidden 进出渲染树，所以显示时这条动画会从头播 */
  animation: material-snack-in 0.24s cubic-bezier(0.2, 0.9, 0.3, 1.1);
}
@keyframes material-snack-in {
  from {
    opacity: 0;
    transform: translateX(-50%) translateY(16px);
  }
  to {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
  }
}
/* 动作按钮：M3 文本按钮——无边框、强调色、右对齐 */
.snackbar-action {
  flex: none;
  margin-left: auto;
  min-height: 36px;
  padding: 6px 12px;
  font: inherit;
  font-weight: 600;
  border: 0;
  border-radius: 4px;
  background: none;
  color: #e8a86a;
  cursor: pointer;
}
.snackbar-action:focus-visible {
  outline: 2px solid #e8a86a;
  outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
  .snackbar {
    animation: none;
  }
}
.material-note {
  font-size: 12px;
  color: var(--ink-2);
  line-height: 1.6;
}
.material-category + .material-category {
  margin-top: 16px;
}
.material-category-title {
  margin-bottom: 8px;
  font-size: 14px;
  color: var(--ink);
}
.material-category .material-grid {
  grid-template-columns: minmax(0, 1fr);
}
.material-scope {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 16px 0;
}
.material-scope select {
  font: inherit;
  padding: 8px;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 8px;
}
.material-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
  margin-bottom: 12px;
}
.material-card {
  display: flex;
  flex-direction: column;
  gap: 6px;
  align-items: center;
  padding: 8px;
  font: inherit;
  font-size: 13px;
  color: var(--ink);
  border: 2px solid transparent;
  background: var(--surface);
  border-radius: 12px;
  cursor: pointer;
}
.material-card img {
  width: 100%;
  height: 100px;
  object-fit: cover;
  border-radius: 8px;
}
.material-card small {
  color: var(--ink-2);
  font-size: 12px;
}
.material-card[aria-pressed="true"] {
  border-color: var(--accent);
  background: var(--accent-soft);
}
.material-card:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

#sheet-mask {
  z-index: 27;
}

.opening-form {
  display: grid;
  gap: 12px;
}
/* ⚠ 说明在左、控件在右（用户 2026-10-05 第七十一条：「输入框/下拉菜单在属性名右侧、
   不要另起一行」）——原来是 `grid`（说明一行、控件一行 ✗）。左列与上面那三条**等宽**
   （`min-width: 5em`，由最长的「中心距区域起点」…不，那一条在折里也够宽 ✓ 取 5em 即可 ✓）。 */
.opening-form label {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 13px;
}
.opening-form label > .section-label {
  flex: 0 0 auto;
  min-width: 5em;
}
.opening-form label > input,
.opening-form label > select {
  flex: 1 1 auto;
  width: auto;
  min-width: 0;
}
/* ⚠ **洞口净宽那颗芯片要排除在外**（用户 2026-10-05 第六十四条要的就是"芯片"）：这一条的
   特异性是 (0,2,1)（元素 ＋ 两个类/属性），**压得过** `.cf-dim-input` 的 (0,1,0) ⇒
   它把那个盒子抻成了整行宽、字号也顶成 16px ✗（实测：用户看到的是一整条白框，
   不是芯片 ✗）。排除它之后，那颗输入框才真正穿上 `.cf-dim-input` 那身盒子 ✓
   （宽度 5 位数的药丸、与比例尺那一行的两个数同一套 ✓）。 */
.opening-form input:not([type="checkbox"]):not(.cf-dim-input),
.opening-form select {
  width: 100%;
  min-width: 0;
  padding: 10px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: 16px;
}
/* ⚠ **一栏**（用户 2026-10-05 第七十条：「所属房间、所在墙段、中心距区域起点、洞口净高
   等属性各成一行」）：原来是 `1fr 1fr` 两栏 ⇒ 两个字段挤在一行上 ✗。 */
.opening-dimensions {
  display: grid;
  grid-template-columns: 1fr;
  gap: 12px;
}
.opening-bay {
  display: grid;
  gap: 12px;
}
.opening-form .opening-measured {
  display: flex;
  align-items: center;
}
.opening-actions {
  display: flex;
  gap: 10px;
}
.opening-actions button {
  flex: 1;
  padding: 12px;
  border: 0;
  border-radius: 12px;
  background: var(--accent);
  color: white;
  font: inherit;
}
.opening-actions .secondary {
  background: var(--surface);
  color: var(--ink);
  border: 1px solid var(--line);
}
/* （2026-10-13 删掉了一整套 **量房表** 的样式：`.dim-field` / `.dim-note` / `.dim-echo` /
   `.dim-help` / `.dim-rooms*` / `.dim-room*` / `.dim-side*` ＋ 那几条 `#plan-dimensions-form …`。
   那张表整个删了（用户：「量房表主要用于总览，但感觉意义不大」✓）——它里面只剩三个全屋数
   （搬进账本表尾那一行 ✓）与每间每段的**只读**明细（账本那张表 ＋ 向导早就在说 ✓）。
   ⚠ 删得干净是**逐类查过**的：这一族只有 `ui/plan-dimensions.js` 用（HTML 里一个都没有 ✓）——
   ⚠ 除了 `.cf-dim-label` / `.cf-dim-value` / `.cf-dim-input` 那三条**另有用处**
   （账本表尾那一行与向导那枚尺寸芯片 ✓，它们不在这一族里 ✓）。） */

.opening-error {
  color: #a5392a;
  font-size: 13px;
  margin: 0;
}
.opening-list {
  display: grid;
  gap: 8px;
  margin: 12px 0;
}
.opening-list button {
  padding: 12px;
  text-align: left;
  border: 1px solid var(--line);
  border-radius: 12px;
  color: var(--ink);
  background: var(--surface);
  font: inherit;
}
.opening-list small {
  display: block;
  color: var(--ink-2);
  margin-top: 4px;
}

.color-filters {
  display: flex;
  gap: 8px;
  margin: 12px 0;
}
.color-filters input,
.color-filters select {
  min-width: 0;
  padding: 10px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--surface);
  color: var(--ink);
  font: inherit;
}
.color-filters input {
  flex: 1;
  width: 55%;
}
.color-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 8px;
}
.color-card {
  display: flex;
  flex-direction: column;
  gap: 4px;
  align-items: center;
  padding: 5px 3px 8px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}
.color-chip {
  display: block;
  width: 100%;
  height: 54px;
  border-radius: 6px;
  border: 1px solid rgba(0, 0, 0, 0.06);
}
.color-card small {
  font-size: 10px;
}
.color-rgb {
  color: var(--ink-2);
}
.color-card[aria-pressed="true"] {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}
.color-card:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

#wood-pattern-preview,
#material-skirting,
#material-doors,
#skirting-preview {
  padding: 10px 14px;
  border: 1px solid var(--line);
  border-radius: 10px;
  color: var(--ink);
  background: var(--surface);
  font: inherit;
  cursor: pointer;
}
#skirting-preview {
  width: 100%;
}
#skirting-height,
#skirting-thickness {
  width: 100px;
  padding: 8px;
  border: 1px solid var(--line);
  border-radius: 8px;
  color: var(--ink);
  background: var(--surface);
  font: inherit;
}
#skirting-card {
  min-width: 0;
  max-width: 240px;
}
#skirting-color {
  width: 42px;
  flex: 0 0 42px;
}
#skirting-error {
  color: #a23a2a;
}

/* 漫游那颗的形态（位置、圆钮长相）全在 .fab-float 里。
   ⚠ **它不换图标**（用户 2026-09-30）：进出漫游只有**一颗脚印**，状态由底色说——
   `.fab-float[aria-pressed="true"]` 那条（accent 底 + 白图标）已经在做这件事。
   这里从前还有三条 `.view-icon-plan` 的规则在两种状态间换图标，一并撤了。 */
.walk-controls {
  position: absolute;
  z-index: 26;
  left: 50%;
  bottom: 24px;
  transform: translateX(-50%);
  width: 156px;
  height: 156px;
  border-radius: 50%;
  border: 1px solid var(--line);
  background: color-mix(in srgb, var(--surface) 82%, transparent);
  box-shadow:
    inset 0 0 20px #0001,
    0 4px 18px #0002;
  touch-action: none;
  user-select: none;
  cursor: grab;
  --stick-x: 0px;
  --stick-y: 0px;
}
.walk-controls:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}
.walk-controls::before {
  content: "";
  position: absolute;
  inset: 17px;
  border: 1px solid var(--line);
  border-radius: 50%;
  opacity: 0.45;
  pointer-events: none;
}
.walk-stick {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 50px;
  height: 50px;
  border-radius: 50%;
  transform: translate(
    calc(-50% + var(--stick-x)),
    calc(-50% + var(--stick-y))
  );
  background: var(--surface);
  border: 1px solid var(--line-2);
  box-shadow:
    0 5px 14px #0003,
    inset 0 1px 2px #fff8;
  transition:
    transform 0.12s ease-out,
    background 0.12s;
  pointer-events: none;
}
.walk-controls.dragging {
  cursor: grabbing;
}
.walk-controls.dragging .walk-stick {
  transition: none;
  background: color-mix(in srgb, var(--accent) 15%, var(--surface));
  border-color: var(--accent);
}

.journey-home {
  position: absolute;
  inset: 0;
  /* 地图是**最底下那层**，子页盖在它上面。
     原来是 z-index:20——比所有子页都高（#confirm/#recognize 是 7、#design-chat 是 5），
     于是「子页滑上来盖住地图」根本盖不住：子页确实在滑，只是全程被地图挡着，要等地图
     到点隐藏才猛地露出来。以前两层反向移动时看不出来，因为地图同时在往右走，两者错开；
     换成「地图不动」之后立刻现形。

     必须是 0，不能是正数：任何正数都会把子页压住；0 与 auto 同层，谁在上面由 DOM
     顺序决定（子页在 #intro 之后）。 */
  z-index: 0;
  display: flex;
  flex-direction: column;
  background: var(--journey-map-bg);
  color: #454d3b;
}



.journey-home button {
  font: inherit;
  cursor: pointer;
}













.journey-scroll {
  flex: 1;
  min-height: 0;
  overflow: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
}
.journey-map {
  position: relative;
  width: 100%;
}
.journey-map-image {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 941/1672;
}
.journey-map ol {
  list-style: none;
  margin: 0;
  padding: 0;
}
.journey-map li {
  position: absolute;
  transform: translate(-50%, -50%);
}
.journey-node {
  border: 0;
  background: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  color: #4c5b40;
  width: 112px;
}
.journey-node-circle {
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  border: 2px solid #667158;
  border-radius: 50%;
  background: #faf7eb;
  font-size: 22px;
  font-weight: 650;
  box-shadow: 0 2px 6px #58452b18;
}
.journey-node.done .journey-node-circle {
  background: #758464;
  color: white;
  border-color: #fff;
}
.journey-node.current .journey-node-circle {
  background: var(--node);
  color: white;
  border-color: #fff;
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--node) 16%, transparent);
}

.journey-node:focus-visible { outline: none; }
.journey-home .journey-node:focus-visible .journey-node-circle {
  outline: 2px solid var(--node);
  outline-offset: 6px;
}





.brief-dialogue {
  display: flex;
  gap: 10px;
  align-items: center;
  margin-bottom: 16px;
}
.brief-dialogue img {
  width: 42px;
  height: 42px;
  border-radius: 50%;
}
.brief-dialogue p {
  background: var(--bg);
  border-radius: 12px;
  padding: 12px;
  font-size: 15px;
}
.opening-form textarea {
  width: 100%;
  box-sizing: border-box;
  resize: vertical;
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 10px;
  font: inherit;
  color: var(--ink);
  background: var(--surface);
}


/* 文字位于3D层面；DOM仅提供收起及无障碍操作。 */
/* ⚠ `top` 从 104 挪到 128（用户 2026-10-01）：标题栏下方右边那一格现在是**漫游钮**
   （40px 圆钮，落在 70~110 之间），这条点选层面的浮条原本占着 104 起，会压住它。 */
#wall-layer-controls { position:fixed; z-index:35; top:128px; right:12px; display:flex; align-items:center; gap:8px; color:#4c5843; font-size:11px; }
#wall-layer-controls[hidden] { display:none; }
#wall-layer-controls > span { padding:5px 8px; border-radius:20px; background:#faf6eee6; }
#wall-layer-controls > button { border:1px solid #d6cdbd; background:#faf6ee; border-radius:24px; padding:12px 16px; color:inherit; }
.wall-layer-accessibility { position:absolute; width:1px; height:1px; overflow:hidden; clip-path:inset(50%); }
.wall-layer-accessibility:focus-within { width:220px; height:auto; clip-path:none; top:48px; right:0; background:#faf6ee; padding:8px; border-radius:10px; }
.wall-layer-accessibility button { padding:10px; }
.wall-layer-hint { position:absolute; top:66px; left:50%; transform:translateX(-50%); white-space:nowrap; color:#72786b; background:#faf8f0ce; padding:5px 11px; border-radius:20px; font-size:11px; pointer-events:none; }


/* ⚠ 这里从前还有一条 `#journey-back { width:42px; height:42px; padding:0; border-radius:50%;
   font-size:23px }` —— 同一个按钮的尺寸与形状**写了两遍**（另一处是 `.nav-back` 与下面那条
   `#journey-back` 的定位）。2026-09-29 用户要求"去掉圆形、只留箭头"时，改了一处、这一处
   还在，于是**地图那颗返回仍然是圆的**（新加的用例当场抓到：其余三个 0px，它 50%）。
   `font-size:23px` 是更早那版"文字箭头"的遗物，图标是 SVG，用不上。
   规矩：**后退入口只有一个出处，就是 `.nav-back`**。 */
.journey-map { min-height:1080px; isolation:isolate; }
.journey-home[data-level="phases"] .journey-map { min-height:820px; }
.journey-map-image { position:absolute; inset:0; height:100%; object-fit:cover; opacity:.65; z-index:-2; }
.journey-route { position:absolute; inset:0; width:100%; height:100%; pointer-events:none; z-index:-1; overflow:visible; }
/* 路本身不再画出来，只留作几何：脚印从这条中心线上取点。注意是 `stroke:none` 而不是把
   path 删掉——`getTotalLength` / `getPointAtLength` 要它在渲染树里。
   用户 2026-09-30：「路径虚线能不能改成脚印？这样可以看出从设计到施工再到入住的顺序」——
   虚线只说得清「这儿有条路」，脚印自带方向。 */
.journey-route path { fill:none; stroke:none; }
/* 脚印：一只一只踩在中心线上，左右交替、脚尖朝着走的方向。位置与旋转由
   domain/journey-route.js 算好写在行内（各向异性的拉伸在那边先换算掉了），
   这里只管一只多大、什么颜色。 */
.journey-trail { position:absolute; inset:0; pointer-events:none; z-index:-1; overflow:hidden; }
.journey-print { position:absolute; display:block; width:11px; height:22px; color:#ad9670; opacity:.9; }
.journey-print svg { display:block; width:100%; height:100%; }
.journey-node { width:132px; }
.journey-node-circle { position:relative; }
.journey-node.current .journey-node-circle { box-shadow:0 2px 6px #58452b18; }



.journey-traveler { position:absolute; width:68px; height:100px; transform:translate(-50%,-100%); pointer-events:none; transition:left .6s ease-in-out,top .6s ease-in-out; z-index:2;}
.journey-traveler img { width:100%; height:100%; object-fit:contain; filter:drop-shadow(0 5px 3px #76684b33); }
.journey-traveler.moving img { animation:journey-hop .22s ease-in-out infinite alternate; }
@keyframes journey-hop { to { transform:translateY(-5px) rotate(2deg); } }
.journey-bubble { position:absolute; box-sizing:border-box; width:290px; max-width:calc(100% - 24px); padding:13px 15px; border:1px solid #ded3bf; border-radius:18px; background:#fffdf7f5; box-shadow:0 5px 20px #715d431a; z-index:3; transition:left .6s ease-in-out,bottom .6s ease-in-out;}
.journey-bubble::after { content:""; position:absolute; bottom:-8px; left:50%; width:14px; height:14px; background:#fffdf7; border-right:1px solid #ded3bf; border-bottom:1px solid #ded3bf; transform:rotate(45deg); }
.journey-bubble > small { font-size:11px; color:#83785e; }
/* ⚠ 字号 / 行距**对齐户型页那颗气泡**（`.guide-bubble`：13px / 1.55 ✓ —— 用户 2026-10-18
   「字号对齐户型图页」✓）。它现在是气泡里**唯一**的东西（那个阶段名的 `<h2>` 撤了 ✓）。 */
.journey-bubble p { font-size:13px; line-height:1.55; margin:0 0 10px; }
/* 气泡里的正文是**最后一项**（下面没有动作按钮 ✓）⇒ 别留那 10px 死白 ✓。 */
.journey-bubble p:last-child { margin-bottom:0; }

@media (prefers-reduced-motion:reduce) {
 .journey-traveler,.journey-bubble { transition:none; }
 .journey-traveler.moving img { animation:none; }
}
.journey-bubble { pointer-events:none; }
.journey-bubble button { pointer-events:auto; }
.journey-map { min-height:1200px; }
.journey-home[data-level="phases"] .journey-map { min-height:960px; }
.journey-bubble { width:min(220px,56%);}



/* 整幅地图等比缩放至可用视口，不产生页面滚动。 */
.journey-scroll { position:relative; overflow:hidden; }
.journey-home .journey-map, .journey-home[data-level="phases"] .journey-map { position:absolute; top:0; width:390px; height:960px; min-height:0; transform-origin:top left; }
.journey-traveler { height:72px; width:52px; transition:none;}
.journey-bubble { transition:none;}
.journey-map-image { object-fit:fill; }

/* 节点直接以名称标识，状态仍由颜色、描边和无障碍文案表达。 */
.journey-node { position:relative; width:88px; }
.journey-node-circle { box-sizing:border-box; width:76px; height:76px; padding:8px; font-size:13px; line-height:1.35; text-align:center; word-break:break-all; }
.journey-node-circle > span { max-width:100%; }
.journey-node-status { position:absolute; top:80px; font-size:10px; color:#827963; background:#fffcf1e6; padding:2px 5px; border-radius:6px; white-space:nowrap; }

/* 叶知秋的指令面板：一张表驱动，界面与将来的语音读同一份。 */
.assistant-commands { list-style:none; margin:10px 0 0; padding:0; display:flex; flex-direction:column; gap:8px; }
.assistant-commands button { width:100%; box-sizing:border-box; display:flex; align-items:center; gap:12px; padding:12px 14px; min-height:56px; text-align:left; border:1px solid #ded3bf; border-radius:14px; background:#fffdf6; color:#43563b; cursor:pointer; }
.assistant-command-icon { flex:none; width:20px; height:20px; color:#8a6a3d; }
.assistant-command-icon svg { display:block; width:20px; height:20px; }
.assistant-command-text { display:flex; flex-direction:column; gap:2px; min-width:0; }
.assistant-command-text strong { font-size:15px; font-weight:500; }
.assistant-command-text small { font-size:12px; color:#83785e; }
.journey-bubble::after { margin-left:-7px;}

/* 两级地图节点统一暖橙填色，选中状态由外圈表达。 */
.journey-home .journey-node .journey-node-circle { background:var(--node); color:#fff; border-color:#fff; }

/* 全屏地图上叠放半透明标题栏。 */
.journey-home .journey-scroll { position:absolute; inset:0; }


@media (max-height:450px) and (orientation:landscape) {
 .journey-node-circle { width:56px; height:56px; font-size:11px; line-height:1.1; }
 .journey-node-status { top:60px; }
}

.journey-bubble { width:max-content; max-width:min(220px,56%); padding:10px 16px; text-align:center; }

/* 空间不足时调整气泡位置，不缩小正文。 */
.journey-bubble { transform:none;}

/* 不设标题栏，返回按钮独立悬浮于地图左上角。 */
/* ⚠ 这一条留着只是因为**位置**（`position:absolute` 那几项）：形状与尺寸都归下面的
   `.nav-back` 管，两处不许各写一份。 */
/* ⚠ 这里原来挂着"不设标题栏、返回按钮独立悬浮在地图左上角"那一条
   （`position:absolute; top:…; left:16px; z-index:6`）—— **整条都是死代码**：下面
   `.page-bar #journey-back { position:static }` 的特异性更高（0,1,1,0 > 0,1,0,0），
   永远赢。它是"返回曾经独立浮在地图上"那版留下的，现在那颗按钮**就在栏里**，
   位置由栏的内边距说了算（14px），而形状尺寸归 `.nav-back`。删掉，别留第二处真相。 */

/* 所有后退入口共用**一个没有框的箭头**。判据是 `.nav-back` **这个类**，不是一张手维护的
   页面名单。
   ⚠ 2026-09-29 之前那张名单写的是 `#journey-back, #calib-back, #design-back`——施工图
   那一页 9-27 加进来时没人想起它，于是**只有它的返回按钮退回了默认的圆角方形**（同一天
   同一个毛病第三次：新加的一页忘了加入一张既有的名单）。类是所有后退入口本来就共用的
   东西，用不着再列一遍。
   ⚠ 2026-09-29 用户又拍了第三刀：「**去掉圆形样式、仅保留 icon**」。于是描边、底色、
   投影、圆角全部撤掉——**只留那颗箭头**。盒子仍是 `--bar-tap`（42px）且**不许缩小**：
   它是**触点**不是装饰，20px 的图标配 20px 的盒子谁也点不着；而"看不见的盒子"正是
   这一条要的效果。 */
.nav-back {
  box-sizing:border-box; width:var(--bar-tap); height:var(--bar-tap); min-width:var(--bar-tap);
  padding:0; display:grid; place-items:center;
  border:0; background:none; color:#454d3b;
  box-shadow:none; cursor:pointer;
}
.nav-back svg { display:block; width:var(--bar-icon); height:var(--bar-icon); }
.nav-back:focus-visible { outline:2px solid var(--accent); outline-offset:3px; }

/* 聊天与通用底部弹层共用外边距、圆角和隐藏距离。 */
.app { --bar-pad:max(10px, env(safe-area-inset-top)); --bar-icon:20px; --bar-tap:42px; /* 右下角那列浮动圆钮的位置公式（见 .fab-float）：右距、底距、以及「一颗 + 一道缝」 */ --fab-right:20px; --fab-bottom:28px; --fab-step:56px; --sheet-edge:8px; --sheet-bottom:max(8px,env(safe-area-inset-bottom,0px)); --sheet-radius:18px; }
.chat-overlay, .sheet {
  left:var(--sheet-edge); right:var(--sheet-edge); bottom:var(--sheet-bottom);
  border-radius:var(--sheet-radius);
  transform:translateY(calc(100% + var(--sheet-bottom) + 8px));
}
.sheet { max-height:min(78%,calc(100% - var(--sheet-bottom) - 8px)); }
/* 阴影只在展开时画。收起时弹层被 transform 推到屏幕外，但 **transform 不裁剪阴影**，
   于是那条向上的 0 -8px 32px 会在整页底部漏出约 26px 的一条渐变。
   顺带修掉「阴影先于弹层出现」——现在两者同步。 */
.chat-overlay.show, .sheet.show {
  transform:translateY(0);
  box-shadow:0 -8px 32px rgba(0,0,0,.22);
}

/* 引导→地图→详情；地图设计师独立悬浮，不再绑定节点。 */
#journey-designer { position:absolute; z-index:8; width:72px; height:110px; left:16px; bottom:auto; top:calc(100% - 134px); }
#journey-designer #journey-traveler { position:relative; left:0; top:0; width:100%; height:100%; transform:none; padding:0; border:0; background:transparent; pointer-events:auto; touch-action:none; cursor:grab; user-select:none; }
#journey-designer #journey-traveler.dragging { cursor:grabbing; }
#journey-designer #journey-traveler img { pointer-events:none; }
/* ⚠⚠ **撑满屏幕宽**（用户 2026-10-18：「气泡宽度对齐户型页：撑满屏幕宽度」✓）：
   原来封在 220px ✗ —— 户型页那颗是"贴着两条内边距"（`--bubble-max` ✓，2026-10-09 那句
   「拉大宽度撑满屏幕」✓）⇒ 两颗用同一套宽度 ✓。宽度由 `floating-designer.js` 的 `place()`
   按容器宽度**量出来**写进 `--floating-bubble-w` ✓（不写死屏幕宽 ✗：桌面窗口里 `.app`
   到 430px 就封顶了 ✓）；**落位与尾巴不用在这儿算** —— 那边把它夹进 12px 内边距、
   尾巴（`--floating-tail` ✓）跟着形象的头 ✓（与 `placeGuideBubble` 同一套算法 ✓）。 */
#journey-designer .journey-bubble { left:0; bottom:calc(100% + 12px); width:var(--floating-bubble-w, calc(100vw - 24px)); max-width:none; transform:none; text-align:center; }
#journey-designer .journey-bubble::after { left:var(--floating-tail,36px); }
.journey-home .journey-node-circle > span { visibility:visible; }
#journey-welcome { position:absolute; inset:0; z-index:7; background:linear-gradient(#f8f3e8a6,#f8f3e8e6),url("assets/community.webp") center/cover; color:#454d3b; padding:80px 22px 150px; display:flex; flex-direction:column; gap:20px; }
#journey-welcome header h1 { font-size:25px; max-width:280px; line-height:1.5; margin:8px 0; }
#journey-welcome header > p { font-size:12px; letter-spacing:3px; }
#journey-welcome header small { font-size:12px; line-height:1.6; }
/* 「跳过」：就是原来那支箭头——圆形、`›`、轻微左右滑动、右下角。位置与尺寸照旧，
   只是含义从「轻触回应」变成了**出口**（对话现在自己往下播，不需要点）。
   与脉冲动效无关：那圈会呼吸的涟漪问的是"你愿不愿意听"，这支箭头只负责"不想看就走"。 */
#onboarding-skip { position:absolute; right:20px; bottom:28px; width:44px; height:44px; display:flex; align-items:center; justify-content:center; border-radius:50%; border:2px solid rgba(74,59,50,.45); background:none; color:rgba(74,59,50,.7); font:inherit; font-size:26px; line-height:1; cursor:pointer; }
/* 指到它就停住：一个一直在滑的点击目标不好瞄准（移动端没有 hover，靠"点一下"本来也不需要静止；
   桌面与自动化都靠这条才点得准）。 */
/* 动效：**圈不动、箭头在圈里滑**。整圈一起滑的话点击目标一直在动——瞄不准、点不到，
   自动化同样点不到（Playwright 的稳定性检查发生在鼠标移上去之前，hover 冻结救不了）。
   视觉上几乎一样：滑的还是那支箭头，只是外圈成了静止的准心。 */
/* 墨迹不是字身框：flex 居中居的是 26px 的字身框（上下各 9px，分毫不差），而 `»` 的墨
   落在框里靠下的位置。实测（canvas 的 actualBoundingBox，3 倍像素截图复核）：墨迹中心在
   15.83px、字身框中心在 13px——差 2.83px。这里按实测值提上来，眼睛看到的才是居中。 */
/* 用 top 而不是 transform：**动画占着 transform**（hintArrow 的关键帧写的就是
   transform: translateX），静态的 translateY 会在动画播放期间被整个覆盖掉——
   实测那条位移从来没生效过（量出来的偏差依旧是 2.83px）。top 是布局量，互不争。 */
.intro-skip-glyph { display:block; position:relative; top:-2.8px; animation:hintArrow 1.4s ease-in-out infinite; }
#onboarding-skip:hover .intro-skip-glyph,
#onboarding-skip:focus-visible .intro-skip-glyph { animation:none; }
@keyframes hintArrow {
  0%, 100% { transform:translateX(0); opacity:.65; }
  50% { transform:translateX(5px); opacity:1; }
}
#onboarding-dialogue { min-height:0; overflow:auto; flex:1; display:flex; flex-direction:column; gap:12px; padding:4px; }
.onboarding-line { display:flex; flex-direction:column; align-items:flex-start; gap:3px; }
.onboarding-line small { font-size:10px; color:#817866; }
.onboarding-line span { padding:10px 14px; background:#fffcf3e8; border-radius:16px; font-size:15px; line-height:1.6; max-width:88%; }
.onboarding-line.me { align-items:flex-end; }
.onboarding-line.me span { background:#e4e8d9eb; }
#onboarding-next { position:absolute; right:24px; bottom:40px; border:0; border-radius:24px; padding:12px 24px; background:#657654; color:#fff; min-height:44px; }
.journey-home[data-screen="welcome"] > #journey-scroll,.journey-home[data-screen="welcome"] > #journey-back,.journey-home[data-screen="welcome"] > #journey-shell,.journey-home[data-screen="welcome"] > #journey-trades,.journey-home[data-screen="welcome"] #journey-bubble { visibility:hidden; pointer-events:none; }
@media(max-height:550px) { #journey-welcome { padding-top:55px; padding-bottom:95px; gap:8px; } #journey-welcome header h1 { font-size:19px; margin:0; } #journey-welcome header small { display:none; } }

/* 恢复原版引导：社区背景、大形象、上方渐隐对话流与轻触提示。 */
#journey-welcome.intro { z-index:7; display:flex; flex-direction:column; align-items:center; justify-content:center; padding:24px 20px 56px; gap:0; background:var(--intro-fallback); color:var(--ink); outline:none; }
#journey-welcome #onboarding-skip { z-index:4; }
.journey-home[data-screen="welcome"] #journey-designer { visibility:hidden; pointer-events:none; }
/* 「设计师辅助」关掉时，设计师（连同她的气泡）**哪一页都不出现**。
   状态挂在 `<html>` 的 data-ai-assist 上（与 data-theme 同一套做法：样式只认属性，
   于是它不靠哪个 JS 模块去动 DOM ✓）。
   ⚠ 两处形象是两个元素，都要盖住：地图上那位是 `#journey-designer`（施工列表页也在它里面），
   入口/识别/户型图/效果图那几页共用的是 index.html 里那位 `#guide`（用户 2026-10-11：
   「不仅地图页，户型页/设计页等都不展示」）。
   ⚠ 用 visibility 而不是 display：floating-designer.js 要量她的尺寸来摆气泡
   （与上面"欢迎页"那条同一个理由）；`pointer-events:none` 顺带让那一块**算空白** ——
   点下去正是"点空白"，该收的那一圈照收 ✓。
   ⚠ **开场引导里那位不算**（`.intro-avatar`＋对话流）：那段引导本身就是她在说话，
   藏了她只剩一块空屏；它也只在你还没设过任何偏好时放一次。 */
:root[data-ai-assist="off"] #journey-designer,
:root[data-ai-assist="off"] #guide { visibility:hidden; pointer-events:none; }
.journey-home:focus { outline:none; }

/* 被搬到 .app 层的设置面板与地图标题栏：两者都靠 JS 显隐，hidden 就是唯一真相 */

/* ============ 地图标题栏：这是哪套房 ============
   左：logo + 房名（点开「我的房子」）；右：设置（外观/主题色/地面显示）。
   只在一级地图上露面——施工列表页的左上角是返回箭头（#journey-bar），同一格坐标不能
   同时站两个人。它跟着 #intro 一起隐藏/滑动，所以这里只负责"一级地图上长什么样"。 */
/* ⚠ 这里原来还写着 `gap:8px; padding-right:12px` —— **两条都是死代码**：`.page-bar` 的
   `gap:10px` 与 `padding:…14px…` 在这条**之后**（2520 行）同分覆盖了它们。实测右边距是
   14 而不是 12，正是这么露出来的。一律删掉：**边距与间距的出处只有 `.page-bar` 一处。** */
.house-bar { display:flex; align-items:center; }
.house-bar[hidden] { display:none; }
/* 左：logo + 房名，点它是开「我的房子」。
   ⚠ **logo 也住进一颗 `--bar-tap` 的盒子里**（用户 2026-09-29：「地图页的 logo 和标题
   不统一」）。原因是那条"图标↔标题 10px"的规则在**两处的视觉结果不一样**：
   别页的图标在一颗 42px 盒子里居中（两侧各空 11px），墨迹因此离栏边 25、离标题 21；
   而 logo 原来直接贴着盒边，墨迹是 **14 / 10**——同一个 `gap` 量出两个距离。
   包上盒子之后两边都自动对上了，**不用补任何魔数**（11 与 21 都是这颗盒子算出来的）。
   竖向内边距同时归零：42px 的盒子 + 4px 内边距会把栏撑到 66px，比别页高——那正是刚修掉的病。 */
/* 标题栏里只剩这一颗（设置挪到右下角之后，用户 2026-09-30）：让它把整栏铺满，
   尾元素离右边就还是自己那份右内边距 14——不然它缩成内容宽，"尾巴"会飘在中间。
   ⚠ 它是个 `<button>`，**默认 `text-align:center`**：铺满之后房名那格跟着变宽，居中的字
   就被推到栏中间去了（用户 2026-09-30「样板间的位置偏了」）。所以显式靠左。 */
.house-bar-home { flex:1 1 auto; display:flex; align-items:center; gap:10px; min-width:0; font:inherit; color:inherit; background:none; border:0; padding:0; cursor:pointer; text-align:left; }
.house-bar-logo-box { flex:0 0 auto; width:var(--bar-tap); height:var(--bar-tap); display:grid; place-items:center; }
/* 图标尺寸**只有一个数**：`--bar-icon`（20px），在 `.app` 上定。落点是四样东西——
   返回箭头、打印、设置、以及那颗 **logo**（它原来是 22px，是全栏唯一不合群的）。
   改尺寸只改 `.app` 里那一个数，别在各处再写一遍像素。 */
.house-bar-logo { width:var(--bar-icon); height:var(--bar-icon); flex:0 0 auto; border-radius:6px; }
/* ⚠ 地图这栏的标题**不参与撑开**（其余四栏仍是 `flex:1`）：房名后面那枚 ⌄ 得贴着房名，
   撑开的话它会被顶到栏的最右边。这条是当年为避让右上角那颗「户型图为示例」水印定的
   （水印 2026-10-09 撤了，规矩没动——⌄ 贴着房名本来就是它该有的样子）。
   长名字照样收尾：`overflow` 与 `text-overflow` 在这条规则里，`min-width:0` 在基类里。 */
.house-bar .page-bar-title { flex:0 1 auto; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
/* ⌄：房名后面那枚"这儿能展开"的记号。尺寸就是 `--bar-icon`——它长在标题栏里，就是
   "标题栏里的图标"那一族，规格的出处只有 `.app` 上那一行（另写一个数还会当场撞上
   「标题栏上的图标一个尺寸」那条断言，实测踩到）。图形本身只占 viewBox 的一半宽，
   所以 20px 的盒子里是一枚 10px 宽的细箭头，不会顶到 17px 的房名。 */
.house-bar-caret { flex:0 0 auto; width:var(--bar-icon); height:var(--bar-icon); color:var(--ink-2); }
/* 按下去得有个交代（用户 2026-09-30 的 e）：`cursor:pointer` 在手机上根本不存在，
   第一次点它的人需要知道"这一下点着了"。整颗按钮横跨整条栏，所以不能给底色块
   （会盖满一栏），改成这一处内容整体变淡——它也只有这一处内容。 */
.house-bar-home:active { opacity:.55; }

/* 「我的家」弹层：一行一套房（缩略图 + 房名 + 户型），当前那套打勾；行上只有两个手势——
   轻点切换、**长按（桌面右键等价）打开这一行的动作**（改名字与地址 / 删除，见 app.js）。
   下面只有「添加新家」。地址不进列表：它长、每家都差不多，是编辑表里那个字段的事。 */
.house-list { list-style:none; margin:0 0 14px; padding:0; display:flex; flex-direction:column; gap:8px; }
/* 左滑露出动作：行自己横拖，动作垫在它下面。纵向留给弹层滚动——
   touch-action:pan-y 把纵向手势让出去，方向仲裁在 app.js 里。
   **这一格必须是 flex**：行靠 flex:1 撑满整个宽度去盖住下面那层动作；
   换成 display:block 的话，<button> 的 width:auto 会收缩到内容（实测动作就露出来了）。 */
.house-item { position:relative; overflow:hidden; border-radius:12px; display:flex; align-items:stretch; }
.house-swipe-actions { position:absolute; inset:0 0 0 auto; display:flex; }
.house-swipe-act { width:84px; border:0; font:inherit; font-size:12.5px; font-weight:700; color:var(--ink); background:var(--warm-strong); cursor:pointer; }
.house-swipe-act-delete { color:#fff; background:var(--danger); }
.house-row { touch-action:pan-y; transition:transform .18s ease; }
/* 「这行可以滑」的演示：轻轻推开再弹回。只在首次打开列表时演一次（app.js 控制），
   有「减弱动态效果」的人不播——对他们来说动作按钮另有长按与右键两条路。 */
@keyframes houseHint {
  0%, 100% { transform:translateX(0); }
  45% { transform:translateX(-18px); }
}
.house-item.hinting .house-row { animation:houseHint .9s ease; }
@media (prefers-reduced-motion: reduce) {
  .house-item.hinting .house-row { animation:none; }
}
.house-item.swiping .house-row { transition:none; }
/* 露出来时，行的右缘要与菜单的左缘拼成一条直缝——两个圆角对在一起会留下一个缺口。
   整行**外缘**的圆角由 .house-item 的 overflow:hidden + border-radius 负责，不用去掉。 */
.house-item.open .house-row { transform:translateX(calc(-1 * var(--swipe-w,168px))); border-top-right-radius:0; border-bottom-right-radius:0; }
.house-row { position:relative; z-index:1; display:flex; align-items:center; gap:10px; flex:1 1 auto; min-width:0; text-align:left; font:inherit; cursor:pointer; color:var(--ink); padding:11px 12px; border:1px solid var(--line); border-radius:12px; background:var(--surface); }
.house-item.current .house-row { background:var(--warm); box-shadow:inset 0 0 0 1px var(--line); }
.house-row-text { display:flex; flex-direction:column; gap:2px; min-width:0; flex:1 1 auto; }
.house-row-text strong { font-size:13.5px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.house-row-text small { font-size:11.5px; color:var(--ink-2); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
/* 选中那一行的对勾：形状与户型图页那颗确认钮一致（用户 2026-10-01），所以是 SVG 不是文字 */
.house-row-check { flex:0 0 auto; width:18px; height:18px; color:var(--accent); }
/* 房源行上的户型缩略图：按这套房自己的房间矩形画（见 app.js 的 planThumbSVG）。
   虚线空框是"还没有户型图"，不是"这是一张图"。 */
.house-row-thumb { flex:0 0 auto; width:44px; height:44px; padding:3px; border-radius:10px; background:var(--surface); border:1px solid var(--line); box-sizing:border-box; }
.house-row-thumb rect { fill:var(--warm-soft); stroke:var(--ink-2); stroke-width:1.5; vector-effect:non-scaling-stroke; }
/* 空态：虚线框要**看得见**（它是"还没有户型图"的占位，不是一片空白） */
.house-row-thumb.is-empty { border-style:dashed; border-color:var(--ink-2); background:none; opacity:.55; }
/* 「添加新房」是**按钮**，不是又一行房源：居中、带加号、染一层底色——
   房源行是左对齐的白卡（缩略图 + 房名 + 户型），两者一眼分得开。 */
.house-add { display:flex; align-items:center; justify-content:center; gap:6px; width:100%; margin-top:16px; font:inherit; font-size:13.5px; font-weight:700; color:var(--ink); cursor:pointer; padding:13px 12px; border:1px solid var(--line); border-radius:12px; background:var(--warm); }
.house-add-plus { font-size:16px; font-weight:400; line-height:1; }
/* 按钮里那两枚图标（导出 / 导入）——与文字同色，走 currentColor */
.house-add svg { flex:none; width:16px; height:16px; fill:none; stroke:currentColor; stroke-width:2; stroke-linecap:round; stroke-linejoin:round; }
/* 危险动作（删除）：与左滑露出的那颗同色同说法。
   **必须排在 .house-add 之后**——同权重下靠后的赢，写在前面会被那层的底色盖掉（实测）。 */
.house-add-danger { color:#fff; background:var(--danger); border-color:var(--danger); }
/* 次要动作：同一块里主按钮是实心的，它是描边的——光靠顺序分不出主次。
   同样**必须排在 .house-add 之后**（同上）。 */
.house-add-quiet { background:var(--surface); color:var(--ink-2); }
/* 提交那一颗：同一块里"事情就定了"的那个（`.house-add` 管的是"再走一步"）。
   同样**必须排在 .house-add 之后**。 */
.house-add-primary { color:#fff; background:var(--accent); border-color:var(--accent); }
/* ⚠ 这三颗之间的间距 = `gap` 加上 `.house-add` 自己的 `margin-top:16px`（那颗是给别的
   Sheet 里的提交按钮用的）——两笔加起来 24px，看着是一堆散开的卡片。用户 2026-10-01
   要求「减小 3 个按钮的间距」，所以这里把 margin 归零、gap 收到 6px。 */
.house-actions { display:flex; flex-direction:column; gap:6px; }
.house-actions .house-add { margin-top:0; }
/* 导入：逐套说清会怎么处理，只有"内容不一样"的才问一句。默认押在「两份都留」上
   ——覆盖是不可逆的，默认值不该是它。 */
.import-list { display:grid; gap:10px; margin:0; padding:0; list-style:none; }
.import-row { display:grid; gap:3px; padding:10px 12px; border:1px solid var(--line); border-radius:12px; background:var(--surface); }
.import-name { font-size:13.5px; font-weight:700; color:var(--ink); }
.import-what { font-size:12px; color:var(--ink-2); }
.import-choice { display:flex; gap:8px; margin-top:6px; }
.import-choice button { flex:1; padding:8px; border:1px solid var(--line-2); border-radius:10px; background:var(--surface); color:var(--ink-2); font:inherit; font-size:12.5px; cursor:pointer; }
.import-choice button[aria-pressed="true"] { border-color:var(--accent); background:var(--accent-soft); color:var(--accent-strong); font-weight:700; }

/* 地图与效果图页共用形象尺寸、停靠过渡与镜像反馈。 */
#journey-designer { width:min(50vw,220px); height:auto; aspect-ratio:2/3; left:8px; top:auto; bottom:24px; }
/* 缩小＝只剩头像：与 #guide 同一条规矩（换图那几条通用规则写在 `.guide.small` 上，这里只是
   **把 id 上写死的 2:3 让开**，否则 id 压过类，地图这颗还是整身）。 */
#journey-designer.small { width:var(--face,64px); aspect-ratio:1/1; }
#journey-designer #journey-traveler { display:block; aspect-ratio:2/3; height:auto; }
#journey-designer.small #journey-traveler { aspect-ratio:1/1; }
/* ⚠ 这里**不写 `transition`**：写了（id 压过类）就轮不到 `.guide-mascot img` 那条
   —— 换态那 .35s 的延迟与 filter 交叉淡入淡出全在它身上，两颗形象共用同一份时序。 */
#journey-designer #journey-traveler img { object-fit:cover; }
#journey-designer #journey-traveler.fading img { opacity:0; transition-duration:.18s; }

/* 地图、户型校准与3D设计共用页面底色。 */
/* 地图纸色。**必须等于底图（trip.webp）自己的纸色**——展开时镜头把底图推走，屏幕上会露出一条
   地图元素本身的底色（实测 390×844 上施工展开后右侧约 55px）。底色和图片对不上，
   那里就是一道 4 个色阶的直边，一眼看得出地图被「切」了一刀。
   量法：渲染后贴手机四边缩进 3% 取一圈点、求中位值。 */
:root { --journey-map-bg:#f9f2e7; --workspace-bg:#faf3e9; }

/* ============ 薄透明标题栏（旅程页与施工相关弹层） ============ */
/* 栏浮在内容之上，不占布局；内容紧接栏的下沿开始，sticky 标题也停在同一位置。
   定义在容器上，页面与弹层各自算各自的（安全区不同）。 */
/* 变量必须定义在容器上而不是栏上：内容区是栏的兄弟节点，定义在栏上取不到。
   两个栏的内高不同 —— 页面栏里是 42px 的返回按钮，弹层栏里是 26px 的关闭按钮，
   所以各自算各自的，否则内容会多让出一段、首个 header 离栏很远。
   内容紧接栏的下沿开始，sticky 标题也停在同一位置。 */
#intro { --bar-pad: max(10px,env(safe-area-inset-top)); --content-top: calc(var(--bar-pad) + 52px); }
.has-bar { --bar-pad: max(10px,env(safe-area-inset-top)); --content-top: calc(var(--bar-pad) + 36px); }
/* 焦点框只在**键盘操作**时出现。
   为什么不能只靠 :focus-visible：按规范，文本输入框的 :focus-visible 在鼠标点击时
   **也成立**（因为要往里打字），所以搜索框和聊天输入框那圈绿框光用 :focus-visible
   去不掉。这里记住最后一次用的是键盘还是指针，html[data-input] 由 app.js 维护。
   只清 outline：像地图节点那种把焦点画在圆圈上的自定义样式不受影响。 */
html:not([data-input="keyboard"]) :focus-visible {
  outline: none;
}

/* 页面标题栏保留薄透（滚过的内容透出来并被模糊掉）；弹层标题栏与吸顶标题才是不透明的。 */
/* ============ 标题栏的布局规格（2026-09-29 用户要求"整理下所有标题栏布局信息"）============
   五栏同一条规格，出处**只有这里**（`.nav-back` / `.house-bar` / `.drawing-print` 里都不许
   再写一遍像素）：

   | 量 | 值 | 出处 |
   | --- | --- | --- |
   | 左 / 右边距 | **14px**（两边一样） | `.page-bar` 的 `padding` |
   | 图标（logo / 返回 / 功能） | **20px** | `--bar-icon`（在 `.app` 上定） |
   | 触点 | **42px** | `--bar-tap` |
   | 图标**墨迹**离栏边 | **25px** | 上两条推出来的：14 + (42−20)/2 |
   | 图标**墨迹** ↔ 标题 | **21px** | 上两条推出来的：(42−20)/2 + 10 |
   | 栏高 | `bar-pad + 52px` | 下面这条 `min-height` |

   ⚠ **规格记在"墨迹"上，不是记在 `gap` 上**（2026-09-29 用户："地图页的 logo 和标题不统一"）。
   同一句 `gap:10px`，在"图标住在一颗 42px 盒子里"和"图标直接贴边"两处**量出两个距离**
   （21 与 10）——地图那栏的 logo 当时就是后者。规矩定成墨迹，谁也不用去心里减那个 11：
   **每颗图标都住进 `--bar-tap` 的盒子**，25 与 21 自然成立。

   ⚠ "整理"时抓到的三处**死代码**，都删了——它们是同一个毛病的三种长相（**同一件事写两遍**，
   而且写的还是旧值）：`#journey-back` 里那条独立定位（被 `.page-bar #journey-back` 以更高
   特异性压掉）、`.house-bar` 的 `gap:8px` 与 `padding-right:12px`（被**在后面**的 `.page-bar`
   同分覆盖）。**实测右边距是 14 而不是它自己写的 12，就是它们露出来的方式。** */
/* ⚠ **同高这件事写在 `.page-bar` 自己身上**，不写名单（2026-09-29 用户报"地图页标题栏
   高度与其他页不一致"）。
   这一条以前是 `#calib-bar, #design-bar, #drawing-bar { min-height: … }` —— 一张**手维护的
   名单**，于是 9-27 加的施工图栏漏过一次（它自己手算 52px）、9-29 加的地图那栏
   （`#journey-bar`）与首页那栏（`#house-bar`，带 logo 的那个）又漏了一次：实测
   **house-bar 54px vs 其余 62px**，差在它最高的子元素是 34px 的设置按钮。
   同一个毛病在这个文件里已经犯到**第三次**了（`journey-map.js` 的 `NOT_BLANK`、
   `.nav-back` 的 id 名单、这张栏高名单）——**凡是"新加一个就得记得改一遍"的名单，
   迟早会漏**。所以这次不补名单，把规则挪到基类上：新加一栏自动同高。 */
.page-bar { position:absolute; top:0; left:0; right:0; z-index:6; display:flex; align-items:center; gap:10px; min-height:calc(var(--bar-pad) + 52px); padding:var(--bar-pad) 14px 10px; background:#f5efe3ad; backdrop-filter:blur(10px); -webkit-backdrop-filter:blur(10px); }
/* 与地图节点同一套：正文字体、650 字重、同一色；字号比节点略大 */
.page-bar-title { flex:1; min-width:0; margin:0; font-family:inherit; font-size:17px; font-weight:650; letter-spacing:0; color:#4c5b40; }
.page-bar #journey-back { position:static; }
/* 户型图与效果图也走同一套薄透标题栏，返回按钮**在栏里**（与施工页同构）。 */
/* ⚠ 这份名单是**手维护**的：新加一页时记得把它的 id 加进来。
   施工图那一页（2026-09-27 加的）当时就没加——于是它自己手算 52px，而标题栏里装的是一颗
   **小一点的打印按钮**，那一栏因此比别的页矮一截（与 journey-map 的 NOT_BLANK 那次是
   同一个毛病：新加的一页忘了加入一张既有的名单）。 */
#confirm, #design-chat, #drawing { --bar-pad: max(10px, env(safe-area-inset-top)); --content-top: calc(var(--bar-pad) + 52px); /* 页内那几颗**圆形图标钮**共用一个尺寸（撤销与内容区那颗眼睛必须一样大，用户 2026-09-30） */ --icon-btn: 32px; --icon-btn-icon: 18px; }
/* 栏高不再靠名单：见 `.page-bar` 自己那条 `min-height`（2026-09-29 撤掉名单）。 */

.sheet-bar { position:absolute; top:0; left:0; right:0; z-index:3; display:flex; align-items:center; gap:10px; padding:var(--bar-pad,10px) 16px 10px; background:var(--bg); border-radius:var(--sheet-radius,18px) var(--sheet-radius,18px) 0 0; }
/* 与页面标题栏、地图节点同一套字体 */
/* 各弹层标题栏统一：与页面标题栏、地图节点同一套字体 */
.sheet-bar-title { flex:1; min-width:0; margin:0; font-family:inherit; font-size:17px; font-weight:650; color:#4c5b40; }
.sheet-bar .sheet-close { position:static; flex:0 0 auto; order:2; }
/* 有标题栏的弹层：自己不再滚动，改由内容区滚动，栏浮在最上面 */
.sheet.has-bar { display:flex; flex-direction:column; overflow:hidden; padding:0; }
/* 拖拽横条在图省空间：它就是首个 header 上方那段多余内容 */
.sheet.has-bar > .sheet-grip { display:none; }
.sheet.has-bar > .sheet-content { flex:1; min-height:0; overflow-y:auto; overscroll-behavior:contain; padding:var(--content-top) 16px 22px; margin-top:0; }

/* ============ 施工页：纵向阶段列表 ============ */
/* 不进缩放地图盒：这是普通文档流里的滚动列表。表头 sticky 在悬浮返回按钮之下。 */
#journey-shell { flex:1 1 auto; min-height:0; overflow-y:auto; overscroll-behavior:contain; padding:var(--content-top) 12px 0; scrollbar-width:none; background:var(--journey-map-bg); }
#journey-shell::-webkit-scrollbar { display:none; }
#journey-trades { list-style:none; margin:0; padding:0 0 calc(max(16px,env(safe-area-inset-bottom)) + 150px); }
.journey-stage-item { margin:0 0 8px; }
/* 吸顶要满足两件事：
   1) 挂在 h3 上而不是按钮上 —— sticky 的活动范围受父盒子限制，h3 只包着按钮时
      按钮一点行程都没有；h3 的父级是整段 li，才滚得动。
   2) top 用 0 而不是返回按钮的高度 —— 吸顶偏移相对容器的内容盒算，
      容器的 padding-top 已经让出了那一段，再加一次会顶到列表中间。 */
.journey-stage-head { position:sticky; top:0; z-index:2; display:flex; align-items:center; gap:4px; margin:0; padding:0 10px 0 0; border:1px solid var(--line-2); border-radius:14px; background:var(--bg); font-size:inherit; font-weight:inherit; }
.journey-stage { flex:1; min-width:0; display:flex; align-items:center; gap:11px; padding:11px 4px 11px 13px; border:0; background:none; color:var(--ink); font:inherit; text-align:left; cursor:pointer; }
.journey-stage-icon { flex:0 0 auto; display:grid; place-items:center; width:26px; height:26px; border-radius:50%; border:2px solid var(--trade,#8a8375); color:var(--trade,#8a8375); }
.journey-stage-icon svg { width:14px; height:14px; }
.journey-stage-title { flex:1; min-width:0; display:flex; flex-direction:column; gap:1px; }
.journey-stage-name { font-size:15px; }
.journey-stage-meta { font-size:11px; color:var(--ink-2); font-variant-numeric:tabular-nums; }
.journey-stage-arrow { flex:0 0 auto; width:9px; height:9px; border-right:2px solid var(--ink-2); border-bottom:2px solid var(--ink-2); transform:rotate(45deg) translate(-2px,-2px); transition:transform .18s ease; }
.journey-stage[aria-expanded="true"] .journey-stage-arrow { transform:rotate(-135deg) translate(-2px,-2px); }
.journey-stage:focus-visible,.journey-stage-worker:focus-visible,.journey-step:focus-visible { outline:2px solid var(--accent); outline-offset:2px; border-radius:8px; }

/* 施工方头像：已签约是实心，未签约是虚线轮廓 */
.journey-stage-worker { flex:0 0 auto; width:38px; height:38px; padding:0; border:0; border-radius:50%; background:none; cursor:pointer; }
.journey-worker-face { display:grid; place-items:center; width:100%; height:100%; border-radius:50%; background:var(--trade,#8a8375); color:#fff; font-size:14px; }
.journey-stage-worker { display:grid; place-items:center; width:38px; height:38px; }
.journey-stage-worker .journey-worker-face { width:34px; height:34px; }
.journey-stage-worker.is-empty .journey-worker-face { background:none; border:1.5px dashed var(--line-2); color:var(--ink-2); }
.journey-stage-worker.is-empty .journey-worker-face svg { width:16px; height:16px; }

.journey-stage-panel { padding:10px 10px 12px; margin-top:-14px; padding-top:24px; border:1px solid var(--line); border-top:0; border-radius:0 0 14px 14px; background:var(--surface); }
/* 工序行按预算表列表项的样子排：卡片背景与边框、左名右值。 */
.journey-steps { list-style:none; margin:0; padding:0; }
.journey-steps li + li { margin-top:8px; }
.journey-step { box-sizing:border-box; width:100%; display:flex; align-items:center; gap:10px; padding:12px 12px; border:1px solid var(--line-2); border-radius:12px; background:var(--surface-2); color:var(--ink); font:inherit; text-align:left; cursor:pointer; }
.journey-step-icon { flex:0 0 auto; display:grid; place-items:center; width:24px; height:24px; border-radius:7px; background:color-mix(in srgb,var(--trade,#8a8375) 14%,transparent); color:var(--trade,#8a8375); }
.journey-step-icon svg { width:14px; height:14px; }
.journey-step-name { flex:1; min-width:0; font-size:13px; }
.journey-step-meta { flex:0 0 auto; font-size:12px; color:var(--ink-2); font-variant-numeric:tabular-nums; white-space:nowrap; }

.trade-notice { margin:12px 0 0; padding-top:11px; border-top:1px solid var(--line); font-size:11px; line-height:1.6; color:var(--ink-2); }

/* 工艺详情与签约弹层 */
/* 工艺弹窗：工艺 / 辅材 / 常见翻车 三个分区 */
.craft-block + .craft-block { margin-top:18px; padding-top:14px; border-top:1px solid var(--line); }
/* 分区标题吸顶。这次不会踩上次那个坑：sticky 的活动范围受父盒子限制，而 h4 的
   父级是整块 .craft-block（内容在它下面），所以标题有行程；阶段列表那次 h3 只
   包着按钮，行程为零才吸不住。背景要盖住滚过去的内容。 */
.craft-heading { position:sticky; top:0; z-index:1; margin:0; padding:14px 0 12px; background:var(--bg); font-size:12px; font-weight:650; color:var(--ink-2); }
.craft-select { box-sizing:border-box; width:100%; padding:11px 12px; border:1px solid var(--line-2); border-radius:12px; background:var(--surface); color:var(--ink); font:inherit; font-size:13px; }
.craft-select:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
/* 说明与验收标准并成一块，不再单列一节 */
.craft-note { margin-top:10px; padding:12px; border-radius:12px; background:var(--surface-2); }
.step-detail { margin:0; font-size:13px; line-height:1.75; }
.craft-checks { list-style:none; margin:0; padding:0; }
.craft-checks li + li { border-top:1px solid var(--line); }
/* 验收一项：文字 + 拍照/现场确认。照片只是记录，判断合格与否仍然是人的事。 */
.craft-check { padding:12px 0; }
.craft-check-text { position:relative; margin:0 0 9px; padding-left:20px; font-size:13px; line-height:1.55; color:var(--ink); }
.craft-check-text::before { content:""; position:absolute; left:2px; top:6px; width:8px; height:8px; border-radius:2px; border:1.5px solid var(--ink-2); }
.craft-check-actions { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
.check-shot { display:grid; place-items:center; width:34px; height:34px; border:1px solid var(--line-2); border-radius:50%; background:var(--surface); color:var(--ink-2); cursor:pointer; }
.check-shot svg { width:17px; height:17px; }
.check-shot:focus-within { outline:2px solid var(--accent); outline-offset:2px; }
.sr-only { position:absolute; width:1px; height:1px; overflow:hidden; clip-path:inset(50%); white-space:nowrap; }
/* 每条验收右侧的设计师头像：点开这一条自己的对话 */
.check-ask { flex:0 0 auto; width:34px; height:34px; margin-left:auto; padding:0; border:1px solid var(--line-2); border-radius:50%; background:var(--surface); cursor:pointer; overflow:hidden; }
.check-ask img { display:block; width:100%; height:100%; object-fit:cover; object-position:50% 18%; }
.check-ask:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }

/* 验收对话：与设计聊天分开的一层，沿用底部面板的观感 */
/* 层级要高于 #sheet(28)：验收对话是叠在工艺弹窗之上的 */
.check-chat { z-index:34; left:var(--sheet-edge); right:var(--sheet-edge); bottom:var(--sheet-bottom); border-radius:var(--sheet-radius); display:flex; flex-direction:column; max-height:min(72%,calc(100% - 32px)); }
.check-chat .chat-head { display:flex; align-items:center; gap:10px; padding:12px 14px 10px; border-bottom:1px solid var(--line); }
.check-chat .sheet-close { position:static; flex:0 0 auto; margin-left:auto; }
#check-chat-mask { z-index:33; }
.chat-head-text { flex:1; min-width:0; }
.chat-head-text strong { display:block; font-size:14px; }
.chat-head-text small { display:block; font-size:11px; color:var(--ink-2); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.check-chat-body { display:flex; flex-direction:column; flex:1; min-height:0; }
.check-chat-body.is-busy { opacity:.6; pointer-events:none; }
.check-chat-photos { padding:10px 14px 0; }
.check-chat-photos ul { display:flex; flex-wrap:wrap; gap:8px; list-style:none; margin:8px 0 0; padding:0; }
.check-chat-photos label { display:block; position:relative; cursor:pointer; }
.check-chat-photos img { display:block; width:56px; height:56px; object-fit:cover; border-radius:9px; border:1px solid var(--line-2); }
.check-chat-photos input { position:absolute; top:4px; left:4px; }
.check-chat-photos input:checked + img { border-color:var(--accent); box-shadow:0 0 0 2px var(--accent-soft); }
.check-chat-hint { margin:0; font-size:11px; line-height:1.6; color:var(--ink-2); }
.check-chat-transcript { flex:1; min-height:120px; overflow-y:auto; padding:12px 14px; display:flex; flex-direction:column; gap:8px; }
.check-chat-empty { margin:0; font-size:12px; line-height:1.7; color:var(--ink-2); }
.check-chat-line { margin:0; max-width:86%; padding:9px 12px; border-radius:14px; font-size:13px; line-height:1.6; }
.check-chat-line.ai { align-self:flex-start; background:var(--surface-2); }
.check-chat-line.me { align-self:flex-end; background:var(--accent-soft); }
.check-chat-input-row { display:flex; gap:8px; padding:10px 14px calc(14px + var(--sheet-bottom)); border-top:1px solid var(--line); }
.check-chat-input-row input { flex:1; min-width:0; padding:10px 12px; border:1px solid var(--line-2); border-radius:999px; background:var(--surface); color:var(--ink); font:inherit; font-size:13px; }
.check-chat-input-row button { flex:0 0 auto; padding:0 18px; border:0; border-radius:999px; background:var(--accent); color:#fff; font:inherit; font-size:13px; cursor:pointer; }
.check-photos { display:flex; gap:8px; list-style:none; margin:10px 0 0; padding:0; }
.check-photos li { position:relative; flex:0 0 auto; }
.check-photos img { display:block; width:64px; height:64px; object-fit:cover; border-radius:9px; border:1px solid var(--line-2); }
.check-photo-remove { position:absolute; top:-6px; right:-6px; width:20px; height:20px; padding:0; border:1px solid var(--line-2); border-radius:50%; background:var(--surface); color:var(--ink-2); font-size:10px; line-height:1; cursor:pointer; }
/* 照片本体不进备份文件，导入回来的记录里只有编号。一条有 3 张照片的验收项静默
   变成 0 张，是"以为照片丢了"的假象——取不到就得说出来。 */
.check-photo-missing { flex:0 0 auto; align-self:center; max-width:150px; font-size:11.5px; line-height:1.5; color:var(--ink-2); }

/* 辅材：类型从标题栏的加号加进来，行内是规格下拉 + 价格 + 删除 */
.craft-head { display:flex; align-items:center; gap:10px; }
.craft-head .craft-heading { flex:1; }
.craft-add { flex:0 0 auto; width:34px; height:34px; padding:0; display:grid; place-items:center; border:1px solid var(--accent); border-radius:50%; background:var(--surface); color:var(--accent); cursor:pointer; }
.craft-add svg { width:18px; height:18px; }
.picker-group { margin:0 0 16px; }
/* 辅材选择器是叠在工艺弹窗之上的一层，不替换它 */
.sub-sheet-mask { position:absolute; inset:0; z-index:39; background:var(--mask); }
.sub-sheet { z-index:40; max-height:min(72%,calc(100% - 24px)); }
.picker-heading { position:sticky; top:0; z-index:1; margin:0; padding:14px 0 12px; background:var(--bg); font-size:12px; font-weight:650; color:var(--ink-2); }
.picker-row[disabled] { opacity:.45; cursor:not-allowed; }
.picker-row[disabled] .picker-meta { color:var(--ink-2); }
.picker-group ul { list-style:none; margin:0; padding:0; }
.picker-group li + li { margin-top:6px; }
.picker-row { box-sizing:border-box; width:100%; display:flex; align-items:baseline; gap:10px; padding:11px 12px; border:1px solid var(--line-2); border-radius:12px; background:var(--surface); color:var(--ink); font:inherit; text-align:left; cursor:pointer; }
.picker-row span:first-child { flex:1; min-width:0; font-size:13px; }
.picker-meta { flex:0 0 auto; font-size:11px; color:var(--ink-2); }
.craft-materials { list-style:none; margin:0; padding:0; }
.craft-material-row { display:grid; grid-template-columns:minmax(0,1fr) minmax(0,0.9fr) auto auto; align-items:center; gap:6px; padding:9px 0; border-bottom:1px solid var(--line); }
.craft-material-row.is-fixed { grid-template-columns:minmax(0,1.4fr) minmax(0,0.9fr) auto auto; }
.craft-material-row select { box-sizing:border-box; width:100%; min-width:0; padding:8px 6px; border:1px solid var(--line-2); border-radius:9px; background:var(--surface); color:var(--ink); font:inherit; font-size:12px; }
.craft-material-row.is-fixed .craft-material-spec { font-size:11px; color:var(--ink-2); }
.craft-price-cell { display:flex; align-items:center; gap:4px; }
.craft-price { box-sizing:border-box; width:58px; padding:8px 6px; border:1px solid var(--line-2); border-radius:9px; background:var(--surface); color:var(--ink); font:inherit; font-size:12px; text-align:right; font-variant-numeric:tabular-nums; }
.craft-unit { font-size:11px; color:var(--ink-2); white-space:nowrap; }
.craft-remove { position:relative; width:26px; height:26px; padding:0; border:0; border-radius:50%; background:var(--surface-3); color:var(--ink-2); cursor:pointer; }
.craft-remove::before { content:"✕"; position:absolute; inset:0; display:grid; place-items:center; font-size:12px; line-height:1; }
.craft-remove:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }

/* 翻车案例：纵向列表，行内缩略图 + 文字，点开全屏 */
.risk-list { list-style:none; margin:0; padding:0; }
.risk-list li + li { margin-top:8px; }
.risk-row { box-sizing:border-box; width:100%; display:flex; align-items:center; gap:10px; padding:10px; border:1px solid var(--line-2); border-radius:12px; background:var(--surface-2); color:var(--ink); font:inherit; text-align:left; cursor:pointer; }
.risk-thumb { flex:0 0 auto; display:grid; place-items:center; width:64px; height:64px; overflow:hidden; border-radius:9px; background:var(--surface-3); }
.risk-thumb img,.risk-thumb video { width:100%; height:100%; object-fit:cover; }
.risk-placeholder { display:grid; place-items:center; width:100%; height:100%; background:repeating-linear-gradient(45deg,var(--surface-3) 0 6px,var(--surface-2) 6px 12px); color:var(--ink-2); font-size:10px; line-height:1.2; text-align:center; }
.risk-placeholder.big { min-width:200px; min-height:140px; font-size:13px; }
.risk-text { flex:1; min-width:0; font-size:12px; line-height:1.55; }
.risk-row:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }

/* 全屏查看 */
.risk-viewer { position:fixed; inset:0; z-index:100; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:16px; padding:24px; background:rgba(20,18,14,.92); }
.risk-viewer-stage { display:grid; place-items:center; max-width:100%; max-height:70%; }
.risk-viewer-stage img,.risk-viewer-stage video { max-width:100%; max-height:70vh; border-radius:12px; }
.risk-viewer-text { margin:0; max-width:560px; font-size:14px; line-height:1.7; color:#f4efe6; text-align:center; }
.risk-viewer-close { position:absolute; top:max(16px,env(safe-area-inset-top)); right:16px; width:42px; height:42px; border:1px solid #ffffff44; border-radius:50%; background:#ffffff1a; color:#f4efe6; font-size:17px; cursor:pointer; }

.step-detail { margin:0; font-size:13px; line-height:1.75; }
.worker-note { margin:0 0 12px; font-size:11px; line-height:1.6; color:var(--ink-2); }
.worker-card { display:flex; align-items:center; gap:12px; }
.worker-card .journey-worker-face { flex:0 0 auto; width:44px; height:44px; font-size:17px; }
.worker-card strong,.worker-list strong { display:block; font-size:14px; }
.worker-card small,.worker-list small { display:block; font-size:11px; color:var(--ink-2); }
.worker-list { list-style:none; margin:0; padding:0; }
.worker-list li { display:flex; align-items:center; gap:12px; padding:12px 0; border-bottom:1px solid var(--line); }
.worker-list .journey-worker-face { flex:0 0 auto; width:40px; height:40px; font-size:15px; }
.worker-list div { flex:1; min-width:0; }
.worker-price { flex:0 0 auto; font-size:12px; color:var(--ink-2); font-variant-numeric:tabular-nums; }
@media (prefers-reduced-motion:reduce) { .journey-stage-arrow { transition:none; } }

#confirm, #design-chat, #design-chat .d3-view { background:var(--workspace-bg); }


.sheet-content { margin-top:6px; }


/* 只读预算表：顶部汇总 + 按阶段分组的工序与辅材。数字不给编辑入口，
   改预算的方式是点开工序改做法，所以行本身就是按钮。 */
.budget-summary { margin:10px 0 4px; padding:10px 12px; border:1px solid var(--line-2); border-radius:12px; background:var(--surface-2); }
.budget-summary .std-row b { font-variant-numeric:tabular-nums; }
.budget-summary .std-row:first-child b { font-size:17px; }
.budget-parsed { display:block; font-size:11px; font-weight:400; color:var(--ink-2); }
.budget-blocked { margin:10px 0; padding:10px 12px; border:1px dashed var(--accent); border-radius:12px; font-size:12px; line-height:1.6; color:var(--ink-2); }
.budget-blocked button { display:block; margin-top:8px; padding:8px 12px; border:1px solid var(--accent); border-radius:10px; background:var(--surface); color:var(--accent); }
.budget-stages { list-style:none; margin:0; padding:0; }
.budget-stage { margin:0 0 14px; }
.budget-stage-head { display:flex; justify-content:space-between; align-items:baseline; gap:10px; margin:0 0 6px; padding-left:9px; border-left:3px solid var(--trade,var(--accent)); font-size:14px; }
.budget-stage-head b { font-size:13px; font-variant-numeric:tabular-nums; font-weight:500; }
.budget-materials { list-style:none; margin:6px 0 10px; padding:0 0 0 12px; }
.budget-materials li { display:flex; justify-content:space-between; align-items:baseline; gap:10px; padding:5px 0; border-bottom:1px dashed var(--line-2); font-size:12px; }
.budget-materials li:last-child { border-bottom:0; }
.budget-material-name { min-width:0; color:var(--ink); }
.budget-material-name small { display:block; font-size:11px; color:var(--ink-2); }
.budget-material-meta { flex:0 0 auto; color:var(--ink-2); font-variant-numeric:tabular-nums; white-space:nowrap; }
.budget-material-meta b { margin-left:8px; color:var(--ink); font-weight:500; }
.budget-materials.is-pending .budget-material-name { color:var(--ink-2); }
.budget-materials.is-pending .budget-material-meta { font-style:italic; }
.budget-note { margin-top:14px; line-height:1.7; }

/* ============ 户型图页：未定态与户型图界面 ============
   同一页两态：还没有图时只摆一句话和两个去处（搜索小区 / 用样板间户型），
   有图时就是户型图界面。搜索与上传收在弹层里（见 openAddPlan）。 */
/* ============ 「原图」那一页的未定态（用户 2026-10-11）============
   它原来是**一整页**（`.plan-chooser`：顶部让开标题栏、一句话 ＋ 两颗大卡按钮）。
   现在收进「原图」那一页：占位纸在内容格（`.cf-base-empty`）、两个去处搬到操作区
   （下面 `.cf-edit-tools #entry-commands` 那一段）✓。
   ⚠ 话只说一句：两个去处就在**上面**那一格里，这里再复述一遍就是"一处两说" ✗。 */
/* ⚠ 里面可能是两样东西（一句话 ＋ 一颗按钮，"底图不在这台"那一态 ✓）⇒ 给个行距；
   `place-items:center` 已经把它们各自居中 ✓。 */
/* ⚠ `align-content:center` **必须写**：网格的行默认 `stretch` ⇒ 每行都被撑满，
   里面的东西各自在**自己那一行**里居中 —— 一句话与一颗按钮能隔出三百多像素 ✗（实测）。
   写上去之后这两行才作为一组居中 ✓。 */
.cf-base-empty { gap: 14px; align-content: center; height: 100%;
  box-sizing: border-box;
  display: grid;
  place-items: center;
  padding: 16px;
  /* 虚线空框 —— 与「我的家」列表里那个空缩略图（`.house-row-thumb.is-empty`）同一个语言 ✓ */
  border: 1px dashed var(--line-2);
  border-radius: 14px;
}
.cf-base-empty[hidden] { display: none; }
/* ⚠⚠ **「正在识别房间」那一幕**（用户 2026-10-18：「识别过程中应该在房型图内容区展示 loading 动画」✓）：
   那几秒里照片已经铺上去了、可**画布上一间房都没有** ✗ —— 不画点什么，看着像"卡住了" ✗。
   它盖**画布那一格**（压过那三颗按钮 ✓，识别的时候它们也没什么可点的 ✓），认完就收 ✓。
   ⚠ 底色用 `color-mix(--surface …)`：深色主题跟着走 ✓（工程里那几处磨砂都是这么写的 ✓）。 */
.cf-recognizing {
  position: absolute;
  inset: 0;
  z-index: 4;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  border-radius: 14px;
  background: color-mix(in srgb, var(--surface) 78%, transparent);
  color: var(--ink-2);
  font-size: 13px;
}
.cf-recognizing[hidden] { display: none; }
.cf-recognizing p { margin: 0; }
.cf-recognize-ring {
  width: 30px;
  height: 30px;
  box-sizing: border-box;
  border: 3px solid var(--line-2);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: cf-recognize-spin 0.9s linear infinite;
}
@keyframes cf-recognize-spin {
  to { transform: rotate(360deg); }
}
/* 少一档动效的人：**不转**，那句话说的事照旧在 ✓（转圈只是"还在干活"的记号 ✓）。 */
@media (prefers-reduced-motion: reduce) {
  .cf-recognize-ring { animation: none; }
}
/* 空白/无图那颗图标（用户 2026-10-12）—— 用**占位色** `--ph`：工程里它就是"这儿还没有东西"
   那一档 ✓（输入框的 placeholder 也是它 ✓）。两行文字换掉之后，这一格的语气没变 ✓。 */
.cf-base-empty .cf-base-icon { width: 40px; height: 40px; color: var(--ph); }
/* ⚠⚠ 纸那一格**让给它**（2026-10-12 实测踩到）：这一格（`.cf-folder-main`）里的几块
   **都是流里的元素** ⇒ 不给纸让位的话两块**上下叠**：这块被挤到纸下面、飘出这一格之外 ✗
   （实测：里面那颗按钮 `element is outside of the viewport` ✗，而 `isVisible()` 照样说"在" ✗✗
   —— 用户看到的是一张空白纸，正是他最初报的那件事）。
   做法与"画布 ↔ 定稿表"同一个规矩：**谁露出来谁占这一格** ✓（那个走 `display:none`，
   这个由 JS 在这层挂一个类 ✓）。 */
.cf-folder-main.is-base-placeholder .cf-plan { display: none; }
.cf-base-empty .plans-empty { margin: 0; text-align: center; font-size: 13.5px; line-height: 1.7; color: var(--ink-2); }

.cf-edit-tools #entry-commands { display: flex; gap: 8px; flex-wrap: wrap; }
/* ⚠ 这条**必须写**：上面那句 `display:flex` 会盖掉 `[hidden]` 的默认样式 ✗ */
.cf-edit-tools #entry-commands[hidden] { display: none; }
/* 两个去处：搜索&选择 / 拍照/上传（用户 2026-10-05 去掉副标题，两颗之间那句「或者」也去了）。 */
/* 2026-10-11：两个去处**只住一个地方**了 —— 户型图页操作区里那一格（从"整页空态"搬来 ✓）
   ⇒ 长相只写这一份，形式照那一格别的按钮（`.cf-edit-btn` 那族：药丸、32 高、13px）。
   从前那套"整页大卡"（`width:100%`、12 圆角、12 内边距）连同面板里那份覆盖一起删掉了 ✓
   —— 同一颗按钮的样子分两处写，改一处忘一处是迟早的事 ✗。 */
.entry-command { display:inline-flex; align-items:center; gap:8px; min-height:var(--cf-ctl-h); padding:0 13px; border:1px solid var(--line-2); border-radius:999px; background:var(--surface-2); color:var(--ink); font:inherit; font-size:13px; cursor:pointer; }
.entry-command svg { width:18px; height:18px; flex:0 0 auto; color:var(--accent); }
.entry-command-text { display:flex; flex-direction:column; gap:2px; min-width:0; }
.entry-command-label { font-size:13px; font-weight:700; }
/* 样板间那一行的第三行：这套方案含什么、按什么图估算大约多少钱。它比 hint 再弱一档
   ——hint 说的是"这一行在做什么"，这一行说的是"它值多少"。 */
.entry-command-price { font-size:11px; color:var(--ink-2); opacity:.88; }
/* 还没做出来的去处：虚线、弱化。点下去要有回应，所以它仍是可点的——
   一个毫无反应的按钮比不显示更糟。 */
.entry-command[data-disabled] { border-style:dashed; cursor:default; }
.entry-command[data-disabled] .entry-command-label { font-weight:600; color:var(--ink-2); }
.entry-command[data-disabled] svg { color:var(--ink-2); opacity:.6; }

/* ⚠⚠ **搜索弹层固定成最大高度**（用户 2026-10-19：「固定为最大高度，避免因搜索结果有无或
   多少出现抖动」✓）：原来只有 `min-height: 62%` ✗ ⇒ 高度**跟着内容走**（62% ⇄ 78% ✓）——
   一搜出结果整张弹层往上长、把结果清掉又缩回去，**一跳一跳** ✗。
   现在**写死**在最大高度上（与 `.sheet` 那条 `max-height` 是**同一个表达式** ✓）：
   结果多就**在弹层里滚** ✓，弹层本身纹丝不动 ✓。 */
.sheet[data-sheet="add"] {
  height: min(78%, calc(100% - var(--sheet-bottom) - 8px));
}
/* 标题栏是绝对定位的，内容靠 --content-top 让位，两边算下来**正好齐平**：
   bar 高 = --bar-pad + 关闭按钮 26 + 下内边距 10 = --bar-pad + 36 = --content-top。
   零余量之下，输入框顶部的圆角和 1px 描边看着就像被那条不透明的栏切掉了一线。
   这里补一点净空——凡是首个元素是输入框的弹层都该补。 */
.sheet[data-sheet="add"] > .sheet-content { padding-top:calc(var(--content-top) + 12px); }
/* 输入与建议列表。输入框铺满弹层宽度；建议列表紧跟其下，不再需要绝对定位。 */
/* 输入框右边那颗搜索按钮（用户 2026-10-12：默认禁用、填了小区名才激活 ✓）。
   ⚠ 用它自己那颗圆钮的样式（`.icon-btn` 那一族 ✓），**不另立一套** ✓；
   禁用时只淡一档 —— 工程里那条"不藏，只禁用" ✓。 */
/* 城市那颗 droplist。⚠⚠ `appearance:none` **必须写**：`<select>` 的原生控件外观会盖掉
   `border-radius` ✗ —— 圆角写了也看着是方的（用户 2026-10-12 报的"改成圆角"就是它 ✓）。
   ⚠ 关掉原生外观，**箭头就得自己画**（原生的那颗没法调位置 ✗）—— 自己画才能
   「**向下的箭头右侧增加边距**」✓（现在 right:12px ✓）。箭头用行内 svg ＋ `currentColor`：
   data-URI 里写死颜色会在深色主题下露馅 ✗。
   ⚠ 圆角与同排那颗输入框一致（都是 10px ✓）。
   （⚠ 这条规则曾在"删引导样式"那一刀里被**按区间切掉**过 —— 切片删代码时先确认区间里还有什么 ✗。） */
.estate-search .estate-city { position: relative; flex: 0 0 auto; display: inline-flex; }
/* 宽度**按 4 个字的城市名预留**（用户 2026-10-12：「宽度增加一倍，预留 4 个字」✓）——
   `4em` ＝ 4 个汉字（中日韩字宽就是 1em ✓），再加上左右内边距与描边 ＝ `+48px` ✓。
   ⚠ 用 `em` 不用 `ch`：`ch` 是"0"的宽度，量汉字会短一截 ✗。 */
.estate-search select { appearance: none; -webkit-appearance: none; box-sizing: border-box; width: calc(4em + 48px); min-height: 38px; padding: 0 34px 0 12px; border: 1px solid var(--line-2); border-radius: 10px; background: var(--surface); color: var(--ink); font: inherit; font-size: 13px; }
.estate-city-arrow { position: absolute; right: 12px; top: 50%; transform: translateY(-50%); width: 14px; height: 14px; color: var(--ink-2); pointer-events: none; }
/* ⚠ 搜索键**收在输入框里**（那一行的右端 ✓）：透明底 ⇒ 看着像输入框自带的放大镜 ✓；
   禁用时只淡一档（工程里那条"不藏，只禁用" ✓）。 */
.estate-search-go { position: absolute; top: 50%; right: 6px; transform: translateY(-50%); width: 30px; height: 30px; padding: 0; display: grid; place-items: center; border: 0; border-radius: 8px; background: transparent; color: var(--ink-2); cursor: pointer; }
/* 输入框给它让出位置 ✓（不然长的小区名会钻到放大镜底下 ✗）。 */
.estate-search input { padding-right: 40px; }
.estate-search-go svg { width: 18px; height: 18px; }
.estate-search-go[disabled] { opacity: .45; cursor: not-allowed; }

/* 弹层里内容少，这一行用 margin-top:auto 沉到底部——它是「还有别的办法」的
   补充，不该和搜索框争位置。纯文本，不用卡片。 */
.sheet[data-sheet="add"] > .sheet-content { display:flex; flex-direction:column; }
/* 通栏的次要按钮：虚线边框与 app 里其他「还不能用」的东西保持同一套说法
   （见 .entry-command[data-disabled]）。 */
.estate-suggest { margin-top:10px; border-radius:12px; overflow:hidden; background:var(--surface); box-shadow:0 6px 20px rgba(0,0,0,.1); }

/* ── 备案号（工信部 2026-10-09 通过、公安联网 2026-10-12 通过，两条并排挂着）──────
   两个都要在网站上挂出来、各自链到官方页面（见 `ui/beian.js`）。
   两个落点：首页最底下那条（`.beian-lines-home`，是 #intro 的孩子 ⇒ 跟地图页一起显隐，
   进子页就不在眼面前了）、分享预览页文档末尾（`.beian-lines-share`，在文档流里）。
   ⚠ **`absolute` 不是 `fixed`**（首页那条）：贴的是**应用框**（`.app`），不是窗口——
   桌面宽度下 `.app` 是居中的窄列，`fixed` 会把它画到窗口右边、跑到应用框外的灰底上
   （2026-09-30 样板间水印在那儿栽过一次，`#intro` 是 `.journey-home`、自己就占满 `.app`，
   所以 `absolute` 正好）。 */
/* 两条并排（工信部 ＋ 公安联网；公安那条 2026-10-12 通过、当天挂上 ✓）。
   ⚠ **落点在这个 flex 容器上**（首页绝对定位、分享页在文档流里），两颗 <a> 只负责排队 ✓
   —— 早先只有工信部一条时，落点与文字是同一个元素；加第二条就必须分开，
   否则两条会各自绝对定位、叠在一起 ✗。 */
/* ⚠ 换图那张确认表：一句话与下面两颗按钮挨得太近（用户 2026-10-12 报的）——
   给那句话一点**底部**边距 ✓。⚠ 只改这一张（按 `data-sheet` 认它 ✓）：
   `.material-note` 是全工程共用的，动全局会把它在别的表里的间距一起改了 ✗
   （「定稿」那张形状同构，要不要一起给，等一句话 ✓）。 */
.sheet[data-sheet="replace-plan"] .material-note { margin: 0 0 18px; }
.beian-lines { display:flex; flex-wrap:wrap; justify-content:center; align-items:center; gap:2px 10px; font-size:10px; }
/* ⚠ 两串号**必须在同一条线上**（用户 2026-10-12 报的"没有竖直对齐"）：
   警徽 17px、文字 12px ⇒ 一条链接里两种高度，而 `vertical-align` 那套老办法是按**基线**
   对齐的 —— 结果公安那串被图标顶得比工信部那串低 1.5px（实测 825.5 vs 827），肉眼就是"歪" ✗。
   改成**行内 flex ＋ 居中**：图标与文字在各自的盒子里居中，外层那行再把两条链接各自居中
   ⇒ 两串号自然落在同一条线上 ✓（也顺手替掉了那颗 `vertical-align:-3px` 的调数值 ✓）。 */
.beian-line { display:inline-flex; align-items:center; gap:4px; font-size:10px; line-height:1.4; letter-spacing:.02em; color:#6f7563cc; text-decoration:none; white-space:nowrap; }
.beian-badge { height:17px; width:auto; }
.beian-line:hover, .beian-line:focus-visible { color:#454d3b; text-decoration:underline; }
/* ⚠ `flex-wrap:nowrap` **必须写**（2026-10-12 实测）：这个容器是 `absolute` ＋ `left:50%`
   ⇒ 它的**收缩到合适**宽度只从"中间到右边缘"那么多（实测 195px），两条加起来 ~300px
   ⇒ 于是 flex 把它们**折成两行**（底部看起来是上下两行，而不是并排 ✗）。
   禁掉换行之后宽度按内容算（~300px，390 的屏上放得下 ✓），两条才真的并排 ✓。 */
.beian-lines-home { position:absolute; z-index:26; left:50%; transform:translateX(-50%); bottom:max(4px, env(safe-area-inset-bottom)); flex-wrap:nowrap; }
.beian-lines-share { margin-top:10px; color:var(--ink-2); }

/* ── 一级地图：推近式展开 ──────────────────────────────────────────────
   相机只动 .journey-camera：底图、道路、阶段节点都在它里面，一起放大；被点的那颗
   节点留在原地、跟着被推大，另外两颗被推出屏幕。
   区域、子节点、顺序箭头在它外面的 .journey-inner 里，用的是**地图坐标**而不是屏幕
   坐标——它们跟着 fit 缩放，但不跟着镜头放大，尺寸始终是设计尺寸。
   几何（角度、环半径、箭头落点）全部来自 domain/journey-map.js，这里只负责画。 */
.journey-camera {
  position: absolute;
  inset: 0;
  transform-origin: 0 0;
  transition: transform .85s cubic-bezier(.2, .85, .25, 1);
}
.journey-map-image, .journey-route { width: 100%; height: 100%; }

.journey-inner { position: absolute; inset: 0; z-index: 3; pointer-events: none; }
.journey-inner > * { position: absolute; }

/* 虚线区域。起点是节点原来在地图上的位置和大小（--dx/--dy 与 --r0），终点是正中、
   scale(1)；收起时原路退回，两头都落在同一颗节点身上。透明度和缩放共用一条缓动、
   同一个时长，所以看到的是一个动作：从节点里涨出来、同时由淡转实。 */
.journey-region {
  left: var(--cx);
  top: var(--cy);
  width: var(--rd);
  height: var(--rd);
  border-radius: 50%;
  background: rgba(224, 201, 166, .5);
  border: 1.5px dashed rgba(190, 160, 116, .85);
  transform: translate(calc(-50% + var(--dx, 0px)), calc(-50% + var(--dy, 0px))) scale(var(--r0, .12));
  opacity: 0;
  pointer-events: none;
  transition: transform .85s cubic-bezier(.2, .85, .25, 1), opacity .85s cubic-bezier(.2, .85, .25, 1);
}
/* 关着的区域恰好停在节点身上（就是圆盘那么大），要是吃得下点击，那颗节点就再也点不开了 */
.journey-inner.open .journey-region {
  opacity: 1;
  pointer-events: auto;
  transform: translate(-50%, -50%) scale(1);
}

/* 子节点：绕成一圈。出场时从环心飞出去，位移和淡入共用同一个 --d 延迟——
   一边飞出去一边显形，而不是先在环心亮一下再走。--d 由脚本按顺序给，逐个错开。 */
.journey-kids { inset: 0; }
.journey-inner .journey-kid {
  left: var(--x);
  top: var(--y);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  transform: translate(calc(-50% + var(--dx0, 0px)), calc(-50% + var(--dy0, 0px))) scale(.4);
  opacity: 0;
  pointer-events: none;
  transition: transform var(--move, .5s) cubic-bezier(.2, .85, .25, 1) var(--d, 0ms),
              opacity var(--fade, .34s) var(--d, 0ms);
}
.journey-inner.open .journey-kid {
  opacity: 1;
  pointer-events: auto;
  transform: translate(-50%, -50%) scale(1);
}
.journey-kid-disc {
  box-sizing: border-box;
  display: grid;
  place-items: center;
  width: var(--kid-disc, 74px);
  height: var(--kid-disc, 74px);
  padding: 6px;
  border-radius: 50%;
  /* 和阶段节点同一套填色：暖橙底 + 白字 + 白边。层级由**大小和位置**承担
     （父节点更大、在正中，还多一圈进度弧），不靠颜色分。 */
  border: 1px solid #fff;
  background: var(--node);
  color: #fff;
  font: inherit;
  font-size: var(--kid-font, 13px);
  font-weight: 700;
  line-height: 1.15;
  text-align: center;
  cursor: pointer;
}
.journey-kid-note { font-size: calc(var(--kid-font, 13px) * .8); }
/* 还没做的照样出现在这条路上，但标着「规划中」——圆盘不填色，边框改虚线 */
.journey-kid[data-planned] .journey-kid-disc {
  background: transparent;
  color: #8b8577;
  border-style: dashed;
}
.journey-kid-note { color: #8b8577; white-space: nowrap; }
.journey-kid-disc:focus-visible { outline: 2px solid #6d8a5a; outline-offset: 3px; }

/* 顺序箭头：每两个圆盘中间的空档里一个紧挨的「》》，指向下一个。它只是标注，
   收起时比子节点先走一步（.3s 对镜头的 .85s），不拖在后面抢注意力。 */
.journey-links { inset: 0; width: 100%; height: 100%; opacity: 0; pointer-events: none;
  transition: opacity .3s var(--ld, 0ms); }
.journey-inner.open .journey-links { opacity: 1; }
.journey-chev { fill: none; stroke: rgba(190, 160, 116, .9); stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }


/* ── 阶段进度：圆盘外一圈弧 ─────────────────────────────────────────────
   回答「这个阶段做到哪了」。底下一圈淡轨道**常驻**——只画弧的话，0% 和「这个阶段
   没有进度可言」长得一模一样（这正是常驻轨道的用处：看得出这里有个容器）。
   conic-gradient 从正上方起、顺时针涨；mask 把它掏成一根 3.5px 的环。

   颜色：画满的那段＝圆盘深一号的同色（#b06f34），轨道＝近白（rgba(255,255,255,.55)）。

   **已知的一处没解决**：这段弧和圆盘是同色系、只深一档（对圆盘 1.40:1），而
   inset:-5px 时弧的内沿几乎贴着圆盘那道 2px 白边（实际缝隙 ≈ 1px，环还压进边框一点），
   眼睛仍会把它归给圆盘。试过的两条出路都退回来了：

     - 改成绿色（#758464）——颜色不对味
     - inset 挪到 -8 / -11（缝隙 2px / 5px）——拉开了，但最后还是收回了原样

   inset 是相对 **padding 盒**算的（圆盘的 2px 边框不算在内），所以
   实际缝隙 = |inset| - 2 - 环宽。 */
.journey-node[data-progress] .journey-node-circle::after {
  content: ""; position: absolute; inset: -6px; border-radius: 50%;
  background: conic-gradient(#b06f34 calc(var(--p, 0) * 1%), rgba(255, 255, 255, .55) 0);
  -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 4px), #000 calc(100% - 3.5px));
  mask: radial-gradient(farthest-side, transparent calc(100% - 4px), #000 calc(100% - 3.5px));
  pointer-events: none;
}

/* 回答「你走到哪一阶段了」：全程只有一颗。
   **必须是一层散开的光，不能是实心环**——`0 0 0 7px` 那种写法和上面那圈进度弧叠起来
   就是两圈同心圆，读起来是「这颗被选中了」。加模糊就散成光晕，和弧分得开。
   顺带把原来那颗的投影一起写上：这条规则权重相同、位置在后，会把上面那条盖掉。 */
.journey-node.current .journey-node-circle {
  box-shadow: 0 2px 6px #58452b18, 0 0 18px 2px rgba(207, 135, 73, .5);
}

/* 被点开的那颗阶段节点**留在原地**：它就是「放大」的那个节点本身，圆盘和名字都不藏。
   只把点击让出去——它已经开着，再点它应该落到下面的空白判定上，也就是收起来。 */
.journey-node.host { pointer-events: none; }

@media (prefers-reduced-motion: reduce) {
  .journey-node-wave { animation: none; transform: translateX(-25%); }
  .journey-node-water-body { transition: none; }
}

/* ── 页面之间的切换：子页从右侧滑入，盖在地图上 ──────────────────────────
   **只有上来的那一层动，下面那层不动**——是「盖上去」不是「推出去」。
   （早先做成两层反向移动，看着像整屏在推；用户要的是地图留在原地、子页盖上来。）

   纯 CSS，没有一行 JS。两件以前得手写定时器才能做到的事，现在由规范负责：

     - `@starting-style` 给出**入场前的样子**。没有它，元素从 display:none 变成
       display:flex 时没有「上一个值」可以过渡，会直接跳到位。
     - `display` 配 `allow-discrete` 让隐藏**推迟到过渡结束**。没有它，hidden 一加上
       元素立刻消失——滑出的那层会当场消失，被盖住的那层也来不及等别人盖严实。

   这么做的另一个好处：不用去改三十多处 `el.hidden = ...`，以后新加的层只要带上
   .page（以便绕开全局的 [hidden]{display:none!important}）再进下面任一列表，
   就自动有动画。 */

/* 会滑动的那一层：从右边的屏幕外进来，收起时滑回右边去。 */
#recognize, #confirm, #design-chat, #journey-shell, #journey-bar {
  transform: translateX(0);
  /* cubic-bezier(.32,.72,0,1)：早先用的是 (.2,.8,.3,1)，前 15% 的时间就走完一半
     路程，位移几乎全挤在开头，看着是「跳」不是「滑」。这条起步慢、尾巴长。 */
  transition: transform .6s cubic-bezier(.32, .72, 0, 1),
              display .6s allow-discrete;
}
/* 收起来＝滑到屏幕右侧外。这条选择器必须能盖过各元素自己的 display
   （#confirm 是 flex、#journey-shell 是块），所以逐个写 id，不靠 .page[hidden]——
   那个权重比 id 低，会输。 */
#recognize[hidden], #confirm[hidden], #design-chat[hidden],
#journey-shell[hidden], #journey-bar[hidden] {
  display: none;
  transform: translateX(100%);
}
@starting-style {
  #recognize:not([hidden]), #confirm:not([hidden]), #design-chat:not([hidden]),
  #journey-shell:not([hidden]), #journey-bar:not([hidden]) {
    transform: translateX(100%);
  }
}

/* 被盖住的那一层：**自己不动**，只把隐藏推迟到别人盖严实之后。
   没有 transform 的变化，推迟靠 display 那条过渡自己的时长。

   `position:absolute` 不是可选项：`.app` 是纵向 flex，而推迟隐藏意味着那 .6 秒里
   地图和刚进来的那一层会**同时占位、平分高度**——地图被压成半屏，而 fit() 恰好在这
   期间按那个被压扁的高度算过一次缩放，于是回到地图之后它一直是缩着的。脱离文档流
   之后它铺满整个 .app，不再跟别人抢高度。 */
#intro {
  position: absolute;
  inset: 0;
}
#intro, #journey-scroll {
  transition: display .6s allow-discrete;
}
#intro[hidden], #journey-scroll[hidden] {
  display: none;
}

@media (prefers-reduced-motion: reduce) {
  /* 减弱动态效果下不做**整屏滑动**——整屏位移正是会引发前庭不适的那一类运动。
     但也别一刀切成硬切：换成淡入淡出，用户仍然看得到「换了一层」，只是没有位移。

     @starting-style 也要重写：基础那条给的是 translateX(100%)，不覆盖掉的话
     减弱动态效果下依然会瞬移一下。 */
  #recognize, #confirm, #design-chat, #journey-shell, #journey-bar {
    transform: none;
    transition: opacity .3s ease, display .3s allow-discrete;
  }
  #recognize[hidden], #confirm[hidden], #design-chat[hidden],
  #journey-shell[hidden], #journey-bar[hidden] {
    transform: none;
    opacity: 0;
  }
  @starting-style {
    #recognize:not([hidden]), #confirm:not([hidden]), #design-chat:not([hidden]),
    #journey-shell:not([hidden]), #journey-bar:not([hidden]) {
      transform: none;
      opacity: 0;
    }
  }
}

/* 首屏不做入场动画。
   页面一开始就在那儿，没有「从哪来」——不加这条，地图每次加载都会从右边滑进来
   （@starting-style 在元素首次渲染时同样生效）。
   这不只是好不好看：实测它毁掉三条钉叶知秋坐标的回归——那些用例做的是「量坐标 →
   操作 → 再量」，而滑动期间她的坐标一直在变，前后对不上。
   .no-slide 由 app.js 在启动后摘掉，之后正常的切页动画照常。 */
.app.no-slide #intro,
.app.no-slide #journey-scroll,
.app.no-slide #recognize,
.app.no-slide #confirm,
.app.no-slide #design-chat {
  transition: none;
}
/* 这里列的是**层**：页内的标题栏与返回按钮跟着页一起出生，不用逐个再列一遍。
   （返回按钮曾经是全局一层，就因为没有列进来，刷新时页面直接就位、只有它从右边滑进来。） */

/* ============ 只读预览页（/s/<slug>） ============
   它是同一个 SPA 的另一个入口：共用这套样式，但不启动主应用。所以这一节自成一体，
   不依赖任何主应用的类名——将来主应用改了版式，这一页也不会跟着散架。 */
#share-preview { min-height: 100dvh; background: var(--bg); }
.share-page { max-width: 640px; margin: 0 auto; padding: 24px 16px calc(32px + env(safe-area-inset-bottom, 0px)); display: grid; gap: 18px; }
.share-heading h1 { margin: 4px 0 6px; font-size: 22px; color: var(--ink); }
.share-from { margin: 0; font-size: 12.5px; color: var(--ink-2); }
.share-note { margin: 8px 0 0; font-size: 12px; line-height: 1.7; color: var(--ink-2); }
.share-foot { margin-top: 4px; }
.share-block { display: grid; gap: 6px; }
.share-block h2 { margin: 0; font-size: 14px; color: var(--ink); }
.share-block .house-row-thumb { width: 100%; height: 180px; border: 1px solid var(--line); border-radius: 12px; background: var(--surface); }
.share-step { border: 1px solid var(--line); border-radius: 12px; background: var(--surface); padding: 10px 12px; }
.share-step summary { display: flex; justify-content: space-between; gap: 10px; cursor: pointer; font-size: 13px; color: var(--ink); }
.share-step summary span { color: var(--ink-2); font-size: 12px; }
.share-rows { width: 100%; margin-top: 8px; border-collapse: collapse; font-size: 12px; color: var(--ink-2); }
.share-rows td { padding: 4px 0; border-top: 1px dashed var(--line); }
.share-rows td:last-child { text-align: right; white-space: nowrap; }
/* 链接那一行本身是给用户长按选中的（剪贴板 API 在非安全上下文里不可用），
   所以它长得像一段可以选中的代码，而不是一句说明。 */
/* 差异视图：一处一行，左"现在"右"换成"。色块让人一眼看出是颜色，而不是一串十六进制。 */
.diff-block { display: grid; gap: 6px; margin-top: 14px; }
.diff-block h2 { margin: 0; font-size: 14px; color: var(--ink); }
.diff-note { display: block; margin-top: 2px; font-weight: 400; font-size: 11.5px; color: var(--accent-strong); }
/* 那一行里的入口：它是一颗**按钮**，但长得像那行的一部分——用户扫的是"方案来源"，
   不是"这里有个可以点的东西"。 */
.diff-open { margin-left: 8px; padding: 3px 8px; border: 1px solid var(--accent); border-radius: 999px; background: transparent; color: var(--accent-strong); font: inherit; font-size: 11.5px; cursor: pointer; }
.diff-chip { display: inline-block; width: 12px; height: 12px; margin-right: 4px; border-radius: 3px; border: 1px solid var(--line-2); vertical-align: -1px; }
.share-url { margin: 10px 0; padding: 12px; border: 1px dashed var(--line-2); border-radius: 12px; background: var(--surface); font: 12.5px/1.6 ui-monospace, monospace; color: var(--ink); word-break: break-all; user-select: all; }
/* 指令面板上的待办数字：需求卡那一行才可能有。它是这一份清单**唯一的露面处**
   （待办的家在需求卡那张表里，这里只报个数）。 */
.assistant-command-badge { display:inline-block; margin-left:6px; min-width:17px; padding:0 5px; border-radius:999px; background:var(--amber); color:#fff; font-size:11px; font-style:normal; font-weight:700; line-height:17px; text-align:center; }
/* 需求卡的待办：哪一格上有没谈拢的要求，以及每一条的来龙去脉。
   它是这一份清单的**家**——指令面板上只报个数字。 */
.brief-mark { display:inline-block; width:7px; height:7px; margin-left:6px; border-radius:50%; background:var(--amber); vertical-align:1px; }
.brief-todos { margin-top:16px; }
.brief-todos h3 { margin:0 0 8px; font-size:13.5px; color:var(--ink); }
.brief-todos ul { display:grid; gap:8px; margin:0; padding:0; list-style:none; }
.brief-todo { display:grid; gap:2px; padding:10px 12px; border:1px solid var(--line); border-left:3px solid var(--line-2); border-radius:10px; background:var(--surface); }
.brief-todo.is-conflict { border-left-color:var(--danger); }
.brief-todo.is-missing { border-left-color:var(--amber); }
.brief-todo.is-met { border-left-color:var(--accent); }
.brief-todo.is-pending { border-left-color:var(--line-2); }
.brief-todo-text { font-size:13px; color:var(--ink); }
.brief-todo-note { font-size:11.5px; line-height:1.6; color:var(--ink-2); }
.brief-todo-where { font-size:11px; color:var(--ph); }
.brief-todo-drop { justify-self:start; margin-top:4px; padding:4px 10px; border:1px solid var(--line-2); border-radius:999px; background:var(--surface); color:var(--ink-2); font:inherit; font-size:11.5px; cursor:pointer; }
/* ============ 施工图 ============
   一页纸、一列到底。**它不是工作页**——只有返回（与第 4 批的打印），
   上面能改的东西一个都没有。 */
/* 三格轨道：上一张 · 当前 · 下一张。**并排**（ViewPager 那一套）——拖动时邻页真的跟着进来，
   而不是"当前这张在往空白里挪"。横向溢出裁掉（纵向滚动照旧），平移由脚本控制。 */
/* 三格**等宽等高**。⚠ 宽度必须三格一个样（当前那张也一样）：只给 `.is-side` 定宽的话，
   翻页时两边角色互换、新当前那张**丢掉这个类**，它的宽度就会从"一格"变回"内容宽"
   ——那就是"滑动完成时明显变小"的那一下抖动（2026-09-29 用户报的）。
   高度由轨道给：除标题栏与芯片行之外**占满剩余屏幕**，四边留距由 .drawing-scroll 的内边距管。 */
/* 轨道：三格 = 三个**布局槽**，槽里才是纸卡。分成两层是为了**留缝**：
   缝只能加在槽上（`padding`），不能加在纸上——纸的宽度正是那套 `1/3` 百分比数学依赖的东西。
   高度**撑满剩余空间**（用户 2026-09-29：占满标题栏、芯片栏之外的地方，四周留边距）。
   ⚠ 而"别出滚动条"靠的是**把高度算准**，不是"给个小高度"：那一次我把用户说的
   「card 高度太大」读成了「高度要给小一点」，结果越改越不对——**太大不是因为它占满了，
   是因为它占的是窗口而不是容器**（下一段）。 */
/* 高度**问容器要**（`100%`），不自己算。
   ⚠ 这里原来写的是 `calc(100dvh - 标题栏 - 芯片行 - 安全区)` —— **按窗口算**。它在手机上恰好对，
   因为 `.app` 在窄屏就是一屏；而桌面下 `.app` 是 `min(860px, 100vh - 140px)`，**比窗口矮 140px**，
   于是这张纸凭空高出 142px、`.drawing-scroll` 滚起来（2026-09-29 用户两次报"滚动条还在"，
   我第一次量在 390×844 上，`over 0`，就以为好了——**量在了不出问题的那个尺寸上**）。
   `100%` 的参照是滚动区的**内容盒**（它 `inset:0`，高度确定），所以
   `纸 + 上内边距 + 下内边距 = 滚动区`，**恒等**：不管窗口多高、`.app` 是不是一屏，都差不出一个像素。
   高度确定，纸卡里的 `flex:1`（见 .drawing-plan）才能把图压到装得下。 */
.drawing-track { display:flex; width:300%; height:100%; }
.drawing-cell { flex:0 0 33.3333%; padding:0 7px; box-sizing:border-box; }
.drawing-paper { height:100%; display:flex; flex-direction:column; }
/* ⚠ **两张纸的底色一样**（用户 2026-09-29：离屏那张看着更淡，跟当前这张不一致）。
   以前是靠 `opacity` 把整张离屏纸**连底色一起**压淡的——底色于是不同（#fff vs 压出来的灰）。
   现在底色两边取同一个值：**"白纸以 55% 压在滚动区底色上"的那个观感**。
   用 `color-mix` 现算而不是写死十六进制：`--shell-bg` 在深色模式下是另一个值
   （:root 那三条），写死的话深色下两边立刻又不一样。 */
.drawing-paper { background:color-mix(in srgb, #fff 55%, var(--shell-bg)); }
/* 到头那一侧没有邻页：位置得占住（三格等宽，否则当前这张会跑偏），但不能露一张白卡片 */
.drawing-paper.is-empty { background:transparent; box-shadow:none; }
/* ⚠ **离屏那两张既不压淡、也不 disable**（用户 2026-09-29，紧接上一条："不要压淡、
   不要 disable 离屏页内容"）。所以 `.is-side` 今天**一条样式都不带** —— 它是"这是邻页那张"
   的语义标记（那两张上也带 `aria-hidden="true"`），留在 HTML 里是为了下次要调时有落点，
   而不是死代码：主次由**位置**（当前那张在正中）与**芯片行**（哪一枚选中）承担，
   不靠把邻页弄灰。 */
.drawing-scroll { position:absolute; inset:0; overflow-y:auto; overflow-x:hidden; overscroll-behavior:contain; padding:calc(var(--content-top) + var(--chips-gap) + var(--chips-h) + var(--chips-lead)) 14px calc(24px + env(safe-area-inset-bottom, 0px)); background:var(--shell-bg); }
/* 滑动换图（ViewPager 那一套）：拖动时摘掉过渡＝1:1 跟手，松手滑到位；
   到头是橡皮筋（脚本给的阻尼），不是"不动"。 */
.drawing-track { transition: transform .26s cubic-bezier(.2,.85,.25,1); will-change:transform; }
.drawing-track.is-dragging { transition: none; }
@media (prefers-reduced-motion: reduce) { .drawing-track { transition: none; } .sheet-chip { transition: none; } }
.drawing-paper { max-width:820px; margin:0 auto; padding:20px 18px 26px; border-radius:12px; box-shadow:0 1px 3px #26241f14; }
.drawing-paper h1 { margin:0 0 10px; font-size:20px; color:var(--ink); }
.drawing-paper h2 { margin:22px 0 8px; font-size:15px; color:var(--ink); }
/* 只有一句话的那张纸（占位、或还没房间）：**整块居中**（用户 2026-09-29）——
   一张空纸上一行字贴在顶上，看着像没加载完。 */
.drawing-paper.is-single { display:grid; place-content:center; justify-items:center; gap:6px; text-align:center; }
.drawing-warn { margin:8px 0 0; padding:8px 10px; border-left:3px solid var(--amber); border-radius:0 8px 8px 0; background:#fdf6ec; font-size:12px; line-height:1.75; color:#7a5320; }
.drawing-note { margin:8px 0 0; font-size:12px; line-height:1.7; color:var(--ink-2); }
/* 图自己缩到装得下：`flex:1` 拿到剩余高度，`min-height:0` 允许它缩到比内容还小；
   viewBox 的 preserveAspectRatio（默认 xMidYMid meet）会**等比**缩放——不会被压扁。 */
.drawing-plan { display:block; width:100%; flex:1 1 auto; min-height:0; margin:8px 0 4px; }
/* 图的颜色只用两种：**打印成黑白也要分得开**。外轮廓粗、内墙细 */
/* ⚠ **这里一个尺寸数都不许写**（`font-size` / `stroke-width` / `stroke-dasharray` 都归
   `ui/drawing.js` 的 `planSVG` 给）：CSS 里的 `px` 在 SVG 里是**用户单位**，而这张图的
   viewBox 宽一万多（毫米），缩到纸上是 0.03 倍——写 `12px` 就是屏幕上 0.35 像素，
   **画了但看不见**（2026-09-29 用户报"看上去没跟户型图匹配"，一半原因在此）。
   这一层只管颜色与对齐。 */
.d-rooms rect { fill:#f2f5f1; stroke:#e4e9e2; }
.d-walls line { stroke:#26241f; stroke-linecap:square; }
.d-walls .d-outline { stroke:#26241f; }
.d-walls .d-inner { stroke:#8b8577; }
.d-skirting line { stroke:var(--amber); stroke-linecap:round; opacity:.55; }
.d-openings circle { fill:#fff; stroke:#26241f; }
.d-openings circle.d-guess { fill:#fdf6ec; stroke:var(--amber); }
.d-openings text { fill:#26241f; }
.d-names text { font-weight:700; fill:#3f7c5c; text-anchor:middle; dominant-baseline:middle; }
.d-dim { stroke:#8b8577; }
.d-dims text { fill:#8b8577; text-anchor:middle; }
.d-noscale { fill:var(--amber); text-anchor:middle; }
/* 打印按钮：它是这一页唯一的动作，放在标题栏右侧 */
/* 图纸切换：一枚芯片一张图。**能画的和还没画的都在**——藏起来的话用户不知道这套图
   本来该有什么。还没画的用虚线圈，与地图上「规划中」那个节点同一套视觉语言。 */
/* 芯片区让出多高，由这三条**一起**说了算：滚动区的上内边距就是它们的和。
   ⚠ 这三条是 2026-09-29 从一次"压住了"里长出来的：那时上内边距硬编码 `40`、
   而行高是量出来的 `42`——**两个数各写各的，压了纸卡 2 像素**，肉眼看不出来。
   现在行高是**被 `height` 钉住的那个值**（`--chips-h`），不是"大概是这么多"：
   芯片真长高了它会**溢出来**（看得见），而不是悄悄压到纸上（看不见）。 */
#drawing { --chips-gap: 12px; /* 距标题栏（用户 2026-09-29：加大，原来是 0） */ --chips-h: 42px; /* 芯片行本身，含它的下内边距 */ --chips-lead: 8px; /* 行底到纸面 */ }
/* 这一张纸的编号（`#drawing-doc-id`）。挂在标题栏里、标题右边：**不上纸**——
   纸只展示图纸是用户 2026-09-29 定的。淡一档，它是一句"这是哪一版"，不是标题。 */
.drawing-doc-id { flex:0 0 auto; margin-right:8px; font-size:11.5px; color:var(--ink-2); letter-spacing:.02em; white-space:nowrap; }
.drawing-sheets { position:absolute; top:calc(var(--content-top) + var(--chips-gap)); box-sizing:border-box; height:var(--chips-h); left:0; right:0; z-index:5;
  display:flex; gap:6px; padding:0 12px 10px; overflow-x:auto; -webkit-overflow-scrolling:touch; scrollbar-width:none; }
.drawing-sheets::-webkit-scrollbar { display:none; }
/* ⚠ 高亮的切换**要有个短暂的过渡**（用户 2026-09-29：「不要瞬间切换选中态」）：
   0.26s 是卡片那一下位移的同一个拍子（.drawing-track 的过渡），两处一起看才像一个动作。 */
.sheet-chip { flex:0 0 auto; padding:5px 11px; border:1px solid var(--line-2); border-radius:999px; background:var(--surface-2); color:var(--ink-2); font:inherit; font-size:12.5px; cursor:pointer; transition:background-color .26s, border-color .26s, color .26s; }
.sheet-chip.is-on { border-color:var(--accent); background:var(--accent); color:#fff; font-weight:700; }
.sheet-chip[data-todo] { border-style:dashed; }
/* 画不出来的那张：不摆空框，就地说清缺什么 */
.d-missing h2 { margin:0 0 8px; font-size:15px; color:var(--ink-2); }
.d-missing .material-note { margin:0 0 6px; }
/* 打印按钮：一颗**图标**按钮（用户 2026-09-29：原来是「打印 / 存 PDF」四个字）。
   ⚠ 同一天用户又拍了「**去掉功能按钮的圆形样式、仅保留 icon**」，所以底色、描边、圆角
   全部撤掉——**只剩那颗打印机**。它是这一页唯一的动作，改成靠**颜色**（主色）显眼，
   不靠一个实心圆。盒子仍是 `--bar-tap`：触点不许跟着一起缩。
   ⚠ **可见文字一没，它就只剩 `aria-label` 这一个名字**（`aria-label="打印 / 存 PDF"`），
   掉了就是个无名按钮，而屏幕上一点都看不出来。`drawing.spec.js` 用 `toHaveAccessibleName`
   钉着这一条。禁用时 `color` 变灰，图标走 `currentColor` 跟着变（见 :disabled）。 */
/* ⚠ 标题栏右侧那**两颗**图标按钮（导出 DXF、打印）走**同一条规则**：尺寸只在 `--bar-tap` /
   `--bar-icon` 两处定义。加第二颗时**不要另写一份像素** —— 这个文件里"同一件事写两遍"
   今天已经犯过四次（返回的定位、house-bar 的间距、返回的圆角、打印禁用态的底色）。 */
/* 效果图那颗漫游钮：**在标题栏下方**（用户 2026-10-01；它前一天刚被搬进栏里，
   这一天又挪到栏下）。长相与大小 = 页内那套圆形图标钮（`.icon-btn`，与户型图页的撤销/原图
   一样大），这里只剩**挂哪儿**：栏高（`--content-top`）+ 8px 的缝，贴右边。 */
.view-mode-toggle { position:absolute; top:calc(var(--content-top) + 8px); right:var(--fab-right); z-index:8; }

/* 栏里那颗图标按钮的几何：**一条规则管所有颗**（打印、设置）。尺寸的出处仍是 `.app` 上
   的 `--bar-*`，所以这里一个像素数都不写。加下一颗时挂 `.bar-icon-btn` 就行，别再抄一遍。
   ⚠ `.nav-back`（返回箭头）比这条早，几何还写在自己身上；下次动它时并过来。 */
.bar-icon-btn { flex:0 0 auto; box-sizing:border-box; width:var(--bar-tap); height:var(--bar-tap); min-width:var(--bar-tap); padding:0; display:grid; place-items:center; border:0; background:none; color:var(--accent); cursor:pointer; }
/* 地图那颗设置齿轮**固定用橙色**（用户 2026-10-10：「改成橙色主题色」）。
   ⚠ 是固定，不是跟随 `--accent`：换主题色它不跟着换。用的是现成的 `--amber`
   （浅色 #c07b3a / 深色 #d9a05c，同一族两支），**不新造色号**。 */
#house-bar-settings { color:var(--amber); }
.bar-icon-btn svg { display:block; width:var(--bar-icon); height:var(--bar-icon); }
/* 禁用态**也只改颜色**（用户 2026-09-29：「去掉打印 icon 的背景」）——
   这里原来还写着 `background:var(--warm)`，于是"按钮的底色撤掉了"这件事在**禁用时**
   并没生效：未确认的方案下这页正是禁用的，看到的还是一个灰底块。图标走 `currentColor`，
   一条 `color` 就够说清"按不动"。 */
/* 打印那颗现在只剩**禁用态**（几何与颜色都归 `.bar-icon-btn`）——禁用态只改颜色，
   理由见下面打印那段的注释 */
.drawing-print:disabled { color:var(--ink-2); cursor:not-allowed; }
/* ⚠ 未确认时的水印：**它才是那条口径真正的落点**——`window.print()` 是浏览器自己的
   命令，App 拦不住；禁用按钮挡不住截图，也挡不住 Ctrl+P。水印跟着任何一份拷贝走。 */
.drawing-watermark { position:relative; margin:-4px 0 12px; padding:10px 12px; border:2px dashed var(--danger); border-radius:10px; color:var(--danger); font-size:13.5px; font-weight:700; text-align:center; letter-spacing:.5px; }
.drawing-watermark span::before { content:"⚠ "; }

/* ============ 打印 ============
   只留那张纸。顶栏、浮层、地图、3D 画布一律不印——**这一页印出来是给工长或自己看
   的纸**，屏幕上那些控件在纸上只是噪音。 */
@media print {
  body { background:#fff; }
  .drawing-scroll { position:static; overflow:visible; padding:0; background:#fff; }
  /* ⚠ 纸的底色在屏幕上是 `color-mix`（白纸压在滚动区底色上）——**纸上要纯白**：
     那不印出一张灰纸来。（浏览器默认不打印背景色，但"默认不打印"不是"印出来是白的"。） */
  .drawing-paper { max-width:none; margin:0; padding:0; box-shadow:none; border-radius:0; background:#fff; }
  /* 一张纸里不要有断在半截的图 */
  .drawing-plan { break-inside:avoid; }
  /* 屏幕上那些壳（地图、3D、弹层、浮标、标题栏）全不印 */
  .page-bar, .drawing-sheets, .fab-float, .chat-tools, .snackbar, .toast, .sheet, .sheet-mask, .chat-overlay,
  #journey-shell, #guide, #debug-panel, .cal-fab-wrap { display:none !important; }
}
/* 预览页那颗「收下这份装法」：它是这一页唯一的动作，也该是**唯一**显眼的 */
.share-take { display:grid; gap:8px; margin-top:18px; padding-top:16px; border-top:1px solid var(--line); }
.share-take button { padding:13px 12px; border:0; border-radius:12px; background:var(--accent); color:#fff; font:inherit; font-size:14px; font-weight:700; cursor:pointer; }
.share-take button:disabled { background:var(--warm); color:var(--ink-2); cursor:not-allowed; }
.share-done { margin:0; font-size:14px; color:var(--ink); }
.share-open { display:block; padding:12px; border:1px solid var(--accent); border-radius:12px; color:var(--accent-strong); font-weight:700; font-size:13.5px; text-align:center; text-decoration:none; }
.share-error { color:var(--danger); }
/* 「我分享过的」：`reno-shares-v1` 那张清单的家（撤回与血缘对比都要用它） */
.sent-shares { margin-top:18px; padding-top:14px; border-top:1px solid var(--line); }
.sent-shares h3 { margin:0 0 8px; font-size:13px; color:var(--ink); }
.sent-shares ul { display:grid; gap:6px; margin:0 0 8px; padding:0; list-style:none; }
.sent-share { display:flex; align-items:center; gap:8px; padding:8px 10px; border:1px solid var(--line); border-radius:10px; background:var(--surface); }
.sent-share.is-expired { opacity:.6; }
.sent-share-name { flex:1; min-width:0; font-size:13px; color:var(--ink); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.sent-share small { flex:0 0 auto; font-size:11px; color:var(--ink-2); }
.sent-share-revoke { flex:0 0 auto; padding:4px 10px; border:1px solid var(--line-2); border-radius:999px; background:var(--surface); color:var(--ink-2); font:inherit; font-size:11.5px; cursor:pointer; }

/* ===== 常驻编辑面板（用户 2026-09-30）=====
   在户型图**上方**，常驻（不需要开关）。2026-10-11 起**只有一排**了：分类那五颗搬去
   文件夹右侧当索引签（见下面的 `.cf-folder`），这一格从此就是"当前那一组的工具"。
   ⚠ 面板要**有高度**：按钮挤在一起既难点也难读。 */
.cf-edit {
  /* 右边让出签那一列：**面板与纸同宽、右边缘齐平**，签那一列从两者右侧探出去
     （用户 2026-10-11：「宽度对齐内容区」）—— 纸宽 ＝ 内容宽 − 列宽 ＋ 压边，
     这里照同一个式子取，所以两边永远对得上 ✓（改列宽不用两处各改一遍）。
     ⚠ 别退回 `margin-right: 0`：那样面板会盖在签那一列的头上，看着像两张不搭界的卡 ✗。 */
  margin: 12px calc(var(--cf-tab-w) - var(--cf-tab-overlap)) 0 0;
  /* 用户 2026-10-11：「减小操作区高度」——上下内边距 16 → 8（一行工具的最小高度
     `--cf-ctl-h + 24` 不动，所以这一格仍是"切组不跳"的那条保证 ✓）。 */
  padding: 8px 12px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--bg);
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 14px;
}
.cf-edit[hidden] { display: none; }
/* ⚠ `align-items` 2026-10-11 从 `flex-end` 改成 `center`（用户：「操作区内容没有纵向居中，
   顶部间距较大」）：`flex-end` 是当年给**签那一行**留的 —— 活动签的圆角要贴在下面这条
   工具行的上沿、熔成一体，所以芯片得靠下 ✓。签搬走之后这一格只剩工具行，内容一律被顶到
   56px 高的底边 ⇒ 上面白留 22px ✗（实测：上 30 / 下 8，一眼就是偏的）。
   现在居中：上下各 19/8+8，左右对齐不变 ✓。 */
.cf-edit-row { display:flex; flex-wrap:wrap; align-items:center; min-height:var(--cf-ctl-h); gap:10px; }
/* ⚠ 2026-10-11：这里的 `padding-right:84px`（给浮在右上角的撤销/重做让位）**撤了** ——
   那两颗搬去内容区右上角之后，面板里再没有绝对定位的东西，这一条就只剩"白留 84px" ✗。
   （更早那 84px 里还塞过五颗分类芯片，芯片同期也搬走了 ✓） */
.cf-edit-row { gap: 6px; }
.cf-edit-btn { display:inline-flex; align-items:center; box-sizing:border-box; min-height:var(--cf-ctl-h); padding:0 13px; border:1px solid var(--line-2); border-radius:999px; background:var(--surface-2); color:var(--ink); font:inherit; font-size:13px; cursor:pointer; }
.cf-edit-btn.is-on { border-color: var(--accent); background: var(--accent); color: #fff; font-weight: 700; }
.cf-edit-btn[hidden] { display: none; }
/* 行高**钉住**（用户 2026-10-04）：分组一切，这一格的高度就跳一下、下面的画布跟着上下窜。
   留出「一颗按钮 + 上下内边距」这个底，各组的行高就一样了（真有两行的还是会长高，不裁）。 */
/* ⚠ 那个 `margin-top:-14px`（把工具行贴到上面那排芯片底下、让活动芯片的圆角与它接成一体）
   跟着芯片一起撤了 —— 现在它是面板里**唯一**的一行，负边距只会把它顶进面板的上内边距里 ✗。
   ⚠ 2026-10-11 用户：「去掉操作区的内层嵌套」—— 这一格原来**自己是一张卡**（`--bg-2` 的底、
   10 圆角、12/10 内边距），套在面板那张卡里 ⇒ 屏幕上就是"卡里还有一张卡" ✗。
   现在只剩布局（底、圆角、内边距、那圈 border 全撤）⇒ 工具行直接落在面板这张卡上 ✓。
   ⚠ **`min-height` 留着**：它是"切组时这一格高度不变"的保证（真有两行的还是会长高，不裁 ✓），
   与长相无关。去掉内边距之后这一格矮了 24px —— 那份高度归纸（`.cf-folder` 是 `flex:1`）✓。 */
.cf-edit-tools { min-height:calc(var(--cf-ctl-h) + 24px); padding:0; justify-content:flex-start; }
/* 「定稿」那一组那一格的住户：**那句话 ＋ 两颗"活" ＋ 那颗圆圈对勾**（用户 2026-10-09）。
   别的组这儿是工具 ✓，它们顶上来，高度不变 ⇒ 下面内容区那一格不会窜 ✓。
   ⚠⚠ **这一格要占满**（用户 2026-10-16：「量房的确认按钮移至操作区右侧」✓）：原来它没有
   `width:100%` ⇒ 宽＝**内容宽** ✗ ⇒ 那颗 ✅ 只是"紧跟在那两颗活后面"（实测：
   一行从 33 起、内容到 249 就断了 ✗）—— 它是**这一行的出口**，该站在该站的那一头 ✓。
   ⚠ `margin-left:auto` 给的是"**这一行的最右**"（不是整屏的右边 ✓）—— 与别的组那一行同宽 ✓。 */
.cf-check-go-wrap { display: flex; align-items: center; gap: 10px; width: 100%; }
/* ⚠⚠ **那句话与那颗 ✅ 2026-10-16 搬到了内容区那张纸的四角**（用户：「把量房操作区的
   "请确认校准结果"放到内容区左上角，确认按钮放到内容区右上角」✓）——
   这一屏那张纸上，撤销/重做（左上）与那颗眼睛（右上）本来就收着 ✓ ⇒ 两个角正好空出来 ✓。
   ⚠ `.cf-check-go` 上一版那条 `margin-left:auto`（把那颗 ✅ 推到那一行的最右 ✓）跟着撤了 ✗
   —— 它现在不在那一行里了 ✓。 */
/* ⚠⚠ **那句话是一条标题栏**（用户 2026-10-16：「量房内容页的"请确认..."应视为标题栏，
   添加背景色」✓）：通栏一条、底色 ＋ 下边一道细线 ✓，圆角跟着那张纸（上面两个角 ✓）——
   换组时这一屏的高度不动 ✓，只是顶上多了一条横带 ✓。
   ⚠ 那颗 ✅ 就**站在标题栏里**（右上角 ✓）—— 与"标题栏右边那颗动作"是同一种写法 ✓。 */
.cf-check-lead {
  position: absolute; inset: 0 0 auto 0;
  display: flex; align-items: center; height: 40px; padding: 0 16px;
  z-index: 2;
  background: var(--surface-2);
  border-bottom: 1px solid var(--line);
  border-radius: 14px 14px 0 0;
  font-size: 13px; font-weight: 600; color: var(--ink);
}
.cf-check-go { position: absolute; top: 4px; right: 8px; z-index: 3; margin: 0; }

/* ============ 文件夹：左边那张纸 ＋ 右边一条竖签（用户 2026-10-11）============
   用户的原话：「内容区做成"文件夹"样式，标签在文件夹右侧（纵向排版）」「只需要看上去像
   一层层的就行；在不同层可以做不同的事；原图在最下层」。
   ⚠ 那句"看上去像一层层"**只能做在纸上**（这个框），不能做在图里：图里那几层
   （`#cf-g-base` → overlay → dims → measure → names）是同一个 SVG 里**对齐描**的 ——
   照片上描的墙歪 3px 就是错的 ⇒ 它们只改显隐与深浅，永不位移 ✓。
   （"原图在最下层"在 z 序上**本来就成立**：`#cf-g-base` 就是第一个 `<g>` ✓ 一行代码没动。）

   ⚠⚠ **宽度是这一改动的全部代价**：纸是 1:1（`aspect-ratio`），而这一格的宽度 = 屏宽 −
   这一页的页边距 ⇒ 右边这一列多宽，纸就窄多少（实测 390 宽的屏：38px 的列把纸从 350
   压到 316，线性 −9.7%）。所以：签里的字**竖排**、列宽钉在 `--cf-tab-w`、**别再往上加**。
   ⚠⚠ 也**别**用负外边距把这一列挪进页边距去讨回那 34px：手机右侧是 **Android 返回手势的
   感应区**（Android 10+ 左右两边都算），这条 20px 的页边距就是安全带。 */
.cf-folder {
  margin-top: 14px;
  display: flex;
  align-items: stretch;
  /* 内容区**一路拉到页面底部**（用户 2026-10-11：「内容区高度拉长到页面底部」）——
     `#confirm` 是纵向 flex，这一句把剩下的高度全给文件夹 ✓。
     ⚠ 纸因此**不是方的**了（宽 × 高）—— 图里那套坐标换算跟着改过，见 `cfPlanPoint`。 */
  flex: 1;
  min-height: 0;
  /* ⚠⚠ **下沿让开叶知秋那颗气泡**（用户 2026-10-16：「减小内容区高度，下沿应避让设计师气泡
     （内容按 2 行算）」✓）：气泡浮在头像**上方**（`.guide-bubble { bottom: calc(100% + gap) }` ✓），
     而头像停在左下角 ⇒ 从屏幕底往上数，占掉的那一条 ＝ 头像离底 ＋ 头像 ＋ 那道缝 ＋ 气泡高 ✓
     —— 那四个数都在 `:root` 上（改一处三处跟着动 ✓）。
     ⚠ 这是**常留**的一条：气泡不是常驻的（叶知秋说完就收 ✓），可"看图的时候它随时会冒出来"
     ⇒ 按它**在**的时候留位 ✓（不然一说话就压在图上 ✗）。
     ⚠ 页面自己那点内边距在文件夹**外面**，所以纸的下沿还会再高一点点 —— 那点是富余，不用扣 ✓。 */
  margin-bottom: calc(
    var(--guide-bottom) + var(--guide-face) + var(--guide-gap) + var(--guide-bubble-2l)
  );
}
/* ⚠⚠ **不看形象的时候，那一条不占位**（用户 2026-10-17：「隐藏设计师形象时拉长内容区长度、
   显示设计师形象时减小」✓）：上面那条留位是给"**随时会冒出来的气泡**"的 ✓ ——
   而「设计师辅助」一关（`:root[data-ai-assist="off"]` ✓，与藏形象、藏气泡是**同一件事** ✓，
   见上面对 `#guide` 那两条 ✓），气泡永远不会冒 ⇒ 那 165px 就是白占 ✗。
   ⇒ 关掉时把留位收回，纸照旧一路拉到页面底部（2026-10-11 那条老规矩 ✓）。
   ⚠ 判据取自那个属性本身（不另设开关 ✗）：辅助开着 = 形象与气泡都在 = 留位 ✓，
     一处判断，CSS 与 JS 说的是同一件事 ✓。 */
:root[data-ai-assist="off"] .cf-folder {
  margin-bottom: 0;
}
.cf-folder[hidden] { display: none; }
/* 纸那一格：画布与定稿表共用它（谁露出来谁定这一格的高度）✓ */
/* 纸那一层要**压得住签**（见 `.cf-tabs` 那段三层 z-index）⇒ 给它一个 z-index ✓。
   ⚠ 它本来就得是定位元素：右上角那颗眼睛（`#cf-tg-base`）是绝对定位在它身上的。 */
.cf-folder-main { position: relative; z-index: 1; flex: 1; min-width: 0; }

/* 签条：只是**一列**，自己**不画底**（用户 2026-10-11：「去掉整体的标签背景」）——
   签各自成颗，靠彼此之间的缝与外侧的边距分开 ✓。
   ⚠⚠ **签要"粘贴"在纸上**（用户 2026-10-11 第三次：「标签不是应该粘贴在图纸上吗？」）：
   `margin-left:-4px` 让整列**往纸上压 4px** —— 每颗签的左边那一小条因此画在**纸上面**，
   看着就是"贴上去的"，而不是"飘在旁边的一列" ✓。
   ⚠ 这 4px 同时是**不需要还的**：纸因此从 306 回到 310（列占 44、压回来 4 ✓）。
   ⚠⚠ **压住的层级全在 z-index 上，三处缺一不可**（用户第二次：「未选中的标签应该被
   图纸压住，也就是标签左侧会被裁掉一条」）：
     · `.cf-folder-main`（纸）`z-index:1` —— 定位元素本来就画在静态元素上面；
     · 未选中的签**不提 z-index** ⇒ 左边那 4px 落在纸底下、被裁掉一条 ✓（就是"被压住"）；
     · 选中那颗 `z-index:2` ⇒ 反过来压在纸上 —— "这一张是从底下**抽出来**的" ✓。
   ⚠ `z-index` 只能在同一层叠上下文里比大小 ⇒ `.cf-tabs` **自己不能带 z-index**
   （一带就自成一个上下文，里面那颗 `z-index:2` 再也够不着纸了 ✗）。
   ⚠ 上下留白：**上头 56**（用户 2026-10-11：「标签整体下移、位于眼睛/undo/redo 行之下」）——
   那一行是 `top:10px` ＋ 32px 高的圆钮 ⇒ 到 42 为止，56 让签堆从它们**下面**起 ✓
   （顺带解掉原来"重做那颗和第一颗签并排、只隔几像素"的挤）。下头 14：不顶到纸的下角 ✓。 */
.cf-tabs {
  flex: 0 0 var(--cf-tab-w);
  display: flex;
  flex-direction: column;
  gap: 5px;
  /* 右侧 1px（用户 2026-10-11：「标签右侧边距减小一半」→ 2 的一半）。
     ⚠ 签本身的宽度（32）没动 —— 收掉的是它右边的空 ⇒ 整列 34 → 33，那 1px 还给纸 ✓。 */
  padding: 56px 1px 14px 0;
  margin-left: calc(-1 * var(--cf-tab-overlap));
  box-sizing: border-box;
}
/* 一颗签＝原来那五颗 `.cal-item[data-group]`（**按钮本体与 `data-group` 一字未动**，
   只换了地方与长相 ⇒ `applyCalGroup` 只改了一处选择器 ✓）。
   ⚠ **长相不再自己写**：它就是工程里那颗**竖排的芯片** —— 描边、底色、未选中/选中的样子
   全部来自 `.sheet-chip` / `.sheet-chip.is-on` 那两条（与图纸页那条芯片行同一套 ✓，
   用户第一条要的"未选中也展示轮廓"就是它自带的 ✓）。这里只改**几何**：竖排、圆角、
   内边距、字号 —— 所以下头一个字都别碰 `background` / `border-color` / `color`
   （一碰就会把 `.is-on` 那颗实心盖掉：两条特异性都是 (0,2,0)，而这一条在后面 ✗）。
   ⚠ 竖排走 `writing-mode`（两字标签用换行硬凑，遇到长标签会散架 ✗）。
   ⚠⚠ 竖排之后**主轴变成了纵向** ⇒ `justify-content` 管上下、`align-items` 管左右。
   ⚠ 高度**钉住**（`flex:0 1 auto` ＋ `height:64px`）：这一列跟纸一样高（一路到页面底部），
   平分的话五颗会各撑到一百多像素、变成五块大板 ✗ —— 签就该是签那个大小 ✓。
   2026-10-11 用户要"增高减宽" ⇒ **32×64**（原来 38×52）✓。
   `flex-shrink:1` ＋ `min-height:44px` 是给矮屏留的余量：地方不够时它们一起缩，不会溢出去 ✗。 */
.cf-tabs .cal-item {
  writing-mode: vertical-rl;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 1 auto;
  height: 64px;
  min-height: 44px;
  /* ⚠ **要定位**：那颗角标（`.cf-tab-badge`）是绝对定位挂在这颗签的角上的 ✓。
     这里只加 `position`、**不许顺手加 `z-index`** ✗ —— 未选中的签必须留在纸底下
     （左边那 4px 要被纸裁掉一条，见 `.cf-tabs` 那段的三层说明 ✓）。
     不带 z-index 的定位元素照样画在 `z-index:1` 的纸**下面** ✓（同一层叠上下文里比大小 ✓）。 */
  position: relative;
  /* ⚠⚠ `line-height` **必须钉住**：竖排（`writing-mode:vertical-rl`）之后它是**横向**的
     字行宽度（十字轴尺寸），继承来的行高（1.6 上下）会把每颗签撑到 35 宽 —— 而签是
     flex 子项、`min-width:auto` 认的就是这个内容宽度 ⇒ 整列跟着涨到 41，`--cf-tab-w`
     说好的 38 根本不生效 ✗（2026-10-11 实测踩到：想减宽减不动）。
     钉成 1.2 之后字行 ≈16px，加上内边距与描边 = 26 ≤ 32 ✓ 列宽才真的听 `--cf-tab-w` ✓。 */
  line-height: 1.2;
  padding: 8px 4px;
  /* 左侧两个角是**直角**（用户 2026-10-11 第一条）：那一侧要么被纸压着、要么压在纸上，
     圆角在那儿只会显得"没对齐" ✗。 */
  border-radius: 0 8px 8px 0;
  font-size: 13px;
  letter-spacing: 1px;
}
/* 当下这一颗：**从底下抽出来**（压在纸上）——见 `.cf-tabs` 那段里的三层 z-index ✓。 */
.cf-tabs .cal-item.is-on {
  position: relative;
  z-index: 2;
}
/* **角标**这一族的长相（现在两个用户：账本那两颗「活」的量墙 / 量门窗 ✓ ——
   「量房」**签上**那颗 2026-10-16 撤了 ✓，用户：「去掉量房便签右上角的 badge」✓）：
   还没量的段数（量完是 ✓、一段没量不挂 ✓）。
   ⚠⚠ **三件事，缺一样就画歪**：
   · `writing-mode: horizontal-tb` —— 它继承签的**竖排**（`vertical-rl`）⇒ 不掰回来的话
     两位数会**竖着码成一列** ✗（这一处最阴：`12` 会变成上面一个 1、下面一个 2，看着像个怪符号）；
   · 绝对定位在角上（`top/right` 都是**物理**方向 ⇒ 不受签的竖排影响 ✓）—— 一个像素都不占宽度 ✓
     （签里塞字会把整列撑宽、纸跟着变窄 ✗，见 `--cf-tab-w` 那条）；
   · `[hidden]` 要自己兜（下面那条 ✓）—— 这条规则写了 `display:grid` ⇒ 它会盖掉浏览器默认的
     `[hidden]{display:none}` ✗（工程里踩过好几次的老坑 ✓）。
   长相与纸上那块饼的 ✎ **同一套**（浅底 ＋ 描边 ＋ 圆角），底色取**量房那个橙**（`--node`，
   与饼、与墙上那颗号同一个 ✓）；数写 `--ink` 不写橙色 —— 10px 的橙字在浅底上太淡（对比度 ~2.9）✗。 */
.cf-tab-badge {
  position: absolute;
  top: -5px;
  right: -5px;
  display: grid;
  place-items: center;
  box-sizing: border-box;
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  border: 1px solid var(--node);
  border-radius: 999px;
  background: var(--surface);
  color: var(--ink);
  font-size: 10px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: 0;
  writing-mode: horizontal-tb;
  font-variant-numeric: tabular-nums;
}
.cf-tab-badge[hidden] { display: none; }
/* （2026-10-12：原来这里有一条 `.cf-tabs .cal-item[hidden]{display:none}` —— 它兜的是
   "没图时藏掉五颗签"。用户改成"六颗一直在"之后，没有任何地方再给它们挂 `hidden` ✓
   ⇒ 那条成了死样式，删掉 ✓。） */

/* ============ 「自检」那一组（第 5 颗 tab，用户 2026-10-09） ============
   它把原来右下角那颗 ✅ 收进来了：进这一组 ⇒ 工具行（连撤销/重做）收起来、这块顶上来，
   面板**拉高到全屏**（`flex:1` + 把画布收掉 ✓）—— 用户要的就是"tab 区高度拉伸为全屏展示
   自检表格和去设计按钮" ✓。
   ⚠ 高度规则挂在 **`#confirm` 的 `data-cal-group`** 上（分组由 `applyCalGroup` 写在页面
   与 `#cf-plan` 两处 ✓）：画布与面板是**平级**的两块，只有页面这一层同时够得着它们 ✓。 */
#confirm[data-cal-group="check"] .cf-plan { display: none; }
/* ⚠ 原图那颗眼睛这一组里**收起来**（用户 2026-10-09：「隐藏原图按钮」✓）：这一屏没有画布，
   它无处可切。用 CSS 不用 js：`#cf-tg-base` 的显隐**另有主人**（`app.js` 的 `updateMascot`
   按"在不在户型图页"管它 ✓），两边都写就会互相打架 ✗。 */
#confirm[data-cal-group="check"] .cf-plan-view { display: none; }
/* ⚠ 「原图」那一组同理（用户 2026-10-11）：那一页看的就是底图本身，"原图/叠加"这颗眼睛
   在那儿无处可切 ✓ —— 与上面那条同一个理由、同一个写法（也归 CSS，别去动 JS 里那个主人 ✓）。 */
#confirm[data-cal-group="base"] .cf-plan-view { display: none; }
/* 定稿表：**占了画布那一格**（同一个 1:1 ⇒ 下沿与画布齐平 ✓，用户 2026-10-09：
   「tab 区拉伸至内容区下沿即可」✓）。长相也照画布那张卡（surface ＋ 描边 ＋ 14 圆角 ✓），
   切换时是"同一块地方换内容"，不是跳一下 ✓。
   ⚠ 条目多过这块地儿时它自己滚（`overflow-y: auto` ✓）。 */
/* ⚠ 滚动条**藏起来**（用户 2026-10-11：「定稿内容去掉滚动条」），但**滚动本身留着**：
   直接把 `overflow-y` 去掉的话，条目多过这一格时后面的行会被卡在卡片外面、够都够不着 ✗
   （那是拿"看不见"换"到不了"）✓。手机上滚动条本来就是浮层，看得见的是桌面那条。 */
.cf-check { height: 100%; box-sizing: border-box; display: flex; flex-direction: column; gap: 10px; min-height: 0; overflow-y: auto; scrollbar-width: none; /* ⚠⚠ **上面那一条留白**（2026-10-16）：左上角那两句「请确认校准结果」是**绝对定位**的 ✓，不给这张表让出这一截的话，它会**压在表头「房间」上** ✗（实测就是这样 ✓）。44 ＝ 那句话的高度 ＋ 上下各 14 的呼吸 ✓。 */ padding: 44px 14px 14px; border: 1px solid var(--line); /* 与画布同一套边角（四角同半径，见 `.cf-plan` 那条注释）✓；高度同理走 `height:100%`（不再 1:1）✓ */ border-radius: 14px; background: var(--surface); }
.cf-check::-webkit-scrollbar { width: 0; height: 0; }
.cf-check[hidden] { display: none; }
/* ⚠ 表里**只有数据行**（用户 2026-10-09 连着两条：「只留下表格，去掉开头和结尾的文本」＋
   「去掉『面积按你逐间…』『还有 23 段墙…』」✓）⇒ 没有 `caption`、也没有 `tfoot` 的说明行 ✓
   —— 那两条样式也别留着（死样式 ✓）。 */
/* ============ 定稿那张**两级表**（用户 2026-10-09：房间当组头，门窗缩在下面） ============
   纸面语言照这一页别的表：细线分隔、数字列右对齐、单位跟着数 ✓。
   ⚠ 数字列右对齐是**能比对**的前提（一列面积竖着扫一眼就知道谁大谁小 ✓）。 */
.cf-table { width: 100%; border-collapse: collapse; font-size: 12.5px; line-height: 1.5; }
.cf-table th, .cf-table td { padding: 5px 4px; border-bottom: 1px solid var(--line); text-align: left; font-weight: 400; }
.cf-table thead th { color: var(--ink-2); border-bottom-color: var(--line-2); }
.cf-table td:nth-child(2), .cf-table td:nth-child(3), .cf-table thead th:nth-child(2), .cf-table thead th:nth-child(3) { text-align: right; white-space: nowrap; }
.cf-table .cf-row-room th { font-weight: 650; border-bottom-color: var(--line-2); }
/* 门窗那几行：缩进去一档、淡一档 —— 它们是**组头那一间的**东西 ✓ */
.cf-table .cf-row-item td { padding-left: 10px; color: var(--ink-2); }
/* 每间自己那条"要看看"（没有门 / 角落那条）占满一行 ✓ */
.cf-table .cf-row-flag td { color: var(--ink-2); font-size: 12px; border-bottom-color: transparent; }
.cf-table tfoot th, .cf-table tfoot td { font-weight: 650; border-top: 1px solid var(--line-2); border-bottom: 0; }
/* 房名旁那颗 ⚠：光标给个问号，说明在 `title` 上（别把整句话塞进格子 ✓） */
.cf-flag { margin-left: 4px; color: var(--accent); cursor: help; }
/* 那颗 **圆圈＋对勾**：长相照从前右下角那颗 ✅（`.fab-float`：40px 圆、2px 主色描边、
   主色对勾 ✓）。⚠ 白色的底要**自己写**：这一格是 `--bg-2`（工具行那块灰 ✓），
   照着 `.fab-float` 的 `--surface` 才浮得起来 ✓。 */
/* ⚠ 尺寸走 `--icon-btn`（面板上那几颗圆钮的同一个数 ✓），**不是** `.fab-float` 的 40：
   这一格的高度是钉住的（`min-height: --cf-ctl-h + 24` ✓），塞一颗 40 进去会把这一行撑高
   6px ⇒ 下面内容区那一格跟着往下挪 ✗（实测："表的三个数与画布那一格"那条当场红 ✓）。
   32 落在那一格里，行高不变 ✓。 */
.cf-check-go { box-sizing: border-box; width: var(--icon-btn); height: var(--icon-btn); padding: 0; display: grid; place-items: center; border: 1px solid var(--accent); border-radius: 50%; background: var(--surface); color: var(--accent); cursor: pointer; }
.cf-check-go svg { display: block; width: var(--icon-btn-icon); height: var(--icon-btn-icon); }

/* 账本**表尾那一行**（2026-10-13）：三个全屋数（面宽 · 进深 · 层高）＋ 一句"按哪条尺算"。
   它们是**数据**（与表尾「全屋套内」同一档 ✓），不是说明文字 —— 所以没填时写「未填」，
   不隐身 ✗（没尺的时候上面那一列面积全是「—」，这一行是**唯一**说得清为什么的地方 ✓）。
   ⚠ 它不进 `<tfoot>`：零房间时那张表整个不输出（`renderCheck` 的 `: ""` 那一支 ✓），
   而这一行在空图上照样该看得见 ✓。 */
.cf-check-scale { margin-top: 10px; padding-top: 8px; border-top: 1px solid var(--line-2); display: flex; flex-direction: column; gap: 6px; }
/* 读态那一行：点哪儿（除了那颗问号）都进改态 —— 与门窗/区域那些芯片同一套 ✓ */
/* ⚠⚠ **三格等距撑满**（用户 2026-10-16：「三个属性等距撑满内容区宽度」✓）：原来是
   `gap:4px` ＋ 各自按内容宽 ✗ ⇒ 三格挤在左边、右边空一大截 ✓。
   现在每一格 `flex:1 1 0`（三份等宽 ✓）＋ 格内"标签在左、值在右" ✓（`space-between` ✓），
   合起来正好铺满这一行 ✓。⚠ 徽标是绝对定位的（`top:-4px/right:-4px` ✓）⇒ 不吃宽度 ✓。 */
.cf-scale-line { position: relative; display: flex; align-items: center; gap: 10px; cursor: pointer; }
.cf-scale-line > .cf-dim-cell { flex: 1 1 0; min-width: 0; justify-content: space-between; }
.cf-scale-line .cf-dim-value { font-variant-numeric: tabular-nums; }
/* 没填的那一格淡一档（与那张删掉的表里"未量"同一套 `--ph` ✓） */
.cf-dim-value.is-todo { color: var(--ph); }
/* ⚠⚠ **在改的那一格：值那半边就地换成输入框**（用户 2026-10-16：「三个属性点哪个就地编辑哪个，
   就像房间标签页操作区的 chip 那样」✓）—— 那一行的高度**不许变**：读数那颗药丸与输入框本来
   就是同一个盒子（`.cf-dim-value, .cf-dim-input` 那条共用规则 ✓），换的只是"显示值 ⇄ 能打字"✓。
   宽度照旧**撑满这一格剩下的地方**（那颗 `5ch` 的窄药丸是给图中标注定的 ✗ —— 这里是"站着填
   一个数"，宽一点好点 ✓）、字靠右（与读数时贴着这一格右边那颗药丸同一个位置 ✓）。 */
.cf-scale-line .cf-dim-cell .cf-dim-input {
  flex: 1 1 auto;
  width: auto;
  min-width: 0;
  padding: 0 8px;
  text-align: right;
}
.cf-scale-note { margin: 0; color: var(--ink-2); font-size: 12.5px; line-height: 1.5; }
/* 没有尺那句话**要是醒目的**（它是账本上唯一说得清"为什么面积算不出"的地方 ✓）——
   用这一页"要看这儿"的那支橙（`--node`：图上高亮的那段墙、进度、那颗号都是它 ✓）。 */
.cf-scale-warn { color: var(--node); }
/* ⚠ 2026-10-16：**「三行输入框 ＋ 保存 / 取消」那套撤了**（`.cf-scale-field` / `.cf-scale-actions` /
   `.cf-scale-cancel` ✓）—— 三个数改成**点哪格改哪格、就地改**（与那颗 chip 同一套：没有那两颗
   按钮，回车 / 点别处 ＝ 落库、Esc ＝ 算了 ✓，单位仍是毫米、写米也认 ✓）。
   留着那几条只会让下一个人以为那一屏还在 ✗。 */
/* 校验没过那句话（它长在 `#cf-check-scale` 的最下面：出错时它自己长出来，**推的是它下面的东西**，
   手里那一格纹丝不动 ✓）。 */
.cf-scale-error { margin: 0; color: var(--danger); font-size: 13px; }

/* 「原图 / 叠加图」开关（用户 2026-09-30）：它是一件事的**两个状态**，所以做成 switch，
   不是一颗普通按钮。⚠ 状态类用的是 `active` —— **与 JS 已有的那套一致**（`cf-tg-base`
   的点击处理器本来就在 toggle `active`），不另起一个状态名。 */
/* 页内那几颗**圆形图标钮**共用一套长相：户型图页的撤销与「原图」、效果图的漫游
   （用户 2026-10-01：漫游那颗对齐撤销/原图的大小与样式）。尺寸与图标走
   `--icon-btn` / `--icon-btn-icon`（定义在 `#confirm, #design-chat, #drawing` 上），
   所以这里**一个像素数都不写**。
   ⚠ 这三颗的"选中"各用各的机制（撤销 `is-on`、原图 `active`、漫游 `aria-pressed`），
   一条选择器列全，别让谁漏在外面——这个工程在这上面栽过四次。 */
.icon-btn { display:grid; place-items:center; box-sizing:border-box; width:var(--icon-btn); height:var(--icon-btn); padding:0; border:1px solid var(--line-2); border-radius:50%; background:transparent; color:var(--ink-2); cursor:pointer; }
.icon-btn svg { display:block; width:var(--icon-btn-icon); height:var(--icon-btn-icon); }
/* 高亮用的是**地图页节点那个暖橙**（用户 2026-10-01：「这 3 个按钮高亮背景色对齐地图页节点」）
   ——不是主题色：节点本来就是固定的暖橙（不随主题走），对齐它才不会出现"同一屏两种橙"。 */
.icon-btn.is-on, .icon-btn.active, .icon-btn[aria-pressed="true"] { border-color:var(--node); background:var(--node); color:#fff; }
@media (prefers-reduced-motion: reduce) { .cf-seg-thumb { transition:none; } }

/* 「原图」开关跟在分类芯片那一排的**最右**（用户 2026-09-30）——它不属于哪个分类，
   所以既不做成芯片，也不跟工具混在一起；`margin-left:auto` 把它推到行尾。 */

/* 「原图」开关浮在**内容区（户型图那块）的右上角**（用户 2026-09-30）——
   与效果图页那排墙层控件同一个位置口径（内容区右上、贴一点边距），不占面板的地方。
   ⚠ 它浮在图上，所以要 `z-index` 高过图，并且点击不会落到图上的拖拽处理里。 */

/* 眼睛图标本体：**空心＝隐藏、高亮＝展示**（用户 2026-09-30）——状态由 `.active` 承载，
   ⚠ 类名沿用 JS 已有的 `active`（`#cf-tg-base` 的处理器本来就在 toggle 它）。 */

/* 撤销 / 重做：挂在**内容区（纸）的右上角**（用户 2026-10-11：「把 undo、redo 移动到内容区
   右上角」）—— 从 2026-09-30 起它们一直挂在面板右上角，这一搬与左上的眼睛配成一行 ✓。
   ⚠ 挂哪儿归它自己（长相归 `.icon-btn`）；`position` 的容器是 `.cf-folder-main`（`relative` ✓）。
   ⚠ 类名跟着地儿改名（`.cf-edit-undo` → `.cf-paper-undo`）：叫 `cf-edit-*` 却住在纸上，
   下一个人照着名字去面板里找就找不着 ✗（id 仍是 `cf-undo` / `cf-redo`，JS 按 id 找 ✓）。
   ⚠ 右侧给签那一列留了 6px 余量（最右那颗 `right:10px`，而签的左沿压在纸上 4px）——
   再加宽按钮就得连签宽一起重算。 */
/* ⚠⚠ **2026-10-13 左右对调**（用户：「把原图按钮和 undo/redo 的位置互换，原图移动到右上角，
   undo/redo 到左上角」✓）：眼睛在**右上角**、撤销/重做在**左上角** ✓ —— 四角的分工照旧 ✓
   （左上是"改这一页"，右上是"切看哪一层"，右下是签那一列 ✓）。
   ⚠ 读起来仍是「撤销 → 重做」（撤销在左边那颗 ✓、重做挨着它 ✓）—— 这套相对顺序没变 ✓。 */
.cf-paper-undo { position:absolute; top:10px; left:10px; }
.cf-paper-redo { position:absolute; top:10px; left:50px; }
.cf-paper-undo:disabled { opacity:.45; cursor:not-allowed; }
/* ⚠⚠ **这一行三颗得"浮"在图上，就得有自己的底**（用户 2026-10-16：「内容区顶部有 3 个按钮，
   加一层半透明白色背景吧，这样当户型图缩放延伸过去时、按钮还能看得清」✓）：
   `.icon-btn` 本身是**透明**的（只有一圈描边 ✓）——从前图缩在纸当中、四角多半空着 ✓，
   2026-10-16 起图**撑满纸的宽**（`fitPlanWidth` ✓）⇒ 这三颗正压在图上，一圈细描边兜不住 ✗。
   用**气泡那一档材质**（`--bubble` ＝ 半透明白 ＋ 磨砂 ✓，与叶知秋那颗、地图那颗同一族 ✓）：
   既压得住底下的图，又还看得见底下是什么 ✓；⚠ 深色主题跟着 `--bubble` 走 ✓。
   ⚠ **只写这三颗**（左上的撤销/重做 ＋ 右上的眼睛 ✓），别去改 `.icon-btn` 本身 ✗ ——
   别的页（效果图那排墙层控件）也穿着它，那些地方**不浮在图上** ✓。 */
.cf-paper-undo,
.cf-paper-redo,
.cf-plan-view {
  background: var(--bubble);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
}

/* （2026-10-13 删掉了一整套 **量房进度饼** 的样式：`.cf-paper-progress` / `.cf-progress-pie` /
   `.cf-progress-disc` / `.cf-progress-wedge` / `.cf-progress-pct` / `.cf-progress-edit`。
   那块饼是 2026-10-12 用户要的（「展示圆形进度条（并展示百分比），点击弹出量房 Sheet」✓），
   2026-10-13 又被他自己撤了（「可以去掉量房标签页右上角的进度和 badge 了，是吧？」✓）——
   那个数当年搬到了**签上那颗角标**（那颗 2026-10-16 也撤了 ✓；今天住账本那两颗「活」上 ✓），
   而"点开量房表"那件事连同那张表一起没了 ✓。
   ⚠ 删得很干净是**因为查过**：`.cf-progress-*` 这一族只有它自己用（HTML/JS 两处一起删 ✓），
   而 `.cf-paper-progress` 那条 `position/top/right` 是**顶撤销/重做那一格**的 ——
   那两颗的定位另有各自主人（`.cf-paper-undo` / `.cf-paper-redo` ✓），没跟着走 ✓。） */

/* **拖一个框＝起一间新房**的那个框（2026-10-15 ✓）：虚线轮廓 ＋ 淡淡一块，与门窗/斜墙
   那套预览同一族长相 ✓。⚠ 旧版那个 `.cf-box-preview` 的三态（能加／落不下去／在减）
   跟着涂格子一起撤了 ✓ —— 现在这个框**只有一个意思**：框到哪儿就落到哪儿 ✓
   （盖住邻居的那块，就从邻居那儿划过来 ✓）。 */
.cf-room-box {
  fill: rgba(63, 124, 92, 0.18);
  stroke: var(--node);
  stroke-width: 1.5;
  stroke-dasharray: 7 5;
  pointer-events: none;
}

/* 底图是**装饰**：它不该自己接指针（否则在空白处按下去，事件落在 <image> 上，
   浏览器可能顺手起一次原生图片拖动——看着就是"在拖原图"）。房间矩形在它上面，照旧可点。 */
#cf-g-base image {
  pointer-events: none;
}

/* **房间那一页的墙手柄**（2026-10-15）：每一面墙当中一颗圆点，垂直拖＝移这条边界 ✓。
   ⚠ 与门窗/斜墙那几颗手柄**同一套长相**（看得见的那颗 `pointer-events:none`，接手指的是
   同半径的透明靶子 ✓）—— 靶子**不许比圆圈大** ✗（2026-10-08 那一课：「端点的热区太大了」✓）。
   ⚠ 它**常驻**（这一页没有笔了 ✓）⇒ 只画**当前这一间**的墙（`roomGrips` 一处判据 ✓）。 */
.cf-room-grip {
  fill: #fff;
  stroke: var(--node);
  stroke-width: 2.5;
  pointer-events: none;
}
.cf-room-hit {
  fill: transparent;
  cursor: grab;
}
/* **压住隔壁的那条窄带**（2026-10-15 ✓）：拖动不再拦 ⇒ 压住了就画出来 ✓。
   与"会没掉/要改"的虚线语言分开：这里用**实心橙**（＝正在发生的事实 ✓），不是虚线（＝预览 ✓）。 */
.cf-bump {
  fill: rgba(192, 123, 58, 0.3);
  stroke: var(--node);
  stroke-width: 1.5;
  pointer-events: none;
}
/* ⚠ **`.cf-split-line` / `.cf-split-cut` / `.cf-split-mark` / `.cf-split-hit` 2026-10-16 撤了**
   —— 剪刀那一整套随"单位＝一块"一起撤（要"只动半截"就**框一块**或**删一块** ✓）。 */
/* 拖动中：**对面那几间也要看得见**（用户 2026-10-15 提的）—— 一条边的对面可能不止一间，
   不说一声用户会以为只动了一间 ✗。底色不动、只加一层虚线描边，免得盖过"当前这一块" ✓。 */
.cf-room.is-neighbor {
  stroke: var(--node);
  stroke-width: 3;
  stroke-dasharray: 6 4;
}
/* **当前选中的那一块**（2026-10-16 ✓）：描一圈橙边 —— 单位是**一块**，所以描在**这一块自己**
   身上 ✓（房名上那颗 ★ 另说一件事："它属于哪一间" ✓ —— 两处各说各的 ✓）。
   ⚠ 有覆盖边那一间画的是**整间的形状**（`<path>` ✓，没有逐块元素 ✗）⇒ 那里只有下面那一条淡的 ✓。 */
.cf-room.is-sel {
  stroke: var(--node);
  stroke-width: 3;
}
/* **同一间的其它块**：一条**淡**描边 —— 它回答"为什么改这块的名字、隔壁那块也跟着换" ✓。
   ⚠ 只动描边（`stroke-opacity` ✓），不碰底色 ✓。
   ⚠⚠ **2026-10-16 收窄到 `<path>` 一支**（用户：「房间标签页的内容区能不能按房间展示不同
   色块，**只选中的区域才展示四条边和 handle**」✓）：
   · `<rect>`（普通房间，一块一个矩形 ✓）**不再描边** ✗ —— 那些方块现在**一间一个底色**
     （`ROOM_FILLS` ✓），"这块和那块是一间"由**同色**说 ✓（比一条虚线更安静、也更准 ✓）；
     "哪一块是当前这一块"由**选中那一块的橙边** ＋ 手柄 ＋ chip 说 ✓ —— 一处一个真相 ✓。
   · `<path>`（**有覆盖边**的那一间：画的是整间的形状，没有逐块元素 ✗）**照旧淡描** ✓ ——
     那里**没有别的能说"选中的就是这一间"**（矩形都不画 ✓）⇒ 全撤了就等于那一间选不亮 ✗。 */
path.cf-room.is-mate {
  stroke: var(--node);
  stroke-width: 1.2;
  stroke-dasharray: 5 5;
  stroke-opacity: 0.45;
}

/* 区域 tab：**一行话**＋最多两颗按钮（用户 2026-10-04 第二次重做）。
   从前是一间房一颗色块按钮——区域一多就占掉好几行，把画布挤下去。现在：
   没选区域＝"请先选择要操作的区域"＋「+ 新增」；**在图上点区域**就算选中（tab 改名成区域名，
   再点它一下取消），选中了才冒出「删除」。 */
.cf-region-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  width: 100%;
  /* 一行的高度**按控件算，不按内容算**（用户 2026-10-04）：墙体那一行没选中时只有一句话
     （没有按钮撑着），行会矮一截，于是它的字跟比例尺/区域那两行的字**不在同一条水平线上**。
     钉住一行至少一颗控件高，配 `align-items: center`，三行的字就都居中对齐了。 */
  min-height: var(--cf-ctl-h);
}
/* 两颗图标钮**不参与收缩**：`.icon-btn` 给的是固定 32px，但在窄屏上 flex 会把它压扁 */
.cf-region-row .icon-btn {
  flex: 0 0 auto;
}
/* ⚠⚠ **带名字的工具**（2026-10-13 用户：「操作区各工具按钮下方添加 2-3 字的工具名」✓）：
   圆形图标钮（32px，`aria-pressed` 那条橙色高亮照旧 ✓）**下面**挂一行 11px 的小字。
   ⇒ 这一族从此长这样（房间/墙体/门窗三行**同一套** ✓）：
     `[⭘ 图标]` / `选框`   —— 整块是一个 `<button>`（点名字也能拿笔 ✓，不必非点那颗圆）。
   ⚠ 圆盒子在**里面**那一格（`.cf-tool-icon`）：按钮本身是"图标 ＋ 名字"的竖排，不能再画圆 ✗
     —— 选中态因此挂在 `.cf-tool[aria-pressed="true"] .cf-tool-icon` 上（与 `.icon-btn`
     那条同一个长相，只是往下挪了一层 ✓）。
   ⚠ 它们**不参与收缩**（同上面那条）：窄屏上一压就成椭圆 ✗ —— 行里真正让位的是芯片与提示语 ✓。 */
.cf-tool {
  display: flex;
  flex-direction: column;
  align-items: center;
  flex: 0 0 auto;
  gap: 3px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--ink);
  font: inherit;
  cursor: pointer;
}
.cf-tool-icon {
  display: grid;
  place-items: center;
  box-sizing: border-box;
  width: 32px;
  height: 32px;
  border: 1px solid var(--line-2);
  border-radius: 50%;
  background: var(--surface);
  color: inherit;
}
.cf-tool-icon svg {
  display: block;
  width: 19px;
  height: 19px;
}
.cf-tool[aria-pressed="true"] .cf-tool-icon {
  border-color: var(--node);
  background: var(--node);
  color: #fff;
}
/* 房间那一行那两颗（＋新建 / 🗑删除）"**正在操作哪一间**"的底色（用户 2026-10-04：
   「两颗图标钮也填上同一个底色」✓ —— 一眼看出它们是在操作"这一间" ✓；颜色＝那间房在画布上的
   色号 ✓，与画布上那颗**变橙的房名**说的是同一件事 ✓）。颜色由 JS 按房色走 `--cf-tint` ✓。
   ⚠⚠ **涂在圆圈上**，不是按钮上（按钮是"圆 ＋ 名字"两列 ✗ —— 涂整颗就成了"蓝方块套白圆" ✗，
   2026-10-14 用户贴图报的正是这个 ✓）；长相与上一条 `aria-pressed` 逐字一致 ✓。 */
.cf-tool.is-tinted .cf-tool-icon {
  border-color: var(--cf-tint);
  background: var(--cf-tint);
  color: #fff;
}
/* 工具名：**两三个字**（用户当天定的 ✓）、比正文小一档、不换行（换行会把这一行撑高 ✗）。 */
.cf-tool-name {
  font-size: 11px;
  line-height: 1.2;
  white-space: nowrap;
  color: var(--ink-2, var(--ink));
}
.cf-tool[aria-pressed="true"] .cf-tool-name {
  color: var(--node);
  font-weight: 700;
}
/* 房间那一行的**三件工具**（2026-10-13）：三颗圆图标钮（`.icon-btn` 32px ✓）。
   ⚠⚠ **这一条是补的**（2026-10-13 批 A 落下、当天查出 ✗）：容器从前是给**分段开关**
   （`.cf-seg`，自带 `display:flex`）住的，换成三颗圆钮之后**没人给它 `display:flex`**
   ⇒ 三颗按钮**竖着叠**成一列（实测 `#cf-cell-tools` ＝ 32×96 ✗✗），把纸整块往下推 40px
   ⇒ 定稿那一屏「表与画布**同一格**」那条保证当场破了 ✗（`confirmation.spec` 的
   「表的上沿与画布那一格」实测 190 ≠ 150 ✓ —— 它是这一刀的哨兵 ✓）。
   ⚠ 宽度**不再需要**从前那套"每段收窄"（`min-width: 2.4em` / `padding: 0 6px` /
   字号 12.5px ✗，那是给三段文字定的，也一并删了）：32px 一颗是 `.icon-btn` 自己的尺寸 ✓。 */
/* ⚠⚠ **「新建」和左边那两件不是一回事**（用户 2026-10-19：「将新建按钮移动至操作区最右侧……
   与扩容、删除区别开，避免被认为是与选中区域有关的操作」✓）：一条细竖线把它们分开 ✓，
   竖线自己 `margin-left:auto` ⇒ 它和「新建」一起被推到最右 ✓。
   ⚠ 这一行**仍然不许换行**（`confirmation.spec` 拿它的高度当哨兵 ✓）。 */
.cf-tools-sep {
  flex: 0 0 auto;
  width: 1px;
  height: 22px;
  margin-left: auto;
  /* ⚠⚠ **与图标那一行对齐**（用户 2026-10-19：「与新建按钮的分割线，向上与 icon 水平对齐」✓）：
     工具是"图标（32 高）＋ 名字"的竖排 ✓ ⇒ 整颗按钮比图标高 ✓，而 `align-items: center`
     会把线对齐**按钮**的中线 ⇒ 线看着**偏下** ✗。这里对齐到**行首**、再往下 5px ⇒
     22 高的线中心正好落在图标的中心（16 ✓）。 */
  align-self: flex-start;
  margin-top: 5px;
  background: var(--line-2);
}
/* ⚠ 类名是**中性**的（`.cf-tools`）—— 这一族现在有**三个用户**：房间那两件
   （`#cf-room-tools` ✓）、墙体那四支（`#cf-wall-tools` ✓）、门窗那六件（`#cf-opening-tools` ✓）。
   叫 `.cf-cell-tools` 就只说了第一处 ✗（第一版就是那么写的，另外两处借了它的名字 ✓ ——
   借名字这种事，下一个人一定会看错一次 ✓）。 */
.cf-tools {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 0 0 auto;
}
/* ⚠ 从前这里有一条 `#cf-cell-tools button`（带 id 才压得住组件那条 `.cf-seg button` ✓）——
   分段开关撤了，那条**一起删** ✓（选择器本体没用了，注释留在上面那段里 ✓）。 */
/* ⚠ **当前拿的是哪一支**由 `aria-pressed="true"` 说（`.icon-btn` 那条自带橙色填充 ✓，
   见上面 `.cf-tools` 的注释）、高亮由 `#cf-plan[data-wall-brush=…]` 那套画（墙体那一组 ✓）——
   这里一个像素都不另写 ✗。 */


/* **门下/窗下那两颗改宽手柄**（2026-10-13）：与斜墙的端点手柄**同一套长相** ——
   看得见的小圆点（r=5 ✓ 橙色 `--node` ✓）＋ 一圈**透明但吃指针**的胖靶子（`.cf-opening-grip-hit` ✓
   r=16 ✓，手指够得着 ✓）。⚠ 靶子是**后画的**（在圆点之后 ✓）⇒ 它压在上面接手指 ✓，
   而两颗都 `pointer-events` 默认 ⇒ 命中判据归 JS（`closest("[data-grip]")` ✓）。 */
/* ⚠ **每扇都画、露不露看选没选中**（与转方向那颗同一条 ✓）：`display:none` 那一下
   连**命中判定**一起没了 ✓ —— 没选中时它们绝不接手指 ✓（选中的那一扇才亮两颗点 ✓）。 */
.cf-opening-grip,
.cf-opening-grip-hit {
  display: none;
}
.cf-item.is-sel .cf-opening-grip,
.cf-item.is-sel .cf-opening-grip-hit {
  display: block;
}
.cf-opening-grip {
  fill: var(--node);
  stroke: #fff;
  stroke-width: 2;
  pointer-events: none;
}
.cf-opening-grip-hit {
  pointer-events: fill;
}

/* **放门窗的落点预览**（2026-10-13）：按住时沿墙描出**洞口会占的那一段** ✓ ——
   一扇 900 的门落在哪儿、会不会与旁边那扇打架，松手之前就看得见 ✓
   （与"目标格""框选预览"同一套"所见即所得" ✓，连两种长相都共用同一套类名：
   落得下＝橙（`--node` ✓）、**落不下＝灰的**（`.is-blocked` ✓ ——
   与已有门窗打架、超出墙段都在这一档 ✓，见 `state` 那几条）。 */
.cf-opening-preview {
  fill: none;
  stroke: var(--node);
  stroke-width: 7;
  stroke-linecap: butt;
  pointer-events: none;
}
.cf-opening-preview.is-blocked {
  stroke: #8a8a8a;
  opacity: 0.5;
}
/* 那句话／"已选择＋芯片"与两颗按钮**挨着排**（用户 2026-10-04：按钮跟在芯片后）：
   内容**不撑开**（`flex-grow: 0`），按钮**不缩**（`.cf-region-row .icon-btn` 那条，窄屏上会被压扁）。
   ⚠⚠ **这一格永远是**一行**（2026-10-13 批 A 的哨兵用例抓出来的）：它从前是"长句自己换行"
   （`flex-shrink` ＋ `flex-wrap` ✓），而那一行的**高度**是要紧的 —— 纸的上沿由它决定，
   各组的纸必须落在同一格（`confirmation.spec` 的「表的上沿与画布那一格」✓）。三支笔常驻之后
   这一行多出 112px，那句指路的话被挤成两行 ⇒ 纸整整低 4px ✗✗。
   ⇒ 现在：这一格是 `flex` 容器（子元素才有机会被压缩 ✓，纯文字也照样摆 ✓），
   里面的**芯片**自己收（见 `.cf-region-name` ✓），不许把这一行撑高 ✓。
   ⚠⚠ 2026-10-13 **指路的那句话整个撤了**（用户：「去掉操作区的引导性文字，改为激活工具时、
   由设计师气泡展示该工具的使用方法」✓）—— 话改由叶知秋的气泡说（`calibration.js` 的
   `CF_GROUP_INTRO` / `CF_BRUSH_GUIDE` ✓）。于是这一格**只住芯片**：没选中时是**空的**，
   由下面那条 `:empty` 整格收掉（不收的话，`gap: 8px` 还会在行首留一段缝 ✓）。
   跟着一起删的还有 `.cf-hint-text`（"一行 ＋ 省略号"那套就是为那句话写的 ✗ —— 元素没了、
   样式就该跟着走 ✓，留着的下场见 `.cal-tool` 那次 ✓）。 */
.cf-region-hint {
  display: flex;
  align-items: center;
  flex: 0 1 auto;
  min-width: 0;
  font-size: 12.5px;
  line-height: 1.45;
  color: var(--ink-2, var(--ink));
}
.cf-region-hint:empty {
  display: none;
}
/* **当前选中那一块的 chip**（2026-10-16 加回来 ✓）：`已选择：<名字>` ＋ 右上角那颗 ✎ ✓
   （用户：「把当前选中区域的 chip 加回到操作区，恢复点击 chip 编辑命名的交互」✓）。
   盒子直接挂 `.cf-edit-btn`（药丸 ✓，与 `.cf-region-edit` 那一族同一套变量 ✓，不另写像素 ✓）。
   ⚠ 名字长 ⇒ **截断**：这一行**不许换行** ✗（`flex-wrap` 一换行就把纸往下推，
   `confirmation.spec` 拿它当哨兵 ✓）。 */
.cf-region-chip {
  position: relative;
  margin-right: 10px; /* ✎ 探出 6px：不留的话快贴到「＋新建」上 ✓ */
  max-width: 11ch;
}
.cf-region-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* 编辑态（就在 chip 里换成输入框 ✓）：底色撤掉 ⇒ 一眼看出"这会儿在改名字" ✓。
   ⚠ 这一条要**放开截断**，不然输入框比 chip 宽出去就被裁 ✗。 */
.cf-region-chip.is-editing {
  max-width: none;
  background: transparent;
  border-color: var(--line-2);
  color: var(--ink);
}
/* ⚠ 宽度**按名字的实宽**由 JS 定（`measureText` ✓）：一个汉字约 2ch，拿 `ch` 估会把
   「客餐厅」这种三个字的名字当场裁掉（用户报过 ✓）。 */
.cf-region-input {
  display: block;
  box-sizing: border-box;
  min-width: 4ch;
  max-width: 16ch;
  padding: 0;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  font-weight: inherit;
  outline: none;
}
.cf-walk-step {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* ⚠⚠ **两颗"活"（量墙 / 量门窗）住在账本那一行**（2026-10-14 晚搬过去 ✓ —— 用户：
   「进入量房标签页时，默认展示测量结果列表，并根据是否需完成测量来决定展示确认按钮
   还是 2 个测量按钮，可行吗？」⇒「同意，搬吧」✓）：它们走**标准工具那一套**
   （`.cf-tool`：圆钮 ＋ 名字 ✓ —— 与房间 / 墙体 / 门窗那三行**逐字一致** ✓，
   用户当天：「改成圆形＋尺 icon 再配文字的样式，对齐其它标签页的工具样式」✓）。
   ⚠ 向导那一行从此**只有一个态**（标题 ＋ 值 ＋ ← → ✓）："挑一件做"那一态撤了 ✓
   （挑的地方在账本 ✓，而账本才是进签的默认屏 ✓）。 */
/* 角标挂在**圆圈**的右上角（不是按钮的：按钮是"圆 ＋ 名字"两行，挂按钮上会飘到名字旁边 ✗）。
   ⚠ 长相照旧共用 `.cf-tab-badge`（底/描边/字号 ✓），这里只把定位交给圆圈 ✓。 */
.cf-tool-icon {
  position: relative;
}
/* 行尾那颗 ✅ ＝**确认**（用户 2026-10-14：「…决定展示确认按钮还是 2 个测量按钮」✓）：
   两样都做完 ⇒ **点亮** ✓（一眼看出可以收工了）。⚠ **不拦人** —— 它照旧只认"有没有尺"
   那一关（2026-10-13 定的「只要有尺就能去设计」✓），差几段也点得动、绝不弹"还没量完" ✗。 */
.cf-check-go.is-ready {
  border-color: var(--accent);
  background: var(--accent);
  color: #fff;
}
/* 「已选择」后面那颗芯片（区域名）。
   ⚠ **别再挂 `.sheet-chip`**：本文件里 `.cf-edit .sheet-chip`（上面那条）把它改成了**扁平的
   下划线 tab**（`border:0` / `border-radius:0` / `background:none`），而这一行就在 `#cf-edit`
   里面——第一版那么写，渲染出来是"一行没有框的字"（实测：圆角 0px、背景透明）。
   这里用**自己的类**：形状只写这几笔，颜色（背景＋描边）由 JS 按区域色**行内**给，
   所以"芯片底色＝画布上那块格子的颜色"是**同一个来源**，不会各写各的。 */
/* ⚠ 芯片的**盒子**不在这儿定：它直接挂 `.cf-edit-btn`（跟墙体/门/窗那几组的功能按钮
   **同一个盒子**——`min-height: var(--cf-ctl-h)` / `padding: 0 13px` / 13px 字 / 药丸圆角，
   用户 2026-10-04：「芯片大小对齐其他墙体/门/窗的功能按钮大小」）。所以这里一个尺寸都不写，
   只负责"颜色"与那颗 × 的定位；底色与描边由 JS 按区域色**行内**给（内联盖过 `.cf-edit-btn`）。 */
/* ⚠ 这一条**区域芯片与门窗芯片共用**（2026-10-05 第六十二条发现的门窗那颗"没有 badge"）：
   徽标是 `position: absolute; top:-6px; right:-6px` ⇒ 它靠**父元素 position: relative** 才贴得住芯片。
   门窗那颗芯片用的是自己的类（`.cf-opening-chip`），而这条规则原先只挂在区域那颗上 ⇒
   徽标按"最近的定位祖先"去摆，看着就像没有 badge ✗。两个类写在一起，别各写一份 ✗。 */
/* （⚠ 2026-10-14 **删掉了 `.cf-opening-chip` 那一整条**：门窗那颗芯片撤了
   —— 用户「把现在门窗标签页的那颗 chip 移过去」✓，见 `renderOpeningBar` ✓。
   它管的是"徽标定位在它右上角 / 名字长了要能收 / 白字"，元素没了这几笔就该走 ✓
   （按类名认领元素的样式，元素没了就该跟着走 —— `.cal-tool` / `.dim-*` 那两次的老账 ✓）。
   ⚠ `.cf-region-edit` 留着：账本表尾那三格（`data-scale-edit` ✓，2026-10-16 起三格各一颗 ✓）
   与 `#cf-walk-edit` 还穿着它 ✓。） */
/* 右上角那颗 ✎ ＝**就地改名字**（2026-10-04 第二次改：它从前是 ×；而"取消选中"挪到了
   **点芯片本体**上——用户：「点击区域芯片则取消选中该区域」）。
   它探出芯片一点点，所以行与面板都别裁（默认就是不裁的）。
   ⚠ 比例尺那一行**不再用这颗徽标**了（用户 2026-10-04 第二十九次：改成像区域那行
   「＋」那样的图标按钮），所以这儿只剩区域芯片一个用户。 */
.cf-region-edit {
  position: absolute;
  top: -6px;
  right: -6px;
  display: grid;
  place-items: center;
  box-sizing: border-box;
  width: 16px;
  height: 16px;
  padding: 0;
  border: 1px solid var(--line-2);
  border-radius: 50%;
  background: var(--surface);
  color: var(--ink-2);
  cursor: pointer;
}
.cf-region-edit svg {
  display: block;
  width: 9px;
  height: 9px;
}
/* 编辑中：名字**就地换成输入框**，芯片**变空心**（底色由 JS 撤掉、字与描边用区域色，
   见 `startRename`）——一眼看出"这会儿在改名字"，也看得清输入框的边界。
   ⚠ 宽度**按名字的实宽**由 JS 定（`measureText`）：一个汉字约 2ch，拿 `ch` 估会把
   「客餐厅」这种三个字的名字当场裁掉（用户报过）。 */
.cf-region-input {
  display: block;
  box-sizing: border-box;
  min-width: 4ch;
  max-width: 16ch;
  padding: 0;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  font-weight: inherit;
  outline: none;
}


/* 那颗眼睛在纸的**左上角**（2026-10-11 从右上角挪过来，用户：「把眼睛按钮移到内容区左上角」）
   —— 右上角让给了撤销/重做那两颗 ✓ 一行四角各司其职：左上眼睛、右上撤销/重做、右侧是签列。 */
.cf-plan-view { position: absolute; top: 10px; right: 10px; z-index: 3; }
/* ⚠ 那颗眼睛**每个 tab 都露着**（用户 2026-10-04）：比例尺那一组的面宽/进深虚线也是叠在
   原图上的，切着看同样要它。所以这里没有"按分组藏"的规则了——从前那条
   `.cf-plan-view.is-group-hidden{display:none}` 连同 JS 里的 `syncBaseEye` 一并撤掉。 */

/* 比例尺那一行的两个数（面宽 × 进深，毫米）。窄、居中，像"填空"不像"输入框"——
   ⚠ **只读的值与可编辑的输入框共用这一条规则**（用户 2026-10-04 第二十九次：
   只读时数字就住在这个盒子里，点那颗按钮才变成能打字的）。同一个盒子是**有意的**：
   切换时这一行不跳、数字不移位。 */
.cf-dim-value,
.cf-dim-input {
  flex: 0 1 auto;
  /* 宽度按**5 位数**留（12500 这种，毫米量级最多 5 位——判据上限是 60000）。
     ⚠ 别退回 `8ch`：`box-sizing: border-box` 下还得扣掉左右内边距与描边，8ch 的盒子
     比里面的数宽出 ~20px，两个盒子就是 40px——正好把「（毫米）」那几个字挤到第二行
     （用户 2026-10-04 第三十三次报的，有用例钉着行高）。 */
  /* ⚠ `box-sizing: border-box` 下，内边距与描边**算在宽度里**——所以这两个数得跟着内边距走：
     6×2 ＋ 2 = 14。（第一版写成 22、又把内边距从 10 改到 6，盒子纹丝不动 63px，白改。） */
  width: calc(5ch + 14px);
  min-width: calc(5ch + 14px);
  box-sizing: border-box;
  /* 盒子照**芯片**那套（用户 2026-10-04：与芯片对齐）——同一套变量，不另写像素：
     `.cf-edit-btn` 是 `min-height: var(--cf-ctl-h)` + 药丸圆角 + `--surface-2` 底。
     ⚠ 左右内边距**比芯片紧**（6px）：这一行要塞"两个标签＋两个数＋单位＋按钮"，
     10px 的内边距让两个盒子多出 8px，正好把「（毫米）」挤下去（实测 300px 栏宽）。 */
  min-height: var(--cf-ctl-h);
  padding: 0 6px;
  border: 1px solid var(--line-2);
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--ink);
  font: inherit;
  font-size: 13px;
  text-align: center;
}
/* 这一行是全页字最多的一行，行距比别的两行紧 2px——为的是让「（毫米）」与那颗按钮
   留在同一行（实测：8px 行距时内容 314px，栏宽 300px）。 */
/* 「这是什么？」那颗小圈问号与它的一行回答（2026-10-07 ✓，见 ui/term-help.js）：
   问号要**低调**（不抢标签 ✓ —— 它是"需要时才问"的 ✓），回答占一整行 ✓（长句不挤布局 ✗）。 */
.term-q {
  display: inline-grid;
  place-items: center;
  box-sizing: border-box;
  width: 15px;
  height: 15px;
  padding: 0;
  border: 1px solid var(--line-2);
  border-radius: 50%;
  background: transparent;
  color: var(--ink-2);
  font: inherit;
  font-size: 10px;
  line-height: 1;
  cursor: pointer;
  vertical-align: middle;
}
.term-q.is-open {
  border-color: var(--accent);
  color: var(--accent);
}
.term-a {
  display: block;
  margin: 2px 0 0;
  color: var(--ink-2);
  font-size: 12.5px;
  line-height: 1.45;
}
/* 「有几处墙没对齐 · 接上」（2026-10-07，B 方案 ✓）：一句人话 ＋ 一颗小按钮 ✓。
   ⚠ 颜色取 `--accent`（主题色 —— 这是"应用建议你做的一件事" ✓，与"当前对象"的 `--node`、
   与门窗的 `--cf-opening` 都分开 ✓）。 */
/* ⚠ 2026-10-14：**账本那一行中间那颗「开始 / 继续 / 再量一遍」也穿这一套**
   （同一族"一句人话 ＋ 一颗小按钮" ✓）⇒ 选择器加上 `#cf-check-walk` ——
   长相只有这一处，改就是一起改 ✓（那一颗是向导唯一的门，见 `index.html` ✓）。
   ⚠ 2026-10-14 收尾：向导那一行"挑一件做"那两颗一度也穿这一套，**当晚改成 `.cf-tool`**
   （用户：「变成圆形＋尺 icon 再配文字的样式，对齐其它标签页的工具样式」✓）⇒
   它们不再进这一条 ✓（那一族是 32px 的圆钮 ＋ 名字，见 `.cf-tool` ✓）。 */
.cf-sew-go,
#cf-check-walk {
  flex: 0 0 auto;
  padding: 2px 10px;
  border: 1px solid var(--accent);
  border-radius: 999px;
  background: transparent;
  color: var(--accent);
  font: inherit;
  font-size: 12.5px;
  cursor: pointer;
}
/* 沿墙那一段的输入框：比芯片宽（要打字 ✓）。 */
.cf-walk-input {
  width: calc(7ch + 14px);
  min-width: calc(7ch + 14px);
}
/* 左右箭头 ＋ 中间那个「第几段/共几段」一起推到这一行的**右端**（用户第一百零八条第三条：
   「左箭头 第几段/总段数 右箭头（右对齐 tab 区）」✓）—— `margin-left: auto` 是这一页推东西
   到行尾的现成写法（「原图」那颗眼睛同款 ✓）。
   ⚠ 2026-10-12：夹在中间那枚计数器撤了（用户：「去掉操作区 index/total」✓）。
   ⚠⚠ 2026-10-13：**选择器从 `.cf-walk-nav:first-of-type` 改成写死 id** ——
   合并那一刀在行首加了一颗「‹ 账本」，它**也是一颗 button** ⇒ `:first-of-type` 当场被它
   抢走 ✗ ⇒ 两颗箭头不再被推到行尾（实测：右边空出 43px，用例「箭头贴着行尾」当场红 ✓）。
   **教训**：`:first-of-type` 这类"按位置"的选择器，**位置一变它就悄悄指错人** ——
   这一行本来就是在动的（加一颗按钮就中 ✗），写 id 才稳 ✓。 */
#cf-walk-prev {
  margin-left: auto;
}
/* 那一枚「第几段/共几段」芯片（`.cf-walk-count` ＋ `.cf-walk-cell` 两条）**2026-10-12 删了**
   —— 用户：「去掉操作区 index/total」✓（开表那件事当时搬到纸上那块进度饼上，饼 2026-10-13 也撤了 ✓）。
   ⚠ 别留死样式：那颗芯片的字号字色、定宽（防箭头左右跳）本来是照它写的 ✓，它没了，那几条也
   跟着没了 ✓（`--cf-ctl-h` 那一档高度归 `.cf-dim-value` 的共用规则管 ✓，这一行不会塌 ✓）。 */
/* 量房那一组**不展示门窗**（用户第一百一十三条）：量墙的时候，门窗那些符号只是干扰 ✓。
   ⚠ 用分组标记（`#cf-plan[data-cal-group]` ✓，与光标那两条同一个机制 ✓）—— 一条 CSS，不动 JS ✓。
   房间底色与房名留着：它们正是"我在哪一间"的线索 ✓。
   ⚠⚠ **第二圈（对门窗）例外**（2026-10-14）：那一圈干的就是"挨个对门窗"，看不见它们就没法干活 ✗
   —— 判据是 `data-walk-pass="opening"`（由 `applyCalGroup` 写 ✓，与 JS 那边 `hideOpenings()`
   判的同一件事 ✓）。那一扇"现在盯着它"的加粗走 `.cf-item.is-sel`（门窗那一组现成那套 ✓）。 */
#cf-plan[data-cal-group="photo"]:not([data-walk-pass="opening"]) #cf-g-overlay .cf-item,
/* ⚠⚠ **「墙体」那一屏也不展示门窗**（用户 2026-10-16：「切换到墙体标签页时能不能不要展示门窗？」✓）：
   那一页干的是**画墙 / 砌墙 / 拆墙**，门窗那些符号正压在墙线上、只是干扰 ✓（与量房那一屏同一条理由 ✓）。
   ⚠ 复用**同一条机制**（分组标记 ＋ 一条 CSS ✓，不动 JS ✓）；⚠ 只藏门窗那两族（`.cf-item` ✓）——
   房间底色、房名、墙线一个都不动 ✓。
   ⚠⚠ **墙上那几道口子留着**（没有跟着补上）：墙线是按门窗的跨度**切开**画的
   （`cutDoorGaps` ✓），而**能点的那几段也正是切开的这几段** ⇒ 补上口子就成了"看着有墙、
   点上去没东西" ✗✗（这一页最恨的就是"看得见的和点得动的不是一回事" ✓）。
   没有符号的缺口读起来就是"这儿有个洞" ✓ —— 与一张没画门符号的平面图同一个意思 ✓。
   （量房那一屏是**另一条路**：那边 `hideOpenings()` 连洞一起补，因为量的是"一整跑墙" ✓。） */
#cf-plan[data-cal-group="wall"] #cf-g-overlay .cf-item {
  display: none;
}
/* ⚠⚠ **四个分组各看各的图层**（用户 2026-10-16：「房间只展示色块、墙体只展示墙、门窗和量房在
   墙体的基础上加上门窗」✓）—— 一张表说清，别再零散地往各处加规则 ✗：

   | 那一屏 | 底色块 | 墙线 | 门窗 |
   | 房间   |   ✓   |  ✗  |  ✗  |
   | 墙体   |   ✗   |  ✓  |  ✗  |
   | 门窗   |   ✗   |  ✓  |  ✓  |
   | 量房   |   ✓   |  ✓  |  ✓（第二圈 ✓） |

   ⚠ 只动**看得见看不见**（`display` ✓）：一个数都不改 ✓ —— 点哪儿算哪儿是**按落点判**的
     （房间那一支走 `cfToNorm` ✓、量房点房间走 `.cf-walk-hit` ✓），不靠元素在不在 ✓。
   ⚠⚠ **量房那一行是问过的**（2026-10-16 ✓）：「在墙体基础上加门窗」这条与两条老规矩有出入
     —— ① 用户 10-06 定的"量墙的时候门窗只是干扰"（第一圈藏门窗 ✓）；② 那一屏一直留着
     **房间底色**，理由是"我在哪一间"（见上面 `.cf-item` 那条注释 ✓）。
     用户当场选的正是「**只改另三屏**」✓ ⇒ 量房**一行都不动** ✓（底色留着 ＋ 两圈照旧 ✓）。
     —— 别把今天这条新说法顺手盖到那两条老决定上 ✗。 */
/* 房间那一屏：**只有底色块**（墙线与门窗都收起来 ✓）—— 这一页干的是"一间一间过" ✓，
   墙线是「墙体」那一页的事 ✓（从前它一直在，用户 2026-10-16 点名去掉 ✓）。 */
/* ⚠ 画出来的那些墙（斜墙/曲墙 ✓）连**它那一族记号**一起收：弦、两端、拉弯那颗、
   命中的靶子、跟着手指跑的影子 —— 它们都属于"墙那一页" ✓（`edgeHandles` 里那一批
   `.cf-edge-*` ✓；⚠ 一个个列全，别用 `[class^=…]` 那种写法 ✗ —— 前缀选择器哪天有人
   多写一个类名排在前头就静默漏掉 ✓，这条规矩在整个文件里都是"宁可写长" ✓）。 */
#cf-plan[data-cal-group="zone"] .cf-wall,
#cf-plan[data-cal-group="zone"] .cf-open,
#cf-plan[data-cal-group="zone"] .cf-outline,
#cf-plan[data-cal-group="zone"] .cf-hitline,
#cf-plan[data-cal-group="zone"] .cf-edge-band,
#cf-plan[data-cal-group="zone"] .cf-edge-start,
#cf-plan[data-cal-group="zone"] .cf-edge-box,
#cf-plan[data-cal-group="zone"] .cf-edge-hit,
#cf-plan[data-cal-group="zone"] .cf-edge-grip,
#cf-plan[data-cal-group="zone"] .cf-edge-bow,
#cf-plan[data-cal-group="zone"] .cf-edge-dot,
#cf-plan[data-cal-group="zone"] .cf-edge-ghost,
#cf-plan[data-cal-group="zone"] .cf-item {
  display: none;
}
/* 墙体 / 门窗这两屏：**不画底色块**（房子由墙线说 ✓ —— 用户：「墙体只展示墙」✓）。 */
#cf-plan[data-cal-group="wall"] .cf-room,
#cf-plan[data-cal-group="opening"] .cf-room {
  display: none;
}
/* ⚠ 那一层的图形**不吃指针**（用户 2026-10-06 第二条：「不允许操作内容区、必须按预设顺序，
   这里不需要用户自由度太高」）⇒ 锚点与线段纯展示 ✓；要改的是那一行的芯片与箭头 ✓。 */
#cf-g-measure {
  pointer-events: none;
}
/* 画布上的锚点：已量＝实心、未量＝空心、**当前这一段**更亮（用户 2026-10-06 的规格 §5.2 ✓）。
   ⚠ 当前那一段的两端会**跳一下**（只在没有"减少动态效果"时 ✓）。 */
/* 当前这一段墙的**亮线**（`walkMarks` 画的那条 ✓）。
   ⚠⚠ 这一条 2026-10-12 **被我误删过**（那一下本意是删"圆圈号"那颗死样式 —— 它叫
   `.cf-walk-no`，与这一条**长得像一对** ⇒ 顺手一起删了 ✗）。后果是真静默的：选择器没了、
   声明还在 ⇒ CSS 恢复机制把那几行当垃圾吃掉 ⇒ 这条 `<line>` **一个样式都没有** ⇒
   SVG 的 `stroke` 初值是 `none` ⇒ **亮线整条不画** ✗（用例只量它的两个端点与图层显隐 ✓，
   量不出"它到底画没画" ✗ —— 是复查 diff 时才看出来的 ✓）。
   ⇒ 教训与工程里那条同源：**删"看着像一对"的东西之前，先确认另一个到底是谁** ✓。 */
.cf-walk-now {
  stroke: var(--node);
  stroke-width: 7;
  stroke-linecap: butt;
}
/* 「这是第几段」那颗**圆心＋数字**（用户 2026-10-12：「在墙体中央上方或右侧展示圆心+数字的
   index，横墙或横向曲墙在上方，竖墙或竖向曲墙在右侧」→ 当天改成：「圆圈+数字不要展示在
   上方或右侧了，展示在**墙体中心**」✓ —— 位置归 `walkIndexMark` ✓，这一条只管长相 ✓）。
   ⚠ 尺寸是**画布用户单位**（900 的 viewBox ⇒ 屏幕上约 ×0.35 ✓）：半径 22 ⇒ 直径约 15px、
   数字 28 ⇒ 约 10px —— 与量房表行首那颗号（16px ＋ 10px 字 ✓）看着是同一个东西 ✓。
   ⚠ 字号写 CSS 不写标记属性（与 `.cf-name` 那条同一个理由 ✓：属性最弱，改起来还得进 JS ✗）；
   基线也不用 `dominant-baseline`（旧浏览器上的表现不一 ✗）⇒ 由 JS 给 `y` 加半个字高 ✓。 */
.cf-walk-index circle {
  fill: var(--surface);
  stroke: var(--node);
  stroke-width: 3;
}
.cf-walk-index text {
  text-anchor: middle;
  font-size: 28px;
  font-weight: 700;
  fill: var(--node);
}
.cf-anchor {
  fill: var(--surface);
  stroke: var(--ink-2);
  stroke-width: 2;
}
.cf-anchor.is-done {
  fill: var(--node);
  stroke: var(--surface);
}
.cf-anchor.is-now {
  fill: var(--node);
  stroke: var(--surface);
  animation: cf-anchor-hop 1.1s ease-in-out infinite;
}
@keyframes cf-anchor-hop {
  0%, 100% { r: 6; }
  50% { r: 8.5; }
}
@media (prefers-reduced-motion: reduce) {
  .cf-anchor.is-now { animation: none; }
}
/* **只高亮正在测量的那一段**（用户第一百一十四条：「修改确认过的墙段不需要改变颜色，
   只需高亮正在测量的墙段即可」✓）⇒ 已测、待测都**保持墙本来的样子** ✓（那两个类还挂着，
   只是不再有样式 ✓ —— 将来想画进度，改 CSS 就够 ✓）。
   ⚠ **不用虚线**（这个画布上虚线已经归"虚墙" ✗）。 */
/* **当前这一间的底色**（用户 2026-10-07：「测量某个房间的墙段时、将该房间也高亮…会不会看上去
   更明显？」✓ 采纳 ✓）：一块极淡的中性色 —— 回答的是"**我在哪一间**" ✓（人量墙时最常问的一句 ✓），
   尤其是不规则房（一间由几块拼成 ✓，光看房名不知道它到哪儿为止 ✗）。
   ⚠⚠ **不是房间自己的颜色** ✗：那个颜色在这个 app 里是"这间选了哪种地板/材质" ✓
   （3D 地面、房间 tab 的地毯都用它 ✓）—— 拿它当"当前在这一间"，会被读成"材质已经定了" ✗。
   用 `--node`（＝"当前操作的对象"，与选中的门窗/墙段同一套语义 ✓）。
   ⚠ **淡**：这一组看的就是原图 ✓（锚点压在图上比数 ✓）⇒ 太重就盖掉参照物 ✗；
   而且要和那条亮线拉开（面淡、线最亮 ✓）。
   ⚠ 0.09 → 0.18 → **0.25**（用户 2026-10-07：「太淡了，深一点」→「调到 0.25」✓ ——
   这一档只有眼睛能定 ✓；0.25 已经明显盖住图上一部分纹理了，再往上就该挡住墙线/标注 ✗）。 */
.cf-walk-room {
  fill: var(--node);
  fill-opacity: 0.25;
  stroke: none;
}
/* ⚠⚠ **高亮是量房那一层自己画的一条线**（`#cf-g-measure`，`walkMarks` ✓），不再是"给画出来的
   墙线染色" ✗：一条画出来的墙线可能是**几间共线的一整条**（外墙常常是 ✓）⇒ 染色会整条亮 ✗
   （用户 2026-10-07：「能否只高亮锚点之间的那段，而不是与其他房间共线的一整段？」✓）。
   这一条线的 `x1..x2` **就是这一段墙的起止** ✓ —— 想多一点都不给 ✓。
   ⚠ `butt`（不是圆头）：圆头会从两个锚点往外各鼓半个线宽 ✗，而"锚点之间"是这句话的字面意思 ✓。 */
/* **斜墙的可编辑提示**（用户 2026-10-08 ✓）：选中的那条边上一条**虚线**（＝"这里可以动" ✓，
   与"虚墙"那条约定不撞 —— 虚墙是灰虚线的墙，这一条是**选中态**的提示 ✓，而且只在墙体这一组、
   只在选中它时出现 ✓）。⚠ 不用实线 ✓：实线看起来像"墙本来就是这样"，虚线才像"待调整" ✓。 */
.cf-edge-box {
  stroke: var(--node);
  stroke-width: 2.5;
  stroke-dasharray: 6 5;
  fill: none;
  /* ⚠⚠ **它不吃指针**（用户 2026-10-08：「斜墙不太好选中，而且没有悬停高亮」✓ 的另一半）：
     这条虚线框**正好压在弦上** ⇒ 它要是吃指针，选中的那条斜墙就**点不动、也悬停不上** ✗
     （实测：`elementsFromPoint` 最上面就是它，`pointerover` 的名字也是它 ✓ —— 悬停一条都不亮 ✓）。
     它是**看的**，不是靶子：拖端点靠端点那几颗胖靶子（`.cf-edge-hit` ✓），
     选它/悬停它靠弦自己那条线（下面垫着 `.cf-hitline` ✓）。 */
  pointer-events: none;
}
/* **两颗端点**手柄（用户 2026-10-08：「斜墙是不是不用旋转，只用 2 个锚点定位就行？」✓ ——
   两颗端点就是全部 ✓，中间那颗"旋转按钮"撤了 ✗）。
   ⚠⚠ **看得见的那颗不吃事件**：真正接手指的是它下面那颗透明的 `.cf-edge-hit` ✓ ——
   **与它同半径**（用户 2026-10-08：「端点的热区太大了，限制在端点圆圈范围内就行」✓）：
   从前那颗垫的是 `r=44`（圆圈的三倍 ✗）⇒ 顺手把旁边的东西全挡住 ✓（斜墙"不太好选中"
   有一半是它 ✓）。第一版反过来把 `r=9` 画成按钮本身 ⇒ 屏幕上只有 **7px** ✗✗（抓不住 ✓）。 */
.cf-edge-grip {
  /* ⚠⚠ **背景半透明**（用户 2026-10-08：「新建好的斜墙/曲墙的端点背景也改成半透明」＋
     「端点不透明导致无法确认落点是否在目标墙体上」✓）：实心那颗正好把底下的**墙线盖住** ✗
     ⇒ 看不清端点压在哪面墙上 ✓。⇒ 边框透出墙 ✓、圆心那颗点仍然标**精确位置** ✓
     —— 这正是用户当初定的"边框 ＋ 圆心" ✓，缺的只是让边框别挡墙 ✓。 */
  fill: var(--surface);
  fill-opacity: 0.45;
  stroke: var(--node);
  stroke-width: 2.5;
  pointer-events: none;
}
/* ⚠⚠ **两颗端点、圆框与圆心，统一用橙色**（用户 2026-10-08：「端点圆框和圆心统一为橙色」✓）。
   ⚠ 这条**推翻了同一天早先**的"两颗端点各用一种颜色"（那会儿是为了口头沟通方便 ✓）——
   加上第三颗"拉弯"手柄之后三种颜色反而花 ✓，用户当天定了统一 ✓。别再改回去 ✗。 */
[data-edge-grip="a"].cf-edge-grip,
[data-edge-grip="b"].cf-edge-grip,
[data-edge-grip="a"].cf-edge-dot,
[data-edge-grip="b"].cf-edge-dot {
  stroke: var(--node);
}
[data-edge-grip="a"].cf-edge-dot,
[data-edge-grip="b"].cf-edge-dot {
  fill: var(--node);
}
/* **圆心那颗点**（用户 2026-10-08：「锚点样式改成边框 ＋ 圆心」✓）：边框是**抓取范围** ✓、
   圆点是**精确位置** ✓ —— 它落在哪条墙上，这一段就落在哪儿 ✓；拖到墙上以外 ⇒ 松手丢掉这段斜墙 ✓。 */
.cf-edge-dot {
  fill: var(--node);
  stroke: none;
  pointer-events: none;
}
/* 拖动中、端点**没落在墙上** ⇒ 整条虚线变淡（"松手就丢" ✓ —— 别让它松手才突然消失 ✗）。 */
#cf-g-overlay.is-drop .cf-edge-box,
#cf-g-overlay.is-drop .cf-edge-grip,
#cf-g-overlay.is-drop .cf-edge-bow,
#cf-g-overlay.is-drop .cf-edge-dot {
  opacity: 0.3;
}
/* **幽灵**：拖动中预告"松手会吸到哪儿"（用户 2026-10-08 那句「松手吸附到最近的墙线」✓）——
   手指底下那颗跟着手走，这一颗钉在墙上 ⇒ 要跳的那一下**先看得见** ✓。
   ⚠ 它不吃事件（手指底下那颗才是活的 ✓）；颜色跟着**正在拖的那颗端点**走
   （`data-which` ✓ —— 与两颗端点各一色同一套 ✓），"橙的在拖"一眼分得清 ✓。 */
.cf-edge-ghost {
  fill: none;
  stroke: var(--node);
  stroke-width: 2;
  stroke-dasharray: 4 4;
  pointer-events: none;
  opacity: 0;
}
.cf-edge-ghost[data-on="1"] {
  opacity: 0.9;
}
/* ⚠ 幽灵也不再按端点分色（与上面同一条 ✓：统一橙 ✓）。 */

/* 「＋ 新建」的两个落点：**起点那颗点** ＋ 从它拉向手指的**橡皮筋** ✓
   （画一条弦的时候，得看得见"这条弦从哪儿起" ✓）。 */
.cf-edge-start {
  fill: var(--node);
  stroke: var(--surface);
  stroke-width: 2;
  pointer-events: none;
}
.cf-edge-band {
  stroke: var(--node);
  stroke-width: 2;
  stroke-dasharray: 6 5;
  opacity: 0.75;
  pointer-events: none;
}
/* **拉弯那颗**（用户 2026-10-08「先做 2」✓ 曲墙）：与两颗端点**长得不一样** ✓ ——
   空心环、没有圆心那颗点 ⇒ "它管的是别的事" 一眼看得出来 ✓（端点管定位 ✓、它管往哪边鼓 ✓）。 */
.cf-edge-bow {
  fill: var(--surface);
  fill-opacity: 0.45;
  stroke: var(--node);
  stroke-width: 2.5;
  pointer-events: none;
}
.cf-edge-hit {
  fill: transparent;
  stroke: none;
  pointer-events: all;
  cursor: grab;
}
.cf-edge-hit:active {
  cursor: grabbing;
}
/* **点哪间就跳哪间**（用户 2026-10-07：「能不能点击房间直接切换到测量该房间的第一段墙？」✓）：
   一间一块**透明的命中块** —— **不画东西，只吃事件**（与门窗那条 `.cf-hit` 同一手 ✓）。
   ⚠ 图层自己还是 `pointer-events: none`（"内容区不吃指针"那条不变 ✓），**只有这些块**自己开 ✓
   ⇒ 切到别的组时这一层 `display:none`，点墙/涂格子的手势一点不受影响 ✓。
   ⚠ 抬手时离按下处 ≥ 4px 就算拖动（平移/缩放 ✓）⇒ JS 那边直接丢掉这一下 ✓。 */
.cf-walk-hit {
  fill: transparent;
  stroke: none;
  pointer-events: all;
}
/* ⚠ 2026-10-16：这里原本挂着一截**没有选择器的声明**（`stroke: var(--node); stroke-width: 7;
   stroke-linecap: butt; }` ✓）—— 是那条"当前这一段墙的亮线"（`.cf-walk-now` ✓）**上一次被误删**
   留下的残骸：选择器补回去了（在文件另一处 ✓），这一截忘了删 ✗ ⇒ 整个 CSS **文件从此是语法错的**
   （浏览器会恢复：把这几行当垃圾吃到 `}` 为止 ✓ 所以屏幕上一直看不出来 ✗；而 prettier / 任何
   解析器一读就报 `Unexpected }` ✓）。它**长得跟 `.cf-walk-now` 一模一样**，所以按"看着像就删"办 ✓。
   ⚠ 教训与前一处同源：**删一半比不删更糟** —— 补回选择器的时候要把残骸一起收走 ✓。 */
#cf-photo-bar {
  /* ⚠ 间距 6 → **4**（用户 2026-10-06 加「(mm)」那一下量出来的）：这一行可用宽只有 **300px**
     （父容器 320 − 内边距），三个标签＋三枚芯片＋「(mm)」＝271，间距 6px×6＝36 ⇒ 一共 307 ✗
     差 7px 就换行（实测行高 58px）。收到 4px 省 12px ⇒ 放得下 ✓。
     ⚠ 只改这一行：别的行（区域/墙体/门窗）内容没这么挤，别跟着动 ✗。 */
  gap: 4px;
}
/* 比例尺每个数**自己一层盒子**：只为托住它右上角那颗 ✎ 徽标（用户第一百零三次第一条）。
   ⚠ 盒子本身**不排布**里面的东西（`inline-flex` 只为让"值 ⇄ 输入框"叠在同一个位置 ✓）——
   间距归这一行的 `gap` 管，别写第二套像素 ✗；`min-width: 0` 让它照旧能被窄屏压缩 ✓。 */
.cf-dim-cell {
  position: relative;
  display: inline-flex;
  align-items: center;
  flex: 0 1 auto;
  min-width: 0;
}
/* ⚠ 这两个数上的徽标**往盒子角里再收 3px**（用户第一百零三次：「位置有点偏右上了，可以参考
   区域 tab 区的样式」）。⚠ 量出来两处本来就是同一个偏移（都压在角内 2px ✓），差的是**对比**：
   区域那颗压在**填色**的芯片上，贴着角一眼看得出 ✓；这两个压在**浅底**的盒子上，同样的偏移
   看着像浮在外面 ✗。收进来之后它明确压住盒子的圆角 ✓（`-3px` 是照着"看得出压住"定的）。 */
/* ⚠⚠ 这两颗徽标的偏移**按用户的两轮反馈定**：-6px 说"太靠右上"✗、-1px 说"太靠左下"✗
   ⇒ 取中间 **-4px**（徽标中心落在盒体里 4px，明确压住圆角那一弯 ✓）。
   ⚠ 与区域那颗（`.cf-region-edit` 的 -6px）**本意是一套**：它那边盒子带 1px 描边 ⇒ 实测
   落在角外 5px ✓；这里盒子是**大圆角药丸**，角上那一块本身就没画出来 ⇒ 同样的偏移看着更"浮" ✗
   （-6 时实测两处只差 1px，可看起来就是不一样 ✓）。所以这一处**故意**比那边收得更多 ✓。 */
.cf-dim-cell > .cf-region-edit {
  top: -4px;
  right: -4px;
}
/* ⚠ 写成"**凡是 dim 芯片里的徽标**"而不是逐个数 id（用户 2026-10-06：「层高和墙段 chip 的
   编辑 badge 位置有点偏，应对齐面宽和进深的样式」✓）：层高与墙段那两颗就是**漏掉**的 ✗
   （实测：面宽/进深 -4/-4 ✓，它们还是默认的 -6/-6 ✗）。以后再加芯片也不会漏 ✓。 */
/* 只读那一半要自己把字摆正（`min-height` 撑着的是盒子，文字默认贴顶）。 */
.cf-dim-value {
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.cf-dim-input:focus { outline: none; border-color: var(--node); }
/* ⚠ 这一行的字要跟「区域」那一行**同一个字号**（用户 2026-10-04）：那边是
   `.cf-region-hint` 的 12.5px（说明文字）＋ `.cf-edit-btn` 的 13px（控件）；这边照抄这两档——
   `.cf-dim-label` / `.cf-dim-x` 是说明文字（12.5px），`.cf-dim-input` 是控件（13px）。
   （第一版忘了给 `.cf-dim-label` 定字号，它按默认的 15px 渲染，比区域那行大一截。） */
.cf-dim-x,
.cf-dim-label {
  flex: 0 0 auto;
  color: var(--ink-2);
  font-size: 12.5px;
}

/* 「更多尺寸」那一折（用户 2026-10-05 第五十条）：这一页只管位置/宽度/归属，其余的折起来。
   ⚠ 关着的时候里面的字段**不可见**（`<details>` 的原生行为）——用例 fill 之前要先把它 open ✓。 */
.opening-more { margin-top: 10px; }
.opening-more > summary {
  cursor: pointer;
  font-size: 12.5px;
  color: var(--ink-2);
  padding: 6px 0;
}
.opening-more[open] > summary { margin-bottom: 6px; }


/* （2026-10-13 **拖放撤了**，这一族样式跟着删 ✓：`.cf-drag-ghost` 那颗抬高 28px 的幽灵、
   `.is-drag` 那三条"会落在哪条墙上"的高亮、以及两颗按钮的 `touch-action:none`
   —— 全部只服务那一条路 ✓。取而代之的是 **落点预览**（`.cf-opening-preview` ✓，
   在"目标格"那一段旁边 ✓）：它画在**画布坐标**里（不是跟着手指的浮层 ✓），
   所以缩放/平移之后照旧贴着那条墙 ✓ —— 幽灵那一版做不到这一点 ✗。）

/* **多档开关**（分段开关，用户 2026-10-05 第七十三条）：「类型」那一行用的就是它 ——
   一条药丸轨道 ＋ 一颗**会滑的填充块** ＋ N 段文字，选中那段白字加粗 ✓。
   ⚠ 它长得像「虚实」那颗 switch，但**不是开关**：开关是两态（开/关），这是**从 N 个里选一个**
   （今天两档：单开 / 移门）⇒ 标记上用 `role="radiogroup"` ✓。
   段数 `--seg-n` 与"选中第几格" `--seg-i` 都由 JS 照 `DOOR_TYPES` 写在轨道上 ⇒
   **加一个类型＝往那个数组里加一行**，这里一个像素都不用动 ✓。
   （2026-10-06 用户第九十五次要求把「类型」加回编辑表 ⇒ 这套样式从 0237d84 里原样取回 ✓。） */
.cf-seg {
  position: relative;
  display: flex;
  flex: 0 0 auto;
  padding: 2px;
  border: 1px solid var(--line-2);
  border-radius: 999px;
  background: var(--surface-2);
}
/* 填充块：绝对定位的包含块是轨道的**内边距盒** ⇒ `100%` ＝ 内宽（含左右各 2px 内边距），
   所以宽度写 `calc((100% - 4px) / N)`、左起 2px，正好一格 ✓（`translateX(100%)` ＝ 走一格 ✓）。
   ⚠ 别改成"按段算 left"：段宽是浏览器分的，算出来的和画出来的对不上。 */
.cf-seg-thumb {
  position: absolute;
  top: 2px;
  bottom: 2px;
  left: 2px;
  width: calc((100% - 4px) / var(--seg-n, 2));
  border-radius: 999px;
  background: var(--node);
  transform: translateX(calc(var(--seg-i, 0) * 100%));
  transition: transform 0.18s ease;
}
/* 每一段：**等宽**（`flex: 1 1 0` ＋ 同一个 `min-width`）——填充块是按等分算的，
   长短不一它就走偏 ✗。`z-index` 压住填充块，字才看得见 ✓。
   ⚠ 高度照 `--cf-ctl-h` 减掉轨道的 2×2 内边距与 2×1 描边 ⇒ 整条正好与右列那颗
   `.cf-dim-input`（洞口净宽）**一样高** ✓（同一行里两个控件不一样高最扎眼）。 */
.cf-seg button {
  position: relative;
  z-index: 1;
  flex: 1 1 0;
  min-width: 3.6em;
  min-height: calc(var(--cf-ctl-h) - 6px);
  padding: 0 12px;
  border: 0;
  border-radius: 999px;
  background: none;
  color: var(--ink);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
}
.cf-seg button.is-on {
  color: #fff;
  font-weight: 700;
}
/* ⚠⚠ 墙体那一行的分段开关（`#cf-wall-seg`）**2026-10-13 撤了**（用户：「切换这个改成 2 个：
   砌墙、拆墙」✓）⇒ 那两条"逐项照 tab 那套"的覆盖（`#cf-wall-seg button:not(.is-on)` 等）
   **一并删掉** ✓ —— 与 `.cal-tool` 那次同一个道理：元素没了，样式还留着就是没人读的死代码 ✗。
   ⚠ `.cf-seg` 组件**本身照旧**（「外观」那一组与门型那条还在用 ✓）：删的只是这里那一处覆盖 ✓。 */

/* 表里那三条属性行（类型 / 洞口净宽 / 开启方向）：**说明在左、控件在右**，
   而且**左列等宽**（用户 2026-10-05 第六十七条：宽度不一致会换行 ✗）。
   `min-width` 取「洞口净宽」四个字的宽度 —— 最长的那一个说了算 ✓，其余跟它对齐 ✓。
   ⚠ 输入框本身借 `.cf-dim-input`（＝比例尺那一行的两个数，同一套药丸盒子 ✓），这里只管摆法。 */
.opening-attr-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-top: 8px;
}
/* ⚠⚠ **2026-10-16 用户两条**（「量房标签页门窗编辑 Sheet 尺寸框宽度增大 1/2、尺寸行间距太大了」✓）：
   · 输入框 **× 1.5**（`5ch + 14px` ⇒ `7.5ch + 21px` ✓）—— 毫米那五位数看着从容一些 ✓
     （⚠ 内边距 6×2 ＋ 描边 2 ＝ 14，按 1.5 倍就是 21 ✓，与上面那条同一个算式 ✓）；
   · 行距**收掉一半**：`.opening-attr-row` 自己那条 `margin-top: 8px` 与表单的 `gap: 12px`
     **叠在一起**（＝ 20px ✗）⇒ 表里那条去掉 ✓、表单的 gap 收到 8px ✓ ⇒ 净行距 8px ✓。 */
.opening-form { gap: 8px; }
.opening-form .opening-attr-row { margin-top: 0; }
.opening-form .cf-dim-input {
  width: calc(7.5ch + 21px);
  min-width: calc(7.5ch + 21px);
}
.opening-attr-row > .section-label {
  flex: 0 0 auto;
  min-width: 5em;
}

/* 启动兜底提示（见 index.html）：应用没起来时把话说出来，别让人对着一块空白猜。
   ⚠ 它要在**样式半残**的情况下也能读：颜色不用变量、布局用最土的写法。
   ⚠ `[hidden]` 那条**必须写**：上面给了 `display:flex`，会盖掉浏览器默认的
   `[hidden]{display:none}` —— 少了它，提示会一直挂在那儿（这个坑工程里踩过）。 */
#boot-hint { position:absolute; inset:0; z-index:99; display:flex; align-items:center; justify-content:center; box-sizing:border-box; padding:32px; background:#f4f1ea; color:#2b2a26; font-size:14px; line-height:1.8; text-align:center; }
#boot-hint[hidden] { display:none; }
