@sleep2agi/agent-network-dashboard 0.5.1-preview.9 → 0.5.1-preview.91

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 (236) 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/017hq2-5l~_98.css +2 -0
  141. package/.next/static/chunks/07t9_p5h7da1u.js +1 -0
  142. package/.next/static/chunks/0ahff_xzbgbg4.js +4 -0
  143. package/.next/static/chunks/10qa7z9iocn6t.js +1 -0
  144. package/.next/trace +2 -2
  145. package/.next/trace-build +1 -1
  146. package/app/components/TopoGraph.tsx +1777 -111
  147. package/package.json +1 -1
  148. package/scripts/topo-active-links-chip-hover-lift-test.mjs +93 -0
  149. package/scripts/topo-chip-digit-fontweight-test.mjs +105 -0
  150. package/scripts/topo-chip-digit-hover-bold-test.mjs +94 -0
  151. package/scripts/topo-chip-row-group-hover-brighten-test.mjs +107 -0
  152. package/scripts/topo-chip-row-hover-lift-test.mjs +95 -0
  153. package/scripts/topo-chrome-button-hover-lift-test.mjs +94 -0
  154. package/scripts/topo-chrome-segmented-radius-test.mjs +100 -0
  155. package/scripts/topo-click-ripple-opacity-test.mjs +99 -0
  156. package/scripts/topo-edge-badge-digit-fw-test.mjs +103 -0
  157. package/scripts/topo-edge-badge-fontsize-test.mjs +90 -0
  158. package/scripts/topo-edge-badge-hover-opacity-test.mjs +94 -0
  159. package/scripts/topo-edge-badge-hover-stroke-test.mjs +92 -0
  160. package/scripts/topo-edge-badge-opacity-test.mjs +80 -0
  161. package/scripts/topo-edge-badge-pin-opacity-test.mjs +86 -0
  162. package/scripts/topo-edge-badge-stroke-test.mjs +92 -0
  163. package/scripts/topo-edge-freshness-floor-test.mjs +99 -0
  164. package/scripts/topo-edge-particle-radius-test.mjs +76 -0
  165. package/scripts/topo-edge-visible-linecap-test.mjs +89 -0
  166. package/scripts/topo-filter-pill-hover-lift-test.mjs +101 -0
  167. package/scripts/topo-filter-pill-hover-opacity-test.mjs +110 -0
  168. package/scripts/topo-filter-pill-value-fw-test.mjs +88 -0
  169. package/scripts/topo-filter-pill-x-hover-scale-test.mjs +99 -0
  170. package/scripts/topo-flow-rail-linecap-test.mjs +79 -0
  171. package/scripts/topo-freshness-chip-hierarchy-test.mjs +93 -0
  172. package/scripts/topo-freshness-chip-tabular-test.mjs +41 -0
  173. package/scripts/topo-freshness-floor-lift-test.mjs +92 -0
  174. package/scripts/topo-freshness-suffix-tabular-test.mjs +88 -0
  175. package/scripts/topo-fullscreen-icon-hover-scale-test.mjs +91 -0
  176. package/scripts/topo-group-box-stroke-test.mjs +105 -0
  177. package/scripts/topo-group-label-count-fontweight-test.mjs +108 -0
  178. package/scripts/topo-hover-detail-body-fw-test.mjs +101 -0
  179. package/scripts/topo-hover-detail-model-fw-test.mjs +98 -0
  180. package/scripts/topo-hover-detail-opacity-test.mjs +98 -0
  181. package/scripts/topo-hover-detail-rx-test.mjs +81 -0
  182. package/scripts/topo-hub-digit-fontsize-test.mjs +86 -0
  183. package/scripts/topo-hub-digit-fw-hover-test.mjs +102 -0
  184. package/scripts/topo-hub-halo-light-trough-test.mjs +88 -0
  185. package/scripts/topo-hub-halo-radius-test.mjs +86 -0
  186. package/scripts/topo-hub-halo-trough-test.mjs +83 -0
  187. package/scripts/topo-hub-highlight-opacity-test.mjs +88 -0
  188. package/scripts/topo-hub-highlight-radius-test.mjs +90 -0
  189. package/scripts/topo-hub-hover-ring-opacity-test.mjs +96 -0
  190. package/scripts/topo-hub-hover-ring-stroke-test.mjs +86 -0
  191. package/scripts/topo-hub-spoke-hover-opacity-test.mjs +119 -0
  192. package/scripts/topo-hub-spoke-linecap-test.mjs +80 -0
  193. package/scripts/topo-label-card-opacity-hover-test.mjs +99 -0
  194. package/scripts/topo-layout-toggle-hover-tracking-test.mjs +109 -0
  195. package/scripts/topo-layout-toggle-radius-test.mjs +87 -0
  196. package/scripts/topo-legend-label-fontweight-test.mjs +94 -0
  197. package/scripts/topo-legend-pin-ring-stroke-test.mjs +101 -0
  198. package/scripts/topo-minimap-offline-opacity-test.mjs +90 -0
  199. package/scripts/topo-minimap-online-hover-opacity-test.mjs +92 -0
  200. package/scripts/topo-minimap-online-opacity-test.mjs +93 -0
  201. package/scripts/topo-minimap-online-radius-test.mjs +85 -0
  202. package/scripts/topo-minimap-viewport-linejoin-test.mjs +75 -0
  203. package/scripts/topo-minimap-viewport-rx-test.mjs +85 -0
  204. package/scripts/topo-more-flows-fontweight-test.mjs +103 -0
  205. package/scripts/topo-node-alias-letter-spacing-test.mjs +112 -0
  206. package/scripts/topo-node-halo-offline-opacity-test.mjs +87 -0
  207. package/scripts/topo-node-label-card-rx-test.mjs +85 -0
  208. package/scripts/topo-node-pulse-peak-test.mjs +89 -0
  209. package/scripts/topo-node-pulse-trough-test.mjs +91 -0
  210. package/scripts/topo-node-sub-text-letter-spacing-test.mjs +115 -0
  211. package/scripts/topo-panel-count-fw-hover-test.mjs +105 -0
  212. package/scripts/topo-panel-count-letterspacing-test.mjs +89 -0
  213. package/scripts/topo-panel-stroke-hover-test.mjs +110 -0
  214. package/scripts/topo-pressure-bar-height-test.mjs +92 -0
  215. package/scripts/topo-pressure-kicker-fontweight-test.mjs +76 -0
  216. package/scripts/topo-recent-pip-radius-2-test.mjs +72 -0
  217. package/scripts/topo-recent-pip-radius-test.mjs +76 -0
  218. package/scripts/topo-recent-row-content-opacity-test.mjs +81 -0
  219. package/scripts/topo-recent-row-text-fontweight-test.mjs +90 -0
  220. package/scripts/topo-reset-hover-rotate-test.mjs +102 -0
  221. package/scripts/topo-spoke-active-opacity-test.mjs +104 -0
  222. package/scripts/topo-spoke-active-stroke-test.mjs +95 -0
  223. package/scripts/topo-spoke-idle-opacity-test.mjs +91 -0
  224. package/scripts/topo-vendor-chip-hover-lift-test.mjs +87 -0
  225. package/scripts/topo-vendor-glyph-fontweight-test.mjs +102 -0
  226. package/scripts/topo-vendor-letter-hover-scale-test.mjs +129 -0
  227. package/scripts/topo-vendor-suffix-hover-brighten-test.mjs +87 -0
  228. package/scripts/topo-zoom-icon-hover-scale-test.mjs +114 -0
  229. package/scripts/topo-zoom-level-hover-fw-test.mjs +95 -0
  230. package/.next/static/chunks/08fc_cz1nk7b9.js +0 -1
  231. package/.next/static/chunks/0aauz~36q5n2a.css +0 -2
  232. package/.next/static/chunks/0e0okm.affulg.js +0 -1
  233. package/.next/static/chunks/0s3vtwfo26_t6.js +0 -4
  234. /package/.next/static/{egukPz1ctU--4WnT2FpEU → i0drwZtW8h-M1ML2C5VZF}/_buildManifest.js +0 -0
  235. /package/.next/static/{egukPz1ctU--4WnT2FpEU → i0drwZtW8h-M1ML2C5VZF}/_clientMiddlewareManifest.js +0 -0
  236. /package/.next/static/{egukPz1ctU--4WnT2FpEU → i0drwZtW8h-M1ML2C5VZF}/_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";
@@ -237,7 +257,21 @@ function FreshnessChip({ sessions }: { sessions: unknown }) {
237
257
  duration-300 still eases the bg/color flip. Title (hover
238
258
  tooltip) still spells out the full meaning in either
239
259
  state. */}
240
- {stale ? 'lag' : 'live'} · {sec}s
260
+ {/* Round 410 / Loop: FreshnessChip body picks up the chip-
261
+ internal-hierarchy arc. Pre-R410 the body rendered as a
262
+ single text node `lag · {sec}s` with the parent's font-
263
+ medium (fw=500) applied uniformly. R410 splits the digit
264
+ and unit into separate spans so the chip's internal
265
+ typography mirrors the family pattern R333-R341/R362/R369/
266
+ R389 established for the chip row:
267
+ digit (fw=600) data tier
268
+ unit (fw=500 + opacity=0.7) label tier
269
+ The `lag` prefix stays at the chip's baseline (fw=500
270
+ from parent font-medium) — it labels the state, not a
271
+ data value. data-freshness-chip-digit / -unit attrs
272
+ surface the spans for tests. tabular-nums + transition-
273
+ colors + R275 stale-only gate all preserved. */}
274
+ {stale ? 'lag' : 'live'} · <span className="font-semibold" data-freshness-chip-digit>{sec}</span><span className="opacity-70" data-freshness-chip-unit>s</span>
241
275
  </span>
242
276
  );
243
277
  }
@@ -1050,6 +1084,18 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1050
1084
  // 200ms ease-out joins the existing R264 color/border transition
1051
1085
  // list on the same span.
1052
1086
  const [hoveredZoomLevel, setHoveredZoomLevel] = useState(false);
1087
+ // Round 350 / Loop: reset-button icon hover-rotate preview of the
1088
+ // R184 click-spin. Pre-R350 hovering the reset button only changed
1089
+ // the button bg (white/5); the icon inside stayed perfectly still.
1090
+ // R350 nudges the icon -8° on hover — a tactile hint that this
1091
+ // button rotates the icon on click. When the click fires, the
1092
+ // R184 anet-reset-spin keyframe animation overrides the hover
1093
+ // transform for its 450 ms run (CSS animations win over transitions
1094
+ // on the same property); when the animation ends + React removes
1095
+ // the className, the inline transform eases back to whatever the
1096
+ // hover state says — either -8° (still hovering) or 0 (mouse left).
1097
+ // 350th-round milestone polish.
1098
+ const [hoveredReset, setHoveredReset] = useState(false);
1053
1099
  // R135: panel-wide hover-elevation. The recent-signal + legend
1054
1100
  // panels both already host clickable rows (R56/R116 recent rows,
1055
1101
  // R55/R61 legend rows) and a clickable footer (R133), so the
@@ -1811,12 +1857,30 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1811
1857
  while honoring R328's wider baseline rhythm. data-topo-
1812
1858
  chrome-layout-trailer attr unchanged — it still marks
1813
1859
  the boundary surface for the gap probe. */}
1860
+ {/* Round 375 / Loop: Layout-toggle wrapper rounded-md → rounded-
1861
+ lg (6 → 8 px). Extends the corner-radius cascade family
1862
+ to the chrome-strip layout-toggle wrapper:
1863
+ R330 canvas wrapper rounded-xl 12 px
1864
+ R331 SVG panels rx=10 10 px
1865
+ R332 minimap container rounded-lg 8 px
1866
+ R375 Layout-toggle wrapper rounded-lg 8 px (this round)
1867
+ Pre-R375 the wrapper at rounded-md (6 px) was the only
1868
+ chrome-strip container still using the smaller corner
1869
+ radius — both R330 outer wrapper and R332 minimap sit at
1870
+ ≥ 8 px, so the Layout toggle's 6 px stood out as a
1871
+ tighter corner against the family. R375 brings it into
1872
+ the rounded-lg tier where the minimap already lives.
1873
+ Pure paint change — overflow-hidden still clips the
1874
+ inner buttons' bg-cyan-500/15 tints; no layout shift.
1875
+ R268 border-color + 200ms transition + R329 mr-0.5 +
1876
+ data-topo-chrome-layout-trailer all preserved. */}
1814
1877
  <div
1815
- className="mr-0.5 inline-flex rounded-md border overflow-hidden"
1878
+ className="mr-0.5 inline-flex rounded-lg border overflow-hidden"
1816
1879
  style={{ borderColor: pal.containerBorder, transition: 'border-color 200ms ease-out' }}
1817
1880
  role="group"
1818
1881
  aria-label="Topology layout"
1819
1882
  data-topo-chrome-layout-trailer
1883
+ data-topo-chrome-layout-radius="rounded-lg"
1820
1884
  >
1821
1885
  <button
1822
1886
  onClick={() => { popChrome('layout-ring'); if (layout !== 'ring') toggleLayout(); }}
@@ -1850,7 +1914,17 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1850
1914
  weight; cyan-400/60 + ring-inset retained. The
1851
1915
  R163/R196 hover/active deeps + R249 chrome-pop
1852
1916
  click feedback continue unchanged. */
1853
- 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' : ''}`}
1917
+ // R351: hover:tracking-wide extends the R344/R345/R347
1918
+ // hover-letter-spacing family to a 4th surface (chrome-
1919
+ // strip Ring/Grid pair). transition-colors className
1920
+ // dropped in favour of an inline transition spec that
1921
+ // bundles bg/color (150ms ease) + letter-spacing
1922
+ // (200ms ease-out) — Tailwind's transition-colors
1923
+ // doesn't list letter-spacing, so without this the
1924
+ // hover:tracking-wide would snap. Sibling change on
1925
+ // the Grid button below.
1926
+ 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' : ''}`}
1927
+ style={{ transition: 'background-color 150ms ease, color 150ms ease, letter-spacing 200ms ease-out' }}
1854
1928
  >
1855
1929
  Ring
1856
1930
  </button>
@@ -1867,14 +1941,20 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1867
1941
  // Round 306 / Loop: focus-visible:ring-2 → ring-1 sibling
1868
1942
  // change to Ring above — unifies focus-ring width across
1869
1943
  // all chrome buttons.
1870
- 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' : ''}`}
1944
+ // R351 sibling Grid button picks up hover:tracking-wide
1945
+ // + inline transition spec. Same vocabulary as Ring.
1946
+ 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' : ''}`}
1871
1947
  /* Round 268 / Loop: Grid button's left border (the
1872
1948
  internal divider between Ring and Grid) picks up
1873
1949
  pal.containerBorder, matching the wrapper change at
1874
1950
  line ~1460 and the chrome strip's segmented borders
1875
- (nodeSize, zoom). transition-colors className covers
1876
- the border-color eased on theme toggle. */
1877
- style={{ borderColor: pal.containerBorder }}
1951
+ (nodeSize, zoom). The R268 transition-colors className
1952
+ used to carry the border-color ease; R351 unfolds the
1953
+ transition list into the inline spec below so the
1954
+ letter-spacing tween rides alongside without snapping
1955
+ the border-color flip — border-color 200ms ease-out
1956
+ keeps R268's theme-toggle smoothness intact. */
1957
+ style={{ borderColor: pal.containerBorder, transition: 'background-color 150ms ease, color 150ms ease, border-color 200ms ease-out, letter-spacing 200ms ease-out' }}
1878
1958
  >
1879
1959
  Grid
1880
1960
  </button>
@@ -1961,11 +2041,43 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
1961
2041
  7th surface in the info-density tabular-nums
1962
2042
  sweep — and the first on the HTML side
1963
2043
  (previous 6 were SVG <text>/<tspan>). */
1964
- className={`tabular-nums font-medium px-2.5 py-1 rounded-md border anet-topo-chip-focus transition-colors duration-200 ${
2044
+ /* Round 398 / Loop: chip-row chips gain hover translateY
2045
+ (-1px) lift on the CLICKABLE variant only (workingCount
2046
+ > 0 here / onlineNodes.length > 0 below / activeLinks
2047
+ > 0 deeper). Pre-R398 the chips brightened bg + border
2048
+ on hover (R201) but didn't lift — only their clickable
2049
+ siblings (filter pin pills R397, recent rows R143,
2050
+ legend rows R144) acknowledged cursor entry with a
2051
+ translate-y. R398 closes the chip-row by extending
2052
+ the same gesture to the static header chips, gated
2053
+ on the clickable role so empty chips (which have
2054
+ no role="button") stay planted at their R205
2055
+ opacity-50 receded paint. transition-transform
2056
+ + duration-200 + ease-out + transform-gpu added
2057
+ alongside existing transition-colors so the lift
2058
+ and the color tween share rhythm.
2059
+ Gesture-vocabulary table (post-R398):
2060
+ recent-signal row -1 px (R143)
2061
+ legend row -1 px (R144)
2062
+ group cluster box fill+sw lift (R142)
2063
+ filter pin pills -1 px (R397)
2064
+ chip-row chips -1 px (R398, this round)
2065
+ Empty chips: no lift. Pin-mirror chips: no
2066
+ conflict (R180 inset double-ring is a box-shadow
2067
+ not a transform). new data-chip-hover-lift attr
2068
+ surfaces the lift surface for tests. */
2069
+ // R414: chip-row chips gain `group` so inner unit
2070
+ // span brightens via group-hover:opacity-100 — sibling
2071
+ // to R355 filter pin pill inner-span hover-brighten.
2072
+ // Hover-brighten family extends from filter pills to
2073
+ // chip-row chips at the inner-span scope.
2074
+ className={`group tabular-nums font-medium px-2.5 py-1 rounded-md border anet-topo-chip-focus transition-colors transition-transform duration-200 ease-out transform-gpu ${
1965
2075
  workingCount > 0
1966
- ? 'bg-green-500/10 text-green-300 border-green-500/20 hover:bg-green-500/15 hover:border-green-500/30'
2076
+ ? 'bg-green-500/10 text-green-300 border-green-500/20 hover:bg-green-500/15 hover:border-green-500/30 hover:-translate-y-px'
1967
2077
  : 'bg-green-500/10 text-green-300 border-green-500/20'
1968
2078
  }`}
2079
+ data-chip-hover-lift={workingCount > 0 ? 'true' : 'false'}
2080
+ data-chip-group-hover-brighten="true"
1969
2081
  data-working-chip
1970
2082
  data-working-chip-aliases={workingAliases.join(',')}
1971
2083
  data-pin-mirror={pinnedStatus === 'working' ? 'true' : 'false'}
@@ -2035,7 +2147,17 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2035
2147
  pattern: small label spans demote, value stays
2036
2148
  prominent. data-working-chip-unit exposes the
2037
2149
  span for tests. */}
2038
- {workingCount}<span className="opacity-70" data-working-chip-unit> working</span>
2150
+ {/* Round 362 / Loop: digit picks up font-semibold
2151
+ (fw 500 → 600) for within-chip weight tier. The
2152
+ chip's outer className stays at font-medium (R313
2153
+ data-weight baseline); the digit overrides to
2154
+ semibold so it reads heavier than its " working"
2155
+ unit (which keeps fw 500 + R338 opacity-70).
2156
+ Joins the R333-R341 chip-internal-hierarchy arc
2157
+ at the chip-count scope. Sibling edits on the
2158
+ online + active-links chip digits below. data-
2159
+ working-chip-digit attr exposes the digit span. */}
2160
+ <span className="font-semibold transition-[font-weight] duration-200 group-hover:font-bold" data-working-chip-digit>{workingCount}</span><span className="opacity-70 transition-opacity duration-200 group-hover:opacity-100" data-working-chip-unit> working</span>
2039
2161
  </span>
2040
2162
  <span
2041
2163
  // Round 201 / Loop: online chip — mirror of the working
@@ -2049,11 +2171,15 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2049
2171
  /* Round 232 / Loop: tabular-nums on online chip
2050
2172
  (sibling treatment to working chip — same row,
2051
2173
  same digit-jitter physics on count crossings). */
2052
- className={`tabular-nums font-medium px-2.5 py-1 rounded-md border anet-topo-chip-focus transition-colors duration-200 ${
2174
+ // R398: hover translate-y lift on clickable variant see working chip above.
2175
+ // R414: `group` parent + inner unit span group-hover-brighten — see working chip above.
2176
+ className={`group tabular-nums font-medium px-2.5 py-1 rounded-md border anet-topo-chip-focus transition-colors transition-transform duration-200 ease-out transform-gpu ${
2053
2177
  onlineNodes.length > 0
2054
- ? 'bg-cyan-500/10 text-cyan-300 border-cyan-500/20 hover:bg-cyan-500/15 hover:border-cyan-500/30'
2178
+ ? 'bg-cyan-500/10 text-cyan-300 border-cyan-500/20 hover:bg-cyan-500/15 hover:border-cyan-500/30 hover:-translate-y-px'
2055
2179
  : 'bg-cyan-500/10 text-cyan-300 border-cyan-500/20'
2056
2180
  }`}
2181
+ data-chip-hover-lift={onlineNodes.length > 0 ? 'true' : 'false'}
2182
+ data-chip-group-hover-brighten="true"
2057
2183
  data-online-chip
2058
2184
  data-online-chip-aliases={onlineAliases.join(',')}
2059
2185
  data-pin-mirror={pinnedStatus === 'idle' ? 'true' : 'false'}
@@ -2098,7 +2224,8 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2098
2224
  }}
