@sleep2agi/agent-network-dashboard 0.5.1-preview.5 → 0.5.1-preview.51

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 (200) 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 +6 -6
  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 +2 -2
  15. package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  16. package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  17. package/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  18. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  19. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  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 +2 -2
  24. package/.next/server/app/admin.segments/_full.segment.rsc +2 -2
  25. package/.next/server/app/admin.segments/_head.segment.rsc +1 -1
  26. package/.next/server/app/admin.segments/_index.segment.rsc +2 -2
  27. package/.next/server/app/admin.segments/_tree.segment.rsc +2 -2
  28. package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +1 -1
  29. package/.next/server/app/admin.segments/admin.segment.rsc +1 -1
  30. package/.next/server/app/index.html +2 -2
  31. package/.next/server/app/index.rsc +3 -3
  32. package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  33. package/.next/server/app/index.segments/_full.segment.rsc +3 -3
  34. package/.next/server/app/index.segments/_head.segment.rsc +1 -1
  35. package/.next/server/app/index.segments/_index.segment.rsc +2 -2
  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 +3 -3
  40. package/.next/server/app/login.segments/_full.segment.rsc +3 -3
  41. package/.next/server/app/login.segments/_head.segment.rsc +1 -1
  42. package/.next/server/app/login.segments/_index.segment.rsc +2 -2
  43. package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
  44. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +2 -2
  45. package/.next/server/app/login.segments/login.segment.rsc +1 -1
  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 +2 -2
  49. package/.next/server/app/logs.segments/_full.segment.rsc +2 -2
  50. package/.next/server/app/logs.segments/_head.segment.rsc +1 -1
  51. package/.next/server/app/logs.segments/_index.segment.rsc +2 -2
  52. package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
  53. package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +1 -1
  54. package/.next/server/app/logs.segments/logs.segment.rsc +1 -1
  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 +2 -2
  58. package/.next/server/app/messages.segments/_full.segment.rsc +2 -2
  59. package/.next/server/app/messages.segments/_head.segment.rsc +1 -1
  60. package/.next/server/app/messages.segments/_index.segment.rsc +2 -2
  61. package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
  62. package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +1 -1
  63. package/.next/server/app/messages.segments/messages.segment.rsc +1 -1
  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 +2 -2
  67. package/.next/server/app/node.segments/_full.segment.rsc +2 -2
  68. package/.next/server/app/node.segments/_head.segment.rsc +1 -1
  69. package/.next/server/app/node.segments/_index.segment.rsc +2 -2
  70. package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
  71. package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +1 -1
  72. package/.next/server/app/node.segments/node.segment.rsc +1 -1
  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 +2 -2
  76. package/.next/server/app/nodes.segments/_full.segment.rsc +2 -2
  77. package/.next/server/app/nodes.segments/_head.segment.rsc +1 -1
  78. package/.next/server/app/nodes.segments/_index.segment.rsc +2 -2
  79. package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
  80. package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +1 -1
  81. package/.next/server/app/nodes.segments/nodes.segment.rsc +1 -1
  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 +2 -2
  86. package/.next/server/app/server-logs.segments/_full.segment.rsc +2 -2
  87. package/.next/server/app/server-logs.segments/_head.segment.rsc +1 -1
  88. package/.next/server/app/server-logs.segments/_index.segment.rsc +2 -2
  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 +1 -1
  91. package/.next/server/app/server-logs.segments/server-logs.segment.rsc +1 -1
  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 +2 -2
  95. package/.next/server/app/settings/networks.segments/_full.segment.rsc +2 -2
  96. package/.next/server/app/settings/networks.segments/_head.segment.rsc +1 -1
  97. package/.next/server/app/settings/networks.segments/_index.segment.rsc +2 -2
  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 +1 -1
  100. package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +1 -1
  101. package/.next/server/app/settings/networks.segments/settings.segment.rsc +1 -1
  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 +2 -2
  106. package/.next/server/app/settings/tokens.segments/_full.segment.rsc +2 -2
  107. package/.next/server/app/settings/tokens.segments/_head.segment.rsc +1 -1
  108. package/.next/server/app/settings/tokens.segments/_index.segment.rsc +2 -2
  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 +1 -1
  111. package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +1 -1
  112. package/.next/server/app/settings/tokens.segments/settings.segment.rsc +1 -1
  113. package/.next/server/app/settings.html +2 -2
  114. package/.next/server/app/settings.rsc +3 -3
  115. package/.next/server/app/settings.segments/_full.segment.rsc +3 -3
  116. package/.next/server/app/settings.segments/_head.segment.rsc +1 -1
  117. package/.next/server/app/settings.segments/_index.segment.rsc +2 -2
  118. package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  119. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +2 -2
  120. package/.next/server/app/settings.segments/settings.segment.rsc +1 -1
  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 +2 -2
  125. package/.next/server/app/tasks.segments/_full.segment.rsc +2 -2
  126. package/.next/server/app/tasks.segments/_head.segment.rsc +1 -1
  127. package/.next/server/app/tasks.segments/_index.segment.rsc +2 -2
  128. package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
  129. package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +1 -1
  130. package/.next/server/app/tasks.segments/tasks.segment.rsc +1 -1
  131. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js +3 -3
  132. package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js.map +1 -1
  133. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js +1 -1
  134. package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js.map +1 -1
  135. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js +1 -1
  136. package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js.map +1 -1
  137. package/.next/server/middleware-build-manifest.js +3 -3
  138. package/.next/server/pages/404.html +2 -2
  139. package/.next/server/pages/500.html +1 -1
  140. package/.next/static/chunks/0bi-134kq-t5m.js +4 -0
  141. package/.next/static/chunks/0hwb_h953qm4d.css +2 -0
  142. package/.next/static/chunks/0sf4jjluqr_2t.js +1 -0
  143. package/.next/static/chunks/109thlclcb2ed.js +1 -0
  144. package/.next/trace +2 -2
  145. package/.next/trace-build +1 -1
  146. package/app/components/TopoGraph.tsx +986 -65
  147. package/package.json +1 -1
  148. package/scripts/topo-chip-digit-fontweight-test.mjs +105 -0
  149. package/scripts/topo-chrome-segmented-radius-test.mjs +100 -0
  150. package/scripts/topo-edge-badge-fontsize-test.mjs +90 -0
  151. package/scripts/topo-edge-badge-opacity-test.mjs +80 -0
  152. package/scripts/topo-edge-badge-stroke-test.mjs +92 -0
  153. package/scripts/topo-edge-visible-linecap-test.mjs +89 -0
  154. package/scripts/topo-filter-pill-hover-opacity-test.mjs +110 -0
  155. package/scripts/topo-filter-pill-x-hover-scale-test.mjs +99 -0
  156. package/scripts/topo-flow-rail-linecap-test.mjs +79 -0
  157. package/scripts/topo-freshness-chip-tabular-test.mjs +41 -0
  158. package/scripts/topo-freshness-floor-lift-test.mjs +92 -0
  159. package/scripts/topo-freshness-suffix-tabular-test.mjs +88 -0
  160. package/scripts/topo-fullscreen-icon-hover-scale-test.mjs +91 -0
  161. package/scripts/topo-group-box-stroke-test.mjs +105 -0
  162. package/scripts/topo-group-label-count-fontweight-test.mjs +108 -0
  163. package/scripts/topo-hover-detail-body-fw-test.mjs +101 -0
  164. package/scripts/topo-hover-detail-model-fw-test.mjs +98 -0
  165. package/scripts/topo-hover-detail-opacity-test.mjs +98 -0
  166. package/scripts/topo-hover-detail-rx-test.mjs +81 -0
  167. package/scripts/topo-hub-digit-fontsize-test.mjs +86 -0
  168. package/scripts/topo-hub-highlight-opacity-test.mjs +88 -0
  169. package/scripts/topo-hub-highlight-radius-test.mjs +90 -0
  170. package/scripts/topo-hub-hover-ring-opacity-test.mjs +96 -0
  171. package/scripts/topo-hub-hover-ring-stroke-test.mjs +86 -0
  172. package/scripts/topo-hub-spoke-linecap-test.mjs +80 -0
  173. package/scripts/topo-layout-toggle-hover-tracking-test.mjs +109 -0
  174. package/scripts/topo-layout-toggle-radius-test.mjs +87 -0
  175. package/scripts/topo-legend-label-fontweight-test.mjs +94 -0
  176. package/scripts/topo-minimap-offline-opacity-test.mjs +90 -0
  177. package/scripts/topo-minimap-online-radius-test.mjs +85 -0
  178. package/scripts/topo-minimap-viewport-hover-test.mjs +109 -0
  179. package/scripts/topo-minimap-viewport-linejoin-test.mjs +75 -0
  180. package/scripts/topo-more-flows-fontweight-test.mjs +103 -0
  181. package/scripts/topo-panel-count-letterspacing-test.mjs +89 -0
  182. package/scripts/topo-panel-rect-opacity-hover-test.mjs +109 -0
  183. package/scripts/topo-panel-title-hover-letterspacing-test.mjs +97 -0
  184. package/scripts/topo-pressure-bar-height-test.mjs +92 -0
  185. package/scripts/topo-pressure-kicker-fontweight-test.mjs +76 -0
  186. package/scripts/topo-recent-pip-radius-2-test.mjs +72 -0
  187. package/scripts/topo-recent-pip-radius-test.mjs +76 -0
  188. package/scripts/topo-recent-row-text-fontweight-test.mjs +90 -0
  189. package/scripts/topo-reset-hover-rotate-test.mjs +102 -0
  190. package/scripts/topo-vendor-glyph-fontweight-test.mjs +102 -0
  191. package/scripts/topo-vendor-letter-hover-scale-test.mjs +129 -0
  192. package/scripts/topo-zoom-icon-hover-scale-test.mjs +114 -0
  193. package/scripts/topo-zoom-level-hover-letterspacing-test.mjs +91 -0
  194. package/.next/static/chunks/020yd2d3i1yew.js +0 -1
  195. package/.next/static/chunks/0aauz~36q5n2a.css +0 -2
  196. package/.next/static/chunks/0txa4xkx0-7v-.js +0 -1
  197. package/.next/static/chunks/0xvj-x25qdu55.js +0 -4
  198. /package/.next/static/{s_gujwHXFfXWh2_yQC6Gk → KuSssxhFiQYKbTq_1BilM}/_buildManifest.js +0 -0
  199. /package/.next/static/{s_gujwHXFfXWh2_yQC6Gk → KuSssxhFiQYKbTq_1BilM}/_clientMiddlewareManifest.js +0 -0
  200. /package/.next/static/{s_gujwHXFfXWh2_yQC6Gk → KuSssxhFiQYKbTq_1BilM}/_ssgManifest.js +0 -0
@@ -194,7 +194,27 @@ function FreshnessChip({ sessions }: { sessions: unknown }) {
194
194
  // tier (R312-R314 family); the amber bg/text/border still does
195
195
  // the warning-state work, the weight just keeps the chip in the
196
196
  // same data-typography ladder as its siblings.
197
- const baseClass = "hidden sm:inline px-2.5 py-1 rounded-md font-mono font-medium border transition-colors duration-300";
197
+ // Round 377 / Loop: FreshnessChip baseClass picks up `tabular-nums`.
198
+ // The chip-row's last untouched chip joins the R224-R357 broader
199
+ // tabular-nums sweep:
200
+ // R224 edge badge digit
201
+ // R225 hub digit / panel header counts / recent row count
202
+ // R232 chip row counts (working / online / active-links)
203
+ // R321 recent row timestamp
204
+ // R322 recent panel hot count
205
+ // R323 filter pin pill counts
206
+ // R333 vendor count suffix
207
+ // R357 active-links freshness suffix wrapper
208
+ // R377 FreshnessChip body (this round)
209
+ // `font-mono` already gives equal-width glyphs but `tabular-nums`
210
+ // is the explicit-invariant the rest of the chip row carries.
211
+ // FreshnessChip body reads `lag · {sec}s` — the {sec} digit grows
212
+ // every second; tabular-nums explicitly locks digit width so the
213
+ // chip stays planted as the seconds counter ticks past 9 → 10 →
214
+ // 99 → 100. R187 transition-colors duration-300 + R275 stale-only
215
+ // render gate + R315 font-medium R313 family alignment all
216
+ // preserved.
217
+ const baseClass = "hidden sm:inline px-2.5 py-1 rounded-md font-mono font-medium tabular-nums border transition-colors duration-300";
198
218
  const colorClass = stale
199
219
  ? "bg-amber-500/10 text-amber-300 border-amber-500/20"
200
220
  : "bg-gray-500/10 text-gray-400 border-gray-500/20";
@@ -1026,6 +1046,42 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1026
1046
  // navigation footer. Drives the on-hover opacity boost + underline
1027
1047
  // that signals interactivity, mirroring the hoveredHub idiom above.
1028
1048
  const [hoveredRecentMore, setHoveredRecentMore] = useState(false);
1049
+ // Round 346 / Loop: minimap-container hover affordance. The minimap
1050
+ // is a click-target (role=button at line ~7810, recenter-on-click +
1051
+ // Enter→resetView) but pre-R346 nothing visually changed on hover —
1052
+ // the only hint was the `cursor: crosshair` style. R346 lifts the
1053
+ // viewport rect (strokeWidth 1.5 → 1.75 + opacity 0.9 → 1.0) when
1054
+ // the user enters the minimap, marking "this is the recenter target
1055
+ // and it's alive". Sibling polish to the R332 minimap rounded-md →
1056
+ // rounded-lg corner family — that round refined geometry, this one
1057
+ // gives the viewport indicator inside the geometry a hover state.
1058
+ // 280ms ease-out transition list matches R199 smoothView vocabulary
1059
+ // so the visual joins the existing rhythm on the same rect.
1060
+ const [hoveredMinimap, setHoveredMinimap] = useState(false);
1061
+ // Round 347 / Loop: zoom-level readout hover-state letter-spacing
1062
+ // tween (0 → 0.5 px). The readout sandwiched between zoom-out /
1063
+ // zoom-in is a passive percent display — pre-R347 it had no hover
1064
+ // feedback at all (only a `title` tooltip). R347 extends the R344
1065
+ // (`+N more flows` footer) + R345 (panel titles) hover-letter-
1066
+ // spacing family from panel/footer surfaces into the HTML chrome
1067
+ // strip. Hovering the readout spreads its digits 0.5 px, signalling
1068
+ // "this is alive". tabular-nums + minWidth: 46 from R225 still lock
1069
+ // the column so the tween doesn't shove neighbouring controls.
1070
+ // 200ms ease-out joins the existing R264 color/border transition
1071
+ // list on the same span.
1072
+ const [hoveredZoomLevel, setHoveredZoomLevel] = useState(false);
1073
+ // Round 350 / Loop: reset-button icon hover-rotate preview of the
1074
+ // R184 click-spin. Pre-R350 hovering the reset button only changed
1075
+ // the button bg (white/5); the icon inside stayed perfectly still.
1076
+ // R350 nudges the icon -8° on hover — a tactile hint that this
1077
+ // button rotates the icon on click. When the click fires, the
1078
+ // R184 anet-reset-spin keyframe animation overrides the hover
1079
+ // transform for its 450 ms run (CSS animations win over transitions
1080
+ // on the same property); when the animation ends + React removes
1081
+ // the className, the inline transform eases back to whatever the
1082
+ // hover state says — either -8° (still hovering) or 0 (mouse left).
1083
+ // 350th-round milestone polish.
1084
+ const [hoveredReset, setHoveredReset] = useState(false);
1029
1085
  // R135: panel-wide hover-elevation. The recent-signal + legend
1030
1086
  // panels both already host clickable rows (R56/R116 recent rows,
1031
1087
  // R55/R61 legend rows) and a clickable footer (R133), so the
@@ -1787,12 +1843,30 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1787
1843
  while honoring R328's wider baseline rhythm. data-topo-
1788
1844
  chrome-layout-trailer attr unchanged — it still marks
1789
1845
  the boundary surface for the gap probe. */}
