/* ============================================================
   Dashboard v2 · design tokens + components
   Ported verbatim from the .24 baseline (public/style.css) — palette, ring
   geometry, KV blocks, spark bars, lv-* status colors, identity colors,
   fonts and spacing are the single source of visual truth (§1).

   §0 performance hardening applied (DO NOT regress):
     • NO conic @property --ang border-flow animation (idle CPU)
     • NO backdrop-filter: blur anywhere (scroll jank)
     • card background fixed at rgba(14,19,28,0.92)
     • identity glow = box-shadow first layer 0 4px 20px -10px var(--accent)
   The ring comet + breathing animations are intentionally KEPT (§6).
   ============================================================ */
:root {
  /* 太空底 */
  --bg0: #04060c;
  --bg1: #080c16;
  --bg2: #0b1120;
  /* 卡面（§0 固定） */
  --card-bg: rgba(14, 19, 28, 0.92);
  /* 玻璃细节 */
  --glass: rgba(255, 255, 255, 0.038);
  --glass2: rgba(255, 255, 255, 0.07);
  --hair: rgba(255, 255, 255, 0.09);
  --hair2: rgba(255, 255, 255, 0.16);
  /* 文本（v1 暖金基调，§复刻）*/
  --text: #cdc4a8;
  --muted: rgba(205, 196, 168, 0.64);
  --muted2: rgba(205, 196, 168, 0.42);
  /* 数据色（鲜艳） */
  --green: #5ef0a8;
  --yellow: #ffd24a;
  --red: #ff6b7a;
  --blue: #5aa6ff;
  --cyan: #4fe3d0;
  --violet: #b18cff;
  /* 状态色 → lv-* 映射（normalize 出 level，前端只贴 class） */
  --lv-ok: var(--green);
  --lv-warn: var(--yellow);
  --lv-danger: var(--red);
  --lv-cool: var(--cyan);
  --idle-spin: rgba(148, 163, 184, 0.78);   /* v2.38 空闲轮询: grey, not a level colour */
  /* 机器识别色（柔和装饰，由 --accent 覆盖） */
  --m24: #5aa6ff;
  --m110: #4fe3d0;
  --m26: #b18cff;
  --track: rgba(255, 255, 255, 0.07);
  /* 间距令牌 */
  --s1: 4px; --s2: 8px; --s3: 12px; --s4: 16px; --s5: 22px; --s6: 30px;
  --radius: 18px;
  --shadow: 0 14px 40px -18px rgba(0,0,0,0.85), 0 2px 8px -4px rgba(0,0,0,0.6);
  /* font stack (themeable via theme.yaml font_family; default = original) */
  --font: ui-sans-serif, -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
}

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
/* v2.23: every number on the board uses tabular digits, so figures in a column line up and
   a changing value does not jitter. body sets it; form controls do not inherit it. */
button, input, select, textarea { font-variant-numeric: tabular-nums; }

/* `el.hidden = true` must actually hide the element — everywhere, once.
   The browser's own [hidden]{display:none} lives in the UA stylesheet, which ANY author
   `display` beats on specificity. So every element this file gives a display to (flex
   rows, grids, the clock) silently ignored its own hidden attribute, and each site that
   noticed patched it locally: .addOpen, .addFoot, .credLocked, .histErr, .demoBar all
   carried their own copy of this rule. The ones nobody had hit yet were simply broken —
   `header.clock: false` set the attribute and the clock stayed on screen.
   One global rule with !important, so the next element to get a `display` cannot
   reintroduce the bug and no one has to remember the workaround. */
[hidden] { display: none !important; }

/* v2.22 page width: everything at page level sits in one column with a side margin (--gutter-min:
   30px, 16px on phones).
   v2.34: the column is the whole window -- no centring, no side margin beyond the gutter. Cards
   are as wide as their grid column; what keeps them from looking stretched is the fixed cell
   size (--cell-min .. --cell-max, below) and the card layouts that switch with the card's width
   (machine: narrow / standard / wide). --page-max stays for the rules that centre on it (100%:
   nothing to centre). The column COUNT lives with the grid (.grid, below). */
:root { --page-max: 100%; --gutter-min: var(--s6);
  /* v2.34 a cell (.kv .item) is 108-150px wide wherever it is: it never shrinks below what its
     widest ordinary reading needs at the normal font (Network "↓1.5M ↑8.5M" 91px + 2 x 9px
     padding + border), and never grows past 150px -- a wider box only adds width to the gaps
     between cells (justify-content: space-between), never to a cell. */
  --cell-min: 108px; --cell-max: 150px;
  /* v2.37 the narrowest a machine card may get from the column count; packGrid() reads it. The cells
     alone fit 258px (narrow layout, see .grid below), the header does not: an English title +
     "2× 7900 XTX" + uptime is cut below 278px even at "0d 1h", and 300px leaves room for "120d 12h". */
  --machine-min: 300px; }
@media (max-width: 560px) { :root { --gutter-min: var(--s4); } }

html, body {
  margin: 0; min-height: 100%;
  overflow-x: hidden; /* B12-row: page never grows past the viewport (stable stack measurement) */
  color: var(--text);
  background:
    radial-gradient(1100px 600px at 8% -5%, rgba(79,227,208,0.10), transparent 55%),
    radial-gradient(1000px 620px at 95% 0%, rgba(177,140,255,0.09), transparent 55%),
    radial-gradient(900px 500px at 50% 120%, rgba(90,166,255,0.07), transparent 60%),
    linear-gradient(180deg, var(--bg0), var(--bg1) 40%, var(--bg2));
  background-attachment: fixed;
  font-family: var(--font);
  font-size: 14px;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.1px;
}

/* ---------- 顶栏 ---------- */
.topbar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
          gap: var(--s3); row-gap: var(--s2);
          padding: var(--s5) max(var(--gutter-min), calc((100% - var(--page-max)) / 2)) var(--s3); }
.brand { display: flex; align-items: center; gap: var(--s3); }
.brand .logo { color: var(--cyan); font-size: 18px; filter: drop-shadow(0 0 8px rgba(79,227,208,0.6)); }
.brand h1 { font-size: 17px; font-weight: 600; margin: 0; letter-spacing: 1px; }
/* gap 用 --s6:Online 与右侧时钟之间要留出明显空档 */
.meta { display: flex; align-items: center; gap: var(--s6); font-size: 12px; color: var(--muted); }
.conn { display: inline-flex; align-items: center; gap: var(--s2); }
.conn::before { content: ""; width: 8px; height: 8px; border-radius: 50%; background: var(--muted2); }
.conn.ok::before { background: var(--green); box-shadow: 0 0 10px var(--green); animation: breathe 2.6s ease-in-out infinite; }
.conn.warn::before { background: var(--yellow); box-shadow: 0 0 10px var(--yellow); animation: breathe 2.6s ease-in-out infinite; } /* B15: reconnecting */
.conn.bad::before { background: var(--red); box-shadow: 0 0 10px var(--red); }

/* ---------- 双时区时钟(右上) ---------- */
/* 两行左对齐,让 ☀/☾ 在竖直方向对齐成一列;整块相对 ● Online 垂直居中。 */
.clock { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; line-height: 1.35; }
.tzrow { display: inline-flex; align-items: center; gap: var(--s2); }
/* 时间等宽:否则秒数变化时字宽抖动,整行会左右跳。 */
.tztime { font-variant-numeric: tabular-nums; }
/* 图标用单色文本符号(☀ U+2600+FE0E / ☾ U+263E),不是彩色 emoji —— 彩色 emoji 由字体
   自带调色板绘制,会无视 CSS color。字体栈把 Segoe UI Symbol 提到最前,避免 Windows 回退
   到 Segoe UI Emoji 又变回彩色;font-variant-emoji 是新浏览器上的第二道保险。 */
.tzicon {
  font-style: normal; line-height: 1;
  font-family: "Segoe UI Symbol", "Segoe UI", "Apple Symbols", var(--font);
  font-variant-emoji: text;
  font-size: 13px; width: 1.15em; height: 1em;
  display: inline-flex; align-items: center; justify-content: center;
}
/* 太阳:文本字形,细笔画在暗底发虚,补一层同色辉光。 */
.tzicon .ic-sun { text-shadow: 0 0 7px currentColor; }
/* 月亮:内联 SVG,两圆布尔差(大圆 r7 挖去偏左上的 r6.2)得到饱满月牙,缺口朝左上,
   与 emoji 月亮同向。fill=currentColor 所以跟着 .tzrow 的冷色走。
   0.88em 是与 ☀ 字形对齐后的视觉配重:SVG 会铺满 em 盒,而字形只占其中约七成,
   直接给 1em 月亮会明显比太阳胖一圈。 */
.tzicon .ic-moon { width: 0.88em; height: auto; display: block; filter: drop-shadow(0 0 5px currentColor); }
/* 两个图标常驻 DOM,按昼夜切显隐 —— JS 只需切 class,不写图标内容。 */
.tzicon .ic-sun, .tzicon .ic-moon { display: none; }
.tzrow.day   .ic-sun  { display: block; }
.tzrow.night .ic-moon { display: block; }
.tzname { font-weight: 600; letter-spacing: 0.5px; min-width: 1.7em; }
/* 整行统一上色:图标/标签/时间都继承,冷暖一眼可分。 */
.tzrow.day   { color: var(--yellow); }
.tzrow.night { color: var(--blue); }

/* ---------- 演示引导条(P2) ---------- */
/* 只在服务端 demo_mode 时出现。用琥珀色而不是红色:这不是错误,是"这还不是你的板子"。 */
.demoBar {
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--s3);
  margin: 0 max(var(--gutter-min), calc((100% - var(--page-max)) / 2)) var(--s3); padding: 10px var(--s4);
  border-radius: 12px;
  background: color-mix(in srgb, var(--yellow) 9%, var(--glass));
  border: 1px solid color-mix(in srgb, var(--yellow) 32%, transparent);
  font-size: 13px;
}
.demoBar .demoDot {
  width: 8px; height: 8px; border-radius: 50%; flex: none;
  background: var(--yellow); box-shadow: 0 0 8px var(--yellow);
  animation: breathe 2.6s ease-in-out infinite;
}
.demoBar .demoText { flex: 1 1 240px; color: var(--text); }
.demoBar .demoText.busy { color: var(--muted); }
.demoBar .demoText.bad  { color: var(--red); }
/* 关闭键推到最右,和两个动作键之间留出距离 —— 免得"关掉提示"和"清空演示"挨在一起。 */
.demoBar .demoHide {
  margin-left: var(--s2); flex: none;
  background: none; border: none; color: var(--muted2);
  font-size: 17px; line-height: 1; cursor: pointer; padding: 2px 6px; border-radius: 6px;
}
.demoBar .demoHide:hover { color: var(--text); background: var(--glass2); }

