@sleep2agi/agent-network-dashboard 0.5.3-preview.7 → 0.5.3-preview.71

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 (226) 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 +3 -3
  134. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js.map +1 -1
  135. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js +1 -1
  136. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js.map +1 -1
  137. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js +1 -1
  138. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js.map +1 -1
  139. package/.next/server/chunks/ssr/agent-network-dashboard_app_components_0mvyi-4._.js +1 -1
  140. package/.next/server/chunks/ssr/agent-network-dashboard_app_components_0mvyi-4._.js.map +1 -1
  141. package/.next/server/middleware-build-manifest.js +3 -3
  142. package/.next/server/pages/404.html +2 -2
  143. package/.next/server/pages/500.html +1 -1
  144. package/.next/static/chunks/0-t3-uzgs6nxo.js +1 -0
  145. package/.next/static/chunks/{0jz0q_~y452m5.js → 0.c773w~yj_xb.js} +1 -1
  146. package/.next/static/chunks/02jvgrnr8cu6v.js +4 -0
  147. package/.next/static/chunks/0f01_~6f-mcbm.js +1 -0
  148. package/.next/static/chunks/0q.024-h32581.css +2 -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 +1954 -87
  154. package/app/globals.css +81 -7
  155. package/package.json +4 -4
  156. package/scripts/p157-servers-copy-test.mjs +95 -0
  157. package/scripts/topo-active-chrome-hover-text-test.mjs +107 -0
  158. package/scripts/topo-alias-glow-test.mjs +121 -0
  159. package/scripts/topo-avatar-brightness-test.mjs +116 -0
  160. package/scripts/topo-brand-logo-breath-test.mjs +102 -0
  161. package/scripts/topo-brand-logo-hover-brightness-test.mjs +105 -0
  162. package/scripts/topo-brand-logo-hover-rotate-test.mjs +93 -0
  163. package/scripts/topo-brand-logo-hover-test.mjs +85 -0
  164. package/scripts/topo-chip-row-digit-ls-test.mjs +135 -0
  165. package/scripts/topo-cluster-count-attr-test.mjs +80 -0
  166. package/scripts/topo-crescent-breath-test.mjs +104 -0
  167. package/scripts/topo-crescent-recede-test.mjs +111 -0
  168. package/scripts/topo-edge-badge-hover-glow-test.mjs +90 -0
  169. package/scripts/topo-edge-pill-glow-test.mjs +67 -0
  170. package/scripts/topo-filter-pill-glow-test.mjs +90 -0
  171. package/scripts/topo-filter-pills-press-test.mjs +96 -0
  172. package/scripts/topo-fleet-density-tier-test.mjs +84 -0
  173. package/scripts/topo-freshness-chip-fade-test.mjs +105 -0
  174. package/scripts/topo-fullscreen-attr-test.mjs +73 -0
  175. package/scripts/topo-grid-content-bottom-attr-test.mjs +72 -0
  176. package/scripts/topo-group-label-hover-glow-test.mjs +86 -0
  177. package/scripts/topo-group-pill-glow-test.mjs +76 -0
  178. package/scripts/topo-hub-digit-ls-test.mjs +119 -0
  179. package/scripts/topo-hub-halo-glow-test.mjs +96 -0
  180. package/scripts/topo-hub-highlight-amplify-test.mjs +139 -0
  181. package/scripts/topo-hub-highlight-fill-transition-test.mjs +84 -0
  182. package/scripts/topo-hub-highlight-glow-test.mjs +99 -0
  183. package/scripts/topo-hub-highlight-r-test.mjs +112 -0
  184. package/scripts/topo-hub-highlight-recede-test.mjs +144 -0
  185. package/scripts/topo-hub-highlight-theme-fill-test.mjs +83 -0
  186. package/scripts/topo-hub-hover-ring-glow-test.mjs +97 -0
  187. package/scripts/topo-hub-idle-breath-test.mjs +109 -0
  188. package/scripts/topo-hub-recede-test.mjs +124 -0
  189. package/scripts/topo-hub-spoke-glow-test.mjs +112 -0
  190. package/scripts/topo-layout-hover-fw-test.mjs +98 -0
  191. package/scripts/topo-legend-count-letter-spacing-test.mjs +108 -0
  192. package/scripts/topo-legend-label-fw-test.mjs +107 -0
  193. package/scripts/topo-legend-swatch-glow-test.mjs +109 -0
  194. package/scripts/topo-minimap-hover-glow-test.mjs +109 -0
  195. package/scripts/topo-nodesize-hover-fw-test.mjs +99 -0
  196. package/scripts/topo-orphan-box-dash-test.mjs +89 -0
  197. package/scripts/topo-orphan-fill-opacity-test.mjs +91 -0
  198. package/scripts/topo-orphan-label-italic-test.mjs +90 -0
  199. package/scripts/topo-orphan-label-opacity-test.mjs +98 -0
  200. package/scripts/topo-panel-title-glow-test.mjs +111 -0
  201. package/scripts/topo-pill-x-rotate-test.mjs +96 -0
  202. package/scripts/topo-pinned-aspect-test.mjs +89 -0
  203. package/scripts/topo-pressure-seg-glow-test.mjs +92 -0
  204. package/scripts/topo-pressure-seg-motion-test.mjs +101 -0
  205. package/scripts/topo-recent-hot-pulse-test.mjs +102 -0
  206. package/scripts/topo-recent-more-fw-test.mjs +126 -0
  207. package/scripts/topo-recent-row-fw-test.mjs +115 -0
  208. package/scripts/topo-reduced-motion-attr-test.mjs +69 -0
  209. package/scripts/topo-reset-icon-hover-scale-test.mjs +102 -0
  210. package/scripts/topo-starfield-hue-test.mjs +109 -0
  211. package/scripts/topo-titleblock-h2-hover-fw-test.mjs +109 -0
  212. package/scripts/topo-titleblock-h2-hover-tracking-test.mjs +128 -0
  213. package/scripts/topo-titleblock-kicker-hover-test.mjs +134 -0
  214. package/scripts/topo-vendor-activelinks-press-test.mjs +100 -0
  215. package/scripts/topo-vendor-chip-glow-test.mjs +97 -0
  216. package/scripts/topo-vendor-pill-glow-test.mjs +98 -0
  217. package/scripts/topo-watermark-breath-test.mjs +100 -0
  218. package/scripts/topo-watermark-recede-test.mjs +114 -0
  219. package/scripts/topo-zoom-level-color-test.mjs +105 -0
  220. package/.next/static/chunks/0hndl9yzpqajt.css +0 -2
  221. package/.next/static/chunks/0ia4_98ytam_7.js +0 -1
  222. package/.next/static/chunks/0md~c._ox5owe.js +0 -4
  223. package/.next/static/chunks/15w33hhbf3.28.js +0 -1
  224. /package/.next/static/{bIxZRyo8qjN2ZGvJ_1dmh → IZQY3ZDALy8SV229GjOss}/_buildManifest.js +0 -0
  225. /package/.next/static/{bIxZRyo8qjN2ZGvJ_1dmh → IZQY3ZDALy8SV229GjOss}/_clientMiddlewareManifest.js +0 -0
  226. /package/.next/static/{bIxZRyo8qjN2ZGvJ_1dmh → IZQY3ZDALy8SV229GjOss}/_ssgManifest.js +0 -0
