/*
  apps/qjqt/public/style.css —— 秦九·供应商的样式（零依赖：不用任何外链字体 / CDN / 预处理器）
  ============================================================================
  ★ 单一文件、原生 CSS：卡面 §1 要求零依赖、无构建。
  ★ 不引 `@import`、不引外部 URL（`S-3`：三个系统的前端不互相 import，也不引第三方资源）。
  ★ 基调与采购中台（`apps/qjzt/public/style.css`）**同一套观感**（同一个项目的两台），
    但**代码是各自独立的一份**（两台互不 require、互不 import）。
*/

:root {
  --bg: #f4f6f8;
  --fg: #1f2733;
  --muted: #6b7785;
  --line: #d8dee6;
  --card: #ffffff;
  --brand: #1f6feb;
  --brand-fg: #ffffff;
  --error: #b3261e;
  --ok: #1a7f37;
  --sel: #fff8e1;
}

* { box-sizing: border-box; }

/*
  ★★ 产品口径（`M-PROC-21`，2026-10-13）：「把前端这个页面**锁定一个容器**，**自适应屏幕分辨率** ⋯
     超出屏幕分辨率之外的内容，**页面内容器直接滚动条**就行了」（例：2560×1440）。
  ⇒ **一屏锁定**：`html`/`body` 撑满一屏且**不许出页面级滚动条**；`#qjqt-app` 的高度 = 一屏；
    滚动的职责下放到**栏内**（`.cols3 > section { overflow-y: auto }`，见那一节）
    与**表内**（`.table-wrap { overflow-x: auto }`）。
  ★ 窄屏（`≤1000px`）三栏塌成一列时**不能**让三栏平分一屏 ⇒ 那一条里把"栏内滚动"复位，
    改由 `main` 纵向滚动（见下面的 `@media`）。
*/
html, body { height: 100%; }

body {
  margin: 0;
  overflow: hidden;              /* ★ 页面本身不滚 —— 滚动条只出现在**容器里** */
  background: var(--bg);
  color: var(--fg);
  font: 14px/1.55 -apple-system, "Segoe UI", "Microsoft YaHei", "PingFang SC", sans-serif;
}

h1 { font-size: 20px; margin: 0 0 8px; }
h2 { font-size: 16px; margin: 0 0 8px; }

.muted { color: var(--muted); }
.small { font-size: 12px; }
.error { color: var(--error); }
.num { text-align: right; font-variant-numeric: tabular-nums; }

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

/* ★ 窄屏（600px）不许出横向滚动条：卡片 `max-width` 是上限、宽度随视口收缩；
   `* { box-sizing: border-box }` 让 100% 宽的输入框不会撑破父元素。 */
.login-card {
  max-width: 420px;
  margin: 8vh auto;
  padding: 24px;
}

.card {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 8px;
}

form { display: flex; flex-direction: column; gap: 10px; margin: 0 0 12px; }

label { display: block; color: var(--muted); font-size: 13px; }

input[type="text"],
input[type="number"],
input[type="password"] {
  display: block;
  width: 100%;
  margin-top: 4px;
  padding: 8px 10px;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: #fff;
  color: var(--fg);
  font: inherit;
}

button {
  padding: 8px 14px;
  border: 1px solid var(--brand);
  border-radius: 6px;
  background: var(--brand);
  color: var(--brand-fg);
  font: inherit;
  cursor: pointer;
}

button:hover { filter: brightness(1.06); }
button.ghost { background: #fff; color: var(--brand); }
button.ghost:hover { background: #eef4ff; }

/* ---------------------------------------------------------------- 顶栏 */

.topbar {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 10px 18px;
  background: var(--card);
  border-bottom: 1px solid var(--line);
}

.topbar .brand { font-weight: 600; }
.topbar .spacer { flex: 1; }

/*
  ★★ `M-PROC-25`（产品口径 2026-10-13）：「**秦九·供应商 放到工作台的上面**」⇒
     页面名从顶栏搬到**左栏一级导航的顶部**（`<aside class="sidegroups">` 的第一个孩子），
     于是它显示在「工作台」那一项**之上**。
  · 竖排（桌面）：块级 + 下边框，像药事管理壳那种"品牌头 + 导航项"的分层；
  · 横排（`≤1000px`，`M-PROC-13` 把导航塌成一行）：它是那一行的**第一个**元素，
    `flex: 0 0 auto` 保证不被挤扁（此时"上面"自然变成"左面"）。
*/
.sidegroups .brand {
  display: block;
  flex: 0 0 auto;
  padding: 2px 4px 8px;
  margin-bottom: 6px;
  border-bottom: 1px solid var(--line);
  font-weight: 600;
  white-space: nowrap;
}

/* ---------------------------------------------------------------- 一级导航（竖向 · 左栏） */

/*
  ★★ `M-PROC-13`（产品口径 2026-10-13）：「前端增加一个一级导航，供应商工作台 · 把目前的
     需求列表跟我的报价都放到这个里面来。」⇒ 前台新增**竖向一级导航**（只有一项「供应商工作台」，
     默认选中），两个视图都在这一项下面**同屏**显示。
  ★ 观感照**药事管理壳**那套一级导航（`server/qjxs/src/web/proc.css` 的 `.sidegroups` / `.group`：
     左栏 + 卡片底 + 选中态 + 窄屏塌成横排）—— 让两个系统看起来像一个产品。
  ★ 本文件仍是**自己独立的一份**（`S-3`：两台前台的前端**不互相 import**）—— 只是"同款观感"，
     不是共用代码。
  ★ 为什么用 `:not([hidden])`：`#qjqt-app` 靠 `hidden` 做登录门（未登录时整个主界面不显示），
     `[hidden]` 的 `display:none` 必须继续赢 ⇒ 网格只在**已登录**时生效。
  ★★ `M-PROC-21`（一屏锁定）：高度钉成 **一屏**（`height: 100vh`），
     第二行用 **`minmax(0, 1fr)`**（不是 `1fr`）—— 子项内容再高也不会把这一行顶出去；
     `align-items` 用默认的 `stretch`（左栏自己写了 `align-self: start`，不受影响），
     让 `main` 真的占满第二行、从而能在**内部**滚动。
*/
#qjqt-app:not([hidden]) {
  display: grid;
  grid-template-columns: 180px minmax(0, 1fr);
  grid-template-rows: auto minmax(0, 1fr);
  height: 100vh;
}

#qjqt-app:not([hidden]) > .sidegroups { grid-column: 1; grid-row: 1 / span 2; }
#qjqt-app:not([hidden]) > .topbar { grid-column: 2; grid-row: 1; }
#qjqt-app:not([hidden]) > main {
  grid-column: 2;
  grid-row: 2;
  min-height: 0;                 /* ★ grid 子项的地板：不写它，`1fr` 会被内容顶高 ⇒ 页面又出现滚动条 */
  overflow: hidden;              /* ★ 桌面档的滚动发生在**栏内**（见 `.cols3 > section`） */
}

