/*
 * app.css —— 骨架与组件
 *
 * 手机：顶栏 + 内容 + 底部 Tab。
 * 网页（≥900px）：左侧栏 + 内容（+ 之后的第三栏放 3D 属性面板）。
 * **一套数据两套壳** —— 只有布局换，屏本身不换。
 */

*, *::before, *::after { box-sizing: border-box; }
html, body { height: 100%; }
body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font: var(--f-md)/1.55 var(--font);
  -webkit-font-smoothing: antialiased;
  overscroll-behavior-y: none;
}
button, input, select, textarea { font: inherit; color: inherit; }
a { color: var(--brand); text-decoration: none; }
:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; border-radius: var(--r-sm); }
.num { font-family: var(--font-num); font-variant-numeric: tabular-nums; }
.hide { display: none !important; }

/* ══ 骨架 ══════════════════════════════════════════════ */
#app { min-height: 100%; display: flex; flex-direction: column; }

.topbar {
  position: sticky; top: 0; z-index: 20;
  height: calc(var(--topbar-h) + var(--safe-t));
  padding: var(--safe-t) var(--sp-3) 0;
  display: flex; align-items: center; gap: var(--sp-2);
  background: var(--bar-bg); backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--line-soft);
}
.topbar h1 { flex: 1; margin: 0; font-size: var(--f-md); font-weight: 600; letter-spacing: .01em; }
.topbar .spacer { flex: 1; }

main { flex: 1; padding: var(--sp-4); padding-bottom: calc(var(--tabbar-h) + var(--sp-6)); }
main.plain { padding-bottom: var(--sp-6); }
.wrap { max-width: 720px; margin: 0 auto; }

/* 底部 Tab —— 手机主导航 */
.tabbar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 30;
  height: var(--tabbar-h); padding-bottom: var(--safe-b);
  /* ㊿ 从四格收成三格。**别再写死格数** —— 写死过一次（4），
     页签改成三个时第四格空在那儿，三个图标全挤在左边。
     auto-flow 让它数 TABS 有几项就分几格。 */
  display: grid; grid-auto-flow: column; grid-auto-columns: 1fr;
  background: var(--tabbar-bg); backdrop-filter: blur(12px);
  border-top: 1px solid var(--line);
}
.tabbar button {
  border: 0; background: none; cursor: pointer;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 3px; color: var(--fg2); font-size: var(--f-xs);
  min-height: var(--tap); transition: color var(--t) var(--ease);
}
.tabbar button .ic { width: 22px; height: 22px; }
.tabbar button[aria-current="page"] { color: var(--brand); }
.tabbar button[data-soon] { opacity: .42; }
.tabbar .badge {
  position: absolute; transform: translate(14px, -12px);
  min-width: 17px; height: 17px; padding: 0 4px; border-radius: var(--r-pill);
  background: var(--warn); color: #000; font-size: 11px; font-weight: 700;
  display: grid; place-items: center;
}

/* 网页三栏：左栏取代底部 Tab */
@media (min-width: 900px) {
  #app { flex-direction: row; }
  .tabbar {
    position: sticky; top: 0; bottom: auto; height: 100vh; width: 216px;
    /**
     * ⚠️ 手机那边是 `grid-auto-flow: column`（一行几格自己数）。
     * 宽屏这边要**把它掰回竖的** —— 只写 grid-template-columns: 1fr 是不够的：
     * auto-flow: column 会无视模板列，继续往右边排，
     * 于是左栏里三个图标横着挤成一条，标签被压成「项 / 目」两行
     * （㊿ 在业主自己的 Chrome 上截图才看见的）。
     * 一个属性改了两处布局，另一处就得跟着改 —— 这条走查抓到过第二次。
     */
    grid-auto-flow: row; grid-template-columns: 1fr; grid-auto-rows: min-content;
    align-content: start; gap: 2px; padding: var(--sp-4) var(--sp-2);
    border-top: 0; border-right: 1px solid var(--line);
  }
  .tabbar button {
    flex-direction: row; justify-content: flex-start; gap: var(--sp-3);
    padding: 0 var(--sp-3); border-radius: var(--r); font-size: var(--f-md);
  }
  .tabbar button:hover { background: var(--s2); color: var(--fg); }
  .tabbar button[aria-current="page"] { background: var(--brand-wash); color: var(--brand); }
  .tabbar .brand {
    padding: var(--sp-2) var(--sp-3) var(--sp-5);
    font-size: var(--f-lg); font-weight: 650; color: var(--fg); letter-spacing: -.01em;
    white-space: nowrap; overflow: hidden;
  }

  /**
   * ⚠️ 「收起左栏」整套拿掉了。
   * ㊿ 它的开关本来挂在顶栏最左，而那个位置现在是抽屉的 ☰（业主要的栏目）。
   * 开关没了、`rail-min` 这个类却还存在 localStorage 里 ——
   * 于是之前收起过的人一进来就是三个没名字的图标，**再也展不开**
   * （业主自己那台就是这样，截图里左栏只剩三个小图标）。
   * 导航现在有抽屉兜着，左栏只当「三步快捷键」，一直带字。
   */
  .col { flex: 1; min-width: 0; display: flex; flex-direction: column; }
  main { padding: var(--sp-5); padding-bottom: var(--sp-6); }
  .wrap { max-width: 860px; margin: 0; }
  /*
   * ⑱：`margin: 0` 是对的**只在左边有那条侧导航的时候** —— 内容挂着侧栏走。
   * 没有侧导航那几屏（隐私政策 · 服务条款 · 分享链接，都是 main.plain）
   * 于是把 860 的正文柱子贴在最左边，右边空掉四百多像素 ——
   * 屏幕上看着像页面加载了一半（◇ 设计标准 B#15）。这几屏要居中。
   */
  main.plain > .wrap { margin: 0 auto; }
}
@media (max-width: 899px) {
  .tabbar .brand { display: none; }
  /**
   * 手机上没有左栏，这颗按钮就不该出现。
   * ⚠️ 第一版写成 `.rail-toggle { display: none }` —— **压不住**：
   * 那时候它住在底部页签里，而 `.tabbar button`（0,1,1）比 `.rail-toggle`（0,1,0）重，
   * 于是它在手机上变成了底部第四格（业主窗口截图才看见）。
   * 现在按钮在顶栏，藏它的规则也就干净了。
   */
  .topbar .rail-toggle { display: none; }
}

/* ══ 控件 ══════════════════════════════════════════════ */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-2);
  min-height: var(--tap); padding: 0 var(--sp-4);
  border: 1px solid var(--line); border-radius: var(--r);
  background: var(--s2); color: var(--fg); cursor: pointer;
  font-weight: 550; transition: background var(--t) var(--ease), border-color var(--t) var(--ease);
}
.btn:hover:not(:disabled) { background: var(--s3); }
.btn:disabled { opacity: .45; cursor: not-allowed; }
.btn.primary { background: var(--brand); border-color: var(--brand); color: var(--brand-ink); }
.btn.primary:hover:not(:disabled) { background: #7fcdbb; }
.btn.ghost { background: none; border-color: transparent; color: var(--fg2); }
.btn.ghost:hover:not(:disabled) { background: var(--s2); color: var(--fg); }
.btn.danger { color: var(--danger); border-color: var(--danger-line); background: var(--danger-wash); }
.btn.block { width: 100%; }
/*
 * ⑯：宽屏上别把「加一个房间」这种整行按钮拉成 860px ——
 * 它会比屏幕的主角（那张俯视图）还抢眼，正是「把手机版拉宽」那一类（◇B#15）。
 * 窄屏照旧整行（手指点得到）；≥700px 封到 360。
 */
@media (min-width: 700px) {
  .btn.block { max-width: 360px; }
  /*
   * ⑰：**已经在窄栏里的整行按钮不许再封**。
   * ⑯ 那条封宽只在建模屏 / 房间屏验过 —— 登录和注册那两屏的主按钮也吃到了它：
   * 输入框 400 宽、按钮 360 宽、左边缘对齐，于是**右边整整差 40px**。
   * 那是人打开 App 看到的第一屏，两条边对不齐一眼就看得出来（◇ 设计标准 B#8）。
   * `.auth` 本来就是 min(400px,100%)、`.slides` 是 420 —— 它们不是「把手机版拉宽」，
   * 封了反而制造错位。
   */
  .auth .btn.block, .slides .btn.block, .splash .btn.block { max-width: none; }
}

/* ⑯ 数字输入格：窄屏铺开、宽屏封住 —— 四位数不该占 390px（◇B#9） */
.numfield { flex: 1 1 96px; min-width: 96px; }
@media (min-width: 700px) { .numfield { max-width: 150px; } }
/* 「我的」那张卡上的名字 —— 字距见 screens/me.js 的头注（⑱） */
.me-name { font-size: var(--f-lg); font-weight: 650; letter-spacing: .06em; }

/* 两个数字格中间那个「×」—— 和格子对齐，不是谁的抬头（⑱，见 screens/nest.js） */
.xsep { color: var(--fg2); padding-bottom: 10px; flex: 0 0 auto; }
.btn.sm { min-height: 34px; padding: 0 var(--sp-3); font-size: var(--f-sm); font-weight: 500; }
.icon-btn {
  width: var(--tap); height: var(--tap); display: grid; place-items: center;
  border: 0; background: none; color: var(--fg2); border-radius: var(--r); cursor: pointer;
}
.icon-btn:hover { background: var(--s2); color: var(--fg); }
/* ㊿ 顶栏右上那颗头像 = 「我的」。红点从底部页签跟着搬上来，
   所以这颗必须是定位父级 —— .dot-badge 是 absolute 的，
   没有它红点会飞到屏幕角上（⑧ 在别处栽过同一个坑）。 */
.me-btn { position: relative; }
.me-btn[aria-current="page"] { color: var(--brand); }

.field { margin-bottom: var(--sp-4); }
.field > label { display: block; font-size: var(--f-sm); color: var(--fg2); margin-bottom: var(--sp-2); }
.field .hint { font-size: var(--f-xs); color: var(--fg2); margin-top: var(--sp-2); }
.field .err { font-size: var(--f-xs); color: var(--danger); margin-top: var(--sp-2); }
input[type=text], input[type=password], input[type=email], input[type=tel],
input[type=number], input[type=search], textarea {
  width: 100%; min-height: var(--tap); padding: 0 var(--sp-3);
  background: var(--s2); border: 1px solid var(--line); border-radius: var(--r);
  color: var(--fg); transition: border-color var(--t) var(--ease);
}
input::placeholder { color: var(--fg3); }
/**
 * ⚠️ `outline: none` **只给鼠标**。
 * 以前这一行是无条件的 `outline: none`，它的选择器权重（0,1,1）压过了
 * 全局那条 `:focus-visible { outline: 2px solid var(--brand) }`（0,1,0）——
 * 于是**用键盘 Tab 走参数表单的人，从头到尾看不出光标在哪一格**
 * （⑭ 机器扫出来 10 屏 13 处，包括登录、改密码、邀请码这些非填不可的屏）。
 * 现在：鼠标点的时候安安静静只换个边框色；键盘走的时候把品牌色的环还给它。
 */
input:focus, textarea:focus { border-color: var(--brand-dim); }
input:focus:not(:focus-visible),
textarea:focus:not(:focus-visible) { outline: none; }
input[aria-invalid="true"] { border-color: var(--danger); }
/**
 * 51 原生 <select> 的那一层皮（appearance:none + 两条渐变画的小三角）撤掉了 ——
 * 全 App 一个 `h('select'` 都不剩，下拉一律走 ui.js 的 `choice`（`.ch-*`）。
 * 留着这段的话，将来谁手写一个 <select> 会「看起来像那么回事」而键盘和说明都缺，
 * 反而更难发现（◇ 设计标准 C：别装成能用的样子）。
 */
.code-input { text-align: center; letter-spacing: .5em; font-family: var(--font-num);
  font-size: var(--f-xl); text-transform: uppercase; height: 64px; }

.row { display: flex; align-items: center; gap: var(--sp-3); }
.row.between { justify-content: space-between; }
.check { display: flex; align-items: center; gap: var(--sp-2); min-height: var(--tap); cursor: pointer; }
.check input { width: 18px; height: 18px; accent-color: var(--brand); }

/* ══ 卡片 / 列表 ══════════════════════════════════════ */
.card {
  background: var(--s1); border: 1px solid var(--line); border-radius: var(--r);
  padding: var(--sp-4); transition: border-color var(--t) var(--ease), background var(--t) var(--ease);
}
.card.tap { cursor: pointer; }
.card.tap:hover { border-color: var(--brand-dim); background: var(--s2); }
.list { display: grid; gap: var(--sp-3); }
.list.grid { grid-template-columns: repeat(auto-fill, minmax(248px, 1fr)); }

.pill {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 2px var(--sp-2); border-radius: var(--r-pill);
  font-size: var(--f-xs); background: var(--s2); color: var(--fg2); border: 1px solid var(--line);
}
/* ⑲：药丸里可以挂一个图标（🔒 换成 ICON.lock）—— 尺寸只在这里定一次 */
.pill .ic { width: 13px; height: 13px; }
.pill.warn { background: var(--warn-wash); color: var(--warn); border-color: var(--warn-line); }
.pill.brand { background: var(--brand-wash); color: var(--brand); border-color: var(--brand-line); }

.menu {
  background: var(--s2); border: 1px solid var(--line); border-radius: var(--r);
  padding: var(--sp-1); box-shadow: var(--shadow); min-width: 180px;
}
.menu button {
  display: flex; width: 100%; align-items: center; gap: var(--sp-3);
  min-height: 40px; padding: 0 var(--sp-3); border: 0; border-radius: var(--r-sm);
  background: none; color: var(--fg); text-align: left; cursor: pointer;
}
.menu button .mi-k { flex: 1; }
.menu button .ic { width: 15px; height: 15px; color: var(--brand); }
.menu button:hover { background: var(--s3); }
.menu button.danger { color: var(--danger); }

/* 设置里的行 */
.rows { background: var(--s1); border: 1px solid var(--line); border-radius: var(--r); overflow: hidden; }
.rows > * + * { border-top: 1px solid var(--line-soft); }
.rowitem {
  display: flex; align-items: center; gap: var(--sp-3); width: 100%;
  min-height: 52px; padding: var(--sp-2) var(--sp-4);
  background: none; border: 0; color: var(--fg); text-align: left; cursor: pointer;
}
.rowitem:hover { background: var(--s2); }
.rowitem .k { flex: 1; }
/* ㉔ 采购件是哪个柜的 —— 和料单「编号」那一栏同一档弱色，不跟件名抢（◇B#8） */
.rowitem .cabtag { flex: 0 0 auto; color: var(--fg2); font-size: var(--f-sm); }
.rowitem .v { color: var(--fg2); font-size: var(--f-sm); }
.rowitem.static { cursor: default; }
.rowitem.static:hover { background: none; }
/*
 * ⑯ 窄屏上这一行三段并排会把左边那句问话压成**一个字一行**（.k 是 flex:1，
 * 而中文的 min-content 就是一个字；右边那串裁定值和按钮都不肯让）。
 * 待确认屏「已裁定」那几条实测就是这样（⑧½ #22 在对照表上抓过同一类）。
 * 竖排：问句一行、值和按钮一行。
 *
 * ⚠️ ⑲ 收窄了适用范围：⑯ 写的是 `.rowitem.static`，于是**所有**设置行都跟着竖排 ——
 * 「我的」里那条「版本 / 0.1.0」在手机上被拆成两行、比左右邻居高一倍，
 * 而它的值只有五个字符，根本不挤（⑯⑰⑱ 连着三轮的那一族：改全局跑到别的屏上去）。
 * 现在要竖排的自己挂 `.long` —— 值真的长的那几处（待确认「已裁定」/ 分享链接）。
 */
@media (max-width: 560px) {
  .rowitem.static.long {
    flex-direction: column; align-items: stretch; gap: var(--sp-2);
    padding-top: var(--sp-3); padding-bottom: var(--sp-3);
  }
  .rowitem.static.long .v { overflow-wrap: anywhere; }
  /* 竖排之后按钮别被 stretch 拉成整行 —— 那看着像浮在中间 */
  .rowitem.static.long > .btn { align-self: flex-start; }
}
.section-title { font-size: var(--f-xs); color: var(--fg2); text-transform: uppercase;
  letter-spacing: .08em; margin: var(--sp-5) 0 var(--sp-2); }

/* ══ 空态 / 错误态 / 骨架屏 ══════════════════════════════ */
.empty { text-align: center; padding: var(--sp-7) var(--sp-4); }
/*
 * ⑰：桌面上空态别吊在最上面。
 * 一屏 900 高，插画+一句话+一个按钮占了上面三分之一，**下面 45% 是一片死黑**——
 * 那不是「留白成节奏」，那是一屏没做完的样子（◇ 设计标准 B#9 / C）。
 * 空态只有一份实现（规则 239），所以这一条改一处，通知 / 封板存档 / 改动记录 /
 * 拆单 / 排版 / 项目列表 那几屏的空态一起端正。
 * 只在**够高**的窗口上居中：矮窗口居中会把插画顶出可视区。
 */
@media (min-width: 700px) and (min-height: 620px) {
  .empty { min-height: min(64vh, 560px); display: grid; align-content: center; }
  /*
   * ⑱：**一屏里的一段**不许吃这条居中。
   * 框柜子那一屏图看完了，「这张图上认过的」下面顶出 560px 空档 ——
   * 那句「这张图还没有框过」跑到屏幕外面，要往下滚半屏才看得见。
   * 是整屏还是一段，只有调用处知道（ui.js 的 emptyState({ inline })）。
   */
  .empty.inline { min-height: 0; }
}
.empty svg { width: 168px; height: 126px; margin-bottom: var(--sp-4); }
.empty h2 { margin: 0 0 var(--sp-2); font-size: var(--f-lg); font-weight: 600; }
.empty p { margin: 0 0 var(--sp-5); color: var(--fg2); }
.empty .actions { display: flex; flex-direction: column; gap: var(--sp-3); max-width: 300px; margin: 0 auto; }

.errbox {
  border: 1px solid var(--danger-line); background: var(--danger-wash); border-radius: var(--r);
  padding: var(--sp-4); display: grid; gap: var(--sp-3);
}
.errbox .msg { color: var(--fg); }
.errbox .why { color: var(--fg2); font-size: var(--f-sm); }

.skel { background: linear-gradient(90deg, var(--s1) 25%, var(--s2) 37%, var(--s1) 63%);
  background-size: 400% 100%; animation: shimmer 1.4s ease infinite; border-radius: var(--r-sm); }
@keyframes shimmer { 0% { background-position: 100% 0 } 100% { background-position: -100% 0 } }
.skel-card { height: 92px; border-radius: var(--r); }
.skel-line { height: 12px; }

/* ══ Toast / 对话框 / 离线横幅 ══════════════════════════ */
/**
 * toast 现在住在底部那一摞里面（㊵）—— 位置由 `#bottombars` 说了算，
 * 这里只管「多宽、怎么居中」。以前它自己 `position: fixed` 在同一个 bottom 上，
 * 和 ㊵ 新加的那条**正好叠在一起**（375 上完全重合，1280 上压掉右边一截）——
 * 是把截图打开看才看见的，三语扫屏和 e2e 那一遍全绿（㉘ 那一课第 N 次）。
 */
#toasts { display: grid; gap: var(--sp-2); width: min(440px, 100%); margin: 0 auto; }
/* 一条 toast 都没有的时候不占位，否则那一摞里会多出一道空档 */
#toasts:empty { display: none; }
.toast {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4); border-radius: var(--r);
  background: var(--s3); border: 1px solid var(--line); box-shadow: var(--shadow);
  animation: rise var(--t-slow) var(--ease);
}
.toast.warn { border-color: var(--warn-line); }
.toast.error { border-color: var(--danger-line); }
.toast .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--brand); flex: none; }
.toast.warn .dot { background: var(--warn); }
.toast.error .dot { background: var(--danger); }
.toast .txt { flex: 1; font-size: var(--f-sm); }
.toast button { background: none; border: 0; color: var(--brand); cursor: pointer; font-weight: 600; }
/**
 * ㊵ 顺手收掉的：`@keyframes rise` 这个文件里**定义了两遍**（这儿一遍 10px、
 * 底部那一摞那儿一遍 12px）。CSS 里后面那一份说了算 —— 也就是 toast 一直
 * 用的是底下那一份，而这儿写的 10px **一天都没生效过**。
 * 同一个名字两份定义 = 规则 199 的 CSS 版。留底下那一份（今天真在跑的那个），
 * 行为一个像素没变；有一条回归钉着不许再定义第二遍。
 */

