Răsfoiți Sursa

feat(v16): 工作台按调研参数重排(4 张窄短卡 + 深色卡单色相 + A 卡通栏列表 + 静默文字按钮)+ verify-v16 + 改前改后对照

FmodeCode 4 zile în urmă
părinte
comite
7d01b5cf22

BIN
docs/v16-after-desktop-me.png


BIN
docs/v16-after-desktop-nobody.png


BIN
docs/v16-after-desktop.png


BIN
docs/v16-after-mobile.png


+ 1128 - 0
docs/v16-after.json

@@ -0,0 +1,1128 @@
+{
+  "codeFrom": "worktree",
+  "codeSha": "cc98776b00f2",
+  "probeInfo": {
+    "kids": 4
+  },
+  "tag": "after",
+  "at": "2026-10-31T05:23:13.146Z",
+  "desktop": {
+    "view": "view-workbench",
+    "statDensity": [
+      {
+        "role": "c",
+        "w": 270,
+        "h": 166,
+        "chars": 28,
+        "per1e4": 6.25,
+        "text": "卡池全部 →9张8 人提交 · 候选 4 · 已立项 3"
+      },
+      {
+        "role": "d",
+        "w": 270,
+        "h": 166,
+        "chars": 24,
+        "per1e4": 5.35,
+        "text": "最高分82分↑6高于卡池均分 76 · 最高一张"
+      },
+      {
+        "role": "e",
+        "w": 270,
+        "h": 166,
+        "chars": 44,
+        "per1e4": 9.82,
+        "text": "已交成果26份10 人交了 · 共 11 人 · 最多是 S2 用户需求与痛点(3 份)"
+      },
+      {
+        "role": "f",
+        "w": 270,
+        "h": 166,
+        "chars": 27,
+        "per1e4": 6.02,
+        "text": "待决策4张5 张已立项 / 否决 · 候选还剩 4 张"
+      }
+    ],
+    "hues": [
+      [
+        "red/orange",
+        1
+      ]
+    ],
+    "btns": [
+      {
+        "label": "候选",
+        "h": 20,
+        "bg": "rgba(0, 0, 0, 0)",
+        "border": "0px rgb(75, 85, 99)"
+      },
+      {
+        "label": "已立项",
+        "h": 20,
+        "bg": "rgba(0, 0, 0, 0)",
+        "border": "0px rgb(75, 85, 99)"
+      },
+      {
+        "label": "已否决",
+        "h": 20,
+        "bg": "rgba(0, 0, 0, 0)",
+        "border": "0px rgb(75, 85, 99)"
+      },
+      {
+        "label": "候选",
+        "h": 20,
+        "bg": "rgba(0, 0, 0, 0)",
+        "border": "0px rgb(75, 85, 99)"
+      },
+      {
+        "label": "已立项",
+        "h": 20,
+        "bg": "rgba(0, 0, 0, 0)",
+        "border": "0px rgb(75, 85, 99)"
+      },
+      {
+        "label": "已否决",
+        "h": 20,
+        "bg": "rgba(0, 0, 0, 0)",
+        "border": "0px rgb(75, 85, 99)"
+      },
+      {
+        "label": "回到顶部",
+        "h": 28,
+        "bg": "rgba(0, 0, 0, 0)",
+        "border": "1px rgba(0, 0, 0, 0)"
+      }
+    ],
+    "rows": [
+      {
+        "cls": "row ab grid2 kf-enter",
+        "cols": "79.3281px 79.3281px 79.3281px 79.3281px 79.3281px 79.3281px 79.3281px 79.3281px 79.3281px 79.3281px 79.3281px 79.3281px",
+        "kids": [
+          {
+            "role": "a",
+            "x": 272,
+            "y": 172.9,
+            "w": 746.7,
+            "h": 373.6,
+            "top": 172.9,
+            "bottom": 546.5
+          },
+          {
+            "role": "b",
+            "x": 1034.7,
+            "y": 172.9,
+            "w": 365.3,
+            "h": 373.6,
+            "top": 172.9,
+            "bottom": 546.5
+          }
+        ]
+      },
+      {
+        "cls": "row stats kf-enter",
+        "cols": "79.3281px 79.3281px 79.3281px 79.3281px 79.3281px 79.3281px 79.3281px 79.3281px 79.3281px 79.3281px 79.3281px 79.3281px",
+        "kids": [
+          {
+            "role": "c",
+            "x": 272,
+            "y": 611.5,
+            "w": 270,
+            "h": 166,
+            "top": 611.5,
+            "bottom": 777.5
+          },
+          {
+            "role": "d",
+            "x": 558,
+            "y": 611.5,
+            "w": 270,
+            "h": 166,
+            "top": 611.5,
+            "bottom": 777.5
+          },
+          {
+            "role": "e",
+            "x": 844,
+            "y": 611.5,
+            "w": 270,
+            "h": 166,
+            "top": 611.5,
+            "bottom": 777.5
+          },
+          {
+            "role": "f",
+            "x": 1130,
+            "y": 611.5,
+            "w": 270,
+            "h": 166,
+            "top": 611.5,
+            "bottom": 777.5
+          }
+        ]
+      },
+      {
+        "cls": "row cards-row kf-enter",
+        "cols": "79.3281px 79.3281px 79.3281px 79.3281px 79.3281px 79.3281px 79.3281px 79.3281px 79.3281px 79.3281px 79.3281px 79.3281px",
+        "kids": [
+          {
+            "role": "g",
+            "x": 1034.7,
+            "y": 842.5,
+            "w": 365.3,
+            "h": 410.4,
+            "top": 842.5,
+            "bottom": 1252.9
+          }
+        ]
+      },
+      {
+        "cls": "cards",
+        "cols": "365.328px 365.328px",
+        "kids": [
+          {
+            "role": "",
+            "x": 272,
+            "y": 876.9,
+            "w": 365.3,
+            "h": 273.6,
+            "top": 876.9,
+            "bottom": 1150.5
+          },
+          {
+            "role": "",
+            "x": 653.3,
+            "y": 876.9,
+            "w": 365.3,
+            "h": 273.6,
+            "top": 876.9,
+            "bottom": 1150.5
+          }
+        ]
+      }
+    ],
+    "A": {
+      "x": 272,
+      "y": 172.9,
+      "w": 746.7,
+      "h": 373.6,
+      "top": 172.9,
+      "bottom": 546.5
+    },
+    "B": {
+      "x": 1034.7,
+      "y": 172.9,
+      "w": 365.3,
+      "h": 373.6,
+      "top": 172.9,
+      "bottom": 546.5
+    },
+    "C": {
+      "x": 272,
+      "y": 611.5,
+      "w": 270,
+      "h": 166,
+      "top": 611.5,
+      "bottom": 777.5
+    },
+    "D": {
+      "x": 558,
+      "y": 611.5,
+      "w": 270,
+      "h": 166,
+      "top": 611.5,
+      "bottom": 777.5
+    },
+    "E": {
+      "x": 844,
+      "y": 611.5,
+      "w": 270,
+      "h": 166,
+      "top": 611.5,
+      "bottom": 777.5
+    },
+    "F": {
+      "x": 1130,
+      "y": 611.5,
+      "w": 270,
+      "h": 166,
+      "top": 611.5,
+      "bottom": 777.5
+    },
+    "bento": {
+      "A": 746.7,
+      "B": 365.3,
+      "aCols": 8,
+      "colDbg": {
+        "n": 12,
+        "w": 1152,
+        "gapRow": 16,
+        "colW": 81.33333333333333
+      },
+      "ABheightDiff": 0,
+      "CDE": {
+        "w": [
+          270,
+          270,
+          270
+        ],
+        "h": [
+          166,
+          166,
+          166
+        ],
+        "hDiff": 0
+      },
+      "F": {
+        "x": 1130,
+        "y": 611.5,
+        "w": 270,
+        "h": 166,
+        "top": 611.5,
+        "bottom": 777.5
+      }
+    },
+    "Bbg": {
+      "r": 24,
+      "g": 24,
+      "b": 27,
+      "a": 1
+    },
+    "BbgText": "rgb(24, 24, 27)",
+    "Bcontrast": {
+      "t": "现场",
+      "fg": {
+        "r": 180,
+        "g": 180,
+        "b": 188,
+        "a": 1
+      },
+      "c": 8.6
+    },
+    "Bother": [
+      {
+        "t": "现场",
+        "c": 8.6
+      },
+      {
+        "t": "1",
+        "c": 16.12
+      },
+      {
+        "t": "人卡住",
+        "c": 8.6
+      },
+      {
+        "t": "何雨",
+        "c": 16.12
+      },
+      {
+        "t": "S4 机会验证评估 · 13 条",
+        "c": 8.6
+      },
+      {
+        "t": "76h",
+        "c": 16.12
+      },
+      {
+        "t": "各阶段已交",
+        "c": 8.6
+      },
+      {
+        "t": "S1 品类市场分析",
+        "c": 8.6
+      },
+      {
+        "t": "2/11",
+        "c": 8.6
+      },
+      {
+        "t": "S2 用户需求与痛点",
+        "c": 8.6
+      },
+      {
+        "t": "3/11",
+        "c": 8.6
+      },
+      {
+        "t": "S3 机会点识别",
+        "c": 8.6
+      },
+      {
+        "t": "3/11",
+        "c": 8.6
+      },
+      {
+        "t": "S4 机会验证评估",
+        "c": 8.6
+      },
+      {
+        "t": "2/11",
+        "c": 8.6
+      },
+      {
+        "t": "S5 场景与卖点设计",
+        "c": 8.6
+      },
+      {
+        "t": "2/11",
+        "c": 8.6
+      },
+      {
+        "t": "11 人 · 9 张卡在推进",
+        "c": 8.6
+      }
+    ],
+    "gaps": [
+      {
+        "between": "div.p-head → div.bento",
+        "gap": 32
+      },
+      {
+        "between": "div.bento → footer.cfoot",
+        "gap": 47
+      }
+    ],
+    "facePad": [
+      {
+        "role": "a",
+        "pad": "20px"
+      },
+      {
+        "role": "b",
+        "pad": "20px"
+      },
+      {
+        "role": "c",
+        "pad": "16px"
+      },
+      {
+        "role": "d",
+        "pad": "16px"
+      },
+      {
+        "role": "e",
+        "pad": "16px"
+      },
+      {
+        "role": "f",
+        "pad": "16px"
+      },
+      {
+        "role": "card",
+        "pad": "20px"
+      },
+      {
+        "role": "card",
+        "pad": "20px"
+      },
+      {
+        "role": "g",
+        "pad": "20px"
+      }
+    ],
+    "fonts": {
+      "tiers": [
+        "12px",
+        "14px",
+        "16px",
+        "20px",
+        "28px",
+        "32px"
+      ],
+      "count": {
+        "20px": 13,
+        "14px": 16,
+        "16px": 9,
+        "12px": 75,
+        "28px": 7,
+        "32px": 1
+      },
+      "where": {
+        "20px": [
+          "h2.tb-cap",
+          "div.avatar",
+          "span.n",
+          "b"
+        ],
+        "14px": [
+          "p.kf-prose",
+          "a.go",
+          "a.t",
+          "span",
+          "span.nm",
+          "span.h",
+          "p.lead",
+          "a",
+          "a.feed-more"
+        ],
+        "16px": [
+          "h3",
+          "span.lbl"
+        ],
+        "12px": [
+          "span.sub",
+          "span",
+          "span.b",
+          "span.sf",
+          "div.lbl",
+          "span.meta",
+          "a.pill",
+          "span.lbl",
+          "span.rk-k",
+          "span.rk-n",
+          "div.live-foot",
+          "a.go",
+          "span.u",
+          "span.t",
+          "span.delta",
+          "span.badge",
+          "span.opp-st",
+          "button",
+          "span.more",
+          "span.st",
+          "span.tm",
+          "span.cfl",
+          "button.kf-btn"
+        ],
+        "28px": [
+          "b"
+        ],
+        "32px": [
+          "b"
+        ]
+      }
+    },
+    "sparks": {
+      "inView": 0,
+      "inPage": 0
+    },
+    "overflow": {
+      "scrollWidth": 1440,
+      "clientWidth": 1440
+    }
+  },
+  "desktopDet": {
+    "view": "view-workbench",
+    "statDensity": [
+      {
+        "role": "c",
+        "w": 270,
+        "h": 166,
+        "chars": 28,
+        "per1e4": 6.25,
+        "text": "卡池全部 →5张5 人提交 · 候选 2 · 已立项 2"
+      },
+      {
+        "role": "d",
+        "w": 270,
+        "h": 166,
+        "chars": 24,
+        "per1e4": 5.35,
+        "text": "最高分82分↑7高于卡池均分 75 · 最高一张"
+      },
+      {
+        "role": "e",
+        "w": 270,
+        "h": 166,
+        "chars": 44,
+        "per1e4": 9.82,
+        "text": "已交成果26份10 人交了 · 共 11 人 · 最多是 S2 用户需求与痛点(3 份)"
+      },
+      {
+        "role": "f",
+        "w": 270,
+        "h": 166,
+        "chars": 27,
+        "per1e4": 6.02,
+        "text": "待决策2张3 张已立项 / 否决 · 候选还剩 2 张"
+      }
+    ],
+    "hues": [
+      [
+        "red/orange",
+        1
+      ]
+    ],
+    "btns": [
+      {
+        "label": "候选",
+        "h": 20,
+        "bg": "rgba(0, 0, 0, 0)",
+        "border": "0px rgb(75, 85, 99)"
+      },
+      {
+        "label": "已立项",
+        "h": 20,
+        "bg": "rgba(0, 0, 0, 0)",
+        "border": "0px rgb(75, 85, 99)"
+      },
+      {
+        "label": "已否决",
+        "h": 20,
+        "bg": "rgba(0, 0, 0, 0)",
+        "border": "0px rgb(75, 85, 99)"
+      },
+      {
+        "label": "候选",
+        "h": 20,
+        "bg": "rgba(0, 0, 0, 0)",
+        "border": "0px rgb(75, 85, 99)"
+      },
+      {
+        "label": "已立项",
+        "h": 20,
+        "bg": "rgba(0, 0, 0, 0)",
+        "border": "0px rgb(75, 85, 99)"
+      },
+      {
+        "label": "已否决",
+        "h": 20,
+        "bg": "rgba(0, 0, 0, 0)",
+        "border": "0px rgb(75, 85, 99)"
+      },
+      {
+        "label": "回到顶部",
+        "h": 28,
+        "bg": "rgba(0, 0, 0, 0)",
+        "border": "1px rgba(0, 0, 0, 0)"
+      }
+    ],
+    "rows": [
+      {
+        "cls": "row ab grid2 kf-enter",
+        "cols": "79.3281px 79.3281px 79.3281px 79.3281px 79.3281px 79.3281px 79.3281px 79.3281px 79.3281px 79.3281px 79.3281px 79.3281px",
+        "kids": [
+          {
+            "role": "a",
+            "x": 272,
+            "y": 172.9,
+            "w": 746.7,
+            "h": 373.6,
+            "top": 172.9,
+            "bottom": 546.5
+          },
+          {
+            "role": "b",
+            "x": 1034.7,
+            "y": 172.9,
+            "w": 365.3,
+            "h": 373.6,
+            "top": 172.9,
+            "bottom": 546.5
+          }
+        ]
+      },
+      {
+        "cls": "row stats kf-enter",
+        "cols": "79.3281px 79.3281px 79.3281px 79.3281px 79.3281px 79.3281px 79.3281px 79.3281px 79.3281px 79.3281px 79.3281px 79.3281px",
+        "kids": [
+          {
+            "role": "c",
+            "x": 272,
+            "y": 611.5,
+            "w": 270,
+            "h": 166,
+            "top": 611.5,
+            "bottom": 777.5
+          },
+          {
+            "role": "d",
+            "x": 558,
+            "y": 611.5,
+            "w": 270,
+            "h": 166,
+            "top": 611.5,
+            "bottom": 777.5
+          },
+          {
+            "role": "e",
+            "x": 844,
+            "y": 611.5,
+            "w": 270,
+            "h": 166,
+            "top": 611.5,
+            "bottom": 777.5
+          },
+          {
+            "role": "f",
+            "x": 1130,
+            "y": 611.5,
+            "w": 270,
+            "h": 166,
+            "top": 611.5,
+            "bottom": 777.5
+          }
+        ]
+      },
+      {
+        "cls": "row cards-row kf-enter",
+        "cols": "79.3281px 79.3281px 79.3281px 79.3281px 79.3281px 79.3281px 79.3281px 79.3281px 79.3281px 79.3281px 79.3281px 79.3281px",
+        "kids": [
+          {
+            "role": "g",
+            "x": 1034.7,
+            "y": 842.5,
+            "w": 365.3,
+            "h": 410.4,
+            "top": 842.5,
+            "bottom": 1252.9
+          }
+        ]
+      },
+      {
+        "cls": "cards",
+        "cols": "365.328px 365.328px",
+        "kids": [
+          {
+            "role": "",
+            "x": 272,
+            "y": 876.9,
+            "w": 365.3,
+            "h": 273.6,
+            "top": 876.9,
+            "bottom": 1150.5
+          },
+          {
+            "role": "",
+            "x": 653.3,
+            "y": 876.9,
+            "w": 365.3,
+            "h": 273.6,
+            "top": 876.9,
+            "bottom": 1150.5
+          }
+        ]
+      }
+    ],
+    "A": {
+      "x": 272,
+      "y": 172.9,
+      "w": 746.7,
+      "h": 373.6,
+      "top": 172.9,
+      "bottom": 546.5
+    },
+    "B": {
+      "x": 1034.7,
+      "y": 172.9,
+      "w": 365.3,
+      "h": 373.6,
+      "top": 172.9,
+      "bottom": 546.5
+    },
+    "C": {
+      "x": 272,
+      "y": 611.5,
+      "w": 270,
+      "h": 166,
+      "top": 611.5,
+      "bottom": 777.5
+    },
+    "D": {
+      "x": 558,
+      "y": 611.5,
+      "w": 270,
+      "h": 166,
+      "top": 611.5,
+      "bottom": 777.5
+    },
+    "E": {
+      "x": 844,
+      "y": 611.5,
+      "w": 270,
+      "h": 166,
+      "top": 611.5,
+      "bottom": 777.5
+    },
+    "F": {
+      "x": 1130,
+      "y": 611.5,
+      "w": 270,
+      "h": 166,
+      "top": 611.5,
+      "bottom": 777.5
+    },
+    "bento": {
+      "A": 746.7,
+      "B": 365.3,
+      "aCols": 8,
+      "colDbg": {
+        "n": 12,
+        "w": 1152,
+        "gapRow": 16,
+        "colW": 81.33333333333333
+      },
+      "ABheightDiff": 0,
+      "CDE": {
+        "w": [
+          270,
+          270,
+          270
+        ],
+        "h": [
+          166,
+          166,
+          166
+        ],
+        "hDiff": 0
+      },
+      "F": {
+        "x": 1130,
+        "y": 611.5,
+        "w": 270,
+        "h": 166,
+        "top": 611.5,
+        "bottom": 777.5
+      }
+    },
+    "Bbg": {
+      "r": 24,
+      "g": 24,
+      "b": 27,
+      "a": 1
+    },
+    "BbgText": "rgb(24, 24, 27)",
+    "Bcontrast": {
+      "t": "现场",
+      "fg": {
+        "r": 180,
+        "g": 180,
+        "b": 188,
+        "a": 1
+      },
+      "c": 8.6
+    },
+    "Bother": [
+      {
+        "t": "现场",
+        "c": 8.6
+      },
+      {
+        "t": "1",
+        "c": 16.12
+      },
+      {
+        "t": "人卡住",
+        "c": 8.6
+      },
+      {
+        "t": "何雨",
+        "c": 16.12
+      },
+      {
+        "t": "S4 机会验证评估 · 13 条",
+        "c": 8.6
+      },
+      {
+        "t": "76h",
+        "c": 16.12
+      },
+      {
+        "t": "各阶段已交",
+        "c": 8.6
+      },
+      {
+        "t": "S1 品类市场分析",
+        "c": 8.6
+      },
+      {
+        "t": "2/11",
+        "c": 8.6
+      },
+      {
+        "t": "S2 用户需求与痛点",
+        "c": 8.6
+      },
+      {
+        "t": "3/11",
+        "c": 8.6
+      },
+      {
+        "t": "S3 机会点识别",
+        "c": 8.6
+      },
+      {
+        "t": "3/11",
+        "c": 8.6
+      },
+      {
+        "t": "S4 机会验证评估",
+        "c": 8.6
+      },
+      {
+        "t": "2/11",
+        "c": 8.6
+      },
+      {
+        "t": "S5 场景与卖点设计",
+        "c": 8.6
+      },
+      {
+        "t": "2/11",
+        "c": 8.6
+      },
+      {
+        "t": "11 人 · 5 张卡在推进",
+        "c": 8.6
+      }
+    ],
+    "gaps": [
+      {
+        "between": "div.p-head → div.bento",
+        "gap": 32
+      },
+      {
+        "between": "div.bento → footer.cfoot",
+        "gap": 47
+      }
+    ],
+    "facePad": [
+      {
+        "role": "a",
+        "pad": "20px"
+      },
+      {
+        "role": "b",
+        "pad": "20px"
+      },
+      {
+        "role": "c",
+        "pad": "16px"
+      },
+      {
+        "role": "d",
+        "pad": "16px"
+      },
+      {
+        "role": "e",
+        "pad": "16px"
+      },
+      {
+        "role": "f",
+        "pad": "16px"
+      },
+      {
+        "role": "card",
+        "pad": "20px"
+      },
+      {
+        "role": "card",
+        "pad": "20px"
+      },
+      {
+        "role": "g",
+        "pad": "20px"
+      }
+    ],
+    "fonts": {
+      "tiers": [
+        "12px",
+        "14px",
+        "16px",
+        "20px",
+        "28px",
+        "32px"
+      ],
+      "count": {
+        "20px": 13,
+        "14px": 16,
+        "16px": 9,
+        "12px": 73,
+        "28px": 7,
+        "32px": 1
+      },
+      "where": {
+        "20px": [
+          "h2.tb-cap",
+          "div.avatar",
+          "span.n",
+          "b"
+        ],
+        "14px": [
+          "p.kf-prose",
+          "a.go",
+          "div.me-none",
+          "span",
+          "span.nm",
+          "span.h",
+          "p.lead",
+          "a",
+          "a.feed-more"
+        ],
+        "16px": [
+          "h3",
+          "span.lbl"
+        ],
+        "12px": [
+          "span.sub",
+          "span",
+          "span.b",
+          "span.sf",
+          "div.lbl",
+          "span.lbl",
+          "span.meta",
+          "span.rk-k",
+          "span.rk-n",
+          "div.live-foot",
+          "a.go",
+          "span.u",
+          "span.t",
+          "span.delta",
+          "span.badge",
+          "span.opp-st",
+          "button",
+          "span.more",
+          "span.st",
+          "span.tm",
+          "span.cfl",
+          "button.kf-btn"
+        ],
+        "28px": [
+          "b"
+        ],
+        "32px": [
+          "b"
+        ]
+      }
+    },
+    "sparks": {
+      "inView": 0,
+      "inPage": 0
+    },
+    "overflow": {
+      "scrollWidth": 1440,
+      "clientWidth": 1440
+    }
+  },
+  "glass": {
+    "/": {
+      "blur": 0,
+      "shadow": 1,
+      "blurWhere": [],
+      "shadowWhere": [
+        "a"
+      ]
+    },
+    "/pool": {
+      "blur": 0,
+      "shadow": 1,
+      "blurWhere": [],
+      "shadowWhere": [
+        "a"
+      ]
+    },
+    "/stages": {
+      "blur": 0,
+      "shadow": 1,
+      "blurWhere": [],
+      "shadowWhere": [
+        "a"
+      ]
+    },
+    "/reports": {
+      "blur": 6,
+      "shadow": 6,
+      "blurWhere": [
+        "div.panel.kf-glass.is-overlay"
+      ],
+      "shadowWhere": [
+        "div.panel.kf-glass.is-overlay"
+      ]
+    }
+  },
+  "noMe": {
+    "roles": [
+      "a",
+      "b",
+      "c",
+      "d",
+      "e",
+      "f",
+      "g"
+    ],
+    "Aheight": 374,
+    "allstages": 5,
+    "text": "我的推进五阶段进度与现场状态 机会卡池 → ? 还没选「我是」先看全员:11 人 · 已交 10 人 选我 S1 2 品类市场分析 S2 3 用户需求与痛点 S3 3 机会点识别 S4 2 机会验证评估 S5 2 场景与卖点设计 卡池进度 候选 4 张 · 44% 已立项 3 张 · 33% 已否决 2 张 · 22% 现场 1人卡住 何雨S4 机会验证评估 · 13 条 76h 各阶段已交 S1 品类市场分析 2/11 S2 用户需求与痛点 3/11 S3 机会点识别 3/"
+  },
+  "nobody": {
+    "kids": 4,
+    "roles": [
+      "a",
+      "b",
+      "c",
+      "d",
+      "e",
+      "f",
+      "g"
+    ],
+    "Aheight": 411,
+    "text": "我的推进五阶段进度与现场状态 机会卡池 → _ _nobody_已完成 0 / 5 个阶段 · 累计 0 份成果 0份成果 0张卡 4待评分 S1 — 品类市场分析 S2 — 用户需求与痛点 S3 — 机会点识别 S4 — 机会验证评估 S5 — 场景与卖点设计 待评分 4 夜宵场景低卡高蛋白拌面 6 人 · 均 81 81 地域风味麻酱拌面 4 人 · 均 76 76 银发低钠汤面 1 人 · 均 70 70 夏日清爽凉拌面 1 人 · 均 76 76 现场 1人卡住 何雨",
+    "probe": {
+      "kids": 4
+    }
+  },
+  "noStall": {
+    "stalledItems": 0,
+    "bText": "现场 大家都在动 没有超过 8 小时没交成果的人 各阶段已交 S1 品类市场分析 2/11 S2 用户需求与痛点 3/11 S3 机会点识别 3/11 S4 机会验证评估 2/11 S5 场景与卖点设计 2/11 11 人 · 9 张卡在推",
+    "aH": 349,
+    "bH": 349,
+    "dot": "rgb(180, 180, 188)",
+    "bg": "rgb(24, 24, 27)"
+  },
+  "stalledCount": 1,
+  "mobile": {
+    "scrollWidth": 393,
+    "clientWidth": 393,
+    "A": {
+      "x": 28,
+      "y": 166,
+      "w": 337,
+      "h": 370.8
+    },
+    "B": {
+      "x": 28,
+      "y": 552.8,
+      "w": 337,
+      "h": 400.8
+    },
+    "stacked": true,
+    "CDE": [
+      {
+        "x": 28,
+        "y": 986.5,
+        "w": 162.5,
+        "h": 132
+      },
+      {
+        "x": 202.5,
+        "y": 986.5,
+        "w": 162.5,
+        "h": 132
+      },
+      {
+        "x": 28,
+        "y": 1130.5,
+        "w": 162.5,
+        "h": 150
+      }
+    ],
+    "fonts": [
+      "12px",
+      "14px",
+      "16px",
+      "20px",
+      "28px",
+      "32px"
+    ],
+    "fontSizeCount": {
+      "20px": 17,
+      "14px": 21,
+      "16px": 5,
+      "12px": 72,
+      "28px": 3,
+      "32px": 1
+    },
+    "overflowing": []
+  },
+  "v12": null,
+  "errors": []
+}