1846
+ {/* Round 375 / Loop: Layout-toggle wrapper rounded-md → rounded-
1847
+ lg (6 → 8 px). Extends the corner-radius cascade family
1848
+ to the chrome-strip layout-toggle wrapper:
1849
+ R330 canvas wrapper rounded-xl 12 px
1850
+ R331 SVG panels rx=10 10 px
1851
+ R332 minimap container rounded-lg 8 px
1852
+ R375 Layout-toggle wrapper rounded-lg 8 px (this round)
1853
+ Pre-R375 the wrapper at rounded-md (6 px) was the only
1854
+ chrome-strip container still using the smaller corner
1855
+ radius — both R330 outer wrapper and R332 minimap sit at
1856
+ ≥ 8 px, so the Layout toggle's 6 px stood out as a
1857
+ tighter corner against the family. R375 brings it into
1858
+ the rounded-lg tier where the minimap already lives.
1859
+ Pure paint change — overflow-hidden still clips the
1860
+ inner buttons' bg-cyan-500/15 tints; no layout shift.
1861
+ R268 border-color + 200ms transition + R329 mr-0.5 +
1862
+ data-topo-chrome-layout-trailer all preserved. */}
1790
1863
  <div
1791
- className="mr-0.5 inline-flex rounded-md border overflow-hidden"
1864
+ className="mr-0.5 inline-flex rounded-lg border overflow-hidden"
1792
1865
  style={{ borderColor: pal.containerBorder, transition: 'border-color 200ms ease-out' }}
1793
1866
  role="group"
1794
1867
  aria-label="Topology layout"
1795
1868
  data-topo-chrome-layout-trailer
1869
+ data-topo-chrome-layout-radius="rounded-lg"
1796
1870
  >
1797
1871
  <button
1798
1872
  onClick={() => { popChrome('layout-ring'); if (layout !== 'ring') toggleLayout(); }}
@@ -1826,7 +1900,17 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1826
1900
  weight; cyan-400/60 + ring-inset retained. The
1827
1901
  R163/R196 hover/active deeps + R249 chrome-pop
1828
1902
  click feedback continue unchanged. */
1829
- className={`px-2.5 py-1 transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset ${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' : ''}`}
1903
+ // R351: hover:tracking-wide extends the R344/R345/R347
1904
+ // hover-letter-spacing family to a 4th surface (chrome-
1905
+ // strip Ring/Grid pair). transition-colors className
1906
+ // dropped in favour of an inline transition spec that
1907
+ // bundles bg/color (150ms ease) + letter-spacing
1908
+ // (200ms ease-out) — Tailwind's transition-colors
1909
+ // doesn't list letter-spacing, so without this the
1910
+ // hover:tracking-wide would snap. Sibling change on
1911
+ // the Grid button below.
1912
+ className={`px-2.5 py-1 focus:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset hover:tracking-wide ${layout === 'ring' ? 'bg-cyan-500/15 text-cyan-300 font-medium hover:bg-cyan-500/20 active:bg-cyan-500/25' : 'text-gray-400 hover:text-cyan-300 hover:bg-cyan-500/5 active:bg-cyan-500/15'} ${chromePopping === 'layout-ring' ? ' anet-chrome-pop' : ''}`}
1913
+ style={{ transition: 'background-color 150ms ease, color 150ms ease, letter-spacing 200ms ease-out' }}
1830
1914
  >
1831
1915
  Ring
1832
1916
  </button>
@@ -1843,14 +1927,20 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1843
1927
  // Round 306 / Loop: focus-visible:ring-2 → ring-1 sibling
1844
1928
  // change to Ring above — unifies focus-ring width across
1845
1929
  // all chrome buttons.
1846
- className={`px-2.5 py-1 border-l transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset ${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' : ''}`}
1930
+ // R351 sibling Grid button picks up hover:tracking-wide
1931
+ // + inline transition spec. Same vocabulary as Ring.
1932
+ className={`px-2.5 py-1 border-l focus:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset hover:tracking-wide ${layout === 'grid' ? 'bg-cyan-500/15 text-cyan-300 font-medium hover:bg-cyan-500/20 active:bg-cyan-500/25' : 'text-gray-400 hover:text-cyan-300 hover:bg-cyan-500/5 active:bg-cyan-500/15'} ${chromePopping === 'layout-grid' ? ' anet-chrome-pop' : ''}`}
1847
1933
  /* Round 268 / Loop: Grid button's left border (the
1848
1934
  internal divider between Ring and Grid) picks up
1849
1935
  pal.containerBorder, matching the wrapper change at
1850
1936
  line ~1460 and the chrome strip's segmented borders
1851
- (nodeSize, zoom). transition-colors className covers
1852
- the border-color eased on theme toggle. */
1853
- style={{ borderColor: pal.containerBorder }}
1937
+ (nodeSize, zoom). The R268 transition-colors className
1938
+ used to carry the border-color ease; R351 unfolds the
1939
+ transition list into the inline spec below so the
1940
+ letter-spacing tween rides alongside without snapping
1941
+ the border-color flip — border-color 200ms ease-out
1942
+ keeps R268's theme-toggle smoothness intact. */
1943
+ style={{ borderColor: pal.containerBorder, transition: 'background-color 150ms ease, color 150ms ease, border-color 200ms ease-out, letter-spacing 200ms ease-out' }}
1854
1944
  >
1855
1945
  Grid
1856
1946
  </button>
@@ -2011,7 +2101,17 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2011
2101
  pattern: small label spans demote, value stays
2012
2102
  prominent. data-working-chip-unit exposes the
2013
2103
  span for tests. */}
2014
- {workingCount}<span className="opacity-70" data-working-chip-unit> working</span>
2104
+ {/* Round 362 / Loop: digit picks up font-semibold
2105
+ (fw 500 → 600) for within-chip weight tier. The
2106
+ chip's outer className stays at font-medium (R313
2107
+ data-weight baseline); the digit overrides to
2108
+ semibold so it reads heavier than its " working"
2109
+ unit (which keeps fw 500 + R338 opacity-70).
2110
+ Joins the R333-R341 chip-internal-hierarchy arc
2111
+ at the chip-count scope. Sibling edits on the
2112
+ online + active-links chip digits below. data-
2113
+ working-chip-digit attr exposes the digit span. */}
2114
+ <span className="font-semibold" data-working-chip-digit>{workingCount}</span><span className="opacity-70" data-working-chip-unit> working</span>
2015
2115
  </span>
2016
2116
  <span
2017
2117
  // Round 201 / Loop: online chip — mirror of the working
@@ -2074,7 +2174,8 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2074
2174
  }}
2075
2175
  >
2076
2176
  {/* R337 sibling — online chip unit demotion. */}
2077
- {onlineNodes.length}<span className="opacity-70" data-online-chip-unit> online</span>
2177
+ {/* R362 sibling — online-chip digit gains font-semibold. */}
2178
+ <span className="font-semibold" data-online-chip-digit>{onlineNodes.length}</span><span className="opacity-70" data-online-chip-unit> online</span>
2078
2179
  </span>
2079
2180
  </>
2080
2181
  );
@@ -2201,8 +2302,45 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2201
2302
  title={`${w} working · ${i} idle · ${o} offline`}
2202
2303
  data-fleet-pressure
2203
2304
  >
2204
- <span className="text-[10px] tracking-wide">pressure</span>
2205
- <span className="inline-flex h-1.5 w-16 rounded-full overflow-hidden" style={{ background: 'rgb(75 85 99 / 0.25)' }}>
2305
+ {/* Round 373 / Loop: pressure-bar kicker label gains
2306
+ font-medium (fw 400 500). Sibling small-text fw
2307
+ lift family with R363 recent-row alias + R364
2308
+ legend-row label + R366 group-label count + R368
2309
+ +N more flows footer — extends to a 5th surface
2310
+ (the chip-row's 'pressure' label). At fontSize
2311
+ 10 px tracking-wide against the chip's gray bg,
2312
+ the default fw 400 sat below the deliberate-data
2313
+ band; fw 500 brings it into parity with the
2314
+ chip-row 'working / online / active links' unit
2315
+ spans (chip-level font-medium R313). data-fleet-
2316
+ pressure-kicker attr exposes the kicker for tests. */}
2317
+ <span className="text-[10px] tracking-wide font-medium" data-fleet-pressure-kicker>pressure</span>
2318
+ {/* Round 374 / Loop: pressure-bar height h-1.5 → h-2
2319
+ (6 → 8 px) — sibling visual-weight bump (8th anchor
2320
+ in the family):
2321
+ R287 minimap viewport stroke 1 → 1.5
2322
+ R295 legend swatch base radius 5.5 → 6
2323
+ R359 recent-row pip base radius 1.6 → 1.8
2324
+ R360 hub digit fontSize 11 → 12
2325
+ R361 edge-badge digit fontSize 10 → 11
2326
+ R365 hub-highlight base radius 5 → 5.5
2327
+ R367 edge-badge rest stroke 1 → 1.25
2328
+ R374 pressure-bar height 1.5 → 2 (this round)
2329
+ +33 % bar height gives the working/idle/offline
2330
+ segments more visibility — at h-1.5 the 3-segment
2331
+ proportion bar was readable but slim; at h-2 the
2332
+ segments parse cleanly even when one tier is
2333
+ < 10 % share. Geometry-safe: items-center flex
2334
+ centers the bar inside the chip's py-1 (4 px top +
2335
+ 4 px bottom) — bar at 8 px stays comfortably
2336
+ inside the 10-px text-row height. R165 segment
2337
+ width transitions + R210 brightness hover + R83
2338
+ pin-mirror box-shadow on segments all preserved
2339
+ (segments inherit width from parent so the height
2340
+ bump propagates without segment-side edits).
2341
+ data-fleet-pressure-bar-height attr exposes the
2342
+ height token for tests. */}
2343
+ <span className="inline-flex h-2 w-16 rounded-full overflow-hidden" style={{ background: 'rgb(75 85 99 / 0.25)' }} data-fleet-pressure-bar-height="h-2">
2206
2344
  {seg(w, isLight ? '#059669' : '#22c55e', 'working', 'working')}
2207
2345
  {seg(i, isLight ? '#0d9488' : '#2dd4bf', 'idle', 'idle')}
2208
2346
  {seg(o, isLight ? '#94a3b8' : '#6b7280', 'offline', 'offline')}
@@ -2259,7 +2397,10 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2259
2397
  data-active-filter="status"
2260
2398
  data-filter-match-count={matchCount}
2261
2399
  data-filter-match-aliases={matchAliases.join(',')}
2262
- 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"
2400
+ // R355: `group` lets the inner opacity-70 spans (prefix
2401
+ // `filter:` + count `· N`) brighten to 100 % on pill hover.
2402
+ // Sibling treatment on group + vendor pills below.
2403
+ 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"
2263
2404
  title={matchCount > 0 ? `${matchPreview}${matchSuffix} — click to clear` : 'Click to clear filter'}
2264
2405
  onClick={() => setPinnedStatus(null)}
2265
2406
  style={{
@@ -2273,12 +2414,26 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2273
2414
  cursor: 'pointer',
2274
2415
  }}
2275
2416
  >
2276
- <span><span className="hidden sm:inline opacity-70" data-filter-prefix>filter: </span>{pinnedStatus}<span className="opacity-70 tabular-nums" data-filter-pill-count> · {matchCount}</span></span>
2417
+ <span><span className="hidden sm:inline opacity-70 transition-opacity duration-200 group-hover:opacity-100" data-filter-prefix>filter: </span>{pinnedStatus}<span className="opacity-70 tabular-nums transition-opacity duration-200 group-hover:opacity-100" data-filter-pill-count> · {matchCount}</span></span>
2277
2418
  <button
2278
2419
  type="button"
2279
2420
  aria-label={`Clear ${pinnedStatus} filter`}
2280
2421
  onClick={(e) => { e.stopPropagation(); setPinnedStatus(null); }}
