/*
 * 任务调度控制台的样式。
 *
 * 三个决定值得写下来:
 *
 * 1. 没有构建步骤,没有 CDN。这是个运维控制台 —— 最需要它的时刻恰恰是网络出问题的时刻
 *    (代理供应商挂了、出口全断)。那种时候还要去外网取 CSS 和字体,页面就打不开了。
 * 2. 深色是刻意选定的,不是省事。这个页面的用途是长时间盯着看数字,
 *    而且经常挂在墙上的显示器里。
 * 3. 毛玻璃只用在顶栏。风格库推荐了 Glassmorphism,但它的模糊在密集数据表上是负担 ——
 *    它自己的适用清单里写的也是"导航与弹层"。
 */

:root {
    /* 深色科技 + 状态绿。绿色只用来表示"正常运转",不当装饰用 —— */
    /* 一个颜色同时表示状态和装饰,状态就没人看得见了。 */
    --bg: #0f172a;
    --card: #1b2336;
    --primary: #1e293b;
    --secondary: #334155;
    --accent: #22c55e;
    --on-accent: #0f172a;
    --fg: #f8fafc;
    --muted: #272f42;
    --muted-fg: #94a3b8;
    --border: #475569;
    --border-soft: #2c3648;
    /* 红色刻意压了饱和度。
       整个配色是石板灰蓝的,一个纯度很高的红压在上面会跳出来,
       抢走本该给数据的注意力 —— 而危险按钮要的是"看清楚",不是"最显眼"。 */
    --danger: #e07a72;
    /* 实心按钮的底色。比 --danger 深一档,好让白字够得着 4.5:1 的对比度 ——
       --danger 那个亮度是给文字和边框用的,拿来当底色白字会糊。 */
    --danger-solid: #a8453d;
    --warn: #f59e0b;
    --info: #38bdf8;
    --ring: #f8fafc;

    /* 密度 8:间距刻度压到 8–32,为的是一屏能装下更多行。 */
    --s1: 4px;
    --s2: 8px;
    --s3: 12px;
    --s4: 16px;
    --s5: 24px;
    --s6: 32px;

    /* 勾选标记。做成变量是因为它在遮罩里用,写死在规则里改起来要翻两处。 */
    --check: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 8.5 6.5 12 13 4.5'/%3E%3C/svg%3E");

    --radius: 6px;
    --radius-lg: 10px;

    /* 数字一律用等宽:进度、计数、比率在表格里要能对齐着比较, */
    /* 比例字体下数字宽度不一,眼睛就没法竖着扫。 */
    --font-mono: 'Fira Code', ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
    --font-sans: 'Fira Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC',
        'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;

    --shadow: 0 1px 2px rgb(0 0 0 / 0.3), 0 4px 12px rgb(0 0 0 / 0.25);
    --dur: 160ms;
}

* { box-sizing: border-box; }

html, body {
    margin: 0;
    padding: 0;
    height: 100%;
}

body {
    background: var(--bg);
    color: var(--fg);
    font-family: var(--font-sans);
    font-size: 14px;
    line-height: 1.5;
    -webkit-font-smoothing: antialiased;
}

/*
 * 滚动条。
 *
 * 不管的话,深色页面上会出现一条系统默认的浅色滚动条 —— 在一整屏深色里
 * 它比它要滚的内容还显眼。停摆面板、表格、侧栏各有一条,加起来相当刺眼。
 *
 * 两套写法都要:Firefox 认 scrollbar-color,WebKit 认伪元素,
 * 只写一套就有一半浏览器还是默认样子。
 */
* {
    scrollbar-width: thin;
    scrollbar-color: var(--border) transparent;
}

::-webkit-scrollbar {
    width: 10px;
    height: 10px;
}

::-webkit-scrollbar-track {
    background: transparent;
}

::-webkit-scrollbar-thumb {
    background: var(--border);
    border-radius: 6px;
    /* 用透明边框把滑块收窄,而不是直接把整条轨道做细 ——
       轨道细了之后鼠标很难抓住它。 */
    border: 2px solid transparent;
    background-clip: padding-box;
}

::-webkit-scrollbar-thumb:hover {
    background: var(--muted-fg);
    background-clip: padding-box;
}

::-webkit-scrollbar-corner {
    background: transparent;
}

/* 有人把系统设成了减少动效,就别再动。这不是可选的润色。 */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
    }
}

/* 焦点环一处都不能少 —— 键盘用户全靠它知道自己在哪。 */
:focus-visible {
    outline: 2px solid var(--ring);
    outline-offset: 2px;
    border-radius: 3px;
}

button, input, select, textarea { font: inherit; color: inherit; }
button { cursor: pointer; }
button:disabled { cursor: not-allowed; opacity: 0.45; }

/* ---------- 骨架 ---------- */

.app {
    display: grid;
    grid-template-columns: 208px 1fr;
    grid-template-rows: auto 1fr;
    grid-template-areas: 'brand topbar' 'nav main';
    height: 100vh;
}