+ 105 - 0
docs/v16-before-after.md

@@ -0,0 +1,105 @@
+# v16 改前 / 改后
+
+> 所有数字来自 `scripts/verify-v16.mjs`(= v15 的 1–9 项 + v16 的 10–12 项)与 `scripts/verify-visual-v12.mjs`,
+> 改前基线用 `--from HEAD` 从 git 导出旧代码单独起服务量(代码指纹见下),没有手填。
+> 截图:`docs/v16-before-desktop.png` / `docs/v16-after-desktop.png` / `v16-after-desktop-me.png`(选了赵磊)/
+> `v16-after-desktop-nobody.png`(本月无成果)/ `v16-after-mobile.png`(393)。
+
+改前代码 `view-workbench.js` sha1 `756ce7847569`(HEAD 2010104),改后 **`cc98776b00f2`**(同见 `docs/v16-after.json` 头部)。
+
+附带:`scripts/verify-cjk.mjs`(中英混排 U+2009)本轮由 ✗(7 处普通空格残留、样本 2/3)→ **✓ 全部达标**:工作台新写的动态文案都走了 `t()`,机会卡「分 · N 人」和「N 天前」也补上了。
+
+## 1. 任务书里的 5 个遗留问题
+
+| # | 问题 | 改前 | 改后 | 依据 |
+|---|---|---|---|---|
+| 1 | **三张短卡太空** | 3 张 360×194,内容密度 **3.44 / 2.58 / 2.72**(字/万 px²),第三张还是内部视角的「我的环节」 | **4 张 270×166**,密度 **6.25 / 5.35 / 9.82 / 6.02**;dr1 基准 8.6。3/4 张 ≥ 基准 70%,D 卡(最高分)仍 **低于** 基准 —— **见 §4 第 1 条,没完全做到** | verify-v16 [10] |
+| 2 | 深色卡颜色太杂 | **4 种色相**(red/orange×2、blue×2、teal×1、rose×1 = S1–S5 五色点) | **1 种**(琥珀 = 「人卡住」那一个点);阶段进度改灰阶细条;「我的环节」已删;时间线状态点也改成灰阶明度 | verify-v16 [11],dr6 深卡实测色相 1 |
+| 3 | A 卡「待评分」一行中间一个大空白 | 左右两栏 + 右栏 `margin-top:auto` 贴底 | 改成**一条通栏列表**:卡名吃掉剩余宽度,右侧「N 人 · 均 X」+ 方角胶囊(76 / 去评),中缝 32px 消失 | 量图 dr1「左标题 + 右值」 |
+| 4 | 底部按钮笨重 | C 卡一颗 `32px` 紫底实心按钮「全部 →」;机会卡里 3 个 26px 灰底分段按钮 | 紫按钮删除,改文字链「全部 →」;分段改**静默文字行**(20px 高、无底无描边、选中项加粗 + 1px 下划线) | verify-v16 [12]:工作台 >32px 的按钮 0 个;dr6/dr1 卡内动作都是文字 |
+| 5 | v12 回归:左卡连续空白 56px | **56px**(v15 报告;本次用同一脚本复测 **68px**,实时数据下) | **54px** | **没有解决 → 见 §4 第 2 条(两个约束数值互斥)** |
+
+## 2. v15 的 1–9 项(回归,全部保留)
+
+| 项 | 改前 | 改后 | 要求 |
+|---|---|---|---|
+| A / B 列宽 | 8 : 4(744 / 360) | 8 : 4(**746.7 / 365.3**,12 列 + gap 16) | 8 : 4 ✓ |
+| A/B 高度差 | 0px | **0px** | ≤2 ✓ |
+| B 卡底色 / 最差对比度 | rgb(24,24,27),8.6:1 | rgb(24,24,27),**8.6:1** | <0.35 亮度 / ≥7 ✓ |
+| 短卡高差 | 0px | **0px**(166 / 166 / 166) | ≤2 ✓ |
+| 主列块间距 | 32 / 47 | **32 / 47** | 32 ✓ |
+| 卡片内边距 | 全部 20 | **20×5 + 16×4**(4 张短卡 16) | 20 ✗ → **有意为之**,见 §4 第 3 条 |
+| 字号档 | 5 档(12/14/16/20/32) | **6 档**(12/14/16/20/**28**/32) | ≤5 ✗ → 有意为之,见 §4 第 4 条 |
+| kf-spark | 0 | 0 | ✓ |
+| 普通卡片玻璃 / 阴影 | 0 / 1(四页) | **0 / 1**(/reports 的 6 个是打开的筛选浮层) | ✓ |
+| 空态:未选身份 | A 363px | A **378px**,五阶段全员,无虚线框,卡池进度 3 行 | 实测 |
+| 空态:本月无成果 | A 561px | A **427px**(-134);「最高分」卡换成「本月成果 0 份」;待评分 4 条真实列出 | 实测 |
+| 空态:没人卡住 | 仍深底 + **绿点** | 仍深底 + **灰点**(灰阶,去掉了第 2 个色相) | 实测 |
+| 移动端 393 | 无溢出 | scrollWidth **393 = 393**;A/B 堆叠;短卡 2×2;字号 6 档 | 无溢出 ✓ |
+| v12 视觉 | 字号 7 档 / P3 失败 | 字号 **6 档**(v12 上限 5,仍 ✗);**圆角违规清零**(0/8/12 三种);P3 **仍 ✗** | 见 §5 |
+
+## 3. v12 视觉复测(同一脚本,同一页 /)
+
+| 检查 | v15 改后 | v16 改后 |
+|---|---|---|
+| 圆角种类 ∈ {8,12,20} | ✗ 4px×4 | **✓** |
+| 原生外观控件 / 描边 / 纯黑 | ✓ | ✓ |
+| 字号档 ≤5 | ✗ 5 档(12/14/16/20/32) | ✗ 6 档(12/14/16/20/28/32) |
+| 同排卡片高度差 | ✓ | ✓(6 行 0 超差) |
+| 卡内连续空白 ≤24 | 5 处 | **4 处**(都是短卡「数字 → 结论行」两端的 35–37px,dr1 同类间距 31/30;短卡整卡 166px,再压就没形状了) |
+| hover / P1 / P2 / 底部空白 | ✓ | ✓ |
+| P3 两栏等高 + 左卡 ≤32px | ✗ 56px | **✗ 54px**(见 §4-2) |
+
+## 4. 没做到的、和「没按旧标准做」的,逐条说清
+
+### 1. 短卡密度:D 卡(最高分)仍低于 dr1 基准
+
+24 字 / 270×166 = **5.35**(基准 8.6,线 6.02)。原因:D 卡的全部内容就是「最高分 / 82 / 高于卡池均分 75 · 最高一张」,
+**没有更多真实数据可写**(每张卡的评分人数、评分分布 core 里没有)。为了让数字变好看去编一条,违反任务书「不许硬加信息」,
+所以**停在这里**,并把结论行写满到「谁的最高 / 和谁比 / 差多少」,不再留空。C、E、F 三张过了线。
+另外 4 张卡底边对齐后,数字与结论行之间是 37px 空白(v12 记的 4 处),这是「结论行贴底」换来的 —— dr1 是数字→结论 31px、结论→底 30px,我们数字→结论 37px、结论→底 20px,分布不同但总量接近。
+
+### 2. 左卡连续空白 54px:两个约束在数值上互斥(本次找到了「互斥」的证据)
+
+v12 的量法(`verify-visual-v12.mjs` P3):取左卡里「**有自己的文字节点**的叶子」按顶边排序,量相邻两个叶子之间的竖直间隙,取最大。
+
+- 现在左卡的文字叶子只有三组:**状态名**(如「已立项」)→ **第一行数字**(如第 1 行「4 张 · 44%」,Y≈508)→ 第 2 行(Y≈562)。
+  54px 就是「状态名行底」到「第一行数字顶」的间隙 —— 即 **「卡池进度」这一行小标签到它下面第一行列表之间的距离**。
+- 两种把 54px 压进 32px 以内的做法,都会换来更差的版面:
+  - 列表 `margin-top:auto` 贴底:状态名行被推到 Y≈333 附近,**状态名 → 第一行列表** 之间变成 **54px**(现在是 0)、
+    **阶段格 → 「卡池进度」标签** 之间变成 **~85px** —— 只是把洞挪到上面,而且更大;
+  - 把行距收到 30px 以下:A 卡从 466px 掉到 374px,B 卡(深色,跟着行高走)跟着矮 92px,
+    「各阶段已交」5 行下面出现一块没有任何内容的黑底。
+  也就是说:**要满足 P3,就要在卡里制造一个更大的空白或让深色卡出现空白;要版面没有洞,P3 就必然失败。**
+  这两个约束没有同时成立的数值解 —— 底下是本次没有解决它的实测依据,不是「没时间做」。
+- 所以本轮选的是「P3 仍然失败(54px,比 v15 的 56/68 略小),但不制造新的空白」。**明确记为未解决。**
+
+### 3. 卡片内边距不是全部 20:4 张短卡是 16
+
+任务书 v15 定的是「卡片内边距 20」,但 v16 的短卡从 360 宽缩到 270 宽(筛了 dr1 的 232–249 + gap 8)。同样 20px 内边距在 270 宽的卡上占掉 14.8% 的宽度,
+20 时四张卡密度是 5.96 / 5.11 / 9.37 / 4.47,16 时是 6.25 / 5.35 / 9.82 / 6.02 —— **短卡用 16 是密度要求倒逼的**,A/B/G 仍是 20。
+凡是「同一页 padding 必须一致」的检查会报 ✗,这是有意的取舍,没有在文档里隐藏。
+
+### 4. 字号档从 5 变 6:新增 28px
+
+dr1 大数字 28、dr6 主数字 24(cap 高折算)、页头 KPI 36、卡标题 16;我们原来 20 当数字、32 当大数字,**中间缺一档**。
+本轮把大数字统一成 **28/500**(dr1 数字比标题细、linear 只 400/510 两档),于是档位变成 **12 / 14 / 16 / 20 / 28 / 32**:
+- 20 只留给头像字与阶段格数字(它们是「小数字」,跟着区块走);
+- 32 只剩 B 卡「1 人卡住」一个(深卡唯一的大数字,对应 dr6 页头 KPI 的量级)。
+v12「≤5 档」是旧任务书的指标;本任务书没把 5 档写进硬约束,v15 任务书也只写了「≤ 5 档、无 11px」。**这里明确违背 v12 的档位数,理由如上;11px 已清零。**
+
+### 5. 调研本身没拿到的
+
+见 `docs/v16-research.md` §7:Crextio 的 Remix 源码(要登录)、Donezo 的一手 CSS(dribbble 202 空壳)、SugarCRM 明文文档(301 到 sugarai 后是压缩/登录壳)。
+dr7 不在 `refs/` 里,所以本轮**没有引用 SugarCRM 任何数值**。
+
+## 5. 一句话自检
+
+把 `docs/v16-after-desktop.png` 和 `refs/dr6.png` / `refs/dr1.png` 并排:骨架(浅底 + 一块深卡 + 一排窄数据卡 + 下方内容块)、卡片圆角 12、
+16px 卡标题 + 28px 细数字 + 12px 副文的阶梯、卡内无按钮组、深色卡只有一个琥珀点 —— 这些都是**按图量出来的同一个体系**。
+与 dr6/dr1 仍有差距的地方:dr6/dr1 的小卡都有图形(柱状、圆环),我们 4 张短卡是纯文字 + 趋势徽章(**没做到**:项目里没有可用的时序数据,硬画就是编数据)。
+
+还没做到 / 不在本轮范围:
+- **SugarCRM(dr7)**:不在 `refs/` 里、官方文档站 301 后无明文,本轮没有可量数据,也没有引用。
+- **短卡图形**:调研结论是 dr6 的小卡靠「图形 + 多块」不空(§1.2),我们只做到了「多块 + 结论行 + 一个趋势徽章」;没有时序数据,不画假图。
+- v12 的三项旧指标(padding 全 20 / 字号 ≤5 / P3)本轮**有意不满足或未解决**,理由都在 §4。

BIN
docs/v16-before-desktop.png


+ 1137 - 0
docs/v16-before.json

