/* AI Product Manager —— 设计系统。
 *
 * 三条铁律，改这个文件时不要破坏：
 *   1. 只有 8 的倍数：8 / 16 / 24 / 32 / 48 / 64。没有 9、11、13、22。
 *   2. 只有四级字号：--f-xs 12 / --f-sm 14 / --f-md 17 / --f-lg 24。层级靠字重和颜色，不靠加字号。
 *   3. 组件写在这里，不写进 app.js 的 style 属性。旧版 154 处内联样式就是「对不齐」的根因。
 *
 * 颜色分两套语义，永不混用：
 *   --pri 蓝  = 可点（链接、主按钮、当前项）
 *   --ok/--drv/--wrn/--bad = 状态（已确认 / AI 推断 / 待处理 / 阻断）
 * 全部文本对比度 ≥ 4.5:1（WCAG 2.1 AA），深浅两套都验过。
 */

/* ── 令牌 ───────────────────────────────────────────────────────────────── */
:root{
  /* 深色是默认。浅色要显式选，见 :root[data-theme="light"] */
  --bg:#10141B; --surf:#171C25; --sunk:#1D2430; --raise:#212936;
  --ink:#E3E8F0; --ink2:#98A3B4; --ink3:#848F9F;
  --line:#262E3B; --line2:#1F2733;
  --pri:#7FA8E8; --pri-soft:#1B2740; --on-pri:#10141B;
  --ok:#3DD68C;  --ok-bg:#12291F;
  --drv:#B18AFF; --drv-bg:#221A38;
  --wrn:#F0B429; --wrn-bg:#2B2313;
  --bad:#FF7B7B; --bad-bg:#2E1A1C;
  --shadow:0 1px 2px rgba(0,0,0,.4), 0 8px 28px -14px rgba(0,0,0,.8);
  --shadow-lg:0 24px 64px -16px rgba(0,0,0,.7);
  --scrim:rgba(4,6,10,.66);

  --s1:8px; --s2:16px; --s3:24px; --s4:32px; --s5:48px; --s6:64px;
  --r:8px; --rc:12px;
  --f-xs:12px; --f-sm:14px; --f-md:17px; --f-lg:24px;
  --side-w:248px; --head-h:72px;

  --sans:-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",system-ui,"Segoe UI",sans-serif;
  --mono:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;
}
:root[data-theme="light"]{
  --bg:#F5F7FA; --surf:#FFFFFF; --sunk:#EEF2F7; --raise:#FFFFFF;
  --ink:#1F2937; --ink2:#5B6676; --ink3:#5F6978;
  --line:#E2E7EE; --line2:#EDF1F6;
  --pri:#24487C; --pri-soft:#E7EDF7; --on-pri:#FFFFFF;
  --ok:#0E7C4A;  --ok-bg:#E4F4EC;
  --drv:#6D3FC4; --drv-bg:#EFE9FB;
  --wrn:#92610A; --wrn-bg:#FBF0DC;
  --bad:#B4232A; --bad-bg:#FBE7E8;
  --shadow:0 1px 2px rgba(20,24,31,.05), 0 8px 24px -12px rgba(20,24,31,.18);
  --shadow-lg:0 24px 64px -16px rgba(20,24,31,.24);
  --scrim:rgba(20,24,31,.44);
}

/* ── 基础 ───────────────────────────────────────────────────────────────── */
*{box-sizing:border-box}
html{color-scheme:dark}
:root[data-theme="light"] html,html:has(:root[data-theme="light"]){color-scheme:light}
body{
  margin:0; background:var(--bg); color:var(--ink);
  font-family:var(--sans); font-size:var(--f-sm); line-height:1.6;
  -webkit-font-smoothing:antialiased;
}
h1,h2,h3,h4,p,figure,dl,dd{margin:0}
ul,ol{margin:0; padding:0}
a{color:var(--pri); text-decoration:none}
a:hover{text-decoration:underline}
img{max-width:100%}
[hidden]{display:none !important}
:focus-visible{outline:2px solid var(--pri); outline-offset:2px; border-radius:4px}
@media (prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.01ms !important;
  animation-iteration-count:1 !important; transition-duration:.01ms !important}}

.mono{font-family:var(--mono); font-variant-numeric:tabular-nums}
.num{font-variant-numeric:tabular-nums}
.dim{color:var(--ink2)}
.dim3{color:var(--ink3)}
.xs{font-size:var(--f-xs)}
.h1{font-size:var(--f-lg); font-weight:600; letter-spacing:-.01em; line-height:1.3}
.h2{font-size:var(--f-md); font-weight:600; line-height:1.4}
.h3{font-size:var(--f-sm); font-weight:600}
.lbl{font-size:var(--f-xs); font-weight:600; letter-spacing:.08em; text-transform:uppercase; color:var(--ink3)}
.txt-ok{color:var(--ok)} .txt-drv{color:var(--drv)} .txt-wrn{color:var(--wrn)} .txt-bad{color:var(--bad)}
.stack{display:flex; flex-direction:column; gap:var(--s2)}
.stack-lg{display:flex; flex-direction:column; gap:var(--s4)}
.row{display:flex; align-items:center; gap:var(--s1)}
.row-wrap{display:flex; align-items:center; gap:var(--s1); flex-wrap:wrap}
.spacer{flex:1}
.ic{width:16px; height:16px; flex:none; display:block}
.ic-sm{width:13px; height:13px}
.ic-lg{width:20px; height:20px}
.scroll-x{overflow-x:auto}

/* ── 按钮 ── 费茨法则：主 CTA 48，不可逆 44，次级 40，行内可逆 32 ───────── */
.btn{
  display:inline-flex; align-items:center; justify-content:center; gap:var(--s1);
  height:48px; padding:0 var(--s3); border:1px solid transparent; border-radius:var(--r);
  font:inherit; font-size:var(--f-sm); font-weight:600; white-space:nowrap; cursor:pointer;
  background:var(--pri); color:var(--on-pri); transition:filter .12s, background .12s;
}
.btn:hover:not(:disabled){filter:brightness(1.08)}
.btn:active:not(:disabled){filter:brightness(.94)}
.btn:disabled{background:var(--sunk); color:var(--ink2); border-color:var(--line); cursor:not-allowed}
.btn.sec{background:var(--surf); color:var(--ink); border-color:var(--line); font-weight:400}
.btn.sec:hover:not(:disabled){background:var(--raise); border-color:var(--ink3)}
.btn.ghost{background:transparent; color:var(--ink2); border-color:transparent; font-weight:400}
.btn.ghost:hover:not(:disabled){background:var(--sunk); color:var(--ink)}
/* 点得动、但里面多半没东西的按钮（如没留档的「看提示词」）。
   不能用 :disabled——那样点不开，也就没法说清「为什么没有、怎么才有」。
   写成 .btn.dim 而不是复用 .dim：.btn.sec 那条也是 0,2,0，
   单个 .dim 压不过它，颜色根本不会变。 */
.btn.dim{color:var(--ink3)}
.btn.danger{background:var(--bad); color:var(--on-pri); height:44px}
.btn.danger.sec{background:var(--surf); color:var(--bad); border-color:var(--bad); font-weight:600}
.btn.md{height:40px; padding:0 var(--s2)}
.btn.xs{height:32px; padding:0 var(--s2); font-size:var(--f-xs); font-weight:400}
/* 不可逆操作（推进状态、批准、采纳）最小 44px —— 费茨法则在这类操作上不能反着来 */
.btn.key{height:44px; padding:0 var(--s3); font-size:var(--f-sm); font-weight:600}
.btn.block{width:100%}
.btn.busy{position:relative; color:transparent !important}
.btn.busy::after{
  content:""; position:absolute; width:16px; height:16px; border-radius:50%;
  border:2px solid currentColor; border-top-color:transparent; color:var(--on-pri);
  animation:spin .7s linear infinite;
}
.btn.sec.busy::after,.btn.ghost.busy::after{color:var(--ink2)}
@keyframes spin{to{transform:rotate(360deg)}}
/* 主 CTA 的点击热区：四周至少 16px 留白，由容器 gap 保证；这里兜底相邻按钮间距 */
.cta-row{display:flex; gap:var(--s2); flex-wrap:wrap; align-items:center}

/* ── 徽章 ───────────────────────────────────────────────────────────────── */
.pill{
  display:inline-flex; align-items:center; gap:6px; height:24px; padding:0 8px;
  border-radius:999px; font-size:var(--f-xs); white-space:nowrap;
  background:var(--sunk); color:var(--ink2);
}
.pill.ok{background:var(--ok-bg); color:var(--ok)}
.pill.drv{background:var(--drv-bg); color:var(--drv)}
.pill.wrn{background:var(--wrn-bg); color:var(--wrn)}
.pill.bad{background:var(--bad-bg); color:var(--bad)}
.pill.pri{background:var(--pri-soft); color:var(--pri)}
.pill .ic{width:12px; height:12px}
.kbd{
  font-family:var(--mono); font-size:var(--f-xs); padding:2px 6px; border-radius:4px;
  border:1px solid var(--line); color:var(--ink3); background:var(--surf);
}

/* ── 容器 ───────────────────────────────────────────────────────────────── */
.card{background:var(--surf); border:1px solid var(--line); border-radius:var(--rc); padding:var(--s3)}
.card.tight{padding:var(--s2) var(--s3)}
.note{
  background:var(--surf); border:1px solid var(--line); border-left:3px solid var(--ink3);
  border-radius:var(--rc); padding:var(--s3); display:flex; gap:var(--s2); align-items:flex-start;
}
.note.ok{border-left-color:var(--ok)}
.note.drv{border-left-color:var(--drv)}
.note.wrn{border-left-color:var(--wrn)}
.note.bad{border-left-color:var(--bad)}
.note > .ic{margin-top:2px}
.note .body{flex:1; min-width:0; display:flex; flex-direction:column; gap:var(--s1)}
.note .body > p{color:var(--ink2); line-height:1.65}
.note .body b{color:var(--ink); font-weight:600}
.note.ok > .ic{color:var(--ok)} .note.drv > .ic{color:var(--drv)}
.note.wrn > .ic{color:var(--wrn)} .note.bad > .ic{color:var(--bad)}

.sect{display:flex; flex-direction:column; gap:var(--s2)}
.sect-head{display:flex; align-items:baseline; gap:var(--s2)}
.sect-head .line{flex:1; height:1px; background:var(--line)}
.empty{
  border:1px dashed var(--line); border-radius:var(--rc); padding:var(--s5) var(--s3);
  text-align:center; color:var(--ink2); display:flex; flex-direction:column;
  align-items:center; gap:var(--s2);
}
.empty .ic{width:24px; height:24px; color:var(--ink3)}

