/* =========================================================
   AssetOps Pro 2.0 —— 主题系统
   ---------------------------------------------------------
   三态：跟随系统（默认）/ 浅色 / 深色
   - 深色 = 无 data-theme 时的默认值（即 :root）
   - 浅色 = html[data-theme="light"]
   - 跟随系统 = JS 依据 prefers-color-scheme 动态写入 data-theme
   JS 入口：static/js/theme.js（先于 CSS 生效，避免闪白）
   ========================================================= */

/* ---------- 深色（默认，也是 fallback） ---------- */
:root {
    --bg-0: #0D1117;
    --bg-1: #161B22;
    --bg-2: #21262D;
    --bg-3: #2D333B;
    --primary: #1F6FEB;
    --primary-2: #2F81F7;
    --accent: #58A6FF;
    --text: #E6EDF3;
    --muted: #9BA3AD;
    --white: #FFFFFF;
    --ok: #3FB950;
    --danger: #F85149;
    --warn: #D29922;
    --info: #58A6FF;
    --line: rgba(240, 246, 252, 0.10);
    --line-strong: rgba(240, 246, 252, 0.18);
    --radius: 12px;
    --radius-sm: 8px;
    --shadow: 0 8px 28px rgba(0, 0, 0, 0.45);
    /* 阴影分层令牌：卡片常态 / 悬浮抬升 / 强调弹出。
       深色用黑阴影，浅色换成带主题色相的灰蓝阴影 —— 此前这三级硬编码 rgba(0,0,0,…)
       在浅色主题下糊成一片灰（浅色打磨 2026-11-02）。 */
    --shadow-card: 0 4px 18px rgba(0, 0, 0, 0.28);
    --shadow-hover: 0 10px 26px rgba(0, 0, 0, 0.42);
    --shadow-pop: 0 12px 28px rgba(0, 0, 0, 0.42);
    /* 次级按钮悬停底色（深色=灰亮一档；浅色=淡灰压暗一档） */
    --btn-hover-bg: rgba(48, 54, 61, 0.9);
    /* 表格斑马纹（极淡，只辅助横向扫读，不抢 hover 高亮） */
    --row-alt: rgba(240, 246, 252, 0.024);
    /* 文字选中底色 */
    --sel-bg: rgba(31, 111, 235, 0.38);
    --sidebar-w: 216px;
    --topbar-h: 60px;
    /* 字体：西文/数字走 Segoe UI，中文落到微软雅黑 UI（比雅黑更紧凑、更轻） */
    --font: "Segoe UI Variable Text", "Segoe UI", "Microsoft YaHei UI", "Microsoft YaHei",
            "PingFang SC", "Hiragino Sans GB", "Helvetica Neue", Arial, sans-serif;

    /* 动效规范：统一缓动与时长，避免各处各写一套 */
    --ease-out: cubic-bezier(0.22, 0.61, 0.36, 1);
    --dur-fast: 0.16s;
    --dur-base: 0.22s;

    /* 语义色：随主题翻转的补充变量 */
    --on-accent: #FFFFFF;
    --hover-overlay: rgba(255, 255, 255, 0.06);
    --hover-overlay-2: rgba(255, 255, 255, 0.10);
    --grid-line: rgba(240, 246, 252, 0.028);
    --scroll-thumb: #30363D;
    --scroll-thumb-hover: #4a5260;
    --inp-bg: #1A2029;
    --code-bg: #0A0E13;
    --diff-line-bg: rgba(255, 255, 255, 0.02);
    --panel-a: rgba(13, 17, 23, 0.78);
    --panel-b: rgba(33, 38, 45, 0.70);
    --panel-c: rgba(22, 27, 34, 0.98);
    --body-grad: linear-gradient(180deg, #0D1117 0%, #0B0F14 100%);
    --elev-grad: linear-gradient(180deg, #0F141A, #0A0E13);
    /* 资产类型配色（深浅主题需不同亮度，保证对比度） */
    --tone-online-1: #CFE8FF;
    --tone-online-2: #C9D1D9;
    --tone-net: #B7F0C2;
    --tone-storage: #E3D5FF;
    --tone-err: #FFD7D4;
    --tone-warn-txt: #FFD28A;
    --tl-old: #FFB3AE;
    --tl-new: #A8E6B0;
}

/* ---------- 浅色 ---------- */
html[data-theme="light"] {
    --bg-0: #F6F8FB;
    --bg-1: #FFFFFF;
    --bg-2: #EEF2F7;
    --bg-3: #E2E8F0;
    --primary: #1F6FEB;
    --primary-2: #1A5FD0;
    --accent: #0B5CB8;
    --text: #1F2328;
    --muted: #5A626B;
    --white: #FFFFFF;
    --ok: #1A7F37;
    --danger: #CF222E;
    --warn: #9A6700;
    --info: #0969DA;
    --line: rgba(31, 35, 40, 0.12);
    --line-strong: rgba(31, 35, 40, 0.22);
    --shadow: 0 6px 22px rgba(31, 35, 40, 0.13);
    /* 浅色阴影分层：黑阴影 → 灰蓝调软阴影，去「灰蒙蒙」的关键 */
    --shadow-card: 0 2px 10px rgba(31, 35, 40, 0.07);
    --shadow-hover: 0 8px 22px rgba(31, 35, 40, 0.13);
    --shadow-pop: 0 10px 24px rgba(31, 35, 40, 0.12);
    --btn-hover-bg: rgba(31, 35, 40, 0.07);
    --row-alt: rgba(31, 35, 40, 0.022);
    --sel-bg: rgba(31, 111, 235, 0.20);

    --on-accent: #FFFFFF;
    --hover-overlay: rgba(31, 35, 40, 0.05);
    --hover-overlay-2: rgba(31, 35, 40, 0.09);
    --grid-line: rgba(31, 35, 40, 0.035);
    --scroll-thumb: #C4CCD6;
    --scroll-thumb-hover: #A8B3BF;
    --inp-bg: #FFFFFF;
    --code-bg: #F2F5F9;
    --diff-line-bg: rgba(31, 35, 40, 0.025);
    --panel-a: rgba(255, 255, 255, 0.86);
    --panel-b: rgba(255, 255, 255, 0.94);
    --panel-c: rgba(255, 255, 255, 0.99);
    --body-grad: linear-gradient(180deg, #F8FAFC 0%, #EDF1F6 100%);
    --elev-grad: linear-gradient(180deg, #FFFFFF, #F3F6FA);
    --tone-online-1: #0B4A8F;
    --tone-online-2: #35506B;
    --tone-net: #14612C;
    --tone-storage: #553098;
    --tone-err: #A3221F;
    --tone-warn-txt: #8A5A00;
    --tl-old: #A3221F;
    --tl-new: #14612C;
}

/* 跟随系统：由 JS 动态切换 data-theme，此块仅兜底（无 JS 时按系统） */
@media (prefers-color-scheme: light) {
    html:not([data-theme]) {
        color-scheme: light;
    }
}

* {
    box-sizing: border-box;
}

html, body {
    height: 100%;
}

html {
    color-scheme: dark;
}

html[data-theme="light"] {
    color-scheme: light;
}

body {
    margin: 0;
    font-family: var(--font);
    font-size: 14px;
    color: var(--text);
    background-color: var(--bg-0);
    background-image:
        radial-gradient(900px 520px at 12% -10%, rgba(31, 111, 235, 0.16), transparent 60%),
        radial-gradient(760px 460px at 100% 0%, rgba(88, 166, 255, 0.10), transparent 58%),
        var(--body-grad);
    background-attachment: fixed;
    -webkit-font-smoothing: antialiased;
}

html[data-theme="light"] body {
    background-image:
        radial-gradient(820px 480px at 10% -12%, rgba(31, 111, 235, 0.10), transparent 62%),
        radial-gradient(700px 420px at 100% 0%, rgba(88, 166, 255, 0.08), transparent 60%),
        var(--body-grad);
}

a {
    color: var(--accent);
    text-decoration: none;
}

a:hover {
    color: var(--primary-2);
}

::-webkit-scrollbar {
    width: 10px;
    height: 10px;
}

::-webkit-scrollbar-track {
    background: transparent;
}

::-webkit-scrollbar-thumb {
    background: var(--scroll-thumb);
    border-radius: 6px;
    border: 2px solid transparent;
    background-clip: padding-box;
}

::-webkit-scrollbar-thumb:hover {
    background: var(--scroll-thumb-hover);
    background-clip: padding-box;
}

/* ============ 登录视图 ============ */
#loginView {
    position: fixed;
    inset: 0;
    z-index: 2000;
    display: flex;
    align-items: center;
    justify-content: center;
    background: radial-gradient(700px 500px at 50% 20%, rgba(31, 111, 235, 0.20), transparent 65%), var(--bg-0);
}

.login-card {
    position: relative;
    width: 380px;
    max-width: calc(100vw - 40px);
    padding: 34px 30px 28px;
    border-radius: 18px;
    background: linear-gradient(160deg, var(--panel-b), var(--panel-a));
    border: 1px solid var(--line-strong);
    box-shadow: var(--shadow), 0 0 0 1px rgba(88, 166, 255, 0.08);
    backdrop-filter: blur(14px);
    animation: popIn 0.35s ease both;
}

.login-card::after {
    content: "";
    position: absolute;
    inset: -1px;
    border-radius: 18px;
    padding: 1px;
    background: linear-gradient(120deg, rgba(31, 111, 235, 0.65), rgba(88, 166, 255, 0.15), transparent 70%);
    -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    -webkit-mask-composite: xor;
    mask-composite: exclude;
    pointer-events: none;
}

.login-logo {
    width: 54px;
    height: 54px;
    border-radius: 14px;
    display: grid;
    place-items: center;
    font-size: 24px;
    color: var(--white);
    background: linear-gradient(135deg, var(--primary), var(--accent));
    box-shadow: 0 8px 24px rgba(31, 111, 235, 0.45);
    margin-bottom: 16px;
}

/* ============ 布局 ============ */
.app-shell {
    position: relative;
    z-index: 1;
    display: none;
}

.app-shell.ready {
    display: block;
}

.topbar {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    height: var(--topbar-h);
    display: flex;
    align-items: center;
    gap: 18px;
    padding: 0 20px;
    background: var(--bg-0);
    border-bottom: 1px solid var(--line);
    z-index: 1000;
}

.topbar::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: -1px;
    height: 1px;
    background: linear-gradient(90deg, transparent, rgba(88, 166, 255, 0.55), transparent);
    pointer-events: none;   /* 1px 装饰线横跨整条顶栏，别挡住底下按钮 */
}

.brand {
    display: flex;
    align-items: center;
    gap: 11px;
    white-space: nowrap;
}

.brand-mark {
    width: 34px;
    height: 34px;
    border-radius: 10px;
    display: grid;
    place-items: center;
    color: #fff;
    font-size: 16px;
    background: linear-gradient(135deg, var(--primary), var(--accent));
    box-shadow: 0 6px 18px rgba(31, 111, 235, 0.45);
}

.brand-title {
    font-size: 16px;
    font-weight: 700;
    letter-spacing: 0.3px;
    line-height: 1.1;
}

.brand-sub {
    font-size: 11px;
    color: var(--muted);
}

.global-search {
    position: relative;
    flex: 1;
    max-width: 460px;
    margin-left: 12px;
}

.global-search input {
    width: 100%;
    height: 38px;
    padding: 0 14px 0 38px;
    border-radius: 10px;
    border: 1px solid var(--line);
    background: var(--panel-b);
    color: var(--text);
    font-size: 13px;
    outline: none;
    transition: border-color 0.2s, box-shadow 0.2s;
}

.global-search input:focus {
    border-color: rgba(88, 166, 255, 0.6);
    box-shadow: 0 0 0 3px rgba(31, 111, 235, 0.18);
}

.global-search i {
    position: absolute;
    left: 13px;
    top: 50%;
    transform: translateY(-50%);
    color: var(--muted);
    font-size: 13px;
}

.search-panel {
    position: absolute;
    top: 46px;
    left: 0;
    right: 0;
    max-height: 460px;
    overflow: auto;
    border-radius: 12px;
    border: 1px solid var(--line-strong);
    background: var(--panel-c);
    backdrop-filter: blur(12px);
    box-shadow: var(--shadow);
    padding: 8px;
    display: none;
    z-index: 1600;
}

.search-panel.show {
    display: block;
    animation: fadeSlide 0.18s ease both;
}

.search-group-title {
    font-size: 11px;
    color: var(--muted);
    text-transform: uppercase;
    letter-spacing: 0.6px;
    padding: 8px 10px 4px;
}

.search-item {
    padding: 8px 10px;
    border-radius: 8px;
    cursor: pointer;
    transition: background 0.15s;
}

.search-item:hover {
    background: rgba(88, 166, 255, 0.10);
}

.search-item .t {
    font-size: 13px;
    color: var(--text);
}

.search-item .s {
    font-size: 11px;
    color: var(--muted);
}

.topbar-right {
    margin-left: auto;
    display: flex;
    align-items: center;
    gap: 10px;
}

.user-chip {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 6px 12px;
    border-radius: 999px;
    border: 1px solid var(--line);
    background: var(--panel-b);
    font-size: 12px;
    color: var(--text);
}

.avatar {
    width: 22px;
    height: 22px;
    border-radius: 50%;
    display: grid;
    place-items: center;
    font-size: 11px;
    color: #fff;
    background: linear-gradient(135deg, #1F6FEB, #58A6FF);
}

/* 侧栏 */
.sidebar {
    position: fixed;
    top: var(--topbar-h);
    bottom: 0;
    left: 0;
    width: var(--sidebar-w);
    padding: 14px 10px;
    background: var(--bg-0);
    border-right: 1px solid var(--line);
    overflow-y: auto;
    z-index: 900;
    transition: transform 0.24s ease;
}

.nav-item {
    position: relative;
    display: flex;
    align-items: center;
    gap: 11px;
    padding: 10px 12px;
    margin-bottom: 4px;
    border-radius: 10px;
    color: var(--muted);
    font-size: 13.5px;
    cursor: pointer;
    user-select: none;
    transition: background 0.18s, color 0.18s, transform 0.18s;
}

.nav-item i {
    width: 18px;
    text-align: center;
    font-size: 14px;
}

.nav-item:hover {
    background: rgba(88, 166, 255, 0.08);
    color: var(--text);
    transform: translateX(2px);
}

.nav-item.active {
    color: var(--white);
    background: linear-gradient(90deg, rgba(31, 111, 235, 0.22), rgba(31, 111, 235, 0.05));
}

.nav-item.active::before {
    content: "";
    position: absolute;
    left: 0;
    top: 8px;
    bottom: 8px;
    width: 3px;
    border-radius: 3px;
    background: linear-gradient(180deg, var(--accent), var(--primary));
    box-shadow: 0 0 12px rgba(88, 166, 255, 0.85);
    pointer-events: none;   /* 纯装饰，不吃导航项左缘的点击 */
}

.nav-badge {
    margin-left: auto;
    font-size: 11px;
    padding: 1px 7px;
    border-radius: 999px;
    background: rgba(88, 166, 255, 0.16);
    color: var(--accent);
}

.sidebar-foot {
    margin-top: 16px;
    padding: 12px;
    border-radius: 10px;
    border: 1px dashed var(--line);
    font-size: 11px;
    color: var(--muted);
    line-height: 1.7;
}

/* 主内容 */
.main {
    margin-left: var(--sidebar-w);
    padding: calc(var(--topbar-h) + 18px) 22px 40px;
    min-height: 100vh;
    position: relative;
    z-index: 1;
}

.view {
    display: none;
}

.view.active {
    display: block;
    /* 只做透明度淡入：整页位移会触发全屏重绘，切长页面时最明显的"卡"就来自这里。
       不能填 both：fadeIn 结束时本就在 opacity:1，加不加填充视觉完全一样；但 both 会让整个
       视图子树（最多 858 节点）被 Firefox 永久当作「有填充动画的元素」处理、每次切页重新
       栅格化整块（实测这一项占掉帧的 38%）。 */
    animation: fadeIn var(--dur-base) var(--ease-out);
}

.page-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    margin-bottom: 12px;
    flex-wrap: wrap;
}
/* 页头左侧：标题 + 行内副标题（紧凑化：副标题不再独占一行） */
.page-head > div:first-child:not(:only-child) {
    display: flex;
    align-items: baseline;
    gap: 10px;
    min-width: 0;
    flex-wrap: wrap;
}
.page-head .page-desc {
    margin-top: 0;
    padding-left: 10px;
    border-left: 1px solid var(--line-strong);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.page-title {
    font-size: 17px;
    font-weight: 700;
    margin: 0;
    letter-spacing: 0.2px;
    white-space: nowrap;
}

.page-desc {
    font-size: 12px;
    color: var(--muted);
    margin-top: 4px;
}

/* ============ 卡片 ============ */
.card-x {
    position: relative;
    border-radius: var(--radius);
    border: 1px solid var(--line);
    background: linear-gradient(165deg, var(--panel-b), var(--panel-c));
    box-shadow: var(--shadow-card);
    padding: 16px;
    transition: border-color 0.2s, transform 0.2s, box-shadow 0.2s;
}

.card-x.hoverable:hover {
    border-color: rgba(88, 166, 255, 0.42);
    transform: translateY(-2px);
    box-shadow: var(--shadow-hover), 0 0 0 1px rgba(88, 166, 255, 0.10);
}

.card-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: 12px;
}

.card-title {
    font-size: 15px;
    font-weight: 600;
    display: flex;
    align-items: center;
    gap: 8px;
}

.card-title i {
    color: var(--accent);
}

/* 统计卡片 */
.stat-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(212px, 1fr));
    gap: 14px;
    margin-bottom: 16px;
}

.stat-card {
    position: relative;
    overflow: hidden;
    border-radius: var(--radius);
    border: 1px solid var(--line);
    background: linear-gradient(150deg, var(--panel-b), var(--panel-a));
    padding: 16px 16px 14px;
    transition: transform 0.22s, border-color 0.22s, box-shadow 0.22s;
}

