@sleep2agi/agent-network-dashboard 0.5.1-preview.14 → 0.5.1-preview.141

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 (284) 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 +32 -32
  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 +12 -12
  16. package/.next/server/app/_not-found.segments/_full.segment.rsc +12 -12
  17. package/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  18. package/.next/server/app/_not-found.segments/_index.segment.rsc +7 -7
  19. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  20. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  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 +14 -14
  25. package/.next/server/app/admin.segments/_full.segment.rsc +14 -14
  26. package/.next/server/app/admin.segments/_head.segment.rsc +4 -4
  27. package/.next/server/app/admin.segments/_index.segment.rsc +7 -7
  28. package/.next/server/app/admin.segments/_tree.segment.rsc +2 -2
  29. package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +4 -4
  30. package/.next/server/app/admin.segments/admin.segment.rsc +3 -3
  31. package/.next/server/app/index.html +2 -2
  32. package/.next/server/app/index.rsc +14 -14
  33. package/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
  34. package/.next/server/app/index.segments/_full.segment.rsc +14 -14
  35. package/.next/server/app/index.segments/_head.segment.rsc +4 -4
  36. package/.next/server/app/index.segments/_index.segment.rsc +7 -7
  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 +14 -14
  41. package/.next/server/app/login.segments/_full.segment.rsc +14 -14
  42. package/.next/server/app/login.segments/_head.segment.rsc +4 -4
  43. package/.next/server/app/login.segments/_index.segment.rsc +7 -7
  44. package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
  45. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +4 -4
  46. package/.next/server/app/login.segments/login.segment.rsc +3 -3
  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 +14 -14
  50. package/.next/server/app/logs.segments/_full.segment.rsc +14 -14
  51. package/.next/server/app/logs.segments/_head.segment.rsc +4 -4
  52. package/.next/server/app/logs.segments/_index.segment.rsc +7 -7
  53. package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
  54. package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +4 -4
  55. package/.next/server/app/logs.segments/logs.segment.rsc +3 -3
  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 +14 -14
  59. package/.next/server/app/messages.segments/_full.segment.rsc +14 -14
  60. package/.next/server/app/messages.segments/_head.segment.rsc +4 -4
  61. package/.next/server/app/messages.segments/_index.segment.rsc +7 -7
  62. package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
  63. package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +4 -4
  64. package/.next/server/app/messages.segments/messages.segment.rsc +3 -3
  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 +14 -14
  68. package/.next/server/app/node.segments/_full.segment.rsc +14 -14
  69. package/.next/server/app/node.segments/_head.segment.rsc +4 -4
  70. package/.next/server/app/node.segments/_index.segment.rsc +7 -7
  71. package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
  72. package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +4 -4
  73. package/.next/server/app/node.segments/node.segment.rsc +3 -3
  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 +14 -14
  77. package/.next/server/app/nodes.segments/_full.segment.rsc +14 -14
  78. package/.next/server/app/nodes.segments/_head.segment.rsc +4 -4
  79. package/.next/server/app/nodes.segments/_index.segment.rsc +7 -7
  80. package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
  81. package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +4 -4
  82. package/.next/server/app/nodes.segments/nodes.segment.rsc +3 -3
  83. package/.next/server/app/page.js.nft.json +1 -1
  84. package/.next/server/app/page_client-reference-manifest.js +1 -1
  85. package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
  86. package/.next/server/app/server-logs.html +2 -2
  87. package/.next/server/app/server-logs.rsc +14 -14
  88. package/.next/server/app/server-logs.segments/_full.segment.rsc +14 -14
  89. package/.next/server/app/server-logs.segments/_head.segment.rsc +4 -4
  90. package/.next/server/app/server-logs.segments/_index.segment.rsc +7 -7
  91. package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
  92. package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +4 -4
  93. package/.next/server/app/server-logs.segments/server-logs.segment.rsc +3 -3
  94. package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
  95. package/.next/server/app/settings/networks.html +2 -2
  96. package/.next/server/app/settings/networks.rsc +14 -14
  97. package/.next/server/app/settings/networks.segments/_full.segment.rsc +14 -14
  98. package/.next/server/app/settings/networks.segments/_head.segment.rsc +4 -4
  99. package/.next/server/app/settings/networks.segments/_index.segment.rsc +7 -7
  100. package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
  101. package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +4 -4
  102. package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +3 -3
  103. package/.next/server/app/settings/networks.segments/settings.segment.rsc +3 -3
  104. package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  105. package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
  106. package/.next/server/app/settings/tokens.html +2 -2
  107. package/.next/server/app/settings/tokens.rsc +14 -14
  108. package/.next/server/app/settings/tokens.segments/_full.segment.rsc +14 -14
  109. package/.next/server/app/settings/tokens.segments/_head.segment.rsc +4 -4
  110. package/.next/server/app/settings/tokens.segments/_index.segment.rsc +7 -7
  111. package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
  112. package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +4 -4
  113. package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +3 -3
  114. package/.next/server/app/settings/tokens.segments/settings.segment.rsc +3 -3
  115. package/.next/server/app/settings.html +2 -2
  116. package/.next/server/app/settings.rsc +14 -14
  117. package/.next/server/app/settings.segments/_full.segment.rsc +14 -14
  118. package/.next/server/app/settings.segments/_head.segment.rsc +4 -4
  119. package/.next/server/app/settings.segments/_index.segment.rsc +7 -7
  120. package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  121. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
  122. package/.next/server/app/settings.segments/settings.segment.rsc +3 -3
  123. package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
  124. package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
  125. package/.next/server/app/tasks.html +2 -2
  126. package/.next/server/app/tasks.rsc +14 -14
  127. package/.next/server/app/tasks.segments/_full.segment.rsc +14 -14
  128. package/.next/server/app/tasks.segments/_head.segment.rsc +4 -4
  129. package/.next/server/app/tasks.segments/_index.segment.rsc +7 -7
  130. package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
  131. package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +4 -4
  132. package/.next/server/app/tasks.segments/tasks.segment.rsc +3 -3
  133. package/.next/server/chunks/ssr/{[root-of-the-server]__03b.f76._.js → [root-of-the-server]__0sv~g.o._.js} +2 -2
  134. package/.next/server/chunks/ssr/[root-of-the-server]__0sv~g.o._.js.map +1 -0
  135. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js +3 -3
  136. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js.map +1 -1
  137. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js +1 -1
  138. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js.map +1 -1
  139. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js +1 -1
  140. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js.map +1 -1
  141. package/.next/server/chunks/ssr/agent-network-dashboard_app_components_0mvyi-4._.js +1 -1
  142. package/.next/server/chunks/ssr/agent-network-dashboard_app_components_0mvyi-4._.js.map +1 -1
  143. package/.next/server/middleware-build-manifest.js +3 -3
  144. package/.next/server/pages/404.html +2 -2
  145. package/.next/server/pages/500.html +1 -1
  146. package/.next/server/server-reference-manifest.js +1 -1
  147. package/.next/server/server-reference-manifest.json +1 -1
  148. package/.next/static/chunks/{0l_~q07bhpkcx.js → 03a4--7ncekmk.js} +1 -1
  149. package/.next/static/chunks/05kol0eq4~_d5.js +4 -0
  150. package/.next/static/chunks/{03~~oirxz7~vc.js → 07gxjdi4w5ot1.js} +1 -1
  151. package/.next/static/chunks/0a4hmfvj-81x5.css +2 -0
  152. package/.next/static/chunks/0b_0vo6gv.2d0.js +1 -0
  153. package/.next/static/chunks/0o~yjk9gw3w3d.js +1 -0
  154. package/.next/trace +2 -2
  155. package/.next/trace-build +1 -1
  156. package/app/components/ServersDrawer.tsx +17 -0
  157. package/app/components/TopoGraph.tsx +2558 -188
  158. package/package.json +1 -1
  159. package/screenshots/v0.10.2-disk-verify/disk-render-full.png +0 -0
  160. package/screenshots/v0.10.2-disk-verify/disk-render.png +0 -0
  161. package/screenshots/v0.11.0-147/after.png +0 -0
  162. package/scripts/p0-147-screenshot.mjs +53 -0
  163. package/scripts/p0-servers-drawer-screenshot.mjs +2 -0
  164. package/scripts/topo-active-links-chip-hover-lift-test.mjs +93 -0
  165. package/scripts/topo-chip-digit-fontweight-test.mjs +105 -0
  166. package/scripts/topo-chip-digit-hover-bold-test.mjs +94 -0
  167. package/scripts/topo-chip-row-group-hover-brighten-test.mjs +107 -0
  168. package/scripts/topo-chip-row-hover-lift-test.mjs +95 -0
  169. package/scripts/topo-chrome-button-hover-lift-test.mjs +94 -0
  170. package/scripts/topo-chrome-fullscreen-icon-sw-test.mjs +92 -0
  171. package/scripts/topo-chrome-reset-icon-sw-test.mjs +80 -0
  172. package/scripts/topo-chrome-segmented-radius-test.mjs +100 -0
  173. package/scripts/topo-chrome-zoom-icon-sw-test.mjs +90 -0
  174. package/scripts/topo-click-ripple-opacity-test.mjs +99 -0
  175. package/scripts/topo-dashboard-version-attr-test.mjs +69 -0
  176. package/scripts/topo-dense-alias-opacity-test.mjs +68 -0
  177. package/scripts/topo-edge-badge-digit-fw-test.mjs +103 -0
  178. package/scripts/topo-edge-badge-fontsize-test.mjs +90 -0
  179. package/scripts/topo-edge-badge-hover-opacity-test.mjs +94 -0
  180. package/scripts/topo-edge-badge-hover-stroke-test.mjs +92 -0
  181. package/scripts/topo-edge-badge-letter-spacing-test.mjs +86 -0
  182. package/scripts/topo-edge-badge-opacity-test.mjs +80 -0
  183. package/scripts/topo-edge-badge-pin-opacity-test.mjs +86 -0
  184. package/scripts/topo-edge-badge-stroke-test.mjs +92 -0
  185. package/scripts/topo-edge-endpoint-hover-sw-test.mjs +127 -0
  186. package/scripts/topo-edge-freshness-floor-test.mjs +99 -0
  187. package/scripts/topo-edge-particle-hover-r-test.mjs +113 -0
  188. package/scripts/topo-edge-particle-radius-test.mjs +76 -0
  189. package/scripts/topo-edge-visible-linecap-test.mjs +89 -0
  190. package/scripts/topo-endpoint-ring-r-hover-test.mjs +89 -0
  191. package/scripts/topo-filter-pill-hover-lift-test.mjs +101 -0
  192. package/scripts/topo-filter-pill-hover-opacity-test.mjs +110 -0
  193. package/scripts/topo-filter-pill-value-fw-test.mjs +88 -0
  194. package/scripts/topo-filter-pill-x-hover-scale-test.mjs +99 -0
  195. package/scripts/topo-flow-rail-hover-sw-test.mjs +113 -0
  196. package/scripts/topo-flow-rail-linecap-test.mjs +79 -0
  197. package/scripts/topo-freshness-chip-hierarchy-test.mjs +93 -0
  198. package/scripts/topo-freshness-chip-tabular-test.mjs +41 -0
  199. package/scripts/topo-freshness-floor-lift-test.mjs +92 -0
  200. package/scripts/topo-freshness-suffix-tabular-test.mjs +88 -0
  201. package/scripts/topo-group-box-geom-transition-test.mjs +110 -0
  202. package/scripts/topo-group-box-rx-pin-test.mjs +103 -0
  203. package/scripts/topo-group-box-stroke-test.mjs +105 -0
  204. package/scripts/topo-group-label-count-fontweight-test.mjs +108 -0
  205. package/scripts/topo-group-label-count-fw-test.mjs +100 -0
  206. package/scripts/topo-group-label-fw-pin-test.mjs +99 -0
  207. package/scripts/topo-group-label-letter-spacing-test.mjs +113 -0
  208. package/scripts/topo-group-label-tint-geom-test.mjs +94 -0
  209. package/scripts/topo-group-label-tint-transition-test.mjs +97 -0
  210. package/scripts/topo-group-pip-fontsize-test.mjs +106 -0
  211. package/scripts/topo-group-tint-rx-pin-test.mjs +107 -0
  212. package/scripts/topo-hover-detail-body-fw-test.mjs +101 -0
  213. package/scripts/topo-hover-detail-model-fw-test.mjs +98 -0
  214. package/scripts/topo-hover-detail-opacity-test.mjs +98 -0
  215. package/scripts/topo-hover-detail-rx-test.mjs +81 -0
  216. package/scripts/topo-hub-core-fill-hover-test.mjs +85 -0
  217. package/scripts/topo-hub-digit-fontsize-test.mjs +86 -0
  218. package/scripts/topo-hub-digit-fw-hover-test.mjs +102 -0
  219. package/scripts/topo-hub-halo-light-trough-test.mjs +88 -0
  220. package/scripts/topo-hub-halo-r-hover-test.mjs +82 -0
  221. package/scripts/topo-hub-halo-radius-test.mjs +86 -0
  222. package/scripts/topo-hub-halo-trough-test.mjs +83 -0
  223. package/scripts/topo-hub-highlight-opacity-test.mjs +88 -0
  224. package/scripts/topo-hub-highlight-radius-test.mjs +90 -0
  225. package/scripts/topo-hub-hover-ring-opacity-test.mjs +96 -0
  226. package/scripts/topo-hub-hover-ring-stroke-test.mjs +86 -0
  227. package/scripts/topo-hub-spoke-hover-opacity-test.mjs +119 -0
  228. package/scripts/topo-hub-spoke-hover-sw-test.mjs +121 -0
  229. package/scripts/topo-hub-spoke-linecap-test.mjs +80 -0
  230. package/scripts/topo-label-card-opacity-hover-test.mjs +99 -0
  231. package/scripts/topo-layout-toggle-radius-test.mjs +87 -0
  232. package/scripts/topo-legend-count-active-opacity-test.mjs +102 -0
  233. package/scripts/topo-legend-count-pin-fw-test.mjs +90 -0
  234. package/scripts/topo-legend-label-fontweight-test.mjs +94 -0
  235. package/scripts/topo-legend-pin-ring-stroke-test.mjs +101 -0
  236. package/scripts/topo-legend-row-letter-spacing-test.mjs +101 -0
  237. package/scripts/topo-minimap-offline-opacity-test.mjs +90 -0
  238. package/scripts/topo-minimap-online-hover-opacity-test.mjs +92 -0
  239. package/scripts/topo-minimap-online-opacity-test.mjs +93 -0
  240. package/scripts/topo-minimap-online-radius-test.mjs +85 -0
  241. package/scripts/topo-minimap-viewport-linejoin-test.mjs +75 -0
  242. package/scripts/topo-minimap-viewport-opacity-test.mjs +96 -0
  243. package/scripts/topo-minimap-viewport-rx-test.mjs +85 -0
  244. package/scripts/topo-more-flows-fontweight-test.mjs +103 -0
  245. package/scripts/topo-node-alias-letter-spacing-test.mjs +112 -0
  246. package/scripts/topo-node-halo-hover-opacity-test.mjs +104 -0
  247. package/scripts/topo-node-halo-light-offline-test.mjs +80 -0
  248. package/scripts/topo-node-halo-offline-opacity-test.mjs +87 -0
  249. package/scripts/topo-node-label-card-rx-test.mjs +85 -0
  250. package/scripts/topo-node-pulse-peak-test.mjs +89 -0
  251. package/scripts/topo-node-pulse-trough-test.mjs +91 -0
  252. package/scripts/topo-node-sub-text-fw-test.mjs +75 -0
  253. package/scripts/topo-node-sub-text-letter-spacing-test.mjs +115 -0
  254. package/scripts/topo-overlap-stale-build-guard-test.mjs +66 -0
  255. package/scripts/topo-overlap-test.mjs +42 -1
  256. package/scripts/topo-panel-count-fw-hover-test.mjs +105 -0
  257. package/scripts/topo-panel-stroke-hover-test.mjs +110 -0
  258. package/scripts/topo-pressure-bar-height-test.mjs +92 -0
  259. package/scripts/topo-pressure-kicker-fontweight-test.mjs +76 -0
  260. package/scripts/topo-recent-pip-radius-2-test.mjs +72 -0
  261. package/scripts/topo-recent-pip-radius-test.mjs +76 -0
  262. package/scripts/topo-recent-row-content-opacity-test.mjs +81 -0
  263. package/scripts/topo-recent-row-count-pin-fw-test.mjs +106 -0
  264. package/scripts/topo-recent-row-letter-spacing-test.mjs +109 -0
  265. package/scripts/topo-recent-row-pip-hover-r-test.mjs +104 -0
  266. package/scripts/topo-recent-row-text-fontweight-test.mjs +90 -0
  267. package/scripts/topo-runtime-icon-hover-test.mjs +96 -0
  268. package/scripts/topo-spoke-active-opacity-test.mjs +104 -0
  269. package/scripts/topo-spoke-active-stroke-test.mjs +95 -0
  270. package/scripts/topo-spoke-idle-opacity-test.mjs +91 -0
  271. package/scripts/topo-status-ring-hover-sw-test.mjs +105 -0
  272. package/scripts/topo-vendor-chip-hover-lift-test.mjs +87 -0
  273. package/scripts/topo-vendor-glyph-fontweight-test.mjs +102 -0
  274. package/scripts/topo-vendor-letter-hover-scale-test.mjs +129 -0
  275. package/scripts/topo-vendor-suffix-hover-brighten-test.mjs +87 -0
  276. package/scripts/topo-zoom-level-hover-fw-test.mjs +95 -0
  277. package/.next/server/chunks/ssr/[root-of-the-server]__03b.f76._.js.map +0 -1
  278. package/.next/static/chunks/03mfrl9fd9smg.js +0 -1
  279. package/.next/static/chunks/0kt7qs66ra2.2.css +0 -2
  280. package/.next/static/chunks/116zy6jdhuaa2.js +0 -1
  281. package/.next/static/chunks/12m~64tlig.y4.js +0 -4
  282. /package/.next/static/{rDAS8dVgx4ldg7AtRGdUB → Bv_r6o_p1UTgHlTvQaZNt}/_buildManifest.js +0 -0
  283. /package/.next/static/{rDAS8dVgx4ldg7AtRGdUB → Bv_r6o_p1UTgHlTvQaZNt}/_clientMiddlewareManifest.js +0 -0
  284. /package/.next/static/{rDAS8dVgx4ldg7AtRGdUB → Bv_r6o_p1UTgHlTvQaZNt}/_ssgManifest.js +0 -0
@@ -8,6 +8,7 @@ import { aliasAvatarColors, aliasInitial } from './AliasAvatar';
8
8
  import { ChatPopover } from './ChatPopover';
9
9
  import { vendorForModel, runtimeIdentity, identityLine } from '../lib/vendorIdentity';
10
10
  import { parseHubTime, relativeAgo } from '../lib/time';
11
+ import { DASHBOARD_VERSION } from '../lib/version';
11
12
 
12
13
  /** v0.10.0 Hero 1+2 / §3.F server-health hook — fetches the normalized
13
14
  * /api/hub/servers payload (preview.370 unblocked real-data via the
@@ -194,7 +195,27 @@ function FreshnessChip({ sessions }: { sessions: unknown }) {
194
195
  // tier (R312-R314 family); the amber bg/text/border still does
195
196
  // the warning-state work, the weight just keeps the chip in the
196
197
  // 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";
198
+ // Round 377 / Loop: FreshnessChip baseClass picks up `tabular-nums`.
199
+ // The chip-row's last untouched chip joins the R224-R357 broader
200
+ // tabular-nums sweep:
201
+ // R224 edge badge digit
202
+ // R225 hub digit / panel header counts / recent row count
203
+ // R232 chip row counts (working / online / active-links)
204
+ // R321 recent row timestamp
205
+ // R322 recent panel hot count
206
+ // R323 filter pin pill counts
207
+ // R333 vendor count suffix
208
+ // R357 active-links freshness suffix wrapper
209
+ // R377 FreshnessChip body (this round)
210
+ // `font-mono` already gives equal-width glyphs but `tabular-nums`
211
+ // is the explicit-invariant the rest of the chip row carries.
212
+ // FreshnessChip body reads `lag · {sec}s` — the {sec} digit grows
213
+ // every second; tabular-nums explicitly locks digit width so the
214
+ // chip stays planted as the seconds counter ticks past 9 → 10 →
215
+ // 99 → 100. R187 transition-colors duration-300 + R275 stale-only
216
+ // render gate + R315 font-medium R313 family alignment all
217
+ // preserved.
218
+ const baseClass = "hidden sm:inline px-2.5 py-1 rounded-md font-mono font-medium tabular-nums border transition-colors duration-300";
198
219
  const colorClass = stale
199
220
  ? "bg-amber-500/10 text-amber-300 border-amber-500/20"
200
221
  : "bg-gray-500/10 text-gray-400 border-gray-500/20";
@@ -237,7 +258,21 @@ function FreshnessChip({ sessions }: { sessions: unknown }) {
237
258
  duration-300 still eases the bg/color flip. Title (hover
238
259
  tooltip) still spells out the full meaning in either
239
260
  state. */}
240
- {stale ? 'lag' : 'live'} · {sec}s
261
+ {/* Round 410 / Loop: FreshnessChip body picks up the chip-
262
+ internal-hierarchy arc. Pre-R410 the body rendered as a
263
+ single text node `lag · {sec}s` with the parent's font-
264
+ medium (fw=500) applied uniformly. R410 splits the digit
265
+ and unit into separate spans so the chip's internal
266
+ typography mirrors the family pattern R333-R341/R362/R369/
267
+ R389 established for the chip row:
268
+ digit (fw=600) data tier
269
+ unit (fw=500 + opacity=0.7) label tier
270
+ The `lag` prefix stays at the chip's baseline (fw=500
271
+ from parent font-medium) — it labels the state, not a
272
+ data value. data-freshness-chip-digit / -unit attrs
273
+ surface the spans for tests. tabular-nums + transition-
274
+ colors + R275 stale-only gate all preserved. */}
275
+ {stale ? 'lag' : 'live'} · <span className="font-semibold" data-freshness-chip-digit>{sec}</span><span className="opacity-70" data-freshness-chip-unit>s</span>
241
276
  </span>
242
277
  );
243
278
  }
@@ -1823,12 +1858,30 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1823
1858
  while honoring R328's wider baseline rhythm. data-topo-
1824
1859
  chrome-layout-trailer attr unchanged — it still marks
1825
1860
  the boundary surface for the gap probe. */}
1861
+ {/* Round 375 / Loop: Layout-toggle wrapper rounded-md → rounded-
1862
+ lg (6 → 8 px). Extends the corner-radius cascade family
1863
+ to the chrome-strip layout-toggle wrapper:
1864
+ R330 canvas wrapper rounded-xl 12 px
1865
+ R331 SVG panels rx=10 10 px
1866
+ R332 minimap container rounded-lg 8 px
1867
+ R375 Layout-toggle wrapper rounded-lg 8 px (this round)
1868
+ Pre-R375 the wrapper at rounded-md (6 px) was the only
1869
+ chrome-strip container still using the smaller corner
1870
+ radius — both R330 outer wrapper and R332 minimap sit at
1871
+ ≥ 8 px, so the Layout toggle's 6 px stood out as a
1872
+ tighter corner against the family. R375 brings it into
1873
+ the rounded-lg tier where the minimap already lives.
1874
+ Pure paint change — overflow-hidden still clips the
1875
+ inner buttons' bg-cyan-500/15 tints; no layout shift.
1876
+ R268 border-color + 200ms transition + R329 mr-0.5 +
1877
+ data-topo-chrome-layout-trailer all preserved. */}
1826
1878
  <div
1827
- className="mr-0.5 inline-flex rounded-md border overflow-hidden"
1879
+ className="mr-0.5 inline-flex rounded-lg border overflow-hidden"
1828
1880
  style={{ borderColor: pal.containerBorder, transition: 'border-color 200ms ease-out' }}
1829
1881
  role="group"
1830
1882
  aria-label="Topology layout"
1831
1883
  data-topo-chrome-layout-trailer
1884
+ data-topo-chrome-layout-radius="rounded-lg"
1832
1885
  >
1833
1886
  <button
1834
1887
  onClick={() => { popChrome('layout-ring'); if (layout !== 'ring') toggleLayout(); }}
@@ -1989,11 +2042,43 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1989
2042
  7th surface in the info-density tabular-nums
1990
2043
  sweep — and the first on the HTML side
1991
2044
  (previous 6 were SVG <text>/<tspan>). */
1992
- className={`tabular-nums font-medium px-2.5 py-1 rounded-md border anet-topo-chip-focus transition-colors duration-200 ${
2045
+ /* Round 398 / Loop: chip-row chips gain hover translateY
2046
+ (-1px) lift on the CLICKABLE variant only (workingCount
2047
+ > 0 here / onlineNodes.length > 0 below / activeLinks
2048
+ > 0 deeper). Pre-R398 the chips brightened bg + border
2049
+ on hover (R201) but didn't lift — only their clickable
2050
+ siblings (filter pin pills R397, recent rows R143,
2051
+ legend rows R144) acknowledged cursor entry with a
2052
+ translate-y. R398 closes the chip-row by extending
2053
+ the same gesture to the static header chips, gated
2054
+ on the clickable role so empty chips (which have
2055
+ no role="button") stay planted at their R205
2056
+ opacity-50 receded paint. transition-transform
2057
+ + duration-200 + ease-out + transform-gpu added
2058
+ alongside existing transition-colors so the lift
2059
+ and the color tween share rhythm.
2060
+ Gesture-vocabulary table (post-R398):
2061
+ recent-signal row -1 px (R143)
2062
+ legend row -1 px (R144)
2063
+ group cluster box fill+sw lift (R142)
2064
+ filter pin pills -1 px (R397)
2065
+ chip-row chips -1 px (R398, this round)
2066
+ Empty chips: no lift. Pin-mirror chips: no
2067
+ conflict (R180 inset double-ring is a box-shadow
2068
+ not a transform). new data-chip-hover-lift attr
2069
+ surfaces the lift surface for tests. */
2070
+ // R414: chip-row chips gain `group` so inner unit
2071
+ // span brightens via group-hover:opacity-100 — sibling
2072
+ // to R355 filter pin pill inner-span hover-brighten.
2073
+ // Hover-brighten family extends from filter pills to
2074
+ // chip-row chips at the inner-span scope.
2075
+ 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 ${
1993
2076
  workingCount > 0
1994
- ? 'bg-green-500/10 text-green-300 border-green-500/20 hover:bg-green-500/15 hover:border-green-500/30'
2077
+ ? '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'
1995
2078
  : 'bg-green-500/10 text-green-300 border-green-500/20'
1996
2079
  }`}
2080
+ data-chip-hover-lift={workingCount > 0 ? 'true' : 'false'}
2081
+ data-chip-group-hover-brighten="true"
1997
2082
  data-working-chip
1998
2083
  data-working-chip-aliases={workingAliases.join(',')}
1999
2084
  data-pin-mirror={pinnedStatus === 'working' ? 'true' : 'false'}
@@ -2063,7 +2148,17 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2063
2148
  pattern: small label spans demote, value stays
2064
2149
  prominent. data-working-chip-unit exposes the
2065
2150
  span for tests. */}
2066
- {workingCount}<span className="opacity-70" data-working-chip-unit> working</span>
2151
+ {/* Round 362 / Loop: digit picks up font-semibold
2152
+ (fw 500 → 600) for within-chip weight tier. The
2153
+ chip's outer className stays at font-medium (R313
2154
+ data-weight baseline); the digit overrides to
2155
+ semibold so it reads heavier than its " working"
2156
+ unit (which keeps fw 500 + R338 opacity-70).
2157
+ Joins the R333-R341 chip-internal-hierarchy arc
2158
+ at the chip-count scope. Sibling edits on the
2159
+ online + active-links chip digits below. data-
2160
+ working-chip-digit attr exposes the digit span. */}
2161
+ <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>
2067
2162
  </span>
2068
2163
  <span
2069
2164
  // Round 201 / Loop: online chip — mirror of the working
@@ -2077,11 +2172,15 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2077
2172
  /* Round 232 / Loop: tabular-nums on online chip
2078
2173
  (sibling treatment to working chip — same row,
2079
2174
  same digit-jitter physics on count crossings). */
2080
- className={`tabular-nums font-medium px-2.5 py-1 rounded-md border anet-topo-chip-focus transition-colors duration-200 ${
2175
+ // R398: hover translate-y lift on clickable variant — see working chip above.
2176
+ // R414: `group` parent + inner unit span group-hover-brighten — see working chip above.
2177
+ 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 ${
2081
2178
  onlineNodes.length > 0
2082
- ? 'bg-cyan-500/10 text-cyan-300 border-cyan-500/20 hover:bg-cyan-500/15 hover:border-cyan-500/30'
2179
+ ? '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'
2083
2180
  : 'bg-cyan-500/10 text-cyan-300 border-cyan-500/20'
2084
2181
  }`}
2182
+ data-chip-hover-lift={onlineNodes.length > 0 ? 'true' : 'false'}
2183
+ data-chip-group-hover-brighten="true"
2085
2184
  data-online-chip
2086
2185
  data-online-chip-aliases={onlineAliases.join(',')}
2087
2186
  data-pin-mirror={pinnedStatus === 'idle' ? 'true' : 'false'}
@@ -2126,7 +2225,8 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2126
2225
  }}
2127
2226
  >
2128
2227
  {/* R337 sibling — online chip unit demotion. */}
2129
- {onlineNodes.length}<span className="opacity-70" data-online-chip-unit> online</span>
2228
+ {/* R362 sibling — online-chip digit gains font-semibold. */}
2229
+ <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>
2130
2230
  </span>
2131
2231
  </>
