:root{
  color-scheme: light;
  --bg:#EEF1F6;
  --surface:#FFFFFF;
  --surface-2:#E7EBF2;
  --line:#D6DCE6;
  --line-soft:#E5E9F1;
  --ink:#151922;
  --ink-2:#4A5464;
  --ink-3:#7C8598;
  --veil:rgba(22,28,42,.34);
  --shadow-1:0 1px 2px rgba(16,20,32,.05);
  --shadow-2:-6px 0 24px rgba(16,20,32,.07);
  --shadow-3:0 26px 64px rgba(10,14,24,.26);
  /* 分组配色不在这里 —— 图谱自己的 groups[].color 决定，
     引擎 mount 时注入成 --g-<key>（浅色/深色两套）。 */
  --code-bg:#EBEFF6;
  --warn:#A8560F;
  --warn-bg:#F6ECE0;
  /* 任务状态用的两套语义色（技能区）。和 --warn 一样三处都写，
     不写在 [data-theme] 里 —— 只在那儿定义的色值，在「跟随系统」
     这条路上永远不生效。 */
  --live:#2A5CBE;  --live-bg:#E4EAF8;
  --ok:#2F6B3A;    --ok-bg:#E4EFE6;
}
@media (prefers-color-scheme: dark){
  :root:not([data-theme="light"]){
    color-scheme: dark;
    --bg:#0B0E14; --surface:#141922; --surface-2:#1C222D;
    --line:#2A323F; --line-soft:#212834;
    --ink:#E7EAF1; --ink-2:#A6B0C1; --ink-3:#6C7688;
    --veil:rgba(0,0,0,.62);
    --shadow-1:0 1px 2px rgba(0,0,0,.45);
    --shadow-2:-6px 0 24px rgba(0,0,0,.4);
    --shadow-3:0 26px 64px rgba(0,0,0,.6);
    --code-bg:#1A212E;
    --warn:#E9A85E; --warn-bg:#2A2116;
    --live:#7FA8F2; --live-bg:#161F2E;
    --ok:#7FCF8E;   --ok-bg:#16241A;
  }
}
:root[data-theme="dark"]{
  color-scheme: dark;
  --bg:#0B0E14; --surface:#141922; --surface-2:#1C222D;
  --line:#2A323F; --line-soft:#212834;
  --ink:#E7EAF1; --ink-2:#A6B0C1; --ink-3:#6C7688;
  --veil:rgba(0,0,0,.62);
  --shadow-1:0 1px 2px rgba(0,0,0,.45);
  --shadow-2:-6px 0 24px rgba(0,0,0,.4);
  --shadow-3:0 26px 64px rgba(0,0,0,.6);
  --code-bg:#1A212E;
  --warn:#E9A85E; --warn-bg:#2A2116;
  --live:#7FA8F2; --live-bg:#161F2E;
  --ok:#7FCF8E;   --ok-bg:#16241A;
}

*{box-sizing:border-box}
html,body{height:100%}
body{
  margin:0;background:var(--bg);color:var(--ink);
  font-family:"IBM Plex Sans","PingFang SC","Microsoft YaHei","Noto Sans SC",system-ui,sans-serif;
  font-size:14px;line-height:1.62;-webkit-font-smoothing:antialiased;overflow:hidden;
}
h1,h2,h3{margin:0;font-family:"Archivo","IBM Plex Sans","PingFang SC","Microsoft YaHei",sans-serif;letter-spacing:-.015em;text-wrap:balance}
.mono{font-family:"IBM Plex Mono",ui-monospace,SFMono-Regular,Menlo,monospace}
:focus-visible{outline:2px solid var(--ink-2);outline-offset:2px;border-radius:6px}
button{font:inherit;color:inherit}

/* 加了对比、右栏要变宽，所以列宽得能过渡 */
/* 默认没有右栏：打开就是整屏的模型。加了对比才把右栏推出来 */
.app{
  height:100%;display:grid;grid-template-columns:minmax(0,1fr) 0px;
  grid-template-rows:minmax(0,1fr);
  transition:grid-template-columns .46s cubic-bezier(.22,.75,.25,1);
}
.app.rail{grid-template-columns:minmax(0,1fr) minmax(340px,28rem)}
.app.rail.wide{grid-template-columns:minmax(0,1fr) minmax(520px,57%)}
.stage{position:relative;display:flex;flex-direction:column;min-width:0;min-height:0;background:var(--bg)}

/* ── 顶栏 / 底栏 ──────────────────────────────────────── */
/* 模型正上方：标题、计数、搜索框，一律居中，和球心对在一条竖线上 */
.bar{flex:none;display:flex;flex-direction:column;align-items:center;gap:10px;padding:16px 22px 10px;text-align:center}
.brand{display:flex;flex-direction:column;gap:1px;min-width:0;align-items:center}
.brand h1{font-size:17px;font-weight:700}
.brand .count{font-size:12px;color:var(--ink-3);font-family:"IBM Plex Mono",ui-monospace,monospace}

/* 树模式的「坐标」：根 › … › 当前。写在模型正上方，替掉了原来的大标题。
   祖先节点可以点着回去，当前节点是这一段里最重的字。
   名字长了允许折行 —— 这里折行比截断好，截断就看不出自己在树的哪儿了。 */
.path{
  display:flex;flex-wrap:wrap;align-items:baseline;justify-content:center;
  gap:2px 3px;max-width:min(62rem,100%);line-height:1.45;
}
.path .seg{
  font:inherit;font-weight:500;font-size:13px;color:var(--ink-3);
  background:transparent;border:0;border-radius:7px;padding:1px 7px;cursor:pointer;
  transition:background .16s,color .16s;overflow-wrap:anywhere;
}
.path .seg:hover{background:var(--surface-2);color:var(--ink)}
.path .seg.cur{font-size:16px;font-weight:700;color:var(--ink);cursor:default;padding:0 4px}
.path .seg.cur:hover{background:transparent}
.path .sl{
  color:var(--ink-3);opacity:.6;user-select:none;
  font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:12px;
}
/* 模型正下方：视图控制。整条居中，不占满宽度 */
.tools{
  flex:none;align-self:center;margin:0 0 15px;
  display:flex;gap:4px;align-items:center;background:var(--surface);border:1px solid var(--line);border-radius:9px;padding:3px;box-shadow:var(--shadow-1);
}
.tools button{
  border:0;background:transparent;border-radius:6px;cursor:pointer;min-width:30px;height:26px;
  padding:0 8px;font-size:12.5px;color:var(--ink-2);
  display:inline-flex;align-items:center;justify-content:center;transition:background .18s,color .18s;
}
.tools button:hover{background:var(--surface-2);color:var(--ink)}
.tools button[aria-pressed="true"]{background:var(--ink);color:var(--bg)}
/* 「上级节点」在根节点上要压掉。压掉不等于藏起来 ——
   藏起来按钮条会左右跳，而且用户也看不出还有这么个功能。 */
.tools button:disabled{cursor:default;opacity:.38}
.tools button:disabled:hover{background:transparent;color:var(--ink-2)}
.tools .sep{width:1px;height:16px;background:var(--line);margin:0 2px}

