/* ============================================================
   格物 GridFlow · 全站设计系统(唯一事实源)
   各页面 <link rel="stylesheet" href="/assets/app.css"> 引用,
   仅把「页面独有组件」留在各页内联 <style>,统一用下面的 token。

   ┌──────────────────────────────────────────────────────────┐
   │  身 份 层 —— 用户已经拍板的决定,写在这里免得反复讨论      │
   │  或者被谁"顺手"改回去。要动这些,是立项级动作,先问用户。  │
   └──────────────────────────────────────────────────────────┘

   ▍类型
   对内后台 / 工作台,compact 密度。不是落地页 —— 判断"好不好看"的标准是
   一屏能看多少行、扫得快不快,不是留白够不够气派。浅灰白底 + 单强调色 +
   语义色 + 等宽数字。

   ▍界面字体 —— 思源黑体 Noto Sans SC(2026-08-07 用户拍板的身份层变更)
   自托管,子集由 `python tools/subset_fonts.py` 生成。此前声明的是 MiSans /
   HarmonyOS Sans SC,但仓库里从来没有这两款的文件,实际一直落在微软雅黑;
   改用思源是因为它的源文件本来就在 vendor/fonts/,能真交付得出来。
   **子集含 GB2312 一级字库 3755 字兜底**,不是只按当前用字裁 —— 后台文案会随
   功能迭代增加,只按现用字裁的话,以后加一句提示就可能掉字,而且是静默掉字。
   缺字检测(改完文案务必跑,或让 CI 跑):
       python tools/subset_fonts.py --check     # 只查不写,缺字则非零退出
       pytest tests/test_font_coverage.py       # 同一判据,外加直接读 woff2 验侧车属实
   加了新文案又忘了重裁,这两条都会红。

   ▍等宽字体
   JetBrains Mono。2026-08-07 接入:本文件有 @font-face,六个后台页 <head> 里
   有 preload(带 crossorigin)。单号、坐标号、时间、KPI 数字走它。

   ▍主色
   --accent 品牌蓝 #1E5FA8(悬停态 --accent-d #16487F)。
   **取自登录页那张实景照片的天空** —— 品牌色有实物出处,不是凭空定的。
   白底对比度 6.4:1,可以直接当正文色用。2026-07-31 由深青绿 #0F6C5B 换来。
   语义色与它分离:红 #C0392B / 黄 #A16207 / 绿 #2F855A / 墨紫 #5B21B6。

   ▍字阶 —— 2026-07-29 定的「B 方案」
   十档基准 + 一个缩放旋钮 --fs-scale,三档 0.93 / 1.00 / 1.12,**默认中档**。
   **不要擅自往小调。** 两条理由都是量出来的,不是手感:
   1. 改版前最小的字只有 10.5px,中文在那个尺寸下笔画糊成一团(「查表」「AI判」
      这类来源标记、S2.N4.S3 这类坐标号)。12px 是中文界面的底线。
   2. 改版前十四个档位挤在 10.5～26px 里,光 10.5/11/11.5/12/12.5/13/13.5/14
      就八档塞在 3.5px 内 —— 肉眼分不出层次,等于白设。
   tests/test_type_scale.py 会算这笔账并挡住写死的值。

   ▍表格(2026-08-07 定)
   行高倍率 1.4(正文仍是 --lh 的 1.6)。单元格上下内边距 calc(8px * var(--fs-scale)),
   左右 12px。
   **上下刻意不走 --pad**:--pad 的 ×1.1 是"字大了留白跟一点"的加成,把它用在
   一个正要收紧的值上,得倒推出 7.27px 这种没人解释得清的基数。直接写 8px 是
   设计基准档的目标值,三档照样线性缩放。
   **表头内边距略高于数据行(中档 11px vs 8px),是有意为之**,不是漏改。

   ▍三页表格行高不一致 —— 正常,不要去"对齐"
   货台 48px / 超管 62px / 设置 88px(中档实测)。内边距、字号、行高倍率、表头高
   四项三页三档完全相同,差的是**单元格里装的控件**:货台是按钮(31px)、超管每行
   有个配额输入框(45px)、设置每行两个输入框加一个勾选框。
   **不要为了让这三个数字对齐去改输入框高度** —— 那会牵动全站所有表单控件,
   为的只是三张不同用途的表在截图里看起来一样高。

   ▍界面文案密度
   **界面上不给模块和按钮写文字注解。** 需要一整段话才讲得清的控件,本身就没
   设计好;而那些段落会层层堆积,最后文字比可操作的东西还多,真正要看的表格
   被挤出屏幕。说明各有各的去处:
     控件语义   → 好的标签 + 表头/控件上的 title= 悬停提示
     操作后果   → 点下去时的确认框
     填错怎么办 → 服务端拒绝时的报错文案(把出路一起写进去)
     设计取舍   → 代码注释和 commit message,不是界面
   ============================================================ */