.brand {
    grid-area: brand;
    display: flex;
    align-items: center;
    gap: var(--s2);
    padding: 0 var(--s4);
    height: 52px;
    border-bottom: 1px solid var(--border-soft);
    border-right: 1px solid var(--border-soft);
    font-weight: 600;
    letter-spacing: 0.01em;
}

.brand .dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--accent);
    flex: none;
}

/* 顶栏是唯一用毛玻璃的地方:它浮在滚动的内容之上,层次感在这里是有用的。 */
.topbar {
    grid-area: topbar;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s4);
    padding: 0 var(--s5);
    height: 52px;
    border-bottom: 1px solid var(--border-soft);
    background: rgb(27 35 54 / 0.72);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
}

.nav {
    grid-area: nav;
    border-right: 1px solid var(--border-soft);
    padding: var(--s3) var(--s2);
    display: flex;
    flex-direction: column;
    gap: 2px;
    overflow-y: auto;
}

.nav a {
    display: flex;
    align-items: center;
    gap: var(--s3);
    padding: var(--s2) var(--s3);
    border-radius: var(--radius);
    color: var(--muted-fg);
    text-decoration: none;
    transition: background var(--dur), color var(--dur);
}

.nav a:hover { background: var(--muted); color: var(--fg); }

.nav a[aria-current='page'] {
    background: var(--primary);
    color: var(--fg);
    font-weight: 500;
}

.nav a svg { flex: none; }

/* 侧栏底部的角标,用来放"有几个任务停摆了"这种需要人处理的计数。 */
.nav .badge {
    margin-left: auto;
    min-width: 20px;
    text-align: center;
    padding: 0 6px;
    border-radius: 10px;
    background: var(--warn);
    color: #1a1205;
    font-family: var(--font-mono);
    font-size: 11px;
    font-weight: 600;
}

.main {
    grid-area: main;
    overflow-y: auto;
    padding: var(--s5);
}

@media (max-width: 900px) {
    .app {
        grid-template-columns: 1fr;
        grid-template-areas: 'brand' 'topbar' 'main';
        grid-template-rows: auto auto 1fr;
    }
    .nav {
        display: none;
    }
    .nav.open {
        display: flex;
        position: fixed;
        inset: 104px 0 0 0;
        z-index: 40;
        background: var(--bg);
        border-right: 0;
    }
    .brand { border-right: 0; }
    .main { padding: var(--s4); }
}

/* ---------- 实时状态 ---------- */

/*
 * "实时"这两个字只有在数据确实是刚取回来的时候才配写上去。
 * 所以这里永远显示上次更新的时刻,拉取失败时整块变成陈旧态 ——
 * 一个停住不动却仍写着"实时"的页面,比没有这两个字危险得多。
 */
.topbar-left {
    display: flex;
    align-items: center;
    gap: var(--s4);
    min-width: 0;
    overflow: hidden;
}

/*
 * 顶栏的系统总览。
 *
 * 排成一行小刻度,而不是几张卡片:它要一直待在顶栏里,不能抢内容区的地方。
 * 数字用等宽,好让它每秒跳动时宽度不变 —— 比例字体下数字一变宽,
 * 整行会跟着左右抽动,盯久了很难受。
 */
.glance {
    display: flex;
    align-items: center;
    gap: var(--s4);
    padding-left: var(--s4);
    border-left: 1px solid var(--border-soft);
    min-width: 0;
    overflow: hidden;
}

.glance .g-item {
    display: flex;
    align-items: baseline;
    gap: 6px;
    white-space: nowrap;
}

.glance .g-label {
    font-size: 11px;
    color: var(--muted-fg);
}

.glance .g-value {
    font-family: var(--font-mono);
    font-size: 13px;
    font-variant-numeric: tabular-nums;
    color: var(--fg);
}

/* 有停摆任务时"在跑"那一项变黄。它不是错误,是"有事等着人处理"。 */
.glance .g-value.warn { color: var(--warn); }

/* 出口刹车时,吞吐那一项变红 —— 那时候速率掉下来不是任务的问题。 */
.glance.braking .g-value#g-rate { color: var(--danger); }

/* 窄屏放不下六项。按重要性依次收起:先去掉"排队"和"预计",
   留下在跑、剩余、今日、吞吐 —— 那四个是随时都想知道的。 */
@media (max-width: 1240px) {
    .glance .g-item:nth-child(2),
    .glance .g-item:nth-child(6) { display: none; }
}

@media (max-width: 1040px) {
    /* 再窄就只留在跑和吞吐。 */
    .glance .g-item:nth-child(3),
    .glance .g-item:nth-child(4) { display: none; }
}

@media (max-width: 820px) {
    .glance { display: none; }
}

.live {
    display: flex;
    align-items: center;
    gap: var(--s2);
    font-family: var(--font-mono);
    font-size: 12px;
    color: var(--muted-fg);
}

.live .pulse {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--accent);
    animation: pulse 2s ease-in-out infinite;
}

