Files
liuzhuohua b7ca9c89e8 feat(i18n/layout): 全站中英双语 + 四轮排版修复收官 + 修 boot_id 重启检测
v1.0.10 两件大事:

1. 界面中英双语(英文界面无中文残留)
   - 新增 web/i18n.js(约 4200 条词条),零依赖零构建
   - 后端下发中文数据走 bt4() 查表翻译;独立模块(upstream/netdetail/
     realtime/update)纳入翻译并注册切语言重绘钩子
   - 后端「状态/聚合」接口只回稳定枚举+原始数值,不下发中文文案
   - 顺带修一个真实 XSS:顶层 const 不挂 window,独立脚本 esc 静默退化成零转义

2. 修掉「文字竖着排/错位」(多分辨率反复暴露)
   - 单元格 min-content 只有 1 字宽 → word-break:keep-all + 表头 nowrap
   - 依赖卡片正文被标签/按钮挤成 27px → .dep-body{flex:1 1 200px} + flex-wrap
   - 裸 flex:1(下限为0)给正文 → 一律改 flex:1 1 <px>
   - .dep-item 横向容器直接塞块级子项 → 退回块级
   - 关键陷阱:auto-fill 布局屏越宽列数越多单卡越窄,窄屏反而测不出来,
     真机审计推进到「逐视图点按钮 + 1920/2560 宽视口 + 中英双语」
   - 守卫 t-layout.py 扩到 35 条,12 处反向验证全红

3. 修真缺陷:_link_since() 重启检测被误改成 if False:(注释还写着「已修」),
   uptime 归零后相减得负数 → 恢复比对 boot_id

附带:新增 25 个检查器(t-*.py)、排版审计工具链(shot-*/ocr-*/live-*)、
版本号兜底值同步到 1.0.10、.gitignore 收纳 i18n/排版排查的一次性脚本。
2026-10-10 04:50:32 +08:00

