@sleep2agi/agent-network-dashboard 0.5.3-preview.1 → 0.5.3-preview.100

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 (261) 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/0-c2b_r~s.a00.js +1 -0
  145. package/.next/static/chunks/{0ce~u5hn~wege.js → 0nvxj45vzw-1t.js} +1 -1
  146. package/.next/static/chunks/{03a4--7ncekmk.js → 0v4-5tng.uh.7.js} +2 -2
  147. package/.next/static/chunks/0v9i~z.k.2uji.js +1 -0
  148. package/.next/static/chunks/0w3dd-f9bl6xs.js +4 -0
  149. package/.next/static/chunks/13~aih56vx-cf.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 +2747 -140
  154. package/app/globals.css +142 -6
  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-press-test.mjs +93 -0
  168. package/scripts/topo-chip-row-tier-glow-brightness-test.mjs +99 -0
  169. package/scripts/topo-chip-row-unit-hover-tracking-test.mjs +124 -0
  170. package/scripts/topo-chrome-press-fullstrip-test.mjs +105 -0
  171. package/scripts/topo-chrome-press-scale-test.mjs +100 -0
  172. package/scripts/topo-cluster-count-attr-test.mjs +80 -0
  173. package/scripts/topo-crescent-breath-test.mjs +104 -0
  174. package/scripts/topo-crescent-recede-test.mjs +111 -0
  175. package/scripts/topo-edge-badge-hover-glow-test.mjs +90 -0
  176. package/scripts/topo-edge-badge-text-brightness-test.mjs +83 -0
  177. package/scripts/topo-edge-particle-brightness-test.mjs +82 -0
  178. package/scripts/topo-edge-pill-glow-test.mjs +67 -0
  179. package/scripts/topo-edge-visible-brightness-test.mjs +84 -0
  180. package/scripts/topo-endpoint-ring-brightness-test.mjs +83 -0
  181. package/scripts/topo-filter-pill-glow-test.mjs +90 -0
  182. package/scripts/topo-filter-pills-press-test.mjs +96 -0
  183. package/scripts/topo-fleet-density-tier-test.mjs +84 -0
  184. package/scripts/topo-flow-rail-brightness-test.mjs +80 -0
  185. package/scripts/topo-focus-outline-transition-test.mjs +107 -0
  186. package/scripts/topo-freshness-chip-fade-test.mjs +105 -0
  187. package/scripts/topo-fullscreen-attr-test.mjs +73 -0
  188. package/scripts/topo-fullscreen-icon-rotate-test.mjs +93 -0
  189. package/scripts/topo-grid-content-bottom-attr-test.mjs +72 -0
  190. package/scripts/topo-group-label-brightness-test.mjs +84 -0
  191. package/scripts/topo-group-label-hover-glow-test.mjs +86 -0
  192. package/scripts/topo-group-label-member-alias-hover-test.mjs +125 -0
  193. package/scripts/topo-group-pill-glow-test.mjs +76 -0
  194. package/scripts/topo-hover-ring-duration-test.mjs +87 -0
  195. package/scripts/topo-hub-digit-brightness-test.mjs +79 -0
  196. package/scripts/topo-hub-digit-ls-test.mjs +119 -0
  197. package/scripts/topo-hub-halo-brightness-test.mjs +80 -0
  198. package/scripts/topo-hub-halo-glow-test.mjs +96 -0
  199. package/scripts/topo-hub-highlight-amplify-test.mjs +139 -0
  200. package/scripts/topo-hub-highlight-brightness-test.mjs +84 -0
  201. package/scripts/topo-hub-highlight-fill-transition-test.mjs +84 -0
  202. package/scripts/topo-hub-highlight-glow-test.mjs +99 -0
  203. package/scripts/topo-hub-highlight-r-test.mjs +112 -0
  204. package/scripts/topo-hub-highlight-recede-test.mjs +144 -0
  205. package/scripts/topo-hub-highlight-theme-fill-test.mjs +83 -0
  206. package/scripts/topo-hub-hover-ring-brightness-test.mjs +79 -0
  207. package/scripts/topo-hub-hover-ring-glow-test.mjs +97 -0
  208. package/scripts/topo-hub-idle-breath-test.mjs +109 -0
  209. package/scripts/topo-hub-recede-test.mjs +124 -0
  210. package/scripts/topo-hub-spoke-brightness-test.mjs +77 -0
  211. package/scripts/topo-hub-spoke-glow-test.mjs +112 -0
  212. package/scripts/topo-layout-hover-fw-test.mjs +98 -0
  213. package/scripts/topo-legend-count-letter-spacing-test.mjs +108 -0
  214. package/scripts/topo-legend-label-fw-test.mjs +107 -0
  215. package/scripts/topo-legend-row-label-glow-test.mjs +102 -0
  216. package/scripts/topo-legend-swatch-glow-test.mjs +109 -0
  217. package/scripts/topo-legend-swatch-member-alias-match-test.mjs +139 -0
  218. package/scripts/topo-minimap-hover-glow-test.mjs +109 -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-hot-pulse-test.mjs +102 -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-reduced-motion-attr-test.mjs +69 -0
  240. package/scripts/topo-reset-icon-hover-scale-test.mjs +102 -0
  241. package/scripts/topo-runtime-badge-brightness-test.mjs +78 -0
  242. package/scripts/topo-runtime-badge-glow-test.mjs +108 -0
  243. package/scripts/topo-starfield-hue-test.mjs +109 -0
  244. package/scripts/topo-status-ring-brightness-test.mjs +84 -0
  245. package/scripts/topo-svg-focus-transition-test.mjs +105 -0
  246. package/scripts/topo-titleblock-h2-hover-fw-test.mjs +109 -0
  247. package/scripts/topo-titleblock-h2-hover-tracking-test.mjs +128 -0
  248. package/scripts/topo-titleblock-kicker-hover-test.mjs +134 -0
  249. package/scripts/topo-vendor-activelinks-press-test.mjs +100 -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-level-color-test.mjs +105 -0
  255. package/.next/static/chunks/0a4hmfvj-81x5.css +0 -2
  256. package/.next/static/chunks/14m8prv3qgm45.js +0 -4
  257. package/.next/static/chunks/17fq.aa.hsdd..js +0 -1
  258. package/.next/static/chunks/17~las5t-t.kj.js +0 -1
  259. /package/.next/static/{cBZM18sgaGtUo-xccD_a3 → oBpu6kOuBIOwABE5kiB8V}/_buildManifest.js +0 -0
  260. /package/.next/static/{cBZM18sgaGtUo-xccD_a3 → oBpu6kOuBIOwABE5kiB8V}/_clientMiddlewareManifest.js +0 -0
  261. /package/.next/static/{cBZM18sgaGtUo-xccD_a3 → oBpu6kOuBIOwABE5kiB8V}/_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
  };
@@ -1699,7 +1725,18 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1699
1725
  width) = 166px total title-block width vs 168px pre-R298 —
1700
1726
  no measurable layout shift, just a deliberate tighter
1701
1727
  grouping. */}
1702
- <div className="flex items-center gap-2.5">
1728
+ {/* Round 554 / Loop — title-block wrapper picks up `group` so
1729
+ the H2 below can subscribe to `group-hover:tracking-tighter`.
1730
+ Pairs with R548/R549 brand-logo hover gestures: cursor
1731
+ sweeping anywhere across the title cluster fires the brand
1732
+ logo's scale-105 + rotate-6 + breath ↔ AND tightens the
1733
+ H2's tracking from -0.025em → -0.05em.
1734
+ Makes the title-block read as one coherent hover cluster —
1735
+ brand mark provides the loud gesture (scale + rotate), H2
1736
+ provides the subtle editorial gesture (kerning tighten).
1737
+ data-topo-section-titleblock-group attr surfaces the gate
1738
+ for tests. */}
1739
+ <div className="group flex items-center gap-2.5" data-topo-section-titleblock-group>
1703
1740
  {/* Round 297 / Loop: brand-logo color picks up the 200ms ease-
1704
1741
  out transition. Pre-R297 the moon glyph had theme-
1705
1742
  conditional color (cyber #67e8f9 cyan ↔ light #0d9488
@@ -1723,13 +1760,136 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1723
1760
  overpowering the h2 at text-lg/font-semibold (R286).
1724
1761
  viewBox 32×32 unchanged so the inner crescent geometry
1725
1762
  scales proportionally. */}
1763
+ {/* Round 548 / Loop — title-block brand logo gains subtle
1764
+ hover-scale gesture. Pre-R548 the 40×40 crescent was
1765
+ fully static — no hover affordance. R548 adds hover:
1766
+ scale-105 (Tailwind 4 emits as `scale: 1.05`) so the
1767
+ brand mark gently responds to attention as the user's
1768
+ cursor sweeps across the title block. 5% scale is
1769
+ intentionally subtle (vs R350 chrome icon hover-scale-
1770
+ 110): the brand logo is a passive identity mark, not
1771
+ an interactive control, so the gesture stays small.
1772
+ cursor: default to clarify non-clickability (the SVG
1773
+ isn't a button; just a brand element).
1774
+ transition-transform duration-200 ease-out matches the
1775
+ dashboard's R350-family hover-scale cadence so the
1776
+ brand logo's response shares the same motion vocabulary
1777
+ as the chrome strip's icon scales. transform-gpu hint
1778
+ promotes the SVG to its own compositor layer for crisp
1779
+ edges during the tween.
1780
+ Brand-mark delight gesture family (1 anchor):
1781
+ R548 title-block brand logo hover:scale-105
1782
+ The crescent at canvas top-left (data-topo-brand-
1783
+ canvas-mark) and the watermark at bottom-left (data-
1784
+ topo-brand-watermark) are intentionally LEFT STATIC —
1785
+ both have pointerEvents:none and exist as ambient
1786
+ decoration with their own breath/recede dynamics
1787
+ (R519/R525/R526/R528). The title-block logo is the
1788
+ ONLY brand surface that's a candidate for hover affordance,
1789
+ since it sits in the chrome-band where the cursor
1790
+ naturally passes during normal use. */}
1791
+ {/* Round 549 / Loop — extends R548 (hover:scale-105) with a
1792
+ subtle hover:rotate-6 rotation. Pairs scale + rotate on
1793
+ the brand mark, same idiom as R547 added to the pill ×
1794
+ close buttons (scale-110 + rotate-12) but at gentler
1795
+ amounts: 105% vs 110% scale and 6° vs 12° rotation —
1796
+ brand marks want restraint vs interactive close buttons.
1797
+
1798
+ The crescent-moon shape (curved cutout via mask) reads
1799
+ visually distinct as it rotates — the asymmetric cutout
1800
+ swings into a fresh angle, telegraphing "this brand mark
1801
+ is alive without being loud". The 6° landing lands the
1802
+ moon's cusp pointing slightly NE rather than straight up,
1803
+ a small but legible reveal of the shape's geometry.
1804
+
1805
+ Tailwind 4 emits BOTH `scale: 1.05` AND `rotate: 6deg`
1806
+ as INDIVIDUAL CSS properties (not a combined transform
1807
+ string — see R547 banked pattern). The className keeps
1808
+ transition-transform so both axes ease at the same 200ms
1809
+ cadence; transform-gpu hint stays so the compositor
1810
+ promotes both axes to GPU layers.
1811
+
1812
+ data-topo-brand-logo-hover-rotate attr surfaces the
1813
+ landing rotation for tests. Pair with R548's
1814
+ data-topo-brand-logo-hover-scale attr — both attrs
1815
+ advertise the dual-axis hover signature. */}
1816
+ {/* Round 553 / Loop — title-block brand logo gains subtle
1817
+ idle opacity breath (~0.92 ↔ 1, 5s ease-in-out cycle).
1818
+ 5th anchor in the 呼吸感 breath family, slotting into
1819
+ the ascending cadence ladder between hub idle (4s) and
1820
+ watermark (6s):
1821
+ row hot 3s
1822
+ hub idle 4s
1823
+ brand logo 5s ← this round
1824
+ watermark 6s
1825
+ crescent 7s
1826
+ Composes cleanly with R548 hover:scale-105 + R549
1827
+ hover:rotate-6 — opacity, scale, and rotate are
1828
+ independent CSS properties; the moon keeps breathing
1829
+ as it scales and rotates on hover. Layered effect
1830
+ reads as "this brand mark is alive even before you
1831
+ touch it, and lights up further on hover".
1832
+ Reduced-motion gate: component-side `!reducedMotion`
1833
+ toggles the className (canonical TopoGraph breath
1834
+ pattern); R29 globals.css blanket provides a
1835
+ defense-in-depth fallback (animation-duration →
1836
+ 0.001ms under prefers-reduced-motion: reduce).
1837
+ data-topo-brand-logo-breath attr exposes the gate
1838
+ state for tests. */}
1839
+ {/* Round 557 / Loop — brand logo gains 4th hover axis:
1840
+ hover:brightness-110 (filter). Adds a chromatic axis
1841
+ to the brand-mark hover signature alongside R548
1842
+ scale, R549 rotate, R553 idle breath:
1843
+ R548 hover:scale-105 transform-scale
1844
+ R549 hover:rotate-6 transform-rotate
1845
+ R553 idle breath (5s) opacity (animation)
1846
+ R557 hover:brightness-110 filter ← this round
1847
+ All 4 axes ride on INDEPENDENT CSS properties (scale,
1848
+ rotate, opacity, filter) — they compose freely without
1849
+ clobbering each other. The cyan/teal crescent gains a
1850
+ soft +10% brightness boost on hover, layered on top of
1851
+ the existing scale + rotate lift + ongoing idle breath.
1852
+ Why +10% (vs more aggressive 125/150): brand mark wants
1853
+ restraint. The eye reads the brightness shift as "this
1854
+ mark lights up under attention" without crossing into
1855
+ "this mark is now glowing".
1856
+ Implementation: className extends transition-transform
1857
+ → transition-[transform,filter] so the brightness
1858
+ tweens at the same 200ms ease-out cadence as the scale
1859
+ + rotate axes — one motion-coherent 3-property hover
1860
+ lift on the className tier (plus the inline color 200ms
1861
+ transition for theme-toggle eases).
1862
+ Brand-mark family axis count: 4 hover-state axes
1863
+ cleanly factor across:
1864
+ geometry (scale + rotate)
1865
+ paint (opacity breath + brightness on hover)
1866
+ Cluster reads as "alive, lifting, and lighting up under
1867
+ attention" — three independent gesture vocabularies on
1868
+ one surface.
1869
+ data-topo-brand-logo-hover-brightness attr surfaces
1870
+ the landing value for tests. */}
1726
1871
  <svg
1727
1872
  width="40" height="40" viewBox="0 0 32 32" aria-hidden
1728
- className="shrink-0"
1873
+ 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
1874
  data-topo-brand-logo
1875
+ data-topo-brand-logo-hover-scale="1.05"
1876
+ data-topo-brand-logo-hover-rotate="6deg"
1877
+ data-topo-brand-logo-hover-brightness="1.1"
1878
+ data-topo-brand-logo-breath={!reducedMotion ? 'true' : 'false'}
1730
1879
  style={{
1731
1880
  color: isLight ? '#0d9488' : '#67e8f9',
1732
- transition: 'color 200ms ease-out',
1881
+ cursor: 'default',
1882
+ // R557 — extend transition list to include filter (and
1883
+ // re-spec transform for cadence parity) so the new
1884
+ // hover:brightness-110 axis eases at 200ms alongside
1885
+ // the existing color 200ms (theme-toggle ease) and the
1886
+ // className-based hover:scale-105 / hover:rotate-6.
1887
+ // Inline transition is a shorthand and overrides the
1888
+ // className's transition-[transform,filter] — listing
1889
+ // all axes here ensures the eased property set covers
1890
+ // color (theme) + transform (scale + rotate) + filter
1891
+ // (brightness) at uniform 200ms ease-out.
1892
+ transition: 'color 200ms ease-out, transform 200ms ease-out, filter 200ms ease-out',
1733
1893
  }}
1734
1894
  >
1735
1895
  <mask id="s2a-titleblock-moon-mask">
@@ -1777,7 +1937,40 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1777
1937
  R300 marks the milestone of 25 rounds (R275-R300) of
1778
1938
  continuous TopoGraph polish + codex's Vincent 5215/
1779
1939
  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>
1940
+ {/* Round 555 / Loop kicker "Network Topology" gains group-
1941
+ hover affordance via the R554 wrapper's `group` flag,
1942
+ closing the title-block cluster's hover coverage at 3
1943
+ surfaces (brand logo + H2 + kicker).
1944
+ Picks up the small-label SPREAD direction (R554 banked
1945
+ "small labels SPREAD on hover / large headlines TIGHTEN"
1946
+ — kicker is xs uppercase, definitely a small label) plus
1947
+ a color brighten (text-gray-500 #6b7280 → text-gray-400
1948
+ #9ca3af).
1949
+ Spread: tracking-widest (0.1em rest) → 0.13em hover —
1950
+ +30% kerning bump. At text-xs (12px) the per-gap shift
1951
+ is 1.2px → 1.56px (+0.36px/gap), legible without
1952
+ overshooting the rest's tracking-widest editorial base.
1953
+ Color: text-gray-500 → text-gray-400 — one tier lighter,
1954
+ same idiom as R296 (kicker rest tone-up from gray-600 to
1955
+ gray-500), now extended at the hover-state tier.
1956
+ transition-[letter-spacing,color] duration-200 ease-out
1957
+ matches the 200ms cadence of R554 H2 ls + the rest of
1958
+ the hover-ls family (R344/R345/R347/R351/R420/R427/R431/
1959
+ R432/R434/R527/R539).
1960
+ Title-block cluster signature post-R555 (3 surfaces):
1961
+ brand logo loud scale + rotate + breath
1962
+ (R548/R549/R553)
1963
+ H2 subtle tracking-tighter
1964
+ (R554, editorial-tighten)
1965
+ kicker subtle tracking-spread + color lift
1966
+ (R555, data-spread) ← this round
1967
+ Two of the three surfaces are typographic; the brand
1968
+ logo carries the geometric+chromatic motion. Cluster
1969
+ reads as ONE coherent hover unit through three
1970
+ independent gesture vocabularies.
1971
+ data-topo-section-kicker-hover-tracking + -hover-color
1972
+ attrs expose the landing values for tests. */}
1973
+ <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
1974
  {/* Round 286 / Loop: title 'Command mesh' adopts tracking-tight
1782
1975
  (-0.025em) to complement R285 kicker tracking-widest. Wide
1783
1976
  eyebrow + tight headline is the conventional editorial
@@ -1789,7 +1982,62 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1789
1982
  cumulatively legible across 12 characters. font-semibold
1790
1983
  (600) stays — tracking-tight does the heavy lifting for
1791
1984
  the editorial register. */}
1792
- <h2 className="text-lg text-white font-semibold leading-tight tracking-tight" data-topo-section-title>Command mesh</h2>
1985
+ {/* Round 554 / Loop — H2 "Command mesh" gains group-hover-
1986
+ gated tracking-tighter via the wrapper's R554 `group`
1987
+ flag. Pre-R554 the H2 was fully static (R286 tracking-
1988
+ tight only at rest). R554 adds an editorial-tighten
1989
+ gesture: when cursor sweeps anywhere across the title
1990
+ cluster (brand logo OR title text), the headline tightens
1991
+ from -0.025em → -0.05em.
1992
+ Inverts the typical hover-letter-spacing direction:
1993
+ small labels (chip counts, panel titles, edge digits)
1994
+ SPREAD on hover → "data telegraphing"
1995
+ large headlines (h2 Command mesh)
1996
+ TIGHTEN on hover → "editorial emphasis"
1997
+ Both directions are coherent design language — small
1998
+ data wants spacing for legibility; large headlines want
1999
+ tightening for designed-headline polish. Same idiom as
2000
+ the conventional editorial pairing of "wide kicker +
2001
+ tight headline" R285/R286 set up (kicker spreads 0.1em
2002
+ tracking-widest; headline tightens -0.025em tracking-
2003
+ tight) — R554 deepens that pairing's tighten side at
2004
+ the hover-state tier.
2005
+ At text-lg (18px) the shift is -0.45px → -0.9px per
2006
+ gap (~5.4px total tightening across "Command mesh" 12
2007
+ chars). Subtle but legible when the cursor sweeps in.
2008
+ transition-[letter-spacing] duration-200 ease-out
2009
+ matches the 200ms hover-ls cadence used at R344/R345/
2010
+ R347/R351/R420/R427/R431/R432/R434/R527/R539 family
2011
+ anchors.
2012
+ data-topo-section-title-hover-tracking attr surfaces
2013
+ the landing tracking class for tests. */}
2014
+ {/* Round 556 / Loop — H2 "Command mesh" gains a 2nd
2015
+ editorial-emphasis axis: group-hover:font-bold paired
2016
+ with R554's group-hover:tracking-tighter. Both lifts
2017
+ fire on the same R554 wrapper's `group` flag (hover
2018
+ anywhere in the title cluster → BOTH H2 axes intensify
2019
+ simultaneously).
2020
+ H2 hover signature post-R556 (2 typographic axes
2021
+ intensify together):
2022
+ rest font-semibold 600 + tracking-tight -0.025em
2023
+ hover font-bold 700 + tracking-tighter -0.05em
2024
+ Editorial emphasis through TWO axes — heavier AND
2025
+ tighter on hover. Mirrors the conventional "designed-
2026
+ headline emphasis" idiom (heavier + tighter = more
2027
+ authoritative; the eye reads both axes as intensifying
2028
+ the same semantic).
2029
+ Hover-fw family extension (6 anchors now):
2030
+ R416 chip-row count digit (chip group-hover)
2031
+ R420 chrome zoom-level (hover)
2032
+ R425 hub-center digit (hub hover)
2033
+ R520 +N more flows footer (recent panel hover)
2034
+ R521 chrome nodeSize S/M/L (inactive hover)
2035
+ R556 title-block H2 (cluster group-hover) ← this round
2036
+ Transition list extends to include 'font-weight 200ms
2037
+ ease-out' alongside the existing 'letter-spacing'
2038
+ 200ms cadence. data-topo-section-title-hover-fw attr
2039
+ surfaces the landing weight for tests. */}
2040
+ <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
2041
  </div>
1794
2042
  </div>
