@sleep2agi/agent-network-dashboard 0.5.3-preview.13 → 0.5.3-preview.131

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 (281) 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/05-~ezd16iskp.css +2 -0
  145. package/.next/static/chunks/0f2vrzslznn_e.js +1 -0
  146. package/.next/static/chunks/{11iqwh145jvo5.js → 0j0mqmu927775.js} +1 -1
  147. package/.next/static/chunks/0l2trp87vwy6a.js +1 -0
  148. package/.next/static/chunks/0r6phtc3s-m6f.js +4 -0
  149. package/.next/static/chunks/{03a4--7ncekmk.js → 0v4-5tng.uh.7.js} +2 -2
  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 +3508 -148
  154. package/app/globals.css +58 -7
  155. package/package.json +4 -4
  156. package/scripts/p157-servers-copy-test.mjs +95 -0
  157. package/scripts/topo-active-chrome-hover-text-test.mjs +107 -0
  158. package/scripts/topo-alias-chat-brightness-test.mjs +79 -0
  159. package/scripts/topo-avatar-brightness-test.mjs +116 -0
  160. package/scripts/topo-avatar-drop-shadow-test.mjs +86 -0
  161. package/scripts/topo-avatar-fallback-hover-test.mjs +104 -0
  162. package/scripts/topo-avatar-fallback-rotate-test.mjs +92 -0
  163. package/scripts/topo-avatar-rotate-test.mjs +85 -0
  164. package/scripts/topo-avatar-scale-test.mjs +89 -0
  165. package/scripts/topo-brand-drop-shadow-test.mjs +71 -0
  166. package/scripts/topo-brand-logo-breath-test.mjs +102 -0
  167. package/scripts/topo-brand-logo-hover-brightness-test.mjs +105 -0
  168. package/scripts/topo-brand-logo-hover-rotate-test.mjs +93 -0
  169. package/scripts/topo-brand-logo-hover-test.mjs +85 -0
  170. package/scripts/topo-chat-ring-brightness-test.mjs +80 -0
  171. package/scripts/topo-chip-row-digit-ls-test.mjs +135 -0
  172. package/scripts/topo-chip-row-member-alias-lit-test.mjs +154 -0
  173. package/scripts/topo-chip-row-tier-glow-brightness-test.mjs +99 -0
  174. package/scripts/topo-chip-row-unit-hover-tracking-test.mjs +124 -0
  175. package/scripts/topo-click-ripple-glow-test.mjs +86 -0
  176. package/scripts/topo-cluster-count-attr-test.mjs +80 -0
  177. package/scripts/topo-crescent-breath-test.mjs +104 -0
  178. package/scripts/topo-crescent-recede-test.mjs +111 -0
  179. package/scripts/topo-edge-badge-circle-brightness-test.mjs +82 -0
  180. package/scripts/topo-edge-badge-hover-glow-test.mjs +90 -0
  181. package/scripts/topo-edge-badge-text-brightness-test.mjs +83 -0
  182. package/scripts/topo-edge-particle-brightness-test.mjs +82 -0
  183. package/scripts/topo-edge-pill-glow-test.mjs +67 -0
  184. package/scripts/topo-edge-visible-brightness-test.mjs +84 -0
  185. package/scripts/topo-endpoint-ring-brightness-test.mjs +83 -0
  186. package/scripts/topo-filter-pill-glow-test.mjs +90 -0
  187. package/scripts/topo-fleet-density-tier-test.mjs +84 -0
  188. package/scripts/topo-flow-arrow-brightness-test.mjs +82 -0
  189. package/scripts/topo-flow-rail-brightness-test.mjs +80 -0
  190. package/scripts/topo-freshness-chip-fade-test.mjs +105 -0
  191. package/scripts/topo-fullscreen-attr-test.mjs +73 -0
  192. package/scripts/topo-fullscreen-brightness-test.mjs +84 -0
  193. package/scripts/topo-fullscreen-icon-rotate-test.mjs +93 -0
  194. package/scripts/topo-grid-content-bottom-attr-test.mjs +72 -0
  195. package/scripts/topo-group-box-brightness-test.mjs +84 -0
  196. package/scripts/topo-group-label-brightness-test.mjs +84 -0
  197. package/scripts/topo-group-label-hover-glow-test.mjs +86 -0
  198. package/scripts/topo-group-label-member-alias-hover-test.mjs +125 -0
  199. package/scripts/topo-group-pill-glow-test.mjs +76 -0
  200. package/scripts/topo-group-tint-brightness-test.mjs +82 -0
  201. package/scripts/topo-hub-core-brightness-test.mjs +82 -0
  202. package/scripts/topo-hub-digit-brightness-test.mjs +79 -0
  203. package/scripts/topo-hub-digit-ls-test.mjs +119 -0
  204. package/scripts/topo-hub-halo-brightness-test.mjs +80 -0
  205. package/scripts/topo-hub-halo-glow-test.mjs +96 -0
  206. package/scripts/topo-hub-highlight-amplify-test.mjs +139 -0
  207. package/scripts/topo-hub-highlight-brightness-test.mjs +84 -0
  208. package/scripts/topo-hub-highlight-fill-transition-test.mjs +84 -0
  209. package/scripts/topo-hub-highlight-glow-test.mjs +99 -0
  210. package/scripts/topo-hub-highlight-r-test.mjs +112 -0
  211. package/scripts/topo-hub-highlight-recede-test.mjs +144 -0
  212. package/scripts/topo-hub-highlight-theme-fill-test.mjs +83 -0
  213. package/scripts/topo-hub-hover-ring-brightness-test.mjs +79 -0
  214. package/scripts/topo-hub-hover-ring-glow-test.mjs +97 -0
  215. package/scripts/topo-hub-idle-breath-test.mjs +7 -2
  216. package/scripts/topo-hub-recede-test.mjs +124 -0
  217. package/scripts/topo-hub-spoke-brightness-test.mjs +77 -0
  218. package/scripts/topo-hub-spoke-glow-test.mjs +112 -0
  219. package/scripts/topo-label-card-brightness-test.mjs +81 -0
  220. package/scripts/topo-layout-hover-fw-test.mjs +98 -0
  221. package/scripts/topo-layout-toggle-brightness-test.mjs +94 -0
  222. package/scripts/topo-legend-count-brightness-test.mjs +80 -0
  223. package/scripts/topo-legend-count-letter-spacing-test.mjs +108 -0
  224. package/scripts/topo-legend-label-fw-test.mjs +107 -0
  225. package/scripts/topo-legend-pin-ring-brightness-test.mjs +82 -0
  226. package/scripts/topo-legend-row-count-brightness-test.mjs +85 -0
  227. package/scripts/topo-legend-row-label-glow-test.mjs +102 -0
  228. package/scripts/topo-legend-swatch-glow-test.mjs +109 -0
  229. package/scripts/topo-legend-swatch-member-alias-match-test.mjs +139 -0
  230. package/scripts/topo-legend-tint-brightness-test.mjs +83 -0
  231. package/scripts/topo-minimap-hover-glow-test.mjs +109 -0
  232. package/scripts/topo-more-footer-brightness-test.mjs +94 -0
  233. package/scripts/topo-node-alias-brightness-test.mjs +84 -0
  234. package/scripts/topo-node-sub-text-brightness-test.mjs +88 -0
  235. package/scripts/topo-nodesize-brightness-test.mjs +82 -0
  236. package/scripts/topo-nodesize-hover-fw-test.mjs +99 -0
  237. package/scripts/topo-orphan-box-dash-test.mjs +89 -0
  238. package/scripts/topo-orphan-fill-opacity-test.mjs +91 -0
  239. package/scripts/topo-orphan-label-opacity-test.mjs +98 -0
  240. package/scripts/topo-panel-count-hover-ls-test.mjs +87 -0
  241. package/scripts/topo-panel-row-brightness-test.mjs +116 -0
  242. package/scripts/topo-panel-title-brightness-test.mjs +98 -0
  243. package/scripts/topo-panel-title-glow-test.mjs +111 -0
  244. package/scripts/topo-pill-x-rotate-test.mjs +96 -0
  245. package/scripts/topo-pinned-aspect-test.mjs +89 -0
  246. package/scripts/topo-pip-brightness-test.mjs +85 -0
  247. package/scripts/topo-pressure-seg-glow-test.mjs +92 -0
  248. package/scripts/topo-pressure-seg-member-alias-match-test.mjs +133 -0
  249. package/scripts/topo-pressure-seg-motion-test.mjs +101 -0
  250. package/scripts/topo-recent-count-brightness-test.mjs +84 -0
  251. package/scripts/topo-recent-more-fw-test.mjs +126 -0
  252. package/scripts/topo-recent-row-fw-test.mjs +115 -0
  253. package/scripts/topo-recent-row-text-glow-test.mjs +86 -0
  254. package/scripts/topo-recent-tint-brightness-test.mjs +80 -0
  255. package/scripts/topo-recent-ts-brightness-test.mjs +86 -0
  256. package/scripts/topo-reduced-motion-attr-test.mjs +69 -0
  257. package/scripts/topo-reset-brightness-test.mjs +83 -0
  258. package/scripts/topo-reset-icon-hover-scale-test.mjs +102 -0
  259. package/scripts/topo-runtime-badge-brightness-test.mjs +78 -0
  260. package/scripts/topo-runtime-badge-glow-test.mjs +108 -0
  261. package/scripts/topo-runtime-badge-rotate-test.mjs +85 -0
  262. package/scripts/topo-starfield-hue-test.mjs +109 -0
  263. package/scripts/topo-status-ring-brightness-test.mjs +84 -0
  264. package/scripts/topo-sub-text-chat-brightness-test.mjs +81 -0
  265. package/scripts/topo-titleblock-h2-hover-fw-test.mjs +109 -0
  266. package/scripts/topo-titleblock-h2-hover-tracking-test.mjs +128 -0
  267. package/scripts/topo-titleblock-kicker-hover-test.mjs +134 -0
  268. package/scripts/topo-vendor-chip-glow-test.mjs +97 -0
  269. package/scripts/topo-vendor-pill-glow-test.mjs +98 -0
  270. package/scripts/topo-watermark-breath-test.mjs +100 -0
  271. package/scripts/topo-watermark-recede-test.mjs +114 -0
  272. package/scripts/topo-zoom-buttons-brightness-test.mjs +94 -0
  273. package/scripts/topo-zoom-level-brightness-test.mjs +83 -0
  274. package/scripts/topo-zoom-level-color-test.mjs +105 -0
  275. package/.next/static/chunks/0-eqn.ga3bcnl.js +0 -1
  276. package/.next/static/chunks/0m.1mvl~t.avc.css +0 -2
  277. package/.next/static/chunks/0tl3h11hxa7oe.js +0 -4
  278. package/.next/static/chunks/0zwxl-vr5q45i.js +0 -1
  279. /package/.next/static/{U6pY8Ja-T2ME4lRhun26w → skBswyT45Rn83Ku6lBH0M}/_buildManifest.js +0 -0
  280. /package/.next/static/{U6pY8Ja-T2ME4lRhun26w → skBswyT45Rn83Ku6lBH0M}/_clientMiddlewareManifest.js +0 -0
  281. /package/.next/static/{U6pY8Ja-T2ME4lRhun26w → skBswyT45Rn83Ku6lBH0M}/_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
@@ -1123,6 +1142,12 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1123
1142
  // hover state says — either -8° (still hovering) or 0 (mouse left).
1124
1143
  // 350th-round milestone polish.
1125
1144
  const [hoveredReset, setHoveredReset] = useState(false);
1145
+ // R595: hover state for the chrome fullscreen button — drives the
1146
+ // brightness(1.15) filter on hover, sibling to hoveredReset above.
1147
+ // Same pattern as R593 hoveredZoomLevel + R594 hoveredReset; closes
1148
+ // the standalone chrome button pair (reset + fullscreen) at
1149
+ // brightness parity.
1150
+ const [hoveredFullscreen, setHoveredFullscreen] = useState(false);
1126
1151
  // R135: panel-wide hover-elevation. The recent-signal + legend
1127
1152
  // panels both already host clickable rows (R56/R116 recent rows,
1128
1153
  // R55/R61 legend rows) and a clickable footer (R133), so the
@@ -1706,7 +1731,18 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1706
1731
  width) = 166px total title-block width vs 168px pre-R298 —
1707
1732
  no measurable layout shift, just a deliberate tighter
1708
1733
  grouping. */}
1709
- <div className="flex items-center gap-2.5">
1734
+ {/* Round 554 / Loop — title-block wrapper picks up `group` so
1735
+ the H2 below can subscribe to `group-hover:tracking-tighter`.
1736
+ Pairs with R548/R549 brand-logo hover gestures: cursor
1737
+ sweeping anywhere across the title cluster fires the brand
1738
+ logo's scale-105 + rotate-6 + breath ↔ AND tightens the
1739
+ H2's tracking from -0.025em → -0.05em.
1740
+ Makes the title-block read as one coherent hover cluster —
1741
+ brand mark provides the loud gesture (scale + rotate), H2
1742
+ provides the subtle editorial gesture (kerning tighten).
1743
+ data-topo-section-titleblock-group attr surfaces the gate
1744
+ for tests. */}
1745
+ <div className="group flex items-center gap-2.5" data-topo-section-titleblock-group>
1710
1746
  {/* Round 297 / Loop: brand-logo color picks up the 200ms ease-
1711
1747
  out transition. Pre-R297 the moon glyph had theme-
1712
1748
  conditional color (cyber #67e8f9 cyan ↔ light #0d9488
@@ -1730,13 +1766,162 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1730
1766
  overpowering the h2 at text-lg/font-semibold (R286).
1731
1767
  viewBox 32×32 unchanged so the inner crescent geometry
1732
1768
  scales proportionally. */}
1769
+ {/* Round 548 / Loop — title-block brand logo gains subtle
1770
+ hover-scale gesture. Pre-R548 the 40×40 crescent was
1771
+ fully static — no hover affordance. R548 adds hover:
1772
+ scale-105 (Tailwind 4 emits as `scale: 1.05`) so the
1773
+ brand mark gently responds to attention as the user's
1774
+ cursor sweeps across the title block. 5% scale is
1775
+ intentionally subtle (vs R350 chrome icon hover-scale-
1776
+ 110): the brand logo is a passive identity mark, not
1777
+ an interactive control, so the gesture stays small.
1778
+ cursor: default to clarify non-clickability (the SVG
1779
+ isn't a button; just a brand element).
1780
+ transition-transform duration-200 ease-out matches the
1781
+ dashboard's R350-family hover-scale cadence so the
1782
+ brand logo's response shares the same motion vocabulary
1783
+ as the chrome strip's icon scales. transform-gpu hint
1784
+ promotes the SVG to its own compositor layer for crisp
1785
+ edges during the tween.
1786
+ Brand-mark delight gesture family (1 anchor):
1787
+ R548 title-block brand logo hover:scale-105
1788
+ The crescent at canvas top-left (data-topo-brand-
1789
+ canvas-mark) and the watermark at bottom-left (data-
1790
+ topo-brand-watermark) are intentionally LEFT STATIC —
1791
+ both have pointerEvents:none and exist as ambient
1792
+ decoration with their own breath/recede dynamics
1793
+ (R519/R525/R526/R528). The title-block logo is the
1794
+ ONLY brand surface that's a candidate for hover affordance,
1795
+ since it sits in the chrome-band where the cursor
1796
+ naturally passes during normal use. */}
1797
+ {/* Round 549 / Loop — extends R548 (hover:scale-105) with a
1798
+ subtle hover:rotate-6 rotation. Pairs scale + rotate on
1799
+ the brand mark, same idiom as R547 added to the pill ×
1800
+ close buttons (scale-110 + rotate-12) but at gentler
1801
+ amounts: 105% vs 110% scale and 6° vs 12° rotation —
1802
+ brand marks want restraint vs interactive close buttons.
1803
+
1804
+ The crescent-moon shape (curved cutout via mask) reads
1805
+ visually distinct as it rotates — the asymmetric cutout
1806
+ swings into a fresh angle, telegraphing "this brand mark
1807
+ is alive without being loud". The 6° landing lands the
1808
+ moon's cusp pointing slightly NE rather than straight up,
1809
+ a small but legible reveal of the shape's geometry.
1810
+
1811
+ Tailwind 4 emits BOTH `scale: 1.05` AND `rotate: 6deg`
1812
+ as INDIVIDUAL CSS properties (not a combined transform
1813
+ string — see R547 banked pattern). The className keeps
1814
+ transition-transform so both axes ease at the same 200ms
1815
+ cadence; transform-gpu hint stays so the compositor
1816
+ promotes both axes to GPU layers.
1817
+
1818
+ data-topo-brand-logo-hover-rotate attr surfaces the
1819
+ landing rotation for tests. Pair with R548's
1820
+ data-topo-brand-logo-hover-scale attr — both attrs
1821
+ advertise the dual-axis hover signature. */}
1822
+ {/* Round 553 / Loop — title-block brand logo gains subtle
1823
+ idle opacity breath (~0.92 ↔ 1, 5s ease-in-out cycle).
1824
+ 5th anchor in the 呼吸感 breath family, slotting into
1825
+ the ascending cadence ladder between hub idle (4s) and
1826
+ watermark (6s):
1827
+ row hot 3s
1828
+ hub idle 4s
1829
+ brand logo 5s ← this round
1830
+ watermark 6s
1831
+ crescent 7s
1832
+ Composes cleanly with R548 hover:scale-105 + R549
1833
+ hover:rotate-6 — opacity, scale, and rotate are
1834
+ independent CSS properties; the moon keeps breathing
1835
+ as it scales and rotates on hover. Layered effect
1836
+ reads as "this brand mark is alive even before you
1837
+ touch it, and lights up further on hover".
1838
+ Reduced-motion gate: component-side `!reducedMotion`
1839
+ toggles the className (canonical TopoGraph breath
1840
+ pattern); R29 globals.css blanket provides a
1841
+ defense-in-depth fallback (animation-duration →
1842
+ 0.001ms under prefers-reduced-motion: reduce).
1843
+ data-topo-brand-logo-breath attr exposes the gate
1844
+ state for tests. */}
1845
+ {/* Round 557 / Loop — brand logo gains 4th hover axis:
1846
+ hover:brightness-110 (filter). Adds a chromatic axis
1847
+ to the brand-mark hover signature alongside R548
1848
+ scale, R549 rotate, R553 idle breath:
1849
+ R548 hover:scale-105 transform-scale
1850
+ R549 hover:rotate-6 transform-rotate
1851
+ R553 idle breath (5s) opacity (animation)
1852
+ R557 hover:brightness-110 filter ← this round
1853
+ All 4 axes ride on INDEPENDENT CSS properties (scale,
1854
+ rotate, opacity, filter) — they compose freely without
1855
+ clobbering each other. The cyan/teal crescent gains a
1856
+ soft +10% brightness boost on hover, layered on top of
1857
+ the existing scale + rotate lift + ongoing idle breath.
1858
+ Why +10% (vs more aggressive 125/150): brand mark wants
1859
+ restraint. The eye reads the brightness shift as "this
1860
+ mark lights up under attention" without crossing into
1861
+ "this mark is now glowing".
1862
+ Implementation: className extends transition-transform
1863
+ → transition-[transform,filter] so the brightness
1864
+ tweens at the same 200ms ease-out cadence as the scale
1865
+ + rotate axes — one motion-coherent 3-property hover
1866
+ lift on the className tier (plus the inline color 200ms
1867
+ transition for theme-toggle eases).
1868
+ Brand-mark family axis count: 4 hover-state axes
1869
+ cleanly factor across:
1870
+ geometry (scale + rotate)
1871
+ paint (opacity breath + brightness on hover)
1872
+ Cluster reads as "alive, lifting, and lighting up under
1873
+ attention" — three independent gesture vocabularies on
1874
+ one surface.
1875
+ data-topo-brand-logo-hover-brightness attr surfaces
1876
+ the landing value for tests. */}
1733
1877
  <svg
1734
1878
  width="40" height="40" viewBox="0 0 32 32" aria-hidden
1735
- className="shrink-0"
1879
+ /* R604 — brand 书生 logo gains hover-drop-shadow as 5th
1880
+ axis (4 hover + 1 always-on breath). Tailwind v4 arbitrary
1881
+ drop-shadow with currentColor — the glow inherits the
1882
+ brand-mark's own colour (light teal #0d9488 / cyber cyan
1883
+ #67e8f9 per inline style.color). 8px radius reads as a
1884
+ "lit up under attention" halo without overwhelming the
1885
+ 40×40 logo footprint.
1886
+
1887
+ Tailwind v4 emits filter utilities through CSS-var system
1888
+ (--tw-drop-shadow, --tw-brightness) that combine in the
1889
+ `filter` shorthand — so hover:brightness-110 + hover:
1890
+ drop-shadow-[0_0_8px_currentColor] stack instead of
1891
+ clobbering each other. Same banked R564/R570 "halo +
1892
+ glow" pattern, now at the brand-mark scope.
1893
+
1894
+ Brand-logo hover signature now 5 axes:
1895
+ R548 hover:scale-105 transform-scale
1896
+ R549 hover:rotate-6 transform-rotate
1897
+ R553 idle breath (5s) opacity (always-on)
1898
+ R557 hover:brightness-110 filter brightness
1899
+ R604 hover:drop-shadow filter drop-shadow ← this round
1900
+
1901
+ Existing inline transition already covers 'filter 200ms
1902
+ ease-out' (R557 cadence) — both brightness AND drop-
1903
+ shadow ease at the same beat. */
1904
+ className={`shrink-0 transition-[transform,filter] duration-200 ease-out hover:scale-105 hover:rotate-6 hover:brightness-110 hover:drop-shadow-[0_0_8px_currentColor] transform-gpu${!reducedMotion ? ' anet-topo-brand-logo-breath' : ''}`}
1736
1905
  data-topo-brand-logo
1906
+ data-topo-brand-logo-hover-scale="1.05"
1907
+ data-topo-brand-logo-hover-rotate="6deg"
1908
+ data-topo-brand-logo-hover-brightness="1.1"
1909
+ data-topo-brand-logo-hover-drop-shadow="0_0_8px_currentColor"
1910
+ data-topo-brand-logo-breath={!reducedMotion ? 'true' : 'false'}
1737
1911
  style={{
1738
1912
  color: isLight ? '#0d9488' : '#67e8f9',
1739
- transition: 'color 200ms ease-out',
1913
+ cursor: 'default',
1914
+ // R557 — extend transition list to include filter (and
1915
+ // re-spec transform for cadence parity) so the new
1916
+ // hover:brightness-110 axis eases at 200ms alongside
1917
+ // the existing color 200ms (theme-toggle ease) and the
1918
+ // className-based hover:scale-105 / hover:rotate-6.
1919
+ // Inline transition is a shorthand and overrides the
1920
+ // className's transition-[transform,filter] — listing
1921
+ // all axes here ensures the eased property set covers
1922
+ // color (theme) + transform (scale + rotate) + filter
1923
+ // (brightness) at uniform 200ms ease-out.
1924
+ transition: 'color 200ms ease-out, transform 200ms ease-out, filter 200ms ease-out',
1740
1925
  }}
1741
1926
  >
1742
1927
  <mask id="s2a-titleblock-moon-mask">
@@ -1784,7 +1969,40 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1784
1969
  R300 marks the milestone of 25 rounds (R275-R300) of
1785
1970
  continuous TopoGraph polish + codex's Vincent 5215/
1786
1971
  5222 logo asset+integration work. */}
1787
- <div className="text-xs uppercase text-gray-500 tracking-widest leading-tight font-medium" data-topo-section-kicker>Network Topology</div>
1972
+ {/* Round 555 / Loop kicker "Network Topology" gains group-
1973
+ hover affordance via the R554 wrapper's `group` flag,
1974
+ closing the title-block cluster's hover coverage at 3
1975
+ surfaces (brand logo + H2 + kicker).
1976
+ Picks up the small-label SPREAD direction (R554 banked
1977
+ "small labels SPREAD on hover / large headlines TIGHTEN"
1978
+ — kicker is xs uppercase, definitely a small label) plus
1979
+ a color brighten (text-gray-500 #6b7280 → text-gray-400
1980
+ #9ca3af).
1981
+ Spread: tracking-widest (0.1em rest) → 0.13em hover —
1982
+ +30% kerning bump. At text-xs (12px) the per-gap shift
1983
+ is 1.2px → 1.56px (+0.36px/gap), legible without
1984
+ overshooting the rest's tracking-widest editorial base.
1985
+ Color: text-gray-500 → text-gray-400 — one tier lighter,
1986
+ same idiom as R296 (kicker rest tone-up from gray-600 to
1987
+ gray-500), now extended at the hover-state tier.
1988
+ transition-[letter-spacing,color] duration-200 ease-out
1989
+ matches the 200ms cadence of R554 H2 ls + the rest of
1990
+ the hover-ls family (R344/R345/R347/R351/R420/R427/R431/
1991
+ R432/R434/R527/R539).
1992
+ Title-block cluster signature post-R555 (3 surfaces):
1993
+ brand logo loud scale + rotate + breath
1994
+ (R548/R549/R553)
1995
+ H2 subtle tracking-tighter
1996
+ (R554, editorial-tighten)
1997
+ kicker subtle tracking-spread + color lift
1998
+ (R555, data-spread) ← this round
1999
+ Two of the three surfaces are typographic; the brand
2000
+ logo carries the geometric+chromatic motion. Cluster
2001
+ reads as ONE coherent hover unit through three
2002
+ independent gesture vocabularies.
2003
+ data-topo-section-kicker-hover-tracking + -hover-color
2004
+ attrs expose the landing values for tests. */}
2005
+ <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>
1788
2006
  {/* Round 286 / Loop: title 'Command mesh' adopts tracking-tight
1789
2007
  (-0.025em) to complement R285 kicker tracking-widest. Wide
1790
2008
  eyebrow + tight headline is the conventional editorial
@@ -1796,7 +2014,62 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1796
2014
  cumulatively legible across 12 characters. font-semibold
1797
2015
  (600) stays — tracking-tight does the heavy lifting for
1798
2016
  the editorial register. */}
1799
- <h2 className="text-lg text-white font-semibold leading-tight tracking-tight" data-topo-section-title>Command mesh</h2>
2017
+ {/* Round 554 / Loop — H2 "Command mesh" gains group-hover-
2018
+ gated tracking-tighter via the wrapper's R554 `group`
2019
+ flag. Pre-R554 the H2 was fully static (R286 tracking-
2020
+ tight only at rest). R554 adds an editorial-tighten
2021
+ gesture: when cursor sweeps anywhere across the title
2022
+ cluster (brand logo OR title text), the headline tightens
2023
+ from -0.025em → -0.05em.
2024
+ Inverts the typical hover-letter-spacing direction:
2025
+ small labels (chip counts, panel titles, edge digits)
2026
+ SPREAD on hover → "data telegraphing"
2027
+ large headlines (h2 Command mesh)
2028
+ TIGHTEN on hover → "editorial emphasis"
2029
+ Both directions are coherent design language — small
2030
+ data wants spacing for legibility; large headlines want
2031
+ tightening for designed-headline polish. Same idiom as
2032
+ the conventional editorial pairing of "wide kicker +
2033
+ tight headline" R285/R286 set up (kicker spreads 0.1em
2034
+ tracking-widest; headline tightens -0.025em tracking-
2035
+ tight) — R554 deepens that pairing's tighten side at
2036
+ the hover-state tier.
2037
+ At text-lg (18px) the shift is -0.45px → -0.9px per
2038
+ gap (~5.4px total tightening across "Command mesh" 12
2039
+ chars). Subtle but legible when the cursor sweeps in.
2040
+ transition-[letter-spacing] duration-200 ease-out
2041
+ matches the 200ms hover-ls cadence used at R344/R345/
2042
+ R347/R351/R420/R427/R431/R432/R434/R527/R539 family
2043
+ anchors.
2044
+ data-topo-section-title-hover-tracking attr surfaces
2045
+ the landing tracking class for tests. */}
2046
+ {/* Round 556 / Loop — H2 "Command mesh" gains a 2nd
2047
+ editorial-emphasis axis: group-hover:font-bold paired
2048
+ with R554's group-hover:tracking-tighter. Both lifts
2049
+ fire on the same R554 wrapper's `group` flag (hover
2050
+ anywhere in the title cluster → BOTH H2 axes intensify
2051
+ simultaneously).
2052
+ H2 hover signature post-R556 (2 typographic axes
2053
+ intensify together):
2054
+ rest font-semibold 600 + tracking-tight -0.025em
2055
+ hover font-bold 700 + tracking-tighter -0.05em
2056
+ Editorial emphasis through TWO axes — heavier AND
2057
+ tighter on hover. Mirrors the conventional "designed-
2058
+ headline emphasis" idiom (heavier + tighter = more
2059
+ authoritative; the eye reads both axes as intensifying
2060
+ the same semantic).
2061
+ Hover-fw family extension (6 anchors now):
2062
+ R416 chip-row count digit (chip group-hover)
2063
+ R420 chrome zoom-level (hover)
2064
+ R425 hub-center digit (hub hover)
2065
+ R520 +N more flows footer (recent panel hover)
2066
+ R521 chrome nodeSize S/M/L (inactive hover)
2067
+ R556 title-block H2 (cluster group-hover) ← this round
2068
+ Transition list extends to include 'font-weight 200ms
2069
+ ease-out' alongside the existing 'letter-spacing'
2070
+ 200ms cadence. data-topo-section-title-hover-fw attr
2071
+ surfaces the landing weight for tests. */}
2072
+ <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>
1800
2073
  </div>
1801
2074
  </div>