@@ -240,11 +240,30 @@ function FreshnessChip({ sessions }: { sessions: unknown }) {
240
240
  stale-onset to direct attention. */
241
241
  if (!stale) return null;
242
242
  return (
243
+ /* Round 505 / Loop — FreshnessChip mount animation. Pre-R505 the
244
+ chip popped into the chip-row instantly when SWR data crossed
245
+ the 10s stale threshold; users saw an abrupt amber pill appear
246
+ mid-row. R505 adds the existing `anet-fade-in` class so the
247
+ chip eases through opacity 0→1 over 150ms (R51 globals.css
248
+ keyframe) on first appearance. The chip itself only renders
249
+ when stale (R275 conditional), so the fade plays exactly when
250
+ the stale signal first arrives — perfectly aligned with the
251
+ semantic. Mount-once via React reconciliation (key not used
252
+ since FreshnessChip is a singleton in the parent).
253
+ a11y respected via R29 blanket — `@media (prefers-reduced-
254
+ motion: reduce)` neutralizes anet-fade-in to `animation:none`
255
+ (globals.css line 1083-1089 includes anet-fade-in in the
256
+ blanket list). Reduced-motion users see the chip pop instantly,
257
+ same as pre-R505 behavior — no regression.
258
+ Pure paint-axis addition (opacity animation, no geometry),
259
+ bbox unchanged. data-freshness-chip-mount-fade attr exposes
260
+ the gate for tests. */
243
261
  <span
244
- className={`${baseClass} ${colorClass}`}
262
+ className={`${baseClass} ${colorClass} anet-fade-in`}
245
263
  title={stale ? `Last sync ${sec}s ago — SWR refresh may be lagging` : `Live data · refreshes every 5s · last sync ${sec}s ago`}
246
264
  data-freshness-chip
247
265
  data-freshness-chip-stale={stale ? 'true' : 'false'}
266
+ data-freshness-chip-mount-fade="true"
248
267
  >
249
268
  {/* Round 272 / Loop: swap prefix word to match color state so
250
269
  text and color point the same way. Pre-R272 the chip read
@@ -867,12 +886,19 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
867
886
  // R63 label render + R86 hover-pin keying + #99 tooltip
868
887
  // member listing, so all the existing group-box machinery
869
888
  // applies uniformly to the orphan bucket too.
889
+ // Round 499 / Loop — surface `isOrphan` flag on the box
890
+ // shape so downstream renderers (label text, future polish)
891
+ // can apply orphan-specific typography (italic) without
892
+ // re-deriving the flag from key === '其他' (key matching
893
+ // would also catch a legitimate "其他" prefix-group, this
894
+ // flag is canonical from the band assignment pass).
870
895
  return {
871
896
  key: band.isOrphan
872
897
  ? '其他'
873
898
  : band.members.length
874
899
  ? groupKeys[band.members[0].alias]
875
900
  : '',
901
+ isOrphan: !!band.isOrphan,
876
902
  count: band.members.length,
877
903
  statuses: { working: w, idle: i, offline: o },
878
904
  x: minX - GROUP_PAD,
@@ -977,7 +1003,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
977
1003
  groupKeys,
978
1004
  // #111: group boxes are a grid-layout feature only — radially scattered
979
1005
  // ring nodes can't be cleanly boxed. Ring keeps the #83 prefix hue.
980
- groupBoxes: [] as { key: string; count: number; statuses: { working: number; idle: number; offline: number }; x: number; y: number; w: number; h: number }[],
1006
+ groupBoxes: [] as { key: string; isOrphan?: boolean; count: number; statuses: { working: number; idle: number; offline: number }; x: number; y: number; w: number; h: number }[],
981
1007
  // ring fits within VIEWBOX_H by construction (offlineRadius=325 + centre at y=330)
982
1008
  gridContentBottom: 0,
983
1009
  };
@@ -1699,7 +1725,18 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1699
1725
  width) = 166px total title-block width vs 168px pre-R298 —
1700
1726
  no measurable layout shift, just a deliberate tighter
1701
1727
  grouping. */}
1702
- <div className="flex items-center gap-2.5">
1728
+ {/* Round 554 / Loop — title-block wrapper picks up `group` so
1729
+ the H2 below can subscribe to `group-hover:tracking-tighter`.
1730
+ Pairs with R548/R549 brand-logo hover gestures: cursor
1731
+ sweeping anywhere across the title cluster fires the brand
1732
+ logo's scale-105 + rotate-6 + breath ↔ AND tightens the
1733
+ H2's tracking from -0.025em → -0.05em.
1734
+ Makes the title-block read as one coherent hover cluster —
1735
+ brand mark provides the loud gesture (scale + rotate), H2
1736
+ provides the subtle editorial gesture (kerning tighten).
1737
+ data-topo-section-titleblock-group attr surfaces the gate
1738
+ for tests. */}
1739
+ <div className="group flex items-center gap-2.5" data-topo-section-titleblock-group>
1703
1740
  {/* Round 297 / Loop: brand-logo color picks up the 200ms ease-
1704
1741
  out transition. Pre-R297 the moon glyph had theme-
1705
1742
  conditional color (cyber #67e8f9 cyan ↔ light #0d9488
@@ -1723,13 +1760,136 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1723
1760
  overpowering the h2 at text-lg/font-semibold (R286).
1724
1761
  viewBox 32×32 unchanged so the inner crescent geometry
1725
1762
  scales proportionally. */}
1763
+ {/* Round 548 / Loop — title-block brand logo gains subtle
1764
+ hover-scale gesture. Pre-R548 the 40×40 crescent was
1765
+ fully static — no hover affordance. R548 adds hover:
1766
+ scale-105 (Tailwind 4 emits as `scale: 1.05`) so the
1767
+ brand mark gently responds to attention as the user's
1768
+ cursor sweeps across the title block. 5% scale is
1769
+ intentionally subtle (vs R350 chrome icon hover-scale-
1770
+ 110): the brand logo is a passive identity mark, not
1771
+ an interactive control, so the gesture stays small.
1772
+ cursor: default to clarify non-clickability (the SVG
1773
+ isn't a button; just a brand element).
1774
+ transition-transform duration-200 ease-out matches the
1775
+ dashboard's R350-family hover-scale cadence so the
1776
+ brand logo's response shares the same motion vocabulary
1777
+ as the chrome strip's icon scales. transform-gpu hint
1778
+ promotes the SVG to its own compositor layer for crisp
1779
+ edges during the tween.
1780
+ Brand-mark delight gesture family (1 anchor):
1781
+ R548 title-block brand logo hover:scale-105
1782
+ The crescent at canvas top-left (data-topo-brand-
1783
+ canvas-mark) and the watermark at bottom-left (data-
1784
+ topo-brand-watermark) are intentionally LEFT STATIC —
1785
+ both have pointerEvents:none and exist as ambient
1786
+ decoration with their own breath/recede dynamics
1787
+ (R519/R525/R526/R528). The title-block logo is the
1788
+ ONLY brand surface that's a candidate for hover affordance,
1789
+ since it sits in the chrome-band where the cursor
1790
+ naturally passes during normal use. */}
1791
+ {/* Round 549 / Loop — extends R548 (hover:scale-105) with a
1792
+ subtle hover:rotate-6 rotation. Pairs scale + rotate on
1793
+ the brand mark, same idiom as R547 added to the pill ×
1794
+ close buttons (scale-110 + rotate-12) but at gentler
1795
+ amounts: 105% vs 110% scale and 6° vs 12° rotation —
1796
+ brand marks want restraint vs interactive close buttons.
1797
+
1798
+ The crescent-moon shape (curved cutout via mask) reads
1799
+ visually distinct as it rotates — the asymmetric cutout
1800
+ swings into a fresh angle, telegraphing "this brand mark
1801
+ is alive without being loud". The 6° landing lands the
1802
+ moon's cusp pointing slightly NE rather than straight up,
1803
+ a small but legible reveal of the shape's geometry.
1804
+
1805
+ Tailwind 4 emits BOTH `scale: 1.05` AND `rotate: 6deg`
1806
+ as INDIVIDUAL CSS properties (not a combined transform
1807
+ string — see R547 banked pattern). The className keeps
1808
+ transition-transform so both axes ease at the same 200ms
1809
+ cadence; transform-gpu hint stays so the compositor
1810
+ promotes both axes to GPU layers.
1811
+
1812
+ data-topo-brand-logo-hover-rotate attr surfaces the
1813
+ landing rotation for tests. Pair with R548's
1814
+ data-topo-brand-logo-hover-scale attr — both attrs
1815
+ advertise the dual-axis hover signature. */}
1816
+ {/* Round 553 / Loop — title-block brand logo gains subtle
1817
+ idle opacity breath (~0.92 ↔ 1, 5s ease-in-out cycle).
1818
+ 5th anchor in the 呼吸感 breath family, slotting into
1819
+ the ascending cadence ladder between hub idle (4s) and
1820
+ watermark (6s):
1821
+ row hot 3s
1822
+ hub idle 4s
1823
+ brand logo 5s ← this round
1824
+ watermark 6s
1825
+ crescent 7s
1826
+ Composes cleanly with R548 hover:scale-105 + R549
1827
+ hover:rotate-6 — opacity, scale, and rotate are
1828
+ independent CSS properties; the moon keeps breathing
1829
+ as it scales and rotates on hover. Layered effect
1830
+ reads as "this brand mark is alive even before you
1831
+ touch it, and lights up further on hover".
1832
+ Reduced-motion gate: component-side `!reducedMotion`
1833
+ toggles the className (canonical TopoGraph breath
1834
+ pattern); R29 globals.css blanket provides a
1835
+ defense-in-depth fallback (animation-duration →
1836
+ 0.001ms under prefers-reduced-motion: reduce).
1837
+ data-topo-brand-logo-breath attr exposes the gate
1838
+ state for tests. */}
1839
+ {/* Round 557 / Loop — brand logo gains 4th hover axis:
1840
+ hover:brightness-110 (filter). Adds a chromatic axis
1841
+ to the brand-mark hover signature alongside R548
1842
+ scale, R549 rotate, R553 idle breath:
1843
+ R548 hover:scale-105 transform-scale
1844
+ R549 hover:rotate-6 transform-rotate
1845
+ R553 idle breath (5s) opacity (animation)
1846
+ R557 hover:brightness-110 filter ← this round
1847
+ All 4 axes ride on INDEPENDENT CSS properties (scale,
1848
+ rotate, opacity, filter) — they compose freely without
1849
+ clobbering each other. The cyan/teal crescent gains a
1850
+ soft +10% brightness boost on hover, layered on top of
1851
+ the existing scale + rotate lift + ongoing idle breath.
1852
+ Why +10% (vs more aggressive 125/150): brand mark wants
1853
+ restraint. The eye reads the brightness shift as "this
1854
+ mark lights up under attention" without crossing into
1855
+ "this mark is now glowing".
1856
+ Implementation: className extends transition-transform
1857
+ → transition-[transform,filter] so the brightness
1858
+ tweens at the same 200ms ease-out cadence as the scale
1859
+ + rotate axes — one motion-coherent 3-property hover
1860
+ lift on the className tier (plus the inline color 200ms
1861
+ transition for theme-toggle eases).
1862
+ Brand-mark family axis count: 4 hover-state axes
1863
+ cleanly factor across:
1864
+ geometry (scale + rotate)
1865
+ paint (opacity breath + brightness on hover)
1866
+ Cluster reads as "alive, lifting, and lighting up under
1867
+ attention" — three independent gesture vocabularies on
1868
+ one surface.
1869
+ data-topo-brand-logo-hover-brightness attr surfaces
1870
+ the landing value for tests. */}
1726
1871
  <svg
1727
1872
  width="40" height="40" viewBox="0 0 32 32" aria-hidden
1728
- className="shrink-0"
1873
+ className={`shrink-0 transition-[transform,filter] duration-200 ease-out hover:scale-105 hover:rotate-6 hover:brightness-110 transform-gpu${!reducedMotion ? ' anet-topo-brand-logo-breath' : ''}`}
1729
1874
  data-topo-brand-logo
1875
+ data-topo-brand-logo-hover-scale="1.05"
1876
+ data-topo-brand-logo-hover-rotate="6deg"
1877
+ data-topo-brand-logo-hover-brightness="1.1"
1878
+ data-topo-brand-logo-breath={!reducedMotion ? 'true' : 'false'}
1730
1879
  style={{
1731
1880
  color: isLight ? '#0d9488' : '#67e8f9',
1732
- transition: 'color 200ms ease-out',
1881
+ cursor: 'default',
1882
+ // R557 — extend transition list to include filter (and
1883
+ // re-spec transform for cadence parity) so the new
1884
+ // hover:brightness-110 axis eases at 200ms alongside
1885
+ // the existing color 200ms (theme-toggle ease) and the
1886
+ // className-based hover:scale-105 / hover:rotate-6.
1887
+ // Inline transition is a shorthand and overrides the
1888
+ // className's transition-[transform,filter] — listing
1889
+ // all axes here ensures the eased property set covers
1890
+ // color (theme) + transform (scale + rotate) + filter
1891
+ // (brightness) at uniform 200ms ease-out.
1892
+ transition: 'color 200ms ease-out, transform 200ms ease-out, filter 200ms ease-out',
1733
1893
  }}
1734
1894
  >
1735
1895
  <mask id="s2a-titleblock-moon-mask">
@@ -1777,7 +1937,40 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1777
1937
  R300 marks the milestone of 25 rounds (R275-R300) of
1778
1938
  continuous TopoGraph polish + codex's Vincent 5215/
1779
1939
  5222 logo asset+integration work. */}
1780
- <div className="text-xs uppercase text-gray-500 tracking-widest leading-tight font-medium" data-topo-section-kicker>Network Topology</div>
1940
+ {/* Round 555 / Loop kicker "Network Topology" gains group-
1941
+ hover affordance via the R554 wrapper's `group` flag,
1942
+ closing the title-block cluster's hover coverage at 3
1943
+ surfaces (brand logo + H2 + kicker).
1944
+ Picks up the small-label SPREAD direction (R554 banked
1945
+ "small labels SPREAD on hover / large headlines TIGHTEN"
1946
+ — kicker is xs uppercase, definitely a small label) plus
1947
+ a color brighten (text-gray-500 #6b7280 → text-gray-400
1948
+ #9ca3af).
1949
+ Spread: tracking-widest (0.1em rest) → 0.13em hover —
1950
+ +30% kerning bump. At text-xs (12px) the per-gap shift
1951
+ is 1.2px → 1.56px (+0.36px/gap), legible without
1952
+ overshooting the rest's tracking-widest editorial base.
1953
+ Color: text-gray-500 → text-gray-400 — one tier lighter,
1954
+ same idiom as R296 (kicker rest tone-up from gray-600 to
1955
+ gray-500), now extended at the hover-state tier.
1956
+ transition-[letter-spacing,color] duration-200 ease-out
1957
+ matches the 200ms cadence of R554 H2 ls + the rest of
1958
+ the hover-ls family (R344/R345/R347/R351/R420/R427/R431/
1959
+ R432/R434/R527/R539).
1960
+ Title-block cluster signature post-R555 (3 surfaces):
1961
+ brand logo loud scale + rotate + breath
1962
+ (R548/R549/R553)
1963
+ H2 subtle tracking-tighter
1964
+ (R554, editorial-tighten)
1965
+ kicker subtle tracking-spread + color lift
1966
+ (R555, data-spread) ← this round
1967
+ Two of the three surfaces are typographic; the brand
1968
+ logo carries the geometric+chromatic motion. Cluster
1969
+ reads as ONE coherent hover unit through three
1970
+ independent gesture vocabularies.
1971
+ data-topo-section-kicker-hover-tracking + -hover-color
1972
+ attrs expose the landing values for tests. */}
1973
+ <div className="text-xs uppercase text-gray-500 group-hover:text-gray-400 tracking-widest group-hover:tracking-[0.13em] transition-[letter-spacing,color] duration-200 ease-out leading-tight font-medium" data-topo-section-kicker data-topo-section-kicker-hover-tracking="0.13em" data-topo-section-kicker-hover-color="text-gray-400">Network Topology</div>
1781
1974
  {/* Round 286 / Loop: title 'Command mesh' adopts tracking-tight
1782
1975
  (-0.025em) to complement R285 kicker tracking-widest. Wide
1783
1976
  eyebrow + tight headline is the conventional editorial
@@ -1789,7 +1982,62 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1789
1982
  cumulatively legible across 12 characters. font-semibold
1790
1983
  (600) stays — tracking-tight does the heavy lifting for
1791
1984
  the editorial register. */}
1792
- <h2 className="text-lg text-white font-semibold leading-tight tracking-tight" data-topo-section-title>Command mesh</h2>
1985
+ {/* Round 554 / Loop — H2 "Command mesh" gains group-hover-
1986
+ gated tracking-tighter via the wrapper's R554 `group`
1987
+ flag. Pre-R554 the H2 was fully static (R286 tracking-
1988
+ tight only at rest). R554 adds an editorial-tighten
1989
+ gesture: when cursor sweeps anywhere across the title
1990
+ cluster (brand logo OR title text), the headline tightens
1991
+ from -0.025em → -0.05em.
1992
+ Inverts the typical hover-letter-spacing direction:
1993
+ small labels (chip counts, panel titles, edge digits)
1994
+ SPREAD on hover → "data telegraphing"
1995
+ large headlines (h2 Command mesh)
1996
+ TIGHTEN on hover → "editorial emphasis"
1997
+ Both directions are coherent design language — small
1998
+ data wants spacing for legibility; large headlines want
1999
+ tightening for designed-headline polish. Same idiom as
2000
+ the conventional editorial pairing of "wide kicker +
2001
+ tight headline" R285/R286 set up (kicker spreads 0.1em
2002
+ tracking-widest; headline tightens -0.025em tracking-
2003
+ tight) — R554 deepens that pairing's tighten side at
2004
+ the hover-state tier.
2005
+ At text-lg (18px) the shift is -0.45px → -0.9px per
2006
+ gap (~5.4px total tightening across "Command mesh" 12
2007
+ chars). Subtle but legible when the cursor sweeps in.
2008
+ transition-[letter-spacing] duration-200 ease-out
2009
+ matches the 200ms hover-ls cadence used at R344/R345/
2010
+ R347/R351/R420/R427/R431/R432/R434/R527/R539 family
2011
+ anchors.
2012
+ data-topo-section-title-hover-tracking attr surfaces
2013
+ the landing tracking class for tests. */}
2014
+ {/* Round 556 / Loop — H2 "Command mesh" gains a 2nd
2015
+ editorial-emphasis axis: group-hover:font-bold paired
2016
+ with R554's group-hover:tracking-tighter. Both lifts
2017
+ fire on the same R554 wrapper's `group` flag (hover
2018
+ anywhere in the title cluster → BOTH H2 axes intensify
2019
+ simultaneously).
2020
+ H2 hover signature post-R556 (2 typographic axes
2021
+ intensify together):
2022
+ rest font-semibold 600 + tracking-tight -0.025em
2023
+ hover font-bold 700 + tracking-tighter -0.05em
2024
+ Editorial emphasis through TWO axes — heavier AND
2025
+ tighter on hover. Mirrors the conventional "designed-
2026
+ headline emphasis" idiom (heavier + tighter = more
2027
+ authoritative; the eye reads both axes as intensifying
2028
+ the same semantic).
2029
+ Hover-fw family extension (6 anchors now):
2030
+ R416 chip-row count digit (chip group-hover)
2031
+ R420 chrome zoom-level (hover)
2032
+ R425 hub-center digit (hub hover)
2033
+ R520 +N more flows footer (recent panel hover)
2034
+ R521 chrome nodeSize S/M/L (inactive hover)
2035
+ R556 title-block H2 (cluster group-hover) ← this round
2036
+ Transition list extends to include 'font-weight 200ms
2037
+ ease-out' alongside the existing 'letter-spacing'
2038
+ 200ms cadence. data-topo-section-title-hover-fw attr
2039
+ surfaces the landing weight for tests. */}
2040
+ <h2 className="text-lg text-white font-semibold group-hover:font-bold leading-tight tracking-tight group-hover:tracking-tighter transition-[letter-spacing,font-weight] duration-200 ease-out" data-topo-section-title data-topo-section-title-hover-tracking="tracking-tighter" data-topo-section-title-hover-fw="700">Command mesh</h2>
1793
2041
  </div>
1794
2042
  </div>
1795
2043
  {/* Round 328 / Loop: chip-row strip wrapper gap 2 → 2.5
@@ -1959,8 +2207,61 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1959
2207
  // overlays the release-pop. Matching `transform-gpu`
1960
2208
  // promotes the layer so the scale doesn't trigger
1961
2209
  // layout/paint thrash. Sibling change on Grid below.
1962
- className={`px-2.5 py-1 focus:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset hover:tracking-wide active:scale-95 transform-gpu ${layout === 'ring' ? 'bg-cyan-500/15 text-cyan-300 font-medium hover:bg-cyan-500/20 active:bg-cyan-500/25' : 'text-gray-400 hover:text-cyan-300 hover:bg-cyan-500/5 active:bg-cyan-500/15'} ${chromePopping === 'layout-ring' ? ' anet-chrome-pop' : ''}`}
1963
- style={{ transition: 'background-color 150ms ease, color 150ms ease, letter-spacing 200ms ease-out, transform 150ms ease-out' }}
2210
+ /* Round 522 / Loop extends R521's typography-preview
2211
+ idiom (chrome nodeSize hover:font-medium 400 500) to
2212
+ the Ring/Grid layout toggle's inactive variant. Pre-
2213
+ R522 the inactive Ring/Grid had `hover:text-cyan-300
2214
+ hover:bg-cyan-500/5` (R270 color + bg previews of the
2215
+ active state) but no typography preview — the active
2216
+ variant uses `font-medium` (fw 500), inactive sat at
2217
+ default fw 400 even on hover. R522 adds `hover:font-
2218
+ medium` to the inactive Ring/Grid so the rest-vs-hover
2219
+ transition previews the typography state the click
2220
+ would commit to, matching the click commits's locked
2221
+ weight.
2222
+ font-weight 150ms appended to the transition list
2223
+ matching the existing 150ms color/bg cadence at this
2224
+ button — when hover lifts color (gray-400 → cyan-300)
2225
+ + bg (transparent → cyan-500/5) + fw (400 → 500), all
2226
+ 3 ease at the same 150ms beat.
2227
+ Hover-fw family extension (6 anchors): R416/R420/R425/
2228
+ R520/R521/R522. R522 closes the chrome toggle group
2229
+ typography preview at the last remaining toggle —
2230
+ layout (Ring/Grid). After R521 (nodeSize) + R522
2231
+ (layout), every multi-state chrome toggle has hover-
2232
+ fw preview on its inactive variant.
2233
+ data-topo-chrome-layout-hover-preview-fw="500" attr
2234
+ on inactive button exposes the polish for tests. */
2235
+ /* Round 552 / Loop — chrome active-variant gains hover:
2236
+ text-cyan-200, lifting text one brightness tier alongside
2237
+ the existing hover:bg-cyan-500/20 bg deepen. Coordinated
2238
+ 4-anchor edit (replace_all touched 4 sibling lines sharing
2239
+ the identical active-variant className substring):
2240
+ Ring (this line) layout === 'ring'
2241
+ Grid (line ~2097) layout === 'grid'
2242
+ S/M/L (line ~12635) nodeScale === v
2243
+ Fscrn (line ~13030) isFullscreen
2244
+ Pre-R552 the active variant's hover state only deepened bg
2245
+ (cyan-500/15 → /20); text stayed planted at cyan-300. The
2246
+ inactive variant already lifts text on hover (text-gray-400
2247
+ → text-cyan-300). R552 brings parity: active variant lifts
2248
+ text one tier brighter (cyan-300 → cyan-200) on hover,
2249
+ mirroring the inactive variant's "text brightens on hover"
2250
+ gesture at the next brightness step.
2251
+ Brightness ladder snapshot (cyan):
2252
+ cyan-400 brand chrome focus ring
2253
+ cyan-300 active-variant rest ←─┐
2254
+ │ +1 tier on hover
2255
+ cyan-200 active-variant hover ←─┘ (this round)
2256
+ Pure paint axis (text color); bbox/geometry unchanged.
2257
+ transition-colors already in the class list so the cyan-
2258
+ 300 → cyan-200 swap eases at the existing 200ms cadence.
2259
+ hover-color brighten family extension at the chrome strip
2260
+ active-variant scope; sibling to the inactive variant's
2261
+ R163/R178/R179/R270 hover:text-cyan-300 idiom. */
2262
+ className={`px-2.5 py-1 focus:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset hover:tracking-wide active:scale-95 transform-gpu ${layout === 'ring' ? 'bg-cyan-500/15 text-cyan-300 font-medium hover:bg-cyan-500/20 hover:text-cyan-200 active:bg-cyan-500/25' : 'text-gray-400 hover:text-cyan-300 hover:bg-cyan-500/5 active:bg-cyan-500/15 hover:font-medium'} ${chromePopping === 'layout-ring' ? ' anet-chrome-pop' : ''}`}
2263
+ data-topo-chrome-layout-hover-preview-fw={layout === 'ring' ? null : '500'}
2264
+ style={{ transition: 'background-color 150ms ease, color 150ms ease, letter-spacing 200ms ease-out, transform 150ms ease-out, font-weight 150ms ease' }}
1964
2265
  >
1965
2266
  Ring
1966
2267
  </button>
@@ -1982,7 +2283,12 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1982
2283
  // R492 sibling — Grid button picks up active:scale-95
1983
2284
  // press feedback + transform in transition list. Same
1984
2285
  // vocabulary as Ring above.
1985
- className={`px-2.5 py-1 border-l focus:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset hover:tracking-wide active:scale-95 transform-gpu ${layout === 'grid' ? 'bg-cyan-500/15 text-cyan-300 font-medium hover:bg-cyan-500/20 active:bg-cyan-500/25' : 'text-gray-400 hover:text-cyan-300 hover:bg-cyan-500/5 active:bg-cyan-500/15'} ${chromePopping === 'layout-grid' ? ' anet-chrome-pop' : ''}`}
2286
+ /* Round 522 sibling Grid button mirrors Ring above:
2287
+ inactive variant gains `hover:font-medium` typography
2288
+ preview + font-weight 150ms in inline transition list.
2289
+ Same idiom, same family (R522 chrome layout). */
2290
+ className={`px-2.5 py-1 border-l focus:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset hover:tracking-wide active:scale-95 transform-gpu ${layout === 'grid' ? 'bg-cyan-500/15 text-cyan-300 font-medium hover:bg-cyan-500/20 hover:text-cyan-200 active:bg-cyan-500/25' : 'text-gray-400 hover:text-cyan-300 hover:bg-cyan-500/5 active:bg-cyan-500/15 hover:font-medium'} ${chromePopping === 'layout-grid' ? ' anet-chrome-pop' : ''}`}
2291
+ data-topo-chrome-layout-hover-preview-fw={layout === 'grid' ? null : '500'}
1986
2292
  /* Round 268 / Loop: Grid button's left border (the
1987
2293
  internal divider between Ring and Grid) picks up
1988
2294
  pal.containerBorder, matching the wrapper change at
@@ -1995,7 +2301,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1995
2301
  keeps R268's theme-toggle smoothness intact.
1996
2302
  R492 adds `transform 150ms ease-out` so active:scale-95
1997
2303
  eases smoothly. */
1998
- style={{ borderColor: pal.containerBorder, transition: 'background-color 150ms ease, color 150ms ease, border-color 200ms ease-out, letter-spacing 200ms ease-out, transform 150ms ease-out' }}
2304
+ style={{ borderColor: pal.containerBorder, transition: 'background-color 150ms ease, color 150ms ease, border-color 200ms ease-out, letter-spacing 200ms ease-out, transform 150ms ease-out, font-weight 150ms ease' }}
1999
2305
  >
2000
2306
  Grid
2001
2307
  </button>
@@ -2206,7 +2512,26 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2206
2512
  at the chip-count scope. Sibling edits on the
2207
2513
  online + active-links chip digits below. data-
2208
2514
  working-chip-digit attr exposes the digit span. */}
2209
- <span className="font-semibold transition-[font-weight] duration-200 group-hover:font-bold" data-working-chip-digit>{workingCount}</span><span className="opacity-70 transition-opacity duration-200 group-hover:opacity-100" data-working-chip-unit> working</span>
2515
+ {/* Round 539 / Loop chip-row digit gains group-hover:
2516
+ tracking-wide alongside the existing R362 group-
2517
+ hover:font-bold. Pre-R539 the chip digit lifted
2518
+ only on the font-weight axis (600 → 700 on chip
2519
+ hover); R539 adds the kerning axis (tracking
2520
+ normal → tracking-wide ≈ 0.025em ≈ 0.3px on a 12px
2521
+ digit) so hover lifts BOTH typography axes
2522
+ together — same idiom R420/R517 establish at the
2523
+ chrome zoom-level (letter-spacing + fontWeight
2524
+ hover delta) and R531/R530 mirror at the panel
2525
+ label scope. transition-[font-weight] extends to
2526
+ transition-[font-weight,letter-spacing] for the
2527
+ smooth dual-axis tween.
2528
+ Sibling treatment across the 3 chip-row digits
2529
+ (working / online / active-links) — single concept
2530
+ replicated at 3 surfaces by replace_all.
2531
+ Hover-letter-spacing family extension (12 anchors
2532
+ now): R344/R345/R347/R420/R427/R431/R432/R433/
2533
+ R434/R517/R518 + R539 (this round). */}
2534
+ <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 duration-200 group-hover:opacity-100" data-working-chip-unit> working</span>
2210
2535
  </span>
2211
2536
  <span
2212
2537
  // Round 201 / Loop: online chip — mirror of the working
@@ -2277,7 +2602,9 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2277
2602
  >
2278
2603
  {/* R337 sibling — online chip unit demotion. */}
2279
2604
  {/* R362 sibling — online-chip digit gains font-semibold. */}
2280
- <span className="font-semibold transition-[font-weight] duration-200 group-hover:font-bold" data-online-chip-digit>{onlineNodes.length}</span><span className="opacity-70 transition-opacity duration-200 group-hover:opacity-100" data-online-chip-unit> online</span>
2605
+ {/* R539 sibling online chip digit. Same idiom as
2606
+ working chip above (group-hover:tracking-wide). */}
2607
+ <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 duration-200 group-hover:opacity-100" data-online-chip-unit> online</span>
2281
2608
  </span>
2282
2609
  </>
2283
2610
  );
@@ -2366,7 +2693,41 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2366
2693
  height: '100%',
2367
2694
  cursor: 'pointer',
2368
2695
  boxShadow: isPinned ? `inset 0 0 0 1px ${color}, inset 0 0 0 2px rgba(255,255,255,0.6)` : undefined,
2369
- filter: hoveredStatus === key ? 'brightness(1.2)' : undefined,
2696
+ /* Round 542 / Loop pressure-bar segments gain
2697
+ drop-shadow tier-color glow on hover, stacked
2698
+ on R210 brightness(1.2). Sibling to R537 legend
2699
+ swatch + R541 vendor chip glow at the chip-row
2700
+ scope — three same-pattern surfaces (legend
2701
+ swatch / vendor chip / pressure segment) all
2702
+ radiate their identity color on hover.
2703
+ 3rd anchor in the chip-row tier-color paint
2704
+ glow sub-family:
2705
+ R537 legend swatch row.fill (status hex)
2706
+ R541 vendor chip v.color (hsl via color-mix)
2707
+ R542 pressure seg color (status hex) ← this round
2708
+ Stacked filter syntax (brightness + drop-shadow
2709
+ in same filter declaration): `brightness(1.2)
2710
+ drop-shadow(...)`. CSS filter supports multiple
2711
+ functions; they apply left-to-right. Brightness
2712
+ boosts the segment's own color, drop-shadow
2713
+ paints the outer halo. Together: hovered seg
2714
+ looks "lit up" with both inner glow + outer
2715
+ halo in its tier color.
2716
+ Hue: `${color}99` hex+alpha (60%) — color here
2717
+ is a 6-char hex (e.g., '#22c55e' for working
2718
+ cyber, '#0d9488' for idle light), not hsl, so
2719
+ hex+alpha concat works (unlike R541 vendor
2720
+ which needed color-mix for hsl). Banked
2721
+ pattern: hex sources use hex+alpha; hsl/color()
2722
+ sources use color-mix.
2723
+ 2px blur (vs R537's 3px) since pressure-seg is
2724
+ small (h-2 = 8px tall, variable width) — a
2725
+ smaller blur keeps the glow tight to the
2726
+ segment without bleeding into neighbors.
2727
+ filter is paint-only; bbox unchanged; R51
2728
+ overlap-test invariants hold. Transition list
2729
+ already includes `filter` (post-R524). */
2730
+ filter: hoveredStatus === key ? `brightness(1.2) drop-shadow(0 0 2px ${color}99)` : undefined,
2370
2731
  transition: 'width 220ms ease-out, box-shadow 150ms ease-out, filter 150ms ease-out',
2371
2732
  }}