.sidegroups {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 12px 10px;
  background: var(--card);
  border-right: 1px solid var(--line);
  align-self: start;
  position: sticky;
  top: 0;
  z-index: 4;
}

/* 一级按钮（`class="group"`；口径照药事管理壳的 `.group`：同圆角 / 同字号 / 同一套选中色） */
.group {
  text-align: left;
  background: none;
  border: 1px solid transparent;
  border-radius: 8px;
  padding: 8px 12px;
  font-size: 14px;
  color: var(--fg);
  cursor: pointer;
}

.group:hover { background: #eef1f4; }
.group.on { background: var(--brand); color: var(--brand-fg); }

main { padding: 18px; }

/*
  ★ 窄屏（`≤1000px`，与药事管理壳同一条断点）：左栏**塌成横排**，且不许出横向滚动条。
  ★ 三条 `grid-column/row: auto` 必须显式复位 —— 否则塌成一列之后上面那句仍把元素钉在"第 2 列"上，
    直接溢出到屏幕外（壳的 `proc.css` 记的是同一笔学费）。
  ★★ `M-PROC-21`：这一档里三栏**塌成一列**，不能再让每栏平分一屏（那会把内容压成三小条）
     ⇒ 把"栏内滚动"**复位**（`.cols3 { height: auto }` / 栏位 `max-height: none; overflow: visible`），
     改由 `main` **纵向滚动** —— 页面本身仍然不滚，滚动条还是在容器里。
*/
@media (max-width: 1000px) {
  #qjqt-app:not([hidden]) {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto auto minmax(0, 1fr);
  }
  #qjqt-app:not([hidden]) > .sidegroups { grid-column: auto; grid-row: auto; }
  #qjqt-app:not([hidden]) > .topbar { grid-column: auto; grid-row: auto; }
  #qjqt-app:not([hidden]) > main { grid-column: auto; grid-row: auto; overflow-y: auto; }
  .cols3 { height: auto; }
  .cols3 > section { max-height: none; overflow: visible; }
  .sidegroups {
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px;
    padding: 6px 10px;
    border-right: none;
    border-bottom: 1px solid var(--line);
    position: static;
    top: auto;
    overflow-x: auto;
  }
  /* ★ `M-PROC-25`：横排时品牌头不再需要"下面一条分割线 + 底边距"（它是行内第一个元素） */
  .sidegroups .brand {
    padding: 0 8px 0 4px;
    margin-bottom: 0;
    border-bottom: none;
    border-right: 1px solid var(--line);
  }
  /* ★★ `M-PROC-28`：中栏那条"顶部固定 + 表头固定 + 只有数据滚"在这一档**必须复位** ——
     窄屏三栏塌成一列、整页（`main`）纵向滚，再把中栏自己钉住会让表头盖住上面两块。 */
  #qjqt-quote { display: block; overflow: visible; }
  #qjqt-quote > .table-wrap { flex: 0 1 auto; min-height: auto; overflow-x: auto; overflow-y: visible; }
  #qjqt-batch-table thead th { position: static; }
  /* ★ `M-PROC-30`：这一档分页条不再 sticky（栏位 `overflow: visible`）⇒ 那条"抵消下内边距"的
     负外边距**必须复位**，否则它会盖住下面一栏的开头。 */
  .pager { margin-bottom: 0; padding-bottom: 4px; }
  /* ★★ `M-PROC-36` 追加：桌面档把栏位的**下内边距挪进了分页条自己**（见 `.pager` 那一段）——
     这一档分页条不再 sticky、栏位也不滚 ⇒ 那条内边距**必须还回栏位**，否则每栏内容会贴到边框上。 */
  #qjqt-view-demands, #qjqt-view-mine { padding-bottom: 12px; }
}

.row { display: flex; flex-wrap: wrap; gap: 12px; align-items: flex-end; }
.row > label { min-width: 150px; }
.row .check { display: flex; align-items: center; gap: 6px; }
.row .check input { width: auto; margin: 0; }

.filters { margin-bottom: 10px; }

/*
  ★★ `M-PROC-36` 追加：这几行（需求表头 / 搜索筛选 / 批量报价 / 我的报价）都是 `.row.filters`，
     而它们各自带着一个「?」提示点。**把定位父级放在"行"上**（而不是那个 16px 宽的 `.hint` 上）
     ⇒ 浮层里的 `width: min(420px, 100%)` 里的 `100%` = **这一行的宽度** ⇒
     浮层永远不可能比它所在那一行还宽，也就**永远不会造出横向滚动条**。
     ★ 为什么必须这么做：`visibility: hidden` 的浮层**照样占布局**（实测一个就把栏位撑宽 296px），
       而它超出栏位右边缘 ⇒ 栏位长出一条占满整栏的横向滚动条，还把底部的 sticky 分页条顶在上面。
*/
.row.filters { position: relative; }

