/* BidPal 全站基础层 —— 设计令牌 + 工具类配色覆盖
   对比度均为对白底实算值，已满足 WCAG 2.2 AA（正文 ≥4.5:1，UI 边界 ≥3:1）

   ⚠️ 本文件是**每一个页面**都会加载的样式（含不继承 base.html 的扫码认证页、供应商门户页）。
   components.css 只有继承 base.html 的页面才加载，所以凡是"全站都要一致"的规则必须放在本文件，
   放在 components.css 里会在独立页面上静默失效。 */

:root{
  /* ── 品牌色阶（保留原品牌色相，按明度派生四档）── */
  --brand-700: #4E5E0C;   /* 7.17:1 — 强调文字、链接 */
  --brand-600: #667D08;   /* 4.66:1 — 主按钮填充（配白字达标） */
  --brand-500: #8BA910;   /* 2.70:1 — 品牌原色，仅用于图标与装饰，禁止用于文字 */
  --brand-100: #F1F5DF;   /* 浅底：选中行、提示条 */
  --brand-border: #C7D68A;/* 品牌色描边 */

  /* ── 文字 ── */
  --text-primary:   #1F2329;  /* 15.78:1 */
  --text-secondary: #5A6472;  /*  6.00:1（替代 Bootstrap 的 gray-500，那个只有 2.07:1） */
  --text-tertiary:  #8A94A6;

  /* Bootstrap 的次级文字色是 rgba(73,80,87,.75)：合成后对白底 4.23:1、对 bg-light #F8F9FA
     只有 4.10:1，小字号均不达 AA。zephyr 里引用它的地方不止 .text-muted——
     .text-body-secondary、面包屑分隔符与当前项、.form-text、::placeholder、
     caption/.figure-caption、分页禁用态，一并换成本令牌。
     真正的禁用态不受影响：.btn:disabled 走 --bs-btn-disabled-color、
     .dropdown-item:disabled 走 --bs-dropdown-link-disabled-color、
     .form-control:disabled 只改背景不改字色，三者都不读这个变量。
     加载顺序：本文件在 zephyr 之后，同权重（:root）靠后生效，不需要 !important。 */
  --bs-secondary-color: var(--text-secondary);

  /* ── 边界 ── */
  --border-base:        #E4E7EC;  /* 装饰性分隔线 */
  --border-strong:      #D0D5DD;  /* 按下态填充（比 hover 再深一档） */
  /* 实心次级按钮的按下态填充，比 --border-strong 再深一档。
     存在的唯一理由：**跟描边版区分**。两族的填充阶梯整体错开一格——
       描边版  透明 → hover #E6E6E6 → active #D0D5DD
       实心版  #E6E6E6 → hover #D0D5DD → active #B4BCC9
     若两族共用一个 active 值，边框/文字/填充/阴影在按下那一瞬全同，
     在白色模态框里就分不出哪个是实心、哪个是描边（第 9 轮实测差异 1.0000）。 */
  --bg-pressed:         #B4BCC9;
  /* 交互边界（输入框/复选框/描边按钮）。WCAG 1.4.11 要求 ≥3:1，但**必须按最差底色取值**：
     同一个 #8A94A6 在白底 3.06:1 够，落到 body 的 #F8F9FA 只剩 2.90:1、
     落到 alert-info 的 #D4E5F0 只剩 2.37:1 —— 只验白底会漏掉后两者（第 8 轮实测）。
     现值 #748092 在**枚举到的每一个实际底色**上都 ≥3:1（真浏览器复算）：
       白 #FFFFFF 4.00 / 卡片头脚 #FAFAFA 3.84 / body #F8F9FA 3.80 / bg-subtle #F7F8FA 3.77 /
       bg-sunken #F2F4F7 3.63 / brand-100 #F1F5DF 3.60 / table-warning #FDF2DF 3.61 /
       table-success #D5F0E6 3.32 / hover 填充 #E6E6E6 3.21 / 最差：alert-info #D4E5F0 3.10
     最差 3.10 留了余量，将来多一种浅底也不会掉出去。 */
  --border-interactive: #748092;

  /* ── 背景 ── */
  --bg-surface: #FFFFFF;
  --bg-subtle:  #F7F8FA;
  --bg-sunken:  #F2F4F7;
  --bg-hover:   #E6E6E6;  /* 悬停/实心次级按钮填充（zephyr 原本的 hover 灰，全站统一用这一个） */

  /* ── 语义色（状态标签用）── */
  --ok-bg: #E8F5EC;     --ok-tx: #046C4E;
  --warn-bg: #FFF4E5;   --warn-tx: #8A4B00;
  --danger-bg: #FDECEC; --danger-tx: #A32116;
  --info-bg: #EAF2FE;   --info-tx: #1B4B91;

  /* ── 间距（4px 基准 / 8px 节奏；补上 Bootstrap 缺的 12/20/32）── */
  --s1:4px; --s2:8px; --s3:12px; --s4:16px;
  --s5:20px; --s6:24px; --s7:32px; --s8:48px;

  /* ── 圆角（只用两档）── */
  --r-sm:4px; --r-md:6px; --r-lg:8px;

  /* ── 阴影（只用两档，强度远低于 Bootstrap 默认）── */
  --e1:0 1px 2px rgba(16,24,40,.06);
  --e2:0 2px 4px rgba(16,24,40,.08),0 1px 2px rgba(16,24,40,.06);
}

/* ══════════════════════════════════════════════════════════════════
   工具类配色覆盖（原在 components.css，因独立页面拿不到而搬到这里）
   本文件在 zephyr 之后加载，同权重靠后生效，所以不需要 !important——
   除了 background-color：Bootstrap 的 .bg-* / .text-bg-* 工具类自身就带 !important，
   普通声明压不过，必须跟着用 !important。
   ══════════════════════════════════════════════════════════════════ */