/* ---------- 空板(清空演示后 / 全新实例) ---------- */
/* 空网格是正常状态,不是加载失败 —— 所以写成邀请,而不是错误。 */
.emptyBoard {
  grid-column: 1 / -1;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--s2); padding: 64px var(--s4); text-align: center;
  border: 1px dashed var(--hair2); border-radius: var(--radius);
  background: var(--card-bg);
}
.emptyBoard .emptyTitle { font-size: 15px; font-weight: 600; letter-spacing: 0.5px; }
.emptyBoard .emptyHint  { font-size: 12.5px; color: var(--muted2); line-height: 1.6; }

/* ---------- 服务健康条 ---------- */
.chips { display: flex; flex-wrap: wrap; gap: var(--s2); padding: 0 max(var(--gutter-min), calc((100% - var(--page-max)) / 2)) var(--s2); }
.chip {
  display: inline-flex; align-items: center; gap: var(--s2); white-space: nowrap;
  padding: 8px 14px; border-radius: 999px;
  background: var(--glass); border: 1px solid var(--hair);
  font-size: 12.5px; font-weight: 500;
  /* §0: 无 backdrop-filter blur */
}
.chip .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--muted2); }
.chip.online .dot { background: var(--green); box-shadow: 0 0 8px var(--green); animation: breathe 2.6s ease-in-out infinite; }
.chip.offline .dot { background: var(--red); box-shadow: 0 0 8px var(--red); }
.chip.unknown .dot { background: var(--muted2); }
.chip.cut .dot { background: var(--yellow); box-shadow: 0 0 8px var(--yellow); }   /* v2.39 连线中断 */
.chip.testing .dot { background: var(--cyan); box-shadow: 0 0 8px var(--cyan); }    /* v2.40 测试中 */
.node-route-fb { color: var(--muted); }                                                 /* v2.40 备援路由 */
.chip .lat { color: var(--muted2); font-size: 11px; }

/* ---------- 卡片网格 ---------- */
/* v2.22 grid. A card takes 1 column by default; layout `span: 2` / `span: full` widens it
   (span-2 falls back to the whole row when there is only one column). Stretch makes every card
   in a row as tall as the tallest.
   v2.23: WHERE each card goes is decided by packGrid() in app.js (explicit grid-row /
   grid-column, rows closed by widening `stretch` cards, never a hole); the span classes
   below are only the no-JS fallback.
   v2.33: the column count divides the number of machine cards, so they fill every row.
   v2.37: packGrid() works the count out and writes it to --cols (1 here is only the value before
   the first pack): the largest divisor d of the machine-card count with
     d x --machine-min + (d - 1) x 16px gap <= the room inside .grid's padding
   (.grid's own width, so a desktop scrollbar is already taken off). Adding or removing a machine
   needs no change here. Nine cards: 3 columns from 992px, 9 from 2888px (2 x 30px gutter included).
   The card layouts switch with the card's own width (card = cells + 2 x 16px padding + 2 x 1px border):
     narrow   cells 2 x 108 + 8          = 224 inside  -> card >= 258px (one column on a phone)
     standard cells 3 x 108 + 2 x 8      = 340 inside  -> card >= 374px
     wide     rings 216 + 22 + cells 340 = 578 inside  -> card >= 612px */
.board { container: board / inline-size; }
.grid { --cols: 1; }
.grid { display: grid; gap: var(--s4);
  padding: var(--s4) max(var(--gutter-min), calc((100% - var(--page-max)) / 2)) var(--s5);
  grid-template-columns: repeat(var(--cols), minmax(0, 1fr));
  align-items: stretch; }