dialog {
  border: 1px solid var(--line); border-radius: var(--r-lg); background: var(--s1); color: var(--fg);
  padding: var(--sp-5); width: min(420px, calc(100vw - 32px)); box-shadow: var(--shadow);
}
dialog::backdrop { background: #000a; backdrop-filter: blur(2px); }
dialog h2 { margin: 0 0 var(--sp-2); font-size: var(--f-lg); }
dialog p { margin: 0 0 var(--sp-5); color: var(--fg2); white-space: pre-line; }
dialog .actions { display: flex; gap: var(--sp-2); justify-content: flex-end; }

#offline {
  position: fixed; left: 0; right: 0; top: 0; z-index: 50;
  padding: calc(var(--safe-t) + 6px) var(--sp-4) 6px;
  background: var(--warn); color: #1a1400; font-size: var(--f-sm); text-align: center; font-weight: 550;
}

/**
 * 「这是上次拿到的那一份」横幅（⑨）。
 * 位置与离线横幅同一处（顶部）。两条**不许同时出现** —— 都是 fixed top:0，
 * 一起挂就叠在一起谁也看不清（实测踩过）。断网走离线那条（黄），
 * 有网但连不上走这条（红）。判在 ui.js 的 paintStale 里，只有那一份。
 */
#stale {
  position: fixed; left: 0; right: 0; top: 0; z-index: 50;
  padding: calc(var(--safe-t) + 6px) var(--sp-4) 6px;
  background: var(--danger); color: var(--ink-danger);
  font-size: var(--f-sm); text-align: center; font-weight: 550;
  display: flex; align-items: center; justify-content: center; gap: var(--sp-3); flex-wrap: wrap;
}
#stale .btn { min-height: 28px; padding: 0 10px; color: var(--ink-danger); border-color: color-mix(in srgb, var(--ink-danger) 33%, transparent); }

/**
 * 底部那一摞常驻条（㊵）——「有新版本」⑨ · 「还有 N 笔没发出去」㊵。
 *
 * 挂**底部**不挂顶部：顶部那一条是离线横幅，两条挤在一起人分不清哪条是哪条；
 * 而且底部离手指近，按钮一下就点得到。
 * 底部导航之上、安全区之外 —— 不许压住 Tab，也不许被 home 条盖掉（◇ 设计标准 A）。
 *
 * ⚠️ **位置只在这一个容器上定，条自己不许再写 `position: fixed`**（㊵）。
 * ⑨ 那条是自己 fixed 的；㊵ 又加了一条，两条的 bottom 值一模一样 ——
 * 那正是顶上 #offline / #stale 已经栽过一次的那个坑（见上面那段注释）。
 * 收成一摞之后，谁都盖不住谁，加第三条也不用再想一遍。
 */
#bottombars {
  position: fixed; z-index: 60;
  left: var(--sp-4); right: var(--sp-4);
  bottom: calc(var(--tabbar-h) + var(--sp-3));
  display: flex; flex-direction: column; gap: var(--sp-2);
  /* 一条都没有的时候不许挡住底下的东西（它是全宽的） */
  pointer-events: none;
}
#bottombars > * { pointer-events: auto; }

.update-bar, .queue-bar {
  display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
  padding: var(--sp-3) var(--sp-4);
  background: var(--s2); color: var(--fg);
  border: 1px solid var(--brand); border-radius: var(--r);
  box-shadow: var(--shadow);
  font-size: var(--f-sm);
  animation: rise var(--t) var(--ease) both;
}
/**
 * 「还有 N 笔没发出去」不是一条「有好东西给你」的提示，是一件**没办完的事** ——
 * 用未确认那一档的琥珀色描边（◇ 设计标准 A：⚠️ #e3b341），
 * 和「有新版本」那条品牌青一眼分得开，而**一个新颜色都没引**。
 */
.queue-bar { border-color: var(--warn); }
.update-bar > span, .queue-bar > span { flex: 1 1 auto; min-width: 120px; }
.update-bar .btn, .queue-bar .btn { flex: 0 0 auto; }
/**
 * 那一摞是**整幅宽**的，右对齐只加在这两条上 —— 这样 toast 照旧居中
 * （网页版上它一直是居中的，不该因为底下多了一条常驻条就整个挪到右边去）。
 */
.update-bar, .queue-bar { margin-left: auto; width: min(420px, 100%); }
@media (min-width: 900px) {
  /* 网页版没有底部 Tab，整摞抬到安全区之上 */
  #bottombars { left: var(--sp-5); right: var(--sp-5); bottom: calc(var(--safe-b) + var(--sp-5)); }
}
@keyframes rise { from { transform: translateY(12px); opacity: 0 } to { transform: none; opacity: 1 } }
@media (prefers-reduced-motion: reduce) { .update-bar, .queue-bar { animation: none } }

/* ══ 启动 / 引导 / 登录 ═════════════════════════════════ */
.center {
  flex: 1; width: 100%; min-height: 100dvh;
  display: grid; place-items: center;
  padding: calc(var(--safe-t) + var(--sp-6)) var(--sp-5) calc(var(--safe-b) + var(--sp-6));
}
/* 门面几屏（启动/登录/注册/邀请码）没有顶栏，语言切换挂右上角 —— 三语必须第一屏就能切 */
.langbar { position: fixed; top: calc(var(--safe-t) + 8px); right: 8px; z-index: 40; display: flex; gap: 2px; }
.langbar button {
  min-height: 34px; padding: 0 10px; border: 0; border-radius: var(--r-pill);
  background: none; color: var(--fg2); font-size: var(--f-sm); cursor: pointer;
}
.langbar button[aria-pressed="true"] { background: var(--s2); color: var(--brand); }
.splash { text-align: center; }
.logo { width: 72px; height: 72px; margin: 0 auto var(--sp-4); }
.splash h1 { margin: 0; font-size: var(--f-2xl); font-weight: 700; letter-spacing: -.02em; }
.splash p { margin: var(--sp-2) 0 0; color: var(--fg2); }

.slides { max-width: 420px; width: 100%; }
.slide { text-align: center; }
.slide .art { height: 190px; display: grid; place-items: center; margin-bottom: var(--sp-5); }
.slide h2 { margin: 0 0 var(--sp-3); font-size: var(--f-xl); font-weight: 650; letter-spacing: -.01em; }
.slide p { margin: 0; color: var(--fg2); }
.dots { display: flex; gap: var(--sp-2); justify-content: center; margin: var(--sp-6) 0 var(--sp-5); }
.dots i { width: 7px; height: 7px; border-radius: 50%; background: var(--line);
  transition: background var(--t) var(--ease), width var(--t) var(--ease); }
.dots i.on { background: var(--brand); width: 20px; border-radius: var(--r-pill); }

.auth { width: min(400px, 100%); }
.auth h1 { margin: 0 0 var(--sp-2); font-size: var(--f-xl); font-weight: 650; letter-spacing: -.01em; }
.auth .sub { margin: 0 0 var(--sp-6); color: var(--fg2); }
.auth .legal { margin-top: var(--sp-4); font-size: var(--f-xs); color: var(--fg2); text-align: center; line-height: 1.7; }
.auth .alt { margin-top: var(--sp-5); text-align: center; }
.strength { display: flex; gap: 4px; margin-top: var(--sp-2); }
.strength i { flex: 1; height: 3px; border-radius: 2px; background: var(--line); transition: background var(--t) var(--ease); }
.strength.s1 i:nth-child(1) { background: var(--danger); }
.strength.s2 i:nth-child(-n+2) { background: var(--warn); }
.strength.s3 i { background: var(--brand); }

/* 项目卡缩略图：暖木色实体，不是照片 */
.thumb { height: 84px; border-radius: var(--r-sm); background: var(--s2);
  display: grid; place-items: center; margin-bottom: var(--sp-3); overflow: hidden; }

/* ══ 拆单表 ⑥ ═══════════════════════════════════════════
 * 窄屏：每行一张卡，每格自己带标题（data-label）。
 * ≥900px：同一份 DOM 变成真正的表格行。**不写两套渲染。**
 */
.gate { border-radius: var(--r); padding: var(--sp-4); margin-bottom: var(--sp-4); border: 1px solid; }
.gate.ok { border-color: var(--brand-line); background: var(--brand-wash); }
.gate.bad { border-color: var(--danger-line); background: var(--danger-wash); }
.gate .gate-mark { font-size: var(--f-lg); }
.gate .gate-body { margin-top: var(--sp-2); color: var(--fg2); }
.gate-list { margin: var(--sp-3) 0 0; padding-left: 1.2em; color: var(--fg); font-size: var(--f-sm); }
.gate-list li { margin-bottom: var(--sp-2); }
.gate-note { margin-top: var(--sp-3); color: var(--fg2); font-size: var(--f-xs); }

/**
 * ㊿ 出单屏在宽屏上排两栏（设计稿 7e：左边检查 + 预览，右边别的）。
 *
 * 以前这一屏在 1568 宽的屏幕上只用了左边 860，右边七百多像素全空 ——
 * 那正是自家 ◇ 设计标准 B#15 禁的「把手机版拉宽」。
 * 「不出单」那条横幅和最后那句提示横跨两栏：它们说的是整屏的结论，
 * 塞进其中一栏会变成「只跟这一栏有关」。
 */
@media (min-width: 1100px) {
  .wrap:has(.checklist) { max-width: 1180px; }
  .wrap:has(.checklist) > div {
    display: grid; grid-template-columns: minmax(0, 400px) minmax(0, 1fr);
    gap: var(--sp-4) var(--sp-5); align-items: start;
  }
  .wrap:has(.checklist) > div > .gate,
  .wrap:has(.checklist) > div > .hint,
  .wrap:has(.checklist) > div > .errbox { grid-column: 1 / -1; }
  /* 检查表整列站在左边，右边那一摞卡自己往下排 */
  .wrap:has(.checklist) > div > .checklist { grid-row: span 3; }
}

/* ㊿ 项目列表顶上那颗最大的按钮 —— 比别的主按钮再高一档（设计稿 56）。
   它是这一屏唯一的青按钮：一屏只许一颗，多一颗就等于一颗都没突出。 */
.bigcta { min-height: 56px; font-size: var(--f-md); font-weight: 600; }

/* ㊿ 一单的状态点。四种颜色各管一件事，**旁边一定跟着字** ——
   只靠颜色传信息的话，色弱的师傅看到的是四颗一样的灰点（◇ 无障碍）。 */
.stchip {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 2px var(--sp-2); border-radius: var(--r-pill);
  font-size: var(--f-xs); font-weight: 500; border: 1px solid transparent;
}
.stchip .stdot { width: 8px; height: 8px; border-radius: 50%; background: currentColor; flex: 0 0 auto; }
.stchip svg { flex: 0 0 auto; }
.stchip.ok    { background: var(--brand-wash); color: var(--brand);  border-color: var(--brand-line); }
.stchip.warn  { background: var(--warn-wash);  color: var(--warn);   border-color: var(--warn-line); }
.stchip.draft { background: var(--s2);         color: var(--fg2);    border-color: var(--line); }

/* ㊿ 「名字 —— 一句说明 ›」这种导航行。
   说明那一段可长可短，**不许把左边的名字挤到换行** ——
   「参数」两个字被挤成上下两行那一版是在手机上截图才看见的。
   名字按内容宽、不换行；说明吃掉剩下的宽度，太长就省略号。 */
.navrow .k { flex: 0 0 auto; white-space: nowrap; }
.navrow .sub {
  flex: 1; min-width: 0; text-align: right;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.navrow .chev { flex: 0 0 auto; }

/* ㊿ 列表底下那种「一行字 + ›」的入口。不是按钮的样子，也不是纯文本 —— 它是一条路。 */
.linkrow {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3);
  width: 100%; min-height: var(--tap); margin-top: var(--sp-4);
  padding: 0 var(--sp-4); border: 1px solid var(--line); border-radius: var(--r);
  background: none; color: var(--fg2); font: inherit; font-size: var(--f-sm); cursor: pointer;
}
.linkrow:hover { background: var(--s2); color: var(--fg); }

/* ㊿ 项目屏上「模型 · 出单」那两颗。两颗一样宽，
   窄屏也不换行 —— 它们是一条路上的两步，拆到两行读起来就断了。 */
.acts2 { gap: var(--sp-3); }
.acts2 > .btn { flex: 1; min-width: 0; }

/* ㊿ 出单屏上摊开的那十二项检查。
   过的一行退到背景里（灰勾 + 灰字），没过的一行才抢眼 ——
   十二行全都一样重的话，等于一行都没标出来。 */
.checklist .rows { border: 0; border-radius: var(--r-sm); background: var(--s2); }
.ck { min-height: 44px; gap: var(--sp-2); font-size: var(--f-sm); }
.ck .k { flex: 1; }
.ck.ok { color: var(--fg2); }
.ck.ok svg { color: var(--brand); flex: 0 0 auto; }
.ck.bad .k, .ck.bad .v { color: var(--danger); }
.ck.na .k, .ck.na .v { color: var(--warn); }
.ck-mark { font-size: 12px; flex: 0 0 auto; }
/* 三张预览卡上那行数字用等宽 —— 38 块板和 4 张板要对得齐 */
.previews .num { font-family: var(--font-num); font-variant-numeric: tabular-nums; }

.stats { flex-wrap: wrap; gap: var(--sp-2); margin-bottom: var(--sp-3); }
.filters { gap: var(--sp-2); margin-bottom: var(--sp-4); }
/**
 * 窄屏两个筛选各占一半（手指点得到），**宽屏封顶** ——
 * `flex:1` 在 1280 上会把「全部柜子」这四个字撑成 500px 宽的空框，
 * 一屏两个大空框摆在一张紧凑的料单上面，看着就是没做完（◇ 设计标准 C「留白失控」）。
 * 260 是这两个下拉里最长那一项（「全部件别」+ 收口件…）舒服放得下的宽。
 */
.filters .ch { flex: 1 1 0; min-width: 0; }
.filters .ch .ch-v { flex: 1; min-width: 0; max-width: none; }
@media (min-width: 700px) { .filters .ch { flex: 0 1 260px; } }

.cutlist { display: grid; gap: var(--sp-2); }
.cut-head { display: none; }
.cut-item { border: 1px solid var(--line); border-radius: var(--r); background: var(--s1); overflow: hidden; }
.cut-row {
  display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-2) var(--sp-3);
  padding: var(--sp-3) var(--sp-4); cursor: pointer; align-items: baseline;
}
.cut-row:hover { background: var(--s2); }
.cut-row.pending { border-left: 3px solid var(--warn); }
.cut-row.open { background: var(--s2); }
.cut-row > span { min-width: 0; overflow-wrap: anywhere; }
/* 窄屏：每格上面挂自己的标题 */
.cut-row > span::before {
  content: attr(data-label); display: block;
  font-size: var(--f-xs); color: var(--fg2); margin-bottom: 2px;
}
.cut-row .nm { grid-column: 1 / -1; font-weight: 600; }
.cut-row .sz { font-family: var(--font-num); font-variant-numeric: tabular-nums; }
.cut-row .cut { font-family: var(--font-num); font-variant-numeric: tabular-nums; }
.cut-row .id { font-family: var(--font-num); color: var(--fg2); }

.cut-detail { padding: var(--sp-4); border-top: 1px solid var(--line-soft); background: var(--bg); }
.trace-title { font-size: var(--f-xs); color: var(--fg2); text-transform: uppercase;
  letter-spacing: .06em; margin-bottom: var(--sp-2); }
.trace-formula { font-family: var(--font-num); font-size: var(--f-sm); line-height: 1.8;
  overflow-wrap: anywhere; color: var(--fg); }

/* 追溯串分段（⑫）：左边一个窄标签，右边一句话。
 * 以前是一整行长串（尺寸·几何·封边·纹向全挤在一行），读的人要自己找断点。
 * 窄屏叠成两行，宽屏 108px 的标签列对齐 —— 四段一眼扫得下来。 */
.trace-segs { display: grid; gap: var(--sp-2); }
.trace-seg { display: grid; gap: 2px; }
.trace-seg > .k { font-size: var(--f-xs); color: var(--fg2); letter-spacing: .04em; }
.trace-seg > .v { font-size: var(--f-sm); line-height: 1.65; color: var(--fg);
  overflow-wrap: anywhere; }
/* 尺寸那一段是数字，用等宽 —— 18 和 1218 要对得齐（◇ 设计标准 A） */
.trace-seg.dims > .v { font-family: var(--font-num); font-variant-numeric: tabular-nums; }
@media (min-width: 700px) {
  .trace-seg { grid-template-columns: 108px 1fr; gap: var(--sp-3); align-items: baseline; }
  .trace-seg > .k { text-align: right; }
}
.why-body { color: var(--fg2); line-height: 1.7; }

/* 出单闸里「还有 N 项没裁」那一条（⑫）：以前六个问句被接成一行 215 字，
 * 而每一句其实都是一个点得动的待办。现在一条一行，右边给出路。 */
.gate-todo { display: grid; gap: var(--sp-2); margin-top: var(--sp-2); }
.gate-todo-row {
  display: flex; align-items: flex-start; gap: var(--sp-3);
  padding: var(--sp-2) 0;
}
.gate-todo-row + .gate-todo-row { border-top: 1px solid var(--line-soft); }
/**
 * 问句封到 62 个字符宽：`flex:1 1 auto` 不封顶的话，1280 屏上问句排到 780px，
 * 而「去填这一项」被推到 1190px —— 中间 400px 空白，眼睛得横着走一趟才知道
 * 这个按钮管的是哪一条（◇ 设计标准 B#9 留白成节奏）。封顶之后按钮紧跟着问句。
 * 62ch 也正好是长句子读着不费劲的行长。
 */
.gate-todo-row .q { flex: 1 1 auto; max-width: 62ch; line-height: 1.6; }
.gate-todo-row .btn { flex: 0 0 auto; }
@media (max-width: 699px) {
  /* 窄屏竖排 —— 问句本来就长，横排会把按钮挤成两个字宽 */
  .gate-todo-row { flex-direction: column; align-items: stretch; gap: var(--sp-2); }
}
/**
 * ㉓：提醒框里那几条带出口的（「去填这一项」/「去改这一项」）。
 * 句子要和同框里别的提醒一个字号一个颜色，不然同一个框里两档正文 = 没有层级。
 * 框自己已经用 grid gap 隔开了，这里不再补内边距和分隔线。
 */