/* 成功徽章：zephyr 的 .badge 默认白字配 --bs-success #2FB380，只有 2.66:1（不达 AA）。
   换成语义令牌的浅底深字：--ok-tx 对 --ok-bg = 5.74:1。
   .text-bg-success 一并修——它也是白字压同一个 #2FB380，且 project_detail.html、
   supplier_portal.html 都用它渲染状态，不修会出现同一页两种绿章。 */
.badge.bg-success,
.text-bg-success {
  background-color: var(--ok-bg) !important;
  color: var(--ok-tx) !important;
}

/* 警告徽章：.text-bg-warning 是 zephyr 的 #F4BD61 配白字，只有 1.71:1（全站最差）。
   同源修法：--warn-tx 对 --warn-bg = 6.26:1。
   （.badge.bg-warning 不动——各处引用都自带 text-dark，实测 9.03:1。） */
.text-bg-warning {
  background-color: var(--warn-bg) !important;
  color: var(--warn-tx) !important;
}

/* 次要徽章：zephyr 下 --bs-secondary = #fff，.bg-secondary 就是纯白底。
   文字本身达标（zephyr 自带 #212529，对白底 15.28:1），
   但白色徽章压在白/近白卡片上没有可见边界，与相邻的绿色徽章不成对。
   换成中性浅底：--text-secondary 对 --bg-sunken = 5.44:1。 */
.badge.bg-secondary {
  background-color: var(--bg-sunken) !important;
  color: var(--text-secondary);
}

/* .text-bg-secondary 是同源的另一条：zephyr 写死了 background:RGBA(255,255,255,…)!important。
   文字本身达标（黑压白 15.28:1），但白徽章压在白/近白卡片上没有边界，
   与相邻的绿章、黄章不成对。跟上面 .badge.bg-secondary 统一成同一个浅灰。 */
.text-bg-secondary {
  background-color: var(--bg-sunken) !important;
  color: var(--text-secondary) !important;
}

/* 浅底徽章：zephyr 把 .badge.bg-light 和 .badge.bg-secondary 写在同一组选择器里
   （.badge.bg-light,.badge.bg-secondary{color:#212529}），权重 0,2,0。
   这里用同权重覆盖，与上面 .badge.bg-secondary 统一成同一个灰，避免同页两种灰。
   层叠说明（别再误判）：
     · 模板里在浅底徽章上再挂 .txt-secondary 是**冗余**——值是同一个灰，但决定权在
       这条 0,2,0 的规则，不是因为 .txt-secondary（0,1,0）压过了它。
     · 挂 Bootstrap 的 .text-secondary 会**坏事**：那条带 !important，权重再高也压不过它，
       而 zephyr 的 secondary 是纯白，结果是白字落在浅底上——实测 1.05:1，等于看不见。 */
.badge.bg-light {
  color: var(--text-secondary);
}

/* 正文绿字：zephyr 的 --bs-success #2FB380 对白底 2.66:1，对 .table-success 底 #D5F0E6 更差，
   远不达 AA 4.5:1。换成语义令牌 --ok-tx，保留绿色语义且达标（白底 6.44:1）。
   权重 0,1,0 低于 .badge.bg-success（0,2,0），所以 dimensions.html 那种
   「bg-success 浅底 + text-success」的徽章仍走上面那条规则，观感不变。 */
.text-success { color: var(--ok-tx) !important; }

/* 正文黄字：zephyr 的 --bs-warning #F4BD61 对白底 1.71:1，全站最差。
   换成语义令牌 --warn-tx（白底 6.80:1）。
   注意 dimensions.html:155/157 与 project_detail.html:413/414 是 if/else 姊妹分支——
   绿黄必须成对改，只改一边会让两个分支一个深一个浅，比原来更怪。 */
.text-warning { color: var(--warn-tx) !important; }

/* 实心成功填充（卡片头 / toast / 按钮）：.bg-success 配 .text-white 是白字压 #2FB380 = 2.66:1。
   改用 --ok-tx 作填充，白字 6.44:1。--ok-tx 本身就是「能在浅底上当字、也能压白字」的深绿。
   只匹配同时带 text-white 的元素：不带 text-white 的 .badge.bg-success 不受影响，仍走浅底规则。 */
.text-white.bg-success { background-color: var(--ok-tx) !important; }

/* 同一招用在 secondary 上：zephyr 下 --bs-secondary 是纯白，
   `bg-secondary text-white`（人员页钉钉联系人列表里的姓名首字头像）= 白字压白底，
   实测 1.00:1，整个圆圈只剩一个看不见的字。换成浅灰底 + 次级灰字（5.44:1），
   与 .badge.bg-secondary 同一个灰，同页不会有第二种灰。 */
.text-white.bg-secondary {
  background-color: var(--bg-sunken) !important;
  color: var(--text-secondary) !important;
}

/* ── 按钮配色 ──
   zephyr 的按钮色都是主题原色：primary 是蓝 #3459E6，success 是亮绿 #2FB380、warning 是亮黄 #F4BD61。
   两种问题：
     ① 白字压后两者只有 2.66:1 / 1.71:1，不达 AA；
     ② primary 是蓝色，跟全站品牌绿不是一套（独立页面尤其明显——它们不加载 components.css，
        原本 .btn-primary 的品牌覆盖压根没生效，413.html 上是一颗蓝按钮）。
   统一成一套规则：实心按钮 = 够深的填充 + 白字；描边按钮 = 同一个深色做文字和边框，hover 才填充。
   放在本文件的原因同上面：独立页面（413 / 快速报价 / 供应商门户 / 团队确认）也要生效。 */