1795
2043
  {/* Round 328 / Loop: chip-row strip wrapper gap 2 → 2.5
@@ -1943,8 +2191,77 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1943
2191
  // doesn't list letter-spacing, so without this the
1944
2192
  // hover:tracking-wide would snap. Sibling change on
1945
2193
  // the Grid button below.
1946
- className={`px-2.5 py-1 focus:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset hover:tracking-wide ${layout === 'ring' ? 'bg-cyan-500/15 text-cyan-300 font-medium hover:bg-cyan-500/20 active:bg-cyan-500/25' : 'text-gray-400 hover:text-cyan-300 hover:bg-cyan-500/5 active:bg-cyan-500/15'} ${chromePopping === 'layout-ring' ? ' anet-chrome-pop' : ''}`}
1947
- style={{ transition: 'background-color 150ms ease, color 150ms ease, letter-spacing 200ms ease-out' }}
2194
+ // Round 492 / Loop add `active:scale-95` press feedback
2195
+ // alongside R196's `active:bg-cyan-500/25` color-deepen.
2196
+ // Pre-R492 the chrome-strip Ring/Grid buttons had color
2197
+ // tactile (deeper cyan on mouse-down) + R249 chrome-pop
2198
+ // on release, but no transform during the press itself —
2199
+ // the button stayed planted between mouse-down and pop.
2200
+ // Adding `active:scale-95` (5% compression) on the
2201
+ // pressed pseudo-state, with `transform 150ms ease-out`
2202
+ // bundled into the inline transition list, gives haptic-
2203
+ // like push-back feedback. The press-down (down to 95%
2204
+ // scale) eases in over 150ms in sync with the bg/color
2205
+ // deepen; the release auto-springs back to scale-100 via
2206
+ // the same transition, then R249's anet-chrome-pop class
2207
+ // overlays the release-pop. Matching `transform-gpu`
2208
+ // promotes the layer so the scale doesn't trigger
2209
+ // layout/paint thrash. Sibling change on Grid below.
2210
+ /* Round 522 / Loop — extends R521's typography-preview
2211
+ idiom (chrome nodeSize hover:font-medium 400 → 500) to
2212
+ the Ring/Grid layout toggle's inactive variant. Pre-
2213
+ R522 the inactive Ring/Grid had `hover:text-cyan-300
2214
+ hover:bg-cyan-500/5` (R270 color + bg previews of the
2215
+ active state) but no typography preview — the active
2216
+ variant uses `font-medium` (fw 500), inactive sat at
2217
+ default fw 400 even on hover. R522 adds `hover:font-
2218
+ medium` to the inactive Ring/Grid so the rest-vs-hover
2219
+ transition previews the typography state the click
2220
+ would commit to, matching the click commits's locked
2221
+ weight.
2222
+ font-weight 150ms appended to the transition list
2223
+ matching the existing 150ms color/bg cadence at this
2224
+ button — when hover lifts color (gray-400 → cyan-300)
2225
+ + bg (transparent → cyan-500/5) + fw (400 → 500), all
2226
+ 3 ease at the same 150ms beat.
2227
+ Hover-fw family extension (6 anchors): R416/R420/R425/
2228
+ R520/R521/R522. R522 closes the chrome toggle group
2229
+ typography preview at the last remaining toggle —
2230
+ layout (Ring/Grid). After R521 (nodeSize) + R522
2231
+ (layout), every multi-state chrome toggle has hover-
2232
+ fw preview on its inactive variant.
2233
+ data-topo-chrome-layout-hover-preview-fw="500" attr
2234
+ on inactive button exposes the polish for tests. */
2235
+ /* Round 552 / Loop — chrome active-variant gains hover:
2236
+ text-cyan-200, lifting text one brightness tier alongside
2237
+ the existing hover:bg-cyan-500/20 bg deepen. Coordinated
2238
+ 4-anchor edit (replace_all touched 4 sibling lines sharing
2239
+ the identical active-variant className substring):
2240
+ Ring (this line) layout === 'ring'
2241
+ Grid (line ~2097) layout === 'grid'
2242
+ S/M/L (line ~12635) nodeScale === v
2243
+ Fscrn (line ~13030) isFullscreen
2244
+ Pre-R552 the active variant's hover state only deepened bg
2245
+ (cyan-500/15 → /20); text stayed planted at cyan-300. The
2246
+ inactive variant already lifts text on hover (text-gray-400
2247
+ → text-cyan-300). R552 brings parity: active variant lifts
2248
+ text one tier brighter (cyan-300 → cyan-200) on hover,
2249
+ mirroring the inactive variant's "text brightens on hover"
2250
+ gesture at the next brightness step.
2251
+ Brightness ladder snapshot (cyan):
2252
+ cyan-400 brand chrome focus ring
2253
+ cyan-300 active-variant rest ←─┐
2254
+ │ +1 tier on hover
2255
+ cyan-200 active-variant hover ←─┘ (this round)
2256
+ Pure paint axis (text color); bbox/geometry unchanged.
2257
+ transition-colors already in the class list so the cyan-
2258
+ 300 → cyan-200 swap eases at the existing 200ms cadence.
2259
+ hover-color brighten family extension at the chrome strip
2260
+ active-variant scope; sibling to the inactive variant's
2261
+ R163/R178/R179/R270 hover:text-cyan-300 idiom. */
2262
+ 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 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' : ''}`}
2263
+ data-topo-chrome-layout-hover-preview-fw={layout === 'ring' ? null : '500'}
2264
+ style={{ transition: 'background-color 150ms ease, color 150ms ease, letter-spacing 200ms ease-out, transform 150ms ease-out, font-weight 150ms ease' }}
1948
2265
  >
1949
2266
  Ring
1950
2267
  </button>
@@ -1963,7 +2280,15 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1963
2280
  // all chrome buttons.
1964
2281
  // R351 sibling — Grid button picks up hover:tracking-wide
1965
2282
  // + inline transition spec. Same vocabulary as Ring.
1966
- className={`px-2.5 py-1 border-l focus:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset hover:tracking-wide ${layout === 'grid' ? 'bg-cyan-500/15 text-cyan-300 font-medium hover:bg-cyan-500/20 active:bg-cyan-500/25' : 'text-gray-400 hover:text-cyan-300 hover:bg-cyan-500/5 active:bg-cyan-500/15'} ${chromePopping === 'layout-grid' ? ' anet-chrome-pop' : ''}`}
2283
+ // R492 sibling Grid button picks up active:scale-95
2284
+ // press feedback + transform in transition list. Same
2285
+ // vocabulary as Ring above.
2286
+ /* Round 522 sibling — Grid button mirrors Ring above:
2287
+ inactive variant gains `hover:font-medium` typography
2288
+ preview + font-weight 150ms in inline transition list.
2289
+ Same idiom, same family (R522 chrome layout). */
2290
+ 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 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' : ''}`}
2291
+ data-topo-chrome-layout-hover-preview-fw={layout === 'grid' ? null : '500'}
1967
2292
  /* Round 268 / Loop: Grid button's left border (the
1968
2293
  internal divider between Ring and Grid) picks up
1969
2294
  pal.containerBorder, matching the wrapper change at
@@ -1973,8 +2298,10 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1973
2298
  transition list into the inline spec below so the
1974
2299
  letter-spacing tween rides alongside without snapping
1975
2300
  the border-color flip — border-color 200ms ease-out
1976
- keeps R268's theme-toggle smoothness intact. */
1977
- style={{ borderColor: pal.containerBorder, transition: 'background-color 150ms ease, color 150ms ease, border-color 200ms ease-out, letter-spacing 200ms ease-out' }}
2301
+ keeps R268's theme-toggle smoothness intact.
2302
+ R492 adds `transform 150ms ease-out` so active:scale-95
2303
+ eases smoothly. */
2304
+ 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' }}
1978
2305
  >
1979
2306
  Grid
1980
2307
  </button>
@@ -2007,6 +2334,41 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2007
2334
  // lists once for both chips to share.
2008
2335
  const workingAliases = onlineNodes.filter(s => s.status === 'working').map(s => s.alias);
2009
2336
  const onlineAliases = onlineNodes.map(s => s.alias);
2337
+ /* Round 565 (50-round milestone) / Loop — extend the
2338
+ inspection-overrides-encoding family to a 7th anchor at
2339
+ the chip-row chip scope. Computes the hovered alias's
2340
+ status tier (same idiom as R562/R563) so each chip's
2341
+ className can include the "lit" bg/border treatment when
2342
+ operator hovers a node matching its tier.
2343
+ Family progression — 7 anchors complete:
2344
+ R484 recent-row timestamp alias hover
2345
+ R485 edge particle opacity alias hover
2346
+ R486 minimap dot opacity alias hover
2347
+ R561 group-label + ants-gate member-alias hover
2348
+ R562 legend-swatch r + glow member-alias status match
2349
+ R563 pressure-seg brightness member-alias status match
2350
+ R565 chip-row chip bg/border member-alias status match ← this round
2351
+ Status-tier-match feedback now SATURATES across panel
2352
+ chrome at 4 surfaces simultaneously:
2353
+ minimap dot (R486)
2354
+ legend swatch (R562)
2355
+ pressure-seg (R563)
2356
+ chip-row chip (R565) ← this round
2357
+ When operator hovers a 'working' node alias, ALL FOUR
2358
+ surfaces light up in green; 'idle' → all four in teal;
2359
+ 'offline' → all four in slate. The eye gets 4-way
2360
+ confirmation of "your inspected node is in this tier"
2361
+ across every persistent status-reference surface. */
2362
+ const hoveredAliasTierKey: 'working' | 'idle' | 'offline' | null = (() => {
2363
+ if (!hoveredAlias) return null;
2364
+ const s = onlineNodes.find(n => n.alias === hoveredAlias)
2365
+ ?? offlineNodes.find(n => n.alias === hoveredAlias);
2366
+ if (!s) return null;
2367
+ if (s.status === 'working') return 'working';
2368
+ return offlineNodes.includes(s) ? 'offline' : 'idle';
2369
+ })();
2370
+ const isWorkingChipLit = hoveredAliasTierKey === 'working';
2371
+ const isOnlineChipLit = hoveredAliasTierKey === 'idle';
2010
2372
  const truncate = (list: string[]) => {
2011
2373
  const head = list.slice(0, 8).join(', ');
2012
2374
  const tail = list.length > 8 ? ` + ${list.length - 8} more` : '';
@@ -2091,13 +2453,27 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2091
2453
  // to R355 filter pin pill inner-span hover-brighten.
2092
2454
  // Hover-brighten family extends from filter pills to
2093
2455
  // chip-row chips at the inner-span scope.
2456
+ // Round 494 / Loop — chip-row working chip joins the
2457
+ // active:scale-95 press-feedback family (R492 Ring/Grid +
2458
+ // R493 chrome-strip rest). Gated on the clickable branch
2459
+ // (workingCount > 0) — when the chip is a placeholder
2460
+ // at count=0, scale-95 stays off to match the existing
2461
+ // R398 hover-lift conditional. Composes with hover:-
2462
+ // translate-y-px for the same lift-and-compress
2463
+ // tactile signature R493 brought to reset/fullscreen.
2464
+ /* R565: when isWorkingChipLit (operator hovers a working
2465
+ node), chip stays in its "lit" bg-green-500/15 +
2466
+ border-green-500/30 state at rest. Same visual as
2467
+ hover; member-alias-matching pins the lift without
2468
+ requiring cursor on the chip. */
2094
2469
  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 ${
2095
2470
  workingCount > 0
2096
- ? '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'
2471
+ ? `${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`
2097
2472
  : 'bg-green-500/10 text-green-300 border-green-500/20'
2098
2473
  }`}
2099
2474
  data-chip-hover-lift={workingCount > 0 ? 'true' : 'false'}
2100
2475
  data-chip-group-hover-brighten="true"
2476
+ data-working-chip-member-alias-lit={isWorkingChipLit ? 'true' : 'false'}
2101
2477
  data-working-chip
2102
2478
  data-working-chip-aliases={workingAliases.join(',')}
2103
2479
  data-pin-mirror={pinnedStatus === 'working' ? 'true' : 'false'}
@@ -2177,7 +2553,26 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2177
2553
  at the chip-count scope. Sibling edits on the
2178
2554
  online + active-links chip digits below. data-
2179
2555
  working-chip-digit attr exposes the digit span. */}
2180
- <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>
2556
+ {/* Round 539 / Loop chip-row digit gains group-hover:
2557
+ tracking-wide alongside the existing R362 group-
2558
+ hover:font-bold. Pre-R539 the chip digit lifted
2559
+ only on the font-weight axis (600 → 700 on chip
2560
+ hover); R539 adds the kerning axis (tracking
2561
+ normal → tracking-wide ≈ 0.025em ≈ 0.3px on a 12px
2562
+ digit) so hover lifts BOTH typography axes
2563
+ together — same idiom R420/R517 establish at the
2564
+ chrome zoom-level (letter-spacing + fontWeight
2565
+ hover delta) and R531/R530 mirror at the panel
2566
+ label scope. transition-[font-weight] extends to
2567
+ transition-[font-weight,letter-spacing] for the
2568
+ smooth dual-axis tween.
2569
+ Sibling treatment across the 3 chip-row digits
2570
+ (working / online / active-links) — single concept
2571
+ replicated at 3 surfaces by replace_all.
2572
+ Hover-letter-spacing family extension (12 anchors
2573
+ now): R344/R345/R347/R420/R427/R431/R432/R433/
2574
+ R434/R517/R518 + R539 (this round). */}
2575
+ <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>
2181
2576
  </span>
2182
2577
  <span
2183
2578
  // Round 201 / Loop: online chip — mirror of the working
@@ -2193,13 +2588,21 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2193
2588
  same digit-jitter physics on count crossings). */
2194
2589
  // R398: hover translate-y lift on clickable variant — see working chip above.
2195
2590
  // R414: `group` parent + inner unit span group-hover-brighten — see working chip above.
2591
+ // R494 sibling — online chip joins the active:scale-95 press
2592
+ // family (gated on onlineNodes.length > 0 clickable branch,
2593
+ // same conditional pattern as the working chip above).
2594
+ /* R565: same lit-on-member-alias-match pattern as
2595
+ working chip — online chip routes hover to 'idle'
2596
+ tier (see onMouseEnter below), so its member-alias
2597
+ gate is `hoveredAliasTierKey === 'idle'`. */
2196
2598
  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 ${
2197
2599
  onlineNodes.length > 0
2198
- ? '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'
2600
+ ? `${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`
2199
2601
  : 'bg-cyan-500/10 text-cyan-300 border-cyan-500/20'
2200
2602
  }`}
2201
2603
  data-chip-hover-lift={onlineNodes.length > 0 ? 'true' : 'false'}
2202
2604
  data-chip-group-hover-brighten="true"
2605
+ data-online-chip-member-alias-lit={isOnlineChipLit ? 'true' : 'false'}
2203
2606
  data-online-chip
2204
2607
  data-online-chip-aliases={onlineAliases.join(',')}
2205
2608
  data-pin-mirror={pinnedStatus === 'idle' ? 'true' : 'false'}
@@ -2245,7 +2648,9 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2245
2648
  >
2246
2649
  {/* R337 sibling — online chip unit demotion. */}
2247
2650
  {/* R362 sibling — online-chip digit gains font-semibold. */}
2248
- <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>
2651
+ {/* R539 sibling online chip digit. Same idiom as
2652
+ working chip above (group-hover:tracking-wide). */}
2653
+ <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>
2249
2654
  </span>
2250
2655
  </>
2251
2656
  );
@@ -2260,6 +2665,31 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2260
2665
  const o = offlineNodes.length;
2261
2666
  const total = w + i + o;
2262
2667
  if (total === 0) return null;
2668
+ /* Round 563 / Loop — inspection-overrides-encoding family
2669
+ 6th anchor at the pressure-bar segment scope. When
2670
+ operator hovers a NODE ALIAS on the canvas, the segment
2671
+ matching that node's status tier lights up with its
2672
+ R210 brightness + R542 drop-shadow treatment — mirror
2673
+ of R562 legend-swatch pattern at the pressure-bar scope.
2674
+ Family progression (6 anchors):
2675
+ R484 recent-row timestamp alias hover
2676
+ R485 edge particle opacity alias hover
2677
+ R486 minimap dot opacity alias hover
2678
+ R561 group-label + ants-gate member-alias hover
2679
+ R562 legend-swatch r + glow member-alias status match
2680
+ R563 pressure-seg brightness member-alias status match ← this round
2681
+ Same status-tier-match computation as R562 (banked
2682
+ idiom): find the hovered alias's session, map to
2683
+ working/idle/offline tier, then per-segment check
2684
+ `hoveredAliasRowKey === key`. Computed once at IIFE
2685
+ scope, used inside the seg() closure. */
2686
+ const hoveredSession = hoveredAlias
2687
+ ? (onlineNodes.find(s => s.alias === hoveredAlias) ?? offlineNodes.find(s => s.alias === hoveredAlias))
2688
+ : null;
2689
+ const hoveredAliasTierKey: 'working' | 'idle' | 'offline' | null = !hoveredSession ? null
2690
+ : hoveredSession.status === 'working' ? 'working'
2691
+ : offlineNodes.includes(hoveredSession) ? 'offline'
2692
+ : 'idle';
2263
2693
  // Round 60 / Loop: each segment toggles a sticky filter via
2264
2694
  // `pinnedStatus`. Click the working segment → all non-working
2265
2695
  // nodes dim; click again → release. Segments share width with
@@ -2272,6 +2702,10 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2272
2702
  const seg = (n: number, color: string, key: 'working' | 'idle' | 'offline', label: string) => {
2273
2703
  if (n === 0) return null;
2274
2704
  const isPinned = pinnedStatus === key;
2705
+ // R563: member-alias-matching flag — when operator hovers
2706
+ // a node alias whose status matches this segment's tier.
2707
+ const isMemberAliasMatching = hoveredAliasTierKey === key;
2708
+ const isSegLit = hoveredStatus === key || isMemberAliasMatching;
2275
2709
  // R102: list the aliases that match this segment's bucket
2276
2710
  // so the title answers WHICH n, not just HOW MANY. Closes
2277
2711
  // the last "info-density gap" in the chip-row surfaces
@@ -2291,6 +2725,8 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2291
2725
  data-pressure-seg={key}
2292
2726
  data-pressure-seg-aliases={matchAliases.join(',')}
2293
2727
  data-pressure-seg-hovered={hoveredStatus === key ? 'true' : 'false'}
2728
+ data-pressure-seg-member-alias-matching={isMemberAliasMatching ? 'true' : 'false'}
2729
+ data-pressure-seg-lit={isSegLit ? 'true' : 'false'}
2294
2730
  role="button"
2295
2731
  tabIndex={0}
2296
2732
  aria-pressed={isPinned}
@@ -2334,7 +2770,47 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2334
2770
  height: '100%',
2335
2771
  cursor: 'pointer',
2336
2772
  boxShadow: isPinned ? `inset 0 0 0 1px ${color}, inset 0 0 0 2px rgba(255,255,255,0.6)` : undefined,
2337
- filter: hoveredStatus === key ? 'brightness(1.2)' : undefined,
2773
+ /* Round 542 / Loop pressure-bar segments gain
2774
+ drop-shadow tier-color glow on hover, stacked
2775
+ on R210 brightness(1.2). Sibling to R537 legend
2776
+ swatch + R541 vendor chip glow at the chip-row
2777
+ scope — three same-pattern surfaces (legend
2778
+ swatch / vendor chip / pressure segment) all
2779
+ radiate their identity color on hover.
2780
+ 3rd anchor in the chip-row tier-color paint
2781
+ glow sub-family:
2782
+ R537 legend swatch row.fill (status hex)
2783
+ R541 vendor chip v.color (hsl via color-mix)
2784
+ R542 pressure seg color (status hex) ← this round
2785
+ Stacked filter syntax (brightness + drop-shadow
2786
+ in same filter declaration): `brightness(1.2)
2787
+ drop-shadow(...)`. CSS filter supports multiple
2788
+ functions; they apply left-to-right. Brightness
2789
+ boosts the segment's own color, drop-shadow
2790
+ paints the outer halo. Together: hovered seg
2791
+ looks "lit up" with both inner glow + outer
2792
+ halo in its tier color.
2793
+ Hue: `${color}99` hex+alpha (60%) — color here
2794
+ is a 6-char hex (e.g., '#22c55e' for working
2795
+ cyber, '#0d9488' for idle light), not hsl, so
2796
+ hex+alpha concat works (unlike R541 vendor
2797
+ which needed color-mix for hsl). Banked
2798
+ pattern: hex sources use hex+alpha; hsl/color()
2799
+ sources use color-mix.
2800
+ 2px blur (vs R537's 3px) since pressure-seg is
2801
+ small (h-2 = 8px tall, variable width) — a
2802
+ smaller blur keeps the glow tight to the
2803
+ segment without bleeding into neighbors.
2804
+ filter is paint-only; bbox unchanged; R51
2805
+ overlap-test invariants hold. Transition list
2806
+ already includes `filter` (post-R524). */
2807
+ /* R563: filter lifts on EITHER direct hover OR member-
2808
+ alias-matching (operator inspecting a node whose
2809
+ status matches this segment's tier). Same R210
2810
+ brightness + R542 drop-shadow value across both
2811
+ gate sources — uniform visual response, distinct
2812
+ semantic gates. */
2813
+ filter: isSegLit ? `brightness(1.2) drop-shadow(0 0 2px ${color}99)` : undefined,
2338
2814
  transition: 'width 220ms ease-out, box-shadow 150ms ease-out, filter 150ms ease-out',
2339
2815
  }}
