/* OKC Cloud —— 纸与墨：技术文档式的操作台
 *
 * 设计语言来源：lroolle/design-skill 的 house 体系（MIT）——"墨在纸上、
 * 技术与管理语域、中文原生、**用分隔线而不是盒子**、无渐变、阴影只用于浮层"。
 * 它给出的两条判据决定了一切：
 *   1) 让界面更好看但让活更难干，就是 costume，必须不做；
 *   2) 只看品类就能猜到长什么样（近黑 + 酸性强调色的"开发者反射"），就还不算设计。
 *
 * 因此这里刻意不做"深色黑客控制台"：那是 2026 年最饱和的一类 AI 生成外观。
 * 纸是暖的、墨是冷的、直尺线是第三种材质、朱砂印泥是唯一的彩——
 * 四种材质各有出处，而不是一个色相拉四个明度（那正是"生成感"的来源）。
 *
 * 加载顺序：tokens -> floor -> 外壳 -> 组件。
 */

/* ============================================================ tokens */

:root {
  /* 纸：80 年代铁路时刻表小册子的胶版纸，边缘发黄 */
  --bg:          oklch(0.960 0.010 88);
  --surface:     oklch(0.935 0.011 88);   /* 同一种纸，压了一层浅网 */
  --surface-2:   oklch(0.900 0.012 85);   /* 更重的网，纸色更明显 */
  --overlay:     oklch(0.970 0.009 88);   /* 覆在上面的一张新纸 */

  /* 墨：铅印碳黑，永远不是纸的色相 */
  --fg:          oklch(0.210 0.010 255);
  --fg-2:        oklch(0.450 0.008 250);  /* 半压印 */
  --fg-3:        oklch(0.520 0.006 240);  /* 占位符/附注：实测对纸 4.9:1（0.55 时只有 4.33） */

  /* 直尺线：铅笔石墨，暖的，第三种材质 */
  --line:        oklch(0.800 0.006 60);
  --line-strong: oklch(0.620 0.008 60);

  /* 唯一的彩：朱砂印泥（印泥），盖上去、略有洇开 */
  --accent:       oklch(0.550 0.190 32);
  --accent-hover: oklch(0.500 0.195 30);
  --accent-fg:    oklch(0.960 0.010 88);
  --accent-soft:  oklch(0.930 0.035 35);

  /* 语义色：同一张桌上的其它墨。
   * 每个语义色分两个用途，因为对比度要求相反：
   *   --warn      画在纸上的**线与底**（边框、软底），彩度可以高；
   *   --warn-ink  画在纸上的**文字**，必须压暗到 4.5:1 以上。
   * 藤黄本身很亮，直接当文字只有 2.4:1 —— 这是实测出来的，不是猜的。 */
  --ok:          oklch(0.520 0.090 165);   /* 铜印的铜绿 */
  --ok-soft:     oklch(0.930 0.030 165);
  --warn:        oklch(0.700 0.140 85);    /* 藤黄 */
  --warn-ink:    oklch(0.470 0.115 80);    /* 文字用：更暗的藤黄 */
  --warn-soft:   oklch(0.950 0.045 88);
  --danger:      oklch(0.470 0.200 25);
  --danger-soft: oklch(0.940 0.035 28);
  --info:        oklch(0.480 0.090 250);   /* 钢笔墨蓝 */
  --info-soft:   oklch(0.935 0.025 250);
  --focus:       oklch(0.550 0.140 250);   /* 焦点环用蓝，永远不会被误读成报错 */

  /* 字体：中文优先（PingFang/鸿蒙/米思/HarmonyOS…），拉丁字体嵌进它的比例里；
   * 等宽只用于代码、数字、编号 —— 不是"技术感"的戏服。 */
  --font-cjk:   "PingFang SC", "HarmonyOS Sans SC", "MiSans", "Noto Sans SC", "Source Han Sans SC", "Microsoft YaHei", sans-serif;
  --font-body:  var(--font-cjk), "Public Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-mono:  ui-monospace, "SF Mono", "Sarasa Mono SC", "Cascadia Mono", Menlo, Consolas, monospace;

  /* 字级：比例 1.2（"安静的那一档"），基准 1rem，密集视图 0.9375rem。
   * 相邻级别差 ≥1.2 倍，否则会出现"扁平字级"——那是 AI 生成味的典型症状。 */
  --text-xs:   0.694rem;   /* 11.1px 仅表格附注 */
  --text-sm:   0.833rem;   /* 13.3px 元信息 */
  --text-base: 1rem;       /* 16px */
  --text-lg:   1.2rem;
  --text-xl:   1.44rem;
  --text-2xl:  1.728rem;
  --text-3xl:  clamp(1.9rem, 1.5rem + 1.2vw, 2.074rem);
  --text-4xl:  clamp(2.2rem, 1.6rem + 2vw, 2.488rem);

  --leading-body:  1.75;   /* 中文正文 1.7-1.8 */
  --leading-tight: 1.32;   /* 中文标题 1.3-1.4 */
  --tracking:      0;      /* 中文永不使用负字距：笔画会撞在一起 */
  --measure:       38em;   /* 中文一行 30-40 字 */

  /* 间距：4px 基准，密度 0.9 —— 是一张表单，不是一间休息室 */
  --density: 1.05;
  --space-1:  calc(0.25rem * var(--density));
  --space-2:  calc(0.5rem  * var(--density));
  --space-3:  calc(0.75rem * var(--density));
  --space-4:  calc(1rem    * var(--density));
  --space-5:  calc(1.5rem  * var(--density));
  --space-6:  calc(2rem    * var(--density));
  --space-7:  calc(3rem    * var(--density));
  --space-8:  calc(4rem    * var(--density));
  --space-9:  calc(6rem    * var(--density));

  /* 形状：近乎直角，印刷表单的转角语言。**只有一套圆角**。 */
  --radius:    0.125rem;
  --radius-sm: 0;
  --radius-lg: 0.25rem;
  --line-w:    1px;
  --rule-w:    2px;        /* 开一个区块用的重线 */

  /* 海拔：只有浮层才有阴影；一张纸盖在另一张纸上会投下柔和的影 */
  --shadow-1: 0 1px 0 oklch(0.21 0.01 255 / 0.10), 0 2px 6px oklch(0.21 0.01 255 / 0.06);
  --shadow-2: 0 2px 0 oklch(0.21 0.01 255 / 0.12), 0 12px 28px oklch(0.21 0.01 255 / 0.10);

  /* 动效：机械式——短、干脆、不表演。60/120/160ms，线性或步进。 */
  --dur-micro: 60ms;
  --dur-base:  120ms;
  --dur-enter: 160ms;
  --ease-out:  linear;
  --ease-step: steps(4, end);

  --container: 68rem;

  color-scheme: light dark;
}

/* 暗色 = 另一件东西：碑帖拓片。墨是地，纸是字，边线是纸自己的灰。
 * 不是反转：地是冷墨（L 0.17 带可辨彩度），字是暖纸（L 0.92，永不用纯白）。 */
[data-theme="dark"] {
  --bg:          oklch(0.170 0.008 250);
  --surface:     oklch(0.210 0.008 250);
  --surface-2:   oklch(0.250 0.008 250);
  --overlay:     oklch(0.200 0.008 250);
  --fg:          oklch(0.920 0.010 88);
  --fg-2:        oklch(0.720 0.010 88);
  --fg-3:        oklch(0.600 0.008 85);
  --line:        oklch(0.300 0.006 250);
  --line-strong: oklch(0.420 0.008 250);
  /* 暗底上的强调色要提亮并降彩度：饱和色在暗底会晕光 */
  --accent:       oklch(0.680 0.160 32);
  --accent-hover: oklch(0.730 0.160 32);
  --accent-fg:    oklch(0.170 0.008 250);
  --accent-soft:  oklch(0.280 0.050 32);
  --ok:          oklch(0.700 0.090 165);
  --ok-soft:     oklch(0.240 0.030 165);
  --warn:        oklch(0.800 0.130 88);
  --warn-ink:    oklch(0.800 0.130 88);   /* 暗底上藤黄本身就够亮，文字直接用 */
  --warn-soft:   oklch(0.260 0.040 88);
  --danger:      oklch(0.660 0.170 25);
  --danger-soft: oklch(0.250 0.045 28);
  --info:        oklch(0.700 0.090 250);
  --info-soft:   oklch(0.240 0.030 250);
  --focus:       oklch(0.720 0.130 250);
  --shadow-1: 0 1px 0 oklch(0 0 0 / 0.30), 0 2px 8px oklch(0 0 0 / 0.30);
  --shadow-2: 0 2px 0 oklch(0 0 0 / 0.35), 0 16px 40px oklch(0 0 0 / 0.45);
}

/* ============================================================= floor */

*, *::before, *::after { box-sizing: border-box; }
[hidden] { display: none !important; }

html, body {
  margin: 0;
  min-height: 100%;
  background: var(--bg);
  color: var(--fg);
  /* 中文正文 15px/1.75：一行 26px 高，38em 一行 37-38 个字——一栏印刷时刻表的宽度 */
  font-family: var(--font-body);
  font-size: var(--text-base);
  line-height: var(--leading-body);
  letter-spacing: var(--tracking);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
/* 破折号、省略号、避头尾：中文排版的三条基线 */
body { line-break: strict; }
:lang(zh) { text-autospace: normal; }

h1, h2, h3, h4 { margin: 0; line-height: var(--leading-tight); font-weight: 600; text-wrap: balance; }
p { margin: 0; text-wrap: pretty; }
/* 链接默认不带下划线（重写这一版时漏了 text-decoration: none，结果商标、
 * 导航、按钮式链接全被浏览器加了下划线——那是浏览器默认样式，不是设计）。
 * 下划线只留给"行文里的链接"和悬停：正文中的蓝字如果不带下划线，
 * 读者分不清它是不是链接。 */
a { color: var(--accent); text-decoration: none; text-decoration-thickness: 1px; text-underline-offset: 2px; }
a:hover { color: var(--accent-hover); text-decoration: underline; }
/* 行文里的链接（markdown 正文、接口说明）始终带下划线 */
.md a, .api-note a { text-decoration: underline; }
hr { border: 0; border-top: var(--line-w) solid var(--line); margin: var(--space-5) 0; }
::selection { background: var(--accent-soft); color: var(--fg); }
img, svg { max-width: 100%; }

/* 滚动条：细、暗、不抢视线。任务页有两栏各自滚动的面板（时间线 / 遥测），默认那条
 * 带槽的粗灰条在暗色页面上比内容还显眼——两条并排时尤其像出了故障。 */
* { scrollbar-width: thin; scrollbar-color: var(--line-strong) transparent; }
*::-webkit-scrollbar { width: 10px; height: 10px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb {
  background: var(--line-strong); border-radius: 999px;
  border: 3px solid transparent; background-clip: content-box;
}
*::-webkit-scrollbar-thumb:hover { background: var(--fg-3); background-clip: content-box; }

/* 焦点：键盘走到哪里，环就亮到哪里。蓝色，永远不会被误读成报错。 */
:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }

/* 等宽只给代码、数字、编号 —— mono 当"技术感"用是最典型的 AI 味之一。 */
.mono, code, kbd, samp { font-family: var(--font-mono); font-size: 0.9em; }
.stat-line, .timer, .quota-line, .hmeta, .dtable, .ledger,
.leaf-num, .idval, .turn-ts, .ts, .bar-cap, .count-num {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums slashed-zero;
}

/* 只给读屏的文本 */
.visually-hidden {
  position: absolute; width: 1px; height: 1px; margin: -1px;
  padding: 0; border: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
}

/* ============================================================= 外壳 */

.app { min-height: 100dvh; }

/* 侧边栏 + 工作区。上一版是"居中的一条窄栏"：1440 宽下右侧 800px 全是空白，
 * 历史页尤其像没做完。app-shell 配方要求的是常驻导航 + 占满余宽的内容区。 */
.shell { display: flex; align-items: stretch; min-height: 100dvh; }

/* 滚动时的顶栏/侧栏：JS 一直在切 `.is-scrolled`（`window.scrollY > 8`），而**这条
 * 规则从来没写**——于是滚动时什么都不会发生，代码注释里那句"把'悬浮层盖在内容上'
 * 的关系画出来"一直是空话。窄屏（≤900px）下这条横条是粘的、内容从它下面滚过去，
 * 最需要它；宽屏它是左栏，一层浅投影也无害。 */
.topbar.is-scrolled { box-shadow: var(--shadow-1); }

.sidebar {
  flex: 0 0 15rem;
  width: 15rem;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  padding: var(--space-5) var(--space-4);
  border-right: var(--line-w) solid var(--line);
  background: var(--surface);
  position: sticky;
  top: 0;
  height: 100dvh;
  overflow-y: auto;
}
.workspace { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }

/* 商标 */
.brand {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-2);
  font-weight: 700;
  font-size: var(--text-lg);
  color: var(--fg);
  letter-spacing: 0;
}
.brand:hover { text-decoration: none; color: var(--fg); }
.brand-sub { color: var(--fg-2); font-weight: 400; font-size: var(--text-sm); }