/* ── 焦点环（全站唯一一套）──
   zephyr 给每族按钮各配一个半透明环 rgba(--bs-btn-focus-shadow-rgb, .5)。
   半透明环合成到实际底色上只有 1.19~2.27:1（真浏览器实测，见下表），
   最差的是描边次级——白色环压白底 1.0000，等于完全看不到焦点在哪。

   统一换成**不透明双层环**：
     外层 4px --border-interactive 深环 —— **这一层才是焦点指示本体**，
       对**每一个实际底色**都 ≥3:1：白底 4.00 / body #F8F9FA 3.80 / bg-sunken 3.63 /
       brand-100 3.60 / 最差 alert-info 3.10
     内层 2px --bg-surface 白环 —— 只是**隔断**，把深色外环跟按钮自身的填充分开，
       免得环和按钮边界糊在一起（深色实心按钮上尤其明显）

   ⚠️ 验收口径（第 11 轮更正，别再写成「两层环都达 3:1」）：
     只有外层深环承担对比度达标。白隔断对禁用填充 #F2F4F7 只有 **1.1019:1**、
     对白底 **1.0000:1** —— 它按定义就达不到 3:1，也**不该**按 3:1 去要求它：
     「焦点在哪」由深色外环读出，白环只负责让外环和填充不粘连。
     白环对实心填充的 4.66 / 6.80 / 6.44 是「隔断读得出」的说明，不是达标依据。

   覆盖方式：zephyr 只按变体改 --bs-btn-focus-shadow-rgb，真正的 --bs-btn-focus-box-shadow
   是在 .btn 上定义的。这里用同权重 (0,1,0)、靠后加载覆盖一条，全族一次生效，
   不需要逐个变体写，也不需要 !important。 */
.btn {
  --bs-btn-focus-box-shadow: 0 0 0 2px var(--bg-surface), 0 0 0 4px var(--border-interactive);
}

/* 可聚焦的禁用元素，焦点环必须还回来
   ──────────────────────────────────────────────────────────────
   背景：全站统一禁用态把禁用元素做成「虚线边框 + 浅灰填充 + 次级灰字」，
   但 zephyr 还有一条 `.btn.disabled,.btn:disabled,fieldset:disabled .btn{…box-shadow:none}`
   （static/vendor/zephyr.bootstrap.min.css:11，字符偏移 56252），它和
   `.btn:focus-visible{box-shadow:var(--bs-btn-box-shadow),var(--bs-btn-focus-box-shadow)}`
   （同文件同行的字符偏移 55387）**权重完全相同 (0,2,0)**，而它排在后面 ——
   于是焦点环被整条清成 `box-shadow:none`。原生 disabled 按钮不进 Tab 序列，看不出来；
   但 `aria-disabled + tabindex="0"` 的元素是能 Tab 到的（这正是它存在的意义），
   键盘用户会卡在一个**看不见的焦点**上 —— WCAG 2.4.7（Focus Visible，AA）不达标。
   实测（真实 Flask + Chromium，键盘 Tab）：
     禁用 span   聚焦时 box-shadow: none
     普通按钮    聚焦时 0 0 0 2px #FFFFFF, 0 0 0 4px #748092 的外层灰环对白底 4.00:1

   修法：把值原样还回来（和普通按钮同一套双层环，不新造第二种焦点语言）。
   为什么不做得更弱：环代表的是「键盘现在停在这里」，不是「这里能点」。
   APG 在这里的态度是**条件式**的，不是一条统一要求（第 11 轮更正，原文见下）：
     「Focusability of disabled controls」（APG 1.2 §6.7，现 APG 站点上位于
     “Developing a Keyboard Interface” 一节）开头即：默认情况下被禁用的 HTML input
     元素会被移出 Tab 序列，多数场景这样就行；**当禁用元素确实需要保持可发现时**，
     才改用 `aria-disabled="true"` 让它保持可聚焦（举例：工具栏的复制/剪切/粘贴按钮）。
     —— 所以「保持可聚焦」是为可发现性做的**选择**，不是对所有禁用控件的统一要求，
     更不是一条「必须保持焦点样式」的硬性条款。本项目选了这条路线（理由见
     templates/project_detail.html 的注释），选了它就得把焦点样式一并做足，
     否则等于白选。
   若把禁用态的环调弱，就会掉到 3:1 以下，反而连「焦点在哪」都读不出来。

   达标依据（条款编号已逐条对 WCAG 2.2 原文核过）：
     · SC 1.4.11 Non-text Contrast（AA）—— 焦点指示与**相邻颜色** ≥3:1。
       外层深环对白底 4.00、对禁用填充 #F2F4F7 3.63，达标。
     · SC 2.4.13 Focus Appearance（AAA）—— 指示区域至少覆盖未聚焦态 2 CSS px 周长的
       边界，且聚焦/未聚焦**同一批像素**的对比变化 ≥3:1。外层深环恰好 2px 厚、绕满整圈，
       白底 4.00:1、禁用填充 3.63:1，达标。
     ⚠️ 编号更正：2.4.11 是 Focus Not Obscured (Minimum)（AA，讲焦点元素不被遮挡），
        2.4.13 才是 Focus Appearance（AAA）。原注释写错过，别再改回去。
     ⚠️ 白隔断不参与达标换算（对禁用填充只有 1.1019:1），口径见上方「焦点环」段。

   作用域边界（第 11 轮澄清）：`fieldset:disabled .btn:focus-visible` 这一条**不只是**
   命中被禁用的按钮 —— HTML 只让 fieldset 里的表单控件变 :disabled，**`<a class="btn">`
   并不会被禁用**，它照样可聚焦，也一样被这条选择器命中。**这是有意保留的**：
   zephyr 的 `fieldset:disabled .btn{box-shadow:none}` 权重 (0,2,1) 高于
   `.btn:focus-visible` 的 (0,2,0)，所以恰恰是这些「没被禁用、但处在禁用 fieldset 里」的
   元素会丢掉焦点环 —— 它们最需要这一条。删掉它反而会新造一处焦点不可见。
   真正成立的边界是另一条：本规则**以 `:focus-visible` 为前提**，只可能给「已经被键盘
   聚焦的元素」补环，不可能给未聚焦的元素添任何东西（下面那条禁用**外观**规则才是另一回事，
   已按「只覆盖真正禁用/aria-disabled 的元素」收窄，见文件末尾「全站统一禁用态」）。
   （模板现状：`<fieldset>` 出现 0 次，本条是为将来留的正确行为，不是现有回归。）

   覆盖方式：`:focus-visible` 的这次选择器吃到 (0,3,0)（fieldset 那条 (0,3,1)），
   都高于 zephyr 的 (0,2,0)，本文件又靠后加载，稳赢；
   只在聚焦时生效，未聚焦的禁用态（虚线 + 浅灰）一点不动。
   ⚠️ 强制颜色模式下 box-shadow 会被整体丢弃，另有回退，见下方
   `@media (forced-colors: active)`。 */
