/* ═══ 趋势榜 `/board` ═══
   只放这一屏独有的规则。共用的骨架（.btn / .chip / .seg / .bg）在 components.css，
   颜色一律取 vendor/tokens.css 的变量，本文件里不出现任何 hex。

   几何来自 2026-09-21 定稿的效果图（scratchpad `p_css_card.txt`），逐值照抄。
   改之前先读这三条，它们是产品裁定不是版式偏好：

   ① **任何宽度都不换行。** 窗口不够宽时右边那串数字左右拉，左边「名次 · 商品 · 价格评分」
      整块冻住（`.cfreeze`），表头跟着冻。plan 第 08 章逐字写着这是第 03 章原则一的执行：
      「列表页展示所有关键字段（宽了就横向滚动，**不靠省略列求好看**）」，
      并且把代价如实写下了：**这张表最窄要 1606 像素**（现行值只写在 `.binner` 那一处；2026-09-21 从 1298 一路到 1600，09-22 因主图 48→114→84 走到 1678→1648，09-28 主图回到 114 → 1680、同日按钮竖排 → 1606），1440 的笔记本每天都会看到横向滚动条；1920 铺得开（窗口 ≥ 1894 不横拉，Windows 那种一直显示的 17px 滚动条下 ≥ 1911）。
      〔09-28 上午按钮两两一排那一版最窄 1680：1920 要横拉 48px（17px 滚动条下 65px），右边那一列「分享 / 看店铺」
       几乎整列被挡住 —— 产品负责人看过两种排法的实际渲染后定竖排，见「按钮」那一段。〕
      三条退路（砍一列 / 收窄侧栏 / 四个按钮收成两个）都是砍信息或砍功能，所以先选横向滚动。
   ② **一列一个数，除了价格下面那行折扣原价，没有任何附属小字。**
      产品负责人为这件事纠正过两次（原话：「每一列都是一个重点主要信息，
      也没有多余的附属补充字眼或信息」）。⛔ 不要往格子里加说明、单位、标签。
   ③ **手机（≤759px）改成上下堆叠，不横拉、不砍列** —— 与第 10 章
      「手机上靠纵向堆叠实现『信息全面』，不靠删字段」同一条。

   ⚠️ **冻结块是一个容器包住四格，不是每格各自 sticky。**
      每格各自 sticky 时，格子之间的 `column-gap` 是漏的 —— 横向拉动时右边那串数字
      会从缝里穿过去，而它看起来只是「有点花」，不像一个 bug。 */

/* ── 页面骨架：页面自己不滚，表格在内部滚 ── */
.screen > .page.boardpage {
  --board-img: 114px;   /* 四行文字块（英文名 · 中文译名 · 店铺 · PID）的实测高度；见 .c-thumb 那一段 */
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow: hidden;                           /* 覆盖 screens.css 的 overflow-y:auto */
}
/* 手机上整页一起滚（2026-10-06，二期工单 8.3b；产品负责人看过效果图）：控件跟着榜单滚走，滑过去之后标题栏以下全是榜单。
   上面那一条「页面自己不滚、表格在内部滚」是给电脑的 —— 冻结列只在表格自己滚时成立；手机上一行一张卡、没有横向滚，用不着它，
   而它在手机上的代价是实测的：按商品看，一屏 844 像素里榜单只露出 151。
   ⚠️ 往下滑加载更多（board.js `maybeMore`）因此要听「真在滚的那一个」：只听 `.bscroll` 的话，手机上永远停在第一批。
   📌 只动这一处就够：页面不再是 flex，`.btable` / `.bscroll` 的高度跟着内容走，它们就不会自己滚了（横向那一刀照旧在 `.bscroll`）。 */
@media (max-width: 759px) {
  .screen > .page.boardpage { display: block; overflow-x: hidden; overflow-y: auto; }
}

/* ── 控件区（三排）── */
.boardpage .bfilters {
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-3) 18px 0;             /* GAP pad-18 与外壳一致 */
}
.boardpage .frow { display: flex; gap: var(--space-2); flex-wrap: wrap; align-items: center; }
.boardpage .frow .gapl { margin-left: var(--space-2); }

/* 筛选菜单：累计销量 · 价位，都可多选；不选 = 全部。 */
/* 🔴 **这一条不能省**：作者样式里的 `display: flex` 会压过浏览器自带的
   `[hidden] { display: none }`，于是 `hidden` 属性设了也没用 —— 菜单一直开着，
   而「筛选 ▾」那个按钮点下去屏幕上什么都不变。
   ⚠️ 它是**截图看出来的，不是断言抓到的**：DOM 上 `hidden` 确实是 true，
   `aria-expanded` 也在正确翻转，只有真的渲染出来才看得见它没关上。 */
.boardpage .bmenu[hidden] { display: none; }
.boardpage .bmenu {
  display: flex; flex-direction: column; gap: var(--space-2);
  border: 1px solid var(--c-neutral-200); border-radius: var(--radius-md);
  background: var(--c-neutral-0); padding: 10px 12px;
}
.boardpage .mrow { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.boardpage .mt { width: 72px; flex: none; font-size: var(--size-sm); color: var(--c-neutral-500); }
.boardpage .mhint { font-size: var(--size-11); color: var(--c-neutral-500); }
.boardpage .opt {
  height: 26px; padding: 0 10px;
  border: 1px solid var(--border-control); border-radius: var(--radius-sm);
  background: var(--c-neutral-0); color: var(--c-neutral-800);
  font: inherit; font-size: var(--size-sm); cursor: pointer; white-space: nowrap;
}
.boardpage .opt:hover { background: var(--c-neutral-50); }
/* 「全部」也是一个**按下的按钮**，不是「一个都没选」（§8b.1 那条：
   它翻译成「不传这个参数」，但用户看到的是一个有按下态的按钮）。 */
.boardpage .opt[aria-pressed="true"] {
  background: var(--c-neutral-900); border-color: var(--c-neutral-900); color: var(--c-neutral-0);
}

/* 页尾「怎么排的」：**常驻**，不是点开才有（plan 第 08 章守的三条之二）。 */
.boardpage .bhow { margin-top: 14px; }
.boardpage .bhow p { margin: 0 0 8px; }
.boardpage .bhow p:last-child { margin-bottom: 0; }

/* 「算不出来 N」「涨跌家数」那一条。 */
.boardpage .cover {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2);
  font-size: var(--size-sm); color: var(--c-neutral-500);
  padding-top: 2px;
}
.boardpage .cover b { color: var(--c-neutral-900); font-weight: 600; font-family: var(--font-mono); }
.boardpage .sepv { width: 1px; height: 12px; background: var(--c-neutral-200); }
.boardpage .note {
  font-size: var(--size-sm); line-height: var(--leading-normal); color: var(--c-neutral-600);
  background: var(--c-neutral-50); border: 1px solid var(--c-neutral-100);
  border-radius: var(--radius-md); padding: 10px 12px;
}
.boardpage .note b { color: var(--c-neutral-900); font-weight: 600; }
.boardpage .note p { margin: 0 0 6px; }
.boardpage .note p:last-child { margin-bottom: 0; }

/* ── 表格 ──
   ⚠️ **类名刻意不叫 `.board`**：`screens.css` 有一条**裸的** `.board`（五列看板的骨架），
   它给的 `align-items: start` 会漏进来，把这个滚动容器从「拉到容器宽、里面横滚」
   变成「长成内容宽 1298、把整页撑开」—— 而那一刻冻结块整个失效，
   因为冻结只对「表格自己在滚」有效。实测抓到的：`.bscroll` clientWidth 1298 / 容器 1152。
   📌 它不报错、不闪、截图上看不出来，只有量 clientWidth 才发现。 */
.boardpage .btable {
  flex: 1; min-height: 0;
  display: flex; flex-direction: column;
  min-width: 0;                               /* 同上：不收住的话下面那层就撑开了 */
  overflow: hidden;
  padding: 10px 18px 14px;                    /* GAP board-pad */
  position: relative;
}
/* `min-width: 0` 是防御，**不是当初修好横向滚动的那一条** —— 这句话要写准，
   因为一个错的根因会让下一个人去动错的地方。
   🔴 **真正的根因是类名撞车**（见上面 `.btable` 那段）：`screens.css` 那条裸的 `.board`
   给了 `align-items: start`，于是滚动容器没有被拉到容器宽、而是长成了内容宽 1298。
   ⚠️ **两条都注入验过，结论是它们不同**：改回 `.board` → 「右边那串数字可以左右拉」当场红
   （clientWidth 1298 == scrollWidth，根本没在滚）；而单独拿掉这一行 `min-width: 0`
   → **28 条全绿**。所以它今天不承重，留着是挡「以后某个子元素又长出一个大 min-width」那一类。
   📌 记下来是因为我第一次把功劳记到了它头上 —— 注入之前那句话读起来完全成立。 */