/* 导航：竖排，当前项用左侧重线 + 字重标记（不用色块、不用药丸） */
.topnav { display: flex; flex-direction: column; gap: 1px; margin: 0; }
.navlink {
  color: var(--fg-2);
  font-size: var(--text-base);
  white-space: nowrap;
  padding: var(--space-2) var(--space-3);
  border-left: var(--rule-w) solid transparent;
  border-radius: 0 var(--radius) var(--radius) 0;
}
.navlink:hover { color: var(--fg); background: var(--bg); text-decoration: none; }
.navlink.is-on { color: var(--fg); font-weight: 600; border-left-color: var(--fg); background: var(--bg); }
.side-foot { margin-top: auto; display: flex; gap: var(--space-4); padding-top: var(--space-4); border-top: var(--line-w) solid var(--line); }
/* 侧栏顶部的常驻动作 + 底部的账号读数：让左栏有用途，而不是一条导航加一片空白 */
.side-action { display: block; }
.side-action .btn { width: 100%; }

/* 「购买积分」：侧栏里的**独立分区**（顶上一条重线把它和导航分开）。
 * 窄屏时整格折成一行标题，点开是一块浮层——顶部横条必须保持一行高。 */
.side-buy { border-top: var(--rule-w) solid var(--fg); padding-top: var(--space-3); }
.side-buy-head { display: none; }
.side-buy-title { font-size: var(--text-base); font-weight: 600; margin: 0 0 var(--space-2); }
.side-buy-body { display: flex; flex-direction: column; gap: var(--space-2); }
/* 这张表是侧栏里唯一带边线的表格，所以不沿用 `.table`（那是主栏数据表的语言：
 * 满宽、隔行底色）。这里是**价目牌**：窄（15rem 侧栏放得下四列）、密、
 * 行间用铅笔线——继续"分隔线而不是盒子"。 */
.buy-table { width: 100%; border-collapse: collapse; font-size: var(--text-xs); table-layout: fixed; }
.buy-table th, .buy-table td { padding: var(--space-1) 0; text-align: right; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.buy-table th:first-child, .buy-table td:first-child { text-align: left; }
.buy-table thead th { color: var(--fg-2); font-weight: 400; border-bottom: var(--line-w) solid var(--line-strong); }
.buy-table tbody tr + tr td { border-top: var(--line-w) solid var(--line); }
/* 轻重：首列（档位）是主语，用正墨；金额是动作（可点的价格）；后两列是
 * 参考换算，压成半压印——同一行里三个声部，不是一排等重的数字。 */
.buy-table td:first-child { color: var(--fg); font-weight: 500; }
.buy-table td:nth-child(3), .buy-table td:nth-child(4) { color: var(--fg-2); }
/* 「¥/百万」在窄屏（浮层）里放不下第四列：那列是给比价用的，手机上
 * 顶栏展开面板本来就窄，砍掉它保住前三列的可读性。 */
@media (max-width: 900px) {
  .buy-table th:nth-child(4), .buy-table td:nth-child(4) { display: none; }
}
.side-buy-note { color: var(--fg-2); font-size: var(--text-xs); line-height: 1.6; margin: 0; }
.buy-cta { width: 100%; text-align: center; }
/* 每一档的价格自己就是购买链接（金额列里那颗 `<a>`）：靠下划线把它和纯文本价格
 * 区分开，不额外摆一排按钮——一档一个链接，点哪个买哪个。 */
.buy-link { color: inherit; font-weight: 600; text-decoration: underline; text-underline-offset: 2px; }
.buy-link:hover { text-decoration-thickness: 2px; }
#buy-qr { width: 100%; max-width: 10rem; margin: var(--space-1) auto 0; display: block; }

/* 「价格与档位」（管理员改价）：主栏里的编辑表。它和侧栏那张价目牌是**两种东西**
 * ——那边是给人读的窄表，这边是给人填的表单，所以不复用 `.buy-table`：
 * 输入框要占满单元格、左对齐、行高留够点得准。 */
.price-table { width: 100%; border-collapse: collapse; font-size: var(--text-sm); margin-top: var(--space-3); }
.price-table th, .price-table td { padding: var(--space-2) var(--space-2); text-align: left; border-bottom: var(--line-w) solid var(--line); vertical-align: middle; }
.price-table th { color: var(--fg-2); font-weight: 400; font-size: var(--text-xs); border-bottom-color: var(--line-strong); }
.price-table th:first-child, .price-table td:first-child { padding-left: 0; }
.price-table th:last-child, .price-table td:last-child { padding-right: 0; width: 1%; white-space: nowrap; }
.price-table td:nth-child(1) { width: 22%; }
.price-table td:nth-child(2) { width: 20%; }
.price-input { width: 100%; min-width: 0; }
.price-actions { display: flex; gap: var(--space-3); align-items: center; margin-top: var(--space-3); }
.side-meta { margin-top: auto; display: flex; flex-direction: column; gap: var(--space-1); padding-top: var(--space-4); font-size: var(--text-sm); color: var(--fg-2); }
.side-meta b { color: var(--fg); font-weight: 600; }
.linkbtn {
  background: none; border: 0; padding: 0;
  color: var(--fg-2); font: inherit; font-size: var(--text-sm); cursor: pointer;
}
.linkbtn:hover { color: var(--fg); text-decoration: underline; text-underline-offset: 2px; }

.main { flex: 1 1 auto; display: flex; flex-direction: column; }

/* 视图：占满工作区宽度，内容左对齐（居中会在宽屏上留下两大块空白） */
.view {
  flex: 1 1 auto;
  width: 100%;
  max-width: 82rem;
  margin: 0 auto;
  padding: var(--space-6) var(--space-6) var(--space-9);
  display: grid;
  gap: var(--space-6);
  align-items: start;
}
/* 两栏视图：主内容 + 侧栏（≥1100px 才分栏，否则单栏）。
 *
 * 分栏的那一档同时让这一页**自己占满一屏**：两栏都吃满视口高度，各自的内部面板滚
 * 自己的，文档因此不再溢出。这一条是照着线上截图改的——以前时间线有 `58vh` 的封顶，
 * 页面照样溢出（出**整页**那条滚动条），而侧栏又吸顶 + 自己的 max-height（出**第二条**），
 * 两条并排贴在右边缘，又丑又不随窗口变。
 *
 * 高度算式 = 视口 − `.view` 自己的上下内边距；窗口极矮时用 30rem 兜底（那时宁可让整页
 * 滚一点，也不要把时间线挤成一条缝）。任务页那一段只负责"哪块面板滚"。 */
.view-split { grid-template-columns: minmax(0, 1fr); }
@media (min-width: 1100px) {
  .view-split {
    grid-template-columns: minmax(0, 1fr) minmax(0, 21rem);
    height: max(30rem, calc(100dvh - var(--space-6) - var(--space-9)));
    align-items: stretch;
    overflow: hidden;
  }
  .view-split > .span-all { grid-column: 1 / -1; }
}
/* 登录页：介绍 + 表单两栏 */
.view-login { max-width: 72rem; }
@media (min-width: 1000px) { .view-login { grid-template-columns: minmax(0, 1fr) 24rem; } }
.login-wrap { display: contents; }
/* 左栏是长介绍、右栏是短表单——桌面端把登录卡 sticky 住，往下滑介绍时
   表单一直在手边，右栏不再是一大片跟不上的空白（"左右不平衡"的病根）。 */
.login-card { align-self: start; }
@media (min-width: 1000px) {
  .login-card { position: sticky; top: var(--space-6); }
}
/* 表单卡里的量级与三条承诺：接在表单后面，间距收紧，最后一块不留底边距 */
.login-card .lip-caps { margin: var(--space-4) 0 0; }
.login-card .lip-list { margin: var(--space-4) 0 0; gap: var(--space-2); }

.card { width: 100%; min-width: 0; }
.card-narrow, .card-wide { max-width: none; }
/* 首页：表单给一个够用的宽度，右边留出概览的位置（两栏都吃满） */
@media (min-width: 1000px) {
  .view-home { grid-template-columns: minmax(0, 32rem) minmax(0, 1fr); }
}
/* 未登录：收起侧栏，落地页自成一屏并居中 */
.app.is-login .sidebar { display: none; }
.app.is-login .view-login { margin: 0 auto; padding-top: var(--space-8); }

/* 侧边栏在窄屏变成顶部横条：所有顶层去处仍然可见（不做汉堡菜单） */
@media (max-width: 900px) {
  .shell { flex-direction: column; }
  .sidebar {
    width: auto;
    flex: 0 0 auto;
    height: auto;
    position: sticky;
    top: 0;
    z-index: 30;
    flex-direction: row;
    align-items: center;
    gap: var(--space-4);
    padding: var(--space-3) var(--space-4);
    border-right: 0;
    border-bottom: var(--line-w) solid var(--line);
    overflow: visible;
  }
  .sidebar .topnav { flex-direction: row; flex-wrap: wrap; gap: var(--space-1); }
  .sidebar .navlink { border-left: 0; border-bottom: var(--rule-w) solid transparent; border-radius: 0; padding: var(--space-1) var(--space-2); }
  .sidebar .navlink.is-on { border-bottom-color: var(--fg); background: none; }
  .side-foot { margin-top: 0; margin-left: auto; padding-top: 0; border-top: 0; }
  /* 「购买积分」在窄屏折起来：标题是一行按钮，展开是一块浮层（不把顶栏撑高）。
   * 浮层的锚是 `.side-buy` 自己（position:relative），但它的宽度只有按钮那么宽
   * ——`left:0;right:0` 会被夹成一条 ~90px 的窄缝。改锚到侧栏（先给它
   * position:relative），面板从顶栏左缘展开，宽度吃满整条顶栏，min-width 兜底。 */
  .side-buy { border-top: 0; padding-top: 0; position: static; }
  .side-buy-title { display: none; }
  .side-buy-head {
    display: inline-flex; align-items: center; gap: var(--space-1);
    background: none; border: 0; cursor: pointer; padding: var(--space-1) var(--space-2);
    color: var(--fg-2); font: inherit; font-size: var(--text-base);
  }
  .side-buy-head:hover { color: var(--fg); }
  .side-buy-arrow { font-size: var(--text-xs); }
  .side-buy.is-open .side-buy-arrow { transform: rotate(180deg); display: inline-block; }
  .side-buy-body { display: none; }
  .side-buy.is-open .side-buy-body {
    display: flex; position: absolute; top: 100%; left: 0; right: 0; z-index: 40;
    min-width: 18rem; max-height: 70dvh; overflow-y: auto;
    padding: var(--space-4); gap: var(--space-3);
    background: var(--surface); border-bottom: var(--line-w) solid var(--line);
    box-shadow: var(--shadow-2);
  }
  .view { padding: var(--space-5) var(--space-4) var(--space-8); }
}

/* 区块 = 一张纸的上沿加一道重线，而不是一个盒子。
 * "cards in cards"是最常见的生成式结构，这里从根上避免。 */
.block { border-top: var(--rule-w) solid var(--fg); padding-top: var(--space-4); margin-top: var(--space-7); }
.block-title {
  margin: 0 0 var(--space-3);
  font-size: var(--text-lg);
  font-weight: 600;
  display: flex; align-items: baseline; gap: var(--space-3);
}
.block-title .block-num { font-size: var(--text-sm); color: var(--fg-2); font-weight: 400; }
.block-title-row { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-4); }