2132
2232
  );
@@ -2253,8 +2353,45 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2253
2353
  title={`${w} working · ${i} idle · ${o} offline`}
2254
2354
  data-fleet-pressure
2255
2355
  >
2256
- <span className="text-[10px] tracking-wide">pressure</span>
2257
- <span className="inline-flex h-1.5 w-16 rounded-full overflow-hidden" style={{ background: 'rgb(75 85 99 / 0.25)' }}>
2356
+ {/* Round 373 / Loop: pressure-bar kicker label gains
2357
+ font-medium (fw 400 → 500). Sibling small-text fw
2358
+ lift family with R363 recent-row alias + R364
2359
+ legend-row label + R366 group-label count + R368
2360
+ +N more flows footer — extends to a 5th surface
2361
+ (the chip-row's 'pressure' label). At fontSize
2362
+ 10 px tracking-wide against the chip's gray bg,
2363
+ the default fw 400 sat below the deliberate-data
2364
+ band; fw 500 brings it into parity with the
2365
+ chip-row 'working / online / active links' unit
2366
+ spans (chip-level font-medium R313). data-fleet-
2367
+ pressure-kicker attr exposes the kicker for tests. */}
2368
+ <span className="text-[10px] tracking-wide font-medium" data-fleet-pressure-kicker>pressure</span>
2369
+ {/* Round 374 / Loop: pressure-bar height h-1.5 → h-2
2370
+ (6 → 8 px) — sibling visual-weight bump (8th anchor
2371
+ in the family):
2372
+ R287 minimap viewport stroke 1 → 1.5
2373
+ R295 legend swatch base radius 5.5 → 6
2374
+ R359 recent-row pip base radius 1.6 → 1.8
2375
+ R360 hub digit fontSize 11 → 12
2376
+ R361 edge-badge digit fontSize 10 → 11
2377
+ R365 hub-highlight base radius 5 → 5.5
2378
+ R367 edge-badge rest stroke 1 → 1.25
2379
+ R374 pressure-bar height 1.5 → 2 (this round)
2380
+ +33 % bar height gives the working/idle/offline
2381
+ segments more visibility — at h-1.5 the 3-segment
2382
+ proportion bar was readable but slim; at h-2 the
2383
+ segments parse cleanly even when one tier is
2384
+ < 10 % share. Geometry-safe: items-center flex
2385
+ centers the bar inside the chip's py-1 (4 px top +
2386
+ 4 px bottom) — bar at 8 px stays comfortably
2387
+ inside the 10-px text-row height. R165 segment
2388
+ width transitions + R210 brightness hover + R83
2389
+ pin-mirror box-shadow on segments all preserved
2390
+ (segments inherit width from parent so the height
2391
+ bump propagates without segment-side edits).
2392
+ data-fleet-pressure-bar-height attr exposes the
2393
+ height token for tests. */}
2394
+ <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">
2258
2395
  {seg(w, isLight ? '#059669' : '#22c55e', 'working', 'working')}
2259
2396
  {seg(i, isLight ? '#0d9488' : '#2dd4bf', 'idle', 'idle')}
2260
2397
  {seg(o, isLight ? '#94a3b8' : '#6b7280', 'offline', 'offline')}
@@ -2311,7 +2448,10 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2311
2448
  data-active-filter="status"
2312
2449
  data-filter-match-count={matchCount}
2313
2450
  data-filter-match-aliases={matchAliases.join(',')}
2314
- 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"
2451
+ // R355: `group` lets the inner opacity-70 spans (prefix
2452
+ // `filter:` + count `· N`) brighten to 100 % on pill hover.
2453
+ // Sibling treatment on group + vendor pills below.
2454
+ 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"
2315
2455
  title={matchCount > 0 ? `${matchPreview}${matchSuffix} — click to clear` : 'Click to clear filter'}
2316
2456
  onClick={() => setPinnedStatus(null)}
2317
2457
  style={{
@@ -2325,12 +2465,37 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2325
2465
  cursor: 'pointer',
2326
2466
  }}
2327
2467
  >
2328
- <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>
2468
+ {/* Round 412 / Loop: filter pin pill VALUE picks up the
2469
+ chip-internal-hierarchy arc. Pre-R412 the value span
2470
+ (pinnedStatus / pinnedGroup / pinnedVendor) inherited
2471
+ the parent's font-medium (fw=500); prefix and suffix
2472
+ were opacity-70 label-tier but the VALUE itself sat
2473
+ at the same baseline weight. R412 wraps the value in
2474
+ a font-semibold span (fw=600) so the pill now reads
2475
+ with proper data-tier emphasis — sibling treatment
2476
+ to R333/R335-R341/R362/R369/R389/R410. data-filter-
2477
+ value attr surfaces the value span for tests.
2478
+ 4-pill replace family — status / group / vendor / edge. */}
2479
+ <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>
2329
2480
  <button
2330
2481
  type="button"
2331
2482
  aria-label={`Clear ${pinnedStatus} filter`}
2332
2483
  onClick={(e) => { e.stopPropagation(); setPinnedStatus(null); }}
2333
- className="ml-0.5 leading-none hover:opacity-70"
2484
+ /* Round 356 / Loop: filter pin pill × buttons gain
2485
+ hover:scale-110 (Tailwind 4 modern CSS `scale` property,
2486
+ not legacy transform). Sibling polish to R354 vendor
2487
+ letter glyph + R350/R352/R353 chrome icon hover-scales.
2488
+ Pre-R356 the × had only hover:opacity-70 — the target
2489
+ dimmed under cursor but didn't lift. R356 adds a 10 %
2490
+ scale on hover so the click-target reads as "press me"
2491
+ alongside the dim. transform-gpu hint promotes the
2492
+ button to its own compositor layer for crisper edges
2493
+ during the scale tween. transition-transform duration-
2494
+ 200 matches the chrome icon hover-scale timing family.
2495
+ inline-block is default for <button> so no display
2496
+ tweak needed. replace_all covers all 4 filter pin
2497
+ pills (status / group / vendor / edge) at once. */
2498
+ className="ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 transform-gpu"
2334
2499
  style={{ background: 'transparent', color: 'inherit', cursor: 'pointer', padding: 0 }}
2335
2500
  >×</button>
2336
2501
  </span>
@@ -2349,7 +2514,8 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2349
2514
  data-active-filter="group"
2350
2515
  data-filter-match-count={matchCount}
2351
2516
  data-filter-match-aliases={matchAliases.join(',')}
2352
- 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"
2517
+ // R355 sibling — `group` parent + group-hover on inner spans.
2518
+ 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"
2353
2519
  title={matchCount > 0 ? `${matchPreview}${matchSuffix} — click to clear` : 'Click to clear filter'}
2354
2520
  onClick={() => setPinnedGroup(null)}
2355
2521
  style={{
@@ -2359,12 +2525,27 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2359
2525
  cursor: 'pointer',
2360
2526
  }}
2361
2527
  >
2362
- <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>
2528
+ {/* R412: see status pill above — filter value fw=600 data tier. */}
2529
+ <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>
2363
2530
  <button
2364
2531
  type="button"
2365
2532
  aria-label={`Clear group filter ${pinnedGroup}`}
2366
2533
  onClick={(e) => { e.stopPropagation(); setPinnedGroup(null); }}
2367
- className="ml-0.5 leading-none hover:opacity-70"
2534
+ /* Round 356 / Loop: filter pin pill × buttons gain
2535
+ hover:scale-110 (Tailwind 4 modern CSS `scale` property,
2536
+ not legacy transform). Sibling polish to R354 vendor
2537
+ letter glyph + R350/R352/R353 chrome icon hover-scales.
2538
+ Pre-R356 the × had only hover:opacity-70 — the target
2539
+ dimmed under cursor but didn't lift. R356 adds a 10 %
2540
+ scale on hover so the click-target reads as "press me"
2541
+ alongside the dim. transform-gpu hint promotes the
2542
+ button to its own compositor layer for crisper edges
2543
+ during the scale tween. transition-transform duration-
2544
+ 200 matches the chrome icon hover-scale timing family.
2545
+ inline-block is default for <button> so no display
2546
+ tweak needed. replace_all covers all 4 filter pin
2547
+ pills (status / group / vendor / edge) at once. */
2548
+ className="ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 transform-gpu"
2368
2549
  style={{ background: 'transparent', color: 'inherit', cursor: 'pointer', padding: 0 }}
2369
2550
  >×</button>
2370
2551
  </span>
@@ -2399,7 +2580,8 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2399
2580
  data-active-filter="vendor"
2400
2581
  data-filter-match-count={matchCount}
2401
2582
  data-filter-match-aliases={matchAliases.join(',')}
2402
- 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"
2583
+ // R355 sibling — `group` parent + group-hover on inner spans.
2584
+ 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"
2403
2585
  title={matchCount > 0 ? `${matchPreview}${matchSuffix} — click to clear` : 'Click to clear vendor filter'}
2404
2586
  onClick={() => setPinnedVendor(null)}
2405
2587
  style={{
@@ -2409,12 +2591,27 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2409
2591
  cursor: 'pointer',
2410
2592
  }}
2411
2593
  >
2412
- <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>
2594
+ {/* R412: see status pill above — filter value fw=600 data tier. */}
2595
+ <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>
2413
2596
  <button
2414
2597
  type="button"
2415
2598
  aria-label={`Clear vendor filter ${pinnedVendor}`}
2416
2599
  onClick={(e) => { e.stopPropagation(); setPinnedVendor(null); }}
2417
- className="ml-0.5 leading-none hover:opacity-70"
2600
+ /* Round 356 / Loop: filter pin pill × buttons gain
2601
+ hover:scale-110 (Tailwind 4 modern CSS `scale` property,
2602
+ not legacy transform). Sibling polish to R354 vendor
2603
+ letter glyph + R350/R352/R353 chrome icon hover-scales.
2604
+ Pre-R356 the × had only hover:opacity-70 — the target
2605
+ dimmed under cursor but didn't lift. R356 adds a 10 %
2606
+ scale on hover so the click-target reads as "press me"
2607
+ alongside the dim. transform-gpu hint promotes the
2608
+ button to its own compositor layer for crisper edges
2609
+ during the scale tween. transition-transform duration-
2610
+ 200 matches the chrome icon hover-scale timing family.
2611
+ inline-block is default for <button> so no display
2612
+ tweak needed. replace_all covers all 4 filter pin
2613
+ pills (status / group / vendor / edge) at once. */
2614
+ className="ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 transform-gpu"
2418
2615
  style={{ background: 'transparent', color: 'inherit', cursor: 'pointer', padding: 0 }}
2419
2616
  >×</button>
2420
2617
  </span>
@@ -2447,7 +2644,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2447
2644
  data-filter-match-count={link.count}
2448
2645
  data-filter-match-aliases={`${link.from},${link.to}`}
2449
2646
  data-active-filter-edge-hot={isHot ? 'true' : 'false'}
2450
- 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"
2647
+ 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"
2451
2648
  title={`${link.from} → ${link.to} (${link.count} msg${link.count === 1 ? '' : 's'}${isHot ? ', hot lane · ≥ 10' : ''}) — click to clear`}
2452
2649
  onClick={() => setPinnedEdgeKey(null)}
2453
2650
  style={{
@@ -2457,9 +2654,11 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2457
2654
  cursor: 'pointer',
2458
2655
  }}
2459
2656
  >
2657
+ {/* R412: filter pin pill value (edge variant) picks up fw=600.
2658
+ Sibling treatment to the status/group/vendor pills above. */}
2460
2659
  <span>
2461
2660
  <span className="hidden sm:inline opacity-70" data-filter-prefix>filter: </span>
2462
- {link.from}→{link.to}
2661
+ <span className="font-semibold" data-filter-value>{link.from}→{link.to}</span>
2463
2662
  {/* Round 323 / Loop: edge filter pill count digit picks
2464
2663
  up tabular-nums (Tailwind class on both cold +
2465
2664
  hot branches). Sibling treatment to the status /
@@ -2491,7 +2690,21 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2491
2690
  type="button"
2492
2691
  aria-label={`Clear edge filter ${link.from} → ${link.to}`}
2493
2692
  onClick={(e) => { e.stopPropagation(); setPinnedEdgeKey(null); }}
2494
- className="ml-0.5 leading-none hover:opacity-70"
2693
+ /* Round 356 / Loop: filter pin pill × buttons gain
2694
+ hover:scale-110 (Tailwind 4 modern CSS `scale` property,
2695
+ not legacy transform). Sibling polish to R354 vendor
2696
+ letter glyph + R350/R352/R353 chrome icon hover-scales.
2697
+ Pre-R356 the × had only hover:opacity-70 — the target
2698
+ dimmed under cursor but didn't lift. R356 adds a 10 %
2699
+ scale on hover so the click-target reads as "press me"
2700
+ alongside the dim. transform-gpu hint promotes the
2701
+ button to its own compositor layer for crisper edges
2702
+ during the scale tween. transition-transform duration-
2703
+ 200 matches the chrome icon hover-scale timing family.
2704
+ inline-block is default for <button> so no display
2705
+ tweak needed. replace_all covers all 4 filter pin
2706
+ pills (status / group / vendor / edge) at once. */
2707
+ className="ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 transform-gpu"
2495
2708
  style={{ background: 'transparent', color: 'inherit', cursor: 'pointer', padding: 0 }}
2496
2709
  >×</button>
2497
2710
  </span>
@@ -2756,9 +2969,35 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2756
2969
  vendor letter chips ('A:N', 'O:N', '书:N',
2757
2970
  '?:N'). They display vendor-distribution
2758
2971
  data; same tier as the sibling data chips. */
2759
- className="tabular-nums font-medium inline-flex items-baseline gap-0.5 px-1 rounded anet-topo-chip-focus"
2972
+ /* Round 401 / Loop: vendor letter chip closes the
2973
+ hover-lift gesture family at its last unaddressed
2974
+ interactive HTML surface. R397/R398/R399 lifted
2975
+ filter pin pills + chip-row chips (working /
2976
+ online / active-links); R400 lifted standalone
2977
+ chrome buttons (reset / fullscreen). The vendor
2978
+ letter chips (A:N / O:N / 书:N / ?:N) are
2979
+ sibling interactive chips in the same chip-row
2980
+ — clickable to toggle the vendor filter pin —
2981
+ but were not yet on the hover-lift family.
2982
+ R401 closes the gap with hover:-translate-y-px
2983
+ + transition-transform + transform-gpu added
2984
+ to the className. The inline transition list
2985
+ (box-shadow + background-color) keeps eaching
2986
+ independently — different property axes compose
2987
+ cleanly. Existing R354 glyph scale-1.1 (inner
2988
+ span) + R202 chip bg color-mix + R180 pin-mirror
2989
+ box-shadow + R354 glyph hover transform all
2990
+ preserved. data-vendor-letter-hover-lift attr
2991
+ surfaces the lift for tests. */
2992
+ // R417: `group` parent enables the count suffix to
2993
+ // brighten on chip hover via group-hover:opacity-100
2994
+ // — sibling to R355 filter-pill prefix/suffix + R414
2995
+ // chip-row unit brighten. Closes the inner-span
2996
+ // hover-brighten family at the vendor chip surface.
2997
+ 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"
2760
2998
  data-vendor-letter={v.initial}
2761
2999
  data-vendor-letter-count={v.count}
3000
+ data-vendor-letter-hover-lift="true"
2762
3001
  data-vendor-pinned={isPinned ? 'true' : 'false'}
2763
3002
  data-vendor-hovered={hoveredVendor === v.initial ? 'true' : 'false'}
2764
3003
  data-vendor-aliases={aliases.join(',')}
@@ -2802,7 +3041,64 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2802
3041
  }
2803
3042
  }}
2804
3043
  >
2805
- <span style={{ color: v.color }}>{v.initial}</span>
3044
+ {/* Round 354 / Loop: vendor letter glyph scales
3045
+ 1.0 → 1.1 on hover. R88 already dims OTHER
3046
+ vendors on hover via canvas-wide opacity
3047
+ masking; R202 added a chip-level bg tint
3048
+ (color-mix 12 % alpha) so the chip itself
3049
+ responds. R354 closes the trio with a glyph-
3050
+ level lift: the focused vendor LETTER actively
3051
+ rises (transform scale) rather than the chip
3052
+ merely changing colour. Three layers of positive
3053
+ feedback on the hovered vendor + canvas-wide
3054
+ negative feedback on the others — a clean
3055
+ figure/ground separation.
3056
+
3057
+ display: inline-block is required for transform
3058
+ to apply (inline elements ignore transform).
3059
+ transformOrigin: 'center' so the glyph pivots
3060
+ around its centre instead of arcing from the
3061
+ baseline anchor. transition rides the existing
3062
+ Tailwind 4 transform/scale list (no new
3063
+ property — Tailwind already lists transform in
3064
+ the default transition-property set). 200ms
3065
+ matches the R202 chip bg-tint timing so the
3066
+ glyph lift and chip background ease in concert. */}
3067
+ {/* Round 369 / Loop: vendor letter glyph picks up
3068
+ fontWeight 600 (font-semibold). The glyph is the
3069
+ vendor identifier — the DATA the operator scans
3070
+ in this chip (A / O / 书 / C / G / ?). R333 set
3071
+ the count suffix `:N` to text-gray-400 + tabular-
3072
+ nums and (via parent inheritance) fw 500. Pre-
3073
+ R369 the LETTER also inherited fw 500 from the
3074
+ chip's font-medium — letter and count read at
3075
+ the same weight, contradicting the data-vs-label
3076
+ hierarchy the rest of the chip-row already speaks.
3077
+ R369 lifts the letter to fw 600 so the chip now
3078
+ reads as the same two-tier pattern R362 closed
3079
+ on the working / online / active-links chips:
3080
+ chip digit/letter fw 600 (data)
3081
+ chip unit/count fw 500 (label)
3082
+ Sibling treatment to R362 — extends the R333-R341
3083
+ chip-internal-hierarchy arc to the vendor-letter
3084
+ chip surface (9th surface family). R354 transform-
3085
+ scale-on-hover + R88 canvas-dim-others + R202
3086
+ chip bg color-mix all preserved on the same span.
3087
+ data-vendor-letter-glyph-font-weight attr exposes
3088
+ the value for tests. */}
3089
+ <span
3090
+ data-vendor-letter-glyph={v.initial}
3091
+ data-vendor-letter-glyph-hover={hoveredVendor === v.initial ? 'true' : 'false'}
3092
+ data-vendor-letter-glyph-font-weight="600"
3093
+ style={{
3094
+ color: v.color,
3095
+ display: 'inline-block',
3096
+ fontWeight: 600,
3097
+ transform: hoveredVendor === v.initial ? 'scale(1.1)' : 'scale(1)',
3098
+ transformOrigin: 'center',
3099
+ transition: 'transform 200ms ease-out',
3100
+ }}
3101
+ >{v.initial}</span>
2806
3102
  {/* Round 333 / Loop: vendor count suffix `:{N}` joins
2807
3103
  the R317 subordinate-text-lift family (gray-500 →
2808
3104
  gray-400) plus picks up tabular-nums for digit
@@ -2815,8 +3111,17 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2815
3111
  reads it as "deliberate subordinate metadata"
2816
3112
  rather than near-invisible chrome. data-vendor-
2817
3113
  letter-count exposes the span for tests. */}
3114
+ {/* R417: count suffix opacity-70 + group-hover:
3115
+ opacity-100 brightens on chip hover. Inner-span
3116
+ hover-brighten family (3rd anchor) — sibling to
3117
+ R355 filter pill prefix/suffix and R414 chip-row
3118
+ unit. Effective shade at rest: text-gray-400 ×
3119
+ 70 % alpha; on hover: full gray-400. The label-
3120
+ tier-vs-glyph differentiation persists on hover
3121
+ since the glyph (R369 fw=600) stays at full
3122
+ opacity. R333 :{count} format preserved. */}
2818
3123
  <span
2819
- className="text-gray-400 tabular-nums"
3124
+ className="text-gray-400 tabular-nums opacity-70 transition-opacity duration-200 group-hover:opacity-100"
2820
3125
  data-vendor-letter-count-suffix
2821
3126
  >:{v.count}</span>
2822
3127
  </span>
@@ -2904,11 +3209,36 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2904
3209
  (third chip in the row — matches working + online
2905
3210
  chip treatment so all three digits in the chip row
2906
3211
  stay width-stable across counter crossings). */
2907
- className={`tabular-nums font-medium hidden sm:inline px-2.5 py-1 rounded-md border anet-topo-chip-focus ${
3212
+ /* Round 399 / Loop: active-links chip closes the 3-chip
3213
+ chip-row by extending R398's hover translateY(-1px)
3214
+ lift onto the third (rightmost) chip. The R398 family
3215
+ already covered working + online chips on the
3216
+ clickable variant; R399 adds the same gate (isInter-
3217
+ active = flowLinks.length > 0) so empty active-links
3218
+ stays planted at R206's opacity-50 receded paint.
3219
+ transition-transform + ease-out + transform-gpu join
3220
+ the inline transition list (different property axes
3221
+ compose cleanly: inline handles color/bg/border/
3222
+ opacity, className handles transform).
3223
+ Gesture-vocabulary table (post-R399 — now complete
3224
+ across the chip-row):
3225
+ working chip -1 px (R398)
3226
+ online chip -1 px (R398)
3227
+ active-links chip -1 px (R399, this round)
3228
+ filter pin pills -1 px (R397)
3229
+ recent-signal row -1 px (R143)
3230
+ legend row -1 px (R144)
3231
+ Every interactive chip in TopoGraph lifts on hover.
3232
+ data-chip-hover-lift attr exposes the lift surface
3233
+ state ('true' clickable, 'false' empty) for tests. */
3234
+ // R414: `group` parent + inner unit span group-hover-brighten — see working chip above.
3235
+ 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 ${
2908
3236
  isInteractive
2909
- ? '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'
3237
+ ? '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'
2910
3238
  : 'bg-gray-500/10 text-gray-400 border-gray-500/20'
2911
3239
  }`}
3240
+ data-chip-hover-lift={isInteractive ? 'true' : 'false'}
3241
+ data-chip-group-hover-brighten="true"
2912
3242
  data-active-links-chip
2913
3243
  data-active-links-flow-count={flowLinks.length}
2914
3244
  data-active-links-clickable={isInteractive ? 'true' : 'false'}
@@ -2936,7 +3266,8 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2936
3266
  the 5th chip surface in the R333/R335/R336/R337
2937
3267
  chip-internal-hierarchy arc. data-active-links-
2938
3268
  chip-unit exposes the unit span for tests. */}