2099
2225
  >
2100
2226
  {/* R337 sibling — online chip unit demotion. */}
2101
- {onlineNodes.length}<span className="opacity-70" data-online-chip-unit> online</span>
2227
+ {/* R362 sibling — online-chip digit gains font-semibold. */}
2228
+ <span className="font-semibold transition-[font-weight] duration-200 group-hover:font-bold" data-online-chip-digit>{onlineNodes.length}</span><span className="opacity-70 transition-opacity duration-200 group-hover:opacity-100" data-online-chip-unit> online</span>
2102
2229
  </span>
2103
2230
  </>
2104
2231
  );
@@ -2225,8 +2352,45 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2225
2352
  title={`${w} working · ${i} idle · ${o} offline`}
2226
2353
  data-fleet-pressure
2227
2354
  >
2228
- <span className="text-[10px] tracking-wide">pressure</span>
2229
- <span className="inline-flex h-1.5 w-16 rounded-full overflow-hidden" style={{ background: 'rgb(75 85 99 / 0.25)' }}>
2355
+ {/* Round 373 / Loop: pressure-bar kicker label gains
2356
+ font-medium (fw 400 500). Sibling small-text fw
2357
+ lift family with R363 recent-row alias + R364
2358
+ legend-row label + R366 group-label count + R368
2359
+ +N more flows footer — extends to a 5th surface
2360
+ (the chip-row's 'pressure' label). At fontSize
2361
+ 10 px tracking-wide against the chip's gray bg,
2362
+ the default fw 400 sat below the deliberate-data
2363
+ band; fw 500 brings it into parity with the
2364
+ chip-row 'working / online / active links' unit
2365
+ spans (chip-level font-medium R313). data-fleet-
2366
+ pressure-kicker attr exposes the kicker for tests. */}
2367
+ <span className="text-[10px] tracking-wide font-medium" data-fleet-pressure-kicker>pressure</span>
2368
+ {/* Round 374 / Loop: pressure-bar height h-1.5 → h-2
2369
+ (6 → 8 px) — sibling visual-weight bump (8th anchor
2370
+ in the family):
2371
+ R287 minimap viewport stroke 1 → 1.5
2372
+ R295 legend swatch base radius 5.5 → 6
2373
+ R359 recent-row pip base radius 1.6 → 1.8
2374
+ R360 hub digit fontSize 11 → 12
2375
+ R361 edge-badge digit fontSize 10 → 11
2376
+ R365 hub-highlight base radius 5 → 5.5
2377
+ R367 edge-badge rest stroke 1 → 1.25
2378
+ R374 pressure-bar height 1.5 → 2 (this round)
2379
+ +33 % bar height gives the working/idle/offline
2380
+ segments more visibility — at h-1.5 the 3-segment
2381
+ proportion bar was readable but slim; at h-2 the
2382
+ segments parse cleanly even when one tier is
2383
+ < 10 % share. Geometry-safe: items-center flex
2384
+ centers the bar inside the chip's py-1 (4 px top +
2385
+ 4 px bottom) — bar at 8 px stays comfortably
2386
+ inside the 10-px text-row height. R165 segment
2387
+ width transitions + R210 brightness hover + R83
2388
+ pin-mirror box-shadow on segments all preserved
2389
+ (segments inherit width from parent so the height
2390
+ bump propagates without segment-side edits).
2391
+ data-fleet-pressure-bar-height attr exposes the
2392
+ height token for tests. */}
2393
+ <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">
2230
2394
  {seg(w, isLight ? '#059669' : '#22c55e', 'working', 'working')}
2231
2395
  {seg(i, isLight ? '#0d9488' : '#2dd4bf', 'idle', 'idle')}
2232
2396
  {seg(o, isLight ? '#94a3b8' : '#6b7280', 'offline', 'offline')}
@@ -2283,7 +2447,10 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2283
2447
  data-active-filter="status"
2284
2448
  data-filter-match-count={matchCount}
2285
2449
  data-filter-match-aliases={matchAliases.join(',')}
2286
- 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"
2450
+ // R355: `group` lets the inner opacity-70 spans (prefix
2451
+ // `filter:` + count `· N`) brighten to 100 % on pill hover.
2452
+ // Sibling treatment on group + vendor pills below.
2453
+ className="group inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md font-mono font-medium text-xs border anet-fade-in anet-topo-chip-focus transition-transform duration-200 ease-out hover:-translate-y-px transform-gpu" data-topo-filter-pill-hover-lift="true"
2287
2454
  title={matchCount > 0 ? `${matchPreview}${matchSuffix} — click to clear` : 'Click to clear filter'}
2288
2455
  onClick={() => setPinnedStatus(null)}
2289
2456
  style={{
@@ -2297,12 +2464,37 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2297
2464
  cursor: 'pointer',
2298
2465
  }}
2299
2466
  >
2300
- <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>
2467
+ {/* Round 412 / Loop: filter pin pill VALUE picks up the
2468
+ chip-internal-hierarchy arc. Pre-R412 the value span
2469
+ (pinnedStatus / pinnedGroup / pinnedVendor) inherited
2470
+ the parent's font-medium (fw=500); prefix and suffix
2471
+ were opacity-70 label-tier but the VALUE itself sat
2472
+ at the same baseline weight. R412 wraps the value in
2473
+ a font-semibold span (fw=600) so the pill now reads
2474
+ with proper data-tier emphasis — sibling treatment
2475
+ to R333/R335-R341/R362/R369/R389/R410. data-filter-
2476
+ value attr surfaces the value span for tests.
2477
+ 4-pill replace family — status / group / vendor / edge. */}
2478
+ <span><span className="hidden sm:inline opacity-70 transition-opacity duration-200 group-hover:opacity-100" data-filter-prefix>filter: </span><span className="font-semibold" data-filter-value>{pinnedStatus}</span><span className="opacity-70 tabular-nums transition-opacity duration-200 group-hover:opacity-100" data-filter-pill-count> · {matchCount}</span></span>
2301
2479
  <button
2302
2480
  type="button"
2303
2481
  aria-label={`Clear ${pinnedStatus} filter`}
2304
2482
  onClick={(e) => { e.stopPropagation(); setPinnedStatus(null); }}
2305
- className="ml-0.5 leading-none hover:opacity-70"
2483
+ /* Round 356 / Loop: filter pin pill × buttons gain
2484
+ hover:scale-110 (Tailwind 4 modern CSS `scale` property,
2485
+ not legacy transform). Sibling polish to R354 vendor
2486
+ letter glyph + R350/R352/R353 chrome icon hover-scales.
2487
+ Pre-R356 the × had only hover:opacity-70 — the target
2488
+ dimmed under cursor but didn't lift. R356 adds a 10 %
2489
+ scale on hover so the click-target reads as "press me"
2490
+ alongside the dim. transform-gpu hint promotes the
2491
+ button to its own compositor layer for crisper edges
2492
+ during the scale tween. transition-transform duration-
2493
+ 200 matches the chrome icon hover-scale timing family.
2494
+ inline-block is default for <button> so no display
2495
+ tweak needed. replace_all covers all 4 filter pin
2496
+ pills (status / group / vendor / edge) at once. */
2497
+ className="ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 transform-gpu"
2306
2498
  style={{ background: 'transparent', color: 'inherit', cursor: 'pointer', padding: 0 }}
2307
2499
  >×</button>
2308
2500
  </span>
@@ -2321,7 +2513,8 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2321
2513
  data-active-filter="group"
2322
2514
  data-filter-match-count={matchCount}
2323
2515
  data-filter-match-aliases={matchAliases.join(',')}
2324
- 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"
2516
+ // R355 sibling `group` parent + group-hover on inner spans.
2517
+ className="group inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md font-mono font-medium text-xs border anet-fade-in anet-topo-chip-focus transition-transform duration-200 ease-out hover:-translate-y-px transform-gpu" data-topo-filter-pill-hover-lift="true"
2325
2518
  title={matchCount > 0 ? `${matchPreview}${matchSuffix} — click to clear` : 'Click to clear filter'}
2326
2519
  onClick={() => setPinnedGroup(null)}
2327
2520
  style={{
@@ -2331,12 +2524,27 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2331
2524
  cursor: 'pointer',
2332
2525
  }}
2333
2526
  >
2334
- <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>
2527
+ {/* R412: see status pill above — filter value fw=600 data tier. */}
2528
+ <span><span className="hidden sm:inline opacity-70 transition-opacity duration-200 group-hover:opacity-100" data-filter-prefix>filter: </span><span className="font-semibold" data-filter-value>{pinnedGroup}</span><span className="opacity-70 tabular-nums transition-opacity duration-200 group-hover:opacity-100" data-filter-pill-count> · {matchCount}</span></span>
2335
2529
  <button
2336
2530
  type="button"
2337
2531
  aria-label={`Clear group filter ${pinnedGroup}`}
2338
2532
  onClick={(e) => { e.stopPropagation(); setPinnedGroup(null); }}
2339
- className="ml-0.5 leading-none hover:opacity-70"
2533
+ /* Round 356 / Loop: filter pin pill × buttons gain
2534
+ hover:scale-110 (Tailwind 4 modern CSS `scale` property,
2535
+ not legacy transform). Sibling polish to R354 vendor
2536
+ letter glyph + R350/R352/R353 chrome icon hover-scales.
2537
+ Pre-R356 the × had only hover:opacity-70 — the target
2538
+ dimmed under cursor but didn't lift. R356 adds a 10 %
2539
+ scale on hover so the click-target reads as "press me"
2540
+ alongside the dim. transform-gpu hint promotes the
2541
+ button to its own compositor layer for crisper edges
2542
+ during the scale tween. transition-transform duration-
2543
+ 200 matches the chrome icon hover-scale timing family.
2544
+ inline-block is default for <button> so no display
2545
+ tweak needed. replace_all covers all 4 filter pin
2546
+ pills (status / group / vendor / edge) at once. */
2547
+ className="ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 transform-gpu"
2340
2548
  style={{ background: 'transparent', color: 'inherit', cursor: 'pointer', padding: 0 }}
2341
2549
  >×</button>
2342
2550
  </span>
@@ -2371,7 +2579,8 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2371
2579
  data-active-filter="vendor"
2372
2580
  data-filter-match-count={matchCount}
2373
2581
  data-filter-match-aliases={matchAliases.join(',')}
2374
- 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"
2582
+ // R355 sibling `group` parent + group-hover on inner spans.
2583
+ className="group inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md font-mono font-medium text-xs border anet-fade-in anet-topo-chip-focus transition-transform duration-200 ease-out hover:-translate-y-px transform-gpu" data-topo-filter-pill-hover-lift="true"
2375
2584
  title={matchCount > 0 ? `${matchPreview}${matchSuffix} — click to clear` : 'Click to clear vendor filter'}
2376
2585
  onClick={() => setPinnedVendor(null)}
2377
2586
  style={{
@@ -2381,12 +2590,27 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2381
2590
  cursor: 'pointer',
2382
2591
  }}
2383
2592
  >
2384
- <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>
2593
+ {/* R412: see status pill above — filter value fw=600 data tier. */}
2594
+ <span><span className="hidden sm:inline opacity-70 transition-opacity duration-200 group-hover:opacity-100" data-filter-prefix>filter: </span><span className="font-semibold" data-filter-value>{pinnedVendor}</span><span className="opacity-70 tabular-nums transition-opacity duration-200 group-hover:opacity-100" data-filter-pill-count> · {matchCount}</span></span>
2385
2595
  <button
2386
2596
  type="button"
2387
2597
  aria-label={`Clear vendor filter ${pinnedVendor}`}
2388
2598
  onClick={(e) => { e.stopPropagation(); setPinnedVendor(null); }}
2389
- className="ml-0.5 leading-none hover:opacity-70"
2599
+ /* Round 356 / Loop: filter pin pill × buttons gain
2600
+ hover:scale-110 (Tailwind 4 modern CSS `scale` property,
2601
+ not legacy transform). Sibling polish to R354 vendor
2602
+ letter glyph + R350/R352/R353 chrome icon hover-scales.
2603
+ Pre-R356 the × had only hover:opacity-70 — the target
2604
+ dimmed under cursor but didn't lift. R356 adds a 10 %
2605
+ scale on hover so the click-target reads as "press me"
2606
+ alongside the dim. transform-gpu hint promotes the
2607
+ button to its own compositor layer for crisper edges
2608
+ during the scale tween. transition-transform duration-
2609
+ 200 matches the chrome icon hover-scale timing family.
2610
+ inline-block is default for <button> so no display
2611
+ tweak needed. replace_all covers all 4 filter pin
2612
+ pills (status / group / vendor / edge) at once. */
2613
+ className="ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 transform-gpu"
2390
2614
  style={{ background: 'transparent', color: 'inherit', cursor: 'pointer', padding: 0 }}
2391
2615
  >×</button>
2392
2616
  </span>
@@ -2419,7 +2643,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2419
2643
  data-filter-match-count={link.count}
2420
2644
  data-filter-match-aliases={`${link.from},${link.to}`}
2421
2645
  data-active-filter-edge-hot={isHot ? 'true' : 'false'}
2422
- 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"
2646
+ className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md font-mono font-medium text-xs border anet-fade-in anet-topo-chip-focus transition-transform duration-200 ease-out hover:-translate-y-px transform-gpu" data-topo-filter-pill-hover-lift="true"
2423
2647
  title={`${link.from} → ${link.to} (${link.count} msg${link.count === 1 ? '' : 's'}${isHot ? ', hot lane · ≥ 10' : ''}) — click to clear`}
2424
2648
  onClick={() => setPinnedEdgeKey(null)}
2425
2649
  style={{
@@ -2429,9 +2653,11 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2429
2653
  cursor: 'pointer',
2430
2654
  }}
2431
2655
  >
2656
+ {/* R412: filter pin pill value (edge variant) picks up fw=600.
2657
+ Sibling treatment to the status/group/vendor pills above. */}
2432
2658
  <span>
2433
2659
  <span className="hidden sm:inline opacity-70" data-filter-prefix>filter: </span>
2434
- {link.from}→{link.to}
2660
+ <span className="font-semibold" data-filter-value>{link.from}→{link.to}</span>
2435
2661
  {/* Round 323 / Loop: edge filter pill count digit picks
2436
2662
  up tabular-nums (Tailwind class on both cold +
2437
2663
  hot branches). Sibling treatment to the status /
@@ -2463,7 +2689,21 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2463
2689
  type="button"
2464
2690
  aria-label={`Clear edge filter ${link.from} → ${link.to}`}
2465
2691
  onClick={(e) => { e.stopPropagation(); setPinnedEdgeKey(null); }}
2466
- className="ml-0.5 leading-none hover:opacity-70"
2692
+ /* Round 356 / Loop: filter pin pill × buttons gain
2693
+ hover:scale-110 (Tailwind 4 modern CSS `scale` property,
2694
+ not legacy transform). Sibling polish to R354 vendor
2695
+ letter glyph + R350/R352/R353 chrome icon hover-scales.
2696
+ Pre-R356 the × had only hover:opacity-70 — the target
2697
+ dimmed under cursor but didn't lift. R356 adds a 10 %
2698
+ scale on hover so the click-target reads as "press me"
2699
+ alongside the dim. transform-gpu hint promotes the
2700
+ button to its own compositor layer for crisper edges
2701
+ during the scale tween. transition-transform duration-
2702
+ 200 matches the chrome icon hover-scale timing family.
2703
+ inline-block is default for <button> so no display
2704
+ tweak needed. replace_all covers all 4 filter pin
2705
+ pills (status / group / vendor / edge) at once. */
2706
+ className="ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 transform-gpu"
2467
2707
  style={{ background: 'transparent', color: 'inherit', cursor: 'pointer', padding: 0 }}
2468
2708
  >×</button>
2469
2709
  </span>
@@ -2728,9 +2968,35 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2728
2968
  vendor letter chips ('A:N', 'O:N', '书:N',
2729
2969
  '?:N'). They display vendor-distribution
2730
2970
  data; same tier as the sibling data chips. */
2731
- className="tabular-nums font-medium inline-flex items-baseline gap-0.5 px-1 rounded anet-topo-chip-focus"
2971
+ /* Round 401 / Loop: vendor letter chip closes the
2972
+ hover-lift gesture family at its last unaddressed
2973
+ interactive HTML surface. R397/R398/R399 lifted
2974
+ filter pin pills + chip-row chips (working /
2975
+ online / active-links); R400 lifted standalone
2976
+ chrome buttons (reset / fullscreen). The vendor
2977
+ letter chips (A:N / O:N / 书:N / ?:N) are
2978
+ sibling interactive chips in the same chip-row
2979
+ — clickable to toggle the vendor filter pin —
2980
+ but were not yet on the hover-lift family.
2981
+ R401 closes the gap with hover:-translate-y-px
2982
+ + transition-transform + transform-gpu added
2983
+ to the className. The inline transition list
2984
+ (box-shadow + background-color) keeps eaching
2985
+ independently — different property axes compose
2986
+ cleanly. Existing R354 glyph scale-1.1 (inner
2987
+ span) + R202 chip bg color-mix + R180 pin-mirror
2988
+ box-shadow + R354 glyph hover transform all
2989
+ preserved. data-vendor-letter-hover-lift attr
2990
+ surfaces the lift for tests. */
2991
+ // R417: `group` parent enables the count suffix to
2992
+ // brighten on chip hover via group-hover:opacity-100
2993
+ // — sibling to R355 filter-pill prefix/suffix + R414
2994
+ // chip-row unit brighten. Closes the inner-span
2995
+ // hover-brighten family at the vendor chip surface.
2996
+ className="group tabular-nums font-medium inline-flex items-baseline gap-0.5 px-1 rounded anet-topo-chip-focus transition-transform duration-200 ease-out transform-gpu hover:-translate-y-px"
2732
2997
  data-vendor-letter={v.initial}
2733
2998
  data-vendor-letter-count={v.count}
2999
+ data-vendor-letter-hover-lift="true"
2734
3000
  data-vendor-pinned={isPinned ? 'true' : 'false'}
2735
3001
  data-vendor-hovered={hoveredVendor === v.initial ? 'true' : 'false'}
2736
3002
  data-vendor-aliases={aliases.join(',')}
@@ -2774,7 +3040,64 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2774
3040
  }
2775
3041
  }}
2776
3042
  >
2777
- <span style={{ color: v.color }}>{v.initial}</span>
3043
+ {/* Round 354 / Loop: vendor letter glyph scales
3044
+ 1.0 → 1.1 on hover. R88 already dims OTHER
3045
+ vendors on hover via canvas-wide opacity
3046
+ masking; R202 added a chip-level bg tint
3047
+ (color-mix 12 % alpha) so the chip itself
3048
+ responds. R354 closes the trio with a glyph-
3049
+ level lift: the focused vendor LETTER actively
3050
+ rises (transform scale) rather than the chip
3051
+ merely changing colour. Three layers of positive
3052
+ feedback on the hovered vendor + canvas-wide
3053
+ negative feedback on the others — a clean
3054
+ figure/ground separation.
3055
+
3056
+ display: inline-block is required for transform
3057
+ to apply (inline elements ignore transform).
3058
+ transformOrigin: 'center' so the glyph pivots
3059
+ around its centre instead of arcing from the
3060
+ baseline anchor. transition rides the existing
3061
+ Tailwind 4 transform/scale list (no new
3062
+ property — Tailwind already lists transform in
3063
+ the default transition-property set). 200ms
3064
+ matches the R202 chip bg-tint timing so the
3065
+ glyph lift and chip background ease in concert. */}
3066
+ {/* Round 369 / Loop: vendor letter glyph picks up
3067
+ fontWeight 600 (font-semibold). The glyph is the
3068
+ vendor identifier — the DATA the operator scans
3069
+ in this chip (A / O / 书 / C / G / ?). R333 set
3070
+ the count suffix `:N` to text-gray-400 + tabular-
3071
+ nums and (via parent inheritance) fw 500. Pre-
3072
+ R369 the LETTER also inherited fw 500 from the
3073
+ chip's font-medium — letter and count read at
3074
+ the same weight, contradicting the data-vs-label
3075
+ hierarchy the rest of the chip-row already speaks.
3076
+ R369 lifts the letter to fw 600 so the chip now
3077
+ reads as the same two-tier pattern R362 closed
3078
+ on the working / online / active-links chips:
3079
+ chip digit/letter fw 600 (data)
3080
+ chip unit/count fw 500 (label)
3081
+ Sibling treatment to R362 — extends the R333-R341
3082
+ chip-internal-hierarchy arc to the vendor-letter
3083
+ chip surface (9th surface family). R354 transform-
3084
+ scale-on-hover + R88 canvas-dim-others + R202
3085
+ chip bg color-mix all preserved on the same span.
3086
+ data-vendor-letter-glyph-font-weight attr exposes
3087
+ the value for tests. */}
3088
+ <span
3089
+ data-vendor-letter-glyph={v.initial}
3090
+ data-vendor-letter-glyph-hover={hoveredVendor === v.initial ? 'true' : 'false'}
3091
+ data-vendor-letter-glyph-font-weight="600"
3092
+ style={{
3093
+ color: v.color,
3094
+ display: 'inline-block',
3095
+ fontWeight: 600,
3096
+ transform: hoveredVendor === v.initial ? 'scale(1.1)' : 'scale(1)',
3097
+ transformOrigin: 'center',
3098
+ transition: 'transform 200ms ease-out',
3099
+ }}
3100
+ >{v.initial}</span>
2778
3101
  {/* Round 333 / Loop: vendor count suffix `:{N}` joins
2779
3102
  the R317 subordinate-text-lift family (gray-500 →
2780
3103
  gray-400) plus picks up tabular-nums for digit
@@ -2787,8 +3110,17 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2787
3110
  reads it as "deliberate subordinate metadata"
2788
3111
  rather than near-invisible chrome. data-vendor-
2789
3112
  letter-count exposes the span for tests. */}
