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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (263) 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/server/app/_global-error.html +1 -1
  6. package/.next/server/app/_global-error.rsc +1 -1
  7. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  8. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  9. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  12. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  13. package/.next/server/app/_not-found.html +2 -2
  14. package/.next/server/app/_not-found.rsc +12 -12
  15. package/.next/server/app/_not-found.segments/_full.segment.rsc +12 -12
  16. package/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  17. package/.next/server/app/_not-found.segments/_index.segment.rsc +7 -7
  18. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  19. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  20. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  21. package/.next/server/app/admin/page_client-reference-manifest.js +1 -1
  22. package/.next/server/app/admin.html +2 -2
  23. package/.next/server/app/admin.rsc +14 -14
  24. package/.next/server/app/admin.segments/_full.segment.rsc +14 -14
  25. package/.next/server/app/admin.segments/_head.segment.rsc +4 -4
  26. package/.next/server/app/admin.segments/_index.segment.rsc +7 -7
  27. package/.next/server/app/admin.segments/_tree.segment.rsc +2 -2
  28. package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +4 -4
  29. package/.next/server/app/admin.segments/admin.segment.rsc +3 -3
  30. package/.next/server/app/index.html +2 -2
  31. package/.next/server/app/index.rsc +14 -14
  32. package/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
  33. package/.next/server/app/index.segments/_full.segment.rsc +14 -14
  34. package/.next/server/app/index.segments/_head.segment.rsc +4 -4
  35. package/.next/server/app/index.segments/_index.segment.rsc +7 -7
  36. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  37. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  38. package/.next/server/app/login.html +2 -2
  39. package/.next/server/app/login.rsc +14 -14
  40. package/.next/server/app/login.segments/_full.segment.rsc +14 -14
  41. package/.next/server/app/login.segments/_head.segment.rsc +4 -4
  42. package/.next/server/app/login.segments/_index.segment.rsc +7 -7
  43. package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
  44. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +4 -4
  45. package/.next/server/app/login.segments/login.segment.rsc +3 -3
  46. package/.next/server/app/logs/page_client-reference-manifest.js +1 -1
  47. package/.next/server/app/logs.html +2 -2
  48. package/.next/server/app/logs.rsc +14 -14
  49. package/.next/server/app/logs.segments/_full.segment.rsc +14 -14
  50. package/.next/server/app/logs.segments/_head.segment.rsc +4 -4
  51. package/.next/server/app/logs.segments/_index.segment.rsc +7 -7
  52. package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
  53. package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +4 -4
  54. package/.next/server/app/logs.segments/logs.segment.rsc +3 -3
  55. package/.next/server/app/messages/page_client-reference-manifest.js +1 -1
  56. package/.next/server/app/messages.html +2 -2
  57. package/.next/server/app/messages.rsc +14 -14
  58. package/.next/server/app/messages.segments/_full.segment.rsc +14 -14
  59. package/.next/server/app/messages.segments/_head.segment.rsc +4 -4
  60. package/.next/server/app/messages.segments/_index.segment.rsc +7 -7
  61. package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
  62. package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +4 -4
  63. package/.next/server/app/messages.segments/messages.segment.rsc +3 -3
  64. package/.next/server/app/node/page_client-reference-manifest.js +1 -1
  65. package/.next/server/app/node.html +2 -2
  66. package/.next/server/app/node.rsc +14 -14
  67. package/.next/server/app/node.segments/_full.segment.rsc +14 -14
  68. package/.next/server/app/node.segments/_head.segment.rsc +4 -4
  69. package/.next/server/app/node.segments/_index.segment.rsc +7 -7
  70. package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
  71. package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +4 -4
  72. package/.next/server/app/node.segments/node.segment.rsc +3 -3
  73. package/.next/server/app/nodes/page_client-reference-manifest.js +1 -1
  74. package/.next/server/app/nodes.html +2 -2
  75. package/.next/server/app/nodes.rsc +14 -14
  76. package/.next/server/app/nodes.segments/_full.segment.rsc +14 -14
  77. package/.next/server/app/nodes.segments/_head.segment.rsc +4 -4
  78. package/.next/server/app/nodes.segments/_index.segment.rsc +7 -7
  79. package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
  80. package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +4 -4
  81. package/.next/server/app/nodes.segments/nodes.segment.rsc +3 -3
  82. package/.next/server/app/page_client-reference-manifest.js +1 -1
  83. package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
  84. package/.next/server/app/server-logs.html +2 -2
  85. package/.next/server/app/server-logs.rsc +14 -14
  86. package/.next/server/app/server-logs.segments/_full.segment.rsc +14 -14
  87. package/.next/server/app/server-logs.segments/_head.segment.rsc +4 -4
  88. package/.next/server/app/server-logs.segments/_index.segment.rsc +7 -7
  89. package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
  90. package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +4 -4
  91. package/.next/server/app/server-logs.segments/server-logs.segment.rsc +3 -3
  92. package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
  93. package/.next/server/app/settings/networks.html +2 -2
  94. package/.next/server/app/settings/networks.rsc +14 -14
  95. package/.next/server/app/settings/networks.segments/_full.segment.rsc +14 -14
  96. package/.next/server/app/settings/networks.segments/_head.segment.rsc +4 -4
  97. package/.next/server/app/settings/networks.segments/_index.segment.rsc +7 -7
  98. package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
  99. package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +4 -4
  100. package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +3 -3
  101. package/.next/server/app/settings/networks.segments/settings.segment.rsc +3 -3
  102. package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  103. package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
  104. package/.next/server/app/settings/tokens.html +2 -2
  105. package/.next/server/app/settings/tokens.rsc +14 -14
  106. package/.next/server/app/settings/tokens.segments/_full.segment.rsc +14 -14
  107. package/.next/server/app/settings/tokens.segments/_head.segment.rsc +4 -4
  108. package/.next/server/app/settings/tokens.segments/_index.segment.rsc +7 -7
  109. package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
  110. package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +4 -4
  111. package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +3 -3
  112. package/.next/server/app/settings/tokens.segments/settings.segment.rsc +3 -3
  113. package/.next/server/app/settings.html +2 -2
  114. package/.next/server/app/settings.rsc +14 -14
  115. package/.next/server/app/settings.segments/_full.segment.rsc +14 -14
  116. package/.next/server/app/settings.segments/_head.segment.rsc +4 -4
  117. package/.next/server/app/settings.segments/_index.segment.rsc +7 -7
  118. package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  119. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
  120. package/.next/server/app/settings.segments/settings.segment.rsc +3 -3
  121. package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
  122. package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
  123. package/.next/server/app/tasks.html +2 -2
  124. package/.next/server/app/tasks.rsc +14 -14
  125. package/.next/server/app/tasks.segments/_full.segment.rsc +14 -14
  126. package/.next/server/app/tasks.segments/_head.segment.rsc +4 -4
  127. package/.next/server/app/tasks.segments/_index.segment.rsc +7 -7
  128. package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
  129. package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +4 -4
  130. package/.next/server/app/tasks.segments/tasks.segment.rsc +3 -3
  131. package/.next/server/chunks/ssr/[root-of-the-server]__0sv~g.o._.js +1 -1
  132. package/.next/server/chunks/ssr/[root-of-the-server]__0sv~g.o._.js.map +1 -1
  133. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js +4 -4
  134. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js.map +1 -1
  135. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js +1 -1
  136. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js.map +1 -1
  137. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js +1 -1
  138. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js.map +1 -1
  139. package/.next/server/chunks/ssr/agent-network-dashboard_app_components_0mvyi-4._.js +1 -1
  140. package/.next/server/chunks/ssr/agent-network-dashboard_app_components_0mvyi-4._.js.map +1 -1
  141. package/.next/server/middleware-build-manifest.js +3 -3
  142. package/.next/server/pages/404.html +2 -2
  143. package/.next/server/pages/500.html +1 -1
  144. package/.next/static/chunks/{06kqj2vd..gul.js → 00l83otlv3g2v.js} +1 -1
  145. package/.next/static/chunks/{03a4--7ncekmk.js → 0v4-5tng.uh.7.js} +2 -2
  146. package/.next/static/chunks/0yu6gfglhtom..js +1 -0
  147. package/.next/static/chunks/11din0k9_2~y3.js +1 -0
  148. package/.next/static/chunks/12yvfwnp519~5.js +4 -0
  149. package/.next/static/chunks/15w3g1wnq9pee.css +2 -0
  150. package/.next/trace +2 -2
  151. package/.next/trace-build +1 -1
  152. package/app/components/ServersDrawer.tsx +16 -3
  153. package/app/components/TopoGraph.tsx +2958 -137
  154. package/app/globals.css +58 -7
  155. package/package.json +4 -4
  156. package/scripts/p157-servers-copy-test.mjs +95 -0
  157. package/scripts/topo-active-chrome-hover-text-test.mjs +107 -0
  158. package/scripts/topo-alias-glow-test.mjs +121 -0
  159. package/scripts/topo-avatar-brightness-test.mjs +116 -0
  160. package/scripts/topo-avatar-fallback-hover-test.mjs +104 -0
  161. package/scripts/topo-brand-logo-breath-test.mjs +102 -0
  162. package/scripts/topo-brand-logo-hover-brightness-test.mjs +105 -0
  163. package/scripts/topo-brand-logo-hover-rotate-test.mjs +93 -0
  164. package/scripts/topo-brand-logo-hover-test.mjs +85 -0
  165. package/scripts/topo-chip-row-digit-ls-test.mjs +135 -0
  166. package/scripts/topo-chip-row-member-alias-lit-test.mjs +154 -0
  167. package/scripts/topo-chip-row-tier-glow-brightness-test.mjs +99 -0
  168. package/scripts/topo-chip-row-unit-hover-tracking-test.mjs +124 -0
  169. package/scripts/topo-cluster-count-attr-test.mjs +80 -0
  170. package/scripts/topo-crescent-breath-test.mjs +104 -0
  171. package/scripts/topo-crescent-recede-test.mjs +111 -0
  172. package/scripts/topo-edge-badge-hover-glow-test.mjs +90 -0
  173. package/scripts/topo-edge-badge-text-brightness-test.mjs +83 -0
  174. package/scripts/topo-edge-particle-brightness-test.mjs +82 -0
  175. package/scripts/topo-edge-pill-glow-test.mjs +67 -0
  176. package/scripts/topo-edge-visible-brightness-test.mjs +84 -0
  177. package/scripts/topo-endpoint-ring-brightness-test.mjs +83 -0
  178. package/scripts/topo-filter-pill-glow-test.mjs +90 -0
  179. package/scripts/topo-fleet-density-tier-test.mjs +84 -0
  180. package/scripts/topo-flow-rail-brightness-test.mjs +80 -0
  181. package/scripts/topo-freshness-chip-fade-test.mjs +105 -0
  182. package/scripts/topo-fullscreen-attr-test.mjs +73 -0
  183. package/scripts/topo-fullscreen-brightness-test.mjs +84 -0
  184. package/scripts/topo-fullscreen-icon-rotate-test.mjs +93 -0
  185. package/scripts/topo-grid-content-bottom-attr-test.mjs +72 -0
  186. package/scripts/topo-group-box-brightness-test.mjs +84 -0
  187. package/scripts/topo-group-label-brightness-test.mjs +84 -0
  188. package/scripts/topo-group-label-hover-glow-test.mjs +86 -0
  189. package/scripts/topo-group-label-member-alias-hover-test.mjs +125 -0
  190. package/scripts/topo-group-pill-glow-test.mjs +76 -0
  191. package/scripts/topo-hub-digit-brightness-test.mjs +79 -0
  192. package/scripts/topo-hub-digit-ls-test.mjs +119 -0
  193. package/scripts/topo-hub-halo-brightness-test.mjs +80 -0
  194. package/scripts/topo-hub-halo-glow-test.mjs +96 -0
  195. package/scripts/topo-hub-highlight-amplify-test.mjs +139 -0
  196. package/scripts/topo-hub-highlight-brightness-test.mjs +84 -0
  197. package/scripts/topo-hub-highlight-fill-transition-test.mjs +84 -0
  198. package/scripts/topo-hub-highlight-glow-test.mjs +99 -0
  199. package/scripts/topo-hub-highlight-r-test.mjs +112 -0
  200. package/scripts/topo-hub-highlight-recede-test.mjs +144 -0
  201. package/scripts/topo-hub-highlight-theme-fill-test.mjs +83 -0
  202. package/scripts/topo-hub-hover-ring-brightness-test.mjs +79 -0
  203. package/scripts/topo-hub-hover-ring-glow-test.mjs +97 -0
  204. package/scripts/topo-hub-idle-breath-test.mjs +7 -2
  205. package/scripts/topo-hub-recede-test.mjs +124 -0
  206. package/scripts/topo-hub-spoke-brightness-test.mjs +77 -0
  207. package/scripts/topo-hub-spoke-glow-test.mjs +112 -0
  208. package/scripts/topo-layout-hover-fw-test.mjs +98 -0
  209. package/scripts/topo-layout-toggle-brightness-test.mjs +94 -0
  210. package/scripts/topo-legend-count-brightness-test.mjs +80 -0
  211. package/scripts/topo-legend-count-letter-spacing-test.mjs +108 -0
  212. package/scripts/topo-legend-label-fw-test.mjs +107 -0
  213. package/scripts/topo-legend-row-count-brightness-test.mjs +85 -0
  214. package/scripts/topo-legend-row-label-glow-test.mjs +102 -0
  215. package/scripts/topo-legend-swatch-glow-test.mjs +109 -0
  216. package/scripts/topo-legend-swatch-member-alias-match-test.mjs +139 -0
  217. package/scripts/topo-minimap-hover-glow-test.mjs +109 -0
  218. package/scripts/topo-more-footer-brightness-test.mjs +94 -0
  219. package/scripts/topo-node-alias-brightness-test.mjs +84 -0
  220. package/scripts/topo-node-sub-text-brightness-test.mjs +88 -0
  221. package/scripts/topo-nodesize-hover-fw-test.mjs +99 -0
  222. package/scripts/topo-orphan-box-dash-test.mjs +89 -0
  223. package/scripts/topo-orphan-fill-opacity-test.mjs +91 -0
  224. package/scripts/topo-orphan-label-italic-test.mjs +90 -0
  225. package/scripts/topo-orphan-label-opacity-test.mjs +98 -0
  226. package/scripts/topo-panel-count-hover-ls-test.mjs +87 -0
  227. package/scripts/topo-panel-row-brightness-test.mjs +116 -0
  228. package/scripts/topo-panel-title-brightness-test.mjs +98 -0
  229. package/scripts/topo-panel-title-glow-test.mjs +111 -0
  230. package/scripts/topo-pill-x-rotate-test.mjs +96 -0
  231. package/scripts/topo-pinned-aspect-test.mjs +89 -0
  232. package/scripts/topo-pressure-seg-glow-test.mjs +92 -0
  233. package/scripts/topo-pressure-seg-member-alias-match-test.mjs +133 -0
  234. package/scripts/topo-pressure-seg-motion-test.mjs +101 -0
  235. package/scripts/topo-recent-count-brightness-test.mjs +84 -0
  236. package/scripts/topo-recent-more-fw-test.mjs +126 -0
  237. package/scripts/topo-recent-row-fw-test.mjs +115 -0
  238. package/scripts/topo-recent-row-text-glow-test.mjs +86 -0
  239. package/scripts/topo-recent-ts-brightness-test.mjs +86 -0
  240. package/scripts/topo-reduced-motion-attr-test.mjs +69 -0
  241. package/scripts/topo-reset-brightness-test.mjs +83 -0
  242. package/scripts/topo-reset-icon-hover-scale-test.mjs +102 -0
  243. package/scripts/topo-runtime-badge-brightness-test.mjs +78 -0
  244. package/scripts/topo-runtime-badge-glow-test.mjs +108 -0
  245. package/scripts/topo-starfield-hue-test.mjs +109 -0
  246. package/scripts/topo-status-ring-brightness-test.mjs +84 -0
  247. package/scripts/topo-titleblock-h2-hover-fw-test.mjs +109 -0
  248. package/scripts/topo-titleblock-h2-hover-tracking-test.mjs +128 -0
  249. package/scripts/topo-titleblock-kicker-hover-test.mjs +134 -0
  250. package/scripts/topo-vendor-chip-glow-test.mjs +97 -0
  251. package/scripts/topo-vendor-pill-glow-test.mjs +98 -0
  252. package/scripts/topo-watermark-breath-test.mjs +100 -0
  253. package/scripts/topo-watermark-recede-test.mjs +114 -0
  254. package/scripts/topo-zoom-buttons-brightness-test.mjs +94 -0
  255. package/scripts/topo-zoom-level-brightness-test.mjs +83 -0
  256. package/scripts/topo-zoom-level-color-test.mjs +105 -0
  257. package/.next/static/chunks/03tmeg87bxqs3.js +0 -4
  258. package/.next/static/chunks/09t2h9c0b1xjg.js +0 -1
  259. package/.next/static/chunks/0m.1mvl~t.avc.css +0 -2
  260. package/.next/static/chunks/0x3fl360u~cjf.js +0 -1
  261. /package/.next/static/{hbG6dNDvul_c04Di9Ydwt → 0yQcMGGAsOS5-fUxeQdp-}/_buildManifest.js +0 -0
  262. /package/.next/static/{hbG6dNDvul_c04Di9Ydwt → 0yQcMGGAsOS5-fUxeQdp-}/_clientMiddlewareManifest.js +0 -0
  263. /package/.next/static/{hbG6dNDvul_c04Di9Ydwt → 0yQcMGGAsOS5-fUxeQdp-}/_ssgManifest.js +0 -0