2340
2816
  onClick={(e) => {
@@ -2470,9 +2946,42 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2470
2946
  // R355: `group` lets the inner opacity-70 spans (prefix
2471
2947
  // `filter:` + count `· N`) brighten to 100 % on pill hover.
2472
2948
  // Sibling treatment on group + vendor pills below.
2473
- className="group inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md font-mono font-medium text-xs border anet-fade-in anet-topo-chip-focus transition-transform duration-200 ease-out hover:-translate-y-px transform-gpu" data-topo-filter-pill-hover-lift="true"
2949
+ // R495 filter pills (3 sibling `group` variants) join the
2950
+ // active:scale-95 press-feedback family. R490's !important
2951
+ // transition list on .anet-topo-chip-focus already covers
2952
+ // transform, so just appending active:scale-95 to the
2953
+ // className wires the press tactile in one token. Compound
2954
+ // with R400-era hover:-translate-y-px gives lift-and-compress.
2955
+ 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"
2474
2956
  title={matchCount > 0 ? `${matchPreview}${matchSuffix} — click to clear` : 'Click to clear filter'}
2475
2957
  onClick={() => setPinnedStatus(null)}
2958
+ /* Round 543 / Loop — status filter pill gains always-on
2959
+ tier-color drop-shadow when rendered. Pre-R543 the
2960
+ pill carried bg-tint + tier-color text + border but
2961
+ no outer paint extent — it sat as a flat tinted chip
2962
+ in the chip row. R543 adds an outer glow at the
2963
+ pill's text color so the pill radiates a soft tier-
2964
+ colored halo signaling "this filter is active." Pin
2965
+ pill only renders when pinnedStatus is set (the JSX
2966
+ gate above), so the drop-shadow appearing reinforces
2967
+ the visual "active pin" state.
2968
+ Sibling pattern: R477 legend pin-ring also paints a
2969
+ pin-gated tier-color drop-shadow. Pin pill follows
2970
+ the same "pin-gated paint glow" semantics but at the
2971
+ chip-row scope vs the panel-row scope. The chip-row
2972
+ tier-color glow trio (R537/R541/R542 hover-gated)
2973
+ plus R543 (pin-gated, this round) closes the chip-
2974
+ row paint-glow family across BOTH gate types
2975
+ (hover for transient affordance, pin for sticky
2976
+ active-state visual).
2977
+ Hue: explicit tier color (extracted from the
2978
+ existing `color` ternary). 0x99 alpha (~60%) +
2979
+ 3px blur. Stays inside the same color hierarchy
2980
+ as the pill's own text/border (currentColor).
2981
+ R543 status pill scope only — R543's pattern can
2982
+ future-extend to group/vendor/edge filter pills
2983
+ (3 more variants at lines 2683/2755/2824). Out of
2984
+ scope to keep R543 single-pill. */
2476
2985
  style={{
2477
2986
  background: pinnedStatus === 'working' ? (isLight ? '#05966914' : '#22c55e1f')
2478
2987
  : pinnedStatus === 'idle' ? (isLight ? '#0d948814' : '#2dd4bf1f')
@@ -2482,6 +2991,11 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2482
2991
  : (isLight ? '#475569' : '#9ca3af'),
2483
2992
  borderColor: 'currentColor',
2484
2993
  cursor: 'pointer',
2994
+ filter: `drop-shadow(0 0 3px ${
2995
+ pinnedStatus === 'working' ? (isLight ? '#047857' : '#86efac')
2996
+ : pinnedStatus === 'idle' ? (isLight ? '#0f766e' : '#5eead4')
2997
+ : (isLight ? '#475569' : '#9ca3af')
2998
+ }99)`,
2485
2999
  }}
2486
3000
  >
2487
3001
  {/* Round 412 / Loop: filter pin pill VALUE picks up the
@@ -2495,7 +3009,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2495
3009
  to R333/R335-R341/R362/R369/R389/R410. data-filter-
2496
3010
  value attr surfaces the value span for tests.
2497
3011
  4-pill replace family — status / group / vendor / edge. */}
2498
- <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>
3012
+ <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>
2499
3013
  <button
2500
3014
  type="button"
2501
3015
  aria-label={`Clear ${pinnedStatus} filter`}
@@ -2514,7 +3028,24 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2514
3028
  inline-block is default for <button> so no display
2515
3029
  tweak needed. replace_all covers all 4 filter pin
2516
3030
  pills (status / group / vendor / edge) at once. */
2517
- className="ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 transform-gpu"
3031
+ /* Round 547 / Loop — extends pill × close-button hover
3032
+ gesture from scale-110 (R356) + opacity-70 to ALSO
3033
+ include rotate-12 on hover. Pre-R547 the × dimmed
3034
+ and grew on hover; R547 adds a 12° twist so the
3035
+ close action telegraphs "discarding/spinning away"
3036
+ with a small delight gesture. Composes with
3037
+ transition-transform (existing) — Tailwind's
3038
+ hover:rotate-12 + hover:scale-110 stack into one
3039
+ transform under the same 200ms ease-out tween.
3040
+ Applied to all 4 pill × buttons (status / group /
3041
+ vendor / edge) via replace_all since the className
3042
+ is identical. Closes the pill × hover gesture
3043
+ vocabulary at 3 axes:
3044
+ hover:opacity-70 paint dim
3045
+ hover:scale-110 geometry grow (R356)
3046
+ hover:rotate-12 geometry twist (R547, this round)
3047
+ Hover-gesture parity across the 4-pill family. */
3048
+ className="ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 hover:rotate-12 transform-gpu"
2518
3049
  style={{ background: 'transparent', color: 'inherit', cursor: 'pointer', padding: 0 }}
2519
3050
  >×</button>
2520
3051
  </span>
@@ -2534,18 +3065,51 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2534
3065
  data-filter-match-count={matchCount}
2535
3066
  data-filter-match-aliases={matchAliases.join(',')}
2536
3067
  // R355 sibling — `group` parent + group-hover on inner spans.
2537
- className="group inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md font-mono font-medium text-xs border anet-fade-in anet-topo-chip-focus transition-transform duration-200 ease-out hover:-translate-y-px transform-gpu" data-topo-filter-pill-hover-lift="true"
3068
+ // R495 filter pills (3 sibling `group` variants) join the
3069
+ // active:scale-95 press-feedback family. R490's !important
3070
+ // transition list on .anet-topo-chip-focus already covers
3071
+ // transform, so just appending active:scale-95 to the
3072
+ // className wires the press tactile in one token. Compound
3073
+ // with R400-era hover:-translate-y-px gives lift-and-compress.
3074
+ 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"
2538
3075
  title={matchCount > 0 ? `${matchPreview}${matchSuffix} — click to clear` : 'Click to clear filter'}
2539
3076
  onClick={() => setPinnedGroup(null)}
3077
+ /* Round 544 / Loop — extends R543 pin-active filter-pill
3078
+ drop-shadow pattern to the GROUP pill (2nd of 4 pill
3079
+ variants). Pre-R544 the group pill carried bg-tint +
3080
+ pal.legendAccent text/border but no outer paint glow.
3081
+ R544 adds the matching cyan-accent drop-shadow so the
3082
+ group pin pill radiates the same paint glow as R543
3083
+ status pill — pin-active visual signal at chip-row
3084
+ scope.
3085
+ Hue: pal.legendAccent (cyber #67e8f9 cyan-300 /
3086
+ light #0d9488 teal-600). Uses color-mix() syntax
3087
+ because pal.legendAccent may resolve to hex; same
3088
+ syntax works for both hex and hsl sources (banked
3089
+ R541 lesson). 60% alpha + 3px blur — same intensity
3090
+ as R543 status pill so the pin-active visual signal
3091
+ reads with matching brightness across pill variants.
3092
+ Pin-active tier-color paint glow sub-family
3093
+ (CLOSED progressively):
3094
+ R477 legend pin-ring (panel-row, row.fill)
3095
+ R543 status pill (chip-row, tier-color)
3096
+ R544 group pill (chip-row, legendAccent)
3097
+ ← this round
3098
+ Out of scope: vendor pill (line ~2755) + edge pill
3099
+ (line ~2824) — can future-extend in subsequent
3100
+ rounds (R545/R546). Both use the same R543 idiom:
3101
+ always-on drop-shadow when rendered, color from the
3102
+ pill's existing text color. */
2540
3103
  style={{
2541
3104
  background: isLight ? '#67e8f914' : '#67e8f91f',
2542
3105
  color: pal.legendAccent,
2543
3106
  borderColor: 'currentColor',
2544
3107
  cursor: 'pointer',
3108
+ filter: `drop-shadow(0 0 3px color-mix(in srgb, ${pal.legendAccent} 60%, transparent))`,
2545
3109
  }}
2546
3110
  >
2547
3111
  {/* R412: see status pill above — filter value fw=600 data tier. */}
2548
- <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>
3112
+ <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>
2549
3113
  <button
2550
3114
  type="button"
2551
3115
  aria-label={`Clear group filter ${pinnedGroup}`}
@@ -2564,7 +3128,24 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2564
3128
  inline-block is default for <button> so no display
2565
3129
  tweak needed. replace_all covers all 4 filter pin
2566
3130
  pills (status / group / vendor / edge) at once. */
2567
- className="ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 transform-gpu"
3131
+ /* Round 547 / Loop — extends pill × close-button hover
3132
+ gesture from scale-110 (R356) + opacity-70 to ALSO
3133
+ include rotate-12 on hover. Pre-R547 the × dimmed
3134
+ and grew on hover; R547 adds a 12° twist so the
3135
+ close action telegraphs "discarding/spinning away"
3136
+ with a small delight gesture. Composes with
3137
+ transition-transform (existing) — Tailwind's
3138
+ hover:rotate-12 + hover:scale-110 stack into one
3139
+ transform under the same 200ms ease-out tween.
3140
+ Applied to all 4 pill × buttons (status / group /
3141
+ vendor / edge) via replace_all since the className
3142
+ is identical. Closes the pill × hover gesture
3143
+ vocabulary at 3 axes:
3144
+ hover:opacity-70 paint dim
3145
+ hover:scale-110 geometry grow (R356)
3146
+ hover:rotate-12 geometry twist (R547, this round)
3147
+ Hover-gesture parity across the 4-pill family. */
3148
+ className="ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 hover:rotate-12 transform-gpu"
2568
3149
  style={{ background: 'transparent', color: 'inherit', cursor: 'pointer', padding: 0 }}
2569
3150
  >×</button>
2570
3151
  </span>
@@ -2600,18 +3181,44 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2600
3181
  data-filter-match-count={matchCount}
2601
3182
  data-filter-match-aliases={matchAliases.join(',')}
2602
3183
  // R355 sibling — `group` parent + group-hover on inner spans.
2603
- className="group inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md font-mono font-medium text-xs border anet-fade-in anet-topo-chip-focus transition-transform duration-200 ease-out hover:-translate-y-px transform-gpu" data-topo-filter-pill-hover-lift="true"
3184
+ // R495 filter pills (3 sibling `group` variants) join the
3185
+ // active:scale-95 press-feedback family. R490's !important
3186
+ // transition list on .anet-topo-chip-focus already covers
3187
+ // transform, so just appending active:scale-95 to the
3188
+ // className wires the press tactile in one token. Compound
3189
+ // with R400-era hover:-translate-y-px gives lift-and-compress.
3190
+ 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"
2604
3191
  title={matchCount > 0 ? `${matchPreview}${matchSuffix} — click to clear` : 'Click to clear vendor filter'}
2605
3192
  onClick={() => setPinnedVendor(null)}
3193
+ /* Round 545 / Loop — extends pin-active filter-pill drop-
3194
+ shadow pattern to VENDOR pill (3rd of 4 pill variants
3195
+ after R543 status + R544 group). vendorColor is HSL
3196
+ format (banked R541 lesson — vendorDist.color sources
3197
+ from mono.text in vendorIdentity.ts, which is `hsl(...)`),
3198
+ so the filter uses color-mix() syntax — same as R544.
3199
+ 60% alpha + 3px blur, matching R543/R544 intensity for
3200
+ consistent pin-active visual signal across all pill
3201
+ variants.
3202
+ Pin-active tier-color paint glow sub-family (progressive
3203
+ extension, 1 pill variant remaining):
3204
+ R477 legend pin-ring (panel-row, row.fill, hex+alpha)
3205
+ R543 status pill (chip-row, tier-color, hex+alpha)
3206
+ R544 group pill (chip-row, legendAccent, color-mix)
3207
+ R545 vendor pill (chip-row, vendorColor, color-mix)
3208
+ ← this round
3209
+ Out of scope: edge pill (line ~2824 pre-R545, now ~2900+).
3210
+ Final 1/4 pill remaining for a future round closes the
3211
+ sub-family. */
2606
3212
  style={{
2607
3213
  background: `${vendorColor}1f`,
2608
3214
  color: vendorColor,
2609
3215
  borderColor: 'currentColor',
2610
3216
  cursor: 'pointer',
3217
+ filter: `drop-shadow(0 0 3px color-mix(in srgb, ${vendorColor} 60%, transparent))`,
2611
3218
  }}
2612
3219
  >
2613
3220
  {/* R412: see status pill above — filter value fw=600 data tier. */}
2614
- <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>
3221
+ <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>
2615
3222
  <button
2616
3223
  type="button"
2617
3224
  aria-label={`Clear vendor filter ${pinnedVendor}`}
@@ -2630,7 +3237,24 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2630
3237
  inline-block is default for <button> so no display
2631
3238
  tweak needed. replace_all covers all 4 filter pin
2632
3239
  pills (status / group / vendor / edge) at once. */
2633
- className="ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 transform-gpu"
3240
+ /* Round 547 / Loop — extends pill × close-button hover
3241
+ gesture from scale-110 (R356) + opacity-70 to ALSO
3242
+ include rotate-12 on hover. Pre-R547 the × dimmed
3243
+ and grew on hover; R547 adds a 12° twist so the
3244
+ close action telegraphs "discarding/spinning away"
3245
+ with a small delight gesture. Composes with
3246
+ transition-transform (existing) — Tailwind's
3247
+ hover:rotate-12 + hover:scale-110 stack into one
3248
+ transform under the same 200ms ease-out tween.
3249
+ Applied to all 4 pill × buttons (status / group /
3250
+ vendor / edge) via replace_all since the className
3251
+ is identical. Closes the pill × hover gesture
3252
+ vocabulary at 3 axes:
3253
+ hover:opacity-70 paint dim
3254
+ hover:scale-110 geometry grow (R356)
3255
+ hover:rotate-12 geometry twist (R547, this round)
3256
+ Hover-gesture parity across the 4-pill family. */
3257
+ className="ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 hover:rotate-12 transform-gpu"
2634
3258
  style={{ background: 'transparent', color: 'inherit', cursor: 'pointer', padding: 0 }}
2635
3259
  >×</button>
2636
3260
  </span>
@@ -2663,14 +3287,40 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2663
3287
  data-filter-match-count={link.count}
2664
3288
  data-filter-match-aliases={`${link.from},${link.to}`}
2665
3289
  data-active-filter-edge-hot={isHot ? 'true' : 'false'}
2666
- className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md font-mono font-medium text-xs border anet-fade-in anet-topo-chip-focus transition-transform duration-200 ease-out hover:-translate-y-px transform-gpu" data-topo-filter-pill-hover-lift="true"
3290
+ // R495 sibling 4th filter pill (no `group` prefix variant)
3291
+ // joins active:scale-95 press family alongside the 3 group
3292
+ // variants above. Same recipe.
3293
+ 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"
2667
3294
  title={`${link.from} → ${link.to} (${link.count} msg${link.count === 1 ? '' : 's'}${isHot ? ', hot lane · ≥ 10' : ''}) — click to clear`}
2668
3295
  onClick={() => setPinnedEdgeKey(null)}
3296
+ /* Round 546 / Loop — CLOSES pin-active filter-pill drop-
3297
+ shadow sub-family at the 4th and final pill variant
3298
+ (edge pill). R543 (status) + R544 (group) + R545
3299
+ (vendor) covered the first three; R546 closes at
3300
+ edge.
3301
+ Pin-active tier-color paint glow sub-family CLOSED
3302
+ (4 anchors):
3303
+ R477 legend pin-ring (panel-row, row.fill)
3304
+ R543 status pill (chip-row, tier-color text)
3305
+ R544 group pill (chip-row, legendAccent)
3306
+ R545 vendor pill (chip-row, vendorColor)
3307
+ R546 edge pill (chip-row, pal.flowEdge)
3308
+ ← this round, family CLOSED
3309
+ All 4 filter pin pills now radiate paint glow in the
3310
+ same hue family as their text/border on render —
3311
+ pin-active visual signal uniform across the chip-row
3312
+ pill family.
3313
+ pal.flowEdge is theme-driven (dynamic); color-mix
3314
+ syntax safe-defaults regardless of resolved format
3315
+ (banked R541/R544/R545 pattern). 60% alpha + 3px blur
3316
+ — same intensity as R543/R544/R545 for consistent
3317
+ cross-pill visual signal. */
2669
3318
  style={{
2670
3319
  background: isLight ? `${pal.flowEdge}14` : `${pal.flowEdge}1f`,
2671
3320
  color: pal.flowEdge,
2672
3321
  borderColor: 'currentColor',
2673
3322
  cursor: 'pointer',
3323
+ filter: `drop-shadow(0 0 3px color-mix(in srgb, ${pal.flowEdge} 60%, transparent))`,
2674
3324
  }}
2675
3325
  >
2676
3326
  {/* R412: filter pin pill value (edge variant) picks up fw=600.
@@ -2723,7 +3373,24 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2723
3373
  inline-block is default for <button> so no display
2724
3374
  tweak needed. replace_all covers all 4 filter pin
2725
3375
  pills (status / group / vendor / edge) at once. */
2726
- className="ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 transform-gpu"
3376
+ /* Round 547 / Loop — extends pill × close-button hover
3377
+ gesture from scale-110 (R356) + opacity-70 to ALSO
3378
+ include rotate-12 on hover. Pre-R547 the × dimmed
3379
+ and grew on hover; R547 adds a 12° twist so the
3380
+ close action telegraphs "discarding/spinning away"
3381
+ with a small delight gesture. Composes with
3382
+ transition-transform (existing) — Tailwind's
3383
+ hover:rotate-12 + hover:scale-110 stack into one
3384
+ transform under the same 200ms ease-out tween.
3385
+ Applied to all 4 pill × buttons (status / group /
3386
+ vendor / edge) via replace_all since the className
3387
+ is identical. Closes the pill × hover gesture
3388
+ vocabulary at 3 axes:
3389
+ hover:opacity-70 paint dim
3390
+ hover:scale-110 geometry grow (R356)
3391
+ hover:rotate-12 geometry twist (R547, this round)
3392
+ Hover-gesture parity across the 4-pill family. */
3393
+ className="ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 hover:rotate-12 transform-gpu"
2727
3394
  style={{ background: 'transparent', color: 'inherit', cursor: 'pointer', padding: 0 }}
2728
3395
  >×</button>
2729
3396
  </span>
@@ -3013,7 +3680,12 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3013
3680
  // — sibling to R355 filter-pill prefix/suffix + R414
3014
3681
  // chip-row unit brighten. Closes the inner-span
3015
3682
  // hover-brighten family at the vendor chip surface.
3016
- className="group tabular-nums font-medium inline-flex items-baseline gap-0.5 px-1 rounded anet-topo-chip-focus transition-transform duration-200 ease-out transform-gpu hover:-translate-y-px"
3683
+ // R496 vendor letter chip joins active:scale-95 press
3684
+ // family. Last vendor-row clickable joining the family
3685
+ // R495 cashed via R490's transition-cascade dividend.
3686
+ // Same compound w/ R401 hover-lift idiom — lift-and-
3687
+ // compress on press, springs back on release.
3688
+ className="group tabular-nums font-medium inline-flex items-baseline gap-0.5 px-1 rounded anet-topo-chip-focus transition-transform duration-200 ease-out transform-gpu hover:-translate-y-px active:scale-95"
3017
3689
  data-vendor-letter={v.initial}
3018
3690
  data-vendor-letter-count={v.count}
3019
3691
  data-vendor-letter-hover-lift="true"
@@ -3040,6 +3712,41 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3040
3712
  // for older browsers the chip falls back to its idle
3041
3713
  // transparent bg (graceful degradation — the canvas-
3042
3714
  // dim effect still fires regardless).
3715
+ /* Round 541 / Loop — vendor letter chip gains drop-
3716
+ shadow glow on hover/pin using its OWN vendor
3717
+ identity color (v.color). Sibling to R537 legend-
3718
+ swatch tier-color glow at the chip-row scope.
3719
+ Pre-R541 the vendor chip lifted on multiple
3720
+ axes (R354 inner-glyph scale-1.1, R202 bg color-
3721
+ mix tint, R180 box-shadow pin-mirror inset, R401
3722
+ hover-translate-y -1px, R496 active:scale-95
3723
+ press) but no paint-axis glow extending past
3724
+ the chip's bbox. R541 adds the outer glow at
3725
+ the paint axis so the vendor chip's identity
3726
+ color radiates beyond the chip on attention —
3727
+ same idiom as legend swatch tier-color glow.
3728
+ 2-tier alpha ladder (mirrors R538 group-label):
3729
+ pin (committed) v.color 99 (~60%)
3730
+ hover (preview) v.color 66 (~40%)
3731
+ rest none
3732
+ Pin is brighter to distinguish locked vs preview
3733
+ at the paint axis. The R180 inset box-shadow
3734
+ (pin-mirror) and R541 outer drop-shadow compose
3735
+ at pin — inside chrome reads as "this is pinned"
3736
+ (inset white double-ring), outside paint reads
3737
+ as "vendor identity is locked" (vendor-colour
3738
+ outer glow). Hover gets only the outer glow.
3739
+ 3px blur tuned to read as soft chip-halo without
3740
+ overwhelming adjacent chips in the chip row.
3741
+ filter property is in the .anet-topo-chip-focus
3742
+ class transition list (R524 banked fix), so the
3743
+ filter eases at 200ms naturally.
3744
+ Drop-shadow visual-polish family — R541 adds
3745
+ chip-row tier-color paint glow as another anchor
3746
+ in the same family pattern R537 established.
3747
+ data-vendor-glow attr ('pin' | 'hover' | 'false')
3748
+ exposes the gate state for tests. */
3749
+ data-vendor-glow={isPinned ? 'pin' : hoveredVendor === v.initial ? 'hover' : 'false'}
3043
3750
  style={{
3044
3751
  cursor: 'pointer',
3045
3752
  backgroundColor: (hoveredVendor === v.initial && !isPinned)
@@ -3048,7 +3755,15 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3048
3755
  boxShadow: isPinned
3049
3756
  ? `inset 0 0 0 1px ${v.color}, inset 0 0 0 2px rgba(255,255,255,0.45)`
3050
3757
  : undefined,
3051
- transition: 'box-shadow 150ms ease-out, background-color 200ms ease-out',
3758
+ /* R578 sibling vendor chip stacks brightness(1.15)
3759
+ onto R541 drop-shadow. Closes chip-row tier-color
3760
+ glow trio at consistent stacked-filter pattern. */
3761
+ filter: isPinned
3762
+ ? `drop-shadow(0 0 3px color-mix(in srgb, ${v.color} 60%, transparent)) brightness(1.15)`
3763
+ : hoveredVendor === v.initial
3764
+ ? `drop-shadow(0 0 3px color-mix(in srgb, ${v.color} 40%, transparent)) brightness(1.15)`
3765
+ : undefined,
3766
+ transition: 'box-shadow 150ms ease-out, background-color 200ms ease-out, filter 200ms ease-out',
3052
3767
  }}
3053
3768
  onMouseEnter={() => setHoveredVendor(v.initial)}
3054
3769
  onMouseLeave={() => setHoveredVendor(prev => prev === v.initial ? null : prev)}
@@ -3140,7 +3855,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3140
3855
  since the glyph (R369 fw=600) stays at full
3141
3856
  opacity. R333 :{count} format preserved. */}
3142
3857
  <span
3143
- className="text-gray-400 tabular-nums opacity-70 transition-opacity duration-200 group-hover:opacity-100"
3858
+ className="text-gray-400 tabular-nums opacity-70 transition-[opacity,letter-spacing] duration-200 group-hover:opacity-100 group-hover:tracking-wide"
3144
3859
  data-vendor-letter-count-suffix
3145
3860
  >:{v.count}</span>
3146
3861
  </span>
@@ -3251,9 +3966,13 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3251
3966
  data-chip-hover-lift attr exposes the lift surface
3252
3967
  state ('true' clickable, 'false' empty) for tests. */
3253
3968
  // R414: `group` parent + inner unit span group-hover-brighten — see working chip above.
3969
+ // R496 — active-links chip joins active:scale-95 press
3970
+ // family. Sibling to working+online chips (R494). Gated
3971
+ // on `isInteractive` (flowLinks.length > 0) — same R399
3972
+ // conditional pattern used for hover-lift.
3254
3973
  className={`group tabular-nums font-medium hidden sm:inline px-2.5 py-1 rounded-md border anet-topo-chip-focus transition-transform duration-200 ease-out transform-gpu ${
3255
3974
  isInteractive
3256
- ? 'bg-gray-500/10 text-gray-400 border-gray-500/20 hover:bg-cyan-500/10 hover:text-cyan-200 hover:border-cyan-500/30 hover:-translate-y-px'
3975
+ ? 'bg-gray-500/10 text-gray-400 border-gray-500/20 hover:bg-cyan-500/10 hover:text-cyan-200 hover:border-cyan-500/30 hover:-translate-y-px active:scale-95'
3257
3976
  : 'bg-gray-500/10 text-gray-400 border-gray-500/20'
3258
3977
  }`}
3259
3978
  data-chip-hover-lift={isInteractive ? 'true' : 'false'}
@@ -3286,7 +4005,9 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3286
4005
  chip-internal-hierarchy arc. data-active-links-
3287
4006
  chip-unit exposes the unit span for tests. */}
3288
4007
  {/* R362 sibling — active-links chip digit gains font-semibold. */}
3289
- <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>
4008
+ {/* R539 sibling active-links chip digit. Same idiom
4009
+ as working + online above. */}
4010
+ <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>
3290
4011
  {rel ? (() => {
3291
4012
  // Round 161 / Loop: extend R160's recency-pip
3292
4013
  // vocabulary up one scope — from per-flow row to
@@ -3500,6 +4221,36 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3500
4221
  on the canvas root for non-visual consumers.
3501
4222
  Composed from existing onlineNodes / workingCount /
3502
4223
  offlineNodes / flowLinks — no new state. */
4224
+ /* Round 502 / Loop — categorical density-tier paired with the
4225
+ R469 numeric counts. data-topo-fleet-density-tier classifies
4226
+ the fleet size into 5 buckets so external consumers (CSS
4227
+ selectors, Playwright probes, future density-conditional
4228
+ polish gates like R109 dense-label collapse at 16+ nodes)
4229
+ can branch on a stable tier name without re-deriving the
4230
+ threshold logic from the raw numeric. Buckets:
4231
+ 'empty' — onlineNodes.length === 0
4232
+ 'sparse' — 1-3 nodes
4233
+ 'normal' — 4-15 nodes
4234
+ 'dense' — 16-30 nodes (matches R109 collapse gate)
4235
+ 'very-dense' — 31+ nodes
4236
+ Picks the gate boundaries that already drive CONDITIONAL
4237
+ RENDER decisions elsewhere (R109 denseLayout = >16, R110
4238
+ plain-text fallback) so the tier name is semantically
4239
+ aligned with the visual mode the canvas already switches
4240
+ to. Composed from existing onlineNodes — no new state.
4241
+ 12th attr in the canvas state surface set (R462/R466/R467/
4242
+ R469×4/R471×2/R487/R488/R502). 12 attrs covers: build
4243
+ identity, transient/sticky inspection modes, fleet split
4244
+ numerics, fleet density tier, canvas layout/theme, canvas
4245
+ zoom, hover identity. A test harness can snapshot the
4246
+ full canvas state with 12 getAttribute calls. */
4247
+ data-topo-fleet-density-tier={
4248
+ onlineNodes.length === 0 ? 'empty' :
4249
+ onlineNodes.length <= 3 ? 'sparse' :
4250
+ onlineNodes.length <= 15 ? 'normal' :
4251
+ onlineNodes.length <= 30 ? 'dense' :
4252
+ 'very-dense'
4253
+ }
3503
4254
  data-topo-online-count={onlineNodes.length}
3504
4255
  data-topo-working-count={workingCount}
3505
4256
  data-topo-offline-count={offlineNodes.length}
@@ -3565,6 +4316,119 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3565
4316
  categorical) — separate dedicated attrs if/when needed.
3566
4317
  Root svg attribute set now 11 attrs total. */
3567
4318
  data-topo-hovered-alias={hoveredAlias ?? ''}
4319
+ /* Round 504 / Loop — categorical pin-aspect attr paired with
4320
+ R467 any-pinned boolean and R488 hovered-alias identity.
4321
+ Pre-R504 the canvas state surface set told tests WHETHER
4322
+ any pin was active (R467 boolean) but tests had to enumerate
4323
+ 4 individual state vars to determine WHICH pin axis fired:
4324
+ pinnedStatus legend-row status filter
4325
+ pinnedGroup prefix-cluster lock
4326
+ pinnedVendor vendor-chip filter
4327
+ pinnedEdgeKey edge-focus
4328
+ R504 surfaces the active aspect as a single categorical
4329
+ attribute: data-topo-pinned-aspect ∈
4330
+ 'none' no pin active
4331
+ 'status' pinnedStatus only
4332
+ 'group' pinnedGroup only
4333
+ 'vendor' pinnedVendor only
4334
+ 'edge' pinnedEdgeKey only
4335
+ 'multi' 2 or more pins active simultaneously
4336
+ ('multi' covers cross-cutting filters — e.g. user pins
4337
+ status='working' AND vendor='claude' simultaneously to
4338
+ narrow the canvas. Each pin axis is independently
4339
+ dismissable via Esc / individual chip click, so multi
4340
+ states are reachable and worth surfacing as a distinct
4341
+ tier.)
4342
+ 13th attr in the canvas state surface set after R502.
4343
+ Composed from 4 existing state vars — no new state. */
4344
+ /* Round 512 / Loop — 14th canvas state attr. groupBoxes.length
4345
+ surfaces the count of cluster boxes currently rendered in
4346
+ grid layout (always 0 in ring). Paired with R502 categorical
4347
+ density tier + R469 fleet numerics for a complete cluster-
4348
+ cardinality surface:
4349
+ R469 data-topo-online-count node-count
4350
+ R502 data-topo-fleet-density-tier categorical
4351
+ R512 data-topo-cluster-count cluster-count ← this round
4352
+ Use cases:
4353
+ - Playwright: assert orphan-band existence by
4354
+ `cluster-count === N + 1` vs prefix-only `=== N`
4355
+ - external CSS: `[data-topo-cluster-count='1']` to apply
4356
+ single-cluster grid-specific layout adjustments
4357
+ - future polish gates: cluster-count > N could trigger
4358
+ dense-grid mode
4359
+ Composed from existing `groupBoxes.length` — no new state.
4360
+ Always renders (0 in ring layout, N in grid), so tests can
4361
+ rely on attribute presence + value. */
4362
+ data-topo-cluster-count={groupBoxes.length}
4363
+ /* Round 513 / Loop — 15th canvas state attr. Surfaces the
4364
+ user's prefers-reduced-motion preference directly on the
4365
+ root SVG so external CSS / Playwright tests can branch on
4366
+ a11y state without re-reading the media query.
4367
+ reducedMotion is already in component scope (R29 a11y
4368
+ blanket reads it via a useEffect listener); R513 just
4369
+ exposes it as a stable attribute handle.
4370
+ Use cases:
4371
+ - Playwright: assert reduced-motion gates from one attr
4372
+ read instead of mocking media-query state per test
4373
+ - External CSS hooks: `[data-topo-prefers-reduced-motion=
4374
+ "true"]` to apply paint-only overrides (e.g. mute
4375
+ hover glows entirely on a11y instead of just
4376
+ disabling transitions)
4377
+ - Future polish rounds: any motion-gated render can
4378
+ read this attr server-side without the media-query
4379
+ hydration mismatch risk
4380
+ 'true' / 'false' string values (consistent with R466/R467
4381
+ boolean attrs). */
4382
+ data-topo-prefers-reduced-motion={reducedMotion ? 'true' : 'false'}
4383
+ /* Round 515 / Loop — 16th canvas state attr. Surfaces the
4384
+ fullscreen-mode state directly on root SVG so external
4385
+ consumers don't have to traverse the chrome strip's
4386
+ `data-topo-chrome-fullscreen-active` button attr (which
4387
+ measures the BUTTON state, not the canvas state — they
4388
+ agree, but reading from the root is semantically cleaner
4389
+ for canvas-state probes).
4390
+ Composed from existing isFullscreen React state (R103
4391
+ fullscreen toggle).
4392
+ Use cases:
4393
+ - Playwright: assert canvas mode in one attr read
4394
+ (paired with R471 data-topo-layout for ring/grid +
4395
+ R487 data-topo-zoom for zoom level + R513 reduced-
4396
+ motion for a11y mode = 4-axis canvas-mode probe)
4397
+ - External CSS: `[data-topo-fullscreen="true"]` to
4398
+ apply fullscreen-only paint adjustments outside the
4399
+ React tree (e.g. body-level scrollbar hide)
4400
+ 'true' / 'false' string values (consistent with R466/
4401
+ R467/R513 boolean attrs). */
4402
+ data-topo-fullscreen={isFullscreen ? 'true' : 'false'}
4403
+ /* Round 516 / Loop — 17th canvas state attr. Surfaces the
4404
+ grid layout's content-bottom y-coordinate so tests can
4405
+ verify grid content doesn't extend past the viewBox or
4406
+ collide with chrome elements positioned below the canvas.
4407
+ Composed from existing gridContentBottom derived state
4408
+ (computed at line ~915 from gy0 + totalRows * cellH + 8).
4409
+ In ring layout, gridContentBottom is 0 (no grid). In grid
4410
+ layout it's the actual pixel y-coordinate where the
4411
+ cluster bands end.
4412
+ Use cases:
4413
+ - Playwright: assert grid layout doesn't exceed viewBox
4414
+ height (680) without re-computing the layout math
4415
+ - External CSS: `[data-topo-grid-content-bottom='0']` to
4416
+ distinguish ring-mode (no grid content) from grid-mode
4417
+ in CSS without parsing layout attr
4418
+ - Future polish gates: if cluster count grows large
4419
+ enough to push grid bottom past viewBox, can trigger
4420
+ a 'compact' mode automatically */
4421
+ data-topo-grid-content-bottom={gridContentBottom}
4422
+ data-topo-pinned-aspect={(() => {
4423
+ const aspects: string[] = [];
4424
+ if (pinnedStatus) aspects.push('status');
4425
+ if (pinnedGroup) aspects.push('group');
4426
+ if (pinnedVendor) aspects.push('vendor');
4427
+ if (pinnedEdgeKey) aspects.push('edge');
4428
+ if (aspects.length === 0) return 'none';
4429
+ if (aspects.length === 1) return aspects[0];
4430
+ return 'multi';
4431
+ })()}
3568
4432
  /* Round 466 / Loop — aggregate hover signal on the root SVG.
3569
4433
  Exposes a single boolean `data-topo-any-hover` that
3570
4434
  reflects whether ANY hover state in the topology is
@@ -3807,7 +4671,38 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3807
4671
  const x = ((seed * 13) % 1000);
3808
4672
  const y = ((seed * 7) % 680);
3809
4673
  const r = (i % 3 === 0) ? 1.2 : 0.7;
3810
- return <circle key={i} cx={x} cy={y} r={r} fill="#a5b4fc" opacity={0.35 + (i % 4) * 0.05} data-topo-starfield-dot={i} />;
4674
+ /* Round 523 / Loop 配色 family extension to a 3rd anchor.
4675
+ Pre-R523 all 14 starfield dots painted at the same
4676
+ hardcoded `#a5b4fc` (indigo-300). The starfield's role
4677
+ is atmospheric depth (R45, R291 comment), but a flat
4678
+ single-hue field reads more like a regular dot grid
4679
+ than a star field — real starlight has color
4680
+ temperature variation (blue-white hot stars / yellow
4681
+ sun-like / cool red).
4682
+ R523 cycles a 3-color deterministic rotation based on
4683
+ `i % 3`:
4684
+ i % 3 === 0 → #a5b4fc indigo-300 (original, cool)
4685
+ i % 3 === 1 → #67e8f9 cyan-300 (cyber accent, hot)
4686
+ i % 3 === 2 → #cbd5e1 slate-300 (neutral, warm white)
4687
+ All three hues sit inside the cyber theme's palette
4688
+ family (indigo / cyan / slate) so the starfield reads
4689
+ varied-but-coherent rather than rainbow. At opacity
4690
+ 0.5 (parent <g>) * 0.35-0.50 (per-dot) the temperature
4691
+ shifts are gentle but perceptible — closes the gap
4692
+ between 'dot grid' and 'star field'.
4693
+ 配色 family extension (3 anchors): R509/R510 hub-
4694
+ highlight cross-theme fill + R523 starfield color
4695
+ temperature variation. Light theme unaffected
4696
+ (starfield gated `!isLight` so light theme stays
4697
+ clean per R45's original 'white surface stays clean'
4698
+ intent).
4699
+ Deterministic on `i` — no JS hydration mismatch,
4700
+ same SSR/client output. data-topo-starfield-dot-hue
4701
+ attr exposes the resolved hue category for tests. */
4702
+ const hues = ['#a5b4fc', '#67e8f9', '#cbd5e1'] as const;
4703
+ const hueNames = ['indigo', 'cyan', 'slate'] as const;
4704
+ const hueIdx = i % 3;
4705
+ 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]} />;
3811
4706
  })}
3812
4707
  </g>
3813
4708
  )}
@@ -4345,8 +5240,60 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4345
5240
  data-topo-hub-spoke-stroke-width={spokeStrokeWidth}
4346
5241
  data-topo-hub-spoke-stroke-width-active="2.25"
4347
5242
  data-topo-hub-spoke-linecap="round"