3113
+ {/* R417: count suffix opacity-70 + group-hover:
3114
+ opacity-100 brightens on chip hover. Inner-span
3115
+ hover-brighten family (3rd anchor) — sibling to
3116
+ R355 filter pill prefix/suffix and R414 chip-row
3117
+ unit. Effective shade at rest: text-gray-400 ×
3118
+ 70 % alpha; on hover: full gray-400. The label-
3119
+ tier-vs-glyph differentiation persists on hover
3120
+ since the glyph (R369 fw=600) stays at full
3121
+ opacity. R333 :{count} format preserved. */}
2790
3122
  <span
2791
- className="text-gray-400 tabular-nums"
3123
+ className="text-gray-400 tabular-nums opacity-70 transition-opacity duration-200 group-hover:opacity-100"
2792
3124
  data-vendor-letter-count-suffix
2793
3125
  >:{v.count}</span>
2794
3126
  </span>
@@ -2876,11 +3208,36 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2876
3208
  (third chip in the row — matches working + online
2877
3209
  chip treatment so all three digits in the chip row
2878
3210
  stay width-stable across counter crossings). */
2879
- className={`tabular-nums font-medium hidden sm:inline px-2.5 py-1 rounded-md border anet-topo-chip-focus ${
3211
+ /* Round 399 / Loop: active-links chip closes the 3-chip
3212
+ chip-row by extending R398's hover translateY(-1px)
3213
+ lift onto the third (rightmost) chip. The R398 family
3214
+ already covered working + online chips on the
3215
+ clickable variant; R399 adds the same gate (isInter-
3216
+ active = flowLinks.length > 0) so empty active-links
3217
+ stays planted at R206's opacity-50 receded paint.
3218
+ transition-transform + ease-out + transform-gpu join
3219
+ the inline transition list (different property axes
3220
+ compose cleanly: inline handles color/bg/border/
3221
+ opacity, className handles transform).
3222
+ Gesture-vocabulary table (post-R399 — now complete
3223
+ across the chip-row):
3224
+ working chip -1 px (R398)
3225
+ online chip -1 px (R398)
3226
+ active-links chip -1 px (R399, this round)
3227
+ filter pin pills -1 px (R397)
3228
+ recent-signal row -1 px (R143)
3229
+ legend row -1 px (R144)
3230
+ Every interactive chip in TopoGraph lifts on hover.
3231
+ data-chip-hover-lift attr exposes the lift surface
3232
+ state ('true' clickable, 'false' empty) for tests. */
3233
+ // R414: `group` parent + inner unit span group-hover-brighten — see working chip above.
3234
+ className={`group tabular-nums font-medium hidden sm:inline px-2.5 py-1 rounded-md border anet-topo-chip-focus transition-transform duration-200 ease-out transform-gpu ${
2880
3235
  isInteractive
2881
- ? 'bg-gray-500/10 text-gray-400 border-gray-500/20 hover:bg-cyan-500/10 hover:text-cyan-200 hover:border-cyan-500/30'
3236
+ ? 'bg-gray-500/10 text-gray-400 border-gray-500/20 hover:bg-cyan-500/10 hover:text-cyan-200 hover:border-cyan-500/30 hover:-translate-y-px'
2882
3237
  : 'bg-gray-500/10 text-gray-400 border-gray-500/20'
2883
3238
  }`}
3239
+ data-chip-hover-lift={isInteractive ? 'true' : 'false'}
3240
+ data-chip-group-hover-brighten="true"
2884
3241
  data-active-links-chip
2885
3242
  data-active-links-flow-count={flowLinks.length}
2886
3243
  data-active-links-clickable={isInteractive ? 'true' : 'false'}
@@ -2908,7 +3265,8 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2908
3265
  the 5th chip surface in the R333/R335/R336/R337
2909
3266
  chip-internal-hierarchy arc. data-active-links-
2910
3267
  chip-unit exposes the unit span for tests. */}
2911
- {flowLinks.length}<span className="opacity-70" data-active-links-chip-unit> active link{flowLinks.length === 1 ? '' : 's'}</span>
3268
+ {/* R362 sibling — active-links chip digit gains font-semibold. */}
3269
+ <span className="font-semibold transition-[font-weight] duration-200 group-hover:font-bold" data-active-links-chip-digit>{flowLinks.length}</span><span className="opacity-70 transition-opacity duration-200 group-hover:opacity-100" data-active-links-chip-unit> active link{flowLinks.length === 1 ? '' : 's'}</span>
2912
3270
  {rel ? (() => {
2913
3271
  // Round 161 / Loop: extend R160's recency-pip
2914
3272
  // vocabulary up one scope — from per-flow row to
@@ -2933,8 +3291,8 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2933
3291
  const alpha = ageSec <= 30
2934
3292
  ? 1
2935
3293
  : ageSec <= 300
2936
- ? 1 - ((ageSec - 30) / 270) * 0.75
2937
- : 0.25;
3294
+ ? 1 - ((ageSec - 30) / 270) * 0.70 /* R358: floor 0.25 → 0.30 lift across 3 freshness scopes */
3295
+ : 0.30; /* R358: stale floor lifted 0.25 → 0.30 — 20% legibility bump while preserving fresh/stale ratio */
2938
3296
  // Cyan dark / teal light to match palette legendAccent.
2939
3297
  const dotColor = isLight
2940
3298
  ? `rgba(13, 148, 136, ${alpha.toFixed(2)})`
@@ -2951,7 +3309,28 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
2951
3309
  // color: dotColor — the lift only affects the trailing
2952
3310
  // literal "last {rel}" text.
2953
3311
  return (
2954
- <span className="text-gray-400">
3312
+ // Round 357 / Loop: active-links chip freshness
3313
+ // suffix wrapper picks up `tabular-nums` for digit
3314
+ // width-lock. Pre-R357 the literal "last {rel}"
3315
+ // text (e.g. "last 5s ago", "last 10s ago",
3316
+ // "last 1m ago") had natural-figure digits — the
3317
+ // freshness ticker updates every second, so the
3318
+ // 9→10 boundary on the seconds counter and the
3319
+ // 59→60s → 1m flip both jittered ~1-2 px of glyph
3320
+ // width which propagated through the chip-row's
3321
+ // inline-flex layout, nudging the freshness DOT
3322
+ // and the chip's left edge. Tabular-nums on the
3323
+ // wrapper applies to all descendant digits only
3324
+ // (letters render at natural widths) so the
3325
+ // ticker stays planted across every count cross.
3326
+ // Joins the R224-R232 info-density tabular-nums
3327
+ // sweep at the chip-row freshness scope. Pure
3328
+ // paint-level change, no geometry shift on rest.
3329
+ // The R342 text-gray-400 lift + R161 dot freshness
3330
+ // alpha ramp + R317 subordinate-text-lift family
3331
+ // all preserved. data-active-links-freshness-
3332
+ // wrapper attr exposes the wrapper for tests.
3333
+ <span className="text-gray-400 tabular-nums" data-active-links-freshness-wrapper>
2955
3334
  <span
2956
3335
  data-active-links-freshness-dot
2957
3336
  data-active-links-freshness-alpha={alpha.toFixed(2)}
@@ -3615,19 +3994,153 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3615
3994
  active surfaces the activity state for test probes
3616
3995
  (active spokes don't carry the bucket/dur attrs so
3617
3996
  they need their own data anchor). */
3997
+ // Round 382 / Loop: hub-spoke path picks up
3998
+ // strokeLinecap='round'. Sibling polish to R378 flow-
3999
+ // rail dashes + R380 group box dashes — three dashed-
4000
+ // stroke surfaces now share 'round' linecap:
4001
+ // R378 flow-rail '2 12' -> soft 3-px pills
4002
+ // R380 group box '6 6' -> soft 7.5-px pills
4003
+ // R382 hub spoke '6 14' -> soft 7-px pills (this round)
4004
+ // For idle spokes (dashed at sw=1), each 6-px dash gains
4005
+ // 0.5-px round caps and reads as a soft pill instead of
4006
+ // a sharp 6 x 1 rectangle. Active spokes (solid, no
4007
+ // dasharray) have caps mostly hidden by the hub center +
4008
+ // node radius. Geometry-safe; paint-only. R51 sentinel
4009
+ // strokeWidth 1.5/3 untouched (idle=1, active=2). data-
4010
+ // topo-hub-spoke-linecap attr exposes the value for tests.
4011
+ // Round 419 / Loop: hub-spoke idle opacity 0.45 → 0.50.
4012
+ // Stale-state legibility lift family 9th anchor — pairs
4013
+ // with R391 (active 0.7 → 0.8) and R415 (active sw 2 →
4014
+ // 2.25) so the same spoke path is now polished on BOTH
4015
+ // active AND idle tiers. Pre-R419 idle spokes painted
4016
+ // at α=0.45 with R46 anet-topo-spoke-flow dashed
4017
+ // animation; the dashed pulses sat at the "background
4018
+ // chatter" floor — visible but understated. R419
4019
+ // lifts to 0.50 so idle spokes read more confidently
4020
+ // while the active/idle contrast ratio stays clear
4021
+ // (0.8/0.50 = 1.6× vs prior 0.8/0.45 = 1.78×; still
4022
+ // a sharp two-tier distinction).
4023
+ // Stale-state legibility lift family (9 anchors now):
4024
+ // R317 subordinate-text gray-500 → gray-400
4025
+ // R358 freshness floor 0.25 → 0.30
4026
+ // R372 minimap offline-dot 0.5 → 0.6
4027
+ // R404 hub-halo cyber trough 0.08 → 0.10
4028
+ // R405 hub-halo light trough 0.32 → 0.34
4029
+ // R406 edge freshness floor 0.35 → 0.40
4030
+ // R407 node halo offline opacity (cyber + light)
4031
+ // R413 active-node pulse trough (cyber + light)
4032
+ // R419 hub-spoke idle opacity 0.45 → 0.50 (this round)
4033
+ // data-topo-hub-spoke-opacity attr (R391) updates to
4034
+ // surface the resolved per-state value.
4035
+ //
4036
+ // Round 415 / Loop: hub-spoke active strokeWidth 2 → 2.25.
4037
+ // Pairs with R391 (active opacity 0.7 → 0.8) so the same
4038
+ // active-state path lifts BOTH stroke weight AND opacity
4039
+ // in concert. Pre-R415 active strokes sat at sw=2 — clear
4040
+ // step over idle sw=1, but a touch lighter than the
4041
+ // weight family's other "active" indicators (R385 hub
4042
+ // hover-ring sw=1.75 / R402 legend pin-ring sw=1.75 /
4043
+ // R367 edge-badge rest sw=1.25). R415 bumps to 2.25 so
4044
+ // the active spoke reads with proportional weight to its
4045
+ // role — the line connecting the focal point to the
4046
+ // active node deserves the heaviest active stroke in the
4047
+ // family (after pin/hot edge-badge sw=2). Stays clear of
4048
+ // R51 sentinels (1.5 / 3) at 2.25.
4049
+ // Visual-weight bump family (14 anchors now):
4050
+ // R287 minimap viewport stroke 1 → 1.5
4051
+ // R295 legend swatch radius 5.5 → 6
4052
+ // R359 recent-row pip radius 1.6 → 1.8
4053
+ // R360 hub digit fontSize 11 → 12
4054
+ // R361 edge-badge digit fontSize 10 → 11
4055
+ // R365 hub-highlight radius 5 → 5.5
4056
+ // R367 edge-badge rest stroke 1 → 1.25
4057
+ // R374 pressure-bar height 1.5 → 2
4058
+ // R383 recent-row pip radius 1.8 → 2.0
4059
+ // R384 minimap online dot 1.7 → 1.9
4060
+ // R385 hub hover-ring stroke 1.5 → 1.75
4061
+ // R402 legend pin-ring stroke 1.5 → 1.75
4062
+ // R408 hub-halo radius 18 → 20
4063
+ // R415 hub-spoke active stroke 2 → 2.25 (this round)
4064
+ // R382 strokeLinecap='round' + R391 opacity 0.45/0.8 +
4065
+ // R51-safe idle sw=1 all preserved. 250ms transition
4066
+ // list already covers stroke-width — the new tier eases
4067
+ // naturally. data-topo-hub-spoke-stroke-width-active
4068
+ // attr surfaces the active value for tests.
4069
+ //
4070
+ // Round 391 / Loop: hub-spoke active opacity 0.7 → 0.8.
4071
+ // Pre-R391 active spokes (the spoke connecting the hub
4072
+ // to the currently-active alias — hovered or pinned)
4073
+ // lifted opacity from rest 0.45 to active 0.7 — a clear
4074
+ // step but slightly understated against the canvas
4075
+ // chrome. R391 lifts active to 0.8 so the "this spoke
4076
+ // connects to your active node" signal reads with
4077
+ // matching weight to the R370 hub hover-ring opacity
4078
+ // (0.7 → 0.8 cyber) — paired canvas signals now share
4079
+ // the same active-state alpha (0.8) so when a user
4080
+ // hovers a node, both the spoke and the hub-ring lift
4081
+ // to identical opacity. Rest 0.45 invariant preserved.
4082
+ // Theme-consistency / canvas-presence polish family
4083
+ // (6th anchor):
4084
+ // R370 hub hover-ring opacity 0.7 → 0.8 cyber
4085
+ // R371 edge-badge rest opacity 0.82 → 0.85 cyber
4086
+ // R372 minimap offline-dot opacity 0.5 → 0.6
4087
+ // R386 hub-highlight idle opacity 0.9 → 0.95
4088
+ // R387 hover-detail panel opacity 0.94 → 0.97 cyber
4089
+ // R391 hub-spoke active opacity 0.7 → 0.8 (this round)
4090
+ // Idle path (45% alpha + dashed flow animation) entirely
4091
+ // untouched — R391 is an active-state-only lift.
4092
+ // data-topo-hub-spoke-opacity attr exposes the resolved
4093
+ // value for tests. R382 strokeLinecap='round' + R51
4094
+ // sentinel-safe sw (1 idle / 2 active) preserved.
4095
+ /* Round 430 / Loop: hub-spoke opacity hover lift on
4096
+ hoveredAlias === session.alias. Adds a "this node's
4097
+ spoke" affordance to the node-hover gesture — in a
4098
+ dense ring layout the spokes are visually quiet
4099
+ (idle α=0.50 dashed, active α=0.80 solid) so hovering
4100
+ a node didn't telegraph which line connects to it.
4101
+ R430 lifts the matched spoke's opacity:
4102
+ idle 0.50 → 0.70 (hover-α=0.70, +0.20)
4103
+ active 0.80 → 0.95 (hover-α=0.95, +0.15)
4104
+ The +0.15-to-0.20 lift keeps the active/idle two-tier
4105
+ distinction (0.95 vs 0.70 still a clear gap) while
4106
+ making the hovered-node's spoke visibly brighter than
4107
+ every other spoke at its own activity tier. R241
4108
+ transition list already covers opacity 250ms so the
4109
+ lift eases for free. Sibling to R429 label-card body
4110
+ solidity lift — both surface a single-node-focused
4111
+ attention cue with the same easing cadence.
4112
+ Stacks with the 6-layer node hover cue stack at the
4113
+ inter-node-link scope:
4114
+ R26 group translateY -2px (per-node)
4115
+ R217 stroke tint legendAccent (per-node card)
4116
+ R142 drop-shadow boost (per-node card)
4117
+ R427 alias letter-spacing (per-node text)
4118
+ R428 sub-text letter-spacing (per-node text)
4119
+ R429 body opacity 0.94 → 1.0 (per-node card)
4120
+ R430 spoke opacity α+ (this round) (link to hub)
4121
+ data-topo-hub-spoke-hovered exposes the gate. */
4122
+ const isHoveredSpoke = !reducedMotion && hoveredAlias === session.alias;
4123
+ const spokeOpacity = isActiveSpoke
4124
+ ? (isHoveredSpoke ? 0.95 : 0.80)
4125
+ : (isHoveredSpoke ? 0.70 : 0.50);
3618
4126
  return (
3619
4127
  <path
3620
4128
  key={`hub-${session.alias}`}
3621
4129
  d={path}
3622
4130
  fill="none"
3623
4131
  stroke={isActiveSpoke ? pal.spokeStroke.active : pal.spokeStroke.idle}
3624
- strokeWidth={isActiveSpoke ? 2 : 1}
4132
+ strokeWidth={isActiveSpoke ? 2.25 : 1}
3625
4133
  strokeDasharray={isActiveSpoke ? 'none' : '6 14'}
3626
- opacity={isActiveSpoke ? 0.7 : 0.45}
4134
+ strokeLinecap="round"
4135
+ opacity={spokeOpacity}
3627
4136
  className={isActiveSpoke ? undefined : 'anet-topo-spoke-flow'}
3628
4137
  data-topo-spoke-bucket={isActiveSpoke ? undefined : busy}
3629
4138
  data-topo-spoke-dur={isActiveSpoke ? undefined : spokeDur}
3630
4139
  data-topo-hub-spoke-active={isActiveSpoke ? 'true' : 'false'}
4140
+ data-topo-hub-spoke-hovered={isHoveredSpoke ? 'true' : 'false'}
4141
+ data-topo-hub-spoke-opacity={spokeOpacity}
4142
+ data-topo-hub-spoke-stroke-width-active="2.25"
4143
+ data-topo-hub-spoke-linecap="round"
3631
4144
  style={{
3632
4145
  transition: 'stroke 250ms ease-out, stroke-width 250ms ease-out, opacity 250ms ease-out',
3633
4146
  ...(isActiveSpoke ? {} : {
@@ -3719,7 +4232,34 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3719
4232
  stroke={(isPinned || isHovered) ? pal.legendAccent : pal.ringStroke}
3720
4233
  strokeWidth={isPinned ? 3 : isHovered ? 2 : 1.5}
3721
4234
  strokeDasharray={(isPinned || isHovered) ? 'none' : '6 6'}
4235
+ /* Round 380 / Loop: cluster box stroke gets round
4236
+ linecap + round linejoin. Sibling SVG stroke-
4237
+ softening polish to R378 flow-rail linecap + R379
4238
+ minimap viewport linejoin — extends the family to
4239
+ the group cluster boundary box (grid layout only):
4240
+ R288 chrome icons strokeLinecap='round'
4241
+ R378 flow-rail dashes strokeLinecap='round'
4242
+ R380 group box dashes strokeLinecap='round' (this round)
4243
+ R379 viewport rect strokeLinejoin='round'
4244
+ R380 group box corners strokeLinejoin='round' (this round)
4245
+ Linecap rounds the R85 '6 6' marching-ants dash
4246
+ pills at rest — each 6 px dash gains a ~0.75 px
4247
+ round cap (sw=1.5 idle), reading as soft pills
4248
+ instead of sharp 6 × 1.5 px rectangles. Linejoin
4249
+ rounds the 4 sharp 90° corners (any state — solid
4250
+ or dashed); at sw=1.5 the join arc is ~0.75 px,
4251
+ matching R379 viewport vocabulary. Geometry-safe:
4252
+ stroke-* properties only affect paint, not bbox.
4253
+ The R51 sentinel 1.5/3 strokeWidth values stay
4254
+ intact (the overlap probe is gated to g[data-
4255
+ node], so this cluster-internal rect is invisible
4256
+ to it anyway). data-group-box-linecap + -linejoin
4257
+ attrs expose the values for tests. */
4258
+ strokeLinecap="round"
4259
+ strokeLinejoin="round"
3722
4260
  data-group-box-pinned={isPinned ? 'true' : 'false'}
4261
+ data-group-box-linecap="round"
4262
+ data-group-box-linejoin="round"
3723
4263
  // R85: ambient "marching ants" drift on the perimeter
3724
4264
  // when this group has at least one working member, and
3725
4265
  // neither pin nor hover is active (those treatments
@@ -3919,12 +4459,33 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
3919
4459
  lives at a fixed dx=6 offset from the name, so a
3920
4460
  digit-width jitter at 9→10 used to shift the
3921
4461
  whole count visibly. Tabular locks it. */}
4462
+ {/* Round 366 / Loop: group label member-count tspan
4463
+ fontWeight 400 → 500. Sibling polish to R363
4464
+ recent-row alias text fw 400 → 500 + R364 legend-
4465
+ row label fw 400 → 500 — closes the per-row 'count
4466
+ is fw 500 against label-tier fw 700' pattern at
4467
+ the group-label scope (grid layout cluster mark).
4468
+ Hierarchy snapshot post-R366 across all 3 row
4469
+ surfaces:
4470
+ recent count(hot/cold) fw 700/600 (R320)
4471
+ recent alias fw 500 (R363)
4472
+ legend count fw 600 (R309)
4473
+ legend label fw 500 (R364)
4474
+ group name fw 700 (legacy)
4475
+ group count fw 500 (R366, this round)
4476
+ Monospace family + R225 tabular-nums lock digit
4477
+ width, so the fw bump is paint-only — bbox
4478
+ unchanged + overlap-test invariants hold. R229
4479
+ fill-inherit from parent label (hover-deepen-own-
4480
+ hue family) preserved. data-group-label-count-
4481
+ font-weight attr exposes the value for tests. */}
3922
4482
  <tspan
3923
4483
  dx="6"
3924
4484
  fontSize="11"
3925
- fontWeight="400"
4485
+ fontWeight="500"
3926
4486
  data-group-label-count={box.key}
3927
4487
  data-group-label-count-value={box.count}
4488
+ data-group-label-count-font-weight="500"
3928
4489
  style={{ fontVariantNumeric: 'tabular-nums' }}
3929
4490
  >· {box.count}</tspan>
3930
4491
  {/* Round 58 / Loop: status mix pip strip. Compact text-
@@ -4055,13 +4616,34 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4055
4616
  // synchronised per-edge animation set.
4056
4617
  const stagger = (index * 0.37) % duration;
4057
4618
  // Round 10 / Loop: freshness fade. An edge that fired ≤30s ago
4058
- // stays at full intensity; over 5 minutes it decays to ~35%.
4059
- // Surfaces "what's happening now" vs background chatter without
4060
- // hiding old flow entirely (some context still useful). `now`
4061
- // captured at useMemo-recompute time (every 5s message refresh)
4062
- // — accuracy is within the poll interval, plenty.
4619
+ // stays at full intensity; over 5 minutes it decays to a
4620
+ // floor. Surfaces "what's happening now" vs background
4621
+ // chatter without hiding old flow entirely (some context
4622
+ // still useful). `now` captured at useMemo-recompute time
4623
+ // (every 5s message refresh) — accuracy is within the poll
4624
+ // interval, plenty.
4625
+ //
4626
+ // Round 406 / Loop: edge freshness fade floor 0.35 → 0.40.
4627
+ // Stale-state legibility lift family (6th anchor) — pre-
4628
+ // R406 edges older than 5 minutes faded to α=0.35 (a 65 %
4629
+ // dim against full intensity). The decay rate is the same
4630
+ // 1 - ageMs/300s curve; only the FLOOR shifts. Sibling
4631
+ // treatment to:
4632
+ // R317 subordinate-text gray-500 → gray-400
4633
+ // R358 freshness ramp floor 0.25 → 0.30
4634
+ // R372 minimap offline-dot opacity 0.5 → 0.6
4635
+ // R404 hub-halo cyber trough 0.08 → 0.10
4636
+ // R405 hub-halo light trough 0.32 → 0.34
4637
+ // R406 edge freshness floor 0.35 → 0.40 (this round)
4638
+ // Edges past 5min now sit at 40% intensity instead of 35%
4639
+ // — they still recede against fresh edges but read
4640
+ // legibly enough to convey "this conversation existed".
4641
+ // ageMs threshold for the 5-minute decay unchanged; the
4642
+ // decay curve shape (linear) unchanged. The visual delta
4643
+ // is most pronounced on edges between 5-60 minutes old —
4644
+ // where the floor was binding pre-R406.
4063
4645
  const ageMs = link.last_at ? Math.max(0, Date.now() - Date.parse(link.last_at)) : 0;
4064
- const fresh = Math.max(0.35, 1 - ageMs / (5 * 60 * 1000));
4646
+ const fresh = Math.max(0.40, 1 - ageMs / (5 * 60 * 1000));
4065
4647
  // Round 16 arrow-tier binning — keep `topo-arrow` as the
4066
4648
  // medium tier id so the legend swatch picks it up unchanged.
4067
4649
  const arrowId = link.count <= 2 ? 'topo-arrow-s'
@@ -4213,20 +4795,56 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4213
4795
  online chips to splice in additional properties
4214
4796
  beside Tailwind's). data-edge-flow-rail attr
4215
4797
  surfaces the path for test introspection. */}
4798
+ {/* Round 381 / Loop: edge visible flow path picks up
4799
+ strokeLinecap='round'. Sibling polish to R378
4800
+ flow-rail dashed linecap — both flow-element paths
4801
+ (visible primary + dashed secondary rail) now share
4802
+ 'round' linecap vocabulary. The visible path runs
4803
+ source-node → dest-node as one continuous line, so
4804
+ the dest-end is covered by the markerEnd arrow and
4805
+ the source-end usually sits inside the source-node
4806
+ circle. At certain alignments (post-zoom, post-
4807
+ layout-switch transitions), the source-end may peek
4808
+ out by a fraction of a px past the node edge —
4809
+ round caps render that overshoot as a smooth half-
4810
+ disc instead of a sharp rectangle. Pure paint
4811
+ refinement, geometry-safe (bbox of the stroke
4812
+ unchanged at the join with the arrow marker).
4813
+ data-edge-visible-linecap attr exposes the value
4814
+ for tests. */}
4216
4815
  <path
4217
4816
  d={path}
4218
4817
  fill="none"
4219
4818
  stroke={pal.flowEdge}
4220
4819
  strokeWidth={renderWidth}
4820
+ strokeLinecap="round"
4221
4821
  opacity={Math.min(1, (isLight ? 0.22 : 0.28) * fresh * edgeOpacityMul)}
4222
4822
  filter={isLight ? undefined : 'url(#topo-glow)'}
4223
4823
  markerEnd={`url(#${arrowId})`}
4224
4824
  data-edge-visible={link.key}
4825
+ data-edge-visible-linecap="round"
4225
4826
  style={{
4226
4827
  pointerEvents: 'none',
4227
4828
  transition: 'opacity 300ms ease-out, stroke-width 300ms ease-out, stroke 300ms ease-out',
4228
4829
  }}
4229
4830
  />
4831
+ {/* Round 378 / Loop: edge flow-path dashed-rail picks
4832
+ up strokeLinecap='round'. Pre-R378 the rail
4833
+ rendered '2 12' dashes as sharp 1×2 rectangles
4834
+ against the canvas backdrop; default 'butt' caps
4835
+ leave dash ends square. R378 rounds each cap so
4836
+ the dashes read as soft 3-px pills (1 px stroke +
4837
+ 0.5 px round cap each end). The flow-rail is the
4838
+ secondary 'invisible-spine' line that gives the
4839
+ R57 spoke flow a directional rail to slide along
4840
+ — rounding the dashes softens its presence
4841
+ against the primary visible flow path (R245 has
4842
+ no strokeLinecap so it inherits 'butt' on a
4843
+ continuous line, irrelevant). Geometry-safe:
4844
+ round caps only widen the visible dash; the
4845
+ bbox of the path is unchanged so overlap-test
4846
+ invariants hold. data-edge-flow-rail-linecap
4847
+ attr exposes the value for tests. */}
4230
4848
  <path
4231
4849
  id={`flow-path-${index}`}
4232
4850
  d={path}
@@ -4234,8 +4852,10 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4234
4852
  stroke={pal.flowPath}
4235
4853
  strokeWidth="1"
4236
4854
  strokeDasharray="2 12"
4855
+ strokeLinecap="round"
4237
4856
  opacity={Math.min(1, (isLight ? 0.4 : 0.75) * fresh * edgeOpacityMul)}
4238
4857
  data-edge-flow-rail={link.key}
4858
+ data-edge-flow-rail-linecap="round"
4239
4859
  style={{ transition: 'opacity 300ms ease-out, stroke 300ms ease-out' }}
4240
4860
  />
4241
4861
  {!reducedMotion && (
@@ -4251,12 +4871,23 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4251
4871
  multiple). Edge order is stable (sorted by recent
4252
4872
  activity), so the offsets feel calm rather than
4253
4873
  reshuffling each refresh. */
4874
+ /* Round 422 / Loop: edge flow particle radius 4 → 4.5.
4875
+ Visual-weight bump family (15th anchor) — particles
4876
+ riding along the edge animateMotion path get +0.5px
4877
+ radius lift, increasing visual area by ~27%
4878
+ (π·4.5² / π·4² = 1.27). Sibling magnitude to R383
4879
+ recent-row pip 1.8 → 2.0 (+25% area), R384 minimap
4880
+ online dot 1.7 → 1.9 (+25% area). R251 fill +
4881
+ R252 transitions + R103 phase-stagger animateMotion
4882
+ all preserved. data-edge-particle-radius attr
4883
+ exposes the value for tests. */
4254
4884
  <circle
4255
- r="4"
4885
+ r="4.5"
4256
4886
  fill={pal.flowParticle}
4257
4887
  filter={isLight ? undefined : 'url(#topo-glow)'}
4258
4888
  opacity={Math.min(1, fresh * edgeOpacityMul)}
4259
4889
  data-edge-particle={link.key}
4890
+ data-edge-particle-radius="4.5"
4260
4891
  /* Round 252 / Loop: particle picks up fill +
4261
4892
  opacity transition for theme-toggle smoothing.
4262
4893
  Pre-R252 pal.flowParticle (cyber #fef08a yellow
@@ -4600,14 +5231,137 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4600
5231
  R251 closes the per-edge surface theme-toggle
4601
5232
  smoothness — every theme-driven property on
4602
5233
  every edge element now eases under cyber↔light. */}
5234
+ {/* Round 367 / Loop: edge midpoint badge rest
5235
+ stroke-width 1 → 1.25. Sibling visual-weight
5236
+ bump family (7th canvas anchor now):
5237
+ R287 minimap viewport stroke 1 → 1.5
5238
+ R295 legend swatch base radius 5.5 → 6
5239
+ R359 recent-row pip base radius 1.6 → 1.8
5240
+ R360 hub digit fontSize 11 → 12
5241
+ R361 edge-badge digit fontSize 10 → 11
5242
+ R365 hub-highlight base radius 5 → 5.5
5243
+ R367 edge-badge rest stroke 1 → 1.25 (this round)
5244
+ Cold edge badges gain ~25 % stroke presence
5245
+ (1.25/1.0 = 1.25). Stays clear of the R51
5246
+ overlap-test sentinel values (1.5 / 3 reserved
5247
+ for node strokes — the test selector is gated
5248
+ to g[data-node] ancestors so this edge-internal
5249
+ circle is invisible to that probe anyway, but
5250
+ 1.25 is a safe non-sentinel value regardless).
5251
+ R188 transition list 'stroke-width 300ms ease-
5252
+ out' still smoothes the hot/pin flip — now
5253
+ 1.25 → 2 instead of 1 → 2, same ease pace.
5254
+ data-edge-badge-stroke-width-rest exposes the
5255
+ new baseline for tests. */}
5256
+ {/* Round 371 / Loop: edge-badge cyber opacity 0.82
5257
+ → 0.85. Sibling theme-consistency polish to R370
5258
+ hub hover-ring 0.7 → 0.8. R251 designed this
5259
+ badge with opacity 0.82 (cyber) / 0.95 (light)
5260
+ — 13 % delta. Cyber-theme dark bg needs more
5261
+ alpha to read as 'present'; R371 narrows the
5262
+ gap to 10 %, bringing the badge closer to light
5263
+ theme's 0.95 floor. Light stays at 0.95
5264
+ (already in the legibility band). data-edge-
5265
+ badge-opacity attr exposes the resolved value.
5266
+ Theme-consistency polish family:
5267
+ R246/R247 panel transition family
5268
+ R251 edge badge fill/opacity baseline
5269
+ R370 hub hover-ring cyber 0.7 → 0.8
5270
+ R371 edge badge cyber 0.82 → 0.85 (this round)
5271
+ R164 r=9/10.5 hover-lift + R188/R251 transition
5272
+ list + R367 strokeWidth=1.25 cold rest preserved. */}
5273
+ {/* Round 394 / Loop: edge-badge gains a hover
5274
+ strokeWidth tier (1.5) between cold rest
5275
+ (R367 1.25) and pin/hot (2). Pre-R394 the
5276
+ badge lifted only its radius on hover (R164
5277
+ 9 → 10.5); the stroke stayed at cold rest
5278
+ 1.25 unless pin/hot kicked in, so a plain
5279
+ hover felt half-lifted — geometry expanded
5280
+ while the contour stayed thin. R394 adds
5281
+ strokeWidth=1.5 on isHoveredEdge so hover
5282
+ now lifts both r AND stroke in concert —
5283
+ same pattern R385 used for the hub hover-
5284
+ ring (1.5 → 1.75) where the ring's three
5285
+ hover axes (r grow / opacity fade-in /
5286
+ stroke thicken) all rise together.
5287
+ Three-tier stroke hierarchy now:
5288
+ cold rest 1.25 (R367)
5289
+ hovered 1.5 (R394 — this round)
5290
+ pinned / hot 2.0 (R188)
5291
+ R51 sentinel concern: strokeWidth=1.5 is
5292
+ one of the two sentinels reserved for node
5293
+ detection, but the R51 selector is gated
5294
+ to `g[data-node]` ancestors so this edge-
5295
+ internal circle is invisible to the probe
5296
+ (same lesson R177 hub hover-ring + R367
5297
+ cold rest documented). 300ms strokeWidth
5298
+ transition already in the style list eases
5299
+ the new tier naturally. data-edge-badge-
5300
+ stroke-width-hover attr exposes the hover
5301
+ value for tests. */}
5302
+ {/* Round 395 / Loop: edge-badge gains a third
5303
+ hover axis — opacity 0.85 (cyber) / 0.95
5304
+ (light) → 1.0 on isHoveredEdge. Pre-R395
5305
+ hovering thickened the stroke (R394 1.25 →
5306
+ 1.5) and grew the radius (R164 9 → 10.5)
5307
+ but the badge's translucency stayed put at
5308
+ R371's rest alpha (cyber 0.85 / light 0.95).
5309
+ R395 lifts hover to a clean 1.0 — fully
5310
+ opaque — so the hovered badge reads as
5311
+ "in focus" against the dim siblings.
5312
+ Three-axis hover-lift parity now complete:
5313
+ hub hover-ring (R177/R370/R385):
5314
+ r 14 → 17, opacity 0 → 0.8 cyber, sw 1.5 → 1.75
5315
+ edge badge (R164/R394/R395):
5316
+ r 9 → 10.5, sw 1.25 → 1.5, opacity → 1.0
5317
+ 200ms opacity transition (already in the
5318
+ style list) eases the new axis naturally.
5319
+ R371 rest opacity (0.85 cyber / 0.95 light)
5320
+ preserved as the resting alpha — R395
5321
+ adds an isHoveredEdge override on top.
5322
+ data-edge-badge-opacity-hover attr exposes
5323
+ the hover value for tests. */}
5324
+ {/* Round 396 / Loop: extend the R395 opacity → 1.0
5325
+ lift to the pinned state. Pre-R396 the badge
5326
+ shared `r=10.5` on both hover AND pin (R164
5327
+ unified-lift) but R395's opacity lift fired
5328
+ ONLY on isHoveredEdge — pinned badges stayed
5329
+ at R371 rest alpha (cyber 0.85 / light 0.95).
5330
+ That left pin (sticky selection) reading
5331
+ softer than hover (transient preview), even
5332
+ though pin is the stronger commitment.
5333
+ R396 unifies hover + pin at opacity=1.0
5334
+ so the same data-edge-badge-lifted='true'
5335
+ surface uniformly carries full alpha. Pin
5336
+ stroke (R188 sw=2 + pal.legendHeadline color)
5337
+ continues to differentiate pin from hover —
5338
+ the opacity track now closes the lift parity.
5339
+ The new gate (isHoveredEdge || isPinned)
5340
+ mirrors the existing R164 r-lift gate, so
5341
+ the badge has a single "active state"
5342
+ signature across r + opacity.
5343
+ 200ms opacity transition (already in style
5344
+ list) eases pin/unpin naturally. R371 rest
5345
+ opacity preserved as the resting alpha.
5346
+ data-edge-badge-opacity-hover renamed
5347
+ semantically to -active (covers hover+pin)
5348
+ via the new -opacity-active attr; the
5349
+ legacy -opacity-hover attr kept for R395
5350
+ test compatibility. */}
4603
5351
  <circle
4604
5352
  cx={badgeX} cy={badgeY}
4605
5353
  r={isHoveredEdge || isPinned ? 10.5 : 9}
4606
5354
  fill={pal.legendBox.fill}
4607
5355
  stroke={isPinned ? pal.legendHeadline : isHot ? hotStroke : pal.flowEdge}
4608
- strokeWidth={isPinned ? 2 : isHot ? 2 : 1}
4609
- opacity={isLight ? 0.95 : 0.82}
5356
+ strokeWidth={isPinned ? 2 : isHot ? 2 : isHoveredEdge ? 1.5 : 1.25}
5357
+ opacity={(isHoveredEdge || isPinned) ? 1 : (isLight ? 0.95 : 0.85)}
4610
5358
  data-edge-badge-lifted={(isHoveredEdge || isPinned) ? 'true' : 'false'}
5359
+ data-edge-badge-stroke-width-rest="1.25"
5360
+ data-edge-badge-stroke-width-hover="1.5"
5361
+ data-edge-badge-opacity={(isHoveredEdge || isPinned) ? 1 : (isLight ? 0.95 : 0.85)}
5362
+ data-edge-badge-opacity-rest={isLight ? 0.95 : 0.85}
5363
+ data-edge-badge-opacity-hover="1"
5364
+ data-edge-badge-opacity-active="1"
4611
5365
  style={{ transition: 'r 180ms ease-out, stroke 300ms ease-out, stroke-width 300ms ease-out, fill 200ms ease-out, opacity 200ms ease-out' }}
4612
5366
  />
4613
5367
  {/* Round 224 / Loop: edge badge text gains the 4th
@@ -4646,16 +5400,61 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4646
5400
  x={badgeX} y={badgeY + 3}
4647
5401
  textAnchor="middle"
4648
5402
  fill={pal.legendHeadline}
4649
- fontSize="10"
5403
+ /* Round 361 / Loop: edge midpoint badge text
5404
+ fontSize 10 → 11. Sibling visual-weight bump
5405
+ to R360 hub digit 11 → 12. The badge digit
5406
+ is the per-edge equivalent of the hub digit
5407
+ — a high-information scalar (link.count) at
5408
+ a stable canvas position. Pre-R361 fontSize=
5409
+ 10 + R220 letter-spacing 0.4 + R224 tabular-
5410
+ nums made the digit READABLE but small
5411
+ against the r=9 / 18-px badge envelope;
5412
+ fontSize=11 nudges the glyph ~10 % bigger
5413
+ (bbox ~7×10 px from ~6×9 px) so the count
5414
+ reads more cleanly at glance — still well
5415
+ inside the r=9 idle circle and the r=10.5
5416
+ hover/pin lift (R164). y=badgeY+3 empirical
5417
+ vertical centring kept (1px drift at the
5418
+ bumped size is below the noise floor in
5419
+ the on-curve flow path).
5420
+ Visual-weight bump family:
5421
+ R287 minimap viewport stroke 1 → 1.5
5422
+ R295 legend swatch base radius 5.5 → 6
5423
+ R359 recent-row pip base radius 1.6 → 1.8
5424
+ R360 hub digit fontSize 11 → 12
5425
+ R361 edge-badge digit fontSize 10 → 11 (this round)
5426
+ data-edge-badge-text-font-size attr exposes
5427
+ the value for tests. R220 pin/hot letter-
5428
+ spacing tween + R224 tabular-nums + R188
5429
+ stroke-width pin/hot transitions all preserved. */
5430
+ fontSize="11"
4650
5431
  fontFamily="monospace"
4651
- fontWeight="700"
5432
+ /* R426 — edge-badge digit fontWeight 700 → 800 on
5433
+ (isPinned || isHot). 4th anchor on the "data
5434
+ tightens under attention" typographic-weight
5435
+ pattern:
5436
+ R416 chip-digit (chip-row hover trigger)
5437
+ R424 panel-digit (panel hover trigger)
5438
+ R425 hub-digit (hub hover trigger)
5439
+ R426 edge-badge-digit (pin/hot trigger) ← this
5440
+ The badge digit is the per-edge equivalent of
5441
+ the hub digit (R361 sibling fontSize bump
5442
+ reasoning). Stacks with R188 stroke-width pin/
5443
+ hot lift (1.25 → 1.5) + R220 letter-spacing pin/
5444
+ hot tween (0 → 0.4) for a 3-axis pin/hot signa-
5445
+ ture (edge structure + text spacing + text
5446
+ weight). The R408 transition is letter-spacing
5447
+ 300ms; R426 appends font-weight 300ms so the
5448
+ weight bump co-eases under the same cadence. */
5449
+ fontWeight={(isPinned || isHot) ? '800' : '700'}
4652
5450
  data-edge-badge-text={link.key}
4653
5451
  data-edge-badge-text-pin={(isPinned || isHot) ? 'true' : 'false'}
5452
+ data-edge-badge-text-font-size="11"
4654
5453
  style={{
4655
5454
  pointerEvents: 'none',
4656
5455
  fontVariantNumeric: 'tabular-nums',
4657
5456
  letterSpacing: (isPinned || isHot) ? '0.4px' : '0px',
4658
- transition: 'letter-spacing 300ms ease-out',
5457
+ transition: 'letter-spacing 300ms ease-out, font-weight 300ms ease-out',
4659
5458
  }}
4660
5459
  >{link.count}</text>
4661
5460
  </g>
@@ -4766,19 +5565,68 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4766
5565
  : workingCount <= 2 ? 1
4767
5566
  : workingCount <= 5 ? 2
4768
5567
  : 3;
5568
+ // Round 404 / Loop: hub-halo cyber trough opacity 0.08 →
5569
+ // 0.10. Pre-R404 the breath's low-point sat at α=0.08
5570
+ // cyber (per R84 family tuning) — the halo nearly faded
5571
+ // out at trough on the dark canvas. R404 lifts cyber
5572
+ // trough to 0.10. Per-bucket peak amplitudes [0.16/0.20/
5573
+ // 0.26/0.32] stay exactly tuned.
5574
+ //
5575
+ // Round 405 / Loop: hub-halo LIGHT trough 0.32 → 0.34 —
5576
+ // symmetric +0.02 lift to mirror R404's cyber treatment
5577
+ // across both themes. Pre-R405 only cyber got the lift
5578
+ // (R404 docstring noted "light already at the strong
5579
+ // end" as deliberate); but the cyber/light delta in
5580
+ // R404 was an inconsistency in the family pattern.
5581
+ // R405 closes the symmetry — both themes get +0.02
5582
+ // baseline lift, so the breath low-point reads with
5583
+ // matching confidence regardless of theme. Light peak
5584
+ // array [0.52/0.58/0.65/0.72] stays tuned.
5585
+ //
5586
+ // Stale-state legibility lift family (5 anchors now):
5587
+ // R317 subordinate-text gray-500 → gray-400
5588
+ // R358 freshness floor 0.25 → 0.30
5589
+ // R372 minimap offline-dot opacity 0.5 → 0.6
5590
+ // R404 hub-halo cyber trough 0.08 → 0.10
5591
+ // R405 hub-halo light trough 0.32 → 0.34 (this round)
5592
+ //
5593
+ // R84 per-bucket peak/dur + R245 ease-in-out spline
5594
+ // keySplines all preserved. Test fixture probes the
5595
+ // SMIL <animate> values via data-topo-hub-halo-trough
5596
+ // attr (now exposes both light + cyber resolved values).
4769
5597
  const peakLight = [0.52, 0.58, 0.65, 0.72][busy];
4770
5598
  const peakDark = [0.16, 0.20, 0.26, 0.32][busy];
4771
- const troughLight = 0.32;
4772
- const troughDark = 0.08;
5599
+ const troughLight = 0.34;
5600
+ const troughDark = 0.10;
4773
5601
  const dur = [4.0, 3.2, 2.7, 2.4][busy];
4774
5602
  const valuesLight = `${troughLight};${peakLight};${troughLight}`;
4775
5603
  const valuesDark = `${troughDark};${peakDark};${troughDark}`;
5604
+ // Round 408 / Loop: hub-halo radius 18 → 20. The
5605
+ // grounding halo (the breathing outer circle around
5606
+ // the hub center) is the canvas's signature breath
5607
+ // element — R84 family. R408 bumps r=18 → 20 so the
5608
+ // breath extends slightly further while keeping 4px
5609
+ // clearance before the spoke origin (still room for
5610
+ // spoke start anchors). Visual presence on the
5611
+ // canvas focal point lifts ~23% area (π·20²/π·18²
5612
+ // = 1.23) without changing the per-bucket opacity
5613
+ // envelope or breath rhythm. Visual-weight bump
5614
+ // family 13th anchor — pairs with R404/R405 trough
5615
+ // lifts so the halo now breathes both with more
5616
+ // visible amplitude AND more visual footprint.
5617
+ // R84 per-bucket peak/dur invariants + R244 calc-
5618
+ // Mode='spline' + R245 ease-in-out keySplines all
5619
+ // preserved. data-topo-hub-halo-radius attr exposes
5620
+ // value for tests.
4776
5621
  return (
4777
5622
  <circle
4778
- cx={cx} cy={cy} r="18"
5623
+ cx={cx} cy={cy} r="20"
4779
5624
  fill={isLight ? '#d1fae5' : '#10b981'}
4780
5625
  opacity={isLight ? 0.42 : 0.12}
4781
5626
  data-hub-busyness={busy}
5627
+ data-topo-hub-halo-radius="20"
5628
+ data-topo-hub-halo-trough={isLight ? troughLight : troughDark}
5629
+ data-topo-hub-halo-peak={isLight ? peakLight : peakDark}
4782
5630
  /* Round 253 / Loop: hub grounding halo fill transition
4783
5631
  for theme toggle. Pre-R253 the base fill (cyber
4784
5632
  #10b981 ↔ light #d1fae5) snapped while R244's SMIL
@@ -4877,11 +5725,40 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4877
5725
  textAnchor="middle"
4878
5726
  dy="0.36em"
4879
5727
  fill={isLight ? '#d1fae5' : '#ecfdf5'}
4880
- fontSize="11"
5728
+ /* Round 360 / Loop: hub working-count digit fontSize 11
5729
+ → 12. The hub is the canvas's focal point — its digit
5730
+ is the most-read scalar on the whole topology. R130
5731
+ sized it at 11 (well inside the r=10 / 20-px core);
5732
+ R360 nudges it to 12 (~13 px wide × 12 px tall, still
5733
+ well inside the 20-px diameter) for ~9 % more presence.
5734
+ Sibling visual-weight bump family:
5735
+ R287 minimap viewport stroke 1 → 1.5
5736
+ R295 legend swatch base radius 5.5 → 6
5737
+ R359 recent-row pip radius 1.6 → 1.8
5738
+ R360 hub digit fontSize 11 → 12 (this round)
5739
+ The R209 scale-1.08-on-hub-hover, R225 tabular-nums,
5740
+ R253 fill transition, R213 always-mount opacity gate
5741
+ all preserved. data-topo-hub-working-count-font-size
5742
+ attr exposes the value for tests. */
5743
+ fontSize="12"
4881
5744
  fontFamily="monospace"
4882
- fontWeight="700"
5745
+ /* R425 — hub digit fontWeight 700 → 800 on hoveredHub.
5746
+ Closes the "data tightens under attention" pattern
5747
+ across three focal scopes: chip-digit (R416, chip
5748
+ scope) → panel-digit (R424, panel-header scope) →
5749
+ hub-digit (R425, hub focal scope). The hub digit is
5750
+ the most-read scalar on the topology; adding a weight
5751
+ axis on hover stacks with the R209 scale-1.08 + R177
5752
+ ring grow + R370 halo opacity + R386 highlight
5753
+ opacity hub-hover gestures, giving the focal point
5754
+ a typographic axis alongside its scale/structure cues.
5755
+ R360 fontSize=12 + R225 tabular-nums + R209 scale +
5756
+ R253 fill transition all preserved. Transition list
5757
+ extends to include font-weight 200ms ease-out. */
5758
+ fontWeight={hoveredHub ? '800' : '700'}
4883
5759
  opacity={workingCount > 0 ? 1 : 0}
4884
5760
  data-topo-hub-working-count={workingCount}
5761
+ data-topo-hub-working-count-font-size="12"
4885
5762
  data-topo-hub-working-count-hovered={hoveredHub ? 'true' : 'false'}
4886
5763
  data-topo-hub-working-count-visible={workingCount > 0 ? 'true' : 'false'}
4887
5764
  // Round 209 / Loop: hub workingCount digit scales 1.0 →
@@ -4915,19 +5792,63 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4915
5792
  transform: !reducedMotion && hoveredHub ? 'scale(1.08)' : 'scale(1)',
4916
5793
  transformBox: 'fill-box',
4917
5794
  transformOrigin: 'center',
4918
- transition: 'transform 200ms ease-out, opacity 300ms ease-out, fill 200ms ease-out',
5795
+ /* R425: font-weight 200ms appended so the hover fw
5796
+ bump 700 → 800 eases under the same cadence as
5797
+ R209 scale + R253 fill + R213 opacity. */
5798
+ transition: 'transform 200ms ease-out, opacity 300ms ease-out, fill 200ms ease-out, font-weight 200ms ease-out',
4919
5799
  fontVariantNumeric: 'tabular-nums',
4920
5800
  }}
4921
5801
  >
4922
5802
  {workingCount}
4923
5803
  </text>
4924
5804
  {/* decorative highlight (visible when workingCount === 0) */}
5805
+ {/* Round 365 / Loop: hub-center 'lit-lamp' decorative highlight
5806
+ circle r 5 → 5.5. Sibling visual-weight bump family —
5807
+ each round lifts one canvas anchor's geometric presence
5808
+ without disturbing its bbox envelope:
5809
+ R287 minimap viewport stroke 1 → 1.5
5810
+ R295 legend swatch base radius 5.5 → 6
5811
+ R359 recent-row pip base radius 1.6 → 1.8
5812
+ R360 hub digit fontSize 11 → 12
5813
+ R361 edge-badge digit fontSize 10 → 11
5814
+ R365 hub-highlight base radius 5 → 5.5 (this round)
5815
+ The highlight only renders when workingCount === 0
5816
+ (decorative 'lamp lit but idle' state per R130 + R213
5817
+ always-mount opacity-gate). At idle, the 0.5-px radius
5818
+ bump (21 % area, π*5.5² / π*5² = 1.21) lifts the lamp's
5819
+ presence — still well inside the r=10 hub-core (R130).
5820
+ opacity=0 when working preserved so the hub-digit's R130
5821
+ takeover stays seamless. R213 always-mount opacity-gate
5822
+ + 300ms opacity transition + pointerEvents:none all
5823
+ preserved. data-topo-hub-highlight-radius attr exposes
5824
+ the value for tests. */}
5825
+ {/* Round 386 / Loop: hub-highlight idle opacity 0.9 → 0.95.
5826
+ When workingCount===0 the highlight paints as the visible
5827
+ idle "lamp lit but no work" core (R130 takeover gate).
5828
+ Pre-R386 idle opacity was 0.9 — a ~6 % fade against full
5829
+ paint that read as slightly-dimmed-ghost on the focal
5830
+ point. R386 lifts to 0.95 (idle alpha gap halved 0.10
5831
+ → 0.05) so the canvas anchor reads more confidently
5832
+ as a present-but-idle state rather than a faded ghost.
5833
+ Theme-consistency / canvas-presence polish family (4th
5834
+ anchor):
5835
+ R370 hub hover-ring opacity 0.7 → 0.8 cyber
5836
+ R371 edge-badge rest opacity 0.82 → 0.85 cyber
5837
+ R372 minimap offline-dot opacity 0.5 → 0.6
5838
+ R386 hub-highlight idle opacity 0.9 → 0.95 (this round)
5839
+ opacity=0 when working preserved so the hub-digit's
5840
+ R130 takeover stays seamless. 300ms opacity transition
5841
+ + R213 always-mount opacity-gate + pointerEvents:none
5842
+ + R365 r=5.5 all preserved. data-topo-hub-highlight-
5843
+ opacity attr exposes the resolved value for tests. */}
4925
5844
  <circle
4926
- cx={cx} cy={cy} r="5"
5845
+ cx={cx} cy={cy} r="5.5"
4927
5846
  fill="#d1fae5"
4928
- opacity={workingCount > 0 ? 0 : 0.9}
5847
+ opacity={workingCount > 0 ? 0 : 0.95}
4929
5848
  data-topo-hub-highlight
4930
5849
  data-topo-hub-highlight-visible={workingCount > 0 ? 'false' : 'true'}
5850
+ data-topo-hub-highlight-radius="5.5"
5851
+ data-topo-hub-highlight-opacity={workingCount > 0 ? 0 : 0.95}
4931
5852
  style={{
4932
5853
  pointerEvents: 'none',
4933
5854
  transition: 'opacity 300ms ease-out',
@@ -4958,15 +5879,46 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
4958
5879
  R142 group boxes, R143/R144 rows, R164 edge badges,
4959
5880
  R177 hub ring). prefers-reduced-motion respected via
4960
5881
  R29 globals.css blanket. */}
5882
+ {/* Round 385 / Loop: hub hover-ring strokeWidth 1.5 → 1.75.
5883
+ Sibling visual-weight bump (11th anchor) to R367 edge-
5884
+ badge rest stroke 1 → 1.25. The ring is only visible
5885
+ during hub hover (opacity=0 rest, R177 + R370 control
5886
+ the hover-state alpha) so the change manifests purely
5887
+ as a thicker hover-state ring on the canvas focal
5888
+ point. R177 r 14 → 17 grow + R370 opacity 0 → 0.8
5889
+ already lift the hover cue; R385 adds stroke weight
5890
+ as the third lift axis. Stays clear of R51 overlap-
5891
+ test sentinel value 3 (1.75 is non-sentinel); the
5892
+ R51 selector is gated to g[data-node] ancestors so
5893
+ this hub-internal circle is invisible to the probe
5894
+ regardless. R253 stroke transition + pointerEvents:
5895
+ none preserved. data-topo-hub-hover-ring-stroke-width
5896
+ attr exposes the value for tests. */}
4961
5897
  <circle
4962
5898
  cx={cx} cy={cy}
4963
5899
  r={hoveredHub ? 17 : 14}
4964
5900
  fill="none"
4965
5901
  stroke={isLight ? '#059669' : '#10b981'}
4966
- strokeWidth="1.5"
4967
- opacity={hoveredHub ? (isLight ? 0.85 : 0.7) : 0}
5902
+ strokeWidth="1.75"
5903
+ /* Round 370 / Loop: hub hover-ring cyber opacity 0.7
5904
+ 0.8. R177 designed the hub hover-ring at opacity-0 →
5905
+ 0.85 (light) / 0 → 0.7 (cyber). The 15 % gap between
5906
+ the two themes meant cyber-theme operators got a
5907
+ noticeably softer hover cue than light-theme users
5908
+ against backgrounds that should equalise (dark bg
5909
+ needs more luminance to read as 'on'). R370 bumps
5910
+ cyber 0.7 → 0.8, narrowing the theme gap to 5 % —
5911
+ sibling theme-consistency polish to R251 edge badge
5912
+ fill/opacity (cyber 0.82 / light 0.95) and R246/R247
5913
+ panel transition families. Light theme 0.85 stays
5914
+ as is (already in the legibility band). data-topo-
5915
+ hub-hover-ring-opacity attr exposes the value for
5916
+ tests. */
5917
+ opacity={hoveredHub ? (isLight ? 0.85 : 0.8) : 0}
4968
5918
  data-topo-hub-hover-ring
4969
5919
  data-topo-hub-hover-ring-radius={hoveredHub ? 17 : 14}
5920
+ data-topo-hub-hover-ring-stroke-width="1.75"
5921
+ data-topo-hub-hover-ring-opacity={hoveredHub ? (isLight ? 0.85 : 0.8) : 0}
4970
5922
  /* Round 253 / Loop: hub hover ring also gets stroke
4971
5923
  transition for theme toggle (cyber #10b981 ↔ light
4972
5924
  #059669). The opacity + r transitions stay for hover
@@ -5395,14 +6347,51 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
5395
6347
  keyTimes="0;0.5;1"
5396
6348
  keySplines="0.42 0 0.58 1;0.42 0 0.58 1"
5397
6349
  />
6350
+ {/* Round 409 / Loop: active-node pulse peak
6351
+ opacity lift — cyber 0.18 → 0.20 / light
6352
+ 0.12 → 0.14. Theme-consistency / canvas-
6353
+ presence family 9th anchor. R243 family
6354
+ rhythm preserved.
6355
+ Round 413 / Loop: trough lift mirrors R409
6356
+ peak — cyber 0.04 → 0.05 / light 0.02 →
6357
+ 0.03. Stale-state legibility lift family
6358
+ 8th anchor — pairs with R404 (hub-halo
6359
+ cyber trough 0.08 → 0.10) and R405 (light
6360
+ trough 0.32 → 0.34). The per-node breath's
6361
+ low-point now reads slightly above the
6362
+ "nearly gone" zone while preserving the
6363
+ breath amplitude (cyber Δ 0.16 vs Δ pre-
6364
+ R409+R413 of 0.14; light Δ 0.11 vs 0.10).
6365
+ Both peak (R409) AND trough (R413) lift
6366
+ together so the active-pulse signal stays
6367
+ confidently present at both ends of its
6368
+ 2.4s cycle.
6369
+ Stale-state legibility lift family (8):
6370
+ R317 subordinate-text gray-500→400
6371
+ R358 freshness floor 0.25→0.30
6372
+ R372 minimap offline-dot 0.5→0.6
6373
+ R404 hub-halo cyber trough 0.08→0.10
6374
+ R405 hub-halo light trough 0.32→0.34
6375
+ R406 edge freshness floor 0.35→0.40
6376
+ R407 node halo offline opacity (cyber+light)
6377
+ R413 active-node pulse trough (this round)
6378
+ cyber 0.04 → 0.05
6379
+ light 0.02 → 0.03
6380
+ R243 always-mount opacity-gate + R243
6381
+ ease-in-out keySplines + r animation
6382
+ (radius+8 ↔ radius+22) preserved.
6383
+ data-node-pulse-peak + new -pulse-trough
6384
+ attrs expose resolved per-theme values. */}
5398
6385
  <animate
5399
6386
  attributeName="opacity"
5400
- values={isLight ? '0.12;0.02;0.12' : '0.18;0.04;0.18'}
6387
+ values={isLight ? '0.14;0.03;0.14' : '0.20;0.05;0.20'}
5401
6388
  dur="2.4s"
5402
6389
  repeatCount="indefinite"
5403
6390
  calcMode="spline"
5404
6391
  keyTimes="0;0.5;1"
5405
6392
  keySplines="0.42 0 0.58 1;0.42 0 0.58 1"
6393
+ data-node-pulse-peak={isLight ? '0.14' : '0.20'}
6394
+ data-node-pulse-trough={isLight ? '0.03' : '0.05'}
5406
6395
  />
5407
6396
  </circle>
5408
6397
  </g>
@@ -5447,12 +6436,43 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
5447
6436
  parent stays for status flips. data-node-halo-
5448
6437
  breath-offset surfaces the chosen offset for
5449
6438
  test introspection. */}