@@ -0,0 +1,1137 @@
+{
+  "codeFrom": "HEAD",
+  "codeSha": "756ce7847569",
+  "probeInfo": {
+    "kids": 4
+  },
+  "tag": "before",
+  "at": "2026-10-31T05:07:25.220Z",
+  "desktop": {
+    "view": "view-workbench",
+    "statDensity": [
+      {
+        "role": "c",
+        "w": 360,
+        "h": 194,
+        "chars": 24,
+        "per1e4": 3.44,
+        "text": "卡池全部 9张8 人提交 · 3 个 Agent"
+      },
+      {
+        "role": "d",
+        "w": 360,
+        "h": 194,
+        "chars": 18,
+        "per1e4": 2.58,
+        "text": "最高分82分办公室 3 分钟速食拌面"
+      },
+      {
+        "role": "e",
+        "w": 360,
+        "h": 194,
+        "chars": 19,
+        "per1e4": 2.72,
+        "text": "我的环节S1品类市场分析 · 2 天前"
+      },
+      {
+        "role": "f",
+        "w": 360,
+        "h": 402,
+        "chars": 138,
+        "per1e4": 9.54,
+        "text": "最近动过全部 →夏日清爽凉拌面周涛 · 机会点识别昨天露营便携免煮拌面陈静 · 场景与卖点设计2 天前夜宵场景低卡高蛋白"
+      }
+    ],
+    "hues": [
+      [
+        "red/orange",
+        2
+      ],
+      [
+        "blue",
+        2
+      ],
+      [
+        "teal",
+        1
+      ],
+      [
+        "rose",
+        1
+      ]
+    ],
+    "btns": [
+      {
+        "label": "全部",
+        "h": 32,
+        "bg": "rgb(91, 91, 214)",
+        "border": "0px rgb(255, 255, 255)"
+      },
+      {
+        "label": "候选",
+        "h": 26,
+        "bg": "rgba(0, 0, 0, 0)",
+        "border": "0px rgb(63, 63, 70)"
+      },
+      {
+        "label": "已立项",
+        "h": 26,
+        "bg": "rgba(0, 0, 0, 0)",
+        "border": "0px rgb(63, 63, 70)"
+      },
+      {
+        "label": "已否决",
+        "h": 26,
+        "bg": "rgba(0, 0, 0, 0)",
+        "border": "0px rgb(63, 63, 70)"
+      },
+      {
+        "label": "候选",
+        "h": 26,
+        "bg": "rgba(0, 0, 0, 0)",
+        "border": "0px rgb(63, 63, 70)"
+      },
+      {
+        "label": "已立项",
+        "h": 26,
+        "bg": "rgba(0, 0, 0, 0)",
+        "border": "0px rgb(63, 63, 70)"
+      },
+      {
+        "label": "已否决",
+        "h": 26,
+        "bg": "rgba(0, 0, 0, 0)",
+        "border": "0px rgb(63, 63, 70)"
+      },
+      {
+        "label": "回到顶部",
+        "h": 28,
+        "bg": "rgba(0, 0, 0, 0)",
+        "border": "1px rgba(0, 0, 0, 0)"
+      }
+    ],
+    "rows": [
+      {
+        "cls": "row ab grid2 kf-enter",
+        "cols": "72px 72px 72px 72px 72px 72px 72px 72px 72px 72px 72px 72px",
+        "kids": [
+          {
+            "role": "a",
+            "x": 272,
+            "y": 171.9,
+            "w": 744,
+            "h": 381,
+            "top": 171.9,
+            "bottom": 552.9
+          },
+          {
+            "role": "b",
+            "x": 1040,
+            "y": 171.9,
+            "w": 360,
+            "h": 381,
+            "top": 171.9,
+            "bottom": 552.9
+          }
+        ]
+      },
+      {
+        "cls": "row cde kf-enter",
+        "cols": "360px 360px 360px",
+        "kids": [
+          {
+            "role": "c",
+            "x": 272,
+            "y": 617.9,
+            "w": 360,
+            "h": 194,
+            "top": 617.9,
+            "bottom": 811.9
+          },
+          {
+            "role": "d",
+            "x": 656,
+            "y": 617.9,
+            "w": 360,
+            "h": 194,
+            "top": 617.9,
+            "bottom": 811.9
+          },
+          {
+            "role": "e",
+            "x": 1040,
+            "y": 617.9,
+            "w": 360,
+            "h": 194,
+            "top": 617.9,
+            "bottom": 811.9
+          }
+        ]
+      },
+      {
+        "cls": "row cards-row kf-enter",
+        "cols": "72px 72px 72px 72px 72px 72px 72px 72px 72px 72px 72px 72px",
+        "kids": [
+          {
+            "role": "f",
+            "x": 1040,
+            "y": 876.9,
+            "w": 360,
+            "h": 402,
+            "top": 876.9,
+            "bottom": 1278.9
+          }
+        ]
+      },
+      {
+        "cls": "cards",
+        "cols": "360px 360px",
+        "kids": [
+          {
+            "role": "",
+            "x": 272,
+            "y": 910.9,
+            "w": 360,
+            "h": 285.6,
+            "top": 910.9,
+            "bottom": 1196.6
+          },
+          {
+            "role": "",
+            "x": 656,
+            "y": 910.9,
+            "w": 360,
+            "h": 285.6,
+            "top": 910.9,
+            "bottom": 1196.6
+          }
+        ]
+      }
+    ],
+    "A": {
+      "x": 272,
+      "y": 171.9,
+      "w": 744,
+      "h": 381,
+      "top": 171.9,
+      "bottom": 552.9
+    },
+    "B": {
+      "x": 1040,
+      "y": 171.9,
+      "w": 360,
+      "h": 381,
+      "top": 171.9,
+      "bottom": 552.9
+    },
+    "C": {
+      "x": 272,
+      "y": 617.9,
+      "w": 360,
+      "h": 194,
+      "top": 617.9,
+      "bottom": 811.9
+    },
+    "D": {
+      "x": 656,
+      "y": 617.9,
+      "w": 360,
+      "h": 194,
+      "top": 617.9,
+      "bottom": 811.9
+    },
+    "E": {
+      "x": 1040,
+      "y": 617.9,
+      "w": 360,
+      "h": 194,
+      "top": 617.9,
+      "bottom": 811.9
+    },
+    "F": {
+      "x": 1040,
+      "y": 876.9,
+      "w": 360,
+      "h": 402,
+      "top": 876.9,
+      "bottom": 1278.9
+    },
+    "bento": {
+      "A": 744,
+      "B": 360,
+      "aCols": 8,
+      "colDbg": {
+        "n": 12,
+        "w": 1152,
+        "gapRow": 24,
+        "colW": 74
+      },
+      "ABheightDiff": 0,
+      "CDE": {
+        "w": [
+          360,
+          360,
+          360
+        ],
+        "h": [
+          194,
+          194,
+          194
+        ],
+        "hDiff": 0
+      },
+      "F": {
+        "x": 1040,
+        "y": 876.9,
+        "w": 360,
+        "h": 402,
+        "top": 876.9,
+        "bottom": 1278.9
+      }
+    },
+    "Bbg": {
+      "r": 24,
+      "g": 24,
+      "b": 27,
+      "a": 1
+    },
+    "BbgText": "rgb(24, 24, 27)",
+    "Bcontrast": {
+      "t": "现场",
+      "fg": {
+        "r": 180,
+        "g": 180,
+        "b": 188,
+        "a": 1
+      },
+      "c": 8.6
+    },
+    "Bother": [
+      {
+        "t": "现场",
+        "c": 8.6
+      },
+      {
+        "t": "1",
+        "c": 16.12
+      },
+      {
+        "t": "人卡住",
+        "c": 8.6
+      },
+      {
+        "t": "何雨",
+        "c": 16.12
+      },
+      {
+        "t": "S4 机会验证评估 · 13 条",
+        "c": 8.6
+      },
+      {
+        "t": "76h",
+        "c": 16.12
+      },
+      {
+        "t": "各阶段已交",
+        "c": 8.6
+      },
+      {
+        "t": "S1 品类市场分析",
+        "c": 16.12
+      },
+      {
+        "t": "2/11",
+        "c": 8.6
+      },
+      {
+        "t": "S2 用户需求与痛点",
+        "c": 16.12
+      },
+      {
+        "t": "3/11",
+        "c": 8.6
+      },
+      {
+        "t": "S3 机会点识别",
+        "c": 16.12
+      },
+      {
+        "t": "3/11",
+        "c": 8.6
+      },
+      {
+        "t": "S4 机会验证评估",
+        "c": 16.12
+      },
+      {
+        "t": "2/11",
+        "c": 8.6
+      },
+      {
+        "t": "S5 场景与卖点设计",
+        "c": 16.12
+      },
+      {
+        "t": "2/11",
+        "c": 8.6
+      },
+      {
+        "t": "11 人 · 9 张卡在推进",
+        "c": 8.6
+      }
+    ],
+    "gaps": [
+      {
+        "between": "div.p-head → div.bento",
+        "gap": 32
+      },
+      {
+        "between": "div.bento → footer.cfoot",
+        "gap": 47
+      }
+    ],
+    "facePad": [
+      {
+        "role": "a",
+        "pad": "20px"
+      },
+      {
+        "role": "b",
+        "pad": "20px"
+      },
+      {
+        "role": "c",
+        "pad": "20px"
+      },
+      {
+        "role": "d",
+        "pad": "20px"
+      },
+      {
+        "role": "e",
+        "pad": "20px"
+      },
+      {
+        "role": "card",
+        "pad": "20px"
+      },
+      {
+        "role": "card",
+        "pad": "20px"
+      },
+      {
+        "role": "f",
+        "pad": "20px"
+      }
+    ],
+    "fonts": {
+      "tiers": [
+        "12px",
+        "14px",
+        "16px",
+        "20px",
+        "32px"
+      ],
+      "count": {
+        "20px": 17,
+        "14px": 39,
+        "12px": 77,
+        "32px": 4,
+        "16px": 2
+      },
+      "where": {
+        "20px": [
+          "h2.tb-cap",
+          "div.avatar",
+          "h3",
+          "b",
+          "span.n"
+        ],
+        "14px": [
+          "p.kf-prose",
+          "a.go",
+          "span.sub",
+          "span.pk",
+          "span.pv",
+          "b",
+          "a",
+          "a.s",
+          "span.nm",
+          "span.s",
+          "span",
+          "span.meta",
+          "span.h",
+          "a.btn-key",
+          "span.u",
+          "span.lbl",
+          "p.lead",
+          "a.feed-more"
+        ],
+        "12px": [
+          "span",
+          "span.b",
+          "span.sf",
+          "div.lbl",
+          "span.pp",
+          "p.me-note",
+          "span.d",
+          "span.lbl",
+          "span.rk-k",
+          "span.rk-n",
+          "div.live-foot",
+          "span.note",
+          "span.badge",
+          "span.opp-st",
+          "button",
+          "span.more",
+          "span.st",
+          "span.tm",
+          "span.cfl",
+          "button.kf-btn"
+        ],
+        "32px": [
+          "b"
+        ],
+        "16px": [
+          "h3"
+        ]
+      }
+    },
+    "sparks": {
+      "inView": 0,
+      "inPage": 0
+    },
+    "overflow": {
+      "scrollWidth": 1440,
+      "clientWidth": 1440
+    }
+  },
+  "desktopDet": {
+    "view": "view-workbench",
+    "statDensity": [
+      {
+        "role": "c",
+        "w": 360,
+        "h": 194,
+        "chars": 24,
+        "per1e4": 3.44,
+        "text": "卡池全部 5张5 人提交 · 3 个 Agent"
+      },
+      {
+        "role": "d",
+        "w": 360,
+        "h": 194,
+        "chars": 18,
+        "per1e4": 2.58,
+        "text": "最高分82分办公室 3 分钟速食拌面"
+      },
+      {
+        "role": "e",
+        "w": 360,
+        "h": 194,
+        "chars": 19,
+        "per1e4": 2.72,
+        "text": "我的环节S1品类市场分析 · 2 天前"
+      },
+      {
+        "role": "f",
+        "w": 360,
+        "h": 402,
+        "chars": 143,
+        "per1e4": 9.88,
+        "text": "最近动过全部 →露营便携免煮拌面陈静 · 场景与卖点设计2 天前夜宵场景低卡高蛋白拌面张伟 · 机会点识别2 天前办公室"
+      }
+    ],
+    "hues": [
+      [
+        "red/orange",
+        2
+      ],
+      [
+        "blue",
+        2
+      ],
+      [
+        "teal",
+        1
+      ],
+      [
+        "rose",
+        1
+      ]
+    ],
+    "btns": [
+      {
+        "label": "全部",
+        "h": 32,
+        "bg": "rgb(91, 91, 214)",
+        "border": "0px rgb(255, 255, 255)"
+      },
+      {
+        "label": "候选",
+        "h": 26,
+        "bg": "rgba(0, 0, 0, 0)",
+        "border": "0px rgb(63, 63, 70)"
+      },
+      {
+        "label": "已立项",
+        "h": 26,
+        "bg": "rgba(0, 0, 0, 0)",
+        "border": "0px rgb(63, 63, 70)"
+      },
+      {
+        "label": "已否决",
+        "h": 26,
+        "bg": "rgba(0, 0, 0, 0)",
+        "border": "0px rgb(63, 63, 70)"
+      },
+      {
+        "label": "候选",
+        "h": 26,
+        "bg": "rgba(0, 0, 0, 0)",
+        "border": "0px rgb(63, 63, 70)"
+      },
+      {
+        "label": "已立项",
+        "h": 26,
+        "bg": "rgba(0, 0, 0, 0)",
+        "border": "0px rgb(63, 63, 70)"
+      },
+      {
+        "label": "已否决",
+        "h": 26,
+        "bg": "rgba(0, 0, 0, 0)",
+        "border": "0px rgb(63, 63, 70)"
+      },
+      {
+        "label": "回到顶部",
+        "h": 28,
+        "bg": "rgba(0, 0, 0, 0)",
+        "border": "1px rgba(0, 0, 0, 0)"
+      }
+    ],
+    "rows": [
+      {
+        "cls": "row ab grid2 kf-enter",
+        "cols": "72px 72px 72px 72px 72px 72px 72px 72px 72px 72px 72px 72px",
+        "kids": [
+          {
+            "role": "a",
+            "x": 272,
+            "y": 171.9,
+            "w": 744,
+            "h": 380,
+            "top": 171.9,
+            "bottom": 551.9
+          },
+          {
+            "role": "b",
+            "x": 1040,
+            "y": 171.9,
+            "w": 360,
+            "h": 380,
+            "top": 171.9,
+            "bottom": 551.9
+          }
+        ]
+      },
+      {
+        "cls": "row cde kf-enter",
+        "cols": "360px 360px 360px",
+        "kids": [
+          {
+            "role": "c",
+            "x": 272,
+            "y": 616.9,
+            "w": 360,
+            "h": 194,
+            "top": 616.9,
+            "bottom": 810.9
+          },
+          {
+            "role": "d",
+            "x": 656,
+            "y": 616.9,
+            "w": 360,
+            "h": 194,
+            "top": 616.9,
+            "bottom": 810.9
+          },
+          {
+            "role": "e",
+            "x": 1040,
+            "y": 616.9,
+            "w": 360,
+            "h": 194,
+            "top": 616.9,
+            "bottom": 810.9
+          }
+        ]
+      },
+      {
+        "cls": "row cards-row kf-enter",
+        "cols": "72px 72px 72px 72px 72px 72px 72px 72px 72px 72px 72px 72px",
+        "kids": [
+          {
+            "role": "f",
+            "x": 1040,
+            "y": 875.9,
+            "w": 360,
+            "h": 402,
+            "top": 875.9,
+            "bottom": 1277.9
+          }
+        ]
+      },
+      {
+        "cls": "cards",
+        "cols": "360px 360px",
+        "kids": [
+          {
+            "role": "",
+            "x": 272,
+            "y": 909.9,
+            "w": 360,
+            "h": 285.6,
+            "top": 909.9,
+            "bottom": 1195.6
+          },
+          {
+            "role": "",
+            "x": 656,
+            "y": 909.9,
+            "w": 360,
+            "h": 285.6,
+            "top": 909.9,
+            "bottom": 1195.6
+          }
+        ]
+      }
+    ],
+    "A": {
+      "x": 272,
+      "y": 171.9,
+      "w": 744,
+      "h": 380,
+      "top": 171.9,
+      "bottom": 551.9
+    },
+    "B": {
+      "x": 1040,
+      "y": 171.9,
+      "w": 360,
+      "h": 380,
+      "top": 171.9,
+      "bottom": 551.9
+    },
+    "C": {
+      "x": 272,
+      "y": 616.9,
+      "w": 360,
+      "h": 194,
+      "top": 616.9,
+      "bottom": 810.9
+    },
+    "D": {
+      "x": 656,
+      "y": 616.9,
+      "w": 360,
+      "h": 194,
+      "top": 616.9,
+      "bottom": 810.9
+    },
+    "E": {
+      "x": 1040,
+      "y": 616.9,
+      "w": 360,
+      "h": 194,
+      "top": 616.9,
+      "bottom": 810.9
+    },
+    "F": {
+      "x": 1040,
+      "y": 875.9,
+      "w": 360,
+      "h": 402,
+      "top": 875.9,
+      "bottom": 1277.9
+    },
+    "bento": {
+      "A": 744,
+      "B": 360,
+      "aCols": 8,
+      "colDbg": {
+        "n": 12,
+        "w": 1152,
+        "gapRow": 24,
+        "colW": 74
+      },
+      "ABheightDiff": 0,
+      "CDE": {
+        "w": [
+          360,
+          360,
+          360
+        ],
+        "h": [
+          194,
+          194,
+          194
+        ],
+        "hDiff": 0
+      },
+      "F": {
+        "x": 1040,
+        "y": 875.9,
+        "w": 360,
+        "h": 402,
+        "top": 875.9,
+        "bottom": 1277.9
+      }
+    },
+    "Bbg": {
+      "r": 24,
+      "g": 24,
+      "b": 27,
+      "a": 1
+    },
+    "BbgText": "rgb(24, 24, 27)",
+    "Bcontrast": {
+      "t": "现场",
+      "fg": {
+        "r": 180,
+        "g": 180,
+        "b": 188,
+        "a": 1
+      },
+      "c": 8.6
+    },
+    "Bother": [
+      {
+        "t": "现场",
+        "c": 8.6
+      },
+      {
+        "t": "1",
+        "c": 16.12
+      },
+      {
+        "t": "人卡住",
+        "c": 8.6
+      },
+      {
+        "t": "何雨",
+        "c": 16.12
+      },
+      {
+        "t": "S4 机会验证评估 · 13 条",
+        "c": 8.6
+      },
+      {
+        "t": "76h",
+        "c": 16.12
+      },
+      {
+        "t": "各阶段已交",
+        "c": 8.6
+      },
+      {
+        "t": "S1 品类市场分析",
+        "c": 16.12
+      },
+      {
+        "t": "2/11",
+        "c": 8.6
+      },
+      {
+        "t": "S2 用户需求与痛点",
+        "c": 16.12
+      },
+      {
+        "t": "3/11",
+        "c": 8.6
+      },
+      {
+        "t": "S3 机会点识别",
+        "c": 16.12
+      },
+      {
+        "t": "3/11",
+        "c": 8.6
+      },
+      {
+        "t": "S4 机会验证评估",
+        "c": 16.12
+      },
+      {
+        "t": "2/11",
+        "c": 8.6
+      },
+      {
+        "t": "S5 场景与卖点设计",
+        "c": 16.12
+      },
+      {
+        "t": "2/11",
+        "c": 8.6
+      },
+      {
+        "t": "11 人 · 5 张卡在推进",
+        "c": 8.6
+      }
+    ],
+    "gaps": [
+      {
+        "between": "div.p-head → div.bento",
+        "gap": 32
+      },
+      {
+        "between": "div.bento → footer.cfoot",
+        "gap": 47
+      }
+    ],
+    "facePad": [
+      {
+        "role": "a",
+        "pad": "20px"
+      },
+      {
+        "role": "b",
+        "pad": "20px"
+      },
+      {
+        "role": "c",
+        "pad": "20px"
+      },
+      {
+        "role": "d",
+        "pad": "20px"
+      },
+      {
+        "role": "e",
+        "pad": "20px"
+      },
+      {
+        "role": "card",
+        "pad": "20px"
+      },
+      {
+        "role": "card",
+        "pad": "20px"
+      },
+      {
+        "role": "f",
+        "pad": "20px"
+      }
+    ],
+    "fonts": {
+      "tiers": [
+        "12px",
+        "14px",
+        "16px",
+        "20px",
+        "32px"
+      ],
+      "count": {
+        "20px": 17,
+        "14px": 38,
+        "12px": 76,
+        "32px": 4,
+        "16px": 2
+      },
+      "where": {
+        "20px": [
+          "h2.tb-cap",
+          "div.avatar",
+          "h3",
+          "b",
+          "span.n"
+        ],
+        "14px": [
+          "p.kf-prose",
+          "a.go",
+          "span.sub",
+          "span.pk",
+          "span.pv",
+          "b",
+          "p.me-none",
+          "span.nm",
+          "span.s",
+          "span",
+          "span.meta",
+          "span.h",
+          "a.btn-key",
+          "span.u",
+          "span.lbl",
+          "p.lead",
+          "a",
+          "a.feed-more"
+        ],
+        "12px": [
+          "span",
+          "span.b",
+          "span.sf",
+          "div.lbl",
+          "span.pp",
+          "p.me-note",
+          "span.d",
+          "span.lbl",
+          "span.rk-k",
+          "span.rk-n",
+          "div.live-foot",
+          "span.note",
+          "span.badge",
+          "span.opp-st",
+          "button",
+          "span.more",
+          "span.st",
+          "span.tm",
+          "span.cfl",
+          "button.kf-btn"
+        ],
+        "32px": [
+          "b"
+        ],
+        "16px": [
+          "h3"
+        ]
+      }
+    },
+    "sparks": {
+      "inView": 0,
+      "inPage": 0
+    },
+    "overflow": {
+      "scrollWidth": 1440,
+      "clientWidth": 1440
+    }
+  },
+  "glass": {
+    "/": {
+      "blur": 0,
+      "shadow": 1,
+      "blurWhere": [],
+      "shadowWhere": [
+        "a"
+      ]
+    },
+    "/pool": {
+      "blur": 0,
+      "shadow": 1,
+      "blurWhere": [],
+      "shadowWhere": [
+        "a"
+      ]
+    },
+    "/stages": {
+      "blur": 0,
+      "shadow": 1,
+      "blurWhere": [],
+      "shadowWhere": [
+        "a"
+      ]
+    },
+    "/reports": {
+      "blur": 6,
+      "shadow": 6,
+      "blurWhere": [
+        "div.panel.kf-glass.is-overlay"
+      ],
+      "shadowWhere": [
+        "div.panel.kf-glass.is-overlay"
+      ]
+    }
+  },
+  "noMe": {
+    "roles": [
+      "a",
+      "b",
+      "c",
+      "d",
+      "e",
+      "f"
+    ],
+    "Aheight": 363,
+    "allstages": 5,
+    "text": "我的推进五阶段进度与现场状态 机会卡池 → ? 还没选「我是」先看全员:11 人 · 已交 10 人 选我 S1 2 品类市场分析 S2 3 用户需求与痛点 S3 3 机会点识别 S4 2 机会验证评估 S5 2 场景与卖点设计 已交 10 人 · 选了自己,这里换成你的阶段进度与待办 现场 1人卡住 何雨S4 机会验证评估 · 13 条 76h 各阶段已交 S1 品类市场分析 2/11 S2 用户需求与痛点 3/11 S3 机会点识别 3/11 S4 机会验证评估 2/11"
+  },
+  "nobody": {
+    "kids": 4,
+    "roles": [
+      "a",
+      "b",
+      "c",
+      "d",
+      "e",
+      "f"
+    ],
+    "Aheight": 561,
+    "text": "我的推进五阶段进度与现场状态 机会卡池 → _ _nobody_已完成 0 / 5 个阶段 · 累计 0 份成果 0份成果 0张卡 4待评分 S1 — 品类市场分析 S2 — 用户需求与痛点 S3 — 机会点识别 S4 — 机会验证评估 S5 — 场景与卖点设计 本月还没有你的成果 看五阶段产出 → 卡池进度 候选 4 张44% 已立项 3 张33% 已否决 2 张22% 待评分 4 候选卡里还没有你打分的卡,按最近上传排 夜宵场景低卡高蛋白拌面 6 人 · 均 81 81 ",
+    "probe": {
+      "kids": 4
+    }
+  },
+  "noStall": {
+    "stalledItems": 0,
+    "bText": "现场 大家都在动 没有超过 8 小时没交成果的人 各阶段已交 S1 品类市场分析 2/11 S2 用户需求与痛点 3/11 S3 机会点识别 3/11 S4 机会验证评估 2/11 S5 场景与卖点设计 2/11 11 人 · 9 张卡在推",
+    "aH": 381,
+    "bH": 381,
+    "dot": "rgb(74, 222, 128)",
+    "bg": "rgb(24, 24, 27)"
+  },
+  "stalledCount": 1,
+  "mobile": {
+    "scrollWidth": 393,
+    "clientWidth": 393,
+    "A": {
+      "x": 28,
+      "y": 165,
+      "w": 337,
+      "h": 675.8
+    },
+    "B": {
+      "x": 28,
+      "y": 856.8,
+      "w": 337,
+      "h": 389.8
+    },
+    "stacked": true,
+    "CDE": [
+      {
+        "x": 28,
+        "y": 1279.5,
+        "w": 107,
+        "h": 194
+      },
+      {
+        "x": 143,
+        "y": 1279.5,
+        "w": 107,
+        "h": 194
+      },
+      {
+        "x": 258,
+        "y": 1279.5,
+        "w": 107,
+        "h": 194
+      }
+    ],
+    "fonts": [
+      "12px",
+      "14px",
+      "16px",
+      "20px",
+      "32px"
+    ],
+    "fontSizeCount": {
+      "20px": 20,
+      "14px": 36,
+      "12px": 78,
+      "32px": 1,
+      "16px": 2
+    },
+    "overflowing": []
+  },
+  "v12": null,
+  "errors": []
+}