5243
+ /* Round 533 / Loop — extends drop-shadow visual-polish
5244
+ family to a 9th anchor: hub spokes gain filter:drop-
5245
+ shadow glow on hub-hover. Subtle 1.5px cyan/teal blur
5246
+ applied across ALL spokes simultaneously when the
5247
+ user hovers the hub — the network mesh visually
5248
+ "lights up" in response to focal attention. Sibling
5249
+ to R476 hub-digit + R532 hub-highlight glow at the
5250
+ same gate (hoveredHub && !reducedMotion); together
5251
+ the three anchors (digit + highlight disc + spokes)
5252
+ form a unified focal-cluster glow that signals
5253
+ "you're focused on the hub" across geometry,
5254
+ paint, and mesh-extent axes.
5255
+ Theme-aware glow palette matches the spoke stroke
5256
+ family:
5257
+ light: rgba(13, 148, 136, 0.4) teal-600
5258
+ cyber: rgba(34, 211, 238, 0.4) cyan-400
5259
+ 0.4 alpha keeps the glow subtle across N spokes
5260
+ (30+ at peak fleet sizes) — loud bloom across many
5261
+ edges would compete with the focal cluster itself.
5262
+ 1.5px blur is conservative; tuned so each spoke
5263
+ gains a faint outer halo rather than a wide bloom.
5264
+ filter is paint-only; bbox unchanged; existing
5265
+ R241 transition list extends to 'filter 250ms
5266
+ ease-out' matching the spoke transition cadence
5267
+ (250ms, distinct from the 200ms hub-cluster
5268
+ cadence — spokes ease slightly slower since they
5269
+ respond to per-alias state, not just hub state).
5270
+ data-topo-hub-spoke-glow attr exposes the gate
5271
+ state for tests. */
5272
+ data-topo-hub-spoke-glow={!reducedMotion && hoveredHub ? 'true' : 'false'}
5273
+ /* Round 580 (65-round milestone) — hub-spokes complete
5274
+ the hub-cluster brightness coverage at 5/5 concentric
5275
+ elements. Stacks brightness(1.15) onto R533's drop-
5276
+ shadow — same R564/R570/R571/R572/R573/R574/R575/
5277
+ R577/R578/R579 stacked-filter pattern.
5278
+ Hub-cluster brightness now FULLY CLOSED:
5279
+ hub digit (R575) innermost typo
5280
+ hub-highlight (R574) middle disc
5281
+ hub-hover-ring (R579) outer ring boundary
5282
+ hub-halo (R577) outermost atmosphere
5283
+ hub-spokes (R580) mesh radial lines ← this round
5284
+ 5 concentric elements + N mesh radial lines all lift
5285
+ uniformly through stacked drop-shadow + brightness on
5286
+ hub-hover. The hub focal cluster now responds as ONE
5287
+ unified motion-coherent paint pulse from center
5288
+ outward through every layer. */
5289
+ data-topo-hub-spoke-brightness={!reducedMotion && hoveredHub ? '1.15' : '1'}
4348
5290
  style={{
4349
- transition: 'stroke 250ms ease-out, stroke-width 250ms ease-out, opacity 250ms ease-out',
5291
+ transition: 'stroke 250ms ease-out, stroke-width 250ms ease-out, opacity 250ms ease-out, filter 250ms ease-out',
5292
+ filter: !reducedMotion && hoveredHub
5293
+ ? (isLight
5294
+ ? 'drop-shadow(0 0 1.5px rgba(13, 148, 136, 0.4)) brightness(1.15)'
5295
+ : 'drop-shadow(0 0 1.5px rgba(34, 211, 238, 0.4)) brightness(1.15)')
5296
+ : undefined,
4350
5297
  ...(isActiveSpoke ? {} : {
4351
5298
  animationDelay: `${-(idx * 0.25)}s`,
4352
5299
  // CSS var consumed by `.anet-topo-spoke-flow`
@@ -4367,6 +5314,30 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4367
5314
  a node click. Restrained dashed container + group-name label. */}
4368
5315
  {groupBoxes.map((box, boxIdx) => {
4369
5316
  const isHovered = activeGroup === box.key;
5317
+ /* Round 561 / Loop — inspection-overrides-encoding family
5318
+ 4th anchor. When operator hovers a NODE ALIAS on the
5319
+ canvas, the group-label that CONTAINS that node lifts
5320
+ to full opacity, signalling "this is the cluster
5321
+ whose member you're inspecting".
5322
+ Family progression (4 anchors now):
5323
+ R484 recent-row timestamp brightens on alias hover
5324
+ R485 edge particle opacity lifts on alias hover
5325
+ R486 minimap dot opacity lifts on alias hover
5326
+ R561 group-label opacity lifts on member-alias hover ← this round
5327
+ Pure paint axis (opacity only) — same restraint as
5328
+ R486. NOT bundled into the existing `isHovered` flag
5329
+ so the marching-ants live animation (gated on
5330
+ `!isPinned && !isHovered`) keeps running; the box
5331
+ rect stroke widen / fill brighten / letter-spacing
5332
+ / filter glow gates stay tied to direct label
5333
+ hover/pin semantics.
5334
+ Mirror of R486's pattern: `hoveredAlias === s.alias`
5335
+ extends a focused opacity branch independent from
5336
+ the rest-state encoding (online/offline). R561 here:
5337
+ `groupKeys[hoveredAlias] === box.key` extends a
5338
+ cluster-awareness opacity branch independent from
5339
+ the existing isHovered / isPinned semantics. */
5340
+ const isMemberAliasHovered = !!hoveredAlias && groupKeys[hoveredAlias] === box.key;
4370
5341
  // R68: distinguish "locked by click" from "currently hovered".
4371
5342
  // R63 made pinned and hovered identical (both hit isHovered
4372
5343
  // via activeGroup). A user with one team pinned should see at
@@ -4494,12 +5465,75 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4494
5465
  // pinned → fill 0.08 / 0.13, stroke 3 px (locked)
4495
5466
  // hovered → fill 0.05 / 0.09, stroke 2 px (inspecting)
4496
5467
  // idle → fill 0.025 / 0.045, stroke 1.5 px dashed
4497
- fillOpacity={isPinned ? (isLight ? 0.08 : 0.13)
4498
- : isHovered ? (isLight ? 0.05 : 0.09)
4499
- : (isLight ? 0.025 : 0.045)}
5468
+ /* Round 506 / Loop category-differentiation family
5469
+ 3rd anchor. Orphan band rest-state fillOpacity drops
5470
+ slightly below prefix-group rest (0.025/0.045
5471
+ 0.015/0.028). Adds a 3rd independent paint
5472
+ differentiator to the orphan visual signature:
5473
+ R499 fontStyle: italic (label text)
5474
+ R503 '3 6' dash pattern (rect stroke)
5475
+ R506 lower fillOpacity (rect fill) ← this round
5476
+ Three independent channels (typography + stroke
5477
+ pattern + fill density) collectively encode the
5478
+ catchall semantic at rest. Pin and hover branches
5479
+ UNCHANGED (still 0.05/0.09 hover, 0.08/0.13 pin) —
5480
+ orphan box gets full visual emphasis on inspection
5481
+ identical to prefix groups; the differentiation
5482
+ lives ONLY in the unsolicited rest state. The
5483
+ ~40% drop (0.045 → 0.028 cyber, 0.025 → 0.015
5484
+ light) is subtle enough that the orphan box stays
5485
+ visible at rest, just quieter — matches the
5486
+ "misc bucket, less attention-deserving" semantic
5487
+ without losing the visual anchor.
5488
+ Pure paint axis; bbox unchanged; R51 SVG sentinel
5489
+ safety untouched (overlap-test gates to g[data-
5490
+ node], cluster rect invisible to it).
5491
+ data-group-box-fill-opacity attr surfaces the
5492
+ resolved value for tests. */
5493
+ fillOpacity={
5494
+ isPinned ? (isLight ? 0.08 : 0.13)
5495
+ : isHovered ? (isLight ? 0.05 : 0.09)
5496
+ : box.isOrphan ? (isLight ? 0.015 : 0.028)
5497
+ : (isLight ? 0.025 : 0.045)
5498
+ }
5499
+ data-group-box-fill-opacity={
5500
+ isPinned ? (isLight ? 0.08 : 0.13)
5501
+ : isHovered ? (isLight ? 0.05 : 0.09)
5502
+ : box.isOrphan ? (isLight ? 0.015 : 0.028)
5503
+ : (isLight ? 0.025 : 0.045)
5504
+ }
4500
5505
  stroke={(isPinned || isHovered) ? pal.legendAccent : pal.ringStroke}
4501
5506
  strokeWidth={isPinned ? 3 : isHovered ? 2 : 1.5}
4502
- strokeDasharray={(isPinned || isHovered) ? 'none' : '6 6'}
5507
+ /* Round 503 / Loop category-differentiation family
5508
+ 2nd anchor (R499 = orphan label italic, 1st anchor).
5509
+ Orphan band rest-state strokeDasharray switches from
5510
+ '6 6' (prefix-group default) to '3 6' (tighter
5511
+ dashes). Pre-R503 the rect dash pattern was uniform
5512
+ across all bands; combined with R499's italic label,
5513
+ the orphan box now has TWO independent paint/
5514
+ typography differentiators at rest:
5515
+ R499 fontStyle: italic (label text)
5516
+ R503 '3 6' dash pattern (rect stroke) ← this round
5517
+ The R85 marching-ants animation continues to work
5518
+ with the new dash size (uses --march-dur custom
5519
+ property, dash-length-agnostic) — orphan's ants
5520
+ just have a different visual rhythm than prefix-
5521
+ group ants, reinforcing the catchall semantic.
5522
+ Pinned/hovered orphan still gets 'none' (solid
5523
+ stroke) so the hover/pin affordance is preserved
5524
+ — the differentiation lives ONLY in the rest
5525
+ state, never blocking inspection.
5526
+ Pure paint axis; no geometry change; bbox unchanged
5527
+ (strokeDasharray is paint-only). R51 SVG sentinel
5528
+ safety untouched (overlap-test gates to g[data-
5529
+ node], this cluster rect is invisible to it).
5530
+ data-group-box-orphan attr surfaces the gate for
5531
+ tests + future polish references. */
5532
+ strokeDasharray={
5533
+ (isPinned || isHovered) ? 'none' :
5534
+ box.isOrphan ? '3 6' : '6 6'
5535
+ }
5536
+ data-group-box-orphan={box.isOrphan ? 'true' : 'false'}
4503
5537
  /* Round 380 / Loop: cluster box stroke gets round
4504
5538
  linecap + round linejoin. Sibling SVG stroke-
4505
5539
  softening polish to R378 flow-rail linecap + R379
@@ -4545,10 +5579,29 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4545
5579
  // motion layers (hub / ring / group) keep a coherent
4546
5580
  // tempo grammar. Default 12s when working=0 doesn't
4547
5581
  // matter — the className is only applied when working>0.
4548
- data-group-box-live={!isPinned && !isHovered && box.statuses.working > 0 ? 'true' : 'false'}
5582
+ /* Round 561 marching-ants gate refined to halt only
5583
+ on direct-label-hover (or pin), NOT on member-alias-
5584
+ hover. Pre-R561 `isHovered` covered BOTH cases via
5585
+ the line 1044 fallback `hoveredGroup ?? (hoveredAlias
5586
+ → groupKeys[hoveredAlias])` — so hovering ANY member
5587
+ node halted the ants, treating indirect inspection
5588
+ the same as direct attention.
5589
+ R561 differentiates: ants now keep running during
5590
+ member-alias hover (indirect / cluster-awareness
5591
+ inspection), halting ONLY on direct label hover or
5592
+ pin. The cluster's live signal stays alive while
5593
+ operator inspects member nodes — distinct visual
5594
+ telegraph for "directly attending this cluster"
5595
+ vs "inspecting one of its members".
5596
+ Gate uses `hoveredGroupLabel === box.key` directly
5597
+ (the LABEL hover state) instead of `isHovered`
5598
+ (which combines label-hover + member-alias-hover
5599
+ via activeGroup). data-group-box-live + the live
5600
+ className both flip on the same refined gate. */
5601
+ data-group-box-live={!isPinned && hoveredGroupLabel !== box.key && box.statuses.working > 0 ? 'true' : 'false'}
4549
5602
  data-group-box-march-dur={marchDur}
4550
5603
  data-group-box-lifted={(isPinned || isHovered) ? 'true' : 'false'}
4551
- className={!isPinned && !isHovered && box.statuses.working > 0 ? 'anet-topo-groupbox-live' : undefined}
5604
+ className={!isPinned && hoveredGroupLabel !== box.key && box.statuses.working > 0 ? 'anet-topo-groupbox-live' : undefined}
4552
5605
  // R142: drop-shadow filter when pinned or hovered. Box
4553
5606
  // visually "rises off the canvas" — same vocabulary
4554
5607
  // R18 KPI cards + R135 overlay panels use. Idle group
@@ -4819,8 +5872,54 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4819
5872
  fontSize="9"
4820
5873
  fontFamily="monospace"
4821
5874
  fontWeight={isPinned ? '800' : '700'}
4822
- opacity={isPinned || isHovered ? 1 : 0.55}
5875
+ /* Round 551 / Loop category-differentiation family
5876
+ 4th anchor. Orphan band ("其他" catchall) rest-state
5877
+ LABEL opacity drops 0.55 → 0.4 (~27% relative dim),
5878
+ mirroring R506's rect fillOpacity drop pattern at
5879
+ the label-paint tier. Adds a 4th independent
5880
+ channel to the orphan visual signature at rest:
5881
+ R499 fontStyle italic (typography style)
5882
+ R503 '3 6' dash pattern (rect stroke)
5883
+ R506 lower rect fill-opacity (rect fill)
5884
+ R551 lower label opacity (label paint) ← this round
5885
+ Four independent channels (typography style +
5886
+ stroke pattern + rect fill density + label paint
5887
+ density) collectively encode the catchall semantic
5888
+ at rest — orphan band reads as "misc bucket, less
5889
+ attention-deserving" through every available paint
5890
+ channel, no chrome / color / geometry change.
5891
+ Pin and hover branches UNCHANGED — orphan label
5892
+ restores to full opacity 1 on inspection, same as
5893
+ prefix groups. The differentiation lives ONLY in
5894
+ the unsolicited rest state. The ~27% drop (0.55 →
5895
+ 0.4) is dimmer than R506's ~40% (rect could
5896
+ tolerate it; small 9px text needs more residual
5897
+ paint to stay legible) — orphan label stays
5898
+ readable when scanning, just clearly quieter.
5899
+ Pure paint axis; bbox unchanged; R51 SVG sentinel
5900
+ safety untouched (overlap-test gates to g[data-
5901
+ node], this group-label is invisible to it).
5902
+ transition list (R55/R432/R457/R479: fill, ls,
5903
+ fw, filter all 200ms) already eases opacity since
5904
+ `opacity 300ms ease-out` lives in the parent <text>
5905
+ CSS — wait, only filter/ls/fw/fill 200ms are
5906
+ listed. Need to add 'opacity 200ms ease-out' for
5907
+ smooth orphan opacity flip on pin/hover transitions
5908
+ (currently opacity 0.55 → 1 was snapping).
5909
+ data-group-label-opacity attr exposes the resolved
5910
+ value for tests. */
5911
+ /* Round 561 — opacity ladder gains inspection-
5912
+ overrides-encoding branch via isMemberAliasHovered.
5913
+ Resolution order (most-emphatic first):
5914
+ isPinned || isHovered → 1 (direct attention)
5915
+ isMemberAliasHovered → 1 (R561 inspection)
5916
+ box.isOrphan → 0.4 (R551 orphan rest)
5917
+ (default) → 0.55
5918
+ R484/R485/R486-family mirror. */
5919
+ opacity={isPinned || isHovered || isMemberAliasHovered ? 1 : box.isOrphan ? 0.4 : 0.55}
5920
+ data-group-label-opacity={isPinned || isHovered || isMemberAliasHovered ? 1 : box.isOrphan ? 0.4 : 0.55}
4823
5921
  data-group-label-hovered={isHovered && !isPinned ? 'true' : 'false'}
5922
+ data-group-label-member-alias-hovered={isMemberAliasHovered ? 'true' : 'false'}
4824
5923
  data-group-label-font-weight={isPinned ? '800' : '700'}
4825
5924
  /* Round 479 / Loop — extend drop-shadow visual-polish
4826
5925
  family to a 4th anchor: group-label parent text
@@ -4843,17 +5942,94 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4843
5942
  transition list extends to include 'filter 200ms
4844
5943
  ease-out' alongside the existing fill/ls/fw/opacity
4845
5944
  200ms tweens. */
4846
- data-group-label-glow={isPinned ? 'true' : 'false'}
5945
+ /* Round 538 / Loop — extends R479 group-label drop-
5946
+ shadow from pin-only to ALSO fire on hover, with
5947
+ a 2-tier alpha ladder matching the R432 letter-
5948
+ spacing 3-tier (hover at 0.25px / pin at 0.5px)
5949
+ pattern. Pre-R538 the paint axis was binary (lit
5950
+ on pin, dark on hover); R538 adds a softer hover
5951
+ glow that distinguishes from the stronger pin
5952
+ glow without losing the "active state lights up"
5953
+ gesture.
5954
+ 2-tier alpha ladder:
5955
+ pin (committed) cyan 80 hex (~50% alpha)
5956
+ hover (preview) cyan 4d hex (~30% alpha)
5957
+ rest none
5958
+ Pin signature stays distinctively brighter, but
5959
+ hover now telegraphs paint-axis attention too.
5960
+ Sibling to R534 edge-badge hover-precedence
5961
+ extension at the drop-shadow family. R479 hue
5962
+ (pal.legendAccent) preserved across both tiers.
5963
+ data-group-label-glow attr upgraded from binary
5964
+ ('true'/'false') to 3-value ('pin' | 'hover' |
5965
+ 'false') so tests can distinguish gate cause. */
5966
+ data-group-label-glow={isPinned ? 'pin' : isHovered ? 'hover' : 'false'}
5967
+ /* Round 499 / Loop — orphan band "其他" label gets
5968
+ fontStyle: italic to visually distinguish the
5969
+ catchall from real prefix-group bands. Pre-R499
5970
+ the orphan box label rendered identically to
5971
+ prefix-group labels (Hero D fontSize=9, fw=700,
5972
+ opacity 0.55 rest), so users had to read the
5973
+ literal text "其他" to identify the catchall. R499
5974
+ adds a pure-typography differentiation: italic
5975
+ signals "this is the misc bucket, not a real
5976
+ named group" while preserving full opacity
5977
+ affordance on hover/pin — the orphan box stays
5978
+ equally inspectable, just typographically marked
5979
+ as a different category. No geometry change
5980
+ (italic shifts glyph slant within the same bbox),
5981
+ no opacity loss, no behavior change. Sibling to
5982
+ R432 letter-spacing 3-tier + R457 pin fw-lift +
5983
+ R479 pin drop-shadow at the group-label scope.
5984
+ Falls under 配色 / 节点视觉 themes per the prompt;
5985
+ advances the "信息密度" axis by encoding
5986
+ category-distinction into a single typography
5987
+ channel without adding visual chrome. */
5988
+ /* Round 571 / Loop — group-label parent text joins the
5989
+ per-element brightness consistency family (R501/
5990
+ R558/R564/R567/R570) at uniform +15%. 8th anchor.
5991
+ Stacks brightness(1.15) onto the existing R479/R538
5992
+ drop-shadow filter — same R564/R570 stacked filter
5993
+ pattern (drop-shadow + brightness in one CSS chain).
5994
+ Pre-R571 the group-label parent text lifted in 5
5995
+ axes on hover/pin (fill + ls 3-tier + fw on pin +
5996
+ drop-shadow + opacity) but the glyph chromatically
5997
+ stayed at flat fill brightness. R571 adds the
5998
+ brightness axis to the glyph itself for cross-
5999
+ element consistency with the rest of the per-
6000
+ element brightness family.
6001
+ Filter chain on isPinned: `drop-shadow(0 0 3px
6002
+ ${pal.legendAccent}80) brightness(1.15)`.
6003
+ On isHovered (weaker tier): `drop-
6004
+ shadow(0 0 3px ${pal.legendAccent}4d) brightness(1.15)`.
6005
+ Rest: undefined.
6006
+ Per-element brightness family — 8 anchors at +15%:
6007
+ R501 vendor.logo image
6008
+ R558 vendor monogram
6009
+ R558 prefix-group fallback
6010
+ R564 alias text (stacked w/ DS)
6011
+ R567 node sub-text
6012
+ R570 edge-badge digit
6013
+ R571 group-label parent text ← this round
6014
+ transition list already includes 'filter 200ms ease-
6015
+ out' from R479 — no change needed. R432 ls + R457
6016
+ fw + R479 drop-shadow + R551 orphan opacity all
6017
+ preserved. */
4847
6018
  style={{
4848
6019
  transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out, font-weight 200ms ease-out, opacity 200ms ease-out, filter 200ms ease-out',
4849
6020
  letterSpacing: isPinned ? '0.5px' :
4850
6021
  isHovered ? '0.25px' : '0px',
6022
+ fontStyle: box.isOrphan ? 'italic' : undefined,
4851
6023
  filter: isPinned
4852
- ? `drop-shadow(0 0 3px ${pal.legendAccent}80)`
4853
- : undefined,
6024
+ ? `drop-shadow(0 0 3px ${pal.legendAccent}80) brightness(1.15)`
6025
+ : isHovered
6026
+ ? `drop-shadow(0 0 3px ${pal.legendAccent}4d) brightness(1.15)`
6027
+ : undefined,
4854
6028
  }}
4855
6029
  data-group-label={box.key}
4856
6030
  data-group-label-pinned={isPinned ? 'true' : 'false'}
6031
+ data-group-label-orphan={box.isOrphan ? 'true' : 'false'}
6032
+ data-group-label-brightness={(isPinned || isHovered) ? '1.15' : '1'}
4857
6033
  >
4858
6034
  {box.key}
4859
6035
  {/* Round 19 / Loop: member-count chip. Inline tspan stays
@@ -5315,6 +6491,19 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
5315
6491
  unchanged at the join with the arrow marker).
5316
6492
  data-edge-visible-linecap attr exposes the value
5317
6493
  for tests. */}
6494
+ {/* Round 582 — edge visible flow path joins per-element
6495
+ brightness family at 21st anchor. Stacks
6496
+ brightness(1.15) with the existing url(#topo-glow)
6497
+ SVG filter (cyber) or applies plain brightness
6498
+ (light). CSS filter accepts mixed url() + function
6499
+ values; inline style.filter overrides any
6500
+ attribute-level filter. Closes edge-tier brightness
6501
+ sub-family at 2 surfaces:
6502
+ R581 flow-rail (dashed underline) brightness
6503
+ R582 visible path (primary curve) brightness ← this round
6504
+ transition list extends to include 'filter 300ms
6505
+ ease-out' matching the existing opacity/sw/stroke
6506
+ cadence. */}
5318
6507
  <path
5319
6508
  d={path}
5320
6509
  fill="none"
@@ -5322,15 +6511,18 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
5322
6511
  strokeWidth={renderWidth}
5323
6512
  strokeLinecap="round"
5324
6513
  opacity={Math.min(1, (isLight ? 0.22 : 0.28) * fresh * edgeOpacityMul)}
5325
- filter={isLight ? undefined : 'url(#topo-glow)'}
5326
6514
  markerEnd={`url(#${arrowId})`}
5327
6515
  data-edge-visible={link.key}
5328
6516
  data-edge-visible-linecap="round"
5329
6517
  data-edge-visible-endpoint-hovered={isEndpointHoveredEdge ? 'true' : 'false'}
5330
6518
  data-edge-visible-stroke-width={renderWidth}
6519
+ data-edge-visible-brightness={(isHoveredEdge || isEndpointHoveredEdge) ? '1.15' : '1'}
5331
6520
  style={{
5332
6521
  pointerEvents: 'none',
5333
- transition: 'opacity 300ms ease-out, stroke-width 300ms ease-out, stroke 300ms ease-out',
6522
+ transition: 'opacity 300ms ease-out, stroke-width 300ms ease-out, stroke 300ms ease-out, filter 300ms ease-out',
6523
+ filter: (isHoveredEdge || isEndpointHoveredEdge)
6524
+ ? (isLight ? 'brightness(1.15)' : 'url(#topo-glow) brightness(1.15)')
6525
+ : (isLight ? undefined : 'url(#topo-glow)'),
5334
6526
  }}
5335
6527
  />
5336
6528
  {/* Round 378 / Loop: edge flow-path dashed-rail picks
@@ -5380,7 +6572,23 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
5380
6572
  data-edge-flow-rail-linecap="round"
5381
6573
  data-edge-flow-rail-stroke-width={(isHoveredEdge || isEndpointHoveredEdge) ? 1.5 : 1}
5382
6574
  data-edge-flow-rail-lifted={(isHoveredEdge || isEndpointHoveredEdge) ? 'true' : 'false'}
5383
- style={{ transition: 'opacity 300ms ease-out, stroke 300ms ease-out, stroke-width 300ms ease-out' }}
6575
+ /* Round 581 flow-rail joins per-element brightness
6576
+ family at 20th anchor. Adds brightness(1.15) on
6577
+ edge hover or endpoint hover. Joins R437 sw-lift
6578
+ paint pattern at the dashed-underline tier — when
6579
+ an edge is in focus, the rail's stroke widens
6580
+ (R437) AND brightens (R581) together, reading
6581
+ as a coherent rail-lift gesture under the flow.
6582
+ transition list extends to include 'filter 300ms
6583
+ ease-out' matching the R245/R437 cadence on this
6584
+ surface. */
6585
+ data-edge-flow-rail-brightness={(isHoveredEdge || isEndpointHoveredEdge) ? '1.15' : '1'}
6586
+ style={{
6587
+ transition: 'opacity 300ms ease-out, stroke 300ms ease-out, stroke-width 300ms ease-out, filter 300ms ease-out',
6588
+ filter: (isHoveredEdge || isEndpointHoveredEdge)
6589
+ ? 'brightness(1.15)'
6590
+ : undefined,
6591
+ }}
5384
6592
  />
5385
6593
  {!reducedMotion && (
5386
6594
  /* Round 103 / Loop: phase-stagger the particles so
@@ -5450,7 +6658,31 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
5450
6658
  data-edge-particle-lifted={(isHoveredEdge || isEndpointHoveredEdge) ? 'true' : 'false'}
5451
6659
  data-edge-particle-opacity-rest={Math.min(1, fresh * edgeOpacityMul).toFixed(2)}
5452
6660
  data-edge-particle-opacity-lifted={(isHoveredEdge || isEndpointHoveredEdge) ? 'true' : 'false'}
5453
- style={{ transition: 'fill 200ms ease-out, opacity 200ms ease-out, r 200ms ease-out' }}
6661
+ /* Round 583 flow particle joins per-element
6662
+ brightness family at 22nd anchor. Adds
6663
+ brightness(1.15) on edge hover or endpoint
6664
+ hover, joining R485 opacity inspection-override
6665
+ + R422 r-lift (4 → 4.5) + R164 hover-r-lift
6666
+ (4.5 → 5.5). Particle now has 4-axis active
6667
+ signature on edge inspection:
6668
+ R485 opacity (freshness → 1.0)
6669
+ R164 r 4.5 → 5.5
6670
+ R422 r-base 4 → 4.5 (visual-weight bump)
6671
+ R583 brightness(1.15) ← this round
6672
+ Particle becomes the brightest paint element
6673
+ along the edge during inspection. */
6674
+ data-edge-particle-brightness={(isHoveredEdge || isEndpointHoveredEdge) ? '1.15' : '1'}
6675
+ style={{
6676
+ transition: 'fill 200ms ease-out, opacity 200ms ease-out, r 200ms ease-out, filter 200ms ease-out',
6677
+ /* R583 — stack brightness(1.15) onto the existing
6678
+ url(#topo-glow) (cyber) or apply plain brightness
6679
+ (light). Inline style.filter overrides attribute
6680
+ filter; stacked syntax preserves the cyber glow
6681
+ on hover. Same R582 visible-path stack pattern. */
6682
+ filter: (isHoveredEdge || isEndpointHoveredEdge)
6683
+ ? (isLight ? 'brightness(1.15)' : 'url(#topo-glow) brightness(1.15)')
6684
+ : undefined,
6685
+ }}
5454
6686
  >
5455
6687
  <animateMotion
5456
6688
  dur={`${duration}s`}
@@ -5940,11 +7172,48 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
5940
7172
  data-edge-badge-opacity-rest={isLight ? 0.95 : 0.85}
5941
7173
  data-edge-badge-opacity-hover="1"
5942
7174
  data-edge-badge-opacity-active="1"
5943
- data-edge-badge-glow={isHot ? 'true' : 'false'}
7175
+ data-edge-badge-glow={(isHoveredEdge || isPinned) ? 'hover' : isHot ? 'hot' : 'false'}
7176
+ /* Round 534 / Loop — extends edge-badge drop-shadow
7177
+ coverage from hot-only (R480 amber) to also fire
7178
+ on hover/pin with a cyan accent glow. Pre-R534
7179
+ the badge's hover/pin lifted r (R164 9 → 10.5)
7180
+ + sw (R394 1.25 → 1.5) + opacity (R395/R396 →
7181
+ 1.0), but the paint axis stayed at the badge's
7182
+ rest fill — no glow to telegraph "in focus" at
7183
+ the paint layer. R534 closes that 4-axis hover-
7184
+ lift parity by adding drop-shadow glow on
7185
+ (hovered || pinned).
7186
+ Precedence: (hover || pin) wins over isHot when
7187
+ BOTH true — interactive signal (user is
7188
+ inspecting) overrides informational signal
7189
+ (hot lane). When only isHot fires (no hover/
7190
+ pin) the amber R480 glow remains; the hover/
7191
+ pin case paints cyan/teal `pal.legendAccent`
7192
+ at 0x99 alpha (~60%) — bright enough to read
7193
+ as "lit" but won't overwhelm at small badge
7194
+ size (r=10.5).
7195
+ Edge-badge 4-axis hover-lift parity now:
7196
+ R164 r 9 → 10.5
7197
+ R394 stroke-wd 1.25 → 1.5
7198
+ R395 opacity rest → 1.0
7199
+ R534 filter none → drop-shadow glow ← this round
7200
+ Drop-shadow visual-polish family extension —
7201
+ edge-badge surface upgraded from single-gate
7202
+ (R480 isHot) to two-gate (isHot OR hover-pin).
7203
+ transition list already includes filter 200ms
7204
+ ease-out (R480). data-edge-badge-glow attr
7205
+ upgraded from `isHot ? true : false` to a
7206
+ 3-value string: 'hot' | 'hover' | 'false' so
7207
+ tests can distinguish gate cause.
7208
+ R51 sentinel safety: badge is edge-internal
7209
+ (not g[data-node] ancestor); filter is paint-
7210
+ only; bbox unchanged. */
5944
7211
  style={{
5945
- filter: isHot
5946
- ? `drop-shadow(0 0 3px ${hotStroke}80)`
5947
- : undefined,
7212
+ filter: (isHoveredEdge || isPinned)
7213
+ ? `drop-shadow(0 0 3px ${pal.legendAccent}99)`
7214
+ : isHot
7215
+ ? `drop-shadow(0 0 3px ${hotStroke}80)`
7216
+ : undefined,
5948
7217
  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',
5949
7218
  }}
5950
7219
  />
@@ -6034,6 +7303,31 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6034
7303
  data-edge-badge-text={link.key}
6035
7304
  data-edge-badge-text-pin={(isPinned || isHot) ? 'true' : 'false'}
6036
7305
  data-edge-badge-text-font-size="11"
7306
+ /* Round 570 / Loop — edge-badge digit joins the per-
7307
+ element brightness consistency family (R501/R558/
7308
+ R564/R567) at uniform +15%. 7th anchor.
7309
+ Gate: (isHoveredEdge || isPinned || isHot) — same
7310
+ 3-tier set as R431 ls (with hover the mid step
7311
+ and pin/hot the strong step). Brightness lifts
7312
+ uniformly across all 3 active sub-states; the
7313
+ ls/fw axes still distinguish hover from pin/hot.
7314
+ Pure paint axis on the digit glyph; bbox
7315
+ unchanged. The R540 badge-circle drop-shadow
7316
+ sits on the parent CIRCLE element (separate
7317
+ filter); the digit's filter is independent and
7318
+ doesn't compound with circle filter (different
7319
+ SVG element).
7320
+ Per-element brightness family — 7 anchors at +15%:
7321
+ R501 vendor.logo image
7322
+ R558 vendor monogram
7323
+ R558 prefix-group fallback
7324
+ R564 alias text (stacked w/ DS)
7325
+ R567 node sub-text
7326
+ R570 edge-badge digit ← this round
7327
+ Plus runtime badge drop-shadow (R559) on same
7328
+ isNodeActive gate. data-edge-badge-text-brightness
7329
+ attr surfaces the lift for tests. */
7330
+ data-edge-badge-text-brightness={(isHoveredEdge || isPinned || isHot) ? '1.15' : '1'}
6037
7331
  style={{
6038
7332
  pointerEvents: 'none',
6039
7333
  fontVariantNumeric: 'tabular-nums',
@@ -6052,7 +7346,10 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6052
7346
  now): R344/R345/R347/R351/R420/R427/R431. */
6053
7347
  letterSpacing: (isPinned || isHot) ? '0.4px' :
6054
7348
  isHoveredEdge ? '0.2px' : '0px',
6055
- transition: 'letter-spacing 300ms ease-out, font-weight 300ms ease-out',
7349
+ filter: (isHoveredEdge || isPinned || isHot)
7350
+ ? 'brightness(1.15)'
7351
+ : undefined,
7352
+ transition: 'letter-spacing 300ms ease-out, font-weight 300ms ease-out, filter 300ms ease-out',
6056
7353
  }}
6057
7354
  >{link.count}</text>
6058
7355
  </g>
@@ -6244,6 +7541,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6244
7541
  data-hub-busyness={busy}
6245
7542
  data-topo-hub-halo-radius={haloR}
6246
7543
  data-topo-hub-halo-hovered={isHaloHovered ? 'true' : 'false'}
7544
+ data-topo-hub-halo-glow={!reducedMotion && hoveredHub ? 'true' : 'false'}
6247
7545
  data-topo-hub-halo-trough={isLight ? troughLight : troughDark}
6248
7546
  data-topo-hub-halo-peak={isLight ? peakLight : peakDark}
6249
7547
  /* Round 253 / Loop: hub grounding halo fill transition
@@ -6255,10 +7553,59 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6255
7553
  conflict.
6256
7554
  R451: r as CSS property (R197/R198 idiom) so the
6257
7555
  hover-radius tween eases smoothly under the same
6258
- 200ms cadence as fill. */
7556
+ 200ms cadence as fill.
7557
+ R536: extends hub-cluster glow QUARTET (R476 digit
7558
+ + R532 disc + R535 ring + R533 spokes) to a 5th
7559
+ tier — the halo gains drop-shadow at the outermost
7560
+ concentric ring on hub-hover. 2px blur + 0.3 alpha
7561
+ keeps the halo's glow subtle since (a) the halo is
7562
+ the LARGEST hub element (r=22 hover) and a heavier
7563
+ glow would bleed visibly past the ring tier into
7564
+ the spoke origin, and (b) the halo already SMIL-
7565
+ animates opacity (R84/R244 breath), so the visible
7566
+ glow pulses with the breath — an atmospheric
7567
+ "breathing glow" idiom rather than a static rim.
7568
+ Hub-cluster glow QUINTET (R476/R532/R533/R535/R536):
7569
+ digit (typo center) 3px emerald 0.6
7570
+ disc (r=5.5/6) 3px emerald 0.6
7571
+ ring (r=14/17) 3px emerald 0.5
7572
+ halo (r=20/22) 2px emerald 0.3 ← this round
7573
+ spokes (mesh) 1.5px cyan/teal 0.4
7574
+ Emerald palette continues through the focal-disc
7575
+ family (digit/disc/ring/halo); spokes break out
7576
+ into cyan/teal at the mesh tier. The 4-step alpha
7577
+ ladder 0.6→0.6→0.5→0.3 reads as the focal cluster
7578
+ fading outward — the OUTERMOST emerald glow is the
7579
+ softest, the focal digit is the brightest.
7580
+ filter is paint-only; SMIL animate on opacity
7581
+ continues independently (attribute vs CSS-property
7582
+ non-conflicting). transition list extends to
7583
+ 'filter 200ms ease-out' alongside fill + r.
7584
+ Drop-shadow visual-polish family extension (12
7585
+ anchors). preview.50 milestone round. data-topo-
7586
+ hub-halo-glow attr exposes the gate state. */
7587
+ data-topo-hub-halo-brightness={!reducedMotion && hoveredHub ? '1.15' : '1'}
7588
+ /* Round 577 — hub-halo joins per-element brightness
7589
+ family at 15th anchor. Stacks brightness(1.15)
7590
+ onto R536's hub-hover drop-shadow — closes the
7591
+ hub-cluster focal brightness coverage at 3
7592
+ concentric elements:
7593
+ hub digit (R575)
7594
+ hub-highlight (R574)
7595
+ hub-halo (R577) ← this round
7596
+ All 3 hub focal elements now lift through stacked
7597
+ drop-shadow + brightness on hub-hover. Halo is
7598
+ the OUTERMOST so a slight chromatic lift reads as
7599
+ the focal cluster intensifying its ambient glow
7600
+ outward. */
6259
7601
  style={{
6260
7602
  r: `${haloR}px`,
6261
- transition: 'fill 200ms ease-out, r 200ms ease-out',
7603
+ filter: !reducedMotion && hoveredHub
7604
+ ? (isLight
7605
+ ? 'drop-shadow(0 0 2px rgba(16, 185, 129, 0.3)) brightness(1.15)'
7606
+ : 'drop-shadow(0 0 2px rgba(52, 211, 153, 0.3)) brightness(1.15)')
7607
+ : undefined,
7608
+ transition: 'fill 200ms ease-out, r 200ms ease-out, filter 200ms ease-out',
6262
7609
  } as React.CSSProperties}
6263
7610
  >
6264
7611
  {/* Round 244 / Loop: hub grounding halo breath gets
@@ -6418,6 +7765,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6418
7765
  data-topo-hub-working-count={workingCount}
6419
7766
  data-topo-hub-working-count-font-size="12"
6420
7767
  data-topo-hub-working-count-hovered={hoveredHub ? 'true' : 'false'}
7768
+ data-topo-hub-working-count-brightness={!reducedMotion && hoveredHub ? '1.15' : '1'}
6421
7769
  data-topo-hub-working-count-visible={workingCount > 0 ? 'true' : 'false'}
6422
7770
  // Round 209 / Loop: hub workingCount digit scales 1.0 →
6423
7771
  // 1.08 on hub-hover, matching R177's r 14→17 ring grow.
@@ -6468,22 +7816,109 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6468
7816
  so the glow eases under the same cadence as the
6469
7817
  scale + fw + fill axes. */
6470
7818
  data-topo-hub-working-count-glow={!reducedMotion && hoveredHub ? 'true' : 'false'}
7819
+ /* Round 507 / Loop — focal recede. When ANY non-hub
7820
+ canvas surface is hovered (a node / an edge / a
7821
+ group label / a legend row / a vendor chip), the
7822
+ hub-center workingCount digit fades to 0.85 opacity,
7823
+ signaling "you're inspecting elsewhere, hub recedes
7824
+ to background." When the user un-hovers (or hovers
7825
+ the hub itself), opacity returns to 1.0. Pure paint
7826
+ polish at the canvas's most prominent focal point.
7827
+ Hits 信息密度 + 动效 themes — the hub digit gives
7828
+ way visually to the surface under inspection,
7829
+ reinforcing the "this is the focal point right now"
7830
+ gesture without requiring users to track which
7831
+ surface holds attention.
7832
+ Gate excludes hoveredHub specifically: hovering the
7833
+ hub itself should LIFT the digit (R425 fw bump +
7834
+ R476 glow + R209 scale 1.08) — the existing hover-
7835
+ on-hub signature is intact; only inspection
7836
+ ELSEWHERE recedes the hub.
7837
+ Composed from existing hoveredAlias / hoveredEdge-
7838
+ Key / hoveredGroupLabel / hoveredStatus / hovered-
7839
+ Vendor — no new state. 300ms ease-out opacity
7840
+ transition already in the style list (existing R213
7841
+ transition spec), so the fade rides on existing
7842
+ infrastructure.
7843
+ data-topo-hub-recede attr surfaces the gate state
7844
+ for tests. */
7845
+ data-topo-hub-recede={
7846
+ (hoveredAlias || hoveredEdgeKey || hoveredGroupLabel ||
7847
+ hoveredStatus || hoveredVendor) && !hoveredHub ? 'true' : 'false'
7848
+ }
7849
+ /* Round 527 / Loop — focal-amplify family extension to a
7850
+ 2nd anchor. R511 introduced focal-amplify at the hub-
7851
+ highlight disc (base opacity 0.95 → 1.0 on hover); R527
7852
+ extends to the hub-center workingCount digit with a
7853
+ letter-spacing tween 0 → 0.3px on hub-hover.
7854
+ Composes with existing 3-axis hub-hover signature on
7855
+ this element:
7856
+ R209 transform scale(1.08) geometry
7857
+ R425 fontWeight 700 → 800 typography weight
7858
+ R476 filter drop-shadow glow paint
7859
+ R527 letter-spacing 0 → 0.3px typography kerning ← this round
7860
+ tabular-nums (R225) preserved — each digit cell keeps
7861
+ fixed width; the inter-digit advance grows by 0.3px
7862
+ per gap. Single-digit counts (1-9) show no kerning
7863
+ effect; multi-digit counts (10+) show the spread as
7864
+ info-density signaling. Sibling to R427/R431/R432/
7865
+ R433/R434 (hover-letter-spacing family at panel-text
7866
+ scope) — R527 brings the same idiom to the canvas's
7867
+ most-read scalar.
7868
+ Reduced-motion gate matches R209 scale, R425 fw, R476
7869
+ filter — !reducedMotion gates the lift; reducedMotion
7870
+ users see static digit baseline regardless of hover.
7871
+ Focal-amplify family extension (2 anchors): R511 hub-
7872
+ highlight opacity / R527 hub-digit letter-spacing.
7873
+ transition list extends to include `letter-spacing
7874
+ 200ms ease-out`, matching the cadence of the other
7875
+ hub-hover axes. data-topo-hub-working-count-letter-
7876
+ spacing attr exposes the resolved value for tests. */
7877
+ data-topo-hub-working-count-letter-spacing={!reducedMotion && hoveredHub ? '0.3px' : '0px'}
6471
7878
  style={{
6472
7879
  pointerEvents: 'none',
6473
7880
  transform: !reducedMotion && hoveredHub ? 'scale(1.08)' : 'scale(1)',
6474
7881
  transformBox: 'fill-box',
6475
7882
  transformOrigin: 'center',
7883
+ opacity: (hoveredAlias || hoveredEdgeKey || hoveredGroupLabel ||
7884
+ hoveredStatus || hoveredVendor) && !hoveredHub
7885
+ ? 0.85
7886
+ : 1,
7887
+ /* Round 575 (60-round milestone) — hub digit joins per-
7888
+ element brightness family at 14th anchor. Stacks
7889
+ brightness(1.15) onto R476's hub-hover drop-shadow
7890
+ — same R564/R570/R571/R572/R573/R574 pattern (drop-
7891
+ shadow + brightness in one filter chain). Closes
7892
+ the hub-cluster focal-element brightness coverage
7893
+ symmetrically: hub digit + hub-highlight disc
7894
+ (R574) now BOTH have stacked filter on hub-hover.
7895
+ Hub digit hub-hover signature post-R575 — 5 active
7896
+ axes:
7897
+ R209 scale 1.08 (geometry)
7898
+ R425 fw 700 → 800 (typography)
7899
+ R527 ls 0 → 0.3px (typography)
7900
+ R476 drop-shadow glow (paint halo)
7901
+ R575 brightness(1.15) (paint glow) ← this round
7902
+ Hub-cluster focal cluster (digit + highlight) now
7903
+ has UNIFIED 5-axis hub-hover signature reading as
7904
+ one tightly-coupled motion-coherent lift. */
6476
7905
  filter: !reducedMotion && hoveredHub
6477
7906
  ? (isLight
6478
- ? 'drop-shadow(0 0 2px rgba(16, 185, 129, 0.6))'
6479
- : 'drop-shadow(0 0 3px rgba(52, 211, 153, 0.6))')
7907
+ ? 'drop-shadow(0 0 2px rgba(16, 185, 129, 0.6)) brightness(1.15)'
7908
+ : 'drop-shadow(0 0 3px rgba(52, 211, 153, 0.6)) brightness(1.15)')
6480
7909
  : undefined,
7910
+ letterSpacing: !reducedMotion && hoveredHub ? '0.3px' : '0px',
6481
7911
  /* R425: font-weight 200ms appended so the hover fw
6482
7912
  bump 700 → 800 eases under the same cadence as
6483
7913
  R209 scale + R253 fill + R213 opacity.
6484
7914
  R476: filter 200ms appended so the new drop-
6485
- shadow glow eases at the same cadence. */
6486
- transition: 'transform 200ms ease-out, opacity 300ms ease-out, fill 200ms ease-out, font-weight 200ms ease-out, filter 200ms ease-out',
7915
+ shadow glow eases at the same cadence.
7916
+ R507: opacity 300ms (existing in list) covers
7917
+ the new focal-recede fade.
7918
+ R527: letter-spacing 200ms appended so the new
7919
+ hover-kerning bump eases at the same cadence
7920
+ as the other axes. */
7921
+ 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',
6487
7922
  fontVariantNumeric: 'tabular-nums',
6488
7923
  }}
6489
7924
  >
@@ -6529,19 +7964,214 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6529
7964
  + R213 always-mount opacity-gate + pointerEvents:none
6530
7965
  + R365 r=5.5 all preserved. data-topo-hub-highlight-
6531
7966
  opacity attr exposes the resolved value for tests. */}
6532
- <circle
6533
- cx={cx} cy={cy} r="5.5"
6534
- fill="#d1fae5"
6535
- opacity={workingCount > 0 ? 0 : 0.95}
6536
- data-topo-hub-highlight
6537
- data-topo-hub-highlight-visible={workingCount > 0 ? 'false' : 'true'}
6538
- data-topo-hub-highlight-radius="5.5"
6539
- data-topo-hub-highlight-opacity={workingCount > 0 ? 0 : 0.95}
6540
- style={{
6541
- pointerEvents: 'none',
6542
- transition: 'opacity 300ms ease-out',
6543
- }}
6544
- />
7967
+ {/* Round 508 / Loop — focal-recede pattern 2nd anchor.
7968
+ Extends R507's hub-digit recede to the hub-highlight
7969
+ circle so the hub focal CLUSTER (digit at z-top + this
7970
+ idle-state highlight beneath) recedes as a unit when
7971
+ canvas attention is elsewhere. Computed once: a single
7972
+ non-hub-hover gate drives BOTH the digit (R507) AND
7973
+ this highlight (R508) so they always co-move.
7974
+ Recede multiplies the visible opacity by 0.85 — when
7975
+ workingCount===0 the rest opacity 0.95 becomes 0.81
7976
+ during external-hover; when workingCount>0 the
7977
+ opacity stays 0 (invisible) regardless of recede.
7978
+ Additionally, when recede is active the SMIL breath
7979
+ animation halts (animate node un-mounts) so the
7980
+ receded state reads as quietly static, not pulsing
7981
+ at 0.85↔1.0 against the recede multiplier (which
7982
+ would visually conflict — competing 15% drops). On
7983
+ un-hover the animate re-mounts and breath resumes.
7984
+ data-topo-hub-recede on both digit AND highlight
7985
+ provides a stable test handle for the unified-recede
7986
+ gate.
7987
+ Composed from existing hover state vars — no new
7988
+ state. Pure paint axis. */}
7989
+ {(() => {
7990
+ const hubRecede = !!((hoveredAlias || hoveredEdgeKey || hoveredGroupLabel ||
7991
+ hoveredStatus || hoveredVendor) && !hoveredHub);
7992
+ /* Round 511 / Loop — hub-highlight gains a 3rd opacity tier
7993
+ (hovered-amplify). Pre-R511 the highlight had 2 visible
7994
+ states: rest (0.95) and recede (0.81 = 0.95 × 0.85).
7995
+ When the hub itself was hovered, the digit got R425 fw
7996
+ lift + R476 drop-shadow + R209 scale-1.08, but the
7997
+ highlight disc sibling stayed at 0.95 — the focal
7998
+ cluster lifted in 3 channels (typography/paint/scale)
7999
+ but the highlight didn't participate.
8000
+ R511 closes that asymmetry: when hoveredHub is true,
8001
+ highlight base opacity lifts to 1.0 (5% boost from
8002
+ rest 0.95). Cluster now lifts as a unit on hub-hover,
8003
+ just like it recedes as a unit on non-hub-hover
8004
+ (R508).
8005
+ 3-state opacity ladder:
8006
+ hub-hovered: baseOpacity = 1.0 (R511 NEW)
8007
+ rest (no hover): baseOpacity = 0.95 (existing)
8008
+ non-hub canvas hov: baseOpacity × 0.85 = 0.81 (R508)
8009
+ Composes cleanly: hubRecede gate requires !hoveredHub,
8010
+ so the hovered-amplify and recede states are mutually
8011
+ exclusive (they can't both fire). breathActive
8012
+ continues to halt on either non-rest state (recede OR
8013
+ hub-hover would visually compete with the 0.85↔1
8014
+ breath — clean for the unit-lift semantic too). */
8015
+ const baseOpacity = workingCount > 0 ? 0
8016
+ : hoveredHub ? 1.0
8017
+ : 0.95;
8018
+ const resolvedOpacity = hubRecede ? baseOpacity * 0.85 : baseOpacity;
8019
+ const breathActive = !reducedMotion && workingCount === 0 && !hubRecede && !hoveredHub;
8020
+ /* Round 529 / Loop — focal-amplify family 3rd anchor.
8021
+ Hub-highlight gains geometric amplify r 5.5 → 6 on
8022
+ hub-hover, mirroring R451's hub-halo r 20 → 22 hover
8023
+ pattern. Pre-R529 the highlight had paint-axis
8024
+ amplify only (R511 opacity 0.95 → 1.0 on hub-hover);
8025
+ R529 adds geometric amplify so the focal disc
8026
+ BREATHES outward on hub attention, like the halo
8027
+ does. Composes with the existing 2-axis hub-hover
8028
+ lift on this element:
8029
+ R511 opacity 0.95 → 1.0 paint (focal-amplify 1st)
8030
+ R529 r 5.5 → 6 geometry (this round)
8031
+ Implementation matches R451: CSS `r` property
8032
+ (R197/R198 idiom) for smooth interpolation. SVG
8033
+ attribute `r="5.5"` provides SSR fallback and serves
8034
+ as default; inline style.r overrides for animated
8035
+ value. transition list extends to include `r 200ms
8036
+ ease-out`, matching the fill cadence (also 200ms);
8037
+ opacity transition stays at 300ms (existing).
8038
+ r 6 sits well inside the existing visual envelope
8039
+ (next-larger sibling r=10 hub core, r=14 hub hover
8040
+ ring). The 0.5px lift is +9% radius / +19% area —
8041
+ enough to read as 'lift' without breaching the core
8042
+ boundary or invalidating overlap-test invariants.
8043
+ SMIL animate on opacity continues independently
8044
+ (animateAttr='opacity' vs CSS-property r — non-
8045
+ conflicting, same pattern R451 noted for halo).
8046
+ Focal-amplify family extension (3 anchors):
8047
+ R511 hub-highlight opacity 0.95 → 1.0
8048
+ R527 hub-digit letter-spacing 0 → 0.3px
8049
+ R529 hub-highlight radius 5.5 → 6 ← this round
8050
+ data-topo-hub-highlight-radius attr now reports the
8051
+ dynamic value (was static '5.5'). */
8052
+ const highlightR = !reducedMotion && hoveredHub ? 6 : 5.5;
8053
+ return (
8054
+ <circle
8055
+ cx={cx} cy={cy} r="5.5"
8056
+ /* Round 509 / Loop — 配色 cross-theme cleanup. Pre-R509
8057
+ the hub-highlight fill was hardcoded `#d1fae5`
8058
+ (emerald-100, a pale tone). On the light theme this
8059
+ near-white green ran against a pale background at
8060
+ 0.95 opacity — the disc was effectively invisible.
8061
+ Matches the existing R253 halo theme-inversion
8062
+ pattern (line ~6481): light theme picks the dark
8063
+ vibrant emerald (#10b981 emerald-600), dark theme
8064
+ keeps the pale emerald (#d1fae5 emerald-100). Both
8065
+ read at the same 0.95 opacity against their
8066
+ respective backdrops — light gets a saturated
8067
+ focal dot; dark keeps the soft glow signature.
8068
+ Pure paint axis (fill change only); bbox unchanged;
8069
+ R51 SVG sentinel safety untouched.
8070
+ transition list already includes `fill 200ms`?
8071
+ Actually the existing transition spec is `opacity
8072
+ 300ms ease-out` — fill change on theme toggle
8073
+ will be instant. That's acceptable: theme toggle
8074
+ is a discrete event, and the halo (line 6500)
8075
+ already snaps fill on theme toggle the same way
8076
+ (`fill 200ms ease-out` was added later to halo
8077
+ via R253). Future round could add `fill 200ms`
8078
+ to highlight too if theme-switch flicker is
8079
+ noticed. */
8080
+ fill={isLight ? '#10b981' : '#d1fae5'}
8081
+ opacity={resolvedOpacity}
8082
+ data-topo-hub-highlight
8083
+ data-topo-hub-highlight-visible={workingCount > 0 ? 'false' : 'true'}
8084
+ data-topo-hub-highlight-radius={highlightR}
8085
+ data-topo-hub-highlight-opacity={resolvedOpacity}
8086
+ data-topo-hub-highlight-breath={breathActive ? 'true' : 'false'}
8087
+ data-topo-hub-highlight-recede={hubRecede ? 'true' : 'false'}
8088
+ data-topo-hub-highlight-hovered={!reducedMotion && hoveredHub ? 'true' : 'false'}
8089
+ data-topo-hub-highlight-glow={!reducedMotion && hoveredHub ? 'true' : 'false'}
8090
+ /* Round 574 — hub-highlight joins per-element brightness
8091
+ family at 13th anchor. Stacks brightness(1.15) onto
8092
+ R532's drop-shadow filter — same R564/R570/R571/R572/
8093
+ R573 pattern (drop-shadow + brightness in one filter
8094
+ chain). Hub idle disc now has 3 active hub-hover
8095
+ axes: R511 opacity 0.95 → 1.0 + R529 r 5.5 → 6 +
8096
+ R574 brightness(1.15). data-topo-hub-highlight-
8097
+ brightness attr surfaces the lift. */
8098
+ data-topo-hub-highlight-brightness={!reducedMotion && hoveredHub ? '1.15' : '1'}
8099
+ /* Round 510 / Loop — R509 follow-on: theme-toggle fill
8100
+ ease. Pre-R510 the hub-highlight transition spec only
8101
+ listed `opacity 300ms ease-out`. When R509 introduced
8102
+ theme-conditional fill (#d1fae5 ↔ #10b981), the fill
8103
+ change SNAPPED on theme toggle because the transition
8104
+ list didn't include `fill`. R510 extends to `fill
8105
+ 200ms ease-out` so theme cycles smoothly through the
8106
+ emerald palette. 200ms timing matches the R253 halo
8107
+ fill transition (line ~6500) — both hub-cluster
8108
+ theme transitions now share a cadence so the focal
8109
+ cluster (digit + highlight + halo) eases as a unit.
8110
+ R508's recede opacity transition unchanged (300ms);
8111
+ fill is independent.
8112
+ R529: r as CSS property (R197/R198 idiom) + `r
8113
+ 200ms ease-out` appended to transition list so
8114
+ the new hub-hover radius lift (5.5 → 6) eases
8115
+ under the same fill cadence. SVG attr r="5.5"
8116
+ above provides SSR fallback; inline style.r
8117
+ wins the cascade for the dynamic value.
8118
+ R532: filter drop-shadow glow on hub-hover —
8119
+ sibling to R476 hub-digit drop-shadow at the
8120
+ same gate (hoveredHub && !reducedMotion). Two
8121
+ adjacent hub focal elements (digit + highlight
8122
+ disc) now BOTH glow on hub-hover, reading as
8123
+ one unified focal cluster. Emerald palette
8124
+ matches R476:
8125
+ light: drop-shadow(0 0 2px rgba(16,185,129,0.6)) emerald-500
8126
+ cyber: drop-shadow(0 0 3px rgba(52,211,153,0.6)) emerald-400
8127
+ filter is paint-only (bbox unchanged); SMIL
8128
+ animate on opacity continues independently
8129
+ (animateAttr='opacity' vs CSS-property filter
8130
+ — non-conflicting). transition list extends to
8131
+ 'filter 200ms ease-out' alongside fill/r.
8132
+ Drop-shadow visual-polish family extension
8133
+ (8 anchors): R476 hub-digit / R477 legend pin-
8134
+ ring / R478 recent freshness / hot edge / group
8135
+ label / zoom-state / node alias + R532 hub-
8136
+ highlight (this round). data-topo-hub-highlight-
8137
+ glow attr exposes the gate state. */
8138
+ style={{
8139
+ pointerEvents: 'none',
8140
+ r: `${highlightR}px`,
8141
+ filter: !reducedMotion && hoveredHub
8142
+ ? (isLight
8143
+ ? 'drop-shadow(0 0 2px rgba(16, 185, 129, 0.6)) brightness(1.15)'
8144
+ : 'drop-shadow(0 0 3px rgba(52, 211, 153, 0.6)) brightness(1.15)')
8145
+ : undefined,
8146
+ transition: 'opacity 300ms ease-out, fill 200ms ease-out, r 200ms ease-out, filter 200ms ease-out',
8147
+ } as React.CSSProperties}
8148
+ >
8149
+ {/* Round 497 / Loop — idle-state breath (呼吸感 theme pivot
8150
+ from the R492-R496 press-family arc). Pre-R497 the hub
8151
+ idle highlight read as a static dim disc — present but
8152
+ motionless, visually mute. R497 adds a 4s opacity breath
8153
+ (0.85 ↔ 1.0 ↔ 0.85) so the hub reads "alive but quiet"
8154
+ instead of "frozen", giving the empty-fleet state a
8155
+ subtle living signature.
8156
+ Gates:
8157
+ - !reducedMotion (R29 a11y blanket) — reducedMotion
8158
+ users see static 0.95 disc, no animate
8159
+ - workingCount === 0 — when fleet is busy, the
8160
+ highlight is invisible (opacity=0) so the animate
8161
+ would waste paint cycles. Gating saves work.
8162
+ SMIL <animate> overrides the static opacity={0.95}
8163
+ during its run; falls back to 0.95 when reducedMotion
8164
+ flips on (the animate node simply doesn't render).
8165
+ 4s cycle is long enough to feel like ambient breath
8166
+ rather than a pulse, matching the "quiet" semantic.
8167
+ data-topo-hub-highlight-breath attr exposes the
8168
+ resolved gate state for tests. */}
8169
+ {breathActive && (
8170
+ <animate attributeName="opacity" values="0.85;1;0.85" dur="4s" repeatCount="indefinite" />
8171
+ )}
8172
+ </circle>
8173
+ );
8174
+ })()}
6545
8175
  {/* R115 / Loop: hover hint ring. Stroke-only circle at r=14
6546
8176
  that fades in when the hub is hovered — the same idea
6547
8177
  R44 used for node avatars (group-hover stroke). r=14
@@ -6607,13 +8237,54 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6607
8237
  data-topo-hub-hover-ring-radius={hoveredHub ? 17 : 14}
6608
8238
  data-topo-hub-hover-ring-stroke-width="1.75"
6609
8239
  data-topo-hub-hover-ring-opacity={hoveredHub ? (isLight ? 0.85 : 0.8) : 0}
6610
- /* Round 253 / Loop: hub hover ring also gets stroke
6611
- transition for theme toggle (cyber #10b981 ↔ light
6612
- #059669). The opacity + r transitions stay for hover
6613
- lift; stroke closes the theme-snap. */
8240
+ /* Round 535 / Loop completes the hub-cluster glow
8241
+ QUARTET by adding drop-shadow to the hub-hover-ring.
8242
+ Pre-R535 the hub-hover trio (R476 digit + R532 highlight
8243
+ disc + R533 spokes) glowed in unified emerald (digit/
8244
+ disc) + cyan/teal (spokes) on hub-hover, but the ring
8245
+ itself — the outermost solid emerald boundary at
8246
+ r=14→17 — stayed flat. R535 adds the matching emerald
8247
+ drop-shadow to the ring so the FULL hub-cluster glows
8248
+ across all four concentric surfaces on hub-hover:
8249
+ digit (typography center) drop-shadow 0 0 3px emerald
8250
+ highlight disc (r=5.5/6) drop-shadow 0 0 3px emerald
8251
+ hover-ring (r=14/17) drop-shadow 0 0 3px emerald ← this round
8252
+ spokes (mesh) drop-shadow 0 0 1.5px cyan/teal
8253
+ The ring is only visible on hub-hover (opacity=0 rest);
8254
+ adding drop-shadow at the same gate means the glow shows
8255
+ the moment the ring shows — no extra state needed.
8256
+ Same R476/R532 emerald palette since the ring sits
8257
+ inside the focal-disc tier (its color is also emerald
8258
+ #059669/#10b981).
8259
+ transition list extends to include 'filter 200ms ease-
8260
+ out' alongside the existing 180ms opacity/r — slight
8261
+ cadence mismatch (180 vs 200) is acceptable; the filter
8262
+ only appears AFTER the ring fades in via opacity, and
8263
+ the 200ms vs 180ms 20ms tail difference is below
8264
+ perceptual threshold.
8265
+ Drop-shadow visual-polish family extension (11 anchors):
8266
+ the hub-cluster glow quartet (R476/R532/R533/R535) plus
8267
+ the 7 non-hub anchors (R477/R478/R479/R480/R481/R483/
8268
+ R534) makes for a thoroughly polished glow vocabulary
8269
+ across the canvas.
8270
+ data-topo-hub-hover-ring-glow attr exposes the gate
8271
+ state for tests. */
8272
+ data-topo-hub-hover-ring-glow={!reducedMotion && hoveredHub ? 'true' : 'false'}
8273
+ /* Round 579 — hub-hover-ring joins per-element brightness
8274
+ family at 18th anchor. Stacks brightness(1.15) onto
8275
+ R535's drop-shadow — same R564/R570/R571/R572/R573/R574/
8276
+ R575/R577/R578 stacked-filter pattern. Closes hub-
8277
+ cluster brightness at 4 concentric elements (digit
8278
+ R575 + highlight R574 + halo R577 + hover-ring R579). */
8279
+ data-topo-hub-hover-ring-brightness={!reducedMotion && hoveredHub ? '1.15' : '1'}
6614
8280
  style={{
6615
8281
  pointerEvents: 'none',
6616
- transition: 'opacity 180ms ease-out, r 180ms ease-out, stroke 200ms ease-out',
8282
+ filter: !reducedMotion && hoveredHub
8283
+ ? (isLight
8284
+ ? 'drop-shadow(0 0 3px rgba(16, 185, 129, 0.5)) brightness(1.15)'
8285
+ : 'drop-shadow(0 0 3px rgba(52, 211, 153, 0.5)) brightness(1.15)')
8286
+ : undefined,
8287
+ transition: 'opacity 180ms ease-out, r 180ms ease-out, stroke 200ms ease-out, filter 200ms ease-out',
6617
8288
  }}
6618
8289
  />
6619
8290
  </g>)}
@@ -6884,7 +8555,17 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6884
8555
  in when the cursor enters the node, signalling clickability
6885
8556
  (real-user feedback for the chat-popover open). Pure CSS via
6886
8557
  Tailwind group-hover, so it costs nothing per frame and
6887
- respects prefers-reduced-motion via the global media query. */}
8558
+ respects prefers-reduced-motion via the global media query.
8559
+ Round 489 / Loop — duration harmonized from 150ms → 200ms
8560
+ to join the Hero D #147 motion-coherence stack (R459-R475
8561
+ cluster surfaces + cadence-sync family). R2 originally
8562
+ picked 150ms for a "snappier feel" before the 200ms ease-
8563
+ out vocabulary was banked as the canvas-wide motion
8564
+ default. Bringing this ring into the family means hover-
8565
+ in / hover-out / cluster cadence / pip-strip transitions
8566
+ all settle on the same timing — the canvas now reads as
8567
+ one motion vocabulary instead of two competing tempos.
8568
+ 11th surface in the motion-coherence stack. */}
6888
8569
  <circle
6889
8570
  cx={pos.x}
6890
8571
  cy={pos.y}
@@ -6896,7 +8577,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6896
8577
  // exact widths and would mis-count this invisible hover
6897
8578
  // ring as a node footprint.
6898
8579
  strokeWidth="2"
6899
- className="opacity-0 group-hover:opacity-70 transition-opacity duration-150"
8580
+ className="opacity-0 group-hover:opacity-70 transition-opacity duration-200"
6900
8581
  style={{ pointerEvents: 'none' }}
6901
8582
  />
6902
8583
  {/* Round 11 / Loop: chat-focus ring — when the ChatPopover is
@@ -7380,10 +9061,38 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7380
9061
  data-edge-endpoint-active={isEndpoint ? 'true' : 'false'}
7381
9062
  data-edge-endpoint-ring-stroke-width={isEndpoint ? 2.4 : 1.6}
7382
9063
  data-edge-endpoint-ring-radius={endpointR}
9064
+ data-edge-endpoint-ring-brightness={isEndpoint ? '1.15' : '1'}
7383
9065
  style={{
7384
9066
  pointerEvents: 'none',
7385
9067
  r: `${endpointR}px`,
7386
- transition: 'opacity 180ms ease-out, stroke-width 180ms ease-out, r 180ms ease-out',
9068
+ /* R585 endpoint emphasis ring gains filter
9069
+ brightness(1.15) when an edge endpoint
9070
+ activates. 24th anchor in per-element
9071
+ brightness family, and the FOURTH edge-tier
9072
+ paint layer:
9073
+ rail (R581)
9074
+ visible path (R582)
9075
+ flow particle (R583)
9076
+ endpoint ring (R585) ← this round
9077
+ Edge-tier brightness coverage closes at 4/4
9078
+ paint surfaces. The endpoint ring is the
9079
+ edge's affinity marker at the connected
9080
+ nodes — when an edge lights up, all four
9081
+ paint surfaces brighten together for a
9082
+ single coherent edge-active gesture spanning
9083
+ the curve + the node ends.
9084
+ Endpoint ring 4-axis active signature now:
9085
+ opacity R182 0 → 0.85/0.9
9086
+ sw R233 1.6 → 2.4
9087
+ r R442 +7 → +8
9088
+ brightness R585 — → 1.15 ← this round
9089
+ Plain brightness (no url-filter stack) since
9090
+ the endpoint ring has no rest-time filter
9091
+ attribute. Inline style.filter undefined at
9092
+ rest (no flicker; opacity=0 already hides
9093
+ the ring). */
9094
+ filter: isEndpoint ? 'brightness(1.15)' : undefined,
9095
+ transition: 'opacity 180ms ease-out, stroke-width 180ms ease-out, r 180ms ease-out, filter 180ms ease-out',
7387
9096
  } as React.CSSProperties}
7388
9097
  />
7389
9098
  );
@@ -7459,8 +9168,40 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7459
9168
  data-node-status-ring={status.label}
7460
9169
  data-node-status-ring-hovered={isRingHovered ? 'true' : 'false'}
7461
9170
  data-node-status-ring-stroke-width={ringStrokeWidth}
9171
+ data-node-status-ring-brightness={isRingHovered ? '1.15' : '1'}
7462
9172
  style={{
7463
- transition: 'fill 300ms ease-out, stroke 300ms ease-out, stroke-width 300ms ease-out',
9173
+ /* R584 status ring gets brightness(1.15) on
9174
+ hover. 23rd anchor in per-element brightness
9175
+ family. Stacks with url(#topo-glow) on
9176
+ cyber+online to preserve the SVG glow filter;
9177
+ plain brightness on light or cyber+offline.
9178
+ Same R582/R583 stacked-filter pattern: inline
9179
+ style.filter overrides the attribute filter,
9180
+ stacked syntax preserves the glow on hover.
9181
+
9182
+ Per-node hover signature now 10 layers (added
9183
+ to the R438 stack):
9184
+ R26 group translateY -2px
9185
+ R217 stroke tint
9186
+ R142 drop-shadow boost
9187
+ R427 alias letter-spacing
9188
+ R428 sub-text letter-spacing
9189
+ R429 body opacity 0.94 → 1.0
9190
+ R430 hub-spoke α+
9191
+ R435 hub-spoke sw+
9192
+ R438 status-ring sw +0.5
9193
+ R584 status-ring brightness(1.15) ← this round
9194
+
9195
+ Per-element brightness family: 23 anchors.
9196
+ Stacked-filter sub-pattern: 17 anchors. */
9197
+ filter: isRingHovered
9198
+ ? (isLight
9199
+ ? 'brightness(1.15)'
9200
+ : (isOnline
9201
+ ? 'url(#topo-glow) brightness(1.15)'
9202
+ : 'brightness(1.15)'))
9203
+ : undefined,
9204
+ transition: 'fill 300ms ease-out, stroke 300ms ease-out, stroke-width 300ms ease-out, filter 300ms ease-out',
7464
9205
  }}
7465
9206
  />
7466
9207
  );