/* 数据行 */
.rows{background:var(--surf); border:1px solid var(--line); border-radius:var(--rc); overflow:hidden}
.rows .r{
  display:flex; align-items:center; gap:var(--s2); padding:var(--s2) var(--s3);
  border-bottom:1px solid var(--line2); text-align:left; width:100%;
  background:none; border-left:0; border-right:0; border-top:0; font:inherit; color:inherit;
}
.rows .r:last-child{border-bottom:0}
.rows a.r,.rows button.r{cursor:pointer}
.rows a.r:hover,.rows button.r:hover{background:var(--sunk); text-decoration:none}
.rows .r .idx{font-size:var(--f-xs); color:var(--ink3); width:32px; flex:none; font-variant-numeric:tabular-nums}
.rows .r .nm{flex:1; min-width:0}
.rows .r .nm b{font-weight:600; display:block}
.rows .r .nm small{display:block; font-size:var(--f-xs); color:var(--ink3); margin-top:2px}
.rows .r .end{margin-left:auto; display:flex; align-items:center; gap:var(--s2); flex:none}
.bar{width:104px; height:6px; border-radius:3px; background:var(--sunk); overflow:hidden; flex:none}
.bar i{display:block; height:100%; background:var(--ok); border-radius:3px}
.bar i.drv{background:var(--drv)}
.bar i.pri{background:var(--pri)}

/* 统计 */
.stats{display:flex; gap:var(--s5); flex-wrap:wrap}
.stat .v{font-size:var(--f-md); font-weight:600; font-variant-numeric:tabular-nums; line-height:1.3}
.stat .k{font-size:var(--f-xs); color:var(--ink3); margin-top:2px}

/* 两栏「你只需决定 / AI 必须准备」 */
.duo{display:grid; grid-template-columns:1fr 1fr; gap:var(--s3);
  padding-top:var(--s3); border-top:1px solid var(--line2)}
@media (max-width:720px){.duo{grid-template-columns:1fr}}
.duo .lbl{display:block; margin-bottom:var(--s1)}
.duo p{font-size:var(--f-xs); color:var(--ink2); line-height:1.65}

/* ── 外壳：侧栏 + 主区 ──────────────────────────────────────────────────── */
.app{display:flex; min-height:100vh}
.side{
  width:var(--side-w); flex:none; background:var(--surf); border-right:1px solid var(--line);
  display:flex; flex-direction:column; padding:var(--s2);
  position:sticky; top:0; height:100vh; overflow-y:auto; z-index:20;
}
/* 这块牌子现在是「回首页」的按钮，所以要有按钮的样子（可聚焦、有 hover）。 */
.side .brand{display:flex; align-items:center; gap:var(--s1); width:100%;
  padding:var(--s1) var(--s2) var(--s2);
  font:inherit; font-weight:600; color:var(--ink); font-size:var(--f-sm);
  background:none; border:0; cursor:pointer; text-align:left}