2939
- {flowLinks.length}<span className="opacity-70" data-active-links-chip-unit> active link{flowLinks.length === 1 ? '' : 's'}</span>
3269
+ {/* R362 sibling — active-links chip digit gains font-semibold. */}
3270
+ <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>
2940
3271
  {rel ? (() => {
2941
3272
  // Round 161 / Loop: extend R160's recency-pip
2942
3273
  // vocabulary up one scope — from per-flow row to
@@ -2961,8 +3292,8 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2961
3292
  const alpha = ageSec <= 30
2962
3293
  ? 1
2963
3294
  : ageSec <= 300
2964
- ? 1 - ((ageSec - 30) / 270) * 0.75
2965
- : 0.25;
3295
+ ? 1 - ((ageSec - 30) / 270) * 0.70 /* R358: floor 0.25 → 0.30 lift across 3 freshness scopes */
3296
+ : 0.30; /* R358: stale floor lifted 0.25 → 0.30 — 20% legibility bump while preserving fresh/stale ratio */
2966
3297
  // Cyan dark / teal light to match palette legendAccent.
2967
3298
  const dotColor = isLight
2968
3299
  ? `rgba(13, 148, 136, ${alpha.toFixed(2)})`
@@ -2979,7 +3310,28 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2979
3310
  // color: dotColor — the lift only affects the trailing
2980
3311
  // literal "last {rel}" text.
2981
3312
  return (
2982
- <span className="text-gray-400">
3313
+ // Round 357 / Loop: active-links chip freshness
3314
+ // suffix wrapper picks up `tabular-nums` for digit
3315
+ // width-lock. Pre-R357 the literal "last {rel}"
3316
+ // text (e.g. "last 5s ago", "last 10s ago",
3317
+ // "last 1m ago") had natural-figure digits — the
3318
+ // freshness ticker updates every second, so the
3319
+ // 9→10 boundary on the seconds counter and the
3320
+ // 59→60s → 1m flip both jittered ~1-2 px of glyph
3321
+ // width which propagated through the chip-row's
3322
+ // inline-flex layout, nudging the freshness DOT
3323
+ // and the chip's left edge. Tabular-nums on the
3324
+ // wrapper applies to all descendant digits only
3325
+ // (letters render at natural widths) so the
3326
+ // ticker stays planted across every count cross.
3327
+ // Joins the R224-R232 info-density tabular-nums
3328
+ // sweep at the chip-row freshness scope. Pure
3329
+ // paint-level change, no geometry shift on rest.
3330
+ // The R342 text-gray-400 lift + R161 dot freshness
3331
+ // alpha ramp + R317 subordinate-text-lift family
3332
+ // all preserved. data-active-links-freshness-
3333
+ // wrapper attr exposes the wrapper for tests.
3334
+ <span className="text-gray-400 tabular-nums" data-active-links-freshness-wrapper>
2983
3335
  <span
2984
3336
  data-active-links-freshness-dot
2985
3337
  data-active-links-freshness-alpha={alpha.toFixed(2)}
@@ -3106,6 +3458,31 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3106
3458
  return `Agent network topology — ${parts.join(' · ')}. Tab to navigate nodes, double-click canvas to reset view.`;
3107
3459
  })()}
3108
3460
  data-topo-canvas-aria
3461
+ /* Round 462 / Loop — surface DASHBOARD_VERSION on the root SVG
3462
+ element as `data-dashboard-version`. Directly closes the
3463
+ feedback_dash_zombie_port_3000.md memory rule: "verify ships
3464
+ via SVG DOM, not tmux 'Ready' — zombie next-servers + stale
3465
+ global installs silently serve old code". Pre-R462 the only
3466
+ ways to know which preview the dash was serving were:
3467
+ 1. parse the npm registry for the latest tag (network)
3468
+ 2. fetch /api/dashboard/version (API surface, no DOM)
3469
+ 3. inspect the /login footer or /settings page (off-route)
3470
+ Test scripts that probe TopoGraph DOM (overlap, group-label
3471
+ tint, pip strip, etc.) couldn't tell whether the dash was
3472
+ actually serving the build they expected to verify. R462
3473
+ threads DASHBOARD_VERSION through to the root <svg> so:
3474
+ - Playwright probes can read svg[data-dashboard-version]
3475
+ directly + fail-fast on stale-build mismatch
3476
+ - the memory rule's manual zombie check ("inspect SVG
3477
+ dom") becomes a one-attr probe
3478
+ - operators DOM-inspect to confirm the live version
3479
+ matches the npm tag without leaving the topology page
3480
+ Geometry/visual impact: ZERO (data-* attrs don't paint).
3481
+ The version string is build-time injected via the existing
3482
+ DASHBOARD_VERSION constant (R51 footer + R51 settings page
3483
+ already consume it from app/lib/version.ts → reads
3484
+ package.json pkg.version). No business logic added. */
3485
+ data-dashboard-version={DASHBOARD_VERSION}
3109
3486
  onPointerDown={onPointerDown}
3110
3487
  onPointerMove={onPointerMove}
3111
3488
  onPointerUp={onPointerUp}
@@ -3643,19 +4020,174 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3643
4020
  active surfaces the activity state for test probes
3644
4021
  (active spokes don't carry the bucket/dur attrs so
3645
4022
  they need their own data anchor). */
4023
+ // Round 382 / Loop: hub-spoke path picks up
4024
+ // strokeLinecap='round'. Sibling polish to R378 flow-
4025
+ // rail dashes + R380 group box dashes — three dashed-
4026
+ // stroke surfaces now share 'round' linecap:
4027
+ // R378 flow-rail '2 12' -> soft 3-px pills
4028
+ // R380 group box '6 6' -> soft 7.5-px pills
4029
+ // R382 hub spoke '6 14' -> soft 7-px pills (this round)
4030
+ // For idle spokes (dashed at sw=1), each 6-px dash gains
4031
+ // 0.5-px round caps and reads as a soft pill instead of
4032
+ // a sharp 6 x 1 rectangle. Active spokes (solid, no
4033
+ // dasharray) have caps mostly hidden by the hub center +
4034
+ // node radius. Geometry-safe; paint-only. R51 sentinel
4035
+ // strokeWidth 1.5/3 untouched (idle=1, active=2). data-
4036
+ // topo-hub-spoke-linecap attr exposes the value for tests.
4037
+ // Round 419 / Loop: hub-spoke idle opacity 0.45 → 0.50.
4038
+ // Stale-state legibility lift family 9th anchor — pairs
4039
+ // with R391 (active 0.7 → 0.8) and R415 (active sw 2 →
4040
+ // 2.25) so the same spoke path is now polished on BOTH
4041
+ // active AND idle tiers. Pre-R419 idle spokes painted
4042
+ // at α=0.45 with R46 anet-topo-spoke-flow dashed
4043
+ // animation; the dashed pulses sat at the "background
4044
+ // chatter" floor — visible but understated. R419
4045
+ // lifts to 0.50 so idle spokes read more confidently
4046
+ // while the active/idle contrast ratio stays clear
4047
+ // (0.8/0.50 = 1.6× vs prior 0.8/0.45 = 1.78×; still
4048
+ // a sharp two-tier distinction).
4049
+ // Stale-state legibility lift family (9 anchors now):
4050
+ // R317 subordinate-text gray-500 → gray-400
4051
+ // R358 freshness floor 0.25 → 0.30
4052
+ // R372 minimap offline-dot 0.5 → 0.6
4053
+ // R404 hub-halo cyber trough 0.08 → 0.10
4054
+ // R405 hub-halo light trough 0.32 → 0.34
4055
+ // R406 edge freshness floor 0.35 → 0.40
4056
+ // R407 node halo offline opacity (cyber + light)
4057
+ // R413 active-node pulse trough (cyber + light)
4058
+ // R419 hub-spoke idle opacity 0.45 → 0.50 (this round)
4059
+ // data-topo-hub-spoke-opacity attr (R391) updates to
4060
+ // surface the resolved per-state value.
4061
+ //
4062
+ // Round 415 / Loop: hub-spoke active strokeWidth 2 → 2.25.
4063
+ // Pairs with R391 (active opacity 0.7 → 0.8) so the same
4064
+ // active-state path lifts BOTH stroke weight AND opacity
4065
+ // in concert. Pre-R415 active strokes sat at sw=2 — clear
4066
+ // step over idle sw=1, but a touch lighter than the
4067
+ // weight family's other "active" indicators (R385 hub
4068
+ // hover-ring sw=1.75 / R402 legend pin-ring sw=1.75 /
4069
+ // R367 edge-badge rest sw=1.25). R415 bumps to 2.25 so
4070
+ // the active spoke reads with proportional weight to its
4071
+ // role — the line connecting the focal point to the
4072
+ // active node deserves the heaviest active stroke in the
4073
+ // family (after pin/hot edge-badge sw=2). Stays clear of
4074
+ // R51 sentinels (1.5 / 3) at 2.25.
4075
+ // Visual-weight bump family (14 anchors now):
4076
+ // R287 minimap viewport stroke 1 → 1.5
4077
+ // R295 legend swatch radius 5.5 → 6
4078
+ // R359 recent-row pip radius 1.6 → 1.8
4079
+ // R360 hub digit fontSize 11 → 12
4080
+ // R361 edge-badge digit fontSize 10 → 11
4081
+ // R365 hub-highlight radius 5 → 5.5
4082
+ // R367 edge-badge rest stroke 1 → 1.25
4083
+ // R374 pressure-bar height 1.5 → 2
4084
+ // R383 recent-row pip radius 1.8 → 2.0
4085
+ // R384 minimap online dot 1.7 → 1.9
4086
+ // R385 hub hover-ring stroke 1.5 → 1.75
4087
+ // R402 legend pin-ring stroke 1.5 → 1.75
4088
+ // R408 hub-halo radius 18 → 20
4089
+ // R415 hub-spoke active stroke 2 → 2.25 (this round)
4090
+ // R382 strokeLinecap='round' + R391 opacity 0.45/0.8 +
4091
+ // R51-safe idle sw=1 all preserved. 250ms transition
4092
+ // list already covers stroke-width — the new tier eases
4093
+ // naturally. data-topo-hub-spoke-stroke-width-active
4094
+ // attr surfaces the active value for tests.
4095
+ //
4096
+ // Round 391 / Loop: hub-spoke active opacity 0.7 → 0.8.
4097
+ // Pre-R391 active spokes (the spoke connecting the hub
4098
+ // to the currently-active alias — hovered or pinned)
4099
+ // lifted opacity from rest 0.45 to active 0.7 — a clear
4100
+ // step but slightly understated against the canvas
4101
+ // chrome. R391 lifts active to 0.8 so the "this spoke
4102
+ // connects to your active node" signal reads with
4103
+ // matching weight to the R370 hub hover-ring opacity
4104
+ // (0.7 → 0.8 cyber) — paired canvas signals now share
4105
+ // the same active-state alpha (0.8) so when a user
4106
+ // hovers a node, both the spoke and the hub-ring lift
4107
+ // to identical opacity. Rest 0.45 invariant preserved.
4108
+ // Theme-consistency / canvas-presence polish family
4109
+ // (6th anchor):
4110
+ // R370 hub hover-ring opacity 0.7 → 0.8 cyber
4111
+ // R371 edge-badge rest opacity 0.82 → 0.85 cyber
4112
+ // R372 minimap offline-dot opacity 0.5 → 0.6
4113
+ // R386 hub-highlight idle opacity 0.9 → 0.95
4114
+ // R387 hover-detail panel opacity 0.94 → 0.97 cyber
4115
+ // R391 hub-spoke active opacity 0.7 → 0.8 (this round)
4116
+ // Idle path (45% alpha + dashed flow animation) entirely
4117
+ // untouched — R391 is an active-state-only lift.
4118
+ // data-topo-hub-spoke-opacity attr exposes the resolved
4119
+ // value for tests. R382 strokeLinecap='round' + R51
4120
+ // sentinel-safe sw (1 idle / 2 active) preserved.
4121
+ /* Round 430 / Loop: hub-spoke opacity hover lift on
4122
+ hoveredAlias === session.alias. Adds a "this node's
4123
+ spoke" affordance to the node-hover gesture — in a
4124
+ dense ring layout the spokes are visually quiet
4125
+ (idle α=0.50 dashed, active α=0.80 solid) so hovering
4126
+ a node didn't telegraph which line connects to it.
4127
+ R430 lifts the matched spoke's opacity:
4128
+ idle 0.50 → 0.70 (hover-α=0.70, +0.20)
4129
+ active 0.80 → 0.95 (hover-α=0.95, +0.15)
4130
+ The +0.15-to-0.20 lift keeps the active/idle two-tier
4131
+ distinction (0.95 vs 0.70 still a clear gap) while
4132
+ making the hovered-node's spoke visibly brighter than
4133
+ every other spoke at its own activity tier. R241
4134
+ transition list already covers opacity 250ms so the
4135
+ lift eases for free. Sibling to R429 label-card body
4136
+ solidity lift — both surface a single-node-focused
4137
+ attention cue with the same easing cadence.
4138
+ Stacks with the 6-layer node hover cue stack at the
4139
+ inter-node-link scope:
4140
+ R26 group translateY -2px (per-node)
4141
+ R217 stroke tint legendAccent (per-node card)
4142
+ R142 drop-shadow boost (per-node card)
4143
+ R427 alias letter-spacing (per-node text)
4144
+ R428 sub-text letter-spacing (per-node text)
4145
+ R429 body opacity 0.94 → 1.0 (per-node card)
4146
+ R430 spoke opacity α+ (this round) (link to hub)
4147
+ data-topo-hub-spoke-hovered exposes the gate. */
4148
+ const isHoveredSpoke = !reducedMotion && hoveredAlias === session.alias;
4149
+ const spokeOpacity = isActiveSpoke
4150
+ ? (isHoveredSpoke ? 0.95 : 0.80)
4151
+ : (isHoveredSpoke ? 0.70 : 0.50);
4152
+ /* Round 435 / Loop: hub-spoke stroke-width hover lift —
4153
+ sibling to R430 opacity hover at the same surface. When
4154
+ hoveredAlias matches, BOTH opacity AND stroke-width
4155
+ lift on the matched spoke so the eye registers a
4156
+ 2-axis "this node's spoke" gesture (paint + geometry).
4157
+ idle 1.00 → 1.25 (Δ +0.25, +25%)
4158
+ active 2.25 → 2.50 (Δ +0.25, +11%)
4159
+ Same +0.25 absolute delta keeps the idle/active visual
4160
+ progression consistent — at rest sw ratio 2.25:1 = 2.25,
4161
+ on hover 2.50:1.25 = 2.0; both still clearly two-tier.
4162
+ R241 transition list already covers stroke-width 250ms
4163
+ so the lift eases for free.
4164
+ R51 sentinel-safe: spoke is canvas <path>, not
4165
+ data-node <circle> (the sentinel selector is gated to
4166
+ g[data-node] descendants). 1.25 and 2.5 are not in the
4167
+ reserved {1.5, 3} set so the overlap-test sentinel
4168
+ attribute selector wouldn't match either way. */
4169
+ const spokeStrokeWidth = isActiveSpoke
4170
+ ? (isHoveredSpoke ? 2.5 : 2.25)
4171
+ : (isHoveredSpoke ? 1.25 : 1);
3646
4172
  return (
3647
4173
  <path
3648
4174
  key={`hub-${session.alias}`}
3649
4175
  d={path}
3650
4176
  fill="none"
3651
4177
  stroke={isActiveSpoke ? pal.spokeStroke.active : pal.spokeStroke.idle}
3652
- strokeWidth={isActiveSpoke ? 2 : 1}
4178
+ strokeWidth={spokeStrokeWidth}
3653
4179
  strokeDasharray={isActiveSpoke ? 'none' : '6 14'}
3654
- opacity={isActiveSpoke ? 0.7 : 0.45}
4180
+ strokeLinecap="round"
4181
+ opacity={spokeOpacity}
3655
4182
  className={isActiveSpoke ? undefined : 'anet-topo-spoke-flow'}
3656
4183
  data-topo-spoke-bucket={isActiveSpoke ? undefined : busy}
3657
4184
  data-topo-spoke-dur={isActiveSpoke ? undefined : spokeDur}
3658
4185
  data-topo-hub-spoke-active={isActiveSpoke ? 'true' : 'false'}
4186
+ data-topo-hub-spoke-hovered={isHoveredSpoke ? 'true' : 'false'}
4187
+ data-topo-hub-spoke-opacity={spokeOpacity}
4188
+ data-topo-hub-spoke-stroke-width={spokeStrokeWidth}
4189
+ data-topo-hub-spoke-stroke-width-active="2.25"
4190
+ data-topo-hub-spoke-linecap="round"
3659
4191
  style={{
3660
4192
  transition: 'stroke 250ms ease-out, stroke-width 250ms ease-out, opacity 250ms ease-out',
3661
4193
  ...(isActiveSpoke ? {} : {
@@ -3735,7 +4267,29 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3735
4267
  y={box.y}
3736
4268
  width={box.w}
3737
4269
  height={box.h}
3738
- rx="14"
4270
+ /* Round 464 / Loop: group-box rx 14 → 16 on isPinned.
4271
+ Geometric softening at the corner radius — locked
4272
+ groups read with subtly rounder shoulders than
4273
+ hovered/idle. +2px reads as a calm \"settled in\"
4274
+ posture (subtler than a fill or stroke bump but
4275
+ unmistakable across the whole cluster boundary).
4276
+ Pin signature on the group-box rect now spans 7
4277
+ axes:
4278
+ R63 text fill brighten
4279
+ R142 drop-shadow filter
4280
+ R432 text letter-spacing 0→0.5
4281
+ R444 count tspan fw 500→600
4282
+ R457 parent text fw 700→800
4283
+ codex p.125 text opacity 0.55→1
4284
+ R464 corner rx 14→16 (this round)
4285
+ transition list (R461) already covers x/y/width/
4286
+ height 200ms ease-out; appended `rx 200ms ease-
4287
+ out` so the rounding eases alongside the geometry
4288
+ axes. SVG2 CSS animation on rx: Chrome 95+ /
4289
+ Safari 16+ / FF 70+ (same matrix as x/y/w/h).
4290
+ data-group-box-rx exposes the resolved value. */
4291
+ rx={isPinned ? '16' : '14'}
4292
+ data-group-box-rx={isPinned ? '16' : '14'}
3739
4293
  fill={isLight ? '#0f172a' : '#a5b4fc'}
3740
4294
  // R68: 3-tier opacity + stroke ladder.
3741
4295
  // pinned → fill 0.08 / 0.13, stroke 3 px (locked)
@@ -3747,7 +4301,35 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3747
4301
  stroke={(isPinned || isHovered) ? pal.legendAccent : pal.ringStroke}
3748
4302
  strokeWidth={isPinned ? 3 : isHovered ? 2 : 1.5}
3749
4303
  strokeDasharray={(isPinned || isHovered) ? 'none' : '6 6'}
4304
+ /* Round 380 / Loop: cluster box stroke gets round
4305
+ linecap + round linejoin. Sibling SVG stroke-
4306
+ softening polish to R378 flow-rail linecap + R379
4307
+ minimap viewport linejoin — extends the family to
4308
+ the group cluster boundary box (grid layout only):
4309
+ R288 chrome icons strokeLinecap='round'
4310
+ R378 flow-rail dashes strokeLinecap='round'
4311
+ R380 group box dashes strokeLinecap='round' (this round)
4312
+ R379 viewport rect strokeLinejoin='round'
4313
+ R380 group box corners strokeLinejoin='round' (this round)
4314
+ Linecap rounds the R85 '6 6' marching-ants dash
4315
+ pills at rest — each 6 px dash gains a ~0.75 px
4316
+ round cap (sw=1.5 idle), reading as soft pills
4317
+ instead of sharp 6 × 1.5 px rectangles. Linejoin
4318
+ rounds the 4 sharp 90° corners (any state — solid
4319
+ or dashed); at sw=1.5 the join arc is ~0.75 px,
4320
+ matching R379 viewport vocabulary. Geometry-safe:
4321
+ stroke-* properties only affect paint, not bbox.
4322
+ The R51 sentinel 1.5/3 strokeWidth values stay
4323
+ intact (the overlap probe is gated to g[data-
4324
+ node], so this cluster-internal rect is invisible
4325
+ to it anyway). data-group-box-linecap + -linejoin
4326
+ attrs expose the values for tests. */
4327
+ strokeLinecap="round"
4328
+ strokeLinejoin="round"
3750
4329
  data-group-box-pinned={isPinned ? 'true' : 'false'}
4330
+ data-group-box-linecap="round"
4331
+ data-group-box-linejoin="round"
4332
+ data-group-box-geom-transition="x,y,width,height"
3751
4333
  // R85: ambient "marching ants" drift on the perimeter
3752
4334
  // when this group has at least one working member, and
3753
4335
  // neither pin nor hover is active (those treatments
@@ -3785,8 +4367,24 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3785
4367
  while stroke / fill-opacity / filter all eased.
3786
4368
  Closes the last theme-toggle snap on the group
3787
4369
  box surface — same idiom R246 + R247 used at
3788
- per-node label-card and side-panel scopes. */
3789
- transition: 'stroke 200ms ease-out, stroke-width 200ms ease-out, fill-opacity 200ms ease-out, filter 200ms ease-out, fill 200ms ease-out',
4370
+ per-node label-card and side-panel scopes.
4371
+ Round 461 / Loop: extend the transition list to
4372
+ all 4 geometry axes (x, y, width, height) so
4373
+ when a cluster grows / shrinks (member joins,
4374
+ leaves, prefix rebalance, dense toggle, status
4375
+ flip) the BIG outer container slides into the
4376
+ new bounds at the same 200ms cadence the R460
4377
+ inner hitbox tint rect now uses. Pre-R461 the
4378
+ outer 200×140 px box snap-jumped on cluster
4379
+ resize while the inner 160×18 hitbox slid —
4380
+ jarring two-rate motion at the same surface.
4381
+ R461 unifies both rects to slide as one, with
4382
+ the parent box driving the visual envelope and
4383
+ the inner hitbox tracking the bottom-edge tint.
4384
+ Hero D #147 motion-coherence at the FULL cluster
4385
+ container tier (not just the label tint).
4386
+ data-group-box-geom-transition attr exposed. */
4387
+ transition: 'stroke 200ms ease-out, stroke-width 200ms ease-out, fill-opacity 200ms ease-out, filter 200ms ease-out, fill 200ms ease-out, x 200ms ease-out, y 200ms ease-out, width 200ms ease-out, height 200ms ease-out, rx 200ms ease-out',
3790
4388
  pointerEvents: 'none',
3791
4389
  // CSS var consumed by `.anet-topo-groupbox-live`
3792
4390
  // (line 877 of globals.css). React's CSSProperties
@@ -3864,27 +4462,83 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3864
4462
  ].filter(Boolean).join('\n')}</title>
3865
4463
  );
3866
4464
  })()}
4465
+ {/* v0.11.0 #147 Hero D — Vincent 5401: "太大太丑,
4466
+ 都放到框的右下角的小字". First-cut bottom-right
4467
+ placement collided with bottom-row nodes (cluster
4468
+ geometry has no bottom padding; only GROUP_TOP=12
4469
+ top band). Pivoted to Option C from #147 spec:
4470
+ keep top-left anchor BUT shrink fontSize (13 → 9)
4471
+ and dim default opacity (1 → 0.55, hover/pin
4472
+ restore to 1). Satisfies "太大太丑" via the size +
4473
+ opacity axes while keeping the existing geometry
4474
+ contract that topo-overlap-test gates. Hitbox
4475
+ rect width tightens to min(box.w-12, 160) to
4476
+ track the narrower label render. */}
4477
+ {/* Round 465 / Loop — hitbox tint rect rx 4 → 5 on
4478
+ pinnedGroup match. Mirrors R464 (parent group-box
4479
+ rx 14 → 16 on isPinned) at the hitbox tier. The
4480
+ R460 hitbox carried fixed rx=4 since codex p.125
4481
+ pivoted it to the bottom-of-band position; the
4482
+ pin-state geometric softening was only on the BIG
4483
+ outer container, not the small hitbox underneath.
4484
+ R465 adds +1 px corner rounding on pin so the
4485
+ tint rect echoes the parent's locked posture at
4486
+ its own scale (8% relative bump matches R464's
4487
+ 14→16 ≈ 14% scaled to the smaller rect).
4488
+ Transition list (R460 fill/opacity/x/width 200ms
4489
+ ease-out) extends to include `rx 200ms ease-out`
4490
+ so the rounding eases under the same cadence.
4491
+ SVG2 CSS animation on rx: Chrome 95+ / Safari
4492
+ 16+ / FF 70+ (same matrix as x/y/w/h).
4493
+ data-group-label-tint-rx exposes the resolved
4494
+ value for tests. */}
3867
4495
  <rect
3868
4496
  x={box.x + 6}
3869
4497
  y={box.y + 2}
3870
- width={Math.min(box.w - 12, 240)}
3871
- height={20}
3872
- rx="4"
3873
- /* R107 / Loop: list-item tint extends to the SVG
3874
- group labels — same idiom R104 added to recent-
3875
- signal rows and R105 to the legend rows. The
3876
- tint colour is pal.legendAccent (cyan) since
3877
- groups don't carry an inherent swatch the way
3878
- legend rows do; this matches R68's group-box
3879
- isPinned/isHovered accent stroke for consistency.
3880
- hover < pinned opacity so locked vs preview is
3881
- discriminable at a glance. */
4498
+ width={Math.min(box.w - 12, 160)}
4499
+ height={18}
4500
+ rx={pinnedGroup === box.key ? '5' : '4'}
4501
+ data-group-label-tint-rx={pinnedGroup === box.key ? '5' : '4'}
3882
4502
  fill={pinnedGroup === box.key || hoveredGroupLabel === box.key ? pal.legendAccent : 'transparent'}
3883
4503
  opacity={pinnedGroup === box.key ? (isLight ? 0.16 : 0.20)
3884
4504
  : hoveredGroupLabel === box.key ? (isLight ? 0.09 : 0.13)
3885
4505
  : 1}
3886
4506
  data-group-label-tinted={pinnedGroup === box.key ? 'pinned' : hoveredGroupLabel === box.key ? 'hover' : 'none'}
3887
- style={{ transition: 'fill 150ms ease-out, opacity 150ms ease-out' }}
4507
+ /* Round 459 / Loop — cadence-sync follow-on to codex
4508
+ preview.125 (Hero D #147). Codex's parent <text>
4509
+ transition list now reads:
4510
+ 'fill 200ms, letter-spacing 200ms,
4511
+ font-weight 200ms, opacity 200ms'
4512
+ — 200ms ease-out across every axis. The label
4513
+ hitbox tint rect underneath was still at 150ms
4514
+ (legacy R107 cadence), so the tint snapped in
4515
+ 50ms ahead of the parent label brightening —
4516
+ a small but perceivable mistimed cascade when
4517
+ hovering or clicking to pin a cluster. R459
4518
+ lifts both axes to 200ms to lock the tint
4519
+ under the label as one motion-coherent state
4520
+ flip. Hover/pin/unpin all feel as a single
4521
+ unified ease rather than "tint pops, label
4522
+ follows". data-group-label-tint-transition
4523
+ attr exposes the timing for tests. */
4524
+ /* Round 460 / Loop — extend the R459-200ms tint rect
4525
+ transition list to include `x` + `width` so the
4526
+ hitbox slides into place when a cluster grows or
4527
+ shrinks (member joins / leaves / status change
4528
+ re-pricing box.w). Pre-R460 every resize snap-
4529
+ jumped the hitbox bounds — a small but visible
4530
+ glitch right at the moment the operator's
4531
+ attention is on the cluster. SVG2 CSS animation
4532
+ on geometry attrs has shipped in Chrome 95+ /
4533
+ Safari 16+ / FF 70+; the runtime gracefully
4534
+ no-ops on older browsers. Sibling motion idiom
4535
+ to R134 / R141 / R142 (panel rect transitions)
4536
+ at the group-label hitbox tier.
4537
+ data-group-label-tint-geom-transition attr
4538
+ exposes the geometry-axis presence for tests. */
4539
+ data-group-label-tint-transition="200ms"
4540
+ data-group-label-tint-geom-transition="x,width,rx"
4541
+ style={{ transition: 'fill 200ms ease-out, opacity 200ms ease-out, x 200ms ease-out, width 200ms ease-out, rx 200ms ease-out' }}
3888
4542
  />
3889
4543
  {/* Round 218 / Loop: group label gains a letter-spacing
3890
4544
  transition on pin — the text subtly spaces out
@@ -3904,16 +4558,75 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3904
4558
  matches R142 fill timing so all the group-label
3905
4559
  state-flip channels (fill colour, rect stroke,
3906
4560
  rect drop-shadow, label tracking) ease as one. */}
4561
+ {/* Round 432 / Loop: extend the group-label letter-
4562
+ spacing tween from 2-tier (rest/pin) to 3-tier
4563
+ (rest/hover/pin → 0/0.25/0.5). Pre-R432 R218
4564
+ spread the text only on pin; hover got an
4565
+ R63 fill brighten (legendText → legendHeadline)
4566
+ but no typographic axis of its own. R432 adds
4567
+ the missing mid tier so hover telegraphs through
4568
+ BOTH the fill brighten AND a subtle kerning
4569
+ spread — sibling pattern to R427 node-alias
4570
+ (0/0.3/0.5) and R431 edge-badge (0/0.2/0.4) at
4571
+ group-label scope. Pin tier (0.5) still wins.
4572
+ Subtler mid tier (0.25 vs alias 0.3) because the
4573
+ group label is a structural anchor — too much
4574
+ spread would steal weight from the per-node
4575
+ alias identity it groups. Hover-letter-spacing
4576
+ family extension (8 anchors now):
4577
+ R344 chip count digit
4578
+ R345 panel title
4579
+ R347 active-links chip
4580
+ R351 vendor chip
4581
+ R420 zoom-level chip
4582
+ R427 node alias text
4583
+ R431 edge-badge digit
4584
+ R432 group label text (this round)
4585
+ R218 transition list ('fill 200ms, letter-spacing
4586
+ 200ms') untouched — additive conditional case. */}
4587
+ {/* Round 457 / Loop: group label parent text fontWeight
4588
+ 700 → 800 on isPinned. Adds typographic weight axis
4589
+ to the group-label parent text, sibling to R432
4590
+ letter-spacing tween at the same surface. Pre-R457
4591
+ pin lifted ls 0 → 0.5px (R218→R432 3-tier) but the
4592
+ fw stayed planted at R63's 700 — locked groups
4593
+ read as wider-but-same-weight. R457 adds the
4594
+ weight axis so pinned groups read as tightened
4595
+ AND wider, matching the R416/R424/R425/R426/R444/
4596
+ R445/R446 "data tightens under attention" idiom
4597
+ (now extended to the parent-text scope at the
4598
+ group-label tier). R63 fill brighten + R432
4599
+ letter-spacing 0/0.25/0.5 3-tier + R55 transition
4600
+ list all preserved; extends to include 'font-
4601
+ weight 200ms ease-out' so the bump eases under
4602
+ the same cadence. */}
4603
+ {/* v0.11.0 #147 Hero D — Vincent 5401 ask: "dash 网络
4604
+ 图里面这个工程的名字也太大了, 超级丑". Per Vincent
4605
+ screenshot 实测. Initial attempt moved label to
4606
+ bottom-right (#147 spec Option A); topo-overlap-test
4607
+ caught 7 grid collisions because cluster boxes have
4608
+ no bottom padding. Pivot to Option C: keep top-left
4609
+ anchor, shrink fontSize 13 → 9 (-31%, watermark
4610
+ register), dim default opacity 1 → 0.55 (hover/pin
4611
+ restore to 1). Net Twitter-grok improvement:
4612
+ cluster labels no longer dominate the canvas at
4613
+ rest; operator still hovers to find specific groups.
4614
+ Position unchanged to preserve the existing
4615
+ geometry that overlap-test gates. */}
3907
4616
  <text
3908
4617
  x={box.x + 12}
3909
- y={box.y + 14}
4618
+ y={box.y + 12}
3910
4619
  fill={isHovered ? pal.legendHeadline : pal.legendText}
3911
- fontSize="13"
4620
+ fontSize="9"
3912
4621
  fontFamily="monospace"
3913
- fontWeight="700"
4622
+ fontWeight={isPinned ? '800' : '700'}
4623
+ opacity={isPinned || isHovered ? 1 : 0.55}
4624
+ data-group-label-hovered={isHovered && !isPinned ? 'true' : 'false'}
4625
+ data-group-label-font-weight={isPinned ? '800' : '700'}
3914
4626
  style={{
3915
- transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out',
3916
- letterSpacing: isPinned ? '0.5px' : '0px',
4627
+ transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out, font-weight 200ms ease-out, opacity 200ms ease-out',
4628
+ letterSpacing: isPinned ? '0.5px' :
4629
+ isHovered ? '0.25px' : '0px',
3917
4630
  }}
3918
4631
  data-group-label={box.key}
3919
4632
  data-group-label-pinned={isPinned ? 'true' : 'false'}
@@ -3947,13 +4660,67 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3947
4660
  lives at a fixed dx=6 offset from the name, so a
3948
4661
  digit-width jitter at 9→10 used to shift the
3949
4662
  whole count visibly. Tabular locks it. */}
4663
+ {/* Round 366 / Loop: group label member-count tspan
4664
+ fontWeight 400 → 500. Sibling polish to R363
4665
+ recent-row alias text fw 400 → 500 + R364 legend-
4666
+ row label fw 400 → 500 — closes the per-row 'count
4667
+ is fw 500 against label-tier fw 700' pattern at
4668
+ the group-label scope (grid layout cluster mark).
4669
+ Hierarchy snapshot post-R366 across all 3 row
4670
+ surfaces:
4671
+ recent count(hot/cold) fw 700/600 (R320)
4672
+ recent alias fw 500 (R363)
4673
+ legend count fw 600 (R309)
4674
+ legend label fw 500 (R364)
4675
+ group name fw 700 (legacy)
4676
+ group count fw 500 (R366, this round)
4677
+ Monospace family + R225 tabular-nums lock digit
4678
+ width, so the fw bump is paint-only — bbox
4679
+ unchanged + overlap-test invariants hold. R229
4680
+ fill-inherit from parent label (hover-deepen-own-
4681
+ hue family) preserved. data-group-label-count-
4682
+ font-weight attr exposes the value for tests. */}
4683
+ {/* Round 444 / Loop: group label count tspan
4684
+ fontWeight 500 → 600 on isPinned. Extends the
4685
+ "data tightens under attention" typographic-
4686
+ weight pattern to a 5th anchor at the group-
4687
+ label-count scope:
4688
+ R416 chip-digit (chip hover)
4689
+ R424 panel-digit (panel hover)
4690
+ R425 hub-digit (hub hover)
4691
+ R426 edge-badge-digit (pin/hot)
4692
+ R444 group-label-count (pinned) ← this round
4693
+ Same idiom — when the group is locked, its
4694
+ member-count tightens typographically alongside
4695
+ the R432 letter-spacing spread (0 → 0.5px) on
4696
+ the parent label. Hover keeps rest fw (500) so
4697
+ the locked vs preview distinction at the type
4698
+ level stays intact — same gate R432 used.
4699
+ Monospace + R225 tabular-nums lock the digit
4700
+ width across fw changes; bbox unchanged; overlap-
4701
+ test invariants hold. transition list adds
4702
+ 'font-weight 200ms ease-out' matching R432
4703
+ letter-spacing cadence. R229 fill-inherit
4704
+ preserved (parent text fill still drives the
4705
+ hover/pin color). data-group-label-count-font-
4706
+ weight + -pinned attrs exposed for tests. */}
4707
+ {/* v0.11.0 #147 — count tspan tracks parent fontSize:
4708
+ 11 → 8 to match the new 9px label scale (parent
4709
+ dropped 13 → 9 with same -2px gap to the count
4710
+ suffix). dx="4" replaces dx="6" — the smaller
4711
+ glyph baseline doesn't need the wider gutter. */}
3950
4712
  <tspan
3951
- dx="6"
3952
- fontSize="11"
3953
- fontWeight="400"
4713
+ dx="4"
4714
+ fontSize="8"
4715
+ fontWeight={isPinned ? '600' : '500'}
3954
4716
  data-group-label-count={box.key}
3955
4717
  data-group-label-count-value={box.count}
3956
- style={{ fontVariantNumeric: 'tabular-nums' }}
4718
+ data-group-label-count-pinned={isPinned ? 'true' : 'false'}
4719
+ data-group-label-count-font-weight={isPinned ? '600' : '500'}
4720
+ style={{
4721
+ fontVariantNumeric: 'tabular-nums',
4722
+ transition: 'font-weight 200ms ease-out',
4723
+ }}
3957
4724
  >· {box.count}</tspan>
