@sleep2agi/agent-network-dashboard 0.5.1-preview.11 → 0.5.1-preview.111

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (257) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +3 -3
  3. package/.next/diagnostics/route-bundle-stats.json +6 -6
  4. package/.next/fallback-build-manifest.json +3 -3
  5. package/.next/prerender-manifest.json +3 -3
  6. package/.next/server/app/_global-error.html +1 -1
  7. package/.next/server/app/_global-error.rsc +1 -1
  8. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  9. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  12. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  14. package/.next/server/app/_not-found.html +2 -2
  15. package/.next/server/app/_not-found.rsc +2 -2
  16. package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  17. package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  18. package/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  19. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  20. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  21. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  22. package/.next/server/app/admin/page_client-reference-manifest.js +1 -1
  23. package/.next/server/app/admin.html +2 -2
  24. package/.next/server/app/admin.rsc +2 -2
  25. package/.next/server/app/admin.segments/_full.segment.rsc +2 -2
  26. package/.next/server/app/admin.segments/_head.segment.rsc +1 -1
  27. package/.next/server/app/admin.segments/_index.segment.rsc +2 -2
  28. package/.next/server/app/admin.segments/_tree.segment.rsc +2 -2
  29. package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +1 -1
  30. package/.next/server/app/admin.segments/admin.segment.rsc +1 -1
  31. package/.next/server/app/index.html +2 -2
  32. package/.next/server/app/index.rsc +3 -3
  33. package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  34. package/.next/server/app/index.segments/_full.segment.rsc +3 -3
  35. package/.next/server/app/index.segments/_head.segment.rsc +1 -1
  36. package/.next/server/app/index.segments/_index.segment.rsc +2 -2
  37. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  38. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  39. package/.next/server/app/login.html +2 -2
  40. package/.next/server/app/login.rsc +3 -3
  41. package/.next/server/app/login.segments/_full.segment.rsc +3 -3
  42. package/.next/server/app/login.segments/_head.segment.rsc +1 -1
  43. package/.next/server/app/login.segments/_index.segment.rsc +2 -2
  44. package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
  45. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +2 -2
  46. package/.next/server/app/login.segments/login.segment.rsc +1 -1
  47. package/.next/server/app/logs/page_client-reference-manifest.js +1 -1
  48. package/.next/server/app/logs.html +2 -2
  49. package/.next/server/app/logs.rsc +2 -2
  50. package/.next/server/app/logs.segments/_full.segment.rsc +2 -2
  51. package/.next/server/app/logs.segments/_head.segment.rsc +1 -1
  52. package/.next/server/app/logs.segments/_index.segment.rsc +2 -2
  53. package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
  54. package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +1 -1
  55. package/.next/server/app/logs.segments/logs.segment.rsc +1 -1
  56. package/.next/server/app/messages/page_client-reference-manifest.js +1 -1
  57. package/.next/server/app/messages.html +2 -2
  58. package/.next/server/app/messages.rsc +2 -2
  59. package/.next/server/app/messages.segments/_full.segment.rsc +2 -2
  60. package/.next/server/app/messages.segments/_head.segment.rsc +1 -1
  61. package/.next/server/app/messages.segments/_index.segment.rsc +2 -2
  62. package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
  63. package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +1 -1
  64. package/.next/server/app/messages.segments/messages.segment.rsc +1 -1
  65. package/.next/server/app/node/page_client-reference-manifest.js +1 -1
  66. package/.next/server/app/node.html +2 -2
  67. package/.next/server/app/node.rsc +2 -2
  68. package/.next/server/app/node.segments/_full.segment.rsc +2 -2
  69. package/.next/server/app/node.segments/_head.segment.rsc +1 -1
  70. package/.next/server/app/node.segments/_index.segment.rsc +2 -2
  71. package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
  72. package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +1 -1
  73. package/.next/server/app/node.segments/node.segment.rsc +1 -1
  74. package/.next/server/app/nodes/page_client-reference-manifest.js +1 -1
  75. package/.next/server/app/nodes.html +2 -2
  76. package/.next/server/app/nodes.rsc +2 -2
  77. package/.next/server/app/nodes.segments/_full.segment.rsc +2 -2
  78. package/.next/server/app/nodes.segments/_head.segment.rsc +1 -1
  79. package/.next/server/app/nodes.segments/_index.segment.rsc +2 -2
  80. package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
  81. package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +1 -1
  82. package/.next/server/app/nodes.segments/nodes.segment.rsc +1 -1
  83. package/.next/server/app/page_client-reference-manifest.js +1 -1
  84. package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
  85. package/.next/server/app/server-logs.html +2 -2
  86. package/.next/server/app/server-logs.rsc +2 -2
  87. package/.next/server/app/server-logs.segments/_full.segment.rsc +2 -2
  88. package/.next/server/app/server-logs.segments/_head.segment.rsc +1 -1
  89. package/.next/server/app/server-logs.segments/_index.segment.rsc +2 -2
  90. package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
  91. package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +1 -1
  92. package/.next/server/app/server-logs.segments/server-logs.segment.rsc +1 -1
  93. package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
  94. package/.next/server/app/settings/networks.html +2 -2
  95. package/.next/server/app/settings/networks.rsc +2 -2
  96. package/.next/server/app/settings/networks.segments/_full.segment.rsc +2 -2
  97. package/.next/server/app/settings/networks.segments/_head.segment.rsc +1 -1
  98. package/.next/server/app/settings/networks.segments/_index.segment.rsc +2 -2
  99. package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
  100. package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +1 -1
  101. package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +1 -1
  102. package/.next/server/app/settings/networks.segments/settings.segment.rsc +1 -1
  103. package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  104. package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
  105. package/.next/server/app/settings/tokens.html +2 -2
  106. package/.next/server/app/settings/tokens.rsc +2 -2
  107. package/.next/server/app/settings/tokens.segments/_full.segment.rsc +2 -2
  108. package/.next/server/app/settings/tokens.segments/_head.segment.rsc +1 -1
  109. package/.next/server/app/settings/tokens.segments/_index.segment.rsc +2 -2
  110. package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
  111. package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +1 -1
  112. package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +1 -1
  113. package/.next/server/app/settings/tokens.segments/settings.segment.rsc +1 -1
  114. package/.next/server/app/settings.html +2 -2
  115. package/.next/server/app/settings.rsc +3 -3
  116. package/.next/server/app/settings.segments/_full.segment.rsc +3 -3
  117. package/.next/server/app/settings.segments/_head.segment.rsc +1 -1
  118. package/.next/server/app/settings.segments/_index.segment.rsc +2 -2
  119. package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  120. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +2 -2
  121. package/.next/server/app/settings.segments/settings.segment.rsc +1 -1
  122. package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
  123. package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
  124. package/.next/server/app/tasks.html +2 -2
  125. package/.next/server/app/tasks.rsc +2 -2
  126. package/.next/server/app/tasks.segments/_full.segment.rsc +2 -2
  127. package/.next/server/app/tasks.segments/_head.segment.rsc +1 -1
  128. package/.next/server/app/tasks.segments/_index.segment.rsc +2 -2
  129. package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
  130. package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +1 -1
  131. package/.next/server/app/tasks.segments/tasks.segment.rsc +1 -1
  132. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js +3 -3
  133. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js.map +1 -1
  134. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js +1 -1
  135. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js.map +1 -1
  136. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js +1 -1
  137. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js.map +1 -1
  138. package/.next/server/middleware-build-manifest.js +3 -3
  139. package/.next/server/pages/404.html +2 -2
  140. package/.next/server/pages/500.html +1 -1
  141. package/.next/server/server-reference-manifest.js +1 -1
  142. package/.next/server/server-reference-manifest.json +1 -1
  143. package/.next/static/chunks/017hq2-5l~_98.css +2 -0
  144. package/.next/static/chunks/059.x2_u04fls.js +1 -0
  145. package/.next/static/chunks/0xvpfbdq_1iwg.js +1 -0
  146. package/.next/static/chunks/152~t8cc8lw5k.js +4 -0
  147. package/.next/trace +2 -2
  148. package/.next/trace-build +1 -1
  149. package/app/components/TopoGraph.tsx +2277 -162
  150. package/package.json +1 -1
  151. package/scripts/topo-active-links-chip-hover-lift-test.mjs +93 -0
  152. package/scripts/topo-chip-digit-fontweight-test.mjs +105 -0
  153. package/scripts/topo-chip-digit-hover-bold-test.mjs +94 -0
  154. package/scripts/topo-chip-row-group-hover-brighten-test.mjs +107 -0
  155. package/scripts/topo-chip-row-hover-lift-test.mjs +95 -0
  156. package/scripts/topo-chrome-button-hover-lift-test.mjs +94 -0
  157. package/scripts/topo-chrome-segmented-radius-test.mjs +100 -0
  158. package/scripts/topo-click-ripple-opacity-test.mjs +99 -0
  159. package/scripts/topo-edge-badge-digit-fw-test.mjs +103 -0
  160. package/scripts/topo-edge-badge-fontsize-test.mjs +90 -0
  161. package/scripts/topo-edge-badge-hover-opacity-test.mjs +94 -0
  162. package/scripts/topo-edge-badge-hover-stroke-test.mjs +92 -0
  163. package/scripts/topo-edge-badge-letter-spacing-test.mjs +86 -0
  164. package/scripts/topo-edge-badge-opacity-test.mjs +80 -0
  165. package/scripts/topo-edge-badge-pin-opacity-test.mjs +86 -0
  166. package/scripts/topo-edge-badge-stroke-test.mjs +92 -0
  167. package/scripts/topo-edge-endpoint-hover-sw-test.mjs +127 -0
  168. package/scripts/topo-edge-freshness-floor-test.mjs +99 -0
  169. package/scripts/topo-edge-particle-hover-r-test.mjs +113 -0
  170. package/scripts/topo-edge-particle-radius-test.mjs +76 -0
  171. package/scripts/topo-edge-visible-linecap-test.mjs +89 -0
  172. package/scripts/topo-endpoint-ring-r-hover-test.mjs +89 -0
  173. package/scripts/topo-filter-pill-hover-lift-test.mjs +101 -0
  174. package/scripts/topo-filter-pill-hover-opacity-test.mjs +110 -0
  175. package/scripts/topo-filter-pill-value-fw-test.mjs +88 -0
  176. package/scripts/topo-filter-pill-x-hover-scale-test.mjs +99 -0
  177. package/scripts/topo-flow-rail-hover-sw-test.mjs +113 -0
  178. package/scripts/topo-flow-rail-linecap-test.mjs +79 -0
  179. package/scripts/topo-freshness-chip-hierarchy-test.mjs +93 -0
  180. package/scripts/topo-freshness-chip-tabular-test.mjs +41 -0
  181. package/scripts/topo-freshness-floor-lift-test.mjs +92 -0
  182. package/scripts/topo-freshness-suffix-tabular-test.mjs +88 -0
  183. package/scripts/topo-fullscreen-icon-hover-scale-test.mjs +91 -0
  184. package/scripts/topo-group-box-stroke-test.mjs +105 -0
  185. package/scripts/topo-group-label-count-fontweight-test.mjs +108 -0
  186. package/scripts/topo-group-label-count-fw-test.mjs +100 -0
  187. package/scripts/topo-group-label-letter-spacing-test.mjs +113 -0
  188. package/scripts/topo-hover-detail-body-fw-test.mjs +101 -0
  189. package/scripts/topo-hover-detail-model-fw-test.mjs +98 -0
  190. package/scripts/topo-hover-detail-opacity-test.mjs +98 -0
  191. package/scripts/topo-hover-detail-rx-test.mjs +81 -0
  192. package/scripts/topo-hub-core-fill-hover-test.mjs +85 -0
  193. package/scripts/topo-hub-digit-fontsize-test.mjs +86 -0
  194. package/scripts/topo-hub-digit-fw-hover-test.mjs +102 -0
  195. package/scripts/topo-hub-halo-light-trough-test.mjs +88 -0
  196. package/scripts/topo-hub-halo-radius-test.mjs +86 -0
  197. package/scripts/topo-hub-halo-trough-test.mjs +83 -0
  198. package/scripts/topo-hub-highlight-opacity-test.mjs +88 -0
  199. package/scripts/topo-hub-highlight-radius-test.mjs +90 -0
  200. package/scripts/topo-hub-hover-ring-opacity-test.mjs +96 -0
  201. package/scripts/topo-hub-hover-ring-stroke-test.mjs +86 -0
  202. package/scripts/topo-hub-spoke-hover-opacity-test.mjs +119 -0
  203. package/scripts/topo-hub-spoke-hover-sw-test.mjs +121 -0
  204. package/scripts/topo-hub-spoke-linecap-test.mjs +80 -0
  205. package/scripts/topo-label-card-opacity-hover-test.mjs +99 -0
  206. package/scripts/topo-layout-toggle-hover-tracking-test.mjs +109 -0
  207. package/scripts/topo-layout-toggle-radius-test.mjs +87 -0
  208. package/scripts/topo-legend-count-active-opacity-test.mjs +102 -0
  209. package/scripts/topo-legend-count-pin-fw-test.mjs +90 -0
  210. package/scripts/topo-legend-label-fontweight-test.mjs +94 -0
  211. package/scripts/topo-legend-pin-ring-stroke-test.mjs +101 -0
  212. package/scripts/topo-legend-row-letter-spacing-test.mjs +101 -0
  213. package/scripts/topo-minimap-offline-opacity-test.mjs +90 -0
  214. package/scripts/topo-minimap-online-hover-opacity-test.mjs +92 -0
  215. package/scripts/topo-minimap-online-opacity-test.mjs +93 -0
  216. package/scripts/topo-minimap-online-radius-test.mjs +85 -0
  217. package/scripts/topo-minimap-viewport-linejoin-test.mjs +75 -0
  218. package/scripts/topo-minimap-viewport-opacity-test.mjs +96 -0
  219. package/scripts/topo-minimap-viewport-rx-test.mjs +85 -0
  220. package/scripts/topo-more-flows-fontweight-test.mjs +103 -0
  221. package/scripts/topo-node-alias-letter-spacing-test.mjs +112 -0
  222. package/scripts/topo-node-halo-hover-opacity-test.mjs +104 -0
  223. package/scripts/topo-node-halo-offline-opacity-test.mjs +87 -0
  224. package/scripts/topo-node-label-card-rx-test.mjs +85 -0
  225. package/scripts/topo-node-pulse-peak-test.mjs +89 -0
  226. package/scripts/topo-node-pulse-trough-test.mjs +91 -0
  227. package/scripts/topo-node-sub-text-fw-test.mjs +75 -0
  228. package/scripts/topo-node-sub-text-letter-spacing-test.mjs +115 -0
  229. package/scripts/topo-panel-count-fw-hover-test.mjs +105 -0
  230. package/scripts/topo-panel-stroke-hover-test.mjs +110 -0
  231. package/scripts/topo-pressure-bar-height-test.mjs +92 -0
  232. package/scripts/topo-pressure-kicker-fontweight-test.mjs +76 -0
  233. package/scripts/topo-recent-pip-radius-2-test.mjs +72 -0
  234. package/scripts/topo-recent-pip-radius-test.mjs +76 -0
  235. package/scripts/topo-recent-row-content-opacity-test.mjs +81 -0
  236. package/scripts/topo-recent-row-count-pin-fw-test.mjs +106 -0
  237. package/scripts/topo-recent-row-letter-spacing-test.mjs +109 -0
  238. package/scripts/topo-recent-row-pip-hover-r-test.mjs +104 -0
  239. package/scripts/topo-recent-row-text-fontweight-test.mjs +90 -0
  240. package/scripts/topo-runtime-icon-hover-test.mjs +96 -0
  241. package/scripts/topo-spoke-active-opacity-test.mjs +104 -0
  242. package/scripts/topo-spoke-active-stroke-test.mjs +95 -0
  243. package/scripts/topo-spoke-idle-opacity-test.mjs +91 -0
  244. package/scripts/topo-status-ring-hover-sw-test.mjs +105 -0
  245. package/scripts/topo-vendor-chip-hover-lift-test.mjs +87 -0
  246. package/scripts/topo-vendor-glyph-fontweight-test.mjs +102 -0
  247. package/scripts/topo-vendor-letter-hover-scale-test.mjs +129 -0
  248. package/scripts/topo-vendor-suffix-hover-brighten-test.mjs +87 -0
  249. package/scripts/topo-zoom-icon-hover-scale-test.mjs +114 -0
  250. package/scripts/topo-zoom-level-hover-fw-test.mjs +95 -0
  251. package/.next/static/chunks/0aauz~36q5n2a.css +0 -2
  252. package/.next/static/chunks/0qtk58rfhepeo.js +0 -1
  253. package/.next/static/chunks/0s2jh27ig6a.q.js +0 -4
  254. package/.next/static/chunks/17q6k~a.gl31q.js +0 -1
  255. /package/.next/static/{Ac38WRM1JjpuTYJKsfcUB → n0xvBKFL_GMRtCPB1M6VF}/_buildManifest.js +0 -0
  256. /package/.next/static/{Ac38WRM1JjpuTYJKsfcUB → n0xvBKFL_GMRtCPB1M6VF}/_clientMiddlewareManifest.js +0 -0
  257. /package/.next/static/{Ac38WRM1JjpuTYJKsfcUB → n0xvBKFL_GMRtCPB1M6VF}/_ssgManifest.js +0 -0
@@ -194,7 +194,27 @@ function FreshnessChip({ sessions }: { sessions: unknown }) {
194
194
  // tier (R312-R314 family); the amber bg/text/border still does
195
195
  // the warning-state work, the weight just keeps the chip in the
196
196
  // same data-typography ladder as its siblings.
197
- const baseClass = "hidden sm:inline px-2.5 py-1 rounded-md font-mono font-medium border transition-colors duration-300";
197
+ // Round 377 / Loop: FreshnessChip baseClass picks up `tabular-nums`.
198
+ // The chip-row's last untouched chip joins the R224-R357 broader
199
+ // tabular-nums sweep:
200
+ // R224 edge badge digit
201
+ // R225 hub digit / panel header counts / recent row count
202
+ // R232 chip row counts (working / online / active-links)
203
+ // R321 recent row timestamp
204
+ // R322 recent panel hot count
205
+ // R323 filter pin pill counts
206
+ // R333 vendor count suffix
207
+ // R357 active-links freshness suffix wrapper
208
+ // R377 FreshnessChip body (this round)
209
+ // `font-mono` already gives equal-width glyphs but `tabular-nums`
210
+ // is the explicit-invariant the rest of the chip row carries.
211
+ // FreshnessChip body reads `lag · {sec}s` — the {sec} digit grows
212
+ // every second; tabular-nums explicitly locks digit width so the
213
+ // chip stays planted as the seconds counter ticks past 9 → 10 →
214
+ // 99 → 100. R187 transition-colors duration-300 + R275 stale-only
215
+ // render gate + R315 font-medium R313 family alignment all
216
+ // preserved.
217
+ const baseClass = "hidden sm:inline px-2.5 py-1 rounded-md font-mono font-medium tabular-nums border transition-colors duration-300";
198
218
  const colorClass = stale
199
219
  ? "bg-amber-500/10 text-amber-300 border-amber-500/20"
200
220
  : "bg-gray-500/10 text-gray-400 border-gray-500/20";
@@ -237,7 +257,21 @@ function FreshnessChip({ sessions }: { sessions: unknown }) {
237
257
  duration-300 still eases the bg/color flip. Title (hover
238
258
  tooltip) still spells out the full meaning in either
239
259
  state. */}
240
- {stale ? 'lag' : 'live'} · {sec}s
260
+ {/* Round 410 / Loop: FreshnessChip body picks up the chip-
261
+ internal-hierarchy arc. Pre-R410 the body rendered as a
262
+ single text node `lag · {sec}s` with the parent's font-
263
+ medium (fw=500) applied uniformly. R410 splits the digit
264
+ and unit into separate spans so the chip's internal
265
+ typography mirrors the family pattern R333-R341/R362/R369/
266
+ R389 established for the chip row:
267
+ digit (fw=600) data tier
268
+ unit (fw=500 + opacity=0.7) label tier
269
+ The `lag` prefix stays at the chip's baseline (fw=500
270
+ from parent font-medium) — it labels the state, not a
271
+ data value. data-freshness-chip-digit / -unit attrs
272
+ surface the spans for tests. tabular-nums + transition-
273
+ colors + R275 stale-only gate all preserved. */}
274
+ {stale ? 'lag' : 'live'} · <span className="font-semibold" data-freshness-chip-digit>{sec}</span><span className="opacity-70" data-freshness-chip-unit>s</span>
241
275
  </span>
242
276
  );
243
277
  }
@@ -1823,12 +1857,30 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1823
1857
  while honoring R328's wider baseline rhythm. data-topo-
1824
1858
  chrome-layout-trailer attr unchanged — it still marks
1825
1859
  the boundary surface for the gap probe. */}
1860
+ {/* Round 375 / Loop: Layout-toggle wrapper rounded-md → rounded-
1861
+ lg (6 → 8 px). Extends the corner-radius cascade family
1862
+ to the chrome-strip layout-toggle wrapper:
1863
+ R330 canvas wrapper rounded-xl 12 px
1864
+ R331 SVG panels rx=10 10 px
1865
+ R332 minimap container rounded-lg 8 px
1866
+ R375 Layout-toggle wrapper rounded-lg 8 px (this round)
1867
+ Pre-R375 the wrapper at rounded-md (6 px) was the only
1868
+ chrome-strip container still using the smaller corner
1869
+ radius — both R330 outer wrapper and R332 minimap sit at
1870
+ ≥ 8 px, so the Layout toggle's 6 px stood out as a
1871
+ tighter corner against the family. R375 brings it into
1872
+ the rounded-lg tier where the minimap already lives.
1873
+ Pure paint change — overflow-hidden still clips the
1874
+ inner buttons' bg-cyan-500/15 tints; no layout shift.
1875
+ R268 border-color + 200ms transition + R329 mr-0.5 +
1876
+ data-topo-chrome-layout-trailer all preserved. */}
1826
1877
  <div
1827
- className="mr-0.5 inline-flex rounded-md border overflow-hidden"
1878
+ className="mr-0.5 inline-flex rounded-lg border overflow-hidden"
1828
1879
  style={{ borderColor: pal.containerBorder, transition: 'border-color 200ms ease-out' }}
1829
1880
  role="group"
1830
1881
  aria-label="Topology layout"
1831
1882
  data-topo-chrome-layout-trailer
1883
+ data-topo-chrome-layout-radius="rounded-lg"
1832
1884
  >
1833
1885
  <button
1834
1886
  onClick={() => { popChrome('layout-ring'); if (layout !== 'ring') toggleLayout(); }}
@@ -1862,7 +1914,17 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1862
1914
  weight; cyan-400/60 + ring-inset retained. The
1863
1915
  R163/R196 hover/active deeps + R249 chrome-pop
1864
1916
  click feedback continue unchanged. */
1865
- className={`px-2.5 py-1 transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset ${layout === 'ring' ? 'bg-cyan-500/15 text-cyan-300 font-medium hover:bg-cyan-500/20 active:bg-cyan-500/25' : 'text-gray-400 hover:text-cyan-300 hover:bg-cyan-500/5 active:bg-cyan-500/15'} ${chromePopping === 'layout-ring' ? ' anet-chrome-pop' : ''}`}
1917
+ // R351: hover:tracking-wide extends the R344/R345/R347
1918
+ // hover-letter-spacing family to a 4th surface (chrome-
1919
+ // strip Ring/Grid pair). transition-colors className
1920
+ // dropped in favour of an inline transition spec that
1921
+ // bundles bg/color (150ms ease) + letter-spacing
1922
+ // (200ms ease-out) — Tailwind's transition-colors
1923
+ // doesn't list letter-spacing, so without this the
1924
+ // hover:tracking-wide would snap. Sibling change on
1925
+ // the Grid button below.
1926
+ className={`px-2.5 py-1 focus:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset hover:tracking-wide ${layout === 'ring' ? 'bg-cyan-500/15 text-cyan-300 font-medium hover:bg-cyan-500/20 active:bg-cyan-500/25' : 'text-gray-400 hover:text-cyan-300 hover:bg-cyan-500/5 active:bg-cyan-500/15'} ${chromePopping === 'layout-ring' ? ' anet-chrome-pop' : ''}`}
1927
+ style={{ transition: 'background-color 150ms ease, color 150ms ease, letter-spacing 200ms ease-out' }}
1866
1928
  >
1867
1929
  Ring
1868
1930
  </button>
@@ -1879,14 +1941,20 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1879
1941
  // Round 306 / Loop: focus-visible:ring-2 → ring-1 sibling
1880
1942
  // change to Ring above — unifies focus-ring width across
1881
1943
  // all chrome buttons.
1882
- className={`px-2.5 py-1 border-l transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset ${layout === 'grid' ? 'bg-cyan-500/15 text-cyan-300 font-medium hover:bg-cyan-500/20 active:bg-cyan-500/25' : 'text-gray-400 hover:text-cyan-300 hover:bg-cyan-500/5 active:bg-cyan-500/15'} ${chromePopping === 'layout-grid' ? ' anet-chrome-pop' : ''}`}
1944
+ // R351 sibling — Grid button picks up hover:tracking-wide
1945
+ // + inline transition spec. Same vocabulary as Ring.
1946
+ className={`px-2.5 py-1 border-l focus:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset hover:tracking-wide ${layout === 'grid' ? 'bg-cyan-500/15 text-cyan-300 font-medium hover:bg-cyan-500/20 active:bg-cyan-500/25' : 'text-gray-400 hover:text-cyan-300 hover:bg-cyan-500/5 active:bg-cyan-500/15'} ${chromePopping === 'layout-grid' ? ' anet-chrome-pop' : ''}`}
1883
1947
  /* Round 268 / Loop: Grid button's left border (the
1884
1948
  internal divider between Ring and Grid) picks up
1885
1949
  pal.containerBorder, matching the wrapper change at
1886
1950
  line ~1460 and the chrome strip's segmented borders
1887
- (nodeSize, zoom). transition-colors className covers
1888
- the border-color eased on theme toggle. */
1889
- style={{ borderColor: pal.containerBorder }}
1951
+ (nodeSize, zoom). The R268 transition-colors className
1952
+ used to carry the border-color ease; R351 unfolds the
1953
+ transition list into the inline spec below so the
1954
+ letter-spacing tween rides alongside without snapping
1955
+ the border-color flip — border-color 200ms ease-out
1956
+ keeps R268's theme-toggle smoothness intact. */
1957
+ style={{ borderColor: pal.containerBorder, transition: 'background-color 150ms ease, color 150ms ease, border-color 200ms ease-out, letter-spacing 200ms ease-out' }}
1890
1958
  >
1891
1959
  Grid
1892
1960
  </button>
@@ -1973,11 +2041,43 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1973
2041
  7th surface in the info-density tabular-nums
1974
2042
  sweep — and the first on the HTML side
1975
2043
  (previous 6 were SVG <text>/<tspan>). */