2281
- className="ml-0.5 leading-none hover:opacity-70"
2422
+ /* Round 356 / Loop: filter pin pill × buttons gain
2423
+ hover:scale-110 (Tailwind 4 modern CSS `scale` property,
2424
+ not legacy transform). Sibling polish to R354 vendor
2425
+ letter glyph + R350/R352/R353 chrome icon hover-scales.
2426
+ Pre-R356 the × had only hover:opacity-70 — the target
2427
+ dimmed under cursor but didn't lift. R356 adds a 10 %
2428
+ scale on hover so the click-target reads as "press me"
2429
+ alongside the dim. transform-gpu hint promotes the
2430
+ button to its own compositor layer for crisper edges
2431
+ during the scale tween. transition-transform duration-
2432
+ 200 matches the chrome icon hover-scale timing family.
2433
+ inline-block is default for <button> so no display
2434
+ tweak needed. replace_all covers all 4 filter pin
2435
+ pills (status / group / vendor / edge) at once. */
2436
+ className="ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 transform-gpu"
2282
2437
  style={{ background: 'transparent', color: 'inherit', cursor: 'pointer', padding: 0 }}
2283
2438
  >×</button>
2284
2439
  </span>
@@ -2297,7 +2452,8 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2297
2452
  data-active-filter="group"
2298
2453
  data-filter-match-count={matchCount}
2299
2454
  data-filter-match-aliases={matchAliases.join(',')}
2300
- 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"
2455
+ // R355 sibling `group` parent + group-hover on inner spans.
2456
+ 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"
2301
2457
  title={matchCount > 0 ? `${matchPreview}${matchSuffix} — click to clear` : 'Click to clear filter'}
2302
2458
  onClick={() => setPinnedGroup(null)}
2303
2459
  style={{
@@ -2307,12 +2463,26 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2307
2463
  cursor: 'pointer',
2308
2464
  }}
2309
2465
  >
2310
- <span><span className="hidden sm:inline opacity-70" data-filter-prefix>filter: </span>{pinnedGroup}<span className="opacity-70 tabular-nums" data-filter-pill-count> · {matchCount}</span></span>
2466
+ <span><span className="hidden sm:inline opacity-70 transition-opacity duration-200 group-hover:opacity-100" data-filter-prefix>filter: </span>{pinnedGroup}<span className="opacity-70 tabular-nums transition-opacity duration-200 group-hover:opacity-100" data-filter-pill-count> · {matchCount}</span></span>
2311
2467
  <button
2312
2468
  type="button"
2313
2469
  aria-label={`Clear group filter ${pinnedGroup}`}
2314
2470
  onClick={(e) => { e.stopPropagation(); setPinnedGroup(null); }}
2315
- className="ml-0.5 leading-none hover:opacity-70"
2471
+ /* Round 356 / Loop: filter pin pill × buttons gain
2472
+ hover:scale-110 (Tailwind 4 modern CSS `scale` property,
2473
+ not legacy transform). Sibling polish to R354 vendor
2474
+ letter glyph + R350/R352/R353 chrome icon hover-scales.
2475
+ Pre-R356 the × had only hover:opacity-70 — the target
2476
+ dimmed under cursor but didn't lift. R356 adds a 10 %
2477
+ scale on hover so the click-target reads as "press me"
2478
+ alongside the dim. transform-gpu hint promotes the
2479
+ button to its own compositor layer for crisper edges
2480
+ during the scale tween. transition-transform duration-
2481
+ 200 matches the chrome icon hover-scale timing family.
2482
+ inline-block is default for <button> so no display
2483
+ tweak needed. replace_all covers all 4 filter pin
2484
+ pills (status / group / vendor / edge) at once. */
2485
+ className="ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 transform-gpu"
2316
2486
  style={{ background: 'transparent', color: 'inherit', cursor: 'pointer', padding: 0 }}
2317
2487
  >×</button>
2318
2488
  </span>
@@ -2347,7 +2517,8 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2347
2517
  data-active-filter="vendor"
2348
2518
  data-filter-match-count={matchCount}
2349
2519
  data-filter-match-aliases={matchAliases.join(',')}
2350
- 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"
2520
+ // R355 sibling `group` parent + group-hover on inner spans.
2521
+ 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"
2351
2522
  title={matchCount > 0 ? `${matchPreview}${matchSuffix} — click to clear` : 'Click to clear vendor filter'}
2352
2523
  onClick={() => setPinnedVendor(null)}
2353
2524
  style={{
@@ -2357,12 +2528,26 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2357
2528
  cursor: 'pointer',
2358
2529
  }}
2359
2530
  >
2360
- <span><span className="hidden sm:inline opacity-70" data-filter-prefix>filter: </span>{pinnedVendor}<span className="opacity-70 tabular-nums" data-filter-pill-count> · {matchCount}</span></span>
2531
+ <span><span className="hidden sm:inline opacity-70 transition-opacity duration-200 group-hover:opacity-100" data-filter-prefix>filter: </span>{pinnedVendor}<span className="opacity-70 tabular-nums transition-opacity duration-200 group-hover:opacity-100" data-filter-pill-count> · {matchCount}</span></span>
2361
2532
  <button
2362
2533
  type="button"
2363
2534
  aria-label={`Clear vendor filter ${pinnedVendor}`}
2364
2535
  onClick={(e) => { e.stopPropagation(); setPinnedVendor(null); }}
2365
- className="ml-0.5 leading-none hover:opacity-70"
2536
+ /* Round 356 / Loop: filter pin pill × buttons gain
2537
+ hover:scale-110 (Tailwind 4 modern CSS `scale` property,
2538
+ not legacy transform). Sibling polish to R354 vendor
2539
+ letter glyph + R350/R352/R353 chrome icon hover-scales.
2540
+ Pre-R356 the × had only hover:opacity-70 — the target
2541
+ dimmed under cursor but didn't lift. R356 adds a 10 %
2542
+ scale on hover so the click-target reads as "press me"
2543
+ alongside the dim. transform-gpu hint promotes the
2544
+ button to its own compositor layer for crisper edges
2545
+ during the scale tween. transition-transform duration-
2546
+ 200 matches the chrome icon hover-scale timing family.
2547
+ inline-block is default for <button> so no display
2548
+ tweak needed. replace_all covers all 4 filter pin
2549
+ pills (status / group / vendor / edge) at once. */
2550
+ className="ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 transform-gpu"
2366
2551
  style={{ background: 'transparent', color: 'inherit', cursor: 'pointer', padding: 0 }}
2367
2552
  >×</button>
2368
2553
  </span>
@@ -2439,7 +2624,21 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2439
2624
  type="button"
2440
2625
  aria-label={`Clear edge filter ${link.from} → ${link.to}`}
2441
2626
  onClick={(e) => { e.stopPropagation(); setPinnedEdgeKey(null); }}
2442
- className="ml-0.5 leading-none hover:opacity-70"
2627
+ /* Round 356 / Loop: filter pin pill × buttons gain
2628
+ hover:scale-110 (Tailwind 4 modern CSS `scale` property,
2629
+ not legacy transform). Sibling polish to R354 vendor
2630
+ letter glyph + R350/R352/R353 chrome icon hover-scales.
2631
+ Pre-R356 the × had only hover:opacity-70 — the target
2632
+ dimmed under cursor but didn't lift. R356 adds a 10 %
2633
+ scale on hover so the click-target reads as "press me"
2634
+ alongside the dim. transform-gpu hint promotes the
2635
+ button to its own compositor layer for crisper edges
2636
+ during the scale tween. transition-transform duration-
2637
+ 200 matches the chrome icon hover-scale timing family.
2638
+ inline-block is default for <button> so no display
2639
+ tweak needed. replace_all covers all 4 filter pin
2640
+ pills (status / group / vendor / edge) at once. */
2641
+ className="ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 transform-gpu"
2443
2642
  style={{ background: 'transparent', color: 'inherit', cursor: 'pointer', padding: 0 }}
2444
2643
  >×</button>
2445
2644
  </span>
@@ -2750,7 +2949,64 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2750
2949
  }
2751
2950
  }}
2752
2951
  >
2753
- <span style={{ color: v.color }}>{v.initial}</span>
2952
+ {/* Round 354 / Loop: vendor letter glyph scales
2953
+ 1.0 → 1.1 on hover. R88 already dims OTHER
2954
+ vendors on hover via canvas-wide opacity
2955
+ masking; R202 added a chip-level bg tint
2956
+ (color-mix 12 % alpha) so the chip itself
2957
+ responds. R354 closes the trio with a glyph-
2958
+ level lift: the focused vendor LETTER actively
2959
+ rises (transform scale) rather than the chip
2960
+ merely changing colour. Three layers of positive
2961
+ feedback on the hovered vendor + canvas-wide
2962
+ negative feedback on the others — a clean
2963
+ figure/ground separation.
2964
+
2965
+ display: inline-block is required for transform
2966
+ to apply (inline elements ignore transform).
2967
+ transformOrigin: 'center' so the glyph pivots
2968
+ around its centre instead of arcing from the
2969
+ baseline anchor. transition rides the existing
2970
+ Tailwind 4 transform/scale list (no new
2971
+ property — Tailwind already lists transform in
2972
+ the default transition-property set). 200ms
2973
+ matches the R202 chip bg-tint timing so the
2974
+ glyph lift and chip background ease in concert. */}
2975
+ {/* Round 369 / Loop: vendor letter glyph picks up
2976
+ fontWeight 600 (font-semibold). The glyph is the
2977
+ vendor identifier — the DATA the operator scans
2978
+ in this chip (A / O / 书 / C / G / ?). R333 set
2979
+ the count suffix `:N` to text-gray-400 + tabular-
2980
+ nums and (via parent inheritance) fw 500. Pre-
2981
+ R369 the LETTER also inherited fw 500 from the
2982
+ chip's font-medium — letter and count read at
2983
+ the same weight, contradicting the data-vs-label
2984
+ hierarchy the rest of the chip-row already speaks.
2985
+ R369 lifts the letter to fw 600 so the chip now
2986
+ reads as the same two-tier pattern R362 closed
2987
+ on the working / online / active-links chips:
2988
+ chip digit/letter fw 600 (data)
2989
+ chip unit/count fw 500 (label)
2990
+ Sibling treatment to R362 — extends the R333-R341
2991
+ chip-internal-hierarchy arc to the vendor-letter
2992
+ chip surface (9th surface family). R354 transform-
2993
+ scale-on-hover + R88 canvas-dim-others + R202
2994
+ chip bg color-mix all preserved on the same span.
2995
+ data-vendor-letter-glyph-font-weight attr exposes
2996
+ the value for tests. */}
2997
+ <span
2998
+ data-vendor-letter-glyph={v.initial}
2999
+ data-vendor-letter-glyph-hover={hoveredVendor === v.initial ? 'true' : 'false'}
3000
+ data-vendor-letter-glyph-font-weight="600"
3001
+ style={{
3002
+ color: v.color,
3003
+ display: 'inline-block',
3004
+ fontWeight: 600,
3005
+ transform: hoveredVendor === v.initial ? 'scale(1.1)' : 'scale(1)',
3006
+ transformOrigin: 'center',
3007
+ transition: 'transform 200ms ease-out',
3008
+ }}
3009
+ >{v.initial}</span>
2754
3010
  {/* Round 333 / Loop: vendor count suffix `:{N}` joins
2755
3011
  the R317 subordinate-text-lift family (gray-500 →
2756
3012
  gray-400) plus picks up tabular-nums for digit
@@ -2884,7 +3140,8 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2884
3140
  the 5th chip surface in the R333/R335/R336/R337
2885
3141
  chip-internal-hierarchy arc. data-active-links-
2886
3142
  chip-unit exposes the unit span for tests. */}