.live[data-state='stale'] .pulse { background: var(--warn); animation: none; }
.live[data-state='stale'] { color: var(--warn); }
.live[data-state='paused'] .pulse { background: var(--muted-fg); animation: none; }

@keyframes pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.35; }
}

/* ---------- 通用块 ---------- */

.page-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--s4);
    margin-bottom: var(--s4);
    flex-wrap: wrap;
}

.page-head h1 {
    margin: 0;
    font-size: 18px;
    font-weight: 600;
}

.page-head p {
    margin: var(--s1) 0 0;
    color: var(--muted-fg);
    font-size: 13px;
}

.card {
    background: var(--card);
    border: 1px solid var(--border-soft);
    border-radius: var(--radius-lg);
    padding: var(--s4);
}

.card + .card { margin-top: var(--s4); }

/* 卡片头:标题在左,动作按钮在右。 */
.card-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s3);
    margin-bottom: var(--s3);
    flex-wrap: wrap;
}
.card-head h2 { margin: 0; }

.card > h2, .card-head h2 {
    margin: 0 0 var(--s3);
    font-size: 13px;
    font-weight: 600;
    color: var(--muted-fg);
    text-transform: uppercase;
    letter-spacing: 0.06em;
}

/* ---------- 指标条 ---------- */

.metrics {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
    gap: var(--s3);
    margin-bottom: var(--s4);
}

.metric {
    background: var(--card);
    border: 1px solid var(--border-soft);
    border-radius: var(--radius-lg);
    padding: var(--s3) var(--s4);
}

.metric .label {
    font-size: 12px;
    color: var(--muted-fg);
    display: flex;
    align-items: center;
    gap: 6px;
}

.metric .value {
    font-family: var(--font-mono);
    font-size: 22px;
    font-weight: 500;
    line-height: 1.25;
    margin-top: var(--s1);
    font-variant-numeric: tabular-nums;
}

.metric .sub {
    font-size: 11px;
    color: var(--muted-fg);
    font-family: var(--font-mono);
}

.metric.good .value { color: var(--accent); }
.metric.bad .value { color: var(--danger); }
.metric.warn .value { color: var(--warn); }

/* ---------- 停摆告警 ---------- */

/*
 * 这一块是整个控制台最需要被看见的东西。
 * 停摆的任务在库里还是"运行中",不报错、不出现在任何失败统计里 ——
 * 从外面看它一切正常,实际一次交付都不会再发生。所以它必须自己跳出来。
 */
.alert {
    border: 1px solid var(--warn);
    border-left-width: 3px;
    background: color-mix(in srgb, var(--warn) 8%, var(--card));
    border-radius: var(--radius-lg);
    padding: var(--s4);
    margin-bottom: var(--s4);
}

/* 停摆的任务可能很多(一次重启就能攒下几十条)。
   面板是提醒,不该把正文挤到屏幕外去 —— 超过高度就自己滚。 */
.alert .table-wrap {
    max-height: 260px;
    overflow-y: auto;
}

.alert h2 {
    margin: 0 0 var(--s2);
    font-size: 14px;
    font-weight: 600;
    color: var(--warn);
    display: flex;
    align-items: center;
    gap: var(--s2);
}

.alert p {
    margin: 0 0 var(--s3);
    font-size: 13px;
    color: var(--muted-fg);
    max-width: 74ch;
}

/* ---------- 表格 ---------- */

.table-wrap {
    overflow-x: auto;
    border: 1px solid var(--border-soft);
    border-radius: var(--radius-lg);
}

table {
    width: 100%;
    border-collapse: collapse;
    font-size: 13px;
    white-space: nowrap;
}

thead th {
    position: sticky;
    top: 0;
    background: var(--primary);
    text-align: left;
    font-weight: 500;
    font-size: 12px;
    color: var(--muted-fg);
    padding: var(--s2) var(--s3);
    border-bottom: 1px solid var(--border-soft);
    z-index: 1;
}

tbody td {
    padding: var(--s2) var(--s3);
    border-bottom: 1px solid var(--border-soft);
    vertical-align: middle;
}

tbody tr:last-child td { border-bottom: 0; }

/* 悬停要看得出来。0.025 的白在深色底上几乎等于没有 ——
   而这张表一行有十来列,鼠标横着扫的时候全靠它对齐。 */
tbody tr:hover { background: rgb(255 255 255 / 0.045); }

/* 表头是吸顶的。内容滚到它下面时给一道细影,不然文字会和表头糊在一起。 */
thead th::after {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    bottom: -1px;
    height: 1px;
    background: var(--border-soft);
}


td.num, th.num {
    text-align: right;
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
}

td.mono { font-family: var(--font-mono); font-size: 12px; }
td.actions { text-align: right; }

.empty {
    padding: var(--s6) var(--s4);
    text-align: center;
    color: var(--muted-fg);
    font-size: 13px;
}

/* ---------- 进度 ---------- */

.progress {
    display: flex;
    align-items: center;
    gap: var(--s2);
    min-width: 150px;
}

