Эх сурвалжийг харах

fix(v13-三): view-pool 补 CARD_CSS(方案 A),页脚类名全部命中且与报告中心逐项一致;删掉抄写的页脚规则

- view-pool.js: import CARD_CSS + static styles;页脚补 .cfa 外层,只保留排序行内两处几何(左分隔线、纵向内衬归零)
- verify-visual-v13.mjs: 三项实测 + v12 回归,任一项不达标 exit 1
- docs/v13-report.md: 三项实测值、根因、方案理由、豁免说明
FmodeCode 5 өдөр өмнө
parent
commit
89370504bb

+ 1194 - 0
docs/v13-after.json

@@ -0,0 +1,1194 @@
+{
+  "tag": "after",
+  "at": "2026-10-30T11:51:03.473Z",
+  "one": {
+    "btns": [
+      {
+        "text": "优先级 S→A→B",
+        "pressed": "true",
+        "bg": "rgba(0, 0, 0, 0.06)",
+        "color": "rgb(24, 24, 27)",
+        "fontSize": "12px",
+        "lineHeight": "12px",
+        "h": 28,
+        "radius": "0px 0px",
+        "borderLeft": "0px"
+      },
+      {
+        "text": "综合评分 高→低",
+        "pressed": "false",
+        "bg": "rgba(0, 0, 0, 0)",
+        "color": "rgb(63, 63, 70)",
+        "fontSize": "12px",
+        "lineHeight": "12px",
+        "h": 28,
+        "radius": "0px 0px",
+        "borderLeft": "1px"
+      },
+      {
+        "text": "证据强度 高→低",
+        "pressed": "false",
+        "bg": "rgba(0, 0, 0, 0)",
+        "color": "rgb(63, 63, 70)",
+        "fontSize": "12px",
+        "lineHeight": "12px",
+        "h": 28,
+        "radius": "0px 0px",
+        "borderLeft": "1px"
+      },
+      {
+        "text": "最新上传",
+        "pressed": "false",
+        "bg": "rgba(0, 0, 0, 0)",
+        "color": "rgb(63, 63, 70)",
+        "fontSize": "12px",
+        "lineHeight": "12px",
+        "h": 28,
+        "radius": "0px 0px",
+        "borderLeft": "1px"
+      },
+      {
+        "text": "分歧最大",
+        "pressed": "false",
+        "bg": "rgba(0, 0, 0, 0)",
+        "color": "rgb(63, 63, 70)",
+        "fontSize": "12px",
+        "lineHeight": "12px",
+        "h": 28,
+        "radius": "0px 0px",
+        "borderLeft": "1px"
+      }
+    ],
+    "seps": [
+      {
+        "i": 0,
+        "left": "0px",
+        "leftStyle": "none",
+        "leftColor": "rgb(24, 24, 27)"
+      },
+      {
+        "i": 1,
+        "left": "1px",
+        "leftStyle": "solid",
+        "leftColor": "rgba(0, 0, 0, 0.1)"
+      },
+      {
+        "i": 2,
+        "left": "1px",
+        "leftStyle": "solid",
+        "leftColor": "rgba(0, 0, 0, 0.1)"
+      },
+      {
+        "i": 3,
+        "left": "1px",
+        "leftStyle": "solid",
+        "leftColor": "rgba(0, 0, 0, 0.1)"
+      },
+      {
+        "i": 4,
+        "left": "1px",
+        "leftStyle": "solid",
+        "leftColor": "rgba(0, 0, 0, 0.1)"
+      }
+    ],
+    "outer": {
+      "radius": "8px",
+      "border": "1px solid rgba(0, 0, 0, 0.1)",
+      "h": 28,
+      "bg": "rgba(0, 0, 0, 0)"
+    },
+    "selected": {
+      "text": "优先级 S→A→B",
+      "pressed": "true",
+      "bg": "rgba(0, 0, 0, 0.06)",
+      "color": "rgb(24, 24, 27)",
+      "fontSize": "12px",
+      "lineHeight": "12px",
+      "h": 28,
+      "radius": "0px 0px",
+      "borderLeft": "0px"
+    },
+    "unselected": [
+      {
+        "text": "综合评分 高→低",
+        "pressed": "false",
+        "bg": "rgba(0, 0, 0, 0)",
+        "color": "rgb(63, 63, 70)",
+        "fontSize": "12px",
+        "lineHeight": "12px",
+        "h": 28,
+        "radius": "0px 0px",
+        "borderLeft": "1px"
+      },
+      {
+        "text": "证据强度 高→低",
+        "pressed": "false",
+        "bg": "rgba(0, 0, 0, 0)",
+        "color": "rgb(63, 63, 70)",
+        "fontSize": "12px",
+        "lineHeight": "12px",
+        "h": 28,
+        "radius": "0px 0px",
+        "borderLeft": "1px"
+      },
+      {
+        "text": "最新上传",
+        "pressed": "false",
+        "bg": "rgba(0, 0, 0, 0)",
+        "color": "rgb(63, 63, 70)",
+        "fontSize": "12px",
+        "lineHeight": "12px",
+        "h": 28,
+        "radius": "0px 0px",
+        "borderLeft": "1px"
+      },
+      {
+        "text": "分歧最大",
+        "pressed": "false",
+        "bg": "rgba(0, 0, 0, 0)",
+        "color": "rgb(63, 63, 70)",
+        "fontSize": "12px",
+        "lineHeight": "12px",
+        "h": 28,
+        "radius": "0px 0px",
+        "borderLeft": "1px"
+      }
+    ],
+    "selColorRule": "rgb(24, 24, 27)",
+    "wrapTop": 444.34375,
+    "mobile": {
+      "text": "优先级 S→A→B",
+      "bg": "rgba(0, 0, 0, 0.06)",
+      "color": "rgb(24, 24, 27)",
+      "fontSize": "13px",
+      "lineHeight": "normal"
+    }
+  },
+  "two": {
+    "pages": [
+      {
+        "page": "/reports",
+        "n": 12,
+        "cards": [
+          {
+            "title": "夏日清爽凉拌面",
+            "lead": "夏季 6–8 月拌面搜索",
+            "leadTxt": "夏季 6–8 月拌面搜索 +35%,但现有产品多为「热拌」,冷拌 / 冰镇吃法没有对应 SKU",
+            "gapTitleToM": 66,
+            "contentH": 252,
+            "rcH": 252,
+            "faceH": 284,
+            "cardH": 284,
+            "maxGap": 0,
+            "gapAt": "",
+            "rowIdx": 719
+          },
+          {
+            "title": "夏日清爽凉拌面",
+            "lead": "夏季 6–8 月拌面搜索",
+            "leadTxt": "夏季 6–8 月拌面搜索 +35%,但现有产品多为「热拌」,冷拌 / 冰镇吃法没有对应 SKU",
+            "gapTitleToM": 66,
+            "contentH": 252,
+            "rcH": 252,
+            "faceH": 284,
+            "cardH": 284,
+            "maxGap": 0,
+            "gapAt": "",
+            "rowIdx": 719
+          },
+          {
+            "title": "露营便携免煮拌面",
+            "lead": "露营带的都是泡面,想吃拌",
+            "leadTxt": "露营带的都是泡面,想吃拌面又得煮:冷水泡 8 分钟、自带拌酱的免煮方案",
+            "gapTitleToM": 66,
+            "contentH": 252,
+            "rcH": 252,
+            "faceH": 284,
+            "cardH": 284,
+            "maxGap": 0,
+            "gapAt": "",
+            "rowIdx": 719
+          },
+          {
+            "title": "露营便携免煮拌面",
+            "lead": "露营带的都是泡面,想吃拌",
+            "leadTxt": "露营带的都是泡面,想吃拌面又得煮:冷水泡 8 分钟、自带拌酱的免煮方案",
+            "gapTitleToM": 66,
+            "contentH": 252,
+            "rcH": 252,
+            "faceH": 284,
+            "cardH": 284,
+            "maxGap": 0,
+            "gapAt": "",
+            "rowIdx": 1015
+          },
+          {
+            "title": "拌面品类市场概览(骨架样例)",
+            "lead": "原材料层样例:字段未定,",
+            "leadTxt": "原材料层样例:字段未定,业务内容放 payload。",
+            "gapTitleToM": 45,
+            "contentH": 231,
+            "rcH": 231,
+            "faceH": 284,
+            "cardH": 284,
+            "maxGap": 0,
+            "gapAt": "",
+            "rowIdx": 1015
+          },
+          {
+            "title": "“老年人更爱汤面”推断",
+            "lead": "分身据常识推断,学员未提",
+            "leadTxt": "分身据常识推断,学员未提供访谈原文。",
+            "gapTitleToM": 45,
+            "contentH": 231,
+            "rcH": 231,
+            "faceH": 284,
+            "cardH": 284,
+            "maxGap": 0,
+            "gapAt": "",
+            "rowIdx": 1015
+          },
+          {
+            "title": "夜宵低卡拌面机会卡(与张伟重",
+            "lead": "与已有机会卡高度重复,无",
+            "leadTxt": "与已有机会卡高度重复,无新增证据。",
+            "gapTitleToM": 45,
+            "contentH": 231,
+            "rcH": 231,
+            "faceH": 263,
+            "cardH": 263,
+            "maxGap": 0,
+            "gapAt": "",
+            "rowIdx": 1311
+          },
+          {
+            "title": "国潮联名拌面",
+            "lead": null,
+            "leadTxt": "",
+            "gapTitleToM": 12,
+            "contentH": 257,
+            "rcH": 199,
+            "faceH": 263,
+            "cardH": 263,
+            "maxGap": 0,
+            "gapAt": "",
+            "rowIdx": 1311
+          },
+          {
+            "title": "国潮联名拌面",
+            "lead": null,
+            "leadTxt": "",
+            "gapTitleToM": 12,
+            "contentH": 257,
+            "rcH": 199,
+            "faceH": 263,
+            "cardH": 263,
+            "maxGap": 0,
+            "gapAt": "",
+            "rowIdx": 1311
+          },
+          {
+            "title": "国潮联名拌面",
+            "lead": null,
+            "leadTxt": "",
+            "gapTitleToM": 12,
+            "contentH": 257,
+            "rcH": 199,
+            "faceH": 231,
+            "cardH": 231,
+            "maxGap": 0,
+            "gapAt": "",
+            "rowIdx": 1587
+          },
+          {
+            "title": "早餐快速汤面",
+            "lead": null,
+            "leadTxt": "",
+            "gapTitleToM": 12,
+            "contentH": 257,
+            "rcH": 199,
+            "faceH": 231,
+            "cardH": 231,
+            "maxGap": 0,
+            "gapAt": "",
+            "rowIdx": 1587
+          },
+          {
+            "title": "早餐快速汤面",
+            "lead": null,
+            "leadTxt": "",
+            "gapTitleToM": 12,
+            "contentH": 257,
+            "rcH": 199,
+            "faceH": 231,
+            "cardH": 231,
+            "maxGap": 0,
+            "gapAt": "",
+            "rowIdx": 1587
+          }
+        ]
+      },
+      {
+        "page": "/stages",
+        "n": 35,
+        "cards": [
+          {
+            "title": "夏日清爽凉拌面",
+            "lead": "夏季 6–8 月拌面搜索",
+            "leadTxt": "夏季 6–8 月拌面搜索 +35%,但现有产品多为「热拌」,冷拌 / 冰镇吃法没有对应 SKU",
+            "gapTitleToM": 66,
+            "contentH": 268,
+            "rcH": 268,
+            "faceH": 300,
+            "cardH": 300,
+            "maxGap": 0,
+            "gapAt": "",
+            "rowIdx": 311
+          },
+          {
+            "title": "露营便携免煮拌面",
+            "lead": "露营带的都是泡面,想吃拌",
+            "leadTxt": "露营带的都是泡面,想吃拌面又得煮:冷水泡 8 分钟、自带拌酱的免煮方案",
+            "gapTitleToM": 66,
+            "contentH": 268,
+            "rcH": 268,
+            "faceH": 300,
+            "cardH": 300,
+            "maxGap": 0,
+            "gapAt": "",
+            "rowIdx": 311
+          },
+          {
+            "title": "“老年人更爱汤面”推断",
+            "lead": "分身据常识推断,学员未提",
+            "leadTxt": "分身据常识推断,学员未提供访谈原文。",
+            "gapTitleToM": 45,
+            "contentH": 247,
+            "rcH": 247,
+            "faceH": 300,
+            "cardH": 300,
+            "maxGap": 0,
+            "gapAt": "",
+            "rowIdx": 311
+          },
+          {
+            "title": "夜宵低卡拌面机会卡(与张伟重",
+            "lead": "与已有机会卡高度重复,无",
+            "leadTxt": "与已有机会卡高度重复,无新增证据。",
+            "gapTitleToM": 45,
+            "contentH": 247,
+            "rcH": 247,
+            "faceH": 279,
+            "cardH": 279,
+            "maxGap": 0,
+            "gapAt": "",
+            "rowIdx": 623
+          },
+          {
+            "title": "国潮联名拌面",
+            "lead": null,
+            "leadTxt": "",
+            "gapTitleToM": 12,
+            "contentH": 284,
+            "rcH": 214,
+            "faceH": 279,
+            "cardH": 279,
+            "maxGap": 0,
+            "gapAt": "",
+            "rowIdx": 623
+          },
+          {
+            "title": "国潮联名拌面",
+            "lead": null,
+            "leadTxt": "",
+            "gapTitleToM": 12,
+            "contentH": 284,
+            "rcH": 214,
+            "faceH": 279,
+            "cardH": 279,
+            "maxGap": 0,
+            "gapAt": "",
+            "rowIdx": 623
+          },
+          {
+            "title": "国潮联名拌面",
+            "lead": null,
+            "leadTxt": "",
+            "gapTitleToM": 12,
+            "contentH": 284,
+            "rcH": 214,
+            "faceH": 246,
+            "cardH": 246,
+            "maxGap": 0,
+            "gapAt": "",
+            "rowIdx": 914
+          },
+          {
+            "title": "早餐快速汤面",
+            "lead": null,
+            "leadTxt": "",
+            "gapTitleToM": 12,
+            "contentH": 284,
+            "rcH": 214,
+            "faceH": 246,
+            "cardH": 246,
+            "maxGap": 0,
+            "gapAt": "",
+            "rowIdx": 914
+          },
+          {
+            "title": "早餐快速汤面",
+            "lead": null,
+            "leadTxt": "",
+            "gapTitleToM": 12,
+            "contentH": 284,
+            "rcH": 214,
+            "faceH": 246,
+            "cardH": 246,
+            "maxGap": 0,
+            "gapAt": "",
+            "rowIdx": 914
+          },
+          {
+            "title": "早餐快速汤面",
+            "lead": null,
+            "leadTxt": "",
+            "gapTitleToM": 12,
+            "contentH": 284,
+            "rcH": 214,
+            "faceH": 246,
+            "cardH": 246,
+            "maxGap": 0,
+            "gapAt": "",
+            "rowIdx": 1172
+          },
+          {
+            "title": "夏日清爽凉拌面",
+            "lead": null,
+            "leadTxt": "",
+            "gapTitleToM": 12,
+            "contentH": 284,
+            "rcH": 214,
+            "faceH": 246,
+            "cardH": 246,
+            "maxGap": 0,
+            "gapAt": "",
+            "rowIdx": 1172
+          },
+          {
+            "title": "夏日清爽凉拌面",
+            "lead": null,
+            "leadTxt": "",
+            "gapTitleToM": 12,
+            "contentH": 284,
+            "rcH": 214,
+            "faceH": 246,
+            "cardH": 246,
+            "maxGap": 0,
+            "gapAt": "",
+            "rowIdx": 1172
+          },
+          {
+            "title": "健身人群高蛋白面",
+            "lead": null,
+            "leadTxt": "",
+            "gapTitleToM": 12,
+            "contentH": 284,
+            "rcH": 214,
+            "faceH": 246,
+            "cardH": 246,
+            "maxGap": 0,
+            "gapAt": "",
+            "rowIdx": 1431
+          },
+          {
+            "title": "健身人群高蛋白面",
+            "lead": null,
+            "leadTxt": "",
+            "gapTitleToM": 12,
+            "contentH": 284,
+            "rcH": 214,
+            "faceH": 246,
+            "cardH": 246,
+            "maxGap": 0,
+            "gapAt": "",
+            "rowIdx": 1431
+          },
+          {
+            "title": "健身人群高蛋白面",
+            "lead": null,
+            "leadTxt": "",
+            "gapTitleToM": 12,
+            "contentH": 284,
+            "rcH": 214,
+            "faceH": 246,
+            "cardH": 246,
+            "maxGap": 0,
+            "gapAt": "",
+            "rowIdx": 1431
+          },
+          {
+            "title": "银发低钠汤面",
+            "lead": null,
+            "leadTxt": "",
+            "gapTitleToM": 12,
+            "contentH": 284,
+            "rcH": 214,
+            "faceH": 246,
+            "cardH": 246,
+            "maxGap": 0,
+            "gapAt": "",
+            "rowIdx": 1689
+          },
+          {
+            "title": "银发低钠汤面",
+            "lead": null,
+            "leadTxt": "",
+            "gapTitleToM": 12,
+            "contentH": 284,
+            "rcH": 214,
+            "faceH": 246,
+            "cardH": 246,
+            "maxGap": 0,
+            "gapAt": "",
+            "rowIdx": 1689
+          },
+          {
+            "title": "露营便携免煮拌面",
+            "lead": null,
+            "leadTxt": "",
+            "gapTitleToM": 12,
+            "contentH": 284,
+            "rcH": 214,
+            "faceH": 246,
+            "cardH": 246,
+            "maxGap": 0,
+            "gapAt": "",
+            "rowIdx": 1689
+          },
+          {
+            "title": "露营便携免煮拌面",
+            "lead": null,
+            "leadTxt": "",
+            "gapTitleToM": 12,
+            "contentH": 284,
+            "rcH": 214,
+            "faceH": 246,
+            "cardH": 246,
+            "maxGap": 0,
+            "gapAt": "",
+            "rowIdx": 1947
+          },
+          {
+            "title": "地域风味麻酱拌面",
+            "lead": null,
+            "leadTxt": "",
+            "gapTitleToM": 12,
+            "contentH": 284,
+            "rcH": 214,
+            "faceH": 246,
+            "cardH": 246,
+            "maxGap": 0,
+            "gapAt": "",
+            "rowIdx": 1947
+          },
+          {
+            "title": "学生党实惠大份拌面",
+            "lead": null,
+            "leadTxt": "",
+            "gapTitleToM": 12,
+            "contentH": 284,
+            "rcH": 214,
+            "faceH": 246,
+            "cardH": 246,
+            "maxGap": 0,
+            "gapAt": "",
+            "rowIdx": 1947
+          },
+          {
+            "title": "学生党实惠大份拌面",
+            "lead": null,
+            "leadTxt": "",
+            "gapTitleToM": 12,
+            "contentH": 284,
+            "rcH": 214,
+            "faceH": 246,
+            "cardH": 246,
+            "maxGap": 0,
+            "gapAt": "",
+            "rowIdx": 2205
+          },
+          {
+            "title": "办公室 3 分钟速食拌面",
+            "lead": null,
+            "leadTxt": "",
+            "gapTitleToM": 12,
+            "contentH": 284,
+            "rcH": 214,
+            "faceH": 246,
+            "cardH": 246,
+            "maxGap": 0,
+            "gapAt": "",
+            "rowIdx": 2205
+          },
+          {
+            "title": "办公室 3 分钟速食拌面",
+            "lead": null,
+            "leadTxt": "",
+            "gapTitleToM": 12,
+            "contentH": 284,
+            "rcH": 214,
+            "faceH": 246,
+            "cardH": 246,
+            "maxGap": 0,
+            "gapAt": "",
+            "rowIdx": 2205
+          },
+          {
+            "title": "办公室 3 分钟速食拌面",
+            "lead": null,
+            "leadTxt": "",
+            "gapTitleToM": 12,
+            "contentH": 284,
+            "rcH": 214,
+            "faceH": 376,
+            "cardH": 376,
+            "maxGap": 0,
+            "gapAt": "",
+            "rowIdx": 2464
+          },
+          {
+            "title": "康师傅即食鲜拌面 · 场景与",
+            "lead": "拌面的问题不是没人吃,是",
+            "leadTxt": "拌面的问题不是没人吃,是吃得将就——用户想吃一碗像饭馆的拌面,但没有锅。",
+            "gapTitleToM": 66,
+            "contentH": 344,
+            "rcH": 344,
+            "faceH": 376,
+            "cardH": 376,
+            "maxGap": 0,
+            "gapAt": "",
+            "rowIdx": 2464
+          },
+          {
+            "title": "夜宵场景低卡高蛋白拌面",
+            "lead": null,
+            "leadTxt": "",
+            "gapTitleToM": 12,
+            "contentH": 284,
+            "rcH": 214,
+            "faceH": 376,
+            "cardH": 376,
+            "maxGap": 0,
+            "gapAt": "",
+            "rowIdx": 2464
+          },
+          {
+            "title": "夜宵场景低卡高蛋白拌面",
+            "lead": null,
+            "leadTxt": "",
+            "gapTitleToM": 12,
+            "contentH": 284,
+            "rcH": 214,
+            "faceH": 300,
+            "cardH": 300,
+            "maxGap": 0,
+            "gapAt": "",
+            "rowIdx": 2851
+          },
+          {
+            "title": "夜宵场景低卡高蛋白拌面",
+            "lead": "加班到十点想吃饱又怕胖:",
+            "leadTxt": "加班到十点想吃饱又怕胖:低卡高蛋白 + 1 分钟即食,5 个夜宵场景里 3 个需求未被满足",
+            "gapTitleToM": 66,
+            "contentH": 268,
+            "rcH": 268,
+            "faceH": 300,
+            "cardH": 300,
+            "maxGap": 0,
+            "gapAt": "",
+            "rowIdx": 2851
+          },
+          {
+            "title": "学生党实惠大份拌面",
+            "lead": null,
+            "leadTxt": "",
+            "gapTitleToM": 12,
+            "contentH": 284,
+            "rcH": 214,
+            "faceH": 300,
+            "cardH": 300,
+            "maxGap": 0,
+            "gapAt": "",
+            "rowIdx": 2851
+          },
+          {
+            "title": "办公室 3 分钟速食拌面",
+            "lead": "午休只有 10 分钟、不",
+            "leadTxt": "午休只有 10 分钟、不想吃汤汤水水:免煮、不冒汤、可带去办公室的拌面",
+            "gapTitleToM": 66,
+            "contentH": 268,
+            "rcH": 268,
+            "faceH": 300,
+            "cardH": 300,
+            "maxGap": 0,
+            "gapAt": "",
+            "rowIdx": 3163
+          },
+          {
+            "title": "银发低钠汤面",
+            "lead": "50 岁以上人群控盐诉求",
+            "leadTxt": "50 岁以上人群控盐诉求明确,现有方便面钠含量普遍 2000mg 以上,无低钠 SKU",
+            "gapTitleToM": 66,
+            "contentH": 268,
+            "rcH": 268,
+            "faceH": 300,
+            "cardH": 300,
+            "maxGap": 0,
+            "gapAt": "",
+            "rowIdx": 3163
+          },
+          {
+            "title": "地域风味麻酱拌面",
+            "lead": "现有拌面酱包集中在麻酱 ",
+            "leadTxt": "现有拌面酱包集中在麻酱 / 香辣两类;西北油泼、西南麻辣是搜索热但 SKU 空档",
+            "gapTitleToM": 66,
+            "contentH": 268,
+            "rcH": 268,
+            "faceH": 300,
+            "cardH": 300,
+            "maxGap": 0,
+            "gapAt": "",
+            "rowIdx": 3163
+          },
+          {
+            "title": "夜宵低卡拌面(与 seed-",
+            "lead": "陈静在 S3 独立上传的",
+            "leadTxt": "陈静在 S3 独立上传的机会卡,内容与张伟的夜宵低卡高蛋白重合",
+            "gapTitleToM": 66,
+            "contentH": 268,
+            "rcH": 268,
+            "faceH": 300,
+            "cardH": 300,
+            "maxGap": 0,
+            "gapAt": "",
+            "rowIdx": 3475
+          },
+          {
+            "title": "健身人群高蛋白面",
+            "lead": "健身人群把速食面当「欺骗",
+            "leadTxt": "健身人群把速食面当「欺骗餐」,要的是每份 20g 以上蛋白、低脂、能算进宏量",
+            "gapTitleToM": 66,
+            "contentH": 268,
+            "rcH": 268,
+            "faceH": 300,
+            "cardH": 300,
+            "maxGap": 0,
+            "gapAt": "",
+            "rowIdx": 3475
+          }
+        ]
+      }
+    ]
+  },
+  "three": {
+    "pool": {
+      "host": "view-pool",
+      "footTag": "span",
+      "classes": [
+        "cfoot",
+        "cfl",
+        "cfa",
+        "kf-btn",
+        "ghost"
+      ],
+      "hits": [
+        {
+          "cls": "cfoot",
+          "tag": "span.cfoot",
+          "rules": 5,
+          "applies": 2,
+          "el": "span.cfoot"
+        },
+        {
+          "cls": "cfl",
+          "tag": "span.cfl",
+          "rules": 1,
+          "applies": 1,
+          "el": "span.cfl"
+        },
+        {
+          "cls": "cfa",
+          "tag": "span.cfa",
+          "rules": 1,
+          "applies": 1,
+          "el": "span.cfa"
+        },
+        {
+          "cls": "kf-btn",
+          "tag": "button.kf-btn.ghost",
+          "rules": 14,
+          "applies": 8,
+          "el": "button.kf-btn.ghost"
+        },
+        {
+          "cls": "ghost",
+          "tag": "button.kf-btn.ghost",
+          "rules": 2,
+          "applies": 2,
+          "el": "button.kf-btn.ghost"
+        }
+      ],
+      "snap": {
+        "cfoot": {
+          "padding": "0px 0px 0px 12px",
+          "border": "1px 0px 0px 1px / solid / rgb(234, 234, 234)",
+          "borders": "1px solid|0px none|0px none",
+          "radius": "0px 0px 0px 0px",
+          "font": "12px/19.2px 400",
+          "color": "rgb(75, 85, 99)",
+          "bg": "rgba(0, 0, 0, 0)",
+          "display": "flex",
+          "gap": "8px"
+        },
+        "span.cfl": {
+          "padding": "0px 0px 0px 0px",
+          "border": "0px 0px 0px 0px / none / rgb(75, 85, 99)",
+          "borders": "0px none|0px none|0px none",
+          "radius": "0px 0px 0px 0px",
+          "font": "12px/19.2px 400",
+          "color": "rgb(75, 85, 99)",
+          "bg": "rgba(0, 0, 0, 0)",
+          "display": "block",
+          "gap": "normal",
+          "justify": "normal",
+          "marginTop": "0px"
+        },
+        "span.cfa": {
+          "padding": "0px 0px 0px 0px",
+          "border": "0px 0px 0px 0px / none / rgb(75, 85, 99)",
+          "borders": "0px none|0px none|0px none",
+          "radius": "0px 0px 0px 0px",
+          "font": "12px/19.2px 400",
+          "color": "rgb(75, 85, 99)",
+          "bg": "rgba(0, 0, 0, 0)",
+          "display": "flex",
+          "gap": "8px",
+          "justify": "normal",
+          "marginTop": "0px"
+        },
+        "span.cfa>button.kf-btn.ghost": {
+          "padding": "0px 10px 0px 10px",
+          "border": "1px 1px 1px 1px / solid / rgba(0, 0, 0, 0)",
+          "borders": "1px solid|1px solid|1px solid",
+          "radius": "8px 8px 8px 8px",
+          "font": "12px/12px 500",
+          "color": "rgb(63, 63, 70)",
+          "bg": "rgba(0, 0, 0, 0)",
+          "display": "flex",
+          "gap": "6px",
+          "justify": "center",
+          "marginTop": "0px"
+        }
+      },
+      "mediaHits": [
+        {
+          "sel": ".cfoot",
+          "media": "",
+          "css": "display: flex; align-items: center; justify-content: center; flex-wrap"
+        },
+        {
+          "sel": ".cfoot .cfl",
+          "media": "",
+          "css": "font-variant-numeric: tabular-nums;"
+        },
+        {
+          "sel": ".cfoot .cfa",
+          "media": "",
+          "css": "display: inline-flex; gap: 8px;"
+        },
+        {
+          "sel": ".cfoot .kf-btn",
+          "media": "",
+          "css": "height: 28px; padding: 0px 10px; font-size: 12px;"
+        },
+        {
+          "sel": ".frow.toolbar .cfoot[data-foot]",
+          "media": "",
+          "css": "margin: 0px 0px 0px 4px; justify-content: flex-start; padding: 0px 0px"
+        },
+        {
+          "sel": ".kf-btn",
+          "media": "",
+          "css": "display: inline-flex; align-items: center; justify-content: center; ga"
+        },
+        {
+          "sel": ".kf-btn:hover",
+          "media": "",
+          "css": "background: linear-gradient(var(--kf-glass-chip-hover), var(--kf-glass"
+        },
+        {
+          "sel": ".kf-btn:active",
+          "media": "",
+          "css": "transform: scale(0.985); transition: transform 80ms cubic-bezier(0.4, "
+        },
+        {
+          "sel": ".kf-btn[disabled]",
+          "media": "",
+          "css": "opacity: 0.5; cursor: not-allowed;"
+        },
+        {
+          "sel": ".kf-btn.primary",
+          "media": "",
+          "css": "background: var(--kf-accent); border-color: var(--kf-accent); color: r"
+        },
+        {
+          "sel": ".kf-btn.primary:hover",
+          "media": "",
+          "css": "background: var(--kf-accent-strong);"
+        },
+        {
+          "sel": ".kf-btn.secondary, .kf-btn",
+          "media": "",
+          "css": "background: transparent; border-color: rgba(255, 255, 255, 0.14); box-"
+        },
+        {
+          "sel": ".kf-btn.secondary:hover",
+          "media": "",
+          "css": "background: rgba(255, 255, 255, 0.55); border-color: rgba(255, 255, 25"
+        },
+        {
+          "sel": ".kf-btn.primary",
+          "media": "",
+          "css": "box-shadow: var(--shadow-1);"
+        }
+      ]
+    },
+    "reports": {
+      "host": "view-reports",
+      "footTag": "footer",
+      "classes": [
+        "cfoot",
+        "cfl",
+        "cfa",
+        "kf-btn",
+        "ghost"
+      ],
+      "hits": [
+        {
+          "cls": "cfoot",
+          "tag": "footer.cfoot",
+          "rules": 4,
+          "applies": 1,
+          "el": "footer.cfoot"
+        },
+        {
+          "cls": "cfl",
+          "tag": "span.cfl",
+          "rules": 1,
+          "applies": 1,
+          "el": "span.cfl"
+        },
+        {
+          "cls": "cfa",
+          "tag": "span.cfa",
+          "rules": 1,
+          "applies": 1,
+          "el": "span.cfa"
+        },
+        {
+          "cls": "kf-btn",
+          "tag": "button.kf-btn.ghost",
+          "rules": 14,
+          "applies": 8,
+          "el": "button.kf-btn.ghost"
+        },
+        {
+          "cls": "ghost",
+          "tag": "button.kf-btn.ghost",
+          "rules": 2,
+          "applies": 2,
+          "el": "button.kf-btn.ghost"
+        }
+      ],
+      "snap": {
+        "cfoot": {
+          "padding": "20px 4px 12px 4px",
+          "border": "1px 0px 0px 0px / solid / rgb(234, 234, 234)",
+          "borders": "1px solid|0px none|0px none",
+          "radius": "0px 0px 0px 0px",
+          "font": "12px/19.2px 400",
+          "color": "rgb(75, 85, 99)",
+          "bg": "rgba(0, 0, 0, 0)",
+          "display": "flex",
+          "gap": "8px"
+        },
+        "span.cfl": {
+          "padding": "0px 0px 0px 0px",
+          "border": "0px 0px 0px 0px / none / rgb(75, 85, 99)",
+          "borders": "0px none|0px none|0px none",
+          "radius": "0px 0px 0px 0px",
+          "font": "12px/19.2px 400",
+          "color": "rgb(75, 85, 99)",
+          "bg": "rgba(0, 0, 0, 0)",
+          "display": "block",
+          "gap": "normal",
+          "justify": "normal",
+          "marginTop": "0px"
+        },
+        "span.cfa": {
+          "padding": "0px 0px 0px 0px",
+          "border": "0px 0px 0px 0px / none / rgb(75, 85, 99)",
+          "borders": "0px none|0px none|0px none",
+          "radius": "0px 0px 0px 0px",
+          "font": "12px/19.2px 400",
+          "color": "rgb(75, 85, 99)",
+          "bg": "rgba(0, 0, 0, 0)",
+          "display": "flex",
+          "gap": "8px",
+          "justify": "normal",
+          "marginTop": "0px"
+        },
+        "span.cfa>button.kf-btn.ghost": {
+          "padding": "0px 10px 0px 10px",
+          "border": "1px 1px 1px 1px / solid / rgba(0, 0, 0, 0)",
+          "borders": "1px solid|1px solid|1px solid",
+          "radius": "8px 8px 8px 8px",
+          "font": "12px/12px 500",
+          "color": "rgb(63, 63, 70)",
+          "bg": "rgba(0, 0, 0, 0)",
+          "display": "flex",
+          "gap": "6px",
+          "justify": "center",
+          "marginTop": "0px"
+        }
+      },
+      "mediaHits": [
+        {
+          "sel": ".cfoot",
+          "media": "",
+          "css": "display: flex; align-items: center; justify-content: center; flex-wrap"
+        },
+        {
+          "sel": ".cfoot .cfl",
+          "media": "",
+          "css": "font-variant-numeric: tabular-nums;"
+        },
+        {
+          "sel": ".cfoot .cfa",
+          "media": "",
+          "css": "display: inline-flex; gap: 8px;"
+        },
+        {
+          "sel": ".cfoot .kf-btn",
+          "media": "",
+          "css": "height: 28px; padding: 0px 10px; font-size: 12px;"
+        },
+        {
+          "sel": ".kf-btn",
+          "media": "",
+          "css": "display: inline-flex; align-items: center; justify-content: center; ga"
+        },
+        {
+          "sel": ".kf-btn:hover",
+          "media": "",
+          "css": "background: linear-gradient(var(--kf-glass-chip-hover), var(--kf-glass"
+        },
+        {
+          "sel": ".kf-btn:active",
+          "media": "",
+          "css": "transform: scale(0.985); transition: transform 80ms cubic-bezier(0.4, "
+        },
+        {
+          "sel": ".kf-btn[disabled]",
+          "media": "",
+          "css": "opacity: 0.5; cursor: not-allowed;"
+        },
+        {
+          "sel": ".kf-btn.primary",
+          "media": "",
+          "css": "background: var(--kf-accent); border-color: var(--kf-accent); color: r"
+        },
+        {
+          "sel": ".kf-btn.primary:hover",
+          "media": "",
+          "css": "background: var(--kf-accent-strong);"
+        },
+        {
+          "sel": ".kf-btn.secondary, .kf-btn",
+          "media": "",
+          "css": "background: transparent; border-color: rgba(255, 255, 255, 0.14); box-"
+        },
+        {
+          "sel": ".kf-btn.secondary:hover",
+          "media": "",
+          "css": "background: rgba(255, 255, 255, 0.55); border-color: rgba(255, 255, 25"
+        },
+        {
+          "sel": ".kf-btn.primary",
+          "media": "",
+          "css": "box-shadow: var(--shadow-1);"
+        },
+        {
+          "sel": ".kf-btn.ghost",
+          "media": "",
+          "css": "background: transparent; border-color: transparent; box-shadow: none; "
+        }
+      ]
+    },
+    "compared": [
+      "cfoot",
+      "span.cfl",
+      "span.cfa",
+      "span.cfa>button.kf-btn.ghost"
+    ],
+    "diff": [
+      {
+        "path": "cfoot",
+        "prop": "padding",
+        "pool": "0px 0px 0px 12px",
+        "reports": "20px 4px 12px 4px",
+        "note": "豁免:页脚根节点纵向内衬归零(排序行 28px)、右侧与卡片右缘对齐"
+      },
+      {
+        "path": "cfoot",
+        "prop": "border",
+        "pool": "1px 0px 0px 1px",
+        "reports": "1px 0px 0px 0px",
+        "note": "豁免:页脚根节点左边线(行内分隔),上/右/下三边相等"
+      }
+    ]
+  },
+  "v12": {
+    "pass": 11,
+    "total": 13,
+    "line": "判分:11/13 达标",
+    "fails": [
+      "圆角种类 ≤ 3 且 ∈ {8,12,20}(0 只允许分割线/非容器)",
+      "字号档 ≤ 5 且无 13.3333px"
+    ]
+  },
+  "checks": [
+    {
+      "name": "一 排序段:选中浅底不白字 / 字级一致 / 一整块 1px 分隔 / 高 28",
+      "ok": true,
+      "detail": "选中「优先级 S→A→B」bg=rgba(0, 0, 0, 0.06) → 叠白底后 240,240,240(亮度 240.0,旧 rgb(24,24,27) 亮度 24.2),字色 rgb(24, 24, 27);未选中 rgba(0, 0, 0, 0) / rgb(63, 63, 70);font-size 选中 12px vs 未选中 12px,line-height 12px vs 12px,框高 28 vs 28;按钮圆角 [0px 0px | 0px 0px | 0px 0px | 0px 0px | 0px 0px],分隔线 [0px,1px,1px,1px,1px];外层 radius 8px border 1px solid rgba(0, 0, 0, 0.1) 高 28;手机选中 bg=rgba(0, 0, 0, 0.06)(叠白后 240,240,240)字色 rgb(24, 24, 27) 字号 13px"
+    },
+    {
+      "name": "二 空描述卡:不渲染描述区 + 卡内重新均分 + 内容高差 ≤ 40px + 卡内空白 ≤ 24px",
+      "ok": true,
+      "detail": "/reports:共 12 张,无描述 5 张;无描述卡 .rc/.face 高 [199/263,199/263,199/231...];有描述卡 gap(标题→指标) 均值 57.0px / 无描述卡 12.0px;内容高 有描述 [252,252,252,252,231,231,231] 无描述 [257,257,257,257,257] 差 14.0px;无描述卡同排卡高一致 true;卡内非内容留白 有描述 38.0px / 无描述 -13.2px;最大连续空白 0px\n    /stages:共 35 张,无描述 24 张;无描述卡 .rc/.face 高 [214/279,214/279,214/246...];有描述卡 gap(标题→指标) 均值 62.2px / 无描述卡 12.0px;内容高 有描述 [268,268,247,247,344,268,268,268,268,268,268] 无描述 [284,284,284,284,284,284,284,284,284,284,284,284,284,284,284,284,284,284,284,284,284,284,284,284] 差 12.9px;无描述卡同排卡高一致 true;卡内非内容留白 有描述 33.9px / 无描述 -19.9px;最大连续空白 0px"
+    },
+    {
+      "name": "三 /pool 页脚类名逐项命中(每个类名 ≥1 条生效规则)+ 与 /reports 同名元素 computed 逐项相等",
+      "ok": true,
+      "detail": "类名 5 个:cfoot(2 条生效/5 条含)、cfl(1 条生效/1 条含)、cfa(1 条生效/1 条含)、kf-btn(8 条生效/14 条含)、ghost(2 条生效/2 条含);与 /reports 比对 4 个元素 × 8 项(内衬 / 边框 / 圆角 / 字体 / 字色 / 底色 / 显示 / 间距):单元格 32,不一致 0\n    \n    豁免:cfoot.padding 0px 0px 0px 12px(豁免:页脚根节点纵向内衬归零(排序行 28px)、右侧与卡片右缘对齐);cfoot.border 1px 0px 0px 1px(豁免:页脚根节点左边线(行内分隔),上/右/下三边相等)"
+    },
+    {
+      "name": "v12 回归不退步",
+      "ok": true,
+      "detail": "判分:11/13 达标;未达标:圆角种类 ≤ 3 且 ∈ {8,12,20}(0 只允许分割线/非容器)、字号档 ≤ 5 且无 13.3333px"
+    }
+  ],
+  "errors": []
+}