/*
  ★★ 左栏**表头一行**（产品口径 2026-10-13：「**需求、窗口（天）、刷新** 这些 —— 一行显示出来不就行了」）：
     标题 + 窗口（天）+ 刷新 合成一行，省掉一整行高度。
     · `h2` 去掉默认的下外边距、`label` 不再吃 `.row > label` 的 `min-width: 150px`（那会把这一行撑开）；
     · 数字框覆盖全局的 `display:block; width:100%; margin-top:4px` ⇒ 行内 88px；
     · 窄屏不用管：`.row` 本来就是 `flex-wrap: wrap`，放不下时自己换行。
*/
.demand-head {
  align-items: center;
  gap: 10px;
  margin-bottom: 8px;
}

.demand-head h2 { margin: 0; }

.demand-head > label {
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 6px;
}

.demand-head > label input {
  display: inline-block;
  width: 88px;
  margin-top: 0;
}

/* ★★ `M-PROC-23`：表头那一行里的「数据截至 …」**靠右站**（`margin-left: auto`），
   ★ 窄屏不用管：`.row` 是 `flex-wrap: wrap`，放不下时它自己换到下一行。 */
.demand-head .computed { margin-left: auto; }

/* ---------------------------------------------------------------- ★★ 三态展示（加载中 / 无数据 / 失败） */

/*
  ★★ `M-PROC-15` ③（产品口径 2026-10-13）：三种状态各一句人话，**失败态**给可重试入口。
  ★ 三态都在**已登录**的主界面里 —— "未登录"是另一个屏（`#qjqt-login`），两者不共用文案。
*/
.state {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin: 6px 0 10px;
  padding: 8px 10px;
  border: 1px solid var(--line);
  border-left-width: 4px;
  border-radius: 6px;
  background: #f8fafc;
}