/*
 * 原生 <progress>。各家浏览器的默认外观差得离谱(有的是立体凹槽,有的带高光),
 * 所以先 appearance:none 抹平,再逐个伪元素上色 —— 轨道和填充在
 * WebKit 与 Firefox 下是两套不同的伪元素,两套都得写,漏一套就有一家变回默认样式。
 */
.progress .bar {
    flex: 1;
    appearance: none;
    -webkit-appearance: none;
    height: 5px;
    border: 0;
    border-radius: 3px;
    background: var(--muted);
    overflow: hidden;
}

.progress .bar::-webkit-progress-bar {
    background: var(--muted);
    border-radius: 3px;
}

.progress .bar::-webkit-progress-value {
    background: var(--accent);
    border-radius: 3px;
    transition: width 300ms ease-out;
}

.progress .bar::-moz-progress-bar {
    background: var(--accent);
    border-radius: 3px;
}

.progress .pct {
    font-family: var(--font-mono);
    font-size: 11px;
    color: var(--muted-fg);
    min-width: 38px;
    text-align: right;
    font-variant-numeric: tabular-nums;
}

/* ---------- 状态标 ---------- */

/*
 * 状态不靠颜色单独表达 —— 每一个都带文字。
 * 只用颜色的话,色觉障碍的人看到的是一排一模一样的圆点。
 */
.tag {
    display: inline-flex;
    align-items: center;
    vertical-align: middle;
    gap: 5px;
    padding: 2px 8px;
    border-radius: 20px;
    font-size: 11px;
    font-weight: 500;
    border: 1px solid transparent;
    white-space: nowrap;
}

.tag::before {
    content: '';
    width: 5px;
    height: 5px;
    border-radius: 50%;
    background: currentColor;
    flex: none;
}

.tag.running { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 35%, transparent); background: color-mix(in srgb, var(--accent) 10%, transparent); }
.tag.done    { color: var(--info);   border-color: color-mix(in srgb, var(--info) 35%, transparent);   background: color-mix(in srgb, var(--info) 10%, transparent); }
.tag.stalled { color: var(--warn);   border-color: color-mix(in srgb, var(--warn) 40%, transparent);   background: color-mix(in srgb, var(--warn) 12%, transparent); }
.tag.failed  { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 35%, transparent); background: color-mix(in srgb, var(--danger) 10%, transparent); }
.tag.stopped { color: var(--muted-fg); border-color: var(--border); }
/* 排队中要和"已停止"分得开:一个是还没轮到,一个是人为终止了,
   两者在列表里挨着出现,颜色一样的话没人分得清。 */
.tag.pending { color: var(--info); border-color: color-mix(in srgb, var(--info) 30%, transparent); }

/* ---------- 按钮 ---------- */

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    padding: 7px 13px;
    border-radius: var(--radius);
    border: 1px solid var(--border);
    background: var(--primary);
    color: var(--fg);
    font-size: 13px;
    font-weight: 500;
    transition: background var(--dur), border-color var(--dur), color var(--dur);
    /* 和输入框同高。差半个像素在并排放着的时候也看得出来 —— 实测按钮 35.5、
       输入框 36,一排控件的底边就是歪的。移动端下面还会再放大到 44。 */
    min-height: var(--control-h);
}

.btn:hover:not(:disabled) { background: var(--secondary); border-color: var(--muted-fg); }

.btn.primary {
    background: var(--accent);
    border-color: var(--accent);
    color: var(--on-accent);
    font-weight: 600;
}

.btn.primary:hover:not(:disabled) { background: color-mix(in srgb, var(--accent) 88%, white); }

/*
 * 危险按钮和主按钮同一种形态(实心填充),只是换个颜色。
 *
 * 原来它是描边的、主按钮是实心的 —— 两个并排放着,看起来像是分量不同的两级操作,
 * 而实际上"全部恢复"和"全部标记失败"是同一层的两个出口,只是方向相反。
 * 形态不同会让人下意识觉得其中一个才是"正经的那个"。
 *
 * 底色比 --danger 深一档,是为了让白字够得着对比度:
 * 白字压在 #ef4444 上只有 3.4:1,压在 #dc2626 上是 4.8:1。
 */
.btn.danger {
    background: var(--danger-solid);
    border-color: var(--danger-solid);
    color: #fff;
    font-weight: 600;
}

.btn.danger:hover:not(:disabled) {
    background: color-mix(in srgb, var(--danger-solid) 86%, white);
    border-color: color-mix(in srgb, var(--danger-solid) 86%, white);
}

.btn.small { padding: 4px 9px; font-size: 12px; min-height: 26px; }
.btn.ghost { background: transparent; border-color: transparent; color: var(--muted-fg); }
.btn.ghost:hover:not(:disabled) { background: var(--muted); color: var(--fg); }

@media (pointer: coarse) {
    /* 高度由 --control-h 统一给到 44,这里只补内边距 —— 手指点比鼠标点粗糙,
       光有高度不够,左右也要有余量。 */
    .btn { padding: 10px 16px; }
    /* 表格行里的按钮例外:一行本来就只有三十几像素高,
       按 44 撑会把整张表拉散。它旁边留白足够,不至于误触。 */
    .btn.small { min-height: 36px; }
}