.boardpage .bscroll { flex: 1; min-width: 0; min-height: 0; overflow: auto; scrollbar-width: thin; }
/* 🔴 这个数是算出来的不是拍的：冻结块 + 八列 + 八个 8px 列间距 64 + 行右内边距 13 + 行左右两道 1px 边框。
   〔历次：1298（八列 698）→ 1376（778）→ 1390 → 1378（780）→ **1600**（冻结块 531 + 八列 976），见第四轮那一段
    → 1678 → 1648（09-22 图 114 → 84）→ **1680**（09-28 图回到 114）
    → **1606**（09-28 同日按钮竖排：按钮那一列 124 → 66、列间距 10 → 8，见「按钮」那一段）〕
   改任何一列的宽度都要跟着改这个数，**也要跟着改 `.bgrid` 第一列的下限** —— 否则冻结块会在最窄处被压掉。
   🔴 **2026-09-28 量出来它此前就被压掉了，而且一眼看得见**：`.bgrid` 第一列的下限一直是 531
      （09-21 第四轮的值：图 48、块内间距 10），09-22 图变 84、间距变 14、冻结块的内容涨到 579 时它没有跟着改。
      最窄处多出来的那点宽度又被它和每小时图那一列对半分掉，于是这一列只分到 554 ——
      **价格评分那一格探出冻结块 25px，冻结块右缘那道影正好从价格数字中间劈过去**（1920 与 1440 都是：
      「$12|.99」，两个评分数字紧贴着那道影）。它不报错、不出滚动条，屏幕上只是一道线。
      ⚠️ 只把图抬到 114、把这个数改成 1678 的话，探出去的是 40px（压进累计销量那一列 30px）。
   ✅ 现在：第一列下限 = 冻结块自己的内容宽 **609**（左内边距 13 + 名次 46 + 图 114 + 商品名下限 240
      + 价格评分 154 + 3 × 14），八列 = 120 × 3 + 132 + 120 × 3 + 66 = 918，
      这个数 = 609 + 918 + 64 + 13 + 2 = **1606**（09-28 上午那一版是 609 + 976 + 80 + 13 + 2 = 1680；更早的和式漏了那 2px 边框）。
   ✅ **量过**：1440 那一档 `.binner` 宽 1606（= min-width，最窄处；把表一像素一像素收窄量出来要的也是 1606），
      价格评分右缘 = 冻结块右缘，冻结块与累计销量那一列之间的 8px 在（`build/verify/board_flow.mjs` ⑲）；
      1920 那一档滚动口 1632、**不横拉**，模拟 Windows 那种一直显示的 17px 滚动条（滚动口 1615）也不横拉（⑧c）。
      窗口宽 ≤ 1893 横滚、≥ 1894 铺开（17px 滚动条下 ≥ 1911）—— 这两个门槛是 09-28 逐像素扫窗口宽量的一次，不在 harness 里。
      〔09-28 上午按钮两两一排那一版：`.binner` 1680，窗口 ≤ 1967 横滚 —— **1920 要横拉 48px**（17px 滚动条下 65px），
       按钮那一列右边「分享 / 看店铺」那一列几乎整列被挡住。同日产品负责人看过两种排法的实际渲染后定竖排。〕
   ⚠️ 此前那句「1440 实测 `.binner` 宽 1600，对得上」量的是它自己：最窄处 `.binner` 的宽按定义就等于
      min-width，量它证明不了冻结块装得下 —— 该量的是「价格评分的右缘有没有探出冻结块」。 */
/* 🔴 2026-09-21 他在 2K（2560）屏上第一眼：「所有数字都挤在右边，左边又特别空」。
   成因不是没按设计做 —— 线上这三条与方案页 v12 的效果图**逐字相同**。是 `.bgrid` 第一列
   `minmax(505px, 1fr)` 里那个 `1fr`：**多出来的宽度全给了冻结块（商品名那一块）**。
   1298 基准下它是 505；2K 上可用约 2308，它吃到 **1546**，而右边八列仍旧只有 682 挤在最右。
   ⚠️ **效果图只在「桌面 / 笔记本 1280 / 手机 390」三档看过，2560 从来没有人看过它。**
   ⛔ 不给八列加宽：它们是右对齐的数字，列变宽只会让每个数字离下一个更远，更难横着读。
      〔2026-09-21 第四轮它们还是变宽了，**而理由不是这一条** —— 是表头一行装不下，见下〕
   ⛔ **也不给整行加 max-width 居中** —— 我先试了（方案页文档区就是 `.wrap{max-width:1560px}`），
      截出来当场看见两个新毛病：**筛选条贴左而表格居中，两者断开**；而且「空」只是从商品名块内部
      搬到了页面左侧，**而他抱怨的正是左边空**。
   ✅ 落法：**第一列从 `1fr` 改成有上限的 `minmax(505px, 880px)`，多出来的宽度给「每小时新增」那张图**
      （`minmax(116px, 340px)`）。表格仍然贴左，和筛选条对齐。
      判据是「哪一列变宽才有信息」：**图变宽 = 柱子看得清；右对齐的数字列变宽 = 每个数离下一个更远，更难横着读**。
   算出来：冻结块 880 → 商品名 880−13−46−48−154−30 = **589px**〔第四轮：价格评分那一格 128 → 154，所以这里从 615 变成 589〕
      〔2026-09-28：图 114、块内间距 14 之后是 880−13−46−114−154−42 = **511px**，2560 上量过〕。
      图 116 → `minmax(116px, 170px)` —— 他第三条：「**每小时新增那个缩略图不用那么长，
      跟信号流那一列宽度差不多就行**」。信号流那张图量过：柱区约 121px、整块约 166px。
      ⛔ 340 那一版是我放宽的，他看了说太长；715（吃满剩余）那一版柱子连成一片黑，当场否掉。
   ✅ **三组列按他说的等宽**（他第三条：「增长率这几格的重点信息，宽度要一致，可以均分」）：
      累计 / 本段新增 / 上一段新增 **各 88**（原 76/84/84）· 增长率 / 总销量占比 / 本店排名 **各 96**（原 96/76/56）。
   🔴 **代价如实记**：固定列总和 682 → 778，所以 `min-width` 要从 1298 抬到 **1376**，
      横向滚动条出现的门槛从约 1550 抬到约 **1628** —— **1600 的屏会从「刚好铺开」变成「要横拉一点」**。
      1440 的 MacBook 本来就横滚，不变。这是为等宽付的钱，不是免费的。
   ✅ **剩余靠 `justify-content: space-between` 摊进列与列之间，整行撑满、右边不留白**
      （他第二条：「卡片肯定需要像信号流一样撑满……剩下的部分，就把后面的内容排列清楚」）。
   ⛔ **试过让图吃掉全部剩余（`1fr`），截出来当场否掉**：图涨到 715px，24 根柱连成一片黑，
      而且**把数字劈成左右两组、中间横着一大块图**，比改前更难读。
   ⛔ **也不把剩余分给那七个数字列**〔第四轮同上：变宽了，但不是为了摊剩余〕：它们右对齐，列变宽只会让每个数离下一个更远 ——
      「排列清楚」不等于「把每一列摊宽」，而是让**列与列之间**松开、列宽本身不动（数字位置才稳）。
   📌 `space-between` 在窄屏上自动失效：列总宽正好等于容器宽时剩余为 0，1440 那一档一字不变。
   🔴 **两个 minmax 的下限一个没动，所以那一轮的 `min-width` 只受固定列变化影响** —— 1440 那一档的横滚行为一字不变。
      〔2026-09-21 改：原文写「所以 `min-width: 1298px` 仍然成立」，**而同一段自己上面十行刚把它抬到了 1376** ——
       一句现状句被它同一段的另一句改假了。现行值只写在 `.binner` 那一处。〕

   🔴 2026-09-21 补齐第四个按钮（收藏）之后，按钮那一列 **110 → 124**：
   `.c-act` 是 `auto auto` 的 2×2，两列取各自那两个按钮里更宽的那个 ——
   实测**要 123px**（打开链接 / 收藏 一列，分享 / 看店铺 一列），而它原来定宽 110，
   **按钮会挤到邻居身上**。⚠️ 这个 123 是量出来的不是估的：我先按字数估了 106，
   harness 那条「按钮没有挤出它自己那一列」当场报 `要 123px · 列宽 110px`。
   〔这一段记的是 09-21 当时。2026-09-28 起桌面上四个按钮竖排成一列，这一列 124 → 66，见「按钮」那一段。〕
   📌 方案页 v12 那份效果图的第四格写的是「**更多**」，装「只看这家店的榜」和「这个数不准」——
   **查过，这两件今天都还不存在**（`board.SCOPES` 只有 `all` / `followed`；「不准」反馈是
   `POST /signals/{id}/inaccurate`、挂在信号上，而榜单行没有信号 id）。
   ⛔ 所以第四格放「看店铺」，不放一个点开只有空壳的「更多」——
   「一个存在但不起作用的开关，比没有开关更坏」。那两件真做出来那天再换。

   🔴 2026-09-21 第三轮，他看完 AI Hot 的模型榜之后定：**「不用等宽，大多数列等宽就可以，
   实在不能等宽的可以略宽。只要是直观美观的就可以。」** 于是六个数字列改成
   **五列 88 + 增长率 100**，而那两个数是**量出来的不是挑的** ——
   把每一列最坏的那个串真的塞进那一格、量渲染之后的宽度：
     累计销量 74（`1,234,567`）· 这一段新增 71（`+123,456`）· **上一段新增 82**（表头「上一个 24 小时」）
     · 总销量占比 67 · 本店排名 47  → **五列都 ≤82，88 装得下**
     · **增长率 99**（`+99999.00%`）→ **它装不进 88，也装不进原来的 96** —— 这就是那个「实在不能等宽的」。
   ⚠️ **`+7383.00%` 不是我编的极端值**：`board.js` 里记着线上 `7d` 档真出过 `23→1721 = +7383%`；
      五位数百分比要 `prev=5` 配一次千倍暴涨，少见但会发生，而多那 4px 不要钱。
   🔴 **第一版是用 canvas `measureText` 量的，它在静默少算** —— `ctx.font` 的简写一旦拼不合法
      就不生效、退回 `10px sans-serif`，**量出来是一个看起来完全正常、而且偏小的数**
      （它把增长率报成 82，于是 96 看起来绰绰有余）。⚠️ 改成把字真的渲染出来再量才看见 99。
      📌 **量一个「要多宽」永远按渲染后的矩形量，不按算出来的字宽。**
   ✅ 顺带便宜了：固定列 792 → **780**，`min-width` 1390 → **1378**，横滚门槛反而比上一版低一点。

   🔴 **2026-09-21 第四轮（他看了线上那一屏）：「表头那些文字也要全部调整」+「『近 24 小时新增』
   为什么要换行？……你的宽度是足够的」+「稍微压缩一下『累计销量』的宽度，价格评分这一列太窄了」。**

   **（一）字号对齐信号卡，判据是量出来的不是挑的。** 信号卡那一套是 2026-09-17 他拍板抬起来的
      （`components.css`：事件名 17 · 商品名 15 · 店铺与 PID 13），当时的判据逐字写着
      「三家参照的正文都在 14–17px，而我们 95.2% 的字 ≤12.5px」。
      **而这一屏是 09-21 新画的，一条都没跟上**：店名 12.5 · PID 12 · 表头 12 · 大部分数字 15。
      现改为 商品名 17 · 译名 14 · 店名/PID 13 · 表头 13 · 数字 17 · 在排那列 20 · 名次 24
      · 价格评分组 14（标签 13）· 折扣原价 12。⛔ 一个都没有新开字阶档（`--size-*` 里本来就有）。

   **（二）表头一行放不下，所以列宽由表头定 —— 不是「宽度够了却换行」。**
      把那两句真的渲染出来量：**「近 24 小时新增」要 89px · 「上一个 24 小时新增」要 115px**，
      而当时的列宽是 88。⚠️ 所以他看到的「宽度是足够的」是 `space-between` 在 2560 上摊出来的
      **列与列之间**的空，不是**列自己**的宽。
      ✅ 六个数字列统一到 **120**（115 + 余量）：它们此后全部等宽，`⑯b` 那条「至少五列等宽」
         不再需要例外。⚠️ **余量不能只留 1px**：`--font-sans` 是系统字体栈，
         同一句中文在别的平台上差几像素是常态，而这一屏没有任何东西会为此报错。

   ⛔ **这和上面那两条「不给八列加宽」不矛盾，理由不同不能混读**：
      那两条挡的是「**把富余摊给数字列**」（摊了只会让每个数离下一个更远）；
      这一轮是「**表头一行装不下**」—— 同一个动作，两个理由，而只有后者是被内容逼出来的。
      📌 判据仍然没变：**哪一列变宽才有信息。** 这一次「有信息」的是表头那句话本身。

   **（三）价格 · 评分 128 → 154**（他第三条的前一半）。
      ⚠️ **后一半「压缩累计销量」没做**，理由写在这里而不是留给下一个人猜：
      表头改成一行之后，六个数字列是被**表头**定宽的（最宽那个 115），
      单独把累计销量收窄会让它成为唯一一根参差的列，而 `⑯b` 那条等宽判据正是他上一轮定的。
      **要收窄的话改这一处即可，代价是那一列比邻居窄一截。**

   🔴 **代价如实记**：固定列 780 → **976**，冻结块 505 → **531**，`min-width` 1378 → **1600**；
      横滚门槛从约 1630 抬到约 **1852**。1920 与 2560 仍然铺得开，**1440 本来就在横滚、不变**。
      ✅ 这个数不是算出来就算数的：1440 那一档实测 `.binner` 宽 **1600**、滚动口 1152，对得上。
      〔这一段记的是第四轮当时。2026-09-28 上午图回到 114 之后 1920 也要横拉 48px，同日按钮竖排之后又铺得开了；
       而「量 `.binner` 宽」证明不了冻结块装得下 —— 都见 `.binner` 上面那一段。〕 */