6439
+ {/* Round 407 / Loop: offline node halo opacity lift —
6440
+ cyber 0.25 → 0.30 and light 0.4 → 0.45. Pre-R407
6441
+ offline node halos faded to α=0.25 cyber (75 %
6442
+ dim) / α=0.4 light. On the dark canvas the 0.25
6443
+ halo read as "nearly gone" — exactly the
6444
+ legibility floor R404/R405 just lifted on the
6445
+ hub-halo and R372 lifted on minimap offline dots.
6446
+ R407 closes the same family at the per-node halo
6447
+ surface: +0.05 lift on both themes so offline
6448
+ anchors stay legibly present without crossing into
6449
+ "could be online" territory (online cyber 0.65 /
6450
+ light 0.85 unchanged — the 0.30/0.65 cyber ratio
6451
+ still gives 2.17× contrast for online/offline).
6452
+ Stale-state legibility lift family (7 anchors now):
6453
+ R317 subordinate-text gray-500 → gray-400
6454
+ R358 freshness floor 0.25 → 0.30
6455
+ R372 minimap offline-dot 0.5 → 0.6
6456
+ R404 hub-halo cyber trough 0.08 → 0.10
6457
+ R405 hub-halo light trough 0.32 → 0.34
6458
+ R406 edge freshness floor 0.35 → 0.40
6459
+ R407 node halo offline opacity (this round)
6460
+ cyber 0.25 → 0.30
6461
+ light 0.4 → 0.45
6462
+ R278 retired-breath gate + R12 status.halo color
6463
+ + R226 phase stagger code-path preserved (the
6464
+ breath stays disabled per Vincent's R278 ask;
6465
+ only the BASE opacity floor shifts here). transi-
6466
+ tion list ('fill,opacity' 300ms ease-out) unchanged.
6467
+ data-node-halo-offline-opacity attr exposes the
6468
+ resolved value for tests. */}
5450
6469
  <circle