3958
4725
  {/* Round 58 / Loop: status mix pip strip. Compact text-
3959
4726
  based chips (e.g. "2w 1i") so the strip stays inside
@@ -4010,11 +4777,36 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4010
4777
  breakdown. Multi-tier groups (e.g. `alpha · 3
4011
4778
  2w 1i`) render unchanged — those pips genuinely
4012
4779
  add breakdown info that the total doesn't carry. */}
4780
+ {/* Round 458 / Loop — Hero D #147 finishing polish on top of
4781
+ N站牛/codex preview.125 (Option C: top-left label fontSize
4782
+ 13→9 + opacity 0.55 rest / 1 hover+pin, count tspan 11→8).
4783
+ That ship left the 3 status pips at fontSize=11 — visibly
4784
+ DOMINATING the now-9px parent label they trail. Result on
4785
+ a 5-member cluster: `alpha · 5 3w 2i` renders inside-out
4786
+ as "tiny name + tiny count + BIG bright pips" rather than
4787
+ a coherent right-tail of metadata. R458 scales the 3 pips
4788
+ to fontSize=8 (matches count tspan) and tightens dx 8/4/4
4789
+ → 6/3/3 (gutter ratio 0.73/0.36 glyph-widths @ 11px ≈
4790
+ 0.75/0.38 glyph-widths @ 8px — same visual rhythm at the
4791
+ smaller scale). The whole group-label bottom-right strip
4792
+ now reads as a unified 9/8/8/8 typographic ladder:
4793
+ name (parent <text>) fontSize 9 fw 700/800
4794
+ · count (1st tspan) fontSize 8 fw 500/600
4795
+ Nw (2nd tspan) fontSize 8 fw 600
4796
+ Ni (3rd tspan) fontSize 8 fw 600
4797
+ No (4th tspan) fontSize 8 fw 600
4798
+ Closes Vincent /goal 5401 ("太大太丑") at the pip-strip
4799
+ tier; with codex preview.125 the spec is fully realized.
4800
+ Geometry-only attribute changes — bbox tightens slightly
4801
+ (8px chars vs 11px chars stay inside the original 240px
4802
+ hitbox max) so topo-overlap-test invariants hold.
4803
+ tabular-nums + anet-fade-in + theme-eased fill 200ms
4804
+ preserved on every tspan. */}
4013
4805
  {box.statuses.working > 0 && box.statuses.working !== box.count && (
4014
4806
  <tspan
4015
- dx="8"
4807
+ dx="6"
4016
4808
  fill={isLight ? '#059669' : '#22c55e'}
4017
- fontSize="11"
4809
+ fontSize="8"
4018
4810
  fontWeight="600"
4019
4811
  className="anet-fade-in"
4020
4812
  data-group-pip="working"
@@ -4023,9 +4815,9 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4023
4815
  )}
4024
4816
  {box.statuses.idle > 0 && box.statuses.idle !== box.count && (
4025
4817
  <tspan
4026
- dx="4"
4818
+ dx="3"
4027
4819
  fill={isLight ? '#0d9488' : '#2dd4bf'}
4028
- fontSize="11"
4820
+ fontSize="8"
4029
4821
  fontWeight="600"
4030
4822
  className="anet-fade-in"
4031
4823
  data-group-pip="idle"
@@ -4034,9 +4826,9 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4034
4826
  )}