1976
- className={`tabular-nums font-medium px-2.5 py-1 rounded-md border anet-topo-chip-focus transition-colors duration-200 ${
2044
+ /* Round 398 / Loop: chip-row chips gain hover translateY
2045
+ (-1px) lift on the CLICKABLE variant only (workingCount
2046
+ > 0 here / onlineNodes.length > 0 below / activeLinks
2047
+ > 0 deeper). Pre-R398 the chips brightened bg + border
2048
+ on hover (R201) but didn't lift — only their clickable
2049
+ siblings (filter pin pills R397, recent rows R143,
2050
+ legend rows R144) acknowledged cursor entry with a
2051
+ translate-y. R398 closes the chip-row by extending
2052
+ the same gesture to the static header chips, gated
2053
+ on the clickable role so empty chips (which have
2054
+ no role="button") stay planted at their R205
2055
+ opacity-50 receded paint. transition-transform
2056
+ + duration-200 + ease-out + transform-gpu added
2057
+ alongside existing transition-colors so the lift
2058
+ and the color tween share rhythm.
2059
+ Gesture-vocabulary table (post-R398):
2060
+ recent-signal row -1 px (R143)
2061
+ legend row -1 px (R144)
2062
+ group cluster box fill+sw lift (R142)
2063
+ filter pin pills -1 px (R397)
2064
+ chip-row chips -1 px (R398, this round)
2065
+ Empty chips: no lift. Pin-mirror chips: no
2066
+ conflict (R180 inset double-ring is a box-shadow
2067
+ not a transform). new data-chip-hover-lift attr
2068
+ surfaces the lift surface for tests. */
2069
+ // R414: chip-row chips gain `group` so inner unit
2070
+ // span brightens via group-hover:opacity-100 — sibling
2071
+ // to R355 filter pin pill inner-span hover-brighten.
2072
+ // Hover-brighten family extends from filter pills to
2073
+ // chip-row chips at the inner-span scope.
2074
+ className={`group tabular-nums font-medium px-2.5 py-1 rounded-md border anet-topo-chip-focus transition-colors transition-transform duration-200 ease-out transform-gpu ${
1977
2075
  workingCount > 0
1978
- ? 'bg-green-500/10 text-green-300 border-green-500/20 hover:bg-green-500/15 hover:border-green-500/30'
2076
+ ? 'bg-green-500/10 text-green-300 border-green-500/20 hover:bg-green-500/15 hover:border-green-500/30 hover:-translate-y-px'
1979
2077
  : 'bg-green-500/10 text-green-300 border-green-500/20'
1980
2078
  }`}
2079
+ data-chip-hover-lift={workingCount > 0 ? 'true' : 'false'}
2080
+ data-chip-group-hover-brighten="true"
1981
2081
  data-working-chip
1982
2082
  data-working-chip-aliases={workingAliases.join(',')}
1983
2083
  data-pin-mirror={pinnedStatus === 'working' ? 'true' : 'false'}
@@ -2047,7 +2147,17 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2047
2147
  pattern: small label spans demote, value stays
2048
2148
  prominent. data-working-chip-unit exposes the
2049
2149
  span for tests. */}
2050
- {workingCount}<span className="opacity-70" data-working-chip-unit> working</span>
2150
+ {/* Round 362 / Loop: digit picks up font-semibold
2151
+ (fw 500 → 600) for within-chip weight tier. The
2152
+ chip's outer className stays at font-medium (R313
2153
+ data-weight baseline); the digit overrides to
2154
+ semibold so it reads heavier than its " working"
2155
+ unit (which keeps fw 500 + R338 opacity-70).
2156
+ Joins the R333-R341 chip-internal-hierarchy arc
2157
+ at the chip-count scope. Sibling edits on the
2158
+ online + active-links chip digits below. data-
2159
+ working-chip-digit attr exposes the digit span. */}
2160
+ <span className="font-semibold transition-[font-weight] duration-200 group-hover:font-bold" data-working-chip-digit>{workingCount}</span><span className="opacity-70 transition-opacity duration-200 group-hover:opacity-100" data-working-chip-unit> working</span>
2051
2161
  </span>
2052
2162
  <span
2053
2163
  // Round 201 / Loop: online chip — mirror of the working
@@ -2061,11 +2171,15 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2061
2171
  /* Round 232 / Loop: tabular-nums on online chip
2062
2172
  (sibling treatment to working chip — same row,
2063
2173
  same digit-jitter physics on count crossings). */
2064
- className={`tabular-nums font-medium px-2.5 py-1 rounded-md border anet-topo-chip-focus transition-colors duration-200 ${
2174
+ // R398: hover translate-y lift on clickable variant — see working chip above.
2175
+ // R414: `group` parent + inner unit span group-hover-brighten — see working chip above.
2176
+ className={`group tabular-nums font-medium px-2.5 py-1 rounded-md border anet-topo-chip-focus transition-colors transition-transform duration-200 ease-out transform-gpu ${
2065
2177
  onlineNodes.length > 0
2066
- ? 'bg-cyan-500/10 text-cyan-300 border-cyan-500/20 hover:bg-cyan-500/15 hover:border-cyan-500/30'
2178
+ ? 'bg-cyan-500/10 text-cyan-300 border-cyan-500/20 hover:bg-cyan-500/15 hover:border-cyan-500/30 hover:-translate-y-px'
2067
2179
  : 'bg-cyan-500/10 text-cyan-300 border-cyan-500/20'
2068
2180
  }`}
2181
+ data-chip-hover-lift={onlineNodes.length > 0 ? 'true' : 'false'}
2182
+ data-chip-group-hover-brighten="true"
2069
2183
  data-online-chip
2070
2184
  data-online-chip-aliases={onlineAliases.join(',')}
2071
2185
  data-pin-mirror={pinnedStatus === 'idle' ? 'true' : 'false'}
@@ -2110,7 +2224,8 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2110
2224
  }}
2111
2225
  >
2112
2226
  {/* R337 sibling — online chip unit demotion. */}
2113
- {onlineNodes.length}<span className="opacity-70" data-online-chip-unit> online</span>
2227
+ {/* R362 sibling — online-chip digit gains font-semibold. */}
2228
+ <span className="font-semibold transition-[font-weight] duration-200 group-hover:font-bold" data-online-chip-digit>{onlineNodes.length}</span><span className="opacity-70 transition-opacity duration-200 group-hover:opacity-100" data-online-chip-unit> online</span>
2114
2229
  </span>
2115
2230
  </>
2116
2231
  );
@@ -2237,8 +2352,45 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2237
2352
  title={`${w} working · ${i} idle · ${o} offline`}
2238
2353
  data-fleet-pressure
2239
2354
  >
2240
- <span className="text-[10px] tracking-wide">pressure</span>
2241
- <span className="inline-flex h-1.5 w-16 rounded-full overflow-hidden" style={{ background: 'rgb(75 85 99 / 0.25)' }}>
2355
+ {/* Round 373 / Loop: pressure-bar kicker label gains
2356
+ font-medium (fw 400 → 500). Sibling small-text fw
2357
+ lift family with R363 recent-row alias + R364
2358
+ legend-row label + R366 group-label count + R368
2359
+ +N more flows footer — extends to a 5th surface
2360
+ (the chip-row's 'pressure' label). At fontSize
2361
+ 10 px tracking-wide against the chip's gray bg,
2362
+ the default fw 400 sat below the deliberate-data
2363
+ band; fw 500 brings it into parity with the
2364
+ chip-row 'working / online / active links' unit
2365
+ spans (chip-level font-medium R313). data-fleet-
2366
+ pressure-kicker attr exposes the kicker for tests. */}
2367
+ <span className="text-[10px] tracking-wide font-medium" data-fleet-pressure-kicker>pressure</span>
2368
+ {/* Round 374 / Loop: pressure-bar height h-1.5 → h-2
2369
+ (6 → 8 px) — sibling visual-weight bump (8th anchor
2370
+ in the family):
2371
+ R287 minimap viewport stroke 1 → 1.5
2372
+ R295 legend swatch base radius 5.5 → 6
2373
+ R359 recent-row pip base radius 1.6 → 1.8
2374
+ R360 hub digit fontSize 11 → 12
2375
+ R361 edge-badge digit fontSize 10 → 11
2376
+ R365 hub-highlight base radius 5 → 5.5
2377
+ R367 edge-badge rest stroke 1 → 1.25
2378
+ R374 pressure-bar height 1.5 → 2 (this round)
2379
+ +33 % bar height gives the working/idle/offline
2380
+ segments more visibility — at h-1.5 the 3-segment
2381
+ proportion bar was readable but slim; at h-2 the
2382
+ segments parse cleanly even when one tier is
2383
+ < 10 % share. Geometry-safe: items-center flex
2384
+ centers the bar inside the chip's py-1 (4 px top +
2385
+ 4 px bottom) — bar at 8 px stays comfortably
2386
+ inside the 10-px text-row height. R165 segment
2387
+ width transitions + R210 brightness hover + R83
2388
+ pin-mirror box-shadow on segments all preserved
2389
+ (segments inherit width from parent so the height
2390
+ bump propagates without segment-side edits).
2391
+ data-fleet-pressure-bar-height attr exposes the
2392
+ height token for tests. */}
2393
+ <span className="inline-flex h-2 w-16 rounded-full overflow-hidden" style={{ background: 'rgb(75 85 99 / 0.25)' }} data-fleet-pressure-bar-height="h-2">
2242
2394
  {seg(w, isLight ? '#059669' : '#22c55e', 'working', 'working')}
2243
2395
  {seg(i, isLight ? '#0d9488' : '#2dd4bf', 'idle', 'idle')}
2244
2396
  {seg(o, isLight ? '#94a3b8' : '#6b7280', 'offline', 'offline')}
@@ -2295,7 +2447,10 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2295
2447
  data-active-filter="status"
2296
2448
  data-filter-match-count={matchCount}
2297
2449
  data-filter-match-aliases={matchAliases.join(',')}
2298
- className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md font-mono font-medium text-xs border anet-fade-in anet-topo-chip-focus"
2450
+ // R355: `group` lets the inner opacity-70 spans (prefix
2451
+ // `filter:` + count `· N`) brighten to 100 % on pill hover.
2452
+ // Sibling treatment on group + vendor pills below.
2453
+ className="group inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md font-mono font-medium text-xs border anet-fade-in anet-topo-chip-focus transition-transform duration-200 ease-out hover:-translate-y-px transform-gpu" data-topo-filter-pill-hover-lift="true"
2299
2454
  title={matchCount > 0 ? `${matchPreview}${matchSuffix} — click to clear` : 'Click to clear filter'}
2300
2455
  onClick={() => setPinnedStatus(null)}
2301
2456
  style={{
@@ -2309,12 +2464,37 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2309
2464
  cursor: 'pointer',
2310
2465
  }}
2311
2466
  >
2312
- <span><span className="hidden sm:inline opacity-70" data-filter-prefix>filter: </span>{pinnedStatus}<span className="opacity-70 tabular-nums" data-filter-pill-count> · {matchCount}</span></span>
2467
+ {/* Round 412 / Loop: filter pin pill VALUE picks up the
2468
+ chip-internal-hierarchy arc. Pre-R412 the value span
2469
+ (pinnedStatus / pinnedGroup / pinnedVendor) inherited
2470
+ the parent's font-medium (fw=500); prefix and suffix
2471
+ were opacity-70 label-tier but the VALUE itself sat
2472
+ at the same baseline weight. R412 wraps the value in
2473
+ a font-semibold span (fw=600) so the pill now reads
2474
+ with proper data-tier emphasis — sibling treatment
2475
+ to R333/R335-R341/R362/R369/R389/R410. data-filter-
2476
+ value attr surfaces the value span for tests.
2477
+ 4-pill replace family — status / group / vendor / edge. */}
2478
+ <span><span className="hidden sm:inline opacity-70 transition-opacity duration-200 group-hover:opacity-100" data-filter-prefix>filter: </span><span className="font-semibold" data-filter-value>{pinnedStatus}</span><span className="opacity-70 tabular-nums transition-opacity duration-200 group-hover:opacity-100" data-filter-pill-count> · {matchCount}</span></span>
2313
2479
  <button
2314
2480
  type="button"
2315
2481
  aria-label={`Clear ${pinnedStatus} filter`}
2316
2482
  onClick={(e) => { e.stopPropagation(); setPinnedStatus(null); }}
2317
- className="ml-0.5 leading-none hover:opacity-70"
2483
+ /* Round 356 / Loop: filter pin pill × buttons gain
2484
+ hover:scale-110 (Tailwind 4 modern CSS `scale` property,
2485
+ not legacy transform). Sibling polish to R354 vendor
2486
+ letter glyph + R350/R352/R353 chrome icon hover-scales.
2487
+ Pre-R356 the × had only hover:opacity-70 — the target
2488
+ dimmed under cursor but didn't lift. R356 adds a 10 %
2489
+ scale on hover so the click-target reads as "press me"
2490
+ alongside the dim. transform-gpu hint promotes the
2491
+ button to its own compositor layer for crisper edges
2492
+ during the scale tween. transition-transform duration-
2493
+ 200 matches the chrome icon hover-scale timing family.
2494
+ inline-block is default for <button> so no display
2495
+ tweak needed. replace_all covers all 4 filter pin
2496
+ pills (status / group / vendor / edge) at once. */
2497
+ className="ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 transform-gpu"
2318
2498
  style={{ background: 'transparent', color: 'inherit', cursor: 'pointer', padding: 0 }}
2319
2499
  >×</button>
2320
2500
  </span>
@@ -2333,7 +2513,8 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2333
2513
  data-active-filter="group"
2334
2514
  data-filter-match-count={matchCount}
2335
2515
  data-filter-match-aliases={matchAliases.join(',')}
2336
- className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md font-mono font-medium text-xs border anet-fade-in anet-topo-chip-focus"
2516
+ // R355 sibling — `group` parent + group-hover on inner spans.
2517
+ className="group inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md font-mono font-medium text-xs border anet-fade-in anet-topo-chip-focus transition-transform duration-200 ease-out hover:-translate-y-px transform-gpu" data-topo-filter-pill-hover-lift="true"
2337
2518
  title={matchCount > 0 ? `${matchPreview}${matchSuffix} — click to clear` : 'Click to clear filter'}
2338
2519
  onClick={() => setPinnedGroup(null)}
2339
2520
  style={{
@@ -2343,12 +2524,27 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2343
2524
  cursor: 'pointer',
2344
2525
  }}
2345
2526
  >
2346
- <span><span className="hidden sm:inline opacity-70" data-filter-prefix>filter: </span>{pinnedGroup}<span className="opacity-70 tabular-nums" data-filter-pill-count> · {matchCount}</span></span>
2527
+ {/* R412: see status pill above — filter value fw=600 data tier. */}
2528
+ <span><span className="hidden sm:inline opacity-70 transition-opacity duration-200 group-hover:opacity-100" data-filter-prefix>filter: </span><span className="font-semibold" data-filter-value>{pinnedGroup}</span><span className="opacity-70 tabular-nums transition-opacity duration-200 group-hover:opacity-100" data-filter-pill-count> · {matchCount}</span></span>
2347
2529
  <button
2348
2530
  type="button"
2349
2531
  aria-label={`Clear group filter ${pinnedGroup}`}
2350
2532
  onClick={(e) => { e.stopPropagation(); setPinnedGroup(null); }}
2351
- className="ml-0.5 leading-none hover:opacity-70"
2533
+ /* Round 356 / Loop: filter pin pill × buttons gain
2534
+ hover:scale-110 (Tailwind 4 modern CSS `scale` property,
2535
+ not legacy transform). Sibling polish to R354 vendor
2536
+ letter glyph + R350/R352/R353 chrome icon hover-scales.
2537
+ Pre-R356 the × had only hover:opacity-70 — the target
2538
+ dimmed under cursor but didn't lift. R356 adds a 10 %
2539
+ scale on hover so the click-target reads as "press me"
2540
+ alongside the dim. transform-gpu hint promotes the
2541
+ button to its own compositor layer for crisper edges
2542
+ during the scale tween. transition-transform duration-
2543
+ 200 matches the chrome icon hover-scale timing family.
2544
+ inline-block is default for <button> so no display
2545
+ tweak needed. replace_all covers all 4 filter pin
2546
+ pills (status / group / vendor / edge) at once. */
2547
+ className="ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 transform-gpu"
2352
2548
  style={{ background: 'transparent', color: 'inherit', cursor: 'pointer', padding: 0 }}
2353
2549
  >×</button>
2354
2550
  </span>
@@ -2383,7 +2579,8 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2383
2579
  data-active-filter="vendor"
2384
2580
  data-filter-match-count={matchCount}
2385
2581
  data-filter-match-aliases={matchAliases.join(',')}
2386
- className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md font-mono font-medium text-xs border anet-fade-in anet-topo-chip-focus"
2582
+ // R355 sibling — `group` parent + group-hover on inner spans.
2583
+ className="group inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md font-mono font-medium text-xs border anet-fade-in anet-topo-chip-focus transition-transform duration-200 ease-out hover:-translate-y-px transform-gpu" data-topo-filter-pill-hover-lift="true"
2387
2584
  title={matchCount > 0 ? `${matchPreview}${matchSuffix} — click to clear` : 'Click to clear vendor filter'}
2388
2585
  onClick={() => setPinnedVendor(null)}
2389
2586
  style={{
@@ -2393,12 +2590,27 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2393
2590
  cursor: 'pointer',
2394
2591
  }}
2395
2592
  >
2396
- <span><span className="hidden sm:inline opacity-70" data-filter-prefix>filter: </span>{pinnedVendor}<span className="opacity-70 tabular-nums" data-filter-pill-count> · {matchCount}</span></span>
2593
+ {/* R412: see status pill above — filter value fw=600 data tier. */}
2594
+ <span><span className="hidden sm:inline opacity-70 transition-opacity duration-200 group-hover:opacity-100" data-filter-prefix>filter: </span><span className="font-semibold" data-filter-value>{pinnedVendor}</span><span className="opacity-70 tabular-nums transition-opacity duration-200 group-hover:opacity-100" data-filter-pill-count> · {matchCount}</span></span>
2397
2595
  <button
2398
2596
  type="button"
2399
2597
  aria-label={`Clear vendor filter ${pinnedVendor}`}
2400
2598
  onClick={(e) => { e.stopPropagation(); setPinnedVendor(null); }}
2401
- className="ml-0.5 leading-none hover:opacity-70"
2599
+ /* Round 356 / Loop: filter pin pill × buttons gain
2600
+ hover:scale-110 (Tailwind 4 modern CSS `scale` property,
2601
+ not legacy transform). Sibling polish to R354 vendor
2602
+ letter glyph + R350/R352/R353 chrome icon hover-scales.
2603
+ Pre-R356 the × had only hover:opacity-70 — the target
2604
+ dimmed under cursor but didn't lift. R356 adds a 10 %
2605
+ scale on hover so the click-target reads as "press me"
2606
+ alongside the dim. transform-gpu hint promotes the
2607
+ button to its own compositor layer for crisper edges
2608
+ during the scale tween. transition-transform duration-
2609
+ 200 matches the chrome icon hover-scale timing family.
2610
+ inline-block is default for <button> so no display
2611
+ tweak needed. replace_all covers all 4 filter pin
2612
+ pills (status / group / vendor / edge) at once. */
2613
+ className="ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 transform-gpu"
2402
2614
  style={{ background: 'transparent', color: 'inherit', cursor: 'pointer', padding: 0 }}
2403
2615
  >×</button>
2404
2616
  </span>
@@ -2431,7 +2643,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2431
2643
  data-filter-match-count={link.count}
2432
2644
  data-filter-match-aliases={`${link.from},${link.to}`}
2433
2645
  data-active-filter-edge-hot={isHot ? 'true' : 'false'}
2434
- className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md font-mono font-medium text-xs border anet-fade-in anet-topo-chip-focus"
2646
+ className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md font-mono font-medium text-xs border anet-fade-in anet-topo-chip-focus transition-transform duration-200 ease-out hover:-translate-y-px transform-gpu" data-topo-filter-pill-hover-lift="true"
2435
2647
  title={`${link.from} → ${link.to} (${link.count} msg${link.count === 1 ? '' : 's'}${isHot ? ', hot lane · ≥ 10' : ''}) — click to clear`}
2436
2648
  onClick={() => setPinnedEdgeKey(null)}
2437
2649
  style={{
@@ -2441,9 +2653,11 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2441
2653
  cursor: 'pointer',
2442
2654
  }}
2443
2655
  >
2656
+ {/* R412: filter pin pill value (edge variant) picks up fw=600.
2657
+ Sibling treatment to the status/group/vendor pills above. */}
2444
2658
  <span>
2445
2659
  <span className="hidden sm:inline opacity-70" data-filter-prefix>filter: </span>
2446
- {link.from}→{link.to}
2660
+ <span className="font-semibold" data-filter-value>{link.from}→{link.to}</span>
2447
2661
  {/* Round 323 / Loop: edge filter pill count digit picks
2448
2662
  up tabular-nums (Tailwind class on both cold +
2449
2663
  hot branches). Sibling treatment to the status /
@@ -2475,7 +2689,21 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2475
2689
  type="button"
2476
2690
  aria-label={`Clear edge filter ${link.from} → ${link.to}`}
2477
2691
  onClick={(e) => { e.stopPropagation(); setPinnedEdgeKey(null); }}
2478
- className="ml-0.5 leading-none hover:opacity-70"
2692
+ /* Round 356 / Loop: filter pin pill × buttons gain
2693
+ hover:scale-110 (Tailwind 4 modern CSS `scale` property,
2694
+ not legacy transform). Sibling polish to R354 vendor
2695
+ letter glyph + R350/R352/R353 chrome icon hover-scales.
2696
+ Pre-R356 the × had only hover:opacity-70 — the target
2697
+ dimmed under cursor but didn't lift. R356 adds a 10 %
2698
+ scale on hover so the click-target reads as "press me"
2699
+ alongside the dim. transform-gpu hint promotes the
2700
+ button to its own compositor layer for crisper edges
2701
+ during the scale tween. transition-transform duration-
2702
+ 200 matches the chrome icon hover-scale timing family.
2703
+ inline-block is default for <button> so no display
2704
+ tweak needed. replace_all covers all 4 filter pin
2705
+ pills (status / group / vendor / edge) at once. */
2706
+ className="ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 transform-gpu"
2479
2707
  style={{ background: 'transparent', color: 'inherit', cursor: 'pointer', padding: 0 }}
2480
2708
  >×</button>
2481
2709
  </span>
@@ -2740,9 +2968,35 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2740
2968
  vendor letter chips ('A:N', 'O:N', '书:N',
2741
2969
  '?:N'). They display vendor-distribution
2742
2970
  data; same tier as the sibling data chips. */
2743
- className="tabular-nums font-medium inline-flex items-baseline gap-0.5 px-1 rounded anet-topo-chip-focus"
2971
+ /* Round 401 / Loop: vendor letter chip closes the
2972
+ hover-lift gesture family at its last unaddressed
2973
+ interactive HTML surface. R397/R398/R399 lifted
2974
+ filter pin pills + chip-row chips (working /
2975
+ online / active-links); R400 lifted standalone
2976
+ chrome buttons (reset / fullscreen). The vendor
2977
+ letter chips (A:N / O:N / 书:N / ?:N) are
2978
+ sibling interactive chips in the same chip-row
2979
+ — clickable to toggle the vendor filter pin —
2980
+ but were not yet on the hover-lift family.
2981
+ R401 closes the gap with hover:-translate-y-px
2982
+ + transition-transform + transform-gpu added
2983
+ to the className. The inline transition list
2984
+ (box-shadow + background-color) keeps eaching
2985
+ independently — different property axes compose
2986
+ cleanly. Existing R354 glyph scale-1.1 (inner
2987
+ span) + R202 chip bg color-mix + R180 pin-mirror
2988
+ box-shadow + R354 glyph hover transform all
2989
+ preserved. data-vendor-letter-hover-lift attr
2990
+ surfaces the lift for tests. */
2991
+ // R417: `group` parent enables the count suffix to
2992
+ // brighten on chip hover via group-hover:opacity-100
2993
+ // — sibling to R355 filter-pill prefix/suffix + R414
2994
+ // chip-row unit brighten. Closes the inner-span
2995
+ // hover-brighten family at the vendor chip surface.
2996
+ className="group tabular-nums font-medium inline-flex items-baseline gap-0.5 px-1 rounded anet-topo-chip-focus transition-transform duration-200 ease-out transform-gpu hover:-translate-y-px"
2744
2997
  data-vendor-letter={v.initial}
2745
2998
  data-vendor-letter-count={v.count}
2999
+ data-vendor-letter-hover-lift="true"
2746
3000
  data-vendor-pinned={isPinned ? 'true' : 'false'}
2747
3001
  data-vendor-hovered={hoveredVendor === v.initial ? 'true' : 'false'}
2748
3002
  data-vendor-aliases={aliases.join(',')}
@@ -2786,7 +3040,64 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2786
3040
  }
2787
3041
  }}
2788
3042
  >
2789
- <span style={{ color: v.color }}>{v.initial}</span>
3043
+ {/* Round 354 / Loop: vendor letter glyph scales
3044
+ 1.0 → 1.1 on hover. R88 already dims OTHER
3045
+ vendors on hover via canvas-wide opacity
3046
+ masking; R202 added a chip-level bg tint
3047
+ (color-mix 12 % alpha) so the chip itself
3048
+ responds. R354 closes the trio with a glyph-
3049
+ level lift: the focused vendor LETTER actively
3050
+ rises (transform scale) rather than the chip
3051
+ merely changing colour. Three layers of positive
3052
+ feedback on the hovered vendor + canvas-wide
3053
+ negative feedback on the others — a clean
3054
+ figure/ground separation.
3055
+
3056
+ display: inline-block is required for transform
3057
+ to apply (inline elements ignore transform).
3058
+ transformOrigin: 'center' so the glyph pivots
3059
+ around its centre instead of arcing from the
3060
+ baseline anchor. transition rides the existing
3061
+ Tailwind 4 transform/scale list (no new
3062
+ property — Tailwind already lists transform in
3063
+ the default transition-property set). 200ms
3064
+ matches the R202 chip bg-tint timing so the
3065
+ glyph lift and chip background ease in concert. */}
3066
+ {/* Round 369 / Loop: vendor letter glyph picks up
3067
+ fontWeight 600 (font-semibold). The glyph is the
3068
+ vendor identifier — the DATA the operator scans
3069
+ in this chip (A / O / 书 / C / G / ?). R333 set
3070
+ the count suffix `:N` to text-gray-400 + tabular-
3071
+ nums and (via parent inheritance) fw 500. Pre-
3072
+ R369 the LETTER also inherited fw 500 from the
3073
+ chip's font-medium — letter and count read at
3074
+ the same weight, contradicting the data-vs-label
3075
+ hierarchy the rest of the chip-row already speaks.
3076
+ R369 lifts the letter to fw 600 so the chip now
3077
+ reads as the same two-tier pattern R362 closed
3078
+ on the working / online / active-links chips:
3079
+ chip digit/letter fw 600 (data)
3080
+ chip unit/count fw 500 (label)
3081
+ Sibling treatment to R362 — extends the R333-R341
3082
+ chip-internal-hierarchy arc to the vendor-letter
3083
+ chip surface (9th surface family). R354 transform-
3084
+ scale-on-hover + R88 canvas-dim-others + R202
3085
+ chip bg color-mix all preserved on the same span.
3086
+ data-vendor-letter-glyph-font-weight attr exposes
3087
+ the value for tests. */}
3088
+ <span
3089
+ data-vendor-letter-glyph={v.initial}
3090
+ data-vendor-letter-glyph-hover={hoveredVendor === v.initial ? 'true' : 'false'}
3091
+ data-vendor-letter-glyph-font-weight="600"
3092
+ style={{
3093
+ color: v.color,
3094
+ display: 'inline-block',
3095
+ fontWeight: 600,
3096
+ transform: hoveredVendor === v.initial ? 'scale(1.1)' : 'scale(1)',
3097
+ transformOrigin: 'center',
3098
+ transition: 'transform 200ms ease-out',
3099
+ }}
3100
+ >{v.initial}</span>
2790
3101
  {/* Round 333 / Loop: vendor count suffix `:{N}` joins
2791
3102
  the R317 subordinate-text-lift family (gray-500 →
2792
3103
  gray-400) plus picks up tabular-nums for digit
@@ -2799,8 +3110,17 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2799
3110
  reads it as "deliberate subordinate metadata"
2800
3111
  rather than near-invisible chrome. data-vendor-
2801
3112
  letter-count exposes the span for tests. */}