.state-loading { border-left-color: var(--brand); }
.state-empty { border-left-color: var(--muted); }
.state-error { border-left-color: var(--error); background: #fdf3f2; }
.state-error #qjqt-demand-state-text { color: var(--error); }

/*
  ★★ `M-PROC-36`（产品口径 2026-10-14）—— 这一条是**修一个一直没人发现的错**：

  供应商看到的"需求下面那条没用的横条"就是**这条三态提示条**。它 HTML 上写着 `hidden`
  （`#qjqt-demand-state`），但 `.state` 自己声明了 `display: flex`（**作者**样式表），
  而作者样式表的 `display` 会盖掉 **UA** 样式表的 `[hidden] { display: none }`
  ⇒ 那条**空的三态条**在"有数据、什么都不用说"的时候照样画出来：
  一条 18px 高、浅灰底、左边 4px 竖线的空带，正好夹在「需求 / 窗口（天） / 刷新」与表头
  「品名 规格 等级」之间 —— 页面上它不解释任何事，谁也看不出它是干什么的。

  ★ 这不是新坑：本仓在 `.pager[hidden]`（`M-PROC-16`）与 `.modal[hidden]`（`M-PROC-18`）
    上**各踩过一次**，两次的注释都写着同一句话。三态条那次是漏了（判据只查了 HTML 上的
    `hidden` 属性，没查 CSS）⇒ `proc-qjqt-pages` §3k 现在把这条 CSS 作为**判据**钉住
    （拿掉它就必须报红）。
  ★ 真 Chrome 实测（改之前，`_tmp/state-probe.html` + `--dump-dom`）：
    `hiddenAttr=true` 而 `computedDisplay="flex"`、`renderedHeight=18`、`borderLeftWidth="4px"`
    —— 属性确实在、元素确实画出来了。
*/
.state[hidden] { display: none; }

/* ---------------------------------------------------------------- ★★ `M-PROC-36`：加载中的小转圈（不占版面） */

/*
  ★★ 产品口径（2026-10-14，原话）：「我每次切换天数 或者筛选的时候会自动刷新需求 ⋯ 就会导致加载
     然后那个进度条就出来了，**导致了页面动了** ⋯ 你看能做到刷新后面转个圈就行了，**不要挤压页面**」。
  ⇒ 加载中不再用上面那条提示条（它占一整行、把需求表整张往下推），改成**表头那一行**里的
     一个 14px 小圈（紧跟在「刷新」按钮右边）：那一行本来就有 34px 高的按钮 / 输入框
     ⇒ **行高一个字都不变** ⇒ 换天数 / 换筛选时表格一动不动。
  ★ 动画只用 `transform: rotate()`（**不触发重排**）—— 转再久也不会挤到任何东西；
    `prefers-reduced-motion` 下转慢一点（信息还在，不晃眼）。
  ★★ `.spinner[hidden] { display: none }` **必须写**：这里给 `.spinner` 声明了 `display: inline-block`，
     而**作者样式表会盖掉 UA 的 `[hidden] { display: none }`** ⇒ 少了这一条，"不转的时候把圈收起来"
     会**静默失效**（圈一直在那儿转）。本仓在 `.state[hidden]`（`M-PROC-36`）· `.pager[hidden]`
     （`M-PROC-16`）· `.modal[hidden]`（`M-PROC-18`）上已经踩过**三次**同款
     ⇒ 判据 `proc-qjqt-pages` §3l 把这条也钉住（拿掉就必须报红）。
*/
.spinner {
  display: inline-block;
  width: 14px;
  height: 14px;
  border: 2px solid var(--line);
  border-top-color: var(--brand);
  border-radius: 50%;
  animation: qjqt-spin 0.7s linear infinite;
}

.spinner[hidden] { display: none; }

@keyframes qjqt-spin {
  to { transform: rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
  .spinner { animation-duration: 2.4s; }
}

/* ---------------------------------------------------------------- ★★ `M-PROC-36`：需求栏的搜索 + 筛选 */

/*
  ★★ 产品口径（2026-10-14）：「需求的下面……**加成筛选框、搜索框**」。
  ⇒ 左栏在**表头那一行**（需求 / 窗口（天） / 刷新 / 数据截至）之下、**需求表**之上，加一条横排的
    工具条：搜索框（`keyword`）+「我报过没」（`quoted`）+「需求量档位」（`level`）+「清空筛选」。
  ★ 它顶的正是上面那条"空的三态条"的位置 —— 观感刻意保持一致（浅底 `#f8fafc` + 细边 + 圆角 6），
    于是供应商看到的是"原来那个条变成了能用的东西"，而不是"页面又长出一块"。
  ★ 窄屏不用管：`.row` 本来就是 `flex-wrap: wrap`（与左栏表头那一行同一套做法）。
  ★★ 输入框与下拉框在这里**自带一份样式**，不去动全局那几条：
    全局只覆盖 `input[type=text|number|password]`（没有 `search`），`select` 更是**一处都没有**
     —— 直接复用全局会让这个搜索框变成没有任何样式的浏览器默认控件（那就不是"能用的东西"了）。
*/
.demand-search {
  align-items: center;
  gap: 10px;
  margin-bottom: 8px;
  padding: 7px 10px;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: #f8fafc;
}

.demand-search > label {
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  color: var(--muted);
  white-space: nowrap;
}

.demand-search input[type="search"],
.demand-search select {
  display: inline-block;
  width: auto;
  margin-top: 0;
  padding: 6px 8px;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: #fff;
  color: var(--fg);
  font: inherit;
  font-size: 13px;
}

/* ★ 搜索框给足宽度（品名 / 规格 / 等级 都可能粘进来），但不是 100%（那会把这一行挤散） */
.demand-search .search-label input { width: 180px; }

/* ★ 「?」气泡靠右站、与「清空筛选」分开（气泡本身复用 `.hint` 那一套，见下） */
.demand-search .hint { margin-left: auto; }

/* ★★ `M-PROC-36` 追加：这一行的「?」在最右端 ⇒ 浮层**朝左展开**（右边缘贴这一行的右边缘）
   —— 若仍用 `left: 0`，420px 的浮层会向右捅出栏位（实测越界 295px ⇒ 栏位多出一条横向滚动条）。 */
.demand-search .hint-bubble { left: auto; right: 0; }

/* ---------------------------------------------------------------- ★★ 竖向三栏（`M-PROC-15`） */

/*
  ★★ 产品口径（2026-10-13）：「界面升级 · **竖向三栏结构** · **左栏是需求**」
     · 第 1 栏 = 需求列表（`#qjqt-view-demands`）
     · 第 2 栏 = 批量报价（`#qjqt-quote`）
     · 第 3 栏 = 我的报价轨迹（`#qjqt-view-mine`）
     ★ 三栏的**顺序**就是 DOM 顺序（`index.html` 里 `.cols3` 的三个 `<section>`）——
       窄屏塌成一列时读起来仍然是"左→中→右"，不靠 `order` 之类的重排魔法。
  ★★ `M-PROC-21`（一屏锁定）：桌面档里 `.cols3` 占满 `main` 的高度，**每栏自己纵向滚动**
     （`max-height: 100%` + `overflow-y: auto`）⇒ 页面永远不滚，滚动条只出现在栏里。
     ★ 栏位仍用 `align-items: start`（内容短时贴顶、不拉伸），所以是"max-height 封顶 + 内部滚动"，
       而不是"每栏都撑成一屏高"。

  ★★ 为什么每栏都写 `min-width: 0`（不写就是一次可复现的横向溢出）：
     grid 子项的默认 `min-width` 是 `auto` ⇒ 子项**不会**小于它内容的 min-content 宽
     （表格里有 `white-space: nowrap`，min-content 可以宽到几百像素）⇒ 600px 视口下
     三栏的 min-content 之和会把文档撑出横向滚动条。`minmax(0, 1fr)` + `min-width: 0`
     把"轨道"和"子项"两级的地板都拆掉，滚动条就只剩 `.table-wrap` 内部那一条（这是有意的）。
*/
.cols3 {
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr) minmax(0, 1.15fr);
  gap: 14px;
  align-items: start;
  height: 100%;                  /* ★ 一屏锁定：占满 main 的高度 */
  min-height: 0;
}

/* 三栏同款外观（★ 用子选择器而不是给 `<section>` 加 class：`proc-qjqt-pages` 有两条断言
   把两个视图容器钉在**逐字** `<section id="…" class="view">` 这个形状上） */
.cols3 > section {
  min-width: 0;
  max-height: 100%;              /* ★ 栏位封顶到一屏高 */
  overflow-y: auto;              /* ★ 超出就在**这一栏里**滚 */
  padding: 12px 14px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 8px;
}

/* 栏内表格不再套一层"卡片框"（外层已经是卡片），但**保留** `overflow-x: auto` */
.cols3 > section .table-wrap {
  border: 0;
  border-radius: 0;
  background: transparent;
}

/* 中栏的报价区：★ `M-PROC-18` 之后中栏只剩批量报价（粘贴区 + 三个按钮 + 预览表），
   原来那条给"价格 / 备注"两个 `<label>` 用的最小宽度规则**已随表单一起删除**。 */

/*
  ★ 窄屏（`≤1000px`，与壳同一条断点）：三栏塌成**单列上下排**，且不许出横向滚动条。
  ★ 与一级导航那条 `@media` 是**同一条断点**（两个都 ≤1000px）⇒ 不会出现"导航已横排、
    三栏还并排"这种一半一半的状态。
*/
@media (max-width: 1000px) {
  .cols3 { grid-template-columns: minmax(0, 1fr); }
}

/* ---------------------------------------------------------------- ★★ 需求列表分页（`M-PROC-16`） */

/*
  ★★ `M-PROC-36` 追加：**有分页器的那两栏**（左＝需求 / 右＝我的报价）把**下内边距挪进分页条自己**
     （`.pager` 的 `padding-bottom: 12px`）—— 这样 sticky 分页条的**背景**能一直铺到栏底的边框上，
     滚动中的表格行**没有地方可露**（原来栏位那 12px 下内边距就是那条"露数据"的缝）。
     ★ 中栏（`#qjqt-quote`）没有分页器 ⇒ 它的下内边距照旧，**不动**。
     ★ 窄屏那一档（分页条不再 sticky）在 `@media (max-width: 1000px)` 里**还回 12px**。
     ★ 真 Chrome 实测（1600×1000 / 左栏 450px / 40 行真数据，滚到中间）：
       改之前缝 **13px**、缝里最上层是 **`TD`（数据行）**；改之后缝 **1px**、最上层全是 **`DIV.pager`**。
*/
#qjqt-view-demands, #qjqt-view-mine { padding-bottom: 0; }

/*
  ★★ `M-PROC-16` ①：左栏需求列表底部的分页条 ——「上一页」「共 N 条 · 第 x / y 页」「下一页」。
  ★ 为什么**必须**单写一条 `.pager[hidden] { display: none }`（这是本仓踩过的那类坑）：
     `.pager` 自己声明了 `display: flex`，而作者样式表里的 `display` **会盖掉** UA 样式表里
     `[hidden] { display: none }` ⇒ 只写 `.pager { display: flex }` 的话，`#qjqt-page` 上的
     `hidden` 属性**在但不起作用**（"隐藏分页条"静默失效，失败态照样摆着一条页码）。
     ★ 断点与三栏/一级导航一致（`≤1000px` 已经塌成单列，这一条靠 `flex-wrap` 自己换行，
       1000–1200px 区间量过一次（见 README §7②）：分页条不溢出、不出横向滚动条）。
*/
.pager {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  /*
    ★★ `M-PROC-23`（产品口径 2026-10-13）：「底部翻页器 ⋯ **要在底部固定显示出来**。现在是滚动才能
       显示」⇒ 它从"内容末尾的一条"变成**钉在栏底的一条**：
       `position: sticky; bottom: 0`（左栏自己会滚动，见 `.cols3 > section { overflow-y: auto }`）
       + **不透明底**（否则下面的表格行会从它背后透出来）+ 一条上边框把它和内容分开。
    ★ 为什么不用 `position: fixed`：那会脱离文档流、盖住右栏/中栏（三栏并排）。
    ★ 它必须是**滚动容器里的最后一个元素**（说明段已挪到它上面）—— 否则后面的兄弟会压住它。
  */
  position: sticky;
  bottom: 0;
  z-index: 2;
  /*
    ★★ `M-PROC-30`（产品口径 2026-10-13）：「这一行的 **再往下去一点点**，现在露了一个小缝，
       导致后面的数据会露出来，影响美观」⇒ 那条缝是**栏位的下内边距**（`.cols3 > section` 的
       `padding: 12px 14px`）造成的：sticky 的活动范围被限制在**内容盒**里 ⇒ 分页条只能停在
       离栏底 12px 处，而**滚动中的表格行会画进这 12px 的 padding 区**（滚动容器的可见区 = padding 盒）
       ⇒ 分页条下面露出一条 12px 的数据缝。
    ⇒ `M-PROC-30` 当时用的是 `margin-bottom: -12px` 把分页条的盒子探进那 12px。
    ★★ `M-PROC-36` 追加（产品口径 2026-10-14：「这一段能不能往下再移动一点，**还是没有盖住下面的**」）：
       实测那条负外边距**没有达到目的** —— 真 Chrome（1600×1000 / 左栏 450px、40 行真数据）：
         · 缝 = **13px**（12px 栏位下内边距 + 1px 边框），且**滚到中间时那一条里最上层是 `TD`**
           （= 表格数据行真的从分页条下面露出来）；把 `margin-bottom` 试到 `-24px` / `-40px`
           量到的缝**一样是 13px** ⇒ 负外边距在 sticky 约束下不起作用（判据见 §3m）。
       ⇒ 换成正解：**把栏位那条下内边距挪进分页条自己**（`#qjqt-view-demands, #qjqt-view-mine
         { padding-bottom: 0 }` + 这里的 `padding-bottom: 12px`）—— 分页条的**背景**从此一直铺到
         栏底的边框上，滚动中的行**没有地方可露**。
       ★ 为什么不是 `bottom: -12px`：那会让分页条自己被**裁掉** 12px（滚动裁剪在 padding 盒边界），
         底部的「跳转」按钮会被切一半。
       ★ 窄屏那一档（`≤1000px`，分页条不再 sticky）**必须把内边距还回栏位**，见上面那条 `@media`。
    实测（候选写法）：缝 **1px**（只剩栏底那 1px 边框），栏底 3~13px 那一条里最上层**全是 `DIV.pager`**。
  */
  margin: 10px 0 0;
  padding: 8px 0 12px;
  border-top: 1px solid var(--line);
  background: var(--card);
}

.pager[hidden] { display: none; }

/* ★★ `M-PROC-23`：跳页（输入页码 + 跳转按钮；回车同效） */
.pager .jump {
  display: flex;
  align-items: center;
  gap: 6px;
  color: var(--muted);
  font-size: 12px;
  white-space: nowrap;
}

.pager .jump input {
  display: inline-block;
  width: 64px;
  margin-top: 0;
  padding: 4px 6px;
}

/* 禁用的翻页按钮：**看得见、点不动**（卡面冻结口径：用 disabled，不隐藏）
   ★ 不给 `filter: brightness()` 那条 hover 效果，否则鼠标扫过时会看起来"可点"。 */
.pager button[disabled] {
  opacity: 0.45;
  cursor: not-allowed;
}

.pager button[disabled]:hover { filter: none; }

#qjqt-page-info { white-space: nowrap; }

/* ---------------------------------------------------------------- ★★ 需求量分级条（1–10 级） */

/*
  ★★ `M-PROC-15`：需求量**只给等级**（1–10 的整数），展示成 **10 格条 + 「N 级」**
     （形如 `████████░░ 8 级`）。
  ★ 格子是 `<i>` 元素，靠 `lvl-on` / `lvl-off` 两个类名上色 —— **元素上不带任何数字**，
    于是 `title` / `data-*` / 文本里都不可能漏出绝对量（那是卡面 §1② 的硬要求）。
*/
.lvl-cell { white-space: nowrap; }

.lvl-track {
  display: inline-flex;
  gap: 2px;
  vertical-align: middle;
  margin-right: 6px;
}

.lvl-track > i {
  display: inline-block;
  width: 8px;
  height: 12px;
  border-radius: 2px;
  background: #e3e8ef;
}

.lvl-track > i.lvl-on { background: var(--brand); }

.lvl-text {
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* 左栏那几行可以点（点行 = 选中它 ⇒ 中栏与右栏一起切过去） */
tr.demand-row { cursor: pointer; }
tr.demand-row:hover td { background: #f2f6fb; }

/* ---------------------------------------------------------------- ★★ 右栏：按「品名+规格」归并的报价组 */

/*
  ★★ `M-PROC-15` ②（产品口径 2026-10-13）：一个商品（品名｜规格）= 一个 `<tbody class="offer-group">`；
     组头给「我报过几次 · 最新多少」，**旧价默认折叠**（`is-expanded` 才显示 `old-row`）。
  ★ 为什么折叠用 `display: none` 而不是 `hidden` 属性：`<tr>` 的 `display` 是 `table-row`，
     `[hidden]` 在部分浏览器里对表格行不生效（本仓的表格样式也没有 `[hidden]` 规则）
     ⇒ 显式写 `.old-row { display: none }` + `.is-expanded .old-row { display: table-row }`。
*/
tbody.offer-group > tr > td { border-top: 2px solid var(--line); }

tbody.offer-group .group-head > td {
  background: #f5f7fa;
  white-space: normal;
}

.group-name { font-weight: 600; margin-right: 8px; }
.group-meta { color: var(--muted); font-size: 12px; }

tbody.offer-group.is-active .group-head > td { background: var(--sel); }

tbody.offer-group .old-row { display: none; }
tbody.offer-group.is-expanded .old-row { display: table-row; }

button.tiny {
  margin-left: 8px;
  padding: 2px 8px;
  font-size: 12px;
  border-radius: 4px;
}

/* ---------------------------------------------------------------- ★★ 批量粘贴报价（`M-PROC-17`；★ `M-PROC-18` 之后是中栏唯一的内容） */

/*
  ★★ `M-PROC-17`（产品口径 2026-10-13）：「报价那里，能不能做一个表格的上传下载，或者说是快速复制
     品名然后报价？但是，**品名一定要绝对匹配**才行」⇒ 中栏（报价区）里那块"粘贴两列文本 ⇒ 预览 ⇒
     逐行提交"的面板。

  ★★ `M-PROC-18`（产品口径 2026-10-13）：「中栏保留的就是**批量报价**，**解析并预览放到顶部**」：
     · 它现在是中栏**唯一**的内容块、**常驻可见** —— 原来那个默认 `hidden` 的 `#qjqt-batch` 包装层
       连同 `#qjqt-batch[hidden] { display: none }` 那条兜底规则**一起删除**（没有"收起"这回事了；
       给一个不存在的元素留规则只会让下一个人以为还能收）；
     · 粘贴区 + 「解析并预览」= 中栏**顶部第一块**（DOM 顺序就是它，不靠 `order` 之类的重排）。
  ★ 粘贴区用等宽字体（表格文本的列宽对得上，人能一眼看出分隔符有没有粘对）。
*/
/* ---------------------------------------------------------------- ★★ `M-PROC-23`：中栏标题行（按钮上移 + 提示气泡） */

/*
  ★★ 产品口径（2026-10-13）：
     ①「中栏的**操作按钮都放到中栏顶部**」⇒ 标题 + 「?」提示 + 三个按钮合成一行（标题行）；
     ②「这些解释不要了 ⋯ 可以做一个**提示小点点**，鼠标移上去就有提示」⇒ 说明收进 `.hint-bubble`，
        平时**看不见**（`visibility: hidden` + `opacity: 0`），`:hover` 与 `:focus-within` 才浮现
        （★ 用 `visibility` 而不是 `display:none`：`display:none` 的文本在部分无障碍树里也读不到，
          而且我们还要它留在 DOM 里可被检索 —— 口径文本仍在页面上，只是不占地方）。
     ③ 三个按钮靠右（`.spacer` 撑开）—— 与左栏"需求 + 窗口 + 刷新"同一套观感。
*/
.batch-head {
  align-items: center;
  gap: 8px;
  margin-bottom: 8px;
}

.batch-head h2 { margin: 0; }

.batch-head .spacer { flex: 1; }

/*
  ★★ `M-PROC-36` 追加（产品口径 2026-10-14：「这个页面并没有见到往右边超出，为什么还有横向
     那么宽的滚动条？」「我怀疑是顶部的说明文字导致的宽度被撑开」）—— **怀疑是对的，元凶就是它**：

  搜索那一行最右边的「?」提示点里藏着这个浮层（`.hint-bubble`）。它原来的宽度是
  `min(420px, 70vw)`，而它的定位父级是那个 **16px 宽的 `.hint`** ⇒ 它从「?」的位置**朝右铺开
  420px**。它 `visibility: hidden`（鼠标不悬停就看不见）—— **可 `hidden` 不等于不占布局**：
  `visibility: hidden` 的元素照样参与布局、照样算进滚动区的溢出。
  真 Chrome 实测（1600×1000 / 左栏内容宽 422px）：它一个就把栏位撑宽 **296px** ⇒ 栏位自己长出
  一条**占满整栏的横向滚动条**（17px 高）。而那 17px 正好把底部那条 sticky 分页条顶在上面
  ⇒ 产品看到的"横向那么宽的滚动条"与"分页条怎么都盖不住下面"是**同一个根因**。

  ⇒ 两条一起改，让浮层**永远不可能比它所在那一行还宽**：
    · 浮层的定位父级从 `.hint`（16px）上提到**那一行**（`.row.filters { position: relative }`）
      ⇒ `width: min(420px, 100%)` 里的 `100%` 从此是"这一行的宽度"；
    · `top: 100%` ⇒ 浮层落在**这一行下面**（不再是"贴着「?」往下 22px"），位置确定、不再压住行内控件；
    · 搜索那一行的「?」在最右端（`margin-left: auto`）⇒ 它的浮层改**朝左展开**（`right: 0`），
      右边不越界（`left: 0` 的那些在最左端，左边不越界）。
  ★ 判据见 `test/proc-qjqt-pages.test.mjs` §3m（含反例：把宽度改回 `70vw` ⇒ 必须报红）。
*/
.hint {
  position: static;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: #fff;
  color: var(--muted);
  font-size: 11px;
  line-height: 1;
  cursor: help;
  user-select: none;
}

.hint:hover, .hint:focus { border-color: var(--brand); color: var(--brand); outline: none; }

.hint-bubble {
  position: absolute;
  top: 100%;
  left: 0;
  z-index: 5;
  visibility: hidden;
  opacity: 0;
  width: min(420px, 100%);
  padding: 8px 10px;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: #fffdf5;
  color: var(--fg);
  font-size: 12px;
  font-weight: normal;
  line-height: 1.6;
  white-space: normal;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
  transition: opacity 0.12s ease;
}

.hint:hover .hint-bubble,
.hint:focus .hint-bubble,
.hint:focus-within .hint-bubble { visibility: visible; opacity: 1; }

#qjqt-batch-text {
  display: block;
  width: 100%;
  margin-top: 0;
  padding: 8px 10px;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: #fff;
  color: var(--fg);
  font: 12px/1.5 ui-monospace, Consolas, "Courier New", monospace;
  resize: vertical;
}

/* 预览表：三态各一格底色（✅ / ⚠️ / ❌ 一眼分得开）；★ 类名只表达**状态**，不携带任何数字 */
#qjqt-batch-table td.batch-status { white-space: normal; }
/* ★ 卡面 §1③ 那句"覆盖前一条"/"为什么不匹配"必须是**可见文本**（不是 title 提示） */
#qjqt-batch-table .batch-note { color: var(--muted); font-size: 12px; }
#qjqt-batch-table tr.batch-ok td.batch-status { color: var(--ok); }
#qjqt-batch-table tr.batch-ambiguous td.batch-status { color: #8a6100; }
#qjqt-batch-table tr.batch-duplicate td.batch-status { color: #8a6100; }
#qjqt-batch-table tr.batch-none td.batch-status,
#qjqt-batch-table tr.batch-invalid td.batch-status { color: var(--error); }

/* 多规格那一行的候选下拉（选之前的默认项是空值 ⇒ 该行不可提交） */
#qjqt-batch-table select.batch-spec {
  max-width: 100%;
  padding: 4px 6px;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: #fff;
  color: var(--fg);
  font: inherit;
}

#qjqt-batch-submit[disabled] {
  opacity: 0.45;
  cursor: not-allowed;
}