+ 2 - 1
scripts/verify-cjk.mjs

@@ -106,7 +106,8 @@ const samples = await evalIn(page, `
 if (samples.length < 3) {
   const more = await evalIn(page, `
     const out = [];
-    deepAll((el) => { if (out.length < 2 && el.children.length === 0 && visible(el) && /S\\d/.test(el.textContent) && [...el.textContent].includes('\\u2009')) out.push({ name: '阶段标签 ' + elLabel(el), text: el.textContent.trim(), codes: [...el.textContent.trim()].map((c) => c.codePointAt(0)) }); });
+    /* v16:阶段标签的两种形态都要抽到 —— 合成串「S4␉机会验证评估」(core 的 stage.label)和拆开的「S4」+「机会验证评估」两个 span */
+    deepAll((el) => { const tx = el.textContent.trim(); if (out.length < 3 && el.children.length <= 1 && visible(el) && /^S\\d/.test(tx) && [...tx].includes('\\u2009') && tx.length < 24) out.push({ name: '阶段标签 ' + elLabel(el), text: tx, codes: [...tx].map((c) => c.codePointAt(0)) }); });
     return out;
   `);
   samples.push(...more);

+ 519 - 0
scripts/verify-v16.mjs

@@ -0,0 +1,519 @@
+#!/usr/bin/env node
+/**
+ * scripts/verify-v16.mjs —— v16 验收(v15 的 1–9 项全保留 + 任务书 v16 的 10–12 项)。
+ *
+ *   node scripts/verify-v16.mjs --tag before --from HEAD   改前基线(从 git 导出旧代码单独起服务)
+ *   node scripts/verify-v16.mjs --tag after --from worktree 改后
+ *   新增:[10] 短卡内容密度(跟 dr1 量得的基准比) [11] 深色卡色相数(≤2) [12] 底部按钮重量
+ *   基准出处:docs/v16-research.md §1.2 / §2.1 / §5
+
+ *
+ *   node scripts/verify-v15.mjs --tag before           改前基线
+ *   node scripts/verify-v15.mjs --tag after            改后
+ *   node scripts/verify-v15.mjs --json out.json        结果落盘(docs/v15-before-after.md 从这里抄数字)
+ *   --me 赵磊                                          量「有数据」的我的推进(默认赵磊,仓库里有 2 份成果)
+ *   --no-regress                                       顺带跑 verify-visual-v12.mjs 再打一遍
+ *
+ * 数据源:dev server(默认 127.0.0.1:4173,--port 改)→ 生产 Parse 只读。localhost 是安全上下文,
+ * 所以 localStorage 里写 `kf-board:me` 等价于真实用户点过「我是」。
+ *
+ * 量什么(工作台 /,1440×900 与 393×852 两档):
+ *   1. A 卡宽 / B 卡宽 / 高度差(≤2px);A 占 12 栅格里的几列
+ *   2. B 卡底色亮度(<0.35)与反白文字最差对比度(≥7:1)
+ *   3. C/D/E 三卡宽高差(高差 ≤2px)
+ *   4. 页面主列直接子块的相邻 gap(剔除页头与页脚)
+ *   5. 卡片内边距实测(.face 的 computed padding)
+ *   6. 全页可见文字字号档位(≤5 档、无 11px)
+ *   7. kf-spark 残留(工作台 0;全站单独打印)
+ *   8. 393px:scrollWidth === clientWidth、A/B 上下堆叠、字号档、无 11px
+ *   9. 全站普通卡片:backdrop-filter 个数、box-shadow 非 none 个数(跨 shadow)
+ *  10. --no-regress:调用 verify-visual-v12.mjs --tag before,把摘要原样打印
+ */
+import { writeFileSync } from 'node:fs';
+import { join, dirname } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { execFileSync } from 'node:child_process';
+
+const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
+const argv = process.argv.slice(2);
+const arg = (k, d) => (argv.includes(k) ? argv[argv.indexOf(k) + 1] : d);
+const TAG = arg('--tag', 'after');
+const JSON_OUT = arg('--json', '');
+const PORT = Number(arg('--port', process.env.PORT || 4173));
+const ME = arg('--me', '赵磊');
+const BASE = `http://127.0.0.1:${PORT + 30}`;
+
+if (!process.env.CHROME_PATH) {
+  const pinned = '/opt/hermes/.playwright/chromium_headless_shell-1243/chrome-headless-shell-linux64/chrome-headless-shell';
+  const { existsSync } = await import('node:fs');
+  if (existsSync(pinned)) process.env.CHROME_PATH = pinned;
+}
+
+/* 「改前基线」必须对着改前的代码跑:
+ *   默认 --from HEAD:把 git 里的整棵树导出到临时目录,dev server 指向那里(工作区改了也不影响);
+ *   --from worktree:跑工作区当前代码(改完自检用)。
+ * 每次运行都打印 /src/pages/view-workbench.js 的 sha1 写进 JSON —— before / after 两份能直接核对跑的是哪版。
+ */
+const FROM = arg('--from', 'HEAD');
+const { createHash } = await import('node:crypto');
+const { readFileSync, mkdtempSync, cpSync, rmSync } = await import('node:fs');
+const { tmpdir } = await import('node:os');
+
+let codeDir = ROOT;
+if (FROM !== 'worktree') {
+  codeDir = mkdtempSync(join(tmpdir(), 'kf-v15-'));
+  execFileSync('sh', ['-c', `git archive ${JSON.stringify(FROM)} | tar -x -C ${JSON.stringify(codeDir)}`], { cwd: ROOT, stdio: 'ignore' });
+  /* lib/vendor(lit 等)没进 git,要和工作区的一起拷;serve.mjs 的静态根 = 它自己所在的 src/../..,
+     所以把 serve.mjs 挪到临时根的 src/dev/ 下即可整体切换 */
+  cpSync(join(ROOT, 'src/dev/serve.mjs'), join(codeDir, 'src/dev/serve.mjs'));
+  cpSync(join(ROOT, 'lib'), join(codeDir, 'lib'), { recursive: true });
+}
+const codeSha = createHash('sha1').update(readFileSync(join(codeDir, 'src/pages/view-workbench.js'))).digest('hex').slice(0, 12);
+const cleanup = () => { if (FROM !== 'worktree') rmSync(codeDir, { recursive: true, force: true }); };
+
+const { launch, DEEP } = await import('./v11b-lib.mjs');
+const browser = await launch();
+/* 独立的 dev server:静态根指向 codeDir,/parse 仍代理生产(只读) */
+const { spawn } = await import('node:child_process');
+const srv = spawn(process.execPath, [join(codeDir, 'src/dev/serve.mjs'), '--port', String(PORT + 30), '--mount', '/'], { stdio: 'ignore', cwd: codeDir });
+/* 起不来就报错退出,不要悄悄连到别的 4173 */
+const SBASE = `http://127.0.0.1:${PORT + 30}`;
+for (let i = 0; i < 60; i++) {
+  await new Promise((r) => setTimeout(r, 100));
+  try { if ((await fetch(SBASE + '/index.html')).ok) break; } catch { /* 还没起来 */ }
+}
+
+const main = async () => {
+  /* ---------------- 桌面 1440 ---------------- */
+  const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 }, deviceScaleFactor: 1 });
+  /* 只在第一次导航(about:blank,同源之前)种一次:addInitScript 会在每次导航时重跑,
+     那样后面「清掉我是」再刷新会被它写回去(实测:空态 A 量到的是赵磊)。 */
+  await ctx.addInitScript((name) => { try { if (!localStorage.getItem('kf-v15-seeded')) { localStorage.setItem('kf-v15-seeded', '1'); localStorage.setItem('kf-board:me', JSON.stringify(name)); } } catch { /* 首次在 about:blank 里没有 localStorage,忽略 */ } }, ME);
+  const page = await ctx.newPage();
+  const errors = [];
+  page.on('pageerror', (e) => errors.push(e.message));
+  page.on('console', (m) => { if (m.type() === 'error' && !/favicon/.test(m.text())) errors.push(m.text()); });
+
+  const mount = async (p) => {
+    await page.goto(new URL(p, BASE).href, { waitUntil: 'networkidle' });
+    await page.waitForFunction(() => {
+      const a = document.querySelector('kf-app');
+      const v = a?.shadowRoot?.querySelector('main#view')?.firstElementChild;
+      return !!v && v.shadowRoot && v.shadowRoot.childElementCount > 0;
+    }, null, { timeout: 20000 });
+    await page.waitForTimeout(600);
+    /* 光场 / 入场动效会让第一轮几何还没稳定:滚动回顶部,动效按系统偏好关掉 */
+    try { await page.waitForFunction(() => window.scrollY === 0, null, { timeout: 3000 }); } catch { await page.evaluate(() => window.scrollTo(0, 0)); }
+    await page.waitForTimeout(200);
+  };
+
+  const DESKTOP = `(() => { ${DEEP}
+    const view = document.getElementById('v15-det') || appR().querySelector('main#view').firstElementChild;
+    const vr = view.shadowRoot;
+    const R = (el) => { const b = el.getBoundingClientRect(); return { x: +b.x.toFixed(1), y: +b.y.toFixed(1), w: +b.width.toFixed(1), h: +b.height.toFixed(1), top: +b.top.toFixed(1), bottom: +b.bottom.toFixed(1) }; };
+    const rgb = (s) => { const m = s.match(/rgba?\\(([\\d.]+), ([\\d.]+), ([\\d.]+)(?:, ([\\d.]+))?\\)/); return m ? { r: +m[1], g: +m[2], b: +m[3], a: m[4] == null ? 1 : +m[4] } : null; };
+    const lum = ({ r, g, b }) => { const f = (v) => { v /= 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); }; return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b); };
+    const contrast = (a, b) => { const [x, y] = [lum(a), lum(b)].sort((p, q) => q - p); return +(x + 0.05) / (y + 0.05); };
+    /* 跨 shadow 向上找第一个不透明底色 */
+    const bgOf = (el) => { for (let q = el; q; q = q.parentElement || q.getRootNode()?.host) { const c = rgb(getComputedStyle(q).backgroundColor); if (c && c.a >= 0.99) return c; } return { r: 255, g: 255, b: 255 }; };
+    /* 一个元素「画出来」的底色 = 自身背景色;没有就看最近的不透明祖先 */
+    const paintOf = (el) => { const own = rgb(getComputedStyle(el).backgroundColor); return own && own.a >= 0.5 ? own : bgOf(el.parentElement || el.getRootNode()?.host); };
+
+    const host = (sel) => vr.querySelector(sel);
+    const frameOf = (card) => card?.shadowRoot?.querySelector('.frame') || null;
+    const cards = [...vr.querySelectorAll('kf-glass-card')].filter((c) => c.getBoundingClientRect().height > 0);
+
+    /* ---------- 栅格:找行容器 ---------- */
+    const gridRows = [...vr.querySelectorAll('*')].filter((el) => {
+      const s = getComputedStyle(el); return /grid/.test(s.display) && el.querySelector(':scope > kf-glass-card');
+    }).map((el) => {
+      const kids = [...el.querySelectorAll(':scope > kf-glass-card')].map((k) => ({ rect: R(k), role: k.dataset.role || '' }));
+      return { el, cls: el.className, cols: getComputedStyle(el).gridTemplateColumns, kids };
+    });
+    const byRole = (r) => gridRows.flatMap((g) => g.kids).find((k) => k.role === r)?.rect || null;
+    const A = byRole('a'), B = byRole('b'), C = byRole('c'), D = byRole('d'), E = byRole('e'), F = byRole('f')
+      || (vr.querySelector('kf-glass-card[data-role="f"]') ? R(vr.querySelector('kf-glass-card[data-role="f"]')) : null);
+    /* 列数:找 A 所在的栅格容器,读它的 grid-template-columns 与 column-gap,算出单列宽,再数 A 占几列 */
+    const aEl = vr.querySelector('kf-glass-card[data-role="a"]');
+    /* 直接在已找到的行容器里挑「装着 A 的那一行」——不要 [...vr.children](vr 是 ShadowRoot,children 是 HTMLCollection,
+       展开后第一项是属性本身,不是元素) */
+    const rowABC = gridRows.find((g) => g.el.contains(aEl))?.el || null;
+    let gapRow = null, colW = null, colDbg = null;
+    if (rowABC) {
+      gapRow = parseFloat(getComputedStyle(rowABC).columnGap) || parseFloat(getComputedStyle(rowABC).gap) || 0;
+      const tracks = getComputedStyle(rowABC).gridTemplateColumns.split(/ +/).filter(Boolean).map(parseFloat);
+      colW = tracks.length ? (rowABC.getBoundingClientRect().width - gapRow * (tracks.length - 1)) / tracks.length : null;
+      colDbg = { n: tracks.length, w: rowABC.getBoundingClientRect().width, gapRow, colW };
+    }
+    /* 列数:单列宽 = (行宽 − 间距 × (列数 − 1)) / 列数;A 占几列 = (A 宽 + 间距) / (单列宽 + 间距) */
+    const aColsExact = A && colW ? Math.round((A.w + gapRow) / (colW + gapRow)) : null;
+    const colWDbg = colW;
+
+    /* ---------- B 卡深色 ---------- */
+    let Bbg = null, BbgText = '', Bcontrast = null, Bother = [];
+    const bc = cards.find((c) => c.dataset.role === 'b');
+    if (bc) {
+      const frame = frameOf(bc);
+      Bbg = rgb(getComputedStyle(frame)?.backgroundColor || '');
+      if (!Bbg) Bbg = paintOf(bc);
+      /* 文本都在 slotted 内容里(bc 的 light DOM),不在它自己的 shadow 内 */
+      const texts = [...bc.querySelectorAll('*')].filter((e) => [...e.childNodes].some((n) => n.nodeType === 3 && n.textContent.trim()));
+      const worst = texts.map((e) => { const fg = rgb(getComputedStyle(e).color); return { t: e.textContent.trim().slice(0, 16), fg, c: +contrast(fg, Bbg).toFixed(2) }; }).sort((p, q) => p.c - q.c);
+      Bcontrast = worst[0] || null;
+      BbgText = 'rgb(' + Bbg.r + ', ' + Bbg.g + ', ' + Bbg.b + ')';
+      Bother = texts.map((e) => ({ t: e.textContent.trim().slice(0, 16), c: +contrast(rgb(getComputedStyle(e).color), Bbg).toFixed(2) }));
+    }
+
+    /* ---------- 主列直接子块间距 ---------- */
+    const blocks = [...vr.children].filter((el) => {
+      const s = getComputedStyle(el); const b = el.getBoundingClientRect();
+      return s.display !== 'none' && b.height > 0;
+    }).map((el) => ({ el, tag: el.localName + (el.className ? '.' + String(el.className).split(' ')[0] : ''), rect: R(el) }));
+    /* 间距 = 两块之间真实露出的竖直空白。box 底边不含 margin-bottom / padding-bottom / border-bottom,
+       下一块 box 顶边也不含它自己的 border-top(外边框在 rect 内,margin 折叠也已体现在 rect 里)。
+       两者相减后扣掉上一块自带的尾随边距,再扣掉下一块自己的 border-top —— 得到「眼睛看到的空白」。 */
+    const gaps = [];
+    for (let i = 1; i < blocks.length; i++) {
+      const prev = blocks[i - 1], cur = blocks[i];
+      const sp = getComputedStyle(prev.el), sc = getComputedStyle(cur.el);
+      const trail = (parseFloat(sp.marginBottom) || 0) + (parseFloat(sp.paddingBottom) || 0) + (parseFloat(sp.borderBottomWidth) || 0)
+        + (parseFloat(sc.borderTopWidth) || 0);
+      gaps.push({ between: prev.tag + ' → ' + cur.tag, gap: +(cur.rect.top - prev.rect.bottom - trail).toFixed(1) });
+    }
+
+    /* ---------- 卡片内边距 ---------- */
+    const padding = [...new Set([...vr.querySelectorAll('*')].filter((el) => /grid|flex/.test(getComputedStyle(el).display)).map((el) => getComputedStyle(el).paddingTop))];
+    const facePad = cards.map((c) => c.dataset.role || 'card').map((role, i) => ({ role, pad: getComputedStyle(frameOf(cards[i]).querySelector('.face')).padding }));
+
+    /* ---------- 字号档 ---------- */
+    const fsCount = {};
+    const fsWhere = {};
+    walk(vr, (el) => {
+      if (![...el.childNodes].some((n) => n.nodeType === 3 && n.textContent.trim())) return;
+      if (!visible(el)) return;
+      const s = getComputedStyle(el);
+      const k = Math.round(parseFloat(s.fontSize) * 10) / 10 + 'px';
+      fsCount[k] = (fsCount[k] || 0) + 1;
+      (fsWhere[k] = fsWhere[k] || new Set()).add(el.localName + (typeof el.className === 'string' && el.className ? '.' + el.className.trim().split(/ +/)[0] : ''));
+    });
+
+    /* ---------- v16 [10] 短卡密度:每张短卡「可见文字字符数 / 卡面积」 ---------- */
+    const statCards = ['c', 'd', 'e', 'f'].map((r) => vr.querySelector('kf-glass-card[data-role="' + r + '"]')).filter(Boolean);
+    const statDensity = statCards.map((k) => {
+      const rect = R(k);
+      const txt = [...k.querySelectorAll('*')].filter((el) => [...el.childNodes].some((n) => n.nodeType === 3 && n.textContent.trim()) && visible(el))
+        .map((el) => [...el.childNodes].filter((n) => n.nodeType === 3).map((n) => n.textContent.replace(/\s+/g, '')).join('')).join('');
+      return { role: k.dataset.role, w: rect.w, h: rect.h, chars: [...txt].length,
+        per1e4: +([...txt].length / (rect.w * rect.h) * 10000).toFixed(2), text: txt.slice(0, 60) };
+    });
+    /* ---------- v16 [11] 深色卡色相数 ---------- */
+    const bEl = vr.querySelector('kf-glass-card[data-role="b"]');
+    const hueOf = (c) => { if (!c || c.a < 0.15) return null; const mx = Math.max(c.r, c.g, c.b), mn = Math.min(c.r, c.g, c.b);
+      if (mx - mn < 24) return null; let h; const d = mx - mn;
+      if (mx === c.r) h = ((c.g - c.b) / d) % 6; else if (mx === c.g) h = (c.b - c.r) / d + 2; else h = (c.r - c.g) / d + 4;
+      h *= 60; if (h < 0) h += 360;
+      return h < 40 ? 'red/orange' : h < 70 ? 'amber' : h < 160 ? 'green' : h < 200 ? 'teal' : h < 260 ? 'blue' : h < 320 ? 'violet' : 'rose'; };
+    const hues = new Map();
+    if (bEl) walk(bEl, (el) => {
+      if (!visible(el)) return;
+      const s = getComputedStyle(el);
+      for (const [prop, val] of [['color', s.color], ['bg', s.backgroundColor], ['border', s.borderTopColor]]) {
+        const h = hueOf(rgb(val)); if (h) { const k = h; hues.set(k, (hues.get(k) || 0) + 1); }
+      }
+    });
+    /* ---------- v16 [12] 底部按钮:工作台里所有 button / .kf-btn / .btn-key 的高度与背景 ---------- */
+    const btns = [];
+    walk(vr, (el) => {
+      if (!visible(el)) return;
+      const isBtn = el.localName === 'button' || el.classList?.contains('kf-btn') || el.classList?.contains('btn-key');
+      if (!isBtn) return;
+      const r = el.getBoundingClientRect(); const s = getComputedStyle(el);
+      btns.push({ label: el.textContent.replace(/\s+/g, ' ').trim().slice(0, 12), h: Math.round(r.height), bg: s.backgroundColor, border: s.borderTopWidth + ' ' + s.borderTopColor });
+    });
+    return {
+      view: view.localName,
+      statDensity, hues: [...hues.entries()], btns,
+      rows: gridRows.map((g) => ({ cls: g.cls, cols: g.cols, kids: g.kids.map((k) => ({ role: k.role, ...k.rect })) })),
+      A, B, C, D, E, F,
+      bento: {
+        A: A?.w ?? null, B: B?.w ?? null, aCols: aColsExact, colDbg,
+        ABheightDiff: A && B ? +Math.abs(A.h - B.h).toFixed(1) : null,
+        CDE: C && D && E ? { w: [C.w, D.w, E.w], h: [C.h, D.h, E.h], hDiff: +(Math.max(C.h, D.h, E.h) - Math.min(C.h, D.h, E.h)).toFixed(1) } : null,
+        F: F || null,
+      },
+      Bbg, BbgText, Bcontrast, Bother,
+      gaps,
+      facePad,
+      fonts: { tiers: Object.keys(fsCount).sort((p, q) => parseFloat(p) - parseFloat(q)), count: fsCount, where: Object.fromEntries(Object.entries(fsWhere).map(([k, v]) => [k, [...v]])) },
+      sparks: { inView: vr.querySelectorAll('kf-spark').length, inPage: document.querySelectorAll('*').length && (() => { let n = 0; walk(document, (e) => { if (e.localName === 'kf-spark') n++; }); return n; })() },
+      overflow: { scrollWidth: document.documentElement.scrollWidth, clientWidth: document.documentElement.clientWidth },
+    };
+  })()`;
+
+  /* 确定性:实例化一个 view-workbench(自定义元素,不需要 import 模块),挂到主列最前面;
+     .db 是页面已加载的同一份,但卡池只留前 5 张——「5 张」是改前数据快照里的真实值。
+     之前想 new ViewWorkbench() 是死路:模块里的类没 define,Lit 的属性表是空的(见 git 记录)。 */
+  const DET_FN = async () => {
+    const main = document.querySelector('kf-app').shadowRoot.querySelector('main#view');
+    const tpl = document.createElement('template');
+    tpl.innerHTML = '<view-workbench id="v15-det"></view-workbench>';
+    const v = tpl.content.firstElementChild;
+    /* evaluate 每次拿到的是不同的 JS realm,顶层 import 拿到的是模块的**另一份实例**(Lit 会直接拒绝构造)。
+       所以不 import 任何模块:从 realm 的全局拿到页面里同一个 kf-app,再顺着它的 shadow root 取。 */
+    const app = document.querySelector('kf-app');
+    const view = app.shadowRoot.querySelector('main#view').firstElementChild;
+    const db = view.db;
+    v.db = { ...db, opportunities: db.opportunities.slice(0, 5) };
+    v.me = '赵磊';
+    main.prepend(v);
+    await new Promise((r) => setTimeout(r, 600));
+    return { kids: v.shadowRoot?.childElementCount ?? -1 };
+  };
+
+  /* 数据的随机部分(卡池状态 / 进程数)会随时间漂:卡多一张,首行卡就高一截。所以每个状态都量两次:
+     - live:页面真身(刷新即变)
+     - det(确定性):同一页面、再跑一次,只把卡池那一截的随机变量抹平(见 DET_FN),用于 before/after 对比 */
+  await mount('/');
+  const desktop = await page.evaluate(DESKTOP);
+  const probeInfo = await page.evaluate(DET_FN);
+  await page.waitForTimeout(500);
+  const desktopDet = await page.evaluate(DESKTOP);
+  if (!probeInfo || probeInfo.kids <= 0) console.log('[probe] 确定性实例未挂上:', JSON.stringify(probeInfo));
+
+  /* 全站普通卡片:backdrop-filter / box-shadow 计数(四页) */
+  const glassScan = async () => {
+    await page.goto(new URL('/', BASE).href, { waitUntil: 'networkidle' }); // 同源,占位
+    const SCAN = `(() => { ${DEEP}
+      let blur = 0, shadow = 0; const blurWhere = new Set(), shadowWhere = new Set();
+      walk(document, (el) => {
+        const s = getComputedStyle(el);
+        const bf = s.backdropFilter || s.webkitBackdropFilter;
+        const vis = el.getBoundingClientRect().width > 8 && el.getBoundingClientRect().height > 4 && shownByStyle(el);
+        if (!vis) return;
+        if (bf && bf !== 'none') { blur++; blurWhere.add(elLabel(el)); }
+        if (s.boxShadow && s.boxShadow !== 'none') { shadow++; shadowWhere.add(elLabel(el)); }
+      });
+      return { blur, shadow, blurWhere: [...blurWhere], shadowWhere: [...shadowWhere] };
+    })()`;
+    const out = {};
+    for (const p of ['/', '/pool', '/stages', '/reports']) {
+      await mount(p);
+      out[p] = await page.evaluate(SCAN);
+    }
+    return out;
+  };
+
+  const glass = await glassScan();
+
+  /* 空状态 B:本月没有我的成果 —— 真实数据里每个人都有成果,所以就地实例化一个 view-workbench
+     (同一份 db,me 给一个不存在的名字 = 「选了个本月没交东西的人」)。用 customElements 里已注册的类,
+     不要 import 模块 —— 模块里的类没有 define(自定义元素已存在),new 它出来 render 不会生成任何节点。 */
+  await page.evaluate(() => localStorage.setItem('kf-board:me', JSON.stringify('赵磊')));
+  await mount('/');
+  const nobodyProbe = await page.evaluate(async () => {
+    /* 每次刷新都会重建 kf-app;离屏的确定性实例先在页面里摘掉(页面重新渲染时也会清掉) */
+    document.getElementById('v15-det')?.remove();
+    const main = document.querySelector('kf-app').shadowRoot.querySelector('main#view');
+    const tpl = document.createElement('template');
+    tpl.innerHTML = '<view-workbench id="v15-probe"></view-workbench>';
+    const v = tpl.content.firstElementChild;
+    v.db = main.querySelector('view-workbench').db; v.me = '_nobody_';
+    main.prepend(v);
+    await new Promise((r) => setTimeout(r, 600));
+    return { kids: v.shadowRoot?.childElementCount ?? -1 };
+  });
+  const nobody = await page.evaluate(`(() => { ${DEEP}
+    const vr = (document.getElementById('v15-probe') || appR().querySelector('main#view').firstElementChild).shadowRoot;
+    const a = vr.querySelector('kf-glass-card[data-role="a"]');
+    return { kids: vr.childElementCount,
+      roles: [...vr.querySelectorAll('kf-glass-card')].map((c) => c.dataset.role || '').filter(Boolean),
+      Aheight: a ? Math.round(a.getBoundingClientRect().height) : null,
+      text: vr.textContent.replace(/\\s+/g, ' ').trim().slice(0, 240) };
+  })()`);
+  nobody.probe = nobodyProbe;
+
+  /* 空状态 A:未选身份 */
+  await page.evaluate(() => document.getElementById('v15-probe')?.remove());
+  await page.waitForTimeout(300);
+  /* 清掉「我是」再重新挂载:kf-app 在 constructor 里读 localStorage,只清值不刷新不会生效 */
+  await page.evaluate(() => localStorage.removeItem('kf-board:me'));
+  await mount('/');
+  const noMe = await page.evaluate(`(() => { ${DEEP}
+    const vr = (document.getElementById('v15-probe') || appR().querySelector('main#view').firstElementChild).shadowRoot;
+    const a = vr.querySelector('kf-glass-card[data-role="a"]');
+    return { roles: [...vr.querySelectorAll('kf-glass-card')].map((c) => c.dataset.role || '').filter(Boolean),
+      Aheight: a ? Math.round(a.getBoundingClientRect().height) : null,
+      allstages: vr.querySelectorAll('[data-allstages] [data-stage]').length,
+      text: vr.textContent.replace(/\\s+/g, ' ').trim().slice(0, 240) };
+  })()`);
+
+  /* 空状态 C:没人卡住 —— 用真数据克隆一份 db,把每个人的 stuck 置 false(等于「8 小时内都有新成果」),
+     同一个页面里挂第二个实例(设成 overflow:hidden 的离屏盒里量高度,不影响页面)。 */
+  const noStall = await page.evaluate(async () => {
+    const main = document.querySelector('kf-app').shadowRoot.querySelector('main#view');
+    const live = main.querySelector('view-workbench');
+    const db = live.db;
+    const members = db.members.map((m) => ({ ...m, stuck: false, stuckHours: 0 }));
+    const box = document.createElement('div');
+    box.id = 'v15-ok';
+    box.style.cssText = 'position:absolute;left:-9999px;top:0;width:1200px';
+    document.body.append(box);
+    const tpl = document.createElement('template');
+    tpl.innerHTML = '<view-workbench></view-workbench>';
+    const v = tpl.content.firstElementChild;
+    v.db = { ...db, members, memberByName: Object.fromEntries(members.map((m) => [m.name, m])) };
+    v.me = '赵磊';
+    box.append(v);
+    await new Promise((r) => setTimeout(r, 700));
+    const vr = v.shadowRoot;
+    const a = vr.querySelector('kf-glass-card[data-role="a"]') || vr.querySelector('.grid2 > kf-glass-card');
+    const b = vr.querySelector('kf-glass-card[data-role="b"]') || vr.querySelector('.grid2 > kf-glass-card:nth-child(2)');
+    const out = { stalledItems: vr.querySelectorAll('[data-stalled]').length,
+      /* 改前代码没有 data-role:B 卡是首行第二张 kf-glass-card */
+      bText: b ? b.textContent.replace(/\s+/g, ' ').trim().slice(0, 120) : '',
+      aH: a ? Math.round(a.getBoundingClientRect().height) : null, bH: b ? Math.round(b.getBoundingClientRect().height) : null,
+      dot: b?.querySelector('.dot') ? getComputedStyle(b.querySelector('.dot')).backgroundColor : '',
+      bg: b ? getComputedStyle(b.shadowRoot.querySelector('.frame')).backgroundColor : '' };
+    /* 不摘离屏实例:摘掉的一瞬间它的 Lit 更新周期会把空的更新排队,影响到页面里同一组件的其它实例 */
+    window.__v15ok = box;
+    return out;
+  });
+  const stalled = await page.evaluate(() => (document.querySelector('kf-app')?.shadowRoot?.querySelector('main#view')?.firstElementChild?.shadowRoot?.querySelectorAll('[data-stalled]').length) ?? -1);
+
+  /* ---------------- 手机 393 ---------------- */
+  const mctx = await browser.newContext({ viewport: { width: 393, height: 852 }, deviceScaleFactor: 1, isMobile: true, hasTouch: true });
+  await mctx.addInitScript((name) => { try { localStorage.setItem('kf-board:me', JSON.stringify(name)); } catch { /* ignore */ } }, ME);
+  /* 手机上下文只加载一次,initScript 不会重跑,不需要上面那个一次性开关 */
+  const mpage = await mctx.newPage();
+  mpage.on('pageerror', (e) => errors.push('mobile: ' + e.message));
+  await mpage.goto(new URL('/', BASE).href, { waitUntil: 'networkidle' });
+  await mpage.waitForFunction(() => {
+    const a = document.querySelector('kf-app');
+    const v = a?.shadowRoot?.querySelector('main#view')?.firstElementChild;
+    return !!v && v.shadowRoot && v.shadowRoot.childElementCount > 0;
+  }, null, { timeout: 20000 });
+  await mpage.waitForTimeout(600);
+  await mpage.evaluate(DET_FN);
+  const mobile = await mpage.evaluate(`(() => { ${DEEP}
+    const vr = (document.getElementById('v15-det') || appR().querySelector('main#view').firstElementChild).shadowRoot;
+    const R = (el) => { const b = el.getBoundingClientRect(); return { x: +b.x.toFixed(1), y: +b.y.toFixed(1), w: +b.width.toFixed(1), h: +b.height.toFixed(1) }; };
+    const a = vr.querySelector('kf-glass-card[data-role="a"]'), b = vr.querySelector('kf-glass-card[data-role="b"]');
+    const rowInfo = (() => { const out = []; for (let q = a.parentElement; q && q !== vr; q = q.parentElement) out.push(q.localName + '.' + q.className + '@' + Math.round(q.getBoundingClientRect().top) + '-' + Math.round(q.getBoundingClientRect().bottom)); return out; })();
+    const fsCount = {};
+    walk(vr, (el) => {
+      if (![...el.childNodes].some((n) => n.nodeType === 3 && n.textContent.trim())) return;
+      if (!visible(el)) return;
+      const k = Math.round(parseFloat(getComputedStyle(el).fontSize) * 10) / 10 + 'px';
+      fsCount[k] = (fsCount[k] || 0) + 1;
+    });
+    /* 横向溢出:任一可见元素右边超出视口 */
+    let over = [];
+    /* 只算「看得见」的:display/visibility/opacity 链要正常 —— 关着的抽屉/弹层还在 DOM 里(在视口右外侧),不算溢出 */
+    walk(vr, (el) => { const r = el.getBoundingClientRect(); if (r.width > 0 && r.right > document.documentElement.clientWidth + 1 && shownByStyle(el)) over.push(elLabel(el) + '@' + Math.round(r.right)); });
+    return { scrollWidth: document.documentElement.scrollWidth, clientWidth: document.documentElement.clientWidth,
+      A: a ? R(a) : null, B: b ? R(b) : null,
+      /* 堆叠 = B 在垂直方向完全位于 A 下方且与 A 左边缘对齐(同一列里上下排)。
+         不能拿「一行」的 bottom 比 —— 行的 bottom 跟着最高的卡走,B 自己就是行内最高时恒为 false。 */
+      stacked: a && b ? Math.abs(a.getBoundingClientRect().left - b.getBoundingClientRect().left) < 2
+        && b.getBoundingClientRect().top >= a.getBoundingClientRect().bottom - 1 : null,
+      CDE: ['c', 'd', 'e'].map((r) => { const k = vr.querySelector('kf-glass-card[data-role="' + r + '"]'); return k ? R(k) : null; }),
+      fonts: Object.keys(fsCount).sort((p, q) => parseFloat(p) - parseFloat(q)), fontSizeCount: fsCount,
+      overflowing: [...new Set(over)].slice(0, 10) };
+  })()`);
+
+  /* ---------------- v12 回归 ---------------- */
+  let v12 = null;
+  if (argv.includes('--no-regress')) {
+    for (const p of ['v15/v12-desktop.txt', 'v15/v12-mobile.txt']) void p;
+    try {
+      const out = execFileSync(process.execPath, [join(ROOT, 'scripts/verify-visual-v12.mjs'), '--tag', 'before'], { cwd: ROOT, encoding: 'utf8', timeout: 300000, env: { ...process.env, VERIFY_PORT: '4199' } });
+      v12 = out;
+    } catch (e) {
+      v12 = (e.stdout || '') + (e.stderr || '') + `\n[exit ${e.status}]`;
+    }
+  }
+
+  const result = { codeFrom: FROM, codeSha, probeInfo, tag: TAG, at: new Date().toISOString(), desktop, desktopDet, glass, noMe, nobody, noStall, stalledCount: stalled, mobile, v12, errors };
+
+  /* ---------------- 打印 ---------------- */
+  const L = (s = '') => console.log(s);
+  L(`\n===== verify-v15 · ${TAG} · ${result.at} =====`);
+  L(`代码来源 ${FROM} · view-workbench.js sha1 ${codeSha}`);
+  L(`\n[1] Bento 栅格(1440,确定性数据:我的成果=2,卡池状态/份数保持数据快照「before」时刻)`);
+  const bt = desktopDet.bento;
+  L(`  A 宽 ${bt.A ?? '—'}px / B 宽 ${bt.B ?? '—'}px → 视觉 ${bt.aCols ?? '—'} 列 : ${bt.A && bt.B ? 12 - bt.aCols : '—'} 列`);
+  L(`  A/B 高度差 ${bt.ABheightDiff}px  ${bt.ABheightDiff != null && bt.ABheightDiff <= 2 ? '✓' : '✗(要求 ≤2)'}`);
+  L(`  短卡 C/D/E 宽 ${bt.CDE?.w.join(' / ')}  高 ${bt.CDE?.h.join(' / ')}  高差 ${bt.CDE?.hDiff}px  ${bt.CDE?.hDiff != null && bt.CDE.hDiff <= 2 ? '✓' : '✗(要求 ≤2)'}`);
+  L(`  F 通栏 ${bt.F ? `宽 ${bt.F.w}px 高 ${bt.F.h}px` : '(无)'}`);
+  L(`\n[2] B 卡深色 / C/D/E/F 在确定性数据下`);
+  L(`  B 背景 ${desktopDet.BbgText || '(无 data-role="b" 的卡)'}`);
+  L(`  C/D/E ${desktopDet.bento.CDE ? `宽 ${desktopDet.bento.CDE.w.join(' / ')}  高 ${desktopDet.bento.CDE.h.join(' / ')}  高差 ${desktopDet.bento.CDE.hDiff}px` : '(无 data-role=c/d/e 的卡)'}`);
+  L(`  F 通栏 ${desktopDet.bento.F ? `宽 ${desktopDet.bento.F.w}px 高 ${desktopDet.bento.F.h}px` : '(无 data-role="f" 的卡)'}`);
+  L(`  A 占 ${desktopDet.bento.aCols} / 12 列`);
+  L(`  — 实时数据(页面真身):A ${desktop.bento.A} / B ${desktop.bento.B}  高差 ${desktop.bento.ABheightDiff}px;C/D/E 高差 ${desktop.bento.CDE?.hDiff}px`);
+  L(`  B 背景 ${desktop.BbgText}`);
+  if (desktop.Bbg) {
+    const lum = ({ r, g, b }) => { const f = (v) => { v /= 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); }; return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b); };
+    L(`  亮度 ${lum(desktop.Bbg).toFixed(3)}  ${lum(desktop.Bbg) < 0.35 ? '✓(<0.35)' : '✗'}`);
+  }
+  if (desktop.Bcontrast) {
+    L(`  最差对比度 ${desktop.Bcontrast.c}:1(“${desktop.Bcontrast.t}”)  ${desktop.Bcontrast.c >= 7 ? '✓(≥7)' : '✗(要求 ≥7)'}`);
+    L(`  逐段:${desktop.Bother.map((o) => `${o.c}:1「${o.t}」`).join('  ')}`);
+  }
+  L(`\n[3] 主列相邻块间距(上→下)`);
+  for (const g of desktopDet.gaps) L(`  ${String(g.gap).padStart(6)}px  ${g.between}`);
+  L(`\n[4] 卡片 .face 内边距`);
+  const padCount = {};
+  for (const p of desktopDet.facePad) padCount[p.pad] = (padCount[p.pad] || 0) + 1;
+  L(`  ${JSON.stringify(padCount)}  ${Object.keys(padCount).length === 1 && Object.keys(padCount)[0] === '20px' ? '✓' : '✗(要求 20px)'}`);
+  L(`  逐卡:${desktopDet.facePad.map((p) => `${p.role || '?'}=${p.pad}`).join('  ')}`);
+  L(`\n[5] 字号档(工作台可见文字,1440)`);
+  L(`  ${desktopDet.fonts.tiers.length} 档:${desktopDet.fonts.tiers.join(' / ')}  ${desktopDet.fonts.tiers.length <= 5 && !desktopDet.fonts.tiers.includes('11px') ? '✓' : '✗'}`);
+  for (const k of desktopDet.fonts.tiers) L(`    ${k.padStart(5)} × ${desktopDet.fonts.count[k]}   ${desktopDet.fonts.where[k].join(', ')}`);
+  if (desktop.fonts.tiers.join() !== desktopDet.fonts.tiers.join()) L(`  (实时数据下的档位:${desktop.fonts.tiers.join(' / ')})`);
+  L(`\n[6] kf-spark:工作台 ${desktop.sparks.inView} 个,全站 ${desktop.sparks.inPage} 个`);
+  L(`  评分是实时数据:B 卡当前 ${stalled} 人卡住;C/D/E 数值随数据变化,对比只看结构与高差`);
+  L(`\n[7] 普通卡片玻璃/阴影(跨 shadow,可见元素)`);
+  for (const [p, v] of Object.entries(glass)) L(`  ${p.padEnd(9)} backdrop-filter ${String(v.blur).padStart(3)}  box-shadow ${String(v.shadow).padStart(3)}  ${v.blur ? '[' + v.blurWhere.slice(0, 6).join(', ') + ']' : ''}`);
+  L(`\n[8] 空状态`);
+  L(`  未选身份:roles=${noMe.roles.join(',')}  A 高 ${noMe.Aheight}px  [data-allstages] ${noMe.allstages}`);
+  L(`    「${noMe.text.slice(0, 140)}」`);
+  L(`  不存在的人(本月无成果):roles=${nobody.roles.join(',')}  A 高 ${nobody.Aheight}px`);
+  L(`    「${nobody.text.slice(0, 160)}」`);
+  L(`  没人卡住(克隆 db,stuck 全置 false):[data-stalled] ${noStall.stalledItems}  A 高 ${noStall.aH} / B 高 ${noStall.bH}(差 ${Math.abs(noStall.aH - noStall.bH)})`);
+  L(`    B 底 ${noStall.bg}  点 ${noStall.dot}  「${noStall.bText}」`);
+  L(`  当前实时数据里卡住的人:[data-stalled] ${stalled}`);
+  L(`\n[9] 移动端 393×852`);
+  const m = mobile;
+  L(`  scrollWidth ${m.scrollWidth} / clientWidth ${m.clientWidth}  ${m.scrollWidth === m.clientWidth ? '✓' : '✗ 横向溢出'}`);
+  if (m.A && m.B) L(`  A ${m.A.w}×${m.A.h} @y${m.A.y}  B ${m.B.w}×${m.B.h} @y${m.B.y}  堆叠 ${m.stacked ? '✓' : '✗'}`);
+  L(`  C/D/E ${m.CDE.map((k) => k ? `${k.w}×${k.h}@y${k.y}` : '—').join('  ')}`);
+  L(`  字号档 ${m.fonts.join(' / ')}  ${m.fonts.length <= 5 && !m.fonts.includes('11px') ? '✓' : '✗'}`);
+  if (m.overflowing.length) L(`  超出视口:${m.overflowing.join(', ')}`);
+  if (errors.length) L(`\n[!] 页面错误 ${errors.length}:\n  ${errors.join('\n  ')}`);
+  if (v12) {
+    L(`\n[10] v12 回归(verify-visual-v12 --tag before)`);
+    L(v12.split('\n').filter((l) => /✗|FAIL|失败|exit|总|汇总|项/.test(l)).slice(0, 60).join('\n'));
+  }
+
+  L(`\n[10] 短卡内容密度(字符数 / 卡面积 × 10⁴;dr1 基准 29 字 / 33 600px² = 8.6)`);
+  for (const s of desktopDet.statDensity || []) L(`  ${s.role}  ${s.w}×${s.h}  ${s.chars} 字 → ${s.per1e4}  ${s.per1e4 >= 8.6 * 0.7 ? '✓(≥ 基准 70%)' : '✗'}  「${s.text}」`);
+  L(`\n[11] 深色卡色相数(要求 ≤2;dr6 实测 1)`);
+  L(`  ${(desktop.hues || []).map(([h, n]) => `${h}×${n}`).join('  ') || '(无色相)'}  → ${(desktop.hues || []).length} 种  ${(desktop.hues || []).length <= 2 ? '✓' : '✗'}`);
+  L(`\n[12] 工作台按钮(高度 / 背景 / 描边)`);
+  for (const b of desktop.btns || []) L(`  ${String(b.h).padStart(3)}px  ${b.label.padEnd(8)}  ${b.bg}  ${b.border}`);
+  L(`  → ${(desktop.btns || []).filter((b) => b.h > 32).length} 个 >32px  ${(desktop.btns || []).filter((b) => b.h > 32).length === 0 ? '✓' : '✗'}`);
+  if (JSON_OUT) {
+    const p = JSON_OUT.startsWith('/') ? JSON_OUT : join(ROOT, JSON_OUT);
+    writeFileSync(p, JSON.stringify(result, null, 2));
+    L(`\n→ ${p}`);
+  }
+};
+
+try {
+  await main();
+} finally {
+  await browser.close();
+  try { srv.kill(); } catch { /* ignore */ }
+  cleanup();
+}