.errbox .gate-todo-row { padding: 0; }
.errbox .gate-todo-row + .gate-todo-row { border-top: none; }
.errbox .gate-todo-row .q { color: var(--fg2); font-size: var(--f-sm); }
/**
 * 同一个框里，带出口那几条的句子封到 62ch（`.gate-todo-row .q`），
 * 不带出口的却铺满 1180px —— 一个框里两种行长，读起来像两块东西（◇B#9）。
 * 只封拆单屏那个提醒框：`.errbox` 别处还当错误框用，行长各有各的道理。
 */
.errbox.notes .why { max-width: 62ch; }

/**
 * ㉓ 说明（ℹ️）：人什么都不用做，只是讲清楚料单为什么长这样（规则 297）。
 * 不用红底 —— 它不是毛病；默认收起来，免得把真提醒衬没了。
 */
.notebox {
  border: 1px solid var(--line); background: var(--s1); border-radius: var(--r);
  padding: var(--sp-3) var(--sp-4); margin-top: var(--sp-3);
}
/* 折叠的样子和拆单屏「看板件明细」那一处同一套（原生 marker，◇C 图标成套） */
.notebox > summary {
  cursor: pointer; color: var(--fg2); font-size: var(--f-sm); padding: var(--sp-1) 0;
}
.notebox > summary:hover { color: var(--fg); }
.notebox > summary:focus-visible { outline: 2px solid var(--brand); outline-offset: 3px; border-radius: var(--r-sm); }
.notebox > .hint { margin: var(--sp-2) 0 var(--sp-3); }
.notebox .why { color: var(--fg2); font-size: var(--f-sm); line-height: 1.7; }
.notebox .why + .why { margin-top: var(--sp-2); }

/* 宽表自己横滚，**页面本身永远不横滚** */
.cut-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }

@media (min-width: 900px) {
  .cut-scroll { border: 1px solid var(--line); border-radius: var(--r); }
  .cutlist { gap: 0; min-width: 940px; }
  .cut-item { border: 0; border-radius: 0; }
  .cut-item + .cut-item { border-top: 1px solid var(--line-soft); }
  .cut-head, .cut-row {
    display: grid;
    grid-template-columns: 8rem minmax(11rem, 1.5fr) 8.5rem 2.6rem 5.2rem 4rem 7rem 3.4rem 6.2rem;
    /* 列间距 12→8（⑫）：九栏 × 12 的间距把整表的最小宽推到 1022，
       1280 的屏上差 23px 就是差 23px —— 表格照样要左右拖。8 够分得开，
       换来的是这张表在常见笔电宽度上一眼看全，不用拖。 */
    gap: var(--sp-2); align-items: center;
  }
  .cut-head {
    padding: var(--sp-3) var(--sp-4); background: var(--s2);
    font-size: var(--f-xs); color: var(--fg2); text-transform: uppercase; letter-spacing: .06em;
  }
  .cut-row > span::before { display: none; }
  .cut-row .nm { grid-column: auto; }
  /* 编号/件名不许把行撑成三层楼；放不下就省略号，鼠标停上去看全的 */
  .cut-row .id, .cut-row .nm, .cut-row .mat {
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  .cut-detail { grid-column: 1 / -1; }
}

/* ══ 待确认队列 ⑦ ═══════════════════════════════════════ */
.card.pend { padding: 0; overflow: hidden; }
.pend-head {
  display: grid; grid-template-columns: auto 1fr auto; gap: var(--sp-3);
  align-items: start; width: 100%; padding: var(--sp-4);
  background: none; border: 0; color: var(--fg); text-align: left; cursor: pointer;
}
.pend-head:hover { background: var(--s2); }
.pend-head .sev { font-size: var(--f-md); line-height: 1.5; }
.pend-head .q { line-height: 1.6; overflow-wrap: anywhere; }
.pend-detail { padding: 0 var(--sp-4) var(--sp-4); border-top: 1px solid var(--line-soft); padding-top: var(--sp-4); }
/**
 * ㉘：那一格改成空的 + 一句占位说明（规则 308）之后，输入框把「保存」挤成了
 * 「保 / 存」两行 —— 一句话被挤成一个字一行，正是规则 263 那一类。
 * 输入框可伸缩、按钮不许被压：这一条只管待确认屏那一格（改全局会跑到别的屏上去，
 * ⑯⑰⑱⑲ 连着栽了四轮）。
 */
.pend-detail .field .row > input { flex: 1 1 auto; min-width: 0; }
.pend-detail .field .row > .btn { flex: 0 0 auto; white-space: nowrap; }
/* ⑯ 折叠区抬头（「这一单没用到的」）—— 箭头用帮助页那一套，同一个图标同一个转法 */
.pend-head .chev { color: var(--fg2); display: flex; transition: transform var(--t) var(--ease); }
.pend-head.on .chev { transform: rotate(90deg); color: var(--brand); }
@media (prefers-reduced-motion: reduce) { .pend-head .chev { transition: none; } }
.opt {
  display: block; width: 100%; text-align: left; cursor: pointer;
  padding: var(--sp-3); border: 1px solid var(--line); border-radius: var(--r);
  background: var(--s2); color: var(--fg);
  transition: border-color var(--t) var(--ease), background var(--t) var(--ease);
}
.opt:hover:not(:disabled) { border-color: var(--brand-dim); background: var(--s3); }
.opt:disabled { opacity: .55; cursor: not-allowed; }
.opt-v { font-weight: 600; margin-bottom: 4px; }
.opt-c { font-size: var(--f-sm); color: var(--fg2); line-height: 1.6; }
.demo-basis {
  margin-top: var(--sp-4); padding: var(--sp-3); border-radius: var(--r);
  background: var(--warn-wash); border: 1px solid var(--warn-line);
}

/* ══ 排版图 ═════════════════════════════════════════════ */
.sheet { background: var(--bg); border-radius: var(--r-sm); overflow: hidden; }
.sheet svg { display: block; width: 100%; height: auto; }

/* ══ 按下去要有反馈 ═════════════════════════════════════
 * ◇ 设计标准 B-7：每个可点的东西必须有 hover / focus-visible / :active / disabled 四态。
 * 只有默认态 = 手指按下去屏幕一动不动 = 用户以为没点到，会连点第二次。
 */
.btn:active:not(:disabled),
.icon-btn:active,
.rowitem:not(.static):active,
.opt:active:not(:disabled),
.menu button:active,
.tabbar button:active,
.langbar button:active { transform: scale(.975); }
.btn, .icon-btn, .rowitem, .opt, .menu button, .tabbar button, .langbar button,
.card.tap, .cut-row, .pend-head {
  transition: background var(--t) var(--ease), border-color var(--t) var(--ease),
              color var(--t) var(--ease), transform var(--t-fast) var(--ease);
  -webkit-tap-highlight-color: transparent;
}
.card.tap:active, .cut-row:active, .pend-head:active { transform: scale(.995); }

/* ══ 屏与屏之间的过渡 ═══════════════════════════════════
 * 180–240ms 淡入 + 6px 上移。没有过渡的换屏是「啪」地整页替换，
 * 眼睛跟不上刚才在哪、现在在哪 —— 这正是「简陋」最直接的来源。
 * 尊重系统的「减少动态效果」：tokens.css 里 reduced-motion 时时长归零。
 */
@keyframes screen-in { from { opacity: 0; transform: translateY(6px) } to { opacity: 1; transform: none } }
main > .wrap, .center > * { animation: screen-in var(--t-slow) var(--ease); }
@media (prefers-reduced-motion: reduce) { main > .wrap, .center > * { animation: none; } }

/* ══ 空态插画：统一一套线条语言 ═══════════════════════════
 * 线宽 2、圆角端点、只用「线 #2c3036 + 暖木 #c08d5a + 品牌青 #6fbfae」三支笔。
 * 三种颜色以外的、或者填充实心的插画一律不许进。
 */
.empty svg { color: var(--fg3); }
.empty svg [stroke="#6fbfae"] { opacity: .95; }

/* ══ 参数表单 ④（S8）═══════════════════════════════════════
 * 字号在这一屏至少走四档：段标题 12 · 说明 13 · 字段名 15 · 数值 19 · 影响数 26。
 * 全部数字走 --font-num + tabular-nums —— 18 和 1218 要对得齐。
 */

/* 顶部「改完之后」：改一个参数，这里当场变数字（本地重算，不等网络） */
.impact {
  background: var(--s1); border: 1px solid var(--line); border-radius: var(--r-lg);
  padding: var(--sp-4); margin-bottom: var(--sp-4);
}
.imp-title {
  font-size: var(--f-xs); color: var(--fg2); text-transform: uppercase;
  letter-spacing: .08em; margin-bottom: var(--sp-3);
}
.imp-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--sp-3); }
@media (min-width: 560px) { .imp-grid { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 900px) { .imp-grid { grid-template-columns: repeat(6, 1fr); } }
.imp-cell { min-width: 0; }
.imp-k { font-size: var(--f-xs); color: var(--fg2); margin-bottom: 2px; }
.imp-v {
  font-size: var(--f-xl); font-weight: 600; line-height: 1.15;
  display: flex; align-items: baseline; gap: var(--sp-2);
}
.imp-v .num { font-family: var(--font-num); font-variant-numeric: tabular-nums; }
/* 改之前那个数：划掉留在旁边，师傅一眼看出「本来 45，现在 41」 */
.imp-u { font-size: var(--f-xs); color: var(--fg2); }
.imp-was {
  font-size: var(--f-sm); color: var(--fg2); text-decoration: line-through;
  text-decoration-color: var(--warn);
}
.imp-gate {
  margin-top: var(--sp-3); padding-top: var(--sp-3); border-top: 1px solid var(--line-soft);
  font-size: var(--f-sm);
}
.imp-gate.ok { color: var(--brand); }
.imp-gate.bad { color: var(--danger); }

.cfg-sub { margin: 0 0 var(--sp-5); color: var(--fg2); font-size: var(--f-sm); line-height: 1.7; }
.cfg-sec { margin-bottom: var(--sp-6); }
.sec-hint { margin: 0 0 var(--sp-3); color: var(--fg2); font-size: var(--f-sm); line-height: 1.7; }
.errbox.soft { background: var(--s1); border-color: var(--line); margin-bottom: var(--sp-3); }

/* 一个字段一张卡：名字 / ? / 已改 在上，控件居中，默认值与规则号在下 */
.fieldcard { padding: var(--sp-4); }
.fieldcard.locked { opacity: .72; }
.field-head { display: flex; align-items: center; gap: var(--sp-2); margin-bottom: var(--sp-3); }
.field-name { font-size: var(--f-md); font-weight: 550; flex: 1; min-width: 0; }
.field-head .pill { flex: none; display: inline-flex; align-items: center; gap: 4px; }
.field-head .pill .ic { width: 13px; height: 13px; }

/* 「为什么」按钮：手指够得着的 44，图标只占 18 */
.why-btn {
  flex: none; width: var(--tap); height: var(--tap); display: grid; place-items: center;
  background: none; border: 0; border-radius: var(--r-sm); color: var(--fg3); cursor: pointer;
  transition: background var(--t) var(--ease), color var(--t) var(--ease), transform var(--t-fast) var(--ease);
}
.why-btn:hover { background: var(--s2); color: var(--brand); }
.why-btn:active { transform: scale(.92); }

/* 分段控件：可选值全摆出来，比下拉少一次点击 */
.seg { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.seg-b {
  flex: 1 1 auto; min-width: 84px; min-height: var(--tap);
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 1px;
  padding: var(--sp-2) var(--sp-3);
  background: var(--s2); border: 1px solid var(--line); border-radius: var(--r);
  color: var(--fg2); cursor: pointer; text-align: center;
  transition: background var(--t) var(--ease), border-color var(--t) var(--ease),
              color var(--t) var(--ease), transform var(--t-fast) var(--ease);
}
.seg-t { font-size: var(--f-sm); font-weight: 550; }
.seg-u { font-size: 10px; color: var(--fg2); letter-spacing: .06em; }
.seg-b:hover:not(:disabled) { background: var(--s3); color: var(--fg); }
.seg-b:active:not(:disabled) { transform: scale(.97); }
.seg-b[aria-checked="true"] {
  background: var(--brand-wash); border-color: var(--brand-dim); color: var(--brand);
}
.seg-b[aria-checked="true"] .seg-u { color: var(--brand-dim); }
.seg-b:disabled { opacity: .5; cursor: not-allowed; }

/* 数值输入：单位常驻在框里，不靠占位符 —— 占位符一打字就没了 */
.numwrap { position: relative; }
.numin {
  width: 100%; height: 52px; padding: 0 56px 0 var(--sp-4);
  background: var(--s2); border: 1px solid var(--line); border-radius: var(--r);
  color: var(--fg); font-size: var(--f-lg); font-variant-numeric: tabular-nums;
  transition: border-color var(--t) var(--ease);
}
.numin:focus { border-color: var(--brand-dim); }
.numin:focus:not(:focus-visible) { outline: none; }   /* 同上：环只给键盘 */
.numin:disabled { opacity: .55; cursor: not-allowed; }
.numin[aria-invalid="true"] { border-color: var(--danger); }
.numwrap .unit {
  position: absolute; right: var(--sp-4); top: 50%; transform: translateY(-50%);
  color: var(--fg2); font-size: var(--f-sm); pointer-events: none;
}
.field-err {
  margin-top: var(--sp-2); color: var(--danger); font-size: var(--f-sm); line-height: 1.6;
}
.field-err .err-rule { color: var(--fg2); }

.field-foot {
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-2) var(--sp-3);
  margin-top: var(--sp-3); font-size: var(--f-xs); color: var(--fg2);
}
.field-def { font-family: var(--font-num); font-variant-numeric: tabular-nums; }
.field-rules { color: var(--fg2); }
.field-foot .btn.sm { margin-left: auto; min-height: 32px; }

/* 网页宽度下参数卡两列 —— 19 张一样宽的卡竖着排一长条，就是「把手机版拉宽」 */
@media (min-width: 900px) {
  .cfg-sec .list { grid-template-columns: repeat(2, 1fr); }
  .wrap:has(.impact) { max-width: 980px; }
}

.cfg-footer {
  display: flex; align-items: center; gap: var(--sp-3);
  padding-top: var(--sp-4); border-top: 1px solid var(--line-soft);
}
.cfg-count { flex: 1; color: var(--fg2); font-size: var(--f-sm); }

/* 还没有柜子时的提示卡：说清楚参数照样能先填，不是坏了 */
.hintcard { display: flex; gap: var(--sp-4); align-items: center; margin-bottom: var(--sp-4); }
.hc-art { flex: none; width: 92px; }
.hc-art svg { width: 92px; height: 69px; color: var(--fg3); }
.hc-t { font-weight: 600; margin-bottom: 4px; }
.hc-b { color: var(--fg2); font-size: var(--f-sm); line-height: 1.7; }

/* ══ 统一的「为什么」对话框 ═════════════════════════════ */
.why-dlg { max-width: 460px; }
.why-text { margin: 0 0 var(--sp-4); color: var(--fg2); line-height: 1.75; white-space: pre-line; }
.why-rows { margin-bottom: var(--sp-3); }
.why-row { display: flex; justify-content: space-between; gap: var(--sp-3); padding: var(--sp-2) 0; }
.why-row + .why-row { border-top: 1px solid var(--line-soft); }
.why-row .k { color: var(--fg2); font-size: var(--f-sm); flex: 0 0 auto; }
/* 一句话的值：靠左、能折行、正文字体（等宽长句难读，见 ui.js whyDialog） */
.why-row .v.prose { flex: 1 1 auto; text-align: left; line-height: 1.6;
  font-size: var(--f-sm); overflow-wrap: anywhere; }
.why-note {
  padding: var(--sp-3); border-radius: var(--r); background: var(--s2);
  color: var(--fg2); font-size: var(--f-sm); margin-bottom: var(--sp-4);
}
.why-meta { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-bottom: var(--sp-5); }
.cfg-blocked {
  padding: var(--sp-3) var(--sp-4); margin-bottom: var(--sp-3);
  border: 1px solid var(--danger-line); background: var(--danger-wash); border-radius: var(--r);
  color: var(--danger); font-size: var(--f-sm);
}

/* 马来文未审提示 —— 细一条，不抢主内容，但不许没有（S14） */
.msnotice {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-4);
  background: var(--warn-wash); border-bottom: 1px solid var(--warn-line);
  color: var(--warn); font-size: var(--f-xs);
}

/* 参数分组：一条细线加一个组名，比再套一层卡片轻 */
.cfg-grp + .cfg-grp { margin-top: var(--sp-5); }
.grp-title {
  display: flex; align-items: center; gap: var(--sp-3);
  margin-bottom: var(--sp-3); color: var(--fg2); font-size: var(--f-sm); font-weight: 550;
}
.grp-line { flex: 1; height: 1px; background: var(--line); }
.grp-title > span:nth-child(2) { flex: none; }

/* ══ 三维核对 ⑤（S13）══════════════════════════════════════
 * 3D 视口永远是主角，UI 退后（S14）。手机上视口占一屏的大半，属性卡在下面；
 * 网页宽了改成「左视口 + 右属性」两栏 —— 不是把手机版拉宽（◇B 15）。
 */
/**
 * ══ ㊿ 模型屏：手机上整屏不滚 ══════════════════════════════
 *
 * 业主 2026-09-14：「上面显示 3D，如果我要调节数据屏幕一直要往下滑动，
 * 设置后又要往上滑动查看，这样肯定是不顺畅的。」
 *
 * 做法是**挤，不是盖**：一列竖排，舞台 flex:1 吃掉剩下的全部高度，
 * 面板贴在底下按档位占高。面板长高 → 舞台变矮 → 三维按新高度重新自适配，
 * 模型**永远整个露着**。浮上去盖住一半看着更酷，但被盖住的正好是人刚改的那块板。
 *
 * 页面本身一个像素都不滚：main 关掉滚动并锁死高度（dvh 才跟得住手机地址栏的伸缩，
 * 用 vh 的话地址栏一收面板会被顶出屏幕）。滚的是面板里面的内容。
 */
.m3-wrap { display: grid; gap: var(--sp-4); }