3113
+ {/* R417: count suffix opacity-70 + group-hover:
3114
+ opacity-100 brightens on chip hover. Inner-span
3115
+ hover-brighten family (3rd anchor) — sibling to
3116
+ R355 filter pill prefix/suffix and R414 chip-row
3117
+ unit. Effective shade at rest: text-gray-400 ×
3118
+ 70 % alpha; on hover: full gray-400. The label-
3119
+ tier-vs-glyph differentiation persists on hover
3120
+ since the glyph (R369 fw=600) stays at full
3121
+ opacity. R333 :{count} format preserved. */}
2802
3122
  <span
2803
- className="text-gray-400 tabular-nums"
3123
+ className="text-gray-400 tabular-nums opacity-70 transition-opacity duration-200 group-hover:opacity-100"
2804
3124
  data-vendor-letter-count-suffix
2805
3125
  >:{v.count}</span>
2806
3126
  </span>
@@ -2888,11 +3208,36 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2888
3208
  (third chip in the row — matches working + online
2889
3209
  chip treatment so all three digits in the chip row
2890
3210
  stay width-stable across counter crossings). */
2891
- className={`tabular-nums font-medium hidden sm:inline px-2.5 py-1 rounded-md border anet-topo-chip-focus ${
3211
+ /* Round 399 / Loop: active-links chip closes the 3-chip
3212
+ chip-row by extending R398's hover translateY(-1px)
3213
+ lift onto the third (rightmost) chip. The R398 family
3214
+ already covered working + online chips on the
3215
+ clickable variant; R399 adds the same gate (isInter-
3216
+ active = flowLinks.length > 0) so empty active-links
3217
+ stays planted at R206's opacity-50 receded paint.
3218
+ transition-transform + ease-out + transform-gpu join
3219
+ the inline transition list (different property axes
3220
+ compose cleanly: inline handles color/bg/border/
3221
+ opacity, className handles transform).
3222
+ Gesture-vocabulary table (post-R399 — now complete
3223
+ across the chip-row):
3224
+ working chip -1 px (R398)
3225
+ online chip -1 px (R398)
3226
+ active-links chip -1 px (R399, this round)
3227
+ filter pin pills -1 px (R397)
3228
+ recent-signal row -1 px (R143)
3229
+ legend row -1 px (R144)
3230
+ Every interactive chip in TopoGraph lifts on hover.
3231
+ data-chip-hover-lift attr exposes the lift surface
3232
+ state ('true' clickable, 'false' empty) for tests. */
3233
+ // R414: `group` parent + inner unit span group-hover-brighten — see working chip above.
3234
+ className={`group tabular-nums font-medium hidden sm:inline px-2.5 py-1 rounded-md border anet-topo-chip-focus transition-transform duration-200 ease-out transform-gpu ${
2892
3235
  isInteractive
2893
- ? 'bg-gray-500/10 text-gray-400 border-gray-500/20 hover:bg-cyan-500/10 hover:text-cyan-200 hover:border-cyan-500/30'
3236
+ ? 'bg-gray-500/10 text-gray-400 border-gray-500/20 hover:bg-cyan-500/10 hover:text-cyan-200 hover:border-cyan-500/30 hover:-translate-y-px'
2894
3237
  : 'bg-gray-500/10 text-gray-400 border-gray-500/20'
2895
3238
  }`}
3239
+ data-chip-hover-lift={isInteractive ? 'true' : 'false'}
3240
+ data-chip-group-hover-brighten="true"
2896
3241
  data-active-links-chip
2897
3242
  data-active-links-flow-count={flowLinks.length}
2898
3243
  data-active-links-clickable={isInteractive ? 'true' : 'false'}
@@ -2920,7 +3265,8 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2920
3265
  the 5th chip surface in the R333/R335/R336/R337
2921
3266
  chip-internal-hierarchy arc. data-active-links-
2922
3267
  chip-unit exposes the unit span for tests. */}
2923
- {flowLinks.length}<span className="opacity-70" data-active-links-chip-unit> active link{flowLinks.length === 1 ? '' : 's'}</span>
3268
+ {/* R362 sibling — active-links chip digit gains font-semibold. */}
3269
+ <span className="font-semibold transition-[font-weight] duration-200 group-hover:font-bold" data-active-links-chip-digit>{flowLinks.length}</span><span className="opacity-70 transition-opacity duration-200 group-hover:opacity-100" data-active-links-chip-unit> active link{flowLinks.length === 1 ? '' : 's'}</span>
2924
3270
  {rel ? (() => {
2925
3271
  // Round 161 / Loop: extend R160's recency-pip
2926
3272
  // vocabulary up one scope — from per-flow row to
@@ -2945,8 +3291,8 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2945
3291
  const alpha = ageSec <= 30
2946
3292
  ? 1
2947
3293
  : ageSec <= 300
2948
- ? 1 - ((ageSec - 30) / 270) * 0.75
2949
- : 0.25;
3294
+ ? 1 - ((ageSec - 30) / 270) * 0.70 /* R358: floor 0.25 → 0.30 lift across 3 freshness scopes */
3295
+ : 0.30; /* R358: stale floor lifted 0.25 → 0.30 — 20% legibility bump while preserving fresh/stale ratio */
2950
3296
  // Cyan dark / teal light to match palette legendAccent.
2951
3297
  const dotColor = isLight
2952
3298
  ? `rgba(13, 148, 136, ${alpha.toFixed(2)})`
@@ -2963,7 +3309,28 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2963
3309
  // color: dotColor — the lift only affects the trailing
2964
3310
  // literal "last {rel}" text.
2965
3311
  return (
2966
- <span className="text-gray-400">
3312
+ // Round 357 / Loop: active-links chip freshness
3313
+ // suffix wrapper picks up `tabular-nums` for digit
3314
+ // width-lock. Pre-R357 the literal "last {rel}"
3315
+ // text (e.g. "last 5s ago", "last 10s ago",
3316
+ // "last 1m ago") had natural-figure digits — the
3317
+ // freshness ticker updates every second, so the
3318
+ // 9→10 boundary on the seconds counter and the
3319
+ // 59→60s → 1m flip both jittered ~1-2 px of glyph
3320
+ // width which propagated through the chip-row's
3321
+ // inline-flex layout, nudging the freshness DOT
3322
+ // and the chip's left edge. Tabular-nums on the
3323
+ // wrapper applies to all descendant digits only
3324
+ // (letters render at natural widths) so the
3325
+ // ticker stays planted across every count cross.
3326
+ // Joins the R224-R232 info-density tabular-nums
3327
+ // sweep at the chip-row freshness scope. Pure
3328
+ // paint-level change, no geometry shift on rest.
3329
+ // The R342 text-gray-400 lift + R161 dot freshness
3330
+ // alpha ramp + R317 subordinate-text-lift family
3331
+ // all preserved. data-active-links-freshness-
3332
+ // wrapper attr exposes the wrapper for tests.
3333
+ <span className="text-gray-400 tabular-nums" data-active-links-freshness-wrapper>
2967
3334
  <span
2968
3335
  data-active-links-freshness-dot
2969
3336
  data-active-links-freshness-alpha={alpha.toFixed(2)}
@@ -3627,19 +3994,174 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3627
3994
  active surfaces the activity state for test probes
3628
3995
  (active spokes don't carry the bucket/dur attrs so
3629
3996
  they need their own data anchor). */
3997
+ // Round 382 / Loop: hub-spoke path picks up
3998
+ // strokeLinecap='round'. Sibling polish to R378 flow-
3999
+ // rail dashes + R380 group box dashes — three dashed-
4000
+ // stroke surfaces now share 'round' linecap:
4001
+ // R378 flow-rail '2 12' -> soft 3-px pills
4002
+ // R380 group box '6 6' -> soft 7.5-px pills
4003
+ // R382 hub spoke '6 14' -> soft 7-px pills (this round)
4004
+ // For idle spokes (dashed at sw=1), each 6-px dash gains
4005
+ // 0.5-px round caps and reads as a soft pill instead of
4006
+ // a sharp 6 x 1 rectangle. Active spokes (solid, no
4007
+ // dasharray) have caps mostly hidden by the hub center +
4008
+ // node radius. Geometry-safe; paint-only. R51 sentinel
4009
+ // strokeWidth 1.5/3 untouched (idle=1, active=2). data-
4010
+ // topo-hub-spoke-linecap attr exposes the value for tests.
4011
+ // Round 419 / Loop: hub-spoke idle opacity 0.45 → 0.50.
4012
+ // Stale-state legibility lift family 9th anchor — pairs
4013
+ // with R391 (active 0.7 → 0.8) and R415 (active sw 2 →
4014
+ // 2.25) so the same spoke path is now polished on BOTH
4015
+ // active AND idle tiers. Pre-R419 idle spokes painted
4016
+ // at α=0.45 with R46 anet-topo-spoke-flow dashed
4017
+ // animation; the dashed pulses sat at the "background
4018
+ // chatter" floor — visible but understated. R419
4019
+ // lifts to 0.50 so idle spokes read more confidently
4020
+ // while the active/idle contrast ratio stays clear
4021
+ // (0.8/0.50 = 1.6× vs prior 0.8/0.45 = 1.78×; still
4022
+ // a sharp two-tier distinction).
4023
+ // Stale-state legibility lift family (9 anchors now):
4024
+ // R317 subordinate-text gray-500 → gray-400
4025
+ // R358 freshness floor 0.25 → 0.30
4026
+ // R372 minimap offline-dot 0.5 → 0.6
4027
+ // R404 hub-halo cyber trough 0.08 → 0.10
4028
+ // R405 hub-halo light trough 0.32 → 0.34
4029
+ // R406 edge freshness floor 0.35 → 0.40
4030
+ // R407 node halo offline opacity (cyber + light)
4031
+ // R413 active-node pulse trough (cyber + light)
4032
+ // R419 hub-spoke idle opacity 0.45 → 0.50 (this round)
4033
+ // data-topo-hub-spoke-opacity attr (R391) updates to
4034
+ // surface the resolved per-state value.
4035
+ //
4036
+ // Round 415 / Loop: hub-spoke active strokeWidth 2 → 2.25.
4037
+ // Pairs with R391 (active opacity 0.7 → 0.8) so the same
4038
+ // active-state path lifts BOTH stroke weight AND opacity
4039
+ // in concert. Pre-R415 active strokes sat at sw=2 — clear
4040
+ // step over idle sw=1, but a touch lighter than the
4041
+ // weight family's other "active" indicators (R385 hub
4042
+ // hover-ring sw=1.75 / R402 legend pin-ring sw=1.75 /
4043
+ // R367 edge-badge rest sw=1.25). R415 bumps to 2.25 so
4044
+ // the active spoke reads with proportional weight to its
4045
+ // role — the line connecting the focal point to the
4046
+ // active node deserves the heaviest active stroke in the
4047
+ // family (after pin/hot edge-badge sw=2). Stays clear of
4048
+ // R51 sentinels (1.5 / 3) at 2.25.
4049
+ // Visual-weight bump family (14 anchors now):
4050
+ // R287 minimap viewport stroke 1 → 1.5
4051
+ // R295 legend swatch radius 5.5 → 6
4052
+ // R359 recent-row pip radius 1.6 → 1.8
4053
+ // R360 hub digit fontSize 11 → 12
4054
+ // R361 edge-badge digit fontSize 10 → 11
4055
+ // R365 hub-highlight radius 5 → 5.5
4056
+ // R367 edge-badge rest stroke 1 → 1.25
4057
+ // R374 pressure-bar height 1.5 → 2
4058
+ // R383 recent-row pip radius 1.8 → 2.0
4059
+ // R384 minimap online dot 1.7 → 1.9
4060
+ // R385 hub hover-ring stroke 1.5 → 1.75
4061
+ // R402 legend pin-ring stroke 1.5 → 1.75
4062
+ // R408 hub-halo radius 18 → 20
4063
+ // R415 hub-spoke active stroke 2 → 2.25 (this round)
4064
+ // R382 strokeLinecap='round' + R391 opacity 0.45/0.8 +
4065
+ // R51-safe idle sw=1 all preserved. 250ms transition
4066
+ // list already covers stroke-width — the new tier eases
4067
+ // naturally. data-topo-hub-spoke-stroke-width-active
4068
+ // attr surfaces the active value for tests.
4069
+ //
4070
+ // Round 391 / Loop: hub-spoke active opacity 0.7 → 0.8.
4071
+ // Pre-R391 active spokes (the spoke connecting the hub
4072
+ // to the currently-active alias — hovered or pinned)
4073
+ // lifted opacity from rest 0.45 to active 0.7 — a clear
4074
+ // step but slightly understated against the canvas
4075
+ // chrome. R391 lifts active to 0.8 so the "this spoke
4076
+ // connects to your active node" signal reads with
4077
+ // matching weight to the R370 hub hover-ring opacity
4078
+ // (0.7 → 0.8 cyber) — paired canvas signals now share
4079
+ // the same active-state alpha (0.8) so when a user
4080
+ // hovers a node, both the spoke and the hub-ring lift
4081
+ // to identical opacity. Rest 0.45 invariant preserved.
4082
+ // Theme-consistency / canvas-presence polish family
4083
+ // (6th anchor):
4084
+ // R370 hub hover-ring opacity 0.7 → 0.8 cyber
4085
+ // R371 edge-badge rest opacity 0.82 → 0.85 cyber
4086
+ // R372 minimap offline-dot opacity 0.5 → 0.6
4087
+ // R386 hub-highlight idle opacity 0.9 → 0.95
4088
+ // R387 hover-detail panel opacity 0.94 → 0.97 cyber
4089
+ // R391 hub-spoke active opacity 0.7 → 0.8 (this round)
4090
+ // Idle path (45% alpha + dashed flow animation) entirely
4091
+ // untouched — R391 is an active-state-only lift.
4092
+ // data-topo-hub-spoke-opacity attr exposes the resolved
4093
+ // value for tests. R382 strokeLinecap='round' + R51
4094
+ // sentinel-safe sw (1 idle / 2 active) preserved.
4095
+ /* Round 430 / Loop: hub-spoke opacity hover lift on
4096
+ hoveredAlias === session.alias. Adds a "this node's
4097
+ spoke" affordance to the node-hover gesture — in a
4098
+ dense ring layout the spokes are visually quiet
4099
+ (idle α=0.50 dashed, active α=0.80 solid) so hovering
4100
+ a node didn't telegraph which line connects to it.
4101
+ R430 lifts the matched spoke's opacity:
4102
+ idle 0.50 → 0.70 (hover-α=0.70, +0.20)
4103
+ active 0.80 → 0.95 (hover-α=0.95, +0.15)
4104
+ The +0.15-to-0.20 lift keeps the active/idle two-tier
4105
+ distinction (0.95 vs 0.70 still a clear gap) while
4106
+ making the hovered-node's spoke visibly brighter than
4107
+ every other spoke at its own activity tier. R241
4108
+ transition list already covers opacity 250ms so the
4109
+ lift eases for free. Sibling to R429 label-card body
4110
+ solidity lift — both surface a single-node-focused
4111
+ attention cue with the same easing cadence.
4112
+ Stacks with the 6-layer node hover cue stack at the
4113
+ inter-node-link scope:
4114
+ R26 group translateY -2px (per-node)
4115
+ R217 stroke tint legendAccent (per-node card)
4116
+ R142 drop-shadow boost (per-node card)
4117
+ R427 alias letter-spacing (per-node text)
4118
+ R428 sub-text letter-spacing (per-node text)
4119
+ R429 body opacity 0.94 → 1.0 (per-node card)
4120
+ R430 spoke opacity α+ (this round) (link to hub)
4121
+ data-topo-hub-spoke-hovered exposes the gate. */
4122
+ const isHoveredSpoke = !reducedMotion && hoveredAlias === session.alias;
4123
+ const spokeOpacity = isActiveSpoke
4124
+ ? (isHoveredSpoke ? 0.95 : 0.80)
4125
+ : (isHoveredSpoke ? 0.70 : 0.50);
4126
+ /* Round 435 / Loop: hub-spoke stroke-width hover lift —
4127
+ sibling to R430 opacity hover at the same surface. When
4128
+ hoveredAlias matches, BOTH opacity AND stroke-width
4129
+ lift on the matched spoke so the eye registers a
4130
+ 2-axis "this node's spoke" gesture (paint + geometry).
4131
+ idle 1.00 → 1.25 (Δ +0.25, +25%)
4132
+ active 2.25 → 2.50 (Δ +0.25, +11%)
4133
+ Same +0.25 absolute delta keeps the idle/active visual
4134
+ progression consistent — at rest sw ratio 2.25:1 = 2.25,
4135
+ on hover 2.50:1.25 = 2.0; both still clearly two-tier.
4136
+ R241 transition list already covers stroke-width 250ms
4137
+ so the lift eases for free.
4138
+ R51 sentinel-safe: spoke is canvas <path>, not
4139
+ data-node <circle> (the sentinel selector is gated to
4140
+ g[data-node] descendants). 1.25 and 2.5 are not in the
4141
+ reserved {1.5, 3} set so the overlap-test sentinel
4142
+ attribute selector wouldn't match either way. */
4143
+ const spokeStrokeWidth = isActiveSpoke
4144
+ ? (isHoveredSpoke ? 2.5 : 2.25)
4145
+ : (isHoveredSpoke ? 1.25 : 1);
3630
4146
  return (
3631
4147
  <path
3632
4148
  key={`hub-${session.alias}`}
3633
4149
  d={path}
3634
4150
  fill="none"
3635
4151
  stroke={isActiveSpoke ? pal.spokeStroke.active : pal.spokeStroke.idle}
3636
- strokeWidth={isActiveSpoke ? 2 : 1}
4152
+ strokeWidth={spokeStrokeWidth}
3637
4153
  strokeDasharray={isActiveSpoke ? 'none' : '6 14'}
3638
- opacity={isActiveSpoke ? 0.7 : 0.45}
4154
+ strokeLinecap="round"
4155
+ opacity={spokeOpacity}
3639
4156
  className={isActiveSpoke ? undefined : 'anet-topo-spoke-flow'}
3640
4157
  data-topo-spoke-bucket={isActiveSpoke ? undefined : busy}
3641
4158
  data-topo-spoke-dur={isActiveSpoke ? undefined : spokeDur}
3642
4159
  data-topo-hub-spoke-active={isActiveSpoke ? 'true' : 'false'}
4160
+ data-topo-hub-spoke-hovered={isHoveredSpoke ? 'true' : 'false'}
4161
+ data-topo-hub-spoke-opacity={spokeOpacity}
4162
+ data-topo-hub-spoke-stroke-width={spokeStrokeWidth}
4163
+ data-topo-hub-spoke-stroke-width-active="2.25"
4164
+ data-topo-hub-spoke-linecap="round"
3643
4165
  style={{
3644
4166
  transition: 'stroke 250ms ease-out, stroke-width 250ms ease-out, opacity 250ms ease-out',
3645
4167
  ...(isActiveSpoke ? {} : {
@@ -3731,7 +4253,34 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3731
4253
  stroke={(isPinned || isHovered) ? pal.legendAccent : pal.ringStroke}
3732
4254
  strokeWidth={isPinned ? 3 : isHovered ? 2 : 1.5}
3733
4255
  strokeDasharray={(isPinned || isHovered) ? 'none' : '6 6'}
4256
+ /* Round 380 / Loop: cluster box stroke gets round
4257
+ linecap + round linejoin. Sibling SVG stroke-
4258
+ softening polish to R378 flow-rail linecap + R379
4259
+ minimap viewport linejoin — extends the family to
4260
+ the group cluster boundary box (grid layout only):
4261
+ R288 chrome icons strokeLinecap='round'
4262
+ R378 flow-rail dashes strokeLinecap='round'
4263
+ R380 group box dashes strokeLinecap='round' (this round)
4264
+ R379 viewport rect strokeLinejoin='round'
4265
+ R380 group box corners strokeLinejoin='round' (this round)
4266
+ Linecap rounds the R85 '6 6' marching-ants dash
4267
+ pills at rest — each 6 px dash gains a ~0.75 px
4268
+ round cap (sw=1.5 idle), reading as soft pills
4269
+ instead of sharp 6 × 1.5 px rectangles. Linejoin
4270
+ rounds the 4 sharp 90° corners (any state — solid
4271
+ or dashed); at sw=1.5 the join arc is ~0.75 px,
4272
+ matching R379 viewport vocabulary. Geometry-safe:
4273
+ stroke-* properties only affect paint, not bbox.
4274
+ The R51 sentinel 1.5/3 strokeWidth values stay
4275
+ intact (the overlap probe is gated to g[data-
4276
+ node], so this cluster-internal rect is invisible
4277
+ to it anyway). data-group-box-linecap + -linejoin
4278
+ attrs expose the values for tests. */
4279
+ strokeLinecap="round"
4280
+ strokeLinejoin="round"
3734
4281
  data-group-box-pinned={isPinned ? 'true' : 'false'}
4282
+ data-group-box-linecap="round"
4283
+ data-group-box-linejoin="round"
3735
4284
  // R85: ambient "marching ants" drift on the perimeter
3736
4285
  // when this group has at least one working member, and
3737
4286
  // neither pin nor hover is active (those treatments
@@ -3888,6 +4437,32 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3888
4437
  matches R142 fill timing so all the group-label
3889
4438
  state-flip channels (fill colour, rect stroke,
3890
4439
  rect drop-shadow, label tracking) ease as one. */}
4440
+ {/* Round 432 / Loop: extend the group-label letter-
4441
+ spacing tween from 2-tier (rest/pin) to 3-tier
4442
+ (rest/hover/pin → 0/0.25/0.5). Pre-R432 R218
4443
+ spread the text only on pin; hover got an
4444
+ R63 fill brighten (legendText → legendHeadline)
4445
+ but no typographic axis of its own. R432 adds
4446
+ the missing mid tier so hover telegraphs through
4447
+ BOTH the fill brighten AND a subtle kerning
4448
+ spread — sibling pattern to R427 node-alias
4449
+ (0/0.3/0.5) and R431 edge-badge (0/0.2/0.4) at
4450
+ group-label scope. Pin tier (0.5) still wins.
4451
+ Subtler mid tier (0.25 vs alias 0.3) because the
4452
+ group label is a structural anchor — too much
4453
+ spread would steal weight from the per-node
4454
+ alias identity it groups. Hover-letter-spacing
4455
+ family extension (8 anchors now):
4456
+ R344 chip count digit
4457
+ R345 panel title
4458
+ R347 active-links chip
4459
+ R351 vendor chip
4460
+ R420 zoom-level chip
4461
+ R427 node alias text
4462
+ R431 edge-badge digit
4463
+ R432 group label text (this round)
4464
+ R218 transition list ('fill 200ms, letter-spacing
4465
+ 200ms') untouched — additive conditional case. */}
3891
4466
  <text
3892
4467
  x={box.x + 12}
3893
4468
  y={box.y + 14}
@@ -3895,9 +4470,11 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3895
4470
  fontSize="13"
3896
4471
  fontFamily="monospace"
3897
4472
  fontWeight="700"
4473
+ data-group-label-hovered={isHovered && !isPinned ? 'true' : 'false'}
3898
4474
  style={{
3899
4475
  transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out',
3900
- letterSpacing: isPinned ? '0.5px' : '0px',
4476
+ letterSpacing: isPinned ? '0.5px' :
4477
+ isHovered ? '0.25px' : '0px',
3901
4478
  }}
3902
4479
  data-group-label={box.key}
3903
4480
  data-group-label-pinned={isPinned ? 'true' : 'false'}
@@ -3931,13 +4508,62 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3931
4508
  lives at a fixed dx=6 offset from the name, so a
3932
4509
  digit-width jitter at 9→10 used to shift the
3933
4510
  whole count visibly. Tabular locks it. */}
4511
+ {/* Round 366 / Loop: group label member-count tspan
4512
+ fontWeight 400 → 500. Sibling polish to R363
4513
+ recent-row alias text fw 400 → 500 + R364 legend-
4514
+ row label fw 400 → 500 — closes the per-row 'count
4515
+ is fw 500 against label-tier fw 700' pattern at
4516
+ the group-label scope (grid layout cluster mark).
4517
+ Hierarchy snapshot post-R366 across all 3 row
4518
+ surfaces:
4519
+ recent count(hot/cold) fw 700/600 (R320)
4520
+ recent alias fw 500 (R363)
4521
+ legend count fw 600 (R309)
4522
+ legend label fw 500 (R364)
4523
+ group name fw 700 (legacy)
4524
+ group count fw 500 (R366, this round)
4525
+ Monospace family + R225 tabular-nums lock digit
4526
+ width, so the fw bump is paint-only — bbox
4527
+ unchanged + overlap-test invariants hold. R229
4528
+ fill-inherit from parent label (hover-deepen-own-
4529
+ hue family) preserved. data-group-label-count-
4530
+ font-weight attr exposes the value for tests. */}
4531
+ {/* Round 444 / Loop: group label count tspan
4532
+ fontWeight 500 → 600 on isPinned. Extends the
4533
+ "data tightens under attention" typographic-
4534
+ weight pattern to a 5th anchor at the group-
4535
+ label-count scope:
4536
+ R416 chip-digit (chip hover)
4537
+ R424 panel-digit (panel hover)
4538
+ R425 hub-digit (hub hover)
4539
+ R426 edge-badge-digit (pin/hot)
4540
+ R444 group-label-count (pinned) ← this round
4541
+ Same idiom — when the group is locked, its
4542
+ member-count tightens typographically alongside
4543
+ the R432 letter-spacing spread (0 → 0.5px) on
4544
+ the parent label. Hover keeps rest fw (500) so
4545
+ the locked vs preview distinction at the type
4546
+ level stays intact — same gate R432 used.
4547
+ Monospace + R225 tabular-nums lock the digit
4548
+ width across fw changes; bbox unchanged; overlap-
4549
+ test invariants hold. transition list adds
4550
+ 'font-weight 200ms ease-out' matching R432
4551
+ letter-spacing cadence. R229 fill-inherit
4552
+ preserved (parent text fill still drives the
4553
+ hover/pin color). data-group-label-count-font-
4554
+ weight + -pinned attrs exposed for tests. */}
3934
4555
  <tspan
3935
4556
  dx="6"
3936
4557
  fontSize="11"
3937
- fontWeight="400"
4558
+ fontWeight={isPinned ? '600' : '500'}
3938
4559
  data-group-label-count={box.key}
3939
4560
  data-group-label-count-value={box.count}
3940
- style={{ fontVariantNumeric: 'tabular-nums' }}
4561
+ data-group-label-count-pinned={isPinned ? 'true' : 'false'}
4562
+ data-group-label-count-font-weight={isPinned ? '600' : '500'}
4563
+ style={{
4564
+ fontVariantNumeric: 'tabular-nums',
4565
+ transition: 'font-weight 200ms ease-out',
4566
+ }}
3941
4567
  >· {box.count}</tspan>
3942
4568
  {/* Round 58 / Loop: status mix pip strip. Compact text-
3943
4569
  based chips (e.g. "2w 1i") so the strip stays inside
@@ -4067,13 +4693,34 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4067
4693
  // synchronised per-edge animation set.
4068
4694
  const stagger = (index * 0.37) % duration;
4069
4695
  // Round 10 / Loop: freshness fade. An edge that fired ≤30s ago
4070
- // stays at full intensity; over 5 minutes it decays to ~35%.
4071
- // Surfaces "what's happening now" vs background chatter without
4072
- // hiding old flow entirely (some context still useful). `now`
4073
- // captured at useMemo-recompute time (every 5s message refresh)
4074
- // — accuracy is within the poll interval, plenty.
4696
+ // stays at full intensity; over 5 minutes it decays to a
4697
+ // floor. Surfaces "what's happening now" vs background
4698
+ // chatter without hiding old flow entirely (some context
4699
+ // still useful). `now` captured at useMemo-recompute time
4700
+ // (every 5s message refresh) — accuracy is within the poll
4701
+ // interval, plenty.
4702
+ //
4703
+ // Round 406 / Loop: edge freshness fade floor 0.35 → 0.40.
4704
+ // Stale-state legibility lift family (6th anchor) — pre-
4705
+ // R406 edges older than 5 minutes faded to α=0.35 (a 65 %
4706
+ // dim against full intensity). The decay rate is the same
4707
+ // 1 - ageMs/300s curve; only the FLOOR shifts. Sibling
4708
+ // treatment to:
4709
+ // R317 subordinate-text gray-500 → gray-400
4710
+ // R358 freshness ramp floor 0.25 → 0.30
4711
+ // R372 minimap offline-dot opacity 0.5 → 0.6
4712
+ // R404 hub-halo cyber trough 0.08 → 0.10
4713
+ // R405 hub-halo light trough 0.32 → 0.34
4714
+ // R406 edge freshness floor 0.35 → 0.40 (this round)
4715
+ // Edges past 5min now sit at 40% intensity instead of 35%
4716
+ // — they still recede against fresh edges but read
4717
+ // legibly enough to convey "this conversation existed".
4718
+ // ageMs threshold for the 5-minute decay unchanged; the
4719
+ // decay curve shape (linear) unchanged. The visual delta
4720
+ // is most pronounced on edges between 5-60 minutes old —
4721
+ // where the floor was binding pre-R406.
4075
4722
  const ageMs = link.last_at ? Math.max(0, Date.now() - Date.parse(link.last_at)) : 0;
4076
- const fresh = Math.max(0.35, 1 - ageMs / (5 * 60 * 1000));
4723
+ const fresh = Math.max(0.40, 1 - ageMs / (5 * 60 * 1000));
4077
4724
  // Round 16 arrow-tier binning — keep `topo-arrow` as the
4078
4725
  // medium tier id so the legend swatch picks it up unchanged.
4079
4726
  const arrowId = link.count <= 2 ? 'topo-arrow-s'
@@ -4138,7 +4785,27 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4138
4785
  // tracks it even at low message counts (width was 3 → 4.5 on
4139
4786
  // hover). 1.4× is enough to read as "lifted" without
4140
4787
  // breaching the 16-px hitbox bound.
4141
- const renderWidth = isHoveredEdge ? Math.min(width * 1.4, 10) : width;
4788
+ // Round 436 / Loop: extend the thickening to the
4789
+ // "hovered-endpoint" case — when hoveredAlias matches one
4790
+ // of this edge's endpoints, lift width by 1.15× (capped at
4791
+ // 8 px to stay clear of the 16-px hitbox). Pre-R436 the
4792
+ // edgeOpacityMul=1.7 (line 4703) lifted the matched edge's
4793
+ // OPACITY when an endpoint was hovered but the stroke-WIDTH
4794
+ // stayed at base — so the edge faded brighter without
4795
+ // thickening, leaving the paint+geometry axes mismatched.
4796
+ // Mirror of R430/R435 hub-spoke pattern (opacity + stroke-
4797
+ // width co-lift on hoveredAlias); R436 brings the same
4798
+ // dual-axis "this node's link" gesture to the edge scope.
4799
+ // 1.15× is subtler than isHoveredEdge=1.4× because
4800
+ // endpoint-hover lifts MANY edges at once (every edge
4801
+ // incident on the hovered node) while edge-hover lifts ONE
4802
+ // — the gesture should read as "highlighted" not "loud".
4803
+ // R166 stroke-width 300ms transition already in the
4804
+ // visible-path style list so the lift eases for free.
4805
+ const isEndpointHoveredEdge = !!hoveredAlias && (link.from === hoveredAlias || link.to === hoveredAlias);
4806
+ const renderWidth = isHoveredEdge ? Math.min(width * 1.4, 10)
4807
+ : isEndpointHoveredEdge ? Math.min(width * 1.15, 8)
4808
+ : width;
4142
4809
  return (
4143
4810
  <g
4144
4811
  key={link.key}
@@ -4225,30 +4892,89 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4225
4892
  online chips to splice in additional properties
4226
4893
  beside Tailwind's). data-edge-flow-rail attr
4227
4894
  surfaces the path for test introspection. */}
4895
+ {/* Round 381 / Loop: edge visible flow path picks up
4896
+ strokeLinecap='round'. Sibling polish to R378
4897
+ flow-rail dashed linecap — both flow-element paths
4898
+ (visible primary + dashed secondary rail) now share
4899
+ 'round' linecap vocabulary. The visible path runs
4900
+ source-node → dest-node as one continuous line, so
4901
+ the dest-end is covered by the markerEnd arrow and
4902
+ the source-end usually sits inside the source-node
4903
+ circle. At certain alignments (post-zoom, post-
4904
+ layout-switch transitions), the source-end may peek
4905
+ out by a fraction of a px past the node edge —
4906
+ round caps render that overshoot as a smooth half-
4907
+ disc instead of a sharp rectangle. Pure paint
4908
+ refinement, geometry-safe (bbox of the stroke
4909
+ unchanged at the join with the arrow marker).
4910
+ data-edge-visible-linecap attr exposes the value
4911
+ for tests. */}
4228
4912
  <path
4229
4913
  d={path}
4230
4914
  fill="none"
4231
4915
  stroke={pal.flowEdge}
4232
4916
  strokeWidth={renderWidth}
4917
+ strokeLinecap="round"
4233
4918
  opacity={Math.min(1, (isLight ? 0.22 : 0.28) * fresh * edgeOpacityMul)}
4234
4919
  filter={isLight ? undefined : 'url(#topo-glow)'}
4235
4920
  markerEnd={`url(#${arrowId})`}
4236
4921
  data-edge-visible={link.key}
4922
+ data-edge-visible-linecap="round"
4923
+ data-edge-visible-endpoint-hovered={isEndpointHoveredEdge ? 'true' : 'false'}
4924
+ data-edge-visible-stroke-width={renderWidth}
4237
4925
  style={{
4238
4926
  pointerEvents: 'none',
4239
4927
  transition: 'opacity 300ms ease-out, stroke-width 300ms ease-out, stroke 300ms ease-out',
4240
4928
  }}
4241
4929
  />
4930
+ {/* Round 378 / Loop: edge flow-path dashed-rail picks
4931
+ up strokeLinecap='round'. Pre-R378 the rail
4932
+ rendered '2 12' dashes as sharp 1×2 rectangles
4933
+ against the canvas backdrop; default 'butt' caps
4934
+ leave dash ends square. R378 rounds each cap so
4935
+ the dashes read as soft 3-px pills (1 px stroke +
4936
+ 0.5 px round cap each end). The flow-rail is the
4937
+ secondary 'invisible-spine' line that gives the
4938
+ R57 spoke flow a directional rail to slide along
4939
+ — rounding the dashes softens its presence
4940
+ against the primary visible flow path (R245 has
4941
+ no strokeLinecap so it inherits 'butt' on a
4942
+ continuous line, irrelevant). Geometry-safe:
4943
+ round caps only widen the visible dash; the
4944
+ bbox of the path is unchanged so overlap-test
4945
+ invariants hold. data-edge-flow-rail-linecap
4946
+ attr exposes the value for tests. */}
4242
4947
  <path
4243
4948
  id={`flow-path-${index}`}
4244
4949
  d={path}
4245
4950
  fill="none"
4246
4951
  stroke={pal.flowPath}
4247
- strokeWidth="1"
4952
+ /* Round 437 / Loop: flow-rail strokeWidth hover lift —
4953
+ 1 → 1.5 on (isHoveredEdge || isEndpointHoveredEdge).
4954
+ Pre-R437 the dashed rail sat at sw=1 always while the
4955
+ visible flow path above it lifted (R50 ×1.4 on
4956
+ isHoveredEdge / R436 ×1.15 on isEndpointHoveredEdge).
4957
+ The two edge paint layers were mismatched on hover:
4958
+ top layer thickened, underline stayed thin — so the
4959
+ hover gesture lifted only half the edge surface.
4960
+ R437 lifts the underline too so the whole edge
4961
+ reads as "raised" on hover, not just its bright
4962
+ top stripe. Same +0.5 absolute delta R435 used at
4963
+ hub-spoke scope (1→1.25 there, slightly bigger
4964
+ here because the rail's base 1 is at the kerning
4965
+ floor and needs more lift to register).
4966
+ Transition list extends to include stroke-width
4967
+ 300ms so the new lift eases under the same R166
4968
+ cadence as the visible path's stroke-width. */
4969
+ strokeWidth={(isHoveredEdge || isEndpointHoveredEdge) ? 1.5 : 1}
4248
4970
  strokeDasharray="2 12"
4971
+ strokeLinecap="round"
4249
4972
  opacity={Math.min(1, (isLight ? 0.4 : 0.75) * fresh * edgeOpacityMul)}
4250
4973
  data-edge-flow-rail={link.key}
4251
- style={{ transition: 'opacity 300ms ease-out, stroke 300ms ease-out' }}
4974
+ data-edge-flow-rail-linecap="round"
4975
+ data-edge-flow-rail-stroke-width={(isHoveredEdge || isEndpointHoveredEdge) ? 1.5 : 1}
4976
+ data-edge-flow-rail-lifted={(isHoveredEdge || isEndpointHoveredEdge) ? 'true' : 'false'}
4977
+ style={{ transition: 'opacity 300ms ease-out, stroke 300ms ease-out, stroke-width 300ms ease-out' }}
4252
4978
  />
4253
4979
  {!reducedMotion && (
4254
4980
  /* Round 103 / Loop: phase-stagger the particles so
@@ -4263,25 +4989,40 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4263
4989
  multiple). Edge order is stable (sorted by recent
4264
4990
  activity), so the offsets feel calm rather than
4265
4991
  reshuffling each refresh. */
4992
+ /* Round 422 / Loop: edge flow particle radius 4 → 4.5.
4993
+ Visual-weight bump family (15th anchor) — particles
4994
+ riding along the edge animateMotion path get +0.5px
4995
+ radius lift, increasing visual area by ~27%
4996
+ (π·4.5² / π·4² = 1.27). Sibling magnitude to R383
4997
+ recent-row pip 1.8 → 2.0 (+25% area), R384 minimap
4998
+ online dot 1.7 → 1.9 (+25% area). R251 fill +
4999
+ R252 transitions + R103 phase-stagger animateMotion
5000
+ all preserved. data-edge-particle-radius attr
5001
+ exposes the value for tests. */
4266
5002
  <circle
4267
- r="4"
5003
+ /* Round 439 / Loop: edge flow particle radius hover
5004
+ lift — r 4.5 → 5.5 on (isHoveredEdge ||
5005
+ isEndpointHoveredEdge). Continues edge paint-
5006
+ layer parity arc (R436 visible path sw / R437
5007
+ flow-rail sw / R439 particle r) so the whole
5008
+ edge surface — including the moving particle —
5009
+ lifts on hover, not just the static stripes.
5010
+ +1px radius gives ~50% area boost. Subtler than
5011
+ 1.4× sw bump on visible path because the
5012
+ particle is already small + motion-bright;
5013
+ +1px reads as "the dot caught attention"
5014
+ without overshadowing the path lift. R252
5015
+ transition list extends to include r 200ms so
5016
+ the size change eases under the same fill/
5017
+ opacity cadence. */
5018
+ r={(isHoveredEdge || isEndpointHoveredEdge) ? 5.5 : 4.5}
4268
5019
  fill={pal.flowParticle}
4269
5020
  filter={isLight ? undefined : 'url(#topo-glow)'}
4270
5021
  opacity={Math.min(1, fresh * edgeOpacityMul)}
4271
5022
  data-edge-particle={link.key}
4272
- /* Round 252 / Loop: particle picks up fill +
4273
- opacity transition for theme-toggle smoothing.
4274
- Pre-R252 pal.flowParticle (cyber #fef08a yellow
4275
- ↔ light #f59e0b amber) snapped on theme toggle
4276
- while every other edge element eased (R245
4277
- paths, R251 badge, R242 chat-target, R233
4278
- endpoint ring). opacity is freshness-driven so
4279
- it transitions per-frame as fresh decays anyway
4280
- — but adding opacity to the explicit transition
4281
- list also covers theme toggle (R3 className
4282
- transition-opacity duration-300 was previously
4283
- absent on this circle). */
4284
- style={{ transition: 'fill 200ms ease-out, opacity 200ms ease-out' }}
5023
+ data-edge-particle-radius={(isHoveredEdge || isEndpointHoveredEdge) ? 5.5 : 4.5}
5024
+ data-edge-particle-lifted={(isHoveredEdge || isEndpointHoveredEdge) ? 'true' : 'false'}
5025
+ style={{ transition: 'fill 200ms ease-out, opacity 200ms ease-out, r 200ms ease-out' }}
4285
5026
  >
4286
5027
  <animateMotion
4287
5028
  dur={`${duration}s`}
@@ -4612,14 +5353,137 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4612
5353
  R251 closes the per-edge surface theme-toggle
4613
5354
  smoothness — every theme-driven property on
4614
5355
  every edge element now eases under cyber↔light. */}
5356
+ {/* Round 367 / Loop: edge midpoint badge rest
5357
+ stroke-width 1 → 1.25. Sibling visual-weight
5358
+ bump family (7th canvas anchor now):
5359
+ R287 minimap viewport stroke 1 → 1.5
5360
+ R295 legend swatch base radius 5.5 → 6
5361
+ R359 recent-row pip base radius 1.6 → 1.8
5362
+ R360 hub digit fontSize 11 → 12
5363
+ R361 edge-badge digit fontSize 10 → 11
5364
+ R365 hub-highlight base radius 5 → 5.5
5365
+ R367 edge-badge rest stroke 1 → 1.25 (this round)
5366
+ Cold edge badges gain ~25 % stroke presence
5367
+ (1.25/1.0 = 1.25). Stays clear of the R51
5368
+ overlap-test sentinel values (1.5 / 3 reserved
5369
+ for node strokes — the test selector is gated
5370
+ to g[data-node] ancestors so this edge-internal
5371
+ circle is invisible to that probe anyway, but
5372
+ 1.25 is a safe non-sentinel value regardless).
5373
+ R188 transition list 'stroke-width 300ms ease-
5374
+ out' still smoothes the hot/pin flip — now
5375
+ 1.25 → 2 instead of 1 → 2, same ease pace.
5376
+ data-edge-badge-stroke-width-rest exposes the
5377
+ new baseline for tests. */}
5378
+ {/* Round 371 / Loop: edge-badge cyber opacity 0.82
5379
+ → 0.85. Sibling theme-consistency polish to R370
5380
+ hub hover-ring 0.7 → 0.8. R251 designed this
5381
+ badge with opacity 0.82 (cyber) / 0.95 (light)
5382
+ — 13 % delta. Cyber-theme dark bg needs more
5383
+ alpha to read as 'present'; R371 narrows the
5384
+ gap to 10 %, bringing the badge closer to light
5385
+ theme's 0.95 floor. Light stays at 0.95
5386
+ (already in the legibility band). data-edge-
5387
+ badge-opacity attr exposes the resolved value.
5388
+ Theme-consistency polish family:
5389
+ R246/R247 panel transition family
5390
+ R251 edge badge fill/opacity baseline
5391
+ R370 hub hover-ring cyber 0.7 → 0.8
5392
+ R371 edge badge cyber 0.82 → 0.85 (this round)
5393
+ R164 r=9/10.5 hover-lift + R188/R251 transition
5394
+ list + R367 strokeWidth=1.25 cold rest preserved. */}
5395
+ {/* Round 394 / Loop: edge-badge gains a hover
5396
+ strokeWidth tier (1.5) between cold rest
5397
+ (R367 1.25) and pin/hot (2). Pre-R394 the
5398
+ badge lifted only its radius on hover (R164
5399
+ 9 → 10.5); the stroke stayed at cold rest
5400
+ 1.25 unless pin/hot kicked in, so a plain
5401
+ hover felt half-lifted — geometry expanded
5402
+ while the contour stayed thin. R394 adds
5403
+ strokeWidth=1.5 on isHoveredEdge so hover
5404
+ now lifts both r AND stroke in concert —
5405
+ same pattern R385 used for the hub hover-
5406
+ ring (1.5 → 1.75) where the ring's three
5407
+ hover axes (r grow / opacity fade-in /
5408
+ stroke thicken) all rise together.
5409
+ Three-tier stroke hierarchy now:
5410
+ cold rest 1.25 (R367)
5411
+ hovered 1.5 (R394 — this round)
5412
+ pinned / hot 2.0 (R188)
5413
+ R51 sentinel concern: strokeWidth=1.5 is
5414
+ one of the two sentinels reserved for node
5415
+ detection, but the R51 selector is gated
5416
+ to `g[data-node]` ancestors so this edge-
5417
+ internal circle is invisible to the probe
5418
+ (same lesson R177 hub hover-ring + R367
5419
+ cold rest documented). 300ms strokeWidth
5420
+ transition already in the style list eases
5421
+ the new tier naturally. data-edge-badge-
5422
+ stroke-width-hover attr exposes the hover
5423
+ value for tests. */}
5424
+ {/* Round 395 / Loop: edge-badge gains a third
5425
+ hover axis — opacity 0.85 (cyber) / 0.95
5426
+ (light) → 1.0 on isHoveredEdge. Pre-R395
5427
+ hovering thickened the stroke (R394 1.25 →
5428
+ 1.5) and grew the radius (R164 9 → 10.5)
5429
+ but the badge's translucency stayed put at
5430
+ R371's rest alpha (cyber 0.85 / light 0.95).
5431
+ R395 lifts hover to a clean 1.0 — fully
5432
+ opaque — so the hovered badge reads as
5433
+ "in focus" against the dim siblings.
5434
+ Three-axis hover-lift parity now complete:
5435
+ hub hover-ring (R177/R370/R385):
5436
+ r 14 → 17, opacity 0 → 0.8 cyber, sw 1.5 → 1.75
5437
+ edge badge (R164/R394/R395):
5438
+ r 9 → 10.5, sw 1.25 → 1.5, opacity → 1.0
5439
+ 200ms opacity transition (already in the
5440
+ style list) eases the new axis naturally.
5441
+ R371 rest opacity (0.85 cyber / 0.95 light)
5442
+ preserved as the resting alpha — R395
5443
+ adds an isHoveredEdge override on top.
5444
+ data-edge-badge-opacity-hover attr exposes
5445
+ the hover value for tests. */}
5446
+ {/* Round 396 / Loop: extend the R395 opacity → 1.0
5447
+ lift to the pinned state. Pre-R396 the badge
5448
+ shared `r=10.5` on both hover AND pin (R164
5449
+ unified-lift) but R395's opacity lift fired
5450
+ ONLY on isHoveredEdge — pinned badges stayed
5451
+ at R371 rest alpha (cyber 0.85 / light 0.95).
5452
+ That left pin (sticky selection) reading
5453
+ softer than hover (transient preview), even
5454
+ though pin is the stronger commitment.
5455
+ R396 unifies hover + pin at opacity=1.0
5456
+ so the same data-edge-badge-lifted='true'
5457
+ surface uniformly carries full alpha. Pin
5458
+ stroke (R188 sw=2 + pal.legendHeadline color)
5459
+ continues to differentiate pin from hover —
5460
+ the opacity track now closes the lift parity.
5461
+ The new gate (isHoveredEdge || isPinned)
5462
+ mirrors the existing R164 r-lift gate, so
5463
+ the badge has a single "active state"
5464
+ signature across r + opacity.
5465
+ 200ms opacity transition (already in style
5466
+ list) eases pin/unpin naturally. R371 rest
5467
+ opacity preserved as the resting alpha.
5468
+ data-edge-badge-opacity-hover renamed
5469
+ semantically to -active (covers hover+pin)
5470
+ via the new -opacity-active attr; the
5471
+ legacy -opacity-hover attr kept for R395
5472
+ test compatibility. */}
4615
5473
  <circle
4616
5474
  cx={badgeX} cy={badgeY}
4617
5475
  r={isHoveredEdge || isPinned ? 10.5 : 9}
4618
5476
  fill={pal.legendBox.fill}
4619
5477
  stroke={isPinned ? pal.legendHeadline : isHot ? hotStroke : pal.flowEdge}
4620
- strokeWidth={isPinned ? 2 : isHot ? 2 : 1}
4621
- opacity={isLight ? 0.95 : 0.82}
5478
+ strokeWidth={isPinned ? 2 : isHot ? 2 : isHoveredEdge ? 1.5 : 1.25}
5479
+ opacity={(isHoveredEdge || isPinned) ? 1 : (isLight ? 0.95 : 0.85)}
4622
5480
  data-edge-badge-lifted={(isHoveredEdge || isPinned) ? 'true' : 'false'}
5481
+ data-edge-badge-stroke-width-rest="1.25"
5482
+ data-edge-badge-stroke-width-hover="1.5"
5483
+ data-edge-badge-opacity={(isHoveredEdge || isPinned) ? 1 : (isLight ? 0.95 : 0.85)}
5484
+ data-edge-badge-opacity-rest={isLight ? 0.95 : 0.85}
5485
+ data-edge-badge-opacity-hover="1"
5486
+ data-edge-badge-opacity-active="1"
4623
5487
  style={{ transition: 'r 180ms ease-out, stroke 300ms ease-out, stroke-width 300ms ease-out, fill 200ms ease-out, opacity 200ms ease-out' }}
4624
5488
  />
4625
5489
  {/* Round 224 / Loop: edge badge text gains the 4th
@@ -4658,16 +5522,75 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4658
5522
  x={badgeX} y={badgeY + 3}
4659
5523
  textAnchor="middle"
4660
5524
  fill={pal.legendHeadline}
4661
- fontSize="10"
5525
+ /* Round 361 / Loop: edge midpoint badge text
5526
+ fontSize 10 → 11. Sibling visual-weight bump
5527
+ to R360 hub digit 11 → 12. The badge digit
5528
+ is the per-edge equivalent of the hub digit
5529
+ — a high-information scalar (link.count) at
5530
+ a stable canvas position. Pre-R361 fontSize=
5531
+ 10 + R220 letter-spacing 0.4 + R224 tabular-
5532
+ nums made the digit READABLE but small
5533
+ against the r=9 / 18-px badge envelope;
5534
+ fontSize=11 nudges the glyph ~10 % bigger
5535
+ (bbox ~7×10 px from ~6×9 px) so the count
5536
+ reads more cleanly at glance — still well
5537
+ inside the r=9 idle circle and the r=10.5
5538
+ hover/pin lift (R164). y=badgeY+3 empirical
5539
+ vertical centring kept (1px drift at the
5540
+ bumped size is below the noise floor in
5541
+ the on-curve flow path).
5542
+ Visual-weight bump family:
5543
+ R287 minimap viewport stroke 1 → 1.5
5544
+ R295 legend swatch base radius 5.5 → 6
5545
+ R359 recent-row pip base radius 1.6 → 1.8
5546
+ R360 hub digit fontSize 11 → 12
5547
+ R361 edge-badge digit fontSize 10 → 11 (this round)
5548
+ data-edge-badge-text-font-size attr exposes
5549
+ the value for tests. R220 pin/hot letter-
5550
+ spacing tween + R224 tabular-nums + R188
5551
+ stroke-width pin/hot transitions all preserved. */
5552
+ fontSize="11"
4662
5553
  fontFamily="monospace"
4663
- fontWeight="700"
5554
+ /* R426 — edge-badge digit fontWeight 700 → 800 on
5555
+ (isPinned || isHot). 4th anchor on the "data
5556
+ tightens under attention" typographic-weight
5557
+ pattern:
5558
+ R416 chip-digit (chip-row hover trigger)
5559
+ R424 panel-digit (panel hover trigger)
5560
+ R425 hub-digit (hub hover trigger)
5561
+ R426 edge-badge-digit (pin/hot trigger) ← this
5562
+ The badge digit is the per-edge equivalent of
5563
+ the hub digit (R361 sibling fontSize bump
5564
+ reasoning). Stacks with R188 stroke-width pin/
5565
+ hot lift (1.25 → 1.5) + R220 letter-spacing pin/
5566
+ hot tween (0 → 0.4) for a 3-axis pin/hot signa-
5567
+ ture (edge structure + text spacing + text
5568
+ weight). The R408 transition is letter-spacing
5569
+ 300ms; R426 appends font-weight 300ms so the
5570
+ weight bump co-eases under the same cadence. */
5571
+ fontWeight={(isPinned || isHot) ? '800' : '700'}
4664
5572
  data-edge-badge-text={link.key}
4665
5573
  data-edge-badge-text-pin={(isPinned || isHot) ? 'true' : 'false'}
5574
+ data-edge-badge-text-font-size="11"
4666
5575
  style={{
4667
5576
  pointerEvents: 'none',
4668
5577
  fontVariantNumeric: 'tabular-nums',
4669
- letterSpacing: (isPinned || isHot) ? '0.4px' : '0px',
4670
- transition: 'letter-spacing 300ms ease-out',
5578
+ /* R431 — edge-badge digit 3-tier letter-spacing:
5579
+ rest 0 / isHoveredEdge 0.2 / (isPinned || isHot) 0.4.
5580
+ Mirrors R427 node-alias 3-tier (rest/hover/chat-
5581
+ target → 0/0.3/0.5) at edge-badge scope. Pre-R431
5582
+ letter-spacing only fired on pin/hot (R220) while
5583
+ pure edge hover lifted stroke (R394) + opacity
5584
+ (R395) + radius (R164) but left the text dead-
5585
+ typographic. R431 adds the missing typographic
5586
+ spacing axis to the edge-hover gesture so the
5587
+ text rises with the badge geometry. Pin/hot
5588
+ tier (0.4) still wins; hover is the mid step.
5589
+ Hover-letter-spacing family extension (7 anchors
5590
+ now): R344/R345/R347/R351/R420/R427/R431. */
5591
+ letterSpacing: (isPinned || isHot) ? '0.4px' :
5592
+ isHoveredEdge ? '0.2px' : '0px',
5593
+ transition: 'letter-spacing 300ms ease-out, font-weight 300ms ease-out',
4671
5594
  }}
4672
5595
  >{link.count}</text>
4673
5596
  </g>
@@ -4778,19 +5701,68 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4778
5701
  : workingCount <= 2 ? 1
4779
5702
  : workingCount <= 5 ? 2
4780
5703
  : 3;
5704
+ // Round 404 / Loop: hub-halo cyber trough opacity 0.08 →
5705
+ // 0.10. Pre-R404 the breath's low-point sat at α=0.08
5706
+ // cyber (per R84 family tuning) — the halo nearly faded
5707
+ // out at trough on the dark canvas. R404 lifts cyber
5708
+ // trough to 0.10. Per-bucket peak amplitudes [0.16/0.20/
5709
+ // 0.26/0.32] stay exactly tuned.
5710
+ //
5711
+ // Round 405 / Loop: hub-halo LIGHT trough 0.32 → 0.34 —
5712
+ // symmetric +0.02 lift to mirror R404's cyber treatment
5713
+ // across both themes. Pre-R405 only cyber got the lift
5714
+ // (R404 docstring noted "light already at the strong
5715
+ // end" as deliberate); but the cyber/light delta in
5716
+ // R404 was an inconsistency in the family pattern.
5717
+ // R405 closes the symmetry — both themes get +0.02
5718
+ // baseline lift, so the breath low-point reads with
5719
+ // matching confidence regardless of theme. Light peak
5720
+ // array [0.52/0.58/0.65/0.72] stays tuned.
5721
+ //
5722
+ // Stale-state legibility lift family (5 anchors now):
5723
+ // R317 subordinate-text gray-500 → gray-400
5724
+ // R358 freshness floor 0.25 → 0.30
5725
+ // R372 minimap offline-dot opacity 0.5 → 0.6
5726
+ // R404 hub-halo cyber trough 0.08 → 0.10
5727
+ // R405 hub-halo light trough 0.32 → 0.34 (this round)
5728
+ //
5729
+ // R84 per-bucket peak/dur + R245 ease-in-out spline
5730
+ // keySplines all preserved. Test fixture probes the
5731
+ // SMIL <animate> values via data-topo-hub-halo-trough
5732
+ // attr (now exposes both light + cyber resolved values).
4781
5733
  const peakLight = [0.52, 0.58, 0.65, 0.72][busy];
4782
5734
  const peakDark = [0.16, 0.20, 0.26, 0.32][busy];
4783
- const troughLight = 0.32;
4784
- const troughDark = 0.08;
5735
+ const troughLight = 0.34;
5736
+ const troughDark = 0.10;
4785
5737
  const dur = [4.0, 3.2, 2.7, 2.4][busy];
4786
5738
  const valuesLight = `${troughLight};${peakLight};${troughLight}`;
4787
5739
  const valuesDark = `${troughDark};${peakDark};${troughDark}`;
5740
+ // Round 408 / Loop: hub-halo radius 18 → 20. The
5741
+ // grounding halo (the breathing outer circle around
5742
+ // the hub center) is the canvas's signature breath
5743
+ // element — R84 family. R408 bumps r=18 → 20 so the
5744
+ // breath extends slightly further while keeping 4px
5745
+ // clearance before the spoke origin (still room for
5746
+ // spoke start anchors). Visual presence on the
5747
+ // canvas focal point lifts ~23% area (π·20²/π·18²
5748
+ // = 1.23) without changing the per-bucket opacity
5749
+ // envelope or breath rhythm. Visual-weight bump
5750
+ // family 13th anchor — pairs with R404/R405 trough
5751
+ // lifts so the halo now breathes both with more
5752
+ // visible amplitude AND more visual footprint.
5753
+ // R84 per-bucket peak/dur invariants + R244 calc-
5754
+ // Mode='spline' + R245 ease-in-out keySplines all
5755
+ // preserved. data-topo-hub-halo-radius attr exposes
5756
+ // value for tests.
4788
5757
  return (
4789
5758
  <circle
4790
- cx={cx} cy={cy} r="18"
5759
+ cx={cx} cy={cy} r="20"
4791
5760
  fill={isLight ? '#d1fae5' : '#10b981'}
4792
5761
  opacity={isLight ? 0.42 : 0.12}
4793
5762
  data-hub-busyness={busy}
5763
+ data-topo-hub-halo-radius="20"
5764
+ data-topo-hub-halo-trough={isLight ? troughLight : troughDark}
5765
+ data-topo-hub-halo-peak={isLight ? peakLight : peakDark}
4794
5766
  /* Round 253 / Loop: hub grounding halo fill transition
4795
5767
  for theme toggle. Pre-R253 the base fill (cyber
4796
5768
  #10b981 ↔ light #d1fae5) snapped while R244's SMIL
@@ -4843,12 +5815,45 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4843
5815
  attr added for test introspection (the parent <g> at
4844
5816
  line 3587 has data-topo-hub but the core specifically
4845
5817
  is the canvas anchor). */}
4846
- <circle
4847
- cx={cx} cy={cy} r="10"
4848
- fill={isLight ? '#059669' : '#10b981'}
4849
- data-topo-hub-core
4850
- style={{ transition: 'fill 200ms ease-out' }}
4851
- />
5818
+ {(() => {
5819
+ /* Round 441 / Loop: hub center core fill brighten on
5820
+ hoveredHub. Pre-R441 the core was static (cyber
5821
+ emerald-500 #10b981 / light emerald-600 #059669) and
5822
+ the hub-hover gesture lifted ring radius (R177) +
5823
+ digit scale (R209) + digit fw (R425) + halo opacity
5824
+ (R370) + highlight opacity (R386) but the focal core
5825
+ ITSELF stayed planted at rest tone. R441 shifts the
5826
+ fill one emerald tier brighter on hover so the canvas
5827
+ anchor itself responds:
5828
+ cyber emerald-500 → emerald-400 (#10b981 → #34d399)
5829
+ light emerald-600 → emerald-500 (#059669 → #10b981)
5830
+ Same +100 step on the emerald scale across both themes.
5831
+ Pure paint axis; no geometry change. R248 fill 200ms
5832
+ transition already in the style list eases the shift.
5833
+ Closes the chroma axis on the hub-hover gesture stack:
5834
+ R177 ring radius lift geometry
5835
+ R209 digit scale 1.08 geometry
5836
+ R425 digit fw 700 → 800 typography
5837
+ R370 halo opacity 0.7 → 0.8 paint
5838
+ R386 highlight opacity paint
5839
+ R441 core fill brighten chroma ← this round
5840
+ data-topo-hub-core-hovered + -fill attrs exposed
5841
+ for tests. */
5842
+ const isCoreHovered = !reducedMotion && hoveredHub;
5843
+ const coreFill = isLight
5844
+ ? (isCoreHovered ? '#10b981' : '#059669')
5845
+ : (isCoreHovered ? '#34d399' : '#10b981');
5846
+ return (
5847
+ <circle
5848
+ cx={cx} cy={cy} r="10"
5849
+ fill={coreFill}
5850
+ data-topo-hub-core
5851
+ data-topo-hub-core-hovered={isCoreHovered ? 'true' : 'false'}
5852
+ data-topo-hub-core-fill={coreFill}
5853
+ style={{ transition: 'fill 200ms ease-out' }}
5854
+ />
5855
+ );
5856
+ })()}
4852
5857
  {/* R130 / Loop: when workingCount > 0, the decorative inner
4853
5858
  highlight gets replaced with the workingCount digit. The
4854
5859
  R84 busyness breath already encodes the same metric
@@ -4889,11 +5894,40 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4889
5894
  textAnchor="middle"
4890
5895
  dy="0.36em"
4891
5896
  fill={isLight ? '#d1fae5' : '#ecfdf5'}
4892
- fontSize="11"
5897
+ /* Round 360 / Loop: hub working-count digit fontSize 11
5898
+ → 12. The hub is the canvas's focal point — its digit
5899
+ is the most-read scalar on the whole topology. R130
5900
+ sized it at 11 (well inside the r=10 / 20-px core);
5901
+ R360 nudges it to 12 (~13 px wide × 12 px tall, still
5902
+ well inside the 20-px diameter) for ~9 % more presence.
5903
+ Sibling visual-weight bump family:
5904
+ R287 minimap viewport stroke 1 → 1.5
5905
+ R295 legend swatch base radius 5.5 → 6
5906
+ R359 recent-row pip radius 1.6 → 1.8
5907
+ R360 hub digit fontSize 11 → 12 (this round)
5908
+ The R209 scale-1.08-on-hub-hover, R225 tabular-nums,
5909
+ R253 fill transition, R213 always-mount opacity gate
5910
+ all preserved. data-topo-hub-working-count-font-size
5911
+ attr exposes the value for tests. */
5912
+ fontSize="12"
4893
5913
  fontFamily="monospace"
4894
- fontWeight="700"
5914
+ /* R425 — hub digit fontWeight 700 → 800 on hoveredHub.
5915
+ Closes the "data tightens under attention" pattern
5916
+ across three focal scopes: chip-digit (R416, chip
5917
+ scope) → panel-digit (R424, panel-header scope) →
5918
+ hub-digit (R425, hub focal scope). The hub digit is
5919
+ the most-read scalar on the topology; adding a weight
5920
+ axis on hover stacks with the R209 scale-1.08 + R177
5921
+ ring grow + R370 halo opacity + R386 highlight
5922
+ opacity hub-hover gestures, giving the focal point
5923
+ a typographic axis alongside its scale/structure cues.
5924
+ R360 fontSize=12 + R225 tabular-nums + R209 scale +
5925
+ R253 fill transition all preserved. Transition list
5926
+ extends to include font-weight 200ms ease-out. */
5927
+ fontWeight={hoveredHub ? '800' : '700'}
4895
5928
  opacity={workingCount > 0 ? 1 : 0}
4896
5929
  data-topo-hub-working-count={workingCount}
5930
+ data-topo-hub-working-count-font-size="12"
4897
5931
  data-topo-hub-working-count-hovered={hoveredHub ? 'true' : 'false'}
4898
5932
  data-topo-hub-working-count-visible={workingCount > 0 ? 'true' : 'false'}
4899
5933
  // Round 209 / Loop: hub workingCount digit scales 1.0 →
@@ -4927,19 +5961,63 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4927
5961
  transform: !reducedMotion && hoveredHub ? 'scale(1.08)' : 'scale(1)',
4928
5962
  transformBox: 'fill-box',
4929
5963
  transformOrigin: 'center',
4930
- transition: 'transform 200ms ease-out, opacity 300ms ease-out, fill 200ms ease-out',
5964
+ /* R425: font-weight 200ms appended so the hover fw
5965
+ bump 700 → 800 eases under the same cadence as
5966
+ R209 scale + R253 fill + R213 opacity. */
5967
+ transition: 'transform 200ms ease-out, opacity 300ms ease-out, fill 200ms ease-out, font-weight 200ms ease-out',
4931
5968
  fontVariantNumeric: 'tabular-nums',
4932
5969
  }}
4933
5970
  >
4934
5971
  {workingCount}
4935
5972
  </text>
4936
5973
  {/* decorative highlight (visible when workingCount === 0) */}
5974
+ {/* Round 365 / Loop: hub-center 'lit-lamp' decorative highlight
5975
+ circle r 5 → 5.5. Sibling visual-weight bump family —
5976
+ each round lifts one canvas anchor's geometric presence
5977
+ without disturbing its bbox envelope:
5978
+ R287 minimap viewport stroke 1 → 1.5
5979
+ R295 legend swatch base radius 5.5 → 6
5980
+ R359 recent-row pip base radius 1.6 → 1.8
5981
+ R360 hub digit fontSize 11 → 12
5982
+ R361 edge-badge digit fontSize 10 → 11
5983
+ R365 hub-highlight base radius 5 → 5.5 (this round)
5984
+ The highlight only renders when workingCount === 0
5985
+ (decorative 'lamp lit but idle' state per R130 + R213
5986
+ always-mount opacity-gate). At idle, the 0.5-px radius
5987
+ bump (21 % area, π*5.5² / π*5² = 1.21) lifts the lamp's
5988
+ presence — still well inside the r=10 hub-core (R130).
5989
+ opacity=0 when working preserved so the hub-digit's R130
5990
+ takeover stays seamless. R213 always-mount opacity-gate
5991
+ + 300ms opacity transition + pointerEvents:none all
5992
+ preserved. data-topo-hub-highlight-radius attr exposes
5993
+ the value for tests. */}
5994
+ {/* Round 386 / Loop: hub-highlight idle opacity 0.9 → 0.95.
5995
+ When workingCount===0 the highlight paints as the visible
5996
+ idle "lamp lit but no work" core (R130 takeover gate).
5997
+ Pre-R386 idle opacity was 0.9 — a ~6 % fade against full
5998
+ paint that read as slightly-dimmed-ghost on the focal
5999
+ point. R386 lifts to 0.95 (idle alpha gap halved 0.10
6000
+ → 0.05) so the canvas anchor reads more confidently
6001
+ as a present-but-idle state rather than a faded ghost.
6002
+ Theme-consistency / canvas-presence polish family (4th
6003
+ anchor):
6004
+ R370 hub hover-ring opacity 0.7 → 0.8 cyber
6005
+ R371 edge-badge rest opacity 0.82 → 0.85 cyber
6006
+ R372 minimap offline-dot opacity 0.5 → 0.6
6007
+ R386 hub-highlight idle opacity 0.9 → 0.95 (this round)
6008
+ opacity=0 when working preserved so the hub-digit's
6009
+ R130 takeover stays seamless. 300ms opacity transition
6010
+ + R213 always-mount opacity-gate + pointerEvents:none
6011
+ + R365 r=5.5 all preserved. data-topo-hub-highlight-
6012
+ opacity attr exposes the resolved value for tests. */}
4937
6013
  <circle
4938
- cx={cx} cy={cy} r="5"
6014
+ cx={cx} cy={cy} r="5.5"
4939
6015
  fill="#d1fae5"
4940
- opacity={workingCount > 0 ? 0 : 0.9}
6016
+ opacity={workingCount > 0 ? 0 : 0.95}
4941
6017
  data-topo-hub-highlight
4942
6018
  data-topo-hub-highlight-visible={workingCount > 0 ? 'false' : 'true'}
6019
+ data-topo-hub-highlight-radius="5.5"
6020
+ data-topo-hub-highlight-opacity={workingCount > 0 ? 0 : 0.95}
4943
6021
  style={{
4944
6022
  pointerEvents: 'none',
4945
6023
  transition: 'opacity 300ms ease-out',
@@ -4970,15 +6048,46 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4970
6048
  R142 group boxes, R143/R144 rows, R164 edge badges,
4971
6049
  R177 hub ring). prefers-reduced-motion respected via
4972
6050
  R29 globals.css blanket. */}
6051
+ {/* Round 385 / Loop: hub hover-ring strokeWidth 1.5 → 1.75.
6052
+ Sibling visual-weight bump (11th anchor) to R367 edge-
6053
+ badge rest stroke 1 → 1.25. The ring is only visible
6054
+ during hub hover (opacity=0 rest, R177 + R370 control
6055
+ the hover-state alpha) so the change manifests purely
6056
+ as a thicker hover-state ring on the canvas focal
6057
+ point. R177 r 14 → 17 grow + R370 opacity 0 → 0.8
6058
+ already lift the hover cue; R385 adds stroke weight
6059
+ as the third lift axis. Stays clear of R51 overlap-
6060
+ test sentinel value 3 (1.75 is non-sentinel); the
6061
+ R51 selector is gated to g[data-node] ancestors so
6062
+ this hub-internal circle is invisible to the probe
6063
+ regardless. R253 stroke transition + pointerEvents:
6064
+ none preserved. data-topo-hub-hover-ring-stroke-width
6065
+ attr exposes the value for tests. */}
4973
6066
  <circle
4974
6067
  cx={cx} cy={cy}
4975
6068
  r={hoveredHub ? 17 : 14}
4976
6069
  fill="none"
4977
6070
  stroke={isLight ? '#059669' : '#10b981'}
4978
- strokeWidth="1.5"
4979
- opacity={hoveredHub ? (isLight ? 0.85 : 0.7) : 0}
6071
+ strokeWidth="1.75"
6072
+ /* Round 370 / Loop: hub hover-ring cyber opacity 0.7 →
6073
+ 0.8. R177 designed the hub hover-ring at opacity-0 →
6074
+ 0.85 (light) / 0 → 0.7 (cyber). The 15 % gap between
6075
+ the two themes meant cyber-theme operators got a
6076
+ noticeably softer hover cue than light-theme users
6077
+ against backgrounds that should equalise (dark bg
6078
+ needs more luminance to read as 'on'). R370 bumps
6079
+ cyber 0.7 → 0.8, narrowing the theme gap to 5 % —
6080
+ sibling theme-consistency polish to R251 edge badge
6081
+ fill/opacity (cyber 0.82 / light 0.95) and R246/R247
6082
+ panel transition families. Light theme 0.85 stays
6083
+ as is (already in the legibility band). data-topo-
6084
+ hub-hover-ring-opacity attr exposes the value for
6085
+ tests. */
6086
+ opacity={hoveredHub ? (isLight ? 0.85 : 0.8) : 0}
4980
6087
  data-topo-hub-hover-ring
4981
6088
  data-topo-hub-hover-ring-radius={hoveredHub ? 17 : 14}
6089
+ data-topo-hub-hover-ring-stroke-width="1.75"
6090
+ data-topo-hub-hover-ring-opacity={hoveredHub ? (isLight ? 0.85 : 0.8) : 0}
4982
6091
  /* Round 253 / Loop: hub hover ring also gets stroke
4983
6092
  transition for theme toggle (cyber #10b981 ↔ light
4984
6093
  #059669). The opacity + r transitions stay for hover
@@ -5407,14 +6516,51 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
5407
6516
  keyTimes="0;0.5;1"
5408
6517
  keySplines="0.42 0 0.58 1;0.42 0 0.58 1"
5409
6518
  />
6519
+ {/* Round 409 / Loop: active-node pulse peak
6520
+ opacity lift — cyber 0.18 → 0.20 / light
6521
+ 0.12 → 0.14. Theme-consistency / canvas-
6522
+ presence family 9th anchor. R243 family
6523
+ rhythm preserved.
6524
+ Round 413 / Loop: trough lift mirrors R409
6525
+ peak — cyber 0.04 → 0.05 / light 0.02 →
6526
+ 0.03. Stale-state legibility lift family
6527
+ 8th anchor — pairs with R404 (hub-halo
6528
+ cyber trough 0.08 → 0.10) and R405 (light
6529
+ trough 0.32 → 0.34). The per-node breath's
6530
+ low-point now reads slightly above the
6531
+ "nearly gone" zone while preserving the
6532
+ breath amplitude (cyber Δ 0.16 vs Δ pre-
6533
+ R409+R413 of 0.14; light Δ 0.11 vs 0.10).
6534
+ Both peak (R409) AND trough (R413) lift
6535
+ together so the active-pulse signal stays
6536
+ confidently present at both ends of its
6537
+ 2.4s cycle.
6538
+ Stale-state legibility lift family (8):
6539
+ R317 subordinate-text gray-500→400
6540
+ R358 freshness floor 0.25→0.30
6541
+ R372 minimap offline-dot 0.5→0.6
6542
+ R404 hub-halo cyber trough 0.08→0.10
6543
+ R405 hub-halo light trough 0.32→0.34
6544
+ R406 edge freshness floor 0.35→0.40
6545
+ R407 node halo offline opacity (cyber+light)
6546
+ R413 active-node pulse trough (this round)
6547
+ cyber 0.04 → 0.05
6548
+ light 0.02 → 0.03
6549
+ R243 always-mount opacity-gate + R243
6550
+ ease-in-out keySplines + r animation
6551
+ (radius+8 ↔ radius+22) preserved.
6552
+ data-node-pulse-peak + new -pulse-trough
6553
+ attrs expose resolved per-theme values. */}
5410
6554
  <animate
5411
6555
  attributeName="opacity"
5412
- values={isLight ? '0.12;0.02;0.12' : '0.18;0.04;0.18'}
6556
+ values={isLight ? '0.14;0.03;0.14' : '0.20;0.05;0.20'}
5413
6557
  dur="2.4s"
5414
6558
  repeatCount="indefinite"
5415
6559
  calcMode="spline"
5416
6560
  keyTimes="0;0.5;1"
5417
6561
  keySplines="0.42 0 0.58 1;0.42 0 0.58 1"
6562
+ data-node-pulse-peak={isLight ? '0.14' : '0.20'}
6563
+ data-node-pulse-trough={isLight ? '0.03' : '0.05'}
5418
6564
  />
5419
6565
  </circle>
5420
6566
  </g>
@@ -5459,12 +6605,78 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
5459
6605
  parent stays for status flips. data-node-halo-
5460
6606
  breath-offset surfaces the chosen offset for
5461
6607
  test introspection. */}
6608
+ {/* Round 407 / Loop: offline node halo opacity lift —
6609
+ cyber 0.25 → 0.30 and light 0.4 → 0.45. Pre-R407
6610
+ offline node halos faded to α=0.25 cyber (75 %
6611
+ dim) / α=0.4 light. On the dark canvas the 0.25
6612
+ halo read as "nearly gone" — exactly the
6613
+ legibility floor R404/R405 just lifted on the
6614
+ hub-halo and R372 lifted on minimap offline dots.
6615
+ R407 closes the same family at the per-node halo
6616
+ surface: +0.05 lift on both themes so offline
6617
+ anchors stay legibly present without crossing into
6618
+ "could be online" territory (online cyber 0.65 /
6619
+ light 0.85 unchanged — the 0.30/0.65 cyber ratio
6620
+ still gives 2.17× contrast for online/offline).
6621
+ Stale-state legibility lift family (7 anchors now):
6622
+ R317 subordinate-text gray-500 → gray-400
6623
+ R358 freshness floor 0.25 → 0.30
6624
+ R372 minimap offline-dot 0.5 → 0.6
6625
+ R404 hub-halo cyber trough 0.08 → 0.10
6626
+ R405 hub-halo light trough 0.32 → 0.34
6627
+ R406 edge freshness floor 0.35 → 0.40
6628
+ R407 node halo offline opacity (this round)
6629
+ cyber 0.25 → 0.30
6630
+ light 0.4 → 0.45
6631
+ R278 retired-breath gate + R12 status.halo color
6632
+ + R226 phase stagger code-path preserved (the
6633
+ breath stays disabled per Vincent's R278 ask;
6634
+ only the BASE opacity floor shifts here). transi-
6635
+ tion list ('fill,opacity' 300ms ease-out) unchanged.
6636
+ data-node-halo-offline-opacity attr exposes the
6637
+ resolved value for tests. */}
6638
+ {(() => {
6639
+ /* Round 440 / Loop: node halo opacity hover lift —
6640
+ lifts toward full on the matched node. Pure paint
6641
+ axis: rest values unchanged for un-hovered halos,
6642
+ hover state lifts the matched halo's alpha by
6643
+ +0.15 on each tier:
6644
+ online cyber 0.65 → 0.80
6645
+ online light 0.85 → 1.00 (capped)
6646
+ offline cyber 0.30 → 0.45
6647
+ offline light 0.45 → 0.60
6648
+ Same paint-only mental model as R430 hub-spoke
6649
+ opacity lift + R429 label-card body opacity lift,
6650
+ now at the per-node halo scope. No geometry
6651
+ change so R51 sentinels stay safe and the overlap-
6652
+ test invariant is unchanged (test runs at rest).
6653
+ Closes a chroma/presence axis on the per-node
6654
+ hover signature alongside the 12-layer cue stack
6655
+ (R26/R217/R142/R427/R428/R429 card + R430/R435/
6656
+ R436/R437/R94 link + R438 ring). R407 offline
6657
+ halo opacity floor (cyber 0.30 / light 0.45) is
6658
+ the rest branch unchanged. Existing transition-
6659
+ [fill,opacity] duration-300 className handles
6660
+ the easing. data-node-halo-hovered exposes the
6661
+ gate; data-node-halo-resolved-opacity exposes
6662
+ the four-state resolved value for tests. */
6663
+ const isHaloHovered = !reducedMotion && hoveredAlias === session.alias;
6664
+ const haloOpacity = (() => {
6665
+ if (isOnline) {
6666
+ return isLight ? (isHaloHovered ? 1 : 0.85) : (isHaloHovered ? 0.80 : 0.65);
6667
+ }
6668
+ return isLight ? (isHaloHovered ? 0.60 : 0.45) : (isHaloHovered ? 0.45 : 0.30);
6669
+ })();
6670
+ return (
5462
6671
  <circle
5463
6672
  cx={pos.x}
5464
6673
  cy={pos.y}
5465
6674
  r={radius + 8}
5466
6675
  fill={status.halo}
5467
- opacity={isOnline ? (isLight ? 0.85 : 0.65) : (isLight ? 0.4 : 0.25)}
6676
+ opacity={haloOpacity}
6677
+ data-node-halo-offline-opacity={isOnline ? undefined : (isLight ? 0.45 : 0.30)}
6678
+ data-node-halo-hovered={isHaloHovered ? 'true' : 'false'}
6679
+ data-node-halo-resolved-opacity={haloOpacity}
5468
6680
  className="transition-[fill,opacity] duration-300 ease-out"
5469
6681
  data-node-halo-breath={!reducedMotion && session.status === 'working' ? 'on' : 'off'}
5470
6682
  data-node-halo-breath-offset={
@@ -5523,6 +6735,8 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
5523
6735
  />
5524
6736
  )}
5525
6737
  </circle>
6738
+ );
6739
+ })()}
5526
6740
  {/* Round 111 / Loop: edge-endpoint emphasis ring. R49
5527
6741
  already keeps endpoint nodes at opacity 1 while
5528
6742
  others dim when an edge is hovered, but the
@@ -5579,11 +6793,37 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
5579
6793
  ring stroke-width). data-edge-endpoint-ring-
5580
6794
  stroke-width attr surfaces the chosen value for
5581
6795
  test introspection. */
6796
+ /* Round 442 / Loop: endpoint emphasis ring radius
6797
+ hover lift — r=radius+7 → radius+8 on isEndpoint,
6798
+ closing a 3-axis hover-elevation parity at endpoint
6799
+ ring scope (r + sw + opacity):
6800
+ opacity R182 0 → 0.85/0.9
6801
+ sw R233 1.6 → 2.4
6802
+ r R442 +7 → +8 ← this round
6803
+ Mirrors the 3-axis trios already established at
6804
+ hub hover-ring (R177/R370/R385) and edge badge
6805
+ (R164/R394/R395). Pre-R442 the endpoint ring
6806
+ faded in + thickened on edge-hover but its radius
6807
+ stayed locked at radius+7 — only the paint/weight
6808
+ axes lifted while the GEOMETRY stayed unchanged.
6809
+ +1px (~radius+7 to radius+8) gives a subtle outward
6810
+ pulse on hover without crowding the status ring
6811
+ (which sits at radius from R438 sw3.5 hover) or
6812
+ the halo (radius+8 from R440 opacity hover — the
6813
+ endpoint ring sits at the SAME radius as the halo
6814
+ but with stroke=cyan vs fill=status.halo so they
6815
+ don't visually collide). The transition list
6816
+ extends to include 'r 180ms ease-out' so the new
6817
+ axis eases under the same R233 cadence. SVG `r`
6818
+ on a <circle> uses CSS-property syntax for inter-
6819
+ polation (same idiom R197/R198 used on the
6820
+ legend swatch). data-edge-endpoint-ring-radius
6821
+ attr exposes the resolved value for tests. */
6822
+ const endpointR = isEndpoint ? radius + 8 : radius + 7;
5582
6823
  return (
5583
6824
  <circle
5584
6825
  cx={pos.x}
5585
6826
  cy={pos.y}
5586
- r={radius + 7}
5587
6827
  fill="none"
5588
6828
  stroke={pal.flowEdge}
5589
6829
  strokeWidth={isEndpoint ? 2.4 : 1.6}
@@ -5591,7 +6831,12 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
5591
6831
  data-edge-endpoint-ring
5592
6832
  data-edge-endpoint-active={isEndpoint ? 'true' : 'false'}
5593
6833
  data-edge-endpoint-ring-stroke-width={isEndpoint ? 2.4 : 1.6}
5594
- style={{ pointerEvents: 'none', transition: 'opacity 180ms ease-out, stroke-width 180ms ease-out' }}
6834
+ data-edge-endpoint-ring-radius={endpointR}
6835
+ style={{
6836
+ pointerEvents: 'none',
6837
+ r: `${endpointR}px`,
6838
+ transition: 'opacity 180ms ease-out, stroke-width 180ms ease-out, r 180ms ease-out',
6839
+ } as React.CSSProperties}
5595
6840
  />
5596
6841
  );
5597
6842
  })()}