1802
2075
  {/* Round 328 / Loop: chip-row strip wrapper gap 2 → 2.5
@@ -1966,8 +2239,75 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1966
2239
  // overlays the release-pop. Matching `transform-gpu`
1967
2240
  // promotes the layer so the scale doesn't trigger
1968
2241
  // layout/paint thrash. Sibling change on Grid below.
1969
- className={`px-2.5 py-1 focus:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset hover:tracking-wide active:scale-95 transform-gpu ${layout === 'ring' ? 'bg-cyan-500/15 text-cyan-300 font-medium hover:bg-cyan-500/20 active:bg-cyan-500/25' : 'text-gray-400 hover:text-cyan-300 hover:bg-cyan-500/5 active:bg-cyan-500/15'} ${chromePopping === 'layout-ring' ? ' anet-chrome-pop' : ''}`}
1970
- style={{ transition: 'background-color 150ms ease, color 150ms ease, letter-spacing 200ms ease-out, transform 150ms ease-out' }}
2242
+ /* Round 522 / Loop extends R521's typography-preview
2243
+ idiom (chrome nodeSize hover:font-medium 400 500) to
2244
+ the Ring/Grid layout toggle's inactive variant. Pre-
2245
+ R522 the inactive Ring/Grid had `hover:text-cyan-300
2246
+ hover:bg-cyan-500/5` (R270 color + bg previews of the
2247
+ active state) but no typography preview — the active
2248
+ variant uses `font-medium` (fw 500), inactive sat at
2249
+ default fw 400 even on hover. R522 adds `hover:font-
2250
+ medium` to the inactive Ring/Grid so the rest-vs-hover
2251
+ transition previews the typography state the click
2252
+ would commit to, matching the click commits's locked
2253
+ weight.
2254
+ font-weight 150ms appended to the transition list
2255
+ matching the existing 150ms color/bg cadence at this
2256
+ button — when hover lifts color (gray-400 → cyan-300)
2257
+ + bg (transparent → cyan-500/5) + fw (400 → 500), all
2258
+ 3 ease at the same 150ms beat.
2259
+ Hover-fw family extension (6 anchors): R416/R420/R425/
2260
+ R520/R521/R522. R522 closes the chrome toggle group
2261
+ typography preview at the last remaining toggle —
2262
+ layout (Ring/Grid). After R521 (nodeSize) + R522
2263
+ (layout), every multi-state chrome toggle has hover-
2264
+ fw preview on its inactive variant.
2265
+ data-topo-chrome-layout-hover-preview-fw="500" attr
2266
+ on inactive button exposes the polish for tests. */
2267
+ /* Round 552 / Loop — chrome active-variant gains hover:
2268
+ text-cyan-200, lifting text one brightness tier alongside
2269
+ the existing hover:bg-cyan-500/20 bg deepen. Coordinated
2270
+ 4-anchor edit (replace_all touched 4 sibling lines sharing
2271
+ the identical active-variant className substring):
2272
+ Ring (this line) layout === 'ring'
2273
+ Grid (line ~2097) layout === 'grid'
2274
+ S/M/L (line ~12635) nodeScale === v
2275
+ Fscrn (line ~13030) isFullscreen
2276
+ Pre-R552 the active variant's hover state only deepened bg
2277
+ (cyan-500/15 → /20); text stayed planted at cyan-300. The
2278
+ inactive variant already lifts text on hover (text-gray-400
2279
+ → text-cyan-300). R552 brings parity: active variant lifts
2280
+ text one tier brighter (cyan-300 → cyan-200) on hover,
2281
+ mirroring the inactive variant's "text brightens on hover"
2282
+ gesture at the next brightness step.
2283
+ Brightness ladder snapshot (cyan):
2284
+ cyan-400 brand chrome focus ring
2285
+ cyan-300 active-variant rest ←─┐
2286
+ │ +1 tier on hover
2287
+ cyan-200 active-variant hover ←─┘ (this round)
2288
+ Pure paint axis (text color); bbox/geometry unchanged.
2289
+ transition-colors already in the class list so the cyan-
2290
+ 300 → cyan-200 swap eases at the existing 200ms cadence.
2291
+ hover-color brighten family extension at the chrome strip
2292
+ active-variant scope; sibling to the inactive variant's
2293
+ R163/R178/R179/R270 hover:text-cyan-300 idiom. */
2294
+ className={`px-2.5 py-1 focus:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset hover:tracking-wide hover:brightness-[1.15] active:scale-95 transform-gpu ${layout === 'ring' ? 'bg-cyan-500/15 text-cyan-300 font-medium hover:bg-cyan-500/20 hover:text-cyan-200 active:bg-cyan-500/25' : 'text-gray-400 hover:text-cyan-300 hover:bg-cyan-500/5 active:bg-cyan-500/15 hover:font-medium'} ${chromePopping === 'layout-ring' ? ' anet-chrome-pop' : ''}`}
2295
+ data-topo-chrome-layout-hover-preview-fw={layout === 'ring' ? null : '500'}
2296
+ data-topo-chrome-layout-ring-brightness-hover="1.15"
2297
+ /* R597 — Ring/Grid segmented buttons gain hover:
2298
+ brightness-[1.15] (37+38th anchors in per-element
2299
+ brightness family, 6+7th HTML). Paired-anchor round
2300
+ mirroring R596's zoom +/- closure at the second
2301
+ segmented control. Same segmented-unity rule
2302
+ (brightness = pure paint, no geometry break).
2303
+ Inline transition list extends with 'filter 150ms
2304
+ ease' so brightness eases under the existing R522
2305
+ 150ms beat that bg/color/fw share (letter-spacing
2306
+ + transform on their own 200ms/150ms cadences).
2307
+ R557 banked pattern: when an element has both
2308
+ inline transition AND new transition-driven axis,
2309
+ extend the INLINE list — not the className. */
2310
+ style={{ transition: 'background-color 150ms ease, color 150ms ease, letter-spacing 200ms ease-out, transform 150ms ease-out, font-weight 150ms ease, filter 150ms ease' }}
1971
2311
  >
1972
2312
  Ring
1973
2313
  </button>
@@ -1989,7 +2329,13 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1989
2329
  // R492 sibling — Grid button picks up active:scale-95
1990
2330
  // press feedback + transform in transition list. Same
1991
2331
  // vocabulary as Ring above.
1992
- className={`px-2.5 py-1 border-l focus:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset hover:tracking-wide active:scale-95 transform-gpu ${layout === 'grid' ? 'bg-cyan-500/15 text-cyan-300 font-medium hover:bg-cyan-500/20 active:bg-cyan-500/25' : 'text-gray-400 hover:text-cyan-300 hover:bg-cyan-500/5 active:bg-cyan-500/15'} ${chromePopping === 'layout-grid' ? ' anet-chrome-pop' : ''}`}
2332
+ /* Round 522 sibling Grid button mirrors Ring above:
2333
+ inactive variant gains `hover:font-medium` typography
2334
+ preview + font-weight 150ms in inline transition list.
2335
+ Same idiom, same family (R522 chrome layout). */
2336
+ className={`px-2.5 py-1 border-l focus:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset hover:tracking-wide hover:brightness-[1.15] active:scale-95 transform-gpu ${layout === 'grid' ? 'bg-cyan-500/15 text-cyan-300 font-medium hover:bg-cyan-500/20 hover:text-cyan-200 active:bg-cyan-500/25' : 'text-gray-400 hover:text-cyan-300 hover:bg-cyan-500/5 active:bg-cyan-500/15 hover:font-medium'} ${chromePopping === 'layout-grid' ? ' anet-chrome-pop' : ''}`}
2337
+ data-topo-chrome-layout-hover-preview-fw={layout === 'grid' ? null : '500'}
2338
+ data-topo-chrome-layout-grid-brightness-hover="1.15"
1993
2339
  /* Round 268 / Loop: Grid button's left border (the
1994
2340
  internal divider between Ring and Grid) picks up
1995
2341
  pal.containerBorder, matching the wrapper change at
@@ -2001,8 +2347,11 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2001
2347
  the border-color flip — border-color 200ms ease-out
2002
2348
  keeps R268's theme-toggle smoothness intact.
2003
2349
  R492 adds `transform 150ms ease-out` so active:scale-95
2004
- eases smoothly. */
2005
- 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' }}
2350
+ eases smoothly.
2351
+ R597 sibling Grid button mirrors Ring above:
2352
+ hover:brightness-[1.15] + filter 150ms ease in
2353
+ the inline transition list. */
2354
+ style={{ borderColor: pal.containerBorder, transition: 'background-color 150ms ease, color 150ms ease, border-color 200ms ease-out, letter-spacing 200ms ease-out, transform 150ms ease-out, font-weight 150ms ease, filter 150ms ease' }}
2006
2355
  >
2007
2356
  Grid
2008
2357
  </button>
@@ -2035,6 +2384,41 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2035
2384
  // lists once for both chips to share.
2036
2385
  const workingAliases = onlineNodes.filter(s => s.status === 'working').map(s => s.alias);
2037
2386
  const onlineAliases = onlineNodes.map(s => s.alias);
2387
+ /* Round 565 (50-round milestone) / Loop — extend the
2388
+ inspection-overrides-encoding family to a 7th anchor at
2389
+ the chip-row chip scope. Computes the hovered alias's
2390
+ status tier (same idiom as R562/R563) so each chip's
2391
+ className can include the "lit" bg/border treatment when
2392
+ operator hovers a node matching its tier.
2393
+ Family progression — 7 anchors complete:
2394
+ R484 recent-row timestamp alias hover
2395
+ R485 edge particle opacity alias hover
2396
+ R486 minimap dot opacity alias hover
2397
+ R561 group-label + ants-gate member-alias hover
2398
+ R562 legend-swatch r + glow member-alias status match
2399
+ R563 pressure-seg brightness member-alias status match
2400
+ R565 chip-row chip bg/border member-alias status match ← this round
2401
+ Status-tier-match feedback now SATURATES across panel
2402
+ chrome at 4 surfaces simultaneously:
2403
+ minimap dot (R486)
2404
+ legend swatch (R562)
2405
+ pressure-seg (R563)
2406
+ chip-row chip (R565) ← this round
2407
+ When operator hovers a 'working' node alias, ALL FOUR
2408
+ surfaces light up in green; 'idle' → all four in teal;
2409
+ 'offline' → all four in slate. The eye gets 4-way
2410
+ confirmation of "your inspected node is in this tier"
2411
+ across every persistent status-reference surface. */
2412
+ const hoveredAliasTierKey: 'working' | 'idle' | 'offline' | null = (() => {
2413
+ if (!hoveredAlias) return null;
2414
+ const s = onlineNodes.find(n => n.alias === hoveredAlias)
2415
+ ?? offlineNodes.find(n => n.alias === hoveredAlias);
2416
+ if (!s) return null;
2417
+ if (s.status === 'working') return 'working';
2418
+ return offlineNodes.includes(s) ? 'offline' : 'idle';
2419
+ })();
2420
+ const isWorkingChipLit = hoveredAliasTierKey === 'working';
2421
+ const isOnlineChipLit = hoveredAliasTierKey === 'idle';
2038
2422
  const truncate = (list: string[]) => {
2039
2423
  const head = list.slice(0, 8).join(', ');
2040
2424
  const tail = list.length > 8 ? ` + ${list.length - 8} more` : '';
@@ -2127,13 +2511,19 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2127
2511
  // R398 hover-lift conditional. Composes with hover:-
2128
2512
  // translate-y-px for the same lift-and-compress
2129
2513
  // tactile signature R493 brought to reset/fullscreen.
2514
+ /* R565: when isWorkingChipLit (operator hovers a working
2515
+ node), chip stays in its "lit" bg-green-500/15 +
2516
+ border-green-500/30 state at rest. Same visual as
2517
+ hover; member-alias-matching pins the lift without
2518
+ requiring cursor on the chip. */
2130
2519
  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 ${
2131
2520
  workingCount > 0
2132
- ? 'bg-green-500/10 text-green-300 border-green-500/20 hover:bg-green-500/15 hover:border-green-500/30 hover:-translate-y-px active:scale-95'
2521
+ ? `${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`
2133
2522
  : 'bg-green-500/10 text-green-300 border-green-500/20'
2134
2523
  }`}
2135
2524
  data-chip-hover-lift={workingCount > 0 ? 'true' : 'false'}
2136
2525
  data-chip-group-hover-brighten="true"
2526
+ data-working-chip-member-alias-lit={isWorkingChipLit ? 'true' : 'false'}
2137
2527
  data-working-chip
2138
2528
  data-working-chip-aliases={workingAliases.join(',')}
2139
2529
  data-pin-mirror={pinnedStatus === 'working' ? 'true' : 'false'}
@@ -2213,7 +2603,26 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2213
2603
  at the chip-count scope. Sibling edits on the
2214
2604
  online + active-links chip digits below. data-
2215
2605
  working-chip-digit attr exposes the digit span. */}
2216
- <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>
2606
+ {/* Round 539 / Loop chip-row digit gains group-hover:
2607
+ tracking-wide alongside the existing R362 group-
2608
+ hover:font-bold. Pre-R539 the chip digit lifted
2609
+ only on the font-weight axis (600 → 700 on chip
2610
+ hover); R539 adds the kerning axis (tracking
2611
+ normal → tracking-wide ≈ 0.025em ≈ 0.3px on a 12px
2612
+ digit) so hover lifts BOTH typography axes
2613
+ together — same idiom R420/R517 establish at the
2614
+ chrome zoom-level (letter-spacing + fontWeight
2615
+ hover delta) and R531/R530 mirror at the panel
2616
+ label scope. transition-[font-weight] extends to
2617
+ transition-[font-weight,letter-spacing] for the
2618
+ smooth dual-axis tween.
2619
+ Sibling treatment across the 3 chip-row digits
2620
+ (working / online / active-links) — single concept
2621
+ replicated at 3 surfaces by replace_all.
2622
+ Hover-letter-spacing family extension (12 anchors
2623
+ now): R344/R345/R347/R420/R427/R431/R432/R433/
2624
+ R434/R517/R518 + R539 (this round). */}
2625
+ <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>
2217
2626
  </span>
2218
2627
  <span
2219
2628
  // Round 201 / Loop: online chip — mirror of the working
@@ -2232,13 +2641,18 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2232
2641
  // R494 sibling — online chip joins the active:scale-95 press
2233
2642
  // family (gated on onlineNodes.length > 0 clickable branch,
2234
2643
  // same conditional pattern as the working chip above).
2644
+ /* R565: same lit-on-member-alias-match pattern as
2645
+ working chip — online chip routes hover to 'idle'
2646
+ tier (see onMouseEnter below), so its member-alias
2647
+ gate is `hoveredAliasTierKey === 'idle'`. */
2235
2648
  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 ${
2236
2649
  onlineNodes.length > 0
2237
- ? 'bg-cyan-500/10 text-cyan-300 border-cyan-500/20 hover:bg-cyan-500/15 hover:border-cyan-500/30 hover:-translate-y-px active:scale-95'
2650
+ ? `${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`
2238
2651
  : 'bg-cyan-500/10 text-cyan-300 border-cyan-500/20'
2239
2652
  }`}
2240
2653
  data-chip-hover-lift={onlineNodes.length > 0 ? 'true' : 'false'}
2241
2654
  data-chip-group-hover-brighten="true"
2655
+ data-online-chip-member-alias-lit={isOnlineChipLit ? 'true' : 'false'}
2242
2656
  data-online-chip
2243
2657
  data-online-chip-aliases={onlineAliases.join(',')}
2244
2658
  data-pin-mirror={pinnedStatus === 'idle' ? 'true' : 'false'}
@@ -2284,7 +2698,9 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2284
2698
  >
2285
2699
  {/* R337 sibling — online chip unit demotion. */}
2286
2700
  {/* R362 sibling — online-chip digit gains font-semibold. */}
2287
- <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>
2701
+ {/* R539 sibling online chip digit. Same idiom as
2702
+ working chip above (group-hover:tracking-wide). */}
2703
+ <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>
2288
2704
  </span>
2289
2705
  </>
2290
2706
  );
@@ -2299,6 +2715,31 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2299
2715
  const o = offlineNodes.length;
2300
2716
  const total = w + i + o;
2301
2717
  if (total === 0) return null;
2718
+ /* Round 563 / Loop — inspection-overrides-encoding family
2719
+ 6th anchor at the pressure-bar segment scope. When
2720
+ operator hovers a NODE ALIAS on the canvas, the segment
2721
+ matching that node's status tier lights up with its
2722
+ R210 brightness + R542 drop-shadow treatment — mirror
2723
+ of R562 legend-swatch pattern at the pressure-bar scope.
2724
+ Family progression (6 anchors):
2725
+ R484 recent-row timestamp alias hover
2726
+ R485 edge particle opacity alias hover
2727
+ R486 minimap dot opacity alias hover
2728
+ R561 group-label + ants-gate member-alias hover
2729
+ R562 legend-swatch r + glow member-alias status match
2730
+ R563 pressure-seg brightness member-alias status match ← this round
2731
+ Same status-tier-match computation as R562 (banked
2732
+ idiom): find the hovered alias's session, map to
2733
+ working/idle/offline tier, then per-segment check
2734
+ `hoveredAliasRowKey === key`. Computed once at IIFE
2735
+ scope, used inside the seg() closure. */
2736
+ const hoveredSession = hoveredAlias
2737
+ ? (onlineNodes.find(s => s.alias === hoveredAlias) ?? offlineNodes.find(s => s.alias === hoveredAlias))
2738
+ : null;
2739
+ const hoveredAliasTierKey: 'working' | 'idle' | 'offline' | null = !hoveredSession ? null
2740
+ : hoveredSession.status === 'working' ? 'working'
2741
+ : offlineNodes.includes(hoveredSession) ? 'offline'
2742
+ : 'idle';
2302
2743
  // Round 60 / Loop: each segment toggles a sticky filter via
2303
2744
  // `pinnedStatus`. Click the working segment → all non-working
2304
2745
  // nodes dim; click again → release. Segments share width with
@@ -2311,6 +2752,10 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2311
2752
  const seg = (n: number, color: string, key: 'working' | 'idle' | 'offline', label: string) => {
2312
2753
  if (n === 0) return null;
2313
2754
  const isPinned = pinnedStatus === key;
2755
+ // R563: member-alias-matching flag — when operator hovers
2756
+ // a node alias whose status matches this segment's tier.
2757
+ const isMemberAliasMatching = hoveredAliasTierKey === key;
2758
+ const isSegLit = hoveredStatus === key || isMemberAliasMatching;
2314
2759
  // R102: list the aliases that match this segment's bucket
2315
2760
  // so the title answers WHICH n, not just HOW MANY. Closes
2316
2761
  // the last "info-density gap" in the chip-row surfaces
@@ -2330,6 +2775,8 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2330
2775
  data-pressure-seg={key}
2331
2776
  data-pressure-seg-aliases={matchAliases.join(',')}
2332
2777
  data-pressure-seg-hovered={hoveredStatus === key ? 'true' : 'false'}
2778
+ data-pressure-seg-member-alias-matching={isMemberAliasMatching ? 'true' : 'false'}
2779
+ data-pressure-seg-lit={isSegLit ? 'true' : 'false'}
2333
2780
  role="button"
2334
2781
  tabIndex={0}
2335
2782
  aria-pressed={isPinned}
@@ -2373,7 +2820,47 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2373
2820
  height: '100%',
2374
2821
  cursor: 'pointer',
2375
2822
  boxShadow: isPinned ? `inset 0 0 0 1px ${color}, inset 0 0 0 2px rgba(255,255,255,0.6)` : undefined,
2376
- filter: hoveredStatus === key ? 'brightness(1.2)' : undefined,
2823
+ /* Round 542 / Loop pressure-bar segments gain
2824
+ drop-shadow tier-color glow on hover, stacked
2825
+ on R210 brightness(1.2). Sibling to R537 legend
2826
+ swatch + R541 vendor chip glow at the chip-row
2827
+ scope — three same-pattern surfaces (legend
2828
+ swatch / vendor chip / pressure segment) all
2829
+ radiate their identity color on hover.
2830
+ 3rd anchor in the chip-row tier-color paint
2831
+ glow sub-family:
2832
+ R537 legend swatch row.fill (status hex)
2833
+ R541 vendor chip v.color (hsl via color-mix)
2834
+ R542 pressure seg color (status hex) ← this round
2835
+ Stacked filter syntax (brightness + drop-shadow
2836
+ in same filter declaration): `brightness(1.2)
2837
+ drop-shadow(...)`. CSS filter supports multiple
2838
+ functions; they apply left-to-right. Brightness
2839
+ boosts the segment's own color, drop-shadow
2840
+ paints the outer halo. Together: hovered seg
2841
+ looks "lit up" with both inner glow + outer
2842
+ halo in its tier color.
2843
+ Hue: `${color}99` hex+alpha (60%) — color here
2844
+ is a 6-char hex (e.g., '#22c55e' for working
2845
+ cyber, '#0d9488' for idle light), not hsl, so
2846
+ hex+alpha concat works (unlike R541 vendor
2847
+ which needed color-mix for hsl). Banked
2848
+ pattern: hex sources use hex+alpha; hsl/color()
2849
+ sources use color-mix.
2850
+ 2px blur (vs R537's 3px) since pressure-seg is
2851
+ small (h-2 = 8px tall, variable width) — a
2852
+ smaller blur keeps the glow tight to the
2853
+ segment without bleeding into neighbors.
2854
+ filter is paint-only; bbox unchanged; R51
2855
+ overlap-test invariants hold. Transition list
2856
+ already includes `filter` (post-R524). */
2857
+ /* R563: filter lifts on EITHER direct hover OR member-
2858
+ alias-matching (operator inspecting a node whose
2859
+ status matches this segment's tier). Same R210
2860
+ brightness + R542 drop-shadow value across both
2861
+ gate sources — uniform visual response, distinct
2862
+ semantic gates. */
2863
+ filter: isSegLit ? `brightness(1.2) drop-shadow(0 0 2px ${color}99)` : undefined,
2377
2864
  transition: 'width 220ms ease-out, box-shadow 150ms ease-out, filter 150ms ease-out',
2378
2865
  }}
2379
2866
  onClick={(e) => {
@@ -2518,6 +3005,33 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2518
3005
  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"
2519
3006
  title={matchCount > 0 ? `${matchPreview}${matchSuffix} — click to clear` : 'Click to clear filter'}
2520
3007
  onClick={() => setPinnedStatus(null)}
3008
+ /* Round 543 / Loop — status filter pill gains always-on
3009
+ tier-color drop-shadow when rendered. Pre-R543 the
3010
+ pill carried bg-tint + tier-color text + border but
3011
+ no outer paint extent — it sat as a flat tinted chip
3012
+ in the chip row. R543 adds an outer glow at the
3013
+ pill's text color so the pill radiates a soft tier-
3014
+ colored halo signaling "this filter is active." Pin
3015
+ pill only renders when pinnedStatus is set (the JSX
3016
+ gate above), so the drop-shadow appearing reinforces
3017
+ the visual "active pin" state.
3018
+ Sibling pattern: R477 legend pin-ring also paints a
3019
+ pin-gated tier-color drop-shadow. Pin pill follows
3020
+ the same "pin-gated paint glow" semantics but at the
3021
+ chip-row scope vs the panel-row scope. The chip-row
3022
+ tier-color glow trio (R537/R541/R542 hover-gated)
3023
+ plus R543 (pin-gated, this round) closes the chip-
3024
+ row paint-glow family across BOTH gate types
3025
+ (hover for transient affordance, pin for sticky
3026
+ active-state visual).
3027
+ Hue: explicit tier color (extracted from the
3028
+ existing `color` ternary). 0x99 alpha (~60%) +
3029
+ 3px blur. Stays inside the same color hierarchy
3030
+ as the pill's own text/border (currentColor).
3031
+ R543 status pill scope only — R543's pattern can
3032
+ future-extend to group/vendor/edge filter pills
3033
+ (3 more variants at lines 2683/2755/2824). Out of
3034
+ scope to keep R543 single-pill. */
2521
3035
  style={{
2522
3036
  background: pinnedStatus === 'working' ? (isLight ? '#05966914' : '#22c55e1f')
2523
3037
  : pinnedStatus === 'idle' ? (isLight ? '#0d948814' : '#2dd4bf1f')
@@ -2527,6 +3041,11 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2527
3041
  : (isLight ? '#475569' : '#9ca3af'),
2528
3042
  borderColor: 'currentColor',
2529
3043
  cursor: 'pointer',
3044
+ filter: `drop-shadow(0 0 3px ${
3045
+ pinnedStatus === 'working' ? (isLight ? '#047857' : '#86efac')
3046
+ : pinnedStatus === 'idle' ? (isLight ? '#0f766e' : '#5eead4')
3047
+ : (isLight ? '#475569' : '#9ca3af')
3048
+ }99)`,
2530
3049
  }}
2531
3050
  >
2532
3051
  {/* Round 412 / Loop: filter pin pill VALUE picks up the
@@ -2540,7 +3059,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2540
3059
  to R333/R335-R341/R362/R369/R389/R410. data-filter-
2541
3060
  value attr surfaces the value span for tests.
2542
3061
  4-pill replace family — status / group / vendor / edge. */}
2543
- <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>
3062
+ <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>
2544
3063
  <button
2545
3064
  type="button"
2546
3065
  aria-label={`Clear ${pinnedStatus} filter`}
@@ -2559,7 +3078,24 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2559
3078
  inline-block is default for <button> so no display
2560
3079
  tweak needed. replace_all covers all 4 filter pin
2561
3080
  pills (status / group / vendor / edge) at once. */
2562
- className="ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 transform-gpu"
3081
+ /* Round 547 / Loop — extends pill × close-button hover
3082
+ gesture from scale-110 (R356) + opacity-70 to ALSO
3083
+ include rotate-12 on hover. Pre-R547 the × dimmed
3084
+ and grew on hover; R547 adds a 12° twist so the
3085
+ close action telegraphs "discarding/spinning away"
3086
+ with a small delight gesture. Composes with
3087
+ transition-transform (existing) — Tailwind's
3088
+ hover:rotate-12 + hover:scale-110 stack into one
3089
+ transform under the same 200ms ease-out tween.
3090
+ Applied to all 4 pill × buttons (status / group /
3091
+ vendor / edge) via replace_all since the className
3092
+ is identical. Closes the pill × hover gesture
3093
+ vocabulary at 3 axes:
3094
+ hover:opacity-70 paint dim
3095
+ hover:scale-110 geometry grow (R356)
3096
+ hover:rotate-12 geometry twist (R547, this round)
3097
+ Hover-gesture parity across the 4-pill family. */
3098
+ className="ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 hover:rotate-12 transform-gpu"
2563
3099
  style={{ background: 'transparent', color: 'inherit', cursor: 'pointer', padding: 0 }}
2564
3100
  >×</button>
2565
3101
  </span>
@@ -2588,15 +3124,42 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2588
3124
  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"
2589
3125
  title={matchCount > 0 ? `${matchPreview}${matchSuffix} — click to clear` : 'Click to clear filter'}
2590
3126
  onClick={() => setPinnedGroup(null)}
3127
+ /* Round 544 / Loop — extends R543 pin-active filter-pill
3128
+ drop-shadow pattern to the GROUP pill (2nd of 4 pill
3129
+ variants). Pre-R544 the group pill carried bg-tint +
3130
+ pal.legendAccent text/border but no outer paint glow.
3131
+ R544 adds the matching cyan-accent drop-shadow so the
3132
+ group pin pill radiates the same paint glow as R543
3133
+ status pill — pin-active visual signal at chip-row
3134
+ scope.
3135
+ Hue: pal.legendAccent (cyber #67e8f9 cyan-300 /
3136
+ light #0d9488 teal-600). Uses color-mix() syntax
3137
+ because pal.legendAccent may resolve to hex; same
3138
+ syntax works for both hex and hsl sources (banked
3139
+ R541 lesson). 60% alpha + 3px blur — same intensity
3140
+ as R543 status pill so the pin-active visual signal
3141
+ reads with matching brightness across pill variants.
3142
+ Pin-active tier-color paint glow sub-family
3143
+ (CLOSED progressively):
3144
+ R477 legend pin-ring (panel-row, row.fill)
3145
+ R543 status pill (chip-row, tier-color)
3146
+ R544 group pill (chip-row, legendAccent)
3147
+ ← this round
3148
+ Out of scope: vendor pill (line ~2755) + edge pill
3149
+ (line ~2824) — can future-extend in subsequent
3150
+ rounds (R545/R546). Both use the same R543 idiom:
3151
+ always-on drop-shadow when rendered, color from the
3152
+ pill's existing text color. */
2591
3153
  style={{
2592
3154
  background: isLight ? '#67e8f914' : '#67e8f91f',
2593
3155
  color: pal.legendAccent,
2594
3156
  borderColor: 'currentColor',
2595
3157
  cursor: 'pointer',
3158
+ filter: `drop-shadow(0 0 3px color-mix(in srgb, ${pal.legendAccent} 60%, transparent))`,
2596
3159
  }}
2597
3160
  >
2598
3161
  {/* R412: see status pill above — filter value fw=600 data tier. */}
2599
- <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>
3162
+ <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>
2600
3163
  <button
2601
3164
  type="button"
2602
3165
  aria-label={`Clear group filter ${pinnedGroup}`}
@@ -2615,7 +3178,24 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2615
3178
  inline-block is default for <button> so no display
2616
3179
  tweak needed. replace_all covers all 4 filter pin
2617
3180
  pills (status / group / vendor / edge) at once. */
2618
- className="ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 transform-gpu"
3181
+ /* Round 547 / Loop — extends pill × close-button hover
3182
+ gesture from scale-110 (R356) + opacity-70 to ALSO
3183
+ include rotate-12 on hover. Pre-R547 the × dimmed
3184
+ and grew on hover; R547 adds a 12° twist so the
3185
+ close action telegraphs "discarding/spinning away"
3186
+ with a small delight gesture. Composes with
3187
+ transition-transform (existing) — Tailwind's
3188
+ hover:rotate-12 + hover:scale-110 stack into one
3189
+ transform under the same 200ms ease-out tween.
3190
+ Applied to all 4 pill × buttons (status / group /
3191
+ vendor / edge) via replace_all since the className
3192
+ is identical. Closes the pill × hover gesture
3193
+ vocabulary at 3 axes:
3194
+ hover:opacity-70 paint dim
3195
+ hover:scale-110 geometry grow (R356)
3196
+ hover:rotate-12 geometry twist (R547, this round)
3197
+ Hover-gesture parity across the 4-pill family. */
3198
+ className="ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 hover:rotate-12 transform-gpu"
2619
3199
  style={{ background: 'transparent', color: 'inherit', cursor: 'pointer', padding: 0 }}
2620
3200
  >×</button>
2621
3201
  </span>
@@ -2660,15 +3240,35 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2660
3240
  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"
2661
3241
  title={matchCount > 0 ? `${matchPreview}${matchSuffix} — click to clear` : 'Click to clear vendor filter'}
2662
3242
  onClick={() => setPinnedVendor(null)}
3243
+ /* Round 545 / Loop — extends pin-active filter-pill drop-
3244
+ shadow pattern to VENDOR pill (3rd of 4 pill variants
3245
+ after R543 status + R544 group). vendorColor is HSL
3246
+ format (banked R541 lesson — vendorDist.color sources
3247
+ from mono.text in vendorIdentity.ts, which is `hsl(...)`),
3248
+ so the filter uses color-mix() syntax — same as R544.
3249
+ 60% alpha + 3px blur, matching R543/R544 intensity for
3250
+ consistent pin-active visual signal across all pill
3251
+ variants.
3252
+ Pin-active tier-color paint glow sub-family (progressive
3253
+ extension, 1 pill variant remaining):
3254
+ R477 legend pin-ring (panel-row, row.fill, hex+alpha)
3255
+ R543 status pill (chip-row, tier-color, hex+alpha)
3256
+ R544 group pill (chip-row, legendAccent, color-mix)
3257
+ R545 vendor pill (chip-row, vendorColor, color-mix)
3258
+ ← this round
3259
+ Out of scope: edge pill (line ~2824 pre-R545, now ~2900+).
3260
+ Final 1/4 pill remaining for a future round closes the
3261
+ sub-family. */
2663
3262
  style={{
2664
3263
  background: `${vendorColor}1f`,
2665
3264
  color: vendorColor,
2666
3265
  borderColor: 'currentColor',
2667
3266
  cursor: 'pointer',
3267
+ filter: `drop-shadow(0 0 3px color-mix(in srgb, ${vendorColor} 60%, transparent))`,
2668
3268
  }}
2669
3269
  >
2670
3270
  {/* R412: see status pill above — filter value fw=600 data tier. */}
2671
- <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>
3271
+ <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>
2672
3272
  <button
2673
3273
  type="button"
2674
3274
  aria-label={`Clear vendor filter ${pinnedVendor}`}
@@ -2687,7 +3287,24 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2687
3287
  inline-block is default for <button> so no display
2688
3288
  tweak needed. replace_all covers all 4 filter pin
2689
3289
  pills (status / group / vendor / edge) at once. */
2690
- className="ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 transform-gpu"
3290
+ /* Round 547 / Loop — extends pill × close-button hover
3291
+ gesture from scale-110 (R356) + opacity-70 to ALSO
3292
+ include rotate-12 on hover. Pre-R547 the × dimmed
3293
+ and grew on hover; R547 adds a 12° twist so the
3294
+ close action telegraphs "discarding/spinning away"
3295
+ with a small delight gesture. Composes with
3296
+ transition-transform (existing) — Tailwind's
3297
+ hover:rotate-12 + hover:scale-110 stack into one
3298
+ transform under the same 200ms ease-out tween.
3299
+ Applied to all 4 pill × buttons (status / group /
3300
+ vendor / edge) via replace_all since the className
3301
+ is identical. Closes the pill × hover gesture
3302
+ vocabulary at 3 axes:
3303
+ hover:opacity-70 paint dim
3304
+ hover:scale-110 geometry grow (R356)
3305
+ hover:rotate-12 geometry twist (R547, this round)
3306
+ Hover-gesture parity across the 4-pill family. */
3307
+ className="ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 hover:rotate-12 transform-gpu"
2691
3308
  style={{ background: 'transparent', color: 'inherit', cursor: 'pointer', padding: 0 }}
2692
3309
  >×</button>
2693
3310
  </span>
@@ -2726,11 +3343,34 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2726
3343
  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"
2727
3344
  title={`${link.from} → ${link.to} (${link.count} msg${link.count === 1 ? '' : 's'}${isHot ? ', hot lane · ≥ 10' : ''}) — click to clear`}
2728
3345
  onClick={() => setPinnedEdgeKey(null)}
3346
+ /* Round 546 / Loop — CLOSES pin-active filter-pill drop-
3347
+ shadow sub-family at the 4th and final pill variant
3348
+ (edge pill). R543 (status) + R544 (group) + R545
3349
+ (vendor) covered the first three; R546 closes at
3350
+ edge.
3351
+ Pin-active tier-color paint glow sub-family CLOSED
3352
+ (4 anchors):
3353
+ R477 legend pin-ring (panel-row, row.fill)
3354
+ R543 status pill (chip-row, tier-color text)
3355
+ R544 group pill (chip-row, legendAccent)
3356
+ R545 vendor pill (chip-row, vendorColor)
3357
+ R546 edge pill (chip-row, pal.flowEdge)
3358
+ ← this round, family CLOSED
3359
+ All 4 filter pin pills now radiate paint glow in the
3360
+ same hue family as their text/border on render —
3361
+ pin-active visual signal uniform across the chip-row
3362
+ pill family.
3363
+ pal.flowEdge is theme-driven (dynamic); color-mix
3364
+ syntax safe-defaults regardless of resolved format
3365
+ (banked R541/R544/R545 pattern). 60% alpha + 3px blur
3366
+ — same intensity as R543/R544/R545 for consistent
3367
+ cross-pill visual signal. */
2729
3368
  style={{
2730
3369
  background: isLight ? `${pal.flowEdge}14` : `${pal.flowEdge}1f`,
2731
3370
  color: pal.flowEdge,
2732
3371
  borderColor: 'currentColor',
2733
3372
  cursor: 'pointer',
3373
+ filter: `drop-shadow(0 0 3px color-mix(in srgb, ${pal.flowEdge} 60%, transparent))`,
2734
3374
  }}
2735
3375
  >
2736
3376
  {/* R412: filter pin pill value (edge variant) picks up fw=600.
@@ -2783,7 +3423,24 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2783
3423
  inline-block is default for <button> so no display
2784
3424
  tweak needed. replace_all covers all 4 filter pin
2785
3425
  pills (status / group / vendor / edge) at once. */
2786
- className="ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 transform-gpu"
3426
+ /* Round 547 / Loop — extends pill × close-button hover
3427
+ gesture from scale-110 (R356) + opacity-70 to ALSO
3428
+ include rotate-12 on hover. Pre-R547 the × dimmed
3429
+ and grew on hover; R547 adds a 12° twist so the
3430
+ close action telegraphs "discarding/spinning away"
3431
+ with a small delight gesture. Composes with
3432
+ transition-transform (existing) — Tailwind's
3433
+ hover:rotate-12 + hover:scale-110 stack into one
3434
+ transform under the same 200ms ease-out tween.
3435
+ Applied to all 4 pill × buttons (status / group /
3436
+ vendor / edge) via replace_all since the className
3437
+ is identical. Closes the pill × hover gesture
3438
+ vocabulary at 3 axes:
3439
+ hover:opacity-70 paint dim
3440
+ hover:scale-110 geometry grow (R356)
3441
+ hover:rotate-12 geometry twist (R547, this round)
3442
+ Hover-gesture parity across the 4-pill family. */
3443
+ className="ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 hover:rotate-12 transform-gpu"
2787
3444
  style={{ background: 'transparent', color: 'inherit', cursor: 'pointer', padding: 0 }}
2788
3445
  >×</button>
2789
3446
  </span>
@@ -3105,6 +3762,41 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3105
3762
  // for older browsers the chip falls back to its idle
3106
3763
  // transparent bg (graceful degradation — the canvas-
3107
3764
  // dim effect still fires regardless).
3765
+ /* Round 541 / Loop — vendor letter chip gains drop-
3766
+ shadow glow on hover/pin using its OWN vendor
3767
+ identity color (v.color). Sibling to R537 legend-
3768
+ swatch tier-color glow at the chip-row scope.
3769
+ Pre-R541 the vendor chip lifted on multiple
3770
+ axes (R354 inner-glyph scale-1.1, R202 bg color-
3771
+ mix tint, R180 box-shadow pin-mirror inset, R401
3772
+ hover-translate-y -1px, R496 active:scale-95
3773
+ press) but no paint-axis glow extending past
3774
+ the chip's bbox. R541 adds the outer glow at
3775
+ the paint axis so the vendor chip's identity
3776
+ color radiates beyond the chip on attention —
3777
+ same idiom as legend swatch tier-color glow.
3778
+ 2-tier alpha ladder (mirrors R538 group-label):
3779
+ pin (committed) v.color 99 (~60%)
3780
+ hover (preview) v.color 66 (~40%)
3781
+ rest none
3782
+ Pin is brighter to distinguish locked vs preview
3783
+ at the paint axis. The R180 inset box-shadow
3784
+ (pin-mirror) and R541 outer drop-shadow compose
3785
+ at pin — inside chrome reads as "this is pinned"
3786
+ (inset white double-ring), outside paint reads
3787
+ as "vendor identity is locked" (vendor-colour
3788
+ outer glow). Hover gets only the outer glow.
3789
+ 3px blur tuned to read as soft chip-halo without
3790
+ overwhelming adjacent chips in the chip row.
3791
+ filter property is in the .anet-topo-chip-focus
3792
+ class transition list (R524 banked fix), so the
3793
+ filter eases at 200ms naturally.
3794
+ Drop-shadow visual-polish family — R541 adds
3795
+ chip-row tier-color paint glow as another anchor
3796
+ in the same family pattern R537 established.
3797
+ data-vendor-glow attr ('pin' | 'hover' | 'false')
3798
+ exposes the gate state for tests. */
3799
+ data-vendor-glow={isPinned ? 'pin' : hoveredVendor === v.initial ? 'hover' : 'false'}
3108
3800
  style={{
3109
3801
  cursor: 'pointer',
3110
3802
  backgroundColor: (hoveredVendor === v.initial && !isPinned)
@@ -3113,7 +3805,15 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3113
3805
  boxShadow: isPinned
3114
3806
  ? `inset 0 0 0 1px ${v.color}, inset 0 0 0 2px rgba(255,255,255,0.45)`
3115
3807
  : undefined,
3116
- transition: 'box-shadow 150ms ease-out, background-color 200ms ease-out',
3808
+ /* R578 sibling vendor chip stacks brightness(1.15)
3809
+ onto R541 drop-shadow. Closes chip-row tier-color
3810
+ glow trio at consistent stacked-filter pattern. */
3811
+ filter: isPinned
3812
+ ? `drop-shadow(0 0 3px color-mix(in srgb, ${v.color} 60%, transparent)) brightness(1.15)`
3813
+ : hoveredVendor === v.initial
3814
+ ? `drop-shadow(0 0 3px color-mix(in srgb, ${v.color} 40%, transparent)) brightness(1.15)`
3815
+ : undefined,
3816
+ transition: 'box-shadow 150ms ease-out, background-color 200ms ease-out, filter 200ms ease-out',
3117
3817
  }}
3118
3818
  onMouseEnter={() => setHoveredVendor(v.initial)}
3119
3819
  onMouseLeave={() => setHoveredVendor(prev => prev === v.initial ? null : prev)}
@@ -3205,7 +3905,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3205
3905
  since the glyph (R369 fw=600) stays at full
3206
3906
  opacity. R333 :{count} format preserved. */}
3207
3907
  <span
3208
- className="text-gray-400 tabular-nums opacity-70 transition-opacity duration-200 group-hover:opacity-100"
3908
+ className="text-gray-400 tabular-nums opacity-70 transition-[opacity,letter-spacing] duration-200 group-hover:opacity-100 group-hover:tracking-wide"
3209
3909
  data-vendor-letter-count-suffix
3210
3910
  >:{v.count}</span>
3211
3911
  </span>
@@ -3355,7 +4055,9 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3355
4055
  chip-internal-hierarchy arc. data-active-links-
3356
4056
  chip-unit exposes the unit span for tests. */}
3357
4057
  {/* R362 sibling — active-links chip digit gains font-semibold. */}
3358
- <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>
4058
+ {/* R539 sibling active-links chip digit. Same idiom
4059
+ as working + online above. */}
4060
+ <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>
3359
4061
  {rel ? (() => {
3360
4062
  // Round 161 / Loop: extend R160's recency-pip
3361
4063
  // vocabulary up one scope — from per-flow row to
@@ -3569,6 +4271,36 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3569
4271
  on the canvas root for non-visual consumers.
3570
4272
  Composed from existing onlineNodes / workingCount /
3571
4273
  offlineNodes / flowLinks — no new state. */
4274
+ /* Round 502 / Loop — categorical density-tier paired with the
4275
+ R469 numeric counts. data-topo-fleet-density-tier classifies
4276
+ the fleet size into 5 buckets so external consumers (CSS
4277
+ selectors, Playwright probes, future density-conditional
4278
+ polish gates like R109 dense-label collapse at 16+ nodes)
4279
+ can branch on a stable tier name without re-deriving the
4280
+ threshold logic from the raw numeric. Buckets:
4281
+ 'empty' — onlineNodes.length === 0
4282
+ 'sparse' — 1-3 nodes
4283
+ 'normal' — 4-15 nodes
4284
+ 'dense' — 16-30 nodes (matches R109 collapse gate)
4285
+ 'very-dense' — 31+ nodes
4286
+ Picks the gate boundaries that already drive CONDITIONAL
4287
+ RENDER decisions elsewhere (R109 denseLayout = >16, R110
4288
+ plain-text fallback) so the tier name is semantically
4289
+ aligned with the visual mode the canvas already switches
4290
+ to. Composed from existing onlineNodes — no new state.
4291
+ 12th attr in the canvas state surface set (R462/R466/R467/
4292
+ R469×4/R471×2/R487/R488/R502). 12 attrs covers: build
4293
+ identity, transient/sticky inspection modes, fleet split
4294
+ numerics, fleet density tier, canvas layout/theme, canvas
4295
+ zoom, hover identity. A test harness can snapshot the
4296
+ full canvas state with 12 getAttribute calls. */
4297
+ data-topo-fleet-density-tier={
4298
+ onlineNodes.length === 0 ? 'empty' :
4299
+ onlineNodes.length <= 3 ? 'sparse' :
4300
+ onlineNodes.length <= 15 ? 'normal' :
4301
+ onlineNodes.length <= 30 ? 'dense' :
4302
+ 'very-dense'
4303
+ }
3572
4304
  data-topo-online-count={onlineNodes.length}
3573
4305
  data-topo-working-count={workingCount}
3574
4306
  data-topo-offline-count={offlineNodes.length}
@@ -3634,6 +4366,119 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3634
4366
  categorical) — separate dedicated attrs if/when needed.
3635
4367
  Root svg attribute set now 11 attrs total. */
3636
4368
  data-topo-hovered-alias={hoveredAlias ?? ''}
4369
+ /* Round 504 / Loop — categorical pin-aspect attr paired with
4370
+ R467 any-pinned boolean and R488 hovered-alias identity.
4371
+ Pre-R504 the canvas state surface set told tests WHETHER
4372
+ any pin was active (R467 boolean) but tests had to enumerate
4373
+ 4 individual state vars to determine WHICH pin axis fired:
4374
+ pinnedStatus legend-row status filter
4375
+ pinnedGroup prefix-cluster lock
4376
+ pinnedVendor vendor-chip filter
4377
+ pinnedEdgeKey edge-focus
4378
+ R504 surfaces the active aspect as a single categorical
4379
+ attribute: data-topo-pinned-aspect ∈
4380
+ 'none' no pin active
4381
+ 'status' pinnedStatus only
4382
+ 'group' pinnedGroup only
4383
+ 'vendor' pinnedVendor only
4384
+ 'edge' pinnedEdgeKey only
4385
+ 'multi' 2 or more pins active simultaneously
4386
+ ('multi' covers cross-cutting filters — e.g. user pins
4387
+ status='working' AND vendor='claude' simultaneously to
4388
+ narrow the canvas. Each pin axis is independently
4389
+ dismissable via Esc / individual chip click, so multi
4390
+ states are reachable and worth surfacing as a distinct
4391
+ tier.)
4392
+ 13th attr in the canvas state surface set after R502.
4393
+ Composed from 4 existing state vars — no new state. */
4394
+ /* Round 512 / Loop — 14th canvas state attr. groupBoxes.length
4395
+ surfaces the count of cluster boxes currently rendered in
4396
+ grid layout (always 0 in ring). Paired with R502 categorical
4397
+ density tier + R469 fleet numerics for a complete cluster-
4398
+ cardinality surface:
4399
+ R469 data-topo-online-count node-count
4400
+ R502 data-topo-fleet-density-tier categorical
4401
+ R512 data-topo-cluster-count cluster-count ← this round
4402
+ Use cases:
4403
+ - Playwright: assert orphan-band existence by
4404
+ `cluster-count === N + 1` vs prefix-only `=== N`
4405
+ - external CSS: `[data-topo-cluster-count='1']` to apply
4406
+ single-cluster grid-specific layout adjustments
4407
+ - future polish gates: cluster-count > N could trigger
4408
+ dense-grid mode
4409
+ Composed from existing `groupBoxes.length` — no new state.
4410
+ Always renders (0 in ring layout, N in grid), so tests can
4411
+ rely on attribute presence + value. */
4412
+ data-topo-cluster-count={groupBoxes.length}
4413
+ /* Round 513 / Loop — 15th canvas state attr. Surfaces the
4414
+ user's prefers-reduced-motion preference directly on the
4415
+ root SVG so external CSS / Playwright tests can branch on
4416
+ a11y state without re-reading the media query.
4417
+ reducedMotion is already in component scope (R29 a11y
4418
+ blanket reads it via a useEffect listener); R513 just
4419
+ exposes it as a stable attribute handle.
4420
+ Use cases:
4421
+ - Playwright: assert reduced-motion gates from one attr
4422
+ read instead of mocking media-query state per test
4423
+ - External CSS hooks: `[data-topo-prefers-reduced-motion=
4424
+ "true"]` to apply paint-only overrides (e.g. mute
4425
+ hover glows entirely on a11y instead of just
4426
+ disabling transitions)
4427
+ - Future polish rounds: any motion-gated render can
4428
+ read this attr server-side without the media-query
4429
+ hydration mismatch risk
4430
+ 'true' / 'false' string values (consistent with R466/R467
4431
+ boolean attrs). */
4432
+ data-topo-prefers-reduced-motion={reducedMotion ? 'true' : 'false'}
4433
+ /* Round 515 / Loop — 16th canvas state attr. Surfaces the
4434
+ fullscreen-mode state directly on root SVG so external
4435
+ consumers don't have to traverse the chrome strip's
4436
+ `data-topo-chrome-fullscreen-active` button attr (which
4437
+ measures the BUTTON state, not the canvas state — they
4438
+ agree, but reading from the root is semantically cleaner
4439
+ for canvas-state probes).
4440
+ Composed from existing isFullscreen React state (R103
4441
+ fullscreen toggle).
4442
+ Use cases:
4443
+ - Playwright: assert canvas mode in one attr read
4444
+ (paired with R471 data-topo-layout for ring/grid +
4445
+ R487 data-topo-zoom for zoom level + R513 reduced-
4446
+ motion for a11y mode = 4-axis canvas-mode probe)
4447
+ - External CSS: `[data-topo-fullscreen="true"]` to
4448
+ apply fullscreen-only paint adjustments outside the
4449
+ React tree (e.g. body-level scrollbar hide)
4450
+ 'true' / 'false' string values (consistent with R466/
4451
+ R467/R513 boolean attrs). */
4452
+ data-topo-fullscreen={isFullscreen ? 'true' : 'false'}
4453
+ /* Round 516 / Loop — 17th canvas state attr. Surfaces the
4454
+ grid layout's content-bottom y-coordinate so tests can
4455
+ verify grid content doesn't extend past the viewBox or
4456
+ collide with chrome elements positioned below the canvas.
4457
+ Composed from existing gridContentBottom derived state
4458
+ (computed at line ~915 from gy0 + totalRows * cellH + 8).
4459
+ In ring layout, gridContentBottom is 0 (no grid). In grid
4460
+ layout it's the actual pixel y-coordinate where the
4461
+ cluster bands end.
4462
+ Use cases:
4463
+ - Playwright: assert grid layout doesn't exceed viewBox
4464
+ height (680) without re-computing the layout math
4465
+ - External CSS: `[data-topo-grid-content-bottom='0']` to
4466
+ distinguish ring-mode (no grid content) from grid-mode
4467
+ in CSS without parsing layout attr
4468
+ - Future polish gates: if cluster count grows large
4469
+ enough to push grid bottom past viewBox, can trigger
4470
+ a 'compact' mode automatically */
4471
+ data-topo-grid-content-bottom={gridContentBottom}
4472
+ data-topo-pinned-aspect={(() => {
4473
+ const aspects: string[] = [];
4474
+ if (pinnedStatus) aspects.push('status');
4475
+ if (pinnedGroup) aspects.push('group');
4476
+ if (pinnedVendor) aspects.push('vendor');
4477
+ if (pinnedEdgeKey) aspects.push('edge');
4478
+ if (aspects.length === 0) return 'none';
4479
+ if (aspects.length === 1) return aspects[0];
4480
+ return 'multi';
4481
+ })()}
3637
4482
  /* Round 466 / Loop — aggregate hover signal on the root SVG.
3638
4483
  Exposes a single boolean `data-topo-any-hover` that
3639
4484
  reflects whether ANY hover state in the topology is
@@ -3876,7 +4721,38 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3876
4721
  const x = ((seed * 13) % 1000);
3877
4722
  const y = ((seed * 7) % 680);
3878
4723
  const r = (i % 3 === 0) ? 1.2 : 0.7;
3879
- return <circle key={i} cx={x} cy={y} r={r} fill="#a5b4fc" opacity={0.35 + (i % 4) * 0.05} data-topo-starfield-dot={i} />;
4724
+ /* Round 523 / Loop 配色 family extension to a 3rd anchor.
4725
+ Pre-R523 all 14 starfield dots painted at the same
4726
+ hardcoded `#a5b4fc` (indigo-300). The starfield's role
4727
+ is atmospheric depth (R45, R291 comment), but a flat
4728
+ single-hue field reads more like a regular dot grid
4729
+ than a star field — real starlight has color
4730
+ temperature variation (blue-white hot stars / yellow
4731
+ sun-like / cool red).
4732
+ R523 cycles a 3-color deterministic rotation based on
4733
+ `i % 3`:
4734
+ i % 3 === 0 → #a5b4fc indigo-300 (original, cool)
4735
+ i % 3 === 1 → #67e8f9 cyan-300 (cyber accent, hot)
4736
+ i % 3 === 2 → #cbd5e1 slate-300 (neutral, warm white)
4737
+ All three hues sit inside the cyber theme's palette
4738
+ family (indigo / cyan / slate) so the starfield reads
4739
+ varied-but-coherent rather than rainbow. At opacity
4740
+ 0.5 (parent <g>) * 0.35-0.50 (per-dot) the temperature
4741
+ shifts are gentle but perceptible — closes the gap
4742
+ between 'dot grid' and 'star field'.
4743
+ 配色 family extension (3 anchors): R509/R510 hub-
4744
+ highlight cross-theme fill + R523 starfield color
4745
+ temperature variation. Light theme unaffected
4746
+ (starfield gated `!isLight` so light theme stays
4747
+ clean per R45's original 'white surface stays clean'
4748
+ intent).
4749
+ Deterministic on `i` — no JS hydration mismatch,
4750
+ same SSR/client output. data-topo-starfield-dot-hue
4751
+ attr exposes the resolved hue category for tests. */
4752
+ const hues = ['#a5b4fc', '#67e8f9', '#cbd5e1'] as const;
4753
+ const hueNames = ['indigo', 'cyan', 'slate'] as const;
4754
+ const hueIdx = i % 3;
4755
+ 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]} />;
3880
4756
  })}