2372
2733
  onClick={(e) => {
@@ -2502,9 +2863,42 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2502
2863
  // R355: `group` lets the inner opacity-70 spans (prefix
2503
2864
  // `filter:` + count `· N`) brighten to 100 % on pill hover.
2504
2865
  // Sibling treatment on group + vendor pills below.
2505
- className="group inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md font-mono font-medium text-xs border anet-fade-in anet-topo-chip-focus transition-transform duration-200 ease-out hover:-translate-y-px transform-gpu" data-topo-filter-pill-hover-lift="true"
2866
+ // R495 filter pills (3 sibling `group` variants) join the
2867
+ // active:scale-95 press-feedback family. R490's !important
2868
+ // transition list on .anet-topo-chip-focus already covers
2869
+ // transform, so just appending active:scale-95 to the
2870
+ // className wires the press tactile in one token. Compound
2871
+ // with R400-era hover:-translate-y-px gives lift-and-compress.
2872
+ 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"
2506
2873
  title={matchCount > 0 ? `${matchPreview}${matchSuffix} — click to clear` : 'Click to clear filter'}
2507
2874
  onClick={() => setPinnedStatus(null)}
2875
+ /* Round 543 / Loop — status filter pill gains always-on
2876
+ tier-color drop-shadow when rendered. Pre-R543 the
2877
+ pill carried bg-tint + tier-color text + border but
2878
+ no outer paint extent — it sat as a flat tinted chip
2879
+ in the chip row. R543 adds an outer glow at the
2880
+ pill's text color so the pill radiates a soft tier-
2881
+ colored halo signaling "this filter is active." Pin
2882
+ pill only renders when pinnedStatus is set (the JSX
2883
+ gate above), so the drop-shadow appearing reinforces
2884
+ the visual "active pin" state.
2885
+ Sibling pattern: R477 legend pin-ring also paints a
2886
+ pin-gated tier-color drop-shadow. Pin pill follows
2887
+ the same "pin-gated paint glow" semantics but at the
2888
+ chip-row scope vs the panel-row scope. The chip-row
2889
+ tier-color glow trio (R537/R541/R542 hover-gated)
2890
+ plus R543 (pin-gated, this round) closes the chip-
2891
+ row paint-glow family across BOTH gate types
2892
+ (hover for transient affordance, pin for sticky
2893
+ active-state visual).
2894
+ Hue: explicit tier color (extracted from the
2895
+ existing `color` ternary). 0x99 alpha (~60%) +
2896
+ 3px blur. Stays inside the same color hierarchy
2897
+ as the pill's own text/border (currentColor).
2898
+ R543 status pill scope only — R543's pattern can
2899
+ future-extend to group/vendor/edge filter pills
2900
+ (3 more variants at lines 2683/2755/2824). Out of
2901
+ scope to keep R543 single-pill. */
2508
2902
  style={{
2509
2903
  background: pinnedStatus === 'working' ? (isLight ? '#05966914' : '#22c55e1f')
2510
2904
  : pinnedStatus === 'idle' ? (isLight ? '#0d948814' : '#2dd4bf1f')
@@ -2514,6 +2908,11 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2514
2908
  : (isLight ? '#475569' : '#9ca3af'),
2515
2909
  borderColor: 'currentColor',
2516
2910
  cursor: 'pointer',
2911
+ filter: `drop-shadow(0 0 3px ${
2912
+ pinnedStatus === 'working' ? (isLight ? '#047857' : '#86efac')
2913
+ : pinnedStatus === 'idle' ? (isLight ? '#0f766e' : '#5eead4')
2914
+ : (isLight ? '#475569' : '#9ca3af')
2915
+ }99)`,
2517
2916
  }}
2518
2917
  >
2519
2918
  {/* Round 412 / Loop: filter pin pill VALUE picks up the
@@ -2546,7 +2945,24 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2546
2945
  inline-block is default for <button> so no display
2547
2946
  tweak needed. replace_all covers all 4 filter pin
2548
2947
  pills (status / group / vendor / edge) at once. */
2549
- className="ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 transform-gpu"
2948
+ /* Round 547 / Loop — extends pill × close-button hover
2949
+ gesture from scale-110 (R356) + opacity-70 to ALSO
2950
+ include rotate-12 on hover. Pre-R547 the × dimmed
2951
+ and grew on hover; R547 adds a 12° twist so the
2952
+ close action telegraphs "discarding/spinning away"
2953
+ with a small delight gesture. Composes with
2954
+ transition-transform (existing) — Tailwind's
2955
+ hover:rotate-12 + hover:scale-110 stack into one
2956
+ transform under the same 200ms ease-out tween.
2957
+ Applied to all 4 pill × buttons (status / group /
2958
+ vendor / edge) via replace_all since the className
2959
+ is identical. Closes the pill × hover gesture
2960
+ vocabulary at 3 axes:
2961
+ hover:opacity-70 paint dim
2962
+ hover:scale-110 geometry grow (R356)
2963
+ hover:rotate-12 geometry twist (R547, this round)
2964
+ Hover-gesture parity across the 4-pill family. */
2965
+ className="ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 hover:rotate-12 transform-gpu"
2550
2966
  style={{ background: 'transparent', color: 'inherit', cursor: 'pointer', padding: 0 }}
2551
2967
  >×</button>
2552
2968
  </span>
@@ -2566,14 +2982,47 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2566
2982
  data-filter-match-count={matchCount}
2567
2983
  data-filter-match-aliases={matchAliases.join(',')}
2568
2984
  // R355 sibling — `group` parent + group-hover on inner spans.
2569
- className="group inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md font-mono font-medium text-xs border anet-fade-in anet-topo-chip-focus transition-transform duration-200 ease-out hover:-translate-y-px transform-gpu" data-topo-filter-pill-hover-lift="true"
2985
+ // R495 filter pills (3 sibling `group` variants) join the
2986
+ // active:scale-95 press-feedback family. R490's !important
2987
+ // transition list on .anet-topo-chip-focus already covers
2988
+ // transform, so just appending active:scale-95 to the
2989
+ // className wires the press tactile in one token. Compound
2990
+ // with R400-era hover:-translate-y-px gives lift-and-compress.
2991
+ 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"
2570
2992
  title={matchCount > 0 ? `${matchPreview}${matchSuffix} — click to clear` : 'Click to clear filter'}
2571
2993
  onClick={() => setPinnedGroup(null)}
2994
+ /* Round 544 / Loop — extends R543 pin-active filter-pill
2995
+ drop-shadow pattern to the GROUP pill (2nd of 4 pill
2996
+ variants). Pre-R544 the group pill carried bg-tint +
2997
+ pal.legendAccent text/border but no outer paint glow.
2998
+ R544 adds the matching cyan-accent drop-shadow so the
2999
+ group pin pill radiates the same paint glow as R543
3000
+ status pill — pin-active visual signal at chip-row
3001
+ scope.
3002
+ Hue: pal.legendAccent (cyber #67e8f9 cyan-300 /
3003
+ light #0d9488 teal-600). Uses color-mix() syntax
3004
+ because pal.legendAccent may resolve to hex; same
3005
+ syntax works for both hex and hsl sources (banked
3006
+ R541 lesson). 60% alpha + 3px blur — same intensity
3007
+ as R543 status pill so the pin-active visual signal
3008
+ reads with matching brightness across pill variants.
3009
+ Pin-active tier-color paint glow sub-family
3010
+ (CLOSED progressively):
3011
+ R477 legend pin-ring (panel-row, row.fill)
3012
+ R543 status pill (chip-row, tier-color)
3013
+ R544 group pill (chip-row, legendAccent)
3014
+ ← this round
3015
+ Out of scope: vendor pill (line ~2755) + edge pill
3016
+ (line ~2824) — can future-extend in subsequent
3017
+ rounds (R545/R546). Both use the same R543 idiom:
3018
+ always-on drop-shadow when rendered, color from the
3019
+ pill's existing text color. */
2572
3020
  style={{
2573
3021
  background: isLight ? '#67e8f914' : '#67e8f91f',
2574
3022
  color: pal.legendAccent,
2575
3023
  borderColor: 'currentColor',
2576
3024
  cursor: 'pointer',
3025
+ filter: `drop-shadow(0 0 3px color-mix(in srgb, ${pal.legendAccent} 60%, transparent))`,
2577
3026
  }}
2578
3027
  >
2579
3028
  {/* R412: see status pill above — filter value fw=600 data tier. */}
@@ -2596,7 +3045,24 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2596
3045
  inline-block is default for <button> so no display
2597
3046
  tweak needed. replace_all covers all 4 filter pin
2598
3047
  pills (status / group / vendor / edge) at once. */
2599
- className="ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 transform-gpu"
3048
+ /* Round 547 / Loop — extends pill × close-button hover
3049
+ gesture from scale-110 (R356) + opacity-70 to ALSO
3050
+ include rotate-12 on hover. Pre-R547 the × dimmed
3051
+ and grew on hover; R547 adds a 12° twist so the
3052
+ close action telegraphs "discarding/spinning away"
3053
+ with a small delight gesture. Composes with
3054
+ transition-transform (existing) — Tailwind's
3055
+ hover:rotate-12 + hover:scale-110 stack into one
3056
+ transform under the same 200ms ease-out tween.
3057
+ Applied to all 4 pill × buttons (status / group /
3058
+ vendor / edge) via replace_all since the className
3059
+ is identical. Closes the pill × hover gesture
3060
+ vocabulary at 3 axes:
3061
+ hover:opacity-70 paint dim
3062
+ hover:scale-110 geometry grow (R356)
3063
+ hover:rotate-12 geometry twist (R547, this round)
3064
+ Hover-gesture parity across the 4-pill family. */
3065
+ className="ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 hover:rotate-12 transform-gpu"
2600
3066
  style={{ background: 'transparent', color: 'inherit', cursor: 'pointer', padding: 0 }}
2601
3067
  >×</button>
2602
3068
  </span>
@@ -2632,14 +3098,40 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2632
3098
  data-filter-match-count={matchCount}
2633
3099
  data-filter-match-aliases={matchAliases.join(',')}
2634
3100
  // R355 sibling — `group` parent + group-hover on inner spans.
2635
- className="group inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md font-mono font-medium text-xs border anet-fade-in anet-topo-chip-focus transition-transform duration-200 ease-out hover:-translate-y-px transform-gpu" data-topo-filter-pill-hover-lift="true"
3101
+ // R495 filter pills (3 sibling `group` variants) join the
3102
+ // active:scale-95 press-feedback family. R490's !important
3103
+ // transition list on .anet-topo-chip-focus already covers
3104
+ // transform, so just appending active:scale-95 to the
3105
+ // className wires the press tactile in one token. Compound
3106
+ // with R400-era hover:-translate-y-px gives lift-and-compress.
3107
+ 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"
2636
3108
  title={matchCount > 0 ? `${matchPreview}${matchSuffix} — click to clear` : 'Click to clear vendor filter'}
2637
3109
  onClick={() => setPinnedVendor(null)}
3110
+ /* Round 545 / Loop — extends pin-active filter-pill drop-
3111
+ shadow pattern to VENDOR pill (3rd of 4 pill variants
3112
+ after R543 status + R544 group). vendorColor is HSL
3113
+ format (banked R541 lesson — vendorDist.color sources
3114
+ from mono.text in vendorIdentity.ts, which is `hsl(...)`),
3115
+ so the filter uses color-mix() syntax — same as R544.
3116
+ 60% alpha + 3px blur, matching R543/R544 intensity for
3117
+ consistent pin-active visual signal across all pill
3118
+ variants.
3119
+ Pin-active tier-color paint glow sub-family (progressive
3120
+ extension, 1 pill variant remaining):
3121
+ R477 legend pin-ring (panel-row, row.fill, hex+alpha)
3122
+ R543 status pill (chip-row, tier-color, hex+alpha)
3123
+ R544 group pill (chip-row, legendAccent, color-mix)
3124
+ R545 vendor pill (chip-row, vendorColor, color-mix)
3125
+ ← this round
3126
+ Out of scope: edge pill (line ~2824 pre-R545, now ~2900+).
3127
+ Final 1/4 pill remaining for a future round closes the
3128
+ sub-family. */
2638
3129
  style={{
2639
3130
  background: `${vendorColor}1f`,
2640
3131
  color: vendorColor,
2641
3132
  borderColor: 'currentColor',
2642
3133
  cursor: 'pointer',
3134
+ filter: `drop-shadow(0 0 3px color-mix(in srgb, ${vendorColor} 60%, transparent))`,
2643
3135
  }}
2644
3136
  >
2645
3137
  {/* R412: see status pill above — filter value fw=600 data tier. */}
@@ -2662,7 +3154,24 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2662
3154
  inline-block is default for <button> so no display
2663
3155
  tweak needed. replace_all covers all 4 filter pin
2664
3156
  pills (status / group / vendor / edge) at once. */
2665
- className="ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 transform-gpu"
3157
+ /* Round 547 / Loop — extends pill × close-button hover
3158
+ gesture from scale-110 (R356) + opacity-70 to ALSO
3159
+ include rotate-12 on hover. Pre-R547 the × dimmed
3160
+ and grew on hover; R547 adds a 12° twist so the
3161
+ close action telegraphs "discarding/spinning away"
3162
+ with a small delight gesture. Composes with
3163
+ transition-transform (existing) — Tailwind's
3164
+ hover:rotate-12 + hover:scale-110 stack into one
3165
+ transform under the same 200ms ease-out tween.
3166
+ Applied to all 4 pill × buttons (status / group /
3167
+ vendor / edge) via replace_all since the className
3168
+ is identical. Closes the pill × hover gesture
3169
+ vocabulary at 3 axes:
3170
+ hover:opacity-70 paint dim
3171
+ hover:scale-110 geometry grow (R356)
3172
+ hover:rotate-12 geometry twist (R547, this round)
3173
+ Hover-gesture parity across the 4-pill family. */
3174
+ className="ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 hover:rotate-12 transform-gpu"
2666
3175
  style={{ background: 'transparent', color: 'inherit', cursor: 'pointer', padding: 0 }}
2667
3176
  >×</button>
2668
3177
  </span>
@@ -2695,14 +3204,40 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2695
3204
  data-filter-match-count={link.count}
2696
3205
  data-filter-match-aliases={`${link.from},${link.to}`}
2697
3206
  data-active-filter-edge-hot={isHot ? 'true' : 'false'}
2698
- className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md font-mono font-medium text-xs border anet-fade-in anet-topo-chip-focus transition-transform duration-200 ease-out hover:-translate-y-px transform-gpu" data-topo-filter-pill-hover-lift="true"
3207
+ // R495 sibling 4th filter pill (no `group` prefix variant)
3208
+ // joins active:scale-95 press family alongside the 3 group
3209
+ // variants above. Same recipe.
3210
+ 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"
2699
3211
  title={`${link.from} → ${link.to} (${link.count} msg${link.count === 1 ? '' : 's'}${isHot ? ', hot lane · ≥ 10' : ''}) — click to clear`}
2700
3212
  onClick={() => setPinnedEdgeKey(null)}
3213
+ /* Round 546 / Loop — CLOSES pin-active filter-pill drop-
3214
+ shadow sub-family at the 4th and final pill variant
3215
+ (edge pill). R543 (status) + R544 (group) + R545
3216
+ (vendor) covered the first three; R546 closes at
3217
+ edge.
3218
+ Pin-active tier-color paint glow sub-family CLOSED
3219
+ (4 anchors):
3220
+ R477 legend pin-ring (panel-row, row.fill)
3221
+ R543 status pill (chip-row, tier-color text)
3222
+ R544 group pill (chip-row, legendAccent)
3223
+ R545 vendor pill (chip-row, vendorColor)
3224
+ R546 edge pill (chip-row, pal.flowEdge)
3225
+ ← this round, family CLOSED
3226
+ All 4 filter pin pills now radiate paint glow in the
3227
+ same hue family as their text/border on render —
3228
+ pin-active visual signal uniform across the chip-row
3229
+ pill family.
3230
+ pal.flowEdge is theme-driven (dynamic); color-mix
3231
+ syntax safe-defaults regardless of resolved format
3232
+ (banked R541/R544/R545 pattern). 60% alpha + 3px blur
3233
+ — same intensity as R543/R544/R545 for consistent
3234
+ cross-pill visual signal. */
2701
3235
  style={{
2702
3236
  background: isLight ? `${pal.flowEdge}14` : `${pal.flowEdge}1f`,
2703
3237
  color: pal.flowEdge,
2704
3238
  borderColor: 'currentColor',
2705
3239
  cursor: 'pointer',
3240
+ filter: `drop-shadow(0 0 3px color-mix(in srgb, ${pal.flowEdge} 60%, transparent))`,
2706
3241
  }}
2707
3242
  >
2708
3243
  {/* R412: filter pin pill value (edge variant) picks up fw=600.
@@ -2755,7 +3290,24 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2755
3290
  inline-block is default for <button> so no display
2756
3291
  tweak needed. replace_all covers all 4 filter pin
2757
3292
  pills (status / group / vendor / edge) at once. */
2758
- className="ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 transform-gpu"
3293
+ /* Round 547 / Loop — extends pill × close-button hover
3294
+ gesture from scale-110 (R356) + opacity-70 to ALSO
3295
+ include rotate-12 on hover. Pre-R547 the × dimmed
3296
+ and grew on hover; R547 adds a 12° twist so the
3297
+ close action telegraphs "discarding/spinning away"
3298
+ with a small delight gesture. Composes with
3299
+ transition-transform (existing) — Tailwind's
3300
+ hover:rotate-12 + hover:scale-110 stack into one
3301
+ transform under the same 200ms ease-out tween.
3302
+ Applied to all 4 pill × buttons (status / group /
3303
+ vendor / edge) via replace_all since the className
3304
+ is identical. Closes the pill × hover gesture
3305
+ vocabulary at 3 axes:
3306
+ hover:opacity-70 paint dim
3307
+ hover:scale-110 geometry grow (R356)
3308
+ hover:rotate-12 geometry twist (R547, this round)
3309
+ Hover-gesture parity across the 4-pill family. */
3310
+ className="ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 hover:rotate-12 transform-gpu"
2759
3311
  style={{ background: 'transparent', color: 'inherit', cursor: 'pointer', padding: 0 }}
2760
3312
  >×</button>
2761
3313
  </span>
@@ -3045,7 +3597,12 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3045
3597
  // — sibling to R355 filter-pill prefix/suffix + R414
3046
3598
  // chip-row unit brighten. Closes the inner-span
3047
3599
  // hover-brighten family at the vendor chip surface.
3048
- className="group tabular-nums font-medium inline-flex items-baseline gap-0.5 px-1 rounded anet-topo-chip-focus transition-transform duration-200 ease-out transform-gpu hover:-translate-y-px"
3600
+ // R496 vendor letter chip joins active:scale-95 press
3601
+ // family. Last vendor-row clickable joining the family
3602
+ // R495 cashed via R490's transition-cascade dividend.
3603
+ // Same compound w/ R401 hover-lift idiom — lift-and-
3604
+ // compress on press, springs back on release.
3605
+ className="group tabular-nums font-medium inline-flex items-baseline gap-0.5 px-1 rounded anet-topo-chip-focus transition-transform duration-200 ease-out transform-gpu hover:-translate-y-px active:scale-95"
3049
3606
  data-vendor-letter={v.initial}
3050
3607
  data-vendor-letter-count={v.count}
3051
3608
  data-vendor-letter-hover-lift="true"
@@ -3072,6 +3629,41 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3072
3629
  // for older browsers the chip falls back to its idle
3073
3630
  // transparent bg (graceful degradation — the canvas-
3074
3631
  // dim effect still fires regardless).
3632
+ /* Round 541 / Loop — vendor letter chip gains drop-
3633
+ shadow glow on hover/pin using its OWN vendor
3634
+ identity color (v.color). Sibling to R537 legend-
3635
+ swatch tier-color glow at the chip-row scope.
3636
+ Pre-R541 the vendor chip lifted on multiple
3637
+ axes (R354 inner-glyph scale-1.1, R202 bg color-
3638
+ mix tint, R180 box-shadow pin-mirror inset, R401
3639
+ hover-translate-y -1px, R496 active:scale-95
3640
+ press) but no paint-axis glow extending past
3641
+ the chip's bbox. R541 adds the outer glow at
3642
+ the paint axis so the vendor chip's identity
3643
+ color radiates beyond the chip on attention —
3644
+ same idiom as legend swatch tier-color glow.
3645
+ 2-tier alpha ladder (mirrors R538 group-label):
3646
+ pin (committed) v.color 99 (~60%)
3647
+ hover (preview) v.color 66 (~40%)
3648
+ rest none
3649
+ Pin is brighter to distinguish locked vs preview
3650
+ at the paint axis. The R180 inset box-shadow
3651
+ (pin-mirror) and R541 outer drop-shadow compose
3652
+ at pin — inside chrome reads as "this is pinned"
3653
+ (inset white double-ring), outside paint reads
3654
+ as "vendor identity is locked" (vendor-colour
3655
+ outer glow). Hover gets only the outer glow.
3656
+ 3px blur tuned to read as soft chip-halo without
3657
+ overwhelming adjacent chips in the chip row.
3658
+ filter property is in the .anet-topo-chip-focus
3659
+ class transition list (R524 banked fix), so the
3660
+ filter eases at 200ms naturally.
3661
+ Drop-shadow visual-polish family — R541 adds
3662
+ chip-row tier-color paint glow as another anchor
3663
+ in the same family pattern R537 established.
3664
+ data-vendor-glow attr ('pin' | 'hover' | 'false')
3665
+ exposes the gate state for tests. */
3666
+ data-vendor-glow={isPinned ? 'pin' : hoveredVendor === v.initial ? 'hover' : 'false'}
3075
3667
  style={{
3076
3668
  cursor: 'pointer',
3077
3669
  backgroundColor: (hoveredVendor === v.initial && !isPinned)
@@ -3080,7 +3672,12 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3080
3672
  boxShadow: isPinned
3081
3673
  ? `inset 0 0 0 1px ${v.color}, inset 0 0 0 2px rgba(255,255,255,0.45)`
3082
3674
  : undefined,
3083
- transition: 'box-shadow 150ms ease-out, background-color 200ms ease-out',
3675
+ filter: isPinned
3676
+ ? `drop-shadow(0 0 3px color-mix(in srgb, ${v.color} 60%, transparent))`
3677
+ : hoveredVendor === v.initial
3678
+ ? `drop-shadow(0 0 3px color-mix(in srgb, ${v.color} 40%, transparent))`
3679
+ : undefined,
3680
+ transition: 'box-shadow 150ms ease-out, background-color 200ms ease-out, filter 200ms ease-out',
3084
3681
  }}
3085
3682
  onMouseEnter={() => setHoveredVendor(v.initial)}
3086
3683
  onMouseLeave={() => setHoveredVendor(prev => prev === v.initial ? null : prev)}
@@ -3283,9 +3880,13 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3283
3880
  data-chip-hover-lift attr exposes the lift surface
3284
3881
  state ('true' clickable, 'false' empty) for tests. */
3285
3882
  // R414: `group` parent + inner unit span group-hover-brighten — see working chip above.
3883
+ // R496 — active-links chip joins active:scale-95 press
3884
+ // family. Sibling to working+online chips (R494). Gated
3885
+ // on `isInteractive` (flowLinks.length > 0) — same R399
3886
+ // conditional pattern used for hover-lift.
3286
3887
  className={`group tabular-nums font-medium hidden sm:inline px-2.5 py-1 rounded-md border anet-topo-chip-focus transition-transform duration-200 ease-out transform-gpu ${
3287
3888
  isInteractive
3288
- ? 'bg-gray-500/10 text-gray-400 border-gray-500/20 hover:bg-cyan-500/10 hover:text-cyan-200 hover:border-cyan-500/30 hover:-translate-y-px'
3889
+ ? 'bg-gray-500/10 text-gray-400 border-gray-500/20 hover:bg-cyan-500/10 hover:text-cyan-200 hover:border-cyan-500/30 hover:-translate-y-px active:scale-95'
3289
3890
  : 'bg-gray-500/10 text-gray-400 border-gray-500/20'
3290
3891
  }`}
3291
3892
  data-chip-hover-lift={isInteractive ? 'true' : 'false'}
@@ -3318,7 +3919,9 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3318
3919
  chip-internal-hierarchy arc. data-active-links-
3319
3920
  chip-unit exposes the unit span for tests. */}
3320
3921
  {/* R362 sibling — active-links chip digit gains font-semibold. */}
3321
- <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>
3922
+ {/* R539 sibling active-links chip digit. Same idiom
3923
+ as working + online above. */}
3924
+ <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 duration-200 group-hover:opacity-100" data-active-links-chip-unit> active link{flowLinks.length === 1 ? '' : 's'}</span>
3322
3925
  {rel ? (() => {
3323
3926
  // Round 161 / Loop: extend R160's recency-pip
3324
3927
  // vocabulary up one scope — from per-flow row to
@@ -3532,6 +4135,36 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3532
4135
  on the canvas root for non-visual consumers.
3533
4136
  Composed from existing onlineNodes / workingCount /
3534
4137
  offlineNodes / flowLinks — no new state. */
4138
+ /* Round 502 / Loop — categorical density-tier paired with the
4139
+ R469 numeric counts. data-topo-fleet-density-tier classifies
4140
+ the fleet size into 5 buckets so external consumers (CSS
4141
+ selectors, Playwright probes, future density-conditional
4142
+ polish gates like R109 dense-label collapse at 16+ nodes)
4143
+ can branch on a stable tier name without re-deriving the
4144
+ threshold logic from the raw numeric. Buckets:
4145
+ 'empty' — onlineNodes.length === 0
4146
+ 'sparse' — 1-3 nodes
4147
+ 'normal' — 4-15 nodes
4148
+ 'dense' — 16-30 nodes (matches R109 collapse gate)
4149
+ 'very-dense' — 31+ nodes
4150
+ Picks the gate boundaries that already drive CONDITIONAL
4151
+ RENDER decisions elsewhere (R109 denseLayout = >16, R110
4152
+ plain-text fallback) so the tier name is semantically
4153
+ aligned with the visual mode the canvas already switches
4154
+ to. Composed from existing onlineNodes — no new state.
4155
+ 12th attr in the canvas state surface set (R462/R466/R467/
4156
+ R469×4/R471×2/R487/R488/R502). 12 attrs covers: build
4157
+ identity, transient/sticky inspection modes, fleet split
4158
+ numerics, fleet density tier, canvas layout/theme, canvas
4159
+ zoom, hover identity. A test harness can snapshot the
4160
+ full canvas state with 12 getAttribute calls. */
4161
+ data-topo-fleet-density-tier={
4162
+ onlineNodes.length === 0 ? 'empty' :
4163
+ onlineNodes.length <= 3 ? 'sparse' :
4164
+ onlineNodes.length <= 15 ? 'normal' :
4165
+ onlineNodes.length <= 30 ? 'dense' :
4166
+ 'very-dense'
4167
+ }
3535
4168
  data-topo-online-count={onlineNodes.length}
3536
4169
  data-topo-working-count={workingCount}
3537
4170
  data-topo-offline-count={offlineNodes.length}
@@ -3597,6 +4230,119 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3597
4230
  categorical) — separate dedicated attrs if/when needed.
3598
4231
  Root svg attribute set now 11 attrs total. */
3599
4232
  data-topo-hovered-alias={hoveredAlias ?? ''}
4233
+ /* Round 504 / Loop — categorical pin-aspect attr paired with
4234
+ R467 any-pinned boolean and R488 hovered-alias identity.
4235
+ Pre-R504 the canvas state surface set told tests WHETHER
4236
+ any pin was active (R467 boolean) but tests had to enumerate
4237
+ 4 individual state vars to determine WHICH pin axis fired:
4238
+ pinnedStatus legend-row status filter
4239
+ pinnedGroup prefix-cluster lock
4240
+ pinnedVendor vendor-chip filter
4241
+ pinnedEdgeKey edge-focus
4242
+ R504 surfaces the active aspect as a single categorical
4243
+ attribute: data-topo-pinned-aspect ∈
4244
+ 'none' no pin active
4245
+ 'status' pinnedStatus only
4246
+ 'group' pinnedGroup only
4247
+ 'vendor' pinnedVendor only
4248
+ 'edge' pinnedEdgeKey only
4249
+ 'multi' 2 or more pins active simultaneously
4250
+ ('multi' covers cross-cutting filters — e.g. user pins
4251
+ status='working' AND vendor='claude' simultaneously to
4252
+ narrow the canvas. Each pin axis is independently
4253
+ dismissable via Esc / individual chip click, so multi
4254
+ states are reachable and worth surfacing as a distinct
4255
+ tier.)
4256
+ 13th attr in the canvas state surface set after R502.
4257
+ Composed from 4 existing state vars — no new state. */
4258
+ /* Round 512 / Loop — 14th canvas state attr. groupBoxes.length
4259
+ surfaces the count of cluster boxes currently rendered in
4260
+ grid layout (always 0 in ring). Paired with R502 categorical
4261
+ density tier + R469 fleet numerics for a complete cluster-
4262
+ cardinality surface:
4263
+ R469 data-topo-online-count node-count
4264
+ R502 data-topo-fleet-density-tier categorical
4265
+ R512 data-topo-cluster-count cluster-count ← this round
4266
+ Use cases:
4267
+ - Playwright: assert orphan-band existence by
4268
+ `cluster-count === N + 1` vs prefix-only `=== N`
4269
+ - external CSS: `[data-topo-cluster-count='1']` to apply
4270
+ single-cluster grid-specific layout adjustments
4271
+ - future polish gates: cluster-count > N could trigger
4272
+ dense-grid mode
4273
+ Composed from existing `groupBoxes.length` — no new state.
4274
+ Always renders (0 in ring layout, N in grid), so tests can
4275
+ rely on attribute presence + value. */
4276
+ data-topo-cluster-count={groupBoxes.length}
4277
+ /* Round 513 / Loop — 15th canvas state attr. Surfaces the
4278
+ user's prefers-reduced-motion preference directly on the
4279
+ root SVG so external CSS / Playwright tests can branch on
4280
+ a11y state without re-reading the media query.
4281
+ reducedMotion is already in component scope (R29 a11y
4282
+ blanket reads it via a useEffect listener); R513 just
4283
+ exposes it as a stable attribute handle.
4284
+ Use cases:
4285
+ - Playwright: assert reduced-motion gates from one attr
4286
+ read instead of mocking media-query state per test
4287
+ - External CSS hooks: `[data-topo-prefers-reduced-motion=
4288
+ "true"]` to apply paint-only overrides (e.g. mute
4289
+ hover glows entirely on a11y instead of just
4290
+ disabling transitions)
4291
+ - Future polish rounds: any motion-gated render can
4292
+ read this attr server-side without the media-query
4293
+ hydration mismatch risk
4294
+ 'true' / 'false' string values (consistent with R466/R467
4295
+ boolean attrs). */
4296
+ data-topo-prefers-reduced-motion={reducedMotion ? 'true' : 'false'}
4297
+ /* Round 515 / Loop — 16th canvas state attr. Surfaces the
4298
+ fullscreen-mode state directly on root SVG so external
4299
+ consumers don't have to traverse the chrome strip's
4300
+ `data-topo-chrome-fullscreen-active` button attr (which
4301
+ measures the BUTTON state, not the canvas state — they
4302
+ agree, but reading from the root is semantically cleaner
4303
+ for canvas-state probes).
4304
+ Composed from existing isFullscreen React state (R103
4305
+ fullscreen toggle).
4306
+ Use cases:
4307
+ - Playwright: assert canvas mode in one attr read
4308
+ (paired with R471 data-topo-layout for ring/grid +
4309
+ R487 data-topo-zoom for zoom level + R513 reduced-
4310
+ motion for a11y mode = 4-axis canvas-mode probe)
4311
+ - External CSS: `[data-topo-fullscreen="true"]` to
4312
+ apply fullscreen-only paint adjustments outside the
4313
+ React tree (e.g. body-level scrollbar hide)
4314
+ 'true' / 'false' string values (consistent with R466/
4315
+ R467/R513 boolean attrs). */
4316
+ data-topo-fullscreen={isFullscreen ? 'true' : 'false'}
4317
+ /* Round 516 / Loop — 17th canvas state attr. Surfaces the
4318
+ grid layout's content-bottom y-coordinate so tests can
4319
+ verify grid content doesn't extend past the viewBox or
4320
+ collide with chrome elements positioned below the canvas.
4321
+ Composed from existing gridContentBottom derived state
4322
+ (computed at line ~915 from gy0 + totalRows * cellH + 8).
4323
+ In ring layout, gridContentBottom is 0 (no grid). In grid
4324
+ layout it's the actual pixel y-coordinate where the
4325
+ cluster bands end.
4326
+ Use cases:
4327
+ - Playwright: assert grid layout doesn't exceed viewBox
4328
+ height (680) without re-computing the layout math
4329
+ - External CSS: `[data-topo-grid-content-bottom='0']` to
4330
+ distinguish ring-mode (no grid content) from grid-mode
4331
+ in CSS without parsing layout attr
4332
+ - Future polish gates: if cluster count grows large
4333
+ enough to push grid bottom past viewBox, can trigger
4334
+ a 'compact' mode automatically */
4335
+ data-topo-grid-content-bottom={gridContentBottom}
4336
+ data-topo-pinned-aspect={(() => {
4337
+ const aspects: string[] = [];
4338
+ if (pinnedStatus) aspects.push('status');
4339
+ if (pinnedGroup) aspects.push('group');
4340
+ if (pinnedVendor) aspects.push('vendor');
4341
+ if (pinnedEdgeKey) aspects.push('edge');
4342
+ if (aspects.length === 0) return 'none';
4343
+ if (aspects.length === 1) return aspects[0];
4344
+ return 'multi';
4345
+ })()}
3600
4346
  /* Round 466 / Loop — aggregate hover signal on the root SVG.
3601
4347
  Exposes a single boolean `data-topo-any-hover` that
3602
4348
  reflects whether ANY hover state in the topology is
@@ -3839,7 +4585,38 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3839
4585
  const x = ((seed * 13) % 1000);
3840
4586
  const y = ((seed * 7) % 680);
3841
4587
  const r = (i % 3 === 0) ? 1.2 : 0.7;
3842
- return <circle key={i} cx={x} cy={y} r={r} fill="#a5b4fc" opacity={0.35 + (i % 4) * 0.05} data-topo-starfield-dot={i} />;
4588
+ /* Round 523 / Loop 配色 family extension to a 3rd anchor.
4589
+ Pre-R523 all 14 starfield dots painted at the same
4590
+ hardcoded `#a5b4fc` (indigo-300). The starfield's role
4591
+ is atmospheric depth (R45, R291 comment), but a flat
4592
+ single-hue field reads more like a regular dot grid
4593
+ than a star field — real starlight has color
4594
+ temperature variation (blue-white hot stars / yellow
4595
+ sun-like / cool red).
4596
+ R523 cycles a 3-color deterministic rotation based on
4597
+ `i % 3`:
4598
+ i % 3 === 0 → #a5b4fc indigo-300 (original, cool)
4599
+ i % 3 === 1 → #67e8f9 cyan-300 (cyber accent, hot)
4600
+ i % 3 === 2 → #cbd5e1 slate-300 (neutral, warm white)
4601
+ All three hues sit inside the cyber theme's palette
4602
+ family (indigo / cyan / slate) so the starfield reads
4603
+ varied-but-coherent rather than rainbow. At opacity
4604
+ 0.5 (parent <g>) * 0.35-0.50 (per-dot) the temperature
4605
+ shifts are gentle but perceptible — closes the gap
4606
+ between 'dot grid' and 'star field'.
4607
+ 配色 family extension (3 anchors): R509/R510 hub-
4608
+ highlight cross-theme fill + R523 starfield color
4609
+ temperature variation. Light theme unaffected
4610
+ (starfield gated `!isLight` so light theme stays
4611
+ clean per R45's original 'white surface stays clean'
4612
+ intent).
4613
+ Deterministic on `i` — no JS hydration mismatch,
4614
+ same SSR/client output. data-topo-starfield-dot-hue
4615
+ attr exposes the resolved hue category for tests. */
4616
+ const hues = ['#a5b4fc', '#67e8f9', '#cbd5e1'] as const;
4617
+ const hueNames = ['indigo', 'cyan', 'slate'] as const;
4618
+ const hueIdx = i % 3;
4619
+ 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]} />;
3843
4620
  })}