@@ -5616,20 +6861,62 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
5616
6861
  stroke-width="3"/"1.5" still works against the
5617
6862
  DOM attribute value (React-rendered, not
5618
6863
  interpolated). */}
5619
- <circle
5620
- cx={pos.x}
5621
- cy={pos.y}
5622
- r={radius}
5623
- fill={isOnline ? pal.nodeFill.online : pal.nodeFill.offline}
5624
- stroke={status.primary}
5625
- strokeWidth={isOnline ? 3 : 1.5}
5626
- strokeDasharray={isOnline ? 'none' : '5 5'}
5627
- filter={isOnline && !isLight ? 'url(#topo-glow)' : undefined}
5628
- data-node-status-ring={status.label}
5629
- style={{
5630
- transition: 'fill 300ms ease-out, stroke 300ms ease-out, stroke-width 300ms ease-out',
5631
- }}
5632
- />
6864
+ {(() => {
6865
+ /* Round 438 / Loop: status ring strokeWidth hover lift —
6866
+ when hoveredAlias matches, the node's status ring
6867
+ thickens by +0.5: online 3 → 3.5, offline 1.5 → 2.
6868
+ Same absolute delta as R435 hub-spoke (idle 1→1.25
6869
+ used Δ +0.25 because rest base was thinner; status
6870
+ ring's heavier rest values 1.5/3 need a bigger
6871
+ +0.5 to register as visible thickening).
6872
+ Status-ring axis joins the node-hover cue stack
6873
+ (now 9 layers including link surfaces):
6874
+ R26 group translateY -2px per-node geometry
6875
+ R217 stroke tint legendAccent per-node card
6876
+ R142 drop-shadow boost per-node card
6877
+ R427 alias letter-spacing per-node text
6878
+ R428 sub-text letter-spacing per-node text
6879
+ R429 body opacity 0.94 → 1.0 per-node card
6880
+ R430 hub-spoke α+ link to hub (paint)
6881
+ R435 hub-spoke sw+ link to hub (geo)
6882
+ R94 edge α 1.7× inter-node link (paint)
6883
+ R436 edge sw 1.15× inter-node link (geo)
6884
+ R437 flow-rail sw 1 → 1.5 edge paint-layer
6885
+ R438 status-ring sw +0.5 ring geometry ← this round
6886
+ R51 sentinel safety: rest values 3 / 1.5 unchanged
6887
+ so the overlap-test selector `circle[stroke-width=
6888
+ "3"]` / `circle[stroke-width="1.5"]` inside
6889
+ g[data-node] still matches at rest. Hover values
6890
+ 3.5 / 2 are not in the reserved {1.5, 3} set so
6891
+ the selector wouldn't match them anyway; but the
6892
+ test runs WITHOUT hover so this never matters
6893
+ in practice. R167 stroke-width 300ms transition
6894
+ already in the style list eases the lift for
6895
+ free. data-node-status-ring-hovered exposes the
6896
+ gate for tests. */
6897
+ const isRingHovered = !reducedMotion && hoveredAlias === session.alias;
6898
+ const ringStrokeWidth = isOnline
6899
+ ? (isRingHovered ? 3.5 : 3)
6900
+ : (isRingHovered ? 2 : 1.5);
6901
+ return (
6902
+ <circle
6903
+ cx={pos.x}
6904
+ cy={pos.y}
6905
+ r={radius}
6906
+ fill={isOnline ? pal.nodeFill.online : pal.nodeFill.offline}
6907
+ stroke={status.primary}
6908
+ strokeWidth={ringStrokeWidth}
6909
+ strokeDasharray={isOnline ? 'none' : '5 5'}
6910
+ filter={isOnline && !isLight ? 'url(#topo-glow)' : undefined}
6911
+ data-node-status-ring={status.label}
6912
+ data-node-status-ring-hovered={isRingHovered ? 'true' : 'false'}
6913
+ data-node-status-ring-stroke-width={ringStrokeWidth}
6914
+ style={{
6915
+ transition: 'fill 300ms ease-out, stroke 300ms ease-out, stroke-width 300ms ease-out',
6916
+ }}
6917
+ />
6918
+ );
6919
+ })()}
5633
6920
  {/* v0.10.0 Hero 1+2 / §3.F server-health node-ring tint.
5634
6921
  When the host server this agent runs on is in the
5635
6922
  `red` tier (CPU/Mem/Disk worst-of ≥ 85% per
@@ -5809,8 +7096,37 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
5809
7096
  transition: 'r 150ms ease-out, stroke-width 150ms ease-out',
5810
7097
  } as React.CSSProperties}
5811
7098
  />
7099
+ {/* Round 443 / Loop: runtime badge inner-icon
7100
+ strokeWidth lift on node hover — 2.4 → 2.8 on
7101
+ isNodeActive. Pre-R443 the outer badge ring
7102
+ lifted (R208 r + sw both grow on hover) but
7103
+ the inner icon path stayed locked at sw=2.4.
7104
+ The two layers of the runtime badge were
7105
+ out of phase: ring thickened, icon stayed
7106
+ thin. R443 closes the 2-axis hover signature
7107
+ on the badge so both ring and icon lift
7108
+ together. +0.4 absolute delta matches the
7109
+ R208 ring's +0.5 sw delta (badge ring 1.5 →
7110
+ 2.0 absolute), proportional to the icon's
7111
+ heavier base of 2.4. Pure paint axis;
7112
+ strokeLinecap='round' + strokeLinejoin='round'
7113
+ preserved. transition list extends to include
7114
+ 'stroke-width 150ms ease-out' matching R208
7115
+ outer-ring cadence. data-runtime-badge-icon
7116
+ + -active attrs exposed for tests. */}
5812
7117
  <g transform={`translate(${bx - icon / 2} ${by - icon / 2}) scale(${icon / 24})`}>