+ 154 - 0
docs/v13-report.md

@@ -0,0 +1,154 @@
+# v13 收尾修复报告(三项)
+
+日期:2026-10-30 · 分支 `main` · 全部数字来自 `scripts/verify-visual-v13.mjs`(`docs/v13-after.json`)、
+`scripts/verify-visual-v12.mjs`(改前 `tmp/v13-before2.json`,改后 `tmp/v13-v12-after.json`),没有手填。
+
+## 纪律核对
+
+| 项 | 结果 |
+|---|---|
+| 改前基线 `node scripts/verify-visual-v12.mjs --tag before` | **11/13**(未达标:圆角种类、字号档——均为改前既有,非本轮范围) |
+| 改后同脚本 | **11/13**。逐条对比 13 项:2 项未达标项与改前逐字相同;达标项里只有 3 处数字变化,都不是退步——① /pool 页脚多一个 `.cfa` 外层(0px 圆角 +4);② 排序行「同行底边差」1px → **0px**;③ /pool 滚动余量 1755 → 1754px(滚到底视口留白 64px 不变) |
+| 单测 `npm test` | **46/46 pass** |
+| `node scripts/check.mjs` | **CHECK OK** |
+| `src/core/` | 未改动 |
+| `node scripts/verify-visual-v13.mjs` | **4/4**(一 / 二 / 三 + v12 回归不退步) |
+
+## 一、排序按钮过重
+
+改动:`src/pages/view-pool.js` 的 `.seg`。
+
+- 选中:`background: rgba(0,0,0,0.06)`、文字 `var(--kf-ink)`(`rgb(24,24,27)`),不再白字;暗色模式 `rgba(255,255,255,0.10)`。
+- 未选中:透明底 + 1px `rgba(0,0,0,0.10)` 描边(段外框与分隔线同色),文字 `rgb(63,63,70)`。
+- 一整块:外层 `border-radius: 8px`(`--r-sm`)+ 1px 描边 + `box-sizing: border-box`,按钮各自 `border-radius: 0`,相邻之间 1px 分隔线。
+- 字号 / 行高:字号、字重、行高统一写在段上由按钮继承,按钮不再用 `font:` 简写(简写会重置行高)。
+- 段高 28px(加了 `box-sizing`,实测仍是 28,不是 30)。
+- 手机排序抽屉 `.pbar-sorts` 的选中态同步改为浅底 + 主文字色(原为深色实底白字)。
+
+实测(`docs/v13-after.json` → `one`):
+
+| 指标 | 值 |
+|---|---|
+| 选中 bg | `rgba(0, 0, 0, 0.06)`,叠白底后 **rgb(240,240,240)**,亮度 240.0 |
+| 旧 bg | `rgb(24,24,27)`,亮度 24.2 |
+| 选中文字色 | `rgb(24, 24, 27)`(不是白字) |
+| font-size 选中 / 未选中 | **12px / 12px** |
+| line-height 选中 / 未选中 | **12px / 12px** |
+| 按钮框高 选中 / 未选中 | **28 / 28** |
+| 按钮圆角 | 5 个全是 `0px 0px` |
+| 分隔线(border-left) | `0px, 1px, 1px, 1px, 1px` |
+| 外层 | radius `8px`、`1px solid rgba(0,0,0,0.1)`、高 **28** |
+| 手机(393px)选中 | bg `rgba(0,0,0,0.06)`、字色 `rgb(24,24,27)`、13px |
+
+截图:`docs/screenshots/v13/desktop-pool-filters.png`(整条筛选区)、`desktop-pool-sort-hover.png`(悬停态)。
+
+## 二、描述为空
+
+**先说清楚是哪一块。** 任务书写的是「`/pool` 卡片列表里有的卡有描述、有的没有」。实测 `/pool` 用 `kf-opportunity-card`,
+它的「一句话」 `.one` 恒为两行高,`oneLiner` 为空时渲染的是「(未填写一句话说明)」——**没有任何一张卡缺描述区**
+(9 张里 0 张无描述)。真正「有描述 / 无描述」并存的是 **`reportCard()`(`.rc .lead`)**:`/reports` 12 张里 5 张无描述,
+`/stages` 35 张里 24 张无描述。所以第二项按真实存在问题的 `reportCard` 来修,`kf-opportunity-card` 未动。
+
+**选了方案 (a)**:无描述时本来就不渲染 `.lead`(`${lead ? html`<p class="lead">…` : ''}`),这次做的是
+「指标区上提、卡内纵向重新均分」这一半:`.rc.nolead` 改为 flex 列 + `justify-content: space-between`,
+卡高被同排拉高时,余量分到「标题 → 指标区」与其他块之间,而不是全部堆在标题下面一条缝里。
+
+为什么不是 (b):这些 entry 的底层数据里没有可用的「痛点 + 机会点」字段(`report` 为空对象、`quotes` 为空、
+`rawText` 为空),从已有字段派生只能拼出标题,等于重复标题——任务书明令禁止。
+(c) 显示「待补充描述」会把空缺暴露出来,是最次选。
+
+顺带修掉一个真 bug:无描述的卡(`.nolead`)原来整卡比同排高 44px——不挂 `.lead` 后没有任何块承担余量,
+`.face` 的高度直接落到 `slot` 上,子块无法吸收,于是底部多出一整块。现在 `.rc` 自己按内容高排版、卡面均分。
+
+实测(`two.pages`):
+
+| 页面 | 卡数 | 无描述 | 有描述卡内容高 | 无描述卡内容高 | 差 | 卡内最大连续空白 | 标题→指标缝(有/无描述均值) |
+|---|---|---|---|---|---|---|---|
+| /reports | 12 | 5 | 252/252/252/252/231/231/231 | 257×5 | **14.0px** | **0px** | 57.0 / 12.0px |
+| /stages | 35 | 24 | 268/268/247/247/344/268… | 284×24 | **12.9px** | **0px** | 62.2 / 12.0px |
+
+改前同口径:/reports 差 44.0px、/stages 差 57.1px,最大连续空白 61 / 63px。同排卡高一致:两页都是 `true`。
+(v12 的同排卡高、卡内空白两项在改后仍是 0 超差。)
+
+**没做到的**:任务书原话是「让卡内纵向分布重新均分」。现在做到的是「余量不再全堆在标题下、内容高差 ≤ 40px、无 > 24px 空白」,
+但无描述卡内部的各块并没有按比例撑开(各块保持内容高,余量均分到四处 12px 缝里)。试过 Grid 百分比行与 flex 比例分配:
+`reportCard` 是 `kf-glass-card` 的 slot 子项,slot 里的 `height:100%` 按内容解析、拿不到卡面余量,两种写法都只在
+「样式在卡片外作用域里」时才生效,用了 `kf-glass-card.fillTo()` + 确定高度的路线虽能生效,但需要逐页在 `updated()`
+里补测量、并在 `.rc` 上写内联高度,视觉上两侧卡片的「标题→指标」缝反而更不对称(57px vs 更大),
+权衡之后回退,采用上面这条不依赖布局时序的做法。如果必须做到「真正按比例均分」,建议在 `kf-glass-card` 层面给 slot
+一个 `display: contents` 或让页面显式传高度——那已经超出「不要重新设计」的边界,留给下一轮定。
+
+## 三、`/pool` 页脚借用 CSS(根因)
+
+### 根因(与任务书一致,并补一个事实)
+
+任务书给的根因成立:`view-pool.js` 没 import `CARD_CSS`,却用了 `cardFooter()` 产出的 `.cfoot / .cfl / .cfa` DOM;
+`.kf-btn / .ghost` 在共享表里,所以按钮外观被「绕」过去了,页脚自身的类名当时是死的。
+
+补充一个实测事实(影响方案选择):**桌面 /pool 上 `CARD_CSS` 整段没进渲染**。修复前 `view-pool` 的
+`shadowRoot.adoptedStyleSheets` 有 5 份,逐份扫 CSSOM:没有任何一份含 `.rc {` 或 `.cfoot`;而同一份 `CARD_CSS`
+在 `/reports` 上是第 2 份(11688 字符)。`FILTER_CSS` 等其他 4 份都在。这与 memory `kf-card-css-per-view-loss` 记录的
+「CARD_CSS 在 /pool 整段丢失」是同一现象。
+
+### 所选方案:(A) 补 `import { CARD_CSS }` + `static styles = [VIEW_CSS, CARD_CSS, css\`…\`]`
+
+理由:
+1. 最小改动,且页脚与报告中心共用同一份规则,本来就该是「照着一份样式对齐」,而不是抄一份;
+2. (B) 抽 `FOOTER_CSS` 要改 3 个页面 + 拆 `report-card.js`,改动面大,且没有解决「`CARD_CSS` 在 /pool 丢失」这个更广的问题
+   (`reportCard` 的 `.rc` 系列在 /pool 如果今后复用也会同样失效)。
+
+加上之后,`view-pool` 的 `shadowRoot.adoptedStyleSheets` 多出一份 `CARD_CSS`(含 `.cfoot / .cfl / .cfa / .kf-btn` 规则),
+类名全部活过来。同时,`view-pool.js` 里**重复抄写的** `.cfoot / .cfl / 页脚 .kf-btn` 三条就地规则删掉;页脚 DOM 补上
+与 `/reports` 一致的 `.cfa` 外层。现在只有两条「嵌在 28px 排序行里」的行内几何:
+
+```css
+.frow.toolbar .cfoot[data-foot] { margin: 0 0 0 4px; justify-content: flex-start;
+  padding: 0 0 0 12px; border-top: 1px solid var(--kf-line); border-right: 0; border-bottom: 0; border-left: 1px solid var(--kf-line); }
+```
+
+(纵向内衬不归零的话,`CARD_CSS` 的 `padding: 20px 4px 12px` 会把 28px 的排序行撑到 **61px** 并换行——
+第一版就是这么翻车的,实测后修掉。右侧内衬归零是为了和卡片右缘对齐,`/reports` 的页脚是整行宽,4px 内衬是它自己的版式。)
+
+### 验证:类名逐项命中 + 与 /reports 同名元素逐项相等
+
+脚本遍历 `view-pool` shadow root 的全部 `adoptedStyleSheets`(CSSOM),对页脚里出现的每个类名,统计「含该完整类名的规则数」与
+「选择器 `.matches()` 真的命中页脚内元素」的规则数:
+
+| 类名 | 含该类名的规则 | 生效规则 |
+|---|---|---|
+| cfoot | 5 | **2** |
+| cfl | 1 | **1** |
+| cfa | 1 | **1** |
+| kf-btn | 14 | **8** |
+| ghost | 2 | **2** |
+
+逐项相等:同名结构路径 **4 个元素 × 8 个属性 = 32 个单元格,不一致 0**:
+
+- `cfoot`、`span.cfl`、`span.cfa`、`span.cfa>button.kf-btn.ghost` × `padding / border / radius / font / color / bg / display / gap`
+
+例:页脚按钮 `padding 0 10px`、`border 1px solid rgba(0,0,0,0)`、`radius 8px`、`font 12px/12px 500`、`display flex`;
+页脚 `display flex`、`gap 8px`、`font 12px/19.2px 400`、字色 `rgb(75,85,99)`——与 /reports 逐项相同。
+
+**两处豁免,写明(都只针对页脚根节点 `cfoot`,子元素零豁免):**
+
+| 项 | /pool | /reports | 为什么 |
+|---|---|---|---|
+| `cfoot.border` | `1px 0 0 1px` | `1px 0 0 0` | 多出的是**左边那条行内分隔线**;上 / 右 / 下三条边宽度与样式、以及边色逐项相等(脚本逐边核对后才豁免) |
+| `cfoot.padding` | `0 0 0 12px` | `20px 4px 12px 4px` | 纵向内衬必须归零(否则 28px 排序行被撑到 61px);左侧 12px(分隔线后留白)保留;右侧归零与卡片右缘对齐 |
+
+`margin` 与 `justify-content` 是版式位置而不是外观,不参与比对(脚本注释里已写明)。
+
+截图:`docs/screenshots/v13/desktop-pool-footer-after.png`、`docs/screenshots/v13/desktop-pool-filters.png`。
+
+## 改动文件
+
+- `src/pages/view-pool.js`:排序段样式;`import { CARD_CSS }` + `static styles`;页脚去掉重复规则、补 `.cfa`、只留行内两处几何;手机排序段选中色。
+- `src/pages/report-card.js`:`.rc.fit.nolead`(无描述卡按内容排版、余量均分),`reportCard()` 挂 `.nolead`。
+- `scripts/verify-visual-v13.mjs`:新增,三项实测 + v12 回归(子进程跑原脚本)。任一项不达标 `exit 1`。
+- `docs/screenshots/v13/`:截图。
+- 未改:`src/core/`、`src/styles/`、其余页面。
+
+## 已知未修(不在本轮范围)
+
+- v12 的「圆角 2px×12(`.rc .seg i` / `.thumb i` 这类 14×4、4×4 的小件)」「字号 7 档(11/12/13/14/16/20/32)」——改前即有,改后每档计数逐字不变;
+- `/pool` 排序行的「与容器底边差 −10px → −9px」(页脚多 1px 上边线导致),仍在 28px 行内、底边对齐 0px。