#qjqt-batch-submit[disabled]:hover { filter: none; }

/* ---------------------------------------------------------------- ★★ `M-PROC-28`：中栏顶部固定 + 表头固定 */

/*
  ★★ 产品口径（2026-10-13）：「**批量报价 这个框 能不能固定在顶部不动，下面的内容标题行也固定，
     内部的数据 翻动就行了**」⇒ 中栏从"整栏一起滚"改成"**顶部不动 + 表头不动 + 只有数据行滚**"：

     · `#qjqt-quote` = flex 竖排 + **栏本身不滚**（`overflow: hidden`，**覆盖**上面
       `.cols3 > section { overflow-y: auto }` 那一句）；
     · 顶部那几块（标题行 `.batch-head`、粘贴区、汇总句）`flex: 0 0 auto` ⇒ 永远钉在栏顶；
     · `.table-wrap` `flex: 1 1 auto` + `min-height: 0` + `overflow: auto` ⇒ **唯一**的滚动区；
     · `#qjqt-batch-table thead th` `position: sticky; top: 0` ⇒ 数据行滚动时**列名一直看得见**
       （`thead th` 本来就是 `#f0f3f7` 的不透明底，这里只是把"必须有底"这件事写进这条规则旁边）。

  ★ 为什么不用 `position: fixed`：三栏并排，fixed 会脱流并盖住别的栏（与本文件分页器那条决策同一个理由）。
  ★ 窄屏（`≤1000px`）那一档里**必须复位**（`@media` 里那三行）—— 那里三栏塌成一列、整页滚。
  ★ 已知代价：粘贴区被钉住之后，中栏**可见的预览行数**比"整栏滚"时少（顶部不再让位）。
    产品要的正是这个（"顶部不动"），若嫌预览区太矮，`#qjqt-batch-text` 的 `rows` 调小即可。
*/
#qjqt-quote {
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