5813
- <path d={rt.iconPath} fill="none" stroke={rt.color} strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" />
7118
+ <path
7119
+ d={rt.iconPath}
7120
+ fill="none"
7121
+ stroke={rt.color}
7122
+ strokeWidth={isNodeActive ? '2.8' : '2.4'}
7123
+ strokeLinecap="round"
7124
+ strokeLinejoin="round"
7125
+ data-runtime-badge-icon={session.alias}
7126
+ data-runtime-badge-icon-active={isNodeActive ? 'true' : 'false'}
7127
+ data-runtime-badge-icon-stroke-width={isNodeActive ? '2.8' : '2.4'}
7128
+ style={{ transition: 'stroke-width 150ms ease-out' }}
7129
+ />
5814
7130
  </g>
5815
7131
  </g>
5816
7132
  );
@@ -5953,14 +7269,69 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
5953
7269
  220ms cadence the existing filter/stroke
5954
7270
  pair uses — coordinated 4-property easing
5955
7271
  across the card. */}
7272
+ {/* Round 411 / Loop: node label card rx 6 → 8.
7273
+ Pre-R411 the per-node label card painted at
7274
+ rx=6, sitting one tier BELOW the R332/R375/
7275
+ R376 compact-chrome tier (rx=8). Inside the
7276
+ corner-radius cascade family the cards used
7277
+ to be the only "smaller" tier — but the
7278
+ label card is a content-bearing surface
7279
+ (alias + sub text + ring), not a sub-
7280
+ element decoration. R411 lifts rx=6 → 8
7281
+ to align with the compact-chrome / segmented-
7282
+ control tier so all "compact card" surfaces
7283
+ read with the same corner radius.
7284
+ Corner-radius cascade (8 anchors now):
7285
+ R330 canvas rx 12 (root)
7286
+ R331 panels rx 10 (recent-signal, legend)
7287
+ R332 minimap container rx 8 (compact chrome)
7288
+ R375 Layout-toggle rx 8 (segmented control)
7289
+ R376 nodeSize/zoom rx 8 (segmented control)
7290
+ R390 hover-detail rx 10 (panel)
7291
+ R393 minimap viewport rx 2 (sub-element)
7292
+ R411 node label card rx 6 → 8 (compact card, this round)
7293
+ Pure paint — rx grows the corner curve
7294
+ inward without changing the card's outer
7295
+ cardW × cardH bbox (cardW=92/cardH=22 for
7296
+ standard nodes per R23 / R187 sizing). R217
7297
+ hover-stroke cyan tint + R142 drop-shadow
7298
+ + R246 fill+opacity 220ms transition list
7299
+ + R211 alias/sub text-fill ease all
7300
+ preserved. data-node-label-card-rx attr
7301
+ exposes the value for tests. */}
7302
+ {/* Round 429 / Loop: node label-card body opacity
7303
+ 0.94 → 1.0 on hover (cyber theme). Sibling
7304
+ treatment to R348 panel rect opacity lift —
7305
+ 0.92 → 0.97 cyber / 0.97 → 1.0 light at the
7306
+ panel scope. Pre-R429 the cyber theme card
7307
+ sat at 0.94 always; on hover R217 tinted the
7308
+ stroke + R142 grew the drop-shadow + R26
7309
+ lifted the group + R427/R428 spaced the text
7310
+ but the rect itself never solidified —
7311
+ the card glowed brighter through the
7312
+ shadow but the body alpha gap (6 pct) stayed
7313
+ fixed. R429 lifts the body to full alpha on
7314
+ hover so the card reads as a confidently
7315
+ present surface under the cursor (matching
7316
+ the panel-pair pattern). Light theme stays
7317
+ at 1.0 in both states (already maxed). R246
7318
+ transition list already covers opacity 220ms
7319
+ so the lift eases for free. R217 stroke tint
7320
+ + R142 drop-shadow + R211 fill ease all
7321
+ preserved (additive opacity branch only). */}
5956
7322
  <rect