3844
4621
  </g>
3845
4622
  )}
@@ -4377,8 +5154,43 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4377
5154
  data-topo-hub-spoke-stroke-width={spokeStrokeWidth}
4378
5155
  data-topo-hub-spoke-stroke-width-active="2.25"
4379
5156
  data-topo-hub-spoke-linecap="round"
5157
+ /* Round 533 / Loop — extends drop-shadow visual-polish
5158
+ family to a 9th anchor: hub spokes gain filter:drop-
5159
+ shadow glow on hub-hover. Subtle 1.5px cyan/teal blur
5160
+ applied across ALL spokes simultaneously when the
5161
+ user hovers the hub — the network mesh visually
5162
+ "lights up" in response to focal attention. Sibling
5163
+ to R476 hub-digit + R532 hub-highlight glow at the
5164
+ same gate (hoveredHub && !reducedMotion); together
5165
+ the three anchors (digit + highlight disc + spokes)
5166
+ form a unified focal-cluster glow that signals
5167
+ "you're focused on the hub" across geometry,
5168
+ paint, and mesh-extent axes.
5169
+ Theme-aware glow palette matches the spoke stroke
5170
+ family:
5171
+ light: rgba(13, 148, 136, 0.4) teal-600
5172
+ cyber: rgba(34, 211, 238, 0.4) cyan-400
5173
+ 0.4 alpha keeps the glow subtle across N spokes
5174
+ (30+ at peak fleet sizes) — loud bloom across many
5175
+ edges would compete with the focal cluster itself.
5176
+ 1.5px blur is conservative; tuned so each spoke
5177
+ gains a faint outer halo rather than a wide bloom.
5178
+ filter is paint-only; bbox unchanged; existing
5179
+ R241 transition list extends to 'filter 250ms
5180
+ ease-out' matching the spoke transition cadence
5181
+ (250ms, distinct from the 200ms hub-cluster
5182
+ cadence — spokes ease slightly slower since they
5183
+ respond to per-alias state, not just hub state).
5184
+ data-topo-hub-spoke-glow attr exposes the gate
5185
+ state for tests. */
5186
+ data-topo-hub-spoke-glow={!reducedMotion && hoveredHub ? 'true' : 'false'}
4380
5187
  style={{
4381
- transition: 'stroke 250ms ease-out, stroke-width 250ms ease-out, opacity 250ms ease-out',
5188
+ transition: 'stroke 250ms ease-out, stroke-width 250ms ease-out, opacity 250ms ease-out, filter 250ms ease-out',
5189
+ filter: !reducedMotion && hoveredHub
5190
+ ? (isLight
5191
+ ? 'drop-shadow(0 0 1.5px rgba(13, 148, 136, 0.4))'
5192
+ : 'drop-shadow(0 0 1.5px rgba(34, 211, 238, 0.4))')
5193
+ : undefined,
4382
5194
  ...(isActiveSpoke ? {} : {
4383
5195
  animationDelay: `${-(idx * 0.25)}s`,
4384
5196
  // CSS var consumed by `.anet-topo-spoke-flow`
@@ -4526,12 +5338,75 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4526
5338
  // pinned → fill 0.08 / 0.13, stroke 3 px (locked)
4527
5339
  // hovered → fill 0.05 / 0.09, stroke 2 px (inspecting)
4528
5340
  // idle → fill 0.025 / 0.045, stroke 1.5 px dashed
4529
- fillOpacity={isPinned ? (isLight ? 0.08 : 0.13)
4530
- : isHovered ? (isLight ? 0.05 : 0.09)
4531
- : (isLight ? 0.025 : 0.045)}
5341
+ /* Round 506 / Loop category-differentiation family
5342
+ 3rd anchor. Orphan band rest-state fillOpacity drops
5343
+ slightly below prefix-group rest (0.025/0.045
5344
+ 0.015/0.028). Adds a 3rd independent paint
5345
+ differentiator to the orphan visual signature:
5346
+ R499 fontStyle: italic (label text)
5347
+ R503 '3 6' dash pattern (rect stroke)
5348
+ R506 lower fillOpacity (rect fill) ← this round
5349
+ Three independent channels (typography + stroke
5350
+ pattern + fill density) collectively encode the
5351
+ catchall semantic at rest. Pin and hover branches
5352
+ UNCHANGED (still 0.05/0.09 hover, 0.08/0.13 pin) —
5353
+ orphan box gets full visual emphasis on inspection
5354
+ identical to prefix groups; the differentiation
5355
+ lives ONLY in the unsolicited rest state. The
5356
+ ~40% drop (0.045 → 0.028 cyber, 0.025 → 0.015
5357
+ light) is subtle enough that the orphan box stays
5358
+ visible at rest, just quieter — matches the
5359
+ "misc bucket, less attention-deserving" semantic
5360
+ without losing the visual anchor.
5361
+ Pure paint axis; bbox unchanged; R51 SVG sentinel
5362
+ safety untouched (overlap-test gates to g[data-
5363
+ node], cluster rect invisible to it).
5364
+ data-group-box-fill-opacity attr surfaces the
5365
+ resolved value for tests. */
5366
+ fillOpacity={
5367
+ isPinned ? (isLight ? 0.08 : 0.13)
5368
+ : isHovered ? (isLight ? 0.05 : 0.09)
5369
+ : box.isOrphan ? (isLight ? 0.015 : 0.028)
5370
+ : (isLight ? 0.025 : 0.045)
5371
+ }
5372
+ data-group-box-fill-opacity={
5373
+ isPinned ? (isLight ? 0.08 : 0.13)
5374
+ : isHovered ? (isLight ? 0.05 : 0.09)
5375
+ : box.isOrphan ? (isLight ? 0.015 : 0.028)
5376
+ : (isLight ? 0.025 : 0.045)
5377
+ }
4532
5378
  stroke={(isPinned || isHovered) ? pal.legendAccent : pal.ringStroke}
4533
5379
  strokeWidth={isPinned ? 3 : isHovered ? 2 : 1.5}
4534
- strokeDasharray={(isPinned || isHovered) ? 'none' : '6 6'}
5380
+ /* Round 503 / Loop category-differentiation family
5381
+ 2nd anchor (R499 = orphan label italic, 1st anchor).
5382
+ Orphan band rest-state strokeDasharray switches from
5383
+ '6 6' (prefix-group default) to '3 6' (tighter
5384
+ dashes). Pre-R503 the rect dash pattern was uniform
5385
+ across all bands; combined with R499's italic label,
5386
+ the orphan box now has TWO independent paint/
5387
+ typography differentiators at rest:
5388
+ R499 fontStyle: italic (label text)
5389
+ R503 '3 6' dash pattern (rect stroke) ← this round
5390
+ The R85 marching-ants animation continues to work
5391
+ with the new dash size (uses --march-dur custom
5392
+ property, dash-length-agnostic) — orphan's ants
5393
+ just have a different visual rhythm than prefix-
5394
+ group ants, reinforcing the catchall semantic.
5395
+ Pinned/hovered orphan still gets 'none' (solid
5396
+ stroke) so the hover/pin affordance is preserved
5397
+ — the differentiation lives ONLY in the rest
5398
+ state, never blocking inspection.
5399
+ Pure paint axis; no geometry change; bbox unchanged
5400
+ (strokeDasharray is paint-only). R51 SVG sentinel
5401
+ safety untouched (overlap-test gates to g[data-
5402
+ node], this cluster rect is invisible to it).
5403
+ data-group-box-orphan attr surfaces the gate for
5404
+ tests + future polish references. */
5405
+ strokeDasharray={
5406
+ (isPinned || isHovered) ? 'none' :
5407
+ box.isOrphan ? '3 6' : '6 6'
5408
+ }
5409
+ data-group-box-orphan={box.isOrphan ? 'true' : 'false'}
4535
5410
  /* Round 380 / Loop: cluster box stroke gets round
4536
5411
  linecap + round linejoin. Sibling SVG stroke-
4537
5412
  softening polish to R378 flow-rail linecap + R379
@@ -4851,7 +5726,44 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4851
5726
  fontSize="9"
4852
5727
  fontFamily="monospace"
4853
5728
  fontWeight={isPinned ? '800' : '700'}
4854
- opacity={isPinned || isHovered ? 1 : 0.55}
5729
+ /* Round 551 / Loop category-differentiation family
5730
+ 4th anchor. Orphan band ("其他" catchall) rest-state
5731
+ LABEL opacity drops 0.55 → 0.4 (~27% relative dim),
5732
+ mirroring R506's rect fillOpacity drop pattern at
5733
+ the label-paint tier. Adds a 4th independent
5734
+ channel to the orphan visual signature at rest:
5735
+ R499 fontStyle italic (typography style)
5736
+ R503 '3 6' dash pattern (rect stroke)
5737
+ R506 lower rect fill-opacity (rect fill)
5738
+ R551 lower label opacity (label paint) ← this round
5739
+ Four independent channels (typography style +
5740
+ stroke pattern + rect fill density + label paint
5741
+ density) collectively encode the catchall semantic
5742
+ at rest — orphan band reads as "misc bucket, less
5743
+ attention-deserving" through every available paint
5744
+ channel, no chrome / color / geometry change.
5745
+ Pin and hover branches UNCHANGED — orphan label
5746
+ restores to full opacity 1 on inspection, same as
5747
+ prefix groups. The differentiation lives ONLY in
5748
+ the unsolicited rest state. The ~27% drop (0.55 →
5749
+ 0.4) is dimmer than R506's ~40% (rect could
5750
+ tolerate it; small 9px text needs more residual
5751
+ paint to stay legible) — orphan label stays
5752
+ readable when scanning, just clearly quieter.
5753
+ Pure paint axis; bbox unchanged; R51 SVG sentinel
5754
+ safety untouched (overlap-test gates to g[data-
5755
+ node], this group-label is invisible to it).
5756
+ transition list (R55/R432/R457/R479: fill, ls,
5757
+ fw, filter all 200ms) already eases opacity since
5758
+ `opacity 300ms ease-out` lives in the parent <text>
5759
+ CSS — wait, only filter/ls/fw/fill 200ms are
5760
+ listed. Need to add 'opacity 200ms ease-out' for
5761
+ smooth orphan opacity flip on pin/hover transitions
5762
+ (currently opacity 0.55 → 1 was snapping).
5763
+ data-group-label-opacity attr exposes the resolved
5764
+ value for tests. */
5765
+ opacity={isPinned || isHovered ? 1 : box.isOrphan ? 0.4 : 0.55}
5766
+ data-group-label-opacity={isPinned || isHovered ? 1 : box.isOrphan ? 0.4 : 0.55}
4855
5767
  data-group-label-hovered={isHovered && !isPinned ? 'true' : 'false'}
4856
5768
  data-group-label-font-weight={isPinned ? '800' : '700'}
4857
5769
  /* Round 479 / Loop — extend drop-shadow visual-polish
@@ -4875,17 +5787,63 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4875
5787
  transition list extends to include 'filter 200ms
4876
5788
  ease-out' alongside the existing fill/ls/fw/opacity
4877
5789
  200ms tweens. */
4878
- data-group-label-glow={isPinned ? 'true' : 'false'}
5790
+ /* Round 538 / Loop — extends R479 group-label drop-
5791
+ shadow from pin-only to ALSO fire on hover, with
5792
+ a 2-tier alpha ladder matching the R432 letter-
5793
+ spacing 3-tier (hover at 0.25px / pin at 0.5px)
5794
+ pattern. Pre-R538 the paint axis was binary (lit
5795
+ on pin, dark on hover); R538 adds a softer hover
5796
+ glow that distinguishes from the stronger pin
5797
+ glow without losing the "active state lights up"
5798
+ gesture.
5799
+ 2-tier alpha ladder:
5800
+ pin (committed) cyan 80 hex (~50% alpha)
5801
+ hover (preview) cyan 4d hex (~30% alpha)
5802
+ rest none
5803
+ Pin signature stays distinctively brighter, but
5804
+ hover now telegraphs paint-axis attention too.
5805
+ Sibling to R534 edge-badge hover-precedence
5806
+ extension at the drop-shadow family. R479 hue
5807
+ (pal.legendAccent) preserved across both tiers.
5808
+ data-group-label-glow attr upgraded from binary
5809
+ ('true'/'false') to 3-value ('pin' | 'hover' |
5810
+ 'false') so tests can distinguish gate cause. */
5811
+ data-group-label-glow={isPinned ? 'pin' : isHovered ? 'hover' : 'false'}
5812
+ /* Round 499 / Loop — orphan band "其他" label gets
5813
+ fontStyle: italic to visually distinguish the
5814
+ catchall from real prefix-group bands. Pre-R499
5815
+ the orphan box label rendered identically to
5816
+ prefix-group labels (Hero D fontSize=9, fw=700,
5817
+ opacity 0.55 rest), so users had to read the
5818
+ literal text "其他" to identify the catchall. R499
5819
+ adds a pure-typography differentiation: italic
5820
+ signals "this is the misc bucket, not a real
5821
+ named group" while preserving full opacity
5822
+ affordance on hover/pin — the orphan box stays
5823
+ equally inspectable, just typographically marked
5824
+ as a different category. No geometry change
5825
+ (italic shifts glyph slant within the same bbox),
5826
+ no opacity loss, no behavior change. Sibling to
5827
+ R432 letter-spacing 3-tier + R457 pin fw-lift +
5828
+ R479 pin drop-shadow at the group-label scope.
5829
+ Falls under 配色 / 节点视觉 themes per the prompt;
5830
+ advances the "信息密度" axis by encoding
5831
+ category-distinction into a single typography
5832
+ channel without adding visual chrome. */
4879
5833
  style={{
4880
5834
  transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out, font-weight 200ms ease-out, opacity 200ms ease-out, filter 200ms ease-out',
4881
5835
  letterSpacing: isPinned ? '0.5px' :
4882
5836
  isHovered ? '0.25px' : '0px',
5837
+ fontStyle: box.isOrphan ? 'italic' : undefined,
4883
5838
  filter: isPinned
4884
5839
  ? `drop-shadow(0 0 3px ${pal.legendAccent}80)`
4885
- : undefined,
5840
+ : isHovered
5841
+ ? `drop-shadow(0 0 3px ${pal.legendAccent}4d)`
5842
+ : undefined,
4886
5843
  }}
4887
5844
  data-group-label={box.key}
4888
5845
  data-group-label-pinned={isPinned ? 'true' : 'false'}
5846
+ data-group-label-orphan={box.isOrphan ? 'true' : 'false'}
4889
5847
  >
4890
5848
  {box.key}
4891
5849
  {/* Round 19 / Loop: member-count chip. Inline tspan stays
@@ -5972,11 +6930,48 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
5972
6930
  data-edge-badge-opacity-rest={isLight ? 0.95 : 0.85}
5973
6931
  data-edge-badge-opacity-hover="1"
5974
6932
  data-edge-badge-opacity-active="1"
5975
- data-edge-badge-glow={isHot ? 'true' : 'false'}
6933
+ data-edge-badge-glow={(isHoveredEdge || isPinned) ? 'hover' : isHot ? 'hot' : 'false'}
6934
+ /* Round 534 / Loop — extends edge-badge drop-shadow
6935
+ coverage from hot-only (R480 amber) to also fire
6936
+ on hover/pin with a cyan accent glow. Pre-R534
6937
+ the badge's hover/pin lifted r (R164 9 → 10.5)
6938
+ + sw (R394 1.25 → 1.5) + opacity (R395/R396 →
6939
+ 1.0), but the paint axis stayed at the badge's
6940
+ rest fill — no glow to telegraph "in focus" at
6941
+ the paint layer. R534 closes that 4-axis hover-
6942
+ lift parity by adding drop-shadow glow on
6943
+ (hovered || pinned).
6944
+ Precedence: (hover || pin) wins over isHot when
6945
+ BOTH true — interactive signal (user is
6946
+ inspecting) overrides informational signal
6947
+ (hot lane). When only isHot fires (no hover/
6948
+ pin) the amber R480 glow remains; the hover/
6949
+ pin case paints cyan/teal `pal.legendAccent`
6950
+ at 0x99 alpha (~60%) — bright enough to read
6951
+ as "lit" but won't overwhelm at small badge
6952
+ size (r=10.5).
6953
+ Edge-badge 4-axis hover-lift parity now:
6954
+ R164 r 9 → 10.5
6955
+ R394 stroke-wd 1.25 → 1.5
6956
+ R395 opacity rest → 1.0
6957
+ R534 filter none → drop-shadow glow ← this round
6958
+ Drop-shadow visual-polish family extension —
6959
+ edge-badge surface upgraded from single-gate
6960
+ (R480 isHot) to two-gate (isHot OR hover-pin).
6961
+ transition list already includes filter 200ms
6962
+ ease-out (R480). data-edge-badge-glow attr
6963
+ upgraded from `isHot ? true : false` to a
6964
+ 3-value string: 'hot' | 'hover' | 'false' so
6965
+ tests can distinguish gate cause.
6966
+ R51 sentinel safety: badge is edge-internal
6967
+ (not g[data-node] ancestor); filter is paint-
6968
+ only; bbox unchanged. */
5976
6969
  style={{
5977
- filter: isHot
5978
- ? `drop-shadow(0 0 3px ${hotStroke}80)`
5979
- : undefined,
6970
+ filter: (isHoveredEdge || isPinned)
6971
+ ? `drop-shadow(0 0 3px ${pal.legendAccent}99)`
6972
+ : isHot
6973
+ ? `drop-shadow(0 0 3px ${hotStroke}80)`
6974
+ : undefined,
5980
6975
  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',
5981
6976
  }}
5982
6977
  />
@@ -6276,6 +7271,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6276
7271
  data-hub-busyness={busy}
6277
7272
  data-topo-hub-halo-radius={haloR}
6278
7273
  data-topo-hub-halo-hovered={isHaloHovered ? 'true' : 'false'}
7274
+ data-topo-hub-halo-glow={!reducedMotion && hoveredHub ? 'true' : 'false'}
6279
7275
  data-topo-hub-halo-trough={isLight ? troughLight : troughDark}
6280
7276
  data-topo-hub-halo-peak={isLight ? peakLight : peakDark}
6281
7277
  /* Round 253 / Loop: hub grounding halo fill transition
@@ -6287,10 +7283,45 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6287
7283
  conflict.
6288
7284
  R451: r as CSS property (R197/R198 idiom) so the
6289
7285
  hover-radius tween eases smoothly under the same
6290
- 200ms cadence as fill. */
7286
+ 200ms cadence as fill.
7287
+ R536: extends hub-cluster glow QUARTET (R476 digit
7288
+ + R532 disc + R535 ring + R533 spokes) to a 5th
7289
+ tier — the halo gains drop-shadow at the outermost
7290
+ concentric ring on hub-hover. 2px blur + 0.3 alpha
7291
+ keeps the halo's glow subtle since (a) the halo is
7292
+ the LARGEST hub element (r=22 hover) and a heavier
7293
+ glow would bleed visibly past the ring tier into
7294
+ the spoke origin, and (b) the halo already SMIL-
7295
+ animates opacity (R84/R244 breath), so the visible
7296
+ glow pulses with the breath — an atmospheric
7297
+ "breathing glow" idiom rather than a static rim.
7298
+ Hub-cluster glow QUINTET (R476/R532/R533/R535/R536):
7299
+ digit (typo center) 3px emerald 0.6
7300
+ disc (r=5.5/6) 3px emerald 0.6
7301
+ ring (r=14/17) 3px emerald 0.5
7302
+ halo (r=20/22) 2px emerald 0.3 ← this round
7303
+ spokes (mesh) 1.5px cyan/teal 0.4
7304
+ Emerald palette continues through the focal-disc
7305
+ family (digit/disc/ring/halo); spokes break out
7306
+ into cyan/teal at the mesh tier. The 4-step alpha
7307
+ ladder 0.6→0.6→0.5→0.3 reads as the focal cluster
7308
+ fading outward — the OUTERMOST emerald glow is the
7309
+ softest, the focal digit is the brightest.
7310
+ filter is paint-only; SMIL animate on opacity
7311
+ continues independently (attribute vs CSS-property
7312
+ non-conflicting). transition list extends to
7313
+ 'filter 200ms ease-out' alongside fill + r.
7314
+ Drop-shadow visual-polish family extension (12
7315
+ anchors). preview.50 milestone round. data-topo-
7316
+ hub-halo-glow attr exposes the gate state. */
6291
7317
  style={{
6292
7318
  r: `${haloR}px`,
6293
- transition: 'fill 200ms ease-out, r 200ms ease-out',
7319
+ filter: !reducedMotion && hoveredHub
7320
+ ? (isLight
7321
+ ? 'drop-shadow(0 0 2px rgba(16, 185, 129, 0.3))'
7322
+ : 'drop-shadow(0 0 2px rgba(52, 211, 153, 0.3))')
7323
+ : undefined,
7324
+ transition: 'fill 200ms ease-out, r 200ms ease-out, filter 200ms ease-out',
6294
7325
  } as React.CSSProperties}
6295
7326
  >
6296
7327
  {/* Round 244 / Loop: hub grounding halo breath gets
@@ -6500,22 +7531,91 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6500
7531
  so the glow eases under the same cadence as the
6501
7532
  scale + fw + fill axes. */
6502
7533
  data-topo-hub-working-count-glow={!reducedMotion && hoveredHub ? 'true' : 'false'}
7534
+ /* Round 507 / Loop — focal recede. When ANY non-hub
7535
+ canvas surface is hovered (a node / an edge / a
7536
+ group label / a legend row / a vendor chip), the
7537
+ hub-center workingCount digit fades to 0.85 opacity,
7538
+ signaling "you're inspecting elsewhere, hub recedes
7539
+ to background." When the user un-hovers (or hovers
7540
+ the hub itself), opacity returns to 1.0. Pure paint
7541
+ polish at the canvas's most prominent focal point.
7542
+ Hits 信息密度 + 动效 themes — the hub digit gives
7543
+ way visually to the surface under inspection,
7544
+ reinforcing the "this is the focal point right now"
7545
+ gesture without requiring users to track which
7546
+ surface holds attention.
7547
+ Gate excludes hoveredHub specifically: hovering the
7548
+ hub itself should LIFT the digit (R425 fw bump +
7549
+ R476 glow + R209 scale 1.08) — the existing hover-
7550
+ on-hub signature is intact; only inspection
7551
+ ELSEWHERE recedes the hub.
7552
+ Composed from existing hoveredAlias / hoveredEdge-
7553
+ Key / hoveredGroupLabel / hoveredStatus / hovered-
7554
+ Vendor — no new state. 300ms ease-out opacity
7555
+ transition already in the style list (existing R213
7556
+ transition spec), so the fade rides on existing
7557
+ infrastructure.
7558
+ data-topo-hub-recede attr surfaces the gate state
7559
+ for tests. */
7560
+ data-topo-hub-recede={
7561
+ (hoveredAlias || hoveredEdgeKey || hoveredGroupLabel ||
7562
+ hoveredStatus || hoveredVendor) && !hoveredHub ? 'true' : 'false'
7563
+ }
7564
+ /* Round 527 / Loop — focal-amplify family extension to a
7565
+ 2nd anchor. R511 introduced focal-amplify at the hub-
7566
+ highlight disc (base opacity 0.95 → 1.0 on hover); R527
7567
+ extends to the hub-center workingCount digit with a
7568
+ letter-spacing tween 0 → 0.3px on hub-hover.
7569
+ Composes with existing 3-axis hub-hover signature on
7570
+ this element:
7571
+ R209 transform scale(1.08) geometry
7572
+ R425 fontWeight 700 → 800 typography weight
7573
+ R476 filter drop-shadow glow paint
7574
+ R527 letter-spacing 0 → 0.3px typography kerning ← this round
7575
+ tabular-nums (R225) preserved — each digit cell keeps
7576
+ fixed width; the inter-digit advance grows by 0.3px
7577
+ per gap. Single-digit counts (1-9) show no kerning
7578
+ effect; multi-digit counts (10+) show the spread as
7579
+ info-density signaling. Sibling to R427/R431/R432/
7580
+ R433/R434 (hover-letter-spacing family at panel-text
7581
+ scope) — R527 brings the same idiom to the canvas's
7582
+ most-read scalar.
7583
+ Reduced-motion gate matches R209 scale, R425 fw, R476
7584
+ filter — !reducedMotion gates the lift; reducedMotion
7585
+ users see static digit baseline regardless of hover.
7586
+ Focal-amplify family extension (2 anchors): R511 hub-
7587
+ highlight opacity / R527 hub-digit letter-spacing.
7588
+ transition list extends to include `letter-spacing
7589
+ 200ms ease-out`, matching the cadence of the other
7590
+ hub-hover axes. data-topo-hub-working-count-letter-
7591
+ spacing attr exposes the resolved value for tests. */
7592
+ data-topo-hub-working-count-letter-spacing={!reducedMotion && hoveredHub ? '0.3px' : '0px'}
6503
7593
  style={{
6504
7594
  pointerEvents: 'none',
6505
7595
  transform: !reducedMotion && hoveredHub ? 'scale(1.08)' : 'scale(1)',
6506
7596
  transformBox: 'fill-box',
6507
7597
  transformOrigin: 'center',
7598
+ opacity: (hoveredAlias || hoveredEdgeKey || hoveredGroupLabel ||
7599
+ hoveredStatus || hoveredVendor) && !hoveredHub
7600
+ ? 0.85
7601
+ : 1,
6508
7602
  filter: !reducedMotion && hoveredHub
6509
7603
  ? (isLight
6510
7604
  ? 'drop-shadow(0 0 2px rgba(16, 185, 129, 0.6))'
6511
7605
  : 'drop-shadow(0 0 3px rgba(52, 211, 153, 0.6))')
6512
7606
  : undefined,
7607
+ letterSpacing: !reducedMotion && hoveredHub ? '0.3px' : '0px',
6513
7608
  /* R425: font-weight 200ms appended so the hover fw
6514
7609
  bump 700 → 800 eases under the same cadence as
6515
7610
  R209 scale + R253 fill + R213 opacity.
6516
7611
  R476: filter 200ms appended so the new drop-
6517
- shadow glow eases at the same cadence. */
6518
- transition: 'transform 200ms ease-out, opacity 300ms ease-out, fill 200ms ease-out, font-weight 200ms ease-out, filter 200ms ease-out',
7612
+ shadow glow eases at the same cadence.
7613
+ R507: opacity 300ms (existing in list) covers
7614
+ the new focal-recede fade.
7615
+ R527: letter-spacing 200ms appended so the new
7616
+ hover-kerning bump eases at the same cadence
7617
+ as the other axes. */
7618
+ 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',
6519
7619
  fontVariantNumeric: 'tabular-nums',
6520
7620
  }}
6521
7621
  >
@@ -6561,19 +7661,205 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6561
7661
  + R213 always-mount opacity-gate + pointerEvents:none
6562
7662
  + R365 r=5.5 all preserved. data-topo-hub-highlight-
6563
7663
  opacity attr exposes the resolved value for tests. */}
6564
- <circle
6565
- cx={cx} cy={cy} r="5.5"
6566
- fill="#d1fae5"
6567
- opacity={workingCount > 0 ? 0 : 0.95}
6568
- data-topo-hub-highlight
6569
- data-topo-hub-highlight-visible={workingCount > 0 ? 'false' : 'true'}
6570
- data-topo-hub-highlight-radius="5.5"
6571
- data-topo-hub-highlight-opacity={workingCount > 0 ? 0 : 0.95}
6572
- style={{
6573
- pointerEvents: 'none',
6574
- transition: 'opacity 300ms ease-out',
6575
- }}
6576
- />
7664
+ {/* Round 508 / Loop — focal-recede pattern 2nd anchor.
7665
+ Extends R507's hub-digit recede to the hub-highlight
7666
+ circle so the hub focal CLUSTER (digit at z-top + this
7667
+ idle-state highlight beneath) recedes as a unit when
7668
+ canvas attention is elsewhere. Computed once: a single
7669
+ non-hub-hover gate drives BOTH the digit (R507) AND
7670
+ this highlight (R508) so they always co-move.
7671
+ Recede multiplies the visible opacity by 0.85 — when
7672
+ workingCount===0 the rest opacity 0.95 becomes 0.81
7673
+ during external-hover; when workingCount>0 the
7674
+ opacity stays 0 (invisible) regardless of recede.
7675
+ Additionally, when recede is active the SMIL breath
7676
+ animation halts (animate node un-mounts) so the
7677
+ receded state reads as quietly static, not pulsing
7678
+ at 0.85↔1.0 against the recede multiplier (which
7679
+ would visually conflict — competing 15% drops). On
7680
+ un-hover the animate re-mounts and breath resumes.
7681
+ data-topo-hub-recede on both digit AND highlight
7682
+ provides a stable test handle for the unified-recede
7683
+ gate.
7684
+ Composed from existing hover state vars — no new
7685
+ state. Pure paint axis. */}
7686
+ {(() => {
7687
+ const hubRecede = !!((hoveredAlias || hoveredEdgeKey || hoveredGroupLabel ||
7688
+ hoveredStatus || hoveredVendor) && !hoveredHub);
7689
+ /* Round 511 / Loop — hub-highlight gains a 3rd opacity tier
7690
+ (hovered-amplify). Pre-R511 the highlight had 2 visible
7691
+ states: rest (0.95) and recede (0.81 = 0.95 × 0.85).
7692
+ When the hub itself was hovered, the digit got R425 fw
7693
+ lift + R476 drop-shadow + R209 scale-1.08, but the
7694
+ highlight disc sibling stayed at 0.95 — the focal
7695
+ cluster lifted in 3 channels (typography/paint/scale)
7696
+ but the highlight didn't participate.
7697
+ R511 closes that asymmetry: when hoveredHub is true,
7698
+ highlight base opacity lifts to 1.0 (5% boost from
7699
+ rest 0.95). Cluster now lifts as a unit on hub-hover,
7700
+ just like it recedes as a unit on non-hub-hover
7701
+ (R508).
7702
+ 3-state opacity ladder:
7703
+ hub-hovered: baseOpacity = 1.0 (R511 NEW)
7704
+ rest (no hover): baseOpacity = 0.95 (existing)
7705
+ non-hub canvas hov: baseOpacity × 0.85 = 0.81 (R508)
7706
+ Composes cleanly: hubRecede gate requires !hoveredHub,
7707
+ so the hovered-amplify and recede states are mutually
7708
+ exclusive (they can't both fire). breathActive
7709
+ continues to halt on either non-rest state (recede OR
7710
+ hub-hover would visually compete with the 0.85↔1
7711
+ breath — clean for the unit-lift semantic too). */
7712
+ const baseOpacity = workingCount > 0 ? 0
7713
+ : hoveredHub ? 1.0
7714
+ : 0.95;
7715
+ const resolvedOpacity = hubRecede ? baseOpacity * 0.85 : baseOpacity;
7716
+ const breathActive = !reducedMotion && workingCount === 0 && !hubRecede && !hoveredHub;
7717
+ /* Round 529 / Loop — focal-amplify family 3rd anchor.
7718
+ Hub-highlight gains geometric amplify r 5.5 → 6 on
7719
+ hub-hover, mirroring R451's hub-halo r 20 → 22 hover
7720
+ pattern. Pre-R529 the highlight had paint-axis
7721
+ amplify only (R511 opacity 0.95 → 1.0 on hub-hover);
7722
+ R529 adds geometric amplify so the focal disc
7723
+ BREATHES outward on hub attention, like the halo
7724
+ does. Composes with the existing 2-axis hub-hover
7725
+ lift on this element:
7726
+ R511 opacity 0.95 → 1.0 paint (focal-amplify 1st)
7727
+ R529 r 5.5 → 6 geometry (this round)
7728
+ Implementation matches R451: CSS `r` property
7729
+ (R197/R198 idiom) for smooth interpolation. SVG
7730
+ attribute `r="5.5"` provides SSR fallback and serves
7731
+ as default; inline style.r overrides for animated
7732
+ value. transition list extends to include `r 200ms
7733
+ ease-out`, matching the fill cadence (also 200ms);
7734
+ opacity transition stays at 300ms (existing).
7735
+ r 6 sits well inside the existing visual envelope
7736
+ (next-larger sibling r=10 hub core, r=14 hub hover
7737
+ ring). The 0.5px lift is +9% radius / +19% area —
7738
+ enough to read as 'lift' without breaching the core
7739
+ boundary or invalidating overlap-test invariants.
7740
+ SMIL animate on opacity continues independently
7741
+ (animateAttr='opacity' vs CSS-property r — non-
7742
+ conflicting, same pattern R451 noted for halo).
7743
+ Focal-amplify family extension (3 anchors):
7744
+ R511 hub-highlight opacity 0.95 → 1.0
7745
+ R527 hub-digit letter-spacing 0 → 0.3px
7746
+ R529 hub-highlight radius 5.5 → 6 ← this round
7747
+ data-topo-hub-highlight-radius attr now reports the
7748
+ dynamic value (was static '5.5'). */
7749
+ const highlightR = !reducedMotion && hoveredHub ? 6 : 5.5;
7750
+ return (
7751
+ <circle
7752
+ cx={cx} cy={cy} r="5.5"
7753
+ /* Round 509 / Loop — 配色 cross-theme cleanup. Pre-R509
7754
+ the hub-highlight fill was hardcoded `#d1fae5`
7755
+ (emerald-100, a pale tone). On the light theme this
7756
+ near-white green ran against a pale background at
7757
+ 0.95 opacity — the disc was effectively invisible.
7758
+ Matches the existing R253 halo theme-inversion
7759
+ pattern (line ~6481): light theme picks the dark
7760
+ vibrant emerald (#10b981 emerald-600), dark theme
7761
+ keeps the pale emerald (#d1fae5 emerald-100). Both
7762
+ read at the same 0.95 opacity against their
7763
+ respective backdrops — light gets a saturated
7764
+ focal dot; dark keeps the soft glow signature.
7765
+ Pure paint axis (fill change only); bbox unchanged;
7766
+ R51 SVG sentinel safety untouched.
7767
+ transition list already includes `fill 200ms`?
7768
+ Actually the existing transition spec is `opacity
7769
+ 300ms ease-out` — fill change on theme toggle
7770
+ will be instant. That's acceptable: theme toggle
7771
+ is a discrete event, and the halo (line 6500)
7772
+ already snaps fill on theme toggle the same way
7773
+ (`fill 200ms ease-out` was added later to halo
7774
+ via R253). Future round could add `fill 200ms`
7775
+ to highlight too if theme-switch flicker is
7776
+ noticed. */
7777
+ fill={isLight ? '#10b981' : '#d1fae5'}
7778
+ opacity={resolvedOpacity}
7779
+ data-topo-hub-highlight
7780
+ data-topo-hub-highlight-visible={workingCount > 0 ? 'false' : 'true'}
7781
+ data-topo-hub-highlight-radius={highlightR}
7782
+ data-topo-hub-highlight-opacity={resolvedOpacity}
7783
+ data-topo-hub-highlight-breath={breathActive ? 'true' : 'false'}
7784
+ data-topo-hub-highlight-recede={hubRecede ? 'true' : 'false'}
7785
+ data-topo-hub-highlight-hovered={!reducedMotion && hoveredHub ? 'true' : 'false'}
7786
+ data-topo-hub-highlight-glow={!reducedMotion && hoveredHub ? 'true' : 'false'}
7787
+ /* Round 510 / Loop — R509 follow-on: theme-toggle fill
7788
+ ease. Pre-R510 the hub-highlight transition spec only
7789
+ listed `opacity 300ms ease-out`. When R509 introduced
7790
+ theme-conditional fill (#d1fae5 ↔ #10b981), the fill
7791
+ change SNAPPED on theme toggle because the transition
7792
+ list didn't include `fill`. R510 extends to `fill
7793
+ 200ms ease-out` so theme cycles smoothly through the
7794
+ emerald palette. 200ms timing matches the R253 halo
7795
+ fill transition (line ~6500) — both hub-cluster
7796
+ theme transitions now share a cadence so the focal
7797
+ cluster (digit + highlight + halo) eases as a unit.
7798
+ R508's recede opacity transition unchanged (300ms);
7799
+ fill is independent.
7800
+ R529: r as CSS property (R197/R198 idiom) + `r
7801
+ 200ms ease-out` appended to transition list so
7802
+ the new hub-hover radius lift (5.5 → 6) eases
7803
+ under the same fill cadence. SVG attr r="5.5"
7804
+ above provides SSR fallback; inline style.r
7805
+ wins the cascade for the dynamic value.
7806
+ R532: filter drop-shadow glow on hub-hover —
7807
+ sibling to R476 hub-digit drop-shadow at the
7808
+ same gate (hoveredHub && !reducedMotion). Two
7809
+ adjacent hub focal elements (digit + highlight
7810
+ disc) now BOTH glow on hub-hover, reading as
7811
+ one unified focal cluster. Emerald palette
7812
+ matches R476:
7813
+ light: drop-shadow(0 0 2px rgba(16,185,129,0.6)) emerald-500
7814
+ cyber: drop-shadow(0 0 3px rgba(52,211,153,0.6)) emerald-400
7815
+ filter is paint-only (bbox unchanged); SMIL
7816
+ animate on opacity continues independently
7817
+ (animateAttr='opacity' vs CSS-property filter
7818
+ — non-conflicting). transition list extends to
7819
+ 'filter 200ms ease-out' alongside fill/r.
7820
+ Drop-shadow visual-polish family extension
7821
+ (8 anchors): R476 hub-digit / R477 legend pin-
7822
+ ring / R478 recent freshness / hot edge / group
7823
+ label / zoom-state / node alias + R532 hub-
7824
+ highlight (this round). data-topo-hub-highlight-
7825
+ glow attr exposes the gate state. */
7826
+ style={{
7827
+ pointerEvents: 'none',
7828
+ r: `${highlightR}px`,
7829
+ filter: !reducedMotion && hoveredHub
7830
+ ? (isLight
7831
+ ? 'drop-shadow(0 0 2px rgba(16, 185, 129, 0.6))'
7832
+ : 'drop-shadow(0 0 3px rgba(52, 211, 153, 0.6))')
7833
+ : undefined,
7834
+ transition: 'opacity 300ms ease-out, fill 200ms ease-out, r 200ms ease-out, filter 200ms ease-out',
7835
+ } as React.CSSProperties}
7836
+ >
7837
+ {/* Round 497 / Loop — idle-state breath (呼吸感 theme pivot
7838
+ from the R492-R496 press-family arc). Pre-R497 the hub
7839
+ idle highlight read as a static dim disc — present but
7840
+ motionless, visually mute. R497 adds a 4s opacity breath
7841
+ (0.85 ↔ 1.0 ↔ 0.85) so the hub reads "alive but quiet"
7842
+ instead of "frozen", giving the empty-fleet state a
7843
+ subtle living signature.
7844
+ Gates:
7845
+ - !reducedMotion (R29 a11y blanket) — reducedMotion
7846
+ users see static 0.95 disc, no animate
7847
+ - workingCount === 0 — when fleet is busy, the
7848
+ highlight is invisible (opacity=0) so the animate
7849
+ would waste paint cycles. Gating saves work.
7850
+ SMIL <animate> overrides the static opacity={0.95}
7851
+ during its run; falls back to 0.95 when reducedMotion
7852
+ flips on (the animate node simply doesn't render).
7853
+ 4s cycle is long enough to feel like ambient breath
7854
+ rather than a pulse, matching the "quiet" semantic.
7855
+ data-topo-hub-highlight-breath attr exposes the
7856
+ resolved gate state for tests. */}
7857
+ {breathActive && (
7858
+ <animate attributeName="opacity" values="0.85;1;0.85" dur="4s" repeatCount="indefinite" />
7859
+ )}
7860
+ </circle>
7861
+ );
7862
+ })()}
6577
7863
  {/* R115 / Loop: hover hint ring. Stroke-only circle at r=14
6578
7864
  that fades in when the hub is hovered — the same idea
6579
7865
  R44 used for node avatars (group-hover stroke). r=14
@@ -6639,13 +7925,47 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6639
7925
  data-topo-hub-hover-ring-radius={hoveredHub ? 17 : 14}
6640
7926
  data-topo-hub-hover-ring-stroke-width="1.75"
6641
7927
  data-topo-hub-hover-ring-opacity={hoveredHub ? (isLight ? 0.85 : 0.8) : 0}
6642
- /* Round 253 / Loop: hub hover ring also gets stroke
6643
- transition for theme toggle (cyber #10b981 ↔ light
6644
- #059669). The opacity + r transitions stay for hover
6645
- lift; stroke closes the theme-snap. */
7928
+ /* Round 535 / Loop completes the hub-cluster glow
7929
+ QUARTET by adding drop-shadow to the hub-hover-ring.
7930
+ Pre-R535 the hub-hover trio (R476 digit + R532 highlight
7931
+ disc + R533 spokes) glowed in unified emerald (digit/
7932
+ disc) + cyan/teal (spokes) on hub-hover, but the ring
7933
+ itself — the outermost solid emerald boundary at
7934
+ r=14→17 — stayed flat. R535 adds the matching emerald
7935
+ drop-shadow to the ring so the FULL hub-cluster glows
7936
+ across all four concentric surfaces on hub-hover:
7937
+ digit (typography center) drop-shadow 0 0 3px emerald
7938
+ highlight disc (r=5.5/6) drop-shadow 0 0 3px emerald
7939
+ hover-ring (r=14/17) drop-shadow 0 0 3px emerald ← this round
7940
+ spokes (mesh) drop-shadow 0 0 1.5px cyan/teal
7941
+ The ring is only visible on hub-hover (opacity=0 rest);
7942
+ adding drop-shadow at the same gate means the glow shows
7943
+ the moment the ring shows — no extra state needed.
7944
+ Same R476/R532 emerald palette since the ring sits
7945
+ inside the focal-disc tier (its color is also emerald
7946
+ #059669/#10b981).
7947
+ transition list extends to include 'filter 200ms ease-
7948
+ out' alongside the existing 180ms opacity/r — slight
7949
+ cadence mismatch (180 vs 200) is acceptable; the filter
7950
+ only appears AFTER the ring fades in via opacity, and
7951
+ the 200ms vs 180ms 20ms tail difference is below
7952
+ perceptual threshold.
7953
+ Drop-shadow visual-polish family extension (11 anchors):
7954
+ the hub-cluster glow quartet (R476/R532/R533/R535) plus
7955
+ the 7 non-hub anchors (R477/R478/R479/R480/R481/R483/
7956
+ R534) makes for a thoroughly polished glow vocabulary
7957
+ across the canvas.
7958
+ data-topo-hub-hover-ring-glow attr exposes the gate
7959
+ state for tests. */
7960
+ data-topo-hub-hover-ring-glow={!reducedMotion && hoveredHub ? 'true' : 'false'}
6646
7961
  style={{
6647
7962
  pointerEvents: 'none',
6648
- transition: 'opacity 180ms ease-out, r 180ms ease-out, stroke 200ms ease-out',
7963
+ filter: !reducedMotion && hoveredHub
7964
+ ? (isLight
7965
+ ? 'drop-shadow(0 0 3px rgba(16, 185, 129, 0.5))'
7966
+ : 'drop-shadow(0 0 3px rgba(52, 211, 153, 0.5))')
7967
+ : undefined,
7968
+ transition: 'opacity 180ms ease-out, r 180ms ease-out, stroke 200ms ease-out, filter 200ms ease-out',
6649
7969
  }}
6650
7970
  />
6651
7971
  </g>)}
@@ -7558,6 +8878,30 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7558
8878
  const internByAlias = /书生|书小生|intern/i.test(session.alias);
7559
8879
 
7560
8880
  if (isIntern || internByAlias || vendor.logo) {
8881
+ /* Round 501 / Loop — vendor avatar inside node circles
8882
+ gains a hover-gated brightness lift. Pre-R501 the
8883
+ avatar <image> was the only per-node surface with
8884
+ NO hover treatment: R26 lifted the card, R242 tinted
8885
+ the card stroke, R427 spread the alias letter-
8886
+ spacing, R500 added the alias drop-shadow, R208
8887
+ lifted the runtime badge ring, R443 thickened
8888
+ the badge icon stroke, R177 brightened the
8889
+ halo — but the most visually-prominent element
8890
+ (the vendor logo / 书生 coin centred in each node)
8891
+ stayed paint-static. R501 closes the per-node
8892
+ hover-affordance arc by adding a 15% brightness
8893
+ lift on hover.
8894
+ Implementation: CSS filter: brightness(1.15)
8895
+ when hoveredAlias === session.alias. Pure paint
8896
+ axis on the <image> element — no geometry change,
8897
+ no bbox shift. Modern-browser supported (Chrome 64+
8898
+ / FF 56+ / Safari 9.1+).
8899
+ Hits 节点视觉 theme. data-node-avatar-hovered
8900
+ attr surfaces the gate for tests.
8901
+ Gated on !reducedMotion as a courtesy (brightness
8902
+ transition < ~50ms still feels instant; the gate
8903
+ avoids the transition cycle for a11y users). */
8904
+ const isAvatarHovered = !reducedMotion && hoveredAlias === session.alias;
7561
8905
  return (
7562
8906
  <image
7563
8907
  href={vendor.logo ?? '/intern_avatar.png'}
@@ -7566,6 +8910,12 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7566
8910
  width={size}
7567
8911
  height={size}
7568
8912
  preserveAspectRatio="xMidYMid meet"
8913
+ data-node-avatar={session.alias}
8914
+ data-node-avatar-hovered={isAvatarHovered ? 'true' : 'false'}
8915
+ style={{
8916
+ filter: isAvatarHovered ? 'brightness(1.15)' : undefined,
8917
+ transition: 'filter 200ms ease-out',
8918
+ }}
7569
8919
  />
7570
8920
  );
7571
8921
  }
@@ -7989,6 +9339,43 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7989
9339
  R211 fill 300ms + R305 letter-spacing 200ms
7990
9340
  transition list preserved; only the
7991
9341
  conditional gets a middle case. */}
9342
+ {/* Round 500 / Loop — milestone round, opens
9343
+ per-node alias drop-shadow polish. Extends the
9344
+ R476-R481 drop-shadow visual-polish family to a
9345
+ 7th anchor: hovered alias text gains a soft
9346
+ status-coloured text-glow. Pre-R500 hover on
9347
+ a node triggered card-lift (R26 translateY) +
9348
+ card-stroke (R242 tint) + alias letter-spacing
9349
+ (R427 0.3px tier) but the alias TEXT itself had
9350
+ no paint-axis cue beyond fill (R211). R500 adds
9351
+ a drop-shadow on the text glyph itself, so the
9352
+ identity glyph itself lights up under attention
9353
+ — matching the R476 idiom (hub-digit emerald
9354
+ glow on hover) at the per-node identity scope.
9355
+ 2px blur radius at 50% alpha — subtler than the
9356
+ R476 hub-digit (3px at 60%) because the alias
9357
+ text is smaller and more numerous (1 per node)
9358
+ so an aggressive glow would multiply into
9359
+ visual noise. Status-coloured (status.text) so
9360
+ the glow inherits the node's working/idle/
9361
+ offline palette — green/cyan/gray respectively.
9362
+ Drop-shadow visual-polish family — 7 anchors:
9363
+ R476 hub digit hover-gated emerald
9364
+ R477 legend pin-ring pin-gated row.fill
9365
+ R478 recent-row pip fresh-gated cyan
9366
+ R479 group-label text pin-gated cyan
9367
+ R480 hot-lane edge hot-gated amber
9368
+ R481 zoom-state minimap zoom-gated cyan
9369
+ R500 node alias text hover-gated status.text ← this round
9370
+ Filter is paint-only; bbox unchanged; overlap-
9371
+ test invariants hold (R51 selector gated to
9372
+ g[data-node] descendants with strokeWidth
9373
+ sentinels; text element doesn't carry stroke).
9374
+ transition list extends to include 'filter
9375
+ 200ms ease-out' alongside the existing fill
9376
+ 300ms + letter-spacing 200ms tweens.
9377
+ data-node-alias-glow attr surfaces the hover
9378
+ gate for tests. */}
7992
9379
  <text
7993
9380
  x="0" y="1" textAnchor="middle"
7994
9381
  fill={status.text}
@@ -7996,11 +9383,15 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7996
9383
  data-node-alias-text={session.alias}
7997
9384
  data-node-alias-chat-target={chatAlias === session.alias ? 'true' : 'false'}
7998
9385
  data-node-alias-hovered={hoveredAlias === session.alias ? 'true' : 'false'}
9386
+ data-node-alias-glow={!reducedMotion && hoveredAlias === session.alias ? 'true' : 'false'}
7999
9387
  style={{
8000
- transition: 'fill 300ms ease-out, letter-spacing 200ms ease-out',
9388
+ transition: 'fill 300ms ease-out, letter-spacing 200ms ease-out, filter 200ms ease-out',
8001
9389
  letterSpacing:
8002
9390
  chatAlias === session.alias ? '0.5px' :
8003
9391
  hoveredAlias === session.alias ? '0.3px' : '0px',
9392
+ filter: !reducedMotion && hoveredAlias === session.alias
9393
+ ? `drop-shadow(0 0 2px ${status.text}80)`
9394
+ : undefined,
8004
9395
  }}
8005
9396
  >
8006
9397
  {truncate(session.alias, fullMax)}
@@ -8528,7 +9919,48 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8528
9919
  200ms ease-out' alongside R345's ls + R55's fill
8529
9920
  200ms. data-recent-panel-title-fw exposes the
8530
9921
  resolved weight for tests. */}
8531
- <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>
9922
+ {/* Round 550 / Loop drop-shadow visual-polish family
9923
+ extends to a 14th anchor: the recent-panel header
9924
+ title gains a soft pal.legendAccent glow when the
9925
+ panel has an active row (activeEdgeKey). Pre-R550
9926
+ the title's state-flip on active was 2-axis (R482
9927
+ fw 700→800 + R345 ls 0.3→0.4 when panel-hovered);
9928
+ R550 adds the paint axis so the title brightens
9929
+ paint-wise alongside the typographic tightening when
9930
+ a row inside its panel is locked.
9931
+
9932
+ Hue: pal.legendAccent + hex alpha '80' (~50%) — same
9933
+ strength as R479 group-label pin-glow at the parent
9934
+ panel-title scope. 2px blur reads soft; cyan accent
9935
+ ties the title visually to the active row's pin
9936
+ colour (cyber: cyan-300 / light: teal-600). Hex+alpha
9937
+ concat safe — pal.legendAccent is '#67e8f9' (cyber)
9938
+ or '#0d9488' (light), both 6-digit hex (banked R541:
9939
+ hex sources use hex+alpha; only hsl/color()/dynamic
9940
+ sources need color-mix).
9941
+
9942
+ Drop-shadow visual-polish family extension (14
9943
+ anchors now):
9944
+ R476 hub digit hover-gated emerald
9945
+ R477 legend pin-ring pin-gated row.fill
9946
+ R478 recent-row pip freshness cyan
9947
+ R479 group-label text pin-gated cyan
9948
+ R532-R536 hub-cluster glow QUINTET
9949
+ R537 legend swatch hover/pin row.fill
9950
+ R538 group-label hover-precedence
9951
+ R540 edge-badge text pin-gated cyan
9952
+ R543-R546 pin-active pill 4-variant arc
9953
+ R550 recent-panel title pin-gated cyan ← this round
9954
+ R550 legend-panel title pin-gated cyan ← sibling (next text below)
9955
+
9956
+ filter is paint-only; bbox unchanged; overlap-test
9957
+ invariants hold. transition list extends to include
9958
+ 'filter 200ms ease-out' alongside R345 ls + R482 fw
9959
+ + R55 fill 200ms — one motion-coherent 3-axis active-
9960
+ state lift.
9961
+ data-recent-panel-title-glow attr exposes the gate
9962
+ state for tests. */}
9963
+ <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)` : 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'}>recent signal</text>
8532
9964
  {/* R96: header count now matches what the rows show. Pre-R96
8533
9965
  this read "X msgs" off the raw messages array, but the
8534
9966
  rows below render DEDUPED flowLinks — so a fleet with 10
@@ -9272,11 +10704,46 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
9272
10704
  tier without disturbing the surrounding family
9273
10705
  baseline. data-recent-row-text-font-weight attr
9274
10706
  exposes the value for tests. */
9275
- fontWeight="500"
10707
+ /* Round 530 / Loop — extends hover-fw family
10708
+ (R416/R420/R425/R520/R521/R522, 6 anchors) to
10709
+ a 7th anchor: recent-row alias text gains
10710
+ fontWeight 500 → 600 on (isRowHovered ||
10711
+ isRowPinned). Pre-R530 R363 set fw=500
10712
+ statically; hover/pin lifted other axes
10713
+ (R55 fill brighten / R434 letter-spacing
10714
+ 3-tier / R143 translateY / R104 row bg-
10715
+ tint / R474 cadence) but the fw stayed
10716
+ flat — same asymmetry R520 closed at the
10717
+ +N more footer.
10718
+ R530 mirrors R520's pattern at the row-
10719
+ text scope. Hover OR pin (isRowActive
10720
+ union) lifts fw to 600, matching the count
10721
+ tspan's cold-state tier (R320 fw=600), so
10722
+ on active state the alias label reads at
10723
+ the same data tier as the count it sits
10724
+ next to. Inner count tspan has its own
10725
+ explicit fontWeight (600 or 700 per R320/
10726
+ R445) so parent fw lift doesn't bleed
10727
+ (inheritance overridden).
10728
+ Hover-fw family extension (7 anchors):
10729
+ R416 chip-row count digit
10730
+ R420 chrome zoom-level
10731
+ R425 hub-center digit
10732
+ R520 +N more flows footer
10733
+ R521 chrome nodeSize S/M/L inactive
10734
+ R522 chrome layout Ring/Grid inactive
10735
+ R530 recent-row alias text ← this round
10736
+ transition list extends to include
10737
+ 'font-weight 200ms ease-out', matching the
10738
+ R474 cadence of the existing fill +
10739
+ letter-spacing axes on this element.
10740
+ data-recent-row-text-font-weight attr
10741
+ flips '500' → '600' on isRowActive. */
10742
+ fontWeight={(isRowHovered || isRowPinned) ? '600' : '500'}
9276
10743
  data-recent-row-text={link.key}
9277
10744
  data-recent-row-text-pinned={isRowPinned ? 'true' : 'false'}
9278
10745
  data-recent-row-text-hovered={!isRowPinned && isRowHovered ? 'true' : 'false'}
9279
- data-recent-row-text-font-weight="500"
10746
+ data-recent-row-text-font-weight={(isRowHovered || isRowPinned) ? '600' : '500'}
9280
10747
  /* Round 434 / Loop: recent-signal row text extends
9281
10748
  from R220's pin-only letter-spacing (0 → 0.5 on
9282
10749
  isRowPinned) to a 3-tier scale matching R433
@@ -9327,7 +10794,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
9327
10794
  shifts. */
9328
10795
  data-recent-row-text-transition="200ms"
9329
10796
  style={{
9330
- transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out',
10797
+ transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out, font-weight 200ms ease-out',
9331
10798
  letterSpacing: isRowPinned ? '0.5px' :
9332
10799
  isRowHovered ? '0.25px' : '0px',
9333
10800
  }}
@@ -9410,12 +10877,30 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
9410
10877
  under the same R320 fill cadence. data-
9411
10878
  recent-row-count-pinned attr exposes the
9412
10879
  pin gate for tests. */}
10880
+ {/* Round 498 / Loop — hot-count subtle pulse. Pre-
10881
+ R498 the hot row count signaled via color (R127
10882
+ amber fill) + weight (R320 fw-700) + (R445 pin
10883
+ lift) but stayed visually motionless. R498 adds
10884
+ a 3s opacity breath (0.85↔1.0) on the digit when
10885
+ isHot && !reducedMotion — gentle "alive" signal
10886
+ on the lane carrying ≥ 10 messages, drawing
10887
+ glance without becoming noisy. Sibling of R497
10888
+ hub-idle-breath in the 呼吸感 theme arc; same
10889
+ 0.85↔1.0 amplitude. Class adds an animation-
10890
+ only paint axis; no layout / bbox change. R29
10891
+ blanket also catches `animation-duration` for
10892
+ reducedMotion users, but the component-side
10893
+ gate makes the intent explicit and avoids
10894
+ a node tree thrash for those users (className
10895
+ stays absent rather than present-but-paused). */}
9413
10896
  <tspan
9414
10897
  fill={isHot ? hotStroke : undefined}
9415
10898
  fontWeight={(isHot || isRowPinned) ? '700' : '600'}
10899
+ className={isHot && !reducedMotion ? 'anet-recent-hot-pulse' : undefined}
9416
10900
  data-recent-row-count
9417
10901
  data-recent-row-count-pinned={isRowPinned ? 'true' : 'false'}
9418
10902
  data-recent-row-count-font-weight={(isHot || isRowPinned) ? '700' : '600'}
10903
+ data-recent-row-count-hot-pulse={isHot && !reducedMotion ? 'true' : 'false'}
9419
10904
  {...(isHot ? { 'data-recent-row-count-hot': 'true' } : {})}
9420
10905
  style={{
9421
10906
  transition: 'fill 300ms ease-out, font-weight 200ms ease-out',
@@ -9659,6 +11144,39 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
9659
11144
  stays as is, so the rest-vs-hover delta still
9660
11145
  reads clearly. data-recent-panel-more-font-weight
9661
11146
  attr exposes the value for tests. */}
11147
+ {/* Round 520 / Loop — extends the `+N more flows` footer
11148
+ to a 5-axis hover signature by adding fontWeight
11149
+ 500 → 600 on hover. Pre-R520 the footer carried 4
11150
+ hover axes:
11151
+ R195 fill legendText → legendAccent
11152
+ R325 letter-spacing 0.2 → 0.3px (R344 tween)
11153
+ R325 opacity 0.55 → 0.85
11154
+ R133 underline none → underline
11155
+ R368 had set fontWeight 500 statically as a sibling
11156
+ to R363/R364/R366 small-text fw lift family — but
11157
+ the footer's hover state didn't carry a fontWeight
11158
+ DELTA the way other interactive surfaces do (chip-
11159
+ row counts R416, chrome zoom-level R420, hub digit
11160
+ R425). R520 adds the missing weight axis: fw 500
11161
+ → 600 on hover, so the footer reads "thickening AND
11162
+ lighting up" under cursor — same idiom as the
11163
+ chrome zoom-level R420 / chip-row digit R416 hover-
11164
+ bold pattern, applied at the panel nav-action
11165
+ surface.
11166
+ data-recent-panel-more-font-weight attr value
11167
+ flips from '500' → '600' on hover (was static
11168
+ '500' pre-R520).
11169
+ Bonus closure — R475 panel-text cadence: pre-R520
11170
+ the footer's transition list had `opacity 150ms`
11171
+ while R475 unified panel-text transitions at
11172
+ 200ms. R518 closed the same gap at legend-count.
11173
+ R520 closes the LAST panel-text 150ms holdout
11174
+ here AND adds the new font-weight 200ms axis. All
11175
+ 4 transition properties (opacity / fill / letter-
11176
+ spacing / font-weight) now uniform 200ms at the
11177
+ footer — same cadence as legend-label / legend-
11178
+ count / recent-row alias / recent-row count /
11179
+ group-label. */}
9662
11180
  <text
9663
11181
  x="115" y="82"
9664
11182
  textAnchor="middle"
@@ -9666,14 +11184,15 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
9666
11184
  fontSize="9"
9667
11185
  fontFamily="monospace"
9668
11186
  fontStyle="italic"
9669
- fontWeight="500"
11187
+ fontWeight={hoveredRecentMore ? '600' : '500'}
9670
11188
  letterSpacing={hoveredRecentMore ? '0.3' : '0.2'}
9671
11189
  opacity={hoveredRecentMore ? 0.85 : 0.55}
9672
11190
  textDecoration={hoveredRecentMore ? 'underline' : 'none'}
9673
11191
  data-recent-panel-more={moreCount}
9674
11192
  data-recent-panel-more-hovered={hoveredRecentMore ? 'true' : 'false'}
9675
- data-recent-panel-more-font-weight="500"
9676
- style={{ transition: 'opacity 150ms ease-out, fill 200ms ease-out, letter-spacing 200ms ease-out' }}
11193
+ data-recent-panel-more-font-weight={hoveredRecentMore ? '600' : '500'}
11194
+ data-recent-panel-more-transition="200ms"
11195
+ style={{ transition: 'opacity 200ms ease-out, fill 200ms ease-out, letter-spacing 200ms ease-out, font-weight 200ms ease-out' }}
9677
11196
  >
9678
11197
  {`+ ${moreCount}`}
9679
11198
  <tspan opacity="0.7" data-recent-panel-more-unit>{` more flow${moreCount === 1 ? '' : 's'}`}</tspan>
@@ -9780,7 +11299,17 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
9780
11299
  data-legend-panel-title-fw + -active exposed for tests. */}
9781
11300
  {/* R345 sibling — legend panel title same hover letter-
9782
11301
  spacing tween 0.3 → 0.4 on panel hover. */}
9783
- <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>
11302
+ {/* Round 550 sibling legend-panel header title mirrors
11303
+ the recent-panel title above: drop-shadow glow on
11304
+ pin-gated active state (pinnedStatus). Same hue
11305
+ (pal.legendAccent + hex alpha 80), same 2px blur,
11306
+ same 200ms ease-out cadence. Family lifts to 15
11307
+ anchors with this sibling (counted as R550-sibling
11308
+ for accounting parity with R532-R536 hub-cluster
11309
+ glow quintet pattern — two co-shipping anchors
11310
+ under a single round number).
11311
+ data-legend-panel-title-glow attr added. */}
11312
+ <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)` : 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'}>legend</text>
9784
11313
  {/* Round 257 / Loop: legend panel header count picks up the
9785
11314
  symmetric 13L/13R inner-padding pattern from the recent-
9786
11315
  signal panel. Pre-R257 the legend header was 13px from
@@ -10069,15 +11598,42 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10069
11598
  R181 pin ring (6 + 0 stroke vs 8 - 0.75 inner
10070
11599
  ≈ 7.25). data-legend-swatch is unchanged so
10071
11600
  R197 / R55 / R61 tests probe the same handle. */}
11601
+ {/* Round 537 / Loop — extends drop-shadow visual-polish
11602
+ family (12 anchors after R536) to a 13th anchor: the
11603
+ legend swatch gains drop-shadow glow on hover/pin
11604
+ using its OWN row fill color (working green / idle
11605
+ teal / offline slate). Pre-R537 the swatch lifted
11606
+ only r (R197/R295 6 → 7) on attention — geometry
11607
+ axis only, no paint glow. R537 adds the paint axis,
11608
+ composing with R181/R402 pin-ring (separate concen-
11609
+ tric circle in the same row.fill color) so on
11610
+ hover/pin the SWATCH AND its pin-ring both contri-
11611
+ bute to a unified tier-coloured glow signature.
11612
+ Hue: row.fill (status hex) concatenated with `99`
11613
+ hex alpha (~60%). Working green / idle teal /
11614
+ offline slate each glow in their OWN tier color
11615
+ — the legend acts as a color-keyed status mirror.
11616
+ 3px blur reads soft; 60% alpha legible without
11617
+ overwhelming the swatch's own paint.
11618
+ Drop-shadow visual-polish family extension (13
11619
+ anchors). filter is paint-only; bbox unchanged.
11620
+ transition list extends to include 'filter 150ms
11621
+ ease-out', matching the existing R197 r 150ms
11622
+ cadence at this swatch. data-legend-swatch-glow
11623
+ attr exposes the gate state for tests. */}
10072
11624
  <circle
10073
11625
  cx="16" cy={row.y0}
10074
11626
  r="6"
10075
11627
  fill={row.fill}
10076
11628
  data-legend-swatch={row.key}
10077
11629
  data-legend-swatch-state={isPinned ? 'pinned' : isRowHovered ? 'hover' : 'idle'}
11630
+ data-legend-swatch-glow={(isRowHovered || isPinned) ? 'true' : 'false'}
10078
11631
  style={{
10079
11632
  r: isRowHovered || isPinned ? '7px' : '6px',
10080
- transition: 'r 150ms ease-out',
11633
+ filter: (isRowHovered || isPinned)
11634
+ ? `drop-shadow(0 0 3px ${row.fill}99)`
11635
+ : undefined,
11636
+ transition: 'r 150ms ease-out, filter 150ms ease-out',
10081
11637
  } as React.CSSProperties}
10082
11638
  />
10083
11639
  {/* R61 pinned-state ring — concentric stroke at r=8 in
@@ -10209,11 +11765,42 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10209
11765
  the value for tests. R219 letter-spacing pin
10210
11766
  tween + R55 fill transition + R181 always-mount
10211
11767
  pin ring all preserved. */
10212
- fontWeight="500"
11768
+ /* Round 531 / Loop — extends hover-fw family (R416/
11769
+ R420/R425/R520/R521/R522/R530, 7 anchors) to an
11770
+ 8th anchor at the legend-row label. Pre-R531
11771
+ R364 set fw=500 statically; hover/pin lifted
11772
+ other axes (R55 fill brighten / R433 letter-
11773
+ spacing 3-tier / R181 pin ring) but the fw
11774
+ stayed flat. R531 mirrors R530's recent-row
11775
+ alias pattern at the legend-row label scope.
11776
+ Hover OR pin (hoveredStatus===row.key ||
11777
+ isPinned) lifts fw to 600, matching the
11778
+ legend-row count tier (R309 fw=600 / R446
11779
+ pin lift 600→700). Active label now reads at
11780
+ the count's data tier — sibling treatment to
11781
+ R530 recent-row.
11782
+ Hover-fw family extension (8 anchors):
11783
+ R416 chip-row count digit
11784
+ R420 chrome zoom-level
11785
+ R425 hub-center digit
11786
+ R520 +N more flows footer
11787
+ R521 chrome nodeSize S/M/L inactive
11788
+ R522 chrome layout Ring/Grid inactive
11789
+ R530 recent-row alias text
11790
+ R531 legend-row label ← this round
11791
+ Two panel-row label surfaces (R530 recent-
11792
+ row alias + R531 legend-row label) now have
11793
+ parallel hover-fw signatures. R475 cadence
11794
+ at 200ms already covers font-weight via the
11795
+ existing transition list extension at this
11796
+ element. data-legend-row-label-font-weight
11797
+ attr flips '500' → '600' on isActive (was
11798
+ static '500' pre-R531). */
11799
+ fontWeight={(hoveredStatus === row.key || isPinned) ? '600' : '500'}
10213
11800
  data-legend-row-label={row.key}
10214
11801
  data-legend-row-label-pinned={isPinned ? 'true' : 'false'}
10215
11802
  data-legend-row-label-hovered={!isPinned && hoveredStatus === row.key ? 'true' : 'false'}
10216
- data-legend-row-label-font-weight="500"
11803
+ data-legend-row-label-font-weight={(hoveredStatus === row.key || isPinned) ? '600' : '500'}
10217
11804
  /* Round 433 / Loop: legend-row text extends from
10218
11805
  R219's pin-only letter-spacing (0px → 0.5px on
10219
11806
  isPinned) to a 3-tier scale matching the R432
@@ -10256,7 +11843,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10256
11843
  the timing axis shifts. */
10257
11844
  data-legend-row-label-transition="200ms"
10258
11845
  style={{
10259
- transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out',
11846
+ transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out, font-weight 200ms ease-out',
10260
11847
  letterSpacing: isPinned ? '0.5px' :
10261
11848
  hoveredStatus === row.key ? '0.25px' : '0px',
10262
11849
  }}
@@ -10390,7 +11977,43 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10390
11977
  data-legend-count-pinned={isPinned ? 'true' : 'false'}
10391
11978
  data-legend-count-font-weight={isPinned ? '700' : '600'}
10392
11979
  data-legend-count-fill={row.count > 0 && (hoveredStatus === row.key || isPinned) ? 'tier' : 'neutral'}
10393
- style={{ pointerEvents: 'none', transition: 'opacity 150ms ease-out, fill 150ms ease-out, font-weight 150ms ease-out', fontVariantNumeric: 'tabular-nums' }}
11980
+ /* Round 518 / Loop extends R433's 3-tier hover-
11981
+ letter-spacing tween from the legend-row LABEL
11982
+ (text at x=30) to the SIBLING legend-row COUNT
11983
+ digit (this text at x=215). Pre-R518 the row's
11984
+ label spread on hover/pin (R433: 0/0.25/0.5px)
11985
+ while the count digit at the row's right edge
11986
+ stayed dead-typographic — same row, two halves,
11987
+ asymmetric kerning gesture. R518 mirrors the
11988
+ 3-tier scale at the count so the WHOLE row's
11989
+ typography reads as one unit under cursor: label
11990
+ + count spread together at matching values.
11991
+ Tabular-nums (R225) makes the kerning still
11992
+ visible on 2-digit counts — each digit cell
11993
+ keeps its fixed width, but the inter-digit
11994
+ advance grows. R518 also closes R475's panel-
11995
+ row TEXT cadence at the count surface — R475
11996
+ lifted the label text transitions to 200ms but
11997
+ the count was missed; R518 lifts opacity / fill
11998
+ / font-weight from 150 → 200ms AND adds the new
11999
+ letter-spacing axis at 200ms. One transition
12000
+ list, one cadence, one motion-coherent multi-
12001
+ axis hover/pin signature across the row.
12002
+ Hover-letter-spacing family extension (10
12003
+ anchors now): R344/R345/R347/R420/R427/R431/
12004
+ R432/R433/R517/R518. R518 closes the legend-
12005
+ row pair (label R433 + count R518). data-
12006
+ legend-count-letter-spacing attr exposes the
12007
+ resolved value for tests. */
12008
+ data-legend-count-letter-spacing={isPinned ? '0.5px' : hoveredStatus === row.key ? '0.25px' : '0px'}
12009
+ data-legend-count-transition="200ms"
12010
+ style={{
12011
+ pointerEvents: 'none',
12012
+ transition: 'opacity 200ms ease-out, fill 200ms ease-out, font-weight 200ms ease-out, letter-spacing 200ms ease-out',
12013
+ fontVariantNumeric: 'tabular-nums',
12014
+ letterSpacing: isPinned ? '0.5px' :
12015
+ hoveredStatus === row.key ? '0.25px' : '0px',
12016
+ }}
10394
12017
  >{row.count}</text>
10395
12018
  </g>
10396
12019
  );
@@ -10449,6 +12072,74 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10449
12072
  spacing as typographic intent. Stays well inside the
10450
12073
  bottom-left corner; opacity 0.4 unchanged so the
10451
12074
  watermark stays a watermark. */}