.grid > .card.span-2 { grid-column: span 2; }
.grid > .card.span-full, .grid > .card.card-wide { grid-column: 1 / -1; }
.card {
  position: relative; border-radius: var(--radius); padding: var(--s4) var(--s4) var(--s5);
  min-width: 0; /* grid item: stay within track (don't let wide content grow the card) */
  /* v2.22: card internals respond to the CARD's width (@container), not the screen's */
  container-type: inline-size; container-name: card;
  /* v2.23: header + body as a column; the body takes whatever height the grid row gives
     the card, so a card stretched to its row can grow its content (token chart) into it */
  display: flex; flex-direction: column;
  background:
    linear-gradient(180deg, rgba(255,255,255,0.03), rgba(255,255,255,0.006) 40%),
    var(--card-bg);
  border: 1px solid transparent;
  background-clip: padding-box;
  /* v1 三层外圈光晕：落影 + 发丝 + 身份辉光(§0 第三层 0 4px 20px -10px,20px 版) */
  box-shadow:
    0 8px 24px -8px rgba(0,0,0,0.6),
    0 0 0 1px rgba(255,255,255,0.02),
    0 4px 20px -10px var(--accent, rgba(80,120,180,0.22));
  /* §0: 无 backdrop-filter blur */
  transition: transform .35s cubic-bezier(.2,.7,.2,1), box-shadow .35s;
}
/* 渐变描边（伪元素遮罩，不吃布局；§0: 静态，无 conic 流光动画） */
.card::before {
  content: ""; position: absolute; inset: 0; border-radius: var(--radius); padding: 1px;
  background: linear-gradient(160deg, var(--hair2), transparent 45%, rgba(79,227,208,0.10));
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor; mask-composite: exclude;
  pointer-events: none;
}
.card:hover { transform: translateY(-3px); box-shadow: 0 22px 50px -18px rgba(0,0,0,0.9), 0 3px 10px -4px rgba(0,0,0,0.7); }
.card h2 {
  font-size: 12.5px; font-weight: 600; margin: 0 0 var(--s4);
  color: var(--muted); letter-spacing: 1px;
  display: flex; align-items: center; gap: var(--s2);
  /* v2.27: one line -- the title keeps its words, the tag on the right gives way (fit.js
     shrinks it to 10px, then cuts it with the full text in title) */
  white-space: nowrap; min-width: 0; line-height: 18px;
}
.card h2 .tag { margin-left: auto; font-size: 11px; color: var(--muted2); font-weight: 500; letter-spacing: 0.5px;
  display: block; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.card h2 .tag .tag-sep { margin: 0 var(--s2); vertical-align: middle; }
/* B13: vertical hairline between combined header_right parts (e.g. badge │ uptime) */
.tag-sep { width: 1px; height: 12px; display: inline-block; vertical-align: middle; background: linear-gradient(180deg, transparent, var(--hair2), transparent); }
/* v2.32 the purpose pill after a machine card's title (target `role`: LLM / ComfyUI) */
.card h2 .role { flex: none; font-size: 10.5px; font-weight: 600; letter-spacing: 0.5px; line-height: 14px; padding: 1px 7px; border-radius: 9px;
  color: var(--accent, var(--muted)); border: 1px solid color-mix(in srgb, var(--accent, var(--muted)) 45%, transparent); }
/* v2.32 an always_on:false machine that is off: 未开机 where the uptime was, grey, never red */
.card h2 .tag .off-word { color: var(--muted); }
.mach.mach-off { filter: grayscale(0.85); }
/* ===== 身份识别色:染 h2 + 实体 border(辉光由 .card 基规则第三层 var(--accent) 承载,v1 同构)===== */
.card[style*="--accent"] h2 { color: var(--accent); }
.card[style*="--accent"] {
  border-color: color-mix(in srgb, var(--accent) 30%, transparent);
}
.card.clickable { cursor: pointer; }
.card > .card-body { flex: 1 1 auto; min-width: 0; }
.card.stale { opacity: 0.55; }

/* ---------- B12: 堆叠卡(多 service 共享外框,各留头部)---------- */
.stack-inner { display: flex; flex-direction: column; }        /* default + column fallback */
.stack.is-row .stack-inner { flex-direction: row; align-items: stretch; } /* app.js adds .is-row when wide enough */
.stack.is-row .stack-item { flex: 1; min-width: 0; } /* 水平等分 */
.stack-item h2 {
  font-size: 12.5px; font-weight: 600; margin: 0 0 var(--s3);
  color: var(--muted); letter-spacing: 1px;
  display: flex; align-items: center; gap: var(--s2);
}
.stack-item[style*="--accent"] h2 { color: var(--accent); } /* 各子卡身份色头部 */
.stack-item h2 .tag { margin-left: auto; font-size: 11px; color: var(--muted2); font-weight: 500; letter-spacing: 0.5px;
  min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.stack-item h2 { white-space: nowrap; min-width: 0; }
.stack-div { flex: 0 0 auto; height: 1px; width: auto; margin: var(--s3) 0; background: linear-gradient(90deg, transparent, var(--hair), transparent); }
.stack.is-row .stack-div { width: 1px; height: auto; align-self: stretch; margin: 0 var(--s3); background: linear-gradient(180deg, transparent, var(--hair), transparent); }
/* B12-row: offline child keeps a full equal slot (row); greyed to signal offline.
   Column mode hides the skeleton to stay compact. */
.stack-item { display: flex; flex-direction: column; } /* let the offline body fill the row height */
.stack-item-offline { filter: grayscale(0.65) opacity(0.72); } /* identity color → desaturated/grey */
.stack-offline { flex: 1; display: flex; flex-direction: column; justify-content: center; gap: var(--s3); }
.stack-offline-status { text-align: center; color: var(--muted2); font-size: 12px; letter-spacing: 0.5px; }
.stack:not(.is-row) .stack-item-offline .kv { display: none; }

/* Stack: align section title->first KV gap with regular cards (was 45px vs 33px) */
.stack .stack-item h2 { margin-bottom: 0; }

/* Stack: KV boxes single-file while a stack child is narrow; v2.23: a stack child is a
   size container, so a wide one lays its cells out like a card would (rules by data-n). */
.stack-item { container-type: inline-size; }
.stack .stack-item .kv { display: grid; grid-template-columns: minmax(0, 1fr); }
@container (min-width: 300px) { .stack .stack-item .kv[data-n="2"], .stack .stack-item .kv[data-n="4"], .stack .stack-item .kv[data-n="6"] { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@container (min-width: 480px) { .stack .stack-item .kv[data-n="3"], .stack .stack-item .kv[data-n="6"] { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@container (min-width: 640px) { .stack .stack-item .kv[data-n="4"] { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
h2:empty { display: none; } /* 堆叠卡外框无标题时不留空行 */

/* ---------- v2.20 推理节点 / Inference Nodes(全宽,一机一栏)---------- */
/* v2.23: one grid for the whole card. Each node column is a subgrid of its rows (head,
   model, then --r rows of cells, then the concurrency bar), so a row of cells is the same
   height in every column. Columns are separated by a hairline drawn in the 33px gap.
   v2.34: the cells are fixed-size (--cell-min .. --cell-max, extra width goes to the gaps) and
   the card switches arrangement with its width instead of stretching them. Per node column:
     --kn 2  cells 2 x 3, in rows   (status | latency, prefill | decode, requests | tokens)
     --kn 3  cells 3 x 2, in columns (health, speed, usage) -- as before
     --kn 6  six in one row
   and the columns stand side by side once each has room for two cells, else they stack.
   Thresholds (card content width; 2 cells 224px, 3 cells 340px, 6 cells 6 x 108 + 5 x 6 = 678px):
     nodes (3 columns)    stacked: 2 <340, 3 <678, 6 <738;  side by side: 2 from 738, 3 from 1086, 6 from 2100
     ComfyUI (2 columns)  stacked: 2 <340, 3 <481;          side by side: 2 from 481, 3 from 713, 6 from 1389
   A mode only sets variables (below); --st-N is the column template of an N-stream concurrency
   bar: the cell grid's own columns (subgrid) when N fills them, else N fixed-size cells. */
.card.card-wide { grid-column: 1 / -1; }
.nodes-grid {
  --kn: 2; --r: 3; --r1: 4; --kflow: row; --kgap: 8px;
  --st-2: subgrid; --sg-2: normal; --sg-3: var(--kgap); --st-3: repeat(1, minmax(var(--cell-min), var(--cell-max))); --st-4: subgrid; --sg-4: normal;
  --sg-5: var(--kgap); --st-5: repeat(1, minmax(var(--cell-min), var(--cell-max))); --st-6: subgrid; --sg-6: normal;
  display: grid; grid-template-columns: minmax(0, 1fr); grid-template-rows: none; column-gap: 33px; row-gap: 0; }
.node-col { position: relative; min-width: 0; display: grid; grid-template-rows: auto auto repeat(var(--r), auto) auto; padding: var(--s3) 0; }
.node-col + .node-col { border-top: 1px solid var(--hair); }
.node-col:first-child { padding-top: 0; }
.node-col:last-child { padding-bottom: 0; }
.node-col + .node-col::before { content: none; position: absolute; left: -17px; top: 0; bottom: 0; width: 1px; background: var(--hair); }
/* v2.27: head and model are one line each (nowrap); the route part / the model name give
   way first -- shrink, then ellipsis with the full text in title (fit.js) */
.node-head { grid-row: 1; display: flex; align-items: center; flex-wrap: nowrap; gap: var(--s2); min-width: 0; white-space: nowrap;
  font-size: 13px; font-weight: 600; letter-spacing: 1px; color: var(--accent, var(--muted)); }
.node-head > .node-name, .node-head > .node-pub { flex: none; }
.node-head > .node-port { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.node-port { color: var(--muted); font-weight: 500; letter-spacing: 0.5px; }
.node-pub {
  font-size: 10.5px; font-weight: 600; letter-spacing: 0.5px; padding: 1px 7px; border-radius: 9px;
  color: var(--green); border: 1px solid color-mix(in srgb, var(--green) 45%, transparent);
}
.node-route { color: var(--muted); }
.node-route-stale { color: var(--muted2); }
.node-route-stale s { text-decoration-thickness: 1px; }
.node-route-pending { color: var(--muted2); font-style: italic; }
.node-route-sep { margin: 0 4px; color: var(--muted2); }
.node-model { grid-row: 2; margin-top: var(--s2); font-size: 12px; line-height: 16px; height: 16px; color: var(--text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
.node-model.is-last { color: var(--muted2); font-style: italic; }
/* (.kv.node-kv: must outrank the generic .kv grid further down) Cells flow down the
   columns: with three per row the columns are health (status / latency), speed (prefill /
   decode) and usage (requests / tokens), and the short health readings get the narrowest
   column; with six per row they sit side by side in the same order, equal width. */
.kv.node-kv { grid-row: 3 / span var(--r); grid-template-rows: subgrid; row-gap: var(--s2); grid-auto-flow: var(--kflow);
  grid-template-columns: repeat(var(--kn), minmax(var(--cell-min), var(--cell-max))); column-gap: var(--kgap);
  justify-content: space-between; margin-top: var(--s3); }
/* (a node cell is the standard cell; v2.28: every node cell has a second line in every
   state, so the node rows are always three lines, 70px, and line up across the columns; a
   status such as "Generating 2/3" that does not fit its column shrinks, then reads "Gen 2/3") */
/* always_on:false 且连不上:整栏变灰(不是故障,不着红)*/
.node-idle { filter: grayscale(0.85) opacity(0.62); }
.node-idle-text { color: var(--muted); }
/* (v2.34 modes, see the head of this section) */
@container card (min-width: 340px)  { .nodes-grid { --kn: 3; --r: 2; --r1: 3; --kflow: column; --kgap: 8px; --sg-2: var(--kgap); --st-2: repeat(2, minmax(var(--cell-min), var(--cell-max))); --st-3: subgrid; --sg-3: normal; --sg-4: var(--kgap); --st-4: repeat(2, minmax(var(--cell-min), var(--cell-max))); --sg-5: var(--kgap); --st-5: repeat(1, minmax(var(--cell-min), var(--cell-max))); --st-6: subgrid; --sg-6: normal; } }
@container card (min-width: 678px)  { .nodes-grid:not(.comfy-grid) { --kn: 6; --r: 1; --r1: 2; --kflow: column; --kgap: 6px; --sg-2: var(--kgap); --st-2: repeat(2, minmax(var(--cell-min), var(--cell-max))); --sg-3: var(--kgap); --st-3: repeat(3, minmax(var(--cell-min), var(--cell-max))); --sg-4: var(--kgap); --st-4: repeat(4, minmax(var(--cell-min), var(--cell-max))); --sg-5: var(--kgap); --st-5: repeat(5, minmax(var(--cell-min), var(--cell-max))); --st-6: subgrid; --sg-6: normal; } }
@container card (min-width: 738px)  {
  .nodes-grid:not(.comfy-grid) { grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-rows: auto auto repeat(var(--r), auto) auto; }
  .nodes-grid:not(.comfy-grid) > .node-col { grid-row: 1 / -1; grid-template-rows: subgrid; padding: 0; border-top: 0; }
  .nodes-grid:not(.comfy-grid) > .node-col + .node-col::before { content: ""; }
  .nodes-grid:not(.comfy-grid) { --kn: 2; --r: 3; --r1: 4; --kflow: row; --kgap: 8px; --st-2: subgrid; --sg-2: normal; --sg-3: var(--kgap); --st-3: repeat(1, minmax(var(--cell-min), var(--cell-max))); --st-4: subgrid; --sg-4: normal; --sg-5: var(--kgap); --st-5: repeat(1, minmax(var(--cell-min), var(--cell-max))); --st-6: subgrid; --sg-6: normal; }
}
@container card (min-width: 1086px) { .nodes-grid:not(.comfy-grid) { --kn: 3; --r: 2; --r1: 3; --kflow: column; --kgap: 8px; --sg-2: var(--kgap); --st-2: repeat(2, minmax(var(--cell-min), var(--cell-max))); --st-3: subgrid; --sg-3: normal; --sg-4: var(--kgap); --st-4: repeat(2, minmax(var(--cell-min), var(--cell-max))); --sg-5: var(--kgap); --st-5: repeat(1, minmax(var(--cell-min), var(--cell-max))); --st-6: subgrid; --sg-6: normal; } }
@container card (min-width: 2100px) { .nodes-grid:not(.comfy-grid) { --kn: 6; --r: 1; --r1: 2; --kflow: column; --kgap: 6px; --sg-2: var(--kgap); --st-2: repeat(2, minmax(var(--cell-min), var(--cell-max))); --sg-3: var(--kgap); --st-3: repeat(3, minmax(var(--cell-min), var(--cell-max))); --sg-4: var(--kgap); --st-4: repeat(4, minmax(var(--cell-min), var(--cell-max))); --sg-5: var(--kgap); --st-5: repeat(5, minmax(var(--cell-min), var(--cell-max))); --st-6: subgrid; --sg-6: normal; } }
@container card (min-width: 481px)  {
  .nodes-grid.comfy-grid { grid-template-columns: repeat(var(--ncols, 2), minmax(0, 1fr)); grid-template-rows: auto auto repeat(var(--r), auto) auto; }
  .nodes-grid.comfy-grid > .node-col { grid-row: 1 / -1; grid-template-rows: subgrid; padding: 0; border-top: 0; }
  .nodes-grid.comfy-grid > .node-col + .node-col::before { content: ""; }
  .nodes-grid.comfy-grid { --kn: 2; --r: 3; --r1: 4; --kflow: row; --kgap: 8px; --st-2: subgrid; --sg-2: normal; --sg-3: var(--kgap); --st-3: repeat(1, minmax(var(--cell-min), var(--cell-max))); --st-4: subgrid; --sg-4: normal; --sg-5: var(--kgap); --st-5: repeat(1, minmax(var(--cell-min), var(--cell-max))); --st-6: subgrid; --sg-6: normal; }
}
@container card (min-width: 713px)  { .nodes-grid.comfy-grid { --kn: 3; --r: 2; --r1: 3; --kflow: column; --kgap: 8px; --sg-2: var(--kgap); --st-2: repeat(2, minmax(var(--cell-min), var(--cell-max))); --st-3: subgrid; --sg-3: normal; --sg-4: var(--kgap); --st-4: repeat(2, minmax(var(--cell-min), var(--cell-max))); --sg-5: var(--kgap); --st-5: repeat(1, minmax(var(--cell-min), var(--cell-max))); --st-6: subgrid; --sg-6: normal; } }
@container card (min-width: 1389px) { .nodes-grid.comfy-grid { --kn: 6; --r: 1; --r1: 2; --kflow: column; --kgap: 6px; --sg-2: var(--kgap); --st-2: repeat(2, minmax(var(--cell-min), var(--cell-max))); --sg-3: var(--kgap); --st-3: repeat(3, minmax(var(--cell-min), var(--cell-max))); --sg-4: var(--kgap); --st-4: repeat(4, minmax(var(--cell-min), var(--cell-max))); --sg-5: var(--kgap); --st-5: repeat(5, minmax(var(--cell-min), var(--cell-max))); --st-6: subgrid; --sg-6: normal; } }

/* v2.26 concurrency bar (a node that runs several requests at once): one cell per stream,
   in the last row of the node's cell grid. When the stream count fits the cell columns
   (3 streams on 3 or 6 columns, 2 on 6, 6 on 3 or 6) it takes the grid's own columns
   (subgrid), so its cells line up with the six above; otherwise it has --mc equal columns,
   a divisor of the stream count, so every row is full. Idle streams are greyed. */
.kv.node-kv.has-streams { grid-row: 3 / span var(--r1); }
.node-streams { grid-row: -2 / -1; grid-column: 1 / -1; display: grid; gap: var(--s2); column-gap: var(--kgap);
  grid-template-columns: repeat(1, minmax(var(--cell-min), var(--cell-max))); justify-content: space-between; }
/* (a subgrid bar keeps the cell grid's gaps -- column-gap: normal -- or its edge cells would
   take the difference and outgrow --cell-max; --sg-N is set with --st-N) */
.node-streams[data-m="2"] { grid-template-columns: var(--st-2); column-gap: var(--sg-2); }
.node-streams[data-m="3"] { grid-template-columns: var(--st-3); column-gap: var(--sg-3); }
.node-streams[data-m="4"] { grid-template-columns: var(--st-4); column-gap: var(--sg-4); }
.node-streams[data-m="5"] { grid-template-columns: var(--st-5); column-gap: var(--sg-5); }
.node-streams[data-m="6"] { grid-template-columns: var(--st-6); column-gap: var(--sg-6); }
.node-streams .item.stream-idle { opacity: 0.55; }
.node-streams .item.stream-idle .value { color: var(--muted2); }
/* the progress bar sits on the cell's bottom edge (absolute), so it adds no height */
.node-streams .sbar { position: absolute; left: 9px; right: 9px; bottom: 4px; height: 3px; border-radius: 2px; background: var(--track); overflow: hidden; }
.node-streams .sbar > i { display: block; height: 100%; background: var(--cyan); border-radius: 2px; }
.node-streams .sbar.none { visibility: hidden; }

/* ---------- 环形仪表 ---------- */
.rings { display: flex; gap: var(--s4); flex-wrap: wrap; }
.ring-wrap { display: flex; flex-direction: column; align-items: center; gap: var(--s2); flex: 1; min-width: 72px; }
.ring {
  --p: 0; --c: var(--blue);
  width: 80px; height: 80px; border-radius: 50%;
  background: conic-gradient(var(--c) calc(var(--p) * 1%), var(--track) 0);
  display: grid; place-items: center; position: relative;
  transition: background .6s ease;
  filter: drop-shadow(0 0 8px var(--c, rgba(80,140,200,0.5)));
}
.ring::before { content: ""; position: absolute; inset: 7px; border-radius: 50%; background: radial-gradient(circle at 50% 35%, #0e1626, #070b14); }
.ring b { position: relative; z-index: 1; font-size: 17px; font-weight: 500; letter-spacing: 0.3px; }
/* v2.38 空闲轮询: two short lines in the ring's centre, grey (server/idle_spin.js) */
.ring.idle-spin b { display: flex; flex-direction: column; align-items: center; font-size: 12px; line-height: 1.2; letter-spacing: 0; color: var(--muted); }
.legend .sw.sw-band { height: 10px; background: rgba(148,163,184,0.22); outline: 1px solid rgba(148,163,184,0.35); }
.ring.idle-spin::after { display: none; }   /* no comet: nothing is working */
.ring-label { font-size: 11.5px; color: var(--muted); letter-spacing: 0.5px; }
/* 绕环跑的一道流光：亮白头 + 状态色拖尾（§6 保留）
   v2.22: the comet used to spin the whole ::after with transform: rotate(). A rotating
   square's bounding box pokes out of the ring at every angle but 0/90/180/270, which is
   real layout overflow (scrollWidth up to 96px on an 80px ring) even though the masked
   arc never leaves the circle. Now the box stays put, inside the ring, and only the
   gradient's start angle turns (an animatable registered property), so nothing overflows.
   inset 0 (was -2px) keeps the arc inside the ring's own band. */
@property --comet { syntax: '<angle>'; inherits: false; initial-value: 0deg; }
.ring::after {
  content: ""; position: absolute; inset: 0; border-radius: 50%;
  background: conic-gradient(from calc(-90deg + var(--comet)),
    transparent 0deg,
    color-mix(in srgb, var(--c) 12%, transparent) 22deg,
    color-mix(in srgb, var(--c) 50%, transparent) 38deg,
    color-mix(in srgb, var(--c) 90%, white 35%) 46deg,
    #ffffff 49deg,
    color-mix(in srgb, var(--c) 90%, white 35%) 52deg,
    transparent 58deg);
  -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 5px), #000 calc(100% - 4px));
          mask: radial-gradient(farthest-side, transparent calc(100% - 5px), #000 calc(100% - 4px));
  filter: drop-shadow(0 0 5px color-mix(in srgb, var(--c) 75%, white 25%));
  mix-blend-mode: screen;
  animation: comet 3s linear infinite;
  pointer-events: none; z-index: 3;
}
@keyframes comet { to { --comet: 360deg; } }

/* ---------- 键值区（玻璃底块）---------- */
/* v2.28 cell: its lines and the uniform padding, nothing more -- label (14px) and value
   (20px) make a two-line cell (54px); a third line (14px) makes it 70px. No line is held
   in reserve. In every cell on the board (machine, service, info, stack, inference nodes)
   nothing wraps: every line is nowrap and clipped, and fit.js shrinks a line that does not
   fit (value down to 13px, label / third line down to 10px), then swaps in its short form,
   and only then cuts it with an ellipsis (full text in title).
   Cells in one row have the same number of lines (fit.js lineRows): a row keeps its third
   lines only when every cell in it has one; in any other row a cell's third line joins its
   value line (.sub-inline: smaller grey text after the value, "552 · via LLM 388"), and
   when it does not fit there it moves into the cell's title -- no cell grows a line for
   itself. Rows are as tall as their cells and nothing is stretched (align-items: start). */
/* (v2.34: side padding 9px, was 11px -- a 108px cell keeps 88px for its text) */
:root { --cell-l: 14px; --cell-v: 20px; --cell-label: 10.5px; --cell-value: 15.5px; --cell-pad: 8px 9px; }
.kv { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); grid-auto-rows: auto; grid-auto-flow: row dense;
  gap: var(--s2); margin-top: var(--s4); align-items: start; }
.kv .item {
  display: grid; grid-auto-rows: auto; grid-template-columns: minmax(0, 1fr);
  row-gap: 2px; align-content: start; min-width: 0; overflow: hidden; position: relative;
  background: linear-gradient(180deg, rgba(255,255,255,0.045), rgba(255,255,255,0.012));
  border: 1px solid var(--hair);
  border-radius: 11px;
  padding: var(--cell-pad);
  box-shadow: 0 0 10px -2px var(--accent, rgba(80,120,180,0.15));
  transition: border-color .3s, background .3s;
}
.kv .item:hover { border-color: var(--hair2); }
.kv .item > .label, .kv .item > .value, .kv .item > .sub, .kv .item > .alt-form > .value, .kv .item > .alt-form > .sub {
  display: block; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* a value is clipped only once fit.js has had to cut it (.is-cut): until then its status glow
   is not cut flat at the line's edges (the cell's own edge still clips anything stray) */
.kv .item > .value, .kv .item > .alt-form > .value { overflow: visible; }
.kv .item > .value.is-cut, .kv .item > .alt-form > .value.is-cut { overflow: hidden; }
.kv .item .label { grid-row: 1; font-size: var(--cell-label); line-height: var(--cell-l); color: var(--muted2); letter-spacing: 0.6px; text-transform: uppercase; opacity: 0.7; text-shadow: none; }
.kv .item .value { grid-row: 2; font-size: var(--cell-value); line-height: var(--cell-v); font-weight: 500; letter-spacing: 0.2px; }
.kv .item .sub { grid-row: 3; font-size: var(--cell-label); line-height: var(--cell-l); color: var(--muted); text-shadow: none; }
/* v2.28 third line on the value line: value first (it keeps its full width, the column is
   max-content), then the third line in what is left, baselines aligned, a "·" drawn in its
   left padding (absolute, so fit.js measures the text alone). .sub-away: it did not fit even
   at its floor or in its short form -- it is in the cell's title instead. */
.kv .item.sub-inline { grid-template-columns: minmax(0, max-content) minmax(0, 1fr); }
.kv .item.sub-inline > .label { grid-column: 1 / -1; }
.kv .item.sub-inline > .value, .kv .item.sub-inline > .alt-form > .value { grid-row: 2; grid-column: 1; align-self: baseline; }
.kv .item.sub-inline > .sub, .kv .item.sub-inline > .alt-form > .sub { grid-row: 2; grid-column: 2; align-self: baseline;
  position: relative; padding-left: 13px; color: var(--muted2); }
.kv .item.sub-inline > .sub::before, .kv .item.sub-inline > .alt-form > .sub::before { content: "·"; position: absolute; left: 4px; }
.kv .item.sub-inline > .sub.sub-away, .kv .item.sub-inline > .alt-form > .sub.sub-away { display: none; }
/* v2.46 「上次 xx」/ Last xx on the Prefill / Decode value line (.vlast): right of the value, small and
   grey, against the cell's right edge, in a column of its own -- the value keeps its full width, the
   cell its size. fit.js shrinks it (down to 10px), then takes its shorter forms ("上次 194", "上 194",
   "194"); the full text is in the title. (A row whose third lines join the value line has no
   room for it: there it is left out, the cell's title still says it.) */
.kv .item.has-last { grid-template-columns: minmax(0, max-content) minmax(0, 1fr); }
.kv .item.has-last > .label, .kv .item.has-last > .sub { grid-column: 1 / -1; }
.kv .item.has-last > .value { grid-column: 1; align-self: baseline; }
.kv .item > .vlast { grid-row: 2; grid-column: 2; align-self: baseline; justify-self: end; display: block; min-width: 0; max-width: 100%;
  padding-left: 6px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  font-size: var(--cell-label); line-height: var(--cell-l); color: var(--muted2); text-shadow: none; }
.kv .item.has-last.sub-inline > .vlast { display: none; }
/* (no shorter form fits either -- a five-figure value in the narrowest cell: it is left out rather
   than cut, the cell's title says it) */
.kv .item > .vlast.is-cut { visibility: hidden; }
/* B20 parts metric (one number per disk, each colored by its own level): one line, the bay
   number a tag after each figure. When the line does not fit even at 13px, fit.js shows the
   .alt-form instead: the hottest disk (with its bay) and the coolest, "max · min" under them.
   The cell's border still takes the worst disk's color either way. */
.kv .item .value i { font-style: normal; }
.kv .item .value.parts i + sup + i, .kv .item .value.parts i + i { margin-left: 7px; }
.kv .item > .alt-form { display: none; }
.kv .item.is-alt > .full-form { display: none; }
.kv .item.is-alt > .alt-form { display: contents; }
.kv .item .mm-sep { margin: 0 5px; color: var(--muted2); text-shadow: none; }
/* Part label (e.g. the DSM bay number): a tag, not data -- small and always the muted
   grey, never the value's status color, so it cannot be mistaken for a reading. 10px is
   the smallest text on the board. */
.kv .item .value sup { font-size: 10px; font-weight: 400; color: var(--muted2); opacity: 0.85; margin-left: 1px; vertical-align: super; line-height: 0; text-shadow: none; }
/* v2.49 the 推理节点 card's ≈: a figure the server marks really uncertain (the reason and the source are in
   the cell's title). Inline on the value line, before the figure, smaller (13px: a value's floor) and
   yellow -- the cell keeps its size (fit.js shrinks the line like any other value). */
.kv .item .value .ctr-mark { font-size: 13px; font-weight: 600; color: var(--yellow); margin-right: 2px; vertical-align: 0.06em; line-height: 0;
  text-shadow: 0 0 10px color-mix(in srgb, var(--yellow) 45%, transparent); cursor: help; }
/* parts_layout: table (gateway By Model): label left and number right in every slot, two
   cells wide. v2.28: one line, the value line (20px) -- the entries above zero, largest
   first, as many as fit (fit.js fitPtab); the rest are counted in a "+N" after them, with
   every entry in the title. */
.kv .item.span2 { grid-column: span 2; }
.kv:is([data-n="1"], [data-n="3"], [data-n="5"]) > .item.span2 { grid-column: 1 / -1; }
.kv .item > .ptab { grid-row: 2; display: grid; grid-template-columns: repeat(var(--pc, 1), minmax(0, 1fr)) auto;
  grid-auto-rows: var(--cell-v); column-gap: 14px; align-items: center; min-width: 0; overflow: hidden; }
.kv .item .ptab .pt { display: flex; align-items: baseline; justify-content: space-between; gap: 6px; min-width: 0; line-height: var(--cell-v); }
.kv .item .ptab .pt.pt-off { display: none; }
.kv .item .ptab .pt-more { font-size: 11px; line-height: var(--cell-v); color: var(--muted2); white-space: nowrap; text-shadow: none; }
.kv .item .ptab .pt-more:empty { display: none; }
.kv .item .ptab .pt-l { font-size: 10.5px; color: var(--muted2); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-shadow: none; }
.kv .item .ptab .pt-v { flex: none; font-size: 13px; font-weight: 500; white-space: nowrap; }
/* Cell grids outside machine cards (info, service, stack children): the column count
   follows the cell count so the last row is never half empty, and grows with the room --
   the nearest container is the card, or the stack child for a card inside a stack. */
.kv[data-n="1"], .kv[data-n="3"], .kv[data-n="5"] { grid-template-columns: minmax(0, 1fr); }
@container (min-width: 480px) { .kv[data-n="3"]:not(.node-kv) { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
/* (v2.27: a service card's four cells -- counts, a status, a latency -- go four across from
   400px, ~95px each, so a short reading is not alone in a 200px cell; an info card's values
   are sentences and wait for 520px. v2.30: from 380px -- a one-column card on an 1800px screen
   is 391px inside, and 2 x 2 there left "Skills 4" alone in a 167px cell) */
@container (min-width: 380px) { .card-body > .kv[data-n="4"]:not(.kv-info) { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
@container (min-width: 520px) { .card-body > .kv.kv-info[data-n="4"] { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
@container (min-width: 820px) { .kv[data-n="5"]:not(.node-kv) { grid-template-columns: repeat(5, minmax(0, 1fr)); } }
/* v2.34 machine card: three layouts, picked by the card's own width (its content box: card - 34px).
   The cells are always --cell-min .. --cell-max wide and the extra width goes to the gaps between
   them; the rings are a fixed size. Every machine card has six cell slots (a two-wide By Model
   counts as two), so the cards on one board are the same height in every layout.
     narrow   (< 340px inside)   rings 64px; cells 2 x 3, flowing in rows
     standard (340 - 577px)      rings 80px above; cells 3 x 2 (as before)
     wide     (>= 578px)         rings 64px on the left, cells 3 x 2 beside them -- one band of
                                 two cell rows, so the card is about half as tall
   (a four-cell card -- none on the board since m27 has six -- is 2 x 2 in all three.) */
.card .mach > .kv { --kc: 2; grid-template-columns: repeat(var(--kc), minmax(var(--cell-min), var(--cell-max)));
  justify-content: space-between; }
.mach > .rings { gap: var(--s2); }
.mach > .rings > .ring-wrap { min-width: 64px; }
.mach > .rings .ring { width: 64px; height: 64px; }
.mach > .rings .ring::before { inset: 6px; }
@container card (min-width: 340px) {
  .card .mach > .kv { --kc: 3; }
  .card .mach > .kv[data-n="4"] { --kc: 2; }
  .mach > .rings { gap: var(--s4); }
  .mach > .rings > .ring-wrap { min-width: 72px; }
  .mach > .rings .ring { width: 80px; height: 80px; }
  .mach > .rings .ring::before { inset: 7px; }
}
@container card (min-width: 578px) {
  .mach { display: grid; grid-template-columns: max-content minmax(0, 1fr); column-gap: var(--s5); align-items: center; }
  .mach > .rings { gap: var(--s3); flex-wrap: nowrap; }
  .mach > .rings > .ring-wrap { flex: 0 0 64px; min-width: 64px; }
  .mach > .rings .ring { width: 64px; height: 64px; }
  .mach > .rings .ring::before { inset: 6px; }
  .card .mach > .kv { margin-top: 0; }
}
/* v2.18 多卡:环下/格子下逐卡小字,每张卡按自己的状态着色;单卡机器不输出这行 */
.gpu-missing, .push-alert {
  margin-bottom: var(--s3); padding: 5px 10px; border-radius: 8px; font-size: 12px; font-weight: 600;
  color: var(--red); background: color-mix(in srgb, var(--red) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--red) 45%, transparent);
}
/* v2.31: names an address and a hostname -- wraps rather than run out of a narrow card */
.push-alert { overflow-wrap: anywhere; }
.gpu-panel-title { margin-top: 16px; font-size: 12px; color: var(--muted); letter-spacing: 0.5px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
#machineModalAgent.lv-ok, #machineModalAgent.lv-danger { text-shadow: none; }
/* v2.27 per-disk temperatures in the machine modal: one fixed-layout row, a column per bay */
#machineModalParts .tbl.partsTbl { margin-top: var(--s3); }
#machineModalParts .tbl.partsTbl th:first-child { width: 30%; }
#machineModalParts .tbl.partsTbl th.num { width: auto; }
/* ===== 键值区小块描边跟数据状态变色(v1:240-243 逐字，只设 border-color)===== */
.kv .item.lv-danger { border-color: color-mix(in srgb, var(--red) 45%, transparent); }
.kv .item.lv-warn   { border-color: color-mix(in srgb, var(--yellow) 42%, transparent); }
.kv .item.lv-ok     { border-color: color-mix(in srgb, var(--green) 32%, transparent); }
.kv .item.lv-cool   { border-color: color-mix(in srgb, var(--cyan) 38%, transparent); }

/* ---------- 大数字 ---------- */
.big { font-size: 38px; font-weight: 500; letter-spacing: 0.5px; line-height: 1.05; }
.big.sub { font-size: 13px; color: var(--muted); font-weight: 500; margin-top: var(--s1); letter-spacing: 0.5px; }

/* ---------- Token 表 ---------- */
/* v2.27: the front is a fixed table (one row per class, Total last; columns all-time / today /
   net / requests / via LiteLLM), so no figure can grow or widen anything. The card body is a
   column and .tk-main takes all of its height, the chart taking whatever the table leaves --
   a token card in a taller row grows its bars instead of ending in a blank strip.
     narrow (< 860): table, chart below
     wide  (>= 860): table on the left, chart beside it filling the rest */
.card[data-kind="token"] > .card-body { display: flex; flex-direction: column; }
.tk-main { flex: 1 1 auto; display: flex; flex-direction: column; min-height: 0; }
.tk-tblwrap { flex: none; min-width: 0; }
.card .tbl.tk-tbl { margin: 0; }
.card .tbl.tk-tbl col.c-num, .card .tbl.tk-tbl th.num { width: auto; }
/* (v2.34 the class-name column as above: up to --id-max, the five figure columns share the rest,
   each 54px (46px on a phone card, 68px beside the chart) to --cell-max; --tk-w is the table's width) */
.card .tbl.tk-tbl { --tk-w: 100cqw; --tk-num: 54px; }
.card .tbl.tk-tbl col.c-name { width: max(min(var(--id-max), var(--tk-w) - var(--nn, 5) * var(--tk-num)), var(--tk-w) - var(--nn, 5) * var(--cell-max)); }
.card .tbl.tk-tbl th { letter-spacing: 0; }
.card .tbl.tk-tbl td.tk-name { color: var(--text); font-weight: 600; }
.card .tbl.tk-tbl td.tk-ll { color: var(--muted2); }
.card .tbl.tk-tbl .sw { display: inline-block; width: 8px; height: 8px; border-radius: 2px; margin-right: 7px; vertical-align: 1px; }
.card .tbl.tk-tbl tr.tot td { border-top: 1px solid var(--hair2); }
/* a phone-width card: narrower figure columns (a figure is at most five characters), so the
   class names keep ~95px */
@container card (max-width: 479px) {
  .card .tbl.tk-tbl { --tk-num: 46px; }
  .card .tbl.tk-tbl th, .card .tbl.tk-tbl td { padding-left: 3px; padding-right: 3px; }
  .card .tbl.tk-tbl td.tk-name { font-size: 12px; padding-left: 0; }
  .card .tbl.tk-tbl .sw { margin-right: 5px; }
}
@container card (min-width: 860px) {
  .tk-main { flex-direction: row; align-items: stretch; gap: var(--s5); }
  .tk-main > .tk-tblwrap { width: min(54%, 600px); align-self: center; }
  .card .tbl.tk-tbl { --tk-num: 68px; --tk-w: min(54cqw, 600px); }
  .tk-main > .spark { margin-top: 0; }
  .tk-main > .spark .col .b { max-width: 30px; }
}

/* ---------- 趋势柱（固定框，不裁切，数字在框内）---------- */
/* v2.23: at least 92px tall, taller when the card has room (see .tk-main). Bar height is
   the fraction --f of the column's bar area, so it scales with the chart. */
.spark {
  flex: 1 1 auto; min-height: 92px; overflow: visible;
  display: flex; align-items: stretch; gap: 12px;
  padding: 0 4px; margin-top: var(--s4);
  position: relative;
}
/* B6追加: 柱状图 柱↔日期标签 之间一条横贯灰色渐变分隔(复用 --hair) */
.spark::after {
  content: ""; position: absolute; left: 4px; right: 4px; bottom: 13px; height: 1px;
  background: linear-gradient(90deg, transparent, var(--hair), transparent);
  pointer-events: none;
}
.spark .col { flex: 1; min-width: 0; display: flex; flex-direction: column; align-items: center; gap: 4px; }
.spark .col .bw { flex: 1 1 auto; min-height: 0; width: 100%; display: flex; flex-direction: column; align-items: center; justify-content: flex-end; gap: 4px; }
.spark .col .n { font-size: 10px; color: var(--muted); line-height: 1; white-space: nowrap; }
.spark .col .b { width: 56%; max-width: 18px; height: calc((100% - 14px) * var(--f, 0) + 4px); border-radius: 5px 5px 0 0; background: linear-gradient(180deg, var(--cyan), rgba(79,227,208,0.18)); box-shadow: 0 0 10px -2px rgba(79,227,208,0.5); min-height: 3px; }
.spark .col .d { font-size: 10px; color: var(--muted2); line-height: 1; white-space: nowrap; }

/* ---------- v2.48 VPS 卡(type: vps,web/renderers/vps.js)---------- */
/* One column: the transfer line and its bar, four standard cells (2 x 2 below 456px inside, 4 across
   from there -- 4 x 108 + 3 x 8), and the 30-day chart, which takes whatever height the row leaves
   (min 28px), so a card beside the taller Per-Project card grows its bars instead of a blank strip.
   Its natural height (header + 207px) stays under that card's at every width. Nothing wraps. */
.card[data-kind="vps"] > .card-body { display: flex; flex-direction: column; }
.vps { flex: 1 1 auto; display: flex; flex-direction: column; min-height: 0; min-width: 0; }
.vps-use { flex: none; display: flex; align-items: baseline; gap: var(--s2); min-width: 0; }
.vps-use > .vps-lbl { flex: none; font-size: var(--cell-label); line-height: var(--cell-l); color: var(--muted2); letter-spacing: 0.6px;
  text-transform: uppercase; opacity: 0.7; white-space: nowrap; }
.vps-use > .vps-amt { flex: 1 1 auto; min-width: 0; font-size: var(--cell-value); line-height: var(--cell-v); font-weight: 500;
  white-space: nowrap; overflow: hidden; font-variant-numeric: tabular-nums; }
.vps-use > .vps-pct { flex: none; font-size: var(--cell-value); line-height: var(--cell-v); font-weight: 600; white-space: nowrap;
  font-variant-numeric: tabular-nums; }
.vps-bar { flex: none; height: 6px; margin-top: 6px; border-radius: 3px; background: var(--track); overflow: hidden; text-shadow: none; }
.vps-bar > i { display: block; height: 100%; min-width: 2px; border-radius: 3px; background: linear-gradient(90deg, rgba(79,227,208,0.45), var(--cyan)); }
.vps-bar.lv-ok > i { background: linear-gradient(90deg, color-mix(in srgb, var(--green) 45%, transparent), var(--green)); }
.vps-bar.lv-warn > i { background: linear-gradient(90deg, color-mix(in srgb, var(--yellow) 45%, transparent), var(--yellow)); }
.vps-bar.lv-danger > i { background: linear-gradient(90deg, color-mix(in srgb, var(--red) 45%, transparent), var(--red)); }
.card .vps > .kv { --kc: 2; margin-top: var(--s2); grid-template-columns: repeat(var(--kc), minmax(var(--cell-min), var(--cell-max)));
  justify-content: space-between; }
@container card (min-width: 456px) { .card .vps > .kv { --kc: 4; } }
.vps-spark { flex: 1 1 auto; min-height: 28px; margin-top: var(--s2); display: flex; align-items: stretch; gap: 2px;
  border-bottom: 1px solid var(--hair); }
.vps-spark > .c { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; justify-content: flex-end; align-items: center; }
.vps-spark > .c > i { display: block; width: 100%; max-width: 16px; height: calc(100% * var(--f, 0)); min-height: 2px;
  border-radius: 2px 2px 0 0; background: linear-gradient(180deg, var(--cyan), rgba(79,227,208,0.22)); box-shadow: 0 0 8px -3px rgba(79,227,208,0.6); }
.vps-spark > .c.today > i { opacity: 0.65; }   /* today is still being counted */
.vps-spark > .c.none > i { height: 2px; background: var(--hair2); box-shadow: none; }   /* no samples that day */
.vps-axis { flex: none; display: flex; align-items: baseline; gap: var(--s2); margin-top: 3px; font-size: 10px; line-height: 12px; color: var(--muted2); }
.vps-axis > .fit { flex: none; white-space: nowrap; overflow: hidden; }
.vps-axis > .fit.mid { flex: 1 1 auto; min-width: 0; text-align: center; }

/* ---------- v2.48.1 不认识的卡片(type: unsupported,web/renderers/unsupported.js)---------- */
/* A placeholder: grey, dashed, the title and one line; nothing in it grows (fit.js shrinks the line, then cuts its
   trailing " · " parts -- the full text stays in the title). */
.card[data-kind="unsupported"] { border: 1px dashed rgba(148, 163, 184, 0.38); box-shadow: none; opacity: 0.78; }
.card[data-kind="unsupported"]::before { display: none; }
.card[data-kind="unsupported"] h2 { color: var(--muted); margin-bottom: var(--s2); }
.card[data-kind="unsupported"]:hover { transform: none; }
.unsup-note { font-size: 11px; line-height: 16px; color: var(--muted2); white-space: nowrap; overflow: hidden; min-width: 0; }

/* ---------- 状态色文本 ---------- */
.lv-ok { color: var(--green); text-shadow: 0 0 14px color-mix(in srgb, var(--green) 50%, transparent); }
.lv-warn { color: var(--yellow); text-shadow: 0 0 14px color-mix(in srgb, var(--yellow) 50%, transparent); }
.lv-danger { color: var(--red); text-shadow: 0 0 15px color-mix(in srgb, var(--red) 55%, transparent); }
.lv-cool { color: var(--cyan); text-shadow: 0 0 14px color-mix(in srgb, var(--cyan) 45%, transparent); }
.note { margin-top: var(--s3); font-size: 11px; color: var(--muted2); line-height: 1.5; }

/* ---------- 表格卡(type:table) ---------- */
/* v2.27: every table on the board is table-layout: fixed. The header row sets the widths: a
   figure column (.num) holds the longest figure it can (K / M / G keep that to five or six
   characters), a later text column (.txt, a date) a date, and the identity column takes the
   rest -- a long project / key / model name is cut with an ellipsis (full text in title)
   and can never widen the table. Figures stay tabular and right-aligned. */
.card .tbl-wrap { margin-top: var(--s3); }
.card .tbl { width: 100%; border-collapse: collapse; font-size: 13px; table-layout: fixed; }
.card .tbl th, .card .tbl td { text-align: left; padding: 6px 5px; border-bottom: 1px solid var(--hair);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.card .tbl th { color: var(--muted2); font-weight: 500; font-size: 10px; text-transform: uppercase; letter-spacing: 0.3px; }
.card .tbl th.num, .card .tbl td.num { text-align: right; font-variant-numeric: tabular-nums; }
.card .tbl th.num { width: 62px; }
.card .tbl th.txt { width: 86px; }
/* v2.34 a figure column is a cell: at least 62px, at most --cell-max. The identity column gets up
   to two cells (--id-max 308px) and takes more only once every figure column is at its cap, so a
   wide card spreads the figures evenly instead of opening one blank gap after a short name.
   The table is the card's full content width (100cqw; a calc() with % on a table cell is not
   reliable); --nn / --nt (table.js) are the visible figure / text columns. */
.card .tbl { --id-max: calc(2 * var(--cell-max) + var(--s2)); }
.card .tbl:not(.tk-tbl) th:first-child {
  width: max(min(var(--id-max), 100cqw - var(--nn, 0) * 62px - var(--nt, 0) * 86px), 100cqw - var(--nn, 0) * var(--cell-max) - var(--nt, 0) * 86px); }
.card .tbl:not(.tk-tbl) th.num { width: auto; }
.card .tbl th:first-child, .card .tbl td:first-child { padding-left: 0; }
.card .tbl th:last-child, .card .tbl td:last-child { padding-right: 0; }
.card .tbl tbody tr:last-child td { border-bottom: none; }
.card .tbl td:first-child { color: var(--muted); }
/* v2.23: a table card the grid widened also shows its modal-only columns (.col-x). v2.27: the
   identity column then gets a fixed share and the figure columns split the rest evenly, so
   the figures spread across the card rather than huddling at its right edge. */
.card .tbl .col-x { display: none; }
@container card (min-width: 760px) {
  .card .tbl .col-x { display: table-cell; }
  .card .tbl.has-x th:first-child {
    width: max(min(var(--id-max), 100cqw - var(--nnx, 0) * 62px - var(--ntx, 0) * 118px), 100cqw - var(--nnx, 0) * var(--cell-max) - var(--ntx, 0) * 118px); }
  .card .tbl.has-x th.num { width: auto; }
  /* v2.30: a date column keeps room for a whole date (36px gap after a figure + "2026-10-26")
     -- split evenly, a two-column table card (Per-Project on four columns) cut it to "2026-0…" */
  .card .tbl.has-x th.txt { width: 118px; }
  .card .tbl.has-x th, .card .tbl.has-x td { padding-left: 14px; }
  /* a left-aligned text column right after a right-aligned figure: keep them apart */
  .card .tbl.has-x th.num + th:not(.num), .card .tbl.has-x td.num + td:not(.num) { padding-left: 36px; }
}

/* 客户端面板:一次性 key 展示框。红框是刻意的 —— 这段文本关掉就再也拿不回来,
   视觉上必须比周围任何东西都更"现在就处理我"。.cliKey 用等宽并允许折行,
   因为 key 比这个框宽,截断或横向滚动都会让人复制到半截。 */
.cliNew { margin: var(--s3) 0; padding: var(--s3); border-radius: 10px;
  border: 1px solid color-mix(in srgb, var(--lv-danger) 55%, transparent);
  background: color-mix(in srgb, var(--lv-danger) 8%, transparent);
  display: flex; flex-direction: column; gap: 4px; }
/* v2.23 client list: a table like every other one -- name left, figures right-aligned */
.modal .cliTbl { margin-top: var(--s2); font-size: 12.5px; }
.modal .cliTbl td, .modal .cliTbl th { padding: 7px 8px; white-space: nowrap; }
.modal .cliTbl th.num { width: 96px; }
.modal .cliTbl td:first-child .addDot { display: inline-block; margin-right: var(--s2); vertical-align: middle; }
.modal .cliTbl td b { color: var(--text); font-weight: 600; }
.modal .cliTbl .cliAct { width: 1%; text-align: right; }
.cliKeyRow { display: flex; gap: var(--s2); align-items: center; margin-top: var(--s2); }
.cliKey { flex: 1; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px;
  word-break: break-all; user-select: all; color: var(--text);
  background: rgba(0,0,0,0.35); padding: 6px 8px; border-radius: 8px; }

/* ---------- 历史双屏 ---------- */
.history { margin: 0 max(var(--gutter-min), calc((100% - var(--page-max)) / 2)) var(--s5); border-radius: var(--radius); padding: var(--s4); position: relative;
  container-type: inline-size;
  background: linear-gradient(180deg, rgba(255,255,255,0.05), rgba(255,255,255,0.012) 30%), var(--card-bg);
  box-shadow: var(--shadow); border: 1px solid transparent; }
.history::before { content: ""; position: absolute; inset: 0; border-radius: var(--radius); padding: 1px;
  background: linear-gradient(160deg, var(--hair2), transparent 50%); -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); -webkit-mask-composite: xor; mask-composite: exclude; pointer-events: none; }
.hist-head { display: flex; align-items: center; justify-content: space-between; gap: var(--s3); flex-wrap: wrap; margin-bottom: var(--s3); }
.hist-head h2 { font-size: 12.5px; font-weight: 600; color: var(--muted); margin: 0; letter-spacing: 1px; }
.ranges { display: inline-flex; gap: var(--s1); background: var(--track); border-radius: 10px; padding: 3px; }
.ranges button { background: transparent; color: var(--muted); border: 0; border-radius: 7px; padding: 6px 14px; font-size: 12px; cursor: pointer; font-weight: 500; transition: background .25s, color .25s; }
.ranges button.active { background: var(--glass2); color: var(--text); box-shadow: 0 0 0 1px var(--hair) inset; }
/* v2.46 the inference node detail's summary line (under the range buttons): the window, then
   label + figure pairs. Each pair is one unbreakable unit; on a narrow screen whole pairs move to
   the next line, nothing inside one wraps or is cut. The full figures are in the titles. */
.node-sum { display: flex; flex-wrap: wrap; align-items: baseline; column-gap: var(--s4); row-gap: var(--s1);
  margin-top: var(--s3); padding: var(--s2) var(--s3); border: 1px solid var(--hair); border-radius: 10px;
  background: var(--track); font-size: 12px; color: var(--muted); }
.node-sum[hidden] { display: none; }
.node-sum .ns-range { font-weight: 600; color: var(--text); white-space: nowrap; }
.node-sum .ns-item { display: inline-flex; align-items: baseline; gap: 6px; white-space: nowrap; cursor: help; }
.node-sum .ns-k { color: var(--muted2); }
.node-sum .ns-v { color: var(--text); font-size: 14px; font-weight: 600; font-variant-numeric: tabular-nums; }
.node-sum .ns-total { margin-left: auto; }
.node-sum .ns-none { color: var(--muted2); white-space: nowrap; }
/* B10: N panes — auto-fit so 2/3/N screens lay out and wrap responsively */
.hist-split { display: grid; grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); gap: var(--s4); }
.hist-pane { display: flex; flex-direction: column; gap: var(--s2); }
.hostSel { align-self: flex-start; background: var(--glass2); color: var(--text); border: 1px solid var(--hair); border-radius: 9px; padding: 7px 13px; font-size: 13px; font-weight: 600; outline: none; color-scheme: dark; }
.hostSel option { background: #0c1322; color: var(--text); }
.histCanvas { width: 100%; display: block; }
/* B17: load failure overlays the canvas instead of silently blanking the pane */
.histWrap { position: relative; }
.histErr { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--s2); background: rgba(2,4,9,0.66); border-radius: 9px; font-size: 12px; color: var(--muted); text-align: center; padding: var(--s2); }
.histErr button { background: var(--glass2); color: var(--text); border: 1px solid var(--hair); border-radius: 7px; padding: 5px 14px; font-size: 12px; font-weight: 500; cursor: pointer; }
.histErr button:hover { border-color: var(--text); }
.legend { display: flex; flex-wrap: wrap; gap: var(--s4); font-size: 11.5px; color: var(--muted); }
.legend .lg { display: inline-flex; align-items: center; gap: 6px; }
.legend .sw { width: 12px; height: 3px; border-radius: 2px; }

/* ---------- token detail modal ---------- */
.modal-mask { position: fixed; inset: 0; background: rgba(2,4,9,0.72); display: none; align-items: center; justify-content: center; z-index: 50; padding: var(--s4); }
.modal-mask.open { display: flex; }
.modal {
  width: min(720px, 96vw); max-height: 90vh; overflow: auto;
  border-radius: var(--radius); padding: var(--s5);
  background: linear-gradient(180deg, rgba(255,255,255,0.05), rgba(255,255,255,0.012) 30%), var(--card-bg);
  border: 1px solid var(--hair2); box-shadow: var(--shadow);
}
.modal-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--s4); }
.modal-head { gap: var(--s3); }
/* v2.27: a modal title is one line (cut with an ellipsis, full text in title); the close
   button never folds */
.modal-head h2 { font-size: 14px; font-weight: 600; margin: 0; letter-spacing: 1px; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.modal-close { background: var(--glass2); border: 1px solid var(--hair); color: var(--muted); border-radius: 9px; padding: 6px 12px; cursor: pointer; font-size: 13px; flex: none; white-space: nowrap; }
.modal .tbl { width: 100%; border-collapse: collapse; margin-top: var(--s4); font-size: 13px; table-layout: fixed; }
.modal .tbl th, .modal .tbl td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--hair);
  overflow: hidden; text-overflow: ellipsis; }
/* v2.27 fixed layout (see the card table above): figure columns by their longest figure, the
   name column the rest */
.modal .tbl th.num { width: 104px; }
.modal .tbl th.num.w-pct { width: 124px; }
.modal .tbl th.txt { width: 108px; }
.modal .tbl th.cliAct { width: 96px; }
.modal .tbl th { color: var(--muted2); font-weight: 500; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; }
.modal .tbl .sw { display: inline-block; width: 10px; height: 10px; border-radius: 3px; margin-right: 8px; vertical-align: middle; }
/* 弹窗表格的数字列右对齐 + 等宽数字,和卡面那张表读起来一致(.card .tbl 早有同款规则,
   .modal .tbl 之前没有,因为在此之前弹窗里只有 token 那张全是短数字的表)。 */
.modal .tbl th.num, .modal .tbl td.num { text-align: right; font-variant-numeric: tabular-nums; }
/* v2.23: a modal table never wraps a cell (a figure split over two lines breaks the
   column's right edge). On a phone-width modal the .opt columns step aside; a table that
   still does not fit (the 8-column Per-Project detail) scrolls sideways inside its own
   box, identity column pinned, instead of pushing past the modal. */
.modal .tbl th, .modal .tbl td { white-space: nowrap; }
.modal .tbl .pct { color: var(--muted); font-size: 0.88em; }
.modal .tbl tr.tot td { font-weight: 600; color: var(--text); }
#tableModalBody, #tokenModalTable { overflow-x: auto; }
/* v2.24: the Per-Project detail has 8 columns and a long "direct" row label */
#tableModal .modal { width: min(880px, 96vw); }
@media (max-width: 560px) {
  .modal { padding: var(--s4); }
  .modal .tbl th, .modal .tbl td { padding: 7px 6px; }
  .modal .tbl th { font-size: 10px; letter-spacing: 0; }
  .modal .tbl .opt { display: none; }
  .modal .tbl th.num, .modal .cliTbl th.num { width: 68px; }
  .modal .tbl th.num.w-pct { width: 100px; }
  .modal .tbl th.txt { width: 92px; }
  .modal .tbl th.cliAct { width: 80px; }
  /* the eight-column Per-Project detail keeps its widths and scrolls inside its box, the
     project column pinned (and given a width, since the figures alone fill the phone) */
  #tableModalBody .tbl { width: max(100%, 640px); }
  #tableModalBody .tbl th:first-child { width: 132px; }
  #tableModalBody .tbl th:first-child, #tableModalBody .tbl td:first-child { position: sticky; left: 0; z-index: 1; background: #10161f; }
}
/* "查看全部(共 N)" —— 这条是行动号召,和 hint 那种常驻说明区分开,所以给它正文色。 */
.card .tblMore { color: var(--text); font-weight: 500; }

/* ---------- 原始 JSON ---------- */
.raw { margin: 0 max(var(--gutter-min), calc((100% - var(--page-max)) / 2)) var(--s6); }
.raw summary { cursor: pointer; color: var(--muted); font-size: 12px; padding: var(--s2) 0; }
.raw pre { background: rgba(0,0,0,0.4); border: 1px solid var(--hair); border-radius: 12px; padding: var(--s3); font-size: 11.5px; line-height: 1.5; overflow: auto; max-height: 480px; color: var(--muted); white-space: pre-wrap; word-break: break-word; }

@keyframes breathe { 0%,100% { opacity: 1; } 50% { opacity: 0.45; } }

/* v2.33: by the section's own width, not the window's -- the section is the board's width now
   (420 / 856 / 1728px), and three panes need ~1084px; below that they stack (never 2 + 1) */
@container (max-width: 1083px) { .hist-split { grid-template-columns: 1fr; } }
@media (max-width: 560px) {
  .grid { padding-top: var(--s3); padding-bottom: var(--s4); }
  .big { font-size: 32px; }
}
@media (prefers-reduced-motion: reduce) { * { animation: none !important; } .card { transition: none; } }

/* ---------- 添加目标面板（切片3） ---------- */
/* 复用 .modal-mask/.modal 外壳,只加面板内部的元件。全部走既有 token,没有新颜色。 */
.addOpen { background: var(--glass2); color: var(--text); border: 1px solid var(--hair); border-radius: 9px;
  padding: 5px 12px; font-size: 12px; font-weight: 600; cursor: pointer; letter-spacing: .5px;
  transition: border-color .2s, background .2s; }
.addOpen:hover { border-color: var(--text); background: var(--glass); }
.addModal { width: min(760px, 96vw); }

.addBar { display: flex; gap: var(--s2); margin-bottom: var(--s3); }
.addInput { flex: 1; background: var(--track, rgba(255,255,255,0.05)); color: var(--text); border: 1px solid var(--hair);
  border-radius: 9px; padding: 9px 13px; font-size: 13px; font-family: var(--font); outline: none; }
.addInput:focus { border-color: var(--hair2); }
.addInput::placeholder { color: var(--muted2); }
.addBtn { background: var(--glass2); color: var(--text); border: 1px solid var(--hair); border-radius: 9px;
  padding: 8px 18px; font-size: 13px; font-weight: 600; cursor: pointer; transition: border-color .2s, opacity .2s; }
.addBtn:hover:not(:disabled) { border-color: var(--text); }
.addBtn:disabled { opacity: .45; cursor: default; }

.addStatus { font-size: 12px; color: var(--muted); min-height: 18px; line-height: 1.6; }
.addStatus.ok { color: var(--green); }
.addStatus.warn { color: var(--yellow); }
.addStatus.bad { color: var(--red); }
.addStatus.busy { color: var(--muted); }

.addSum { display: flex; align-items: center; flex-wrap: wrap; gap: var(--s2); font-size: 13px;
  margin: var(--s3) 0 var(--s2); }
.addDot { width: 8px; height: 8px; border-radius: 50%; background: var(--muted2); }
.addDot.ok { background: var(--green); box-shadow: 0 0 8px var(--green); }
.addDot.bad { background: var(--red); box-shadow: 0 0 8px var(--red); }
.addMuted { color: var(--muted2); font-size: 11.5px; }

.addRow { display: flex; gap: var(--s3); align-items: flex-start; margin: var(--s2) 0; font-size: 12px; }
.addRow > .addMuted { min-width: 56px; padding-top: 3px; }
.addChips { display: flex; flex-wrap: wrap; gap: 6px; }
.addChip { display: inline-flex; align-items: baseline; gap: 5px; background: var(--glass); border: 1px solid var(--hair);
  border-radius: 7px; padding: 2px 8px; font-size: 11.5px; color: var(--text); }
.addChip i { font-style: normal; color: var(--muted2); font-size: 10.5px; }
.addSvcs { display: flex; flex-direction: column; gap: 3px; }
.addSvc { display: flex; gap: var(--s2); flex-wrap: wrap; font-size: 11.5px; color: var(--muted); }
.addSvc b { color: var(--text); }

.addCaps { margin-top: var(--s3); border-top: 1px solid var(--hair); padding-top: var(--s3); }
.addGroup { margin-bottom: var(--s3); }
.addGroup h4 { margin: 0 0 6px; font-size: 11px; font-weight: 500; color: var(--muted2);
  text-transform: uppercase; letter-spacing: .8px; }
.addCap { display: flex; align-items: center; gap: var(--s2); padding: 5px 8px; border-radius: 8px;
  font-size: 12.5px; color: var(--text); cursor: pointer; }
.addCap:hover:not(.off) { background: var(--glass); }
.addCap.off { color: var(--muted2); cursor: default; }
.addCap.done { opacity: .55; }
.addCapLabel { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.addCap .addName { display: none; }
.addCap.picked .addName { display: inline-block; }
/* 凭据下拉不是可选项,是必填项 —— 与"自定义名称"不同,它常显,否则勾选前看不出
   这条能力还需要一个凭据。 */
.addCap .addCred { display: inline-block; width: 168px; padding: 2px 6px; }
.addName { width: 150px; background: transparent; color: var(--text); border: 1px solid transparent;
  border-bottom: 1px solid var(--hair); border-radius: 0; padding: 2px 4px; font-size: 11.5px;
  font-family: var(--font); outline: none; }
.addName:focus { border-bottom-color: var(--hair2); }
.addName::placeholder { color: var(--muted2); }
.addPending { font-size: 11px; color: var(--muted2); }
.addResult { font-size: 11px; min-width: 62px; text-align: right; }
.addResult.ok { color: var(--green); }
.addResult.warn { color: var(--yellow); }
.addResult.bad { color: var(--red); text-align: left; }

/* 主操作贴底:面板内容常常长过一屏,按钮沉到折叠线以下等于没有按钮。 */
.addFoot { display: flex; justify-content: flex-end; margin-top: var(--s2);
  position: sticky; bottom: -1px; padding: var(--s2) 0;
  background: linear-gradient(180deg, transparent, var(--bg1) 40%); }
.addSection { margin: var(--s5) 0 var(--s2); font-size: 11px; font-weight: 500; color: var(--muted2);
  text-transform: uppercase; letter-spacing: .8px; border-top: 1px solid var(--hair); padding-top: var(--s3); }
.addItem { display: flex; align-items: center; gap: var(--s2); padding: 6px 8px; border-radius: 8px; font-size: 12.5px; }
.addItem:hover { background: var(--glass); }
.addItem b { color: var(--text); font-weight: 600; }
.addItem .addChip { margin-left: auto; }
.addDel { background: transparent; color: var(--muted); border: 1px solid var(--hair); border-radius: 7px;
  padding: 3px 10px; font-size: 11px; cursor: pointer; }
.addDel:hover:not(:disabled) { border-color: var(--red); color: var(--red); }
.addDel:disabled { opacity: .5; cursor: default; }

@media (max-width: 915px) {   /* (v2.33; v2.34: kept -- logged in, the English admin header needs it up to ~900px) */
  .addOpen { padding: 4px 9px; font-size: 11px; }
  .addName { width: 96px; }
  .addCap { flex-wrap: wrap; }
}

/* ---------- 版本号页脚 ---------- */
/* 右下角一行,用最暗的一档文本色 —— 要能查到,不该被看到。空 #version 时
   :empty 让整个页脚不占高度,旧后端(无 version 字段)下版面与改前一致。 */
.foot { padding: 0 max(var(--gutter-min), calc((100% - var(--page-max)) / 2)) var(--s5); text-align: right; }
.foot #version { font-size: 10.5px; color: var(--muted2); letter-spacing: .5px;
  font-variant-numeric: tabular-nums; }
.foot:has(#version:empty) { padding: 0; }

/* ---------- 凭据面板(切片2e) ---------- */
.credGrid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s3); margin-bottom: var(--s3); }
.credGrid label { display: flex; flex-direction: column; gap: 5px; font-size: 11px; color: var(--muted2);
  text-transform: uppercase; letter-spacing: .6px; }
.credGrid .addInput { width: 100%; text-transform: none; letter-spacing: 0; }
/* 自己的下拉样式,不借用 .hostSel —— 那是 history.js 用来找 pane 选择器的行为类,
   它会 querySelectorAll('.hostSel') 并重写 innerHTML,把这里的选项换成目标列表。 */
.credSelect { appearance: none; cursor: pointer; color-scheme: dark; }
.credSelect option { background: #0c1322; color: var(--text); }

/* ---------- 修改管理密码(切片2h) ---------- */
/* 借 .credGrid 的标签/输入框样式,但列数自己定:三个框在两列网格里会摊成 2+1,
   最后一个"确认新密码"独占一行、宽度还和上面对不齐。三列是并排,窄屏收成一列。 */
#passwdModal .modal { width: min(560px, 96vw); }
#passwdModal .credGrid { grid-template-columns: 1fr; }
@media (min-width: 560px) { #passwdModal .credGrid { grid-template-columns: repeat(3, 1fr); } }
.credLocked { display: flex; flex-direction: column; gap: 6px; padding: var(--s3) var(--s4);
  border: 1px solid var(--hair); border-radius: 10px; background: var(--glass); margin-bottom: var(--s3); }
.credLocked b { color: var(--yellow); font-size: 13px; }
@media (max-width: 560px) { .credGrid { grid-template-columns: 1fr; } }

/* ---------- 语言开关(i18n) ---------- */
/* 两段式小开关,常驻 header —— 它是浏览器本地的偏好,不随登录状态显隐。
   尺寸对齐 .addOpen(同样的 12px/9px 圆角),所以它落在 .meta 那行里不会顶高
   header;选中的一段用 --text 描边 + 玻璃底,和 .addOpen:hover 是同一套。 */
.langSw { display: inline-flex; border: 1px solid var(--hair); border-radius: 9px;
  overflow: hidden; background: var(--glass2); }
.langBtn { background: transparent; color: var(--muted); border: 0; cursor: pointer;
  padding: 5px 10px; font-size: 12px; font-weight: 600; letter-spacing: .5px;
  font-family: var(--font); line-height: 1.2; transition: color .2s, background .2s; }
.langBtn + .langBtn { border-left: 1px solid var(--hair); }
.langBtn:hover { color: var(--text); }
.langBtn.active { color: var(--text); background: var(--glass); }
@media (max-width: 915px) { .langBtn { padding: 4px 8px; font-size: 11px; } }   /* v2.33 (v2.34: kept, see .addOpen) */
/* v2.23: header controls keep their words on one line; on a phone the row wraps instead
   of squeezing the language switch (it used to clip to "中 E") or a button to one
   character per line. */
.addOpen, .langBtn { white-space: nowrap; }
.langSw { flex: none; }
/* v2.33: also up to 915px (v2.34: kept -- logged in, the English admin controls fill an 800px header) */
@media (max-width: 915px) { .meta { flex-wrap: wrap; justify-content: flex-end; gap: var(--s2) var(--s3); } }