4035
4827
  {box.statuses.offline > 0 && box.statuses.offline !== box.count && (
4036
4828
  <tspan
4037
- dx="4"
4829
+ dx="3"
4038
4830
  fill={isLight ? '#94a3b8' : '#6b7280'}
4039
- fontSize="11"
4831
+ fontSize="8"
4040
4832
  fontWeight="600"
4041
4833
  className="anet-fade-in"
4042
4834
  data-group-pip="offline"
@@ -4083,13 +4875,34 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4083
4875
  // synchronised per-edge animation set.
4084
4876
  const stagger = (index * 0.37) % duration;
4085
4877
  // Round 10 / Loop: freshness fade. An edge that fired ≤30s ago
4086
- // stays at full intensity; over 5 minutes it decays to ~35%.
4087
- // Surfaces "what's happening now" vs background chatter without
4088
- // hiding old flow entirely (some context still useful). `now`
4089
- // captured at useMemo-recompute time (every 5s message refresh)
4090
- // — accuracy is within the poll interval, plenty.
4878
+ // stays at full intensity; over 5 minutes it decays to a
4879
+ // floor. Surfaces "what's happening now" vs background
4880
+ // chatter without hiding old flow entirely (some context
4881
+ // still useful). `now` captured at useMemo-recompute time
4882
+ // (every 5s message refresh) — accuracy is within the poll
4883
+ // interval, plenty.
4884
+ //
4885
+ // Round 406 / Loop: edge freshness fade floor 0.35 → 0.40.
4886
+ // Stale-state legibility lift family (6th anchor) — pre-
4887
+ // R406 edges older than 5 minutes faded to α=0.35 (a 65 %
4888
+ // dim against full intensity). The decay rate is the same
4889
+ // 1 - ageMs/300s curve; only the FLOOR shifts. Sibling
4890
+ // treatment to:
4891
+ // R317 subordinate-text gray-500 → gray-400
4892
+ // R358 freshness ramp floor 0.25 → 0.30
4893
+ // R372 minimap offline-dot opacity 0.5 → 0.6
4894
+ // R404 hub-halo cyber trough 0.08 → 0.10
4895
+ // R405 hub-halo light trough 0.32 → 0.34
4896
+ // R406 edge freshness floor 0.35 → 0.40 (this round)
4897
+ // Edges past 5min now sit at 40% intensity instead of 35%
4898
+ // — they still recede against fresh edges but read
4899
+ // legibly enough to convey "this conversation existed".
4900
+ // ageMs threshold for the 5-minute decay unchanged; the
4901
+ // decay curve shape (linear) unchanged. The visual delta
4902
+ // is most pronounced on edges between 5-60 minutes old —
4903
+ // where the floor was binding pre-R406.
4091
4904
  const ageMs = link.last_at ? Math.max(0, Date.now() - Date.parse(link.last_at)) : 0;
4092
- const fresh = Math.max(0.35, 1 - ageMs / (5 * 60 * 1000));
4905
+ const fresh = Math.max(0.40, 1 - ageMs / (5 * 60 * 1000));
4093
4906
  // Round 16 arrow-tier binning — keep `topo-arrow` as the
4094
4907
  // medium tier id so the legend swatch picks it up unchanged.
4095
4908
  const arrowId = link.count <= 2 ? 'topo-arrow-s'
@@ -4154,7 +4967,27 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4154
4967
  // tracks it even at low message counts (width was 3 → 4.5 on
4155
4968
  // hover). 1.4× is enough to read as "lifted" without
4156
4969
  // breaching the 16-px hitbox bound.
4157
- const renderWidth = isHoveredEdge ? Math.min(width * 1.4, 10) : width;
4970
+ // Round 436 / Loop: extend the thickening to the
4971
+ // "hovered-endpoint" case — when hoveredAlias matches one
4972
+ // of this edge's endpoints, lift width by 1.15× (capped at
4973
+ // 8 px to stay clear of the 16-px hitbox). Pre-R436 the
4974
+ // edgeOpacityMul=1.7 (line 4703) lifted the matched edge's
4975
+ // OPACITY when an endpoint was hovered but the stroke-WIDTH
4976
+ // stayed at base — so the edge faded brighter without
4977
+ // thickening, leaving the paint+geometry axes mismatched.
4978
+ // Mirror of R430/R435 hub-spoke pattern (opacity + stroke-
4979
+ // width co-lift on hoveredAlias); R436 brings the same
4980
+ // dual-axis "this node's link" gesture to the edge scope.
4981
+ // 1.15× is subtler than isHoveredEdge=1.4× because
4982
+ // endpoint-hover lifts MANY edges at once (every edge
4983
+ // incident on the hovered node) while edge-hover lifts ONE
4984
+ // — the gesture should read as "highlighted" not "loud".
4985
+ // R166 stroke-width 300ms transition already in the
4986
+ // visible-path style list so the lift eases for free.
4987
+ const isEndpointHoveredEdge = !!hoveredAlias && (link.from === hoveredAlias || link.to === hoveredAlias);
4988
+ const renderWidth = isHoveredEdge ? Math.min(width * 1.4, 10)
4989
+ : isEndpointHoveredEdge ? Math.min(width * 1.15, 8)
4990
+ : width;
4158
4991
  return (
4159
4992
  <g
4160
4993
  key={link.key}
@@ -4241,30 +5074,89 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4241
5074
  online chips to splice in additional properties
4242
5075
  beside Tailwind's). data-edge-flow-rail attr
4243
5076
  surfaces the path for test introspection. */}
5077
+ {/* Round 381 / Loop: edge visible flow path picks up
5078
+ strokeLinecap='round'. Sibling polish to R378
5079
+ flow-rail dashed linecap — both flow-element paths
5080
+ (visible primary + dashed secondary rail) now share
5081
+ 'round' linecap vocabulary. The visible path runs
5082
+ source-node → dest-node as one continuous line, so
5083
+ the dest-end is covered by the markerEnd arrow and
5084
+ the source-end usually sits inside the source-node
5085
+ circle. At certain alignments (post-zoom, post-
5086
+ layout-switch transitions), the source-end may peek
5087
+ out by a fraction of a px past the node edge —
5088
+ round caps render that overshoot as a smooth half-
5089
+ disc instead of a sharp rectangle. Pure paint
5090
+ refinement, geometry-safe (bbox of the stroke
5091
+ unchanged at the join with the arrow marker).
5092
+ data-edge-visible-linecap attr exposes the value
5093
+ for tests. */}
4244
5094
  <path
4245
5095
  d={path}
4246
5096
  fill="none"
4247
5097
  stroke={pal.flowEdge}
4248
5098
  strokeWidth={renderWidth}
5099
+ strokeLinecap="round"
4249
5100
  opacity={Math.min(1, (isLight ? 0.22 : 0.28) * fresh * edgeOpacityMul)}
4250
5101
  filter={isLight ? undefined : 'url(#topo-glow)'}
4251
5102
  markerEnd={`url(#${arrowId})`}
4252
5103
  data-edge-visible={link.key}
5104
+ data-edge-visible-linecap="round"
5105
+ data-edge-visible-endpoint-hovered={isEndpointHoveredEdge ? 'true' : 'false'}
5106
+ data-edge-visible-stroke-width={renderWidth}
4253
5107
  style={{
4254
5108
  pointerEvents: 'none',
4255
5109
  transition: 'opacity 300ms ease-out, stroke-width 300ms ease-out, stroke 300ms ease-out',
4256
5110
  }}
4257
5111
  />
5112
+ {/* Round 378 / Loop: edge flow-path dashed-rail picks
5113
+ up strokeLinecap='round'. Pre-R378 the rail
5114
+ rendered '2 12' dashes as sharp 1×2 rectangles
5115
+ against the canvas backdrop; default 'butt' caps
5116
+ leave dash ends square. R378 rounds each cap so
5117
+ the dashes read as soft 3-px pills (1 px stroke +
5118
+ 0.5 px round cap each end). The flow-rail is the
5119
+ secondary 'invisible-spine' line that gives the
5120
+ R57 spoke flow a directional rail to slide along
5121
+ — rounding the dashes softens its presence
5122
+ against the primary visible flow path (R245 has
5123
+ no strokeLinecap so it inherits 'butt' on a
5124
+ continuous line, irrelevant). Geometry-safe:
5125
+ round caps only widen the visible dash; the
5126
+ bbox of the path is unchanged so overlap-test
5127
+ invariants hold. data-edge-flow-rail-linecap
5128
+ attr exposes the value for tests. */}
4258
5129
  <path
4259
5130
  id={`flow-path-${index}`}
4260
5131
  d={path}
4261
5132
  fill="none"
4262
5133
  stroke={pal.flowPath}
4263
- strokeWidth="1"
5134
+ /* Round 437 / Loop: flow-rail strokeWidth hover lift —
5135
+ 1 → 1.5 on (isHoveredEdge || isEndpointHoveredEdge).
5136
+ Pre-R437 the dashed rail sat at sw=1 always while the
5137
+ visible flow path above it lifted (R50 ×1.4 on
5138
+ isHoveredEdge / R436 ×1.15 on isEndpointHoveredEdge).
5139
+ The two edge paint layers were mismatched on hover:
5140
+ top layer thickened, underline stayed thin — so the
5141
+ hover gesture lifted only half the edge surface.
5142
+ R437 lifts the underline too so the whole edge
5143
+ reads as "raised" on hover, not just its bright
5144
+ top stripe. Same +0.5 absolute delta R435 used at
5145
+ hub-spoke scope (1→1.25 there, slightly bigger
5146
+ here because the rail's base 1 is at the kerning
5147
+ floor and needs more lift to register).
5148
+ Transition list extends to include stroke-width
5149
+ 300ms so the new lift eases under the same R166
5150
+ cadence as the visible path's stroke-width. */
5151
+ strokeWidth={(isHoveredEdge || isEndpointHoveredEdge) ? 1.5 : 1}
4264
5152
  strokeDasharray="2 12"
5153
+ strokeLinecap="round"
4265
5154
  opacity={Math.min(1, (isLight ? 0.4 : 0.75) * fresh * edgeOpacityMul)}
4266
5155
  data-edge-flow-rail={link.key}
4267
- style={{ transition: 'opacity 300ms ease-out, stroke 300ms ease-out' }}
5156
+ data-edge-flow-rail-linecap="round"
5157
+ data-edge-flow-rail-stroke-width={(isHoveredEdge || isEndpointHoveredEdge) ? 1.5 : 1}
5158
+ data-edge-flow-rail-lifted={(isHoveredEdge || isEndpointHoveredEdge) ? 'true' : 'false'}
5159
+ style={{ transition: 'opacity 300ms ease-out, stroke 300ms ease-out, stroke-width 300ms ease-out' }}
4268
5160
  />
4269
5161
  {!reducedMotion && (
4270
5162
  /* Round 103 / Loop: phase-stagger the particles so
@@ -4279,25 +5171,40 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4279
5171
  multiple). Edge order is stable (sorted by recent
4280
5172
  activity), so the offsets feel calm rather than
4281
5173
  reshuffling each refresh. */
5174
+ /* Round 422 / Loop: edge flow particle radius 4 → 4.5.
5175
+ Visual-weight bump family (15th anchor) — particles
5176
+ riding along the edge animateMotion path get +0.5px
5177
+ radius lift, increasing visual area by ~27%
5178
+ (π·4.5² / π·4² = 1.27). Sibling magnitude to R383
5179
+ recent-row pip 1.8 → 2.0 (+25% area), R384 minimap
5180
+ online dot 1.7 → 1.9 (+25% area). R251 fill +
5181
+ R252 transitions + R103 phase-stagger animateMotion
5182
+ all preserved. data-edge-particle-radius attr
5183
+ exposes the value for tests. */
4282
5184
  <circle
4283
- r="4"
5185
+ /* Round 439 / Loop: edge flow particle radius hover
5186
+ lift — r 4.5 → 5.5 on (isHoveredEdge ||
5187
+ isEndpointHoveredEdge). Continues edge paint-
5188
+ layer parity arc (R436 visible path sw / R437
5189
+ flow-rail sw / R439 particle r) so the whole
5190
+ edge surface — including the moving particle —
5191
+ lifts on hover, not just the static stripes.
5192
+ +1px radius gives ~50% area boost. Subtler than
5193
+ 1.4× sw bump on visible path because the
5194
+ particle is already small + motion-bright;
5195
+ +1px reads as "the dot caught attention"
5196
+ without overshadowing the path lift. R252
5197
+ transition list extends to include r 200ms so
5198
+ the size change eases under the same fill/
5199
+ opacity cadence. */
5200
+ r={(isHoveredEdge || isEndpointHoveredEdge) ? 5.5 : 4.5}
4284
5201
  fill={pal.flowParticle}
4285
5202
  filter={isLight ? undefined : 'url(#topo-glow)'}
4286
5203
  opacity={Math.min(1, fresh * edgeOpacityMul)}
4287
5204
  data-edge-particle={link.key}
4288
- /* Round 252 / Loop: particle picks up fill +
4289
- opacity transition for theme-toggle smoothing.
4290
- Pre-R252 pal.flowParticle (cyber #fef08a yellow
4291
- ↔ light #f59e0b amber) snapped on theme toggle
4292
- while every other edge element eased (R245
4293
- paths, R251 badge, R242 chat-target, R233
4294
- endpoint ring). opacity is freshness-driven so
4295
- it transitions per-frame as fresh decays anyway
4296
- — but adding opacity to the explicit transition
4297
- list also covers theme toggle (R3 className
4298
- transition-opacity duration-300 was previously
4299
- absent on this circle). */
4300
- style={{ transition: 'fill 200ms ease-out, opacity 200ms ease-out' }}
5205
+ data-edge-particle-radius={(isHoveredEdge || isEndpointHoveredEdge) ? 5.5 : 4.5}
5206
+ data-edge-particle-lifted={(isHoveredEdge || isEndpointHoveredEdge) ? 'true' : 'false'}
5207
+ style={{ transition: 'fill 200ms ease-out, opacity 200ms ease-out, r 200ms ease-out' }}
4301
5208
  >
4302
5209
  <animateMotion
4303
5210
  dur={`${duration}s`}
@@ -4628,14 +5535,137 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4628
5535
  R251 closes the per-edge surface theme-toggle
4629
5536
  smoothness — every theme-driven property on
4630
5537
  every edge element now eases under cyber↔light. */}
5538
+ {/* Round 367 / Loop: edge midpoint badge rest
5539
+ stroke-width 1 → 1.25. Sibling visual-weight
5540
+ bump family (7th canvas anchor now):
5541
+ R287 minimap viewport stroke 1 → 1.5
5542
+ R295 legend swatch base radius 5.5 → 6
5543
+ R359 recent-row pip base radius 1.6 → 1.8
5544
+ R360 hub digit fontSize 11 → 12
5545
+ R361 edge-badge digit fontSize 10 → 11
5546
+ R365 hub-highlight base radius 5 → 5.5
5547
+ R367 edge-badge rest stroke 1 → 1.25 (this round)
5548
+ Cold edge badges gain ~25 % stroke presence
5549
+ (1.25/1.0 = 1.25). Stays clear of the R51
5550
+ overlap-test sentinel values (1.5 / 3 reserved
5551
+ for node strokes — the test selector is gated
5552
+ to g[data-node] ancestors so this edge-internal
5553
+ circle is invisible to that probe anyway, but
5554
+ 1.25 is a safe non-sentinel value regardless).
5555
+ R188 transition list 'stroke-width 300ms ease-
5556
+ out' still smoothes the hot/pin flip — now
5557
+ 1.25 → 2 instead of 1 → 2, same ease pace.
5558
+ data-edge-badge-stroke-width-rest exposes the
5559
+ new baseline for tests. */}
5560
+ {/* Round 371 / Loop: edge-badge cyber opacity 0.82
5561
+ → 0.85. Sibling theme-consistency polish to R370
5562
+ hub hover-ring 0.7 → 0.8. R251 designed this
5563
+ badge with opacity 0.82 (cyber) / 0.95 (light)
5564
+ — 13 % delta. Cyber-theme dark bg needs more
5565
+ alpha to read as 'present'; R371 narrows the
5566
+ gap to 10 %, bringing the badge closer to light
5567
+ theme's 0.95 floor. Light stays at 0.95
5568
+ (already in the legibility band). data-edge-
5569
+ badge-opacity attr exposes the resolved value.
5570
+ Theme-consistency polish family:
5571
+ R246/R247 panel transition family
5572
+ R251 edge badge fill/opacity baseline
5573
+ R370 hub hover-ring cyber 0.7 → 0.8
5574
+ R371 edge badge cyber 0.82 → 0.85 (this round)
5575
+ R164 r=9/10.5 hover-lift + R188/R251 transition
5576
+ list + R367 strokeWidth=1.25 cold rest preserved. */}
5577
+ {/* Round 394 / Loop: edge-badge gains a hover
5578
+ strokeWidth tier (1.5) between cold rest
5579
+ (R367 1.25) and pin/hot (2). Pre-R394 the
5580
+ badge lifted only its radius on hover (R164
5581
+ 9 → 10.5); the stroke stayed at cold rest
5582
+ 1.25 unless pin/hot kicked in, so a plain
5583
+ hover felt half-lifted — geometry expanded
5584
+ while the contour stayed thin. R394 adds
5585
+ strokeWidth=1.5 on isHoveredEdge so hover
5586
+ now lifts both r AND stroke in concert —
5587
+ same pattern R385 used for the hub hover-
5588
+ ring (1.5 → 1.75) where the ring's three
5589
+ hover axes (r grow / opacity fade-in /
5590
+ stroke thicken) all rise together.
5591
+ Three-tier stroke hierarchy now:
5592
+ cold rest 1.25 (R367)
5593
+ hovered 1.5 (R394 — this round)
5594
+ pinned / hot 2.0 (R188)
5595
+ R51 sentinel concern: strokeWidth=1.5 is
5596
+ one of the two sentinels reserved for node
5597
+ detection, but the R51 selector is gated
5598
+ to `g[data-node]` ancestors so this edge-
5599
+ internal circle is invisible to the probe
5600
+ (same lesson R177 hub hover-ring + R367
5601
+ cold rest documented). 300ms strokeWidth
5602
+ transition already in the style list eases
5603
+ the new tier naturally. data-edge-badge-
5604
+ stroke-width-hover attr exposes the hover
5605
+ value for tests. */}
5606
+ {/* Round 395 / Loop: edge-badge gains a third
5607
+ hover axis — opacity 0.85 (cyber) / 0.95
5608
+ (light) → 1.0 on isHoveredEdge. Pre-R395
5609
+ hovering thickened the stroke (R394 1.25 →
5610
+ 1.5) and grew the radius (R164 9 → 10.5)
5611
+ but the badge's translucency stayed put at
5612
+ R371's rest alpha (cyber 0.85 / light 0.95).
5613
+ R395 lifts hover to a clean 1.0 — fully
5614
+ opaque — so the hovered badge reads as
5615
+ "in focus" against the dim siblings.
5616
+ Three-axis hover-lift parity now complete:
5617
+ hub hover-ring (R177/R370/R385):
5618
+ r 14 → 17, opacity 0 → 0.8 cyber, sw 1.5 → 1.75
5619
+ edge badge (R164/R394/R395):
5620
+ r 9 → 10.5, sw 1.25 → 1.5, opacity → 1.0
5621
+ 200ms opacity transition (already in the
5622
+ style list) eases the new axis naturally.
5623
+ R371 rest opacity (0.85 cyber / 0.95 light)
5624
+ preserved as the resting alpha — R395
5625
+ adds an isHoveredEdge override on top.
5626
+ data-edge-badge-opacity-hover attr exposes
5627
+ the hover value for tests. */}
5628
+ {/* Round 396 / Loop: extend the R395 opacity → 1.0
5629
+ lift to the pinned state. Pre-R396 the badge
5630
+ shared `r=10.5` on both hover AND pin (R164
5631
+ unified-lift) but R395's opacity lift fired
5632
+ ONLY on isHoveredEdge — pinned badges stayed
5633
+ at R371 rest alpha (cyber 0.85 / light 0.95).
5634
+ That left pin (sticky selection) reading
5635
+ softer than hover (transient preview), even
5636
+ though pin is the stronger commitment.
5637
+ R396 unifies hover + pin at opacity=1.0
5638
+ so the same data-edge-badge-lifted='true'
5639
+ surface uniformly carries full alpha. Pin
5640
+ stroke (R188 sw=2 + pal.legendHeadline color)
5641
+ continues to differentiate pin from hover —
5642
+ the opacity track now closes the lift parity.
5643
+ The new gate (isHoveredEdge || isPinned)
5644
+ mirrors the existing R164 r-lift gate, so
5645
+ the badge has a single "active state"
5646
+ signature across r + opacity.
5647
+ 200ms opacity transition (already in style
5648
+ list) eases pin/unpin naturally. R371 rest
5649
+ opacity preserved as the resting alpha.
5650
+ data-edge-badge-opacity-hover renamed
5651
+ semantically to -active (covers hover+pin)
5652
+ via the new -opacity-active attr; the
5653
+ legacy -opacity-hover attr kept for R395
5654
+ test compatibility. */}
4631
5655
  <circle
4632
5656
  cx={badgeX} cy={badgeY}
4633
5657
  r={isHoveredEdge || isPinned ? 10.5 : 9}
4634
5658
  fill={pal.legendBox.fill}
4635
5659
  stroke={isPinned ? pal.legendHeadline : isHot ? hotStroke : pal.flowEdge}
4636
- strokeWidth={isPinned ? 2 : isHot ? 2 : 1}
4637
- opacity={isLight ? 0.95 : 0.82}
5660
+ strokeWidth={isPinned ? 2 : isHot ? 2 : isHoveredEdge ? 1.5 : 1.25}
5661
+ opacity={(isHoveredEdge || isPinned) ? 1 : (isLight ? 0.95 : 0.85)}
4638
5662
  data-edge-badge-lifted={(isHoveredEdge || isPinned) ? 'true' : 'false'}
5663
+ data-edge-badge-stroke-width-rest="1.25"
5664
+ data-edge-badge-stroke-width-hover="1.5"
5665
+ data-edge-badge-opacity={(isHoveredEdge || isPinned) ? 1 : (isLight ? 0.95 : 0.85)}
5666
+ data-edge-badge-opacity-rest={isLight ? 0.95 : 0.85}
5667
+ data-edge-badge-opacity-hover="1"
5668
+ data-edge-badge-opacity-active="1"
4639
5669
  style={{ transition: 'r 180ms ease-out, stroke 300ms ease-out, stroke-width 300ms ease-out, fill 200ms ease-out, opacity 200ms ease-out' }}
4640
5670
  />
4641
5671
  {/* Round 224 / Loop: edge badge text gains the 4th
@@ -4674,16 +5704,75 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4674
5704
  x={badgeX} y={badgeY + 3}
4675
5705
  textAnchor="middle"
4676
5706
  fill={pal.legendHeadline}
4677
- fontSize="10"
5707
+ /* Round 361 / Loop: edge midpoint badge text
5708
+ fontSize 10 → 11. Sibling visual-weight bump
5709
+ to R360 hub digit 11 → 12. The badge digit
5710
+ is the per-edge equivalent of the hub digit
5711
+ — a high-information scalar (link.count) at
5712
+ a stable canvas position. Pre-R361 fontSize=
5713
+ 10 + R220 letter-spacing 0.4 + R224 tabular-
5714
+ nums made the digit READABLE but small
5715
+ against the r=9 / 18-px badge envelope;
5716
+ fontSize=11 nudges the glyph ~10 % bigger
5717
+ (bbox ~7×10 px from ~6×9 px) so the count
5718
+ reads more cleanly at glance — still well
5719
+ inside the r=9 idle circle and the r=10.5
5720
+ hover/pin lift (R164). y=badgeY+3 empirical
5721
+ vertical centring kept (1px drift at the
5722
+ bumped size is below the noise floor in
5723
+ the on-curve flow path).
5724
+ Visual-weight bump family:
5725
+ R287 minimap viewport stroke 1 → 1.5
5726
+ R295 legend swatch base radius 5.5 → 6
5727
+ R359 recent-row pip base radius 1.6 → 1.8
5728
+ R360 hub digit fontSize 11 → 12
5729
+ R361 edge-badge digit fontSize 10 → 11 (this round)
5730
+ data-edge-badge-text-font-size attr exposes
5731
+ the value for tests. R220 pin/hot letter-
5732
+ spacing tween + R224 tabular-nums + R188
5733
+ stroke-width pin/hot transitions all preserved. */
5734
+ fontSize="11"
4678
5735
  fontFamily="monospace"
4679
- fontWeight="700"
5736
+ /* R426 — edge-badge digit fontWeight 700 → 800 on
5737
+ (isPinned || isHot). 4th anchor on the "data
5738
+ tightens under attention" typographic-weight
5739
+ pattern:
5740
+ R416 chip-digit (chip-row hover trigger)
5741
+ R424 panel-digit (panel hover trigger)
5742
+ R425 hub-digit (hub hover trigger)
5743
+ R426 edge-badge-digit (pin/hot trigger) ← this
5744
+ The badge digit is the per-edge equivalent of
5745
+ the hub digit (R361 sibling fontSize bump
5746
+ reasoning). Stacks with R188 stroke-width pin/
5747
+ hot lift (1.25 → 1.5) + R220 letter-spacing pin/
5748
+ hot tween (0 → 0.4) for a 3-axis pin/hot signa-
5749
+ ture (edge structure + text spacing + text
5750
+ weight). The R408 transition is letter-spacing
5751
+ 300ms; R426 appends font-weight 300ms so the
5752
+ weight bump co-eases under the same cadence. */
5753
+ fontWeight={(isPinned || isHot) ? '800' : '700'}
4680
5754
  data-edge-badge-text={link.key}
4681
5755
  data-edge-badge-text-pin={(isPinned || isHot) ? 'true' : 'false'}
5756
+ data-edge-badge-text-font-size="11"
4682
5757
  style={{
4683
5758
  pointerEvents: 'none',
4684
5759
  fontVariantNumeric: 'tabular-nums',
4685
- letterSpacing: (isPinned || isHot) ? '0.4px' : '0px',
4686
- transition: 'letter-spacing 300ms ease-out',
5760
+ /* R431 — edge-badge digit 3-tier letter-spacing:
5761
+ rest 0 / isHoveredEdge 0.2 / (isPinned || isHot) 0.4.
5762
+ Mirrors R427 node-alias 3-tier (rest/hover/chat-
5763
+ target → 0/0.3/0.5) at edge-badge scope. Pre-R431
5764
+ letter-spacing only fired on pin/hot (R220) while
5765
+ pure edge hover lifted stroke (R394) + opacity
5766
+ (R395) + radius (R164) but left the text dead-
5767
+ typographic. R431 adds the missing typographic
5768
+ spacing axis to the edge-hover gesture so the
5769
+ text rises with the badge geometry. Pin/hot
5770
+ tier (0.4) still wins; hover is the mid step.
5771
+ Hover-letter-spacing family extension (7 anchors
5772
+ now): R344/R345/R347/R351/R420/R427/R431. */
5773
+ letterSpacing: (isPinned || isHot) ? '0.4px' :
5774
+ isHoveredEdge ? '0.2px' : '0px',
5775
+ transition: 'letter-spacing 300ms ease-out, font-weight 300ms ease-out',
4687
5776
  }}
4688
5777
  >{link.count}</text>
4689
5778
  </g>
@@ -4794,27 +5883,103 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4794
5883
  : workingCount <= 2 ? 1
4795
5884
  : workingCount <= 5 ? 2
4796
5885
  : 3;
5886
+ // Round 404 / Loop: hub-halo cyber trough opacity 0.08 →
5887
+ // 0.10. Pre-R404 the breath's low-point sat at α=0.08
5888
+ // cyber (per R84 family tuning) — the halo nearly faded
5889
+ // out at trough on the dark canvas. R404 lifts cyber
5890
+ // trough to 0.10. Per-bucket peak amplitudes [0.16/0.20/
5891
+ // 0.26/0.32] stay exactly tuned.
5892
+ //
5893
+ // Round 405 / Loop: hub-halo LIGHT trough 0.32 → 0.34 —
5894
+ // symmetric +0.02 lift to mirror R404's cyber treatment
5895
+ // across both themes. Pre-R405 only cyber got the lift
5896
+ // (R404 docstring noted "light already at the strong
5897
+ // end" as deliberate); but the cyber/light delta in
5898
+ // R404 was an inconsistency in the family pattern.
5899
+ // R405 closes the symmetry — both themes get +0.02
5900
+ // baseline lift, so the breath low-point reads with
5901
+ // matching confidence regardless of theme. Light peak
5902
+ // array [0.52/0.58/0.65/0.72] stays tuned.
5903
+ //
5904
+ // Stale-state legibility lift family (5 anchors now):
5905
+ // R317 subordinate-text gray-500 → gray-400
5906
+ // R358 freshness floor 0.25 → 0.30
5907
+ // R372 minimap offline-dot opacity 0.5 → 0.6
5908
+ // R404 hub-halo cyber trough 0.08 → 0.10
5909
+ // R405 hub-halo light trough 0.32 → 0.34 (this round)
5910
+ //
5911
+ // R84 per-bucket peak/dur + R245 ease-in-out spline
5912
+ // keySplines all preserved. Test fixture probes the
5913
+ // SMIL <animate> values via data-topo-hub-halo-trough
5914
+ // attr (now exposes both light + cyber resolved values).
4797
5915
  const peakLight = [0.52, 0.58, 0.65, 0.72][busy];
4798
5916
  const peakDark = [0.16, 0.20, 0.26, 0.32][busy];
4799
- const troughLight = 0.32;
4800
- const troughDark = 0.08;
5917
+ const troughLight = 0.34;
5918
+ const troughDark = 0.10;
4801
5919
  const dur = [4.0, 3.2, 2.7, 2.4][busy];
4802
5920
  const valuesLight = `${troughLight};${peakLight};${troughLight}`;
4803
5921
  const valuesDark = `${troughDark};${peakDark};${troughDark}`;
5922
+ // Round 408 / Loop: hub-halo radius 18 → 20. The
5923
+ // grounding halo (the breathing outer circle around
5924
+ // the hub center) is the canvas's signature breath
5925
+ // element — R84 family. R408 bumps r=18 → 20 so the
5926
+ // breath extends slightly further while keeping 4px
5927
+ // clearance before the spoke origin (still room for
5928
+ // spoke start anchors). Visual presence on the
5929
+ // canvas focal point lifts ~23% area (π·20²/π·18²
5930
+ // = 1.23) without changing the per-bucket opacity
5931
+ // envelope or breath rhythm. Visual-weight bump
5932
+ // family 13th anchor — pairs with R404/R405 trough
5933
+ // lifts so the halo now breathes both with more
5934
+ // visible amplitude AND more visual footprint.
5935
+ // R84 per-bucket peak/dur invariants + R244 calc-
5936
+ // Mode='spline' + R245 ease-in-out keySplines all
5937
+ // preserved. data-topo-hub-halo-radius attr exposes
5938
+ // value for tests.
5939
+ /* Round 451 / Loop: hub center halo radius lift on
5940
+ hoveredHub — r 20 → 22 (+2px, ~21% area). Adds another
5941
+ geometric axis to the hub-hover signature stack
5942
+ alongside R177 ring radius lift + R209 digit scale +
5943
+ R425 digit fw + R370 halo opacity + R386 highlight
5944
+ opacity + R441 core fill chroma. Pre-R451 the halo
5945
+ r stayed planted at R408's 20px while the rest of
5946
+ the hub structure responded to hover. R451 makes
5947
+ the halo breath outward on hover so the focal pulse
5948
+ intensifies under attention. SMIL `<animate>` on
5949
+ opacity continues independently (animateAttr=
5950
+ 'opacity' vs CSS-property r — non-conflicting). R408
5951
+ base radius 20 preserved as rest; +2 hover delta
5952
+ keeps clearance from the R177 hub-hover-ring at
5953
+ r=17 hover (halo is BEHIND the ring, halo r=22 sits
5954
+ 5px beyond the ring's hover-r=17, still well within
5955
+ the hub canvas envelope). data-topo-hub-halo-radius
5956
+ attr now reports the dynamic value. */
5957
+ const isHaloHovered = !reducedMotion && hoveredHub;
5958
+ const haloR = isHaloHovered ? 22 : 20;
4804
5959
  return (
4805
5960
  <circle
4806
- cx={cx} cy={cy} r="18"
5961
+ cx={cx} cy={cy}
4807
5962
  fill={isLight ? '#d1fae5' : '#10b981'}
4808
5963
  opacity={isLight ? 0.42 : 0.12}
4809
5964
  data-hub-busyness={busy}
5965
+ data-topo-hub-halo-radius={haloR}
5966
+ data-topo-hub-halo-hovered={isHaloHovered ? 'true' : 'false'}
5967
+ data-topo-hub-halo-trough={isLight ? troughLight : troughDark}
5968
+ data-topo-hub-halo-peak={isLight ? peakLight : peakDark}
4810
5969
  /* Round 253 / Loop: hub grounding halo fill transition
4811
5970
  for theme toggle. Pre-R253 the base fill (cyber
4812
5971
  #10b981 ↔ light #d1fae5) snapped while R244's SMIL
4813
5972
  animate on opacity continued running. CSS fill
4814
5973
  transition is independent of the SMIL animate
4815
5974
  (different attributes), so they compose without
4816
- conflict. */
4817
- style={{ transition: 'fill 200ms ease-out' }}
5975
+ conflict.
5976
+ R451: r as CSS property (R197/R198 idiom) so the
5977
+ hover-radius tween eases smoothly under the same
5978
+ 200ms cadence as fill. */
5979
+ style={{
5980
+ r: `${haloR}px`,
5981
+ transition: 'fill 200ms ease-out, r 200ms ease-out',
5982
+ } as React.CSSProperties}
4818
5983
  >
4819
5984
  {/* Round 244 / Loop: hub grounding halo breath gets
4820
5985
  ease-in-out keySplines, matching the active-node
@@ -4859,12 +6024,45 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4859
6024
  attr added for test introspection (the parent <g> at
4860
6025
  line 3587 has data-topo-hub but the core specifically
4861
6026
  is the canvas anchor). */}
4862
- <circle
4863
- cx={cx} cy={cy} r="10"
4864
- fill={isLight ? '#059669' : '#10b981'}
4865
- data-topo-hub-core
4866
- style={{ transition: 'fill 200ms ease-out' }}
4867
- />
6027
+ {(() => {
6028
+ /* Round 441 / Loop: hub center core fill brighten on
6029
+ hoveredHub. Pre-R441 the core was static (cyber
6030
+ emerald-500 #10b981 / light emerald-600 #059669) and
6031
+ the hub-hover gesture lifted ring radius (R177) +
6032
+ digit scale (R209) + digit fw (R425) + halo opacity
6033
+ (R370) + highlight opacity (R386) but the focal core
6034
+ ITSELF stayed planted at rest tone. R441 shifts the
6035
+ fill one emerald tier brighter on hover so the canvas
6036
+ anchor itself responds:
6037
+ cyber emerald-500 → emerald-400 (#10b981 → #34d399)
6038
+ light emerald-600 → emerald-500 (#059669 → #10b981)
6039
+ Same +100 step on the emerald scale across both themes.
6040
+ Pure paint axis; no geometry change. R248 fill 200ms
6041
+ transition already in the style list eases the shift.
6042
+ Closes the chroma axis on the hub-hover gesture stack:
6043
+ R177 ring radius lift geometry
6044
+ R209 digit scale 1.08 geometry
6045
+ R425 digit fw 700 → 800 typography
6046
+ R370 halo opacity 0.7 → 0.8 paint
6047
+ R386 highlight opacity paint
6048
+ R441 core fill brighten chroma ← this round
6049
+ data-topo-hub-core-hovered + -fill attrs exposed
6050
+ for tests. */
6051
+ const isCoreHovered = !reducedMotion && hoveredHub;
6052
+ const coreFill = isLight
6053
+ ? (isCoreHovered ? '#10b981' : '#059669')
6054
+ : (isCoreHovered ? '#34d399' : '#10b981');
6055
+ return (
6056
+ <circle
6057
+ cx={cx} cy={cy} r="10"
6058
+ fill={coreFill}
6059
+ data-topo-hub-core
6060
+ data-topo-hub-core-hovered={isCoreHovered ? 'true' : 'false'}
6061
+ data-topo-hub-core-fill={coreFill}
6062
+ style={{ transition: 'fill 200ms ease-out' }}
6063
+ />
6064
+ );
6065
+ })()}
4868
6066
  {/* R130 / Loop: when workingCount > 0, the decorative inner
4869
6067
  highlight gets replaced with the workingCount digit. The
4870
6068
  R84 busyness breath already encodes the same metric
@@ -4905,11 +6103,40 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4905
6103
  textAnchor="middle"
4906
6104
  dy="0.36em"
4907
6105
  fill={isLight ? '#d1fae5' : '#ecfdf5'}
4908
- fontSize="11"
6106
+ /* Round 360 / Loop: hub working-count digit fontSize 11
6107
+ → 12. The hub is the canvas's focal point — its digit
6108
+ is the most-read scalar on the whole topology. R130
6109
+ sized it at 11 (well inside the r=10 / 20-px core);
6110
+ R360 nudges it to 12 (~13 px wide × 12 px tall, still
6111
+ well inside the 20-px diameter) for ~9 % more presence.
6112
+ Sibling visual-weight bump family:
6113
+ R287 minimap viewport stroke 1 → 1.5
6114
+ R295 legend swatch base radius 5.5 → 6
6115
+ R359 recent-row pip radius 1.6 → 1.8
6116
+ R360 hub digit fontSize 11 → 12 (this round)
6117
+ The R209 scale-1.08-on-hub-hover, R225 tabular-nums,
6118
+ R253 fill transition, R213 always-mount opacity gate
6119
+ all preserved. data-topo-hub-working-count-font-size
6120
+ attr exposes the value for tests. */
6121
+ fontSize="12"
4909
6122
  fontFamily="monospace"
4910
- fontWeight="700"
6123
+ /* R425 — hub digit fontWeight 700 → 800 on hoveredHub.
6124
+ Closes the "data tightens under attention" pattern
6125
+ across three focal scopes: chip-digit (R416, chip
6126
+ scope) → panel-digit (R424, panel-header scope) →
6127
+ hub-digit (R425, hub focal scope). The hub digit is
6128
+ the most-read scalar on the topology; adding a weight
6129
+ axis on hover stacks with the R209 scale-1.08 + R177
6130
+ ring grow + R370 halo opacity + R386 highlight
6131
+ opacity hub-hover gestures, giving the focal point
6132
+ a typographic axis alongside its scale/structure cues.
6133
+ R360 fontSize=12 + R225 tabular-nums + R209 scale +
6134
+ R253 fill transition all preserved. Transition list
6135
+ extends to include font-weight 200ms ease-out. */
6136
+ fontWeight={hoveredHub ? '800' : '700'}
4911
6137
  opacity={workingCount > 0 ? 1 : 0}
4912
6138
  data-topo-hub-working-count={workingCount}
6139
+ data-topo-hub-working-count-font-size="12"
4913
6140
  data-topo-hub-working-count-hovered={hoveredHub ? 'true' : 'false'}
4914
6141
  data-topo-hub-working-count-visible={workingCount > 0 ? 'true' : 'false'}
4915
6142
  // Round 209 / Loop: hub workingCount digit scales 1.0 →
@@ -4943,19 +6170,63 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4943
6170
  transform: !reducedMotion && hoveredHub ? 'scale(1.08)' : 'scale(1)',
4944
6171
  transformBox: 'fill-box',
4945
6172
  transformOrigin: 'center',
4946
- transition: 'transform 200ms ease-out, opacity 300ms ease-out, fill 200ms ease-out',
6173
+ /* R425: font-weight 200ms appended so the hover fw
6174
+ bump 700 → 800 eases under the same cadence as
6175
+ R209 scale + R253 fill + R213 opacity. */
6176
+ transition: 'transform 200ms ease-out, opacity 300ms ease-out, fill 200ms ease-out, font-weight 200ms ease-out',
4947
6177
  fontVariantNumeric: 'tabular-nums',
4948
6178
  }}
4949
6179
  >
4950
6180
  {workingCount}
4951
6181
  </text>
4952
6182
  {/* decorative highlight (visible when workingCount === 0) */}
6183
+ {/* Round 365 / Loop: hub-center 'lit-lamp' decorative highlight
6184
+ circle r 5 → 5.5. Sibling visual-weight bump family —
6185
+ each round lifts one canvas anchor's geometric presence
6186
+ without disturbing its bbox envelope:
6187
+ R287 minimap viewport stroke 1 → 1.5
6188
+ R295 legend swatch base radius 5.5 → 6
6189
+ R359 recent-row pip base radius 1.6 → 1.8
6190
+ R360 hub digit fontSize 11 → 12
6191
+ R361 edge-badge digit fontSize 10 → 11
6192
+ R365 hub-highlight base radius 5 → 5.5 (this round)
6193
+ The highlight only renders when workingCount === 0
6194
+ (decorative 'lamp lit but idle' state per R130 + R213
6195
+ always-mount opacity-gate). At idle, the 0.5-px radius
6196
+ bump (21 % area, π*5.5² / π*5² = 1.21) lifts the lamp's
6197
+ presence — still well inside the r=10 hub-core (R130).
6198
+ opacity=0 when working preserved so the hub-digit's R130
6199
+ takeover stays seamless. R213 always-mount opacity-gate
6200
+ + 300ms opacity transition + pointerEvents:none all
6201
+ preserved. data-topo-hub-highlight-radius attr exposes
6202
+ the value for tests. */}
6203
+ {/* Round 386 / Loop: hub-highlight idle opacity 0.9 → 0.95.
6204
+ When workingCount===0 the highlight paints as the visible
6205
+ idle "lamp lit but no work" core (R130 takeover gate).
6206
+ Pre-R386 idle opacity was 0.9 — a ~6 % fade against full
6207
+ paint that read as slightly-dimmed-ghost on the focal
6208
+ point. R386 lifts to 0.95 (idle alpha gap halved 0.10
6209
+ → 0.05) so the canvas anchor reads more confidently
6210
+ as a present-but-idle state rather than a faded ghost.
6211
+ Theme-consistency / canvas-presence polish family (4th
6212
+ anchor):
6213
+ R370 hub hover-ring opacity 0.7 → 0.8 cyber
6214
+ R371 edge-badge rest opacity 0.82 → 0.85 cyber
6215
+ R372 minimap offline-dot opacity 0.5 → 0.6
6216
+ R386 hub-highlight idle opacity 0.9 → 0.95 (this round)
6217
+ opacity=0 when working preserved so the hub-digit's
6218
+ R130 takeover stays seamless. 300ms opacity transition
6219
+ + R213 always-mount opacity-gate + pointerEvents:none
6220
+ + R365 r=5.5 all preserved. data-topo-hub-highlight-
6221
+ opacity attr exposes the resolved value for tests. */}
4953
6222
  <circle
4954
- cx={cx} cy={cy} r="5"
6223
+ cx={cx} cy={cy} r="5.5"
4955
6224
  fill="#d1fae5"
4956
- opacity={workingCount > 0 ? 0 : 0.9}
6225
+ opacity={workingCount > 0 ? 0 : 0.95}
4957
6226
  data-topo-hub-highlight
4958
6227
  data-topo-hub-highlight-visible={workingCount > 0 ? 'false' : 'true'}
6228
+ data-topo-hub-highlight-radius="5.5"
6229
+ data-topo-hub-highlight-opacity={workingCount > 0 ? 0 : 0.95}
4959
6230
  style={{
4960
6231
  pointerEvents: 'none',
4961
6232
  transition: 'opacity 300ms ease-out',
@@ -4986,15 +6257,46 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4986
6257
  R142 group boxes, R143/R144 rows, R164 edge badges,
4987
6258
  R177 hub ring). prefers-reduced-motion respected via
4988
6259
  R29 globals.css blanket. */}
6260
+ {/* Round 385 / Loop: hub hover-ring strokeWidth 1.5 → 1.75.
6261
+ Sibling visual-weight bump (11th anchor) to R367 edge-
6262
+ badge rest stroke 1 → 1.25. The ring is only visible
6263
+ during hub hover (opacity=0 rest, R177 + R370 control
6264
+ the hover-state alpha) so the change manifests purely
6265
+ as a thicker hover-state ring on the canvas focal
6266
+ point. R177 r 14 → 17 grow + R370 opacity 0 → 0.8
6267
+ already lift the hover cue; R385 adds stroke weight
6268
+ as the third lift axis. Stays clear of R51 overlap-
6269
+ test sentinel value 3 (1.75 is non-sentinel); the
6270
+ R51 selector is gated to g[data-node] ancestors so
6271
+ this hub-internal circle is invisible to the probe
6272
+ regardless. R253 stroke transition + pointerEvents:
6273
+ none preserved. data-topo-hub-hover-ring-stroke-width
6274
+ attr exposes the value for tests. */}
4989
6275
  <circle
4990
6276
  cx={cx} cy={cy}
4991
6277
  r={hoveredHub ? 17 : 14}
4992
6278
  fill="none"
4993
6279
  stroke={isLight ? '#059669' : '#10b981'}
4994
- strokeWidth="1.5"
4995
- opacity={hoveredHub ? (isLight ? 0.85 : 0.7) : 0}
6280
+ strokeWidth="1.75"
6281
+ /* Round 370 / Loop: hub hover-ring cyber opacity 0.7 →
6282
+ 0.8. R177 designed the hub hover-ring at opacity-0 →
6283
+ 0.85 (light) / 0 → 0.7 (cyber). The 15 % gap between
6284
+ the two themes meant cyber-theme operators got a
6285
+ noticeably softer hover cue than light-theme users
6286
+ against backgrounds that should equalise (dark bg
6287
+ needs more luminance to read as 'on'). R370 bumps
6288
+ cyber 0.7 → 0.8, narrowing the theme gap to 5 % —
6289
+ sibling theme-consistency polish to R251 edge badge
6290
+ fill/opacity (cyber 0.82 / light 0.95) and R246/R247
6291
+ panel transition families. Light theme 0.85 stays
6292
+ as is (already in the legibility band). data-topo-
6293
+ hub-hover-ring-opacity attr exposes the value for
6294
+ tests. */
6295
+ opacity={hoveredHub ? (isLight ? 0.85 : 0.8) : 0}
4996
6296
  data-topo-hub-hover-ring
4997
6297
  data-topo-hub-hover-ring-radius={hoveredHub ? 17 : 14}
6298
+ data-topo-hub-hover-ring-stroke-width="1.75"
6299
+ data-topo-hub-hover-ring-opacity={hoveredHub ? (isLight ? 0.85 : 0.8) : 0}
4998
6300
  /* Round 253 / Loop: hub hover ring also gets stroke
4999
6301
  transition for theme toggle (cyber #10b981 ↔ light
5000
6302
  #059669). The opacity + r transitions stay for hover
@@ -5423,14 +6725,51 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
5423
6725
  keyTimes="0;0.5;1"
5424
6726
  keySplines="0.42 0 0.58 1;0.42 0 0.58 1"
5425
6727
  />
6728
+ {/* Round 409 / Loop: active-node pulse peak
6729
+ opacity lift — cyber 0.18 → 0.20 / light
6730
+ 0.12 → 0.14. Theme-consistency / canvas-
6731
+ presence family 9th anchor. R243 family
6732
+ rhythm preserved.
6733
+ Round 413 / Loop: trough lift mirrors R409
6734
+ peak — cyber 0.04 → 0.05 / light 0.02 →
6735
+ 0.03. Stale-state legibility lift family
6736
+ 8th anchor — pairs with R404 (hub-halo
6737
+ cyber trough 0.08 → 0.10) and R405 (light
6738
+ trough 0.32 → 0.34). The per-node breath's
6739
+ low-point now reads slightly above the
6740
+ "nearly gone" zone while preserving the
6741
+ breath amplitude (cyber Δ 0.16 vs Δ pre-
6742
+ R409+R413 of 0.14; light Δ 0.11 vs 0.10).
6743
+ Both peak (R409) AND trough (R413) lift
6744
+ together so the active-pulse signal stays
6745
+ confidently present at both ends of its
6746
+ 2.4s cycle.
6747
+ Stale-state legibility lift family (8):
6748
+ R317 subordinate-text gray-500→400
6749
+ R358 freshness floor 0.25→0.30
6750
+ R372 minimap offline-dot 0.5→0.6
6751
+ R404 hub-halo cyber trough 0.08→0.10
6752
+ R405 hub-halo light trough 0.32→0.34
6753
+ R406 edge freshness floor 0.35→0.40
6754
+ R407 node halo offline opacity (cyber+light)
6755
+ R413 active-node pulse trough (this round)
6756
+ cyber 0.04 → 0.05
6757
+ light 0.02 → 0.03
6758
+ R243 always-mount opacity-gate + R243
6759
+ ease-in-out keySplines + r animation
6760
+ (radius+8 ↔ radius+22) preserved.
6761
+ data-node-pulse-peak + new -pulse-trough
6762
+ attrs expose resolved per-theme values. */}
5426
6763
  <animate
5427
6764
  attributeName="opacity"
5428
- values={isLight ? '0.12;0.02;0.12' : '0.18;0.04;0.18'}
6765
+ values={isLight ? '0.14;0.03;0.14' : '0.20;0.05;0.20'}
5429
6766
  dur="2.4s"
5430
6767
  repeatCount="indefinite"
5431
6768
  calcMode="spline"
5432
6769
  keyTimes="0;0.5;1"
5433
6770
  keySplines="0.42 0 0.58 1;0.42 0 0.58 1"
6771
+ data-node-pulse-peak={isLight ? '0.14' : '0.20'}
6772
+ data-node-pulse-trough={isLight ? '0.03' : '0.05'}
5434
6773
  />
5435
6774
  </circle>
5436
6775
  </g>
@@ -5475,12 +6814,107 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
5475
6814
  parent stays for status flips. data-node-halo-
5476
6815
  breath-offset surfaces the chosen offset for
5477
6816
  test introspection. */}
6817
+ {/* Round 407 / Loop: offline node halo opacity lift —
6818
+ cyber 0.25 → 0.30 and light 0.4 → 0.45. Pre-R407
6819
+ offline node halos faded to α=0.25 cyber (75 %
6820
+ dim) / α=0.4 light. On the dark canvas the 0.25
6821
+ halo read as "nearly gone" — exactly the
6822
+ legibility floor R404/R405 just lifted on the
6823
+ hub-halo and R372 lifted on minimap offline dots.
6824
+ R407 closes the same family at the per-node halo
6825
+ surface: +0.05 lift on both themes so offline
6826
+ anchors stay legibly present without crossing into
6827
+ "could be online" territory (online cyber 0.65 /
6828
+ light 0.85 unchanged — the 0.30/0.65 cyber ratio
6829
+ still gives 2.17× contrast for online/offline).
6830
+ Stale-state legibility lift family (7 anchors now):
6831
+ R317 subordinate-text gray-500 → gray-400
6832
+ R358 freshness floor 0.25 → 0.30
6833
+ R372 minimap offline-dot 0.5 → 0.6
6834
+ R404 hub-halo cyber trough 0.08 → 0.10
6835
+ R405 hub-halo light trough 0.32 → 0.34
6836
+ R406 edge freshness floor 0.35 → 0.40
6837
+ R407 node halo offline opacity (this round)
6838
+ cyber 0.25 → 0.30
6839
+ light 0.4 → 0.45
6840
+ R278 retired-breath gate + R12 status.halo color
6841
+ + R226 phase stagger code-path preserved (the
6842
+ breath stays disabled per Vincent's R278 ask;
6843
+ only the BASE opacity floor shifts here). transi-
6844
+ tion list ('fill,opacity' 300ms ease-out) unchanged.
6845
+ data-node-halo-offline-opacity attr exposes the
6846
+ resolved value for tests. */}
6847
+ {(() => {
6848
+ /* Round 440 / Loop: node halo opacity hover lift —
6849
+ lifts toward full on the matched node. Pure paint
6850
+ axis: rest values unchanged for un-hovered halos,
6851
+ hover state lifts the matched halo's alpha by
6852
+ +0.15 on each tier:
6853
+ online cyber 0.65 → 0.80
6854
+ online light 0.85 → 1.00 (capped)
6855
+ offline cyber 0.30 → 0.45
6856
+ offline light 0.45 → 0.60
6857
+ Same paint-only mental model as R430 hub-spoke
6858
+ opacity lift + R429 label-card body opacity lift,
6859
+ now at the per-node halo scope. No geometry
6860
+ change so R51 sentinels stay safe and the overlap-
6861
+ test invariant is unchanged (test runs at rest).
6862
+ Closes a chroma/presence axis on the per-node
6863
+ hover signature alongside the 12-layer cue stack
6864
+ (R26/R217/R142/R427/R428/R429 card + R430/R435/
6865
+ R436/R437/R94 link + R438 ring). R407 offline
6866
+ halo opacity floor (cyber 0.30 / light 0.45) is
6867
+ the rest branch unchanged. Existing transition-
6868
+ [fill,opacity] duration-300 className handles
6869
+ the easing. data-node-halo-hovered exposes the
6870
+ gate; data-node-halo-resolved-opacity exposes
6871
+ the four-state resolved value for tests. */
6872
+ const isHaloHovered = !reducedMotion && hoveredAlias === session.alias;
6873
+ /* Round 456 / Loop: light-theme offline node halo
6874
+ rest opacity 0.45 → 0.50. Stale-state legibility
6875
+ lift family extension (10th anchor) at the per-
6876
+ node halo light-theme scope:
6877
+ R317 subordinate-text gray-500 → gray-400
6878
+ R358 freshness floor 0.25 → 0.30
6879
+ R372 minimap offline-dot 0.5 → 0.6
6880
+ R404 hub-halo cyber trough 0.08 → 0.10
6881
+ R405 hub-halo light trough 0.32 → 0.34
6882
+ R406 edge freshness floor 0.35 → 0.40
6883
+ R407 node halo offline opacity
6884
+ cyber 0.25 → 0.30
6885
+ light 0.4 → 0.45
6886
+ R419 hub-spoke idle 0.45 → 0.50
6887
+ R452 dense alias rest 0.9 → 0.95
6888
+ R456 node halo offline LIGHT 0.45 → 0.50 ← this round
6889
+ Pre-R456 light-theme offline halo at 0.45 sat at
6890
+ the upper end of "near-floor" but read as soft-
6891
+ focus on the lighter canvas; +0.05 (~11 % opacity
6892
+ gain) lifts it to 0.50 — the midpoint between
6893
+ R407 rest 0.45 and R440 hover 0.60 — closing the
6894
+ gap so offline halos read more confidently as
6895
+ present-but-stale anchors. Cyber theme stays at
6896
+ R407's 0.30 (cyber backdrop is dark; the cyber
6897
+ offline halo against #080814 contains a stronger
6898
+ contrast envelope than light, so doesn't need
6899
+ the same lift). R440 hover 0.45→0.60 light + R12
6900
+ status.halo color + R407 transition list all
6901
+ preserved. */
6902
+ const haloOpacity = (() => {
6903
+ if (isOnline) {
6904
+ return isLight ? (isHaloHovered ? 1 : 0.85) : (isHaloHovered ? 0.80 : 0.65);
6905
+ }
6906
+ return isLight ? (isHaloHovered ? 0.60 : 0.50) : (isHaloHovered ? 0.45 : 0.30);
6907
+ })();
6908
+ return (
5478
6909
  <circle
5479
6910
  cx={pos.x}
5480
6911
  cy={pos.y}
5481
6912
  r={radius + 8}
5482
6913
  fill={status.halo}
5483
- opacity={isOnline ? (isLight ? 0.85 : 0.65) : (isLight ? 0.4 : 0.25)}
6914
+ opacity={haloOpacity}
6915
+ data-node-halo-offline-opacity={isOnline ? undefined : (isLight ? 0.45 : 0.30)}
6916
+ data-node-halo-hovered={isHaloHovered ? 'true' : 'false'}
6917
+ data-node-halo-resolved-opacity={haloOpacity}
5484
6918
  className="transition-[fill,opacity] duration-300 ease-out"
5485
6919
  data-node-halo-breath={!reducedMotion && session.status === 'working' ? 'on' : 'off'}
5486
6920
  data-node-halo-breath-offset={
@@ -5539,6 +6973,8 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
5539
6973
  />
5540
6974
  )}
5541
6975
  </circle>
6976
+ );
6977
+ })()}
5542
6978
  {/* Round 111 / Loop: edge-endpoint emphasis ring. R49
5543
6979
  already keeps endpoint nodes at opacity 1 while
5544
6980
  others dim when an edge is hovered, but the
@@ -5595,11 +7031,37 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
5595
7031
  ring stroke-width). data-edge-endpoint-ring-
5596
7032
  stroke-width attr surfaces the chosen value for
5597
7033
  test introspection. */
7034
+ /* Round 442 / Loop: endpoint emphasis ring radius
7035
+ hover lift — r=radius+7 → radius+8 on isEndpoint,
7036
+ closing a 3-axis hover-elevation parity at endpoint
7037
+ ring scope (r + sw + opacity):
7038
+ opacity R182 0 → 0.85/0.9
7039
+ sw R233 1.6 → 2.4
7040
+ r R442 +7 → +8 ← this round
7041
+ Mirrors the 3-axis trios already established at
7042
+ hub hover-ring (R177/R370/R385) and edge badge
7043
+ (R164/R394/R395). Pre-R442 the endpoint ring
7044
+ faded in + thickened on edge-hover but its radius
7045
+ stayed locked at radius+7 — only the paint/weight
7046
+ axes lifted while the GEOMETRY stayed unchanged.
7047
+ +1px (~radius+7 to radius+8) gives a subtle outward
7048
+ pulse on hover without crowding the status ring
7049
+ (which sits at radius from R438 sw3.5 hover) or
7050
+ the halo (radius+8 from R440 opacity hover — the
7051
+ endpoint ring sits at the SAME radius as the halo
7052
+ but with stroke=cyan vs fill=status.halo so they
7053
+ don't visually collide). The transition list
7054
+ extends to include 'r 180ms ease-out' so the new
7055
+ axis eases under the same R233 cadence. SVG `r`
7056
+ on a <circle> uses CSS-property syntax for inter-
7057
+ polation (same idiom R197/R198 used on the
7058
+ legend swatch). data-edge-endpoint-ring-radius
7059
+ attr exposes the resolved value for tests. */
7060
+ const endpointR = isEndpoint ? radius + 8 : radius + 7;
5598
7061
  return (
5599
7062
  <circle
5600
7063
  cx={pos.x}
5601
7064
  cy={pos.y}
5602
- r={radius + 7}
5603
7065
  fill="none"
5604
7066
  stroke={pal.flowEdge}
5605
7067
  strokeWidth={isEndpoint ? 2.4 : 1.6}
@@ -5607,7 +7069,12 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
5607
7069
  data-edge-endpoint-ring
5608
7070
  data-edge-endpoint-active={isEndpoint ? 'true' : 'false'}
5609
7071
  data-edge-endpoint-ring-stroke-width={isEndpoint ? 2.4 : 1.6}
5610
- style={{ pointerEvents: 'none', transition: 'opacity 180ms ease-out, stroke-width 180ms ease-out' }}
7072
+ data-edge-endpoint-ring-radius={endpointR}
7073
+ style={{
7074
+ pointerEvents: 'none',
7075
+ r: `${endpointR}px`,
7076
+ transition: 'opacity 180ms ease-out, stroke-width 180ms ease-out, r 180ms ease-out',
7077
+ } as React.CSSProperties}
5611
7078
  />
5612
7079
  );
5613
7080
  })()}