/* 顶栏竖排，这里是「模型正上方」那一行：限宽居中，别拉满整屏 */
.search{position:relative;flex:none;width:min(24rem,100%);display:flex;align-items:center}
.search input{
  width:100%;height:32px;padding:0 46px 0 30px;border-radius:9px;border:1px solid var(--line);text-align:left;
  background:var(--surface);color:var(--ink);font-family:inherit;font-size:13px;box-shadow:var(--shadow-1);
}
.search input::placeholder{color:var(--ink-3)}
.search input:focus{outline:2px solid var(--ink-3);outline-offset:0;border-color:transparent}
.search .ico{position:absolute;left:10px;top:50%;transform:translateY(-50%);color:var(--ink-3);pointer-events:none;display:flex}
.qres{position:absolute;right:10px;top:50%;transform:translateY(-50%);font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:11px;color:var(--ink-3);pointer-events:none;background:var(--surface);padding-left:6px}

/* ── 画布 ─────────────────────────────────────────────── */
.mapwrap{flex:1;min-height:0;position:relative;overflow:hidden}
#map{position:absolute;inset:0;width:100%;height:100%;display:block;touch-action:none;cursor:grab}
#map.grabbing{cursor:grabbing}
.hud{
  position:absolute;left:16px;bottom:14px;right:16px;font-family:"IBM Plex Mono",ui-monospace,monospace;
  font-size:11px;color:var(--ink-3);pointer-events:none;line-height:1.5;
}
.hud b{color:var(--ink-2);font-weight:500}
/* 对比区收起来时，留一个角标提示还有东西在里面 */
.railpill{
  position:absolute;top:11px;right:13px;z-index:4;cursor:pointer;
  display:inline-flex;align-items:center;gap:8px;
  border:1px solid var(--line);background:var(--surface);color:var(--ink);
  border-radius:999px;padding:6px 14px;font-size:12.5px;font-weight:500;
  box-shadow:var(--shadow-1);
  transition:border-color .18s,transform .18s cubic-bezier(.2,.75,.25,1);
}
.railpill:hover{border-color:var(--ink-3);transform:translateY(-1px)}
.railpill b{
  font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:11px;font-weight:600;
  background:var(--ink);color:var(--bg);border-radius:999px;padding:1px 7px;
}
.railpill[hidden]{display:none}

/* ── 右栏 ─────────────────────────────────────────────── */
/* 收起时列宽是 0，靠 overflow-x:hidden 把内容裁掉，不留边线也不吃点击 */
.panel{
  background:var(--surface);min-height:0;min-width:0;display:flex;flex-direction:column;
  overflow-x:hidden;overflow-y:auto;overscroll-behavior:contain;
  border-left:0;box-shadow:none;
}
.app.rail .panel{border-left:1px solid var(--line);box-shadow:var(--shadow-2)}
.railhead{
  position:sticky;top:0;z-index:3;flex:none;display:flex;align-items:center;gap:8px;
  padding:13px 16px 12px;background:var(--surface);border-bottom:1px solid var(--line-soft);
}
.railhead .rt{
  font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:11px;font-weight:600;
  letter-spacing:.11em;text-transform:uppercase;color:var(--ink-3);
}
.railhead b{
  font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:11px;font-weight:600;
  background:var(--ink);color:var(--bg);border-radius:999px;padding:1px 7px;min-width:17px;text-align:center;
}
.railhead .cap{font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:11px;color:var(--ink-3)}
.railhead button{
  border:0;background:transparent;cursor:pointer;color:var(--ink-3);font-size:12px;
  padding:4px 9px;border-radius:7px;transition:background .16s,color .16s;
}
.railhead button:hover{background:var(--surface-2);color:var(--ink)}
.railhead .fold{margin-left:auto;font-size:16px;line-height:1;padding:2px 9px 5px}

/* 右栏里只有对比卡：整条不滚，高度全给卡片 —— 每张卡里的文档自己滚自己。
   overflow-y 留着当兜底（万一有哪张卡的抬头特别长，至少还能整条滚一下），
   正常情况下内容正好等于容器高度，它不会出现。 */
.pbody{
  padding:16px 16px 16px;display:flex;flex-direction:column;gap:14px;
  flex:1;min-height:0;overflow-y:auto;overflow-x:hidden;
}

/* 对比卡片：窄栏一张、宽栏并排，手机横向滑动。
   列宽下限 240px —— 是让右栏刚展开到 520px 时也能并排两张的那个数
   （258px 时差一点点，会掉回一张一列，几份文档就没法对着看了）。 */
.cmps{
  display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));
  grid-auto-rows:1fr;gap:14px;align-items:stretch;flex:1;min-height:0;
}
.cmpcard{
  border:1px solid var(--line);border-top:3px solid var(--accent);border-radius:12px;
  background:var(--surface);display:flex;flex-direction:column;
  min-height:0;max-height:100%;overflow:hidden;
  animation:cardIn .34s cubic-bezier(.22,.75,.25,1) both;
}
@keyframes cardIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}
.cmpcard header{flex:none;display:flex;align-items:center;gap:8px;padding:13px 15px 0}
.cmpcard .slot{
  font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:10.5px;font-weight:600;
  color:var(--accent);border:1px solid currentColor;border-radius:999px;padding:0 6px;line-height:1.5;
}
.cmpcard .x{
  margin-left:auto;border:0;background:transparent;cursor:pointer;color:var(--ink-3);
  font-size:17px;line-height:1;padding:2px 5px;border-radius:6px;transition:background .16s,color .16s;
}
.cmpcard .x:hover{background:var(--surface-2);color:var(--ink)}
/* 卡自己的抬头和兜底正文。作用域收在 .cmphead/.cmpfoot 里 —— 写成
   .cmpcard p 这种会把卡片下面 .doc 渲染出来的正文一起改了。 */
.cmphead,.cmpfoot{flex:none;padding:10px 15px 0;display:flex;flex-direction:column;gap:10px}
.cmphead h3{font-size:15.5px;font-weight:650;line-height:1.3}
.cmphead .latin{font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:11px;color:var(--ink-3);overflow-wrap:anywhere}
.cmphead p,.cmpfoot p{margin:0;font-size:12.5px;line-height:1.68;color:var(--ink-2)}
.cmpfoot dl{margin:0;display:flex;flex-direction:column;gap:5px;border-top:1px solid var(--line-soft);padding-top:10px}
.cmpfoot dl>div{display:grid;grid-template-columns:4.2rem minmax(0,1fr);gap:9px}
.cmpfoot dt{font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:10.5px;color:var(--ink-3);padding-top:2px}
.cmpfoot dd{margin:0;font-size:11.5px;font-family:"IBM Plex Mono",ui-monospace,monospace;color:var(--ink-2);overflow-wrap:anywhere}
/* 正文那一格：flex:1 + min-height:0 让它吃掉卡里剩下的高度，
   overflow-y:auto 给它自己的滚动条。overscroll-behavior:contain 是这里的
   关键 —— 滚到底之后滚轮不再往上传，不然手一滑整条右栏跟着动，
   并排的另外几份文档也跟着挪。 */
.cmpdoc{
  flex:1;min-height:0;margin-top:12px;padding:0 15px 15px;
  overflow-y:auto;overflow-x:hidden;overscroll-behavior:contain;
  scrollbar-width:thin;
}
.cmpdoc > *:first-child{margin-top:0}
.cmpdoc > *:last-child{margin-bottom:0}