5451
6470
  cx={pos.x}
5452
6471
  cy={pos.y}
5453
6472
  r={radius + 8}
5454
6473
  fill={status.halo}
5455
- opacity={isOnline ? (isLight ? 0.85 : 0.65) : (isLight ? 0.4 : 0.25)}
6474
+ opacity={isOnline ? (isLight ? 0.85 : 0.65) : (isLight ? 0.45 : 0.30)}
6475
+ data-node-halo-offline-opacity={isOnline ? undefined : (isLight ? 0.45 : 0.30)}
5456
6476
  className="transition-[fill,opacity] duration-300 ease-out"
5457
6477
  data-node-halo-breath={!reducedMotion && session.status === 'working' ? 'on' : 'off'}
5458
6478
  data-node-halo-breath-offset={
@@ -5941,14 +6961,69 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
5941
6961
  220ms cadence the existing filter/stroke
5942
6962
  pair uses — coordinated 4-property easing
5943
6963
  across the card. */}
6964
+ {/* Round 411 / Loop: node label card rx 6 → 8.
6965
+ Pre-R411 the per-node label card painted at
6966
+ rx=6, sitting one tier BELOW the R332/R375/
6967
+ R376 compact-chrome tier (rx=8). Inside the
6968
+ corner-radius cascade family the cards used
6969
+ to be the only "smaller" tier — but the
6970
+ label card is a content-bearing surface
6971
+ (alias + sub text + ring), not a sub-
6972
+ element decoration. R411 lifts rx=6 → 8
6973
+ to align with the compact-chrome / segmented-
6974
+ control tier so all "compact card" surfaces
6975
+ read with the same corner radius.
6976
+ Corner-radius cascade (8 anchors now):
6977
+ R330 canvas rx 12 (root)
6978
+ R331 panels rx 10 (recent-signal, legend)
6979
+ R332 minimap container rx 8 (compact chrome)
6980
+ R375 Layout-toggle rx 8 (segmented control)
6981
+ R376 nodeSize/zoom rx 8 (segmented control)
6982
+ R390 hover-detail rx 10 (panel)
6983
+ R393 minimap viewport rx 2 (sub-element)
6984
+ R411 node label card rx 6 → 8 (compact card, this round)
6985
+ Pure paint — rx grows the corner curve
6986
+ inward without changing the card's outer
6987
+ cardW × cardH bbox (cardW=92/cardH=22 for
6988
+ standard nodes per R23 / R187 sizing). R217
6989
+ hover-stroke cyan tint + R142 drop-shadow
6990
+ + R246 fill+opacity 220ms transition list
6991
+ + R211 alias/sub text-fill ease all
6992
+ preserved. data-node-label-card-rx attr
6993
+ exposes the value for tests. */}
6994
+ {/* Round 429 / Loop: node label-card body opacity
6995
+ 0.94 → 1.0 on hover (cyber theme). Sibling
6996
+ treatment to R348 panel rect opacity lift —
6997
+ 0.92 → 0.97 cyber / 0.97 → 1.0 light at the
6998
+ panel scope. Pre-R429 the cyber theme card
6999
+ sat at 0.94 always; on hover R217 tinted the
7000
+ stroke + R142 grew the drop-shadow + R26
7001
+ lifted the group + R427/R428 spaced the text
7002
+ but the rect itself never solidified —
7003
+ the card glowed brighter through the
7004
+ shadow but the body alpha gap (6 pct) stayed
7005
+ fixed. R429 lifts the body to full alpha on
7006
+ hover so the card reads as a confidently
7007
+ present surface under the cursor (matching
7008
+ the panel-pair pattern). Light theme stays
7009
+ at 1.0 in both states (already maxed). R246
7010
+ transition list already covers opacity 220ms
7011
+ so the lift eases for free. R217 stroke tint
7012
+ + R142 drop-shadow + R211 fill ease all
7013
+ preserved (additive opacity branch only). */}
5944
7014
  <rect