/* ── 自托管字体(两款都真交付,不再有"声明了但没文件"的欠账)────────────
   **文件名里的 -v2 是缓存版本,不是随便写的。** 字体走 7 天长缓存(见 server.py
   的 _LONG_CACHE_EXT),不改名就穿不透:老用户七天内拿到的还是旧子集,新加的字
   直接缺失。**每次重裁已部署的字体,必须把 tools/subset_fonts.py 里的 `版本`
   递增,并把这里、site.css、以及各页 preload 的 URL 一起改掉。**

   思源黑体走可变字重(100–900),整条字重范围一份文件,别为 bold 再引一个。
   ────────────────────────────────────────────────────────────── */
@font-face{font-family:"思源黑体";src:url("/assets/fonts/noto-sub-v2.woff2") format("woff2");
           font-weight:100 900;font-style:normal;font-display:swap}
@font-face{font-family:"JetBrains Mono";src:url("/assets/fonts/jbmono-v2.woff2") format("woff2");
           font-weight:400;font-style:normal;font-display:swap}

:root{
  color-scheme: light;
  /* 中性阶(冷调,略偏强调色) */
  --bg:#F4F6F8; --surface:#FFFFFF; --surface2:#FAFBFC; --surface3:#F0F2F5;
  --border:#E3E7EC; --border2:#EDEFF3;
  --text:#1B2430; --muted:#5C6675; --faint:#94A0B0;
  /* 模态遮罩。取 --text 的墨色压到 38% —— 不是另挑一支灰,是同一支墨的淡版,
     所以框后面的页面暗下去之后仍然是这套界面的冷调,不会泛出一层脏灰。 */
  --scrim:rgba(27,36,48,.38);
  /* 单一强调:品牌蓝。**这支蓝取自登录页那张实景照片的天空**(也是图没下完时的兜底渐变
     起始色)——品牌色第一次有了实物出处,不是凭空定的。2026-07-31 由深青绿 #0F6C5B 换来。
     白底对比度 6.4:1,可当正文用。 */
  --accent:#1E5FA8; --accent-d:#16487F; --accent-tint:rgba(30,95,168,.10); --accent-tint2:rgba(30,95,168,.16);
  /* 语义色(与强调色分离) */
  --danger:#C0392B; --danger-tint:rgba(192,57,43,.09);
  --warn:#A16207;  --warn-tint:rgba(161,98,7,.12);
  --ok:#2F855A;    --ok-tint:rgba(47,133,90,.12);
  /* 人工/红线拦截。**原来是蓝 #3557B7,强调色换成蓝之后必须让开**——否则
     `.tag.human`(红线拦截)和 `.tag.teal`(普通意图)在应答台里是同一个位置、
     同一段文字,只靠颜色区分,两个都蓝就等于「红线命中」长得跟「正常回答」一模一样,
     而且不报错。换成墨紫,与蓝/红/黄/绿四支都拉得开;对比度 9.0:1。 */
  --human:#5B21B6; --human-tint:rgba(91,33,182,.10);
  /* 字体 */
  /* 思源黑体在前(自托管、真交付得出来);后面那串是最后兜底 —— 字体还没下完、
     或者 woff2 加载失败时,界面用系统字体先撑住,不至于变成方框。 */
  --sans:"思源黑体","PingFang SC","Microsoft YaHei",system-ui,sans-serif;
  --mono:"JetBrains Mono",ui-monospace,"Cascadia Code",Consolas,monospace;

  /* ── 字阶(2026-07-29 定,用户在真数据预览页上逐档比过才拍板的「B 方案」) ──
     **全站字号只许从这十个 token 里取,不许再写死 px。**
     `tests/test_type_scale.py` 会挡住新的写死值。

     为什么要有这套东西:
     1. 旧值最小到 10.5px,中文在这个尺寸下笔画会糊(「查表」「AI判」这类来源标记、
        S2.N4.S3 这类坐标号)。**12px 是中文界面的底线**,现在最小档是 13px。
     2. 旧值有 14 个档位挤在 10.5～26px 之间,光 10.5/11/11.5/12/12.5/13/13.5/14/14.5/15
        就十档全塞在 4.5px 里 —— 肉眼分不出层次,等于白设。现在收敛成十档且拉开。
     3. 用户此前一直把浏览器缩放到 125% 才看得舒服。**缩放会把行高、内边距、边框
        一起放大**,他 2560 宽的屏在 125% 下只剩 2048 可用,台账一屏从 21 行掉到 15 行;
        只放大字号则是 18 行。同样的可读性,多看 3 行。

     --fs-scale 是唯一的旋钮:设置页的「字号 小/中/大」改的就是它(见 app.js),
     存在浏览器本地,**按人生效不按公司** —— 不同客服眼睛不一样,这是显示偏好、
     不是业务配置,不该占数据库、也不该一个人改了全公司跟着变。 */
  --fs-scale:1;
  --fs-xs:    calc(13px   * var(--fs-scale));  /* 来源标记 / 坐标号 / 眉标 / 超时角标 */
  --fs-tag:   calc(14px   * var(--fs-scale));  /* 状态标签 / KPI 标题 */
  --fs-sub:   calc(15px   * var(--fs-scale));  /* 说明文字 / 档案字段 / 轨迹表 */
  --fs-th:    calc(15.5px * var(--fs-scale));  /* 表头 */
  --fs-label: calc(16px   * var(--fs-scale));  /* 表单标签 / 小按钮 */
  --fs-body:  calc(17.5px * var(--fs-scale));  /* 正文 / 表格 / 输入框 / 按钮 / 导航 —— 主力 */
  --fs-title: calc(18px   * var(--fs-scale));  /* 页面基准 / 卡片标题 / 面板标题 */
  --fs-brand: calc(21px   * var(--fs-scale));  /* 顶栏品牌 */
  --fs-h1:    calc(24px   * var(--fs-scale));  /* 页面大标题 */
  --fs-num:   calc(31px   * var(--fs-scale));  /* KPI 大数字 */
  /* 字大了留白要跟一点,但**只跟一点**(×1.1)——跟满就退化成浏览器缩放,
     把一屏能看的行数又赔回去了。只作用于控件与表格这类"字撑着的"内边距,
     卡片、栅格那些装饰性留白不动。 */
  --pad: calc(1.1 * var(--fs-scale));
  --lh: 1.6;
  /* 圆角:控件 4 / 卡片 8 / 表格分隔 0 */
  --r-ctl:4px; --r-card:8px;
  /* 阴影 */
  --shadow:0 1px 2px rgba(16,24,40,.04);
  --shadow-l:0 4px 16px rgba(16,24,40,.10);
}