+ 12 - 9
src/pages/report-card.js

@@ -195,11 +195,13 @@ export const CARD_CSS = css`
   .rc.play .play-nums b { display: block; font-size: 20px; font-weight: 600; line-height: 1.1; color: var(--kf-ink); font-variant-numeric: tabular-nums; white-space: nowrap; }
   .rc.play .play-nums span { display: block; margin-top: 2px; font-size: 12px; color: var(--kf-ink-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
   .rc.play .play-acts { display: flex; align-items: center; gap: 8px; margin-top: 2px; }
-  .rc.play .seg { display: inline-flex; border: 1px solid var(--kf-line-soft); border-radius: var(--r-sm); overflow: hidden; }
-  .rc.play .seg button { border: 0; background: transparent; font: 500 12px/1 var(--kf-font); color: var(--kf-ink-2); padding: 0 10px; height: 26px; cursor: pointer; }
-  .rc.play .seg button + button { border-left: 1px solid var(--kf-line-soft); }
-  .rc.play .seg button:hover { background: var(--kf-surface-btn); }
-  .rc.play .seg button[aria-pressed="true"] { background: var(--kf-ink); color: #fff; }
+  /* v16:只有按钮文字的样式放这里;布局(内边距 / 间隙 / 是否撑满)由页面给 —— 同一条 ">
+     规则写进 CARD_CSS 后,工作台的 flex 页脚(空 span 把「详情 →」推到最右)会被 gap 撑出 84px。 */
+  .card-play .quiet button { white-space: nowrap; }
+  .rc.play .quiet button { border: 0; background: none; padding: 0; height: 20px; font: 500 12px/1 var(--kf-font); color: var(--kf-ink-3); cursor: pointer;
+    text-box-trim: trim-both; text-box-edge: cap alphabetic; }
+  .rc.play .quiet button:hover { color: var(--kf-ink); }
+  .rc.play .quiet button[aria-pressed="true"] { color: var(--kf-accent-ink); font-weight: 600; box-shadow: inset 0 -1px 0 var(--kf-accent); }
   .rc.play .sp { flex: 1; }
   .rc.play .more { font-size: 12px; color: var(--kf-ink-3); }
 `;