.btn-row { display: flex; gap: var(--s2); flex-wrap: wrap; align-items: center; }

/*
 * "新建"推到最右边,和左边那组批量操作彻底分开。
 *
 * 这两者的性质完全不同:一个是往里加东西,一组是改/删已经有的东西。
 * 挨在一起的话点错的代价不对等 —— 点错"停止全部"是回不来的。
 * 隔开比加一条分隔线更管用:手要移过去,顺手点错就不会发生。
 */
.btn-row .push-right {
    margin-left: auto;
}

/* ---------- 表单 ---------- */

.field { margin-bottom: var(--s3); }

/* 标签一直在,不用占位符代替 —— 占位符一开始打字就消失,
   填到一半的表单就成了一排没人知道是什么的输入框。 */
.field label {
    display: block;
    font-size: 12px;
    color: var(--muted-fg);
    margin-bottom: 5px;
}

/*
 * 所有控件定同一个高度。
 *
 * 不定的话,每种控件按自己的字体度量算固有高度,实测差出 5.5 像素:
 * 文本框 39、下拉框 36.5、日期框 41、按钮 35.5。它们在筛选条里是底对齐的,
 * 于是顶边参差不齐,连带上面的标签也错开 —— 一排本该整齐的东西看着就是歪的。
 */
:root {
    --control-h: 36px;
}

/*
 * 触屏下把整套控件放大到 44 —— 那是手指的最小可点尺寸。
 *
 * 改这一个变量,而不是给按钮单独加一条规则:单独加的话,权重更高的选择器
 * (比如 .filters .btn)会把它压过去,于是同一排里有的按钮 44、有的 36,
 * 而且只在触屏上才看得出来。实测就踩过这个 —— 筛选条的按钮变成了 42。
 */
@media (pointer: coarse) {
    :root {
        --control-h: 44px;
    }
}

.field input, .field select, .field textarea {
    width: 100%;
    height: var(--control-h);
    padding: 0 11px;
    border-radius: var(--radius);
    border: 1px solid var(--border);
    background: var(--bg);
    color: var(--fg);
    font-size: 13px;
    transition: border-color var(--dur), box-shadow var(--dur);
}

/* 多行输入框按内容长,不受统一高度约束。 */
.field textarea {
    height: auto;
    padding: 9px 11px;
    resize: vertical;
    min-height: 96px;
    font-family: var(--font-mono);
    font-size: 12px;
    line-height: 1.6;
}

.field input:hover, .field select:hover, .field textarea:hover {
    border-color: var(--muted-fg);
}

/* 聚焦时除了焦点环再给个边框色,免得深色下光靠外面那圈白线不够明显。 */
.field input:focus, .field select:focus, .field textarea:focus {
    border-color: var(--muted-fg);
}

/*
 * 自己做的下拉框。
 *
 * 为什么不用原生 <select>:能改的只有它收起来时的样子。展开之后那个列表
 * 是操作系统画的,CSS 管不着 —— 于是一个深色界面点开下拉,弹出来一块
 * 系统配色的菜单,割裂得很明显。
 *
 * 代价是键盘和读屏的支持要自己补齐:方向键、Home/End、回车、Esc、
 * 焦点移开自动收起、aria-activedescendant。这些原生 select 是白送的,
 * 自己做就得一条条补 —— 少补一条,靠键盘的人就用不了这个筛选。
 */
.sel {
    position: relative;
    width: 100%;
}

.sel-btn {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s2);
    width: 100%;
    height: var(--control-h);
    padding: 0 11px;
    border-radius: var(--radius);
    border: 1px solid var(--border);
    background: var(--bg);
    color: var(--fg);
    font-size: 13px;
    text-align: left;
    transition: border-color var(--dur);
}

.sel-btn:hover { border-color: var(--muted-fg); }
.sel-btn[aria-expanded='true'] { border-color: var(--muted-fg); }

.sel-btn .sel-text {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.sel-btn .sel-arrow {
    flex: none;
    color: var(--muted-fg);
    transition: transform var(--dur), color var(--dur);
}

.sel-btn:hover .sel-arrow { color: var(--fg); }
/* 箭头跟着开合翻转 —— 它是"现在是开是合"最直接的那个提示。 */
.sel-btn[aria-expanded='true'] .sel-arrow { transform: rotate(180deg); color: var(--fg); }

.sel-list {
    position: absolute;
    z-index: 50;
    top: calc(100% + 4px);
    left: 0;
    right: 0;
    margin: 0;
    padding: 4px;
    list-style: none;
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    max-height: 260px;
    overflow-y: auto;
}

.sel-list[hidden] { display: none; }

.sel-opt {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s2);
    padding: 7px 9px;
    border-radius: 4px;
    font-size: 13px;
    color: var(--fg);
    cursor: pointer;
    white-space: nowrap;
}