*{box-sizing:border-box;margin:0;padding:0}
body{background:var(--bg);color:var(--text);font-family:var(--sans);
     font-size:var(--fs-title);line-height:var(--lh);-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}
a{color:inherit}
.tnum{font-variant-numeric:tabular-nums}
.mono{font-family:var(--mono);font-variant-numeric:tabular-nums}

/* 无障碍:键盘焦点可见 */
:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:2px}
@media (prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important}}

/* 细滚动条 */
::-webkit-scrollbar{width:9px;height:9px}
::-webkit-scrollbar-thumb{background:#CFD6DE;border-radius:6px}
::-webkit-scrollbar-thumb:hover{background:#B8C1CC}
::-webkit-scrollbar-track{background:transparent}

/* ---------- 顶栏(全站统一) ---------- */
.top{display:flex;align-items:center;gap:16px;padding:12px 22px;
     border-bottom:1px solid var(--border);background:var(--surface)}
.blogo{width:26px;height:26px;border:1.5px solid var(--accent);border-radius:6px;position:relative;flex:none}
.blogo::before,.blogo::after{content:"";position:absolute;background:rgba(15,108,91,.28)}
.blogo::before{left:5px;right:5px;top:50%;height:1px}
.blogo::after{top:5px;bottom:5px;left:50%;width:1px}
.blogo i{position:absolute;left:50%;top:50%;width:6px;height:6px;border-radius:50%;
         background:var(--accent);transform:translate(-50%,-50%)}
.brand{display:flex;align-items:center;gap:10px;font-weight:700;font-size:var(--fs-brand);color:var(--text)}
.brand .en{color:var(--accent)}
.brand small{font-family:var(--mono);font-size:var(--fs-xs);color:var(--faint);letter-spacing:2px;font-weight:500;margin-left:2px}
.account{margin-left:6px;display:flex;align-items:center;gap:7px;font-size:var(--fs-body);color:var(--muted)}
.account b{color:var(--text);font-weight:600}
.account .dot{width:7px;height:7px;border-radius:50%;background:var(--ok)}
.navlink{margin-left:2px;font-size:var(--fs-body);color:var(--muted);text-decoration:none;
         padding:6px 11px;border-radius:var(--r-ctl);transition:.12s;white-space:nowrap}
.navlink:hover{color:var(--accent);background:var(--accent-tint)}
.navlink.on{color:var(--accent);background:var(--accent-tint);font-weight:600}
.clock{margin-left:auto;font-family:var(--mono);font-size:var(--fs-body);color:var(--muted);font-variant-numeric:tabular-nums}
.clock b{color:var(--text)}

/* ---------- 页标题区 ---------- */
.page-head{display:flex;align-items:baseline;gap:12px;padding:18px 24px 4px}
.page-head h1{font-size:var(--fs-h1);font-weight:700}
.page-head .sub{font-size:var(--fs-sub);color:var(--faint)}
.eyebrow{font-family:var(--mono);font-size:var(--fs-xs);letter-spacing:1px;color:var(--faint);text-transform:uppercase}

/* ---------- 卡片 ---------- */
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--r-card);box-shadow:var(--shadow)}
.card-pad{padding:16px 18px}
.card-title{font-size:var(--fs-title);font-weight:600;color:var(--text);display:flex;align-items:center;gap:8px}