.side .brand:hover{color:var(--pri)}
.side .brand .ic{color:var(--pri); flex:none}
.prodsw{
  display:flex; align-items:center; gap:var(--s1); width:100%; height:40px; padding:0 var(--s2);
  border:1px solid var(--line); border-radius:var(--r); background:var(--bg); color:var(--ink);
  font:inherit; font-size:var(--f-sm); cursor:pointer; text-align:left;
}
.prodsw:hover{border-color:var(--ink3)}
.prodsw .t{flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
.prodmeta{display:flex; gap:6px; margin-top:var(--s1); padding:0 2px; flex-wrap:wrap}
.prodsw[aria-expanded="true"]{border-color:var(--pri)}
.prodsw[aria-expanded="true"] .ic{transform:rotate(180deg)}

/* ── 产品切换下拉 ────────────────────────────────────────────────────────
   position:fixed 而不是侧栏内的绝对定位：.side 是 overflow-y:auto，
   绝对定位的浮层会被它裁掉一半。坐标按锚点算，滚动与改窗口时重算。 */
.prodpop{
  position:fixed; z-index:90; min-width:260px;
  background:var(--surf); border:1px solid var(--line); border-radius:var(--rc);
  box-shadow:var(--shadow-lg); padding:6px; overflow:hidden;
}
.pp-list{display:flex; flex-direction:column; gap:2px;
  max-height:min(60vh,420px); overflow-y:auto}
.pp-item{
  display:flex; align-items:center; gap:var(--s1); width:100%; padding:var(--s1) var(--s2);
  border:0; border-radius:var(--r); background:none; color:var(--ink);
  font:inherit; font-size:var(--f-sm); cursor:pointer; text-align:left;
}
.pp-item:hover{background:var(--sunk)}
.pp-item.on{background:var(--pri-soft); color:var(--pri)}
/* 名字一行、标签一行。挤在同一行时被截掉的总是名字，而名字才是用来认人的。 */
.pp-item .b{flex:1; min-width:0; display:flex; flex-direction:column; gap:4px}
.pp-item .t{min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
.pp-item .m{display:flex; gap:4px; align-items:center; flex-wrap:wrap}
.pp-item .ic{flex:none}
/* 「全部产品」是出口，不是又一个产品——用一条分隔线把它和上面的清单分开 */
.pp-all{margin-top:4px; border-top:1px solid var(--line); border-radius:0 0 var(--r) var(--r);
  color:var(--ink2); padding-top:var(--s1); align-items:center}
.pp-all .t{flex:1}
.pp-hint{padding:var(--s2); font-size:var(--f-xs); color:var(--ink3)}
.sidesearch{
  display:flex; align-items:center; gap:var(--s1); width:100%; height:36px; padding:0 var(--s2);
  margin-top:var(--s2); border:1px solid var(--line); border-radius:var(--r);
  background:var(--bg); color:var(--ink3); font:inherit; font-size:var(--f-xs); cursor:pointer;
}
.sidesearch:hover{border-color:var(--ink3); color:var(--ink2)}
.sidesearch .t{flex:1; text-align:left}

.navgroup{margin-top:var(--s3)}
.navgroup > .lbl{padding:0 var(--s2) var(--s1); display:block}
.nav{display:flex; flex-direction:column; gap:2px}
.navi{
  display:flex; align-items:center; gap:var(--s2); height:44px; padding:0 var(--s2);
  border-radius:var(--r); color:var(--ink2); font-size:var(--f-sm); cursor:pointer;
  background:none; border:0; font:inherit; width:100%; text-align:left; font-weight:400;
}
.navi:hover{background:var(--sunk); color:var(--ink); text-decoration:none}
.navi.on{background:var(--pri-soft); color:var(--pri); font-weight:600}
.navi .t{flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
.navi .tail{font-size:var(--f-xs); color:var(--ink3); flex:none}
.navi.on .tail{color:var(--pri)}
.navi .step{
  width:22px; height:22px; flex:none; border-radius:999px; border:1px solid var(--line);
  display:flex; align-items:center; justify-content:center; font-size:var(--f-xs); color:var(--ink3);
  font-variant-numeric:tabular-nums;
}
.navi.on .step{border-color:var(--pri); color:var(--pri)}
.navi.done .step{background:var(--ok-bg); border-color:var(--ok-bg); color:var(--ok)}
.navi.warn .step{border-color:var(--wrn); color:var(--wrn)}
.navi.warn .tail{color:var(--wrn)}
.navi .cnt{
  min-width:22px; height:22px; padding:0 8px; border-radius:999px; flex:none;
  display:flex; align-items:center; justify-content:center;
  font-size:var(--f-xs); font-weight:600; font-variant-numeric:tabular-nums;
  background:var(--bad-bg); color:var(--bad);
}
.subnav{display:flex; flex-direction:column; gap:2px; margin:2px 0 var(--s1) 30px;
  padding-left:var(--s2); border-left:1px solid var(--line)}
.subi{
  display:flex; align-items:center; gap:var(--s1); height:32px; padding:0 var(--s1);
  border-radius:6px; font-size:var(--f-xs); color:var(--ink2); cursor:pointer;
  background:none; border:0; font:inherit; width:100%; text-align:left;
}
.subi:hover{background:var(--sunk); color:var(--ink); text-decoration:none}
.subi.on{background:var(--sunk); color:var(--ink); font-weight:600}
.subi .t{flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
.subi.locked{color:var(--ink3); cursor:not-allowed}
.subi.locked:hover{background:none; color:var(--ink3)}

.side .util{margin-top:auto; padding-top:var(--s2); border-top:1px solid var(--line);
  display:flex; flex-direction:column; gap:2px}
.utili{
  display:flex; align-items:center; gap:var(--s2); height:36px; padding:0 var(--s2);
  border-radius:var(--r); color:var(--ink3); font-size:var(--f-xs); cursor:pointer;
  background:none; border:0; font:inherit; width:100%; text-align:left;
}
.utili:hover{background:var(--sunk); color:var(--ink2); text-decoration:none}
.utili.on{background:var(--sunk); color:var(--ink)}
.utili .t{flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
.utili.me{height:44px; color:var(--ink2)}
.avatar{
  width:24px; height:24px; border-radius:999px; flex:none; font-size:var(--f-xs); font-weight:600;
  display:flex; align-items:center; justify-content:center;
  background:var(--pri-soft); color:var(--pri);
}

/* 主区 */
.main{flex:1; min-width:0; display:flex; flex-direction:column; background:var(--bg)}
.ph{
  flex:none; padding:var(--s3) var(--s4); background:var(--surf); border-bottom:1px solid var(--line);
  display:flex; align-items:flex-start; gap:var(--s3); position:sticky; top:0; z-index:10;
}
.ph .l{flex:1; min-width:0; display:flex; flex-direction:column; gap:var(--s1)}
.ph .meta{display:flex; align-items:center; gap:var(--s1); flex-wrap:wrap}
.ph .r{flex:none; display:flex; align-items:center; gap:var(--s2)}
.ph .r.col{flex-direction:column; align-items:flex-end; gap:6px}
.content{padding:var(--s4); display:flex; flex-direction:column; gap:var(--s5); max-width:1180px; width:100%}
.content.narrow{max-width:840px}
.content.wide{max-width:none}

/* 居中页（登录、首次设置、全部产品）*/
.center{max-width:1000px; margin:0 auto; padding:var(--s6) var(--s4) var(--s6);
  display:flex; flex-direction:column; gap:var(--s5); width:100%}
.center.sm{max-width:440px; padding-top:96px}
.topbar{
  height:var(--head-h); display:flex; align-items:center; gap:var(--s2); padding:0 var(--s4);
  border-bottom:1px solid var(--line); background:var(--surf); position:sticky; top:0; z-index:20;
}

/* ── 表单 ───────────────────────────────────────────────────────────────── */
.field{display:flex; flex-direction:column; gap:var(--s1)}
.field > label{font-size:var(--f-xs); font-weight:600; color:var(--ink2)}
.field .hint{font-size:var(--f-xs); color:var(--ink3); line-height:1.6}
input[type=text],input[type=password],input[type=number],input:not([type]),textarea,select{
  width:100%; font:inherit; font-size:var(--f-sm); color:var(--ink);
  background:var(--bg); border:1px solid var(--line); border-radius:var(--r);
  padding:0 var(--s2); height:40px; transition:border-color .12s;
}
textarea{height:auto; padding:var(--s1) var(--s2); line-height:1.6; resize:vertical; min-height:80px}
select{cursor:pointer; height:40px; appearance:none;
  padding-right:var(--s4); background-image:none}
input:hover,textarea:hover,select:hover{border-color:var(--ink3)}
input:focus,textarea:focus,select:focus{outline:none; border-color:var(--pri); box-shadow:0 0 0 3px var(--pri-soft)}
input::placeholder,textarea::placeholder{color:var(--ink3)}
input[type=file]{padding:8px var(--s2); height:auto; cursor:pointer}
input[type=range]{padding:0; height:auto; border:0; background:none; accent-color:var(--pri)}
.field.bad input,.field.bad textarea,.field.bad select{border-color:var(--bad)}
.field.bad input:focus,.field.bad textarea:focus{box-shadow:0 0 0 3px var(--bad-bg)}
.field .err{font-size:var(--f-xs); color:var(--bad); display:flex; align-items:center; gap:6px}
.field .ok{font-size:var(--f-xs); color:var(--ok); display:flex; align-items:center; gap:6px}
.check{display:flex; align-items:flex-start; gap:var(--s1); font-size:var(--f-xs); color:var(--ink2);
  cursor:pointer; line-height:1.6}
.check input{width:16px; height:16px; margin:2px 0 0; flex:none; accent-color:var(--pri)}
.seg{display:inline-flex; border:1px solid var(--line); border-radius:var(--r); overflow:hidden; background:var(--surf)}
.seg button{
  font:inherit; font-size:var(--f-xs); cursor:pointer; border:0; background:transparent;
  color:var(--ink2); height:32px; padding:0 var(--s2);
}
.seg button[aria-pressed="true"]{background:var(--pri-soft); color:var(--pri); font-weight:600}

/* ── 页签 ───────────────────────────────────────────────────────────────── */
.tabs{display:flex; gap:var(--s3); border-bottom:1px solid var(--line); flex-wrap:wrap}
.tab{
  font:inherit; font-size:var(--f-sm); cursor:pointer; background:none; border:0;
  border-bottom:2px solid transparent; color:var(--ink2); padding:0 0 var(--s2);
  display:flex; align-items:center; gap:var(--s1); min-height:40px;
}
.tab:hover{color:var(--ink)}
.tab[aria-selected="true"]{color:var(--ink); border-bottom-color:var(--pri); font-weight:600}

/* ── 收件箱（首页）─────────────────────────────────────────────────────── */
.inbox{display:flex; flex:1; min-height:0}
.inbox .list{
  width:392px; flex:none; background:var(--surf); border-right:1px solid var(--line);
  display:flex; flex-direction:column; overflow-y:auto;
}
.inbox .list .hd{padding:var(--s3) var(--s3) var(--s2); display:flex; flex-direction:column;
  gap:var(--s2); border-bottom:1px solid var(--line); position:sticky; top:0; background:var(--surf); z-index:2}
.chips{display:flex; gap:6px; flex-wrap:wrap}
.chip{
  font:inherit; font-size:var(--f-xs); padding:0 var(--s2); height:32px; cursor:pointer;
  display:inline-flex; align-items:center; gap:6px; border-radius:999px;
  border:1px solid var(--line); color:var(--ink2); background:transparent;
}
.chip:hover{color:var(--ink); border-color:var(--ink3)}
.chip[aria-pressed="true"]{background:var(--ink); color:var(--surf); border-color:var(--ink); font-weight:600}
.task{
  display:flex; gap:var(--s2); padding:var(--s2) var(--s3); width:100%; text-align:left;
  border:0; border-bottom:1px solid var(--line2); background:none; font:inherit; color:inherit;
  cursor:pointer; align-items:flex-start;
}
.task:hover{background:var(--sunk)}
.task[aria-current="true"]{background:var(--pri-soft); box-shadow:inset 3px 0 0 var(--pri)}
.task .sev{width:8px; height:8px; border-radius:2px; margin-top:6px; flex:none; background:var(--ink3)}
.task .sev.bad{background:var(--bad)} .task .sev.drv{background:var(--drv)}
.task .sev.wrn{background:var(--wrn)} .task .sev.ok{background:var(--ok)}
.task .bd{flex:1; min-width:0}
.task .bd .t{font-size:var(--f-sm); font-weight:600; line-height:1.45;
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden}
.task .bd .s{font-size:var(--f-xs); color:var(--ink3); margin-top:4px;
  display:flex; gap:var(--s1); flex-wrap:nowrap; align-items:center; overflow:hidden}
.task .bd .s span{overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
.task .bd .s .who{color:var(--ink2)}
.task .end{margin-left:auto; flex:none; display:flex; align-items:center; gap:var(--s1); padding-top:2px}
.detail{flex:1; min-width:0; display:flex; flex-direction:column; background:var(--bg)}
.detail .content{flex:1}
.actbar{
  margin-top:auto; padding:var(--s3) var(--s4); background:var(--surf); border-top:1px solid var(--line);
  display:flex; align-items:center; gap:var(--s2); flex-wrap:wrap; position:sticky; bottom:0;
}
@media (max-width:1080px){
  .inbox{flex-direction:column}
  .inbox .list{width:100%; border-right:0; border-bottom:1px solid var(--line); max-height:44vh}
}

/* ── 引文（AI 推断内容）────────────────────────────────────────────────── */
.quote{
  background:var(--surf); border:1px solid var(--line); border-left:3px solid var(--drv);
  border-radius:var(--rc); padding:var(--s3); line-height:1.7;
}
/* 字段内容本来就是 Markdown。原样当纯文本贴出来，星号和表格管道符会全露在脸上。 */
.md > *:first-child{margin-top:0}
.md > *:last-child{margin-bottom:0}
.md p{margin:var(--s2) 0}
.md ul,.md ol{margin:var(--s2) 0; padding-left:var(--s3)}
.md li{margin:4px 0}
.md h1,.md h2,.md h3,.md h4{font-size:var(--f-sm); font-weight:600; margin:var(--s3) 0 var(--s1)}
.md strong{font-weight:600}
.md hr{border:0; border-top:1px solid var(--line); margin:var(--s2) 0}
.md code{font-family:var(--mono); font-size:var(--f-xs); background:var(--sunk);
  padding:2px 6px; border-radius:4px}
.md blockquote{margin:var(--s2) 0; padding:var(--s1) var(--s2); border-left:2px solid var(--line);
  color:var(--ink2); font-size:var(--f-xs)}
.md table{border-collapse:collapse; width:100%; font-size:var(--f-xs); min-width:420px}
.md th,.md td{border:1px solid var(--line); padding:6px var(--s1); text-align:left; vertical-align:top}
.md th{background:var(--sunk); font-weight:600}
.quote.ok{border-left-color:var(--ok)}
.quote .src{margin-top:var(--s2); padding-top:var(--s2); border-top:1px solid var(--line2);
  font-size:var(--f-xs); color:var(--ink2); white-space:normal; line-height:1.65}
.quote .src b{color:var(--drv); font-weight:600}

/* ── 视觉工作室 ────────────────────────────────────────────────────────── */
.basis{
  background:var(--surf); border:1px solid var(--line); border-radius:var(--rc);
  padding:var(--s2) var(--s3); display:flex; align-items:center; gap:var(--s3); flex-wrap:wrap;
}
.basis .grp{display:flex; align-items:baseline; gap:var(--s1); font-size:var(--f-xs); color:var(--ink2)}
.basis .grp b{font-weight:600; color:var(--ink)}
/* 同一行的卡等高（grid 默认 stretch）。等高本身不是目的——
   目的是让「图片顶边、头部、动作区底边」三条线在一行里对齐。
   高度差由中间的正文吸收（.shot .b 有 flex:1），不会在动作区下方留出空档。 */
.gal{display:grid; grid-template-columns:repeat(auto-fill,minmax(268px,1fr)); gap:var(--s2)}
.shot{background:var(--surf); border:1px solid var(--line); border-radius:var(--rc);
  overflow:hidden; display:flex; flex-direction:column}
.shot.sel{border-color:var(--pri); box-shadow:0 0 0 2px var(--pri-soft)}
.shot.appr{border-color:var(--ok); box-shadow:0 0 0 2px var(--ok-bg)}
/* 比例钉在按钮上而不只在 img 上：图片加载失败时占位仍守住高度，
   否则一张 404 会把同一行其它卡的标题、按钮全带偏。 */
.shot > button.thumb{padding:0; border:0; background:var(--sunk); cursor:zoom-in; display:block;
  width:100%; aspect-ratio:1; overflow:hidden}
.shot img{width:100%; height:100%; aspect-ratio:1; object-fit:cover; display:block}
/* 头部：徽章行 / 标题 / 署名，三行固定，不参与伸缩。
   每张卡都有这三行（没内容也占位），同一行里的标题才落在同一条线上。 */
.shot .hd{padding:var(--s2) var(--s2) 0; display:flex; flex-direction:column; gap:6px; flex:0 0 auto}
.shot .hd .t{font-weight:600; font-size:var(--f-sm); line-height:1.4}
/* 正文：唯一会伸缩的一段，把同行的高度差吃掉 */
.shot .b{padding:var(--s2); display:flex; flex-direction:column; gap:var(--s2); flex:1 1 auto}
.shot .b .d{font-size:var(--f-xs); color:var(--ink2); line-height:1.6}
/* 收件箱里的简版方案卡没有 .hd，标题直接落在 .b 里 */
.shot .b > .t{font-weight:600; font-size:var(--f-sm)}
.shot .b .d b{color:var(--ink); font-weight:600}
/* 设计主张偶尔会写得很长（模型超了字数上限），一张卡就能把整行撑高。
   收到 6 行，全文挂在 title 上，也仍在灯箱里 —— 收的是行高，不是信息。 */
.shot .b .d.clamp{display:-webkit-box; -webkit-line-clamp:6; -webkit-box-orient:vertical;
  overflow:hidden}

/* 动作：两列定位网格。
   原来是 flex-wrap，换行点随按钮文字宽度走，同一行几张卡各断各的——
   截图里「排布混乱」就是这么来的。网格把每个按钮钉死在固定格子上。 */
.shot .acts{display:grid; grid-template-columns:1fr 1fr; gap:var(--s1);
  padding:0 var(--s2) var(--s2); align-content:end}
.shot .acts > *{min-width:0}
.shot .acts .btn{width:100%}
.shot .acts .btn span{overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
.shot .acts .span2{grid-column:1 / -1}
/* 删除是破坏性操作：平时压低存在感，悬停时才转成危险色。
   用 ghost（全透明）的话，占满整行时看着像一行散字，不像按钮。 */
.shot .acts .btn.del{color:var(--ink3)}
.shot .acts .btn.del:hover:not(:disabled){color:var(--bad); border-color:var(--bad); background:var(--surf)}
.btn.full{width:100%}
/* 衍生版本左边一道竖线，一眼看出它挂在前一张下面。
   用 inset 阴影而不是 border-left：加粗边框会把内容盒挤窄 2px，
   缩略图按 aspect-ratio 就矮 2px，同一行的标题、署名全跟着错开。 */
.shot.kid{box-shadow:inset 3px 0 0 var(--pri-soft)}
/* 署名行：谁、什么时候。轮次一多，这是区分两张相似图的唯一线索 */
.shot .stamp{display:flex; align-items:center; gap:6px; font-size:var(--f-xs);
  color:var(--ink3); font-variant-numeric:tabular-nums; flex-wrap:wrap}
.shot .stamp .ic{opacity:.7}
/* 参考图改成按钮（可点开灯箱），保持和原来 img 一样的尺寸 */
.refs button.th{padding:0; border:0; background:transparent; cursor:zoom-in; line-height:0}
.refs button.th:hover img{border-color:var(--pri)}
.fail{font-size:var(--f-xs); line-height:1.6; display:flex; flex-direction:column; gap:4px}
.fail .h{color:var(--bad)} .fail .h b{font-family:var(--mono)}
.refs{display:flex; gap:var(--s1); flex-wrap:wrap}
.refs .th{position:relative}
.refs img{width:72px; height:72px; object-fit:cover; border-radius:var(--r); border:1px solid var(--line); display:block}
.refs .rm{position:absolute; top:-6px; right:-6px; width:22px; height:22px; padding:0; border-radius:999px;
  background:var(--surf); border:1px solid var(--line); color:var(--ink2); cursor:pointer;
  display:flex; align-items:center; justify-content:center; font:inherit; line-height:1}
.refs .rm:hover{color:var(--bad); border-color:var(--bad)}
.paint{position:relative; display:inline-block; max-width:100%; touch-action:none;
  border-radius:var(--rc); overflow:hidden; border:1px solid var(--line)}
.paint canvas{position:absolute; left:0; top:0; cursor:crosshair}
.paint img{display:block; max-width:100%}
.revise-grid{display:grid; grid-template-columns:240px 1fr; gap:var(--s3); align-items:start}
@media (max-width:820px){.revise-grid{grid-template-columns:1fr}}
.revise-grid > img{width:100%; border-radius:var(--rc); border:1px solid var(--line)}

/* ── 进度步骤（SSE）────────────────────────────────────────────────────── */
.steps{display:flex; flex-direction:column; gap:var(--s1)}
.step{display:flex; align-items:center; gap:var(--s2); padding:var(--s2);
  background:var(--surf); border:1px solid var(--line); border-radius:var(--r); color:var(--ink2)}
.step .dot{width:8px; height:8px; border-radius:999px; background:var(--line); flex:none}
.step .t{flex:1; min-width:0}
.step .st{font-size:var(--f-xs); color:var(--ink3); flex:none}
.step.run .dot{background:var(--pri); animation:pulse 1.1s ease-in-out infinite}
.step.ok .dot{background:var(--ok)}
.step.bad .dot{background:var(--bad)}
.step.ok .st{color:var(--ok)} .step.bad .st{color:var(--bad)}
@keyframes pulse{50%{opacity:.25}}

/* ── 文档渲染 ──────────────────────────────────────────────────────────── */
.doc{background:var(--surf); border:1px solid var(--line); border-radius:var(--rc);
  padding:var(--s4); line-height:1.75; max-width:840px}
.doc h1{font-size:var(--f-lg); font-weight:600; margin:0 0 var(--s2)}
.doc h2{font-size:var(--f-md); font-weight:600; margin:var(--s4) 0 var(--s2);
  padding-bottom:var(--s1); border-bottom:1px solid var(--line)}
.doc h3,.doc h4{font-size:var(--f-sm); font-weight:600; margin:var(--s3) 0 var(--s1)}
.doc p{margin:var(--s2) 0; color:var(--ink2)}
.doc ul,.doc ol{margin:var(--s2) 0; padding-left:var(--s3); color:var(--ink2)}
.doc li{margin:4px 0}
.doc strong{color:var(--ink); font-weight:600}
.doc hr{border:0; border-top:1px solid var(--line); margin:var(--s3) 0}
.doc blockquote{margin:var(--s2) 0; padding:var(--s2) var(--s2) var(--s2) var(--s2);
  border-left:3px solid var(--drv); background:var(--sunk); border-radius:0 var(--r) var(--r) 0;
  font-size:var(--f-xs); color:var(--ink2)}
.doc code{font-family:var(--mono); font-size:var(--f-xs); background:var(--sunk);
  padding:2px 6px; border-radius:4px}
.tablewrap{overflow-x:auto; margin:var(--s2) 0}
.doc table{border-collapse:collapse; width:100%; font-size:var(--f-xs); min-width:480px}
.doc th,.doc td{border:1px solid var(--line); padding:var(--s1); text-align:left; vertical-align:top}
.doc th{background:var(--sunk); font-weight:600}

/* ── 底部操作条 ────────────────────────────────────────────────────────── */
.dock{
  position:sticky; bottom:0; z-index:15; margin:0 calc(-1 * var(--s4)) calc(-1 * var(--s4));
  padding:var(--s2) var(--s4); background:var(--surf); border-top:1px solid var(--line);
  display:flex; align-items:center; gap:var(--s2); flex-wrap:wrap; box-shadow:0 -8px 24px -16px rgba(0,0,0,.5);
}
.dock .cnt{font-size:var(--f-sm); color:var(--ink2)}
.dock .cnt b{color:var(--ok); font-size:var(--f-md); font-variant-numeric:tabular-nums}
.dock .cnt b.zero{color:var(--ink3)}

/* ── 对话框 / 命令面板 / 提示 ──────────────────────────────────────────── */
.scrim{position:fixed; inset:0; background:var(--scrim); z-index:100;
  display:flex; align-items:flex-start; justify-content:center; padding:var(--s5) var(--s3)}
/* 高度必须收在视口内，正文自己滚，动作行钉住。
   不收的话内容一长就把按钮顶出屏幕——而 .scrim 是 position:fixed，
   页面也滚不到那里去，等于这个对话框没法确认也没法取消。
   144px = 上下各 48 的 scrim 内边距 + 48 的 margin-top。 */
.modal{
  background:var(--surf); border:1px solid var(--line); border-radius:var(--rc);
  box-shadow:var(--shadow-lg); width:100%; max-width:480px; margin-top:48px;
  max-height:calc(100vh - 144px);
  padding:var(--s4); display:flex; flex-direction:column; gap:var(--s3);
}
.modal.wide{max-width:720px}
.modal > h2{flex:none}
.modal h2{font-size:var(--f-md); font-weight:600}
/* 只有正文滚。min-height:0 是必需的：flex 子项的默认 min-height:auto
   会让它按内容撑开，overflow 根本不生效。 */
.modal > .msg{flex:1 1 auto; min-height:0; overflow-y:auto}
.modal .msg{color:var(--ink2); line-height:1.7}
.modal .msg b{color:var(--ink); font-weight:600}
.modal .acts{display:flex; gap:var(--s2); justify-content:flex-end; flex-wrap:wrap; flex:none}
/* 提示词原文。等宽 + 保留换行：出图模型收到的就是这些字符，显示上做任何加工
   （渲染 markdown、折行改写）都会让人看不出提示词里到底写了什么。
   滚动交给外面的 .msg，pre 自己不再开一层——两层滚动区套在一起，
   滚轮落在哪一层全看鼠标位置，那种滚不动的感觉查都查不出原因。 */
.promptwrap{display:flex; flex-direction:column; gap:var(--s2)}
/* 可改的那段约束。跟下面的全文同字号同行高——改的是哪几行、落到全文哪个位置，
   两边字形一致才对得上。 */
.prompt-edit{font-size:var(--f-xs); line-height:1.7}
/* 约束被手改过时的「看提示词」。用 --pri 是因为它此刻在陈述一个**待生效的选择**，
   不是警告；旁边的 .key 生成按钮仍是这一屏唯一的主行动。 */
.btn.edited{border-color:var(--pri); color:var(--pri)}
.promptbox{
  margin:0; padding:var(--s2); border:1px solid var(--line); border-radius:var(--r);
  background:var(--sunk); color:var(--ink);
  font-family:var(--mono); font-size:var(--f-xs); line-height:1.7;
  white-space:pre-wrap; overflow-wrap:anywhere;
}
/* 命令面板自己管高度（.res 那一段有 max-height 与滚动），
   所以不套 .modal 那条 144px 的上限——套了会跟它自己的算法打架。 */
.palette{max-width:600px; margin-top:64px; padding:0; overflow:hidden; max-height:none}
.palette input{border:0; border-bottom:1px solid var(--line); border-radius:0; height:56px;
  font-size:var(--f-md); padding:0 var(--s3); background:transparent}
.palette input:focus{box-shadow:none; border-color:var(--line)}
.palette .res{max-height:min(420px,50vh); overflow-y:auto; display:flex; flex-direction:column}
.palette .res button{
  display:flex; align-items:center; gap:var(--s2); padding:var(--s2) var(--s3); width:100%;
  border:0; background:none; font:inherit; color:inherit; cursor:pointer; text-align:left;
}
.palette .res button .t{flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
.palette .res button .k{font-size:var(--f-xs); color:var(--ink3); flex:none}
.palette .res button[aria-selected="true"]{background:var(--pri-soft); color:var(--pri)}
.palette .foot{padding:var(--s2) var(--s3); border-top:1px solid var(--line);
  font-size:var(--f-xs); color:var(--ink3); display:flex; gap:var(--s2); flex-wrap:wrap}

.toasts{position:fixed; right:var(--s3); bottom:var(--s3); z-index:200;
  display:flex; flex-direction:column; gap:var(--s1); max-width:min(400px,calc(100vw - 48px))}
.toast{
  background:var(--surf); border:1px solid var(--line); border-left:3px solid var(--ok);
  border-radius:var(--r); box-shadow:var(--shadow); padding:var(--s2);
  display:flex; gap:var(--s1); align-items:flex-start; animation:slidein .18s ease-out;
}
.toast.bad{border-left-color:var(--bad)}
.toast.wrn{border-left-color:var(--wrn)}
.toast .ic{margin-top:2px; color:var(--ok)}
.toast.bad .ic{color:var(--bad)} .toast.wrn .ic{color:var(--wrn)}
/* pre-line：正文里的换行要真的换行。有些 toast 要说两件事（做了什么 + 接下来去哪看），
   挤成一行读不出来是两句。行首行尾的空白仍然折叠，所以缩进的模板字符串不受影响。 */
.toast .tx{flex:1; min-width:0; font-size:var(--f-xs); line-height:1.6; white-space:pre-line}
.toast .tx b{display:block; font-size:var(--f-sm); font-weight:600; margin-bottom:2px}
.toast .x{background:none; border:0; color:var(--ink3); cursor:pointer; padding:0; font:inherit;
  width:20px; height:20px; display:flex; align-items:center; justify-content:center; flex:none}
.toast .x:hover{color:var(--ink)}
@keyframes slidein{from{opacity:0; transform:translateY(8px)}}

/* ── 产品卡（全部产品）─────────────────────────────────────────────────── */
.plist{display:grid; grid-template-columns:repeat(auto-fill,minmax(320px,1fr)); gap:var(--s2)}
.pcard{
  background:var(--surf); border:1px solid var(--line); border-radius:var(--rc); padding:var(--s3);
  display:flex; flex-direction:column; gap:var(--s2); cursor:pointer; text-align:left;
  font:inherit; color:inherit;
}
.pcard:hover{border-color:var(--pri); text-decoration:none}
.pcard .t{font-size:var(--f-md); font-weight:600}

/* ── 窄屏 ──────────────────────────────────────────────────────────────── */
@media (max-width:900px){
  .app{flex-direction:column}
  .side{width:100%; height:auto; position:static; border-right:0; border-bottom:1px solid var(--line)}
  .side .util{margin-top:var(--s2)}
  .ph{position:static; padding:var(--s3) var(--s2); flex-direction:column; align-items:stretch}
  .ph .r{justify-content:flex-start}
  .ph .r.col{align-items:stretch}
  .content{padding:var(--s3) var(--s2); gap:var(--s4)}
  .dock{margin:0 calc(-1 * var(--s2)) calc(-1 * var(--s3)); padding:var(--s2)}
  .stats{gap:var(--s3)}
}

/* 收件箱要两栏各自滚动，所以这一页的 main 固定视口高 */
.main.fixed{height:100vh; overflow:hidden}
.main.fixed .detail{overflow-y:auto}
@media (max-width:900px){.main.fixed{height:auto; overflow:visible}
  .main.fixed .detail{overflow:visible}}

/* 375px 手机宽：顶栏一行放不下，让它换行而不是把内容顶出屏幕 */
@media (max-width:900px){
  /* 窄屏上把对话框的留白压到最小，否则可用高度会被边距吃掉一大半 */
  .scrim{padding:var(--s2)}
  .modal{margin-top:var(--s2); max-height:calc(100vh - var(--s5))}
}
@media (max-width:600px){
  /* Evidence status belongs below the description on narrow screens. */
  .rows .gate-row,.rows .package-row{display:grid;grid-template-columns:16px 32px minmax(0,1fr);align-items:start}
  .rows .gate-row .end,.rows .package-row > .pill{grid-column:3;margin-left:0;justify-self:start;max-width:100%;white-space:normal}
  .topbar{height:auto; padding:var(--s2); flex-wrap:wrap; row-gap:var(--s1)}
  .topbar .h3{white-space:nowrap}
  .topbar .spacer{flex-basis:100%; height:0}
  .center{padding:var(--s4) var(--s2)}
  .center.sm{padding-top:var(--s4)}
  .plist{grid-template-columns:1fr}
  .stats{gap:var(--s2) var(--s3)}
  .ph .r,.actbar,.dock{flex-wrap:wrap}
}

/* 出图模型 / 提示词模板选择器 —— 多模型接入后新增 */

/* 选中的模型不吃参考图时，那一栏整体压暗但不隐藏：
   隐藏会让人以为功能没了，压暗才读得出「这个模型做不到」。 */
.field.off { opacity: .45; }
.field.off input, .field.off select, .field.off textarea { pointer-events: none; }

/* 五槽位微调用原生 details，自带键盘可达性 */
.fold { border: 1px solid var(--line); border-radius: 8px; padding: .5rem .75rem; }
.fold > summary { cursor: pointer; font-size: .85rem; color: var(--ink3); user-select: none; }
.fold > summary:hover { color: var(--ink); }
.fold[open] > summary { margin-bottom: .6rem; }
.fold textarea { font-size: .8rem; line-height: 1.5; }

/* ── 图片灯箱 ─────────────────────────────────────────────────────────────
   评审看的是细节（分模线、按键相对手的大小、材质），所以必须能到 100%。
   原来点缩略图是另开标签页，浏览器把图缩到适应窗口就没有下文了。 */
.scrim.lb{padding:var(--s2); align-items:stretch}
.lb-box{background:var(--surf); border:1px solid var(--line); border-radius:var(--rc);
  box-shadow:var(--shadow-lg); width:100%; display:flex; flex-direction:column; overflow:hidden}
.lb-bar{display:flex; align-items:center; gap:var(--s2); padding:var(--s2) var(--s3);
  border-bottom:1px solid var(--line); flex-wrap:wrap}
.lb-title{display:flex; flex-direction:column; gap:2px; margin-right:auto; min-width:0}
.lb-title b{font-size:var(--f-sm); font-weight:600}
.lb-tools{display:flex; align-items:center; gap:var(--s1); flex-wrap:wrap}
.lb-zoom{font-size:var(--f-xs); color:var(--ink3); font-variant-numeric:tabular-nums;
  min-width:5.5em; text-align:right}
.lb-stage{flex:1; overflow:hidden; background:var(--sunk); display:flex;
  align-items:center; justify-content:center; min-height:0; touch-action:none}
.lb-stage img{transform-origin:center center; image-rendering:auto; cursor:grab;
  max-width:none; max-height:none; display:block;
  user-select:none; -webkit-user-drag:none}   /* 连同 draggable=false 一起挡住原生拖拽 */
.lb-stage img.fit{max-width:100%; max-height:100%; cursor:default}
.lb-stage.grab img{cursor:grabbing}

/* ── 卡内的标准视图条与 3D 行 ─────────────────────────────────────────────
   两者都是「这张图派生出来的东西」，属于卡片内部信息。
   之前视图各占一格，一个方案能把画廊撑成五格，还看不出它们是一组。 */
.vstrip{display:flex; flex-direction:column; gap:6px}
.vstrip .lbl{font-size:var(--f-xs); color:var(--ink3)}
.vrow{display:grid; grid-template-columns:repeat(4,1fr); gap:4px}
/* 删除键不能嵌在放大键里面（按钮套按钮是非法结构），所以外面套一层来定位它。 */
.vth-wrap{position:relative; display:flex; min-width:0}
.vth{padding:0; border:1px solid var(--line); border-radius:var(--r); background:var(--sunk);
  cursor:zoom-in; overflow:hidden; display:flex; flex-direction:column; min-width:0; flex:1}
.vth:hover{border-color:var(--pri)}
/* 常驻但压暗：hover 才显形会让人以为没有这个功能，触屏上更是根本摸不到。 */
.vth-wrap .vrm{
  position:absolute; top:3px; right:3px; width:20px; height:20px; padding:0; border-radius:999px;
  background:var(--scrim); color:var(--ink2); border:0; opacity:.55; transition:opacity .12s;
}
.vth-wrap:hover .vrm,.vth-wrap .vrm:focus-visible{opacity:1}
.vth-wrap .vrm:hover:not(:disabled){color:var(--bad); background:var(--bad-bg); opacity:1}
.vth-wrap .vrm:disabled{opacity:.25}
.vth img{width:100%; aspect-ratio:1; object-fit:cover; display:block}
.vth .cap{font-size:10px; line-height:1.6; color:var(--ink3); padding:2px 2px 3px;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis; text-align:center}

/* 结构写死成两段（标题行 + 动作行），不用 flex-wrap 自己找换行点——
   换行点随文字长短变，一个长文件名就能把删除键挤成孤零零的一行。 */
.meshrow{display:flex; flex-direction:column; gap:6px; min-width:0;
  padding:6px; border:1px solid var(--line); border-radius:var(--r); background:var(--sunk)}
.mr-hd{display:flex; align-items:center; gap:var(--s1); min-width:0}
/* 说明文字是唯一该被压缩的东西：缩略图和删除键都是定宽的 */
.mr-t{flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
  font-size:var(--f-xs); line-height:1.6}
.mr-hd > .btn{flex:none}
.mr-acts{display:grid; grid-template-columns:1fr 1fr; gap:var(--s1)}
.mr-acts.one{grid-template-columns:1fr}
.mr-acts > .btn,.mr-acts > a.btn{width:100%; min-width:0}
/* 按钮里的字也要能截断，否则「.zip（3 个文件）」会把格子撑开 */
.mr-acts .btn span,.mr-acts a.btn span{overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
.meshrow.ok{border-color:var(--ok)}
.meshrow.bad{border-color:var(--bad)}
.meshrow .mth{width:32px; height:32px; object-fit:cover; border-radius:4px; display:block}
.meshrow .xs{margin-right:auto}
/* 下载是 <a> 不是 <button>，补上按钮的行高与内边距 */
a.btn{text-decoration:none}

/* ── 涂改 / 批注 ─────────────────────────────────────────────────────────
   两层画布叠在原图上：下层是画笔（mask 的唯一依据），上层是矢量批注（可撤销）。
   指针事件统一由上层接，再按当前工具分流——否则两层会抢事件。 */
.paint .annlayer{z-index:2}
.tabs.sub{gap:4px}
.tabs.sub .tab{height:32px; padding:0 var(--s2); font-size:var(--f-xs)}
.btn.tool[aria-pressed="true"]{background:var(--pri); color:var(--on-pri); border-color:var(--pri)}

/* ── 3D 实时预览 ─────────────────────────────────────────────────────────
   复用灯箱的外壳，只把舞台换成 WebGL 画布。舞台必须有确定的高度：
   three 按 clientHeight 设视口，父级是 flex 时高度会塌成 0，画面就整块空白。 */
.lb-stage.mesh{min-height:60vh; position:relative; background:#e9ebee}
.lb-stage.mesh canvas{display:block; width:100%; height:100%; touch-action:none}
.meshfail{display:flex; flex-direction:column; align-items:center; gap:var(--s2);
  padding:var(--s5) var(--s3); text-align:center; color:var(--ink2); background:var(--surf);
  width:100%; height:100%; justify-content:center}
.meshfail b{color:var(--ink); font-size:var(--f-md)}
.meshfail p{max-width:44ch; line-height:1.7}
/* 卡内 3D 行里的缩略图：Tripo 那张预览图很糊，只当入口，点开进实时预览 */
.mthb{padding:0; border:0; background:transparent; cursor:pointer; line-height:0; border-radius:4px}
.mthb:hover{outline:2px solid var(--pri)}

/* ── 回收站 ─────────────────────────────────────────────────────────────
   卡片结构和工作室一致（图/头/正文/动作），只是正文是空的——
   回收站里要看的只有「这是哪张、谁什么时候删的」。
   卡也比画廊小一档：这里要的是「认出来是哪一张、恢复还是删」，不是评审细节——
   细节点开大图看。密度上去了，六张不用滚两屏。 */
.gal.trash{grid-template-columns:repeat(auto-fill,minmax(208px,1fr))}
.gal.trash .shot{opacity:.72}
.gal.trash .shot:hover{opacity:1}
.gal.trash .shot .b{padding:0}
/* 卡窄了一档，「彻底删除」四个字加图标会被省略号吃掉尾巴——
   一个写着「彻底删…」的按钮，正是最不该看不清的那种按钮。 */
.gal.trash .acts .btn{padding:0 var(--s1)}
.btn.del{color:var(--ink3)}
.btn.del:hover:not(:disabled){color:var(--bad); border-color:var(--bad)}

/* 轮次标题行：徽章与「删除整轮」都挂在右侧。
   .sect-head 是 baseline 对齐（为了标题和那条分隔线对齐），
   但按钮按 baseline 会歪，所以这一组单独按 center 对。 */
.sect-head > .row-wrap{align-items:center; gap:var(--s1); align-self:center}
.sect-head > .row-wrap .btn{flex:0 0 auto}

/* 转 3D 的三选一：整行可点，不用非得点中那个小圆点 */
.rows .r.pick{cursor:pointer; gap:var(--s2); align-items:flex-start}
.rows .r.pick input{margin-top:4px; flex:0 0 auto}
.rows .r.pick:hover{background:var(--raise)}

/* CAD 判据行：通过的用绿色，不通过的沿用 .fail 的红 */
.fail.ok .h{color:var(--ok)}
.rows .r .idx{font-size:var(--f-sm); min-width:1.4em; text-align:center}

/* ── 作业条 ─────────────────────────────────────────────────────────────
   固定右下角，跨页面存在。长活儿离开页面后还能看到做到哪一步——
   这正是它存在的理由，所以它不能挂在任何一个页面的 DOM 里。 */
/* 右下角那一摞：toast 在上、作业条在下，不再各自 fixed 到同一个点互相盖。
   容器不吃指针事件，否则它那块透明区域会挡住底下页面的点击。 */
.dockstack{
  position:fixed; right:var(--s3); bottom:var(--s3); z-index:200;
  display:flex; flex-direction:column; align-items:flex-end; gap:var(--s1);
  max-height:calc(100vh - 2 * var(--s3)); pointer-events:none;
}
.dockstack > *{pointer-events:auto; position:static; right:auto; bottom:auto; z-index:auto}
.jobs{position:fixed; right:var(--s3); bottom:var(--s3); z-index:90; width:min(360px,calc(100vw - 2*var(--s3)));
  background:var(--surf); border:1px solid var(--line); border-radius:var(--rc);
  box-shadow:var(--shadow-lg); overflow:hidden; max-height:60vh; overflow-y:auto}
.jhead{position:sticky; top:0; z-index:1;    /* 展开后 dock 会变长并滚动，标题不该跟着滚走 */
  display:flex; align-items:center; gap:var(--s2); padding:var(--s2) var(--s3);
  border-bottom:1px solid var(--line); font-size:var(--f-sm);
  background:var(--surf)}          /* sticky 必须自带底色，否则行会从它底下透出来 */
.jrow{display:grid; grid-template-columns:1fr auto; gap:var(--s1) var(--s2);
  padding:var(--s2) var(--s3); border-bottom:1px solid var(--line2)}
.jrow:last-child{border-bottom:0}
.jrow.done{background:var(--ok-bg)}
.jrow.failed{background:var(--bad-bg)}
.jrow.unknown{background:var(--wrn-bg)}
.jrow.unknown .jerr{color:var(--wrn)}
html.noscroll .dockstack .jobs{display:none}
.jcontext{font-size:var(--f-xs);color:var(--ink2);overflow-wrap:anywhere}
.jhead{flex-wrap:wrap}
.jrow .btn{justify-self:start}
.jt{min-width:0}
.jt b{display:block; font-size:var(--f-sm); font-weight:600;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
.jt small{color:var(--ink3); font-size:var(--f-xs); font-variant-numeric:tabular-nums}
.jstep,.jerr{grid-column:1 / -1; font-size:var(--f-xs); color:var(--ink2); line-height:1.55}
.jerr{color:var(--bad);
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden}

/* 收着时标题只占一行、错误只占两行，点一下整行展开看全文与走过的每一步。
   一条失败的活儿，关键信息往往正好在被截掉的那半截里。 */
.jrow{cursor:pointer}
.jrow.open .jt b{white-space:normal; overflow:visible; text-overflow:clip}
.jrow.open .jerr{-webkit-line-clamp:none}
.jrow.open .jstep{display:none}   /* 全量列表的最后一条就是它，别重复一遍 */
.jall{display:none}
.jrow.open .jall{display:flex; flex-direction:column; gap:3px; grid-column:1 / -1;
  margin-top:4px; padding-top:6px; border-top:1px dashed var(--line)}
.jline{font-size:var(--f-xs); color:var(--ink3); line-height:1.5;
  padding-left:12px; position:relative}
.jline::before{content:"·"; position:absolute; left:2px}
.jline.ok{color:var(--ok)} .jline.bad{color:var(--bad)}
.spin{animation:spin 1.1s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}

/* 大图查看的翻页计数。等宽数字，翻页时不左右跳。 */
.lb-count{font-size:var(--f-xs); color:var(--ink3); font-variant-numeric:tabular-nums;
  min-width:3.5em; text-align:center}

/* ── 大弹窗（sheet）───────────────────────────────────────────────────────
   .modal 是给「一句话 + 确定/取消」用的，装不下一张表单或一个画廊。
   sheet 分三段：固定的头、可滚的身、固定的脚——表单再长，主按钮也永远在
   右下角同一个位置，不用滚到底去找。 */
.scrim.sh-scrim{align-items:center; padding:var(--s3)}
.sheet{
  background:var(--surf); border:1px solid var(--line); border-radius:var(--rc);
  box-shadow:var(--shadow-lg); width:100%; max-height:calc(100vh - var(--s5));
  display:flex; flex-direction:column; overflow:hidden;
}
.sheet.md{max-width:560px}
.sheet.lg{max-width:760px}
.sheet.xl{max-width:1080px}
.sh-head{display:flex; align-items:flex-start; gap:var(--s2); flex:none;
  padding:var(--s3) var(--s4); border-bottom:1px solid var(--line)}
.sh-t{flex:1; min-width:0; display:flex; flex-direction:column; gap:var(--s1)}
.sh-sub{font-size:var(--f-xs); color:var(--ink3); line-height:1.7; max-width:78ch}
.btn.sh-x{width:40px; padding:0; flex:none}
/* 钉在头下面的一条轨。空着就整条收掉——没有内容的分隔线只会读成「这里少了什么」。 */
.sh-rail{flex:none; padding:var(--s2) var(--s4); border-bottom:1px solid var(--line)}
.sh-rail:empty{display:none}
.sh-body{flex:1 1 auto; min-height:0; overflow-y:auto; padding:var(--s3) var(--s4);
  display:flex; flex-direction:column; gap:var(--s3)}
.sh-foot{flex:none; padding:var(--s2) var(--s4); border-top:1px solid var(--line); background:var(--surf)}
.sh-foot:empty{display:none}
.sh-foot-in{display:flex; align-items:center; gap:var(--s2); flex-wrap:wrap}
.sheet:focus{outline:none}          /* 打开时把焦点收进来，但不该画出一圈框 */
/* 弹窗开着的时候背景不许滚。滚得动就说明它其实不是一层弹窗，只是一块盖布。 */
html.noscroll,html.noscroll body{overflow:hidden}

/* ── 流程条 ──────────────────────────────────────────────────────────────
   静止时是预告（点了生成之后机器要干哪几件事），跑起来是进度。
   同一条东西两种用途，人不用学两遍。 */
.flow{display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:var(--s2); list-style:none}
.fs{position:relative; display:flex; align-items:flex-start; gap:var(--s1);
  padding:var(--s2); border:1px solid var(--line); border-radius:var(--r);
  background:var(--sunk); transition:border-color .15s, background .15s}
.fs::after{content:""; position:absolute; top:50%; right:calc(-1 * var(--s2));
  width:var(--s2); height:1px; background:var(--line)}
.fs:last-child::after{display:none}
.fs-n{flex:none; width:20px; height:20px; border-radius:999px; background:var(--line);
  color:var(--ink3); font-size:var(--f-xs); font-variant-numeric:tabular-nums;
  display:flex; align-items:center; justify-content:center}
.fs-b{display:flex; flex-direction:column; gap:2px; min-width:0}
.fs-b b{font-size:var(--f-xs); font-weight:600; color:var(--ink2)}
.fs-st{font-size:var(--f-xs); color:var(--ink3); line-height:1.5}
.fs.run{border-color:var(--pri); background:var(--pri-soft)}
.fs.run .fs-n{background:var(--pri); color:var(--on-pri); animation:pulse 1.1s ease-in-out infinite}
.fs.run .fs-b b{color:var(--ink)}
.fs.ok{border-color:var(--ok)}
.fs.ok .fs-n{background:var(--ok); color:var(--on-pri)}
.fs.ok .fs-b b{color:var(--ink)}
.fs.ok .fs-st{color:var(--ok)}
.fs.bad{border-color:var(--bad)}
.fs.bad .fs-n{background:var(--bad); color:var(--on-pri)}
.fs.bad .fs-st{color:var(--bad)}

/* ── 出图表单 ────────────────────────────────────────────────────────────
   两栏不是按长短分的，是按谁决定什么分的：
   左＝每一轮都要重新想的，右＝一轮之内必须保持不变的。

   整张表单撑满弹窗身体的高度，多出来的高度全给「设计方向」那个输入框。
   这条链上一律用 flex:1 0 auto（只长不缩）：换成 1 1 auto 配 min-height:0，
   内容比容器高的时候每一层都会被压成 0 —— 窄屏上两栏正是这么叠到一起去的。 */
.gf-pane{flex:1 0 auto; display:flex; flex-direction:column; gap:var(--s3)}
.gf-form{flex:1 0 auto; display:flex; flex-direction:column; gap:var(--s3)}
.gf{flex:1 0 auto;
  display:grid; grid-template-columns:minmax(0,1.15fr) minmax(0,1fr); gap:var(--s4)}
.gf-col{display:flex; flex-direction:column; gap:var(--s2); min-width:0}
.gf-col > .stack{flex:1 0 auto}
.gf-col .field.grow{flex:1 0 auto}
.gf-col .field.grow textarea{flex:1 1 auto; min-height:96px; height:auto}
.gf-h{display:flex; align-items:baseline; gap:var(--s1); flex-wrap:wrap;
  padding-bottom:var(--s1); border-bottom:1px solid var(--line)}
.gf-h b{font-size:var(--f-sm); font-weight:600}
.gf-basis{display:flex; align-items:center; gap:var(--s1); flex-wrap:wrap;
  padding:var(--s2); border:1px solid var(--line); border-radius:var(--r);
  background:var(--sunk); font-size:var(--f-xs); color:var(--ink2)}
.gf-basis b{color:var(--ink); font-weight:600}
.gf-cnt{display:flex; align-items:center; gap:var(--s2); flex-wrap:wrap}
.gf-cnt b{font-size:var(--f-xs); font-weight:600; color:var(--ink2)}
.gf-cnt .seg button{min-width:32px; justify-content:center}

/* 参考图投放区。原来是一个裸 <input type=file>，浏览器画成「未选择文件」——
   既看不出能放几张，也拖不进去。 */
.dropz{
  width:100%; font:inherit; text-align:center; cursor:pointer;
  display:flex; flex-direction:column; align-items:center; gap:6px;
  padding:var(--s2); border:1px dashed var(--line); border-radius:var(--r);
  background:var(--bg); color:var(--ink2); transition:border-color .12s, background .12s;
}
.dropz b{font-size:var(--f-xs); font-weight:600; color:var(--ink)}
.dropz .ic{color:var(--ink3)}
.dropz:hover:not(:disabled){border-color:var(--pri)}
.dropz:hover:not(:disabled) .ic{color:var(--pri)}
.dropz.over{border-color:var(--pri); background:var(--pri-soft)}
.dropz:disabled{cursor:not-allowed}

@media (max-width:900px){
  /* 窄屏上弹窗铺满：留一圈背景只会把内容挤成一条 */
  .scrim.sh-scrim{padding:0; align-items:stretch}
  .sheet{max-width:none; max-height:none; height:100%; border:0; border-radius:0}
  .sh-head,.sh-rail,.sh-body,.sh-foot{padding-left:var(--s2); padding-right:var(--s2)}
  .gf{grid-template-columns:1fr; gap:var(--s3)}
  /* 流程条在窄屏折成两行两列并收紧。四步各占一整行的话，
     光这条预告就吃掉第一屏——它是背景信息，不是主角。 */
  .flow{grid-template-columns:1fr 1fr; gap:var(--s1)}
  .fs{padding:var(--s1)}
  .fs::after{display:none}
}

/* ── 转 3D：各家自己的参数 ────────────────────────────────────────────────
   选项不是固定三档，是照着所选那一家的登记项渲染出来的（见 lib/mesh-providers）。
   两列是因为这些项彼此独立、多数很短，竖着排会把弹窗拉得很长。 */
.m3-opts{display:grid; grid-template-columns:repeat(auto-fit,minmax(240px,1fr)); gap:var(--s2)}
.m3-opt{display:flex; flex-direction:column; gap:6px; min-width:0}
.m3-opt .check{align-items:flex-start}
.m3-opt .check b{font-size:var(--f-xs); color:var(--ink)}

/* 多视图挑选。复用标准视图条的缩略图尺寸，只是把说明换成勾选框。 */
.m3-views{grid-template-columns:repeat(4,1fr)}
.m3-view{cursor:pointer; position:relative}
.m3-view.on{border-color:var(--pri); box-shadow:0 0 0 2px var(--pri-soft)}
.m3-view img{opacity:.5; transition:opacity .12s}
.m3-view.on img{opacity:1}
.m3-view .cap{display:flex; align-items:center; justify-content:center; gap:4px;
  white-space:nowrap; overflow:hidden}
.m3-view .cap input{width:13px; height:13px; margin:0; flex:none; accent-color:var(--pri)}
.m3-view.on .cap{color:var(--ink)}

/* ── 卡内进度 ────────────────────────────────────────────────────────────
   出多视图、转 3D 都是**对着某一张卡按下去的**，状态就该长在那张卡上。
   右下角那条留着管另一件事：离开这一页之后还想知道跑完没有。 */
.cprog{display:flex; flex-direction:column; gap:6px; min-width:0;
  padding:var(--s1); border:1px solid var(--line); border-radius:var(--r); background:var(--sunk)}
/* .meshrow 是 flex-wrap 的一行按钮，进度条挤在里面只能得到一条缝。
   flex-basis:100% 让它换到下一行占满整宽。 */
.meshrow .cprog{flex:1 0 100%; margin-top:4px}
/* flex-direction 必须显式写 row：方案卡自己的 .shot .hd 是 column，同样两级特异性，
   不写方向的话那条还在生效，图标、文字、百分比会竖着排成三行。
   .t 的字重字号同理——不覆盖就会套用卡片标题的样式。 */
.cprog .hd{display:flex; flex-direction:row; align-items:center; gap:6px;
  font-size:var(--f-xs); color:var(--ink2)}
.cprog .hd .t{flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
  font-size:var(--f-xs); font-weight:400; line-height:1.6}
.cprog .hd .p{font-variant-numeric:tabular-nums; color:var(--ink3); flex:none}
.cprog .track{height:4px; border-radius:999px; background:var(--line); overflow:hidden}
.cprog .track i{display:block; height:100%; background:var(--pri); border-radius:999px;
  transition:width .3s ease}
/* 不知道到哪了就来回跑，而不是画一条假的百分比——编出来的进度比没有更糟 */
.cprog .track i.indet{animation:indet 1.4s ease-in-out infinite; width:40% !important}
@keyframes indet{0%{transform:translateX(-100%)}100%{transform:translateX(250%)}}
.cprog.ok{border-color:var(--ok)} .cprog.ok .track i{background:var(--ok)}
.cprog.ok .hd .ic{color:var(--ok)}
.cprog.bad{border-color:var(--bad)} .cprog.bad .hd{color:var(--bad)}
.cprog.bad .track i{background:var(--bad)}
.cprog.warn{border-color:var(--wrn)} .cprog.warn .hd{color:var(--wrn)}
.cprog.warn .track i{background:var(--wrn)}
/* 跑完/失败之后就别再转了 */
.cprog.ok .hd .ic.spin,.cprog.bad .hd .ic.spin,.cprog.warn .hd .ic.spin{animation:none}
/* 多视图那条逐张明细：卡里放不下完整的 steps 列表，压缩成小字 */
.cprog-detail:empty{display:none}
.cprog-detail .steps{gap:2px}
.cprog-detail .step{padding:4px 6px; font-size:var(--f-xs)}
.cprog-detail .step .st{font-size:var(--f-xs)}
.cprog-detail .dim{font-size:var(--f-xs)}
.cprog-detail .note{padding:var(--s1); font-size:var(--f-xs)}

/* 一张卡可能有好几个 3D 产物（同一张图转过多次）。竖着列，别挤成一行。 */
.meshlist{display:flex; flex-direction:column; gap:6px}

/* ── 我的 CAD 模型 ────────────────────────────────────────────────────────
   一行里除了名字和动作，还要放已出的效果图缩略图——那是这一行真正的产物，
   藏进二级页面等于让人每次都多点一次才知道出没出过图。 */
/* 竖排卡片：标题行贯通、网格占整行宽。
   写成 .rows .r.cadrow 而不是 .cadrow：.rows .r 是 0,2,0，单个类压不过它。 */
.rows .r.cadrow{flex-direction:column; align-items:stretch; gap:var(--s1)}
.cadhead{display:flex; align-items:center; gap:var(--s1); min-width:0}
.cadhead > b{font-size:var(--f-sm); overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
.cadhead > .ic{flex:none}
/* 状态徽章行：关键状态（标定/材质）用 pill，次要信息（大小/人/时间）退灰字——
   原来一长串灰字把两种层级揉在一起。 */
.cadmeta{display:flex; align-items:center; gap:6px; flex-wrap:wrap; margin-top:4px}
/* 四步状态条。未完成的**下一步**是唯一的高亮——流程引导全靠它。 */
.cadsteps{display:flex; align-items:center; gap:6px; flex-wrap:wrap; margin-top:6px}
.cadstep{display:inline-flex; align-items:center; gap:5px; font-size:var(--f-xs); color:var(--ink3)}
.cadstep .dot{width:16px; height:16px; border-radius:999px; background:var(--sunk); color:var(--ink3);
  font-style:normal; font-size:10px; line-height:16px; text-align:center; flex:none;
  font-variant-numeric:tabular-nums}
.cadstep.done{color:var(--ink2)}
.cadstep.done .dot{background:var(--ok-bg); color:var(--ok)}
.cadstep.todo{color:var(--pri); font-weight:600}
.cadstep.todo .dot{background:var(--pri-soft); color:var(--pri)}
.cadstep-sep{color:var(--ink3); font-size:var(--f-xs); opacity:.5}
/* 机位 × 来源网格：一列一个机位，「直接渲」和「AI 出图」上下对齐。
   布局用 flex 列组而不是 grid——列数随机位数变，grid 的列模板得写进
   style 属性（铁律③不许），flex 天然不用；机位多时整块自己横向滚。 */
.cadgrid{display:flex; gap:6px; margin-top:var(--s2); overflow-x:auto; padding-bottom:2px}
.cadgrid-h{display:flex; flex-direction:column; gap:6px; flex:none; width:80px}
.cadcol{display:flex; flex-direction:column; gap:6px; flex:none}
.cadcol-t{height:16px; font-size:var(--f-xs); color:var(--ink2); text-align:center;
  line-height:16px; font-weight:600}
.cadgrid-r{height:112px; display:flex; flex-direction:column; justify-content:center; gap:1px}
.cadgrid-r b{font-size:var(--f-xs); color:var(--ink2); font-weight:600; line-height:1.3}
.cadgrid-r small{font-size:10px; color:var(--ink3); line-height:1.3; white-space:nowrap;
  overflow:hidden; text-overflow:ellipsis}
/* 1:1 方格——渲图和成图本来就是方图，4:3 的格子会把竖长产品的头尾裁掉，
   缩略图里只剩中段躯干（实测截图就是这样）。 */
.cadcell{position:relative; margin:0; width:112px; height:112px; border:1px solid var(--line);
  border-radius:var(--r); background:var(--sunk); overflow:hidden; flex:none}
.cadcell:hover{border-color:var(--pri)}
.cadcell.empty{display:flex; align-items:center; justify-content:center;
  border-style:dashed; background:none; color:var(--ink3); font-size:var(--f-xs)}
.cadcell-img{display:block; width:100%; height:100%; padding:0; border:0; background:none; cursor:pointer}
.cadcell-img img{width:100%; height:100%; object-fit:cover; display:block}
/* 真值角标：只有**不是**「已渲效果图」出的才标——三种来源可信度差一大截，
   但都标的话最常见的那种反而被噪声淹掉。 */
.cadcell-c{position:absolute; left:4px; bottom:4px; font-style:normal; font-size:10px; line-height:1;
  color:var(--wrn); background:var(--wrn-bg); border-radius:4px; padding:2px 5px}
/* 单张删除。悬停/聚焦才出现——它是每格都有的次要动作，常驻会糊满整个网格。 */
.cadcell-x{position:absolute; top:2px; right:2px; width:22px; height:22px; padding:0;
  border-radius:999px; background:var(--scrim); color:var(--ink2); border:0;
  opacity:0; transition:opacity .12s}
.cadcell:hover .cadcell-x,.cadcell-x:focus-visible{opacity:1}
.cadcell-x:hover:not(:disabled){color:var(--bad); background:var(--bad-bg)}
/* 出效果图弹窗里的角度预览：这张就是发出去的底图，所见即所得。 */
.shootprev{display:flex; flex-direction:column; align-items:center; gap:var(--s1);
  background:var(--sunk); border:1px solid var(--line); border-radius:var(--rc);
  padding:var(--s2); min-height:200px; justify-content:center}
.shootprev img{width:min(320px, 100%); aspect-ratio:1; border-radius:var(--r);
  border:1px solid var(--line); display:block}
/* 说明折叠。第一次点开看一遍就够了——之后每次进来先看到的应该是自己的模型。 */
.cadguide{border:1px solid var(--line); border-radius:var(--rc); background:var(--surf)}
.cadguide summary{cursor:pointer; padding:var(--s1) var(--s2); color:var(--ink2);
  list-style:none; display:flex; gap:6px; align-items:baseline; font-size:var(--f-sm)}
.cadguide summary::-webkit-details-marker{display:none}
.cadguide summary::before{content:"▸"; color:var(--ink3); flex:none; transition:transform .12s}
.cadguide[open] summary::before{transform:rotate(90deg)}
.cadguide summary b{color:var(--ink)}
.cadguide-body{padding:0 var(--s2) var(--s2); color:var(--ink2);
  display:flex; flex-direction:column; gap:var(--s1); font-size:var(--f-sm)}
/* key 与 xs 组合：.btn.key 的 44px 在源码里写在 .btn.xs 之后，同特异性会赢——
   两类选择器把小号主按钮定回小号。 */
.btn.key.xs{height:32px; padding:0 var(--s2); font-size:var(--f-xs)}
/* 拖拽悬停。.dropz 本身在「生成新一轮方案」那边已经定义过，这里只加悬停态。 */
.dropz.over{border-color:var(--pri); background:var(--pri-soft)}
/* 标定过正面的按钮。用 --ok 而不是 --pri：它陈述的是一个**已完成的状态**，
   不是一个可点的去处——虽然它仍然可以再点一次改标定。 */
.lb-tools .btn.ok{border-color:var(--ok); color:var(--ok)}

/* moodboard 缩略图。复用 .refs 的尺寸，加一个可删的角标。
   .vrm 原来只写在 .vth-wrap 下面，这里把同一份样子给 .refs .ref ——
   两处是同一个东西（一张可移除的缩略图），不该长得不一样。 */
.refs .ref{position:relative; line-height:0}
.refs .ref .vrm{
  position:absolute; top:3px; right:3px; width:20px; height:20px; padding:0; border-radius:999px;
  background:var(--scrim); color:var(--ink2); border:0; opacity:.55; transition:opacity .12s;
}
.refs .ref:hover .vrm,.refs .ref .vrm:focus-visible{opacity:1}
.refs .ref .vrm:hover:not(:disabled){color:var(--bad); background:var(--bad-bg); opacity:1}

/* ── 材质提示行 + 右键菜单 ────────────────────────────────────────────────
   指材质的主路径是查看器里的**右键菜单**（弹在手边，视线不离开件）；
   底下这一条只放操作提示和「选了什么 → 换成了什么」的反馈。 */
.matbar{border-top:1px solid var(--line); padding:var(--s1) var(--s3);
  display:flex; align-items:center; gap:var(--s2); flex-wrap:wrap; flex:none}
.matbar:empty{display:none; padding:0; border-top:0}
.mat-who{font-size:var(--f-xs); color:var(--pri); margin-left:auto;
  font-variant-numeric:tabular-nums}
/* 菜单浮在查看器之上（scrim 之内），fixed 定位贴着鼠标点。
   两段式：质感一栏、颜色一栏，自由组合、点了不关（组合是连续比出来的）。 */
.mat-menu{position:fixed; z-index:200; min-width:264px; max-height:min(560px, 76vh); overflow:auto;
  background:var(--raise); border:1px solid var(--line); border-radius:var(--rc);
  box-shadow:var(--shadow-lg); padding:6px; display:flex; flex-direction:column; gap:1px}
.mat-menu-t{font-size:var(--f-xs); color:var(--ink3); padding:4px 10px 6px;
  border-bottom:1px solid var(--line); margin-bottom:2px}
.mat-sec{font-size:10px; letter-spacing:.08em; color:var(--ink3); padding:8px 10px 4px; font-weight:600}
.mat-fins{display:grid; grid-template-columns:1fr 1fr; gap:2px; padding:0 4px}
.mat-fi{border:1px solid transparent; background:none; color:var(--ink2); font:inherit;
  font-size:var(--f-xs); padding:5px 8px; border-radius:var(--r); cursor:pointer; text-align:left}
.mat-fi:hover{background:var(--sunk); color:var(--ink)}
.mat-fi.on{border-color:var(--pri); color:var(--pri); background:var(--pri-soft)}
.mat-cols{display:grid; grid-template-columns:repeat(8, 24px); gap:4px; padding:2px 8px 6px}
.mat-ci{position:relative; width:24px; height:24px; padding:0; border-radius:6px; cursor:pointer;
  background:var(--sw); border:1px solid rgba(128,128,128,.35)}
.mat-ci:hover{outline:2px solid var(--ink3); outline-offset:1px}
.mat-ci.on{outline:2px solid var(--pri); outline-offset:1px}
/* 自定义取色：色轮示意 + 盖一个透明的原生取色 input。 */
.mat-ci.custom{display:block; overflow:hidden;
  background:conic-gradient(#e5484d,#e0a92f,#46a758,#2a9d97,#3e63dd,#8e4ec6,#e5484d)}
.mat-ci.custom input{position:absolute; inset:0; opacity:0; width:100%; height:100%; cursor:pointer}
/* 菜单项：一块色 + 名字。名字必须在——十几种黑色材质光看色块分不出哪个是包胶。 */
.mat-it{display:flex; align-items:center; gap:8px; padding:5px 10px; border:0; border-radius:var(--r);
  background:none; color:var(--ink2); font:inherit; font-size:var(--f-xs); cursor:pointer; text-align:left}
.mat-it::before{content:""; width:16px; height:16px; border-radius:999px; flex:none;
  background:var(--sw); border:1px solid rgba(128,128,128,.35)}
.mat-it:hover{background:var(--sunk); color:var(--ink)}
.mat-it.on{color:var(--pri); font-weight:600}
/* 「不显示」那一档画成斜杠，跟真材质区分开——它不是一种颜色。 */
.mat-it.none::before{background:
  linear-gradient(135deg, transparent 44%, var(--bad) 44%, var(--bad) 56%, transparent 56%), var(--sunk)}
/* 查看器工具条的分组分隔：「视角｜标定｜材质｜输出」各归各。 */
.lb-tools .tb-sep{width:1px; height:16px; background:var(--line); margin:0 2px; flex:none}

/* ── 渲染收到后台：查看器缩成右下角小窗 ──────────────────────────────────
   渲染跑在这个查看器的 renderer 里，「后台」不能是关窗——是把窗缩到角落：
   画面继续渐进（它自己就是进度条），背景不再拦截，页面照常用。
   z 高于作业条（90）：两者同角，渲染小窗是更活跃的那个。 */
.scrim.lb.lb-min{background:transparent; pointer-events:none}
.scrim.lb-min .lb-box{position:fixed; right:var(--s3); bottom:var(--s3); z-index:95;
  width:304px; pointer-events:auto}
.scrim.lb-min .lb-bar,.scrim.lb-min .matbar,.scrim.lb-min .mat-menu{display:none}
.scrim.lb-min .lb-stage.mesh{min-height:0; height:184px}
.lb-minbar{display:none}
.scrim.lb-min .lb-minbar{display:flex; align-items:center; gap:var(--s1);
  padding:var(--s1) var(--s2); border-top:1px solid var(--line)}
.lb-minbar b{font-size:var(--f-xs); font-weight:600; flex:none; max-width:104px;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
.lb-minst{flex:1; min-width:0; font-size:var(--f-xs); color:var(--pri);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-variant-numeric:tabular-nums}
/* 用已渲图当几何真值时，没渲过的机位选不了——压暗并说明，
   比直接不显示好：人要看得见「还有这几个机位，去渲一下就能用」。 */
.check.off{opacity:.4}
/* 失败过的机位。留白会被读成「还没出」，而实际是出过、失败了——
   两者该做的事完全不同（重试 vs 去出一次）。 */
.cadfail{display:flex; flex-direction:column; gap:4px; margin-top:var(--s1);
  padding:var(--s1); border-left:2px solid var(--bad); background:var(--bad-bg); border-radius:var(--r)}
.cadfail .xs{display:flex; align-items:flex-start; gap:6px; min-width:0}
.cadfail b{color:var(--ink); flex:none}
.cadfail .dim3{min-width:0; overflow-wrap:anywhere}
.cadfail-at{margin-left:auto; flex:none; color:var(--ink3); font-variant-numeric:tabular-nums}
.cadfail .btn{flex:none; margin-top:-4px}