.boardpage .binner {
  min-width: 1606px;   /* 冻结块 609 + 八列 918 + 64 gap + 13 pad + 2 行边框；2026-09-21 1298 → … → 1600 → 1678 → 1648（09-22 图 48 → 114 → 84、块内间距 10 → 14）→ 1680（09-28 图 114，补上漏算的 2px 边框）→ 1606（09-28 按钮竖排：最后一列 124 → 66、列间距 10 → 8）*/
  display: flex; flex-direction: column; gap: 6px;   /* GAP row-gap-6 */
  padding-bottom: 2px;
}

/* 外层：冻结区 + 右边一串数字 + 按钮。列按 DOM 顺序自动放，不用 areas。
   ⚠️ 第一列的下限 609 = 冻结块自己的内容宽（`.cfreeze` 那几列 + 间距 + 左内边距），
      改 `--board-img` 或 `.cfreeze` 的任何一列都要跟着改它和 `.binner` 的 min-width —— 见 `.binner` 上面那一段。 */
.boardpage .bgrid {
  display: grid; column-gap: 8px; align-items: center;   /* GAP col-gap-8 · 2026-09-28 10 → 8，见「按钮」那一段 */
  justify-content: space-between;
  grid-template-columns: minmax(609px, 880px) 120px 120px 120px minmax(132px, 170px) 120px 120px 120px 66px;
}
/* 🔴 2026-09-21：这四格**顶部对齐**，不是各自垂直居中。
   卡片高度提到约 136 之后，四行字那一格比名次格、头图格高一大截，
   `align-items: center` 会让它们三个各自居中 —— 屏幕上就是名次浮在文字块中间、和第一行对不齐。
   信号卡是顶部对齐的（头图顶 = 标题顶），照它。
   〔2026-09-28 起桌面上文字块与价格评分那一格拉满这一行（顶照旧对齐，底也对齐），名次仍居中 ——
    见「四行字的上下两条线」与名次那一段。〕 */
.boardpage .cfreeze {
  align-items: start;
  position: sticky; left: 0; z-index: 2;
  background-color: inherit;
  /* ⚠️ 这里**不许再写一次 `align-items`** —— 上面那条 `start` 是这一段注释说的那件事，
     而 2026-09-21 之前这一行写着 `center`、排在它后面，于是后者胜出：
     注释说「顶部对齐」，屏幕上却是名次浮在文字块中间。
     📌 同一条属性在同一个规则里写两遍时，**注释描述的是第一遍，生效的是第二遍** ——
        它不报错、不警告，而读注释的人会以为自己读懂了这段代码。 */
  display: grid; column-gap: 14px; min-width: 0;
  padding-left: 13px;                                     /* GAP freeze-pad-13 */
  /* 🔴 2026-09-22 他同时定：「店铺、商品名可以整体往右移一点，包括价格、评分，
     也可以整体稍微往后移动一点。」落法是**只加这一块内部的列间距**（10 → 14），
     ⛔ 不动 `.bgrid` 那一层 —— 那一层的间距是右边八列之间的，跟这件事无关。 */
  grid-template-columns: 46px var(--board-img) minmax(240px, 1fr) 154px;
  /* 冻结块右缘那道影：横向拉动时它是「右边还有内容」的唯一提示。
     GAP —— 令牌里没有「贴边阴影」这一档，只有卡片那几档。 */
  box-shadow: 8px 0 8px -8px rgb(18 24 26 / 14%);
}
:root[data-theme="dark"] .boardpage .cfreeze,
:root:not([data-theme="light"]) .boardpage .cfreeze {
  /* GAP 同上：深色下要更重才看得见 */
  box-shadow: 8px 0 8px -8px rgb(0 0 0 / 50%);
}

/* 表头：跟着上边缘冻，横向拉动时跟着走。 */
.boardpage .bhead {
  position: sticky; top: 0; z-index: 4;
  background: var(--c-neutral-0);
  padding: 8px 14px 6px 0;                    /* GAP head-pad */
  font-size: var(--size-sm); line-height: var(--leading-snug);
  color: var(--c-neutral-500);
  align-items: end;
  border-bottom: 1px solid var(--c-neutral-100);
}
.boardpage .bhead .cfreeze { background: var(--c-neutral-0); z-index: 5; align-items: end; padding-left: 14px; }
.boardpage .bhead span { white-space: nowrap; }
.boardpage .bhead .r { text-align: right; }
.boardpage .bhead .l2 { display: block; }
.boardpage .bhead .sub2 { display: block; font-weight: 400; font-size: var(--size-xs); color: var(--c-neutral-500); }
/* 当前在排的那一列，表头加重 —— 用户要看得出「这一屏此刻按哪个数排」。 */
.boardpage .bhead .on { color: var(--c-neutral-900); font-weight: 600; }
.boardpage .h-rank { grid-column: 1; }
.boardpage .h-body { grid-column: 2 / 4; }
.boardpage .h-kv { grid-column: 4; }