3881
4757
  </g>
3882
4758
  )}
@@ -4414,8 +5290,60 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4414
5290
  data-topo-hub-spoke-stroke-width={spokeStrokeWidth}
4415
5291
  data-topo-hub-spoke-stroke-width-active="2.25"
4416
5292
  data-topo-hub-spoke-linecap="round"
5293
+ /* Round 533 / Loop — extends drop-shadow visual-polish
5294
+ family to a 9th anchor: hub spokes gain filter:drop-
5295
+ shadow glow on hub-hover. Subtle 1.5px cyan/teal blur
5296
+ applied across ALL spokes simultaneously when the
5297
+ user hovers the hub — the network mesh visually
5298
+ "lights up" in response to focal attention. Sibling
5299
+ to R476 hub-digit + R532 hub-highlight glow at the
5300
+ same gate (hoveredHub && !reducedMotion); together
5301
+ the three anchors (digit + highlight disc + spokes)
5302
+ form a unified focal-cluster glow that signals
5303
+ "you're focused on the hub" across geometry,
5304
+ paint, and mesh-extent axes.
5305
+ Theme-aware glow palette matches the spoke stroke
5306
+ family:
5307
+ light: rgba(13, 148, 136, 0.4) teal-600
5308
+ cyber: rgba(34, 211, 238, 0.4) cyan-400
5309
+ 0.4 alpha keeps the glow subtle across N spokes
5310
+ (30+ at peak fleet sizes) — loud bloom across many
5311
+ edges would compete with the focal cluster itself.
5312
+ 1.5px blur is conservative; tuned so each spoke
5313
+ gains a faint outer halo rather than a wide bloom.
5314
+ filter is paint-only; bbox unchanged; existing
5315
+ R241 transition list extends to 'filter 250ms
5316
+ ease-out' matching the spoke transition cadence
5317
+ (250ms, distinct from the 200ms hub-cluster
5318
+ cadence — spokes ease slightly slower since they
5319
+ respond to per-alias state, not just hub state).
5320
+ data-topo-hub-spoke-glow attr exposes the gate
5321
+ state for tests. */
5322
+ data-topo-hub-spoke-glow={!reducedMotion && hoveredHub ? 'true' : 'false'}
5323
+ /* Round 580 (65-round milestone) — hub-spokes complete
5324
+ the hub-cluster brightness coverage at 5/5 concentric
5325
+ elements. Stacks brightness(1.15) onto R533's drop-
5326
+ shadow — same R564/R570/R571/R572/R573/R574/R575/
5327
+ R577/R578/R579 stacked-filter pattern.
5328
+ Hub-cluster brightness now FULLY CLOSED:
5329
+ hub digit (R575) innermost typo
5330
+ hub-highlight (R574) middle disc
5331
+ hub-hover-ring (R579) outer ring boundary
5332
+ hub-halo (R577) outermost atmosphere
5333
+ hub-spokes (R580) mesh radial lines ← this round
5334
+ 5 concentric elements + N mesh radial lines all lift
5335
+ uniformly through stacked drop-shadow + brightness on
5336
+ hub-hover. The hub focal cluster now responds as ONE
5337
+ unified motion-coherent paint pulse from center
5338
+ outward through every layer. */
5339
+ data-topo-hub-spoke-brightness={!reducedMotion && hoveredHub ? '1.15' : '1'}
4417
5340
  style={{
4418
- transition: 'stroke 250ms ease-out, stroke-width 250ms ease-out, opacity 250ms ease-out',
5341
+ transition: 'stroke 250ms ease-out, stroke-width 250ms ease-out, opacity 250ms ease-out, filter 250ms ease-out',
5342
+ filter: !reducedMotion && hoveredHub
5343
+ ? (isLight
5344
+ ? 'drop-shadow(0 0 1.5px rgba(13, 148, 136, 0.4)) brightness(1.15)'
5345
+ : 'drop-shadow(0 0 1.5px rgba(34, 211, 238, 0.4)) brightness(1.15)')
5346
+ : undefined,
4419
5347
  ...(isActiveSpoke ? {} : {
4420
5348
  animationDelay: `${-(idx * 0.25)}s`,
4421
5349
  // CSS var consumed by `.anet-topo-spoke-flow`
@@ -4436,6 +5364,30 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4436
5364
  a node click. Restrained dashed container + group-name label. */}
4437
5365
  {groupBoxes.map((box, boxIdx) => {
4438
5366
  const isHovered = activeGroup === box.key;
5367
+ /* Round 561 / Loop — inspection-overrides-encoding family
5368
+ 4th anchor. When operator hovers a NODE ALIAS on the
5369
+ canvas, the group-label that CONTAINS that node lifts
5370
+ to full opacity, signalling "this is the cluster
5371
+ whose member you're inspecting".
5372
+ Family progression (4 anchors now):
5373
+ R484 recent-row timestamp brightens on alias hover
5374
+ R485 edge particle opacity lifts on alias hover
5375
+ R486 minimap dot opacity lifts on alias hover
5376
+ R561 group-label opacity lifts on member-alias hover ← this round
5377
+ Pure paint axis (opacity only) — same restraint as
5378
+ R486. NOT bundled into the existing `isHovered` flag
5379
+ so the marching-ants live animation (gated on
5380
+ `!isPinned && !isHovered`) keeps running; the box
5381
+ rect stroke widen / fill brighten / letter-spacing
5382
+ / filter glow gates stay tied to direct label
5383
+ hover/pin semantics.
5384
+ Mirror of R486's pattern: `hoveredAlias === s.alias`
5385
+ extends a focused opacity branch independent from
5386
+ the rest-state encoding (online/offline). R561 here:
5387
+ `groupKeys[hoveredAlias] === box.key` extends a
5388
+ cluster-awareness opacity branch independent from
5389
+ the existing isHovered / isPinned semantics. */
5390
+ const isMemberAliasHovered = !!hoveredAlias && groupKeys[hoveredAlias] === box.key;
4439
5391
  // R68: distinguish "locked by click" from "currently hovered".
4440
5392
  // R63 made pinned and hovered identical (both hit isHovered
4441
5393
  // via activeGroup). A user with one team pinned should see at
@@ -4563,12 +5515,75 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4563
5515
  // pinned → fill 0.08 / 0.13, stroke 3 px (locked)
4564
5516
  // hovered → fill 0.05 / 0.09, stroke 2 px (inspecting)
4565
5517
  // idle → fill 0.025 / 0.045, stroke 1.5 px dashed
4566
- fillOpacity={isPinned ? (isLight ? 0.08 : 0.13)
4567
- : isHovered ? (isLight ? 0.05 : 0.09)
4568
- : (isLight ? 0.025 : 0.045)}
5518
+ /* Round 506 / Loop category-differentiation family
5519
+ 3rd anchor. Orphan band rest-state fillOpacity drops
5520
+ slightly below prefix-group rest (0.025/0.045
5521
+ 0.015/0.028). Adds a 3rd independent paint
5522
+ differentiator to the orphan visual signature:
5523
+ R499 fontStyle: italic (label text)
5524
+ R503 '3 6' dash pattern (rect stroke)
5525
+ R506 lower fillOpacity (rect fill) ← this round
5526
+ Three independent channels (typography + stroke
5527
+ pattern + fill density) collectively encode the
5528
+ catchall semantic at rest. Pin and hover branches
5529
+ UNCHANGED (still 0.05/0.09 hover, 0.08/0.13 pin) —
5530
+ orphan box gets full visual emphasis on inspection
5531
+ identical to prefix groups; the differentiation
5532
+ lives ONLY in the unsolicited rest state. The
5533
+ ~40% drop (0.045 → 0.028 cyber, 0.025 → 0.015
5534
+ light) is subtle enough that the orphan box stays
5535
+ visible at rest, just quieter — matches the
5536
+ "misc bucket, less attention-deserving" semantic
5537
+ without losing the visual anchor.
5538
+ Pure paint axis; bbox unchanged; R51 SVG sentinel
5539
+ safety untouched (overlap-test gates to g[data-
5540
+ node], cluster rect invisible to it).
5541
+ data-group-box-fill-opacity attr surfaces the
5542
+ resolved value for tests. */
5543
+ fillOpacity={
5544
+ isPinned ? (isLight ? 0.08 : 0.13)
5545
+ : isHovered ? (isLight ? 0.05 : 0.09)
5546
+ : box.isOrphan ? (isLight ? 0.015 : 0.028)
5547
+ : (isLight ? 0.025 : 0.045)
5548
+ }
5549
+ data-group-box-fill-opacity={
5550
+ isPinned ? (isLight ? 0.08 : 0.13)
5551
+ : isHovered ? (isLight ? 0.05 : 0.09)
5552
+ : box.isOrphan ? (isLight ? 0.015 : 0.028)
5553
+ : (isLight ? 0.025 : 0.045)
5554
+ }
4569
5555
  stroke={(isPinned || isHovered) ? pal.legendAccent : pal.ringStroke}
4570
5556
  strokeWidth={isPinned ? 3 : isHovered ? 2 : 1.5}
4571
- strokeDasharray={(isPinned || isHovered) ? 'none' : '6 6'}
5557
+ /* Round 503 / Loop category-differentiation family
5558
+ 2nd anchor (R499 = orphan label italic, 1st anchor).
5559
+ Orphan band rest-state strokeDasharray switches from
5560
+ '6 6' (prefix-group default) to '3 6' (tighter
5561
+ dashes). Pre-R503 the rect dash pattern was uniform
5562
+ across all bands; combined with R499's italic label,
5563
+ the orphan box now has TWO independent paint/
5564
+ typography differentiators at rest:
5565
+ R499 fontStyle: italic (label text)
5566
+ R503 '3 6' dash pattern (rect stroke) ← this round
5567
+ The R85 marching-ants animation continues to work
5568
+ with the new dash size (uses --march-dur custom
5569
+ property, dash-length-agnostic) — orphan's ants
5570
+ just have a different visual rhythm than prefix-
5571
+ group ants, reinforcing the catchall semantic.
5572
+ Pinned/hovered orphan still gets 'none' (solid
5573
+ stroke) so the hover/pin affordance is preserved
5574
+ — the differentiation lives ONLY in the rest
5575
+ state, never blocking inspection.
5576
+ Pure paint axis; no geometry change; bbox unchanged
5577
+ (strokeDasharray is paint-only). R51 SVG sentinel
5578
+ safety untouched (overlap-test gates to g[data-
5579
+ node], this cluster rect is invisible to it).
5580
+ data-group-box-orphan attr surfaces the gate for
5581
+ tests + future polish references. */
5582
+ strokeDasharray={
5583
+ (isPinned || isHovered) ? 'none' :
5584
+ box.isOrphan ? '3 6' : '6 6'
5585
+ }
5586
+ data-group-box-orphan={box.isOrphan ? 'true' : 'false'}
4572
5587
  /* Round 380 / Loop: cluster box stroke gets round
4573
5588
  linecap + round linejoin. Sibling SVG stroke-
4574
5589
  softening polish to R378 flow-rail linecap + R379
@@ -4598,6 +5613,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4598
5613
  data-group-box-linecap="round"
4599
5614
  data-group-box-linejoin="round"
4600
5615
  data-group-box-geom-transition="x,y,width,height"
5616
+ data-group-box-brightness={(isPinned || isHovered) ? '1.15' : '1'}
4601
5617
  // R85: ambient "marching ants" drift on the perimeter
4602
5618
  // when this group has at least one working member, and
4603
5619
  // neither pin nor hover is active (those treatments
@@ -4614,10 +5630,29 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4614
5630
  // motion layers (hub / ring / group) keep a coherent
4615
5631
  // tempo grammar. Default 12s when working=0 doesn't
4616
5632
  // matter — the className is only applied when working>0.
4617
- data-group-box-live={!isPinned && !isHovered && box.statuses.working > 0 ? 'true' : 'false'}
5633
+ /* Round 561 marching-ants gate refined to halt only
5634
+ on direct-label-hover (or pin), NOT on member-alias-
5635
+ hover. Pre-R561 `isHovered` covered BOTH cases via
5636
+ the line 1044 fallback `hoveredGroup ?? (hoveredAlias
5637
+ → groupKeys[hoveredAlias])` — so hovering ANY member
5638
+ node halted the ants, treating indirect inspection
5639
+ the same as direct attention.
5640
+ R561 differentiates: ants now keep running during
5641
+ member-alias hover (indirect / cluster-awareness
5642
+ inspection), halting ONLY on direct label hover or
5643
+ pin. The cluster's live signal stays alive while
5644
+ operator inspects member nodes — distinct visual
5645
+ telegraph for "directly attending this cluster"
5646
+ vs "inspecting one of its members".
5647
+ Gate uses `hoveredGroupLabel === box.key` directly
5648
+ (the LABEL hover state) instead of `isHovered`
5649
+ (which combines label-hover + member-alias-hover
5650
+ via activeGroup). data-group-box-live + the live
5651
+ className both flip on the same refined gate. */
5652
+ data-group-box-live={!isPinned && hoveredGroupLabel !== box.key && box.statuses.working > 0 ? 'true' : 'false'}
4618
5653
  data-group-box-march-dur={marchDur}
4619
5654
  data-group-box-lifted={(isPinned || isHovered) ? 'true' : 'false'}
4620
- className={!isPinned && !isHovered && box.statuses.working > 0 ? 'anet-topo-groupbox-live' : undefined}
5655
+ className={!isPinned && hoveredGroupLabel !== box.key && box.statuses.working > 0 ? 'anet-topo-groupbox-live' : undefined}
4621
5656
  // R142: drop-shadow filter when pinned or hovered. Box
4622
5657
  // visually "rises off the canvas" — same vocabulary
4623
5658
  // R18 KPI cards + R135 overlay panels use. Idle group
@@ -4628,6 +5663,34 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4628
5663
  // is preserved.
4629
5664
  filter={(isPinned || isHovered) ? 'url(#topo-groupbox-lift)' : undefined}
4630
5665
  style={{
5666
+ /* R587 — group box gains stacked brightness(1.15)
5667
+ on hover/pin. 26th anchor in per-element
5668
+ brightness family, 19th in stacked-filter
5669
+ sub-pattern. Inline style.filter overrides the
5670
+ attribute filter (kept intact for the R142
5671
+ documentation trail); stacked syntax preserves
5672
+ the R142 url(#topo-groupbox-lift) SVG drop-shadow
5673
+ lift on hover/pin, with brightness layered on
5674
+ top.
5675
+
5676
+ Same R582/R583 stacked-filter pattern at the
5677
+ group-cluster scope.
5678
+
5679
+ Group box inspection signature now 6 layers
5680
+ (spans paint + stroke + geometry + filter):
5681
+ R68 fillOpacity 0.045 → 0.13 (cyber pin)
5682
+ R68 strokeWidth 1.5 → 2 → 3
5683
+ R68 stroke tint → legendAccent
5684
+ R503 strokeDasharray → 'none' on activation
5685
+ R142 filter → url(#topo-groupbox-lift)
5686
+ R587 filter stack → brightness(1.15) ← this round
5687
+
5688
+ Same existing transition list already includes
5689
+ 'filter 200ms ease-out' (R142 cadence). No
5690
+ transition change needed. */
5691
+ filter: (isPinned || isHovered)
5692
+ ? 'url(#topo-groupbox-lift) brightness(1.15)'
5693
+ : undefined,
4631
5694
  /* Round 248 / Loop: append fill 200ms ease-out to
4632
5695
  the existing R66 transition list. Pre-R248 the
4633
5696
  rect's fill (isLight ? '#0f172a' (slate-900) :
@@ -4806,7 +5869,32 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4806
5869
  exposes the geometry-axis presence for tests. */
4807
5870
  data-group-label-tint-transition="200ms"
4808
5871
  data-group-label-tint-geom-transition="x,width,rx"
4809
- style={{ transition: 'fill 200ms ease-out, opacity 200ms ease-out, x 200ms ease-out, width 200ms ease-out, rx 200ms ease-out' }}
5872
+ data-group-label-tint-brightness={(pinnedGroup === box.key || hoveredGroupLabel === box.key) ? '1.15' : '1'}
5873
+ style={{
5874
+ /* R610 — group label tint rect gains filter
5875
+ brightness(1.15) on the same pin OR hover-label
5876
+ gate that drives the fill swap. The pal.legend-
5877
+ Accent cyan/teal fill (0.20 pin / 0.13 hover
5878
+ alpha) lifts +15% — small but perceivable lift
5879
+ that ties the tint band's paint axis to its
5880
+ already-strong fill/opacity/geometry response
5881
+ on inspection.
5882
+
5883
+ Pin/hover-gated brightness family extension:
5884
+ tint-band joins the legend pin-ring (R607) +
5885
+ group box (R587) + group label text (R571) +
5886
+ flow-arrow on legend hover (R609) at the
5887
+ panel-tier brightness coverage.
5888
+
5889
+ Pure paint axis; no geometry shift. transition
5890
+ list extends with 'filter 200ms ease-out'
5891
+ alongside the existing 200ms fill/opacity/geom
5892
+ cadence. */
5893
+ filter: (pinnedGroup === box.key || hoveredGroupLabel === box.key)
5894
+ ? 'brightness(1.15)'
5895
+ : undefined,
5896
+ transition: 'fill 200ms ease-out, opacity 200ms ease-out, x 200ms ease-out, width 200ms ease-out, rx 200ms ease-out, filter 200ms ease-out',
5897
+ }}
4810
5898
  />
4811
5899
  {/* Round 218 / Loop: group label gains a letter-spacing
4812
5900
  transition on pin — the text subtly spaces out
@@ -4888,8 +5976,54 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4888
5976
  fontSize="9"
4889
5977
  fontFamily="monospace"
4890
5978
  fontWeight={isPinned ? '800' : '700'}
4891
- opacity={isPinned || isHovered ? 1 : 0.55}
5979
+ /* Round 551 / Loop category-differentiation family
5980
+ 4th anchor. Orphan band ("其他" catchall) rest-state
5981
+ LABEL opacity drops 0.55 → 0.4 (~27% relative dim),
5982
+ mirroring R506's rect fillOpacity drop pattern at
5983
+ the label-paint tier. Adds a 4th independent
5984
+ channel to the orphan visual signature at rest:
5985
+ R499 fontStyle italic (typography style)
5986
+ R503 '3 6' dash pattern (rect stroke)
5987
+ R506 lower rect fill-opacity (rect fill)
5988
+ R551 lower label opacity (label paint) ← this round
5989
+ Four independent channels (typography style +
5990
+ stroke pattern + rect fill density + label paint
5991
+ density) collectively encode the catchall semantic
5992
+ at rest — orphan band reads as "misc bucket, less
5993
+ attention-deserving" through every available paint
5994
+ channel, no chrome / color / geometry change.
5995
+ Pin and hover branches UNCHANGED — orphan label
5996
+ restores to full opacity 1 on inspection, same as
5997
+ prefix groups. The differentiation lives ONLY in
5998
+ the unsolicited rest state. The ~27% drop (0.55 →
5999
+ 0.4) is dimmer than R506's ~40% (rect could
6000
+ tolerate it; small 9px text needs more residual
6001
+ paint to stay legible) — orphan label stays
6002
+ readable when scanning, just clearly quieter.
6003
+ Pure paint axis; bbox unchanged; R51 SVG sentinel
6004
+ safety untouched (overlap-test gates to g[data-
6005
+ node], this group-label is invisible to it).
6006
+ transition list (R55/R432/R457/R479: fill, ls,
6007
+ fw, filter all 200ms) already eases opacity since
6008
+ `opacity 300ms ease-out` lives in the parent <text>
6009
+ CSS — wait, only filter/ls/fw/fill 200ms are
6010
+ listed. Need to add 'opacity 200ms ease-out' for
6011
+ smooth orphan opacity flip on pin/hover transitions
6012
+ (currently opacity 0.55 → 1 was snapping).
6013
+ data-group-label-opacity attr exposes the resolved
6014
+ value for tests. */
6015
+ /* Round 561 — opacity ladder gains inspection-
6016
+ overrides-encoding branch via isMemberAliasHovered.
6017
+ Resolution order (most-emphatic first):
6018
+ isPinned || isHovered → 1 (direct attention)
6019
+ isMemberAliasHovered → 1 (R561 inspection)
6020
+ box.isOrphan → 0.4 (R551 orphan rest)
6021
+ (default) → 0.55
6022
+ R484/R485/R486-family mirror. */
6023
+ opacity={isPinned || isHovered || isMemberAliasHovered ? 1 : box.isOrphan ? 0.4 : 0.55}
6024
+ data-group-label-opacity={isPinned || isHovered || isMemberAliasHovered ? 1 : box.isOrphan ? 0.4 : 0.55}
4892
6025
  data-group-label-hovered={isHovered && !isPinned ? 'true' : 'false'}
6026
+ data-group-label-member-alias-hovered={isMemberAliasHovered ? 'true' : 'false'}
4893
6027
  data-group-label-font-weight={isPinned ? '800' : '700'}
4894
6028
  /* Round 479 / Loop — extend drop-shadow visual-polish
4895
6029
  family to a 4th anchor: group-label parent text
@@ -4912,7 +6046,28 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4912
6046
  transition list extends to include 'filter 200ms
4913
6047
  ease-out' alongside the existing fill/ls/fw/opacity
4914
6048
  200ms tweens. */
4915
- data-group-label-glow={isPinned ? 'true' : 'false'}
6049
+ /* Round 538 / Loop — extends R479 group-label drop-
6050
+ shadow from pin-only to ALSO fire on hover, with
6051
+ a 2-tier alpha ladder matching the R432 letter-
6052
+ spacing 3-tier (hover at 0.25px / pin at 0.5px)
6053
+ pattern. Pre-R538 the paint axis was binary (lit
6054
+ on pin, dark on hover); R538 adds a softer hover
6055
+ glow that distinguishes from the stronger pin
6056
+ glow without losing the "active state lights up"
6057
+ gesture.
6058
+ 2-tier alpha ladder:
6059
+ pin (committed) cyan 80 hex (~50% alpha)
6060
+ hover (preview) cyan 4d hex (~30% alpha)
6061
+ rest none
6062
+ Pin signature stays distinctively brighter, but
6063
+ hover now telegraphs paint-axis attention too.
6064
+ Sibling to R534 edge-badge hover-precedence
6065
+ extension at the drop-shadow family. R479 hue
6066
+ (pal.legendAccent) preserved across both tiers.
6067
+ data-group-label-glow attr upgraded from binary
6068
+ ('true'/'false') to 3-value ('pin' | 'hover' |
6069
+ 'false') so tests can distinguish gate cause. */
6070
+ data-group-label-glow={isPinned ? 'pin' : isHovered ? 'hover' : 'false'}
4916
6071
  /* Round 499 / Loop — orphan band "其他" label gets
4917
6072
  fontStyle: italic to visually distinguish the
4918
6073
  catchall from real prefix-group bands. Pre-R499
@@ -4934,18 +6089,51 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4934
6089
  advances the "信息密度" axis by encoding
4935
6090
  category-distinction into a single typography
4936
6091
  channel without adding visual chrome. */
6092
+ /* Round 571 / Loop — group-label parent text joins the
6093
+ per-element brightness consistency family (R501/
6094
+ R558/R564/R567/R570) at uniform +15%. 8th anchor.
6095
+ Stacks brightness(1.15) onto the existing R479/R538
6096
+ drop-shadow filter — same R564/R570 stacked filter
6097
+ pattern (drop-shadow + brightness in one CSS chain).
6098
+ Pre-R571 the group-label parent text lifted in 5
6099
+ axes on hover/pin (fill + ls 3-tier + fw on pin +
6100
+ drop-shadow + opacity) but the glyph chromatically
6101
+ stayed at flat fill brightness. R571 adds the
6102
+ brightness axis to the glyph itself for cross-
6103
+ element consistency with the rest of the per-
6104
+ element brightness family.
6105
+ Filter chain on isPinned: `drop-shadow(0 0 3px
6106
+ ${pal.legendAccent}80) brightness(1.15)`.
6107
+ On isHovered (weaker tier): `drop-
6108
+ shadow(0 0 3px ${pal.legendAccent}4d) brightness(1.15)`.
6109
+ Rest: undefined.
6110
+ Per-element brightness family — 8 anchors at +15%:
6111
+ R501 vendor.logo image
6112
+ R558 vendor monogram
6113
+ R558 prefix-group fallback
6114
+ R564 alias text (stacked w/ DS)
6115
+ R567 node sub-text
6116
+ R570 edge-badge digit
6117
+ R571 group-label parent text ← this round
6118
+ transition list already includes 'filter 200ms ease-
6119
+ out' from R479 — no change needed. R432 ls + R457
6120
+ fw + R479 drop-shadow + R551 orphan opacity all
6121
+ preserved. */
4937
6122
  style={{
4938
6123
  transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out, font-weight 200ms ease-out, opacity 200ms ease-out, filter 200ms ease-out',
4939
6124
  letterSpacing: isPinned ? '0.5px' :
4940
6125
  isHovered ? '0.25px' : '0px',
4941
6126
  fontStyle: box.isOrphan ? 'italic' : undefined,
4942
6127
  filter: isPinned
4943
- ? `drop-shadow(0 0 3px ${pal.legendAccent}80)`
4944
- : undefined,
6128
+ ? `drop-shadow(0 0 3px ${pal.legendAccent}80) brightness(1.15)`
6129
+ : isHovered
6130
+ ? `drop-shadow(0 0 3px ${pal.legendAccent}4d) brightness(1.15)`
6131
+ : undefined,
4945
6132
  }}
4946
6133
  data-group-label={box.key}
4947
6134
  data-group-label-pinned={isPinned ? 'true' : 'false'}
4948
6135
  data-group-label-orphan={box.isOrphan ? 'true' : 'false'}
6136
+ data-group-label-brightness={(isPinned || isHovered) ? '1.15' : '1'}
4949
6137
  >
4950
6138
  {box.key}
4951
6139
  {/* Round 19 / Loop: member-count chip. Inline tspan stays
@@ -5407,6 +6595,19 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
5407
6595
  unchanged at the join with the arrow marker).
5408
6596
  data-edge-visible-linecap attr exposes the value
5409
6597
  for tests. */}
6598
+ {/* Round 582 — edge visible flow path joins per-element
6599
+ brightness family at 21st anchor. Stacks
6600
+ brightness(1.15) with the existing url(#topo-glow)
6601
+ SVG filter (cyber) or applies plain brightness
6602
+ (light). CSS filter accepts mixed url() + function
6603
+ values; inline style.filter overrides any
6604
+ attribute-level filter. Closes edge-tier brightness
6605
+ sub-family at 2 surfaces:
6606
+ R581 flow-rail (dashed underline) brightness
6607
+ R582 visible path (primary curve) brightness ← this round
6608
+ transition list extends to include 'filter 300ms
6609
+ ease-out' matching the existing opacity/sw/stroke
6610
+ cadence. */}
5410
6611
  <path
5411
6612
  d={path}
5412
6613
  fill="none"
@@ -5414,15 +6615,18 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
5414
6615
  strokeWidth={renderWidth}
5415
6616
  strokeLinecap="round"
5416
6617
  opacity={Math.min(1, (isLight ? 0.22 : 0.28) * fresh * edgeOpacityMul)}
5417
- filter={isLight ? undefined : 'url(#topo-glow)'}
5418
6618
  markerEnd={`url(#${arrowId})`}
5419
6619
  data-edge-visible={link.key}
5420
6620
  data-edge-visible-linecap="round"
5421
6621
  data-edge-visible-endpoint-hovered={isEndpointHoveredEdge ? 'true' : 'false'}
5422
6622
  data-edge-visible-stroke-width={renderWidth}
6623
+ data-edge-visible-brightness={(isHoveredEdge || isEndpointHoveredEdge) ? '1.15' : '1'}
5423
6624
  style={{
5424
6625
  pointerEvents: 'none',
5425
- transition: 'opacity 300ms ease-out, stroke-width 300ms ease-out, stroke 300ms ease-out',
6626
+ transition: 'opacity 300ms ease-out, stroke-width 300ms ease-out, stroke 300ms ease-out, filter 300ms ease-out',
6627
+ filter: (isHoveredEdge || isEndpointHoveredEdge)
6628
+ ? (isLight ? 'brightness(1.15)' : 'url(#topo-glow) brightness(1.15)')
6629
+ : (isLight ? undefined : 'url(#topo-glow)'),
5426
6630
  }}
5427
6631
  />
5428
6632
  {/* Round 378 / Loop: edge flow-path dashed-rail picks
@@ -5472,7 +6676,23 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
5472
6676
  data-edge-flow-rail-linecap="round"
5473
6677
  data-edge-flow-rail-stroke-width={(isHoveredEdge || isEndpointHoveredEdge) ? 1.5 : 1}
5474
6678
  data-edge-flow-rail-lifted={(isHoveredEdge || isEndpointHoveredEdge) ? 'true' : 'false'}
5475
- style={{ transition: 'opacity 300ms ease-out, stroke 300ms ease-out, stroke-width 300ms ease-out' }}
6679
+ /* Round 581 flow-rail joins per-element brightness
6680
+ family at 20th anchor. Adds brightness(1.15) on
6681
+ edge hover or endpoint hover. Joins R437 sw-lift
6682
+ paint pattern at the dashed-underline tier — when
6683
+ an edge is in focus, the rail's stroke widens
6684
+ (R437) AND brightens (R581) together, reading
6685
+ as a coherent rail-lift gesture under the flow.
6686
+ transition list extends to include 'filter 300ms
6687
+ ease-out' matching the R245/R437 cadence on this
6688
+ surface. */
6689
+ data-edge-flow-rail-brightness={(isHoveredEdge || isEndpointHoveredEdge) ? '1.15' : '1'}
6690
+ style={{
6691
+ transition: 'opacity 300ms ease-out, stroke 300ms ease-out, stroke-width 300ms ease-out, filter 300ms ease-out',
6692
+ filter: (isHoveredEdge || isEndpointHoveredEdge)
6693
+ ? 'brightness(1.15)'
6694
+ : undefined,
6695
+ }}
5476
6696
  />
5477
6697
  {!reducedMotion && (
5478
6698
  /* Round 103 / Loop: phase-stagger the particles so
@@ -5542,7 +6762,31 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
5542
6762
  data-edge-particle-lifted={(isHoveredEdge || isEndpointHoveredEdge) ? 'true' : 'false'}
5543
6763
  data-edge-particle-opacity-rest={Math.min(1, fresh * edgeOpacityMul).toFixed(2)}
5544
6764
  data-edge-particle-opacity-lifted={(isHoveredEdge || isEndpointHoveredEdge) ? 'true' : 'false'}
5545
- style={{ transition: 'fill 200ms ease-out, opacity 200ms ease-out, r 200ms ease-out' }}
6765
+ /* Round 583 flow particle joins per-element
6766
+ brightness family at 22nd anchor. Adds
6767
+ brightness(1.15) on edge hover or endpoint
6768
+ hover, joining R485 opacity inspection-override
6769
+ + R422 r-lift (4 → 4.5) + R164 hover-r-lift
6770
+ (4.5 → 5.5). Particle now has 4-axis active
6771
+ signature on edge inspection:
6772
+ R485 opacity (freshness → 1.0)
6773
+ R164 r 4.5 → 5.5
6774
+ R422 r-base 4 → 4.5 (visual-weight bump)
6775
+ R583 brightness(1.15) ← this round
6776
+ Particle becomes the brightest paint element
6777
+ along the edge during inspection. */
6778
+ data-edge-particle-brightness={(isHoveredEdge || isEndpointHoveredEdge) ? '1.15' : '1'}
6779
+ style={{
6780
+ transition: 'fill 200ms ease-out, opacity 200ms ease-out, r 200ms ease-out, filter 200ms ease-out',
6781
+ /* R583 — stack brightness(1.15) onto the existing
6782
+ url(#topo-glow) (cyber) or apply plain brightness
6783
+ (light). Inline style.filter overrides attribute
6784
+ filter; stacked syntax preserves the cyber glow
6785
+ on hover. Same R582 visible-path stack pattern. */
6786
+ filter: (isHoveredEdge || isEndpointHoveredEdge)
6787
+ ? (isLight ? 'brightness(1.15)' : 'url(#topo-glow) brightness(1.15)')
6788
+ : undefined,
6789
+ }}
5546
6790
  >
5547
6791
  <animateMotion
5548
6792
  dur={`${duration}s`}
@@ -6032,11 +7276,77 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6032
7276
  data-edge-badge-opacity-rest={isLight ? 0.95 : 0.85}
6033
7277
  data-edge-badge-opacity-hover="1"
6034
7278
  data-edge-badge-opacity-active="1"
6035
- data-edge-badge-glow={isHot ? 'true' : 'false'}
7279
+ data-edge-badge-glow={(isHoveredEdge || isPinned) ? 'hover' : isHot ? 'hot' : 'false'}
7280
+ data-edge-badge-brightness={(isHoveredEdge || isPinned || isHot) ? '1.15' : '1'}
7281
+ /* Round 534 / Loop — extends edge-badge drop-shadow
7282
+ coverage from hot-only (R480 amber) to also fire
7283
+ on hover/pin with a cyan accent glow. Pre-R534
7284
+ the badge's hover/pin lifted r (R164 9 → 10.5)
7285
+ + sw (R394 1.25 → 1.5) + opacity (R395/R396 →
7286
+ 1.0), but the paint axis stayed at the badge's
7287
+ rest fill — no glow to telegraph "in focus" at
7288
+ the paint layer. R534 closes that 4-axis hover-
7289
+ lift parity by adding drop-shadow glow on
7290
+ (hovered || pinned).
7291
+ Precedence: (hover || pin) wins over isHot when
7292
+ BOTH true — interactive signal (user is
7293
+ inspecting) overrides informational signal
7294
+ (hot lane). When only isHot fires (no hover/
7295
+ pin) the amber R480 glow remains; the hover/
7296
+ pin case paints cyan/teal `pal.legendAccent`
7297
+ at 0x99 alpha (~60%) — bright enough to read
7298
+ as "lit" but won't overwhelm at small badge
7299
+ size (r=10.5).
7300
+ Edge-badge 4-axis hover-lift parity now:
7301
+ R164 r 9 → 10.5
7302
+ R394 stroke-wd 1.25 → 1.5
7303
+ R395 opacity rest → 1.0
7304
+ R534 filter none → drop-shadow glow ← this round
7305
+ Drop-shadow visual-polish family extension —
7306
+ edge-badge surface upgraded from single-gate
7307
+ (R480 isHot) to two-gate (isHot OR hover-pin).
7308
+ transition list already includes filter 200ms
7309
+ ease-out (R480). data-edge-badge-glow attr
7310
+ upgraded from `isHot ? true : false` to a
7311
+ 3-value string: 'hot' | 'hover' | 'false' so
7312
+ tests can distinguish gate cause.
7313
+ R51 sentinel safety: badge is edge-internal
7314
+ (not g[data-node] ancestor); filter is paint-
7315
+ only; bbox unchanged. */
6036
7316
  style={{
6037
- filter: isHot
6038
- ? `drop-shadow(0 0 3px ${hotStroke}80)`
6039
- : undefined,
7317
+ /* R603 — edge-badge CIRCLE stacks brightness(1.15)
7318
+ onto the R534/R480 drop-shadow filter on hover/
7319
+ pin/hot. 5th hover axis on the badge ring
7320
+ (after R164 r, R394 sw, R395 opacity, R534
7321
+ filter drop-shadow → R603 filter brightness).
7322
+
7323
+ Brings the CIRCLE's brightness coverage to
7324
+ parity with the DIGIT's R570 brightness:
7325
+ both elements (ring + text) now brighten
7326
+ together on the same 3-state gate
7327
+ (hover/pin/hot). The whole edge-badge reads
7328
+ as one coherent "lit up under attention"
7329
+ unit at the brightness axis.
7330
+
7331
+ Banked R564/R570 stacked-filter pattern:
7332
+ `drop-shadow(...) brightness(1.15)` chains
7333
+ the SVG drop-shadow halo + brightness lift
7334
+ in one CSS filter chain on the same element.
7335
+
7336
+ Per-element brightness family extension —
7337
+ edge-badge circle joins as a stacked-filter
7338
+ anchor. Hot-only state gets amber drop-shadow
7339
+ + brightness; hover/pin gets cyan/teal drop-
7340
+ shadow + brightness; rest gets neither.
7341
+
7342
+ transition list already includes 'filter
7343
+ 200ms ease-out' (R534 cadence). No
7344
+ transition change needed. */
7345
+ filter: (isHoveredEdge || isPinned)
7346
+ ? `drop-shadow(0 0 3px ${pal.legendAccent}99) brightness(1.15)`
7347
+ : isHot
7348
+ ? `drop-shadow(0 0 3px ${hotStroke}80) brightness(1.15)`
7349
+ : undefined,
6040
7350
  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',
6041
7351
  }}
6042
7352
  />
@@ -6126,6 +7436,31 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6126
7436
  data-edge-badge-text={link.key}
6127
7437
  data-edge-badge-text-pin={(isPinned || isHot) ? 'true' : 'false'}
6128
7438
  data-edge-badge-text-font-size="11"
7439
+ /* Round 570 / Loop — edge-badge digit joins the per-
7440
+ element brightness consistency family (R501/R558/
7441
+ R564/R567) at uniform +15%. 7th anchor.
7442
+ Gate: (isHoveredEdge || isPinned || isHot) — same
7443
+ 3-tier set as R431 ls (with hover the mid step
7444
+ and pin/hot the strong step). Brightness lifts
7445
+ uniformly across all 3 active sub-states; the
7446
+ ls/fw axes still distinguish hover from pin/hot.
7447
+ Pure paint axis on the digit glyph; bbox
7448
+ unchanged. The R540 badge-circle drop-shadow
7449
+ sits on the parent CIRCLE element (separate
7450
+ filter); the digit's filter is independent and
7451
+ doesn't compound with circle filter (different
7452
+ SVG element).
7453
+ Per-element brightness family — 7 anchors at +15%:
7454
+ R501 vendor.logo image
7455
+ R558 vendor monogram
7456
+ R558 prefix-group fallback
7457
+ R564 alias text (stacked w/ DS)
7458
+ R567 node sub-text
7459
+ R570 edge-badge digit ← this round
7460
+ Plus runtime badge drop-shadow (R559) on same
7461
+ isNodeActive gate. data-edge-badge-text-brightness
7462
+ attr surfaces the lift for tests. */
7463
+ data-edge-badge-text-brightness={(isHoveredEdge || isPinned || isHot) ? '1.15' : '1'}
6129
7464
  style={{
6130
7465
  pointerEvents: 'none',
6131
7466
  fontVariantNumeric: 'tabular-nums',
@@ -6144,7 +7479,10 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6144
7479
  now): R344/R345/R347/R351/R420/R427/R431. */
6145
7480
  letterSpacing: (isPinned || isHot) ? '0.4px' :
6146
7481
  isHoveredEdge ? '0.2px' : '0px',
6147
- transition: 'letter-spacing 300ms ease-out, font-weight 300ms ease-out',
7482
+ filter: (isHoveredEdge || isPinned || isHot)
7483
+ ? 'brightness(1.15)'
7484
+ : undefined,
7485
+ transition: 'letter-spacing 300ms ease-out, font-weight 300ms ease-out, filter 300ms ease-out',
6148
7486
  }}
6149
7487
  >{link.count}</text>
6150
7488
  </g>
@@ -6336,6 +7674,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6336
7674
  data-hub-busyness={busy}
6337
7675
  data-topo-hub-halo-radius={haloR}
6338
7676
  data-topo-hub-halo-hovered={isHaloHovered ? 'true' : 'false'}
7677
+ data-topo-hub-halo-glow={!reducedMotion && hoveredHub ? 'true' : 'false'}
6339
7678
  data-topo-hub-halo-trough={isLight ? troughLight : troughDark}
6340
7679
  data-topo-hub-halo-peak={isLight ? peakLight : peakDark}
6341
7680
  /* Round 253 / Loop: hub grounding halo fill transition
@@ -6347,10 +7686,59 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6347
7686
  conflict.
6348
7687
  R451: r as CSS property (R197/R198 idiom) so the
6349
7688
  hover-radius tween eases smoothly under the same
6350
- 200ms cadence as fill. */
7689
+ 200ms cadence as fill.
7690
+ R536: extends hub-cluster glow QUARTET (R476 digit
7691
+ + R532 disc + R535 ring + R533 spokes) to a 5th
7692
+ tier — the halo gains drop-shadow at the outermost
7693
+ concentric ring on hub-hover. 2px blur + 0.3 alpha
7694
+ keeps the halo's glow subtle since (a) the halo is
7695
+ the LARGEST hub element (r=22 hover) and a heavier
7696
+ glow would bleed visibly past the ring tier into
7697
+ the spoke origin, and (b) the halo already SMIL-
7698
+ animates opacity (R84/R244 breath), so the visible
7699
+ glow pulses with the breath — an atmospheric
7700
+ "breathing glow" idiom rather than a static rim.
7701
+ Hub-cluster glow QUINTET (R476/R532/R533/R535/R536):
7702
+ digit (typo center) 3px emerald 0.6
7703
+ disc (r=5.5/6) 3px emerald 0.6
7704
+ ring (r=14/17) 3px emerald 0.5
7705
+ halo (r=20/22) 2px emerald 0.3 ← this round
7706
+ spokes (mesh) 1.5px cyan/teal 0.4
7707
+ Emerald palette continues through the focal-disc
7708
+ family (digit/disc/ring/halo); spokes break out
7709
+ into cyan/teal at the mesh tier. The 4-step alpha
7710
+ ladder 0.6→0.6→0.5→0.3 reads as the focal cluster
7711
+ fading outward — the OUTERMOST emerald glow is the
7712
+ softest, the focal digit is the brightest.
7713
+ filter is paint-only; SMIL animate on opacity
7714
+ continues independently (attribute vs CSS-property
7715
+ non-conflicting). transition list extends to
7716
+ 'filter 200ms ease-out' alongside fill + r.
7717
+ Drop-shadow visual-polish family extension (12
7718
+ anchors). preview.50 milestone round. data-topo-
7719
+ hub-halo-glow attr exposes the gate state. */
7720
+ data-topo-hub-halo-brightness={!reducedMotion && hoveredHub ? '1.15' : '1'}
7721
+ /* Round 577 — hub-halo joins per-element brightness
7722
+ family at 15th anchor. Stacks brightness(1.15)
7723
+ onto R536's hub-hover drop-shadow — closes the
7724
+ hub-cluster focal brightness coverage at 3
7725
+ concentric elements:
7726
+ hub digit (R575)
7727
+ hub-highlight (R574)
7728
+ hub-halo (R577) ← this round
7729
+ All 3 hub focal elements now lift through stacked
7730
+ drop-shadow + brightness on hub-hover. Halo is
7731
+ the OUTERMOST so a slight chromatic lift reads as
7732
+ the focal cluster intensifying its ambient glow
7733
+ outward. */
6351
7734
  style={{
6352
7735
  r: `${haloR}px`,
6353
- transition: 'fill 200ms ease-out, r 200ms ease-out',
7736
+ filter: !reducedMotion && hoveredHub
7737
+ ? (isLight
7738
+ ? 'drop-shadow(0 0 2px rgba(16, 185, 129, 0.3)) brightness(1.15)'
7739
+ : 'drop-shadow(0 0 2px rgba(52, 211, 153, 0.3)) brightness(1.15)')
7740
+ : undefined,
7741
+ transition: 'fill 200ms ease-out, r 200ms ease-out, filter 200ms ease-out',
6354
7742
  } as React.CSSProperties}
6355
7743
  >
6356
7744
  {/* Round 244 / Loop: hub grounding halo breath gets
@@ -6431,7 +7819,50 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6431
7819
  data-topo-hub-core
6432
7820
  data-topo-hub-core-hovered={isCoreHovered ? 'true' : 'false'}
6433
7821
  data-topo-hub-core-fill={coreFill}
6434
- style={{ transition: 'fill 200ms ease-out' }}
7822
+ data-topo-hub-core-glow={isCoreHovered ? 'true' : 'false'}
7823
+ data-topo-hub-core-brightness={isCoreHovered ? '1.15' : '1'}
7824
+ /* R614 — hub-core gains stacked drop-shadow + brightness
7825
+ on hub-hover. Extends hub-cluster brightness coverage
7826
+ to a 6th concentric element (innermost emerald r=10
7827
+ disc), beyond R580's 5/5 closure (digit + highlight +
7828
+ hover-ring + halo + spokes).
7829
+
7830
+ Hub-cluster concentric brightness coverage now 6/6:
7831
+ hub digit (R575) innermost typo
7832
+ hub-core (R614) innermost emerald disc ← this round
7833
+ hub-highlight (R574) middle disc
7834
+ hub-hover-ring (R579) outer ring boundary
7835
+ hub-halo (R577) outermost atmosphere
7836
+ hub-spokes (R580) mesh radial lines
7837
+
7838
+ Compounds R441's fill brighten (emerald-600→500 or
7839
+ emerald-500→400) with stacked drop-shadow + brightness.
7840
+ Triple-paint multiplicative interaction at hub-core:
7841
+ R441 fill swap (emerald tier brighter) ×
7842
+ R614 brightness(1.15) ×
7843
+ R614 drop-shadow (emerald halo)
7844
+ The focal anchor reads as fully "lit and pulled
7845
+ forward" on hub-hover, with paint-chroma + glow +
7846
+ brightness all firing in unison.
7847
+
7848
+ Theme-aware drop-shadow palette matches the cluster:
7849
+ light: rgba(16, 185, 129, 0.5) emerald-500
7850
+ cyber: rgba(52, 211, 153, 0.5) emerald-400
7851
+ 2px blur — slightly tighter than the halo's 2px
7852
+ (R536) since core is smallest concentric element.
7853
+ 0.5 alpha matches the hover-ring (R535).
7854
+
7855
+ Same banked R582/R583 stacked-filter pattern.
7856
+ transition list extends with 'filter 200ms ease-out'
7857
+ alongside the existing fill 200ms cadence. */
7858
+ style={{
7859
+ filter: isCoreHovered
7860
+ ? (isLight
7861
+ ? 'drop-shadow(0 0 2px rgba(16, 185, 129, 0.5)) brightness(1.15)'
7862
+ : 'drop-shadow(0 0 2px rgba(52, 211, 153, 0.5)) brightness(1.15)')
7863
+ : undefined,
7864
+ transition: 'fill 200ms ease-out, filter 200ms ease-out',
7865
+ }}
6435
7866
  />
6436
7867
  );
6437
7868
  })()}
@@ -6510,6 +7941,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6510
7941
  data-topo-hub-working-count={workingCount}
6511
7942
  data-topo-hub-working-count-font-size="12"
6512
7943
  data-topo-hub-working-count-hovered={hoveredHub ? 'true' : 'false'}
7944
+ data-topo-hub-working-count-brightness={!reducedMotion && hoveredHub ? '1.15' : '1'}
6513
7945
  data-topo-hub-working-count-visible={workingCount > 0 ? 'true' : 'false'}
6514
7946
  // Round 209 / Loop: hub workingCount digit scales 1.0 →
6515
7947
  // 1.08 on hub-hover, matching R177's r 14→17 ring grow.
@@ -6560,22 +7992,109 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6560
7992
  so the glow eases under the same cadence as the
6561
7993
  scale + fw + fill axes. */
6562
7994
  data-topo-hub-working-count-glow={!reducedMotion && hoveredHub ? 'true' : 'false'}
7995
+ /* Round 507 / Loop — focal recede. When ANY non-hub
7996
+ canvas surface is hovered (a node / an edge / a
7997
+ group label / a legend row / a vendor chip), the
7998
+ hub-center workingCount digit fades to 0.85 opacity,
7999
+ signaling "you're inspecting elsewhere, hub recedes
8000
+ to background." When the user un-hovers (or hovers
8001
+ the hub itself), opacity returns to 1.0. Pure paint
8002
+ polish at the canvas's most prominent focal point.
8003
+ Hits 信息密度 + 动效 themes — the hub digit gives
8004
+ way visually to the surface under inspection,
8005
+ reinforcing the "this is the focal point right now"
8006
+ gesture without requiring users to track which
8007
+ surface holds attention.
8008
+ Gate excludes hoveredHub specifically: hovering the
8009
+ hub itself should LIFT the digit (R425 fw bump +
8010
+ R476 glow + R209 scale 1.08) — the existing hover-
8011
+ on-hub signature is intact; only inspection
8012
+ ELSEWHERE recedes the hub.
8013
+ Composed from existing hoveredAlias / hoveredEdge-
8014
+ Key / hoveredGroupLabel / hoveredStatus / hovered-
8015
+ Vendor — no new state. 300ms ease-out opacity
8016
+ transition already in the style list (existing R213
8017
+ transition spec), so the fade rides on existing
8018
+ infrastructure.
8019
+ data-topo-hub-recede attr surfaces the gate state
8020
+ for tests. */
8021
+ data-topo-hub-recede={
8022
+ (hoveredAlias || hoveredEdgeKey || hoveredGroupLabel ||
8023
+ hoveredStatus || hoveredVendor) && !hoveredHub ? 'true' : 'false'
8024
+ }
8025
+ /* Round 527 / Loop — focal-amplify family extension to a
8026
+ 2nd anchor. R511 introduced focal-amplify at the hub-
8027
+ highlight disc (base opacity 0.95 → 1.0 on hover); R527
8028
+ extends to the hub-center workingCount digit with a
8029
+ letter-spacing tween 0 → 0.3px on hub-hover.
8030
+ Composes with existing 3-axis hub-hover signature on
8031
+ this element:
8032
+ R209 transform scale(1.08) geometry
8033
+ R425 fontWeight 700 → 800 typography weight
8034
+ R476 filter drop-shadow glow paint
8035
+ R527 letter-spacing 0 → 0.3px typography kerning ← this round
8036
+ tabular-nums (R225) preserved — each digit cell keeps
8037
+ fixed width; the inter-digit advance grows by 0.3px
8038
+ per gap. Single-digit counts (1-9) show no kerning
8039
+ effect; multi-digit counts (10+) show the spread as
8040
+ info-density signaling. Sibling to R427/R431/R432/
8041
+ R433/R434 (hover-letter-spacing family at panel-text
8042
+ scope) — R527 brings the same idiom to the canvas's
8043
+ most-read scalar.
8044
+ Reduced-motion gate matches R209 scale, R425 fw, R476
8045
+ filter — !reducedMotion gates the lift; reducedMotion
8046
+ users see static digit baseline regardless of hover.
8047
+ Focal-amplify family extension (2 anchors): R511 hub-
8048
+ highlight opacity / R527 hub-digit letter-spacing.
8049
+ transition list extends to include `letter-spacing
8050
+ 200ms ease-out`, matching the cadence of the other
8051
+ hub-hover axes. data-topo-hub-working-count-letter-
8052
+ spacing attr exposes the resolved value for tests. */
8053
+ data-topo-hub-working-count-letter-spacing={!reducedMotion && hoveredHub ? '0.3px' : '0px'}
6563
8054
  style={{
6564
8055
  pointerEvents: 'none',
6565
8056
  transform: !reducedMotion && hoveredHub ? 'scale(1.08)' : 'scale(1)',
6566
8057
  transformBox: 'fill-box',
6567
8058
  transformOrigin: 'center',
8059
+ opacity: (hoveredAlias || hoveredEdgeKey || hoveredGroupLabel ||
8060
+ hoveredStatus || hoveredVendor) && !hoveredHub
8061
+ ? 0.85
8062
+ : 1,
8063
+ /* Round 575 (60-round milestone) — hub digit joins per-
8064
+ element brightness family at 14th anchor. Stacks
8065
+ brightness(1.15) onto R476's hub-hover drop-shadow
8066
+ — same R564/R570/R571/R572/R573/R574 pattern (drop-
8067
+ shadow + brightness in one filter chain). Closes
8068
+ the hub-cluster focal-element brightness coverage
8069
+ symmetrically: hub digit + hub-highlight disc
8070
+ (R574) now BOTH have stacked filter on hub-hover.
8071
+ Hub digit hub-hover signature post-R575 — 5 active
8072
+ axes:
8073
+ R209 scale 1.08 (geometry)
8074
+ R425 fw 700 → 800 (typography)
8075
+ R527 ls 0 → 0.3px (typography)
8076
+ R476 drop-shadow glow (paint halo)
8077
+ R575 brightness(1.15) (paint glow) ← this round
8078
+ Hub-cluster focal cluster (digit + highlight) now
8079
+ has UNIFIED 5-axis hub-hover signature reading as
8080
+ one tightly-coupled motion-coherent lift. */
6568
8081
  filter: !reducedMotion && hoveredHub
6569
8082
  ? (isLight
6570
- ? 'drop-shadow(0 0 2px rgba(16, 185, 129, 0.6))'
6571
- : 'drop-shadow(0 0 3px rgba(52, 211, 153, 0.6))')
8083
+ ? 'drop-shadow(0 0 2px rgba(16, 185, 129, 0.6)) brightness(1.15)'
8084
+ : 'drop-shadow(0 0 3px rgba(52, 211, 153, 0.6)) brightness(1.15)')
6572
8085
  : undefined,
8086
+ letterSpacing: !reducedMotion && hoveredHub ? '0.3px' : '0px',
6573
8087
  /* R425: font-weight 200ms appended so the hover fw
6574
8088
  bump 700 → 800 eases under the same cadence as
6575
8089
  R209 scale + R253 fill + R213 opacity.
6576
8090
  R476: filter 200ms appended so the new drop-
6577
- shadow glow eases at the same cadence. */
6578
- transition: 'transform 200ms ease-out, opacity 300ms ease-out, fill 200ms ease-out, font-weight 200ms ease-out, filter 200ms ease-out',
8091
+ shadow glow eases at the same cadence.
8092
+ R507: opacity 300ms (existing in list) covers
8093
+ the new focal-recede fade.
8094
+ R527: letter-spacing 200ms appended so the new
8095
+ hover-kerning bump eases at the same cadence
8096
+ as the other axes. */
8097
+ 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',
6579
8098
  fontVariantNumeric: 'tabular-nums',
6580
8099
  }}
6581
8100
  >
@@ -6621,20 +8140,188 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6621
8140
  + R213 always-mount opacity-gate + pointerEvents:none
6622
8141
  + R365 r=5.5 all preserved. data-topo-hub-highlight-
6623
8142
  opacity attr exposes the resolved value for tests. */}
6624
- <circle
6625
- cx={cx} cy={cy} r="5.5"
6626
- fill="#d1fae5"
6627
- opacity={workingCount > 0 ? 0 : 0.95}
6628
- data-topo-hub-highlight
6629
- data-topo-hub-highlight-visible={workingCount > 0 ? 'false' : 'true'}
6630
- data-topo-hub-highlight-radius="5.5"
6631
- data-topo-hub-highlight-opacity={workingCount > 0 ? 0 : 0.95}
6632
- data-topo-hub-highlight-breath={!reducedMotion && workingCount === 0 ? 'true' : 'false'}
6633
- style={{
6634
- pointerEvents: 'none',
6635
- transition: 'opacity 300ms ease-out',
6636
- }}
6637
- >
8143
+ {/* Round 508 / Loop — focal-recede pattern 2nd anchor.
8144
+ Extends R507's hub-digit recede to the hub-highlight
8145
+ circle so the hub focal CLUSTER (digit at z-top + this
8146
+ idle-state highlight beneath) recedes as a unit when
8147
+ canvas attention is elsewhere. Computed once: a single
8148
+ non-hub-hover gate drives BOTH the digit (R507) AND
8149
+ this highlight (R508) so they always co-move.
8150
+ Recede multiplies the visible opacity by 0.85 — when
8151
+ workingCount===0 the rest opacity 0.95 becomes 0.81
8152
+ during external-hover; when workingCount>0 the
8153
+ opacity stays 0 (invisible) regardless of recede.
8154
+ Additionally, when recede is active the SMIL breath
8155
+ animation halts (animate node un-mounts) so the
8156
+ receded state reads as quietly static, not pulsing
8157
+ at 0.85↔1.0 against the recede multiplier (which
8158
+ would visually conflict — competing 15% drops). On
8159
+ un-hover the animate re-mounts and breath resumes.
8160
+ data-topo-hub-recede on both digit AND highlight
8161
+ provides a stable test handle for the unified-recede
8162
+ gate.
8163
+ Composed from existing hover state vars — no new
8164
+ state. Pure paint axis. */}
8165
+ {(() => {
8166
+ const hubRecede = !!((hoveredAlias || hoveredEdgeKey || hoveredGroupLabel ||
8167
+ hoveredStatus || hoveredVendor) && !hoveredHub);
8168
+ /* Round 511 / Loop — hub-highlight gains a 3rd opacity tier
8169
+ (hovered-amplify). Pre-R511 the highlight had 2 visible
8170
+ states: rest (0.95) and recede (0.81 = 0.95 × 0.85).
8171
+ When the hub itself was hovered, the digit got R425 fw
8172
+ lift + R476 drop-shadow + R209 scale-1.08, but the
8173
+ highlight disc sibling stayed at 0.95 — the focal
8174
+ cluster lifted in 3 channels (typography/paint/scale)
8175
+ but the highlight didn't participate.
8176
+ R511 closes that asymmetry: when hoveredHub is true,
8177
+ highlight base opacity lifts to 1.0 (5% boost from
8178
+ rest 0.95). Cluster now lifts as a unit on hub-hover,
8179
+ just like it recedes as a unit on non-hub-hover
8180
+ (R508).
8181
+ 3-state opacity ladder:
8182
+ hub-hovered: baseOpacity = 1.0 (R511 NEW)
8183
+ rest (no hover): baseOpacity = 0.95 (existing)
8184
+ non-hub canvas hov: baseOpacity × 0.85 = 0.81 (R508)
8185
+ Composes cleanly: hubRecede gate requires !hoveredHub,
8186
+ so the hovered-amplify and recede states are mutually
8187
+ exclusive (they can't both fire). breathActive
8188
+ continues to halt on either non-rest state (recede OR
8189
+ hub-hover would visually compete with the 0.85↔1
8190
+ breath — clean for the unit-lift semantic too). */
8191
+ const baseOpacity = workingCount > 0 ? 0
8192
+ : hoveredHub ? 1.0
8193
+ : 0.95;
8194
+ const resolvedOpacity = hubRecede ? baseOpacity * 0.85 : baseOpacity;
8195
+ const breathActive = !reducedMotion && workingCount === 0 && !hubRecede && !hoveredHub;
8196
+ /* Round 529 / Loop — focal-amplify family 3rd anchor.
8197
+ Hub-highlight gains geometric amplify r 5.5 → 6 on
8198
+ hub-hover, mirroring R451's hub-halo r 20 → 22 hover
8199
+ pattern. Pre-R529 the highlight had paint-axis
8200
+ amplify only (R511 opacity 0.95 → 1.0 on hub-hover);
8201
+ R529 adds geometric amplify so the focal disc
8202
+ BREATHES outward on hub attention, like the halo
8203
+ does. Composes with the existing 2-axis hub-hover
8204
+ lift on this element:
8205
+ R511 opacity 0.95 → 1.0 paint (focal-amplify 1st)
8206
+ R529 r 5.5 → 6 geometry (this round)
8207
+ Implementation matches R451: CSS `r` property
8208
+ (R197/R198 idiom) for smooth interpolation. SVG
8209
+ attribute `r="5.5"` provides SSR fallback and serves
8210
+ as default; inline style.r overrides for animated
8211
+ value. transition list extends to include `r 200ms
8212
+ ease-out`, matching the fill cadence (also 200ms);
8213
+ opacity transition stays at 300ms (existing).
8214
+ r 6 sits well inside the existing visual envelope
8215
+ (next-larger sibling r=10 hub core, r=14 hub hover
8216
+ ring). The 0.5px lift is +9% radius / +19% area —
8217
+ enough to read as 'lift' without breaching the core
8218
+ boundary or invalidating overlap-test invariants.
8219
+ SMIL animate on opacity continues independently
8220
+ (animateAttr='opacity' vs CSS-property r — non-
8221
+ conflicting, same pattern R451 noted for halo).
8222
+ Focal-amplify family extension (3 anchors):
8223
+ R511 hub-highlight opacity 0.95 → 1.0
8224
+ R527 hub-digit letter-spacing 0 → 0.3px
8225
+ R529 hub-highlight radius 5.5 → 6 ← this round
8226
+ data-topo-hub-highlight-radius attr now reports the
8227
+ dynamic value (was static '5.5'). */
8228
+ const highlightR = !reducedMotion && hoveredHub ? 6 : 5.5;
8229
+ return (
8230
+ <circle
8231
+ cx={cx} cy={cy} r="5.5"
8232
+ /* Round 509 / Loop — 配色 cross-theme cleanup. Pre-R509
8233
+ the hub-highlight fill was hardcoded `#d1fae5`
8234
+ (emerald-100, a pale tone). On the light theme this
8235
+ near-white green ran against a pale background at
8236
+ 0.95 opacity — the disc was effectively invisible.
8237
+ Matches the existing R253 halo theme-inversion
8238
+ pattern (line ~6481): light theme picks the dark
8239
+ vibrant emerald (#10b981 emerald-600), dark theme
8240
+ keeps the pale emerald (#d1fae5 emerald-100). Both
8241
+ read at the same 0.95 opacity against their
8242
+ respective backdrops — light gets a saturated
8243
+ focal dot; dark keeps the soft glow signature.
8244
+ Pure paint axis (fill change only); bbox unchanged;
8245
+ R51 SVG sentinel safety untouched.
8246
+ transition list already includes `fill 200ms`?
8247
+ Actually the existing transition spec is `opacity
8248
+ 300ms ease-out` — fill change on theme toggle
8249
+ will be instant. That's acceptable: theme toggle
8250
+ is a discrete event, and the halo (line 6500)
8251
+ already snaps fill on theme toggle the same way
8252
+ (`fill 200ms ease-out` was added later to halo
8253
+ via R253). Future round could add `fill 200ms`
8254
+ to highlight too if theme-switch flicker is
8255
+ noticed. */
8256
+ fill={isLight ? '#10b981' : '#d1fae5'}
8257
+ opacity={resolvedOpacity}
8258
+ data-topo-hub-highlight
8259
+ data-topo-hub-highlight-visible={workingCount > 0 ? 'false' : 'true'}
8260
+ data-topo-hub-highlight-radius={highlightR}
8261
+ data-topo-hub-highlight-opacity={resolvedOpacity}
8262
+ data-topo-hub-highlight-breath={breathActive ? 'true' : 'false'}
8263
+ data-topo-hub-highlight-recede={hubRecede ? 'true' : 'false'}
8264
+ data-topo-hub-highlight-hovered={!reducedMotion && hoveredHub ? 'true' : 'false'}
8265
+ data-topo-hub-highlight-glow={!reducedMotion && hoveredHub ? 'true' : 'false'}
8266
+ /* Round 574 — hub-highlight joins per-element brightness
8267
+ family at 13th anchor. Stacks brightness(1.15) onto
8268
+ R532's drop-shadow filter — same R564/R570/R571/R572/
8269
+ R573 pattern (drop-shadow + brightness in one filter
8270
+ chain). Hub idle disc now has 3 active hub-hover
8271
+ axes: R511 opacity 0.95 → 1.0 + R529 r 5.5 → 6 +
8272
+ R574 brightness(1.15). data-topo-hub-highlight-
8273
+ brightness attr surfaces the lift. */
8274
+ data-topo-hub-highlight-brightness={!reducedMotion && hoveredHub ? '1.15' : '1'}
8275
+ /* Round 510 / Loop — R509 follow-on: theme-toggle fill
8276
+ ease. Pre-R510 the hub-highlight transition spec only
8277
+ listed `opacity 300ms ease-out`. When R509 introduced
8278
+ theme-conditional fill (#d1fae5 ↔ #10b981), the fill
8279
+ change SNAPPED on theme toggle because the transition
8280
+ list didn't include `fill`. R510 extends to `fill
8281
+ 200ms ease-out` so theme cycles smoothly through the
8282
+ emerald palette. 200ms timing matches the R253 halo
8283
+ fill transition (line ~6500) — both hub-cluster
8284
+ theme transitions now share a cadence so the focal
8285
+ cluster (digit + highlight + halo) eases as a unit.
8286
+ R508's recede opacity transition unchanged (300ms);
8287
+ fill is independent.
8288
+ R529: r as CSS property (R197/R198 idiom) + `r
8289
+ 200ms ease-out` appended to transition list so
8290
+ the new hub-hover radius lift (5.5 → 6) eases
8291
+ under the same fill cadence. SVG attr r="5.5"
8292
+ above provides SSR fallback; inline style.r
8293
+ wins the cascade for the dynamic value.
8294
+ R532: filter drop-shadow glow on hub-hover —
8295
+ sibling to R476 hub-digit drop-shadow at the
8296
+ same gate (hoveredHub && !reducedMotion). Two
8297
+ adjacent hub focal elements (digit + highlight
8298
+ disc) now BOTH glow on hub-hover, reading as
8299
+ one unified focal cluster. Emerald palette
8300
+ matches R476:
8301
+ light: drop-shadow(0 0 2px rgba(16,185,129,0.6)) emerald-500
8302
+ cyber: drop-shadow(0 0 3px rgba(52,211,153,0.6)) emerald-400
8303
+ filter is paint-only (bbox unchanged); SMIL
8304
+ animate on opacity continues independently
8305
+ (animateAttr='opacity' vs CSS-property filter
8306
+ — non-conflicting). transition list extends to
8307
+ 'filter 200ms ease-out' alongside fill/r.
8308
+ Drop-shadow visual-polish family extension
8309
+ (8 anchors): R476 hub-digit / R477 legend pin-
8310
+ ring / R478 recent freshness / hot edge / group
8311
+ label / zoom-state / node alias + R532 hub-
8312
+ highlight (this round). data-topo-hub-highlight-
8313
+ glow attr exposes the gate state. */
8314
+ style={{
8315
+ pointerEvents: 'none',
8316
+ r: `${highlightR}px`,
8317
+ filter: !reducedMotion && hoveredHub
8318
+ ? (isLight
8319
+ ? 'drop-shadow(0 0 2px rgba(16, 185, 129, 0.6)) brightness(1.15)'
8320
+ : 'drop-shadow(0 0 3px rgba(52, 211, 153, 0.6)) brightness(1.15)')
8321
+ : undefined,
8322
+ transition: 'opacity 300ms ease-out, fill 200ms ease-out, r 200ms ease-out, filter 200ms ease-out',
8323
+ } as React.CSSProperties}
8324
+ >
6638
8325
  {/* Round 497 / Loop — idle-state breath (呼吸感 theme pivot
6639
8326
  from the R492-R496 press-family arc). Pre-R497 the hub
6640
8327
  idle highlight read as a static dim disc — present but
@@ -6655,10 +8342,12 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6655
8342
  rather than a pulse, matching the "quiet" semantic.
6656
8343
  data-topo-hub-highlight-breath attr exposes the
6657
8344
  resolved gate state for tests. */}
6658
- {!reducedMotion && workingCount === 0 && (
8345
+ {breathActive && (
6659
8346
  <animate attributeName="opacity" values="0.85;1;0.85" dur="4s" repeatCount="indefinite" />
6660
8347
  )}
6661
8348
  </circle>
8349
+ );
8350
+ })()}
6662
8351
  {/* R115 / Loop: hover hint ring. Stroke-only circle at r=14
6663
8352
  that fades in when the hub is hovered — the same idea
6664
8353
  R44 used for node avatars (group-hover stroke). r=14
@@ -6724,13 +8413,54 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6724
8413
  data-topo-hub-hover-ring-radius={hoveredHub ? 17 : 14}
6725
8414
  data-topo-hub-hover-ring-stroke-width="1.75"
6726
8415
  data-topo-hub-hover-ring-opacity={hoveredHub ? (isLight ? 0.85 : 0.8) : 0}
6727
- /* Round 253 / Loop: hub hover ring also gets stroke
6728
- transition for theme toggle (cyber #10b981 ↔ light
6729
- #059669). The opacity + r transitions stay for hover
6730
- lift; stroke closes the theme-snap. */
8416
+ /* Round 535 / Loop completes the hub-cluster glow
8417
+ QUARTET by adding drop-shadow to the hub-hover-ring.
8418
+ Pre-R535 the hub-hover trio (R476 digit + R532 highlight
8419
+ disc + R533 spokes) glowed in unified emerald (digit/
8420
+ disc) + cyan/teal (spokes) on hub-hover, but the ring
8421
+ itself — the outermost solid emerald boundary at
8422
+ r=14→17 — stayed flat. R535 adds the matching emerald
8423
+ drop-shadow to the ring so the FULL hub-cluster glows
8424
+ across all four concentric surfaces on hub-hover:
8425
+ digit (typography center) drop-shadow 0 0 3px emerald
8426
+ highlight disc (r=5.5/6) drop-shadow 0 0 3px emerald
8427
+ hover-ring (r=14/17) drop-shadow 0 0 3px emerald ← this round
8428
+ spokes (mesh) drop-shadow 0 0 1.5px cyan/teal
8429
+ The ring is only visible on hub-hover (opacity=0 rest);
8430
+ adding drop-shadow at the same gate means the glow shows
8431
+ the moment the ring shows — no extra state needed.
8432
+ Same R476/R532 emerald palette since the ring sits
8433
+ inside the focal-disc tier (its color is also emerald
8434
+ #059669/#10b981).
8435
+ transition list extends to include 'filter 200ms ease-
8436
+ out' alongside the existing 180ms opacity/r — slight
8437
+ cadence mismatch (180 vs 200) is acceptable; the filter
8438
+ only appears AFTER the ring fades in via opacity, and
8439
+ the 200ms vs 180ms 20ms tail difference is below
8440
+ perceptual threshold.
8441
+ Drop-shadow visual-polish family extension (11 anchors):
8442
+ the hub-cluster glow quartet (R476/R532/R533/R535) plus
8443
+ the 7 non-hub anchors (R477/R478/R479/R480/R481/R483/
8444
+ R534) makes for a thoroughly polished glow vocabulary
8445
+ across the canvas.
8446
+ data-topo-hub-hover-ring-glow attr exposes the gate
8447
+ state for tests. */
8448
+ data-topo-hub-hover-ring-glow={!reducedMotion && hoveredHub ? 'true' : 'false'}
8449
+ /* Round 579 — hub-hover-ring joins per-element brightness
8450
+ family at 18th anchor. Stacks brightness(1.15) onto
8451
+ R535's drop-shadow — same R564/R570/R571/R572/R573/R574/
8452
+ R575/R577/R578 stacked-filter pattern. Closes hub-
8453
+ cluster brightness at 4 concentric elements (digit
8454
+ R575 + highlight R574 + halo R577 + hover-ring R579). */
8455
+ data-topo-hub-hover-ring-brightness={!reducedMotion && hoveredHub ? '1.15' : '1'}
6731
8456
  style={{
6732
8457
  pointerEvents: 'none',
6733
- transition: 'opacity 180ms ease-out, r 180ms ease-out, stroke 200ms ease-out',
8458
+ filter: !reducedMotion && hoveredHub
8459
+ ? (isLight
8460
+ ? 'drop-shadow(0 0 3px rgba(16, 185, 129, 0.5)) brightness(1.15)'
8461
+ : 'drop-shadow(0 0 3px rgba(52, 211, 153, 0.5)) brightness(1.15)')
8462
+ : undefined,
8463
+ transition: 'opacity 180ms ease-out, r 180ms ease-out, stroke 200ms ease-out, filter 200ms ease-out',
6734
8464
  }}
6735
8465
  />
6736
8466
  </g>)}
@@ -7091,7 +8821,42 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7091
8821
  strokeWidth="2.5"
7092
8822
  opacity={isChat ? (isLight ? 0.85 : 0.95) : 0}
7093
8823
  filter={!isLight && isChat ? 'url(#topo-glow)' : undefined}
7094
- style={{ pointerEvents: 'none', transition: 'opacity 200ms ease-out, stroke 200ms ease-out, filter 200ms ease-out' }}
8824
+ data-node-chat-ring-brightness={isChat ? '1.15' : '1'}
8825
+ /* 🎯 R615 — 100th consecutive visible-polish round.
8826
+ Chat-target ring (visible only when chatAlias ===
8827
+ session.alias) stacks brightness(1.15) onto its
8828
+ existing url(#topo-glow) SVG filter on cyber, or
8829
+ plain brightness on light. Same R582/R583 stacked-
8830
+ filter pattern.
8831
+
8832
+ INTRODUCES A NEW 4th BRIGHTNESS GATE TYPE — chat-
8833
+ target-gated. The cross-gate brightness family
8834
+ taxonomy now spans 4 distinct trigger conditions:
8835
+ hover-gated: R501-R613 (many surfaces)
8836
+ pin/active-gated: R571 + R587 + R607 + R609-R612
8837
+ freshness-gated: R606 (recent-row pip)
8838
+ chat-target-gated: R615 (this round, chat ring)
8839
+
8840
+ When the user opens a chat popover with a specific
8841
+ node, that node's surrounding ring (r=radius+14)
8842
+ identifies the chat target with status.primary
8843
+ stroke. R615 adds +15% brightness so the
8844
+ identification ring reads brighter — the active
8845
+ chat partner is unmistakable on the canvas.
8846
+
8847
+ Inline style.filter overrides the attribute
8848
+ filter; stacked syntax preserves the url(#topo-
8849
+ glow) on cyber. Same pattern banked at R582
8850
+ visible-path + R583 particle + R584 status-ring. */
8851
+ style={{
8852
+ pointerEvents: 'none',
8853
+ filter: isChat
8854
+ ? (isLight
8855
+ ? 'brightness(1.15)'
8856
+ : 'url(#topo-glow) brightness(1.15)')
8857
+ : undefined,
8858
+ transition: 'opacity 200ms ease-out, stroke 200ms ease-out, filter 200ms ease-out',
8859
+ } as React.CSSProperties}
7095
8860
  data-chat-target-ring
7096
8861
  data-chat-target-active={isChat ? 'true' : 'false'}
7097
8862
  data-chat-target-breath={!reducedMotion && isChat ? 'on' : 'off'}
@@ -7507,10 +9272,38 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7507
9272
  data-edge-endpoint-active={isEndpoint ? 'true' : 'false'}
7508
9273
  data-edge-endpoint-ring-stroke-width={isEndpoint ? 2.4 : 1.6}
7509
9274
  data-edge-endpoint-ring-radius={endpointR}
9275
+ data-edge-endpoint-ring-brightness={isEndpoint ? '1.15' : '1'}
7510
9276
  style={{
7511
9277
  pointerEvents: 'none',
7512
9278
  r: `${endpointR}px`,
7513
- transition: 'opacity 180ms ease-out, stroke-width 180ms ease-out, r 180ms ease-out',
9279
+ /* R585 endpoint emphasis ring gains filter
9280
+ brightness(1.15) when an edge endpoint
9281
+ activates. 24th anchor in per-element
9282
+ brightness family, and the FOURTH edge-tier
9283
+ paint layer:
9284
+ rail (R581)
9285
+ visible path (R582)
9286
+ flow particle (R583)
9287
+ endpoint ring (R585) ← this round
9288
+ Edge-tier brightness coverage closes at 4/4
9289
+ paint surfaces. The endpoint ring is the
9290
+ edge's affinity marker at the connected
9291
+ nodes — when an edge lights up, all four
9292
+ paint surfaces brighten together for a
9293
+ single coherent edge-active gesture spanning
9294
+ the curve + the node ends.
9295
+ Endpoint ring 4-axis active signature now:
9296
+ opacity R182 0 → 0.85/0.9
9297
+ sw R233 1.6 → 2.4
9298
+ r R442 +7 → +8
9299
+ brightness R585 — → 1.15 ← this round
9300
+ Plain brightness (no url-filter stack) since
9301
+ the endpoint ring has no rest-time filter
9302
+ attribute. Inline style.filter undefined at
9303
+ rest (no flicker; opacity=0 already hides
9304
+ the ring). */
9305
+ filter: isEndpoint ? 'brightness(1.15)' : undefined,
9306
+ transition: 'opacity 180ms ease-out, stroke-width 180ms ease-out, r 180ms ease-out, filter 180ms ease-out',
7514
9307
  } as React.CSSProperties}
7515
9308
  />
7516
9309
  );
@@ -7586,8 +9379,40 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7586
9379
  data-node-status-ring={status.label}
7587
9380
  data-node-status-ring-hovered={isRingHovered ? 'true' : 'false'}
7588
9381
  data-node-status-ring-stroke-width={ringStrokeWidth}
9382
+ data-node-status-ring-brightness={isRingHovered ? '1.15' : '1'}
7589
9383
  style={{
7590
- transition: 'fill 300ms ease-out, stroke 300ms ease-out, stroke-width 300ms ease-out',
9384
+ /* R584 status ring gets brightness(1.15) on
9385
+ hover. 23rd anchor in per-element brightness
9386
+ family. Stacks with url(#topo-glow) on
9387
+ cyber+online to preserve the SVG glow filter;
9388
+ plain brightness on light or cyber+offline.
9389
+ Same R582/R583 stacked-filter pattern: inline
9390
+ style.filter overrides the attribute filter,
9391
+ stacked syntax preserves the glow on hover.
9392
+
9393
+ Per-node hover signature now 10 layers (added
9394
+ to the R438 stack):
9395
+ R26 group translateY -2px
9396
+ R217 stroke tint
9397
+ R142 drop-shadow boost
9398
+ R427 alias letter-spacing
9399
+ R428 sub-text letter-spacing
9400
+ R429 body opacity 0.94 → 1.0
9401
+ R430 hub-spoke α+
9402
+ R435 hub-spoke sw+
9403
+ R438 status-ring sw +0.5
9404
+ R584 status-ring brightness(1.15) ← this round
9405
+
9406
+ Per-element brightness family: 23 anchors.
9407
+ Stacked-filter sub-pattern: 17 anchors. */
9408
+ filter: isRingHovered
9409
+ ? (isLight
9410
+ ? 'brightness(1.15)'
9411
+ : (isOnline
9412
+ ? 'url(#topo-glow) brightness(1.15)'
9413
+ : 'brightness(1.15)'))
9414
+ : undefined,
9415
+ transition: 'fill 300ms ease-out, stroke 300ms ease-out, stroke-width 300ms ease-out, filter 300ms ease-out',
7591
9416
  }}
7592
9417
  />
7593
9418
  );
@@ -7643,6 +9468,30 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7643
9468
  const internByAlias = /书生|书小生|intern/i.test(session.alias);
7644
9469
 
7645
9470
  if (isIntern || internByAlias || vendor.logo) {
9471
+ /* Round 501 / Loop — vendor avatar inside node circles
9472
+ gains a hover-gated brightness lift. Pre-R501 the
9473
+ avatar <image> was the only per-node surface with
9474
+ NO hover treatment: R26 lifted the card, R242 tinted
9475
+ the card stroke, R427 spread the alias letter-
9476
+ spacing, R500 added the alias drop-shadow, R208
9477
+ lifted the runtime badge ring, R443 thickened
9478
+ the badge icon stroke, R177 brightened the
9479
+ halo — but the most visually-prominent element
9480
+ (the vendor logo / 书生 coin centred in each node)
9481
+ stayed paint-static. R501 closes the per-node
9482
+ hover-affordance arc by adding a 15% brightness
9483
+ lift on hover.
9484
+ Implementation: CSS filter: brightness(1.15)
9485
+ when hoveredAlias === session.alias. Pure paint
9486
+ axis on the <image> element — no geometry change,
9487
+ no bbox shift. Modern-browser supported (Chrome 64+
9488
+ / FF 56+ / Safari 9.1+).
9489
+ Hits 节点视觉 theme. data-node-avatar-hovered
9490
+ attr surfaces the gate for tests.
9491
+ Gated on !reducedMotion as a courtesy (brightness
9492
+ transition < ~50ms still feels instant; the gate
9493
+ avoids the transition cycle for a11y users). */
9494
+ const isAvatarHovered = !reducedMotion && hoveredAlias === session.alias;
7646
9495
  return (
7647
9496
  <image
7648
9497
  href={vendor.logo ?? '/intern_avatar.png'}
@@ -7651,9 +9500,137 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7651
9500
  width={size}
7652
9501
  height={size}
7653
9502
  preserveAspectRatio="xMidYMid meet"
9503
+ data-node-avatar={session.alias}
9504
+ data-node-avatar-hovered={isAvatarHovered ? 'true' : 'false'}
9505
+ data-node-avatar-rotate={isAvatarHovered ? '3' : '0'}
9506
+ data-node-avatar-scale={isAvatarHovered ? '1.05' : '1'}
9507
+ data-node-avatar-drop-shadow={isAvatarHovered ? `0 0 4px ${pal.legendAccent}99` : 'none'}
9508
+ style={{
9509
+ /* R600 milestone — node vendor avatar gains
9510
+ hover-rotate-3 via Tailwind v4 individual
9511
+ `rotate` CSS property. 6th anchor in hover-
9512
+ rotate idiom (R350 reset / R547 pill × /
9513
+ R549 brand logo / R576 fullscreen / R599
9514
+ runtime badge / R600 node avatar).
9515
+
9516
+ Same +3° "wobble awake" tilt as R576
9517
+ fullscreen + R599 runtime badge — the
9518
+ vendor logo (Claude/OpenAI/etc.) at the
9519
+ center of every node gently tilts under
9520
+ cursor. Most visible per-node element gains
9521
+ a rotation signal on top of R501's
9522
+ brightness lift.
9523
+
9524
+ transform-origin pinned to the node center
9525
+ (pos.x, pos.y) so the logo rotates around
9526
+ its own visual centre, not the <image>
9527
+ element's default bbox top-left.
9528
+
9529
+ Per-node hover signature now 11 axes:
9530
+ R26 group translateY -2px
9531
+ R217 stroke tint
9532
+ R142 drop-shadow boost
9533
+ R427 alias letter-spacing
9534
+ R428 sub-text letter-spacing
9535
+ R429 body opacity 0.94 → 1.0
9536
+ R430 hub-spoke α+
9537
+ R435 hub-spoke sw+
9538
+ R438 status-ring sw +0.5
9539
+ R584 status-ring brightness(1.15)
9540
+ R600 avatar rotate 0 → 3deg ← this round
9541
+
9542
+ Hover-rotate idiom family (6 anchors):
9543
+ R350 chrome reset icon -8°
9544
+ R547 chip pill × close (rotate)
9545
+ R549 brand 书生 logo (rotate)
9546
+ R576 chrome fullscreen icon +3°
9547
+ R599 node runtime badge +3°
9548
+ R600 node vendor avatar +3° ← milestone
9549
+
9550
+ transition list extends 'rotate 200ms
9551
+ ease-out' alongside the existing 'filter
9552
+ 200ms ease-out' — both axes ride one beat. */
9553
+ rotate: isAvatarHovered ? '3deg' : '0deg',
9554
+ /* R602 — per-node avatar gains hover-scale-1.05.
9555
+ Mirrors R548 brand 书生 logo's scale-105 idiom
9556
+ at the per-node tier. Stacks with R600 rotate
9557
+ + R501 brightness so the avatar gets a 3-axis
9558
+ hover signature (scale + rotate + brightness)
9559
+ matching the brand-logo polish vocabulary.
9560
+ Tailwind v4 individual `scale` CSS property
9561
+ (banked R547 sibling to `rotate`) — independent
9562
+ of SVG <image>'s x/y/width/height attributes;
9563
+ composes cleanly with rotate. transform-origin
9564
+ shared with R600 — pivots around node center.
9565
+ Pure paint axis: SVG bbox attributes unchanged
9566
+ at rest, so overlap-test invariant holds (the
9567
+ probe reads attributes, not visual bbox post-
9568
+ CSS transform). Scale only fires on hover. */
9569
+ scale: isAvatarHovered ? '1.05' : '1',
9570
+ transformOrigin: `${pos.x}px ${pos.y}px`,
9571
+ /* R605 — per-node avatar gains drop-shadow on
9572
+ hover, stacked with R501 brightness(1.15).
9573
+ 4th hover axis on the avatar (brightness +
9574
+ rotate + scale + drop-shadow), bringing
9575
+ avatar coverage closer to brand 书生 logo's
9576
+ 5-axis signature (only missing R553 always-
9577
+ on breath, which would be visually noisy
9578
+ across 30 nodes).
9579
+
9580
+ Color: pal.legendAccent at 0x99 alpha (~60%)
9581
+ — cyan/teal accent matches the edge-tier
9582
+ vocabulary (R534 edge-badge hover-glow,
9583
+ R478 recent-row pip glow, R479 group-label
9584
+ glow). The avatar joins the "lit by accent
9585
+ on attention" family at the per-node tier.
9586
+
9587
+ 4px blur radius reads tight on the avatar's
9588
+ 14-28px footprint — smaller than R604 brand
9589
+ logo's 8px (40×40 footprint) for size
9590
+ proportionality.
9591
+
9592
+ Same R582/R583 stacked-filter pattern.
9593
+ transition list already includes 'filter
9594
+ 200ms ease-out' from R501; no transition
9595
+ change needed.
9596
+
9597
+ Per-node avatar hover signature now 4 axes:
9598
+ R501/R558 brightness 1 → 1.15
9599
+ R600/R601 rotate 0 → 3deg
9600
+ R602 scale 1 → 1.05
9601
+ R605 drop-shadow → pal.legendAccent ← this round */
9602
+ filter: isAvatarHovered
9603
+ ? `drop-shadow(0 0 4px ${pal.legendAccent}99) brightness(1.15)`
9604
+ : undefined,
9605
+ transition: 'filter 200ms ease-out, rotate 200ms ease-out, scale 200ms ease-out',
9606
+ }}
7654
9607
  />
7655
9608
  );
7656
9609
  }
9610
+ /* Round 558 / Loop — closes the per-node avatar hover-
9611
+ affordance arc by extending R501's brightness lift
9612
+ (image-branch only) to the TWO remaining avatar
9613
+ variants: vendor monogram + prefix-group hue-hashed
9614
+ initial fallback. Pre-R558 only the vendor.logo
9615
+ image branch lifted on hover; the other two
9616
+ variants stayed paint-static under attention.
9617
+
9618
+ Per-node avatar hover-brightness family (3 anchors,
9619
+ all gated on !reducedMotion && hoveredAlias matches):
9620
+ R501 vendor.logo image filter on <image>
9621
+ R558 vendor monogram filter on wrapping <g>
9622
+ R558 prefix-group fallback filter on wrapping <g>
9623
+
9624
+ Implementation: each fallback branch returns a
9625
+ fragment with <circle> + <text> as siblings.
9626
+ Wrapping them in a single <g> with the filter
9627
+ centralizes the paint axis. Same brightness(1.15)
9628
+ value as R501 for cross-branch consistency. Same
9629
+ transition cadence (filter 200ms ease-out).
9630
+
9631
+ data-node-avatar-monogram-hovered + -fallback-
9632
+ hovered attrs surface the gates for tests. */
9633
+ const isAvatarFallbackHovered = !reducedMotion && hoveredAlias === session.alias;
7657
9634
  if (vendor.id !== 'unknown') {
7658
9635
  // Known model house, logo asset not in public/vendors/
7659
9636
  // yet — vendor-tinted monogram stands in.
@@ -7673,7 +9650,43 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7673
9650
  where less visual weight signals "we don't know
7674
9651
  what this is" appropriately. */
7675
9652
  return (
7676
- <>
9653
+ <g
9654
+ data-node-avatar-monogram={session.alias}
9655
+ data-node-avatar-monogram-hovered={isAvatarFallbackHovered ? 'true' : 'false'}
9656
+ data-node-avatar-monogram-rotate={isAvatarFallbackHovered ? '3' : '0'}
9657
+ data-node-avatar-monogram-scale={isAvatarFallbackHovered ? '1.05' : '1'}
9658
+ data-node-avatar-monogram-drop-shadow={isAvatarFallbackHovered ? `0 0 4px ${pal.legendAccent}99` : 'none'}
9659
+ style={{
9660
+ /* R601 — vendor-monogram fallback gains hover-
9661
+ rotate-3 mirroring R600's image-branch
9662
+ closure. Per-node avatar rotate coverage
9663
+ now 2/3 (image R600, monogram R601 here;
9664
+ prefix-group fallback completes 3/3 below).
9665
+ Same R558 closure-of-arc semantics applied
9666
+ to the rotate axis — what brightness did
9667
+ for the avatar 3-branch arc (R501 image +
9668
+ R558 monogram + R558 fallback), rotate
9669
+ now does (R600 + R601 monogram + R601
9670
+ fallback).
9671
+ transform-origin pinned to node center
9672
+ (pos.x, pos.y) — same idiom as R600. */
9673
+ rotate: isAvatarFallbackHovered ? '3deg' : '0deg',
9674
+ /* R602 sibling — vendor monogram fallback
9675
+ gains hover-scale-1.05 to match the image
9676
+ branch above. Per-node avatar scale coverage
9677
+ now 2/3 (image R602, monogram R602 here;
9678
+ prefix-group fallback completes 3/3 below). */
9679
+ scale: isAvatarFallbackHovered ? '1.05' : '1',
9680
+ transformOrigin: `${pos.x}px ${pos.y}px`,
9681
+ /* R605 sibling — monogram fallback gains
9682
+ stacked drop-shadow + brightness filter
9683
+ on hover, mirroring the image branch above. */
9684
+ filter: isAvatarFallbackHovered
9685
+ ? `drop-shadow(0 0 4px ${pal.legendAccent}99) brightness(1.15)`
9686
+ : undefined,
9687
+ transition: 'filter 200ms ease-out, rotate 200ms ease-out, scale 200ms ease-out',
9688
+ }}
9689
+ >
7677
9690
  <circle cx={pos.x} cy={pos.y} r={ar} fill={vendor.mono.bg} stroke={vendor.mono.ring} strokeWidth="1.5" />
7678
9691
  {/* Round 284 / Loop: known-vendor monogram letter
7679
9692
  swaps fontFamily monospace → system sans-serif.
@@ -7705,14 +9718,51 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7705
9718
  >
7706
9719
  {vendor.initial}
7707
9720
  </text>
7708
- </>
9721
+ </g>
7709
9722
  );
7710
9723
  }
7711
9724
  // Round 106 (issue #83): hue keyed to the prefix group,
7712
9725
  // not the full alias — every 通信* node shares one color.
7713
9726
  const c = aliasAvatarColors(groupKeys[session.alias] || session.alias);
7714
9727
  return (
7715
- <>
9728
+ <g
9729
+ data-node-avatar-fallback={session.alias}
9730
+ data-node-avatar-fallback-hovered={isAvatarFallbackHovered ? 'true' : 'false'}
9731
+ data-node-avatar-fallback-rotate={isAvatarFallbackHovered ? '3' : '0'}
9732
+ data-node-avatar-fallback-scale={isAvatarFallbackHovered ? '1.05' : '1'}
9733
+ data-node-avatar-fallback-drop-shadow={isAvatarFallbackHovered ? `0 0 4px ${pal.legendAccent}99` : 'none'}
9734
+ style={{
9735
+ /* R601 — prefix-group hue-hashed initial fallback
9736
+ gains hover-rotate-3 closing per-node avatar
9737
+ rotate coverage at 3/3 branches (image R600 +
9738
+ monogram R601 + fallback R601 here).
9739
+ Same idiom across all 3 branches: rotate 3deg
9740
+ on hover, transform-origin pinned to node
9741
+ center, transition 'rotate 200ms ease-out'
9742
+ alongside the existing 'filter 200ms' filter
9743
+ transition from R558. Whatever the vendor-
9744
+ detection branch resolves to (image / monogram
9745
+ / hue-hashed initial), the user gets the same
9746
+ +3° wobble-awake gesture on alias hover. */
9747
+ rotate: isAvatarFallbackHovered ? '3deg' : '0deg',
9748
+ /* R602 sibling — prefix-group hue-hashed fallback
9749
+ gains hover-scale-1.05 closing per-node avatar
9750
+ scale coverage at 3/3 branches (image + monogram
9751
+ + fallback all share the same scale-1.05 +
9752
+ rotate-3deg + brightness-1.15 hover signature). */
9753
+ scale: isAvatarFallbackHovered ? '1.05' : '1',
9754
+ transformOrigin: `${pos.x}px ${pos.y}px`,
9755
+ /* R605 sibling — prefix-group hue-hashed fallback
9756
+ gains stacked drop-shadow + brightness filter
9757
+ on hover. Closes per-node avatar drop-shadow
9758
+ coverage at 3/3 branches (image + monogram +
9759
+ fallback all share the same stacked filter). */
9760
+ filter: isAvatarFallbackHovered
9761
+ ? `drop-shadow(0 0 4px ${pal.legendAccent}99) brightness(1.15)`
9762
+ : undefined,
9763
+ transition: 'filter 200ms ease-out, rotate 200ms ease-out, scale 200ms ease-out',
9764
+ }}
9765
+ >
7716
9766
  <circle cx={pos.x} cy={pos.y} r={ar} fill={c.bg} stroke={c.ring} strokeWidth="1" />
7717
9767
  <text
7718
9768
  x={pos.x}
@@ -7726,7 +9776,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7726
9776
  >
7727
9777
  {aliasInitial(session.alias)}
7728
9778
  </text>
7729
- </>
9779
+ </g>
7730
9780
  );
7731
9781
  })()}
7732
9782
  {/* Issue #96: runtime badge — small corner glyph marking the
@@ -7757,7 +9807,106 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7757
9807
  // badge-active exposes the gate for tests.
7758
9808
  const isNodeActive = !reducedMotion && hoveredAlias === session.alias;
7759
9809
  return (
7760
- <g style={{ pointerEvents: 'none' }}>
9810
+ /* Round 559 / Loop — runtime badge outer <g> picks up
9811
+ a drop-shadow glow on node hover, using the runtime's
9812
+ own identity color (rt.color, hex). 16th anchor in
9813
+ the drop-shadow visual-polish family. Pairs with
9814
+ existing R208 ring r-lift + R443 icon sw-lift for
9815
+ a 3-axis runtime-badge hover signature now spanning
9816
+ geometry + stroke + paint glow:
9817
+ R208 ring r 7 → 8 (online)
9818
+ R208 ring sw 1.5 → 2
9819
+ R443 icon sw 2.4 → 2.8
9820
+ R559 outer filter none → drop-shadow(rt.color) ← this round
9821
+ Filter on the OUTER <g> applies uniformly to both
9822
+ the ring <circle> and the inner icon <path> —
9823
+ single paint-axis lift covers both layers in one
9824
+ motion-coherent gesture.
9825
+ Hue: `${rt.color}99` hex+alpha (60%) — rt.color is
9826
+ 6-digit hex (#a78bfa / #38bdf8 / #34d399 / #fbbf24
9827
+ per lib/vendorIdentity.ts), so hex+alpha concat is
9828
+ safe (banked R541 pattern: hex sources use hex+
9829
+ alpha; only hsl/color()/dynamic sources need color-
9830
+ mix).
9831
+ 2px blur reads tight on a small badge (r=7 online
9832
+ / r=5.5 offline). transition list adds 'filter
9833
+ 150ms ease-out' matching the R208 ring r/sw cadence
9834
+ at this surface.
9835
+ Drop-shadow visual-polish family extension (16
9836
+ anchors now): R476/R477/R478/R479/R480/R481 +
9837
+ R500/R532-R536/R537/R538/R540/R543-R546/R550 +
9838
+ R559 (this round).
9839
+ data-runtime-badge-glow attr surfaces the gate
9840
+ for tests. */
9841
+ <g
9842
+ data-runtime-badge-glow={isNodeActive ? 'true' : 'false'}
9843
+ data-runtime-badge-brightness={isNodeActive ? '1.15' : '1'}
9844
+ data-runtime-badge-rotate={isNodeActive ? '3' : '0'}
9845
+ style={{
9846
+ pointerEvents: 'none',
9847
+ /* R599 — runtime badge gains hover-rotate-3.
9848
+ 5th anchor in hover-rotate idiom (R350 reset
9849
+ -8° / R547 pill × / R549 brand logo / R576
9850
+ fullscreen +3° / R599 runtime badge +3°).
9851
+ Same +3° tilt as R576 fullscreen icon — at the
9852
+ 14×14 badge scale, a 3° rotation reads as a
9853
+ subtle "wobble awake under attention" gesture
9854
+ that doesn't overwhelm the small surface.
9855
+ Applied via CSS individual `rotate` property
9856
+ (Tailwind v4 banked R547 — independent of SVG
9857
+ transform attribute, composes cleanly with
9858
+ the inner <g>'s translate+scale transform).
9859
+ transform-origin pinned to the badge center
9860
+ (bx, by) so rotation pivots around the
9861
+ visual centre, not the outer <g>'s default
9862
+ bbox corner — keeps the ring and icon spinning
9863
+ in place rather than orbiting offset.
9864
+ Runtime badge hover signature now 6 axes —
9865
+ densest per-node element in TopoGraph:
9866
+ R208 ring r 7 → 8
9867
+ R208 ring sw 1.5 → 2
9868
+ R443 icon sw 2.4 → 2.8
9869
+ R559 outer filter none → drop-shadow(rt.color)
9870
+ R586 outer filter stack brightness(1.15)
9871
+ R599 outer rotate 0 → 3deg ← this round
9872
+ transition list extends with 'rotate 150ms
9873
+ ease-out' matching R586/R559 cadence at this
9874
+ surface. Six axes all ride one 150ms beat.
9875
+ Hover-rotate idiom family extension
9876
+ (5 anchors): R350 / R547 / R549 / R576 / R599. */
9877
+ rotate: isNodeActive ? '3deg' : '0deg',
9878
+ transformOrigin: `${bx}px ${by}px`,
9879
+ /* R586 — runtime badge outer <g> stacks
9880
+ brightness(1.15) onto the existing R559
9881
+ drop-shadow on node hover. 25th anchor in
9882
+ per-element brightness family, 18th in
9883
+ stacked-filter sub-pattern.
9884
+
9885
+ Runtime badge hover signature now CLOSED
9886
+ at 4 axes (geometry + stroke + paint glow
9887
+ + paint brightness):
9888
+ R208 ring r 7 → 8
9889
+ R208 ring sw 1.5 → 2
9890
+ R443 icon sw 2.4 → 2.8
9891
+ R559 outer filter none → drop-shadow(rt.color)
9892
+ R586 outer filter stack brightness(1.15) ← this round
9893
+
9894
+ The drop-shadow + brightness stack is the
9895
+ banked R564/R570 "halo + glow" pattern —
9896
+ drop-shadow paints the colored halo, brightness
9897
+ lifts the underlying paint (ring stroke +
9898
+ icon path both gain ~15% luminance). Single
9899
+ CSS filter chain on the outer <g> covers
9900
+ both child layers uniformly.
9901
+
9902
+ Same R208/R443/R559 150ms cadence preserved
9903
+ via the existing transition. */
9904
+ filter: isNodeActive
9905
+ ? `drop-shadow(0 0 2px ${rt.color}99) brightness(1.15)`
9906
+ : undefined,
9907
+ transition: 'filter 150ms ease-out, rotate 150ms ease-out',
9908
+ }}
9909
+ >
7761
9910
  <circle
7762
9911
  cx={bx} cy={by} r={br}
7763
9912
  fill={pal.containerBg}
@@ -8010,11 +10159,39 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8010
10159
  data-node-label-card-elevation={
8011
10160
  !reducedMotion && hoveredAlias === session.alias ? 'hover' : 'idle'
8012
10161
  }
10162
+ data-node-label-card-brightness={!reducedMotion && hoveredAlias === session.alias ? '1.15' : '1'}
8013
10163
  style={{
10164
+ /* R613 — per-node label card stacks brightness
10165
+ (1.15) onto R142's hover drop-shadow. Same
10166
+ banked R582/R583 stacked-filter pattern.
10167
+ 4th paint axis on the card's hover signature:
10168
+ R217 stroke tint → pal.legendAccent
10169
+ R211 fill ease 200ms
10170
+ R142 drop-shadow rest → hover (deeper)
10171
+ R613 brightness 1 → 1.15 ← this round
10172
+
10173
+ The card's bg fill (pal.labelBox.fill: dark
10174
+ navy on cyber / white on light) lifts +15%
10175
+ alongside the existing drop-shadow
10176
+ elevation — cyber theme reads as "card lit
10177
+ warmer under attention" since the dark fill
10178
+ gains visible luminance from the +15%
10179
+ multiplication. Light theme bg is already
10180
+ near-white so brightness barely shifts the
10181
+ rect, but the cyan-tinted stroke (R217)
10182
+ still lifts visibly through the brightness
10183
+ filter.
10184
+
10185
+ Drop-shadow exists at BOTH rest + hover
10186
+ tiers (R142); brightness only stacks on
10187
+ the hover tier. Rest stays at plain DS.
10188
+
10189
+ Existing 'filter 220ms ease-out' transition
10190
+ covers brightness at the same cadence. */
8014
10191
  filter: !reducedMotion && hoveredAlias === session.alias
8015
10192
  ? (isLight
8016
- ? 'drop-shadow(0 3px 8px rgba(15,23,42,0.20))'
8017
- : 'drop-shadow(0 4px 12px rgba(0,0,0,0.60))')
10193
+ ? 'drop-shadow(0 3px 8px rgba(15,23,42,0.20)) brightness(1.15)'
10194
+ : 'drop-shadow(0 4px 12px rgba(0,0,0,0.60)) brightness(1.15)')
8018
10195
  : (isLight
8019
10196
  ? 'drop-shadow(0 1px 2px rgba(15,23,42,0.08))'
8020
10197
  : 'drop-shadow(0 1px 2px rgba(0,0,0,0.30))'),
@@ -8124,10 +10301,63 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8124
10301
  letterSpacing:
8125
10302
  chatAlias === session.alias ? '0.5px' :
8126
10303
  hoveredAlias === session.alias ? '0.3px' : '0px',
8127
- filter: !reducedMotion && hoveredAlias === session.alias
8128
- ? `drop-shadow(0 0 2px ${status.text}80)`
10304
+ /* Round 564 / Loop alias text filter stacks
10305
+ brightness(1.15) on top of R500's drop-shadow
10306
+ on hover. Mirrors R542 pressure-seg pattern
10307
+ (brightness + drop-shadow in one stacked
10308
+ filter declaration). Pre-R564 hover added
10309
+ only a drop-shadow halo around the glyph;
10310
+ post-R564 the glyph ALSO brightens, so the
10311
+ identity text reads as both "glowing" AND
10312
+ "lit up" under attention — dual paint axes
10313
+ through one filter chain.
10314
+ CSS filter supports multiple functions
10315
+ applied left-to-right. brightness(1.15)
10316
+ lifts the per-status text color (status.text:
10317
+ green/teal/slate per tier) by 15%; the drop-
10318
+ shadow then paints the outer halo in the
10319
+ status-tier hue. Together: the alias glyph
10320
+ both intensifies its identity color AND
10321
+ radiates outward in that same color.
10322
+ Same +15% brightness as R501 vendor logo
10323
+ avatar (banked per-node hover-brightness
10324
+ pattern). Consistent +15% across all per-
10325
+ node identity surfaces (logo, monogram,
10326
+ fallback avatar from R558, AND now alias
10327
+ text). Cross-element brightness consistency.
10328
+ data-node-alias-brightness attr surfaces
10329
+ the lift for tests. */
10330
+ /* R616 — extends R564 alias text brightness
10331
+ gate to ALSO fire on chatAlias === alias.
10332
+ 2nd anchor in chat-target-gated brightness
10333
+ family (sibling to R615 chat ring).
10334
+
10335
+ Pre-R616: filter only fired on hoveredAlias
10336
+ match. The chat-target stayed at flat fill
10337
+ color while the chat ring glowed around it
10338
+ — visual disparity between identification
10339
+ elements that should read as a unified
10340
+ "this is the chat partner" gesture.
10341
+
10342
+ Post-R616: alias text brightens on EITHER
10343
+ hover OR chat-target. Mirrors the existing
10344
+ 3-tier letter-spacing (R427/R427-chat) which
10345
+ already responds to both gates. The chat
10346
+ partner's alias text now reads:
10347
+ +15% brighter (R616)
10348
+ +0.5px letter-spaced (R427-chat tier)
10349
+ surrounded by a glowing ring (R615)
10350
+ Unified identification gesture across 3
10351
+ axes when chat is open.
10352
+
10353
+ Same drop-shadow + brightness filter chain
10354
+ (no separate color for chat — same R564
10355
+ pattern, just expanded gate). */
10356
+ filter: !reducedMotion && (hoveredAlias === session.alias || chatAlias === session.alias)
10357
+ ? `drop-shadow(0 0 2px ${status.text}80) brightness(1.15)`
8129
10358
  : undefined,
8130
10359
  }}
10360
+ data-node-alias-brightness={!reducedMotion && (hoveredAlias === session.alias || chatAlias === session.alias) ? '1.15' : '1'}
8131
10361
  >
8132
10362
  {truncate(session.alias, fullMax)}
8133
10363
  </text>
@@ -8167,6 +10397,31 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8167
10397
  alias > status hierarchy holds at the type
8168
10398
  level. data-node-sub-text-font-weight attr
8169
10399
  exposes the value for tests. */}
10400
+ {/* Round 567 / Loop — node sub-text joins the per-
10401
+ node hover-brightness consistency family. Pre-
10402
+ R567 sub-text had only fill brighten (R211) +
10403
+ ls 0→0.2 (R428); the alias text above lifted
10404
+ via R500 drop-shadow + R564 brightness(1.15)
10405
+ stacked. R567 adds brightness(1.15) to sub-
10406
+ text on the same isNodeActive gate so it
10407
+ chromatically lifts together with the alias.
10408
+ Per-node hover-brightness consistency family
10409
+ — 6 anchors at uniform +15%:
10410
+ R501 vendor.logo image filter on <image>
10411
+ R558 vendor monogram filter on <g>
10412
+ R558 prefix-group fallback filter on <g>
10413
+ R564 alias text (stacked w/ DS) brightness(1.15)
10414
+ R567 node sub-text brightness(1.15) ← this round
10415
+ (+ R559 runtime badge drop-shadow tier-color glow)
10416
+ Now every per-node identity surface (3 avatar
10417
+ variants + alias + sub-text + badge) lifts
10418
+ together on node hover with consistent visual
10419
+ response.
10420
+ Pure paint axis; bbox unchanged. transition
10421
+ list extends to include 'filter 200ms ease-
10422
+ out' matching R428 ls cadence at this scope.
10423
+ data-node-sub-text-brightness attr exposes
10424
+ the lift for tests. */}
8170
10425
  <text
8171
10426
  x="0" y={subY} textAnchor="middle"
8172
10427
  fill={status.primary}
@@ -8174,10 +10429,29 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8174
10429
  fontWeight="500"
8175
10430
  data-node-sub-text={session.alias}
8176
10431
  data-node-sub-text-hovered={hoveredAlias === session.alias ? 'true' : 'false'}
10432
+ data-node-sub-text-chat-target={chatAlias === session.alias ? 'true' : 'false'}
8177
10433
  data-node-sub-text-font-weight="500"
10434
+ data-node-sub-text-brightness={!reducedMotion && (hoveredAlias === session.alias || chatAlias === session.alias) ? '1.15' : '1'}
10435
+ /* R617 — extends R567 sub-text brightness gate
10436
+ to ALSO fire on chatAlias === alias. 3rd anchor
10437
+ in chat-target-gated brightness family (R615
10438
+ chat ring + R616 alias text + R617 sub-text).
10439
+
10440
+ Both per-node text lines (alias + sub-text)
10441
+ now brighten together when their node is the
10442
+ chat partner — unified "this is the chat
10443
+ partner" gesture across all per-node text
10444
+ identification surfaces.
10445
+
10446
+ Same R567 plain brightness(1.15) filter (no
10447
+ drop-shadow stack at sub-text scope per R567).
10448
+ Same gate-union pattern as R616. */
8178
10449
  style={{
8179
- transition: 'fill 300ms ease-out, letter-spacing 200ms ease-out',
10450
+ transition: 'fill 300ms ease-out, letter-spacing 200ms ease-out, filter 200ms ease-out',
8180
10451
  letterSpacing: hoveredAlias === session.alias ? '0.2px' : '0px',
10452
+ filter: !reducedMotion && (hoveredAlias === session.alias || chatAlias === session.alias)
10453
+ ? 'brightness(1.15)'
10454
+ : undefined,
8181
10455
  }}
8182
10456
  >
8183
10457
  {status.label}{isOnline && sseCountFor != null ? ` sse:${sseCountFor}` : ''}
@@ -8440,7 +10714,38 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8440
10714
  strokeWidth="2"
8441
10715
  opacity="0"
8442
10716
  data-click-ripple
8443
- style={{ pointerEvents: 'none' }}
10717
+ data-click-ripple-glow={`0 0 4px ${clickRipple.color}99`}
10718
+ style={{
10719
+ pointerEvents: 'none',
10720
+ /* R608 — click-ripple gains drop-shadow glow that
10721
+ matches the ripple's stroke color. The expanding
10722
+ ring (r0+4 → r0+30 over 500ms) now visually pulses
10723
+ outward as a "lit" ring rather than a plain stroke
10724
+ line — the user's "I clicked" moment reads more
10725
+ confidently as a feedback signal.
10726
+
10727
+ Color: clickRipple.color (status.primary from the
10728
+ click target: green for working / teal for idle /
10729
+ slate for offline; cyan for hub clicks) at 0x99
10730
+ alpha (~60%) — matches the source element's status
10731
+ identity, so the ripple inherits the visual hue of
10732
+ what the user clicked.
10733
+
10734
+ 4px blur radius is the same as R605 avatar
10735
+ drop-shadow; reads as soft halo, not loud bloom,
10736
+ at the ripple's wide 30+ px expansion range.
10737
+
10738
+ Drop-shadow visual-polish family extension —
10739
+ click-feedback surface adds 1 more anchor to the
10740
+ family (now ~23 anchors with R604/R605 + R608).
10741
+
10742
+ The SMIL animates r and opacity for 500ms; filter
10743
+ is static (no animation needed — the opacity fade
10744
+ carries the visual decay automatically). No
10745
+ transition needed since the element is created
10746
+ fresh each click. */
10747
+ filter: `drop-shadow(0 0 4px ${clickRipple.color}99)`,
10748
+ }}
8444
10749
  >
8445
10750
  <animate
8446
10751
  attributeName="r"
@@ -8654,7 +10959,55 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8654
10959
  200ms ease-out' alongside R345's ls + R55's fill
8655
10960
  200ms. data-recent-panel-title-fw exposes the
8656
10961
  resolved weight for tests. */}