.btn.disabled:focus-visible,
.btn:disabled:focus-visible,
.btn[aria-disabled="true"]:focus-visible,
fieldset:disabled .btn:focus-visible {
  box-shadow: var(--bs-btn-box-shadow), var(--bs-btn-focus-box-shadow);
}

/* ── 强制颜色模式（Windows 高对比度）下的焦点回退 ──
   为什么必须单独写一条：上面整套焦点指示是**纯 box-shadow**，而强制颜色模式下
   `box-shadow` 会被强制为 `none`（浏览器把颜色类属性整体换成系统色，阴影不参与，
   直接丢弃），zephyr 又在 `.btn:focus-visible` 里写了 `outline:0`
   （static/vendor/zephyr.bootstrap.min.css:11，与上面两条同一行）——
   于是阴影没了、outline 也被作者置空，焦点指示**彻底消失**。

   实测（真实 Flask + Chromium 148，`forced-colors: active` 模拟；修复前）：
     a.btn.btn-sm.btn-primary      聚焦时 outline-style:none / outline-width:0px / box-shadow:none
     span.btn[aria-disabled=true]  聚焦时 outline-style:none / outline-width:0px / box-shadow:none
   —— 两者 `:focus-visible` 都为 true，屏幕上却没有任何焦点痕迹。

   修法：改用 `outline` 画焦点，颜色取系统色关键字 `CanvasText`。
   依据：
     · 强制颜色模式下 outline-color 本身是被强制替换的属性，写 `CanvasText` 与系统的
       高对比度配色自洽；CanvasText 与 Canvas 是一对保证可读的系统色，必然拉得开对比，
       不会像写死 #748092 那样在深色高对比主题上失效。
     · 2px 实线 + 2px 偏移，落在按钮外侧的页面底色上；面积与厚度都 ≥ 上面 2.4.13 的
       「2 CSS px 周长」口径，且不依赖 box-shadow，强制颜色模式下依然画得出来。
     · 不用 `forced-color-adjust: none` 把整个按钮排除出强制颜色 —— 那会连带把按钮的
       文字/填充也钉死在品牌色上，在用户自定义的高对比配色里可能再次不可读。只补 outline
       是最小干预。
     · 不需要 !important：zephyr 的 `outline:0` 在 `.btn:focus-visible`（0,2,0），
       本条带 `:focus-visible` 的选择器最低也是 (0,3,0)，且本文件靠后加载。

   ⚠️ 覆盖范围与已知缺口：
     · 本轮只覆盖按钮（含上面那组禁用/aria-disabled 选择器），与第 11 轮的改动范围一致。
     · **表单控件的焦点同样依赖 box-shadow**（`.form-control:focus` / `.form-check-input:focus`
       都用品牌色光圈），强制颜色模式下会有同一类缺口，本轮**未处理**，留作已知遗留项。
     · 本项**未做真机强制颜色模式验收**：只有 Chromium 148 的 `forced-colors: active`
       媒体模拟（能验证 `box-shadow` 归零与 outline 生效），没有在 Windows 高对比度 /
       真实强制配色主题下人工确认过配色观感。 */
@media (forced-colors: active) {
  .btn:focus-visible,
  .btn.disabled:focus-visible,
  .btn:disabled:focus-visible,
  .btn[aria-disabled="true"]:focus-visible,
  fieldset:disabled .btn:focus-visible {
    outline: 2px solid CanvasText;
    outline-offset: 2px;
  }
}

/* 主按钮：brand-600（白字 4.66:1），hover/active 加深到 brand-700 */
.btn-primary {
  --bs-btn-bg: var(--brand-600);
  --bs-btn-border-color: var(--brand-600);
  --bs-btn-hover-bg: var(--brand-700);
  --bs-btn-hover-border-color: var(--brand-700);
  --bs-btn-active-bg: var(--brand-700);
  --bs-btn-active-border-color: var(--brand-700);
}

/* 描边主按钮：文字与边框都用 brand-600（对白底 4.66:1），hover 填充后白字 */
.btn-outline-primary {
  --bs-btn-color: var(--brand-600);
  --bs-btn-border-color: var(--brand-600);
  --bs-btn-hover-bg: var(--brand-600);
  --bs-btn-hover-border-color: var(--brand-600);
  --bs-btn-active-bg: var(--brand-700);
  --bs-btn-active-border-color: var(--brand-700);
}

