/* gw_test_console · 工业仪表风格 · 明暗双主题 · 零外部依赖 */
:root{
  --ground:#F4F6F8; --surface:#FFFFFF; --surface2:#EBEFF3; --surface3:#E1E7ED;
  --ink:#141A21; --muted:#5A6674; --faint:#8A94A1;
  --rule:#DBE1E8; --rule2:#C4CDD7;
  --accent:#0E6A72; --accent-bg:#E0EFEF; --accent-ink:#075158;
  --pass:#2E7D57; --pass-bg:#E2F1E9;
  --fail:#B23A48; --fail-bg:#F8E7E9;
  --warn:#A97217; --warn-bg:#F8EFDC;
  --blocked:#5B6B8A; --blocked-bg:#E7EBF2;
  --shadow:0 1px 2px rgba(18,26,34,.06),0 4px 14px rgba(18,26,34,.05);
  --mono:"Cascadia Mono","JetBrains Mono",Consolas,"Courier New",monospace;
  --sans:"Segoe UI","Microsoft YaHei",system-ui,-apple-system,sans-serif;
}
:root[data-theme="dark"]{
  --ground:#0F141A; --surface:#161C24; --surface2:#1D252F; --surface3:#242E3A;
  --ink:#E6ECF2; --muted:#98A5B4; --faint:#6E7B8A;
  --rule:#25303C; --rule2:#33404E;
  --accent:#4CA9B0; --accent-bg:#143238; --accent-ink:#8FD3D8;
  --pass:#5FBF92; --pass-bg:#15301F;
  --fail:#E4777F; --fail-bg:#321A1E;
  --warn:#D9A648; --warn-bg:#31280F;
  --blocked:#93A3C0; --blocked-bg:#1E2634;
  --shadow:0 1px 2px rgba(0,0,0,.4),0 4px 16px rgba(0,0,0,.3);
}
@media (prefers-color-scheme: dark){
  :root[data-theme="auto"]{
    --ground:#0F141A; --surface:#161C24; --surface2:#1D252F; --surface3:#242E3A;
    --ink:#E6ECF2; --muted:#98A5B4; --faint:#6E7B8A;
    --rule:#25303C; --rule2:#33404E;
    --accent:#4CA9B0; --accent-bg:#143238; --accent-ink:#8FD3D8;
    --pass:#5FBF92; --pass-bg:#15301F;
    --fail:#E4777F; --fail-bg:#321A1E;
    --warn:#D9A648; --warn-bg:#31280F;
    --blocked:#93A3C0; --blocked-bg:#1E2634;
  }
}
*{box-sizing:border-box}
html,body{margin:0;padding:0;height:100%}
body{background:var(--ground);color:var(--ink);font:14px/1.5 var(--sans);
  display:flex;flex-direction:column;height:100vh;overflow:hidden}

/* ── 顶栏 ── */
.top{display:flex;align-items:center;justify-content:space-between;gap:16px;
  padding:10px 18px;background:var(--surface);border-bottom:1px solid var(--rule);
  box-shadow:var(--shadow);z-index:20;flex:0 0 auto}
.brand{display:flex;align-items:center;gap:12px;color:var(--accent)}
.brand h1{margin:0;font-size:16px;font-weight:650;color:var(--ink);letter-spacing:.2px}
.brand .sub{margin:1px 0 0;font-size:11.5px;color:var(--muted)}
.topright{display:flex;align-items:center;gap:8px}

.chip{font:12px/1 var(--mono);padding:7px 11px;border-radius:7px;background:var(--surface2);
  border:1px solid var(--rule);cursor:pointer;white-space:nowrap;max-width:44vw;
  overflow:hidden;text-overflow:ellipsis}
.chip:hover{border-color:var(--accent);color:var(--accent-ink)}
.chip.ok{background:var(--pass-bg);color:var(--pass);border-color:transparent}
.chip.bad{background:var(--fail-bg);color:var(--fail);border-color:transparent}
.chip.warn{background:var(--warn-bg);color:var(--warn);border-color:transparent}

.btn{font:13px/1 var(--sans);padding:7px 12px;border-radius:7px;cursor:pointer;
  background:var(--surface2);color:var(--ink);border:1px solid var(--rule);
  transition:.12s;white-space:nowrap}