/* 段落标签：靠字重与一道线，不做全大写微标签 */
.label {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--fg);
  margin: 0 0 var(--space-2);
}
.sub { color: var(--fg-2); font-size: var(--text-sm); line-height: 1.75; margin: var(--space-2) 0 0; }
.sub-center { text-align: center; }
.empty { color: var(--fg-2); font-size: var(--text-base); margin: 0; padding: var(--space-5) 0; }

/* 空态：画成虚线的"待填区"，说清这里会出现什么、下一步做什么 */
.void, .empty-state {
  padding: var(--space-6) var(--space-4);
  border: var(--line-w) dashed var(--line-strong);
  border-radius: var(--radius);
  color: var(--fg-2);
  max-width: 40rem;
  margin: 0 auto;
  text-align: center;
}
.void > strong, .empty-text { display: block; color: var(--fg); font-weight: 600; font-size: var(--text-lg); margin-bottom: var(--space-1); }
.empty-text { font-size: var(--text-lg); }
.empty-hint { font-size: var(--text-sm); color: var(--fg-2); }

/* ========================================================== 公告条 */

.notices { border-bottom: var(--line-w) solid var(--line); }
.notice {
  display: flex; align-items: baseline; gap: var(--space-3);
  padding: var(--space-2) var(--space-5);
  background: var(--surface);
  font-size: var(--text-sm);
  border-bottom: var(--line-w) solid var(--line);
}
.notice:last-child { border-bottom: 0; }
.notice-body { flex: 1 1 auto; min-width: 0; display: flex; gap: var(--space-3); flex-wrap: wrap; }
/* 级别用文字本身的墨色表达，不用彩色圆点徽标 */
.notice-mark { flex: 0 0 auto; font-weight: 600; }
.notice-text { color: var(--fg); overflow-wrap: anywhere; }
.notice-when { color: var(--fg-2); }
.notice-close {
  flex: 0 0 auto; appearance: none; background: none; border: 0;
  color: var(--fg-2); font-size: var(--text-base); line-height: 1; cursor: pointer; padding: 0 var(--space-1);
}
.notice-close:hover { color: var(--fg); }
.notice-info { border-left: var(--rule-w) solid var(--info); }
.notice-info .notice-mark { color: var(--info); }
.notice-warn { background: var(--warn-soft); border-left: var(--rule-w) solid var(--warn); }
.notice-warn .notice-mark { color: var(--warn-ink); }
.notice-danger { background: var(--danger-soft); border-left: var(--rule-w) solid var(--danger); }
.notice-danger .notice-mark { color: var(--danger); }

/* 窄屏：公告默认只占**一行**（正文省略号 + 到期时间），点一下展开全文。
 *
 * 为什么现在才补：JS 一直在切 `.is-open`（`renderNotices`），走查清单也一直写着
 * "每条公告只有一行、两条合计约 80px"，但**这条样式从来没写**——实测 390px 下
 * 两条公告吃掉 199px（每条约三行，占掉小半屏），而且点正文毫无反应（切了一个没有
 * 任何样式对应的 class）。这是"接线只接了一半"的第三个例子（前两个：`.row-inline`
 * 没定义、`field-hint` 借错误样式），所以判据也钉在这里：**`is-open` 必须有人认**。 */
@media (max-width: 900px) {
  .notice-body { flex-wrap: nowrap; }
  .notice:not(.is-open) .notice-text {
    flex: 1 1 auto; min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    /* 只有窄屏才有"可以点开"这回事，所以只有这里给手型光标：
     * 宽屏上正文本来就是全文，点它什么都不会变。 */
    cursor: pointer;
  }
  .notice:not(.is-open) .notice-when { flex: 0 0 auto; }
  /* 展开：正文独占一行，到期时间落到下一行。 */
  .notice.is-open .notice-body { flex-wrap: wrap; }
  .notice.is-open .notice-text { flex: 1 1 100%; white-space: normal; cursor: pointer; }
}

/* ========================================================== 落地/登录 */

/* `.login-wrap` 用 display:contents（在 269 行声明）：两栏直接落进 .view 的网格 */
.lip { min-width: 0; }

.lip-brand { display: inline-flex; align-items: baseline; gap: var(--space-2); font-weight: 700; font-size: var(--text-lg); color: var(--fg); }
.lip-brand-sub { color: var(--fg-2); font-weight: 400; font-size: var(--text-sm); }
/* 标题：中文标题很少超过 12 字，不要靠放大来撑气势，靠数字或一道线 */
.lip-title { margin: var(--space-4) 0 var(--space-3); font-size: var(--text-4xl); font-weight: 700; line-height: 1.3; }
.lip-sub { color: var(--fg-2); font-size: var(--text-lg); line-height: 1.7; margin: 0 0 var(--space-5); max-width: var(--measure); }

/* 对照组：两栏并排，左边灰、右边墨。不用色块、不用勾叉图标。 */
.lip-vs { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-4); margin: 0 0 var(--space-5); }
.lip-vs-col { border-top: var(--line-w) solid var(--line-strong); padding-top: var(--space-2); min-width: 0; }
.lip-vs-head { font-size: var(--text-sm); font-weight: 600; color: var(--fg-2); margin-bottom: var(--space-2); }
.lip-vs-col ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-1); }
.lip-vs-col li { font-size: var(--text-sm); line-height: 1.6; color: var(--fg-2); }
.lip-vs-new { border-top-color: var(--fg); }
.lip-vs-new .lip-vs-head { color: var(--fg); }
.lip-vs-new li { color: var(--fg); }

.lip-list { list-style: none; margin: 0 0 var(--space-5); padding: 0; display: flex; flex-direction: column; gap: var(--space-3); }
.lip-list li { display: flex; gap: var(--space-3); align-items: baseline; }
.lip-ic { flex: 0 0 auto; color: var(--fg-2); font-size: var(--text-sm); min-width: 2.5rem; }
.lip-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.lip-body b { font-size: var(--text-base); font-weight: 600; color: var(--fg); }
.lip-body span { color: var(--fg-2); font-size: var(--text-sm); line-height: 1.65; }

/* 隐私与数据安全：纸面上一块带重线的区域，不是弹窗也不是徽章。
 * 语气与全站一致——把平台**真的做到**的事实摆出来（每条都能在代码里对上），
 * 不写"军用级加密"那类没法核对的形容词。 */
.lip-privacy {
  margin: 0 0 var(--space-5);
  padding: var(--space-4) 0 0;
  border-top: var(--line-w) solid var(--line-strong);
}
.lip-privacy-head {
  font-size: var(--text-sm); font-weight: 600; color: var(--fg);
  letter-spacing: 0.02em; margin-bottom: var(--space-3);
}
.lip-privacy-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-2); }
.lip-privacy-list li { font-size: var(--text-sm); line-height: 1.65; color: var(--fg-2); }
/* 一句话的承诺放在最前面，比它下面的三条细则重一档——读者只记住这一句。 */
.lip-privacy-lead { margin: 0 0 var(--space-3); font-size: var(--text-base); font-weight: 600; color: var(--fg); }
.lip-privacy-list b { color: var(--fg); font-weight: 600; }
.lip-privacy-list code {
  font-family: var(--font-mono); font-size: 0.92em;
  background: var(--surface); padding: 0 4px; border-radius: var(--radius);
}
/* 授权红线：藤黄底 + 左重线。文字是事实，不是营销。 */
.lip-warn {
  margin: 0 0 var(--space-4);
  padding: var(--space-3) var(--space-4);
  background: var(--warn-soft);
  border-left: var(--rule-w) solid var(--warn);
  font-size: var(--text-sm);
  line-height: 1.7;
  color: var(--fg);
}
.login-boot { margin: var(--space-3) 0 0; color: var(--fg-3); font-family: var(--font-mono); font-size: var(--text-sm); }

/* ========================================================== 表单 */

.stack { display: block; }
/* 标签与输入框必须按**块级**排版。`.flabel` 以前是 inline，而**行内元素的上/下外边距
 * 完全不参与布局**——于是"标签上方 space-5、标签到输入框 space-2"这两条一直是死代码：
 * 实测标签只靠行高余量贴在上一个输入框下面（3.3px），注册表单看起来挤成一片。
 * 规则**必须全局**：先前只写在 `.stack` 里，个人中心的密钥 / 兑换码表单（`.claim`）漏在
 * 外面，那里的标签仍是 inline、间距照样失效（实测 `#key-form .flabel` display=inline）。 */
.flabel, .field > label { display: block; }
input:not([type="checkbox"]):not([type="radio"]), select, textarea { display: block; }
.field { display: grid; gap: var(--space-2); margin-bottom: var(--space-5); }
/* 字段之间给足呼吸：标签上方 space-5、标签到输入框 space-2。
 * 原来标签只隔 space-3，表单读起来是一整块密不透风的字。 */
.field > label, .flabel { font-weight: 500; font-size: var(--text-base); margin: var(--space-5) 0 var(--space-2); }
/* 表单的第一个标签不留上方空隙：它上面是卡片标题，不是上一个字段。
 * 原来这条只覆盖 `#start-form`，于是登录卡与注册卡里「登录 / 注册」标题下面
 * 多出一整段 space-5——标题自己的 margin-bottom 再叠上标签的 margin-top，
 * 表单看着被整体推下去一截，也是操作者说的"登录界面间距不对"。
 * **必须用子选择器 `>`**：`:first-of-type` 在**每个父元素内部**都成立，写成后代选择器时
 * 注册表单里「邮箱」那个标签（`#reg-email-block` 里的第一个 label）也会被误清零，
 * 于是它紧贴「再输一次」输入框（实测 0px）。 */
#start-form > .flabel:first-of-type,
#login-form > .flabel:first-of-type,
#register-form > .flabel:first-of-type,
.field > label:first-child { margin-top: 0; }
input[type="text"], input[type="password"], input[type="email"], input[type="number"],
input[type="tel"], input[type="search"], select, textarea {
  width: 100%;
  min-height: 2.75rem;
  padding: var(--space-2) var(--space-3);
  background: var(--surface);
  color: var(--fg);
  border: 0;
  border-bottom: var(--line-w) solid var(--line-strong);
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
  font: inherit;
  font-size: var(--text-base);
}
textarea { min-height: 4rem; padding: var(--space-3); line-height: 1.7; }
input::placeholder { color: var(--fg-3); }
input:hover, select:hover { border-bottom-color: var(--fg-2); }
input:focus-visible, select:focus-visible, textarea:focus-visible {
  outline: 2px solid var(--focus); outline-offset: 1px; border-bottom-color: transparent;
}
input:disabled, select:disabled, textarea:disabled { opacity: .5; cursor: not-allowed; }
.req { color: var(--danger); font-weight: 600; }

/* 密码框的显示/隐藏：贴着输入框右侧的一枚文字按钮 */
.pw-wrap { position: relative; display: block; }
.pw-wrap input { padding-right: 4.5rem; }
.pw-toggle {
  position: absolute; right: var(--space-1); top: 50%; transform: translateY(-50%);
  min-height: 1.75rem; padding: 0 var(--space-2);
  background: var(--bg); border: var(--line-w) solid var(--line); border-radius: var(--radius);
  color: var(--fg-2); font: inherit; font-size: var(--text-sm); cursor: pointer;
}
.pw-toggle:hover { color: var(--fg); border-color: var(--line-strong); }

/* 一行两件：输入框长、按钮短（注册表单的「邮箱 + 获取验证码」）。
 * `minmax(0, 1fr)` 而不是 `1fr`：`1fr` 的最小尺寸是 auto，一个长邮箱地址会把这一行
 * 撑破（grid 的老毛病）。按钮与输入框同高（输入框是 2.75rem）。 */