/* ---------- 按钮(禁 pill,统一 4px) ---------- */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;
     font-family:var(--sans);font-size:var(--fs-body);font-weight:600;line-height:1;
     padding:calc(9px * var(--pad)) calc(15px * var(--pad));border-radius:var(--r-ctl);border:1px solid transparent;
     cursor:pointer;transition:.12s;white-space:nowrap;background:none}
.btn:disabled{opacity:.5;cursor:not-allowed}
.btn-primary{background:var(--accent);color:#fff;border-color:var(--accent)}
.btn-primary:hover:not(:disabled){background:var(--accent-d);border-color:var(--accent-d)}
.btn-ghost{background:var(--surface);color:var(--text);border-color:var(--border)}
.btn-ghost:hover:not(:disabled){border-color:var(--accent);color:var(--accent)}
.btn-danger{background:var(--surface);color:var(--danger);border-color:rgba(192,57,43,.4)}
.btn-danger:hover:not(:disabled){background:var(--danger-tint);border-color:var(--danger)}
.btn-sm{padding:calc(6px * var(--pad)) calc(11px * var(--pad));font-size:var(--fs-label)}

/* ---------- 表单控件 ---------- */
input,select,textarea{font-family:var(--sans);font-size:var(--fs-body);color:var(--text);
     background:var(--surface);border:1px solid var(--border);border-radius:var(--r-ctl);
     padding:calc(9px * var(--pad)) 11px;outline:none;transition:.12s;width:100%}
input:focus,select:focus,textarea:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-tint)}
input::placeholder,textarea::placeholder{color:var(--faint)}
label{font-size:var(--fs-label);color:var(--muted);font-weight:500}

/* ---------- 语义标签/状态芯片(4px,非 pill) ---------- */
.tag{display:inline-flex;align-items:center;gap:4px;font-size:var(--fs-tag);font-weight:600;
     border-radius:var(--r-ctl);padding:2px 8px;line-height:1.5;white-space:nowrap}