@@ -7516,6 +9257,30 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7516
9257
  const internByAlias = /书生|书小生|intern/i.test(session.alias);
7517
9258
 
7518
9259
  if (isIntern || internByAlias || vendor.logo) {
9260
+ /* Round 501 / Loop — vendor avatar inside node circles
9261
+ gains a hover-gated brightness lift. Pre-R501 the
9262
+ avatar <image> was the only per-node surface with
9263
+ NO hover treatment: R26 lifted the card, R242 tinted
9264
+ the card stroke, R427 spread the alias letter-
9265
+ spacing, R500 added the alias drop-shadow, R208
9266
+ lifted the runtime badge ring, R443 thickened
9267
+ the badge icon stroke, R177 brightened the
9268
+ halo — but the most visually-prominent element
9269
+ (the vendor logo / 书生 coin centred in each node)
9270
+ stayed paint-static. R501 closes the per-node
9271
+ hover-affordance arc by adding a 15% brightness
9272
+ lift on hover.
9273
+ Implementation: CSS filter: brightness(1.15)
9274
+ when hoveredAlias === session.alias. Pure paint
9275
+ axis on the <image> element — no geometry change,
9276
+ no bbox shift. Modern-browser supported (Chrome 64+
9277
+ / FF 56+ / Safari 9.1+).
9278
+ Hits 节点视觉 theme. data-node-avatar-hovered
9279
+ attr surfaces the gate for tests.
9280
+ Gated on !reducedMotion as a courtesy (brightness
9281
+ transition < ~50ms still feels instant; the gate
9282
+ avoids the transition cycle for a11y users). */
9283
+ const isAvatarHovered = !reducedMotion && hoveredAlias === session.alias;
7519
9284
  return (
7520
9285
  <image
7521
9286
  href={vendor.logo ?? '/intern_avatar.png'}
@@ -7524,9 +9289,39 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7524
9289
  width={size}
7525
9290
  height={size}
7526
9291
  preserveAspectRatio="xMidYMid meet"
9292
+ data-node-avatar={session.alias}
9293
+ data-node-avatar-hovered={isAvatarHovered ? 'true' : 'false'}
9294
+ style={{
9295
+ filter: isAvatarHovered ? 'brightness(1.15)' : undefined,
9296
+ transition: 'filter 200ms ease-out',
9297
+ }}
7527
9298
  />
7528
9299
  );
7529
9300
  }