.boardpage .brow {
  line-height: var(--leading-normal);
  border: 1px solid var(--c-neutral-100); border-radius: var(--radius-lg);
  padding: 16px 13px 16px 0;                  /* GAP row-pad · 2026-09-21 10 → 20 → 16，见 .c-body */
  background: var(--c-neutral-0);
  position: relative; cursor: pointer;
}
.boardpage .brow:hover { background: var(--c-neutral-50); border-color: var(--c-neutral-200); }

/* ── 名次 ── */
/* 🔴 2026-09-22 他定：「**名次这个数字能不能居中？上下居中、左右居中都要居中**」。
   ⚠️ 这一格因此是整个冻结块里**唯一**不跟 `align-items: start` 的 —— 图和文字仍旧顶端对齐，
      只有名次在它那一列里整块居中。**这是有意的不对称，⛔ 不要「顺手统一」回去。**
   📌 代价如实记：四行高的行里（2026-09-28 起每一行都是），名次会比商品名低一截 ——
      而那正是他要的：名次是这一行的编号，不是这一行的第一句话。 */
.boardpage .c-rank {
  display: grid; gap: 2px; justify-items: center; align-content: center;
  align-self: center; justify-self: center; text-align: center;
}
.boardpage .rk {
  font-family: var(--font-mono); font-size: var(--size-2xl);   /* 24 —— 名次是这一屏的主锚点，要压得住 20 的在排列 */
  font-weight: 600; line-height: 1.1; font-variant-numeric: tabular-nums;
}
.boardpage .mv {
  font-family: var(--font-mono); font-size: var(--size-sm);
  color: var(--c-neutral-800); white-space: nowrap; min-height: 20px;   /* GAP mv-h-18：占位，避免有无记号时行高跳动 */
  /* 🔴 `gap` 不是 1px：等宽字体里 `1` 的底衬线会和三角连成一条，**`▼1` 读起来像 `▼−1`** ——
     2026-09-22 我自己在 2× 截图上就读错了一次，去核字符码（U+25BC + `1`）才确认没有负号。
     ⚠️ 而「看起来像负号」在这一格上特别坏：下降本来就是负的，**一个假负号不会显得荒谬，
     只会让人以为那是真的**，于是没有人会去查。3px 把两个字形分开。 */
  display: inline-flex; align-items: center; gap: 3px;
}
/* 🔴 三角单独放大（他：「名次上下的标也很小」）。**放大的是方向，不是那个位数差** ——
   位数差是次要信息，跟着一起变大只会把这一格顶成两行。
   ⚠️ 用 `em` 不用 px：`.mv` 在窄屏那一档会变，写死 px 三角就不跟着走。 */
.boardpage .mv .tri { font-size: 1.35em; line-height: 1; }
/* 🔴 名次记号：2026-09-20 产品负责人拍板红涨绿跌，这推翻了 plan 第 11 章「涨跌不用红绿」。
   那条的两个理由一条在这里不成立、一条仍然成立：
     ·「红色留给『现在就去看』」—— 这一屏不发信号、没有严重度，那个名额在这里是空的
     ·「销量下跌不一定是坏事」—— **仍然成立**，所以红绿只画在名次变化上，⛔ 不画在销量数字上
   ⚠️ **▲▼ 两个形状一个都不许去掉**：第 11 章「红绿色盲约占男性 1/12，状态必须同时用
      颜色、形状、文字三重编码」**没有被推翻** —— 它管的是「颜色不能是唯一编码」，
      与用哪两个颜色是两件事。
   🔴 **2026-09-22 他把红绿扩到增长率**（看过五种画法的对照图之后选的「无符号 · 三角 + 红绿」）。
   **于是「销量跌不一定是坏事」那一半也被放下了 —— 只放下这一格**：
   ⛔ **销量本身（累计销量 / 这一段新增 / 上一段新增 / 总销量占比）仍然一律不上色**，
      那几个数上那条理由原样成立。⚠️ 令牌因此从 `--c-rank-*` 改名 `--c-change-*`：
      名字只盖住名次时，用到增长率上它就开始撒谎（`hour_baseline.samples` 那次同一条）。
   ⛔ 令牌 `--c-change-up` / `--c-change-down` 的作用域就是这一屏的这两格，别处不许援引。 */
.boardpage .mv.up { font-weight: 700; color: var(--c-change-up); }
.boardpage .mv.down { font-weight: 700; color: var(--c-change-down); }
.boardpage .mv.na { color: var(--c-neutral-500); }
.boardpage .mv.new {
  font-family: var(--font-sans); font-size: var(--size-xs); font-weight: 600;
  line-height: 20px; min-height: 0;
  color: var(--c-accent-text); background: var(--c-accent-wash);
  border-radius: var(--radius-sm); padding: 0 6px;
}

/* ── 商品 ── */
/* 🔴 **主图 114、正方形** —— 2026-09-28 产品负责人看过三种画法的实际渲染后定的
   （plan 第 08 章「有中文译名之后的一行」）。
   · **114 是四行文字块的实测高度**（商品名 · 中文译名 · 店铺 · PID，样本上量 114.2）：
     图的上下边就是这一行的上下两条线 —— 正是 09-22 上午他定的「保持正方形；从上到下占满 4 行的位置」。
   · 09-22 下午改成过 84（他看线上：「过高、过大了」），**只因为那时线上还没有中文译名**：
     每一行只有三行字（84.4px），114 的图每一行都多探出 30px。译名上线、文字块回到四行，那个前提就没了。
   · 同一次定的另外几条在下面「四行字的上下两条线」那一段：价格评分与按钮对齐这两条线 ·
     每小时图仍是 40 像素高、在正中 · 没有中文译名的行把那一行空着、行高与别的行相同。
   ⚠️ **正方形不是可选项**：商品头图是方的，跟着行高拉成长方形会裁掉主体。
   📌 行高因此一律约 148（此前三行 118 / 四行 148 两档）；比三行那一版一屏少看约一行半。 */
.boardpage .c-thumb { display: grid; place-items: start; }
.boardpage .c-thumb .thumb {
  width: var(--board-img); height: var(--board-img);
  border-radius: var(--radius-md); object-fit: cover;
}
/* 🔴 2026-09-21 他第四条：「**每一个卡片的高度，我倒是觉得可以参考信号流**……它能够完全容纳
   我们现在文字大小的要求，以及排版、间距等要求。而且它跟整个产品在信号流这块的观感上，
   是同样的视觉体验 —— 信号流切到趋势榜的时候，不会让人感觉看的是两个不同的产品。」
   量过：信号卡 2560 上高 **141px**（padding 11/13），趋势榜这一行只有 **88px**。
   〔2026-09-21 第四轮抬字阶之后重量：四行的那种 **148px**、三行的（没有中文译名）118px；
    为了不从 141 跑太远，gap 8 → 6、padding 20 → 16，**字号一个都没退回去**。
    2026-09-28 起没有中文译名的行也留出那一行（空着），行高一律 148，见「四行字的上下两条线」〕
   两边都是四行字，差的是行距：信号卡每行约 30px，这里每行 17px（紧了 1.76 倍）。
   ⛔ 不靠加 padding 把高度垫上去 —— 那是把卡片撑空，不是把字排开。
   ✅ 四行之间给 `gap`，行本身放松到 `--leading-snug`，padding 抬起来。
   📌 当时量出来 88 → **140px**，信号卡是 141 —— 两屏切换时高度不再差一截。
   ⚠️ **那两个字面量（gap 11 / padding 23）写下的时候就和代码对不上**，现行值只看代码。
   ⚠️ 为什么这里的行距必然比信号卡松：**信号卡那 141px 里是五行**（信号标题 + 商品名 + 译名 +
      店铺 + PID/时刻），这一行只有四行。同样的高度、少一行字，间距自然要大一点。
   ⚠️ 代价如实记：一屏能看到的行数从 8 行降到约 6 行。他明确要这个高度。 */