5957
- x={-cardW / 2} y={cardTopY} width={cardW} height={cardH} rx="6"
7323
+ x={-cardW / 2} y={cardTopY} width={cardW} height={cardH} rx="8"
5958
7324
  fill={pal.labelBox.fill}
5959
7325
  stroke={!reducedMotion && hoveredAlias === session.alias
5960
7326
  ? pal.legendAccent
5961
7327
  : pal.labelBox.stroke}
5962
- opacity={isLight ? 1 : 0.94}
7328
+ opacity={
7329
+ !reducedMotion && hoveredAlias === session.alias
7330
+ ? 1
7331
+ : (isLight ? 1 : 0.94)
7332
+ }
5963
7333
  data-node-label-card={session.alias}
7334
+ data-node-label-card-rx="8"
5964
7335
  data-node-label-card-elevation={
5965
7336
  !reducedMotion && hoveredAlias === session.alias ? 'hover' : 'idle'
5966
7337
  }
@@ -6004,25 +7375,94 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6004
7375
  transition list extends 'letter-spacing
6005
7376
  200ms ease-out' so it eases alongside the
6006
7377
  existing 300ms fill transition. */}
7378
+ {/* Round 427 / Loop: extend the node alias label
7379
+ letter-spacing family to a 3-tier scale —
7380
+ rest 0px → hover 0.3px → chat-target 0.5px.
7381
+ Pre-R427 the alias text shifted only when
7382
+ chat was actively pinned (R305); pure node-
7383
+ hover left the text dead-typographic while
7384
+ the surrounding card lifted (R26 translateY
7385
+ + R242 stroke + filter cues). R427 adds the
7386
+ missing typographic axis to the hover gesture
7387
+ so the alias text rises with the card.
7388
+ The chat-target tier still wins (0.5 > 0.3)
7389
+ so the pin signature stays at the top of the
7390
+ scale — hover is the mid tier between rest
7391
+ and chat-target.
7392
+ Hover-letter-spacing family extension:
7393
+ R344 chip count digit
7394
+ R345 panel title (R423 sibling)
7395
+ R347 active-links chip
7396
+ R351 vendor chip
7397
+ R420 zoom-level chip
7398
+ R427 node alias text (this round)
7399
+ R211 fill 300ms + R305 letter-spacing 200ms
7400
+ transition list preserved; only the
7401
+ conditional gets a middle case. */}
6007
7402
  <text
6008
7403
  x="0" y="1" textAnchor="middle"
6009
7404
  fill={status.text}
6010
7405
  fontSize={aliasFs} fontFamily="monospace" fontWeight="700"
6011
7406
  data-node-alias-text={session.alias}
6012
7407
  data-node-alias-chat-target={chatAlias === session.alias ? 'true' : 'false'}
7408
+ data-node-alias-hovered={hoveredAlias === session.alias ? 'true' : 'false'}
6013
7409
  style={{
6014
7410
  transition: 'fill 300ms ease-out, letter-spacing 200ms ease-out',
6015
- letterSpacing: chatAlias === session.alias ? '0.5px' : '0px',
7411
+ letterSpacing:
7412
+ chatAlias === session.alias ? '0.5px' :
7413
+ hoveredAlias === session.alias ? '0.3px' : '0px',
6016
7414
  }}
6017
7415
  >
6018
7416
  {truncate(session.alias, fullMax)}
6019
7417
  </text>
7418
+ {/* Round 428 / Loop: node sub-text (status label
7419
+ line beneath the alias) adopts hover letter-
7420
+ spacing tween 0 → 0.2px on hoveredAlias.
7421
+ Sibling treatment to R427 alias-text hover
7422
+ tween (0 → 0.3) — the alias is the primary
7423
+ identity (top-tier kerning 0.3), the sub-text
7424
+ is the secondary status line (one tier lower
7425
+ at 0.2). Now both lines of the label card
7426
+ telegraph hover typographically as one unit,
7427
+ matching the R26 card lift + R242 stroke
7428
+ tint + R975 filter cues. Subtler delta on
7429
+ the sub-text (0.2 vs alias 0.3) preserves
7430
+ the alias > status visual hierarchy at the
7431
+ hover scope. R211 fill 300ms transition
7432
+ preserved (additive letter-spacing branch
7433
+ + appended 'letter-spacing 200ms ease-out'). */}
7434
+ {/* Round 448 / Loop: node sub-text fontWeight
7435
+ 400 → 500 (font-medium). Sibling to R363
7436
+ (recent-row text fw 400→500) + R364 (legend-
7437
+ row label fw 400→500) — same "small mono
7438
+ text at fontSize=9-11 needs 500-tier weight
7439
+ for legibility" pattern, now applied to the
7440
+ per-node sub-text line. At fontSize=8-9
7441
+ monospace against the label-card chrome
7442
+ (pal.labelBox.fill cyber #020617 / light
7443
+ #ffffff), the default fw=400 sits at the
7444
+ legibility floor; fw=500 (font-medium) lifts
7445
+ it into a clearly readable band without
7446
+ changing geometry. R211 fill 300ms +
7447
+ R428 letter-spacing 0→0.2 hover + R427
7448
+ alias-text + R429 body opacity all preserved.
7449
+ Pure typography lift; no layout shift; the
7450
+ alias-text fw=700 (R427) still wins so the
7451
+ alias > status hierarchy holds at the type
7452
+ level. data-node-sub-text-font-weight attr
7453
+ exposes the value for tests. */}
6020
7454
  <text
6021
7455
  x="0" y={subY} textAnchor="middle"
6022
7456
  fill={status.primary}
6023
7457
  fontSize={subFs} fontFamily="monospace"
7458
+ fontWeight="500"
6024
7459
  data-node-sub-text={session.alias}
6025
- style={{ transition: 'fill 300ms ease-out' }}
7460
+ data-node-sub-text-hovered={hoveredAlias === session.alias ? 'true' : 'false'}
7461
+ data-node-sub-text-font-weight="500"
7462
+ style={{
7463
+ transition: 'fill 300ms ease-out, letter-spacing 200ms ease-out',
7464
+ letterSpacing: hoveredAlias === session.alias ? '0.2px' : '0px',
7465
+ }}
6026
7466
  >
6027
7467
  {status.label}{isOnline && sseCountFor != null ? ` sse:${sseCountFor}` : ''}
6028
7468
  </text>
@@ -6095,26 +7535,122 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6095
7535
  const detailY = pos.y - detailH / 2;
6096
7536
  return (
6097
7537
  <g transform={`translate(${detailX}, ${detailY})`} data-topo-hover-detail={session.alias} style={{ pointerEvents: 'none' }}>
7538
+ {/* Round 387 / Loop: hover-detail panel cyber backdrop
7539
+ opacity 0.94 → 0.97. The hover-detail card is
7540
+ ALWAYS rendered in active-hover context (it IS
7541
+ the hover product), so it should carry the
7542
+ same backdrop weight as the R348 recent-signal /
7543
+ legend panel HOVER state (which lifts 0.92 →
7544
+ 0.97 cyber). Pre-R387 the card sat at 0.94
7545
+ cyber, leaving a 0.03 alpha gap against the
7546
+ R348 panel-hover state — small but visible
7547
+ when the hover-detail floats next to a hovered
7548
+ recent-signal panel. R387 unifies them at 0.97
7549
+ so all active-hover panels paint with the same
7550
+ confident backdrop opacity in cyber. Light
7551
+ stays at 0.98 (already at the strong end —
7552
+ R348 light also stays at 0.97/0.98 max).
7553
+ Theme-consistency / canvas-presence polish
7554
+ family (5th anchor):
7555
+ R370 hub hover-ring opacity 0.7 → 0.8 cyber
7556
+ R371 edge-badge rest opacity 0.82 → 0.85 cyber
7557
+ R372 minimap offline-dot opacity 0.5 → 0.6
7558
+ R386 hub-highlight idle opacity 0.9 → 0.95
7559
+ R387 hover-detail panel opacity 0.94 → 0.97 cyber (this round)
7560
+ data-topo-hover-detail-opacity attr exposes
7561
+ the resolved value for tests. R348 drop-shadow
7562
+ + rx=8 + stroke=pal.legendAccent + fill=pal.
7563
+ labelBox.fill all preserved. */}
7564
+ {/* Round 390 / Loop: hover-detail card rx 8 → 10.
7565
+ Corner-radius cascade family — the hover-detail
7566
+ card is a panel-tier surface (192×88 floating
7567
+ info card with drop-shadow + stroke), so its
7568
+ corner radius should match the R331 panel tier
7569
+ (rx=10) used by the recent-signal and legend
7570
+ panels. Pre-R390 it shared rx=8 with the R332
7571
+ minimap and R375/R376 segmented-control tier
7572
+ (Layout-toggle, nodeSize, zoom wrappers),
7573
+ which is the "compact chrome control" tier —
7574
+ a tier mismatch for a content-bearing panel.
7575
+ Corner-radius cascade (6 anchors now):
7576
+ R330 canvas rx 12 (root)
7577
+ R331 panels rx 10 (recent-signal, legend)
7578
+ R332 minimap rx 8 (compact chrome)
7579
+ R375 Layout-toggle rx 8 (segmented control)
7580
+ R376 nodeSize/zoom rx 8 (segmented control)
7581
+ R390 hover-detail rx 10 (panel — this round)
7582
+ Pure paint change; no layout shift (rx grows
7583
+ the corner curve INWARD without changing the
7584
+ card's outer bbox). data-topo-hover-detail-
7585
+ rx attr exposes the resolved value for tests.
7586
+ R348 drop-shadow + stroke + R387 opacity all
7587
+ preserved. */}
6098
7588
  <rect
6099
- x="0" y="0" width={detailW} height={detailH} rx="8"
7589
+ x="0" y="0" width={detailW} height={detailH} rx="10"
6100
7590
  fill={pal.labelBox.fill}
6101
7591
  stroke={pal.legendAccent}
6102
- opacity={isLight ? 0.98 : 0.94}
7592
+ opacity={isLight ? 0.98 : 0.97}
7593
+ data-topo-hover-detail-opacity={isLight ? 0.98 : 0.97}
7594
+ data-topo-hover-detail-rx="10"
6103
7595
  style={{ filter: isLight ? 'drop-shadow(0 4px 12px rgba(15,23,42,0.16))' : 'drop-shadow(0 4px 12px rgba(0,0,0,0.6))' }}
6104
7596
  />
6105
7597
  <text x="10" y="16" fontSize="9" fontFamily="monospace" fill={pal.legendAccent} fontWeight="700">
6106
7598
  {v.id !== 'unknown' ? v.label : '—'}
6107
7599
  </text>
6108
- <text x="10" y="32" fontSize="10" fontFamily="monospace" fill={pal.legendHeadline}>
7600
+ {/* Round 389 / Loop: hover-detail model line (y=32)
7601
+ fontWeight 400 → 600. R388 lifted body lines
7602
+ (runtime/host/task at fontSize=9) to fw=500;
7603
+ R389 closes the typography hierarchy by giving
7604
+ the model name (the dominant subhead text in
7605
+ the card) its own weight tier. Three-tier
7606
+ ladder now reads cleanly:
7607
+ vendor fontSize=9 fw=700 (label badge)
7608
+ model fontSize=10 fw=600 (subhead — this round)
7609
+ body 3× fontSize=9 fw=500 (R388)
7610
+ One tier step per dimension (size + weight)
7611
+ between adjacent levels — classic editorial
7612
+ hierarchy idiom adapted to a 192×88 SVG card.
7613
+ Sibling to the chip-internal-hierarchy arc
7614
+ (R333-R341/R362/R369) which uses fw=600/500
7615
+ for digit/unit pairs; R389 applies the same
7616
+ fw=600 to a content-bearing identity line.
7617
+ data-topo-hover-detail-model-fw attr exposes
7618
+ the resolved value for tests. pal.legendHeadline
7619
+ fill preserved (R389 doesn't touch color). */}
7620
+ <text x="10" y="32" fontSize="10" fontFamily="monospace" fontWeight="600" fill={pal.legendHeadline} data-topo-hover-detail-model-fw="600">
6109
7621
  {session.model || 'model · pending'}
6110
7622
  </text>
6111
- <text x="10" y="48" fontSize="9" fontFamily="monospace" fill={pal.legendText}>
7623
+ {/* Round 388 / Loop: hover-detail body lines (the
7624
+ three fontSize=9 lines: runtime, host, task)
7625
+ gain fontWeight=500. Small-text fw lift family
7626
+ (6th anchor) — fontSize 9-10 px text reads
7627
+ consistently bolder at fw=500 than at the
7628
+ default 400 weight at small sizes, especially
7629
+ on the cyber-theme backdrop where stroke-
7630
+ rendering is the limiting factor.
7631
+ Sibling lifts in this family:
7632
+ R363 recent-row alias text 400 → 500
7633
+ R364 legend-row label 400 → 500
7634
+ R366 group-label count tspan 400 → 500
7635
+ R368 +N more flows footer 400 → 500
7636
+ R373 pressure-bar kicker (font-medium)
7637
+ R388 hover-detail body lines 400 → 500 (this round)
7638
+ Tier structure preserved:
7639
+ y=16 vendor (fw=700, headline)
7640
+ y=32 model (fontSize=10, subhead by size)
7641
+ y=48 runtime / y=64 host / y=80 task (body, now fw=500)
7642
+ The y=80 task line keeps opacity=0.7 so its
7643
+ caption-tier identity stays distinct from the
7644
+ y=48 / y=64 body lines despite shared fw.
7645
+ data-topo-hover-detail-body-fw attr exposes
7646
+ the resolved value for tests. */}
7647
+ <text x="10" y="48" fontSize="9" fontFamily="monospace" fontWeight="500" fill={pal.legendText} data-topo-hover-detail-body-fw="500">
6112
7648
  {rt ? rt.label : 'runtime · pending'}
6113
7649
  </text>
6114
- <text x="10" y="64" fontSize="9" fontFamily="monospace" fill={pal.legendText}>
7650
+ <text x="10" y="64" fontSize="9" fontFamily="monospace" fontWeight="500" fill={pal.legendText} data-topo-hover-detail-body-fw="500">
6115
7651
  host · {session.server || 'unknown'}
6116
7652
  </text>
6117
- <text x="10" y="80" fontSize="9" fontFamily="monospace" fill={pal.legendText} opacity="0.7">
7653
+ <text x="10" y="80" fontSize="9" fontFamily="monospace" fontWeight="500" fill={pal.legendText} opacity="0.7" data-topo-hover-detail-body-fw="500">
6118
7654
  {session.task ? truncate(session.task, 28) : 'no recent task'}
6119
7655
  </text>
6120
7656
  </g>
@@ -6178,14 +7714,41 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6178
7714
  keySplines="0.25 0.1 0.25 1"
6179
7715
  fill="freeze"
6180
7716
  />
7717
+ {/* Round 403 / Loop: click-ripple SMIL initial opacity
7718
+ 0.7 → 0.8. Pre-R403 the ripple's opacity animation
7719
+ faded from 0.7 to 0 over 500ms, providing a clean
7720
+ click-feedback pulse. Theme-consistency / canvas-
7721
+ presence polish family (R370 hub hover-ring +
7722
+ R391 hub-spoke active) already lifted paired
7723
+ hover-state alphas from 0.7 → 0.8. R403 brings
7724
+ click-feedback into that same alpha — three canvas
7725
+ state-feedback indicators (hover-ring, active spoke,
7726
+ click ripple) now share a uniform 0.8 start alpha
7727
+ so the visual "I responded" signal carries the
7728
+ same weight regardless of which state fired it.
7729
+ Pre-R403 invariants preserved: 500ms duration,
7730
+ R227 calcMode='spline' + ease-out keySplines
7731
+ (0.25 0.1 0.25 1), fill='freeze', concurrent r
7732
+ animation. Theme-consistency family (8 anchors):
7733
+ R370 hub hover-ring 0.7 → 0.8
7734
+ R371 edge-badge rest 0.82 → 0.85 cyber
7735
+ R372 minimap offline-dot 0.5 → 0.6
7736
+ R386 hub-highlight idle 0.9 → 0.95
7737
+ R387 hover-detail panel 0.94 → 0.97 cyber
7738
+ R391 hub-spoke active 0.7 → 0.8
7739
+ R392 minimap online-dot 0.9 → 0.95
7740
+ R403 click-ripple start 0.7 → 0.8 (this round)
7741
+ data-click-ripple-start-opacity attr exposes the
7742
+ resolved value for tests. */}
6181
7743
  <animate
6182
7744
  attributeName="opacity"
6183
- values="0.7;0"
7745
+ values="0.8;0"
6184
7746
  dur="0.5s"
6185
7747
  calcMode="spline"
6186
7748
  keyTimes="0;1"
6187
7749
  keySplines="0.25 0.1 0.25 1"
6188
7750
  fill="freeze"
7751
+ data-click-ripple-start-opacity="0.8"
6189
7752
  />
6190
7753
  </circle>
6191
7754
  )}
@@ -6259,17 +7822,21 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6259
7822
  <rect
6260
7823
  x="0" y="0" width="230" height="88" rx="10"
6261
7824
  fill={pal.legendBox.fill}
6262
- stroke={pal.legendBox.stroke}
6263
- // Round 348 / Loop: recent-signal panel rect opacity hover-
6264
- // state bump — joins the panel-hover cue stack (R135 drop-
6265
- // shadow boost + R345 title letter-spacing tween 0.3 → 0.4
6266
- // + R266 fill theme-flip). Cyber 0.92 → 0.97, light 0.97 →
6267
- // 1.0 on hoveredPanel === 'recent'. The panel "solidifies"
6268
- // on hover — pure paint-level change, geometry-safe (bbox
6269
- // unchanged so topo-overlap-test invariants hold). The
6270
- // R247 transition list already includes `opacity 200ms
6271
- // ease-out` so the value tween is automatic. Sibling
6272
- // change at legend panel rect below (~line 7222).
7825
+ // Round 423 / Loop: panel rect stroke tints to legendAccent
7826
+ // (cyan) on hover — sibling to R217 label-card stroke
7827
+ // hover-tint at the panel scope. Pre-R423 the panel rect
7828
+ // stroke painted pal.legendBox.stroke (neutral) regardless
7829
+ // of hover state, while every other panel hover cue stacked:
7830
+ // R135 drop-shadow boost
7831
+ // R348 rect opacity 0.92 → 0.97 cyber
7832
+ // R345 title letter-spacing 0.3 → 0.4
7833
+ // R423 rect stroke → legendAccent (this round)
7834
+ // Four hover layers now telegraph "you're entering this
7835
+ // panel" through structural, paint, and typographic axes
7836
+ // simultaneously. R247 transition list already covers
7837
+ // stroke 200ms ease-out so the tint eases naturally.
7838
+ // Sibling change at the legend panel rect below.
7839
+ stroke={hoveredPanel === 'recent' ? pal.legendAccent : pal.legendBox.stroke}
6273
7840
  opacity={hoveredPanel === 'recent' ? (isLight ? 1 : 0.97) : (isLight ? 0.97 : 0.92)}