@@ -5632,20 +7099,62 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
5632
7099
  stroke-width="3"/"1.5" still works against the
5633
7100
  DOM attribute value (React-rendered, not
5634
7101
  interpolated). */}
5635
- <circle
5636
- cx={pos.x}
5637
- cy={pos.y}
5638
- r={radius}
5639
- fill={isOnline ? pal.nodeFill.online : pal.nodeFill.offline}
5640
- stroke={status.primary}
5641
- strokeWidth={isOnline ? 3 : 1.5}
5642
- strokeDasharray={isOnline ? 'none' : '5 5'}
5643
- filter={isOnline && !isLight ? 'url(#topo-glow)' : undefined}
5644
- data-node-status-ring={status.label}
5645
- style={{
5646
- transition: 'fill 300ms ease-out, stroke 300ms ease-out, stroke-width 300ms ease-out',
5647
- }}
5648
- />
7102
+ {(() => {
7103
+ /* Round 438 / Loop: status ring strokeWidth hover lift —
7104
+ when hoveredAlias matches, the node's status ring
7105
+ thickens by +0.5: online 3 → 3.5, offline 1.5 → 2.
7106
+ Same absolute delta as R435 hub-spoke (idle 1→1.25
7107
+ used Δ +0.25 because rest base was thinner; status
7108
+ ring's heavier rest values 1.5/3 need a bigger
7109
+ +0.5 to register as visible thickening).
7110
+ Status-ring axis joins the node-hover cue stack
7111
+ (now 9 layers including link surfaces):
7112
+ R26 group translateY -2px per-node geometry
7113
+ R217 stroke tint legendAccent per-node card
7114
+ R142 drop-shadow boost per-node card
7115
+ R427 alias letter-spacing per-node text
7116
+ R428 sub-text letter-spacing per-node text
7117
+ R429 body opacity 0.94 → 1.0 per-node card
7118
+ R430 hub-spoke α+ link to hub (paint)
7119
+ R435 hub-spoke sw+ link to hub (geo)
7120
+ R94 edge α 1.7× inter-node link (paint)
7121
+ R436 edge sw 1.15× inter-node link (geo)
7122
+ R437 flow-rail sw 1 → 1.5 edge paint-layer
7123
+ R438 status-ring sw +0.5 ring geometry ← this round
7124
+ R51 sentinel safety: rest values 3 / 1.5 unchanged
7125
+ so the overlap-test selector `circle[stroke-width=
7126
+ "3"]` / `circle[stroke-width="1.5"]` inside
7127
+ g[data-node] still matches at rest. Hover values
7128
+ 3.5 / 2 are not in the reserved {1.5, 3} set so
7129
+ the selector wouldn't match them anyway; but the
7130
+ test runs WITHOUT hover so this never matters
7131
+ in practice. R167 stroke-width 300ms transition
7132
+ already in the style list eases the lift for
7133
+ free. data-node-status-ring-hovered exposes the
7134
+ gate for tests. */
7135
+ const isRingHovered = !reducedMotion && hoveredAlias === session.alias;
7136
+ const ringStrokeWidth = isOnline
7137
+ ? (isRingHovered ? 3.5 : 3)
7138
+ : (isRingHovered ? 2 : 1.5);
7139
+ return (
7140
+ <circle
7141
+ cx={pos.x}
7142
+ cy={pos.y}
7143
+ r={radius}
7144
+ fill={isOnline ? pal.nodeFill.online : pal.nodeFill.offline}
7145
+ stroke={status.primary}
7146
+ strokeWidth={ringStrokeWidth}
7147
+ strokeDasharray={isOnline ? 'none' : '5 5'}
7148
+ filter={isOnline && !isLight ? 'url(#topo-glow)' : undefined}
7149
+ data-node-status-ring={status.label}
7150
+ data-node-status-ring-hovered={isRingHovered ? 'true' : 'false'}
7151
+ data-node-status-ring-stroke-width={ringStrokeWidth}
7152
+ style={{
7153
+ transition: 'fill 300ms ease-out, stroke 300ms ease-out, stroke-width 300ms ease-out',
7154
+ }}
7155
+ />
7156
+ );
7157
+ })()}
5649
7158
  {/* v0.10.0 Hero 1+2 / §3.F server-health node-ring tint.
5650
7159
  When the host server this agent runs on is in the
5651
7160
  `red` tier (CPU/Mem/Disk worst-of ≥ 85% per
@@ -5825,8 +7334,37 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
5825
7334
  transition: 'r 150ms ease-out, stroke-width 150ms ease-out',
5826
7335
  } as React.CSSProperties}
5827
7336
  />
7337
+ {/* Round 443 / Loop: runtime badge inner-icon
7338
+ strokeWidth lift on node hover — 2.4 → 2.8 on
7339
+ isNodeActive. Pre-R443 the outer badge ring
7340
+ lifted (R208 r + sw both grow on hover) but
7341
+ the inner icon path stayed locked at sw=2.4.
7342
+ The two layers of the runtime badge were
7343
+ out of phase: ring thickened, icon stayed
7344
+ thin. R443 closes the 2-axis hover signature
7345
+ on the badge so both ring and icon lift
7346
+ together. +0.4 absolute delta matches the
7347
+ R208 ring's +0.5 sw delta (badge ring 1.5 →
7348
+ 2.0 absolute), proportional to the icon's
7349
+ heavier base of 2.4. Pure paint axis;
7350
+ strokeLinecap='round' + strokeLinejoin='round'
7351
+ preserved. transition list extends to include
7352
+ 'stroke-width 150ms ease-out' matching R208
7353
+ outer-ring cadence. data-runtime-badge-icon
7354
+ + -active attrs exposed for tests. */}
5828
7355
  <g transform={`translate(${bx - icon / 2} ${by - icon / 2}) scale(${icon / 24})`}>
5829
- <path d={rt.iconPath} fill="none" stroke={rt.color} strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" />
7356
+ <path
7357
+ d={rt.iconPath}
7358
+ fill="none"
7359
+ stroke={rt.color}
7360
+ strokeWidth={isNodeActive ? '2.8' : '2.4'}
7361
+ strokeLinecap="round"
7362
+ strokeLinejoin="round"
7363
+ data-runtime-badge-icon={session.alias}
7364
+ data-runtime-badge-icon-active={isNodeActive ? 'true' : 'false'}
7365
+ data-runtime-badge-icon-stroke-width={isNodeActive ? '2.8' : '2.4'}
7366
+ style={{ transition: 'stroke-width 150ms ease-out' }}
7367
+ />
5830
7368
  </g>
5831
7369
  </g>
5832
7370
  );
@@ -5969,14 +7507,69 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
5969
7507
  220ms cadence the existing filter/stroke
5970
7508
  pair uses — coordinated 4-property easing
5971
7509
  across the card. */}
7510
+ {/* Round 411 / Loop: node label card rx 6 → 8.
7511
+ Pre-R411 the per-node label card painted at
7512
+ rx=6, sitting one tier BELOW the R332/R375/
7513
+ R376 compact-chrome tier (rx=8). Inside the
7514
+ corner-radius cascade family the cards used
7515
+ to be the only "smaller" tier — but the
7516
+ label card is a content-bearing surface
7517
+ (alias + sub text + ring), not a sub-
7518
+ element decoration. R411 lifts rx=6 → 8
7519
+ to align with the compact-chrome / segmented-
7520
+ control tier so all "compact card" surfaces
7521
+ read with the same corner radius.
7522
+ Corner-radius cascade (8 anchors now):
7523
+ R330 canvas rx 12 (root)
7524
+ R331 panels rx 10 (recent-signal, legend)
7525
+ R332 minimap container rx 8 (compact chrome)
7526
+ R375 Layout-toggle rx 8 (segmented control)
7527
+ R376 nodeSize/zoom rx 8 (segmented control)
7528
+ R390 hover-detail rx 10 (panel)
7529
+ R393 minimap viewport rx 2 (sub-element)
7530
+ R411 node label card rx 6 → 8 (compact card, this round)
7531
+ Pure paint — rx grows the corner curve
7532
+ inward without changing the card's outer
7533
+ cardW × cardH bbox (cardW=92/cardH=22 for
7534
+ standard nodes per R23 / R187 sizing). R217
7535
+ hover-stroke cyan tint + R142 drop-shadow
7536
+ + R246 fill+opacity 220ms transition list
7537
+ + R211 alias/sub text-fill ease all
7538
+ preserved. data-node-label-card-rx attr
7539
+ exposes the value for tests. */}
7540
+ {/* Round 429 / Loop: node label-card body opacity
7541
+ 0.94 → 1.0 on hover (cyber theme). Sibling
7542
+ treatment to R348 panel rect opacity lift —
7543
+ 0.92 → 0.97 cyber / 0.97 → 1.0 light at the
7544
+ panel scope. Pre-R429 the cyber theme card
7545
+ sat at 0.94 always; on hover R217 tinted the
7546
+ stroke + R142 grew the drop-shadow + R26
7547
+ lifted the group + R427/R428 spaced the text
7548
+ but the rect itself never solidified —
7549
+ the card glowed brighter through the
7550
+ shadow but the body alpha gap (6 pct) stayed
7551
+ fixed. R429 lifts the body to full alpha on
7552
+ hover so the card reads as a confidently
7553
+ present surface under the cursor (matching
7554
+ the panel-pair pattern). Light theme stays
7555
+ at 1.0 in both states (already maxed). R246
7556
+ transition list already covers opacity 220ms
7557
+ so the lift eases for free. R217 stroke tint
7558
+ + R142 drop-shadow + R211 fill ease all
7559
+ preserved (additive opacity branch only). */}
5972
7560
  <rect
5973
- x={-cardW / 2} y={cardTopY} width={cardW} height={cardH} rx="6"
7561
+ x={-cardW / 2} y={cardTopY} width={cardW} height={cardH} rx="8"
5974
7562
  fill={pal.labelBox.fill}
5975
7563
  stroke={!reducedMotion && hoveredAlias === session.alias
5976
7564
  ? pal.legendAccent
5977
7565
  : pal.labelBox.stroke}
5978
- opacity={isLight ? 1 : 0.94}
7566
+ opacity={
7567
+ !reducedMotion && hoveredAlias === session.alias
7568
+ ? 1
7569
+ : (isLight ? 1 : 0.94)
7570
+ }
5979
7571
  data-node-label-card={session.alias}
7572
+ data-node-label-card-rx="8"
5980
7573
  data-node-label-card-elevation={
5981
7574
  !reducedMotion && hoveredAlias === session.alias ? 'hover' : 'idle'
5982
7575
  }
@@ -6020,25 +7613,94 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6020
7613
  transition list extends 'letter-spacing
6021
7614
  200ms ease-out' so it eases alongside the
6022
7615
  existing 300ms fill transition. */}
7616
+ {/* Round 427 / Loop: extend the node alias label
7617
+ letter-spacing family to a 3-tier scale —
7618
+ rest 0px → hover 0.3px → chat-target 0.5px.
7619
+ Pre-R427 the alias text shifted only when
7620
+ chat was actively pinned (R305); pure node-
7621
+ hover left the text dead-typographic while
7622
+ the surrounding card lifted (R26 translateY
7623
+ + R242 stroke + filter cues). R427 adds the
7624
+ missing typographic axis to the hover gesture
7625
+ so the alias text rises with the card.
7626
+ The chat-target tier still wins (0.5 > 0.3)
7627
+ so the pin signature stays at the top of the
7628
+ scale — hover is the mid tier between rest
7629
+ and chat-target.
7630
+ Hover-letter-spacing family extension:
7631
+ R344 chip count digit
7632
+ R345 panel title (R423 sibling)
7633
+ R347 active-links chip
7634
+ R351 vendor chip
7635
+ R420 zoom-level chip
7636
+ R427 node alias text (this round)
7637
+ R211 fill 300ms + R305 letter-spacing 200ms
7638
+ transition list preserved; only the
7639
+ conditional gets a middle case. */}
6023
7640
  <text
6024
7641
  x="0" y="1" textAnchor="middle"
6025
7642
  fill={status.text}
6026
7643
  fontSize={aliasFs} fontFamily="monospace" fontWeight="700"
6027
7644
  data-node-alias-text={session.alias}
6028
7645
  data-node-alias-chat-target={chatAlias === session.alias ? 'true' : 'false'}
7646
+ data-node-alias-hovered={hoveredAlias === session.alias ? 'true' : 'false'}
6029
7647
  style={{
6030
7648
  transition: 'fill 300ms ease-out, letter-spacing 200ms ease-out',
6031
- letterSpacing: chatAlias === session.alias ? '0.5px' : '0px',
7649
+ letterSpacing:
7650
+ chatAlias === session.alias ? '0.5px' :
7651
+ hoveredAlias === session.alias ? '0.3px' : '0px',
6032
7652
  }}
6033
7653
  >
6034
7654
  {truncate(session.alias, fullMax)}
6035
7655
  </text>
7656
+ {/* Round 428 / Loop: node sub-text (status label
7657
+ line beneath the alias) adopts hover letter-
7658
+ spacing tween 0 → 0.2px on hoveredAlias.
7659
+ Sibling treatment to R427 alias-text hover
7660
+ tween (0 → 0.3) — the alias is the primary
7661
+ identity (top-tier kerning 0.3), the sub-text
7662
+ is the secondary status line (one tier lower
7663
+ at 0.2). Now both lines of the label card
7664
+ telegraph hover typographically as one unit,
7665
+ matching the R26 card lift + R242 stroke
7666
+ tint + R975 filter cues. Subtler delta on
7667
+ the sub-text (0.2 vs alias 0.3) preserves
7668
+ the alias > status visual hierarchy at the
7669
+ hover scope. R211 fill 300ms transition
7670
+ preserved (additive letter-spacing branch
7671
+ + appended 'letter-spacing 200ms ease-out'). */}
7672
+ {/* Round 448 / Loop: node sub-text fontWeight
7673
+ 400 → 500 (font-medium). Sibling to R363
7674
+ (recent-row text fw 400→500) + R364 (legend-
7675
+ row label fw 400→500) — same "small mono
7676
+ text at fontSize=9-11 needs 500-tier weight
7677
+ for legibility" pattern, now applied to the
7678
+ per-node sub-text line. At fontSize=8-9
7679
+ monospace against the label-card chrome
7680
+ (pal.labelBox.fill cyber #020617 / light
7681
+ #ffffff), the default fw=400 sits at the
7682
+ legibility floor; fw=500 (font-medium) lifts
7683
+ it into a clearly readable band without
7684
+ changing geometry. R211 fill 300ms +
7685
+ R428 letter-spacing 0→0.2 hover + R427
7686
+ alias-text + R429 body opacity all preserved.
7687
+ Pure typography lift; no layout shift; the
7688
+ alias-text fw=700 (R427) still wins so the
7689
+ alias > status hierarchy holds at the type
7690
+ level. data-node-sub-text-font-weight attr
7691
+ exposes the value for tests. */}
6036
7692
  <text
6037
7693
  x="0" y={subY} textAnchor="middle"
6038
7694
  fill={status.primary}
6039
7695
  fontSize={subFs} fontFamily="monospace"
7696
+ fontWeight="500"
6040
7697
  data-node-sub-text={session.alias}
6041
- style={{ transition: 'fill 300ms ease-out' }}
7698
+ data-node-sub-text-hovered={hoveredAlias === session.alias ? 'true' : 'false'}
7699
+ data-node-sub-text-font-weight="500"
7700
+ style={{
7701
+ transition: 'fill 300ms ease-out, letter-spacing 200ms ease-out',
7702
+ letterSpacing: hoveredAlias === session.alias ? '0.2px' : '0px',
7703
+ }}
6042
7704
  >
6043
7705
  {status.label}{isOnline && sseCountFor != null ? ` sse:${sseCountFor}` : ''}
6044
7706
  </text>
@@ -6061,6 +7723,26 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6061
7723
  // CSS pseudo-class; only the transition-property
6062
7724
  // moves to inline. Big fleets benefit most — this is
6063
7725
  // the path users see when their dashboard is busiest.
7726
+ /* Round 452 / Loop: dense plain-text alias rest
7727
+ opacity 0.9 → 0.95. Closes the alpha gap on the
7728
+ dense fleet's per-node label, sibling to R449
7729
+ legend-count-active 0.95→1.0 and R450 minimap
7730
+ viewport rest 0.9→0.95 — same "close the
7731
+ active-presence alpha gap" idiom applied here
7732
+ to the dense-mode alias text at fontSize=9-10
7733
+ monospace. Pre-R452 dense aliases at α=0.9 sat
7734
+ just below full alpha; for un-hovered nodes in
7735
+ a busy >16-node fleet this is the only label
7736
+ readable, so the 10% alpha gap added a subtle
7737
+ "soft-focused chrome" feel where the labels
7738
+ should read as definitive. +0.05 lift makes
7739
+ them confidently present without erasing the
7740
+ status.text + R110 stroke halo + paintOrder
7741
+ layering. R26 group-hover translate + R212
7742
+ fill 300ms transition + R110 stroke=container-
7743
+ Bg halo all preserved. data-node-dense-alias-
7744
+ text-opacity attr exposes the resolved value
7745
+ for tests. */
6064
7746
  <text
6065
7747
  x={pos.x}
6066
7748
  y={pos.y + radius + denseDrop}
@@ -6069,9 +7751,10 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6069
7751
  fontSize={denseFs}
6070
7752
  fontFamily="monospace"
6071
7753
  fontWeight="700"
6072
- opacity={0.9}
7754
+ opacity={0.95}
6073
7755
  className="group-hover:-translate-y-[1.5px]"
6074
7756
  data-node-dense-alias-text={session.alias}
7757
+ data-node-dense-alias-text-opacity="0.95"
6075
7758
  style={{
6076
7759
  pointerEvents: 'none',
6077
7760
  paintOrder: 'stroke',
@@ -6111,26 +7794,122 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6111
7794
  const detailY = pos.y - detailH / 2;
6112
7795
  return (
6113
7796
  <g transform={`translate(${detailX}, ${detailY})`} data-topo-hover-detail={session.alias} style={{ pointerEvents: 'none' }}>
7797
+ {/* Round 387 / Loop: hover-detail panel cyber backdrop
7798
+ opacity 0.94 → 0.97. The hover-detail card is
7799
+ ALWAYS rendered in active-hover context (it IS
7800
+ the hover product), so it should carry the
7801
+ same backdrop weight as the R348 recent-signal /
7802
+ legend panel HOVER state (which lifts 0.92 →
7803
+ 0.97 cyber). Pre-R387 the card sat at 0.94
7804
+ cyber, leaving a 0.03 alpha gap against the
7805
+ R348 panel-hover state — small but visible
7806
+ when the hover-detail floats next to a hovered
7807
+ recent-signal panel. R387 unifies them at 0.97
7808
+ so all active-hover panels paint with the same
7809
+ confident backdrop opacity in cyber. Light
7810
+ stays at 0.98 (already at the strong end —
7811
+ R348 light also stays at 0.97/0.98 max).
7812
+ Theme-consistency / canvas-presence polish
7813
+ family (5th anchor):
7814
+ R370 hub hover-ring opacity 0.7 → 0.8 cyber
7815
+ R371 edge-badge rest opacity 0.82 → 0.85 cyber
7816
+ R372 minimap offline-dot opacity 0.5 → 0.6
7817
+ R386 hub-highlight idle opacity 0.9 → 0.95
7818
+ R387 hover-detail panel opacity 0.94 → 0.97 cyber (this round)
7819
+ data-topo-hover-detail-opacity attr exposes
7820
+ the resolved value for tests. R348 drop-shadow
7821
+ + rx=8 + stroke=pal.legendAccent + fill=pal.
7822
+ labelBox.fill all preserved. */}
7823
+ {/* Round 390 / Loop: hover-detail card rx 8 → 10.
7824
+ Corner-radius cascade family — the hover-detail
7825
+ card is a panel-tier surface (192×88 floating
7826
+ info card with drop-shadow + stroke), so its
7827
+ corner radius should match the R331 panel tier
7828
+ (rx=10) used by the recent-signal and legend
7829
+ panels. Pre-R390 it shared rx=8 with the R332
7830
+ minimap and R375/R376 segmented-control tier
7831
+ (Layout-toggle, nodeSize, zoom wrappers),
7832
+ which is the "compact chrome control" tier —
7833
+ a tier mismatch for a content-bearing panel.
7834
+ Corner-radius cascade (6 anchors now):
7835
+ R330 canvas rx 12 (root)
7836
+ R331 panels rx 10 (recent-signal, legend)
7837
+ R332 minimap rx 8 (compact chrome)
7838
+ R375 Layout-toggle rx 8 (segmented control)
7839
+ R376 nodeSize/zoom rx 8 (segmented control)
7840
+ R390 hover-detail rx 10 (panel — this round)
7841
+ Pure paint change; no layout shift (rx grows
7842
+ the corner curve INWARD without changing the
7843
+ card's outer bbox). data-topo-hover-detail-
7844
+ rx attr exposes the resolved value for tests.
7845
+ R348 drop-shadow + stroke + R387 opacity all
7846
+ preserved. */}
6114
7847
  <rect
6115
- x="0" y="0" width={detailW} height={detailH} rx="8"
7848
+ x="0" y="0" width={detailW} height={detailH} rx="10"
6116
7849
  fill={pal.labelBox.fill}
6117
7850
  stroke={pal.legendAccent}
6118
- opacity={isLight ? 0.98 : 0.94}
7851
+ opacity={isLight ? 0.98 : 0.97}
7852
+ data-topo-hover-detail-opacity={isLight ? 0.98 : 0.97}
7853
+ data-topo-hover-detail-rx="10"
6119
7854
  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))' }}
6120
7855
  />
6121
7856
  <text x="10" y="16" fontSize="9" fontFamily="monospace" fill={pal.legendAccent} fontWeight="700">
6122
7857
  {v.id !== 'unknown' ? v.label : '—'}
6123
7858
  </text>
6124
- <text x="10" y="32" fontSize="10" fontFamily="monospace" fill={pal.legendHeadline}>
7859
+ {/* Round 389 / Loop: hover-detail model line (y=32)
7860
+ fontWeight 400 → 600. R388 lifted body lines
7861
+ (runtime/host/task at fontSize=9) to fw=500;
7862
+ R389 closes the typography hierarchy by giving
7863
+ the model name (the dominant subhead text in
7864
+ the card) its own weight tier. Three-tier
7865
+ ladder now reads cleanly:
7866
+ vendor fontSize=9 fw=700 (label badge)
7867
+ model fontSize=10 fw=600 (subhead — this round)
7868
+ body 3× fontSize=9 fw=500 (R388)
7869
+ One tier step per dimension (size + weight)
7870
+ between adjacent levels — classic editorial
7871
+ hierarchy idiom adapted to a 192×88 SVG card.
7872
+ Sibling to the chip-internal-hierarchy arc
7873
+ (R333-R341/R362/R369) which uses fw=600/500
7874
+ for digit/unit pairs; R389 applies the same
7875
+ fw=600 to a content-bearing identity line.
7876
+ data-topo-hover-detail-model-fw attr exposes
7877
+ the resolved value for tests. pal.legendHeadline
7878
+ fill preserved (R389 doesn't touch color). */}
7879
+ <text x="10" y="32" fontSize="10" fontFamily="monospace" fontWeight="600" fill={pal.legendHeadline} data-topo-hover-detail-model-fw="600">
6125
7880
  {session.model || 'model · pending'}
6126
7881
  </text>
6127
- <text x="10" y="48" fontSize="9" fontFamily="monospace" fill={pal.legendText}>
7882
+ {/* Round 388 / Loop: hover-detail body lines (the
7883
+ three fontSize=9 lines: runtime, host, task)
7884
+ gain fontWeight=500. Small-text fw lift family
7885
+ (6th anchor) — fontSize 9-10 px text reads
7886
+ consistently bolder at fw=500 than at the
7887
+ default 400 weight at small sizes, especially
7888
+ on the cyber-theme backdrop where stroke-
7889
+ rendering is the limiting factor.
7890
+ Sibling lifts in this family:
7891
+ R363 recent-row alias text 400 → 500
7892
+ R364 legend-row label 400 → 500
7893
+ R366 group-label count tspan 400 → 500
7894
+ R368 +N more flows footer 400 → 500
7895
+ R373 pressure-bar kicker (font-medium)
7896
+ R388 hover-detail body lines 400 → 500 (this round)
7897
+ Tier structure preserved:
7898
+ y=16 vendor (fw=700, headline)
7899
+ y=32 model (fontSize=10, subhead by size)
7900
+ y=48 runtime / y=64 host / y=80 task (body, now fw=500)
7901
+ The y=80 task line keeps opacity=0.7 so its
7902
+ caption-tier identity stays distinct from the
7903
+ y=48 / y=64 body lines despite shared fw.
7904
+ data-topo-hover-detail-body-fw attr exposes
7905
+ the resolved value for tests. */}
7906
+ <text x="10" y="48" fontSize="9" fontFamily="monospace" fontWeight="500" fill={pal.legendText} data-topo-hover-detail-body-fw="500">
6128
7907
  {rt ? rt.label : 'runtime · pending'}
6129
7908
  </text>
6130
- <text x="10" y="64" fontSize="9" fontFamily="monospace" fill={pal.legendText}>
7909
+ <text x="10" y="64" fontSize="9" fontFamily="monospace" fontWeight="500" fill={pal.legendText} data-topo-hover-detail-body-fw="500">
6131
7910
  host · {session.server || 'unknown'}
6132
7911
  </text>
6133
- <text x="10" y="80" fontSize="9" fontFamily="monospace" fill={pal.legendText} opacity="0.7">
7912
+ <text x="10" y="80" fontSize="9" fontFamily="monospace" fontWeight="500" fill={pal.legendText} opacity="0.7" data-topo-hover-detail-body-fw="500">
6134
7913
  {session.task ? truncate(session.task, 28) : 'no recent task'}
6135
7914
  </text>
6136
7915
  </g>
@@ -6194,14 +7973,41 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6194
7973
  keySplines="0.25 0.1 0.25 1"
6195
7974
  fill="freeze"
6196
7975
  />
7976
+ {/* Round 403 / Loop: click-ripple SMIL initial opacity
7977
+ 0.7 → 0.8. Pre-R403 the ripple's opacity animation
7978
+ faded from 0.7 to 0 over 500ms, providing a clean
7979
+ click-feedback pulse. Theme-consistency / canvas-
7980
+ presence polish family (R370 hub hover-ring +
7981
+ R391 hub-spoke active) already lifted paired
7982
+ hover-state alphas from 0.7 → 0.8. R403 brings
7983
+ click-feedback into that same alpha — three canvas
7984
+ state-feedback indicators (hover-ring, active spoke,
7985
+ click ripple) now share a uniform 0.8 start alpha
7986
+ so the visual "I responded" signal carries the
7987
+ same weight regardless of which state fired it.
7988
+ Pre-R403 invariants preserved: 500ms duration,
7989
+ R227 calcMode='spline' + ease-out keySplines
7990
+ (0.25 0.1 0.25 1), fill='freeze', concurrent r
7991
+ animation. Theme-consistency family (8 anchors):
7992
+ R370 hub hover-ring 0.7 → 0.8
7993
+ R371 edge-badge rest 0.82 → 0.85 cyber
7994
+ R372 minimap offline-dot 0.5 → 0.6
7995
+ R386 hub-highlight idle 0.9 → 0.95
7996
+ R387 hover-detail panel 0.94 → 0.97 cyber
7997
+ R391 hub-spoke active 0.7 → 0.8
7998
+ R392 minimap online-dot 0.9 → 0.95
7999
+ R403 click-ripple start 0.7 → 0.8 (this round)
8000
+ data-click-ripple-start-opacity attr exposes the
8001
+ resolved value for tests. */}
6197
8002
  <animate
6198
8003
  attributeName="opacity"
6199
- values="0.7;0"
8004
+ values="0.8;0"
6200
8005
  dur="0.5s"
6201
8006
  calcMode="spline"
6202
8007
  keyTimes="0;1"
6203
8008
  keySplines="0.25 0.1 0.25 1"
6204
8009
  fill="freeze"
8010
+ data-click-ripple-start-opacity="0.8"
6205
8011
  />
6206
8012
  </circle>
6207
8013
  )}