12075
+ {/* Round 519 / Loop — 呼吸感 family 3rd anchor. Pre-R519 the
12076
+ breath family had 2 anchors (R497 hub idle digit + R498
12077
+ recent-row hot pulse). Both signal active state — the
12078
+ digit when canvas is idle (no work pending), the recent
12079
+ row when fresh signal arrives. R519 adds a SLOW ambient
12080
+ breath to the brand watermark — present always, not gated
12081
+ on activity state. The watermark IS the canvas-corner
12082
+ register that says "the canvas is alive even when nothing
12083
+ is happening"; a 6s opacity pulse around its 0.4 mean
12084
+ (±0.08 swing → 0.32 ↔ 0.48) reads as ambient liveness
12085
+ rather than foreground signal.
12086
+ Why 6s (not R497's 4s): the breath family now spans
12087
+ activity registers (R497 4s — idle-focal: present and
12088
+ waiting; R498 ~3s — hot signal: just arrived) and now
12089
+ ambient register (R519 6s — corner watermark: always-on
12090
+ background). Slower cadence keeps the watermark in the
12091
+ background; ~10 pct slower than R497 keeps it out of
12092
+ phase so the two anchors never beat together visibly.
12093
+ Gate: !reducedMotion. Inside the prefers-reduced-motion
12094
+ media query, SMIL animate isn't covered by globals.css
12095
+ R29 (which only kills CSS animation property), so we
12096
+ gate at JSX level — when reducedMotion is true the
12097
+ <animate> child isn't mounted and opacity stays at the
12098
+ static 0.4. data-topo-brand-watermark-breath attr
12099
+ exposes the gate state for tests.
12100
+ 呼吸感 family extension (3 anchors): R497 hub idle / R498
12101
+ recent-row hot / R519 brand watermark ambient. */}
12102
+ {/* Round 525 / Loop — focal-recede family 3rd anchor. R507
12103
+ receded the hub-center workingCount digit; R508 receded
12104
+ the hub-highlight disc; both fade to 0.85× when any non-
12105
+ hub canvas surface is hovered (alias / edge / group /
12106
+ status / vendor) — the "you're inspecting elsewhere"
12107
+ gesture. R525 extends the pattern to the brand watermark
12108
+ at canvas bottom-left, the always-on decorative brand
12109
+ element. Pre-R525 the watermark stayed at its R519
12110
+ breath baseline (0.32-0.48 SMIL pulse) regardless of
12111
+ canvas attention; post-R525 it fades to 70% wrapper
12112
+ opacity (effective 0.224-0.336 with breath) when canvas
12113
+ attention is elsewhere, matching the same focal-recede
12114
+ semantic R507/R508 establish at the hub focal cluster.
12115
+ Implementation: wrap the existing <text> in a <g>
12116
+ wrapper whose opacity multiplies with the inner text's
12117
+ SMIL-animated opacity. SVG opacity composes
12118
+ multiplicatively across the parent/child chain, so:
12119
+ normal: g.opacity=1.0 × text.opacity(SMIL 0.32-0.48) = 0.32-0.48
12120
+ recede: g.opacity=0.7 × text.opacity(SMIL 0.32-0.48) = 0.224-0.336
12121
+ SMIL on inner text continues running through both
12122
+ states; only the wrapper opacity flips. 300ms ease-out
12123
+ transition on wrapper (matches R508 hub-highlight recede
12124
+ transition).
12125
+ Gate matches R507/R508 — focal-recede is a UNIFIED
12126
+ non-hub-canvas-hover signal driving multiple anchors,
12127
+ so all three (hub digit / hub-highlight / brand
12128
+ watermark) fade together as the canvas's decorative
12129
+ register, leaving only the surface under inspection
12130
+ foregrounded.
12131
+ Focal-recede family extension (3 anchors): R507 hub
12132
+ digit / R508 hub-highlight / R525 brand watermark. */}
12133
+ <g
12134
+ opacity={(hoveredAlias || hoveredEdgeKey || hoveredGroupLabel ||
12135
+ hoveredStatus || hoveredVendor) && !hoveredHub ? 0.7 : 1}
12136
+ data-topo-brand-watermark-wrapper
12137
+ data-topo-brand-watermark-recede={
12138
+ (hoveredAlias || hoveredEdgeKey || hoveredGroupLabel ||
12139
+ hoveredStatus || hoveredVendor) && !hoveredHub ? 'true' : 'false'
12140
+ }
12141
+ style={{ pointerEvents: 'none', transition: 'opacity 300ms ease-out' }}
12142
+ >
10452
12143
  <text