#qjqt-quote > .batch-head,
#qjqt-quote > #qjqt-batch-text,
#qjqt-quote > #qjqt-batch-summary { flex: 0 0 auto; }

#qjqt-quote > .table-wrap {
  flex: 1 1 auto;
  min-height: 0;
  overflow: auto;                /* ★ 只有这里滚（横竖都滚） */
}

#qjqt-batch-table thead th {
  position: sticky;
  top: 0;
  z-index: 2;
}

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

.table-wrap {
  overflow-x: auto;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 8px;
}

table { border-collapse: collapse; width: 100%; }

th, td {
  padding: 8px 10px;
  border-bottom: 1px solid var(--line);
  text-align: left;
  white-space: nowrap;
}

/*
  ★★ 表头行**压扁**（产品口径 2026-10-13：「**表头现在太高了**，需要调整下」）：
     `th, td` 那条共用规则给的是 `padding: 8px 10px` + 14px 字号 ⇒ 表头行整行约 **38px**
     （8+8 留白 + 21.7px 行高，按本文件的行高算出来的，不是实测）；表头只是一行**列名**，
     不该和数据行一样厚。⇒ 只把 `thead th` 压到 `padding: 3px 10px` + 12px 字号 ≈ **25px**，
     **数据行一个像素没动**（`td` 仍是 8px 留白），列宽与配色也不变。
  ★ 三张表（左栏需求 / 中栏批量预览 / 右栏我的报价）共用这一条 ⇒ 观感一致，不会一张厚一张薄。
*/
thead th {
  background: #f0f3f7;
  color: var(--muted);
  font-weight: 600;
  padding: 3px 10px;
  font-size: 12px;
}
tbody tr:last-child td { border-bottom: 0; }
tr.selected-row { background: var(--sel); }