/* ── 次级描边按钮 ──
   zephyr 的 --bs-secondary 是纯白 #fff，`.btn-outline-secondary` 里
   --bs-btn-color / --bs-btn-border-color / --bs-btn-disabled-color 全是 #fff；
   边框另被普通规则写死 #e6e6e6（对白底 1.25:1）。描边按钮没有填充色，
   边框是它唯一的形状信息 —— 1.25:1 等于一颗幽灵按钮，启用态也不达 WCAG 1.4.11 的 3:1。
   这里按「一个够深的边界 + 逐级加深的填充」把整套状态重做。

   状态差异（填充三档，互不相同；边框全程同一个深色）：
     基础  填充透明（页面底色透出来）
     hover 填充 --bg-hover #E6E6E6（zephyr 原值，保留）
     active 填充 --border-strong #D0D5DD（比 hover 再深一档，做出按下去的手感）
     禁用  见文件末尾的「全站统一禁用态」——虚线边框 + --bg-sunken 填充 + 次级灰字
     focus-visible 边框同上 + 焦点环（焦点环在 .btn 上统一，见上文）

   覆盖方式（第 8 轮踩过，别再走弯路）：
     zephyr 有一批**不读变量**的普通规则直接写死 #e6e6e6，而且权重不同——
       .btn-outline-secondary{border-color:#e6e6e6}                          (0,1,0)
       .btn-outline-secondary:hover / :active{...border-color:#e6e6e6}       (0,2,0)
       .btn-outline-secondary.disabled / :disabled{border:1px solid #e6e6e6} (0,2,0)
     另外 Bootstrap 的 `.btn:first-child:active`、`:not(.btn-check)+.btn:active` 是 (0,3,0)，
     读的是 --bs-btn-active-border-color。
     → 只改变量不管用（普通规则不读变量）；只写基础态选择器也不管用（hover/active 漏网）；
       只写 (0,2,0) 也不管用（(0,3,0) 那两条压过来）。
     三条路一起走：变量全量换 + 同权重普通规则兜底（本文件靠后加载）+ active 填充走变量。
     :focus-visible 读 --bs-btn-hover-border-color，.active/.show 读 --bs-btn-active-*，
     都由变量收口，不必逐个写选择器。 */
.btn-outline-secondary {
  --bs-btn-border-color: var(--border-interactive);
  --bs-btn-hover-border-color: var(--border-interactive);
  --bs-btn-active-border-color: var(--border-interactive);
  --bs-btn-active-bg: var(--border-strong);
}
/* 基础态与禁用态：zephyr 的普通规则不读变量，必须同权重直写 */
.btn-outline-secondary,
.btn-outline-secondary.disabled,
.btn-outline-secondary:disabled {
  border-color: var(--border-interactive);
}
/* hover / active：zephyr 的 (0,2,0) `border-color:#e6e6e6` 会盖掉基础态，同样同权重直写。
   :active 的填充也直写一遍。这里两条路各管一半，别只写一条：
     · 按钮**不是**首个子元素时（首页批量条«锁»那种），zephyr 的 (0,2,0) 填充生效 → 靠这里的直写压掉；
     · 按钮**是**首个子元素时，`.btn:first-child:active`（0,3,0）压过本文件的 (0,2,0)
       → 靠上面的 --bs-btn-active-bg 变量收口。
   两条路取值相同，所以哪种都落到 --border-strong。 */
.btn-outline-secondary:hover,
.btn-outline-secondary:active,
.btn-outline-secondary.active,
.btn-outline-secondary.show {
  border-color: var(--border-interactive);
}
.btn-outline-secondary:active {
  background-color: var(--border-strong);
}

/* ── 次级实心按钮 ──
   zephyr 的 --bs-secondary 是纯白，`.btn-secondary` = 白底 + #e6e6e6 边框。
   全站 54 处都是模态框的「取消 / 关闭」，模态框底色恒为白 ——
   实测边框对白底只有 1.2481:1，等于一颗「只有文字、没有边界」的按钮（文字本身 15.43:1 达标）。
   修法与描边版同档：边框 --border-interactive；同时把填充从纯白换成灰。
   换填充不是为了对比度，是为了**跟描边版不撞脸**：
   两者改完边框完全相同，若填充也相同，在白色模态框里就彻底分不出来
   （描边版本就是为「两者语义不同：普通按钮 vs 描边按钮」留的区分）。
   顺带这也恢复了 Bootstrap secondary「灰色按钮」的本意（zephyr 把它改成了白的）。

   ⚠️ 填充深度是**实测挑的，别再往回改浅**：
     第一版填 --bg-sunken #F2F4F7（对白底 1.10:1），真浏览器截图里跟描边版
     在白色模态框内肉眼分不出来 —— 两颗形状、边框、大小完全一样，只差一层几乎看不见的灰。
     改填 --bg-hover #E6E6E6（对白底 1.25:1）后，实心块才稳定读得出来。
     再深就得连 hover 一起重排（hover 必须比基础态更深），不划算。

   与 .btn-outline-secondary 的可区分性（白底模态内实测）：
     本按钮   填充 #E6E6E6 实心块，对白底 1.2481:1；边框 #748092 对填充 3.2078:1
     描边版   填充透明，中间透出底色；边框相同、对白底 4.0036:1
   基础态差一层实心灰，hover 后本按钮变 #D0D5DD、描边版才刚变 #E6E6E6，
   两档梯度全程错开一格 —— **包括按下态**（第 9 轮实测发现原来两族 active 都写 --border-strong，
   那一格是撞的：见 --bg-pressed 的说明）。 */
.btn-secondary {
  --bs-btn-bg: var(--bg-hover);
  --bs-btn-border-color: var(--border-interactive);
  --bs-btn-hover-bg: var(--border-strong);
  --bs-btn-hover-border-color: var(--border-interactive);
  --bs-btn-active-bg: var(--bg-pressed);
  --bs-btn-active-border-color: var(--border-interactive);
  color: var(--text-primary);
}
/* 同描边版：普通规则不读变量，基础态/禁用态/hover/active 各直写一遍 */
.btn-secondary,
.btn-secondary.disabled,
.btn-secondary:disabled {
  border-color: var(--border-interactive);
}
.btn-secondary:hover,
.btn-secondary:active,
.btn-secondary.active,
.btn-secondary.show {
  border-color: var(--border-interactive);
}
/* hover/active 的填充也直写：zephyr 那条 (0,2,0) 把两者的填充都钉成 #e6e6e6，
   只靠 --bs-btn-hover-bg / --bs-btn-active-bg 变量会被它压住。
   ⚠️ hover 与 active 必须分开写：写在一起的话 active 会拿到 hover 的值，
   两族在按下那一瞬又撞回同一个填充（第 9 轮实测差异 1.0000 的成因之一）。 */