6274
7841
  style={{
6275
7842
  /* R135: drop-shadow intensifies on panel hover. Base
@@ -6393,8 +7960,8 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6393
7960
  const alpha = ageSec <= 30
6394
7961
  ? 1
6395
7962
  : ageSec <= 300
6396
- ? 1 - ((ageSec - 30) / 270) * 0.75
6397
- : 0.25;
7963
+ ? 1 - ((ageSec - 30) / 270) * 0.70 /* R358: floor 0.25 → 0.30 lift across 3 freshness scopes */
7964
+ : 0.30; /* R358: stale floor lifted 0.25 → 0.30 — 20% legibility bump while preserving fresh/stale ratio */
6398
7965
  // Dark cyan-400 / light teal-600 with alpha — same
6399
7966
  // palette as R161's chip bullet so the two scopes
6400
7967
  // visually align even side-by-side.
@@ -6462,13 +8029,27 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6462
8029
  tests + count value reads still resolve via
6463
8030
  .textContent. data-recent-panel-count-unit on
6464
8031
  the inner unit tspan for R336 introspection. */}
8032
+ {/* R424 — recent-signal panel count digit fontWeight
8033
+ 600 → 700 on panel hover. Closes the 5-layer panel
8034
+ hover cue stack with a typographic-weight axis at
8035
+ the panel-header data scope: depth (R135 drop-
8036
+ shadow) + solidity (R348 fill opacity) + spacing
8037
+ (R345 title letter-spacing) + edge color (R423
8038
+ stroke tint) + weight (THIS, digit fw). Sibling
8039
+ pattern to R416 chip-digit-hover-bold at chip
8040
+ scope — same "data tightens under attention"
8041
+ idiom now at the panel-header data scope. R311
8042
+ base fw=600 + R225 tabular-nums + R162 fill
8043
+ transition + R336 unit-tspan opacity-0.7 all
8044
+ preserved; only the weight axis tweens via R247's
8045
+ transition shape (added font-weight to the list). */}
6465
8046
  <tspan
6466
8047
  fill={freshFill}
6467
- fontWeight="600"
8048
+ fontWeight={hoveredPanel === 'recent' ? '700' : '600'}
6468
8049
  data-recent-panel-count
6469
8050
  data-recent-panel-count-freshness-alpha={alpha.toFixed(2)}
6470
8051
  style={{
6471
- transition: 'fill 200ms ease-out',
8052
+ transition: 'fill 200ms ease-out, font-weight 200ms ease-out',
6472
8053
  fontVariantNumeric: 'tabular-nums',
6473
8054
  }}
6474
8055
  >{flowLinks.length}<tspan opacity="0.7" data-recent-panel-count-unit> flows</tspan></tspan>
@@ -6890,18 +8471,83 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6890
8471
  const alpha = ageSec <= 30
6891
8472
  ? 1
6892
8473
  : ageSec <= 300
6893
- ? 1 - ((ageSec - 30) / 270) * 0.75
6894
- : 0.25;
8474
+ ? 1 - ((ageSec - 30) / 270) * 0.70 /* R358: floor 0.25 → 0.30 lift across 3 freshness scopes */
8475
+ : 0.30; /* R358: stale floor lifted 0.25 → 0.30 — 20% legibility bump while preserving fresh/stale ratio */
6895
8476
  return (
6896
8477
  <circle
6897
8478
  cx={10}
6898
8479
  cy={38 + index * 16 - 3}
6899
- r={1.6}
8480
+ /* Round 359 / Loop: recency pip base radius
8481
+ 1.6 → 1.8. Sibling lift to R358's freshness-
8482
+ floor bump (alpha 0.25 → 0.30) — pre-R358/
8483
+ R359 the stale pip painted at r=1.6 + α=0.25
8484
+ which read as near-invisible chrome. R358
8485
+ gave it more alpha; R359 gives it more area
8486
+ (1.8² / 1.6² ≈ 1.27, so ~27 % more glyph)
8487
+ so the pip stays distinguishable across the
8488
+ freshness ramp. Geometry: 1.8-radius dot
8489
+ centred at (10, row_y - 3) is bbox 3.6×3.6,
8490
+ still well inside the 7-px left margin
8491
+ (x=6 rect-start → x=13 text-start) the R160
8492
+ pip was placed in. Overlap-test reads the
8493
+ parent row rect's bbox, not this pip's, so
8494
+ grid+ring invariants hold. Matches the same
8495
+ 1.6 → 1.8 visual-weight bump R295 applied
8496
+ to the legend swatch (5.5 → 6 base radius)
8497
+ and R287 to the minimap viewport stroke
8498
+ (1 → 1.5). data-recent-row-freshness-radius
8499
+ attr exposes the value for tests. */
8500
+ /* Round 383 / Loop: recency pip base radius
8501
+ 1.8 → 2.0. Continues the R359 lift
8502
+ trajectory — pip area grows ~23 % (π·2²/
8503
+ π·1.8² ≈ 1.23) for a clearer at-a-glance
8504
+ freshness anchor in each row. Bbox 4.0×4.0
8505
+ still inside the 7-px R160 left margin
8506
+ (3-px remaining clearance vs 3.4 at r=1.8
8507
+ — geometry-safe margin holds). Sibling
8508
+ visual-weight bump family (9th anchor now):
8509
+ R287 minimap viewport stroke 1 → 1.5
8510
+ R295 legend swatch base radius 5.5 → 6
8511
+ R359 recent-row pip base radius 1.6 → 1.8
8512
+ R360 hub digit fontSize 11 → 12
8513
+ R361 edge-badge digit fontSize 10 → 11
8514
+ R365 hub-highlight base radius 5 → 5.5
8515
+ R367 edge-badge rest stroke 1 → 1.25
8516
+ R374 pressure-bar height 1.5 → 2
8517
+ R383 recent-row pip radius 1.8 → 2.0 (this round)
8518
+ data-recent-row-freshness-radius attr
8519
+ bumps to '2.0' for tests. */
8520
+ /* Round 447 / Loop: recent-row freshness pip
8521
+ radius lift on (isRowHovered || isRowPinned)
8522
+ — r 2.0 → 2.5 (+0.5px, sibling to R442
8523
+ endpoint-ring r lift). Adds a geometric
8524
+ axis to the recent-row hover/pin gesture
8525
+ alongside R143 translateY + R104 row bg-
8526
+ tint + R434 letter-spacing + R445 count
8527
+ fw. Pre-R447 the pip stayed at r=2.0 always
8528
+ — the freshness alpha (R162) tracked
8529
+ recency but didn't telegraph "this row is
8530
+ in focus" geometrically. R447 lifts the
8531
+ pip outward by 25% area (π·2.5² / π·2.0²
8532
+ = 1.56) on attention, closing a 5-axis
8533
+ row-attention signature (geometry + paint
8534
+ + typography + spacing + position).
8535
+ SVG `r` as CSS property for interpolation
8536
+ (R197/R198 idiom). transition list extends
8537
+ to include 'r 200ms ease-out' matching the
8538
+ opacity cadence. data-recent-row-freshness-
8539
+ lifted attr exposes the gate for tests. */
6900
8540
  fill={pal.legendAccent}
6901
8541
  opacity={alpha}
6902
8542
  data-recent-row-freshness={link.key}
6903
8543
  data-recent-row-freshness-alpha={alpha.toFixed(2)}
6904
- style={{ pointerEvents: 'none', transition: 'opacity 200ms ease-out' }}
8544
+ data-recent-row-freshness-radius={(isRowHovered || isRowPinned) ? 2.5 : 2.0}
8545
+ data-recent-row-freshness-lifted={(isRowHovered || isRowPinned) ? 'true' : 'false'}
8546
+ style={{
8547
+ pointerEvents: 'none',
8548
+ r: `${(isRowHovered || isRowPinned) ? 2.5 : 2.0}px`,
8549
+ transition: 'opacity 200ms ease-out, r 200ms ease-out',
8550
+ } as React.CSSProperties}
6905
8551
  />
6906
8552
  );
6907
8553
  })()}
@@ -6930,11 +8576,65 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6930
8576
  fill={isRowActive ? pal.legendHeadline : pal.legendText}
6931
8577
  fontSize="9"
6932
8578
  fontFamily="monospace"
8579
+ /* Round 363 / Loop: recent-row text fontWeight 400
8580
+ → 500 (font-medium tier). At fontSize=9 the
8581
+ default-weight 400 glyphs read thin against the
8582
+ panel chrome (pal.legendBox.fill with 0.92/0.97
8583
+ opacity); the 100-weight bump lifts the alias→
8584
+ alias text into the legibility band without
8585
+ changing geometry. The R320 count tspan fw=600
8586
+ (cold) / fw=700 (hot) override still wins
8587
+ locally via inline fontWeight on the inner
8588
+ tspan, so the count-vs-alias hierarchy stays
8589
+ intact:
8590
+ alias fw 500 (R363, this round)
8591
+ count fw 600/700 (R320)
8592
+ Sibling typography lift to R362 chip-row digit
8593
+ 500 → 600 — both nudge a within-element data
8594
+ tier without disturbing the surrounding family
8595
+ baseline. data-recent-row-text-font-weight attr
8596
+ exposes the value for tests. */
8597
+ fontWeight="500"
6933
8598
  data-recent-row-text={link.key}
6934
8599
  data-recent-row-text-pinned={isRowPinned ? 'true' : 'false'}
8600
+ data-recent-row-text-hovered={!isRowPinned && isRowHovered ? 'true' : 'false'}
8601
+ data-recent-row-text-font-weight="500"
8602
+ /* Round 434 / Loop: recent-signal row text extends
8603
+ from R220's pin-only letter-spacing (0 → 0.5 on
8604
+ isRowPinned) to a 3-tier scale matching R433
8605
+ legend-row at the sibling panel-row scope:
8606
+ rest → 0px
8607
+ isRowHovered → 0.25px ← this round
8608
+ isRowPinned → 0.5px (R220 preserved)
8609
+ Pre-R434 R220 noted: "Hover stays at default
8610
+ tracking — the spread is pin-exclusive so
8611
+ users can read pinned vs hovered at the text
8612
+ alone." R427-R433 established a 3-tier pattern
8613
+ across 4 surfaces (node-alias, edge-badge,
8614
+ group-label, legend-row) where hover gets a
8615
+ subtler intermediate kerning step distinct
8616
+ from the pin tier's stronger spread — the
8617
+ locked vs preview discrimination R220 wanted
8618
+ is preserved (0.5 > 0.25) AND hover gets a
8619
+ typographic axis of its own. R434 completes
8620
+ the 5-surface arc.
8621
+ 5-surface 3-tier letter-spacing pattern now
8622
+ spans every interactive label on TopoGraph
8623
+ that distinguishes hover from pin:
8624
+ node-alias (R427) 0 / 0.3 / 0.5
8625
+ edge-badge (R431) 0 / 0.2 / 0.4
8626
+ group-label (R432) 0 / 0.25 / 0.5
8627
+ legend-row (R433) 0 / 0.25 / 0.5
8628
+ recent-row (R434) 0 / 0.25 / 0.5 ← this round
8629
+ Hover-letter-spacing family extension
8630
+ (10 anchors now): R344/R345/R347/R351/R420/
8631
+ R427/R431/R432/R433/R434. R55 fill 150ms +
8632
+ R220 letter-spacing 150ms transition kept
8633
+ (additive conditional case, no new property). */
6935
8634
  style={{
6936
8635
  transition: 'fill 150ms ease-out, letter-spacing 150ms ease-out',
6937
- letterSpacing: isRowPinned ? '0.5px' : '0px',
8636
+ letterSpacing: isRowPinned ? '0.5px' :
8637
+ isRowHovered ? '0.25px' : '0px',
6938
8638
  }}
6939
8639
  >
6940
8640
  {/* R138 / Loop: typography unification with the rest
@@ -6994,19 +8694,60 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6994
8694
  distinct, plus the fill flip from legendText
6995
8695
  → amber carries the dramatic part of the cue).
6996
8696
  Sibling treatment in the data-weight tier. */}
8697
+ {/* Round 445 / Loop: extend the R320 cold/hot fw
8698
+ binary (600/700) to ALSO fire on isRowPinned —
8699
+ pinned-cold now lifts to 700 alongside the
8700
+ existing hot-triggered lift. Sibling to R444
8701
+ group-label-count-pin (500→600) at the
8702
+ recent-row scope. Both panel-row counts now
8703
+ respond to pin with a typographic weight lift,
8704
+ part of the "data tightens under attention"
8705
+ family (R416/R424/R425/R426/R444/R445).
8706
+ Effective tiers:
8707
+ cold + un-pinned → fw 600
8708
+ cold + pinned → fw 700 ← this round
8709
+ hot (any pin state) → fw 700 (R320 preserved)
8710
+ hot is still amber-filled (R127); cold pin
8711
+ stays at the parent fill, so the two routes
8712
+ to fw=700 are visually distinct (color vs
8713
+ no color). transition list adds 'font-
8714
+ weight 200ms ease-out' so the lift eases
8715
+ under the same R320 fill cadence. data-
8716
+ recent-row-count-pinned attr exposes the
8717
+ pin gate for tests. */}
6997
8718
  <tspan
6998
8719
  fill={isHot ? hotStroke : undefined}
6999
- fontWeight={isHot ? '700' : '600'}
8720
+ fontWeight={(isHot || isRowPinned) ? '700' : '600'}
7000
8721
  data-recent-row-count
8722
+ data-recent-row-count-pinned={isRowPinned ? 'true' : 'false'}
8723
+ data-recent-row-count-font-weight={(isHot || isRowPinned) ? '700' : '600'}
7001
8724
  {...(isHot ? { 'data-recent-row-count-hot': 'true' } : {})}
7002
8725
  style={{
7003
- transition: 'fill 300ms ease-out',
8726
+ transition: 'fill 300ms ease-out, font-weight 200ms ease-out',
7004
8727
  fontVariantNumeric: 'tabular-nums',
7005
8728
  }}
7006
8729
  >
7007
8730
  {link.count}
7008
8731
  </tspan>
7009
- {' · '}{truncate(link.content, 8)}
8732
+ {/* Round 418 / Loop: recent-row content preview
8733
+ gains opacity=0.7 wrapper — subordinate-text
8734
+ tier at the SVG-text scope. Pre-R418 the
8735
+ truncated content preview (e.g. " · hi there")
8736
+ inherited the row's full opacity, reading at
8737
+ the same emphasis as the alias text and
8738
+ count digit. R418 wraps it in a <tspan> at
8739
+ opacity=0.7 so the preview reads as
8740
+ subordinate metadata — sibling to R333-R341/
8741
+ R362/R369/R389/R410/R412 chip-internal-
8742
+ hierarchy "label tier" (opacity-70) at the
8743
+ HTML scope, and R317 subordinate-text-lift
8744
+ gray-500 → gray-400 family. The leading
8745
+ " · " separator stays at full opacity so
8746
+ the row punctuation rhythm holds. data-
8747
+ recent-row-content-tspan attr surfaces the
8748
+ subordinate wrapper for tests. */}
8749
+ {' · '}
8750
+ <tspan opacity="0.7" data-recent-row-content-tspan>{truncate(link.content, 8)}</tspan>
7010
8751
  </text>
7011
8752
  {lastAt ? (
7012
8753
  /* Round 321 / Loop: lastAt freshness timestamp picks
@@ -7193,6 +8934,23 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7193
8934
  surface. transition list extends letter-spacing
7194
8935
  200ms ease-out alongside the existing opacity/
7195
8936
  fill easings. */}
8937
+ {/* Round 368 / Loop: `+N more flows` footer text gains
8938
+ fontWeight=500 (font-medium tier). Sibling small-
8939
+ text fw lift family with R363 recent-row alias
8940
+ + R364 legend-row label + R366 group-label count
8941
+ — all four lifts share the same theory: at small
8942
+ fontSize (9-11 px) against panel chrome, SVG-
8943
+ default fw 400 sits at the legibility floor;
8944
+ fw 500 brings the glyph into the deliberate-data
8945
+ band. fontStyle=italic + opacity 0.55 rest + R325
8946
+ letterSpacing 0.2 baseline + R344 hover-spread
8947
+ 0.2 → 0.3 + R195 cyan fill on hover all preserved
8948
+ — the fw bump just thickens the italic stroke.
8949
+ Hover-state punch (R195 fill + R325 opacity 0.55
8950
+ → 0.85 + R344 letter-spacing + R133 underline)
8951
+ stays as is, so the rest-vs-hover delta still
8952
+ reads clearly. data-recent-panel-more-font-weight
8953
+ attr exposes the value for tests. */}
7196
8954
  <text
7197
8955
  x="115" y="82"
7198
8956
  textAnchor="middle"
@@ -7200,11 +8958,13 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7200
8958
  fontSize="9"
7201
8959
  fontFamily="monospace"
7202
8960
  fontStyle="italic"
8961
+ fontWeight="500"
7203
8962
  letterSpacing={hoveredRecentMore ? '0.3' : '0.2'}
7204
8963
  opacity={hoveredRecentMore ? 0.85 : 0.55}
7205
8964
  textDecoration={hoveredRecentMore ? 'underline' : 'none'}
7206
8965
  data-recent-panel-more={moreCount}
7207
8966
  data-recent-panel-more-hovered={hoveredRecentMore ? 'true' : 'false'}
8967
+ data-recent-panel-more-font-weight="500"
7208
8968
  style={{ transition: 'opacity 150ms ease-out, fill 200ms ease-out, letter-spacing 200ms ease-out' }}
7209
8969
  >
7210
8970
  {`+ ${moreCount}`}
@@ -7256,7 +9016,11 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7256
9016
  <rect
7257
9017
  x="0" y="0" width="224" height="88" rx="10"
7258
9018
  fill={pal.legendBox.fill}
7259
- stroke={pal.legendBox.stroke}
9019
+ // R423 sibling — legend panel rect stroke tints to
9020
+ // legendAccent on hover (mirrors recent-signal panel
9021
+ // above). 4-layer hover cue stack now symmetric across
9022
+ // both side panels.
9023
+ stroke={hoveredPanel === 'legend' ? pal.legendAccent : pal.legendBox.stroke}
7260
9024
  // R348 sibling — legend panel rect opacity hover-state
7261
9025
  // bump 0.92 → 0.97 (cyber) / 0.97 → 1 (light) on
7262
9026
  // hoveredPanel === 'legend'. Pairs with the recent-signal
@@ -7348,9 +9112,17 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7348
9112
  R336 introspection; the parent .textContent still
7349
9113
  reads "{N} node(s)" so existing R310 count tests via
7350
9114
  textContent unchanged. */}
9115
+ {/* R424 sibling — legend panel count digit fontWeight 600
9116
+ → 700 on panel hover. Closes 5-layer panel hover cue
9117
+ stack symmetric across both side panels (recent-signal
9118
+ + legend): depth (R135) + solidity (R348) + spacing
9119
+ (R345) + edge color (R423) + weight (R424). R310 base
9120
+ fw=600 + R292 tabular-nums + R266 fill transition + R336
9121
+ unit-tspan opacity-0.7 all preserved. Same "data tightens
9122
+ under attention" idiom R416 established at chip scope. */}
7351
9123
  <text
7352
9124
  x="211" y="21" textAnchor="end"
7353
- fill={pal.legendAccent} fontSize="10" fontFamily="monospace" fontWeight="600"
9125
+ fill={pal.legendAccent} fontSize="10" fontFamily="monospace" fontWeight={hoveredPanel === 'legend' ? '700' : '600'}
7354
9126
  // R349 sibling — legend panel header count picks up
7355
9127
  // letterSpacing="0.2", one tier below the R301 panel
7356
9128
  // title 0.3. Pairs with the recent-signal panel count
@@ -7360,7 +9132,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7360
9132
  data-legend-panel-count
7361
9133
  data-legend-panel-count-letter-spacing="0.2"
7362
9134
  style={{
7363
- transition: 'fill 200ms ease-out',
9135
+ transition: 'fill 200ms ease-out, font-weight 200ms ease-out',
7364
9136
  fontVariantNumeric: 'tabular-nums',
7365
9137
  }}
7366
9138
  >{sessions.length}<tspan opacity="0.7" data-legend-panel-count-unit> node{sessions.length === 1 ? '' : 's'}</tspan></text>
@@ -7579,14 +9351,45 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7579
9351
  so this legend-internal circle is invisible to
7580
9352
  that probe. pointerEvents:none so the ring can't
7581
9353
  intercept the row click that produced it. */}
9354
+ {/* Round 402 / Loop: legend pin-ring strokeWidth 1.5
9355
+ → 1.75. Sibling visual-weight bump (12th anchor)
9356
+ to R385 hub hover-ring strokeWidth 1.5 → 1.75 —
9357
+ both are pin/hover state indicators painted as
9358
+ stroke-only circles outside their target swatch
9359
+ with the R51 sentinel value 1.5. R402 lifts to
9360
+ 1.75 (matching R385's choice) so the pin signal
9361
+ reads slightly heavier without crossing the
9362
+ R51 sentinel band (3 reserved for offline node).
9363
+ The R51 selector is gated to g[data-node]
9364
+ ancestors so this legend-internal circle (lives
9365
+ under a <g data-legend-status>) is invisible
9366
+ to the probe — same lesson R177/R385 documented.
9367
+ Visual-weight bump family (12 anchors now):
9368
+ R287 minimap viewport stroke 1 → 1.5
9369
+ R295 legend swatch radius 5.5 → 6
9370
+ R359 recent-row pip radius 1.6 → 1.8
9371
+ R360 hub digit fontSize 11 → 12
9372
+ R361 edge-badge digit fontSize 10 → 11
9373
+ R365 hub-highlight radius 5 → 5.5
9374
+ R367 edge-badge rest stroke 1 → 1.25
9375
+ R374 pressure-bar height 1.5 → 2
9376
+ R383 recent-row pip radius 1.8 → 2.0
9377
+ R384 minimap online dot 1.7 → 1.9
9378
+ R385 hub hover-ring stroke 1.5 → 1.75
9379
+ R402 legend pin-ring stroke 1.5 → 1.75 (this round)
9380
+ R181 always-mount opacity gate + 150ms transition
9381
+ + pointerEvents:none all preserved. data-legend-
9382
+ pin-ring-stroke-width attr exposes the value for
9383
+ tests. */}
7582
9384
  <circle
7583
9385
  cx="16" cy={row.y0} r="8"
7584
9386
  fill="none"
7585
9387
  stroke={row.fill}
7586
- strokeWidth="1.5"
9388
+ strokeWidth="1.75"
7587
9389
  opacity={isPinned ? 1 : 0}
7588
9390
  data-legend-pin-ring={row.key}
7589
9391
  data-legend-pin-ring-pinned={isPinned ? 'true' : 'false'}
9392
+ data-legend-pin-ring-stroke-width="1.75"
7590
9393
  style={{
7591
9394
  pointerEvents: 'none',
7592
9395
  transition: 'opacity 150ms ease-out',
@@ -7613,11 +9416,61 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7613
9416
  fill={hoveredStatus === row.key || isPinned ? pal.legendHeadline : pal.legendText}
7614
9417
  fontSize="11"
7615
9418
  fontFamily="monospace"
9419
+ /* Round 364 / Loop: legend-row label fontWeight 400
9420
+ → 500. Sibling typography lift to R363 recent-row
9421
+ text fw 400 → 500. Both surfaces render small
9422
+ monospace text against panel chrome at fontSize
9423
+ 9-11 where SVG-default fw 400 sits at the
9424
+ legibility floor. font-medium tier (500) gives
9425
+ the label a more deliberate-data register.
9426
+ The R309 per-row count text (separate element
9427
+ below at x=215 textAnchor=end) keeps its own
9428
+ fontWeight 600 inline override, so the count >
9429
+ label hierarchy stays intact at the legend
9430
+ scope same as R363 holds it at the recent-row
9431
+ scope:
9432
+ legend label fw 500 (R364, this round)
9433
+ legend count fw 600 (R309)
9434
+ recent alias fw 500 (R363)
9435
+ recent count fw 600/700 (R320)
9436
+ data-legend-row-label-font-weight attr exposes
9437
+ the value for tests. R219 letter-spacing pin
9438
+ tween + R55 fill transition + R181 always-mount
9439
+ pin ring all preserved. */
9440
+ fontWeight="500"
7616
9441
  data-legend-row-label={row.key}
7617
9442
  data-legend-row-label-pinned={isPinned ? 'true' : 'false'}
9443
+ data-legend-row-label-hovered={!isPinned && hoveredStatus === row.key ? 'true' : 'false'}
9444
+ data-legend-row-label-font-weight="500"
9445
+ /* Round 433 / Loop: legend-row text extends from
9446
+ R219's pin-only letter-spacing (0px → 0.5px on
9447
+ isPinned) to a 3-tier scale matching the R432
9448
+ group-label pattern:
9449
+ rest → 0px
9450
+ hoveredStatus → 0.25px ← this round
9451
+ isPinned → 0.5px (R219 preserved)
9452
+ Pre-R433 hover already brightened the fill
9453
+ (hoveredStatus===row.key || isPinned matches the
9454
+ legendHeadline branch) but the letter-form
9455
+ stayed dead-typographic on transient hover —
9456
+ only the pin tier carried a kerning signature.
9457
+ R433 adds the missing mid tier so hover
9458
+ telegraphs through BOTH fill brighten AND a
9459
+ subtle 0.25-px kerning spread, mirroring
9460
+ R427/R431/R432 at legend-row scope. Pin tier
9461
+ still wins so the locked vs preview distinction
9462
+ at the type level stays intact.
9463
+ Hover-letter-spacing family extension (9 anchors
9464
+ now): R344/R345/R347/R351/R420/R427/R431/R432/
9465
+ R433. 3-tier letter-spacing pattern now spans 4
9466
+ surfaces (node-alias R427, edge-badge R431,
9467
+ group-label R432, legend-row R433). R55 fill
9468
+ 150ms + R219 letter-spacing 150ms transition
9469
+ untouched — additive conditional case. */
7618
9470
  style={{
7619
9471
  transition: 'fill 150ms ease-out, letter-spacing 150ms ease-out',
7620
- letterSpacing: isPinned ? '0.5px' : '0px',
9472
+ letterSpacing: isPinned ? '0.5px' :
9473
+ hoveredStatus === row.key ? '0.25px' : '0px',
7621
9474
  }}
7622
9475
  >{row.label}</text>
7623
9476
  {/* R95: live count anchored to the right edge of the
@@ -7696,20 +9549,60 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7696
9549
  labels to single status words, the count
7697
9550
  becomes proportionally more important; this
7698
9551
  round emphasizes that role typographically. */}
9552
+ {/* Round 446 / Loop: legend per-row count fontWeight
9553
+ lift 600 → 700 on isPinned. Mirror of R444 group-
9554
+ label-count + R445 recent-row-count at the
9555
+ legend-row scope. Closes the 3-panel-row family
9556
+ for the "data tightens under attention" pattern —
9557
+ every panel-row count now responds to pin with a
9558
+ typographic-weight bump:
9559
+ R444 group-label-count 500 → 600
9560
+ R445 recent-row-count 600 → 700 (cold-pin route)
9561
+ R446 legend-row-count 600 → 700 ← this round
9562
+ Hover gate (hoveredStatus===row.key) keeps rest
9563
+ fw=600 so the locked-vs-preview distinction at
9564
+ the type level stays intact — same gate R433 used
9565
+ on the parent <text> letter-spacing tween. R309
9566
+ fw=600 baseline + R204 empty-row opacity dim +
9567
+ R225 tabular-nums all preserved. transition list
9568
+ extends to include 'font-weight 150ms ease-out'
9569
+ matching R433 fill/letter-spacing cadence.
9570
+ data-legend-count-pinned + -font-weight attrs
9571
+ exposed for tests. */}
7699
9572
  <text
7700
9573
  x="215" y={row.y1}
7701
9574
  textAnchor="end"
7702
9575
  fill={row.count > 0 && (hoveredStatus === row.key || isPinned) ? row.fill : pal.legendText}
7703
9576
  fontSize="11"
7704
9577
  fontFamily="monospace"
7705
- fontWeight="600"
9578
+ fontWeight={isPinned ? '700' : '600'}
9579
+ /* Round 449 / Loop: legend-row count active-state
9580
+ opacity 0.95 → 1.0 on (hoveredStatus===row.key
9581
+ || isPinned). Pre-R449 R204 lifted populated-row
9582
+ active opacity from rest 0.65 to 0.95 — visibly
9583
+ brighter but kept a 5 pct alpha gap (1 - 0.95).
9584
+ R449 closes the gap to 1.0 so the active count
9585
+ reads as confidently present alongside the R446
9586
+ fw=600→700 + R433 letter-spacing tween. Theme-
9587
+ consistency / canvas-presence family extension
9588
+ (7th anchor on the active-presence lift sub-
9589
+ family): R370 hub hover-ring 0.7→0.8, R371 edge-
9590
+ badge rest 0.82→0.85, R372 minimap offline-dot
9591
+ 0.5→0.6, R386 hub-highlight idle 0.9→0.95, R387
9592
+ hover-detail panel 0.94→0.97, R429 label-card
9593
+ body 0.94→1.0, R449 legend-count active 0.95→1.0
9594
+ ← this round. Empty-row opacity (R204: 0.28
9595
+ light / 0.30 cyber) and idle 0.65 rest both
9596
+ preserved. */
7706
9597
  opacity={row.count === 0
7707
9598
  ? (isLight ? 0.28 : 0.30)
7708
- : (hoveredStatus === row.key || isPinned ? 0.95 : 0.65)}
9599
+ : (hoveredStatus === row.key || isPinned ? 1 : 0.65)}
7709
9600
  data-legend-count={row.key}
7710
9601
  data-legend-count-empty={row.count === 0 ? 'true' : 'false'}
9602
+ data-legend-count-pinned={isPinned ? 'true' : 'false'}
9603
+ data-legend-count-font-weight={isPinned ? '700' : '600'}
7711
9604
  data-legend-count-fill={row.count > 0 && (hoveredStatus === row.key || isPinned) ? 'tier' : 'neutral'}
7712
- style={{ pointerEvents: 'none', transition: 'opacity 150ms ease-out, fill 150ms ease-out', fontVariantNumeric: 'tabular-nums' }}
9605
+ style={{ pointerEvents: 'none', transition: 'opacity 150ms ease-out, fill 150ms ease-out, font-weight 150ms ease-out', fontVariantNumeric: 'tabular-nums' }}
7713
9606
  >{row.count}</text>
7714
9607
  </g>
7715
9608
  );