@@ -6275,17 +8081,21 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6275
8081
  <rect
6276
8082
  x="0" y="0" width="230" height="88" rx="10"
6277
8083
  fill={pal.legendBox.fill}
6278
- stroke={pal.legendBox.stroke}
6279
- // Round 348 / Loop: recent-signal panel rect opacity hover-
6280
- // state bump — joins the panel-hover cue stack (R135 drop-
6281
- // shadow boost + R345 title letter-spacing tween 0.3 → 0.4
6282
- // + R266 fill theme-flip). Cyber 0.92 → 0.97, light 0.97 →
6283
- // 1.0 on hoveredPanel === 'recent'. The panel "solidifies"
6284
- // on hover — pure paint-level change, geometry-safe (bbox
6285
- // unchanged so topo-overlap-test invariants hold). The
6286
- // R247 transition list already includes `opacity 200ms
6287
- // ease-out` so the value tween is automatic. Sibling
6288
- // change at legend panel rect below (~line 7222).
8084
+ // Round 423 / Loop: panel rect stroke tints to legendAccent
8085
+ // (cyan) on hover — sibling to R217 label-card stroke
8086
+ // hover-tint at the panel scope. Pre-R423 the panel rect
8087
+ // stroke painted pal.legendBox.stroke (neutral) regardless
8088
+ // of hover state, while every other panel hover cue stacked:
8089
+ // R135 drop-shadow boost
8090
+ // R348 rect opacity 0.92 → 0.97 cyber
8091
+ // R345 title letter-spacing 0.3 → 0.4
8092
+ // R423 rect stroke → legendAccent (this round)
8093
+ // Four hover layers now telegraph "you're entering this
8094
+ // panel" through structural, paint, and typographic axes
8095
+ // simultaneously. R247 transition list already covers
8096
+ // stroke 200ms ease-out so the tint eases naturally.
8097
+ // Sibling change at the legend panel rect below.
8098
+ stroke={hoveredPanel === 'recent' ? pal.legendAccent : pal.legendBox.stroke}
6289
8099
  opacity={hoveredPanel === 'recent' ? (isLight ? 1 : 0.97) : (isLight ? 0.97 : 0.92)}
6290
8100
  style={{
6291
8101
  /* R135: drop-shadow intensifies on panel hover. Base
@@ -6409,8 +8219,8 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6409
8219
  const alpha = ageSec <= 30
6410
8220
  ? 1
6411
8221
  : ageSec <= 300
6412
- ? 1 - ((ageSec - 30) / 270) * 0.75
6413
- : 0.25;
8222
+ ? 1 - ((ageSec - 30) / 270) * 0.70 /* R358: floor 0.25 → 0.30 lift across 3 freshness scopes */
8223
+ : 0.30; /* R358: stale floor lifted 0.25 → 0.30 — 20% legibility bump while preserving fresh/stale ratio */
6414
8224
  // Dark cyan-400 / light teal-600 with alpha — same
6415
8225
  // palette as R161's chip bullet so the two scopes
6416
8226
  // visually align even side-by-side.
@@ -6478,13 +8288,27 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6478
8288
  tests + count value reads still resolve via
6479
8289
  .textContent. data-recent-panel-count-unit on
6480
8290
  the inner unit tspan for R336 introspection. */}
8291
+ {/* R424 — recent-signal panel count digit fontWeight
8292
+ 600 → 700 on panel hover. Closes the 5-layer panel
8293
+ hover cue stack with a typographic-weight axis at
8294
+ the panel-header data scope: depth (R135 drop-
8295
+ shadow) + solidity (R348 fill opacity) + spacing
8296
+ (R345 title letter-spacing) + edge color (R423
8297
+ stroke tint) + weight (THIS, digit fw). Sibling
8298
+ pattern to R416 chip-digit-hover-bold at chip
8299
+ scope — same "data tightens under attention"
8300
+ idiom now at the panel-header data scope. R311
8301
+ base fw=600 + R225 tabular-nums + R162 fill
8302
+ transition + R336 unit-tspan opacity-0.7 all
8303
+ preserved; only the weight axis tweens via R247's
8304
+ transition shape (added font-weight to the list). */}
6481
8305
  <tspan
6482
8306
  fill={freshFill}
6483
- fontWeight="600"
8307
+ fontWeight={hoveredPanel === 'recent' ? '700' : '600'}
6484
8308
  data-recent-panel-count
6485
8309
  data-recent-panel-count-freshness-alpha={alpha.toFixed(2)}
6486
8310
  style={{
6487
- transition: 'fill 200ms ease-out',
8311
+ transition: 'fill 200ms ease-out, font-weight 200ms ease-out',
6488
8312
  fontVariantNumeric: 'tabular-nums',
6489
8313
  }}
6490
8314
  >{flowLinks.length}<tspan opacity="0.7" data-recent-panel-count-unit> flows</tspan></tspan>
@@ -6906,18 +8730,83 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6906
8730
  const alpha = ageSec <= 30
6907
8731
  ? 1
6908
8732
  : ageSec <= 300
6909
- ? 1 - ((ageSec - 30) / 270) * 0.75
6910
- : 0.25;
8733
+ ? 1 - ((ageSec - 30) / 270) * 0.70 /* R358: floor 0.25 → 0.30 lift across 3 freshness scopes */
8734
+ : 0.30; /* R358: stale floor lifted 0.25 → 0.30 — 20% legibility bump while preserving fresh/stale ratio */
6911
8735
  return (
6912
8736
  <circle
6913
8737
  cx={10}
6914
8738
  cy={38 + index * 16 - 3}
6915
- r={1.6}
8739
+ /* Round 359 / Loop: recency pip base radius
8740
+ 1.6 → 1.8. Sibling lift to R358's freshness-
8741
+ floor bump (alpha 0.25 → 0.30) — pre-R358/
8742
+ R359 the stale pip painted at r=1.6 + α=0.25
8743
+ which read as near-invisible chrome. R358
8744
+ gave it more alpha; R359 gives it more area
8745
+ (1.8² / 1.6² ≈ 1.27, so ~27 % more glyph)
8746
+ so the pip stays distinguishable across the
8747
+ freshness ramp. Geometry: 1.8-radius dot
8748
+ centred at (10, row_y - 3) is bbox 3.6×3.6,
8749
+ still well inside the 7-px left margin
8750
+ (x=6 rect-start → x=13 text-start) the R160
8751
+ pip was placed in. Overlap-test reads the
8752
+ parent row rect's bbox, not this pip's, so
8753
+ grid+ring invariants hold. Matches the same
8754
+ 1.6 → 1.8 visual-weight bump R295 applied
8755
+ to the legend swatch (5.5 → 6 base radius)
8756
+ and R287 to the minimap viewport stroke
8757
+ (1 → 1.5). data-recent-row-freshness-radius
8758
+ attr exposes the value for tests. */
8759
+ /* Round 383 / Loop: recency pip base radius
8760
+ 1.8 → 2.0. Continues the R359 lift
8761
+ trajectory — pip area grows ~23 % (π·2²/
8762
+ π·1.8² ≈ 1.23) for a clearer at-a-glance
8763
+ freshness anchor in each row. Bbox 4.0×4.0
8764
+ still inside the 7-px R160 left margin
8765
+ (3-px remaining clearance vs 3.4 at r=1.8
8766
+ — geometry-safe margin holds). Sibling
8767
+ visual-weight bump family (9th anchor now):
8768
+ R287 minimap viewport stroke 1 → 1.5
8769
+ R295 legend swatch base radius 5.5 → 6
8770
+ R359 recent-row pip base radius 1.6 → 1.8
8771
+ R360 hub digit fontSize 11 → 12
8772
+ R361 edge-badge digit fontSize 10 → 11
8773
+ R365 hub-highlight base radius 5 → 5.5
8774
+ R367 edge-badge rest stroke 1 → 1.25
8775
+ R374 pressure-bar height 1.5 → 2
8776
+ R383 recent-row pip radius 1.8 → 2.0 (this round)
8777
+ data-recent-row-freshness-radius attr
8778
+ bumps to '2.0' for tests. */
8779
+ /* Round 447 / Loop: recent-row freshness pip
8780
+ radius lift on (isRowHovered || isRowPinned)
8781
+ — r 2.0 → 2.5 (+0.5px, sibling to R442
8782
+ endpoint-ring r lift). Adds a geometric
8783
+ axis to the recent-row hover/pin gesture
8784
+ alongside R143 translateY + R104 row bg-
8785
+ tint + R434 letter-spacing + R445 count
8786
+ fw. Pre-R447 the pip stayed at r=2.0 always
8787
+ — the freshness alpha (R162) tracked
8788
+ recency but didn't telegraph "this row is
8789
+ in focus" geometrically. R447 lifts the
8790
+ pip outward by 25% area (π·2.5² / π·2.0²
8791
+ = 1.56) on attention, closing a 5-axis
8792
+ row-attention signature (geometry + paint
8793
+ + typography + spacing + position).
8794
+ SVG `r` as CSS property for interpolation
8795
+ (R197/R198 idiom). transition list extends
8796
+ to include 'r 200ms ease-out' matching the
8797
+ opacity cadence. data-recent-row-freshness-
8798
+ lifted attr exposes the gate for tests. */
6916
8799
  fill={pal.legendAccent}
6917
8800
  opacity={alpha}
6918
8801
  data-recent-row-freshness={link.key}
6919
8802
  data-recent-row-freshness-alpha={alpha.toFixed(2)}
6920
- style={{ pointerEvents: 'none', transition: 'opacity 200ms ease-out' }}
8803
+ data-recent-row-freshness-radius={(isRowHovered || isRowPinned) ? 2.5 : 2.0}
8804
+ data-recent-row-freshness-lifted={(isRowHovered || isRowPinned) ? 'true' : 'false'}
8805
+ style={{
8806
+ pointerEvents: 'none',
8807
+ r: `${(isRowHovered || isRowPinned) ? 2.5 : 2.0}px`,
8808
+ transition: 'opacity 200ms ease-out, r 200ms ease-out',
8809
+ } as React.CSSProperties}
6921
8810
  />
6922
8811
  );
6923
8812
  })()}
@@ -6946,11 +8835,65 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6946
8835
  fill={isRowActive ? pal.legendHeadline : pal.legendText}
6947
8836
  fontSize="9"
6948
8837
  fontFamily="monospace"
8838
+ /* Round 363 / Loop: recent-row text fontWeight 400
8839
+ → 500 (font-medium tier). At fontSize=9 the
8840
+ default-weight 400 glyphs read thin against the
8841
+ panel chrome (pal.legendBox.fill with 0.92/0.97
8842
+ opacity); the 100-weight bump lifts the alias→
8843
+ alias text into the legibility band without
8844
+ changing geometry. The R320 count tspan fw=600
8845
+ (cold) / fw=700 (hot) override still wins
8846
+ locally via inline fontWeight on the inner
8847
+ tspan, so the count-vs-alias hierarchy stays
8848
+ intact:
8849
+ alias fw 500 (R363, this round)
8850
+ count fw 600/700 (R320)
8851
+ Sibling typography lift to R362 chip-row digit
8852
+ 500 → 600 — both nudge a within-element data
8853
+ tier without disturbing the surrounding family
8854
+ baseline. data-recent-row-text-font-weight attr
8855
+ exposes the value for tests. */
8856
+ fontWeight="500"
6949
8857
  data-recent-row-text={link.key}
6950
8858
  data-recent-row-text-pinned={isRowPinned ? 'true' : 'false'}
8859
+ data-recent-row-text-hovered={!isRowPinned && isRowHovered ? 'true' : 'false'}
8860
+ data-recent-row-text-font-weight="500"
8861
+ /* Round 434 / Loop: recent-signal row text extends
8862
+ from R220's pin-only letter-spacing (0 → 0.5 on
8863
+ isRowPinned) to a 3-tier scale matching R433
8864
+ legend-row at the sibling panel-row scope:
8865
+ rest → 0px
8866
+ isRowHovered → 0.25px ← this round
8867
+ isRowPinned → 0.5px (R220 preserved)
8868
+ Pre-R434 R220 noted: "Hover stays at default
8869
+ tracking — the spread is pin-exclusive so
8870
+ users can read pinned vs hovered at the text
8871
+ alone." R427-R433 established a 3-tier pattern
8872
+ across 4 surfaces (node-alias, edge-badge,
8873
+ group-label, legend-row) where hover gets a
8874
+ subtler intermediate kerning step distinct
8875
+ from the pin tier's stronger spread — the
8876
+ locked vs preview discrimination R220 wanted
8877
+ is preserved (0.5 > 0.25) AND hover gets a
8878
+ typographic axis of its own. R434 completes
8879
+ the 5-surface arc.
8880
+ 5-surface 3-tier letter-spacing pattern now
8881
+ spans every interactive label on TopoGraph
8882
+ that distinguishes hover from pin:
8883
+ node-alias (R427) 0 / 0.3 / 0.5
8884
+ edge-badge (R431) 0 / 0.2 / 0.4
8885
+ group-label (R432) 0 / 0.25 / 0.5
8886
+ legend-row (R433) 0 / 0.25 / 0.5
8887
+ recent-row (R434) 0 / 0.25 / 0.5 ← this round
8888
+ Hover-letter-spacing family extension
8889
+ (10 anchors now): R344/R345/R347/R351/R420/
8890
+ R427/R431/R432/R433/R434. R55 fill 150ms +
8891
+ R220 letter-spacing 150ms transition kept
8892
+ (additive conditional case, no new property). */
6951
8893
  style={{
6952
8894
  transition: 'fill 150ms ease-out, letter-spacing 150ms ease-out',
6953
- letterSpacing: isRowPinned ? '0.5px' : '0px',
8895
+ letterSpacing: isRowPinned ? '0.5px' :
8896
+ isRowHovered ? '0.25px' : '0px',
6954
8897
  }}
6955
8898
  >
6956
8899
  {/* R138 / Loop: typography unification with the rest
@@ -7010,19 +8953,60 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7010
8953
  distinct, plus the fill flip from legendText
7011
8954
  → amber carries the dramatic part of the cue).
7012
8955
  Sibling treatment in the data-weight tier. */}
8956
+ {/* Round 445 / Loop: extend the R320 cold/hot fw
8957
+ binary (600/700) to ALSO fire on isRowPinned —
8958
+ pinned-cold now lifts to 700 alongside the
8959
+ existing hot-triggered lift. Sibling to R444
8960
+ group-label-count-pin (500→600) at the
8961
+ recent-row scope. Both panel-row counts now
8962
+ respond to pin with a typographic weight lift,
8963
+ part of the "data tightens under attention"
8964
+ family (R416/R424/R425/R426/R444/R445).
8965
+ Effective tiers:
8966
+ cold + un-pinned → fw 600
8967
+ cold + pinned → fw 700 ← this round
8968
+ hot (any pin state) → fw 700 (R320 preserved)
8969
+ hot is still amber-filled (R127); cold pin
8970
+ stays at the parent fill, so the two routes
8971
+ to fw=700 are visually distinct (color vs
8972
+ no color). transition list adds 'font-
8973
+ weight 200ms ease-out' so the lift eases
8974
+ under the same R320 fill cadence. data-
8975
+ recent-row-count-pinned attr exposes the
8976
+ pin gate for tests. */}
7013
8977
  <tspan
7014
8978
  fill={isHot ? hotStroke : undefined}
7015
- fontWeight={isHot ? '700' : '600'}
8979
+ fontWeight={(isHot || isRowPinned) ? '700' : '600'}
7016
8980
  data-recent-row-count
8981
+ data-recent-row-count-pinned={isRowPinned ? 'true' : 'false'}
8982
+ data-recent-row-count-font-weight={(isHot || isRowPinned) ? '700' : '600'}
7017
8983
  {...(isHot ? { 'data-recent-row-count-hot': 'true' } : {})}
7018
8984
  style={{
7019
- transition: 'fill 300ms ease-out',
8985
+ transition: 'fill 300ms ease-out, font-weight 200ms ease-out',
7020
8986
  fontVariantNumeric: 'tabular-nums',
7021
8987
  }}
7022
8988
  >
7023
8989
  {link.count}
7024
8990
  </tspan>
7025
- {' · '}{truncate(link.content, 8)}
8991
+ {/* Round 418 / Loop: recent-row content preview
8992
+ gains opacity=0.7 wrapper — subordinate-text
8993
+ tier at the SVG-text scope. Pre-R418 the
8994
+ truncated content preview (e.g. " · hi there")
8995
+ inherited the row's full opacity, reading at
8996
+ the same emphasis as the alias text and
8997
+ count digit. R418 wraps it in a <tspan> at
8998
+ opacity=0.7 so the preview reads as
8999
+ subordinate metadata — sibling to R333-R341/
9000
+ R362/R369/R389/R410/R412 chip-internal-
9001
+ hierarchy "label tier" (opacity-70) at the
9002
+ HTML scope, and R317 subordinate-text-lift
9003
+ gray-500 → gray-400 family. The leading
9004
+ " · " separator stays at full opacity so
9005
+ the row punctuation rhythm holds. data-
9006
+ recent-row-content-tspan attr surfaces the
9007
+ subordinate wrapper for tests. */}
9008
+ {' · '}
9009
+ <tspan opacity="0.7" data-recent-row-content-tspan>{truncate(link.content, 8)}</tspan>
7026
9010
  </text>
7027
9011
  {lastAt ? (
7028
9012
  /* Round 321 / Loop: lastAt freshness timestamp picks
@@ -7209,6 +9193,23 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7209
9193
  surface. transition list extends letter-spacing
7210
9194
  200ms ease-out alongside the existing opacity/
7211
9195
  fill easings. */}
9196
+ {/* Round 368 / Loop: `+N more flows` footer text gains
9197
+ fontWeight=500 (font-medium tier). Sibling small-
9198
+ text fw lift family with R363 recent-row alias
9199
+ + R364 legend-row label + R366 group-label count
9200
+ — all four lifts share the same theory: at small
9201
+ fontSize (9-11 px) against panel chrome, SVG-
9202
+ default fw 400 sits at the legibility floor;
9203
+ fw 500 brings the glyph into the deliberate-data
9204
+ band. fontStyle=italic + opacity 0.55 rest + R325
9205
+ letterSpacing 0.2 baseline + R344 hover-spread
9206
+ 0.2 → 0.3 + R195 cyan fill on hover all preserved
9207
+ — the fw bump just thickens the italic stroke.
9208
+ Hover-state punch (R195 fill + R325 opacity 0.55
9209
+ → 0.85 + R344 letter-spacing + R133 underline)
9210
+ stays as is, so the rest-vs-hover delta still
9211
+ reads clearly. data-recent-panel-more-font-weight
9212
+ attr exposes the value for tests. */}
7212
9213
  <text
7213
9214
  x="115" y="82"
7214
9215
  textAnchor="middle"
@@ -7216,11 +9217,13 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7216
9217
  fontSize="9"
7217
9218
  fontFamily="monospace"
7218
9219
  fontStyle="italic"
9220
+ fontWeight="500"
7219
9221
  letterSpacing={hoveredRecentMore ? '0.3' : '0.2'}
7220
9222
  opacity={hoveredRecentMore ? 0.85 : 0.55}
7221
9223
  textDecoration={hoveredRecentMore ? 'underline' : 'none'}
7222
9224
  data-recent-panel-more={moreCount}
7223
9225
  data-recent-panel-more-hovered={hoveredRecentMore ? 'true' : 'false'}
9226
+ data-recent-panel-more-font-weight="500"
7224
9227
  style={{ transition: 'opacity 150ms ease-out, fill 200ms ease-out, letter-spacing 200ms ease-out' }}
7225
9228
  >
7226
9229
  {`+ ${moreCount}`}
@@ -7272,7 +9275,11 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7272
9275
  <rect
7273
9276
  x="0" y="0" width="224" height="88" rx="10"
7274
9277
  fill={pal.legendBox.fill}
7275
- stroke={pal.legendBox.stroke}
9278
+ // R423 sibling — legend panel rect stroke tints to
9279
+ // legendAccent on hover (mirrors recent-signal panel
9280
+ // above). 4-layer hover cue stack now symmetric across
9281
+ // both side panels.
9282
+ stroke={hoveredPanel === 'legend' ? pal.legendAccent : pal.legendBox.stroke}
7276
9283
  // R348 sibling — legend panel rect opacity hover-state
7277
9284
  // bump 0.92 → 0.97 (cyber) / 0.97 → 1 (light) on
7278
9285
  // hoveredPanel === 'legend'. Pairs with the recent-signal
@@ -7364,9 +9371,17 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7364
9371
  R336 introspection; the parent .textContent still
7365
9372
  reads "{N} node(s)" so existing R310 count tests via
7366
9373
  textContent unchanged. */}
9374
+ {/* R424 sibling — legend panel count digit fontWeight 600
9375
+ → 700 on panel hover. Closes 5-layer panel hover cue
9376
+ stack symmetric across both side panels (recent-signal
9377
+ + legend): depth (R135) + solidity (R348) + spacing
9378
+ (R345) + edge color (R423) + weight (R424). R310 base
9379
+ fw=600 + R292 tabular-nums + R266 fill transition + R336
9380
+ unit-tspan opacity-0.7 all preserved. Same "data tightens
9381
+ under attention" idiom R416 established at chip scope. */}
7367
9382
  <text
7368
9383
  x="211" y="21" textAnchor="end"
7369
- fill={pal.legendAccent} fontSize="10" fontFamily="monospace" fontWeight="600"
9384
+ fill={pal.legendAccent} fontSize="10" fontFamily="monospace" fontWeight={hoveredPanel === 'legend' ? '700' : '600'}
7370
9385
  // R349 sibling — legend panel header count picks up
7371
9386
  // letterSpacing="0.2", one tier below the R301 panel
7372
9387
  // title 0.3. Pairs with the recent-signal panel count
@@ -7376,7 +9391,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7376
9391
  data-legend-panel-count
7377
9392
  data-legend-panel-count-letter-spacing="0.2"
7378
9393
  style={{
7379
- transition: 'fill 200ms ease-out',
9394
+ transition: 'fill 200ms ease-out, font-weight 200ms ease-out',
7380
9395
  fontVariantNumeric: 'tabular-nums',
7381
9396
  }}
7382
9397
  >{sessions.length}<tspan opacity="0.7" data-legend-panel-count-unit> node{sessions.length === 1 ? '' : 's'}</tspan></text>
@@ -7595,14 +9610,45 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7595
9610
  so this legend-internal circle is invisible to
7596
9611
  that probe. pointerEvents:none so the ring can't
7597
9612
  intercept the row click that produced it. */}
9613
+ {/* Round 402 / Loop: legend pin-ring strokeWidth 1.5
9614
+ → 1.75. Sibling visual-weight bump (12th anchor)
9615
+ to R385 hub hover-ring strokeWidth 1.5 → 1.75 —
9616
+ both are pin/hover state indicators painted as
9617
+ stroke-only circles outside their target swatch
9618
+ with the R51 sentinel value 1.5. R402 lifts to
9619
+ 1.75 (matching R385's choice) so the pin signal
9620
+ reads slightly heavier without crossing the
9621
+ R51 sentinel band (3 reserved for offline node).
9622
+ The R51 selector is gated to g[data-node]
9623
+ ancestors so this legend-internal circle (lives
9624
+ under a <g data-legend-status>) is invisible
9625
+ to the probe — same lesson R177/R385 documented.
9626
+ Visual-weight bump family (12 anchors now):
9627
+ R287 minimap viewport stroke 1 → 1.5
9628
+ R295 legend swatch radius 5.5 → 6
9629
+ R359 recent-row pip radius 1.6 → 1.8
9630
+ R360 hub digit fontSize 11 → 12
9631
+ R361 edge-badge digit fontSize 10 → 11
9632
+ R365 hub-highlight radius 5 → 5.5
9633
+ R367 edge-badge rest stroke 1 → 1.25
9634
+ R374 pressure-bar height 1.5 → 2
9635
+ R383 recent-row pip radius 1.8 → 2.0
9636
+ R384 minimap online dot 1.7 → 1.9
9637
+ R385 hub hover-ring stroke 1.5 → 1.75
9638
+ R402 legend pin-ring stroke 1.5 → 1.75 (this round)
9639
+ R181 always-mount opacity gate + 150ms transition
9640
+ + pointerEvents:none all preserved. data-legend-
9641
+ pin-ring-stroke-width attr exposes the value for
9642
+ tests. */}
7598
9643
  <circle
7599
9644
  cx="16" cy={row.y0} r="8"
7600
9645
  fill="none"
7601
9646
  stroke={row.fill}
7602
- strokeWidth="1.5"
9647
+ strokeWidth="1.75"
7603
9648
  opacity={isPinned ? 1 : 0}
7604
9649
  data-legend-pin-ring={row.key}
7605
9650
  data-legend-pin-ring-pinned={isPinned ? 'true' : 'false'}