.btn-secondary:hover {
  background-color: var(--border-strong);
}
.btn-secondary:active,
.btn-secondary.active,
.btn-secondary.show {
  background-color: var(--bg-pressed);
}

/* 成功按钮：填充 --ok-tx（白字 6.44:1），hover 再深一档 #03573E（8.62:1） */
.btn-success {
  --bs-btn-bg: var(--ok-tx);
  --bs-btn-border-color: var(--ok-tx);
  --bs-btn-hover-bg: #03573E;
  --bs-btn-hover-border-color: #03573E;
  --bs-btn-active-bg: #03573E;
  --bs-btn-active-border-color: #03573E;
}

/* 描边成功按钮：zephyr 是 #2FB380 的字压在白底上（2.66:1） */
.btn-outline-success {
  --bs-btn-color: var(--ok-tx);
  --bs-btn-border-color: var(--ok-tx);
  --bs-btn-hover-bg: var(--ok-tx);
  --bs-btn-hover-border-color: var(--ok-tx);
  --bs-btn-active-bg: #03573E;
  --bs-btn-active-border-color: #03573E;
}

/* 警告按钮：填充 --warn-tx（白字 6.80:1），hover 再深一档 #6E3C00（9.09:1） */
.btn-warning {
  --bs-btn-bg: var(--warn-tx);
  --bs-btn-border-color: var(--warn-tx);
  --bs-btn-hover-bg: #6E3C00;
  --bs-btn-hover-border-color: #6E3C00;
  --bs-btn-active-bg: #6E3C00;
  --bs-btn-active-border-color: #6E3C00;
}

/* 描边警告按钮：zephyr 是 #F4BD61 的字压在白底上（1.71:1，全站最差） */
.btn-outline-warning {
  --bs-btn-color: var(--warn-tx);
  --bs-btn-border-color: var(--warn-tx);
  --bs-btn-hover-bg: var(--warn-tx);
  --bs-btn-hover-border-color: var(--warn-tx);
  --bs-btn-hover-color: #fff;
  --bs-btn-active-bg: #6E3C00;
  --bs-btn-active-border-color: #6E3C00;
  --bs-btn-active-color: #fff;
}

/* ══════════════════════════════════════════════════════════════════
   全站统一禁用态 —— 一条规则盖住**所有** .btn 变体
   ══════════════════════════════════════════════════════════════════
   第 9 轮之前只有「描边次级 / 实心次级」两族改了禁用样式，其余族仍走 zephyr 的
   `opacity: var(--bs-btn-disabled-opacity)`（= .65）。实测那是个**双重失败**：
     ① 统一性：同页上「虚线灰按钮」和「半透明彩色按钮」两种禁用语言并存；
     ② 可达性：整元素 65% 透明会把文字和填充一起往底色上摊薄，几族全都掉出 AA——
        白底实测 .btn-primary 2.51:1、.btn-success 3.08:1、.btn-warning 3.14:1、
        .btn-outline-primary 2.89:1、.btn-outline-success 1.88:1、.btn-outline-warning 1.41:1。
        （要求是「改成不透明后仍要 ≥4.5:1，若原本就不达标请一并修好」——
         实测原本**就不达标**，所以这里不是「保持原有达标值」，是真把它修上去了。）

   统一后的禁用态（**所有**变体同一个长相，用户不需要记两套）：
     填充   --bg-sunken #F2F4F7（不透明，因此文字对比与页面底色无关）
     文字   --text-secondary #5A6472 → 对填充 **5.44:1**，全站禁用按钮一个数
     边框   --border-interactive + **虚线**（虚线是不挑底色的「不可用」信号）
     opacity 1（不再整体透明）

   权重 (0,2,0) > 各变体 (0,1,0) 的 --bs-btn-disabled-* 变量，
   所以各变体上不再需要逐个写禁用覆盖，全站一处维护。
   注意 zephyr 另有一条 `.btn-secondary.disabled,…{border:1px solid #e6e6e6}`（0,2,0），
   同权重靠本文件靠后加载压掉，所以 border-style:dashed 必须写在这里。

   ⚠️ 作用域收窄（第 11 轮）：原第三条是 `fieldset:disabled .btn`，等于把**禁用 fieldset
   里的每一个 .btn 都涂成禁用相**，包括 HTML 层面根本没被禁用的 `<a class="btn">`
   （fieldset 只禁用它内部的表单控件，**不禁用链接**）—— 那些元素照样能点，却被画成
   「不可用」，是在撒谎。收窄成只覆盖真正禁用/aria-disabled 的三条。
   为什么去掉 fieldset 那条不会漏：
     · fieldset 里的原生控件（button/input/select/textarea）会被 HTML 自动置为 :disabled，
       直接命中 `.btn:disabled`；
     · 模板里手写的禁用态本来就带 `.disabled` 类（project_detail.html 的两个 span），
       命中 `.btn.disabled`。
   「真正被禁用」的一个都没漏，被排除的只是「没被禁用却被画成禁用」的那类。
   顺带补上 `[aria-disabled="true"]`：焦点环那条早就认它（见上文「可聚焦的禁用元素」），
   禁用**外观**却没认，两处口径原来不一致；补上后 aria-disabled 这套写法与原生 disabled
   长相完全一致。
   （模板现状：`<fieldset>` 0 处；单独用 aria-disabled 而不带 .disabled 的 0 处 ——
     本次收窄不改变任何现有页面的渲染结果。） */