.tag.teal{color:var(--accent);background:var(--accent-tint)}
.tag.red{color:var(--danger);background:var(--danger-tint)}
.tag.amber{color:var(--warn);background:var(--warn-tint)}
.tag.green{color:var(--ok);background:var(--ok-tint)}
.tag.human{color:var(--human);background:var(--human-tint)}
.tag.gray{color:var(--muted);background:var(--surface3)}

/* ---------- KPI 数字块 ---------- */
.kpi{display:flex;flex-direction:column;gap:4px}
.kpi .k-label{font-size:var(--fs-tag);color:var(--muted)}
.kpi .k-num{font-family:var(--mono);font-variant-numeric:tabular-nums;font-size:var(--fs-num);font-weight:700;color:var(--text);line-height:1}
.kpi .k-num.accent{color:var(--accent)}
.kpi .k-unit{font-family:var(--sans);font-size:var(--fs-tag);color:var(--faint);font-weight:400;margin-left:3px}

/* ---------- 表格 ---------- */
/* 表格行高压到 1.4(正文仍是 --lh:1.6)。表格里一格就是一个值,不是要连读的句子,
   1.6 的行距在这里只是把行撑高、一屏少看几行。长文本块在单元格里另有自己的
   line-height(.note/.why/.tplbody 那些 1.7 起步的),不受这条影响。 */
.tbl{width:100%;border-collapse:collapse;font-size:var(--fs-body);line-height:1.4}
.tbl th{text-align:left;font-weight:600;color:var(--muted);font-size:var(--fs-th);
        padding:calc(10px * var(--pad)) 12px;border-bottom:1px solid var(--border);white-space:nowrap}
/* 上下 12.1px → 8px(左右 12px 不动)。**这里刻意直接乘 --fs-scale,没走 --pad**:
   --pad 的 ×1.1 是"字大了留白跟一点"的加成,用在正要收紧的这个值上,得倒推出
   7.27px 这种没人看得懂的基数。直接写 8px 是设计基准档的目标值,三档照样跟着缩放。
   表头 th 仍走 --pad(中档 11px)—— 表头比数据行略高是常规,没跟着一起收。 */
.tbl td{padding:calc(8px * var(--fs-scale)) 12px;border-bottom:1px solid var(--border2);color:var(--text)}
.tbl tbody tr:hover{background:var(--surface2)}
.tbl .num{font-family:var(--mono);font-variant-numeric:tabular-nums;text-align:right}

/* ---------- 弹窗(gfConfirm / gfPrompt,行为实现在 app.js) ---------- */
/* 用原生 <dialog> + showModal(),**不自己写遮罩**:遮罩、Tab 焦点圈在框内出不去、
   ESC 关闭、背后的页面自动 inert(点不到、读屏也读不到),这四件事浏览器全包了,
   自己写一遍只会做漏其中两三件。

   而且模态 <dialog> 进的是浏览器的 top layer —— **不参与 z-index 竞争**,
   所以这套设计系统至今一个 z-index 令牌都不需要,官网顶栏那个 70 也压不到它头上。

   视觉基调对齐设置页那块「红线放松」的就地确认条(.loosen):说清后果 → 显式确认。
   暖黄 = 要留个心眼;危险色 = 这一步撤不回来(货台的闭环就是它)。 */
dialog.gfd{
  /* **别删这行 margin:auto。** 本文件顶上那条 *{margin:0} 会把浏览器给模态框的
     margin:auto 盖掉(作者样式恒赢 UA 样式),不写就不是居中,是贴在屏幕左上角。 */
  margin:auto;
  width:min(calc(560px * var(--fs-scale)), calc(100vw - 32px));
  /* 内边距一点都不给 dialog 本体,全放到 .gfd-box 上 —— 这样"点到 dialog 本体"
     就精确等于"点在框外面",遮罩点击判定不用算坐标。 */
  padding:0;
  border:1px solid var(--border);border-radius:var(--r-card);
  background:var(--surface);color:var(--text);
  box-shadow:var(--shadow-l);overflow:hidden;
  transition:.12s;
}
dialog.gfd::backdrop{background:var(--scrim);transition:.12s}
/* 开场:淡入 + 轻微下沉。@starting-style 是唯一能给 showModal() 那一瞬间
   补上"起始态"的办法(元素是刚从 display:none 里出来的,没有起始态就没有过渡)。
   浏览器不支持就直接出现,只少一层动效,不影响任何功能。
   **关闭刻意不做动效** —— 关的时候人已经在等结果了,不该再拖 .12s。 */