/*
 * 键盘高亮和鼠标悬停用同一个底色,但它们是两回事:
 * active 是"回车会选中这一项",hover 只是鼠标碰到了。
 * 两者同时存在时以键盘的为准 —— 不然人用方向键走到一半,
 * 鼠标恰好停在别处,回车下去选中的是另一个。
 */
.sel-opt:hover { background: var(--muted); }
.sel-opt.active { background: var(--primary); }

.sel-opt[aria-selected='true'] {
    color: var(--accent);
    font-weight: 500;
}

/* 选中的那一项打个勾。只靠颜色的话,色觉障碍的人看到的是一样的一行。 */
.sel-opt[aria-selected='true']::after {
    content: '';
    width: 12px;
    height: 12px;
    flex: none;
    background: currentColor;
    -webkit-mask: var(--check) center / contain no-repeat;
    mask: var(--check) center / contain no-repeat;
}

/* 日期框:原生的日历图标在深色下是个几乎看不见的白块。 */
.field input[type='date'] {
    padding-right: 8px;
}

.field input[type='date']::-webkit-calendar-picker-indicator {
    filter: invert(0.65);
    cursor: pointer;
    opacity: 0.85;
    transition: opacity var(--dur);
}

.field input[type='date']:hover::-webkit-calendar-picker-indicator {
    opacity: 1;
}

.field input::placeholder {
    color: color-mix(in srgb, var(--muted-fg) 70%, transparent);
}

.field .hint { font-size: 11px; color: var(--muted-fg); margin-top: 4px; }
.field .err { font-size: 12px; color: var(--danger); margin-top: 4px; }
.field input[aria-invalid='true'] { border-color: var(--danger); }

.form-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
    gap: 0 var(--s3);
}

/* ---------- 弹层 ---------- */

dialog {
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    background: var(--card);
    color: var(--fg);
    padding: 0;
    max-width: 460px;
    width: calc(100% - 32px);
    box-shadow: var(--shadow);
}

dialog::backdrop {
    background: rgb(2 6 23 / 0.72);
    backdrop-filter: blur(3px);
}

dialog .body { padding: var(--s5); }
dialog h2 { margin: 0 0 var(--s2); font-size: 15px; }
dialog p { margin: 0 0 var(--s4); color: var(--muted-fg); font-size: 13px; line-height: 1.6; }
dialog .btn-row { justify-content: flex-end; }

/* 批量下单的弹窗比确认框宽:清单一行就有六十来个字符,窄了会折行,
   折行之后一行一个任务的对应关系就看不出来了。 */
/*
 * 一组按钮,中间用一条竖线隔开危险的那个。
 *
 * 用分隔线而不是单纯留一段空白:空白看起来像没排好,而一条线是明确的
 * "这两边不是一回事"。它隔开的两个按钮代价完全不对等 ——
 * 建错了删掉重来,删错了就没了,手滑一次的后果差着量级。
 *
 * 真正的门是那个要打字的确认框,这条线只是让手别顺势滑过去。
 */
.btn-group {
    display: flex;
    align-items: center;
    gap: var(--s2);
}

.btn-divider {
    width: 1px;
    height: 18px;
    margin: 0 var(--s2);
    background: var(--border);
    flex: none;
}

#wipe-dialog { max-width: 480px; }
#wipe-dialog code { color: var(--danger); font-weight: 600; }

#batch-dialog { max-width: 640px; }
#batch-dialog code { font-family: var(--font-mono); font-size: 12px; color: var(--fg); }
#batch-dialog textarea { min-height: 160px; }

/* 没建成的那几行。留在弹窗里而不是弹个提示就完事 —— 提示会消失,
   而人需要照着它改清单。 */
.batch-errs {
    margin-bottom: var(--s4);
    padding: var(--s3);
    border: 1px solid var(--danger);
    border-left-width: 3px;
    border-radius: var(--radius);
    background: color-mix(in srgb, var(--danger) 8%, transparent);
    font-size: 12px;
    max-height: 180px;
    overflow-y: auto;
}
.batch-errs ul { margin: var(--s2) 0 0; padding-left: 18px; }
.batch-errs li { margin-bottom: 4px; color: var(--muted-fg); }
.batch-errs code { color: var(--fg); }

/* ---------- 提示条 ---------- */

.toasts {
    position: fixed;
    right: var(--s4);
    bottom: var(--s4);
    display: flex;
    flex-direction: column;
    gap: var(--s2);
    z-index: 60;
    max-width: min(400px, calc(100vw - 32px));
}

.toast {
    display: flex;
    align-items: flex-start;
    gap: var(--s2);
    padding: var(--s3) var(--s4);
    border-radius: var(--radius);
    border: 1px solid var(--border);
    border-left: 3px solid var(--accent);
    background: var(--card);
    box-shadow: var(--shadow);
    font-size: 13px;
    animation: slide-in 220ms ease-out;
}

.toast.leaving { opacity: 0; transition: opacity 200ms; }
.toast.error { border-left-color: var(--danger); }
.toast.warn  { border-left-color: var(--warn); }

@keyframes slide-in {
    from { opacity: 0; transform: translateY(8px); }
    to   { opacity: 1; transform: none; }
}