.row-inline { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--space-2); align-items: start; }
.row-inline .btn { min-height: 2.75rem; }
@media (max-width: 640px) {
  /* 窄屏：按钮掉到下一行并占满，别把输入框挤成一条缝 */
  .row-inline { grid-template-columns: 1fr; }
  .row-inline .btn { width: 100%; }
}

/* 字段下面的一句提示（例如"10 分钟内有效"）：比错误提示弱，但比正文小。 */
.field-hint { color: var(--fg-3); font-size: var(--text-sm); margin: var(--space-2) 0 0; line-height: 1.6; }

/* 按钮：表单上的方框，里面是动词。没有渐变、没有发光、没有药丸。 */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2);
  min-height: 2.25rem; padding: 0 var(--space-4);
  border: var(--line-w) solid var(--line-strong);
  border-radius: var(--radius);
  background: var(--bg);
  color: var(--fg);
  font: inherit; font-size: var(--text-base); font-weight: 500; line-height: 1;
  cursor: pointer; white-space: nowrap;
  transition: background-color var(--dur-micro) var(--ease-out), border-color var(--dur-micro) var(--ease-out);
}
.btn:hover { background: var(--surface); border-color: var(--fg-2); }
.btn:active { background: var(--surface-2); }
.btn:disabled { opacity: .5; cursor: not-allowed; }
.btn-block { display: flex; width: 100%; }
/* 登录 / 注册的提交按钮：下面那个 `.ferr` 是**隐藏**的（`display:none` 不占位），
 * 于是按钮上沿直接贴着密码框下沿——线上实测间距 0px，看着像两个控件粘在一起。
 * 这里给它一段上边距；错误提示显示出来时两者相邻外边距会折叠成较大的那个，
 * 不会叠成双倍空隙。 */
#login-form .btn-block, #register-form .btn-block { margin-top: var(--space-4); }
.btn-sm { min-height: 1.75rem; padding: 0 var(--space-2); font-size: var(--text-sm); }
/* 主操作：**墨色实心**。浅底上朱砂红看起来像危险操作；朱砂只留给印章、
 * 链接与焦点这类小面积强调（这也是 ui-skills 的约定：primary 用 fill-inverse）。 */
.btn-primary { background: var(--fg); border-color: var(--fg); color: var(--bg); }
.btn-primary:hover { background: var(--fg-2); border-color: var(--fg-2); }
/* 破坏性动作：静息时只是红字红框；**确认那一步**才填实（data-confirm） */
.btn-danger { color: var(--danger); border-color: var(--danger); background: var(--bg); }
.btn-danger:hover { background: var(--danger-soft); }
.btn-ghost { border-color: transparent; background: transparent; color: var(--fg-2); }
.btn-ghost:hover { background: var(--surface); border-color: var(--line); color: var(--fg); }

.ferr, .ferr-center { color: var(--danger); font-size: var(--text-sm); margin: var(--space-2) 0 0; line-height: 1.6; }.fok { color: var(--ok); font-size: var(--text-sm); margin: var(--space-2) 0 0; line-height: 1.6; }
/* 登录 / 注册卡里的提示：它夹在最后一个输入框与提交按钮之间。`.ferr` 只有上边距，
 * 于是错误文字**下沿直接贴着按钮**（两个元素之间零空隙）。显示时补一段下边距。 */
#login-form .ferr:not([hidden]), #login-form .fok:not([hidden]),
#register-form .ferr:not([hidden]), #register-form .fok:not([hidden]) {
  margin: var(--space-2) 0 var(--space-3);
}
/* 「保存成功了，但这份价目表有问题」——不是错误（值已经生效），也不是成功
   （顾客会挑小的那档买）。所以既不用 --danger 也不用 --ok：藤黄，写在纸上。 */
.fwarn { color: var(--warn-ink); font-size: var(--text-sm); margin: var(--space-2) 0 0; line-height: 1.6; }
input[aria-invalid="true"] { border-bottom-color: var(--danger); }

.adv-toggle {
  display: block; width: 100%; margin-top: var(--space-3); padding: var(--space-2) 0;
  background: none; border: 0; color: var(--fg-2); font: inherit; font-size: var(--text-sm); cursor: pointer;
}
.adv-toggle:hover { color: var(--fg); text-decoration: underline; text-underline-offset: 2px; }
.adv { margin-top: var(--space-3); padding: var(--space-4); background: var(--surface); border-radius: var(--radius); }
.adv .row { display: flex; gap: var(--space-4); margin-top: var(--space-4); }
.adv .col-f { flex: 1 1 0; min-width: 0; }
.footnote { margin: var(--space-6) 0 0; color: var(--fg-2); font-size: var(--text-sm); }

/* ========================================================== 开始页 */

.page-head { border-bottom: var(--rule-w) solid var(--fg); padding-bottom: var(--space-3); margin-bottom: var(--space-5); }
.page-head .ask { font-size: var(--text-3xl); font-weight: 700; line-height: 1.3; }
.ask { font-size: var(--text-xl); font-weight: 600; margin: 0 0 var(--space-3); }
.ask-gap { margin-top: var(--space-6); }
#start-form .ferr { margin-top: var(--space-2); }
#start-status { font-size: var(--text-sm); color: var(--fg-2); margin-top: var(--space-3); }

/* 概览：四格数字，用线分栏而不是四个盒子 */
.home-stats {
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));
  border-top: var(--line-w) solid var(--line); border-bottom: var(--line-w) solid var(--line);
  margin: var(--space-5) 0;
}
.home-cell {
  display: flex; flex-direction: column; gap: var(--space-1);
  padding: var(--space-4) var(--space-4);
  border-left: var(--line-w) solid var(--line);
}
.home-cell:first-child { border-left: 0; padding-left: 0; }
.home-k { color: var(--fg-2); font-size: var(--text-sm); white-space: nowrap; }
/* 数字不折行：四格并排时 "7182 万" 会被断成两行（生产数据比预览长）。
 * 字号收一档（2xl → xl）换取一行放得下，仍然明显是"一个数字"。 */
.home-v { color: var(--fg); font-size: var(--text-lg); font-weight: 600; white-space: nowrap; }
.home-v.leaf-num { font-size: var(--text-xl); }

/* 最近记录：就是一张小账本 */
.homelist { list-style: none; margin: 0; padding: 0; }
.homerow { border-top: var(--line-w) solid var(--line); }
.homerow:first-child { border-top: 0; }
.homerow-link {
  display: flex; align-items: baseline; gap: var(--space-3);
  padding: var(--space-3) 0; color: inherit; text-decoration: none;
}
.homerow-link:hover { background: var(--surface); text-decoration: none; }
.homerow-state { flex: 0 0 auto; font-size: var(--text-sm); color: var(--fg-2); }
.homerow-state.st-done { color: var(--ok); }
.homerow-state.st-running, .homerow-state.st-starting { color: var(--fg); font-weight: 600; }
.homerow-state.st-failed, .homerow-state.st-aborted { color: var(--danger); }
.homerow-host { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.homerow-meta { flex: 0 0 auto; font-size: var(--text-sm); color: var(--fg-2); }
.home-link { color: var(--fg-2); font-size: var(--text-sm); }
.home-link:hover { color: var(--fg); }

/* ========================================================== 任务页 */

/* 任务页双栏：主栏时间线吃满余宽，侧栏放遥测与动作。
 * 宽度 ≥1100px 时页面自己占满一屏（见上面 `.view-split`），所以这里的活儿是：
 * 决定**哪一块面板**吃掉剩下的高度、自己滚。 */
.task-side .telemetry { margin-top: var(--space-4); }
.task-side .telemetry .kv { margin-top: var(--space-2); }
.task-side .actions { margin-top: var(--space-4); }
/* 任务不存在（编号打错 / 已被 purge）：侧栏只留任务编号那一行。
   其余面板填的都是占位值（正在准备 / 已花 — / 没有产出文件），既和「这个任务不存在」
   打架，又让这一页看起来像"一条马上要开始的任务"。编号要留着——操作者刚输入它，
   得能一眼对照自己打错了哪个字符。 */
.task-side.is-missing > *:not(.idline) { display: none; }
@media (min-width: 1100px) {
  .task-main { display: flex; flex-direction: column; min-height: 0; }
  .task-main #process-block {
    flex: 1 1 auto; min-height: 0;
    display: flex; flex-direction: column;
  }
  /* 收起过程时不占那一片空白：让它缩回自己该有的高度。 */
  .task-main #process-block.is-collapsed { flex: 0 0 auto; }
  .task-main .activity-scroll { flex: 1 1 auto; min-height: 0; max-height: none; }
  .task-side { position: static; overflow-y: auto; }
}

.back { color: var(--fg-2); font-size: var(--text-sm); display: inline-block; margin-bottom: var(--space-3); }
.back:hover { color: var(--fg); text-decoration: none; }
.task-host {
  font-size: var(--text-3xl); font-weight: 700; line-height: 1.3;
  font-family: var(--font-body);
  overflow-wrap: anywhere; color: var(--fg);
}
.live-row { display: flex; align-items: center; gap: var(--space-3); margin-top: var(--space-2); }
.state-text { font-size: var(--text-lg); }
.timer { margin-left: auto; color: var(--fg-2); font-size: var(--text-base); }

/* 状态标记：一个方块 + 一个词。**不脉冲**——闪烁的状态点是紧迫感工具箱里的东西。 */
.dot {
  width: 0.6rem; height: 0.6rem; border-radius: var(--radius-sm);
  background: var(--fg-3); flex: 0 0 auto;
}
.dot.run { background: var(--fg); }
.dot.done { background: var(--ok); }
.dot.bad { background: var(--danger); }
.dot.stop { background: var(--fg-3); }

.stat-line { margin: var(--space-3) 0 0; color: var(--fg-2); font-size: var(--text-sm); }
.stat-line .mono, .stat-line.mono { text-transform: none; }

/* 实时脉冲：一行陈述句，说明"现在在做什么、多久了" */
.pulse-line { margin: var(--space-2) 0 0; font-size: var(--text-sm); color: var(--fg-2); display: flex; align-items: center; gap: var(--space-2); }
.pulse-line.is-busy::before {
  content: ""; flex: 0 0 auto; width: 0.5rem; height: 0.5rem;
  background: var(--fg-2); border-radius: var(--radius-sm);
  animation: step-blink 1.2s var(--ease-step) infinite;
}
@keyframes step-blink { 50% { opacity: .25; } }
.pulse-note { margin: var(--space-2) 0 0; font-size: var(--text-sm); color: var(--fg-3); }

/* 行动区：一行动词按钮 */
.actions { display: flex; gap: var(--space-3); margin-top: var(--space-5); flex-wrap: wrap; }
.actions .btn { flex: 1 1 0; min-width: 0; }
.actions:not(:has(.btn:not([hidden]))) { display: none; }

.idline { display: flex; align-items: baseline; gap: var(--space-3); flex-wrap: wrap; margin-top: var(--space-4); }
.idlabel { font-size: var(--text-sm); color: var(--fg-2); }
.idval { color: var(--fg); font-size: var(--text-sm); overflow-wrap: anywhere; }

/* 时间线：一张"边栏账本"——左边时刻，右边内容，靠线分隔 */
.activity-scroll {
  max-height: min(58vh, 34rem);
  overflow-y: auto; overflow-x: hidden;
  border-top: var(--line-w) solid var(--line);
  border-bottom: var(--line-w) solid var(--line);
  padding: var(--space-2) 0;
}
.activity { list-style: none; margin: 0; padding: 0; }

/* 没有正文的折叠行（两道审核阶段的指令不下发正文）：一行静态说明，不可点。
 * 用户的话："很多东西展开的话都是显示空，这种还不如展开不了算了。" */
.act-prompt-plain { color: var(--fg-2); font-size: var(--text-sm); padding: var(--space-1) 0; }

/* 空轮摊平之后不显示那个"可展开"的小三角——它已经没有展开这回事了。 */
.turn-flat .turn-caret { visibility: hidden; }