.boardpage .c-body { min-width: 0; display: grid; gap: 6px; align-content: start; }
.boardpage .nm { font-size: var(--size-lg); font-weight: 500; line-height: var(--leading-snug); }
.boardpage .cn { font-size: var(--size-base); color: var(--c-neutral-500); }
.boardpage .shp { font-size: var(--size-sm); color: var(--c-neutral-600); }
.boardpage .pidl { font-size: var(--size-sm); color: var(--c-neutral-600); }
.boardpage .nm,
.boardpage .cn,
.boardpage .shp,
.boardpage .pidl { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.boardpage .cnmark {
  display: inline-block; border: 1px solid var(--c-neutral-200); border-radius: var(--radius-sm);
  font-size: var(--size-xs); line-height: 16px; padding: 0 4px;
  margin-right: 5px; vertical-align: 1px;
}
.boardpage .lnk {
  border: 0; background: none; padding: 0; font: inherit;
  color: var(--c-neutral-800); cursor: pointer; border-bottom: 1px solid transparent;
}
.boardpage .lnk:hover { border-bottom-color: var(--c-neutral-400); }
.boardpage .ccompany { color: var(--c-neutral-600); }

/* ── 价格 · 商品评分 · 店铺评分：一组三行 ── */
.boardpage .c-kv {
  display: grid; gap: 2px;
  border-left: 1px solid var(--c-neutral-100); padding-left: 18px;   /* GAP kv-pad · 2026-09-22 12 → 18，见 .cfreeze */
  font-size: var(--size-base); line-height: var(--leading-snug); min-width: 0;
}
.boardpage .kvi { display: grid; grid-template-columns: auto 1fr; column-gap: var(--space-2); align-items: baseline; }
.boardpage .kl { color: var(--c-neutral-500); font-size: var(--size-sm); white-space: nowrap; }
.boardpage .kvv {
  justify-self: end; font-family: var(--font-mono); font-variant-numeric: tabular-nums;
  font-weight: 600; color: var(--c-neutral-900); white-space: nowrap;
}
.boardpage .kvv.na { color: var(--c-neutral-400); font-weight: 400; }
/* **全屏唯一允许的附属小字**：折扣价下面那行原价 —— 「卖得好」和「在打折」是同一个判断里的两半。 */
.boardpage .dis {
  grid-column: 1 / -1; justify-self: end;
  font-size: var(--size-xs); line-height: 1.4; color: var(--c-neutral-500); white-space: nowrap;
}
.boardpage .dis .n { font-family: var(--font-mono); }

/* ── 每小时图：实心柱 = 这一段，虚线 = 上一个同时段 ── */
.boardpage .c-chart { min-width: 0; display: grid; }
.boardpage .c-chart .ml { display: none; }
.boardpage .c-chart svg { display: block; width: 100%; height: 40px; }   /* GAP spark-h-40 */
.boardpage .bar { fill: var(--c-neutral-900); }
/* 虚线走两层：底下一层用背景色加粗垫一道，柱子密集时虚线才认得出来。 */
.boardpage .prevline-halo { stroke: var(--c-neutral-0); }
.boardpage .prevline { stroke: var(--c-neutral-500); }
.boardpage .baseaxis { stroke: var(--c-neutral-100); }

/* ── 右边那串数字 ── */
.boardpage .c-nums { display: contents; }
.boardpage .m { display: grid; justify-items: end; min-width: 0; }
.boardpage .m .ml { display: none; font-size: var(--size-9); color: var(--c-neutral-500); white-space: nowrap; font-weight: 400; }
.boardpage .m .v {
  font-family: var(--font-mono); font-variant-numeric: tabular-nums;
  font-size: var(--size-lg); font-weight: 500; line-height: var(--leading-snug);
  color: var(--c-neutral-800); white-space: nowrap;
}
/* 当前在排的那一列，数字加重（plan 第 08 章：「被排的那一行加粗」）。 */
.boardpage .m.k .v { font-size: var(--size-xl); font-weight: 700; color: var(--c-neutral-900); }
.boardpage .m .v.na { color: var(--c-neutral-400); font-weight: 400; }
/* ── 增长率：▲▼ + 红绿，不写 + / − ──（2026-09-22 产品负责人从五种画法里选的这一种）
   🔴 **真正修的是对齐，不是记号**：他说「人眼很难分辨是 + 还是 −」，而量出来根因是
   这一列右对齐、`−22.52%` 与 `−5.34%` 长度不同，**符号的横坐标每行都在动**。
   所以三角落在一个定宽格里（`0.95em`，跟着字号缩放），**竖着对成一条**；数字仍右对齐。
   ⚠️ 宽度用 `em` 不用 px：这一列被选作排序键时字号从 17 跳到 20，写死 px 那一格就不跟着走。
   ⛔ **▲▼ 不许去掉**：颜色不能是唯一编码（红绿色盲约占男性 1/12）。
   ⛔ **红绿只到这一格为止**：销量本身（累计 / 这一段新增 / 上一段新增 / 总销量占比）
      一律不上色 —— 「销量跌不一定是坏事」那条理由在那几个数上原样成立。 */
/* 🔴 2026-09-22 他看线上：「**你上下的标也太小了，我的老天**」。
   0.62em 在 17px 的字上只有 10.5px —— 而这一格的方向正是靠这个三角编码的
   （颜色不能是唯一编码，红绿色盲约占男性 1/12），**标太小 = 那一半编码等于没有**。
   改成 0.88em（17 → 15px；这一列被选作排序键时 20 → 17.6px），定宽格跟着从 0.95em 抬到 1.15em。
   ⚠️ 定宽格必须跟着抬：格子比字窄时三角会被挤出去，**而挤出去的那一下不报错、只是歪一点**。 */
.boardpage .m .v.grow { display: grid; grid-template-columns: 1.15em auto; justify-content: end; }
.boardpage .m .v.grow .tri { font-size: 0.88em; line-height: inherit; text-align: left; }
/* 🔴 **定宽格只固定了三角与数字之间的距离，没固定三角的位置** —— 数字宽度还在变，
   整块右对齐就把三角推着走。实测左边界 1444 / 1434 / 1455，三条线。
   ⚠️ 这一条是注入之外抓到的：我先画了一张五种画法的对照图，**图里 B / C 两栏也没对齐，
      而我把它当成「对齐了」发了出去** —— 自己造的样张，它不会反驳我。
   ✅ 数字给一个按字符数的定宽，于是数字这一格宽度恒定、三角的左边界跟着恒定。
      **取 8ch**：等宽字体里正好装下线上实测最大的那个 `7383.00%`（board.js 里记着 7d 档
      真出过 `23→1721`）。⚠️ 9ch 也够（`99999.00%`），但**六位数那一列常见值只有六七个字符，
      多出来的那一格会在三角和数字之间空出一条缝** —— 截出来看着是断开的两样东西。
   📌 顺带记一条：他选的那一种（市值榜常见的写法）**本来不靠对齐**（实测那边 `text-align: end`，
      三角同样跟着数字走）—— 颜色把方向变成整格可见，没人再去逐个读符号。
      这里两样都要，是因为对齐不要钱。 */
/* 🔴 2026-09-22 他问「**增长率不是有颜色吗**」—— 而像素量下来**数字是黑的，只有三角有颜色**。
   成因不在这一屏：`pct()` 返回的是 `<b class="n">`，而 `base.css` 给 `b.n` **自己声明了颜色**
   （`--c-provenance-measured`）—— **元素自带的颜色压过继承，与选择器权重无关**。
   于是 `.v.grow.up/.down` 那个红绿只落到没有自带颜色的三角上。
   ✅ 只在这一格解开继承。⛔ 不去动 `base.css` 的 `b.n`：那是全站「这是实测值」那套记号。
   ⚠️ **代价如实记**：这一个数从此不再带那套记号。**而这大概率是个纠正不是损失** ——
      增长率是**算出来的**（两个实测值的比），不是实测值，按这个项目自己的规矩
      （「判『实测 vs 推算』看的是这个数怎么来的」）它本来就不该穿 `b.n` 这身。
      📌 **但「派生值该怎么标」是权威处的事，已单列报产品负责人，这里只解决颜色到不了的问题。** */
.boardpage .m .v.grow .n { min-width: 8ch; text-align: right; display: inline-block; color: inherit; }
.boardpage .m .v.grow.up { color: var(--c-change-up); }
.boardpage .m .v.grow.down { color: var(--c-change-down); }
.boardpage .m .v.tx { font-family: var(--font-sans); font-size: var(--size-md); font-weight: 500; color: var(--c-neutral-800); }
.boardpage .m.k .v.tx { font-size: var(--size-lg); font-weight: 600; }

/* ── 按钮 ──
   🔴 **桌面上四个按钮竖排成一列**（打开链接 · 分享 · 收藏 · 看店铺，DOM 顺序不变）——
   2026-09-28 产品负责人看过「两两一排」与「竖排」两种排法的实际渲染后定的（plan 第 08 章「有中文译名之后的一行」）。
   · 为什么：两两一排时按钮那一列要 124，表最窄 1680；1920 的屏滚动口只有 1632，要横拉 48px
     （Windows 那种一直显示的 17px 滚动条下 65px），**右边那一列「分享 / 看店铺」几乎整列被挡住**。
   · 竖排之后这一列 = 最宽那个按钮（「打开链接」，渲染后实测 66），另把 `.bgrid` 的列间距 10 → 8
     （看不出区别，为那条 17px 的滚动条留地方）：表最窄 1606，1920 不横拉，17px 滚动条下（滚动口 1615）也不横拉。
   · 等宽：只有一列 `auto`，列宽取最宽那个按钮，其余按钮拉满这一列（网格项默认 stretch），所以四个一样宽。
   · 上下：最上面那个的顶 = 商品名的顶、最下面那个的底 = PID 的底，中间两个均分 —— 见「四行字的上下两条线」。
   ⚠️ **竖排那一条只写在下面 min-width: 760px 那一段里**：手机上仍是一横排四格（`repeat(4, 1fr)`），
      ⛔ 一个像素都不许变。这里只放两档共用的骨架。 */
.boardpage .c-act { display: grid; gap: 4px; justify-content: end; }
.boardpage .c-act .btn { text-decoration: none; display: inline-grid; place-items: center; padding: 0 6px; }

/* ── 四行字的上下两条线（2026-09-28 产品负责人定，plan 第 08 章「有中文译名之后的一行」）──
   商品那一格是四行字：英文名 · 中文译名 · 店铺 · PID。从「价格 · 评分」往右，每一列对齐这四行的
   **上线（商品名的顶）与下线（PID 的底）**（他：「店铺评分就需要和 PID 列的底部对齐」）：
   · 价格评分：价格顶 = 商品名顶，店铺评分底 = PID 底，商品评分在两者正中。
     折扣原价那一行在第一格（`.kvi`）里面，跟着价格走 ——
     ⛔ 不要把它挪成 `.c-kv` 的第四个孩子：space-between 会把它和价格摊开。
   · 按钮（竖排一列，09-28 同日定，见「按钮」那一段）：最上面那个的顶 = 商品名顶，最下面那个的底 = PID 底，
     中间两个均分（`align-content: space-between`）。〔同日上午那一版是 2×2：上排顶 = 商品名顶，下排底 = PID 底。〕
   · 只有一个数的那几列：不动，照旧在这一行的正中（`.bgrid` 的 `align-items: center`）。
   · **每小时图不动：40 像素高、在正中。** 拉到四行高的那一版他看过，原话「有点太高了。
     原来那个样式看起来会更加舒服，还原到 40 吧」。⛔ 所以这一段里没有 `.c-chart` —— 不是漏了。
   · **没有中文译名的行**（还没轮到，或被质量闸判掉的标题）：中文那一行**空着**，店铺与 PID 仍在
     第三、四行，PID 的底仍落在下线上，行高与有译名的行相同。
     做法是四条轨道写死：第二条是 `1fr`，有译名时就是译名那一行，没有时空着 —— 两种行同一条规则。
     📌 没用 `:has(.cn)` 把两种行分开写：一条规则管两种行，少一个分支，也不依赖 `:has`。
   ⚠️ 这几条成立的前提：这一行最高的是冻结块，冻结块的高 = 文字块的高 ≈ 图的高（114）。
      谁把别的格子做得比它还高（比如把每小时图拉高），上下线就不再是文字块的线了。
      竖排的四个按钮自己叠起来是 4 × 24 + 3 × 4 = 108，比 114 矮，所以它们不撑高这一行（行高仍是 148）；
      按钮换高一档（比如 `.btn` 默认那一档），它们就会比文字块高。
   ⚠️ **整段只在桌面（≥760px）。** 手机是上下堆叠，没有列可对齐，⛔ 那一档一个像素都不许变。
      写在 min-width 里，而不是写在外面、再到下面手机那一段一条条撤销 ——
      撤销漏一条，手机上没有译名的那张卡就凭空多出一个空行。
   📌 判据在 `build/verify/board_flow.mjs` 的⑲：1920 与 1440 两档逐行量（按钮竖排、等宽、顶底对齐两条线、行高不变），
      手机 375 一档量「没有多出空行」与「按钮仍是一横排四格」；1920 不横拉（含模拟的 17px 常显滚动条）在 ⑧c。 */
@media (min-width: 760px) {
  /* 文字块拉满这一行，四行各占一条轨道。 */
  .boardpage .brow .c-body { align-self: stretch; grid-template-rows: auto 1fr auto auto; }
  .boardpage .brow .c-body .nm { grid-row: 1; }
  .boardpage .brow .c-body .cn { grid-row: 2; }
  .boardpage .brow .c-body .shp { grid-row: 3; }
  .boardpage .brow .c-body .pidl { grid-row: 4; }
  /* 价格评分那一组拉满这一行，三格摊开：第一格贴上线，最后一格贴下线。 */
  .boardpage .brow .c-kv { align-self: stretch; align-content: space-between; }
  /* 按钮那一格同理，而且竖排成一列：最上面那个贴上线，最下面那个贴下线，中间两个均分。 */
  .boardpage .brow .c-act { align-self: stretch; align-content: space-between; grid-template-columns: auto; }
}

/* ── 换位后那一下背景高亮 ──
   plan 第 11 章：「新信号到达只做 240ms 背景高亮淡出」。这里是换位之后留一道痕，
   让人知道**哪几行动过** —— 换位动画本身只说明「有东西在动」，说不出是谁。 */
@keyframes board-just {
  0%, 82% { background-color: var(--c-accent-wash); }
  100% { background-color: var(--c-neutral-0); }
}
.boardpage .brow.just { animation: board-just 1900ms var(--motion-easing-exit) both; }
@media (prefers-reduced-motion: reduce) {
  .boardpage .brow.just { animation: none; background: var(--c-accent-wash); }
}

.boardpage .endnote { text-align: center; font-size: var(--size-sm); color: var(--c-neutral-500); padding: 10px 0 4px; }
/* 「榜单有更新 · 点一下换位」那颗药丸：正在读的时候不换位，换位排队时它出现。 */
.boardpage .updpill { position: absolute; top: 14px; left: 50%; transform: translateX(-50%); z-index: 6; }
.boardpage .pausednote {
  position: absolute; top: 14px; left: 50%; transform: translateX(-50%); z-index: 6;
  font-size: var(--size-sm); color: var(--c-neutral-600);
  background: var(--c-neutral-0); border: 1px solid var(--c-neutral-200);
  border-radius: var(--radius-md); padding: 6px 10px;
}

/* 只给自己看的计时面板（ui/timing.js）：管理员、默认关、不上传。固定在右下角，等宽小字；
   不压色、不挡表格的横向滚动条（离底 16px，比滚动条高）。 */
.boardpage .btiming {
  position: fixed; right: 16px; bottom: 16px; z-index: 40; max-width: min(380px, calc(100vw - 32px));
  display: grid; gap: 4px; padding: 8px 10px;
  font-family: var(--font-mono); font-size: var(--size-xs); color: var(--c-neutral-600);
  background: var(--c-neutral-0); border: 1px solid var(--c-neutral-200); border-radius: var(--radius-md);
  box-shadow: var(--shadow-popover);
}
.boardpage .btiming .bt-head { display: flex; gap: 8px; align-items: baseline; color: var(--c-neutral-800); }
.boardpage .btiming .bt-head .linkbtn { margin-left: auto; }
.boardpage .btiming .bt-log { border-collapse: collapse; }
.boardpage .btiming .bt-log td { padding: 1px 8px 1px 0; white-space: nowrap; }
.boardpage .btiming .bt-log td.r { text-align: right; font-variant-numeric: tabular-nums; }

/* 榜单还在算（`state: "warming"`）与空榜是两句话，不是同一屏 —— 见 board.js。 */
.boardpage .bwarming { padding: 32px 18px; text-align: center; color: var(--c-neutral-600); }

/* ── 手机：这里才堆叠，桌面永远不换行 ──
   断点 759 来自 plan 第 08 章「手机（≤759 像素）改成上下堆叠」。 */
@media (max-width: 759px) {
  .boardpage .btable { padding-inline: 12px; }
  .boardpage .bfilters { padding-inline: 12px; }
  /* 🔴 控件排装不下时**换行**，⛔ 不许横向切掉。
     `.seg` 在 components 那一侧是 `overflow: hidden` 的定宽胶囊，七个时间档在 390px 上
     会被齐刷刷切掉右边三个 —— 实测「近 48 小时 / 近 72 小时 / 近 7 天」**点不到**，
     而屏幕上看不出它们存在过。这与「手机上靠纵向堆叠实现信息全面，不靠删字段」是同一条。
     ⚠️ 解法照搬 `s-signals.css` 那一屏已经在用的那套（同样的 `flex-wrap` + 撑到触达下限），
     不另造一套 —— 整个产品对「控件排装不下」只该有一种做法。 */
  .boardpage .seg { flex-wrap: wrap; }
  .boardpage .seg .seg-b { flex: 1 1 auto; justify-content: center; min-height: var(--a11y-floor-target-used); }
  .boardpage .opt { height: auto; min-height: var(--a11y-floor-target-used); }
  .boardpage .mt { width: auto; flex: 1 0 100%; }
  .boardpage .bscroll { overflow-x: hidden; }
  .boardpage .binner { min-width: 0; width: auto; }
  .boardpage .bhead { display: none; }
  .boardpage .bgrid { grid-template-columns: minmax(0, 1fr); row-gap: var(--space-2); }
  .boardpage .brow { padding: 10px; }
  .boardpage .cfreeze {
    position: static; padding-left: 0; box-shadow: none;
    grid-template-columns: 40px 52px minmax(0, 1fr); row-gap: var(--space-2);
  }
  .boardpage .c-kv {
    grid-column: 1 / -1; border-left: 0; padding-left: 0;
    grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-2) 6px;
  }
  .boardpage .kvi { grid-template-columns: minmax(0, 1fr); justify-items: start; align-content: start; }
  .boardpage .kl { font-size: var(--size-9); }
  .boardpage .kvv { justify-self: start; font-size: var(--size-sm); }
  .boardpage .dis { justify-self: start; white-space: normal; }
  .boardpage .rk { font-size: var(--size-xl); }
  .boardpage .c-thumb .thumb { width: 52px; height: 52px; }
  .boardpage .nm, .boardpage .shp { white-space: normal; }
  /* 堆叠之后每个数字要自报是什么 —— 表头没了，`.ml` 就是它唯一的标签。
     ⚠️ 这不违反「一列一个数、没有附属小字」：桌面上那个标签在表头里，
        手机上表头整条不画，标签只是**挪了个位置**，不是新加的小字。 */
  .boardpage .c-nums {
    display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-2) 6px;
    border-top: 1px solid var(--c-neutral-100); padding-top: var(--space-2);
    align-items: end;
  }
  .boardpage .m { justify-items: start; }
  .boardpage .m .ml { display: block; white-space: normal; word-break: keep-all; line-height: 1.3; }
  .boardpage .m .v { font-size: var(--size-sm); }
  .boardpage .m.k .v { font-size: var(--size-md); }
  .boardpage .c-nums .c-chart { grid-column: 1 / -1; order: 9; gap: 3px; }
  .boardpage .c-chart .ml { display: block; font-size: var(--size-9); color: var(--c-neutral-500); }
  .boardpage .c-chart svg { height: 44px; }
  .boardpage .c-act { grid-template-columns: repeat(4, 1fr); justify-content: stretch; }
  .boardpage .c-act .btn { padding: 0 4px; }
}