.btn:hover{border-color:var(--accent);color:var(--accent-ink)}
.btn:disabled{opacity:.42;cursor:not-allowed}
.btn.primary{background:var(--accent);color:#fff;border-color:transparent;font-weight:600}
.btn.primary:hover{filter:brightness(1.08);color:#fff}
.btn.danger{background:var(--fail-bg);color:var(--fail);border-color:transparent}
.btn.ghost{background:transparent}
.btn.tiny{font-size:11.5px;padding:4px 9px;border-radius:5px}

/* ── 标签页 ── */
.tabs{display:flex;gap:2px;padding:0 14px;background:var(--surface);
  border-bottom:1px solid var(--rule);flex:0 0 auto;overflow-x:auto}
.tab{font:13px/1 var(--sans);padding:10px 15px;border:0;background:transparent;
  color:var(--muted);cursor:pointer;border-bottom:2px solid transparent;white-space:nowrap}
.tab:hover{color:var(--ink)}
.tab.active{color:var(--accent);border-bottom-color:var(--accent);font-weight:600}

main{flex:1 1 auto;overflow:hidden;position:relative}
.pane{display:none;position:absolute;inset:0;overflow:auto;padding:14px}
.pane.active{display:block}

.card{background:var(--surface);border:1px solid var(--rule);border-radius:10px;
  box-shadow:var(--shadow);margin-bottom:14px;overflow:hidden}
.cardhd{display:flex;align-items:center;justify-content:space-between;gap:10px;
  padding:10px 14px;border-bottom:1px solid var(--rule);background:var(--surface);
  position:sticky;top:0;z-index:5}
.cardhd h2{margin:0;font-size:13.5px;font-weight:650;letter-spacing:.2px}
.hdacts{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.hdacts.pad{padding:8px 12px 0}
.chk{font-size:12px;color:var(--muted);display:flex;align-items:center;gap:5px;cursor:pointer}
.muted{color:var(--muted)}
.pad{padding:10px 14px}

/* ── 三栏 ── */
.cols{display:grid;grid-template-columns:290px minmax(0,1fr) 330px;gap:14px;height:100%}
.cols.proc{grid-template-columns:260px minmax(0,1fr)}
.cols.msg{grid-template-columns:290px minmax(0,1fr)}
.col-left,.col-main,.col-right{min-width:0;margin-bottom:0;display:flex;flex-direction:column;
  max-height:100%}
.col-left>.tree,.col-left>.joblist,.col-left>.caplist{overflow:auto;flex:1 1 auto}
.col-main{overflow:hidden}
.col-right>.detail{overflow:auto}
.grid2{display:grid;grid-template-columns:repeat(auto-fit,minmax(430px,1fr));gap:14px}

/* ── 树 ── */
.filterbar{display:flex;gap:6px;padding:8px 12px;border-bottom:1px solid var(--rule)}
.inp{font:12.5px/1.3 var(--sans);padding:6px 9px;border-radius:6px;border:1px solid var(--rule2);
  background:var(--surface);color:var(--ink);width:100%;min-width:0}
.inp.sm{width:auto;min-width:120px}
/* ④ 过程报文新增的两个时间框：datetime-local 的默认宽度把工具栏挤到两行，给它一个够放
   "2026-10-18 14:03:05" 的固定宽（步长 1 秒 ⇒ 必须显示到秒）。 */
input[type="datetime-local"].inp{width:172px;min-width:172px;font-family:var(--mono)}
.inp:focus{outline:none;border-color:var(--accent)}
.tree{font-size:12.5px}
.chap{border-bottom:1px solid var(--rule)}
.chaphd{display:flex;align-items:center;gap:7px;padding:8px 12px;cursor:pointer;
  background:var(--surface2);font-weight:600;font-size:12.5px}
.chaphd:hover{background:var(--surface3)}
.chaphd .n{margin-left:auto;font:11px var(--mono);color:var(--faint)}
.chaphd .tick{width:13px;height:13px;flex:0 0 auto}
.cl{border-top:1px solid var(--rule)}
.clrow{display:flex;align-items:flex-start;gap:7px;padding:6px 12px 6px 16px;cursor:pointer}
.clrow:hover{background:var(--surface2)}
.clrow input{margin-top:2px;flex:0 0 auto}
.clrow .cid{font:11.5px var(--mono);color:var(--accent-ink);flex:0 0 auto;padding-top:1px}
.clrow .ctext{min-width:0;font-size:12px;line-height:1.35}
/* 副标题 = 规范要求正文的首句摘录。Q/GDW 12589.4 与 11627 的条款没有叶标题，
   名称列是章节名，光看名称连着一排"数据接入"根本挑不出来。
   用灰色小字与名称分开，让人一眼看出哪句是规范标题、哪句是正文摘录。 */
.clrow .chint{display:block;color:var(--muted);font-size:11px;margin-top:1px;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.clrow .cat{margin-left:auto;font:10px var(--mono);color:var(--faint);flex:0 0 auto;
  padding:1px 5px;border-radius:4px;background:var(--surface2)}
.clrow.nomap .cid{color:var(--fail)}
/* 「真缺口」与「客观测不了」必须看得出区别：前者是待办，用实心红字标出来；
   后者只是"这台机器上测不了"，保持灰。原来两者同色，缺口会被看成正常。 */
.clrow.nogap .cid{color:var(--fail);font-weight:700}
.clrow .nt{font:10px var(--mono)}
.clrow .nt-instrument{color:var(--faint)}
.clrow .nt-scope{color:var(--faint)}
.clrow .nt-gap{color:var(--fail);font-weight:700}
.chap.hidden,.cl.hidden{display:none}

/* ── 工具列表 ── */
.toollist{padding:6px 0;max-height:44vh;overflow:auto}
.toolrow{display:flex;align-items:flex-start;gap:9px;padding:8px 14px;cursor:pointer;
  border-bottom:1px solid var(--rule)}
.toolrow:hover{background:var(--surface2)}
.toolrow.sel{background:var(--accent-bg)}
.toolrow .tid{font:11px var(--mono);color:var(--faint);flex:0 0 auto;width:30px}
.toolrow .tbody{min-width:0;flex:1 1 auto}
.toolrow .tname{font-size:12.5px;line-height:1.35}
.toolrow .tmeta{font:11px var(--mono);color:var(--faint);margin-top:2px;word-break:break-all}
.badge{font:10.5px var(--mono);padding:2px 6px;border-radius:4px;white-space:nowrap;flex:0 0 auto}
.badge.ready{background:var(--pass-bg);color:var(--pass)}
.badge.blocked{background:var(--fail-bg);color:var(--fail)}
.badge.entry_missing{background:var(--fail-bg);color:var(--fail)}
.badge.builtin{background:var(--blocked-bg);color:var(--blocked)}

/* ── 运行条 ── */
.runbar{display:flex;align-items:center;justify-content:space-between;gap:14px;
  padding:10px 14px;border-bottom:1px solid var(--rule);background:var(--surface2);
  font-size:12.5px;flex-wrap:wrap}
.selectionsum{color:var(--ink)}
.selectionsum b{color:var(--accent-ink)}
.plansum{font:11.5px var(--mono);color:var(--muted)}
.steps{padding:8px 12px;max-height:30vh;overflow:auto}
.step{display:flex;align-items:center;gap:9px;padding:6px 8px;border-radius:6px;font-size:12.5px}
.step+.step{border-top:1px solid var(--rule)}
.step.run{background:var(--accent-bg)}
.step .sn{font:11px var(--mono);color:var(--faint);width:22px;flex:0 0 auto;text-align:right}
.step .sname{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.step .scl{font:10.5px var(--mono);color:var(--faint);max-width:38%;overflow:hidden;
  text-overflow:ellipsis;white-space:nowrap}
.step .lnk{font:11.5px var(--accent);cursor:pointer;text-decoration:underline}
.step.off{opacity:.6}
.step.warnbar{background:var(--warn-bg);border-radius:7px;align-items:flex-start}
.step.warnbar .sname{color:var(--warn);white-space:normal;font-size:12px}
.step.errbar{background:var(--fail-bg);border-radius:7px;align-items:flex-start}
.step.errbar .sname{color:var(--fail);white-space:normal;font-size:12px}

/* ── 详情 ── */
.detail{padding:12px 14px;font-size:12.5px}
.detail h3{margin:0 0 4px;font-size:13px}
.detail .kv{display:grid;grid-template-columns:88px minmax(0,1fr);gap:4px 8px;margin:8px 0}
.detail .kv dt{color:var(--muted);font-size:11.5px}
.detail .kv dd{margin:0;word-break:break-word}
.arg{border:1px solid var(--rule);border-radius:7px;padding:8px 10px;margin:7px 0;background:var(--surface2)}
.arg label{display:block;font-size:11.5px;color:var(--muted);margin-bottom:4px}
.arg .help{font-size:10.5px;color:var(--faint);margin-top:4px;line-height:1.4}
.arg input[type=text],.arg input[type=number],.arg select{width:100%}
.cmdprev{font:11px var(--mono);background:var(--surface3);padding:7px 9px;border-radius:6px;
  margin-top:8px;white-space:pre-wrap;word-break:break-all;color:var(--muted)}

/* ── 网关启动模式 ── */
.gwmodes{display:grid;grid-template-columns:1fr 1fr;gap:0;border-bottom:1px solid var(--rule)}
.gwmode{display:flex;gap:9px;padding:11px 14px;cursor:pointer;font-size:12px;
  align-items:flex-start;border-right:1px solid var(--rule)}
.gwmode:last-child{border-right:0}
.gwmode:hover{background:var(--surface2)}
.gwmode.on{background:var(--accent-bg)}
.gwmode input{margin-top:3px;flex:0 0 auto}
.gwmode b{display:block;font-size:12.5px;margin-bottom:3px}
.gwmode .muted{font-size:11.5px;line-height:1.45;display:block}
.gwmode code{font:11px var(--mono);background:var(--surface3);padding:0 3px;border-radius:3px}
.netseg{border:1px solid var(--rule);border-radius:7px;padding:9px 11px;margin:7px 0;
  background:var(--surface2)}
.netseg .nh{display:flex;align-items:center;gap:9px;flex-wrap:wrap}
.netseg .nseg{font:12px var(--mono);color:var(--accent-ink);font-weight:600}
.netseg .nmeta{font:10.5px var(--mono);color:var(--faint);margin-top:4px}
.netseg .npurpose{font-size:11.5px;color:var(--muted);margin-top:4px;line-height:1.4}
.cmdlist{font:10.5px var(--mono);background:var(--surface3);padding:8px 10px;
  border-radius:6px;margin-top:7px;max-height:190px;overflow:auto;white-space:pre-wrap;
  word-break:break-all}

/* ── 模型导入列表 ── */
.modellist{max-height:34vh;overflow:auto;border-top:1px solid var(--rule)}
.modelrow{display:flex;align-items:center;gap:12px;padding:8px 14px;
  border-bottom:1px solid var(--rule);cursor:pointer}
.modelrow:hover{background:var(--surface2)}
.modelrow.sel{background:var(--accent-bg);border-left:3px solid var(--accent)}
.modelrow .mleft{min-width:0;flex:1 1 auto}
.modelrow .mname{font-size:12.5px;display:flex;align-items:center;gap:7px;flex-wrap:wrap}
.modelrow .mpath{font:10.5px var(--mono);color:var(--faint);word-break:break-all;margin-top:2px}
.modelrow .mreasons{font-size:11px;color:var(--muted);margin-top:3px;line-height:1.4}
.modelrow .mright{display:flex;align-items:center;gap:10px;flex:0 0 auto}
.modelrow .mmeta{font:10.5px var(--mono);color:var(--faint);text-align:right;line-height:1.4}

/* ── 环境面板 ── */
.envbox{padding:12px 14px;font-size:12.5px}
.envbox .row{display:flex;gap:10px;padding:6px 0;border-bottom:1px solid var(--rule)}
.envbox .row:last-child{border-bottom:0}
.envbox .k{color:var(--muted);flex:0 0 118px;font-size:11.5px}
.envbox .v{min-width:0;flex:1 1 auto;word-break:break-word;font-family:var(--mono);font-size:11.5px}
.pill{display:inline-block;font:11px var(--mono);padding:2px 7px;border-radius:4px;margin:2px 4px 2px 0}
.pill.ok{background:var(--pass-bg);color:var(--pass)}
.pill.bad{background:var(--fail-bg);color:var(--fail)}
.pill.warn{background:var(--warn-bg);color:var(--warn)}
.pill.info{background:var(--surface2);color:var(--muted)}
.alert{border-left:3px solid var(--warn);background:var(--warn-bg);padding:8px 11px;
  border-radius:0 7px 7px 0;margin:7px 0;font-size:12px}
.alert.err{border-color:var(--fail);background:var(--fail-bg)}
.alert.ok{border-color:var(--pass);background:var(--pass-bg)}
.alert pre{margin:4px 0 0;font:11px var(--mono);white-space:pre-wrap;word-break:break-word}
details.cand{margin-top:6px}
details.cand summary{cursor:pointer;font-size:11.5px;color:var(--muted)}
details.cand .cp{font:10.5px var(--mono);padding:2px 0 2px 10px;word-break:break-all}
details.cand .cp.miss{color:var(--faint);text-decoration:line-through}

/* ── 过程流 ── */
.joblist{overflow:auto;flex:1 1 auto;font-size:12.5px}
.jobrow{padding:8px 12px;border-bottom:1px solid var(--rule);cursor:pointer}
.jobrow:hover{background:var(--surface2)}
.jobrow.sel{background:var(--accent-bg)}
.jobrow .jname{font-size:12px;line-height:1.35;word-break:break-word}
.jobrow .jmeta{font:10.5px var(--mono);color:var(--faint);margin-top:3px;display:flex;
  gap:7px;flex-wrap:wrap;align-items:center}
.procmeta{padding:8px 14px;border-bottom:1px solid var(--rule);background:var(--surface2);
  font:11.5px var(--mono);color:var(--muted);word-break:break-all}
.stream{font:12px/1.55 var(--mono);padding:10px 14px;overflow:auto;flex:1 1 auto;
  background:var(--surface);min-height:0;max-height:none}
.ln{display:flex;gap:9px;padding:1px 0;border-radius:3px;word-break:break-word}
.ln .lv{flex:0 0 auto;width:52px;font-size:10px;text-align:right;color:var(--faint);padding-top:2px}
.ln .tx{min-width:0;white-space:pre-wrap;word-break:break-word}
.ln.phase{background:var(--accent-bg);color:var(--accent-ink);font-weight:600}
.ln.verdict{color:var(--ink)}
.ln.warn{background:var(--warn-bg);color:var(--warn)}
.ln.error{background:var(--fail-bg);color:var(--fail);font-weight:600}
.ln.raw{color:var(--muted)}
.stream.hideRaw .ln.raw{display:none}
/* ── ③ 过程信息：分步输出块 + 分步台账（第 15 轮）────────────────────────
   为什么要有这两个东西：一轮计划 7 步，旧界面只有一个容器，**每结束一步就把整个面板
   清空一次**（见 app.js 的 attachJobStream 注释），跑完只剩最后一步；而且没有任何地方
   能一眼看出"这一轮到底有哪几步、各是什么结论、哪条条款归谁"。 */
.stepblk{border:1px solid var(--rule);border-radius:6px;margin:0 0 10px;overflow:hidden}
.stepblk.planlog{border-color:var(--accent)}
.stepblk.flash{box-shadow:0 0 0 2px var(--accent)}
.stepblkhd{display:flex;flex-wrap:wrap;align-items:center;gap:6px;
  background:var(--surface2);padding:6px 10px;font:11.5px var(--mono);color:var(--muted);
  border-bottom:1px solid var(--rule)}
.stepblk.closed .stepblkbody{display:none}
.stepblkhd a{color:var(--accent-ink);cursor:pointer}
.stepblkbody{padding:6px 10px;background:var(--surface)}
.stepmeta{font:11px var(--mono);color:var(--muted);padding:4px 0;border-bottom:1px dashed var(--rule);
  margin-bottom:4px;word-break:break-all}
table.mt.sm{font-size:11.5px}
table.mt.sm td,table.mt.sm th{padding:3px 6px}
/* ══════════ 第 16 轮：条款目标卡 / 折叠徽标 / 超长行 / 覆盖体检 ══════════
   存在的理由（都在真数据上量过，见 .dsh_tmp/r16_*.json）：
   · `检测目标` 在 2016 份真日志里 0 命中 ⇒ 目标卡是"关联性"唯一的落点，必须显眼；
   · 55.77% 的非空行是同一句话的复制 ⇒ ×N 徽标；最长单行 8,204 字符 ⇒ 折起来可点开。 */
.objcard{margin:2px 0 6px;border:1px solid var(--rule);border-radius:5px;background:var(--accent-bg)}
.objcard>summary{cursor:pointer;padding:5px 8px;font:11.5px var(--mono);color:var(--accent-ink);font-weight:700}
.objrow{padding:4px 10px 8px;border-top:1px dashed var(--rule)}
.objhd{display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin:2px 0 3px;font-size:12px}
.objtx{font:11.5px var(--mono);color:var(--ink);margin:2px 0;line-height:1.55;word-break:break-word}
.objtx .lb{display:inline-block;min-width:58px;margin-right:6px;padding:0 5px;border-radius:3px;
  background:var(--surface2);color:var(--muted)}
.objmini{margin-top:2px}
.objmini>summary{cursor:pointer;color:var(--accent-ink);font:11.5px var(--mono)}
.ruler{display:flex;align-items:center;gap:6px;padding:3px 10px;font:11.5px var(--mono)}
.ruler .muted{word-break:break-all}
.ln.rep .repn{margin-left:6px;font-weight:700}
.ln .fulltoggle{color:var(--accent-ink);cursor:pointer;font-size:11px}
.ln .fulltext{white-space:pre-wrap;word-break:break-all;max-height:220px;overflow:auto;
  margin:4px 0 0;padding:6px 8px;background:var(--surface2);border-radius:4px;font:11px var(--mono)}
.covlist{max-height:420px;overflow:auto;margin-top:6px}
.covlist .pill{margin-right:3px}
/* "已折叠 N 行"的提示条。存在的理由：工具**通过时输出本来就少**，勾着"只看关键"
   很容易只剩标题+汇总，面板像空的；必须明说折叠了多少，否则用户会以为过程丢了。 */
.foldhint{padding:6px 14px;border-bottom:1px solid var(--rule);background:var(--accent-bg);
  color:var(--accent-ink);font:11.5px var(--mono)}
.foldhint b{font-weight:700}

/* ══════════ 第 17 轮：过程树（条款 → 测试工具 → 过程）与左侧导航树 ══════════
   现场第 8 问："请改用树形方式显示和管理每一个条款的每一个测试工具的测试过程信息。"
   为什么要树：一轮计划里条款与工具是**多对多**（一个工具覆盖多条条款、一条条款被多个
   工具覆盖），平铺成列表后"这条条款到底有几个工具、各是什么结论"必须靠人横向对齐；
   而 ② 每步几万行正文又不能全铺开 ⇒ 树是"结构可见 + 正文按需展开"唯一同时满足两点的形状。 */
.navmode{display:flex;gap:6px;padding:6px 10px;border-bottom:1px solid var(--rule)}
.navmode .btn.on{background:var(--accent-bg);color:var(--accent-ink);font-weight:700}
.clausetree{overflow:auto;flex:1 1 auto;font-size:12.5px;padding:4px 6px}
.clausetree .ctclause{margin:1px 0}
.clausetree .ctrow{display:flex;align-items:center;gap:5px;padding:3px 6px;border-radius:4px;
  cursor:pointer;line-height:1.4}
.clausetree .ctrow:hover{background:var(--surface2)}
.clausetree .ctrow.sel{background:var(--accent-bg);box-shadow:inset 2px 0 0 var(--accent)}
.clausetree .ctrow .ctitle{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--muted)}
.clausetree .ctkid{padding:1px 6px 1px 22px;cursor:pointer;border-radius:4px;
  display:flex;align-items:center;gap:5px;line-height:1.4}
.clausetree .ctkid:hover{background:var(--surface2)}
.clausetree .ctkid.sel{background:var(--accent-bg);box-shadow:inset 2px 0 0 var(--accent)}
.clausetree .ctkid .ctool{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.clausetree .ctkid .cjob{color:var(--faint);font:10.5px var(--mono)}
.clausetree .ctclosed>.ctkids{display:none}
.clausetree .ctkids{display:block}
.tgl{flex:0 0 auto;width:12px;color:var(--faint);cursor:pointer;user-select:none;font-size:10px}
.treebar{display:flex;flex-wrap:wrap;align-items:center;gap:8px;padding:7px 14px;
  border-bottom:1px solid var(--rule);background:var(--surface2);font:11.5px var(--mono)}
.treebar .sp{margin-left:auto}
.treebar a{color:var(--accent-ink);cursor:pointer}
.treebar .btn.on{background:var(--accent-bg);color:var(--accent-ink);font-weight:700}
.proctree{padding:8px 10px}
.tnode{margin:0 0 8px}
.tnode>.trow{display:flex;flex-wrap:wrap;align-items:center;gap:6px;padding:5px 8px;
  border:1px solid var(--rule);border-radius:5px;background:var(--surface2);cursor:pointer}
.tnode>.trow:hover{border-color:var(--accent)}
.tnode.sel>.trow{box-shadow:0 0 0 2px var(--accent)}
.tnode.flash>.trow{box-shadow:0 0 0 2px var(--accent);background:var(--accent-bg)}
.tnode>.trow .tname{font-weight:700}
.tnode>.trow .tacts{margin-left:auto;font:11px var(--mono);color:var(--accent-ink)}
.tnode>.trow .tacts a{margin-left:8px;cursor:pointer}
.tnode>.tkids{margin:6px 0 10px 16px;border-left:1px dashed var(--rule);padding-left:8px}
.tnode.closed>.tkids{display:none}
.tnode.ttool>.trow{background:var(--surface);font:12.5px var(--mono)}
.tnode.ttool>.tbody{margin:4px 0 8px 14px}
.tnode.closed>.tbody{display:none}
.tobj{font:11.5px var(--mono);color:var(--ink);margin:2px 0 4px 16px;line-height:1.55;
  word-break:break-word}
.tobj .lb{display:inline-block;min-width:64px;margin-right:6px;padding:0 5px;border-radius:3px;
  background:var(--surface2);color:var(--muted)}
.tmiles{margin:2px 0 6px;padding:5px 8px;border:1px solid var(--rule);border-radius:4px;
  background:var(--surface2)}
.tmiles .tl{font:11px var(--mono);color:var(--muted)}
.tmiles .tm{font:11.5px/1.5 var(--mono);color:var(--ink);white-space:pre-wrap;word-break:break-word}
.tbody .ln.skel{opacity:.62}
/* 过滤：按结论隐藏节点（"只看有问题的"是现场最常用的一眼）。
   注意这只是**显示**过滤，节点数据与正文一个字节都没删（与"只看关键"同一纪律）。 */
.proctree[data-filter="bad"] .tnode[data-bad="0"],
.proctree[data-filter="verdict"] .tnode[data-verdicted="0"],
.proctree[data-filter="norun"] .tnode[data-norun="0"]{display:none}
.tlazy{margin:2px 0 6px;font:11.5px var(--mono);color:var(--muted)}
.tlazy a{color:var(--accent-ink);cursor:pointer}
/* 详情栏顶部的"本栏只显示什么"（现场第 8 问：选条款看整条、选工具看一个）。
   这一行必须在，否则用户分不清眼前是"整轮"还是"某一条" —— 两把尺子的老坑。 */
.crumb{padding:6px 10px;margin:0 0 6px;border:1px solid var(--rule);border-radius:5px;
  background:var(--surface2);font:11.5px/1.6 var(--mono);color:var(--ink)}
.crumb b{color:var(--accent-ink)}
/* 计划级骨架行（开始第 N 步 / 跳过 / 计划结束）：**自己的固定宿主**，不随选中范围消失。
   空的时候不占地方（`:empty`）。 */
.skelhost{border:1px solid var(--rule);border-radius:6px;margin:8px 0 0;background:var(--surface2);
  font:12px/1.55 var(--mono);overflow:auto;max-height:30vh}
.skelhost:empty{display:none}
.skelhost .ln{padding:1px 8px}
/* 左侧导航的"整轮计划"根节点：与条款行区分开（它不是一个条款）。 */
.clausetree .ctclause.ctroot>.ctrow{background:var(--surface2);font-weight:600;
  border-bottom:1px solid var(--rule);border-radius:4px}
.clausetree .ctclause.ctroot>.ctrow.sel{background:var(--accent-bg)}
/* "只这一条款/工具"的头行不可折叠（它是范围声明，不是可展开的节点）。 */
.tnode[data-solo]>.trow{cursor:default}
.tnode[data-solo]>.trow:hover{border-color:var(--rule)}
/* 弹窗里的小节标题（检测报告的"各项 / 子项"两节） */
.sechd{margin:14px 0 6px;font-size:13px;color:var(--ink);border-bottom:1px solid var(--rule);
  padding-bottom:4px}
.vPASS{color:var(--pass);font-weight:700}
.vFAIL{color:var(--fail);font-weight:700}
.vBLOCKED{color:var(--blocked);font-weight:700}
.vUNKNOWN{color:var(--warn);font-weight:700}

/* ── 报文 ── */
.caplist{overflow:auto;flex:1 1 auto;font-size:12.5px}
.caprow{padding:8px 12px;border-bottom:1px solid var(--rule);cursor:pointer}
.caprow:hover{background:var(--surface2)}
.caprow.sel{background:var(--accent-bg)}
.caprow .cname{font-size:12px;word-break:break-word;line-height:1.35}
.caprow .cmeta{font:10.5px var(--mono);color:var(--faint);margin-top:3px}
.msgtats{padding:8px 14px;border-bottom:1px solid var(--rule);background:var(--surface2);
  font-size:11.5px;line-height:1.7}
.msgtable{overflow:auto;flex:1 1 auto;font-size:11.5px}
table.mt{width:100%;border-collapse:collapse;font-family:var(--mono)}
table.mt th{position:sticky;top:0;background:var(--surface3);text-align:left;
  padding:6px 8px;font-size:10.5px;color:var(--muted);border-bottom:1px solid var(--rule2);
  white-space:nowrap;z-index:2}
table.mt td{padding:5px 8px;border-bottom:1px solid var(--rule);vertical-align:top;
  word-break:break-word}
table.mt tr:hover td{background:var(--surface2)}
/* 表格里的协议 pill 要紧凑：默认 .pill 的 margin 在每行格里会把列撑宽。 */
table.mt td .pill{margin:0;padding:1px 6px}
td.dir-tx{color:var(--accent-ink)}
td.dir-rx{color:var(--pass)}
td.k-I{color:var(--accent-ink);font-weight:600}
td.k-S{color:var(--muted)}
td.k-U{color:var(--warn);font-weight:600}
.detailrow{background:var(--surface2)}
.detailrow td{font-size:11px}
.detailrow pre{margin:0;white-space:pre-wrap;word-break:break-word;max-height:260px;overflow:auto}

/* ── 状态总览 ── */
.clausestatus{padding:10px 14px;font-size:12px}
.csgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(268px,1fr));gap:8px}
.cscard{border:1px solid var(--rule);border-radius:7px;padding:8px 10px;font-size:11.5px}
.cscard .csh{display:flex;align-items:center;gap:7px}
.cscard .csc{font:11px var(--mono);color:var(--accent-ink)}
.cscard .cst{margin:4px 0 0;line-height:1.35;color:var(--muted)}
.cscard .csb{margin-top:5px;display:flex;gap:5px;flex-wrap:wrap;font:10px var(--mono)}
.cscard.pass{border-left:3px solid var(--pass)}
.cscard.fail{border-left:3px solid var(--fail)}
.cscard.blocked{border-left:3px solid var(--blocked)}
.cscard.none{border-left:3px solid var(--rule2)}

/* ── 模态 ── */
.modal{position:fixed;inset:0;background:rgba(10,14,19,.55);display:flex;
  align-items:center;justify-content:center;z-index:100;padding:24px}
.modal.hidden{display:none}
.modalbox{max-width:min(1100px,94vw);max-height:88vh;display:flex;flex-direction:column;
  margin:0}
.modalbody{overflow:auto;padding:14px;font-size:12.5px}
.modalbody pre{font:11.5px var(--mono);white-space:pre-wrap;word-break:break-word;
  background:var(--surface2);padding:10px;border-radius:7px;max-height:60vh;overflow:auto}

.toast{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);z-index:200;
  background:var(--ink);color:var(--surface);padding:10px 18px;border-radius:8px;
  font-size:12.5px;box-shadow:var(--shadow);max-width:80vw}
.toast.hidden{display:none}

/* ── 「测试进行中」横幅（2026-10-19 现场第 5 问第 1、2 条）──
   为什么要有它：刷新页面后前端会丢掉"谁在跑"，而服务端还在跑；工具又可能几分钟
   不往 stdout 写一行（它们把输出写进自己的日志文件）⇒ 光看过程面板，安静与卡死
   无法区分。这条横幅的秒表**每秒自己走**，不依赖任何事件，所以"还在测"这件事
   在任何时刻都看得见；点它跳到过程信息。 */
.runbanner{position:fixed;top:10px;left:50%;transform:translateX(-50%);z-index:150;
  background:var(--warn,#a86a00);color:#fff;padding:7px 14px;border-radius:8px;
  font-size:12.5px;box-shadow:var(--shadow);cursor:pointer;max-width:92vw}
.runbanner .u{text-decoration:underline}
.runbanner.hidden{display:none}
.empty{padding:22px 14px;color:var(--faint);font-size:12.5px;text-align:center}

/* ── 远程访问：登录层 / 二次确认 / 审计表（见 README §13） ──
   两层遮罩都放在模态(100)与 toast(200)之上：远程会话没登录时，底下那十几个
   401 的 toast 不该穿过登录卡片影响判断。 */
.chip.hidden{display:none}
.gate{position:fixed;inset:0;background:rgba(10,14,19,.62);display:flex;
  align-items:center;justify-content:center;z-index:400;padding:20px}
.gate.hidden{display:none}
.gatebox{width:min(640px,94vw);max-height:90vh;overflow:auto;margin:0;
  box-shadow:var(--shadow)}
.gatebody{padding:14px}
.gateacts{display:flex;gap:10px;justify-content:flex-end;margin-top:14px}
.confirm-reason{font-size:13px;line-height:1.5;margin:2px 0 12px}
.loginerr{color:var(--fail);font-size:12px;min-height:18px;margin:8px 0 2px}
.logininfo{font:11.5px var(--mono);color:var(--muted);line-height:1.75;margin-top:8px;
  border-top:1px solid var(--rule);padding-top:10px;word-break:break-word}
.modalbody .row{display:flex;gap:10px;padding:6px 0;border-bottom:1px solid var(--rule)}
.modalbody .row:last-child{border-bottom:0}
.modalbody .k{color:var(--muted);flex:0 0 96px;font-size:11.5px}
.modalbody .v{min-width:0;flex:1 1 auto;word-break:break-word;font-size:12px}
table.tbl{width:100%;border-collapse:collapse;font-size:11.5px}
table.tbl th{position:sticky;top:0;background:var(--surface3);text-align:left;
  padding:5px 8px;font:11px var(--mono);color:var(--muted)}
table.tbl td{padding:5px 8px;border-bottom:1px solid var(--rule);vertical-align:top;
  word-break:break-word}
table.tbl tr:hover td{background:var(--surface2)}

/* ══════════ ③ 测试过程：右侧过程信息窗占满右列（现场 2026-10-21）══════════
   现场原话：「测试过程」页面右侧下部的过程信息窗口太小，应占据右侧全部。

   旧布局的病因（全是 flex 压缩规则，不是数据问题）：
     · .stream 有 min-height:220px + max-height:calc(100vh - 260px)，
       又被 flex:1 1 auto 允许收缩 ⇒ 上面的块一变高，它先被压到 220px 就不再让；
     · 上面的块（#procMeta/#procSkel/#foldHint）是默认的 flex:0 1 auto
       ⇒ 它们也被压，但压到 min-content 就停 —— 结果是"谁都小一点、谁都没占满"，
       右列底部留一大片空白。
   现在：过程窗之上的块一律 flex:0 0 auto（不参与压缩）+ 各自上限 + 自己滚，
   剩余高度全部给过程窗（flex:1 1 auto + min-height:0 + 无上限）。 */
.cols.proc{height:100%;min-height:0;align-items:stretch}
.cols.proc>.col-main{height:100%;min-height:0}
.cols.proc>.col-left{min-height:0}
.cols.proc #procMeta{flex:0 0 auto;max-height:24vh;overflow:auto}
.cols.proc #procSkel{flex:0 0 auto;max-height:20vh}
.cols.proc #foldHint{flex:0 0 auto}
.cols.proc #procTreeBar{flex:0 0 auto}
/* 过程窗：占满右列剩余的全部高度（不再是 220px 的小窗口）。 */
.cols.proc>.col-main>.stream{flex:1 1 auto;min-height:0;max-height:none}

/* ★ 现场（2026-10-21 续）：「本轮计划」栏整条删掉了（"只保留过程信息滚动"）。
   它里面的**两把尺子**搬进了树形工具条，压成一枚 pill；长了就带"…"（悬停看全），
   不许悄悄截成假信息。 */
.rulerpill{font:11.5px var(--mono);color:var(--muted);border:1px solid var(--rule);
  border-radius:10px;padding:1px 8px;max-width:46vw;overflow:hidden;text-overflow:ellipsis;
  white-space:nowrap}
.rulerpill b{color:var(--fg)}