/* 一轮 = 一个折叠区。用 +/- 的等宽符号，不用旋转的三角。 */
.turn { display: block; border-top: var(--line-w) solid var(--line); padding: var(--space-2) 0; }
.turn:first-child { border-top: 0; }
.turn-head {
  display: flex; align-items: baseline; gap: var(--space-3);
  cursor: pointer; list-style: none; padding: var(--space-1) 0;
}
.turn-head::-webkit-details-marker { display: none; }
.turn-head:hover .turn-tag { text-decoration: underline; text-underline-offset: 2px; }
.turn-caret { flex: 0 0 auto; color: var(--fg-3); font-family: var(--font-mono); font-size: var(--text-sm); width: 1ch; }
.turn-tag { flex: 0 0 auto; font-size: var(--text-sm); font-weight: 600; color: var(--fg); }
.turn-ts { flex: 0 0 auto; font-size: var(--text-sm); color: var(--fg-3); }
.turn-preview {
  flex: 1 1 auto; min-width: 0; font-size: var(--text-sm); color: var(--fg-2); line-height: 1.6;
  overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  /* 折叠条上写的是一段 AI 输出的纯文本，里面真的会出现很长的无空格 token
   * （base64、JWT、哈希、URL）。没有 anywhere 时这些 token 会把 scrollWidth
   * 撑到数千像素——显示上被 line-clamp 裁掉了，但溢出是真实存在的（实测
   * 5423px vs 205px），换个渲染器或换个字号就会直接顶出卡片。 */
  overflow-wrap: anywhere; word-break: break-word;
}
.turn[open] > .turn-head .turn-preview { display: none; }

/* 开场轮（沙箱就绪、引擎启动、输入审核）：它不是一次作业，所以头比别的轮轻一点，
 * 编号那一栏写的是「开场」而不是数字。整块仍然可以点开看细节，只是不抢视线。 */
.turn-preamble > .turn-head .turn-tag { font-weight: 500; color: var(--fg-2); }
.turn-preamble > .turn-head .turn-preview { color: var(--fg-3); }

.turn-sum {
  margin: var(--space-2) 0 var(--space-3);
  padding: var(--space-3) var(--space-4);
  background: var(--surface);
  border-left: var(--rule-w) solid var(--fg);
}
.turn-sum-tag { display: block; font-size: var(--text-sm); font-weight: 600; margin-bottom: var(--space-1); }
.turn-sum-text { color: var(--fg); }
.turn-sum-text .md { font-size: var(--text-base); }
.turn-sum-text .md > *:first-child { margin-top: 0; }
.turn-sum-text .md > *:last-child { margin-bottom: 0; }
.turn-body { list-style: none; margin: 0; padding: 0 0 0 var(--space-4); }

.act { padding: var(--space-1) 0; font-size: var(--text-sm); line-height: 1.7; display: flex; gap: var(--space-3); align-items: baseline; }
.ts { flex: 0 0 auto; color: var(--fg-3); font-size: var(--text-xs); min-width: 4.5rem; }
.act-say .md { font-size: var(--text-base); }
.act-tool { color: var(--fg-2); }
/* 工具行最多两行：实测一条带满参数的调用是 224 字符 / 7 行，而线上那条任务有
 * 177 次调用。前两行已经能认出"它做了什么"，完整命令在 title 里（原文在
 * logs/runtime.jsonl，界面按约定不展示工具输出）。 */
.tool-text { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere;
             display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
             line-clamp: 2; overflow: hidden; }
.tool-mark { flex: 0 0 auto; color: var(--fg-3); }
.tool-mark.ok { color: var(--ok); }
.tool-mark.bad { color: var(--danger); }
.tick { color: var(--ok); flex: 0 0 auto; }
.act-done { color: var(--fg); }
.you-mark { flex: 0 0 auto; font-weight: 600; color: var(--info); }
.you-text { flex: 1 1 auto; min-width: 0; }
.act-note .note-mark { flex: 0 0 auto; font-weight: 600; color: var(--info); }
.note-text { flex: 1 1 auto; min-width: 0; }
.act-note.lv-warn .note-mark, .act-note.lv-warn .note-text { color: var(--warn-ink); }
.act-note.lv-error .note-mark, .act-note.lv-error .note-text { color: var(--danger); }
.goal-mark { flex: 0 0 auto; color: var(--fg-2); }
.goal-text { flex: 0 0 auto; font-weight: 600; }
.goal-obj { flex: 1 1 auto; min-width: 0; color: var(--fg-2); }
.act-phase { border-top: var(--line-w) solid var(--line); margin-top: var(--space-2); padding-top: var(--space-2); }
.phase-mark { flex: 0 0 auto; font-weight: 600; color: var(--fg); }
.phase-text { flex: 1 1 auto; font-weight: 600; }
/* 阶段成本：刚结束那一段花了多少 token / 积分 / 多长时间。
   数字用等宽字体、颜色退一档——它是读数，不是结论。 */
.phase-cost {
  flex: 0 1 auto;
  min-width: 0;
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  color: var(--fg-2);
}

/* 段：AI 的一段话，可折叠 */
.seg { border-top: var(--line-w) solid var(--line); }
.seg-head { cursor: pointer; padding: var(--space-2) 0; list-style: none; display: flex; gap: var(--space-2); align-items: baseline; font-size: var(--text-sm); color: var(--fg-2); }
.seg-head::-webkit-details-marker { display: none; }
.seg-head:hover { color: var(--fg); }
.seg-body { list-style: none; margin: 0; padding: 0 0 var(--space-2); }

/* 工具调用组与其它折叠区 */
.fold { border-top: var(--line-w) solid var(--line); }
.fold > summary { cursor: pointer; padding: var(--space-2) 0; list-style: none; display: flex; gap: var(--space-2); align-items: baseline; font-size: var(--text-sm); color: var(--fg-2); }
.fold > summary::-webkit-details-marker { display: none; }
.fold > summary:hover { color: var(--fg); }
.fold-body { padding: 0 0 var(--space-2) var(--space-4); }
/* 折叠组里的工具调用清单（`toolGroup()` 造的 `<ul class="tools-body">`）。
 * 它以前没有这条：浏览器的默认 `<ul>` 会带**项目符号 + 约 40px 左缩进**，于是展开
 * 一个工具组看到的是时间线里冒出一串圆点和一段莫名的缩进。控制台里每一张列表
 * （`.activity`/`.turn-body`/`.seg-body`/`.artlist`/`.result-list`…）都做了这个重置，
 * 只有它漏了。缩进交给子行自己的 `.act`，这里不再额外缩一层。 */
.tools-body { list-style: none; margin: 0; padding: 0; }
/* 折叠条右侧的元信息（字数、条数…）：推到行尾、弱一级，不跟标题抢注意力。 */
.fold-meta { margin-left: auto; flex: 0 0 auto; color: var(--fg-3); font-size: var(--text-xs); }
.tools > summary { font-size: var(--text-sm); }
.toolrow { padding: var(--space-1) 0; }
.toolrow .fold > summary { font-size: var(--text-sm); }
.fold-caret { color: var(--fg-3); }
.review-prompt .fold-body { background: var(--surface); padding: var(--space-3) var(--space-4); }

/* ========================================================== 报告与产出 */

.report { margin-top: var(--space-4); }
.report-head { display: flex; align-items: baseline; gap: var(--space-3); padding-bottom: var(--space-2); border-bottom: var(--rule-w) solid var(--fg); }
.report-title { font-size: var(--text-lg); font-weight: 600; }
.report-body { padding-top: var(--space-3); max-width: var(--measure); }

/* 产出文件列表（布局在下面的 `.artlist > li`，这里只管标签与字号的语义色）。
 * 原先这里还留着 `.artifact-list` / `.art-row` / `.art-name` 三条旧类名的规则：
 * 它们对应的类早就不在 DOM 里了（markup 用 `.artlist`、app.js 用 `.art-path`），
 * 留着只会让下一个改样式的人以为改这里有效。 */
/* 下载引导与打包按钮。它出现在「产出文件」标题下方，用的是纸面上的一段小字
 * 加一行动作，不是又一个盒子——这套版式里区块靠分隔线划分，不靠卡片。 */
.art-hint { margin: var(--space-3) 0 0; font-size: var(--text-sm); color: var(--fg-2); line-height: 1.7; }
.art-hint b { color: var(--fg); font-weight: 600; }
.art-hint code { font-family: var(--mono); font-size: 0.95em; }
.art-actions-line { display: flex; flex-wrap: wrap; gap: var(--space-2); margin: var(--space-3) 0 0; }
.art-actions-line .btn { text-decoration: none; }

/* 标签列固定宽度：中文标签有 2 字也有 4 字，各占各的宽会把右边的文件名推得
 * 参差不齐——这是一张账本，列要对齐（4.6em 放下最长的「交付文件」）。 */
.art-kind { flex: 0 0 auto; min-width: 4.6em; font-size: var(--text-sm); font-weight: 600; color: var(--fg-2); }
/* 颜色只区分"要不要交"：**朱砂＝交付文件**（唯一要提交的东西），
 * 赭石＝总报告，墨色＝证据与评估结论，过程草稿压到最弱的次要前景色——
 * 它和交付文件混在一起时，操作者曾经以为那些草稿就是要交的东西。 */
.art-kind.k-deliverable { color: var(--accent); }
.art-kind.k-report { color: var(--warn-ink); }
.art-kind.k-verdict { color: var(--fg); }
.art-kind.k-evidence { color: var(--fg); }
.art-kind.k-finding { color: var(--fg-2); }
/* 定级清单：内部总账，不是要交的东西——同过程材料的灰，别和「交付文件」抢眼。 */
.art-kind.k-grading { color: var(--fg-2); }
.art-size { flex: 0 0 auto; font-size: var(--text-sm); color: var(--fg-3); }
/* 扩展名小标：`md` 与 `pdf` 是同一个漏洞的两份（一份可改、一份提交用），
 * 扫一眼就能配对。文件名本身动辄 120+ 个字符，靠结尾的扩展名分辨太费眼。 */
.art-ext { flex: 0 0 auto; font-family: var(--mono); font-size: 0.7rem; letter-spacing: 0.04em;
           text-transform: uppercase; color: var(--fg-3); border: var(--line-w) solid var(--line-strong);
           border-radius: 2px; padding: 0 0.25rem; align-self: center; }
/* 折起来的「狩猎过程草稿」：交付文件已经在上面列过了，这里是同一批漏洞的过程稿。
 * 缩进 + 弱化，视觉上当它是附录，而不是第二套要交的东西。 */
.art-drafts { border-top: var(--line-w) solid var(--line); padding-top: var(--space-2); margin-top: var(--space-2); }
.artlist-sunk { margin: var(--space-2) 0 0 var(--space-4); padding-left: 0; }

.codeblock { position: relative; margin-top: var(--space-3); }
.codeblock pre {
  margin: 0; padding: var(--space-3) var(--space-4);
  background: var(--surface); border-left: var(--rule-w) solid var(--line-strong);
  border-radius: var(--radius);
  overflow: auto; max-height: 26rem;
  font-size: var(--text-sm); line-height: 1.6;
}
.codeblock .copyex { position: absolute; top: var(--space-2); right: var(--space-2); opacity: .7; }
.codeblock .copyex:hover { opacity: 1; }

/* 技术信息：规格表（dt/dd 两栏，一条条线） */
.tech { border-top: var(--rule-w) solid var(--fg); margin-top: var(--space-6); padding-top: var(--space-3); }
.tech > summary { cursor: pointer; font-size: var(--text-lg); font-weight: 600; list-style: none; }
.tech > summary::-webkit-details-marker { display: none; }
.kv-row { display: grid; grid-template-columns: max-content 1fr; gap: 0 var(--space-5); margin: 0; }
.kv-row dt { color: var(--fg-2); padding: var(--space-1) 0; border-bottom: var(--line-w) solid var(--line); font-size: var(--text-sm); }
.kv-row dd { margin: 0; padding: var(--space-1) 0; border-bottom: var(--line-w) solid var(--line); font-size: var(--text-sm); overflow-wrap: anywhere; }