/* ═══ 二期第二步：回看某一天（2026-10-01；判据 plan 第 08 章「二期」，样子照 09-30 那张效果图 ——
   他看过说「先暂时这样」，以后随整站设计重构一起调）。逻辑在 screens/board-days.js。
   ⚠️ 宽屏那一套靠容器查询，**只在回看时给页面开**（`.p2-dayon`）：容器会让页面成为里面 fixed 元素的定位框
      （右下角那个计时面板），最新那几档一个字都不动，就不给它开。 ═══ */
.screen > .page.boardpage.p2-dayon { container-type: inline-size; container-name: board; }

/* 时间范围那一行的回看半边：‹ 某月某日 周几 ▾ › */
.boardpage .p2-date { display: inline-flex; align-items: center; gap: 4px; position: relative; }
.boardpage .p2-step,
.boardpage .p2-mon {
  width: 26px; height: 26px; padding: 0; display: inline-grid; place-items: center;
  border: 1px solid var(--c-neutral-200); border-radius: var(--radius-md); background: transparent;
  color: var(--c-neutral-800); font: inherit; font-size: var(--size-md); line-height: 1; cursor: pointer;
}
.boardpage .p2-step:hover,
.boardpage .p2-mon:hover { background: var(--c-neutral-50); }
.boardpage .p2-step[disabled],
.boardpage .p2-mon[disabled] { color: var(--c-neutral-400); cursor: default; background: transparent; }
.boardpage .p2-pick {
  height: 26px; min-width: 136px; padding: 0 10px; display: inline-flex; align-items: center;
  justify-content: center; gap: 6px; border: 1px solid var(--c-neutral-200); border-radius: var(--radius-md);
  background: var(--c-neutral-100); color: var(--c-neutral-900); font: inherit; font-size: var(--size-sm);
  font-weight: 600; cursor: pointer; white-space: nowrap;
}
.boardpage .p2-pick .wd { font-weight: 400; color: var(--c-neutral-600); }
.boardpage .p2-pick .car { font-size: var(--size-9); color: var(--c-neutral-500); }
.boardpage .p2-cal {
  position: absolute; top: 32px; left: 30px; z-index: 30; width: 252px; padding: 10px 12px;
  background: var(--c-neutral-0); border: 1px solid var(--c-neutral-200); border-radius: var(--radius-lg);
  box-shadow: var(--shadow-popover);
}
.boardpage .p2-cal .hd {
  display: flex; align-items: center; justify-content: space-between;
  font-size: var(--size-sm); font-weight: 600; margin-bottom: 6px;
}
.boardpage .p2-cal .grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
.boardpage .p2-cal .wk { font-size: var(--size-9); color: var(--c-neutral-500); text-align: center; padding: 2px 0; }
.boardpage .p2-cal .d {
  height: 30px; padding: 0; border: 0; border-radius: var(--radius-sm); background: transparent;
  font: inherit; font-family: var(--font-mono); font-size: var(--size-sm); color: var(--c-neutral-900); cursor: pointer;
}
.boardpage .p2-cal .d:hover { background: var(--c-neutral-50); }
.boardpage .p2-cal .d[disabled] { color: var(--c-neutral-400); cursor: default; background: transparent; }
.boardpage .p2-cal .d.on { background: var(--c-neutral-900); color: var(--c-neutral-0); }
.boardpage .p2-cal .ft { margin-top: 6px; font-size: var(--size-11); color: var(--c-neutral-500); }
.boardpage .p2-focus:focus-visible, .boardpage .p2-cal .d:focus-visible, .boardpage .p2-step:focus-visible,
.boardpage .p2-mon:focus-visible, .boardpage .p2-pick:focus-visible, .boardpage .p2-hd:focus-visible {
  outline: var(--a11y-floor-focus-ring) solid var(--a11y-floor-focus-color); outline-offset: 1px;
}