8657
- <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>
10962
+ {/* Round 550 / Loop drop-shadow visual-polish family
10963
+ extends to a 14th anchor: the recent-panel header
10964
+ title gains a soft pal.legendAccent glow when the
10965
+ panel has an active row (activeEdgeKey). Pre-R550
10966
+ the title's state-flip on active was 2-axis (R482
10967
+ fw 700→800 + R345 ls 0.3→0.4 when panel-hovered);
10968
+ R550 adds the paint axis so the title brightens
10969
+ paint-wise alongside the typographic tightening when
10970
+ a row inside its panel is locked.
10971
+
10972
+ Hue: pal.legendAccent + hex alpha '80' (~50%) — same
10973
+ strength as R479 group-label pin-glow at the parent
10974
+ panel-title scope. 2px blur reads soft; cyan accent
10975
+ ties the title visually to the active row's pin
10976
+ colour (cyber: cyan-300 / light: teal-600). Hex+alpha
10977
+ concat safe — pal.legendAccent is '#67e8f9' (cyber)
10978
+ or '#0d9488' (light), both 6-digit hex (banked R541:
10979
+ hex sources use hex+alpha; only hsl/color()/dynamic
10980
+ sources need color-mix).
10981
+
10982
+ Drop-shadow visual-polish family extension (14
10983
+ anchors now):
10984
+ R476 hub digit hover-gated emerald
10985
+ R477 legend pin-ring pin-gated row.fill
10986
+ R478 recent-row pip freshness cyan
10987
+ R479 group-label text pin-gated cyan
10988
+ R532-R536 hub-cluster glow QUINTET
10989
+ R537 legend swatch hover/pin row.fill
10990
+ R538 group-label hover-precedence
10991
+ R540 edge-badge text pin-gated cyan
10992
+ R543-R546 pin-active pill 4-variant arc
10993
+ R550 recent-panel title pin-gated cyan ← this round
10994
+ R550 legend-panel title pin-gated cyan ← sibling (next text below)
10995
+
10996
+ filter is paint-only; bbox unchanged; overlap-test
10997
+ invariants hold. transition list extends to include
10998
+ 'filter 200ms ease-out' alongside R345 ls + R482 fw
10999
+ + R55 fill 200ms — one motion-coherent 3-axis active-
11000
+ state lift.
11001
+ data-recent-panel-title-glow attr exposes the gate
11002
+ state for tests. */}
11003
+ {/* Round 573 / Loop — recent panel-title joins per-element
11004
+ brightness family at 11th anchor. Stacks brightness(1.15)
11005
+ onto R550's active-gated drop-shadow. Mirrors R572
11006
+ panel-row text pattern at the panel-TITLE tier — both
11007
+ panels now have brightness in their active signature
11008
+ at BOTH chrome tiers (title + row text), completing
11009
+ the panel paint-axis cascade. */}
11010
+ <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>
8658
11011
  {/* R96: header count now matches what the rows show. Pre-R96
8659
11012
  this read "X msgs" off the raw messages array, but the
8660
11013
  rows below render DEDUPED flowLinks — so a fleet with 10
@@ -8729,21 +11082,59 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8729
11082
  textAnchor="end"
8730
11083
  fontSize="10"
8731
11084
  fontFamily="monospace"
8732
- // Round 349 / Loop: editorial letter-spacing 0.2 on the
8733
- // recent-signal panel header count. Sits one tier below
8734
- // the R301 panel title letterSpacing="0.3" so the panel
8735
- // header reads as a 2-step hierarchy (title 0.3 / count
8736
- // 0.2). Sibling change on the legend panel count below
8737
- // closes the panel-pair editorial symmetry. Joins the
8738
- // R285 / R289 / R301 / R302 / R304 / R325 editorial-
8739
- // letterspacing tier at the panel-summary scope. The
8740
- // R162 freshness fill, R225 tabular-nums, R311 fw=600,
8741
- // R336 unit-tspan opacity-0.7 split all preserved —
8742
- // the tier propagates to all descendant tspans via
8743
- // SVG inheritance. data-recent-panel-count-letter-
8744
- // spacing exposes the value for tests.
8745
- letterSpacing="0.2"
8746
- data-recent-panel-count-letter-spacing="0.2"
11085
+ /* Round 566 / Loop recent-panel-count gains hover-
11086
+ state letter-spacing tween (0.2 0.4 on hovered-
11087
+ Panel === 'recent'). Pairs with R424 fw 600→700
11088
+ on the same gate. Count now has 2-axis hover
11089
+ signature (fw + ls), matching the panel title's
11090
+ R345 ls + R482 fw lift pattern at the panel-
11091
+ header data-tspan scope. R349 editorial 0.2
11092
+ baseline preserved at rest only hover lifts.
11093
+ Sibling treatment on the legend-panel count
11094
+ below closes the panel-pair symmetry. */
11095
+ letterSpacing={hoveredPanel === 'recent' ? '0.4' : '0.2'}
11096
+ data-recent-panel-count-letter-spacing={hoveredPanel === 'recent' ? '0.4' : '0.2'}
11097
+ data-recent-panel-count-brightness={hoveredPanel === 'recent' ? '1.15' : '1'}
11098
+ style={{
11099
+ /* R589 — recent-signal panel count <text> root gains
11100
+ brightness(1.15) on hoveredPanel === 'recent'.
11101
+ 28th anchor in per-element brightness family.
11102
+ Sibling to R588 at the legend panel; closes
11103
+ panel-pair brightness symmetry at the header-
11104
+ count scope.
11105
+
11106
+ Panel-pair title↔count brightness parity now
11107
+ complete:
11108
+ legend title pinnedStatus gate (R571 family)
11109
+ legend count hoveredPanel gate (R588)
11110
+ recent title activeEdgeKey gate (R571 family)
11111
+ recent count hoveredPanel gate (R589) ← this round
11112
+
11113
+ All 4 panel-header text elements respond on the
11114
+ brightness axis — full symmetric closure.
11115
+
11116
+ Recent-signal panel count hover signature now
11117
+ 3 axes (mirrors R588 legend count closure):
11118
+ R311/R424 fontWeight 600 → 700
11119
+ R349/R566 letter-spacing 0.2 → 0.4
11120
+ R589 brightness 1 → 1.15 ← this round
11121
+
11122
+ Filter applied at <text> root (the parent here);
11123
+ the nested fw-changing tspan inherits the lift
11124
+ via SVG inheritance — unlike applying to tspan
11125
+ directly, which is unreliable cross-browser.
11126
+
11127
+ The R162 freshness-tinted fill (cyan/teal alpha
11128
+ 1.0 → 0.30 by ageSec) gets the +15% multiplied
11129
+ in — fresh data (alpha=1) reads dramatically
11130
+ brighter on hover; stale data (alpha=0.30) gets
11131
+ a proportionally smaller absolute lift. Hover
11132
+ brightness is freshness-amplifying at this
11133
+ surface — coherent with the panel's "freshness
11134
+ is the primary signal" semantic. */
11135
+ filter: hoveredPanel === 'recent' ? 'brightness(1.15)' : undefined,
11136
+ transition: 'letter-spacing 200ms ease-out, filter 200ms ease-out',
11137
+ }}
8747
11138
  >
8748
11139
  {/* Round 225 / Loop: tabular-nums on the panel-header
8749
11140
  flow-count tspan. The "{N} flows" string lives in
@@ -9208,7 +11599,39 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
9208
11599
  : 1}
9209
11600
  data-recent-row-tint={link.key}
9210
11601
  data-recent-row-tint-transition="200ms"
9211
- style={{ transition: 'fill 200ms ease-out, opacity 200ms ease-out' }}
11602
+ data-recent-row-tint-brightness={isRowActive ? '1.15' : '1'}
11603
+ /* R611 — recent-row tint rect brightness on
11604
+ hover/pin. Sibling to R610 group-label tint
11605
+ rect — closes panel-row tint rect brightness
11606
+ parity across both panel scopes:
11607
+ R610 group label tint rect (pin/hover-label)
11608
+ R611 recent-row tint rect (hover/pin row) ← this round
11609
+
11610
+ Same banked R582/R583 stacked-filter pattern
11611
+ (here plain brightness — no drop-shadow stack
11612
+ since the tint rect doesn't carry one). The
11613
+ pal.legendAccent cyan/teal fill (0.18-0.22 pin
11614
+ / 0.10-0.14 hover alpha) lifts +15% — small
11615
+ but perceivable lift that ties the row tint
11616
+ paint axis to its fill/opacity response on
11617
+ row inspection.
11618
+
11619
+ Pin/hover-gated brightness family at panel tier
11620
+ now 6 anchors:
11621
+ R571 group label text (pin)
11622
+ R587 group cluster box (pin/hover)
11623
+ R607 legend pin-ring (pin)
11624
+ R609 legend flow-arrow (panel-hover)
11625
+ R610 group label tint rect (pin/hover-label)
11626
+ R611 recent-row tint rect (hover/pin row) ← this round
11627
+
11628
+ transition list extends with 'filter 200ms
11629
+ ease-out' alongside the existing 200ms
11630
+ fill/opacity cadence. */
11631
+ style={{
11632
+ filter: isRowActive ? 'brightness(1.15)' : undefined,
11633
+ transition: 'fill 200ms ease-out, opacity 200ms ease-out, filter 200ms ease-out',
11634
+ }}
9212
11635
  />
9213
11636
  {/* Round 160 / Loop: recency pip. Canvas flow edges
9214
11637
  fade by freshness (R10: full intensity ≤30s →
@@ -9344,11 +11767,38 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
9344
11767
  data-recent-row-freshness-radius={(isRowHovered || isRowPinned) ? 2.5 : 2.0}
9345
11768
  data-recent-row-freshness-lifted={(isRowHovered || isRowPinned) ? 'true' : 'false'}
9346
11769
  data-recent-row-freshness-glow={alpha > 0.7 ? 'true' : 'false'}
11770
+ data-recent-row-freshness-brightness={alpha > 0.7 ? '1.15' : '1'}
9347
11771
  style={{
9348
11772
  pointerEvents: 'none',
9349
11773
  r: `${(isRowHovered || isRowPinned) ? 2.5 : 2.0}px`,
11774
+ /* R606 — recent-row freshness pip stacks
11775
+ brightness(1.15) onto R478's freshness-gated
11776
+ drop-shadow. Same alpha > 0.7 gate so both
11777
+ effects activate together when the signal
11778
+ is "live" (~30s window per R10 freshness
11779
+ ramp) and ease off in lockstep as data
11780
+ ages past ~45s.
11781
+
11782
+ Pip 4-axis freshness/hover signature now:
11783
+ R10 opacity tracks alpha (freshness)
11784
+ R447 r 2.0 → 2.5 (hover/pin)
11785
+ R478 drop-shadow on alpha > 0.7
11786
+ R606 brightness(1.15) on alpha > 0.7 ← this round
11787
+
11788
+ R478 + R606 share one filter chain on the
11789
+ same gate — banked R582/R583 stacked-filter
11790
+ pattern at the recent-row pip scope. The
11791
+ fresh signal reads ~15% brighter + glows
11792
+ cyan; the stale signal sits at flat
11793
+ legendAccent fill with no filter.
11794
+
11795
+ Hover brightness is freshness-gated here
11796
+ (vs hover-gated elsewhere) — matches the
11797
+ "this signal is live" semantic that R478
11798
+ established. data-recent-row-freshness-
11799
+ brightness attr exposes the gate for tests. */
9350
11800
  filter: alpha > 0.7
9351
- ? `drop-shadow(0 0 3px ${pal.legendAccent}80)`
11801
+ ? `drop-shadow(0 0 3px ${pal.legendAccent}80) brightness(1.15)`
9352
11802
  : undefined,
9353
11803
  transition: 'opacity 200ms ease-out, r 200ms ease-out, filter 200ms ease-out',
9354
11804
  } as React.CSSProperties}
@@ -9398,11 +11848,46 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
9398
11848
  tier without disturbing the surrounding family
9399
11849
  baseline. data-recent-row-text-font-weight attr
9400
11850
  exposes the value for tests. */
9401
- fontWeight="500"
11851
+ /* Round 530 / Loop — extends hover-fw family
11852
+ (R416/R420/R425/R520/R521/R522, 6 anchors) to
11853
+ a 7th anchor: recent-row alias text gains
11854
+ fontWeight 500 → 600 on (isRowHovered ||
11855
+ isRowPinned). Pre-R530 R363 set fw=500
11856
+ statically; hover/pin lifted other axes
11857
+ (R55 fill brighten / R434 letter-spacing
11858
+ 3-tier / R143 translateY / R104 row bg-
11859
+ tint / R474 cadence) but the fw stayed
11860
+ flat — same asymmetry R520 closed at the
11861
+ +N more footer.
11862
+ R530 mirrors R520's pattern at the row-
11863
+ text scope. Hover OR pin (isRowActive
11864
+ union) lifts fw to 600, matching the count
11865
+ tspan's cold-state tier (R320 fw=600), so
11866
+ on active state the alias label reads at
11867
+ the same data tier as the count it sits
11868
+ next to. Inner count tspan has its own
11869
+ explicit fontWeight (600 or 700 per R320/
11870
+ R445) so parent fw lift doesn't bleed
11871
+ (inheritance overridden).
11872
+ Hover-fw family extension (7 anchors):
11873
+ R416 chip-row count digit
11874
+ R420 chrome zoom-level
11875
+ R425 hub-center digit
11876
+ R520 +N more flows footer
11877
+ R521 chrome nodeSize S/M/L inactive
11878
+ R522 chrome layout Ring/Grid inactive
11879
+ R530 recent-row alias text ← this round
11880
+ transition list extends to include
11881
+ 'font-weight 200ms ease-out', matching the
11882
+ R474 cadence of the existing fill +
11883
+ letter-spacing axes on this element.
11884
+ data-recent-row-text-font-weight attr
11885
+ flips '500' → '600' on isRowActive. */
11886
+ fontWeight={(isRowHovered || isRowPinned) ? '600' : '500'}
9402
11887
  data-recent-row-text={link.key}
9403
11888
  data-recent-row-text-pinned={isRowPinned ? 'true' : 'false'}
9404
11889
  data-recent-row-text-hovered={!isRowPinned && isRowHovered ? 'true' : 'false'}
9405
- data-recent-row-text-font-weight="500"
11890
+ data-recent-row-text-font-weight={(isRowHovered || isRowPinned) ? '600' : '500'}
9406
11891
  /* Round 434 / Loop: recent-signal row text extends
9407
11892
  from R220's pin-only letter-spacing (0 → 0.5 on
9408
11893
  isRowPinned) to a 3-tier scale matching R433
@@ -9451,11 +11936,49 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
9451
11936
  spacing values unchanged; R363 fw + R55 fill
9452
11937
  brighten unchanged — only the timing axis
9453
11938
  shifts. */
11939
+ /* Round 568 / Loop — extends the drop-shadow visual-
11940
+ polish family (16 anchors after R559) to a 17th
11941
+ anchor at the recent-row text scope. Adds a soft
11942
+ pal.legendAccent glow on isRowHovered || isRow-
11943
+ Pinned, completing the row's hover/pin signature
11944
+ at 4 paint+typography axes:
11945
+ R55 fill brighten (fill)
11946
+ R434 letter-spacing 3-tier (typography kerning)
11947
+ R530 fontWeight 500 → 600 (typography weight)
11948
+ R568 drop-shadow glow (paint glow) ← this round
11949
+ Mirror of R550 panel-title pin-gated glow pattern,
11950
+ applied at the panel-ROW tier rather than panel-
11951
+ TITLE tier. Hue: pal.legendAccent + hex alpha 80
11952
+ (~50%) — same strength as R479 group-label /
11953
+ R550 panel-title glows for cross-element
11954
+ consistency. 2px blur (smaller than R478 pip's
11955
+ 3px since text is fontSize 9 and a heavier blur
11956
+ would bleed into adjacent row text); blur 2px
11957
+ keeps the glow tight to the row's text glyphs.
11958
+ transition list extends to include 'filter
11959
+ 200ms ease-out' matching the R474 200ms cadence
11960
+ of the existing 3 axes.
11961
+ data-recent-row-text-glow attr surfaces the
11962
+ gate for tests. */
9454
11963
  data-recent-row-text-transition="200ms"
11964
+ data-recent-row-text-glow={(isRowHovered || isRowPinned) ? 'true' : 'false'}
11965
+ /* Round 572 / Loop — per-element brightness family
11966
+ 9th anchor at recent-row text scope. Stacks
11967
+ brightness(1.15) onto R568's drop-shadow in
11968
+ one filter chain (same R564/R570/R571 pattern).
11969
+ Glyph BOTH glows (R568 drop-shadow halo) AND
11970
+ brightens (R572 inner lift) simultaneously.
11971
+ Cross-element brightness consistency: same +15%
11972
+ across alias / sub-text / edge-badge / group-
11973
+ label / and now recent-row text. */
11974
+ data-recent-row-text-brightness={(isRowHovered || isRowPinned) ? '1.15' : '1'}
9455
11975
  style={{
9456
- transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out',
11976
+ transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out, font-weight 200ms ease-out, filter 200ms ease-out',
9457
11977
  letterSpacing: isRowPinned ? '0.5px' :
9458
11978
  isRowHovered ? '0.25px' : '0px',
11979
+ filter: (isRowHovered || isRowPinned)
11980
+ ? `drop-shadow(0 0 2px ${pal.legendAccent}80) brightness(1.15)`
11981
+ : undefined,
9459
11982
  }}
9460
11983
  >
9461
11984
  {/* R138 / Loop: typography unification with the rest
@@ -9627,9 +12150,51 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
9627
12150
  data-recent-row-ts={link.key}
9628
12151
  data-recent-row-ts-alpha={tsAlpha.toFixed(2)}
9629
12152
  data-recent-row-ts-lifted={(isRowHovered || isRowPinned) ? 'true' : 'false'}
12153
+ data-recent-row-ts-brightness={(isRowHovered || isRowPinned) ? '1.15' : '1'}
9630
12154
  style={{
9631
12155
  pointerEvents: 'none',
9632
- transition: 'opacity 200ms ease-out',
12156
+ /* R591 recent-row timestamp gains filter
12157
+ brightness(1.15) on row hover/pin. 30th
12158
+ anchor in per-element brightness family.
12159
+ Closes recent-row 3-element brightness
12160
+ coverage:
12161
+ recent-row text (R572, parent <text>
12162
+ filter — count tspan
12163
+ inherits)
12164
+ recent-row ts (R591, sibling <text>
12165
+ — its own filter) ← this round
12166
+
12167
+ Symmetric with the legend-row R590 closure
12168
+ pattern: when a sibling text element can't
12169
+ inherit the row's brightness via ancestor
12170
+ filter, it needs its own filter. R590
12171
+ solved this for legend-count; R591 does
12172
+ the same for recent-ts.
12173
+
12174
+ Triple multiplicative interaction at this
12175
+ surface:
12176
+ opacity: tsAlpha (R191 freshness 1.0 →
12177
+ 0.25 by ageSec) → 1.0 on hover/pin
12178
+ (R484)
12179
+ fill: pal.legendText (neutral gray)
12180
+ brightness: 1.0 → 1.15 on hover/pin
12181
+
12182
+ Stale-data hover semantic: a stale row's
12183
+ timestamp at tsAlpha=0.25 gets opacity 0.25
12184
+ × brightness 1.0 at rest, but jumps to
12185
+ opacity 1.0 × brightness 1.15 on hover —
12186
+ revealing the dim metadata at maximum
12187
+ legibility under attention. Hover brightness
12188
+ is freshness-overriding at this surface,
12189
+ same as R589's freshness-amplifying behavior
12190
+ at the recent-panel count above.
12191
+
12192
+ data-recent-row-ts-brightness attr exposes
12193
+ the gate for tests. */
12194
+ filter: (isRowHovered || isRowPinned)
12195
+ ? 'brightness(1.15)'
12196
+ : undefined,
12197
+ transition: 'opacity 200ms ease-out, filter 200ms ease-out',
9633
12198
  fontVariantNumeric: 'tabular-nums',
9634
12199
  }}