/* ---------------------------------------------------------------- 报价卡片与提示 */

.offer-card { padding: 14px 16px; margin: 10px 0; }

.view { display: block; }
.view[hidden] { display: none; }

.toast {
  position: fixed;
  left: 50%;
  bottom: 26px;
  transform: translateX(-50%);
  max-width: 80vw;
  padding: 10px 16px;
  border-radius: 6px;
  background: #22303f;
  color: #fff;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.18);
  z-index: 20;
}

.toast.is-error { background: var(--error); }

/* ---------------------------------------------------------------- ★★ `M-PROC-18`：左栏行内「报价」按钮 + 单条报价小框 */

/*
  ★★ 产品口径（2026-10-13）：「左栏**每一个品种**那里都增加一个**报价**的按钮，把中栏将单个的报价
     实现归入到这个按钮来：点击按钮**弹出一个小框填价格，点确定就行**；中栏保留的就是**批量报价**，
     解析并预览放到**顶部**」；★ **同日追加**：「点报价**不用谈那么多的模态框**出来，只需要**一个小的
     数字框**，填好点**确认**就行了」。
  ⇒ 单条报价的入口**只有**左栏那一行的按钮；小框（`.modal`）在 `.cols3` **之外**，且**很小**：
    一行"报哪个品种" + 一个数字框 + 一个「确定」（备注框 / 说明段 / 取消按钮都没有了）。
  ★★ **再追加（`M-PROC-21`）**：「报价那个框，**直接悬浮出现**就行了，**不要出现弹窗**了 ——
     直接**悬浮到「报价」的右侧**就行了」⇒ 它已从"居中遮罩弹窗"改成"**贴着按钮右侧的浮层**"：
     `.modal` 是 `pointer-events: none`、无灰底的定位层；`.modal-card` 的坐标由 `app.js` 现算。

  ★ 为什么**必须**显式写 `.modal[hidden] { display: none }`（本仓踩过两次的同类坑：`.pager[hidden]`
     与已删的 `#qjqt-batch[hidden]`）：`.modal` 自己声明了 `display: block`，而作者样式表里的
     `display` 会**盖掉** UA 样式表的 `[hidden] { display: none }` ⇒ 少了这一条，"关不掉"会
     **静默失效**（浮层一直挂在页面上，用户以为页面卡死了）。
*/
.modal {
  position: fixed;
  inset: 0;
  z-index: 30;                 /* 比 toast（20）高：填价格时提示不该压在小框上 */
  display: block;
  pointer-events: none;        /* ★ `M-PROC-21`：不是弹窗 —— 这层不吃点击（点它 = 点在页面上 ⇒ 关掉浮层） */
}