/* ---------- 登录 ---------- */

.login {
    min-height: 100vh;
    display: grid;
    place-items: center;
    padding: var(--s4);
}

.login .box {
    width: 100%;
    max-width: 360px;
    background: var(--card);
    border: 1px solid var(--border-soft);
    border-radius: var(--radius-lg);
    padding: var(--s6);
    box-shadow: var(--shadow);
}

.login h1 {
    margin: 0 0 var(--s1);
    font-size: 17px;
    display: flex;
    align-items: center;
    gap: var(--s2);
}

.login .sub { color: var(--muted-fg); font-size: 13px; margin: 0 0 var(--s5); }
.login .btn { width: 100%; margin-top: var(--s2); }

/* ---------- 杂项 ---------- */

.muted { color: var(--muted-fg); }
.mono { font-family: var(--font-mono); }
.nowrap { white-space: nowrap; }
.right { text-align: right; }
.spacer { flex: 1; }
.hidden { display: none !important; }

.checkbox {
    width: 15px;
    height: 15px;
    accent-color: var(--accent);
    cursor: pointer;
}

.sr-only {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
}

.spin { animation: spin 900ms linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }

/* 只有一个页可看时(普通用户),整条导航收起来 —— 一个只有一项的菜单
   不提供任何信息,只是白占 208 像素。 */
.nav.single { display: none; }

/* 只在宽屏收起。窄屏本来就没有侧栏,再套一层两列反而把顶栏挤扁。 */
@media (min-width: 901px) {
    .app:has(.nav.single) {
        grid-template-areas: 'brand topbar' 'main main';
    }
}

/* ---------- 取代内联样式的工具类 ---------- */

/*
 * 这些原本是写在标签上的 style="" 属性。挪进来有两个理由:
 * 一是内容安全策略不放行内联样式(放行它等于把刚加的那道防护拆掉);
 * 二是散落在模板字符串里的样式改起来根本找不全。
 */

.mb-3 { margin-bottom: var(--s3); }
.mt-4 { margin-top: var(--s4); }
.fs-12 { font-size: 12px; }
.fs-13 { font-size: 13px; }
.danger-text { color: var(--danger); }
.col-pick { width: 34px; }
.col-progress { min-width: 150px; }
.col-reason { white-space: normal; max-width: 38ch; }
.nudge-left { margin-left: -6px; }
.unit { font-size: 13px; }

/* 出口整体不可用时的那块告警,底色比一般的警告更重。 */
.alert.critical {
    border-color: var(--danger);
    background: color-mix(in srgb, var(--danger) 8%, var(--card));
}
.alert.critical h2 { color: var(--danger); }

/* 登录页的错误行。默认藏着,出错时脚本把 hidden 去掉。 */
.login-err {
    color: var(--danger);
    font-size: 13px;
}

/* ---------- 筛选与分页 ---------- */

/*
 * 筛选条横着排,窄了自动换行。每一栏都带独立的标签 —— 只靠占位符的话,
 * 一填上内容就不知道这栏原本是什么了,而筛选条恰恰是长期填着内容的。
 */
.filters {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: var(--s3);
    margin-bottom: var(--s3);
    padding-bottom: var(--s3);
    border-bottom: 1px solid var(--border-soft);
}

.filters .field {
    margin-bottom: 0;
    /* 默认不伸展,给一个够放下标签的固定宽度。 */
    flex: 0 0 132px;
}

/*
 * 两个 ID 输入框按**角色**配对,不按位置。
 *
 * 原来写的是 :nth-child(2),于是只有笔记 ID 会伸展,作者 ID 不会 ——
 * 实测一个 233 一个 199,并排放着一眼就看得出不齐。而且位置规则很脆:
 * 中间插一栏,伸展的就换成别人了,而没人会想到去改这条 CSS。
 *
 * 两个都写 flex: 1 1 0 才是"平分剩余空间",于是它们永远一样长。
 */
.filters .field.grow {
    flex: 1 1 0;
    /* 下限压得低一点,好让整排(六个字段加一个按钮)在常见宽度下挤得进一行。
       之前定 160,七项的基准宽度加起来正好比容器多几十像素,
       于是"重置"被挤到第二行单独站着 —— 一个按钮独占一行看着就是没排好。 */
    min-width: 120px;
}

/* 日期框要比默认宽一点:年月日三段加上日历图标,132 放不下。 */
.filters .field.date {
    flex: 0 0 154px;
}

/* 按钮和输入框同高,底边才对得齐。
   高度走 --control-h,不写死数字 —— 写死的话触屏下那条 44px 的规则会被
   这个更高权重的选择器压过去,同一排里就会有的 44 有的 36。 */
.filters .btn {
    min-height: var(--control-h);
    flex: 0 0 auto;
}

.pager {
    display: flex;
    align-items: center;
    gap: var(--s2);
    margin-top: var(--s3);
    padding-top: var(--s3);
    border-top: 1px solid var(--border-soft);
    flex-wrap: wrap;
}