9635
12200
  >
@@ -9803,6 +12368,39 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
9803
12368
  stays as is, so the rest-vs-hover delta still
9804
12369
  reads clearly. data-recent-panel-more-font-weight
9805
12370
  attr exposes the value for tests. */}
12371
+ {/* Round 520 / Loop — extends the `+N more flows` footer
12372
+ to a 5-axis hover signature by adding fontWeight
12373
+ 500 → 600 on hover. Pre-R520 the footer carried 4
12374
+ hover axes:
12375
+ R195 fill legendText → legendAccent
12376
+ R325 letter-spacing 0.2 → 0.3px (R344 tween)
12377
+ R325 opacity 0.55 → 0.85
12378
+ R133 underline none → underline
12379
+ R368 had set fontWeight 500 statically as a sibling
12380
+ to R363/R364/R366 small-text fw lift family — but
12381
+ the footer's hover state didn't carry a fontWeight
12382
+ DELTA the way other interactive surfaces do (chip-
12383
+ row counts R416, chrome zoom-level R420, hub digit
12384
+ R425). R520 adds the missing weight axis: fw 500
12385
+ → 600 on hover, so the footer reads "thickening AND
12386
+ lighting up" under cursor — same idiom as the
12387
+ chrome zoom-level R420 / chip-row digit R416 hover-
12388
+ bold pattern, applied at the panel nav-action
12389
+ surface.
12390
+ data-recent-panel-more-font-weight attr value
12391
+ flips from '500' → '600' on hover (was static
12392
+ '500' pre-R520).
12393
+ Bonus closure — R475 panel-text cadence: pre-R520
12394
+ the footer's transition list had `opacity 150ms`
12395
+ while R475 unified panel-text transitions at
12396
+ 200ms. R518 closed the same gap at legend-count.
12397
+ R520 closes the LAST panel-text 150ms holdout
12398
+ here AND adds the new font-weight 200ms axis. All
12399
+ 4 transition properties (opacity / fill / letter-
12400
+ spacing / font-weight) now uniform 200ms at the
12401
+ footer — same cadence as legend-label / legend-
12402
+ count / recent-row alias / recent-row count /
12403
+ group-label. */}
9806
12404
  <text