@media (max-width: 899px) {
  main:has(.m3-wrap) {
    padding: 0;
    overflow: hidden;
    height: calc(100dvh - var(--topbar-h) - var(--safe-t) - var(--tabbar-h));
  }
  /**
   * ⚠️ 高度要一路传下去，中间**一层都不能漏**。
   * 结构是 main > .wrap > 屏自己的根 div > .m3-wrap ——
   * 只给 .wrap 设 100% 的话，中间那个根 div 按内容高，
   * 于是舞台只有两百来像素，底下空掉一大片（实测截图就是这样）。
   */
  main:has(.m3-wrap) > .wrap { height: 100%; max-width: none; }
  main:has(.m3-wrap) > .wrap > * { height: 100%; }
  /* overflow:hidden 兜住最后那几像素：两行加起来正好铺满，但边框和内距凑出 6px
     溢出（实测 scrollHeight 710 / clientHeight 704）。面板自己内部会滚，不丢东西。 */
  .m3-wrap { height: 100%; gap: 0; grid-template-rows: minmax(0, 1fr) auto; overflow: hidden; }
  /**
   * ⚠️ **必须是 column。**
   * `.m3-main` 里不止舞台一个孩子 —— inmodel.js 还会往这里塞一条快捷条（.im-quick）。
   * 写成默认的横向 flex 时，快捷条按内容占满 400，而舞台是 `flex:1 1 0%`（基准 0），
   * 于是舞台被挤成**宽度 0**：三维画布 clientWidth=0、手柄一个都画不出来，
   * 而屏幕上还留着上一帧的画面，看起来一切正常（业主说「完全不能正常使用」就是这个）。
   * 竖排之后两者各占一行，舞台吃掉剩下的高度，快捷条贴在它底下。
   */
  .m3-main { min-width: 0; min-height: 0; display: flex; flex-direction: column; }
  /* 舞台吃掉剩下的全部高度。手机上不要圆角和边框 —— 它已经贴着屏幕边了 */
  .m3-wrap .stage { flex: 1; height: auto; min-height: 0; border: 0; border-radius: 0; }

  .m3-panel {
    background: var(--s1); border-top: 2px solid var(--brand);
    border-radius: var(--r-lg) var(--r-lg) 0 0; box-shadow: 0 -8px 28px var(--scrim);
    padding: 0 var(--sp-3) var(--sp-3);
    overflow-y: auto; overscroll-behavior: contain;
    /**
     * ⚠️ **这里不做过渡动画。** 两个原因，第二个才是真的：
     *
     * 1 实测它会卡住：在 auto 的网格行里过渡 max-height，行高跟着元素走、
     *   元素又被行高圈着，值停在中途不动（截到过：data-h 都换成 full 了，
     *   算出来的 max-height 还是 46dvh，peek 也是同一个数）。
     * 2 就算它不卡 —— 面板一高一矮，舞台就跟着一高一矮，
     *   盯着舞台那个 ResizeObserver 会在这 200ms 里被叫六十次，
     *   每次都要重画三维。手机上（尤其软件渲染那条路）直接掉帧。
     * 直接跳档，没有中间态。
     */
  }
  /**
   * 三段高。peek 只露把手和标题那一行；full 留一条给模型，免得人忘了自己在哪一屏。
   *
   * ⚠️ **不许用百分比。** 百分比的参照是这个格子自己，而格子是 auto（按内容高）——
   * 于是「格子有多高」要先知道「内容有多高」，而内容又被「格子的 46%」卡着，绕成一个圈：
   * 实测格子算出 328，面板被卡在它的 46% = 151，剩下 177 成了一片空白（截图上那一块）。
   * dvh 的参照是视口，跟格子无关，圈就断了。
   */
  .m3-panel[data-h="peek"] { max-height: 92px; overflow: hidden; }
  .m3-panel[data-h="half"] { max-height: 42dvh; }
  .m3-panel[data-h="full"] { max-height: 72dvh; }
  /* 「开门 / 拆开看」这些看的方式只在整屏那一档露出来 —— 日常不占地方 */
  .m3-panel:not([data-h="full"]) .m3-more { display: none; }
}

/**
 * ══ ㊿ 电脑：自己的一套，不是把手机版拉宽 ════════════════════
 *
 * 业主 2026-09-14：「电脑版本需要用自己的操作方式而且是和 3D 图对齐的」。
 * 以前这一屏在 1440 的屏上只用中间一小块，右面板顶边和模型对不齐，下半截空着。
 *
 * 现在整屏铺满：左栏（页签，可收）· 中间模型吃掉全部剩余高度 · 右面板顶边与模型齐平、
 * 独立滚动。页面本身照样不滚 —— 和手机是同一条规矩，只是排法不同。
 */
@media (min-width: 900px) {
  main:has(.m3-wrap) {
    padding: 0;
    overflow: hidden;
    height: calc(100dvh - var(--topbar-h) - var(--safe-t));
    /**
     * ⚠️ `flex: 0 0 auto` 不能省 —— 全局那条 `main { flex: 1 }` 会让它**长过**上面那个
     * height：`.col` 是按内容高的，main 撑大 → .col 跟着大 → main 再撑大。
     * 51 加了底部坞（200px）之后这个洞当场通了：1920×1000 上整页多出 154px 可以滚，
     * 而模型屏的规矩是「页面本身一动不动」（手机那边同一条）。
     * 加了坞之前内容从来没超过一屏，所以看不出来。
     */
    flex: 0 0 auto;
  }
  main:has(.m3-wrap) > .wrap { height: 100%; max-width: none; margin: 0; }
  main:has(.m3-wrap) > .wrap > * { height: 100%; }

  .m3-wrap {
    height: 100%; gap: 0; overflow: hidden;
    grid-template-columns: minmax(0, 1fr) 380px;
    align-items: stretch;
  }
  /* 同上：快捷条是舞台的兄弟，横排会把舞台挤成 0 宽 */
  .m3-main { min-width: 0; min-height: 0; display: flex; flex-direction: column; }
  /* 模型吃掉全部高度。电脑上留边框和圆角没意义 —— 它贴着左右两条栏 */
  .m3-wrap .stage { flex: 1; height: auto; min-height: 0; border: 0; border-radius: 0; }

  /* 右面板：顶边和模型齐平，自己滚，不跟着页面走 */
  .m3-panel {
    display: flex; flex-direction: column; gap: var(--sp-4);
    border-left: 1px solid var(--line); background: var(--s1);
    padding: var(--sp-4); overflow-y: auto; min-height: 0;
    max-height: none;            /* 三段高是手指那一套，鼠标这边不用 */
  }
  /* 把手是给手指的，鼠标这边藏掉；「看的方式」那一组在电脑上一直露着（地方够） */
  .m3-grip { display: none; }
  .m3-panel .m3-more { display: flex !important; }
}

/**
 * 把手。**够得着比看得见重要** —— 视觉上只有 4px 高的一条，
 * 但整个按钮 28px 高、横跨整条面板，手指随便按哪里都能拖到。
 */
/**
 * 把手只给手指。
 * ⚠️ 这条规则写在两个媒体块**之后**，所以它会盖掉 `min-width:900` 里那句
 * `display:none` —— 电脑上顶着一条拖不动的短横（业主截图里就是）。
 * 所以隐藏条件写在这里，不靠后面的媒体块去压。
 */
.m3-grip { display: none; }
@media (max-width: 899px) {
  .m3-grip {
    display: block; width: 100%; height: 44px; border: 0; background: none;
    cursor: grab; padding: 0; position: relative;
  }
}
.m3-grip::before {
  content: ''; position: absolute; left: 50%; top: 20px; transform: translateX(-50%);
  width: 36px; height: 4px; border-radius: 999px; background: var(--fg3);
}
.m3-grip:active { cursor: grabbing; }
.m3-grip:hover::before { background: var(--fg2); }
.m3-more { display: flex; flex-direction: column; gap: var(--sp-3); }

/**
 * ㊿ 模型屏里**不要那条快捷条**（.im-quick）。
 *
 * 它是 ㊺ 做在单柜编辑屏上的：那一屏的属性卡在页面下方很远，所以在模型底下
 * 摆一条「第 2 格 · 门 / 抽屉 / 开放格 / 饰面板 / 电器开口」应急。
 * 现在这一屏的面板**就贴在模型正下方**，两者一字不差地重复
 * （业主 2026-09-14：「功能不能重复」），而且在 400 宽的手机上它要占掉 138px 高 ——
 * 模型只剩 165，比面板还矮。
 * 单柜编辑屏那条照旧留着（那边确实够不着），所以只藏 .m3-main 里面这一条。
 */
.m3-main .im-quick { display: none; }

/**
 * ㊿ 属性面板的页签（方案 B）。
 * 横着切页，每页三五行 —— 以前是十四样竖着摞一千多像素，自己要滚，
 * 模型被挤在中间还看不全（业主 2026-09-14）。
 */
.prop-tabs {
  display: flex; gap: 2px; margin: 0 calc(var(--sp-3) * -1) var(--sp-3);
  padding: 0 var(--sp-3); border-bottom: 1px solid var(--line);
}
.prop-tab {
  /* 规则 328 那把尺子：手指按的东西不小于 44 —— 卡浮在手机上之后这三个页签是真要用手指按的 */
  padding: var(--sp-2) var(--sp-3); min-height: var(--tap); min-width: var(--tap);
  border: 0; border-bottom: 2px solid transparent; background: none;
  color: var(--fg3); font: inherit; font-size: var(--f-sm); cursor: pointer;
  white-space: nowrap;
}
.prop-tab:hover { color: var(--fg2); }
.prop-tab[aria-selected="true"] { color: var(--brand); border-bottom-color: var(--brand); font-weight: 650; }
.prop-page { display: flex; flex-direction: column; gap: var(--sp-3); }

/**
 * ㊿ 面板里那种「名字 + 加减 + 一句说明」的行。
 *
 * inmodel.js 把三样东西塞进一个 .row，在 380 宽的柱子里说明那一段会把名字挤扁 ——
 * 「挂衣杆」被压成上下三个字各占一行（业主截图里就是这样）。
 * 名字不许缩，加减跟着它；**末尾那句说明自己占一整行**（它本来就是补充，不是并列）。
 */
.m3-panel .row { flex-wrap: wrap; row-gap: var(--sp-2); }
.m3-panel .row > .hint { flex: 0 0 auto; white-space: nowrap; }
.m3-panel .row > .hint:last-child:not(:first-child) {
  flex: 1 1 100%; white-space: normal; color: var(--fg3);
}
.m3-panel .stepper { flex: 0 0 auto; }

/**
 * ㊿ 抽屉式栏目（照业主给的参考图）。
 *
 * 从左边滑出，盖住整屏高度；背后压一层半透明。
 * 手机上宽 84vw 但最多 320（再宽就把背后那一条也盖没了，人会不知道怎么退出来）；
 * 电脑上固定 300。分组抬头 · 每条图标加文字 · 当前那条高亮。
 */
.dw { position: fixed; inset: 0; z-index: 90; display: flex; }
.dw-back { position: absolute; inset: 0; background: var(--scrim); border: 0; }
.dw-panel {
  position: relative; width: min(84vw, 320px); height: 100%;
  background: var(--s1); border-right: 1px solid var(--line);
  display: flex; flex-direction: column;
  box-shadow: 8px 0 28px var(--scrim);
  animation: dwIn var(--t) var(--ease);
  padding-top: var(--safe-t); padding-bottom: var(--safe-b);
}
@media (min-width: 900px) { .dw-panel { width: 300px; } }
@keyframes dwIn { from { transform: translateX(-100%); } to { transform: none; } }
@media (prefers-reduced-motion: reduce) { .dw-panel { animation: none; } }

.dw-top {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-3) var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--line-soft); flex: 0 0 auto;
}
.dw-brand { flex: 1; font-size: var(--f-lg); font-weight: 650; letter-spacing: -.01em; }

.dw-body { flex: 1; min-height: 0; overflow-y: auto; padding: var(--sp-3) var(--sp-2) var(--sp-4); }

/**
 * 51 密度。业主 2026-09-14：「栏目的按钮不齐，目前太空了，整个显示很奇怪栏目」。
 *
 * 原来是 48 高 + 组间 16 + 组内也跟着行高散开，十八条撑满一屏还留着大片空白 ——
 * 一列名词飘在黑底上，看不出哪几条是一伙的。
 * 改成 44 高（还是够手指按的 --tap）、**组内 0 间距、组间 16**：
 * 一组挤成一块，块与块之间才是空白 —— 空白用来分组，不用来填时间。
 */
.dw-group + .dw-group { margin-top: var(--sp-4); }
.dw-head {
  padding: var(--sp-2) var(--sp-3) var(--sp-1);
  font-size: var(--f-xs); color: var(--fg2); letter-spacing: .08em;
  text-transform: uppercase;
}

/**
 * 一条 = 图标 + 名字 + 值（+ 角标）。44 高 —— 手指够得着（◇ 无障碍 · --tap）。
 *
 * 三列固定：**图标那一格钉死 20px**（图标本身有的 24 视框画满、有的留白，
 * 不钉宽度的话名字那一列会跟着图标胖瘦左右飘 —— 业主说的「不齐」就是这个）；
 * 名字吃掉中间全部；值靠右，右对齐成一列。
 */
.dw-row {
  display: flex; align-items: center; gap: var(--sp-3); width: 100%;
  min-height: 44px; padding: 0 var(--sp-3); border: 0; border-radius: var(--r);
  background: none; color: var(--fg2); text-align: left; cursor: pointer;
  font: inherit; font-size: var(--f-md);
}
.dw-row > .ic { flex: 0 0 20px; width: 20px; height: 20px; display: grid; place-items: center; }
.dw-row svg { color: var(--fg3); }
.dw-k { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/**
 * 值那一列：**右对齐 + 等宽数字**（◇ 设计标准 A：18 和 1218 要对得齐）。
 * 以前是 `white-space: nowrap` 就算完，左边界跟着文字长短飘；
 * 现在给一个下限宽度，三个语言下这一列的位置都一样。
 */
.dw-v {
  flex: 0 0 auto; min-width: 44px; text-align: right;
  color: var(--fg2); font-size: var(--f-sm); white-space: nowrap;
}
.dw-v:empty { min-width: 0; }
.dw-row:hover { background: var(--s2); color: var(--fg); }
.dw-row:hover svg { color: var(--fg2); }
/* 当前这一屏：底色 + 青字 + 图标也跟着青（参考图里就是整条亮起来） */
.dw-row[aria-current="page"] { background: var(--brand-wash); color: var(--brand); font-weight: 600; }
.dw-row[aria-current="page"] svg { color: var(--brand); }
.dw-row[aria-current="page"] .dw-v { color: var(--brand); }
.dw-row .dot-badge { position: static; transform: none; flex: 0 0 auto; }

/**
 * 51 抽屉顶上那一块「这一单」。
 *
 * 原来抽屉一开头就是「总览 / 项目 / 通知」—— 人按 ☰ 的时候脑子里想的是
 * 「我这一单还差什么」，而屏幕先给他一列名词。这一块把答案摆在第一眼：
 * 单名 + 那颗全 App 统一的状态点（ui.js 的 statusChip），点了进模型。
 * **没开单时整块不出现** —— 摆一块写着「—」的卡比不摆更糟（◇ 设计标准 C）。
 */
.dw-job {
  display: flex; flex-direction: column; gap: var(--sp-2); align-items: flex-start;
  width: 100%; margin-bottom: var(--sp-4);
  padding: var(--sp-3); border: 1px solid var(--line); border-radius: var(--r);
  background: var(--s2); color: var(--fg); text-align: left; cursor: pointer;
  font: inherit;
}
.dw-job:hover { background: var(--s3); }
.dw-job-name {
  font-size: var(--f-md); font-weight: 650; letter-spacing: .01em;
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.dw-job-st:empty { display: none; }

/**
 * 51 抽屉最底下那一块：谁在用这个 App。
 *
 * 「我的」原来只在设置那一组里占一行，和「参数」「拆单」一样大 —— 而它不是
 * 这一单的一个步骤。挪到最底下单独一块（参考图上那一排就是这样），
 * 贴着面板底边，不跟着上面那一列滚。
 */
.dw-acct {
  flex: 0 0 auto; display: flex; align-items: center; gap: var(--sp-3);
  width: 100%; min-height: 56px; padding: var(--sp-2) var(--sp-4);
  border: 0; border-top: 1px solid var(--line-soft); background: none;
  color: var(--fg); text-align: left; cursor: pointer; font: inherit;
}
.dw-acct:hover { background: var(--s2); }
.dw-acct[aria-current="page"] { background: var(--brand-wash); color: var(--brand); }
.dw-ava {
  flex: 0 0 32px; width: 32px; height: 32px; border-radius: var(--r-pill);
  display: grid; place-items: center;
  background: var(--brand); color: var(--brand-ink);
  font-size: var(--f-sm); font-weight: 700;
}
.dw-acct-txt { flex: 1; min-width: 0; display: grid; }
.dw-acct-name,
.dw-acct-sub { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dw-acct-name { font-size: var(--f-sm); font-weight: 600; }
.dw-acct-sub { font-size: var(--f-xs); color: var(--fg2); }
.dw-acct-sub:empty { display: none; }
/* 角色是**在这一单里的**身份 —— 没开单时算不出来，那就一个字都不印（铁律 2） */
.dw-acct-role { flex: 0 0 auto; font-size: var(--f-xs); color: var(--fg2); }

/* 抽屉开着时背后别跟着滚 */
body.drawer-open { overflow: hidden; }

/* 顶栏右边那两颗圆钮（通知 · 头像）—— 红点挂在通知上 */
.bell-btn, .me-btn { position: relative; }
.bell-btn[aria-current="page"], .me-btn[aria-current="page"] { color: var(--brand); }

/* ══ 宽数据屏放开宽度（⑫）══════════════════════════════
 * 料单 12 栏、排版一张张板材图，都被 .wrap 那个 860 硬顶住：
 * 表格自己横滚，而窗口右边还空着一两百像素 —— 27 寸屏幕上仍要左右拖表格。
 * 这就是「把手机版拉宽」的另一种样子（◇ 设计标准 B#15）。
 * 放到 1400 为止：再宽一行拉到两尺开外，眼睛会串行。
 * 超过 1400 表格照样自己横滚 —— **页面本身永远不横滚**这条不动。 */
@media (min-width: 900px) {
  .wrap:has(.cut-scroll), .wrap:has(.sheet) { max-width: 1400px; }
}

.stage {
  position: relative; background: var(--s1); border: 1px solid var(--line);
  border-radius: var(--r-lg); overflow: hidden;
  height: min(52vh, 460px);
}
@media (min-width: 900px) { .stage { height: min(64vh, 620px); } }
.viewport3d { display: block; width: 100%; height: 100%; touch-action: none; cursor: grab; }
.viewport3d:active { cursor: grabbing; }

/* 标注层：只是画，不吃点击 —— 吃了就转不动模型 */
.dim-layer { position: absolute; inset: 0; pointer-events: none; }
.dim-svg { position: absolute; inset: 0; }
.dim-line, .dim-tick { stroke: var(--brand); stroke-width: 1; opacity: .75; }
.dim-tick { opacity: .4; stroke-dasharray: 3 3; }
.dim-text {
  fill: var(--brand); font-family: var(--font-num); font-size: 12px;
  font-variant-numeric: tabular-nums; paint-order: stroke;
  stroke: var(--bg); stroke-width: 4px; stroke-linejoin: round;
}

.stage-top { position: absolute; top: var(--sp-3); left: var(--sp-3); }
.gate-badge {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  min-height: var(--tap); padding: 0 var(--sp-4);
  border-radius: var(--r-pill); border: 1px solid; cursor: pointer;
  font-size: var(--f-xs); backdrop-filter: blur(6px);
  transition: transform var(--t-fast) var(--ease), background var(--t) var(--ease);
}
.gate-badge.ok { background: var(--glass); border-color: var(--brand-line); color: var(--brand); }
.gate-badge.bad { background: var(--glass); border-color: var(--danger-line); color: var(--danger); }
.gate-badge:hover { background: var(--s2); }
.gate-badge:active { transform: scale(.96); }

.stage-tools {
  position: absolute; right: var(--sp-3); top: var(--sp-3);
  display: flex; flex-direction: column; gap: var(--sp-2);
}
.tool-btn {
  width: var(--tap); height: var(--tap); display: grid; place-items: center;
  background: var(--glass); border: 1px solid var(--line); border-radius: var(--r);
  color: var(--fg2); cursor: pointer; backdrop-filter: blur(6px);
  transition: background var(--t) var(--ease), color var(--t) var(--ease), transform var(--t-fast) var(--ease);
}
.tool-btn:hover { background: var(--s3); color: var(--fg); }
.tool-btn:active { transform: scale(.94); }
.tool-btn[aria-pressed="true"] { color: var(--brand); border-color: var(--brand-dim); }

.controls { display: grid; gap: var(--sp-3); margin-top: var(--sp-4); }
@media (min-width: 560px) { .controls { grid-template-columns: 1fr 1fr; } }
.slider {
  background: var(--s1); border: 1px solid var(--line); border-radius: var(--r);
  padding: var(--sp-3) var(--sp-4);
}
.sl-head { display: flex; align-items: center; gap: var(--sp-2); margin-bottom: var(--sp-2); }
.sl-head .ic { width: 18px; height: 18px; color: var(--fg3); }
.sl-k { flex: 1; font-size: var(--f-sm); color: var(--fg2); }
.sl-val { font-size: var(--f-sm); color: var(--fg); font-variant-numeric: tabular-nums; }
input.sl {
  -webkit-appearance: none; appearance: none; width: 100%; height: var(--tap);
  background: none; cursor: pointer;
}
input.sl::-webkit-slider-runnable-track { height: 4px; border-radius: 2px; background: var(--s3); }
input.sl::-moz-range-track { height: 4px; border-radius: 2px; background: var(--s3); }
input.sl::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none; width: 22px; height: 22px; margin-top: -9px;
  border-radius: 50%; background: var(--brand); border: 3px solid var(--s1); cursor: grab;
}
input.sl::-moz-range-thumb {
  width: 22px; height: 22px; border-radius: 50%; background: var(--brand);
  border: 3px solid var(--s1); cursor: grab;
}
input.sl:active::-webkit-slider-thumb { transform: scale(1.12); }

.m3-stats { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-top: var(--sp-4); }

/**
 * ㉟ 「另有 N 个柜还没排位置，图上没有它们」——
 * 贴着图下面那排数走，说的是同一张图的事。
 * 按钮不许铺满正文柱（◇B#9 / 规则 283 的 W2），所以整条 fit-content。
 */
.m3-unplaced {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2);
  margin-top: var(--sp-2); color: var(--fg2); font-size: var(--f-sm);
}


/* 属性卡 */
.prop {
  background: var(--s1); border: 1px solid var(--line); border-radius: var(--r-lg);
  padding: var(--sp-4);
}
.prop.empty-prop { text-align: center; color: var(--fg2); padding: var(--sp-6) var(--sp-4); }
.prop.empty-prop .ic { color: var(--fg3); }
.prop-hint { margin-top: var(--sp-3); font-size: var(--f-sm); line-height: 1.7; color: var(--fg2); }
.prop-head { display: flex; align-items: flex-start; gap: var(--sp-2); margin-bottom: var(--sp-3); }
.prop-name { flex: 1; font-size: var(--f-md); font-weight: 600; line-height: 1.5; overflow-wrap: anywhere; }
.prop-rows { border-top: 1px solid var(--line-soft); }
.prop-row {
  display: flex; justify-content: space-between; gap: var(--sp-3);
  padding: var(--sp-2) 0; border-bottom: 1px solid var(--line-soft);
}
.prop-row .k { color: var(--fg2); font-size: var(--f-sm); }
.prop-row .v { font-family: var(--font-num); font-variant-numeric: tabular-nums; }
.prop-pills { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-top: var(--sp-3); }
.prop-acts { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-top: var(--sp-4); }

/* 从三维跳过来那一行闪一下 —— 跳过去却不知道跳到哪，等于没跳 */
@keyframes row-flash { 0%, 100% { background: var(--s2) } 40% { background: var(--brand-wash) } }
.cut-row.flash { animation: row-flash 1.4s var(--ease); }
@media (prefers-reduced-motion: reduce) { .cut-row.flash { animation: none; background: var(--brand-wash); } }
/* ㉓：从提醒里「去改这一项」跳过来时，指一下是哪一格（和料单那一行同一个动法，◇C 图标成套的同族） */
@keyframes card-flash { 0%, 100% { border-color: var(--line) } 40% { border-color: var(--brand) } }
.fieldcard.flash { animation: card-flash 1.4s var(--ease); }
@media (prefers-reduced-motion: reduce) { .fieldcard.flash { animation: none; border-color: var(--brand); } }

/* ㉔ 提醒框里那条「去看那几条」把人送回顶上的闸门框 —— 闪一下，不然
   一个已经在视野里的框「按了没反应」。闸门自己有边框色，闪它的边框。 */
@keyframes gate-flash { 0%, 100% { box-shadow: none } 40% { box-shadow: 0 0 0 2px var(--brand) } }
.gate.flash { animation: gate-flash 1.4s var(--ease); }
@media (prefers-reduced-motion: reduce) { .gate.flash { animation: none; box-shadow: 0 0 0 2px var(--brand); } }
.gate-badge.warnbadge { background: var(--glass); border-color: var(--warn-line); color: var(--warn); cursor: default; }

/* ══ 导出屏 ⑧ ═══════════════════════════════════════════ */
.expcard { margin-bottom: var(--sp-3); }
.exp-head { display: flex; align-items: center; gap: var(--sp-2); margin-bottom: var(--sp-2); }
.exp-head .ic { color: var(--fg3); }
.exp-t { font-size: var(--f-md); font-weight: 600; }
.exp-b { color: var(--fg2); font-size: var(--f-sm); line-height: 1.7; }
.exp-acts { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-top: var(--sp-4); }
.share-meta { color: var(--fg2); font-size: var(--f-xs); margin-top: 2px; }
.rowitem .k > .num { overflow-wrap: anywhere; font-size: var(--f-sm); }

/* 分享链接落地页：顶上那条常驻提示，别让人以为这是自己的 App */
.sharebar {
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3); margin-bottom: var(--sp-4);
  background: var(--brand-wash); border: 1px solid var(--brand-line); border-radius: var(--r);
  color: var(--brand); font-size: var(--f-xs);
}
.sharebar .ic { width: 16px; height: 16px; }
.sharebar > span:last-child { margin-left: auto; color: var(--fg2); }