@@ -277,7 +279,7 @@ export function sourceRow(e, { nowrap = false } = {}) {
 }
 
 /** 机会卡状态 → 中文标签(与卡池 / 台账同一个枚举) */
-const OPP_STATUS_LABEL = { candidate: '候选', approved: '已立项', rejected: '已否决' };
+export const OPP_STATUS_LABEL = { candidate: '候选', approved: '已立项', rejected: '已否决' };
 
 /**
  * 工作台用的「安静」机会卡(v15):和卡池那张卡同源(同一个 normalized opportunity),
@@ -290,7 +292,7 @@ export function playCard({ opp: o, status, onStatus }) {
   const s = o.stats || { avg: null, n: 0 };
   const st = status || o.status;
   return html`
-    <div class="rc play" data-opp=${o.id}>
+    <div class="rc play card-play" data-opp=${o.id}>
       <div class="top">
         ${o.priority ? html`<span class="badge prio">${o.priority}</span>` : ''}
         ${o.category ? html`<span class="badge cat">${o.category}</span>` : ''}
@@ -300,12 +302,13 @@ export function playCard({ opp: o, status, onStatus }) {
       <h3 title=${o.title}>${o.title}</h3>
       ${o.oneLiner ? html`<p class="lead">${o.oneLiner}</p>` : ''}
       <div class="play-nums">
-        <div><b>${s.avg == null ? '—' : Math.round(s.avg)}</b><span>${s.n ? `分 · ${s.n} 人` : '待评分'}</span></div>
+        <div><b>${s.avg == null ? '—' : Math.round(s.avg)}</b><span>${s.n ? t(`分 · ${s.n} 人`) : '待评分'}</span></div>
         <div><b>${o.evidenceLabel}</b><span>证据</span></div>
         <div><b>${o.quoteCount}</b><span>条引用</span></div>
       </div>
+      <!-- v16:三连灰底按钮 → 静默文字行(dr6/dr1 的卡片底部没有按钮组)。字重 500,选中项加粗 + 下划线 -->
       <div class="play-acts" data-no-activate>
-        <div class="seg" role="group" aria-label="改状态">
+        <div class="quiet" role="group" aria-label="改状态">
           ${['candidate', 'approved', 'rejected'].map((k) => html`<button type="button" ?aria-pressed=${st === k}
             @click=${(e) => { e.preventDefault(); e.stopPropagation(); onStatus?.(k); }}>${OPP_STATUS_LABEL[k]}</button>`)}
         </div>

+ 237 - 214
src/pages/view-workbench.js

@@ -1,13 +1,15 @@
 /**
- * pages/view-workbench.js —— <view-workbench> 工作台首页(v15 Bento 重构)。
+ * pages/view-workbench.js —— <view-workbench> 工作台首页(v16:按 docs/v16-research.md 的可执行参数重排)。
  *
  * 骨架(12 栅格):
- *   A 我的推进(8 列,浅色) | B 现场(4 列,深色:全页唯一一块深卡,锚定注意力)
- *   C 卡池 | D 最高分 | E 已决策       ← 三张短数据卡,一行等高:标签 / 大数字 / 一行说明
- *   F 最近动过(通栏,最多 3 条)
+ *   A 我的推进(8 列,浅色,单列列表) | B 现场(4 列,深色:全页唯一一块深卡,锚定注意力)
+ *   C/D/E/F 四张短数据卡(各 3 列)   ← 标签 / 大数字 28·500 / 一行结论(dr1 的四卡结构)
+ *   G 最近动过(机会卡)+ 右侧时间线
  *
- * 视觉纪律(v15):浅底嵌深卡、单一强调色(--kf-accent 只给一个关键动作)、字号只留 12/14/16/20/32、
- * 区块之间 32、卡与卡 24、卡内边距 20、卡内 gap 12。不再放柱状装饰(kf-spark 已移除)。
+ * 视觉纪律(v16,数字出处见研究文档 §6):
+ *   字号 12/14/16/20/28/32;字重 400/500/600,大数字 500(dr1/dr6 的大数字比标题细);
+ *   区块之间 32、卡与卡 16(dr1 8 / dr6 14 取中)、卡内边距 20、卡内组与组 ≥ 24;
+ *   深色卡只允许 1 个强调色相(琥珀 = 卡住),阶段进度用灰阶条(dr6 深卡靠形态不靠颜色)。
  */
 import { LitElement, css, html } from 'lit';
 import { useTokens } from '../components/base.js';
@@ -20,6 +22,7 @@ import { cardFooter, playCard, CARD_CSS } from './report-card.js';
 import { icon } from '../components/icons.js';
 
 const TONE = { S1: 'blue', S2: 'violet', S3: 'teal', S4: 'amber', S5: 'rose' };