@starting-style{
  dialog.gfd[open]{opacity:0;transform:translateY(-4px)}
  dialog.gfd[open]::backdrop{background:transparent}
}
.gfd-box{display:flex;flex-direction:column;gap:12px;padding:18px 20px;
         max-height:calc(100vh - 64px);overflow-y:auto}
.gfd-t{font-size:var(--fs-title);font-weight:600;color:var(--text);line-height:1.4}
/* 后果条 —— 跟 .loosen 同一套配方:语义色文字 + 同色淡底 + 一道竖杠,行距放宽好读。
   `white-space:pre-line` 让文案里的换行真的换行:后果常常是两句话,一句讲现在
   会发生什么,一句讲以后想反悔要怎么办,挤成一段就没人读了。 */
.gfd-why{font-size:var(--fs-sub);line-height:1.7;white-space:pre-line;
         padding:11px 13px;border-radius:var(--r-ctl);
         color:var(--warn);background:var(--warn-tint);border-left:3px solid var(--warn)}
.gfd-why.danger{color:var(--danger);background:var(--danger-tint);border-left-color:var(--danger)}
.gfd-lab{display:flex;flex-direction:column;gap:6px}
textarea.gfd-in{resize:vertical;line-height:var(--lh)}
.gfd-foot{display:flex;justify-content:flex-end;gap:9px;margin-top:2px}

/* ---------- 字段级报错 gf-err / gf-bad(行为实现在 app.js 的 gfFieldError) ---------- */
/* **弹窗里的「这里必须填」和页面上的字段报错共用这一条,不写两份。** 它们是同一件事:
   "你这个框填得不对"。分开写迟早跑偏成两种红。弹窗里那个是预先建好、用 hidden 切
   显隐;页面上那个是 gfFieldError 现插现删 —— 行为不同,长相必须相同。 */
.gf-err{font-size:var(--fs-sub);line-height:1.5;color:var(--danger);margin-top:4px}
/* 长相共用,**排版规则只给页面上那一种**(gfFieldError 插的节点带这个类,弹窗里
   那个不带)。弹窗里的 .gfd-box 是我们自己排的竖列,红字本来就在该在的位置;
   页面上的红字则是空降到别人的行里,得自己想办法活下来:

   ① width:0 + min-width:100% —— 落进会换行的 flex 行(设置页的登记列表、助手机
      那一栏)时逼它独占下一行,否则会跟输入框并排挤在同一行。
      **不用 flex-basis:100%**:那个写法会把整行撑宽,实测把「生成激活码」连人带
      钮往左推了 100px;width:0 对"这行到底该多宽"贡献为零,行宽纹丝不动。
   ② order:99 —— 红字在 DOM 里紧跟着输入框(读屏和 aria-describedby 要的就是这个
      顺序),但那样排版时会把排在它后面的按钮顶到下一行去,而那正是用户刚点过、
      马上还要再点的那个钮(实测横着挪了 800px)。order 只改排版位置、不动 DOM 顺序。
      **这也是它必须跟弹窗那份分开的原因**:弹窗里 .gfd-foot(算了/确定)排在红字
      后面,给红字 order:99 会把它甩到按钮下面去。
   ③ 非 flex 容器(超管的表格单元格)里这三条都不生效,天然就在输入框正下方。 */
.gf-err.gf-err-loose{width:0;min-width:100%;max-width:100%;order:99}
/* `hidden` 属性靠的是浏览器内置的 [hidden]{display:none},而作者样式里随便哪一条
   display 都能盖过它 —— 本项目在这个坑上栽过两次。这里虽然没给 .gf-err 写
   display,仍然显式钉一条,免得哪天有人加一句 display:flex 就把报错悄悄点亮。 */