9301
+ /* Round 558 / Loop — closes the per-node avatar hover-
9302
+ affordance arc by extending R501's brightness lift
9303
+ (image-branch only) to the TWO remaining avatar
9304
+ variants: vendor monogram + prefix-group hue-hashed
9305
+ initial fallback. Pre-R558 only the vendor.logo
9306
+ image branch lifted on hover; the other two
9307
+ variants stayed paint-static under attention.
9308
+
9309
+ Per-node avatar hover-brightness family (3 anchors,
9310
+ all gated on !reducedMotion && hoveredAlias matches):
9311
+ R501 vendor.logo image filter on <image>
9312
+ R558 vendor monogram filter on wrapping <g>
9313
+ R558 prefix-group fallback filter on wrapping <g>
9314
+
9315
+ Implementation: each fallback branch returns a
9316
+ fragment with <circle> + <text> as siblings.
9317
+ Wrapping them in a single <g> with the filter
9318
+ centralizes the paint axis. Same brightness(1.15)
9319
+ value as R501 for cross-branch consistency. Same
9320
+ transition cadence (filter 200ms ease-out).
9321
+
9322
+ data-node-avatar-monogram-hovered + -fallback-
9323
+ hovered attrs surface the gates for tests. */
9324
+ const isAvatarFallbackHovered = !reducedMotion && hoveredAlias === session.alias;
7530
9325
  if (vendor.id !== 'unknown') {
7531
9326
  // Known model house, logo asset not in public/vendors/
7532
9327
  // yet — vendor-tinted monogram stands in.
@@ -7546,7 +9341,14 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7546
9341
  where less visual weight signals "we don't know
7547
9342
  what this is" appropriately. */
7548
9343
  return (
7549
- <>
9344
+ <g
9345
+ data-node-avatar-monogram={session.alias}
9346
+ data-node-avatar-monogram-hovered={isAvatarFallbackHovered ? 'true' : 'false'}
9347
+ style={{
9348
+ filter: isAvatarFallbackHovered ? 'brightness(1.15)' : undefined,
9349
+ transition: 'filter 200ms ease-out',
9350
+ }}
9351
+ >
7550
9352
  <circle cx={pos.x} cy={pos.y} r={ar} fill={vendor.mono.bg} stroke={vendor.mono.ring} strokeWidth="1.5" />
7551
9353
  {/* Round 284 / Loop: known-vendor monogram letter
7552
9354
  swaps fontFamily monospace → system sans-serif.
@@ -7578,14 +9380,21 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7578
9380
  >
7579
9381
  {vendor.initial}
7580
9382
  </text>
7581
- </>
9383
+ </g>
7582
9384
  );
7583
9385
  }
7584
9386
  // Round 106 (issue #83): hue keyed to the prefix group,
7585
9387
  // not the full alias — every 通信* node shares one color.
7586
9388
  const c = aliasAvatarColors(groupKeys[session.alias] || session.alias);
7587
9389
  return (
7588
- <>
9390
+ <g
9391
+ data-node-avatar-fallback={session.alias}
9392
+ data-node-avatar-fallback-hovered={isAvatarFallbackHovered ? 'true' : 'false'}
9393
+ style={{
9394
+ filter: isAvatarFallbackHovered ? 'brightness(1.15)' : undefined,
9395
+ transition: 'filter 200ms ease-out',
9396
+ }}
9397
+ >
7589
9398
  <circle cx={pos.x} cy={pos.y} r={ar} fill={c.bg} stroke={c.ring} strokeWidth="1" />
7590
9399
  <text
7591
9400
  x={pos.x}
@@ -7599,7 +9408,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7599
9408
  >
7600
9409
  {aliasInitial(session.alias)}
7601
9410
  </text>
7602
- </>
9411
+ </g>
7603
9412
  );
7604
9413
  })()}
7605
9414
  {/* Issue #96: runtime badge — small corner glyph marking the
@@ -7630,7 +9439,73 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7630
9439
  // badge-active exposes the gate for tests.
7631
9440
  const isNodeActive = !reducedMotion && hoveredAlias === session.alias;
7632
9441
  return (
7633
- <g style={{ pointerEvents: 'none' }}>
9442
+ /* Round 559 / Loop — runtime badge outer <g> picks up
9443
+ a drop-shadow glow on node hover, using the runtime's
9444
+ own identity color (rt.color, hex). 16th anchor in
9445
+ the drop-shadow visual-polish family. Pairs with
9446
+ existing R208 ring r-lift + R443 icon sw-lift for
9447
+ a 3-axis runtime-badge hover signature now spanning
9448
+ geometry + stroke + paint glow:
9449
+ R208 ring r 7 → 8 (online)
9450
+ R208 ring sw 1.5 → 2
9451
+ R443 icon sw 2.4 → 2.8
9452
+ R559 outer filter none → drop-shadow(rt.color) ← this round
9453
+ Filter on the OUTER <g> applies uniformly to both
9454
+ the ring <circle> and the inner icon <path> —
9455
+ single paint-axis lift covers both layers in one
9456
+ motion-coherent gesture.
9457
+ Hue: `${rt.color}99` hex+alpha (60%) — rt.color is
9458
+ 6-digit hex (#a78bfa / #38bdf8 / #34d399 / #fbbf24
9459
+ per lib/vendorIdentity.ts), so hex+alpha concat is
9460
+ safe (banked R541 pattern: hex sources use hex+
9461
+ alpha; only hsl/color()/dynamic sources need color-
9462
+ mix).
9463
+ 2px blur reads tight on a small badge (r=7 online
9464
+ / r=5.5 offline). transition list adds 'filter
9465
+ 150ms ease-out' matching the R208 ring r/sw cadence
9466
+ at this surface.
9467
+ Drop-shadow visual-polish family extension (16
9468
+ anchors now): R476/R477/R478/R479/R480/R481 +
9469
+ R500/R532-R536/R537/R538/R540/R543-R546/R550 +
9470
+ R559 (this round).
9471
+ data-runtime-badge-glow attr surfaces the gate
9472
+ for tests. */
9473
+ <g
9474
+ data-runtime-badge-glow={isNodeActive ? 'true' : 'false'}
9475
+ data-runtime-badge-brightness={isNodeActive ? '1.15' : '1'}
9476
+ style={{
9477
+ pointerEvents: 'none',
9478
+ /* R586 — runtime badge outer <g> stacks
9479
+ brightness(1.15) onto the existing R559
9480
+ drop-shadow on node hover. 25th anchor in
9481
+ per-element brightness family, 18th in
9482
+ stacked-filter sub-pattern.
9483
+
9484
+ Runtime badge hover signature now CLOSED
9485
+ at 4 axes (geometry + stroke + paint glow
9486
+ + paint brightness):
9487
+ R208 ring r 7 → 8
9488
+ R208 ring sw 1.5 → 2
9489
+ R443 icon sw 2.4 → 2.8
9490
+ R559 outer filter none → drop-shadow(rt.color)
9491
+ R586 outer filter stack brightness(1.15) ← this round
9492
+
9493
+ The drop-shadow + brightness stack is the
9494
+ banked R564/R570 "halo + glow" pattern —
9495
+ drop-shadow paints the colored halo, brightness
9496
+ lifts the underlying paint (ring stroke +
9497
+ icon path both gain ~15% luminance). Single
9498
+ CSS filter chain on the outer <g> covers
9499
+ both child layers uniformly.
9500
+
9501
+ Same R208/R443/R559 150ms cadence preserved
9502
+ via the existing transition. */
9503
+ filter: isNodeActive
9504
+ ? `drop-shadow(0 0 2px ${rt.color}99) brightness(1.15)`
9505
+ : undefined,
9506
+ transition: 'filter 150ms ease-out',
9507
+ }}
9508
+ >
7634
9509
  <circle
7635
9510
  cx={bx} cy={by} r={br}
7636
9511
  fill={pal.containerBg}
@@ -7947,6 +9822,43 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7947
9822
  R211 fill 300ms + R305 letter-spacing 200ms
7948
9823
  transition list preserved; only the
7949
9824
  conditional gets a middle case. */}
9825
+ {/* Round 500 / Loop — milestone round, opens
9826
+ per-node alias drop-shadow polish. Extends the
9827
+ R476-R481 drop-shadow visual-polish family to a
9828
+ 7th anchor: hovered alias text gains a soft
9829
+ status-coloured text-glow. Pre-R500 hover on
9830
+ a node triggered card-lift (R26 translateY) +
9831
+ card-stroke (R242 tint) + alias letter-spacing
9832
+ (R427 0.3px tier) but the alias TEXT itself had
9833
+ no paint-axis cue beyond fill (R211). R500 adds
9834
+ a drop-shadow on the text glyph itself, so the
9835
+ identity glyph itself lights up under attention
9836
+ — matching the R476 idiom (hub-digit emerald
9837
+ glow on hover) at the per-node identity scope.
9838
+ 2px blur radius at 50% alpha — subtler than the
9839
+ R476 hub-digit (3px at 60%) because the alias
9840
+ text is smaller and more numerous (1 per node)
9841
+ so an aggressive glow would multiply into
9842
+ visual noise. Status-coloured (status.text) so
9843
+ the glow inherits the node's working/idle/
9844
+ offline palette — green/cyan/gray respectively.
9845
+ Drop-shadow visual-polish family — 7 anchors:
9846
+ R476 hub digit hover-gated emerald
9847
+ R477 legend pin-ring pin-gated row.fill
9848
+ R478 recent-row pip fresh-gated cyan
9849
+ R479 group-label text pin-gated cyan
9850
+ R480 hot-lane edge hot-gated amber
9851
+ R481 zoom-state minimap zoom-gated cyan
9852
+ R500 node alias text hover-gated status.text ← this round
9853
+ Filter is paint-only; bbox unchanged; overlap-
9854
+ test invariants hold (R51 selector gated to
9855
+ g[data-node] descendants with strokeWidth
9856
+ sentinels; text element doesn't carry stroke).
9857
+ transition list extends to include 'filter
9858
+ 200ms ease-out' alongside the existing fill
9859
+ 300ms + letter-spacing 200ms tweens.
9860
+ data-node-alias-glow attr surfaces the hover
9861
+ gate for tests. */}
7950
9862
  <text
7951
9863
  x="0" y="1" textAnchor="middle"
7952
9864
  fill={status.text}
@@ -7954,12 +9866,43 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7954
9866
  data-node-alias-text={session.alias}
7955
9867
  data-node-alias-chat-target={chatAlias === session.alias ? 'true' : 'false'}
7956
9868
  data-node-alias-hovered={hoveredAlias === session.alias ? 'true' : 'false'}
9869
+ data-node-alias-glow={!reducedMotion && hoveredAlias === session.alias ? 'true' : 'false'}
7957
9870
  style={{
7958
- transition: 'fill 300ms ease-out, letter-spacing 200ms ease-out',
9871
+ transition: 'fill 300ms ease-out, letter-spacing 200ms ease-out, filter 200ms ease-out',
7959
9872
  letterSpacing:
7960
9873
  chatAlias === session.alias ? '0.5px' :
7961
9874
  hoveredAlias === session.alias ? '0.3px' : '0px',
9875
+ /* Round 564 / Loop — alias text filter stacks
9876
+ brightness(1.15) on top of R500's drop-shadow
9877
+ on hover. Mirrors R542 pressure-seg pattern
9878
+ (brightness + drop-shadow in one stacked
9879
+ filter declaration). Pre-R564 hover added
9880
+ only a drop-shadow halo around the glyph;
9881
+ post-R564 the glyph ALSO brightens, so the
9882
+ identity text reads as both "glowing" AND
9883
+ "lit up" under attention — dual paint axes
9884
+ through one filter chain.
9885
+ CSS filter supports multiple functions
9886
+ applied left-to-right. brightness(1.15)
9887
+ lifts the per-status text color (status.text:
9888
+ green/teal/slate per tier) by 15%; the drop-
9889
+ shadow then paints the outer halo in the
9890
+ status-tier hue. Together: the alias glyph
9891
+ both intensifies its identity color AND
9892
+ radiates outward in that same color.
9893
+ Same +15% brightness as R501 vendor logo
9894
+ avatar (banked per-node hover-brightness
9895
+ pattern). Consistent +15% across all per-
9896
+ node identity surfaces (logo, monogram,
9897
+ fallback avatar from R558, AND now alias
9898
+ text). Cross-element brightness consistency.
9899
+ data-node-alias-brightness attr surfaces
9900
+ the lift for tests. */
9901
+ filter: !reducedMotion && hoveredAlias === session.alias
9902
+ ? `drop-shadow(0 0 2px ${status.text}80) brightness(1.15)`
9903
+ : undefined,
7962
9904
  }}
9905
+ data-node-alias-brightness={!reducedMotion && hoveredAlias === session.alias ? '1.15' : '1'}
7963
9906
  >
7964
9907
  {truncate(session.alias, fullMax)}
7965
9908
  </text>
@@ -7999,6 +9942,31 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7999
9942
  alias > status hierarchy holds at the type
8000
9943
  level. data-node-sub-text-font-weight attr
8001
9944
  exposes the value for tests. */}
9945
+ {/* Round 567 / Loop — node sub-text joins the per-
9946
+ node hover-brightness consistency family. Pre-
9947
+ R567 sub-text had only fill brighten (R211) +
9948
+ ls 0→0.2 (R428); the alias text above lifted
9949
+ via R500 drop-shadow + R564 brightness(1.15)
9950
+ stacked. R567 adds brightness(1.15) to sub-
9951
+ text on the same isNodeActive gate so it
9952
+ chromatically lifts together with the alias.
9953
+ Per-node hover-brightness consistency family
9954
+ — 6 anchors at uniform +15%:
9955
+ R501 vendor.logo image filter on <image>
9956
+ R558 vendor monogram filter on <g>
9957
+ R558 prefix-group fallback filter on <g>
9958
+ R564 alias text (stacked w/ DS) brightness(1.15)
9959
+ R567 node sub-text brightness(1.15) ← this round
9960
+ (+ R559 runtime badge drop-shadow tier-color glow)
9961
+ Now every per-node identity surface (3 avatar
9962
+ variants + alias + sub-text + badge) lifts
9963
+ together on node hover with consistent visual
9964
+ response.
9965
+ Pure paint axis; bbox unchanged. transition
9966
+ list extends to include 'filter 200ms ease-
9967
+ out' matching R428 ls cadence at this scope.
9968
+ data-node-sub-text-brightness attr exposes
9969
+ the lift for tests. */}
8002
9970
  <text
8003
9971
  x="0" y={subY} textAnchor="middle"
8004
9972
  fill={status.primary}
@@ -8007,9 +9975,13 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8007
9975
  data-node-sub-text={session.alias}
8008
9976
  data-node-sub-text-hovered={hoveredAlias === session.alias ? 'true' : 'false'}
8009
9977
  data-node-sub-text-font-weight="500"
9978
+ data-node-sub-text-brightness={!reducedMotion && hoveredAlias === session.alias ? '1.15' : '1'}
8010
9979
  style={{
8011
- transition: 'fill 300ms ease-out, letter-spacing 200ms ease-out',
9980
+ transition: 'fill 300ms ease-out, letter-spacing 200ms ease-out, filter 200ms ease-out',
8012
9981
  letterSpacing: hoveredAlias === session.alias ? '0.2px' : '0px',
9982
+ filter: !reducedMotion && hoveredAlias === session.alias
9983
+ ? 'brightness(1.15)'
9984
+ : undefined,
8013
9985
  }}
8014
9986
  >
8015
9987
  {status.label}{isOnline && sseCountFor != null ? ` sse:${sseCountFor}` : ''}
@@ -8486,7 +10458,55 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8486
10458
  200ms ease-out' alongside R345's ls + R55's fill
8487
10459
  200ms. data-recent-panel-title-fw exposes the
8488
10460
  resolved weight for tests. */}