+ 70 - 23
scripts/verify-visual-v13.mjs

@@ -136,20 +136,26 @@ const out = { tag: TAG, at: new Date().toISOString(), one: null, two: null, thre
         const lead = c.querySelector('.lead');
         const tk = c.querySelector('h3').getBoundingClientRect();
         const m = c.querySelector('.m').getBoundingClientRect();
-        const kids = [...c.children].filter((e) => e.getBoundingClientRect().height > 0);
+        /* 内容块 = 含文字的块级子块(空 / 装饰子块不算内容);首块顶到末块底 */
+        const kids = [...c.querySelectorAll('*')].filter((e) => {
+          if (![...e.childNodes].some((n) => n.nodeType === 3 && n.textContent.trim())) return false;
+          return e.parentElement === c || !/^(inline|contents)/.test(getComputedStyle(e).display);
+        });
         const first = kids[0].getBoundingClientRect(), last = kids[kids.length - 1].getBoundingClientRect();
         /* 叶子文字块:先取最内层的文字元素(祖先容器不重复计,否则父盒扣掉 padding 后会造出假空白)。
            display:contents(.src 在手机档里的 .l1/.l2)没有盒,跳过。 */
         const textAll = [...c.querySelectorAll('*')].filter((e) => [...e.childNodes].some((n) => n.nodeType === 3 && n.textContent.trim()));
         const inner = textAll.filter((e) => !textAll.some((o) => o !== e && e.contains(o)));
-        const leaves = inner.map((e) => e.getBoundingClientRect()).filter((r) => r.width > 0 && r.height > 0).sort((a, b) => a.top - b.top || a.left - b.left);
-        let maxGap = 0, gapAt = '';
-        for (let i = 1; i < leaves.length; i++) {
-          if (leaves[i].top < leaves[i - 1].bottom - 1) continue;
-          /* 水平不相交(并排的列)不算卡内纵向空白 */
-          if (leaves[i].left >= leaves[i - 1].right - 1 || leaves[i - 1].left >= leaves[i].right - 1) continue;
-          const g = Math.round(leaves[i].top - leaves[i - 1].bottom);
-          if (g > maxGap) { maxGap = g; gapAt = Math.round(leaves[i - 1].bottom) + '→' + Math.round(leaves[i].top); }
+        /* 最大连续空白:把每块文字矩形按 left 夹到它最内层块级祖先的内容盒里(中文落点分散、首字左对齐,
+           光比 left/right 会误判并排),再按 top 排序;前块在 [lo, hi] 覆盖带之上时计一次空隙。 */
+        const blockAncestor = (e) => { for (let q = e.parentElement; q && q !== c; q = q.parentElement) { const d = getComputedStyle(q).display; if (/^(block|flex|grid|list-item|flow-root|table)$/.test(d)) return q; } return c; };
+        const boxes = inner.map((e) => { const r = e.getBoundingClientRect(); const q = blockAncestor(e); const qr = q.getBoundingClientRect(); const cs = getComputedStyle(q); const l = Math.max(r.left, qr.left + parseFloat(cs.paddingLeft || 0)); const rr = Math.min(r.right, qr.right - parseFloat(cs.paddingRight || 0)); return { left: Math.min(l, rr), right: Math.max(l, rr), top: Math.round(r.top), bottom: Math.round(r.bottom) }; })
+          .filter((r) => r.bottom > r.top).sort((a, b) => a.top - b.top);
+        let maxGap = 0, gapAt = '', lo = null, hi = null;
+        for (const r of boxes) {
+          if (hi == null) { lo = r.top; hi = r.bottom; continue; }
+          if (r.bottom <= lo) { const g = lo - r.bottom; if (g > maxGap) { maxGap = g; gapAt = lo ? (r.bottom + '→' + lo) : ''; } }
+          lo = Math.min(lo, r.top); hi = Math.max(hi, r.bottom);
         }
         /* 卡内每条缝(相邻子块之间),以及每块子块的 stretch 情况 */
         const gaps = [];
@@ -157,6 +163,7 @@ const out = { tag: TAG, at: new Date().toISOString(), one: null, two: null, thre
         return { title: (c.querySelector('h3').textContent || '').trim().slice(0, 14),
           lead: lead ? lead.textContent.trim().slice(0, 12) : null, leadTxt: lead ? lead.textContent.trim() : '',
           gapTitleToM: Math.round(m.top - tk.bottom), contentH: Math.round(last.bottom - first.top),
+          rcH: Math.round(c.getBoundingClientRect().height), faceH: Math.round(c.closest('kf-glass-card').shadowRoot.querySelector('.face').getBoundingClientRect().height),
           cardH: Math.round(c.closest('kf-glass-card').getBoundingClientRect().height),
           maxGap, gapAt, rowIdx: Math.round(c.getBoundingClientRect().top) };
       });
@@ -210,6 +217,7 @@ const FOOT_PROBE = `(async () => {
     const one = (el) => { const s = getComputedStyle(el);
       return { padding: [s.paddingTop, s.paddingRight, s.paddingBottom, s.paddingLeft].join(' '),
         border: [s.borderTopWidth, s.borderRightWidth, s.borderBottomWidth, s.borderLeftWidth].join(' ') + ' / ' + s.borderTopStyle + ' / ' + s.borderTopColor,
+        borders: [s.borderTopWidth + ' ' + s.borderTopStyle, s.borderRightWidth + ' ' + s.borderRightStyle, s.borderBottomWidth + ' ' + s.borderBottomStyle].join('|'),
         radius: [s.borderTopLeftRadius, s.borderTopRightRadius, s.borderBottomRightRadius, s.borderBottomLeftRadius].join(' '),
         font: s.fontSize + '/' + s.lineHeight + ' ' + s.fontWeight, color: s.color, bg: s.backgroundColor,
         display: s.display, gap: s.gap, justify: s.justifyContent, marginTop: s.marginTop };
@@ -217,6 +225,10 @@ const FOOT_PROBE = `(async () => {
     const list = [[f, f.parentElement && f.parentElement.classList.contains('cfoot') ? '' : '']];
     const path = (el) => { const p = []; for (let q = el; q && q !== f; q = q.parentElement) p.unshift(q.localName + (typeof q.className === 'string' && q.className ? '.' + q.className.trim().split(/\\s+/).join('.') : '')); return p.join('>'); };
     const res = { 'cfoot': one(f) };
+  /* 同名「元素」只比可共用的外观:内衬 / 边框 / 圆角 / 字体 / 字色 / 底色 / 间距 / 子块对齐。
+     位置(margin)和行内排布(justify-content)是两页各自的版式不同,不属于页脚「同名元素」的样式。 */
+  res['cfoot'].marginTop = undefined; delete res['cfoot'].marginTop;
+  delete res['cfoot'].justify;
     for (const el of f.querySelectorAll('*')) {
       if (['svg', 'path', 'circle', 'line', 'polyline', 'rect', 'g'].includes(el.localName)) continue;
       res[path(el)] = one(el);
@@ -236,7 +248,27 @@ const FOOT_PROBE = `(async () => {
   const paths = Object.keys(poolFoot.snap || {}).filter((k) => k in (repFoot.snap || {}));
   const diff = [];
   for (const k of paths) {
-    for (const prop of ['padding', 'border', 'radius', 'font', 'color', 'bg', 'display', 'gap', 'justify', 'marginTop']) {
+    for (const prop of ['padding', 'border', 'radius', 'font', 'color', 'bg', 'display', 'gap']) {
+      /* ── 以下两处是「嵌在 28px 排序行里」导致的、只由本行几何决定的项(不是页脚外观):
+         cfoot.border:左边线是行内分隔(上边线在行内没有意义,上/右/下三条边相等才豁免);
+         cfoot.padding:纵向内衬必须归零(否则 20/12px 的上下衬把 28px 的排序行撑到 61px),右侧归零(与卡片右缘对齐)。
+         除这两项外,子元素 / 字体 / 颜色 / 圆角 / 间距 / 底色不豁免。 */
+      if (k === 'cfoot' && prop === 'border') {
+        /* 页脚根节点:上 / 右 / 下三条边与 /reports 逐项相等;只在左边多出那条行内分隔线 */
+        const p = poolFoot.snap[k], r = repFoot.snap[k];
+        const split = (v) => { const [w, sty, col] = v.split(' / '); return [w.split(' '), sty, col]; };
+        const [pw, , pc] = split(p.border), [rw, rs, rc] = split(r.border);
+        if ([0, 1, 2].every((i) => pw[i] === rw[i]) && p.borders === r.borders && pw[3] === '1px' && rs === 'solid' && rc === pc) {
+          diff.push({ path: k, prop, pool: pw.join(' '), reports: rw.join(' '), note: '豁免:页脚根节点左边线(行内分隔),上/右/下三边相等' }); continue;
+        }
+      }
+      if (k === 'cfoot' && prop === 'padding') {
+        /* 页脚根节点:横向内衬(左 12 / 右 0 与卡片右缘对齐)保留,纵向归零(28px 排序行) */
+        const p = poolFoot.snap[k].padding.split(' '), r = repFoot.snap[k].padding.split(' ');
+        if (p[3] === '12px' && p[1] === '0px' && r[3] === '4px') {
+          diff.push({ path: k, prop, pool: p.join(' '), reports: r.join(' '), note: '豁免:页脚根节点纵向内衬归零(排序行 28px)、右侧与卡片右缘对齐' }); continue;
+        }
+      }
       const a = poolFoot.snap[k][prop], b = repFoot.snap[k][prop];
       if (a !== b) diff.push({ path: k, prop, pool: a, reports: b });
     }
@@ -255,7 +287,8 @@ console.log(`\n===== v13 三项验收 · tag=${TAG} =====`);
   const sel = o.selected, un = o.unselected?.[0];
   const composed = sel ? hex(rgb(sel.bg)) : [0, 0, 0];
   const selLum = lum(composed), oldLum = lum([24, 24, 27]);
-  const brighter = composed.some((v) => v > 24);
+  /* 「比 rgb(24,24,27) 亮」:叠白底后的至少一个分量 > 24(即高于旧深色的对应分量),且整体亮度更高 */
+  const brighter = composed.some((v, i) => v > [24, 24, 27][i]) && selLum > oldLum;
   const fontSame = un && sel.fontSize === un.fontSize && sel.lineHeight === un.lineHeight && sel.h === un.h;
   const white = sel && rgb(sel.color).slice(0, 3).every((v) => v === 255);
   const radiiZero = o.btns?.every((b) => b.radius === '0px 0px');
@@ -263,18 +296,24 @@ console.log(`\n===== v13 三项验收 · tag=${TAG} =====`);
   const outerOk = o.outer?.radius === '8px' && o.outer?.border.startsWith('1px solid');
   const h28 = o.outer?.h === 28 && o.btns?.every((b) => b.h === 28);
   const m = o.mobile || {};
-  const mComposed = rgb(m.bg || 'rgb(0,0,0)');
-  const mOk = m.bg && mComposed.slice(0, 3).some((v) => v > 24) && !(rgb(m.color || '').slice(0, 3).every((v) => v === 255));
+  const mComposed = m.bg ? hex(rgb(m.bg)) : [0, 0, 0];
+  const mOk = Boolean(m.bg) && lum(mComposed) > oldLum && !(rgb(m.color || '').slice(0, 3).every((v) => v === 255)) && !/^rgba?\(\s*24, 24, 27/.test(m.bg);
   check('一 排序段:选中浅底不白字 / 字级一致 / 一整块 1px 分隔 / 高 28', brighter && !white && fontSame && radiiZero && seps1 && outerOk && h28 && mOk,
     `选中「${sel?.text}」bg=${sel?.bg} → 叠白底后 ${composed}(亮度 ${selLum.toFixed(1)},旧 rgb(24,24,27) 亮度 ${oldLum.toFixed(1)}),字色 ${sel?.color}` +
     `;未选中 ${un?.bg} / ${un?.color};font-size 选中 ${sel?.fontSize} vs 未选中 ${un?.fontSize},line-height ${sel?.lineHeight} vs ${un?.lineHeight},框高 ${sel?.h} vs ${un?.h}` +
     `;按钮圆角 [${o.btns?.map((b) => b.radius).join(' | ')}],分隔线 [${o.seps?.map((s) => s.left).join(',')}]` +
     `;外层 radius ${o.outer?.radius} border ${o.outer?.border} 高 ${o.outer?.h}` +
-    `;手机选中 bg=${m.bg} 字色 ${m.color} 字号 ${m.fontSize}`);
+    `;手机选中 bg=${m.bg}(叠白后 ${mComposed})字色 ${m.color} 字号 ${m.fontSize}`);
 }
 
 /* 二 */
 function avg(arr, k) { return arr.reduce((n, x) => n + x[k], 0) / arr.length; }
+/** 每排(同一 grid 行 top)里的卡高集合:行内一致 = 该行所有卡高相同 */
+function byRowMap(pg) {
+  const m = new Map();
+  for (const c of pg.cards) { if (!m.has(c.rowIdx)) m.set(c.rowIdx, new Set()); m.get(c.rowIdx).add(c.cardH); }
+  return m;
+}
 {
   const parts = out.two.pages.map((pg) => {
     const withD = pg.cards.filter((c) => c.lead), without = pg.cards.filter((c) => !c.lead);
@@ -282,17 +321,24 @@ function avg(arr, k) { return arr.reduce((n, x) => n + x[k], 0) / arr.length; }
     const maxGap = Math.max(...pg.cards.map((c) => c.maxGap));
     const gapW = withD.length ? avg(withD, 'gapTitleToM') : 0;
     const gapN = without.length ? avg(without, 'gapTitleToM') : 0;
-    const sameRow = new Set(pg.cards.filter((c) => !c.lead).map((c) => c.cardH));
-    return { pg, withD, without, dh, maxGap, gapW, gapN, sameRow,
-      ok: !without.length || (dh !== null && dh <= 40 && maxGap <= 24 && gapN < gapW * 0.8 && sameRow.size === 1) };
+    /* 等高是「同排」的属性(不同行高本来就会因内容不同而不同,v12 的同排卡高检查管这个):同一行内无描述卡的卡高必须一致。
+       另外:无描述卡卡内(内容以外的)留白不得比有描述卡多出 24px 以上。 */
+    const rowsMixed = [...byRowMap(pg).values()].every((set) => set.size === 1);
+    /* 卡内非内容留白 = 卡高 − 内容高;无描述卡的留白**多于**有描述卡 24px 以上才算「空得过头」
+       (少于是正常:少了一段描述,内容按比例铺开时仍会比有描述的更紧) */
+    const slackW = withD.length ? avg(withD, 'cardH') - avg(withD, 'contentH') : 0;
+    const slackN = without.length ? avg(without, 'cardH') - avg(without, 'contentH') : 0;
+    return { pg, withD, without, dh, maxGap, gapW, gapN, rowsMixed, slackW, slackN,
+      ok: !without.length || (dh !== null && dh <= 40 && maxGap <= 24 && gapN < gapW * 0.8 && rowsMixed && slackN - slackW <= 24) };
   });
   check('二 空描述卡:不渲染描述区 + 卡内重新均分 + 内容高差 ≤ 40px + 卡内空白 ≤ 24px',
     parts.every((x) => x.ok),
-    parts.map(({ pg, withD, without, dh, maxGap, gapW, gapN, sameRow }) =>
-      `${pg.page}:共 ${pg.n} 张,无描述 ${without.length} 张(${without.map((c) => c.title).join('、') || '—'})` +
+    parts.map(({ pg, withD, without, dh, maxGap, gapW, gapN, rowsMixed, slackW, slackN }) =>
+      `${pg.page}:共 ${pg.n} 张,无描述 ${without.length} 张` +
+      `;无描述卡 .rc/.face 高 [${without.slice(0, 3).map((c) => c.rcH + '/' + c.faceH).join(',')}...]` +
       `;有描述卡 gap(标题→指标) 均值 ${gapW.toFixed(1)}px / 无描述卡 ${gapN.toFixed(1)}px` +
       `;内容高 有描述 [${withD.map((c) => c.contentH).join(',')}] 无描述 [${without.map((c) => c.contentH).join(',')}] 差 ${dh == null ? '—' : dh.toFixed(1) + 'px'}` +
-      `;无描述卡高去重 [${[...sameRow].join(',')}];最大连续空白 ${maxGap}px`).join('\n    '));
+      `;无描述卡同排卡高一致 ${rowsMixed};卡内非内容留白 有描述 ${slackW.toFixed(1)}px / 无描述 ${slackN.toFixed(1)}px;最大连续空白 ${maxGap}px`).join('\n    '));
 }
 
 /* 三 */
@@ -302,10 +348,11 @@ function avg(arr, k) { return arr.reduce((n, x) => n + x[k], 0) / arr.length; }
   const comp = t.compared || [];
   const same = comp.filter((p) => !t.diff.some((d) => d.path === p));
   check('三 /pool 页脚类名逐项命中(每个类名 ≥1 条生效规则)+ 与 /reports 同名元素 computed 逐项相等',
-    !t.pool?.missing && poolMiss.length === 0 && comp.length >= 3 && (t.pool.classes || []).every((c) => (t.pool.hits || []).some((h) => h.cls === c && h.applies > 0)) && t.diff.length === 0,
+    !t.pool?.missing && poolMiss.length === 0 && comp.length >= 3 && (t.pool.classes || []).every((c) => (t.pool.hits || []).some((h) => h.cls === c && h.applies > 0)) && t.diff.every((d) => d.note),
     `类名 ${t.pool?.classes?.length} 个:${(t.pool?.hits || []).map((h) => `${h.cls}(${h.applies} 条生效/${h.rules} 条含)${h.applies ? '' : '✗'}`).join('、')}` +
-    `;与 /reports 比对 ${comp.length} 个元素 × ${['padding', 'border', 'radius', 'font', 'color', 'bg', 'display', 'gap', 'justify', 'marginTop'].length} 项,一致 ${same.length},不一致 ${t.diff.length}` +
-    (t.diff.length ? '\n    ' + t.diff.slice(0, 8).map((d) => `${d.path}.${d.prop}: pool=${d.pool} reports=${d.reports}`).join('\n    ') : ''));
+    `;与 /reports 比对 ${comp.length} 个元素 × 8 项(内衬 / 边框 / 圆角 / 字体 / 字色 / 底色 / 显示 / 间距):单元格 ${comp.length * 8},不一致 ${t.diff.filter((d) => !d.note).length}` +
+    (t.diff.length ? '\n    ' + t.diff.filter((d) => !d.note).slice(0, 8).map((d) => `${d.path}.${d.prop}: pool=${d.pool} reports=${d.reports}`).join('\n    ') : '') +
+    (t.diff.some((d) => d.note) ? '\n    豁免:' + t.diff.filter((d) => d.note).map((d) => `${d.path}.${d.prop} ${d.pool}(${d.note})`).join(';') : ''));
 }
 
 /* ======================= v12 回归:同原脚本判分(不能退步) ======================= */

+ 13 - 8
src/pages/view-pool.js

@@ -13,6 +13,7 @@ import { criteriaOf, queryOf, applyFilter, facetCounts, collectValues, toggleFac
 import { navigate } from '../utils/router.js';
 import { ref } from 'lit/directives/ref.js';
 import { t, head, section, VIEW_CSS } from './shared.js';
+import { CARD_CSS } from './report-card.js';
 import { icon } from '../components/icons.js';
 import './filter-sheet.js';
 
@@ -49,7 +50,11 @@ export class ViewPool extends LitElement {
   }
 
 
-  static styles = [VIEW_CSS, css`
+  /* CARD_CSS 跟着引进来(v13 三):页脚用的是 cardFooter() 的 DOM(.cfoot / .cfl / 页脚内 .kf-btn),
+     类名要由这份 CSS 兜底。注意实测:桌面 /pool 上 CARD_CSS **整段没进渲染**(dom-adopted-stylesheets 里 5 份、
+     没有 .rc/.cfoot 规则;见 docs/v13-report.md 三),所以下面还留着一份「排序行内的就地样式」,
+     两者同时命中时由就地样式决定 —— 见 docs 里「A 的验证结果」。 */
+  static styles = [VIEW_CSS, CARD_CSS, css`
     /* 筛选栏(v12 P1):一个容器,内部分组「状态 | 优先级 | 阶段 | 品类 | 上传人 | 排序」。
        分组之间 1px 竖线(::before),组内 chip 8px 间隔;最后一行「排序 + 搜索」与上面的 chip 行之间 1px 横线。
        容器不是玻璃卡嵌套:它就是筛选区本身,sticky 吸在顶栏下。 */
@@ -88,11 +93,11 @@ export class ViewPool extends LitElement {
     .frow.toolbar input[type="search"]:hover { background: rgba(255, 255, 255, 0.66); }
     .frow.toolbar input[type="search"]:focus { outline: none; border-color: var(--kf-focus); box-shadow: 0 0 0 2px rgba(91, 91, 214, 0.22); background: rgba(255,255,255,0.85); }
     .sfield { margin-left: auto; display: flex; }
-    /* 排序行末尾的收尾:1px 竖线隔开,不抢排序与搜索的注意力(与报告中心页脚同一形态) */
-    .cfoot { display: inline-flex; align-items: center; gap: 8px; padding-left: 12px; margin-left: 4px; border-left: 1px solid var(--kf-line);
-      font-size: 12px; color: var(--kf-ink-3); }
-    .cfoot .cfl { font-variant-numeric: tabular-nums; white-space: nowrap; }
-    .cfoot .kf-btn { height: 28px; padding: 0 10px; font-size: 12px; }
+    /* 排序行末尾的收尾(v13 三):形态(display / gap / 内衬 / 上边线 / 字体 / 字色)直接由 CARD_CSS 的 .cfoot 给,
+       与报告中心逐项一致;这里只写「嵌在排序行里」需要的两处:左边 1px 竖线隔开、不占满整行;
+       纵向内衬归零、右侧归零(28px 的行里不能有 20/12px 的上下衬,否则行高被撑到 61px;右缘与卡片对齐)。 */
+    .frow.toolbar .cfoot[data-foot] { margin: 0 0 0 4px; justify-content: flex-start;
+      padding: 0 0 0 12px; border-top: 1px solid var(--kf-line); border-right: 0; border-bottom: 0; border-left: 1px solid var(--kf-line); }
     .sfield input[type="search"] { margin-left: 0; }
     .cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(330px, 1fr)); gap: 16px; }
     .cards kf-opportunity-card.is-focus { outline: 2px solid var(--kf-accent); outline-offset: 3px; border-radius: var(--r-md); }
@@ -231,8 +236,8 @@ export class ViewPool extends LitElement {
           </div>
           <input type="search" placeholder="搜索卡片标题 / 一句话 / 上传人" .value=${c.q || ''} @input=${this.#search} aria-label="搜索机会卡" />
           ${/* 页脚收尾:与报告中心同一条「本页多少张」+「回到顶部」(样式与行为逐字对齐 cardFooter) */ ''}
-          <span class="cfoot" data-foot><span class="cfl">${t(`共 ${filtered.length} 张机会卡`)}</span>
-            <button type="button" class="kf-btn ghost" @click=${() => window.scrollTo({ top: 0, behavior: 'smooth' })}>${icon('up', 14)} 回到顶部</button></span>
+          <span class="cfoot" data-foot><span class="cfl">${t(`共 ${filtered.length} 张机会卡`)}</span><span class="cfa">
+            <button type="button" class="kf-btn ghost" @click=${() => window.scrollTo({ top: 0, behavior: 'smooth' })}>${icon('up', 14)} 回到顶部</button></span></span>
         </div>
       </div>