9807
12405
  x="115" y="82"
9808
12406
  textAnchor="middle"
@@ -9810,14 +12408,49 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
9810
12408
  fontSize="9"
9811
12409
  fontFamily="monospace"
9812
12410
  fontStyle="italic"
9813
- fontWeight="500"
12411
+ fontWeight={hoveredRecentMore ? '600' : '500'}
9814
12412
  letterSpacing={hoveredRecentMore ? '0.3' : '0.2'}
9815
12413
  opacity={hoveredRecentMore ? 0.85 : 0.55}
9816
12414
  textDecoration={hoveredRecentMore ? 'underline' : 'none'}
9817
12415
  data-recent-panel-more={moreCount}
9818
12416
  data-recent-panel-more-hovered={hoveredRecentMore ? 'true' : 'false'}
9819
- data-recent-panel-more-font-weight="500"
9820
- style={{ transition: 'opacity 150ms ease-out, fill 200ms ease-out, letter-spacing 200ms ease-out' }}
12417
+ data-recent-panel-more-font-weight={hoveredRecentMore ? '600' : '500'}
12418
+ data-recent-panel-more-transition="200ms"
12419
+ data-recent-panel-more-brightness={hoveredRecentMore ? '1.15' : '1'}
12420
+ style={{
12421
+ /* R592 — +N more flows footer gains filter
12422
+ brightness(1.15) on hover. 31st anchor in
12423
+ per-element brightness family. Closes the
12424
+ footer's hover signature at 6 axes — the
12425
+ densest hover signature on any topology
12426
+ surface:
12427
+ R195 fill legendText → legendAccent
12428
+ R325 letter-spacing 0.2 → 0.3 (R344 tween)
12429
+ R325 opacity 0.55 → 0.85
12430
+ R133 underline none → underline
12431
+ R520 fontWeight 500 → 600
12432
+ R592 brightness 1 → 1.15 ← this round
12433
+
12434
+ The footer is the recent-signal panel's
12435
+ primary nav affordance into /messages — when
12436
+ user hovers it, EVERYTHING about it shifts:
12437
+ color (cyan), size (fw), spacing (ls), opacity
12438
+ (0.85), decoration (underline), brightness
12439
+ (+15%). 6-axis hover signature reads as "this
12440
+ is the most actionable thing on the panel —
12441
+ click me".
12442
+
12443
+ Triple-paint multiplicative interaction:
12444
+ opacity 0.85 × cyan fill × brightness(1.15)
12445
+ — the cyan reads dramatically brighter than
12446
+ a plain fill swap would.
12447
+
12448
+ Existing transition list extends with 'filter
12449
+ 200ms ease-out' matching the existing 200ms
12450
+ cadence across all 5 other axes. */
12451
+ filter: hoveredRecentMore ? 'brightness(1.15)' : undefined,
12452
+ transition: 'opacity 200ms ease-out, fill 200ms ease-out, letter-spacing 200ms ease-out, font-weight 200ms ease-out, filter 200ms ease-out',
12453
+ }}
9821
12454
  >