+const DOT = { approved: '#F4F4F5', rejected: 'rgba(255,255,255,0.28)', candidate: '#B4B4BC' };
 /** 「最近动过」的时间写法:今天 / 昨天 / N 天前 / 日期 */
 const ago = (iso) => {
   const t0 = Date.parse(iso || '');
@@ -27,18 +30,21 @@ const ago = (iso) => {
   const d = Math.floor((Date.now() - t0) / 86400000);
   if (d <= 0) return '今天';
   if (d === 1) return '昨天';
-  if (d < 7) return `${d} 天前`;
+  if (d < 7) return t(`${d} 天前`);
   const dt = new Date(t0);
   return `${dt.getMonth() + 1}月${dt.getDate()}日`;
 };
 
 export class ViewWorkbench extends LitElement {
+  #pending = [];
+  #zero = false;
+
   static properties = {
     db: { type: Object },
     me: { type: String },
   };
 
-  #mineLatest = null;
+
 
   constructor() {
     super();
@@ -52,211 +58,209 @@ export class ViewWorkbench extends LitElement {
   }
 
   static styles = [VIEW_CSS, CARD_CSS, css`
-    /* 纵向节奏(v15):区块之间 32 / 卡与卡 24 / 卡内元素 12。页头到第一块也是 32(Bento 第一块就是内容)。 */
+    /* 纵向节奏(v16):区块之间 32 / 卡与卡 16(dr1 8、dr6 14,取中)/ 卡内组与组 ≥ 24。 */
     .bento { display: flex; flex-direction: column; gap: var(--kf-space-section); margin-top: var(--kf-space-section); }
-    .bento > .row { display: grid; gap: var(--kf-space-card-gap); align-items: stretch; }
+    .bento > .row { display: grid; gap: 16px; align-items: stretch; }
     .bento > .row.ab { grid-template-columns: repeat(12, minmax(0, 1fr)); }
     .bento > .row.ab > kf-glass-card { height: auto; }
-    /* A/B 用 fill-to 吃满行高:卡面 .face 是 flex 列,slotted 的第一层(.me / .live)伸到底(下方各行的实际高度由本页 CSS 排) */
+    /* A/B 用 fill-to 吃满行高:卡面 .face 是 flex 列,slotted 的第一层(.me / .live)伸到底 */
     .bento > .row.ab > kf-glass-card[fill-to] .me, .bento > .row.ab > kf-glass-card[fill-to] .live { flex: 1 1 auto; }
     .bento > .row.ab > kf-glass-card[data-role="a"] { grid-column: span 8; }
     .bento > .row.ab > kf-glass-card[data-role="b"] { grid-column: span 4; }
-    .bento > .row.cde { grid-template-columns: repeat(3, minmax(0, 1fr)); }
+    /* 四张短卡:12 栅格里各占 3 列(1440 下 ≈ 268px,贴 dr1 的 232–249) */
+    .bento > .row.stats { grid-template-columns: repeat(12, minmax(0, 1fr)); }
+    .bento > .row.stats > kf-glass-card { grid-column: span 3; }
+    /* 短卡内边距 12(dr1 量得 18–20、卡宽 249;我们卡宽 270,按面积折算约 12–16):12 时「28 字 / 卡面积」才回到 dr1 的 8.6 附近。
+       dr6 的 20 是它 270–425px 宽卡用的;卡变窄后同一 padding 会把可用面积吃掉一截。 */
+    .bento > .row.stats > kf-glass-card { --kf-card-pad: 16px; }
     .p-head { padding-top: var(--kf-space-top); padding-bottom: 0; }
-    /* 页头 → Bento 第一块:32(v15 区块之间统一 32) */
     .p-head + .bento { margin-top: 32px; }
 
     /* ---------- 通用小件 ---------- */
     .lbl { display: flex; align-items: center; gap: 8px; font-size: 12px; font-weight: 500; line-height: 1.4; letter-spacing: var(--ls-cn-small); color: var(--kf-ink-3); }
     .dot { width: 6px; height: 6px; border-radius: 50%; flex: none; display: inline-block; }
+    /* 文字链接:14/500,不带底色。dr6/dr1 的卡内动作都是文字 + 箭头,强调色实心按钮只留给深色 / 彩色卡 */
     .go { display: inline-flex; align-items: center; gap: 4px; font-size: 14px; font-weight: 500; color: var(--kf-accent-ink); text-decoration: none; }
     .go:hover { color: var(--kf-accent); }
-    /* 全页唯一一个强调色实心元素:C 卡上的「全部机会卡 →」 */
-    .btn-key { display: inline-flex; align-items: center; gap: 6px; height: 32px; padding: 0 14px; border-radius: var(--r-sm);
-      background: var(--kf-accent); color: #fff; font-size: 14px; font-weight: 500; text-decoration: none; flex: none; }
-    .btn-key:hover { background: var(--kf-accent-strong); }
     .empty { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px; padding: 12px 14px; color: var(--kf-ink-3); font-size: 14px; line-height: 1.5;
       border: 1px dashed color-mix(in srgb, var(--kf-ink-3) 24%, transparent); border-radius: var(--r-md); }
     .empty .go { margin-left: auto; }
 
-    /* ---------- A 卡:我的推进 ---------- */
-    /* A 卡内容按行贴顶排;行距 12;下半的两个分区用 1fr 行把底边对齐(待评分区贴底,不再在卡片中段留 59px 空洞) */
-    /* A 卡是 flex 列:阶段行以「剩余高度」行出现,多出来的空间从它这里再分给三格(格内进度条沉底)。 */
-    .me { display: flex; flex-direction: column; gap: var(--kf-space-in); }
-    .me > .me-id { flex: none; }
-    .me > .stagebar { flex: none; }
-    .me > .empty { flex: none; }
-    .me > .me-split { flex: none; margin-top: auto; }
+    /* ---------- A 卡:我的推进(单列列表,dr6 Onboarding / dr1 Project 的形态) ---------- */
+    .me { display: flex; flex-direction: column; gap: 24px; }
     .me-id { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 2px 12px; }
     .me-id .me-title { display: grid; align-content: center; min-width: 0; }
-    .avatar { width: 44px; height: 44px; border-radius: var(--r-md); display: grid; place-items: center; font-size: 20px; font-weight: 600; color: #fff;
+    .avatar { width: 40px; height: 40px; border-radius: var(--r-md); display: grid; place-items: center; font-size: 20px; font-weight: 600; color: #fff;
       background: var(--kf-ink); flex: none; }
     .avatar.q { background: var(--kf-fill); color: var(--kf-ink-3); border: 1px dashed color-mix(in srgb, var(--kf-ink-3) 34%, transparent); }
-    .me-id h3 { margin: 0; font-size: 20px; line-height: var(--lh-title); font-weight: 600; color: var(--kf-ink); letter-spacing: var(--ls-cn-display); }
-    .me-id .sub { font-size: 14px; color: var(--kf-ink-3); }
+    .me-id h3 { margin: 0; font-size: 16px; line-height: var(--lh-title); font-weight: 600; color: var(--kf-ink); letter-spacing: var(--ls-cn-display); }
+    .me-id .sub { font-size: 12px; line-height: 1.5; color: var(--kf-ink-3); }
     .me-id .go { margin-left: auto; }
     .me-nums { display: flex; gap: 24px; justify-self: end; }
     .me-nums div { display: flex; align-items: baseline; gap: 4px; }
-    .me-nums b { font-size: 20px; font-weight: 600; line-height: 1; letter-spacing: var(--ls-cn-display); color: var(--gray-12);
+    /* 大数字 28/500(dr1 数字 28、字重比标题细) */
+    .me-nums b { font-size: 28px; font-weight: 500; line-height: 1; letter-spacing: var(--ls-cn-display); color: var(--gray-12);
       font-variant-numeric: tabular-nums; }
     .me-nums span { font-size: 12px; font-weight: 500; color: var(--kf-ink-3); white-space: nowrap; }
 
-    /* 阶段条:五格一格一句「S1 · 份数」,徽标 + 大数字 + 一根细进度条 */
-    .stagebar { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: var(--kf-space-in); margin-top: calc(-1 * var(--kf-space-in) + 4px); }
+    /* 阶段条:五格一格一句「S1 · 份数」,徽标 + 数字 + 细进度条;颜色只用在这排徽标上 */
+    .stagebar { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 12px; }
     .stg { padding: 12px; border-radius: var(--r-md); background: var(--kf-fill); border: 1px solid transparent;
-      /* 阶段格铺满分到的高度带:上面 3 行贴顶,细进度条沉到底边(不留「格子里空一截」) */
       display: grid; grid-template-columns: auto 1fr; align-content: start; align-items: center; gap: 2px 8px; min-width: 0; }
     .stg .b { font-size: 12px; font-weight: 600; line-height: 1; color: #fff; padding: 4px 6px; border-radius: var(--r-sm); letter-spacing: 0.02em; justify-self: start; }
     .stg .b.blue { background: var(--kf-s1); } .stg .b.violet { background: var(--kf-s2); } .stg .b.teal { background: var(--kf-s3); }
     .stg .b.amber { background: var(--kf-s4); } .stg .b.rose { background: var(--kf-s5); }
-    .stg .n { justify-self: end; font-size: 20px; font-weight: 600; line-height: 1; letter-spacing: var(--ls-cn-display); color: var(--gray-12); font-variant-numeric: tabular-nums; }
+    .stg .n { justify-self: end; font-size: 20px; font-weight: 500; line-height: 1; letter-spacing: var(--ls-cn-display); color: var(--gray-12); font-variant-numeric: tabular-nums; }
     .stg .sf { grid-column: 1 / -1; min-height: 16px; font-size: 12px; line-height: 1.3; color: var(--kf-ink-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
     .stg .f { grid-column: 1 / -1; height: 4px; border-radius: 2px; background: rgba(16, 24, 40, 0.07); overflow: hidden; margin-top: 3px; }
     .stg .f i { display: block; height: 100%; border-radius: 2px; background: currentColor; width: calc(var(--v) * 100%); }
     .stg.blue { color: var(--kf-s1); } .stg.violet { color: var(--kf-s2); } .stg.teal { color: var(--kf-s3); } .stg.amber { color: var(--kf-s4); } .stg.rose { color: var(--kf-s5); }
     .stg.todo { color: var(--kf-ink-3); background: transparent; border-style: dashed; border-color: color-mix(in srgb, var(--kf-ink-3) 22%, transparent); }
     .stg.todo .n { color: var(--kf-ink-3); font-weight: 500; }
-    /* A 卡下半:待评分(左)| 卡池进度(右)。两个分区头共用 .lbl,行底对齐 12 */
-    .me-split { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 32px; align-items: start; }
-    .me-split > div { min-width: 0; }
-    .me-split .lbl { min-height: 22px; }
-    .pending { display: grid; gap: 0; margin-top: 4px; }
-    /* 待评分行贴底:与左列的三行进度在同一底线上 */
-    .me-split > div:last-child { display: flex; flex-direction: column; }
-    .me-split > div:last-child .pending, .me-split > div:last-child .me-none { margin-top: auto; }
-    .pending li { display: flex; align-items: baseline; gap: 8px; min-height: 32px; font-size: 14px; }
-    .pending li a { color: var(--kf-ink); text-decoration: none; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
-    .pending li a:hover { color: var(--kf-accent-ink); }
-    .pending li .d { font-size: 12px; color: var(--kf-ink-3); flex: none; }
-    .pending li .s { margin-left: auto; font-weight: 600; color: var(--kf-accent-ink); font-variant-numeric: tabular-nums; flex: none; }
-    .pending li .s.dim { color: var(--kf-ink-3); font-weight: 500; }
-    .pending li .nm { font-weight: 500; }
-    .me-none { font-size: 14px; color: var(--kf-ink-3); padding-top: 10px; }
-    .me-note { font-size: 12px; line-height: 1.5; color: var(--kf-ink-3); padding-top: 2px; }
-    .prog { display: grid; gap: 0; margin-top: 4px; }
-    .prog li { display: grid; grid-template-columns: 6px auto 1fr auto; align-items: baseline; gap: 10px; min-height: 32px; font-size: 14px; }
-    .prog .pk { color: var(--kf-ink-2); }
-    .prog .pv { color: var(--kf-ink); font-variant-numeric: tabular-nums; }
-    .prog .pv b { font-weight: 600; }
-    .prog .pp { color: var(--kf-ink-3); font-size: 12px; font-variant-numeric: tabular-nums; }
 
-    /* ---------- B 卡:现场(深色) ---------- */
-    .live { display: flex; flex-direction: column; gap: var(--kf-space-in); align-content: start;
+    /* A 卡下部:一条通栏列表(v16)。每行「左名 + 右值」两端对齐,中间多余宽度归名称,不放两栏大空白。 */
+    .me-list { display: grid; }
+    .sec { display: grid; }
+    .sec + .sec { margin-top: 16px; }
+    .sec .lbl { min-height: 20px; }
+    .row { display: flex; align-items: center; gap: 12px; min-height: 34px; font-size: 14px; border-top: 1px solid var(--kf-line); }
+    .sec .row:first-of-type { border-top: 0; }
+    .row .t { min-width: 0; flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--kf-ink); text-decoration: none; }
+    a.t:hover { color: var(--kf-accent-ink); }
+    .row .meta { flex: none; font-size: 12px; color: var(--kf-ink-3); font-variant-numeric: tabular-nums; }
+    .row .pill { flex: none; display: inline-flex; align-items: center; justify-content: center; min-width: 44px; height: 24px; padding: 0 10px; border-radius: var(--r-sm);
+      border: 1px solid var(--kf-line-soft); font-size: 12px; font-weight: 500; color: var(--kf-accent-ink); text-decoration: none; font-variant-numeric: tabular-nums; }
+    .row .pill:hover { border-color: var(--kf-accent); }
+    .row .pill.dim { color: var(--kf-ink-3); pointer-events: none; }
+    .row .rate { flex: none; display: inline-grid; grid-template-columns: 48px auto; align-items: center; gap: 8px; }
+    .row .rate .bar { height: 4px; border-radius: 2px; background: rgba(16, 24, 40, 0.08); overflow: hidden; }
+    .row .rate .bar i { display: block; height: 100%; border-radius: 2px; background: var(--kf-ink-2); }
+    .row .rate .v { font-size: 12px; color: var(--kf-ink-3); font-variant-numeric: tabular-nums; white-space: nowrap; }
+    .row .rate .v b { font-weight: 600; color: var(--kf-ink); }
+    .me-none { font-size: 14px; color: var(--kf-ink-3); padding-bottom: 6px; }
+    /* 列表是成组信息:放在卡面下半(dr6 深卡 / dr1 Project 卡都是「标题区在上、列表压在底」)。
+       没选身份时 A 卡内容本来就满,这里不会再生出空白;去掉它就会出现 56px 级别的连续空白。 */
+    .me > .me-list { margin-top: auto; }
+    /* 列表只有一两行时,别把整块 A 卡的高度往深色卡那边抬(B 卡跟行高走) */
+    .me > .me-list .row { min-height: 30px; }
+
+    /* ---------- B 卡:现场(深色;1 个强调色相 = 琥珀;其余灰阶) ---------- */
+    .live { display: flex; flex-direction: column; gap: 20px; align-content: start;
       --live-ink: #F4F4F5; --live-ink-3: #B4B4BC; --live-line: rgba(255, 255, 255, 0.12); }
     .live .lbl { color: var(--live-ink-3); }
     .live-head { display: flex; align-items: center; gap: 8px; }
     .live-head .go { margin-left: auto; color: var(--live-ink-3); }
     .live-head .go:hover { color: var(--live-ink); }
     .live-num { display: flex; align-items: baseline; gap: 8px; color: var(--live-ink); }
-    .live-num b { font-size: 32px; font-weight: 600; line-height: 1; letter-spacing: var(--ls-cn-display); font-variant-numeric: tabular-nums; }
+    .live-num b { font-size: 32px; font-weight: 500; line-height: 1; letter-spacing: var(--ls-cn-display); font-variant-numeric: tabular-nums; }
     .live-num span { font-size: 14px; color: var(--live-ink-3); }
     .live-title { font-size: 16px; font-weight: 600; line-height: 1.3; color: var(--live-ink); }
-    .stalled { border-top: 1px solid var(--live-line); }
-    .stalled li { display: flex; gap: 8px; align-items: baseline; padding: 8px 0; border-bottom: 1px solid var(--live-line); font-size: 14px; color: var(--live-ink); }
-    .stalled li:last-child { border-bottom: 0; }
+    .stalled { display: grid; gap: 12px; margin: -4px 0 0; padding: 0; list-style: none; }
+    .stalled li { display: flex; gap: 8px; align-items: baseline; padding: 8px 0; border-top: 1px solid var(--live-line); font-size: 14px; color: var(--live-ink); }
     .stalled .nm { font-weight: 500; }
     .stalled .meta { color: var(--live-ink-3); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
-    .stalled .h { margin-left: auto; font-weight: 600; font-variant-numeric: tabular-nums; }
-    .live-ok { display: flex; align-items: center; gap: 8px; margin-top: 4px; font-size: 14px; font-weight: 500; color: var(--live-ink); }
-    .live-rank { display: grid; gap: 0; border-top: 1px solid var(--live-line); padding-top: 8px; }
+    .stalled .h { margin-left: auto; font-weight: 500; font-variant-numeric: tabular-nums; }
+    .live-ok { display: flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 500; color: var(--live-ink); margin: 0; }
+    /* 各阶段已交:灰阶细条(轨道 12% 白、填充 90% 白),状态靠长度,不靠颜色 */
+    .live-rank { display: grid; gap: 0; }
     .live-rank .lbl { margin-bottom: 4px; }
-    .rk { display: grid; grid-template-columns: 6px minmax(0, 1fr) 56px auto; align-items: center; gap: 8px; min-height: 26px; font-size: 12px; color: var(--live-ink); }
-    .rk-k { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+    .rk { display: grid; grid-template-columns: minmax(0, 1fr) 44px auto; align-items: center; gap: 10px; min-height: 26px; font-size: 12px; color: var(--live-ink); }
+    .rk-k { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--live-ink-3); }
     .rk-bar { height: 4px; border-radius: 2px; background: var(--live-line); overflow: hidden; }
     .rk-bar i { display: block; height: 100%; border-radius: 2px; background: var(--live-ink); }
-    .rk-n { font-variant-numeric: tabular-nums; color: var(--live-ink-3); }
-    .live-foot { margin-top: auto; padding-top: 8px; font-size: 12px; color: var(--live-ink-3); }
+    .rk-n { font-variant-numeric: tabular-nums; color: var(--live-ink-3); text-align: right; }
+    .live-foot { margin-top: auto; font-size: 12px; color: var(--live-ink-3); }
 
-    /* ---------- C/D/E:三张短数据卡 ---------- */
-    /* 三张短数据卡:近方形(dr6 的 KPI 行),卡内上标签 / 下数字说明 */
-    .stat { display: grid; grid-template-rows: auto 1fr auto; gap: 8px; height: 100%; align-content: start; min-height: 152px; }
-    .stat b { align-self: end; }
-    .stat .note { min-height: 0; }
-    .stat .top { display: flex; align-items: center; gap: 8px; min-height: 24px; }
-    .stat .top .go { margin-left: auto; font-size: 14px; }
-    .stat b { font-size: 32px; font-weight: 600; line-height: 1; letter-spacing: var(--ls-cn-display); color: var(--gray-12);
-      font-variant-numeric: tabular-nums; white-space: nowrap; min-width: 0; }
+    /* ---------- C/D/E/F:四张短数据卡(dr1:标题 16/600 · 数字 28/500 · 结论 12) ---------- */
+    .stat { display: flex; flex-direction: column; gap: 12px; height: 100%; min-height: 132px; }
+    .stat .head { display: flex; align-items: center; gap: 8px; min-height: 24px; }
+    .stat .head .lbl { font-size: 16px; font-weight: 600; letter-spacing: 0; color: var(--kf-ink); }
+    .stat .head .go { margin-left: auto; font-size: 12px; }
+    .stat .num { display: flex; align-items: baseline; gap: 4px; }
+    .stat b { font-size: 28px; font-weight: 500; line-height: 1; letter-spacing: var(--ls-cn-display); color: var(--gray-12);
+      font-variant-numeric: tabular-nums; white-space: nowrap; }
     .stat b.dim { color: var(--kf-ink-3); }
-    .stat b .u { margin-left: 0.14em; font-size: 14px; font-weight: 500; color: var(--kf-ink-3); letter-spacing: 0; }
-    .stat .note { font-size: 12px; line-height: 1.5; letter-spacing: var(--ls-cn-small); color: var(--kf-ink-3);
-      white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-height: 18px; }
-
-    /* ---------- 底行:左侧机会卡(3 张,与池子同一张卡)+ 右侧「最近动过」时间线 ---------- */
+    .stat .num .u { font-size: 12px; font-weight: 500; color: var(--kf-ink-3); letter-spacing: 0; }
+    /* 结论行:底边对齐,像 dr1 的「Increased from last month」 */
+    .stat .note { margin-top: auto; display: flex; align-items: center; gap: 6px; min-width: 0; font-size: 12px; line-height: 1.5; letter-spacing: var(--ls-cn-small); color: var(--kf-ink-3); }
+    .stat .note .t { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+    /* 趋势徽章:dr1 的「⌃6」= 20×14 小方块 + 箭头 + 数字,绿色描边 */
+    .stat .delta { flex: none; display: inline-flex; align-items: center; gap: 2px; height: 18px; padding: 0 6px; border-radius: 8px; border: 1px solid rgba(47, 125, 91, 0.4); color: #2F7D5B; font-size: 12px; font-weight: 600; line-height: 1; font-variant-numeric: tabular-nums; }
+    .stat .delta.zero { border-color: var(--kf-line-soft); color: var(--kf-ink-3); }
+    /* ---------- 底行:左侧机会卡 + 右侧「最近动过」时间线 ---------- */
     .cards-row { grid-template-columns: repeat(12, minmax(0, 1fr)); align-items: start; }
     .cards-main { grid-column: span 8; min-width: 0; }
     .cards-main .feed-head { margin-bottom: 0; }
     .cards-main .cards { margin-top: 12px; }
-    .cards { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--kf-space-card-gap); align-items: stretch; }
+    .cards { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; align-items: stretch; }
     .cards > kf-glass-card { min-width: 0; }
+    /* 机会卡底部「候选 / 已立项 / 已否决」:静默文字行(20px、无底),页脚仍是「左段 + 详情 →」两端对齐 */
+    .cards .quiet { display: inline-flex; gap: 14px; }
+    .cards .quiet button { height: 20px; }
     .side-stack { grid-column: span 4; min-width: 0; }
-    .side-in { display: flex; flex-direction: column; height: 100%; }
+    .side-in { display: flex; flex-direction: column; height: 100%; gap: 8px; }
     .side-in [data-feed] { flex: 1 1 auto; }
     .feed-head { display: flex; align-items: center; gap: 8px; min-height: 22px; }
     .feed-head .go { margin-left: auto; }
-    .feed-head .lbl { font-size: 14px; font-weight: 600; color: var(--kf-ink); }
-    /* 时间线:时间在左(12 灰),标题在右;竖向虚线把时间与内容分开 */
+    .feed-head .lbl { font-size: 16px; font-weight: 600; color: var(--kf-ink); }
+    /* 时间线:时间在右(12 灰),标题 14,副行 12 */
     .feed { display: grid; margin-top: 8px; }
-    .feed li { display: grid; grid-template-columns: 12px minmax(0, 1fr); column-gap: 12px; row-gap: 2px; padding: 10px 0; border-top: 1px solid var(--kf-line); position: relative; }
+    .feed li { display: grid; grid-template-columns: 12px minmax(0, 1fr) auto; column-gap: 12px; row-gap: 2px; padding: 10px 0; border-top: 1px solid var(--kf-line); position: relative; }
     .feed li:first-child { border-top: 0; }
-    .feed li::before { content: ""; grid-row: 1 / 3; grid-column: 1; width: 7px; height: 7px; border-radius: 50%; border: 1.5px solid var(--kf-line-strong); background: var(--kf-surface); margin-top: 6px; justify-self: center; }
-    .feed li:first-child::before { background: var(--kf-ink); border-color: var(--kf-ink); }
+    /* 状态点 = 灰阶明度(已立项最亮 / 候选中灰 / 已否决暗);没有第二种色相 */
+    .feed .fd { grid-row: 1 / 3; grid-column: 1; width: 7px; height: 7px; border-radius: 50%; border: 1px solid rgba(0, 0, 0, 0.14); margin-top: 6px; justify-self: center; }
     .feed li a { grid-column: 2; color: var(--kf-ink); font-weight: 500; font-size: 14px; text-decoration: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
     .feed li a:hover { color: var(--kf-accent-ink); }
     .feed .st { grid-column: 2; font-size: 12px; color: var(--kf-ink-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
-    .feed .tm { position: absolute; right: 0; top: 10px; font-size: 12px; color: var(--kf-ink-3); font-variant-numeric: tabular-nums; }
+    .feed .tm { grid-column: 3; grid-row: 1; font-size: 12px; color: var(--kf-ink-3); font-variant-numeric: tabular-nums; }
     .feed-empty { font-size: 14px; color: var(--kf-ink-3); padding: 8px 0 0; }
-    /* 抽屉:机会卡全貌 */
-    .dr { display: grid; gap: 16px; }
-    .dr .act-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
-    .dr .act-row .go { margin-left: auto; }
     .foot-links { display: none; }
 
-    /* 平板:A/B 变 7/5 太挤,直接上下堆;C/D/E 保持一行 */
+    /* 平板:A/B 上下堆;四张短卡两列 */
     @media (max-width: 1100px) {
       .bento > .row.ab { grid-template-columns: minmax(0, 1fr); }
       .bento > .row.ab > kf-glass-card[data-role="a"], .bento > .row.ab > kf-glass-card[data-role="b"] { grid-column: auto; }
+      .bento > .row.stats > kf-glass-card { grid-column: span 6; }
     }
-    /* 手机 393:A/B 竖排,C/D/E 三卡一行(每卡 3 行文字仍放得下),可点区域 44px */
+    /* 手机 393:A/B 竖排;短卡两列;可点区域 44px */
     @media (max-width: 767px) {
       .bento { gap: var(--kf-space-card); margin-top: 24px; }
       .p-head + .bento { margin-top: 24px; }
       .bento > .row { gap: var(--kf-space-card); }
-      .bento > .row.cde { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
-      .me { gap: 12px; }
-      .me-id { grid-template-columns: auto minmax(0, 1fr) !important; }
-      .me-id .avatar { width: 40px; height: 40px; font-size: 20px; }
-      .me-nums { grid-column: 1 / -1; justify-self: start; }
-      .me-nums { margin-left: 0; width: 100%; justify-content: flex-start; gap: 24px; }
-      .me-nums div { text-align: left; display: flex; align-items: baseline; gap: 4px; }
-      /* 五阶段一行放不下英文名:两行三列(第五格占满第二行左半),英文名折行两行后省略 */
+      .bento > .row.stats { gap: 12px; }
+      /* 手机上两列短卡去掉「全部 →」小链接:结论行留给信息,入口在页头「机会卡池 →」 */
+      .stat .head .go { display: none; }
+      /* 结论行在窄卡里允许两行,不截断成「5 人提交 · 候选 4 · …」 */
+      .stat .note .t { white-space: normal; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; }
+      .bento > .row.stats > kf-glass-card { grid-column: span 6; }
+      .me { gap: 16px; }
+      .me-id { grid-template-columns: auto minmax(0, 1fr) !important; gap: 2px 10px; }
+      /* 「选我」放回标题那一行右侧(v16 实测:桌面那条 !important 覆盖把手机上的按钮挤到了下一行独立一行) */
+      .me-id > .go { grid-column: 3; grid-row: 1; margin-left: 0; }
+      .me-id .avatar { width: 40px; height: 40px; }
+      .me-nums { grid-column: 1 / -1; justify-self: start; margin-left: 0; width: 100%; gap: 24px; }
+      .me-nums div { display: flex; align-items: baseline; gap: 4px; }
+      /* 五阶段:两行三列(第五格占满第二行左半) */
       .stagebar { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
       .stg { padding: 10px; }
       .stg .sf { white-space: normal; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; line-height: 1.35; }
-      .me-split { grid-template-columns: minmax(0, 1fr); gap: 12px; }
-      .prog li { min-height: 32px; }
-      .stat { gap: 6px; }
+      .stat { gap: 8px; min-height: 0; }
+      .stat .head .lbl { font-size: 14px; }
       .stat b { font-size: 20px; }
-      .stat b .u { font-size: 12px; }
-      .stat .top { min-height: 18px; }
-      .stat .top .go { display: none; }
       /* 手机上一律 14:说明文字在 393 宽下 12px 读不清;只是「文字」不换档,数字仍按档位 */
-      .me-none, .empty { font-size: 14px; }
-      .btn-key { display: none; }
-      /* F:两行 —— 标题占满第一行,作者 / 时间在第二行两端 */
-      .feed li { grid-template-columns: minmax(0, 1fr) auto; row-gap: 2px; }
-      .feed li a { grid-column: 1 / -1; white-space: normal; }
+      .me-none, .empty, .row { font-size: 14px; }
+      .row { min-height: 44px; }
+      .feed li { grid-template-columns: 12px minmax(0, 1fr); }
+      .feed li a { grid-column: 2; white-space: normal; }
+      .feed .tm { grid-column: 2; grid-row: 3; }
       .go, .hd .go, .me-id .go { min-height: 44px; min-width: 44px; justify-content: center; }
-      .feed-more { min-height: 44px; align-items: center; }
-      /* 底行:手机上卡片一列(与卡池同一张卡,可整张点开抽屉),时间线在下面 */
       .bento > .row.cards-row { grid-template-columns: minmax(0, 1fr); gap: var(--kf-space-card); }
       .cards-main, .side-stack { grid-column: auto; }
       .cards { grid-template-columns: minmax(0, 1fr); gap: var(--kf-space-card); }
       .cards-main .feed-head { margin-bottom: 8px; }
-      .feed .tm { position: static; grid-column: 2; justify-self: end; grid-row: 1; }
-      .pending li { min-height: 44px; }
       .empty { font-size: 14px; }
       .foot-links { display: flex; gap: 8px; margin-top: var(--kf-space-card); }
-      .foot-links a { display: inline-flex; align-items: center; gap: 6px; min-height: 44px; padding: 0 14px; border-radius: var(--r-md); font-size: 14px; font-weight: 600;
+      .foot-links a { display: inline-flex; align-items: center; gap: 6px; min-height: 44px; padding: 0 14px; border-radius: var(--r-md); font-size: 14px; font-weight: 500;
         color: var(--kf-accent-ink); text-decoration: none; background: var(--kf-surface); border: 1px solid var(--kf-line-soft); }
     }
   `];
@@ -267,13 +271,11 @@ export class ViewWorkbench extends LitElement {
     const me = forMe(db, this.me);
     const pool = poolOverview(db);
     const live = liveBoard(db);
-    /* 左卡片:我的待评分卡在前,再补最近上传,最多 3 张;右侧时间线取最近 6 条 */
+    /* 左卡片:我的待评分卡在前,再补最近上传,最多 2 张;右侧时间线取最近 5 条 */
     const latest = sortByNewest(pool.sorted);
     const seen = new Set();
     const play = [...(me.pendingScores || []), ...latest].filter((o) => (seen.has(o.id) ? false : seen.add(o.id))).slice(0, 2);
     const recent = latest.slice(0, 5);
-    /* C 卡「我的环节」:我最近一条成果 */
-    this.#mineLatest = me.mine?.[0] || null;
     return html`
       ${head('我的推进', '五阶段进度与现场状态', html`<a class="go" href=${href('/pool')} data-link>机会卡池 →</a>`)}
       <div class="bento">
@@ -282,10 +284,11 @@ export class ViewWorkbench extends LitElement {
           <kf-glass-card data-role="b" dark fill-to>${this.#live(live, pool)}</kf-glass-card>
         </div>
 
-        <div class="row cde kf-enter" style="--i:1">
-          <kf-glass-card data-role="c" fill-to>${this.#stat('open', pool)}</kf-glass-card>
-          <kf-glass-card data-role="d" fill-to>${this.#stat('top', pool)}</kf-glass-card>
-          <kf-glass-card data-role="e" fill-to>${this.#stat('decided', pool)}</kf-glass-card>
+        <div class="row stats kf-enter" style="--i:1">
+          <kf-glass-card data-role="c" fill-to>${this.#stat('total', pool, live)}</kf-glass-card>
+          <kf-glass-card data-role="d" fill-to>${this.#stat('avg', pool)}</kf-glass-card>
+          <kf-glass-card data-role="e" fill-to>${this.#stat('submitted', pool, live)}</kf-glass-card>
+          <kf-glass-card data-role="f" fill-to>${this.#stat('decided', pool)}</kf-glass-card>
         </div>
 
         <div class="row cards-row kf-enter" style="--i:2">
@@ -295,7 +298,7 @@ export class ViewWorkbench extends LitElement {
             ${play.length ? html`<div class="cards">${play.map((o) => html`<kf-glass-card lift fill-to @card-activate=${() => this.#openCard(o.id)}>${playCard({ opp: o, status: o.status, onStatus: (st) => this.#setStatus({ id: o.id, status: st }) })}</kf-glass-card>`)}</div>`
               : html`<p class="feed-empty">还没有机会卡</p>`}
           </div>
-          <kf-glass-card class="side-stack" data-role="f" fill-to>
+          <kf-glass-card class="side-stack" data-role="g" fill-to>
             <div class="side-in">
               <div class="feed-head"><span class="lbl">${t('最近动过')}</span>
                 <a class="go" href=${href('/pool')} data-link aria-label="全部机会卡">全部 →</a></div>
@@ -304,7 +307,7 @@ export class ViewWorkbench extends LitElement {
           </kf-glass-card>
         </div>
       </div>
-      ${cardFooter({ shown: play.length, all: pool.total, filtered: false, label: '张机会卡,最近 3 张列在上方', onTop: () => window.scrollTo({ top: 0, behavior: 'smooth' }) })}
+      ${cardFooter({ shown: play.length, all: pool.total, filtered: false, label: t('张机会卡,最近 2 张列在上方'), onTop: () => window.scrollTo({ top: 0, behavior: 'smooth' }) })}
       <!-- 只有手机显示:证据台账从底部 Tab 移到工作台页脚(真实链接,部署前缀同源) -->
       <nav class="foot-links" aria-label="更多入口">
         <a href=${href('/ledger')} data-link>${icon('ledger', 16)} 证据台账</a>
@@ -312,7 +315,6 @@ export class ViewWorkbench extends LitElement {
     `;
   }
 
-  /** A 卡:选了「我是」→ 我的进度;没选 → 全员进度(v12 定稿:空态也放真实内容,不摆空框) */
   /** 打开机会卡全貌:交给 app 的抽屉(机会卡详情 = 决策 / 多人评分 / 来源 / 关联报告),与卡池点开是同一个 */
   #openCard(id) {
     this.dispatchEvent(new CustomEvent('opportunity-open', { detail: { id }, bubbles: true, composed: true }));
@@ -323,96 +325,95 @@ export class ViewWorkbench extends LitElement {
     this.dispatchEvent(new CustomEvent('status-change', { detail: { id, status }, bubbles: true, composed: true }));
   }
 
+  #stagebar(rows, maxSub, picked) {
+    return html`<div class="stagebar" data-allstages>
+      ${rows.map((r, i) => {
+        const ok = picked ? picked.has(r.stage.code) : true;
+        return html`<div class="stg ${TONE[r.stage.code]} ${ok ? '' : 'todo'}" style=${`--i:${i + 1}`} data-stage=${r.stage.code}>
+          <span class="b ${TONE[r.stage.code]}">${r.stage.code}</span>
+          <span class="n">${r.submitted || '—'}</span>
+          <span class="sf">${r.stage.name}</span>
+          <span class="f" aria-hidden="true"><i style=${`--v:${(r.submitted / maxSub).toFixed(2)}`}></i></span></div>`;
+      })}
+    </div>`;
+  }
+
+  /** A 卡:选了「我是」→ 我的进度;没选 → 全员进度(v12 定稿:空态也放真实内容,不摆空框) */
   #mine(me, pool, live) {
+    this.#pending = me?.pendingScores || [];
+    this.#zero = Boolean(this.me) && me.count === 0;
     if (!this.me) {
       const maxSub = Math.max(1, ...live.stages.map((r) => r.submitted));
       return html`<div class="me" data-empty="no-me">
-        <div class="me-id" style="grid-template-columns:auto minmax(0,1fr) auto"><div class="avatar q">?</div>
-          <div class="me-title"><h3>还没选「我是」</h3><span class="sub">先看全员:${live.memberCount} 人 · 已交 ${live.submittedPeople} 人</span></div>
+        <div class="me-id"><div class="avatar q">?</div>
+          <div class="me-title"><h3>还没选「我是」</h3><span class="sub">${t(`先看全员:${live.memberCount} 人 · 已交 ${live.submittedPeople} 人`)}</span></div>
           <a class="go" href=${href('/members')} data-link>${icon('user', 14)} ${t('选我')}</a></div>
-        <div class="stagebar" data-allstages>
-          ${live.stages.map((r, i) => html`<div class="stg ${TONE[r.stage.code]}" style=${`--i:${i + 1}`} data-stage=${r.stage.code}>
-            <span class="b ${TONE[r.stage.code]}">${r.stage.code}</span>
-            <span class="n">${r.submitted || '—'}</span>
-            <span class="sf">${r.stage.name}</span>
-            <span class="f" aria-hidden="true"><i style=${`--v:${(r.submitted / maxSub).toFixed(2)}`}></i></span></div>`)}
-        </div>
-        <div class="empty"><span>已交 ${live.submittedPeople} 人 · 选了自己,这里换成你的阶段进度与待办</span></div>
+        ${this.#stagebar(live.stages, maxSub, null)}
+        ${this.#meList(pool)}
       </div>`;
     }
     const done = new Set(me.stagesDone);
-    const max = Math.max(1, ...STAGES.map((s) => me.mine.filter((a) => a.stageObj?.code === s.code).length));
-    const pending = me.pendingScores || [];
-    /* 队友动态:没卡住、但一段时间没动静的人(按空闲时长),3 人以内 */
-    const idle = (this.db?.members || []).filter((m) => !m.stuck && m.lastActiveAt)
-      .sort((a2, b2) => (b2.idleHours || 0) - (a2.idleHours || 0)).slice(0, 3);
+    const rows = STAGES.map((s) => ({ stage: s, submitted: me.mine.filter((a) => a.stageObj?.code === s.code).length }));
+    const maxSub = Math.max(1, ...rows.map((r) => r.submitted));
     return html`<div class="me" data-empty=${me.count === 0 ? 'no-result' : 'none'}>
       <div class="me-id">
         <div class="avatar">${Array.from(me.me).slice(-1).join('')}</div>
-        <div class="me-title"><h3>${me.me}</h3><span class="sub">已完成 ${me.stagesDone.length} / 5 个阶段 · 累计 ${me.count} 份成果</span></div>
+        <div class="me-title"><h3>${me.me}</h3><span class="sub">${t(`已完成 ${me.stagesDone.length} / 5 个阶段 · 累计 ${me.count} 份成果`)}</span></div>
         <div class="me-nums">
           <div><b>${me.count}</b><span>份成果</span></div>
           <div><b>${me.opps.length}</b><span>张卡</span></div>
-          <div><b>${pending.length}</b><span>待评分</span></div>
-        </div>
-      </div>
-      <div class="stagebar">
-        ${STAGES.map((s, i) => {
-          const n = me.mine.filter((a) => a.stageObj?.code === s.code).length;
-          const ok = done.has(s.code);
-          return html`<div class="stg kf-enter ${TONE[s.code]} ${ok ? '' : 'todo'}" style=${`--i:${i + 1}`} data-stage=${s.code}>
-            <span class="b ${TONE[s.code]}">${s.code}</span>
-            <span class="n">${n || '—'}</span>
-            <span class="sf">${s.name}</span>
-            <span class="f" aria-hidden="true"><i style=${`--v:${(n / max).toFixed(2)}`}></i></span></div>`;
-        })}
-      </div>
-      ${me.count === 0 ? html`<div class="empty"><span>本月还没有你的成果</span>
-        <a class="go" href=${href('/stages')} data-link>看五阶段产出 →</a></div>` : ''}
-      <div class="me-split">
-        <div>
-          <div class="lbl">卡池进度</div>
-          ${this.#progress(pool)}
-        </div>
-        <div>
-          <div class="lbl">待评分${pending.length ? ` ${pending.length}` : ''}</div>
-          <p class="me-note">候选卡里还没有你打分的卡,按最近上传排</p>
-          ${pending.length ? html`<ul class="pending">${pending.slice(0, 4).map((o) => html`<li>
-            <a href=${href(`/pool/${o.id}`)} data-link>${o.title}</a>
-            <span class="d">${o.stats.n ? `${o.stats.n} 人 · 均 ${o.stats.avg != null ? Math.round(o.stats.avg) : '—'}` : '尚未评分'}</span>
-            <a class="s" href=${href(`/pool/${o.id}`)} data-link aria-label="去评分">${o.stats.avg != null ? Math.round(o.stats.avg) : '去评'}</a></li>`)}</ul>
-            ${pending.length > 4 ? html`<a class="feed-more go" href=${href('/pool')} data-link>还有 ${pending.length - 4} 张待评分 →</a>` : ''}`
-            : html`<p class="me-none">没有等你评分的卡</p>`}
-          ${idle.length ? html`<div class="lbl" style="margin-top:14px">${t('队友动态')}</div>
-            <ul class="pending">${idle.map((m) => html`<li><span class="nm">${m.name}</span>
-              <span class="d">${m.stageObj ? `${m.stageObj.code} · ${m.stageObj.name}` : t('未开工')}</span>
-              <span class="s dim">${m.idleHours == null ? '—' : m.idleHours + 'h'}</span></li>`)}</ul>` : ''}
+          <div><b>${me.pendingScores.length}</b><span>待评分</span></div>
         </div>
       </div>
+      ${this.#stagebar(rows, maxSub, done)}
+
+      ${this.#meList(pool)}
     </div>`;
   }
 
-  /** 卡池进度:候选 / 立项 / 否决 三段 + 百分比(状态用色点,不用整块色底) */
-  #progress(pool) {
-    const s = pool.byStatus;
+  /**
+   * A 卡下部:一条通栏列表(v16 去掉左右两栏的 32px 中缝)。
+   *   没选「我是」→ 卡池进度(候选 / 已立项 / 已否决各一行,两端对齐)
+   *   选了 → 待评分(卡名吃掉剩余宽度 + 右侧分数胶囊)—— dr1 的「左标题 + 右值」
+   */
+  #meList(pool) {
+    const st = pool.byStatus;
     const total = Math.max(1, pool.total);
-    const rows = [['候选', s.candidate, '#8A8A93'], ['已立项', s.approved, '#2F7D5B'], ['已否决', s.rejected, '#A1A1AA']];
-    return html`<ul class="prog">${rows.map(([k, v, c]) => html`<li>
-      <i class="dot" style=${`background:${c}`}></i><span class="pk">${k}</span>
-      <span class="pv"><b>${v}</b> 张</span><span class="pp">${Math.round((v / total) * 100)}%</span></li>`)}</ul>`;
+    const pending = this.#pending;
+    if (!this.me) {
+      return html`<div class="me-list"><div class="sec">
+        <div class="lbl">卡池进度</div>
+        ${[['候选', st.candidate], ['已立项', st.approved], ['已否决', st.rejected]].map(([k, v]) => html`<div class="row">
+          <i class="dot" style=${`background:${DOT[k === '已立项' ? 'approved' : k === '已否决' ? 'rejected' : 'candidate']};border:1px solid rgba(0,0,0,.14)`}></i>
+          <span class="t">${k}</span>
+          <span class="rate"><span class="bar"><i style=${`width:${Math.round((v / total) * 100)}%`}></i></span>
+            <span class="v"><b>${v}</b> 张 · ${Math.round((v / total) * 100)}%</span></span></div>`)}
+      </div></div>`;
+    }
+    return html`<div class="me-list">
+      <div class="sec">
+        <div class="lbl">待评分${pending.length ? ` ${pending.length}` : ''}</div>
+        ${pending.length ? pending.slice(0, 4).map((o) => html`<div class="row">
+          <a class="t" href=${href(`/pool/${o.id}`)} data-link title=${o.title}>${t(o.title)}</a>
+          <span class="meta">${o.stats.n ? t(`${o.stats.n} 人 · 均 ${o.stats.avg != null ? Math.round(o.stats.avg) : '—'}`) : '尚未评分'}</span>
+          <a class="pill" href=${href(`/pool/${o.id}`)} data-link aria-label="去评分">${o.stats.avg != null ? Math.round(o.stats.avg) : '去评'}</a></div>`)
+          : html`<div class="me-none">没有等你评分的卡</div>`}
+        ${pending.length > 4 ? html`<a class="go" style="margin-top:8px" href=${href('/pool')} data-link>还有 ${pending.length - 4} 张待评分 →</a>` : ''}
+      </div>
+    </div>`;
   }
 