10453
12144
  x="16" y="672"
10454
12145
  fontSize="11" fontFamily="monospace" fontWeight="600"
@@ -10456,8 +12147,12 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10456
12147
  fill={pal.legendText}
10457
12148
  opacity="0.4"
10458
12149
  data-topo-brand-watermark
12150
+ data-topo-brand-watermark-breath={reducedMotion ? 'false' : 'true'}
10459
12151
  style={{ pointerEvents: 'none', transition: 'fill 200ms ease-out' }}
10460
- >sleep2agi</text>
12152
+ >sleep2agi{!reducedMotion && (
12153
+ <animate attributeName="opacity" values="0.32;0.48;0.32" dur="6s" repeatCount="indefinite" />
12154
+ )}</text>
12155
+ </g>
10461
12156
  {/* v0.10.0 Hero 3 Wave 1 / RFC §3.I (Vincent 5215 + 通信龙
10462
12157
  lead-autonomy Q4 dual-anchor minimal): canvas top-left
10463
12158
  crescent moon brand mark, visible ONLY when the
@@ -10492,10 +12187,47 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10492
12187
  the R175 panel-fade-in uses for cascade rhythm. data-
10493
12188
  topo-brand-canvas-mark-visible exposes the gate for
10494
12189
  tests. */}
12190
+ {/* Round 526 / Loop — focal-recede family 4th anchor.
12191
+ Symmetric polish to R525 (watermark recede). The
12192
+ brand crescent at canvas top-left is the second
12193
+ decorative brand element on the canvas; pre-R526 it
12194
+ stayed at flat opacity 0.35 (when visible) regardless
12195
+ of canvas attention. R526 multiplies its visible
12196
+ opacity by 0.7 when ANY non-hub canvas surface is
12197
+ hovered, matching R525's deeper-recede semantic for
12198
+ decorative brand elements (vs hub focal cluster's
12199
+ 0.85× recede at R507/R508).
12200
+ Composes cleanly with existing flowLinks gate:
12201
+ normal, flowLinks=0: opacity = 0.35 * 1.0 = 0.350
12202
+ recede, flowLinks=0: opacity = 0.35 * 0.7 = 0.245
12203
+ invisible, flowLinks>0: opacity = 0.00 * any = 0.000
12204
+ Multiplicative chain means recede only matters when
12205
+ crescent is visible (quiet canvas, flowLinks=0) —
12206
+ exactly when canvas attention elsewhere should
12207
+ dim the decorative register. 300ms transition
12208
+ already covers both axes (the existing visibility
12209
+ opacity ramp + the new recede multiplier easing).
12210
+ Focal-recede family extension (4 anchors): R507 hub
12211
+ digit / R508 hub-highlight / R525 watermark / R526
12212
+ crescent (this round). Canvas brand surfaces (R525
12213
+ watermark + R526 crescent) now BOTH carry focal-
12214
+ recede at the same 0.7 multiplier, fading as a
12215
+ decorative pair when the canvas's focal attention
12216
+ shifts elsewhere.
12217
+ data-topo-brand-canvas-mark-recede attr exposes the
12218
+ gate state for tests. */}
10495
12219
  <g