/* ══ 纸上那一份（S15 ⑥）══════════════════════════════════
 * 屏幕是深色工作台，纸是白的。整套颜色在这里推翻重来 ——
 * 把深色底印出去等于把一整版墨吃掉，而且线全看不清。
 */
#printarea { display: none; }

@page { size: A4; margin: 12mm 10mm 14mm; }

@media print {
  /* 打印时只留纸上那一份，App 外壳整个不出现 */
  body.printing > #app,
  body.printing > .tabbar,
  body.printing > #offline,
  /**
   * ㊵ 顺手补的：底部那两条常驻条以前**会印到纸上去** ——
   * 它们是 fixed，打印时会落在第一页上，而这张纸是发给工厂开料的。
   * ⑨ 那条「有新版本了」从来没进过这张名单（#offline 进了，它漏了）。
   * ⚠️ 原来那条 `body.printing > #toasts` 删了 —— toast 现在住在这一摞里面，
   *    那个选择器再也匹配不到任何东西（留着就是一条指不到东西的规矩）。
   */
  body.printing > #bottombars,
  body.printing > dialog { display: none !important; }
  body.printing { background: #fff; }
  #printarea {
    display: block; color: #111; background: #fff;
    font-family: var(--font); font-size: 9.5pt; line-height: 1.35;
  }
  .pr-sheet { break-inside: auto; }
  .pr-break { break-before: page; }

  .pr-head { border-bottom: 1.6pt solid #111; padding-bottom: 3mm; margin-bottom: 4mm; }
  .pr-title { font-size: 16pt; font-weight: 700; letter-spacing: -.01em; }
  .pr-sub { display: flex; flex-wrap: wrap; gap: 5mm; margin-top: 1.5mm; font-size: 8.5pt; color: #444; }

  .pr-gate { border: 1pt solid #111; padding: 3mm; margin-bottom: 4mm; break-inside: avoid; }
  .pr-gate.bad { border-width: 2.4pt; }
  .pr-gate-t { font-weight: 700; font-size: 11pt; margin-bottom: 1.5mm; }
  .pr-gate.bad .pr-gate-t { color: #000; background: #f2f2f2; padding: 1.5mm 2mm; }
  .pr-reasons { margin: 0; padding-left: 5mm; }
  .pr-reasons li { margin-bottom: 1mm; }
  .pr-note { margin-top: 2mm; font-size: 8pt; color: #555; }

  .pr-h3 { font-size: 11pt; margin: 6mm 0 2mm; }
  .pr-table { width: 100%; border-collapse: collapse; }
  /* 表头每页重复 —— 第二页开始没有表头的料单，工厂看不懂哪列是哪列 */
  .pr-table thead { display: table-header-group; }
  .pr-table tr { break-inside: avoid; }
  .pr-table th, .pr-table td {
    border: 0.4pt solid #999; padding: 1.2mm 1.6mm; text-align: left; vertical-align: top;
  }
  .pr-table th { background: #ececec; font-weight: 600; font-size: 8.5pt; }
  .pr-table td:nth-child(1), .pr-table td:nth-child(3),
  .pr-table td:nth-child(4), .pr-table td:nth-child(7) {
    font-family: var(--font-num); font-variant-numeric: tabular-nums; white-space: nowrap;
  }
  .pr-table tr.pending td { background: #fbf6e6; }

  .pr-nest { break-inside: avoid; margin: 3mm 0; }
  .pr-nest svg { width: 100%; height: auto; }

  .pr-foot {
    display: flex; justify-content: space-between; gap: 4mm;
    margin-top: 4mm; padding-top: 2mm; border-top: 0.6pt solid #999;
    font-size: 8pt; color: #444;
  }
}

/* ══ 手指优先：窄屏上小按钮也得 ≥44（◇ 设计标准 B-14）══════
 * .btn.sm 的 34px 是给鼠标用的密度。手指按不到 34px 的目标，
 * 会连点两次 —— 在「还原 / 删除 / 关掉链接」这种按钮上，第二次点的是别的东西。
 */
@media (max-width: 899px) {
  .btn.sm { min-height: var(--tap); padding: 0 var(--sp-4); }
  .field-foot .btn.sm { min-height: var(--tap); }
  /**
   * ㊶：toast 里那颗「撤销」实测只有 **25px 高**（`.scratch/shots41.mts` 量的）。
   * 而 ⑬ 那条「23 屏 0 个小于 44px」**结构上够不着它** —— 扫屏是逐屏 visit 之后量，
   * 那一刻屏幕上根本没有 toast（toast 只在人做完一件事之后冒三五秒）。
   * 于是那句「一个都不小于 44px」对 toast 从来没成立过（假绿那一族）。
   * 它偏偏挂在最要紧的那颗按钮上：删柜子 / 删房间 / 删柜段 / 删图 / 删单 的**唯一回头路**
   * （规则 249）。按不中就是那一单真的没了。
   * ⚠️ 只在窄屏提 —— 桌面那 25px 是鼠标密度，和 `.btn.sm` 同一条思路。
   */
  .toast button { min-height: var(--tap); padding: 0 var(--sp-2); }
  /**
   * ㊷：弹出来的那种菜单（`.menu`）每一条实测 **170×40**（`.scratch/probe42.mts` 量的）。
   * 和上面那颗 toast「撤销」是同一族 —— **扫屏结构上够不着它**：
   * ⑬ 那条「23 屏 0 个小于 44px」是逐屏 `visit` 之后量的，
   * 而这两个菜单都得先点一下才冒出来，扫的那一刻屏幕上根本没有。
   * 用它的只有两处，两处都在手机的主路上（改之前把用到这个类的屏全列了一遍，⑯⑰⑱⑲ 那一课）：
   *   顶栏那颗地球 → 语言菜单（`#langmenu`，**每一屏都有**，三语产品的主路径）
   *   项目卡右上角那颗「更多」→「归档 / 删除」（`#cardmenu`）——
   *     **删除是不可逆的**，40px 上按歪一下点到的是隔壁那条「归档」（规则 242/249）
   * ⚠️ 只在窄屏提 —— 桌面那 40px 是鼠标密度，和 `.btn.sm` / toast 同一条思路。
   */
  .menu button { min-height: var(--tap); }
}

/* ══ 建模编辑器 ⑦（S21）══════════════════════════════════ */
.cabrow .cab-name { font-size: var(--f-md); font-weight: 600; }
.cabrow .cab-meta { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-top: var(--sp-3); }
.pill.danger { background: var(--danger-wash); color: var(--danger); border-color: var(--danger-line); }

/* 编辑器里的 3D 是「改了立刻看见」用的，比核对屏矮一半，不抢表单的位置 */
.stage-sm { height: min(34vh, 300px); margin-bottom: var(--sp-4); }

/* ㊹ 在模型上直接改：舞台 + 旁边那张「能改什么」的卡。
   手柄层沿用标注层（默认不吃指针，不然转不动模型），只有圆点自己吃指针。 */
.im-wrap { display: grid; gap: var(--sp-4); margin-bottom: var(--sp-4); }
@media (min-width: 900px) {
  .im-wrap { grid-template-columns: minmax(0, 1fr) 300px; align-items: start; }
  .im-side { position: sticky; top: var(--sp-4); }
}
.im-stage { height: min(48vh, 440px); margin-bottom: var(--sp-3); }
.im-grip { pointer-events: auto; }
/* 那张卡只有 300px 宽：前脸那张 bayCard 在正文柱里是一行的，到这里得折行，
   不然「均分」被压成「均 / 分」两行（规则 263，⑯ 撞见过的那种）。提示句独占一行。 */
.im-side .card.bay .row, .m3-side .card.bay .row { flex-wrap: wrap; }
.im-side .card.bay .row > .hint, .m3-side .card.bay .row > .hint { flex-basis: 100%; }
.im-side .card.bay .btn, .m3-side .card.bay .btn { white-space: nowrap; }
/* ㊺「模型」页签那张属性卡：能改什么排在料单那几行上面，中间一条细线分开 */
.im-edit { padding-bottom: var(--sp-3); margin-bottom: var(--sp-3); border-bottom: 1px solid var(--line-soft); }
.im-edit:empty { display: none; }
.im-edit .card.bay { margin-top: var(--sp-2); }

.card.zone { padding: var(--sp-4); background: var(--s1); }
.card.zone > .list { margin-top: var(--sp-3); }
.card.bay { background: var(--s2); border-color: var(--line-soft); padding: var(--sp-3); }
.bay-t { font-size: var(--f-sm); color: var(--fg2); font-weight: 550; }
.card.bay .seg { margin-top: var(--sp-2); }
.card.bay .seg-b { min-width: 62px; }

.stepper { display: inline-flex; align-items: center; gap: var(--sp-2); }
.stepper .btn.sm { min-width: 44px; font-size: var(--f-lg); line-height: 1; }
.stepper output { min-width: 2.2em; text-align: center; font-size: var(--f-md); }
.quick { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-top: var(--sp-2); }

/* 新建向导：一个对话框走三步，点数在底下 */
dialog.wiz { max-width: 460px; width: min(460px, calc(100vw - 32px)); }
dialog.wiz .list { margin-bottom: var(--sp-4); }
dialog.wiz .opt[aria-pressed="true"] { border-color: var(--brand-dim); background: var(--brand-wash); }
dialog.wiz .dots { margin: var(--sp-4) 0 var(--sp-3); }
dialog.wiz .field { margin-bottom: var(--sp-4); }
.btn.sm.ghost.danger { color: var(--danger); }


/* ══ 房间与柜段 ⑦b ═══════════════════════════════════════
 * 俯视图是这一屏的主角（铁律 6：确认靠画图）。UI 退到它后面去：
 * 图上只有四支笔 —— 墙灰、柜暖木、洞警告黄、梁柱阻断红。
 */
.planwrap {
  position: relative;
  background: var(--s1);
  border: 1px solid var(--line);
  border-radius: var(--r);
  padding: var(--sp-2);
  margin-bottom: var(--sp-4);
  overflow: hidden;
}
.plan { display: block; width: 100%; height: auto; }
.pl-floorfill { fill: var(--floor); stroke: none; }
.pl-wallline { stroke: var(--fg3); stroke-width: 5; stroke-linecap: square; }
.pl-wallline.slant { stroke: var(--warn); stroke-dasharray: 7 5; }
.pl-openline { stroke-width: 5.5; stroke-linecap: butt; }
.pl-openline.door { stroke: var(--warn); }
.pl-openline.window { stroke: var(--warn); opacity: .6; }
.pl-obsbox { fill: var(--danger-wash); stroke: var(--danger); stroke-width: 1.4; stroke-dasharray: 4 3; }
.pl-cabbox { fill: #c08d5a26; stroke: var(--wood); stroke-width: 1.4; }
.pl-cabbox.bad { fill: var(--danger-wash); stroke: var(--danger); stroke-width: 2; }
.pl-front { stroke: var(--brand); stroke-width: 2.6; stroke-linecap: round; }
.pl-cablabel { fill: var(--fg2); font-family: var(--font-num); font-size: 11px; }
.pl-fixbox { fill: none; stroke: var(--fg2); stroke-width: 1.3; stroke-dasharray: 5 4; }
.pl-dimline { stroke: var(--brand); stroke-width: 1.2; }
.pl-dimline.faint { stroke: var(--fg3); }
.pl-dimlabel { fill: var(--brand); font-family: var(--font-num); font-size: 12px; }
.pl-dimlabel.bad { fill: var(--danger); font-weight: 600; }
.pl-dimlabel.faint { fill: var(--fg3); }
.pl-scale { stroke: var(--fg3); stroke-width: 1.2; }
.pl-scalelabel { fill: var(--fg3); font-family: var(--font-num); font-size: 10px; }

/* 闭合面板：一屏里最大的两个数，等宽对齐，差多少写成人话 */
/**
 * 闭合算式现在住在结论框**里面**（⑭）—— 它不再自己是一个框。
 * 以前它有自己的绿/红/黄底，下面紧跟着一个同样颜色的守门框：
 * **一屏两个结论框**，◇ 设计标准明令禁止（⑦b 踩过）。
 * 现在只在顶上隔一道线，跟结论那句话分开。
 */
.closenums { margin-top: var(--sp-3); padding-top: var(--sp-3); border-top: 1px solid var(--line); }
.close-nums { display: flex; align-items: baseline; gap: var(--sp-4); flex-wrap: wrap; }
.close-one { display: flex; flex-direction: column; gap: 2px; }
.close-one .k { font-size: var(--f-xs); color: var(--fg2); text-transform: uppercase; letter-spacing: .06em; }
.close-one .v { font-family: var(--font-num); font-variant-numeric: tabular-nums; font-size: var(--f-xl); }
.close-one .v.small { font-size: var(--f-lg); color: var(--fg2); }
.close-eq { font-size: var(--f-lg); color: var(--fg2); align-self: center; }
.close-hint { margin-top: var(--sp-2); font-size: var(--f-sm); color: var(--fg2); line-height: 1.7; }
/* ㉙ 调整板（规则 309）—— 差额那一句话底下的第三条出路。
   窄屏竖排（规则 263：一句话不许被挤成一个字一行），宽屏跟在问句后面 */
.filler-row {
  margin-top: var(--sp-3); display: flex; flex-wrap: wrap; gap: var(--sp-2);
  align-items: center; font-size: var(--f-sm); color: var(--fg2); line-height: 1.7;
}
.filler-row .filler-q, .filler-row .filler-has { max-width: 62ch; }
.filler-row .filler-has { color: var(--fg); }
@media (max-width: 560px) {
  .filler-row { flex-direction: column; align-items: stretch; }
}

.close-terms {
  margin-top: var(--sp-3); padding-top: var(--sp-3); border-top: 1px solid var(--line);
  display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center;
  font-family: var(--font-num); font-variant-numeric: tabular-nums;
  font-size: var(--f-sm); color: var(--fg2);
}
.close-terms .op { color: var(--fg2); }
.close-terms b { color: var(--fg); font-weight: 550; }

/* 段里的柜子：一行一个，左边序号右边挪动 */
.segcab { display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-3); }
/* 序号 = 选中开关。触摸区 44，看上去还是那个小圈（⑦c） */
.segcab .ord {
  width: 44px; height: 44px; flex: none; padding: 0;
  background: none; border: none; cursor: pointer;
  display: grid; place-items: center;
}
.segcab .ord > span {
  width: 26px; height: 26px; border-radius: var(--r-pill);
  background: var(--s3); color: var(--fg2);
  display: grid; place-items: center;
  font-family: var(--font-num); font-size: var(--f-xs);
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.segcab .ord:hover > span { background: var(--line); color: var(--fg); }
.segcab .ord:active > span { transform: scale(.92); }
.segcab .ord:focus-visible { outline: 2px solid var(--brand); outline-offset: -4px; border-radius: var(--r-pill); }
.segcab .ord.on > span { background: var(--brand); color: var(--brand-ink); }
.segcab.on { background: var(--brand-wash); }
.segcab .who { flex: 1; min-width: 0; }
.segcab .who .n { font-size: var(--f-md); font-weight: 550; }
.segcab .who .m {
  font-family: var(--font-num); font-variant-numeric: tabular-nums;
  font-size: var(--f-xs); color: var(--fg2); margin-top: 2px;
}
.segcab .acts { display: flex; gap: var(--sp-1); flex: none; }
.segcab .acts .btn.sm { min-width: 44px; min-height: 44px; padding: 0; }
.jointrow {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-1) var(--sp-3) var(--sp-1) 42px;
  font-size: var(--f-xs); color: var(--fg2);
}
.jointrow .seg { flex: 1; }
.jointrow .seg-b { font-size: var(--f-xs); min-height: 30px; }

/* 房间卡：缩略俯视图 + 两个数 */
.roomcard .thumb {
  background: var(--floor); border: 1px solid var(--line-soft); border-radius: var(--r-sm);
  overflow: hidden; margin-bottom: var(--sp-3);
}
/*
 * 缩略图给定比例、SVG 铺满它 —— 只给 SVG 定高，容器会按内在比例塌一截，把柜子裁掉。
 * ⑯：比例必须和 planSvg 的画布（640×345）对上，不然 preserveAspectRatio
 * 会在上下留一圈黑边，房间画得比该有的小一大截。
 */
/* height:auto 是必须的 —— 不写就落回上面那条通用的 .thumb{height:84px} */
.roomcard .thumb { width: 100%; height: auto; aspect-ratio: 640 / 345; }
.roomcard .thumb .plan { width: 100%; height: 100%; }
.roomcard .rm-name { font-size: var(--f-md); font-weight: 600; }
.roomcard .rm-meta { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-top: var(--sp-2); }

@media (min-width: 900px) {
  .planwrap .plan { max-height: 62vh; }
}
@media (max-width: 420px) {
  .close-one .v { font-size: var(--f-lg); }
  .segcab { flex-wrap: wrap; }
  .segcab .acts { width: 100%; justify-content: flex-end; }
  /* 34px 是鼠标密度，手指按不到 —— 窄屏一律提到 44（踩过一次，见交接件） */
  .jointrow { flex-wrap: wrap; padding-left: var(--sp-3); }
  .jointrow .seg { flex-basis: 100%; }
  .jointrow .seg-b { min-height: 44px; }
}

/* ── 拖拽 · 吸附 · 选中 ⑦c ──────────────────────────────
 * 手柄要够大（手指 ≥44px 的判据在触摸区，不在圆点半径），
 * 吸附提示线只在拖的那一刻出现，别让它变成第二套「标注」。
 */
.pl-gridline { stroke: #23262b; stroke-width: 1; }
.pl-cabbox.on { stroke: var(--brand); stroke-width: 2.4; fill: #6fbfae1a; }
.pl-cabbox.pick { cursor: pointer; }
.pl-cabbox.pick:hover { fill: #c08d5a3d; }
.pl-cabbox.pick:focus-visible { outline: none; stroke: var(--brand); stroke-width: 2.6; }
.pl-hline { stroke: var(--brand-dim); stroke-width: 1.2; stroke-dasharray: 3 3; }
.pl-hline.seg { stroke: var(--fg3); }
.pl-grip {
  fill: var(--s2); stroke: var(--brand); stroke-width: 2;
  cursor: ew-resize; touch-action: none;
}
.pl-grip.seg { stroke: var(--fg2); cursor: grab; }
.pl-grip:hover { fill: var(--s3); r: 11; }
.pl-grip:active { fill: var(--brand); }
.pl-grip:focus-visible { outline: none; stroke: var(--fg); stroke-width: 3; }
.pl-guideline { stroke: var(--brand); stroke-width: 1.6; stroke-dasharray: 6 4; }
.pl-guidelabel { fill: var(--brand); font-family: var(--font-num); font-size: 12px; }

/* 拖动中的读数：贴在图的右上角，等宽，跟着变 */
/*
 * 拖动中那块读数。
 *
 * ⚠️ ⑱ 从右上挪到**右下**：⑦c 放右上时只有俯视图一张图，那儿是空的。
 * 侧剖右上角有面层链的抬头「竖向·面层」和吸附提示的数，读数一压上去
 * 三样东西叠在一起（那正是规则 253 / 263 那一族的毛病）。
 * 三张图的右下角都是空的（比例尺在左下、段总长在中间）。
 */
.dragread {
  position: absolute; bottom: var(--sp-3); right: var(--sp-3);
  background: #191b1eee; border: 1px solid var(--brand-dim); border-radius: var(--r-sm);
  padding: var(--sp-2) var(--sp-3); font-family: var(--font-num);
  font-variant-numeric: tabular-nums; font-size: var(--f-md); color: var(--brand);
  pointer-events: none;
}
.dragread .snap { display: block; font-size: var(--f-xs); color: var(--fg2); margin-top: 2px; }

/* 图上面那条工具条：格子开关 · 吸附开关 · 选中计数 */
.plantools {
  display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap;
  margin-bottom: var(--sp-3);
}
.plantools .spacer { flex: 1; }
.toggle {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  min-height: 44px; padding: 0 var(--sp-3);
  background: var(--s1); border: 1px solid var(--line); border-radius: var(--r-pill);
  color: var(--fg2); font-size: var(--f-sm); cursor: pointer;
}
.toggle:hover { background: var(--s2); color: var(--fg); }
.toggle[aria-pressed="true"] { border-color: var(--brand-dim); background: var(--brand-wash); color: var(--brand); }
.toggle:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.toggle input { display: none; }

/* 多选那一条 */
.selbar {
  display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
  border: 1px solid var(--brand-dim); background: var(--brand-wash);
  border-radius: var(--r); padding: var(--sp-3); margin-bottom: var(--sp-4);
}
.selbar .who { font-size: var(--f-md); color: var(--brand); font-weight: 550; }
.selbar .spacer { flex: 1; }


/* ══════════════════════════════════════════════════════════
 * 导入 ⑤ · 识别确认（2026-09-03）
 * 颜色一律 var()，这里不新引任何色值（tokens.css 是唯一一处）
 * ════════════════════════════════════════════════════════ */

/* ── 顶上那条「这一屏干什么」的说明 ────────────────── */
.note {
  display: flex; gap: 10px; align-items: flex-start;
  padding: 14px 16px; margin-bottom: var(--sp-5);
  background: var(--s1); border: 1px solid var(--line);
  border-radius: var(--r); color: var(--fg2); font-size: var(--f-sm); line-height: 1.55;
}
.note .ic { color: var(--fg3); flex: 0 0 auto; margin-top: 1px; }
.note.brandline { border-left: 3px solid var(--brand); }
.note.brandline .ic { color: var(--brand); }
.note.warn { border-left: 3px solid var(--warn); background: var(--warn-wash); }
.note.warn .ic { color: var(--warn); }

.linkish {
  margin-top: 6px; padding: 0; background: none; border: 0;
  color: var(--brand); font: inherit; font-size: var(--f-sm); cursor: pointer;
  text-decoration: underline; text-underline-offset: 3px;
}
.linkish:hover { color: var(--fg); }
.linkish:focus-visible { outline: 2px solid var(--brand); outline-offset: 3px; border-radius: 3px; }

/* ── 四个入口 ─────────────────────────────────────── */
.list.entries { display: grid; gap: 10px; grid-template-columns: 1fr 1fr; }
@media (max-width: 560px) { .list.entries { grid-template-columns: 1fr; } }

.entry {
  display: flex; gap: 12px; align-items: center; text-align: left;
  min-height: var(--tap); padding: 14px 16px;
  background: var(--s1); border: 1px solid var(--line); border-radius: var(--r);
  color: var(--fg); font: inherit; cursor: pointer;
  transition: background var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease),
              transform var(--t-fast) var(--ease);
}
.entry:hover { background: var(--s2); border-color: var(--brand-dim); }
.entry:active { transform: scale(.985); }
.entry:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.entry.primary { border-color: var(--brand-dim); background: var(--brand-wash); }
.entry.primary .entry-ic { color: var(--brand); }
/* 「敬请期待」的东西不许长得像能点的：不给 hover、不给指针、写明没做 */
.entry.soon { cursor: default; opacity: .62; }
.entry.soon:hover { background: var(--s1); border-color: var(--line); }
.entry.soon .entry-ic { color: var(--fg3); }

.entry-ic {
  flex: 0 0 auto; width: 40px; height: 40px; border-radius: var(--r-sm);
  display: grid; place-items: center; background: var(--s2); color: var(--fg2);
}
.entry-txt { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.entry-txt b { font-size: var(--f-md); font-weight: 600; }
.entry-txt small { color: var(--fg2); font-size: var(--f-xs); line-height: 1.4; }

/* ── 上传进度 ─────────────────────────────────────── */
.card.upload { margin-top: 12px; }
.card.upload .bar {
  height: 6px; margin-top: 12px; border-radius: var(--r-pill);
  background: var(--s2); overflow: hidden;
}
.card.upload .bar i {
  display: block; height: 100%; background: var(--brand);
  transition: width 120ms linear;
}

/* ── 图的列表 ─────────────────────────────────────── */
.list.assets { display: grid; gap: 12px; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); }
.card.asset { padding: 0; overflow: hidden; display: flex; flex-direction: column; }
.card.asset .thumb {
  width: 100%; aspect-ratio: 4 / 3; object-fit: cover; display: block;
  background: var(--s2); border-bottom: 1px solid var(--line);
}
.card.asset .thumb.doc { display: grid; place-items: center; color: var(--fg3); }
.card.asset .thumb.doc.warn { color: var(--warn); }
.asset-meta { padding: 12px 14px 14px; }
.asset-name {
  font-weight: 600; font-size: var(--f-sm); min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* 项目卡上的真照片 */
.card .thumb > img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* ── 识别确认：图 + 框 ────────────────────────────── */
/* ㊻ 照片认结构：提案卡 + 结构小图（画的是形状不是尺寸 —— 每层一样高、每格一样宽） */
.card.recog { border-color: var(--brand-dim); }
.card.recog .rc-conf { margin-top: var(--sp-2); }
.rc-sketch { margin-top: var(--sp-3); }
.rc-rows { display: flex; flex-direction: column; gap: 6px; }
.rc-row { display: grid; grid-template-columns: minmax(0, 132px) 1fr; gap: var(--sp-3); align-items: stretch; }
.rc-zone { color: var(--fg2); font-size: var(--f-sm); align-self: center; }
.rc-bays { display: flex; gap: 4px; min-height: 56px; }
.rc-bay {
  flex: 1 1 0; min-width: 0; display: flex; flex-direction: column;
  border: 1px solid var(--line); border-radius: var(--r); background: var(--s2); overflow: hidden;
}
.rc-bay > .rc-leaves, .rc-bay > .rc-strips, .rc-bay > .rc-void { flex: 1 1 auto; display: flex; gap: 2px; padding: 4px; min-height: 30px; }
.rc-leaves i { flex: 1 1 0; background: var(--wood, #c08d5a); opacity: .55; border-radius: 2px; }
.rc-strips { flex-direction: column; }
.rc-strips i { flex: 1 1 0; background: var(--wood, #c08d5a); opacity: .55; border-radius: 2px; }
.rc-void { background: repeating-linear-gradient(135deg, transparent 0 6px, var(--line) 6px 7px); }
.rc-void.appliance { background: var(--s3); border: 1px dashed var(--fg3); border-radius: 4px; margin: 4px; }
.rc-void.panel { background: var(--wood-dark, #8d6136); opacity: .55; border-radius: 2px; margin: 4px; }
.rc-type { font-weight: 600; margin-top: 4px; }
/* ㊼ 提案的每一格画回照片上：框里的比例 → 图上的比例。点一格换类型（≥44px 由格子本身撑起） */
.mk-sub {
  position: absolute; box-sizing: border-box; pointer-events: auto;
  border: 2px solid var(--brand); background: rgba(78, 141, 128, .18); border-radius: 3px;
  display: flex; align-items: flex-end; justify-content: center; overflow: hidden;
}
.mk-sub.drawers { border-color: var(--wood, #c08d5a); background: rgba(192, 141, 90, .22); }
.mk-sub.open { border-style: dashed; background: transparent; }
.mk-sub.void { border-color: var(--fg3); border-style: dotted; background: rgba(255, 255, 255, .06); }
.mk-sub.panel { border-color: var(--wood-dark, #8d6136); background: rgba(141, 97, 54, .3); }
.mk-sub.tap { cursor: pointer; }
.mk-sub.tap:hover { filter: brightness(1.2); }
.mk-sub:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.mk-sub-lbl {
  font-size: var(--f-xs, 12px); line-height: 1.2; padding: 1px 6px; margin-bottom: 2px;
  background: var(--scrim); color: #fff; border-radius: 3px; white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis;
}
.rc-lbl { font-size: var(--f-xs, 12px); color: var(--fg); padding: 2px 6px 4px; border-top: 1px solid var(--line); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-align: center; }
/* ㊽ 两层之间的分界（规则 335）：照片上一根横杆 + 中间一个把手。杆横着画 —— 它量的是竖向（规则 267）。
 * 只在 ≥700px 画（规则 209）；窄屏是下面那一排「拉高 / 拉低」 */
.mk-edge {
  position: absolute; height: 0; pointer-events: none;
  border-top: 2px dashed var(--warn); box-shadow: 0 0 0 1px var(--scrim-soft);
  display: flex; justify-content: center; align-items: center;
}
.mk-grip {
  pointer-events: auto; cursor: ns-resize; touch-action: none;
  width: 22px; height: 22px; margin-top: -1px; border-radius: 50%;
  background: var(--warn); border: 2px solid var(--ink-warn); box-shadow: 0 2px 8px var(--scrim);
}
.mk-grip:hover { filter: brightness(1.15); }
.mk-grip:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.rc-nudge { display: flex; flex-direction: column; gap: 6px; margin-top: var(--sp-2); }
.rc-nudge-row { gap: var(--sp-2); }
.rc-nudge-lbl { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--f-sm); color: var(--fg2); }
.rc-nudge-btns { gap: 6px; flex: 0 0 auto; }
@media (max-width: 560px) {
  .rc-row { grid-template-columns: 1fr; gap: 4px; }
}

.card.mk-wrap { padding: 12px; }
.mk-stage {
  /* 舞台**贴着图的边**（width: fit-content）：覆盖层是 inset:0，
   * 舞台一旦比图宽，框就会整体偏出去。别改成 width:100%。 */
  position: relative; display: block; width: fit-content; max-width: 100%; margin: 0 auto;
  border-radius: var(--r-sm); overflow: hidden; background: var(--s2);
  touch-action: none;                 /* 手机上拖框不许被当成滚动页面 */
  cursor: crosshair;
}
/* 图再高也不许把下面的「认这一框是什么」挤出屏幕 —— 一屏要看得见图和结论 */
.mk-img {
  display: block; max-width: 100%; max-height: 60vh; width: auto; height: auto;
  user-select: none; -webkit-user-drag: none;
}
.mk-ov { position: absolute; inset: 0; pointer-events: none; }

.mk-box {
  position: absolute; box-sizing: border-box;
  border: 2px solid var(--brand); border-radius: 3px;
  background: var(--brand-wash);
  transition: border-color var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
}
.mk-box.on { border-color: var(--warn); background: var(--warn-wash); box-shadow: 0 0 0 2px #0007; }
.mk-box.draft { border-style: dashed; background: transparent; }
.mk-num {
  position: absolute; left: -1px; top: -1px;
  min-width: 22px; height: 22px; padding: 0 6px;
  display: grid; place-items: center;
  background: var(--brand); color: var(--brand-ink);
  font-family: var(--font-num); font-size: var(--f-xs); font-weight: 700;
  border-radius: 3px 0 3px 0;
}
.mk-box.on .mk-num { background: var(--warn); color: var(--ink-warn); }

/* ── 认归属那一块 ─────────────────────────────────── */
.card.assign { border-color: var(--brand-dim); }
.card.assign .lbl {
  display: block; margin: 18px 0 8px;
  color: var(--fg2); font-size: var(--f-xs); letter-spacing: .04em;
}
.card.assign input[type="text"] {
  width: 100%; min-height: var(--tap); padding: 0 14px;
  background: var(--s2); border: 1px solid var(--line); border-radius: var(--r-sm);
  color: var(--fg); font: inherit; font-size: var(--f-md);
}
.card.assign input[type="text"]:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }

.chips { display: flex; flex-wrap: wrap; gap: 8px; }
.chip {
  display: inline-flex; align-items: center; gap: 6px;
  min-height: var(--tap); padding: 0 14px;
  background: var(--s2); border: 1px solid var(--line); border-radius: var(--r-pill);
  color: var(--fg2); font: inherit; font-size: var(--f-sm); cursor: pointer;
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease),
              border-color var(--t-fast) var(--ease);
}
.chip:hover { background: var(--s3); color: var(--fg); }
.chip:active { transform: scale(.97); }
.chip:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.chip.on { background: var(--brand-wash); border-color: var(--brand); color: var(--fg); }
.chip[disabled] { opacity: .45; cursor: not-allowed; }
.chip .ic { color: currentColor; }

/* ── 认过的那些 ───────────────────────────────────── */
.mk-row {
  min-height: var(--tap); padding: 12px 14px;
  background: var(--s1); border: 1px solid var(--line); border-radius: var(--r);
  cursor: pointer;
  transition: background var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
}
.mk-row:hover { background: var(--s2); }
.mk-row:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.mk-row.on { border-color: var(--warn); background: var(--warn-wash); }
.mk-row .seq {
  flex: 0 0 auto; width: 28px; height: 28px; margin-right: 12px;
  display: grid; place-items: center; border-radius: var(--r-pill);
  background: var(--s3); color: var(--fg2);
  font-family: var(--font-num); font-size: var(--f-xs); font-weight: 700;
}
.mk-row.on .seq { background: var(--warn); color: var(--ink-warn); }

/* 手机上图要占满，别留一圈没用的边 */
@media (max-width: 560px) {
  .card.mk-wrap { padding: 8px; }
  .list.assets { grid-template-columns: 1fr; }
}

@media (prefers-reduced-motion: reduce) {
  .entry, .chip, .mk-box, .card.upload .bar i, .mk-row { transition: none; }
}

/* .hint 以前只在 .field 里有样式 —— 导入这几屏在卡片里直接用它，补一条通用的 */
.hint { font-size: var(--f-xs); color: var(--fg2); margin-top: var(--sp-2); line-height: 1.5; }
/* 小标题右边挂个计数时要能排开 */
.section-title { display: flex; align-items: baseline; gap: var(--sp-2); }

/* 图上那个删除按钮：压在缩略图右上角，44px 手指区，深底保证在浅色照片上也看得见 */
.asset-pic { position: relative; }
.icon-btn.on-pic {
  position: absolute; top: 6px; right: 6px;
  width: 36px; height: 36px; border-radius: var(--r-sm);
  background: #000000a8; color: var(--fg); border: 1px solid #ffffff26;
  backdrop-filter: blur(2px);
}
.icon-btn.on-pic:hover { background: var(--danger); color: #fff; border-color: var(--danger); }
.icon-btn.on-pic:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
@media (max-width: 700px) { .icon-btn.on-pic { width: var(--tap); height: var(--tap); } }

/* PDF 文件卡：一个图标 + 文件名 + 页数，不再嵌一个可能一片黑的阅读器 */
.doc-card {
  display: flex; gap: 14px; align-items: center;
  margin-top: 14px; padding: 16px;
  background: var(--s2); border: 1px solid var(--line); border-radius: var(--r);
}
.doc-ic {
  flex: 0 0 auto; width: 52px; height: 52px; border-radius: var(--r-sm);
  display: grid; place-items: center; background: var(--s3); color: var(--fg2);
}
a.btn.ghost { display: inline-flex; align-items: center; text-decoration: none; }

/* 窄屏上文字链也得够手指按（规则：可点的东西 ≥44px；⑦c 那条 .btn.sm 的同一类问题） */
@media (max-width: 700px) {
  .linkish { display: inline-flex; align-items: center; min-height: var(--tap); }

  /* ⑬ 窄屏扫屏抓到的两处漏网 —— ⑪ 那一轮只补了 .linkish 这一类：
   *
   * ① 门面几屏右上角那排语言（34px，且「中」只有 33px 宽）。它是**每个人
   *    打开 App 头一眼要点的东西** —— 看不懂中文的人第一件事就是找它。
   * ② 登录/注册/邀请码底下那条「返回 / 已经有账号」（30×20）。
   *    进不去门的人只剩这一条路，偏偏它最小。 */
  .langbar button { min-width: var(--tap); min-height: var(--tap); }
  .auth .alt a { display: inline-flex; align-items: center; justify-content: center;
    min-height: var(--tap); padding: 0 var(--sp-3); }
}

/* ══════════════════════════════════════════════════════════
 * ⑧ 通知 · 改动记录 · 封板对照 · 帮助 · 法务页（2026-09-03）
 * ════════════════════════════════════════════════════════ */

/* ── 底部 Tab 上的红点 ── */
.tabbar .tabic { position: relative; display: inline-grid; place-items: center; }
.dot-badge {
  position: absolute; top: -4px; right: -10px;
  min-width: 18px; height: 18px; padding: 0 5px;
  display: grid; place-items: center; border-radius: var(--r-pill);
  background: var(--danger); color: #fff;
  font-size: 11px; font-weight: 700; line-height: 1;
}

/* ── 一行记录 ── */
.logrow { display: grid; gap: 4px; }
.logsay { font-size: var(--f-md); line-height: 1.45; min-width: 0; }
.logtime { flex: 0 0 auto; color: var(--fg2); font-size: var(--f-xs); padding-left: 12px; }
.logval { color: var(--fg2); font-size: var(--f-sm); }
.card.logcard { padding: 14px 16px; }
.card.lognote { padding: 14px 16px; }
/* 没看过的那几条：左边一条青线，不是整块变色（整块变色一屏全是色，反而看不出哪条新） */
.card.lognote.fresh { border-left: 3px solid var(--brand); }

/* ── 封板对照的两列表 ── */
.difftable { border: 1px solid var(--line); border-radius: var(--r); overflow: hidden; }
.diffhead, .diffrow {
  display: grid; grid-template-columns: minmax(0, 1.3fr) minmax(0, 2fr);
  gap: 10px; padding: 10px 14px; align-items: baseline;
}
.diffvals { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; align-items: baseline; }
/* 宽屏有表头两列说明谁是旧谁是新，箭头是多余的一笔；窄屏没表头才要它 */
.diffrow .arrow, .diffhead .diffvals > span:nth-child(2) { display: none; }
.diffhead { background: var(--s2); color: var(--fg2); font-size: var(--f-xs); letter-spacing: .04em; }
.diffrow { border-top: 1px solid var(--line-soft); }
.diffrow .k { color: var(--fg2); font-size: var(--f-sm); min-width: 0; overflow-wrap: anywhere; }
.diffrow .v { font-size: var(--f-sm); overflow-wrap: anywhere; }
/* 旧值退到次要色就够了 —— 不用删除线：删除线压在数字上，18 会看成 1B */
.diffrow .was { color: var(--fg2); }
.diffrow .arrow { color: var(--fg2); }
.diffrow .is { color: var(--brand); }
@media (max-width: 560px) {
  /* 窄屏：标签一行，「旧 → 新」挤在下面一行，不许横滚 */
  .diffhead { display: none; }
  .diffrow { grid-template-columns: 1fr; gap: 4px; }
  /* 值当成一整块来折行：三列网格会让长值在自己格子里折断，
     箭头就卡在第一行中间 ——「三聚氰胺板（走 J／C → 贴面板…型材）」根本读不通。
     改成行内块之后，长值整块换行，箭头永远跟在整块后面 */
  .diffvals { display: block; }
  .diffvals > .v { display: inline-block; max-width: 100%; vertical-align: top; }
  .diffrow .arrow { display: inline; margin: 0 4px; }
  .diffhead .diffvals > span:nth-child(2) { display: none; }
}

/* ── 帮助页 ── */
.card.helphead { text-align: left; }
.card.helpstep { display: flex; gap: 16px; align-items: center; }
.helpart { flex: 0 0 auto; width: 140px; }
.helpart svg { width: 100%; height: auto; display: block; }
.helpstep-t { font-size: var(--f-md); font-weight: 650; margin-bottom: 6px; }
.helpstep-b { margin: 0; color: var(--fg2); font-size: var(--f-sm); line-height: 1.6; }
@media (max-width: 560px) {
  .card.helpstep { flex-direction: column; align-items: flex-start; gap: 10px; }
  .helpart { width: 100%; max-width: 240px; }
}

.faq { border: 1px solid var(--line); border-radius: var(--r); background: var(--s1); overflow: hidden; }
.faq-q {
  width: 100%; min-height: var(--tap); padding: 12px 16px;
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  background: none; border: 0; color: var(--fg); font: inherit; font-weight: 550;
  text-align: left; cursor: pointer;
}
.faq-q:hover { background: var(--s2); }
.faq-q:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
.faq-q .chev { color: var(--fg3); transition: transform var(--t) var(--ease); }
.faq-q.on .chev { transform: rotate(90deg); color: var(--brand); }
.faq-a { margin: 0; padding: 0 16px 16px; color: var(--fg2); font-size: var(--f-sm); line-height: 1.65; }

/* ── 第一次进来的三步卡 ── */
.card.firstrun { border-color: var(--brand-dim); background: var(--brand-wash); margin-bottom: 16px; }
.firstrun-t { font-size: var(--f-lg); font-weight: 650; margin-bottom: 6px; }
.firstrun-b { margin: 0 0 14px; color: var(--fg2); font-size: var(--f-sm); line-height: 1.6; }

/* ── 隐私政策 / 服务条款 ── */
.legal { max-width: 720px; margin: 0 auto; padding-bottom: var(--sp-7); }
.legal h1 { font-size: var(--f-xl); font-weight: 650; margin: 0 0 4px; }
.legal h2 {
  font-size: var(--f-md); font-weight: 650; color: var(--fg);
  margin: var(--sp-6) 0 var(--sp-2); padding-top: var(--sp-4);
  border-top: 1px solid var(--line);
}
.legal p { margin: 0 0 var(--sp-3); color: var(--fg2); font-size: var(--f-sm); line-height: 1.75; }
.legal-meta { color: var(--fg2); font-size: var(--f-xs); margin-bottom: var(--sp-5); }
.tofill {
  padding: 16px; border: 1px dashed var(--warn); border-radius: var(--r);
  background: var(--warn-wash); display: grid; gap: 8px; justify-items: start;
}

@media (prefers-reduced-motion: reduce) { .faq-q .chev { transition: none; } }

/* ⚠️ 全局兜底：**类选择器的 display 会盖过浏览器给 [hidden] 的 display:none**。
 * 2026-09-03 踩过：底部 Tab 的红点写了 display:grid，于是 el.hidden = true
 * 之后它照样挂在那儿显示一个「0」—— 屏幕上是个假红点，代码里一切正常。
 * 与其每加一个组件都记得补一条，不如在这里一次说死。 */
[hidden] { display: none !important; }

/* ── 正立面 / 侧剖（⑰，app/js/elev.js）────────────────────────
   和俯视图同一套语汇：线用 --fg3、标注用品牌青、数字走等宽。
   板本身的填色由 JS 按 BOARD_COLOR 给（和三维同一份），CSS 只管线与字。 */
.elev { display: block; width: 100%; height: auto; }
.ev-floor { stroke: var(--fg3); stroke-width: 3; stroke-linecap: square; }
.ev-ceil { stroke: var(--fg3); stroke-width: 1.4; stroke-dasharray: 6 5; opacity: .7; }
.ev-opening { fill: #e3b34122; stroke: var(--warn); stroke-width: 1.6; stroke-dasharray: 5 4; }
.ev-opening.window { opacity: .7; }
.ev-b { stroke: var(--scrim-soft); stroke-width: .6; }
.ev-b.on { stroke: var(--brand); stroke-width: 1.6; }
.ev-b.cut { stroke: #00000080; stroke-width: 1; }
.ev-dimline { stroke: var(--brand); stroke-width: 1.1; }
.ev-dimtick { stroke: var(--brand); stroke-width: 1.1; }
.ev-lead { stroke: var(--brand); stroke-width: .8; opacity: .55; }
.ev-dimlabel { fill: var(--brand); font-family: var(--font-num); font-size: 11px; }
.ev-chainlabel { fill: var(--fg2); font-size: 11px; }
.ev-scale { stroke: var(--fg3); stroke-width: 1.2; }
.ev-scalelabel { fill: var(--fg2); font-family: var(--font-num); font-size: 10px; }
.ev-note { fill: var(--fg2); font-size: 11px; }
/* 画不出来时那一句实话 —— 用正文色，不用占位色（规则 246：--fg3 不许写正文） */
.ev-cannot { fill: var(--fg2); font-size: 13px; }

/* 拖柜深 / 拖柜高的手柄（⑱）—— 和俯视图那一套同一个语汇，只是光标换轴。
   `.locked` 是通到天花的柜：一个**实心小点**，没有把手的样子、也不给 grab 光标 ——
   动不了的东西不许装成能动（◇ 设计标准 C）。 */
.ev-hline { stroke: var(--brand-dim); stroke-width: 1.2; stroke-dasharray: 3 3; }
.ev-grip {
  fill: var(--s2); stroke: var(--brand); stroke-width: 2; touch-action: none;
}
/* ㊿ 规则 337：手机上的隐形靶（直径 44，看不见）+ 上面那个看得见的小点（不吃指针） */
.ev-grip.ev-hit { fill: transparent; stroke: transparent; }
/* 按住时隐形靶只淡淡地亮一圈，别变成一个 44 的实心大饼（他嫌大的正是那个） */
.ev-grip.ev-hit:hover, .ev-grip.ev-hit:active { fill: var(--brand-wash); }
.ev-grip.ev-hit:focus-visible { stroke: var(--fg); stroke-width: 2; }
.ev-dot { fill: var(--brand); stroke: var(--s1); stroke-width: 2; pointer-events: none; }
/* 模型底下那条快捷条：点中就冒出来（这一端怎么收 / 这一格是什么 / 太窄太宽的提示）—— 只有它自己吃指针 */
.im-quick {
  margin-top: var(--sp-2);
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
  padding: var(--sp-2) var(--sp-3); border-radius: var(--r-sm);
  background: var(--s1); border: 1px solid var(--line);
}
.im-quick .chips { gap: 4px; }
.im-quick .chip { min-height: 32px; }
.im-quick-lbl { font-size: var(--f-sm); color: var(--fg2); white-space: nowrap; }
.im-quick-warn { flex-basis: 100%; gap: var(--sp-2); flex-wrap: wrap; }
.im-quick-warn .why { font-size: var(--f-sm); color: var(--warn); flex: 1 1 200px; }
@media (max-width: 899px) { .im-quick .chip { min-height: var(--tap); min-width: var(--tap); justify-content: center; } }
.ev-grip.d { cursor: ew-resize; }
.ev-grip.h { cursor: ns-resize; }
/* ㉞ 拖吊柜底离地 —— 和拖柜高同一条轴，所以同一个光标；杆往下伸把两个手柄分开 */
.ev-grip.z { cursor: ns-resize; }
/* 同一面墙上别的段里的柜：只带轮廓当背景，不填色 —— 它不是这一屏在改的东西 */
rect.ev-nb { fill: none; stroke: var(--line); stroke-width: 1; stroke-dasharray: 4 4; }
.ev-grip:hover { fill: var(--s3); }
.ev-grip:active { fill: var(--brand); }
.ev-grip:focus-visible { outline: none; stroke: var(--fg); stroke-width: 3; }
.ev-grip.locked { fill: var(--fg3); stroke: var(--fg3); stroke-width: 1; cursor: not-allowed; }
/* ⑲ 剖切线：制图惯例的细虚线 + 两端箭头 + 一个记号。
 * 不引新颜色（◇A）：它是一条「看图的线」，走次要文字色，
 * 不抢品牌青（那一支给尺寸与吸附用，抢了就分不出谁是谁）。 */
.ev-cutline { stroke: var(--fg2); stroke-width: 1.1; stroke-dasharray: 9 5 2 5; }
.ev-cutarrow { fill: var(--fg2); stroke: none; }
.ev-cutlabel { fill: var(--fg); font-family: var(--font-num); font-size: 12px; font-weight: 600; }
.ev-guideline { stroke: var(--brand); stroke-width: 1.6; stroke-dasharray: 6 4; }
.ev-guidelabel { fill: var(--brand); font-family: var(--font-num); font-size: 12px; }

/* ══ 自己的选择框（51 · ui.js 的 choice / openChoice）══════
 *
 * 业主 2026-09-14：「栏目里面的设置选择框太简陋了，我要属于自己 APP 的选择框」。
 * 原生 <select> 在 Windows 上是系统灰底 + 系统箭头，和这一整套深色界面两张皮；
 * 这一套只用 token，深浅两套都跟着走。
 */
.ch {
  display: flex; align-items: center; gap: var(--sp-2);
  min-height: var(--tap); border: 1px solid var(--line); border-radius: var(--r);
  background: var(--s2); color: var(--fg); cursor: pointer;
  font: inherit; font-size: var(--f-md); text-align: left;
}
.ch:hover:not(:disabled) { background: var(--s3); }
.ch:disabled { opacity: .55; cursor: default; }
.ch > .ic { flex: 0 0 auto; color: var(--fg3); }
.ch-v { color: var(--fg); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* 整行：名字（+ 一句说明）…… 当前值 ›。摆在「我的」那种一列设置里 */
.ch-row { width: 100%; padding: var(--sp-2) var(--sp-3); }
.ch-row .ch-k { flex: 1; min-width: 0; display: grid; gap: 2px; }
.ch-row .ch-hint { font-size: var(--f-xs); color: var(--fg2); }
.ch-row .ch-v { flex: 0 0 auto; max-width: 45%; color: var(--fg2); font-size: var(--f-sm); }

/* 一格：摆在已经有 <label> 的 .field 里，宽度吃满 */
.ch-field { width: 100%; padding: 0 var(--sp-3); }
.ch-field .ch-v { flex: 1; min-width: 0; }

/* 一颗：摆在筛选条那种一排小控件里。高度让给那一排，别把行撑高 */
.ch-compact { min-height: var(--tap); padding: 0 var(--sp-2) 0 var(--sp-3); font-size: var(--f-sm); }
@media (min-width: 900px) { .ch-compact { min-height: 38px; } }
.ch-compact .ch-v { max-width: 12em; }

/**
 * 弹层。手机从底部升起一张小条，电脑就地弹一张卡。
 * z-index 要压过抽屉（.dw 是 90）—— 在抽屉里点开的选择框被抽屉盖住的话，
 * 屏幕上就是「点了没反应」（◇ 设计标准 C 最忌讳的那一种）。
 */
.ch-back { position: fixed; inset: 0; z-index: 100; display: flex; }
.ch-pop {
  background: var(--s1); border: 1px solid var(--line);
  box-shadow: var(--shadow); overflow: hidden;
  display: flex; flex-direction: column;
}
.ch-pop.sheet {
  margin-top: auto; width: 100%; max-height: 72vh;
  border-radius: var(--r-lg) var(--r-lg) 0 0; border-bottom: 0;
  padding-bottom: var(--safe-b);
  animation: chUp var(--t) var(--ease);
}
.ch-back:has(.sheet) { background: var(--scrim); }
@keyframes chUp { from { transform: translateY(100%); } to { transform: none; } }
.ch-pop.wide {
  position: fixed; min-width: 220px; max-width: min(340px, 92vw); max-height: 60vh;
  border-radius: var(--r); animation: chIn var(--t-fast) var(--ease);
}
@keyframes chIn { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .ch-pop { animation: none; } }

.ch-pop-head {
  flex: 0 0 auto; display: grid; gap: 2px;
  padding: var(--sp-3) var(--sp-4) var(--sp-2);
  border-bottom: 1px solid var(--line-soft);
}
.ch-pop-k { font-size: var(--f-sm); font-weight: 650; }
.ch-pop-hint { font-size: var(--f-xs); color: var(--fg2); }

.ch-list { flex: 1; min-height: 0; overflow-y: auto; padding: var(--sp-2); }
.ch-opt {
  display: flex; align-items: center; gap: var(--sp-3); width: 100%;
  min-height: var(--tap); padding: var(--sp-2) var(--sp-3);
  border: 0; border-radius: var(--r-sm); background: none; color: var(--fg2);
  text-align: left; cursor: pointer; font: inherit; font-size: var(--f-md);
}
.ch-opt:hover, .ch-opt:focus-visible { background: var(--s2); color: var(--fg); }
@media (min-width: 900px) { .ch-opt { min-height: 36px; } }
.ch-opt-t { flex: 1; min-width: 0; display: grid; gap: 2px; }
.ch-opt-k { overflow: hidden; text-overflow: ellipsis; }
.ch-opt-h { font-size: var(--f-xs); color: var(--fg2); white-space: normal; }
/* 选中的那个：青字 + 右边一个青勾（全 App 的勾都是 ICON.check，⑲）*/
.ch-opt[aria-selected="true"] { color: var(--brand); font-weight: 600; }
.ch-opt[aria-selected="true"] svg { color: var(--brand); }

/**
 * 摆进 `.rows` 那一摞里时，交出自己的边框和底色。
 * 那一摞本身有一圈边、一个圆角和一条一条的分隔线 —— 不交的话屏幕上是
 * 「一条框里套着一条框」，而且底色比同摞的别的行亮一档（实测「我的」那一屏就是这样）。
 * 分隔线那条规则写在后面：同权重、后写的赢，否则 `border: 0` 会把它一起抹掉。
 */
.rows > .ch {
  border: 0; border-radius: 0; background: none;
  min-height: 52px; padding: var(--sp-2) var(--sp-4);
}
.rows > * + .ch { border-top: 1px solid var(--line-soft); }
.rows > .ch:hover:not(:disabled) { background: var(--s2); }

/* ══ 51 底图那一层（设计稿 3a · model3d.js 的 paintPhoto）══════
 *
 * 模型屏最远那一层：照片铺满舞台，上面框柜子。
 * 布局吃的是模型屏原来那一套（.m3-wrap / .m3-main / .m3-panel）——
 * 手机上整屏不滚、面板贴底那几条规矩一条都不用重写。
 */
.m3-photo .m3-main {
  display: flex; flex-direction: column; min-height: 0; gap: var(--sp-2);
  padding: var(--sp-2);
}
/**
 * 照片这一块吃掉剩下的全部高度，图自己居中。
 * ⚠️ `.mk-stage` 在框柜子那一屏是 `width: fit-content` + `max-height: 60vh`
 *    （那一屏是长页，图不许把结论挤出屏幕）。这一层不是长页，是**整屏不滚**：
 *    所以这里把高度交给剩下的空间，`.mk-img` 跟着这一格走。
 */
.m3-photowrap {
  position: relative; flex: 1; min-height: 0;
  display: flex; align-items: center; justify-content: center;
}
.m3-photowrap > .mk-stage { max-height: 100%; }
.m3-photowrap .mk-img { max-height: 100%; max-width: 100%; }

/**
 * 「叠着比」：3D 那张 canvas 半透明压在照片上。
 *
 * canvas 本来就是透明底（cabinet3d 的 `clearColor(0,0,0,0)`），直接压就行。
 * ⚠️ **不接指针**：接了的话它会把拖框整个吃掉 —— 人在照片上画不出框，
 *    而屏幕上看不出是谁挡的（◇ 设计标准 C 最忌讳的「点了没反应」）。
 * ⚠️ 工具条和拖动手柄也一起藏掉：它们是能点的东西，压在一层点不动的膜上，
 *    等于摆了一排点了没反应的按钮。
 */
.m3-ghost {
  position: absolute; inset: 0; pointer-events: none; opacity: .55;
}
.m3-ghost > .stage { width: 100%; height: 100%; }
.m3-ghost .stage-tools,
.m3-ghost .im-grips,
.m3-ghost .stage-top { display: none; }

/* 「只看照片 / 叠着比 / 回模型」+ 哪一张。贴在照片底下，不跟着照片缩 */
.m3-phbar {
  flex: 0 0 auto; display: flex; align-items: center; gap: var(--sp-3);
  flex-wrap: wrap;
}
.m3-phbar .chips { flex: 1 1 auto; margin: 0; }
/* 「一个数字都不许从图上读」—— 和框柜子那一屏逐字同一句，字号收一档，不抢照片 */
.m3-phnote { flex: 0 0 auto; margin: 0; font-size: var(--f-sm); }
.m3-phpanel { display: grid; gap: var(--sp-3); }

/* ══ 51 整排那一层（设计稿 3b · model3d.js 的 paintRun）══════
 *
 * 一面墙上几个柜并排 —— 画的是 elev.js 的正立面（和柜段那一屏同一份）。
 * 布局照旧吃模型屏那一套（.m3-wrap / .m3-main / .m3-panel）。
 */
.m3-run .m3-main {
  display: flex; flex-direction: column; min-height: 0; gap: var(--sp-2);
  padding: var(--sp-2);
}
/* 顶上那一行：哪一段 · 这面墙多长（读数）· 段总长 · 天花 */
.m3-runhead {
  flex: 0 0 auto; display: flex; align-items: center; gap: var(--sp-2);
  flex-wrap: wrap;
}
.m3-runhead .field { margin: 0; display: flex; align-items: center; gap: var(--sp-2); }
.m3-runhead .field > label { margin: 0; white-space: nowrap; }
.m3-runhead .numin { width: 7em; min-height: 38px; }
/* 图吃掉剩下的全部高度。SVG 自己按实测的宽高重画（drawRun 里那个 ResizeObserver）*/
.m3-runstage { flex: 1; min-height: 0; display: flex; }
.m3-runstage > svg { width: 100%; height: 100%; }

/**
 * 撞了的柜标红。**判据在引擎 `clashPairs()`**，这里只负责让人一眼看见。
 * 用描边不用填充：填掉的话板自己的暖木色没了，人会以为那是另一种板。
 */
.ev-b.clash { stroke: var(--danger); stroke-width: 2.5; }
.m3-clash { border: 1px solid var(--danger-line); background: var(--danger-wash); border-radius: var(--r); padding: var(--sp-3); }
.m3-clash ul { margin: var(--sp-2) 0 0; padding-left: 1.1em; }
.m3-clash li { font-size: var(--f-sm); color: var(--fg2); margin-top: 2px; }

/* 梯子那一排（照片 ← 整排 ← 单柜）—— 三层共用一份 zoomBar */
.m3-ladder { margin: 0; flex: 0 0 auto; }

/* ══ 51 电脑版四件套（设计稿 7c / 7d）════════════════════════
 *
 * 四样都**只在 ≥900 出现**。手机上原样摆一遍的后果是：400 宽的屏上模型
 * 本来就只剩两百来像素，再切 200 给坞、再切一栏给树，模型就没了。
 */

/* ── ① 左栏板件树：住在外壳左栏里，页签底下 ── */
.rail-extra { display: none; }
@media (min-width: 900px) {
  .rail-extra { display: block; min-height: 0; overflow-y: auto; margin-top: var(--sp-4); }
  .tabbar:has(.m3-tree) { overflow-y: auto; }
}
.m3-tree { border-top: 1px solid var(--line-soft); padding-top: var(--sp-3); }
.tr-head {
  padding: 0 var(--sp-2) var(--sp-2);
  font-size: var(--f-xs); color: var(--fg2); letter-spacing: .08em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.tr-row {
  display: flex; align-items: center; gap: 4px; width: 100%;
  min-height: 26px; padding: 0 var(--sp-2); border: 0; border-radius: var(--r-sm);
  background: none; color: var(--fg2); text-align: left; cursor: pointer;
  font: inherit; font-size: var(--f-sm);
}
.tr-row:hover { background: var(--s2); color: var(--fg); }
/* 双向高亮：点树 = 点模型，选中的那一条和模型上那块板是同一个 */
.tr-row.on { background: var(--brand-wash); color: var(--brand); font-weight: 600; }
.tr-caret { flex: 0 0 12px; color: var(--fg2); font-size: 10px; }
.tr-k { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tr-n { flex: 0 0 auto; color: var(--fg2); font-size: var(--f-xs); }

/* ── ② 底部坞：模型底下 200 高，四个页签，可收 ── */
.m3-dock { display: none; }
@media (min-width: 900px) {
  .m3-dock {
    display: flex; flex-direction: column; flex: 0 0 auto;
    border-top: 1px solid var(--line); background: var(--s1);
  }
  /* 展开时正好 200（含那条页签栏）；收起来只剩页签栏，模型长回那 200px */
  .m3-dock[data-open="1"] { height: 200px; }
}
.dk-bar {
  flex: 0 0 auto; display: flex; align-items: center; gap: var(--sp-2);
  padding: 0 var(--sp-3); border-bottom: 1px solid var(--line-soft);
}
.dk-tabs { display: flex; gap: 2px; flex: 1; min-width: 0; }
.dk-tab {
  min-height: 34px; padding: 0 var(--sp-3); border: 0; border-bottom: 2px solid transparent;
  background: none; color: var(--fg3); cursor: pointer; font: inherit; font-size: var(--f-sm);
}
.dk-tab:hover { color: var(--fg2); }
.dk-tab[aria-selected="true"] { color: var(--brand); border-bottom-color: var(--brand); font-weight: 650; }
.dk-toggle { flex: 0 0 auto; }
.dk-body { flex: 1; min-height: 0; overflow-y: auto; }
.dk-table { display: grid; }
.dk-row {
  display: flex; align-items: center; gap: var(--sp-3); width: 100%;
  min-height: 30px; padding: 0 var(--sp-3); border: 0; border-bottom: 1px solid var(--line-soft);
  background: none; color: var(--fg2); text-align: left; font: inherit; font-size: var(--f-sm);
}
button.dk-row { cursor: pointer; }
button.dk-row:hover { background: var(--s2); color: var(--fg); }
/* 点表里一行，模型上那块板亮 —— 亮的是同一处选中，所以这一行也跟着亮 */
.dk-row.on { background: var(--brand-wash); color: var(--brand); }
.dk-id { flex: 0 0 7em; color: var(--fg2); font-size: var(--f-xs); }
.dk-k { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.dk-v { flex: 0 0 auto; color: var(--fg2); }
.dk-chk .dk-v { flex: 2 1 0; min-width: 0; white-space: normal; }
.dk-mark { flex: 0 0 auto; }
.dk-pills { display: flex; flex-wrap: wrap; gap: var(--sp-2); padding: var(--sp-3); align-items: center; }
.dk-empty { padding: var(--sp-4); color: var(--fg2); font-size: var(--f-sm); }

/* ── ④ 把图拖进窗口：模型区罩一层青虚线 ── */
.m3-droplay {
  display: none; position: absolute; inset: 0; z-index: 5;
  place-items: center; align-content: center; gap: var(--sp-2);
  border: 2px dashed var(--brand); border-radius: var(--r);
  background: var(--brand-wash); color: var(--brand);
  font-size: var(--f-md); font-weight: 600;
  pointer-events: none;              /* 罩子不许把 drop 事件自己吃掉 */
}
.m3-main { position: relative; }
.m3-dropping .m3-droplay { display: grid; }

/* ══ 规则 338 那一轮（业主 2026-09-14 上午）：手机上卡浮在板旁边 · 撤销在舞台上 ══════ */

/**
 * 撤销 / 重做 贴舞台左下角 —— 宽窄屏同一个位置。
 * 以前它们在面板顶上，手机上面板收成一行时根本不在屏幕上，拖坏了一个尺寸没处退。
 */
.stage-undo {
  position: absolute; left: var(--sp-3); bottom: var(--sp-3);
  display: flex; gap: var(--sp-2); z-index: 4;
}
.stage-undo .tool-btn:disabled { opacity: .35; cursor: default; transform: none; }

/**
 * 手机上那张浮卡：点中哪块板就浮在它旁边（位置由 model3d.js 的 placePop 按板的投影摆）。
 * 宽不超过舞台的 72%、高不超过舞台的 70%，自己滚；模型整屏都在，卡不占一行。
 * 电脑上不用它（右栏本来就在旁边）—— `isDesk()` 那一份判据说了算，这里只管长相。
 */
.m3-pop {
  position: absolute; z-index: 6;
  width: min(280px, 68%); max-height: 62%; overflow-y: auto; overscroll-behavior: contain;
  background: var(--glass); backdrop-filter: blur(8px);
  border: 1px solid var(--brand-line); border-radius: var(--r-lg); box-shadow: var(--shadow);
  padding: var(--sp-3);
}
.m3-pop .prop { border: 0; background: none; padding: 0; }
.m3-pop-x { position: absolute; top: 4px; right: 4px; z-index: 1; }
.m3-pop .prop-head { padding-right: 36px; }
/* 卡在舞台里，模型上的圆点仍然要按得中 —— 卡不吃它外面的指针（本来就是 absolute 小块，这里只写明） */
.m3-pop:empty { display: none; }

/* 规则 338：卡最上面那一行「这扇门现在多宽 + 填」 */
.im-doorw { gap: var(--sp-2); margin-bottom: var(--sp-3); flex-wrap: wrap; }
.im-doorw-hint { flex-basis: 100%; }

/**
 * 手机横过来（业主 2026-09-14 上午：「电话打横放，为什么页面模型会变得那么小，模型是主要的」）。
 * 横屏只有 390 高：底下那个面板哪怕半屏也把模型压成一条。横屏时面板一律只留一行，
 * 「看的方式」那一组要看就把它拉到整屏那一档（那一档照旧是 72dvh，拉上来看完再收回去）。
 */
@media (max-width: 899px) and (orientation: landscape) {
  /* 横屏只剩 400 高：顶栏 52 + 底栏 56 已经吃掉 108，面板一行也只留一条把手（36），模型拿剩下的全部 */
  .m3-panel[data-h="peek"], .m3-panel[data-h="half"] { max-height: 44px; overflow: hidden; padding-bottom: 0; }
  .m3-pop { width: min(300px, 46%); max-height: 88%; }
}
/**
 * 规则 341（业主 2026-09-15）：**打横的平板 / 手机也归这一套**，不管屏多宽。
 * 他那台打横 1280 宽落在电脑版那边，模型只占屏 29%（右栏 380 + 坞 200 吃掉的）；
 * iPad 横屏 1180×820 更差，只有 34%。判据是「打横 + 手指」——
 * 鼠标那边（pointer: fine）一个像素都不动，所以这一段必须排在 900 那一段**后面**才盖得住它。
 */
@media (orientation: landscape) and (pointer: coarse) {
  /* 右栏让位：整行都给模型，属性卡改成浮在板旁边（JS 那边 wideUI() 同一条判据） */
  .m3-wrap { grid-template-columns: minmax(0, 1fr); }
  .m3-panel { border-left: 0; padding: 0; background: none; }
  .m3-panel[data-h="peek"], .m3-panel[data-h="half"] { max-height: 44px; overflow: hidden; padding-bottom: 0; }
  /* 把手是给手指的 —— 900 那一段把它藏了，这里要回来 */
  .m3-grip { display: block; }
  /* 平板横屏比手机宽，卡可以宽一点，但照旧不许盖过四成 */
  .m3-pop { width: min(360px, 40%); max-height: 88%; }
}
/**
 * 圆点永远压在浮卡上面（规则 338 乙 + 336）：400 宽的手机上卡总会盖住模型的一部分，
 * 盖住的那块正好有手柄时，手柄照样要按得中 —— 「圆点在模型上，人要的是当场拖」。
 * 手柄那一层本来 pointer-events:none，只有圆点自己吃指针，所以压在卡上不影响卡的按钮。
 */
.im-grips { z-index: 7; }

/* 浮卡顶上那条把手（拖它挪卡）+ 手指按在模型上时卡淡下去（规则 338 乙，业主 2026-09-14 追加） */
.m3-pop-grab {
  height: 22px; margin: -6px -6px 4px; border-radius: var(--r) var(--r) 0 0;
  cursor: grab; touch-action: none; position: relative;
}
.m3-pop-grab::before {
  content: ''; position: absolute; left: 50%; top: 9px; transform: translateX(-50%);
  width: 36px; height: 4px; border-radius: 999px; background: var(--fg3);
}
.m3-pop-grab:active { cursor: grabbing; }
.m3-pop { transition: opacity var(--t-fast) var(--ease); }
.m3-pop.dim { opacity: .22; pointer-events: none; }
.m3-pop-x { top: 18px; }
/* 收成圆点那一颗在 ✕ 左边（两颗都在把手那一行下面）。
   箭头转成朝下 = 收起（图标表里没有「减号」，转一下 chev 比新画一个稳） */
.m3-pop-min { position: absolute; top: 18px; right: 38px; z-index: 1; }
.m3-pop-min svg { transform: rotate(90deg); }

/**
 * ══ 规则 342：浮卡收成一颗圆点 ═══════════════════════════════
 * 业主 2026-09-15：「那个浮窗帮我做成可以像 iPhone 那个圆圈那样，平时可以把它缩小成
 * 一个圆圈，要用的时候点开来调整数值，圆圈要做得精致」。
 *
 * ⚠️ 收着的时候整颗都能拖（像 iPhone 那颗悬浮球），所以 `touch-action: none` ——
 *    不写的话手指一按就被当成页面滚动，球拖不动。
 * ⚠️ 56 不是随手定的：手指最小 44（◇ 设计标准 B#14），球比按钮再大一圈才好按到，
 *    而且大过 64 就开始挡模型了。
 */
.m3-pop.ball {
  width: 56px; height: 56px; min-height: 56px; max-height: 56px;
  padding: 0; overflow: visible; border: 0; background: none;
  box-shadow: none; backdrop-filter: none;
}
.m3-ball {
  width: 56px; height: 56px; border-radius: 999px; padding: 0;
  display: grid; place-items: center; position: relative;
  cursor: grab; touch-action: none; color: var(--brand);
  /**
   * 玻璃球：左上打一道光 + 一圈发丝亮边 + 一层落影。
   * 三层叠起来才有「球」的感觉 —— 只铺一层平的 --glass 是一枚贴纸，不是球
   * （他要的是「圆圈要做得精致」）。亮色深色两边都是同一套，只靠 --glass 自己换底。
   */
  background:
    radial-gradient(118% 118% at 30% 22%, rgb(255 255 255 / .22), transparent 60%),
    var(--glass-thin);
  /* 底越透，越要靠模糊把底下的模型糊成一片底色 —— 不然球里花花的，反而脏 */
  backdrop-filter: blur(16px) saturate(1.5);
  border: 1px solid var(--brand-line);
  box-shadow:
    0 4px 14px rgb(0 0 0 / .14),
    0 1px 2px rgb(0 0 0 / .08),
    inset 0 1px 0 rgb(255 255 255 / .26);
  transition: transform var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
}
.m3-ball:active { cursor: grabbing; transform: scale(.94); }
@media (hover: hover) { .m3-ball:hover { box-shadow: var(--shadow-lg, var(--shadow)); } }
/* 扣着一块板的小记号 —— 告诉他这不是一颗空按钮 */
.m3-ball-dot {
  position: absolute; top: 10px; right: 10px; width: 6px; height: 6px;
  border-radius: 999px; background: var(--brand);
  /* 一圈底色把它从球面上「托」起来，再加一点点辉 —— 小而看得见，不抢那个图标 */
  box-shadow: 0 0 0 2px var(--glass-thin), 0 0 7px var(--brand);
}
/* 换了一块板还收着：闪一圈，让他看见球跟着新的板走了 */
@keyframes m3-ball-pulse {
  0%   { box-shadow: 0 0 0 0 var(--brand-line); }
  70%  { box-shadow: 0 0 0 12px transparent; }
  100% { box-shadow: 0 0 0 0 transparent; }
}
.m3-pop.ball.pulse .m3-ball::after {
  content: ''; position: absolute; inset: -1px; border-radius: 999px;
  animation: m3-ball-pulse .6s var(--ease) 1;
}
@media (prefers-reduced-motion: reduce) {
  .m3-pop.ball.pulse .m3-ball::after { animation: none; }
  .m3-ball { transition: none; }
}

/* 工具条现在十颗：手机横屏只有 256 高的舞台竖着放不下 —— 横过来排两行，贴右上角 */
@media (max-width: 899px) and (orientation: landscape) {
  .stage-tools { flex-direction: row; flex-wrap: wrap; justify-content: flex-end; max-width: 62%; }
}

/**
 * ══ 规则 344：测试版的「报问题」 ══════════════════════════════
 * 业主 2026-09-15：「工作人员在手机上测试，不止一个人，反馈的东西要可以让你知道」。
 * 弹框本身走 <dialog> 那一套（和别处同一份），这里只管里面那三样。
 */
.fb-text {
  width: 100%; box-sizing: border-box; resize: vertical; min-height: 96px;
  padding: var(--sp-3); border: 1px solid var(--line); border-radius: var(--r);
  background: var(--s1); color: var(--fg); font: inherit; font-size: var(--f-md);
}
.fb-text:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
.fb-text[aria-invalid="true"] { border-color: var(--danger); }
/* 「会一起发过去什么」那一句：小字，但**不许小到看不见** —— 它是知情那一句 */
.fb-auto {
  margin-top: var(--sp-3); color: var(--fg2); font-size: var(--f-sm); line-height: 1.5;
}

/**
 * ══ 规则 345：图取不回来时那一句 ═════════════════════════════
 * 裂开的框什么都不说。这一句要**看得见**（它是唯一的线索），但也别喧宾夺主。
 */
.imgfail {
  display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap;
  margin: var(--sp-2) 0; padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--warn-line); border-radius: var(--r);
  background: var(--warn-wash); color: var(--fg);
  font-size: var(--f-sm); line-height: 1.5;
}
.imgfail-why { flex: 1 1 12em; min-width: 0; }
/* 码永远印出来 —— 说人话那一句可能不准，码是事实 */
.imgfail-code { flex: 0 0 auto; color: var(--fg2); font-size: var(--f-xs); }