9822
12455
  {`+ ${moreCount}`}
9823
12456
  <tspan opacity="0.7" data-recent-panel-more-unit>{` more flow${moreCount === 1 ? '' : 's'}`}</tspan>
@@ -9924,7 +12557,20 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
9924
12557
  data-legend-panel-title-fw + -active exposed for tests. */}
9925
12558
  {/* R345 sibling — legend panel title same hover letter-
9926
12559
  spacing tween 0.3 → 0.4 on panel hover. */}
9927
- <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>
12560
+ {/* Round 550 sibling legend-panel header title mirrors
12561
+ the recent-panel title above: drop-shadow glow on
12562
+ pin-gated active state (pinnedStatus). Same hue
12563
+ (pal.legendAccent + hex alpha 80), same 2px blur,
12564
+ same 200ms ease-out cadence. Family lifts to 15
12565
+ anchors with this sibling (counted as R550-sibling
12566
+ for accounting parity with R532-R536 hub-cluster
12567
+ glow quintet pattern — two co-shipping anchors
12568
+ under a single round number).
12569
+ data-legend-panel-title-glow attr added. */}
12570
+ {/* Round 573 sibling — legend panel-title 12th anchor.
12571
+ Same stacked filter pattern at the legend-panel-title
12572
+ scope. Both panel titles now lift in lockstep. */}
12573
+ <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>
9928
12574
  {/* Round 257 / Loop: legend panel header count picks up the
9929
12575
  symmetric 13L/13R inner-padding pattern from the recent-
9930
12576
  signal panel. Pre-R257 the legend header was 13px from
@@ -10001,11 +12647,42 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10001
12647
  // title 0.3. Pairs with the recent-signal panel count
10002
12648
  // letter-spacing above so the two corner panels' header
10003
12649
  // typography stays editorially symmetric.
10004
- letterSpacing="0.2"
12650
+ /* Round 566 / Loop — legend panel-count gains hover-state
12651
+ letter-spacing tween (0.2 → 0.4 on hoveredPanel ===
12652
+ 'legend'). Pairs with existing R310 fw 600→700 on the
12653
+ same gate — count now has 2-axis hover signature (fw
12654
+ + ls), matching the panel title's R345 ls + R482 fw
12655
+ lift pattern at the panel-header data-tspan scope.
12656
+ Hover-letter-spacing family extension (R566 = 2 sibling
12657
+ anchors at recent + legend panel-count). */
12658
+ letterSpacing={hoveredPanel === 'legend' ? '0.4' : '0.2'}
10005
12659
  data-legend-panel-count
10006
- data-legend-panel-count-letter-spacing="0.2"
12660
+ data-legend-panel-count-letter-spacing={hoveredPanel === 'legend' ? '0.4' : '0.2'}
12661
+ data-legend-panel-count-brightness={hoveredPanel === 'legend' ? '1.15' : '1'}
10007
12662
  style={{
10008
- transition: 'fill 200ms ease-out, font-weight 200ms ease-out',
12663
+ /* R588 legend panel count gains brightness(1.15) on
12664
+ panel hover. 27th anchor in per-element brightness
12665
+ family. Closes title↔count parity at the legend
12666
+ panel header — the title already has brightness
12667
+ (R571/R567 lineage on pinnedStatus gate at line
12668
+ ~11926); the count now matches at the broader
12669
+ hoveredPanel gate.
12670
+
12671
+ Legend panel count hover signature now 3 axes:
12672
+ R310/R424 fontWeight 600 → 700
12673
+ R349/R566 letter-spacing 0.2 → 0.4
12674
+ R588 brightness 1 → 1.15 ← this round
12675
+
12676
+ The cyan numeral (pal.legendAccent) lifts ~15%
12677
+ alongside the typographic tightening — 3-axis
12678
+ panel-count hover signal at full visual presence.
12679
+
12680
+ Pure paint filter on <text> root element (NOT a
12681
+ nested tspan — SVG filter on tspan is unreliable
12682
+ cross-browser; the parent <text> takes the filter
12683
+ and inherits to children). */
12684
+ filter: hoveredPanel === 'legend' ? 'brightness(1.15)' : undefined,
12685
+ transition: 'fill 200ms ease-out, font-weight 200ms ease-out, letter-spacing 200ms ease-out, filter 200ms ease-out',
10009
12686
  fontVariantNumeric: 'tabular-nums',
10010
12687
  }}
10011
12688
  >{sessions.length}<tspan opacity="0.7" data-legend-panel-count-unit> node{sessions.length === 1 ? '' : 's'}</tspan></text>
@@ -10082,6 +12759,34 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10082
12759
  const isPinned = pinnedStatus === row.key;
10083
12760
  const isRowHovered = hoveredStatus === row.key;
10084
12761
  const isLifted = isRowHovered || isPinned;
12762
+ /* Round 562 / Loop — inspection-overrides-encoding family
12763
+ 5th anchor at the legend-swatch scope. When operator
12764
+ hovers a NODE ALIAS on the canvas, the legend swatch
12765
+ that matches that node's status tier lifts r + drop-
12766
+ shadow (R197 + R537 axes) — telegraphing "your
12767
+ inspected node is in this status group".
12768
+ Mirror of R486 minimap-dot inspection-override at the
12769
+ legend-swatch scope.
12770
+ Family progression (5 anchors):
12771
+ R484 recent-row timestamp on alias hover
12772
+ R485 edge particle opacity on alias hover
12773
+ R486 minimap dot opacity to 1.0 on alias hover
12774
+ R561 group-label opacity-1 + ants-gate refinement
12775
+ R562 legend-swatch r+glow on member-alias-matching ← this round
12776
+ Restraint: ONLY swatch lifts, NOT label/fill/ls/fw
12777
+ axes. Direct row-hover gets full treatment; inspection
12778
+ signal gets swatch-only lift — distinct "lighter"
12779
+ visual register matching R561's ants-gate approach
12780
+ (indirect inspection ≠ direct attention). */
12781
+ const hoveredSession = hoveredAlias
12782
+ ? (onlineNodes.find(s => s.alias === hoveredAlias) ?? offlineNodes.find(s => s.alias === hoveredAlias))
12783
+ : null;
12784
+ const hoveredAliasRowKey: 'working' | 'idle' | 'offline' | null = !hoveredSession ? null
12785
+ : hoveredSession.status === 'working' ? 'working'
12786
+ : offlineNodes.includes(hoveredSession) ? 'offline'
12787
+ : 'idle';
12788
+ const isMemberAliasMatching = hoveredAliasRowKey === row.key;
12789
+ const isSwatchLifted = isLifted || isMemberAliasMatching;
10085
12790
  return (
10086
12791
  <g
10087
12792
  key={row.key}
@@ -10184,7 +12889,38 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10184
12889
  : 1}
10185
12890
  data-legend-row-tinted={isPinned ? 'pinned' : hoveredStatus === row.key ? 'hover' : 'none'}
10186
12891
  data-legend-row-tint-transition="200ms"
10187
- style={{ transition: 'fill 200ms ease-out, opacity 200ms ease-out' }}
12892
+ data-legend-row-tint-brightness={(hoveredStatus === row.key || isPinned) ? '1.15' : '1'}
12893
+ /* R612 — legend-row tint rect brightness on hover/
12894
+ pin. Sibling to R610 group-label tint + R611
12895
+ recent-row tint — closes the PANEL-ROW TINT RECT
12896
+ BRIGHTNESS TRIO at full parity:
12897
+ R610 group label tint rect (pin/hover-label)
12898
+ R611 recent-row tint rect (hover/pin row)
12899
+ R612 legend-row tint rect (hover/pin row) ← this round
12900
+
12901
+ All 3 panel-row tint rects (group / recent /
12902
+ legend) now lift +15% brightness alongside their
12903
+ fill/opacity response under inspection.
12904
+
12905
+ Note: row.fill here is the TIER COLOR (green for
12906
+ working / teal for idle / slate for offline)
12907
+ rather than the cyan pal.legendAccent used by
12908
+ group + recent panels. Each tier brightens its
12909
+ OWN hue by +15% — the row reads as "this status
12910
+ lit up" with the row's status-identifying color
12911
+ intensified. Per-tier brightness lift carries
12912
+ semantic information at the legend scope.
12913
+
12914
+ Pin/hover-gated brightness family at panel tier
12915
+ now 7 anchors (R571/R587/R607/R609/R610/R611/
12916
+ R612). transition list extends with 'filter
12917
+ 200ms ease-out' alongside fill/opacity. */
12918
+ style={{
12919
+ filter: (hoveredStatus === row.key || isPinned)
12920
+ ? 'brightness(1.15)'
12921
+ : undefined,
12922
+ transition: 'fill 200ms ease-out, opacity 200ms ease-out, filter 200ms ease-out',
12923
+ }}
10188
12924
  />
10189
12925
  {/* Round 197 / Loop: swatch dot scales r 5.5 → 7 when its
10190
12926
  row is hovered or pinned. Pre-R197 the swatch was a
@@ -10213,15 +12949,50 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10213
12949
  R181 pin ring (6 + 0 stroke vs 8 - 0.75 inner
10214
12950
  ≈ 7.25). data-legend-swatch is unchanged so
10215
12951
  R197 / R55 / R61 tests probe the same handle. */}
12952
+ {/* Round 537 / Loop — extends drop-shadow visual-polish
12953
+ family (12 anchors after R536) to a 13th anchor: the
12954
+ legend swatch gains drop-shadow glow on hover/pin
12955
+ using its OWN row fill color (working green / idle
12956
+ teal / offline slate). Pre-R537 the swatch lifted
12957
+ only r (R197/R295 6 → 7) on attention — geometry
12958
+ axis only, no paint glow. R537 adds the paint axis,
12959
+ composing with R181/R402 pin-ring (separate concen-
12960
+ tric circle in the same row.fill color) so on
12961
+ hover/pin the SWATCH AND its pin-ring both contri-
12962
+ bute to a unified tier-coloured glow signature.
12963
+ Hue: row.fill (status hex) concatenated with `99`
12964
+ hex alpha (~60%). Working green / idle teal /
12965
+ offline slate each glow in their OWN tier color
12966
+ — the legend acts as a color-keyed status mirror.
12967
+ 3px blur reads soft; 60% alpha legible without
12968
+ overwhelming the swatch's own paint.
12969
+ Drop-shadow visual-polish family extension (13
12970
+ anchors). filter is paint-only; bbox unchanged.
12971
+ transition list extends to include 'filter 150ms
12972
+ ease-out', matching the existing R197 r 150ms
12973
+ cadence at this swatch. data-legend-swatch-glow
12974
+ attr exposes the gate state for tests. */}
10216
12975
  <circle
10217
12976
  cx="16" cy={row.y0}
10218
12977
  r="6"
10219
12978
  fill={row.fill}
10220
12979
  data-legend-swatch={row.key}
10221
- data-legend-swatch-state={isPinned ? 'pinned' : isRowHovered ? 'hover' : 'idle'}
12980
+ data-legend-swatch-state={isPinned ? 'pinned' : isRowHovered ? 'hover' : isMemberAliasMatching ? 'member-alias-matching' : 'idle'}
12981
+ data-legend-swatch-glow={isSwatchLifted ? 'true' : 'false'}
12982
+ data-legend-swatch-member-alias-matching={isMemberAliasMatching ? 'true' : 'false'}
12983
+ /* Round 578 — legend swatch joins per-element brightness
12984
+ family at 16th anchor. Stacks brightness(1.15) onto
12985
+ R537 drop-shadow. Closes chip-row tier-color glow
12986
+ trio at consistent stacked-filter pattern alongside
12987
+ R542 pressure-seg (already stacks brightness 1.2)
12988
+ and the sibling vendor chip (R578-sibling). */
12989
+ data-legend-swatch-brightness={isSwatchLifted ? '1.15' : '1'}
10222
12990
  style={{
10223
- r: isRowHovered || isPinned ? '7px' : '6px',
10224
- transition: 'r 150ms ease-out',
12991
+ r: isSwatchLifted ? '7px' : '6px',
12992
+ filter: isSwatchLifted
12993
+ ? `drop-shadow(0 0 3px ${row.fill}99) brightness(1.15)`
12994
+ : undefined,
12995
+ transition: 'r 150ms ease-out, filter 150ms ease-out',
10225
12996
  } as React.CSSProperties}
10226
12997
  />
10227
12998
  {/* R61 pinned-state ring — concentric stroke at r=8 in
@@ -10303,10 +13074,30 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10303
13074
  data-legend-pin-ring-pinned={isPinned ? 'true' : 'false'}
10304
13075
  data-legend-pin-ring-stroke-width="1.75"
10305
13076
  data-legend-pin-ring-glow={isPinned ? 'true' : 'false'}
13077
+ data-legend-pin-ring-brightness={isPinned ? '1.15' : '1'}
10306
13078
  style={{
10307
13079
  pointerEvents: 'none',
13080
+ /* R607 — legend pin-ring stacks brightness(1.15)
13081
+ onto R477's pin-gated drop-shadow. Extends the
13082
+ pin-gated brightness family to a 3rd anchor:
13083
+ R571 group label text (pin-gated)
13084
+ R587 group cluster box (pin-gated)
13085
+ R607 legend pin-ring (pin-gated) ← this round
13086
+ Same banked R582/R583 stacked-filter pattern.
13087
+ Pin-ring 3-axis pin signature now:
13088
+ R181 opacity 0 → 1
13089
+ R477 drop-shadow → row.fill 88 alpha
13090
+ R607 brightness 1 → 1.15 ← this round
13091
+ The row.fill tier color (green working / teal
13092
+ idle / slate offline) brightens ~15% alongside
13093
+ the drop-shadow halo — the pin-ring reads as
13094
+ "lit + locked" instead of just "locked". The
13095
+ color signal (which tier you pinned) reads more
13096
+ vividly under the brightness lift.
13097
+ Existing 'filter 200ms ease-out' transition
13098
+ covers brightness ease at the same cadence. */
10308
13099
  filter: isPinned
10309
- ? `drop-shadow(0 0 3px ${row.fill}88)`
13100
+ ? `drop-shadow(0 0 3px ${row.fill}88) brightness(1.15)`
10310
13101
  : undefined,
10311
13102
  transition: 'opacity 150ms ease-out, filter 200ms ease-out',
10312
13103
  }}
@@ -10353,11 +13144,42 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10353
13144
  the value for tests. R219 letter-spacing pin
10354
13145
  tween + R55 fill transition + R181 always-mount
10355
13146
  pin ring all preserved. */
10356
- fontWeight="500"
13147
+ /* Round 531 / Loop — extends hover-fw family (R416/
13148
+ R420/R425/R520/R521/R522/R530, 7 anchors) to an
13149
+ 8th anchor at the legend-row label. Pre-R531
13150
+ R364 set fw=500 statically; hover/pin lifted
13151
+ other axes (R55 fill brighten / R433 letter-
13152
+ spacing 3-tier / R181 pin ring) but the fw
13153
+ stayed flat. R531 mirrors R530's recent-row
13154
+ alias pattern at the legend-row label scope.
13155
+ Hover OR pin (hoveredStatus===row.key ||
13156
+ isPinned) lifts fw to 600, matching the
13157
+ legend-row count tier (R309 fw=600 / R446
13158
+ pin lift 600→700). Active label now reads at
13159
+ the count's data tier — sibling treatment to
13160
+ R530 recent-row.
13161
+ Hover-fw family extension (8 anchors):
13162
+ R416 chip-row count digit
13163
+ R420 chrome zoom-level
13164
+ R425 hub-center digit
13165
+ R520 +N more flows footer
13166
+ R521 chrome nodeSize S/M/L inactive
13167
+ R522 chrome layout Ring/Grid inactive
13168
+ R530 recent-row alias text
13169
+ R531 legend-row label ← this round
13170
+ Two panel-row label surfaces (R530 recent-
13171
+ row alias + R531 legend-row label) now have
13172
+ parallel hover-fw signatures. R475 cadence
13173
+ at 200ms already covers font-weight via the
13174
+ existing transition list extension at this
13175
+ element. data-legend-row-label-font-weight
13176
+ attr flips '500' → '600' on isActive (was
13177
+ static '500' pre-R531). */
13178
+ fontWeight={(hoveredStatus === row.key || isPinned) ? '600' : '500'}
10357
13179
  data-legend-row-label={row.key}
10358
13180
  data-legend-row-label-pinned={isPinned ? 'true' : 'false'}
10359
13181
  data-legend-row-label-hovered={!isPinned && hoveredStatus === row.key ? 'true' : 'false'}
10360
- data-legend-row-label-font-weight="500"
13182
+ data-legend-row-label-font-weight={(hoveredStatus === row.key || isPinned) ? '600' : '500'}
10361
13183
  /* Round 433 / Loop: legend-row text extends from
10362
13184
  R219's pin-only letter-spacing (0px → 0.5px on
10363
13185
  isPinned) to a 3-tier scale matching the R432
@@ -10399,10 +13221,46 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10399
13221
  unchanged; R55 fill brighten unchanged — only
10400
13222
  the timing axis shifts. */
10401
13223
  data-legend-row-label-transition="200ms"
13224
+ /* Round 569 / Loop — extends R568's panel-row drop-
13225
+ shadow pattern to the SIBLING legend-row label.
13226
+ Symmetric closure of the panel-row drop-shadow
13227
+ across both panel surfaces (recent + legend).
13228
+ Pre-R569 the legend-row label had 3 hover/pin
13229
+ axes (R55 fill + R433 ls 3-tier + R531 fw); R569
13230
+ adds the 4th paint axis to match R568 recent-
13231
+ row text exactly.
13232
+ Two-tier paint-axis cascade now SYMMETRIC across
13233
+ both side panels:
13234
+ recent panel title (R550) + row text (R568)
13235
+ legend panel title (R550-sibling) + label (R569) ← this round
13236
+ Each panel has glow at BOTH chrome tiers (title
13237
+ active + row hover/pin). The 4-axis row signature
13238
+ (fill + ls + fw + glow) is now identical at both
13239
+ panel-row text scopes — completes the panel-row
13240
+ text-treatment parity.
13241
+ Hue/blur/cadence: same as R568 (pal.legendAccent
13242
+ + hex alpha 80, 2px blur, 200ms ease-out). Gate
13243
+ matches R531/R433 (hoveredStatus === row.key ||
13244
+ isPinned) — single boolean drives all 4 axes
13245
+ together for motion-coherent state-flip.
13246
+ Drop-shadow family extends to 18 anchors total
13247
+ (R568 was 17; R569 = 18).
13248
+ data-legend-row-label-glow attr added for tests. */
13249
+ data-legend-row-label-glow={(hoveredStatus === row.key || isPinned) ? 'true' : 'false'}
13250
+ /* Round 572 / Loop — sibling to recent-row text above.
13251
+ Stacks brightness(1.15) onto R569's drop-shadow at
13252
+ legend-row label scope (10th anchor in per-element
13253
+ brightness family). Matches recent-row text 4-axis
13254
+ signature exactly: fill + ls + fw + drop-shadow +
13255
+ brightness now BOTH panel-row text surfaces. */
13256
+ data-legend-row-label-brightness={(hoveredStatus === row.key || isPinned) ? '1.15' : '1'}
10402
13257
  style={{
10403
- transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out',
13258
+ transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out, font-weight 200ms ease-out, filter 200ms ease-out',
10404
13259
  letterSpacing: isPinned ? '0.5px' :
10405
13260
  hoveredStatus === row.key ? '0.25px' : '0px',
13261
+ filter: (hoveredStatus === row.key || isPinned)
13262
+ ? `drop-shadow(0 0 2px ${pal.legendAccent}80) brightness(1.15)`
13263
+ : undefined,
10406
13264
  }}
10407
13265
  >{row.label}</text>
10408
13266
  {/* R95: live count anchored to the right edge of the
@@ -10534,7 +13392,74 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10534
13392
  data-legend-count-pinned={isPinned ? 'true' : 'false'}
10535
13393
  data-legend-count-font-weight={isPinned ? '700' : '600'}
10536
13394
  data-legend-count-fill={row.count > 0 && (hoveredStatus === row.key || isPinned) ? 'tier' : 'neutral'}
10537
- style={{ pointerEvents: 'none', transition: 'opacity 150ms ease-out, fill 150ms ease-out, font-weight 150ms ease-out', fontVariantNumeric: 'tabular-nums' }}
13395
+ /* Round 518 / Loop extends R433's 3-tier hover-
13396
+ letter-spacing tween from the legend-row LABEL
13397
+ (text at x=30) to the SIBLING legend-row COUNT
13398
+ digit (this text at x=215). Pre-R518 the row's
13399
+ label spread on hover/pin (R433: 0/0.25/0.5px)
13400
+ while the count digit at the row's right edge
13401
+ stayed dead-typographic — same row, two halves,
13402
+ asymmetric kerning gesture. R518 mirrors the
13403
+ 3-tier scale at the count so the WHOLE row's
13404
+ typography reads as one unit under cursor: label
13405
+ + count spread together at matching values.
13406
+ Tabular-nums (R225) makes the kerning still
13407
+ visible on 2-digit counts — each digit cell
13408
+ keeps its fixed width, but the inter-digit
13409
+ advance grows. R518 also closes R475's panel-
13410
+ row TEXT cadence at the count surface — R475
13411
+ lifted the label text transitions to 200ms but
13412
+ the count was missed; R518 lifts opacity / fill
13413
+ / font-weight from 150 → 200ms AND adds the new
13414
+ letter-spacing axis at 200ms. One transition
13415
+ list, one cadence, one motion-coherent multi-
13416
+ axis hover/pin signature across the row.
13417
+ Hover-letter-spacing family extension (10
13418
+ anchors now): R344/R345/R347/R420/R427/R431/
13419
+ R432/R433/R517/R518. R518 closes the legend-
13420
+ row pair (label R433 + count R518). data-
13421
+ legend-count-letter-spacing attr exposes the
13422
+ resolved value for tests. */
13423
+ data-legend-count-letter-spacing={isPinned ? '0.5px' : hoveredStatus === row.key ? '0.25px' : '0px'}
13424
+ data-legend-count-transition="200ms"
13425
+ /* R590 — legend-row count gains filter brightness(1.15)
13426
+ on hover/pin. 29th anchor in per-element brightness
13427
+ family. Closes legend-row label↔count brightness
13428
+ parity within each row:
13429
+ legend label (R572, 10th anchor — stacks w/ DS)
13430
+ legend count (R590 ← this round, plain brightness)
13431
+ Plain brightness (no DS stack) because the count
13432
+ has its own multi-axis hover signature already
13433
+ (opacity 0.65 → 1.0, fill neutral → row.fill tier
13434
+ color, fw 600 → 700 on pin, letter-spacing 3-tier)
13435
+ — adding brightness amplifies the tier-color fill
13436
+ to its hover-locked brightest possible.
13437
+ Triple multiplicative interaction: opacity 1.0 ×
13438
+ tier-color fill × brightness(1.15) — the count
13439
+ reads as confidently "this is the active tier".
13440
+ Existing transition list extends with 'filter
13441
+ 200ms ease-out' matching the existing 200ms
13442
+ cadence across opacity / fill / fw / letter-
13443
+ spacing.
13444
+ Legend-row scope now has parity with recent-row:
13445
+ both row text and count surfaces brighten +15%
13446
+ on row inspection. Recent-row count INHERITS its
13447
+ brightness via the parent <text>'s R572 filter
13448
+ (single ancestor filter covers all child tspans);
13449
+ legend-row count is a sibling <text> so it needs
13450
+ its OWN filter — R590 supplies that.
13451
+ data-legend-count-brightness attr exposes gate. */
13452
+ data-legend-count-brightness={(hoveredStatus === row.key || isPinned) ? '1.15' : '1'}
13453
+ style={{
13454
+ pointerEvents: 'none',
13455
+ filter: (hoveredStatus === row.key || isPinned)
13456
+ ? 'brightness(1.15)'
13457
+ : undefined,
13458
+ transition: 'opacity 200ms ease-out, fill 200ms ease-out, font-weight 200ms ease-out, letter-spacing 200ms ease-out, filter 200ms ease-out',
13459
+ fontVariantNumeric: 'tabular-nums',
13460
+ letterSpacing: isPinned ? '0.5px' :
13461
+ hoveredStatus === row.key ? '0.25px' : '0px',
13462
+ }}
10538
13463
  >{row.count}</text>
10539
13464
  </g>
10540
13465
  );
@@ -10553,7 +13478,43 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10553
13478
  logically tied to the row it demonstrates; control point
10554
13479
  proportionally shifts so apex stays mid-arc between
10555
13480
  rows 2 and 3. */}
10556
- <path d="M140,68 Q164,44 196,68" fill="none" stroke={pal.flowEdge} strokeWidth="3" markerEnd="url(#topo-arrow)" data-legend-flow-arrow style={{ pointerEvents: 'none', transition: 'stroke 200ms ease-out' }} />
13481
+ <path
13482
+ d="M140,68 Q164,44 196,68"
13483
+ fill="none"
13484
+ stroke={pal.flowEdge}
13485
+ strokeWidth="3"
13486
+ markerEnd="url(#topo-arrow)"
13487
+ data-legend-flow-arrow
13488
+ data-legend-flow-arrow-glow={hoveredPanel === 'legend' ? 'true' : 'false'}
13489
+ data-legend-flow-arrow-brightness={hoveredPanel === 'legend' ? '1.15' : '1'}
13490
+ style={{
13491
+ pointerEvents: 'none',
13492
+ /* R609 — legend flow-arrow gains stacked drop-shadow +
13493
+ brightness on hoveredPanel === 'legend'. Banked
13494
+ R582/R583 stacked-filter pattern at the legend
13495
+ decoration scope.
13496
+
13497
+ The arrow is the legend's "what an edge flow looks
13498
+ like" representation — when user hovers the legend
13499
+ panel to inspect rows, the demo arrow lights up
13500
+ too, tying it visually to the panel-wide hover
13501
+ gesture. Same pal.flowEdge cyan/teal hue as live
13502
+ edges, so the demo and the real edges share a
13503
+ coherent visual vocabulary.
13504
+
13505
+ Pure paint axis: drop-shadow halo + brightness
13506
+ stack on the same panel-hover gate. transition
13507
+ list extends with 'filter 200ms ease-out'
13508
+ alongside the existing stroke 200ms cadence.
13509
+
13510
+ data-legend-flow-arrow-glow + -brightness attrs
13511
+ expose the gate for tests. */
13512
+ filter: hoveredPanel === 'legend'
13513
+ ? `drop-shadow(0 0 3px ${pal.flowEdge}80) brightness(1.15)`
13514
+ : undefined,
13515
+ transition: 'stroke 200ms ease-out, filter 200ms ease-out',
13516
+ }}
13517
+ />
10557
13518
  </g>
10558
13519
 
10559
13520
  {/* Round 282 / Loop: sleep2agi brand watermark per Vincent
@@ -10593,6 +13554,74 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10593
13554
  spacing as typographic intent. Stays well inside the
10594
13555
  bottom-left corner; opacity 0.4 unchanged so the
10595
13556
  watermark stays a watermark. */}