/* 当日时段：一天 23 / 24 / 25 根柱子（列数由脚本按那一天写），按住拖选一段 */
.boardpage .p2-day { display: grid; gap: 4px; min-width: 0; max-width: 760px; }
.boardpage .p2-dayhead { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); min-height: 26px; }
.boardpage .p2-range { font-family: var(--font-mono); font-size: var(--size-base); font-weight: 600; color: var(--c-neutral-900); }
.boardpage .p2-len { font-size: var(--size-sm); color: var(--c-neutral-500); }
/* 读数那一行单独占一行（效果图里它跟在表头那一行右边）：🔴 跟在同一行时，悬停一根柱子读数变短、换行没了，
   整块柱子往上跳一行 —— 按下去的那一下就落在柱子外面（验收台 board_days_flow ⑤ 抓到的）。单独一行，长短怎么变都不挪柱子。 */
.boardpage .p2-read {
  flex: 1 0 100%; text-align: right; font-size: var(--size-11); color: var(--c-neutral-600);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-height: 1.4em;
}
.boardpage .p2-read b { font-family: var(--font-mono); font-weight: 600; color: var(--c-neutral-900); }
.boardpage .p2-toggle { display: none; }
.boardpage .p2-chart {
  position: relative; display: grid; grid-template-columns: repeat(24, minmax(0, 1fr)); column-gap: 3px;
  height: 50px; padding-top: 4px; border-bottom: 1px solid var(--c-neutral-200); touch-action: pan-y;
  user-select: none; -webkit-user-select: none; cursor: crosshair;
}
.boardpage .p2-col { grid-row: 1; position: relative; display: flex; align-items: flex-end; z-index: 1; }
.boardpage .p2-col .bar { display: block; width: 100%; min-height: 2px; border-radius: 2px 2px 0 0; background: var(--c-neutral-200); }
.boardpage .p2-col.sel .bar { background: var(--c-neutral-900); }
/* 虚线 = 前一天同一钟点 */
.boardpage .p2-col .prev { position: absolute; left: -1.5px; right: -1.5px; height: 0; border-top: 1.5px dashed var(--c-neutral-500); }
/* 斜纹 = 没采到（不画成 0）；虚框 = 这一小时还没过完 */
.boardpage .p2-col.gap .bar {
  height: 100%; border-radius: 0;
  background: repeating-linear-gradient(135deg, var(--c-provenance-missing-hatch-chart) 0 3px, transparent 3px 7px);
}
.boardpage .p2-col.fut .bar {
  height: 100%; box-sizing: border-box; background: transparent;
  border: 1px dashed var(--c-neutral-200); border-bottom: 0;
}
.boardpage .p2-sel {
  grid-row: 1; position: relative; z-index: 0; margin: -4px -3px 0; background: var(--c-accent-wash);
  border-inline: 2px solid var(--c-accent-base); border-radius: 3px; pointer-events: none;
}
.boardpage .p2-sel[hidden] { display: none; }
.boardpage .p2-hd {
  position: absolute; top: 50%; width: 10px; height: 22px; margin-top: -11px; box-sizing: border-box; padding: 0;
  border: 2px solid var(--c-neutral-0); border-radius: 5px; background: var(--c-accent-base);
  pointer-events: auto; cursor: ew-resize; z-index: 3;
}
.boardpage .p2-hd.l { left: -6px; }
.boardpage .p2-hd.r { right: -6px; }
/* 横轴：刻度按整点的位置摆（23 / 25 小时那天也对得上柱子），首尾两个不出界 */
.boardpage .p2-axis { position: relative; height: 14px; font-family: var(--font-mono); font-size: var(--size-9); color: var(--c-neutral-500); }
.boardpage .p2-axis span { position: absolute; top: 0; transform: translateX(-50%); }
.boardpage .p2-axis span:first-child { transform: none; }
.boardpage .p2-axis span:last-child { transform: translateX(-100%); }
.boardpage .p2-selects { display: none; align-items: center; gap: 6px; font-size: var(--size-sm); color: var(--c-neutral-600); }
.boardpage .p2-selects select {
  height: var(--a11y-floor-target-used); padding: 0 6px; border: 1px solid var(--border-control);
  border-radius: var(--radius-sm); background: var(--c-neutral-0); color: var(--c-neutral-900); font: inherit; font-size: var(--size-sm);
}

/* 评分那两格的「现在」：那一刻没有记录，写的是现在的分（写法同中文名前面那个「译」） */
.boardpage .p2-now {
  display: inline-block; border: 1px solid var(--c-neutral-200); border-radius: var(--radius-sm);
  font-family: var(--font-sans); font-size: var(--size-xs); font-weight: 400; line-height: 16px; color: var(--c-neutral-600);
  padding: 0 4px; margin-right: 5px; vertical-align: 1px;
}