.stat-card::before {
    content: "";
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    width: 3px;
    background: linear-gradient(180deg, var(--c1, #1F6FEB), var(--c2, #58A6FF));
    box-shadow: 0 0 16px var(--c2, #58A6FF);
    pointer-events: none;   /* 同上：纯装饰，不参与命中 */
}

.stat-card:hover {
    transform: translateY(-3px);
    border-color: rgba(88, 166, 255, 0.4);
    box-shadow: var(--shadow-pop);
}

.stat-label {
    font-size: 12.5px;
    color: var(--muted);
    display: flex;
    align-items: center;
    gap: 7px;
}

.stat-value {
    font-family: "JetBrains Mono", "Cascadia Mono", Consolas, monospace;
    font-size: 30px;
    font-weight: 700;
    line-height: 1.25;
    margin-top: 6px;
    letter-spacing: -0.5px;
}

.stat-sub {
    font-size: 11.5px;
    color: var(--muted);
    margin-top: 2px;
}

/* ============ 工具栏 / 表单控件 ============ */
.toolbar {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
    margin-bottom: 10px;
}

.toolbar .spacer {
    flex: 1;
}

.inp,
.sel {
    height: 30px;
    padding: 0 11px;
    border-radius: 8px;
    border: 1px solid var(--line-strong);
    background: var(--panel-a);
    color: var(--text);
    font-size: 12.5px;
    outline: none;
    transition: border-color 0.18s, box-shadow 0.18s;
    font-family: var(--font);
}

textarea.inp {
    height: auto;
    min-height: 92px;
    padding: 10px 12px;
    line-height: 1.6;
    resize: vertical;
    width: 100%;
}

.inp:focus,
.sel:focus {
    border-color: rgba(88, 166, 255, 0.65);
    box-shadow: 0 0 0 3px rgba(31, 111, 235, 0.16);
}

.inp::placeholder {
    color: #6E7681;
}

.inp-search {
    padding-left: 32px;
    width: 240px;
}

.search-wrap {
    position: relative;
    display: inline-flex;
}

.search-wrap i {
    position: absolute;
    left: 11px;
    top: 50%;
    transform: translateY(-50%);
    color: var(--muted);
    font-size: 12px;
    pointer-events: none;
}

.btn-x {
    height: 30px;
    padding: 0 12px;
    border-radius: 8px;
    border: 1px solid var(--line-strong);
    background: var(--panel-b);
    color: var(--text);
    font-size: 12.5px;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    white-space: nowrap;
    transition: background-color 0.18s var(--ease-out), border-color 0.18s var(--ease-out), color 0.18s var(--ease-out), box-shadow 0.18s var(--ease-out), transform 0.18s var(--ease-out);
    font-family: var(--font);
}

.btn-x:hover {
    background: var(--btn-hover-bg);
    border-color: rgba(88, 166, 255, 0.45);
    transform: translateY(-1px);
}

.btn-x:active {
    transform: translateY(0);
}

.btn-primary-x {
    background: linear-gradient(135deg, var(--primary), #1A5FD0);
    border-color: transparent;
    color: #fff;
    box-shadow: 0 6px 16px rgba(31, 111, 235, 0.35);
}

.btn-primary-x:hover {
    background: linear-gradient(135deg, #2472E8, #1A5FD0);
    box-shadow: 0 8px 22px rgba(31, 111, 235, 0.5);
}

.btn-danger-x {
    background: rgba(248, 81, 73, 0.14);
    border-color: rgba(248, 81, 73, 0.45);
    color: #FF9A94;
}

.btn-danger-x:hover {
    background: rgba(248, 81, 73, 0.26);
}

.btn-ok-x {
    background: rgba(63, 185, 80, 0.14);
    border-color: rgba(63, 185, 80, 0.45);
    color: #7EE787;
}

.btn-ok-x:hover {
    background: rgba(63, 185, 80, 0.26);
}

.btn-xs {
    height: 28px;
    padding: 0 10px;
    font-size: 12px;
    border-radius: 7px;
}

.btn-icon {
    width: 30px;
    height: 30px;
    padding: 0;
    justify-content: center;
    border-radius: 8px;
}

/* 幽灵按钮：用于「高级筛选」等可展开/收起的切换入口 */
.btn-ghost {
    border-style: dashed;
}
.btn-ghost.active {
    border-color: var(--primary);
    color: var(--primary);
    background: rgba(31, 111, 235, 0.08);
}

/* 高级筛选折叠面板 */
.adv-panel {
    display: none;
    gap: 8px;
    flex-wrap: wrap;
    align-items: center;
    padding: 10px 12px;
    margin-bottom: 10px;
    border: 1px dashed var(--line-strong);
    border-radius: 9px;
    background: var(--panel-a);
}
.adv-panel.show {
    display: flex;
    animation: fadeIn var(--dur-base) var(--ease-out);
}

/* ============ 表格 ============ */
.table-wrap {
    border-radius: var(--radius);
    border: 1px solid var(--line);
    overflow: hidden;
    background: var(--panel-a);
}

.table-scroll {
    overflow: auto;
    max-height: calc(100vh - 288px);
}

table.data-grid {
    width: 100%;
    border-collapse: separate;
    border-spacing: 0;
    font-size: 13px;
    white-space: nowrap;
}

table.data-grid thead th {
    position: sticky;
    top: 0;
    z-index: 3;
    background: var(--inp-bg);
    color: var(--muted);
    font-weight: 600;
    font-size: 12px;
    text-align: left;
    padding: 10px 12px;
    border-bottom: 1px solid var(--line-strong);
    letter-spacing: 0.3px;
}

table.data-grid thead th.sortable {
    cursor: pointer;
    user-select: none;
}

table.data-grid thead th.sortable:hover {
    color: var(--accent);
}

table.data-grid tbody td {
    padding: 9px 12px;
    border-bottom: 1px solid var(--line);
    color: var(--text);
    vertical-align: middle;
}

table.data-grid tbody tr {
    transition: background 0.15s;
}

/* 斑马纹：极淡一行隔一行，辅助 200~300 台资产的宽表横向扫读。
   ⚠️ 必须放在 :hover 规则之前 —— 两者特异性相同，后来者居上，hover 才能盖过斑马纹。 */
table.data-grid tbody tr:nth-child(even) {
    background: var(--row-alt);
}

table.data-grid tbody tr:hover {
    background: rgba(88, 166, 255, 0.06);
}

table.data-grid tbody tr.row-deleted {
    opacity: 0.55;
}

.mono {
    font-family: "JetBrains Mono", "Cascadia Mono", Consolas, monospace;
    font-size: 12.5px;
}

.muted {
    color: var(--muted);
}

.empty-state {
    padding: 46px 20px;
    text-align: center;
    color: var(--muted);
}

.empty-state i {
    font-size: 30px;
    display: block;
    margin-bottom: 12px;
    opacity: 0.5;
}

/* 徽标 */
.badge-x {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 2px 9px;
    border-radius: 999px;
    font-size: 11.5px;
    line-height: 1.7;
    border: 1px solid transparent;
}

.badge-ok {
    background: rgba(63, 185, 80, 0.14);
    color: #7EE787;
    border-color: rgba(63, 185, 80, 0.35);
}

.badge-off {
    background: rgba(139, 148, 158, 0.14);
    color: var(--muted);
    border-color: rgba(139, 148, 158, 0.3);
}

.badge-warn {
    background: rgba(210, 153, 34, 0.15);
    color: #E3B341;
    border-color: rgba(210, 153, 34, 0.38);
}

.badge-danger {
    background: rgba(248, 81, 73, 0.14);
    color: #FF9A94;
    border-color: rgba(248, 81, 73, 0.38);
}

.badge-info {
    background: rgba(88, 166, 255, 0.14);
    color: var(--accent);
    border-color: rgba(88, 166, 255, 0.38);
}

.badge-neon {
    background: linear-gradient(135deg, rgba(31, 111, 235, 0.28), rgba(88, 166, 255, 0.16));
    color: #A5D6FF;
    border-color: rgba(88, 166, 255, 0.5);
}

/* 分页 */
.pager {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 5px 10px;
    border-top: 1px solid var(--line);
    background: var(--panel-c);
    font-size: 12px;
    color: var(--muted);
    flex-wrap: wrap;
}

/* App.pagerHtml() 自带一层 .pager，而页面容器（#assetPager 等）也带 .pager 类，
   嵌套会叠加双份 padding/border/底色 —— 在内层中和掉 */
.pager .pager {
    padding: 0;
    border-top: 0;
    background: transparent;
}

.pager .btn-xs {
    height: 24px;
    padding: 0 9px;
    border-radius: 6px;
}

.pager .spacer {
    flex: 1;
}

/* 标签页 */
.tabs-x {
    display: inline-flex;
    gap: 3px;
    padding: 3px;
    border-radius: 9px;
    border: 1px solid var(--line);
    background: var(--panel-a);
}

/* 独立成块的标签页（如系统页）保留下边距；放进工具栏行的（台账页）由 toolbar 控制间距 */
.tabs-x.tabs-standalone {
    margin-bottom: 10px;
}

/* 工单状态页签：独立成块，tab 内带计数徽标 */
.tabs-x.tk-tabs {
    margin-bottom: 10px;
}
.tk-tab-cnt {
    display: inline-block;
    margin-left: 5px;
    padding: 0 6px;
    border-radius: 8px;
    font-size: 11px;
    line-height: 17px;
    background: var(--hover-overlay);
    color: var(--muted);
}
.tab-x.active .tk-tab-cnt {
    background: rgba(255, 255, 255, 0.96);
    color: #1A5FD0;
}
/* 「全部」总管页签：加底色背景，突出它是「看全部」的总入口（区别于四个状态页签） */
.tab-x.tk-tab-all {
    background: rgba(31, 111, 235, 0.12);
    color: var(--accent);
    font-weight: 600;
}
.tab-x.tk-tab-all:hover {
    background: rgba(31, 111, 235, 0.18);
    color: var(--accent);
}
.tab-x.tk-tab-all .tk-tab-cnt {
    background: rgba(31, 111, 235, 0.14);
    color: var(--accent);
}
/* 选中「全部」时仍是主题色实心（active 优先级更高，覆盖上面的浅色底） */
.tab-x.tk-tab-all.active {
    background: linear-gradient(135deg, var(--primary), #1A5FD0);
    color: #fff;
}
.tab-x.tk-tab-all.active .tk-tab-cnt {
    background: rgba(255, 255, 255, 0.96);
    color: #1A5FD0;
}

/* 申请日期搁置时长着色（仅未解决工单生效）：7 / 15 / 30 天三档 */
.tk-age-7  { color: var(--warn); font-weight: 600; }
.tk-age-15 { color: #e8871a; font-weight: 600; }
.tk-age-30 { color: var(--danger); font-weight: 700; }
.tk-age-days {
    display: inline-block;
    margin-left: 6px;
    padding: 0 6px;
    border-radius: 8px;
    font-size: 11px;
    line-height: 17px;
    color: #fff;
    background: var(--danger);
}
/* 徽标颜色跟随档位（与日期文字同色系） */
.tk-age-7 .tk-age-days  { background: var(--warn); }
.tk-age-15 .tk-age-days { background: #e8871a; }
.tk-age-30 .tk-age-days { background: var(--danger); }

.tab-x {
    padding: 5px 13px;
    border-radius: 6px;
    font-size: 12.5px;
    color: var(--muted);
    cursor: pointer;
    transition: background-color 0.18s var(--ease-out), border-color 0.18s var(--ease-out), color 0.18s var(--ease-out), box-shadow 0.18s var(--ease-out), transform 0.18s var(--ease-out);
    user-select: none;
    white-space: nowrap;
}

.tab-x:hover {
    color: var(--text);
}

.tab-x.active {
    color: #fff;
    background: linear-gradient(135deg, var(--primary), #1A5FD0);
    box-shadow: 0 3px 10px rgba(31, 111, 235, 0.32);
}

/* ============ 弹层 ============ */
.modal-mask {
    position: fixed;
    inset: 0;
    z-index: 1800;
    display: none;
    align-items: flex-start;
    justify-content: center;
    padding: 40px 20px;
    overflow: auto;
    background: rgba(1, 4, 9, 0.68);
    backdrop-filter: blur(6px);
}

.modal-mask.show {
    display: flex;
    animation: fadeIn 0.16s ease both;
}

/* 弹层宿主：自身不绘制任何东西，只负责整体显隐 */
.modal-host {
    display: none;
}

.modal-host.show {
    display: block;
}

/* 被压在下层的弹层：上层的遮罩已负责压暗，这里只把卡片"往后推"一点做出层次 */
.modal-mask.under {
    background: transparent;
    backdrop-filter: none;
}

.modal-mask.under > .modal-box {
    animation: none;
    transform: scale(0.97) translateY(-6px);
}

.modal-box {
    width: 100%;
    max-width: 720px;
    border-radius: 14px;
    border: 1px solid var(--line-strong);
    background: linear-gradient(165deg, var(--panel-b), var(--panel-a));
    box-shadow: var(--shadow);
    animation: popIn var(--dur-base) var(--ease-out) both;
}

.modal-box.wide {
    max-width: 1080px;
}

.modal-box.narrow {
    max-width: 460px;
}

.modal-head {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 15px 18px;
    border-bottom: 1px solid var(--line);
}

.modal-head h5 {
    margin: 0;
    font-size: 15.5px;
    font-weight: 600;
}

.modal-head .close-x {
    margin-left: auto;
    width: 30px;
    height: 30px;
    border-radius: 8px;
    border: 1px solid var(--line);
    background: transparent;
    color: var(--muted);
    cursor: pointer;
    transition: background-color 0.18s var(--ease-out), border-color 0.18s var(--ease-out), color 0.18s var(--ease-out), box-shadow 0.18s var(--ease-out), transform 0.18s var(--ease-out);
}

.modal-head .close-x:hover {
    color: #fff;
    border-color: rgba(248, 81, 73, 0.5);
    background: rgba(248, 81, 73, 0.12);
}

.modal-body {
    padding: 18px;
    max-height: calc(100vh - 240px);
    overflow: auto;
}

.modal-foot {
    display: flex;
    justify-content: flex-end;
    gap: 10px;
    padding: 14px 18px;
    border-top: 1px solid var(--line);
    background: var(--panel-a);
}

/* 表单网格 */
.form-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 13px 16px;
}

.form-grid .full {
    grid-column: 1 / -1;
}

.field {
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.field label {
    font-size: 12.5px;
    color: var(--muted);
}

.field label .req {
    color: var(--danger);
    margin-left: 3px;
}

.field .field-hint {
    font-size: 11.5px;
    color: var(--muted);
    line-height: 1.4;
}

.field .inp,
.field .sel {
    width: 100%;
}

.form-section-title {
    grid-column: 1 / -1;
    font-size: 12px;
    color: var(--accent);
    letter-spacing: 0.5px;
    padding-top: 6px;
    border-top: 1px dashed var(--line);
    margin-top: 4px;
}

.form-section-title:first-child {
    border-top: none;
    padding-top: 0;
    margin-top: 0;
}

/* ============ 机柜立面图 ============ */
.rack-layout {
    display: grid;
    grid-template-columns: minmax(320px, 1fr) 320px;
    gap: 16px;
    align-items: start;
}

.rack-frame {
    border-radius: var(--radius);
    border: 1px solid var(--line-strong);
    background: var(--elev-grad);
    padding: 10px;
    /* 柜体内凹阴影按主题切换：深色的"黑洞感"是刻意为之，但同一块 40px 黑阴影压在
       浅色主题的白卡片上就是一坨灰雾（2026-11-03 修复，--rk-well 在 light 覆盖） */
    box-shadow: var(--rk-well, inset 0 0 40px rgba(0, 0, 0, 0.6));
}

.rack-rows {
    display: flex;
    flex-direction: column;
    gap: var(--rk-gap);
    /* 行高/行距是常量，供"多 U 合并块"与"幽灵高亮框"做几何计算（与 JS 里同步） */
    --rk-row-h: 20px;
    --rk-gap: 2px;
    --rk-step: 22px;
    position: relative;
    /* 设备色板 v2（2026-11-03 精修）：色相保持语义不变（蓝=在线 / 绿=网络 / 紫=存储 /
       灰=离线 / 橙=其他），整体压深一档、边框提亮 —— 旧版半透明渐变 + 粉彩字在两种
       主题下都发"飘"，浅色主题下粉彩字直接糊掉。白字对比度均 ≥6:1。
       .rack-dev-block 与遗留的 .rack-slot.filled 共用这一套。 */
    --rk-on-1: #2563EB;  --rk-on-2: #1D4FD7;  --rk-on-bd: #60A5FA;   /* 在线服务器 */
    --rk-net-1: #15803D; --rk-net-2: #166534; --rk-net-bd: #4ADE80;  /* 网络设备 */
    --rk-sto-1: #7C3AED; --rk-sto-2: #6D28D9; --rk-sto-bd: #A78BFA;  /* 存储设备 */
    --rk-off-1: #4B5563; --rk-off-2: #374151; --rk-off-bd: #6B7280;  /* 离线 */
    --rk-pln-1: #B45309; --rk-pln-2: #92400E; --rk-pln-bd: #F59E0B;  /* 其他类型 */
    --rk-cf-1: #DC2626;  --rk-cf-2: #B91C1C;  --rk-cf-bd: #F87171;   /* U 位冲突 */
}

.rack-row {
    display: grid;
    grid-template-columns: 34px 1fr;
    gap: 6px;
    align-items: stretch;
    height: var(--rk-row-h);
    position: relative;
}

/* ---- v16-②：多柜并排（2K 宽屏同机房一排展示） ---- */
/* v18.1：恒一排不换行，放不下出横向滚动条（配合滚轮横滑，见 views-racks.js）；
   ⚠️ 不能加 scroll-snap：会把滚轮横滑的中间位置弹回吸附点（探针实测设100弹回0） */
.rack-multi {
    display: flex;
    flex-wrap: nowrap;
    gap: 14px;
    align-items: flex-start;
    overflow-x: auto;
    overflow-y: hidden;
    padding-bottom: 8px;          /* 给滚动条留位，避免压住柜体阴影 */
}
.rack-unit {
    flex: 0 0 auto;               /* 不压缩：多柜恒 240px 宽，放不下就横向滚 */
    border: 1px solid var(--line-strong);
    border-radius: var(--radius);
    background: var(--elev-grad);
    padding: 8px;
    box-shadow: var(--rk-well-unit, inset 0 0 26px rgba(0, 0, 0, 0.45));
    transition: border-color 0.15s var(--ease-out);
    cursor: pointer;              /* 整个柜体可点选（设备块除外，其自身有详情/拖拽） */
}
.rack-unit:hover { border-color: var(--primary); }
.rack-unit-on { border-color: var(--primary); box-shadow: 0 0 0 1px var(--primary), var(--rk-well-unit, inset 0 0 26px rgba(0,0,0,0.45)); }
.rack-unit-head {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 2px 4px 6px;
    cursor: pointer;
    flex-wrap: wrap;
}
.rack-unit-name { font-weight: 600; font-size: 13px; }
.rack-unit-u { font-size: 11.5px; color: var(--muted); }
.rack-unit-free { font-size: 11px; padding: 0 4px 6px; }
.rack-rows-multi { width: 240px; }
.rack-rows-multi .rack-row { grid-template-columns: 26px 1fr; }
/* 多柜视图对齐修正（2026-11-03）：.rack-dev-block / .rack-ghost 的 left 是按单柜的
   「34px U 列 + 6px gap = 40px」写死的，多柜 U 列只有 26px（+6px gap = 32px）——
   不覆盖的话色块整体右移 8px，和空槽、U 位刻度对不齐。 */
.rack-rows-multi .rack-dev-block,
.rack-rows-multi .rack-ghost { left: 32px; }
/* 240px 窄柜里刻度与文字各升半档，弥补宽度损失（单柜不受影响） */
.rack-rows-multi .rack-u { font-size: 11px; }
.rack-rows-multi .rack-dev-block .rdb-name { font-size: 12px; }
.rack-rows-multi .rack-dev-block .rdb-meta { font-size: 10.5px; }

.rack-u {
    font-size: 10.5px;
    color: var(--muted);
    display: grid;
    place-items: center;
    font-family: "JetBrains Mono", Consolas, monospace;
    border-right: 1px solid var(--line);
}

.rack-slot {
    border-radius: 5px;
    /* 空槽可见度 v2（2026-11-03）：原 dashed --line + 2% 底在两种主题下都淡到"网格消失"，
       占用块像飘在柜子里。提亮到仍克制、但能一眼读出 U 位网格的程度。 */
    border: 1px dashed rgba(240, 246, 252, 0.16);
    background: rgba(240, 246, 252, 0.032);
    transition: background 0.16s, border-color 0.16s;
}

.rack-slot.filled {
    border: 1px solid transparent;
    cursor: pointer;
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 3px 9px;
    font-size: 11.5px;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

.rack-slot.filled:hover {
    transform: translateX(2px);
    filter: brightness(1.18);
}

/* 以下四个色调类是历史遗留类名（当前立面渲染走 .rack-dev-block，这里已无生产者），
   保留并与 v2 色板对齐 —— 否则将来任何地方再用到它们，又是半透明渐变 + 粉彩字那套
   （浅色主题下糊掉的根源）。 */
.rack-slot.online {
    background: linear-gradient(180deg, var(--rk-on-1), var(--rk-on-2));
    border-color: var(--rk-on-bd);
    color: #FFFFFF;
}

.rack-slot.offline {
    background: linear-gradient(180deg, var(--rk-off-1), var(--rk-off-2));
    border-color: var(--rk-off-bd);
    color: #E5E7EB;
}

.rack-slot.network {
    background: linear-gradient(180deg, var(--rk-net-1), var(--rk-net-2));
    border-color: var(--rk-net-bd);
    color: #FFFFFF;
}

.rack-slot.conflict {
    background: linear-gradient(180deg, var(--rk-cf-1), var(--rk-cf-2));
    border-color: var(--rk-cf-bd);
    color: #FFFFFF;
}

.rack-slot.cont {
    border-top: none;
    border-top-left-radius: 0;
    border-top-right-radius: 0;
}

.legend {
    display: flex;
    gap: 14px;
    flex-wrap: wrap;
    font-size: 12px;
    color: var(--muted);
    align-items: center;
}

.legend span.dot {
    display: inline-block;
    width: 10px;
    height: 10px;
    border-radius: 3px;
    margin-right: 6px;
    vertical-align: -1px;
}

.list-mini {
    display: flex;
    flex-direction: column;
    gap: 8px;
    max-height: 420px;
    overflow: auto;
}

.list-mini-item {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 9px 11px;
    border-radius: 9px;
    border: 1px solid var(--line);
    background: var(--panel-c);
    font-size: 12.5px;
    transition: background-color 0.18s var(--ease-out), border-color 0.18s var(--ease-out), color 0.18s var(--ease-out), box-shadow 0.18s var(--ease-out), transform 0.18s var(--ease-out);
}

.list-mini-item:hover {
    border-color: rgba(88, 166, 255, 0.4);
    background: rgba(88, 166, 255, 0.07);
}

.list-mini-item .nm {
    flex: 1;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* ============ Diff ============ */
.diff-stat {
    display: flex;
    gap: 14px;
    font-size: 12.5px;
    align-items: center;
    flex-wrap: wrap;
}

.diff-wrap {
    border-radius: var(--radius);
    border: 1px solid var(--line);
    overflow: auto;
    max-height: calc(100vh - 400px);
    background: var(--code-bg);
    font-family: "JetBrains Mono", "Cascadia Mono", Consolas, monospace;
    font-size: 12.5px;
}

.diff-row {
    display: grid;
    grid-template-columns: 52px 52px 1fr;
    gap: 0;
    align-items: stretch;
    border-bottom: 1px solid var(--line);
}

.diff-row .ln {
    text-align: right;
    padding: 2px 8px;
    color: #5A6472;
    background: var(--hover-overlay);
    user-select: none;
    font-size: 11.5px;
}

.diff-row .tx {
    padding: 2px 12px;
    white-space: pre-wrap;
    word-break: break-all;
    color: #C9D1D9;
}

.diff-row.add .tx {
    background: rgba(63, 185, 80, 0.15);
    color: #AFF5B9;
}

.diff-row.del .tx {
    background: rgba(248, 81, 73, 0.15);
    color: #FFC4C1;
}

.diff-row.ctx .tx {
    color: #8B949E;
}

.diff-row.add .ln {
    background: rgba(63, 185, 80, 0.12);
}

.diff-row.del .ln {
    background: rgba(248, 81, 73, 0.12);
}

/* 上传拖拽区 */
.drop-zone {
    border: 1.5px dashed var(--line-strong);
    border-radius: var(--radius);
    padding: 22px;
    text-align: center;
    color: var(--muted);
    cursor: pointer;
    transition: background-color 0.2s var(--ease-out), border-color 0.2s var(--ease-out), color 0.2s var(--ease-out), box-shadow 0.2s var(--ease-out), transform 0.2s var(--ease-out);
    font-size: 13px;
}

.drop-zone:hover,
.drop-zone.over {
    border-color: rgba(88, 166, 255, 0.7);
    background: rgba(88, 166, 255, 0.07);
    color: var(--text);
}

.drop-zone i {
    font-size: 24px;
    display: block;
    margin-bottom: 8px;
    color: var(--accent);
}

/* 工单看板 */
.board {
    display: grid;
    grid-template-columns: repeat(4, minmax(220px, 1fr));
    gap: 14px;
}

.board-col {
    border-radius: var(--radius);
    border: 1px solid var(--line);
    background: var(--panel-a);
    padding: 12px;
    min-height: 220px;
}

.board-col-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    font-size: 13px;
    font-weight: 600;
    padding-bottom: 10px;
    margin-bottom: 10px;
    border-bottom: 1px solid var(--line);
}

.ticket-card {
    border-radius: 10px;
    border: 1px solid var(--line);
    background: linear-gradient(160deg, var(--panel-b), var(--panel-c));
    padding: 10px 11px;
    margin-bottom: 9px;
    cursor: pointer;
    position: relative;
    overflow: hidden;
    transition: background-color 0.18s var(--ease-out), border-color 0.18s var(--ease-out), color 0.18s var(--ease-out), box-shadow 0.18s var(--ease-out), transform 0.18s var(--ease-out);
}

.ticket-card::before {
    content: "";
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    width: 3px;
    background: var(--sev, #8B949E);
    pointer-events: none;   /* 纯装饰，工单卡本身才是点击目标 */
}

.ticket-card:hover {
    transform: translateY(-2px);
    border-color: rgba(88, 166, 255, 0.45);
}

.ticket-card .tk-no {
    font-size: 11px;
    color: var(--accent);
    font-family: "JetBrains Mono", Consolas, monospace;
}

.ticket-card .tk-title {
    font-size: 13px;
    margin: 3px 0 6px;
    line-height: 1.45;
}

.ticket-card .tk-meta {
    font-size: 11px;
    color: var(--muted);
    display: flex;
    gap: 10px;
    flex-wrap: wrap;
}

/* 提醒横幅 */
.alert-banner {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 11px 14px;
    border-radius: 11px;
    border: 1px solid rgba(210, 153, 34, 0.4);
    background: linear-gradient(90deg, rgba(210, 153, 34, 0.14), rgba(210, 153, 34, 0.04));
    margin-bottom: 14px;
    font-size: 13px;
    animation: fadeSlide 0.3s ease both;
}

.alert-banner.info {
    border-color: rgba(88, 166, 255, 0.4);
    background: linear-gradient(90deg, rgba(88, 166, 255, 0.13), rgba(88, 166, 255, 0.03));
}

/* 通用「文字按钮」：卡片内的小动作（全部恢复 / 推送到手机 / 跳运维日志）。
   原来每个容器的 .act-link 各写一份，资产详情里要用时就没样式了（<a> 无 href
   会退化成黑字 + 文本光标），所以提到全局。 */
.act-link {
    color: var(--accent);
    cursor: pointer;
}

.act-link:hover {
    text-decoration: underline;
}

/* 三个级别各一色：err 红（已过期 / 容量见底）、warn 黄（在逼近）、info 蓝（常态提示）。
   .warn 与基础态同色，显式写出来是为了让「类名 = 级别」对得上，
   否则 JS 加个 warn 类、CSS 里查无此规则，读代码的人会以为漏了。 */
.alert-banner.warn {
    border-color: rgba(210, 153, 34, 0.4);
    background: linear-gradient(90deg, rgba(210, 153, 34, 0.14), rgba(210, 153, 34, 0.04));
}

.alert-banner .alert-ico {
    flex: 0 0 auto;
    font-size: 13px;
    opacity: 0.9;
}

.alert-banner.err .alert-ico { color: #F85149; }
.alert-banner.warn .alert-ico { color: #D29922; }
.alert-banner.info .alert-ico { color: #58A6FF; }

html[data-theme="light"] .alert-banner.err .alert-ico { color: #CF222E; }
html[data-theme="light"] .alert-banner.warn .alert-ico { color: #9A6700; }
html[data-theme="light"] .alert-banner.info .alert-ico { color: #0969DA; }

.alert-banner.ok {
    border-color: rgba(63, 185, 80, 0.4);
    background: linear-gradient(90deg, rgba(63, 185, 80, 0.14), rgba(63, 185, 80, 0.04));
}

.alert-banner.err {
    border-color: rgba(248, 81, 73, 0.4);
    background: linear-gradient(90deg, rgba(248, 81, 73, 0.14), rgba(248, 81, 73, 0.04));
}

.alert-banner .close-x {
    margin-left: auto;
    background: transparent;
    border: none;
    color: var(--muted);
    cursor: pointer;
    font-size: 14px;
}

.alert-banner .txt {
    line-height: 1.6;
}

.alert-banner .close-x:hover {
    color: var(--text);
}

/* 「已忽略的提醒」恢复入口：误关之后还能找回来 */
.alert-muted-bar {
    display: flex;
    align-items: center;
    gap: 8px;
    margin: -4px 0 14px;
    padding: 7px 13px;
    font-size: 12.5px;
    color: var(--muted);
    border: 1px dashed var(--line);
    border-radius: 10px;
    background: rgba(139, 148, 158, 0.05);
}

/* 提醒区底部工具条：推送到手机 / 自动推送说明 */
.alert-tools {
    display: flex;
    align-items: center;
    gap: 8px;
    margin: -4px 0 14px;
    padding: 0 2px;
    font-size: 12.5px;
}

/* 轻提示 */
#toastBox {
    position: fixed;
    right: 22px;
    bottom: 22px;
    z-index: 3000;
    display: flex;
    flex-direction: column;
    gap: 10px;
}

.toast-x {
    min-width: 240px;
    max-width: 380px;
    padding: 11px 14px;
    border-radius: 11px;
    border: 1px solid var(--line-strong);
    background: var(--panel-c);
    backdrop-filter: blur(10px);
    box-shadow: var(--shadow);
    font-size: 13px;
    display: flex;
    align-items: flex-start;
    gap: 9px;
    animation: slideIn 0.24s ease both;
}

.toast-x.ok {
    border-color: rgba(63, 185, 80, 0.5);
}

.toast-x.err {
    border-color: rgba(248, 81, 73, 0.55);
}

.toast-x.warn {
    border-color: rgba(210, 153, 34, 0.55);
}

.toast-x i {
    margin-top: 1px;
}

.toast-x.ok i {
    color: var(--ok);
}

.toast-x.err i {
    color: var(--danger);
}

.toast-x.warn i {
    color: var(--warn);
}

/* 进度条 */
.bar {
    height: 6px;
    border-radius: 999px;
    background: var(--hover-overlay-2);
    overflow: hidden;
    margin-top: 8px;
}

.bar > span {
    display: block;
    height: 100%;
    border-radius: 999px;
    background: linear-gradient(90deg, var(--primary), var(--accent));
    transition: width 0.5s ease;
}

.bar.warn > span {
    background: linear-gradient(90deg, #D29922, #E3B341);
}

.bar.danger > span {
    background: linear-gradient(90deg, #F85149, #FF9A94);
}

/* 键值展示 */
.kv {
    display: grid;
    grid-template-columns: 118px 1fr;
    gap: 8px 12px;
    font-size: 13px;
}

.kv .k {
    color: var(--muted);
    font-size: 12.5px;
}

.kv .v {
    word-break: break-all;
}

/* 时间线 */
.timeline {
    position: relative;
    padding-left: 20px;
}

.timeline::before {
    content: "";
    position: absolute;
    left: 5px;
    top: 4px;
    bottom: 4px;
    width: 1px;
    background: var(--line-strong);
    pointer-events: none;   /* 纯装饰竖线 */
}

.tl-item {
    position: relative;
    padding: 0 0 14px 4px;
    font-size: 12.5px;
}

.tl-item::before {
    content: "";
    position: absolute;
    left: -19px;
    top: 5px;
    width: 9px;
    height: 9px;
    border-radius: 50%;
    background: var(--primary);
    box-shadow: 0 0 0 3px rgba(31, 111, 235, 0.18);
    pointer-events: none;   /* 纯装饰圆点，不挡时间线里的链接 */
}

.tl-time {
    color: var(--muted);
    font-size: 11.5px;
}

.tl-text {
    margin-top: 2px;
    line-height: 1.6;
}

/* 分组标题 */
.group-title {
    font-size: 12.5px;
    color: var(--muted);
    letter-spacing: 0.5px;
    margin: 18px 0 10px;
    padding-bottom: 7px;
    border-bottom: 1px solid var(--line);
    display: flex;
    align-items: center;
    gap: 8px;
}

.group-title:first-child {
    margin-top: 0;
}

/* 骨架/加载 */
.loading {
    padding: 26px;
    text-align: center;
    color: var(--muted);
    font-size: 13px;
}

.spin {
    display: inline-block;
    width: 15px;
    height: 15px;
    border: 2px solid rgba(88, 166, 255, 0.25);
    border-top-color: var(--accent);
    border-radius: 50%;
    animation: spin 0.7s linear infinite;
    vertical-align: -2px;
    margin-right: 8px;
}

/* 动画 */
@keyframes spin {
    to { transform: rotate(360deg); }
}

@keyframes fadeIn {
    from { opacity: 0; }
    to { opacity: 1; }
}

@keyframes fadeSlide {
    from { opacity: 0; transform: translateY(8px); }
    to { opacity: 1; transform: translateY(0); }
}

@keyframes popIn {
    from { opacity: 0; transform: scale(0.96) translateY(10px); }
    to { opacity: 1; transform: scale(1) translateY(0); }
}

@keyframes slideIn {
    from { opacity: 0; transform: translateX(26px); }
    to { opacity: 1; transform: translateX(0); }
}

/* ============ 响应式 ============ */
.sidebar-toggle {
    display: none;
    width: 34px;
    height: 34px;
    border-radius: 9px;
    border: 1px solid var(--line);
    background: var(--panel-b);
    color: var(--text);
    cursor: pointer;
}

/* 宽屏（>992px）：侧栏可折叠。折叠后侧栏左移出屏、正文左边距归零。
   ⚠️ 必须排在上面 .sidebar-toggle{display:none} 之后（同特异性、靠后的胜），
      写在前面的话宽屏按钮会被那条 none 吃掉。
   窄屏的「抽屉」语义在下面 992px 的媒体查询里，两者共用同一个按钮。 */
@media (min-width: 993px) {
    .sidebar-toggle {
        display: inline-flex;
        align-items: center;
        justify-content: center;
    }
    /* ⚠️ transition 写在基类上：写在状态选择器里只有「进入」有动画、退出是瞬变 */
    .main {
        transition: margin-left 0.24s ease;
    }
    html[data-sidebar="collapsed"] .sidebar {
        transform: translateX(-100%);
    }
    html[data-sidebar="collapsed"] .main {
        margin-left: 0;
    }
}

@media (max-width: 1180px) {
    .rack-layout {
        grid-template-columns: 1fr;
    }
    .board {
        grid-template-columns: repeat(2, minmax(200px, 1fr));
    }
}

@media (max-width: 992px) {
    .sidebar-toggle {
        display: inline-flex;
        align-items: center;
        justify-content: center;
    }
    .sidebar {
        transform: translateX(-100%);
        box-shadow: var(--shadow);
    }
    .sidebar.open {
        transform: translateX(0);
    }
    .main {
        margin-left: 0;
        padding: calc(var(--topbar-h) + 14px) 14px 34px;
    }
    .form-grid {
        grid-template-columns: 1fr;
    }
    .board {
        grid-template-columns: 1fr;
    }
    .global-search {
        display: none;
    }
    .table-scroll {
        max-height: none;
    }
    .diff-wrap {
        max-height: none;
    }
}

@media (max-width: 620px) {
    .stat-grid {
        grid-template-columns: 1fr 1fr;
    }
    .stat-value {
        font-size: 24px;
    }
    .brand-sub {
        display: none;
    }
}

/* =========================================================
   v2.0 批次③ 新增：组织结构 / 资产详情 / 机柜空位 / 动效 / 2K 适配
   ========================================================= */

/* ---------- 统计卡色调（概览与组织结构共用） ---------- */
.stat-card.tone-primary { --c1: #1F6FEB; --c2: #58A6FF; }
.stat-card.tone-info    { --c1: #1F6FEB; --c2: #79C0FF; }
.stat-card.tone-ok      { --c1: #238636; --c2: #3FB950; }
.stat-card.tone-warn    { --c1: #9E6A03; --c2: #D29922; }
.stat-card.tone-purple  { --c1: #6E40C9; --c2: #A371F7; }

@keyframes cardIn {
    from { opacity: 0; transform: translateY(10px); }
    to   { opacity: 1; transform: translateY(0); }
}

.stat-card {
    animation: cardIn 0.3s ease both;
}

/* ---------- 通用小件 ---------- */
.link-name {
    cursor: pointer;
    color: inherit;
}

.link-name:hover {
    color: var(--accent);
}

a.link-name {
    text-decoration: none;
}

.tone-ok { color: var(--ok); }
.tone-danger { color: var(--danger); }
.tone-spec {
    color: var(--accent);
    font-family: "JetBrains Mono", Consolas, monospace;
}

.chip-x {
    display: inline-block;
    font-size: 11.5px;
    padding: 2px 9px;
    border-radius: 999px;
    border: 1px solid var(--line-strong);
    color: var(--muted);
    background: var(--hover-overlay);
}

/* ---------- 组织结构：公司 → 机房 → 机柜 ---------- */
.org-company {
    border-radius: var(--radius);
    border: 1px solid var(--line);
    background: linear-gradient(165deg, var(--panel-b), var(--panel-c));
    box-shadow: var(--shadow-card);
    padding: 15px 16px;
    margin-bottom: 14px;
    animation: cardIn 0.32s ease both;
}

.org-company-head {
    display: flex;
    align-items: center;
    gap: 12px;
    flex-wrap: wrap;
}

.org-company-name {
    font-size: 15.5px;
    font-weight: 700;
    display: flex;
    align-items: center;
    gap: 8px;
}

.org-company-name i {
    color: var(--accent);
}

.code-tag {
    font-size: 11px;
    font-weight: 500;
    font-family: var(--font-mono, monospace);
    padding: 2px 7px;
    border-radius: 6px;
    background: var(--color-background-info, rgba(88, 166, 255, 0.12));
    color: var(--accent);
    border: 0.5px solid var(--color-border-info, rgba(88, 166, 255, 0.35));
    letter-spacing: 0.3px;
}

.org-company-meta {
    font-size: 12.5px;
    color: var(--muted);
}

.org-actions {
    display: flex;
    gap: 6px;
    margin-left: auto;
    flex-wrap: wrap;
}

.org-rooms {
    margin-top: 13px;
    padding-left: 12px;
    border-left: 2px solid var(--line);
    display: flex;
    flex-direction: column;
    gap: 11px;
}

.org-room-head {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
}

.org-room-name {
    font-size: 13.5px;
    font-weight: 600;
    display: flex;
    align-items: center;
    gap: 7px;
}

.org-room-name i {
    color: var(--ok);
}

.org-room-meta {
    font-size: 12px;
    color: var(--muted);
}

.org-racks {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(158px, 1fr));
    gap: 9px;
    margin-top: 9px;
    padding-left: 10px;
}

.org-rack {
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    padding: 9px 11px;
    cursor: pointer;
    background: var(--hover-overlay);
    transition: border-color 0.18s, transform 0.18s, box-shadow 0.18s;
}

.org-rack:hover {
    border-color: rgba(88, 166, 255, 0.42);
    transform: translateY(-2px);
    box-shadow: var(--shadow-hover);
}

.org-rack-top {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    font-size: 12.5px;
}

.org-rack-name {
    font-weight: 600;
}

.org-rack-u {
    color: var(--muted);
    font-family: "JetBrains Mono", Consolas, monospace;
    font-size: 11.5px;
}

.org-rack-bar {
    height: 5px;
    border-radius: 3px;
    background: var(--hover-overlay-2);
    overflow: hidden;
    margin: 7px 0 5px;
}

.org-rack-bar i {
    display: block;
    height: 100%;
    border-radius: 3px;
    background: linear-gradient(90deg, var(--primary), var(--accent));
    transition: width 0.4s ease;
}

.org-rack-bar i.high {
    background: linear-gradient(90deg, #9E6A03, #D29922);
}

.org-rack-bar i.full {
    background: linear-gradient(90deg, #B62324, #F85149);
}

.org-rack-sub {
    font-size: 11.5px;
    color: var(--muted);
}

.org-empty {
    color: var(--muted);
    font-size: 12.5px;
    padding: 5px 0;
}

/* ---------- 机柜管理弹层（层级） ---------- */
.mgr-company {
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    padding: 11px 13px;
    margin-bottom: 11px;
}

.mgr-company-head,
.mgr-room-head {
    display: flex;
    align-items: center;
    gap: 9px;
    font-size: 13.5px;
    font-weight: 600;
}

.mgr-company-head i {
    color: var(--accent);
}

.mgr-room {
    margin: 9px 0 0 10px;
    padding-left: 11px;
    border-left: 2px solid var(--line);
}

.mgr-room-head {
    font-weight: 500;
    font-size: 12.8px;
}

.mgr-room-head i {
    color: var(--ok);
}

/* ---------- 资产详情 ---------- */
.detail-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    flex-wrap: wrap;
    padding-bottom: 12px;
    border-bottom: 1px solid var(--line);
    margin-bottom: 15px;
}

.detail-name {
    font-size: 16.5px;
    font-weight: 700;
    display: flex;
    align-items: center;
    gap: 9px;
}

.detail-name i {
    color: var(--accent);
}

.detail-tags {
    display: flex;
    align-items: center;
    gap: 6px;
    flex-wrap: wrap;
}

.detail-block {
    margin-bottom: 18px;
}

.block-title {
    font-size: 13.5px;
    font-weight: 600;
    display: flex;
    align-items: center;
    gap: 7px;
    margin-bottom: 9px;
}

.block-title i {
    color: var(--accent);
}

.block-sub {
    font-size: 12px;
    color: var(--muted);
    margin: 9px 0 6px;
}

.kv-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(228px, 1fr));
    gap: 6px 18px;
}

.kv {
    display: flex;
    gap: 8px;
    font-size: 12.5px;
    padding: 5px 0;
    border-bottom: 1px dashed var(--line);
}

.kv label {
    color: var(--muted);
    min-width: 96px;
    flex-shrink: 0;
}

.kv div {
    word-break: break-all;
}

/* ---------- 资源汇总条 ---------- */
.res-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
    gap: 16px;
}

.res-head {
    font-size: 12.5px;
    color: var(--muted);
    margin-bottom: 7px;
}

.res-bar {
    height: 8px;
    border-radius: 5px;
    background: var(--hover-overlay-2);
    overflow: hidden;
}

.res-bar i {
    display: block;
    height: 100%;
    border-radius: 5px;
    background: linear-gradient(90deg, var(--primary), var(--accent));
    transition: width 0.5s ease;
}

.res-bar i.over {
    background: linear-gradient(90deg, #B62324, #F85149);
}

.res-legend {
    display: flex;
    gap: 15px;
    flex-wrap: wrap;
    font-size: 12px;
    color: var(--muted);
    margin-top: 7px;
}

.res-legend b {
    color: var(--text);
}

.res-note {
    font-size: 12px;
}

/* ---------- 变更履历时间线 ---------- */
.timeline {
    border-left: 2px solid var(--line);
    padding-left: 15px;
    display: flex;
    flex-direction: column;
    gap: 11px;
}

.tl-item {
    position: relative;
}

.tl-item::before {
    content: "";
    position: absolute;
    left: -20px;
    top: 5px;
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--accent);
    box-shadow: 0 0 0 3px rgba(88, 166, 255, 0.14);
    pointer-events: none;   /* 纯装饰圆点，不挡时间线里的链接 */
}

.tl-time {
    font-size: 11.5px;
    color: var(--muted);
}

.tl-body {
    font-size: 12.5px;
    margin-top: 2px;
}

.tl-old { color: #FFB3AE; }
.tl-new { color: #A8E6B0; }

/* ---------- 机柜空位提示 ---------- */
.rack-hint {
    display: flex;
    align-items: center;
    gap: 16px;
    flex-wrap: wrap;
    font-size: 12.5px;
    padding: 9px 12px;
    border-radius: var(--radius-sm);
    background: rgba(88, 166, 255, 0.06);
    border: 1px solid rgba(88, 166, 255, 0.18);
    margin-bottom: 12px;
    animation: cardIn 0.28s ease both;
}

.hint-ok {
    color: var(--ok);
}

.hint-ok b {
    font-size: 14px;
}

.hint-segs {
    color: var(--muted);
}

.hint-segs b {
    color: var(--text);
    font-family: "JetBrains Mono", Consolas, monospace;
}

.rack-slot.storage {
    background: linear-gradient(180deg, var(--rk-sto-1), var(--rk-sto-2));
    border-color: var(--rk-sto-bd);
    color: #FFFFFF;
}

.rack-slot.clickable {
    cursor: pointer;
}

.u-tag {
    font-size: 10px;
    padding: 0 5px;
    border-radius: 4px;
    background: rgba(0, 0, 0, 0.3);
    color: #FFFFFF;
}

/* ---------- 过渡动效（克制：150–250ms） ---------- */
@keyframes rowIn {
    from { opacity: 0; transform: translateX(-4px); }
    to   { opacity: 1; transform: translateX(0); }
}

/* 行入场：只给前 15 行做动画。几十行的大表整屏跑动画是"卡"的主因之一 */
.data-grid tbody tr:nth-child(-n+15) {
    animation: rowIn var(--dur-fast) var(--ease-out) both;
}

.btn-x:active {
    transform: scale(0.97);
}

.list-mini-item {
    transition: background 0.16s, border-color 0.16s;
}

.list-mini-item:hover {
    background: rgba(88, 166, 255, 0.06);
}

.rack-slot.filled {
    transition: background 0.16s, transform 0.16s, filter 0.16s;
}

@media (prefers-reduced-motion: reduce) {
    .stat-card,
    .org-company,
    .rack-hint,
    .data-grid tbody tr,
    .view.active {
        animation: none !important;
    }
}

/* ---------- 2K 屏（≥1900px）布局密度优化 ---------- */
@media (min-width: 1900px) {
    :root {
        --sidebar-w: 234px;
    }

    body {
        font-size: 14.5px;
    }

    .main {
        padding: calc(var(--topbar-h) + 22px) 34px 52px;
    }

    .stat-grid {
        grid-template-columns: repeat(auto-fit, minmax(248px, 1fr));
        gap: 16px;
    }

    .stat-card {
        padding: 19px 20px 16px;
    }

    .stat-value {
        font-size: 31px;
    }

    .card-x {
        padding: 18px;
    }

    .data-grid th,
    .data-grid td {
        padding: 10px 13px;
    }

    .rack-layout {
        grid-template-columns: 1.4fr 1fr;
        gap: 18px;
    }

    .modal-box.wide {
        max-width: 1120px;
    }

    .kv-grid {
        grid-template-columns: repeat(auto-fill, minmax(258px, 1fr));
    }

    .org-racks {
        grid-template-columns: repeat(auto-fill, minmax(178px, 1fr));
    }
}

/* ============ 交互增强（本轮反馈） ============ */
/* 概览卡片可点击 */
.stat-click { cursor: pointer; position: relative; transition: transform .18s ease, border-color .18s ease, box-shadow .18s ease; }
.stat-click:hover { transform: translateY(-3px); border-color: var(--accent, #58A6FF); box-shadow: 0 8px 24px rgba(0,0,0,.25); }
.stat-click:active { transform: translateY(-1px) scale(.98); }
.stat-go { position: absolute; right: 14px; bottom: 12px; font-size: 12px; color: var(--muted, #8B949E); opacity: 0; transition: opacity .18s ease, transform .18s ease; transform: translateX(-4px); }
.stat-click:hover .stat-go { opacity: 1; transform: translateX(0); }

/* IPAM 子网卡片：可点击钻取 */
.subnet-card { cursor: pointer; transition: transform .18s ease, border-color .18s ease, box-shadow .18s ease; }
.subnet-card:hover { transform: translateY(-3px); box-shadow: 0 8px 24px rgba(0,0,0,.25); border-color: var(--accent, #58A6FF); }
.subnet-card-active { border-color: var(--accent, #58A6FF); box-shadow: 0 0 0 1px var(--accent, #58A6FF) inset, 0 6px 18px rgba(0,0,0,.2); }
.subnet-card-ops { float: right; display: inline-flex; gap: 8px; opacity: 0; transition: opacity .15s ease; }
.subnet-card:hover .subnet-card-ops { opacity: .85; }
.subnet-card-ops i { cursor: pointer; font-size: 12px; padding: 3px 5px; border-radius: 5px; }
.subnet-card-ops i:hover { background: rgba(255,255,255,.12); }

/* ============ 本轮反馈：宿主机徽章 / 拖拽上架 / 机柜分组总览 / IPAM 公司栏 ============ */
/* 虚拟机列表：宿主机徽章（与虚拟机字段明显区分） */
.host-chip {
    display: inline-flex; align-items: center; gap: 7px;
    padding: 4px 10px; border-radius: 8px; cursor: pointer;
    background: rgba(45, 212, 191, 0.10);
    border: 1px solid rgba(45, 212, 191, 0.45);
    color: #A9F0E6; text-decoration: none;
    transition: background .16s ease, border-color .16s ease, transform .16s ease;
}
.host-chip:hover { background: rgba(45, 212, 191, 0.20); border-color: rgba(45, 212, 191, 0.75); transform: translateY(-1px); }
.host-chip > i { color: #2DD4BF; font-size: 12px; }
.host-chip-body { display: flex; flex-direction: column; line-height: 1.25; }
.host-chip-body b { font-size: 12.5px; font-weight: 600; }
.host-chip-ip { font-size: 11px; color: rgba(169, 240, 230, 0.72); font-family: "JetBrains Mono", Consolas, monospace; }
/* 浅色主题：宿主机徽章用深青保证对比度（此前硬编码浅紫文字在浅色下看不清） */
html[data-theme="light"] .host-chip { background: rgba(13, 148, 136, 0.10); border-color: rgba(13, 148, 136, 0.45); color: #0F766E; }
html[data-theme="light"] .host-chip:hover { background: rgba(13, 148, 136, 0.18); border-color: rgba(13, 148, 136, 0.70); }
html[data-theme="light"] .host-chip > i { color: #0D9488; }
html[data-theme="light"] .host-chip-ip { color: rgba(15, 118, 110, 0.80); }

/* 机柜页：可拖拽设备条目 */
.drag-dev { cursor: grab; user-select: none; }
.drag-dev:active { cursor: grabbing; }
.drag-dev.dragging { opacity: .45; border-style: dashed; }
.drag-grip { color: var(--muted, #8B949E); font-size: 11px; flex: none; }
.drag-tip { font-size: 11.5px; margin-bottom: 8px; display: flex; align-items: center; gap: 6px; }
.drag-tip i { color: var(--accent, #58A6FF); }

/* 机柜 U 位放置反馈 */
/* 拖拽进行中：让所有空位亮起来，明确"哪里能放" */
.rack-rows.drag-active .rack-slot:not(.filled) {
    background: rgba(88, 166, 255, 0.07);
    border: 1px dashed rgba(88, 166, 255, 0.45);
}

/* 幽灵高亮框：按区间整段提示"这几 U 会被占用"（绿=可放 / 红=放不下） */
.rack-ghost {
    position: absolute;
    left: 40px;
    right: 0;
    display: none;
    border-radius: 5px;
    pointer-events: none;
    z-index: 6;
    transition: top 0.06s linear, height 0.06s linear;
}
.rack-ghost.show { display: block; }
.rack-ghost.ok {
    border: 1px dashed rgba(63, 185, 80, 0.95);
    background: rgba(63, 185, 80, 0.2);
    box-shadow: 0 0 0 1px rgba(63, 185, 80, 0.35) inset, 0 0 12px rgba(63, 185, 80, 0.25);
}
.rack-ghost.no {
    border: 1px dashed rgba(248, 81, 73, 0.85);
    background: rgba(248, 81, 73, 0.16);
}

/* 设备色块：多 U 设备合并成一整块（绝对定位向上覆盖整个占用区间） */
.rack-dev-block {
    position: absolute;
    left: 40px;
    right: 0;
    bottom: 0;
    height: calc(var(--span, 1) * var(--rk-row-h) + (var(--span, 1) - 1) * var(--rk-gap));
    border-radius: 5px;
    cursor: pointer;
    z-index: 3;
    overflow: hidden;
    padding: 2px 9px;
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    align-content: center;
    gap: 2px 9px;
    font-size: 11.5px;
    line-height: 1.25;
    /* 白字压在饱和色上，极轻的文字投影能明显抬升小字号清晰度（2026-11-03） */
    text-shadow: 0 1px 1px rgba(0, 0, 0, 0.28);
    transition: filter 0.16s, transform 0.16s;
}
.rack-dev-block.single { flex-wrap: nowrap; white-space: nowrap; }
/* 提亮幅度收一档：1.2 在白字块上接近"过曝",1.12 足够被感知又不刺眼 */
.rack-dev-block:hover { filter: brightness(1.12); transform: translateX(2px); }
/* 色板 v2：竖向渐变（上亮下暗）+ 1px 顶部内高光。旧版 90deg 横渐变在 20px 高的条上
   是一条死板的"贴纸"；内高光给出设备面板的物理质感。色值见 .rack-rows 的 --rk-*。 */
.rack-dev-block.online {
    background: linear-gradient(180deg, var(--rk-on-1), var(--rk-on-2));
    border: 1px solid var(--rk-on-bd);
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.14);
    color: #FFFFFF;
}
.rack-dev-block.offline {
    background: linear-gradient(180deg, var(--rk-off-1), var(--rk-off-2));
    border: 1px solid var(--rk-off-bd);
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.10);
    color: #E5E7EB;
}
.rack-dev-block.network {
    background: linear-gradient(180deg, var(--rk-net-1), var(--rk-net-2));
    border: 1px solid var(--rk-net-bd);
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.14);
    color: #FFFFFF;
}
.rack-dev-block.storage {
    background: linear-gradient(180deg, var(--rk-sto-1), var(--rk-sto-2));
    border: 1px solid var(--rk-sto-bd);
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.14);
    color: #FFFFFF;
}
.rack-dev-block.plain {
    background: linear-gradient(180deg, var(--rk-pln-1), var(--rk-pln-2));
    border: 1px solid var(--rk-pln-bd);
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.14);
    color: #FFFFFF;
}
.rack-dev-block .rdb-name { font-weight: 700; }
.rack-dev-block .rdb-meta {
    font-size: 10px;
    opacity: .96;
    font-family: "JetBrains Mono", Consolas, monospace;
    display: inline-flex;
    align-items: center;
    gap: 4px;
    color: rgba(255, 255, 255, 0.92);
}
.rack-dev-block.offline .rdb-meta { color: rgba(255, 255, 255, 0.82); }
.rack-dev-block .rdb-meta i { font-size: 9px; opacity: .85; }
.rack-dev-block .rdb-ipmi { color: #FFF4DC; }

/* 显示内容切换（主机名 / +IP / +IPMI） */
.seg-x {
    display: inline-flex;
    align-items: center;
    gap: 3px;
    padding: 3px;
    border: 1px solid var(--line-strong);
    border-radius: 9px;
    background: var(--panel-a);
}
/* 前缀标签：弱化，与可点选的按钮形成对比，让人一眼看出「后面是选项组」 */
.seg-x-label {
    font-size: 11.5px;
    color: var(--muted);
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 0 7px 0 5px;
    border-right: 1px solid var(--line);
    line-height: 1;
}
.seg-x-btn {
    border: none;
    background: transparent;
    color: var(--muted);
    font-size: 11.5px;
    padding: 5px 9px;
    border-radius: 6px;
    cursor: pointer;
    transition: background-color 0.15s var(--ease-out), color 0.15s var(--ease-out);
}
.seg-x-btn:hover {
    background: var(--hover-overlay);
    color: var(--text);
}
/* 选中态：实心主题色 + 白字，与未选中（透明底灰字）形成强对比，深浅主题都清晰 */
.seg-x-btn.active {
    background: linear-gradient(135deg, var(--primary), #1A5FD0);
    color: #fff;
    font-weight: 600;
    box-shadow: 0 2px 8px rgba(31, 111, 235, 0.32);
}

/* 机柜总览分组（公司 → 机房 → 机柜） */
.rack-overview { margin-bottom: 14px; }
.rov-groups { display: flex; flex-direction: column; gap: 12px; }
.rov-company { border: 1px solid var(--line); border-radius: var(--radius); padding: 10px 14px 12px; background: rgba(255,255,255,0.015); }
.rov-company-head { display: flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 700; margin-bottom: 8px; }
.rov-company-head i { color: var(--accent, #58A6FF); }
.rov-company-head .muted { font-weight: 400; font-size: 12px; margin-left: 4px; }
.rov-room { margin-top: 6px; }
.rov-room-head { display: flex; align-items: center; gap: 7px; font-size: 12.5px; font-weight: 600; color: var(--text, #E6EDF3); }
.rov-room-head i { color: #D29922; }
.rov-room-head .muted { font-weight: 400; font-size: 11.5px; }
.org-rack.active { border-color: var(--accent, #58A6FF); box-shadow: 0 0 0 1px var(--accent, #58A6FF) inset, 0 6px 18px rgba(0,0,0,.28); }

/* IPAM：公司优先筛选栏 */
.ipam-company-bar {
    display: flex; align-items: center; gap: 10px;
    padding: 10px 14px; margin-bottom: 14px;
    border: 1px solid var(--line); border-radius: var(--radius);
    background: rgba(88, 166, 255, 0.05);
}
.ipam-company-bar > i { color: var(--accent, #58A6FF); font-size: 15px; }
.ipam-company-label { font-size: 13px; font-weight: 700; }
.subnet-empty { border-style: dashed; }

/* ============ 主题切换（跟随系统 / 浅色 / 深色） ============ */
.theme-seg {
    display: inline-flex;
    align-items: center;
    gap: 2px;
    padding: 3px;
    border: 1px solid var(--line);
    border-radius: 9px;
    background: var(--panel-b);
}

.theme-btn {
    width: 28px;
    height: 26px;
    display: grid;
    place-items: center;
    border: 0;
    border-radius: 6px;
    background: transparent;
    color: var(--muted);
    font-size: 12px;
    cursor: pointer;
    transition: background 0.18s ease, color 0.18s ease;
}

.theme-btn:hover {
    background: var(--hover-overlay);
    color: var(--text);
}

.theme-btn.active {
    background: rgba(31, 111, 235, 0.20);
    color: var(--accent);
    box-shadow: inset 0 0 0 1px rgba(88, 166, 255, 0.32);
}

/* 浅色下部分元素需要单独微调 */
html[data-theme="light"] .login-card {
    background: linear-gradient(160deg, #FFFFFF 0%, #F1F5FA 100%);
    box-shadow: 0 10px 30px rgba(31, 35, 40, 0.14), 0 0 0 1px rgba(31, 111, 235, 0.06);
}

html[data-theme="light"] .topbar {
    background: var(--bg-0);
    box-shadow: 0 1px 0 rgba(31, 35, 40, 0.04);
}

/* 机柜浅色主题兜底（2026-11-03 色板 v2）：
   1) 柜体内凹阴影：深色那块 40px 黑阴影原样压到浅色的白卡片上就是一坨灰雾，
      换成几乎读不出、但仍有"凹槽"感的浅灰;
   2) 设备块色板已改为主题无关（深浅同一套实心色 + 白字，色弱可辨），
      原先整段复制到浅色的理由不再成立，删除。 */
html[data-theme="light"] {
    --rk-well: inset 0 0 0 1px rgba(31, 35, 40, 0.05), inset 0 2px 10px rgba(31, 35, 40, 0.05);
    --rk-well-unit: inset 0 0 0 1px rgba(31, 35, 40, 0.05), inset 0 1px 6px rgba(31, 35, 40, 0.06);
}

/* ============================================================
   v21: 浅色主题可读性补齐（P2-8 逐页巡检的产物）
   ------------------------------------------------------------
   巡检方式见 tools/devcheck/contrastprobe.html：先冻结全站 transition/animation，
   再逐页把每个文本元素与它的「合成背景」（逐层 background-color + linear/radial-gradient
   的色标两端，取最差一侧）算 WCAG 对比度。判据 正文 4.5:1 / 大字 3:1。

   修的都是实测不达标的，其中「激活态白字 + 半透明底」在浅色下最严重——
   白字压在 22% 蓝染上等于白底白字（侧栏高亮项实测 1.13:1）。
   深色侧只动了 --muted 与几处 3.2~3.8:1 的边角。
   ============================================================ */
/* 色调文字：深色主题用的粉彩字色（#7EE787 / #E3B341 / #FF9A94）压在浅色淡染底上会糊掉，
   换成语义色的深色版；底色淡染保持不变（浅色下 4.4~7:1）。 */
html[data-theme="light"] .badge-ok,
html[data-theme="light"] .btn-ok-x { color: #116329; }
html[data-theme="light"] .badge-warn,
html[data-theme="light"] .rov-room-head i { color: #7A5100; }
html[data-theme="light"] .badge-danger,
html[data-theme="light"] .btn-danger-x { color: #B01E28; }
html[data-theme="light"] .badge-info,
html[data-theme="light"] .nav-badge,
html[data-theme="light"] .code-tag { color: #0B4FA8; }
html[data-theme="light"] .diff-row.add .tx { color: #116329; }
html[data-theme="light"] .diff-row.del .tx { color: #B01E28; }

/* --- 第二轮补齐（第一轮漏了这两条规则，只写了注释没写规则，实测后才发现） --- */
/* 侧栏高亮项：底色是 22%→5% 的极淡蓝染，白字在浅色下等于白底白字（实测 1.13:1）。
   深色主题下白字没问题，所以只覆盖浅色。 */
html[data-theme="light"] .nav-item.active { color: #0F4EB0; }
/* U 位标签：底色是 rgba(0,0,0,.3)，压在浅色底上只剩浅灰，白字实测 2.11:1 → 加深底色。
   必须排除 .u-tag-none（「未指定 U 位」那个暖色标签），否则会连它的底色一起改掉。 */
html[data-theme="light"] .u-tag:not(.u-tag-none) { background: rgba(31, 35, 40, 0.62); color: #FFFFFF; }
/* IPMI 标记：浅色下被「.rack-dev-block .rdb-meta{白字}」按特异性盖掉了，这里补一条更高的。 */
html[data-theme="light"] .rack-dev-block .rdb-meta.rdb-ipmi { color: #FFF4DC; }
/* 密码复用徽章：--warn 落在 10% 淡黄底上只有 4.37:1 */
html[data-theme="light"] .cred-reuse { color: #7A5100; border-color: #7A5100; }

/* 空槽浅色可见度：与深色同档提亮（2026-11-03，原 3.5%/10% 淡到网格消失） */
html[data-theme="light"] .rack-slot {
    background: rgba(31, 35, 40, 0.05);
    border-color: rgba(31, 35, 40, 0.18);
}

/* ============ 凭据保险箱：统计卡 / 关联标记 / 图标按钮 ============ */
#credStats { margin-bottom: 14px; }
#credStats .stat-value.tone-ok { color: var(--ok); }
#credStats .stat-value.tone-warn { color: var(--warn); }
#credStats .stat-value.tone-danger { color: var(--danger); }

.cred-link {
    margin-top: 3px;
    font-size: 11px;
    font-weight: 400;
    color: var(--accent);
    display: flex;
    align-items: center;
    gap: 5px;
}
.cred-link i { font-size: 10px; }
.cred-link-none { color: var(--muted); opacity: .75; }

.btn-x.icon-only {
    padding: 3px 6px;
    margin-left: 5px;
    line-height: 1;
}
.btn-x.icon-only i { font-size: 11px; }

/* ============ IPAM：空闲段与批量分配 ============ */
.ipam-freebar {
    display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
    padding: 10px 14px; margin-bottom: 12px;
    border: 1px solid var(--line); border-radius: var(--radius);
    background: var(--panel-b);
}
.ipam-freebar > i { color: var(--ok); font-size: 15px; }
.ipam-freebar .free-meta { font-size: 12.5px; color: var(--muted); }
.ipam-freebar .free-meta b { color: var(--text); }

.free-block-list { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
.free-block-chip {
    display: inline-flex; align-items: center; gap: 7px;
    padding: 5px 10px; border-radius: 8px;
    border: 1px solid var(--line); background: var(--panel-c);
    font-size: 12px; cursor: pointer;
    transition: border-color .15s ease, background .15s ease;
}
.free-block-chip:hover { border-color: var(--accent); background: var(--hover-overlay); }
.free-block-chip .mono { font-size: 11.5px; }
.free-block-chip .badge-x { font-size: 10.5px; padding: 1px 6px; }

.addr-used { color: var(--danger); }
.addr-free { color: var(--ok); }


/* ============ 折叠区块（详情页 / IPAM 上半部分） ============ */
.coll-head {
    cursor: pointer; user-select: none;
    display: flex; align-items: center; gap: 7px;
    transition: color .15s ease;
}
.coll-head:hover { color: var(--accent, #58A6FF); }
.coll-head .coll-chevron {
    font-size: 10.5px; width: 12px; text-align: center;
    color: var(--muted); transition: transform .16s ease;
}
.coll-head:hover .coll-chevron { color: var(--accent, #58A6FF); }
.coll-body { margin-top: 10px; }

/* ============ 机柜三面板布局 ============ */
.rack-layout {
    grid-template-columns: minmax(260px, 380px) minmax(300px, 1fr);
}
.rack-left { min-width: 0; }
.rack-right {
    display: flex; flex-direction: column; gap: 12px;
    min-width: 0;
    /* 右栏三块各自滚动，总高与左栏机柜对齐 */
    height: 100%;
    max-height: calc(100vh - 190px);
}
.rack-panel {
    display: flex; flex-direction: column;
    min-height: 0;                  /* flex 子项能真正收缩，否则 overflow 不生效 */
    flex: 1 1 0;
    padding: 12px;
}
.rack-panel-sub { font-size: 11.5px; margin: 2px 0 8px; }
.rack-panel-search { margin: 8px 0 4px; }
.rack-panel-body {
    flex: 1 1 auto;
    min-height: 60px;
    overflow-y: auto;
    overflow-x: hidden;
    margin-top: 6px;
    padding-right: 4px;             /* 给滚动条留位，避免贴边 */
}
.rack-panel-body::-webkit-scrollbar { width: 8px; }
.rack-panel-body::-webkit-scrollbar-thumb {
    background: var(--line-strong, rgba(255,255,255,.16));
    border-radius: 4px;
}
.rack-panel-body::-webkit-scrollbar-thumb:hover { background: var(--line-strong); }

/* 紧凑拖拽行：主机名 + U 数 + 类型色点 */
.drag-compact {
    display: flex; align-items: center; gap: 7px;
    padding: 5px 8px !important;
}
.drag-compact .nm {
    flex: 1 1 auto; min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    font-size: 12.5px;
}
.drag-compact .u-tag { flex: 0 0 auto; }
.dot-type {
    flex: 0 0 auto; width: 10px; height: 10px; border-radius: 50%;
    display: inline-block;
    background: var(--muted);
    border: 2px solid rgba(255, 255, 255, 0.35);
}
/* 色点与机柜色板 v2 对齐（2026-11-03）：右栏设备列表、图例、立面色块三处同色 */
.dot-type.network { background: #15803D; }
.dot-type.storage { background: #7C3AED; }
.dot-type.online { background: #2563EB; }
.dot-type.offline { background: #4B5563; }
.dot-type.plain { background: #B45309; }

/* 未定位面板作为"拖出卸下"投放区时的提示 */
.rack-panel-body.drop-out-hint {
    outline: 1px dashed var(--ok, #3FB950);
    outline-offset: -3px;
    border-radius: 8px;
}
.rack-panel-body.drop-out-active {
    background: rgba(63, 185, 80, .10);
    outline: 2px solid var(--ok, #3FB950);
    outline-offset: -3px;
}

/* 其他未上架设备：按机房分组 */
.autre-group { margin-bottom: 10px; }
.autre-group:last-child { margin-bottom: 0; }
.autre-head {
    display: flex; align-items: center; gap: 7px;
    font-size: 12px; color: var(--muted);
    padding: 3px 2px 6px;
    border-bottom: 1px solid var(--line);
    margin-bottom: 5px;
}
.autre-head i { color: var(--accent, #58A6FF); }

/* 空位推荐提示 */
.snap-tip { font-size: 12px; color: var(--warn, #D29922); }
.snap-tip b { color: var(--text); }

/* ============ IPAM：选中框批量分配 ============ */
.ipam-selbar {
    display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
    padding: 8px 14px; margin-bottom: 10px;
    border: 1px solid var(--accent, #58A6FF);
    border-radius: var(--radius);
    background: var(--panel-c);
    font-size: 12.5px;
}
.ipam-selbar .sel-count { color: var(--accent, #58A6FF); font-weight: 600; }
.ipam-search-toggle { margin-left: auto; }

/* 密码复用徽章 */
.cred-reuse {
    display: inline-flex; align-items: center; gap: 4px;
    margin-left: 6px; padding: 1px 7px;
    border-radius: 999px;
    font-size: 10.5px; cursor: pointer;
    border: 1px solid var(--warn, #D29922);
    color: var(--warn, #D29922);
    background: rgba(210, 153, 34, .10);
}
.cred-reuse:hover { background: rgba(210, 153, 34, .22); }

/* IP 状态：保留 */
.badge-reserved {
    background: rgba(163, 113, 247, .16) !important;
    color: #A371F7 !important;
    border-color: rgba(163, 113, 247, .45) !important;
}

/* 收起 IPAM 上半部分后，明细表拿到更大的可用高度 */
.ipam-table-tall { max-height: calc(100vh - 210px) !important; overflow-y: auto; }

/* 批量分配给设备的预览清单 */
.assign-preview {
    padding: 10px 12px; margin-bottom: 12px;
    border: 1px solid var(--line); border-radius: var(--radius);
    background: var(--panel-c);
}
.assign-preview-head { font-size: 12.5px; color: var(--muted); margin-bottom: 6px; }
.assign-preview-list {
    font-size: 12px; line-height: 1.7; word-break: break-all;
    max-height: 120px; overflow-y: auto;
    color: var(--accent, #58A6FF);
}

/* IPAM 子网卡片上的保留数提示 */
.subnet-card .stat-sub .res-keep { color: #A371F7; }

/* ============================================================
   可拖拽分栏（机柜视图 / 配置保险箱）
   ------------------------------------------------------------
   两栏改成 flex：左栏 width = --split-w，中间 9px 分隔条，右栏吃掉剩余。
   写在文件末尾，用于覆盖前面的 grid 定义（同优先级后者胜）。
   ============================================================ */
.rack-layout {
    display: flex;
    align-items: stretch;
    gap: 0;
    --split-w: 430px;
}
.rack-layout > :first-child {
    flex: 0 0 var(--split-w);
    min-width: 0;
}
.rack-layout > :last-child {
    flex: 1 1 auto;
    min-width: 0;
}
.rack-layout > .splitter {
    flex: 0 0 9px;
    align-self: stretch;
    position: relative;
    cursor: col-resize;
    border-radius: 4px;
    background: transparent;
    transition: background .12s ease;
}
.rack-layout > .splitter::before {
    content: '';
    position: absolute;
    left: 4px;
    top: 0;
    bottom: 0;
    width: 1px;
    background: var(--line);
}
.rack-layout > .splitter::after {
    content: '';
    position: absolute;
    left: 2.5px;
    top: 50%;
    width: 4px;
    height: 28px;
    margin-top: -14px;
    border-radius: 2px;
    background: var(--line-strong);
    transition: height .12s ease, margin-top .12s ease, background .12s ease;
}
.rack-layout > .splitter:hover,
.rack-layout > .splitter.dragging { background: rgba(31, 111, 235, 0.18); }
.rack-layout > .splitter:hover::before,
.rack-layout > .splitter.dragging::before { background: var(--accent); width: 2px; }
.rack-layout > .splitter:hover::after,
.rack-layout > .splitter.dragging::after {
    background: var(--accent);
    height: 44px;
    margin-top: -22px;
}
body.is-col-resizing { user-select: none; }
body.is-col-resizing,
body.is-col-resizing * { cursor: col-resize !important; }

@media (max-width: 1180px) {
    .rack-layout { flex-direction: column; gap: 16px; }
    .rack-layout > :first-child { flex: 1 1 auto; }
    .rack-layout > .splitter { display: none; }
    .rack-layout > .rack-right { max-height: none; }
}

/* ============================================================
   模块化面板
   ============================================================ */
.mod-bar {
    display: flex;
    align-items: center;
    gap: 10px;
    margin: 0 0 12px;
}
.mod-bar-text { font-size: 11.5px; color: var(--muted); margin-right: auto; }
.rack-right > .mod-bar { flex: 0 0 auto; margin-bottom: 0; }
/* 概览网格 = 12 列坐标网格（需求 5：可拖动换位 / 可拖右下角改大小）。
   列数、行高最小值、间距三处必须与 modules.js 的
   App.OV_COLS / App.OV_ROW_H / App.OV_GAP 保持一致，否则吸附会错格。
   行高用 minmax(--ov-row-h, auto)：内容撑得开就长高，永远不裁切、不出滚动条。 */
.mod-grid {
    display: grid;
    --ov-cols: 12;
    --ov-row-h: 96px;
    grid-template-columns: repeat(var(--ov-cols), minmax(0, 1fr));
    grid-auto-rows: minmax(var(--ov-row-h), auto);
    gap: 14px;
    align-items: start;
}
/* 网格项：坐标（grid-column / grid-row）与 --mod-h 由 modules.js 写成 inline 样式。
   .mod-span 的「整行」语义改由默认坐标（recent 是 w:12）承担，
   这里只兜住没被包进 .mod-cell 的老路径。 */
.mod-grid > .mod-span { grid-column: 1 / -1; }

.mod-cell {
    position: relative;
    min-width: 0;                 /* 防止内层 SVG / 长文本把网格撑破 */
    align-self: stretch;          /* 格子比内容高时卡片跟着长高（拉高才看得出效果） */
}
.mod-cell > .card-x,
.mod-cell > .stat-card { height: 100%; }
.mod-cell.is-moving > .card-x,
.mod-cell.is-moving > .stat-card {
    opacity: .82;
    border-color: var(--accent) !important;
    box-shadow: 0 0 0 2px var(--accent), var(--shadow-pop) !important;
}
.mod-cell.is-resizing > .card-x,
.mod-cell.is-resizing > .stat-card {
    border-color: var(--accent) !important;
    box-shadow: 0 0 0 2px var(--accent) !important;
}
body.is-mod-dragging,
body.is-mod-dragging * { cursor: grabbing !important; user-select: none; }
body.is-mod-dragging .mod-cell.is-resizing,
body.is-mod-dragging .mod-cell.is-resizing * { cursor: se-resize !important; }

/* .card-head 是 space-between；网格页面多了一个 ⠿ 握把当第一个子元素，
   space-between 会把它和标题、右侧按钮摊成三段。改成 flex-start + 右侧
   margin-left:auto（.mod-head-right 本来就有），两个子元素时表现与原来一致。 */
.mod-head { align-items: center; justify-content: flex-start; }
.mod-head-right {
    display: flex;
    align-items: center;
    gap: 6px;
    margin-left: auto;
}
.mod-kebab {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    border: none;
    border-radius: 6px;
    background: transparent;
    color: var(--muted);
    font-size: 12px;
    line-height: 1;
    cursor: pointer;
}
.mod-kebab:hover { background: var(--hover-overlay-2); color: var(--text); }

.mod-float {
    position: absolute;
    z-index: 3000;
    min-width: 172px;
    padding: 5px;
    border-radius: 10px;
    border: 1px solid var(--line-strong);
    background: var(--bg-1);
    box-shadow: var(--shadow);
}
.mod-float-title {
    padding: 5px 8px 7px;
    margin-bottom: 4px;
    border-bottom: 1px solid var(--line);
    font-size: 11px;
    color: var(--muted);
}
.mod-float-item {
    display: flex;
    align-items: center;
    gap: 9px;
    width: 100%;
    padding: 6px 8px;
    border: none;
    border-radius: 6px;
    background: transparent;
    color: var(--text);
    font-family: inherit;
    font-size: 12.5px;
    text-align: left;
    cursor: pointer;
}
.mod-float-item i { width: 13px; font-size: 11.5px; color: var(--muted); }
.mod-float-item:hover:not([disabled]) { background: var(--hover-overlay-2); }
.mod-float-item[disabled] { opacity: .35; cursor: not-allowed; }

.mod-pick {
    display: flex;
    flex-direction: column;
    gap: 2px;
    max-height: 54vh;
    overflow-y: auto;
}
/* 模块选择器的分组标题（def.group），见 modules.js 的 App.MODULE_GROUP_ORDER */
.mod-pick-group {
    font-size: 11px;
    color: var(--muted);
    margin: 9px 0 3px;
}
.mod-pick-group:first-child { margin-top: 0; }
.mod-pick-item {
    display: flex;
    align-items: center;
    gap: 9px;
    padding: 8px 10px;
    border-radius: 8px;
    font-size: 13px;
    cursor: pointer;
}
.mod-pick-item:hover { background: var(--hover-overlay); }
.mod-pick-hint { margin-left: auto; font-size: 11.5px; }

/* 网络访问 */
.net-url {
    display: flex;
    align-items: center;
    gap: 9px;
    margin-top: 6px;
    padding: 7px 10px;
    border: 1px solid var(--line);
    border-radius: 8px;
    background: var(--hover-overlay);
    font-size: 13px;
}
.net-url a {
    color: var(--accent);
    font-family: "Cascadia Mono", Consolas, monospace;
    text-decoration: none;
}
.net-url a:hover { text-decoration: underline; }
.net-url .btn-x { margin-left: auto; }

/* ============================================================
   机柜右栏排版修正（2026-09-14 二次修复）
   ------------------------------------------------------------
   问题：三个面板原本 flex:1 1 0 平分右栏高度，右栏又被
   max-height: calc(100vh - 190px) 卡住。窗口一矮（或模块变多），
   每个面板被压到只剩几十像素，而 .rack-panel-body 有 min-height，
   内容直接溢出盖住下一个面板的标题 —— 就是"挤到一起"。
   修正：面板改为按内容高度排列（不再抢高），列表超过上限就自己
   滚动；右栏不再限制总高，交给页面滚动，避免嵌套滚动条。
   ============================================================ */
.rack-layout > .rack-right {
    align-self: flex-start;     /* 高度随内容，不再被拉伸 */
    height: auto;
    max-height: none;
    overflow: visible;
    gap: 14px;
}
.rack-layout > .rack-right > .mod-bar { margin: 0 0 2px; }
.rack-panel {
    flex: 0 0 auto;             /* 关键：不再与其它面板平分高度 */
    min-height: 0;
    padding: 14px;
}
.rack-panel-body {
    flex: 0 0 auto;
    min-height: 0;
    max-height: 480px;          /* 单个列表超过就内部滚动，面板本身不被压扁 */
                                /* 420 是为了让「已定 U 位 + 未定 U 位」两段尽量一屏看完 */
    margin-top: 8px;
}
.rack-panel > .search-wrap { margin: 10px 0 2px; }
.rack-panel-body > .list-mini { margin: 0; }

@media (max-width: 1180px) {
    .rack-panel-body { max-height: none; }     /* 单列布局：让内容自然展开 */
}

/* 保险箱：两栏列表各自的搜索框 */
.vault-search { margin: 10px 0 8px; }
.vault-list-head {
    display: flex; align-items: center; gap: 8px;
    margin: 4px 0 8px;
    font-size: 11.5px; color: var(--muted);
}
.vault-list-head .spacer { flex: 1 1 auto; }
.list-mini-item.vault-on { background: var(--hover-overlay); box-shadow: inset 2px 0 0 var(--accent); }

/* ============ 凭据保险箱：统计卡可点筛选 + 密码复用分组 ============ */
/* 与概览统计卡、IPAM 子网卡同一套交互：hover 抬起、点击筛选、再点取消 */
.cred-stat-on {
    border-color: var(--accent);
    box-shadow: 0 0 0 1px var(--accent) inset, var(--shadow-card);
}

.cred-stat-on-tip {
    display: flex;
    align-items: center;
    gap: 5px;
    margin-top: 4px;
    font-size: 11px;
    color: var(--accent);
}

.cred-stat-on-tip i { font-size: 10px; }

/* 筛选提示条：告诉用户「下面这张表为什么只剩几条」 */
.filter-bar {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
    margin: -6px 0 12px;
    padding: 8px 12px;
    border: 1px solid var(--accent);
    border-radius: var(--radius);
    background: rgba(88, 166, 255, 0.08);
    font-size: 12.5px;
    color: var(--muted);
}

.filter-bar > i { color: var(--accent); }
.filter-bar b { color: var(--text); }
.filter-bar .btn-x { margin-left: auto; }

/* 「复用 N」徽章弹出的同组账号清单 */
.reuse-list {
    max-height: 320px;
    overflow: auto;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--panel-b);
}

.reuse-item {
    padding: 9px 12px;
    border-bottom: 1px solid var(--line);
}

.reuse-item:last-child { border-bottom: none; }
.reuse-item-self { background: rgba(88, 166, 255, 0.10); }

.reuse-item-main {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 13px;
}

.reuse-item-sub {
    margin-top: 3px;
    font-size: 11.5px;
    color: var(--muted);
}

/* ============ IPAM：游离 IP 卡片（可点，点进去只看这些记录） ============ */
.orphan-card .stat-label i { color: var(--muted); transition: color .18s ease; }
.orphan-card:hover .stat-label i { color: var(--warn); }
.orphan-card .stat-value.orphan-warn { color: var(--warn); }
/* ============ 机柜视图：机柜内「未定 U 位」分组 ============ */
/* 设备已选本机柜但没填起始 U：既上不了 U 位图，也不该算「未定位」，
   单独一段列出来，可以直接拖到左侧 U 位图落位。 */
.rack-sub-head {
    display: flex;
    align-items: center;
    gap: 8px;
    margin: 10px 0 6px;
    padding-top: 9px;
    border-top: 1px dashed var(--line);
    font-size: 12px;
    color: var(--text);
}

.rack-sub-head i { color: var(--warn); }
.rack-sub-head .muted { font-size: 11.5px; }

.rack-unpos-list .list-mini-item {
    border-left: 2px solid var(--warn);
    background: rgba(210, 153, 34, 0.05);
}

.u-tag.u-tag-none {
    color: var(--warn);
    border-color: rgba(210, 153, 34, 0.42);
    background: rgba(210, 153, 34, 0.12);
}


/* 物理机磁盘组编辑器（一对多 RAID） */
.dg-list { display: flex; flex-direction: column; gap: 6px; }
.dg-row {
    display: grid;
    grid-template-columns: 1.2fr 0.8fr 0.9fr 0.7fr 0.9fr 1.1fr auto;
    gap: 6px;
    align-items: center;
}
.dg-row input, .dg-row select {
    width: 100%;
    box-sizing: border-box;
    padding: 6px 8px;
    border: 1px solid var(--line, #d8d8d8);
    border-radius: 6px;
    font-size: 12.5px;
    background: var(--bg, #fff);
    color: var(--text, #222);
}
.dg-hint { font-size: 11.5px; color: var(--muted, #888); margin: 4px 0 8px; }
.dg-add { margin-top: 8px; font-size: 12.5px; }
.dg-del {
    padding: 4px 8px;
    border: none;
    background: transparent;
    color: var(--danger, #d33);
    cursor: pointer;
    font-size: 14px;
}
.dg-del:hover { background: rgba(211, 51, 51, 0.08); border-radius: 6px; }
@media (max-width: 720px) {
    .dg-row { grid-template-columns: 1fr 1fr; }
}

/* ---------------- 新建资产统一入口：类型切换按钮 ---------------- */
.new-asset-types {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  padding: 2px 2px 12px;
  border-bottom: 1px dashed var(--line, #e3e6ee);
  margin-bottom: 12px;
}
.new-asset-types .nat-tip {
  font-size: 12.5px;
  color: var(--muted, #8a90a3);
}
.nat-btn {
  border: 1px solid var(--line, #d9dde8);
  background: var(--panel, #fff);
  color: var(--text, #333);
  border-radius: 8px;
  padding: 6px 14px;
  font-size: 13px;
  cursor: pointer;
  transition: background-color 0.15s var(--ease-out), border-color 0.15s var(--ease-out), color 0.15s var(--ease-out), box-shadow 0.15s var(--ease-out), transform 0.15s var(--ease-out);
}
.nat-btn i { margin-right: 4px; opacity: .75; }
.nat-btn:hover { border-color: var(--primary, #3b6ef6); color: var(--primary, #3b6ef6); }
.nat-btn.nat-on {
  background: var(--primary, #3b6ef6);
  border-color: var(--primary, #3b6ef6);
  color: #fff;
}
.nat-btn.nat-on i { opacity: 1; }
/* ---- v16-①：复制上一台 ---- */
.clone-last-bar {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  padding: 0 2px 10px;
}
.clone-last-bar .btn-x { padding: 4px 12px; font-size: 12.5px; }
.clone-last-tip { font-size: 12px; color: var(--muted, #8a90a3); }
/* ---- 三期：网络接口/端口面板 ---- */
.badge-muted {
  background: rgba(128, 128, 128, 0.14);
  color: #6b7280;
}
.btn-sm {
  padding: 3px 10px;
  font-size: 12px;
  border-radius: 6px;
}
#ifaceSection .sel {
  min-width: 86px;
}

/* =========================================================
   三期修补（v29）
   1) 表格操作图标：统一间距与 hover 反馈，避免误触
   2) 接口表单内的「分配 IP」区块
   3) 连线对话框的筛选/列表控件
   ========================================================= */

/* ---- 1) 操作图标按钮 ---- */
.ops-x {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

i.act-x {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border-radius: 7px;
  font-size: 13px;
  color: var(--muted);
  cursor: pointer;
  transition: background 0.16s ease, color 0.16s ease, transform 0.16s ease;
}

i.act-x:hover {
  background: var(--hover-overlay-2);
  color: var(--accent);
  transform: translateY(-1px);
}

i.act-x:active {
  transform: translateY(0) scale(0.94);
}

i.act-x.act-danger:hover {
  background: rgba(248, 81, 73, 0.16);
  color: var(--danger);
}

/* 子网卡片上的操作图标沿用同一套观感 */
.subnet-card-ops i.act-x {
  width: 24px;
  height: 24px;
}

/* ---- 2) 接口表单：分配 IP ---- */
.iface-ip-block {
  margin-top: 14px;
  padding-top: 12px;
  border-top: 1px dashed var(--line-strong);
}

.ip-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-bottom: 8px;
}

.ip-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  align-items: center;
  gap: 8px;
  padding: 6px 8px;
  border: 1px solid var(--line);
  border-radius: 9px;
  background: var(--inp-bg);
}

.ip-row .ip-cell {
  font-size: 12.5px;
}

.ip-add-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-bottom: 6px;
}

.ip-add-bar .sel {
  flex: 1 1 180px;
  min-width: 150px;
}

/* ---- 3) 连线对话框：可滚动的设备列表 ---- */
select.sel.lk-list {
  height: auto;
  padding: 4px;
  line-height: 1.6;
}

select.sel.lk-list option {
  padding: 5px 8px;
}

/* =========================================================
   动效与字体收口（v29）
   ========================================================= */

/* 西文/数字走 Segoe UI 时开启等宽数字：IP、容量这类列左右不会抖 */
.data-grid td,
.kv-grid,
.mono {
    font-variant-numeric: tabular-nums;
}

/* 尊重系统「减少动态效果」设置 */
@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }
}


/* =========================================================
   v30 工单管理（看板统计条 / 分段切换 / 附件 / 回收站 / 巡检明细）
   + 全局搜索分组条数
   ========================================================= */

/* ---- 状态统计条：4 张大卡，点一下按该状态过滤（再点取消） ---- */
.ticket-stat {
    position: relative;
}

.ticket-stat .stat-value {
    font-variant-numeric: tabular-nums;
}

.ticket-stat-on {
    border-color: var(--primary);
    box-shadow: inset 0 0 0 1px var(--primary);
}

/* ---- 看板：四列各自限高滚动，列高按内容自适应，不再被拉成一样高 ---- */
.board {
    align-items: start;
}

.board-col {
    display: flex;
    flex-direction: column;
}

.board-col-body {
    max-height: 58vh;
    overflow-y: auto;
}

.board-empty {
    font-size: 12.5px;
    padding: 12px 2px;
    text-align: center;
}

.board-more {
    font-size: 11.5px;
    padding: 8px 2px 0;
    text-align: center;
}

/* ---- 工单卡片里的小警示（关联设备已不在台账） ---- */
.tk-warn {
    color: var(--warn);
}

.tk-no .tk-warn {
    margin-left: 4px;
}

/* ---- 详情里的多行长文本 ---- */
.tk-text {
    font-size: 13px;
    line-height: 1.75;
    word-break: break-word;
}

/* ---- 详情里可点的对象链接（设备名等） ---- */
.link-x {
    color: var(--accent);
    cursor: pointer;
    text-decoration: none;
    border-bottom: 1px dashed currentColor;
}

.link-x:hover {
    color: var(--primary);
}

/* ---- 附件 ---- */
/* label 当按钮用，把原生 file input 藏掉 */
.tk-upload {
    margin-left: auto;
    cursor: pointer;
    font-weight: 400;
}

.tk-attach-list {
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.tk-attach {
    display: flex;
    align-items: center;
    gap: 9px;
    font-size: 12.5px;
    padding: 7px 10px;
    border: 1px solid var(--line);
    border-radius: 9px;
    background: var(--hover-overlay);
}

.tk-attach > i {
    color: var(--muted);
}

.tk-attach-name {
    flex: 1 1 auto;
    min-width: 0;
    color: var(--accent);
    text-decoration: none;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.tk-attach-name:hover {
    text-decoration: underline;
}

/* ---- 工单回收站 ---- */
.tk-trash-list {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.tk-trash {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 12px;
    border: 1px solid var(--line);
    border-radius: 10px;
}

.tk-trash-main {
    flex: 1 1 auto;
    min-width: 0;
    font-size: 13px;
}

.tk-trash-main .muted {
    font-size: 11.5px;
    margin-top: 3px;
}

/* ---- 回收站：批量彻底删除条 ---- */
.tk-trash-bar {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-bottom: 10px;
    padding: 8px 12px;
    background: var(--bg-2);
    border: 1px solid var(--line);
    border-radius: 10px;
}

.tk-trash-bar .spacer {
    flex: 1 1 auto;
}

.tk-trash-all {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    font-size: 12.5px;
    cursor: pointer;
}

.tk-trash-cb {
    flex: 0 0 auto;
    cursor: pointer;
}

.tk-trash.tk-trash-on {
    background: rgba(248, 81, 73, 0.08);
    border-color: rgba(248, 81, 73, 0.4);
}

/* ---- 一键巡检明细 ---- */
.poll-sub {
    font-size: 11.5px;
    color: var(--muted);
    margin: 8px 0 4px;
    letter-spacing: 0.4px;
}

.poll-row {
    display: flex;
    align-items: center;
    gap: 10px;
    font-size: 12.5px;
    padding: 5px 8px;
    border-radius: 7px;
}

.poll-row:nth-child(odd) {
    background: var(--hover-overlay);
}

.poll-row.poll-bad {
    color: var(--danger);
}

.poll-row.poll-bad .muted {
    color: inherit;
    opacity: 0.75;
}

.poll-row .mono {
    min-width: 128px;
}

/* ---- 全局搜索：分组标题右侧显示条数 ---- */
.search-group-title {
    display: flex;
    align-items: center;
    gap: 6px;
}

.search-group-n {
    margin-left: auto;
    font-size: 11px;
    color: var(--muted);
    letter-spacing: 0;
}


/* =========================================================
   v31：通用右键菜单 + 工单批量操作条
   ========================================================= */
/* 挂 document.body + position:fixed：不能挂弹层里，会被 .modal-body 的 overflow 裁掉。
   z-index 2900 介于弹层（1800+栈深）与 toast（3000）之间。 */
.ctx-menu {
    position: fixed;
    z-index: 2900;
    min-width: 176px;
    max-width: 320px;
    padding: 5px;
    background: var(--bg-1);
    border: 1px solid var(--line-strong);
    border-radius: var(--radius-sm);
    box-shadow: var(--shadow);
    font-family: var(--font);
    font-size: 13px;
    color: var(--text);
    opacity: 0;
    transform: translateY(-4px);
    transition: opacity var(--dur-fast) var(--ease-out), transform var(--dur-fast) var(--ease-out);
}

.ctx-menu.show {
    opacity: 1;
    transform: translateY(0);
}

.ctx-item {
    display: flex;
    align-items: center;
    gap: 9px;
    padding: 7px 10px;
    border-radius: 6px;
    cursor: pointer;
    white-space: nowrap;
    user-select: none;
}

/* 无图标项渲染 .ctx-ico-empty：占位保持缩进对齐，不显示 */
.ctx-item > i {
    flex: 0 0 auto;
    width: 14px;
    text-align: center;
    font-size: 12px;
    color: var(--muted);
}

.ctx-ico-empty {
    visibility: hidden;
}

.ctx-item .ctx-label {
    flex: 1 1 auto;
    overflow: hidden;
    text-overflow: ellipsis;
}

.ctx-hint {
    flex: 0 0 auto;
    font-size: 11.5px;
    color: var(--muted);
}

.ctx-item:hover {
    background: var(--hover-overlay-2);
}

.ctx-item.ctx-danger,
.ctx-item.ctx-danger > i {
    color: var(--danger);
}

.ctx-item.ctx-danger:hover {
    background: rgba(248, 81, 73, 0.12);
}

/* 置灰项：仍可见（让用户知道有这个动作），但不响应 */
.ctx-item.ctx-disabled {
    opacity: 0.45;
    cursor: not-allowed;
}

.ctx-item.ctx-disabled:hover {
    background: transparent;
}

.ctx-sep {
    height: 1px;
    margin: 5px 8px;
    background: var(--line);
}

/* ---------- 工单批量操作条 ---------- */
.tk-batch {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 8px;
    margin: -2px 0 10px;
    padding: 8px 12px;
    background: var(--bg-2);
    border: 1px solid var(--primary);
    border-left-width: 3px;
    border-radius: var(--radius-sm);
}

.tk-batch-ico {
    color: var(--primary);
}

.tk-batch-count {
    font-size: 12.5px;
    font-weight: 500;
    color: var(--text);
}

.tk-batch-hint {
    font-size: 12px;
    color: var(--muted);
}

.tk-batch .spacer {
    flex: 1 1 auto;
}

/* 选中行：specificity 要压过 table.data-grid tbody tr:hover */
table.data-grid tbody tr.tk-sel {
    background: rgba(88, 166, 255, 0.12);
}


/* =========================================================
   v11：数据挂载（§4.8）+ 聚合口成员选择
   ========================================================= */
.agg-pick-list {
    max-height: 320px;
    overflow: auto;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    padding: 6px;
}

.agg-pick {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 5px 8px;
    border-radius: 6px;
    font-size: 12.5px;
    cursor: pointer;
    transition: background var(--dur-fast) var(--ease-out);
}

.agg-pick:hover { background: var(--hover-overlay); }

.agg-pick input { margin: 0; flex: 0 0 auto; }

/* 列表里"已删除"的挂载对端：变灰但仍可辨认 */
a.link-name + .badge-x.badge-danger { margin-left: 4px; }
/* ============================================================
   v12 新增：生命周期徽章 / 子网容量分段条 / 报表中心卡片
   ------------------------------------------------------------
   生命周期（采购中/在库/使用中/维修中/已报废）与「运行状态」
   （在线/离线/已关机）是两个独立维度，这里补一个紫色徽章把两者
   配色区分开，避免一眼看混。
   ============================================================ */
.badge-purple {
    background: rgba(163, 113, 247, 0.16);
    color: #A371F7;
    border-color: rgba(163, 113, 247, 0.42);
}

/* 子网容量分段条：蓝=已分配，紫=预留，灰=空闲
   （预留也算容量占用，所以不能只画一根「已分配」的条） */
.bar2 {
    display: flex;
    height: 6px;
    border-radius: 999px;
    overflow: hidden;
    background: var(--hover-overlay-2);
    margin-top: 8px;
}

.bar2 > i { display: block; height: 100%; }
.bar2 > .b2-alloc { background: linear-gradient(90deg, var(--primary), var(--accent)); transition: width 0.5s ease; }
.bar2 > .b2-res { background: linear-gradient(90deg, #8957E5, #A371F7); transition: width 0.5s ease; }
.bar2 > .b2-free { background: transparent; }
.bar2.warn > .b2-alloc { background: linear-gradient(90deg, #D29922, #E3B341); }
.bar2.danger > .b2-alloc { background: linear-gradient(90deg, #F85149, #FF9A94); }

/* 报表中心卡片 */
.report-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(420px, 1fr));
    gap: 14px;
}

.report-card {
    display: flex;
    align-items: center;
    gap: 14px;
    padding: 16px;
}

.report-icon {
    flex: 0 0 auto;
    width: 46px;
    height: 46px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 12px;
    font-size: 20px;
    color: var(--accent);
    background: linear-gradient(135deg, rgba(31, 111, 235, 0.22), rgba(88, 166, 255, 0.10));
    border: 1px solid rgba(88, 166, 255, 0.28);
}

.report-body { flex: 1 1 auto; min-width: 0; }
.report-name { font-weight: 700; font-size: 14px; margin-bottom: 3px; }
.report-desc { color: var(--muted); font-size: 12.5px; line-height: 1.55; }
.report-param {
    margin-top: 8px;
    font-size: 12.5px;
    color: var(--muted);
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 6px;
}
.report-param > * { white-space: nowrap; }
.report-param .inp { padding: 4px 8px; width: 70px; }
.report-go { flex: 0 0 auto; }

.cert-stat .stat-value { font-variant-numeric: tabular-nums; }

/* ================== 末尾追加区 ================== */

/* ---------- v13 网络拓扑 ---------- */
/* 画布分层：顶部工具栏/底部图例保持 --bg-0（最深），画布主体用稍浅的 --bg-1 + 圆角 + 细边框，
   让拓扑区域从黑色背景里「浮」出来，形成「深顶 → 浅中 → 深底」的层次感。 */
#topoHost {
    background: var(--bg-1);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: 10px;
    overflow-x: auto;
}
html[data-theme="light"] #topoHost {
    background: var(--bg-1);
    border-color: var(--line-strong);
}
.topo-scroll { overflow-x: auto; max-height: none; padding: 6px 2px 14px; }
.topo-layer-name { font-size: 12px; fill: var(--muted); }
.topo-edge { fill: none; stroke: var(--line); stroke-width: 1.6; transition: opacity .15s, stroke .15s; }
.topo-host { stroke-dasharray: 5 4; stroke-width: 1.4; }
/* v15.3 机房视图：推断互联（待确认）用琥珀虚线，设备归属边用淡虚线 */
/* 提权到 (0,1,1)：拓扑自身在 SVG 内联 <style> 里写了 .topo-edge{stroke:#64748B}，
   内联 <style> 位于 app.css 之后，同为 (0,1,0) 时 stroke 会被它覆盖
   （实测机房间推断互联边虚线生效、颜色却仍是 #64748B）。 */
path.topo-site-infer { stroke-dasharray: 6 4; stroke: #BA7517; stroke-width: 2.8; }
/* 机房间互联边加粗：缩到「公司视角」后它是唯一要回答的问题（机房与机房靠什么连），
   原先与普通终端-设备连线同为 1.7px，缩略后几乎看不见。
   同样提权到 (0,1,1) 才盖得过拓扑内联 <style> 里的 .topo-edge{stroke-width:1.7}。 */
path.topo-site-inter { stroke-width: 2.8; }
path.topo-site-home { stroke-dasharray: 3 3; opacity: .45; }
.topo-dimming .topo-edge { opacity: .12; }
.topo-dimming .topo-edge-on { opacity: 1; stroke: var(--primary); stroke-width: 2.4; }
.topo-dimming .topo-node { opacity: .25; }
.topo-dimming .topo-node-on { opacity: 1; }
.topo-node { transition: opacity .15s, filter .15s; }
.topo-node-name { font-size: 12.5px; font-weight: 600; fill: #FFFFFF; }
.topo-node-ip { font-size: 10.5px; fill: rgba(255, 255, 255, .85); }
/* 节点 hover 发光：玻璃卡片 hover 时整卡泛柔和辉光 + 描边提亮（液态玻璃风） */
.topo-node:hover { filter: drop-shadow(0 0 8px rgba(88, 166, 255, .5)); }

/* v15 网络拓扑图增强 */
.topo-panel-title { font-size: 13px; font-weight: 700; }
.topo-edge-stub { stroke-dasharray: 4 4; opacity: .55; }
.topo-node-rect { opacity: .96; transition: filter .12s, stroke .12s; }
.topo-node-host { stroke: #F59E0B; stroke-width: 2; stroke-dasharray: 4 3; }
.topo-node-vmchild { opacity: .86; }
.topo-node-icon { font-size: 15px; font-weight: 800; fill: #FFFFFF; }
.topo-node:hover .topo-node-rect { opacity: 1; }

/* ---- v17 拓扑图形化连线（eNSP 式） ---- */
.topo-draft-line { fill: none; stroke: #F59E0B; stroke-width: 2; stroke-dasharray: 6 4;
    pointer-events: none; stroke-linecap: round; }
.topo-node-linksrc .topo-node-rect { stroke: #F59E0B; stroke-width: 2.5; }
.topo-node-linksrc { filter: drop-shadow(0 0 4px rgba(245, 158, 11, 0.55)); }

.topo-tooltip {
  position: fixed;
  z-index: 3000;
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 10px 12px;
  font-size: 12.5px;
  box-shadow: 0 8px 24px rgba(0,0,0,.14);
  pointer-events: none;
  max-width: 260px;
  display: none;
}

.topo-side-panel {
  width: 270px;
  min-width: 270px;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 8px;
  box-shadow: 0 4px 16px rgba(0,0,0,.08);
  overflow: hidden;
}
.topo-side-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px 12px;
  border-bottom: 1px solid var(--line);
  background: var(--bg2);
}
.topo-side-body { padding: 10px 12px; }
.topo-side-foot { padding: 10px 12px; border-top: 1px solid var(--line); }
.topo-side-row { display: flex; justify-content: space-between; gap: 10px; padding: 4px 0; font-size: 12.5px; }
.topo-side-section { font-size: 12px; font-weight: 700; margin: 8px 0 4px; color: var(--muted); }
.topo-side-vm { display: flex; align-items: center; gap: 8px; padding: 5px 8px; border-radius: 6px; cursor: pointer; font-size: 12px; }
.topo-side-vm:hover { background: var(--bg2); }
.topo-vm-dot { width: 8px; height: 8px; border-radius: 50%; }

#topoControls .switch-x { display: inline-flex; align-items: center; gap: 6px; font-size: 12.5px; cursor: pointer; }
#topoControls .switch-x input { cursor: pointer; }

/* v15.2 泳道分层拓扑 */
.topo-lane-label { cursor: pointer; transition: filter .12s; }
.topo-lane-label:hover { filter: brightness(1.06); }
.topo-grp { cursor: pointer; }
.topo-grp-rect { transition: filter .12s; }
.topo-grp:hover .topo-grp-rect { filter: brightness(1.08) drop-shadow(0 3px 6px rgba(0,0,0,.22)); }
.topo-edge-dev { stroke-width: 1.1; stroke-dasharray: 3 3; opacity: .8; }
.topo-edge-agg { stroke-dasharray: 5 4; }
.topo-scroll { overflow-x: auto; }

/* ============================================================
   v18: 机柜设备栏吸顶跟随（页面再长，拖拽源始终可见）
   ============================================================ */
.rack-layout > .rack-right {
    position: sticky;
    top: 10px;
    max-height: calc(100vh - 20px);
    overflow-y: auto;
}

/* ============================================================
   v20: 键盘快捷键（P2-9）—— 帮助面板与按键样式
   ------------------------------------------------------------
   只用主题变量（--text/--muted/--bg-1/--line），深浅两套主题共用一份，
   对比度由 tools/devcheck/contrastprobe.html 逐页采样验证，不做主题分叉。
   ============================================================ */
.kbd-help { font-size: 13px; }
.kbd-help .kbd-sec { font-weight: 600; color: var(--text); margin-bottom: 6px; }
.kbd-help .kbd-sec:not(:first-child) { margin-top: 14px; }
.kbd-table { width: 100%; border-collapse: collapse; }
.kbd-table td { padding: 4px 6px; vertical-align: middle; border-bottom: 1px dashed var(--line); }
.kbd-table tr:last-child td { border-bottom: none; }
.kbd-table td:first-child { width: 110px; white-space: nowrap; }
.kbd-help kbd {
    display: inline-block; min-width: 18px; padding: 1px 6px; margin-right: 2px;
    font-family: Consolas, monospace; font-size: 12px; line-height: 18px; text-align: center;
    color: var(--text); background: var(--bg-1);
    border: 1px solid var(--line); border-bottom-width: 2px; border-radius: 5px;
}

/* ============================================================
   v19: 打印样式 —— 台账列表 / 详情弹层 / 报表中心，Ctrl+P 出 A4 文档
   ------------------------------------------------------------
   为什么要有这一整块：全站此前没有任何 @media print，唯一能看的打印
   是「资产标签」（views-assets.js / views-netiface.js 自开窗口内联样式）。
   列表页直接 Ctrl+P 会连侧栏、顶栏、深色底一起打，还会被下面三处裁切/错位：
     - .table-scroll (L840)   overflow:auto; max-height: calc(100vh - 288px)
                              -> 只出可视区那十几行，其余全被裁掉
     - thead th       (L853)  position:sticky; z-index:3; background:var(--inp-bg)
                              -> 纸上错位、不透明底色压住首行，且表头不会每页重复
     - .modal-body    (L1182) max-height: calc(100vh - 240px); overflow:auto -> 弹层截断
   另外默认主题是深色（app.css:11 起），纸张必须显式强制白底黑字，
   不能依赖 --bg/--text 这些变量。下面所有 !important 都是为压掉上述
   定位 / 裁切 / 配色而写；半年后要动这里，先回去看这几行的原始定义，
   别把 !important 当噪音删掉。
   ============================================================ */
@page { size: A4; margin: 12mm; }

@media print {
  /* 纸张统一白底黑字；去掉网页高度/滚动约束，让正文自然铺满整页 */
  html, body { background: #fff !important; color: #000 !important; }
  html, body, .app-shell { height: auto !important; overflow: visible !important; }

  /* --- 场景 A：没开弹层 -> 只打印当前视图正文 --- */
  .view:not(.active) { display: none !important; }
  .topbar, .sidebar, .sidebar-toggle, .toolbar, .pager,
  .btn-icon, .ctx-menu, .hint, .no-print { display: none !important; }
  /* .main 原本有 margin-left:var(--sidebar-w) 与顶部留白，纸上要归零，否则左侧空一条 */
  .main { margin: 0 !important; padding: 0 !important; overflow: visible !important; max-height: none !important; }

  /* 卡片 / 统计卡：去背景、去阴影、去彩色边框，省墨且不糊成一团 */
  .card-x, .stat-card {
    background: #fff !important; box-shadow: none !important;
    border-color: #ccc !important; border-radius: 0 !important;
  }

  /* 表格：解开滚动裁切；表头不再吸顶、每页重复；行不跨页断开；允许换行防横向溢出被切 */
  .table-scroll { max-height: none !important; overflow: visible !important; }
  table.data-grid { font-size: 11px !important; }
  table.data-grid thead { display: table-header-group; }
  table.data-grid thead th {
    position: static !important; z-index: auto !important;
    background: #fff !important; color: #000 !important;
    border-bottom: 1px solid #999 !important;
  }
  table.data-grid tbody tr { page-break-inside: avoid; }
  table.data-grid th, table.data-grid td { white-space: normal !important; }

  /* --- 场景 B：开着弹层（body.modal-open）-> 只打最顶层弹层内容，页面背景不进纸 --- */
  /* body.modal-open 由 js/api.js 的 openModal / closeModal 维护（见那里注释） */
  body.modal-open .app-shell { display: none !important; }
  body.modal-open .modal-host { display: block !important; position: static !important; }
  /* 蒙层原本 position:fixed + 深色底 + backdrop-filter 毛玻璃，纸上一律还原成普通块。
     必须带 .show：js/api.js 的 closeModal 是延时 160ms 才把蒙层移出 DOM，
     若不带 .show，刚被关掉、还在 DOM 里那一层会命中本规则、被渲染成实心块
     并真的打进 PDF（独立验证已用 printToPDF 复现）。带 .show 后它落回基础
     `.modal-mask{display:none}`；下层 `.under` 规则在本规则之后、特异性相同
     (0,3,1)，仍会覆盖它，故「只打最顶层」的语义不变。 */
  body.modal-open .modal-mask.show {
    display: block !important;
    position: static !important; inset: auto !important; padding: 0 !important;
    overflow: visible !important; background: none !important;
    backdrop-filter: none !important; z-index: auto !important; animation: none !important;
  }
  /* 多层叠加时只打最顶层：下层带 .under，隐藏掉 */
  body.modal-open .modal-mask.under { display: none !important; }
  body.modal-open .modal-box {
    max-width: none !important; width: auto !important;
    border: 0 !important; border-radius: 0 !important;
    box-shadow: none !important; background: #fff !important; animation: none !important;
  }
  body.modal-open .modal-body {
    max-height: none !important; overflow: visible !important; padding: 0 !important;
  }
  /* 关闭叉与底部按钮区不进纸 */
  body.modal-open .modal-foot,
  body.modal-open .modal-head .close-x { display: none !important; }
}


/* ============================================================
   v22 月度报告（P2-10）
   ------------------------------------------------------------
   只用主题变量（--text / --muted / --bg-1 / --bg-2 / --line /
   --primary / --accent / --white …），深浅两套主题共用一份，不做主题分叉。
   配色全部沿用 .stat-card / .card-x / .data-grid 的既有值，不引入新色值 ——
   全站刚做过对比度达标（正文 >=4.5:1、大字 >=3:1），新增样式必须站在同一
   标准上，所以这里只搭骨架、不另调色。
   ============================================================ */

/* 顶部一行：月份选择 + 查询 + 导出。
   窄屏换行，不横向撑破页面（1920 与 1366 两档都要能看）。 */
.monthly-bar {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    margin-bottom: 14px;
}

.monthly-bar .monthly-lb {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-size: 12.5px;
    color: var(--muted);
    white-space: nowrap;
}

.monthly-bar .monthly-lb i {
    color: var(--accent);
}

.monthly-bar input[type="month"].inp {
    width: 168px;
    flex: 0 0 auto;
}

/* 对比月份 / 导出说明：一行灰字，不抢视觉重心 */
.monthly-hint {
    font-size: 12px;
    color: var(--muted);
}

/* 面板之间的竖向间距（.card-x 自身没有 margin） */
.monthly-panel {
    padding: 14px;
    margin-top: 14px;
}

.monthly-panel .card-title {
    margin-bottom: 10px;
}

/* 表格下方的合计 / 条数小结：放在 .table-scroll 之外，滚表格时始终可见 */
.monthly-total {
    margin-top: 8px;
    font-size: 12.5px;
    color: var(--muted);
}

/* 类型键（physical / virtual …）用等宽字，扫一眼就能和中文标签区分开 */
.monthly-mono {
    font-family: "JetBrains Mono", "Cascadia Mono", Consolas, monospace;
    font-size: 12px;
}

.monthly-loading {
    padding: 12px 2px;
    font-size: 13px;
    color: var(--muted);
}

.monthly-loading i {
    margin-right: 6px;
    color: var(--accent);
}

/* 数字列（新增/变更/下线/合计）用等宽数字，上下行位数对得齐 */
.monthly-panel table.data-grid td:nth-child(n+3) {
    font-variant-numeric: tabular-nums;
}

@media (max-width: 1100px) {
    .monthly-bar input[type="month"].inp {
        width: 150px;
    }
    .monthly-panel {
        padding: 12px;
    }
}

/* 打印：本视图特有的两条。表格滚动裁剪的通用解绑在 v19 的 @media print 里
   （.table-scroll { max-height:none !important }），这里不重复写。 */
@media print {
    .monthly-bar {
        margin-bottom: 8px;
    }
    .monthly-panel {
        padding: 0;
        margin-top: 10px;
    }
}

/* ============================================================
   v23 概览看板 · 图表模块（纯 SVG / CSS 手绘，零第三方库）
   ------------------------------------------------------------
   配套 static/js/views-overview-charts.js。卡片仍由 modules.js 的
   「添加 / 隐藏 / 上移 / 下移」统一管理（布局存 settings.ui_layout）。

   ⚠️ 改这里之前先读这五条：
   1) 本区块在文件最末尾，**排在 @media print 区块之后**。要覆盖打印表现
      只能写在本区块自带的 @media print 里，且必须带 !important。
   2) 柱状图 .cb 的 height:% 只能相对 .cw 取值 —— .cw 是柱子的专属高度
      上下文。若把百分比放在同时含「数字 .cv + 标签 .cl」的 .col 上，
      高度会被上下文字挤掉，41 和 81 会画成差不多高（比例失真）。
   3) 卡片内文字颜色一律**继承**，不要写死 color: var(--text)。.card-x 本身
      不设 color，所以纸上能靠 @media print 的 color:#000 自动变黑；写死了
      就管不到，深色主题会打出「白字白纸」。SVG 文字同理用 fill="currentColor"。
   4) 环形图的底环、条形图的轨道、证书进度条的槽统一用 --chart-track，
      不要直接用 var(--bg-3)：深色主题下 --bg-3 是近黑色，压到白纸上是一条
      黑带；本变量在打印时会自动换成浅灰。
   5) 同一个坑还有两个变量：--panel-a（.mini / .cert .ci 的底色）与 --muted
      （卡片内的灰字，深色主题下是 #9BA3AD），另外 --text 被 .mnote b 用着。
      v19 的通用打印块只兜 .card-x / .stat-card **自身**，卡片内部的深底浅字
      它管不到 —— 必须在本区块自带的打印块里显式压掉，见下面第 ①② 条。
   ============================================================ */
.mchart {
    grid-column: span 2;              /* 跨 2 列（.mod-grid 是 212px 起的最小列宽） */
    min-width: 0;                     /* 防止内层 SVG / 长文本把网格撑破 */
    --chart-track: var(--bg-3);
}
.mchart.wide { grid-column: span 4; }
/* 窄屏放不下两列时退回整行，避免图表被压成细条 */
@media (max-width: 1400px) { .mchart, .mchart.wide { grid-column: 1 / -1; } }

/* 卡片底部那行补充说明 */
.mchart .mnote { margin-top: 11px; font-size: 11.5px; color: var(--muted); line-height: 1.6; }
.mchart .mnote b { color: var(--text); }

/* --- 环形图 / 进度环 --- */
.dn-wrap { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
.dn { position: relative; flex: 0 0 auto; }
.dn svg { display: block; }
.dn .mid {
    position: absolute; inset: 0; display: flex; flex-direction: column;
    align-items: center; justify-content: center; pointer-events: none;
}
.dn .mid .n {
    font-family: "JetBrains Mono", Consolas, monospace;
    font-size: 25px; font-weight: 700; line-height: 1; letter-spacing: -.5px;
}
.dn .mid .t { font-size: 11px; color: var(--muted); margin-top: 3px; }

/* 图例（环形图右侧） */
.lgd { display: flex; flex-direction: column; gap: 7px; min-width: 158px; flex: 1; }
.lgd .r { display: flex; align-items: center; gap: 9px; font-size: 12.5px; }
.lgd .dot { width: 10px; height: 10px; border-radius: 3px; flex: 0 0 auto; }
.lgd .nm { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lgd .vv { font-family: "JetBrains Mono", Consolas, monospace; font-weight: 600; font-size: 12.5px; }
.lgd .pc { color: var(--muted); font-size: 11.5px; width: 44px; text-align: right; }

/* --- 水平条形（在线状态 / 机柜 U 位 / 操作热度 / 子网 / 端口 …）--- */
.hb { display: flex; flex-direction: column; gap: 10px; }
.hb .row { font-size: 12.5px; }
.hb .top { display: flex; justify-content: space-between; margin-bottom: 5px; gap: 8px; }
.hb .top .n { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hb .top .v {
    font-family: "JetBrains Mono", Consolas, monospace;
    color: var(--muted); font-size: 11.5px; flex: 0 0 auto;
}
.hb .track {
    height: 7px; border-radius: 4px; overflow: hidden;
    background: var(--chart-track); display: flex;
}
.hb .track span { height: 100%; display: block; }
.hb .track span + span { margin-left: 1px; }

/* --- 柱状图（近 7 天操作趋势）--- */
.cols { display: flex; align-items: flex-end; justify-content: space-between; gap: 10px; height: 150px; }
.col { flex: 1; display: flex; flex-direction: column; align-items: center; height: 100%; min-width: 0; }
.col .cv {
    font-family: "JetBrains Mono", Consolas, monospace;
    font-size: 11px; margin-bottom: 4px; flex: 0 0 auto;
}
/* 柱区专属高度上下文：.cb 的 height:% 相对这一层算，见文件头第 2 条 */
.col .cw { flex: 1 1 auto; width: 100%; display: flex; align-items: flex-end; justify-content: center; min-height: 0; }
.col .cb {
    width: 100%; max-width: 38px; min-height: 3px;
    border-radius: 5px 5px 2px 2px;
    background: linear-gradient(180deg, var(--accent), var(--primary));
    transition: opacity .15s;
}
.col .cb:hover { opacity: .82; }
.col .cl { font-size: 10.5px; color: var(--muted); margin-top: 6px; white-space: nowrap; flex: 0 0 auto; }

/* --- 迷你指标（基础设施与安全）--- */
.mgrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(118px, 1fr)); gap: 10px; }
.mini { border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 10px 12px; background: var(--panel-a); }
.mini .mn { font-size: 11.5px; color: var(--muted); display: flex; align-items: center; gap: 6px; }
.mini .mv { font-family: "JetBrains Mono", Consolas, monospace; font-size: 20px; font-weight: 700; margin-top: 3px; }
.mini .ms { font-size: 11px; color: var(--muted); }

/* --- 证书到期列表 --- */
.cert { display: flex; flex-direction: column; gap: 10px; }
.cert .ci { border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 10px 12px; background: var(--panel-a); }
.cert .ci .d { font-size: 13px; font-weight: 600; }
.cert .ci .sub { font-size: 11.5px; color: var(--muted); margin-top: 2px; }
.cert .ci .bar { height: 6px; border-radius: 3px; background: var(--chart-track); margin-top: 8px; overflow: hidden; }
.cert .ci .bar span { display: block; height: 100%; background: linear-gradient(90deg, var(--ok), #7ee787); }
.cert .empty { font-size: 12.5px; color: var(--muted); }

/* --- 图表卡印到纸上的修正 --- */
@media print {
    /* 条/柱的填充全部是 background，浏览器默认不打印背景 —— 不强制的话纸上只剩空白。
       必须在 .mchart 上开，子元素靠继承生效。 */
    .mchart {
        print-color-adjust: exact;
        -webkit-print-color-adjust: exact;
        page-break-inside: avoid;
        --chart-track: #e8e8e8 !important;   /* 见文件头第 4 条 */
    }
    .mchart .cols { height: 118px; }

    /* ① 卡片**内部**子元素的白底。深色主题（app.css:11 起，默认就是它）下
       .mini / .cert .ci 是 background:var(--panel-a)=rgba(13,17,23,.78)，
       上面 v19 的通用打印块只兜了 .card-x 与 .stat-card 自身，没管卡片内部，
       于是这两个小方块压到白纸上就是一块深灰；边框 --line 也淡到看不见。
       ⚠️ 只兜 .mchart 范围内 —— .mini / .cert .ci 目前只出现在概览图表卡里，
       别顺手放大到全站（将来别处用到再按需补）。 */
    .mchart .mini,
    .mchart .cert .ci {
        background: #fff !important;
        border-color: #ccc !important;
    }

    /* ② 灰字。--muted 是配深色底的浅灰 #9BA3AD（app.css:21），白纸上对比度约
       2.6:1，低于 4.5:1；--text 是近白 #E6EDF3，.mnote b 在白纸上几乎看不见。
       打印态一律压成深灰，与主题无关地保住可读性。 */
    .mchart .mnote,
    .mchart .mnote b,
    .mchart .dn .mid .t,
    .mchart .lgd .pc,
    .mchart .hb .top .v,
    .mchart .col .cl,
    .mchart .mini .mn,
    .mchart .mini .ms,
    .mchart .cert .ci .sub,
    .mchart .cert .empty {
        color: #333 !important;
    }

    /* ③ 条形图里「未用 / 占位」那一段（.hb-rest 由 views-overview-charts.js 的
       stackBars() 与端口卡生成）。它原本是 --hover-overlay-2 = rgba(255,255,255,.1)，
       叠在白纸上比纸还淡、等于没有 —— 而端口卡文案写着「显示为占位」，语义就落空了。
       打印态换成深色低透明度，占位才看得见。
       ⚠️ 用类名而不是 [style*=...] 属性选择器：那个值里带 var()，换个写法就失配。 */
    .mchart .hb .track span.hb-rest {
        background: rgba(0, 0, 0, 0.12) !important;
    }
}


/* ============================================================
   v36 概览看板 · 吸附式网格排版（需求 5：模块可拖动换位 / 可拖右下角改大小）
   ------------------------------------------------------------
   配套 static/js/modules.js 的「概览看板：吸附式网格排版」区块。改这里之前先读：

   1) 模块坐标是 modules.js 写在 .mod-cell 上的 **inline 样式**
      （grid-column / grid-row / --mod-h）。inline 只有 !important 压得住 ——
      本区块末尾的 @media print 正是为此而写，别把 !important 当噪音删掉。
   2) 本区块在文件最末尾，排在 v23 的 @media print 之后，所以打印修正
      只能写在本区块自带的 @media print 里。
   3) 三处常量必须与 JS 一一对应，改一个就要改两处，否则吸附错格：
        --ov-cols: 12     <-> App.OV_COLS
        --ov-row-h: 96px  <-> App.OV_ROW_H
        gap: 14px         <-> App.OV_GAP
   4) 断点 1279px 与 App.OV_NARROW 一致：窄屏由 JS 清掉 inline 坐标，
      这里用媒体查询接管成流式排列（两列，统计卡半行）。
   ============================================================ */

/* --- ⠿ 拖动握把：模块唯一可拖的地方 --- */
.mod-grip {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 18px;
    height: 22px;
    margin-right: 3px;
    border-radius: 5px;
    color: var(--muted);
    font-size: 13px;
    line-height: 1;
    cursor: grab;
    user-select: none;
    -webkit-user-select: none;
    opacity: .5;
    transition: opacity .15s, background .15s, color .15s;
}
.mod-grip:hover { opacity: 1; background: var(--hover-overlay-2); color: var(--text); }
.mod-grip:active { cursor: grabbing; }
/* 统计卡没有 .card-head，握把放到卡片右上角的空白处（避开标签与数字）。
   卡片自身有 onclick，握把上带了 stopPropagation，点它不会跳转。 */
.mod-grip-corner {
    position: absolute;
    right: 4px;
    top: 4px;
    margin-right: 0;
    z-index: 5;
}

/* --- ◢ 右下角缩放手柄 ---
   必须是真能命中的元素：16×16 实体盒 + z-index 6，压在卡片之上
   （注：.stat-card 的装饰光晕层已于 v39.5 下线）。
   常态就可见（opacity .45），不做「hover 才出现」——
   避免出现「看着有、点不到」。 */
.mod-cell > .mod-size {
    position: absolute;
    right: 0;
    bottom: 0;
    width: 16px;
    height: 16px;
    z-index: 6;
    cursor: se-resize;
    opacity: .45;
    transition: opacity .15s;
}
.mod-cell > .mod-size::before {
    content: "";
    position: absolute;
    right: 1px;
    bottom: 1px;
    width: 0;
    height: 0;
    border-style: solid;
    border-width: 0 0 11px 11px;
    border-color: transparent transparent var(--muted) transparent;
}
.mod-cell:hover > .mod-size,
.mod-cell.is-resizing > .mod-size { opacity: 1; }

/* --- 图表响应式：拉宽 / 拉高之后图形真的跟着变 ---
   环形图与进度环的 <svg> 带固定 width/height **属性**，而属性只是表示性提示，
   CSS 一句就能覆盖。viewBox 本来就在（donut / ring 都写了 0 0 size size），
   所以只给宽度、高度 auto —— 长宽比自动保持，不会拉成椭圆。
   .dn 从「固定 140px、不许伸缩」改成「可以长，但最多占卡片 42% / 260px」；
   .dn .mid 是 absolute inset:0，跟着一起缩放，环心数字始终居中。 */
.mod-cell .mchart .dn {
    flex: 1 1 140px;
    /* min-width 压住 max-width（CSS 里 min-* 优先）：环最窄 132px，
       否则卡片被拖到最窄时 46% 会把 140px 的环压到 100px 出头。 */
    min-width: 132px;
    max-width: min(46%, 260px);
}
.mod-cell .mchart .dn svg { width: 100%; height: auto; display: block; }
/* 柱状图高度跟模块的 h（格数）走：modules.js 把 h 写进 --mod-h。
   拉高一个柱状模块，柱子真的会长高，而不是只多出一块空白。 */
.mod-cell .mchart .cols { height: calc(96px + (var(--mod-h, 1) - 1) * 110px); }

/* --- 窄屏降级：忽略坐标，按 order 流式排列 --- */
@media screen and (max-width: 1279px) {
    .mod-grid { --ov-cols: 2; }
    .mod-grid > .mod-cell { grid-column: span 2; }
    .mod-grid > .mod-cell[data-narrow="half"] { grid-column: span 1; }
    /* 小屏拖不动（modules.js 里也拦了），把手藏掉免得误导；
       换位置/隐藏仍然可以用 ⋯ 菜单里的上移 / 下移 / 隐藏。 */
    .mod-grip, .mod-cell > .mod-size { display: none; }
}

/* --- 打印：纸上不按屏幕坐标摆 ---
   屏幕坐标是 inline 的 grid-column / grid-row，只有 !important 压得住。
   这里直接把网格拆掉（display:block），模块按 DOM 顺序纵向流式排列 ——
   DOM 顺序 = order，所以印出来是一列「顺序正确、不重叠、无空洞」的卡片，
   不会是一条条错乱的长条加空洞。 */
@media print {
    .mod-grid {
        display: block !important;
        grid-template-columns: none !important;
        grid-auto-rows: auto !important;
        --ov-cols: 1;
        gap: 0 !important;
    }
    .mod-grid > .mod-cell {
        display: block !important;
        grid-column: auto !important;
        grid-row: auto !important;
        width: auto !important;
        height: auto !important;
        margin: 0 0 10px !important;
        page-break-inside: avoid;
        break-inside: avoid;
    }
    .mod-cell > .card-x,
    .mod-cell > .stat-card { height: auto !important; }
    .mod-grip,
    .mod-cell > .mod-size { display: none !important; }
    /* 还原 v23 的纸上尺寸：柱状图固定高度、环形图固定 128px，
       不让「按格数算高度」和「随卡片宽度放大」带到纸上。 */
    .mod-cell .mchart .cols { height: 118px !important; }
    .mod-cell .mchart .dn { flex: 0 0 auto; max-width: none; }
    .mod-cell .mchart .dn svg { width: 128px !important; height: 128px !important; }
}

/* ============================================================
   端口面板：拖拽排序握把 + 插入提示 + 聚合口成员缩进
   ------------------------------------------------------------
   1) 插入提示一律用 **inset box-shadow**，不用 border ——
      table.data-grid 是 border-collapse:separate，给 tr 加 border 会改行高、
      把下方所有行整体顶一下；inset 阴影画在行内部，零布局影响。
   2) 装饰只用一个真实 DOM 节点（握把图标），**不引入 ::before/::after**：
      项目踩过"大尺寸负偏移伪元素盖在内容上形成点击黑洞、编辑/删除图标点不动"的坑。
      握把上没有任何可交互子元素，也不会去和状态列的下拉抢事件。
   3) 颜色全部走主题变量（--accent / --line），深浅两套主题共用，不硬编码。
   ============================================================ */
.iface-grip {
    cursor: grab;
    margin-right: 6px;
    opacity: .45;
    user-select: none;
    -webkit-user-select: none;
}

.iface-grip:hover {
    opacity: 1;
}

.iface-grip:active,
tr.dragging .iface-grip {
    cursor: grabbing;
}

/* 成员行上的等宽隐形占位：只占位对齐，不可点、不接收拖拽 */
.iface-grip-ph {
    visibility: hidden;
    cursor: default;
    pointer-events: none;
}

/* 拖动中的源行 */
.table-scroll tbody tr.dragging {
    opacity: .55;
}

/* 插入提示：inset 阴影，零布局影响（见本区块开头第 1 条） */
.table-scroll tbody tr.drop-hint-top {
    box-shadow: inset 0 2px 0 0 var(--accent);
}

.table-scroll tbody tr.drop-hint-bottom {
    box-shadow: inset 0 -2px 0 0 var(--accent);
}

/* 聚合口（父行）的接口名加粗，和下面的成员口拉开层级 */
.iface-parent {
    font-weight: 600;
}

/* 成员口接口名：缩进 + 左侧竖线，风格对齐 .org-rooms / .mgr-room。
   ⚠️ margin-left 取 18px（不是照抄 .mgr-room 的 10px）：成员行的接口名元素相对
   聚合口那一行的接口名元素要保证错开 ≥16px，18px 留出余量，不会卡在边界上。
   文字实际内缩 = 18(缩进线位置) + 2(线) + 14(padding) = 34px。 */
.iface-child {
    display: inline-block;
    margin-left: 18px;
    padding-left: 14px;
    border-left: 2px solid var(--line);
}

/* 聚合口成员口的展开 / 收缩三角（默认收缩，见 views-netiface.js 的 App.aggOpenMap）。
   ⚠️ 宽度 13px 是**契约**：成员行上补了一个 .iface-agg-tg-ph 等宽隐形占位，
   两者必须等宽，否则聚合口行与成员行的接口名会错开、「成员口挂在聚合口下面」
   的缩进关系会整个反过来。改宽度时两处一起改。
   ⚠️ 刻意**不复用** i.act-x：那是 26x26 的方块（app.css:3515），塞进接口名单元格
   会把名称整体推远、行高也被撑开。 */
.iface-agg-tg {
    display: inline-block;
    width: 13px;
    height: 13px;
    line-height: 13px;
    margin-right: 5px;
    font-size: 10px;
    text-align: center;
    color: var(--muted);
    cursor: pointer;
    user-select: none;
    -webkit-user-select: none;
    /* 过渡只做 color + transform：不动 width/height（13px 是契约，见上）。 */
    transition: color var(--dur-fast) var(--ease-out),
                transform var(--dur-fast) var(--ease-out);
}

.iface-agg-tg:hover {
    color: var(--accent);
    transform: scale(1.18);
}

/* 展开态：同一个字形旋转 90°（配合 JS 里固定的 fa-chevron-right）。
   旋转与悬停缩放会产生 transform 覆盖，所以组合态必须自己写全。 */
.iface-agg-tg.is-open {
    color: var(--accent);
    transform: rotate(90deg);
}

.iface-agg-tg.is-open:hover {
    transform: rotate(90deg) scale(1.18);
}

/* 按下瞬间轻微回缩，"点了"有即时反馈 */
.iface-agg-tg:active {
    transform: scale(0.86);
}

.iface-agg-tg.is-open:active {
    transform: rotate(90deg) scale(0.86);
}

/* 成员行上的等宽隐形占位：只占位对齐，不可点 */
.iface-agg-tg-ph {
    visibility: hidden;
    cursor: default;
    pointer-events: none;
}

/* 「全部展开 / 全部收起」总开关。
   ⚠️ 刻意**不复用 .btn-x**：btn-x 是 30px 高，它和标题同处 .block-title 这一行，
   会把整行高度顶高、与上方区块的间距错位。做成 22px 的小胶囊。
   ⚠️ 颜色一律走主题变量（--accent / --line-strong / --hover-overlay-2），
   深浅两套主题自适应，不硬编码 RGB（本文件早期硬编码 88,166,255 的教训）。 */
.iface-agg-all {
    height: 22px;
    padding: 0 9px;
    border-radius: 11px;
    border: 1px solid var(--line-strong);
    background: transparent;
    color: var(--muted);
    font-size: 11.5px;
    line-height: 1;
    font-family: var(--font);
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    gap: 5px;
    white-space: nowrap;
    transition: background-color var(--dur-fast) var(--ease-out),
                border-color var(--dur-fast) var(--ease-out),
                color var(--dur-fast) var(--ease-out),
                transform var(--dur-fast) var(--ease-out);
}

.iface-agg-all:hover {
    color: var(--accent);
    border-color: var(--accent);
    background: var(--hover-overlay-2);
    transform: translateY(-1px);
}

.iface-agg-all:active {
    transform: translateY(0) scale(0.95);
}

/* 聚合口（父）行：左侧一条 2px 强调竖线，把「这一组」的范围画出来。
   ⚠️ 用 inset box-shadow 而不是 border —— table.data-grid 是 border-collapse:separate，
   给 tr 加 border 会改行高、把下方所有行整体顶一下（见本文件 iface-grip 区块第 1 条）。
   ⚠️ 刻意**不给整行加底色**：静态底色会把聚合口行伪装成 hover 态（浅色主题下尤甚），
   而且底色只能硬编码 RGB —— 竖线走 var(--accent)，深浅两套主题自适应。
   ⚠️ 拖拽插入提示（.drop-hint-top/bottom）也只写 box-shadow，同为"单类+元素"特异性，
   谁在后面谁赢 —— 本区块排在它们之后，所以必须自己把两种情况都补全，
   否则拖动聚合口行时**看不到插入位置提示**。 */
.table-scroll tbody tr.iface-row-agg {
    box-shadow: inset 2px 0 0 0 var(--accent);
}

.table-scroll tbody tr.iface-row-agg.drop-hint-top {
    box-shadow: inset 2px 0 0 0 var(--accent), inset 0 2px 0 0 var(--accent);
}

.table-scroll tbody tr.iface-row-agg.drop-hint-bottom {
    box-shadow: inset 2px 0 0 0 var(--accent), inset 0 -2px 0 0 var(--accent);
}

/* 成员行的缩进竖线：静态用 --line-strong 划出层级，鼠标落在该行时点亮成 --accent，
   提示"这一行挂在上面那个聚合口下"。
   ⚠️ transition 必须写在**基类**上：写在 :hover 里只有"进入"有动画、"退出"是瞬变。 */
.table-scroll tbody tr .iface-child {
    transition: border-left-color var(--dur-fast) var(--ease-out);
}

.table-scroll tbody tr.iface-row-mem .iface-child {
    border-left-color: var(--line-strong);
}

.table-scroll tbody tr:hover .iface-child {
    border-left-color: var(--accent);
}

/* 端口面板内的按钮：按下时轻微收缩。全局 .btn-x:active 只有 translateY，
   没有"按下去"的手感；这里只作用于 #ifaceSection，不碰全局。 */
#ifaceSection .btn-x:active {
    transform: translateY(0) scale(0.97);
}

/* 纸上不留拖拽握把（本区块在文件最末尾，打印修正只能写在这里，故带上 !important） */
@media print {
    .iface-grip {
        display: none !important;
    }
}
/* ============================================================
   v24 概览看板 · 图表显示类型（5 档）
   ------------------------------------------------------------
   配套 static/js/views-overview-charts.js 的 trend / tiles / table。
   本区块同样在文件最末尾（排在所有 @media print 之后），
   所以打印覆盖必须写在自己这段 @media print 里并带 !important。
   1) 折线用非等比铺满（preserveAspectRatio="none"），线宽靠 SVG 的
      vector-effect="non-scaling-stroke" 保持恒定；SVG 内**没有文字**，
      标签全在 HTML 里，避免被非等比缩放压扁。
   2) 表格只让 .ctbl-wrap 滚，卡片自身不滚；表头 sticky，底色走 --panel-b
      （深色主题下是深色，打印时被下面的 @media print 压成白底黑字）。
   3) 数字磁贴复用 .mgrid / .mini，不新增样式。
   ============================================================ */

/* ---- 折线走势 ---- */
.card-x .tl {
    display: flex;
    flex-direction: column;
    gap: 6px;
    min-height: 0;
}

.card-x .tl-head {
    display: flex;
    align-items: baseline;
    gap: 8px;
}

.card-x .tl-cur {
    font-size: 24px;
    font-weight: 700;
    line-height: 1.1;
}

.card-x .tl-unit {
    font-size: 12px;
    color: var(--muted);
}

/* 可切档的三张卡走纵向 flex，让折线 / 表格**撑满卡片剩余高度**。
   ★ 为什么不沿用 .mchart .cols 那套 `calc(96px + (--mod-h-1)*110px)`：
     网格是 grid-auto-rows: minmax(96px, auto)，行高会被内容顶开。
     实测（无头 Chrome）定高会让「切到走势档」的瞬间把整行顶高：
     c_cred 316px -> 469px、c_ip 352px -> 378px，同一行的邻居卡跟着变高，
     用户摆好的版面会跳。撑满则换档不改卡片高度。 */
.card-x.is-tiers {
    display: flex;
    flex-direction: column;
}

.card-x.is-tiers > .tl,
.card-x.is-tiers > .ctbl-wrap {
    flex: 1 1 auto;
    min-height: 0;
}

/* 环形档的高度封顶：.dn 的 max-width 是 min(46%, 260px)，在宽卡上会把环撑到 260px，
   使「切到环形图」比其它档高出一截（实测 c_cred 316px -> 358px，整行跟着跳）。
   451px 宽的卡 46% 只有 207px，所以这条只对宽卡生效（c_compose / c_ip 视觉零变化）。 */
.card-x.is-tiers .dn {
    max-width: min(46%, 210px);
}

.card-x .tl-svg {
    display: block;
    width: 100%;
    flex: 1 1 auto;
    min-height: 48px;   /* 卡片被拖到最矮时折线也不至于消失成一条线 */
}

.card-x .tl-axis {
    display: flex;
    justify-content: space-between;
    font-size: 11px;
    color: var(--muted);
}

.card-x .tl-flat {
    font-size: 11px;
    color: var(--muted);
}

/* ---- 明细表格 ---- */
/* 只让 .ctbl-wrap 滚，卡片自身不滚（否则表头会跟着跑掉）。
   高度不再自定 —— 由 .card-x.is-tiers 的 flex:1 撑满，换档不改卡片高度。 */
.card-x .ctbl-wrap {
    overflow: auto;
}

.card-x .ctbl {
    width: 100%;
    border-collapse: collapse;
    font-size: 12px;
}

.card-x .ctbl th,
.card-x .ctbl td {
    padding: 5px 8px;
    text-align: left;
    border-bottom: 1px solid var(--line);
    white-space: nowrap;
}

.card-x .ctbl thead th {
    position: sticky;
    top: 0;
    z-index: 1;
    background: var(--panel-b);
    font-weight: 600;
}

.card-x .ctbl th + th,
.card-x .ctbl td + td {
    text-align: right;
}

.card-x .ctbl tbody tr:last-child td {
    border-bottom: 0;
}

/* ---- ⋯ 菜单里的「图表显示类型」段 ---- */
.mod-float-head {
    padding: 6px 10px 2px;
    font-size: 11px;
    color: var(--muted);
}

.mod-float-sep {
    height: 1px;
    margin: 4px 6px;
    background: var(--line);
}

.mod-float-item.is-on {
    font-weight: 600;
}

.mod-float-tick {
    width: auto !important;
    margin-left: auto;
    color: var(--primary-2) !important;
}

@media print {
    /* 折线高与 .cols 的打印高度对齐（v23 区块里 .mchart .cols 是 118px !important） */
    .card-x .tl-svg {
        height: 118px !important;
    }

    .card-x .tl-svg polygon {
        opacity: 0.1 !important;
    }

    /* --muted 在深色主题下是浅灰，压到白纸上几乎看不见 —— 必须显式转深色。
       这正是 v23 区块头部第 5 条警告的那个坑。 */
    .card-x .tl-unit,
    .card-x .tl-axis,
    .card-x .tl-flat,
    .card-x .ctbl td,
    .card-x .ctbl th {
        color: #333 !important;
    }

    .card-x .ctbl thead th {
        position: static;
        background: #fff !important;
    }

    .card-x .ctbl-wrap {
        max-height: none !important;
        overflow: visible !important;
    }

    .card-x .ctbl th,
    .card-x .ctbl td {
        border-bottom: 1px solid #ccc !important;
    }
}

/* =========================================================
   UI 精致打磨（2026-11-02 · 浅色主题去灰 + 两主题共通细节）
   - 阴影三级令牌（--shadow-card/-hover/-pop）与按钮悬停底色（--btn-hover-bg）
     已在 :root 与浅色段定义并替换硬编码，这里是其余共通细节。
   ========================================================= */
::selection {
    background: var(--sel-bg);
}

/* 键盘可达性：Tab 聚焦时给清晰的高亮环（鼠标点击不触发，只有键盘导航出现） */
.btn-x:focus-visible,
.btn-icon:focus-visible,
.nav-item:focus-visible {
    outline: 2px solid rgba(88, 166, 255, 0.55);
    outline-offset: 2px;
}

/* 大数字等宽对齐（千分位/小数不跳宽） */
.stat-value {
    font-variant-numeric: tabular-nums;
}

/* 空态：淡虚线框 + 微底色，比一排孤零零的灰字更「设计过」 */
.empty-state {
    border: 1px dashed var(--line-strong);
    border-radius: var(--radius);
    background: var(--diff-line-bg);
    margin: 8px 0;
}


/* ============================================================
   液态玻璃 + 动效精修（2026-11-04）
   ------------------------------------------------------------
   为什么整块写在文件末尾：上面的老规则里混着大量实测修正记录
   （点击黑洞、打印还原、2K 密度、浅色主题去灰雾……），就地改动很容易
   连带踩到那些坑。这里统一用「同特异性后来者居上」覆盖，读的时候
   从上往下找不到效果就来文件末尾看这一段。

   配色纪律：只用主题变量 + 下面新增的玻璃令牌，深浅两套主题共用一份。
   深色的玻璃感 = 半透明底 + 白 6% 内高光 + 背景透视；
   浅色的玻璃感 = 白色半透明底 + 冷灰细边（浅色用白高光是看不见的）。
   不引入第二套色板，不做主题分叉。
   ============================================================ */

/* ---------- 0) 玻璃令牌 ---------- */
:root {
    --glass-top: rgba(13, 17, 23, 0.74);          /* 顶栏底 */
    --glass-side: rgba(13, 17, 23, 0.62);         /* 侧栏底 */
    --glass-edge: rgba(255, 255, 255, 0.08);      /* 边缘 1px 高光（深色） */
    --glass-sheen: rgba(255, 255, 255, 0.055);    /* 卡片内顶部柔光 */
    --glass-blur: blur(20px) saturate(170%);
    --glass-shadow-top: 0 8px 28px rgba(0, 0, 0, 0.34);
}

html[data-theme="light"] {
    --glass-top: rgba(255, 255, 255, 0.80);
    --glass-side: rgba(255, 255, 255, 0.66);
    --glass-edge: rgba(255, 255, 255, 0.95);
    --glass-sheen: rgba(31, 35, 40, 0.055);       /* 浅色改冷灰细边，白高光在白卡上等于没有 */
    --glass-shadow-top: 0 6px 20px rgba(31, 35, 40, 0.09);
}

/* ---------- 1) 顶栏 / 侧栏：磨砂玻璃 ---------- */
/* 顶栏是唯一真正「有内容从下面滚过」的固定层，模糊才有意义；
   侧栏内容被 .main 的 margin-left 让开了，给它半透明只是让左侧边缘
   和背景的蓝色辉光连成一片，不做重模糊省性能。 */
.topbar {
    background: var(--glass-top);
    -webkit-backdrop-filter: var(--glass-blur);
    backdrop-filter: var(--glass-blur);
    box-shadow: inset 0 1px 0 var(--glass-edge), var(--glass-shadow-top);
}

.sidebar {
    background: var(--glass-side);
    -webkit-backdrop-filter: blur(12px) saturate(140%);
    backdrop-filter: blur(12px) saturate(140%);
}

/* ---------- 2) 卡片：内高光 + 图表卡顶线 ---------- */
/* 靠 inset 阴影而不是伪元素做高光：.stat-card 的 ::before/::after 已经被
   左侧色条和右上角光晕占了，再塞伪元素就是第三次抢位置；inset 阴影
   不占命中区、不影响布局，也不会变成「点击黑洞」。 */
.card-x,
.stat-card {
    box-shadow: var(--shadow-card), inset 0 1px 0 var(--glass-sheen);
}

/* hover 态必须把内高光一起带上：老规则 .card-x.hoverable:hover 特异性更高，
   会整条覆盖 box-shadow，漏掉这段卡片一悬停玻璃边就消失了。
   ⚠️ 只给本来就声明了可点的卡（.hoverable / .stat-card）—— 给所有 .card-x
   加悬停抬升等于向用户撒谎「这张卡能点」，纯信息卡不该有这种反馈。 */
.card-x.hoverable:hover,
.stat-card:hover {
    box-shadow: var(--shadow-hover), inset 0 1px 0 var(--glass-sheen),
                0 0 0 1px rgba(88, 166, 255, 0.10);
}

/* 图表卡顶部一道极淡的蓝色渐变线：让「数据卡」和普通信息卡在扫视时分开。
   左右各留 16px 内缩，避免和圆角打架。 */
.mchart::before {
    content: "";
    position: absolute;
    top: 0;
    left: 16px;
    right: 16px;
    height: 1px;
    background: linear-gradient(90deg, transparent, rgba(88, 166, 255, 0.45), transparent);
    pointer-events: none;
}

/* ---------- 3) 表格行 hover：左侧 2px 强调条 ---------- */
/* :not(.drop-hint-*) 是必须的 —— 拖动排序时插入提示也用 box-shadow 画，
   不排除的话「悬停中的那一行」会把插入提示盖掉，拖到哪都看不见落点。 */
table.data-grid tbody tr:hover:not(.drop-hint-top):not(.drop-hint-bottom) {
    background: rgba(88, 166, 255, 0.07);
    box-shadow: inset 2px 0 0 0 var(--accent);
}

/* ---------- 4) 轻提示：退场动画 + 倒计时条 ---------- */
/* api.js 的 App.toast 到点会加 .out 再 300ms 移除元素，但 CSS 里一直没有
   .out 的规则 —— 等于 toast 是「瞬间消失」的，看着像卡掉。这里补上退场。 */
.toast-x {
    position: relative;
    overflow: hidden;
}

.toast-x::after {
    content: "";
    position: absolute;
    left: 0;
    bottom: 0;
    height: 2px;
    width: 100%;
    background: var(--accent);
    opacity: 0.55;
    transform-origin: left center;
    animation: toastBar 2.8s linear forwards;   /* 与 api.js 的 2800ms 对齐 */
    pointer-events: none;
}

.toast-x.ok::after   { background: var(--ok); }
.toast-x.err::after  { background: var(--danger); }
.toast-x.warn::after { background: var(--warn); }

.toast-x.out {
    /* 只写 to：from 由当前计算值决定，退场从「静止态」接着走，不会跳一下 */
    animation: toastOut 0.3s var(--ease-out) forwards;
}

/* ---------- 5) 概览模块：错峰入场 ---------- */
/* .fx-enter / .fx-draw 由 App.fxEntrance() 临时挂到网格宿主上（1.8s 后
   摘掉；fx-draw 是图表绘制动画的开关，见文件末尾 B 区块），只在「这一屏
   第一次渲染」时挂 —— 拖动换位、拉高拉宽会重渲染整个网格，无条件加动画
   的话每拖一次整屏就闪一遍。 */
.mod-grid.fx-enter > .mod-cell {
    animation: cellIn 0.42s var(--ease-out) both;
}

.mod-grid.fx-enter > .mod-cell:nth-child(1)  { animation-delay: 0.00s; }
.mod-grid.fx-enter > .mod-cell:nth-child(2)  { animation-delay: 0.035s; }
.mod-grid.fx-enter > .mod-cell:nth-child(3)  { animation-delay: 0.07s; }
.mod-grid.fx-enter > .mod-cell:nth-child(4)  { animation-delay: 0.105s; }
.mod-grid.fx-enter > .mod-cell:nth-child(5)  { animation-delay: 0.14s; }
.mod-grid.fx-enter > .mod-cell:nth-child(6)  { animation-delay: 0.175s; }
.mod-grid.fx-enter > .mod-cell:nth-child(7)  { animation-delay: 0.21s; }
.mod-grid.fx-enter > .mod-cell:nth-child(8)  { animation-delay: 0.245s; }
.mod-grid.fx-enter > .mod-cell:nth-child(n+9) { animation-delay: 0.28s; }

/* ---------- 6) 主按钮：hover 扫光 ---------- */
/* 一条斜向白光从左外侧扫到右外侧，只在 hover 触发；
   伪元素 pointer-events:none，绝不会抢按钮的点击。 */
.btn-primary-x {
    position: relative;
    overflow: hidden;
}

.btn-primary-x::after {
    content: "";
    position: absolute;
    top: -60%;
    left: 0;
    width: 38%;
    height: 220%;
    background: linear-gradient(100deg, transparent, rgba(255, 255, 255, 0.42), transparent);
    transform: skewX(-20deg) translateX(-260%);
    transition: transform 0.62s var(--ease-out);
    pointer-events: none;
}

.btn-primary-x:hover::after {
    transform: skewX(-20deg) translateX(420%);
}

/* ---------- 7) 空状态：图标轻微浮动 ---------- */
/* 空列表最容易被误认为「没加载出来」，一点点呼吸能明显降低这种误判。 */
.empty-state i {
    animation: floatY 3.4s ease-in-out infinite;
}

/* ---------- 8) 登录页：缓慢流动的极光 ---------- */
#loginView {
    overflow: hidden;
}

#loginView::before,
#loginView::after {
    content: "";
    position: absolute;
    width: 62vw;
    height: 62vw;
    border-radius: 50%;
    filter: blur(70px);
    opacity: 0.55;
    pointer-events: none;
    will-change: transform;
}

#loginView::before {
    left: -18vw;
    top: -22vw;
    background: radial-gradient(circle, rgba(31, 111, 235, 0.55), transparent 62%);
    animation: auroraA 22s ease-in-out infinite alternate;
}

#loginView::after {
    right: -20vw;
    bottom: -26vw;
    background: radial-gradient(circle, rgba(88, 166, 255, 0.42), transparent 64%);
    animation: auroraB 27s ease-in-out infinite alternate;
}

.login-card {
    z-index: 1;   /* 极光在卡片后面，别把表单压灰 */
}

/* ---------- 9) 弹层：玻璃化 + 退场 ---------- */
/* 蒙层本来就有 6px 模糊，卡片再叠一层 24px 玻璃，
   「弹层浮在页面上」的层次才立得住（原来卡片是完全不透明的渐变块）。 */
.modal-box {
    -webkit-backdrop-filter: blur(24px) saturate(160%);
    backdrop-filter: blur(24px) saturate(160%);
    box-shadow: var(--shadow), inset 0 1px 0 var(--glass-edge);
}

/* 关闭走的是「加 .closing → 160ms 后移出 DOM」（见 api.js closeModal），动画取 150ms，留 10ms 余量。
   ⚠️ 不能顺手 remove('show')：.modal-mask 的基础规则是 display:none，
   一摘 show 当场就没了，动画根本没机会播。 */
.modal-mask.closing {
    animation: maskOut 0.15s ease forwards;
}

.modal-mask.closing > .modal-box {
    animation: boxOut 0.15s var(--ease-out) forwards;
}

/* ---------- 10) 侧栏当前项：指示条落位 ---------- */
.nav-item.active::before {
    animation: navGlow 0.36s var(--ease-out) both;
}

/* ---------- 11) 新增关键帧 ---------- */
@keyframes cellIn {
    from { opacity: 0; transform: translateY(10px) scale(0.985); }
    to   { opacity: 1; transform: translateY(0) scale(1); }
}

@keyframes toastOut {
    to { opacity: 0; transform: translateX(30px) scale(0.96); }
}

@keyframes toastBar {
    from { transform: scaleX(1); }
    to   { transform: scaleX(0); }
}

@keyframes floatY {
    0%, 100% { transform: translateY(0); }
    50%      { transform: translateY(-6px); }
}

@keyframes auroraA {
    from { transform: translate3d(0, 0, 0) scale(1); }
    to   { transform: translate3d(9vw, 7vw, 0) scale(1.18); }
}

@keyframes auroraB {
    from { transform: translate3d(0, 0, 0) scale(1.12); }
    to   { transform: translate3d(-7vw, -6vw, 0) scale(0.96); }
}

@keyframes maskOut {
    to { opacity: 0; }
}

@keyframes boxOut {
    to { opacity: 0; transform: scale(0.975) translateY(8px); }
}

@keyframes navGlow {
    from { transform: scaleY(0.25); opacity: 0; }
    to   { transform: scaleY(1); opacity: 1; }
}

/* ---------- 12) 降级：系统开了「减少动态效果」就全部停掉 ---------- */
/* 只停新增的这几处，老的 reduced-motion 块（app.css:2521）不动。 */
@media (prefers-reduced-motion: reduce) {
    .mod-grid.fx-enter > .mod-cell,
    .empty-state i,
    #loginView::before,
    #loginView::after,
    .nav-item.active::before,
    .toast-x::after {
        animation: none !important;
    }

    .toast-x.out {
        opacity: 0;
    }

    .btn-primary-x::after {
        display: none;
    }
}

/* ============================================================
   骨架屏 + 图表入场绘制（2026-11-05）
   ------------------------------------------------------------
   1) 骨架屏：App.go 切视图时，目标宿主还空着就先铺一层骨架
      （App.SKELETONS + App.showSkeletons，见 app.js）。数据回来后
      loader 自己写 innerHTML 会把骨架顶掉，**不需要逐视图改渲染
      时机**；宿主里已有内容则不动 —— 老内容保持可见直到新数据到。
      接口 12s 不回，骨架会被换成「加载时间过长」空态，不会永远闪。
   2) 图表入场：.fx-draw 挂在网格宿主（首屏，App.fxEntrance）或
      单个 .mod-cell（切图表档位，App.ovRepaintCard）上。所有规则
      只写 keyframes 的 from、不碰基础样式 —— 类一摘元素立刻回到
      最终态，绝不会卡在半程（上一轮的 fx-enter 在 1.8s 后摘掉，
      图表动画必须在 1.8s 内演完）。
   3) 卡片跟随光斑：v39.5 已下线（与游标光效叠加显乱，只留 fxSeg
      三方案），相关规则与 JS 驱动一并移除。
   ============================================================ */

/* ---------- 0) 骨架基色（深浅主题各一组，比 --row-alt 略醒目一点） ---------- */
:root {
    --sk-lo: rgba(240, 246, 252, 0.05);    /* 骨架底色 */
    --sk-hi: rgba(240, 246, 252, 0.13);    /* 扫过高光 */
}
html[data-theme="light"] {
    --sk-lo: rgba(31, 35, 40, 0.06);
    --sk-hi: rgba(31, 35, 40, 0.14);
}

/* ---------- A) 骨架屏 ---------- */
.skel { pointer-events: none; user-select: none; }
/* 骨架被插进网格宿主时占满整行，不要缩成一格 */
.mod-grid > .skel,
.stat-grid > .skel { grid-column: 1 / -1; }

.skel .sk-b,
.skel .sk-ic,
.skel-canvas::before {
    background: linear-gradient(90deg, var(--sk-lo) 25%, var(--sk-hi) 47%, var(--sk-lo) 68%);
    background-size: 260% 100%;
    animation: skShimmer 1.5s ease-in-out infinite;
}
.skel .sk-b { display: inline-block; height: 12px; border-radius: 5px; }
.skel .sk-ic { display: inline-block; width: 26px; height: 26px; border-radius: 7px; flex: 0 0 auto; }

/* 表格骨架：外框仿 .data-grid 的容器感，行高仿真实表格行 */
.skel-t {
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: linear-gradient(150deg, var(--panel-b), var(--panel-a));
    overflow: hidden;
}
.skel-t .sk-tr {
    display: flex; align-items: center; gap: 18px;
    height: 37px; padding: 0 16px;
    border-bottom: 1px solid var(--line);
}
.skel-t .sk-tr:last-child { border-bottom: none; }
.skel-t .sk-tr .sk-b { height: 11px; }

/* 卡片骨架：统计卡 / 概览网格 / 报表目录 */
.skel-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(178px, 1fr)); gap: 14px; }
.skel-card {
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: linear-gradient(150deg, var(--panel-b), var(--panel-a));
    padding: 15px 16px;
    display: flex; flex-direction: column; gap: 9px; justify-content: center;
    min-height: 96px;
}

/* 列表骨架：设备清单 / 组织树 / 巡检记录 */
.skel-list { display: flex; flex-direction: column; gap: 8px; }
.skel-li {
    display: flex; align-items: center; gap: 10px;
    border: 1px solid var(--line); border-radius: var(--radius-sm);
    background: var(--panel-a); padding: 10px 12px;
}
.skel-li .sk-b { flex: 1 1 auto; }

/* 大画布骨架（网络拓扑） */
.skel-canvas {
    position: relative; height: 480px; overflow: hidden;
    border: 1px solid var(--line); border-radius: var(--radius);
    background: var(--panel-a);
}
.skel-canvas::before { content: ""; position: absolute; inset: 0; }
/* 拓扑宿主 #topoHost 自己已有底色 + 边框 + 内边距，里面的骨架不再套第二层框 */
#topoHost .skel-canvas { border: none; background: transparent; height: 460px; }

/* ---------- B) 图表入场绘制 ---------- */
/* 环形图描边：每段从 0 长到自己的弧长。to 省略 = 元素各自的最终
   dasharray 属性值；--fx-del / --fx-d 由 views-overview-charts.js 按
   弧长占比算好 —— 整环看起来是一支笔顺时针扫过去。 */
.fx-draw .dn circle.dn-seg {
    animation: dnDraw var(--fx-d, 0.6s) var(--ease-out) both;
    animation-delay: var(--fx-del, 0s);
}
@keyframes dnDraw { from { stroke-dasharray: 0 10000; } }

/* 环心数字 / 图例：描边扫过近半时浮现 */
.fx-draw .dn .mid,
.fx-draw .lgd { animation: fxIn 0.5s var(--ease-out) 0.45s both; }

/* 横向条：从左长出。用 scaleX 而不是动画 width —— inline 的百分比
   宽度保持原值，布局一次都不用重算；span 本身没有圆角，不会变形。 */
.fx-draw .hb .track > span,
.fx-draw .cert .ci .bar > span {
    transform-origin: left center;
    animation: barGrow 0.7s var(--ease-out) both;
}
.fx-draw .hb > .row:nth-child(1) .track > span { animation-delay: 0.04s; }
.fx-draw .hb > .row:nth-child(2) .track > span { animation-delay: 0.09s; }
.fx-draw .hb > .row:nth-child(3) .track > span { animation-delay: 0.14s; }
.fx-draw .hb > .row:nth-child(4) .track > span { animation-delay: 0.19s; }
.fx-draw .hb > .row:nth-child(5) .track > span { animation-delay: 0.24s; }
.fx-draw .hb > .row:nth-child(6) .track > span { animation-delay: 0.29s; }
.fx-draw .hb > .row:nth-child(7) .track > span { animation-delay: 0.34s; }
.fx-draw .hb > .row:nth-child(8) .track > span { animation-delay: 0.39s; }
.fx-draw .hb > .row:nth-child(n+9) .track > span { animation-delay: 0.44s; }
.fx-draw .cert > .ci:nth-child(1) .bar > span { animation-delay: 0.06s; }
.fx-draw .cert > .ci:nth-child(2) .bar > span { animation-delay: 0.16s; }
.fx-draw .cert > .ci:nth-child(3) .bar > span { animation-delay: 0.26s; }
.fx-draw .cert > .ci:nth-child(4) .bar > span { animation-delay: 0.36s; }
.fx-draw .cert > .ci:nth-child(n+5) .bar > span { animation-delay: 0.46s; }

/* 柱状图：从地面长起（scaleY，同样不碰 inline 的 height%） */
.fx-draw .cols .cb {
    transform-origin: bottom center;
    animation: colGrow 0.75s var(--ease-out) both;
}
.fx-draw .cols > .col:nth-child(1) .cb { animation-delay: 0.04s; }
.fx-draw .cols > .col:nth-child(2) .cb { animation-delay: 0.11s; }
.fx-draw .cols > .col:nth-child(3) .cb { animation-delay: 0.18s; }
.fx-draw .cols > .col:nth-child(4) .cb { animation-delay: 0.25s; }
.fx-draw .cols > .col:nth-child(5) .cb { animation-delay: 0.32s; }
.fx-draw .cols > .col:nth-child(6) .cb { animation-delay: 0.39s; }
.fx-draw .cols > .col:nth-child(7) .cb { animation-delay: 0.46s; }
.fx-draw .cols > .col:nth-child(n+8) .cb { animation-delay: 0.53s; }
.fx-draw .cols .cv,
.fx-draw .cols .cl { animation: fxIn 0.45s var(--ease-out) 0.3s both; }

/* 折线走势：整幅从左向右揭示。clip-path 只出现在 keyframes 里，
   动画结束后（fill both 持有终态 / 类被摘掉）都不裁切。 */
.fx-draw .tl-svg { animation: tlWipe 0.9s var(--ease-out) 0.12s both; }
.fx-draw .tl-head,
.fx-draw .tl-axis { animation: fxIn 0.5s var(--ease-out) 0.2s both; }

/* 迷你指标格：错峰浮现 */
.fx-draw .mgrid .mini { animation: fxIn 0.5s var(--ease-out) both; }
.fx-draw .mgrid .mini:nth-child(2) { animation-delay: 0.06s; }
.fx-draw .mgrid .mini:nth-child(3) { animation-delay: 0.12s; }
.fx-draw .mgrid .mini:nth-child(4) { animation-delay: 0.18s; }
.fx-draw .mgrid .mini:nth-child(n+5) { animation-delay: 0.24s; }

@keyframes fxIn { from { opacity: 0; } }
@keyframes barGrow { from { transform: scaleX(0); } }
@keyframes colGrow { from { transform: scaleY(0); } }
@keyframes tlWipe {
    from { clip-path: inset(0 100% 0 0); }
    to   { clip-path: inset(0 0 0 0); }
}
@keyframes skShimmer {
    0%   { background-position: 130% 0; }
    100% { background-position: -130% 0; }
}

/* ---------- C) 卡片跟随光斑：v39.5 已下线 ---------- */
/* 与顶栏游标光效（fxSeg 三方案）两套叠加显得乱（卡片内圆光斑与
   游标拖尾共存），老板拍板只留游标光效。本区光晕/透镜环规则、浅色覆盖、
   app.js 的缓动跟随驱动（位置变量）一并移除。卡片悬停仍保留上浮与边框变亮。 */
/* ---------- D) 降级与打印 ---------- */
@media (prefers-reduced-motion: reduce) {
    .skel .sk-b,
    .skel .sk-ic,
    .skel-canvas::before,
    .fx-draw .dn circle.dn-seg,
    .fx-draw .dn .mid,
    .fx-draw .lgd,
    .fx-draw .hb .track > span,
    .fx-draw .cert .ci .bar > span,
    .fx-draw .cols .cb,
    .fx-draw .cols .cv,
    .fx-draw .cols .cl,
    .fx-draw .tl-svg,
    .fx-draw .tl-head,
    .fx-draw .tl-axis,
    .fx-draw .mgrid .mini { animation: none !important; }
}
@media print { .skel { display: none !important; } }

/* ---------- 系统信息（v39.2）：运行环境 / 资源占用 / 数据统计 ---------- */
.sys-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
    gap: 12px;
}
.sys-kv {
    display: grid;
    grid-template-columns: 76px 1fr;
    gap: 7px 10px;
    font-size: 12.5px;
    align-items: start;
}
.sys-kv .k { color: var(--muted); }
.sys-kv .v { word-break: break-all; line-height: 1.45; }
.mono-sm {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 11.5px;
}
.sys-meter { margin-bottom: 12px; }
.sys-meter:last-child { margin-bottom: 0; }
.sys-meter .mh {
    display: flex;
    justify-content: space-between;
    gap: 10px;
    font-size: 12.5px;
}
.sys-meter .mh b { font-variant-numeric: tabular-nums; }
.sys-meter .bar { margin-top: 5px; height: 7px; }
.count-group { margin-top: 12px; }
.count-group-head {
    display: flex;
    justify-content: space-between;
    font-size: 12.5px;
    margin-bottom: 6px;
    padding-bottom: 4px;
    border-bottom: 1px dashed var(--line);
}
.count-grid { display: flex; flex-wrap: wrap; gap: 6px; }
.count-chip {
    display: inline-flex;
    align-items: baseline;
    gap: 6px;
    padding: 3px 9px;
    border-radius: 999px;
    background: var(--hover-overlay);
    border: 1px solid var(--line);
    font-size: 12px;
}
.count-chip b { font-weight: 500; }
.count-chip i {
    font-style: normal;
    color: var(--accent);
    font-variant-numeric: tabular-nums;
    font-weight: 600;
}
.count-chip.zero { opacity: 0.42; }
.count-chip.zero i { color: var(--muted); }

/* ============================================================
   v39.4 全站游标光效层（2026-11-05）
   ------------------------------------------------------------
   v39.3 的「圆星云+长方形光带」被吐槽丑（形状和指针无关、像灯
   泡），重做成三种顶栏可切换方案（fxSeg，偏好存 localStorage：
   assetops.cursorfx）：点串拖尾（默认）/ 残影丝带 / 星尘散落 / 关
   闭。深浅两套画法：深色 additive 发光（.fx-dot=screen、canvas=
   lighter）；浅色 additive 在白底会直接变白消失 → .fx-dot=
   normal、canvas=multiply 的蓝调淡彩。配色放 CSS：主题切换自动
   跟随，无需重建。点串 = .fx-dot（只写 transform/opacity，GPU
   合成零重排）；丝带/星尘 = #fxCanvas 每帧重画（JS 按 data-theme
   每帧取 isDark）。层级 z-index:9999：盖过所有弹窗遮罩(1800+)与
   顶栏(1000)，使光效在详情弹窗等层级上仍可见（v39.7 修正）。
   ============================================================ */
/* fxLayer 用 display:contents：不生成盒子也不生成堆叠上下文，
   .fx-dot（fixed+z-index:60）直接落入根堆叠上下文，screen 加色混合
   才能真正作用于页面内容（否则被 fixed 容器隔离，光点只是半透明
   圆片，盖在表格上几乎没有存在感 —— v39.5 实测修正）。 */
#fxLayer { display: contents; }
#fxCanvas {
    position: fixed;
    left: 0;
    top: 0;
    pointer-events: none;
    z-index: 9999;
}
.fx-dot {
    position: fixed;
    left: 0;
    top: 0;
    border-radius: 50%;
    mix-blend-mode: screen;
    z-index: 9999;
    will-change: transform, opacity;
    background: radial-gradient(circle,
        rgba(170, 215, 255, 0.85), rgba(80, 140, 255, 0.35) 55%, transparent 75%);
}
html[data-theme="light"] .fx-dot {
    mix-blend-mode: normal;
    background: radial-gradient(circle,
        rgba(70, 110, 225, 0.50), rgba(100, 140, 235, 0.22) 55%, transparent 75%);
}
@media (prefers-reduced-motion: reduce) {
    #fxLayer, #fxCanvas { display: none; }
}