.btn.disabled,
.btn:disabled,
.btn[aria-disabled="true"] {
  --bs-btn-disabled-opacity: 1;
  --bs-btn-disabled-color: var(--text-secondary);
  --bs-btn-disabled-bg: var(--bg-sunken);
  --bs-btn-disabled-border-color: var(--border-interactive);
  border-style: dashed;
}

/* ══════════════════════════════════════════════════════════════════
   表单控件（输入框 / 下拉 / 复选框）
   ⚠️ 原本在 components.css，第 9 轮搬到这里 —— 独立页面（快速报价、供应商门户、
   团队确认、扫码认证页）**不加载 components.css**，所以那边一直拿 zephyr 的
   #dee2e6 默认边框（对白底 1.30:1）和蓝色复选框，跟主站是两套长相。
   ══════════════════════════════════════════════════════════════════

   ⚠️⚠️ 已接受的**有意设计变更**（第 11 轮明确记录，不是搬家时无意带出来的副产品）
   ──────────────────────────────────────────────────────────────────
   规则搬进 tokens.css 后，**未加载 components.css 的独立页**除了拿到一致的边框色，
   还额外拿到了这几个尺寸，等于顺带做了一次全站统一：
     · .form-label 字号     16px   → 13.6px（0.85rem）
     · .form-label 下间距   8px    → 4.8px（0.3rem）
     · .form-control 圆角   6px    → 8px（--r-lg）
     · .form-control-sm 圆角 4px   → 8px（--r-lg）
   依据：zephyr 的 `.form-label{margin-bottom:.5rem}` 不设字号，于是继承 body 的 16px；
   `.form-control{border-radius:var(--bs-border-radius)}` = 6px、`-sm` = 4px（真浏览器读
   变量确认：--bs-border-radius:0.375rem、--bs-border-radius-sm:0.25rem）。
   **用户已决定保留这套统一**（2026-10-08 确认）。理由：
     改前独立页上 label 是 16px、而它下面的 input 只有 14px —— 标签比正文框还大，自相矛盾；
     统一后独立页与后台页同一套尺寸，全站一种表单语言，不再两套。

   已知受影响的模板（**按模板文件枚举**——只按页面 URL 枚举会漏掉
   需要「身份 / 数据 / 交互」才出现的表单分支）：

   受影响（加载 tokens.css、不加载 components.css、且页面里有这几类控件）：
     1. templates/project_doc_portal.html
        「上传文件」卡片：.form-label ×2（"选择文件 *"、"文件说明（可选）"）字号 16→13.6、
        下间距 8→4.8；.form-control ×2（file 输入 + text 输入）圆角 6→8。
        ⚠️ 这段表单**默认不渲染**：未认证时整页只显示「需要身份认证 + 扫码」，
        必须登录或扫码通过后才出现。
     2. templates/team_confirm_portal.html
        「不同意」理由输入框：.form-control.form-control-sm ×1（textarea），圆角 4→8
        （字号不变，zephyr 的 -sm 本来就是 .875rem）。
        ⚠️ 要**同时**满足三个条件才可见：已认证为项目团队成员 且 该供应商是最优供应商
        且 选中「不同意」单选钮（markup 带 `style="display:none"`，靠 toggleReason() 打开）。
        只按 URL 枚举必然漏掉它。
     3. templates/quick_quote.html
        报价表单 .form-label ×5 + .form-control ×5（公司名称 / 不含税金额 / 税点 /
        报价说明 textarea / 报价文件 file）。
        ⚠️ markup 是页面内 JS 拼字符串**运行时注入**的，HTML 源码里搜不到；
        且「省事模式」与「对号模式」两套互斥分支任一时刻只渲染一套。
     4. templates/supplier_portal.html
        .form-control.form-control-sm ×5（补填邮箱、上传报价附件、填单价/金额、留言等），
        圆角 4→8。⚠️ 同样是 JS 在展开某一行/某个面板时才注入。
     5. templates/file_input_test.html
        开发用测试页（/test/file-input）：.form-control ×5（全是 file 输入），圆角 6→8，
        页面内无 .form-label。

   不受影响（逐类列出，避免「以为改了其实没改」）：
     · 加载了 components.css 的独立页 —— login.html / landing.html / quote_form.html /
       standalone_register.html / standalone_forgot_password.html /
       standalone_reset_password.html / super_login.html：规则搬家前后都在，视觉零变化。
     · 只加载 tokens.css、但页面里没有这几类控件 —— 413.html / login_scan_auth.html /
       evaluate_scan_auth.html / project_doc_scan_auth.html：无控件可影响。
     · 不加载 tokens.css —— report_pdf.html（打印用，自带样式表）。
     · 片段模板 _quote_history.html 自身不加载 CSS，是否受影响取决于引入它的页面。
     · 继承 base.html 的页面：两份 CSS 都加载，规则只是从 components.css 挪到 tokens.css，
       取值一字未改，且 components.css 已无任何 form-* 规则（grep 确认），无同属性竞争者
       → 视觉零变化。

   实测（真实 Flask + Chromium 148；脚本与原始 JSON 见本轮验收记录）：
     · 变更只作用于这几条规则命中的控件，不是全局字号改动 —— 同页未挂 .form-label 的
       `<label>`（.form-check-label）实测仍是 zephyr 回退值 **16px**，挂了 .form-label 的是
       **13.6px / 下间距 4.8px**。
     · .form-control 计算值：圆角 8px、高度 38.39px（text/number/file 一致）；
       .form-control-sm 圆角 8px、高度 31px。
     · team_confirm_portal 那条默认隐藏的分支：点「不同意」后 textarea 实测
       可见（1084 × 50.78px）、无裁切；隐藏时 height 0（display:none，符合预期）。
     · 受影响的 5 个模板在 390px 与 1280px 两个宽度下：横向溢出元素 0 个、
       documentElement.scrollWidth == 视口宽度、无裁切。
       最小触控目标是 .form-control-sm 的 31px（高），已高于 WCAG 2.5.8
       Target Size (Minimum)（AA）的 24×24 要求。
   ══════════════════════════════════════════════════════════════════ */