/* ========================================================== 账本表格 */

.table-wrap { overflow-x: auto; margin-top: var(--space-3); }
.dtable, .ledger { width: 100%; border-collapse: collapse; font-size: var(--text-sm); }
.dtable th, .ledger th {
  position: sticky; top: 0; z-index: 1;
  background: var(--bg);
  border-bottom: var(--rule-w) solid var(--fg);
  font-weight: 600; text-align: start; padding: var(--space-2) var(--space-3);
}
.dtable td, .ledger td { padding: var(--space-2) var(--space-3); border-bottom: var(--line-w) solid var(--line); vertical-align: baseline; }
.dtable tbody tr:hover td, .ledger tbody tr:hover td { background: var(--surface); }
.dtable td[data-num], .dtable th[data-num] { text-align: end; font-family: var(--font-mono); font-size: 0.95em; }
.dim { color: var(--fg-2); }
.ok-text { color: var(--ok); }
.bad-text { color: var(--danger); }

/* ========================================================== 历史 */

.lookup { display: flex; gap: var(--space-3); align-items: flex-end; margin-bottom: var(--space-4); }
.lookup input { flex: 1 1 auto; min-width: 0; }
/* 历史列表的旧样式（.history/.hrow/.hmain/.host/.hmeta）已随窄栏列表一起删除：
   布局重建后历史改成了满宽账本表格（#history-table > #history-rows），这些选择器
   在 HTML 与 JS 里都不再有任何引用——留着只会让人以为还有一个列表视图。 */

#history-count { font-size: var(--text-sm); color: var(--fg-2); margin: 0 0 var(--space-4); }
#history-more { margin: var(--space-4) 0; }

/* 筛选：一排文字按钮，当前项加粗并带一道线（不用药丸、不用计数气泡） */
/* 筛选行：状态按钮靠左，查编号靠右，输入框给一个合理的宽度（不要拉满一整行） */
.filters { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-4); margin: 0; border-bottom: var(--line-w) solid var(--line); }
.filters .lookup { margin-left: auto; display: flex; gap: var(--space-2); align-items: baseline; }
.filters .lookup input { width: 12rem; min-height: 2rem; }

/* 表格里的行链接：墨色 + 悬停下划线。朱砂是强调色，不该铺满整列数据。 */
.row-link { color: var(--fg); text-decoration: none; }
.row-link:hover { color: var(--fg); text-decoration: underline; text-underline-offset: 2px; }
tbody tr:hover td { background: var(--surface); }
.chip {
  appearance: none; cursor: pointer; background: none; border: 0;
  padding: var(--space-2) 0; margin-bottom: -1px;
  border-bottom: var(--rule-w) solid transparent;
  color: var(--fg-2); font: inherit; font-size: var(--text-base);
}
.chip:hover { color: var(--fg); }
.chip.is-on { color: var(--fg); font-weight: 600; border-bottom-color: var(--fg); }
.chip-n { color: var(--fg-3); font-size: var(--text-sm); }
.chip.is-on .chip-n { color: var(--fg-2); }

/* 状态词：用文字，配一枚矩形印。color 与词一起出现，颜色本身不是消息。 */
.badge {
  flex: 0 0 auto; font-size: var(--text-sm); font-weight: 600;
  padding: 0 var(--space-2); border: var(--line-w) solid currentColor; border-radius: var(--radius);
  color: var(--fg-2); white-space: nowrap;
}
.badge.run { color: var(--fg); }
.badge.done { color: var(--ok); }
.badge.bad, .badge.wait { color: var(--fg-2); }
.badge.bad { color: var(--danger); }

/* 个人中心（单栏）：密钥管理与兑换码充值回到主栏。右边那栏只有它们自己，
 * 右边空着一整列——操作者原话"个人信息界面右边是没东西的，不需要有栏"。
 *
 * 表单的输入框在这里是**竖排**（输入在上、按钮在下）：全局 `.claim-row input
 * { flex: 1 1 16rem }` 是按横排写的（16rem 是宽度基准），竖排时同一个
 * flex-basis 会变成**高度** 256px——第一版正是这样把"输入密钥"框变成了一整块
 * 大板子。所以这里退出伸缩、写死高度。 */
.acct-main .claim { margin-top: var(--space-3); max-width: 28rem; }
.acct-main .claim-row { flex-direction: column; align-items: stretch; gap: var(--space-2); }
.acct-main .claim-row input { flex: 0 0 auto; width: 100%; min-height: 2.5rem; height: 2.5rem; }
/* 单栏之后两个动作区块（生成密钥 / 兑换码充值）前后相接，中间要留出**区块级**
 * 的呼吸感：block-title 自带上边距，但被 .claim 的 margin 压扁了——给标题
 * 补回区块间距，读起来才是"两个独立的动作"，而不是"一张表单接着一张"。 */
.acct-main .block-title { margin-top: var(--space-7); }
.acct-main .block-title:first-child { margin-top: 0; }

/* ---- 我的密钥：一行一把，用分隔线而不是小盒子 ----
 * 侧栏只有 ~24rem 宽，一把 Key 要在一行里说清"叫什么、哪一把、是不是默认、
 * 还能做什么"。所以上面一行是名字 + 印，中间一行等宽元信息，下面一行动作。 */
.keylist { margin: var(--space-3) 0 0; }
.keyrow { padding: var(--space-3) 0; border-top: var(--line-w) solid var(--line); }
.keyrow:first-child { border-top: 0; padding-top: var(--space-2); }
.keyrow-head { display: flex; align-items: baseline; gap: var(--space-2); min-width: 0; }
.keyrow-name {
  flex: 1 1 auto; min-width: 0; font-weight: 600; color: var(--fg);
  overflow-wrap: anywhere;
}
.keyrow-meta {
  margin-top: 2px; color: var(--fg-3); font-size: var(--text-xs);
  line-height: 1.6; overflow-wrap: anywhere;
}
.keyrow-acts { display: flex; flex-wrap: wrap; gap: var(--space-4); margin-top: var(--space-2); }
.keyrow-acts .linkbtn { font-size: var(--text-sm); }
.linkbtn-danger { color: var(--danger); }
.linkbtn-danger:hover { color: var(--danger); }

/* 新密钥的明文：只出现这一次，所以要看得见、好复制、能收起 */
.keysecret {
  margin: var(--space-3) 0 0; padding: var(--space-3);
  background: var(--surface-2); border-left: var(--rule-w) solid var(--ok);
}
.keysecret-title { margin: 0 0 var(--space-2); font-size: var(--text-sm); font-weight: 600; color: var(--fg); }
.keysecret-row { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.keysecret-row code {
  flex: 1 1 100%; min-width: 0; font-size: var(--text-xs); line-height: 1.6;
  overflow-wrap: anywhere; color: var(--fg);
}
#key-secret-done { margin-top: var(--space-2); font-size: var(--text-sm); }

/* ========================================================== 个人中心 */

.quota { border-top: var(--rule-w) solid var(--fg); padding-top: var(--space-3); margin-top: var(--space-5); }
/* 额度条原本是 8px 的浅色细条，在纸面上几乎看不见（实测像一条杂线）。
 * 加高、加一条可见的轨道与描边，让它读起来是"进度"而不是"分隔线"。 */
.quota-bar {
  height: 0.75rem;
  background: var(--surface-2);
  border: var(--line-w) solid var(--line-strong);
  border-radius: var(--radius-sm);
  overflow: hidden;
  margin: var(--space-3) 0 var(--space-2);
}
.quota-fill {
  display: block;
  height: 100%;
  background: var(--ok);
  transform: scaleX(var(--quota-ratio, 0));
  transform-origin: left center;
  transition: transform var(--dur-base) var(--ease-out);
}
.quota-fill.warn { background: var(--warn); }
.quota-fill.over { background: var(--danger); }
.quota-fill.unlimited { background: var(--fg-3); transform: scaleX(1); }
.quota-line { font-size: var(--text-sm); color: var(--fg-2); }
.balance-line { margin-top: var(--space-4); font-size: var(--text-sm); color: var(--fg-2); }

.stats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); border-top: var(--line-w) solid var(--line); border-bottom: var(--line-w) solid var(--line); margin: var(--space-4) 0; }
.stat-cell { padding: var(--space-3) var(--space-4); border-left: var(--line-w) solid var(--line); }
.stat-cell:first-child { border-left: 0; padding-left: 0; }
.stat-label { display: block; color: var(--fg-2); font-size: var(--text-sm); }
.stat-value { display: block; margin-top: 2px; font-size: var(--text-xl); font-weight: 600; }

/* 柱状图：一根柱子一个量。同一种墨，深色表示最大值。 */
.chart { position: relative; display: flex; align-items: flex-end; gap: var(--space-1); height: 9rem; margin-top: var(--space-4); padding: var(--space-3) 0 var(--space-1); border-top: var(--line-w) solid var(--line); border-bottom: var(--line-w) solid var(--line); --caps-h: 1.25rem; }
/* 基线：柱子的地平线，画在"标签行上缘 + 列间距"的那个高度。
 * 没有它，花费很小的一天就是一根 4px 的短柱悬在半空——用户原话：
 * "这个日用统计图好奇怪"。一条线就能把短柱变成"很小但站在地上"。 */
.chart::after { content: ''; position: absolute; left: 0; right: 0; bottom: calc(var(--space-1) + var(--caps-h) + var(--space-1)); height: var(--line-w); background: var(--line); pointer-events: none; }
.bar-col { flex: 1 1 0; display: flex; flex-direction: column; justify-content: flex-end; align-items: center; gap: var(--space-1); height: 100%; min-width: 0; }
.bar { width: 100%; max-width: 1.75rem; background: var(--fg-2); }
/* 零花销的日子就是空列：以前给一条 2px 的"底座"想区分"没跑"和"跑了没花钱"，
 * 实际上用户只看到一排莫名其妙的灰点。hover 的明细照常可查。 */
.bar-col:hover .bar { background: var(--accent); }
.bar-cap {
  /* 横轴日期横排，宽度不够时由渲染器按容器实际宽度**抽稀**（每列 ~46px 的
   * 预算决定隔几天标一个），不再竖排——竖排是给"每列只有十几像素"准备的，
   * 桌面上七根柱子配一列竖字反而像排错了版。 */
  font-size: var(--text-xs); color: var(--fg-3);
  line-height: 1.1; height: var(--caps-h);
  white-space: nowrap; overflow: visible;
}
/* 首页迷你图：矮一些，只做趋势条 */
.chart-mini { height: 5.5rem; padding-top: var(--space-2); --caps-h: 0rem; }
.chart-mini .bar-cap { display: none; }

/* 时间范围：一排文字按钮 */
.range { display: inline-flex; border: var(--line-w) solid var(--line-strong); border-radius: var(--radius); overflow: hidden; }
.range-btn { appearance: none; border: 0; background: var(--bg); color: var(--fg-2); font: inherit; font-size: var(--text-sm); padding: var(--space-1) var(--space-3); cursor: pointer; }
.range-btn + .range-btn { border-left: var(--line-w) solid var(--line); }
.range-btn:hover { background: var(--surface); color: var(--fg); }
.range-btn.is-on { background: var(--fg); color: var(--bg); }

/* （旧密钥面板的 .keyform/.keynew/.userrow 等规则已随那块 UI 一起删除。
 * 它们曾与新密钥面板的 .keyrow/.keyrow-meta 同名冲突：旧的 `display:flex;
 * flex-wrap:wrap` + `flex:0 0 auto` 让新密钥行的元信息不可收缩，390px 下把
 * 页面撑出 188px 的横向溢出——这类"同名死规则"删 UI 时必须一起删。） */

/* 平台健康：一行一项，符号在墨色里 */
/* （平台健康面板的规则已随面板撤下；.health-* 不再是页面选择器。） */

/* ========================================================== API 面板 */

.api-note { background: var(--surface); border-left: var(--rule-w) solid var(--line-strong); padding: var(--space-3) var(--space-4); margin: var(--space-3) 0; font-size: var(--text-sm); line-height: 1.7; }
.api-note code, .md code { background: var(--surface-2); border-radius: var(--radius-sm); padding: 0 0.25em; color: var(--fg); }