13557
+ {/* Round 519 / Loop — 呼吸感 family 3rd anchor. Pre-R519 the
13558
+ breath family had 2 anchors (R497 hub idle digit + R498
13559
+ recent-row hot pulse). Both signal active state — the
13560
+ digit when canvas is idle (no work pending), the recent
13561
+ row when fresh signal arrives. R519 adds a SLOW ambient
13562
+ breath to the brand watermark — present always, not gated
13563
+ on activity state. The watermark IS the canvas-corner
13564
+ register that says "the canvas is alive even when nothing
13565
+ is happening"; a 6s opacity pulse around its 0.4 mean
13566
+ (±0.08 swing → 0.32 ↔ 0.48) reads as ambient liveness
13567
+ rather than foreground signal.
13568
+ Why 6s (not R497's 4s): the breath family now spans
13569
+ activity registers (R497 4s — idle-focal: present and
13570
+ waiting; R498 ~3s — hot signal: just arrived) and now
13571
+ ambient register (R519 6s — corner watermark: always-on
13572
+ background). Slower cadence keeps the watermark in the
13573
+ background; ~10 pct slower than R497 keeps it out of
13574
+ phase so the two anchors never beat together visibly.
13575
+ Gate: !reducedMotion. Inside the prefers-reduced-motion
13576
+ media query, SMIL animate isn't covered by globals.css
13577
+ R29 (which only kills CSS animation property), so we
13578
+ gate at JSX level — when reducedMotion is true the
13579
+ <animate> child isn't mounted and opacity stays at the
13580
+ static 0.4. data-topo-brand-watermark-breath attr
13581
+ exposes the gate state for tests.
13582
+ 呼吸感 family extension (3 anchors): R497 hub idle / R498
13583
+ recent-row hot / R519 brand watermark ambient. */}
13584
+ {/* Round 525 / Loop — focal-recede family 3rd anchor. R507
13585
+ receded the hub-center workingCount digit; R508 receded
13586
+ the hub-highlight disc; both fade to 0.85× when any non-
13587
+ hub canvas surface is hovered (alias / edge / group /
13588
+ status / vendor) — the "you're inspecting elsewhere"
13589
+ gesture. R525 extends the pattern to the brand watermark
13590
+ at canvas bottom-left, the always-on decorative brand
13591
+ element. Pre-R525 the watermark stayed at its R519
13592
+ breath baseline (0.32-0.48 SMIL pulse) regardless of
13593
+ canvas attention; post-R525 it fades to 70% wrapper
13594
+ opacity (effective 0.224-0.336 with breath) when canvas
13595
+ attention is elsewhere, matching the same focal-recede
13596
+ semantic R507/R508 establish at the hub focal cluster.
13597
+ Implementation: wrap the existing <text> in a <g>
13598
+ wrapper whose opacity multiplies with the inner text's
13599
+ SMIL-animated opacity. SVG opacity composes
13600
+ multiplicatively across the parent/child chain, so:
13601
+ normal: g.opacity=1.0 × text.opacity(SMIL 0.32-0.48) = 0.32-0.48
13602
+ recede: g.opacity=0.7 × text.opacity(SMIL 0.32-0.48) = 0.224-0.336
13603
+ SMIL on inner text continues running through both
13604
+ states; only the wrapper opacity flips. 300ms ease-out
13605
+ transition on wrapper (matches R508 hub-highlight recede
13606
+ transition).
13607
+ Gate matches R507/R508 — focal-recede is a UNIFIED
13608
+ non-hub-canvas-hover signal driving multiple anchors,
13609
+ so all three (hub digit / hub-highlight / brand
13610
+ watermark) fade together as the canvas's decorative
13611
+ register, leaving only the surface under inspection
13612
+ foregrounded.
13613
+ Focal-recede family extension (3 anchors): R507 hub
13614
+ digit / R508 hub-highlight / R525 brand watermark. */}
13615
+ <g
13616
+ opacity={(hoveredAlias || hoveredEdgeKey || hoveredGroupLabel ||
13617
+ hoveredStatus || hoveredVendor) && !hoveredHub ? 0.7 : 1}
13618
+ data-topo-brand-watermark-wrapper
13619
+ data-topo-brand-watermark-recede={
13620
+ (hoveredAlias || hoveredEdgeKey || hoveredGroupLabel ||
13621
+ hoveredStatus || hoveredVendor) && !hoveredHub ? 'true' : 'false'
13622
+ }
13623
+ style={{ pointerEvents: 'none', transition: 'opacity 300ms ease-out' }}
13624
+ >
10596
13625
  <text
10597
13626
  x="16" y="672"
10598
13627
  fontSize="11" fontFamily="monospace" fontWeight="600"
@@ -10600,8 +13629,12 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10600
13629
  fill={pal.legendText}
10601
13630
  opacity="0.4"
10602
13631
  data-topo-brand-watermark
13632
+ data-topo-brand-watermark-breath={reducedMotion ? 'false' : 'true'}
10603
13633
  style={{ pointerEvents: 'none', transition: 'fill 200ms ease-out' }}
10604
- >sleep2agi</text>
13634
+ >sleep2agi{!reducedMotion && (
13635
+ <animate attributeName="opacity" values="0.32;0.48;0.32" dur="6s" repeatCount="indefinite" />
13636
+ )}</text>
13637
+ </g>
10605
13638
  {/* v0.10.0 Hero 3 Wave 1 / RFC §3.I (Vincent 5215 + 通信龙
10606
13639
  lead-autonomy Q4 dual-anchor minimal): canvas top-left
10607
13640
  crescent moon brand mark, visible ONLY when the
@@ -10636,10 +13669,47 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10636
13669
  the R175 panel-fade-in uses for cascade rhythm. data-
10637
13670
  topo-brand-canvas-mark-visible exposes the gate for
10638
13671
  tests. */}
13672
+ {/* Round 526 / Loop — focal-recede family 4th anchor.
13673
+ Symmetric polish to R525 (watermark recede). The
13674
+ brand crescent at canvas top-left is the second
13675
+ decorative brand element on the canvas; pre-R526 it
13676
+ stayed at flat opacity 0.35 (when visible) regardless
13677
+ of canvas attention. R526 multiplies its visible
13678
+ opacity by 0.7 when ANY non-hub canvas surface is
13679
+ hovered, matching R525's deeper-recede semantic for
13680
+ decorative brand elements (vs hub focal cluster's
13681
+ 0.85× recede at R507/R508).
13682
+ Composes cleanly with existing flowLinks gate:
13683
+ normal, flowLinks=0: opacity = 0.35 * 1.0 = 0.350
13684
+ recede, flowLinks=0: opacity = 0.35 * 0.7 = 0.245
13685
+ invisible, flowLinks>0: opacity = 0.00 * any = 0.000
13686
+ Multiplicative chain means recede only matters when
13687
+ crescent is visible (quiet canvas, flowLinks=0) —
13688
+ exactly when canvas attention elsewhere should
13689
+ dim the decorative register. 300ms transition
13690
+ already covers both axes (the existing visibility
13691
+ opacity ramp + the new recede multiplier easing).
13692
+ Focal-recede family extension (4 anchors): R507 hub
13693
+ digit / R508 hub-highlight / R525 watermark / R526
13694
+ crescent (this round). Canvas brand surfaces (R525
13695
+ watermark + R526 crescent) now BOTH carry focal-
13696
+ recede at the same 0.7 multiplier, fading as a
13697
+ decorative pair when the canvas's focal attention
13698
+ shifts elsewhere.
13699
+ data-topo-brand-canvas-mark-recede attr exposes the
13700
+ gate state for tests. */}
10639
13701
  <g
10640
- opacity={flowLinks.length === 0 ? 0.35 : 0}
13702
+ opacity={(flowLinks.length === 0 ? 0.35 : 0) * (
13703
+ (hoveredAlias || hoveredEdgeKey || hoveredGroupLabel ||
13704
+ hoveredStatus || hoveredVendor) && !hoveredHub ? 0.7 : 1
13705
+ )}
10641
13706
  data-topo-brand-canvas-mark
10642
13707
  data-topo-brand-canvas-mark-visible={flowLinks.length === 0 ? 'true' : 'false'}
13708
+ data-topo-brand-canvas-mark-recede={
13709
+ (hoveredAlias || hoveredEdgeKey || hoveredGroupLabel ||
13710
+ hoveredStatus || hoveredVendor) && !hoveredHub ? 'true' : 'false'
13711
+ }
13712
+ data-topo-brand-canvas-mark-breath={reducedMotion ? 'false' : 'true'}
10643
13713
  style={{ pointerEvents: 'none', transition: 'opacity 300ms ease-out, fill 200ms ease-out' }}
10644
13714
  >
10645
13715
  <defs>
@@ -10649,11 +13719,44 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10649
13719
  <circle cx="17.5" cy="13" r="10" fill="black" />
10650
13720
  </mask>
10651
13721
  </defs>
13722
+ {/* Round 528 / Loop — 呼吸感 family 4th anchor. Symmetric
13723
+ polish to R519 watermark ambient breath. The brand
13724
+ crescent at canvas top-left is the second decorative
13725
+ canvas brand surface; pre-R528 it stayed at the static
13726
+ composed opacity (wrapper 0.35 × no inner anim = flat).
13727
+ Post-R528 the inner <rect>'s fill-opacity breathes
13728
+ 0.8 ↔ 1.0 on a 7s cycle, composing multiplicatively
13729
+ with the wrapper's recede gate:
13730
+ normal visible: 0.35 × (0.8-1.0) = 0.280-0.350
13731
+ recede visible: 0.245 × (0.8-1.0) = 0.196-0.245
13732
+ invisible: 0 × any = 0
13733
+ 7s cadence intentionally OUT OF PHASE with R519
13734
+ watermark's 6s — the two ambient anchors never beat
13735
+ together visibly when both visible. R497 hub idle
13736
+ breath (4s) is the loudest; R498 recent-row hot pulse
13737
+ (~3s) is the most-active; R519 watermark (6s) +
13738
+ R528 crescent (7s) are the quietest ambient pair.
13739
+ 呼吸感 family extension (4 anchors):
13740
+ R497 hub idle digit 4s active-idle register
13741
+ R498 recent-row hot pulse 3s active-fresh register
13742
+ R519 watermark ambient 6s ambient (always-on)
13743
+ R528 crescent ambient 7s ambient (quiet-only) ← this round
13744
+ SMIL <animate> on fill-opacity (not parent opacity) so
13745
+ the wrapper's React-controlled gate compositions stay
13746
+ intact. Gated on !reducedMotion at JSX level —
13747
+ reducedMotion users see the inner rect at default
13748
+ fill-opacity=1.0 (no SMIL mounted, wrapper's static
13749
+ composed opacity wins). data-topo-brand-canvas-mark-
13750
+ breath attr exposes the gate state. */}
10652
13751
  <rect
10653
13752
  x="16" y="16" width="28" height="28"
10654
13753
  fill={pal.legendText}
10655
13754
  mask="url(#s2a-canvas-corner-mask)"
10656
- />
13755
+ >
13756
+ {!reducedMotion && (
13757
+ <animate attributeName="fill-opacity" values="0.8;1;0.8" dur="7s" repeatCount="indefinite" />
13758
+ )}
13759
+ </rect>
10657
13760
  </g>
10658
13761
  </svg>
10659
13762
 
@@ -11005,12 +14108,32 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
11005
14108
  (the minimap viewport is small, ~120×82 px).
11006
14109
  Filter is paint-only — bbox unchanged. transition
11007
14110
  list extends to include 'filter 200ms ease-out'
11008
- so the glow eases when zoom crosses 1.5x. */
11009
- data-topo-minimap-viewport-glow={view.zoom > 1.5 ? 'true' : 'false'}
14111
+ so the glow eases when zoom crosses 1.5x.
14112
+ R540: extends the drop-shadow to also fire on
14113
+ hoveredMinimap with HOVER PRECEDENCE over zoom-
14114
+ state. Pre-R540 the viewport drop-shadow was
14115
+ zoom-only (single gate); R540 adds an
14116
+ interactional gate at lighter blur intensity.
14117
+ Hover wins when both true — interactional signal
14118
+ (user is inspecting) trumps informational signal
14119
+ (you're zoomed). Sibling to R534 edge-badge
14120
+ hover-precedence + R538 group-label hover-tier
14121
+ extensions.
14122
+ 2-tier alpha ladder:
14123
+ hover (interactional) legendAccent 99 (~60%)
14124
+ zoom > 1.5 (info) legendAccent 80 (~50%)
14125
+ rest none
14126
+ data-topo-minimap-viewport-glow attr upgraded
14127
+ binary ('true'/'false') → 3-value ('hover' |
14128
+ 'zoom' | 'false') so tests can distinguish
14129
+ gate cause. */
14130
+ data-topo-minimap-viewport-glow={hoveredMinimap ? 'hover' : view.zoom > 1.5 ? 'zoom' : 'false'}
11010
14131
  style={{
11011
- filter: view.zoom > 1.5
11012
- ? `drop-shadow(0 0 2px ${pal.legendAccent}80)`
11013
- : undefined,
14132
+ filter: hoveredMinimap
14133
+ ? `drop-shadow(0 0 2px ${pal.legendAccent}99)`
14134
+ : view.zoom > 1.5
14135
+ ? `drop-shadow(0 0 2px ${pal.legendAccent}80)`
14136
+ : undefined,
11014
14137
  transition: smoothView
11015
14138
  ? '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'
11016
14139
  : 'stroke-width 200ms ease-out, opacity 200ms ease-out, filter 200ms ease-out',
@@ -11151,7 +14274,57 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
11151
14274
  // transition-colors only — without the transform transition,
11152
14275
  // active:scale-95 would hard-cut. transform-gpu promotes the
11153
14276
  // layer so scale doesn't trigger paint thrash.
11154
- className={`px-2 py-1 transition-colors transition-transform duration-200 ease-out transform-gpu active:scale-95 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset ${idx > 0 ? 'border-l' : ''} ${nodeScale === v ? 'bg-cyan-500/15 text-cyan-300 font-medium hover:bg-cyan-500/20 active:bg-cyan-500/25' : 'hover:bg-cyan-500/5 active:bg-cyan-500/15'}${chromePopping === popKey ? ' anet-chrome-pop' : ''}`}
14277
+ /* Round 521 / Loop extends R270's hover-preview pattern
14278
+ (inactive toggle hover previews the active state's
14279
+ visual register) to the TYPOGRAPHY axis at the chrome
14280
+ nodeSize S/M/L surface. Pre-R521 the inactive variant
14281
+ had `hover:bg-cyan-500/5` (R270 bg preview) but no
14282
+ typography preview — active variant uses `font-medium`
14283
+ (fw 500), inactive variant sat at default fw 400 even
14284
+ on hover.
14285
+ R521 adds `hover:font-medium` + `transition-[font-
14286
+ weight]` to the inactive variant so hovering an
14287
+ inactive S/M/L letter thickens the glyph 400 → 500,
14288
+ previewing the typography of the active state the
14289
+ click would commit to. Sibling to R421 chrome zoom-
14290
+ level fontWeight hover delta (rest 500 → hover 600)
14291
+ and R520 footer fontWeight hover (500 → 600) — same
14292
+ idiom: thicken-on-hover for chrome surfaces with a
14293
+ pre-commit gesture.
14294
+ `font-medium` (500) matches the ACTIVE variant's
14295
+ fw exactly — the inactive hover landing weight equals
14296
+ the active locked weight, so clicking commits to a
14297
+ typography state the eye already saw 'on the way in'.
14298
+ Hover-fw family extension (5 anchors now):
14299
+ R416 chip-row count digit rest 500 → hover 700/600
14300
+ R420 chrome zoom-level rest 500 → hover 600
14301
+ R425 hub-center digit rest 700 → hover 800
14302
+ R520 +N more flows footer rest 500 → hover 600
14303
+ R521 chrome nodeSize S/M/L inactive rest 400 → hover 500 ← this round
14304
+ Active variant `font-medium` unchanged so the rest-vs-
14305
+ active typography distinction stays intact when the
14306
+ user IS clicked-in (active stays at fw 500, inactive
14307
+ rest at fw 400, inactive hover preview at fw 500).
14308
+ data-topo-chrome-nodesize-hover-preview-fw="500" attr
14309
+ exposes the polish for tests. */
14310
+ /* R598 — nodeSize S/M/L segmented buttons gain hover:
14311
+ brightness-[1.15] (39+40+41st anchors, 8+9+10th HTML).
14312
+ Final segmented chrome control to close brightness
14313
+ coverage. After R598 all three segmented controls
14314
+ (zoom + layout + nodeSize) have full hover-brightness
14315
+ parity.
14316
+ Tailwind v4 arbitrary `[transition-property:color,
14317
+ background-color,transform,font-weight,filter]`
14318
+ replaces the chain of `transition-colors transition-
14319
+ transform transition-[font-weight]` so the filter
14320
+ property joins the existing 200ms cadence at the
14321
+ same beat. Segmented-unity rule (R400) preserved —
14322
+ brightness is pure paint, no geometric break.
14323
+ data-topo-chrome-nodesize-brightness-hover='1.15'
14324
+ attr documents the hover value for tests. */
14325
+ className={`px-2 py-1 [transition-property:color,background-color,transform,font-weight,filter] duration-200 ease-out transform-gpu active:scale-95 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset hover:brightness-[1.15] ${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' : ''}`}
14326
+ data-topo-chrome-nodesize-hover-preview-fw={nodeScale === v ? null : '500'}
14327
+ data-topo-chrome-nodesize-brightness-hover="1.15"
11155
14328
  style={{ color: nodeScale === v ? undefined : pal.legendText, borderColor: pal.containerBorder }}
11156
14329
  >
11157
14330
  {lbl}
@@ -11189,6 +14362,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
11189
14362
  onClick={() => { popChrome('zoom-out'); zoomByDiscrete(1 / 1.2); }}
11190
14363
  data-topo-chrome-zoom-out
11191
14364
  data-topo-chrome-zoom-out-popping={chromePopping === 'zoom-out' ? 'true' : 'false'}
14365
+ data-topo-chrome-zoom-out-brightness-hover="1.15"
11192
14366
  // R196: press-state deepens bg one tier above hover (white/5
11193
14367
  // → white/10) so mouse-down has a tactile dim before the
11194
14368
  // R186 icon pop fires on release.
@@ -11197,7 +14371,26 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
11197
14371
  // press-feedback family (R492 + nodeSize above). transition-
11198
14372
  // transform + duration-200 + ease-out + transform-gpu added
11199
14373
  // since the className had only transition-colors.
11200
- 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"
14374
+ /* R596 zoom-out segmented button gains hover:brightness-[1.15]
14375
+ (35th anchor in per-element brightness family, 4th HTML).
14376
+ Sibling to zoom-in below — paired-anchor round closing the
14377
+ entire zoom trio at brightness parity (R593 zoom-level
14378
+ readout + R596 zoom-out + zoom-in).
14379
+
14380
+ Segmented-control constraint preserved: brightness is pure
14381
+ paint (no geometry shift) so it doesn't break the R400
14382
+ segmented-unity rule (which only excludes geometric hover-
14383
+ lift like translateY). Each segment can brighten
14384
+ independently while the strip stays planted as one unit.
14385
+
14386
+ Tailwind v4 arbitrary `[transition-property:...]` replaces
14387
+ `transition-colors transition-transform` so the filter
14388
+ property joins the existing 200ms cadence — bg / color /
14389
+ transform / filter all ease in unison.
14390
+
14391
+ data-topo-chrome-zoom-out-brightness-hover='1.15' attr
14392
+ documents the hover value for tests. */
14393
+ className="group px-2 py-1 hover:bg-white/5 hover:brightness-[1.15] active:bg-white/10 [transition-property:color,background-color,transform,filter] duration-200 ease-out transform-gpu active:scale-95 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset"
11201
14394
  style={{ color: pal.legendText }}
11202
14395
  aria-label="Zoom out"
11203
14396
  title="Zoom out (−)"
@@ -11289,13 +14482,66 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
11289
14482
  ? 'true' : 'false'
11290
14483
  }
11291
14484
  data-topo-chrome-zoom-level-hover={hoveredZoomLevel ? 'true' : 'false'}
14485
+ /* Round 517 / Loop — extends the chrome zoom-level readout
14486
+ from 2-axis (R347 letter-spacing + R420 fontWeight) to
14487
+ 3-axis hover signature by adding a color brighten to
14488
+ pal.legendHeadline. Pre-R517 the readout's color stayed
14489
+ at pal.legendText on hover; the digits got tighter
14490
+ kerning (0→0.5px) and heavier weight (500→600) but
14491
+ stayed the same legendText gray tone. R517 lifts color
14492
+ to legendHeadline on hover so the readout brightens
14493
+ into the headline tier at the same beat — matching the
14494
+ R55/R197/R239 hover-deepen-own-hue idiom that legend-
14495
+ row label + count carry at panel scope. Chrome strip's
14496
+ only data display now has full 3-axis hover signature
14497
+ (letter-spacing + fontWeight + color), parity with the
14498
+ chip-row chips' own hover-brighten pattern.
14499
+ Implementation: inline color uses the same hoveredZoom-
14500
+ Level state as R347/R420 — no new state. Transition
14501
+ already includes 'color 200ms ease-out' (R264) so the
14502
+ brighten eases under the same cadence as the kerning +
14503
+ weight tweens — one motion-coherent 3-axis lift.
14504
+ data-topo-chrome-zoom-level-color attr exposes the
14505
+ resolved color string for tests. */
14506
+ data-topo-chrome-zoom-level-color={hoveredZoomLevel ? 'headline' : 'text'}
14507
+ /* R593 — chrome zoom-level readout gains filter
14508
+ brightness(1.15) on hover. 32nd anchor in per-element
14509
+ brightness family. FIRST HTML-element brightness
14510
+ anchor outside the SVG canvas (the readout is an
14511
+ HTML <span>, not SVG).
14512
+
14513
+ Closes the chrome zoom-level readout's hover signature
14514
+ at 4 AXES:
14515
+ R347 letter-spacing 0 → 0.5px
14516
+ R420 fontWeight 500 → 600
14517
+ R517 color legendText → legendHeadline
14518
+ R593 brightness 1 → 1.15 ← this round
14519
+
14520
+ The chrome strip's only data display now has full
14521
+ 4-axis hover signature — color brightens to headline
14522
+ tier, glyph thickens, kerning spreads, AND brightness
14523
+ lifts another +15% on top of the color swap.
14524
+
14525
+ Triple-paint multiplicative interaction:
14526
+ color (legendHeadline) × brightness(1.15) — the
14527
+ headline tier reads dramatically brighter than a
14528
+ plain color swap.
14529
+
14530
+ Transition list extends with 'filter 200ms ease-out'
14531
+ matching the existing 200ms cadence across all 4 axes
14532
+ — motion-coherent state-flip.
14533
+
14534
+ data-topo-chrome-zoom-level-brightness attr exposes
14535
+ the gate for tests. */
14536
+ data-topo-chrome-zoom-level-brightness={hoveredZoomLevel ? '1.15' : '1'}
11292
14537
  onMouseEnter={() => setHoveredZoomLevel(true)}
11293
14538
  onMouseLeave={() => setHoveredZoomLevel(false)}
11294
14539
  style={{
11295
- color: pal.legendText,
14540
+ color: hoveredZoomLevel ? pal.legendHeadline : pal.legendText,
11296
14541
  borderColor: pal.containerBorder,
11297
14542
  minWidth: 46,
11298
14543
  display: 'inline-block',
14544
+ filter: hoveredZoomLevel ? 'brightness(1.15)' : undefined,
11299
14545
  // R347: letter-spacing hover tween — extends R344/R345
11300
14546
  // hover-letter-spacing family into the chrome strip.
11301
14547
  letterSpacing: hoveredZoomLevel ? '0.5px' : '0',
@@ -11325,7 +14571,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
11325
14571
  on theme flip while siblings eased. Sibling treatment
11326
14572
  to the nodeSize + zoom wrapper transitions added this
11327
14573
  round. */
11328
- transition: 'color 200ms ease-out, border-color 200ms ease-out, letter-spacing 200ms ease-out, font-weight 200ms ease-out',
14574
+ transition: 'color 200ms ease-out, border-color 200ms ease-out, letter-spacing 200ms ease-out, font-weight 200ms ease-out, filter 200ms ease-out',
11329
14575
  }}
11330
14576
  title="Current zoom level"
11331
14577
  >
@@ -11335,13 +14581,16 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
11335
14581
  onClick={() => { popChrome('zoom-in'); zoomByDiscrete(1.2); }}
11336
14582
  data-topo-chrome-zoom-in
11337
14583
  data-topo-chrome-zoom-in-popping={chromePopping === 'zoom-in' ? 'true' : 'false'}
14584
+ data-topo-chrome-zoom-in-brightness-hover="1.15"
11338
14585
  // R196: press-state (mirror of zoom-out above).
11339
14586
  // R352: `group` lets the inner svg respond via group-hover.
11340
14587
  // R493 — zoom +/− buttons join the chrome-strip active:scale-95
11341
14588
  // press-feedback family (R492 + nodeSize above). transition-
11342
14589
  // transform + duration-200 + ease-out + transform-gpu added
11343
14590
  // since the className had only transition-colors.
11344
- 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"
14591
+ /* R596 sibling zoom-in mirrors zoom-out above. 36th anchor
14592
+ in per-element brightness family, 5th HTML. */
14593
+ className="group px-2 py-1 hover:bg-white/5 hover:brightness-[1.15] active:bg-white/10 [transition-property:color,background-color,transform,filter] duration-200 ease-out transform-gpu active:scale-95 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset"
11345
14594
  style={{ color: pal.legendText }}
11346
14595
  aria-label="Zoom in"
11347
14596
  title="Zoom in (+)"
@@ -11400,9 +14649,49 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
11400
14649
  // active state during press = hover-lift (-1px) + scale-95
11401
14650
  // composes as translateY(-1px) scale(0.95) — lift-and-compress
11402
14651
  // for tactile click feel.
11403
- 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"
14652
+ className="p-1.5 rounded-md border hover:bg-white/5 active:bg-white/10 hover:-translate-y-px active:scale-95 transform-gpu focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60"
11404
14653
  data-topo-chrome-reset-hover-lift="true"
11405
- style={{ background: pal.legendBox.fill, borderColor: pal.containerBorder, color: pal.legendText }}
14654
+ /* R594 chrome reset button gains filter brightness(1.15)
14655
+ on hoveredReset. 33rd anchor in per-element brightness
14656
+ family, 2nd HTML-element anchor (R593 zoom-level was
14657
+ the first).
14658
+
14659
+ Closes reset button hover signature at 5 axes:
14660
+ R400 button hover-lift translateY(-1px)
14661
+ R350 icon hover-rotate -8°
14662
+ R453 icon stroke-width 2.5 → 2.8
14663
+ R514 icon scale 1.0 → 1.10
14664
+ R594 button brightness 1 → 1.15 ← this round
14665
+
14666
+ The reset button now has the densest hover signature
14667
+ among the 2 standalone chrome buttons (reset + fullscreen
14668
+ from R400). When user hovers, the entire button lifts
14669
+ (-1px), brightens (+15%), AND its icon rotates (-8°),
14670
+ thickens (+0.3 sw), and scales up (+10%). Inside +
14671
+ outside motion-coherent.
14672
+
14673
+ Triple-paint multiplicative interaction at this surface:
14674
+ bg (hover:bg-white/5) × border (pal.containerBorder)
14675
+ × color (pal.legendText) ALL get brightness(1.15)
14676
+ multiplied in — the button's chrome stack uniformly
14677
+ brightens, reading as "this control is awake under
14678
+ your cursor".
14679
+
14680
+ Inline transition shorthand replaces the className-
14681
+ based `transition-colors transition-transform` (R557
14682
+ banked: when adding new transition-driven hover axes
14683
+ to an element, extend the INLINE list — inline
14684
+ overrides className). All 5 transition properties (bg
14685
+ / color / border / transform / filter) now ride one
14686
+ 200ms ease-out beat. */
14687
+ data-topo-chrome-reset-brightness={hoveredReset ? '1.15' : '1'}
14688
+ style={{
14689
+ background: pal.legendBox.fill,
14690
+ borderColor: pal.containerBorder,
14691
+ color: pal.legendText,
14692
+ filter: hoveredReset ? 'brightness(1.15)' : undefined,
14693
+ transition: 'color 200ms ease-out, background-color 200ms ease-out, border-color 200ms ease-out, transform 200ms ease-out, filter 200ms ease-out',
14694
+ }}
11406
14695
  aria-label="Reset view"
11407
14696
  title="Reset zoom + pan (0, or double-click the canvas)"
11408
14697
  >
@@ -11448,8 +14737,34 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
11448
14737
  // owns transform during its 450ms run. transformOrigin
11449
14738
  // 'center' so rotation pivots around the icon's centre
11450
14739
  // (default would be top-left and the icon would arc).
14740
+ /* Round 514 / Loop — extends R352/R353 chrome icon hover-
14741
+ scale family to the reset button. Pre-R514 the reset
14742
+ icon had hover-rotate (-8°, R350) + hover-sw (2.5→2.8,
14743
+ R453) but no hover-scale, while zoom-out (R352), zoom-
14744
+ in (R352), and fullscreen (R353) icons all carried
14745
+ `group-hover:scale-110`. R514 brings the reset icon
14746
+ into the same 3-axis hover signature (rotate + sw +
14747
+ scale) as the rest of the chrome strip.
14748
+ Implementation: inline transform composes rotate +
14749
+ scale into one string. `transform: rotate(-8deg)
14750
+ scale(1.1)` on hover; `rotate(0) scale(1)` at rest.
14751
+ transformOrigin 'center' applies to both — rotation
14752
+ pivots around centre AND scale grows from centre.
14753
+ The Tailwind `group-hover:scale-110` approach can't
14754
+ work here because inline `style.transform` overrides
14755
+ className-based transforms; compose the multi-axis
14756
+ transform inline instead.
14757
+ Chrome icon hover gesture parity (post-R514):
14758
+ zoom-out scale-110 + sw-lift (R352/R454)
14759
+ zoom-in scale-110 + sw-lift (R352/R454)
14760
+ fullscreen scale-110 + sw-lift (R353/R455)
14761
+ reset scale-1.1 + sw-lift + rotate -8°
14762
+ (R514 + R453 + R350)
14763
+ reset gets the EXTRA rotate axis because R350's spin
14764
+ preview semantic is reset-specific — the rotation
14765
+ hints at the click-spin (R184) the button will fire. */
11451
14766
  style={{
11452
- transform: hoveredReset && !resetSpinning ? 'rotate(-8deg)' : 'rotate(0deg)',
14767
+ transform: hoveredReset && !resetSpinning ? 'rotate(-8deg) scale(1.1)' : 'rotate(0deg) scale(1)',
11453
14768
  transformOrigin: 'center',
11454
14769
  transition: 'transform 200ms ease-out, stroke-width 200ms ease-out',
11455
14770
  }}
@@ -11473,9 +14788,15 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
11473
14788
  classes win specificity. */}
11474
14789
  <button
11475
14790
  onClick={() => { popChrome('fullscreen'); toggleFullscreen(); }}
14791
+ onMouseEnter={() => setHoveredFullscreen(true)}
14792
+ onMouseLeave={() => setHoveredFullscreen(false)}
14793
+ onFocus={() => setHoveredFullscreen(true)}
14794
+ onBlur={() => setHoveredFullscreen(false)}
11476
14795
  data-topo-chrome-fullscreen
11477
14796
  data-topo-chrome-fullscreen-active={isFullscreen ? 'true' : 'false'}
11478
14797
  data-topo-chrome-fullscreen-popping={chromePopping === 'fullscreen' ? 'true' : 'false'}
14798
+ data-topo-chrome-fullscreen-hover={hoveredFullscreen ? 'true' : 'false'}
14799
+ data-topo-chrome-fullscreen-brightness={hoveredFullscreen ? '1.15' : '1'}
11479
14800
  // R196: fullscreen also picks up press-state — active variant
11480
14801
  // deepens cyan-500/20 → cyan-500/25 on press; non-active
11481
14802
  // deepens white/5 → white/10.
@@ -11495,17 +14816,48 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
11495
14816
  // R400: hover translateY(-1px) lift — see reset button above for family doc.
11496
14817
  // R493 — fullscreen joins active:scale-95 press family (same as
11497
14818
  // reset above: lift-and-compress compound transform on press).
11498
- 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 ${
14819
+ className={`group p-1.5 rounded-md border hover:-translate-y-px active:scale-95 transform-gpu focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 ${
11499
14820
  isFullscreen
11500
- ? 'bg-cyan-500/15 text-cyan-300 font-medium hover:bg-cyan-500/20 active:bg-cyan-500/25'
14821
+ ? 'bg-cyan-500/15 text-cyan-300 font-medium hover:bg-cyan-500/20 hover:text-cyan-200 active:bg-cyan-500/25'
11501
14822
  : 'hover:bg-cyan-500/5 active:bg-cyan-500/15'
11502
14823
  }${chromePopping === 'fullscreen' ? ' anet-chrome-pop' : ''}`}
11503
14824
  data-topo-chrome-fullscreen-hover-lift="true"
14825
+ /* R595 — chrome fullscreen button gains filter brightness(1.15)
14826
+ on hoveredFullscreen. 34th anchor in per-element brightness
14827
+ family, 3rd HTML-element anchor (R593 zoom-level + R594
14828
+ reset). Sibling to R594 — closes the standalone chrome
14829
+ button pair (reset + fullscreen) at full brightness parity.
14830
+
14831
+ Per the R400 family doc, reset + fullscreen are the only
14832
+ two standalone (non-segmented) chrome buttons. Both now
14833
+ share the same 5-axis hover signature pattern:
14834
+ button hover-lift translateY(-1px) R400
14835
+ icon hover-scale 1.0 → 1.10 R353 (full) / R514 (reset)
14836
+ icon stroke-width 2.5 → 2.8 R455 (full) / R453 (reset)
14837
+ icon hover-rotate R576 (full +3°) / R350 (reset -8°)
14838
+ button brightness 1 → 1.15 R595 (full) / R594 (reset)
14839
+
14840
+ Inline transition shorthand replaces the className-based
14841
+ `transition-colors transition-transform` (R557 banked:
14842
+ inline overrides className for transition-driven hover
14843
+ axes). 5 transition properties (bg / color / border /
14844
+ transform / filter) ride one 200ms ease-out beat.
14845
+
14846
+ Active-variant interaction: when isFullscreen=true the
14847
+ className applies cyan bg + cyan text. brightness(1.15)
14848
+ on top makes the active+hovered state read at maximum
14849
+ vivid cyan — the user knows they're poised to EXIT
14850
+ fullscreen with extra visual confirmation.
14851
+
14852
+ data-topo-chrome-fullscreen-brightness attr exposes
14853
+ the gate for tests. */
11504
14854
  style={{
11505
14855
  borderColor: pal.containerBorder,
11506
14856
  ...(isFullscreen
11507
14857
  ? {}
11508
14858
  : { background: pal.legendBox.fill, color: pal.legendText }),
14859
+ filter: hoveredFullscreen ? 'brightness(1.15)' : undefined,
14860
+ transition: 'color 200ms ease-out, background-color 200ms ease-out, border-color 200ms ease-out, transform 200ms ease-out, filter 200ms ease-out',
11509
14861
  }}
11510
14862
  aria-label={isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}
11511
14863
  title={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}
@@ -11531,12 +14883,20 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
11531
14883
  + R455 fullscreen (this round). transition-[transform,
11532
14884
  stroke-width] expands existing transition-transform
11533
14885
  so the sw lift eases under R352 scale-110 cadence. */}
14886
+ {/* Round 576 / Loop — fullscreen icon picks up hover-rotate-3.
14887
+ Joins R350 reset / R547 pill × / R549 brand logo at 4th
14888
+ anchor in hover-rotate idiom. Corner arrows rotate 3° on
14889
+ hover — subtle "preparing to transform" gesture (enter
14890
+ or exit fullscreen). Tailwind v4 emits individual rotate
14891
+ property (banked R547) — sits alongside group-hover:
14892
+ scale-110 in independent rotate axis. */}
11534
14893
  {isFullscreen ? (
11535
- <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">
14894
+ <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">
11536
14895
  <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" />
11537
14896
  </svg>
11538
14897
  ) : (
11539
- <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">
14898
+ // R576 sibling enter variant also picks up rotate-3.
14899
+ <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">
11540
14900
  <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" />
11541
14901
  </svg>
11542
14902
  )}