2887
- {flowLinks.length}<span className="opacity-70" data-active-links-chip-unit> active link{flowLinks.length === 1 ? '' : 's'}</span>
3143
+ {/* R362 sibling — active-links chip digit gains font-semibold. */}
3144
+ <span className="font-semibold" data-active-links-chip-digit>{flowLinks.length}</span><span className="opacity-70" data-active-links-chip-unit> active link{flowLinks.length === 1 ? '' : 's'}</span>
2888
3145
  {rel ? (() => {
2889
3146
  // Round 161 / Loop: extend R160's recency-pip
2890
3147
  // vocabulary up one scope — from per-flow row to
@@ -2909,8 +3166,8 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2909
3166
  const alpha = ageSec <= 30
2910
3167
  ? 1
2911
3168
  : ageSec <= 300
2912
- ? 1 - ((ageSec - 30) / 270) * 0.75
2913
- : 0.25;
3169
+ ? 1 - ((ageSec - 30) / 270) * 0.70 /* R358: floor 0.25 → 0.30 lift across 3 freshness scopes */
3170
+ : 0.30; /* R358: stale floor lifted 0.25 → 0.30 — 20% legibility bump while preserving fresh/stale ratio */
2914
3171
  // Cyan dark / teal light to match palette legendAccent.
2915
3172
  const dotColor = isLight
2916
3173
  ? `rgba(13, 148, 136, ${alpha.toFixed(2)})`
@@ -2927,7 +3184,28 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2927
3184
  // color: dotColor — the lift only affects the trailing
2928
3185
  // literal "last {rel}" text.
2929
3186
  return (
2930
- <span className="text-gray-400">
3187
+ // Round 357 / Loop: active-links chip freshness
3188
+ // suffix wrapper picks up `tabular-nums` for digit
3189
+ // width-lock. Pre-R357 the literal "last {rel}"
3190
+ // text (e.g. "last 5s ago", "last 10s ago",
3191
+ // "last 1m ago") had natural-figure digits — the
3192
+ // freshness ticker updates every second, so the
3193
+ // 9→10 boundary on the seconds counter and the
3194
+ // 59→60s → 1m flip both jittered ~1-2 px of glyph
3195
+ // width which propagated through the chip-row's
3196
+ // inline-flex layout, nudging the freshness DOT
3197
+ // and the chip's left edge. Tabular-nums on the
3198
+ // wrapper applies to all descendant digits only
3199
+ // (letters render at natural widths) so the
3200
+ // ticker stays planted across every count cross.
3201
+ // Joins the R224-R232 info-density tabular-nums
3202
+ // sweep at the chip-row freshness scope. Pure
3203
+ // paint-level change, no geometry shift on rest.
3204
+ // The R342 text-gray-400 lift + R161 dot freshness
3205
+ // alpha ramp + R317 subordinate-text-lift family
3206
+ // all preserved. data-active-links-freshness-
3207
+ // wrapper attr exposes the wrapper for tests.
3208
+ <span className="text-gray-400 tabular-nums" data-active-links-freshness-wrapper>
2931
3209
  <span
2932
3210
  data-active-links-freshness-dot
2933
3211
  data-active-links-freshness-alpha={alpha.toFixed(2)}
@@ -3591,6 +3869,20 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3591
3869
  active surfaces the activity state for test probes
3592
3870
  (active spokes don't carry the bucket/dur attrs so
3593
3871
  they need their own data anchor). */
3872
+ // Round 382 / Loop: hub-spoke path picks up
3873
+ // strokeLinecap='round'. Sibling polish to R378 flow-
3874
+ // rail dashes + R380 group box dashes — three dashed-
3875
+ // stroke surfaces now share 'round' linecap:
3876
+ // R378 flow-rail '2 12' -> soft 3-px pills
3877
+ // R380 group box '6 6' -> soft 7.5-px pills
3878
+ // R382 hub spoke '6 14' -> soft 7-px pills (this round)
3879
+ // For idle spokes (dashed at sw=1), each 6-px dash gains
3880
+ // 0.5-px round caps and reads as a soft pill instead of
3881
+ // a sharp 6 x 1 rectangle. Active spokes (solid, no
3882
+ // dasharray) have caps mostly hidden by the hub center +
3883
+ // node radius. Geometry-safe; paint-only. R51 sentinel
3884
+ // strokeWidth 1.5/3 untouched (idle=1, active=2). data-
3885
+ // topo-hub-spoke-linecap attr exposes the value for tests.
3594
3886
  return (
3595
3887
  <path
3596
3888
  key={`hub-${session.alias}`}
@@ -3599,11 +3891,13 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3599
3891
  stroke={isActiveSpoke ? pal.spokeStroke.active : pal.spokeStroke.idle}
3600
3892
  strokeWidth={isActiveSpoke ? 2 : 1}
3601
3893
  strokeDasharray={isActiveSpoke ? 'none' : '6 14'}
3894
+ strokeLinecap="round"
3602
3895
  opacity={isActiveSpoke ? 0.7 : 0.45}
3603
3896
  className={isActiveSpoke ? undefined : 'anet-topo-spoke-flow'}
3604
3897
  data-topo-spoke-bucket={isActiveSpoke ? undefined : busy}
3605
3898
  data-topo-spoke-dur={isActiveSpoke ? undefined : spokeDur}
3606
3899
  data-topo-hub-spoke-active={isActiveSpoke ? 'true' : 'false'}
3900
+ data-topo-hub-spoke-linecap="round"
3607
3901
  style={{
3608
3902
  transition: 'stroke 250ms ease-out, stroke-width 250ms ease-out, opacity 250ms ease-out',
3609
3903
  ...(isActiveSpoke ? {} : {
@@ -3695,7 +3989,34 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3695
3989
  stroke={(isPinned || isHovered) ? pal.legendAccent : pal.ringStroke}
3696
3990
  strokeWidth={isPinned ? 3 : isHovered ? 2 : 1.5}
3697
3991
  strokeDasharray={(isPinned || isHovered) ? 'none' : '6 6'}
3992
+ /* Round 380 / Loop: cluster box stroke gets round
3993
+ linecap + round linejoin. Sibling SVG stroke-
3994
+ softening polish to R378 flow-rail linecap + R379
3995
+ minimap viewport linejoin — extends the family to
3996
+ the group cluster boundary box (grid layout only):
3997
+ R288 chrome icons strokeLinecap='round'
3998
+ R378 flow-rail dashes strokeLinecap='round'
3999
+ R380 group box dashes strokeLinecap='round' (this round)
4000
+ R379 viewport rect strokeLinejoin='round'
4001
+ R380 group box corners strokeLinejoin='round' (this round)
4002
+ Linecap rounds the R85 '6 6' marching-ants dash
4003
+ pills at rest — each 6 px dash gains a ~0.75 px
4004
+ round cap (sw=1.5 idle), reading as soft pills
4005
+ instead of sharp 6 × 1.5 px rectangles. Linejoin
4006
+ rounds the 4 sharp 90° corners (any state — solid
4007
+ or dashed); at sw=1.5 the join arc is ~0.75 px,
4008
+ matching R379 viewport vocabulary. Geometry-safe:
4009
+ stroke-* properties only affect paint, not bbox.
4010
+ The R51 sentinel 1.5/3 strokeWidth values stay
4011
+ intact (the overlap probe is gated to g[data-
4012
+ node], so this cluster-internal rect is invisible
4013
+ to it anyway). data-group-box-linecap + -linejoin
4014
+ attrs expose the values for tests. */
4015
+ strokeLinecap="round"
4016
+ strokeLinejoin="round"
3698
4017
  data-group-box-pinned={isPinned ? 'true' : 'false'}
4018
+ data-group-box-linecap="round"
4019
+ data-group-box-linejoin="round"
3699
4020
  // R85: ambient "marching ants" drift on the perimeter
3700
4021
  // when this group has at least one working member, and
3701
4022
  // neither pin nor hover is active (those treatments
@@ -3895,12 +4216,33 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3895
4216
  lives at a fixed dx=6 offset from the name, so a
3896
4217
  digit-width jitter at 9→10 used to shift the
3897
4218
  whole count visibly. Tabular locks it. */}
4219
+ {/* Round 366 / Loop: group label member-count tspan
4220
+ fontWeight 400 → 500. Sibling polish to R363
4221
+ recent-row alias text fw 400 → 500 + R364 legend-
4222
+ row label fw 400 → 500 — closes the per-row 'count
4223
+ is fw 500 against label-tier fw 700' pattern at
4224
+ the group-label scope (grid layout cluster mark).
4225
+ Hierarchy snapshot post-R366 across all 3 row
4226
+ surfaces:
4227
+ recent count(hot/cold) fw 700/600 (R320)
4228
+ recent alias fw 500 (R363)
4229
+ legend count fw 600 (R309)
4230
+ legend label fw 500 (R364)
4231
+ group name fw 700 (legacy)
4232
+ group count fw 500 (R366, this round)
4233
+ Monospace family + R225 tabular-nums lock digit
4234
+ width, so the fw bump is paint-only — bbox
4235
+ unchanged + overlap-test invariants hold. R229
4236
+ fill-inherit from parent label (hover-deepen-own-
4237
+ hue family) preserved. data-group-label-count-
4238
+ font-weight attr exposes the value for tests. */}
3898
4239
  <tspan
3899
4240
  dx="6"
3900
4241
  fontSize="11"
3901
- fontWeight="400"
4242
+ fontWeight="500"
3902
4243
  data-group-label-count={box.key}
3903
4244
  data-group-label-count-value={box.count}
4245
+ data-group-label-count-font-weight="500"
3904
4246
  style={{ fontVariantNumeric: 'tabular-nums' }}
3905
4247
  >· {box.count}</tspan>
3906
4248
  {/* Round 58 / Loop: status mix pip strip. Compact text-
@@ -4189,20 +4531,56 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4189
4531
  online chips to splice in additional properties
4190
4532
  beside Tailwind's). data-edge-flow-rail attr
4191
4533
  surfaces the path for test introspection. */}
4534
+ {/* Round 381 / Loop: edge visible flow path picks up
4535
+ strokeLinecap='round'. Sibling polish to R378
4536
+ flow-rail dashed linecap — both flow-element paths
4537
+ (visible primary + dashed secondary rail) now share
4538
+ 'round' linecap vocabulary. The visible path runs
4539
+ source-node → dest-node as one continuous line, so
4540
+ the dest-end is covered by the markerEnd arrow and
4541
+ the source-end usually sits inside the source-node
4542
+ circle. At certain alignments (post-zoom, post-
4543
+ layout-switch transitions), the source-end may peek
4544
+ out by a fraction of a px past the node edge —
4545
+ round caps render that overshoot as a smooth half-
4546
+ disc instead of a sharp rectangle. Pure paint
4547
+ refinement, geometry-safe (bbox of the stroke
4548
+ unchanged at the join with the arrow marker).
4549
+ data-edge-visible-linecap attr exposes the value
4550
+ for tests. */}
4192
4551
  <path
4193
4552
  d={path}
4194
4553
  fill="none"
4195
4554
  stroke={pal.flowEdge}
4196
4555
  strokeWidth={renderWidth}
4556
+ strokeLinecap="round"
4197
4557
  opacity={Math.min(1, (isLight ? 0.22 : 0.28) * fresh * edgeOpacityMul)}
4198
4558
  filter={isLight ? undefined : 'url(#topo-glow)'}
4199
4559
  markerEnd={`url(#${arrowId})`}
4200
4560
  data-edge-visible={link.key}
4561
+ data-edge-visible-linecap="round"
4201
4562
  style={{
4202
4563
  pointerEvents: 'none',
4203
4564
  transition: 'opacity 300ms ease-out, stroke-width 300ms ease-out, stroke 300ms ease-out',
4204
4565
  }}
4205
4566
  />
4567
+ {/* Round 378 / Loop: edge flow-path dashed-rail picks
4568
+ up strokeLinecap='round'. Pre-R378 the rail
4569
+ rendered '2 12' dashes as sharp 1×2 rectangles
4570
+ against the canvas backdrop; default 'butt' caps
4571
+ leave dash ends square. R378 rounds each cap so
4572
+ the dashes read as soft 3-px pills (1 px stroke +
4573
+ 0.5 px round cap each end). The flow-rail is the
4574
+ secondary 'invisible-spine' line that gives the
4575
+ R57 spoke flow a directional rail to slide along
4576
+ — rounding the dashes softens its presence
4577
+ against the primary visible flow path (R245 has
4578
+ no strokeLinecap so it inherits 'butt' on a
4579
+ continuous line, irrelevant). Geometry-safe:
4580
+ round caps only widen the visible dash; the
4581
+ bbox of the path is unchanged so overlap-test
4582
+ invariants hold. data-edge-flow-rail-linecap
4583
+ attr exposes the value for tests. */}
4206
4584
  <path
4207
4585
  id={`flow-path-${index}`}
4208
4586
  d={path}
@@ -4210,8 +4588,10 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4210
4588
  stroke={pal.flowPath}
4211
4589
  strokeWidth="1"
4212
4590
  strokeDasharray="2 12"
4591
+ strokeLinecap="round"
4213
4592
  opacity={Math.min(1, (isLight ? 0.4 : 0.75) * fresh * edgeOpacityMul)}
4214
4593
  data-edge-flow-rail={link.key}
4594
+ data-edge-flow-rail-linecap="round"
4215
4595
  style={{ transition: 'opacity 300ms ease-out, stroke 300ms ease-out' }}
4216
4596
  />
4217
4597
  {!reducedMotion && (
@@ -4576,14 +4956,55 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4576
4956
  R251 closes the per-edge surface theme-toggle
4577
4957
  smoothness — every theme-driven property on
4578
4958
  every edge element now eases under cyber↔light. */}
4959
+ {/* Round 367 / Loop: edge midpoint badge rest
4960
+ stroke-width 1 → 1.25. Sibling visual-weight
4961
+ bump family (7th canvas anchor now):
4962
+ R287 minimap viewport stroke 1 → 1.5
4963
+ R295 legend swatch base radius 5.5 → 6
4964
+ R359 recent-row pip base radius 1.6 → 1.8
4965
+ R360 hub digit fontSize 11 → 12
4966
+ R361 edge-badge digit fontSize 10 → 11
4967
+ R365 hub-highlight base radius 5 → 5.5
4968
+ R367 edge-badge rest stroke 1 → 1.25 (this round)
4969
+ Cold edge badges gain ~25 % stroke presence
4970
+ (1.25/1.0 = 1.25). Stays clear of the R51
4971
+ overlap-test sentinel values (1.5 / 3 reserved
4972
+ for node strokes — the test selector is gated
4973
+ to g[data-node] ancestors so this edge-internal
4974
+ circle is invisible to that probe anyway, but
4975
+ 1.25 is a safe non-sentinel value regardless).
4976
+ R188 transition list 'stroke-width 300ms ease-
4977
+ out' still smoothes the hot/pin flip — now
4978
+ 1.25 → 2 instead of 1 → 2, same ease pace.
4979
+ data-edge-badge-stroke-width-rest exposes the
4980
+ new baseline for tests. */}
4981
+ {/* Round 371 / Loop: edge-badge cyber opacity 0.82
4982
+ → 0.85. Sibling theme-consistency polish to R370
4983
+ hub hover-ring 0.7 → 0.8. R251 designed this
4984
+ badge with opacity 0.82 (cyber) / 0.95 (light)
4985
+ — 13 % delta. Cyber-theme dark bg needs more
4986
+ alpha to read as 'present'; R371 narrows the
4987
+ gap to 10 %, bringing the badge closer to light
4988
+ theme's 0.95 floor. Light stays at 0.95
4989
+ (already in the legibility band). data-edge-
4990
+ badge-opacity attr exposes the resolved value.
4991
+ Theme-consistency polish family:
4992
+ R246/R247 panel transition family
4993
+ R251 edge badge fill/opacity baseline
4994
+ R370 hub hover-ring cyber 0.7 → 0.8
4995
+ R371 edge badge cyber 0.82 → 0.85 (this round)
4996
+ R164 r=9/10.5 hover-lift + R188/R251 transition
4997
+ list + R367 strokeWidth=1.25 cold rest preserved. */}
4579
4998
  <circle
4580
4999
  cx={badgeX} cy={badgeY}
4581
5000
  r={isHoveredEdge || isPinned ? 10.5 : 9}
4582
5001
  fill={pal.legendBox.fill}
4583
5002
  stroke={isPinned ? pal.legendHeadline : isHot ? hotStroke : pal.flowEdge}
4584
- strokeWidth={isPinned ? 2 : isHot ? 2 : 1}
4585
- opacity={isLight ? 0.95 : 0.82}
5003
+ strokeWidth={isPinned ? 2 : isHot ? 2 : 1.25}
5004
+ opacity={isLight ? 0.95 : 0.85}
4586
5005
  data-edge-badge-lifted={(isHoveredEdge || isPinned) ? 'true' : 'false'}
5006
+ data-edge-badge-stroke-width-rest="1.25"
5007
+ data-edge-badge-opacity={isLight ? 0.95 : 0.85}
4587
5008
  style={{ transition: 'r 180ms ease-out, stroke 300ms ease-out, stroke-width 300ms ease-out, fill 200ms ease-out, opacity 200ms ease-out' }}
4588
5009
  />
4589
5010
  {/* Round 224 / Loop: edge badge text gains the 4th
@@ -4622,11 +5043,39 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4622
5043
  x={badgeX} y={badgeY + 3}
4623
5044
  textAnchor="middle"
4624
5045
  fill={pal.legendHeadline}
4625
- fontSize="10"
5046
+ /* Round 361 / Loop: edge midpoint badge text
5047
+ fontSize 10 → 11. Sibling visual-weight bump
5048
+ to R360 hub digit 11 → 12. The badge digit
5049
+ is the per-edge equivalent of the hub digit
5050
+ — a high-information scalar (link.count) at
5051
+ a stable canvas position. Pre-R361 fontSize=
5052
+ 10 + R220 letter-spacing 0.4 + R224 tabular-
5053
+ nums made the digit READABLE but small
5054
+ against the r=9 / 18-px badge envelope;
5055
+ fontSize=11 nudges the glyph ~10 % bigger
5056
+ (bbox ~7×10 px from ~6×9 px) so the count
5057
+ reads more cleanly at glance — still well
5058
+ inside the r=9 idle circle and the r=10.5
5059
+ hover/pin lift (R164). y=badgeY+3 empirical
5060
+ vertical centring kept (1px drift at the
5061
+ bumped size is below the noise floor in
5062
+ the on-curve flow path).
5063
+ Visual-weight bump family:
5064
+ R287 minimap viewport stroke 1 → 1.5
5065
+ R295 legend swatch base radius 5.5 → 6
5066
+ R359 recent-row pip base radius 1.6 → 1.8
5067
+ R360 hub digit fontSize 11 → 12
5068
+ R361 edge-badge digit fontSize 10 → 11 (this round)
5069
+ data-edge-badge-text-font-size attr exposes
5070
+ the value for tests. R220 pin/hot letter-
5071
+ spacing tween + R224 tabular-nums + R188
5072
+ stroke-width pin/hot transitions all preserved. */
5073
+ fontSize="11"
4626
5074
  fontFamily="monospace"
4627
5075
  fontWeight="700"
4628
5076
  data-edge-badge-text={link.key}
4629
5077
  data-edge-badge-text-pin={(isPinned || isHot) ? 'true' : 'false'}
5078
+ data-edge-badge-text-font-size="11"
4630
5079
  style={{
4631
5080
  pointerEvents: 'none',
4632
5081
  fontVariantNumeric: 'tabular-nums',
@@ -4853,11 +5302,27 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4853
5302
  textAnchor="middle"
4854
5303
  dy="0.36em"
4855
5304
  fill={isLight ? '#d1fae5' : '#ecfdf5'}
4856
- fontSize="11"
5305
+ /* Round 360 / Loop: hub working-count digit fontSize 11
5306
+ → 12. The hub is the canvas's focal point — its digit
5307
+ is the most-read scalar on the whole topology. R130
5308
+ sized it at 11 (well inside the r=10 / 20-px core);
5309
+ R360 nudges it to 12 (~13 px wide × 12 px tall, still
5310
+ well inside the 20-px diameter) for ~9 % more presence.
5311
+ Sibling visual-weight bump family:
5312
+ R287 minimap viewport stroke 1 → 1.5
5313
+ R295 legend swatch base radius 5.5 → 6
5314
+ R359 recent-row pip radius 1.6 → 1.8
5315
+ R360 hub digit fontSize 11 → 12 (this round)
5316
+ The R209 scale-1.08-on-hub-hover, R225 tabular-nums,
5317
+ R253 fill transition, R213 always-mount opacity gate
5318
+ all preserved. data-topo-hub-working-count-font-size
5319
+ attr exposes the value for tests. */
5320
+ fontSize="12"
4857
5321
  fontFamily="monospace"
4858
5322
  fontWeight="700"
4859
5323
  opacity={workingCount > 0 ? 1 : 0}
4860
5324
  data-topo-hub-working-count={workingCount}
5325
+ data-topo-hub-working-count-font-size="12"
4861
5326
  data-topo-hub-working-count-hovered={hoveredHub ? 'true' : 'false'}
4862
5327
  data-topo-hub-working-count-visible={workingCount > 0 ? 'true' : 'false'}
4863
5328
  // Round 209 / Loop: hub workingCount digit scales 1.0 →
@@ -4898,12 +5363,53 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4898
5363
  {workingCount}
4899
5364
  </text>
4900
5365
  {/* decorative highlight (visible when workingCount === 0) */}
5366
+ {/* Round 365 / Loop: hub-center 'lit-lamp' decorative highlight
5367
+ circle r 5 → 5.5. Sibling visual-weight bump family —
5368
+ each round lifts one canvas anchor's geometric presence
5369
+ without disturbing its bbox envelope:
5370
+ R287 minimap viewport stroke 1 → 1.5
5371
+ R295 legend swatch base radius 5.5 → 6
5372
+ R359 recent-row pip base radius 1.6 → 1.8
5373
+ R360 hub digit fontSize 11 → 12
5374
+ R361 edge-badge digit fontSize 10 → 11
5375
+ R365 hub-highlight base radius 5 → 5.5 (this round)
5376
+ The highlight only renders when workingCount === 0
5377
+ (decorative 'lamp lit but idle' state per R130 + R213
5378
+ always-mount opacity-gate). At idle, the 0.5-px radius
5379
+ bump (21 % area, π*5.5² / π*5² = 1.21) lifts the lamp's
5380
+ presence — still well inside the r=10 hub-core (R130).
5381
+ opacity=0 when working preserved so the hub-digit's R130
5382
+ takeover stays seamless. R213 always-mount opacity-gate
5383
+ + 300ms opacity transition + pointerEvents:none all
5384
+ preserved. data-topo-hub-highlight-radius attr exposes
5385
+ the value for tests. */}
5386
+ {/* Round 386 / Loop: hub-highlight idle opacity 0.9 → 0.95.
5387
+ When workingCount===0 the highlight paints as the visible
5388
+ idle "lamp lit but no work" core (R130 takeover gate).
5389
+ Pre-R386 idle opacity was 0.9 — a ~6 % fade against full
5390
+ paint that read as slightly-dimmed-ghost on the focal
5391
+ point. R386 lifts to 0.95 (idle alpha gap halved 0.10
5392
+ → 0.05) so the canvas anchor reads more confidently
5393
+ as a present-but-idle state rather than a faded ghost.
5394
+ Theme-consistency / canvas-presence polish family (4th
5395
+ anchor):
5396
+ R370 hub hover-ring opacity 0.7 → 0.8 cyber
5397
+ R371 edge-badge rest opacity 0.82 → 0.85 cyber
5398
+ R372 minimap offline-dot opacity 0.5 → 0.6
5399
+ R386 hub-highlight idle opacity 0.9 → 0.95 (this round)
5400
+ opacity=0 when working preserved so the hub-digit's
5401
+ R130 takeover stays seamless. 300ms opacity transition
5402
+ + R213 always-mount opacity-gate + pointerEvents:none
5403
+ + R365 r=5.5 all preserved. data-topo-hub-highlight-
5404
+ opacity attr exposes the resolved value for tests. */}
4901
5405
  <circle
4902
- cx={cx} cy={cy} r="5"
5406
+ cx={cx} cy={cy} r="5.5"
4903
5407
  fill="#d1fae5"
4904
- opacity={workingCount > 0 ? 0 : 0.9}
5408
+ opacity={workingCount > 0 ? 0 : 0.95}
4905
5409
  data-topo-hub-highlight
4906
5410
  data-topo-hub-highlight-visible={workingCount > 0 ? 'false' : 'true'}
5411
+ data-topo-hub-highlight-radius="5.5"
5412
+ data-topo-hub-highlight-opacity={workingCount > 0 ? 0 : 0.95}
4907
5413
  style={{
4908
5414
  pointerEvents: 'none',
4909
5415
  transition: 'opacity 300ms ease-out',
@@ -4934,15 +5440,46 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4934
5440
  R142 group boxes, R143/R144 rows, R164 edge badges,
4935
5441
  R177 hub ring). prefers-reduced-motion respected via
4936
5442
  R29 globals.css blanket. */}
5443
+ {/* Round 385 / Loop: hub hover-ring strokeWidth 1.5 → 1.75.
5444
+ Sibling visual-weight bump (11th anchor) to R367 edge-
5445
+ badge rest stroke 1 → 1.25. The ring is only visible
5446
+ during hub hover (opacity=0 rest, R177 + R370 control
5447
+ the hover-state alpha) so the change manifests purely
5448
+ as a thicker hover-state ring on the canvas focal
5449
+ point. R177 r 14 → 17 grow + R370 opacity 0 → 0.8
5450
+ already lift the hover cue; R385 adds stroke weight
5451
+ as the third lift axis. Stays clear of R51 overlap-
5452
+ test sentinel value 3 (1.75 is non-sentinel); the
5453
+ R51 selector is gated to g[data-node] ancestors so
5454
+ this hub-internal circle is invisible to the probe
5455
+ regardless. R253 stroke transition + pointerEvents:
5456
+ none preserved. data-topo-hub-hover-ring-stroke-width
5457
+ attr exposes the value for tests. */}
4937
5458
  <circle
4938
5459
  cx={cx} cy={cy}
4939
5460
  r={hoveredHub ? 17 : 14}
4940
5461
  fill="none"
4941
5462
  stroke={isLight ? '#059669' : '#10b981'}
4942
- strokeWidth="1.5"
4943
- opacity={hoveredHub ? (isLight ? 0.85 : 0.7) : 0}
5463
+ strokeWidth="1.75"
5464
+ /* Round 370 / Loop: hub hover-ring cyber opacity 0.7
5465
+ 0.8. R177 designed the hub hover-ring at opacity-0 →
5466
+ 0.85 (light) / 0 → 0.7 (cyber). The 15 % gap between
5467
+ the two themes meant cyber-theme operators got a
5468
+ noticeably softer hover cue than light-theme users
5469
+ against backgrounds that should equalise (dark bg
5470
+ needs more luminance to read as 'on'). R370 bumps
5471
+ cyber 0.7 → 0.8, narrowing the theme gap to 5 % —
5472
+ sibling theme-consistency polish to R251 edge badge
5473
+ fill/opacity (cyber 0.82 / light 0.95) and R246/R247
5474
+ panel transition families. Light theme 0.85 stays
5475
+ as is (already in the legibility band). data-topo-
5476
+ hub-hover-ring-opacity attr exposes the value for
5477
+ tests. */
5478
+ opacity={hoveredHub ? (isLight ? 0.85 : 0.8) : 0}
4944
5479
  data-topo-hub-hover-ring
4945
5480
  data-topo-hub-hover-ring-radius={hoveredHub ? 17 : 14}
5481
+ data-topo-hub-hover-ring-stroke-width="1.75"
5482
+ data-topo-hub-hover-ring-opacity={hoveredHub ? (isLight ? 0.85 : 0.8) : 0}
4946
5483
  /* Round 253 / Loop: hub hover ring also gets stroke
4947
5484
  transition for theme toggle (cyber #10b981 ↔ light
4948
5485
  #059669). The opacity + r transitions stay for hover
@@ -6059,26 +6596,122 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6059
6596
  const detailY = pos.y - detailH / 2;
6060
6597
  return (
6061
6598
  <g transform={`translate(${detailX}, ${detailY})`} data-topo-hover-detail={session.alias} style={{ pointerEvents: 'none' }}>
6599
+ {/* Round 387 / Loop: hover-detail panel cyber backdrop
6600
+ opacity 0.94 → 0.97. The hover-detail card is
6601
+ ALWAYS rendered in active-hover context (it IS
6602
+ the hover product), so it should carry the
6603
+ same backdrop weight as the R348 recent-signal /
6604
+ legend panel HOVER state (which lifts 0.92 →
6605
+ 0.97 cyber). Pre-R387 the card sat at 0.94
6606
+ cyber, leaving a 0.03 alpha gap against the
6607
+ R348 panel-hover state — small but visible
6608
+ when the hover-detail floats next to a hovered
6609
+ recent-signal panel. R387 unifies them at 0.97
6610
+ so all active-hover panels paint with the same
6611
+ confident backdrop opacity in cyber. Light
6612
+ stays at 0.98 (already at the strong end —
6613
+ R348 light also stays at 0.97/0.98 max).
6614
+ Theme-consistency / canvas-presence polish
6615
+ family (5th anchor):
6616
+ R370 hub hover-ring opacity 0.7 → 0.8 cyber
6617
+ R371 edge-badge rest opacity 0.82 → 0.85 cyber
6618
+ R372 minimap offline-dot opacity 0.5 → 0.6
6619
+ R386 hub-highlight idle opacity 0.9 → 0.95
6620
+ R387 hover-detail panel opacity 0.94 → 0.97 cyber (this round)
6621
+ data-topo-hover-detail-opacity attr exposes
6622
+ the resolved value for tests. R348 drop-shadow
6623
+ + rx=8 + stroke=pal.legendAccent + fill=pal.
6624
+ labelBox.fill all preserved. */}
6625
+ {/* Round 390 / Loop: hover-detail card rx 8 → 10.
6626
+ Corner-radius cascade family — the hover-detail
6627
+ card is a panel-tier surface (192×88 floating
6628
+ info card with drop-shadow + stroke), so its
6629
+ corner radius should match the R331 panel tier
6630
+ (rx=10) used by the recent-signal and legend
6631
+ panels. Pre-R390 it shared rx=8 with the R332
6632
+ minimap and R375/R376 segmented-control tier
6633
+ (Layout-toggle, nodeSize, zoom wrappers),
6634
+ which is the "compact chrome control" tier —
6635
+ a tier mismatch for a content-bearing panel.
6636
+ Corner-radius cascade (6 anchors now):
6637
+ R330 canvas rx 12 (root)
6638
+ R331 panels rx 10 (recent-signal, legend)
6639
+ R332 minimap rx 8 (compact chrome)
6640
+ R375 Layout-toggle rx 8 (segmented control)
6641
+ R376 nodeSize/zoom rx 8 (segmented control)
6642
+ R390 hover-detail rx 10 (panel — this round)
6643
+ Pure paint change; no layout shift (rx grows
6644
+ the corner curve INWARD without changing the
6645
+ card's outer bbox). data-topo-hover-detail-
6646
+ rx attr exposes the resolved value for tests.
6647
+ R348 drop-shadow + stroke + R387 opacity all
6648
+ preserved. */}
6062
6649
  <rect
6063
- x="0" y="0" width={detailW} height={detailH} rx="8"
6650
+ x="0" y="0" width={detailW} height={detailH} rx="10"
6064
6651
  fill={pal.labelBox.fill}
6065
6652
  stroke={pal.legendAccent}
6066
- opacity={isLight ? 0.98 : 0.94}
6653
+ opacity={isLight ? 0.98 : 0.97}
6654
+ data-topo-hover-detail-opacity={isLight ? 0.98 : 0.97}
6655
+ data-topo-hover-detail-rx="10"
6067
6656
  style={{ filter: isLight ? 'drop-shadow(0 4px 12px rgba(15,23,42,0.16))' : 'drop-shadow(0 4px 12px rgba(0,0,0,0.6))' }}
6068
6657
  />
6069
6658
  <text x="10" y="16" fontSize="9" fontFamily="monospace" fill={pal.legendAccent} fontWeight="700">
6070
6659
  {v.id !== 'unknown' ? v.label : '—'}
6071
6660
  </text>
6072
- <text x="10" y="32" fontSize="10" fontFamily="monospace" fill={pal.legendHeadline}>
6661
+ {/* Round 389 / Loop: hover-detail model line (y=32)
6662
+ fontWeight 400 → 600. R388 lifted body lines
6663
+ (runtime/host/task at fontSize=9) to fw=500;
6664
+ R389 closes the typography hierarchy by giving
6665
+ the model name (the dominant subhead text in
6666
+ the card) its own weight tier. Three-tier
6667
+ ladder now reads cleanly:
6668
+ vendor fontSize=9 fw=700 (label badge)
6669
+ model fontSize=10 fw=600 (subhead — this round)
6670
+ body 3× fontSize=9 fw=500 (R388)
6671
+ One tier step per dimension (size + weight)
6672
+ between adjacent levels — classic editorial
6673
+ hierarchy idiom adapted to a 192×88 SVG card.
6674
+ Sibling to the chip-internal-hierarchy arc
6675
+ (R333-R341/R362/R369) which uses fw=600/500
6676
+ for digit/unit pairs; R389 applies the same
6677
+ fw=600 to a content-bearing identity line.
6678
+ data-topo-hover-detail-model-fw attr exposes
6679
+ the resolved value for tests. pal.legendHeadline
6680
+ fill preserved (R389 doesn't touch color). */}
6681
+ <text x="10" y="32" fontSize="10" fontFamily="monospace" fontWeight="600" fill={pal.legendHeadline} data-topo-hover-detail-model-fw="600">
6073
6682
  {session.model || 'model · pending'}
6074
6683
  </text>
6075
- <text x="10" y="48" fontSize="9" fontFamily="monospace" fill={pal.legendText}>
6684
+ {/* Round 388 / Loop: hover-detail body lines (the
6685
+ three fontSize=9 lines: runtime, host, task)
6686
+ gain fontWeight=500. Small-text fw lift family
6687
+ (6th anchor) — fontSize 9-10 px text reads
6688
+ consistently bolder at fw=500 than at the
6689
+ default 400 weight at small sizes, especially
6690
+ on the cyber-theme backdrop where stroke-
6691
+ rendering is the limiting factor.
6692
+ Sibling lifts in this family:
6693
+ R363 recent-row alias text 400 → 500
6694
+ R364 legend-row label 400 → 500
6695
+ R366 group-label count tspan 400 → 500
6696
+ R368 +N more flows footer 400 → 500
6697
+ R373 pressure-bar kicker (font-medium)
6698
+ R388 hover-detail body lines 400 → 500 (this round)
6699
+ Tier structure preserved:
6700
+ y=16 vendor (fw=700, headline)
6701
+ y=32 model (fontSize=10, subhead by size)
6702
+ y=48 runtime / y=64 host / y=80 task (body, now fw=500)
6703
+ The y=80 task line keeps opacity=0.7 so its
6704
+ caption-tier identity stays distinct from the
6705
+ y=48 / y=64 body lines despite shared fw.
6706
+ data-topo-hover-detail-body-fw attr exposes
6707
+ the resolved value for tests. */}
6708
+ <text x="10" y="48" fontSize="9" fontFamily="monospace" fontWeight="500" fill={pal.legendText} data-topo-hover-detail-body-fw="500">
6076
6709
  {rt ? rt.label : 'runtime · pending'}
6077
6710
  </text>
6078
- <text x="10" y="64" fontSize="9" fontFamily="monospace" fill={pal.legendText}>
6711
+ <text x="10" y="64" fontSize="9" fontFamily="monospace" fontWeight="500" fill={pal.legendText} data-topo-hover-detail-body-fw="500">
6079
6712
  host · {session.server || 'unknown'}
6080
6713
  </text>
6081
- <text x="10" y="80" fontSize="9" fontFamily="monospace" fill={pal.legendText} opacity="0.7">
6714
+ <text x="10" y="80" fontSize="9" fontFamily="monospace" fontWeight="500" fill={pal.legendText} opacity="0.7" data-topo-hover-detail-body-fw="500">
6082
6715
  {session.task ? truncate(session.task, 28) : 'no recent task'}
6083
6716
  </text>
6084
6717
  </g>
@@ -6224,7 +6857,17 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6224
6857
  x="0" y="0" width="230" height="88" rx="10"
6225
6858
  fill={pal.legendBox.fill}
6226
6859
  stroke={pal.legendBox.stroke}
6227
- opacity={isLight ? 0.97 : 0.92}
6860
+ // Round 348 / Loop: recent-signal panel rect opacity hover-
6861
+ // state bump — joins the panel-hover cue stack (R135 drop-
6862
+ // shadow boost + R345 title letter-spacing tween 0.3 → 0.4
6863
+ // + R266 fill theme-flip). Cyber 0.92 → 0.97, light 0.97 →
6864
+ // 1.0 on hoveredPanel === 'recent'. The panel "solidifies"
6865
+ // on hover — pure paint-level change, geometry-safe (bbox
6866
+ // unchanged so topo-overlap-test invariants hold). The
6867
+ // R247 transition list already includes `opacity 200ms
6868
+ // ease-out` so the value tween is automatic. Sibling
6869
+ // change at legend panel rect below (~line 7222).
6870
+ opacity={hoveredPanel === 'recent' ? (isLight ? 1 : 0.97) : (isLight ? 0.97 : 0.92)}
6228
6871
  style={{
6229
6872
  /* R135: drop-shadow intensifies on panel hover. Base
6230
6873
  shadow (2px / 6px blur) signals card elevation
@@ -6276,7 +6919,17 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6276
6919
  title (line ~6556) — both panels share the same
6277
6920
  editorial-text-spacing convention. data-recent-panel-
6278
6921
  title handle unchanged so R266 test still resolves. */}
6279
- <text x="13" y="21" fill={pal.legendHeadline} fontSize="12" fontFamily="monospace" fontWeight="700" letterSpacing="0.3" style={{ transition: 'fill 200ms ease-out' }} data-recent-panel-title>recent signal</text>
6922
+ {/* Round 345 / Loop: recent-signal panel title gains
6923
+ letter-spacing tween 0.3 → 0.4 on panel hover.
6924
+ hoveredPanel === 'recent' is set by the panel <g>
6925
+ wrapper's onMouseEnter (line ~6263 area). Sibling to
6926
+ R344 hover-letter-spacing applied to the +N more
6927
+ flows footer — same gesture vocabulary at a panel-
6928
+ title scope: hovering the panel chrome spreads the
6929
+ title 0.1 px, signalling "this is a coherent unit
6930
+ you're entering". transition list extends letter-
6931
+ spacing 200ms ease-out alongside existing fill 200ms. */}
6932
+ <text x="13" y="21" fill={pal.legendHeadline} fontSize="12" fontFamily="monospace" fontWeight="700" letterSpacing={hoveredPanel === 'recent' ? '0.4' : '0.3'} style={{ transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out' }} data-recent-panel-title>recent signal</text>
6280
6933
  {/* R96: header count now matches what the rows show. Pre-R96
6281
6934
  this read "X msgs" off the raw messages array, but the
6282
6935
  rows below render DEDUPED flowLinks — so a fleet with 10
@@ -6337,8 +6990,8 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6337
6990
  const alpha = ageSec <= 30
6338
6991
  ? 1
6339
6992
  : ageSec <= 300
6340
- ? 1 - ((ageSec - 30) / 270) * 0.75
6341
- : 0.25;
6993
+ ? 1 - ((ageSec - 30) / 270) * 0.70 /* R358: floor 0.25 → 0.30 lift across 3 freshness scopes */
6994
+ : 0.30; /* R358: stale floor lifted 0.25 → 0.30 — 20% legibility bump while preserving fresh/stale ratio */
6342
6995
  // Dark cyan-400 / light teal-600 with alpha — same
6343
6996
  // palette as R161's chip bullet so the two scopes
6344
6997
  // visually align even side-by-side.
@@ -6351,6 +7004,21 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6351
7004
  textAnchor="end"
6352
7005
  fontSize="10"
6353
7006
  fontFamily="monospace"
7007
+ // Round 349 / Loop: editorial letter-spacing 0.2 on the
7008
+ // recent-signal panel header count. Sits one tier below
7009
+ // the R301 panel title letterSpacing="0.3" so the panel
7010
+ // header reads as a 2-step hierarchy (title 0.3 / count
7011
+ // 0.2). Sibling change on the legend panel count below
7012
+ // closes the panel-pair editorial symmetry. Joins the
7013
+ // R285 / R289 / R301 / R302 / R304 / R325 editorial-
7014
+ // letterspacing tier at the panel-summary scope. The
7015
+ // R162 freshness fill, R225 tabular-nums, R311 fw=600,
7016
+ // R336 unit-tspan opacity-0.7 split all preserved —
7017
+ // the tier propagates to all descendant tspans via
7018
+ // SVG inheritance. data-recent-panel-count-letter-
7019
+ // spacing exposes the value for tests.
7020
+ letterSpacing="0.2"
7021
+ data-recent-panel-count-letter-spacing="0.2"
6354
7022
  >
6355
7023
  {/* Round 225 / Loop: tabular-nums on the panel-header
6356
7024
  flow-count tspan. The "{N} flows" string lives in
@@ -6819,17 +7487,58 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6819
7487
  const alpha = ageSec <= 30
6820
7488
  ? 1
6821
7489
  : ageSec <= 300
6822
- ? 1 - ((ageSec - 30) / 270) * 0.75
6823
- : 0.25;
7490
+ ? 1 - ((ageSec - 30) / 270) * 0.70 /* R358: floor 0.25 → 0.30 lift across 3 freshness scopes */
7491
+ : 0.30; /* R358: stale floor lifted 0.25 → 0.30 — 20% legibility bump while preserving fresh/stale ratio */
6824
7492
  return (
6825
7493
  <circle
6826
7494
  cx={10}
6827
7495
  cy={38 + index * 16 - 3}
6828
- r={1.6}
7496
+ /* Round 359 / Loop: recency pip base radius
7497
+ 1.6 → 1.8. Sibling lift to R358's freshness-
7498
+ floor bump (alpha 0.25 → 0.30) — pre-R358/
7499
+ R359 the stale pip painted at r=1.6 + α=0.25
7500
+ which read as near-invisible chrome. R358
7501
+ gave it more alpha; R359 gives it more area
7502
+ (1.8² / 1.6² ≈ 1.27, so ~27 % more glyph)
7503
+ so the pip stays distinguishable across the
7504
+ freshness ramp. Geometry: 1.8-radius dot
7505
+ centred at (10, row_y - 3) is bbox 3.6×3.6,
7506
+ still well inside the 7-px left margin
7507
+ (x=6 rect-start → x=13 text-start) the R160
7508
+ pip was placed in. Overlap-test reads the
7509
+ parent row rect's bbox, not this pip's, so
7510
+ grid+ring invariants hold. Matches the same
7511
+ 1.6 → 1.8 visual-weight bump R295 applied
7512
+ to the legend swatch (5.5 → 6 base radius)
7513
+ and R287 to the minimap viewport stroke
7514
+ (1 → 1.5). data-recent-row-freshness-radius
7515
+ attr exposes the value for tests. */
7516
+ /* Round 383 / Loop: recency pip base radius
7517
+ 1.8 → 2.0. Continues the R359 lift
7518
+ trajectory — pip area grows ~23 % (π·2²/
7519
+ π·1.8² ≈ 1.23) for a clearer at-a-glance
7520
+ freshness anchor in each row. Bbox 4.0×4.0
7521
+ still inside the 7-px R160 left margin
7522
+ (3-px remaining clearance vs 3.4 at r=1.8
7523
+ — geometry-safe margin holds). Sibling
7524
+ visual-weight bump family (9th anchor now):
7525
+ R287 minimap viewport stroke 1 → 1.5
7526
+ R295 legend swatch base radius 5.5 → 6
7527
+ R359 recent-row pip base radius 1.6 → 1.8
7528
+ R360 hub digit fontSize 11 → 12
7529
+ R361 edge-badge digit fontSize 10 → 11
7530
+ R365 hub-highlight base radius 5 → 5.5
7531
+ R367 edge-badge rest stroke 1 → 1.25
7532
+ R374 pressure-bar height 1.5 → 2
7533
+ R383 recent-row pip radius 1.8 → 2.0 (this round)
7534
+ data-recent-row-freshness-radius attr
7535
+ bumps to '2.0' for tests. */
7536
+ r={2.0}
6829
7537
  fill={pal.legendAccent}
6830
7538
  opacity={alpha}
6831
7539
  data-recent-row-freshness={link.key}
6832
7540
  data-recent-row-freshness-alpha={alpha.toFixed(2)}
7541
+ data-recent-row-freshness-radius="2.0"
6833
7542
  style={{ pointerEvents: 'none', transition: 'opacity 200ms ease-out' }}
6834
7543
  />
6835
7544
  );
@@ -6859,8 +7568,28 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6859
7568
  fill={isRowActive ? pal.legendHeadline : pal.legendText}
6860
7569
  fontSize="9"
6861
7570
  fontFamily="monospace"
7571
+ /* Round 363 / Loop: recent-row text fontWeight 400
7572
+ → 500 (font-medium tier). At fontSize=9 the
7573
+ default-weight 400 glyphs read thin against the
7574
+ panel chrome (pal.legendBox.fill with 0.92/0.97
7575
+ opacity); the 100-weight bump lifts the alias→
7576
+ alias text into the legibility band without
7577
+ changing geometry. The R320 count tspan fw=600
7578
+ (cold) / fw=700 (hot) override still wins
7579
+ locally via inline fontWeight on the inner
7580
+ tspan, so the count-vs-alias hierarchy stays
7581
+ intact:
7582
+ alias fw 500 (R363, this round)
7583
+ count fw 600/700 (R320)
7584
+ Sibling typography lift to R362 chip-row digit
7585
+ 500 → 600 — both nudge a within-element data
7586
+ tier without disturbing the surrounding family
7587
+ baseline. data-recent-row-text-font-weight attr
7588
+ exposes the value for tests. */
7589
+ fontWeight="500"
6862
7590
  data-recent-row-text={link.key}
6863
7591
  data-recent-row-text-pinned={isRowPinned ? 'true' : 'false'}
7592
+ data-recent-row-text-font-weight="500"
6864
7593
  style={{
6865
7594
  transition: 'fill 150ms ease-out, letter-spacing 150ms ease-out',
6866
7595
  letterSpacing: isRowPinned ? '0.5px' : '0px',
@@ -7122,6 +7851,23 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7122
7851
  surface. transition list extends letter-spacing
7123
7852
  200ms ease-out alongside the existing opacity/
7124
7853
  fill easings. */}
7854
+ {/* Round 368 / Loop: `+N more flows` footer text gains
7855
+ fontWeight=500 (font-medium tier). Sibling small-
7856
+ text fw lift family with R363 recent-row alias
7857
+ + R364 legend-row label + R366 group-label count
7858
+ — all four lifts share the same theory: at small
7859
+ fontSize (9-11 px) against panel chrome, SVG-
7860
+ default fw 400 sits at the legibility floor;
7861
+ fw 500 brings the glyph into the deliberate-data
7862
+ band. fontStyle=italic + opacity 0.55 rest + R325
7863
+ letterSpacing 0.2 baseline + R344 hover-spread
7864
+ 0.2 → 0.3 + R195 cyan fill on hover all preserved
7865
+ — the fw bump just thickens the italic stroke.
7866
+ Hover-state punch (R195 fill + R325 opacity 0.55
7867
+ → 0.85 + R344 letter-spacing + R133 underline)
7868
+ stays as is, so the rest-vs-hover delta still
7869
+ reads clearly. data-recent-panel-more-font-weight
7870
+ attr exposes the value for tests. */}
7125
7871
  <text
7126
7872
  x="115" y="82"
7127
7873
  textAnchor="middle"
@@ -7129,11 +7875,13 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7129
7875
  fontSize="9"
7130
7876
  fontFamily="monospace"
7131
7877
  fontStyle="italic"
7878
+ fontWeight="500"
7132
7879
  letterSpacing={hoveredRecentMore ? '0.3' : '0.2'}
7133
7880
  opacity={hoveredRecentMore ? 0.85 : 0.55}
7134
7881
  textDecoration={hoveredRecentMore ? 'underline' : 'none'}
7135
7882
  data-recent-panel-more={moreCount}
7136
7883
  data-recent-panel-more-hovered={hoveredRecentMore ? 'true' : 'false'}
7884
+ data-recent-panel-more-font-weight="500"
7137
7885
  style={{ transition: 'opacity 150ms ease-out, fill 200ms ease-out, letter-spacing 200ms ease-out' }}
7138
7886
  >
7139
7887
  {`+ ${moreCount}`}
@@ -7186,7 +7934,12 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7186
7934
  x="0" y="0" width="224" height="88" rx="10"
7187
7935
  fill={pal.legendBox.fill}
7188
7936
  stroke={pal.legendBox.stroke}
7189
- opacity={isLight ? 0.97 : 0.92}
7937
+ // R348 sibling legend panel rect opacity hover-state
7938
+ // bump 0.92 → 0.97 (cyber) / 0.97 → 1 (light) on
7939
+ // hoveredPanel === 'legend'. Pairs with the recent-signal
7940
+ // panel rect above so the two corner panels' hover cues
7941
+ // stay symmetric. Geometry-safe (paint-only).
7942
+ opacity={hoveredPanel === 'legend' ? (isLight ? 1 : 0.97) : (isLight ? 0.97 : 0.92)}
7190
7943
  style={{
7191
7944
  filter: hoveredPanel === 'legend'
7192
7945
  ? (isLight ? 'drop-shadow(0 4px 12px rgba(15,23,42,0.14))'
@@ -7209,7 +7962,9 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7209
7962
  surrounding chrome eased; R266 closes both at once. */}
7210
7963
  {/* R301: sibling to recent-signal panel title above —
7211
7964
  same letterSpacing 0.3 for editorial parity. */}
7212
- <text x="13" y="21" fill={pal.legendHeadline} fontSize="12" fontFamily="monospace" fontWeight="700" letterSpacing="0.3" style={{ transition: 'fill 200ms ease-out' }} data-legend-panel-title>legend</text>
7965
+ {/* R345 sibling legend panel title same hover letter-
7966
+ spacing tween 0.3 → 0.4 on panel hover. */}
7967
+ <text x="13" y="21" fill={pal.legendHeadline} fontSize="12" fontFamily="monospace" fontWeight="700" letterSpacing={hoveredPanel === 'legend' ? '0.4' : '0.3'} style={{ transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out' }} data-legend-panel-title>legend</text>
7213
7968
  {/* Round 257 / Loop: legend panel header count picks up the
7214
7969
  symmetric 13L/13R inner-padding pattern from the recent-
7215
7970
  signal panel. Pre-R257 the legend header was 13px from
@@ -7273,7 +8028,14 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7273
8028
  <text
7274
8029
  x="211" y="21" textAnchor="end"
7275
8030
  fill={pal.legendAccent} fontSize="10" fontFamily="monospace" fontWeight="600"
8031
+ // R349 sibling — legend panel header count picks up
8032
+ // letterSpacing="0.2", one tier below the R301 panel
8033
+ // title 0.3. Pairs with the recent-signal panel count
8034
+ // letter-spacing above so the two corner panels' header
8035
+ // typography stays editorially symmetric.
8036
+ letterSpacing="0.2"
7276
8037
  data-legend-panel-count
8038
+ data-legend-panel-count-letter-spacing="0.2"
7277
8039
  style={{
7278
8040
  transition: 'fill 200ms ease-out',
7279
8041
  fontVariantNumeric: 'tabular-nums',
@@ -7528,8 +8290,31 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7528
8290
  fill={hoveredStatus === row.key || isPinned ? pal.legendHeadline : pal.legendText}
7529
8291
  fontSize="11"
7530
8292
  fontFamily="monospace"
8293
+ /* Round 364 / Loop: legend-row label fontWeight 400
8294
+ → 500. Sibling typography lift to R363 recent-row
8295
+ text fw 400 → 500. Both surfaces render small
8296
+ monospace text against panel chrome at fontSize
8297
+ 9-11 where SVG-default fw 400 sits at the
8298
+ legibility floor. font-medium tier (500) gives
8299
+ the label a more deliberate-data register.
8300
+ The R309 per-row count text (separate element
8301
+ below at x=215 textAnchor=end) keeps its own
8302
+ fontWeight 600 inline override, so the count >
8303
+ label hierarchy stays intact at the legend
8304
+ scope same as R363 holds it at the recent-row
8305
+ scope:
8306
+ legend label fw 500 (R364, this round)
8307
+ legend count fw 600 (R309)
8308
+ recent alias fw 500 (R363)
8309
+ recent count fw 600/700 (R320)
8310
+ data-legend-row-label-font-weight attr exposes
8311
+ the value for tests. R219 letter-spacing pin
8312
+ tween + R55 fill transition + R181 always-mount
8313
+ pin ring all preserved. */
8314
+ fontWeight="500"
7531
8315
  data-legend-row-label={row.key}
7532
8316
  data-legend-row-label-pinned={isPinned ? 'true' : 'false'}
8317
+ data-legend-row-label-font-weight="500"
7533
8318
  style={{
7534
8319
  transition: 'fill 150ms ease-out, letter-spacing 150ms ease-out',
7535
8320
  letterSpacing: isPinned ? '0.5px' : '0px',
@@ -7815,7 +8600,13 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7815
8600
  resetView();
7816
8601
  }
7817
8602
  }}
8603
+ // R346: viewport rect hover affordance driven by parent.
8604
+ onMouseEnter={() => setHoveredMinimap(true)}
8605
+ onMouseLeave={() => setHoveredMinimap(false)}
8606
+ onFocus={() => setHoveredMinimap(true)}
8607
+ onBlur={() => setHoveredMinimap(false)}
7818
8608
  data-topo-minimap
8609
+ data-topo-minimap-hovered={hoveredMinimap ? 'true' : 'false'}
7819
8610
  >
7820
8611
  <svg width={MW} height={MH} viewBox={`0 0 ${MW} ${MH}`} style={{ display: 'block' }}>
7821
8612
  {/* Round 198 / Loop: minimap dots gain smooth status
@@ -7842,14 +8633,48 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7842
8633
  const isOn = s.status !== 'offline' || !!sseN;
7843
8634
  const st = nodeStatus(s, isOn, isLight);
7844
8635
  return (
8636
+ /* Round 372 / Loop: minimap offline-dot opacity
8637
+ 0.5 → 0.6. Sibling stale-state legibility lift
8638
+ to R358 freshness ramp floor 0.25 → 0.30 + R317
8639
+ subordinate-text-lift family. Pre-R372 R198
8640
+ drew offline dots at α=0.5 (44 % below online
8641
+ 0.9). The minimap is a small overlay against
8642
+ the canvas backdrop — at α=0.5 offline dots
8643
+ sat at the legibility floor when the minimap
8644
+ mounted (only on non-default view). R372 lifts
8645
+ offline 0.5 → 0.6 for +20 % relative presence;
8646
+ online stays at 0.9 so the offline/online
8647
+ contrast ratio is now 0.6/0.9 ≈ 0.67 (vs prior
8648
+ 0.5/0.9 ≈ 0.56) — still a clear two-tier
8649
+ distinction. R198 opacity + fill + r transition
8650
+ list preserved so status flips still ease
8651
+ smoothly. data-topo-minimap-dot-opacity attr
8652
+ exposes the resolved value for tests. */
7845
8653
  <circle
7846
8654
  key={s.alias}
7847
8655
  cx={p.x * sx} cy={p.y * sy}
7848
- r={isOn ? 1.7 : 1.2}
8656
+ /* Round 384 / Loop: minimap online dot radius 1.7
8657
+ → 1.9. Sibling visual-weight bump (10th anchor)
8658
+ to R383 recent-row pip 1.8 → 2.0. R198 designed
8659
+ the dots at 1.7 (online) / 1.2 (offline) — at
8660
+ the minimap's 120 × 82 scale these read clearly
8661
+ but the online ↔ offline contrast was modest
8662
+ (1.7/1.2 = 1.42×). R384 bumps online to 1.9 so
8663
+ the tier delta widens to 1.58× (1.9/1.2). Pair
8664
+ completes minimap-dot legibility polish:
8665
+ R358 (era R372) offline opacity 0.5 → 0.6
8666
+ R384 online radius 1.7 → 1.9 (this round)
8667
+ R198 transition list (opacity + fill + r 200ms)
8668
+ preserved so status flips still ease smoothly.
8669
+ data-topo-minimap-dot-radius attr exposes the
8670
+ resolved value for tests. */
8671
+ r={isOn ? 1.9 : 1.2}
7849
8672
  fill={st.primary}
7850
- opacity={isOn ? 0.9 : 0.5}
8673
+ opacity={isOn ? 0.9 : 0.6}
7851
8674
  data-topo-minimap-dot={s.alias}
7852
8675
  data-topo-minimap-dot-online={isOn ? 'true' : 'false'}
8676
+ data-topo-minimap-dot-opacity={isOn ? 0.9 : 0.6}
8677
+ data-topo-minimap-dot-radius={isOn ? 1.9 : 1.2}
7853
8678
  style={{
7854
8679
  transition: 'opacity 200ms ease-out, fill 200ms ease-out, r 200ms ease-out',
7855
8680
  } as React.CSSProperties}
@@ -7890,17 +8715,40 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7890
8715
  information element to lift it above ambient
7891
8716
  chrome. opacity 0.9 stays — strokeWidth alone
7892
8717
  does the lifting. */}
8718
+ {/* Round 379 / Loop: minimap viewport rect picks up
8719
+ strokeLinejoin='round'. Pre-R379 the rect's 4
8720
+ corners painted with default 'miter' joins —
8721
+ sharp 90° corners with a small miter overshoot
8722
+ (≈ strokeWidth × 1.4 = 2.1 px at sw=1.5). R379
8723
+ rounds the joins so corners arc smoothly through
8724
+ a quarter-circle of radius ≈ strokeWidth/2. At
8725
+ sw=1.5 that's a 0.75-px radius — subtle but
8726
+ matches the same stroke-softening vocabulary R288
8727
+ chrome icons (zoom/reset/fullscreen) and R378
8728
+ flow-rail already speak. Geometry-safe: stroke-
8729
+ linejoin only affects the corner overshoot, the
8730
+ rect's bbox is unchanged. R287 strokeWidth=1.5 +
8731
+ R346 hover-state strokeWidth/opacity bump + R199
8732
+ smoothView x/y/w/h transition all preserved.
8733
+ data-topo-minimap-viewport-linejoin attr exposes
8734
+ the value for tests. */}
7893
8735
  <rect
7894
8736
  x={Math.max(0, rectX)} y={Math.max(0, rectY)}
7895
8737
  width={Math.max(0, Math.min(MW - Math.max(0, rectX), rectW))}
7896
8738
  height={Math.max(0, Math.min(MH - Math.max(0, rectY), rectH))}
7897
- fill="none" stroke={pal.legendAccent} strokeWidth="1.5" opacity="0.9"
8739
+ fill="none" stroke={pal.legendAccent}
8740
+ // R346: strokeWidth + opacity tween on container hover.
8741
+ strokeWidth={hoveredMinimap ? '1.75' : '1.5'}
8742
+ strokeLinejoin="round"
8743
+ opacity={hoveredMinimap ? '1' : '0.9'}
7898
8744
  data-topo-minimap-viewport
7899
8745
  data-topo-minimap-viewport-smooth={smoothView ? 'true' : 'false'}
8746
+ data-topo-minimap-viewport-hover={hoveredMinimap ? 'true' : 'false'}
8747
+ data-topo-minimap-viewport-linejoin="round"
7900
8748
  style={{
7901
8749
  transition: smoothView
7902
- ? 'x 280ms ease-out, y 280ms ease-out, width 280ms ease-out, height 280ms ease-out'
7903
- : 'none',
8750
+ ? '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'
8751
+ : 'stroke-width 200ms ease-out, opacity 200ms ease-out',
7904
8752
  } as React.CSSProperties}
7905
8753
  />
7906
8754
  </svg>
@@ -7955,8 +8803,22 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7955
8803
  own transition-colors. Same R254 holdover pattern that
7956
8804
  R263 just closed at the canvas wrapper scope, now at the
7957
8805
  chrome strip's nodeSize sub-wrapper scope. */}
8806
+ {/* Round 376 / Loop: nodeSize wrapper rounded-md → rounded-lg.
8807
+ Sibling polish to R375 Layout-toggle wrapper. Three
8808
+ chrome-strip segmented controls now all share rounded-lg
8809
+ at the wrapper tier:
8810
+ R375 Layout-toggle wrapper rounded-lg 8 px
8811
+ R376 nodeSize wrapper rounded-lg 8 px (this round)
8812
+ R376 zoom wrapper rounded-lg 8 px (this round)
8813
+ Individual atomic chrome buttons (reset, fullscreen) keep
8814
+ rounded-md (6 px) as their own atomic-button tier — the
8815
+ chrome strip's typography now expresses a clear two-tier
8816
+ hierarchy: 'segmented control container' (rounded-lg)
8817
+ vs 'standalone button' (rounded-md). Pure paint change,
8818
+ no layout shift. */}
7958
8819
  <div
7959
- className="flex items-center rounded-md border overflow-hidden"
8820
+ className="flex items-center rounded-lg border overflow-hidden"
8821
+ data-topo-chrome-nodesize-radius="rounded-lg"
7960
8822
  style={{
7961
8823
  background: pal.legendBox.fill,
7962
8824
  borderColor: pal.containerBorder,
@@ -8036,8 +8898,12 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8036
8898
  data-topo-chrome-view-group-leader marks the boundary surface
8037
8899
  for the test probe; data-topo-chrome-fleet-group-trailer marks
8038
8900
  the nodeSize wrapper's right edge for the gap measurement. */}
8901
+ {/* R376 sibling — zoom wrapper rounded-md → rounded-lg.
8902
+ Closes the chrome-strip segmented-control corner radius
8903
+ cascade (Layout R375 + nodeSize R376 + zoom R376). */}
8039
8904
  <div
8040
- className="ml-1.5 flex items-center rounded-md border overflow-hidden"
8905
+ className="ml-1.5 flex items-center rounded-lg border overflow-hidden"
8906
+ data-topo-chrome-zoom-wrapper-radius="rounded-lg"
8041
8907
  style={{
8042
8908
  background: pal.legendBox.fill,
8043
8909
  borderColor: pal.containerBorder,
@@ -8053,7 +8919,8 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8053
8919
  // R196: press-state deepens bg one tier above hover (white/5
8054
8920
  // → white/10) so mouse-down has a tactile dim before the
8055
8921
  // R186 icon pop fires on release.
8056
- className="px-2 py-1 hover:bg-white/5 active:bg-white/10 transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset"
8922
+ // R352: `group` lets the inner svg respond via group-hover.
8923
+ className="group px-2 py-1 hover:bg-white/5 active:bg-white/10 transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset"
8057
8924
  style={{ color: pal.legendText }}
8058
8925
  aria-label="Zoom out"
8059
8926
  title="Zoom out (−)"
@@ -8061,11 +8928,25 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8061
8928
  {/* R186: icon pop on click. CSS animation runs once;
8062
8929
  React removes the class after 240ms so a quick
8063
8930
  re-click can replay. */}
8931
+ {/* Round 352 / Loop: zoom-out icon picks up group-hover:
8932
+ scale-110 — sibling to R350 reset hover-rotate. Pre-
8933
+ R352 hovering the zoom button only changed the bg
8934
+ (white/5); the icon inside stayed perfectly still.
8935
+ R352 lifts the icon 10% on hover for a tactile "this
8936
+ button does something" cue. The R186 anet-chrome-pop
8937
+ keyframe (220ms scale 1→1.06→1) still owns transform
8938
+ during click via CSS-animation precedence over
8939
+ transition-transform; after the pop ends + className
8940
+ is removed, the group-hover scale-110 picks up
8941
+ smoothly. `transform-gpu` hint promotes the svg to
8942
+ its own compositor layer for crisper edges during
8943
+ the scale tween. Sibling change on zoom-in icon
8944
+ below. */}
8064
8945
  <svg
8065
8946
  width="12" height="12" viewBox="0 0 24 24"
8066
8947
  fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"
8067
8948
  aria-hidden
8068
- className={chromePopping === 'zoom-out' ? 'anet-chrome-pop' : undefined}
8949
+ className={`transition-transform duration-200 ease-out group-hover:scale-110 transform-gpu${chromePopping === 'zoom-out' ? ' anet-chrome-pop' : ''}`}
8069
8950
  data-topo-chrome-zoom-out-icon
8070
8951
  ><path d="M5 12h14" /></svg>
8071
8952
  </button>
@@ -8111,11 +8992,17 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8111
8992
  chromePopping === 'zoom-in' || chromePopping === 'zoom-out'
8112
8993
  ? 'true' : 'false'
8113
8994
  }
8995
+ data-topo-chrome-zoom-level-hover={hoveredZoomLevel ? 'true' : 'false'}
8996
+ onMouseEnter={() => setHoveredZoomLevel(true)}
8997
+ onMouseLeave={() => setHoveredZoomLevel(false)}
8114
8998
  style={{
8115
8999
  color: pal.legendText,
8116
9000
  borderColor: pal.containerBorder,
8117
9001
  minWidth: 46,
8118
9002
  display: 'inline-block',
9003
+ // R347: letter-spacing hover tween — extends R344/R345
9004
+ // hover-letter-spacing family into the chrome strip.
9005
+ letterSpacing: hoveredZoomLevel ? '0.5px' : '0',
8119
9006
  /* Round 264 / Loop: zoom level readout gains theme-toggle
8120
9007
  transition. The span has theme-driven color (pal.
8121
9008
  legendText) + border-x (pal.containerBorder via the
@@ -8124,7 +9011,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8124
9011
  on theme flip while siblings eased. Sibling treatment
8125
9012
  to the nodeSize + zoom wrapper transitions added this
8126
9013
  round. */
8127
- transition: 'color 200ms ease-out, border-color 200ms ease-out',
9014
+ transition: 'color 200ms ease-out, border-color 200ms ease-out, letter-spacing 200ms ease-out',
8128
9015
  }}
8129
9016
  title="Current zoom level"
8130
9017
  >
@@ -8135,18 +9022,22 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8135
9022
  data-topo-chrome-zoom-in
8136
9023
  data-topo-chrome-zoom-in-popping={chromePopping === 'zoom-in' ? 'true' : 'false'}
8137
9024
  // R196: press-state (mirror of zoom-out above).
8138
- className="px-2 py-1 hover:bg-white/5 active:bg-white/10 transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset"
9025
+ // R352: `group` lets the inner svg respond via group-hover.
9026
+ className="group px-2 py-1 hover:bg-white/5 active:bg-white/10 transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset"
8139
9027
  style={{ color: pal.legendText }}
8140
9028
  aria-label="Zoom in"
8141
9029
  title="Zoom in (+)"
8142
9030
  >
8143
9031
  {/* R186: icon pop on click. Same one-shot CSS animation
8144
9032
  as zoom-out; React removes the class after 240ms. */}
9033
+ {/* R352 sibling — zoom-in icon picks up the same
9034
+ group-hover:scale-110 family. Mirror change at
9035
+ the zoom-out icon above. */}
8145
9036
  <svg
8146
9037
  width="12" height="12" viewBox="0 0 24 24"
8147
9038
  fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"
8148
9039
  aria-hidden
8149
- className={chromePopping === 'zoom-in' ? 'anet-chrome-pop' : undefined}
9040
+ className={`transition-transform duration-200 ease-out group-hover:scale-110 transform-gpu${chromePopping === 'zoom-in' ? ' anet-chrome-pop' : ''}`}
8150
9041
  data-topo-chrome-zoom-in-icon
8151
9042
  ><path d="M12 5v14M5 12h14" /></svg>
8152
9043
  </button>
@@ -8155,6 +9046,12 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8155
9046
  onClick={() => { armResetSpin(); resetView(); }}
8156
9047
  data-topo-chrome-reset
8157
9048
  data-topo-chrome-reset-spinning={resetSpinning ? 'true' : 'false'}
9049
+ data-topo-chrome-reset-hover={hoveredReset ? 'true' : 'false'}
9050
+ // R350: hover state drives the icon transform below.
9051
+ onMouseEnter={() => setHoveredReset(true)}
9052
+ onMouseLeave={() => setHoveredReset(false)}
9053
+ onFocus={() => setHoveredReset(true)}
9054
+ onBlur={() => setHoveredReset(false)}
8158
9055
  // R196: press-state deepens before R184 reset-spin fires on
8159
9056
  // release — mouse-down dim then 450ms spin = full handshake.
8160
9057
  className="p-1.5 rounded-md border hover:bg-white/5 active:bg-white/10 transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60"
@@ -8184,6 +9081,17 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8184
9081
  aria-hidden
8185
9082
  className={resetSpinning ? 'anet-reset-spin' : undefined}
8186
9083
  data-topo-chrome-reset-icon
9084
+ // R350: hover-rotate preview of the R184 click-spin.
9085
+ // Gated on !resetSpinning so the anet-reset-spin keyframe
9086
+ // owns transform during its 450ms run. transformOrigin
9087
+ // 'center' so rotation pivots around the icon's centre
9088
+ // (default would be top-left and the icon would arc).
9089
+ style={{
9090
+ transform: hoveredReset && !resetSpinning ? 'rotate(-8deg)' : 'rotate(0deg)',
9091
+ transformOrigin: 'center',
9092
+ transition: 'transform 200ms ease-out',
9093
+ }}
9094
+ data-topo-chrome-reset-icon-hover={hoveredReset && !resetSpinning ? 'true' : 'false'}
8187
9095
  >
8188
9096
  <path d="M3 12a9 9 0 1 0 9-9 9 9 0 0 0-6.4 2.6L3 8" />
8189
9097
  <path d="M3 3v5h5" />
@@ -8217,7 +9125,12 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8217
9125
  its inactive state benefits from the same "hover previews
8218
9126
  active state" idiom R163 designed. Sibling treatment to
8219
9127
  the nodeSize buttons at line ~6711. */
8220
- className={`p-1.5 rounded-md border transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 ${
9128
+ // R353: `group` lets the inner svg respond via group-hover
9129
+ // sibling to R352 zoom buttons. Closes the chrome-strip per-
9130
+ // icon hover-affordance arc (zoom-out / zoom-in / reset /
9131
+ // fullscreen now all carry an icon-level hover gesture in
9132
+ // addition to the bg hover).
9133
+ className={`group p-1.5 rounded-md border transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 ${
8221
9134
  isFullscreen
8222
9135
  ? 'bg-cyan-500/15 text-cyan-300 font-medium hover:bg-cyan-500/20 active:bg-cyan-500/25'
8223
9136
  : 'hover:bg-cyan-500/5 active:bg-cyan-500/15'
@@ -8236,12 +9149,20 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8236
9149
  at the reset icon above. data-topo-chrome-fullscreen-
8237
9150
  icon attribute exposes BOTH variants (entered / exited)
8238
9151
  for the round's stroke-width regression probe. */}
9152
+ {/* Round 353 / Loop: fullscreen icon (both enter + exit
9153
+ variants) picks up the R352 family group-hover:scale-110.
9154
+ Pre-R353 hovering the button only changed the bg; the
9155
+ icon stayed still. R353 lifts the icon 10 % on hover —
9156
+ same gesture vocabulary as the zoom buttons. transform-
9157
+ gpu hint promotes the svg to its own compositor layer
9158
+ for crisper edges during the scale tween. Closes the
9159
+ chrome-strip per-icon hover-affordance arc. */}
8239
9160
  {isFullscreen ? (
8240
- <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden data-topo-chrome-fullscreen-icon="exit">
9161
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden className="transition-transform duration-200 ease-out group-hover:scale-110 transform-gpu" data-topo-chrome-fullscreen-icon="exit">
8241
9162
  <path d="M8 3v4a1 1 0 0 1-1 1H3M21 8h-4a1 1 0 0 1-1-1V3M3 16h4a1 1 0 0 1 1 1v4M16 21v-4a1 1 0 0 1 1-1h4" />
8242
9163
  </svg>
8243
9164
  ) : (
8244
- <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden data-topo-chrome-fullscreen-icon="enter">
9165
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden className="transition-transform duration-200 ease-out group-hover:scale-110 transform-gpu" data-topo-chrome-fullscreen-icon="enter">
8245
9166
  <path d="M3 8V5a2 2 0 0 1 2-2h3M21 8V5a2 2 0 0 0-2-2h-3M3 16v3a2 2 0 0 0 2 2h3M21 16v3a2 2 0 0 1-2 2h-3" />
8246
9167
  </svg>
8247
9168
  )}