.modal[hidden] { display: none; }

/* ★★ `M-PROC-21`：浮层本体 —— 坐标（`left` / `top`）由 `app.js` 的 `placeQuotePopover()`
   按「报价」按钮的 `getBoundingClientRect()` 现算（贴到按钮**右侧**；右边放不下就翻到左侧）。
   ★ `position: fixed` 而不是 `absolute`：左栏自己会滚动（`.cols3 > section { overflow-y: auto }`），
     `fixed` + 视口坐标才不会被"栏内滚动"带偏。 */
.modal-card {
  position: fixed;
  pointer-events: auto;        /* ★ 只有这一小块吃点击 */
  width: max-content;
  min-width: 200px;
  max-width: min(320px, calc(100vw - 24px));
  padding: 8px 10px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.22);
}

/* 小框里那一行"报的是哪个品种"（只有 `品名｜规格`，没有任何别的字样） */
.quote-head {
  margin: 0 0 8px;
  font-size: 13px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* 数字框与「确定」并排一行（全局 `form` 是竖排、`input` 是 `width:100%` ⇒ 这里按小框的形态覆盖） */
#qjqt-quote-form {
  flex-direction: row;
  align-items: center;
  gap: 8px;
  margin: 0;
}

#qjqt-quote-form #qjqt-offer-price {
  width: 120px;
  margin-top: 0;
  flex: 0 0 auto;
}

#qjqt-quote-form button { flex: 0 0 auto; }

/* 左栏那一行的「报价」按钮（第 5 列）：小、不抢眼 —— 整行本来就可点，它只是"报价"这个动作的明确入口 */
tr.demand-row button.quote-btn { margin-left: 0; }

/*
  ★★ `M-PROC-22`：右栏「操作」列里的「取消」小按钮（取消自己最新那一条报价）。
  ★ 用**红字白底**（而不是实心红）：它是可逆的（软删留痕、随时能重新报），
    不该用销毁级别的观感把供应商吓住；但也要一眼看出"这条是作废动作"。
*/
button.cancel-btn {
  margin-left: 0;
  padding: 2px 8px;
  border: 1px solid var(--error);
  border-radius: 4px;
  background: #fff;
  color: var(--error);
  font-size: 12px;
  cursor: pointer;
}

button.cancel-btn:hover { background: #fdf3f2; filter: none; }

/* 在飞时禁用（`app.js` 的 `offerCancelBusy`）：看得见、点不动（与分页按钮同一套观感） */
button.cancel-btn[disabled] {
  opacity: 0.45;
  cursor: not-allowed;
}

button.cancel-btn[disabled]:hover { background: #fff; }