10496
- opacity={flowLinks.length === 0 ? 0.35 : 0}
12220
+ opacity={(flowLinks.length === 0 ? 0.35 : 0) * (
12221
+ (hoveredAlias || hoveredEdgeKey || hoveredGroupLabel ||
12222
+ hoveredStatus || hoveredVendor) && !hoveredHub ? 0.7 : 1
12223
+ )}
10497
12224
  data-topo-brand-canvas-mark
10498
12225
  data-topo-brand-canvas-mark-visible={flowLinks.length === 0 ? 'true' : 'false'}
12226
+ data-topo-brand-canvas-mark-recede={
12227
+ (hoveredAlias || hoveredEdgeKey || hoveredGroupLabel ||
12228
+ hoveredStatus || hoveredVendor) && !hoveredHub ? 'true' : 'false'
12229
+ }
12230
+ data-topo-brand-canvas-mark-breath={reducedMotion ? 'false' : 'true'}
10499
12231
  style={{ pointerEvents: 'none', transition: 'opacity 300ms ease-out, fill 200ms ease-out' }}
10500
12232
  >
10501
12233
  <defs>
@@ -10505,11 +12237,44 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10505
12237
  <circle cx="17.5" cy="13" r="10" fill="black" />
10506
12238
  </mask>
10507
12239
  </defs>