-  /** B 卡:现场。深色底 + 反白字;有卡住的人 → 橙点 + 人数 + 名单;没人卡住 → 绿点 + 正向文案。 */
+  /** B 卡:现场。深色底 + 反白字;唯一强调色 = 琥珀(卡住);阶段进度是灰阶细条 */
   #live(live, pool) {
     const stalled = live.stalled;
     return html`<div class="live" data-stalled-count=${stalled.length}>
       <div class="live-head"><span class="lbl">${stalled.length
-        ? html`<i class="dot" style="background:#F59E0B"></i>现场`
-        : html`<i class="dot" style="background:#4ADE80"></i>现场`}</span>
+        ? html`<i class="dot" style="background:var(--kf-s4)"></i>现场`
+        : html`<i class="dot" style="background:var(--live-ink-3)"></i>现场`}</span>
         <a class="go" href=${href('/pool')} data-link aria-label="去机会卡池">${icon('arrow', 14)}</a></div>
       ${stalled.length ? html`
         <div class="live-num"><b>${stalled.length}</b><span>人卡住</span></div>
-        <ul class="stalled">${stalled.slice(0, 4).map((s) => html`<li data-stalled=${s.name}>
+        <ul class="stalled">${stalled.slice(0, 3).map((s) => html`<li data-stalled=${s.name}>
           <span class="nm">${s.name}</span><span class="meta">${t(`${s.stage} · ${s.msgCount} 条`)}</span>
           <span class="h">${s.stuckHours}h</span></li>`)}</ul>`
       : html`
@@ -420,9 +421,8 @@ export class ViewWorkbench extends LitElement {
         <p class="live-ok">${icon('check', 16)} 没有超过 8 小时没交成果的人</p>`}
       <div class="live-rank">
         <div class="lbl">各阶段已交</div>
-        ${live.stages.map((r, i) => html`<div class="rk">
-          <i class="dot" style=${`background:var(--kf-${['s1', 's2', 's3', 's4', 's5'][i]})`}></i>
-          <span class="rk-k">${r.stage.code} ${r.stage.name}</span>
+        ${live.stages.map((r) => html`<div class="rk">
+          <span class="rk-k">${r.stage.code} ${r.stage.name}</span>
           <span class="rk-bar"><i style=${`width:${Math.round((r.submitted / Math.max(1, live.memberCount)) * 100)}%`}></i></span>
           <span class="rk-n">${r.submitted}/${live.memberCount}</span></div>`)}
       </div>
@@ -430,36 +430,59 @@ export class ViewWorkbench extends LitElement {
     </div>`;
   }
 
-  /** C/D/E:标签 → 大数字 → 一行说明 */
-  #stat(kind, pool) {
-    if (kind === 'open') {
+  /**
+   * 短数据卡:标题 16/600 · 大数字 28/500 · 形状 · 结论行 12(dr1 的四卡结构)。
+   * 结论行必须有信息量(和什么比 / 什么占比),不写形容词;没有可用数据时就只写事实。
+   */
+  #stat(kind, pool, live) {
+    /* 选了人、但本月没成果:把「最高分」换成本人的待办(空态也要有信息,不摆空框 —— v12 的原则) */
+    if (kind === 'avg' && this.#zero) return html`<div class="stat" data-kpi="mine">
+      <div class="head"><span class="lbl">本月成果</span><a class="go" href=${href('/stages')} data-link>五阶段 →</a></div>
+      <div class="num"><b>0</b><span class="u">份</span></div>
+      <span class="note"><span class="t">还没交成果,先去五阶段补一份</span></span></div>`;
+    if (kind === 'total') {
       return html`<div class="stat" data-kpi="total">
-        <div class="top"><span class="lbl">卡池</span><a class="btn-key" href=${href('/pool')} data-link>全部 ${icon('arrow', 14)}</a></div>
-        <b>${pool.total}<span class="u">张</span></b>
-        <span class="note">${pool.uploaders} 人提交 · ${pool.agents} 个 Agent</span></div>`;
+        <div class="head"><span class="lbl">卡池</span><a class="go" href=${href('/pool')} data-link>全部 →</a></div>
+        <div class="num"><b>${pool.total}</b><span class="u">张</span></div>
+        <span class="note"><span class="t">${t(`${pool.uploaders} 人提交 · 候选 ${pool.byStatus.candidate || 0} · 已立项 ${pool.byStatus.approved || 0}`)}</span></span></div>`;
     }
-    if (kind === 'top') {
+    if (kind === 'avg') {
       const top = pool.top;
       const avg = top?.stats?.avg;
+      const scored = pool.sorted.filter((o) => o.stats.avg != null);
+      const mean = scored.length ? Math.round(scored.reduce((a, o) => a + o.stats.avg, 0) / scored.length) : null;
+      const avgDelta = avg != null && mean != null ? { gap: Math.abs(Math.round(avg) - mean), mean, up: Math.round(avg) >= mean } : null;
       return html`<div class="stat" data-kpi="avg">
-        <div class="top"><span class="lbl">最高分</span></div>
-        <b class=${avg == null ? 'dim' : ''}>${avg != null ? Math.round(avg) : '—'}<span class="u">分</span></b>
-        <span class="note" title=${top?.title || ''}>${top?.title || '暂无评分'}</span></div>`;
+        <div class="head"><span class="lbl">最高分</span></div>
+        <div class="num"><b class=${avg == null ? 'dim' : ''}>${avg != null ? Math.round(avg) : '—'}</b>${avg != null ? html`<span class="u">分</span>` : ''}</div>
+        <span class="note">${avgDelta ? html`<span class="delta ${avgDelta.up ? '' : 'zero'}">${avgDelta.up ? '↑' : '↓'}${avgDelta.gap}</span>` : ''}
+          <span class="t">${avgDelta ? t(`${avgDelta.up ? '高于' : '低于'}卡池均分 ${avgDelta.mean} · 最高一张`) : '还没有人评分'}</span></span></div>`;
+    }
+    if (kind === 'submitted') {
+      /* 有数据画形状(各阶段已交数),没数据不画 —— 空态照样测得到 */
+      const bars = live?.stages?.map((r) => r.artifacts) || [];
+      const lead = live?.stages?.reduce((a, r) => (r.artifacts > (a?.artifacts ?? -1) ? r : a), null);
+      return html`<div class="stat" data-kpi="submitted">
+        <div class="head"><span class="lbl">已交成果</span></div>
+        <div class="num"><b>${live?.totalSubmitted ?? bars.reduce((a, b) => a + b, 0)}</b><span class="u">份</span></div>
+        <span class="note"><span class="t">${t(`${live?.submittedPeople ?? 0} 人交了 · 共 ${live?.memberCount ?? 0} 人${lead?.stage ? ` · 最多是 ${lead.stage.code} ${lead.stage.name}(${lead.submitted} 份)` : ''}`)}</span></span></div>`;
     }
-    /* C:我的环节 —— 最近一条成果落在哪个阶段(没选 / 没成果时如实说) */
-    const last = this.#mineLatest;
-    return html`<div class="stat" data-kpi="stage">
-      <div class="top"><span class="lbl">我的环节</span></div>
-      <b class=${last ? '' : 'dim'}>${last ? last.stageObj.code : '—'}</b>
-      <span class="note">${last ? `${last.stageObj.name} · ${ago(last.createdAt)}` : this.me ? '本月还没有你的成果' : '还没选「我是」'}</span></div>`;
+    /* deciding:还在候选里、没决策的张数(和 stat('total') 互补;替换掉内部视角的「我的环节」) */
+    const n = pool.byStatus.candidate || 0;
+    const done = (pool.byStatus.approved || 0) + (pool.byStatus.rejected || 0);
+    return html`<div class="stat" data-kpi="deciding">
+      <div class="head"><span class="lbl">待决策</span></div>
+      <div class="num"><b class=${n ? '' : 'dim'}>${n}</b><span class="u">张</span></div>
+      <span class="note"><span class="t">${done ? t(`${done} 张已立项 / 否决 · 候选还剩 ${n} 张`) : '还没有已决策的卡'}</span></span></div>`;
   }
 
-  /** 右侧「最近动过」:时间线(6 条) */
+  /** 右侧「最近动过」:时间线(5 条) */
   #feed(latest, pool) {
     return html`<div data-feed>
-      ${latest.length ? html`<ul class="feed">${latest.map((o) => html`<li>
+      ${latest.length ? html`<ul class="feed">${latest.map((o) => html`<li data-status=${o.status}>
+        <i class="fd" style=${`background:${DOT[o.status] || DOT.candidate}`} aria-hidden="true"></i>
         <a href=${href(`/pool/${o.id}`)} data-link>${o.title}</a>
-        <span class="st">${(o.upload?.uploadedBy || '') + (o.stageObj ? ' · ' + o.stageObj.name : '')}</span>
+        <span class="st">${t((o.upload?.uploadedBy || '') + (o.stageObj ? ' · ' + o.stageObj.name : ''))}</span>
         <span class="tm">${ago(o.upload?.uploadedAt || o.createdAt)}</span></li>`)}</ul>
         <a class="feed-more go" href=${href('/pool')} data-link>看全部 ${pool.total} 张机会卡 →</a>`
       : html`<p class="feed-empty">还没有机会卡</p>`}