@@ -7933,14 +9826,75 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7933
9826
  const isOn = s.status !== 'offline' || !!sseN;
7934
9827
  const st = nodeStatus(s, isOn, isLight);
7935
9828
  return (
9829
+ /* Round 372 / Loop: minimap offline-dot opacity
9830
+ 0.5 → 0.6. Sibling stale-state legibility lift
9831
+ to R358 freshness ramp floor 0.25 → 0.30 + R317
9832
+ subordinate-text-lift family. Pre-R372 R198
9833
+ drew offline dots at α=0.5 (44 % below online
9834
+ 0.9). The minimap is a small overlay against
9835
+ the canvas backdrop — at α=0.5 offline dots
9836
+ sat at the legibility floor when the minimap
9837
+ mounted (only on non-default view). R372 lifts
9838
+ offline 0.5 → 0.6 for +20 % relative presence;
9839
+ online stays at 0.9 so the offline/online
9840
+ contrast ratio is now 0.6/0.9 ≈ 0.67 (vs prior
9841
+ 0.5/0.9 ≈ 0.56) — still a clear two-tier
9842
+ distinction. R198 opacity + fill + r transition
9843
+ list preserved so status flips still ease
9844
+ smoothly. data-topo-minimap-dot-opacity attr
9845
+ exposes the resolved value for tests. */
7936
9846
  <circle
7937
9847
  key={s.alias}
7938
9848
  cx={p.x * sx} cy={p.y * sy}
7939
- r={isOn ? 1.7 : 1.2}
9849
+ /* Round 384 / Loop: minimap online dot radius 1.7
9850
+ → 1.9. Sibling visual-weight bump (10th anchor)
9851
+ to R383 recent-row pip 1.8 → 2.0. R198 designed
9852
+ the dots at 1.7 (online) / 1.2 (offline) — at
9853
+ the minimap's 120 × 82 scale these read clearly
9854
+ but the online ↔ offline contrast was modest
9855
+ (1.7/1.2 = 1.42×). R384 bumps online to 1.9 so
9856
+ the tier delta widens to 1.58× (1.9/1.2). Pair
9857
+ completes minimap-dot legibility polish:
9858
+ R358 (era R372) offline opacity 0.5 → 0.6
9859
+ R384 online radius 1.7 → 1.9 (this round)
9860
+ R198 transition list (opacity + fill + r 200ms)
9861
+ preserved so status flips still ease smoothly.
9862
+ data-topo-minimap-dot-radius attr exposes the
9863
+ resolved value for tests. */
9864
+ /* Round 392 / Loop: minimap online dot opacity
9865
+ 0.9 → 0.95. Theme-consistency / canvas-presence
9866
+ polish family (7th anchor) — mirrors R386's
9867
+ hub-highlight idle 0.9 → 0.95 lift on the
9868
+ minimap surface: the online-dot's idle alpha
9869
+ gap (0.10 against full presence) halves to
9870
+ 0.05, so the live-fleet anchors on the minimap
9871
+ read more confidently. Offline dot stays at
9872
+ R372 0.6 — the binary online/offline contrast
9873
+ ratio shifts from 0.6/0.9 ≈ 0.67 to 0.6/0.95
9874
+ ≈ 0.63, preserved as a clear two-tier
9875
+ distinction. R198 opacity + fill + r transition
9876
+ list + R384 r=1.9 + R372 offline 0.6 all
9877
+ preserved. data-topo-minimap-dot-opacity attr
9878
+ bumps to '0.95' for tests. */
9879
+ /* Round 421 / Loop: online dot opacity 0.95 → 1.0
9880
+ on minimap container hover. Sibling to R346
9881
+ viewport rect strokeWidth/opacity hover tween.
9882
+ When the user hovers the minimap container,
9883
+ the live-fleet anchors brighten from R392
9884
+ baseline (0.95) to full opacity in concert
9885
+ with the R346 viewport rect lift. Offline
9886
+ stays at R372 0.6 — hover state focuses
9887
+ attention on the ACTIVE anchors, not the
9888
+ stale ones. data-topo-minimap-dot-opacity
9889
+ attr (R392) reflects the resolved hover-
9890
+ state value for tests. */
9891
+ r={isOn ? 1.9 : 1.2}
7940
9892
  fill={st.primary}
7941
- opacity={isOn ? 0.9 : 0.5}
9893
+ opacity={isOn ? (hoveredMinimap ? 1 : 0.95) : 0.6}
7942
9894
  data-topo-minimap-dot={s.alias}
7943
9895
  data-topo-minimap-dot-online={isOn ? 'true' : 'false'}
9896
+ data-topo-minimap-dot-opacity={isOn ? (hoveredMinimap ? 1 : 0.95) : 0.6}
9897
+ data-topo-minimap-dot-radius={isOn ? 1.9 : 1.2}
7944
9898
  style={{
7945
9899
  transition: 'opacity 200ms ease-out, fill 200ms ease-out, r 200ms ease-out',
7946
9900
  } as React.CSSProperties}
@@ -7981,17 +9935,88 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7981
9935
  information element to lift it above ambient
7982
9936
  chrome. opacity 0.9 stays — strokeWidth alone
7983
9937
  does the lifting. */}
9938
+ {/* Round 379 / Loop: minimap viewport rect picks up
9939
+ strokeLinejoin='round'. Pre-R379 the rect's 4
9940
+ corners painted with default 'miter' joins —
9941
+ sharp 90° corners with a small miter overshoot
9942
+ (≈ strokeWidth × 1.4 = 2.1 px at sw=1.5). R379
9943
+ rounds the joins so corners arc smoothly through
9944
+ a quarter-circle of radius ≈ strokeWidth/2. At
9945
+ sw=1.5 that's a 0.75-px radius — subtle but
9946
+ matches the same stroke-softening vocabulary R288
9947
+ chrome icons (zoom/reset/fullscreen) and R378
9948
+ flow-rail already speak. Geometry-safe: stroke-
9949
+ linejoin only affects the corner overshoot, the
9950
+ rect's bbox is unchanged. R287 strokeWidth=1.5 +
9951
+ R346 hover-state strokeWidth/opacity bump + R199
9952
+ smoothView x/y/w/h transition all preserved.
9953
+ data-topo-minimap-viewport-linejoin attr exposes
9954
+ the value for tests. */}
9955
+ {/* Round 393 / Loop: minimap viewport rect rx 0 → 2.
9956
+ Pre-R393 the cyan-stroked viewport rect (the frame
9957
+ showing what's currently visible on the canvas)
9958
+ drew with sharp corners inside the R332 rounded
9959
+ minimap container (rx=8). A small frame with sharp
9960
+ corners sitting inside a rounded container reads
9961
+ as visually loud — the 90° corners catch the eye
9962
+ against the soft container edge. R393 adds rx=2
9963
+ so the viewport corners get a subtle radius that
9964
+ matches the family's softening idiom on a sub-
9965
+ element scale. The R379 strokeLinejoin='round'
9966
+ already softens stroke joins; R393 adds a complete
9967
+ geometric soften via rx.
9968
+ Corner-radius cascade (7 anchors now):
9969
+ R330 canvas rx 12
9970
+ R331 panels rx 10
9971
+ R332 minimap container rx 8
9972
+ R375 Layout-toggle rx 8
9973
+ R376 nodeSize/zoom rx 8
9974
+ R390 hover-detail rx 10
9975
+ R393 minimap viewport rx 2 (this round, sub-element)
9976
+ The 2-px radius is intentionally small — the
9977
+ viewport rect is typically only 30-50px wide,
9978
+ where rx=2 reads as "rounded enough to not snap"
9979
+ without feeling pillowy. data-topo-minimap-
9980
+ viewport-rx attr exposes the resolved value
9981
+ for tests. R346 hover-state tweens (strokeWidth
9982
+ + opacity) preserved verbatim. */}
7984
9983
  <rect
7985
9984
  x={Math.max(0, rectX)} y={Math.max(0, rectY)}
7986
9985
  width={Math.max(0, Math.min(MW - Math.max(0, rectX), rectW))}
7987
9986
  height={Math.max(0, Math.min(MH - Math.max(0, rectY), rectH))}
9987
+ rx="2"
7988
9988
  fill="none" stroke={pal.legendAccent}
7989
9989
  // R346: strokeWidth + opacity tween on container hover.
7990
9990
  strokeWidth={hoveredMinimap ? '1.75' : '1.5'}
7991
- opacity={hoveredMinimap ? '1' : '0.9'}
9991
+ strokeLinejoin="round"
9992
+ /* Round 450 / Loop · milestone: minimap viewport rest
9993
+ opacity 0.9 → 0.95. Closes half the alpha gap on
9994
+ the wayfinding indicator while preserving the
9995
+ R346 hover delta to 1.0. Pre-R450 the rest viewport
9996
+ sat at 0.9 (10 pct alpha gap) — adequate but
9997
+ under-confident for the user's primary "you are
9998
+ here" indicator on the minimap. R450 lifts to 0.95
9999
+ so the rest read is more present without erasing
10000
+ the hover lift cue (the +0.05 rest-to-hover delta
10001
+ is small but pairs with R346 sw 1.5→1.75 to keep
10002
+ hover clearly distinguishable). Sibling to R449
10003
+ legend-count active opacity 0.95→1.0 — same
10004
+ "close the active-presence alpha gap" idiom now
10005
+ applied to the REST tier of the wayfinding rect
10006
+ (the minimap viewport stays at canvas-presence
10007
+ register even when un-hovered since it's the
10008
+ spatial referent). Theme-consistency / canvas-
10009
+ presence family (8th anchor on the active-
10010
+ presence lift sub-arc).
10011
+ R287 strokeWidth=1.5 + R379 strokeLinejoin='round'
10012
+ + R346 hover-state tweens + R393 rx=2 + R199
10013
+ smoothView x/y/w/h transition all preserved. */
10014
+ opacity={hoveredMinimap ? '1' : '0.95'}
7992
10015
  data-topo-minimap-viewport
10016
+ data-topo-minimap-viewport-rx="2"
7993
10017
  data-topo-minimap-viewport-smooth={smoothView ? 'true' : 'false'}
7994
10018
  data-topo-minimap-viewport-hover={hoveredMinimap ? 'true' : 'false'}
10019
+ data-topo-minimap-viewport-linejoin="round"
7995
10020
  style={{
7996
10021
  transition: smoothView
7997
10022
  ? 'x 280ms ease-out, y 280ms ease-out, width 280ms ease-out, height 280ms ease-out, stroke-width 200ms ease-out, opacity 200ms ease-out'
@@ -8050,8 +10075,22 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8050
10075
  own transition-colors. Same R254 holdover pattern that
8051
10076
  R263 just closed at the canvas wrapper scope, now at the
8052
10077
  chrome strip's nodeSize sub-wrapper scope. */}
10078
+ {/* Round 376 / Loop: nodeSize wrapper rounded-md → rounded-lg.
10079
+ Sibling polish to R375 Layout-toggle wrapper. Three
10080
+ chrome-strip segmented controls now all share rounded-lg
10081
+ at the wrapper tier:
10082
+ R375 Layout-toggle wrapper rounded-lg 8 px
10083
+ R376 nodeSize wrapper rounded-lg 8 px (this round)
10084
+ R376 zoom wrapper rounded-lg 8 px (this round)
10085
+ Individual atomic chrome buttons (reset, fullscreen) keep
10086
+ rounded-md (6 px) as their own atomic-button tier — the
10087
+ chrome strip's typography now expresses a clear two-tier
10088
+ hierarchy: 'segmented control container' (rounded-lg)
10089
+ vs 'standalone button' (rounded-md). Pure paint change,
10090
+ no layout shift. */}
8053
10091
  <div
8054
- className="flex items-center rounded-md border overflow-hidden"
10092
+ className="flex items-center rounded-lg border overflow-hidden"
10093
+ data-topo-chrome-nodesize-radius="rounded-lg"
8055
10094
  style={{
8056
10095
  background: pal.legendBox.fill,
8057
10096
  borderColor: pal.containerBorder,
@@ -8131,8 +10170,12 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8131
10170
  data-topo-chrome-view-group-leader marks the boundary surface
8132
10171
  for the test probe; data-topo-chrome-fleet-group-trailer marks
8133
10172
  the nodeSize wrapper's right edge for the gap measurement. */}
10173
+ {/* R376 sibling — zoom wrapper rounded-md → rounded-lg.
10174
+ Closes the chrome-strip segmented-control corner radius
10175
+ cascade (Layout R375 + nodeSize R376 + zoom R376). */}
8134
10176
  <div
8135
- className="ml-1.5 flex items-center rounded-md border overflow-hidden"
10177
+ className="ml-1.5 flex items-center rounded-lg border overflow-hidden"
10178
+ data-topo-chrome-zoom-wrapper-radius="rounded-lg"
8136
10179
  style={{
8137
10180
  background: pal.legendBox.fill,
8138
10181
  borderColor: pal.containerBorder,
@@ -8148,7 +10191,8 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8148
10191
  // R196: press-state deepens bg one tier above hover (white/5
8149
10192
  // → white/10) so mouse-down has a tactile dim before the
8150
10193
  // R186 icon pop fires on release.
8151
- className="px-2 py-1 hover:bg-white/5 active:bg-white/10 transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset"
10194
+ // R352: `group` lets the inner svg respond via group-hover.
10195
+ className="group px-2 py-1 hover:bg-white/5 active:bg-white/10 transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset"
8152
10196
  style={{ color: pal.legendText }}
8153
10197
  aria-label="Zoom out"
8154
10198
  title="Zoom out (−)"
@@ -8156,11 +10200,25 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8156
10200
  {/* R186: icon pop on click. CSS animation runs once;
8157
10201
  React removes the class after 240ms so a quick
8158
10202
  re-click can replay. */}
10203
+ {/* Round 352 / Loop: zoom-out icon picks up group-hover:
10204
+ scale-110 — sibling to R350 reset hover-rotate. Pre-
10205
+ R352 hovering the zoom button only changed the bg
10206
+ (white/5); the icon inside stayed perfectly still.
10207
+ R352 lifts the icon 10% on hover for a tactile "this
10208
+ button does something" cue. The R186 anet-chrome-pop
10209
+ keyframe (220ms scale 1→1.06→1) still owns transform
10210
+ during click via CSS-animation precedence over
10211
+ transition-transform; after the pop ends + className
10212
+ is removed, the group-hover scale-110 picks up
10213
+ smoothly. `transform-gpu` hint promotes the svg to
10214
+ its own compositor layer for crisper edges during
10215
+ the scale tween. Sibling change on zoom-in icon
10216
+ below. */}
8159
10217
  <svg
8160
10218
  width="12" height="12" viewBox="0 0 24 24"
8161
10219
  fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"
8162
10220
  aria-hidden
8163
- className={chromePopping === 'zoom-out' ? 'anet-chrome-pop' : undefined}
10221
+ className={`transition-transform duration-200 ease-out group-hover:scale-110 transform-gpu${chromePopping === 'zoom-out' ? ' anet-chrome-pop' : ''}`}
8164
10222
  data-topo-chrome-zoom-out-icon
8165
10223
  ><path d="M5 12h14" /></svg>
8166
10224
  </button>
@@ -8217,6 +10275,24 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8217
10275
  // R347: letter-spacing hover tween — extends R344/R345
8218
10276
  // hover-letter-spacing family into the chrome strip.
8219
10277
  letterSpacing: hoveredZoomLevel ? '0.5px' : '0',
10278
+ // Round 420 / Loop: zoom-level readout gains a SECOND
10279
+ // hover axis — fontWeight 500 → 600 on hover. Sibling
10280
+ // to R347 (same element, hover letter-spacing tween).
10281
+ // The chrome strip's only data display now has a two-
10282
+ // axis hover signature (letter-spacing + fontWeight),
10283
+ // matching the R416 chip-row chip digit hover-bold
10284
+ // pattern at the chrome scope. Pre-R420 hovering only
10285
+ // spread the digits 0 → 0.5px; the weight stayed at
10286
+ // R332's 'font-medium' (500) baseline. Post-R420
10287
+ // hover lifts BOTH letter-spacing AND weight so the
10288
+ // percent reads with the same data-tier emphasis
10289
+ // intensification the chip-row chips do on hover.
10290
+ // Inline fontWeight overrides the className's
10291
+ // 'font-medium' since they target the same property.
10292
+ // 200ms transition list extends to font-weight for
10293
+ // smooth easing. data-topo-chrome-zoom-level-hover
10294
+ // attr surfaces the hover state for tests.
10295
+ fontWeight: hoveredZoomLevel ? 600 : 500,
8220
10296
  /* Round 264 / Loop: zoom level readout gains theme-toggle
8221
10297
  transition. The span has theme-driven color (pal.
8222
10298
  legendText) + border-x (pal.containerBorder via the
@@ -8225,7 +10301,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8225
10301
  on theme flip while siblings eased. Sibling treatment
8226
10302
  to the nodeSize + zoom wrapper transitions added this
8227
10303
  round. */
8228
- transition: 'color 200ms ease-out, border-color 200ms ease-out, letter-spacing 200ms ease-out',
10304
+ transition: 'color 200ms ease-out, border-color 200ms ease-out, letter-spacing 200ms ease-out, font-weight 200ms ease-out',
8229
10305
  }}
8230
10306
  title="Current zoom level"
8231
10307
  >
@@ -8236,18 +10312,22 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8236
10312
  data-topo-chrome-zoom-in
8237
10313
  data-topo-chrome-zoom-in-popping={chromePopping === 'zoom-in' ? 'true' : 'false'}
8238
10314
  // R196: press-state (mirror of zoom-out above).
8239
- className="px-2 py-1 hover:bg-white/5 active:bg-white/10 transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset"
10315
+ // R352: `group` lets the inner svg respond via group-hover.
10316
+ className="group px-2 py-1 hover:bg-white/5 active:bg-white/10 transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset"
8240
10317
  style={{ color: pal.legendText }}
8241
10318
  aria-label="Zoom in"
8242
10319
  title="Zoom in (+)"
8243
10320
  >
8244
10321
  {/* R186: icon pop on click. Same one-shot CSS animation
8245
10322
  as zoom-out; React removes the class after 240ms. */}
10323
+ {/* R352 sibling — zoom-in icon picks up the same
10324
+ group-hover:scale-110 family. Mirror change at
10325
+ the zoom-out icon above. */}
8246
10326
  <svg
8247
10327
  width="12" height="12" viewBox="0 0 24 24"
8248
10328
  fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"
8249
10329
  aria-hidden
8250
- className={chromePopping === 'zoom-in' ? 'anet-chrome-pop' : undefined}
10330
+ className={`transition-transform duration-200 ease-out group-hover:scale-110 transform-gpu${chromePopping === 'zoom-in' ? ' anet-chrome-pop' : ''}`}
8251
10331
  data-topo-chrome-zoom-in-icon
8252
10332
  ><path d="M12 5v14M5 12h14" /></svg>
8253
10333
  </button>
@@ -8264,7 +10344,27 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8264
10344
  onBlur={() => setHoveredReset(false)}
8265
10345
  // R196: press-state deepens before R184 reset-spin fires on
8266
10346
  // release — mouse-down dim then 450ms spin = full handshake.
8267
- className="p-1.5 rounded-md border hover:bg-white/5 active:bg-white/10 transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60"
10347
+ /* Round 400 / Loop · milestone: chrome reset + fullscreen
10348
+ buttons gain hover:-translate-y-px lift — closes the
10349
+ hover-lift gesture vocabulary across every standalone
10350
+ interactive HTML element in TopoGraph. Segmented
10351
+ controls (zoom -/+, nodeSize S/M/L, Layout Ring/Grid)
10352
+ intentionally stay planted: lifting one segment of a
10353
+ unified strip would tear the visual unity of the
10354
+ segmented control. Only the standalone chrome buttons
10355
+ (reset, fullscreen) get the lift.
10356
+ Gesture vocabulary post-R400 (now complete across HTML):
10357
+ chip-row chips (3×) -1 px R398, R399
10358
+ filter pin pills (4×) -1 px R397
10359
+ recent-signal row -1 px R143
10360
+ legend row -1 px R144
10361
+ reset button -1 px R400 (this round)
10362
+ fullscreen button -1 px R400 (this round)
10363
+ Every standalone interactive HTML surface in TopoGraph
10364
+ now lifts on hover. data-topo-chrome-reset-hover-lift
10365
+ attr surfaces the lift for tests. */
10366
+ className="p-1.5 rounded-md border hover:bg-white/5 active:bg-white/10 hover:-translate-y-px transition-colors transition-transform duration-200 ease-out transform-gpu focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60"
10367
+ data-topo-chrome-reset-hover-lift="true"
8268
10368
  style={{ background: pal.legendBox.fill, borderColor: pal.containerBorder, color: pal.legendText }}
8269
10369
  aria-label="Reset view"
8270
10370
  title="Reset zoom + pan (0, or double-click the canvas)"
@@ -8335,11 +10435,18 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8335
10435
  its inactive state benefits from the same "hover previews
8336
10436
  active state" idiom R163 designed. Sibling treatment to
8337
10437
  the nodeSize buttons at line ~6711. */
8338
- className={`p-1.5 rounded-md border transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 ${
10438
+ // R353: `group` lets the inner svg respond via group-hover —
10439
+ // sibling to R352 zoom buttons. Closes the chrome-strip per-
10440
+ // icon hover-affordance arc (zoom-out / zoom-in / reset /
10441
+ // fullscreen now all carry an icon-level hover gesture in
10442
+ // addition to the bg hover).
10443
+ // R400: hover translateY(-1px) lift — see reset button above for family doc.
10444
+ className={`group p-1.5 rounded-md border hover:-translate-y-px transition-colors transition-transform duration-200 ease-out transform-gpu focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 ${
8339
10445
  isFullscreen
8340
10446
  ? 'bg-cyan-500/15 text-cyan-300 font-medium hover:bg-cyan-500/20 active:bg-cyan-500/25'
8341
10447
  : 'hover:bg-cyan-500/5 active:bg-cyan-500/15'
8342
10448
  }${chromePopping === 'fullscreen' ? ' anet-chrome-pop' : ''}`}
10449
+ data-topo-chrome-fullscreen-hover-lift="true"
8343
10450
  style={{
8344
10451
  borderColor: pal.containerBorder,
8345
10452
  ...(isFullscreen
@@ -8354,12 +10461,20 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8354
10461
  at the reset icon above. data-topo-chrome-fullscreen-
8355
10462
  icon attribute exposes BOTH variants (entered / exited)
8356
10463
  for the round's stroke-width regression probe. */}
10464
+ {/* Round 353 / Loop: fullscreen icon (both enter + exit
10465
+ variants) picks up the R352 family group-hover:scale-110.
10466
+ Pre-R353 hovering the button only changed the bg; the
10467
+ icon stayed still. R353 lifts the icon 10 % on hover —
10468
+ same gesture vocabulary as the zoom buttons. transform-
10469
+ gpu hint promotes the svg to its own compositor layer
10470
+ for crisper edges during the scale tween. Closes the
10471
+ chrome-strip per-icon hover-affordance arc. */}
8357
10472
  {isFullscreen ? (
8358
- <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden data-topo-chrome-fullscreen-icon="exit">
10473
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden className="transition-transform duration-200 ease-out group-hover:scale-110 transform-gpu" data-topo-chrome-fullscreen-icon="exit">
8359
10474
  <path d="M8 3v4a1 1 0 0 1-1 1H3M21 8h-4a1 1 0 0 1-1-1V3M3 16h4a1 1 0 0 1 1 1v4M16 21v-4a1 1 0 0 1 1-1h4" />
8360
10475
  </svg>
8361
10476
  ) : (
8362
- <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden data-topo-chrome-fullscreen-icon="enter">
10477
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden className="transition-transform duration-200 ease-out group-hover:scale-110 transform-gpu" data-topo-chrome-fullscreen-icon="enter">
8363
10478
  <path d="M3 8V5a2 2 0 0 1 2-2h3M21 8V5a2 2 0 0 0-2-2h-3M3 16v3a2 2 0 0 0 2 2h3M21 16v3a2 2 0 0 1-2 2h-3" />
8364
10479
  </svg>
8365
10480
  )}