12240
+ {/* Round 528 / Loop — 呼吸感 family 4th anchor. Symmetric
12241
+ polish to R519 watermark ambient breath. The brand
12242
+ crescent at canvas top-left is the second decorative
12243
+ canvas brand surface; pre-R528 it stayed at the static
12244
+ composed opacity (wrapper 0.35 × no inner anim = flat).
12245
+ Post-R528 the inner <rect>'s fill-opacity breathes
12246
+ 0.8 ↔ 1.0 on a 7s cycle, composing multiplicatively
12247
+ with the wrapper's recede gate:
12248
+ normal visible: 0.35 × (0.8-1.0) = 0.280-0.350
12249
+ recede visible: 0.245 × (0.8-1.0) = 0.196-0.245
12250
+ invisible: 0 × any = 0
12251
+ 7s cadence intentionally OUT OF PHASE with R519
12252
+ watermark's 6s — the two ambient anchors never beat
12253
+ together visibly when both visible. R497 hub idle
12254
+ breath (4s) is the loudest; R498 recent-row hot pulse
12255
+ (~3s) is the most-active; R519 watermark (6s) +
12256
+ R528 crescent (7s) are the quietest ambient pair.
12257
+ 呼吸感 family extension (4 anchors):
12258
+ R497 hub idle digit 4s active-idle register
12259
+ R498 recent-row hot pulse 3s active-fresh register
12260
+ R519 watermark ambient 6s ambient (always-on)
12261
+ R528 crescent ambient 7s ambient (quiet-only) ← this round
12262
+ SMIL <animate> on fill-opacity (not parent opacity) so
12263
+ the wrapper's React-controlled gate compositions stay
12264
+ intact. Gated on !reducedMotion at JSX level —
12265
+ reducedMotion users see the inner rect at default
12266
+ fill-opacity=1.0 (no SMIL mounted, wrapper's static
12267
+ composed opacity wins). data-topo-brand-canvas-mark-
12268
+ breath attr exposes the gate state. */}
10508
12269
  <rect
10509
12270
  x="16" y="16" width="28" height="28"
10510
12271
  fill={pal.legendText}
10511
12272
  mask="url(#s2a-canvas-corner-mask)"
10512
- />
12273
+ >
12274
+ {!reducedMotion && (
12275
+ <animate attributeName="fill-opacity" values="0.8;1;0.8" dur="7s" repeatCount="indefinite" />
12276
+ )}
12277
+ </rect>
10513
12278
  </g>
10514
12279
  </svg>
10515
12280
 
@@ -10861,12 +12626,32 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
10861
12626
  (the minimap viewport is small, ~120×82 px).
10862
12627
  Filter is paint-only — bbox unchanged. transition
10863
12628
  list extends to include 'filter 200ms ease-out'
10864
- so the glow eases when zoom crosses 1.5x. */
10865
- data-topo-minimap-viewport-glow={view.zoom > 1.5 ? 'true' : 'false'}
12629
+ so the glow eases when zoom crosses 1.5x.
12630
+ R540: extends the drop-shadow to also fire on
12631
+ hoveredMinimap with HOVER PRECEDENCE over zoom-
12632
+ state. Pre-R540 the viewport drop-shadow was
12633
+ zoom-only (single gate); R540 adds an
12634
+ interactional gate at lighter blur intensity.
12635
+ Hover wins when both true — interactional signal
12636
+ (user is inspecting) trumps informational signal
12637
+ (you're zoomed). Sibling to R534 edge-badge
12638
+ hover-precedence + R538 group-label hover-tier
12639
+ extensions.
12640
+ 2-tier alpha ladder:
12641
+ hover (interactional) legendAccent 99 (~60%)
12642
+ zoom > 1.5 (info) legendAccent 80 (~50%)
12643
+ rest none
12644
+ data-topo-minimap-viewport-glow attr upgraded
12645
+ binary ('true'/'false') → 3-value ('hover' |
12646
+ 'zoom' | 'false') so tests can distinguish
12647
+ gate cause. */
12648
+ data-topo-minimap-viewport-glow={hoveredMinimap ? 'hover' : view.zoom > 1.5 ? 'zoom' : 'false'}
10866
12649
  style={{
10867
- filter: view.zoom > 1.5
10868
- ? `drop-shadow(0 0 2px ${pal.legendAccent}80)`
10869
- : undefined,
12650
+ filter: hoveredMinimap
12651
+ ? `drop-shadow(0 0 2px ${pal.legendAccent}99)`
12652
+ : view.zoom > 1.5
12653
+ ? `drop-shadow(0 0 2px ${pal.legendAccent}80)`
12654
+ : undefined,
10870
12655
  transition: smoothView
10871
12656
  ? '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'
10872
12657
  : 'stroke-width 200ms ease-out, opacity 200ms ease-out, filter 200ms ease-out',
@@ -11007,7 +12792,41 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
11007
12792
  // transition-colors only — without the transform transition,
11008
12793
  // active:scale-95 would hard-cut. transform-gpu promotes the
11009
12794
  // layer so scale doesn't trigger paint thrash.
11010
- 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' : ''}`}
12795
+ /* Round 521 / Loop extends R270's hover-preview pattern
12796
+ (inactive toggle hover previews the active state's
12797
+ visual register) to the TYPOGRAPHY axis at the chrome
12798
+ nodeSize S/M/L surface. Pre-R521 the inactive variant
12799
+ had `hover:bg-cyan-500/5` (R270 bg preview) but no
12800
+ typography preview — active variant uses `font-medium`
12801
+ (fw 500), inactive variant sat at default fw 400 even
12802
+ on hover.
12803
+ R521 adds `hover:font-medium` + `transition-[font-
12804
+ weight]` to the inactive variant so hovering an
12805
+ inactive S/M/L letter thickens the glyph 400 → 500,
12806
+ previewing the typography of the active state the
12807
+ click would commit to. Sibling to R421 chrome zoom-
12808
+ level fontWeight hover delta (rest 500 → hover 600)
12809
+ and R520 footer fontWeight hover (500 → 600) — same
12810
+ idiom: thicken-on-hover for chrome surfaces with a
12811
+ pre-commit gesture.
12812
+ `font-medium` (500) matches the ACTIVE variant's
12813
+ fw exactly — the inactive hover landing weight equals
12814
+ the active locked weight, so clicking commits to a
12815
+ typography state the eye already saw 'on the way in'.
12816
+ Hover-fw family extension (5 anchors now):
12817
+ R416 chip-row count digit rest 500 → hover 700/600
12818
+ R420 chrome zoom-level rest 500 → hover 600
12819
+ R425 hub-center digit rest 700 → hover 800
12820
+ R520 +N more flows footer rest 500 → hover 600
12821
+ R521 chrome nodeSize S/M/L inactive rest 400 → hover 500 ← this round
12822
+ Active variant `font-medium` unchanged so the rest-vs-
12823
+ active typography distinction stays intact when the
12824
+ user IS clicked-in (active stays at fw 500, inactive
12825
+ rest at fw 400, inactive hover preview at fw 500).
12826
+ data-topo-chrome-nodesize-hover-preview-fw="500" attr
12827
+ exposes the polish for tests. */
12828
+ className={`px-2 py-1 transition-colors transition-transform transition-[font-weight] duration-200 ease-out transform-gpu active:scale-95 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset ${idx > 0 ? 'border-l' : ''} ${nodeScale === v ? 'bg-cyan-500/15 text-cyan-300 font-medium hover:bg-cyan-500/20 hover:text-cyan-200 active:bg-cyan-500/25' : 'hover:bg-cyan-500/5 active:bg-cyan-500/15 hover:font-medium'}${chromePopping === popKey ? ' anet-chrome-pop' : ''}`}
12829
+ data-topo-chrome-nodesize-hover-preview-fw={nodeScale === v ? null : '500'}
11011
12830
  style={{ color: nodeScale === v ? undefined : pal.legendText, borderColor: pal.containerBorder }}
11012
12831
  >
11013
12832
  {lbl}
@@ -11145,10 +12964,32 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
11145
12964
  ? 'true' : 'false'
11146
12965
  }
11147
12966
  data-topo-chrome-zoom-level-hover={hoveredZoomLevel ? 'true' : 'false'}
12967
+ /* Round 517 / Loop — extends the chrome zoom-level readout
12968
+ from 2-axis (R347 letter-spacing + R420 fontWeight) to
12969
+ 3-axis hover signature by adding a color brighten to
12970
+ pal.legendHeadline. Pre-R517 the readout's color stayed
12971
+ at pal.legendText on hover; the digits got tighter
12972
+ kerning (0→0.5px) and heavier weight (500→600) but
12973
+ stayed the same legendText gray tone. R517 lifts color
12974
+ to legendHeadline on hover so the readout brightens
12975
+ into the headline tier at the same beat — matching the
12976
+ R55/R197/R239 hover-deepen-own-hue idiom that legend-
12977
+ row label + count carry at panel scope. Chrome strip's
12978
+ only data display now has full 3-axis hover signature
12979
+ (letter-spacing + fontWeight + color), parity with the
12980
+ chip-row chips' own hover-brighten pattern.
12981
+ Implementation: inline color uses the same hoveredZoom-
12982
+ Level state as R347/R420 — no new state. Transition
12983
+ already includes 'color 200ms ease-out' (R264) so the
12984
+ brighten eases under the same cadence as the kerning +
12985
+ weight tweens — one motion-coherent 3-axis lift.
12986
+ data-topo-chrome-zoom-level-color attr exposes the
12987
+ resolved color string for tests. */
12988
+ data-topo-chrome-zoom-level-color={hoveredZoomLevel ? 'headline' : 'text'}
11148
12989
  onMouseEnter={() => setHoveredZoomLevel(true)}
11149
12990
  onMouseLeave={() => setHoveredZoomLevel(false)}
11150
12991
  style={{
11151
- color: pal.legendText,
12992
+ color: hoveredZoomLevel ? pal.legendHeadline : pal.legendText,
11152
12993
  borderColor: pal.containerBorder,
11153
12994
  minWidth: 46,
11154
12995
  display: 'inline-block',
@@ -11304,8 +13145,34 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
11304
13145
  // owns transform during its 450ms run. transformOrigin
11305
13146
  // 'center' so rotation pivots around the icon's centre
11306
13147
  // (default would be top-left and the icon would arc).
13148
+ /* Round 514 / Loop — extends R352/R353 chrome icon hover-
13149
+ scale family to the reset button. Pre-R514 the reset
13150
+ icon had hover-rotate (-8°, R350) + hover-sw (2.5→2.8,
13151
+ R453) but no hover-scale, while zoom-out (R352), zoom-
13152
+ in (R352), and fullscreen (R353) icons all carried
13153
+ `group-hover:scale-110`. R514 brings the reset icon
13154
+ into the same 3-axis hover signature (rotate + sw +
13155
+ scale) as the rest of the chrome strip.
13156
+ Implementation: inline transform composes rotate +
13157
+ scale into one string. `transform: rotate(-8deg)
13158
+ scale(1.1)` on hover; `rotate(0) scale(1)` at rest.
13159
+ transformOrigin 'center' applies to both — rotation
13160
+ pivots around centre AND scale grows from centre.
13161
+ The Tailwind `group-hover:scale-110` approach can't
13162
+ work here because inline `style.transform` overrides
13163
+ className-based transforms; compose the multi-axis
13164
+ transform inline instead.
13165
+ Chrome icon hover gesture parity (post-R514):
13166
+ zoom-out scale-110 + sw-lift (R352/R454)
13167
+ zoom-in scale-110 + sw-lift (R352/R454)
13168
+ fullscreen scale-110 + sw-lift (R353/R455)
13169
+ reset scale-1.1 + sw-lift + rotate -8°
13170
+ (R514 + R453 + R350)
13171
+ reset gets the EXTRA rotate axis because R350's spin
13172
+ preview semantic is reset-specific — the rotation
13173
+ hints at the click-spin (R184) the button will fire. */
11307
13174
  style={{
11308
- transform: hoveredReset && !resetSpinning ? 'rotate(-8deg)' : 'rotate(0deg)',
13175
+ transform: hoveredReset && !resetSpinning ? 'rotate(-8deg) scale(1.1)' : 'rotate(0deg) scale(1)',
11309
13176
  transformOrigin: 'center',
11310
13177
  transition: 'transform 200ms ease-out, stroke-width 200ms ease-out',
11311
13178
  }}
@@ -11353,7 +13220,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
11353
13220
  // reset above: lift-and-compress compound transform on press).
11354
13221
  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 ${
11355
13222
  isFullscreen
11356
- ? 'bg-cyan-500/15 text-cyan-300 font-medium hover:bg-cyan-500/20 active:bg-cyan-500/25'
13223
+ ? 'bg-cyan-500/15 text-cyan-300 font-medium hover:bg-cyan-500/20 hover:text-cyan-200 active:bg-cyan-500/25'
11357
13224
  : 'hover:bg-cyan-500/5 active:bg-cyan-500/15'
11358
13225
  }${chromePopping === 'fullscreen' ? ' anet-chrome-pop' : ''}`}
11359
13226
  data-topo-chrome-fullscreen-hover-lift="true"