/* 影响版本 —— 详情与卡片里都排在最显眼的位置 */
.vers{
  border:1px solid var(--line);border-left:3px solid var(--accent,var(--warn));
  background:var(--warn-bg);border-radius:10px;padding:12px 14px;
  display:flex;flex-direction:column;gap:7px;
}
.vers .vh{
  font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:10.5px;font-weight:600;
  letter-spacing:.11em;text-transform:uppercase;color:var(--warn);
}
.vers ul{margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:4px}
.vers li{display:flex;gap:8px;align-items:baseline;font-size:12.5px;color:var(--ink)}
.vers li::before{content:"";width:5px;height:5px;border-radius:50%;background:var(--accent,var(--warn));flex:none;transform:translateY(-1px)}
.vers .num{font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:12px;font-weight:500}

.chip{
  font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:10.5px;letter-spacing:.05em;text-transform:uppercase;
  padding:3px 8px;border-radius:999px;border:1px solid var(--line);color:var(--ink-2);background:var(--surface-2);white-space:nowrap;
}
.chip.grp{border-color:transparent;color:#fff;font-weight:600;letter-spacing:.07em}

.sec{display:flex;flex-direction:column;gap:9px}
.sec h3{
  font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:11px;font-weight:600;
  letter-spacing:.11em;text-transform:uppercase;color:var(--ink-3);display:flex;align-items:center;gap:9px;
}
.sec h3::after{content:"";flex:1;height:1px;background:var(--line-soft)}
.sec p{margin:0;color:var(--ink-2);font-size:13.5px;line-height:1.72;overflow-wrap:anywhere}

.rows{display:flex;flex-direction:column;border:1px solid var(--line-soft);border-radius:10px;overflow:hidden}
.row{display:grid;grid-template-columns:5.2rem minmax(0,1fr);gap:12px;padding:10px 13px;border-bottom:1px solid var(--line-soft)}
.row:last-child{border-bottom:0}
.row dt{font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:11px;color:var(--ink-3);padding-top:2px}
.row dd{margin:0;font-size:13px;overflow-wrap:anywhere}

pre{margin:0;background:var(--code-bg);border:1px solid var(--line-soft);border-radius:10px;padding:12px 14px;overflow-x:auto}
pre code{font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:12px;line-height:1.65;white-space:pre}

.rel{display:flex;flex-wrap:wrap;gap:6px}
.rel button{
  font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:11.5px;background:var(--surface);
  border:1px solid var(--line);color:var(--ink-2);padding:4px 10px;border-radius:999px;cursor:pointer;
  transition:border-color .18s,color .18s,transform .18s cubic-bezier(.2,.75,.25,1);
}
.rel button:hover{border-color:var(--ink-3);color:var(--ink);transform:translateY(-1px)}

.grp{display:flex;flex-direction:column;gap:7px}
.grphead{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--ink-3);font-family:"IBM Plex Mono",ui-monospace,monospace}
.grphead .sw{width:8px;height:8px;border-radius:2px;flex:none}
.grphead .n{margin-left:auto}
.ix{display:flex;flex-direction:column;border:1px solid var(--line-soft);border-radius:10px;overflow:hidden}
.ix button{
  display:grid;grid-template-columns:minmax(0,auto) minmax(0,1fr);gap:10px;align-items:baseline;text-align:left;
  background:transparent;border:0;border-bottom:1px solid var(--line-soft);padding:9px 12px;cursor:pointer;transition:background .16s;
}
.ix button:last-child{border-bottom:0}
.ix button:hover{background:var(--surface-2)}
.ix .k{font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:11.5px;color:var(--ink);white-space:nowrap}
.ix .v{font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:11px;color:var(--ink-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.hint{font-size:12px;color:var(--ink-3);border-top:1px solid var(--line-soft);padding-top:13px}
.hint kbd{font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:11px;background:var(--surface-2);border:1px solid var(--line-soft);border-radius:4px;padding:1px 5px}

.empty{
  border:1px dashed var(--line);border-radius:12px;padding:22px 18px;text-align:center;
  display:flex;flex-direction:column;gap:8px;align-items:center;
}
.empty p{margin:0;font-size:13px;color:var(--ink-3);max-width:26rem;line-height:1.7}

/* ── 附件区 ────────────────────────────────────────────────────
   全部走 :root 的变量，所以深浅两套主题自动就对 —— 这里不写任何
   只在某一边成立的色值。字体跟着站上已有的三种走：正文 IBM Plex Sans、
   数字和名字 IBM Plex Mono、标题用 .sec h3 那套（复用，不另起）。 */
.attnote{font-size:12.5px;color:var(--ink-3);line-height:1.66}
.attnote b{color:var(--ink-2);font-weight:600}
.attbad{color:var(--warn);background:var(--warn-bg);border-radius:8px;padding:8px 11px}
.atthint{font-size:11.5px;color:var(--ink-3);line-height:1.6}

.attlist{display:flex;flex-direction:column;border:1px solid var(--line-soft);border-radius:10px;overflow:hidden}
/* 四格：图标 | 名字 | 大小 | 一排操作。
   名字那一格是 minmax(0,1fr) 而不是 auto —— 长文件名要能被压扁并省略号收尾，
   写成 auto 的话一个 200 字符的名字会把整行撑出卡片外面。

   ⚠ 三个操作**合成一格**（.attacts）而不是各占一格。这不是为了省地方：
   不能预览的文件不画「预览」按钮，于是它只有两个操作 —— 平铺六格的话，
   这一行的下载和 ✕ 会落在第 4、5 格，能预览的那些落在第 5、6 格，
   一份既有 pdf 又有 zip 的节点上，✕ 一列就会**上下错开**。
   合成一格 + 靠右对齐之后，✕ 永远贴着右边，少一个按钮只是那一组变短。 */
.attrow{
  display:grid;grid-template-columns:auto minmax(0,1fr) auto auto;
  gap:10px;align-items:center;
  padding:8px 12px;border-bottom:1px solid var(--line-soft);
}
.attrow:last-child{border-bottom:0}
.attacts{display:flex;align-items:center;gap:6px;justify-content:flex-end}
/* 图标固定 26px：一排下来左边要对齐，名字长短不能把它推歪 */
.attglyph{
  width:26px;height:26px;flex:none;display:grid;place-items:center;border-radius:7px;
  background:var(--surface-2);color:var(--ink-3);font-size:12px;line-height:1;
}
/* 名字是**一段文字**，不是一个链接 —— 它原来是个 <a>（点名字就下载），
   现在下载另有按钮，所以 text-decoration 和那条 hover 下划线都撤了：
   给一个点不动的东西画下划线，比不画更让人误以为能点。
   整行里它颜色最重（--ink），其余都在 --ink-3，这是这一区的层次。 */
.attname{
  min-width:0;font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:12px;
  color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.attsize{
  font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:11px;color:var(--ink-3);
  white-space:nowrap;font-variant-numeric:tabular-nums;
}
/* 预览 / 下载。和 .attdel 同一档高度（约 25px、7px 圆角），
   但这两个是**有字**的按钮：「预览」这件事一个字符表达不了，
   而这两下是这一区存在的理由，不该让人去猜图标什么意思。
   默认态克制（--ink-2 + --surface），hover 才提到 --ink —— 一行里
   同时挂着三个按钮，全都高对比就变成一排都在喊。 */
.attpv,.attdl{
  cursor:pointer;white-space:nowrap;text-decoration:none;text-align:center;
  border:1px solid var(--line);background:var(--surface);color:var(--ink-2);
  border-radius:7px;padding:3px 10px;
  font-family:inherit;font-size:11.5px;line-height:1.5;
  transition:color .16s,border-color .16s,background .16s;
}
.attpv:hover,.attdl:hover{color:var(--ink);border-color:var(--ink-3);background:var(--surface-2)}
/* 键盘用户要看得见落点。三个按钮里 ✕ 是唯一不可逆的那个，
   焦点看不见的时候最容易误触。 */
.attpv:focus-visible,.attdl:focus-visible,.attdel:focus-visible{
  outline:2px solid var(--ink-3);outline-offset:2px;
}
.attdel{
  width:24px;height:24px;flex:none;display:grid;place-items:center;cursor:pointer;
  background:transparent;border:1px solid transparent;border-radius:7px;
  color:var(--ink-3);font-size:11px;line-height:1;
  transition:color .16s,border-color .16s,background .16s;
}
.attdel:hover{color:var(--warn);border-color:var(--line);background:var(--surface-2)}
.attdel:disabled{cursor:not-allowed;opacity:.45}

/* 这里原来有 .attbar/.attup/.attmeta/.attprog/.atttrack/.attlbl ——
   上传那一套（选文件、进度条、计数）。2026-09-27 撤了：普通节点不再
   是「喂」的地方，上传搬去中心节点那个终端（期 3）。服务端那条
   POST /api/upload 一行没动，worker 的产物回流还走它。
   删的时候留个记号，免得下次看见 .attform 以为是漏了上传框。
   顺带记一条坑：progress 那一行原来自己写着 display:flex，
   于是必须再补一条 `.attprog[hidden]{display:none}` 才盖得住 ——
   作者样式压过浏览器默认的 [hidden]。同一个坑 .veil 和 .pv 都踩过。 */

.attform{display:flex;flex-wrap:wrap;gap:8px}
.attform input{
  flex:1;min-width:0;font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:12px;
  background:var(--surface);color:var(--ink);border:1px solid var(--line);border-radius:9px;padding:6px 11px;
}
.attform input::placeholder{color:var(--ink-3)}
.attform button{
  cursor:pointer;border:1px solid var(--line);background:var(--surface-2);color:var(--ink);
  border-radius:9px;padding:6px 14px;font-size:12.5px;font-weight:600;
  transition:background .16s,border-color .16s;
}
.attform button:hover{background:var(--surface);border-color:var(--ink-3)}

/* ── 技能区 ────────────────────────────────────────────────────
   和附件区同一套规矩：颜色全走 :root 的变量，深浅两套自动成立；
   字体还是那三种（正文 IBM Plex Sans、机器名/数字 IBM Plex Mono、
   小标题复用 .sec h3）。新增的语义色只有 --live / --ok 两组，
   和 --warn 一起写在三个 :root 里。 */
/* .sec{display:flex} 会压过浏览器默认的 [hidden]{display:none} ——
   没技能的节点要把整块收掉，所以必须自己再写一遍。 */
.skill[hidden]{display:none}

.skillnote{font-size:12.5px;color:var(--ink-3);line-height:1.66}
.skillbad{font-size:12.5px;color:var(--warn);background:var(--warn-bg);border-radius:8px;padding:8px 11px;overflow-wrap:anywhere}
.skillhint{font-size:11.5px;color:var(--ink-3);line-height:1.6}

.skillform{display:flex;flex-direction:column;gap:9px}
.skillname{
  font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:11px;font-weight:600;
  letter-spacing:.06em;color:var(--ink-2);overflow-wrap:anywhere;
}
.skillpromptin{
  width:100%;font:inherit;font-size:13px;background:var(--surface);color:var(--ink);
  border:1px solid var(--line);border-radius:9px;padding:7px 11px;
}
.skillpromptin::placeholder{color:var(--ink-3)}

/* 技能名旁边那一行：指纹、来源、有没有被盖住。
   比 .skillname 还小一档、还不显眼 —— 它是「需要时能查到」的信息，
   不是每次都要读的。但也不能藏进 title 里：报告不对的时候，
   「跑的是哪一版技能」是第一个要问的问题。 */
.skillsrctags{
  font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:10px;font-weight:400;
  letter-spacing:.03em;color:var(--ink-3);
}
.skillsrctags code{font-size:10px;color:var(--ink-2)}
.skillwarn{color:var(--warn)}

/* 「它写了什么」——右边那个小按钮，和「运行」同一个行里但明显是次要动作 */
.skillsrc{
  cursor:pointer;margin-left:auto;border:1px solid var(--line-soft);background:none;
  color:var(--ink-3);border-radius:8px;padding:5px 10px;font-size:11.5px;
}
.skillsrc:hover{color:var(--ink-2);border-color:var(--line)}
.skillsrcbox{
  border:1px solid var(--line-soft);border-radius:9px;padding:9px 11px;
  display:flex;flex-direction:column;gap:8px;
}
.skillsrcbox[hidden]{display:none}
.skillpath{
  font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:10.5px;
  color:var(--ink-3);overflow-wrap:anywhere;
}
.skillfile summary{
  cursor:pointer;font-family:"IBM Plex Mono",ui-monospace,monospace;
  font-size:11px;color:var(--ink-2);padding:2px 0;
}
.skillfile summary:hover{color:var(--ink)}
/* 技能正文里常有一行很长的命令（find 一大串、grep 的正则），
   不给它自己的滚动条就会把整张卡片顶宽 —— 那时候页面是横着滚的。 */
.skillfile pre{
  margin:6px 0 0;max-height:22rem;overflow:auto;
  font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:11.5px;line-height:1.6;
  color:var(--ink-2);background:var(--surface-2);border-radius:8px;padding:9px 11px;
  white-space:pre-wrap;overflow-wrap:anywhere;
}

/* 材料列表。高了就自己滚 —— 一个节点挂二十个附件时不能让卡片长到没边 */
.skillpicks{
  display:flex;flex-direction:column;gap:5px;max-height:11rem;overflow-y:auto;
  border:1px solid var(--line-soft);border-radius:9px;padding:9px 11px;
}
.skillpickhead{font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:11px;color:var(--ink-3)}
.skillpick{display:flex;align-items:center;gap:8px;font-size:12.5px;color:var(--ink-2);cursor:pointer}
.skillpick input{flex:none;accent-color:var(--ink-2);cursor:pointer}
.skillpickname{
  min-width:0;font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:12px;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}

/* ── 「这个节点还没挂技能」 ──────────────────────────────────────
   这一段是网页上**唯一**能把技能挂到节点上的入口（写 front.skill 的口子
   只有 PATCH /api/node/:id 和 MCP 的 kg_set_skill）。原来这种节点整块是
   隐藏的，用户看到的是「打开一个节点，只有一个上传框，什么都干不了」。
   同一屏上摆两件事：从库里挑一个挂上（挑不用令牌），或者传一份 md 装一个。

   ⚠ 下拉的类名是 .skillsel 不是 .skillpick —— 后者已经被上面的**材料**
     勾选框占了（<label class="skillpick">）。同名的话 bind() 里
     querySelector(".skillpick") 会先捞到材料那一行，拿它的 .value 当技能名。 */

.skillpickwrap{display:flex;flex-direction:column;gap:9px}

/* 下拉和「挂上」同一行。窄了就折 —— 折行比把一个下拉压到 20px 宽好。 */
.skillpickrow{display:flex;flex-wrap:wrap;align-items:center;gap:8px}
.skillsel{
  flex:1 1 11rem;min-width:0;font:inherit;font-size:13px;
  background:var(--surface);color:var(--ink);border:1px solid var(--line);
  border-radius:9px;padding:7px 9px;
}
.skillattach,.skilldoinstall{
  cursor:pointer;border:1px solid var(--line);background:var(--surface);color:var(--ink);
  border-radius:9px;padding:6px 13px;font-size:12.5px;font-weight:600;
  transition:background .16s,border-color .16s,transform .16s cubic-bezier(.2,.75,.25,1);
}
.skillattach:hover:not(:disabled),.skilldoinstall:hover:not(:disabled){
  background:var(--surface-2);border-color:var(--ink-3);transform:translateY(-1px);
}
.skillattach:disabled,.skilldoinstall:disabled{
  cursor:progress;color:var(--ink-3);border-color:var(--line-soft);transform:none;
}
/* 「覆盖重装」是个**写操作的分岔**，不是主按钮：它要拿库里同名那份换掉。
   得看得见，但不能长得跟「装并挂上」一样 —— 橙色，和告警同一档。 */
.skillforce{
  cursor:pointer;border:1px solid var(--warn);background:var(--warn-bg);color:var(--warn);
  border-radius:9px;padding:6px 13px;font-size:12.5px;font-weight:600;
}
.skillcancelmd{
  cursor:pointer;border:0;background:none;color:var(--ink-3);
  padding:6px 4px;font-size:12.5px;text-decoration:underline;text-underline-offset:3px;
}
.skillcancelmd:hover{color:var(--ink-2)}

/* 「装一个新技能」收在 details 里 —— 它比「挑一个现成的」少见得多，
   敞着会把那排下拉挤到看不见的地方。三角自己画：默认那个 WebKit 的
   ▸ 跟着系统字体走，和这一屏的字重对不上。 */
.skillinstallbox{border-top:1px solid var(--line-soft);padding-top:9px}
.skillinstallbox>summary{
  cursor:pointer;font-size:12px;color:var(--ink-3);list-style:none;padding:2px 0;
}
.skillinstallbox>summary::-webkit-details-marker{display:none}
.skillinstallbox>summary::before{content:"▸ "}
.skillinstallbox[open]>summary::before{content:"▾ "}
.skillinstallbox>summary:hover{color:var(--ink-2)}
.skillinstallbox>*:not(summary){margin-top:8px}

/* 选了 md 之后那一块：说清楚选了哪个文件、名字是从哪儿来的、
   按下去会发生什么。它每次都整块重画（面板 2 秒一轮），所以里面的
   输入框得靠 skillask 这个类被 snapshotInputs 认出来。 */
.skillmdready{
  display:flex;flex-direction:column;gap:8px;
  border:1px solid var(--line-soft);border-radius:9px;padding:9px 11px;
}
.skillmd{font-size:12.5px;color:var(--ink-2);max-width:100%}
.skillmd::file-selector-button{
  cursor:pointer;font:inherit;font-size:12px;margin-right:9px;
  background:var(--surface-2);color:var(--ink-2);
  border:1px solid var(--line);border-radius:8px;padding:5px 11px;
}
.skillaskrow{display:flex;flex-wrap:wrap;align-items:center;gap:8px;font-size:12.5px;color:var(--ink-3)}
.skillask{
  flex:1 1 11rem;min-width:0;font:inherit;font-size:13px;
  background:var(--surface);color:var(--ink);border:1px solid var(--line);
  border-radius:9px;padding:6px 10px;
  font-family:"IBM Plex Mono",ui-monospace,monospace;
}
.skillask::placeholder{color:var(--ink-3)}

.skillbar2{display:flex;flex-wrap:wrap;align-items:center;gap:10px}
.skillrun{
  cursor:pointer;border:1px solid var(--line);background:var(--surface);color:var(--ink);
  border-radius:9px;padding:6px 13px;font-size:12.5px;font-weight:600;
  transition:background .16s,border-color .16s,transform .16s cubic-bezier(.2,.75,.25,1);
}
.skillrun:hover:not(:disabled){background:var(--surface-2);border-color:var(--ink-3);transform:translateY(-1px)}
.skillrun:disabled{cursor:progress;color:var(--ink-3);border-color:var(--line-soft);transform:none}

/* 一次任务一张小卡。状态用底色区分（在跑=蓝、完成=绿、没成=橙、
   自己停的=灰），所以隔着半屏扫一眼就知道哪次出了事，不用读字。
   灰色那一档是后加的：用户自己按的停也在「橙」里的话，这一眼就废了
   —— 满屏告警色里混着几件本来就没问题的事。 */
.skilljob{
  display:flex;flex-direction:column;gap:7px;
  border:1px solid var(--line-soft);border-radius:10px;padding:10px 12px;background:var(--surface);
}
.skillhead{display:flex;flex-wrap:wrap;align-items:center;gap:9px}
.skillstate{
  font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:10.5px;font-weight:600;
  letter-spacing:.08em;padding:2px 9px;border-radius:999px;
  background:var(--surface-2);color:var(--ink-2);white-space:nowrap;
}
.s-queued .skillstate,.s-running .skillstate{background:var(--live-bg);color:var(--live)}
.s-done .skillstate{background:var(--ok-bg);color:var(--ok)}
/* failed / timeout 是**出了事**，橙色。
   killed 故意不在这儿 —— 它只有一个写入点（db.killJob），而那只被用户
   自己的「结束它」调用。把用户主动按的停和跑挂了涂成同一个颜色，等于
   每次主动结束都在界面上留一个「出过故障」的痕迹，扫一眼分不出哪些是
   真需要看的。它落回 .skillstate 的默认灰。 */
.s-failed .skillstate,.s-timeout .skillstate{background:var(--warn-bg);color:var(--warn)}
.skillpctnum{
  font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:12px;
  color:var(--ink-2);font-variant-numeric:tabular-nums;
}
/* 时间和「结束它」都靠右。时间吃掉空白，结束键跟在它后面 ——
   这样换行的时候按键不会先跳出去。 */
.skillwhen{
  margin-left:auto;font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:11px;
  color:var(--ink-3);font-variant-numeric:tabular-nums;white-space:nowrap;
}
.skillkill{
  cursor:pointer;border:1px solid var(--line);background:var(--surface-2);color:var(--ink-3);
  border-radius:8px;padding:3px 10px;font-size:11.5px;
  transition:color .16s,border-color .16s,background .16s;
}
.skillkill:hover{color:var(--warn);border-color:var(--warn);background:var(--warn-bg)}

/* 「原样再跑一次」。和「结束它」同一个位置（任务卡右上角），只是不同
   状态各出现一个 —— 所以样式长得像，但不是同一种语气：这个是中性的，
   不涂成告警色。 */
.skillretry{
  cursor:pointer;border:1px solid var(--line);background:var(--surface);color:var(--ink);
  border-radius:8px;padding:3px 10px;font-size:11.5px;font-weight:600;
  transition:background .16s,border-color .16s;
}
.skillretry:hover:not(:disabled){background:var(--surface-2);border-color:var(--ink-3)}
.skillretry:disabled{cursor:progress;color:var(--ink-3);border-color:var(--line-soft)}

.skillprompt{font-size:13px;color:var(--ink);overflow-wrap:anywhere}
.skillins{
  font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:11.5px;
  color:var(--ink-3);overflow-wrap:anywhere;
}
.skillbar{height:4px;border-radius:999px;background:var(--surface-2);overflow:hidden}
.skillbar i{display:block;height:100%;border-radius:999px;background:var(--live);transition:width .3s linear}
/* 只显示最后一行 —— 那是一条快照，不是日志流。要完整的去看任务目录里的 run.log */
.skilltail{
  font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:11.5px;color:var(--ink-3);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.skillerr{font-size:12px;color:var(--warn);background:var(--warn-bg);border-radius:8px;padding:7px 10px;overflow-wrap:anywhere}
/* 「为什么不动」。名字还叫 stale，但内容已经不止「心跳断了」—— 排队中没人领
   也走这一条。**故意不改类名**：浏览器可能还揣着上一版 kg3d.css，换名字的话
   新文案在旧样式表下会变成一段裸 div（能读，但看着像页面坏了）。多一个名字
   不值这个风险。
   overflow-wrap 是必须的：worker 报上来的闸门原因里可能有长的数字串。 */
.skillstale{font-size:11.5px;color:var(--warn);line-height:1.6;overflow-wrap:anywhere}
/* 终态任务（failed / timeout / killed）的「出了什么事、现在能做什么」。
   和上面的 .skillstale 分工：那个说的是**正在进行**的任务为什么不动，
   这个说的是**已经结束**的任务为什么没成。两者永远不会同时出现（live
   和终态互斥），所以不必长得一样。

   和 .skillerr 分开是有意的：.skillerr 里是 worker 报回来的**原话**，
   排查时那句话就是唯一线索，一个字都不能改；这里是人的翻译。混在一起
   的话，调文案就等于改证据。 */
.skillwhy{
  font-size:12px;color:var(--ink-2);line-height:1.66;overflow-wrap:anywhere;
  border-left:2px solid var(--line);padding-left:9px;
}
.skillby{
  font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:10.5px;
  color:var(--ink-3);letter-spacing:.04em;overflow-wrap:anywhere;
}
/* 「这条报告是按旧版技能做的」。
   两个 sha 并排摆着等于什么都没说（没人会去比对十六进制），所以这里
   不是加一串字，是说一句结论，并且是唯一一处带颜色的 —— 它要能被扫到。 */
.skilldrift{
  margin-left:6px;color:var(--warn);background:var(--warn-bg);
  border-radius:999px;padding:1px 7px;letter-spacing:0;
}

/* 产物。签名 URL 带不了请求头，所以只能是一个普通 <a href> ——
   样式上做成一枚按钮，但它是链接下来，不是 fetch。 */
.skillouts{display:flex;flex-wrap:wrap;gap:8px}
.skillouts a{
  display:inline-flex;align-items:center;gap:6px;min-width:0;max-width:100%;
  font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:12px;color:var(--ink);
  text-decoration:none;border:1px solid var(--line);border-radius:8px;padding:4px 10px;
  background:var(--surface-2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  transition:border-color .16s,background .16s;
}
.skillouts a::before{content:"↓";color:var(--ink-3);flex:none}
.skillouts a:hover{border-color:var(--ink-3);background:var(--surface)}

.skillhist{display:flex;flex-direction:column;gap:8px}
.skillhist summary{
  cursor:pointer;font-family:"IBM Plex Mono",ui-monospace,monospace;
  font-size:11.5px;color:var(--ink-3);
}
.skillhist summary:hover{color:var(--ink-2)}
/* 历史里的任务卡是回看的，不该和「当前这次」抢注意力 */
.skillhist .skilljob{background:transparent;opacity:.82}

/* ── 详情模态框：屏幕正中一个框，只有这一个节点的内容 ─────── */
.veil{
  position:fixed;inset:0;z-index:40;display:grid;place-items:center;
  padding:max(16px, env(safe-area-inset-top,0px)) 16px max(16px, env(safe-area-inset-bottom,0px));
  background:var(--veil);-webkit-backdrop-filter:blur(7px);backdrop-filter:blur(7px);
  /* 关闭态必须同时收掉透明度、可见性和命中测试：只靠 [hidden] 不够，
     作者样式里的 display:grid 会压过浏览器默认的 [hidden]{display:none} */
  opacity:0;visibility:hidden;pointer-events:none;
  transition:opacity .2s ease,visibility .2s;
}
.veil[hidden]{display:none}
.veil.on{opacity:1;visibility:visible;pointer-events:auto}
.card{
  width:min(47rem,100%);max-height:100%;display:flex;flex-direction:column;overflow:hidden;
  background:var(--surface);border:1px solid var(--line);border-radius:16px;box-shadow:var(--shadow-3);
  transform:translateY(12px) scale(.985);transition:transform .3s cubic-bezier(.22,.75,.25,1);
}
.veil.on .card{transform:none}
.cardbar{
  flex:none;display:flex;align-items:center;gap:12px;padding:13px 16px;
  border-bottom:1px solid var(--line-soft);background:var(--surface);
}
.goback{
  display:inline-flex;align-items:center;gap:6px;flex:none;cursor:pointer;
  border:1px solid var(--line);background:var(--surface);color:var(--ink);
  border-radius:8px;padding:5px 12px 5px 9px;font-size:12.5px;font-weight:500;
  transition:background .16s,border-color .16s,transform .16s cubic-bezier(.2,.75,.25,1);
}
.goback:hover{background:var(--surface-2);border-color:var(--ink-3);transform:translateX(-2px)}
.cardbar .crumb{
  flex:1;min-width:0;font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:11px;color:var(--ink-3);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
/* 卡片里的「进入下级」：把这一支挪到球心。
   和「加入对比」并排，但要比它轻 —— 一个是在树里走，一个是攒对比，两回事 */
.enterbtn{
  flex:none;cursor:pointer;border:1px solid var(--line);background:var(--surface);color:var(--ink);
  border-radius:9px;padding:6px 13px;font-size:12.5px;font-weight:600;
  transition:background .16s,border-color .16s,transform .16s cubic-bezier(.2,.75,.25,1);
}
.enterbtn:hover{background:var(--surface-2);border-color:var(--ink-3);transform:translateY(-1px)}
.enterbtn[hidden]{display:none}
.addbtn{
  flex:none;cursor:pointer;border:1px solid transparent;background:var(--ink);color:var(--bg);
  border-radius:9px;padding:6px 14px;font-size:12.5px;font-weight:600;
  transition:background .16s,color .16s,border-color .16s,transform .16s cubic-bezier(.2,.75,.25,1);
}
.addbtn:hover:not(:disabled){transform:translateY(-1px)}
.addbtn.on{background:var(--surface-2);color:var(--ink-2);border-color:var(--line)}
.addbtn:disabled{cursor:not-allowed;background:var(--surface-2);color:var(--ink-3);border-color:var(--line);transform:none}
.cardbody{overflow-y:auto;overscroll-behavior:contain;padding:22px 24px 30px;display:flex;flex-direction:column;gap:19px}
.cardbody h2{font-size:23px;font-weight:700;line-height:1.24}
.cardbody .latin{font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:12px;color:var(--ink-3);margin-top:3px;overflow-wrap:anywhere}
.cardbody .lead{margin:0;font-size:15px;line-height:1.7}
.eyebrow{display:flex;flex-wrap:wrap;gap:6px;align-items:center}

@media (max-width:940px){
  body{overflow:auto}
  .app,.app.rail,.app.rail.wide{height:auto;grid-template-columns:1fr;grid-template-rows:auto}
  /* ★ 没开对比栏时，舞台正好占满一屏 —— 下面这条 1fr 就是干这个的。
     以前这里三档一律 auto，而 .app 自己是被 site.css 撑满一屏的（页脚上面
     那一块），于是舞台只占内容那么高：390×844 上量到「顶栏 106 + 画布 304 +
     工具条 34」= 444px，工具条停在离屏底 **400px** 的地方，底下一大片是空的。
     看着就是「底部那排按钮浮到半空去了」。
     填满之后画布从 min(78vw,22rem)=304px 涨到 644，球面半径由 H*0.42 那条
     约束换成 W*0.44（S=min(W*0.44,H*0.42)，128 → 172），字不再挤成一团。
     ⚠ 上面这些数字都出自 _tools/probe-mobile.js —— 改完版式自己跑一遍，
       别抄这里的。数不重要，重要的是它得是刚量出来的。
     开了对比栏就**不**填：那时候舞台要贴着顶，让面板从下面滚进来。 */
  .app:not(.rail){grid-template-rows:minmax(0,1fr)}
  /* 手机上右栏排在下方，收起来就直接不占位 */
  .app:not(.rail) .panel{display:none}
  .stage{position:sticky;top:env(safe-area-inset-top,0px);z-index:2;background:var(--bg);border-bottom:1px solid var(--line);min-height:0}
  .bar{padding:13px 16px 10px}
  .brand h1{font-size:15px}
  /* 手机上坐标会折两三行，字号收一点，别把模型挤没了 */
  .path .seg{font-size:12px;padding:1px 5px}
  .path .seg.cur{font-size:14px}
  .search{width:100%}
  /* ★ 16px 不能改小：iOS Safari 在聚焦**字号小于 16px** 的输入框时会把整个
     页面放大。桌面那 13px 配鼠标没问题，到手机上就是「点一下搜索框，界面
     自己缩放了一下」—— 搜索框「没做适配」指的多半就是这个。
     高度 32 → 38 同理：32px 是配鼠标的，手指点不准。 */
  .search input{height:38px;font-size:16px}
  /* 技能区那几个也是输入框，同一条理由：13px 在桌面上配鼠标没问题，
     手指点上去会让 iOS 把整页放大一下。补在这里而不是写进各自的规则里，
     是因为「手机上要 16px」是**版式**的事，不该混进组件的定义。 */
  .skillpromptin,.skillask,.skillsel{font-size:16px}
  /* 画布吃掉顶栏和工具条之间剩下的全部高度（原来是 flex:none + 固定高度，
     一屏只给模型留 304px）。引擎那边 .mapwrap 上挂着 ResizeObserver，
     尺寸变了它自己会重算，不用额外通知。
     ⚠ 这两条必须成对：开了对比栏时舞台是 auto 高度，容器高度不确定，
       光给 `flex:1`（= flex-basis 0）会让画布塌成 **0** —— 因为 canvas 是
       绝对定位的，撑不出内容高度。所以 rail 那一档把固定高度还回去。 */
  .app:not(.rail) .mapwrap{flex:1;min-height:0}
  .app.rail .mapwrap{flex:none;height:min(78vw,22rem)}
  /* 手机上按钮条撑不下就折行，别横向溢出。
     按钮 26px → 34px：26px 的点击目标在手机上是要点错的，而这排按钮是
     手机上唯一的视图控制（没有 hover、没有快捷键）。这一条和上面那条
     16px 一样，是「适配」而不是「好看」。 */
  .tools{align-self:stretch;margin:0 16px 12px;justify-content:center;flex-wrap:wrap;row-gap:4px}
  .tools button{height:34px;min-width:38px;font-size:13px}
  .tools .sep{height:20px}
  .panel{border-left:0;box-shadow:none}
  .railhead{padding:10px 14px}
  .railpill{top:8px;right:10px}
  .pbody{padding:16px 16px 52px}
  .cardbody{padding:18px 16px 26px}
  .cardbody h2{font-size:19px}
  .card{width:100%;border-radius:14px}
  .cardbar .crumb{display:none}
  .row{grid-template-columns:1fr;gap:2px}
  /* 手机上并排不可能，改成横向滑动，一张一张翻 */
  .cmps{display:flex;overflow-x:auto;scroll-snap-type:x mandatory;padding-bottom:6px;-webkit-overflow-scrolling:touch;scrollbar-width:none;flex:none}
  .cmps::-webkit-scrollbar{display:none}
  .cmpcard{flex:0 0 84%;scroll-snap-align:start;max-height:none}
  /* 手机上右栏跟着页面走，高度不是定值 —— 正文格子得自己给个上限，
     不然它会一直长下去，滚轮落到的还是整页。 */
  .cmpdoc{max-height:56vh}
  .hud{left:12px;right:12px;bottom:10px;font-size:10px}
  .hint{display:none}
}
/* 窄屏上先把附件行的「大小」那一格收掉。
   这一格是六个里最不重要的一格（名字和两个按钮才是这一区存在的理由），
   而它一收，名字那一格就多出约 46px。
   为什么需要收：360px 的机器上 .attrow 能排的宽度只有
   360 − 卡片内边距 32 − 面板内边距 32 − 行内边距 24 ≈ 272px，
   而三格固定内容（图标 26 + 大小 46 + 那排操作 126）再加三道 10px 间隙
   就吃掉 228，只剩 44px 给名字 —— 等于名字没了。收掉「大小」之后是 90px。
   收到 640 而不是等到 380：iPhone 那几个 390/430 的宽度也都在
   这个区间里，等到 380 就只有最小的那批机器才吃到。 */
@media (max-width:640px){
  .attsize{display:none}
}
/* 窄一档的手机（360px 那种）：按钮条的按钮加宽到 38px 之后就撑不下了，
   会折成两行。这里把内边距和最小宽度各收一点，换回一行 ——
   折行本身不算坏（当初就是故意这么写的），但一行更省高度。 */
@media (max-width:380px){
  .tools button{min-width:32px;padding:0 6px;font-size:12.5px}
}
/* ★ 矮一屏的横屏手机（844×390 之类）：顶栏竖排太费高度，一屏根本装不下。
   量到的是「画布盒子 352px、整页要往下滚 102px、那排按钮整个落在视口下方
   102px 处」—— 盒子写着 352，能看见的没那么多，按钮还得往下滚才够得着。
   顶栏那一坨高度省不掉，只能改成横排一行（左标题、右搜索）。改完量到画布
   251px、页面可滚 0、按钮离页脚 43px：盒子矮了，但整块都在屏幕里。
   球面半径跟着从 148 降到 105（S=min(W*0.44,H*0.42)，横屏下一直是 H 那条
   约着），换来的是模型不再有一截画在屏幕外 —— 原来那 352px 里有 102px 是白搭。
   阈值取 520：竖屏手机最矮也就 640，横屏最高的常见值是 430（iPhone 15
   Pro Max），中间留足余量，不会误伤竖屏。 */
@media (max-width:940px) and (max-height:520px){
  .bar{flex-direction:row;align-items:center;gap:12px;padding:8px 14px;text-align:left}
  .brand{align-items:flex-start}
  .brand h1{font-size:14px}
  .brand .count{display:none}
  .brand .path{justify-content:flex-start}
  .search{flex:1;width:auto;min-width:0}
  .tools{margin:0 14px 8px}
}
@media (prefers-reduced-motion: reduce){
  *{animation-duration:.001ms !important;transition-duration:.001ms !important}
}

/* ══ 底部那条：这一次到底还在不在跑 ══════════════════════════════════
   它是 skills.js 里 run 那一块的外壳。两个元素（条、弹窗）都挂在
   document.body 上，**不在卡片里** —— 卡片一关 mBody 整块被换掉，挂里面
   的东西跟着就没了，而这一条存在的理由正是「卡片关了还要看得见」。

   放在文件最后、所有 @media 之后：下面那条把 .hud 顶上去的规则靠**特异性**
   赢（body.runbar-on .hud 比 .hud 高一档），但同一个偏移在窄屏是另一套数，
   所以两套都写在这儿，谁也不用去猜哪条先。

   z-index 60/61：卡片的遮罩是 40，两个都要在它上面 —— 卡片开着的时候底部
   那条也还在，被遮罩盖住就等于没有。
   ══════════════════════════════════════════════════════════════════ */
.runbar{
  position:fixed;left:0;right:0;bottom:0;z-index:60;
  background:var(--surface);border-top:1px solid var(--line);
  box-shadow:var(--shadow-3);
  padding-bottom:env(safe-area-inset-bottom,0px);
}
.runbar[hidden]{display:none}
.runbarin{
  display:flex;flex-wrap:wrap;align-items:center;gap:10px;
  padding:10px 16px;
}
.runbarin>*{flex:none}
/* 把右边那组（用时/按钮）顶到最右。flex:1 的空档 —— 比给末尾元素加
   margin-left:auto 稳：尾巴那一行有没有内容都不影响这一条。 */
.runsp{flex:1 1 auto;min-width:0}

.runst{
  font-size:12px;font-weight:500;padding:2px 9px;border-radius:999px;
  background:var(--surface-2);color:var(--ink-2);
}
.runst.live{background:var(--live-bg);color:var(--live)}
.runst.good{background:var(--ok-bg);color:var(--ok)}
.runst.bad{background:var(--warn-bg);color:var(--warn)}

.runpct{font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:12px;color:var(--ink-2);font-variant-numeric:tabular-nums}
.runnode{font-size:12px;color:var(--ink-2);max-width:16rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.runel{font-size:11.5px;color:var(--ink-3);font-variant-numeric:tabular-nums}
/* 「它此刻在干什么」。**这一行是「真的在动」唯一看得见的证据**，所以给它
   最大的伸缩余地：位置够就多显示几个字，不够就截断（截断总比把别的挤下去强）。
   等宽字体是有意的 —— 里面装的是命令和路径，比例字体读起来会很费劲。 */
.runtail{
  font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:11.5px;color:var(--ink-3);
  flex:1 1 12rem;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}

/* 转的那个圈。**转起来才是「在跑」** —— 一个不动的百分比和卡死长得一样。
   reduced-motion 下被上面的通配规则压成静止，那就只剩颜色在说话，可以接受。 */
.runspin{
  width:11px;height:11px;border-radius:50%;flex:none;
  border:2px solid var(--live-bg);border-top-color:var(--live);
  animation:runspin .8s linear infinite;
}
@keyframes runspin{to{transform:rotate(360deg)}}

.runkill,.runclose{
  font:inherit;font-size:12px;background:transparent;color:var(--ink-3);
  border:1px solid var(--line);border-radius:999px;padding:4px 12px;cursor:pointer;
  transition:color .16s,border-color .16s,background .16s;
}
.runkill:hover{color:var(--warn);border-color:var(--warn);background:var(--warn-bg)}
.runclose{line-height:1;font-size:14px;padding:3px 9px}
.runclose:hover{color:var(--ink);border-color:var(--ink-3)}

/* 下载链。**是 <a download> 不是按钮** —— 产物走的是带签名的 URL，签名在
   query 里，只有直接导航才带得上（fetch 带不了）。做成按钮再 fetch 就废了。 */
.rundl{
  font-size:12px;color:var(--ok);background:var(--ok-bg);border-radius:999px;
  padding:4px 12px;text-decoration:none;max-width:22rem;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.rundl:hover{text-decoration:underline}

.runprog{height:3px;background:var(--surface-2);overflow:hidden}
.runprog i{display:block;height:100%;background:var(--live);transition:width .3s linear}

/* ── 跑完弹的那一扇 ─────────────────────────────────────────────── */
.runpop{
  position:fixed;right:16px;z-index:61;
  bottom:calc(var(--runbar-h,0px) + 16px + env(safe-area-inset-bottom,0px));
  width:min(24rem,calc(100vw - 32px));
  background:var(--surface);border:1px solid var(--line);border-radius:14px;
  box-shadow:var(--shadow-3);padding:16px 16px 14px;
  display:flex;flex-direction:column;gap:9px;
  animation:runpopin .22s ease-out;
}
.runpop[hidden]{display:none}
@keyframes runpopin{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}
.runpopx{
  position:absolute;top:9px;right:11px;background:transparent;border:0;cursor:pointer;
  font-size:15px;line-height:1;color:var(--ink-3);padding:3px 6px;
}
.runpopx:hover{color:var(--ink)}
.runpoptitle{font-size:14px;font-weight:600;color:var(--ink);padding-right:22px}
.runpopwho{font-size:11.5px;color:var(--ink-3)}
.runpopdl{
  display:block;font-size:13px;color:var(--ok);background:var(--ok-bg);
  border-radius:9px;padding:9px 12px;text-decoration:none;overflow-wrap:anywhere;
}
.runpopdl:hover{text-decoration:underline}
.runpophint{font-size:11.5px;color:var(--ink-3);line-height:1.6}

/* 底部那条立起来的时候，把画布底下那行提示顶上去 —— 不顶的话它会正好压在
   提示上。高度由 JS 量了写进 --runbar-h（那条会折行，写死一个数迟早不对；
   手机上命令一长就折成两行，写死 44 就会盖住半行）。 */
body.runbar-on .hud{bottom:calc(var(--runbar-h,0px) + 14px)}

@media (max-width:940px){
  .runbarin{padding:9px 12px;gap:8px}
  /* 手机上「它在干什么」优先于节点名：节点名是用户自己刚点开的，他知道；
     那一行才是他关掉卡片之后唯一还想看的。 */
  .runnode{display:none}
  .runpop{left:12px;right:12px;width:auto}
  body.runbar-on .hud{bottom:calc(var(--runbar-h,0px) + 10px)}
}