@@ -240,11 +240,30 @@ function FreshnessChip({ sessions }: { sessions: unknown }) {
240
240
  stale-onset to direct attention. */
241
241
  if (!stale) return null;
242
242
  return (
243
+ /* Round 505 / Loop — FreshnessChip mount animation. Pre-R505 the
244
+ chip popped into the chip-row instantly when SWR data crossed
245
+ the 10s stale threshold; users saw an abrupt amber pill appear
246
+ mid-row. R505 adds the existing `anet-fade-in` class so the
247
+ chip eases through opacity 0→1 over 150ms (R51 globals.css
248
+ keyframe) on first appearance. The chip itself only renders
249
+ when stale (R275 conditional), so the fade plays exactly when
250
+ the stale signal first arrives — perfectly aligned with the
251
+ semantic. Mount-once via React reconciliation (key not used
252
+ since FreshnessChip is a singleton in the parent).
253
+ a11y respected via R29 blanket — `@media (prefers-reduced-
254
+ motion: reduce)` neutralizes anet-fade-in to `animation:none`
255
+ (globals.css line 1083-1089 includes anet-fade-in in the
256
+ blanket list). Reduced-motion users see the chip pop instantly,
257
+ same as pre-R505 behavior — no regression.
258
+ Pure paint-axis addition (opacity animation, no geometry),
259
+ bbox unchanged. data-freshness-chip-mount-fade attr exposes
260
+ the gate for tests. */
243
261
  <span
244
- className={`${baseClass} ${colorClass}`}
262
+ className={`${baseClass} ${colorClass} anet-fade-in`}
245
263
  title={stale ? `Last sync ${sec}s ago — SWR refresh may be lagging` : `Live data · refreshes every 5s · last sync ${sec}s ago`}
246
264
  data-freshness-chip
247
265
  data-freshness-chip-stale={stale ? 'true' : 'false'}
266
+ data-freshness-chip-mount-fade="true"
248
267
  >
249
268
  {/* Round 272 / Loop: swap prefix word to match color state so
250
269
  text and color point the same way. Pre-R272 the chip read
@@ -867,12 +886,19 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
867
886
  // R63 label render + R86 hover-pin keying + #99 tooltip
868
887
  // member listing, so all the existing group-box machinery
869
888
  // applies uniformly to the orphan bucket too.
889
+ // Round 499 / Loop — surface `isOrphan` flag on the box
890
+ // shape so downstream renderers (label text, future polish)
891
+ // can apply orphan-specific typography (italic) without
892
+ // re-deriving the flag from key === '其他' (key matching
893
+ // would also catch a legitimate "其他" prefix-group, this
894
+ // flag is canonical from the band assignment pass).
870
895
  return {
871
896
  key: band.isOrphan
872
897
  ? '其他'
873
898
  : band.members.length
874
899
  ? groupKeys[band.members[0].alias]
875
900
  : '',
901
+ isOrphan: !!band.isOrphan,
876
902
  count: band.members.length,
877
903
  statuses: { working: w, idle: i, offline: o },
878
904
  x: minX - GROUP_PAD,
@@ -977,7 +1003,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
977
1003
  groupKeys,
978
1004
  // #111: group boxes are a grid-layout feature only — radially scattered
979
1005
  // ring nodes can't be cleanly boxed. Ring keeps the #83 prefix hue.
980
- groupBoxes: [] as { key: string; count: number; statuses: { working: number; idle: number; offline: number }; x: number; y: number; w: number; h: number }[],
1006
+ groupBoxes: [] as { key: string; isOrphan?: boolean; count: number; statuses: { working: number; idle: number; offline: number }; x: number; y: number; w: number; h: number }[],
981
1007
  // ring fits within VIEWBOX_H by construction (offlineRadius=325 + centre at y=330)
982
1008
  gridContentBottom: 0,
983
1009
  };
@@ -1116,6 +1142,12 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1116
1142
  // hover state says — either -8° (still hovering) or 0 (mouse left).
1117
1143
  // 350th-round milestone polish.
1118
1144
  const [hoveredReset, setHoveredReset] = useState(false);
1145
+ // R595: hover state for the chrome fullscreen button — drives the
1146
+ // brightness(1.15) filter on hover, sibling to hoveredReset above.
1147
+ // Same pattern as R593 hoveredZoomLevel + R594 hoveredReset; closes
1148
+ // the standalone chrome button pair (reset + fullscreen) at
1149
+ // brightness parity.
1150
+ const [hoveredFullscreen, setHoveredFullscreen] = useState(false);
1119
1151
  // R135: panel-wide hover-elevation. The recent-signal + legend
1120
1152
  // panels both already host clickable rows (R56/R116 recent rows,
1121
1153
  // R55/R61 legend rows) and a clickable footer (R133), so the
@@ -1699,7 +1731,18 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1699
1731
  width) = 166px total title-block width vs 168px pre-R298 —
1700
1732
  no measurable layout shift, just a deliberate tighter
1701
1733
  grouping. */}
1702
- <div className="flex items-center gap-2.5">
1734
+ {/* Round 554 / Loop — title-block wrapper picks up `group` so
1735
+ the H2 below can subscribe to `group-hover:tracking-tighter`.
1736
+ Pairs with R548/R549 brand-logo hover gestures: cursor
1737
+ sweeping anywhere across the title cluster fires the brand
1738
+ logo's scale-105 + rotate-6 + breath ↔ AND tightens the
1739
+ H2's tracking from -0.025em → -0.05em.
1740
+ Makes the title-block read as one coherent hover cluster —
1741
+ brand mark provides the loud gesture (scale + rotate), H2
1742
+ provides the subtle editorial gesture (kerning tighten).
1743
+ data-topo-section-titleblock-group attr surfaces the gate
1744
+ for tests. */}
1745
+ <div className="group flex items-center gap-2.5" data-topo-section-titleblock-group>
1703
1746
  {/* Round 297 / Loop: brand-logo color picks up the 200ms ease-
1704
1747
  out transition. Pre-R297 the moon glyph had theme-
1705
1748
  conditional color (cyber #67e8f9 cyan ↔ light #0d9488
@@ -1723,13 +1766,136 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1723
1766
  overpowering the h2 at text-lg/font-semibold (R286).
1724
1767
  viewBox 32×32 unchanged so the inner crescent geometry
1725
1768
  scales proportionally. */}
1769
+ {/* Round 548 / Loop — title-block brand logo gains subtle
1770
+ hover-scale gesture. Pre-R548 the 40×40 crescent was
1771
+ fully static — no hover affordance. R548 adds hover:
1772
+ scale-105 (Tailwind 4 emits as `scale: 1.05`) so the
1773
+ brand mark gently responds to attention as the user's
1774
+ cursor sweeps across the title block. 5% scale is
1775
+ intentionally subtle (vs R350 chrome icon hover-scale-
1776
+ 110): the brand logo is a passive identity mark, not
1777
+ an interactive control, so the gesture stays small.
1778
+ cursor: default to clarify non-clickability (the SVG
1779
+ isn't a button; just a brand element).
1780
+ transition-transform duration-200 ease-out matches the
1781
+ dashboard's R350-family hover-scale cadence so the
1782
+ brand logo's response shares the same motion vocabulary
1783
+ as the chrome strip's icon scales. transform-gpu hint
1784
+ promotes the SVG to its own compositor layer for crisp
1785
+ edges during the tween.
1786
+ Brand-mark delight gesture family (1 anchor):
1787
+ R548 title-block brand logo hover:scale-105
1788
+ The crescent at canvas top-left (data-topo-brand-
1789
+ canvas-mark) and the watermark at bottom-left (data-
1790
+ topo-brand-watermark) are intentionally LEFT STATIC —
1791
+ both have pointerEvents:none and exist as ambient
1792
+ decoration with their own breath/recede dynamics
1793
+ (R519/R525/R526/R528). The title-block logo is the
1794
+ ONLY brand surface that's a candidate for hover affordance,
1795
+ since it sits in the chrome-band where the cursor
1796
+ naturally passes during normal use. */}
1797
+ {/* Round 549 / Loop — extends R548 (hover:scale-105) with a
1798
+ subtle hover:rotate-6 rotation. Pairs scale + rotate on
1799
+ the brand mark, same idiom as R547 added to the pill ×
1800
+ close buttons (scale-110 + rotate-12) but at gentler
1801
+ amounts: 105% vs 110% scale and 6° vs 12° rotation —
1802
+ brand marks want restraint vs interactive close buttons.
1803
+
1804
+ The crescent-moon shape (curved cutout via mask) reads
1805
+ visually distinct as it rotates — the asymmetric cutout
1806
+ swings into a fresh angle, telegraphing "this brand mark
1807
+ is alive without being loud". The 6° landing lands the
1808
+ moon's cusp pointing slightly NE rather than straight up,
1809
+ a small but legible reveal of the shape's geometry.
1810
+
1811
+ Tailwind 4 emits BOTH `scale: 1.05` AND `rotate: 6deg`
1812
+ as INDIVIDUAL CSS properties (not a combined transform
1813
+ string — see R547 banked pattern). The className keeps
1814
+ transition-transform so both axes ease at the same 200ms
1815
+ cadence; transform-gpu hint stays so the compositor
1816
+ promotes both axes to GPU layers.
1817
+
1818
+ data-topo-brand-logo-hover-rotate attr surfaces the
1819
+ landing rotation for tests. Pair with R548's
1820
+ data-topo-brand-logo-hover-scale attr — both attrs
1821
+ advertise the dual-axis hover signature. */}
1822
+ {/* Round 553 / Loop — title-block brand logo gains subtle
1823
+ idle opacity breath (~0.92 ↔ 1, 5s ease-in-out cycle).
1824
+ 5th anchor in the 呼吸感 breath family, slotting into
1825
+ the ascending cadence ladder between hub idle (4s) and
1826
+ watermark (6s):
1827
+ row hot 3s
1828
+ hub idle 4s
1829
+ brand logo 5s ← this round
1830
+ watermark 6s
1831
+ crescent 7s
1832
+ Composes cleanly with R548 hover:scale-105 + R549
1833
+ hover:rotate-6 — opacity, scale, and rotate are
1834
+ independent CSS properties; the moon keeps breathing
1835
+ as it scales and rotates on hover. Layered effect
1836
+ reads as "this brand mark is alive even before you
1837
+ touch it, and lights up further on hover".
1838
+ Reduced-motion gate: component-side `!reducedMotion`
1839
+ toggles the className (canonical TopoGraph breath
1840
+ pattern); R29 globals.css blanket provides a
1841
+ defense-in-depth fallback (animation-duration →
1842
+ 0.001ms under prefers-reduced-motion: reduce).
1843
+ data-topo-brand-logo-breath attr exposes the gate
1844
+ state for tests. */}
1845
+ {/* Round 557 / Loop — brand logo gains 4th hover axis:
1846
+ hover:brightness-110 (filter). Adds a chromatic axis
1847
+ to the brand-mark hover signature alongside R548
1848
+ scale, R549 rotate, R553 idle breath:
1849
+ R548 hover:scale-105 transform-scale
1850
+ R549 hover:rotate-6 transform-rotate
1851
+ R553 idle breath (5s) opacity (animation)
1852
+ R557 hover:brightness-110 filter ← this round
1853
+ All 4 axes ride on INDEPENDENT CSS properties (scale,
1854
+ rotate, opacity, filter) — they compose freely without
1855
+ clobbering each other. The cyan/teal crescent gains a
1856
+ soft +10% brightness boost on hover, layered on top of
1857
+ the existing scale + rotate lift + ongoing idle breath.
1858
+ Why +10% (vs more aggressive 125/150): brand mark wants
1859
+ restraint. The eye reads the brightness shift as "this
1860
+ mark lights up under attention" without crossing into
1861
+ "this mark is now glowing".
1862
+ Implementation: className extends transition-transform
1863
+ → transition-[transform,filter] so the brightness
1864
+ tweens at the same 200ms ease-out cadence as the scale
1865
+ + rotate axes — one motion-coherent 3-property hover
1866
+ lift on the className tier (plus the inline color 200ms
1867
+ transition for theme-toggle eases).
1868
+ Brand-mark family axis count: 4 hover-state axes
1869
+ cleanly factor across:
1870
+ geometry (scale + rotate)
1871
+ paint (opacity breath + brightness on hover)
1872
+ Cluster reads as "alive, lifting, and lighting up under
1873
+ attention" — three independent gesture vocabularies on
1874
+ one surface.
1875
+ data-topo-brand-logo-hover-brightness attr surfaces
1876
+ the landing value for tests. */}
1726
1877
  <svg
1727
1878
  width="40" height="40" viewBox="0 0 32 32" aria-hidden
1728
- className="shrink-0"
1879
+ className={`shrink-0 transition-[transform,filter] duration-200 ease-out hover:scale-105 hover:rotate-6 hover:brightness-110 transform-gpu${!reducedMotion ? ' anet-topo-brand-logo-breath' : ''}`}
1729
1880
  data-topo-brand-logo
1881
+ data-topo-brand-logo-hover-scale="1.05"
1882
+ data-topo-brand-logo-hover-rotate="6deg"
1883
+ data-topo-brand-logo-hover-brightness="1.1"
1884
+ data-topo-brand-logo-breath={!reducedMotion ? 'true' : 'false'}
1730
1885
  style={{
1731
1886
  color: isLight ? '#0d9488' : '#67e8f9',
1732
- transition: 'color 200ms ease-out',
1887
+ cursor: 'default',
1888
+ // R557 — extend transition list to include filter (and
1889
+ // re-spec transform for cadence parity) so the new
1890
+ // hover:brightness-110 axis eases at 200ms alongside
1891
+ // the existing color 200ms (theme-toggle ease) and the
1892
+ // className-based hover:scale-105 / hover:rotate-6.
1893
+ // Inline transition is a shorthand and overrides the
1894
+ // className's transition-[transform,filter] — listing
1895
+ // all axes here ensures the eased property set covers
1896
+ // color (theme) + transform (scale + rotate) + filter
1897
+ // (brightness) at uniform 200ms ease-out.
1898
+ transition: 'color 200ms ease-out, transform 200ms ease-out, filter 200ms ease-out',
1733
1899
  }}
1734
1900
  >
1735
1901
  <mask id="s2a-titleblock-moon-mask">
@@ -1777,7 +1943,40 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1777
1943
  R300 marks the milestone of 25 rounds (R275-R300) of
1778
1944
  continuous TopoGraph polish + codex's Vincent 5215/
1779
1945
  5222 logo asset+integration work. */}
1780
- <div className="text-xs uppercase text-gray-500 tracking-widest leading-tight font-medium" data-topo-section-kicker>Network Topology</div>
1946
+ {/* Round 555 / Loop kicker "Network Topology" gains group-
1947
+ hover affordance via the R554 wrapper's `group` flag,
1948
+ closing the title-block cluster's hover coverage at 3
1949
+ surfaces (brand logo + H2 + kicker).
1950
+ Picks up the small-label SPREAD direction (R554 banked
1951
+ "small labels SPREAD on hover / large headlines TIGHTEN"
1952
+ — kicker is xs uppercase, definitely a small label) plus
1953
+ a color brighten (text-gray-500 #6b7280 → text-gray-400
1954
+ #9ca3af).
1955
+ Spread: tracking-widest (0.1em rest) → 0.13em hover —
1956
+ +30% kerning bump. At text-xs (12px) the per-gap shift
1957
+ is 1.2px → 1.56px (+0.36px/gap), legible without
1958
+ overshooting the rest's tracking-widest editorial base.
1959
+ Color: text-gray-500 → text-gray-400 — one tier lighter,
1960
+ same idiom as R296 (kicker rest tone-up from gray-600 to
1961
+ gray-500), now extended at the hover-state tier.
1962
+ transition-[letter-spacing,color] duration-200 ease-out
1963
+ matches the 200ms cadence of R554 H2 ls + the rest of
1964
+ the hover-ls family (R344/R345/R347/R351/R420/R427/R431/
1965
+ R432/R434/R527/R539).
1966
+ Title-block cluster signature post-R555 (3 surfaces):
1967
+ brand logo loud scale + rotate + breath
1968
+ (R548/R549/R553)
1969
+ H2 subtle tracking-tighter
1970
+ (R554, editorial-tighten)
1971
+ kicker subtle tracking-spread + color lift
1972
+ (R555, data-spread) ← this round
1973
+ Two of the three surfaces are typographic; the brand
1974
+ logo carries the geometric+chromatic motion. Cluster
1975
+ reads as ONE coherent hover unit through three
1976
+ independent gesture vocabularies.
1977
+ data-topo-section-kicker-hover-tracking + -hover-color
1978
+ attrs expose the landing values for tests. */}
1979
+ <div className="text-xs uppercase text-gray-500 group-hover:text-gray-400 tracking-widest group-hover:tracking-[0.13em] transition-[letter-spacing,color] duration-200 ease-out leading-tight font-medium" data-topo-section-kicker data-topo-section-kicker-hover-tracking="0.13em" data-topo-section-kicker-hover-color="text-gray-400">Network Topology</div>
1781
1980
  {/* Round 286 / Loop: title 'Command mesh' adopts tracking-tight
1782
1981
  (-0.025em) to complement R285 kicker tracking-widest. Wide
1783
1982
  eyebrow + tight headline is the conventional editorial
@@ -1789,7 +1988,62 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1789
1988
  cumulatively legible across 12 characters. font-semibold
1790
1989
  (600) stays — tracking-tight does the heavy lifting for
1791
1990
  the editorial register. */}
1792
- <h2 className="text-lg text-white font-semibold leading-tight tracking-tight" data-topo-section-title>Command mesh</h2>
1991
+ {/* Round 554 / Loop — H2 "Command mesh" gains group-hover-
1992
+ gated tracking-tighter via the wrapper's R554 `group`
1993
+ flag. Pre-R554 the H2 was fully static (R286 tracking-
1994
+ tight only at rest). R554 adds an editorial-tighten
1995
+ gesture: when cursor sweeps anywhere across the title
1996
+ cluster (brand logo OR title text), the headline tightens
1997
+ from -0.025em → -0.05em.
1998
+ Inverts the typical hover-letter-spacing direction:
1999
+ small labels (chip counts, panel titles, edge digits)
2000
+ SPREAD on hover → "data telegraphing"
2001
+ large headlines (h2 Command mesh)
2002
+ TIGHTEN on hover → "editorial emphasis"
2003
+ Both directions are coherent design language — small
2004
+ data wants spacing for legibility; large headlines want
2005
+ tightening for designed-headline polish. Same idiom as
2006
+ the conventional editorial pairing of "wide kicker +
2007
+ tight headline" R285/R286 set up (kicker spreads 0.1em
2008
+ tracking-widest; headline tightens -0.025em tracking-
2009
+ tight) — R554 deepens that pairing's tighten side at
2010
+ the hover-state tier.
2011
+ At text-lg (18px) the shift is -0.45px → -0.9px per
2012
+ gap (~5.4px total tightening across "Command mesh" 12
2013
+ chars). Subtle but legible when the cursor sweeps in.
2014
+ transition-[letter-spacing] duration-200 ease-out
2015
+ matches the 200ms hover-ls cadence used at R344/R345/
2016
+ R347/R351/R420/R427/R431/R432/R434/R527/R539 family
2017
+ anchors.
2018
+ data-topo-section-title-hover-tracking attr surfaces
2019
+ the landing tracking class for tests. */}
2020
+ {/* Round 556 / Loop — H2 "Command mesh" gains a 2nd
2021
+ editorial-emphasis axis: group-hover:font-bold paired
2022
+ with R554's group-hover:tracking-tighter. Both lifts
2023
+ fire on the same R554 wrapper's `group` flag (hover
2024
+ anywhere in the title cluster → BOTH H2 axes intensify
2025
+ simultaneously).
2026
+ H2 hover signature post-R556 (2 typographic axes
2027
+ intensify together):
2028
+ rest font-semibold 600 + tracking-tight -0.025em
2029
+ hover font-bold 700 + tracking-tighter -0.05em
2030
+ Editorial emphasis through TWO axes — heavier AND
2031
+ tighter on hover. Mirrors the conventional "designed-
2032
+ headline emphasis" idiom (heavier + tighter = more
2033
+ authoritative; the eye reads both axes as intensifying
2034
+ the same semantic).
2035
+ Hover-fw family extension (6 anchors now):
2036
+ R416 chip-row count digit (chip group-hover)
2037
+ R420 chrome zoom-level (hover)
2038
+ R425 hub-center digit (hub hover)
2039
+ R520 +N more flows footer (recent panel hover)
2040
+ R521 chrome nodeSize S/M/L (inactive hover)
2041
+ R556 title-block H2 (cluster group-hover) ← this round
2042
+ Transition list extends to include 'font-weight 200ms
2043
+ ease-out' alongside the existing 'letter-spacing'
2044
+ 200ms cadence. data-topo-section-title-hover-fw attr
2045
+ surfaces the landing weight for tests. */}
2046
+ <h2 className="text-lg text-white font-semibold group-hover:font-bold leading-tight tracking-tight group-hover:tracking-tighter transition-[letter-spacing,font-weight] duration-200 ease-out" data-topo-section-title data-topo-section-title-hover-tracking="tracking-tighter" data-topo-section-title-hover-fw="700">Command mesh</h2>
1793
2047
  </div>
1794
2048
  </div>
1795
2049
  {/* Round 328 / Loop: chip-row strip wrapper gap 2 → 2.5
@@ -1959,8 +2213,75 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1959
2213
  // overlays the release-pop. Matching `transform-gpu`
1960
2214
  // promotes the layer so the scale doesn't trigger
1961
2215
  // layout/paint thrash. Sibling change on Grid below.
1962
- className={`px-2.5 py-1 focus:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset hover:tracking-wide active:scale-95 transform-gpu ${layout === 'ring' ? 'bg-cyan-500/15 text-cyan-300 font-medium hover:bg-cyan-500/20 active:bg-cyan-500/25' : 'text-gray-400 hover:text-cyan-300 hover:bg-cyan-500/5 active:bg-cyan-500/15'} ${chromePopping === 'layout-ring' ? ' anet-chrome-pop' : ''}`}
1963
- style={{ transition: 'background-color 150ms ease, color 150ms ease, letter-spacing 200ms ease-out, transform 150ms ease-out' }}
2216
+ /* Round 522 / Loop extends R521's typography-preview
2217
+ idiom (chrome nodeSize hover:font-medium 400 500) to
2218
+ the Ring/Grid layout toggle's inactive variant. Pre-
2219
+ R522 the inactive Ring/Grid had `hover:text-cyan-300
2220
+ hover:bg-cyan-500/5` (R270 color + bg previews of the
2221
+ active state) but no typography preview — the active
2222
+ variant uses `font-medium` (fw 500), inactive sat at
2223
+ default fw 400 even on hover. R522 adds `hover:font-
2224
+ medium` to the inactive Ring/Grid so the rest-vs-hover
2225
+ transition previews the typography state the click
2226
+ would commit to, matching the click commits's locked
2227
+ weight.
2228
+ font-weight 150ms appended to the transition list
2229
+ matching the existing 150ms color/bg cadence at this
2230
+ button — when hover lifts color (gray-400 → cyan-300)
2231
+ + bg (transparent → cyan-500/5) + fw (400 → 500), all
2232
+ 3 ease at the same 150ms beat.
2233
+ Hover-fw family extension (6 anchors): R416/R420/R425/
2234
+ R520/R521/R522. R522 closes the chrome toggle group
2235
+ typography preview at the last remaining toggle —
2236
+ layout (Ring/Grid). After R521 (nodeSize) + R522
2237
+ (layout), every multi-state chrome toggle has hover-
2238
+ fw preview on its inactive variant.
2239
+ data-topo-chrome-layout-hover-preview-fw="500" attr
2240
+ on inactive button exposes the polish for tests. */
2241
+ /* Round 552 / Loop — chrome active-variant gains hover:
2242
+ text-cyan-200, lifting text one brightness tier alongside
2243
+ the existing hover:bg-cyan-500/20 bg deepen. Coordinated
2244
+ 4-anchor edit (replace_all touched 4 sibling lines sharing
2245
+ the identical active-variant className substring):
2246
+ Ring (this line) layout === 'ring'
2247
+ Grid (line ~2097) layout === 'grid'
2248
+ S/M/L (line ~12635) nodeScale === v
2249
+ Fscrn (line ~13030) isFullscreen
2250
+ Pre-R552 the active variant's hover state only deepened bg
2251
+ (cyan-500/15 → /20); text stayed planted at cyan-300. The
2252
+ inactive variant already lifts text on hover (text-gray-400
2253
+ → text-cyan-300). R552 brings parity: active variant lifts
2254
+ text one tier brighter (cyan-300 → cyan-200) on hover,
2255
+ mirroring the inactive variant's "text brightens on hover"
2256
+ gesture at the next brightness step.
2257
+ Brightness ladder snapshot (cyan):
2258
+ cyan-400 brand chrome focus ring
2259
+ cyan-300 active-variant rest ←─┐
2260
+ │ +1 tier on hover
2261
+ cyan-200 active-variant hover ←─┘ (this round)
2262
+ Pure paint axis (text color); bbox/geometry unchanged.
2263
+ transition-colors already in the class list so the cyan-
2264
+ 300 → cyan-200 swap eases at the existing 200ms cadence.
2265
+ hover-color brighten family extension at the chrome strip
2266
+ active-variant scope; sibling to the inactive variant's
2267
+ R163/R178/R179/R270 hover:text-cyan-300 idiom. */
2268
+ className={`px-2.5 py-1 focus:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset hover:tracking-wide hover:brightness-[1.15] active:scale-95 transform-gpu ${layout === 'ring' ? 'bg-cyan-500/15 text-cyan-300 font-medium hover:bg-cyan-500/20 hover:text-cyan-200 active:bg-cyan-500/25' : 'text-gray-400 hover:text-cyan-300 hover:bg-cyan-500/5 active:bg-cyan-500/15 hover:font-medium'} ${chromePopping === 'layout-ring' ? ' anet-chrome-pop' : ''}`}
2269
+ data-topo-chrome-layout-hover-preview-fw={layout === 'ring' ? null : '500'}
2270
+ data-topo-chrome-layout-ring-brightness-hover="1.15"
2271
+ /* R597 — Ring/Grid segmented buttons gain hover:
2272
+ brightness-[1.15] (37+38th anchors in per-element
2273
+ brightness family, 6+7th HTML). Paired-anchor round
2274
+ mirroring R596's zoom +/- closure at the second
2275
+ segmented control. Same segmented-unity rule
2276
+ (brightness = pure paint, no geometry break).
2277
+ Inline transition list extends with 'filter 150ms
2278
+ ease' so brightness eases under the existing R522
2279
+ 150ms beat that bg/color/fw share (letter-spacing
2280
+ + transform on their own 200ms/150ms cadences).
2281
+ R557 banked pattern: when an element has both
2282
+ inline transition AND new transition-driven axis,
2283
+ extend the INLINE list — not the className. */
2284
+ style={{ transition: 'background-color 150ms ease, color 150ms ease, letter-spacing 200ms ease-out, transform 150ms ease-out, font-weight 150ms ease, filter 150ms ease' }}
1964
2285
  >
1965
2286
  Ring
1966
2287
  </button>
@@ -1982,7 +2303,13 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1982
2303
  // R492 sibling — Grid button picks up active:scale-95
1983
2304
  // press feedback + transform in transition list. Same
1984
2305
  // vocabulary as Ring above.
1985
- className={`px-2.5 py-1 border-l focus:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset hover:tracking-wide active:scale-95 transform-gpu ${layout === 'grid' ? 'bg-cyan-500/15 text-cyan-300 font-medium hover:bg-cyan-500/20 active:bg-cyan-500/25' : 'text-gray-400 hover:text-cyan-300 hover:bg-cyan-500/5 active:bg-cyan-500/15'} ${chromePopping === 'layout-grid' ? ' anet-chrome-pop' : ''}`}
2306
+ /* Round 522 sibling Grid button mirrors Ring above:
2307
+ inactive variant gains `hover:font-medium` typography
2308
+ preview + font-weight 150ms in inline transition list.
2309
+ Same idiom, same family (R522 chrome layout). */
2310
+ className={`px-2.5 py-1 border-l focus:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset hover:tracking-wide hover:brightness-[1.15] active:scale-95 transform-gpu ${layout === 'grid' ? 'bg-cyan-500/15 text-cyan-300 font-medium hover:bg-cyan-500/20 hover:text-cyan-200 active:bg-cyan-500/25' : 'text-gray-400 hover:text-cyan-300 hover:bg-cyan-500/5 active:bg-cyan-500/15 hover:font-medium'} ${chromePopping === 'layout-grid' ? ' anet-chrome-pop' : ''}`}
2311
+ data-topo-chrome-layout-hover-preview-fw={layout === 'grid' ? null : '500'}
2312
+ data-topo-chrome-layout-grid-brightness-hover="1.15"
1986
2313
  /* Round 268 / Loop: Grid button's left border (the
1987
2314
  internal divider between Ring and Grid) picks up
1988
2315
  pal.containerBorder, matching the wrapper change at
@@ -1994,8 +2321,11 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1994
2321
  the border-color flip — border-color 200ms ease-out
1995
2322
  keeps R268's theme-toggle smoothness intact.
1996
2323
  R492 adds `transform 150ms ease-out` so active:scale-95
1997
- eases smoothly. */
1998
- style={{ borderColor: pal.containerBorder, transition: 'background-color 150ms ease, color 150ms ease, border-color 200ms ease-out, letter-spacing 200ms ease-out, transform 150ms ease-out' }}
2324
+ eases smoothly.
2325
+ R597 sibling Grid button mirrors Ring above:
2326
+ hover:brightness-[1.15] + filter 150ms ease in
2327
+ the inline transition list. */
2328
+ style={{ borderColor: pal.containerBorder, transition: 'background-color 150ms ease, color 150ms ease, border-color 200ms ease-out, letter-spacing 200ms ease-out, transform 150ms ease-out, font-weight 150ms ease, filter 150ms ease' }}
1999
2329
  >
2000
2330
  Grid
2001
2331
  </button>
@@ -2028,6 +2358,41 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2028
2358
  // lists once for both chips to share.
2029
2359
  const workingAliases = onlineNodes.filter(s => s.status === 'working').map(s => s.alias);
2030
2360
  const onlineAliases = onlineNodes.map(s => s.alias);
2361
+ /* Round 565 (50-round milestone) / Loop — extend the
2362
+ inspection-overrides-encoding family to a 7th anchor at
2363
+ the chip-row chip scope. Computes the hovered alias's
2364
+ status tier (same idiom as R562/R563) so each chip's
2365
+ className can include the "lit" bg/border treatment when
2366
+ operator hovers a node matching its tier.
2367
+ Family progression — 7 anchors complete:
2368
+ R484 recent-row timestamp alias hover
2369
+ R485 edge particle opacity alias hover
2370
+ R486 minimap dot opacity alias hover
2371
+ R561 group-label + ants-gate member-alias hover
2372
+ R562 legend-swatch r + glow member-alias status match
2373
+ R563 pressure-seg brightness member-alias status match
2374
+ R565 chip-row chip bg/border member-alias status match ← this round
2375
+ Status-tier-match feedback now SATURATES across panel
2376
+ chrome at 4 surfaces simultaneously:
2377
+ minimap dot (R486)
2378
+ legend swatch (R562)
2379
+ pressure-seg (R563)
2380
+ chip-row chip (R565) ← this round
2381
+ When operator hovers a 'working' node alias, ALL FOUR
2382
+ surfaces light up in green; 'idle' → all four in teal;
2383
+ 'offline' → all four in slate. The eye gets 4-way
2384
+ confirmation of "your inspected node is in this tier"
2385
+ across every persistent status-reference surface. */
2386
+ const hoveredAliasTierKey: 'working' | 'idle' | 'offline' | null = (() => {
2387
+ if (!hoveredAlias) return null;
2388
+ const s = onlineNodes.find(n => n.alias === hoveredAlias)
2389
+ ?? offlineNodes.find(n => n.alias === hoveredAlias);
2390
+ if (!s) return null;
2391
+ if (s.status === 'working') return 'working';
2392
+ return offlineNodes.includes(s) ? 'offline' : 'idle';
2393
+ })();
2394
+ const isWorkingChipLit = hoveredAliasTierKey === 'working';
2395
+ const isOnlineChipLit = hoveredAliasTierKey === 'idle';
2031
2396
  const truncate = (list: string[]) => {
2032
2397
  const head = list.slice(0, 8).join(', ');
2033
2398
  const tail = list.length > 8 ? ` + ${list.length - 8} more` : '';
@@ -2120,13 +2485,19 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2120
2485
  // R398 hover-lift conditional. Composes with hover:-
2121
2486
  // translate-y-px for the same lift-and-compress
2122
2487
  // tactile signature R493 brought to reset/fullscreen.
2488
+ /* R565: when isWorkingChipLit (operator hovers a working
2489
+ node), chip stays in its "lit" bg-green-500/15 +
2490
+ border-green-500/30 state at rest. Same visual as
2491
+ hover; member-alias-matching pins the lift without
2492
+ requiring cursor on the chip. */
2123
2493
  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 ${
2124
2494
  workingCount > 0
2125
- ? '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 active:scale-95'
2495
+ ? `${isWorkingChipLit ? 'bg-green-500/15 border-green-500/30' : 'bg-green-500/10 border-green-500/20'} text-green-300 hover:bg-green-500/15 hover:border-green-500/30 hover:-translate-y-px active:scale-95`
2126
2496
  : 'bg-green-500/10 text-green-300 border-green-500/20'
2127
2497
  }`}
2128
2498
  data-chip-hover-lift={workingCount > 0 ? 'true' : 'false'}
2129
2499
  data-chip-group-hover-brighten="true"
2500
+ data-working-chip-member-alias-lit={isWorkingChipLit ? 'true' : 'false'}
2130
2501
  data-working-chip
2131
2502
  data-working-chip-aliases={workingAliases.join(',')}
2132
2503
  data-pin-mirror={pinnedStatus === 'working' ? 'true' : 'false'}
@@ -2206,7 +2577,26 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2206
2577
  at the chip-count scope. Sibling edits on the
2207
2578
  online + active-links chip digits below. data-
2208
2579
  working-chip-digit attr exposes the digit span. */}
2209
- <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>
2580
+ {/* Round 539 / Loop chip-row digit gains group-hover:
2581
+ tracking-wide alongside the existing R362 group-
2582
+ hover:font-bold. Pre-R539 the chip digit lifted
2583
+ only on the font-weight axis (600 → 700 on chip
2584
+ hover); R539 adds the kerning axis (tracking
2585
+ normal → tracking-wide ≈ 0.025em ≈ 0.3px on a 12px
2586
+ digit) so hover lifts BOTH typography axes
2587
+ together — same idiom R420/R517 establish at the
2588
+ chrome zoom-level (letter-spacing + fontWeight
2589
+ hover delta) and R531/R530 mirror at the panel
2590
+ label scope. transition-[font-weight] extends to
2591
+ transition-[font-weight,letter-spacing] for the
2592
+ smooth dual-axis tween.
2593
+ Sibling treatment across the 3 chip-row digits
2594
+ (working / online / active-links) — single concept
2595
+ replicated at 3 surfaces by replace_all.
2596
+ Hover-letter-spacing family extension (12 anchors
2597
+ now): R344/R345/R347/R420/R427/R431/R432/R433/
2598
+ R434/R517/R518 + R539 (this round). */}
2599
+ <span className="font-semibold transition-[font-weight,letter-spacing] duration-200 group-hover:font-bold group-hover:tracking-wide" data-working-chip-digit>{workingCount}</span><span className="opacity-70 transition-[opacity,letter-spacing] duration-200 group-hover:opacity-100 group-hover:tracking-wide" data-working-chip-unit> working</span>
2210
2600
  </span>
2211
2601
  <span
2212
2602
  // Round 201 / Loop: online chip — mirror of the working
@@ -2225,13 +2615,18 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2225
2615
  // R494 sibling — online chip joins the active:scale-95 press
2226
2616
  // family (gated on onlineNodes.length > 0 clickable branch,
2227
2617
  // same conditional pattern as the working chip above).
2618
+ /* R565: same lit-on-member-alias-match pattern as
2619
+ working chip — online chip routes hover to 'idle'
2620
+ tier (see onMouseEnter below), so its member-alias
2621
+ gate is `hoveredAliasTierKey === 'idle'`. */
2228
2622
  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 ${
2229
2623
  onlineNodes.length > 0
2230
- ? '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 active:scale-95'
2624
+ ? `${isOnlineChipLit ? 'bg-cyan-500/15 border-cyan-500/30' : 'bg-cyan-500/10 border-cyan-500/20'} text-cyan-300 hover:bg-cyan-500/15 hover:border-cyan-500/30 hover:-translate-y-px active:scale-95`
2231
2625
  : 'bg-cyan-500/10 text-cyan-300 border-cyan-500/20'
2232
2626
  }`}
2233
2627
  data-chip-hover-lift={onlineNodes.length > 0 ? 'true' : 'false'}
2234
2628
  data-chip-group-hover-brighten="true"
2629
+ data-online-chip-member-alias-lit={isOnlineChipLit ? 'true' : 'false'}
2235
2630
  data-online-chip
2236
2631
  data-online-chip-aliases={onlineAliases.join(',')}
2237
2632
  data-pin-mirror={pinnedStatus === 'idle' ? 'true' : 'false'}
@@ -2277,7 +2672,9 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2277
2672
  >
2278
2673
  {/* R337 sibling — online chip unit demotion. */}
2279
2674
  {/* R362 sibling — online-chip digit gains font-semibold. */}
2280
- <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>
2675
+ {/* R539 sibling online chip digit. Same idiom as
2676
+ working chip above (group-hover:tracking-wide). */}
2677
+ <span className="font-semibold transition-[font-weight,letter-spacing] duration-200 group-hover:font-bold group-hover:tracking-wide" data-online-chip-digit>{onlineNodes.length}</span><span className="opacity-70 transition-[opacity,letter-spacing] duration-200 group-hover:opacity-100 group-hover:tracking-wide" data-online-chip-unit> online</span>
2281
2678
  </span>
2282
2679
  </>
2283
2680
  );
@@ -2292,6 +2689,31 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2292
2689
  const o = offlineNodes.length;
2293
2690
  const total = w + i + o;
2294
2691
  if (total === 0) return null;
2692
+ /* Round 563 / Loop — inspection-overrides-encoding family
2693
+ 6th anchor at the pressure-bar segment scope. When
2694
+ operator hovers a NODE ALIAS on the canvas, the segment
2695
+ matching that node's status tier lights up with its
2696
+ R210 brightness + R542 drop-shadow treatment — mirror
2697
+ of R562 legend-swatch pattern at the pressure-bar scope.
2698
+ Family progression (6 anchors):
2699
+ R484 recent-row timestamp alias hover
2700
+ R485 edge particle opacity alias hover
2701
+ R486 minimap dot opacity alias hover
2702
+ R561 group-label + ants-gate member-alias hover
2703
+ R562 legend-swatch r + glow member-alias status match
2704
+ R563 pressure-seg brightness member-alias status match ← this round
2705
+ Same status-tier-match computation as R562 (banked
2706
+ idiom): find the hovered alias's session, map to
2707
+ working/idle/offline tier, then per-segment check
2708
+ `hoveredAliasRowKey === key`. Computed once at IIFE
2709
+ scope, used inside the seg() closure. */
2710
+ const hoveredSession = hoveredAlias
2711
+ ? (onlineNodes.find(s => s.alias === hoveredAlias) ?? offlineNodes.find(s => s.alias === hoveredAlias))
2712
+ : null;
2713
+ const hoveredAliasTierKey: 'working' | 'idle' | 'offline' | null = !hoveredSession ? null
2714
+ : hoveredSession.status === 'working' ? 'working'
2715
+ : offlineNodes.includes(hoveredSession) ? 'offline'
2716
+ : 'idle';
2295
2717
  // Round 60 / Loop: each segment toggles a sticky filter via
2296
2718
  // `pinnedStatus`. Click the working segment → all non-working
2297
2719
  // nodes dim; click again → release. Segments share width with
@@ -2304,6 +2726,10 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2304
2726
  const seg = (n: number, color: string, key: 'working' | 'idle' | 'offline', label: string) => {
2305
2727
  if (n === 0) return null;
2306
2728
  const isPinned = pinnedStatus === key;
2729
+ // R563: member-alias-matching flag — when operator hovers
2730
+ // a node alias whose status matches this segment's tier.
2731
+ const isMemberAliasMatching = hoveredAliasTierKey === key;
2732
+ const isSegLit = hoveredStatus === key || isMemberAliasMatching;
2307
2733
  // R102: list the aliases that match this segment's bucket
2308
2734
  // so the title answers WHICH n, not just HOW MANY. Closes
2309
2735
  // the last "info-density gap" in the chip-row surfaces
@@ -2323,6 +2749,8 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2323
2749
  data-pressure-seg={key}
2324
2750
  data-pressure-seg-aliases={matchAliases.join(',')}
2325
2751
  data-pressure-seg-hovered={hoveredStatus === key ? 'true' : 'false'}
2752
+ data-pressure-seg-member-alias-matching={isMemberAliasMatching ? 'true' : 'false'}
2753
+ data-pressure-seg-lit={isSegLit ? 'true' : 'false'}
2326
2754
  role="button"
2327
2755
  tabIndex={0}
2328
2756
  aria-pressed={isPinned}
@@ -2366,7 +2794,47 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2366
2794
  height: '100%',
2367
2795
  cursor: 'pointer',
2368
2796
  boxShadow: isPinned ? `inset 0 0 0 1px ${color}, inset 0 0 0 2px rgba(255,255,255,0.6)` : undefined,
2369
- filter: hoveredStatus === key ? 'brightness(1.2)' : undefined,
2797
+ /* Round 542 / Loop pressure-bar segments gain
2798
+ drop-shadow tier-color glow on hover, stacked
2799
+ on R210 brightness(1.2). Sibling to R537 legend
2800
+ swatch + R541 vendor chip glow at the chip-row
2801
+ scope — three same-pattern surfaces (legend
2802
+ swatch / vendor chip / pressure segment) all
2803
+ radiate their identity color on hover.
2804
+ 3rd anchor in the chip-row tier-color paint
2805
+ glow sub-family:
2806
+ R537 legend swatch row.fill (status hex)
2807
+ R541 vendor chip v.color (hsl via color-mix)
2808
+ R542 pressure seg color (status hex) ← this round
2809
+ Stacked filter syntax (brightness + drop-shadow
2810
+ in same filter declaration): `brightness(1.2)
2811
+ drop-shadow(...)`. CSS filter supports multiple
2812
+ functions; they apply left-to-right. Brightness
2813
+ boosts the segment's own color, drop-shadow
2814
+ paints the outer halo. Together: hovered seg
2815
+ looks "lit up" with both inner glow + outer
2816
+ halo in its tier color.
2817
+ Hue: `${color}99` hex+alpha (60%) — color here
2818
+ is a 6-char hex (e.g., '#22c55e' for working
2819
+ cyber, '#0d9488' for idle light), not hsl, so
2820
+ hex+alpha concat works (unlike R541 vendor
2821
+ which needed color-mix for hsl). Banked
2822
+ pattern: hex sources use hex+alpha; hsl/color()
2823
+ sources use color-mix.
2824
+ 2px blur (vs R537's 3px) since pressure-seg is
2825
+ small (h-2 = 8px tall, variable width) — a
2826
+ smaller blur keeps the glow tight to the
2827
+ segment without bleeding into neighbors.
2828
+ filter is paint-only; bbox unchanged; R51
2829
+ overlap-test invariants hold. Transition list
2830
+ already includes `filter` (post-R524). */
2831
+ /* R563: filter lifts on EITHER direct hover OR member-
2832
+ alias-matching (operator inspecting a node whose
2833
+ status matches this segment's tier). Same R210
2834
+ brightness + R542 drop-shadow value across both
2835
+ gate sources — uniform visual response, distinct
2836
+ semantic gates. */
2837
+ filter: isSegLit ? `brightness(1.2) drop-shadow(0 0 2px ${color}99)` : undefined,
2370
2838
  transition: 'width 220ms ease-out, box-shadow 150ms ease-out, filter 150ms ease-out',
2371
2839
  }}