/* 输入框 / 下拉：默认边框改可访问值——zephyr 的 #dee2e6 对白底只有 1.28:1，
   不满足 WCAG 1.4.11 Non-text Contrast（AA，UI 边界 ≥3:1）；
   --border-interactive 取值与实测见文件开头。
   尺寸与后台页对齐（zephyr 默认 .875rem 字号 / 6px 圆角）。 */
.form-control, .form-select {
  font-size: 0.85rem;
  border-radius: var(--r-lg);
  border-color: var(--border-interactive);
}
.form-label { font-size: 0.85rem; font-weight: 500; margin-bottom: 0.3rem; }

/* 聚焦态：边框与光圈都用品牌色（rgba 取自 --brand-600 #667D08） */
.form-control:focus, .form-select:focus {
  border-color: var(--brand-600);
  box-shadow: 0 0 0 0.2rem rgba(102, 125, 8, 0.18);
}

/* 未选中的复选框：zephyr 读 --bs-border-color #dee2e6，真浏览器实测对白底 **1.3022:1**，
   远不到 1.4.11 的 3:1 —— 白底上等于只有一个隐约的小方块。
   （.form-control 那条只管输入框，管不到 .form-check-input，所以必须单写一条。） */
.form-check-input { border-color: var(--border-interactive); }

/* 选中态：原本写在 base.html 的内联样式里（`.form-check-input:checked{...var(--green)}`），
   只有继承 base.html 的页面吃得到；独立页面上仍是 zephyr 的蓝 #3459e6，
   于是同一套系统里出现绿色和蓝色两种勾选框。搬到这里，全站一次统一。
   勾是白色 SVG（zephyr 自带），压在 --brand-600 上 4.66:1，图形元素 ≥3:1 达标。

   半选态（:indeterminate）与选中态同色：zephyr 单独给了一条
   `.form-check-input[type=checkbox]:indeterminate{background-color:#3459e6;border-color:#3459e6}`，
   所以只写 `:checked` 覆盖不到它，会出现「全选/部分选中/未选」三态里中间那个还是蓝色。
   权重核对（第 11 轮更正）：那条是 **(0,3,0)** —— 类 `.form-check-input` + 属性
   `[type=checkbox]` + 伪类 `:indeterminate`，三个选择器各计一次，**没有元素选择器**
   （不是原注释写的 (0,2,1)）。而 `.form-check-input:checked` 只有 (0,2,0)，
   确实压不过它。所以要覆盖就必须**同权重**：本条的
   `.form-check-input[type="checkbox"]:indeterminate` 同样是 (0,3,0)，
   同权重 + 本文件靠后加载 → 胜出。（值本身与前一条相同，写法统一只是为了让权重对上。）
   全站唯一使用点：templates/index.html 的批量选择全选框
   （`selectAll.indeterminate = 部分选中`，对应 markup 在 index.html:267）。
   半选的横杠同样是白色 SVG（stroke='%23fff'），压在 --brand-600 上 4.66:1。 */
.form-check-input:checked,
.form-check-input[type="checkbox"]:indeterminate {
  background-color: var(--brand-600);
  border-color: var(--brand-600);
}

/* 复选框聚焦态：zephyr 默认是蓝色光圈 rgba(52,89,230,.25)，跟上面刚统一的绿色勾不同源。
   与 .form-control:focus 用同一个品牌光圈。 */
.form-check-input:focus {
  border-color: var(--brand-600);
  box-shadow: 0 0 0 0.2rem rgba(102, 125, 8, 0.18);
}

/* 辅助文字：Bootstrap 的 .text-muted = rgba(73,80,87,.75)，对白底 4.23:1，
   对 .table-success 的 #D5F0E6 只有 3.78:1、对 .table-warning 的 #FDF2DF 只有 3.98:1，
   小字号均不达 AA 4.5:1。改用 --text-secondary：白底 6.00:1 / 绿底 4.98:1 / 黄底 5.41:1。
   类名带 txt- 前缀，与 Bootstrap 的 .text-secondary 区分——后者历史上是白字（见下一条）。 */
.txt-secondary { color: var(--text-secondary); }

/* Bootstrap 的 .text-secondary = rgba(var(--bs-secondary-rgb))，zephyr 下就是纯白：
   白字压白底，实测 1.05:1，等于隐形。模板里当前 0 处引用（历史代码统一走上面的 .txt-secondary），
   但留着一个「谁顺手写一次就看不见」的坑不划算，一并收掉，取值与 .txt-secondary 等价。
   它是 !important 工具类，覆盖必须跟着带 !important。至此 .text-secondary /
   .txt-secondary / .text-body-secondary 三者同色。 */
.text-secondary { color: var(--text-secondary) !important; }

/* 全站 .text-muted 收口（约 200 处）。zephyr 里 .text-muted 就是 var(--bs-secondary-color)，
   上面那条 :root 覆盖已经能让它变深；这里再写一条是防止将来重新 vendor 上游 Bootstrap——
   5.3.3 的 .text-muted 改走 --bs-secondary-color-rgb / -opacity，届时只改变量会静默失效。
   带 !important 是因为 Bootstrap 的 .text-* 工具类自身带 !important，同权重必须跟。
   ⚠️ 本规则只改颜色深浅，不适用于元素自己另挂 opacity 的地方——那种叠乘后仍可能不达标。 */
.text-muted { color: var(--text-secondary) !important; }