8489
- <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>
10461
+ {/* Round 550 / Loop drop-shadow visual-polish family
10462
+ extends to a 14th anchor: the recent-panel header
10463
+ title gains a soft pal.legendAccent glow when the
10464
+ panel has an active row (activeEdgeKey). Pre-R550
10465
+ the title's state-flip on active was 2-axis (R482
10466
+ fw 700→800 + R345 ls 0.3→0.4 when panel-hovered);
10467
+ R550 adds the paint axis so the title brightens
10468
+ paint-wise alongside the typographic tightening when
10469
+ a row inside its panel is locked.
10470
+
10471
+ Hue: pal.legendAccent + hex alpha '80' (~50%) — same
10472
+ strength as R479 group-label pin-glow at the parent
10473
+ panel-title scope. 2px blur reads soft; cyan accent
10474
+ ties the title visually to the active row's pin
10475
+ colour (cyber: cyan-300 / light: teal-600). Hex+alpha
10476
+ concat safe — pal.legendAccent is '#67e8f9' (cyber)
10477
+ or '#0d9488' (light), both 6-digit hex (banked R541:
10478
+ hex sources use hex+alpha; only hsl/color()/dynamic
10479
+ sources need color-mix).
10480
+
10481
+ Drop-shadow visual-polish family extension (14
10482
+ anchors now):
10483
+ R476 hub digit hover-gated emerald
10484
+ R477 legend pin-ring pin-gated row.fill
10485
+ R478 recent-row pip freshness cyan
10486
+ R479 group-label text pin-gated cyan
10487
+ R532-R536 hub-cluster glow QUINTET
10488
+ R537 legend swatch hover/pin row.fill
10489
+ R538 group-label hover-precedence
10490
+ R540 edge-badge text pin-gated cyan
10491
+ R543-R546 pin-active pill 4-variant arc
10492
+ R550 recent-panel title pin-gated cyan ← this round
10493
+ R550 legend-panel title pin-gated cyan ← sibling (next text below)
10494
+
10495
+ filter is paint-only; bbox unchanged; overlap-test
10496
+ invariants hold. transition list extends to include
10497
+ 'filter 200ms ease-out' alongside R345 ls + R482 fw
10498
+ + R55 fill 200ms — one motion-coherent 3-axis active-
10499
+ state lift.
10500
+ data-recent-panel-title-glow attr exposes the gate
10501
+ state for tests. */}
10502
+ {/* Round 573 / Loop — recent panel-title joins per-element
10503
+ brightness family at 11th anchor. Stacks brightness(1.15)
10504
+ onto R550's active-gated drop-shadow. Mirrors R572
10505
+ panel-row text pattern at the panel-TITLE tier — both
10506
+ panels now have brightness in their active signature
10507
+ at BOTH chrome tiers (title + row text), completing
10508
+ the panel paint-axis cascade. */}
10509
+ <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>
8490
10510
  {/* R96: header count now matches what the rows show. Pre-R96
8491
10511
  this read "X msgs" off the raw messages array, but the
8492
10512
  rows below render DEDUPED flowLinks — so a fleet with 10
@@ -8561,21 +10581,19 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8561
10581
  textAnchor="end"
8562
10582
  fontSize="10"
8563
10583
  fontFamily="monospace"
8564
- // Round 349 / Loop: editorial letter-spacing 0.2 on the
8565
- // recent-signal panel header count. Sits one tier below
8566
- // the R301 panel title letterSpacing="0.3" so the panel
8567
- // header reads as a 2-step hierarchy (title 0.3 / count
8568
- // 0.2). Sibling change on the legend panel count below
8569
- // closes the panel-pair editorial symmetry. Joins the
8570
- // R285 / R289 / R301 / R302 / R304 / R325 editorial-
8571
- // letterspacing tier at the panel-summary scope. The
8572
- // R162 freshness fill, R225 tabular-nums, R311 fw=600,
8573
- // R336 unit-tspan opacity-0.7 split all preserved —
8574
- // the tier propagates to all descendant tspans via
8575
- // SVG inheritance. data-recent-panel-count-letter-
8576
- // spacing exposes the value for tests.
8577
- letterSpacing="0.2"
8578
- data-recent-panel-count-letter-spacing="0.2"
10584
+ /* Round 566 / Loop recent-panel-count gains hover-
10585
+ state letter-spacing tween (0.2 0.4 on hovered-
10586
+ Panel === 'recent'). Pairs with R424 fw 600→700
10587
+ on the same gate. Count now has 2-axis hover
10588
+ signature (fw + ls), matching the panel title's
10589
+ R345 ls + R482 fw lift pattern at the panel-
10590
+ header data-tspan scope. R349 editorial 0.2
10591
+ baseline preserved at rest only hover lifts.
10592
+ Sibling treatment on the legend-panel count
10593
+ below closes the panel-pair symmetry. */
10594
+ letterSpacing={hoveredPanel === 'recent' ? '0.4' : '0.2'}
10595
+ data-recent-panel-count-letter-spacing={hoveredPanel === 'recent' ? '0.4' : '0.2'}
10596
+ style={{ transition: 'letter-spacing 200ms ease-out' }}
8579
10597
  >
8580
10598
  {/* Round 225 / Loop: tabular-nums on the panel-header
8581
10599
  flow-count tspan. The "{N} flows" string lives in
@@ -9230,11 +11248,46 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
9230
11248
  tier without disturbing the surrounding family
9231
11249
  baseline. data-recent-row-text-font-weight attr
9232
11250
  exposes the value for tests. */
9233
- fontWeight="500"
11251
+ /* Round 530 / Loop — extends hover-fw family
11252
+ (R416/R420/R425/R520/R521/R522, 6 anchors) to
11253
+ a 7th anchor: recent-row alias text gains
11254
+ fontWeight 500 → 600 on (isRowHovered ||
11255
+ isRowPinned). Pre-R530 R363 set fw=500
11256
+ statically; hover/pin lifted other axes
11257
+ (R55 fill brighten / R434 letter-spacing
11258
+ 3-tier / R143 translateY / R104 row bg-
11259
+ tint / R474 cadence) but the fw stayed
11260
+ flat — same asymmetry R520 closed at the
11261
+ +N more footer.
11262
+ R530 mirrors R520's pattern at the row-
11263
+ text scope. Hover OR pin (isRowActive
11264
+ union) lifts fw to 600, matching the count
11265
+ tspan's cold-state tier (R320 fw=600), so
11266
+ on active state the alias label reads at
11267
+ the same data tier as the count it sits
11268
+ next to. Inner count tspan has its own
11269
+ explicit fontWeight (600 or 700 per R320/
11270
+ R445) so parent fw lift doesn't bleed
11271
+ (inheritance overridden).
11272
+ Hover-fw family extension (7 anchors):
11273
+ R416 chip-row count digit
11274
+ R420 chrome zoom-level
11275
+ R425 hub-center digit
11276
+ R520 +N more flows footer
11277
+ R521 chrome nodeSize S/M/L inactive
11278
+ R522 chrome layout Ring/Grid inactive
11279
+ R530 recent-row alias text ← this round
11280
+ transition list extends to include
11281
+ 'font-weight 200ms ease-out', matching the
11282
+ R474 cadence of the existing fill +
11283
+ letter-spacing axes on this element.
11284
+ data-recent-row-text-font-weight attr
11285
+ flips '500' → '600' on isRowActive. */
11286
+ fontWeight={(isRowHovered || isRowPinned) ? '600' : '500'}
9234
11287
  data-recent-row-text={link.key}
9235
11288
  data-recent-row-text-pinned={isRowPinned ? 'true' : 'false'}
9236
11289
  data-recent-row-text-hovered={!isRowPinned && isRowHovered ? 'true' : 'false'}
9237
- data-recent-row-text-font-weight="500"
11290
+ data-recent-row-text-font-weight={(isRowHovered || isRowPinned) ? '600' : '500'}
9238
11291
  /* Round 434 / Loop: recent-signal row text extends
9239
11292
  from R220's pin-only letter-spacing (0 → 0.5 on
9240
11293
  isRowPinned) to a 3-tier scale matching R433
@@ -9283,11 +11336,49 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
9283
11336
  spacing values unchanged; R363 fw + R55 fill
9284
11337
  brighten unchanged — only the timing axis
9285
11338
  shifts. */
11339
+ /* Round 568 / Loop — extends the drop-shadow visual-
11340
+ polish family (16 anchors after R559) to a 17th
11341
+ anchor at the recent-row text scope. Adds a soft
11342
+ pal.legendAccent glow on isRowHovered || isRow-
11343
+ Pinned, completing the row's hover/pin signature
11344
+ at 4 paint+typography axes:
11345
+ R55 fill brighten (fill)
11346
+ R434 letter-spacing 3-tier (typography kerning)
11347
+ R530 fontWeight 500 → 600 (typography weight)
11348
+ R568 drop-shadow glow (paint glow) ← this round
11349
+ Mirror of R550 panel-title pin-gated glow pattern,
11350
+ applied at the panel-ROW tier rather than panel-
11351
+ TITLE tier. Hue: pal.legendAccent + hex alpha 80
11352
+ (~50%) — same strength as R479 group-label /
11353
+ R550 panel-title glows for cross-element
11354
+ consistency. 2px blur (smaller than R478 pip's
11355
+ 3px since text is fontSize 9 and a heavier blur
11356
+ would bleed into adjacent row text); blur 2px
11357
+ keeps the glow tight to the row's text glyphs.
11358
+ transition list extends to include 'filter
11359
+ 200ms ease-out' matching the R474 200ms cadence
11360
+ of the existing 3 axes.
11361
+ data-recent-row-text-glow attr surfaces the
11362
+ gate for tests. */
9286
11363
  data-recent-row-text-transition="200ms"
11364
+ data-recent-row-text-glow={(isRowHovered || isRowPinned) ? 'true' : 'false'}
11365
+ /* Round 572 / Loop — per-element brightness family
11366
+ 9th anchor at recent-row text scope. Stacks
11367
+ brightness(1.15) onto R568's drop-shadow in
11368
+ one filter chain (same R564/R570/R571 pattern).
11369
+ Glyph BOTH glows (R568 drop-shadow halo) AND
11370
+ brightens (R572 inner lift) simultaneously.
11371
+ Cross-element brightness consistency: same +15%
11372
+ across alias / sub-text / edge-badge / group-
11373
+ label / and now recent-row text. */
11374
+ data-recent-row-text-brightness={(isRowHovered || isRowPinned) ? '1.15' : '1'}
9287
11375
  style={{
9288
- transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out',
11376
+ transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out, font-weight 200ms ease-out, filter 200ms ease-out',
9289
11377
  letterSpacing: isRowPinned ? '0.5px' :
9290
11378
  isRowHovered ? '0.25px' : '0px',
11379
+ filter: (isRowHovered || isRowPinned)
11380
+ ? `drop-shadow(0 0 2px ${pal.legendAccent}80) brightness(1.15)`
11381
+ : undefined,
9291
11382
  }}
9292
11383
  >
9293
11384
  {/* R138 / Loop: typography unification with the rest
@@ -9368,12 +11459,30 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
9368
11459
  under the same R320 fill cadence. data-
9369
11460
  recent-row-count-pinned attr exposes the
9370
11461
  pin gate for tests. */}
11462
+ {/* Round 498 / Loop — hot-count subtle pulse. Pre-
11463
+ R498 the hot row count signaled via color (R127
11464
+ amber fill) + weight (R320 fw-700) + (R445 pin
11465
+ lift) but stayed visually motionless. R498 adds
11466
+ a 3s opacity breath (0.85↔1.0) on the digit when
11467
+ isHot && !reducedMotion — gentle "alive" signal
11468
+ on the lane carrying ≥ 10 messages, drawing
11469
+ glance without becoming noisy. Sibling of R497
11470
+ hub-idle-breath in the 呼吸感 theme arc; same
11471
+ 0.85↔1.0 amplitude. Class adds an animation-
11472
+ only paint axis; no layout / bbox change. R29
11473
+ blanket also catches `animation-duration` for
11474
+ reducedMotion users, but the component-side
11475
+ gate makes the intent explicit and avoids
11476
+ a node tree thrash for those users (className
11477
+ stays absent rather than present-but-paused). */}
9371
11478
  <tspan
9372
11479
  fill={isHot ? hotStroke : undefined}
9373
11480
  fontWeight={(isHot || isRowPinned) ? '700' : '600'}
11481
+ className={isHot && !reducedMotion ? 'anet-recent-hot-pulse' : undefined}
9374
11482
  data-recent-row-count
9375
11483
  data-recent-row-count-pinned={isRowPinned ? 'true' : 'false'}
9376
11484
  data-recent-row-count-font-weight={(isHot || isRowPinned) ? '700' : '600'}
11485
+ data-recent-row-count-hot-pulse={isHot && !reducedMotion ? 'true' : 'false'}
9377
11486
  {...(isHot ? { 'data-recent-row-count-hot': 'true' } : {})}
9378
11487
  style={{
9379
11488
  transition: 'fill 300ms ease-out, font-weight 200ms ease-out',
@@ -9617,6 +11726,39 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
9617
11726
  stays as is, so the rest-vs-hover delta still
9618
11727
  reads clearly. data-recent-panel-more-font-weight
9619
11728
  attr exposes the value for tests. */}
11729
+ {/* Round 520 / Loop — extends the `+N more flows` footer
11730
+ to a 5-axis hover signature by adding fontWeight
11731
+ 500 → 600 on hover. Pre-R520 the footer carried 4
11732
+ hover axes:
11733
+ R195 fill legendText → legendAccent
11734
+ R325 letter-spacing 0.2 → 0.3px (R344 tween)
11735
+ R325 opacity 0.55 → 0.85
11736
+ R133 underline none → underline
11737
+ R368 had set fontWeight 500 statically as a sibling
11738
+ to R363/R364/R366 small-text fw lift family — but
11739
+ the footer's hover state didn't carry a fontWeight
11740
+ DELTA the way other interactive surfaces do (chip-
11741
+ row counts R416, chrome zoom-level R420, hub digit
11742
+ R425). R520 adds the missing weight axis: fw 500
11743
+ → 600 on hover, so the footer reads "thickening AND
11744
+ lighting up" under cursor — same idiom as the
11745
+ chrome zoom-level R420 / chip-row digit R416 hover-
11746
+ bold pattern, applied at the panel nav-action
11747
+ surface.
11748
+ data-recent-panel-more-font-weight attr value
11749
+ flips from '500' → '600' on hover (was static
11750
+ '500' pre-R520).
11751
+ Bonus closure — R475 panel-text cadence: pre-R520
11752
+ the footer's transition list had `opacity 150ms`
11753
+ while R475 unified panel-text transitions at
11754
+ 200ms. R518 closed the same gap at legend-count.
11755
+ R520 closes the LAST panel-text 150ms holdout
11756
+ here AND adds the new font-weight 200ms axis. All
11757
+ 4 transition properties (opacity / fill / letter-
11758
+ spacing / font-weight) now uniform 200ms at the
11759
+ footer — same cadence as legend-label / legend-
11760
+ count / recent-row alias / recent-row count /
11761
+ group-label. */}
9620
11762
  <text
9621
11763
  x="115" y="82"
9622
11764
  textAnchor="middle"
@@ -9624,14 +11766,15 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
9624
11766
  fontSize="9"
9625
11767
  fontFamily="monospace"
9626
11768
  fontStyle="italic"
9627
- fontWeight="500"
11769
+ fontWeight={hoveredRecentMore ? '600' : '500'}
9628
11770
  letterSpacing={hoveredRecentMore ? '0.3' : '0.2'}
9629
11771
  opacity={hoveredRecentMore ? 0.85 : 0.55}
9630
11772
  textDecoration={hoveredRecentMore ? 'underline' : 'none'}
9631
11773
  data-recent-panel-more={moreCount}
9632
11774
  data-recent-panel-more-hovered={hoveredRecentMore ? 'true' : 'false'}
9633
- data-recent-panel-more-font-weight="500"
9634
- style={{ transition: 'opacity 150ms ease-out, fill 200ms ease-out, letter-spacing 200ms ease-out' }}
11775
+ data-recent-panel-more-font-weight={hoveredRecentMore ? '600' : '500'}
11776
+ data-recent-panel-more-transition="200ms"
11777
+ style={{ transition: 'opacity 200ms ease-out, fill 200ms ease-out, letter-spacing 200ms ease-out, font-weight 200ms ease-out' }}
9635
11778
  >
9636
11779
  {`+ ${moreCount}`}
9637
11780
  <tspan opacity="0.7" data-recent-panel-more-unit>{` more flow${moreCount === 1 ? '' : 's'}`}</tspan>
@@ -9738,7 +11881,20 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
9738
11881
  data-legend-panel-title-fw + -active exposed for tests. */}
9739
11882
  {/* R345 sibling — legend panel title same hover letter-
9740
11883
  spacing tween 0.3 → 0.4 on panel hover. */}
9741
- <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>
11884
+ {/* Round 550 sibling legend-panel header title mirrors
11885
+ the recent-panel title above: drop-shadow glow on
11886
+ pin-gated active state (pinnedStatus). Same hue
11887
+ (pal.legendAccent + hex alpha 80), same 2px blur,
11888
+ same 200ms ease-out cadence. Family lifts to 15
11889
+ anchors with this sibling (counted as R550-sibling
11890
+ for accounting parity with R532-R536 hub-cluster
11891
+ glow quintet pattern — two co-shipping anchors
11892
+ under a single round number).
11893
+ data-legend-panel-title-glow attr added. */}
11894
+ {/* Round 573 sibling — legend panel-title 12th anchor.
11895
+ Same stacked filter pattern at the legend-panel-title
11896
+ scope. Both panel titles now lift in lockstep. */}
11897
+ <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>
9742
11898
  {/* Round 257 / Loop: legend panel header count picks up the
9743
11899
  symmetric 13L/13R inner-padding pattern from the recent-
9744
11900
  signal panel. Pre-R257 the legend header was 13px from
@@ -9815,11 +11971,19 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
9815
11971
  // title 0.3. Pairs with the recent-signal panel count
9816
11972
  // letter-spacing above so the two corner panels' header
9817
11973
  // typography stays editorially symmetric.
9818
- letterSpacing="0.2"
11974
+ /* Round 566 / Loop — legend panel-count gains hover-state
11975
+ letter-spacing tween (0.2 → 0.4 on hoveredPanel ===
11976
+ 'legend'). Pairs with existing R310 fw 600→700 on the
11977
+ same gate — count now has 2-axis hover signature (fw
11978
+ + ls), matching the panel title's R345 ls + R482 fw
11979
+ lift pattern at the panel-header data-tspan scope.
11980
+ Hover-letter-spacing family extension (R566 = 2 sibling
11981
+ anchors at recent + legend panel-count). */
11982
+ letterSpacing={hoveredPanel === 'legend' ? '0.4' : '0.2'}
9819
11983
  data-legend-panel-count
9820
- data-legend-panel-count-letter-spacing="0.2"
11984
+ data-legend-panel-count-letter-spacing={hoveredPanel === 'legend' ? '0.4' : '0.2'}
9821
11985
  style={{
9822
- transition: 'fill 200ms ease-out, font-weight 200ms ease-out',
11986
+ transition: 'fill 200ms ease-out, font-weight 200ms ease-out, letter-spacing 200ms ease-out',
9823
11987
  fontVariantNumeric: 'tabular-nums',
9824
11988
  }}
9825
11989
  >{sessions.length}<tspan opacity="0.7" data-legend-panel-count-unit> node{sessions.length === 1 ? '' : 's'}</tspan></text>
@@ -9896,6 +12060,34 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
9896
12060
  const isPinned = pinnedStatus === row.key;
9897
12061
  const isRowHovered = hoveredStatus === row.key;
9898
12062
  const isLifted = isRowHovered || isPinned;
12063
+ /* Round 562 / Loop — inspection-overrides-encoding family
12064
+ 5th anchor at the legend-swatch scope. When operator
12065
+ hovers a NODE ALIAS on the canvas, the legend swatch
12066
+ that matches that node's status tier lifts r + drop-
12067
+ shadow (R197 + R537 axes) — telegraphing "your
12068
+ inspected node is in this status group".
12069
+ Mirror of R486 minimap-dot inspection-override at the
12070
+ legend-swatch scope.
12071
+ Family progression (5 anchors):
12072
+ R484 recent-row timestamp on alias hover
12073
+ R485 edge particle opacity on alias hover
12074
+ R486 minimap dot opacity to 1.0 on alias hover
12075
+ R561 group-label opacity-1 + ants-gate refinement
12076
+ R562 legend-swatch r+glow on member-alias-matching ← this round
12077
+ Restraint: ONLY swatch lifts, NOT label/fill/ls/fw
12078
+ axes. Direct row-hover gets full treatment; inspection
12079
+ signal gets swatch-only lift — distinct "lighter"
12080
+ visual register matching R561's ants-gate approach
12081
+ (indirect inspection ≠ direct attention). */
12082
+ const hoveredSession = hoveredAlias
12083
+ ? (onlineNodes.find(s => s.alias === hoveredAlias) ?? offlineNodes.find(s => s.alias === hoveredAlias))
12084
+ : null;
12085
+ const hoveredAliasRowKey: 'working' | 'idle' | 'offline' | null = !hoveredSession ? null
12086
+ : hoveredSession.status === 'working' ? 'working'
12087
+ : offlineNodes.includes(hoveredSession) ? 'offline'
12088
+ : 'idle';
12089
+ const isMemberAliasMatching = hoveredAliasRowKey === row.key;
12090
+ const isSwatchLifted = isLifted || isMemberAliasMatching;
9899
12091
  return (
9900
12092
  <g
9901
12093
  key={row.key}
@@ -10027,15 +12219,50 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10027
12219
  R181 pin ring (6 + 0 stroke vs 8 - 0.75 inner
10028
12220
  ≈ 7.25). data-legend-swatch is unchanged so
10029
12221
  R197 / R55 / R61 tests probe the same handle. */}
12222
+ {/* Round 537 / Loop — extends drop-shadow visual-polish
12223
+ family (12 anchors after R536) to a 13th anchor: the
12224
+ legend swatch gains drop-shadow glow on hover/pin
12225
+ using its OWN row fill color (working green / idle
12226
+ teal / offline slate). Pre-R537 the swatch lifted
12227
+ only r (R197/R295 6 → 7) on attention — geometry
12228
+ axis only, no paint glow. R537 adds the paint axis,
12229
+ composing with R181/R402 pin-ring (separate concen-
12230
+ tric circle in the same row.fill color) so on
12231
+ hover/pin the SWATCH AND its pin-ring both contri-
12232
+ bute to a unified tier-coloured glow signature.
12233
+ Hue: row.fill (status hex) concatenated with `99`
12234
+ hex alpha (~60%). Working green / idle teal /
12235
+ offline slate each glow in their OWN tier color
12236
+ — the legend acts as a color-keyed status mirror.
12237
+ 3px blur reads soft; 60% alpha legible without
12238
+ overwhelming the swatch's own paint.
12239
+ Drop-shadow visual-polish family extension (13
12240
+ anchors). filter is paint-only; bbox unchanged.
12241
+ transition list extends to include 'filter 150ms
12242
+ ease-out', matching the existing R197 r 150ms
12243
+ cadence at this swatch. data-legend-swatch-glow
12244
+ attr exposes the gate state for tests. */}
10030
12245
  <circle
10031
12246
  cx="16" cy={row.y0}
10032
12247
  r="6"
10033
12248
  fill={row.fill}
10034
12249
  data-legend-swatch={row.key}
10035
- data-legend-swatch-state={isPinned ? 'pinned' : isRowHovered ? 'hover' : 'idle'}
12250
+ data-legend-swatch-state={isPinned ? 'pinned' : isRowHovered ? 'hover' : isMemberAliasMatching ? 'member-alias-matching' : 'idle'}
12251
+ data-legend-swatch-glow={isSwatchLifted ? 'true' : 'false'}
12252
+ data-legend-swatch-member-alias-matching={isMemberAliasMatching ? 'true' : 'false'}
12253
+ /* Round 578 — legend swatch joins per-element brightness
12254
+ family at 16th anchor. Stacks brightness(1.15) onto
12255
+ R537 drop-shadow. Closes chip-row tier-color glow
12256
+ trio at consistent stacked-filter pattern alongside
12257
+ R542 pressure-seg (already stacks brightness 1.2)
12258
+ and the sibling vendor chip (R578-sibling). */
12259
+ data-legend-swatch-brightness={isSwatchLifted ? '1.15' : '1'}
10036
12260
  style={{
10037
- r: isRowHovered || isPinned ? '7px' : '6px',
10038
- transition: 'r 150ms ease-out',
12261
+ r: isSwatchLifted ? '7px' : '6px',
12262
+ filter: isSwatchLifted
12263
+ ? `drop-shadow(0 0 3px ${row.fill}99) brightness(1.15)`
12264
+ : undefined,
12265
+ transition: 'r 150ms ease-out, filter 150ms ease-out',
10039
12266
  } as React.CSSProperties}
10040
12267
  />
10041
12268
  {/* R61 pinned-state ring — concentric stroke at r=8 in
@@ -10167,11 +12394,42 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10167
12394
  the value for tests. R219 letter-spacing pin
10168
12395
  tween + R55 fill transition + R181 always-mount
10169
12396
  pin ring all preserved. */
10170
- fontWeight="500"
12397
+ /* Round 531 / Loop — extends hover-fw family (R416/
12398
+ R420/R425/R520/R521/R522/R530, 7 anchors) to an
12399
+ 8th anchor at the legend-row label. Pre-R531
12400
+ R364 set fw=500 statically; hover/pin lifted
12401
+ other axes (R55 fill brighten / R433 letter-
12402
+ spacing 3-tier / R181 pin ring) but the fw
12403
+ stayed flat. R531 mirrors R530's recent-row
12404
+ alias pattern at the legend-row label scope.
12405
+ Hover OR pin (hoveredStatus===row.key ||
12406
+ isPinned) lifts fw to 600, matching the
12407
+ legend-row count tier (R309 fw=600 / R446
12408
+ pin lift 600→700). Active label now reads at
12409
+ the count's data tier — sibling treatment to
12410
+ R530 recent-row.
12411
+ Hover-fw family extension (8 anchors):
12412
+ R416 chip-row count digit
12413
+ R420 chrome zoom-level
12414
+ R425 hub-center digit
12415
+ R520 +N more flows footer
12416
+ R521 chrome nodeSize S/M/L inactive
12417
+ R522 chrome layout Ring/Grid inactive
12418
+ R530 recent-row alias text
12419
+ R531 legend-row label ← this round
12420
+ Two panel-row label surfaces (R530 recent-
12421
+ row alias + R531 legend-row label) now have
12422
+ parallel hover-fw signatures. R475 cadence
12423
+ at 200ms already covers font-weight via the
12424
+ existing transition list extension at this
12425
+ element. data-legend-row-label-font-weight
12426
+ attr flips '500' → '600' on isActive (was
12427
+ static '500' pre-R531). */
12428
+ fontWeight={(hoveredStatus === row.key || isPinned) ? '600' : '500'}
10171
12429
  data-legend-row-label={row.key}
10172
12430
  data-legend-row-label-pinned={isPinned ? 'true' : 'false'}
10173
12431
  data-legend-row-label-hovered={!isPinned && hoveredStatus === row.key ? 'true' : 'false'}
10174
- data-legend-row-label-font-weight="500"
12432
+ data-legend-row-label-font-weight={(hoveredStatus === row.key || isPinned) ? '600' : '500'}
10175
12433
  /* Round 433 / Loop: legend-row text extends from
10176
12434
  R219's pin-only letter-spacing (0px → 0.5px on
10177
12435
  isPinned) to a 3-tier scale matching the R432
@@ -10213,10 +12471,46 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10213
12471
  unchanged; R55 fill brighten unchanged — only
10214
12472
  the timing axis shifts. */
10215
12473
  data-legend-row-label-transition="200ms"
12474
+ /* Round 569 / Loop — extends R568's panel-row drop-
12475
+ shadow pattern to the SIBLING legend-row label.
12476
+ Symmetric closure of the panel-row drop-shadow
12477
+ across both panel surfaces (recent + legend).
12478
+ Pre-R569 the legend-row label had 3 hover/pin
12479
+ axes (R55 fill + R433 ls 3-tier + R531 fw); R569
12480
+ adds the 4th paint axis to match R568 recent-
12481
+ row text exactly.
12482
+ Two-tier paint-axis cascade now SYMMETRIC across
12483
+ both side panels:
12484
+ recent panel title (R550) + row text (R568)
12485
+ legend panel title (R550-sibling) + label (R569) ← this round
12486
+ Each panel has glow at BOTH chrome tiers (title
12487
+ active + row hover/pin). The 4-axis row signature
12488
+ (fill + ls + fw + glow) is now identical at both
12489
+ panel-row text scopes — completes the panel-row
12490
+ text-treatment parity.
12491
+ Hue/blur/cadence: same as R568 (pal.legendAccent
12492
+ + hex alpha 80, 2px blur, 200ms ease-out). Gate
12493
+ matches R531/R433 (hoveredStatus === row.key ||
12494
+ isPinned) — single boolean drives all 4 axes
12495
+ together for motion-coherent state-flip.
12496
+ Drop-shadow family extends to 18 anchors total
12497
+ (R568 was 17; R569 = 18).
12498
+ data-legend-row-label-glow attr added for tests. */
12499
+ data-legend-row-label-glow={(hoveredStatus === row.key || isPinned) ? 'true' : 'false'}
12500
+ /* Round 572 / Loop — sibling to recent-row text above.
12501
+ Stacks brightness(1.15) onto R569's drop-shadow at
12502
+ legend-row label scope (10th anchor in per-element
12503
+ brightness family). Matches recent-row text 4-axis
12504
+ signature exactly: fill + ls + fw + drop-shadow +
12505
+ brightness now BOTH panel-row text surfaces. */
12506
+ data-legend-row-label-brightness={(hoveredStatus === row.key || isPinned) ? '1.15' : '1'}
10216
12507
  style={{
10217
- transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out',
12508
+ transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out, font-weight 200ms ease-out, filter 200ms ease-out',
10218
12509
  letterSpacing: isPinned ? '0.5px' :
10219
12510
  hoveredStatus === row.key ? '0.25px' : '0px',
12511
+ filter: (hoveredStatus === row.key || isPinned)
12512
+ ? `drop-shadow(0 0 2px ${pal.legendAccent}80) brightness(1.15)`
12513
+ : undefined,
10220
12514
  }}
10221
12515
  >{row.label}</text>
10222
12516
  {/* R95: live count anchored to the right edge of the
@@ -10348,7 +12642,43 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10348
12642
  data-legend-count-pinned={isPinned ? 'true' : 'false'}
10349
12643
  data-legend-count-font-weight={isPinned ? '700' : '600'}
10350
12644
  data-legend-count-fill={row.count > 0 && (hoveredStatus === row.key || isPinned) ? 'tier' : 'neutral'}
10351
- style={{ pointerEvents: 'none', transition: 'opacity 150ms ease-out, fill 150ms ease-out, font-weight 150ms ease-out', fontVariantNumeric: 'tabular-nums' }}
12645
+ /* Round 518 / Loop extends R433's 3-tier hover-
12646
+ letter-spacing tween from the legend-row LABEL
12647
+ (text at x=30) to the SIBLING legend-row COUNT
12648
+ digit (this text at x=215). Pre-R518 the row's
12649
+ label spread on hover/pin (R433: 0/0.25/0.5px)
12650
+ while the count digit at the row's right edge
12651
+ stayed dead-typographic — same row, two halves,
12652
+ asymmetric kerning gesture. R518 mirrors the
12653
+ 3-tier scale at the count so the WHOLE row's
12654
+ typography reads as one unit under cursor: label
12655
+ + count spread together at matching values.
12656
+ Tabular-nums (R225) makes the kerning still
12657
+ visible on 2-digit counts — each digit cell
12658
+ keeps its fixed width, but the inter-digit
12659
+ advance grows. R518 also closes R475's panel-
12660
+ row TEXT cadence at the count surface — R475
12661
+ lifted the label text transitions to 200ms but
12662
+ the count was missed; R518 lifts opacity / fill
12663
+ / font-weight from 150 → 200ms AND adds the new
12664
+ letter-spacing axis at 200ms. One transition
12665
+ list, one cadence, one motion-coherent multi-
12666
+ axis hover/pin signature across the row.
12667
+ Hover-letter-spacing family extension (10
12668
+ anchors now): R344/R345/R347/R420/R427/R431/
12669
+ R432/R433/R517/R518. R518 closes the legend-
12670
+ row pair (label R433 + count R518). data-
12671
+ legend-count-letter-spacing attr exposes the
12672
+ resolved value for tests. */
12673
+ data-legend-count-letter-spacing={isPinned ? '0.5px' : hoveredStatus === row.key ? '0.25px' : '0px'}
12674
+ data-legend-count-transition="200ms"
12675
+ style={{
12676
+ pointerEvents: 'none',
12677
+ transition: 'opacity 200ms ease-out, fill 200ms ease-out, font-weight 200ms ease-out, letter-spacing 200ms ease-out',
12678
+ fontVariantNumeric: 'tabular-nums',
12679
+ letterSpacing: isPinned ? '0.5px' :
12680
+ hoveredStatus === row.key ? '0.25px' : '0px',
12681
+ }}
10352
12682
  >{row.count}</text>
10353
12683
  </g>
10354
12684
  );
@@ -10407,6 +12737,74 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10407
12737
  spacing as typographic intent. Stays well inside the
10408
12738
  bottom-left corner; opacity 0.4 unchanged so the
10409
12739
  watermark stays a watermark. */}
12740
+ {/* Round 519 / Loop — 呼吸感 family 3rd anchor. Pre-R519 the
12741
+ breath family had 2 anchors (R497 hub idle digit + R498
12742
+ recent-row hot pulse). Both signal active state — the
12743
+ digit when canvas is idle (no work pending), the recent
12744
+ row when fresh signal arrives. R519 adds a SLOW ambient
12745
+ breath to the brand watermark — present always, not gated
12746
+ on activity state. The watermark IS the canvas-corner
12747
+ register that says "the canvas is alive even when nothing
12748
+ is happening"; a 6s opacity pulse around its 0.4 mean
12749
+ (±0.08 swing → 0.32 ↔ 0.48) reads as ambient liveness
12750
+ rather than foreground signal.
12751
+ Why 6s (not R497's 4s): the breath family now spans
12752
+ activity registers (R497 4s — idle-focal: present and
12753
+ waiting; R498 ~3s — hot signal: just arrived) and now
12754
+ ambient register (R519 6s — corner watermark: always-on
12755
+ background). Slower cadence keeps the watermark in the
12756
+ background; ~10 pct slower than R497 keeps it out of
12757
+ phase so the two anchors never beat together visibly.
12758
+ Gate: !reducedMotion. Inside the prefers-reduced-motion
12759
+ media query, SMIL animate isn't covered by globals.css
12760
+ R29 (which only kills CSS animation property), so we
12761
+ gate at JSX level — when reducedMotion is true the
12762
+ <animate> child isn't mounted and opacity stays at the
12763
+ static 0.4. data-topo-brand-watermark-breath attr
12764
+ exposes the gate state for tests.
12765
+ 呼吸感 family extension (3 anchors): R497 hub idle / R498
12766
+ recent-row hot / R519 brand watermark ambient. */}
12767
+ {/* Round 525 / Loop — focal-recede family 3rd anchor. R507
12768
+ receded the hub-center workingCount digit; R508 receded
12769
+ the hub-highlight disc; both fade to 0.85× when any non-
12770
+ hub canvas surface is hovered (alias / edge / group /
12771
+ status / vendor) — the "you're inspecting elsewhere"
12772
+ gesture. R525 extends the pattern to the brand watermark
12773
+ at canvas bottom-left, the always-on decorative brand
12774
+ element. Pre-R525 the watermark stayed at its R519
12775
+ breath baseline (0.32-0.48 SMIL pulse) regardless of
12776
+ canvas attention; post-R525 it fades to 70% wrapper
12777
+ opacity (effective 0.224-0.336 with breath) when canvas
12778
+ attention is elsewhere, matching the same focal-recede
12779
+ semantic R507/R508 establish at the hub focal cluster.
12780
+ Implementation: wrap the existing <text> in a <g>
12781
+ wrapper whose opacity multiplies with the inner text's
12782
+ SMIL-animated opacity. SVG opacity composes
12783
+ multiplicatively across the parent/child chain, so:
12784
+ normal: g.opacity=1.0 × text.opacity(SMIL 0.32-0.48) = 0.32-0.48
12785
+ recede: g.opacity=0.7 × text.opacity(SMIL 0.32-0.48) = 0.224-0.336
12786
+ SMIL on inner text continues running through both
12787
+ states; only the wrapper opacity flips. 300ms ease-out
12788
+ transition on wrapper (matches R508 hub-highlight recede
12789
+ transition).
12790
+ Gate matches R507/R508 — focal-recede is a UNIFIED
12791
+ non-hub-canvas-hover signal driving multiple anchors,
12792
+ so all three (hub digit / hub-highlight / brand
12793
+ watermark) fade together as the canvas's decorative
12794
+ register, leaving only the surface under inspection
12795
+ foregrounded.
12796
+ Focal-recede family extension (3 anchors): R507 hub
12797
+ digit / R508 hub-highlight / R525 brand watermark. */}
12798
+ <g
12799
+ opacity={(hoveredAlias || hoveredEdgeKey || hoveredGroupLabel ||
12800
+ hoveredStatus || hoveredVendor) && !hoveredHub ? 0.7 : 1}
12801
+ data-topo-brand-watermark-wrapper
12802
+ data-topo-brand-watermark-recede={
12803
+ (hoveredAlias || hoveredEdgeKey || hoveredGroupLabel ||
12804
+ hoveredStatus || hoveredVendor) && !hoveredHub ? 'true' : 'false'
12805
+ }
12806
+ style={{ pointerEvents: 'none', transition: 'opacity 300ms ease-out' }}
12807
+ >
10410
12808
  <text
10411
12809
  x="16" y="672"
10412
12810
  fontSize="11" fontFamily="monospace" fontWeight="600"
@@ -10414,8 +12812,12 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10414
12812
  fill={pal.legendText}
10415
12813
  opacity="0.4"
10416
12814
  data-topo-brand-watermark
12815
+ data-topo-brand-watermark-breath={reducedMotion ? 'false' : 'true'}
10417
12816
  style={{ pointerEvents: 'none', transition: 'fill 200ms ease-out' }}
10418
- >sleep2agi</text>
12817
+ >sleep2agi{!reducedMotion && (
12818
+ <animate attributeName="opacity" values="0.32;0.48;0.32" dur="6s" repeatCount="indefinite" />
12819
+ )}</text>
12820
+ </g>
10419
12821
  {/* v0.10.0 Hero 3 Wave 1 / RFC §3.I (Vincent 5215 + 通信龙
10420
12822
  lead-autonomy Q4 dual-anchor minimal): canvas top-left
10421
12823
  crescent moon brand mark, visible ONLY when the
@@ -10450,10 +12852,47 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10450
12852
  the R175 panel-fade-in uses for cascade rhythm. data-
10451
12853
  topo-brand-canvas-mark-visible exposes the gate for
10452
12854
  tests. */}
12855
+ {/* Round 526 / Loop — focal-recede family 4th anchor.
12856
+ Symmetric polish to R525 (watermark recede). The
12857
+ brand crescent at canvas top-left is the second
12858
+ decorative brand element on the canvas; pre-R526 it
12859
+ stayed at flat opacity 0.35 (when visible) regardless
12860
+ of canvas attention. R526 multiplies its visible
12861
+ opacity by 0.7 when ANY non-hub canvas surface is
12862
+ hovered, matching R525's deeper-recede semantic for
12863
+ decorative brand elements (vs hub focal cluster's
12864
+ 0.85× recede at R507/R508).
12865
+ Composes cleanly with existing flowLinks gate:
12866
+ normal, flowLinks=0: opacity = 0.35 * 1.0 = 0.350
12867
+ recede, flowLinks=0: opacity = 0.35 * 0.7 = 0.245
12868
+ invisible, flowLinks>0: opacity = 0.00 * any = 0.000
12869
+ Multiplicative chain means recede only matters when
12870
+ crescent is visible (quiet canvas, flowLinks=0) —
12871
+ exactly when canvas attention elsewhere should
12872
+ dim the decorative register. 300ms transition
12873
+ already covers both axes (the existing visibility
12874
+ opacity ramp + the new recede multiplier easing).
12875
+ Focal-recede family extension (4 anchors): R507 hub
12876
+ digit / R508 hub-highlight / R525 watermark / R526
12877
+ crescent (this round). Canvas brand surfaces (R525
12878
+ watermark + R526 crescent) now BOTH carry focal-
12879
+ recede at the same 0.7 multiplier, fading as a
12880
+ decorative pair when the canvas's focal attention
12881
+ shifts elsewhere.
12882
+ data-topo-brand-canvas-mark-recede attr exposes the
12883
+ gate state for tests. */}
10453
12884
  <g
10454
- opacity={flowLinks.length === 0 ? 0.35 : 0}
12885
+ opacity={(flowLinks.length === 0 ? 0.35 : 0) * (
12886
+ (hoveredAlias || hoveredEdgeKey || hoveredGroupLabel ||
12887
+ hoveredStatus || hoveredVendor) && !hoveredHub ? 0.7 : 1
12888
+ )}
10455
12889
  data-topo-brand-canvas-mark
10456
12890
  data-topo-brand-canvas-mark-visible={flowLinks.length === 0 ? 'true' : 'false'}
12891
+ data-topo-brand-canvas-mark-recede={
12892
+ (hoveredAlias || hoveredEdgeKey || hoveredGroupLabel ||
12893
+ hoveredStatus || hoveredVendor) && !hoveredHub ? 'true' : 'false'
12894
+ }
12895
+ data-topo-brand-canvas-mark-breath={reducedMotion ? 'false' : 'true'}
10457
12896
  style={{ pointerEvents: 'none', transition: 'opacity 300ms ease-out, fill 200ms ease-out' }}
10458
12897
  >
10459
12898
  <defs>
@@ -10463,11 +12902,44 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10463
12902
  <circle cx="17.5" cy="13" r="10" fill="black" />
10464
12903
  </mask>
10465
12904
  </defs>
12905
+ {/* Round 528 / Loop — 呼吸感 family 4th anchor. Symmetric
12906
+ polish to R519 watermark ambient breath. The brand
12907
+ crescent at canvas top-left is the second decorative
12908
+ canvas brand surface; pre-R528 it stayed at the static
12909
+ composed opacity (wrapper 0.35 × no inner anim = flat).
12910
+ Post-R528 the inner <rect>'s fill-opacity breathes
12911
+ 0.8 ↔ 1.0 on a 7s cycle, composing multiplicatively
12912
+ with the wrapper's recede gate:
12913
+ normal visible: 0.35 × (0.8-1.0) = 0.280-0.350
12914
+ recede visible: 0.245 × (0.8-1.0) = 0.196-0.245
12915
+ invisible: 0 × any = 0
12916
+ 7s cadence intentionally OUT OF PHASE with R519
12917
+ watermark's 6s — the two ambient anchors never beat
12918
+ together visibly when both visible. R497 hub idle
12919
+ breath (4s) is the loudest; R498 recent-row hot pulse
12920
+ (~3s) is the most-active; R519 watermark (6s) +
12921
+ R528 crescent (7s) are the quietest ambient pair.
12922
+ 呼吸感 family extension (4 anchors):
12923
+ R497 hub idle digit 4s active-idle register
12924
+ R498 recent-row hot pulse 3s active-fresh register
12925
+ R519 watermark ambient 6s ambient (always-on)
12926
+ R528 crescent ambient 7s ambient (quiet-only) ← this round
12927
+ SMIL <animate> on fill-opacity (not parent opacity) so
12928
+ the wrapper's React-controlled gate compositions stay
12929
+ intact. Gated on !reducedMotion at JSX level —
12930
+ reducedMotion users see the inner rect at default
12931
+ fill-opacity=1.0 (no SMIL mounted, wrapper's static
12932
+ composed opacity wins). data-topo-brand-canvas-mark-
12933
+ breath attr exposes the gate state. */}
10466
12934
  <rect
10467
12935
  x="16" y="16" width="28" height="28"
10468
12936
  fill={pal.legendText}
10469
12937
  mask="url(#s2a-canvas-corner-mask)"
10470
- />
12938
+ >
12939
+ {!reducedMotion && (
12940
+ <animate attributeName="fill-opacity" values="0.8;1;0.8" dur="7s" repeatCount="indefinite" />
12941
+ )}
12942
+ </rect>
10471
12943
  </g>
10472
12944
  </svg>
10473
12945
 
@@ -10819,12 +13291,32 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10819
13291
  (the minimap viewport is small, ~120×82 px).
10820
13292
  Filter is paint-only — bbox unchanged. transition
10821
13293
  list extends to include 'filter 200ms ease-out'
10822
- so the glow eases when zoom crosses 1.5x. */
10823
- data-topo-minimap-viewport-glow={view.zoom > 1.5 ? 'true' : 'false'}
13294
+ so the glow eases when zoom crosses 1.5x.
13295
+ R540: extends the drop-shadow to also fire on
13296
+ hoveredMinimap with HOVER PRECEDENCE over zoom-
13297
+ state. Pre-R540 the viewport drop-shadow was
13298
+ zoom-only (single gate); R540 adds an
13299
+ interactional gate at lighter blur intensity.
13300
+ Hover wins when both true — interactional signal
13301
+ (user is inspecting) trumps informational signal
13302
+ (you're zoomed). Sibling to R534 edge-badge
13303
+ hover-precedence + R538 group-label hover-tier
13304
+ extensions.
13305
+ 2-tier alpha ladder:
13306
+ hover (interactional) legendAccent 99 (~60%)
13307
+ zoom > 1.5 (info) legendAccent 80 (~50%)
13308
+ rest none
13309
+ data-topo-minimap-viewport-glow attr upgraded
13310
+ binary ('true'/'false') → 3-value ('hover' |
13311
+ 'zoom' | 'false') so tests can distinguish
13312
+ gate cause. */
13313
+ data-topo-minimap-viewport-glow={hoveredMinimap ? 'hover' : view.zoom > 1.5 ? 'zoom' : 'false'}
10824
13314
  style={{
10825
- filter: view.zoom > 1.5
10826
- ? `drop-shadow(0 0 2px ${pal.legendAccent}80)`
10827
- : undefined,
13315
+ filter: hoveredMinimap
13316
+ ? `drop-shadow(0 0 2px ${pal.legendAccent}99)`
13317
+ : view.zoom > 1.5
13318
+ ? `drop-shadow(0 0 2px ${pal.legendAccent}80)`
13319
+ : undefined,
10828
13320
  transition: smoothView
10829
13321
  ? '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'
10830
13322
  : 'stroke-width 200ms ease-out, opacity 200ms ease-out, filter 200ms ease-out',
@@ -10957,7 +13449,49 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10957
13449
  / fullscreen) preview their active state on hover.
10958
13450
  Pure actions (zoom -/+, reset) stay white — they
10959
13451
  aren't toggles, have no active state to preview. */
10960
- className={`px-2 py-1 transition-colors 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' : ''}`}
13452
+ // Round 493 / Loop extends R492 chrome-strip press-feedback
13453
+ // family to nodeSize S/M/L buttons. Adds active:scale-95
13454
+ // alongside the existing color-deepen (R196) + chrome-pop
13455
+ // (R249). transition-transform + duration-200 + ease-out
13456
+ // + transform-gpu added since this className previously had
13457
+ // transition-colors only — without the transform transition,
13458
+ // active:scale-95 would hard-cut. transform-gpu promotes the
13459
+ // layer so scale doesn't trigger paint thrash.
13460
+ /* Round 521 / Loop — extends R270's hover-preview pattern
13461
+ (inactive toggle hover previews the active state's
13462
+ visual register) to the TYPOGRAPHY axis at the chrome
13463
+ nodeSize S/M/L surface. Pre-R521 the inactive variant
13464
+ had `hover:bg-cyan-500/5` (R270 bg preview) but no
13465
+ typography preview — active variant uses `font-medium`
13466
+ (fw 500), inactive variant sat at default fw 400 even
13467
+ on hover.
13468
+ R521 adds `hover:font-medium` + `transition-[font-
13469
+ weight]` to the inactive variant so hovering an
13470
+ inactive S/M/L letter thickens the glyph 400 → 500,
13471
+ previewing the typography of the active state the
13472
+ click would commit to. Sibling to R421 chrome zoom-
13473
+ level fontWeight hover delta (rest 500 → hover 600)
13474
+ and R520 footer fontWeight hover (500 → 600) — same
13475
+ idiom: thicken-on-hover for chrome surfaces with a
13476
+ pre-commit gesture.
13477
+ `font-medium` (500) matches the ACTIVE variant's
13478
+ fw exactly — the inactive hover landing weight equals
13479
+ the active locked weight, so clicking commits to a
13480
+ typography state the eye already saw 'on the way in'.
13481
+ Hover-fw family extension (5 anchors now):
13482
+ R416 chip-row count digit rest 500 → hover 700/600
13483
+ R420 chrome zoom-level rest 500 → hover 600
13484
+ R425 hub-center digit rest 700 → hover 800
13485
+ R520 +N more flows footer rest 500 → hover 600
13486
+ R521 chrome nodeSize S/M/L inactive rest 400 → hover 500 ← this round
13487
+ Active variant `font-medium` unchanged so the rest-vs-
13488
+ active typography distinction stays intact when the
13489
+ user IS clicked-in (active stays at fw 500, inactive
13490
+ rest at fw 400, inactive hover preview at fw 500).
13491
+ data-topo-chrome-nodesize-hover-preview-fw="500" attr
13492
+ exposes the polish for tests. */
13493
+ 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' : ''}`}
13494
+ data-topo-chrome-nodesize-hover-preview-fw={nodeScale === v ? null : '500'}
10961
13495
  style={{ color: nodeScale === v ? undefined : pal.legendText, borderColor: pal.containerBorder }}
10962
13496
  >
10963
13497
  {lbl}
@@ -10999,7 +13533,11 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10999
13533
  // → white/10) so mouse-down has a tactile dim before the
11000
13534
  // R186 icon pop fires on release.
11001
13535
  // R352: `group` lets the inner svg respond via group-hover.
11002
- className="group px-2 py-1 hover:bg-white/5 active:bg-white/10 transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset"
13536
+ // R493 zoom +/− buttons join the chrome-strip active:scale-95
13537
+ // press-feedback family (R492 + nodeSize above). transition-
13538
+ // transform + duration-200 + ease-out + transform-gpu added
13539
+ // since the className had only transition-colors.
13540
+ 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"
11003
13541
  style={{ color: pal.legendText }}
11004
13542
  aria-label="Zoom out"
11005
13543
  title="Zoom out (−)"
@@ -11091,10 +13629,32 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
11091
13629
  ? 'true' : 'false'
11092
13630
  }
11093
13631
  data-topo-chrome-zoom-level-hover={hoveredZoomLevel ? 'true' : 'false'}
13632
+ /* Round 517 / Loop — extends the chrome zoom-level readout
13633
+ from 2-axis (R347 letter-spacing + R420 fontWeight) to
13634
+ 3-axis hover signature by adding a color brighten to
13635
+ pal.legendHeadline. Pre-R517 the readout's color stayed
13636
+ at pal.legendText on hover; the digits got tighter
13637
+ kerning (0→0.5px) and heavier weight (500→600) but
13638
+ stayed the same legendText gray tone. R517 lifts color
13639
+ to legendHeadline on hover so the readout brightens
13640
+ into the headline tier at the same beat — matching the
13641
+ R55/R197/R239 hover-deepen-own-hue idiom that legend-
13642
+ row label + count carry at panel scope. Chrome strip's
13643
+ only data display now has full 3-axis hover signature
13644
+ (letter-spacing + fontWeight + color), parity with the
13645
+ chip-row chips' own hover-brighten pattern.
13646
+ Implementation: inline color uses the same hoveredZoom-
13647
+ Level state as R347/R420 — no new state. Transition
13648
+ already includes 'color 200ms ease-out' (R264) so the
13649
+ brighten eases under the same cadence as the kerning +
13650
+ weight tweens — one motion-coherent 3-axis lift.
13651
+ data-topo-chrome-zoom-level-color attr exposes the
13652
+ resolved color string for tests. */
13653
+ data-topo-chrome-zoom-level-color={hoveredZoomLevel ? 'headline' : 'text'}
11094
13654
  onMouseEnter={() => setHoveredZoomLevel(true)}
11095
13655
  onMouseLeave={() => setHoveredZoomLevel(false)}
11096
13656
  style={{
11097
- color: pal.legendText,
13657
+ color: hoveredZoomLevel ? pal.legendHeadline : pal.legendText,
11098
13658
  borderColor: pal.containerBorder,
11099
13659
  minWidth: 46,
11100
13660
  display: 'inline-block',
@@ -11139,7 +13699,11 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
11139
13699
  data-topo-chrome-zoom-in-popping={chromePopping === 'zoom-in' ? 'true' : 'false'}
11140
13700
  // R196: press-state (mirror of zoom-out above).
11141
13701
  // R352: `group` lets the inner svg respond via group-hover.
11142
- className="group px-2 py-1 hover:bg-white/5 active:bg-white/10 transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset"
13702
+ // R493 zoom +/− buttons join the chrome-strip active:scale-95
13703
+ // press-feedback family (R492 + nodeSize above). transition-
13704
+ // transform + duration-200 + ease-out + transform-gpu added
13705
+ // since the className had only transition-colors.
13706
+ 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"
11143
13707
  style={{ color: pal.legendText }}
11144
13708
  aria-label="Zoom in"
11145
13709
  title="Zoom in (+)"
@@ -11191,7 +13755,14 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
11191
13755
  Every standalone interactive HTML surface in TopoGraph
11192
13756
  now lifts on hover. data-topo-chrome-reset-hover-lift
11193
13757
  attr surfaces the lift for tests. */
11194
- className="p-1.5 rounded-md border hover:bg-white/5 active:bg-white/10 hover:-translate-y-px transition-colors transition-transform duration-200 ease-out transform-gpu focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60"
13758
+ // R493 reset button joins the chrome-strip active:scale-95
13759
+ // press-feedback family. The button already has transition-
13760
+ // transform + transform-gpu (R350 reset spin + R400 hover lift),
13761
+ // so just appending active:scale-95 plugs straight in. Compound
13762
+ // active state during press = hover-lift (-1px) + scale-95
13763
+ // composes as translateY(-1px) scale(0.95) — lift-and-compress
13764
+ // for tactile click feel.
13765
+ 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"
11195
13766
  data-topo-chrome-reset-hover-lift="true"
11196
13767
  style={{ background: pal.legendBox.fill, borderColor: pal.containerBorder, color: pal.legendText }}
11197
13768
  aria-label="Reset view"
@@ -11239,8 +13810,34 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
11239
13810
  // owns transform during its 450ms run. transformOrigin
11240
13811
  // 'center' so rotation pivots around the icon's centre
11241
13812
  // (default would be top-left and the icon would arc).
13813
+ /* Round 514 / Loop — extends R352/R353 chrome icon hover-
13814
+ scale family to the reset button. Pre-R514 the reset
13815
+ icon had hover-rotate (-8°, R350) + hover-sw (2.5→2.8,
13816
+ R453) but no hover-scale, while zoom-out (R352), zoom-
13817
+ in (R352), and fullscreen (R353) icons all carried
13818
+ `group-hover:scale-110`. R514 brings the reset icon
13819
+ into the same 3-axis hover signature (rotate + sw +
13820
+ scale) as the rest of the chrome strip.
13821
+ Implementation: inline transform composes rotate +
13822
+ scale into one string. `transform: rotate(-8deg)
13823
+ scale(1.1)` on hover; `rotate(0) scale(1)` at rest.
13824
+ transformOrigin 'center' applies to both — rotation
13825
+ pivots around centre AND scale grows from centre.
13826
+ The Tailwind `group-hover:scale-110` approach can't
13827
+ work here because inline `style.transform` overrides
13828
+ className-based transforms; compose the multi-axis
13829
+ transform inline instead.
13830
+ Chrome icon hover gesture parity (post-R514):
13831
+ zoom-out scale-110 + sw-lift (R352/R454)
13832
+ zoom-in scale-110 + sw-lift (R352/R454)
13833
+ fullscreen scale-110 + sw-lift (R353/R455)
13834
+ reset scale-1.1 + sw-lift + rotate -8°
13835
+ (R514 + R453 + R350)
13836
+ reset gets the EXTRA rotate axis because R350's spin
13837
+ preview semantic is reset-specific — the rotation
13838
+ hints at the click-spin (R184) the button will fire. */
11242
13839
  style={{
11243
- transform: hoveredReset && !resetSpinning ? 'rotate(-8deg)' : 'rotate(0deg)',
13840
+ transform: hoveredReset && !resetSpinning ? 'rotate(-8deg) scale(1.1)' : 'rotate(0deg) scale(1)',
11244
13841
  transformOrigin: 'center',
11245
13842
  transition: 'transform 200ms ease-out, stroke-width 200ms ease-out',
11246
13843
  }}
@@ -11284,9 +13881,11 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
11284
13881
  // fullscreen now all carry an icon-level hover gesture in
11285
13882
  // addition to the bg hover).
11286
13883
  // R400: hover translateY(-1px) lift — see reset button above for family doc.
11287
- className={`group p-1.5 rounded-md border hover:-translate-y-px transition-colors transition-transform duration-200 ease-out transform-gpu focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 ${
13884
+ // R493 fullscreen joins active:scale-95 press family (same as
13885
+ // reset above: lift-and-compress compound transform on press).
13886
+ 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 ${
11288
13887
  isFullscreen
11289
- ? 'bg-cyan-500/15 text-cyan-300 font-medium hover:bg-cyan-500/20 active:bg-cyan-500/25'
13888
+ ? 'bg-cyan-500/15 text-cyan-300 font-medium hover:bg-cyan-500/20 hover:text-cyan-200 active:bg-cyan-500/25'
11290
13889
  : 'hover:bg-cyan-500/5 active:bg-cyan-500/15'
11291
13890
  }${chromePopping === 'fullscreen' ? ' anet-chrome-pop' : ''}`}
11292
13891
  data-topo-chrome-fullscreen-hover-lift="true"
@@ -11320,12 +13919,20 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
11320
13919
  + R455 fullscreen (this round). transition-[transform,
11321
13920
  stroke-width] expands existing transition-transform
11322
13921
  so the sw lift eases under R352 scale-110 cadence. */}
13922
+ {/* Round 576 / Loop — fullscreen icon picks up hover-rotate-3.
13923
+ Joins R350 reset / R547 pill × / R549 brand logo at 4th
13924
+ anchor in hover-rotate idiom. Corner arrows rotate 3° on
13925
+ hover — subtle "preparing to transform" gesture (enter
13926
+ or exit fullscreen). Tailwind v4 emits individual rotate
13927
+ property (banked R547) — sits alongside group-hover:
13928
+ scale-110 in independent rotate axis. */}
11323
13929
  {isFullscreen ? (
11324
- <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">
13930
+ <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">
11325
13931
  <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" />
11326
13932
  </svg>
11327
13933
  ) : (
11328
- <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">
13934
+ // R576 sibling enter variant also picks up rotate-3.
13935
+ <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">
11329
13936
  <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" />
11330
13937
  </svg>
11331
13938
  )}