5945
- x={-cardW / 2} y={cardTopY} width={cardW} height={cardH} rx="6"
7015
+ x={-cardW / 2} y={cardTopY} width={cardW} height={cardH} rx="8"
5946
7016
  fill={pal.labelBox.fill}
5947
7017
  stroke={!reducedMotion && hoveredAlias === session.alias
5948
7018
  ? pal.legendAccent
5949
7019
  : pal.labelBox.stroke}
5950
- opacity={isLight ? 1 : 0.94}
7020
+ opacity={
7021
+ !reducedMotion && hoveredAlias === session.alias
7022
+ ? 1
7023
+ : (isLight ? 1 : 0.94)
7024
+ }
5951
7025
  data-node-label-card={session.alias}
7026
+ data-node-label-card-rx="8"
5952
7027
  data-node-label-card-elevation={
5953
7028
  !reducedMotion && hoveredAlias === session.alias ? 'hover' : 'idle'
5954
7029
  }
@@ -5992,25 +7067,72 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
5992
7067
  transition list extends 'letter-spacing
5993
7068
  200ms ease-out' so it eases alongside the
5994
7069
  existing 300ms fill transition. */}
7070
+ {/* Round 427 / Loop: extend the node alias label
7071
+ letter-spacing family to a 3-tier scale —
7072
+ rest 0px → hover 0.3px → chat-target 0.5px.
7073
+ Pre-R427 the alias text shifted only when
7074
+ chat was actively pinned (R305); pure node-
7075
+ hover left the text dead-typographic while
7076
+ the surrounding card lifted (R26 translateY
7077
+ + R242 stroke + filter cues). R427 adds the
7078
+ missing typographic axis to the hover gesture
7079
+ so the alias text rises with the card.
7080
+ The chat-target tier still wins (0.5 > 0.3)
7081
+ so the pin signature stays at the top of the
7082
+ scale — hover is the mid tier between rest
7083
+ and chat-target.
7084
+ Hover-letter-spacing family extension:
7085
+ R344 chip count digit
7086
+ R345 panel title (R423 sibling)
7087
+ R347 active-links chip
7088
+ R351 vendor chip
7089
+ R420 zoom-level chip
7090
+ R427 node alias text (this round)
7091
+ R211 fill 300ms + R305 letter-spacing 200ms
7092
+ transition list preserved; only the
7093
+ conditional gets a middle case. */}
5995
7094
  <text
5996
7095
  x="0" y="1" textAnchor="middle"
5997
7096
  fill={status.text}
5998
7097
  fontSize={aliasFs} fontFamily="monospace" fontWeight="700"
5999
7098
  data-node-alias-text={session.alias}
6000
7099
  data-node-alias-chat-target={chatAlias === session.alias ? 'true' : 'false'}
7100
+ data-node-alias-hovered={hoveredAlias === session.alias ? 'true' : 'false'}
6001
7101
  style={{
6002
7102
  transition: 'fill 300ms ease-out, letter-spacing 200ms ease-out',
6003
- letterSpacing: chatAlias === session.alias ? '0.5px' : '0px',
7103
+ letterSpacing:
7104
+ chatAlias === session.alias ? '0.5px' :
7105
+ hoveredAlias === session.alias ? '0.3px' : '0px',
6004
7106
  }}
6005
7107
  >
6006
7108
  {truncate(session.alias, fullMax)}
6007
7109
  </text>
7110
+ {/* Round 428 / Loop: node sub-text (status label
7111
+ line beneath the alias) adopts hover letter-
7112
+ spacing tween 0 → 0.2px on hoveredAlias.
7113
+ Sibling treatment to R427 alias-text hover
7114
+ tween (0 → 0.3) — the alias is the primary
7115
+ identity (top-tier kerning 0.3), the sub-text
7116
+ is the secondary status line (one tier lower
7117
+ at 0.2). Now both lines of the label card
7118
+ telegraph hover typographically as one unit,
7119
+ matching the R26 card lift + R242 stroke
7120
+ tint + R975 filter cues. Subtler delta on
7121
+ the sub-text (0.2 vs alias 0.3) preserves
7122
+ the alias > status visual hierarchy at the
7123
+ hover scope. R211 fill 300ms transition
7124
+ preserved (additive letter-spacing branch
7125
+ + appended 'letter-spacing 200ms ease-out'). */}
6008
7126
  <text
6009
7127
  x="0" y={subY} textAnchor="middle"
6010
7128
  fill={status.primary}
6011
7129
  fontSize={subFs} fontFamily="monospace"
6012
7130
  data-node-sub-text={session.alias}
6013
- style={{ transition: 'fill 300ms ease-out' }}
7131
+ data-node-sub-text-hovered={hoveredAlias === session.alias ? 'true' : 'false'}
7132
+ style={{
7133
+ transition: 'fill 300ms ease-out, letter-spacing 200ms ease-out',
7134
+ letterSpacing: hoveredAlias === session.alias ? '0.2px' : '0px',
7135
+ }}
6014
7136
  >
6015
7137
  {status.label}{isOnline && sseCountFor != null ? ` sse:${sseCountFor}` : ''}
6016
7138
  </text>
@@ -6083,26 +7205,122 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6083
7205
  const detailY = pos.y - detailH / 2;
6084
7206
  return (
6085
7207
  <g transform={`translate(${detailX}, ${detailY})`} data-topo-hover-detail={session.alias} style={{ pointerEvents: 'none' }}>
7208
+ {/* Round 387 / Loop: hover-detail panel cyber backdrop
7209
+ opacity 0.94 → 0.97. The hover-detail card is
7210
+ ALWAYS rendered in active-hover context (it IS
7211
+ the hover product), so it should carry the
7212
+ same backdrop weight as the R348 recent-signal /
7213
+ legend panel HOVER state (which lifts 0.92 →
7214
+ 0.97 cyber). Pre-R387 the card sat at 0.94
7215
+ cyber, leaving a 0.03 alpha gap against the
7216
+ R348 panel-hover state — small but visible
7217
+ when the hover-detail floats next to a hovered
7218
+ recent-signal panel. R387 unifies them at 0.97
7219
+ so all active-hover panels paint with the same
7220
+ confident backdrop opacity in cyber. Light
7221
+ stays at 0.98 (already at the strong end —
7222
+ R348 light also stays at 0.97/0.98 max).
7223
+ Theme-consistency / canvas-presence polish
7224
+ family (5th anchor):
7225
+ R370 hub hover-ring opacity 0.7 → 0.8 cyber
7226
+ R371 edge-badge rest opacity 0.82 → 0.85 cyber
7227
+ R372 minimap offline-dot opacity 0.5 → 0.6
7228
+ R386 hub-highlight idle opacity 0.9 → 0.95
7229
+ R387 hover-detail panel opacity 0.94 → 0.97 cyber (this round)
7230
+ data-topo-hover-detail-opacity attr exposes
7231
+ the resolved value for tests. R348 drop-shadow
7232
+ + rx=8 + stroke=pal.legendAccent + fill=pal.
7233
+ labelBox.fill all preserved. */}
7234
+ {/* Round 390 / Loop: hover-detail card rx 8 → 10.
7235
+ Corner-radius cascade family — the hover-detail
7236
+ card is a panel-tier surface (192×88 floating
7237
+ info card with drop-shadow + stroke), so its
7238
+ corner radius should match the R331 panel tier
7239
+ (rx=10) used by the recent-signal and legend
7240
+ panels. Pre-R390 it shared rx=8 with the R332
7241
+ minimap and R375/R376 segmented-control tier
7242
+ (Layout-toggle, nodeSize, zoom wrappers),
7243
+ which is the "compact chrome control" tier —
7244
+ a tier mismatch for a content-bearing panel.
7245
+ Corner-radius cascade (6 anchors now):
7246
+ R330 canvas rx 12 (root)
7247
+ R331 panels rx 10 (recent-signal, legend)
7248
+ R332 minimap rx 8 (compact chrome)
7249
+ R375 Layout-toggle rx 8 (segmented control)
7250
+ R376 nodeSize/zoom rx 8 (segmented control)
7251
+ R390 hover-detail rx 10 (panel — this round)
7252
+ Pure paint change; no layout shift (rx grows
7253
+ the corner curve INWARD without changing the
7254
+ card's outer bbox). data-topo-hover-detail-
7255
+ rx attr exposes the resolved value for tests.
7256
+ R348 drop-shadow + stroke + R387 opacity all
7257
+ preserved. */}
6086
7258
  <rect
6087
- x="0" y="0" width={detailW} height={detailH} rx="8"
7259
+ x="0" y="0" width={detailW} height={detailH} rx="10"
6088
7260
  fill={pal.labelBox.fill}
6089
7261
  stroke={pal.legendAccent}
6090
- opacity={isLight ? 0.98 : 0.94}
7262
+ opacity={isLight ? 0.98 : 0.97}
7263
+ data-topo-hover-detail-opacity={isLight ? 0.98 : 0.97}
7264
+ data-topo-hover-detail-rx="10"
6091
7265
  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))' }}
6092
7266
  />
6093
7267
  <text x="10" y="16" fontSize="9" fontFamily="monospace" fill={pal.legendAccent} fontWeight="700">
6094
7268
  {v.id !== 'unknown' ? v.label : '—'}
6095
7269
  </text>
6096
- <text x="10" y="32" fontSize="10" fontFamily="monospace" fill={pal.legendHeadline}>
7270
+ {/* Round 389 / Loop: hover-detail model line (y=32)
7271
+ fontWeight 400 → 600. R388 lifted body lines
7272
+ (runtime/host/task at fontSize=9) to fw=500;
7273
+ R389 closes the typography hierarchy by giving
7274
+ the model name (the dominant subhead text in
7275
+ the card) its own weight tier. Three-tier
7276
+ ladder now reads cleanly:
7277
+ vendor fontSize=9 fw=700 (label badge)
7278
+ model fontSize=10 fw=600 (subhead — this round)
7279
+ body 3× fontSize=9 fw=500 (R388)
7280
+ One tier step per dimension (size + weight)
7281
+ between adjacent levels — classic editorial
7282
+ hierarchy idiom adapted to a 192×88 SVG card.
7283
+ Sibling to the chip-internal-hierarchy arc
7284
+ (R333-R341/R362/R369) which uses fw=600/500
7285
+ for digit/unit pairs; R389 applies the same
7286
+ fw=600 to a content-bearing identity line.
7287
+ data-topo-hover-detail-model-fw attr exposes
7288
+ the resolved value for tests. pal.legendHeadline
7289
+ fill preserved (R389 doesn't touch color). */}
7290
+ <text x="10" y="32" fontSize="10" fontFamily="monospace" fontWeight="600" fill={pal.legendHeadline} data-topo-hover-detail-model-fw="600">
6097
7291
  {session.model || 'model · pending'}
6098
7292
  </text>
6099
- <text x="10" y="48" fontSize="9" fontFamily="monospace" fill={pal.legendText}>
7293
+ {/* Round 388 / Loop: hover-detail body lines (the
7294
+ three fontSize=9 lines: runtime, host, task)
7295
+ gain fontWeight=500. Small-text fw lift family
7296
+ (6th anchor) — fontSize 9-10 px text reads
7297
+ consistently bolder at fw=500 than at the
7298
+ default 400 weight at small sizes, especially
7299
+ on the cyber-theme backdrop where stroke-
7300
+ rendering is the limiting factor.
7301
+ Sibling lifts in this family:
7302
+ R363 recent-row alias text 400 → 500
7303
+ R364 legend-row label 400 → 500
7304
+ R366 group-label count tspan 400 → 500
7305
+ R368 +N more flows footer 400 → 500
7306
+ R373 pressure-bar kicker (font-medium)
7307
+ R388 hover-detail body lines 400 → 500 (this round)
7308
+ Tier structure preserved:
7309
+ y=16 vendor (fw=700, headline)
7310
+ y=32 model (fontSize=10, subhead by size)
7311
+ y=48 runtime / y=64 host / y=80 task (body, now fw=500)
7312
+ The y=80 task line keeps opacity=0.7 so its
7313
+ caption-tier identity stays distinct from the
7314
+ y=48 / y=64 body lines despite shared fw.
7315
+ data-topo-hover-detail-body-fw attr exposes
7316
+ the resolved value for tests. */}
7317
+ <text x="10" y="48" fontSize="9" fontFamily="monospace" fontWeight="500" fill={pal.legendText} data-topo-hover-detail-body-fw="500">
6100
7318
  {rt ? rt.label : 'runtime · pending'}
6101
7319
  </text>
6102
- <text x="10" y="64" fontSize="9" fontFamily="monospace" fill={pal.legendText}>
7320
+ <text x="10" y="64" fontSize="9" fontFamily="monospace" fontWeight="500" fill={pal.legendText} data-topo-hover-detail-body-fw="500">
6103
7321
  host · {session.server || 'unknown'}
6104
7322
  </text>
6105
- <text x="10" y="80" fontSize="9" fontFamily="monospace" fill={pal.legendText} opacity="0.7">
7323
+ <text x="10" y="80" fontSize="9" fontFamily="monospace" fontWeight="500" fill={pal.legendText} opacity="0.7" data-topo-hover-detail-body-fw="500">
6106
7324
  {session.task ? truncate(session.task, 28) : 'no recent task'}
6107
7325
  </text>
6108
7326
  </g>
@@ -6166,14 +7384,41 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6166
7384
  keySplines="0.25 0.1 0.25 1"
6167
7385
  fill="freeze"
6168
7386
  />
7387
+ {/* Round 403 / Loop: click-ripple SMIL initial opacity
7388
+ 0.7 → 0.8. Pre-R403 the ripple's opacity animation
7389
+ faded from 0.7 to 0 over 500ms, providing a clean
7390
+ click-feedback pulse. Theme-consistency / canvas-
7391
+ presence polish family (R370 hub hover-ring +
7392
+ R391 hub-spoke active) already lifted paired
7393
+ hover-state alphas from 0.7 → 0.8. R403 brings
7394
+ click-feedback into that same alpha — three canvas
7395
+ state-feedback indicators (hover-ring, active spoke,
7396
+ click ripple) now share a uniform 0.8 start alpha
7397
+ so the visual "I responded" signal carries the
7398
+ same weight regardless of which state fired it.
7399
+ Pre-R403 invariants preserved: 500ms duration,
7400
+ R227 calcMode='spline' + ease-out keySplines
7401
+ (0.25 0.1 0.25 1), fill='freeze', concurrent r
7402
+ animation. Theme-consistency family (8 anchors):
7403
+ R370 hub hover-ring 0.7 → 0.8
7404
+ R371 edge-badge rest 0.82 → 0.85 cyber
7405
+ R372 minimap offline-dot 0.5 → 0.6
7406
+ R386 hub-highlight idle 0.9 → 0.95
7407
+ R387 hover-detail panel 0.94 → 0.97 cyber
7408
+ R391 hub-spoke active 0.7 → 0.8
7409
+ R392 minimap online-dot 0.9 → 0.95
7410
+ R403 click-ripple start 0.7 → 0.8 (this round)
7411
+ data-click-ripple-start-opacity attr exposes the
7412
+ resolved value for tests. */}
6169
7413
  <animate
6170
7414
  attributeName="opacity"
6171
- values="0.7;0"
7415
+ values="0.8;0"
6172
7416
  dur="0.5s"
6173
7417
  calcMode="spline"
6174
7418
  keyTimes="0;1"
6175
7419
  keySplines="0.25 0.1 0.25 1"
6176
7420
  fill="freeze"
7421
+ data-click-ripple-start-opacity="0.8"
6177
7422
  />
6178
7423
  </circle>
6179
7424
  )}
@@ -6247,17 +7492,21 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6247
7492
  <rect
6248
7493
  x="0" y="0" width="230" height="88" rx="10"
6249
7494
  fill={pal.legendBox.fill}
6250
- stroke={pal.legendBox.stroke}
6251
- // Round 348 / Loop: recent-signal panel rect opacity hover-
6252
- // state bump joins the panel-hover cue stack (R135 drop-
6253
- // shadow boost + R345 title letter-spacing tween 0.3 0.4
6254
- // + R266 fill theme-flip). Cyber 0.92 0.97, light 0.97 →
6255
- // 1.0 on hoveredPanel === 'recent'. The panel "solidifies"
6256
- // on hover pure paint-level change, geometry-safe (bbox
6257
- // unchanged so topo-overlap-test invariants hold). The
6258
- // R247 transition list already includes `opacity 200ms
6259
- // ease-out` so the value tween is automatic. Sibling
6260
- // change at legend panel rect below (~line 7222).
7495
+ // Round 423 / Loop: panel rect stroke tints to legendAccent
7496
+ // (cyan) on hover sibling to R217 label-card stroke
7497
+ // hover-tint at the panel scope. Pre-R423 the panel rect
7498
+ // stroke painted pal.legendBox.stroke (neutral) regardless
7499
+ // of hover state, while every other panel hover cue stacked:
7500
+ // R135 drop-shadow boost
7501
+ // R348 rect opacity 0.92 0.97 cyber
7502
+ // R345 title letter-spacing 0.3 → 0.4
7503
+ // R423 rect stroke legendAccent (this round)
7504
+ // Four hover layers now telegraph "you're entering this
7505
+ // panel" through structural, paint, and typographic axes
7506
+ // simultaneously. R247 transition list already covers
7507
+ // stroke 200ms ease-out so the tint eases naturally.
7508
+ // Sibling change at the legend panel rect below.
7509
+ stroke={hoveredPanel === 'recent' ? pal.legendAccent : pal.legendBox.stroke}
6261
7510
  opacity={hoveredPanel === 'recent' ? (isLight ? 1 : 0.97) : (isLight ? 0.97 : 0.92)}