@media (max-width: 640px) {
    .filters .field { min-width: 100%; }
    .pager { justify-content: center; }
    .pager .spacer { flex: 0 0 100%; }
}

/* ---------- 系统面板 ---------- */

/*
 * 分组横排,每组内部是"标签 — 值 — 说明"三列。
 *
 * 用行而不是卡片:这里的指标有二十来项,做成卡片会占掉整屏,
 * 而它们多数时候只是被扫一眼。密排才扫得快。
 */
.sys-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    gap: var(--s5) var(--s6);
}

.sys-group h3 {
    margin: 0 0 var(--s2);
    padding-bottom: 6px;
    border-bottom: 1px solid var(--border-soft);
    font-size: 12px;
    font-weight: 600;
    color: var(--muted-fg);
    text-transform: uppercase;
    letter-spacing: 0.06em;
}

.sys-row {
    display: grid;
    grid-template-columns: 1fr auto;
    align-items: baseline;
    gap: var(--s2);
    padding: 5px 0;
    font-size: 13px;
}

.sys-label { color: var(--muted-fg); }

.sys-value {
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
    text-align: right;
    white-space: nowrap;
}

/* 说明占满第二行。它是给"这个数字为什么重要"留的位置 ——
   比如 fd 用量旁边那句"常驻连接数 = 账号数 × …",
   没有它,那个数字对第一次看的人毫无意义。 */
.sys-hint {
    grid-column: 1 / -1;
    font-size: 11px;
    color: color-mix(in srgb, var(--muted-fg) 75%, transparent);
    line-height: 1.5;
}

.sys-value .good { color: var(--accent); }
.sys-value .warn { color: var(--warn); }
.sys-value .bad  { color: var(--danger); }

/* 调参那一行:输入框和按钮并排。 */
.tune-row {
    display: flex;
    gap: var(--s2);
    align-items: center;
}
.tune-row input { flex: 1; }
.tune-row .btn { flex: none; }

/* ---------------------------------------------------------------- 日志台 */

/* 卡片是定位参照:"回到底部"浮在日志区右下角,得相对它定位。 */
.log-card { position: relative; padding: var(--s3); }

.log-bar {
    display: flex;
    align-items: center;
    gap: var(--s2);
    margin-bottom: var(--s3);
}

.log-bar input[type="search"] {
    flex: 0 1 320px;
    min-width: 0;
    height: var(--control-h);
    padding: 0 var(--s3);
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--fg);
    font-size: 13px;
}

.log-bar input[type="search"]:focus-visible {
    outline: none;
    border-color: var(--primary);
    box-shadow: var(--ring);
}

/* 高度写死而不是跟着内容长:日志是无限的,让它撑开页面等于让整页永远在滚,
   而顶上的筛选框会被推出视野 —— 那是最需要留在手边的控件。 */
.log-body {
    height: min(62vh, 640px);
    overflow-y: auto;
    overflow-x: auto;
    background: var(--bg);
    border: 1px solid var(--border-soft);
    border-radius: var(--radius);
    padding: var(--s2) 0;
    font-family: var(--font-mono);
    font-size: 12.5px;
    line-height: 1.7;
}

.log-line {
    display: flex;
    gap: var(--s3);
    padding: 1px var(--s3);
    white-space: pre-wrap;
    /* 日志里最长的是 URL 和错误串,它们没有空格,断不开就会把整行撑宽。 */
    overflow-wrap: anywhere;
}

.log-line:hover { background: var(--muted); }

/* 时刻列固定宽度并且不参与换行,让所有正文左边对齐成一条线 ——
   扫一列时间戳比扫一堆参差不齐的开头快得多。 */
.log-at {
    flex: none;
    color: var(--muted-fg);
    font-variant-numeric: tabular-nums;
    user-select: none;
}

.log-text { flex: 1; min-width: 0; }

/* 分级只染文字不染底色:一屏里报警往往连着好几条,整块底色会糊成一片。 */
.log-line.warn .log-text { color: var(--warn); }
.log-line.err  .log-text { color: var(--danger); }
.log-line.note .log-text { color: var(--info); }

.log-tail {
    position: absolute;
    right: calc(var(--s3) + var(--s3));
    bottom: calc(var(--s3) + var(--s3));
    box-shadow: var(--shadow);
}

@media (max-width: 640px) {
    .log-body { height: 60vh; font-size: 12px; }
    .log-bar input[type="search"] { flex: 1 1 auto; }
}

/* 任务表里几个"出事了才出现"的读数。
   平时不占视觉位置,出事时一眼能看到 —— 所以只给颜色,不给底色也不给边框。 */

/* 失败次数跟在"完成 / 下单量"后面。它和完成数是同一个量级的东西,
   分开一列反而要人左右对照;贴着放,一眼就知道这单烧掉了多少。 */
.fail-n { color: var(--danger); }

/* 还在跑、但很久没往前走。状态那一格是绿的,唯一能戳穿它的就是这个时刻。 */
.stale-cell { color: var(--warn) !important; }