/* 宽屏：当日时段放进控件区右侧原本空着的地方，不占竖向高度。
   1500 = 左边那一排控件不折行要的 980 + 留白 40 + 这一块最窄 440 + 两侧内边距 36（效果图量的） */
@container board (min-width: 1500px) {
  .boardpage .bfilters.has-day {
    display: grid; grid-template-columns: minmax(980px, 1fr) minmax(440px, 760px);
    column-gap: 40px; row-gap: var(--space-2); align-items: start;
  }
  .boardpage .bfilters.has-day > * { grid-column: 1; }
  .boardpage .bfilters.has-day > .p2-day { grid-column: 2; grid-row: 1 / span 3; width: 100%; justify-self: end; }
  .boardpage .bfilters.has-day > .p2-day .p2-chart { height: 46px; }
}

/* 手机：当日时段默认收起，点「选几个小时」展开，另有起止两个下拉（手指拖不准时用） */
@media (max-width: 759px) {
  .boardpage .p2-date { width: 100%; }
  .boardpage .p2-pick { flex: 1; height: var(--a11y-floor-target-used); }
  .boardpage .p2-step { width: var(--a11y-floor-target-used); height: var(--a11y-floor-target-used); }
  .boardpage .p2-cal { left: 0; }
  .boardpage .p2-day { max-width: none; }
  .boardpage .p2-read { text-align: left; white-space: normal; }
  .boardpage .p2-chart { height: 56px; column-gap: 2px; }
  .boardpage .p2-toggle { display: inline-flex; }
  .boardpage .p2-day:not(.open) .p2-chart,
  .boardpage .p2-day:not(.open) .p2-axis,
  .boardpage .p2-day:not(.open) .p2-read { display: none; }
  .boardpage .p2-day.open .p2-selects { display: flex; }
  .boardpage .p2-hd { width: 14px; height: 28px; margin-top: -14px; }
  .boardpage .p2-hd.l { left: -8px; }
  .boardpage .p2-hd.r { right: -8px; }
}

/* ═══ 二期第三步：名次走势（2026-10-03；判据 plan 第 08 章「二期」，样子照 09-30 那张效果图）。逻辑在 ui/rank-trend.js。
   趋势榜点名次展开的那一块（`.p2-hist`）+ 那张图本身（`.rt-*`）。⚠️ `.rt-*` 不带 `.boardpage`：商品详情页那张卡
   （screens/product.js 的 `.pd-rank`）用的是同一张图、同一份样子。没上榜那几天的斜纹走 ui/chart.js 的 `hatchDefs`。 ═══ */
.boardpage .brow .c-rank.p2-rankbtn { cursor: pointer; border-radius: var(--radius-md); }
.boardpage .brow .c-rank.p2-rankbtn:hover { background: var(--c-neutral-100); }
.boardpage .brow.p2-open { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
.boardpage .p2-hist { margin-top: -7px; border: 1px solid var(--c-neutral-100); border-top: 1px dashed var(--c-neutral-200);
  border-radius: 0 0 var(--radius-lg) var(--radius-lg); background: var(--c-neutral-25); --rt-bg: var(--c-neutral-25); }
/* 表比窗口宽时钉在看得见的那一截里（`--p2-vw` = 横向滚动那一层的可见宽，board.js 量） */
.boardpage .p2-hist-in { position: sticky; left: 0; box-sizing: border-box; width: min(1040px, var(--p2-vw, 100%));
  padding: 12px 18px 14px 60px; }
.pd-rank { --rt-bg: var(--c-neutral-0); }

.rt-box { display: grid; gap: 6px; min-width: 0; }
.rt-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 12px; font-size: var(--size-sm); color: var(--c-neutral-600); }
.rt-head:empty { display: none; }
.rt-head b { font-size: var(--size-base); color: var(--c-neutral-900); }
.rt-head .s { font-family: var(--font-mono); color: var(--c-neutral-900); }
.rt-keep { white-space: nowrap; }          /* 说明里的一段不从中间折（ui/rank-trend.js `keep`），详情页那张卡的副标题也用 */
.rt-plot { min-width: 0; }
.rt-plot svg { display: block; max-width: 100%; height: auto; overflow: visible; }
.rt-foot { font-size: var(--size-11); color: var(--c-neutral-500); }
.rt-none, .rt-wait, .rt-err { margin: 0; font-size: var(--size-sm); color: var(--c-neutral-600); }
.rt-grid { stroke: var(--c-neutral-100); stroke-width: 1; }
.rt-ax { font-size: 12px; font-family: var(--font-mono); fill: var(--c-neutral-500); }
.rt-line { fill: none; stroke: var(--c-neutral-900); stroke-width: 2; stroke-linejoin: round; }
.rt-bandbg { fill: var(--rt-bg, var(--c-neutral-0)); }
.rt-bandtx { font-size: 12px; fill: var(--c-neutral-600); }
.rt-hit { fill: transparent; outline: none; }       /* 一列一天的命中面：键盘也停得到（ui/rank-trend.js）。⛔ 浏览器自带的焦点框（点一下就出一个蓝框） */
.rt-hit:focus-visible { stroke: var(--c-accent-base); stroke-width: 2; }      /* 键盘停在没名次的那一天：与点的焦点同一个颜色 */
.rt-pt { cursor: pointer; outline: none; }
.rt-ptzone { fill: transparent; }
.rt-ptdot { fill: var(--c-neutral-0); stroke: var(--c-neutral-900); stroke-width: 2; }
.rt-pt.on .rt-ptdot { fill: var(--c-accent-base); stroke: var(--c-accent-base); }
.rt-pt:focus-visible .rt-ptzone { stroke: var(--c-accent-base); stroke-width: 2; }

@media (max-width: 759px) {
  .boardpage .p2-hist-in { position: static; width: auto; padding: 10px; }
}

/* ═══ 二期第四步：按店铺（2026-10-03；判据 plan 第 08 章「二期」，样子照 09-30 那张效果图）。逻辑在 screens/board.js 的「按店铺」一节。
   ⚠️ 一格只有一个数，数下面不加小字（2026-10-07 产品负责人看截图后定）：这一格没加进去的品、比不了时是哪一种
      挂在那一格的 title 上，规则在页尾「怎么算的」。 ═══ */
/* 宽度：最窄 975 = 八列下限 904 + 七个列间距 56 + 行右内边距 13 + 两条边框 2 —— 1280 的笔记本（表格口约 992，滚动条一直显示的系统上约 975）不横拉。
   改任何一列的下限都要跟着改 `.binner.p2-shopmode` 的 min-width（两处说的是同一个数）。
   更窄时（平板横放之类）表格横着滚，店那一格照商品那一张的冻结块钉在左边。 */
.boardpage .binner.p2-shopmode { min-width: 975px; }
.boardpage .bgrid.p2-shopgrid { grid-template-columns: minmax(170px, 420px) 64px 104px 120px minmax(118px, 170px) 104px 84px minmax(140px, 420px); }
.boardpage .p2-shophead {
  display: grid; grid-template-columns: 46px minmax(0, 1fr); align-items: center; column-gap: 14px; padding-left: 14px; min-width: 0;
  position: sticky; left: 0; z-index: 2; background-color: inherit; align-self: stretch;
}
.boardpage .bhead .p2-shophead { align-items: end; background: var(--c-neutral-0); z-index: 5; }
.boardpage .p2-sum { display: inline-flex; align-items: center; gap: var(--space-2); }
.boardpage .p2-shophead .c-body .nm { font-size: var(--size-base); font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.boardpage .p2-topprod { min-width: 0; display: grid; gap: 2px; padding-right: 14px; }
.boardpage .p2-topprod .nm { font-size: var(--size-base); font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: inherit; text-decoration: none; }
.boardpage .p2-topprod .nm:hover { text-decoration: underline; }
.boardpage .p2-topprod .ml { display: none; font-size: var(--size-9); color: var(--c-neutral-500); }
.boardpage .p2-topprod .v { font-family: var(--font-mono); font-size: var(--size-sm); color: var(--c-neutral-600); }

@media (max-width: 759px) {
  .boardpage .binner.p2-shopmode { min-width: 0; }
  /* ⚠️ 上面那一行（三个类）比手机那一段的 `.boardpage .bgrid`（两个类）重：这里不重写一遍，手机上照旧排八列、挤出卡外 */
  .boardpage .bgrid.p2-shopgrid { grid-template-columns: minmax(0, 1fr); }
  .boardpage .p2-shophead { padding-left: 0; grid-template-columns: 40px minmax(0, 1fr); position: static; }
  .boardpage .p2-sum { flex: 1 0 100%; flex-wrap: wrap; }
  .boardpage .p2-shophead .c-body .nm { white-space: normal; }
  /* 手机上一格只有约 70 像素：增长率那个数不留桌面上对齐用的 8 位宽（这里没有一整列要对齐），⛔ 不顶到右边那一格 */
  .boardpage .p2-shopgrid .m-grow .v.grow .n { min-width: 0; }
  .boardpage .p2-topprod { padding-right: 0; border-top: 1px solid var(--c-neutral-100); padding-top: var(--space-2); }
  .boardpage .p2-topprod .ml { display: block; }
  .boardpage .p2-topprod .nm { white-space: normal; }
  /* 上面那一行的两句都长，手机上各占一行：中间那条竖线会单独落在一行上，不画 */
  .boardpage .p2-shopcover .sepv { display: none; }
}