/* ========================================================== markdown */

.md { line-height: 1.75; max-width: var(--measure); overflow-wrap: anywhere; }
.md h1, .md h2, .md h3 { margin: var(--space-5) 0 var(--space-2); }
.md h1 { font-size: var(--text-2xl); }
.md h2 { font-size: var(--text-xl); }
.md h3 { font-size: var(--text-lg); }
.md h4, .md h5, .md h6 { font-size: var(--text-base); color: var(--fg-2); }
.md p { margin: var(--space-3) 0; }
.md ul, .md ol { margin: var(--space-3) 0; padding-left: 1.5em; }
/* 列表项里的长路径（`/srv/okc/tasks/…/work/notes/eval/grading.md` 这种）没有断行
 * 机会，会把 `li` 撑到比容器还宽——实测时间线里两行 AI 输出就这么顶出了卡片。
 * 断行规则给整棵 markdown 树，别只给段落。 */
.md li, .md td, .md th, .md p { overflow-wrap: anywhere; }
.md li { margin: var(--space-1) 0; }
.md blockquote { margin: var(--space-3) 0; padding-left: var(--space-4); border-left: var(--rule-w) solid var(--line-strong); color: var(--fg-2); }
.md table { border-collapse: collapse; width: 100%; font-size: var(--text-sm); }
.md th, .md td { padding: var(--space-1) var(--space-2); border-bottom: var(--line-w) solid var(--line); text-align: start; }
.md th { border-bottom: var(--rule-w) solid var(--fg); font-weight: 600; }
.md pre { margin: var(--space-3) 0; padding: var(--space-3) var(--space-4); background: var(--surface); border-radius: var(--radius); overflow: auto; max-height: 26rem; }
.md pre code { background: none; padding: 0; white-space: pre; }
.md strong { font-weight: 600; }
.md a { text-decoration: underline; }
.md a.ext::after { content: "↗"; font-size: 0.85em; color: var(--fg-2); margin-left: 2px; }
.md hr { margin: var(--space-5) 0; }

/* ========================================================== 提示与浮层 */

.toasts { position: fixed; right: var(--space-4); bottom: calc(var(--space-4) + env(safe-area-inset-bottom, 0px)); display: flex; flex-direction: column; gap: var(--space-2); z-index: 60; max-width: min(24rem, calc(100vw - 2 * var(--space-4))); }
.toast {
  padding: var(--space-3) var(--space-4);
  background: var(--overlay);
  border: var(--line-w) solid var(--line-strong);
  border-left: var(--rule-w) solid var(--fg);
  border-radius: var(--radius);
  font-size: var(--text-sm);
  line-height: 1.65;
  overflow-wrap: anywhere;
  box-shadow: var(--shadow-1);
  animation: rise-in var(--dur-enter) var(--ease-out) both;
}
.toast.ok { border-left-color: var(--ok); }
.toast.warn { border-left-color: var(--warn); }
.toast.err { border-left-color: var(--danger); }
.toast.is-out { animation: fade-out var(--dur-base) var(--ease-out) both; }
@keyframes rise-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
@keyframes fade-out { to { opacity: 0; } }

.bootbar {
  position: fixed; left: 50%; bottom: calc(var(--space-4) + env(safe-area-inset-bottom, 0px)); transform: translateX(-50%);
  font-size: var(--text-sm); color: var(--fg-2);
  background: var(--overlay); border: var(--line-w) solid var(--line-strong); border-radius: var(--radius);
  padding: var(--space-2) var(--space-4); z-index: 20;
}

/* 确认对话框：一张纸盖在另一张纸上 */
dialog.modal {
  width: 100%; max-width: 32rem; padding: 0;
  background: var(--overlay); color: var(--fg);
  border: var(--line-w) solid var(--line-strong);
  border-top: var(--rule-w) solid var(--fg);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-2);
}
dialog.modal::backdrop { background: oklch(0.2 0.01 250 / 0.35); }
.modal-body { padding: var(--space-5) var(--space-5) var(--space-4); }
.modal-title { font-size: var(--text-lg); font-weight: 600; margin: 0 0 var(--space-2); }
/* 确认说的是**后果**，不是问句。 */
.modal-text { margin: 0; color: var(--fg-2); font-size: var(--text-base); line-height: 1.75; }
.modal-actions { display: flex; gap: var(--space-3); padding: 0 var(--space-5) var(--space-5); flex-wrap: wrap; }
.modal-actions .btn { flex: 1 1 0; min-width: 0; }
/* 破坏性动作的**确认步**才填实朱砂：静息的按钮只是红字 */
#confirm-ok { background: var(--danger); border-color: var(--danger); color: var(--accent-fg); }

/* 加载：一块静态的"待印区"，不是闪烁的骨架。
 * 骨架流光是最饱和的生成式外观之一；这里只用静止的底色块。 */
.skel, [data-loading] { display: block; background: var(--surface-2); border-radius: var(--radius-sm); }
.skel-line { height: 0.75rem; margin: var(--space-2) 0; }
.skel-line.w-40 { width: 40%; }
.skel-line.w-70 { width: 70%; }
.skel-block { height: 4rem; margin: var(--space-3) 0; }
.skel-wrap { display: block; }
.skel-row { list-style: none; padding: var(--space-3) 0; border-top: var(--line-w) solid var(--line); }

/* ============================================ 应用专有部件（补齐） */

/* 落地页登录卡的标题（JS 在登录/注册之间切换它） */
.lip-card-title { font-size: var(--text-xl); font-weight: 700; margin: 0 0 var(--space-3); }

/* 完成页：标题本身就是状态，颜色是语义墨色 */
.head-done { margin-bottom: var(--space-2); }
.done-title { font-size: var(--text-3xl); font-weight: 700; }
.done-title.is-ok { color: var(--ok); }
.done-title.is-bad { color: var(--danger); }
.done-sub { margin: var(--space-2) 0 0; color: var(--fg-2); font-size: var(--text-base); }

/* 小节标题（技术信息里的"进度 / Token / 产出文件"） */
.tech-h { font-size: var(--text-base); font-weight: 600; margin: var(--space-5) 0 var(--space-2); border-top: var(--line-w) solid var(--line); padding-top: var(--space-2); }
.tech-body { padding-top: var(--space-2); }
.pre { margin: var(--space-2) 0 0; padding: var(--space-3) var(--space-4); background: var(--surface); border-radius: var(--radius); overflow: auto; max-height: 26rem; font-size: var(--text-sm); white-space: pre-wrap; overflow-wrap: anywhere; }
.start-status { margin-top: var(--space-3); }

/* 概览格子（个人中心用的旧类名，语义同首页四格） */
.stat-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); border-top: var(--line-w) solid var(--line); border-bottom: var(--line-w) solid var(--line); margin: var(--space-4) 0; }
.stat { padding: var(--space-4) var(--space-4); border-left: var(--line-w) solid var(--line); display: flex; flex-direction: column; gap: 2px; }
.stat:first-child { border-left: 0; padding-left: 0; }

/* 产出文件列表 */
.artlist { list-style: none; margin: var(--space-3) 0 0; padding: 0; }
.artlist > li { display: flex; align-items: baseline; gap: var(--space-3); flex-wrap: wrap; padding: var(--space-2) 0; border-top: var(--line-w) solid var(--line); }
/* 文件名最多显示两行：agent 常拿整条漏洞标题当文件名（实测 126 个字符 → 8 行），
 * 一屏只看得到两三个文件。前两行已经足够认出是哪一条，完整路径在 title 里，
 * 下载下来的名字也是完整的——省下的是版面，不是信息。 */
.art-path { flex: 1 1 12rem; min-width: 0; overflow-wrap: anywhere; font-size: var(--text-sm);
            display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
            line-clamp: 2; overflow: hidden; }
.art-actions { flex: 0 0 auto; display: flex; gap: var(--space-2); align-items: baseline; }
.art-act { appearance: none; background: none; border: 0; padding: 0; color: var(--accent); font: inherit; font-size: var(--text-sm); cursor: pointer; text-decoration: underline; text-underline-offset: 2px; }
.art-act:hover { color: var(--accent-hover); }
.art-more { color: var(--fg-2); font-size: var(--text-sm); padding: var(--space-2) 0; border-top: var(--line-w) solid var(--line); }

/* 产出文件的分组标题（第 124 轮）：可提交 / 过程材料。它是一行 <li>，但**不是**
 * 一个文件行——所以要把 `.artlist > li` 的 flex/border 收掉，否则它会像一条空文件。
 * 这一行要回答两个问题：这组是什么、要不要交。 */
.artlist > li.art-group { display: block; padding: var(--space-3) 0 var(--space-1); border-top: 0; }
.art-group-title { font-weight: 600; font-size: var(--text-sm); }
.art-group-n { color: var(--fg-2); font-size: var(--text-sm); margin-left: var(--space-2); }
.art-group-hint { color: var(--fg-3); font-size: var(--text-xs); margin-left: var(--space-2); }

/* 平台状态 */
.health-summary { font-size: var(--text-sm); font-weight: 400; margin-left: var(--space-2); }
.health-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr)); gap: 0; border-top: var(--line-w) solid var(--line); border-bottom: var(--line-w) solid var(--line); margin: var(--space-3) 0; }
.health-cell { padding: var(--space-3) var(--space-4); border-left: var(--line-w) solid var(--line); display: flex; flex-direction: column; gap: 2px; }
.health-cell:first-child { border-left: 0; padding-left: 0; }
.health-k { color: var(--fg-2); font-size: var(--text-sm); }
.health-v { font-size: var(--text-lg); font-weight: 600; }
.health-hint { flex: 1 1 100%; min-width: 0; overflow-wrap: anywhere; color: var(--fg-3); font-size: var(--text-xs); line-height: 1.5; }
.health-fails, .health-list { list-style: none; margin: var(--space-3) 0 0; padding: 0; }
.health-list > li { display: flex; gap: var(--space-3); align-items: baseline; padding: var(--space-1) 0; font-size: var(--text-sm); }
.health-group { color: var(--fg-3); font-size: var(--text-xs); }

/* 领取密钥 */
.claim { margin-top: var(--space-3); }
.claim-row { display: flex; gap: var(--space-3); align-items: flex-end; flex-wrap: wrap; }
.claim-row input { flex: 1 1 16rem; min-width: 0; }
/* 领到之后那排「下一步」（去提交任务 / 查看密钥 / 看记录）：与上面的输入框同一套
 * 间距，按钮自己换行——侧栏那份只有一个按钮，主栏那份有三个。 */
.claim-next { display: flex; gap: var(--space-2); flex-wrap: wrap; margin-top: var(--space-2); }

/* 侧栏里的兑换码表单（「购买积分」分区内那份）：全局 `.claim-row input
 * { flex: 1 1 16rem }` 的 16rem 是按主栏横排写的宽度基准，15rem 的侧栏里
 * 它必然换行——兑换按钮被挤到第二行去，右下还吊着半行空白（和
 * `.acct-main .claim-row` 是同一个坑，那里是 basis 变高度，这里是真的放不下）。
 * 竖排：输入满宽在上、按钮满宽在下，窄栏里按钮不会变成孤行。 */
.side-buy .claim-row { flex-direction: column; align-items: stretch; gap: var(--space-2); }
.side-buy .claim-row input { flex: 0 0 auto; width: 100%; min-height: 2.5rem; }
.side-buy .claim-row .btn { width: 100%; text-align: center; }
/* 输入框 + 满宽按钮已经把"这里能兑换"说清楚了，标签换成轻的问句式说明，
 * 免得 15rem 里连着两行同重量的文字。 */
.side-buy .flabel { font-size: var(--text-sm); font-weight: 500; margin: 0 0 var(--space-1); }

/* API 面板的状态行 */
.api-note .api-on { color: var(--ok); font-weight: 600; }
.api-note .api-off { color: var(--warn-ink); font-weight: 600; }