9651
+ data-legend-pin-ring-stroke-width="1.75"
7606
9652
  style={{
7607
9653
  pointerEvents: 'none',
7608
9654
  transition: 'opacity 150ms ease-out',
@@ -7629,11 +9675,61 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7629
9675
  fill={hoveredStatus === row.key || isPinned ? pal.legendHeadline : pal.legendText}
7630
9676
  fontSize="11"
7631
9677
  fontFamily="monospace"
9678
+ /* Round 364 / Loop: legend-row label fontWeight 400
9679
+ → 500. Sibling typography lift to R363 recent-row
9680
+ text fw 400 → 500. Both surfaces render small
9681
+ monospace text against panel chrome at fontSize
9682
+ 9-11 where SVG-default fw 400 sits at the
9683
+ legibility floor. font-medium tier (500) gives
9684
+ the label a more deliberate-data register.
9685
+ The R309 per-row count text (separate element
9686
+ below at x=215 textAnchor=end) keeps its own
9687
+ fontWeight 600 inline override, so the count >
9688
+ label hierarchy stays intact at the legend
9689
+ scope same as R363 holds it at the recent-row
9690
+ scope:
9691
+ legend label fw 500 (R364, this round)
9692
+ legend count fw 600 (R309)
9693
+ recent alias fw 500 (R363)
9694
+ recent count fw 600/700 (R320)
9695
+ data-legend-row-label-font-weight attr exposes
9696
+ the value for tests. R219 letter-spacing pin
9697
+ tween + R55 fill transition + R181 always-mount
9698
+ pin ring all preserved. */
9699
+ fontWeight="500"
7632
9700
  data-legend-row-label={row.key}
7633
9701
  data-legend-row-label-pinned={isPinned ? 'true' : 'false'}
9702
+ data-legend-row-label-hovered={!isPinned && hoveredStatus === row.key ? 'true' : 'false'}
9703
+ data-legend-row-label-font-weight="500"
9704
+ /* Round 433 / Loop: legend-row text extends from
9705
+ R219's pin-only letter-spacing (0px → 0.5px on
9706
+ isPinned) to a 3-tier scale matching the R432
9707
+ group-label pattern:
9708
+ rest → 0px
9709
+ hoveredStatus → 0.25px ← this round
9710
+ isPinned → 0.5px (R219 preserved)
9711
+ Pre-R433 hover already brightened the fill
9712
+ (hoveredStatus===row.key || isPinned matches the
9713
+ legendHeadline branch) but the letter-form
9714
+ stayed dead-typographic on transient hover —
9715
+ only the pin tier carried a kerning signature.
9716
+ R433 adds the missing mid tier so hover
9717
+ telegraphs through BOTH fill brighten AND a
9718
+ subtle 0.25-px kerning spread, mirroring
9719
+ R427/R431/R432 at legend-row scope. Pin tier
9720
+ still wins so the locked vs preview distinction
9721
+ at the type level stays intact.
9722
+ Hover-letter-spacing family extension (9 anchors
9723
+ now): R344/R345/R347/R351/R420/R427/R431/R432/
9724
+ R433. 3-tier letter-spacing pattern now spans 4
9725
+ surfaces (node-alias R427, edge-badge R431,
9726
+ group-label R432, legend-row R433). R55 fill
9727
+ 150ms + R219 letter-spacing 150ms transition
9728
+ untouched — additive conditional case. */
7634
9729
  style={{
7635
9730
  transition: 'fill 150ms ease-out, letter-spacing 150ms ease-out',
7636
- letterSpacing: isPinned ? '0.5px' : '0px',
9731
+ letterSpacing: isPinned ? '0.5px' :
9732
+ hoveredStatus === row.key ? '0.25px' : '0px',
7637
9733
  }}
7638
9734
  >{row.label}</text>
7639
9735
  {/* R95: live count anchored to the right edge of the
@@ -7712,20 +9808,60 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7712
9808
  labels to single status words, the count
7713
9809
  becomes proportionally more important; this
7714
9810
  round emphasizes that role typographically. */}
9811
+ {/* Round 446 / Loop: legend per-row count fontWeight
9812
+ lift 600 → 700 on isPinned. Mirror of R444 group-
9813
+ label-count + R445 recent-row-count at the
9814
+ legend-row scope. Closes the 3-panel-row family
9815
+ for the "data tightens under attention" pattern —
9816
+ every panel-row count now responds to pin with a
9817
+ typographic-weight bump:
9818
+ R444 group-label-count 500 → 600
9819
+ R445 recent-row-count 600 → 700 (cold-pin route)
9820
+ R446 legend-row-count 600 → 700 ← this round
9821
+ Hover gate (hoveredStatus===row.key) keeps rest
9822
+ fw=600 so the locked-vs-preview distinction at
9823
+ the type level stays intact — same gate R433 used
9824
+ on the parent <text> letter-spacing tween. R309
9825
+ fw=600 baseline + R204 empty-row opacity dim +
9826
+ R225 tabular-nums all preserved. transition list
9827
+ extends to include 'font-weight 150ms ease-out'
9828
+ matching R433 fill/letter-spacing cadence.
9829
+ data-legend-count-pinned + -font-weight attrs
9830
+ exposed for tests. */}
7715
9831
  <text
7716
9832
  x="215" y={row.y1}
7717
9833
  textAnchor="end"
7718
9834
  fill={row.count > 0 && (hoveredStatus === row.key || isPinned) ? row.fill : pal.legendText}
7719
9835
  fontSize="11"
7720
9836
  fontFamily="monospace"
7721
- fontWeight="600"
9837
+ fontWeight={isPinned ? '700' : '600'}
9838
+ /* Round 449 / Loop: legend-row count active-state
9839
+ opacity 0.95 → 1.0 on (hoveredStatus===row.key
9840
+ || isPinned). Pre-R449 R204 lifted populated-row
9841
+ active opacity from rest 0.65 to 0.95 — visibly
9842
+ brighter but kept a 5 pct alpha gap (1 - 0.95).
9843
+ R449 closes the gap to 1.0 so the active count
9844
+ reads as confidently present alongside the R446
9845
+ fw=600→700 + R433 letter-spacing tween. Theme-
9846
+ consistency / canvas-presence family extension
9847
+ (7th anchor on the active-presence lift sub-
9848
+ family): R370 hub hover-ring 0.7→0.8, R371 edge-
9849
+ badge rest 0.82→0.85, R372 minimap offline-dot
9850
+ 0.5→0.6, R386 hub-highlight idle 0.9→0.95, R387
9851
+ hover-detail panel 0.94→0.97, R429 label-card
9852
+ body 0.94→1.0, R449 legend-count active 0.95→1.0
9853
+ ← this round. Empty-row opacity (R204: 0.28
9854
+ light / 0.30 cyber) and idle 0.65 rest both
9855
+ preserved. */
7722
9856
  opacity={row.count === 0
7723
9857
  ? (isLight ? 0.28 : 0.30)
7724
- : (hoveredStatus === row.key || isPinned ? 0.95 : 0.65)}
9858
+ : (hoveredStatus === row.key || isPinned ? 1 : 0.65)}
7725
9859
  data-legend-count={row.key}
7726
9860
  data-legend-count-empty={row.count === 0 ? 'true' : 'false'}
9861
+ data-legend-count-pinned={isPinned ? 'true' : 'false'}
9862
+ data-legend-count-font-weight={isPinned ? '700' : '600'}
7727
9863
  data-legend-count-fill={row.count > 0 && (hoveredStatus === row.key || isPinned) ? 'tier' : 'neutral'}
7728
- style={{ pointerEvents: 'none', transition: 'opacity 150ms ease-out, fill 150ms ease-out', fontVariantNumeric: 'tabular-nums' }}
9864
+ style={{ pointerEvents: 'none', transition: 'opacity 150ms ease-out, fill 150ms ease-out, font-weight 150ms ease-out', fontVariantNumeric: 'tabular-nums' }}
7729
9865
  >{row.count}</text>
7730
9866
  </g>
7731
9867
  );
@@ -7949,14 +10085,75 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7949
10085
  const isOn = s.status !== 'offline' || !!sseN;
7950
10086
  const st = nodeStatus(s, isOn, isLight);
7951
10087
  return (
10088
+ /* Round 372 / Loop: minimap offline-dot opacity
10089
+ 0.5 → 0.6. Sibling stale-state legibility lift
10090
+ to R358 freshness ramp floor 0.25 → 0.30 + R317
10091
+ subordinate-text-lift family. Pre-R372 R198
10092
+ drew offline dots at α=0.5 (44 % below online
10093
+ 0.9). The minimap is a small overlay against
10094
+ the canvas backdrop — at α=0.5 offline dots
10095
+ sat at the legibility floor when the minimap
10096
+ mounted (only on non-default view). R372 lifts
10097
+ offline 0.5 → 0.6 for +20 % relative presence;
10098
+ online stays at 0.9 so the offline/online
10099
+ contrast ratio is now 0.6/0.9 ≈ 0.67 (vs prior
10100
+ 0.5/0.9 ≈ 0.56) — still a clear two-tier
10101
+ distinction. R198 opacity + fill + r transition
10102
+ list preserved so status flips still ease
10103
+ smoothly. data-topo-minimap-dot-opacity attr
10104
+ exposes the resolved value for tests. */
7952
10105
  <circle
7953
10106
  key={s.alias}
7954
10107
  cx={p.x * sx} cy={p.y * sy}
7955
- r={isOn ? 1.7 : 1.2}
10108
+ /* Round 384 / Loop: minimap online dot radius 1.7
10109
+ → 1.9. Sibling visual-weight bump (10th anchor)
10110
+ to R383 recent-row pip 1.8 → 2.0. R198 designed
10111
+ the dots at 1.7 (online) / 1.2 (offline) — at
10112
+ the minimap's 120 × 82 scale these read clearly
10113
+ but the online ↔ offline contrast was modest
10114
+ (1.7/1.2 = 1.42×). R384 bumps online to 1.9 so
10115
+ the tier delta widens to 1.58× (1.9/1.2). Pair
10116
+ completes minimap-dot legibility polish:
10117
+ R358 (era R372) offline opacity 0.5 → 0.6
10118
+ R384 online radius 1.7 → 1.9 (this round)
10119
+ R198 transition list (opacity + fill + r 200ms)
10120
+ preserved so status flips still ease smoothly.
10121
+ data-topo-minimap-dot-radius attr exposes the
10122
+ resolved value for tests. */
10123
+ /* Round 392 / Loop: minimap online dot opacity
10124
+ 0.9 → 0.95. Theme-consistency / canvas-presence
10125
+ polish family (7th anchor) — mirrors R386's
10126
+ hub-highlight idle 0.9 → 0.95 lift on the
10127
+ minimap surface: the online-dot's idle alpha
10128
+ gap (0.10 against full presence) halves to
10129
+ 0.05, so the live-fleet anchors on the minimap
10130
+ read more confidently. Offline dot stays at
10131
+ R372 0.6 — the binary online/offline contrast
10132
+ ratio shifts from 0.6/0.9 ≈ 0.67 to 0.6/0.95
10133
+ ≈ 0.63, preserved as a clear two-tier
10134
+ distinction. R198 opacity + fill + r transition
10135
+ list + R384 r=1.9 + R372 offline 0.6 all
10136
+ preserved. data-topo-minimap-dot-opacity attr
10137
+ bumps to '0.95' for tests. */
10138
+ /* Round 421 / Loop: online dot opacity 0.95 → 1.0
10139
+ on minimap container hover. Sibling to R346
10140
+ viewport rect strokeWidth/opacity hover tween.
10141
+ When the user hovers the minimap container,
10142
+ the live-fleet anchors brighten from R392
10143
+ baseline (0.95) to full opacity in concert
10144
+ with the R346 viewport rect lift. Offline
10145
+ stays at R372 0.6 — hover state focuses
10146
+ attention on the ACTIVE anchors, not the
10147
+ stale ones. data-topo-minimap-dot-opacity
10148
+ attr (R392) reflects the resolved hover-
10149
+ state value for tests. */
10150
+ r={isOn ? 1.9 : 1.2}
7956
10151
  fill={st.primary}
7957
- opacity={isOn ? 0.9 : 0.5}
10152
+ opacity={isOn ? (hoveredMinimap ? 1 : 0.95) : 0.6}
7958
10153
  data-topo-minimap-dot={s.alias}
7959
10154
  data-topo-minimap-dot-online={isOn ? 'true' : 'false'}
10155
+ data-topo-minimap-dot-opacity={isOn ? (hoveredMinimap ? 1 : 0.95) : 0.6}
10156
+ data-topo-minimap-dot-radius={isOn ? 1.9 : 1.2}
7960
10157
  style={{
7961
10158
  transition: 'opacity 200ms ease-out, fill 200ms ease-out, r 200ms ease-out',
7962
10159
  } as React.CSSProperties}
@@ -7997,17 +10194,88 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7997
10194
  information element to lift it above ambient
7998
10195
  chrome. opacity 0.9 stays — strokeWidth alone
7999
10196
  does the lifting. */}
10197
+ {/* Round 379 / Loop: minimap viewport rect picks up
10198
+ strokeLinejoin='round'. Pre-R379 the rect's 4
10199
+ corners painted with default 'miter' joins —
10200
+ sharp 90° corners with a small miter overshoot
10201
+ (≈ strokeWidth × 1.4 = 2.1 px at sw=1.5). R379
10202
+ rounds the joins so corners arc smoothly through
10203
+ a quarter-circle of radius ≈ strokeWidth/2. At
10204
+ sw=1.5 that's a 0.75-px radius — subtle but
10205
+ matches the same stroke-softening vocabulary R288
10206
+ chrome icons (zoom/reset/fullscreen) and R378
10207
+ flow-rail already speak. Geometry-safe: stroke-
10208
+ linejoin only affects the corner overshoot, the
10209
+ rect's bbox is unchanged. R287 strokeWidth=1.5 +
10210
+ R346 hover-state strokeWidth/opacity bump + R199
10211
+ smoothView x/y/w/h transition all preserved.
10212
+ data-topo-minimap-viewport-linejoin attr exposes
10213
+ the value for tests. */}
10214
+ {/* Round 393 / Loop: minimap viewport rect rx 0 → 2.
10215
+ Pre-R393 the cyan-stroked viewport rect (the frame
10216
+ showing what's currently visible on the canvas)
10217
+ drew with sharp corners inside the R332 rounded
10218
+ minimap container (rx=8). A small frame with sharp
10219
+ corners sitting inside a rounded container reads
10220
+ as visually loud — the 90° corners catch the eye
10221
+ against the soft container edge. R393 adds rx=2
10222
+ so the viewport corners get a subtle radius that
10223
+ matches the family's softening idiom on a sub-
10224
+ element scale. The R379 strokeLinejoin='round'
10225
+ already softens stroke joins; R393 adds a complete
10226
+ geometric soften via rx.
10227
+ Corner-radius cascade (7 anchors now):
10228
+ R330 canvas rx 12
10229
+ R331 panels rx 10
10230
+ R332 minimap container rx 8
10231
+ R375 Layout-toggle rx 8
10232
+ R376 nodeSize/zoom rx 8
10233
+ R390 hover-detail rx 10
10234
+ R393 minimap viewport rx 2 (this round, sub-element)
10235
+ The 2-px radius is intentionally small — the
10236
+ viewport rect is typically only 30-50px wide,
10237
+ where rx=2 reads as "rounded enough to not snap"
10238
+ without feeling pillowy. data-topo-minimap-
10239
+ viewport-rx attr exposes the resolved value
10240
+ for tests. R346 hover-state tweens (strokeWidth
10241
+ + opacity) preserved verbatim. */}
8000
10242
  <rect
8001
10243
  x={Math.max(0, rectX)} y={Math.max(0, rectY)}
8002
10244
  width={Math.max(0, Math.min(MW - Math.max(0, rectX), rectW))}
8003
10245
  height={Math.max(0, Math.min(MH - Math.max(0, rectY), rectH))}
10246
+ rx="2"
8004
10247
  fill="none" stroke={pal.legendAccent}
8005
10248
  // R346: strokeWidth + opacity tween on container hover.
8006
10249
  strokeWidth={hoveredMinimap ? '1.75' : '1.5'}
8007
- opacity={hoveredMinimap ? '1' : '0.9'}
10250
+ strokeLinejoin="round"
10251
+ /* Round 450 / Loop · milestone: minimap viewport rest
10252
+ opacity 0.9 → 0.95. Closes half the alpha gap on
10253
+ the wayfinding indicator while preserving the
10254
+ R346 hover delta to 1.0. Pre-R450 the rest viewport
10255
+ sat at 0.9 (10 pct alpha gap) — adequate but
10256
+ under-confident for the user's primary "you are
10257
+ here" indicator on the minimap. R450 lifts to 0.95
10258
+ so the rest read is more present without erasing
10259
+ the hover lift cue (the +0.05 rest-to-hover delta
10260
+ is small but pairs with R346 sw 1.5→1.75 to keep
10261
+ hover clearly distinguishable). Sibling to R449
10262
+ legend-count active opacity 0.95→1.0 — same
10263
+ "close the active-presence alpha gap" idiom now
10264
+ applied to the REST tier of the wayfinding rect
10265
+ (the minimap viewport stays at canvas-presence
10266
+ register even when un-hovered since it's the
10267
+ spatial referent). Theme-consistency / canvas-
10268
+ presence family (8th anchor on the active-
10269
+ presence lift sub-arc).
10270
+ R287 strokeWidth=1.5 + R379 strokeLinejoin='round'
10271
+ + R346 hover-state tweens + R393 rx=2 + R199
10272
+ smoothView x/y/w/h transition all preserved. */
10273
+ opacity={hoveredMinimap ? '1' : '0.95'}
8008
10274
  data-topo-minimap-viewport
10275
+ data-topo-minimap-viewport-rx="2"
8009
10276
  data-topo-minimap-viewport-smooth={smoothView ? 'true' : 'false'}
8010
10277
  data-topo-minimap-viewport-hover={hoveredMinimap ? 'true' : 'false'}
10278
+ data-topo-minimap-viewport-linejoin="round"
8011
10279
  style={{
8012
10280
  transition: smoothView
8013
10281
  ? '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'
@@ -8066,8 +10334,22 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8066
10334
  own transition-colors. Same R254 holdover pattern that
8067
10335
  R263 just closed at the canvas wrapper scope, now at the
8068
10336
  chrome strip's nodeSize sub-wrapper scope. */}
10337
+ {/* Round 376 / Loop: nodeSize wrapper rounded-md → rounded-lg.
10338
+ Sibling polish to R375 Layout-toggle wrapper. Three
10339
+ chrome-strip segmented controls now all share rounded-lg
10340
+ at the wrapper tier:
10341
+ R375 Layout-toggle wrapper rounded-lg 8 px
10342
+ R376 nodeSize wrapper rounded-lg 8 px (this round)
10343
+ R376 zoom wrapper rounded-lg 8 px (this round)
10344
+ Individual atomic chrome buttons (reset, fullscreen) keep
10345
+ rounded-md (6 px) as their own atomic-button tier — the
10346
+ chrome strip's typography now expresses a clear two-tier
10347
+ hierarchy: 'segmented control container' (rounded-lg)
10348
+ vs 'standalone button' (rounded-md). Pure paint change,
10349
+ no layout shift. */}
8069
10350
  <div
8070
- className="flex items-center rounded-md border overflow-hidden"
10351
+ className="flex items-center rounded-lg border overflow-hidden"
10352
+ data-topo-chrome-nodesize-radius="rounded-lg"
8071
10353
  style={{
8072
10354
  background: pal.legendBox.fill,
8073
10355
  borderColor: pal.containerBorder,
@@ -8147,8 +10429,12 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8147
10429
  data-topo-chrome-view-group-leader marks the boundary surface
8148
10430
  for the test probe; data-topo-chrome-fleet-group-trailer marks
8149
10431
  the nodeSize wrapper's right edge for the gap measurement. */}
10432
+ {/* R376 sibling — zoom wrapper rounded-md → rounded-lg.
10433
+ Closes the chrome-strip segmented-control corner radius
10434
+ cascade (Layout R375 + nodeSize R376 + zoom R376). */}
8150
10435
  <div
8151
- className="ml-1.5 flex items-center rounded-md border overflow-hidden"
10436
+ className="ml-1.5 flex items-center rounded-lg border overflow-hidden"
10437
+ data-topo-chrome-zoom-wrapper-radius="rounded-lg"
8152
10438
  style={{
8153
10439
  background: pal.legendBox.fill,
8154
10440
  borderColor: pal.containerBorder,
@@ -8187,11 +10473,30 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8187
10473
  its own compositor layer for crisper edges during
8188
10474
  the scale tween. Sibling change on zoom-in icon
8189
10475
  below. */}
10476
+ {/* Round 454 / Loop: extend R453 chrome reset icon hover
10477
+ sw lift to zoom +/− icons via Tailwind arbitrary class
10478
+ group-hover:[stroke-width:2.8]. Chrome icon hover sw
10479
+ lift family now 5 anchors:
10480
+ R208 runtime badge outer ring 1.5 → 2
10481
+ R443 runtime badge inner icon 2.4 → 2.8
10482
+ R453 chrome reset icon 2.5 → 2.8
10483
+ R454 chrome zoom-out icon 2.5 → 2.8 ← this round
10484
+ R454 chrome zoom-in icon 2.5 → 2.8 ← this round
10485
+ Tailwind v4 arbitrary-value group-hover variant
10486
+ resolves [stroke-width:2.8] as a CSS property which
10487
+ overrides the static strokeWidth='2.5' attribute on
10488
+ hover. transition-[stroke-width] appended to the
10489
+ existing transition-transform list so the sw tween
10490
+ eases under the same 200ms cadence as R352 group-
10491
+ hover:scale-110. R186 anet-chrome-pop keyframe still
10492
+ owns transform during click via CSS-animation
10493
+ precedence over transition-transform. Sibling change
10494
+ on zoom-in icon below. */}
8190
10495
  <svg
8191
10496
  width="12" height="12" viewBox="0 0 24 24"
8192
10497
  fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"
8193
10498
  aria-hidden
8194
- className={`transition-transform duration-200 ease-out group-hover:scale-110 transform-gpu${chromePopping === 'zoom-out' ? ' anet-chrome-pop' : ''}`}
10499
+ className={`transition-[transform,stroke-width] duration-200 ease-out group-hover:scale-110 group-hover:[stroke-width:2.8] transform-gpu${chromePopping === 'zoom-out' ? ' anet-chrome-pop' : ''}`}
8195
10500
  data-topo-chrome-zoom-out-icon
8196
10501
  ><path d="M5 12h14" /></svg>
8197
10502
  </button>
@@ -8248,6 +10553,24 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8248
10553
  // R347: letter-spacing hover tween — extends R344/R345
8249
10554
  // hover-letter-spacing family into the chrome strip.
8250
10555
  letterSpacing: hoveredZoomLevel ? '0.5px' : '0',
10556
+ // Round 420 / Loop: zoom-level readout gains a SECOND
10557
+ // hover axis — fontWeight 500 → 600 on hover. Sibling
10558
+ // to R347 (same element, hover letter-spacing tween).
10559
+ // The chrome strip's only data display now has a two-
10560
+ // axis hover signature (letter-spacing + fontWeight),
10561
+ // matching the R416 chip-row chip digit hover-bold
10562
+ // pattern at the chrome scope. Pre-R420 hovering only
10563
+ // spread the digits 0 → 0.5px; the weight stayed at
10564
+ // R332's 'font-medium' (500) baseline. Post-R420
10565
+ // hover lifts BOTH letter-spacing AND weight so the
10566
+ // percent reads with the same data-tier emphasis
10567
+ // intensification the chip-row chips do on hover.
10568
+ // Inline fontWeight overrides the className's
10569
+ // 'font-medium' since they target the same property.
10570
+ // 200ms transition list extends to font-weight for
10571
+ // smooth easing. data-topo-chrome-zoom-level-hover
10572
+ // attr surfaces the hover state for tests.
10573
+ fontWeight: hoveredZoomLevel ? 600 : 500,
8251
10574
  /* Round 264 / Loop: zoom level readout gains theme-toggle
8252
10575
  transition. The span has theme-driven color (pal.
8253
10576
  legendText) + border-x (pal.containerBorder via the
@@ -8256,7 +10579,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8256
10579
  on theme flip while siblings eased. Sibling treatment
8257
10580
  to the nodeSize + zoom wrapper transitions added this
8258
10581
  round. */
8259
- transition: 'color 200ms ease-out, border-color 200ms ease-out, letter-spacing 200ms ease-out',
10582
+ transition: 'color 200ms ease-out, border-color 200ms ease-out, letter-spacing 200ms ease-out, font-weight 200ms ease-out',
8260
10583
  }}
8261
10584
  title="Current zoom level"
8262
10585
  >
@@ -8278,11 +10601,13 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8278
10601
  {/* R352 sibling — zoom-in icon picks up the same
8279
10602
  group-hover:scale-110 family. Mirror change at
8280
10603
  the zoom-out icon above. */}
10604
+ {/* R454 sibling — zoom-in icon picks up the same
10605
+ group-hover:[stroke-width:2.8] family lift. */}
8281
10606
  <svg
8282
10607
  width="12" height="12" viewBox="0 0 24 24"
8283
10608
  fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"
8284
10609
  aria-hidden
8285
- className={`transition-transform duration-200 ease-out group-hover:scale-110 transform-gpu${chromePopping === 'zoom-in' ? ' anet-chrome-pop' : ''}`}
10610
+ className={`transition-[transform,stroke-width] duration-200 ease-out group-hover:scale-110 group-hover:[stroke-width:2.8] transform-gpu${chromePopping === 'zoom-in' ? ' anet-chrome-pop' : ''}`}
8286
10611
  data-topo-chrome-zoom-in-icon
8287
10612
  ><path d="M12 5v14M5 12h14" /></svg>
8288
10613
  </button>
@@ -8299,7 +10624,27 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8299
10624
  onBlur={() => setHoveredReset(false)}
8300
10625
  // R196: press-state deepens before R184 reset-spin fires on
8301
10626
  // release — mouse-down dim then 450ms spin = full handshake.
8302
- 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"
10627
+ /* Round 400 / Loop · milestone: chrome reset + fullscreen
10628
+ buttons gain hover:-translate-y-px lift — closes the
10629
+ hover-lift gesture vocabulary across every standalone
10630
+ interactive HTML element in TopoGraph. Segmented
10631
+ controls (zoom -/+, nodeSize S/M/L, Layout Ring/Grid)
10632
+ intentionally stay planted: lifting one segment of a
10633
+ unified strip would tear the visual unity of the
10634
+ segmented control. Only the standalone chrome buttons
10635
+ (reset, fullscreen) get the lift.
10636
+ Gesture vocabulary post-R400 (now complete across HTML):
10637
+ chip-row chips (3×) -1 px R398, R399
10638
+ filter pin pills (4×) -1 px R397
10639
+ recent-signal row -1 px R143
10640
+ legend row -1 px R144
10641
+ reset button -1 px R400 (this round)
10642
+ fullscreen button -1 px R400 (this round)
10643
+ Every standalone interactive HTML surface in TopoGraph
10644
+ now lifts on hover. data-topo-chrome-reset-hover-lift
10645
+ attr surfaces the lift for tests. */
10646
+ 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"
10647
+ data-topo-chrome-reset-hover-lift="true"
8303
10648
  style={{ background: pal.legendBox.fill, borderColor: pal.containerBorder, color: pal.legendText }}
8304
10649
  aria-label="Reset view"
8305
10650
  title="Reset zoom + pan (0, or double-click the canvas)"
@@ -8319,13 +10664,28 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8319
10664
  (2.5) all share one weight. View-box (24×24) and
8320
10665
  display size (13×13) unchanged, so geometry stays
8321
10666
  pixel-stable — only the stroke deepens. */}
10667
+ {/* Round 453 / Loop: chrome reset icon strokeWidth hover
10668
+ lift — 2.5 → 2.8 on hoveredReset && !resetSpinning.
10669
+ Sibling to R443 runtime badge inner-icon sw lift
10670
+ (2.4→2.8) — both chrome icons now thicken on hover
10671
+ for tactile feedback. Pre-R453 reset hover was a
10672
+ rotate-only cue (R350); R453 adds a stroke-weight
10673
+ axis so the affordance reads with both motion (R350
10674
+ rotate -8°) AND geometry (R453 sw +0.3). Gated on
10675
+ !resetSpinning so the R184 spin keyframe owns paint
10676
+ during its 450ms run. 200ms stroke-width transition
10677
+ appended to the style list matches R350 transform
10678
+ cadence. data-topo-chrome-reset-icon-stroke-width
10679
+ attr exposes the resolved value for tests. */}
8322
10680
  <svg
8323
10681
  width="13" height="13" viewBox="0 0 24 24"
8324
- fill="none" stroke="currentColor" strokeWidth="2.5"
10682
+ fill="none" stroke="currentColor"
10683
+ strokeWidth={hoveredReset && !resetSpinning ? '2.8' : '2.5'}
8325
10684
  strokeLinecap="round" strokeLinejoin="round"
8326
10685
  aria-hidden
8327
10686
  className={resetSpinning ? 'anet-reset-spin' : undefined}
8328
10687
  data-topo-chrome-reset-icon
10688
+ data-topo-chrome-reset-icon-stroke-width={hoveredReset && !resetSpinning ? '2.8' : '2.5'}
8329
10689
  // R350: hover-rotate preview of the R184 click-spin.
8330
10690
  // Gated on !resetSpinning so the anet-reset-spin keyframe
8331
10691
  // owns transform during its 450ms run. transformOrigin
@@ -8334,7 +10694,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8334
10694
  style={{
8335
10695
  transform: hoveredReset && !resetSpinning ? 'rotate(-8deg)' : 'rotate(0deg)',
8336
10696
  transformOrigin: 'center',
8337
- transition: 'transform 200ms ease-out',
10697
+ transition: 'transform 200ms ease-out, stroke-width 200ms ease-out',
8338
10698
  }}
8339
10699
  data-topo-chrome-reset-icon-hover={hoveredReset && !resetSpinning ? 'true' : 'false'}
8340
10700
  >
@@ -8375,11 +10735,13 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8375
10735
  // icon hover-affordance arc (zoom-out / zoom-in / reset /
8376
10736
  // fullscreen now all carry an icon-level hover gesture in
8377
10737
  // addition to the bg hover).
8378
- className={`group p-1.5 rounded-md border transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 ${
10738
+ // R400: hover translateY(-1px) lift — see reset button above for family doc.
10739
+ 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 ${
8379
10740
  isFullscreen
8380
10741
  ? 'bg-cyan-500/15 text-cyan-300 font-medium hover:bg-cyan-500/20 active:bg-cyan-500/25'
8381
10742
  : 'hover:bg-cyan-500/5 active:bg-cyan-500/15'
8382
10743
  }${chromePopping === 'fullscreen' ? ' anet-chrome-pop' : ''}`}
10744
+ data-topo-chrome-fullscreen-hover-lift="true"
8383
10745
  style={{
8384
10746
  borderColor: pal.containerBorder,
8385
10747
  ...(isFullscreen
@@ -8402,12 +10764,20 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8402
10764
  gpu hint promotes the svg to its own compositor layer
8403
10765
  for crisper edges during the scale tween. Closes the
8404
10766
  chrome-strip per-icon hover-affordance arc. */}
10767
+ {/* R455 — fullscreen ENTER + EXIT icons pick up the same
10768
+ group-hover:[stroke-width:2.8] family lift as the
10769
+ zoom +/− icons (R454) and chrome reset icon (R453).
10770
+ Chrome icon hover sw lift family now 6 anchors —
10771
+ R208/R443 runtime badge + R453/R454-zoom-out/zoom-in
10772
+ + R455 fullscreen (this round). transition-[transform,
10773
+ stroke-width] expands existing transition-transform
10774
+ so the sw lift eases under R352 scale-110 cadence. */}
8405
10775
  {isFullscreen ? (
8406
- <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">
10776
+ <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,stroke-width] duration-200 ease-out group-hover:scale-110 group-hover:[stroke-width:2.8] transform-gpu" data-topo-chrome-fullscreen-icon="exit">
8407
10777
  <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" />
8408
10778
  </svg>
8409
10779
  ) : (
8410
- <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">
10780
+ <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,stroke-width] duration-200 ease-out group-hover:scale-110 group-hover:[stroke-width:2.8] transform-gpu" data-topo-chrome-fullscreen-icon="enter">
8411
10781
  <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" />
8412
10782
  </svg>
8413
10783
  )}