2372
2840
  onClick={(e) => {
@@ -2511,6 +2979,33 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2511
2979
  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 active:scale-95 transform-gpu" data-topo-filter-pill-hover-lift="true"
2512
2980
  title={matchCount > 0 ? `${matchPreview}${matchSuffix} — click to clear` : 'Click to clear filter'}
2513
2981
  onClick={() => setPinnedStatus(null)}
2982
+ /* Round 543 / Loop — status filter pill gains always-on
2983
+ tier-color drop-shadow when rendered. Pre-R543 the
2984
+ pill carried bg-tint + tier-color text + border but
2985
+ no outer paint extent — it sat as a flat tinted chip
2986
+ in the chip row. R543 adds an outer glow at the
2987
+ pill's text color so the pill radiates a soft tier-
2988
+ colored halo signaling "this filter is active." Pin
2989
+ pill only renders when pinnedStatus is set (the JSX
2990
+ gate above), so the drop-shadow appearing reinforces
2991
+ the visual "active pin" state.
2992
+ Sibling pattern: R477 legend pin-ring also paints a
2993
+ pin-gated tier-color drop-shadow. Pin pill follows
2994
+ the same "pin-gated paint glow" semantics but at the
2995
+ chip-row scope vs the panel-row scope. The chip-row
2996
+ tier-color glow trio (R537/R541/R542 hover-gated)
2997
+ plus R543 (pin-gated, this round) closes the chip-
2998
+ row paint-glow family across BOTH gate types
2999
+ (hover for transient affordance, pin for sticky
3000
+ active-state visual).
3001
+ Hue: explicit tier color (extracted from the
3002
+ existing `color` ternary). 0x99 alpha (~60%) +
3003
+ 3px blur. Stays inside the same color hierarchy
3004
+ as the pill's own text/border (currentColor).
3005
+ R543 status pill scope only — R543's pattern can
3006
+ future-extend to group/vendor/edge filter pills
3007
+ (3 more variants at lines 2683/2755/2824). Out of
3008
+ scope to keep R543 single-pill. */
2514
3009
  style={{
2515
3010
  background: pinnedStatus === 'working' ? (isLight ? '#05966914' : '#22c55e1f')
2516
3011
  : pinnedStatus === 'idle' ? (isLight ? '#0d948814' : '#2dd4bf1f')
@@ -2520,6 +3015,11 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2520
3015
  : (isLight ? '#475569' : '#9ca3af'),
2521
3016
  borderColor: 'currentColor',
2522
3017
  cursor: 'pointer',
3018
+ filter: `drop-shadow(0 0 3px ${
3019
+ pinnedStatus === 'working' ? (isLight ? '#047857' : '#86efac')
3020
+ : pinnedStatus === 'idle' ? (isLight ? '#0f766e' : '#5eead4')
3021
+ : (isLight ? '#475569' : '#9ca3af')
3022
+ }99)`,
2523
3023
  }}
2524
3024
  >
2525
3025
  {/* Round 412 / Loop: filter pin pill VALUE picks up the
@@ -2533,7 +3033,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2533
3033
  to R333/R335-R341/R362/R369/R389/R410. data-filter-
2534
3034
  value attr surfaces the value span for tests.
2535
3035
  4-pill replace family — status / group / vendor / edge. */}
2536
- <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>
3036
+ <span><span className="hidden sm:inline opacity-70 transition-[opacity,letter-spacing] duration-200 group-hover:opacity-100 group-hover:tracking-wide" 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>
2537
3037
  <button
2538
3038
  type="button"
2539
3039
  aria-label={`Clear ${pinnedStatus} filter`}
@@ -2552,7 +3052,24 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2552
3052
  inline-block is default for <button> so no display
2553
3053
  tweak needed. replace_all covers all 4 filter pin
2554
3054
  pills (status / group / vendor / edge) at once. */
2555
- className="ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 transform-gpu"
3055
+ /* Round 547 / Loop — extends pill × close-button hover
3056
+ gesture from scale-110 (R356) + opacity-70 to ALSO
3057
+ include rotate-12 on hover. Pre-R547 the × dimmed
3058
+ and grew on hover; R547 adds a 12° twist so the
3059
+ close action telegraphs "discarding/spinning away"
3060
+ with a small delight gesture. Composes with
3061
+ transition-transform (existing) — Tailwind's
3062
+ hover:rotate-12 + hover:scale-110 stack into one
3063
+ transform under the same 200ms ease-out tween.
3064
+ Applied to all 4 pill × buttons (status / group /
3065
+ vendor / edge) via replace_all since the className
3066
+ is identical. Closes the pill × hover gesture
3067
+ vocabulary at 3 axes:
3068
+ hover:opacity-70 paint dim
3069
+ hover:scale-110 geometry grow (R356)
3070
+ hover:rotate-12 geometry twist (R547, this round)
3071
+ Hover-gesture parity across the 4-pill family. */
3072
+ className="ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 hover:rotate-12 transform-gpu"
2556
3073
  style={{ background: 'transparent', color: 'inherit', cursor: 'pointer', padding: 0 }}
2557
3074
  >×</button>
2558
3075
  </span>
@@ -2581,15 +3098,42 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2581
3098
  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 active:scale-95 transform-gpu" data-topo-filter-pill-hover-lift="true"
2582
3099
  title={matchCount > 0 ? `${matchPreview}${matchSuffix} — click to clear` : 'Click to clear filter'}
2583
3100
  onClick={() => setPinnedGroup(null)}
3101
+ /* Round 544 / Loop — extends R543 pin-active filter-pill
3102
+ drop-shadow pattern to the GROUP pill (2nd of 4 pill
3103
+ variants). Pre-R544 the group pill carried bg-tint +
3104
+ pal.legendAccent text/border but no outer paint glow.
3105
+ R544 adds the matching cyan-accent drop-shadow so the
3106
+ group pin pill radiates the same paint glow as R543
3107
+ status pill — pin-active visual signal at chip-row
3108
+ scope.
3109
+ Hue: pal.legendAccent (cyber #67e8f9 cyan-300 /
3110
+ light #0d9488 teal-600). Uses color-mix() syntax
3111
+ because pal.legendAccent may resolve to hex; same
3112
+ syntax works for both hex and hsl sources (banked
3113
+ R541 lesson). 60% alpha + 3px blur — same intensity
3114
+ as R543 status pill so the pin-active visual signal
3115
+ reads with matching brightness across pill variants.
3116
+ Pin-active tier-color paint glow sub-family
3117
+ (CLOSED progressively):
3118
+ R477 legend pin-ring (panel-row, row.fill)
3119
+ R543 status pill (chip-row, tier-color)
3120
+ R544 group pill (chip-row, legendAccent)
3121
+ ← this round
3122
+ Out of scope: vendor pill (line ~2755) + edge pill
3123
+ (line ~2824) — can future-extend in subsequent
3124
+ rounds (R545/R546). Both use the same R543 idiom:
3125
+ always-on drop-shadow when rendered, color from the
3126
+ pill's existing text color. */
2584
3127
  style={{
2585
3128
  background: isLight ? '#67e8f914' : '#67e8f91f',
2586
3129
  color: pal.legendAccent,
2587
3130
  borderColor: 'currentColor',
2588
3131
  cursor: 'pointer',
3132
+ filter: `drop-shadow(0 0 3px color-mix(in srgb, ${pal.legendAccent} 60%, transparent))`,
2589
3133
  }}
2590
3134
  >
2591
3135
  {/* R412: see status pill above — filter value fw=600 data tier. */}
2592
- <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>
3136
+ <span><span className="hidden sm:inline opacity-70 transition-[opacity,letter-spacing] duration-200 group-hover:opacity-100 group-hover:tracking-wide" 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>
2593
3137
  <button
2594
3138
  type="button"
2595
3139
  aria-label={`Clear group filter ${pinnedGroup}`}
@@ -2608,7 +3152,24 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2608
3152
  inline-block is default for <button> so no display
2609
3153
  tweak needed. replace_all covers all 4 filter pin
2610
3154
  pills (status / group / vendor / edge) at once. */
2611
- className="ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 transform-gpu"
3155
+ /* Round 547 / Loop — extends pill × close-button hover
3156
+ gesture from scale-110 (R356) + opacity-70 to ALSO
3157
+ include rotate-12 on hover. Pre-R547 the × dimmed
3158
+ and grew on hover; R547 adds a 12° twist so the
3159
+ close action telegraphs "discarding/spinning away"
3160
+ with a small delight gesture. Composes with
3161
+ transition-transform (existing) — Tailwind's
3162
+ hover:rotate-12 + hover:scale-110 stack into one
3163
+ transform under the same 200ms ease-out tween.
3164
+ Applied to all 4 pill × buttons (status / group /
3165
+ vendor / edge) via replace_all since the className
3166
+ is identical. Closes the pill × hover gesture
3167
+ vocabulary at 3 axes:
3168
+ hover:opacity-70 paint dim
3169
+ hover:scale-110 geometry grow (R356)
3170
+ hover:rotate-12 geometry twist (R547, this round)
3171
+ Hover-gesture parity across the 4-pill family. */
3172
+ className="ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 hover:rotate-12 transform-gpu"
2612
3173
  style={{ background: 'transparent', color: 'inherit', cursor: 'pointer', padding: 0 }}
2613
3174
  >×</button>
2614
3175
  </span>
@@ -2653,15 +3214,35 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2653
3214
  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 active:scale-95 transform-gpu" data-topo-filter-pill-hover-lift="true"
2654
3215
  title={matchCount > 0 ? `${matchPreview}${matchSuffix} — click to clear` : 'Click to clear vendor filter'}
2655
3216
  onClick={() => setPinnedVendor(null)}
3217
+ /* Round 545 / Loop — extends pin-active filter-pill drop-
3218
+ shadow pattern to VENDOR pill (3rd of 4 pill variants
3219
+ after R543 status + R544 group). vendorColor is HSL
3220
+ format (banked R541 lesson — vendorDist.color sources
3221
+ from mono.text in vendorIdentity.ts, which is `hsl(...)`),
3222
+ so the filter uses color-mix() syntax — same as R544.
3223
+ 60% alpha + 3px blur, matching R543/R544 intensity for
3224
+ consistent pin-active visual signal across all pill
3225
+ variants.
3226
+ Pin-active tier-color paint glow sub-family (progressive
3227
+ extension, 1 pill variant remaining):
3228
+ R477 legend pin-ring (panel-row, row.fill, hex+alpha)
3229
+ R543 status pill (chip-row, tier-color, hex+alpha)
3230
+ R544 group pill (chip-row, legendAccent, color-mix)
3231
+ R545 vendor pill (chip-row, vendorColor, color-mix)
3232
+ ← this round
3233
+ Out of scope: edge pill (line ~2824 pre-R545, now ~2900+).
3234
+ Final 1/4 pill remaining for a future round closes the
3235
+ sub-family. */
2656
3236
  style={{
2657
3237
  background: `${vendorColor}1f`,
2658
3238
  color: vendorColor,
2659
3239
  borderColor: 'currentColor',
2660
3240
  cursor: 'pointer',
3241
+ filter: `drop-shadow(0 0 3px color-mix(in srgb, ${vendorColor} 60%, transparent))`,
2661
3242
  }}
2662
3243
  >
2663
3244
  {/* R412: see status pill above — filter value fw=600 data tier. */}
2664
- <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>
3245
+ <span><span className="hidden sm:inline opacity-70 transition-[opacity,letter-spacing] duration-200 group-hover:opacity-100 group-hover:tracking-wide" 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>
2665
3246
  <button
2666
3247
  type="button"
2667
3248
  aria-label={`Clear vendor filter ${pinnedVendor}`}
@@ -2680,7 +3261,24 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2680
3261
  inline-block is default for <button> so no display
2681
3262
  tweak needed. replace_all covers all 4 filter pin
2682
3263
  pills (status / group / vendor / edge) at once. */
2683
- className="ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 transform-gpu"
3264
+ /* Round 547 / Loop — extends pill × close-button hover
3265
+ gesture from scale-110 (R356) + opacity-70 to ALSO
3266
+ include rotate-12 on hover. Pre-R547 the × dimmed
3267
+ and grew on hover; R547 adds a 12° twist so the
3268
+ close action telegraphs "discarding/spinning away"
3269
+ with a small delight gesture. Composes with
3270
+ transition-transform (existing) — Tailwind's
3271
+ hover:rotate-12 + hover:scale-110 stack into one
3272
+ transform under the same 200ms ease-out tween.
3273
+ Applied to all 4 pill × buttons (status / group /
3274
+ vendor / edge) via replace_all since the className
3275
+ is identical. Closes the pill × hover gesture
3276
+ vocabulary at 3 axes:
3277
+ hover:opacity-70 paint dim
3278
+ hover:scale-110 geometry grow (R356)
3279
+ hover:rotate-12 geometry twist (R547, this round)
3280
+ Hover-gesture parity across the 4-pill family. */
3281
+ className="ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 hover:rotate-12 transform-gpu"
2684
3282
  style={{ background: 'transparent', color: 'inherit', cursor: 'pointer', padding: 0 }}
2685
3283
  >×</button>
2686
3284
  </span>
@@ -2719,11 +3317,34 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2719
3317
  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 active:scale-95 transform-gpu" data-topo-filter-pill-hover-lift="true"
2720
3318
  title={`${link.from} → ${link.to} (${link.count} msg${link.count === 1 ? '' : 's'}${isHot ? ', hot lane · ≥ 10' : ''}) — click to clear`}
2721
3319
  onClick={() => setPinnedEdgeKey(null)}
3320
+ /* Round 546 / Loop — CLOSES pin-active filter-pill drop-
3321
+ shadow sub-family at the 4th and final pill variant
3322
+ (edge pill). R543 (status) + R544 (group) + R545
3323
+ (vendor) covered the first three; R546 closes at
3324
+ edge.
3325
+ Pin-active tier-color paint glow sub-family CLOSED
3326
+ (4 anchors):
3327
+ R477 legend pin-ring (panel-row, row.fill)
3328
+ R543 status pill (chip-row, tier-color text)
3329
+ R544 group pill (chip-row, legendAccent)
3330
+ R545 vendor pill (chip-row, vendorColor)
3331
+ R546 edge pill (chip-row, pal.flowEdge)
3332
+ ← this round, family CLOSED
3333
+ All 4 filter pin pills now radiate paint glow in the
3334
+ same hue family as their text/border on render —
3335
+ pin-active visual signal uniform across the chip-row
3336
+ pill family.
3337
+ pal.flowEdge is theme-driven (dynamic); color-mix
3338
+ syntax safe-defaults regardless of resolved format
3339
+ (banked R541/R544/R545 pattern). 60% alpha + 3px blur
3340
+ — same intensity as R543/R544/R545 for consistent
3341
+ cross-pill visual signal. */
2722
3342
  style={{
2723
3343
  background: isLight ? `${pal.flowEdge}14` : `${pal.flowEdge}1f`,
2724
3344
  color: pal.flowEdge,
2725
3345
  borderColor: 'currentColor',
2726
3346
  cursor: 'pointer',
3347
+ filter: `drop-shadow(0 0 3px color-mix(in srgb, ${pal.flowEdge} 60%, transparent))`,
2727
3348
  }}
2728
3349
  >
2729
3350
  {/* R412: filter pin pill value (edge variant) picks up fw=600.
@@ -2776,7 +3397,24 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2776
3397
  inline-block is default for <button> so no display
2777
3398
  tweak needed. replace_all covers all 4 filter pin
2778
3399
  pills (status / group / vendor / edge) at once. */
2779
- className="ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 transform-gpu"
3400
+ /* Round 547 / Loop — extends pill × close-button hover
3401
+ gesture from scale-110 (R356) + opacity-70 to ALSO
3402
+ include rotate-12 on hover. Pre-R547 the × dimmed
3403
+ and grew on hover; R547 adds a 12° twist so the
3404
+ close action telegraphs "discarding/spinning away"
3405
+ with a small delight gesture. Composes with
3406
+ transition-transform (existing) — Tailwind's
3407
+ hover:rotate-12 + hover:scale-110 stack into one
3408
+ transform under the same 200ms ease-out tween.
3409
+ Applied to all 4 pill × buttons (status / group /
3410
+ vendor / edge) via replace_all since the className
3411
+ is identical. Closes the pill × hover gesture
3412
+ vocabulary at 3 axes:
3413
+ hover:opacity-70 paint dim
3414
+ hover:scale-110 geometry grow (R356)
3415
+ hover:rotate-12 geometry twist (R547, this round)
3416
+ Hover-gesture parity across the 4-pill family. */
3417
+ className="ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 hover:rotate-12 transform-gpu"
2780
3418
  style={{ background: 'transparent', color: 'inherit', cursor: 'pointer', padding: 0 }}
2781
3419
  >×</button>
2782
3420
  </span>
@@ -3098,6 +3736,41 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3098
3736
  // for older browsers the chip falls back to its idle
3099
3737
  // transparent bg (graceful degradation — the canvas-
3100
3738
  // dim effect still fires regardless).
3739
+ /* Round 541 / Loop — vendor letter chip gains drop-
3740
+ shadow glow on hover/pin using its OWN vendor
3741
+ identity color (v.color). Sibling to R537 legend-
3742
+ swatch tier-color glow at the chip-row scope.
3743
+ Pre-R541 the vendor chip lifted on multiple
3744
+ axes (R354 inner-glyph scale-1.1, R202 bg color-
3745
+ mix tint, R180 box-shadow pin-mirror inset, R401
3746
+ hover-translate-y -1px, R496 active:scale-95
3747
+ press) but no paint-axis glow extending past
3748
+ the chip's bbox. R541 adds the outer glow at
3749
+ the paint axis so the vendor chip's identity
3750
+ color radiates beyond the chip on attention —
3751
+ same idiom as legend swatch tier-color glow.
3752
+ 2-tier alpha ladder (mirrors R538 group-label):
3753
+ pin (committed) v.color 99 (~60%)
3754
+ hover (preview) v.color 66 (~40%)
3755
+ rest none
3756
+ Pin is brighter to distinguish locked vs preview
3757
+ at the paint axis. The R180 inset box-shadow
3758
+ (pin-mirror) and R541 outer drop-shadow compose
3759
+ at pin — inside chrome reads as "this is pinned"
3760
+ (inset white double-ring), outside paint reads
3761
+ as "vendor identity is locked" (vendor-colour
3762
+ outer glow). Hover gets only the outer glow.
3763
+ 3px blur tuned to read as soft chip-halo without
3764
+ overwhelming adjacent chips in the chip row.
3765
+ filter property is in the .anet-topo-chip-focus
3766
+ class transition list (R524 banked fix), so the
3767
+ filter eases at 200ms naturally.
3768
+ Drop-shadow visual-polish family — R541 adds
3769
+ chip-row tier-color paint glow as another anchor
3770
+ in the same family pattern R537 established.
3771
+ data-vendor-glow attr ('pin' | 'hover' | 'false')
3772
+ exposes the gate state for tests. */
3773
+ data-vendor-glow={isPinned ? 'pin' : hoveredVendor === v.initial ? 'hover' : 'false'}
3101
3774
  style={{
3102
3775
  cursor: 'pointer',
3103
3776
  backgroundColor: (hoveredVendor === v.initial && !isPinned)
@@ -3106,7 +3779,15 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3106
3779
  boxShadow: isPinned
3107
3780
  ? `inset 0 0 0 1px ${v.color}, inset 0 0 0 2px rgba(255,255,255,0.45)`
3108
3781
  : undefined,
3109
- transition: 'box-shadow 150ms ease-out, background-color 200ms ease-out',
3782
+ /* R578 sibling vendor chip stacks brightness(1.15)
3783
+ onto R541 drop-shadow. Closes chip-row tier-color
3784
+ glow trio at consistent stacked-filter pattern. */
3785
+ filter: isPinned
3786
+ ? `drop-shadow(0 0 3px color-mix(in srgb, ${v.color} 60%, transparent)) brightness(1.15)`
3787
+ : hoveredVendor === v.initial
3788
+ ? `drop-shadow(0 0 3px color-mix(in srgb, ${v.color} 40%, transparent)) brightness(1.15)`
3789
+ : undefined,
3790
+ transition: 'box-shadow 150ms ease-out, background-color 200ms ease-out, filter 200ms ease-out',
3110
3791
  }}
3111
3792
  onMouseEnter={() => setHoveredVendor(v.initial)}
3112
3793
  onMouseLeave={() => setHoveredVendor(prev => prev === v.initial ? null : prev)}
@@ -3198,7 +3879,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3198
3879
  since the glyph (R369 fw=600) stays at full
3199
3880
  opacity. R333 :{count} format preserved. */}
3200
3881
  <span
3201
- className="text-gray-400 tabular-nums opacity-70 transition-opacity duration-200 group-hover:opacity-100"
3882
+ className="text-gray-400 tabular-nums opacity-70 transition-[opacity,letter-spacing] duration-200 group-hover:opacity-100 group-hover:tracking-wide"
3202
3883
  data-vendor-letter-count-suffix
3203
3884
  >:{v.count}</span>
3204
3885
  </span>
@@ -3348,7 +4029,9 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3348
4029
  chip-internal-hierarchy arc. data-active-links-
3349
4030
  chip-unit exposes the unit span for tests. */}
3350
4031
  {/* R362 sibling — active-links chip digit gains font-semibold. */}
3351
- <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>
4032
+ {/* R539 sibling active-links chip digit. Same idiom
4033
+ as working + online above. */}
4034
+ <span className="font-semibold transition-[font-weight,letter-spacing] duration-200 group-hover:font-bold group-hover:tracking-wide" data-active-links-chip-digit>{flowLinks.length}</span><span className="opacity-70 transition-[opacity,letter-spacing] duration-200 group-hover:opacity-100 group-hover:tracking-wide" data-active-links-chip-unit> active link{flowLinks.length === 1 ? '' : 's'}</span>
3352
4035
  {rel ? (() => {
3353
4036
  // Round 161 / Loop: extend R160's recency-pip
3354
4037
  // vocabulary up one scope — from per-flow row to
@@ -3562,6 +4245,36 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3562
4245
  on the canvas root for non-visual consumers.
3563
4246
  Composed from existing onlineNodes / workingCount /
3564
4247
  offlineNodes / flowLinks — no new state. */
4248
+ /* Round 502 / Loop — categorical density-tier paired with the
4249
+ R469 numeric counts. data-topo-fleet-density-tier classifies
4250
+ the fleet size into 5 buckets so external consumers (CSS
4251
+ selectors, Playwright probes, future density-conditional
4252
+ polish gates like R109 dense-label collapse at 16+ nodes)
4253
+ can branch on a stable tier name without re-deriving the
4254
+ threshold logic from the raw numeric. Buckets:
4255
+ 'empty' — onlineNodes.length === 0
4256
+ 'sparse' — 1-3 nodes
4257
+ 'normal' — 4-15 nodes
4258
+ 'dense' — 16-30 nodes (matches R109 collapse gate)
4259
+ 'very-dense' — 31+ nodes
4260
+ Picks the gate boundaries that already drive CONDITIONAL
4261
+ RENDER decisions elsewhere (R109 denseLayout = >16, R110
4262
+ plain-text fallback) so the tier name is semantically
4263
+ aligned with the visual mode the canvas already switches
4264
+ to. Composed from existing onlineNodes — no new state.
4265
+ 12th attr in the canvas state surface set (R462/R466/R467/
4266
+ R469×4/R471×2/R487/R488/R502). 12 attrs covers: build
4267
+ identity, transient/sticky inspection modes, fleet split
4268
+ numerics, fleet density tier, canvas layout/theme, canvas
4269
+ zoom, hover identity. A test harness can snapshot the
4270
+ full canvas state with 12 getAttribute calls. */
4271
+ data-topo-fleet-density-tier={
4272
+ onlineNodes.length === 0 ? 'empty' :
4273
+ onlineNodes.length <= 3 ? 'sparse' :
4274
+ onlineNodes.length <= 15 ? 'normal' :
4275
+ onlineNodes.length <= 30 ? 'dense' :
4276
+ 'very-dense'
4277
+ }
3565
4278
  data-topo-online-count={onlineNodes.length}
3566
4279
  data-topo-working-count={workingCount}
3567
4280
  data-topo-offline-count={offlineNodes.length}
@@ -3627,6 +4340,119 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3627
4340
  categorical) — separate dedicated attrs if/when needed.
3628
4341
  Root svg attribute set now 11 attrs total. */
3629
4342
  data-topo-hovered-alias={hoveredAlias ?? ''}
4343
+ /* Round 504 / Loop — categorical pin-aspect attr paired with
4344
+ R467 any-pinned boolean and R488 hovered-alias identity.
4345
+ Pre-R504 the canvas state surface set told tests WHETHER
4346
+ any pin was active (R467 boolean) but tests had to enumerate
4347
+ 4 individual state vars to determine WHICH pin axis fired:
4348
+ pinnedStatus legend-row status filter
4349
+ pinnedGroup prefix-cluster lock
4350
+ pinnedVendor vendor-chip filter
4351
+ pinnedEdgeKey edge-focus
4352
+ R504 surfaces the active aspect as a single categorical
4353
+ attribute: data-topo-pinned-aspect ∈
4354
+ 'none' no pin active
4355
+ 'status' pinnedStatus only
4356
+ 'group' pinnedGroup only
4357
+ 'vendor' pinnedVendor only
4358
+ 'edge' pinnedEdgeKey only
4359
+ 'multi' 2 or more pins active simultaneously
4360
+ ('multi' covers cross-cutting filters — e.g. user pins
4361
+ status='working' AND vendor='claude' simultaneously to
4362
+ narrow the canvas. Each pin axis is independently
4363
+ dismissable via Esc / individual chip click, so multi
4364
+ states are reachable and worth surfacing as a distinct
4365
+ tier.)
4366
+ 13th attr in the canvas state surface set after R502.
4367
+ Composed from 4 existing state vars — no new state. */
4368
+ /* Round 512 / Loop — 14th canvas state attr. groupBoxes.length
4369
+ surfaces the count of cluster boxes currently rendered in
4370
+ grid layout (always 0 in ring). Paired with R502 categorical
4371
+ density tier + R469 fleet numerics for a complete cluster-
4372
+ cardinality surface:
4373
+ R469 data-topo-online-count node-count
4374
+ R502 data-topo-fleet-density-tier categorical
4375
+ R512 data-topo-cluster-count cluster-count ← this round
4376
+ Use cases:
4377
+ - Playwright: assert orphan-band existence by
4378
+ `cluster-count === N + 1` vs prefix-only `=== N`
4379
+ - external CSS: `[data-topo-cluster-count='1']` to apply
4380
+ single-cluster grid-specific layout adjustments
4381
+ - future polish gates: cluster-count > N could trigger
4382
+ dense-grid mode
4383
+ Composed from existing `groupBoxes.length` — no new state.
4384
+ Always renders (0 in ring layout, N in grid), so tests can
4385
+ rely on attribute presence + value. */
4386
+ data-topo-cluster-count={groupBoxes.length}
4387
+ /* Round 513 / Loop — 15th canvas state attr. Surfaces the
4388
+ user's prefers-reduced-motion preference directly on the
4389
+ root SVG so external CSS / Playwright tests can branch on
4390
+ a11y state without re-reading the media query.
4391
+ reducedMotion is already in component scope (R29 a11y
4392
+ blanket reads it via a useEffect listener); R513 just
4393
+ exposes it as a stable attribute handle.
4394
+ Use cases:
4395
+ - Playwright: assert reduced-motion gates from one attr
4396
+ read instead of mocking media-query state per test
4397
+ - External CSS hooks: `[data-topo-prefers-reduced-motion=
4398
+ "true"]` to apply paint-only overrides (e.g. mute
4399
+ hover glows entirely on a11y instead of just
4400
+ disabling transitions)
4401
+ - Future polish rounds: any motion-gated render can
4402
+ read this attr server-side without the media-query
4403
+ hydration mismatch risk
4404
+ 'true' / 'false' string values (consistent with R466/R467
4405
+ boolean attrs). */
4406
+ data-topo-prefers-reduced-motion={reducedMotion ? 'true' : 'false'}
4407
+ /* Round 515 / Loop — 16th canvas state attr. Surfaces the
4408
+ fullscreen-mode state directly on root SVG so external
4409
+ consumers don't have to traverse the chrome strip's
4410
+ `data-topo-chrome-fullscreen-active` button attr (which
4411
+ measures the BUTTON state, not the canvas state — they
4412
+ agree, but reading from the root is semantically cleaner
4413
+ for canvas-state probes).
4414
+ Composed from existing isFullscreen React state (R103
4415
+ fullscreen toggle).
4416
+ Use cases:
4417
+ - Playwright: assert canvas mode in one attr read
4418
+ (paired with R471 data-topo-layout for ring/grid +
4419
+ R487 data-topo-zoom for zoom level + R513 reduced-
4420
+ motion for a11y mode = 4-axis canvas-mode probe)
4421
+ - External CSS: `[data-topo-fullscreen="true"]` to
4422
+ apply fullscreen-only paint adjustments outside the
4423
+ React tree (e.g. body-level scrollbar hide)
4424
+ 'true' / 'false' string values (consistent with R466/
4425
+ R467/R513 boolean attrs). */
4426
+ data-topo-fullscreen={isFullscreen ? 'true' : 'false'}
4427
+ /* Round 516 / Loop — 17th canvas state attr. Surfaces the
4428
+ grid layout's content-bottom y-coordinate so tests can
4429
+ verify grid content doesn't extend past the viewBox or
4430
+ collide with chrome elements positioned below the canvas.
4431
+ Composed from existing gridContentBottom derived state
4432
+ (computed at line ~915 from gy0 + totalRows * cellH + 8).
4433
+ In ring layout, gridContentBottom is 0 (no grid). In grid
4434
+ layout it's the actual pixel y-coordinate where the
4435
+ cluster bands end.
4436
+ Use cases:
4437
+ - Playwright: assert grid layout doesn't exceed viewBox
4438
+ height (680) without re-computing the layout math
4439
+ - External CSS: `[data-topo-grid-content-bottom='0']` to
4440
+ distinguish ring-mode (no grid content) from grid-mode
4441
+ in CSS without parsing layout attr
4442
+ - Future polish gates: if cluster count grows large
4443
+ enough to push grid bottom past viewBox, can trigger
4444
+ a 'compact' mode automatically */
4445
+ data-topo-grid-content-bottom={gridContentBottom}
4446
+ data-topo-pinned-aspect={(() => {
4447
+ const aspects: string[] = [];
4448
+ if (pinnedStatus) aspects.push('status');
4449
+ if (pinnedGroup) aspects.push('group');
4450
+ if (pinnedVendor) aspects.push('vendor');
4451
+ if (pinnedEdgeKey) aspects.push('edge');
4452
+ if (aspects.length === 0) return 'none';
4453
+ if (aspects.length === 1) return aspects[0];
4454
+ return 'multi';
4455
+ })()}
3630
4456
  /* Round 466 / Loop — aggregate hover signal on the root SVG.
3631
4457
  Exposes a single boolean `data-topo-any-hover` that
3632
4458
  reflects whether ANY hover state in the topology is
@@ -3869,7 +4695,38 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3869
4695
  const x = ((seed * 13) % 1000);
3870
4696
  const y = ((seed * 7) % 680);
3871
4697
  const r = (i % 3 === 0) ? 1.2 : 0.7;
3872
- return <circle key={i} cx={x} cy={y} r={r} fill="#a5b4fc" opacity={0.35 + (i % 4) * 0.05} data-topo-starfield-dot={i} />;
4698
+ /* Round 523 / Loop 配色 family extension to a 3rd anchor.
4699
+ Pre-R523 all 14 starfield dots painted at the same
4700
+ hardcoded `#a5b4fc` (indigo-300). The starfield's role
4701
+ is atmospheric depth (R45, R291 comment), but a flat
4702
+ single-hue field reads more like a regular dot grid
4703
+ than a star field — real starlight has color
4704
+ temperature variation (blue-white hot stars / yellow
4705
+ sun-like / cool red).
4706
+ R523 cycles a 3-color deterministic rotation based on
4707
+ `i % 3`:
4708
+ i % 3 === 0 → #a5b4fc indigo-300 (original, cool)
4709
+ i % 3 === 1 → #67e8f9 cyan-300 (cyber accent, hot)
4710
+ i % 3 === 2 → #cbd5e1 slate-300 (neutral, warm white)
4711
+ All three hues sit inside the cyber theme's palette
4712
+ family (indigo / cyan / slate) so the starfield reads
4713
+ varied-but-coherent rather than rainbow. At opacity
4714
+ 0.5 (parent <g>) * 0.35-0.50 (per-dot) the temperature
4715
+ shifts are gentle but perceptible — closes the gap
4716
+ between 'dot grid' and 'star field'.
4717
+ 配色 family extension (3 anchors): R509/R510 hub-
4718
+ highlight cross-theme fill + R523 starfield color
4719
+ temperature variation. Light theme unaffected
4720
+ (starfield gated `!isLight` so light theme stays
4721
+ clean per R45's original 'white surface stays clean'
4722
+ intent).
4723
+ Deterministic on `i` — no JS hydration mismatch,
4724
+ same SSR/client output. data-topo-starfield-dot-hue
4725
+ attr exposes the resolved hue category for tests. */
4726
+ const hues = ['#a5b4fc', '#67e8f9', '#cbd5e1'] as const;
4727
+ const hueNames = ['indigo', 'cyan', 'slate'] as const;
4728
+ const hueIdx = i % 3;
4729
+ return <circle key={i} cx={x} cy={y} r={r} fill={hues[hueIdx]} opacity={0.35 + (i % 4) * 0.05} data-topo-starfield-dot={i} data-topo-starfield-dot-hue={hueNames[hueIdx]} />;
3873
4730
  })}
3874
4731
  </g>
3875
4732
  )}
@@ -4407,8 +5264,60 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4407
5264
  data-topo-hub-spoke-stroke-width={spokeStrokeWidth}
4408
5265
  data-topo-hub-spoke-stroke-width-active="2.25"
4409
5266
  data-topo-hub-spoke-linecap="round"