6262
7511
  style={{
6263
7512
  /* R135: drop-shadow intensifies on panel hover. Base
@@ -6381,8 +7630,8 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6381
7630
  const alpha = ageSec <= 30
6382
7631
  ? 1
6383
7632
  : ageSec <= 300
6384
- ? 1 - ((ageSec - 30) / 270) * 0.75
6385
- : 0.25;
7633
+ ? 1 - ((ageSec - 30) / 270) * 0.70 /* R358: floor 0.25 → 0.30 lift across 3 freshness scopes */
7634
+ : 0.30; /* R358: stale floor lifted 0.25 → 0.30 — 20% legibility bump while preserving fresh/stale ratio */
6386
7635
  // Dark cyan-400 / light teal-600 with alpha — same
6387
7636
  // palette as R161's chip bullet so the two scopes
6388
7637
  // visually align even side-by-side.
@@ -6395,6 +7644,21 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6395
7644
  textAnchor="end"
6396
7645
  fontSize="10"
6397
7646
  fontFamily="monospace"
7647
+ // Round 349 / Loop: editorial letter-spacing 0.2 on the
7648
+ // recent-signal panel header count. Sits one tier below
7649
+ // the R301 panel title letterSpacing="0.3" so the panel
7650
+ // header reads as a 2-step hierarchy (title 0.3 / count
7651
+ // 0.2). Sibling change on the legend panel count below
7652
+ // closes the panel-pair editorial symmetry. Joins the
7653
+ // R285 / R289 / R301 / R302 / R304 / R325 editorial-
7654
+ // letterspacing tier at the panel-summary scope. The
7655
+ // R162 freshness fill, R225 tabular-nums, R311 fw=600,
7656
+ // R336 unit-tspan opacity-0.7 split all preserved —
7657
+ // the tier propagates to all descendant tspans via
7658
+ // SVG inheritance. data-recent-panel-count-letter-
7659
+ // spacing exposes the value for tests.
7660
+ letterSpacing="0.2"
7661
+ data-recent-panel-count-letter-spacing="0.2"
6398
7662
  >
6399
7663
  {/* Round 225 / Loop: tabular-nums on the panel-header
6400
7664
  flow-count tspan. The "{N} flows" string lives in
@@ -6435,13 +7699,27 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6435
7699
  tests + count value reads still resolve via
6436
7700
  .textContent. data-recent-panel-count-unit on
6437
7701
  the inner unit tspan for R336 introspection. */}
7702
+ {/* R424 — recent-signal panel count digit fontWeight
7703
+ 600 → 700 on panel hover. Closes the 5-layer panel
7704
+ hover cue stack with a typographic-weight axis at
7705
+ the panel-header data scope: depth (R135 drop-
7706
+ shadow) + solidity (R348 fill opacity) + spacing
7707
+ (R345 title letter-spacing) + edge color (R423
7708
+ stroke tint) + weight (THIS, digit fw). Sibling
7709
+ pattern to R416 chip-digit-hover-bold at chip
7710
+ scope — same "data tightens under attention"
7711
+ idiom now at the panel-header data scope. R311
7712
+ base fw=600 + R225 tabular-nums + R162 fill
7713
+ transition + R336 unit-tspan opacity-0.7 all
7714
+ preserved; only the weight axis tweens via R247's
7715
+ transition shape (added font-weight to the list). */}
6438
7716
  <tspan
6439
7717
  fill={freshFill}
6440
- fontWeight="600"
7718
+ fontWeight={hoveredPanel === 'recent' ? '700' : '600'}
6441
7719
  data-recent-panel-count
6442
7720
  data-recent-panel-count-freshness-alpha={alpha.toFixed(2)}
6443
7721
  style={{
6444
- transition: 'fill 200ms ease-out',
7722
+ transition: 'fill 200ms ease-out, font-weight 200ms ease-out',
6445
7723
  fontVariantNumeric: 'tabular-nums',
6446
7724
  }}
6447
7725
  >{flowLinks.length}<tspan opacity="0.7" data-recent-panel-count-unit> flows</tspan></tspan>
@@ -6863,17 +8141,58 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6863
8141
  const alpha = ageSec <= 30
6864
8142
  ? 1
6865
8143
  : ageSec <= 300
6866
- ? 1 - ((ageSec - 30) / 270) * 0.75
6867
- : 0.25;
8144
+ ? 1 - ((ageSec - 30) / 270) * 0.70 /* R358: floor 0.25 → 0.30 lift across 3 freshness scopes */
8145
+ : 0.30; /* R358: stale floor lifted 0.25 → 0.30 — 20% legibility bump while preserving fresh/stale ratio */
6868
8146
  return (
6869
8147
  <circle
6870
8148
  cx={10}
6871
8149
  cy={38 + index * 16 - 3}
6872
- r={1.6}
8150
+ /* Round 359 / Loop: recency pip base radius
8151
+ 1.6 → 1.8. Sibling lift to R358's freshness-
8152
+ floor bump (alpha 0.25 → 0.30) — pre-R358/
8153
+ R359 the stale pip painted at r=1.6 + α=0.25
8154
+ which read as near-invisible chrome. R358
8155
+ gave it more alpha; R359 gives it more area
8156
+ (1.8² / 1.6² ≈ 1.27, so ~27 % more glyph)
8157
+ so the pip stays distinguishable across the
8158
+ freshness ramp. Geometry: 1.8-radius dot
8159
+ centred at (10, row_y - 3) is bbox 3.6×3.6,
8160
+ still well inside the 7-px left margin
8161
+ (x=6 rect-start → x=13 text-start) the R160
8162
+ pip was placed in. Overlap-test reads the
8163
+ parent row rect's bbox, not this pip's, so
8164
+ grid+ring invariants hold. Matches the same
8165
+ 1.6 → 1.8 visual-weight bump R295 applied
8166
+ to the legend swatch (5.5 → 6 base radius)
8167
+ and R287 to the minimap viewport stroke
8168
+ (1 → 1.5). data-recent-row-freshness-radius
8169
+ attr exposes the value for tests. */
8170
+ /* Round 383 / Loop: recency pip base radius
8171
+ 1.8 → 2.0. Continues the R359 lift
8172
+ trajectory — pip area grows ~23 % (π·2²/
8173
+ π·1.8² ≈ 1.23) for a clearer at-a-glance
8174
+ freshness anchor in each row. Bbox 4.0×4.0
8175
+ still inside the 7-px R160 left margin
8176
+ (3-px remaining clearance vs 3.4 at r=1.8
8177
+ — geometry-safe margin holds). Sibling
8178
+ visual-weight bump family (9th anchor now):
8179
+ R287 minimap viewport stroke 1 → 1.5
8180
+ R295 legend swatch base radius 5.5 → 6
8181
+ R359 recent-row pip base radius 1.6 → 1.8
8182
+ R360 hub digit fontSize 11 → 12
8183
+ R361 edge-badge digit fontSize 10 → 11
8184
+ R365 hub-highlight base radius 5 → 5.5
8185
+ R367 edge-badge rest stroke 1 → 1.25
8186
+ R374 pressure-bar height 1.5 → 2
8187
+ R383 recent-row pip radius 1.8 → 2.0 (this round)
8188
+ data-recent-row-freshness-radius attr
8189
+ bumps to '2.0' for tests. */
8190
+ r={2.0}
6873
8191
  fill={pal.legendAccent}
6874
8192
  opacity={alpha}
6875
8193
  data-recent-row-freshness={link.key}
6876
8194
  data-recent-row-freshness-alpha={alpha.toFixed(2)}
8195
+ data-recent-row-freshness-radius="2.0"
6877
8196
  style={{ pointerEvents: 'none', transition: 'opacity 200ms ease-out' }}
6878
8197
  />
6879
8198
  );
@@ -6903,8 +8222,28 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6903
8222
  fill={isRowActive ? pal.legendHeadline : pal.legendText}
6904
8223
  fontSize="9"
6905
8224
  fontFamily="monospace"
8225
+ /* Round 363 / Loop: recent-row text fontWeight 400
8226
+ → 500 (font-medium tier). At fontSize=9 the
8227
+ default-weight 400 glyphs read thin against the
8228
+ panel chrome (pal.legendBox.fill with 0.92/0.97
8229
+ opacity); the 100-weight bump lifts the alias→
8230
+ alias text into the legibility band without
8231
+ changing geometry. The R320 count tspan fw=600
8232
+ (cold) / fw=700 (hot) override still wins
8233
+ locally via inline fontWeight on the inner
8234
+ tspan, so the count-vs-alias hierarchy stays
8235
+ intact:
8236
+ alias fw 500 (R363, this round)
8237
+ count fw 600/700 (R320)
8238
+ Sibling typography lift to R362 chip-row digit
8239
+ 500 → 600 — both nudge a within-element data
8240
+ tier without disturbing the surrounding family
8241
+ baseline. data-recent-row-text-font-weight attr
8242
+ exposes the value for tests. */
8243
+ fontWeight="500"
6906
8244
  data-recent-row-text={link.key}
6907
8245
  data-recent-row-text-pinned={isRowPinned ? 'true' : 'false'}
8246
+ data-recent-row-text-font-weight="500"
6908
8247
  style={{
6909
8248
  transition: 'fill 150ms ease-out, letter-spacing 150ms ease-out',
6910
8249
  letterSpacing: isRowPinned ? '0.5px' : '0px',
@@ -6979,7 +8318,25 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
6979
8318
  >
6980
8319
  {link.count}
6981
8320
  </tspan>
6982
- {' · '}{truncate(link.content, 8)}
8321
+ {/* Round 418 / Loop: recent-row content preview
8322
+ gains opacity=0.7 wrapper — subordinate-text
8323
+ tier at the SVG-text scope. Pre-R418 the
8324
+ truncated content preview (e.g. " · hi there")
8325
+ inherited the row's full opacity, reading at
8326
+ the same emphasis as the alias text and
8327
+ count digit. R418 wraps it in a <tspan> at
8328
+ opacity=0.7 so the preview reads as
8329
+ subordinate metadata — sibling to R333-R341/
8330
+ R362/R369/R389/R410/R412 chip-internal-
8331
+ hierarchy "label tier" (opacity-70) at the
8332
+ HTML scope, and R317 subordinate-text-lift
8333
+ gray-500 → gray-400 family. The leading
8334
+ " · " separator stays at full opacity so
8335
+ the row punctuation rhythm holds. data-
8336
+ recent-row-content-tspan attr surfaces the
8337
+ subordinate wrapper for tests. */}
8338
+ {' · '}
8339
+ <tspan opacity="0.7" data-recent-row-content-tspan>{truncate(link.content, 8)}</tspan>
6983
8340
  </text>
6984
8341
  {lastAt ? (
6985
8342
  /* Round 321 / Loop: lastAt freshness timestamp picks
@@ -7166,6 +8523,23 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7166
8523
  surface. transition list extends letter-spacing
7167
8524
  200ms ease-out alongside the existing opacity/
7168
8525
  fill easings. */}
8526
+ {/* Round 368 / Loop: `+N more flows` footer text gains
8527
+ fontWeight=500 (font-medium tier). Sibling small-
8528
+ text fw lift family with R363 recent-row alias
8529
+ + R364 legend-row label + R366 group-label count
8530
+ — all four lifts share the same theory: at small
8531
+ fontSize (9-11 px) against panel chrome, SVG-
8532
+ default fw 400 sits at the legibility floor;
8533
+ fw 500 brings the glyph into the deliberate-data
8534
+ band. fontStyle=italic + opacity 0.55 rest + R325
8535
+ letterSpacing 0.2 baseline + R344 hover-spread
8536
+ 0.2 → 0.3 + R195 cyan fill on hover all preserved
8537
+ — the fw bump just thickens the italic stroke.
8538
+ Hover-state punch (R195 fill + R325 opacity 0.55
8539
+ → 0.85 + R344 letter-spacing + R133 underline)
8540
+ stays as is, so the rest-vs-hover delta still
8541
+ reads clearly. data-recent-panel-more-font-weight
8542
+ attr exposes the value for tests. */}
7169
8543
  <text
7170
8544
  x="115" y="82"
7171
8545
  textAnchor="middle"
@@ -7173,11 +8547,13 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7173
8547
  fontSize="9"
7174
8548
  fontFamily="monospace"
7175
8549
  fontStyle="italic"
8550
+ fontWeight="500"
7176
8551
  letterSpacing={hoveredRecentMore ? '0.3' : '0.2'}
7177
8552
  opacity={hoveredRecentMore ? 0.85 : 0.55}
7178
8553
  textDecoration={hoveredRecentMore ? 'underline' : 'none'}
7179
8554
  data-recent-panel-more={moreCount}
7180
8555
  data-recent-panel-more-hovered={hoveredRecentMore ? 'true' : 'false'}
8556
+ data-recent-panel-more-font-weight="500"
7181
8557
  style={{ transition: 'opacity 150ms ease-out, fill 200ms ease-out, letter-spacing 200ms ease-out' }}
7182
8558
  >
7183
8559
  {`+ ${moreCount}`}
@@ -7229,7 +8605,11 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7229
8605
  <rect
7230
8606
  x="0" y="0" width="224" height="88" rx="10"
7231
8607
  fill={pal.legendBox.fill}
7232
- stroke={pal.legendBox.stroke}
8608
+ // R423 sibling — legend panel rect stroke tints to
8609
+ // legendAccent on hover (mirrors recent-signal panel
8610
+ // above). 4-layer hover cue stack now symmetric across
8611
+ // both side panels.
8612
+ stroke={hoveredPanel === 'legend' ? pal.legendAccent : pal.legendBox.stroke}
7233
8613
  // R348 sibling — legend panel rect opacity hover-state
7234
8614
  // bump 0.92 → 0.97 (cyber) / 0.97 → 1 (light) on
7235
8615
  // hoveredPanel === 'legend'. Pairs with the recent-signal
@@ -7321,12 +8701,27 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7321
8701
  R336 introspection; the parent .textContent still
7322
8702
  reads "{N} node(s)" so existing R310 count tests via
7323
8703
  textContent unchanged. */}
8704
+ {/* R424 sibling — legend panel count digit fontWeight 600
8705
+ → 700 on panel hover. Closes 5-layer panel hover cue
8706
+ stack symmetric across both side panels (recent-signal
8707
+ + legend): depth (R135) + solidity (R348) + spacing
8708
+ (R345) + edge color (R423) + weight (R424). R310 base
8709
+ fw=600 + R292 tabular-nums + R266 fill transition + R336
8710
+ unit-tspan opacity-0.7 all preserved. Same "data tightens
8711
+ under attention" idiom R416 established at chip scope. */}
7324
8712
  <text
7325
8713
  x="211" y="21" textAnchor="end"
7326
- fill={pal.legendAccent} fontSize="10" fontFamily="monospace" fontWeight="600"
8714
+ fill={pal.legendAccent} fontSize="10" fontFamily="monospace" fontWeight={hoveredPanel === 'legend' ? '700' : '600'}
8715
+ // R349 sibling — legend panel header count picks up
8716
+ // letterSpacing="0.2", one tier below the R301 panel
8717
+ // title 0.3. Pairs with the recent-signal panel count
8718
+ // letter-spacing above so the two corner panels' header
8719
+ // typography stays editorially symmetric.
8720
+ letterSpacing="0.2"
7327
8721
  data-legend-panel-count
8722
+ data-legend-panel-count-letter-spacing="0.2"
7328
8723
  style={{
7329
- transition: 'fill 200ms ease-out',
8724
+ transition: 'fill 200ms ease-out, font-weight 200ms ease-out',
7330
8725
  fontVariantNumeric: 'tabular-nums',
7331
8726
  }}
7332
8727
  >{sessions.length}<tspan opacity="0.7" data-legend-panel-count-unit> node{sessions.length === 1 ? '' : 's'}</tspan></text>
@@ -7545,14 +8940,45 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7545
8940
  so this legend-internal circle is invisible to
7546
8941
  that probe. pointerEvents:none so the ring can't
7547
8942
  intercept the row click that produced it. */}
8943
+ {/* Round 402 / Loop: legend pin-ring strokeWidth 1.5
8944
+ → 1.75. Sibling visual-weight bump (12th anchor)
8945
+ to R385 hub hover-ring strokeWidth 1.5 → 1.75 —
8946
+ both are pin/hover state indicators painted as
8947
+ stroke-only circles outside their target swatch
8948
+ with the R51 sentinel value 1.5. R402 lifts to
8949
+ 1.75 (matching R385's choice) so the pin signal
8950
+ reads slightly heavier without crossing the
8951
+ R51 sentinel band (3 reserved for offline node).
8952
+ The R51 selector is gated to g[data-node]
8953
+ ancestors so this legend-internal circle (lives
8954
+ under a <g data-legend-status>) is invisible
8955
+ to the probe — same lesson R177/R385 documented.
8956
+ Visual-weight bump family (12 anchors now):
8957
+ R287 minimap viewport stroke 1 → 1.5
8958
+ R295 legend swatch radius 5.5 → 6
8959
+ R359 recent-row pip radius 1.6 → 1.8
8960
+ R360 hub digit fontSize 11 → 12
8961
+ R361 edge-badge digit fontSize 10 → 11
8962
+ R365 hub-highlight radius 5 → 5.5
8963
+ R367 edge-badge rest stroke 1 → 1.25
8964
+ R374 pressure-bar height 1.5 → 2
8965
+ R383 recent-row pip radius 1.8 → 2.0
8966
+ R384 minimap online dot 1.7 → 1.9
8967
+ R385 hub hover-ring stroke 1.5 → 1.75
8968
+ R402 legend pin-ring stroke 1.5 → 1.75 (this round)
8969
+ R181 always-mount opacity gate + 150ms transition
8970
+ + pointerEvents:none all preserved. data-legend-
8971
+ pin-ring-stroke-width attr exposes the value for
8972
+ tests. */}
7548
8973
  <circle
7549
8974
  cx="16" cy={row.y0} r="8"
7550
8975
  fill="none"
7551
8976
  stroke={row.fill}
7552
- strokeWidth="1.5"
8977
+ strokeWidth="1.75"
7553
8978
  opacity={isPinned ? 1 : 0}
7554
8979
  data-legend-pin-ring={row.key}
7555
8980
  data-legend-pin-ring-pinned={isPinned ? 'true' : 'false'}
8981
+ data-legend-pin-ring-stroke-width="1.75"
7556
8982
  style={{
7557
8983
  pointerEvents: 'none',
7558
8984
  transition: 'opacity 150ms ease-out',
@@ -7579,8 +9005,31 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7579
9005
  fill={hoveredStatus === row.key || isPinned ? pal.legendHeadline : pal.legendText}
7580
9006
  fontSize="11"
7581
9007
  fontFamily="monospace"
9008
+ /* Round 364 / Loop: legend-row label fontWeight 400
9009
+ → 500. Sibling typography lift to R363 recent-row
9010
+ text fw 400 → 500. Both surfaces render small
9011
+ monospace text against panel chrome at fontSize
9012
+ 9-11 where SVG-default fw 400 sits at the
9013
+ legibility floor. font-medium tier (500) gives
9014
+ the label a more deliberate-data register.
9015
+ The R309 per-row count text (separate element
9016
+ below at x=215 textAnchor=end) keeps its own
9017
+ fontWeight 600 inline override, so the count >
9018
+ label hierarchy stays intact at the legend
9019
+ scope same as R363 holds it at the recent-row
9020
+ scope:
9021
+ legend label fw 500 (R364, this round)
9022
+ legend count fw 600 (R309)
9023
+ recent alias fw 500 (R363)
9024
+ recent count fw 600/700 (R320)
9025
+ data-legend-row-label-font-weight attr exposes
9026
+ the value for tests. R219 letter-spacing pin
9027
+ tween + R55 fill transition + R181 always-mount
9028
+ pin ring all preserved. */
9029
+ fontWeight="500"
7582
9030
  data-legend-row-label={row.key}
7583
9031
  data-legend-row-label-pinned={isPinned ? 'true' : 'false'}
9032
+ data-legend-row-label-font-weight="500"
7584
9033
  style={{
7585
9034
  transition: 'fill 150ms ease-out, letter-spacing 150ms ease-out',
7586
9035
  letterSpacing: isPinned ? '0.5px' : '0px',
@@ -7899,14 +9348,75 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7899
9348
  const isOn = s.status !== 'offline' || !!sseN;
7900
9349
  const st = nodeStatus(s, isOn, isLight);
7901
9350
  return (
9351
+ /* Round 372 / Loop: minimap offline-dot opacity
9352
+ 0.5 → 0.6. Sibling stale-state legibility lift
9353
+ to R358 freshness ramp floor 0.25 → 0.30 + R317
9354
+ subordinate-text-lift family. Pre-R372 R198
9355
+ drew offline dots at α=0.5 (44 % below online
9356
+ 0.9). The minimap is a small overlay against
9357
+ the canvas backdrop — at α=0.5 offline dots
9358
+ sat at the legibility floor when the minimap
9359
+ mounted (only on non-default view). R372 lifts
9360
+ offline 0.5 → 0.6 for +20 % relative presence;
9361
+ online stays at 0.9 so the offline/online
9362
+ contrast ratio is now 0.6/0.9 ≈ 0.67 (vs prior
9363
+ 0.5/0.9 ≈ 0.56) — still a clear two-tier
9364
+ distinction. R198 opacity + fill + r transition
9365
+ list preserved so status flips still ease
9366
+ smoothly. data-topo-minimap-dot-opacity attr
9367
+ exposes the resolved value for tests. */
7902
9368
  <circle
7903
9369
  key={s.alias}
7904
9370
  cx={p.x * sx} cy={p.y * sy}
7905
- r={isOn ? 1.7 : 1.2}
9371
+ /* Round 384 / Loop: minimap online dot radius 1.7
9372
+ → 1.9. Sibling visual-weight bump (10th anchor)
9373
+ to R383 recent-row pip 1.8 → 2.0. R198 designed
9374
+ the dots at 1.7 (online) / 1.2 (offline) — at
9375
+ the minimap's 120 × 82 scale these read clearly
9376
+ but the online ↔ offline contrast was modest
9377
+ (1.7/1.2 = 1.42×). R384 bumps online to 1.9 so
9378
+ the tier delta widens to 1.58× (1.9/1.2). Pair
9379
+ completes minimap-dot legibility polish:
9380
+ R358 (era R372) offline opacity 0.5 → 0.6
9381
+ R384 online radius 1.7 → 1.9 (this round)
9382
+ R198 transition list (opacity + fill + r 200ms)
9383
+ preserved so status flips still ease smoothly.
9384
+ data-topo-minimap-dot-radius attr exposes the
9385
+ resolved value for tests. */
9386
+ /* Round 392 / Loop: minimap online dot opacity
9387
+ 0.9 → 0.95. Theme-consistency / canvas-presence
9388
+ polish family (7th anchor) — mirrors R386's
9389
+ hub-highlight idle 0.9 → 0.95 lift on the
9390
+ minimap surface: the online-dot's idle alpha
9391
+ gap (0.10 against full presence) halves to
9392
+ 0.05, so the live-fleet anchors on the minimap
9393
+ read more confidently. Offline dot stays at
9394
+ R372 0.6 — the binary online/offline contrast
9395
+ ratio shifts from 0.6/0.9 ≈ 0.67 to 0.6/0.95
9396
+ ≈ 0.63, preserved as a clear two-tier
9397
+ distinction. R198 opacity + fill + r transition
9398
+ list + R384 r=1.9 + R372 offline 0.6 all
9399
+ preserved. data-topo-minimap-dot-opacity attr
9400
+ bumps to '0.95' for tests. */
9401
+ /* Round 421 / Loop: online dot opacity 0.95 → 1.0
9402
+ on minimap container hover. Sibling to R346
9403
+ viewport rect strokeWidth/opacity hover tween.
9404
+ When the user hovers the minimap container,
9405
+ the live-fleet anchors brighten from R392
9406
+ baseline (0.95) to full opacity in concert
9407
+ with the R346 viewport rect lift. Offline
9408
+ stays at R372 0.6 — hover state focuses
9409
+ attention on the ACTIVE anchors, not the
9410
+ stale ones. data-topo-minimap-dot-opacity
9411
+ attr (R392) reflects the resolved hover-
9412
+ state value for tests. */
9413
+ r={isOn ? 1.9 : 1.2}
7906
9414
  fill={st.primary}
7907
- opacity={isOn ? 0.9 : 0.5}
9415
+ opacity={isOn ? (hoveredMinimap ? 1 : 0.95) : 0.6}
7908
9416
  data-topo-minimap-dot={s.alias}
7909
9417
  data-topo-minimap-dot-online={isOn ? 'true' : 'false'}
9418
+ data-topo-minimap-dot-opacity={isOn ? (hoveredMinimap ? 1 : 0.95) : 0.6}
9419
+ data-topo-minimap-dot-radius={isOn ? 1.9 : 1.2}
7910
9420
  style={{
7911
9421
  transition: 'opacity 200ms ease-out, fill 200ms ease-out, r 200ms ease-out',
7912
9422
  } as React.CSSProperties}
@@ -7947,17 +9457,66 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
7947
9457
  information element to lift it above ambient
7948
9458
  chrome. opacity 0.9 stays — strokeWidth alone
7949
9459
  does the lifting. */}
9460
+ {/* Round 379 / Loop: minimap viewport rect picks up
9461
+ strokeLinejoin='round'. Pre-R379 the rect's 4
9462
+ corners painted with default 'miter' joins —
9463
+ sharp 90° corners with a small miter overshoot
9464
+ (≈ strokeWidth × 1.4 = 2.1 px at sw=1.5). R379
9465
+ rounds the joins so corners arc smoothly through
9466
+ a quarter-circle of radius ≈ strokeWidth/2. At
9467
+ sw=1.5 that's a 0.75-px radius — subtle but
9468
+ matches the same stroke-softening vocabulary R288
9469
+ chrome icons (zoom/reset/fullscreen) and R378
9470
+ flow-rail already speak. Geometry-safe: stroke-
9471
+ linejoin only affects the corner overshoot, the
9472
+ rect's bbox is unchanged. R287 strokeWidth=1.5 +
9473
+ R346 hover-state strokeWidth/opacity bump + R199
9474
+ smoothView x/y/w/h transition all preserved.
9475
+ data-topo-minimap-viewport-linejoin attr exposes
9476
+ the value for tests. */}
9477
+ {/* Round 393 / Loop: minimap viewport rect rx 0 → 2.
9478
+ Pre-R393 the cyan-stroked viewport rect (the frame
9479
+ showing what's currently visible on the canvas)
9480
+ drew with sharp corners inside the R332 rounded
9481
+ minimap container (rx=8). A small frame with sharp
9482
+ corners sitting inside a rounded container reads
9483
+ as visually loud — the 90° corners catch the eye
9484
+ against the soft container edge. R393 adds rx=2
9485
+ so the viewport corners get a subtle radius that
9486
+ matches the family's softening idiom on a sub-
9487
+ element scale. The R379 strokeLinejoin='round'
9488
+ already softens stroke joins; R393 adds a complete
9489
+ geometric soften via rx.
9490
+ Corner-radius cascade (7 anchors now):
9491
+ R330 canvas rx 12
9492
+ R331 panels rx 10
9493
+ R332 minimap container rx 8
9494
+ R375 Layout-toggle rx 8
9495
+ R376 nodeSize/zoom rx 8
9496
+ R390 hover-detail rx 10
9497
+ R393 minimap viewport rx 2 (this round, sub-element)
9498
+ The 2-px radius is intentionally small — the
9499
+ viewport rect is typically only 30-50px wide,
9500
+ where rx=2 reads as "rounded enough to not snap"
9501
+ without feeling pillowy. data-topo-minimap-
9502
+ viewport-rx attr exposes the resolved value
9503
+ for tests. R346 hover-state tweens (strokeWidth
9504
+ + opacity) preserved verbatim. */}
7950
9505
  <rect
7951
9506
  x={Math.max(0, rectX)} y={Math.max(0, rectY)}
7952
9507
  width={Math.max(0, Math.min(MW - Math.max(0, rectX), rectW))}
7953
9508
  height={Math.max(0, Math.min(MH - Math.max(0, rectY), rectH))}
9509
+ rx="2"
7954
9510
  fill="none" stroke={pal.legendAccent}
7955
9511
  // R346: strokeWidth + opacity tween on container hover.
7956
9512
  strokeWidth={hoveredMinimap ? '1.75' : '1.5'}
9513
+ strokeLinejoin="round"
7957
9514
  opacity={hoveredMinimap ? '1' : '0.9'}
7958
9515
  data-topo-minimap-viewport
9516
+ data-topo-minimap-viewport-rx="2"
7959
9517
  data-topo-minimap-viewport-smooth={smoothView ? 'true' : 'false'}
7960
9518
  data-topo-minimap-viewport-hover={hoveredMinimap ? 'true' : 'false'}
9519
+ data-topo-minimap-viewport-linejoin="round"
7961
9520
  style={{
7962
9521
  transition: smoothView
7963
9522
  ? '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'
@@ -8016,8 +9575,22 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8016
9575
  own transition-colors. Same R254 holdover pattern that
8017
9576
  R263 just closed at the canvas wrapper scope, now at the
8018
9577
  chrome strip's nodeSize sub-wrapper scope. */}
9578
+ {/* Round 376 / Loop: nodeSize wrapper rounded-md → rounded-lg.
9579
+ Sibling polish to R375 Layout-toggle wrapper. Three
9580
+ chrome-strip segmented controls now all share rounded-lg
9581
+ at the wrapper tier:
9582
+ R375 Layout-toggle wrapper rounded-lg 8 px
9583
+ R376 nodeSize wrapper rounded-lg 8 px (this round)
9584
+ R376 zoom wrapper rounded-lg 8 px (this round)
9585
+ Individual atomic chrome buttons (reset, fullscreen) keep
9586
+ rounded-md (6 px) as their own atomic-button tier — the
9587
+ chrome strip's typography now expresses a clear two-tier
9588
+ hierarchy: 'segmented control container' (rounded-lg)
9589
+ vs 'standalone button' (rounded-md). Pure paint change,
9590
+ no layout shift. */}
8019
9591
  <div
8020
- className="flex items-center rounded-md border overflow-hidden"
9592
+ className="flex items-center rounded-lg border overflow-hidden"
9593
+ data-topo-chrome-nodesize-radius="rounded-lg"
8021
9594
  style={{
8022
9595
  background: pal.legendBox.fill,
8023
9596
  borderColor: pal.containerBorder,
@@ -8097,8 +9670,12 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8097
9670
  data-topo-chrome-view-group-leader marks the boundary surface
8098
9671
  for the test probe; data-topo-chrome-fleet-group-trailer marks
8099
9672
  the nodeSize wrapper's right edge for the gap measurement. */}
9673
+ {/* R376 sibling — zoom wrapper rounded-md → rounded-lg.
9674
+ Closes the chrome-strip segmented-control corner radius
9675
+ cascade (Layout R375 + nodeSize R376 + zoom R376). */}
8100
9676
  <div
8101
- className="ml-1.5 flex items-center rounded-md border overflow-hidden"
9677
+ className="ml-1.5 flex items-center rounded-lg border overflow-hidden"
9678
+ data-topo-chrome-zoom-wrapper-radius="rounded-lg"
8102
9679
  style={{
8103
9680
  background: pal.legendBox.fill,
8104
9681
  borderColor: pal.containerBorder,
@@ -8114,7 +9691,8 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8114
9691
  // R196: press-state deepens bg one tier above hover (white/5
8115
9692
  // → white/10) so mouse-down has a tactile dim before the
8116
9693
  // R186 icon pop fires on release.
8117
- 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"
9694
+ // R352: `group` lets the inner svg respond via group-hover.
9695
+ 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"
8118
9696
  style={{ color: pal.legendText }}
8119
9697
  aria-label="Zoom out"
8120
9698
  title="Zoom out (−)"
@@ -8122,11 +9700,25 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8122
9700
  {/* R186: icon pop on click. CSS animation runs once;
8123
9701
  React removes the class after 240ms so a quick
8124
9702
  re-click can replay. */}
9703
+ {/* Round 352 / Loop: zoom-out icon picks up group-hover:
9704
+ scale-110 — sibling to R350 reset hover-rotate. Pre-
9705
+ R352 hovering the zoom button only changed the bg
9706
+ (white/5); the icon inside stayed perfectly still.
9707
+ R352 lifts the icon 10% on hover for a tactile "this
9708
+ button does something" cue. The R186 anet-chrome-pop
9709
+ keyframe (220ms scale 1→1.06→1) still owns transform
9710
+ during click via CSS-animation precedence over
9711
+ transition-transform; after the pop ends + className
9712
+ is removed, the group-hover scale-110 picks up
9713
+ smoothly. `transform-gpu` hint promotes the svg to
9714
+ its own compositor layer for crisper edges during
9715
+ the scale tween. Sibling change on zoom-in icon
9716
+ below. */}
8125
9717
  <svg
8126
9718
  width="12" height="12" viewBox="0 0 24 24"
8127
9719
  fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"
8128
9720
  aria-hidden
8129
- className={chromePopping === 'zoom-out' ? 'anet-chrome-pop' : undefined}
9721
+ className={`transition-transform duration-200 ease-out group-hover:scale-110 transform-gpu${chromePopping === 'zoom-out' ? ' anet-chrome-pop' : ''}`}
8130
9722
  data-topo-chrome-zoom-out-icon
8131
9723
  ><path d="M5 12h14" /></svg>
8132
9724
  </button>
@@ -8183,6 +9775,24 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8183
9775
  // R347: letter-spacing hover tween — extends R344/R345
8184
9776
  // hover-letter-spacing family into the chrome strip.
8185
9777
  letterSpacing: hoveredZoomLevel ? '0.5px' : '0',
9778
+ // Round 420 / Loop: zoom-level readout gains a SECOND
9779
+ // hover axis — fontWeight 500 → 600 on hover. Sibling
9780
+ // to R347 (same element, hover letter-spacing tween).
9781
+ // The chrome strip's only data display now has a two-
9782
+ // axis hover signature (letter-spacing + fontWeight),
9783
+ // matching the R416 chip-row chip digit hover-bold
9784
+ // pattern at the chrome scope. Pre-R420 hovering only
9785
+ // spread the digits 0 → 0.5px; the weight stayed at
9786
+ // R332's 'font-medium' (500) baseline. Post-R420
9787
+ // hover lifts BOTH letter-spacing AND weight so the
9788
+ // percent reads with the same data-tier emphasis
9789
+ // intensification the chip-row chips do on hover.
9790
+ // Inline fontWeight overrides the className's
9791
+ // 'font-medium' since they target the same property.
9792
+ // 200ms transition list extends to font-weight for
9793
+ // smooth easing. data-topo-chrome-zoom-level-hover
9794
+ // attr surfaces the hover state for tests.
9795
+ fontWeight: hoveredZoomLevel ? 600 : 500,
8186
9796
  /* Round 264 / Loop: zoom level readout gains theme-toggle
8187
9797
  transition. The span has theme-driven color (pal.
8188
9798
  legendText) + border-x (pal.containerBorder via the
@@ -8191,7 +9801,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8191
9801
  on theme flip while siblings eased. Sibling treatment
8192
9802
  to the nodeSize + zoom wrapper transitions added this
8193
9803
  round. */
8194
- transition: 'color 200ms ease-out, border-color 200ms ease-out, letter-spacing 200ms ease-out',
9804
+ transition: 'color 200ms ease-out, border-color 200ms ease-out, letter-spacing 200ms ease-out, font-weight 200ms ease-out',
8195
9805
  }}
8196
9806
  title="Current zoom level"
8197
9807
  >
@@ -8202,18 +9812,22 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8202
9812
  data-topo-chrome-zoom-in
8203
9813
  data-topo-chrome-zoom-in-popping={chromePopping === 'zoom-in' ? 'true' : 'false'}
8204
9814
  // R196: press-state (mirror of zoom-out above).
8205
- 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"
9815
+ // R352: `group` lets the inner svg respond via group-hover.
9816
+ 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"
8206
9817
  style={{ color: pal.legendText }}
8207
9818
  aria-label="Zoom in"
8208
9819
  title="Zoom in (+)"
8209
9820
  >
8210
9821
  {/* R186: icon pop on click. Same one-shot CSS animation
8211
9822
  as zoom-out; React removes the class after 240ms. */}
9823
+ {/* R352 sibling — zoom-in icon picks up the same
9824
+ group-hover:scale-110 family. Mirror change at
9825
+ the zoom-out icon above. */}
8212
9826
  <svg
8213
9827
  width="12" height="12" viewBox="0 0 24 24"
8214
9828
  fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"
8215
9829
  aria-hidden
8216
- className={chromePopping === 'zoom-in' ? 'anet-chrome-pop' : undefined}
9830
+ className={`transition-transform duration-200 ease-out group-hover:scale-110 transform-gpu${chromePopping === 'zoom-in' ? ' anet-chrome-pop' : ''}`}
8217
9831
  data-topo-chrome-zoom-in-icon
8218
9832
  ><path d="M12 5v14M5 12h14" /></svg>
8219
9833
  </button>
@@ -8222,9 +9836,35 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8222
9836
  onClick={() => { armResetSpin(); resetView(); }}
8223
9837
  data-topo-chrome-reset
8224
9838
  data-topo-chrome-reset-spinning={resetSpinning ? 'true' : 'false'}
9839
+ data-topo-chrome-reset-hover={hoveredReset ? 'true' : 'false'}
9840
+ // R350: hover state drives the icon transform below.
9841
+ onMouseEnter={() => setHoveredReset(true)}
9842
+ onMouseLeave={() => setHoveredReset(false)}
9843
+ onFocus={() => setHoveredReset(true)}
9844
+ onBlur={() => setHoveredReset(false)}
8225
9845
  // R196: press-state deepens before R184 reset-spin fires on
8226
9846
  // release — mouse-down dim then 450ms spin = full handshake.
8227
- 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"
9847
+ /* Round 400 / Loop · milestone: chrome reset + fullscreen
9848
+ buttons gain hover:-translate-y-px lift — closes the
9849
+ hover-lift gesture vocabulary across every standalone
9850
+ interactive HTML element in TopoGraph. Segmented
9851
+ controls (zoom -/+, nodeSize S/M/L, Layout Ring/Grid)
9852
+ intentionally stay planted: lifting one segment of a
9853
+ unified strip would tear the visual unity of the
9854
+ segmented control. Only the standalone chrome buttons
9855
+ (reset, fullscreen) get the lift.
9856
+ Gesture vocabulary post-R400 (now complete across HTML):
9857
+ chip-row chips (3×) -1 px R398, R399
9858
+ filter pin pills (4×) -1 px R397
9859
+ recent-signal row -1 px R143
9860
+ legend row -1 px R144
9861
+ reset button -1 px R400 (this round)
9862
+ fullscreen button -1 px R400 (this round)
9863
+ Every standalone interactive HTML surface in TopoGraph
9864
+ now lifts on hover. data-topo-chrome-reset-hover-lift
9865
+ attr surfaces the lift for tests. */
9866
+ className="p-1.5 rounded-md border hover:bg-white/5 active:bg-white/10 hover:-translate-y-px transition-colors transition-transform duration-200 ease-out transform-gpu focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60"
9867
+ data-topo-chrome-reset-hover-lift="true"
8228
9868
  style={{ background: pal.legendBox.fill, borderColor: pal.containerBorder, color: pal.legendText }}
8229
9869
  aria-label="Reset view"
8230
9870
  title="Reset zoom + pan (0, or double-click the canvas)"
@@ -8251,6 +9891,17 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8251
9891
  aria-hidden
8252
9892
  className={resetSpinning ? 'anet-reset-spin' : undefined}
8253
9893
  data-topo-chrome-reset-icon
9894
+ // R350: hover-rotate preview of the R184 click-spin.
9895
+ // Gated on !resetSpinning so the anet-reset-spin keyframe
9896
+ // owns transform during its 450ms run. transformOrigin
9897
+ // 'center' so rotation pivots around the icon's centre
9898
+ // (default would be top-left and the icon would arc).
9899
+ style={{
9900
+ transform: hoveredReset && !resetSpinning ? 'rotate(-8deg)' : 'rotate(0deg)',
9901
+ transformOrigin: 'center',
9902
+ transition: 'transform 200ms ease-out',
9903
+ }}
9904
+ data-topo-chrome-reset-icon-hover={hoveredReset && !resetSpinning ? 'true' : 'false'}
8254
9905
  >
8255
9906
  <path d="M3 12a9 9 0 1 0 9-9 9 9 0 0 0-6.4 2.6L3 8" />
8256
9907
  <path d="M3 3v5h5" />
@@ -8284,11 +9935,18 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8284
9935
  its inactive state benefits from the same "hover previews
8285
9936
  active state" idiom R163 designed. Sibling treatment to
8286
9937
  the nodeSize buttons at line ~6711. */
8287
- className={`p-1.5 rounded-md border transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 ${
9938
+ // R353: `group` lets the inner svg respond via group-hover
9939
+ // sibling to R352 zoom buttons. Closes the chrome-strip per-
9940
+ // icon hover-affordance arc (zoom-out / zoom-in / reset /
9941
+ // fullscreen now all carry an icon-level hover gesture in
9942
+ // addition to the bg hover).
9943
+ // R400: hover translateY(-1px) lift — see reset button above for family doc.
9944
+ className={`group p-1.5 rounded-md border hover:-translate-y-px transition-colors transition-transform duration-200 ease-out transform-gpu focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 ${
8288
9945
  isFullscreen
8289
9946
  ? 'bg-cyan-500/15 text-cyan-300 font-medium hover:bg-cyan-500/20 active:bg-cyan-500/25'
8290
9947
  : 'hover:bg-cyan-500/5 active:bg-cyan-500/15'
8291
9948
  }${chromePopping === 'fullscreen' ? ' anet-chrome-pop' : ''}`}
9949
+ data-topo-chrome-fullscreen-hover-lift="true"
8292
9950
  style={{
8293
9951
  borderColor: pal.containerBorder,
8294
9952
  ...(isFullscreen
@@ -8303,12 +9961,20 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
8303
9961
  at the reset icon above. data-topo-chrome-fullscreen-
8304
9962
  icon attribute exposes BOTH variants (entered / exited)
8305
9963
  for the round's stroke-width regression probe. */}
9964
+ {/* Round 353 / Loop: fullscreen icon (both enter + exit
9965
+ variants) picks up the R352 family group-hover:scale-110.
9966
+ Pre-R353 hovering the button only changed the bg; the
9967
+ icon stayed still. R353 lifts the icon 10 % on hover —
9968
+ same gesture vocabulary as the zoom buttons. transform-
9969
+ gpu hint promotes the svg to its own compositor layer
9970
+ for crisper edges during the scale tween. Closes the
9971
+ chrome-strip per-icon hover-affordance arc. */}
8306
9972
  {isFullscreen ? (
8307
- <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">
9973
+ <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">
8308
9974
  <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" />
8309
9975
  </svg>
8310
9976
  ) : (
8311
- <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">
9977
+ <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">
8312
9978
  <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" />
8313
9979
  </svg>
8314
9980
  )}