/* 时间线里的补充件 */
.note-where { color: var(--fg-3); font-size: var(--text-xs); }
.seg-text { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; word-break: break-word; }
.tools-label, .tools-count { color: var(--fg-2); }
.tools-mark { color: var(--fg-3); }
/* 折叠组里的失败计数要变红：`markGroupBad()` 加的是 `tools-mark bad`。
 * 这里以前只有 `.tool-mark.bad`（工具**行**那个基名），少一个 s——于是"· 2 个失败"
 * 永远是灰的，而它本该是最扎眼的一条（失败才是要看的信号，组还会自动展开）。
 * 这条是 `tools/check-classes.py` 抓出来的：它比对"JS 加的组合"与"CSS 里写死的组合"，
 * 发现两者只差一个字符。 */
.tools-mark.bad { color: var(--danger); }
.chart-loading { color: var(--fg-3); font-size: var(--text-sm); align-self: center; }

/* 折叠符号统一由 CSS 画（JS 不再塞 ▸/▾ 字符） */
.fold > summary::before, .seg-head::before, .turn-head::before, .tech > summary::before {
  content: "+";
  font-family: var(--font-mono);
  color: var(--fg-3);
  width: 1ch;
  flex: 0 0 auto;
}
details[open] > summary.fold-head::before,
details[open] > summary.seg-head::before,
details[open] > summary.turn-head::before,
details.tech[open] > summary::before { content: "−"; }
.turn-caret, .seg-caret, .fold-caret, .tools-caret { display: none; }

/* ========================================================== 窄屏 */

@media (max-width: 640px) {
  .topbar { padding: var(--space-2) var(--space-4); gap: var(--space-4); flex-wrap: wrap; row-gap: var(--space-1); }
  .topnav { margin-left: 0; gap: var(--space-4); flex-wrap: wrap; width: 100%; }
  .lip-title { font-size: var(--text-2xl); }
  .login-wrap { flex-direction: column; gap: var(--space-6); }
  .actions { flex-direction: column; }
  .adv .row { flex-direction: column; gap: var(--space-4); }
  .kv-row { grid-template-columns: 1fr; gap: 0; }
  .kv-row dt { border-bottom: 0; padding-bottom: 0; color: var(--fg-2); }
  /* 四格在窄屏排 2×2：四列并排会把"100,000,000"这种数字撑破纸张（360px 实测溢出 4px）。
   * `.stat-grid` 是个人中心实际用的类名，别漏——只改 .stats 的那版在 360 下会漏。 */
  .home-stats, .stats, .stat-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .home-cell:nth-child(odd), .stat-cell:nth-child(odd) { border-left: 0; padding-left: 0; }
  .home-cell:nth-child(n+3), .stat-cell:nth-child(n+3) { border-top: var(--line-w) solid var(--line); }
  .lip-vs { grid-template-columns: 1fr; }
  .notice { padding: var(--space-2) var(--space-4); }
  .ts { display: none; }
  .turn-body { padding-left: var(--space-2); }
  /* 触屏：输入框 16px 起，避免 iOS 自动放大（类型列表与上方基础样式保持一致） */
  input[type="text"], input[type="password"], input[type="email"], input[type="number"],
  input[type="tel"], input[type="search"], select, textarea { font-size: max(1rem, 16px); }
  .btn { min-height: 2.75rem; }
  .btn-sm { min-height: 2rem; }
}

/* 窄屏的表格：改成「标签：值」的卡片，而不是让人左右拖。
 *
 * 实测（390px）：历史表宽 661px，**320px 在屏幕外**，六个列头只看得到三个；
 * 个人中心的流水表七列里有三列在屏幕外。表格靠 `.table-wrap` 横向滚动不算溢出，
 * 但"要交多少钱、什么时候跑的"这些恰好是被藏起来的那几列——账本读不起来就白做了。
 * 每行的 `td` 带 `data-label`（JS 里设），这里把它画成行首的小标签。 */
@media (max-width: 560px) {
  .table-wrap { overflow-x: visible; }
  .table-wrap table, .table-wrap tbody, .table-wrap tr, .table-wrap td { display: block; width: auto; }
  .table-wrap thead { display: none; }
  .table-wrap tr { padding: var(--space-3) 0; border-top: var(--line-w) solid var(--line); }
  .table-wrap tr:first-child { border-top: 0; }
  .table-wrap td { display: flex; gap: var(--space-3); align-items: baseline; padding: 0.15rem 0; border: 0; }
  .table-wrap td::before {
    content: attr(data-label);
    flex: 0 0 5.5em;
    color: var(--fg-3);
    font-size: var(--text-sm);
  }
  /* 没有标签的单元格（少数表格）保持原样，别把它压成一行空白。 */
  .table-wrap td:not([data-label])::before { content: none; }
}

/* 系统偏好：减弱动效时，一切动效关闭 */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation: none !important; transition: none !important; }
}

/* 后台标签页：循环动画暂停（不可见的动画只有耗电） */
body.is-hidden .pulse-line.is-busy::before,
body.is-hidden .caret { animation-play-state: paused; }

/* 完成态顶部的第一动作：测试结束后要看的就是报告，所以它跟着标题一起出现在
 * 页面最上面，而不是缩在侧栏底部要人先滚一遍。窄屏上占满整行。 */
.done-actions { margin: var(--space-4) 0 0; display: flex; flex-wrap: wrap; gap: var(--space-3); align-items: center; }
.done-actions .btn { min-width: 10rem; }
@media (max-width: 560px) {
  .done-actions .btn { width: 100%; min-width: 0; }
}

/* 过程栏收起：只留标题行与按钮。完成态默认收起——这一刻要看的是结论与文件，
 * 而不是几万行过程；手机上尤其明显（一份长报告加全过程能拉到几万像素）。 */
.block.is-collapsed .activity-scroll { display: none; }
.block.is-collapsed .block-title-row { border-bottom: 0; }

/* ---------------------------------------------------------------- 结果页
 *
 * 「查看报告」跳到这里：一条任务的报告正文 + 产出文件清单，独立成页。
 * 排版意图：正文占左边宽栏（报告是要读的，一行 42–46 个字最舒服），文件清单
 * 占右边窄栏（扫一眼、点一下）。窄屏（≤980px）改成上下堆叠，清单在前——手机
 * 上先想知道"有哪些文件"，再往下读正文。
 */
.result-body { background: var(--bg); color: var(--fg); font-family: var(--font-body); }
.result-main {
  /* 比控制台宽一档：这里读的是长报告，正文一行太窄会频繁折行、太宽又难跟行。 */
  width: 100%; max-width: 92rem; margin: 0 auto;
  padding: var(--space-6) var(--space-6) var(--space-9);
}
.result-head { border-bottom: var(--line-w) solid var(--line-strong); padding-bottom: var(--space-4); }
.result-head-top { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-4); }
.result-when { font-size: var(--text-sm); color: var(--fg-3); }
.result-title { margin: var(--space-3) 0 var(--space-2); font-size: var(--text-3xl); font-weight: 700; line-height: 1.3; overflow-wrap: anywhere; }
.result-meta { margin: 0; color: var(--fg-2); font-size: var(--text-sm); line-height: 1.7; overflow-wrap: anywhere; }

.result-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-6); margin-top: var(--space-6); align-items: start; }
@media (min-width: 980px) {
  .result-grid { grid-template-columns: minmax(0, 1fr) 22rem; gap: var(--space-7); }
}

/* 正文：纸面上一块带细边的区域，和报告合订本的感觉一致 */
.result-doc {
  min-width: 0; padding: var(--space-5) var(--space-6);
  background: var(--overlay); border: var(--line-w) solid var(--line);
  border-radius: var(--radius);
}
.result-doc-head {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-4);
  padding-bottom: var(--space-3); margin-bottom: var(--space-4);
  border-bottom: var(--line-w) solid var(--line);
}
.result-md { max-width: 46rem; }

/* 文件清单 */
.result-files { min-width: 0; }
.result-h2 { margin: 0 0 var(--space-2); font-size: var(--text-lg); font-weight: 600; }
.result-count { color: var(--fg-3); font-size: var(--text-sm); font-weight: 400; }
.result-list { list-style: none; margin: var(--space-3) 0 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-1); }
/* 分组标题：小一号、全大写间距拉开，像清单的分节线 */
.result-group {
  margin-top: var(--space-3); padding-top: var(--space-2);
  border-top: var(--line-w) solid var(--line);
  font-size: var(--text-xs); font-weight: 600; color: var(--fg-3);
  letter-spacing: 0.06em;
}
.result-item { display: flex; align-items: center; gap: var(--space-2); min-width: 0; }
.result-item.is-current { border-left: var(--rule-w) solid var(--fg); padding-left: var(--space-2); }
.result-item-open {
  flex: 1 1 auto; min-width: 0; text-align: left; display: flex; flex-direction: column;
  gap: 1px; padding: var(--space-2) 0; background: none; border: 0; cursor: pointer;
  color: inherit; font: inherit;
}
.result-item-open:hover .result-item-name { text-decoration: underline; text-underline-offset: 2px; }
.result-item-name {
  font-size: var(--text-sm); color: var(--fg); overflow-wrap: anywhere;
  /* 文件名可能很长（实测有 100+ 字的"标题即文件名"）。两行封顶：够看清是哪一份，
   * 又不会让一行占掉半屏——清单的价值在"扫一眼有多少份、交哪几份"。 */
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; line-clamp: 2;
}
.result-item-meta { font-size: var(--text-xs); color: var(--fg-3); overflow-wrap: anywhere; }
.result-item-dl { flex: 0 0 auto; font-size: var(--text-xs); color: var(--fg-2); }

@media (max-width: 979px) {
  /* 手机上清单排在正文前面：先知道有哪些文件，再决定读哪一份。 */
  .result-files { order: -1; }
  .result-doc { padding: var(--space-4); }
  .result-main { padding: var(--space-4) var(--space-3) var(--space-8); }
}

/* ---------------------------------------------------------------- 平台量级
 *
 * 进门介绍里的那行"平台弹药"：只报数量（N 个技能 · N 个工具 · N 个 MCP），
 * 不报内容——技能包的名字与描述是平台自己的手法库，不对外列清单（原来的技能库
 * 页连同 /api/skills 已因此下线）。样式刻意安静：就是一行次级文字，不加边框、
 * 不加底色，免得在窄屏上多出一条横线或一块色带。
 */
.lip-caps, .home-caps {
  margin: var(--space-2) 0 0; color: var(--fg-2); font-size: var(--text-sm);
  letter-spacing: 0.02em;
}
.lip-caps { margin-top: var(--space-3); }

/* 老引擎的 sRGB 兜底（只在这一处，组件里不写色值） */
@supports not (color: oklch(0 0 0)) {
  :root {
    --bg: #f5f3ee; --surface: #ece9e3; --surface-2: #e2ded6; --overlay: #faf8f4;
    --fg: #1f2226; --fg-2: #5b5f66; --fg-3: #74777d;
    --line: #cfc9bf; --line-strong: #9a958c;
    --accent: #b23a26; --accent-hover: #9c3020; --accent-fg: #f7f5f0; --accent-soft: #f0d9d3;
    --ok: #2f6b56; --ok-soft: #dfe9e2; --warn: #9a6b1e; --warn-soft: #f4ead6;
    --danger: #9e2b21; --danger-soft: #f2ded9; --info: #2d4a70; --info-soft: #dfe4ec;
    --focus: #2d4a70;
  }
  [data-theme="dark"] {
    --bg: #22262b; --surface: #292e34; --surface-2: #31363d; --overlay: #272c31;
    --fg: #eae7e0; --fg-2: #b4b1a9; --fg-3: #94918a;
    --line: #3c4148; --line-strong: #565c65;
    --accent: #cf6a52; --accent-hover: #dd7b63; --accent-fg: #22262b; --accent-soft: #3d2c28;
    --ok: #6aa88f; --ok-soft: #26332e; --warn: #cfa25c; --warn-soft: #352d20;
    --danger: #cf6f62; --danger-soft: #35262a; --info: #7d9cc4; --info-soft: #262c35;
    --focus: #8fadd4;
  }
}