.gf-err[hidden]{display:none}
/* 出错的框自己也要变红 —— 只有红字没有红框,眼睛得先读完字才知道说的是哪个框。
   **:focus 那条不能省**:gfFieldError 会把焦点送回这个框,而上面 input:focus
   的蓝边框权重(0,1,1)压得过单独的 .gf-bad(0,1,0),不写就是"红字配蓝框"。 */
.gf-bad{border-color:var(--danger)}
.gf-bad:focus{border-color:var(--danger);box-shadow:0 0 0 3px var(--danger-tint)}
/* 键盘焦点圈也跟着变红。**不能直接把它去掉** —— 那是键盘用户唯一能看到的"我在哪"。
   但保留默认的蓝色会在同一个框上套出一红一蓝两道圈:焦点是 gfFieldError 主动送
   回来的,所以这个框十有八九同时处在"出错"和"聚焦"两种状态里,天天撞。 */
.gf-bad:focus-visible{outline-color:var(--danger)}

/* ---------- 出错提示条 gfToast(行为实现在 app.js) ---------- */
/* 只做「出错」一档。**成功不进这里** —— 成功的反馈就该出现在你刚动过的那个东西
   旁边(按钮变「已保存」、表头出 ✅),飞到屏幕角上反而要人扭头去找。

   **靠 popover 进浏览器的 top layer,不是靠 z-index。** 后台页面里唯一的 z-index 是
   表格吸顶表头那个 1,而它会造出层叠上下文 —— 一个不写 z-index 的固定层会被压在
   表头下面(长表格滚动时正好挡住)。用 popover 就完全绕开这场竞争,这套设计系统
   到现在仍然一个 z-index 令牌都不需要。 */
.gft{
  position:fixed;inset:auto;margin:0;padding:0;border:0;background:none;overflow:visible;
  /* 顶栏高度随字号档变,由 app.js 量一次写进这个变量;拿不到就用兜底值 */
  top:var(--gf-toast-top,64px);right:14px;
  width:min(calc(380px * var(--fs-scale)), calc(100vw - 28px));
  display:flex;flex-direction:column;gap:8px;
}
/* **容器只在有提示条的时候才存在于 DOM 里**(app.js 建了又删),所以这里可以
   放心写 display:flex。别改成"常驻 DOM + 靠 display 切换" —— `[popover]` 的隐藏
   同样是靠浏览器内置的 display:none,作者样式里随便哪条 display 都能盖过它,
   本项目在这个坑上已经栽过两次(见 setup.html 顶部的 [hidden] 兜底)。 */
.gft-i{
  display:flex;align-items:flex-start;gap:9px;
  padding:11px 12px;border-radius:var(--r-ctl);border-left:3px solid var(--danger);
  /* 两层背景叠出"不透明的淡红" —— 提示条飘在正文之上,纯 tint 是半透明的,
     底下压着表格就花了。渐变那一层是纯色,只是借它来叠色,不是真渐变。 */
  background:linear-gradient(var(--danger-tint),var(--danger-tint)),var(--surface);
  box-shadow:var(--shadow-l);
  font-size:var(--fs-sub);line-height:1.6;color:var(--text);
  transition:.12s;
}
/* 从右侧滑入。同 dialog 那边:@starting-style 是给"刚插进 DOM 的元素"补起始态的
   唯一办法,浏览器不支持就直接出现,不影响功能。 */
@starting-style{ .gft-i{opacity:0;transform:translateX(10px)} }
.gft-msg{flex:1;min-width:0;overflow-wrap:anywhere}
.gft-x{flex:none;background:none;border:0;cursor:pointer;color:var(--faint);
       font-size:var(--fs-tag);line-height:1;padding:2px 3px;
       border-radius:var(--r-ctl);transition:.12s}
.gft-x:hover{color:var(--danger);background:var(--danger-tint)}
/* 兜底:浏览器不认 popover 时它就是个普通固定层,得给个 z-index 才压得住吸顶表头。
   **这是全站唯一一个写死的 z-index**,只在这条降级路径上生效 —— 为一条兜底
   单开一套 z-index 令牌,比这个字面量更糟。 */
@supports not selector(:popover-open){ .gft{z-index:9999} }