1197 lines
72 KiB
CSS
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* Drouter 管理台样式 —— 轻量、浅色主题、无外部依赖 */
:root{
--bg:#eef2f9; --panel:#ffffff; --line:#e2e7f0; --line2:#eef1f7;
--txt:#161f30; --txt2:#556480; --txt3:#8894a8;
--pri:#1f6feb; --pri-d:#1858c4; --pri-l:#e8f0fe;
--ok:#12813f; --ok-l:#e3f7ea; --warn:#9a6700; --warn-l:#fff8e1;
--err:#c0392b; --err-l:#fdecea; --info:#0b6e99; --info-l:#e5f4fb;
--r:12px; --sh:0 1px 2px rgba(18,28,55,.05),0 2px 8px rgba(18,28,55,.05);
--side:236px; --side-c:68px;
--grad:linear-gradient(135deg,#2b7bf3 0%,#1f6feb 45%,#1552c0 100%);
}
*{box-sizing:border-box}
html,body{margin:0;height:100%}
body{
font:14px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI","Microsoft YaHei",system-ui,sans-serif;
background:var(--bg); color:var(--txt); -webkit-font-smoothing:antialiased;
}
.hidden{display:none !important}
button{font:inherit;cursor:pointer;border:1px solid var(--line);background:#fff;color:var(--txt);
padding:8px 16px;border-radius:9px;transition:.15s}
button:hover{border-color:#c3cfe3;background:#fafbfe}
button:active{transform:translateY(1px)}
button:focus-visible{outline:none;box-shadow:0 0 0 3px var(--pri-l);border-color:var(--pri)}
button.primary{background:var(--grad);border-color:transparent;color:#fff;font-weight:600;
box-shadow:0 1px 3px rgba(31,111,235,.35)}
button.primary:hover{filter:brightness(1.06)}
button.ghost{background:#fff}
button.small{padding:5px 11px;font-size:13px}
.filepick{display:inline-block;background:#fff;border:1px solid var(--line);border-radius:9px;
padding:5px 11px;font-size:13px;color:var(--txt);cursor:pointer;margin-top:6px;margin-right:8px}
.filepick:hover{border-color:var(--pri);background:var(--pri-l)}
#key-file-name{color:var(--txt3);font-size:12px;word-break:break-all}
button.danger{color:var(--err);border-color:#f0c8c3}
button.danger:hover{background:var(--err-l)}
button:disabled{opacity:.5;cursor:not-allowed;transform:none}
.icon-btn{padding:6px;border-radius:8px;display:inline-flex;align-items:center;color:var(--txt2)}
input,select,textarea{font:inherit;color:var(--txt);background:#fff;border:1px solid var(--line);
border-radius:9px;padding:8px 11px;width:100%;transition:.15s}
input:focus,select:focus,textarea:focus{outline:none;border-color:var(--pri);box-shadow:0 0 0 3px var(--pri-l)}
textarea{font-family:ui-monospace,Consolas,"Cascadia Code",monospace;font-size:13px;line-height:1.6;resize:vertical}
label{display:block;font-size:13px;color:var(--txt2);margin-bottom:12px}
label>input,label>select,label>textarea{margin-top:5px}
a{color:var(--pri);text-decoration:none}
a:hover{text-decoration:underline}
/* ---------- 登录 ---------- */
.login-wrap{display:flex;align-items:center;justify-content:center;height:100%;position:relative;
overflow:hidden;background:linear-gradient(155deg,#e9f0fb 0%,#f6f9fd 48%,#e6eefb 100%)}
.login-glow{position:absolute;width:820px;height:820px;border-radius:50%;pointer-events:none;
background:radial-gradient(circle at 50% 50%,rgba(31,111,235,.13),rgba(31,111,235,0) 62%)}
.login-box{position:relative;width:392px;background:var(--panel);border:1px solid var(--line);
border-radius:18px;padding:38px 34px 28px;box-shadow:0 18px 50px rgba(20,45,95,.11);text-align:center}
.login-logo{display:flex;justify-content:center;margin-bottom:10px}
.lg-mark{color:var(--pri);filter:drop-shadow(0 4px 10px rgba(31,111,235,.25))}
.login-box h1{margin:0;font-size:27px;letter-spacing:.6px;color:var(--txt);font-weight:700}
.login-box .sub{margin:5px 0 24px;color:var(--txt3);font-size:13px}
.login-box label{text-align:left}
.login-box button{width:100%;margin-top:6px;padding:11px}
.login-box .hint{margin:18px 0 0;font-size:12px;color:var(--txt3);text-align:center}
/* ---------- 布局 ---------- */
#app{display:flex;height:100%}
.sidebar{width:var(--side);flex:0 0 var(--side);background:#fff;border-right:1px solid var(--line);
display:flex;flex-direction:column;transition:flex-basis .2s,width .2s}
#app.side-collapsed .sidebar{width:var(--side-c);flex-basis:var(--side-c)}
#app.side-collapsed .brand-txt,#app.side-collapsed .nav-search,
#app.side-collapsed .ng-t,#app.side-collapsed .ng-n,#app.side-collapsed .ng-arrow,
#app.side-collapsed .nitem-t,#app.side-collapsed .nitem-tag{display:none}
#app.side-collapsed .nitem,#app.side-collapsed .ng-head{justify-content:center}
.brand{display:flex;align-items:center;gap:11px;padding:18px 18px 14px}
/* brand 是 <a>(可点击回概述页),要显式去掉 <a> 的默认外观 ——
只写 .brand-home 的样式不够,浏览器给 <a> 的 color/text-decoration
是 UA 样式表里的规则,作者样式表不声明就仍然生效。
padding/flex 沿用上面的 .brand,这里不重复声明。 */
.brand-home{text-decoration:none;color:inherit;border-radius:8px;transition:background .15s}
/* 焦点环不能去掉:这是纯键盘用户的唯一路标 */
.brand-home:focus-visible{outline:2px solid var(--pri);outline-offset:-2px}
.brand-home:hover{background:var(--line2)}
/* 侧栏收起时只剩 logo,此时 hover 反馈面积很小 */
#app.side-collapsed .brand-home{padding-left:0;padding-right:0;justify-content:center}
.brand-mark{color:var(--pri);flex:0 0 auto}
.brand-txt{min-width:0}
.brand-txt b{display:block;font-size:17px;font-weight:700;color:var(--txt);letter-spacing:.3px;line-height:1.2}
.brand-txt span{display:block;font-size:11px;font-weight:400;color:var(--txt3);
letter-spacing:0;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.nav-search{padding:0 12px 10px}
.nav-search input{padding:7px 11px;font-size:12.5px;background:var(--line2);border-color:transparent}
.nav-search input:focus{background:#fff;border-color:var(--pri)}
#nav{flex:1;overflow-y:auto;padding:2px 10px 16px}
/* 分组(可折叠父菜单) */
.ngroup{margin-bottom:3px;border-radius:10px;transition:background .15s}
.ngroup.has-cur{background:hsl(var(--h) 85% 55% / .06)}
.ng-head{display:flex;align-items:center;gap:9px;width:100%;text-align:left;border:none;
background:none;border-radius:9px;padding:8px 10px;color:var(--txt2);
font-size:12.5px;font-weight:600;letter-spacing:.3px}
.ng-head:hover{background:var(--line2);color:var(--txt)}
.ng-ico{width:24px;height:24px;flex:0 0 24px;display:inline-flex;align-items:center;justify-content:center;
border-radius:7px;font-size:13px;line-height:1;
background:hsl(var(--h) 85% 55% / .15);color:hsl(var(--h) 72% 42%)}
.ng-t{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ng-n{font-size:10.5px;font-weight:500;color:hsl(var(--h) 60% 45%);
background:hsl(var(--h) 85% 55% / .13);padding:1px 7px;border-radius:9px}
.ng-arrow{flex:0 0 12px;color:var(--txt3);transition:transform .2s}
.ngroup.open .ng-arrow{transform:rotate(90deg)}
.ng-body{display:none;padding:1px 0 5px 34px}
.ngroup.open .ng-body{display:block}
.nitem{display:flex;align-items:center;gap:8px;width:100%;text-align:left;border:none;
background:none;border-radius:8px;padding:7px 10px;margin-bottom:1px;
color:var(--txt2);font-size:13px;position:relative}
.nitem:hover{background:var(--line2);color:var(--txt)}
.nitem-dot{width:5px;height:5px;flex:0 0 5px;border-radius:50%;background:#ccd5e3;transition:.15s}
.nitem-t{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.nitem-tag{font-size:9.5px;color:hsl(var(--h) 60% 45%);background:hsl(var(--h) 85% 55% / .13);
padding:1px 6px;border-radius:8px;flex:0 0 auto}
.nitem.on{color:hsl(var(--h) 68% 40%);font-weight:600;background:hsl(var(--h) 85% 55% / .12)}
.nitem.on .nitem-dot{background:hsl(var(--h) 75% 52%);box-shadow:0 0 0 3px hsl(var(--h) 85% 55% / .22)}
.nav-empty{font-size:12.5px;color:var(--txt3);padding:14px 10px;text-align:center}
.side-foot{padding:12px;border-top:1px solid var(--line)}
.dot-line{font-size:12px;color:var(--txt2);margin-bottom:0;display:flex;align-items:center;gap:6px}
.dot{width:7px;height:7px;border-radius:50%;display:inline-block;flex:0 0 7px}
.dot.on{background:var(--ok)} .dot.off{background:#c3ccdb}
/* 侧栏底部「退出系统」:与导航项同一视觉语言,悬停才显红,避免平时过于刺眼 */
.side-logout{margin-top:10px;width:100%;display:flex;align-items:center;gap:8px;
padding:8px 10px;border-radius:9px;border:1px solid transparent;background:transparent;
color:var(--txt2);font:inherit;font-size:13px;cursor:pointer;transition:.15s;white-space:nowrap}
.side-logout:hover{background:var(--err-l);color:var(--err);border-color:#f0c8c3}
.side-logout .sl-txt{overflow:hidden;text-overflow:ellipsis}
/* 顶栏刷新按钮:刷新中图标旋转 */
#btn-refresh.spinning #btn-refresh-ico{animation:spin .7s linear infinite}
#btn-refresh.spinning{pointer-events:none;opacity:.6}
@keyframes spin{to{transform:rotate(360deg)}}
main{flex:1;display:flex;flex-direction:column;min-width:0;position:relative}
.topbar{height:58px;flex:0 0 58px;background:rgba(255,255,255,.92);border-bottom:1px solid var(--line);
display:flex;align-items:center;justify-content:space-between;padding:0 20px;
backdrop-filter:blur(8px);position:relative;z-index:5}
.tb-left{display:flex;align-items:center;gap:10px;min-width:0}
.topbar h2{margin:0;font-size:16px;font-weight:650;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.tb-right{display:flex;align-items:center;gap:12px}
.mini{font-size:12px;color:var(--txt3);white-space:nowrap}
/* 顶栏时钟 / 农历(#12) */
.clockbox{display:flex;align-items:baseline;gap:10px;cursor:pointer;user-select:none;
padding:4px 10px;border-radius:9px;transition:.15s;white-space:nowrap}
.clockbox:hover{background:var(--bg2)}
.ck-time{font-size:17px;font-weight:650;font-variant-numeric:tabular-nums;
color:var(--txt);letter-spacing:.3px}
.ck-date{font-size:12.5px;color:var(--txt2)}
.ck-lunar{font-size:12.5px;color:var(--txt3)}
@media(max-width:900px){.ck-lunar{display:none}}
@media(max-width:680px){.ck-date{display:none}}
.view{flex:1;overflow-y:auto;padding:20px 22px 90px}
#actionbar{position:absolute;left:0;right:0;bottom:0;background:rgba(255,255,255,.97);
border-top:1px solid var(--line);padding:12px 22px;display:flex;align-items:center;
justify-content:space-between;gap:16px;backdrop-filter:blur(8px);z-index:4}
.ab-msg{font-size:13px;color:var(--txt2);min-height:20px}
.ab-msg.ok{color:var(--ok)} .ab-msg.err{color:var(--err)}
.ab-btns{display:flex;gap:10px;flex:0 0 auto}
/* ---------- 组件 ---------- */
.card{background:var(--panel);border:1px solid var(--line);border-radius:var(--r);
padding:18px 20px;margin-bottom:16px;box-shadow:var(--sh)}
.card h3{margin:0 0 4px;font-size:14.5px;font-weight:650}
.card .desc{margin:0 0 16px;font-size:12.5px;color:var(--txt3);line-height:1.65}
.grid{display:grid;gap:14px}
.g2{grid-template-columns:repeat(2,minmax(0,1fr))}
.g3{grid-template-columns:repeat(3,minmax(0,1fr))}
.g4{grid-template-columns:repeat(4,minmax(0,1fr))}
@media(max-width:1080px){.g4{grid-template-columns:repeat(2,minmax(0,1fr))}.g3{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media(max-width:760px){
.g2,.g3,.g4{grid-template-columns:minmax(0,1fr)}
.sidebar{width:var(--side-c);flex-basis:var(--side-c)}
.brand-txt,.nav-search,.ng-t,.ng-n,.ng-arrow,.nitem-t,.nitem-tag{display:none}
.ng-head,.nitem{justify-content:center}
}
.kv{display:flex;justify-content:space-between;gap:2px 12px;flex-wrap:wrap;
padding:7px 0;border-bottom:1px dashed var(--line2);font-size:13px}
.kv:last-child{border-bottom:none}
/* ⚠️ 标签**不能**收缩(这里踩过两次坑,两个方向都错过):
· `flex:0 0 auto` —— 英文长标签(Local machine LAN Address)撑破父级,
手机端整页横向滚动;
· `flex:0 1 auto; min-width:0` —— 反过来被压成一字一行的**竖排**,
实测 390px 下「Boot Mode」被压成 9px 宽 / 9 行(见 _dev/text-audit.sh)。
正解:标签不收缩 + 允许整行换行(上面的 flex-wrap:wrap)。
标签比整行还宽时由 max-width:100% + break-word 在**空格处**折行,
永远不会变成竖排。 */
.kv b{font-weight:500;color:var(--txt2);flex:0 0 auto;max-width:100%;
overflow-wrap:break-word}
.kv span{flex:1 1 auto;min-width:0;text-align:right;word-break:break-all;
font-variant-numeric:tabular-nums}
.stat{background:#fff;border:1px solid var(--line);border-radius:var(--r);padding:14px 16px;
box-shadow:var(--sh);position:relative;overflow:hidden}
.stat::before{content:'';position:absolute;left:0;top:0;bottom:0;width:3px;background:var(--grad);opacity:.75}
.stat .lbl{font-size:12px;color:var(--txt3);margin-bottom:6px}
.stat .val{font-size:20px;font-weight:650;font-variant-numeric:tabular-nums;letter-spacing:-.2px}
.stat .sub{font-size:11.5px;color:var(--txt3);margin-top:3px}
/* ⚠️ 徽标不能被压成一列一个字:它是 flex 项时会被挤到 25px 宽
(真机 800px 实测「虚拟磁盘」4 行 / 每行 1 字)。
用 keep-all 而不是 nowrap —— 中文标签不许按字断(保住「一个词」的下限),
英文长标签("DPI Classification database:Not Installed")仍要能在
空格处折行,否则 390px 下会把内容区顶出 13px。 */
.tag{display:inline-block;font-size:11px;padding:1px 7px;border-radius:5px;font-weight:500;vertical-align:middle;
white-space:normal;word-break:keep-all;overflow-wrap:break-word}
.tag.ok{background:var(--ok-l);color:var(--ok)}
.tag.warn{background:var(--warn-l);color:var(--warn)}
.tag.err{background:var(--err-l);color:var(--err)}
.tag.info{background:var(--info-l);color:var(--info)}
.tag.gray{background:#eef1f6;color:var(--txt2)}
table{width:100%;border-collapse:collapse;font-size:13px}
/* ⚠️ `word-break:keep-all` 不是装饰,是这一版排版修复的关键:
默认 CJK 可以在任意两个字之间断行 → 单元格的 min-content 只有**一个字**宽,
浏览器在自动表格布局里会把这一列一路压到 ~25px,于是「虚拟磁盘」「修改时间」
这类短标签被压成一列一个字(真机 800px 实测 w=25 / 4 行)。
keep-all 让 CJK 只在词边界断行,列宽就有了下限;表头再 nowrap 兜一层。
表因此变宽由 .tw 的横向滚动兜住,不会顶破内容区。
(判据:_dev/t-layout.py;实测工具:_dev/live-text-audit.sh) */
th,td{text-align:left;padding:8px 10px;border-bottom:1px solid var(--line2);
vertical-align:top;word-break:keep-all;overflow-wrap:break-word}
th{font-weight:600;color:var(--txt2);font-size:12px;background:#fafbfe;position:sticky;top:0;
white-space:nowrap}
tr:last-child td{border-bottom:none}
tbody tr:hover{background:#fafcff}
.mono{font-family:ui-monospace,Consolas,monospace;font-size:12.5px}
pre{background:#fbfcfe;border:1px solid var(--line);border-radius:9px;padding:12px;
overflow:auto;max-height:340px;font-size:12.5px;line-height:1.55;margin:0}
.banner{background:var(--warn-l);border-bottom:1px solid #f0e0a8;color:var(--warn);
padding:10px 22px;font-size:13px;display:flex;justify-content:space-between;align-items:center;gap:12px}
/* 新网卡提示的关闭按钮:点掉后按网卡集合记住,不再反复打扰 */
.banner .tip-body{flex:1 1 auto;min-width:0}
.banner .tip-x{flex:0 0 auto;width:24px;height:24px;line-height:22px;text-align:center;
border:1px solid currentColor;border-radius:6px;background:transparent;color:inherit;
cursor:pointer;font-size:13px;padding:0;opacity:.65;transition:.15s}
.banner .tip-x:hover{opacity:1;background:rgba(0,0,0,.06)}
.msg{font-size:12.5px;min-height:18px;margin-top:10px}
.msg.ok{color:var(--ok)} .msg.err{color:var(--err)} .msg.warn{color:var(--warn)}
.row{display:flex;gap:10px;align-items:flex-end;flex-wrap:wrap}
.row>*{flex:1 1 180px}
.row .fixed{flex:0 0 auto}
/* 表单行里的 label 同样不能被压成一列一个字:`flex:1` 的 label 在兄弟项
抢走空间后只剩 34px 宽(真机 800px 实测「其它域名(逗号分隔)」5 行)。
keep-all 给它一个「一个词」的下限,抢不过就整行换行(.row 本来就 wrap)。 */
.row>label{word-break:keep-all}
/* ⚠️ .switch 是「拨杆 + 名称 + 说明」的 inline-flex 一行。CJK 的 min-content
只有 **1 个字**,所以窄屏下名称会被后面的说明挤成细柱 —— 真机 390px 实测
「不主动获取(without-acquire)」被压成 66px / 3 行。
修法:允许整行换行 + 名称不可收缩(装不下时说明整段换到下一行)。 */
.switch{display:inline-flex;align-items:center;gap:8px;flex-wrap:wrap;cursor:pointer;font-size:13px;color:var(--txt2);
margin-bottom:12px;user-select:none}
.switch b{flex:0 0 auto}
.switch input{display:none}
.switch i{width:38px;height:21px;border-radius:11px;background:#ccd5e3;position:relative;transition:.18s;flex:0 0 38px}
.switch i::after{content:'';position:absolute;top:2px;left:2px;width:17px;height:17px;border-radius:50%;
background:#fff;transition:.18s;box-shadow:0 1px 2px rgba(0,0,0,.2)}
.switch input:checked+i{background:var(--pri)}
.switch input:checked+i::after{transform:translateX(17px)}
.hint-inline{font-size:12px;color:var(--txt3);margin:-6px 0 12px}
.toast{position:fixed;right:24px;bottom:24px;z-index:99;max-width:440px;
padding:13px 18px;border-radius:11px;background:#1d2a3c;color:#fff;font-size:13px;
box-shadow:0 10px 30px rgba(10,20,40,.32);animation:tin .22s ease}
.toast.ok{background:#12693a} .toast.err{background:#9d2f22} .toast.warn{background:#8a5b00}
@keyframes tin{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
.modal{position:fixed;inset:0;background:rgba(16,26,46,.5);z-index:100;display:flex;
align-items:center;justify-content:center;padding:24px;backdrop-filter:blur(2px)}
.modal-box{background:#fff;border-radius:15px;padding:24px 26px;max-width:620px;width:100%;
max-height:82vh;overflow:auto;box-shadow:0 24px 60px rgba(10,25,55,.34);animation:tin .18s ease}
.modal-box h3{margin:0 0 10px;font-size:16px}
.modal-box .acts{display:flex;justify-content:flex-end;gap:10px;margin-top:20px}
.countdown{font-size:26px;font-weight:700;color:var(--err);font-variant-numeric:tabular-nums}
.bar{height:7px;border-radius:4px;background:var(--line2);overflow:hidden;margin-top:4px}
.bar i{display:block;height:100%;background:var(--pri);transition:width .4s}
.ifrow{display:flex;align-items:center;gap:12px;padding:12px;border:1px solid var(--line);
border-radius:10px;margin-bottom:10px;background:#fff;flex-wrap:wrap}
.ifrow .nm{font-weight:600;font-size:13.5px;min-width:74px}
.ifrow .mac{color:var(--txt3);font-size:12px}
.ifrow input,.ifrow select{width:auto;flex:0 0 150px;padding:6px 10px;font-size:13px}
.seg{display:inline-flex;border:1px solid var(--line);border-radius:9px;overflow:hidden}
.seg button{border:none;border-radius:0;padding:7px 14px;font-size:13px;background:#fff}
.seg button+button{border-left:1px solid var(--line)}
.seg button.on{background:var(--grad);color:#fff}
/* ⚠️ 日志行必须允许换行,正文列不能用 flex:1 硬挤。
级别 46 + 时间 130 + 模块 76 + 间距 30 = 282px 是**固定**的;
原来 `.ms{flex:1}` 在窄屏下只能拿到剩下的 28px ——
实测 390px 视口下一条日志被折成 24 行、每行 3 个字,就是「竖着的字」。
改成 flex-basis 240px + flex-wrap:放不下时正文整行换到下一行(占满整宽),
任何宽度下都不会被挤成细柱。 */
.logline{padding:5px 8px;border-bottom:1px solid var(--line2);font-size:12.5px;
font-family:ui-monospace,Consolas,monospace;display:flex;gap:10px;flex-wrap:wrap}
.logline:hover{background:#fafbfe}
.logline .lv{flex:0 0 46px;font-weight:600}
.lv.info{color:var(--info)} .lv.warn{color:var(--warn)} .lv.error{color:var(--err)}
.logline .ts{flex:0 1 130px;min-width:0;color:var(--txt3)}
.logline .mo{flex:0 0 76px;color:var(--txt2)}
/* word-break 用 normal + overflow-wrap:break-word:长 token(MAC / IPv6)
放不下时仍会断开,但正常文本按词换行,比 break-all 好读得多。
注意别用 anywhere —— 它会把 min-content 变成 1 字符,又会退回竖排。 */
.logline .ms{flex:1 1 240px;word-break:normal;overflow-wrap:break-word}
/* ---------- 页脚(开源协议与作者) ---------- */
.site-footer{margin:26px 4px 8px;padding:14px 4px 6px;border-top:1px solid var(--line);
color:var(--txt3);font-size:12px;text-align:center;line-height:1.9}
.site-footer .sep{margin:0 8px;color:var(--line)}
.site-footer a{color:var(--pri);text-decoration:none}
.site-footer a:hover{text-decoration:underline}
/* ---------- PPPoE 实时状态栏与日志 ---------- */
.ppbar{display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:10px 14px;
border-radius:10px;background:var(--line2);border:1px solid var(--line);font-size:13.5px}
.pplog{margin-top:12px;height:230px;overflow:auto;border:1px solid var(--line);border-radius:10px;
background:#0f172a;padding:8px 10px;font-family:ui-monospace,Consolas,monospace;font-size:12px}
.ppline{padding:3px 0;color:#cdd9ea;border-bottom:1px solid rgba(255,255,255,.05);word-break:break-all}
.ppline .ppt{color:#7c8ba3;margin-right:10px}
.ppline.lv-warn{color:#fbbf24}
.ppline.lv-error{color:#f87171}
.ppline.lv-info{color:#cdd9ea}
/* ---------- 仪表与环形图 ---------- */
.stat .bar i{transition:width .5s}
svg[role=img]{display:block;margin:0 auto}
/* ---------- 快照覆盖范围 / 状态标签 ---------- */
.snap-scope{margin:2px 0 14px;padding:12px 14px;border:1px dashed var(--line);
border-radius:10px;background:var(--line2);font-size:12.5px}
.snap-scope-t{font-weight:600;color:var(--txt2);margin-bottom:8px}
.snap-g{margin-bottom:8px}
.snap-g:last-child{margin-bottom:0}
.snap-g b{display:block;color:var(--pri);font-size:12.5px;margin-bottom:3px}
.snap-g ul{margin:0;padding-left:18px;color:var(--txt2);line-height:1.75}
.snap-g li{list-style:disc}
/* 快照列表里的备注输入框。宽度给满一列但设上限:全宽会让「时间」列
被挤到换行,而 max-width 保证备注一长也不会把操作按钮顶出屏幕。 */
.snap-tag-input{width:100%;max-width:420px;padding:5px 9px;font-size:12.5px}
.snap-tag-input:focus{outline:2px solid var(--pri);outline-offset:0}
.tag-ok{display:inline-block;font-size:11px;padding:1px 7px;border-radius:5px;
font-weight:500;background:var(--ok-l);color:var(--ok)}
.tag-warn{display:inline-block;font-size:11px;padding:1px 7px;border-radius:5px;
font-weight:500;background:var(--warn-l);color:var(--warn)}
/* ---------- 网卡多选 chip ---------- */
.chipbox{display:flex;flex-wrap:wrap;gap:8px;margin:4px 0 10px}
.chip{display:inline-flex;align-items:center;gap:6px;padding:5px 11px;border:1px solid var(--line);
border-radius:20px;background:var(--panel);cursor:pointer;font-size:12.5px;
font-family:ui-monospace,Consolas,monospace;transition:.15s}
.chip:hover{border-color:var(--pri)}
.chip input{margin:0;accent-color:var(--pri)}
.chip:has(input:checked){background:var(--pri-l);border-color:var(--pri);color:var(--pri)}
/* ---------- 未开发模块占位页 ---------- */
.soon-card{text-align:center;padding:52px 26px;background:linear-gradient(180deg,#fbfdff 0%,#fff 60%)}
.soon-ico{color:var(--pri);opacity:.5;margin-bottom:12px;display:flex;justify-content:center}
.soon-card h3{font-size:19px;margin-bottom:8px}
.soon-pill{display:inline-block;margin-top:18px;padding:6px 20px;border-radius:20px;font-size:13px;
font-weight:600;color:var(--pri);background:var(--pri-l);border:1px dashed #a9c8f7}
/* ---------- 连接状态 / NAT 结果徽标 ---------- */
.nat-badge{display:flex;align-items:center;gap:16px;padding:16px 18px;border-radius:12px;
border:1px solid var(--line);background:linear-gradient(135deg,#fbfdff,#f4f8ff)}
.nat-badge .nb-score{flex:0 0 auto;width:64px;height:64px;border-radius:50%;display:flex;
align-items:center;justify-content:center;font-size:20px;font-weight:700;color:#fff;
box-shadow:0 4px 14px rgba(20,45,95,.16)}
.nat-badge .nb-body{min-width:0;flex:1}
.nat-badge .nb-t{font-size:16px;font-weight:650;margin-bottom:3px}
.nat-badge .nb-d{font-size:12.5px;color:var(--txt3);line-height:1.65}
.nat-badge.n1 .nb-score{background:linear-gradient(135deg,#12b76a,#0a8f50)}
.nat-badge.n2 .nb-score{background:linear-gradient(135deg,#3b82f6,#1d5fd0)}
.nat-badge.n3 .nb-score{background:linear-gradient(135deg,#f59e0b,#d97706)}
.nat-badge.n4 .nb-score{background:linear-gradient(135deg,#ef4444,#c0392b)}
.nat-badge.na .nb-score{background:linear-gradient(135deg,#94a3b8,#64748b)}
.probe-row{display:flex;justify-content:space-between;gap:12px;padding:6px 0;
border-bottom:1px dashed var(--line2);font-size:12.5px}
.probe-row:last-child{border-bottom:none}
.probe-row .pk{color:var(--txt3);flex:0 0 auto}
.probe-row .pv{font-family:ui-monospace,Consolas,monospace;text-align:right;word-break:break-all}
/* ---------- 依赖自检 ---------- */
.dep-summary{display:flex;gap:22px;flex-wrap:wrap;margin-bottom:4px}
.dep-summary .ds-item{display:flex;align-items:baseline;gap:7px}
.dep-summary .ds-num{font-size:22px;font-weight:700;font-variant-numeric:tabular-nums}
.dep-summary .ds-lbl{font-size:12px;color:var(--txt3)}
.dep-table td:first-child{width:32px;text-align:center;font-size:15px}
.dep-ok{color:var(--ok)} .dep-miss{color:var(--err)} .dep-opt{color:var(--warn)}
/* ---------- 代码高亮(通用 API 范例 / 错误日志) ---------- */
.code-wrap{position:relative}
.code-wrap .code-copy{position:absolute;top:8px;right:8px;padding:3px 10px;font-size:11.5px;
border-radius:7px;background:#fff;border:1px solid var(--line);z-index:2}
.code-wrap .code-copy:hover{background:var(--pri-l);border-color:var(--pri);color:var(--pri)}
.lang-tabs{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:10px}
.lang-tabs button{padding:5px 13px;font-size:12.5px;border-radius:18px}
.lang-tabs button.on{background:var(--grad);color:#fff;border-color:transparent;font-weight:600}
/* ---------- Web 终端 ---------- */
/* 历史上这里有过一套 .term-wrap/.term-bar/.term-body/.term-in/.tk-* 的旧定义,
已被下方「真 PTY 终端」那套(.term-dot/.term-title/.term-body.term-screen)
完全取代:旧的一组属性全部被后者覆盖,.tdot/.tt/.term-in/.ps1/.tk-*
在 app.js 里 0 引用。留着只会让人误以为改这里有作用,故删除。
新增终端样式请一律加到下方「真 PTY 终端」段。 */
/* ---------- 文件管理器 ---------- */
.fm-toolbar{display:flex;align-items:center;gap:9px;flex-wrap:wrap;margin-bottom:12px}
.fm-path{flex:1 1 300px;min-width:220px;display:flex;align-items:center;gap:6px;
border:1px solid var(--line);border-radius:9px;padding:5px 10px;background:#fbfcfe}
.fm-path input{border:none;background:transparent;padding:2px 0;
font-family:ui-monospace,Consolas,monospace;font-size:12.5px}
.fm-path input:focus{box-shadow:none}
.fm-list{border:1px solid var(--line);border-radius:11px;overflow:hidden;margin-top:12px}
.fm-row{display:flex;align-items:center;gap:11px;padding:8px 12px;border-bottom:1px solid var(--line2);
font-size:13px}
.fm-row:last-child{border-bottom:none}
.fm-row:hover{background:#fafcff}
.fm-row.head{background:#fafbfe;font-size:12px;color:var(--txt2);font-weight:600}
.fm-row.head:hover{background:#fafbfe}
.fm-row .fi{width:20px;text-align:center;flex:0 0 20px;font-size:14px}
.fm-row .fn{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:pointer}
.fm-row .fn:hover{color:var(--pri);text-decoration:underline}
.fm-row .fs{width:90px;flex:0 0 90px;text-align:right;color:var(--txt3);font-size:12px;
font-variant-numeric:tabular-nums}
.fm-row .fm{width:132px;flex:0 0 132px;color:var(--txt3);font-size:12px;
font-family:ui-monospace,Consolas,monospace}
.fm-row .fa{flex:0 0 auto;display:flex;gap:5px}
.fm-row .fa button{padding:3px 8px;font-size:11.5px;border-radius:7px}
@media(max-width:900px){.fm-row .fm{display:none}}
.drop-zone{border:2px dashed var(--line);border-radius:12px;padding:20px;text-align:center;
color:var(--txt3);font-size:13px;transition:.15s;margin-top:12px;background:#fbfcfe}
.drop-zone.hot{border-color:var(--pri);background:var(--pri-l);color:var(--pri)}
.up-item{display:flex;align-items:center;gap:10px;padding:6px 0;font-size:12.5px}
.up-item .un{flex:1 1 auto;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.up-item .ub{flex:0 0 150px;height:7px;border-radius:4px;background:var(--line2);overflow:hidden}
.up-item .ub i{display:block;height:100%;background:var(--grad);width:0;transition:width .2s}
.up-item .up{flex:0 0 52px;text-align:right;color:var(--txt3);font-variant-numeric:tabular-nums}
.up-item.done .un{color:var(--ok)} .up-item.bad .un{color:var(--err)}
/* ---------- 加速开关大按钮 ---------- */
.accel-box{display:flex;align-items:center;gap:18px;flex-wrap:wrap;padding:16px 18px;
border:1px solid var(--line);border-radius:12px;background:linear-gradient(135deg,#fbfdff,#f4f8ff)}
.accel-state{flex:0 0 auto;display:flex;align-items:center;gap:9px;font-size:15px;font-weight:650}
.accel-state .as-dot{width:11px;height:11px;border-radius:50%;flex:0 0 11px}
.accel-state.on .as-dot{background:#12b76a;box-shadow:0 0 0 4px rgba(18,183,106,.18)}
.accel-state.off .as-dot{background:#c3ccdb;box-shadow:0 0 0 4px rgba(195,204,219,.22)}
.accel-note{font-size:12.5px;color:var(--txt3);line-height:1.7;flex:1 1 340px;min-width:240px}
/* ==================================================================
新增模块样式(#17–#22)—— 与视图中的类名严格对应
================================================================== */
/* ---------- 仪表卡惊叹号提示(虚拟机无温度传感器等) ---------- */
.gauge-warn{display:inline-flex;align-items:center;justify-content:center;width:17px;height:17px;
border-radius:50%;background:var(--warn);color:#fff;font-size:11.5px;font-weight:800;
cursor:help;flex:0 0 17px;line-height:1;box-shadow:0 0 0 3px rgba(245,158,11,.16);
animation:gwpulse 2.2s ease-in-out infinite}
@keyframes gwpulse{0%,100%{box-shadow:0 0 0 3px rgba(245,158,11,.16)}
50%{box-shadow:0 0 0 6px rgba(245,158,11,.05)}}
.gauge-note{margin-top:9px;padding:8px 11px;border-radius:9px;font-size:12px;line-height:1.7;
color:#8a5a00;background:#fff8e6;border:1px solid #f5dfae}
/* ---------- NAT 徽标(新结构:nat-badge + nat-ico/nat-txt/nat-t/nat-d) ---------- */
.nat-badge{display:flex;align-items:center;gap:16px;padding:16px 18px;border-radius:12px;
border:1px solid var(--line);background:linear-gradient(135deg,#fbfdff,#f4f8ff)}
.nat-badge .nat-ico{flex:0 0 auto;width:56px;height:56px;border-radius:50%;display:flex;
align-items:center;justify-content:center;font-size:22px;font-weight:700;color:#fff;
background:linear-gradient(135deg,#94a3b8,#64748b);box-shadow:0 4px 14px rgba(20,45,95,.14)}
.nat-badge .nat-txt{min-width:0;flex:1}
.nat-badge .nat-t{font-size:16px;font-weight:650;margin-bottom:3px}
.nat-badge .nat-d{font-size:12.5px;color:var(--txt3);line-height:1.68}
.nat-badge.ok .nat-ico{background:linear-gradient(135deg,#12b76a,#0a8f50)}
.nat-badge.info .nat-ico{background:linear-gradient(135deg,#3b82f6,#1d5fd0)}
.nat-badge.warn .nat-ico{background:linear-gradient(135deg,#f59e0b,#d97706)}
.nat-badge.err .nat-ico{background:linear-gradient(135deg,#ef4444,#c0392b)}
.nat-badge.ok{border-color:#b7e4c7;background:linear-gradient(135deg,#f4fdf8,#eefaf3)}
.nat-badge.err{border-color:#f7c9c4;background:linear-gradient(135deg,#fff7f6,#fdeeed)}
.nat-detail{margin-top:12px;padding:11px 13px;border-radius:9px;background:#0d1526;color:#cdd9ea;
font-size:12px;line-height:1.7;white-space:pre-wrap;word-break:break-all}
/* ---------- 软加速状态框(新结构) ---------- */
.accel-box{display:block;padding:16px 18px;border:1px solid var(--line);border-radius:12px;
background:linear-gradient(135deg,#fbfdff,#f4f8ff)}
.accel-box.on{border-color:#b7e4c7;background:linear-gradient(135deg,#f4fdf8,#eefaf3)}
.accel-state{display:flex;align-items:center;gap:10px;font-size:15px;font-weight:650;margin-bottom:6px}
.accel-meta{margin-top:8px}
.impact-list{display:flex;flex-direction:column;gap:2px}
.impact-item{display:flex;gap:11px;align-items:flex-start;padding:9px 0;border-bottom:1px dashed var(--line2)}
.impact-item:last-child{border-bottom:none}
.impact-dot{flex:0 0 9px;width:9px;height:9px;border-radius:50%;margin-top:5px;background:var(--txt3)}
.impact-dot.warn{background:var(--warn)} .impact-dot.err{background:var(--err)}
.impact-dot.info{background:var(--pri)} .impact-dot.ok{background:var(--ok)}
/* ---------- VLAN 行 ---------- */
.vlan-row{display:flex;align-items:center;justify-content:space-between;gap:12px;
padding:11px 14px;border:1px solid var(--line);border-radius:11px;margin-bottom:9px;
background:#fbfcfe;flex-wrap:wrap}
.vlan-row:hover{background:#f7faff;border-color:#cddffa}
.vlan-row > div{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
/* ---------- 依赖自检(新结构:dep-grid / dep-item) ---------- */
/* ⚠️ .dep-grid 的 minmax(300px,1fr) + auto-fill 意味着**屏越宽、列数越多、
单张卡片越窄**(1920 下 5~7 列,每张只有 ~307px;而 800px 窗口只有 1 列、
卡片 560px)。所以这一块的排版问题必须在 1920/2560 下量,窄屏反而看不出来。 */
.dep-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:10px;margin-top:10px}
/* flex-wrap:wrap 是必需的:卡片里除了「图标 + 正文」还有分组标签、必需/可选
标签、「安装」按钮,一行塞不下。不给 wrap,正文就会被挤成细柱。 */
.dep-item{display:flex;align-items:flex-start;gap:11px;flex-wrap:wrap;padding:11px 13px;border-radius:10px;
border:1px solid var(--line);background:#fbfcfe}
.dep-item .dep-ico{flex:0 0 22px;width:22px;height:22px;border-radius:50%;display:flex;
align-items:center;justify-content:center;font-size:12.5px;font-weight:700;color:#fff;margin-top:1px}
.dep-item.ok .dep-ico{background:var(--ok)}
.dep-item.err .dep-ico{background:var(--err)}
.dep-item.warn .dep-ico{background:var(--warn)}
.dep-item.ok{border-color:#c9ecd8}
.dep-item.err{border-color:#f7c9c4;background:#fff8f7}
.dep-item.warn{border-color:#f5dfae;background:#fffcf4}
/* ⚠️ 必须是 `flex:1 1 200px`(**带下限的 basis**),不能是 `flex:1`
(= 1 1 0%,下限为 0)。真机 1908px 实测(英文依赖自检页,2026-10-10):
卡片 307px,标签 + 按钮吃掉 ~190px,`flex:1` 的正文只剩 **27px**,
描述被折成 23 行、每行 5 个字符 —— 用户看到的就是「字是竖着的」。
给 basis 一个 200px 下限后:装不下时标签/按钮整行换到下一行,
正文拿到 246px / 5 行。
⚠️ app.js 里有 6 处内联 `style="flex:1"` 覆盖(内联优先级更高),已一并改。 */
.dep-item .dep-body{flex:1 1 200px;min-width:0}
/* ⚠️ 告警规则卡(#151)外层的 .dep-item 是「图标 + 正文」的**横向** flex 容器,
但模板里直接塞了 .row + <p> 两个块 → 它们被当成并排的 flex 项,.row 被压到
26px、规则名一字一行(真机 390px 实测「外网连接中断」w=26 / 6 行)。
这里让外层退回块级,两个块正常上下堆叠、各占满宽。 */
#al-rules>.dep-item{display:block}
.dep-item .dep-body b{font-size:13.5px}
.dep-item .dep-where{font-size:11.5px;color:var(--txt3);margin-top:3px;word-break:break-all}
/* 分组过滤按钮的选中态:没有它用户点「外置存储」看不出到底选中了哪个 */
.dep-grp.on{border-color:var(--pri);color:var(--pri);background:var(--pri-l);font-weight:600}
/* ---------- 顶部小提示条 ---------- */
.notice{padding:11px 14px;border-radius:10px;font-size:13px;line-height:1.7;border:1px solid var(--line);
background:#fbfcfe;color:var(--txt2)}
.notice.ok{border-color:#c9ecd8;background:#f4fdf8;color:#0a6b3d}
.notice.err{border-color:#f7c9c4;background:#fff8f7;color:#a3271c}
.notice.warn{border-color:#f5dfae;background:#fffcf4;color:#8a5a00}
/* ---------- Web 终端(新结构:term-bar/term-dot/term-title/term-body/term-input/term-prompt) ---------- */
.term-wrap{border:1px solid #22314d;border-radius:11px;overflow:hidden;background:#0d1526;
display:flex;flex-direction:column;margin-bottom:14px;position:relative}
.term-bar{display:flex;align-items:center;gap:9px;padding:8px 12px;background:#16233a;
border-bottom:1px solid #22314d;font-size:12.5px;color:#9db0cc}
.term-dot{width:10px;height:10px;border-radius:50%;background:#31405c;flex:0 0 10px}
.term-dot.r{background:#e05c4c} .term-dot.y{background:#d9a400} .term-dot.g{background:#3fa15c}
.term-title{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* 栏内小按钮(清屏 / 回到底部)。刻意做得比正文小、低对比:
这些是辅助动作,不该和「连接 / 断开」抢注意力。 */
.term-btn{flex:0 0 auto;padding:2px 9px;font-size:11.5px;line-height:1.6;cursor:pointer;
border:1px solid #2b3c5c;border-radius:6px;background:#1b2a44;color:#9db0cc;
font-family:inherit;transition:background .15s,color .15s,border-color .15s}
.term-btn:hover{background:#233754;color:#dce7f7;border-color:#3a5080}
.term-btn:active{background:#16233a}
.term-btn[hidden]{display:none}
.term-body{height:clamp(260px,54vh,560px);overflow-y:auto;overflow-x:hidden;
margin:0;padding:10px 12px;
font-family:ui-monospace,Consolas,"Cascadia Code",monospace;font-size:12.5px;line-height:1.62;
color:#cdd9ea;white-space:pre-wrap;word-break:break-all;background:transparent;border:none;border-radius:0;
/* 去掉右侧滚动条:长回显靠「自动跟随底部」呈现,不靠拖条。
滚动能力保留(滚轮 / PgUp / Ctrl+Home 仍可用),只是不画那根条。 */
scrollbar-width:none; /* Firefox */
-ms-overflow-style:none; /* 旧 Edge / IE */
/* 滚到边界时不要把滚动传给外层页面,否则在终端里滚到底会把整页卷走 */
overscroll-behavior:contain}
.term-body::-webkit-scrollbar{width:0;height:0;display:none} /* Chrome / Edge / Safari */
.term-input{display:flex;align-items:center;gap:8px;margin-top:10px}
.term-prompt{color:var(--pri);font-family:ui-monospace,Consolas,monospace;font-size:14px;flex:0 0 auto}
.term-input input{flex:1;font-size:12.5px;font-family:ui-monospace,Consolas,monospace}
/* ---------- 真 PTY 终端(#4 重做):网格化渲染 ---------- */
/* 终端按「行」渲染:每行一个 .tl,字符对齐靠等宽字体 + 固定行高。
这里必须 white-space:pre —— pre-wrap 会把超宽行折回来,行列就对不上了。 */
.term-screen{white-space:pre;word-break:normal;overflow-x:hidden}
.tl{white-space:pre;height:1.62em;line-height:1.62;min-height:1.62em}
/* 光标方块:只在当前行当前列画一个 */
.tcur{background:#7fa6d8;color:#0d1526;border-radius:1px}
.t-b{font-weight:700}
.t-dim{opacity:.62}
.t-i{font-style:italic}
.t-u{text-decoration:underline}
/* 隐藏的输入框:真输入框才能拿到输入法 / 粘贴 / 组合键,
但不能挡住终端,也不能让用户看见它。放在终端底部让输入法候选框位置正常。 */
.ws-capture{position:absolute;left:0;bottom:0;width:100%;height:1.4em;
pointer-events:none; /* 不能挡住终端的文字选择;聚焦由脚本在 mouseup 时补 */
opacity:0;border:0;padding:0;margin:0;resize:none;outline:none;
background:transparent;color:transparent;caret-color:transparent;
font-family:ui-monospace,Consolas,monospace;font-size:12.5px;z-index:2}
.ws-probe{pointer-events:none}
/* ---------- 文件管理器(新结构:表格 + 面包屑 + 进度) ---------- */
.fm-crumbs{display:flex;align-items:center;flex-wrap:wrap;gap:2px;font-size:12.5px;
padding:7px 11px;border:1px solid var(--line);border-radius:9px;background:#fbfcfe}
.fm-crumb{color:var(--pri);cursor:pointer;padding:1px 4px;border-radius:5px}
.fm-crumb:hover{background:var(--pri-l)}
.fm-sep{color:var(--txt3);padding:0 1px}
.fm-upload{cursor:pointer}
.fm-name{display:flex;align-items:center;gap:7px;min-width:0}
.fm-open{cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.fm-open:hover{color:var(--pri);text-decoration:underline}
/* ⚠️ flex 必须放在**内层 div** 上,不能放在 td 上(2026-10-05 修)。
td 变 flex 容器后会脱离表格列宽计算:表头「操作」列按 <th> 内容定宽,
数据行却按 4 个按钮的宽度撑开 —— 两边对不齐,标题与按钮组错位。
现在 td 只负责列宽对齐,flex 交给 .fm-acts-in。 */
.fm-acts{white-space:nowrap;vertical-align:top}
.fm-acts-in{display:flex;gap:5px;flex-wrap:wrap}
.fm-acts button{padding:3px 8px;font-size:11.5px;border-radius:7px}
/* 各列固定宽度:表头与数据行共用,保证「名称/大小/修改时间/权限/操作」纵向对齐 */
.fm-chk{width:34px}
.fm-c-size{width:88px}
.fm-c-time{width:132px}
.fm-c-mode{width:104px}
.fm-c-act{width:250px}
.fm-chk{width:30px;text-align:center}
.up-name{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.up-st{flex:0 0 190px;text-align:right}
@media(max-width:760px){.up-st{flex:0 0 auto}.fm-acts button span{display:none}}
/* ---------- PPPoE 多拨 ---------- */
.ppm-row{padding:9px 12px}
.ppm-row input{font-size:12.5px}
.ppm-row .ppm-weight{text-align:center}
/* 多拨会话表:窄屏时允许横向滚动,避免挤压 */
.ppm-sess td{vertical-align:middle}
/* ---------- QoS 智能限速 ---------- */
.qos-ip-row{padding:9px 12px}
.qos-ip-row input{font-size:12.5px}
/* 影响说明卡片:用左边框颜色区分级别,比圆点更醒目 */
.impact-item{padding:10px 12px;border:1px solid var(--line);border-left-width:3px;
border-radius:9px;background:#fbfcfe;margin-bottom:8px;display:block}
.impact-item b{font-size:13px;display:block;margin-bottom:3px}
.impact-warn{border-color:#f5dfae;border-left-color:var(--warn);background:#fffcf4}
.impact-err{border-color:#f7c9c4;border-left-color:var(--err);background:#fff8f7}
.impact-info{border-color:#cfe0f7;border-left-color:var(--pri);background:#f7faff}
.impact-ok{border-color:#c9ecd8;border-left-color:var(--ok);background:#f4fdf8}
/* QoS 页里内嵌的 accel-box(与网络状态页风格保持一致) */
.accel-box{border:1px solid var(--line);border-radius:11px;padding:12px 14px;background:#fbfcfe}
/* ---------- DPI 应用识别 ---------- */
/* 等宽代码块:命令、日志都用它,方便用户三指选择复制 */
.term-pre{margin:0;padding:11px 13px;border-radius:10px;background:#0d1526;color:#cdd9ea;
font-family:ui-monospace,Consolas,"Cascadia Code",monospace;font-size:12.5px;line-height:1.62;
white-space:pre-wrap;word-break:break-all;overflow:auto;max-height:420px}
/* 浅色主题下也保持深色代码块 —— 与终端视觉一致,用户一眼识别「这是可复制内容」 */
.term-pre + button{margin-top:8px}
.dpi-mirror-ok{color:var(--ok);font-weight:600}
.dpi-mirror-bad{color:var(--txt3)}
/* ---------- 端口转发 / DMZ ---------- */
/* 每条转发规则的卡片行:小屏自动换行,避免横向滚动 */
.pfrow{background:#fbfcfe;transition:border-color .15s}
.pfrow:hover{border-color:var(--acc,#3b82f6)}
.pfrow label{font-size:12px;color:var(--txt3)}
.pfrow input,.pfrow select{font-size:12.5px;padding:5px 8px}
.pfrow button{margin-bottom:1px}
/* ---------- 动态域名 DDNS ---------- */
.dd-prov-tag{font-size:11px}
#dd-fields .row{gap:10px}
#dd-fields input{font-size:12.5px}
/* ---------- 访问控制 / 家长时间组(#8) ---------- */
.aclrow{background:#fbfcfe;transition:border-color .15s}
.aclrow:hover{border-color:var(--acc,#3b82f6)}
.aclrow label{font-size:12px;color:var(--txt3)}
.aclrow input,.aclrow select{font-size:12.5px;padding:5px 8px}
.aclrow .switch{font-size:12px;color:var(--txt2)}
#acl-presets button{font-size:12.5px}
#acl-out pre{max-height:420px;overflow:auto}
#acl-out pre,#pf-out pre{white-space:pre;tab-size:2}
/* ---------- 内网文件共享 SMB / NFS(#9) ---------- */
.sharecard label{font-size:12px;color:var(--txt3)}
.sharecard input,.sharecard select{font-size:12.5px;padding:5px 8px}
.sharecard .switch{font-size:12px;color:var(--txt2)}
#sh-hints .mono{cursor:copy}
#sh-out pre{max-height:460px;overflow:auto;white-space:pre;tab-size:2}
/* ---------- Docker / Docker Compose 面板(#10) ---------- */
.dk-table{display:flex;flex-direction:column;gap:0}
.dk-thead,.dk-trow{display:grid;grid-template-columns:minmax(0,1.6fr) minmax(0,.9fr) minmax(0,1.1fr) minmax(0,1.1fr) auto;
gap:10px;align-items:center;padding:9px 10px;border-bottom:1px solid var(--line2);font-size:12.5px}
.dk-thead{color:var(--txt3);font-size:11.5px;font-weight:600;border-bottom:1px solid var(--line);
background:var(--panel2);border-radius:8px 8px 0 0}
.dk-trow:last-child{border-bottom:none}
.dk-trow:hover{background:#fbfcfe}
/* 单元格内容要能断:长设备名(HP_OfficeJet_Pro_7740_series)是不可断的
Latin token,不给 break-word 就会撑出 300+px,把整块内容顶出内容区
(真机 390px 实测 .mono 内容 487px / 盒子 148px)。 */
.dk-trow>*{min-width:0;overflow-wrap:break-word}
.dk-crow{align-items:start}
.dk-metric{display:flex;flex-direction:column;gap:2px}
.dk-acts{display:flex;gap:5px;flex-wrap:wrap;justify-content:flex-end}
.dk-acts button{padding:3px 8px;font-size:11.5px}
.dk-bar{display:inline-block;width:74px;height:6px;border-radius:3px;background:#e9edf3;
overflow:hidden;vertical-align:middle;margin-right:6px}
.dk-bar i{display:block;height:100%;border-radius:3px}
#dk-out pre,#dk-cv-out pre{tab-size:2}
#dk-st-yaml,#dk-cv-in{width:100%;box-sizing:border-box}
#dk-cv-msg p{margin:6px 0}
@media (max-width:960px){
.dk-thead,.dk-trow{grid-template-columns:minmax(0,1fr) minmax(0,1fr);}
.dk-thead{display:none}
.dk-acts{grid-column:1/-1}
}
/* ---------- 主题之家(#12) ---------- */
.tm-current{display:flex;align-items:center;gap:16px;padding:12px 14px;border-radius:11px;
border:1px solid var(--line);background:linear-gradient(135deg,#fbfdff,#f4f8ff);flex-wrap:wrap}
.tm-cur-swatch{flex:0 0 auto;display:flex;border-radius:9px;overflow:hidden;
border:1px solid var(--line);box-shadow:0 1px 3px rgba(18,28,55,.08)}
.tm-cur-swatch i{display:block;width:26px;height:34px}
.tm-cur-body{flex:1;min-width:180px}
.tm-cur-acts{flex:0 0 auto}
.tm-tabs{display:flex;gap:6px;margin:0 0 14px;flex-wrap:wrap}
.tm-tab{padding:7px 18px;font-size:13px;border-radius:20px}
.tm-tab.on{background:var(--grad);color:#fff;border-color:transparent;font-weight:600}
/* 主题卡片网格 */
.tm-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(258px,1fr));gap:14px;margin-top:12px}
.tm-card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel);
display:flex;flex-direction:column;transition:.16s}
.tm-card:hover{border-color:var(--pri);box-shadow:0 4px 14px rgba(20,45,95,.1)}
.tm-card.on{border-color:var(--ok);box-shadow:0 0 0 2px var(--ok-l)}
.tm-card-top{position:relative;height:96px;padding:8px;background:linear-gradient(135deg,var(--sw1),var(--sw2))}
.tm-card-mock{display:flex;gap:5px;height:100%;border-radius:7px;overflow:hidden;
border:1px solid var(--line);background:var(--sw2)}
.tm-mk-side{flex:0 0 30%;background:var(--sw2);border-right:1px solid var(--line);
padding:6px 5px;display:flex;flex-direction:column;gap:4px}
.tm-mk-side i{display:block;height:8px;width:8px;border-radius:3px;background:var(--sw0)}
.tm-mk-side span{display:block;height:4px;border-radius:2px;background:var(--sw1)}
.tm-mk-side span:nth-child(3){width:70%}
.tm-mk-main{flex:1;padding:6px 5px;display:flex;flex-direction:column;gap:5px;min-width:0}
.tm-mk-bar{height:6px;border-radius:2px;background:var(--sw3);opacity:.16}
.tm-mk-body{flex:1;border-radius:4px;background:var(--sw1);border:1px solid var(--line)}
.tm-card-use{position:absolute;right:8px;top:8px;font-size:10.5px;font-weight:600;
color:#fff;background:var(--ok);padding:2px 8px;border-radius:9px;
box-shadow:0 1px 4px rgba(10,30,60,.2)}
.tm-card-info{padding:11px 13px 6px;flex:1}
.tm-card-info b{font-size:13.5px;display:inline-block;margin-right:6px}
.tm-card-info .tag{font-size:10.5px;transform:translateY(-1px)}
.tm-card-acts{display:flex;gap:6px;flex-wrap:wrap;padding:0 13px 12px}
.tm-card-acts button{padding:4px 11px;font-size:12px}
/* 设计器双栏 */
.tm-split{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:16px;align-items:start}
@media(max-width:1080px){.tm-split{grid-template-columns:minmax(0,1fr)}}
.tm-frame{width:100%;height:460px;border:1px solid var(--line);border-radius:10px;
background:var(--panel2);display:block}
/* 变量行 */
.tm-cat{margin-bottom:14px}
.tm-cat-t{font-size:12px;font-weight:600;color:var(--pri);margin-bottom:7px;
padding-bottom:4px;border-bottom:1px dashed var(--line2)}
.tm-vrow{display:flex;align-items:center;gap:8px;padding:4px 0}
.tm-vname{flex:1 1 130px;min-width:92px;font-size:12.5px;color:var(--txt2);line-height:1.35}
.tm-color{flex:0 0 34px;width:34px;height:28px;padding:0;border:1px solid var(--line);
border-radius:7px;background:none;cursor:pointer}
.tm-color::-webkit-color-swatch-wrapper{padding:2px}
.tm-color::-webkit-color-swatch{border:none;border-radius:5px}
.tm-vtext{flex:1 1 130px;min-width:96px;font-size:12px;padding:5px 8px}
.tm-vreset{flex:0 0 auto;padding:3px 7px;font-size:12px;color:var(--txt3)}
/* 校验结果 */
.tm-check{margin-top:10px;padding:11px 13px;border-radius:9px;font-size:12.5px;line-height:1.7}
.tm-check.ok{border:1px solid #c9ecd8;background:#f4fdf8;color:#0a6b3d}
.tm-check.bad{border:1px solid #f7c9c4;background:#fff8f7;color:#a3271c}
.tm-check.bad ul{margin:6px 0 0;padding-left:18px}
.tm-check.bad li{list-style:disc}
/* 拖拽上传 */
.tm-drop{border:2px dashed var(--line);border-radius:12px;padding:22px;text-align:center;
background:#fbfcfe;transition:.15s}
.tm-drop.hot{border-color:var(--pri);background:var(--pri-l)}
#tm-code{max-height:280px;white-space:pre;tab-size:2}
.code-wrap{position:relative}
/* ---------- 连接跟踪与统一日志(#11) ---------- */
.ul-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:12px 20px}
.ul-stats>b{display:block}
.ul-stats>div>b{display:block;font-size:12px;color:var(--txt3);font-weight:600;
margin-bottom:5px}
.ul-chip{display:inline-block;font-size:11.5px;padding:2px 7px;border-radius:6px;
background:#eef2f8;color:var(--txt2);margin:0 4px 4px 0;font-variant-numeric:tabular-nums}
.ul-chip b{font-weight:600;color:var(--txt)}
.ul-table{display:flex;flex-direction:column}
.ul-thead,.ul-trow{display:grid;
grid-template-columns:128px 104px 74px 92px 200px minmax(0,1fr);
gap:10px;align-items:start;padding:8px 10px;border-bottom:1px solid var(--line2);
font-size:12.5px}
.ul-thead{color:var(--txt3);font-size:11.5px;font-weight:600;
border-bottom:1px solid var(--line);background:var(--panel2);
border-radius:8px 8px 0 0;position:sticky;top:0;z-index:1}
.ul-trow{cursor:pointer;transition:background .12s}
.ul-trow:hover{background:#fbfcfe}
.ul-trow:last-child{border-bottom:none}
/* 窄屏下 .ul-trow 折成两列,单元格只有 ~150px;里面的 MAC / IPv6 / 长主机名
是不可断的 Latin token,不给 break-word 就会把内容顶出内容区
(真机 390px 实测 #view 横向溢出 12px,来源是 .ul-trow 里的 197px 长串)。 */
.ul-trow>*{min-width:0;overflow-wrap:break-word}
.ul-ico{display:inline-block;width:16px;color:var(--txt3)}
#ul-out p{margin:8px 0 0}
/* ---------- 新手向导 ----------
步骤条刻意做成「可点的按钮」而不是进度指示器:新手最常做的事是
「刚才那步我填错了,回去看看」,做成纯展示的话他得从菜单绕一圈。 */
.wiz-steps{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
.wiz-step{display:flex;align-items:center;gap:8px;padding:8px 12px;border-radius:10px;
border:1px solid var(--line);background:#fff;font-size:13px;cursor:pointer;
transition:all .14s;font-family:inherit}
.wiz-step:hover{border-color:var(--pri);color:var(--pri)}
.wiz-step.on{border-color:var(--pri);background:var(--pri-l, #eef4ff);box-shadow:0 1px 6px rgba(30,90,200,.12)}
.wiz-step.ok .wiz-step-ico{background:var(--ok);color:#fff}
.wiz-step-ico{display:inline-flex;align-items:center;justify-content:center;
width:22px;height:22px;border-radius:50%;background:var(--panel2);
color:var(--txt2);font-size:12px;font-weight:600;flex:0 0 auto}
.wiz-step.on .wiz-step-ico{background:var(--pri);color:#fff}
.wiz-arrow{color:var(--txt3);font-size:15px;padding:0 1px}
/* DNS 服务商选择卡:整块可点,比下拉框更容易看懂「选它是为了什么」 */
.wiz-dns-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));
gap:9px;margin-top:8px}
.wiz-dns{display:flex;flex-direction:column;gap:3px;align-items:flex-start;text-align:left;
padding:10px 12px;border-radius:10px;border:1px solid var(--line);background:#fff;
cursor:pointer;font-family:inherit;transition:all .14s}
.wiz-dns:hover{border-color:var(--pri);background:#fafcff}
.wiz-dns b{font-size:13px;color:var(--txt)}
.wiz-dns .mono{font-size:11.5px;color:var(--pri)}
.wiz-dns-d{font-size:11.5px;color:var(--txt3);line-height:1.6}
.wiz-dns-isp{border-style:dashed}
@media (max-width:760px){
/* 窄屏下步骤条改成两列网格,横向排会挤出屏幕并触发整页横向滚动 */
.wiz-steps{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:8px}
.wiz-arrow{display:none}
.wiz-step{width:100%;justify-content:flex-start;padding:10px}
.wiz-step-n{font-size:12px}
.wiz-dns-grid{grid-template-columns:minmax(0,1fr)}
}
@media (max-width:1080px){
.ul-thead{display:none}
.ul-trow{grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:4px 10px}
}
/* ==========================================================================
手机 / 窄屏适配(#14)
思路:窄屏下侧边栏改为「抽屉」,主区占满整宽;表格统一包 .tw 横向滚动;
表单单列堆叠;控件放大到可点尺寸;避开 iOS 输入框自动缩放与刘海安全区。
========================================================================== */
/* 表格横向滚动容器:由 JS 在渲染后统一包裹,桌面端也能用(内容过宽时不撑破布局) */
.tw{overflow-x:auto;max-width:100%;-webkit-overflow-scrolling:touch}
.tw>table{margin:0}
/* 抽屉遮罩:默认不存在,窄屏才启用 */
.m-mask{display:none}
@media (max-width:760px){
/* ---- 1. 侧边栏 → 抽屉 ---- */
.sidebar{position:fixed;left:0;top:0;bottom:0;z-index:60;
width:min(84vw,304px);flex:0 0 auto;
transform:translateX(-100%);transition:transform .22s ease;
box-shadow:0 12px 40px rgba(15,23,42,.28);
padding-top:env(safe-area-inset-top);padding-bottom:env(safe-area-inset-bottom)}
#app.m-drawer .sidebar{transform:translateX(0)}
/* 抽屉里恢复完整文字(覆盖上面 760px 里把文字隐藏的规则) */
#app.m-drawer .brand-txt{display:block}
#app.m-drawer .nav-search{display:block}
#app.m-drawer .ng-t,#app.m-drawer .ng-n,#app.m-drawer .ng-arrow{display:block}
#app.m-drawer .nitem-t{display:block}
#app.m-drawer .nitem-tag{display:inline-block}
#app.m-drawer .ng-head,#app.m-drawer .nitem{justify-content:flex-start}
#app.m-drawer .side-foot{display:block}
/* 折叠态在窄屏无意义(抽屉已接管),避免图标被挤到中间 */
#app.side-collapsed .sidebar{width:min(84vw,304px);flex-basis:auto}
#app.side-collapsed .ng-head,#app.side-collapsed .nitem{justify-content:flex-start}
.m-mask{display:block;position:fixed;inset:0;z-index:55;
background:rgba(15,23,42,.42);opacity:0;pointer-events:none;transition:opacity .2s}
#app.m-drawer .m-mask{opacity:1;pointer-events:auto}
/* ---- 2. 顶栏瘦身 ---- */
.topbar{height:52px;flex:0 0 52px;padding:0 12px;gap:8px}
.topbar h2{font-size:15px}
.tb-right{gap:8px}
.clockbox{padding:3px 6px;gap:7px}
.ck-time{font-size:15px}
/* ---- 3. 主区:留出底部操作条与安全区 ---- */
.view{padding:12px 12px calc(104px + env(safe-area-inset-bottom))}
.card{padding:14px 14px;margin-bottom:12px;border-radius:12px}
.card .desc{margin:0 0 12px}
/* ---- 4. 表单:单列堆叠,输入框 16px 防止 iOS 自动放大 ----
⚠️ 必须 !important:视图里有几十处 `style="flex:0 0 110px"` 这类**内联**
固定宽度(协议/端口/时间范围等),内联样式优先级高于样式表,
不压过它的话手机上输入框仍是固定宽度左对齐 —— 右边留一大片空白,
看着就是「文字和控件都错位了」。 */
.row{gap:8px}
.row>*{flex:1 1 100% !important;min-width:0}
.row .fixed{flex:0 0 auto !important}
input,select,textarea{font-size:16px}
input,select,textarea,button{max-width:100%}
label{display:block}
/* ---- 5. 触控尺寸 ---- */
#btn-side,.icon-btn{min-width:40px;min-height:40px;display:inline-flex;
align-items:center;justify-content:center}
.ab-btns button{min-height:42px;padding:0 16px}
/* ---- 6. 底部操作条:窄屏换行,避免按钮被挤出屏幕 ---- */
#actionbar{flex-wrap:wrap;gap:8px;padding:10px 12px;
padding-bottom:calc(10px + env(safe-area-inset-bottom))}
.ab-msg{flex:1 1 100%;order:2;min-height:0}
.ab-btns{flex:1 1 100%;order:1;justify-content:stretch}
.ab-btns button{flex:1 1 0}
/* ---- 7. 弹窗 / 提示 ---- */
.modal-box{max-width:100%;margin:0 12px;padding:18px 16px;border-radius:14px;
max-height:86vh;overflow-y:auto}
.modal-box .acts{flex-wrap:wrap;gap:8px}
.modal-box .acts button{flex:1 1 120px}
.toast{right:12px;left:12px;bottom:calc(12px + env(safe-area-inset-bottom));
max-width:none}
/* ---- 8. 表格:缩小字号,交给 .tw 横向滚动 ---- */
table{font-size:12.5px}
th,td{padding:7px 8px}
/* 统一日志的自定义网格表:窄屏已在 1080px 断点折叠为两列,这里再压紧一点 */
.ul-trow{font-size:12px;padding:7px 8px}
/* ---- 9. 等宽代码块 ---- */
pre{font-size:12px;max-height:260px;padding:10px}
/* ---- 10. 登录页 ---- */
.login-box{padding:26px 20px;margin:0 14px;width:100%;max-width:420px}
.login-box h1{font-size:23px}
.login-box button{min-height:46px}
}
/* ---- 网卡「当前角色」联动标签(1.0.10)----
跟在「备注」输入框下面,让用户一眼看到这张卡当前被分成了什么角色,
不用横向去找右侧的下拉框。放在 app.css 而非 update.css ——
它属于网卡页,不属于版本检测模块。 */
.role-live { margin-top:5px; font-size:12px; color:var(--txt3);
display:flex; align-items:center; gap:6px; line-height:1.6; }
.role-live .tag { font-size:11px; padding:1px 8px; border-radius:99px; }
/* ---- 网卡 ethtool 详细计数(1.0.10)----
掉包/错误/冲突这类计数非 0 才标红 —— 大部分时候全是 0,
红字只出现在真的有问题时。 */
.eth-box { margin-top:10px; padding:11px 13px; border:1px solid var(--line);
border-radius:9px; background:rgba(0,0,0,.018); }
.eth-box.eth-off { display:flex; align-items:center; gap:9px;
flex-wrap:wrap; background:transparent; }
.eth-reason { font-size:12px; color:var(--txt3); }
.eth-head { display:flex; align-items:center; gap:7px; flex-wrap:wrap;
margin-bottom:9px; }
.eth-upd { margin-left:auto; font-size:11px; color:var(--txt3);
font-family:var(--mono,ui-monospace,monospace); }
.eth-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(168px,1fr));
gap:2px 14px; }
.eth-row { display:flex; align-items:baseline; gap:8px;
font-size:12px; line-height:1.9; }
.eth-row.eth-warn .eth-k { color:var(--txt2); }
.eth-k { color:var(--txt3); flex:1 1 0; min-width:0; }
.eth-v { color:var(--txt2); font-size:12px; }
.eth-v.cnt-bad { color:var(--err); font-weight:600; }
@media (max-width:640px) {
.eth-grid { grid-template-columns:minmax(0,1fr); }
.eth-upd { margin-left:0; }
}
/* ---- 上游链路卡片(1.0.10)---- */
.up-card { margin-bottom:16px; }
.up-card h3 { display:flex; align-items:center; gap:9px; flex-wrap:wrap; }
.up-row { display:flex; align-items:center; gap:9px; flex-wrap:wrap;
font-size:12.5px; color:var(--txt3); }
.up-card code { background:rgba(0,0,0,.05); padding:1px 5px; border-radius:4px;
font-size:12px; }
/* ---- 网络明细:邻居 / 路由 / 规则表(1.0.10)----
表格用 .tbl(项目已有样式);这里只补这块特有的:
① 默认路由行加重 —— 它最重要,不该和普通路由一样看待
② 长 MAC / IPv6 地址在窄屏下要能换行,不能撑破布局 */
#netdetail-box .tbl { width:100%; }
#netdetail-box tr.row-def td { background:rgba(16,185,129,.06); }
#netdetail-box .mono { font-size:12px; word-break:break-all; }
#netdetail-box .tag { white-space:nowrap; }
@media (max-width:720px) {
#netdetail-box .tbl { display:block; overflow-x:auto; white-space:nowrap; }
}
/* ---- 实时柱状流动图(1.0.10)---- */
.rt-h3t { font-weight:400; font-size:13px; color:var(--txt3); margin-left:8px; }
#realtime-box h3 { display:flex; align-items:center; gap:10px; flex-wrap:wrap; }
.rt-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(268px,1fr));
gap:16px; }
.rt-item { min-width:0; }
.rt-t { font-size:13px; font-weight:600; margin-bottom:6px; }
.rt-u { font-size:11.5px; font-weight:400; color:var(--txt3); margin-left:5px; }
#realtime-box canvas { display:block; width:100%; height:74px;
border:1px solid var(--line); border-radius:7px;
background:rgba(0,0,0,.012); }
.rt-legend { display:flex; gap:12px; flex-wrap:wrap; margin:7px 0 4px; }
.rt-d { font-size:11.5px; color:var(--txt3); display:inline-flex;
align-items:center; gap:5px; }
.rt-d i { width:9px; height:9px; border-radius:2px; display:inline-block; }
.rt-kv { display:flex; gap:8px; font-size:12px; line-height:1.9; }
.rt-kv b { color:var(--txt3); font-weight:400; min-width:56px; }
@media (max-width:640px) {
.rt-grid { grid-template-columns:minmax(0,1fr); }
}
/* ==========================================================================
模块健康总览(#5)
灯的四档语义固定:绿=正常 / 黄=留意 / 红=异常 / 灰=没启用(不是故障)。
颜色只用作**语义信号**,卡片底色与文字仍然全部走主题变量,
这样电光蓝 / 藏青色等主题切换时不会出现「一块不跟着变的白板」。
========================================================================== */
.st-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(272px,1fr));gap:12px}
.st-card{background:var(--panel);border:1px solid var(--line);border-left-width:3px;
border-radius:var(--r);padding:12px 14px;box-shadow:var(--sh);
display:flex;flex-direction:column;gap:9px;min-width:0;transition:.15s}
.st-card:hover{box-shadow:0 3px 12px rgba(18,28,55,.09)}
.st-card.lv-ok{border-left-color:var(--ok)}
.st-card.lv-warn{border-left-color:var(--warn)}
.st-card.lv-err{border-left-color:var(--err)}
.st-card.lv-off,.st-card.lv-na{border-left-color:#c3ccdb}
.st-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap;min-width:0}
.st-name{font-size:13.5px;font-weight:650;min-width:0;overflow-wrap:break-word}
.st-code{font-size:11.5px;color:var(--txt3);min-width:0;overflow-wrap:break-word}
/* 状态灯:外圈用同色低透明描边,让小圆点在浅底上也有体积感 */
.st-dot{width:9px;height:9px;flex:0 0 9px;border-radius:50%;background:#c3ccdb}
.st-dot.lv-ok,.st-card.lv-ok .st-dot{background:var(--ok);box-shadow:0 0 0 3px rgba(18,129,63,.16)}
.st-dot.lv-warn,.st-card.lv-warn .st-dot{background:var(--warn);box-shadow:0 0 0 3px rgba(154,103,0,.16)}
.st-dot.lv-err,.st-card.lv-err .st-dot{background:var(--err);box-shadow:0 0 0 3px rgba(192,57,43,.16)}
.st-dot.lv-off,.st-dot.lv-na,.st-card.lv-off .st-dot,.st-card.lv-na .st-dot{background:#c3ccdb}
/* 异常项轻微呼吸,让「红」在满屏卡片里能被第一眼抓到 */
.st-card.lv-err .st-dot{animation:stpulse 1.9s ease-in-out infinite}
@keyframes stpulse{0%,100%{box-shadow:0 0 0 3px rgba(192,57,43,.16)}
50%{box-shadow:0 0 0 7px rgba(192,57,43,.04)}}
.st-metrics{display:flex;flex-wrap:wrap;gap:5px;min-width:0}
.st-chip{display:inline-flex;align-items:baseline;gap:5px;font-size:11.5px;padding:2px 8px;
border-radius:6px;background:rgba(0,0,0,.035);max-width:100%}
.st-chip i{color:var(--txt3);font-style:normal;white-space:nowrap}
.st-chip b{font-weight:600;font-variant-numeric:tabular-nums;overflow-wrap:break-word}
.st-foot{margin-top:auto}
.st-summary .st-sum-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.st-summary .st-when{margin-left:auto;font-size:12px;color:var(--txt3);
font-variant-numeric:tabular-nums}
@media (max-width:520px){
.st-summary .st-when{margin-left:0}
.st-grid{grid-template-columns:minmax(0,1fr)}
}
/* ⓘ 提示图标。
⚠️ 提示语**不用** CSS ::after 气泡。绝对定位的伪元素会参与滚动容器的
scrollable overflow —— 实测在 health 页把内容区撑出 72px(手机)/106px
(桌面)的横向滚动条,用户能往右拖出一片空白。改用两条腿:
· title 属性 → 桌面悬停出原生气泡,零布局影响;
· 点一下 ⓘ → 展开卡片内的 .st-hint 行内说明(手机上也能读全)。
注意 title 是浏览器原生气泡,样式不可控但永远不会破坏布局。 */
.st-info{flex:0 0 17px;width:17px;height:17px;padding:0;border-radius:50%;
border:1px solid var(--line);background:var(--line2);color:var(--txt3);
font-size:11px;font-weight:700;line-height:1;cursor:pointer;
display:inline-flex;align-items:center;justify-content:center}
.st-info:hover,.st-info:focus-visible{border-color:var(--pri);color:var(--pri);background:var(--pri-l)}
.st-info[aria-expanded="true"]{border-color:var(--pri);color:var(--pri);background:var(--pri-l)}
/* 行内说明:点 ⓘ 展开,占满卡片宽度,不产生任何浮动层 */
.st-hint{font-size:12px;line-height:1.7;color:var(--txt2);background:var(--line2);
border-left:2px solid var(--pri);border-radius:0 7px 7px 0;padding:8px 11px;
overflow-wrap:break-word}
.st-hint[hidden]{display:none}
/* 顶栏模块健康灯:跨页面常驻,点一下进健康总览 */
.tb-health{position:relative}
.tb-health .tb-h-dot{display:block;width:11px;height:11px;border-radius:50%;background:#c3ccdb}
.tb-health.lv-ok .tb-h-dot{background:var(--ok);box-shadow:0 0 0 3px rgba(18,129,63,.16)}
.tb-health.lv-warn .tb-h-dot{background:var(--warn);box-shadow:0 0 0 3px rgba(154,103,0,.16)}
.tb-health.lv-err .tb-h-dot{background:var(--err);box-shadow:0 0 0 3px rgba(192,57,43,.18);
animation:stpulse 1.9s ease-in-out infinite}
.tb-health.lv-off .tb-h-dot,.tb-health.lv-na .tb-h-dot{background:#c3ccdb}
/* ==========================================================================
全局视觉精修(#2)
原则:**只动结构与节奏,不动配色体系**。
· 新颜色一律走主题变量(--pri/--ok/--line…),否则电光蓝 / 藏青色主题下
会冒出一块「不跟着变」的固定色;
· 不改 --r / --sh 这些既有令牌的取值(全站都在用,改了等于全站重排),
只新增细分令牌给新规则用;
· 全部规则追加在文件末尾 —— 依赖 CSS 层叠来覆盖,不动上面的既有声明,
这样任何一条出问题都能单独摘掉,不会连坐。
========================================================================== */
:root{
/* 间距节奏:4 的倍数。新写组件优先用这三档,避免出现 7px / 13px 这类随手值 */
--sp1:6px; --sp2:12px; --sp3:18px;
/* 圆角细分:小控件 / 卡片 / 大容器 */
--r-sm:9px; --r-lg:16px;
/* 统一过渡:所有 hover 反馈用同一条曲线,避免有的 .15s 有的 .3s 显得散 */
--tr:.16s ease;
--sh-hov:0 4px 16px rgba(18,28,55,.10);
}
/* ---------- 1. 键盘可达性:焦点环 ---------- */
/* 鼠标点击不显示、键盘 Tab 才显示。项目里只有部分按钮写了 focus-visible,
补一条全局兜底 —— 纯键盘用户靠它认路。 */
button:focus-visible,
a:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible,
select:focus-visible,
input[type=checkbox]:focus-visible,
input[type=radio]:focus-visible{
outline:2px solid var(--pri);outline-offset:2px;
}
input:focus-visible,textarea:focus-visible{outline:none}
/* ---------- 2. 动效尊重系统偏好 ---------- */
/* 前庭功能敏感的用户会因动画不适;系统开了「减少动态效果」就全部停掉。
注意:不能简单写 *{animation:none} —— 会把加载态的旋转也停掉,
那些是**功能性**动画,停了用户就不知道还在跑。所以只关装饰性动效。 */
@media (prefers-reduced-motion: reduce){
.st-card.lv-err .st-dot,.tb-health.lv-err .tb-h-dot,.gauge-warn{animation:none}
*{transition-duration:.01ms !important}
}
/* ---------- 3. 文字选中与滚动条 ---------- */
::selection{background:var(--pri-l);color:var(--txt)}
/* 细滚动条:默认的系统滚动条在深色主题下是一条刺眼的白带 */
*{scrollbar-width:thin;scrollbar-color:#c3ccdb transparent}
::-webkit-scrollbar{width:9px;height:9px}
::-webkit-scrollbar-track{background:transparent}
::-webkit-scrollbar-thumb{background:#c3ccdb;border-radius:5px;
border:2px solid transparent;background-clip:content-box}
::-webkit-scrollbar-thumb:hover{background:#a9b6cc;background-clip:content-box}
/* 深色底容器(终端 / 代码块)里换深色滑块 */
.pplog::-webkit-scrollbar-thumb,.term-body::-webkit-scrollbar-thumb,
.term-pre::-webkit-scrollbar-thumb,.nat-detail::-webkit-scrollbar-thumb{background:#3a4a66}
.pplog,.term-pre{scrollbar-color:#3a4a66 transparent}
/* ---------- 4. 卡片标题:左侧主色小竖条 ---------- */
/* 一屏里卡片多,标题纯靠字号区分层级不够;加一条 3px 主色条后
「这是第几块」一眼可辨。绝对定位所以不参与 flex 布局,不会挤动标题里的徽标。 */
/* ⚠️ 竖条用 left:-12px 挂在卡片的内边距里,**不给 h3 加 padding-left**。
加 padding-left 会把标题文字整体右移 11px,而下面的 .desc / 表格仍在 0 位,
一屏卡片读起来就是「标题歪了」(实测被 _dev/text-audit.sh 的 offset 判据抓到)。 */
.card>h3{position:relative}
.card>h3::before{content:'';position:absolute;left:-12px;top:2px;bottom:2px;width:3px;
border-radius:2px;background:var(--pri);opacity:.8}
/* 报错卡片的标题条跟着变红,避免「红标题配蓝竖条」的割裂 */
.card[style*="border-color:var(--err)"]>h3::before{background:var(--err)}
/* ---------- 5. 表格:斑马纹 + 表头浮起 + 悬停主色提示 ---------- */
tbody tr:nth-child(even){background:rgba(0,0,0,.014)}
tbody tr:hover{background:var(--pri-l)}
th{box-shadow:inset 0 -1px 0 var(--line)}
/* 首列不加粗但加深一点,长表格里横向找行更容易 */
tbody td:first-child{color:var(--txt)}
/* ---------- 6. 表单控件:统一手感 ---------- */
input:hover:not(:disabled),select:hover:not(:disabled),textarea:hover:not(:disabled){
border-color:#c3cfe3}
input:disabled,select:disabled,textarea:disabled{background:#f5f7fb;color:var(--txt3);
cursor:not-allowed}
/* 多选框 / 单选框跟随主题主色(默认是系统蓝,与主题不搭) */
input[type=checkbox],input[type=radio]{accent-color:var(--pri);width:auto}
button{transition:var(--tr)}
button:not(:disabled):hover{transform:translateY(-1px)}
button:not(:disabled):active{transform:translateY(1px)}
/* small 按钮与 icon-btn 不参与位移,避免密集工具条里整体抖动 */
button.small:not(:disabled):hover,button.icon-btn:not(:disabled):hover,
.seg button:not(:disabled):hover,.lang-tabs button:not(:disabled):hover{transform:none}
/* ---------- 7. 可点击卡片:悬停抬升 ---------- */
.tm-card:hover,.st-card:hover{box-shadow:var(--sh-hov)}
/* ---------- 8. 空状态:统一「这里什么都没有」的观感 ---------- */
.empty-state{padding:34px 18px;text-align:center;color:var(--txt3);font-size:13px;
line-height:1.8;border:1px dashed var(--line);border-radius:var(--r);
background:rgba(0,0,0,.012)}
.empty-state b{display:block;color:var(--txt2);font-size:13.5px;margin-bottom:4px}
/* ---------- 9. 徽标统一 ---------- */
/* .tag-ok / .tag-warn 是历史遗留的独立类,与 .tag.ok / .tag.warn 重复。
这里补齐它们缺的垂直对齐与圆角,避免同一行里两种徽标高度不一致。 */
.tag-ok,.tag-warn{vertical-align:middle;line-height:1.6}
.tag.pri{background:var(--pri-l);color:var(--pri)}
/* ---------- 10. 分区小标题(.sec-t)与分隔线(.hr) ---------- */
.sec-t{font-size:12px;font-weight:600;color:var(--pri);margin:2px 0 9px;
padding-bottom:5px;border-bottom:1px dashed var(--line2)}
.hr{height:1px;background:var(--line2);border:none;margin:14px 0}
/* 本页模块状态灯(#5):贴在页面标题右边,只在有对应模块的页面出现 */
.page-health{display:inline-flex;align-items:center;gap:7px;flex:0 0 auto;
padding:3px 10px;border-radius:20px;font-size:12px;color:var(--txt2);
background:rgba(0,0,0,.035);cursor:pointer;transition:var(--tr)}
.page-health[hidden]{display:none}
.page-health:hover{background:var(--pri-l);color:var(--pri)}
.page-health .ph-txt{white-space:nowrap}
@media (max-width:520px){.page-health .ph-txt{display:none}}