5267
+ /* Round 533 / Loop — extends drop-shadow visual-polish
5268
+ family to a 9th anchor: hub spokes gain filter:drop-
5269
+ shadow glow on hub-hover. Subtle 1.5px cyan/teal blur
5270
+ applied across ALL spokes simultaneously when the
5271
+ user hovers the hub — the network mesh visually
5272
+ "lights up" in response to focal attention. Sibling
5273
+ to R476 hub-digit + R532 hub-highlight glow at the
5274
+ same gate (hoveredHub && !reducedMotion); together
5275
+ the three anchors (digit + highlight disc + spokes)
5276
+ form a unified focal-cluster glow that signals
5277
+ "you're focused on the hub" across geometry,
5278
+ paint, and mesh-extent axes.
5279
+ Theme-aware glow palette matches the spoke stroke
5280
+ family:
5281
+ light: rgba(13, 148, 136, 0.4) teal-600
5282
+ cyber: rgba(34, 211, 238, 0.4) cyan-400
5283
+ 0.4 alpha keeps the glow subtle across N spokes
5284
+ (30+ at peak fleet sizes) — loud bloom across many
5285
+ edges would compete with the focal cluster itself.
5286
+ 1.5px blur is conservative; tuned so each spoke
5287
+ gains a faint outer halo rather than a wide bloom.
5288
+ filter is paint-only; bbox unchanged; existing
5289
+ R241 transition list extends to 'filter 250ms
5290
+ ease-out' matching the spoke transition cadence
5291
+ (250ms, distinct from the 200ms hub-cluster
5292
+ cadence — spokes ease slightly slower since they
5293
+ respond to per-alias state, not just hub state).
5294
+ data-topo-hub-spoke-glow attr exposes the gate
5295
+ state for tests. */
5296
+ data-topo-hub-spoke-glow={!reducedMotion && hoveredHub ? 'true' : 'false'}
5297
+ /* Round 580 (65-round milestone) — hub-spokes complete
5298
+ the hub-cluster brightness coverage at 5/5 concentric
5299
+ elements. Stacks brightness(1.15) onto R533's drop-
5300
+ shadow — same R564/R570/R571/R572/R573/R574/R575/
5301
+ R577/R578/R579 stacked-filter pattern.
5302
+ Hub-cluster brightness now FULLY CLOSED:
5303
+ hub digit (R575) innermost typo
5304
+ hub-highlight (R574) middle disc
5305
+ hub-hover-ring (R579) outer ring boundary
5306
+ hub-halo (R577) outermost atmosphere
5307
+ hub-spokes (R580) mesh radial lines ← this round
5308
+ 5 concentric elements + N mesh radial lines all lift
5309
+ uniformly through stacked drop-shadow + brightness on
5310
+ hub-hover. The hub focal cluster now responds as ONE
5311
+ unified motion-coherent paint pulse from center
5312
+ outward through every layer. */
5313
+ data-topo-hub-spoke-brightness={!reducedMotion && hoveredHub ? '1.15' : '1'}
4410
5314
  style={{
4411
- transition: 'stroke 250ms ease-out, stroke-width 250ms ease-out, opacity 250ms ease-out',
5315
+ transition: 'stroke 250ms ease-out, stroke-width 250ms ease-out, opacity 250ms ease-out, filter 250ms ease-out',
5316
+ filter: !reducedMotion && hoveredHub
5317
+ ? (isLight
5318
+ ? 'drop-shadow(0 0 1.5px rgba(13, 148, 136, 0.4)) brightness(1.15)'
5319
+ : 'drop-shadow(0 0 1.5px rgba(34, 211, 238, 0.4)) brightness(1.15)')
5320
+ : undefined,
4412
5321
  ...(isActiveSpoke ? {} : {
4413
5322
  animationDelay: `${-(idx * 0.25)}s`,
4414
5323
  // CSS var consumed by `.anet-topo-spoke-flow`
@@ -4429,6 +5338,30 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4429
5338
  a node click. Restrained dashed container + group-name label. */}
4430
5339
  {groupBoxes.map((box, boxIdx) => {
4431
5340
  const isHovered = activeGroup === box.key;
5341
+ /* Round 561 / Loop — inspection-overrides-encoding family
5342
+ 4th anchor. When operator hovers a NODE ALIAS on the
5343
+ canvas, the group-label that CONTAINS that node lifts
5344
+ to full opacity, signalling "this is the cluster
5345
+ whose member you're inspecting".
5346
+ Family progression (4 anchors now):
5347
+ R484 recent-row timestamp brightens on alias hover
5348
+ R485 edge particle opacity lifts on alias hover
5349
+ R486 minimap dot opacity lifts on alias hover
5350
+ R561 group-label opacity lifts on member-alias hover ← this round
5351
+ Pure paint axis (opacity only) — same restraint as
5352
+ R486. NOT bundled into the existing `isHovered` flag
5353
+ so the marching-ants live animation (gated on
5354
+ `!isPinned && !isHovered`) keeps running; the box
5355
+ rect stroke widen / fill brighten / letter-spacing
5356
+ / filter glow gates stay tied to direct label
5357
+ hover/pin semantics.
5358
+ Mirror of R486's pattern: `hoveredAlias === s.alias`
5359
+ extends a focused opacity branch independent from
5360
+ the rest-state encoding (online/offline). R561 here:
5361
+ `groupKeys[hoveredAlias] === box.key` extends a
5362
+ cluster-awareness opacity branch independent from
5363
+ the existing isHovered / isPinned semantics. */
5364
+ const isMemberAliasHovered = !!hoveredAlias && groupKeys[hoveredAlias] === box.key;
4432
5365
  // R68: distinguish "locked by click" from "currently hovered".
4433
5366
  // R63 made pinned and hovered identical (both hit isHovered
4434
5367
  // via activeGroup). A user with one team pinned should see at
@@ -4556,12 +5489,75 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4556
5489
  // pinned → fill 0.08 / 0.13, stroke 3 px (locked)
4557
5490
  // hovered → fill 0.05 / 0.09, stroke 2 px (inspecting)
4558
5491
  // idle → fill 0.025 / 0.045, stroke 1.5 px dashed
4559
- fillOpacity={isPinned ? (isLight ? 0.08 : 0.13)
4560
- : isHovered ? (isLight ? 0.05 : 0.09)
4561
- : (isLight ? 0.025 : 0.045)}
5492
+ /* Round 506 / Loop category-differentiation family
5493
+ 3rd anchor. Orphan band rest-state fillOpacity drops
5494
+ slightly below prefix-group rest (0.025/0.045
5495
+ 0.015/0.028). Adds a 3rd independent paint
5496
+ differentiator to the orphan visual signature:
5497
+ R499 fontStyle: italic (label text)
5498
+ R503 '3 6' dash pattern (rect stroke)
5499
+ R506 lower fillOpacity (rect fill) ← this round
5500
+ Three independent channels (typography + stroke
5501
+ pattern + fill density) collectively encode the
5502
+ catchall semantic at rest. Pin and hover branches
5503
+ UNCHANGED (still 0.05/0.09 hover, 0.08/0.13 pin) —
5504
+ orphan box gets full visual emphasis on inspection
5505
+ identical to prefix groups; the differentiation
5506
+ lives ONLY in the unsolicited rest state. The
5507
+ ~40% drop (0.045 → 0.028 cyber, 0.025 → 0.015
5508
+ light) is subtle enough that the orphan box stays
5509
+ visible at rest, just quieter — matches the
5510
+ "misc bucket, less attention-deserving" semantic
5511
+ without losing the visual anchor.
5512
+ Pure paint axis; bbox unchanged; R51 SVG sentinel
5513
+ safety untouched (overlap-test gates to g[data-
5514
+ node], cluster rect invisible to it).
5515
+ data-group-box-fill-opacity attr surfaces the
5516
+ resolved value for tests. */
5517
+ fillOpacity={
5518
+ isPinned ? (isLight ? 0.08 : 0.13)
5519
+ : isHovered ? (isLight ? 0.05 : 0.09)
5520
+ : box.isOrphan ? (isLight ? 0.015 : 0.028)
5521
+ : (isLight ? 0.025 : 0.045)
5522
+ }
5523
+ data-group-box-fill-opacity={
5524
+ isPinned ? (isLight ? 0.08 : 0.13)
5525
+ : isHovered ? (isLight ? 0.05 : 0.09)
5526
+ : box.isOrphan ? (isLight ? 0.015 : 0.028)
5527
+ : (isLight ? 0.025 : 0.045)
5528
+ }
4562
5529
  stroke={(isPinned || isHovered) ? pal.legendAccent : pal.ringStroke}
4563
5530
  strokeWidth={isPinned ? 3 : isHovered ? 2 : 1.5}
4564
- strokeDasharray={(isPinned || isHovered) ? 'none' : '6 6'}
5531
+ /* Round 503 / Loop category-differentiation family
5532
+ 2nd anchor (R499 = orphan label italic, 1st anchor).
5533
+ Orphan band rest-state strokeDasharray switches from
5534
+ '6 6' (prefix-group default) to '3 6' (tighter
5535
+ dashes). Pre-R503 the rect dash pattern was uniform
5536
+ across all bands; combined with R499's italic label,
5537
+ the orphan box now has TWO independent paint/
5538
+ typography differentiators at rest:
5539
+ R499 fontStyle: italic (label text)
5540
+ R503 '3 6' dash pattern (rect stroke) ← this round
5541
+ The R85 marching-ants animation continues to work
5542
+ with the new dash size (uses --march-dur custom
5543
+ property, dash-length-agnostic) — orphan's ants
5544
+ just have a different visual rhythm than prefix-
5545
+ group ants, reinforcing the catchall semantic.
5546
+ Pinned/hovered orphan still gets 'none' (solid
5547
+ stroke) so the hover/pin affordance is preserved
5548
+ — the differentiation lives ONLY in the rest
5549
+ state, never blocking inspection.
5550
+ Pure paint axis; no geometry change; bbox unchanged
5551
+ (strokeDasharray is paint-only). R51 SVG sentinel
5552
+ safety untouched (overlap-test gates to g[data-
5553
+ node], this cluster rect is invisible to it).
5554
+ data-group-box-orphan attr surfaces the gate for
5555
+ tests + future polish references. */
5556
+ strokeDasharray={
5557
+ (isPinned || isHovered) ? 'none' :
5558
+ box.isOrphan ? '3 6' : '6 6'
5559
+ }
5560
+ data-group-box-orphan={box.isOrphan ? 'true' : 'false'}
4565
5561
  /* Round 380 / Loop: cluster box stroke gets round
4566
5562
  linecap + round linejoin. Sibling SVG stroke-
4567
5563
  softening polish to R378 flow-rail linecap + R379
@@ -4591,6 +5587,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4591
5587
  data-group-box-linecap="round"
4592
5588
  data-group-box-linejoin="round"
4593
5589
  data-group-box-geom-transition="x,y,width,height"
5590
+ data-group-box-brightness={(isPinned || isHovered) ? '1.15' : '1'}
4594
5591
  // R85: ambient "marching ants" drift on the perimeter
4595
5592
  // when this group has at least one working member, and
4596
5593
  // neither pin nor hover is active (those treatments
@@ -4607,10 +5604,29 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4607
5604
  // motion layers (hub / ring / group) keep a coherent
4608
5605
  // tempo grammar. Default 12s when working=0 doesn't
4609
5606
  // matter — the className is only applied when working>0.
4610
- data-group-box-live={!isPinned && !isHovered && box.statuses.working > 0 ? 'true' : 'false'}
5607
+ /* Round 561 marching-ants gate refined to halt only
5608
+ on direct-label-hover (or pin), NOT on member-alias-
5609
+ hover. Pre-R561 `isHovered` covered BOTH cases via
5610
+ the line 1044 fallback `hoveredGroup ?? (hoveredAlias
5611
+ → groupKeys[hoveredAlias])` — so hovering ANY member
5612
+ node halted the ants, treating indirect inspection
5613
+ the same as direct attention.
5614
+ R561 differentiates: ants now keep running during
5615
+ member-alias hover (indirect / cluster-awareness
5616
+ inspection), halting ONLY on direct label hover or
5617
+ pin. The cluster's live signal stays alive while
5618
+ operator inspects member nodes — distinct visual
5619
+ telegraph for "directly attending this cluster"
5620
+ vs "inspecting one of its members".
5621
+ Gate uses `hoveredGroupLabel === box.key` directly
5622
+ (the LABEL hover state) instead of `isHovered`
5623
+ (which combines label-hover + member-alias-hover
5624
+ via activeGroup). data-group-box-live + the live
5625
+ className both flip on the same refined gate. */
5626
+ data-group-box-live={!isPinned && hoveredGroupLabel !== box.key && box.statuses.working > 0 ? 'true' : 'false'}
4611
5627
  data-group-box-march-dur={marchDur}
4612
5628
  data-group-box-lifted={(isPinned || isHovered) ? 'true' : 'false'}
4613
- className={!isPinned && !isHovered && box.statuses.working > 0 ? 'anet-topo-groupbox-live' : undefined}
5629
+ className={!isPinned && hoveredGroupLabel !== box.key && box.statuses.working > 0 ? 'anet-topo-groupbox-live' : undefined}
4614
5630
  // R142: drop-shadow filter when pinned or hovered. Box
4615
5631
  // visually "rises off the canvas" — same vocabulary
4616
5632
  // R18 KPI cards + R135 overlay panels use. Idle group
@@ -4621,6 +5637,34 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4621
5637
  // is preserved.
4622
5638
  filter={(isPinned || isHovered) ? 'url(#topo-groupbox-lift)' : undefined}
4623
5639
  style={{
5640
+ /* R587 — group box gains stacked brightness(1.15)
5641
+ on hover/pin. 26th anchor in per-element
5642
+ brightness family, 19th in stacked-filter
5643
+ sub-pattern. Inline style.filter overrides the
5644
+ attribute filter (kept intact for the R142
5645
+ documentation trail); stacked syntax preserves
5646
+ the R142 url(#topo-groupbox-lift) SVG drop-shadow
5647
+ lift on hover/pin, with brightness layered on
5648
+ top.
5649
+
5650
+ Same R582/R583 stacked-filter pattern at the
5651
+ group-cluster scope.
5652
+
5653
+ Group box inspection signature now 6 layers
5654
+ (spans paint + stroke + geometry + filter):
5655
+ R68 fillOpacity 0.045 → 0.13 (cyber pin)
5656
+ R68 strokeWidth 1.5 → 2 → 3
5657
+ R68 stroke tint → legendAccent
5658
+ R503 strokeDasharray → 'none' on activation
5659
+ R142 filter → url(#topo-groupbox-lift)
5660
+ R587 filter stack → brightness(1.15) ← this round
5661
+
5662
+ Same existing transition list already includes
5663
+ 'filter 200ms ease-out' (R142 cadence). No
5664
+ transition change needed. */
5665
+ filter: (isPinned || isHovered)
5666
+ ? 'url(#topo-groupbox-lift) brightness(1.15)'
5667
+ : undefined,
4624
5668
  /* Round 248 / Loop: append fill 200ms ease-out to
4625
5669
  the existing R66 transition list. Pre-R248 the
4626
5670
  rect's fill (isLight ? '#0f172a' (slate-900) :
@@ -4881,8 +5925,54 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4881
5925
  fontSize="9"
4882
5926
  fontFamily="monospace"
4883
5927
  fontWeight={isPinned ? '800' : '700'}
4884
- opacity={isPinned || isHovered ? 1 : 0.55}
5928
+ /* Round 551 / Loop category-differentiation family
5929
+ 4th anchor. Orphan band ("其他" catchall) rest-state
5930
+ LABEL opacity drops 0.55 → 0.4 (~27% relative dim),
5931
+ mirroring R506's rect fillOpacity drop pattern at
5932
+ the label-paint tier. Adds a 4th independent
5933
+ channel to the orphan visual signature at rest:
5934
+ R499 fontStyle italic (typography style)
5935
+ R503 '3 6' dash pattern (rect stroke)
5936
+ R506 lower rect fill-opacity (rect fill)
5937
+ R551 lower label opacity (label paint) ← this round
5938
+ Four independent channels (typography style +
5939
+ stroke pattern + rect fill density + label paint
5940
+ density) collectively encode the catchall semantic
5941
+ at rest — orphan band reads as "misc bucket, less
5942
+ attention-deserving" through every available paint
5943
+ channel, no chrome / color / geometry change.
5944
+ Pin and hover branches UNCHANGED — orphan label
5945
+ restores to full opacity 1 on inspection, same as
5946
+ prefix groups. The differentiation lives ONLY in
5947
+ the unsolicited rest state. The ~27% drop (0.55 →
5948
+ 0.4) is dimmer than R506's ~40% (rect could
5949
+ tolerate it; small 9px text needs more residual
5950
+ paint to stay legible) — orphan label stays
5951
+ readable when scanning, just clearly quieter.
5952
+ Pure paint axis; bbox unchanged; R51 SVG sentinel
5953
+ safety untouched (overlap-test gates to g[data-
5954
+ node], this group-label is invisible to it).
5955
+ transition list (R55/R432/R457/R479: fill, ls,
5956
+ fw, filter all 200ms) already eases opacity since
5957
+ `opacity 300ms ease-out` lives in the parent <text>
5958
+ CSS — wait, only filter/ls/fw/fill 200ms are
5959
+ listed. Need to add 'opacity 200ms ease-out' for
5960
+ smooth orphan opacity flip on pin/hover transitions
5961
+ (currently opacity 0.55 → 1 was snapping).
5962
+ data-group-label-opacity attr exposes the resolved
5963
+ value for tests. */
5964
+ /* Round 561 — opacity ladder gains inspection-
5965
+ overrides-encoding branch via isMemberAliasHovered.
5966
+ Resolution order (most-emphatic first):
5967
+ isPinned || isHovered → 1 (direct attention)
5968
+ isMemberAliasHovered → 1 (R561 inspection)
5969
+ box.isOrphan → 0.4 (R551 orphan rest)
5970
+ (default) → 0.55
5971
+ R484/R485/R486-family mirror. */
5972
+ opacity={isPinned || isHovered || isMemberAliasHovered ? 1 : box.isOrphan ? 0.4 : 0.55}
5973
+ data-group-label-opacity={isPinned || isHovered || isMemberAliasHovered ? 1 : box.isOrphan ? 0.4 : 0.55}
4885
5974
  data-group-label-hovered={isHovered && !isPinned ? 'true' : 'false'}
5975
+ data-group-label-member-alias-hovered={isMemberAliasHovered ? 'true' : 'false'}
4886
5976
  data-group-label-font-weight={isPinned ? '800' : '700'}
4887
5977
  /* Round 479 / Loop — extend drop-shadow visual-polish
4888
5978
  family to a 4th anchor: group-label parent text
@@ -4905,17 +5995,94 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4905
5995
  transition list extends to include 'filter 200ms
4906
5996
  ease-out' alongside the existing fill/ls/fw/opacity
4907
5997
  200ms tweens. */
4908
- data-group-label-glow={isPinned ? 'true' : 'false'}
5998
+ /* Round 538 / Loop — extends R479 group-label drop-
5999
+ shadow from pin-only to ALSO fire on hover, with
6000
+ a 2-tier alpha ladder matching the R432 letter-
6001
+ spacing 3-tier (hover at 0.25px / pin at 0.5px)
6002
+ pattern. Pre-R538 the paint axis was binary (lit
6003
+ on pin, dark on hover); R538 adds a softer hover
6004
+ glow that distinguishes from the stronger pin
6005
+ glow without losing the "active state lights up"
6006
+ gesture.
6007
+ 2-tier alpha ladder:
6008
+ pin (committed) cyan 80 hex (~50% alpha)
6009
+ hover (preview) cyan 4d hex (~30% alpha)
6010
+ rest none
6011
+ Pin signature stays distinctively brighter, but
6012
+ hover now telegraphs paint-axis attention too.
6013
+ Sibling to R534 edge-badge hover-precedence
6014
+ extension at the drop-shadow family. R479 hue
6015
+ (pal.legendAccent) preserved across both tiers.
6016
+ data-group-label-glow attr upgraded from binary
6017
+ ('true'/'false') to 3-value ('pin' | 'hover' |
6018
+ 'false') so tests can distinguish gate cause. */
6019
+ data-group-label-glow={isPinned ? 'pin' : isHovered ? 'hover' : 'false'}
6020
+ /* Round 499 / Loop — orphan band "其他" label gets
6021
+ fontStyle: italic to visually distinguish the
6022
+ catchall from real prefix-group bands. Pre-R499
6023
+ the orphan box label rendered identically to
6024
+ prefix-group labels (Hero D fontSize=9, fw=700,
6025
+ opacity 0.55 rest), so users had to read the
6026
+ literal text "其他" to identify the catchall. R499
6027
+ adds a pure-typography differentiation: italic
6028
+ signals "this is the misc bucket, not a real
6029
+ named group" while preserving full opacity
6030
+ affordance on hover/pin — the orphan box stays
6031
+ equally inspectable, just typographically marked
6032
+ as a different category. No geometry change
6033
+ (italic shifts glyph slant within the same bbox),
6034
+ no opacity loss, no behavior change. Sibling to
6035
+ R432 letter-spacing 3-tier + R457 pin fw-lift +
6036
+ R479 pin drop-shadow at the group-label scope.
6037
+ Falls under 配色 / 节点视觉 themes per the prompt;
6038
+ advances the "信息密度" axis by encoding
6039
+ category-distinction into a single typography
6040
+ channel without adding visual chrome. */
6041
+ /* Round 571 / Loop — group-label parent text joins the
6042
+ per-element brightness consistency family (R501/
6043
+ R558/R564/R567/R570) at uniform +15%. 8th anchor.
6044
+ Stacks brightness(1.15) onto the existing R479/R538
6045
+ drop-shadow filter — same R564/R570 stacked filter
6046
+ pattern (drop-shadow + brightness in one CSS chain).
6047
+ Pre-R571 the group-label parent text lifted in 5
6048
+ axes on hover/pin (fill + ls 3-tier + fw on pin +
6049
+ drop-shadow + opacity) but the glyph chromatically
6050
+ stayed at flat fill brightness. R571 adds the
6051
+ brightness axis to the glyph itself for cross-
6052
+ element consistency with the rest of the per-
6053
+ element brightness family.
6054
+ Filter chain on isPinned: `drop-shadow(0 0 3px
6055
+ ${pal.legendAccent}80) brightness(1.15)`.
6056
+ On isHovered (weaker tier): `drop-
6057
+ shadow(0 0 3px ${pal.legendAccent}4d) brightness(1.15)`.
6058
+ Rest: undefined.
6059
+ Per-element brightness family — 8 anchors at +15%:
6060
+ R501 vendor.logo image
6061
+ R558 vendor monogram
6062
+ R558 prefix-group fallback
6063
+ R564 alias text (stacked w/ DS)
6064
+ R567 node sub-text
6065
+ R570 edge-badge digit
6066
+ R571 group-label parent text ← this round
6067
+ transition list already includes 'filter 200ms ease-
6068
+ out' from R479 — no change needed. R432 ls + R457
6069
+ fw + R479 drop-shadow + R551 orphan opacity all
6070
+ preserved. */
4909
6071
  style={{
4910
6072
  transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out, font-weight 200ms ease-out, opacity 200ms ease-out, filter 200ms ease-out',
4911
6073
  letterSpacing: isPinned ? '0.5px' :
4912
6074
  isHovered ? '0.25px' : '0px',
6075
+ fontStyle: box.isOrphan ? 'italic' : undefined,
4913
6076
  filter: isPinned
4914
- ? `drop-shadow(0 0 3px ${pal.legendAccent}80)`
4915
- : undefined,
6077
+ ? `drop-shadow(0 0 3px ${pal.legendAccent}80) brightness(1.15)`
6078
+ : isHovered
6079
+ ? `drop-shadow(0 0 3px ${pal.legendAccent}4d) brightness(1.15)`
6080
+ : undefined,
4916
6081
  }}
4917
6082
  data-group-label={box.key}
4918
6083
  data-group-label-pinned={isPinned ? 'true' : 'false'}
6084
+ data-group-label-orphan={box.isOrphan ? 'true' : 'false'}
6085
+ data-group-label-brightness={(isPinned || isHovered) ? '1.15' : '1'}
4919
6086
  >
4920
6087
  {box.key}
4921
6088
  {/* Round 19 / Loop: member-count chip. Inline tspan stays
@@ -5377,6 +6544,19 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
5377
6544
  unchanged at the join with the arrow marker).
5378
6545
  data-edge-visible-linecap attr exposes the value
5379
6546
  for tests. */}
6547
+ {/* Round 582 — edge visible flow path joins per-element
6548
+ brightness family at 21st anchor. Stacks
6549
+ brightness(1.15) with the existing url(#topo-glow)
6550
+ SVG filter (cyber) or applies plain brightness
6551
+ (light). CSS filter accepts mixed url() + function
6552
+ values; inline style.filter overrides any
6553
+ attribute-level filter. Closes edge-tier brightness
6554
+ sub-family at 2 surfaces:
6555
+ R581 flow-rail (dashed underline) brightness
6556
+ R582 visible path (primary curve) brightness ← this round
6557
+ transition list extends to include 'filter 300ms
6558
+ ease-out' matching the existing opacity/sw/stroke
6559
+ cadence. */}
5380
6560
  <path
5381
6561
  d={path}
5382
6562
  fill="none"
@@ -5384,15 +6564,18 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
5384
6564
  strokeWidth={renderWidth}
5385
6565
  strokeLinecap="round"
5386
6566
  opacity={Math.min(1, (isLight ? 0.22 : 0.28) * fresh * edgeOpacityMul)}
5387
- filter={isLight ? undefined : 'url(#topo-glow)'}
5388
6567
  markerEnd={`url(#${arrowId})`}
5389
6568
  data-edge-visible={link.key}
5390
6569
  data-edge-visible-linecap="round"
5391
6570
  data-edge-visible-endpoint-hovered={isEndpointHoveredEdge ? 'true' : 'false'}
5392
6571
  data-edge-visible-stroke-width={renderWidth}
6572
+ data-edge-visible-brightness={(isHoveredEdge || isEndpointHoveredEdge) ? '1.15' : '1'}
5393
6573
  style={{
5394
6574
  pointerEvents: 'none',
5395
- transition: 'opacity 300ms ease-out, stroke-width 300ms ease-out, stroke 300ms ease-out',
6575
+ transition: 'opacity 300ms ease-out, stroke-width 300ms ease-out, stroke 300ms ease-out, filter 300ms ease-out',
6576
+ filter: (isHoveredEdge || isEndpointHoveredEdge)
6577
+ ? (isLight ? 'brightness(1.15)' : 'url(#topo-glow) brightness(1.15)')
6578
+ : (isLight ? undefined : 'url(#topo-glow)'),
5396
6579
  }}
5397
6580
  />
5398
6581
  {/* Round 378 / Loop: edge flow-path dashed-rail picks
@@ -5442,7 +6625,23 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
5442
6625
  data-edge-flow-rail-linecap="round"
5443
6626
  data-edge-flow-rail-stroke-width={(isHoveredEdge || isEndpointHoveredEdge) ? 1.5 : 1}
5444
6627
  data-edge-flow-rail-lifted={(isHoveredEdge || isEndpointHoveredEdge) ? 'true' : 'false'}
5445
- style={{ transition: 'opacity 300ms ease-out, stroke 300ms ease-out, stroke-width 300ms ease-out' }}
6628
+ /* Round 581 flow-rail joins per-element brightness
6629
+ family at 20th anchor. Adds brightness(1.15) on
6630
+ edge hover or endpoint hover. Joins R437 sw-lift
6631
+ paint pattern at the dashed-underline tier — when
6632
+ an edge is in focus, the rail's stroke widens
6633
+ (R437) AND brightens (R581) together, reading
6634
+ as a coherent rail-lift gesture under the flow.
6635
+ transition list extends to include 'filter 300ms
6636
+ ease-out' matching the R245/R437 cadence on this
6637
+ surface. */
6638
+ data-edge-flow-rail-brightness={(isHoveredEdge || isEndpointHoveredEdge) ? '1.15' : '1'}
6639
+ style={{
6640
+ transition: 'opacity 300ms ease-out, stroke 300ms ease-out, stroke-width 300ms ease-out, filter 300ms ease-out',
6641
+ filter: (isHoveredEdge || isEndpointHoveredEdge)
6642
+ ? 'brightness(1.15)'
6643
+ : undefined,
6644
+ }}
5446
6645
  />
5447
6646
  {!reducedMotion && (
5448
6647
  /* Round 103 / Loop: phase-stagger the particles so
@@ -5512,7 +6711,31 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
5512
6711
  data-edge-particle-lifted={(isHoveredEdge || isEndpointHoveredEdge) ? 'true' : 'false'}
5513
6712
  data-edge-particle-opacity-rest={Math.min(1, fresh * edgeOpacityMul).toFixed(2)}
5514
6713
  data-edge-particle-opacity-lifted={(isHoveredEdge || isEndpointHoveredEdge) ? 'true' : 'false'}
5515
- style={{ transition: 'fill 200ms ease-out, opacity 200ms ease-out, r 200ms ease-out' }}
6714
+ /* Round 583 flow particle joins per-element
6715
+ brightness family at 22nd anchor. Adds
6716
+ brightness(1.15) on edge hover or endpoint
6717
+ hover, joining R485 opacity inspection-override
6718
+ + R422 r-lift (4 → 4.5) + R164 hover-r-lift
6719
+ (4.5 → 5.5). Particle now has 4-axis active
6720
+ signature on edge inspection:
6721
+ R485 opacity (freshness → 1.0)
6722
+ R164 r 4.5 → 5.5
6723
+ R422 r-base 4 → 4.5 (visual-weight bump)
6724
+ R583 brightness(1.15) ← this round
6725
+ Particle becomes the brightest paint element
6726
+ along the edge during inspection. */
6727
+ data-edge-particle-brightness={(isHoveredEdge || isEndpointHoveredEdge) ? '1.15' : '1'}
6728
+ style={{
6729
+ transition: 'fill 200ms ease-out, opacity 200ms ease-out, r 200ms ease-out, filter 200ms ease-out',
6730
+ /* R583 — stack brightness(1.15) onto the existing
6731
+ url(#topo-glow) (cyber) or apply plain brightness
6732
+ (light). Inline style.filter overrides attribute
6733
+ filter; stacked syntax preserves the cyber glow
6734
+ on hover. Same R582 visible-path stack pattern. */
6735
+ filter: (isHoveredEdge || isEndpointHoveredEdge)
6736
+ ? (isLight ? 'brightness(1.15)' : 'url(#topo-glow) brightness(1.15)')
6737
+ : undefined,
6738
+ }}
5516
6739
  >
5517
6740
  <animateMotion
5518
6741
  dur={`${duration}s`}
@@ -6002,11 +7225,48 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6002
7225
  data-edge-badge-opacity-rest={isLight ? 0.95 : 0.85}
6003
7226
  data-edge-badge-opacity-hover="1"
6004
7227
  data-edge-badge-opacity-active="1"
6005
- data-edge-badge-glow={isHot ? 'true' : 'false'}
7228
+ data-edge-badge-glow={(isHoveredEdge || isPinned) ? 'hover' : isHot ? 'hot' : 'false'}
7229
+ /* Round 534 / Loop — extends edge-badge drop-shadow
7230
+ coverage from hot-only (R480 amber) to also fire
7231
+ on hover/pin with a cyan accent glow. Pre-R534
7232
+ the badge's hover/pin lifted r (R164 9 → 10.5)
7233
+ + sw (R394 1.25 → 1.5) + opacity (R395/R396 →
7234
+ 1.0), but the paint axis stayed at the badge's
7235
+ rest fill — no glow to telegraph "in focus" at
7236
+ the paint layer. R534 closes that 4-axis hover-
7237
+ lift parity by adding drop-shadow glow on
7238
+ (hovered || pinned).
7239
+ Precedence: (hover || pin) wins over isHot when
7240
+ BOTH true — interactive signal (user is
7241
+ inspecting) overrides informational signal
7242
+ (hot lane). When only isHot fires (no hover/
7243
+ pin) the amber R480 glow remains; the hover/
7244
+ pin case paints cyan/teal `pal.legendAccent`
7245
+ at 0x99 alpha (~60%) — bright enough to read
7246
+ as "lit" but won't overwhelm at small badge
7247
+ size (r=10.5).
7248
+ Edge-badge 4-axis hover-lift parity now:
7249
+ R164 r 9 → 10.5
7250
+ R394 stroke-wd 1.25 → 1.5
7251
+ R395 opacity rest → 1.0
7252
+ R534 filter none → drop-shadow glow ← this round
7253
+ Drop-shadow visual-polish family extension —
7254
+ edge-badge surface upgraded from single-gate
7255
+ (R480 isHot) to two-gate (isHot OR hover-pin).
7256
+ transition list already includes filter 200ms
7257
+ ease-out (R480). data-edge-badge-glow attr
7258
+ upgraded from `isHot ? true : false` to a
7259
+ 3-value string: 'hot' | 'hover' | 'false' so
7260
+ tests can distinguish gate cause.
7261
+ R51 sentinel safety: badge is edge-internal
7262
+ (not g[data-node] ancestor); filter is paint-
7263
+ only; bbox unchanged. */
6006
7264
  style={{
6007
- filter: isHot
6008
- ? `drop-shadow(0 0 3px ${hotStroke}80)`
6009
- : undefined,
7265
+ filter: (isHoveredEdge || isPinned)
7266
+ ? `drop-shadow(0 0 3px ${pal.legendAccent}99)`
7267
+ : isHot
7268
+ ? `drop-shadow(0 0 3px ${hotStroke}80)`
7269
+ : undefined,
6010
7270
  transition: 'r 180ms ease-out, stroke 300ms ease-out, stroke-width 300ms ease-out, fill 200ms ease-out, opacity 200ms ease-out, filter 200ms ease-out',
6011
7271
  }}
6012
7272
  />
@@ -6096,6 +7356,31 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6096
7356
  data-edge-badge-text={link.key}
6097
7357
  data-edge-badge-text-pin={(isPinned || isHot) ? 'true' : 'false'}
6098
7358
  data-edge-badge-text-font-size="11"
7359
+ /* Round 570 / Loop — edge-badge digit joins the per-
7360
+ element brightness consistency family (R501/R558/
7361
+ R564/R567) at uniform +15%. 7th anchor.
7362
+ Gate: (isHoveredEdge || isPinned || isHot) — same
7363
+ 3-tier set as R431 ls (with hover the mid step
7364
+ and pin/hot the strong step). Brightness lifts
7365
+ uniformly across all 3 active sub-states; the
7366
+ ls/fw axes still distinguish hover from pin/hot.
7367
+ Pure paint axis on the digit glyph; bbox
7368
+ unchanged. The R540 badge-circle drop-shadow
7369
+ sits on the parent CIRCLE element (separate
7370
+ filter); the digit's filter is independent and
7371
+ doesn't compound with circle filter (different
7372
+ SVG element).
7373
+ Per-element brightness family — 7 anchors at +15%:
7374
+ R501 vendor.logo image
7375
+ R558 vendor monogram
7376
+ R558 prefix-group fallback
7377
+ R564 alias text (stacked w/ DS)
7378
+ R567 node sub-text
7379
+ R570 edge-badge digit ← this round
7380
+ Plus runtime badge drop-shadow (R559) on same
7381
+ isNodeActive gate. data-edge-badge-text-brightness
7382
+ attr surfaces the lift for tests. */
7383
+ data-edge-badge-text-brightness={(isHoveredEdge || isPinned || isHot) ? '1.15' : '1'}
6099
7384
  style={{
6100
7385
  pointerEvents: 'none',
6101
7386
  fontVariantNumeric: 'tabular-nums',
@@ -6114,7 +7399,10 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6114
7399
  now): R344/R345/R347/R351/R420/R427/R431. */
6115
7400
  letterSpacing: (isPinned || isHot) ? '0.4px' :
6116
7401
  isHoveredEdge ? '0.2px' : '0px',
6117
- transition: 'letter-spacing 300ms ease-out, font-weight 300ms ease-out',
7402
+ filter: (isHoveredEdge || isPinned || isHot)
7403
+ ? 'brightness(1.15)'
7404
+ : undefined,
7405
+ transition: 'letter-spacing 300ms ease-out, font-weight 300ms ease-out, filter 300ms ease-out',
6118
7406
  }}
6119
7407
  >{link.count}</text>
6120
7408
  </g>
@@ -6306,6 +7594,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6306
7594
  data-hub-busyness={busy}
6307
7595
  data-topo-hub-halo-radius={haloR}
6308
7596
  data-topo-hub-halo-hovered={isHaloHovered ? 'true' : 'false'}
7597
+ data-topo-hub-halo-glow={!reducedMotion && hoveredHub ? 'true' : 'false'}
6309
7598
  data-topo-hub-halo-trough={isLight ? troughLight : troughDark}
6310
7599
  data-topo-hub-halo-peak={isLight ? peakLight : peakDark}
6311
7600
  /* Round 253 / Loop: hub grounding halo fill transition
@@ -6317,10 +7606,59 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6317
7606
  conflict.
6318
7607
  R451: r as CSS property (R197/R198 idiom) so the
6319
7608
  hover-radius tween eases smoothly under the same
6320
- 200ms cadence as fill. */
7609
+ 200ms cadence as fill.
7610
+ R536: extends hub-cluster glow QUARTET (R476 digit
7611
+ + R532 disc + R535 ring + R533 spokes) to a 5th
7612
+ tier — the halo gains drop-shadow at the outermost
7613
+ concentric ring on hub-hover. 2px blur + 0.3 alpha
7614
+ keeps the halo's glow subtle since (a) the halo is
7615
+ the LARGEST hub element (r=22 hover) and a heavier
7616
+ glow would bleed visibly past the ring tier into
7617
+ the spoke origin, and (b) the halo already SMIL-
7618
+ animates opacity (R84/R244 breath), so the visible
7619
+ glow pulses with the breath — an atmospheric
7620
+ "breathing glow" idiom rather than a static rim.
7621
+ Hub-cluster glow QUINTET (R476/R532/R533/R535/R536):
7622
+ digit (typo center) 3px emerald 0.6
7623
+ disc (r=5.5/6) 3px emerald 0.6
7624
+ ring (r=14/17) 3px emerald 0.5
7625
+ halo (r=20/22) 2px emerald 0.3 ← this round
7626
+ spokes (mesh) 1.5px cyan/teal 0.4
7627
+ Emerald palette continues through the focal-disc
7628
+ family (digit/disc/ring/halo); spokes break out
7629
+ into cyan/teal at the mesh tier. The 4-step alpha
7630
+ ladder 0.6→0.6→0.5→0.3 reads as the focal cluster
7631
+ fading outward — the OUTERMOST emerald glow is the
7632
+ softest, the focal digit is the brightest.
7633
+ filter is paint-only; SMIL animate on opacity
7634
+ continues independently (attribute vs CSS-property
7635
+ non-conflicting). transition list extends to
7636
+ 'filter 200ms ease-out' alongside fill + r.
7637
+ Drop-shadow visual-polish family extension (12
7638
+ anchors). preview.50 milestone round. data-topo-
7639
+ hub-halo-glow attr exposes the gate state. */
7640
+ data-topo-hub-halo-brightness={!reducedMotion && hoveredHub ? '1.15' : '1'}
7641
+ /* Round 577 — hub-halo joins per-element brightness
7642
+ family at 15th anchor. Stacks brightness(1.15)
7643
+ onto R536's hub-hover drop-shadow — closes the
7644
+ hub-cluster focal brightness coverage at 3
7645
+ concentric elements:
7646
+ hub digit (R575)
7647
+ hub-highlight (R574)
7648
+ hub-halo (R577) ← this round
7649
+ All 3 hub focal elements now lift through stacked
7650
+ drop-shadow + brightness on hub-hover. Halo is
7651
+ the OUTERMOST so a slight chromatic lift reads as
7652
+ the focal cluster intensifying its ambient glow
7653
+ outward. */
6321
7654
  style={{
6322
7655
  r: `${haloR}px`,
6323
- transition: 'fill 200ms ease-out, r 200ms ease-out',
7656
+ filter: !reducedMotion && hoveredHub
7657
+ ? (isLight
7658
+ ? 'drop-shadow(0 0 2px rgba(16, 185, 129, 0.3)) brightness(1.15)'
7659
+ : 'drop-shadow(0 0 2px rgba(52, 211, 153, 0.3)) brightness(1.15)')
7660
+ : undefined,
7661
+ transition: 'fill 200ms ease-out, r 200ms ease-out, filter 200ms ease-out',
6324
7662
  } as React.CSSProperties}
6325
7663
  >
6326
7664
  {/* Round 244 / Loop: hub grounding halo breath gets
@@ -6480,6 +7818,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6480
7818
  data-topo-hub-working-count={workingCount}
6481
7819
  data-topo-hub-working-count-font-size="12"
6482
7820
  data-topo-hub-working-count-hovered={hoveredHub ? 'true' : 'false'}
7821
+ data-topo-hub-working-count-brightness={!reducedMotion && hoveredHub ? '1.15' : '1'}
6483
7822
  data-topo-hub-working-count-visible={workingCount > 0 ? 'true' : 'false'}
6484
7823
  // Round 209 / Loop: hub workingCount digit scales 1.0 →
6485
7824
  // 1.08 on hub-hover, matching R177's r 14→17 ring grow.
@@ -6530,22 +7869,109 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6530
7869
  so the glow eases under the same cadence as the
6531
7870
  scale + fw + fill axes. */
6532
7871
  data-topo-hub-working-count-glow={!reducedMotion && hoveredHub ? 'true' : 'false'}
7872
+ /* Round 507 / Loop — focal recede. When ANY non-hub
7873
+ canvas surface is hovered (a node / an edge / a
7874
+ group label / a legend row / a vendor chip), the
7875
+ hub-center workingCount digit fades to 0.85 opacity,
7876
+ signaling "you're inspecting elsewhere, hub recedes
7877
+ to background." When the user un-hovers (or hovers
7878
+ the hub itself), opacity returns to 1.0. Pure paint
7879
+ polish at the canvas's most prominent focal point.
7880
+ Hits 信息密度 + 动效 themes — the hub digit gives
7881
+ way visually to the surface under inspection,
7882
+ reinforcing the "this is the focal point right now"
7883
+ gesture without requiring users to track which
7884
+ surface holds attention.
7885
+ Gate excludes hoveredHub specifically: hovering the
7886
+ hub itself should LIFT the digit (R425 fw bump +
7887
+ R476 glow + R209 scale 1.08) — the existing hover-
7888
+ on-hub signature is intact; only inspection
7889
+ ELSEWHERE recedes the hub.
7890
+ Composed from existing hoveredAlias / hoveredEdge-
7891
+ Key / hoveredGroupLabel / hoveredStatus / hovered-
7892
+ Vendor — no new state. 300ms ease-out opacity
7893
+ transition already in the style list (existing R213
7894
+ transition spec), so the fade rides on existing
7895
+ infrastructure.
7896
+ data-topo-hub-recede attr surfaces the gate state
7897
+ for tests. */
7898
+ data-topo-hub-recede={
7899
+ (hoveredAlias || hoveredEdgeKey || hoveredGroupLabel ||
7900
+ hoveredStatus || hoveredVendor) && !hoveredHub ? 'true' : 'false'
7901
+ }
7902
+ /* Round 527 / Loop — focal-amplify family extension to a
7903
+ 2nd anchor. R511 introduced focal-amplify at the hub-
7904
+ highlight disc (base opacity 0.95 → 1.0 on hover); R527
7905
+ extends to the hub-center workingCount digit with a
7906
+ letter-spacing tween 0 → 0.3px on hub-hover.
7907
+ Composes with existing 3-axis hub-hover signature on
7908
+ this element:
7909
+ R209 transform scale(1.08) geometry
7910
+ R425 fontWeight 700 → 800 typography weight
7911
+ R476 filter drop-shadow glow paint
7912
+ R527 letter-spacing 0 → 0.3px typography kerning ← this round
7913
+ tabular-nums (R225) preserved — each digit cell keeps
7914
+ fixed width; the inter-digit advance grows by 0.3px
7915
+ per gap. Single-digit counts (1-9) show no kerning
7916
+ effect; multi-digit counts (10+) show the spread as
7917
+ info-density signaling. Sibling to R427/R431/R432/
7918
+ R433/R434 (hover-letter-spacing family at panel-text
7919
+ scope) — R527 brings the same idiom to the canvas's
7920
+ most-read scalar.
7921
+ Reduced-motion gate matches R209 scale, R425 fw, R476
7922
+ filter — !reducedMotion gates the lift; reducedMotion
7923
+ users see static digit baseline regardless of hover.
7924
+ Focal-amplify family extension (2 anchors): R511 hub-
7925
+ highlight opacity / R527 hub-digit letter-spacing.
7926
+ transition list extends to include `letter-spacing
7927
+ 200ms ease-out`, matching the cadence of the other
7928
+ hub-hover axes. data-topo-hub-working-count-letter-
7929
+ spacing attr exposes the resolved value for tests. */
7930
+ data-topo-hub-working-count-letter-spacing={!reducedMotion && hoveredHub ? '0.3px' : '0px'}
6533
7931
  style={{
6534
7932
  pointerEvents: 'none',
6535
7933
  transform: !reducedMotion && hoveredHub ? 'scale(1.08)' : 'scale(1)',
6536
7934
  transformBox: 'fill-box',
6537
7935
  transformOrigin: 'center',
7936
+ opacity: (hoveredAlias || hoveredEdgeKey || hoveredGroupLabel ||
7937
+ hoveredStatus || hoveredVendor) && !hoveredHub
7938
+ ? 0.85
7939
+ : 1,
7940
+ /* Round 575 (60-round milestone) — hub digit joins per-
7941
+ element brightness family at 14th anchor. Stacks
7942
+ brightness(1.15) onto R476's hub-hover drop-shadow
7943
+ — same R564/R570/R571/R572/R573/R574 pattern (drop-
7944
+ shadow + brightness in one filter chain). Closes
7945
+ the hub-cluster focal-element brightness coverage
7946
+ symmetrically: hub digit + hub-highlight disc
7947
+ (R574) now BOTH have stacked filter on hub-hover.
7948
+ Hub digit hub-hover signature post-R575 — 5 active
7949
+ axes:
7950
+ R209 scale 1.08 (geometry)
7951
+ R425 fw 700 → 800 (typography)
7952
+ R527 ls 0 → 0.3px (typography)
7953
+ R476 drop-shadow glow (paint halo)
7954
+ R575 brightness(1.15) (paint glow) ← this round
7955
+ Hub-cluster focal cluster (digit + highlight) now
7956
+ has UNIFIED 5-axis hub-hover signature reading as
7957
+ one tightly-coupled motion-coherent lift. */
6538
7958
  filter: !reducedMotion && hoveredHub
6539
7959
  ? (isLight
6540
- ? 'drop-shadow(0 0 2px rgba(16, 185, 129, 0.6))'
6541
- : 'drop-shadow(0 0 3px rgba(52, 211, 153, 0.6))')
7960
+ ? 'drop-shadow(0 0 2px rgba(16, 185, 129, 0.6)) brightness(1.15)'
7961
+ : 'drop-shadow(0 0 3px rgba(52, 211, 153, 0.6)) brightness(1.15)')
6542
7962
  : undefined,
7963
+ letterSpacing: !reducedMotion && hoveredHub ? '0.3px' : '0px',
6543
7964
  /* R425: font-weight 200ms appended so the hover fw
6544
7965
  bump 700 → 800 eases under the same cadence as
6545
7966
  R209 scale + R253 fill + R213 opacity.
6546
7967
  R476: filter 200ms appended so the new drop-
6547
- shadow glow eases at the same cadence. */
6548
- transition: 'transform 200ms ease-out, opacity 300ms ease-out, fill 200ms ease-out, font-weight 200ms ease-out, filter 200ms ease-out',
7968
+ shadow glow eases at the same cadence.
7969
+ R507: opacity 300ms (existing in list) covers
7970
+ the new focal-recede fade.
7971
+ R527: letter-spacing 200ms appended so the new
7972
+ hover-kerning bump eases at the same cadence
7973
+ as the other axes. */
7974
+ transition: 'transform 200ms ease-out, opacity 300ms ease-out, fill 200ms ease-out, font-weight 200ms ease-out, filter 200ms ease-out, letter-spacing 200ms ease-out',
6549
7975
  fontVariantNumeric: 'tabular-nums',
6550
7976
  }}
6551
7977
  >
@@ -6591,20 +8017,188 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6591
8017
  + R213 always-mount opacity-gate + pointerEvents:none
6592
8018
  + R365 r=5.5 all preserved. data-topo-hub-highlight-
6593
8019
  opacity attr exposes the resolved value for tests. */}
6594
- <circle
6595
- cx={cx} cy={cy} r="5.5"
6596
- fill="#d1fae5"
6597
- opacity={workingCount > 0 ? 0 : 0.95}
6598
- data-topo-hub-highlight
6599
- data-topo-hub-highlight-visible={workingCount > 0 ? 'false' : 'true'}
6600
- data-topo-hub-highlight-radius="5.5"
6601
- data-topo-hub-highlight-opacity={workingCount > 0 ? 0 : 0.95}
6602
- data-topo-hub-highlight-breath={!reducedMotion && workingCount === 0 ? 'true' : 'false'}
6603
- style={{
6604
- pointerEvents: 'none',
6605
- transition: 'opacity 300ms ease-out',
6606
- }}
6607
- >
8020
+ {/* Round 508 / Loop — focal-recede pattern 2nd anchor.
8021
+ Extends R507's hub-digit recede to the hub-highlight
8022
+ circle so the hub focal CLUSTER (digit at z-top + this
8023
+ idle-state highlight beneath) recedes as a unit when
8024
+ canvas attention is elsewhere. Computed once: a single
8025
+ non-hub-hover gate drives BOTH the digit (R507) AND
8026
+ this highlight (R508) so they always co-move.
8027
+ Recede multiplies the visible opacity by 0.85 — when
8028
+ workingCount===0 the rest opacity 0.95 becomes 0.81
8029
+ during external-hover; when workingCount>0 the
8030
+ opacity stays 0 (invisible) regardless of recede.
8031
+ Additionally, when recede is active the SMIL breath
8032
+ animation halts (animate node un-mounts) so the
8033
+ receded state reads as quietly static, not pulsing
8034
+ at 0.85↔1.0 against the recede multiplier (which
8035
+ would visually conflict — competing 15% drops). On
8036
+ un-hover the animate re-mounts and breath resumes.
8037
+ data-topo-hub-recede on both digit AND highlight
8038
+ provides a stable test handle for the unified-recede
8039
+ gate.
8040
+ Composed from existing hover state vars — no new
8041
+ state. Pure paint axis. */}
8042
+ {(() => {
8043
+ const hubRecede = !!((hoveredAlias || hoveredEdgeKey || hoveredGroupLabel ||
8044
+ hoveredStatus || hoveredVendor) && !hoveredHub);
8045
+ /* Round 511 / Loop — hub-highlight gains a 3rd opacity tier
8046
+ (hovered-amplify). Pre-R511 the highlight had 2 visible
8047
+ states: rest (0.95) and recede (0.81 = 0.95 × 0.85).
8048
+ When the hub itself was hovered, the digit got R425 fw
8049
+ lift + R476 drop-shadow + R209 scale-1.08, but the
8050
+ highlight disc sibling stayed at 0.95 — the focal
8051
+ cluster lifted in 3 channels (typography/paint/scale)
8052
+ but the highlight didn't participate.
8053
+ R511 closes that asymmetry: when hoveredHub is true,
8054
+ highlight base opacity lifts to 1.0 (5% boost from
8055
+ rest 0.95). Cluster now lifts as a unit on hub-hover,
8056
+ just like it recedes as a unit on non-hub-hover
8057
+ (R508).
8058
+ 3-state opacity ladder:
8059
+ hub-hovered: baseOpacity = 1.0 (R511 NEW)
8060
+ rest (no hover): baseOpacity = 0.95 (existing)
8061
+ non-hub canvas hov: baseOpacity × 0.85 = 0.81 (R508)
8062
+ Composes cleanly: hubRecede gate requires !hoveredHub,
8063
+ so the hovered-amplify and recede states are mutually
8064
+ exclusive (they can't both fire). breathActive
8065
+ continues to halt on either non-rest state (recede OR
8066
+ hub-hover would visually compete with the 0.85↔1
8067
+ breath — clean for the unit-lift semantic too). */
8068
+ const baseOpacity = workingCount > 0 ? 0
8069
+ : hoveredHub ? 1.0
8070
+ : 0.95;
8071
+ const resolvedOpacity = hubRecede ? baseOpacity * 0.85 : baseOpacity;
8072
+ const breathActive = !reducedMotion && workingCount === 0 && !hubRecede && !hoveredHub;
8073
+ /* Round 529 / Loop — focal-amplify family 3rd anchor.
8074
+ Hub-highlight gains geometric amplify r 5.5 → 6 on
8075
+ hub-hover, mirroring R451's hub-halo r 20 → 22 hover
8076
+ pattern. Pre-R529 the highlight had paint-axis
8077
+ amplify only (R511 opacity 0.95 → 1.0 on hub-hover);
8078
+ R529 adds geometric amplify so the focal disc
8079
+ BREATHES outward on hub attention, like the halo
8080
+ does. Composes with the existing 2-axis hub-hover
8081
+ lift on this element:
8082
+ R511 opacity 0.95 → 1.0 paint (focal-amplify 1st)
8083
+ R529 r 5.5 → 6 geometry (this round)
8084
+ Implementation matches R451: CSS `r` property
8085
+ (R197/R198 idiom) for smooth interpolation. SVG
8086
+ attribute `r="5.5"` provides SSR fallback and serves
8087
+ as default; inline style.r overrides for animated
8088
+ value. transition list extends to include `r 200ms
8089
+ ease-out`, matching the fill cadence (also 200ms);
8090
+ opacity transition stays at 300ms (existing).
8091
+ r 6 sits well inside the existing visual envelope
8092
+ (next-larger sibling r=10 hub core, r=14 hub hover
8093
+ ring). The 0.5px lift is +9% radius / +19% area —
8094
+ enough to read as 'lift' without breaching the core
8095
+ boundary or invalidating overlap-test invariants.
8096
+ SMIL animate on opacity continues independently
8097
+ (animateAttr='opacity' vs CSS-property r — non-
8098
+ conflicting, same pattern R451 noted for halo).
8099
+ Focal-amplify family extension (3 anchors):
8100
+ R511 hub-highlight opacity 0.95 → 1.0
8101
+ R527 hub-digit letter-spacing 0 → 0.3px
8102
+ R529 hub-highlight radius 5.5 → 6 ← this round
8103
+ data-topo-hub-highlight-radius attr now reports the
8104
+ dynamic value (was static '5.5'). */
8105
+ const highlightR = !reducedMotion && hoveredHub ? 6 : 5.5;
8106
+ return (
8107
+ <circle
8108
+ cx={cx} cy={cy} r="5.5"
8109
+ /* Round 509 / Loop — 配色 cross-theme cleanup. Pre-R509
8110
+ the hub-highlight fill was hardcoded `#d1fae5`
8111
+ (emerald-100, a pale tone). On the light theme this
8112
+ near-white green ran against a pale background at
8113
+ 0.95 opacity — the disc was effectively invisible.
8114
+ Matches the existing R253 halo theme-inversion
8115
+ pattern (line ~6481): light theme picks the dark
8116
+ vibrant emerald (#10b981 emerald-600), dark theme
8117
+ keeps the pale emerald (#d1fae5 emerald-100). Both
8118
+ read at the same 0.95 opacity against their
8119
+ respective backdrops — light gets a saturated
8120
+ focal dot; dark keeps the soft glow signature.
8121
+ Pure paint axis (fill change only); bbox unchanged;
8122
+ R51 SVG sentinel safety untouched.
8123
+ transition list already includes `fill 200ms`?
8124
+ Actually the existing transition spec is `opacity
8125
+ 300ms ease-out` — fill change on theme toggle
8126
+ will be instant. That's acceptable: theme toggle
8127
+ is a discrete event, and the halo (line 6500)
8128
+ already snaps fill on theme toggle the same way
8129
+ (`fill 200ms ease-out` was added later to halo
8130
+ via R253). Future round could add `fill 200ms`
8131
+ to highlight too if theme-switch flicker is
8132
+ noticed. */
8133
+ fill={isLight ? '#10b981' : '#d1fae5'}
8134
+ opacity={resolvedOpacity}
8135
+ data-topo-hub-highlight
8136
+ data-topo-hub-highlight-visible={workingCount > 0 ? 'false' : 'true'}
8137
+ data-topo-hub-highlight-radius={highlightR}
8138
+ data-topo-hub-highlight-opacity={resolvedOpacity}
8139
+ data-topo-hub-highlight-breath={breathActive ? 'true' : 'false'}
8140
+ data-topo-hub-highlight-recede={hubRecede ? 'true' : 'false'}
8141
+ data-topo-hub-highlight-hovered={!reducedMotion && hoveredHub ? 'true' : 'false'}
8142
+ data-topo-hub-highlight-glow={!reducedMotion && hoveredHub ? 'true' : 'false'}
8143
+ /* Round 574 — hub-highlight joins per-element brightness
8144
+ family at 13th anchor. Stacks brightness(1.15) onto
8145
+ R532's drop-shadow filter — same R564/R570/R571/R572/
8146
+ R573 pattern (drop-shadow + brightness in one filter
8147
+ chain). Hub idle disc now has 3 active hub-hover
8148
+ axes: R511 opacity 0.95 → 1.0 + R529 r 5.5 → 6 +
8149
+ R574 brightness(1.15). data-topo-hub-highlight-
8150
+ brightness attr surfaces the lift. */
8151
+ data-topo-hub-highlight-brightness={!reducedMotion && hoveredHub ? '1.15' : '1'}
8152
+ /* Round 510 / Loop — R509 follow-on: theme-toggle fill
8153
+ ease. Pre-R510 the hub-highlight transition spec only
8154
+ listed `opacity 300ms ease-out`. When R509 introduced
8155
+ theme-conditional fill (#d1fae5 ↔ #10b981), the fill
8156
+ change SNAPPED on theme toggle because the transition
8157
+ list didn't include `fill`. R510 extends to `fill
8158
+ 200ms ease-out` so theme cycles smoothly through the
8159
+ emerald palette. 200ms timing matches the R253 halo
8160
+ fill transition (line ~6500) — both hub-cluster
8161
+ theme transitions now share a cadence so the focal
8162
+ cluster (digit + highlight + halo) eases as a unit.
8163
+ R508's recede opacity transition unchanged (300ms);
8164
+ fill is independent.
8165
+ R529: r as CSS property (R197/R198 idiom) + `r
8166
+ 200ms ease-out` appended to transition list so
8167
+ the new hub-hover radius lift (5.5 → 6) eases
8168
+ under the same fill cadence. SVG attr r="5.5"
8169
+ above provides SSR fallback; inline style.r
8170
+ wins the cascade for the dynamic value.
8171
+ R532: filter drop-shadow glow on hub-hover —
8172
+ sibling to R476 hub-digit drop-shadow at the
8173
+ same gate (hoveredHub && !reducedMotion). Two
8174
+ adjacent hub focal elements (digit + highlight
8175
+ disc) now BOTH glow on hub-hover, reading as
8176
+ one unified focal cluster. Emerald palette
8177
+ matches R476:
8178
+ light: drop-shadow(0 0 2px rgba(16,185,129,0.6)) emerald-500
8179
+ cyber: drop-shadow(0 0 3px rgba(52,211,153,0.6)) emerald-400
8180
+ filter is paint-only (bbox unchanged); SMIL
8181
+ animate on opacity continues independently
8182
+ (animateAttr='opacity' vs CSS-property filter
8183
+ — non-conflicting). transition list extends to
8184
+ 'filter 200ms ease-out' alongside fill/r.
8185
+ Drop-shadow visual-polish family extension
8186
+ (8 anchors): R476 hub-digit / R477 legend pin-
8187
+ ring / R478 recent freshness / hot edge / group
8188
+ label / zoom-state / node alias + R532 hub-
8189
+ highlight (this round). data-topo-hub-highlight-
8190
+ glow attr exposes the gate state. */
8191
+ style={{
8192
+ pointerEvents: 'none',
8193
+ r: `${highlightR}px`,
8194
+ filter: !reducedMotion && hoveredHub
8195
+ ? (isLight
8196
+ ? 'drop-shadow(0 0 2px rgba(16, 185, 129, 0.6)) brightness(1.15)'
8197
+ : 'drop-shadow(0 0 3px rgba(52, 211, 153, 0.6)) brightness(1.15)')
8198
+ : undefined,
8199
+ transition: 'opacity 300ms ease-out, fill 200ms ease-out, r 200ms ease-out, filter 200ms ease-out',
8200
+ } as React.CSSProperties}
8201
+ >
6608
8202
  {/* Round 497 / Loop — idle-state breath (呼吸感 theme pivot
6609
8203
  from the R492-R496 press-family arc). Pre-R497 the hub
6610
8204
  idle highlight read as a static dim disc — present but
@@ -6625,10 +8219,12 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6625
8219
  rather than a pulse, matching the "quiet" semantic.
6626
8220
  data-topo-hub-highlight-breath attr exposes the
6627
8221
  resolved gate state for tests. */}
6628
- {!reducedMotion && workingCount === 0 && (
8222
+ {breathActive && (
6629
8223
  <animate attributeName="opacity" values="0.85;1;0.85" dur="4s" repeatCount="indefinite" />
6630
8224
  )}
6631
8225
  </circle>
8226
+ );
8227
+ })()}
6632
8228
  {/* R115 / Loop: hover hint ring. Stroke-only circle at r=14
6633
8229
  that fades in when the hub is hovered — the same idea
6634
8230
  R44 used for node avatars (group-hover stroke). r=14
@@ -6694,13 +8290,54 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6694
8290
  data-topo-hub-hover-ring-radius={hoveredHub ? 17 : 14}
6695
8291
  data-topo-hub-hover-ring-stroke-width="1.75"
6696
8292
  data-topo-hub-hover-ring-opacity={hoveredHub ? (isLight ? 0.85 : 0.8) : 0}
6697
- /* Round 253 / Loop: hub hover ring also gets stroke
6698
- transition for theme toggle (cyber #10b981 ↔ light
6699
- #059669). The opacity + r transitions stay for hover
6700
- lift; stroke closes the theme-snap. */
8293
+ /* Round 535 / Loop completes the hub-cluster glow
8294
+ QUARTET by adding drop-shadow to the hub-hover-ring.
8295
+ Pre-R535 the hub-hover trio (R476 digit + R532 highlight
8296
+ disc + R533 spokes) glowed in unified emerald (digit/
8297
+ disc) + cyan/teal (spokes) on hub-hover, but the ring
8298
+ itself — the outermost solid emerald boundary at
8299
+ r=14→17 — stayed flat. R535 adds the matching emerald
8300
+ drop-shadow to the ring so the FULL hub-cluster glows
8301
+ across all four concentric surfaces on hub-hover:
8302
+ digit (typography center) drop-shadow 0 0 3px emerald
8303
+ highlight disc (r=5.5/6) drop-shadow 0 0 3px emerald
8304
+ hover-ring (r=14/17) drop-shadow 0 0 3px emerald ← this round
8305
+ spokes (mesh) drop-shadow 0 0 1.5px cyan/teal
8306
+ The ring is only visible on hub-hover (opacity=0 rest);
8307
+ adding drop-shadow at the same gate means the glow shows
8308
+ the moment the ring shows — no extra state needed.
8309
+ Same R476/R532 emerald palette since the ring sits
8310
+ inside the focal-disc tier (its color is also emerald
8311
+ #059669/#10b981).
8312
+ transition list extends to include 'filter 200ms ease-
8313
+ out' alongside the existing 180ms opacity/r — slight
8314
+ cadence mismatch (180 vs 200) is acceptable; the filter
8315
+ only appears AFTER the ring fades in via opacity, and
8316
+ the 200ms vs 180ms 20ms tail difference is below
8317
+ perceptual threshold.
8318
+ Drop-shadow visual-polish family extension (11 anchors):
8319
+ the hub-cluster glow quartet (R476/R532/R533/R535) plus
8320
+ the 7 non-hub anchors (R477/R478/R479/R480/R481/R483/
8321
+ R534) makes for a thoroughly polished glow vocabulary
8322
+ across the canvas.
8323
+ data-topo-hub-hover-ring-glow attr exposes the gate
8324
+ state for tests. */
8325
+ data-topo-hub-hover-ring-glow={!reducedMotion && hoveredHub ? 'true' : 'false'}
8326
+ /* Round 579 — hub-hover-ring joins per-element brightness
8327
+ family at 18th anchor. Stacks brightness(1.15) onto
8328
+ R535's drop-shadow — same R564/R570/R571/R572/R573/R574/
8329
+ R575/R577/R578 stacked-filter pattern. Closes hub-
8330
+ cluster brightness at 4 concentric elements (digit
8331
+ R575 + highlight R574 + halo R577 + hover-ring R579). */
8332
+ data-topo-hub-hover-ring-brightness={!reducedMotion && hoveredHub ? '1.15' : '1'}
6701
8333
  style={{
6702
8334
  pointerEvents: 'none',
6703
- transition: 'opacity 180ms ease-out, r 180ms ease-out, stroke 200ms ease-out',
8335
+ filter: !reducedMotion && hoveredHub
8336
+ ? (isLight
8337
+ ? 'drop-shadow(0 0 3px rgba(16, 185, 129, 0.5)) brightness(1.15)'
8338
+ : 'drop-shadow(0 0 3px rgba(52, 211, 153, 0.5)) brightness(1.15)')
8339
+ : undefined,
8340
+ transition: 'opacity 180ms ease-out, r 180ms ease-out, stroke 200ms ease-out, filter 200ms ease-out',
6704
8341
  }}
6705
8342
  />
6706
8343
  </g>)}
@@ -7477,10 +9114,38 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7477
9114
  data-edge-endpoint-active={isEndpoint ? 'true' : 'false'}
7478
9115
  data-edge-endpoint-ring-stroke-width={isEndpoint ? 2.4 : 1.6}
7479
9116
  data-edge-endpoint-ring-radius={endpointR}
9117
+ data-edge-endpoint-ring-brightness={isEndpoint ? '1.15' : '1'}
7480
9118
  style={{
7481
9119
  pointerEvents: 'none',
7482
9120
  r: `${endpointR}px`,
7483
- transition: 'opacity 180ms ease-out, stroke-width 180ms ease-out, r 180ms ease-out',
9121
+ /* R585 endpoint emphasis ring gains filter
9122
+ brightness(1.15) when an edge endpoint
9123
+ activates. 24th anchor in per-element
9124
+ brightness family, and the FOURTH edge-tier
9125
+ paint layer:
9126
+ rail (R581)
9127
+ visible path (R582)
9128
+ flow particle (R583)
9129
+ endpoint ring (R585) ← this round
9130
+ Edge-tier brightness coverage closes at 4/4
9131
+ paint surfaces. The endpoint ring is the
9132
+ edge's affinity marker at the connected
9133
+ nodes — when an edge lights up, all four
9134
+ paint surfaces brighten together for a
9135
+ single coherent edge-active gesture spanning
9136
+ the curve + the node ends.
9137
+ Endpoint ring 4-axis active signature now:
9138
+ opacity R182 0 → 0.85/0.9
9139
+ sw R233 1.6 → 2.4
9140
+ r R442 +7 → +8
9141
+ brightness R585 — → 1.15 ← this round
9142
+ Plain brightness (no url-filter stack) since
9143
+ the endpoint ring has no rest-time filter
9144
+ attribute. Inline style.filter undefined at
9145
+ rest (no flicker; opacity=0 already hides
9146
+ the ring). */
9147
+ filter: isEndpoint ? 'brightness(1.15)' : undefined,
9148
+ transition: 'opacity 180ms ease-out, stroke-width 180ms ease-out, r 180ms ease-out, filter 180ms ease-out',
7484
9149
  } as React.CSSProperties}
7485
9150
  />
7486
9151
  );
@@ -7556,8 +9221,40 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7556
9221
  data-node-status-ring={status.label}
7557
9222
  data-node-status-ring-hovered={isRingHovered ? 'true' : 'false'}
7558
9223
  data-node-status-ring-stroke-width={ringStrokeWidth}
9224
+ data-node-status-ring-brightness={isRingHovered ? '1.15' : '1'}
7559
9225
  style={{
7560
- transition: 'fill 300ms ease-out, stroke 300ms ease-out, stroke-width 300ms ease-out',
9226
+ /* R584 status ring gets brightness(1.15) on
9227
+ hover. 23rd anchor in per-element brightness
9228
+ family. Stacks with url(#topo-glow) on
9229
+ cyber+online to preserve the SVG glow filter;
9230
+ plain brightness on light or cyber+offline.
9231
+ Same R582/R583 stacked-filter pattern: inline
9232
+ style.filter overrides the attribute filter,
9233
+ stacked syntax preserves the glow on hover.
9234
+
9235
+ Per-node hover signature now 10 layers (added
9236
+ to the R438 stack):
9237
+ R26 group translateY -2px
9238
+ R217 stroke tint
9239
+ R142 drop-shadow boost
9240
+ R427 alias letter-spacing
9241
+ R428 sub-text letter-spacing
9242
+ R429 body opacity 0.94 → 1.0
9243
+ R430 hub-spoke α+
9244
+ R435 hub-spoke sw+
9245
+ R438 status-ring sw +0.5
9246
+ R584 status-ring brightness(1.15) ← this round
9247
+
9248
+ Per-element brightness family: 23 anchors.
9249
+ Stacked-filter sub-pattern: 17 anchors. */
9250
+ filter: isRingHovered
9251
+ ? (isLight
9252
+ ? 'brightness(1.15)'
9253
+ : (isOnline
9254
+ ? 'url(#topo-glow) brightness(1.15)'
9255
+ : 'brightness(1.15)'))
9256
+ : undefined,
9257
+ transition: 'fill 300ms ease-out, stroke 300ms ease-out, stroke-width 300ms ease-out, filter 300ms ease-out',
7561
9258
  }}
7562
9259
  />
7563
9260
  );
@@ -7613,6 +9310,30 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7613
9310
  const internByAlias = /书生|书小生|intern/i.test(session.alias);
7614
9311
 
7615
9312
  if (isIntern || internByAlias || vendor.logo) {
9313
+ /* Round 501 / Loop — vendor avatar inside node circles
9314
+ gains a hover-gated brightness lift. Pre-R501 the
9315
+ avatar <image> was the only per-node surface with
9316
+ NO hover treatment: R26 lifted the card, R242 tinted
9317
+ the card stroke, R427 spread the alias letter-
9318
+ spacing, R500 added the alias drop-shadow, R208
9319
+ lifted the runtime badge ring, R443 thickened
9320
+ the badge icon stroke, R177 brightened the
9321
+ halo — but the most visually-prominent element
9322
+ (the vendor logo / 书生 coin centred in each node)
9323
+ stayed paint-static. R501 closes the per-node
9324
+ hover-affordance arc by adding a 15% brightness
9325
+ lift on hover.
9326
+ Implementation: CSS filter: brightness(1.15)
9327
+ when hoveredAlias === session.alias. Pure paint
9328
+ axis on the <image> element — no geometry change,
9329
+ no bbox shift. Modern-browser supported (Chrome 64+
9330
+ / FF 56+ / Safari 9.1+).
9331
+ Hits 节点视觉 theme. data-node-avatar-hovered
9332
+ attr surfaces the gate for tests.
9333
+ Gated on !reducedMotion as a courtesy (brightness
9334
+ transition < ~50ms still feels instant; the gate
9335
+ avoids the transition cycle for a11y users). */
9336
+ const isAvatarHovered = !reducedMotion && hoveredAlias === session.alias;
7616
9337
  return (
7617
9338
  <image
7618
9339
  href={vendor.logo ?? '/intern_avatar.png'}
@@ -7621,9 +9342,39 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7621
9342
  width={size}
7622
9343
  height={size}
7623
9344
  preserveAspectRatio="xMidYMid meet"
9345
+ data-node-avatar={session.alias}
9346
+ data-node-avatar-hovered={isAvatarHovered ? 'true' : 'false'}
9347
+ style={{
9348
+ filter: isAvatarHovered ? 'brightness(1.15)' : undefined,
9349
+ transition: 'filter 200ms ease-out',
9350
+ }}
7624
9351
  />
7625
9352
  );
7626
9353
  }
9354
+ /* Round 558 / Loop — closes the per-node avatar hover-
9355
+ affordance arc by extending R501's brightness lift
9356
+ (image-branch only) to the TWO remaining avatar
9357
+ variants: vendor monogram + prefix-group hue-hashed
9358
+ initial fallback. Pre-R558 only the vendor.logo
9359
+ image branch lifted on hover; the other two
9360
+ variants stayed paint-static under attention.
9361
+
9362
+ Per-node avatar hover-brightness family (3 anchors,
9363
+ all gated on !reducedMotion && hoveredAlias matches):
9364
+ R501 vendor.logo image filter on <image>
9365
+ R558 vendor monogram filter on wrapping <g>
9366
+ R558 prefix-group fallback filter on wrapping <g>
9367
+
9368
+ Implementation: each fallback branch returns a
9369
+ fragment with <circle> + <text> as siblings.
9370
+ Wrapping them in a single <g> with the filter
9371
+ centralizes the paint axis. Same brightness(1.15)
9372
+ value as R501 for cross-branch consistency. Same
9373
+ transition cadence (filter 200ms ease-out).
9374
+
9375
+ data-node-avatar-monogram-hovered + -fallback-
9376
+ hovered attrs surface the gates for tests. */
9377
+ const isAvatarFallbackHovered = !reducedMotion && hoveredAlias === session.alias;
7627
9378
  if (vendor.id !== 'unknown') {
7628
9379
  // Known model house, logo asset not in public/vendors/
7629
9380
  // yet — vendor-tinted monogram stands in.
@@ -7643,7 +9394,14 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7643
9394
  where less visual weight signals "we don't know
7644
9395
  what this is" appropriately. */
7645
9396
  return (
7646
- <>
9397
+ <g
9398
+ data-node-avatar-monogram={session.alias}
9399
+ data-node-avatar-monogram-hovered={isAvatarFallbackHovered ? 'true' : 'false'}
9400
+ style={{
9401
+ filter: isAvatarFallbackHovered ? 'brightness(1.15)' : undefined,
9402
+ transition: 'filter 200ms ease-out',
9403
+ }}
9404
+ >
7647
9405
  <circle cx={pos.x} cy={pos.y} r={ar} fill={vendor.mono.bg} stroke={vendor.mono.ring} strokeWidth="1.5" />
7648
9406
  {/* Round 284 / Loop: known-vendor monogram letter
7649
9407
  swaps fontFamily monospace → system sans-serif.
@@ -7675,14 +9433,21 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7675
9433
  >
7676
9434
  {vendor.initial}
7677
9435
  </text>
7678
- </>
9436
+ </g>
7679
9437
  );
7680
9438
  }
7681
9439
  // Round 106 (issue #83): hue keyed to the prefix group,
7682
9440
  // not the full alias — every 通信* node shares one color.
7683
9441
  const c = aliasAvatarColors(groupKeys[session.alias] || session.alias);
7684
9442
  return (
7685
- <>
9443
+ <g
9444
+ data-node-avatar-fallback={session.alias}
9445
+ data-node-avatar-fallback-hovered={isAvatarFallbackHovered ? 'true' : 'false'}
9446
+ style={{
9447
+ filter: isAvatarFallbackHovered ? 'brightness(1.15)' : undefined,
9448
+ transition: 'filter 200ms ease-out',
9449
+ }}
9450
+ >
7686
9451
  <circle cx={pos.x} cy={pos.y} r={ar} fill={c.bg} stroke={c.ring} strokeWidth="1" />
7687
9452
  <text
7688
9453
  x={pos.x}
@@ -7696,7 +9461,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7696
9461
  >
7697
9462
  {aliasInitial(session.alias)}
7698
9463
  </text>
7699
- </>
9464
+ </g>
7700
9465
  );
7701
9466
  })()}
7702
9467
  {/* Issue #96: runtime badge — small corner glyph marking the
@@ -7727,7 +9492,73 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7727
9492
  // badge-active exposes the gate for tests.
7728
9493
  const isNodeActive = !reducedMotion && hoveredAlias === session.alias;
7729
9494
  return (
7730
- <g style={{ pointerEvents: 'none' }}>
9495
+ /* Round 559 / Loop — runtime badge outer <g> picks up
9496
+ a drop-shadow glow on node hover, using the runtime's
9497
+ own identity color (rt.color, hex). 16th anchor in
9498
+ the drop-shadow visual-polish family. Pairs with
9499
+ existing R208 ring r-lift + R443 icon sw-lift for
9500
+ a 3-axis runtime-badge hover signature now spanning
9501
+ geometry + stroke + paint glow:
9502
+ R208 ring r 7 → 8 (online)
9503
+ R208 ring sw 1.5 → 2
9504
+ R443 icon sw 2.4 → 2.8
9505
+ R559 outer filter none → drop-shadow(rt.color) ← this round
9506
+ Filter on the OUTER <g> applies uniformly to both
9507
+ the ring <circle> and the inner icon <path> —
9508
+ single paint-axis lift covers both layers in one
9509
+ motion-coherent gesture.
9510
+ Hue: `${rt.color}99` hex+alpha (60%) — rt.color is
9511
+ 6-digit hex (#a78bfa / #38bdf8 / #34d399 / #fbbf24
9512
+ per lib/vendorIdentity.ts), so hex+alpha concat is
9513
+ safe (banked R541 pattern: hex sources use hex+
9514
+ alpha; only hsl/color()/dynamic sources need color-
9515
+ mix).
9516
+ 2px blur reads tight on a small badge (r=7 online
9517
+ / r=5.5 offline). transition list adds 'filter
9518
+ 150ms ease-out' matching the R208 ring r/sw cadence
9519
+ at this surface.
9520
+ Drop-shadow visual-polish family extension (16
9521
+ anchors now): R476/R477/R478/R479/R480/R481 +
9522
+ R500/R532-R536/R537/R538/R540/R543-R546/R550 +
9523
+ R559 (this round).
9524
+ data-runtime-badge-glow attr surfaces the gate
9525
+ for tests. */
9526
+ <g
9527
+ data-runtime-badge-glow={isNodeActive ? 'true' : 'false'}
9528
+ data-runtime-badge-brightness={isNodeActive ? '1.15' : '1'}
9529
+ style={{
9530
+ pointerEvents: 'none',
9531
+ /* R586 — runtime badge outer <g> stacks
9532
+ brightness(1.15) onto the existing R559
9533
+ drop-shadow on node hover. 25th anchor in
9534
+ per-element brightness family, 18th in
9535
+ stacked-filter sub-pattern.
9536
+
9537
+ Runtime badge hover signature now CLOSED
9538
+ at 4 axes (geometry + stroke + paint glow
9539
+ + paint brightness):
9540
+ R208 ring r 7 → 8
9541
+ R208 ring sw 1.5 → 2
9542
+ R443 icon sw 2.4 → 2.8
9543
+ R559 outer filter none → drop-shadow(rt.color)
9544
+ R586 outer filter stack brightness(1.15) ← this round
9545
+
9546
+ The drop-shadow + brightness stack is the
9547
+ banked R564/R570 "halo + glow" pattern —
9548
+ drop-shadow paints the colored halo, brightness
9549
+ lifts the underlying paint (ring stroke +
9550
+ icon path both gain ~15% luminance). Single
9551
+ CSS filter chain on the outer <g> covers
9552
+ both child layers uniformly.
9553
+
9554
+ Same R208/R443/R559 150ms cadence preserved
9555
+ via the existing transition. */
9556
+ filter: isNodeActive
9557
+ ? `drop-shadow(0 0 2px ${rt.color}99) brightness(1.15)`
9558
+ : undefined,
9559
+ transition: 'filter 150ms ease-out',
9560
+ }}
9561
+ >
7731
9562
  <circle
7732
9563
  cx={bx} cy={by} r={br}
7733
9564
  fill={pal.containerBg}
@@ -8044,6 +9875,43 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8044
9875
  R211 fill 300ms + R305 letter-spacing 200ms
8045
9876
  transition list preserved; only the
8046
9877
  conditional gets a middle case. */}
9878
+ {/* Round 500 / Loop — milestone round, opens
9879
+ per-node alias drop-shadow polish. Extends the
9880
+ R476-R481 drop-shadow visual-polish family to a
9881
+ 7th anchor: hovered alias text gains a soft
9882
+ status-coloured text-glow. Pre-R500 hover on
9883
+ a node triggered card-lift (R26 translateY) +
9884
+ card-stroke (R242 tint) + alias letter-spacing
9885
+ (R427 0.3px tier) but the alias TEXT itself had
9886
+ no paint-axis cue beyond fill (R211). R500 adds
9887
+ a drop-shadow on the text glyph itself, so the
9888
+ identity glyph itself lights up under attention
9889
+ — matching the R476 idiom (hub-digit emerald
9890
+ glow on hover) at the per-node identity scope.
9891
+ 2px blur radius at 50% alpha — subtler than the
9892
+ R476 hub-digit (3px at 60%) because the alias
9893
+ text is smaller and more numerous (1 per node)
9894
+ so an aggressive glow would multiply into
9895
+ visual noise. Status-coloured (status.text) so
9896
+ the glow inherits the node's working/idle/
9897
+ offline palette — green/cyan/gray respectively.
9898
+ Drop-shadow visual-polish family — 7 anchors:
9899
+ R476 hub digit hover-gated emerald
9900
+ R477 legend pin-ring pin-gated row.fill
9901
+ R478 recent-row pip fresh-gated cyan
9902
+ R479 group-label text pin-gated cyan
9903
+ R480 hot-lane edge hot-gated amber
9904
+ R481 zoom-state minimap zoom-gated cyan
9905
+ R500 node alias text hover-gated status.text ← this round
9906
+ Filter is paint-only; bbox unchanged; overlap-
9907
+ test invariants hold (R51 selector gated to
9908
+ g[data-node] descendants with strokeWidth
9909
+ sentinels; text element doesn't carry stroke).
9910
+ transition list extends to include 'filter
9911
+ 200ms ease-out' alongside the existing fill
9912
+ 300ms + letter-spacing 200ms tweens.
9913
+ data-node-alias-glow attr surfaces the hover
9914
+ gate for tests. */}
8047
9915
  <text
8048
9916
  x="0" y="1" textAnchor="middle"
8049
9917
  fill={status.text}
@@ -8051,12 +9919,43 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8051
9919
  data-node-alias-text={session.alias}
8052
9920
  data-node-alias-chat-target={chatAlias === session.alias ? 'true' : 'false'}
8053
9921
  data-node-alias-hovered={hoveredAlias === session.alias ? 'true' : 'false'}
9922
+ data-node-alias-glow={!reducedMotion && hoveredAlias === session.alias ? 'true' : 'false'}
8054
9923
  style={{
8055
- transition: 'fill 300ms ease-out, letter-spacing 200ms ease-out',
9924
+ transition: 'fill 300ms ease-out, letter-spacing 200ms ease-out, filter 200ms ease-out',
8056
9925
  letterSpacing:
8057
9926
  chatAlias === session.alias ? '0.5px' :
8058
9927
  hoveredAlias === session.alias ? '0.3px' : '0px',
9928
+ /* Round 564 / Loop — alias text filter stacks
9929
+ brightness(1.15) on top of R500's drop-shadow
9930
+ on hover. Mirrors R542 pressure-seg pattern
9931
+ (brightness + drop-shadow in one stacked
9932
+ filter declaration). Pre-R564 hover added
9933
+ only a drop-shadow halo around the glyph;
9934
+ post-R564 the glyph ALSO brightens, so the
9935
+ identity text reads as both "glowing" AND
9936
+ "lit up" under attention — dual paint axes
9937
+ through one filter chain.
9938
+ CSS filter supports multiple functions
9939
+ applied left-to-right. brightness(1.15)
9940
+ lifts the per-status text color (status.text:
9941
+ green/teal/slate per tier) by 15%; the drop-
9942
+ shadow then paints the outer halo in the
9943
+ status-tier hue. Together: the alias glyph
9944
+ both intensifies its identity color AND
9945
+ radiates outward in that same color.
9946
+ Same +15% brightness as R501 vendor logo
9947
+ avatar (banked per-node hover-brightness
9948
+ pattern). Consistent +15% across all per-
9949
+ node identity surfaces (logo, monogram,
9950
+ fallback avatar from R558, AND now alias
9951
+ text). Cross-element brightness consistency.
9952
+ data-node-alias-brightness attr surfaces
9953
+ the lift for tests. */
9954
+ filter: !reducedMotion && hoveredAlias === session.alias
9955
+ ? `drop-shadow(0 0 2px ${status.text}80) brightness(1.15)`
9956
+ : undefined,
8059
9957
  }}
9958
+ data-node-alias-brightness={!reducedMotion && hoveredAlias === session.alias ? '1.15' : '1'}
8060
9959
  >
8061
9960
  {truncate(session.alias, fullMax)}
8062
9961
  </text>
@@ -8096,6 +9995,31 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8096
9995
  alias > status hierarchy holds at the type
8097
9996
  level. data-node-sub-text-font-weight attr
8098
9997
  exposes the value for tests. */}
9998
+ {/* Round 567 / Loop — node sub-text joins the per-
9999
+ node hover-brightness consistency family. Pre-
10000
+ R567 sub-text had only fill brighten (R211) +
10001
+ ls 0→0.2 (R428); the alias text above lifted
10002
+ via R500 drop-shadow + R564 brightness(1.15)
10003
+ stacked. R567 adds brightness(1.15) to sub-
10004
+ text on the same isNodeActive gate so it
10005
+ chromatically lifts together with the alias.
10006
+ Per-node hover-brightness consistency family
10007
+ — 6 anchors at uniform +15%:
10008
+ R501 vendor.logo image filter on <image>
10009
+ R558 vendor monogram filter on <g>
10010
+ R558 prefix-group fallback filter on <g>
10011
+ R564 alias text (stacked w/ DS) brightness(1.15)
10012
+ R567 node sub-text brightness(1.15) ← this round
10013
+ (+ R559 runtime badge drop-shadow tier-color glow)
10014
+ Now every per-node identity surface (3 avatar
10015
+ variants + alias + sub-text + badge) lifts
10016
+ together on node hover with consistent visual
10017
+ response.
10018
+ Pure paint axis; bbox unchanged. transition
10019
+ list extends to include 'filter 200ms ease-
10020
+ out' matching R428 ls cadence at this scope.
10021
+ data-node-sub-text-brightness attr exposes
10022
+ the lift for tests. */}
8099
10023
  <text
8100
10024
  x="0" y={subY} textAnchor="middle"
8101
10025
  fill={status.primary}
@@ -8104,9 +10028,13 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8104
10028
  data-node-sub-text={session.alias}
8105
10029
  data-node-sub-text-hovered={hoveredAlias === session.alias ? 'true' : 'false'}
8106
10030
  data-node-sub-text-font-weight="500"
10031
+ data-node-sub-text-brightness={!reducedMotion && hoveredAlias === session.alias ? '1.15' : '1'}
8107
10032
  style={{
8108
- transition: 'fill 300ms ease-out, letter-spacing 200ms ease-out',
10033
+ transition: 'fill 300ms ease-out, letter-spacing 200ms ease-out, filter 200ms ease-out',
8109
10034
  letterSpacing: hoveredAlias === session.alias ? '0.2px' : '0px',
10035
+ filter: !reducedMotion && hoveredAlias === session.alias
10036
+ ? 'brightness(1.15)'
10037
+ : undefined,
8110
10038
  }}
8111
10039
  >
8112
10040
  {status.label}{isOnline && sseCountFor != null ? ` sse:${sseCountFor}` : ''}
@@ -8583,7 +10511,55 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8583
10511
  200ms ease-out' alongside R345's ls + R55's fill
8584
10512
  200ms. data-recent-panel-title-fw exposes the
8585
10513
  resolved weight for tests. */}
8586
- <text x="13" y="21" fill={pal.legendHeadline} fontSize="12" fontFamily="monospace" fontWeight={activeEdgeKey ? '800' : '700'} letterSpacing={hoveredPanel === 'recent' ? '0.4' : '0.3'} style={{ transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out, font-weight 200ms ease-out' }} data-recent-panel-title data-recent-panel-title-fw={activeEdgeKey ? '800' : '700'} data-recent-panel-title-active={activeEdgeKey ? 'true' : 'false'}>recent signal</text>
10514
+ {/* Round 550 / Loop drop-shadow visual-polish family
10515
+ extends to a 14th anchor: the recent-panel header
10516
+ title gains a soft pal.legendAccent glow when the
10517
+ panel has an active row (activeEdgeKey). Pre-R550
10518
+ the title's state-flip on active was 2-axis (R482
10519
+ fw 700→800 + R345 ls 0.3→0.4 when panel-hovered);
10520
+ R550 adds the paint axis so the title brightens
10521
+ paint-wise alongside the typographic tightening when
10522
+ a row inside its panel is locked.
10523
+
10524
+ Hue: pal.legendAccent + hex alpha '80' (~50%) — same
10525
+ strength as R479 group-label pin-glow at the parent
10526
+ panel-title scope. 2px blur reads soft; cyan accent
10527
+ ties the title visually to the active row's pin
10528
+ colour (cyber: cyan-300 / light: teal-600). Hex+alpha
10529
+ concat safe — pal.legendAccent is '#67e8f9' (cyber)
10530
+ or '#0d9488' (light), both 6-digit hex (banked R541:
10531
+ hex sources use hex+alpha; only hsl/color()/dynamic
10532
+ sources need color-mix).
10533
+
10534
+ Drop-shadow visual-polish family extension (14
10535
+ anchors now):
10536
+ R476 hub digit hover-gated emerald
10537
+ R477 legend pin-ring pin-gated row.fill
10538
+ R478 recent-row pip freshness cyan
10539
+ R479 group-label text pin-gated cyan
10540
+ R532-R536 hub-cluster glow QUINTET
10541
+ R537 legend swatch hover/pin row.fill
10542
+ R538 group-label hover-precedence
10543
+ R540 edge-badge text pin-gated cyan
10544
+ R543-R546 pin-active pill 4-variant arc
10545
+ R550 recent-panel title pin-gated cyan ← this round
10546
+ R550 legend-panel title pin-gated cyan ← sibling (next text below)
10547
+
10548
+ filter is paint-only; bbox unchanged; overlap-test
10549
+ invariants hold. transition list extends to include
10550
+ 'filter 200ms ease-out' alongside R345 ls + R482 fw
10551
+ + R55 fill 200ms — one motion-coherent 3-axis active-
10552
+ state lift.
10553
+ data-recent-panel-title-glow attr exposes the gate
10554
+ state for tests. */}
10555
+ {/* Round 573 / Loop — recent panel-title joins per-element
10556
+ brightness family at 11th anchor. Stacks brightness(1.15)
10557
+ onto R550's active-gated drop-shadow. Mirrors R572
10558
+ panel-row text pattern at the panel-TITLE tier — both
10559
+ panels now have brightness in their active signature
10560
+ at BOTH chrome tiers (title + row text), completing
10561
+ the panel paint-axis cascade. */}
10562
+ <text x="13" y="21" fill={pal.legendHeadline} fontSize="12" fontFamily="monospace" fontWeight={activeEdgeKey ? '800' : '700'} letterSpacing={hoveredPanel === 'recent' ? '0.4' : '0.3'} style={{ transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out, font-weight 200ms ease-out, filter 200ms ease-out', filter: activeEdgeKey ? `drop-shadow(0 0 2px ${pal.legendAccent}80) brightness(1.15)` : undefined }} data-recent-panel-title data-recent-panel-title-fw={activeEdgeKey ? '800' : '700'} data-recent-panel-title-active={activeEdgeKey ? 'true' : 'false'} data-recent-panel-title-glow={activeEdgeKey ? 'true' : 'false'} data-recent-panel-title-brightness={activeEdgeKey ? '1.15' : '1'}>recent signal</text>
8587
10563
  {/* R96: header count now matches what the rows show. Pre-R96
8588
10564
  this read "X msgs" off the raw messages array, but the
8589
10565
  rows below render DEDUPED flowLinks — so a fleet with 10
@@ -8658,21 +10634,59 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8658
10634
  textAnchor="end"
8659
10635
  fontSize="10"
8660
10636
  fontFamily="monospace"
8661
- // Round 349 / Loop: editorial letter-spacing 0.2 on the
8662
- // recent-signal panel header count. Sits one tier below
8663
- // the R301 panel title letterSpacing="0.3" so the panel
8664
- // header reads as a 2-step hierarchy (title 0.3 / count
8665
- // 0.2). Sibling change on the legend panel count below
8666
- // closes the panel-pair editorial symmetry. Joins the
8667
- // R285 / R289 / R301 / R302 / R304 / R325 editorial-
8668
- // letterspacing tier at the panel-summary scope. The
8669
- // R162 freshness fill, R225 tabular-nums, R311 fw=600,
8670
- // R336 unit-tspan opacity-0.7 split all preserved —
8671
- // the tier propagates to all descendant tspans via
8672
- // SVG inheritance. data-recent-panel-count-letter-
8673
- // spacing exposes the value for tests.
8674
- letterSpacing="0.2"
8675
- data-recent-panel-count-letter-spacing="0.2"
10637
+ /* Round 566 / Loop recent-panel-count gains hover-
10638
+ state letter-spacing tween (0.2 0.4 on hovered-
10639
+ Panel === 'recent'). Pairs with R424 fw 600→700
10640
+ on the same gate. Count now has 2-axis hover
10641
+ signature (fw + ls), matching the panel title's
10642
+ R345 ls + R482 fw lift pattern at the panel-
10643
+ header data-tspan scope. R349 editorial 0.2
10644
+ baseline preserved at rest only hover lifts.
10645
+ Sibling treatment on the legend-panel count
10646
+ below closes the panel-pair symmetry. */
10647
+ letterSpacing={hoveredPanel === 'recent' ? '0.4' : '0.2'}
10648
+ data-recent-panel-count-letter-spacing={hoveredPanel === 'recent' ? '0.4' : '0.2'}
10649
+ data-recent-panel-count-brightness={hoveredPanel === 'recent' ? '1.15' : '1'}
10650
+ style={{
10651
+ /* R589 — recent-signal panel count <text> root gains
10652
+ brightness(1.15) on hoveredPanel === 'recent'.
10653
+ 28th anchor in per-element brightness family.
10654
+ Sibling to R588 at the legend panel; closes
10655
+ panel-pair brightness symmetry at the header-
10656
+ count scope.
10657
+
10658
+ Panel-pair title↔count brightness parity now
10659
+ complete:
10660
+ legend title pinnedStatus gate (R571 family)
10661
+ legend count hoveredPanel gate (R588)
10662
+ recent title activeEdgeKey gate (R571 family)
10663
+ recent count hoveredPanel gate (R589) ← this round
10664
+
10665
+ All 4 panel-header text elements respond on the
10666
+ brightness axis — full symmetric closure.
10667
+
10668
+ Recent-signal panel count hover signature now
10669
+ 3 axes (mirrors R588 legend count closure):
10670
+ R311/R424 fontWeight 600 → 700
10671
+ R349/R566 letter-spacing 0.2 → 0.4
10672
+ R589 brightness 1 → 1.15 ← this round
10673
+
10674
+ Filter applied at <text> root (the parent here);
10675
+ the nested fw-changing tspan inherits the lift
10676
+ via SVG inheritance — unlike applying to tspan
10677
+ directly, which is unreliable cross-browser.
10678
+
10679
+ The R162 freshness-tinted fill (cyan/teal alpha
10680
+ 1.0 → 0.30 by ageSec) gets the +15% multiplied
10681
+ in — fresh data (alpha=1) reads dramatically
10682
+ brighter on hover; stale data (alpha=0.30) gets
10683
+ a proportionally smaller absolute lift. Hover
10684
+ brightness is freshness-amplifying at this
10685
+ surface — coherent with the panel's "freshness
10686
+ is the primary signal" semantic. */
10687
+ filter: hoveredPanel === 'recent' ? 'brightness(1.15)' : undefined,
10688
+ transition: 'letter-spacing 200ms ease-out, filter 200ms ease-out',
10689
+ }}
8676
10690
  >
8677
10691
  {/* Round 225 / Loop: tabular-nums on the panel-header
8678
10692
  flow-count tspan. The "{N} flows" string lives in
@@ -9327,11 +11341,46 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
9327
11341
  tier without disturbing the surrounding family
9328
11342
  baseline. data-recent-row-text-font-weight attr
9329
11343
  exposes the value for tests. */
9330
- fontWeight="500"
11344
+ /* Round 530 / Loop — extends hover-fw family
11345
+ (R416/R420/R425/R520/R521/R522, 6 anchors) to
11346
+ a 7th anchor: recent-row alias text gains
11347
+ fontWeight 500 → 600 on (isRowHovered ||
11348
+ isRowPinned). Pre-R530 R363 set fw=500
11349
+ statically; hover/pin lifted other axes
11350
+ (R55 fill brighten / R434 letter-spacing
11351
+ 3-tier / R143 translateY / R104 row bg-
11352
+ tint / R474 cadence) but the fw stayed
11353
+ flat — same asymmetry R520 closed at the
11354
+ +N more footer.
11355
+ R530 mirrors R520's pattern at the row-
11356
+ text scope. Hover OR pin (isRowActive
11357
+ union) lifts fw to 600, matching the count
11358
+ tspan's cold-state tier (R320 fw=600), so
11359
+ on active state the alias label reads at
11360
+ the same data tier as the count it sits
11361
+ next to. Inner count tspan has its own
11362
+ explicit fontWeight (600 or 700 per R320/
11363
+ R445) so parent fw lift doesn't bleed
11364
+ (inheritance overridden).
11365
+ Hover-fw family extension (7 anchors):
11366
+ R416 chip-row count digit
11367
+ R420 chrome zoom-level
11368
+ R425 hub-center digit
11369
+ R520 +N more flows footer
11370
+ R521 chrome nodeSize S/M/L inactive
11371
+ R522 chrome layout Ring/Grid inactive
11372
+ R530 recent-row alias text ← this round
11373
+ transition list extends to include
11374
+ 'font-weight 200ms ease-out', matching the
11375
+ R474 cadence of the existing fill +
11376
+ letter-spacing axes on this element.
11377
+ data-recent-row-text-font-weight attr
11378
+ flips '500' → '600' on isRowActive. */
11379
+ fontWeight={(isRowHovered || isRowPinned) ? '600' : '500'}
9331
11380
  data-recent-row-text={link.key}
9332
11381
  data-recent-row-text-pinned={isRowPinned ? 'true' : 'false'}
9333
11382
  data-recent-row-text-hovered={!isRowPinned && isRowHovered ? 'true' : 'false'}
9334
- data-recent-row-text-font-weight="500"
11383
+ data-recent-row-text-font-weight={(isRowHovered || isRowPinned) ? '600' : '500'}
9335
11384
  /* Round 434 / Loop: recent-signal row text extends
9336
11385
  from R220's pin-only letter-spacing (0 → 0.5 on
9337
11386
  isRowPinned) to a 3-tier scale matching R433
@@ -9380,11 +11429,49 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
9380
11429
  spacing values unchanged; R363 fw + R55 fill
9381
11430
  brighten unchanged — only the timing axis
9382
11431
  shifts. */
11432
+ /* Round 568 / Loop — extends the drop-shadow visual-
11433
+ polish family (16 anchors after R559) to a 17th
11434
+ anchor at the recent-row text scope. Adds a soft
11435
+ pal.legendAccent glow on isRowHovered || isRow-
11436
+ Pinned, completing the row's hover/pin signature
11437
+ at 4 paint+typography axes:
11438
+ R55 fill brighten (fill)
11439
+ R434 letter-spacing 3-tier (typography kerning)
11440
+ R530 fontWeight 500 → 600 (typography weight)
11441
+ R568 drop-shadow glow (paint glow) ← this round
11442
+ Mirror of R550 panel-title pin-gated glow pattern,
11443
+ applied at the panel-ROW tier rather than panel-
11444
+ TITLE tier. Hue: pal.legendAccent + hex alpha 80
11445
+ (~50%) — same strength as R479 group-label /
11446
+ R550 panel-title glows for cross-element
11447
+ consistency. 2px blur (smaller than R478 pip's
11448
+ 3px since text is fontSize 9 and a heavier blur
11449
+ would bleed into adjacent row text); blur 2px
11450
+ keeps the glow tight to the row's text glyphs.
11451
+ transition list extends to include 'filter
11452
+ 200ms ease-out' matching the R474 200ms cadence
11453
+ of the existing 3 axes.
11454
+ data-recent-row-text-glow attr surfaces the
11455
+ gate for tests. */
9383
11456
  data-recent-row-text-transition="200ms"
11457
+ data-recent-row-text-glow={(isRowHovered || isRowPinned) ? 'true' : 'false'}
11458
+ /* Round 572 / Loop — per-element brightness family
11459
+ 9th anchor at recent-row text scope. Stacks
11460
+ brightness(1.15) onto R568's drop-shadow in
11461
+ one filter chain (same R564/R570/R571 pattern).
11462
+ Glyph BOTH glows (R568 drop-shadow halo) AND
11463
+ brightens (R572 inner lift) simultaneously.
11464
+ Cross-element brightness consistency: same +15%
11465
+ across alias / sub-text / edge-badge / group-
11466
+ label / and now recent-row text. */
11467
+ data-recent-row-text-brightness={(isRowHovered || isRowPinned) ? '1.15' : '1'}
9384
11468
  style={{
9385
- transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out',
11469
+ transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out, font-weight 200ms ease-out, filter 200ms ease-out',
9386
11470
  letterSpacing: isRowPinned ? '0.5px' :
9387
11471
  isRowHovered ? '0.25px' : '0px',
11472
+ filter: (isRowHovered || isRowPinned)
11473
+ ? `drop-shadow(0 0 2px ${pal.legendAccent}80) brightness(1.15)`
11474
+ : undefined,
9388
11475
  }}
9389
11476
  >
9390
11477
  {/* R138 / Loop: typography unification with the rest
@@ -9556,9 +11643,51 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
9556
11643
  data-recent-row-ts={link.key}
9557
11644
  data-recent-row-ts-alpha={tsAlpha.toFixed(2)}
9558
11645
  data-recent-row-ts-lifted={(isRowHovered || isRowPinned) ? 'true' : 'false'}
11646
+ data-recent-row-ts-brightness={(isRowHovered || isRowPinned) ? '1.15' : '1'}
9559
11647
  style={{
9560
11648
  pointerEvents: 'none',
9561
- transition: 'opacity 200ms ease-out',
11649
+ /* R591 recent-row timestamp gains filter
11650
+ brightness(1.15) on row hover/pin. 30th
11651
+ anchor in per-element brightness family.
11652
+ Closes recent-row 3-element brightness
11653
+ coverage:
11654
+ recent-row text (R572, parent <text>
11655
+ filter — count tspan
11656
+ inherits)
11657
+ recent-row ts (R591, sibling <text>
11658
+ — its own filter) ← this round
11659
+
11660
+ Symmetric with the legend-row R590 closure
11661
+ pattern: when a sibling text element can't
11662
+ inherit the row's brightness via ancestor
11663
+ filter, it needs its own filter. R590
11664
+ solved this for legend-count; R591 does
11665
+ the same for recent-ts.
11666
+
11667
+ Triple multiplicative interaction at this
11668
+ surface:
11669
+ opacity: tsAlpha (R191 freshness 1.0 →
11670
+ 0.25 by ageSec) → 1.0 on hover/pin
11671
+ (R484)
11672
+ fill: pal.legendText (neutral gray)
11673
+ brightness: 1.0 → 1.15 on hover/pin
11674
+
11675
+ Stale-data hover semantic: a stale row's
11676
+ timestamp at tsAlpha=0.25 gets opacity 0.25
11677
+ × brightness 1.0 at rest, but jumps to
11678
+ opacity 1.0 × brightness 1.15 on hover —
11679
+ revealing the dim metadata at maximum
11680
+ legibility under attention. Hover brightness
11681
+ is freshness-overriding at this surface,
11682
+ same as R589's freshness-amplifying behavior
11683
+ at the recent-panel count above.
11684
+
11685
+ data-recent-row-ts-brightness attr exposes
11686
+ the gate for tests. */
11687
+ filter: (isRowHovered || isRowPinned)
11688
+ ? 'brightness(1.15)'
11689
+ : undefined,
11690
+ transition: 'opacity 200ms ease-out, filter 200ms ease-out',
9562
11691
  fontVariantNumeric: 'tabular-nums',
9563
11692
  }}
9564
11693
  >
@@ -9732,6 +11861,39 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
9732
11861
  stays as is, so the rest-vs-hover delta still
9733
11862
  reads clearly. data-recent-panel-more-font-weight
9734
11863
  attr exposes the value for tests. */}
11864
+ {/* Round 520 / Loop — extends the `+N more flows` footer
11865
+ to a 5-axis hover signature by adding fontWeight
11866
+ 500 → 600 on hover. Pre-R520 the footer carried 4
11867
+ hover axes:
11868
+ R195 fill legendText → legendAccent
11869
+ R325 letter-spacing 0.2 → 0.3px (R344 tween)
11870
+ R325 opacity 0.55 → 0.85
11871
+ R133 underline none → underline
11872
+ R368 had set fontWeight 500 statically as a sibling
11873
+ to R363/R364/R366 small-text fw lift family — but
11874
+ the footer's hover state didn't carry a fontWeight
11875
+ DELTA the way other interactive surfaces do (chip-
11876
+ row counts R416, chrome zoom-level R420, hub digit
11877
+ R425). R520 adds the missing weight axis: fw 500
11878
+ → 600 on hover, so the footer reads "thickening AND
11879
+ lighting up" under cursor — same idiom as the
11880
+ chrome zoom-level R420 / chip-row digit R416 hover-
11881
+ bold pattern, applied at the panel nav-action
11882
+ surface.
11883
+ data-recent-panel-more-font-weight attr value
11884
+ flips from '500' → '600' on hover (was static
11885
+ '500' pre-R520).
11886
+ Bonus closure — R475 panel-text cadence: pre-R520
11887
+ the footer's transition list had `opacity 150ms`
11888
+ while R475 unified panel-text transitions at
11889
+ 200ms. R518 closed the same gap at legend-count.
11890
+ R520 closes the LAST panel-text 150ms holdout
11891
+ here AND adds the new font-weight 200ms axis. All
11892
+ 4 transition properties (opacity / fill / letter-
11893
+ spacing / font-weight) now uniform 200ms at the
11894
+ footer — same cadence as legend-label / legend-
11895
+ count / recent-row alias / recent-row count /
11896
+ group-label. */}
9735
11897
  <text
9736
11898
  x="115" y="82"
9737
11899
  textAnchor="middle"
@@ -9739,14 +11901,49 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
9739
11901
  fontSize="9"
9740
11902
  fontFamily="monospace"
9741
11903
  fontStyle="italic"
9742
- fontWeight="500"
11904
+ fontWeight={hoveredRecentMore ? '600' : '500'}
9743
11905
  letterSpacing={hoveredRecentMore ? '0.3' : '0.2'}
9744
11906
  opacity={hoveredRecentMore ? 0.85 : 0.55}
9745
11907
  textDecoration={hoveredRecentMore ? 'underline' : 'none'}
9746
11908
  data-recent-panel-more={moreCount}
9747
11909
  data-recent-panel-more-hovered={hoveredRecentMore ? 'true' : 'false'}
9748
- data-recent-panel-more-font-weight="500"
9749
- style={{ transition: 'opacity 150ms ease-out, fill 200ms ease-out, letter-spacing 200ms ease-out' }}
11910
+ data-recent-panel-more-font-weight={hoveredRecentMore ? '600' : '500'}
11911
+ data-recent-panel-more-transition="200ms"
11912
+ data-recent-panel-more-brightness={hoveredRecentMore ? '1.15' : '1'}
11913
+ style={{
11914
+ /* R592 — +N more flows footer gains filter
11915
+ brightness(1.15) on hover. 31st anchor in
11916
+ per-element brightness family. Closes the
11917
+ footer's hover signature at 6 axes — the
11918
+ densest hover signature on any topology
11919
+ surface:
11920
+ R195 fill legendText → legendAccent
11921
+ R325 letter-spacing 0.2 → 0.3 (R344 tween)
11922
+ R325 opacity 0.55 → 0.85
11923
+ R133 underline none → underline
11924
+ R520 fontWeight 500 → 600
11925
+ R592 brightness 1 → 1.15 ← this round
11926
+
11927
+ The footer is the recent-signal panel's
11928
+ primary nav affordance into /messages — when
11929
+ user hovers it, EVERYTHING about it shifts:
11930
+ color (cyan), size (fw), spacing (ls), opacity
11931
+ (0.85), decoration (underline), brightness
11932
+ (+15%). 6-axis hover signature reads as "this
11933
+ is the most actionable thing on the panel —
11934
+ click me".
11935
+
11936
+ Triple-paint multiplicative interaction:
11937
+ opacity 0.85 × cyan fill × brightness(1.15)
11938
+ — the cyan reads dramatically brighter than
11939
+ a plain fill swap would.
11940
+
11941
+ Existing transition list extends with 'filter
11942
+ 200ms ease-out' matching the existing 200ms
11943
+ cadence across all 5 other axes. */
11944
+ filter: hoveredRecentMore ? 'brightness(1.15)' : undefined,
11945
+ transition: 'opacity 200ms ease-out, fill 200ms ease-out, letter-spacing 200ms ease-out, font-weight 200ms ease-out, filter 200ms ease-out',
11946
+ }}
9750
11947
  >
9751
11948
  {`+ ${moreCount}`}
9752
11949
  <tspan opacity="0.7" data-recent-panel-more-unit>{` more flow${moreCount === 1 ? '' : 's'}`}</tspan>
@@ -9853,7 +12050,20 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
9853
12050
  data-legend-panel-title-fw + -active exposed for tests. */}
9854
12051
  {/* R345 sibling — legend panel title same hover letter-
9855
12052
  spacing tween 0.3 → 0.4 on panel hover. */}
9856
- <text x="13" y="21" fill={pal.legendHeadline} fontSize="12" fontFamily="monospace" fontWeight={pinnedStatus ? '800' : '700'} letterSpacing={hoveredPanel === 'legend' ? '0.4' : '0.3'} style={{ transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out, font-weight 200ms ease-out' }} data-legend-panel-title data-legend-panel-title-fw={pinnedStatus ? '800' : '700'} data-legend-panel-title-active={pinnedStatus ? 'true' : 'false'}>legend</text>
12053
+ {/* Round 550 sibling legend-panel header title mirrors
12054
+ the recent-panel title above: drop-shadow glow on
12055
+ pin-gated active state (pinnedStatus). Same hue
12056
+ (pal.legendAccent + hex alpha 80), same 2px blur,
12057
+ same 200ms ease-out cadence. Family lifts to 15
12058
+ anchors with this sibling (counted as R550-sibling
12059
+ for accounting parity with R532-R536 hub-cluster
12060
+ glow quintet pattern — two co-shipping anchors
12061
+ under a single round number).
12062
+ data-legend-panel-title-glow attr added. */}
12063
+ {/* Round 573 sibling — legend panel-title 12th anchor.
12064
+ Same stacked filter pattern at the legend-panel-title
12065
+ scope. Both panel titles now lift in lockstep. */}
12066
+ <text x="13" y="21" fill={pal.legendHeadline} fontSize="12" fontFamily="monospace" fontWeight={pinnedStatus ? '800' : '700'} letterSpacing={hoveredPanel === 'legend' ? '0.4' : '0.3'} style={{ transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out, font-weight 200ms ease-out, filter 200ms ease-out', filter: pinnedStatus ? `drop-shadow(0 0 2px ${pal.legendAccent}80) brightness(1.15)` : undefined }} data-legend-panel-title data-legend-panel-title-fw={pinnedStatus ? '800' : '700'} data-legend-panel-title-active={pinnedStatus ? 'true' : 'false'} data-legend-panel-title-glow={pinnedStatus ? 'true' : 'false'} data-legend-panel-title-brightness={pinnedStatus ? '1.15' : '1'}>legend</text>
9857
12067
  {/* Round 257 / Loop: legend panel header count picks up the
9858
12068
  symmetric 13L/13R inner-padding pattern from the recent-
9859
12069
  signal panel. Pre-R257 the legend header was 13px from
@@ -9930,11 +12140,42 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
9930
12140
  // title 0.3. Pairs with the recent-signal panel count
9931
12141
  // letter-spacing above so the two corner panels' header
9932
12142
  // typography stays editorially symmetric.
9933
- letterSpacing="0.2"
12143
+ /* Round 566 / Loop — legend panel-count gains hover-state
12144
+ letter-spacing tween (0.2 → 0.4 on hoveredPanel ===
12145
+ 'legend'). Pairs with existing R310 fw 600→700 on the
12146
+ same gate — count now has 2-axis hover signature (fw
12147
+ + ls), matching the panel title's R345 ls + R482 fw
12148
+ lift pattern at the panel-header data-tspan scope.
12149
+ Hover-letter-spacing family extension (R566 = 2 sibling
12150
+ anchors at recent + legend panel-count). */
12151
+ letterSpacing={hoveredPanel === 'legend' ? '0.4' : '0.2'}
9934
12152
  data-legend-panel-count
9935
- data-legend-panel-count-letter-spacing="0.2"
12153
+ data-legend-panel-count-letter-spacing={hoveredPanel === 'legend' ? '0.4' : '0.2'}
12154
+ data-legend-panel-count-brightness={hoveredPanel === 'legend' ? '1.15' : '1'}
9936
12155
  style={{
9937
- transition: 'fill 200ms ease-out, font-weight 200ms ease-out',
12156
+ /* R588 legend panel count gains brightness(1.15) on
12157
+ panel hover. 27th anchor in per-element brightness
12158
+ family. Closes title↔count parity at the legend
12159
+ panel header — the title already has brightness
12160
+ (R571/R567 lineage on pinnedStatus gate at line
12161
+ ~11926); the count now matches at the broader
12162
+ hoveredPanel gate.
12163
+
12164
+ Legend panel count hover signature now 3 axes:
12165
+ R310/R424 fontWeight 600 → 700
12166
+ R349/R566 letter-spacing 0.2 → 0.4
12167
+ R588 brightness 1 → 1.15 ← this round
12168
+
12169
+ The cyan numeral (pal.legendAccent) lifts ~15%
12170
+ alongside the typographic tightening — 3-axis
12171
+ panel-count hover signal at full visual presence.
12172
+
12173
+ Pure paint filter on <text> root element (NOT a
12174
+ nested tspan — SVG filter on tspan is unreliable
12175
+ cross-browser; the parent <text> takes the filter
12176
+ and inherits to children). */
12177
+ filter: hoveredPanel === 'legend' ? 'brightness(1.15)' : undefined,
12178
+ transition: 'fill 200ms ease-out, font-weight 200ms ease-out, letter-spacing 200ms ease-out, filter 200ms ease-out',
9938
12179
  fontVariantNumeric: 'tabular-nums',
9939
12180
  }}
9940
12181
  >{sessions.length}<tspan opacity="0.7" data-legend-panel-count-unit> node{sessions.length === 1 ? '' : 's'}</tspan></text>
@@ -10011,6 +12252,34 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10011
12252
  const isPinned = pinnedStatus === row.key;
10012
12253
  const isRowHovered = hoveredStatus === row.key;
10013
12254
  const isLifted = isRowHovered || isPinned;
12255
+ /* Round 562 / Loop — inspection-overrides-encoding family
12256
+ 5th anchor at the legend-swatch scope. When operator
12257
+ hovers a NODE ALIAS on the canvas, the legend swatch
12258
+ that matches that node's status tier lifts r + drop-
12259
+ shadow (R197 + R537 axes) — telegraphing "your
12260
+ inspected node is in this status group".
12261
+ Mirror of R486 minimap-dot inspection-override at the
12262
+ legend-swatch scope.
12263
+ Family progression (5 anchors):
12264
+ R484 recent-row timestamp on alias hover
12265
+ R485 edge particle opacity on alias hover
12266
+ R486 minimap dot opacity to 1.0 on alias hover
12267
+ R561 group-label opacity-1 + ants-gate refinement
12268
+ R562 legend-swatch r+glow on member-alias-matching ← this round
12269
+ Restraint: ONLY swatch lifts, NOT label/fill/ls/fw
12270
+ axes. Direct row-hover gets full treatment; inspection
12271
+ signal gets swatch-only lift — distinct "lighter"
12272
+ visual register matching R561's ants-gate approach
12273
+ (indirect inspection ≠ direct attention). */
12274
+ const hoveredSession = hoveredAlias
12275
+ ? (onlineNodes.find(s => s.alias === hoveredAlias) ?? offlineNodes.find(s => s.alias === hoveredAlias))
12276
+ : null;
12277
+ const hoveredAliasRowKey: 'working' | 'idle' | 'offline' | null = !hoveredSession ? null
12278
+ : hoveredSession.status === 'working' ? 'working'
12279
+ : offlineNodes.includes(hoveredSession) ? 'offline'
12280
+ : 'idle';
12281
+ const isMemberAliasMatching = hoveredAliasRowKey === row.key;
12282
+ const isSwatchLifted = isLifted || isMemberAliasMatching;
10014
12283
  return (
10015
12284
  <g
10016
12285
  key={row.key}
@@ -10142,15 +12411,50 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10142
12411
  R181 pin ring (6 + 0 stroke vs 8 - 0.75 inner
10143
12412
  ≈ 7.25). data-legend-swatch is unchanged so
10144
12413
  R197 / R55 / R61 tests probe the same handle. */}
12414
+ {/* Round 537 / Loop — extends drop-shadow visual-polish
12415
+ family (12 anchors after R536) to a 13th anchor: the
12416
+ legend swatch gains drop-shadow glow on hover/pin
12417
+ using its OWN row fill color (working green / idle
12418
+ teal / offline slate). Pre-R537 the swatch lifted
12419
+ only r (R197/R295 6 → 7) on attention — geometry
12420
+ axis only, no paint glow. R537 adds the paint axis,
12421
+ composing with R181/R402 pin-ring (separate concen-
12422
+ tric circle in the same row.fill color) so on
12423
+ hover/pin the SWATCH AND its pin-ring both contri-
12424
+ bute to a unified tier-coloured glow signature.
12425
+ Hue: row.fill (status hex) concatenated with `99`
12426
+ hex alpha (~60%). Working green / idle teal /
12427
+ offline slate each glow in their OWN tier color
12428
+ — the legend acts as a color-keyed status mirror.
12429
+ 3px blur reads soft; 60% alpha legible without
12430
+ overwhelming the swatch's own paint.
12431
+ Drop-shadow visual-polish family extension (13
12432
+ anchors). filter is paint-only; bbox unchanged.
12433
+ transition list extends to include 'filter 150ms
12434
+ ease-out', matching the existing R197 r 150ms
12435
+ cadence at this swatch. data-legend-swatch-glow
12436
+ attr exposes the gate state for tests. */}
10145
12437
  <circle
10146
12438
  cx="16" cy={row.y0}
10147
12439
  r="6"
10148
12440
  fill={row.fill}
10149
12441
  data-legend-swatch={row.key}
10150
- data-legend-swatch-state={isPinned ? 'pinned' : isRowHovered ? 'hover' : 'idle'}
12442
+ data-legend-swatch-state={isPinned ? 'pinned' : isRowHovered ? 'hover' : isMemberAliasMatching ? 'member-alias-matching' : 'idle'}
12443
+ data-legend-swatch-glow={isSwatchLifted ? 'true' : 'false'}
12444
+ data-legend-swatch-member-alias-matching={isMemberAliasMatching ? 'true' : 'false'}
12445
+ /* Round 578 — legend swatch joins per-element brightness
12446
+ family at 16th anchor. Stacks brightness(1.15) onto
12447
+ R537 drop-shadow. Closes chip-row tier-color glow
12448
+ trio at consistent stacked-filter pattern alongside
12449
+ R542 pressure-seg (already stacks brightness 1.2)
12450
+ and the sibling vendor chip (R578-sibling). */
12451
+ data-legend-swatch-brightness={isSwatchLifted ? '1.15' : '1'}
10151
12452
  style={{
10152
- r: isRowHovered || isPinned ? '7px' : '6px',
10153
- transition: 'r 150ms ease-out',
12453
+ r: isSwatchLifted ? '7px' : '6px',
12454
+ filter: isSwatchLifted
12455
+ ? `drop-shadow(0 0 3px ${row.fill}99) brightness(1.15)`
12456
+ : undefined,
12457
+ transition: 'r 150ms ease-out, filter 150ms ease-out',
10154
12458
  } as React.CSSProperties}
10155
12459
  />
10156
12460
  {/* R61 pinned-state ring — concentric stroke at r=8 in
@@ -10282,11 +12586,42 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10282
12586
  the value for tests. R219 letter-spacing pin
10283
12587
  tween + R55 fill transition + R181 always-mount
10284
12588
  pin ring all preserved. */
10285
- fontWeight="500"
12589
+ /* Round 531 / Loop — extends hover-fw family (R416/
12590
+ R420/R425/R520/R521/R522/R530, 7 anchors) to an
12591
+ 8th anchor at the legend-row label. Pre-R531
12592
+ R364 set fw=500 statically; hover/pin lifted
12593
+ other axes (R55 fill brighten / R433 letter-
12594
+ spacing 3-tier / R181 pin ring) but the fw
12595
+ stayed flat. R531 mirrors R530's recent-row
12596
+ alias pattern at the legend-row label scope.
12597
+ Hover OR pin (hoveredStatus===row.key ||
12598
+ isPinned) lifts fw to 600, matching the
12599
+ legend-row count tier (R309 fw=600 / R446
12600
+ pin lift 600→700). Active label now reads at
12601
+ the count's data tier — sibling treatment to
12602
+ R530 recent-row.
12603
+ Hover-fw family extension (8 anchors):
12604
+ R416 chip-row count digit
12605
+ R420 chrome zoom-level
12606
+ R425 hub-center digit
12607
+ R520 +N more flows footer
12608
+ R521 chrome nodeSize S/M/L inactive
12609
+ R522 chrome layout Ring/Grid inactive
12610
+ R530 recent-row alias text
12611
+ R531 legend-row label ← this round
12612
+ Two panel-row label surfaces (R530 recent-
12613
+ row alias + R531 legend-row label) now have
12614
+ parallel hover-fw signatures. R475 cadence
12615
+ at 200ms already covers font-weight via the
12616
+ existing transition list extension at this
12617
+ element. data-legend-row-label-font-weight
12618
+ attr flips '500' → '600' on isActive (was
12619
+ static '500' pre-R531). */
12620
+ fontWeight={(hoveredStatus === row.key || isPinned) ? '600' : '500'}
10286
12621
  data-legend-row-label={row.key}
10287
12622
  data-legend-row-label-pinned={isPinned ? 'true' : 'false'}
10288
12623
  data-legend-row-label-hovered={!isPinned && hoveredStatus === row.key ? 'true' : 'false'}
10289
- data-legend-row-label-font-weight="500"
12624
+ data-legend-row-label-font-weight={(hoveredStatus === row.key || isPinned) ? '600' : '500'}
10290
12625
  /* Round 433 / Loop: legend-row text extends from
10291
12626
  R219's pin-only letter-spacing (0px → 0.5px on
10292
12627
  isPinned) to a 3-tier scale matching the R432
@@ -10328,10 +12663,46 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10328
12663
  unchanged; R55 fill brighten unchanged — only
10329
12664
  the timing axis shifts. */
10330
12665
  data-legend-row-label-transition="200ms"
12666
+ /* Round 569 / Loop — extends R568's panel-row drop-
12667
+ shadow pattern to the SIBLING legend-row label.
12668
+ Symmetric closure of the panel-row drop-shadow
12669
+ across both panel surfaces (recent + legend).
12670
+ Pre-R569 the legend-row label had 3 hover/pin
12671
+ axes (R55 fill + R433 ls 3-tier + R531 fw); R569
12672
+ adds the 4th paint axis to match R568 recent-
12673
+ row text exactly.
12674
+ Two-tier paint-axis cascade now SYMMETRIC across
12675
+ both side panels:
12676
+ recent panel title (R550) + row text (R568)
12677
+ legend panel title (R550-sibling) + label (R569) ← this round
12678
+ Each panel has glow at BOTH chrome tiers (title
12679
+ active + row hover/pin). The 4-axis row signature
12680
+ (fill + ls + fw + glow) is now identical at both
12681
+ panel-row text scopes — completes the panel-row
12682
+ text-treatment parity.
12683
+ Hue/blur/cadence: same as R568 (pal.legendAccent
12684
+ + hex alpha 80, 2px blur, 200ms ease-out). Gate
12685
+ matches R531/R433 (hoveredStatus === row.key ||
12686
+ isPinned) — single boolean drives all 4 axes
12687
+ together for motion-coherent state-flip.
12688
+ Drop-shadow family extends to 18 anchors total
12689
+ (R568 was 17; R569 = 18).
12690
+ data-legend-row-label-glow attr added for tests. */
12691
+ data-legend-row-label-glow={(hoveredStatus === row.key || isPinned) ? 'true' : 'false'}
12692
+ /* Round 572 / Loop — sibling to recent-row text above.
12693
+ Stacks brightness(1.15) onto R569's drop-shadow at
12694
+ legend-row label scope (10th anchor in per-element
12695
+ brightness family). Matches recent-row text 4-axis
12696
+ signature exactly: fill + ls + fw + drop-shadow +
12697
+ brightness now BOTH panel-row text surfaces. */
12698
+ data-legend-row-label-brightness={(hoveredStatus === row.key || isPinned) ? '1.15' : '1'}
10331
12699
  style={{
10332
- transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out',
12700
+ transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out, font-weight 200ms ease-out, filter 200ms ease-out',
10333
12701
  letterSpacing: isPinned ? '0.5px' :
10334
12702
  hoveredStatus === row.key ? '0.25px' : '0px',
12703
+ filter: (hoveredStatus === row.key || isPinned)
12704
+ ? `drop-shadow(0 0 2px ${pal.legendAccent}80) brightness(1.15)`
12705
+ : undefined,
10335
12706
  }}
10336
12707
  >{row.label}</text>
10337
12708
  {/* R95: live count anchored to the right edge of the
@@ -10463,7 +12834,74 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10463
12834
  data-legend-count-pinned={isPinned ? 'true' : 'false'}
10464
12835
  data-legend-count-font-weight={isPinned ? '700' : '600'}
10465
12836
  data-legend-count-fill={row.count > 0 && (hoveredStatus === row.key || isPinned) ? 'tier' : 'neutral'}
10466
- style={{ pointerEvents: 'none', transition: 'opacity 150ms ease-out, fill 150ms ease-out, font-weight 150ms ease-out', fontVariantNumeric: 'tabular-nums' }}
12837
+ /* Round 518 / Loop extends R433's 3-tier hover-
12838
+ letter-spacing tween from the legend-row LABEL
12839
+ (text at x=30) to the SIBLING legend-row COUNT
12840
+ digit (this text at x=215). Pre-R518 the row's
12841
+ label spread on hover/pin (R433: 0/0.25/0.5px)
12842
+ while the count digit at the row's right edge
12843
+ stayed dead-typographic — same row, two halves,
12844
+ asymmetric kerning gesture. R518 mirrors the
12845
+ 3-tier scale at the count so the WHOLE row's
12846
+ typography reads as one unit under cursor: label
12847
+ + count spread together at matching values.
12848
+ Tabular-nums (R225) makes the kerning still
12849
+ visible on 2-digit counts — each digit cell
12850
+ keeps its fixed width, but the inter-digit
12851
+ advance grows. R518 also closes R475's panel-
12852
+ row TEXT cadence at the count surface — R475
12853
+ lifted the label text transitions to 200ms but
12854
+ the count was missed; R518 lifts opacity / fill
12855
+ / font-weight from 150 → 200ms AND adds the new
12856
+ letter-spacing axis at 200ms. One transition
12857
+ list, one cadence, one motion-coherent multi-
12858
+ axis hover/pin signature across the row.
12859
+ Hover-letter-spacing family extension (10
12860
+ anchors now): R344/R345/R347/R420/R427/R431/
12861
+ R432/R433/R517/R518. R518 closes the legend-
12862
+ row pair (label R433 + count R518). data-
12863
+ legend-count-letter-spacing attr exposes the
12864
+ resolved value for tests. */
12865
+ data-legend-count-letter-spacing={isPinned ? '0.5px' : hoveredStatus === row.key ? '0.25px' : '0px'}
12866
+ data-legend-count-transition="200ms"
12867
+ /* R590 — legend-row count gains filter brightness(1.15)
12868
+ on hover/pin. 29th anchor in per-element brightness
12869
+ family. Closes legend-row label↔count brightness
12870
+ parity within each row:
12871
+ legend label (R572, 10th anchor — stacks w/ DS)
12872
+ legend count (R590 ← this round, plain brightness)
12873
+ Plain brightness (no DS stack) because the count
12874
+ has its own multi-axis hover signature already
12875
+ (opacity 0.65 → 1.0, fill neutral → row.fill tier
12876
+ color, fw 600 → 700 on pin, letter-spacing 3-tier)
12877
+ — adding brightness amplifies the tier-color fill
12878
+ to its hover-locked brightest possible.
12879
+ Triple multiplicative interaction: opacity 1.0 ×
12880
+ tier-color fill × brightness(1.15) — the count
12881
+ reads as confidently "this is the active tier".
12882
+ Existing transition list extends with 'filter
12883
+ 200ms ease-out' matching the existing 200ms
12884
+ cadence across opacity / fill / fw / letter-
12885
+ spacing.
12886
+ Legend-row scope now has parity with recent-row:
12887
+ both row text and count surfaces brighten +15%
12888
+ on row inspection. Recent-row count INHERITS its
12889
+ brightness via the parent <text>'s R572 filter
12890
+ (single ancestor filter covers all child tspans);
12891
+ legend-row count is a sibling <text> so it needs
12892
+ its OWN filter — R590 supplies that.
12893
+ data-legend-count-brightness attr exposes gate. */
12894
+ data-legend-count-brightness={(hoveredStatus === row.key || isPinned) ? '1.15' : '1'}
12895
+ style={{
12896
+ pointerEvents: 'none',
12897
+ filter: (hoveredStatus === row.key || isPinned)
12898
+ ? 'brightness(1.15)'
12899
+ : undefined,
12900
+ transition: 'opacity 200ms ease-out, fill 200ms ease-out, font-weight 200ms ease-out, letter-spacing 200ms ease-out, filter 200ms ease-out',
12901
+ fontVariantNumeric: 'tabular-nums',
12902
+ letterSpacing: isPinned ? '0.5px' :
12903
+ hoveredStatus === row.key ? '0.25px' : '0px',
12904
+ }}
10467
12905
  >{row.count}</text>
10468
12906
  </g>
10469
12907
  );
@@ -10522,6 +12960,74 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10522
12960
  spacing as typographic intent. Stays well inside the
10523
12961
  bottom-left corner; opacity 0.4 unchanged so the
10524
12962
  watermark stays a watermark. */}
12963
+ {/* Round 519 / Loop — 呼吸感 family 3rd anchor. Pre-R519 the
12964
+ breath family had 2 anchors (R497 hub idle digit + R498
12965
+ recent-row hot pulse). Both signal active state — the
12966
+ digit when canvas is idle (no work pending), the recent
12967
+ row when fresh signal arrives. R519 adds a SLOW ambient
12968
+ breath to the brand watermark — present always, not gated
12969
+ on activity state. The watermark IS the canvas-corner
12970
+ register that says "the canvas is alive even when nothing
12971
+ is happening"; a 6s opacity pulse around its 0.4 mean
12972
+ (±0.08 swing → 0.32 ↔ 0.48) reads as ambient liveness
12973
+ rather than foreground signal.
12974
+ Why 6s (not R497's 4s): the breath family now spans
12975
+ activity registers (R497 4s — idle-focal: present and
12976
+ waiting; R498 ~3s — hot signal: just arrived) and now
12977
+ ambient register (R519 6s — corner watermark: always-on
12978
+ background). Slower cadence keeps the watermark in the
12979
+ background; ~10 pct slower than R497 keeps it out of
12980
+ phase so the two anchors never beat together visibly.
12981
+ Gate: !reducedMotion. Inside the prefers-reduced-motion
12982
+ media query, SMIL animate isn't covered by globals.css
12983
+ R29 (which only kills CSS animation property), so we
12984
+ gate at JSX level — when reducedMotion is true the
12985
+ <animate> child isn't mounted and opacity stays at the
12986
+ static 0.4. data-topo-brand-watermark-breath attr
12987
+ exposes the gate state for tests.
12988
+ 呼吸感 family extension (3 anchors): R497 hub idle / R498
12989
+ recent-row hot / R519 brand watermark ambient. */}
12990
+ {/* Round 525 / Loop — focal-recede family 3rd anchor. R507
12991
+ receded the hub-center workingCount digit; R508 receded
12992
+ the hub-highlight disc; both fade to 0.85× when any non-
12993
+ hub canvas surface is hovered (alias / edge / group /
12994
+ status / vendor) — the "you're inspecting elsewhere"
12995
+ gesture. R525 extends the pattern to the brand watermark
12996
+ at canvas bottom-left, the always-on decorative brand
12997
+ element. Pre-R525 the watermark stayed at its R519
12998
+ breath baseline (0.32-0.48 SMIL pulse) regardless of
12999
+ canvas attention; post-R525 it fades to 70% wrapper
13000
+ opacity (effective 0.224-0.336 with breath) when canvas
13001
+ attention is elsewhere, matching the same focal-recede
13002
+ semantic R507/R508 establish at the hub focal cluster.
13003
+ Implementation: wrap the existing <text> in a <g>
13004
+ wrapper whose opacity multiplies with the inner text's
13005
+ SMIL-animated opacity. SVG opacity composes
13006
+ multiplicatively across the parent/child chain, so:
13007
+ normal: g.opacity=1.0 × text.opacity(SMIL 0.32-0.48) = 0.32-0.48
13008
+ recede: g.opacity=0.7 × text.opacity(SMIL 0.32-0.48) = 0.224-0.336
13009
+ SMIL on inner text continues running through both
13010
+ states; only the wrapper opacity flips. 300ms ease-out
13011
+ transition on wrapper (matches R508 hub-highlight recede
13012
+ transition).
13013
+ Gate matches R507/R508 — focal-recede is a UNIFIED
13014
+ non-hub-canvas-hover signal driving multiple anchors,
13015
+ so all three (hub digit / hub-highlight / brand
13016
+ watermark) fade together as the canvas's decorative
13017
+ register, leaving only the surface under inspection
13018
+ foregrounded.
13019
+ Focal-recede family extension (3 anchors): R507 hub
13020
+ digit / R508 hub-highlight / R525 brand watermark. */}
13021
+ <g
13022
+ opacity={(hoveredAlias || hoveredEdgeKey || hoveredGroupLabel ||
13023
+ hoveredStatus || hoveredVendor) && !hoveredHub ? 0.7 : 1}
13024
+ data-topo-brand-watermark-wrapper
13025
+ data-topo-brand-watermark-recede={
13026
+ (hoveredAlias || hoveredEdgeKey || hoveredGroupLabel ||
13027
+ hoveredStatus || hoveredVendor) && !hoveredHub ? 'true' : 'false'
13028
+ }
13029
+ style={{ pointerEvents: 'none', transition: 'opacity 300ms ease-out' }}
13030
+ >
10525
13031
  <text
10526
13032
  x="16" y="672"
10527
13033
  fontSize="11" fontFamily="monospace" fontWeight="600"
@@ -10529,8 +13035,12 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10529
13035
  fill={pal.legendText}
10530
13036
  opacity="0.4"
10531
13037
  data-topo-brand-watermark
13038
+ data-topo-brand-watermark-breath={reducedMotion ? 'false' : 'true'}
10532
13039
  style={{ pointerEvents: 'none', transition: 'fill 200ms ease-out' }}
10533
- >sleep2agi</text>
13040
+ >sleep2agi{!reducedMotion && (
13041
+ <animate attributeName="opacity" values="0.32;0.48;0.32" dur="6s" repeatCount="indefinite" />
13042
+ )}</text>
13043
+ </g>
10534
13044
  {/* v0.10.0 Hero 3 Wave 1 / RFC §3.I (Vincent 5215 + 通信龙
10535
13045
  lead-autonomy Q4 dual-anchor minimal): canvas top-left
10536
13046
  crescent moon brand mark, visible ONLY when the
@@ -10565,10 +13075,47 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10565
13075
  the R175 panel-fade-in uses for cascade rhythm. data-
10566
13076
  topo-brand-canvas-mark-visible exposes the gate for
10567
13077
  tests. */}
13078
+ {/* Round 526 / Loop — focal-recede family 4th anchor.
13079
+ Symmetric polish to R525 (watermark recede). The
13080
+ brand crescent at canvas top-left is the second
13081
+ decorative brand element on the canvas; pre-R526 it
13082
+ stayed at flat opacity 0.35 (when visible) regardless
13083
+ of canvas attention. R526 multiplies its visible
13084
+ opacity by 0.7 when ANY non-hub canvas surface is
13085
+ hovered, matching R525's deeper-recede semantic for
13086
+ decorative brand elements (vs hub focal cluster's
13087
+ 0.85× recede at R507/R508).
13088
+ Composes cleanly with existing flowLinks gate:
13089
+ normal, flowLinks=0: opacity = 0.35 * 1.0 = 0.350
13090
+ recede, flowLinks=0: opacity = 0.35 * 0.7 = 0.245
13091
+ invisible, flowLinks>0: opacity = 0.00 * any = 0.000
13092
+ Multiplicative chain means recede only matters when
13093
+ crescent is visible (quiet canvas, flowLinks=0) —
13094
+ exactly when canvas attention elsewhere should
13095
+ dim the decorative register. 300ms transition
13096
+ already covers both axes (the existing visibility
13097
+ opacity ramp + the new recede multiplier easing).
13098
+ Focal-recede family extension (4 anchors): R507 hub
13099
+ digit / R508 hub-highlight / R525 watermark / R526
13100
+ crescent (this round). Canvas brand surfaces (R525
13101
+ watermark + R526 crescent) now BOTH carry focal-
13102
+ recede at the same 0.7 multiplier, fading as a
13103
+ decorative pair when the canvas's focal attention
13104
+ shifts elsewhere.
13105
+ data-topo-brand-canvas-mark-recede attr exposes the
13106
+ gate state for tests. */}
10568
13107
  <g
10569
- opacity={flowLinks.length === 0 ? 0.35 : 0}
13108
+ opacity={(flowLinks.length === 0 ? 0.35 : 0) * (
13109
+ (hoveredAlias || hoveredEdgeKey || hoveredGroupLabel ||
13110
+ hoveredStatus || hoveredVendor) && !hoveredHub ? 0.7 : 1
13111
+ )}
10570
13112
  data-topo-brand-canvas-mark
10571
13113
  data-topo-brand-canvas-mark-visible={flowLinks.length === 0 ? 'true' : 'false'}
13114
+ data-topo-brand-canvas-mark-recede={
13115
+ (hoveredAlias || hoveredEdgeKey || hoveredGroupLabel ||
13116
+ hoveredStatus || hoveredVendor) && !hoveredHub ? 'true' : 'false'
13117
+ }
13118
+ data-topo-brand-canvas-mark-breath={reducedMotion ? 'false' : 'true'}
10572
13119
  style={{ pointerEvents: 'none', transition: 'opacity 300ms ease-out, fill 200ms ease-out' }}
10573
13120
  >
10574
13121
  <defs>
@@ -10578,11 +13125,44 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10578
13125
  <circle cx="17.5" cy="13" r="10" fill="black" />
10579
13126
  </mask>
10580
13127
  </defs>
13128
+ {/* Round 528 / Loop — 呼吸感 family 4th anchor. Symmetric
13129
+ polish to R519 watermark ambient breath. The brand
13130
+ crescent at canvas top-left is the second decorative
13131
+ canvas brand surface; pre-R528 it stayed at the static
13132
+ composed opacity (wrapper 0.35 × no inner anim = flat).
13133
+ Post-R528 the inner <rect>'s fill-opacity breathes
13134
+ 0.8 ↔ 1.0 on a 7s cycle, composing multiplicatively
13135
+ with the wrapper's recede gate:
13136
+ normal visible: 0.35 × (0.8-1.0) = 0.280-0.350
13137
+ recede visible: 0.245 × (0.8-1.0) = 0.196-0.245
13138
+ invisible: 0 × any = 0
13139
+ 7s cadence intentionally OUT OF PHASE with R519
13140
+ watermark's 6s — the two ambient anchors never beat
13141
+ together visibly when both visible. R497 hub idle
13142
+ breath (4s) is the loudest; R498 recent-row hot pulse
13143
+ (~3s) is the most-active; R519 watermark (6s) +
13144
+ R528 crescent (7s) are the quietest ambient pair.
13145
+ 呼吸感 family extension (4 anchors):
13146
+ R497 hub idle digit 4s active-idle register
13147
+ R498 recent-row hot pulse 3s active-fresh register
13148
+ R519 watermark ambient 6s ambient (always-on)
13149
+ R528 crescent ambient 7s ambient (quiet-only) ← this round
13150
+ SMIL <animate> on fill-opacity (not parent opacity) so
13151
+ the wrapper's React-controlled gate compositions stay
13152
+ intact. Gated on !reducedMotion at JSX level —
13153
+ reducedMotion users see the inner rect at default
13154
+ fill-opacity=1.0 (no SMIL mounted, wrapper's static
13155
+ composed opacity wins). data-topo-brand-canvas-mark-
13156
+ breath attr exposes the gate state. */}
10581
13157
  <rect
10582
13158
  x="16" y="16" width="28" height="28"
10583
13159
  fill={pal.legendText}
10584
13160
  mask="url(#s2a-canvas-corner-mask)"
10585
- />
13161
+ >
13162
+ {!reducedMotion && (
13163
+ <animate attributeName="fill-opacity" values="0.8;1;0.8" dur="7s" repeatCount="indefinite" />
13164
+ )}
13165
+ </rect>
10586
13166
  </g>
10587
13167
  </svg>
10588
13168
 
@@ -10934,12 +13514,32 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10934
13514
  (the minimap viewport is small, ~120×82 px).
10935
13515
  Filter is paint-only — bbox unchanged. transition
10936
13516
  list extends to include 'filter 200ms ease-out'
10937
- so the glow eases when zoom crosses 1.5x. */
10938
- data-topo-minimap-viewport-glow={view.zoom > 1.5 ? 'true' : 'false'}
13517
+ so the glow eases when zoom crosses 1.5x.
13518
+ R540: extends the drop-shadow to also fire on
13519
+ hoveredMinimap with HOVER PRECEDENCE over zoom-
13520
+ state. Pre-R540 the viewport drop-shadow was
13521
+ zoom-only (single gate); R540 adds an
13522
+ interactional gate at lighter blur intensity.
13523
+ Hover wins when both true — interactional signal
13524
+ (user is inspecting) trumps informational signal
13525
+ (you're zoomed). Sibling to R534 edge-badge
13526
+ hover-precedence + R538 group-label hover-tier
13527
+ extensions.
13528
+ 2-tier alpha ladder:
13529
+ hover (interactional) legendAccent 99 (~60%)
13530
+ zoom > 1.5 (info) legendAccent 80 (~50%)
13531
+ rest none
13532
+ data-topo-minimap-viewport-glow attr upgraded
13533
+ binary ('true'/'false') → 3-value ('hover' |
13534
+ 'zoom' | 'false') so tests can distinguish
13535
+ gate cause. */
13536
+ data-topo-minimap-viewport-glow={hoveredMinimap ? 'hover' : view.zoom > 1.5 ? 'zoom' : 'false'}
10939
13537
  style={{
10940
- filter: view.zoom > 1.5
10941
- ? `drop-shadow(0 0 2px ${pal.legendAccent}80)`
10942
- : undefined,
13538
+ filter: hoveredMinimap
13539
+ ? `drop-shadow(0 0 2px ${pal.legendAccent}99)`
13540
+ : view.zoom > 1.5
13541
+ ? `drop-shadow(0 0 2px ${pal.legendAccent}80)`
13542
+ : undefined,
10943
13543
  transition: smoothView
10944
13544
  ? '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, filter 200ms ease-out'
10945
13545
  : 'stroke-width 200ms ease-out, opacity 200ms ease-out, filter 200ms ease-out',
@@ -11080,7 +13680,41 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
11080
13680
  // transition-colors only — without the transform transition,
11081
13681
  // active:scale-95 would hard-cut. transform-gpu promotes the
11082
13682
  // layer so scale doesn't trigger paint thrash.
11083
- className={`px-2 py-1 transition-colors transition-transform duration-200 ease-out transform-gpu active:scale-95 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset ${idx > 0 ? 'border-l' : ''} ${nodeScale === v ? 'bg-cyan-500/15 text-cyan-300 font-medium hover:bg-cyan-500/20 active:bg-cyan-500/25' : 'hover:bg-cyan-500/5 active:bg-cyan-500/15'}${chromePopping === popKey ? ' anet-chrome-pop' : ''}`}
13683
+ /* Round 521 / Loop extends R270's hover-preview pattern
13684
+ (inactive toggle hover previews the active state's
13685
+ visual register) to the TYPOGRAPHY axis at the chrome
13686
+ nodeSize S/M/L surface. Pre-R521 the inactive variant
13687
+ had `hover:bg-cyan-500/5` (R270 bg preview) but no
13688
+ typography preview — active variant uses `font-medium`
13689
+ (fw 500), inactive variant sat at default fw 400 even
13690
+ on hover.
13691
+ R521 adds `hover:font-medium` + `transition-[font-
13692
+ weight]` to the inactive variant so hovering an
13693
+ inactive S/M/L letter thickens the glyph 400 → 500,
13694
+ previewing the typography of the active state the
13695
+ click would commit to. Sibling to R421 chrome zoom-
13696
+ level fontWeight hover delta (rest 500 → hover 600)
13697
+ and R520 footer fontWeight hover (500 → 600) — same
13698
+ idiom: thicken-on-hover for chrome surfaces with a
13699
+ pre-commit gesture.
13700
+ `font-medium` (500) matches the ACTIVE variant's
13701
+ fw exactly — the inactive hover landing weight equals
13702
+ the active locked weight, so clicking commits to a
13703
+ typography state the eye already saw 'on the way in'.
13704
+ Hover-fw family extension (5 anchors now):
13705
+ R416 chip-row count digit rest 500 → hover 700/600
13706
+ R420 chrome zoom-level rest 500 → hover 600
13707
+ R425 hub-center digit rest 700 → hover 800
13708
+ R520 +N more flows footer rest 500 → hover 600
13709
+ R521 chrome nodeSize S/M/L inactive rest 400 → hover 500 ← this round
13710
+ Active variant `font-medium` unchanged so the rest-vs-
13711
+ active typography distinction stays intact when the
13712
+ user IS clicked-in (active stays at fw 500, inactive
13713
+ rest at fw 400, inactive hover preview at fw 500).
13714
+ data-topo-chrome-nodesize-hover-preview-fw="500" attr
13715
+ exposes the polish for tests. */
13716
+ className={`px-2 py-1 transition-colors transition-transform transition-[font-weight] duration-200 ease-out transform-gpu active:scale-95 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset ${idx > 0 ? 'border-l' : ''} ${nodeScale === v ? 'bg-cyan-500/15 text-cyan-300 font-medium hover:bg-cyan-500/20 hover:text-cyan-200 active:bg-cyan-500/25' : 'hover:bg-cyan-500/5 active:bg-cyan-500/15 hover:font-medium'}${chromePopping === popKey ? ' anet-chrome-pop' : ''}`}
13717
+ data-topo-chrome-nodesize-hover-preview-fw={nodeScale === v ? null : '500'}
11084
13718
  style={{ color: nodeScale === v ? undefined : pal.legendText, borderColor: pal.containerBorder }}
11085
13719
  >
11086
13720
  {lbl}
@@ -11118,6 +13752,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
11118
13752
  onClick={() => { popChrome('zoom-out'); zoomByDiscrete(1 / 1.2); }}
11119
13753
  data-topo-chrome-zoom-out
11120
13754
  data-topo-chrome-zoom-out-popping={chromePopping === 'zoom-out' ? 'true' : 'false'}
13755
+ data-topo-chrome-zoom-out-brightness-hover="1.15"
11121
13756
  // R196: press-state deepens bg one tier above hover (white/5
11122
13757
  // → white/10) so mouse-down has a tactile dim before the
11123
13758
  // R186 icon pop fires on release.
@@ -11126,7 +13761,26 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
11126
13761
  // press-feedback family (R492 + nodeSize above). transition-
11127
13762
  // transform + duration-200 + ease-out + transform-gpu added
11128
13763
  // since the className had only transition-colors.
11129
- className="group px-2 py-1 hover:bg-white/5 active:bg-white/10 transition-colors transition-transform duration-200 ease-out transform-gpu active:scale-95 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset"
13764
+ /* R596 zoom-out segmented button gains hover:brightness-[1.15]
13765
+ (35th anchor in per-element brightness family, 4th HTML).
13766
+ Sibling to zoom-in below — paired-anchor round closing the
13767
+ entire zoom trio at brightness parity (R593 zoom-level
13768
+ readout + R596 zoom-out + zoom-in).
13769
+
13770
+ Segmented-control constraint preserved: brightness is pure
13771
+ paint (no geometry shift) so it doesn't break the R400
13772
+ segmented-unity rule (which only excludes geometric hover-
13773
+ lift like translateY). Each segment can brighten
13774
+ independently while the strip stays planted as one unit.
13775
+
13776
+ Tailwind v4 arbitrary `[transition-property:...]` replaces
13777
+ `transition-colors transition-transform` so the filter
13778
+ property joins the existing 200ms cadence — bg / color /
13779
+ transform / filter all ease in unison.
13780
+
13781
+ data-topo-chrome-zoom-out-brightness-hover='1.15' attr
13782
+ documents the hover value for tests. */
13783
+ className="group px-2 py-1 hover:bg-white/5 hover:brightness-[1.15] active:bg-white/10 [transition-property:color,background-color,transform,filter] duration-200 ease-out transform-gpu active:scale-95 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset"
11130
13784
  style={{ color: pal.legendText }}
11131
13785
  aria-label="Zoom out"
11132
13786
  title="Zoom out (−)"
@@ -11218,13 +13872,66 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
11218
13872
  ? 'true' : 'false'
11219
13873
  }
11220
13874
  data-topo-chrome-zoom-level-hover={hoveredZoomLevel ? 'true' : 'false'}
13875
+ /* Round 517 / Loop — extends the chrome zoom-level readout
13876
+ from 2-axis (R347 letter-spacing + R420 fontWeight) to
13877
+ 3-axis hover signature by adding a color brighten to
13878
+ pal.legendHeadline. Pre-R517 the readout's color stayed
13879
+ at pal.legendText on hover; the digits got tighter
13880
+ kerning (0→0.5px) and heavier weight (500→600) but
13881
+ stayed the same legendText gray tone. R517 lifts color
13882
+ to legendHeadline on hover so the readout brightens
13883
+ into the headline tier at the same beat — matching the
13884
+ R55/R197/R239 hover-deepen-own-hue idiom that legend-
13885
+ row label + count carry at panel scope. Chrome strip's
13886
+ only data display now has full 3-axis hover signature
13887
+ (letter-spacing + fontWeight + color), parity with the
13888
+ chip-row chips' own hover-brighten pattern.
13889
+ Implementation: inline color uses the same hoveredZoom-
13890
+ Level state as R347/R420 — no new state. Transition
13891
+ already includes 'color 200ms ease-out' (R264) so the
13892
+ brighten eases under the same cadence as the kerning +
13893
+ weight tweens — one motion-coherent 3-axis lift.
13894
+ data-topo-chrome-zoom-level-color attr exposes the
13895
+ resolved color string for tests. */
13896
+ data-topo-chrome-zoom-level-color={hoveredZoomLevel ? 'headline' : 'text'}
13897
+ /* R593 — chrome zoom-level readout gains filter
13898
+ brightness(1.15) on hover. 32nd anchor in per-element
13899
+ brightness family. FIRST HTML-element brightness
13900
+ anchor outside the SVG canvas (the readout is an
13901
+ HTML <span>, not SVG).
13902
+
13903
+ Closes the chrome zoom-level readout's hover signature
13904
+ at 4 AXES:
13905
+ R347 letter-spacing 0 → 0.5px
13906
+ R420 fontWeight 500 → 600
13907
+ R517 color legendText → legendHeadline
13908
+ R593 brightness 1 → 1.15 ← this round
13909
+
13910
+ The chrome strip's only data display now has full
13911
+ 4-axis hover signature — color brightens to headline
13912
+ tier, glyph thickens, kerning spreads, AND brightness
13913
+ lifts another +15% on top of the color swap.
13914
+
13915
+ Triple-paint multiplicative interaction:
13916
+ color (legendHeadline) × brightness(1.15) — the
13917
+ headline tier reads dramatically brighter than a
13918
+ plain color swap.
13919
+
13920
+ Transition list extends with 'filter 200ms ease-out'
13921
+ matching the existing 200ms cadence across all 4 axes
13922
+ — motion-coherent state-flip.
13923
+
13924
+ data-topo-chrome-zoom-level-brightness attr exposes
13925
+ the gate for tests. */
13926
+ data-topo-chrome-zoom-level-brightness={hoveredZoomLevel ? '1.15' : '1'}
11221
13927
  onMouseEnter={() => setHoveredZoomLevel(true)}
11222
13928
  onMouseLeave={() => setHoveredZoomLevel(false)}
11223
13929
  style={{
11224
- color: pal.legendText,
13930
+ color: hoveredZoomLevel ? pal.legendHeadline : pal.legendText,
11225
13931
  borderColor: pal.containerBorder,
11226
13932
  minWidth: 46,
11227
13933
  display: 'inline-block',
13934
+ filter: hoveredZoomLevel ? 'brightness(1.15)' : undefined,
11228
13935
  // R347: letter-spacing hover tween — extends R344/R345
11229
13936
  // hover-letter-spacing family into the chrome strip.
11230
13937
  letterSpacing: hoveredZoomLevel ? '0.5px' : '0',
@@ -11254,7 +13961,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
11254
13961
  on theme flip while siblings eased. Sibling treatment
11255
13962
  to the nodeSize + zoom wrapper transitions added this
11256
13963
  round. */
11257
- transition: 'color 200ms ease-out, border-color 200ms ease-out, letter-spacing 200ms ease-out, font-weight 200ms ease-out',
13964
+ transition: 'color 200ms ease-out, border-color 200ms ease-out, letter-spacing 200ms ease-out, font-weight 200ms ease-out, filter 200ms ease-out',
11258
13965
  }}
11259
13966
  title="Current zoom level"
11260
13967
  >
@@ -11264,13 +13971,16 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
11264
13971
  onClick={() => { popChrome('zoom-in'); zoomByDiscrete(1.2); }}
11265
13972
  data-topo-chrome-zoom-in
11266
13973
  data-topo-chrome-zoom-in-popping={chromePopping === 'zoom-in' ? 'true' : 'false'}
13974
+ data-topo-chrome-zoom-in-brightness-hover="1.15"
11267
13975
  // R196: press-state (mirror of zoom-out above).
11268
13976
  // R352: `group` lets the inner svg respond via group-hover.
11269
13977
  // R493 — zoom +/− buttons join the chrome-strip active:scale-95
11270
13978
  // press-feedback family (R492 + nodeSize above). transition-
11271
13979
  // transform + duration-200 + ease-out + transform-gpu added
11272
13980
  // since the className had only transition-colors.
11273
- className="group px-2 py-1 hover:bg-white/5 active:bg-white/10 transition-colors transition-transform duration-200 ease-out transform-gpu active:scale-95 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset"
13981
+ /* R596 sibling zoom-in mirrors zoom-out above. 36th anchor
13982
+ in per-element brightness family, 5th HTML. */
13983
+ className="group px-2 py-1 hover:bg-white/5 hover:brightness-[1.15] active:bg-white/10 [transition-property:color,background-color,transform,filter] duration-200 ease-out transform-gpu active:scale-95 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset"
11274
13984
  style={{ color: pal.legendText }}
11275
13985
  aria-label="Zoom in"
11276
13986
  title="Zoom in (+)"
@@ -11329,9 +14039,49 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
11329
14039
  // active state during press = hover-lift (-1px) + scale-95
11330
14040
  // composes as translateY(-1px) scale(0.95) — lift-and-compress
11331
14041
  // for tactile click feel.
11332
- className="p-1.5 rounded-md border hover:bg-white/5 active:bg-white/10 hover:-translate-y-px active:scale-95 transition-colors transition-transform duration-200 ease-out transform-gpu focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60"
14042
+ className="p-1.5 rounded-md border hover:bg-white/5 active:bg-white/10 hover:-translate-y-px active:scale-95 transform-gpu focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60"
11333
14043
  data-topo-chrome-reset-hover-lift="true"
11334
- style={{ background: pal.legendBox.fill, borderColor: pal.containerBorder, color: pal.legendText }}
14044
+ /* R594 chrome reset button gains filter brightness(1.15)
14045
+ on hoveredReset. 33rd anchor in per-element brightness
14046
+ family, 2nd HTML-element anchor (R593 zoom-level was
14047
+ the first).
14048
+
14049
+ Closes reset button hover signature at 5 axes:
14050
+ R400 button hover-lift translateY(-1px)
14051
+ R350 icon hover-rotate -8°
14052
+ R453 icon stroke-width 2.5 → 2.8
14053
+ R514 icon scale 1.0 → 1.10
14054
+ R594 button brightness 1 → 1.15 ← this round
14055
+
14056
+ The reset button now has the densest hover signature
14057
+ among the 2 standalone chrome buttons (reset + fullscreen
14058
+ from R400). When user hovers, the entire button lifts
14059
+ (-1px), brightens (+15%), AND its icon rotates (-8°),
14060
+ thickens (+0.3 sw), and scales up (+10%). Inside +
14061
+ outside motion-coherent.
14062
+
14063
+ Triple-paint multiplicative interaction at this surface:
14064
+ bg (hover:bg-white/5) × border (pal.containerBorder)
14065
+ × color (pal.legendText) ALL get brightness(1.15)
14066
+ multiplied in — the button's chrome stack uniformly
14067
+ brightens, reading as "this control is awake under
14068
+ your cursor".
14069
+
14070
+ Inline transition shorthand replaces the className-
14071
+ based `transition-colors transition-transform` (R557
14072
+ banked: when adding new transition-driven hover axes
14073
+ to an element, extend the INLINE list — inline
14074
+ overrides className). All 5 transition properties (bg
14075
+ / color / border / transform / filter) now ride one
14076
+ 200ms ease-out beat. */
14077
+ data-topo-chrome-reset-brightness={hoveredReset ? '1.15' : '1'}
14078
+ style={{
14079
+ background: pal.legendBox.fill,
14080
+ borderColor: pal.containerBorder,
14081
+ color: pal.legendText,
14082
+ filter: hoveredReset ? 'brightness(1.15)' : undefined,
14083
+ transition: 'color 200ms ease-out, background-color 200ms ease-out, border-color 200ms ease-out, transform 200ms ease-out, filter 200ms ease-out',
14084
+ }}
11335
14085
  aria-label="Reset view"
11336
14086
  title="Reset zoom + pan (0, or double-click the canvas)"
11337
14087
  >
@@ -11377,8 +14127,34 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
11377
14127
  // owns transform during its 450ms run. transformOrigin
11378
14128
  // 'center' so rotation pivots around the icon's centre
11379
14129
  // (default would be top-left and the icon would arc).
14130
+ /* Round 514 / Loop — extends R352/R353 chrome icon hover-
14131
+ scale family to the reset button. Pre-R514 the reset
14132
+ icon had hover-rotate (-8°, R350) + hover-sw (2.5→2.8,
14133
+ R453) but no hover-scale, while zoom-out (R352), zoom-
14134
+ in (R352), and fullscreen (R353) icons all carried
14135
+ `group-hover:scale-110`. R514 brings the reset icon
14136
+ into the same 3-axis hover signature (rotate + sw +
14137
+ scale) as the rest of the chrome strip.
14138
+ Implementation: inline transform composes rotate +
14139
+ scale into one string. `transform: rotate(-8deg)
14140
+ scale(1.1)` on hover; `rotate(0) scale(1)` at rest.
14141
+ transformOrigin 'center' applies to both — rotation
14142
+ pivots around centre AND scale grows from centre.
14143
+ The Tailwind `group-hover:scale-110` approach can't
14144
+ work here because inline `style.transform` overrides
14145
+ className-based transforms; compose the multi-axis
14146
+ transform inline instead.
14147
+ Chrome icon hover gesture parity (post-R514):
14148
+ zoom-out scale-110 + sw-lift (R352/R454)
14149
+ zoom-in scale-110 + sw-lift (R352/R454)
14150
+ fullscreen scale-110 + sw-lift (R353/R455)
14151
+ reset scale-1.1 + sw-lift + rotate -8°
14152
+ (R514 + R453 + R350)
14153
+ reset gets the EXTRA rotate axis because R350's spin
14154
+ preview semantic is reset-specific — the rotation
14155
+ hints at the click-spin (R184) the button will fire. */
11380
14156
  style={{
11381
- transform: hoveredReset && !resetSpinning ? 'rotate(-8deg)' : 'rotate(0deg)',
14157
+ transform: hoveredReset && !resetSpinning ? 'rotate(-8deg) scale(1.1)' : 'rotate(0deg) scale(1)',
11382
14158
  transformOrigin: 'center',
11383
14159
  transition: 'transform 200ms ease-out, stroke-width 200ms ease-out',
11384
14160
  }}
@@ -11402,9 +14178,15 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
11402
14178
  classes win specificity. */}
11403
14179
  <button
11404
14180
  onClick={() => { popChrome('fullscreen'); toggleFullscreen(); }}
14181
+ onMouseEnter={() => setHoveredFullscreen(true)}
14182
+ onMouseLeave={() => setHoveredFullscreen(false)}
14183
+ onFocus={() => setHoveredFullscreen(true)}
14184
+ onBlur={() => setHoveredFullscreen(false)}
11405
14185
  data-topo-chrome-fullscreen
11406
14186
  data-topo-chrome-fullscreen-active={isFullscreen ? 'true' : 'false'}
11407
14187
  data-topo-chrome-fullscreen-popping={chromePopping === 'fullscreen' ? 'true' : 'false'}
14188
+ data-topo-chrome-fullscreen-hover={hoveredFullscreen ? 'true' : 'false'}
14189
+ data-topo-chrome-fullscreen-brightness={hoveredFullscreen ? '1.15' : '1'}
11408
14190
  // R196: fullscreen also picks up press-state — active variant
11409
14191
  // deepens cyan-500/20 → cyan-500/25 on press; non-active
11410
14192
  // deepens white/5 → white/10.
@@ -11424,17 +14206,48 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
11424
14206
  // R400: hover translateY(-1px) lift — see reset button above for family doc.
11425
14207
  // R493 — fullscreen joins active:scale-95 press family (same as
11426
14208
  // reset above: lift-and-compress compound transform on press).
11427
- className={`group p-1.5 rounded-md border hover:-translate-y-px active:scale-95 transition-colors transition-transform duration-200 ease-out transform-gpu focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 ${
14209
+ className={`group p-1.5 rounded-md border hover:-translate-y-px active:scale-95 transform-gpu focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 ${
11428
14210
  isFullscreen
11429
- ? 'bg-cyan-500/15 text-cyan-300 font-medium hover:bg-cyan-500/20 active:bg-cyan-500/25'
14211
+ ? 'bg-cyan-500/15 text-cyan-300 font-medium hover:bg-cyan-500/20 hover:text-cyan-200 active:bg-cyan-500/25'
11430
14212
  : 'hover:bg-cyan-500/5 active:bg-cyan-500/15'
11431
14213
  }${chromePopping === 'fullscreen' ? ' anet-chrome-pop' : ''}`}
11432
14214
  data-topo-chrome-fullscreen-hover-lift="true"
14215
+ /* R595 — chrome fullscreen button gains filter brightness(1.15)
14216
+ on hoveredFullscreen. 34th anchor in per-element brightness
14217
+ family, 3rd HTML-element anchor (R593 zoom-level + R594
14218
+ reset). Sibling to R594 — closes the standalone chrome
14219
+ button pair (reset + fullscreen) at full brightness parity.
14220
+
14221
+ Per the R400 family doc, reset + fullscreen are the only
14222
+ two standalone (non-segmented) chrome buttons. Both now
14223
+ share the same 5-axis hover signature pattern:
14224
+ button hover-lift translateY(-1px) R400
14225
+ icon hover-scale 1.0 → 1.10 R353 (full) / R514 (reset)
14226
+ icon stroke-width 2.5 → 2.8 R455 (full) / R453 (reset)
14227
+ icon hover-rotate R576 (full +3°) / R350 (reset -8°)
14228
+ button brightness 1 → 1.15 R595 (full) / R594 (reset)
14229
+
14230
+ Inline transition shorthand replaces the className-based
14231
+ `transition-colors transition-transform` (R557 banked:
14232
+ inline overrides className for transition-driven hover
14233
+ axes). 5 transition properties (bg / color / border /
14234
+ transform / filter) ride one 200ms ease-out beat.
14235
+
14236
+ Active-variant interaction: when isFullscreen=true the
14237
+ className applies cyan bg + cyan text. brightness(1.15)
14238
+ on top makes the active+hovered state read at maximum
14239
+ vivid cyan — the user knows they're poised to EXIT
14240
+ fullscreen with extra visual confirmation.
14241
+
14242
+ data-topo-chrome-fullscreen-brightness attr exposes
14243
+ the gate for tests. */
11433
14244
  style={{
11434
14245
  borderColor: pal.containerBorder,
11435
14246
  ...(isFullscreen
11436
14247
  ? {}
11437
14248
  : { background: pal.legendBox.fill, color: pal.legendText }),
14249
+ filter: hoveredFullscreen ? 'brightness(1.15)' : undefined,
14250
+ transition: 'color 200ms ease-out, background-color 200ms ease-out, border-color 200ms ease-out, transform 200ms ease-out, filter 200ms ease-out',
11438
14251
  }}
11439
14252
  aria-label={isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}
11440
14253
  title={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}
@@ -11460,12 +14273,20 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
11460
14273
  + R455 fullscreen (this round). transition-[transform,
11461
14274
  stroke-width] expands existing transition-transform
11462
14275
  so the sw lift eases under R352 scale-110 cadence. */}
14276
+ {/* Round 576 / Loop — fullscreen icon picks up hover-rotate-3.
14277
+ Joins R350 reset / R547 pill × / R549 brand logo at 4th
14278
+ anchor in hover-rotate idiom. Corner arrows rotate 3° on
14279
+ hover — subtle "preparing to transform" gesture (enter
14280
+ or exit fullscreen). Tailwind v4 emits individual rotate
14281
+ property (banked R547) — sits alongside group-hover:
14282
+ scale-110 in independent rotate axis. */}
11463
14283
  {isFullscreen ? (
11464
- <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">
14284
+ <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:rotate-3 group-hover:[stroke-width:2.8] transform-gpu" data-topo-chrome-fullscreen-icon="exit">
11465
14285
  <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" />
11466
14286
  </svg>
11467
14287
  ) : (
11468
- <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">
14288
+ // R576 sibling enter variant also picks up rotate-3.
14289
+ <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:rotate-3 group-hover:[stroke-width:2.8] transform-gpu" data-topo-chrome-fullscreen-icon="enter">
11469
14290
  <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" />
11470
14291
  </svg>
11471
14292
  )}