@sleep2agi/agent-network-dashboard 0.5.3-preview.6 → 0.5.3-preview.8
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.
- package/.next/BUILD_ID +1 -1
- package/.next/build-manifest.json +3 -3
- package/.next/diagnostics/route-bundle-stats.json +5 -5
- package/.next/fallback-build-manifest.json +3 -3
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +2 -2
- package/.next/server/app/_not-found.rsc +2 -2
- package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/admin/page_client-reference-manifest.js +1 -1
- package/.next/server/app/admin.html +2 -2
- package/.next/server/app/admin.rsc +2 -2
- package/.next/server/app/admin.segments/_full.segment.rsc +2 -2
- package/.next/server/app/admin.segments/_head.segment.rsc +1 -1
- package/.next/server/app/admin.segments/_index.segment.rsc +2 -2
- package/.next/server/app/admin.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/admin.segments/admin.segment.rsc +1 -1
- package/.next/server/app/index.html +2 -2
- package/.next/server/app/index.rsc +3 -3
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/index.segments/_full.segment.rsc +3 -3
- package/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/.next/server/app/index.segments/_index.segment.rsc +2 -2
- package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/.next/server/app/login.html +2 -2
- package/.next/server/app/login.rsc +3 -3
- package/.next/server/app/login.segments/_full.segment.rsc +3 -3
- package/.next/server/app/login.segments/_head.segment.rsc +1 -1
- package/.next/server/app/login.segments/_index.segment.rsc +2 -2
- package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/login.segments/login.segment.rsc +1 -1
- package/.next/server/app/logs/page_client-reference-manifest.js +1 -1
- package/.next/server/app/logs.html +2 -2
- package/.next/server/app/logs.rsc +2 -2
- package/.next/server/app/logs.segments/_full.segment.rsc +2 -2
- package/.next/server/app/logs.segments/_head.segment.rsc +1 -1
- package/.next/server/app/logs.segments/_index.segment.rsc +2 -2
- package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/logs.segments/logs.segment.rsc +1 -1
- package/.next/server/app/messages/page_client-reference-manifest.js +1 -1
- package/.next/server/app/messages.html +2 -2
- package/.next/server/app/messages.rsc +2 -2
- package/.next/server/app/messages.segments/_full.segment.rsc +2 -2
- package/.next/server/app/messages.segments/_head.segment.rsc +1 -1
- package/.next/server/app/messages.segments/_index.segment.rsc +2 -2
- package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/messages.segments/messages.segment.rsc +1 -1
- package/.next/server/app/node/page_client-reference-manifest.js +1 -1
- package/.next/server/app/node.html +2 -2
- package/.next/server/app/node.rsc +2 -2
- package/.next/server/app/node.segments/_full.segment.rsc +2 -2
- package/.next/server/app/node.segments/_head.segment.rsc +1 -1
- package/.next/server/app/node.segments/_index.segment.rsc +2 -2
- package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/node.segments/node.segment.rsc +1 -1
- package/.next/server/app/nodes/page_client-reference-manifest.js +1 -1
- package/.next/server/app/nodes.html +2 -2
- package/.next/server/app/nodes.rsc +2 -2
- package/.next/server/app/nodes.segments/_full.segment.rsc +2 -2
- package/.next/server/app/nodes.segments/_head.segment.rsc +1 -1
- package/.next/server/app/nodes.segments/_index.segment.rsc +2 -2
- package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/nodes.segments/nodes.segment.rsc +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
- package/.next/server/app/server-logs.html +2 -2
- package/.next/server/app/server-logs.rsc +2 -2
- package/.next/server/app/server-logs.segments/_full.segment.rsc +2 -2
- package/.next/server/app/server-logs.segments/_head.segment.rsc +1 -1
- package/.next/server/app/server-logs.segments/_index.segment.rsc +2 -2
- package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/server-logs.segments/server-logs.segment.rsc +1 -1
- package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/networks.html +2 -2
- package/.next/server/app/settings/networks.rsc +2 -2
- package/.next/server/app/settings/networks.segments/_full.segment.rsc +2 -2
- package/.next/server/app/settings/networks.segments/_head.segment.rsc +1 -1
- package/.next/server/app/settings/networks.segments/_index.segment.rsc +2 -2
- package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +1 -1
- package/.next/server/app/settings/networks.segments/settings.segment.rsc +1 -1
- package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/tokens.html +2 -2
- package/.next/server/app/settings/tokens.rsc +2 -2
- package/.next/server/app/settings/tokens.segments/_full.segment.rsc +2 -2
- package/.next/server/app/settings/tokens.segments/_head.segment.rsc +1 -1
- package/.next/server/app/settings/tokens.segments/_index.segment.rsc +2 -2
- package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +1 -1
- package/.next/server/app/settings/tokens.segments/settings.segment.rsc +1 -1
- package/.next/server/app/settings.html +2 -2
- package/.next/server/app/settings.rsc +3 -3
- package/.next/server/app/settings.segments/_full.segment.rsc +3 -3
- package/.next/server/app/settings.segments/_head.segment.rsc +1 -1
- package/.next/server/app/settings.segments/_index.segment.rsc +2 -2
- package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/settings.segments/settings.segment.rsc +1 -1
- package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
- package/.next/server/app/tasks.html +2 -2
- package/.next/server/app/tasks.rsc +2 -2
- package/.next/server/app/tasks.segments/_full.segment.rsc +2 -2
- package/.next/server/app/tasks.segments/_head.segment.rsc +1 -1
- package/.next/server/app/tasks.segments/_index.segment.rsc +2 -2
- package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/tasks.segments/tasks.segment.rsc +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0sv~g.o._.js +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0sv~g.o._.js.map +1 -1
- package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js +2 -2
- package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js.map +1 -1
- package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js +1 -1
- package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js.map +1 -1
- package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js +1 -1
- package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js.map +1 -1
- package/.next/server/middleware-build-manifest.js +3 -3
- package/.next/server/pages/404.html +2 -2
- package/.next/server/pages/500.html +1 -1
- package/.next/static/chunks/{16qx24lc72~7v.js → 0200cxbmk961-.js} +1 -1
- package/.next/static/chunks/{0ic678xqvd4ys.js → 07fkn2-8-6ejj.js} +2 -2
- package/.next/static/chunks/0hndl9yzpqajt.css +2 -0
- package/.next/static/chunks/{17v63m4g4.i5h.js → 0nvcaq5be21x_.js} +1 -1
- package/.next/static/chunks/{0swbhc-5l4rz9.js → 0~r4zc7qlx96l.js} +1 -1
- package/.next/trace +2 -2
- package/.next/trace-build +1 -1
- package/app/components/TopoGraph.tsx +38 -6
- package/package.json +1 -1
- package/scripts/topo-chip-row-press-test.mjs +93 -0
- package/scripts/topo-filter-pills-press-test.mjs +96 -0
- package/.next/static/chunks/018~fceya_6uk.css +0 -2
- /package/.next/static/{FpDDygQl1AAn4qwXBn3mt → iEkfox1TCVhnJmt_k5SoD}/_buildManifest.js +0 -0
- /package/.next/static/{FpDDygQl1AAn4qwXBn3mt → iEkfox1TCVhnJmt_k5SoD}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{FpDDygQl1AAn4qwXBn3mt → iEkfox1TCVhnJmt_k5SoD}/_ssgManifest.js +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../agent-network-dashboard/node_modules/styled-jsx/dist/index/index.js","../../../../../agent-network-dashboard/node_modules/styled-jsx/style.js","../../../../../agent-network-dashboard/app/page.tsx","../../../../../agent-network-dashboard/app/components/StatsBar.tsx","../../../../../agent-network-dashboard/app/components/BroadcastBar.tsx","../../../../../agent-network-dashboard/app/components/TopoGraph.tsx","../../../../../agent-network-dashboard/app/components/ChatPopover.tsx","../../../../../agent-network-dashboard/app/lib/vendorIdentity.ts","../../../../../agent-network-dashboard/app/components/AgentCard.tsx","../../../../../agent-network-dashboard/app/components/InboxPanel.tsx","../../../../../agent-network-dashboard/app/components/LoadingSkeleton.tsx","../../../../../agent-network-dashboard/app/components/UserBar.tsx","../../../../../agent-network-dashboard/app/components/CommandCenter.tsx","../../../../../agent-network-dashboard/app/components/DispatchPanel.tsx"],"sourcesContent":["require('client-only');\nvar React = require('react');\n\nfunction _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }\n\nvar React__default = /*#__PURE__*/_interopDefaultLegacy(React);\n\n/*\nBased on Glamor's sheet\nhttps://github.com/threepointone/glamor/blob/667b480d31b3721a905021b26e1290ce92ca2879/src/sheet.js\n*/ function _defineProperties(target, props) {\n for(var i = 0; i < props.length; i++){\n var descriptor = props[i];\n descriptor.enumerable = descriptor.enumerable || false;\n descriptor.configurable = true;\n if (\"value\" in descriptor) descriptor.writable = true;\n Object.defineProperty(target, descriptor.key, descriptor);\n }\n}\nfunction _createClass(Constructor, protoProps, staticProps) {\n if (protoProps) _defineProperties(Constructor.prototype, protoProps);\n if (staticProps) _defineProperties(Constructor, staticProps);\n return Constructor;\n}\nvar isProd = typeof process !== \"undefined\" && process.env && process.env.NODE_ENV === \"production\";\nvar isString = function(o) {\n return Object.prototype.toString.call(o) === \"[object String]\";\n};\nvar StyleSheet = /*#__PURE__*/ function() {\n function StyleSheet(param) {\n var ref = param === void 0 ? {} : param, _name = ref.name, name = _name === void 0 ? \"stylesheet\" : _name, _optimizeForSpeed = ref.optimizeForSpeed, optimizeForSpeed = _optimizeForSpeed === void 0 ? isProd : _optimizeForSpeed;\n invariant$1(isString(name), \"`name` must be a string\");\n this._name = name;\n this._deletedRulePlaceholder = \"#\" + name + \"-deleted-rule____{}\";\n invariant$1(typeof optimizeForSpeed === \"boolean\", \"`optimizeForSpeed` must be a boolean\");\n this._optimizeForSpeed = optimizeForSpeed;\n this._serverSheet = undefined;\n this._tags = [];\n this._injected = false;\n this._rulesCount = 0;\n var node = typeof window !== \"undefined\" && document.querySelector('meta[property=\"csp-nonce\"]');\n this._nonce = node ? node.getAttribute(\"content\") : null;\n }\n var _proto = StyleSheet.prototype;\n _proto.setOptimizeForSpeed = function setOptimizeForSpeed(bool) {\n invariant$1(typeof bool === \"boolean\", \"`setOptimizeForSpeed` accepts a boolean\");\n invariant$1(this._rulesCount === 0, \"optimizeForSpeed cannot be when rules have already been inserted\");\n this.flush();\n this._optimizeForSpeed = bool;\n this.inject();\n };\n _proto.isOptimizeForSpeed = function isOptimizeForSpeed() {\n return this._optimizeForSpeed;\n };\n _proto.inject = function inject() {\n var _this = this;\n invariant$1(!this._injected, \"sheet already injected\");\n this._injected = true;\n if (typeof window !== \"undefined\" && this._optimizeForSpeed) {\n this._tags[0] = this.makeStyleTag(this._name);\n this._optimizeForSpeed = \"insertRule\" in this.getSheet();\n if (!this._optimizeForSpeed) {\n if (!isProd) {\n console.warn(\"StyleSheet: optimizeForSpeed mode not supported falling back to standard mode.\");\n }\n this.flush();\n this._injected = true;\n }\n return;\n }\n this._serverSheet = {\n cssRules: [],\n insertRule: function(rule, index) {\n if (typeof index === \"number\") {\n _this._serverSheet.cssRules[index] = {\n cssText: rule\n };\n } else {\n _this._serverSheet.cssRules.push({\n cssText: rule\n });\n }\n return index;\n },\n deleteRule: function(index) {\n _this._serverSheet.cssRules[index] = null;\n }\n };\n };\n _proto.getSheetForTag = function getSheetForTag(tag) {\n if (tag.sheet) {\n return tag.sheet;\n }\n // this weirdness brought to you by firefox\n for(var i = 0; i < document.styleSheets.length; i++){\n if (document.styleSheets[i].ownerNode === tag) {\n return document.styleSheets[i];\n }\n }\n };\n _proto.getSheet = function getSheet() {\n return this.getSheetForTag(this._tags[this._tags.length - 1]);\n };\n _proto.insertRule = function insertRule(rule, index) {\n invariant$1(isString(rule), \"`insertRule` accepts only strings\");\n if (typeof window === \"undefined\") {\n if (typeof index !== \"number\") {\n index = this._serverSheet.cssRules.length;\n }\n this._serverSheet.insertRule(rule, index);\n return this._rulesCount++;\n }\n if (this._optimizeForSpeed) {\n var sheet = this.getSheet();\n if (typeof index !== \"number\") {\n index = sheet.cssRules.length;\n }\n // this weirdness for perf, and chrome's weird bug\n // https://stackoverflow.com/questions/20007992/chrome-suddenly-stopped-accepting-insertrule\n try {\n sheet.insertRule(rule, index);\n } catch (error) {\n if (!isProd) {\n console.warn(\"StyleSheet: illegal rule: \\n\\n\" + rule + \"\\n\\nSee https://stackoverflow.com/q/20007992 for more info\");\n }\n return -1;\n }\n } else {\n var insertionPoint = this._tags[index];\n this._tags.push(this.makeStyleTag(this._name, rule, insertionPoint));\n }\n return this._rulesCount++;\n };\n _proto.replaceRule = function replaceRule(index, rule) {\n if (this._optimizeForSpeed || typeof window === \"undefined\") {\n var sheet = typeof window !== \"undefined\" ? this.getSheet() : this._serverSheet;\n if (!rule.trim()) {\n rule = this._deletedRulePlaceholder;\n }\n if (!sheet.cssRules[index]) {\n // @TBD Should we throw an error?\n return index;\n }\n sheet.deleteRule(index);\n try {\n sheet.insertRule(rule, index);\n } catch (error) {\n if (!isProd) {\n console.warn(\"StyleSheet: illegal rule: \\n\\n\" + rule + \"\\n\\nSee https://stackoverflow.com/q/20007992 for more info\");\n }\n // In order to preserve the indices we insert a deleteRulePlaceholder\n sheet.insertRule(this._deletedRulePlaceholder, index);\n }\n } else {\n var tag = this._tags[index];\n invariant$1(tag, \"old rule at index `\" + index + \"` not found\");\n tag.textContent = rule;\n }\n return index;\n };\n _proto.deleteRule = function deleteRule(index) {\n if (typeof window === \"undefined\") {\n this._serverSheet.deleteRule(index);\n return;\n }\n if (this._optimizeForSpeed) {\n this.replaceRule(index, \"\");\n } else {\n var tag = this._tags[index];\n invariant$1(tag, \"rule at index `\" + index + \"` not found\");\n tag.parentNode.removeChild(tag);\n this._tags[index] = null;\n }\n };\n _proto.flush = function flush() {\n this._injected = false;\n this._rulesCount = 0;\n if (typeof window !== \"undefined\") {\n this._tags.forEach(function(tag) {\n return tag && tag.parentNode.removeChild(tag);\n });\n this._tags = [];\n } else {\n // simpler on server\n this._serverSheet.cssRules = [];\n }\n };\n _proto.cssRules = function cssRules() {\n var _this = this;\n if (typeof window === \"undefined\") {\n return this._serverSheet.cssRules;\n }\n return this._tags.reduce(function(rules, tag) {\n if (tag) {\n rules = rules.concat(Array.prototype.map.call(_this.getSheetForTag(tag).cssRules, function(rule) {\n return rule.cssText === _this._deletedRulePlaceholder ? null : rule;\n }));\n } else {\n rules.push(null);\n }\n return rules;\n }, []);\n };\n _proto.makeStyleTag = function makeStyleTag(name, cssString, relativeToTag) {\n if (cssString) {\n invariant$1(isString(cssString), \"makeStyleTag accepts only strings as second parameter\");\n }\n var tag = document.createElement(\"style\");\n if (this._nonce) tag.setAttribute(\"nonce\", this._nonce);\n tag.type = \"text/css\";\n tag.setAttribute(\"data-\" + name, \"\");\n if (cssString) {\n tag.appendChild(document.createTextNode(cssString));\n }\n var head = document.head || document.getElementsByTagName(\"head\")[0];\n if (relativeToTag) {\n head.insertBefore(tag, relativeToTag);\n } else {\n head.appendChild(tag);\n }\n return tag;\n };\n _createClass(StyleSheet, [\n {\n key: \"length\",\n get: function get() {\n return this._rulesCount;\n }\n }\n ]);\n return StyleSheet;\n}();\nfunction invariant$1(condition, message) {\n if (!condition) {\n throw new Error(\"StyleSheet: \" + message + \".\");\n }\n}\n\nfunction hash(str) {\n var _$hash = 5381, i = str.length;\n while(i){\n _$hash = _$hash * 33 ^ str.charCodeAt(--i);\n }\n /* JavaScript does bitwise operations (like XOR, above) on 32-bit signed\n * integers. Since we want the results to be always positive, convert the\n * signed int to an unsigned by doing an unsigned bitshift. */ return _$hash >>> 0;\n}\nvar stringHash = hash;\n\nvar sanitize = function(rule) {\n return rule.replace(/\\/style/gi, \"\\\\/style\");\n};\nvar cache = {};\n/**\n * computeId\n *\n * Compute and memoize a jsx id from a basedId and optionally props.\n */ function computeId(baseId, props) {\n if (!props) {\n return \"jsx-\" + baseId;\n }\n var propsToString = String(props);\n var key = baseId + propsToString;\n if (!cache[key]) {\n cache[key] = \"jsx-\" + stringHash(baseId + \"-\" + propsToString);\n }\n return cache[key];\n}\n/**\n * computeSelector\n *\n * Compute and memoize dynamic selectors.\n */ function computeSelector(id, css) {\n var selectoPlaceholderRegexp = /__jsx-style-dynamic-selector/g;\n // Sanitize SSR-ed CSS.\n // Client side code doesn't need to be sanitized since we use\n // document.createTextNode (dev) and the CSSOM api sheet.insertRule (prod).\n if (typeof window === \"undefined\") {\n css = sanitize(css);\n }\n var idcss = id + css;\n if (!cache[idcss]) {\n cache[idcss] = css.replace(selectoPlaceholderRegexp, id);\n }\n return cache[idcss];\n}\n\nfunction mapRulesToStyle(cssRules, options) {\n if (options === void 0) options = {};\n return cssRules.map(function(args) {\n var id = args[0];\n var css = args[1];\n return /*#__PURE__*/ React__default[\"default\"].createElement(\"style\", {\n id: \"__\" + id,\n // Avoid warnings upon render with a key\n key: \"__\" + id,\n nonce: options.nonce ? options.nonce : undefined,\n dangerouslySetInnerHTML: {\n __html: css\n }\n });\n });\n}\nvar StyleSheetRegistry = /*#__PURE__*/ function() {\n function StyleSheetRegistry(param) {\n var ref = param === void 0 ? {} : param, _styleSheet = ref.styleSheet, styleSheet = _styleSheet === void 0 ? null : _styleSheet, _optimizeForSpeed = ref.optimizeForSpeed, optimizeForSpeed = _optimizeForSpeed === void 0 ? false : _optimizeForSpeed;\n this._sheet = styleSheet || new StyleSheet({\n name: \"styled-jsx\",\n optimizeForSpeed: optimizeForSpeed\n });\n this._sheet.inject();\n if (styleSheet && typeof optimizeForSpeed === \"boolean\") {\n this._sheet.setOptimizeForSpeed(optimizeForSpeed);\n this._optimizeForSpeed = this._sheet.isOptimizeForSpeed();\n }\n this._fromServer = undefined;\n this._indices = {};\n this._instancesCounts = {};\n }\n var _proto = StyleSheetRegistry.prototype;\n _proto.add = function add(props) {\n var _this = this;\n if (undefined === this._optimizeForSpeed) {\n this._optimizeForSpeed = Array.isArray(props.children);\n this._sheet.setOptimizeForSpeed(this._optimizeForSpeed);\n this._optimizeForSpeed = this._sheet.isOptimizeForSpeed();\n }\n if (typeof window !== \"undefined\" && !this._fromServer) {\n this._fromServer = this.selectFromServer();\n this._instancesCounts = Object.keys(this._fromServer).reduce(function(acc, tagName) {\n acc[tagName] = 0;\n return acc;\n }, {});\n }\n var ref = this.getIdAndRules(props), styleId = ref.styleId, rules = ref.rules;\n // Deduping: just increase the instances count.\n if (styleId in this._instancesCounts) {\n this._instancesCounts[styleId] += 1;\n return;\n }\n var indices = rules.map(function(rule) {\n return _this._sheet.insertRule(rule);\n })// Filter out invalid rules\n .filter(function(index) {\n return index !== -1;\n });\n this._indices[styleId] = indices;\n this._instancesCounts[styleId] = 1;\n };\n _proto.remove = function remove(props) {\n var _this = this;\n var styleId = this.getIdAndRules(props).styleId;\n invariant(styleId in this._instancesCounts, \"styleId: `\" + styleId + \"` not found\");\n this._instancesCounts[styleId] -= 1;\n if (this._instancesCounts[styleId] < 1) {\n var tagFromServer = this._fromServer && this._fromServer[styleId];\n if (tagFromServer) {\n tagFromServer.parentNode.removeChild(tagFromServer);\n delete this._fromServer[styleId];\n } else {\n this._indices[styleId].forEach(function(index) {\n return _this._sheet.deleteRule(index);\n });\n delete this._indices[styleId];\n }\n delete this._instancesCounts[styleId];\n }\n };\n _proto.update = function update(props, nextProps) {\n this.add(nextProps);\n this.remove(props);\n };\n _proto.flush = function flush() {\n this._sheet.flush();\n this._sheet.inject();\n this._fromServer = undefined;\n this._indices = {};\n this._instancesCounts = {};\n };\n _proto.cssRules = function cssRules() {\n var _this = this;\n var fromServer = this._fromServer ? Object.keys(this._fromServer).map(function(styleId) {\n return [\n styleId,\n _this._fromServer[styleId]\n ];\n }) : [];\n var cssRules = this._sheet.cssRules();\n return fromServer.concat(Object.keys(this._indices).map(function(styleId) {\n return [\n styleId,\n _this._indices[styleId].map(function(index) {\n return cssRules[index].cssText;\n }).join(_this._optimizeForSpeed ? \"\" : \"\\n\")\n ];\n })// filter out empty rules\n .filter(function(rule) {\n return Boolean(rule[1]);\n }));\n };\n _proto.styles = function styles(options) {\n return mapRulesToStyle(this.cssRules(), options);\n };\n _proto.getIdAndRules = function getIdAndRules(props) {\n var css = props.children, dynamic = props.dynamic, id = props.id;\n if (dynamic) {\n var styleId = computeId(id, dynamic);\n return {\n styleId: styleId,\n rules: Array.isArray(css) ? css.map(function(rule) {\n return computeSelector(styleId, rule);\n }) : [\n computeSelector(styleId, css)\n ]\n };\n }\n return {\n styleId: computeId(id),\n rules: Array.isArray(css) ? css : [\n css\n ]\n };\n };\n /**\n * selectFromServer\n *\n * Collects style tags from the document with id __jsx-XXX\n */ _proto.selectFromServer = function selectFromServer() {\n var elements = Array.prototype.slice.call(document.querySelectorAll('[id^=\"__jsx-\"]'));\n return elements.reduce(function(acc, element) {\n var id = element.id.slice(2);\n acc[id] = element;\n return acc;\n }, {});\n };\n return StyleSheetRegistry;\n}();\nfunction invariant(condition, message) {\n if (!condition) {\n throw new Error(\"StyleSheetRegistry: \" + message + \".\");\n }\n}\nvar StyleSheetContext = /*#__PURE__*/ React.createContext(null);\nStyleSheetContext.displayName = \"StyleSheetContext\";\nfunction createStyleRegistry() {\n return new StyleSheetRegistry();\n}\nfunction StyleRegistry(param) {\n var configuredRegistry = param.registry, children = param.children;\n var rootRegistry = React.useContext(StyleSheetContext);\n var ref = React.useState(function() {\n return rootRegistry || configuredRegistry || createStyleRegistry();\n }), registry = ref[0];\n return /*#__PURE__*/ React__default[\"default\"].createElement(StyleSheetContext.Provider, {\n value: registry\n }, children);\n}\nfunction useStyleRegistry() {\n return React.useContext(StyleSheetContext);\n}\n\n// Opt-into the new `useInsertionEffect` API in React 18, fallback to `useLayoutEffect`.\n// https://github.com/reactwg/react-18/discussions/110\nvar useInsertionEffect = React__default[\"default\"].useInsertionEffect || React__default[\"default\"].useLayoutEffect;\nvar defaultRegistry = typeof window !== \"undefined\" ? createStyleRegistry() : undefined;\nfunction JSXStyle(props) {\n var registry = defaultRegistry ? defaultRegistry : useStyleRegistry();\n // If `registry` does not exist, we do nothing here.\n if (!registry) {\n return null;\n }\n if (typeof window === \"undefined\") {\n registry.add(props);\n return null;\n }\n useInsertionEffect(function() {\n registry.add(props);\n return function() {\n registry.remove(props);\n };\n // props.children can be string[], will be striped since id is identical\n }, [\n props.id,\n String(props.dynamic)\n ]);\n return null;\n}\nJSXStyle.dynamic = function(info) {\n return info.map(function(tagInfo) {\n var baseId = tagInfo[0];\n var props = tagInfo[1];\n return computeId(baseId, props);\n }).join(\" \");\n};\n\nexports.StyleRegistry = StyleRegistry;\nexports.createStyleRegistry = createStyleRegistry;\nexports.style = JSXStyle;\nexports.useStyleRegistry = useStyleRegistry;\n","module.exports = require('./dist/index').style\n","'use client';\n\nimport { useEffect, useState } from 'react';\nimport Link from 'next/link';\nimport { formatUptime, previewContent } from './components/utils';\nimport { StatsBar } from './components/StatsBar';\nimport { BroadcastBar } from './components/BroadcastBar';\nimport { TopoGraph } from './components/TopoGraph';\nimport { AgentCard } from './components/AgentCard';\nimport { InboxPanel } from './components/InboxPanel';\nimport { LoadingSkeleton } from './components/LoadingSkeleton';\nimport { NodesEmptyState as EmptyState } from './components/EmptyState';\nimport { AliasAvatar } from './components/AliasAvatar';\nimport { STATUS_DOT_HEX, STATUS_CHIP_CLASS } from './lib/status';\nimport { UserBar } from './components/UserBar';\nimport { CommandCenter, useCommandCenter } from './components/CommandCenter';\nimport { DispatchPanel } from './components/DispatchPanel';\nimport { useSessions, useHealth, useAnetConfig, useTasks, useStats } from './lib/hooks';\nimport { useSSE } from './lib/useSSE';\nimport { InboxMessage } from './components/types';\nimport { useSWRConfig } from 'swr';\n\nexport default function Dashboard() {\n // Auto-upgrade: if no V3 auth in session, force re-login to get user token\n useEffect(() => {\n const hasV3 = sessionStorage.getItem('anet_v3_auth');\n if (!hasV3) {\n // Try silent re-auth: logout old cookie + redirect to login\n fetch('/api/auth/logout', { method: 'POST' }).catch(() => {});\n window.location.assign('/login');\n }\n }, []);\n\n const { sessions, hint: sessHint, error: sessError, isLoading } = useSessions();\n const { health } = useHealth();\n const { config: anetConfig } = useAnetConfig();\n const { tasks } = useTasks({ limit: '500' });\n const { stats } = useStats();\n const [showTopo, setShowTopo] = useState(typeof window !== 'undefined' && window.innerWidth >= 1024);\n const [showConfig, setShowConfig] = useState(false);\n const cmd = useCommandCenter();\n const [showDispatch, setShowDispatch] = useState(false);\n const [inbox, setInbox] = useState<InboxMessage[]>([]);\n const [agentFilter, setAgentFilter] = useState<'all' | 'working' | 'idle' | 'offline'>('all');\n // #84: last node.renamed event — passed to TopoGraph so an open chat\n // popover follows the rename instead of pointing at a dead alias. `ts`\n // makes the effect re-fire even when the same from/to repeats.\n const [renameSignal, setRenameSignal] = useState<{ from: string; to: string; ts: number } | null>(null);\n const { mutate } = useSWRConfig();\n\n // SSE: instant revalidation on CommHub events\n // Opt-out via NEXT_PUBLIC_DISABLE_SSE=1 — avoids HTTP/1.1 head-of-line blocking on navigation\n const sseEnabled = process.env.NEXT_PUBLIC_DISABLE_SSE !== '1';\n const { connected: sseConnected, supported: sseSupported } = useSSE({\n url: '/api/hub/events',\n enabled: sseEnabled,\n onEvent: (event) => {\n // #84 (RFC-010 §3.4): node.renamed — revalidate the session list so the\n // new alias propagates instantly (TopoGraph + node grid re-render, the\n // avatar hue recomputes as a pure fn of alias) instead of waiting for\n // the next 5s poll. The renamed node's history keeps the old alias\n // server-side, so the task list needs no revalidation here.\n if (event.type === 'node.renamed') {\n mutate('/api/hub/status');\n const d = event.data as { old_alias?: string; new_alias?: string } | undefined;\n if (d?.old_alias && d?.new_alias) {\n setRenameSignal({ from: d.old_alias, to: d.new_alias, ts: Date.now() });\n }\n return;\n }\n if (['new_task', 'new_message', 'new_reply', 'node_status_changed', 'broadcast'].includes(event.type)) {\n mutate('/api/hub/status');\n mutate((key: string) => typeof key === 'string' && key.startsWith('/api/hub/tasks'), undefined, { revalidate: true });\n }\n },\n });\n\n // Fetch inbox (not in SWR since it accumulates)\n useEffect(() => {\n const fetchInbox = () => {\n fetch('/api/hub/inbox').then(r => r.json()).then(data => {\n if (data.messages?.length) setInbox(prev => {\n const ids = new Set(prev.map(m => m.id));\n const newMsgs = data.messages.filter((m: { id: string }) => !ids.has(m.id));\n return [...newMsgs, ...prev].slice(0, 100);\n });\n }).catch(() => {});\n };\n fetchInbox();\n const interval = setInterval(fetchInbox, 10000);\n return () => clearInterval(interval);\n }, []);\n\n if (isLoading) return <LoadingSkeleton />;\n\n const sseSessions = health?.sse_sessions || {};\n // SSE keys are `network_id:alias` since v0.7+ (per-network scoping).\n // Fall back to alias-only for legacy hubs.\n const sseLookup = (s: { alias: string; network_id?: string }) =>\n (s.network_id ? sseSessions[`${s.network_id}:${s.alias}`] : undefined) ?? sseSessions[s.alias];\n // Online = status is not 'offline' (not just SSE-connected)\n const isOnline = (s: { alias: string; status: string; network_id?: string }) => s.status !== 'offline' || !!sseLookup(s);\n const online = sessions.filter(isOnline).length;\n const total = sessions.length;\n const working = sessions.filter(s => s.status === 'working').length;\n const uptime = health ? formatUptime(health.uptime) : '--';\n const version = health?.version || '--';\n const configHealthy = Boolean(anetConfig?.hub && anetConfig.tokenConfigured);\n const configSourceLabel =\n anetConfig?.source === 'file' ? 'Local config'\n : anetConfig?.source === 'runtime-env' ? 'Runtime env'\n : 'Config missing';\n\n // Task stats: prefer /api/stats, fallback to manual\n const taskStats: Record<string, number> = {};\n if (stats?.tasks?.by_status?.length) {\n for (const s of stats.tasks.by_status) {\n taskStats[s.status] = s.count;\n }\n } else {\n for (const t of tasks) {\n taskStats[t.status] = (taskStats[t.status] || 0) + 1;\n }\n }\n\n const sortedSessions = [...sessions].sort((a, b) => {\n const aOnline = isOnline(a) ? 1 : 0;\n const bOnline = isOnline(b) ? 1 : 0;\n if (aOnline !== bOnline) return bOnline - aOnline;\n const aWorking = a.status === 'working' ? 1 : 0;\n const bWorking = b.status === 'working' ? 1 : 0;\n return bWorking - aWorking;\n });\n\n /** Round 70: when the fleet is empty, the Overview reorders to lead with\n * the \"Spin up your first agent\" CTA and hides the Quick Navigation /\n * Nav rail / Broadcast bar that would otherwise occupy prime real estate\n * with zeros and dead-end links. Computed once, used as a gate below. */\n const fleetEmpty = sessions.length === 0 && !sessError;\n\n return (\n <div className=\"min-h-screen bg-[#0a0a1a] text-gray-100 p-4 sm:p-6 font-mono\">\n <div className=\"lg:ml-0 ml-10\">\n <StatsBar online={online} working={working} total={total} version={version} uptime={uptime} />\n </div>\n\n {/* Dispatch + User Bar — Dispatch hidden when fleet empty (nothing to\n dispatch to); UserBar still useful (account/sign-out menu). */}\n <div className=\"flex items-center gap-3 mb-4\">\n {!fleetEmpty && (\n <button onClick={() => setShowDispatch(true)}\n className=\"px-4 py-2 bg-gradient-to-r from-cyan-600 to-blue-600 hover:from-cyan-500 hover:to-blue-500 text-white text-sm font-medium rounded-xl shadow-lg shadow-cyan-500/10 transition-all active:scale-95 shrink-0\">\n ⚡ Dispatch\n </button>\n )}\n <div className=\"flex-1\"><UserBar /></div>\n </div>\n\n {/* anet config (collapsed by default) */}\n <section className=\"mb-6 rounded-lg border border-[#2a2a4a] bg-[#111128] px-4 py-3 shadow-lg shadow-black/20\">\n <button onClick={() => setShowConfig(!showConfig)} className=\"w-full flex items-center justify-between text-left\">\n <div className=\"flex items-center gap-2 text-xs\">\n <span className=\"uppercase text-gray-600\">Config</span>\n <span className={`w-2 h-2 rounded-full ${configHealthy ? 'bg-green-400' : 'bg-red-400'}`} />\n <span className=\"text-gray-500\">{configSourceLabel}</span>\n </div>\n <svg className={`w-4 h-4 text-gray-600 transition-transform ${showConfig ? 'rotate-180' : ''}`} fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" strokeWidth={2}>\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M19 9l-7 7-7-7\" />\n </svg>\n </button>\n {showConfig && (\n <div className=\"mt-3 pt-3 border-t border-[#2a2a4a]\">\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"min-w-0\">\n <div className=\"text-gray-100 truncate text-sm\" title={anetConfig?.hub || undefined}>\n Hub: <span className={anetConfig?.hub ? 'text-cyan-300' : 'text-red-300'}>{anetConfig?.hub?.trim() || 'not configured'}</span>\n </div>\n </div>\n <div className=\"flex flex-wrap gap-2 text-xs\">\n <span className={`px-2.5 py-1 rounded-md border ${anetConfig?.tokenConfigured ? 'bg-blue-500/5 text-blue-300 border-blue-500/20' : 'bg-gray-500/5 text-gray-400 border-gray-500/20'}`}>\n Token: {anetConfig?.tokenPreview || 'not configured'}\n </span>\n </div>\n </div>\n {anetConfig?.error && <div className=\"mt-2 text-xs text-gray-600\">{anetConfig.error}</div>}\n </div>\n )}\n </section>\n\n {/* Task Status Stats */}\n {Object.keys(taskStats).length > 0 && (\n <section className=\"mb-6 rounded-lg border border-[#2a2a4a] bg-[#111128] px-4 py-3 shadow-lg shadow-black/20\">\n <div className=\"flex items-center justify-between mb-2\">\n <div className=\"text-xs uppercase text-gray-600\">Task Status</div>\n <Link href=\"/tasks\" prefetch={false} className=\"text-xs text-cyan-400 hover:text-cyan-300\">View all →</Link>\n </div>\n <div className=\"flex flex-wrap gap-2\">\n {/* Round 69: order is \"hot first\" — active flow before terminal\n states — intentionally different from the lifecycle order on\n /tasks. Colors come from shared STATUS_CHIP_CLASS so a\n palette tweak in app/lib/status.ts updates here too.\n 'created' added in r69 (was missing — enum-coverage bug). */}\n {(['running', 'delivered', 'acked', 'replied', 'created', 'failed', 'cancelled', 'expired', 'closed'] as const)\n .filter((key) => taskStats[key])\n .map((key) => (\n <Link key={key} href={`/tasks?status=${key}`} prefetch={false} className={`px-2.5 py-1 rounded-md border text-xs ${STATUS_CHIP_CLASS[key]} hover:opacity-80 transition-opacity`}>\n {key}: {taskStats[key]}\n </Link>\n ))}\n </div>\n </section>\n )}\n\n {/* Quick Actions — split into two distinct intents:\n (1) Top: live stat cards (carry data, drill-in on click)\n (2) Bottom: pure nav rail (no number, icon + label)\n Round 24 — wrap both in a labelled block so the rhythm reads as\n \"here are the main jumps\" instead of two disconnected strips.\n Round 70 — entire Quick Nav + Nav rail + Broadcast + Recent\n Activity block is hidden when the fleet is empty so the\n onboarding CTA gets the page above the fold. */}\n {!fleetEmpty && <>\n <div className=\"text-[10px] uppercase tracking-[0.12em] text-gray-600 mb-2\">Quick navigation</div>\n <section className=\"mb-3 grid grid-cols-3 gap-2 sm:gap-3\">\n {(() => {\n // Build breakdown popover content per card. Pure data — pure CSS\n // popover (no JS state) wires the hover-show transition below.\n const idleCount = sessions.filter(s => isOnline(s) && s.status !== 'working').length;\n const offlineCount = total - online;\n const orderedStatuses = ['running', 'replied', 'failed', 'cancelled', 'expired', 'closed', 'created', 'delivered', 'acked'];\n const failedRecent = tasks.filter((t: { status: string }) => t.status === 'failed').length;\n\n const cards = [\n {\n href: '/nodes', label: 'Nodes',\n value: `${online}/${total}`,\n sub: `${total > 0 ? Math.round((online / total) * 100) : 0}% online`,\n color: 'text-green-400 border-green-500/20',\n popover: total === 0 ? null : [\n { k: 'working', v: working, dot: '', color: '#4ade80' },\n { k: 'idle', v: idleCount, dot: '', color: '#22d3ee' },\n { k: 'offline', v: offlineCount, dot: '', color: '#9ca3af' },\n ],\n },\n {\n href: '/tasks', label: 'Tasks',\n value: String(Object.values(taskStats).reduce((a, b) => a + b, 0) || 0),\n sub: 'all-time',\n color: 'text-cyan-400 border-cyan-500/20',\n popover: Object.keys(taskStats).length === 0 ? null\n : orderedStatuses\n .filter(s => taskStats[s])\n .map(s => {\n // Inline hex avoids Tailwind purging dynamic\n // `bg-${color}-400` class names.\n const hex = ({\n running: '#4ade80', replied: '#a78bfa', failed: '#f87171',\n cancelled: '#facc15', expired: '#fb923c', closed: '#9ca3af',\n created: '#9ca3af', delivered: '#60a5fa', acked: '#22d3ee',\n } as Record<string, string>)[s] || '#9ca3af';\n return { k: s, v: taskStats[s], dot: '', color: hex };\n }),\n },\n {\n href: '/tasks?status=failed', label: 'Failed',\n value: String(taskStats['failed'] || 0),\n sub: taskStats['failed'] ? 'needs review' : 'none',\n color: taskStats['failed'] ? 'text-red-400 border-red-500/25' : 'text-gray-500 border-gray-700/30',\n popover: !failedRecent ? [{ k: 'no failures yet', v: '', dot: '', color: '#4b5563' }]\n : [{ k: `${failedRecent} in current view`, v: '', dot: '', color: '#f87171' }],\n },\n ];\n\n return cards.map(a => (\n <Link\n key={a.href}\n href={a.href}\n prefetch={false}\n className={`anet-stat-link group relative rounded-xl border ${a.color} bg-[#111128] px-3 py-3 transition-all hover:-translate-y-px`}\n >\n <div className=\"flex items-baseline justify-between\">\n <div className={`text-xl font-semibold tabular-nums ${a.color.split(' ')[0]}`}>{a.value}</div>\n <div className=\"hidden sm:block text-[10px] text-gray-600 group-hover:text-gray-400 transition-colors\">View →</div>\n </div>\n <div className=\"text-[11px] text-gray-400 mt-0.5\">{a.label}</div>\n <div className=\"text-[10px] text-gray-600 mt-px\">{a.sub}</div>\n\n {/* Hover popover — CSS-only, restrained. Hidden on touch\n (no :hover) so mobile is unaffected. Positioned just\n below the card so it doesn't fight nav rail. */}\n {a.popover && a.popover.length > 0 && (\n <div className=\"anet-stat-popover hidden md:block pointer-events-none absolute left-2 right-2 top-full mt-1 z-20 rounded-lg border border-[#2a2a4a] bg-[#0d0d1a] shadow-lg shadow-black/30 px-3 py-2 opacity-0 translate-y-[-2px] group-hover:opacity-100 group-hover:translate-y-0 transition-all duration-150 delay-100\">\n <div className=\"text-[10px] text-gray-600 uppercase tracking-wider mb-1.5\">Breakdown</div>\n <ul className=\"space-y-1\">\n {a.popover.map(row => (\n <li key={row.k} className=\"flex items-center gap-2 text-[11px]\">\n <span\n className=\"inline-block w-1.5 h-1.5 rounded-full shrink-0\"\n style={{ backgroundColor: row.color }}\n aria-hidden\n />\n <span className=\"text-gray-400 capitalize\">{row.k}</span>\n {row.v !== '' && <span className=\"ml-auto text-gray-300 tabular-nums font-medium\">{row.v}</span>}\n </li>\n ))}\n </ul>\n </div>\n )}\n </Link>\n ));\n })()}\n </section>\n\n {/* Nav rail — pure navigation, icon + label, no data */}\n <section className=\"mb-6 grid grid-cols-3 gap-2 sm:gap-3\">\n {[\n { href: '/messages', label: 'Messages', icon: 'M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z' },\n { href: '/logs', label: 'Audit log', icon: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z M14 2v6h6 M16 13H8 M16 17H8 M10 9H8' },\n { href: '/admin', label: 'Admin', icon: 'M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4z M6 21v-2a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v2' },\n ].map(a => (\n <Link key={a.href} href={a.href} prefetch={false}\n className=\"anet-nav-tile flex items-center justify-center gap-2 rounded-xl border border-[#2a2a4a] bg-[#111128] px-3 py-2.5 text-[12px] text-gray-400 hover:text-gray-200 transition-colors\">\n <svg className=\"w-4 h-4 shrink-0\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n <path d={a.icon} />\n </svg>\n <span>{a.label}</span>\n </Link>\n ))}\n </section>\n\n <BroadcastBar />\n\n {/* Recent Activity */}\n {tasks.length > 0 && (\n <section className=\"mb-6 bg-[#111128] border border-[#2a2a4a] rounded-xl p-4\">\n <div className=\"flex items-center justify-between mb-3\">\n <h2 className=\"text-sm font-semibold text-gray-300\">Recent Activity</h2>\n <Link href=\"/tasks\" className=\"text-xs text-cyan-400 hover:text-cyan-300\">All tasks →</Link>\n </div>\n <div className=\"space-y-2 max-h-40 overflow-y-auto\">\n {tasks.slice(0, 5).map((t: { task_id: string; from_name: string; to_name: string; status: string; content: string; created_at: string }) => (\n <div key={t.task_id} className=\"flex items-center gap-2 text-xs\">\n <span\n className=\"w-1.5 h-1.5 rounded-full shrink-0\"\n style={{ backgroundColor: STATUS_DOT_HEX[t.status] || '#6b7280' }}\n />\n {t.from_name && <AliasAvatar alias={t.from_name} size={14} />}\n <span className=\"text-gray-300 shrink-0 max-w-[20%] truncate\">{t.from_name || '?'}</span>\n <span className=\"text-gray-600\">→</span>\n {t.to_name && <AliasAvatar alias={t.to_name} size={14} />}\n <span className=\"text-gray-300 shrink-0 max-w-[20%] truncate\">{t.to_name || '?'}</span>\n <span className=\"text-gray-500 truncate flex-1\" title={t.content || ''}>{previewContent(t.content).slice(0, 60)}</span>\n <span className={`shrink-0 px-1.5 py-0.5 rounded text-[10px] border ${\n STATUS_CHIP_CLASS[t.status] || 'text-gray-500 border-gray-700/30'\n }`}>{t.status}</span>\n </div>\n ))}\n </div>\n </section>\n )}\n </>}\n\n {sessError && (\n <div className=\"bg-red-900/20 border border-red-800/40 text-red-300 px-4 py-3 rounded-lg mb-6 text-sm flex items-center justify-between\" role=\"alert\">\n <span>{String(sessError)}</span>\n <span className=\"text-gray-500 text-xs\">Check CommHub connection</span>\n </div>\n )}\n\n {sessions.length > 0 && (\n <div className=\"flex justify-center mb-4\">\n <button\n onClick={() => setShowTopo(!showTopo)}\n className=\"text-xs text-gray-500 hover:text-gray-300 border border-gray-700/50 px-4 py-1.5 rounded-lg transition-colors hover:border-gray-600 cursor-pointer\"\n >\n {showTopo ? 'Hide Topology' : 'Show Topology'}\n </button>\n </div>\n )}\n\n {/* Mobile hint when topo hidden */}\n {!showTopo && sessions.length > 0 && (\n <div className=\"lg:hidden text-center text-xs text-gray-600 mb-4\">\n Topology hidden on mobile for better readability\n </div>\n )}\n\n {showTopo && sessions.length > 0 && <TopoGraph sessions={sessions} sseSessions={sseSessions} renameSignal={renameSignal} />}\n\n {sessions.length === 0 && !sessError ? (\n <EmptyState\n hint={sessHint}\n taskHistoryCount={Object.values(taskStats).reduce((a, b) => a + b, 0)}\n />\n ) : (() => {\n const counts = {\n all: sortedSessions.length,\n working: sortedSessions.filter(s => isOnline(s) && s.status === 'working').length,\n idle: sortedSessions.filter(s => isOnline(s) && s.status !== 'working').length,\n offline: sortedSessions.filter(s => !isOnline(s)).length,\n };\n const filtered = sortedSessions.filter(s => {\n if (agentFilter === 'all') return true;\n if (agentFilter === 'offline') return !isOnline(s);\n if (agentFilter === 'working') return isOnline(s) && s.status === 'working';\n if (agentFilter === 'idle') return isOnline(s) && s.status !== 'working';\n return true;\n });\n // Filter chip color keyed to status (round 34): working=green, idle=cyan,\n // offline=gray, all=neutral. Inline hex dots avoid Tailwind v4 purge.\n const chips: { key: typeof agentFilter; label: string; dot?: string }[] = [\n { key: 'all', label: 'All' },\n { key: 'working', label: 'Working', dot: '#4ade80' },\n { key: 'idle', label: 'Idle', dot: '#22d3ee' },\n { key: 'offline', label: 'Offline', dot: '#6b7280' },\n ];\n return (\n <>\n <div className=\"mb-3 flex flex-wrap items-center gap-1\">\n {chips.map(c => (\n <button\n key={c.key}\n type=\"button\"\n onClick={() => setAgentFilter(c.key)}\n disabled={counts[c.key] === 0 && c.key !== 'all'}\n className={`flex items-center gap-1.5 rounded-md px-2.5 py-1 text-xs transition-colors disabled:opacity-30 disabled:cursor-not-allowed ${\n agentFilter === c.key\n ? 'bg-cyan-500/10 text-cyan-300 border border-cyan-500/30'\n : 'text-gray-500 hover:text-gray-200 hover:bg-[#1a1a2a] border border-transparent'\n }`}\n >\n {c.dot && <span aria-hidden className=\"inline-block w-1.5 h-1.5 rounded-full\" style={{ backgroundColor: c.dot }} />}\n <span>{c.label}</span>\n <span className={`text-[10px] tabular-nums ${agentFilter === c.key ? 'text-cyan-400' : 'text-gray-600'}`}>{counts[c.key]}</span>\n </button>\n ))}\n </div>\n {/* Round 48: previous breakpoints had `lg:grid-cols-2 xl:grid-cols-3`\n which kept lg (1024-1279px) at only 2 columns even though\n each AgentCard is fine ≥260px wide. With the sidebar (208px),\n main area at lg is ~816px so 3 cols at ~272px each fits.\n xl breakpoint auto-inherits lg=3 cols; 2xl bumps to 4. */}\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-3 sm:gap-4\">\n {filtered.map(s => (\n <AgentCard key={s.alias} session={s} hasSse={isOnline(s)} sseCount={sseLookup(s) || 0} onChat={cmd.openTab} />\n ))}\n </div>\n {filtered.length === 0 && (\n <div className=\"mt-4 text-center text-xs text-gray-600\">\n No agents match \"{agentFilter}\" — <button onClick={() => setAgentFilter('all')} className=\"underline hover:text-gray-400\">Show all</button>\n </div>\n )}\n </>\n );\n })()}\n\n <InboxPanel messages={inbox} />\n\n {/* Round 111 (issue #82): dropped the license badge — \"trial (12d\n left)\" read like a paywall countdown on an open-source dashboard\n and Vincent flagged it as misleading more than once. The SSE /\n polling dot stays: it's a real connection-status indicator, not\n a sales surface. */}\n <div className=\"mt-8 text-center text-xs text-gray-600 flex items-center justify-center gap-2 flex-wrap\">\n {sseSupported && (\n <>\n <span className={`w-1.5 h-1.5 rounded-full ${sseConnected ? 'bg-green-400' : 'bg-gray-600'}`} />\n {sseConnected ? 'SSE live' : 'SWR polling'}\n </>\n )}\n </div>\n\n {/* Dispatch Panel */}\n {showDispatch && <DispatchPanel sessions={sessions} onClose={() => setShowDispatch(false)} />}\n\n {/* Command Center (multi-tab chat) */}\n {cmd.tabs.length > 0 && (\n <CommandCenter\n tabs={cmd.tabs}\n activeTab={cmd.activeTab}\n onOpenTab={cmd.openTab}\n onCloseTab={cmd.closeTab}\n onSetActive={cmd.setActiveTab}\n onClose={cmd.closeAll}\n />\n )}\n </div>\n );\n}\n","'use client';\n\ninterface StatsBarProps {\n online: number;\n working: number;\n total: number;\n version: string;\n uptime: string;\n}\n\nexport function StatsBar({ online, working, total, version, uptime }: StatsBarProps) {\n const onlinePercent = total > 0 ? Math.round((online / total) * 100) : 0;\n const fleetEmpty = total === 0;\n\n return (\n <div className={fleetEmpty ? 'mb-4' : 'mb-8'}>\n {/* Title row */}\n <div className=\"flex flex-wrap items-center gap-3 mb-4\">\n <h1 className=\"text-2xl font-bold text-white tracking-tight\">Agent Network</h1>\n <span className=\"text-xs text-gray-500\">\n CommHub {version} · {uptime}\n </span>\n </div>\n\n {fleetEmpty ? (\n /* Round 72: thin status strip replaces the 4-card grid when fleet\n is empty. Saves ~280px on mobile (CTA y=650 → ~370) and keeps\n the same data visible in a single inline row. */\n <div className=\"anet-stat-strip flex flex-wrap items-center gap-x-4 gap-y-1.5 text-xs text-gray-500 border-t border-b border-[#2a2a4a] py-2\">\n <span className=\"inline-flex items-center gap-1.5\">\n <span aria-hidden className=\"inline-block w-1.5 h-1.5 rounded-full bg-gray-600\" />\n <span className=\"text-gray-300 tabular-nums\">0</span> online\n </span>\n <span className=\"text-gray-700\">·</span>\n <span className=\"inline-flex items-center gap-1.5\">\n <span aria-hidden className=\"inline-block w-1.5 h-1.5 rounded-full bg-gray-600\" />\n <span className=\"text-gray-300 tabular-nums\">0</span> working\n </span>\n <span className=\"text-gray-700\">·</span>\n <span className=\"inline-flex items-center gap-1.5\">\n <span aria-hidden className=\"inline-block w-1.5 h-1.5 rounded-full bg-gray-600\" />\n <span className=\"text-gray-300 tabular-nums\">0</span> registered\n </span>\n </div>\n ) : (\n /* Populated state — full 4-card grid as before */\n <div className=\"grid grid-cols-2 sm:grid-cols-4 gap-3\">\n <StatCard\n value={online}\n label=\"Online\"\n sub={`${onlinePercent}% of fleet`}\n color=\"text-green-400\"\n accent=\"from-green-500/20 to-green-500/0\"\n border=\"border-green-500/15\"\n />\n <StatCard\n value={working}\n label=\"Working\"\n sub={online > 0 ? `${Math.round((working / online) * 100)}% utilization` : '--'}\n color=\"text-cyan-400\"\n accent=\"from-cyan-500/20 to-cyan-500/0\"\n border=\"border-cyan-500/15\"\n />\n <StatCard\n value={total - online}\n label=\"Offline\"\n sub={total - online === 0 ? 'All systems go' : `${total - online} disconnected`}\n color=\"text-gray-400\"\n accent=\"from-gray-500/10 to-gray-500/0\"\n border=\"border-gray-500/15\"\n />\n <StatCard\n value={total}\n label=\"Total\"\n sub=\"Registered nodes\"\n color=\"text-white\"\n accent=\"from-blue-500/15 to-blue-500/0\"\n border=\"border-blue-500/15\"\n />\n </div>\n )}\n </div>\n );\n}\n\nfunction StatCard({ value, label, sub, color, accent, border }: {\n value: number; label: string; sub: string; color: string; accent: string; border: string;\n}) {\n // Extract the color family (green/cyan/gray/blue/white) from `color` prop\n // so the light-theme top-strip CSS can pick the right accent.\n const accentKey = color.replace('text-', '').split('-')[0];\n return (\n <div\n data-anet-stat-card={accentKey}\n className={`anet-stat-card relative overflow-hidden rounded-xl border ${border} bg-[#111128] px-4 py-3 transition-all`}\n >\n <div className={`absolute inset-0 bg-gradient-to-br ${accent} pointer-events-none`} />\n <div className=\"relative\">\n <div className={`text-3xl font-bold ${color} tabular-nums leading-tight`}>{value}</div>\n <div className=\"text-sm text-gray-300 mt-0.5\">{label}</div>\n <div className=\"text-xs text-gray-600 mt-1\">{sub}</div>\n </div>\n </div>\n );\n}\n","'use client';\n\nimport { useState } from 'react';\n\nexport function BroadcastBar() {\n const [broadcastMsg, setBroadcastMsg] = useState('');\n const [broadcasting, setBroadcasting] = useState(false);\n const [broadcastResult, setBroadcastResult] = useState('');\n\n const sendBroadcast = async () => {\n if (!broadcastMsg.trim()) return;\n setBroadcasting(true);\n setBroadcastResult('');\n try {\n const res = await fetch('/api/hub/broadcast', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ message: broadcastMsg }),\n });\n const data = await res.json();\n if (data.ok) {\n setBroadcastResult(`Broadcast sent to ${data.recipients} node(s)`);\n setBroadcastMsg('');\n } else {\n setBroadcastResult(`Failed: ${data.error || 'Send error'}`);\n }\n } catch (e: unknown) {\n setBroadcastResult(`Failed: ${e instanceof Error ? e.message : 'Send error'}`);\n }\n setBroadcasting(false);\n setTimeout(() => setBroadcastResult(''), 5000);\n };\n\n return (\n <div className=\"mb-6\">\n <div className=\"flex gap-2\">\n <input\n type=\"text\"\n value={broadcastMsg}\n onChange={e => setBroadcastMsg(e.target.value)}\n onKeyDown={e => e.key === 'Enter' && sendBroadcast()}\n placeholder=\"Broadcast message to all online agents...\"\n maxLength={500}\n aria-label=\"Broadcast message\"\n className=\"flex-1 bg-[#111128] border border-[#2a2a4a] rounded-lg px-4 py-2.5 text-sm text-white placeholder-gray-600 focus:border-blue-500/50 focus:ring-1 focus:ring-blue-500/20 focus:outline-none transition-colors\"\n />\n <button\n onClick={sendBroadcast}\n disabled={broadcasting || !broadcastMsg.trim()}\n aria-label=\"Send broadcast\"\n className=\"flex items-center gap-2 px-5 py-2.5 bg-blue-600 hover:bg-blue-500 disabled:bg-gray-800 disabled:text-gray-600 text-white text-sm rounded-lg transition-all font-medium cursor-pointer disabled:cursor-not-allowed\"\n >\n {broadcasting ? (\n <>\n <svg aria-hidden className=\"w-4 h-4 animate-spin\" viewBox=\"0 0 24 24\" fill=\"none\">\n <circle cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" strokeWidth=\"3\" opacity=\"0.25\" />\n <path d=\"M4 12a8 8 0 018-8\" stroke=\"currentColor\" strokeWidth=\"3\" strokeLinecap=\"round\" />\n </svg>\n <span>Sending…</span>\n </>\n ) : (\n <>\n {/* Megaphone icon — \"broadcast to all\" affordance */}\n <svg aria-hidden className=\"w-4 h-4\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n <path d=\"M3 11v3a1 1 0 0 0 1 1h2l3.29 3.29a1 1 0 0 0 1.71-.71V7.42a1 1 0 0 0-1.71-.71L6 10H4a1 1 0 0 0-1 1Z\" />\n <path d=\"M16 8a5 5 0 0 1 0 6\" opacity=\"0.7\" />\n <path d=\"M19 5a9 9 0 0 1 0 12\" opacity=\"0.45\" />\n </svg>\n <span>Broadcast</span>\n </>\n )}\n </button>\n </div>\n {broadcastMsg.length > 0 && (\n <div className=\"text-right text-xs text-gray-600 mt-1 tabular-nums\">{broadcastMsg.length}/500</div>\n )}\n {broadcastResult && (\n <div className={`mt-3 text-sm text-center anet-fade-in ${broadcastResult.startsWith('Failed') ? 'text-red-400' : 'text-green-400/80'}`}>\n {broadcastResult}\n </div>\n )}\n </div>\n );\n}\n","'use client';\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport useSWR from 'swr';\nimport { useRouter } from 'next/navigation';\nimport { Session } from './types';\nimport { aliasAvatarColors, aliasInitial } from './AliasAvatar';\nimport { ChatPopover } from './ChatPopover';\nimport { vendorForModel, runtimeIdentity, identityLine } from '../lib/vendorIdentity';\nimport { parseHubTime, relativeAgo } from '../lib/time';\nimport { DASHBOARD_VERSION } from '../lib/version';\n\n/** v0.10.0 Hero 1+2 / §3.F server-health hook — fetches the normalized\n * /api/hub/servers payload (preview.370 unblocked real-data via the\n * proxy schema-normalize layer) and exposes a per-hostname health\n * tier. red → worst-of-CPU/Mem/Disk ≥ 85%; amber → 60-85%; green\n * → < 60%; null when telemetry hasn't shipped yet OR the host is\n * offline.\n *\n * Shared with ServersDrawer via SWR's key-based dedup — both\n * consumers point at /api/hub/servers so the cache layer fans out\n * to a single fetch per refresh interval.\n */\ninterface ServerHealthRow {\n hostname: string;\n cpu_load_1min: number | null;\n cpu_cores: number;\n mem_used_gb: number | null;\n mem_total_gb: number | null;\n disk_used_gb: number | null;\n disk_total_gb: number | null;\n status: 'online' | 'offline';\n}\ntype ServerTier = 'red' | 'amber' | 'green';\nfunction classifyServer(s: ServerHealthRow): ServerTier | null {\n if (s.status === 'offline') return null;\n const cpuPct = s.cpu_load_1min != null && s.cpu_cores > 0 ? (s.cpu_load_1min / s.cpu_cores) * 100 : null;\n const memPct = s.mem_used_gb != null && s.mem_total_gb != null && s.mem_total_gb > 0 ? (s.mem_used_gb / s.mem_total_gb) * 100 : null;\n const diskPct = s.disk_used_gb != null && s.disk_total_gb != null && s.disk_total_gb > 0 ? (s.disk_used_gb / s.disk_total_gb) * 100 : null;\n const vals = [cpuPct, memPct, diskPct].filter((v): v is number => typeof v === 'number');\n if (vals.length === 0) return null;\n const worst = Math.max(...vals);\n if (worst >= 85) return 'red';\n if (worst >= 60) return 'amber';\n return 'green';\n}\nconst serversFetcher = async (url: string): Promise<{ servers: ServerHealthRow[] } | null> => {\n const res = await fetch(url);\n if (!res.ok) return null;\n return res.json();\n};\nfunction useServerHealthMap(): Map<string, ServerTier> {\n const { data } = useSWR<{ servers: ServerHealthRow[] } | null>(\n '/api/hub/servers',\n serversFetcher,\n { refreshInterval: 15000, dedupingInterval: 5000 },\n );\n return useMemo(() => {\n const m = new Map<string, ServerTier>();\n for (const s of data?.servers ?? []) {\n const tier = classifyServer(s);\n if (tier) m.set(s.hostname, tier);\n }\n return m;\n }, [data]);\n}\n\ninterface MessageFlow {\n from_alias: string;\n to_alias: string;\n content: string;\n created_at: string;\n}\n\ninterface TopoGraphProps {\n sessions: Session[];\n sseSessions: Record<string, number>;\n // #84: a node.renamed event from the Overview's SSE listener. When the\n // currently open chat popover targets `from`, it follows the rename to `to`.\n renameSignal?: { from: string; to: string; ts: number } | null;\n}\n\ninterface Point {\n x: number;\n y: number;\n}\n\ninterface FlowLink {\n key: string;\n from: string;\n to: string;\n count: number;\n content: string;\n /** ISO timestamp of the most recent message on this edge — drives\n * the Round 10 freshness fade so dormant links recede visually. */\n last_at: string;\n}\n\nconst cx = 500;\nconst cy = 330;\nconst onlineRadius = 220;\n// Round 97 (issue #50): tier into two rings when N > 8; round 98\n// (issue #61): tier into three rings when N > 14. At N=22 (Vincent's\n// network) two rings still leave 11 nodes per ring → inner chord of\n// 88px can't fit a 100px label; three rings give ~⌈N/3⌉ per ring so\n// every tier has enough arc room.\nconst onlineTierThreshold = 8;\nconst onlineTripleThreshold = 14;\nconst onlineInnerRadius = 175;\nconst onlineOuterRadius = 260;\nconst onlineTripleInnerR = 145;\nconst onlineTripleMidR = 215;\nconst onlineTripleOuterR = 285;\nconst offlineRadius = 325;\n\n/** Polar coordinate for a node on a ring. `rotateBy` lets the caller offset\n * the whole ring so two stacked rings don't align radially (round 25: the\n * offline ring gets a half-step rotation so its nodes sit in the gaps\n * between online ones). */\nfunction polarPoint(index: number, total: number, radius: number, rotateBy = 0) {\n const spread = total <= 2 ? Math.PI : Math.PI * 1.78;\n const start = -Math.PI / 2 - spread / 2;\n const angle = total <= 1 ? -Math.PI / 2 : start + (spread * index) / (total - 1) + rotateBy;\n return {\n x: cx + radius * Math.cos(angle),\n y: cy + radius * Math.sin(angle),\n };\n}\n\nfunction curvePath(from: Point, to: Point, lift = 0) {\n const mx = (from.x + to.x) / 2;\n const my = (from.y + to.y) / 2;\n const dx = to.x - from.x;\n const dy = to.y - from.y;\n const length = Math.hypot(dx, dy) || 1;\n const normalX = -dy / length;\n const normalY = dx / length;\n const control = {\n x: mx + normalX * lift,\n y: my + normalY * lift,\n };\n\n return `M${from.x},${from.y} Q${control.x},${control.y} ${to.x},${to.y}`;\n}\n\nfunction truncate(value: string, max: number) {\n return value.length > max ? `${value.slice(0, max - 1)}...` : value;\n}\n\n/** Round 46 / Loop: SWR-freshness chip.\n *\n * TopoGraph's data refreshes every 5 s via SWR but the user has no\n * visible signal that the canvas they're looking at is current. The\n * flow particles and chips (R42 active-links, R43 hub) tell you when\n * the FLEET last did something, not when the DATA last syncing. This\n * chip ticks `live · Ns` against a 1-second interval so the operator\n * can trust freshness without doing internal math.\n *\n * Owns its own setInterval so the parent topology doesn't re-render\n * every second (only this small chip does). lastSyncRef is updated\n * whenever the `sessions` prop reference changes — that's the SWR\n * refresh signal. */\nfunction FreshnessChip({ sessions }: { sessions: unknown }) {\n const lastSyncRef = useRef<number>(Date.now());\n const [now, setNow] = useState(() => Date.now());\n useEffect(() => {\n // sessions reference changed → SWR just delivered a new payload.\n lastSyncRef.current = Date.now();\n }, [sessions]);\n useEffect(() => {\n const id = setInterval(() => setNow(Date.now()), 1000);\n return () => clearInterval(id);\n }, []);\n const sec = Math.max(0, Math.floor((now - lastSyncRef.current) / 1000));\n // Tint the chip warmer when the data goes stale (>10s since last sync).\n // SWR's default refreshInterval here is 5s, so anything past ~10s is\n // either a poll miss or a network hiccup.\n const stale = sec > 10;\n // Round 187 / Loop: chip transitions between fresh (gray) and stale\n // (amber) colour palettes smoothly. Pre-R187 the className swap\n // snapped every time the stale boundary was crossed — could happen\n // multiple times per minute on a flaky network. Adding\n // transition-colors makes the stale-onset (gray → amber) and\n // recovery (amber → gray) ease through the bg / text / border\n // palette together. 300ms matches R161/R162 active-links chip\n // freshness fade timing for visual consistency in the chip row.\n // Round 315 / Loop: FreshnessChip joins the R313-R314 chip-row\n // data-weight family. When it appears (stale state only — R275\n // gated rendering to stale), it sits next to working/online/\n // active-links chips that all carry font-medium (R313) plus the\n // vendor letter chips (R314). Without font-medium the warning\n // chip would render at default 400 next to data chips at 500\n // — visual inconsistency right at the moment the chip exists to\n // grab attention. font-medium adds it to the HTML-context data\n // tier (R312-R314 family); the amber bg/text/border still does\n // the warning-state work, the weight just keeps the chip in the\n // same data-typography ladder as its siblings.\n // Round 377 / Loop: FreshnessChip baseClass picks up `tabular-nums`.\n // The chip-row's last untouched chip joins the R224-R357 broader\n // tabular-nums sweep:\n // R224 edge badge digit\n // R225 hub digit / panel header counts / recent row count\n // R232 chip row counts (working / online / active-links)\n // R321 recent row timestamp\n // R322 recent panel hot count\n // R323 filter pin pill counts\n // R333 vendor count suffix\n // R357 active-links freshness suffix wrapper\n // R377 FreshnessChip body (this round)\n // `font-mono` already gives equal-width glyphs but `tabular-nums`\n // is the explicit-invariant the rest of the chip row carries.\n // FreshnessChip body reads `lag · {sec}s` — the {sec} digit grows\n // every second; tabular-nums explicitly locks digit width so the\n // chip stays planted as the seconds counter ticks past 9 → 10 →\n // 99 → 100. R187 transition-colors duration-300 + R275 stale-only\n // render gate + R315 font-medium R313 family alignment all\n // preserved.\n const baseClass = \"hidden sm:inline px-2.5 py-1 rounded-md font-mono font-medium tabular-nums border transition-colors duration-300\";\n const colorClass = stale\n ? \"bg-amber-500/10 text-amber-300 border-amber-500/20\"\n : \"bg-gray-500/10 text-gray-400 border-gray-500/20\";\n /* Round 275 / Loop: simplification per Vincent 5214/5215-5217 visual\n audit (clutter cleanup for Twitter screenshot). Pre-R275 the chip\n ALWAYS rendered — \"live · 5s\" gray-on-gray at rest, \"lag · 15s\"\n amber when stale. The fresh state is an \"everything's fine\"\n affirmation that's implicit elsewhere on the canvas (counts\n updating, flows animating). Adding a permanent chip to the chip-\n row's right end for that affirmation is added visual chrome\n without proportional info value.\n\n R275 converts the chip to a CONDITIONAL warning indicator: render\n only when stale (sec > 10). Fresh state → null (chip absent). The\n amber stale chip still appears as a warning when SWR lags, so\n users see the problem signal; the fresh state implicitly relies\n on other liveness signals (recent-signal panel rows, edge\n animations, count updates).\n\n Net effect: chip-row at rest has 1 fewer chip (cleaner Twitter\n screenshot, less right-edge chrome), but signals appear on\n stale-onset to direct attention. */\n if (!stale) return null;\n return (\n <span\n className={`${baseClass} ${colorClass}`}\n title={stale ? `Last sync ${sec}s ago — SWR refresh may be lagging` : `Live data · refreshes every 5s · last sync ${sec}s ago`}\n data-freshness-chip\n data-freshness-chip-stale={stale ? 'true' : 'false'}\n >\n {/* Round 272 / Loop: swap prefix word to match color state so\n text and color point the same way. Pre-R272 the chip read\n \"live · {sec}s\" in BOTH fresh (gray) and stale (amber)\n states — the amber color signals \"concerning\" but \"live\"\n still says \"fresh data flowing\", a visual contradiction.\n Post-R272: fresh=\"live · {sec}s\" (gray + reassuring), stale=\n \"lag · {sec}s\" (amber + signals lagging). Same monospace\n cell count (3 chars + \" · \" + digits + \"s\") so no chip\n width jitter on threshold crossing; R187 transition-colors\n duration-300 still eases the bg/color flip. Title (hover\n tooltip) still spells out the full meaning in either\n state. */}\n {/* Round 410 / Loop: FreshnessChip body picks up the chip-\n internal-hierarchy arc. Pre-R410 the body rendered as a\n single text node `lag · {sec}s` with the parent's font-\n medium (fw=500) applied uniformly. R410 splits the digit\n and unit into separate spans so the chip's internal\n typography mirrors the family pattern R333-R341/R362/R369/\n R389 established for the chip row:\n digit (fw=600) data tier\n unit (fw=500 + opacity=0.7) label tier\n The `lag` prefix stays at the chip's baseline (fw=500\n from parent font-medium) — it labels the state, not a\n data value. data-freshness-chip-digit / -unit attrs\n surface the spans for tests. tabular-nums + transition-\n colors + R275 stale-only gate all preserved. */}\n {stale ? 'lag' : 'live'} · <span className=\"font-semibold\" data-freshness-chip-digit>{sec}</span><span className=\"opacity-70\" data-freshness-chip-unit>s</span>\n </span>\n );\n}\n\n/** Round 36 / Loop: prefers-reduced-motion hook.\n *\n * Round 29's a11y sweep zeroed CSS animations via media query, but SVG\n * SMIL `<animate>` / `<animateMotion>` elements aren't reachable from CSS\n * — they need JS to opt out. This hook reads the media query and listens\n * for changes so the topology's flow particles, pulses, click ripple\n * and hub breath honour the OS-level preference. */\nfunction useReducedMotion(): boolean {\n const [reduced, setReduced] = useState(false);\n useEffect(() => {\n if (typeof window === 'undefined' || !window.matchMedia) return;\n const mq = window.matchMedia('(prefers-reduced-motion: reduce)');\n setReduced(mq.matches);\n const onChange = (e: MediaQueryListEvent) => setReduced(e.matches);\n mq.addEventListener('change', onChange);\n return () => mq.removeEventListener('change', onChange);\n }, []);\n return reduced;\n}\n\n// Round 38 / Loop: parseHubTime + relativeAgo factored to app/lib/time.ts\n// — the Round 35 fix lives there now alongside the Round 37 mirror so\n// any future TZ-safe parse update happens in one place.\n\n/** Round 12 / Loop: status trio audit.\n *\n * Each (status × theme) cell returns a {primary, halo, text} trio. The trio\n * invariant — keep it when adding states or tweaking shades:\n *\n * light: primary = <hue>-600 halo = <hue>-100 * text = <hue>-800/900\n * dark: primary = <hue>-400/500 halo = <hue>-900 text = <hue>-100\n *\n * * offline.halo light deviates intentionally to slate-200 (#e2e8f0):\n * slate-100 (#f1f5f9) is too close to the panel bg (#f8fafc) and the\n * halo would vanish. The other three rows keep the 100-shade.\n *\n * Audit caught one cross-family drift before this round: online-other halo\n * light was #dbeafe (blue-100) while its primary (#0284c7 sky-600) and text\n * (#0c4a6e sky-900) were sky-family — now sky-100 (#e0f2fe). Tiny visual\n * difference; large hygiene win — every trio is now mono-hue. */\nfunction nodeStatus(session: Session, isOnline: boolean, isLight: boolean) {\n if (!isOnline) {\n return {\n label: 'offline',\n primary: isLight ? '#94a3b8' : '#6b7280', // slate-400 / gray-500\n halo: isLight ? '#e2e8f0' : '#111827', // slate-200* / gray-900\n text: isLight ? '#475569' : '#9ca3af', // slate-600 / gray-400\n };\n }\n if (session.status === 'working') {\n return {\n label: 'working',\n primary: isLight ? '#059669' : '#22c55e', // emerald-600 / green-500\n halo: isLight ? '#d1fae5' : '#14532d', // emerald-100 / green-900\n text: isLight ? '#065f46' : '#dcfce7', // emerald-800 / green-100\n };\n }\n if (session.status === 'idle') {\n return {\n label: 'idle',\n primary: isLight ? '#0d9488' : '#2dd4bf', // teal-600 / teal-400\n halo: isLight ? '#ccfbf1' : '#134e4a', // teal-100 / teal-900\n text: isLight ? '#115e59' : '#ccfbf1', // teal-800 / teal-100\n };\n }\n return {\n label: session.status || 'online',\n primary: isLight ? '#0284c7' : '#38bdf8', // sky-600 / sky-400\n halo: isLight ? '#e0f2fe' : '#0c4a6e', // sky-100 / sky-900 (was blue-100 — drift fixed Round 12)\n text: isLight ? '#0c4a6e' : '#e0f2fe', // sky-900 / sky-100\n };\n}\n\n/** Theme-aware color palette for the topology SVG. */\ninterface Palette {\n panelStops: [string, string, string];\n radarStops: { color: string; opacity: number }[];\n arrowFill: string;\n ringStroke: string;\n spokeStroke: { active: string; idle: string };\n flowEdge: string;\n flowPath: string;\n flowParticle: string;\n nodeFill: { online: string; offline: string };\n labelBox: { fill: string; stroke: string };\n legendBox: { fill: string; stroke: string };\n legendText: string;\n legendHeadline: string;\n legendAccent: string;\n containerBg: string;\n containerBorder: string;\n topRailGradient: string;\n}\n\nconst DARK_PALETTE: Palette = {\n panelStops: ['#0b1220', '#080814', '#101018'],\n radarStops: [\n { color: '#22d3ee', opacity: 0.18 },\n { color: '#22c55e', opacity: 0.045 },\n { color: '#020617', opacity: 0 },\n ],\n arrowFill: '#67e8f9',\n ringStroke: '#164e63',\n spokeStroke: { active: '#22d3ee', idle: '#155e75' },\n flowEdge: '#67e8f9',\n flowPath: '#e0f2fe',\n flowParticle: '#fef08a',\n nodeFill: { online: '#020617', offline: '#080814' },\n labelBox: { fill: '#020617', stroke: '#1f2937' },\n legendBox: { fill: '#020617', stroke: '#1f2937' },\n legendText: '#94a3b8',\n legendHeadline: '#e5e7eb',\n legendAccent: '#67e8f9',\n containerBg: '#080814',\n containerBorder: '#2a2a4a',\n topRailGradient: 'from-transparent via-cyan-400/70 to-transparent',\n};\n\nconst LIGHT_PALETTE: Palette = {\n panelStops: ['#f8fafc', '#ffffff', '#f1f5f9'],\n radarStops: [\n { color: '#10b981', opacity: 0.06 },\n { color: '#3b82f6', opacity: 0.03 },\n { color: '#ffffff', opacity: 0 },\n ],\n arrowFill: '#10b981',\n ringStroke: '#cbd5e1',\n spokeStroke: { active: '#10b981', idle: '#cbd5e1' },\n flowEdge: '#10b981',\n flowPath: '#475569',\n flowParticle: '#f59e0b',\n nodeFill: { online: '#ffffff', offline: '#f8fafc' },\n labelBox: { fill: '#ffffff', stroke: '#e2e8f0' },\n legendBox: { fill: '#ffffff', stroke: '#e2e8f0' },\n legendText: '#475569',\n legendHeadline: '#0f172a',\n legendAccent: '#0d9488',\n containerBg: '#ffffff',\n containerBorder: '#e3e6eb',\n topRailGradient: 'from-transparent via-emerald-500/40 to-transparent',\n};\n\nfunction useTheme(): 'light' | 'dark' {\n const [theme, setTheme] = useState<'light' | 'dark'>('dark');\n useEffect(() => {\n const read = () => {\n const t = document.documentElement.getAttribute('data-theme') || 'cyber';\n setTheme(t === 'light' || t === 'mint' ? 'light' : 'dark');\n };\n read();\n const obs = new MutationObserver(read);\n obs.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });\n return () => obs.disconnect();\n }, []);\n return theme;\n}\n\n/** Round 100 (issue #79): brand showcase mode. Activate via `?brand=intern`\n * on any dashboard page — TopoGraph nodes show the 书小生 mascot instead\n * of alias initials. Stored in localStorage so the flag persists across\n * navigation. Clears with `?brand=none` or `?brand=` (empty). */\nfunction useBrand(): string | null {\n const [brand, setBrand] = useState<string | null>(null);\n useEffect(() => {\n try {\n const url = new URL(window.location.href);\n const param = url.searchParams.get('brand');\n if (param !== null) {\n if (param === '' || param === 'none') {\n localStorage.removeItem('anet-brand');\n setBrand(null);\n } else {\n localStorage.setItem('anet-brand', param);\n setBrand(param);\n }\n } else {\n setBrand(localStorage.getItem('anet-brand'));\n }\n } catch {}\n }, []);\n return brand;\n}\n\n/** Round 106 (issue #83): cluster agents by shared alias prefix so a team\n * reads as one unit in the topology. Adjacent (sorted) aliases that share\n * a ≥2-char prefix join the same group; the group key is the prefix common\n * to every member. Singletons map to their own alias (no hue shift). The\n * group key feeds aliasAvatarColors() so e.g. all 通信* nodes get one hue,\n * all 研究员* another — the \"同色相 tint\" clustering option from #83. */\nfunction commonPrefix(a: string, b: string): string {\n let i = 0;\n while (i < a.length && i < b.length && a[i] === b[i]) i++;\n return a.slice(0, i);\n}\n\n/** #83 + #111: group nodes that share a ≥2-char alias prefix OR a project_dir\n * (\"either criterion → same group\", Vincent 4724). Union-find over the\n * sessions; the component label prefers the shared project_dir's basename,\n * else the common alias prefix. Returns alias → groupKey. */\nfunction computeGroups(sessions: { alias: string; project_dir?: string | null }[]): Record<string, string> {\n const n = sessions.length;\n const parent = sessions.map((_, i) => i);\n const find = (i: number): number => {\n while (parent[i] !== i) { parent[i] = parent[parent[i]]; i = parent[i]; }\n return i;\n };\n const union = (a: number, b: number) => {\n const ra = find(a), rb = find(b);\n if (ra !== rb) parent[ra] = rb;\n };\n\n // union by shared project_dir\n const byDir: Record<string, number[]> = {};\n sessions.forEach((s, i) => {\n const d = s.project_dir?.trim();\n if (d) (byDir[d] ||= []).push(i);\n });\n for (const idxs of Object.values(byDir)) {\n for (let k = 1; k < idxs.length; k++) union(idxs[0], idxs[k]);\n }\n\n // union by shared ≥2-char alias prefix — sort, link adjacent pairs\n const order = sessions.map((_, i) => i).sort((a, b) => sessions[a].alias.localeCompare(sessions[b].alias));\n for (let k = 0; k + 1 < order.length; k++) {\n if (commonPrefix(sessions[order[k]].alias, sessions[order[k + 1]].alias).length >= 2) {\n union(order[k], order[k + 1]);\n }\n }\n\n // label each connected component\n const comps: Record<number, number[]> = {};\n for (let i = 0; i < n; i++) (comps[find(i)] ||= []).push(i);\n const keys: Record<string, string> = {};\n for (const members of Object.values(comps)) {\n let label: string;\n if (members.length === 1) {\n label = sessions[members[0]].alias;\n } else {\n const dirs = new Set(members.map(i => sessions[i].project_dir?.trim()).filter(Boolean) as string[]);\n if (dirs.size === 1) {\n const d = [...dirs][0];\n label = d.split('/').filter(Boolean).pop() || d;\n } else {\n label = members.map(i => sessions[i].alias).reduce((a, b) => commonPrefix(a, b));\n if (label.length < 2) label = sessions[members[0]].alias;\n }\n }\n for (const i of members) keys[sessions[i].alias] = label;\n }\n return keys;\n}\n\nfunction buildFlowLinks(messages: MessageFlow[], positions: Record<string, Point>) {\n const links = new Map<string, FlowLink>();\n\n messages.forEach(message => {\n if (\n !positions[message.from_alias] ||\n !positions[message.to_alias] ||\n message.from_alias === message.to_alias\n ) {\n return;\n }\n\n const key = `${message.from_alias}->${message.to_alias}`;\n const current = links.get(key);\n\n // Keep the most-recent timestamp per pair so the render can fade\n // dormant edges (Round 10 freshness fade).\n const incoming = message.created_at || '';\n const last_at = !current\n ? incoming\n : (incoming > current.last_at ? incoming : current.last_at);\n\n links.set(key, {\n key,\n from: message.from_alias,\n to: message.to_alias,\n count: (current?.count || 0) + 1,\n content: current?.content || message.content,\n last_at,\n });\n });\n\n return [...links.values()].slice(0, 18);\n}\n\nexport function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProps) {\n const theme = useTheme();\n const isLight = theme === 'light';\n const reducedMotion = useReducedMotion();\n const pal = isLight ? LIGHT_PALETTE : DARK_PALETTE;\n const brand = useBrand();\n const isIntern = brand === 'intern';\n // v0.10.0 Hero 1+2 / §3.F — per-host health tier map. Composes\n // with #119 ServersDrawer (same SWR key, deduped). When a node's\n // host server crosses into the red tier (CPU/Mem/Disk ≥ 85%),\n // the per-node SVG render adds a faint amber outer ring to flag\n // the issue without leaving the topology view.\n const hostHealthMap = useServerHealthMap();\n // R133: Next.js client-router for the recent-signal panel \"+N more\"\n // navigation. TopoGraph hasn't needed routing before — every other\n // affordance composes back into the canvas's own state — but the\n // truncated-flow footer is the one place where the user logically\n // wants to leave: \"show me the rest of the flows\" → /messages.\n const router = useRouter();\n const [messages, setMessages] = useState<MessageFlow[]>([]);\n // Issue #87: ring | grid layout toggle. Ring is the tiered-radial default;\n // grid arranges nodes in an N×M grid (better for 30+ nodes). Persisted to\n // localStorage like the zoom/pan view state. Declared above nodePositions\n // since that useMemo branches on it.\n const [layout, setLayout] = useState<'ring' | 'grid'>('ring');\n useEffect(() => {\n try {\n const saved = localStorage.getItem('anet-topo-layout');\n if (saved === 'grid' || saved === 'ring') {\n setLayout(saved);\n } else if (sessions.length >= 20) {\n // v0.10.0 Hero 3 Wave 1 §3.D — auto-grid for dense fleets.\n // When the user hasn't explicitly chosen a layout (no\n // localStorage entry), default to `grid` once the fleet\n // crosses 20 nodes. Below 20, the ring layout reads more\n // attractive (per #87 + R97-99 tier-ring history); at 20+\n // the ring starts packing tier 3 (R98 triple-tier\n // threshold) and grid scales cleaner — every cell visible\n // at the same density, no overlap-test risk from tier\n // crowding. The user's explicit toggle (R163 chrome\n // Ring|Grid) always wins by writing to anet-topo-layout,\n // so this only sets the *initial* preference for first-\n // time users on a dense fleet. Threshold 20 picked to\n // align with the existing density-aware breakpoints (R98\n // tier flip; R109 dense-label gating at 16). */}\n setLayout('grid');\n }\n } catch {}\n // sessions.length intentionally NOT in deps — this runs once on\n // mount and shouldn't re-fire when nodes join/leave (which would\n // override a user's mid-session toggle). The Ring|Grid chrome\n // button remains the authoritative source post-mount.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n // Round 170 / Loop: layout toggle (Ring ↔ Grid) used to teleport\n // every node from its old position to its new one in one paint\n // frame — the most jarring single user action left on the\n // canvas. Solution: dim the viewport <g> to ~45% opacity for\n // the duration of the swap so the eye reads it as a soft\n // crossfade-blink rather than a hard teleport. layoutSwitching\n // is a one-shot flag; the inline style on the viewport <g>\n // reads it and lerps opacity 1 → 0.45 → 1 across the swap.\n // Auto-clears after 400ms (covers fade-down 250ms + buffer for\n // React to commit the new layout's positions). Same pattern as\n // R168's smoothView arming but on a different visual axis\n // (opacity, not transform).\n const [layoutSwitching, setLayoutSwitching] = useState(false);\n const toggleLayout = () => {\n setLayoutSwitching(true);\n setTimeout(() => setLayoutSwitching(false), 400);\n setLayout(prev => {\n const next = prev === 'ring' ? 'grid' : 'ring';\n try { localStorage.setItem('anet-topo-layout', next); } catch {}\n return next;\n });\n };\n // Issue #113: node size scale (Vincent 4727 — nodes too big / crowded at\n // ~22 nodes). S/M/L → 0.7/0.84/1.0; default M (one notch down from the old\n // fixed size). Persisted like the layout toggle.\n const [nodeScale, setNodeScale] = useState(0.84);\n useEffect(() => {\n try {\n const saved = parseFloat(localStorage.getItem('anet-topo-nodescale') || '');\n if (saved === 0.7 || saved === 0.84 || saved === 1) setNodeScale(saved);\n } catch {}\n }, []);\n // Round 171 / Loop: nodeSize change picks up the R170 crossfade\n // pattern. Clicking S/M/L in the chrome re-derives every node's\n // radius + label sizing + (in grid layout) cell spacing — a\n // wholesale visual shift. Pre-R171 the resize snapped in one\n // paint frame; with this flag the viewport <g> opacity dims to\n // 0.45 for ~400ms, masking the redraw as a soft blink (same\n // vocabulary R170 uses for layout toggle). Bails early when\n // the picked scale matches the current one — clicking the\n // already-active button shouldn't fire a fade. Composes with\n // R170 layoutSwitching via `||` in the opacity expression; both\n // flags drive the same opacity transition but expose distinct\n // data-* attributes so tests can disambiguate which gesture\n // armed the crossfade.\n const [nodeSizeSwitching, setNodeSizeSwitching] = useState(false);\n const pickNodeScale = (v: number) => {\n if (v === nodeScale) return;\n setNodeSizeSwitching(true);\n setTimeout(() => setNodeSizeSwitching(false), 400);\n setNodeScale(v);\n try { localStorage.setItem('anet-topo-nodescale', String(v)); } catch {}\n };\n\n useEffect(() => {\n const fetchMessages = async () => {\n try {\n const res = await fetch('/api/hub/messages?limit=50');\n if (res.status === 401) {\n window.location.assign('/login');\n return;\n }\n\n const data = await res.json();\n setMessages(data.messages || []);\n } catch {}\n };\n fetchMessages();\n const interval = setInterval(fetchMessages, 5000);\n return () => clearInterval(interval);\n }, []);\n\n const {\n onlineNodes,\n offlineNodes,\n nodePositions,\n flowLinks,\n activeAliases,\n groupKeys,\n groupBoxes,\n gridContentBottom,\n } = useMemo(() => {\n const sseCount = (s: { alias: string; network_id?: string }) =>\n (s.network_id ? sseSessions[`${s.network_id}:${s.alias}`] : undefined) ?? sseSessions[s.alias];\n // Round 106 (issue #83): sort by alias so same-prefix agents\n // (通信龙 / 通信牛 / 通信工程马 …, or 研究员1号 / 研究员2号 …) end up\n // adjacent in the array — the tier layout below assigns angles by\n // index, so contiguous-in-array becomes contiguous-in-ring, i.e.\n // each team visually clusters. localeCompare keeps CJK ordering sane.\n const byAlias = (a: Session, b: Session) => a.alias.localeCompare(b.alias);\n // #112 umbrella: ghost age-out — an offline node not seen recently is\n // almost certainly a deleted node the server `/api/status` still\n // returns (#74 root cause is server-side; this is the dashboard-side\n // fallback so stale ghosts stop cluttering the topology). A missing\n // last_seen_at is kept (conservative — could be a legitimately new node).\n // Round 27 / P0: the 24h threshold was too lenient — Vincent's preview.29\n // screenshot showed B站马 nodes deleted ~4 h earlier still visible. A\n // healthy agent heartbeats every few seconds; if it's been silent for\n // an hour it's effectively gone. 1 h gives a fresh disconnect time to\n // come back while removing dead nodes well within an operator session.\n const GHOST_MS = 60 * 60 * 1000;\n const now = Date.now();\n const isGhost = (s: Session) => {\n if (s.status !== 'offline' || sseCount(s) || !s.last_seen_at) return false;\n // Round 35 / Loop: parseHubTime normalises SQL-style timestamps to UTC\n // before parsing so non-UTC browsers don't see a phantom 8-hour skew\n // and ghost freshly-disconnected nodes.\n const t = parseHubTime(s.last_seen_at);\n return t !== null && now - t > GHOST_MS;\n };\n const online = sessions.filter(s => s.status !== 'offline' || sseCount(s)).sort(byAlias);\n const offline = sessions.filter(s => s.status === 'offline' && !sseCount(s) && !isGhost(s)).sort(byAlias);\n const positions: Record<string, Point> = {};\n\n if (layout === 'grid') {\n // Issue #87 + #111: group-banded grid. Nodes are sorted by alias so\n // same-prefix aliases are adjacent; each multi-member prefix group then\n // gets its OWN row(s) starting at column 0, while singletons pack into\n // shared rows. Group-banded placement keeps every group's bounding box\n // (#111) a clean rectangle — no box ever overlaps another group's.\n const all = [...online, ...offline];\n const groupKeys = computeGroups(all);\n const cols = Math.max(1, Math.ceil(Math.sqrt(all.length)));\n // #112: gy0 starts below the (now compact) recent-signal / legend\n // overlay panels — their max bottom edge is y≈112 — so grid row 0 is\n // never occluded; gy1 extends near the canvas bottom for breathing room.\n const gx0 = 150, gx1 = 850, gy0 = 126, gy1 = 652;\n const cellW = (gx1 - gx0) / cols;\n // largest node radius at the current size scale — drives the group\n // label band + the row-height floor so nothing ever overlaps.\n const nodeR = Math.round(26 * nodeScale);\n\n // ordered runs of consecutive same-group-key nodes (≥2 = real group)\n const runs: { key: string; members: Session[] }[] = [];\n for (const s of all) {\n const gk = groupKeys[s.alias];\n const last = runs[runs.length - 1];\n if (last && last.key === gk) last.members.push(s);\n else runs.push({ key: gk, members: [s] });\n }\n\n // Pass 1 — assign each run to a band.\n //\n // v0.10.4 #150 (Vincent /goal 5453): \"不是一起的落单的怎么散落在中间了\".\n // Pre-#150 algo interleaved singletons between real groups as\n // centred bands, so orphan nodes appeared scattered in the middle\n // between cluster boxes. Vincent screenshot called this out as\n // \"layout 算法一点都不好\". Fix: bundle ALL singletons into ONE\n // band at the bottom of the grid + render an \"其他\" cluster box\n // around them. Multi-member prefix groups still go first in\n // alias order (existing #83/#111 behaviour). Net effect:\n // row 0..N-1: real prefix groups (left-aligned, own cluster box)\n // row N..M: single \"其他\" band collecting all orphans\n // (left-aligned, single cluster box at bottom)\n // No orphans → no orphan band → behaviour identical to pre-#150\n // for fleets where every node has a prefix-group match.\n type Band = { members: Session[]; startRow: number; centred: boolean; isGroup: boolean; isOrphan?: boolean };\n const bands: Band[] = [];\n let row = 0;\n const orphanMembers: Session[] = [];\n for (const run of runs) {\n if (run.members.length >= 2) {\n bands.push({ members: run.members, startRow: row, centred: false, isGroup: true });\n row += Math.ceil(run.members.length / cols);\n } else {\n // single-member run → collect for the bottom orphan band\n orphanMembers.push(...run.members);\n }\n }\n if (orphanMembers.length > 0) {\n bands.push({ members: orphanMembers, startRow: row, centred: false, isGroup: true, isOrphan: true });\n row += Math.ceil(orphanMembers.length / cols);\n }\n const totalRows = Math.max(1, row);\n // #112: the group label sits in a band ABOVE the topmost node, so the\n // band must clear the node radius — GROUP_TOP is node-relative, never\n // cellH-derived (cellH-derived was the label↔node overlap bug Vincent\n // hit). cellH then floors at GROUP_TOP + 30 so the label band + bottom\n // padding always fit and stacked group boxes never touch; past the\n // floor the grid overflows and zoom/pan handles it.\n const GROUP_TOP = nodeR + 20;\n // Round 27 / P0 (Vincent screenshot preview.29): dense plain-text\n // node labels (`pos.y + radius + denseDrop`, with a 3 px containerBg\n // stroke halo for readability) at the BOTTOM row of band N would\n // paint OVER the start of band N+1's group label, creating the\n // \"blueleap → eleap\", \"agent-network-dashboard → t-network / board\"\n // visual chopping. Geometry of the collision:\n // dense label visual bottom = node_N.y + radius + denseDrop + halo\n // group label glyph top = boxY + 4 (text y=boxY+14, ~10px ascent)\n // = node_N+1.y - GROUP_TOP + 4\n // node_N+1.y - node_N.y = cellH for consecutive rows, so no-collide:\n // cellH ≥ radius + denseDrop + halo + GROUP_TOP - 4 + buffer\n // With halo=3 buffer=4: cellH ≥ nodeR + denseDrop + GROUP_TOP + 3.\n const denseDrop = nodeScale < 0.8 ? 12 : 14;\n const cellH = Math.max(\n 2 * nodeR + 22, // node + dense label within band\n GROUP_TOP + 12, // group-label band + box padding\n nodeR + denseDrop + GROUP_TOP + 3, // round-27 dense↔group label clearance\n Math.min(100, (gy1 - gy0) / totalRows),\n );\n\n // Pass 2 — place each band's members.\n for (const band of bands) {\n band.members.forEach((s, idx) => {\n const rowInBand = Math.floor(idx / cols);\n const c = idx % cols;\n const inRow = Math.min(cols, band.members.length - rowInBand * cols);\n const inset = band.centred ? ((cols - inRow) * cellW) / 2 : 0;\n positions[s.alias] = {\n x: gx0 + inset + (c + 0.5) * cellW,\n y: gy0 + (band.startRow + rowInBand + 0.5) * cellH,\n };\n });\n }\n\n const links = buildFlowLinks(messages, positions);\n const active = new Set<string>();\n links.forEach(link => { active.add(link.from); active.add(link.to); });\n // #111: one bounding box per multi-member group (Vincent 4722). Each\n // group owns its rows; GROUP_PAD fills the row space left below the\n // nodes after the label band, so stacked group boxes always have a\n // gap between them (GROUP_TOP is defined above, with cellH).\n const GROUP_PAD = Math.max(8, Math.min(26, cellH - GROUP_TOP - 8)); // side/bottom\n const groupBoxes = bands\n .filter(b => b.isGroup)\n .map(band => {\n const pts = band.members.map(s => positions[s.alias]).filter(Boolean);\n const xs = pts.map(p => p.x);\n const ys = pts.map(p => p.y);\n const minX = Math.min(...xs), minY = Math.min(...ys);\n // Round 58 / Loop: per-group status mix for the label pip strip.\n // Working = status==='working'. Idle = online but not working.\n // Offline = !isOnline (either status==='offline' AND no SSE, or\n // ghost-purged elsewhere — but ghosts never reach groupBoxes\n // since they're filtered out upstream). Counts feed the label\n // tspans directly so the strip stays inside the label's bbox,\n // preserving the node↔label overlap-test guarantee from R19.\n let w = 0, i = 0, o = 0;\n for (const s of band.members) {\n const isOn = s.status !== 'offline' || !!sseCount(s);\n if (s.status === 'working') w++;\n else if (isOn) i++;\n else o++;\n }\n // v0.10.4 #150 — orphan band (singletons bundled at bottom)\n // renders with a \"其他\" cluster box; the box-key drives the\n // R63 label render + R86 hover-pin keying + #99 tooltip\n // member listing, so all the existing group-box machinery\n // applies uniformly to the orphan bucket too.\n return {\n key: band.isOrphan\n ? '其他'\n : band.members.length\n ? groupKeys[band.members[0].alias]\n : '',\n count: band.members.length,\n statuses: { working: w, idle: i, offline: o },\n x: minX - GROUP_PAD,\n y: minY - GROUP_TOP,\n w: Math.max(...xs) - minX + GROUP_PAD * 2,\n h: Math.max(...ys) - minY + GROUP_TOP + GROUP_PAD,\n };\n });\n // Round 28 / Loop: surface the grid's natural content bottom so the\n // mount effect can auto-fit zoom when the layout would overflow the\n // viewBox. = bottom of the last node row + its label drop + a small\n // breathing buffer. Round 27's cellH bump makes this overflow more\n // common (30-node fleets reach ~774 px, viewBox is 680).\n const gridContentBottom = gy0 + totalRows * cellH + 8;\n return {\n onlineNodes: online,\n offlineNodes: offline,\n nodePositions: positions,\n flowLinks: links,\n activeAliases: active,\n groupKeys,\n groupBoxes,\n gridContentBottom,\n };\n }\n\n // Round 97 (issue #50) + 98 (issue #61): three layout modes by N.\n // N ≤ 8 → single ring (r=220)\n // 8 < N ≤ 14 → two rings (inner r=175, outer r=260, half-step rot)\n // N > 14 → three rings (r=145/215/285, ⌈N/3⌉ per ring)\n // Each ring's spread is 1.78π so its node count drives chord length:\n // labels are 100px wide so each ring needs ≥110px chord per node.\n const tripleTier = online.length > onlineTripleThreshold;\n const dualTier = !tripleTier && online.length > onlineTierThreshold;\n let outerOnlineCount = online.length;\n if (tripleTier) {\n const per = Math.ceil(online.length / 3);\n const r1 = online.slice(0, per);\n const r2 = online.slice(per, 2 * per);\n const r3 = online.slice(2 * per);\n r1.forEach((s, index) => {\n positions[s.alias] = polarPoint(index, Math.max(r1.length, 1), onlineTripleInnerR);\n });\n const r1Spread = r1.length <= 2 ? Math.PI : Math.PI * 1.78;\n const r1Step = r1.length > 1 ? r1Spread / (r1.length - 1) : 0;\n r2.forEach((s, index) => {\n positions[s.alias] = polarPoint(index, Math.max(r2.length, 1), onlineTripleMidR, r1Step / 2);\n });\n r3.forEach((s, index) => {\n positions[s.alias] = polarPoint(index, Math.max(r3.length, 1), onlineTripleOuterR);\n });\n outerOnlineCount = r3.length;\n } else if (dualTier) {\n const innerCount = Math.ceil(online.length / 2);\n const outerCount = online.length - innerCount;\n const innerNodes = online.slice(0, innerCount);\n const outerNodes = online.slice(innerCount);\n innerNodes.forEach((s, index) => {\n positions[s.alias] = polarPoint(index, Math.max(innerCount, 1), onlineInnerRadius);\n });\n const innerSpread = innerCount <= 2 ? Math.PI : Math.PI * 1.78;\n const innerStep = innerCount > 1 ? innerSpread / (innerCount - 1) : 0;\n outerNodes.forEach((s, index) => {\n positions[s.alias] = polarPoint(index, Math.max(outerCount, 1), onlineOuterRadius, innerStep / 2);\n });\n outerOnlineCount = outerCount;\n } else {\n online.forEach((s, index) => {\n positions[s.alias] = polarPoint(index, Math.max(online.length, 1), onlineRadius);\n });\n }\n\n // Offset the offline ring radially by half the outermost online step so\n // offline bubbles sit in the angular gaps between online bubbles instead\n // of stacking directly behind them. Also push the outer ring further when\n // there are many offline nodes so labels don't crowd the legend.\n const outerSpreadBase = outerOnlineCount <= 2 ? Math.PI : Math.PI * 1.78;\n const outerStep = outerOnlineCount > 1 ? outerSpreadBase / (outerOnlineCount - 1) : 0;\n const offlineRotation = outerOnlineCount > 0 ? outerStep / 2 : 0;\n const offlineR = offlineRadius + Math.max(0, offline.length - 4) * 6;\n\n offline.forEach((s, index) => {\n positions[s.alias] = polarPoint(index, Math.max(offline.length, 1), offlineR, offlineRotation);\n });\n\n const links = buildFlowLinks(messages, positions);\n const active = new Set<string>();\n links.forEach(link => {\n active.add(link.from);\n active.add(link.to);\n });\n\n // Round 106 (issue #83): group key per alias → shared hue per team.\n const groupKeys = computeGroups([...online, ...offline]);\n\n return {\n onlineNodes: online,\n offlineNodes: offline,\n nodePositions: positions,\n flowLinks: links,\n activeAliases: active,\n groupKeys,\n // #111: group boxes are a grid-layout feature only — radially scattered\n // ring nodes can't be cleanly boxed. Ring keeps the #83 prefix hue.\n groupBoxes: [] as { key: string; count: number; statuses: { working: number; idle: number; offline: number }; x: number; y: number; w: number; h: number }[],\n // ring fits within VIEWBOX_H by construction (offlineRadius=325 + centre at y=330)\n gridContentBottom: 0,\n };\n }, [messages, sessions, sseSessions, layout, nodeScale]);\n\n const workingCount = onlineNodes.filter(s => s.status === 'working').length;\n // Round 6 / Loop: vendor distribution for the header chip — at a glance\n // \"what's in the fleet\" (A:5 M:2 O:8 书:12 …) without opening a node.\n // Sorted by count desc; \"unknown\" vendors collapse into a \"?\" bucket.\n const vendorDist = useMemo(() => {\n const tally = new Map<string, { initial: string; count: number; color: string }>();\n for (const s of [...onlineNodes, ...offlineNodes]) {\n const v = vendorForModel(s.model);\n const key = v.id === 'unknown' ? '?' : v.initial;\n const cur = tally.get(key);\n if (cur) cur.count++;\n else tally.set(key, { initial: key, count: 1, color: v.mono.text });\n }\n return [...tally.values()].sort((a, b) => b.count - a.count);\n }, [onlineNodes, offlineNodes]);\n // Round 109 (Vincent 4582 P0): hover-gated labels above this node count\n // so dense fleets show clean avatars instead of a wall of overlapping\n // label cards. 16 ≈ where the triple-tier rings start to crowd.\n const denseLayout = onlineNodes.length + offlineNodes.length > 16;\n const [hoveredAlias, setHoveredAlias] = useState<string | null>(null);\n // Round 86 / Loop: pointer-over-label preview. R63 wired the group\n // label to click-pin but hover gave no preview — the same hover/click\n // gap R83 closed for pressure-bar segments. This state lets the\n // label trigger the same dim mechanism a node hover already drives,\n // independently of whether the cursor happens to be over a member\n // node. ORs into hoveredGroup below so the existing derivation logic\n // (which composes with hoveredAlias, activeGroup, R85 marching ants)\n // keeps working unchanged.\n const [hoveredGroupLabel, setHoveredGroupLabel] = useState<string | null>(null);\n // Round 8 / Loop: which group is currently focused. Nodes outside this\n // group + other group boxes fade so the eye locks onto the team you're\n // pointing at. Singletons use their own alias as the group key.\n const hoveredGroup = hoveredGroupLabel\n ?? (hoveredAlias ? (groupKeys[hoveredAlias] ?? hoveredAlias) : null);\n // Round 63 / Loop: sticky group focus. R8 dims non-group nodes while\n // a member is hovered, but releasing the hover lets the focus fade.\n // Clicking the group label pins the group key here; activeGroup =\n // hoveredGroup ?? pinnedGroup so hover transiently overrides the\n // pin (handy for spot-comparing teams). Same compose pattern as\n // R60/R61 pinnedStatus.\n const [pinnedGroup, setPinnedGroup] = useState<string | null>(() => {\n // R66: same per-tab persistence treatment as pinnedStatus above.\n // The group key is opaque text (could be any prefix), so the init\n // reader can't validate it against a known enum — it just reads\n // whatever's stored. A useEffect below clears it if the value no\n // longer matches any current group (sessions changed since save).\n if (typeof window === 'undefined') return null;\n try { return sessionStorage.getItem('anet-topo-pinned-group'); } catch { return null; }\n });\n const activeGroup = hoveredGroup ?? pinnedGroup;\n // R66: sync pinnedGroup into sessionStorage when it changes; clear\n // it if it no longer matches any current group (the session set\n // can change between loads). The pinnedStatus sync sits next to\n // its declaration further down.\n useEffect(() => {\n try {\n if (pinnedGroup) sessionStorage.setItem('anet-topo-pinned-group', pinnedGroup);\n else sessionStorage.removeItem('anet-topo-pinned-group');\n } catch {}\n }, [pinnedGroup]);\n useEffect(() => {\n if (!pinnedGroup) return;\n const known = new Set(Object.values(groupKeys));\n if (!known.has(pinnedGroup)) setPinnedGroup(null);\n }, [pinnedGroup, groupKeys]);\n // Round 49 / Loop: reverse-direction of R40's edge-on-node-hover linkage.\n // R48 widened the flow hitbox to 16 px, so edges are precise enough to\n // serve as a state trigger. When the user hovers a flow edge, light up its\n // two endpoint nodes and dim the rest — \"who is this edge between\" becomes\n // visible without reading the tooltip. The set is the link's two aliases\n // (null when no edge hovered); node opacity composes this after inFocus.\n const [hoveredEdgeKey, setHoveredEdgeKey] = useState<string | null>(null);\n // Round 116 / Loop: sticky variant of hoveredEdgeKey. R56 made the\n // recent-signal rows brighten the matching edge on hover, but the\n // filter released on mouseleave — no way to lock a flow for a\n // closer look. Click-to-pin closes that gap, matching the\n // established hover→pin idiom (R60 status, R61 legend, R63 group,\n // R88 vendor). activeEdgeKey = hoveredEdgeKey ?? pinnedEdgeKey\n // below so hover still wins for spot-comparison while a pin is set.\n const [pinnedEdgeKey, setPinnedEdgeKey] = useState<string | null>(null);\n const activeEdgeKey = hoveredEdgeKey ?? pinnedEdgeKey;\n // Round 77 / Loop: hovering the \"N active links\" header chip globally\n // brightens every flow edge (1.5× opacity). Transient affordance — the\n // chip already says HOW MANY active flows exist; this answers WHERE\n // they are in one glance, without the user scanning the canvas for\n // moving particles. Reset on mouse leave.\n const [hoveredActiveLinks, setHoveredActiveLinks] = useState(false);\n // Round 115 / Loop: hub-center hover state. R52 made the hub\n // clickable (fit view) + cursor:pointer, but there was no\n // hover-time visual feedback — the click target felt guessed at\n // rather than confirmed. This state drives a subtle hint ring on\n // hover so users see the affordance before committing the click.\n const [hoveredHub, setHoveredHub] = useState(false);\n // R133: hover state for the recent-signal panel's \"+N more flows\"\n // navigation footer. Drives the on-hover opacity boost + underline\n // that signals interactivity, mirroring the hoveredHub idiom above.\n const [hoveredRecentMore, setHoveredRecentMore] = useState(false);\n // Round 346 / Loop: minimap-container hover affordance. The minimap\n // is a click-target (role=button at line ~7810, recenter-on-click +\n // Enter→resetView) but pre-R346 nothing visually changed on hover —\n // the only hint was the `cursor: crosshair` style. R346 lifts the\n // viewport rect (strokeWidth 1.5 → 1.75 + opacity 0.9 → 1.0) when\n // the user enters the minimap, marking \"this is the recenter target\n // and it's alive\". Sibling polish to the R332 minimap rounded-md →\n // rounded-lg corner family — that round refined geometry, this one\n // gives the viewport indicator inside the geometry a hover state.\n // 280ms ease-out transition list matches R199 smoothView vocabulary\n // so the visual joins the existing rhythm on the same rect.\n const [hoveredMinimap, setHoveredMinimap] = useState(false);\n // Round 347 / Loop: zoom-level readout hover-state letter-spacing\n // tween (0 → 0.5 px). The readout sandwiched between zoom-out /\n // zoom-in is a passive percent display — pre-R347 it had no hover\n // feedback at all (only a `title` tooltip). R347 extends the R344\n // (`+N more flows` footer) + R345 (panel titles) hover-letter-\n // spacing family from panel/footer surfaces into the HTML chrome\n // strip. Hovering the readout spreads its digits 0.5 px, signalling\n // \"this is alive\". tabular-nums + minWidth: 46 from R225 still lock\n // the column so the tween doesn't shove neighbouring controls.\n // 200ms ease-out joins the existing R264 color/border transition\n // list on the same span.\n const [hoveredZoomLevel, setHoveredZoomLevel] = useState(false);\n // Round 350 / Loop: reset-button icon hover-rotate preview of the\n // R184 click-spin. Pre-R350 hovering the reset button only changed\n // the button bg (white/5); the icon inside stayed perfectly still.\n // R350 nudges the icon -8° on hover — a tactile hint that this\n // button rotates the icon on click. When the click fires, the\n // R184 anet-reset-spin keyframe animation overrides the hover\n // transform for its 450 ms run (CSS animations win over transitions\n // on the same property); when the animation ends + React removes\n // the className, the inline transform eases back to whatever the\n // hover state says — either -8° (still hovering) or 0 (mouse left).\n // 350th-round milestone polish.\n const [hoveredReset, setHoveredReset] = useState(false);\n // R135: panel-wide hover-elevation. The recent-signal + legend\n // panels both already host clickable rows (R56/R116 recent rows,\n // R55/R61 legend rows) and a clickable footer (R133), so the\n // chrome itself is interactive territory. Drop-shadow boost on\n // mouseenter says \"this whole panel is alive\" — matches the R18\n // KPI-card-hover idiom from the Overview page. Single state for\n // both panels since they don't overlap; null when neither hovered.\n const [hoveredPanel, setHoveredPanel] = useState<'recent' | 'legend' | null>(null);\n // Round 80 / Loop: vendor-letter hover in the distribution chip. The\n // chip already names vendor mix (`C:5 G:3 ?:1`); hover a letter and\n // every node from OTHER vendors dims. Surfaces the breakdown spatially\n // without inventing a new pin slot. Stores the vendor `initial`\n // (single char or \"?\") that matches the tally key.\n const [hoveredVendor, setHoveredVendor] = useState<string | null>(null);\n // Round 88 / Loop: vendor filter pin. R80 added hover-to-dim on the\n // vendor letters but the filter released as soon as the cursor left\n // — vendor was the only filter dimension without a sticky variant\n // (R60 status, R63 group, R69 Cmd+K all support pin). This state\n // closes the gap. Same pattern as pinnedStatus / pinnedGroup:\n // per-tab sessionStorage persistence, Esc clears, activeVendor =\n // hoveredVendor ?? pinnedVendor so hover still wins for spot-\n // comparison while a pin is active.\n const [pinnedVendor, setPinnedVendor] = useState<string | null>(() => {\n if (typeof window === 'undefined') return null;\n try { return sessionStorage.getItem('anet-topo-pinned-vendor'); } catch { return null; }\n });\n const activeVendor = hoveredVendor ?? pinnedVendor;\n useEffect(() => {\n try {\n if (pinnedVendor) sessionStorage.setItem('anet-topo-pinned-vendor', pinnedVendor);\n else sessionStorage.removeItem('anet-topo-pinned-vendor');\n } catch {}\n }, [pinnedVendor]);\n // R89: stale-purge. If the fleet's vendor distribution changes such\n // that a previously-pinned vendor is gone (last node of that\n // vendor disconnected), clear the pin so the chip row doesn't\n // show \"filter: A · 0\" forever. Matches the same defensive purge\n // pattern pinnedGroup uses higher up — sessionStorage survives\n // reloads, but a stored value that no longer matches reality\n // would paint with an impossible filter.\n useEffect(() => {\n if (pinnedVendor && !vendorDist.some(v => v.initial === pinnedVendor)) {\n setPinnedVendor(null);\n }\n }, [pinnedVendor, vendorDist]);\n // Round 55 / Loop: hovering a legend status row dims nodes whose status\n // doesn't match. The legend was passive — \"what does this colour mean\".\n // Now it answers \"show me all of these\" the same way R8 group-focus\n // answers \"show me this team\". Three values match the legend rows.\n const [hoveredStatus, setHoveredStatus] = useState<'working' | 'idle' | 'offline' | null>(null);\n // Round 60 / Loop: sticky variant of `hoveredStatus`. R55 only filters\n // while the user is actively hovering the legend; for sweeping a fleet\n // you want to LOCK the filter. Each segment of the R31 pressure bar\n // toggles a pin — click again on the same segment to release. The node\n // opacity formula reads `activeStatus = hoveredStatus ?? pinnedStatus`\n // so hover transiently overrides a pin (handy for spot-comparison)\n // without nuking it.\n // Round 66 / Loop: pin survives a page reload via sessionStorage —\n // per-tab not per-browser (a new tab starts clean, intentionally).\n // The init reader validates against the known status set so a stale\n // / corrupt value can't paint the canvas with an impossible filter.\n const [pinnedStatus, setPinnedStatus] = useState<'working' | 'idle' | 'offline' | null>(() => {\n if (typeof window === 'undefined') return null;\n try {\n const v = sessionStorage.getItem('anet-topo-pinned-status');\n return (v === 'working' || v === 'idle' || v === 'offline') ? v : null;\n } catch { return null; }\n });\n const activeStatus = hoveredStatus ?? pinnedStatus;\n // R66: sync pinnedStatus into sessionStorage when it changes. Paired\n // with the matching effect for pinnedGroup higher up.\n useEffect(() => {\n try {\n if (pinnedStatus) sessionStorage.setItem('anet-topo-pinned-status', pinnedStatus);\n else sessionStorage.removeItem('anet-topo-pinned-status');\n } catch {}\n }, [pinnedStatus]);\n // R69: listen for Cmd+K palette pin actions. The palette can't reach\n // this component's state directly so it dispatches a CustomEvent;\n // we react by setting pinnedStatus / pinnedGroup. The palette also\n // writes to sessionStorage in lockstep, so the R66 init readers\n // would pick it up on reload — the event handler is what keeps the\n // currently-mounted canvas in sync without one.\n useEffect(() => {\n const onPin = (e: Event) => {\n const detail = (e as CustomEvent).detail || {};\n if (detail.kind === 'clear') {\n // R90: extend the universal clear to vendor. R69 only cleared\n // status + group because pinnedVendor didn't exist yet; R88\n // shipped the third pin and R90 closes the palette gap so\n // \"Clear topology filters\" really means all of them.\n // R117: extend again to pinnedEdgeKey — R116 added the 4th\n // pin dim and this command must clear it too or the edge stays\n // bright in the canvas while every other pill clears.\n setPinnedStatus(null);\n setPinnedGroup(null);\n setPinnedVendor(null);\n setPinnedEdgeKey(null);\n } else if (detail.kind === 'clear-vendor') {\n // R90: granular vendor-only clear so power users can release\n // just the vendor without disturbing status/group pins.\n setPinnedVendor(null);\n } else if (detail.kind === 'clear-edge') {\n // R117: granular edge-only clear, mirror of R90 clear-vendor.\n setPinnedEdgeKey(null);\n } else if (detail.kind === 'status') {\n const v = detail.value;\n if (v === 'working' || v === 'idle' || v === 'offline') setPinnedStatus(v);\n } else if (detail.kind === 'group' && typeof detail.value === 'string') {\n setPinnedGroup(detail.value);\n } else if (detail.kind === 'vendor' && typeof detail.value === 'string') {\n // R108: palette pin-vendor support. R69 added pin-status, R88\n // added clickable vendor letters in the chip row, R90 added\n // granular clear-vendor — but the palette never gained a\n // PIN-vendor command. This branch listens for it; the matching\n // commands live in CommandPalette R108. The stale-purge\n // useEffect higher up already drops a pin whose vendor isn't\n // in the current distribution, so commands for an absent\n // vendor are harmless.\n setPinnedVendor(detail.value);\n }\n };\n window.addEventListener('anet:topo-pin', onPin);\n return () => window.removeEventListener('anet:topo-pin', onPin);\n }, []);\n // R74 listener for layout + view palette commands lives below\n // fitView's declaration — see further down in the file.\n const hoveredEdgeEndpoints = useMemo<Set<string> | null>(() => {\n // R116: compose hover ?? pin so pinning a row via click keeps the\n // endpoint ring + edge ladder lit after mouseleave.\n if (!activeEdgeKey) return null;\n const link = flowLinks.find(l => l.key === activeEdgeKey);\n return link ? new Set([link.from, link.to]) : null;\n }, [activeEdgeKey, flowLinks]);\n\n // --- Round 103 (issue #81): fullscreen + zoom + pan interaction layer ---\n // DIY native (no d3 / svg-pan-zoom): wrap the topology content in a single\n // <g transform> and drive it with wheel + pointer-drag. The panel <rect>\n // backdrop stays fixed so panning never reveals empty canvas. View state\n // {zoom,x,y} persists to localStorage (same sticky pattern as brand flag).\n const VIEWBOX_W = 1000;\n const VIEWBOX_H = 680;\n const ZOOM_MIN = 0.5;\n const ZOOM_MAX = 4;\n const containerRef = useRef<HTMLDivElement>(null);\n const svgRef = useRef<SVGSVGElement>(null);\n const [view, setView] = useState({ zoom: 1, x: 0, y: 0 });\n const viewRef = useRef(view);\n const dragRef = useRef({ active: false, startX: 0, startY: 0, baseX: 0, baseY: 0 });\n const [isFullscreen, setIsFullscreen] = useState(false);\n // Round 184 / Loop: chrome reset button gets a one-shot icon spin\n // on click. resetSpinning is armed for 450ms (matches the CSS\n // animation duration in globals.css `.anet-reset-spin`); the SVG\n // icon picks up the class while armed. Same arming pattern as\n // R168/R169/R170/R171 smoothView flags but driving a CSS animation\n // instead of a CSS transition. prefers-reduced-motion blanket\n // override in R29 globals.css neutralises animation-duration\n // universally so the spin completes in 1ms when reduced motion is\n // requested.\n const [resetSpinning, setResetSpinning] = useState(false);\n const armResetSpin = () => {\n setResetSpinning(true);\n setTimeout(() => setResetSpinning(false), 460);\n };\n // Round 186 / Loop: chrome zoom-in / zoom-out buttons get a brief\n // icon pop on click — same click-feel idiom R184 added for the\n // reset spin, but a scale pulse rather than a rotation since +/−\n // icons rotating wouldn't read semantically. Tracks which button\n // is currently popping so only that icon picks up the class.\n // Arms for 240ms (CSS animation 220ms + 20ms buffer) so a quick\n // re-click can replay cleanly.\n // Round 249 / Loop: extend chromePopping to cover ring/grid layout\n // toggle + fullscreen button alongside the existing zoom-in/zoom-out.\n // Pre-R249 only the zoom buttons fired the R186 .anet-chrome-pop scale\n // pulse on click; the other chrome controls (layout toggle, fullscreen)\n // had no transient \"I just clicked\" signal — silent click → state change\n // with only the post-click visual difference to confirm action. R249\n // gives every clickable chrome control the same 220ms scale pulse, so\n // the whole strip speaks one consistent click vocabulary. Reset button\n // keeps its own R184 rotation animation (different gesture, semantic).\n // Node-size S/M/L keep their R171 layoutSwitching crossfade (already\n // gestural). Type union grows but the helper signature stays one-arg.\n type ChromePop = 'zoom-in' | 'zoom-out' | 'layout-ring' | 'layout-grid' | 'fullscreen' | 'size-S' | 'size-M' | 'size-L';\n const [chromePopping, setChromePopping] = useState<ChromePop | null>(null);\n const popChrome = (which: ChromePop) => {\n setChromePopping(which);\n setTimeout(() => setChromePopping(prev => prev === which ? null : prev), 240);\n };\n // Issue #100: singleton chat popover. One alias at a time — clicking\n // another node swaps the target and the conversation switches in place.\n const [chatAlias, setChatAlias] = useState<string | null>(null);\n // Round 14 / Loop: one-shot click ripple — fades outward from the clicked\n // node so the click registers physically before the popover snaps in.\n // Pairs with the Round 11 chat-focus ring: ripple expands, ring locks on.\n // Keyed by ts so re-clicking the same node remounts the <circle> and the\n // SMIL <animate> replays. Cleared 600ms after click (longer than the\n // 500ms animation so a final frame at opacity 0 is still in the tree).\n const [clickRipple, setClickRipple] = useState<{\n ts: number; x: number; y: number; r0: number; color: string;\n } | null>(null);\n // #84: when a node is renamed while its chat popover is open, follow the\n // rename so the conversation keeps targeting a live alias.\n useEffect(() => {\n if (renameSignal && renameSignal.from === chatAlias) {\n setChatAlias(renameSignal.to);\n }\n // chatAlias intentionally omitted — only react to a new rename signal,\n // not to the user opening/closing the popover.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [renameSignal]);\n\n useEffect(() => { viewRef.current = view; }, [view]);\n\n // restore persisted view once on mount\n useEffect(() => {\n try {\n const raw = localStorage.getItem('anet-topo-view');\n if (raw) {\n const v = JSON.parse(raw);\n if (typeof v?.zoom === 'number') {\n setView({\n zoom: Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, v.zoom)),\n x: typeof v.x === 'number' ? v.x : 0,\n y: typeof v.y === 'number' ? v.y : 0,\n });\n }\n }\n } catch {}\n }, []);\n\n // Round 28 / Loop: first-paint auto-fit. Round 27's cellH bump means\n // dense grids (≥6-7 rows) overflow the 680-px viewBox at the natural\n // 100% zoom — a new user sees the topology with its bottom rows\n // clipped and no hint that there's more below. Auto-fit on first\n // paint (and only if the user has no persisted view) sets the\n // initial zoom so all content fits. Subsequent zoom changes persist\n // and override the auto-fit on reload; explicit reset (0 key) still\n // goes back to 100% so the gesture's \"reset to natural size\" semantic\n // is preserved.\n //\n // Capture pre-mount persistence in a useState initializer — the\n // existing `persist` effect (declared below) runs on first render\n // with the default {1,0,0} view and writes it to localStorage before\n // the auto-fit effect (deps on async-arriving sessions) gets a turn.\n // Reading localStorage from the effect would see that write and\n // skip the fit. The useState snapshot fires once, before any effects.\n const [hadPersistedViewOnMount] = useState<boolean>(\n () => typeof window !== 'undefined' && !!localStorage.getItem('anet-topo-view'),\n );\n const autoFitDoneRef = useRef(false);\n useEffect(() => {\n if (autoFitDoneRef.current) return;\n if (hadPersistedViewOnMount) {\n autoFitDoneRef.current = true;\n return;\n }\n if (layout !== 'grid' || sessions.length === 0 || !gridContentBottom) return;\n if (gridContentBottom <= VIEWBOX_H) {\n autoFitDoneRef.current = true; // no overflow → no fit needed\n return;\n }\n const fitZoom = Math.max(ZOOM_MIN, Math.min(1, VIEWBOX_H / gridContentBottom));\n setView({ zoom: fitZoom, x: 0, y: 0 });\n autoFitDoneRef.current = true;\n }, [layout, sessions.length, gridContentBottom, hadPersistedViewOnMount]);\n\n // persist view\n useEffect(() => {\n try { localStorage.setItem('anet-topo-view', JSON.stringify(view)); } catch {}\n }, [view]);\n\n // track fullscreen state (button label + Esc-exit sync)\n useEffect(() => {\n const onFsChange = () => setIsFullscreen(document.fullscreenElement === containerRef.current);\n document.addEventListener('fullscreenchange', onFsChange);\n return () => document.removeEventListener('fullscreenchange', onFsChange);\n }, []);\n\n // wheel zoom — native non-passive listener so preventDefault() actually\n // stops the page from scrolling. Uses functional setState so the listener\n // never goes stale and can attach once.\n useEffect(() => {\n const svg = svgRef.current;\n if (!svg) return;\n const onWheel = (e: WheelEvent) => {\n // Round 23 / Loop: only zoom on Ctrl/Meta+wheel when inline; plain\n // wheel over the canvas keeps scrolling the page (the topo sits at\n // the top of /; trapping page scroll mid-page is the classic\n // \"scroll-jail\" anti-pattern). In fullscreen the canvas owns the\n // viewport so any wheel zooms — no page scroll to preserve. Pinch-\n // zoom on a trackpad surfaces as ctrlKey=true natively, which is\n // also what we want (zoom the canvas, not the browser).\n if (!isFullscreen && !e.ctrlKey && !e.metaKey) return;\n e.preventDefault();\n const rect = svg.getBoundingClientRect();\n const mx = ((e.clientX - rect.left) / rect.width) * VIEWBOX_W;\n const my = ((e.clientY - rect.top) / rect.height) * VIEWBOX_H;\n setView(prev => {\n // Round 104 (issue #81 follow-up): Vincent 实测 zoom 太灵敏. The\n // old factor was a fixed 1.15x per wheel *event* — fine for a\n // mouse notch but a trackpad fires dozens of events per gesture\n // so it compounded into huge jumps. Scale the factor by deltaY\n // magnitude with a small coefficient, then clamp the per-event\n // change so no single tick moves more than ~8%.\n const factor = Math.min(1.08, Math.max(0.926, Math.exp(-e.deltaY * 0.0006)));\n const nz = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, prev.zoom * factor));\n const ratio = nz / prev.zoom;\n // keep the point under the cursor stationary\n return { zoom: nz, x: mx - (mx - prev.x) * ratio, y: my - (my - prev.y) * ratio };\n });\n };\n svg.addEventListener('wheel', onWheel, { passive: false });\n return () => svg.removeEventListener('wheel', onWheel);\n // Re-attach when the fullscreen flag toggles so the handler's\n // closure picks up the new value (capture-by-closure means we'd\n // otherwise read the stale boolean forever).\n }, [isFullscreen]);\n\n // Round 21 / Loop: pan-aware cursor. Static `grab` gave no tactile cue\n // that the canvas was actually being dragged — \"grabbing\" while\n // dragRef.current.active = true tells the hand it's moving the\n // viewport. Mirroring dragRef.active into state is the only way to\n // re-render the SVG style; the ref itself doesn't trigger renders.\n const [isPanning, setIsPanning] = useState(false);\n const onPointerDown = (e: React.PointerEvent<SVGSVGElement>) => {\n if (e.button !== 0) return;\n (e.currentTarget as Element).setPointerCapture?.(e.pointerId);\n dragRef.current = {\n active: true,\n startX: e.clientX,\n startY: e.clientY,\n baseX: viewRef.current.x,\n baseY: viewRef.current.y,\n };\n setIsPanning(true);\n };\n const onPointerMove = (e: React.PointerEvent<SVGSVGElement>) => {\n const d = dragRef.current;\n if (!d.active) return;\n const svg = svgRef.current;\n if (!svg) return;\n const rect = svg.getBoundingClientRect();\n const dx = ((e.clientX - d.startX) / rect.width) * VIEWBOX_W;\n const dy = ((e.clientY - d.startY) / rect.height) * VIEWBOX_H;\n setView(prev => ({ ...prev, x: d.baseX + dx, y: d.baseY + dy }));\n };\n const onPointerUp = (e: React.PointerEvent<SVGSVGElement>) => {\n if (!dragRef.current.active) return;\n dragRef.current.active = false;\n setIsPanning(false);\n try {\n (e.currentTarget as Element).releasePointerCapture?.(e.pointerId);\n } catch {}\n };\n\n // zoom buttons — zoom around the canvas center\n const zoomBy = (factor: number) => {\n setView(prev => {\n const nz = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, prev.zoom * factor));\n const ratio = nz / prev.zoom;\n const cx0 = VIEWBOX_W / 2;\n const cy0 = VIEWBOX_H / 2;\n return { zoom: nz, x: cx0 - (cx0 - prev.x) * ratio, y: cy0 - (cy0 - prev.y) * ratio };\n });\n };\n // Round 169 / Loop: discrete-zoom wrapper that arms the R168\n // smoothView flag before invoking zoomBy. Keyboard + / − and\n // the chrome zoom buttons fire once per gesture, so each\n // 1.2× step deserves the same 300ms glide R168 gives\n // reset/fit. Wheel zoom keeps calling zoomBy() directly —\n // every tick should respond live without lag. The arming\n // path is identical to resetView/fitView so all four\n // discrete-zoom surfaces share one timing constant.\n const zoomByDiscrete = (factor: number) => {\n setSmoothView(true);\n setTimeout(() => setSmoothView(false), 350);\n zoomBy(factor);\n };\n // Round 168 / Loop: smoothView is a one-shot flag that arms a\n // CSS transition on the viewport <g> transform attribute. Set\n // true when resetView/fitView fires; the inline style on the\n // viewport <g> reads this flag and conditionally applies\n // `transition: transform 300ms ease-out`. Auto-clears after\n // 350ms (just past the transition end) via setTimeout so\n // subsequent pan/wheel zoom stays snappy with no lag. Keeping\n // it as state (vs ref) so the React rerender re-applies the\n // style attribute synchronously when the transform also\n // changes — both attribute mutation and transition class\n // arrive in the same paint frame, which is what triggers the\n // browser to animate between the old and new transform values.\n const [smoothView, setSmoothView] = useState(false);\n const armSmoothView = () => {\n setSmoothView(true);\n setTimeout(() => setSmoothView(false), 350);\n };\n const resetView = () => {\n armSmoothView();\n setView({ zoom: 1, x: 0, y: 0 });\n };\n\n // Round 29 / Loop: `f` = fit-to-content. Shared by the Round 28\n // first-paint auto-fit effect and the keyboard handler so the math is\n // in one place. When content already fits at natural zoom, this is\n // effectively a \"recenter\" — `f` always lands on a known good view.\n // R168: arm smoothView so the transition glides instead of snapping\n // when the operator invokes fit-to-content via the hub click (R52),\n // chrome button, `f` key, or palette command.\n const fitView = useCallback(() => {\n const zoom = !gridContentBottom || gridContentBottom <= VIEWBOX_H\n ? 1\n : Math.max(ZOOM_MIN, Math.min(1, VIEWBOX_H / gridContentBottom));\n setSmoothView(true);\n setTimeout(() => setSmoothView(false), 350);\n setView({ zoom, x: 0, y: 0 });\n }, [gridContentBottom]);\n\n // R74: listen for layout + view palette commands. Sister to R69's\n // pin listener — palette dispatches a CustomEvent, the reducer here\n // calls toggleLayout / fitView. Sits below fitView's declaration so\n // the deps list resolves cleanly.\n useEffect(() => {\n const onLayout = (e: Event) => {\n const detail = (e as CustomEvent).detail || {};\n if (detail.kind === 'toggle') toggleLayout();\n };\n const onView = (e: Event) => {\n const detail = (e as CustomEvent).detail || {};\n if (detail.kind === 'fit') fitView();\n };\n window.addEventListener('anet:topo-layout', onLayout);\n window.addEventListener('anet:topo-view', onView);\n return () => {\n window.removeEventListener('anet:topo-layout', onLayout);\n window.removeEventListener('anet:topo-view', onView);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [fitView]);\n\n // Round 22 / Loop: keyboard zoom — +/= zoom in, - zoom out, 0 reset.\n // Round 29 / Loop: +f to fit content.\n // Listen on window so the user doesn't need to focus the SVG first,\n // but only act when no text input has focus (otherwise typing \"-\" in\n // a chat would zoom the topology — surprising and bad). Modifier keys\n // pass through so Cmd/Ctrl combos still hit their owners. The button\n // titles below document these shortcuts so users discover them.\n useEffect(() => {\n const onKey = (e: KeyboardEvent) => {\n if (e.ctrlKey || e.metaKey || e.altKey) return;\n const ae = document.activeElement as HTMLElement | null;\n if (ae) {\n const tag = ae.tagName;\n if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || ae.isContentEditable) return;\n }\n if (e.key === '+' || e.key === '=') { zoomByDiscrete(1.2); e.preventDefault(); }\n else if (e.key === '-' || e.key === '_') { zoomByDiscrete(1 / 1.2); e.preventDefault(); }\n else if (e.key === '0') { resetView(); e.preventDefault(); }\n else if (e.key === 'f' || e.key === 'F') { fitView(); e.preventDefault(); }\n // Round 32 / Loop: `l` toggles ring|grid. The vim-style `g l` route\n // (Audit Log) requires a preceding `g` within 1500ms; a bare `l`\n // outside that window is free for topology use.\n else if (e.key === 'l' || e.key === 'L') { toggleLayout(); e.preventDefault(); }\n // Round 62 / Loop: Esc clears the R60/R61 pinned status filter so\n // users have a universal-cancel keyboard out. Esc on an open chat\n // is owned by ChatPopover (which only mounts when chatAlias is\n // set), so this handler is effectively scoped to \"no chat open\".\n // We additionally guard on chatAlias to be explicit — if the chat\n // closed mid-cycle, the pin can still be cleared on the next Esc.\n // R63: extends to pinnedGroup too. Clears WHATEVER pin is active\n // (one Esc collapses all topology pins) so the keyboard escape\n // route stays a single key, not \"Esc maybe Esc again\".\n else if (e.key === 'Escape' && !chatAlias && (pinnedStatus || pinnedGroup || pinnedVendor || pinnedEdgeKey)) {\n // R88/R116: extend the universal-cancel to vendor + edge too.\n // One Esc collapses every topology pin (matches R62/R63's\n // \"single key, not Esc-maybe-Esc-again\" promise).\n if (pinnedStatus) setPinnedStatus(null);\n if (pinnedGroup) setPinnedGroup(null);\n if (pinnedVendor) setPinnedVendor(null);\n if (pinnedEdgeKey) setPinnedEdgeKey(null);\n e.preventDefault();\n }\n };\n window.addEventListener('keydown', onKey);\n return () => window.removeEventListener('keydown', onKey);\n // zoomBy / resetView are stable wrt setView callback; fitView changes\n // with gridContentBottom so deps list catches it. R62 adds chatAlias\n // and pinnedStatus so the Escape branch reads fresh state (re-binding\n // the listener on these state changes is sub-ms — cheaper than refs).\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [fitView, chatAlias, pinnedStatus, pinnedGroup, pinnedVendor, pinnedEdgeKey]);\n\n const toggleFullscreen = () => {\n const el = containerRef.current;\n if (!el) return;\n if (document.fullscreenElement) {\n document.exitFullscreen?.();\n } else {\n const req =\n el.requestFullscreen ||\n (el as unknown as { webkitRequestFullscreen?: () => void }).webkitRequestFullscreen;\n req?.call(el);\n }\n };\n\n return (\n <section className=\"w-full max-w-6xl mx-auto mb-8\">\n {/* Round 299 / Loop: title block bottom margin mb-3 (12px) →\n mb-4 (16px). After R298 tightened the title-block internal\n gap (12→10px) packing brand-logo + kicker + h2 into a more\n cohesive editorial unit, the outer bottom margin to the\n topology canvas should breathe more — denser title block\n + tighter follow-on space read as cramped. Bumping the\n gap below the title block lets the canvas frame\n itself more clearly as the main visual subject. 16px is\n the conventional SaaS-product section-header-to-content\n baseline (Stripe / Linear / Vercel marketing). Geometry:\n adds 4 CSS px between title block bottom and topology\n frame top — small but cumulative with the R298 internal\n tighten, the title block reads as a *deliberate* badge\n rather than a casually-stacked label. */}\n {/* Round 334 / Loop: header outer wrapper mobile gap-3 → gap-2.5\n (12 px → 10 px). The wrapper is `flex flex-col` on narrow\n viewports (title-block above, chip-row below) — at mobile\n its vertical gap was 12 px while the title-block internal\n (R298) and chip-row internal (R328) both rhythm at 10 px.\n R334 unifies the OUTER vertical gap to 10 px so mobile\n stacked layout matches the established gap-rhythm tier\n (title-block 10 / chip-row 10 / chrome 8 — R298/R328/R326).\n Desktop `sm:flex-row` is unaffected: in row mode the gap-3\n would have applied horizontally but the wrapper relies on\n `sm:justify-between` for left/right anchoring (gap is then\n decorative only between the two flex-grow groups). Net\n mobile bump: 2 px tighter vertical breathing between\n title-block + chip-row. Geometry-safe — topo-overlap-test\n reads SVG-internal bbox, not header layout. */}\n <div\n className={`flex flex-col gap-2.5 sm:flex-row sm:items-end sm:justify-between mb-4 px-1${isFullscreen ? ' hidden' : ''}`}\n data-topo-header-row\n data-topo-header-hidden={isFullscreen ? 'true' : 'false'}\n >\n {/* Round 267 / Loop: title block adopts leading-tight on both\n kicker and h2 for a tighter editorial-style rhythm. Pre-\n R267 the kicker used Tailwind's compound `text-xs` (line-\n height 16px = 1.33 ratio) and the h2 used `text-lg` (line-\n height 28px = 1.56) — adequate but loose for a kicker→\n title sequence. R267 applies `leading-tight` (1.25) to\n both, shrinking effective line-heights to 15px + 22.5px =\n 37.5px total title block height (vs 44px pre-R267 → ~15%\n more compact) while preserving the cap-top to descender\n visual proportions. Result: kicker and title read as a\n single typographic unit rather than two loosely-stacked\n lines. data-topo-section-kicker / data-topo-section-title\n attrs make both probe-able. */}\n {/* P0 (Vincent 5222 / 通信龙 R278 dispatch): integrate sleep2agi\n brand logo into the title-block.\n Why HTML title-block instead of SVG canvas: the SVG region\n has a high-frequency concurrent-editor edit race with codex\n (see project_dashboard_concurrent_editors). The title block\n is HTML-side, low edit traffic, and gives the brand mark\n first-glance presence above the topology canvas — the exact\n \"Twitter screenshot 一眼看出 sleep2agi\" outcome Vincent 5215\n asked for.\n Logo construction: inline SVG so currentColor inherits\n from the parent text-{color} class — theme-aware without\n an extra asset request. Same crescent geometry as\n public/sleep2agi-logo.svg. 36×36 px so it's clearly\n readable at a16:9 Twitter crop, paired with the kicker +\n h2 typography via flex layout. text-cyan-300 in cyber +\n text-emerald-600 in light keeps the moon brand-aligned\n with the canvas accent palette. */}\n {/* Round 298 / Loop: title-block gap-3 (12px) → gap-2.5 (10px).\n The R297 codex-bundle brand-logo at 36×36 paired with the\n R285 tracking-widest kicker + R286 tracking-tight h2 forms\n an editorial \"logo + title\" unit. At gap-3 the 12px between\n logo right-edge and text left-edge reads as two separate\n elements — visual spacing slightly outpaces the relationship\n density (logo IS the brand mark for the kicker's\n \"Network Topology\"). gap-2.5 (10px) is the tight-pack\n convention SaaS-product header logos use (Stripe / Vercel /\n Linear top-nav logo + product name spacing), grouping logo\n + title as one read. Geometry: 36 + 10 + ~120 (title text\n width) = 166px total title-block width vs 168px pre-R298 —\n no measurable layout shift, just a deliberate tighter\n grouping. */}\n <div className=\"flex items-center gap-2.5\">\n {/* Round 297 / Loop: brand-logo color picks up the 200ms ease-\n out transition. Pre-R297 the moon glyph had theme-\n conditional color (cyber #67e8f9 cyan ↔ light #0d9488\n teal) but no transition declaration — flipping themes\n made the brand mark snap to its new color in one frame,\n jarring against the surrounding R245/R246/R247/R253/R254\n family that smooths every neighbouring fill / stroke /\n filter at the same 200ms cadence. Adding the transition\n brings the brand mark into the coordinated theme-toggle\n choreography: title block + canvas + chrome all ease as\n one unit. CSS color transition is well supported on the\n `color` property (which currentColor inside the masked\n <rect> inherits), so no SMIL trick needed. */}\n {/* Round 316 / Loop: brand-logo width/height 36 → 40 for\n slightly stronger first-glance presence. After R298\n tightened the title-block flex gap (12→10px) and R299\n widened the bottom margin (12→16px), the logo can hold\n more visual weight to balance the kicker+h2 stack on\n its right. 40px is 11% larger by edge, ~23% by area —\n visible bump on a Twitter-screenshot crop without\n overpowering the h2 at text-lg/font-semibold (R286).\n viewBox 32×32 unchanged so the inner crescent geometry\n scales proportionally. */}\n <svg\n width=\"40\" height=\"40\" viewBox=\"0 0 32 32\" aria-hidden\n className=\"shrink-0\"\n data-topo-brand-logo\n style={{\n color: isLight ? '#0d9488' : '#67e8f9',\n transition: 'color 200ms ease-out',\n }}\n >\n <mask id=\"s2a-titleblock-moon-mask\">\n <rect width=\"32\" height=\"32\" fill=\"black\" />\n <circle cx=\"16\" cy=\"16\" r=\"13\" fill=\"white\" />\n <circle cx=\"20.5\" cy=\"14.5\" r=\"11\" fill=\"black\" />\n </mask>\n <rect width=\"32\" height=\"32\" fill=\"currentColor\" mask=\"url(#s2a-titleblock-moon-mask)\" />\n </svg>\n <div>\n {/* Round 285 / Loop: kicker tracking-wider → tracking-widest.\n An uppercase eyebrow label at text-xs benefits from\n wider letter-spacing — Tailwind's tracking-widest is\n 0.1em vs tracking-wider's 0.05em. At small caps,\n 0.1em is the conventional SaaS-eyebrow spacing (Stripe,\n Linear, Vercel marketing kicker style); 0.05em reads\n closer to body-text density. The widened spacing\n telegraphs \"this is a label, not a sentence\" without\n changing color or size, deepening the editorial\n hierarchy R267 set up between kicker and h2. */}\n {/* Round 296 / Loop: kicker text-gray-600 → text-gray-500\n for slightly better legibility on the dark cyber backdrop.\n gray-600 (#4b5563) read as a near-invisible label on\n cyber (the canvas + side rail are deeply dark); gray-500\n (#6b7280) lifts the eyebrow into the band where the eye\n registers it as a deliberate label vs swallowed text,\n while still sitting clearly below text-white h2 title\n in the visual hierarchy. Tailwind classes are theme-\n neutral so the bump applies to both themes; in light\n theme gray-500 is still appropriate as a muted-label\n shade on white bg. Hierarchy preserved: title-white >\n kicker-gray-500 (R285 tracking-widest still in place). */}\n {/* Round 300 / Loop (milestone): kicker picks up font-medium\n (500). Pre-R300 the eyebrow used default font-weight\n (400/normal). At text-xs (12px) + uppercase + R285\n tracking-widest, default-weight letters read slightly\n under-authored — uppercase at small sizes wants a touch\n more stroke weight to feel like a deliberate label.\n font-medium (500) is the conventional SaaS-eyebrow\n weight (Stripe / Vercel / Linear marketing kicker\n style — same family that informed R285's tracking-\n widest decision). Stays clearly below the h2's font-\n semibold (600) + larger size so hierarchy is preserved:\n h2 (text-lg/600) > kicker (text-xs/500/gray-500).\n R300 marks the milestone of 25 rounds (R275-R300) of\n continuous TopoGraph polish + codex's Vincent 5215/\n 5222 logo asset+integration work. */}\n <div className=\"text-xs uppercase text-gray-500 tracking-widest leading-tight font-medium\" data-topo-section-kicker>Network Topology</div>\n {/* Round 286 / Loop: title 'Command mesh' adopts tracking-tight\n (-0.025em) to complement R285 kicker tracking-widest. Wide\n eyebrow + tight headline is the conventional editorial\n pairing — Apple / Stripe / Vercel / Linear all use this\n dual-axis typographic rhythm. The kicker's 0.1em pushes\n letters APART (label feel); the headline's -0.025em pulls\n them TOGETHER (deliberate, designed-headline feel). At\n text-lg (18px) the shift is ~0.45px per gap — small but\n cumulatively legible across 12 characters. font-semibold\n (600) stays — tracking-tight does the heavy lifting for\n the editorial register. */}\n <h2 className=\"text-lg text-white font-semibold leading-tight tracking-tight\" data-topo-section-title>Command mesh</h2>\n </div>\n </div>\n {/* Round 328 / Loop: chip-row strip wrapper gap 2 → 2.5\n (8px → 10px between chips). Pre-R328 the inter-chip gap\n sat at 8px while each chip's own horizontal padding was\n `px-2.5` (10px) — the chip's internal space was wider\n than the gap between chips, so adjacent chips read as\n \"touching\" rather than \"neighboring\". Bumping the gap\n to 10px makes inter-chip = chip-padding, visually\n balancing the rhythm. Sibling treatment to R298 title-\n block `gap-2.5` (brand-logo ↔ kicker/title) and R326\n chrome strip `gap-2` extension family. Layout strip:\n R298 title-block gap-2.5 (top of canvas)\n R328 chip-row gap-2.5 (below title) ← NEW\n R326 chrome gap-2 (bottom of canvas)\n Risk-bounded: chip-row uses `flex-wrap`; if it wraps to\n a new line on narrow viewports the row-gap also bumps to\n 10px, which only helps mobile rhythm. Topo-overlap-test\n is HTML-overlay-only at this scope; SVG viewBox layout\n untouched. */}\n <div className=\"flex flex-wrap items-center gap-2.5 text-xs\">\n {/* Issue #87: ring | grid layout toggle — segmented control,\n persisted to localStorage anet-topo-layout.\n Round 163 / Loop: bring the layout toggle into the R154\n chrome-button focus convention. Pre-R163 the buttons had\n aria-pressed + transition-colors but no focus-visible ring\n (browser default — invisible against dark canvas) and the\n inactive variant only nudged text from gray-500 → gray-400\n on hover with no bg tint, so the hover-to-click affordance\n was barely perceptible.\n\n R163 closes both gaps to match R154:\n focus-visible:ring-2 focus-visible:ring-cyan-400/60\n → keyboard users see exactly which segment is focused\n hover:bg-cyan-500/5 (inactive)\n → mouse hover shows a faint cyan ghost of the active\n state, signalling 'click to switch'\n hover:bg-cyan-500/20 (active)\n → active button responds too, says 'still clickable'\n data-topo-chrome-layout for testability symmetric with the\n R154 chrome buttons (data-topo-chrome-zoom-in / -reset /\n -fullscreen etc).\n\n Round 260 / Loop: chip-row semantic gap — Layout toggle is\n the only CONTROL in the chip row; everything that follows\n (working / online / pressure / vendor letters / active-\n links / filter pills / freshness) is READ-ONLY display.\n Pre-R260 all 8 children sat at uniform gap-2 (8px) — the\n spatial signal read as \"8 separate things\" instead of\n \"1 control + 7 display\". mr-1 (4px) on the Layout toggle\n stacks on top of the parent flex's gap-2 (8px) for an\n effective 12px gap before the first status chip — same\n law-of-proximity pattern R255 applied to the bottom-right\n chrome strip (fleet vs view groups). data-topo-chrome-\n layout-trailer marks the boundary surface for the gap\n probe. */}\n {/* Round 268 / Loop: Layout toggle border unified with the\n chrome strip's theme-aware borderColor. Pre-R268 the\n wrapper + Grid button's internal divider used hardcoded\n `border-gray-500/25` (pale gray, fixed in both themes)\n while the bottom-right chrome strip (nodeSize, zoom)\n used pal.containerBorder (cyber #2a2a4a dark indigo ↔\n light #e3e6eb pale gray). Visible mismatch in cyber\n theme: Layout toggle border read as pale gray while\n chrome strip borders read as darker indigo — two\n different border colors on visually-analogous\n segmented controls. R268 replaces the hardcoded class\n with inline pal.containerBorder + a border-color\n transition, so the Layout toggle (a) matches the chrome\n strip border color and (b) joins the canvas-wide\n theme-ease vocabulary (eases on cyber↔light toggle\n instead of snapping). Same change applied to the Grid\n button's border-l on line ~1493. */}\n {/* Round 329 / Loop: Layout toggle wrapper `mr-1` → `mr-0.5`\n to compensate for R328's chip-row gap bump (8 → 10 px).\n R260 designed for an effective 12 px gap between the\n Layout CONTROL and the first DISPLAY chip (working /\n online / etc): mr-1 (4 px) + chip-row gap-2 (8 px) = 12.\n R328 widened chip-row to gap-2.5 (10 px), pushing the\n effective gap to 14 px — semantically still \"control\n vs display\" but louder than R260 specified.\n R329 dials mr-1 → mr-0.5 (2 px) so the effective gap\n returns to 12 px (mr-0.5 + chip-row gap-2.5 = 2 + 10).\n Keeps the law-of-proximity semantic R260 designed\n while honoring R328's wider baseline rhythm. data-topo-\n chrome-layout-trailer attr unchanged — it still marks\n the boundary surface for the gap probe. */}\n {/* Round 375 / Loop: Layout-toggle wrapper rounded-md → rounded-\n lg (6 → 8 px). Extends the corner-radius cascade family\n to the chrome-strip layout-toggle wrapper:\n R330 canvas wrapper rounded-xl 12 px\n R331 SVG panels rx=10 10 px\n R332 minimap container rounded-lg 8 px\n R375 Layout-toggle wrapper rounded-lg 8 px (this round)\n Pre-R375 the wrapper at rounded-md (6 px) was the only\n chrome-strip container still using the smaller corner\n radius — both R330 outer wrapper and R332 minimap sit at\n ≥ 8 px, so the Layout toggle's 6 px stood out as a\n tighter corner against the family. R375 brings it into\n the rounded-lg tier where the minimap already lives.\n Pure paint change — overflow-hidden still clips the\n inner buttons' bg-cyan-500/15 tints; no layout shift.\n R268 border-color + 200ms transition + R329 mr-0.5 +\n data-topo-chrome-layout-trailer all preserved. */}\n <div\n className=\"mr-0.5 inline-flex rounded-lg border overflow-hidden\"\n style={{ borderColor: pal.containerBorder, transition: 'border-color 200ms ease-out' }}\n role=\"group\"\n aria-label=\"Topology layout\"\n data-topo-chrome-layout-trailer\n data-topo-chrome-layout-radius=\"rounded-lg\"\n >\n <button\n onClick={() => { popChrome('layout-ring'); if (layout !== 'ring') toggleLayout(); }}\n aria-pressed={layout === 'ring'}\n title=\"Ring layout (l to toggle)\"\n data-topo-chrome-layout=\"ring\"\n data-topo-chrome-layout-active={layout === 'ring' ? 'true' : 'false'}\n data-topo-chrome-layout-ring-popping={chromePopping === 'layout-ring' ? 'true' : 'false'}\n // Round 196 / Loop: add active: (pressed) state for tactile\n // click feedback — bridges mouse-down → R186/R184/R192 pop-on-\n // release. Selected variant deepens to cyan-500/25 (one tier\n // above its hover:cyan-500/20); unselected variant deepens\n // to cyan-500/15 (one tier above its hover:cyan-500/5).\n // Round 249 / Loop: chrome-pop joins the click handshake —\n // mouse-down deepens cyan press (R196), release fires\n // .anet-chrome-pop on the button (R249) AND triggers\n // toggleLayout if state changes. The pop runs even when\n // clicking the already-active layout (no state change),\n // confirming the click was received either way.\n /* Round 306 / Loop: focus-visible:ring-2 → ring-1 unifies\n with the rest of the chrome button family. Pre-R306\n the Layout toggle (Ring/Grid) used `focus-visible:\n ring-2` (2px outline) while nodeSize S/M/L (line\n ~7291), zoom -/+ (~7328/~7395), reset (~7417), and\n fullscreen (~7477) all use `focus-visible:ring-1`\n (1px outline). Two different focus-ring widths on\n visually-analogous chrome controls — same R268\n border-color unification + R288 icon-stroke\n unification family. Reducing Ring/Grid to ring-1\n lets all 7 chrome buttons share one focus-ring\n weight; cyan-400/60 + ring-inset retained. The\n R163/R196 hover/active deeps + R249 chrome-pop\n click feedback continue unchanged. */\n // R351: hover:tracking-wide extends the R344/R345/R347\n // hover-letter-spacing family to a 4th surface (chrome-\n // strip Ring/Grid pair). transition-colors className\n // dropped in favour of an inline transition spec that\n // bundles bg/color (150ms ease) + letter-spacing\n // (200ms ease-out) — Tailwind's transition-colors\n // doesn't list letter-spacing, so without this the\n // hover:tracking-wide would snap. Sibling change on\n // the Grid button below.\n // Round 492 / Loop — add `active:scale-95` press feedback\n // alongside R196's `active:bg-cyan-500/25` color-deepen.\n // Pre-R492 the chrome-strip Ring/Grid buttons had color\n // tactile (deeper cyan on mouse-down) + R249 chrome-pop\n // on release, but no transform during the press itself —\n // the button stayed planted between mouse-down and pop.\n // Adding `active:scale-95` (5% compression) on the\n // pressed pseudo-state, with `transform 150ms ease-out`\n // bundled into the inline transition list, gives haptic-\n // like push-back feedback. The press-down (down to 95%\n // scale) eases in over 150ms in sync with the bg/color\n // deepen; the release auto-springs back to scale-100 via\n // the same transition, then R249's anet-chrome-pop class\n // overlays the release-pop. Matching `transform-gpu`\n // promotes the layer so the scale doesn't trigger\n // layout/paint thrash. Sibling change on Grid below.\n className={`px-2.5 py-1 focus:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset hover:tracking-wide active:scale-95 transform-gpu ${layout === 'ring' ? 'bg-cyan-500/15 text-cyan-300 font-medium hover:bg-cyan-500/20 active:bg-cyan-500/25' : 'text-gray-400 hover:text-cyan-300 hover:bg-cyan-500/5 active:bg-cyan-500/15'} ${chromePopping === 'layout-ring' ? ' anet-chrome-pop' : ''}`}\n style={{ transition: 'background-color 150ms ease, color 150ms ease, letter-spacing 200ms ease-out, transform 150ms ease-out' }}\n >\n Ring\n </button>\n <button\n onClick={() => { popChrome('layout-grid'); if (layout !== 'grid') toggleLayout(); }}\n aria-pressed={layout === 'grid'}\n title=\"Grid layout (l to toggle)\"\n data-topo-chrome-layout=\"grid\"\n data-topo-chrome-layout-active={layout === 'grid' ? 'true' : 'false'}\n data-topo-chrome-layout-grid-popping={chromePopping === 'layout-grid' ? 'true' : 'false'}\n // Round 196 / Loop: R163 layout-toggle Grid variant picks up\n // press-state — same tier pattern as Ring above.\n // Round 249 / Loop: chrome-pop on click — same as Ring.\n // Round 306 / Loop: focus-visible:ring-2 → ring-1 sibling\n // change to Ring above — unifies focus-ring width across\n // all chrome buttons.\n // R351 sibling — Grid button picks up hover:tracking-wide\n // + inline transition spec. Same vocabulary as Ring.\n // R492 sibling — Grid button picks up active:scale-95\n // press feedback + transform in transition list. Same\n // vocabulary as Ring above.\n className={`px-2.5 py-1 border-l focus:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset hover:tracking-wide active:scale-95 transform-gpu ${layout === 'grid' ? 'bg-cyan-500/15 text-cyan-300 font-medium hover:bg-cyan-500/20 active:bg-cyan-500/25' : 'text-gray-400 hover:text-cyan-300 hover:bg-cyan-500/5 active:bg-cyan-500/15'} ${chromePopping === 'layout-grid' ? ' anet-chrome-pop' : ''}`}\n /* Round 268 / Loop: Grid button's left border (the\n internal divider between Ring and Grid) picks up\n pal.containerBorder, matching the wrapper change at\n line ~1460 and the chrome strip's segmented borders\n (nodeSize, zoom). The R268 transition-colors className\n used to carry the border-color ease; R351 unfolds the\n transition list into the inline spec below so the\n letter-spacing tween rides alongside without snapping\n the border-color flip — border-color 200ms ease-out\n keeps R268's theme-toggle smoothness intact.\n R492 adds `transform 150ms ease-out` so active:scale-95\n eases smoothly. */\n style={{ borderColor: pal.containerBorder, transition: 'background-color 150ms ease, color 150ms ease, border-color 200ms ease-out, letter-spacing 200ms ease-out, transform 150ms ease-out' }}\n >\n Grid\n </button>\n </div>\n {/* R79: working + online count chips become hover affordances —\n extends R77's chip-hover pattern to status counts. Hover the\n working chip → setHoveredStatus('working') so the R55 dim\n mechanic kicks in (same as hovering the SVG legend \"working\"\n row, just a different surface for the same gesture).\n The online chip uses the same setHoveredStatus('idle') path\n when there's idle but no working — falling back to working\n if any working node exists; \"online\" without a single bucket\n isn't part of the R55 type set, so this chip routes to the\n dominant online sub-tier instead of inventing a new state.\n Cursor only flips when there's anything to highlight. */}\n {/* R82: pin-mirror. R60 pressure-bar segments visualise the\n pinned status via an inset boxShadow; R61 legend rows via a\n concentric r=8 ring; the chip-row chips next to them did\n not — so a user who pinned via Cmd+K (R69) or the legend\n had no chip-row signal that \"working\" was the current\n filter. Mirror the pressure-bar treatment here so all\n status surfaces sing in unison. The online chip mirrors\n for idle pins (working ⊆ online; the routing in\n onMouseEnter already treats online as the idle fallback). */}\n {(() => {\n // R113 / Loop: extend the R97-R102/R101 alias-list tooltip\n // sweep to the remaining chip-row affordances. R79 made\n // these chips hoverable; their generic \"Hover to highlight\"\n // titles never said WHICH nodes match. Compute the alias\n // lists once for both chips to share.\n const workingAliases = onlineNodes.filter(s => s.status === 'working').map(s => s.alias);\n const onlineAliases = onlineNodes.map(s => s.alias);\n const truncate = (list: string[]) => {\n const head = list.slice(0, 8).join(', ');\n const tail = list.length > 8 ? ` + ${list.length - 8} more` : '';\n return head + tail;\n };\n const workingTitle = workingCount === 0\n ? undefined\n : pinnedStatus === 'working'\n ? `${truncate(workingAliases)} — pinned, Esc to clear`\n : `${truncate(workingAliases)} — hover highlights, click to pin`;\n // R140: online chip title gains a \"click to open /nodes\" tail\n // when interactive. R79 made the cursor pointer-shaped but\n // wired nothing; R136 + R139 closed the same lie on two\n // sibling chips by wiring real actions. The online chip\n // can't pin a single status (online = working + idle,\n // not a single pinnedStatus value), so a pin idiom would\n // be semantically wrong. /nodes is the natural full-list\n // destination — same \"click chip for the full list\" idiom\n // the active-links chip uses (R136 → /messages).\n const onlineTitle = onlineNodes.length === 0\n ? undefined\n : pinnedStatus === 'idle'\n ? `${truncate(onlineAliases)} — pinned, Esc to clear`\n : `${truncate(onlineAliases)} — hover highlights · click to open /nodes`;\n return (\n <>\n <span\n // Round 201 / Loop: the working chip joins the\n // \"chip's hover state deepens its OWN identity colour\"\n // family that R193 opened (active-links chip) and R195\n // extended (recent-panel footer). Pre-R201 hovering the\n // working chip fired R55 canvas dim + chip-row highlight\n // but the chip itself stayed at bg-green-500/10 — cause\n // silent, effect loud. R201 deepens its OWN green hue\n // (10→15 bg, 20→30 border) only when clickable.\n // transition-colors duration-200 blends the swap to\n // match R193's timing on the active-links chip.\n /* Round 232 / Loop: HTML chip row picks up the\n tabular-nums info-density treatment R224-R230\n established on the SVG side. The chip text reads\n \"{N} working\"; when workingCount crosses 9→10\n the leading digit's width shift propagates the\n trailing ' working' label right by the digit-vs-\n control glyph delta, and the chip's right edge\n re-flows because the parent is inline flex. The\n Tailwind `tabular-nums` utility sets font-variant-\n numeric: tabular-nums, locking the digit width\n so the chip text + chip width stay stable across\n all counter values. Sibling chips (online,\n active-links) get the same treatment in this\n round so the three-chip row reads uniformly.\n 7th surface in the info-density tabular-nums\n sweep — and the first on the HTML side\n (previous 6 were SVG <text>/<tspan>). */\n /* Round 398 / Loop: chip-row chips gain hover translateY\n (-1px) lift on the CLICKABLE variant only (workingCount\n > 0 here / onlineNodes.length > 0 below / activeLinks\n > 0 deeper). Pre-R398 the chips brightened bg + border\n on hover (R201) but didn't lift — only their clickable\n siblings (filter pin pills R397, recent rows R143,\n legend rows R144) acknowledged cursor entry with a\n translate-y. R398 closes the chip-row by extending\n the same gesture to the static header chips, gated\n on the clickable role so empty chips (which have\n no role=\"button\") stay planted at their R205\n opacity-50 receded paint. transition-transform\n + duration-200 + ease-out + transform-gpu added\n alongside existing transition-colors so the lift\n and the color tween share rhythm.\n Gesture-vocabulary table (post-R398):\n recent-signal row -1 px (R143)\n legend row -1 px (R144)\n group cluster box fill+sw lift (R142)\n filter pin pills -1 px (R397)\n chip-row chips -1 px (R398, this round)\n Empty chips: no lift. Pin-mirror chips: no\n conflict (R180 inset double-ring is a box-shadow\n not a transform). new data-chip-hover-lift attr\n surfaces the lift surface for tests. */\n // R414: chip-row chips gain `group` so inner unit\n // span brightens via group-hover:opacity-100 — sibling\n // to R355 filter pin pill inner-span hover-brighten.\n // Hover-brighten family extends from filter pills to\n // chip-row chips at the inner-span scope.\n 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 ${\n workingCount > 0\n ? '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'\n : 'bg-green-500/10 text-green-300 border-green-500/20'\n }`}\n data-chip-hover-lift={workingCount > 0 ? 'true' : 'false'}\n data-chip-group-hover-brighten=\"true\"\n data-working-chip\n data-working-chip-aliases={workingAliases.join(',')}\n data-pin-mirror={pinnedStatus === 'working' ? 'true' : 'false'}\n data-working-chip-clickable={workingCount > 0 ? 'true' : 'false'}\n data-working-chip-empty={workingCount === 0 ? 'true' : 'false'}\n title={workingTitle}\n role={workingCount > 0 ? 'button' : undefined}\n tabIndex={workingCount > 0 ? 0 : undefined}\n aria-pressed={workingCount > 0 ? (pinnedStatus === 'working') : undefined}\n // Round 180 / Loop: pin-mirror inset rings now ease in/out\n // instead of snapping. R165 added this transition to the\n // pressure-bar segments; R180 closes the smooth-pin-mirror\n // family across the three remaining chip-row pin chips\n // (working / online / vendor letter). The visual is small\n // — a 1-2 px inset double ring — but the eye catches the\n // pop on every pin/unpin without the ease.\n // Round 201 / Loop: inline transition list now also covers\n // background-color + border-color so the R201 hover tint\n // eases. Tailwind transition-colors on the className would\n // be overridden by this inline declaration, so we splice\n // the colour properties directly into the existing\n // R180 box-shadow transition.\n // Round 205 / Loop: chip recedes to opacity 0.5 when its\n // tier is empty (workingCount=0). Pre-R205 \"0 working\"\n // displayed at full bg-green-500/10 chrome — visually\n // indistinguishable from \"12 working\". Eye got zero\n // empty-tier signal. R205 mirrors R204's legend count\n // recede-on-empty pattern at the chip-row scope. Inline\n // transition list extends `opacity 200ms ease-out` so\n // the crossing-zero ease matches R201's bg/border\n // timing. Empty-state combines with R139's clickable=\n // false + tooltip-undefined: visual + interactive +\n // affordance all say \"this tier has nothing to act on\".\n style={{\n cursor: workingCount > 0 ? 'pointer' : undefined,\n opacity: workingCount === 0 ? 0.5 : 1,\n boxShadow: pinnedStatus === 'working' ? 'inset 0 0 0 1px #4ade80, inset 0 0 0 2px rgba(255,255,255,0.45)' : undefined,\n transition: 'box-shadow 150ms ease-out, background-color 200ms ease-out, border-color 200ms ease-out, opacity 200ms ease-out',\n }}\n onMouseEnter={() => { if (workingCount > 0) setHoveredStatus('working'); }}\n onMouseLeave={() => setHoveredStatus(prev => prev === 'working' ? null : prev)}\n // R139: the title hover-text has been promising \"click to\n // pin\" since R79 but no onClick was ever wired. The cursor:\n // pointer at line 1363 set up the same lie R136 fixed on\n // the active-links chip. Wire it now: click toggles the\n // status pin to 'working', composing with R60 (pressure-\n // bar segments) and R61 (legend rows) — three different\n // surfaces that all toggle the same pinnedStatus. boxShadow\n // pin-mirror at line 1364 already reflects the state; aria-\n // pressed now exposes it for screen readers too.\n onClick={() => {\n if (workingCount > 0) setPinnedStatus(prev => prev === 'working' ? null : 'working');\n }}\n onKeyDown={(e) => {\n if (workingCount === 0) return;\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n setPinnedStatus(prev => prev === 'working' ? null : 'working');\n }\n }}\n >\n {/* Round 337 / Loop: split working chip into digit +\n \" working\" unit, with the unit at opacity-70.\n Extends the R333/R335/R336 chip-internal-hierarchy\n arc from SVG (panel headers) and pin-chip prefix\n surfaces into HTML chip-row chips. Recurring\n pattern: small label spans demote, value stays\n prominent. data-working-chip-unit exposes the\n span for tests. */}\n {/* Round 362 / Loop: digit picks up font-semibold\n (fw 500 → 600) for within-chip weight tier. The\n chip's outer className stays at font-medium (R313\n data-weight baseline); the digit overrides to\n semibold so it reads heavier than its \" working\"\n unit (which keeps fw 500 + R338 opacity-70).\n Joins the R333-R341 chip-internal-hierarchy arc\n at the chip-count scope. Sibling edits on the\n online + active-links chip digits below. data-\n working-chip-digit attr exposes the digit span. */}\n <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>\n </span>\n <span\n // Round 201 / Loop: online chip — mirror of the working\n // chip treatment above. cyan hue 10→15 bg + 20→30 border\n // on hover, only when there's at least one online node\n // to highlight. Three sibling chips in the chip row now\n // all speak the same gesture vocabulary:\n // working chip · green 10→15 (R201)\n // online chip · cyan 10→15 (R201)\n // active-links · gray → cyan (R193)\n /* Round 232 / Loop: tabular-nums on online chip\n (sibling treatment to working chip — same row,\n same digit-jitter physics on count crossings). */\n // R398: hover translate-y lift on clickable variant — see working chip above.\n // R414: `group` parent + inner unit span group-hover-brighten — see working chip above.\n 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 ${\n onlineNodes.length > 0\n ? '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'\n : 'bg-cyan-500/10 text-cyan-300 border-cyan-500/20'\n }`}\n data-chip-hover-lift={onlineNodes.length > 0 ? 'true' : 'false'}\n data-chip-group-hover-brighten=\"true\"\n data-online-chip\n data-online-chip-aliases={onlineAliases.join(',')}\n data-pin-mirror={pinnedStatus === 'idle' ? 'true' : 'false'}\n data-online-chip-clickable={onlineNodes.length > 0 ? 'true' : 'false'}\n data-online-chip-empty={onlineNodes.length === 0 ? 'true' : 'false'}\n title={onlineTitle}\n role={onlineNodes.length > 0 ? 'link' : undefined}\n tabIndex={onlineNodes.length > 0 ? 0 : undefined}\n // R180: smooth-pin-mirror family — see working chip above.\n // R201: inline transition list also covers bg + border so\n // the new R201 hover tint eases (mirror of working chip).\n // R205: empty-tier recede — opacity 0.5 when onlineNodes\n // is empty (mirror of working chip above).\n style={{\n cursor: onlineNodes.length > 0 ? 'pointer' : undefined,\n opacity: onlineNodes.length === 0 ? 0.5 : 1,\n boxShadow: pinnedStatus === 'idle' ? 'inset 0 0 0 1px #67e8f9, inset 0 0 0 2px rgba(255,255,255,0.45)' : undefined,\n transition: 'box-shadow 150ms ease-out, background-color 200ms ease-out, border-color 200ms ease-out, opacity 200ms ease-out',\n }}\n onMouseEnter={() => {\n // If a working filter would isolate nothing, route to idle.\n const idleCount = onlineNodes.length - workingCount;\n if (workingCount > 0) setHoveredStatus('working');\n else if (idleCount > 0) setHoveredStatus('idle');\n }}\n onMouseLeave={() => setHoveredStatus(prev => prev === 'working' || prev === 'idle' ? null : prev)}\n // R140: click → /nodes. Mirrors R136 active-links→/messages\n // idiom. The chip's hover semantics (R79 highlight all\n // online) keep their meaning — hover for canvas preview,\n // click for the full list. Pinning idle here would\n // semantically misrepresent the chip (which means\n // working+idle), so we navigate instead of pin.\n onClick={() => {\n if (onlineNodes.length > 0) router.push('/nodes');\n }}\n onKeyDown={(e) => {\n if (onlineNodes.length === 0) return;\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n router.push('/nodes');\n }\n }}\n >\n {/* R337 sibling — online chip unit demotion. */}\n {/* R362 sibling — online-chip digit gains font-semibold. */}\n <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>\n </span>\n </>\n );\n })()}\n {/* Round 31 / Loop: fleet-health pressure bar. The \"X working /\n Y online\" chips already carry the raw counts; the bar lets\n the eye get the working/idle/offline RATIO in one glance\n without mental math. Stacked 3-segment chip, ~64 px wide. */}\n {(() => {\n const w = workingCount;\n const i = onlineNodes.length - workingCount; // idle = online - working\n const o = offlineNodes.length;\n const total = w + i + o;\n if (total === 0) return null;\n // Round 60 / Loop: each segment toggles a sticky filter via\n // `pinnedStatus`. Click the working segment → all non-working\n // nodes dim; click again → release. Segments share width with\n // their proportion of `total`, so a 1-node working share is\n // ~3 px wide on a 64-px bar. We pad the click target with a\n // negative-margin overlay wrapper to give thin slices a\n // 14-px minimum hit area without disturbing the rendered\n // chip width. cursor:pointer + a one-line title hint at the\n // affordance.\n const seg = (n: number, color: string, key: 'working' | 'idle' | 'offline', label: string) => {\n if (n === 0) return null;\n const isPinned = pinnedStatus === key;\n // R102: list the aliases that match this segment's bucket\n // so the title answers WHICH n, not just HOW MANY. Closes\n // the last \"info-density gap\" in the chip-row surfaces\n // (R97 pills / R99 group labels / R101 vendor letters all\n // already enumerate). Truncates at 8 with \"+N more\".\n const matchAliases = key === 'working'\n ? onlineNodes.filter(s => s.status === 'working').map(s => s.alias)\n : key === 'idle'\n ? onlineNodes.filter(s => s.status !== 'working').map(s => s.alias)\n : offlineNodes.map(s => s.alias);\n const previewList = matchAliases.slice(0, 8).join(', ');\n const suffix = matchAliases.length > 8 ? ` + ${matchAliases.length - 8} more` : '';\n const titleAction = isPinned ? 'click to release filter' : 'click to highlight';\n return (\n <span\n key={key}\n data-pressure-seg={key}\n data-pressure-seg-aliases={matchAliases.join(',')}\n data-pressure-seg-hovered={hoveredStatus === key ? 'true' : 'false'}\n role=\"button\"\n tabIndex={0}\n aria-pressed={isPinned}\n className=\"anet-topo-chip-focus\"\n title={`${n} ${label}\\n${previewList}${suffix}\\n${titleAction}`}\n // Round 165 / Loop: smooth width transitions on the\n // pressure-bar segments. Pre-R165 the widths snapped\n // instantly when fleet composition shifted (a node\n // going idle → working would visibly jump the green\n // segment by a few px). 220ms ease-out makes the bar\n // visually breathe with state — segment shifts now\n // glide into place instead of cutting. Pure CSS\n // transition on width; respects prefers-reduced-\n // motion via globals.css blanket override that\n // neutralises transition-duration universally.\n // boxShadow gets its own transition so pin\n // state-changes also fade smoothly, not snap.\n //\n // Round 210 / Loop: segment deepens its OWN colour on\n // hover via filter: brightness(1.2). Closes the\n // chip-row \"hover deepens own identity\" family at the\n // pressure-bar scope — R83 already setHoveredStatus\n // to drive canvas dim, but the segment itself stayed\n // at flat `background: color`. R210 makes the cause\n // element (segment) light up with the same gesture\n // it fires on the canvas (effect element). Family\n // surfaces (6 with R210):\n // R193 active-links chip · gray → cyan\n // R195 recent-panel footer · gray → cyan\n // R201 working / online · own /10 → /15 (×2)\n // R202 vendor letter · per-vendor color-mix\n // R210 pressure-bar seg · brightness(1.2) ← NEW\n // brightness(1.2) lightens the segment's own hue by\n // 20% — keeps the tier identity (green stays green,\n // teal stays teal, gray stays gray) while signalling\n // \"this is hovered\". transition adds filter 150ms\n // ease-out alongside the existing width / box-shadow.\n style={{\n width: `${(n / total) * 100}%`,\n background: color,\n height: '100%',\n cursor: 'pointer',\n boxShadow: isPinned ? `inset 0 0 0 1px ${color}, inset 0 0 0 2px rgba(255,255,255,0.6)` : undefined,\n filter: hoveredStatus === key ? 'brightness(1.2)' : undefined,\n transition: 'width 220ms ease-out, box-shadow 150ms ease-out, filter 150ms ease-out',\n }}\n onClick={(e) => {\n e.stopPropagation();\n setPinnedStatus(prev => prev === key ? null : key);\n }}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n setPinnedStatus(prev => prev === key ? null : key);\n }\n }}\n // R83: hover preview — segments now match the R55 legend,\n // R79 working/online chips, and R80 vendor letters in\n // offering a hover-transient highlight before the click\n // commits to a pin. Users get to feel what the filter\n // does before they lock it in. Same activeStatus =\n // hoveredStatus ?? pinnedStatus formula; releasing the\n // pointer falls back to the pin if one is set, or to\n // baseline. Thin (3-px) segments still hit-test fine\n // because the chip itself is a flex row — span doesn't\n // need any extra hit padding for hover.\n onMouseEnter={() => setHoveredStatus(key)}\n onMouseLeave={() => setHoveredStatus(prev => prev === key ? null : prev)}\n />\n );\n };\n // Round 47 / Loop: hidden on mobile — at <640px the chip row\n // wraps to multiple lines and crowds the topology header;\n // pressure ratio is best read with the working+online raw\n // counts (kept visible) anyway.\n return (\n <span\n className=\"hidden sm:inline-flex items-center gap-2 px-2.5 py-1 rounded-md bg-gray-500/10 text-gray-400 border border-gray-500/20 font-mono\"\n title={`${w} working · ${i} idle · ${o} offline`}\n data-fleet-pressure\n >\n {/* Round 373 / Loop: pressure-bar kicker label gains\n font-medium (fw 400 → 500). Sibling small-text fw\n lift family with R363 recent-row alias + R364\n legend-row label + R366 group-label count + R368\n +N more flows footer — extends to a 5th surface\n (the chip-row's 'pressure' label). At fontSize\n 10 px tracking-wide against the chip's gray bg,\n the default fw 400 sat below the deliberate-data\n band; fw 500 brings it into parity with the\n chip-row 'working / online / active links' unit\n spans (chip-level font-medium R313). data-fleet-\n pressure-kicker attr exposes the kicker for tests. */}\n <span className=\"text-[10px] tracking-wide font-medium\" data-fleet-pressure-kicker>pressure</span>\n {/* Round 374 / Loop: pressure-bar height h-1.5 → h-2\n (6 → 8 px) — sibling visual-weight bump (8th anchor\n in the family):\n R287 minimap viewport stroke 1 → 1.5\n R295 legend swatch base radius 5.5 → 6\n R359 recent-row pip base radius 1.6 → 1.8\n R360 hub digit fontSize 11 → 12\n R361 edge-badge digit fontSize 10 → 11\n R365 hub-highlight base radius 5 → 5.5\n R367 edge-badge rest stroke 1 → 1.25\n R374 pressure-bar height 1.5 → 2 (this round)\n +33 % bar height gives the working/idle/offline\n segments more visibility — at h-1.5 the 3-segment\n proportion bar was readable but slim; at h-2 the\n segments parse cleanly even when one tier is\n < 10 % share. Geometry-safe: items-center flex\n centers the bar inside the chip's py-1 (4 px top +\n 4 px bottom) — bar at 8 px stays comfortably\n inside the 10-px text-row height. R165 segment\n width transitions + R210 brightness hover + R83\n pin-mirror box-shadow on segments all preserved\n (segments inherit width from parent so the height\n bump propagates without segment-side edits).\n data-fleet-pressure-bar-height attr exposes the\n height token for tests. */}\n <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\">\n {seg(w, isLight ? '#059669' : '#22c55e', 'working', 'working')}\n {seg(i, isLight ? '#0d9488' : '#2dd4bf', 'idle', 'idle')}\n {seg(o, isLight ? '#94a3b8' : '#6b7280', 'offline', 'offline')}\n </span>\n </span>\n );\n })()}\n {/* Round 64 / Loop: active-filter pills. When pinnedStatus or\n pinnedGroup is set, show a small \"filter: <key> ×\" pill so\n the user can see the pin from the chip row even if they\n scrolled the canvas off-screen. × button clears the\n specific pin (Esc still clears all — both paths are\n valid). pinnedStatus and pinnedGroup pin independently so\n both pills may render simultaneously. */}\n {/* R67: pills enter via anet-fade-in so they appear softly\n instead of popping. The \"filter: \" prefix collapses below\n sm — at narrow viewports the chip row is precious real\n estate (R47 already hides pressure / vendor / freshness),\n so dropping the redundant label keeps the working/idle/\n alpha keys readable without overflow. */}\n {/* R71: each pill picks up a \"· N\" match count tail. Tells the\n user at a glance how many sessions the active filter\n matches without scanning the canvas. Counts come from the\n already-computed workingCount + onlineNodes + offlineNodes\n for status, and groupKeys for group. */}\n {/* R73: entire pill body is a click-to-clear target — matches\n the Notion / Linear tag UX (the whole chip releases). The\n × keeps its dedicated <button> + aria-label for screen\n readers; the outer span just adds an extra mouse-friendly\n hit area with a title hint. ×'s onClick stopPropagation\n so the redundant outer onClick doesn't double-fire (no\n functional difference since both clear, but cleaner\n event flow). */}\n {pinnedStatus && (() => {\n const matchCount = pinnedStatus === 'working' ? workingCount\n : pinnedStatus === 'idle' ? (onlineNodes.length - workingCount)\n : offlineNodes.length;\n // Round 97 / Loop: the pill says HOW MANY but not WHICH.\n // Hovering it now shows the matched alias list in the\n // tooltip — answers \"exactly who is this filtering to\"\n // without forcing the user to scan the dim mask on the\n // canvas. Truncates at 8 names so a 50-node working\n // bucket doesn't produce a 12-line tooltip; the count\n // chip already answers \"how many overall\".\n const matchAliases = pinnedStatus === 'working'\n ? onlineNodes.filter(s => s.status === 'working').map(s => s.alias)\n : pinnedStatus === 'idle'\n ? onlineNodes.filter(s => s.status !== 'working').map(s => s.alias)\n : offlineNodes.map(s => s.alias);\n const matchPreview = matchAliases.slice(0, 8).join(', ');\n const matchSuffix = matchAliases.length > 8 ? ` + ${matchAliases.length - 8} more` : '';\n return (\n <span\n data-active-filter=\"status\"\n data-filter-match-count={matchCount}\n data-filter-match-aliases={matchAliases.join(',')}\n // R355: `group` lets the inner opacity-70 spans (prefix\n // `filter:` + count `· N`) brighten to 100 % on pill hover.\n // Sibling treatment on group + vendor pills below.\n 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\"\n title={matchCount > 0 ? `${matchPreview}${matchSuffix} — click to clear` : 'Click to clear filter'}\n onClick={() => setPinnedStatus(null)}\n style={{\n background: pinnedStatus === 'working' ? (isLight ? '#05966914' : '#22c55e1f')\n : pinnedStatus === 'idle' ? (isLight ? '#0d948814' : '#2dd4bf1f')\n : (isLight ? '#94a3b814' : '#6b72801f'),\n color: pinnedStatus === 'working' ? (isLight ? '#047857' : '#86efac')\n : pinnedStatus === 'idle' ? (isLight ? '#0f766e' : '#5eead4')\n : (isLight ? '#475569' : '#9ca3af'),\n borderColor: 'currentColor',\n cursor: 'pointer',\n }}\n >\n {/* Round 412 / Loop: filter pin pill VALUE picks up the\n chip-internal-hierarchy arc. Pre-R412 the value span\n (pinnedStatus / pinnedGroup / pinnedVendor) inherited\n the parent's font-medium (fw=500); prefix and suffix\n were opacity-70 label-tier but the VALUE itself sat\n at the same baseline weight. R412 wraps the value in\n a font-semibold span (fw=600) so the pill now reads\n with proper data-tier emphasis — sibling treatment\n to R333/R335-R341/R362/R369/R389/R410. data-filter-\n value attr surfaces the value span for tests.\n 4-pill replace family — status / group / vendor / edge. */}\n <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>\n <button\n type=\"button\"\n aria-label={`Clear ${pinnedStatus} filter`}\n onClick={(e) => { e.stopPropagation(); setPinnedStatus(null); }}\n /* Round 356 / Loop: filter pin pill × buttons gain\n hover:scale-110 (Tailwind 4 modern CSS `scale` property,\n not legacy transform). Sibling polish to R354 vendor\n letter glyph + R350/R352/R353 chrome icon hover-scales.\n Pre-R356 the × had only hover:opacity-70 — the target\n dimmed under cursor but didn't lift. R356 adds a 10 %\n scale on hover so the click-target reads as \"press me\"\n alongside the dim. transform-gpu hint promotes the\n button to its own compositor layer for crisper edges\n during the scale tween. transition-transform duration-\n 200 matches the chrome icon hover-scale timing family.\n inline-block is default for <button> so no display\n tweak needed. replace_all covers all 4 filter pin\n pills (status / group / vendor / edge) at once. */\n className=\"ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 transform-gpu\"\n style={{ background: 'transparent', color: 'inherit', cursor: 'pointer', padding: 0 }}\n >×</button>\n </span>\n );\n })()}\n {pinnedGroup && (() => {\n const matchCount = Object.values(groupKeys).filter(k => k === pinnedGroup).length;\n // R97: list group members in the tooltip.\n const matchAliases = Object.entries(groupKeys)\n .filter(([, key]) => key === pinnedGroup)\n .map(([alias]) => alias);\n const matchPreview = matchAliases.slice(0, 8).join(', ');\n const matchSuffix = matchAliases.length > 8 ? ` + ${matchAliases.length - 8} more` : '';\n return (\n <span\n data-active-filter=\"group\"\n data-filter-match-count={matchCount}\n data-filter-match-aliases={matchAliases.join(',')}\n // R355 sibling — `group` parent + group-hover on inner spans.\n 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\"\n title={matchCount > 0 ? `${matchPreview}${matchSuffix} — click to clear` : 'Click to clear filter'}\n onClick={() => setPinnedGroup(null)}\n style={{\n background: isLight ? '#67e8f914' : '#67e8f91f',\n color: pal.legendAccent,\n borderColor: 'currentColor',\n cursor: 'pointer',\n }}\n >\n {/* R412: see status pill above — filter value fw=600 data tier. */}\n <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>\n <button\n type=\"button\"\n aria-label={`Clear group filter ${pinnedGroup}`}\n onClick={(e) => { e.stopPropagation(); setPinnedGroup(null); }}\n /* Round 356 / Loop: filter pin pill × buttons gain\n hover:scale-110 (Tailwind 4 modern CSS `scale` property,\n not legacy transform). Sibling polish to R354 vendor\n letter glyph + R350/R352/R353 chrome icon hover-scales.\n Pre-R356 the × had only hover:opacity-70 — the target\n dimmed under cursor but didn't lift. R356 adds a 10 %\n scale on hover so the click-target reads as \"press me\"\n alongside the dim. transform-gpu hint promotes the\n button to its own compositor layer for crisper edges\n during the scale tween. transition-transform duration-\n 200 matches the chrome icon hover-scale timing family.\n inline-block is default for <button> so no display\n tweak needed. replace_all covers all 4 filter pin\n pills (status / group / vendor / edge) at once. */\n className=\"ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 transform-gpu\"\n style={{ background: 'transparent', color: 'inherit', cursor: 'pointer', padding: 0 }}\n >×</button>\n </span>\n );\n })()}\n {/* R89: vendor pin gets its own filter pill, matching the R64\n status + group pattern. R88 added the pin but only the\n letter itself carried the state; the chip-row had no\n \"filter: A · 2 ×\" affordance the other two pins have. The\n pill colour borrows the vendor's own swatch so each pin\n still reads in its native hue (A green, O cyan, 书 blue,\n ? slate). Same body-click-clears + × button pattern as\n R64. */}\n {pinnedVendor && (() => {\n const matchEntry = vendorDist.find(v => v.initial === pinnedVendor);\n const matchCount = matchEntry?.count ?? 0;\n const vendorColor = matchEntry?.color ?? pal.legendText;\n // R97: list vendor users in the tooltip. The vendorIdentity\n // resolver maps model strings to a vendor initial — match\n // any session whose initial equals the pinned letter (with\n // unknowns folded to '?').\n const matchAliases = [...onlineNodes, ...offlineNodes]\n .filter(s => {\n const v = vendorForModel(s.model);\n return (v.id === 'unknown' ? '?' : v.initial) === pinnedVendor;\n })\n .map(s => s.alias);\n const matchPreview = matchAliases.slice(0, 8).join(', ');\n const matchSuffix = matchAliases.length > 8 ? ` + ${matchAliases.length - 8} more` : '';\n return (\n <span\n data-active-filter=\"vendor\"\n data-filter-match-count={matchCount}\n data-filter-match-aliases={matchAliases.join(',')}\n // R355 sibling — `group` parent + group-hover on inner spans.\n 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\"\n title={matchCount > 0 ? `${matchPreview}${matchSuffix} — click to clear` : 'Click to clear vendor filter'}\n onClick={() => setPinnedVendor(null)}\n style={{\n background: `${vendorColor}1f`,\n color: vendorColor,\n borderColor: 'currentColor',\n cursor: 'pointer',\n }}\n >\n {/* R412: see status pill above — filter value fw=600 data tier. */}\n <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>\n <button\n type=\"button\"\n aria-label={`Clear vendor filter ${pinnedVendor}`}\n onClick={(e) => { e.stopPropagation(); setPinnedVendor(null); }}\n /* Round 356 / Loop: filter pin pill × buttons gain\n hover:scale-110 (Tailwind 4 modern CSS `scale` property,\n not legacy transform). Sibling polish to R354 vendor\n letter glyph + R350/R352/R353 chrome icon hover-scales.\n Pre-R356 the × had only hover:opacity-70 — the target\n dimmed under cursor but didn't lift. R356 adds a 10 %\n scale on hover so the click-target reads as \"press me\"\n alongside the dim. transform-gpu hint promotes the\n button to its own compositor layer for crisper edges\n during the scale tween. transition-transform duration-\n 200 matches the chrome icon hover-scale timing family.\n inline-block is default for <button> so no display\n tweak needed. replace_all covers all 4 filter pin\n pills (status / group / vendor / edge) at once. */\n className=\"ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 transform-gpu\"\n style={{ background: 'transparent', color: 'inherit', cursor: 'pointer', padding: 0 }}\n >×</button>\n </span>\n );\n })()}\n {/* R119: edge pin filter pill — completes the R64 / R89 pill\n pattern across all four pin dimensions. R116 added\n pinnedEdgeKey but the chip row never grew the matching\n pill, leaving the locked flow visible only on the canvas +\n recent-signal row tint. This pill shows \"filter:\n alpha→beta · 3\" so the locked flow appears in the same\n row as the other three pin types. Body-click + × button\n clear, same pattern as R64. */}\n {pinnedEdgeKey && (() => {\n const link = flowLinks.find(l => l.key === pinnedEdgeKey);\n if (!link) return null;\n // R150 / Loop: extend the hot-lane amber convention from the\n // canvas badge (R126) / recent-row count (R127) / panel\n // header (R129) to the R119 edge filter pill — when the\n // pinned edge has count ≥ 10 the count tspan flips to\n // amber + 700 weight, and the tooltip grows a \"(hot lane\n // · ≥ 10)\" marker. Closes the 4th hot-lane surface,\n // completing R150's milestone: every place that surfaces\n // an edge count now uses the same amber-when-hot vocab.\n const isHot = link.count >= 10;\n const hotStroke = isLight ? '#d97706' : '#fbbf24';\n return (\n <span\n data-active-filter=\"edge\"\n data-filter-match-count={link.count}\n data-filter-match-aliases={`${link.from},${link.to}`}\n data-active-filter-edge-hot={isHot ? 'true' : 'false'}\n 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\"\n title={`${link.from} → ${link.to} (${link.count} msg${link.count === 1 ? '' : 's'}${isHot ? ', hot lane · ≥ 10' : ''}) — click to clear`}\n onClick={() => setPinnedEdgeKey(null)}\n style={{\n background: isLight ? `${pal.flowEdge}14` : `${pal.flowEdge}1f`,\n color: pal.flowEdge,\n borderColor: 'currentColor',\n cursor: 'pointer',\n }}\n >\n {/* R412: filter pin pill value (edge variant) picks up fw=600.\n Sibling treatment to the status/group/vendor pills above. */}\n <span>\n <span className=\"hidden sm:inline opacity-70\" data-filter-prefix>filter: </span>\n <span className=\"font-semibold\" data-filter-value>{link.from}→{link.to}</span>\n {/* Round 323 / Loop: edge filter pill count digit picks\n up tabular-nums (Tailwind class on both cold +\n hot branches). Sibling treatment to the status /\n group / vendor pin pills (R323 replace_all upstream\n in this same round added `tabular-nums` to those\n three pills' count spans). Pre-R323 a matchCount /\n link.count crossing 9→10 widened the digit and\n shifted the trailing × button right ~3px in font-\n mono (mono digits still have natural-vs-tabular\n variance). Locks the slot so the × button stays\n planted as the count grows. 9th surface in the\n info-density tabular-nums sweep after R322 panel\n hot count. */}\n {isHot ? (\n <span\n className=\"opacity-90 tabular-nums\"\n style={{ color: hotStroke, fontWeight: 700 }}\n data-active-filter-edge-count-hot\n >\n {' · '}{link.count}\n </span>\n ) : (\n <span className=\"opacity-70 tabular-nums\" data-active-filter-edge-count>\n {' · '}{link.count}\n </span>\n )}\n </span>\n <button\n type=\"button\"\n aria-label={`Clear edge filter ${link.from} → ${link.to}`}\n onClick={(e) => { e.stopPropagation(); setPinnedEdgeKey(null); }}\n /* Round 356 / Loop: filter pin pill × buttons gain\n hover:scale-110 (Tailwind 4 modern CSS `scale` property,\n not legacy transform). Sibling polish to R354 vendor\n letter glyph + R350/R352/R353 chrome icon hover-scales.\n Pre-R356 the × had only hover:opacity-70 — the target\n dimmed under cursor but didn't lift. R356 adds a 10 %\n scale on hover so the click-target reads as \"press me\"\n alongside the dim. transform-gpu hint promotes the\n button to its own compositor layer for crisper edges\n during the scale tween. transition-transform duration-\n 200 matches the chrome icon hover-scale timing family.\n inline-block is default for <button> so no display\n tweak needed. replace_all covers all 4 filter pin\n pills (status / group / vendor / edge) at once. */\n className=\"ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 transform-gpu\"\n style={{ background: 'transparent', color: 'inherit', cursor: 'pointer', padding: 0 }}\n >×</button>\n </span>\n );\n })()}\n {/* Round 124 / Loop: pin-intersection summary chip. The four\n filter pills (R64 status, R63 group, R89 vendor, R119\n edge) each report their own dim's match count in\n isolation — that answers \"what does THIS filter catch\"\n but not \"what survives ALL pins\". Since the node-opacity\n chain AND-composes the four pin dimensions, two active\n pins routinely produce an intersection smaller than\n either individual count. With three or four pins active\n the gap widens further. This chip appears ONLY when\n ≥ 2 pin dims are active (a single pin's pill already\n tells the whole story) and shows the count of nodes\n that satisfy every active pin simultaneously. Color is\n deliberately neutral (gray) since the chip represents\n the AND of mixed-color filters — borrowing any one of\n the pill hues would mis-signal which dim dominates.\n Tooltip lists the surviving aliases with the same\n 8-truncate + \"+N more\" pattern as the individual pill\n tooltips (R97). No click handler — it's a pure readout;\n Esc / per-pill × are still the cancel paths. */}\n {(() => {\n const pinDimCount =\n (pinnedStatus ? 1 : 0) +\n (pinnedGroup ? 1 : 0) +\n (pinnedVendor ? 1 : 0) +\n (pinnedEdgeKey ? 1 : 0);\n if (pinDimCount < 2) return null;\n const edgeLink = pinnedEdgeKey\n ? flowLinks.find(l => l.key === pinnedEdgeKey)\n : null;\n const edgeEndpoints: Set<string> | null = edgeLink\n ? new Set([edgeLink.from, edgeLink.to])\n : null;\n const allSessions = [...onlineNodes, ...offlineNodes];\n const survivors = allSessions.filter(s => {\n const isOnline = s.status !== 'offline';\n if (pinnedStatus === 'working' && s.status !== 'working') return false;\n if (pinnedStatus === 'idle' && !(isOnline && s.status !== 'working')) return false;\n if (pinnedStatus === 'offline' && isOnline) return false;\n if (pinnedGroup) {\n const gk = groupKeys[s.alias] ?? s.alias;\n if (gk !== pinnedGroup) return false;\n }\n if (pinnedVendor) {\n const v = vendorForModel(s.model);\n const initial = v.id === 'unknown' ? '?' : v.initial;\n if (initial !== pinnedVendor) return false;\n }\n if (edgeEndpoints && !edgeEndpoints.has(s.alias)) return false;\n return true;\n });\n const matchAliases = survivors.map(s => s.alias);\n const matchPreview = matchAliases.slice(0, 8).join(', ');\n const matchSuffix = matchAliases.length > 8 ? ` + ${matchAliases.length - 8} more` : '';\n const isEmpty = matchAliases.length === 0;\n const tooltip = !isEmpty\n ? `${matchPreview}${matchSuffix} — nodes passing all ${pinDimCount} pinned filters`\n : `No nodes pass all ${pinDimCount} pinned filters — release one to widen (Esc clears all)`;\n // R125: when the intersection drops to zero, the chip flips\n // from neutral gray to a warning amber. Zero-overlap is the\n // exact case users get confused — canvas dims to 0.28\n // everywhere, no positive signal explains why. The \"· 0\"\n // tail in neutral gray reads as just another number,\n // indistinguishable from \"· 12\". Amber + a ⚠ glyph lifts\n // it to \"your filters cancel out\" at a glance. Color\n // choice: amber (#d97706 light, #fbbf24 dark) — same hue\n // family as warning chips elsewhere in the dashboard,\n // distinct from any pill color (status green/teal/slate,\n // group cyan, vendor varies, edge cyan, neutral gray for\n // non-empty intersection) so the empty state stands out.\n const emptyColor = isLight ? '#d97706' : '#fbbf24';\n return (\n <span\n data-pin-intersection\n data-pin-dim-count={pinDimCount}\n data-pin-intersection-count={matchAliases.length}\n data-pin-intersection-empty={isEmpty ? 'true' : 'false'}\n data-pin-intersection-aliases={matchAliases.join(',')}\n /* Round 235 / Loop: pin-intersection chip joins the\n info-density tabular-nums sweep. The chip has TWO\n digits visible at once — '{pinDimCount} pins ·\n {matchAliases.length}' — and the matchAliases count\n in particular rolls frequently as filters tighten /\n widen against the live fleet. font-mono already\n makes the digits uniform-ish, but tabular-nums\n further locks digit width within the mono cell so\n the gap between 'pins' and '·' stays stable, and\n the chip's overall width doesn't bump when either\n number changes. 9th surface in the sweep — the\n third and last HTML chip surface, completing\n coverage across chip-row + vendor-row + pin-\n intersection. */\n className=\"hidden sm:inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md font-mono tabular-nums text-xs border anet-fade-in anet-topo-chip-focus\"\n title={tooltip}\n /* Round 236 / Loop: smooth the empty/non-empty colour\n crossing. Pre-R236 the chip snap-flipped between\n slate-on-slate (non-empty filter intersection) and\n amber-on-amber (empty intersection — the 'your\n pinned filters cancel out' warning). bg, color,\n and borderColor (which inherits currentColor) all\n changed in one frame. R236 adds a 200ms ease-out\n transition on all three so when a filter tightens\n matches across the 0-boundary the chip eases\n through the colour shift instead of snapping. Same\n 200ms cadence the other chip-row members use\n (R201 working/online tint, R193 active-links\n tint, R202 vendor letter color-mix). One more\n surface where colour state-changes ease rather\n than snap — consistent with the topology's\n broader transitions vocabulary. */\n style={{\n background: isEmpty\n ? (isLight ? '#d97706' + '14' : '#fbbf24' + '1f')\n : (isLight ? '#94a3b814' : '#94a3b81f'),\n color: isEmpty\n ? emptyColor\n : (isLight ? '#475569' : '#9ca3af'),\n borderColor: 'currentColor',\n transition: 'background-color 200ms ease-out, color 200ms ease-out, border-color 200ms ease-out',\n }}\n >\n <span>\n <span className=\"hidden sm:inline opacity-70\" data-pin-intersection-prefix>match: </span>\n {/* Round 324 / Loop: pin-intersection chip carries TWO\n numeric counts in one breath — pinDimCount (\"how\n many filter pins are active\") and matchAliases.\n length (\"how many aliases land in the intersection\n after pins compose\"). Both jitter on digit-width\n crossings (1→10 etc) without tabular-nums even\n under font-mono. Pre-R324 a fleet busying up so\n one dimension flips from 0→non-zero (chip mounts\n via R237 always-mount opacity gate) AND the match\n count digit ticks 9→10 simultaneously visibly\n jolted the trailing `× pins` / ` × ` segments.\n Two dedicated tabular-nums spans (one per count)\n lock both digit slots so the chip's text geometry\n stays planted through both crossings. 10th\n surface in the info-density tabular-nums sweep\n after R323 filter pin pill counts (R64/R89/R119/\n R150 pin-pill family parity now complete with\n this composed-pin sibling). data-pin-intersection-\n count-* attrs expose both spans for tests. */}\n {/* Round 341 / Loop: middle \" pins\" unit word\n previously sat as a bare text node between the\n two count spans, while the matches-count span\n already carried opacity-0.7 (R335 + R324 era).\n The pinDimCount span is prominent and the\n matches count is recessive — but the literal\n \" pins\" was at FULL opacity, breaking the\n chip-internal hierarchy unified across R333/\n R335/R336/R337/R338/R340. R341 wraps \" pins\"\n in an opacity-0.7 span so the chip reads:\n pinDimCount (prominent value)\n \" pins\" (recessive unit)\n \" · {N}\" (recessive count)\n Three-tier hierarchy on a single chip; 7th\n surface in the chip-internal-hierarchy arc. */}\n <span className=\"tabular-nums\" data-pin-intersection-count-dims>{pinDimCount}</span><span className=\"opacity-70\" data-pin-intersection-unit> pins</span><span className=\"opacity-70 tabular-nums\" data-pin-intersection-count-matches> · {matchAliases.length}</span>\n {/* Round 237 / Loop: ⚠ warning glyph picks up the\n always-mount-opacity-gate idiom. Pre-R237 the\n glyph was conditionally rendered on isEmpty,\n snap-mounting when filter intersection\n narrowed to 0 AND introducing a layout shift\n (ml-1 margin appears alongside the glyph,\n widening the chip by ~16px). The R236 color\n easing made the colour crossing smooth but\n the glyph still pop-jumped, breaking the\n polish that R236 just installed at this\n same chip.\n\n Always-mount the glyph with opacity gated by\n isEmpty + the same 200ms ease-out that R236\n uses on the chip's colour transition. Now the\n whole isEmpty crossing — bg, color, border,\n AND glyph visibility — eases as one\n coordinated 200ms event. ml-1 margin is\n reserved permanently, so the chip width\n stays stable through the crossing (no\n layout-shift jank against neighbouring\n chips). data-pin-intersection-warning attr\n surfaces the visibility state for test\n introspection. 11th surface in the always-\n mount-opacity-gate family (R181 / R182 /\n R183 / R213 ×2 / R214 / R215 / R221 / R222 /\n R223 / R237). */}\n <span\n className=\"ml-1\"\n aria-hidden\n data-pin-intersection-warning={isEmpty ? 'true' : 'false'}\n style={{ opacity: isEmpty ? 1 : 0, transition: 'opacity 200ms ease-out' }}\n >⚠</span>\n </span>\n </span>\n );\n })()}\n {/* Round 281 / Loop: vendor letters chip threshold tightens\n from >1 to >2 per 减法 cut #7. Pre-R281 the chip showed\n whenever ≥2 vendor types existed in the fleet — for a\n typical demo (claude + 1 other = 2 types), the chip\n rendered \"A:N C:M\" adding ~50-80px to the chip-row width.\n Tightening to >2 keeps the chip useful for fleets with\n ACTUAL vendor diversity (3+ types) where the\n composition matters at a glance, but hides it for the\n common 1-2 vendor case where the info is low-signal.\n Continues the R275-R280 simplification arc. */}\n {vendorDist.length > 2 && (\n <span\n className=\"hidden sm:inline-flex items-center gap-2 px-2.5 py-1 rounded-md bg-gray-500/10 text-gray-400 border border-gray-500/20 font-mono\"\n title=\"Hover to highlight; click to pin\"\n >\n {vendorDist.map(v => {\n const isPinned = pinnedVendor === v.initial;\n // R88: click toggles a sticky filter the same way R60\n // pressure-bar segments toggle pinnedStatus. Visual\n // mirror = inset boxShadow using the vendor's own\n // colour, so each pinned letter sings in its own hue\n // (Anthropic green / OpenAI cyan / 书 blue / ?).\n // R101: tooltip lists the aliases that use this vendor —\n // completes the info-density triple started by R97 pills,\n // R98 node titles, R99 group-label titles. Anywhere the\n // UI shows \"A:3\" should hover-explain which 3.\n const aliases = [...onlineNodes, ...offlineNodes]\n .filter(s => {\n const vid = vendorForModel(s.model);\n return (vid.id === 'unknown' ? '?' : vid.initial) === v.initial;\n })\n .map(s => s.alias);\n const preview = aliases.slice(0, 8).join(', ');\n const suffix = aliases.length > 8 ? ` + ${aliases.length - 8} more` : '';\n const tooltip = isPinned\n ? `${preview}${suffix} — click again or Esc to clear`\n : `${preview}${suffix} — click to pin`;\n return (\n <span\n key={v.initial}\n role=\"button\"\n tabIndex={0}\n aria-pressed={isPinned}\n /* Round 234 / Loop: vendor letter chip picks up\n tabular-nums to lock the ':N' suffix's digit\n width. Each vendor chip renders 'A:3', 'C:2',\n etc. inline at gap-0.5 — when one vendor's\n count rolls 9→10 the chip widens by the digit\n glyph delta and pushes downstream chips right,\n making the row visibly jitter. 8th surface\n in the info-density tabular-nums sweep,\n completing the HTML chip-side coverage after\n R232 working/online/active-links chips. The\n digit lives in the inner <span> at line 2194,\n but font-variant-numeric inherits, so applying\n it at the outer chip span reaches every\n descendant glyph for free. */\n /* Round 314 / Loop: vendor letter chip joins the\n R312-R313 'HTML-context data chip = font-medium'\n family. R313 weighted the 3 main chips\n (working/online/active-links); R314 closes the\n chip-row weight sweep by extending to the\n vendor letter chips ('A:N', 'O:N', '书:N',\n '?:N'). They display vendor-distribution\n data; same tier as the sibling data chips. */\n /* Round 401 / Loop: vendor letter chip closes the\n hover-lift gesture family at its last unaddressed\n interactive HTML surface. R397/R398/R399 lifted\n filter pin pills + chip-row chips (working /\n online / active-links); R400 lifted standalone\n chrome buttons (reset / fullscreen). The vendor\n letter chips (A:N / O:N / 书:N / ?:N) are\n sibling interactive chips in the same chip-row\n — clickable to toggle the vendor filter pin —\n but were not yet on the hover-lift family.\n R401 closes the gap with hover:-translate-y-px\n + transition-transform + transform-gpu added\n to the className. The inline transition list\n (box-shadow + background-color) keeps eaching\n independently — different property axes compose\n cleanly. Existing R354 glyph scale-1.1 (inner\n span) + R202 chip bg color-mix + R180 pin-mirror\n box-shadow + R354 glyph hover transform all\n preserved. data-vendor-letter-hover-lift attr\n surfaces the lift for tests. */\n // R417: `group` parent enables the count suffix to\n // brighten on chip hover via group-hover:opacity-100\n // — sibling to R355 filter-pill prefix/suffix + R414\n // chip-row unit brighten. Closes the inner-span\n // hover-brighten family at the vendor chip surface.\n 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\"\n data-vendor-letter={v.initial}\n data-vendor-letter-count={v.count}\n data-vendor-letter-hover-lift=\"true\"\n data-vendor-pinned={isPinned ? 'true' : 'false'}\n data-vendor-hovered={hoveredVendor === v.initial ? 'true' : 'false'}\n data-vendor-aliases={aliases.join(',')}\n title={tooltip}\n // R180: smooth-pin-mirror family — see working chip above.\n // Round 202 / Loop: vendor letter chip joins the \"hover\n // deepens own identity hue\" family R193/R195/R201 built\n // up across the rest of the chip row. Pre-R202 hovering\n // a vendor letter (A/C/G/K/书/?) fired R88 canvas dim\n // via setHoveredVendor, but the chip itself stayed at\n // bg=transparent — cause silent, effect loud. R202\n // tints the chip with its OWN vendor colour at 12%\n // alpha via color-mix() so each vendor's chip lights\n // up in its own hue (Anthropic green / OpenAI cyan /\n // 书 blue / ?). No layout shift: only background-color\n // changes, no border/padding swap. transition list\n // extends the existing R180 box-shadow 150ms with\n // background-color 200ms ease-out (same splice idiom\n // R201 used on the working/online chips). color-mix()\n // is supported Chrome ≥ 111 / Safari ≥ 16.2 / FF ≥ 113;\n // for older browsers the chip falls back to its idle\n // transparent bg (graceful degradation — the canvas-\n // dim effect still fires regardless).\n style={{\n cursor: 'pointer',\n backgroundColor: (hoveredVendor === v.initial && !isPinned)\n ? `color-mix(in srgb, ${v.color} 12%, transparent)`\n : 'transparent',\n boxShadow: isPinned\n ? `inset 0 0 0 1px ${v.color}, inset 0 0 0 2px rgba(255,255,255,0.45)`\n : undefined,\n transition: 'box-shadow 150ms ease-out, background-color 200ms ease-out',\n }}\n onMouseEnter={() => setHoveredVendor(v.initial)}\n onMouseLeave={() => setHoveredVendor(prev => prev === v.initial ? null : prev)}\n onClick={() => setPinnedVendor(prev => prev === v.initial ? null : v.initial)}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n setPinnedVendor(prev => prev === v.initial ? null : v.initial);\n }\n }}\n >\n {/* Round 354 / Loop: vendor letter glyph scales\n 1.0 → 1.1 on hover. R88 already dims OTHER\n vendors on hover via canvas-wide opacity\n masking; R202 added a chip-level bg tint\n (color-mix 12 % alpha) so the chip itself\n responds. R354 closes the trio with a glyph-\n level lift: the focused vendor LETTER actively\n rises (transform scale) rather than the chip\n merely changing colour. Three layers of positive\n feedback on the hovered vendor + canvas-wide\n negative feedback on the others — a clean\n figure/ground separation.\n\n display: inline-block is required for transform\n to apply (inline elements ignore transform).\n transformOrigin: 'center' so the glyph pivots\n around its centre instead of arcing from the\n baseline anchor. transition rides the existing\n Tailwind 4 transform/scale list (no new\n property — Tailwind already lists transform in\n the default transition-property set). 200ms\n matches the R202 chip bg-tint timing so the\n glyph lift and chip background ease in concert. */}\n {/* Round 369 / Loop: vendor letter glyph picks up\n fontWeight 600 (font-semibold). The glyph is the\n vendor identifier — the DATA the operator scans\n in this chip (A / O / 书 / C / G / ?). R333 set\n the count suffix `:N` to text-gray-400 + tabular-\n nums and (via parent inheritance) fw 500. Pre-\n R369 the LETTER also inherited fw 500 from the\n chip's font-medium — letter and count read at\n the same weight, contradicting the data-vs-label\n hierarchy the rest of the chip-row already speaks.\n R369 lifts the letter to fw 600 so the chip now\n reads as the same two-tier pattern R362 closed\n on the working / online / active-links chips:\n chip digit/letter fw 600 (data)\n chip unit/count fw 500 (label)\n Sibling treatment to R362 — extends the R333-R341\n chip-internal-hierarchy arc to the vendor-letter\n chip surface (9th surface family). R354 transform-\n scale-on-hover + R88 canvas-dim-others + R202\n chip bg color-mix all preserved on the same span.\n data-vendor-letter-glyph-font-weight attr exposes\n the value for tests. */}\n <span\n data-vendor-letter-glyph={v.initial}\n data-vendor-letter-glyph-hover={hoveredVendor === v.initial ? 'true' : 'false'}\n data-vendor-letter-glyph-font-weight=\"600\"\n style={{\n color: v.color,\n display: 'inline-block',\n fontWeight: 600,\n transform: hoveredVendor === v.initial ? 'scale(1.1)' : 'scale(1)',\n transformOrigin: 'center',\n transition: 'transform 200ms ease-out',\n }}\n >{v.initial}</span>\n {/* Round 333 / Loop: vendor count suffix `:{N}` joins\n the R317 subordinate-text-lift family (gray-500 →\n gray-400) plus picks up tabular-nums for digit\n width-lock. Pre-R333 a vendor whose count\n crossed 9→10 widened the suffix and (since the\n parent chip has `px-2.5` padding but no fixed\n width) shifted the chip-row's downstream chips\n a couple px right. Tabular-nums locks the slot;\n gray-400 lifts the digit into the band where eye\n reads it as \"deliberate subordinate metadata\"\n rather than near-invisible chrome. data-vendor-\n letter-count exposes the span for tests. */}\n {/* R417: count suffix opacity-70 + group-hover:\n opacity-100 brightens on chip hover. Inner-span\n hover-brighten family (3rd anchor) — sibling to\n R355 filter pill prefix/suffix and R414 chip-row\n unit. Effective shade at rest: text-gray-400 ×\n 70 % alpha; on hover: full gray-400. The label-\n tier-vs-glyph differentiation persists on hover\n since the glyph (R369 fw=600) stays at full\n opacity. R333 :{count} format preserved. */}\n <span\n className=\"text-gray-400 tabular-nums opacity-70 transition-opacity duration-200 group-hover:opacity-100\"\n data-vendor-letter-count-suffix\n >:{v.count}</span>\n </span>\n );\n })}\n </span>\n )}\n {/* Round 42 / Loop: extend active-links chip with the timestamp\n of the most-recent flow event. Tells the operator at a glance\n whether the topology is currently humming (last 30s) or has\n been quiet for a while — the visual flow particles and\n edge brightness only show that there IS traffic, not when\n the last one was. Reuses Round 38's relativeAgo. */}\n {(() => {\n const recent = flowLinks.reduce<number | null>((acc, l) => {\n if (!l.last_at) return acc;\n const t = parseHubTime(l.last_at);\n if (t === null) return acc;\n return acc === null || t > acc ? t : acc;\n }, null);\n const rel = recent !== null ? relativeAgo(new Date(recent).toISOString()) : null;\n // R114: tooltip lists the actual flows. Closes the\n // info-density sweep on the last chip-row hover surface\n // (R97-R113 covered everything else). Format:\n // \"alpha→beta (3), gamma→delta (1) — hover brightens all\"\n // Truncates at 6 flows with \"+N more\" so a busy fleet\n // doesn't paint a tall tooltip; the recent-signal panel\n // already shows the top 3 in detail.\n const flowList = flowLinks\n .slice(0, 6)\n .map(l => `${l.from}→${l.to} (${l.count})`)\n .join(', ');\n const flowSuffix = flowLinks.length > 6 ? ` + ${flowLinks.length - 6} more` : '';\n // R136: the chip already had cursor:pointer when flowLinks\n // > 0 (line 1877) — but no onClick was wired. The cursor\n // lied; users got the affordance signal with no follow-\n // through. Wire it to /messages, mirroring R133's footer-\n // nav idiom. Hover (R77) keeps its semantic \"preview all\n // flows on canvas\"; click is the action \"open the full\n // list\". Two distinct gestures, both meaningful. The\n // tooltip grows a \"click to open\" tail when interactive.\n // Drop the chip out of click territory entirely when\n // flowLinks is empty — no flows = no list to open.\n const isInteractive = flowLinks.length > 0;\n const tooltip = !isInteractive\n ? undefined\n : `${flowList}${flowSuffix} — hover brightens all · click to open /messages`;\n return (\n // Round 193 / Loop: the chip itself adopts a subtle cyan\n // tint on its own hover, mirroring the cyan flowEdge\n // highlight it fires on the canvas. Pre-R193 the gesture\n // was visually asymmetric:\n // hover this chip → canvas edges brighten (cyan)\n // chip itself → stays gray (silent)\n // The *cause element* (chip) gave no response while the\n // *effect element* (canvas edges) painted loud. Same\n // pin-mirror logic R165 / R180 use on the four other\n // chip-row surfaces — the chip and the canvas edge it\n // pins should speak the same color vocabulary. Tailwind\n // :hover variant cyan-500/10 bg + cyan-500/30 border +\n // cyan-200 text matches the same palette R178/R163 use\n // for the active chrome buttons. transition-colors\n // duration-200 blends the swap smoothly. Hover variant\n // only attaches when isInteractive — a chip showing\n // \"0 active links\" has no list to open and should\n // stay gray. data-active-links-clickable already\n // exposes that gate to tests.\n <span\n // Round 206 / Loop: extend the R204/R205 empty-recede\n // family to the active-links chip. Pre-R206 \"0 active\n // links\" rendered at the same bg-gray-500/10 + full\n // opacity as \"12 active links\" — same eye-no-signal\n // problem the legend count (R204) and working/online\n // chips (R205) just solved at their own grain levels.\n // Inline opacity 0.5 when !isInteractive (flowLinks=0)\n // joins R136's already-removed cursor + R114's tooltip-\n // text gate to give the empty state visual + interactive\n // + affordance signals in lockstep.\n // Tailwind transition-colors duration-200 on className\n // would be overridden by the inline transition list, so\n // we replicate color/bg/border transitions inline\n // alongside the new opacity 200ms — same splice idiom\n // R201 used on the working/online chips.\n /* Round 232 / Loop: tabular-nums on active-links chip\n (third chip in the row — matches working + online\n chip treatment so all three digits in the chip row\n stay width-stable across counter crossings). */\n /* Round 399 / Loop: active-links chip closes the 3-chip\n chip-row by extending R398's hover translateY(-1px)\n lift onto the third (rightmost) chip. The R398 family\n already covered working + online chips on the\n clickable variant; R399 adds the same gate (isInter-\n active = flowLinks.length > 0) so empty active-links\n stays planted at R206's opacity-50 receded paint.\n transition-transform + ease-out + transform-gpu join\n the inline transition list (different property axes\n compose cleanly: inline handles color/bg/border/\n opacity, className handles transform).\n Gesture-vocabulary table (post-R399 — now complete\n across the chip-row):\n working chip -1 px (R398)\n online chip -1 px (R398)\n active-links chip -1 px (R399, this round)\n filter pin pills -1 px (R397)\n recent-signal row -1 px (R143)\n legend row -1 px (R144)\n Every interactive chip in TopoGraph lifts on hover.\n data-chip-hover-lift attr exposes the lift surface\n state ('true' clickable, 'false' empty) for tests. */\n // R414: `group` parent + inner unit span group-hover-brighten — see working chip above.\n 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 ${\n isInteractive\n ? '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'\n : 'bg-gray-500/10 text-gray-400 border-gray-500/20'\n }`}\n data-chip-hover-lift={isInteractive ? 'true' : 'false'}\n data-chip-group-hover-brighten=\"true\"\n data-active-links-chip\n data-active-links-flow-count={flowLinks.length}\n data-active-links-clickable={isInteractive ? 'true' : 'false'}\n data-active-links-empty={isInteractive ? 'false' : 'true'}\n title={tooltip}\n role={isInteractive ? 'link' : undefined}\n tabIndex={isInteractive ? 0 : undefined}\n style={{\n cursor: isInteractive ? 'pointer' : undefined,\n opacity: isInteractive ? 1 : 0.5,\n transition: 'color 200ms ease-out, background-color 200ms ease-out, border-color 200ms ease-out, opacity 200ms ease-out',\n }}\n onMouseEnter={() => { if (isInteractive) setHoveredActiveLinks(true); }}\n onMouseLeave={() => setHoveredActiveLinks(false)}\n onClick={() => { if (isInteractive) router.push('/messages'); }}\n onKeyDown={(e) => {\n if (!isInteractive) return;\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n router.push('/messages');\n }\n }}\n >\n {/* R338 — active-links chip digit/unit split, completes\n the 5th chip surface in the R333/R335/R336/R337\n chip-internal-hierarchy arc. data-active-links-\n chip-unit exposes the unit span for tests. */}\n {/* R362 sibling — active-links chip digit gains font-semibold. */}\n <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>\n {rel ? (() => {\n // Round 161 / Loop: extend R160's recency-pip\n // vocabulary up one scope — from per-flow row to\n // fleet-aggregate chip. The chip already shows\n // recency in text (\"last 5s ago\"); the \" · \"\n // separator bullet was dead gray. Color the\n // bullet by freshness using the same alpha\n // ramp R160 uses on recent-signal rows:\n // ageSec ≤ 30 → 1.0 (fully fresh)\n // 30-300s → smooth decay 1.0 → 0.25\n // > 300s → 0.25 (stale floor)\n // Cyan bullet pulse when fresh + gray text tail\n // = \"is the network firing right now\" readable\n // at a glance, without parsing the timestamp\n // numerals. Same vocabulary the canvas uses\n // (R10 edge fade) and the recent-signal panel\n // uses (R160 row pip) — three nested scopes\n // now share one freshness ladder.\n const ageSec = recent !== null\n ? Math.max(0, (Date.now() - recent) / 1000)\n : 999;\n const alpha = ageSec <= 30\n ? 1\n : ageSec <= 300\n ? 1 - ((ageSec - 30) / 270) * 0.70 /* R358: floor 0.25 → 0.30 lift across 3 freshness scopes */\n : 0.30; /* R358: stale floor lifted 0.25 → 0.30 — 20% legibility bump while preserving fresh/stale ratio */\n // Cyan dark / teal light to match palette legendAccent.\n const dotColor = isLight\n ? `rgba(13, 148, 136, ${alpha.toFixed(2)})`\n : `rgba(34, 211, 238, ${alpha.toFixed(2)})`;\n // Round 342 / Loop: active-links chip freshness suffix\n // wrapper text-gray-500 → text-gray-400 (R317\n // subordinate-text-lift family applied to chrome\n // inactive Layout toggle + R333 vendor count suffix).\n // The \"last 5s ago\" suffix is chip-subordinate\n // metadata; gray-500 sat near-invisible against the\n // chip's outer color, gray-400 lifts it into the band\n // where the eye reads it as deliberate freshness\n // annotation. The freshness DOT keeps its own inline\n // color: dotColor — the lift only affects the trailing\n // literal \"last {rel}\" text.\n return (\n // Round 357 / Loop: active-links chip freshness\n // suffix wrapper picks up `tabular-nums` for digit\n // width-lock. Pre-R357 the literal \"last {rel}\"\n // text (e.g. \"last 5s ago\", \"last 10s ago\",\n // \"last 1m ago\") had natural-figure digits — the\n // freshness ticker updates every second, so the\n // 9→10 boundary on the seconds counter and the\n // 59→60s → 1m flip both jittered ~1-2 px of glyph\n // width which propagated through the chip-row's\n // inline-flex layout, nudging the freshness DOT\n // and the chip's left edge. Tabular-nums on the\n // wrapper applies to all descendant digits only\n // (letters render at natural widths) so the\n // ticker stays planted across every count cross.\n // Joins the R224-R232 info-density tabular-nums\n // sweep at the chip-row freshness scope. Pure\n // paint-level change, no geometry shift on rest.\n // The R342 text-gray-400 lift + R161 dot freshness\n // alpha ramp + R317 subordinate-text-lift family\n // all preserved. data-active-links-freshness-\n // wrapper attr exposes the wrapper for tests.\n <span className=\"text-gray-400 tabular-nums\" data-active-links-freshness-wrapper>\n <span\n data-active-links-freshness-dot\n data-active-links-freshness-alpha={alpha.toFixed(2)}\n style={{\n color: dotColor,\n fontWeight: alpha > 0.7 ? 700 : 400,\n transition: 'color 200ms ease-out',\n }}\n >{' · '}</span>\n last {rel}\n </span>\n );\n })() : null}\n </span>\n );\n })()}\n <FreshnessChip sessions={sessions} />\n </div>\n </div>\n\n <div\n ref={containerRef}\n /* Round 330 / Loop (milestone): canvas wrapper rounded-lg\n → rounded-xl (8px → 12px corner radius). The biggest\n single surface on the dashboard by pixel area now reads\n as modern-SaaS-contemporary rather than 2020-conservative\n — same 4px bump R197 applied to the legend swatch and\n R295 applied to the title-block crescent. R330 ports\n the gesture to the OUTER envelope.\n Inner content (SVG viewBox 1000×680) sits behind\n `overflow-hidden`, so corner-radius change only affects\n the wrapper's own paint area and the shadow contour;\n the topo-overlap-test reads SVG-internal geometry and is\n unaffected. R254 background-color / R254 border-color /\n R263 box-shadow transitions all carry through unchanged.\n Marks R330 milestone of 5 rounds (R326-R330) of layout-\n geometry polish (gap-tier + crescent fade + trailer\n compensator + corner radius). */\n className={`relative overflow-hidden rounded-xl border shadow-2xl ${isLight ? 'shadow-zinc-900/5' : 'shadow-cyan-950/30'} ${isFullscreen ? 'flex items-center justify-center' : ''}`}\n data-topo-wrapper\n /* Round 254 / Loop: top-level TopoGraph wrapper gains theme-\n toggle transition. This is the BIGGEST theme-driven surface\n on the dashboard by pixel area — pal.containerBg fills the\n entire visible canvas area (cyber #080814 ↔ light #ffffff),\n and pal.containerBorder rims it. Pre-R254 every inner\n element eased through theme but the outer wrapper hard-cut,\n visually anchoring the snap. R253 declared\n \"no visible snap remains\" prematurely — this wrapper was\n the largest holdout. 200ms ease-out matches the panel\n treatment (R247) so wrapper + panels ease as one unit.\n\n Round 263 / Loop: close R254's holdover gap — the wrapper's\n shadow-2xl + theme-conditional `shadow-{color}/{opacity}`\n Tailwind class (cyber `shadow-cyan-950/30` ↔ light\n `shadow-zinc-900/5`) ALSO changes on theme toggle, but the\n inline transition list only covered background-color +\n border-color. Result: every inner element eased through\n theme, the wrapper bg/border eased, but the wrapper's\n DROP-SHADOW snapped — a subtle but real holdover from\n R254's \"TRULY complete\" claim. Adding `box-shadow 200ms\n ease-out` to the transition list catches the className-\n driven box-shadow swap (CSS transition on box-shadow eases\n the shadow property even when its color comes from a\n Tailwind class change, because the property itself is\n transition-eligible regardless of source). */\n style={{\n background: pal.containerBg,\n borderColor: pal.containerBorder,\n transition: 'background-color 200ms ease-out, border-color 200ms ease-out, box-shadow 200ms ease-out',\n }}\n >\n {/* Round 265 / Loop: top-rail (1px-tall colored line at the top\n of the canvas wrapper) picks up theme-toggle transition.\n Pre-R265 the className `bg-gradient-to-r ${pal.topRail\n Gradient}` was theme-conditional — cyber `via-cyan-400/70`\n ↔ light `via-emerald-500/40` — but no inline transition,\n so the rail SNAPPED on theme flip while the wrapper bg\n (R254) + border (R254) + shadow (R263) all eased. The top-\n rail is the THIN BRIGHT LINE that visually anchors the\n canvas top edge — its hard color flip was a small but real\n theme-snap that broke the otherwise-eased canvas envelope.\n transition: background-image catches the className-driven\n gradient swap; Chrome ≥ 89 / Safari ≥ 14.1 / FF ≥ 96\n interpolate linear-gradients with matching stop structures\n (both gradients are `from-transparent via-X to-transparent`\n → same 3-stop layout). data-topo-top-rail makes the probe\n deterministic. */}\n <div\n className={`absolute inset-x-0 top-0 h-px bg-gradient-to-r ${pal.topRailGradient}`}\n data-topo-top-rail\n style={{ transition: 'background-image 200ms ease-out' }}\n />\n\n {/* Round 158 / Loop: give the canvas SVG itself an accessible\n name + role description. R151-R157 added a11y to every\n interactive surface inside the canvas (nodes, group labels,\n badges, rows, minimap, chrome buttons) — but the canvas\n container itself was a nameless 1000×680 box. A screen\n reader that hit the SVG before tab-diving into its\n children heard nothing identifying it as \"the topology\".\n aria-roledescription gives it a meaningful announcement;\n aria-label provides a live snapshot of the network\n (online / working / active links / offline) plus the two\n canvas-scope gestures (Tab + double-click). Default SVG\n role is graphics-document so children remain navigable —\n we deliberately don't override role with \"img\" which\n would flatten the SVG to a single opaque image. */}\n <svg\n ref={svgRef}\n viewBox=\"0 0 1000 680\"\n className=\"w-full h-auto block\"\n preserveAspectRatio=\"xMidYMid meet\"\n aria-roledescription=\"agent network topology\"\n aria-label={(() => {\n const online = onlineNodes.length;\n const working = workingCount;\n const offline = offlineNodes.length;\n const flows = flowLinks.length;\n const parts: string[] = [];\n parts.push(`${online} agent${online === 1 ? '' : 's'} online`);\n if (working > 0) parts.push(`${working} working`);\n if (offline > 0) parts.push(`${offline} offline`);\n parts.push(`${flows} active link${flows === 1 ? '' : 's'}`);\n return `Agent network topology — ${parts.join(' · ')}. Tab to navigate nodes, double-click canvas to reset view.`;\n })()}\n data-topo-canvas-aria\n /* Round 469 / Loop — fleet-split numeric attrs on the root\n svg. The aria-label already encodes online/working/offline\n /flow counts in text form (R7 origin) but DOM probes had\n to PARSE the label string to extract the numbers. R469\n surfaces them as 4 numeric data-attrs alongside the R462\n dashboard-version + R466 any-hover + R467 any-pinned set\n that already live on the root svg:\n data-topo-online-count total online sessions\n data-topo-working-count subset currently working\n data-topo-offline-count offline / ghost-purged\n data-topo-flow-count active flow links\n Use cases:\n - Playwright: one-line `svg.getAttribute('data-topo-\n working-count')` instead of parsing aria-label\n - external CSS: data-attribute selectors for empty\n vs populated states (`[data-topo-online-count='0']`)\n - a11y enrichment: screen-reader scripts can read the\n numeric attrs directly\n - hub-aria parity: the hub-center text already shows\n `workingCount` digit (R130); R469 puts the same scalar\n on the canvas root for non-visual consumers.\n Composed from existing onlineNodes / workingCount /\n offlineNodes / flowLinks — no new state. */\n data-topo-online-count={onlineNodes.length}\n data-topo-working-count={workingCount}\n data-topo-offline-count={offlineNodes.length}\n data-topo-flow-count={flowLinks.length}\n /* Round 471 / Loop — surface 2 remaining canvas-level mode\n attrs alongside the R462/R466/R467/R469 set. Pre-R471 the\n root svg exposed 7 attrs but tests probing \"what layout\n is active\" had to query DOM internals (data-topo-chrome-\n layout-active on the chrome button row) or parse the URL\n for theme. R471 puts both modes on the root for one-stop\n snapshot reads:\n data-topo-layout — 'ring' | 'grid'\n data-topo-theme — 'cyber' | 'light'\n Together with R469 the canvas root now carries 9 cross-\n cutting attrs (1 build identity + 2 inspection mode + 4\n fleet split + 2 layout/theme). Test harness can read the\n FULL canvas state with 9 getAttribute calls; no traversal\n into chrome strip / theme provider / panel rows.\n Composed from existing `layout` (R138 ring↔grid toggle\n state) + `isLight` (R12 theme palette gate) — no new\n state, zero re-render cost. */\n data-topo-layout={layout}\n data-topo-theme={isLight ? 'light' : 'cyber'}\n /* Round 487 / Loop — extends R469/R471 root-svg state surface\n with current zoom level (numeric attr, 2 decimals). Pre-\n R487 the canvas zoom was queryable via `data-topo-minimap-\n viewport-glow='true'` boolean (R481, gated at > 1.5) but\n the exact zoom number only lived in the chrome-strip span\n (`{Math.round(view.zoom * 100)}%`). Tests + external CSS\n that need the zoom value had to traverse to the chrome\n strip or read view state via React internals.\n R487 surfaces it at the canvas root, consistent with\n R469's fleet-count numeric pattern. Two-decimal precision\n matches the internal `view.zoom` float without losing\n info. Composed from existing state — no new state.\n Root svg attribute set now 10 attrs total:\n R462 data-dashboard-version build identity\n R466 data-topo-any-hover transient mode\n R467 data-topo-any-pinned sticky mode\n R469 data-topo-online-count fleet (4 numeric)\n R469 data-topo-working-count\n R469 data-topo-offline-count\n R469 data-topo-flow-count\n R471 data-topo-layout canvas mode\n R471 data-topo-theme canvas mode\n R487 data-topo-zoom canvas zoom */\n data-topo-zoom={view.zoom.toFixed(2)}\n /* Round 488 / Loop — pairs the R466 hover-aggregate BOOLEAN\n with the corresponding hover IDENTITY attr. Pre-R488 a\n test harness could query \"is anything hovered\" but had to\n traverse per-node `data-node` elements with focus-state\n attrs to recover WHICH alias. R488 surfaces it directly\n at canvas root. Empty string when null (always-present\n attr, consistent with the 10-attr state-surface set —\n never `undefined`-collapsed so observers can rely on a\n single `getAttribute('data-topo-hovered-alias')` returning\n either '' or the alias string).\n Note: only the `hoveredAlias` axis (R466's first source)\n gets the identity twin in R488. The other 5 hover sources\n (hoveredHub / hoveredEdgeKey / hoveredGroupLabel / hovered\n Status / hoveredVendor) are non-alias-shaped (hub center\n is singleton; edge has `from→to` key; status/vendor are\n categorical) — separate dedicated attrs if/when needed.\n Root svg attribute set now 11 attrs total. */\n data-topo-hovered-alias={hoveredAlias ?? ''}\n /* Round 466 / Loop — aggregate hover signal on the root SVG.\n Exposes a single boolean `data-topo-any-hover` that\n reflects whether ANY hover state in the topology is\n active. Composed from the existing per-surface hover\n vars; doesn't introduce new state. Useful for:\n - Playwright tests asserting \"topology entered a hover\n mode\" without enumerating per-surface attrs\n - external CSS hooks targeting `[data-topo-any-hover=\n \"true\"]` to dim adjacent UI (e.g. chrome strip)\n while the user is inspecting the canvas\n - debug overlays that visualise hover dwell-time\n The 6 hover sources contributing:\n hoveredAlias (node circle / card / alias text)\n hoveredHub (hub center, halo, ring)\n hoveredEdgeKey (flow link path / particle / endpoint)\n hoveredGroupLabel (cluster name / count / pips)\n hoveredStatus (legend row)\n hoveredVendor (vendor chip in chip row)\n Read-only computed attr — zero re-render cost beyond the\n React update that already fires when any of those state\n vars flips. Geometry / paint untouched. */\n data-topo-any-hover={\n (hoveredAlias || hoveredHub || hoveredEdgeKey || hoveredGroupLabel ||\n hoveredStatus || hoveredVendor) ? 'true' : 'false'\n }\n /* Round 467 / Loop — pin-aggregate sibling to R466 hover-\n aggregate. Exposes `data-topo-any-pinned` reflecting\n whether ANY sticky inspection mode is active. Composed\n from the 4 pinned state vars:\n pinnedStatus (legend row click → status filter)\n pinnedGroup (group label click → cluster lock)\n pinnedVendor (vendor chip click → vendor filter)\n pinnedEdgeKey (edge click → edge focus)\n Together with R466 the root svg now carries a 2-bit\n inspection-mode surface:\n data-topo-any-hover — transient (mouse hover)\n data-topo-any-pinned — sticky (click-to-lock)\n Useful for:\n - Playwright tests: one-line query for either mode\n - external CSS hooks: render a persistent \"filter\n active\" badge when pinned, distinct from the\n transient hover dim\n - Esc-handler tests: assert all 4 pins clear after\n the universal-cancel Escape press (R62/R63/R88/\n R116 — single Esc collapses every pin)\n Read-only computed disjunction; no new state, zero\n re-render cost beyond the React pin-flip updates. */\n data-topo-any-pinned={\n (pinnedStatus || pinnedGroup || pinnedVendor || pinnedEdgeKey) ? 'true' : 'false'\n }\n /* Round 462 / Loop — surface DASHBOARD_VERSION on the root SVG\n element as `data-dashboard-version`. Directly closes the\n feedback_dash_zombie_port_3000.md memory rule: \"verify ships\n via SVG DOM, not tmux 'Ready' — zombie next-servers + stale\n global installs silently serve old code\". Pre-R462 the only\n ways to know which preview the dash was serving were:\n 1. parse the npm registry for the latest tag (network)\n 2. fetch /api/dashboard/version (API surface, no DOM)\n 3. inspect the /login footer or /settings page (off-route)\n Test scripts that probe TopoGraph DOM (overlap, group-label\n tint, pip strip, etc.) couldn't tell whether the dash was\n actually serving the build they expected to verify. R462\n threads DASHBOARD_VERSION through to the root <svg> so:\n - Playwright probes can read svg[data-dashboard-version]\n directly + fail-fast on stale-build mismatch\n - the memory rule's manual zombie check (\"inspect SVG\n dom\") becomes a one-attr probe\n - operators DOM-inspect to confirm the live version\n matches the npm tag without leaving the topology page\n Geometry/visual impact: ZERO (data-* attrs don't paint).\n The version string is build-time injected via the existing\n DASHBOARD_VERSION constant (R51 footer + R51 settings page\n already consume it from app/lib/version.ts → reads\n package.json pkg.version). No business logic added. */\n data-dashboard-version={DASHBOARD_VERSION}\n onPointerDown={onPointerDown}\n onPointerMove={onPointerMove}\n onPointerUp={onPointerUp}\n onPointerLeave={onPointerUp}\n // Round 41 / Loop: the reset-button title (R22) and the Help\n // overlay (R25) both advertise \"double-click the canvas to\n // reset\", but the handler was never actually wired. The text\n // was lying. Wire it here, guarded so dbl-clicking a node\n // (which would also trigger the SVG-level handler via event\n // bubbling) doesn't unexpectedly reset the view on the user.\n onDoubleClick={(e) => {\n const t = e.target as Element | null;\n if (t?.closest('g[data-node]')) return;\n resetView();\n }}\n style={{ cursor: isPanning ? 'grabbing' : 'grab', touchAction: 'none' }}\n >\n <defs>\n <linearGradient id=\"topo-panel\" x1=\"0\" x2=\"1\" y1=\"0\" y2=\"1\">\n <stop offset=\"0%\" stopColor={pal.panelStops[0]} />\n <stop offset=\"48%\" stopColor={pal.panelStops[1]} />\n <stop offset=\"100%\" stopColor={pal.panelStops[2]} />\n </linearGradient>\n <radialGradient id=\"topo-radar\" cx=\"50%\" cy=\"50%\" r=\"55%\">\n <stop offset=\"0%\" stopColor={pal.radarStops[0].color} stopOpacity={pal.radarStops[0].opacity} />\n <stop offset=\"45%\" stopColor={pal.radarStops[1].color} stopOpacity={pal.radarStops[1].opacity} />\n <stop offset=\"100%\" stopColor={pal.radarStops[2].color} stopOpacity={pal.radarStops[2].opacity} />\n </radialGradient>\n {!isLight && (\n <filter id=\"topo-glow\">\n <feGaussianBlur stdDeviation=\"4\" result=\"blur\" />\n <feMerge>\n <feMergeNode in=\"blur\" />\n <feMergeNode in=\"SourceGraphic\" />\n </feMerge>\n </filter>\n )}\n {/* R142 / Loop: drop-shadow filter for group-box hover-lift.\n Mirrors R135's panel hover-elevation idiom but applied\n at the per-group canvas level. R68 already gives a\n hovered/pinned group box a solid accent stroke; R142\n adds a soft outward shadow on top so the box visually\n \"rises off the canvas\" when selected — same visual\n vocabulary as the panels + the Overview KPI cards\n (R18). Theme-aware flood: darker shadow on cyber so\n it reads above the dark canvas, lighter for light\n theme. Bounding box of the group box is unchanged —\n filters only affect paint area, not bbox geometry, so\n the overlap-test invariant is preserved. */}\n <filter id=\"topo-groupbox-lift\" x=\"-10%\" y=\"-10%\" width=\"120%\" height=\"120%\">\n <feDropShadow\n dx=\"0\" dy=\"3\" stdDeviation=\"4\"\n floodColor={isLight ? '#0f172a' : '#000000'}\n floodOpacity={isLight ? 0.18 : 0.55}\n />\n </filter>\n {/* Round 16 / Loop: 3-tier flow-link arrow markers.\n The single marker had `markerUnits` defaulting to\n `strokeWidth`, so heavy edges (stroke=7) rendered 35-user-\n unit arrowheads — visually dominant, the head outweighed\n the line. Switching to `userSpaceOnUse` decouples arrow\n size from stroke; binning by count gives a clearer\n hierarchy than continuous linear scaling:\n s (count 1-2) → 12 user units\n m (count 3-4) → 16 user units (alias for `topo-arrow`)\n l (count 5+) → 22 user units\n `topo-arrow` stays bound to the medium tier so the legend\n swatch (line ~1500) renders without change. */}\n {[\n { id: 'topo-arrow-s', size: 12 },\n { id: 'topo-arrow', size: 16 },\n { id: 'topo-arrow-l', size: 22 },\n ].map(m => (\n <marker\n key={m.id}\n id={m.id}\n viewBox=\"0 0 10 10\"\n refX=\"8\"\n refY=\"5\"\n markerWidth={m.size}\n markerHeight={m.size}\n markerUnits=\"userSpaceOnUse\"\n orient=\"auto-start-reverse\"\n >\n <path d=\"M 0 0 L 10 5 L 0 10 z\" fill={pal.arrowFill} />\n </marker>\n ))}\n {/* Round 45: radial radar sweep gradient — bright at center\n (where it meets the hub) fading to leading edge. */}\n <radialGradient id=\"topo-sweep\" cx=\"0%\" cy=\"50%\" r=\"100%\">\n <stop offset=\"0%\" stopColor={isLight ? '#0d9488' : '#22d3ee'} stopOpacity={isLight ? 0.18 : 0.32} />\n <stop offset=\"70%\" stopColor={isLight ? '#0d9488' : '#22d3ee'} stopOpacity={isLight ? 0.10 : 0.18} />\n <stop offset=\"100%\" stopColor={isLight ? '#0d9488' : '#22d3ee'} stopOpacity=\"0\" />\n </radialGradient>\n </defs>\n\n {/* panel backdrop stays fixed — panning never reveals empty canvas */}\n <rect width=\"1000\" height=\"680\" fill=\"url(#topo-panel)\" />\n\n {/* Round 103 (issue #81): everything inside this <g> zooms + pans\n together. transform order = translate then scale.\n Round 168 / Loop: smoothView arms a one-shot transition on\n the transform attribute, active only when resetView/fitView\n fires. Pan (R103 pointer drag) and wheel zoom never set the\n flag, so they stay snappy with no lag. Pressing `0`, `f`,\n clicking the hub (R52), or chrome reset/fit buttons triggers\n a 300ms ease-out glide instead of a jolt. Respects prefers-\n reduced-motion via the R29 globals.css blanket override that\n neutralises transition-duration universally. */}\n <g\n transform={`translate(${view.x} ${view.y}) scale(${view.zoom})`}\n data-topo-viewport\n data-topo-viewport-smooth={smoothView ? 'true' : 'false'}\n data-topo-viewport-layout-switching={layoutSwitching ? 'true' : 'false'}\n data-topo-viewport-nodesize-switching={nodeSizeSwitching ? 'true' : 'false'}\n style={{\n // R170 / Loop: opacity always carries a transition so the\n // layout-switch crossfade fires cleanly. R168 smoothView\n // transition on transform is added only when armed (pan\n // and wheel zoom MUST stay snappy). The two arming flags\n // compose without conflict — different visual axes.\n // R171: nodeSizeSwitching shares the same opacity-dim\n // pathway as layoutSwitching — clicking S/M/L in the\n // chrome triggers the same soft-blink masking. ORing\n // both flags keeps the opacity expression simple while\n // the two distinct data-* attributes let tests\n // disambiguate which gesture armed the crossfade.\n transition: smoothView\n ? 'opacity 250ms ease-out, transform 300ms ease-out'\n : 'opacity 250ms ease-out',\n opacity: (layoutSwitching || nodeSizeSwitching) ? 0.45 : 1,\n }}\n >\n {/* Issue #87: radar/ring ambiance renders only in ring layout —\n grid mode drops it so the concentric rings don't sit behind a\n rectangular grid. */}\n {layout === 'ring' && (<>\n {/* R52: radar bg is pure decoration — drop its pointer events so\n the hub <g> under it (and any future inner-disk affordances)\n can receive clicks. Previously the r=330 disc intercepted\n the hub click outright. */}\n <circle cx={cx} cy={cy} r=\"330\" fill=\"url(#topo-radar)\" style={{ pointerEvents: 'none' }} />\n\n {/* Round 45: subtle star field — deterministic dots scattered\n across the canvas give the radar bg some depth. Skipped on\n light theme so the white surface stays clean.\n Round 291 / Loop: starfield dot count 28 → 14 (50%\n reduction). Post-R290 inner radar ring retirement the\n canvas has cleared meaningfully — sweep + 3 radar rings\n + tier guides + nodes + edges are doing the visual work.\n The starfield's role is atmospheric depth, not\n information; cutting density by half preserves the\n \"space/radar\" feel while removing decoration the eye\n has to skip. Same R275-R281 减法 family idiom as the\n orbit / halo / spoke retirements; same R290 pivot back\n to subtractive register. data-topo-starfield-dot\n attribute makes the dots probe-able for the regression\n test. */}\n {!isLight && (\n <g opacity=\"0.5\" style={{ pointerEvents: 'none' }} data-topo-starfield>\n {Array.from({ length: 14 }).map((_, i) => {\n // Deterministic pseudo-random scatter so positions are\n // stable between renders (no JS hydration mismatch).\n const seed = i * 9301 + 49297;\n const x = ((seed * 13) % 1000);\n const y = ((seed * 7) % 680);\n const r = (i % 3 === 0) ? 1.2 : 0.7;\n return <circle key={i} cx={x} cy={y} r={r} fill=\"#a5b4fc\" opacity={0.35 + (i % 4) * 0.05} data-topo-starfield-dot={i} />;\n })}\n </g>\n )}\n\n {/* Round 45: rotating radar sweep — a 40° wedge with a soft\n leading-edge gradient. Slow 6s rotation reads as a radar\n scan without being noisy. Inline transform-origin on the\n <g> wrapper ensures Chrome / Firefox rotate around (cx,cy)\n instead of the SVG viewBox corner.\n\n v0.10.0 Hero 3 Wave 1 / RFC §3.B (Vincent 5222 holdover):\n sweep arc retired. The diagonal rotating wedge competes\n with working-halo SMIL, hub busyness breath, and edge\n flow animation — on a 16:9 Twitter screenshot it reads\n 'wow lots of motion' rather than 'agents communicating'.\n Same idiom as R278/R279/R280 retirements — `false &&`\n short-circuits the IIFE so it's a one-line rollback. */}\n {false && (() => {\n // R146: radar sweep rotation buckets on workingCount, joining\n // R84 hub breath / R131 outer orbit / R132 group march /\n // R145 idle spokes as the 5th and final layer in the busyness-\n // driven motion family. 8 / 6 / 4 / 3 seconds — same 0 / 1-2 /\n // 3-5 / 6+ thresholds R84 uses. \"Busier fleet = more frequent\n // scans\" feels semantically right for a radar idiom. R45\n // baseline 6s sits at bucket 1 so the calm/busy spread bracks\n // around the historical default.\n const busy = workingCount === 0 ? 0\n : workingCount <= 2 ? 1\n : workingCount <= 5 ? 2\n : 3;\n const sweepDur = [8, 6, 4, 3][busy];\n return (\n <g\n style={{\n transformOrigin: `${cx}px ${cy}px`,\n transformBox: 'view-box',\n pointerEvents: 'none',\n // CSS var consumed by `.anet-topo-sweep` (line 848 of\n // globals.css). React's CSSProperties type doesn't model\n // custom properties → cast through Record<string, string>.\n ...({ ['--sweep-dur']: `${sweepDur}s` } as Record<string, string>),\n } as React.CSSProperties}\n className=\"anet-topo-sweep\"\n opacity={isLight ? 0.7 : 1}\n data-topo-sweep-bucket={busy}\n data-topo-sweep-dur={sweepDur}\n >\n <path\n d={`M ${cx} ${cy} L ${cx + 330} ${cy} A 330 330 0 0 0 ${cx + 330 * Math.cos(-Math.PI / 4.5)} ${cy + 330 * Math.sin(-Math.PI / 4.5)} Z`}\n fill=\"url(#topo-sweep)\"\n />\n </g>\n );\n })()}\n\n {/* radar rings — pure decoration at fixed radii, independent of\n node positions so the radar aesthetic is preserved across tier\n changes.\n Round 290 / Loop: drop the innermost radar ring at r=90.\n That ring sat ~66px outside the hub (hub radius 24, halo\n r=18), in the exact zone R276 (orbit particles) / R278\n (working halo) / R280 (backdrop spokes) cleared during\n the R275-R281 减法 arc. Post-cleanup the lone r=90 ring\n read as a leftover decorative loop hugging the hub — a\n visual element with no remaining sibling to anchor.\n Dropping it returns to the subtractive register after\n R282-R289's 8 加法 rounds and lets the hub breathe. The\n outer three rings (170 / 250 / 330) still carry the\n radar aesthetic across the canvas. New data-topo-radar-\n ring attribute exposes each remaining ring radius for\n test probing. */}\n {[170, 250, 330].map(radius => (\n <circle\n key={radius}\n cx={cx} cy={cy} r={radius}\n fill=\"none\" stroke={pal.ringStroke} strokeWidth=\"1\"\n opacity={isLight ? 0.6 : 0.35}\n data-topo-radar-ring={radius}\n />\n ))}\n\n {/* Round 54 / Loop: tier-radius guide rings. The radar rings above\n are decorative and don't match the actual tier radii nodes sit\n on (single 220 / dual 175,260 / triple 145,215,285). Drawing\n a faint dashed ring at each ACTIVE tier radius lets the eye\n anchor \"this is the inner / outer ring\" without inferring from\n node spacing. Picked based on online node count so only the\n tiers currently in use draw — empty tiers stay quiet. pointer-\n events:none so they never intercept hub or node clicks. The\n 0.7 stroke + dashed pattern reads as guide, not feature. */}\n {(() => {\n const tierRadii = onlineNodes.length > onlineTripleThreshold\n ? [onlineTripleInnerR, onlineTripleMidR, onlineTripleOuterR]\n : onlineNodes.length > onlineTierThreshold\n ? [onlineInnerRadius, onlineOuterRadius]\n : onlineNodes.length > 0\n ? [onlineRadius]\n : [];\n // Round 92 / Loop: tier-ring occupancy. R54 drew the guide\n // rings at fixed opacity regardless of how many nodes lived\n // on each tier. With pinned filters dimming most nodes to\n // 0.28, the ring at a deserted tier looked identical to a\n // crowded one — wasting a free piece of canvas. Count\n // online nodes whose hub-distance falls within ±15 px of\n // each ring (15 px = half the inter-tier gap, so each\n // node is assigned to exactly one ring). Empty tier → skip\n // entirely. Crowded tier → stronger opacity, says \"look\n // here\". Buckets so the ladder feels intentional, not\n // jittery as one node migrates between tiers.\n const occupancyOf = (r: number) => onlineNodes.reduce((acc, s) => {\n const p = nodePositions[s.alias];\n if (!p) return acc;\n const d = Math.hypot(p.x - cx, p.y - cy);\n return Math.abs(d - r) < 15 ? acc + 1 : acc;\n }, 0);\n // Round 93 / Loop: when any pin is active, tint the tier\n // rings to the legend accent so the spatial guide visually\n // shares the canvas's \"filtered mode\" colour. The chip\n // row already says WHICH filter is on (pills + letter\n // mirror); rings answering \"we're filtered\" reinforces\n // the state when the eye is on the canvas, not the chip\n // row. Composes cleanly with R92 occupancy — same opacity\n // bucket logic; just the stroke colour swaps.\n const anyPin = !!(pinnedStatus || pinnedGroup || pinnedVendor);\n const tierStroke = anyPin ? pal.legendAccent : pal.ringStroke;\n return tierRadii.map((r, tierIdx) => {\n const n = occupancyOf(r);\n if (n === 0) return null;\n const bucket = n <= 2 ? 0 : n <= 6 ? 1 : 2;\n const opLight = [0.24, 0.36, 0.50][bucket];\n const opDark = [0.32, 0.46, 0.62][bucket];\n // Round 174 / Loop: tier guide rings fade-in alongside\n // the R9/R72/R172/R173 first-paint wave. Ring layout's\n // structural scaffolding (R54 dashed concentric guides)\n // was the last instant-pop element after R173 closed\n // group boxes in grid. Same vocabulary — .anet-fade-in\n // mount-once CSS animation + per-ring stagger 60ms ×\n // tierIdx (cap 8). Tier rings are at most 3 (single /\n // dual / triple), so the visible range is 0-120ms.\n // Inner ring leads outward — emanates from the hub.\n // transition list grows `opacity 250ms ease-out` so the\n // post-animation snap from animation's end state (1) to\n // the bucket opacity (0.24-0.62) eases instead of cuts.\n // Same pattern node fade-in uses (R9 + transition-opacity\n // from R3 className). data-tier-fade-delay exposes the\n // computed delay for test probing.\n const fadeDelay = Math.min(tierIdx, 8) * 60;\n return (\n <circle\n key={`tier-${r}`}\n cx={cx} cy={cy} r={r}\n fill=\"none\"\n stroke={tierStroke}\n strokeWidth=\"0.7\"\n /* Round 303 / Loop: tier guide dashes tighten from\n \"2 8\" → \"2 6\" (8px gap → 6px gap). R54 set \"2 8\"\n to read as a faint hint behind everything else;\n after R290 (inner radar ring retired) + R291\n (starfield 50%) cleared the surrounding backdrop\n density, the tier guides carry more \"this is the\n ring nodes sit on\" visual responsibility. Pulling\n the gap from 8→6 puts dashes closer together so\n the ring reads as a clearer continuous mark\n rather than scattered dots, without bumping\n strokeWidth (0.7) or opacity (R92 bucketed). The\n 2px dash itself unchanged — same density signal\n per dash, just fewer-px space between them. */\n strokeDasharray=\"2 6\"\n opacity={isLight ? opLight : opDark}\n className=\"anet-fade-in\"\n style={{\n pointerEvents: 'none',\n transition: 'stroke 200ms ease-out, opacity 250ms ease-out',\n animationDelay: `${fadeDelay}ms`,\n }}\n data-tier-ring={r}\n data-tier-occupancy={n}\n data-tier-bucket={bucket}\n data-tier-tinted={anyPin ? 'true' : 'false'}\n data-tier-fade-delay={fadeDelay}\n />\n );\n });\n })()}\n\n {/* Round 50: 4 small particles slowly orbiting the outer ring\n (r=330). Each starts at a different angle (offset 0/0.25/0.5/0.75\n of the cycle) so they're evenly spaced. 16s per revolution is\n slow enough to feel ambient, not noisy. Skipped on light theme\n so the white surface stays clean.\n\n R131 / Loop: orbit period now buckets on workingCount,\n mirroring R84's hub-busyness breath cadence. An idle\n fleet keeps the original 16s \"calm sweep\"; as work\n accumulates the orbit subtly accelerates (capped at\n 10s so it never feels frantic). Two-layer motion\n coordination now: R84 breathes the hub, R131 spins\n the outer ring — both reading the same underlying\n \"is the network busy\" signal, both visible\n simultaneously without competing. Same bucket\n thresholds (0 / 1-2 / 3-5 / 6+) the R84 block uses\n at line ~2702 so the two cadences stay in sync if\n a future refactor shifts buckets.\n\n Round 276 / Loop: orbit particles DISABLED by default\n per Vincent 5214/5215-5217 visual-audit relay\n (clutter cleanup for Twitter screenshot). The 4\n particles encode workingCount busyness via speed\n (R131) + opacity (R216) — but that signal is\n ALREADY conveyed by:\n · hub halo opacity breath (R244, R84)\n · hub digit workingCount text (R130)\n · pressure-bar working/idle/offline ratio (R31)\n So orbit particles are info-redundant decoration:\n they don't add new signal, just add visual noise at\n the canvas outer edge. R276 gates the render block\n with `false &&` so the code stays (commented context\n + tests preserved for hypothetical rollback) but\n nothing renders. R131 busy-bucket constant + R216\n opacity-bucket constant are dead code post-R276 —\n acceptable since the family was R50/R131/R216 and\n R276 retires the family entirely. Net: 4 fewer\n moving dots on canvas. */}\n {false && !isLight && (() => {\n const busy = workingCount === 0 ? 0\n : workingCount <= 2 ? 1\n : workingCount <= 5 ? 2\n : 3;\n const dur = [16, 14, 12, 10][busy];\n // Round 216 / Loop: orbit particle opacity scales with busy\n // bucket alongside R131's speed scaling. Pre-R216 the\n // particles sat at flat opacity 0.9 regardless of fleet\n // workload — speed conveyed busyness but brightness was\n // mute. R216 layers brightness on top of speed so idle\n // fleets read calm (dim particles) and busy fleets read\n // bright (loud particles). Same R84 hub-breath /\n // R131 orbit-speed bucket thresholds (0 / 1-2 / 3-5 / 6+)\n // so the three motion layers (hub breath / orbit / group\n // march) plus the new brightness channel all derive from\n // one busyness metric. transition: opacity 300ms ease-out\n // matches R167 status-flip + R213 hub crossfade timing —\n // when first working node appears, hub focal point AND\n // outer ring particles brighten on the same 300ms beat.\n const orbitOpacity = [0.5, 0.7, 0.85, 1.0][busy];\n return [0, 0.25, 0.5, 0.75].map((phase, i) => (\n <g\n key={`orbit-${i}`}\n data-topo-orbit-bucket={busy}\n data-topo-orbit-dur={dur}\n data-topo-orbit-opacity={orbitOpacity}\n >\n <circle\n cx={cx + 330} cy={cy}\n r={i === 0 ? 2.8 : 2.2}\n fill=\"#22d3ee\"\n opacity={orbitOpacity}\n filter=\"url(#topo-glow)\"\n style={{ transition: 'opacity 300ms ease-out' }}\n >\n <animateTransform\n attributeName=\"transform\"\n type=\"rotate\"\n from={`${phase * 360} ${cx} ${cy}`}\n to={`${phase * 360 + 360} ${cx} ${cy}`}\n dur={`${dur}s`}\n repeatCount=\"indefinite\"\n />\n </circle>\n </g>\n ));\n })()}\n\n {/* Round 240 / Loop: extend R93 anyPin tinting from tier-rings\n to backdrop spokes. Pre-R240 the 6 radar-style spokes\n stayed at pal.ringStroke regardless of filter state,\n while R93 already shifted tier-rings to pal.legendAccent\n on any active pin. Result: ring scaffolding said\n 'filtered mode' but spoke scaffolding said 'rest' — the\n two halves of the canvas's spatial guide were out of\n sync. R240 ties them together; whole backdrop now reads\n as one filtered-mode-coloured unit when a pin is active.\n\n Same anyPin signal R93 uses (pinnedStatus || pinnedGroup\n || pinnedVendor). Same legendAccent tint colour. Same\n 200ms ease-out transition timing — pin a status, both\n tier-rings AND spokes ease to cyan together. */}\n {/* Round 280 / Loop: backdrop spokes RETIRED (R93 family with\n R240 tinting) per 减法 cut #6. The 6 radial lines at\n every 30° formed 12 rays from canvas center — even at\n opacity 0.18 (cyber) / 0.35 (light) they added explicit\n radial-line clutter behind the hub-and-spoke topology.\n The radial-gradient backdrop (topo-radar) ALREADY\n provides soft hub-centered glow; explicit lines on top\n were decorative density without structural signal that\n the topology itself doesn't already convey (hub at\n center + nodes on rings = radial structure inherent).\n `false &&` gates the render; code preserved for\n rollback. Same idiom as R276 orbit / R278 working halo\n / R279 ping+pulse retirements. */}\n {false && (() => {\n const anyPin = !!(pinnedStatus || pinnedGroup || pinnedVendor);\n const spokeStroke = anyPin ? pal.legendAccent : pal.ringStroke;\n return [0, 30, 60, 90, 120, 150].map(angle => (\n <line\n key={angle}\n x1={cx - 360 * Math.cos(angle * Math.PI / 180)}\n y1={cy - 360 * Math.sin(angle * Math.PI / 180)}\n x2={cx + 360 * Math.cos(angle * Math.PI / 180)}\n y2={cy + 360 * Math.sin(angle * Math.PI / 180)}\n stroke={spokeStroke}\n strokeWidth=\"1\"\n opacity={isLight ? 0.35 : 0.18}\n data-topo-spoke-angle={angle}\n data-topo-spoke-tinted={anyPin ? 'true' : 'false'}\n style={{ transition: 'stroke 200ms ease-out' }}\n />\n ));\n })()}\n </>)}\n\n {/* hub links — round 46: idle spokes now have animated\n stroke-dashoffset so dashes flow outward from the hub\n (\"command relay\" feel). Active spokes carrying live message\n flow stay as solid bright strokes.\n R145 / Loop: idle-spoke animation cadence buckets on\n workingCount, mirroring R84 hub-breath / R131 outer-\n ring orbit / R132 groupbox-march coordination. Same\n 0 / 1-2 / 3-5 / 6+ thresholds. Idle ↔ idle network\n has a slow \"command relay\" feel; as work accumulates\n the dashes accelerate outward, completing the 4th\n motion layer in the busyness-driven family:\n R84 hub breath (centre)\n R131 outer ring orbit (periphery)\n R132 groupbox march (per-team, grid layout)\n R145 idle-spoke flow (ring layout, hub→nodes)\n 4 surfaces, 1 signal. Same cadence ladder 2.8/2.4/2.0/\n 1.6s — 1.75× range from calm to busy, capped so the\n network never feels frantic. */}\n {layout === 'ring' && (() => {\n const busy = workingCount === 0 ? 0\n : workingCount <= 2 ? 1\n : workingCount <= 5 ? 2\n : 3;\n const spokeDur = [2.8, 2.4, 2.0, 1.6][busy];\n return onlineNodes.map((session, idx) => {\n const pos = nodePositions[session.alias];\n if (!pos) return null;\n const path = curvePath({ x: cx, y: cy }, pos, 0);\n const isActiveSpoke = activeAliases.has(session.alias);\n\n /* Round 241 / Loop: hub-link spokes (agent→hub paths in\n ring layout) eased state-flip between idle and active.\n Pre-R241 when a node sent or received a message its\n hub-spoke jumped one-frame from idle gray (pal.spoke-\n Stroke.idle + strokeWidth=1 + opacity=0.45) to active\n cyan (pal.spokeStroke.active + strokeWidth=2 + opacity\n =0.7) — three discrete property snaps in lockstep.\n R241 adds a 250ms ease-out transition list covering\n stroke + stroke-width + opacity so the activation\n 'lights up' smoothly. strokeDasharray stays binary\n (none ↔ '6 14') — dasharray doesn't interpolate\n cleanly between continuous and discrete forms across\n browsers (same lesson R167 documented for the node\n status ring). The CSS keyframe animation on idle\n spokes (anet-topo-spoke-flow) drives stroke-dashoffset\n separately and stays untouched. data-topo-hub-spoke-\n active surfaces the activity state for test probes\n (active spokes don't carry the bucket/dur attrs so\n they need their own data anchor). */\n // Round 382 / Loop: hub-spoke path picks up\n // strokeLinecap='round'. Sibling polish to R378 flow-\n // rail dashes + R380 group box dashes — three dashed-\n // stroke surfaces now share 'round' linecap:\n // R378 flow-rail '2 12' -> soft 3-px pills\n // R380 group box '6 6' -> soft 7.5-px pills\n // R382 hub spoke '6 14' -> soft 7-px pills (this round)\n // For idle spokes (dashed at sw=1), each 6-px dash gains\n // 0.5-px round caps and reads as a soft pill instead of\n // a sharp 6 x 1 rectangle. Active spokes (solid, no\n // dasharray) have caps mostly hidden by the hub center +\n // node radius. Geometry-safe; paint-only. R51 sentinel\n // strokeWidth 1.5/3 untouched (idle=1, active=2). data-\n // topo-hub-spoke-linecap attr exposes the value for tests.\n // Round 419 / Loop: hub-spoke idle opacity 0.45 → 0.50.\n // Stale-state legibility lift family 9th anchor — pairs\n // with R391 (active 0.7 → 0.8) and R415 (active sw 2 →\n // 2.25) so the same spoke path is now polished on BOTH\n // active AND idle tiers. Pre-R419 idle spokes painted\n // at α=0.45 with R46 anet-topo-spoke-flow dashed\n // animation; the dashed pulses sat at the \"background\n // chatter\" floor — visible but understated. R419\n // lifts to 0.50 so idle spokes read more confidently\n // while the active/idle contrast ratio stays clear\n // (0.8/0.50 = 1.6× vs prior 0.8/0.45 = 1.78×; still\n // a sharp two-tier distinction).\n // Stale-state legibility lift family (9 anchors now):\n // R317 subordinate-text gray-500 → gray-400\n // R358 freshness floor 0.25 → 0.30\n // R372 minimap offline-dot 0.5 → 0.6\n // R404 hub-halo cyber trough 0.08 → 0.10\n // R405 hub-halo light trough 0.32 → 0.34\n // R406 edge freshness floor 0.35 → 0.40\n // R407 node halo offline opacity (cyber + light)\n // R413 active-node pulse trough (cyber + light)\n // R419 hub-spoke idle opacity 0.45 → 0.50 (this round)\n // data-topo-hub-spoke-opacity attr (R391) updates to\n // surface the resolved per-state value.\n //\n // Round 415 / Loop: hub-spoke active strokeWidth 2 → 2.25.\n // Pairs with R391 (active opacity 0.7 → 0.8) so the same\n // active-state path lifts BOTH stroke weight AND opacity\n // in concert. Pre-R415 active strokes sat at sw=2 — clear\n // step over idle sw=1, but a touch lighter than the\n // weight family's other \"active\" indicators (R385 hub\n // hover-ring sw=1.75 / R402 legend pin-ring sw=1.75 /\n // R367 edge-badge rest sw=1.25). R415 bumps to 2.25 so\n // the active spoke reads with proportional weight to its\n // role — the line connecting the focal point to the\n // active node deserves the heaviest active stroke in the\n // family (after pin/hot edge-badge sw=2). Stays clear of\n // R51 sentinels (1.5 / 3) at 2.25.\n // Visual-weight bump family (14 anchors now):\n // R287 minimap viewport stroke 1 → 1.5\n // R295 legend swatch radius 5.5 → 6\n // R359 recent-row pip radius 1.6 → 1.8\n // R360 hub digit fontSize 11 → 12\n // R361 edge-badge digit fontSize 10 → 11\n // R365 hub-highlight radius 5 → 5.5\n // R367 edge-badge rest stroke 1 → 1.25\n // R374 pressure-bar height 1.5 → 2\n // R383 recent-row pip radius 1.8 → 2.0\n // R384 minimap online dot 1.7 → 1.9\n // R385 hub hover-ring stroke 1.5 → 1.75\n // R402 legend pin-ring stroke 1.5 → 1.75\n // R408 hub-halo radius 18 → 20\n // R415 hub-spoke active stroke 2 → 2.25 (this round)\n // R382 strokeLinecap='round' + R391 opacity 0.45/0.8 +\n // R51-safe idle sw=1 all preserved. 250ms transition\n // list already covers stroke-width — the new tier eases\n // naturally. data-topo-hub-spoke-stroke-width-active\n // attr surfaces the active value for tests.\n //\n // Round 391 / Loop: hub-spoke active opacity 0.7 → 0.8.\n // Pre-R391 active spokes (the spoke connecting the hub\n // to the currently-active alias — hovered or pinned)\n // lifted opacity from rest 0.45 to active 0.7 — a clear\n // step but slightly understated against the canvas\n // chrome. R391 lifts active to 0.8 so the \"this spoke\n // connects to your active node\" signal reads with\n // matching weight to the R370 hub hover-ring opacity\n // (0.7 → 0.8 cyber) — paired canvas signals now share\n // the same active-state alpha (0.8) so when a user\n // hovers a node, both the spoke and the hub-ring lift\n // to identical opacity. Rest 0.45 invariant preserved.\n // Theme-consistency / canvas-presence polish family\n // (6th anchor):\n // R370 hub hover-ring opacity 0.7 → 0.8 cyber\n // R371 edge-badge rest opacity 0.82 → 0.85 cyber\n // R372 minimap offline-dot opacity 0.5 → 0.6\n // R386 hub-highlight idle opacity 0.9 → 0.95\n // R387 hover-detail panel opacity 0.94 → 0.97 cyber\n // R391 hub-spoke active opacity 0.7 → 0.8 (this round)\n // Idle path (45% alpha + dashed flow animation) entirely\n // untouched — R391 is an active-state-only lift.\n // data-topo-hub-spoke-opacity attr exposes the resolved\n // value for tests. R382 strokeLinecap='round' + R51\n // sentinel-safe sw (1 idle / 2 active) preserved.\n /* Round 430 / Loop: hub-spoke opacity hover lift on\n hoveredAlias === session.alias. Adds a \"this node's\n spoke\" affordance to the node-hover gesture — in a\n dense ring layout the spokes are visually quiet\n (idle α=0.50 dashed, active α=0.80 solid) so hovering\n a node didn't telegraph which line connects to it.\n R430 lifts the matched spoke's opacity:\n idle 0.50 → 0.70 (hover-α=0.70, +0.20)\n active 0.80 → 0.95 (hover-α=0.95, +0.15)\n The +0.15-to-0.20 lift keeps the active/idle two-tier\n distinction (0.95 vs 0.70 still a clear gap) while\n making the hovered-node's spoke visibly brighter than\n every other spoke at its own activity tier. R241\n transition list already covers opacity 250ms so the\n lift eases for free. Sibling to R429 label-card body\n solidity lift — both surface a single-node-focused\n attention cue with the same easing cadence.\n Stacks with the 6-layer node hover cue stack at the\n inter-node-link scope:\n R26 group translateY -2px (per-node)\n R217 stroke tint legendAccent (per-node card)\n R142 drop-shadow boost (per-node card)\n R427 alias letter-spacing (per-node text)\n R428 sub-text letter-spacing (per-node text)\n R429 body opacity 0.94 → 1.0 (per-node card)\n R430 spoke opacity α+ (this round) (link to hub)\n data-topo-hub-spoke-hovered exposes the gate. */\n const isHoveredSpoke = !reducedMotion && hoveredAlias === session.alias;\n const spokeOpacity = isActiveSpoke\n ? (isHoveredSpoke ? 0.95 : 0.80)\n : (isHoveredSpoke ? 0.70 : 0.50);\n /* Round 435 / Loop: hub-spoke stroke-width hover lift —\n sibling to R430 opacity hover at the same surface. When\n hoveredAlias matches, BOTH opacity AND stroke-width\n lift on the matched spoke so the eye registers a\n 2-axis \"this node's spoke\" gesture (paint + geometry).\n idle 1.00 → 1.25 (Δ +0.25, +25%)\n active 2.25 → 2.50 (Δ +0.25, +11%)\n Same +0.25 absolute delta keeps the idle/active visual\n progression consistent — at rest sw ratio 2.25:1 = 2.25,\n on hover 2.50:1.25 = 2.0; both still clearly two-tier.\n R241 transition list already covers stroke-width 250ms\n so the lift eases for free.\n R51 sentinel-safe: spoke is canvas <path>, not\n data-node <circle> (the sentinel selector is gated to\n g[data-node] descendants). 1.25 and 2.5 are not in the\n reserved {1.5, 3} set so the overlap-test sentinel\n attribute selector wouldn't match either way. */\n const spokeStrokeWidth = isActiveSpoke\n ? (isHoveredSpoke ? 2.5 : 2.25)\n : (isHoveredSpoke ? 1.25 : 1);\n return (\n <path\n key={`hub-${session.alias}`}\n d={path}\n fill=\"none\"\n stroke={isActiveSpoke ? pal.spokeStroke.active : pal.spokeStroke.idle}\n strokeWidth={spokeStrokeWidth}\n strokeDasharray={isActiveSpoke ? 'none' : '6 14'}\n strokeLinecap=\"round\"\n opacity={spokeOpacity}\n className={isActiveSpoke ? undefined : 'anet-topo-spoke-flow'}\n data-topo-spoke-bucket={isActiveSpoke ? undefined : busy}\n data-topo-spoke-dur={isActiveSpoke ? undefined : spokeDur}\n data-topo-hub-spoke-active={isActiveSpoke ? 'true' : 'false'}\n data-topo-hub-spoke-hovered={isHoveredSpoke ? 'true' : 'false'}\n data-topo-hub-spoke-opacity={spokeOpacity}\n data-topo-hub-spoke-stroke-width={spokeStrokeWidth}\n data-topo-hub-spoke-stroke-width-active=\"2.25\"\n data-topo-hub-spoke-linecap=\"round\"\n style={{\n transition: 'stroke 250ms ease-out, stroke-width 250ms ease-out, opacity 250ms ease-out',\n ...(isActiveSpoke ? {} : {\n animationDelay: `${-(idx * 0.25)}s`,\n // CSS var consumed by `.anet-topo-spoke-flow`\n // (line 859 of globals.css). React's CSSProperties\n // type doesn't model custom properties → cast\n // through Record<string, string>.\n ...({ ['--spoke-dur']: `${spokeDur}s` } as Record<string, string>),\n }),\n } as React.CSSProperties}\n />\n );\n });\n })()}\n\n {/* #111: prefix-group boundary boxes (Vincent 4722). Grid layout\n only — groupBoxes is empty in ring mode. Rendered behind the\n flow links + nodes; pointer-events off so they never intercept\n a node click. Restrained dashed container + group-name label. */}\n {groupBoxes.map((box, boxIdx) => {\n const isHovered = activeGroup === box.key;\n // R68: distinguish \"locked by click\" from \"currently hovered\".\n // R63 made pinned and hovered identical (both hit isHovered\n // via activeGroup). A user with one team pinned should see at\n // a glance which is the locked one even while their cursor\n // sweeps elsewhere. isPinned reads pinnedGroup directly\n // (NOT activeGroup) so the visual is specific to the sticky\n // state — transient hover keeps the R63 isHovered styling.\n const isPinned = pinnedGroup === box.key;\n // Round 18 / Loop: group-box hover linkage. The Round 8 fade\n // already dropped OUT-of-focus groups to 0.28, but the IN-focus\n // group sat at its baseline appearance — no positive emphasis.\n // Hovering now upgrades the box to an \"accent\" treatment:\n // solid stroke (not dashed), thicker, accent-coloured; brighter\n // text and slightly stronger fill. Label and box read as one\n // selected unit. Geometry unchanged → overlap test untouched.\n // R132: per-group marching-ants duration computed once,\n // reused on the data attribute + the inline custom property.\n const w = box.statuses.working;\n const marchDur = w >= 6 ? 8 : w >= 4 ? 10 : w >= 2 ? 12 : 14;\n // Round 468 / Loop — single-tier classifier. Surfaces the\n // semantic the R319 pip-strip already encodes implicitly:\n // a cluster where every member sits in one status tier\n // renders as `name · count` only (offending duplicate pip\n // dropped). Pre-R468 that \"all members in tier X\" fact\n // was visible to the eye (no pips) but not queryable from\n // the DOM. R468 attaches the classifier as\n // `data-group-tier`:\n // 'all-working' — w===count, fleet uniformly busy\n // 'all-idle' — i===count, fleet uniformly waiting\n // 'all-offline' — o===count, fleet uniformly down\n // 'mixed' — at least 2 tiers present\n // Sibling R466/R467 pattern — expose composed state as a\n // data-attr without changing paint. Use cases: Playwright\n // assertions, external CSS hooks, accessibility enrichment.\n const groupTier =\n box.statuses.working === box.count ? 'all-working' :\n box.statuses.idle === box.count ? 'all-idle' :\n box.statuses.offline === box.count ? 'all-offline' :\n 'mixed';\n return (\n <g\n key={`grp-${box.key}`}\n data-group={box.key}\n data-group-tier={groupTier}\n // Round 173 / Loop: group boxes pick up the first-paint\n // fade-in wave alongside R9 staggered nodes (0-540ms)\n // and R172 staggered edges (280-980ms). Pre-R173 the\n // structural box frames appeared instantly while the\n // nodes inside eased in — the reveal felt like\n // 'frame slams down, nodes drift in'. The .anet-fade-in\n // CSS animation (R3 origin, 0.15s ease-out, mount-once)\n // plays alongside the node R9 stagger; each box offset\n // by boxIdx × 60ms (cap at 8 indices so a fleet with\n // many groups still finishes within ~500ms) so boxes\n // appear like a soft sweep across the grid rather than\n // a single pop. animation-fill-mode default 'none' →\n // post-animation control reverts to the inline opacity\n // style below (1 / 0.28 based on filter pin state).\n // data-group-fade-delay exposes the computed delay for\n // test probes.\n /* Round 470 / Loop — sync the R8 out-of-focus dim\n transition cadence from Tailwind's `transition-\n opacity` default (150ms ease-in-out) to 200ms\n ease-out to match the rest of the cluster's\n motion vocabulary. Hero D #147 stack established\n 200ms ease-out across every cluster axis:\n parent text (codex p.125)\n parent rect (R461 xywh + R464 rx + R248 paint)\n hitbox rect (R459 fill+opacity + R460 x+width\n + R465 rx)\n The wrapper <g>'s opacity flip (1 → 0.28 when\n another group is active) was the LAST surface\n still at 150ms — when the user hovers a group\n label, out-of-focus groups dimmed 50ms faster\n than the focused group's tint brightened, a\n small but perceivable rate-desync. R470 lifts\n the wrapper to 200ms ease-out. duration-200 +\n ease-out are Tailwind v4 utility classes; the\n anet-fade-in mount-once keyframe stays in the\n className for first-paint stagger (R173). */\n className=\"transition-opacity duration-200 ease-out anet-fade-in\"\n data-group-fade-delay={Math.min(boxIdx, 8) * 60}\n data-group-fade-transition=\"200ms\"\n // R63: drop the blanket pointerEvents:'none' that\n // previously sat here. Chrome's SVG impl doesn't let a\n // child override a parent's `none` even though the spec\n // says it should — moving the property onto just the\n // rect (where it's needed so nodes underneath stay\n // clickable) lets the label text receive its own click.\n style={{\n opacity: !activeGroup || isHovered ? 1 : 0.28,\n animationDelay: `${Math.min(boxIdx, 8) * 60}ms`,\n }}\n >\n <rect\n x={box.x}\n y={box.y}\n width={box.w}\n height={box.h}\n /* Round 464 / Loop: group-box rx 14 → 16 on isPinned.\n Geometric softening at the corner radius — locked\n groups read with subtly rounder shoulders than\n hovered/idle. +2px reads as a calm \\\"settled in\\\"\n posture (subtler than a fill or stroke bump but\n unmistakable across the whole cluster boundary).\n Pin signature on the group-box rect now spans 7\n axes:\n R63 text fill brighten\n R142 drop-shadow filter\n R432 text letter-spacing 0→0.5\n R444 count tspan fw 500→600\n R457 parent text fw 700→800\n codex p.125 text opacity 0.55→1\n R464 corner rx 14→16 (this round)\n transition list (R461) already covers x/y/width/\n height 200ms ease-out; appended `rx 200ms ease-\n out` so the rounding eases alongside the geometry\n axes. SVG2 CSS animation on rx: Chrome 95+ /\n Safari 16+ / FF 70+ (same matrix as x/y/w/h).\n data-group-box-rx exposes the resolved value. */\n rx={isPinned ? '16' : '14'}\n data-group-box-rx={isPinned ? '16' : '14'}\n fill={isLight ? '#0f172a' : '#a5b4fc'}\n // R68: 3-tier opacity + stroke ladder.\n // pinned → fill 0.08 / 0.13, stroke 3 px (locked)\n // hovered → fill 0.05 / 0.09, stroke 2 px (inspecting)\n // idle → fill 0.025 / 0.045, stroke 1.5 px dashed\n fillOpacity={isPinned ? (isLight ? 0.08 : 0.13)\n : isHovered ? (isLight ? 0.05 : 0.09)\n : (isLight ? 0.025 : 0.045)}\n stroke={(isPinned || isHovered) ? pal.legendAccent : pal.ringStroke}\n strokeWidth={isPinned ? 3 : isHovered ? 2 : 1.5}\n strokeDasharray={(isPinned || isHovered) ? 'none' : '6 6'}\n /* Round 380 / Loop: cluster box stroke gets round\n linecap + round linejoin. Sibling SVG stroke-\n softening polish to R378 flow-rail linecap + R379\n minimap viewport linejoin — extends the family to\n the group cluster boundary box (grid layout only):\n R288 chrome icons strokeLinecap='round'\n R378 flow-rail dashes strokeLinecap='round'\n R380 group box dashes strokeLinecap='round' (this round)\n R379 viewport rect strokeLinejoin='round'\n R380 group box corners strokeLinejoin='round' (this round)\n Linecap rounds the R85 '6 6' marching-ants dash\n pills at rest — each 6 px dash gains a ~0.75 px\n round cap (sw=1.5 idle), reading as soft pills\n instead of sharp 6 × 1.5 px rectangles. Linejoin\n rounds the 4 sharp 90° corners (any state — solid\n or dashed); at sw=1.5 the join arc is ~0.75 px,\n matching R379 viewport vocabulary. Geometry-safe:\n stroke-* properties only affect paint, not bbox.\n The R51 sentinel 1.5/3 strokeWidth values stay\n intact (the overlap probe is gated to g[data-\n node], so this cluster-internal rect is invisible\n to it anyway). data-group-box-linecap + -linejoin\n attrs expose the values for tests. */\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n data-group-box-pinned={isPinned ? 'true' : 'false'}\n data-group-box-linecap=\"round\"\n data-group-box-linejoin=\"round\"\n data-group-box-geom-transition=\"x,y,width,height\"\n // R85: ambient \"marching ants\" drift on the perimeter\n // when this group has at least one working member, and\n // neither pin nor hover is active (those treatments\n // already shout for attention via solid stroke). 12s\n // cycle reads as ambient — the eye parses \"live work\n // here\" without registering the box as animating.\n // R132: per-group ant rate buckets on box.statuses.working\n // — the same coupling-to-busyness idiom R84 uses for the\n // hub and R131 uses for the outer-ring orbit, applied at\n // the GROUP scale. A team with one working member ambles;\n // a team with five working members visibly accelerates.\n // Bucket boundaries (1 / 2-3 / 4-5 / 6+) chosen to land\n // on the same 14/12/10/8 cadence ladder so the three\n // motion layers (hub / ring / group) keep a coherent\n // tempo grammar. Default 12s when working=0 doesn't\n // matter — the className is only applied when working>0.\n data-group-box-live={!isPinned && !isHovered && box.statuses.working > 0 ? 'true' : 'false'}\n data-group-box-march-dur={marchDur}\n data-group-box-lifted={(isPinned || isHovered) ? 'true' : 'false'}\n className={!isPinned && !isHovered && box.statuses.working > 0 ? 'anet-topo-groupbox-live' : undefined}\n // R142: drop-shadow filter when pinned or hovered. Box\n // visually \"rises off the canvas\" — same vocabulary\n // R18 KPI cards + R135 overlay panels use. Idle group\n // boxes carry no filter (purely flat dashed outline)\n // so the unstyled canvas stays uncluttered. Filter\n // affects paint area only, not the geometric bbox\n // the overlap-test reads, so zero-overlap invariant\n // is preserved.\n filter={(isPinned || isHovered) ? 'url(#topo-groupbox-lift)' : undefined}\n style={{\n /* Round 248 / Loop: append fill 200ms ease-out to\n the existing R66 transition list. Pre-R248 the\n rect's fill (isLight ? '#0f172a' (slate-900) :\n '#a5b4fc' (indigo-300)) snapped on theme toggle\n while stroke / fill-opacity / filter all eased.\n Closes the last theme-toggle snap on the group\n box surface — same idiom R246 + R247 used at\n per-node label-card and side-panel scopes.\n Round 461 / Loop: extend the transition list to\n all 4 geometry axes (x, y, width, height) so\n when a cluster grows / shrinks (member joins,\n leaves, prefix rebalance, dense toggle, status\n flip) the BIG outer container slides into the\n new bounds at the same 200ms cadence the R460\n inner hitbox tint rect now uses. Pre-R461 the\n outer 200×140 px box snap-jumped on cluster\n resize while the inner 160×18 hitbox slid —\n jarring two-rate motion at the same surface.\n R461 unifies both rects to slide as one, with\n the parent box driving the visual envelope and\n the inner hitbox tracking the bottom-edge tint.\n Hero D #147 motion-coherence at the FULL cluster\n container tier (not just the label tint).\n data-group-box-geom-transition attr exposed. */\n transition: 'stroke 200ms ease-out, stroke-width 200ms ease-out, fill-opacity 200ms ease-out, filter 200ms ease-out, fill 200ms ease-out, x 200ms ease-out, y 200ms ease-out, width 200ms ease-out, height 200ms ease-out, rx 200ms ease-out',\n pointerEvents: 'none',\n // CSS var consumed by `.anet-topo-groupbox-live`\n // (line 877 of globals.css). React's CSSProperties\n // type doesn't model custom properties, so cast\n // through Record<string, string>.\n ...({['--march-dur']: `${marchDur}s`} as Record<string, string>),\n }}\n />\n {/* R63: wrap label in a clickable <g> with an invisible\n rect hitbox. The text alone wasn't getting hit-tested\n reliably — the SVG-wide topo-panel <rect> intercepts\n at the label's screen position when the label sits at\n a high viewBox-y (it lands below where the compositor\n expects the text to paint on top, same gotcha as the\n recent-signal panel rows in R56). Hitbox rect + the\n parent <g> taking the click + onPointerDown stop-\n propagation match the R55/R56/R61 pattern. */}\n <g\n role=\"button\"\n tabIndex={0}\n aria-pressed={pinnedGroup === box.key}\n data-group-label-hit={box.key}\n className=\"anet-topo-svg-focus\"\n style={{ pointerEvents: 'all', cursor: 'pointer' }}\n onPointerDown={(e) => e.stopPropagation()}\n onClick={() => setPinnedGroup(prev => prev === box.key ? null : box.key)}\n // R86: hover the label → transient group focus. Releases\n // on leave; activeGroup = hoveredGroup ?? pinnedGroup\n // formula already handles transient-over-pin so a\n // user can spot-compare teams without losing their\n // pinned one. Closes the same R83-style hover/click\n // gap (segments) — now group labels carry it too.\n onPointerEnter={() => setHoveredGroupLabel(box.key)}\n onPointerLeave={() => setHoveredGroupLabel(prev => prev === box.key ? null : prev)}\n // R152: a11y completeness — R63 added role + tabIndex +\n // aria-pressed but never wired onKeyDown, so the focused\n // group label was tab-reachable but Enter/Space was a\n // no-op. Closes the last keyboard gap among the\n // role=\"button\" surfaces. Other group-pin trigger paths\n // (R69 palette, R74 cmdk, R86 hover, dispatchEvent) are\n // unchanged. Matches the onKeyDown idiom from R116 /\n // R139 / R140 / R151 (Enter & Space → same setter as\n // onClick, preventDefault on Space to stop SVG scroll).\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n setPinnedGroup(prev => prev === box.key ? null : box.key);\n }\n }}\n >\n {/* R99: SVG <title> tooltip listing group members +\n status breakdown. Same info-density spirit as\n R97 pill tooltips + R98 node tooltips — anywhere\n a UI element says \"alpha · 3\" should hover-\n explain WHICH 3. Truncates at 8 aliases with a\n \"+N more\" suffix so a 20-member band doesn't\n paint a 22-line tooltip. */}\n {(() => {\n const members = Object.entries(groupKeys)\n .filter(([, key]) => key === box.key)\n .map(([alias]) => alias);\n const memberPreview = members.slice(0, 8).join(', ');\n const suffix = members.length > 8 ? ` + ${members.length - 8} more` : '';\n const statusSummary = [\n box.statuses.working > 0 ? `${box.statuses.working} working` : null,\n box.statuses.idle > 0 ? `${box.statuses.idle} idle` : null,\n box.statuses.offline > 0 ? `${box.statuses.offline} offline` : null,\n ].filter(Boolean).join(' · ');\n return (\n <title>{[\n `${box.key} (${members.length} member${members.length === 1 ? '' : 's'})`,\n statusSummary || null,\n `${memberPreview}${suffix}`,\n pinnedGroup === box.key ? 'click to release pin' : 'click to pin this group',\n ].filter(Boolean).join('\\n')}</title>\n );\n })()}\n {/* v0.11.0 #147 Hero D — Vincent 5401: \"太大太丑,\n 都放到框的右下角的小字\". First-cut bottom-right\n placement collided with bottom-row nodes (cluster\n geometry has no bottom padding; only GROUP_TOP=12\n top band). Pivoted to Option C from #147 spec:\n keep top-left anchor BUT shrink fontSize (13 → 9)\n and dim default opacity (1 → 0.55, hover/pin\n restore to 1). Satisfies \"太大太丑\" via the size +\n opacity axes while keeping the existing geometry\n contract that topo-overlap-test gates. Hitbox\n rect width tightens to min(box.w-12, 160) to\n track the narrower label render. */}\n {/* Round 465 / Loop — hitbox tint rect rx 4 → 5 on\n pinnedGroup match. Mirrors R464 (parent group-box\n rx 14 → 16 on isPinned) at the hitbox tier. The\n R460 hitbox carried fixed rx=4 since codex p.125\n pivoted it to the bottom-of-band position; the\n pin-state geometric softening was only on the BIG\n outer container, not the small hitbox underneath.\n R465 adds +1 px corner rounding on pin so the\n tint rect echoes the parent's locked posture at\n its own scale (8% relative bump matches R464's\n 14→16 ≈ 14% scaled to the smaller rect).\n Transition list (R460 fill/opacity/x/width 200ms\n ease-out) extends to include `rx 200ms ease-out`\n so the rounding eases under the same cadence.\n SVG2 CSS animation on rx: Chrome 95+ / Safari\n 16+ / FF 70+ (same matrix as x/y/w/h).\n data-group-label-tint-rx exposes the resolved\n value for tests. */}\n <rect\n x={box.x + 6}\n y={box.y + 2}\n width={Math.min(box.w - 12, 160)}\n height={18}\n rx={pinnedGroup === box.key ? '5' : '4'}\n data-group-label-tint-rx={pinnedGroup === box.key ? '5' : '4'}\n fill={pinnedGroup === box.key || hoveredGroupLabel === box.key ? pal.legendAccent : 'transparent'}\n opacity={pinnedGroup === box.key ? (isLight ? 0.16 : 0.20)\n : hoveredGroupLabel === box.key ? (isLight ? 0.09 : 0.13)\n : 1}\n data-group-label-tinted={pinnedGroup === box.key ? 'pinned' : hoveredGroupLabel === box.key ? 'hover' : 'none'}\n /* Round 459 / Loop — cadence-sync follow-on to codex\n preview.125 (Hero D #147). Codex's parent <text>\n transition list now reads:\n 'fill 200ms, letter-spacing 200ms,\n font-weight 200ms, opacity 200ms'\n — 200ms ease-out across every axis. The label\n hitbox tint rect underneath was still at 150ms\n (legacy R107 cadence), so the tint snapped in\n 50ms ahead of the parent label brightening —\n a small but perceivable mistimed cascade when\n hovering or clicking to pin a cluster. R459\n lifts both axes to 200ms to lock the tint\n under the label as one motion-coherent state\n flip. Hover/pin/unpin all feel as a single\n unified ease rather than \"tint pops, label\n follows\". data-group-label-tint-transition\n attr exposes the timing for tests. */\n /* Round 460 / Loop — extend the R459-200ms tint rect\n transition list to include `x` + `width` so the\n hitbox slides into place when a cluster grows or\n shrinks (member joins / leaves / status change\n re-pricing box.w). Pre-R460 every resize snap-\n jumped the hitbox bounds — a small but visible\n glitch right at the moment the operator's\n attention is on the cluster. SVG2 CSS animation\n on geometry attrs has shipped in Chrome 95+ /\n Safari 16+ / FF 70+; the runtime gracefully\n no-ops on older browsers. Sibling motion idiom\n to R134 / R141 / R142 (panel rect transitions)\n at the group-label hitbox tier.\n data-group-label-tint-geom-transition attr\n exposes the geometry-axis presence for tests. */\n data-group-label-tint-transition=\"200ms\"\n data-group-label-tint-geom-transition=\"x,width,rx\"\n style={{ transition: 'fill 200ms ease-out, opacity 200ms ease-out, x 200ms ease-out, width 200ms ease-out, rx 200ms ease-out' }}\n />\n {/* Round 218 / Loop: group label gains a letter-spacing\n transition on pin — the text subtly spaces out\n (0px → 0.5px) when the group is locked, giving the\n pinned state its own typographic signature distinct\n from R63's transient hover fill brighten. Hover and\n pin share the same fill colour (legendHeadline), so\n pre-R218 the only thing distinguishing them was\n R142 drop-shadow + R68 rect stroke. R218 adds a\n type-level signal: pinned text spreads slightly,\n feels \"locked in\" / \"open and held\". Letter-\n spacing is one of the few SVG text properties that\n interpolates smoothly across the major browsers.\n Hover stays at default tracking — the spread is\n pin-exclusive so users can read pinned vs\n hovered at the text alone. transition 200ms\n matches R142 fill timing so all the group-label\n state-flip channels (fill colour, rect stroke,\n rect drop-shadow, label tracking) ease as one. */}\n {/* Round 432 / Loop: extend the group-label letter-\n spacing tween from 2-tier (rest/pin) to 3-tier\n (rest/hover/pin → 0/0.25/0.5). Pre-R432 R218\n spread the text only on pin; hover got an\n R63 fill brighten (legendText → legendHeadline)\n but no typographic axis of its own. R432 adds\n the missing mid tier so hover telegraphs through\n BOTH the fill brighten AND a subtle kerning\n spread — sibling pattern to R427 node-alias\n (0/0.3/0.5) and R431 edge-badge (0/0.2/0.4) at\n group-label scope. Pin tier (0.5) still wins.\n Subtler mid tier (0.25 vs alias 0.3) because the\n group label is a structural anchor — too much\n spread would steal weight from the per-node\n alias identity it groups. Hover-letter-spacing\n family extension (8 anchors now):\n R344 chip count digit\n R345 panel title\n R347 active-links chip\n R351 vendor chip\n R420 zoom-level chip\n R427 node alias text\n R431 edge-badge digit\n R432 group label text (this round)\n R218 transition list ('fill 200ms, letter-spacing\n 200ms') untouched — additive conditional case. */}\n {/* Round 457 / Loop: group label parent text fontWeight\n 700 → 800 on isPinned. Adds typographic weight axis\n to the group-label parent text, sibling to R432\n letter-spacing tween at the same surface. Pre-R457\n pin lifted ls 0 → 0.5px (R218→R432 3-tier) but the\n fw stayed planted at R63's 700 — locked groups\n read as wider-but-same-weight. R457 adds the\n weight axis so pinned groups read as tightened\n AND wider, matching the R416/R424/R425/R426/R444/\n R445/R446 \"data tightens under attention\" idiom\n (now extended to the parent-text scope at the\n group-label tier). R63 fill brighten + R432\n letter-spacing 0/0.25/0.5 3-tier + R55 transition\n list all preserved; extends to include 'font-\n weight 200ms ease-out' so the bump eases under\n the same cadence. */}\n {/* v0.11.0 #147 Hero D — Vincent 5401 ask: \"dash 网络\n 图里面这个工程的名字也太大了, 超级丑\". Per Vincent\n screenshot 实测. Initial attempt moved label to\n bottom-right (#147 spec Option A); topo-overlap-test\n caught 7 grid collisions because cluster boxes have\n no bottom padding. Pivot to Option C: keep top-left\n anchor, shrink fontSize 13 → 9 (-31%, watermark\n register), dim default opacity 1 → 0.55 (hover/pin\n restore to 1). Net Twitter-grok improvement:\n cluster labels no longer dominate the canvas at\n rest; operator still hovers to find specific groups.\n Position unchanged to preserve the existing\n geometry that overlap-test gates. */}\n <text\n x={box.x + 12}\n y={box.y + 12}\n fill={isHovered ? pal.legendHeadline : pal.legendText}\n fontSize=\"9\"\n fontFamily=\"monospace\"\n fontWeight={isPinned ? '800' : '700'}\n opacity={isPinned || isHovered ? 1 : 0.55}\n data-group-label-hovered={isHovered && !isPinned ? 'true' : 'false'}\n data-group-label-font-weight={isPinned ? '800' : '700'}\n /* Round 479 / Loop — extend drop-shadow visual-polish\n family to a 4th anchor: group-label parent text\n on isPinned. Continues the R476/R477/R478 arc:\n R476 hub digit hover-gated emerald\n R477 legend pin-ring pin-gated row.fill\n R478 recent-row pip freshness-gated cyan\n R479 group-label text pin-gated cyan\n Hue: pal.legendAccent at 0x80 alpha (≈50%) — same\n accent family R107/R477 use for tint surfaces. 3px\n blur reads as a soft cyan halo around the locked\n cluster name. Stacks with the R432 letter-spacing\n spread + R457 fw lift + R63 fill brighten + R142\n drop-shadow on the parent rect — pin signature on\n group label scope now spans typography + chroma +\n paint + container-lift + text-glow.\n Filter is paint-only; bbox unchanged; overlap-test\n invariants hold (R51 selector gated to g[data-node]\n descendants, this label is invisible to the probe).\n transition list extends to include 'filter 200ms\n ease-out' alongside the existing fill/ls/fw/opacity\n 200ms tweens. */\n data-group-label-glow={isPinned ? 'true' : 'false'}\n style={{\n transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out, font-weight 200ms ease-out, opacity 200ms ease-out, filter 200ms ease-out',\n letterSpacing: isPinned ? '0.5px' :\n isHovered ? '0.25px' : '0px',\n filter: isPinned\n ? `drop-shadow(0 0 3px ${pal.legendAccent}80)`\n : undefined,\n }}\n data-group-label={box.key}\n data-group-label-pinned={isPinned ? 'true' : 'false'}\n >\n {box.key}\n {/* Round 19 / Loop: member-count chip. Inline tspan stays\n in the single <text> bbox the overlap test reads, so\n the node↔label guard still catches if the chip ever\n pushes the label far enough right to clip a node.\n Smaller + lighter weight reads as metadata, not name. */}\n {/* Round 229 / Loop: member-count chip drops its explicit\n fill so it inherits from the parent <text>, which means\n R142's hover-fill transition (legendText → legend-\n Headline, 200ms ease-out) NOW carries the count chip\n with it. Pre-R229 the parent name brightened on\n hover while the count tspan stayed at legendText —\n \"name lit, count dimmer than at rest\" inverted the\n tonal hierarchy. Inheriting matches the name's\n transition; both rest and hover keep the SAME\n tonal relationship between name and count.\n 7th surface in the hover-deepen-own-hue family\n (legend rows, chip-row counts, status pip, recent\n row text, pressure-bar segments, group-box fill +\n this round's group-label-count chip).\n\n Also picks up tabular-nums (5th surface in the\n info-density tabular-nums sweep after R224 edge\n badge / R225 hub digit / R225 panel header /\n R225 recent row count). The member count rolls\n over often (4→5→…→9→10 as a group grows) and\n lives at a fixed dx=6 offset from the name, so a\n digit-width jitter at 9→10 used to shift the\n whole count visibly. Tabular locks it. */}\n {/* Round 366 / Loop: group label member-count tspan\n fontWeight 400 → 500. Sibling polish to R363\n recent-row alias text fw 400 → 500 + R364 legend-\n row label fw 400 → 500 — closes the per-row 'count\n is fw 500 against label-tier fw 700' pattern at\n the group-label scope (grid layout cluster mark).\n Hierarchy snapshot post-R366 across all 3 row\n surfaces:\n recent count(hot/cold) fw 700/600 (R320)\n recent alias fw 500 (R363)\n legend count fw 600 (R309)\n legend label fw 500 (R364)\n group name fw 700 (legacy)\n group count fw 500 (R366, this round)\n Monospace family + R225 tabular-nums lock digit\n width, so the fw bump is paint-only — bbox\n unchanged + overlap-test invariants hold. R229\n fill-inherit from parent label (hover-deepen-own-\n hue family) preserved. data-group-label-count-\n font-weight attr exposes the value for tests. */}\n {/* Round 444 / Loop: group label count tspan\n fontWeight 500 → 600 on isPinned. Extends the\n \"data tightens under attention\" typographic-\n weight pattern to a 5th anchor at the group-\n label-count scope:\n R416 chip-digit (chip hover)\n R424 panel-digit (panel hover)\n R425 hub-digit (hub hover)\n R426 edge-badge-digit (pin/hot)\n R444 group-label-count (pinned) ← this round\n Same idiom — when the group is locked, its\n member-count tightens typographically alongside\n the R432 letter-spacing spread (0 → 0.5px) on\n the parent label. Hover keeps rest fw (500) so\n the locked vs preview distinction at the type\n level stays intact — same gate R432 used.\n Monospace + R225 tabular-nums lock the digit\n width across fw changes; bbox unchanged; overlap-\n test invariants hold. transition list adds\n 'font-weight 200ms ease-out' matching R432\n letter-spacing cadence. R229 fill-inherit\n preserved (parent text fill still drives the\n hover/pin color). data-group-label-count-font-\n weight + -pinned attrs exposed for tests. */}\n {/* v0.11.0 #147 — count tspan tracks parent fontSize:\n 11 → 8 to match the new 9px label scale (parent\n dropped 13 → 9 with same -2px gap to the count\n suffix). dx=\"4\" replaces dx=\"6\" — the smaller\n glyph baseline doesn't need the wider gutter. */}\n <tspan\n dx=\"4\"\n fontSize=\"8\"\n fontWeight={isPinned ? '600' : '500'}\n data-group-label-count={box.key}\n data-group-label-count-value={box.count}\n data-group-label-count-pinned={isPinned ? 'true' : 'false'}\n data-group-label-count-font-weight={isPinned ? '600' : '500'}\n style={{\n fontVariantNumeric: 'tabular-nums',\n transition: 'font-weight 200ms ease-out',\n }}\n >· {box.count}</tspan>\n {/* Round 58 / Loop: status mix pip strip. Compact text-\n based chips (e.g. \"2w 1i\") so the strip stays inside\n the same <text> bbox the overlap-test reads — keeps\n the R27 label↔label and R19 node↔label guards intact.\n Each tier is colour-coded against the legend swatches\n and only renders when count > 0, so a healthy all-\n working group reads simply \" · 2w\".\n\n Round 207 / Loop: each tspan eases in on mount\n via anet-fade-in. Pre-R207 when a group's first\n working node went idle (or first idle node went\n working), the new tier's tspan snap-popped into\n the label. Same snap-on-mount issue R203 fixed\n for recent-signal rows, applied at the group-\n label scope. Each tier is keyed on its boolean\n mount, so the animation fires once when the\n tspan first appears (count crosses 0 → 1+),\n not on every count update (e.g., 1 → 2 working\n preserves the tspan via React reconciliation).\n Exit remains snap — matches R190's \"fade-IN\n smooth, accept exit snap\" trade-off used for\n the R129 hot-tail. */}\n {/* Round 230 / Loop: tabular-nums on the 3 status pips\n so the count digit doesn't jitter the adjacent\n pip when a tier crosses 9 → 10. The pips render\n in sequence at dx=8/4/4 — width-shift on any\n tier propagates rightward through the strip,\n visibly compressing or stretching the gap\n between adjacent tier chips. Tabular locks the\n digit so the strip stays stable as tiers grow.\n 6th surface in the info-density tabular-nums\n sweep after R224 edge badge / R225 hub digit /\n R225 panel header / R225 recent row count /\n R229 group-label count. Tier-specific fill\n colours stay (semantic — working green /\n idle teal / offline slate). */}\n {/* Round 253 / Loop: append fill 200ms ease-out to\n each tspan's style so theme toggle eases the\n tier-coloured pips alongside every other\n theme-driven element. R230's tabular-nums\n stays. */}\n {/* Round 319 / Loop: drop a tier pip when its count\n equals box.count — i.e. single-tier groups (all\n working, all idle, all offline). Pre-R319 a 4-all-\n idle group rendered as `P站 · 4 4i` with the \"4\"\n visually doubled; Vincent telegram 5304 flagged\n this as 比较难看 in a real-data screenshot\n (ai-insight · 6 6i, blueleap · 3 3i, P站 · 4 4i).\n The dropped pip's information is already conveyed\n by the group-box stroke colour (R68 isPinned/\n hover accent uses the dominant-tier hue) plus\n the SVG <title> tooltip listing the status\n breakdown. Multi-tier groups (e.g. `alpha · 3\n 2w 1i`) render unchanged — those pips genuinely\n add breakdown info that the total doesn't carry. */}\n {/* Round 458 / Loop — Hero D #147 finishing polish on top of\n N站牛/codex preview.125 (Option C: top-left label fontSize\n 13→9 + opacity 0.55 rest / 1 hover+pin, count tspan 11→8).\n That ship left the 3 status pips at fontSize=11 — visibly\n DOMINATING the now-9px parent label they trail. Result on\n a 5-member cluster: `alpha · 5 3w 2i` renders inside-out\n as \"tiny name + tiny count + BIG bright pips\" rather than\n a coherent right-tail of metadata. R458 scales the 3 pips\n to fontSize=8 (matches count tspan) and tightens dx 8/4/4\n → 6/3/3 (gutter ratio 0.73/0.36 glyph-widths @ 11px ≈\n 0.75/0.38 glyph-widths @ 8px — same visual rhythm at the\n smaller scale). The whole group-label bottom-right strip\n now reads as a unified 9/8/8/8 typographic ladder:\n name (parent <text>) fontSize 9 fw 700/800\n · count (1st tspan) fontSize 8 fw 500/600\n Nw (2nd tspan) fontSize 8 fw 600\n Ni (3rd tspan) fontSize 8 fw 600\n No (4th tspan) fontSize 8 fw 600\n Closes Vincent /goal 5401 (\"太大太丑\") at the pip-strip\n tier; with codex preview.125 the spec is fully realized.\n Geometry-only attribute changes — bbox tightens slightly\n (8px chars vs 11px chars stay inside the original 240px\n hitbox max) so topo-overlap-test invariants hold.\n tabular-nums + anet-fade-in + theme-eased fill 200ms\n preserved on every tspan. */}\n {box.statuses.working > 0 && box.statuses.working !== box.count && (\n <tspan\n dx=\"6\"\n fill={isLight ? '#059669' : '#22c55e'}\n fontSize=\"8\"\n fontWeight=\"600\"\n className=\"anet-fade-in\"\n data-group-pip=\"working\"\n style={{ fontVariantNumeric: 'tabular-nums', transition: 'fill 200ms ease-out' }}\n >{box.statuses.working}w</tspan>\n )}\n {box.statuses.idle > 0 && box.statuses.idle !== box.count && (\n <tspan\n dx=\"3\"\n fill={isLight ? '#0d9488' : '#2dd4bf'}\n fontSize=\"8\"\n fontWeight=\"600\"\n className=\"anet-fade-in\"\n data-group-pip=\"idle\"\n style={{ fontVariantNumeric: 'tabular-nums', transition: 'fill 200ms ease-out' }}\n >{box.statuses.idle}i</tspan>\n )}\n {box.statuses.offline > 0 && box.statuses.offline !== box.count && (\n <tspan\n dx=\"3\"\n fill={isLight ? '#94a3b8' : '#6b7280'}\n fontSize=\"8\"\n fontWeight=\"600\"\n className=\"anet-fade-in\"\n data-group-pip=\"offline\"\n style={{ fontVariantNumeric: 'tabular-nums', transition: 'fill 200ms ease-out' }}\n >{box.statuses.offline}o</tspan>\n )}\n </text>\n </g>\n </g>\n );\n })}\n\n {/* directed message flows */}\n {flowLinks.map((link, index) => {\n const from = nodePositions[link.from];\n const to = nodePositions[link.to];\n if (!from || !to) return null;\n\n // Round 7 / Loop: lift now scales with distance so short links\n // aren't over-bent (long links keep the ~36px hump), and the\n // particle period shortens with link.count so busier edges\n // visibly pulse faster — instant info density on top of the\n // existing stroke-width-by-count chip.\n const dist = Math.hypot(to.x - from.x, to.y - from.y);\n const lift = (index % 2 === 0 ? 1 : -1) * Math.min(36, dist * 0.18);\n const path = curvePath(from, to, lift);\n const width = Math.min(2 + link.count, 7);\n const duration = Math.max(0.9, 2.6 / Math.sqrt(link.count));\n // Round 231 / Loop: per-edge phase stagger lifted into a\n // named constant so the R75 arrival ping + R76 dispatch\n // pulse SMIL animates can RE-COUPLE to the R103 particle's\n // cycle. Pre-R103 (when particle started at phase 0) the\n // ping fired at \"near end of cycle\" (-0.92*dur) and dispatch\n // at \"cycle start\" (0) — both phase-coincident with particle\n // arrival/departure respectively. R103's golden-ratio\n // stagger broke that coupling — particle now started at\n // phase (index*0.37)%dur while ping+pulse stayed at fixed\n // offsets, so the rings fired at random moments relative\n // to particle position. R231 expresses dispatch_begin and\n // arrival_begin in terms of THIS stagger so they fire\n // exactly when the particle is at source / near destination\n // respectively — restoring R75/R76's original semantic\n // and unifying the three SMIL elements into one\n // synchronised per-edge animation set.\n const stagger = (index * 0.37) % duration;\n // Round 10 / Loop: freshness fade. An edge that fired ≤30s ago\n // stays at full intensity; over 5 minutes it decays to a\n // floor. Surfaces \"what's happening now\" vs background\n // chatter without hiding old flow entirely (some context\n // still useful). `now` captured at useMemo-recompute time\n // (every 5s message refresh) — accuracy is within the poll\n // interval, plenty.\n //\n // Round 406 / Loop: edge freshness fade floor 0.35 → 0.40.\n // Stale-state legibility lift family (6th anchor) — pre-\n // R406 edges older than 5 minutes faded to α=0.35 (a 65 %\n // dim against full intensity). The decay rate is the same\n // 1 - ageMs/300s curve; only the FLOOR shifts. Sibling\n // treatment to:\n // R317 subordinate-text gray-500 → gray-400\n // R358 freshness ramp floor 0.25 → 0.30\n // R372 minimap offline-dot opacity 0.5 → 0.6\n // R404 hub-halo cyber trough 0.08 → 0.10\n // R405 hub-halo light trough 0.32 → 0.34\n // R406 edge freshness floor 0.35 → 0.40 (this round)\n // Edges past 5min now sit at 40% intensity instead of 35%\n // — they still recede against fresh edges but read\n // legibly enough to convey \"this conversation existed\".\n // ageMs threshold for the 5-minute decay unchanged; the\n // decay curve shape (linear) unchanged. The visual delta\n // is most pronounced on edges between 5-60 minutes old —\n // where the floor was binding pre-R406.\n const ageMs = link.last_at ? Math.max(0, Date.now() - Date.parse(link.last_at)) : 0;\n const fresh = Math.max(0.40, 1 - ageMs / (5 * 60 * 1000));\n // Round 16 arrow-tier binning — keep `topo-arrow` as the\n // medium tier id so the legend swatch picks it up unchanged.\n const arrowId = link.count <= 2 ? 'topo-arrow-s'\n : link.count <= 4 ? 'topo-arrow'\n : 'topo-arrow-l';\n\n // Round 39 / Loop: edge hover tooltip — surface the same\n // last_at + count info the freshness fade and arrow tier\n // already encode visually, in plain text. The stroke is the\n // hover target; SVG `<title>` honours newlines on every\n // browser the dashboard targets.\n const lastAt = relativeAgo(link.last_at);\n const tooltip = `${link.from} → ${link.to}\\n${link.count} message${link.count === 1 ? '' : 's'}${lastAt ? ` · last ${lastAt}` : ''}`;\n // Round 40 / Loop: edges follow node hover — when an alias is\n // hovered, every edge touching it brightens, the rest fade.\n // Pairs with the Round 8 group-focus fade on nodes: hover a\n // node to find \"who is this agent talking to\" at a glance.\n // No hover → multiplier is 1.0 (current behaviour preserved).\n // Round 50 / Loop: edge-on-self priority. When the user hovers\n // a flow edge directly (R48 widened the hitbox so this is now\n // a precise gesture), THAT edge gets the strongest boost (2.0)\n // and every other edge dims to 0.35. Node-hover (R40) keeps\n // its own ladder. Edge-hover and node-hover are mutually\n // exclusive in practice — the cursor is over one or the\n // other — but the order below makes edge-hover win if both\n // ever read truthy at the same React tick.\n // Round 53 / Loop: in-group edges follow team focus. R40\n // brightened only edges touching the exact hovered alias —\n // but with R8/R18 prefix-clustering, a user hovering one\n // member is asking about the team. So when BOTH endpoints of\n // an edge share the hoveredGroup (and neither is the exact\n // hovered alias — that gets the stronger 1.7×), the edge\n // boosts to 1.3×. Edges leaving the team (one endpoint in,\n // one out) still dim to 0.35× per R40 since they read as\n // background to the focus. Singletons fall through unchanged\n // (their group key is the alias itself, so bothInHoveredGroup\n // is impossible for a non-self-edge).\n // R116: composes hover ?? pin — a pinned edge stays \"hot\" after the cursor leaves.\n const isHoveredEdge = activeEdgeKey === link.key;\n const fromGroup = groupKeys[link.from] ?? link.from;\n const toGroup = groupKeys[link.to] ?? link.to;\n const bothInHoveredGroup = !!activeGroup && fromGroup === activeGroup && toGroup === activeGroup;\n // R63: also gate \"no filter\" on activeGroup so pinnedGroup\n // alone activates the in-group 1.3× boost + non-group dim.\n // When neither a node nor a group is in focus, mul is 1.\n // R77: when the user hovers the \"N active links\" chip, the\n // baseline 1× becomes 1.5× — every flow brightens at once.\n // Sits inside the no-other-hover branch so it doesn't fight\n // the edge-hover (2.0×) or node-hover (1.7×) priorities.\n const edgeOpacityMul = isHoveredEdge\n ? 2.0\n : activeEdgeKey\n ? 0.35\n : !hoveredAlias && !activeGroup\n ? (hoveredActiveLinks ? 1.5 : 1)\n : (link.from === hoveredAlias || link.to === hoveredAlias)\n ? 1.7\n : bothInHoveredGroup\n ? 1.3\n : 0.35;\n // Round 50: the hovered edge also visibly thickens so the eye\n // tracks it even at low message counts (width was 3 → 4.5 on\n // hover). 1.4× is enough to read as \"lifted\" without\n // breaching the 16-px hitbox bound.\n // Round 436 / Loop: extend the thickening to the\n // \"hovered-endpoint\" case — when hoveredAlias matches one\n // of this edge's endpoints, lift width by 1.15× (capped at\n // 8 px to stay clear of the 16-px hitbox). Pre-R436 the\n // edgeOpacityMul=1.7 (line 4703) lifted the matched edge's\n // OPACITY when an endpoint was hovered but the stroke-WIDTH\n // stayed at base — so the edge faded brighter without\n // thickening, leaving the paint+geometry axes mismatched.\n // Mirror of R430/R435 hub-spoke pattern (opacity + stroke-\n // width co-lift on hoveredAlias); R436 brings the same\n // dual-axis \"this node's link\" gesture to the edge scope.\n // 1.15× is subtler than isHoveredEdge=1.4× because\n // endpoint-hover lifts MANY edges at once (every edge\n // incident on the hovered node) while edge-hover lifts ONE\n // — the gesture should read as \"highlighted\" not \"loud\".\n // R166 stroke-width 300ms transition already in the\n // visible-path style list so the lift eases for free.\n const isEndpointHoveredEdge = !!hoveredAlias && (link.from === hoveredAlias || link.to === hoveredAlias);\n const renderWidth = isHoveredEdge ? Math.min(width * 1.4, 10)\n : isEndpointHoveredEdge ? Math.min(width * 1.15, 8)\n : width;\n return (\n <g\n key={link.key}\n // Round 172 / Loop: edges fade-in alongside the R9\n // staggered node reveal so the canvas first-paint\n // reads as one coordinated wave instead of \"edges\n // pop, nodes ease in\". The .anet-fade-in CSS\n // animation (0.15s ease-out, runs once on mount,\n // R3 origin) plays only on first render — React\n // preserves the <g> via the link.key on subsequent\n // re-renders so the animation doesn't replay when\n // flowLinks recomputes (every 5s SSE poll).\n // Animation-delay offsets each edge by 280ms +\n // 35ms × index so edges start fading in AFTER\n // most nodes have appeared (node R9 stagger caps\n // at ~600ms; ring layout R72 emanates 0→540ms).\n // Cap at 20 indices so a busy fleet with 50\n // flowLinks still finishes within ~1s. Respects\n // prefers-reduced-motion via R29 globals.css\n // blanket that neutralises animation-duration.\n className=\"anet-fade-in\"\n style={{\n animationDelay: `${280 + Math.min(index, 20) * 35}ms`,\n }}\n data-edge-group={link.key}\n >\n {/* Round 48 / Loop: invisible hover hitbox — visible flow\n path is 3-7 px wide and damn hard to hover precisely.\n Stack a transparent 16-px-wide stroke behind it so the\n cursor only needs to be ~8 px from the line for the\n tooltip to fire. Native <title> moves here; the\n visible path no longer needs it. pointer-events on\n the visible path drop to \"none\" since the hitbox\n owns the hover surface. */}\n <path\n d={path}\n fill=\"none\"\n stroke=\"transparent\"\n strokeWidth={Math.max(width + 10, 16)}\n style={{ pointerEvents: 'stroke' }}\n data-edge-hitbox\n onMouseEnter={() => setHoveredEdgeKey(link.key)}\n onMouseLeave={() => setHoveredEdgeKey(prev => prev === link.key ? null : prev)}\n >\n <title>{tooltip}</title>\n </path>\n {/* Round 166 / Loop: stroke-width transition pairs\n with R164 edge badge r-lift. Pre-R166 the\n visible flow path's hover thickening (R50:\n renderWidth = isHoveredEdge ? width * 1.4 :\n width) snapped instantly even though opacity\n transitioned smoothly. Edge hover now lifts\n the line AND the badge in coordinated 300ms\n ease-out motion. Drop the Tailwind transition\n class for inline style so both opacity and\n stroke-width pick up the same timing without\n arbitrary-property class compilation risk.\n data-edge-visible exposes the path for test\n probes (the R48 hitbox sibling already has\n data-edge-hitbox). Respects prefers-reduced-\n motion via the R29 globals.css blanket\n override that neutralises transition-duration\n universally. */}\n {/* Round 245 / Loop: edge surface picks up stroke\n color transition for theme-toggle smoothing.\n R166 already eased opacity + stroke-width on the\n visible flow path; the stroke COLOR (pal.flowEdge:\n cyber cyan ↔ light emerald) and the underlying\n flow-rail's stroke (pal.flowPath: cyber pale-sky\n ↔ light slate-600) still snapped on theme switch.\n The rest of the topology smooths theme through R4\n transitions (status rings) / R242 chat-target ring\n / R244 halos / R241 hub spokes / R240 backdrop\n spokes — R245 closes the edge surface.\n\n Visible flow path: append 'stroke 300ms ease-out'\n to the existing transition list (300ms matches\n R166 opacity + stroke-width pace).\n\n Flow rail (dashed underline): convert the Tailwind\n `transition-opacity` className to inline style so\n we can list opacity AND stroke together at 300ms\n ease-out (same idiom R201 used on the working/\n online chips to splice in additional properties\n beside Tailwind's). data-edge-flow-rail attr\n surfaces the path for test introspection. */}\n {/* Round 381 / Loop: edge visible flow path picks up\n strokeLinecap='round'. Sibling polish to R378\n flow-rail dashed linecap — both flow-element paths\n (visible primary + dashed secondary rail) now share\n 'round' linecap vocabulary. The visible path runs\n source-node → dest-node as one continuous line, so\n the dest-end is covered by the markerEnd arrow and\n the source-end usually sits inside the source-node\n circle. At certain alignments (post-zoom, post-\n layout-switch transitions), the source-end may peek\n out by a fraction of a px past the node edge —\n round caps render that overshoot as a smooth half-\n disc instead of a sharp rectangle. Pure paint\n refinement, geometry-safe (bbox of the stroke\n unchanged at the join with the arrow marker).\n data-edge-visible-linecap attr exposes the value\n for tests. */}\n <path\n d={path}\n fill=\"none\"\n stroke={pal.flowEdge}\n strokeWidth={renderWidth}\n strokeLinecap=\"round\"\n opacity={Math.min(1, (isLight ? 0.22 : 0.28) * fresh * edgeOpacityMul)}\n filter={isLight ? undefined : 'url(#topo-glow)'}\n markerEnd={`url(#${arrowId})`}\n data-edge-visible={link.key}\n data-edge-visible-linecap=\"round\"\n data-edge-visible-endpoint-hovered={isEndpointHoveredEdge ? 'true' : 'false'}\n data-edge-visible-stroke-width={renderWidth}\n style={{\n pointerEvents: 'none',\n transition: 'opacity 300ms ease-out, stroke-width 300ms ease-out, stroke 300ms ease-out',\n }}\n />\n {/* Round 378 / Loop: edge flow-path dashed-rail picks\n up strokeLinecap='round'. Pre-R378 the rail\n rendered '2 12' dashes as sharp 1×2 rectangles\n against the canvas backdrop; default 'butt' caps\n leave dash ends square. R378 rounds each cap so\n the dashes read as soft 3-px pills (1 px stroke +\n 0.5 px round cap each end). The flow-rail is the\n secondary 'invisible-spine' line that gives the\n R57 spoke flow a directional rail to slide along\n — rounding the dashes softens its presence\n against the primary visible flow path (R245 has\n no strokeLinecap so it inherits 'butt' on a\n continuous line, irrelevant). Geometry-safe:\n round caps only widen the visible dash; the\n bbox of the path is unchanged so overlap-test\n invariants hold. data-edge-flow-rail-linecap\n attr exposes the value for tests. */}\n <path\n id={`flow-path-${index}`}\n d={path}\n fill=\"none\"\n stroke={pal.flowPath}\n /* Round 437 / Loop: flow-rail strokeWidth hover lift —\n 1 → 1.5 on (isHoveredEdge || isEndpointHoveredEdge).\n Pre-R437 the dashed rail sat at sw=1 always while the\n visible flow path above it lifted (R50 ×1.4 on\n isHoveredEdge / R436 ×1.15 on isEndpointHoveredEdge).\n The two edge paint layers were mismatched on hover:\n top layer thickened, underline stayed thin — so the\n hover gesture lifted only half the edge surface.\n R437 lifts the underline too so the whole edge\n reads as \"raised\" on hover, not just its bright\n top stripe. Same +0.5 absolute delta R435 used at\n hub-spoke scope (1→1.25 there, slightly bigger\n here because the rail's base 1 is at the kerning\n floor and needs more lift to register).\n Transition list extends to include stroke-width\n 300ms so the new lift eases under the same R166\n cadence as the visible path's stroke-width. */\n strokeWidth={(isHoveredEdge || isEndpointHoveredEdge) ? 1.5 : 1}\n strokeDasharray=\"2 12\"\n strokeLinecap=\"round\"\n opacity={Math.min(1, (isLight ? 0.4 : 0.75) * fresh * edgeOpacityMul)}\n data-edge-flow-rail={link.key}\n data-edge-flow-rail-linecap=\"round\"\n data-edge-flow-rail-stroke-width={(isHoveredEdge || isEndpointHoveredEdge) ? 1.5 : 1}\n data-edge-flow-rail-lifted={(isHoveredEdge || isEndpointHoveredEdge) ? 'true' : 'false'}\n style={{ transition: 'opacity 300ms ease-out, stroke 300ms ease-out, stroke-width 300ms ease-out' }}\n />\n {!reducedMotion && (\n /* Round 103 / Loop: phase-stagger the particles so\n concurrent edges don't pulse in lockstep. SMIL\n `begin` accepts negative offsets to shift the cycle\n backwards in time, which means the particle starts\n mid-flight on first paint — no visible \"all\n particles spawn from source simultaneously\" tell.\n `(index * 0.37) % duration` gives a deterministic,\n well-distributed offset (the golden-ratio-ish 0.37\n fraction prevents lining up when N is a small\n multiple). Edge order is stable (sorted by recent\n activity), so the offsets feel calm rather than\n reshuffling each refresh. */\n /* Round 422 / Loop: edge flow particle radius 4 → 4.5.\n Visual-weight bump family (15th anchor) — particles\n riding along the edge animateMotion path get +0.5px\n radius lift, increasing visual area by ~27%\n (π·4.5² / π·4² = 1.27). Sibling magnitude to R383\n recent-row pip 1.8 → 2.0 (+25% area), R384 minimap\n online dot 1.7 → 1.9 (+25% area). R251 fill +\n R252 transitions + R103 phase-stagger animateMotion\n all preserved. data-edge-particle-radius attr\n exposes the value for tests. */\n <circle\n /* Round 439 / Loop: edge flow particle radius hover\n lift — r 4.5 → 5.5 on (isHoveredEdge ||\n isEndpointHoveredEdge). Continues edge paint-\n layer parity arc (R436 visible path sw / R437\n flow-rail sw / R439 particle r) so the whole\n edge surface — including the moving particle —\n lifts on hover, not just the static stripes.\n +1px radius gives ~50% area boost. Subtler than\n 1.4× sw bump on visible path because the\n particle is already small + motion-bright;\n +1px reads as \"the dot caught attention\"\n without overshadowing the path lift. R252\n transition list extends to include r 200ms so\n the size change eases under the same fill/\n opacity cadence. */\n r={(isHoveredEdge || isEndpointHoveredEdge) ? 5.5 : 4.5}\n fill={pal.flowParticle}\n filter={isLight ? undefined : 'url(#topo-glow)'}\n /* Round 485 / Loop — extends R484's \"inspection\n overrides encoding\" pattern to a 2nd anchor:\n edge particle opacity lifts to 1.0 on\n isHoveredEdge OR isEndpointHoveredEdge (user\n hovering the edge directly OR hovering one\n of its endpoint nodes). Pre-R485 the particle\n inherited freshness × edgeOpacityMul decay\n so a stale edge's particle painted near the\n 0.30 floor even when the operator was\n inspecting it; R485 lifts to 1.0 on attention.\n data-recent-row-ts-alpha-attribute analog —\n freshness encoding preserved on rest tier,\n opacity override engages only on inspection.\n Sibling lift family — inspection-overrides-\n encoding pattern, now 2 anchors:\n R484 recent-row timestamp freshness → 1.0\n R485 edge particle freshness → 1.0 (this)\n data-edge-particle-opacity-lifted attr exposes\n the override gate; data-edge-particle-opacity-\n rest preserves the freshness reading. */\n opacity={(isHoveredEdge || isEndpointHoveredEdge) ? 1 : Math.min(1, fresh * edgeOpacityMul)}\n data-edge-particle={link.key}\n data-edge-particle-radius={(isHoveredEdge || isEndpointHoveredEdge) ? 5.5 : 4.5}\n data-edge-particle-lifted={(isHoveredEdge || isEndpointHoveredEdge) ? 'true' : 'false'}\n data-edge-particle-opacity-rest={Math.min(1, fresh * edgeOpacityMul).toFixed(2)}\n data-edge-particle-opacity-lifted={(isHoveredEdge || isEndpointHoveredEdge) ? 'true' : 'false'}\n style={{ transition: 'fill 200ms ease-out, opacity 200ms ease-out, r 200ms ease-out' }}\n >\n <animateMotion\n dur={`${duration}s`}\n begin={`-${stagger.toFixed(3)}s`}\n repeatCount=\"indefinite\"\n path={path}\n />\n </circle>\n )}\n {/* Round 75 / Loop: arrival ping at the destination. The\n particle currently fades into the arrow marker\n silently — adding a small radiating ring synchronised\n to the particle's period turns message delivery into\n a visible event. begin = -dur*0.92 offsets the\n animation so the ring expands NEAR the end of each\n cycle (≈when the particle arrives). Gated by\n reducedMotion and on fresh > 0.5 — stale edges that\n haven't fired in minutes don't need the eye-grab.\n data-arrival-ping for testability.\n\n Round 279 / Loop: arrival ping RETIRED (R75 + R228\n + R231 + R252 family) per 减法 cut #5. Per active\n edge the SMIL family was: particle (R50\n animateMotion) + arrival ping (R75 r+opacity SMIL)\n + dispatch pulse (R76 r+opacity SMIL). For a 5-\n edge fleet that's 5×3 = 15 simultaneous SMIL.\n The PARTICLE (a moving dot along the path) is the\n primary \"data flowing from A → B\" visual signal;\n ping + pulse are secondary \"arrival/dispatch\n confirmation\" that the moving particle already\n conveys. Cull ping + pulse, keep particle.\n `false &&` gates the render; code preserved for\n rollback. */}\n {false && !reducedMotion && fresh > 0.5 && (\n <circle\n cx={to.x}\n cy={to.y}\n r=\"0\"\n fill=\"none\"\n stroke={pal.flowEdge}\n strokeWidth=\"1.5\"\n opacity=\"0\"\n /* Round 252 / Loop: stroke transition for theme\n toggle. SMIL animates r + opacity continuously;\n stroke is static per render but theme-driven\n (pal.flowEdge: cyber cyan ↔ light emerald). */\n style={{ pointerEvents: 'none', transition: 'stroke 200ms ease-out' }}\n data-arrival-ping={link.key}\n >\n {/* Round 228 / Loop: pulse-pop ease curves on the\n arrival ping SMIL — extends R227's keySplines\n adoption from the click ripple (one-shot, two-\n value linear) to the canvas's repeating delivery-\n confirmation surfaces. Both <animate>s use\n calcMode=spline with keyTimes='0;0.5;1' (two\n segments).\n • r grows 0→14→22 (monotonic): unified ease-out\n across both segments — the ring decelerates\n as it expands, settling at its widest.\n • opacity bumps 0→0.55→0 (pulse): ease-out on\n the rise (fast appearance), ease-in on the\n fall (slow start of fade then accelerates) —\n the canonical \"pulse-pop\" kinetic shape.\n Together r decelerating and opacity pulse-popping\n give the arrival ping a real-event physicality\n instead of the prior linear-velocity tick. */}\n <animate\n attributeName=\"r\"\n values=\"0;14;22\"\n dur={`${duration}s`}\n begin={`-${((stagger + duration * 0.92) % duration).toFixed(3)}s`}\n repeatCount=\"indefinite\"\n calcMode=\"spline\"\n keyTimes=\"0;0.5;1\"\n keySplines=\"0.25 0.1 0.25 1;0.25 0.1 0.25 1\"\n />\n <animate\n attributeName=\"opacity\"\n values=\"0;0.55;0\"\n dur={`${duration}s`}\n begin={`-${((stagger + duration * 0.92) % duration).toFixed(3)}s`}\n repeatCount=\"indefinite\"\n calcMode=\"spline\"\n keyTimes=\"0;0.5;1\"\n keySplines=\"0.25 0.1 0.25 1;0.42 0 1 1\"\n />\n </circle>\n )}\n {/* Round 76 / Loop: source dispatch pulse — mirror to the\n R75 arrival ping. begin = 0s (start of cycle) so the\n ring expands as the particle LEAVES the source. Only\n fires for high-traffic edges (link.count >= 3) — on\n quiet conversations the canvas should stay calm; on\n busy senders the pulse plus arrival ping bookend\n every message in flight, making the topology feel\n alive. Same fresh/reducedMotion gates as R75. Slightly\n smaller radius (0→12→18 vs R75's 0→14→22) so the\n source reads as \"smaller event than arrival\" — the\n destination is the meaningful endpoint.\n\n Round 279 / Loop: dispatch pulse RETIRED with the\n arrival ping (R75) above — same 减法 rationale.\n Particle remains as the sole \"data flow\" SMIL\n signal per active edge. */}\n {false && !reducedMotion && fresh > 0.5 && link.count >= 3 && (\n <circle\n cx={from.x}\n cy={from.y}\n r=\"0\"\n fill=\"none\"\n stroke={pal.flowEdge}\n strokeWidth=\"1.5\"\n opacity=\"0\"\n /* Round 252 / Loop: stroke transition for theme\n toggle. Same idiom as arrival ping above. */\n style={{ pointerEvents: 'none', transition: 'stroke 200ms ease-out' }}\n data-dispatch-pulse={link.key}\n >\n {/* Round 228 / Loop: same pulse-pop curves as the\n arrival ping above. r ease-out + opacity\n ease-out→ease-in. The dispatch pulse is smaller\n (0→12→18 vs arrival's 0→14→22), but the kinetic\n feel should be identical — both bookend a\n single message in flight, so they should\n physically ease the same way. */}\n <animate\n attributeName=\"r\"\n values=\"0;12;18\"\n dur={`${duration}s`}\n begin={`-${stagger.toFixed(3)}s`}\n repeatCount=\"indefinite\"\n calcMode=\"spline\"\n keyTimes=\"0;0.5;1\"\n keySplines=\"0.25 0.1 0.25 1;0.25 0.1 0.25 1\"\n />\n <animate\n attributeName=\"opacity\"\n values=\"0;0.45;0\"\n dur={`${duration}s`}\n begin={`-${stagger.toFixed(3)}s`}\n repeatCount=\"indefinite\"\n calcMode=\"spline\"\n keyTimes=\"0;0.5;1\"\n keySplines=\"0.25 0.1 0.25 1;0.42 0 1 1\"\n />\n </circle>\n )}\n {/* Round 100 / Loop: midpoint count badge for high-\n traffic edges. Width already gauges intensity but\n \"5 vs 12\" is hard to read off stroke alone. Render\n a compact pill at the bezier midpoint (t=0.5 →\n midpoint + perpendicular × lift/2) showing the\n integer count. Threshold link.count >= 3 keeps\n the canvas quiet for sparse traffic. Bezier\n midpoint math: for a quadratic curve M A Q C B,\n the t=0.5 point is (A + 2C + B) / 4, which\n simplifies to (midAB) + 0.5 × (control - midAB)\n = midAB + perpendicular × lift/2. */}\n {(() => {\n // Round 215 / Loop: badge always-mounts; visibility\n // crossfades via the wrapper <g>'s opacity instead of\n // React conditional mount/unmount on link.count >= 3.\n // Pre-R215 a flow's first 2 → 3 message crossing made\n // the badge appear in one frame, and a hot flow\n // tapering 3 → 2 vanished in one frame. R215 extends\n // the always-mount-opacity-gate idiom (R181 / R182 /\n // R183 ring family + R213 hub digit/highlight + R214\n // pulse dot) to the canvas-edge surface. Inner R164\n // r-lift / R188 stroke transitions continue to ease\n // their respective state flips independently. pointer\n // events gated to 'none' when invisible so a sub-\n // threshold badge can't intercept the hitbox click.\n const visible = link.count >= 3;\n const midX = (from.x + to.x) / 2;\n const midY = (from.y + to.y) / 2;\n const dx = to.x - from.x;\n const dy = to.y - from.y;\n const len = Math.hypot(dx, dy) || 1;\n const badgeX = midX + (-dy / len) * lift * 0.5;\n const badgeY = midY + ( dx / len) * lift * 0.5;\n const badgeOpacity = visible ? Math.min(1, fresh * edgeOpacityMul) : 0;\n /* R121: the badge becomes a canvas-side click-to-pin\n affordance. R100 introduced it as a passive count\n display; R116 added pinnedEdgeKey. Joining them\n lets users pin a flow directly from the canvas\n without crossing to the recent-signal panel.\n pointerEvents move from 'none' to 'all' on the\n wrapper <g>; the underlying R48 edge hitbox is\n wider (16 px) than the 18-px badge diameter so\n clicks landing on either still route correctly\n — the badge consumes its small footprint, the\n hitbox owns the rest. stopPropagation on\n pointerdown so the SVG pan capture doesn't\n redirect the click. */\n const isPinned = pinnedEdgeKey === link.key;\n // R126: hot-edge accent. The edge stroke width (line\n // 2344) already scales with count but clamps at 7 —\n // so count=5 and count=50 look identical at the line,\n // and the only signal differentiating them is the\n // integer in the badge. Bucket count into a \"hot\"\n // band (≥ 10) and flip the badge stroke to a warmer\n // tone + thicker ring so busy lanes telegraph at a\n // glance without reading the digit. Reuses the amber\n // family from R125 (chip empty-state) — semantic is\n // \"draw the eye here\"; R125 amber is in the chip row\n // above the SVG, R126 amber is on the canvas, so the\n // surfaces never compete in the same eye-sweep. Pin\n // still wins (uses legendHeadline) so a pinned hot\n // edge reads as \"locked + busy\" via the badge text\n // and edge brightness, not via a third stroke colour.\n const isHot = link.count >= 10;\n const hotStroke = isLight ? '#d97706' : '#fbbf24';\n return (\n <g\n data-edge-count-badge={link.key}\n data-edge-count-badge-pinned={isPinned ? 'true' : 'false'}\n data-edge-count-badge-hot={isHot ? 'true' : 'false'}\n data-edge-count-badge-visible={visible ? 'true' : 'false'}\n role={visible ? 'button' : undefined}\n tabIndex={visible ? 0 : -1}\n aria-pressed={visible ? isPinned : undefined}\n aria-hidden={visible ? undefined : true}\n className=\"anet-topo-svg-focus\"\n style={{\n pointerEvents: visible ? 'all' : 'none',\n cursor: visible ? 'pointer' : undefined,\n transition: 'opacity 300ms ease-out',\n }}\n opacity={badgeOpacity}\n onPointerDown={(e) => e.stopPropagation()}\n // R122: badge hover propagates to hoveredEdgeKey so\n // moving the cursor onto the badge lights the\n // same endpoint rings + edge brighten as hovering\n // the line. R121 only wired click; the badge sat\n // visually separate from the line on hover,\n // which felt like two surfaces rather than one.\n onMouseEnter={() => setHoveredEdgeKey(link.key)}\n onMouseLeave={() => setHoveredEdgeKey(prev => prev === link.key ? null : prev)}\n // Round 185 / Loop: edge badge click fires the same\n // one-shot expanding-ring ripple R14 uses for node\n // click and R52 uses for hub click — anchored at\n // the badge midpoint with the edge's own flowEdge\n // colour. Closes the click-feel idiom across all\n // three pinnable canvas surfaces (hub / node /\n // edge badge). Reused setClickRipple state machine\n // so only one ripple at a time; ts coordinate\n // guard in the setTimeout cleanup prevents an\n // older ripple from clobbering a newer one if\n // the user clicks two badges in quick succession.\n onClick={(e) => {\n e.stopPropagation();\n setPinnedEdgeKey(prev => prev === link.key ? null : link.key);\n const ts = Date.now();\n setClickRipple({ ts, x: badgeX, y: badgeY, r0: 10.5, color: pal.flowEdge });\n setTimeout(() => setClickRipple(prev => prev && prev.ts === ts ? null : prev), 600);\n }}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n setPinnedEdgeKey(prev => prev === link.key ? null : link.key);\n const ts = Date.now();\n setClickRipple({ ts, x: badgeX, y: badgeY, r0: 10.5, color: pal.flowEdge });\n setTimeout(() => setClickRipple(prev => prev && prev.ts === ts ? null : prev), 600);\n }\n }}\n >\n <title>{isPinned\n ? `${link.from} → ${link.to} (${link.count}) — click to release pin`\n : `${link.from} → ${link.to} (${link.count}) — click to pin`}</title>\n {/* Round 164 / Loop: edge badge gains hover-lift\n to match the 5-surface hover-elevation\n family (R51 node / R135 panel / R142 group\n box / R143 recent row / R144 legend row).\n Pre-R164 the badge had R122 hover→edge-brighten\n propagation but the badge ITSELF stayed\n static, so the cursor-on-target feedback\n felt mismatched with every other interactive\n surface. Bumping r 9 → 10.5 on hover OR pin\n gives the same \"lift\" gesture in canvas\n space (the badge sits on a curved path, so\n translate-Y wouldn't track the line; radius\n growth is the SVG-native equivalent). Pin\n and hover share the lift so a pinned badge\n stays visually raised even after mouseleave —\n mirrors R143/R144 where row pin gets the\n same lift as row hover. Pinned still keeps\n its R121 stroke change (legendHeadline +\n width 2) so pin and hover stay\n discriminable on the same lifted state.\n strokeWidth stays at 1 / 2 — won't trip the\n R51 overlap-test sentinels (1.5 / 3 are\n reserved). transition keeps the lift smooth\n (180ms ease-out) and respects prefers-\n reduced-motion via the globals.css blanket\n override that neutralises transitions.\n\n Six surfaces now share the hover-elevation\n idiom: nodes (R51), panels (R135), group\n boxes (R142), recent rows (R143), legend\n rows (R144), edge badges (R164). */}\n {/* Round 188 / Loop: extend the badge transition to\n include stroke + stroke-width. R164 added the\n r 9↔10.5 lift; R188 closes the smoothness gap\n on the R121 pin-stroke flip (cyan flowEdge ↔\n legendHeadline) and R126 hot-lane flip (cyan\n ↔ amber). Both used to snap when crossing the\n state boundary (pin click, or count crossing\n 10). Now they ease 300ms through the colour\n and width change — same idiom R167 uses for\n the node status ring. The badge strokeWidth\n values are 1/2 (not R51 sentinels 1.5/3) so\n the always-rendered badge stays invisible to\n the overlap-test guard rails. */}\n {/* Round 251 / Loop: edge badge circle transition\n list grows fill + opacity at 200ms so theme\n toggle no longer snaps the badge background\n while the rest of the circle eases.\n Pre-R251:\n r 180ms (R164 hover lift)\n stroke 300ms (R188 hot/pinned colour flip)\n stroke-width 300ms (R188 hot/pinned width flip)\n fill (pal.legendBox.fill: cyber #020617 ↔ light\n #ffffff) and opacity (cyber 0.82 ↔ light 0.95)\n were theme-driven but missed from the list —\n the badge chrome snapped on theme switch while\n the per-edge ring + visible flow path (R245)\n and per-node surfaces (R246) all eased.\n R251 closes the per-edge surface theme-toggle\n smoothness — every theme-driven property on\n every edge element now eases under cyber↔light. */}\n {/* Round 367 / Loop: edge midpoint badge rest\n stroke-width 1 → 1.25. Sibling visual-weight\n bump family (7th canvas anchor now):\n R287 minimap viewport stroke 1 → 1.5\n R295 legend swatch base radius 5.5 → 6\n R359 recent-row pip base radius 1.6 → 1.8\n R360 hub digit fontSize 11 → 12\n R361 edge-badge digit fontSize 10 → 11\n R365 hub-highlight base radius 5 → 5.5\n R367 edge-badge rest stroke 1 → 1.25 (this round)\n Cold edge badges gain ~25 % stroke presence\n (1.25/1.0 = 1.25). Stays clear of the R51\n overlap-test sentinel values (1.5 / 3 reserved\n for node strokes — the test selector is gated\n to g[data-node] ancestors so this edge-internal\n circle is invisible to that probe anyway, but\n 1.25 is a safe non-sentinel value regardless).\n R188 transition list 'stroke-width 300ms ease-\n out' still smoothes the hot/pin flip — now\n 1.25 → 2 instead of 1 → 2, same ease pace.\n data-edge-badge-stroke-width-rest exposes the\n new baseline for tests. */}\n {/* Round 371 / Loop: edge-badge cyber opacity 0.82\n → 0.85. Sibling theme-consistency polish to R370\n hub hover-ring 0.7 → 0.8. R251 designed this\n badge with opacity 0.82 (cyber) / 0.95 (light)\n — 13 % delta. Cyber-theme dark bg needs more\n alpha to read as 'present'; R371 narrows the\n gap to 10 %, bringing the badge closer to light\n theme's 0.95 floor. Light stays at 0.95\n (already in the legibility band). data-edge-\n badge-opacity attr exposes the resolved value.\n Theme-consistency polish family:\n R246/R247 panel transition family\n R251 edge badge fill/opacity baseline\n R370 hub hover-ring cyber 0.7 → 0.8\n R371 edge badge cyber 0.82 → 0.85 (this round)\n R164 r=9/10.5 hover-lift + R188/R251 transition\n list + R367 strokeWidth=1.25 cold rest preserved. */}\n {/* Round 394 / Loop: edge-badge gains a hover\n strokeWidth tier (1.5) between cold rest\n (R367 1.25) and pin/hot (2). Pre-R394 the\n badge lifted only its radius on hover (R164\n 9 → 10.5); the stroke stayed at cold rest\n 1.25 unless pin/hot kicked in, so a plain\n hover felt half-lifted — geometry expanded\n while the contour stayed thin. R394 adds\n strokeWidth=1.5 on isHoveredEdge so hover\n now lifts both r AND stroke in concert —\n same pattern R385 used for the hub hover-\n ring (1.5 → 1.75) where the ring's three\n hover axes (r grow / opacity fade-in /\n stroke thicken) all rise together.\n Three-tier stroke hierarchy now:\n cold rest 1.25 (R367)\n hovered 1.5 (R394 — this round)\n pinned / hot 2.0 (R188)\n R51 sentinel concern: strokeWidth=1.5 is\n one of the two sentinels reserved for node\n detection, but the R51 selector is gated\n to `g[data-node]` ancestors so this edge-\n internal circle is invisible to the probe\n (same lesson R177 hub hover-ring + R367\n cold rest documented). 300ms strokeWidth\n transition already in the style list eases\n the new tier naturally. data-edge-badge-\n stroke-width-hover attr exposes the hover\n value for tests. */}\n {/* Round 395 / Loop: edge-badge gains a third\n hover axis — opacity 0.85 (cyber) / 0.95\n (light) → 1.0 on isHoveredEdge. Pre-R395\n hovering thickened the stroke (R394 1.25 →\n 1.5) and grew the radius (R164 9 → 10.5)\n but the badge's translucency stayed put at\n R371's rest alpha (cyber 0.85 / light 0.95).\n R395 lifts hover to a clean 1.0 — fully\n opaque — so the hovered badge reads as\n \"in focus\" against the dim siblings.\n Three-axis hover-lift parity now complete:\n hub hover-ring (R177/R370/R385):\n r 14 → 17, opacity 0 → 0.8 cyber, sw 1.5 → 1.75\n edge badge (R164/R394/R395):\n r 9 → 10.5, sw 1.25 → 1.5, opacity → 1.0\n 200ms opacity transition (already in the\n style list) eases the new axis naturally.\n R371 rest opacity (0.85 cyber / 0.95 light)\n preserved as the resting alpha — R395\n adds an isHoveredEdge override on top.\n data-edge-badge-opacity-hover attr exposes\n the hover value for tests. */}\n {/* Round 396 / Loop: extend the R395 opacity → 1.0\n lift to the pinned state. Pre-R396 the badge\n shared `r=10.5` on both hover AND pin (R164\n unified-lift) but R395's opacity lift fired\n ONLY on isHoveredEdge — pinned badges stayed\n at R371 rest alpha (cyber 0.85 / light 0.95).\n That left pin (sticky selection) reading\n softer than hover (transient preview), even\n though pin is the stronger commitment.\n R396 unifies hover + pin at opacity=1.0\n so the same data-edge-badge-lifted='true'\n surface uniformly carries full alpha. Pin\n stroke (R188 sw=2 + pal.legendHeadline color)\n continues to differentiate pin from hover —\n the opacity track now closes the lift parity.\n The new gate (isHoveredEdge || isPinned)\n mirrors the existing R164 r-lift gate, so\n the badge has a single \"active state\"\n signature across r + opacity.\n 200ms opacity transition (already in style\n list) eases pin/unpin naturally. R371 rest\n opacity preserved as the resting alpha.\n data-edge-badge-opacity-hover renamed\n semantically to -active (covers hover+pin)\n via the new -opacity-active attr; the\n legacy -opacity-hover attr kept for R395\n test compatibility. */}\n {/* Round 480 / Loop — 5th anchor in the drop-shadow\n visual-polish family. Gates on isHot (link.\n count >= 10, R129 hot-lane threshold) so the\n badge gets a warm-amber halo when its edge\n crosses the high-traffic boundary.\n Drop-shadow family ledger now:\n R476 hub digit hover-gated emerald\n R477 legend pin-ring pin-gated row.fill\n R478 freshness pip freshness-gated cyan\n R479 group label pin-gated cyan\n R480 edge badge hot-lane-gated amber ← this round\n 5th gate type — traffic volume — joins hover,\n pin, freshness, pin. Each polish anchor uses\n a distinct semantic gate but the same paint\n vocabulary. Hue: hotStroke (amber-tinted\n palette member) at 0x80 alpha — picks up the\n R126/R188 hot-edge accent colour family so\n the glow reads as a chromatic extension of\n the existing hot-lane stroke. 3-px blur\n radius reads as soft heat rather than\n emergency klaxon.\n R51 sentinel safety: badge sw=2 only matters\n when the overlap probe runs on g[data-node]\n descendants, which this edge-internal badge\n is not. Filter is paint-only, bbox unchanged.\n transition list extends to include 'filter\n 200ms ease-out' so the heat halo eases on\n the count-crosses-threshold flip. */}\n <circle\n cx={badgeX} cy={badgeY}\n r={isHoveredEdge || isPinned ? 10.5 : 9}\n fill={pal.legendBox.fill}\n stroke={isPinned ? pal.legendHeadline : isHot ? hotStroke : pal.flowEdge}\n strokeWidth={isPinned ? 2 : isHot ? 2 : isHoveredEdge ? 1.5 : 1.25}\n opacity={(isHoveredEdge || isPinned) ? 1 : (isLight ? 0.95 : 0.85)}\n data-edge-badge-lifted={(isHoveredEdge || isPinned) ? 'true' : 'false'}\n data-edge-badge-stroke-width-rest=\"1.25\"\n data-edge-badge-stroke-width-hover=\"1.5\"\n data-edge-badge-opacity={(isHoveredEdge || isPinned) ? 1 : (isLight ? 0.95 : 0.85)}\n data-edge-badge-opacity-rest={isLight ? 0.95 : 0.85}\n data-edge-badge-opacity-hover=\"1\"\n data-edge-badge-opacity-active=\"1\"\n data-edge-badge-glow={isHot ? 'true' : 'false'}\n style={{\n filter: isHot\n ? `drop-shadow(0 0 3px ${hotStroke}80)`\n : undefined,\n transition: 'r 180ms ease-out, stroke 300ms ease-out, stroke-width 300ms ease-out, fill 200ms ease-out, opacity 200ms ease-out, filter 200ms ease-out',\n }}\n />\n {/* Round 224 / Loop: edge badge text gains the 4th\n pin-signature typography. Pre-R224 the digit\n rendered with no transition surface: when the\n flow crossed count=10 (isHot flip) the badge\n stroke eased 300ms (R188) but the digit itself\n stayed dead-typographic. R224 adds two clean\n improvements stacked on the same <text> node:\n\n 1) fontVariantNumeric: 'tabular-nums' — locks\n digit width so a 9→10 transition doesn't\n jitter the textAnchor='middle' centering by\n half a glyph. The badge is on a curved\n flow path; any width-change of the centered\n digit visibly shifts the anchor relative\n to the underlying circle. Info-density\n win — digits transition cleanly without\n pixel-jitter at the boundary.\n\n 2) letterSpacing pin signature, 4th surface\n after R218 group label / R219 legend row /\n R220 recent row. Baseline 0px; widens to\n 0.4px when isPinned || isHot. The transition\n marks the \"this lane just went special\"\n event typographically — same 300ms cadence\n as the R188 stroke flip, so the badge stroke\n + text co-ease on the hot/pin threshold.\n '0px' resolves to keyword 'normal' in\n computed style (R218 test trap learned);\n test parsers must accept either form.\n\n data-edge-badge-text-pin attr surfaces the\n isPinned||isHot state for introspection. */}\n <text\n x={badgeX} y={badgeY + 3}\n textAnchor=\"middle\"\n fill={pal.legendHeadline}\n /* Round 361 / Loop: edge midpoint badge text\n fontSize 10 → 11. Sibling visual-weight bump\n to R360 hub digit 11 → 12. The badge digit\n is the per-edge equivalent of the hub digit\n — a high-information scalar (link.count) at\n a stable canvas position. Pre-R361 fontSize=\n 10 + R220 letter-spacing 0.4 + R224 tabular-\n nums made the digit READABLE but small\n against the r=9 / 18-px badge envelope;\n fontSize=11 nudges the glyph ~10 % bigger\n (bbox ~7×10 px from ~6×9 px) so the count\n reads more cleanly at glance — still well\n inside the r=9 idle circle and the r=10.5\n hover/pin lift (R164). y=badgeY+3 empirical\n vertical centring kept (1px drift at the\n bumped size is below the noise floor in\n the on-curve flow path).\n Visual-weight bump family:\n R287 minimap viewport stroke 1 → 1.5\n R295 legend swatch base radius 5.5 → 6\n R359 recent-row pip base radius 1.6 → 1.8\n R360 hub digit fontSize 11 → 12\n R361 edge-badge digit fontSize 10 → 11 (this round)\n data-edge-badge-text-font-size attr exposes\n the value for tests. R220 pin/hot letter-\n spacing tween + R224 tabular-nums + R188\n stroke-width pin/hot transitions all preserved. */\n fontSize=\"11\"\n fontFamily=\"monospace\"\n /* R426 — edge-badge digit fontWeight 700 → 800 on\n (isPinned || isHot). 4th anchor on the \"data\n tightens under attention\" typographic-weight\n pattern:\n R416 chip-digit (chip-row hover trigger)\n R424 panel-digit (panel hover trigger)\n R425 hub-digit (hub hover trigger)\n R426 edge-badge-digit (pin/hot trigger) ← this\n The badge digit is the per-edge equivalent of\n the hub digit (R361 sibling fontSize bump\n reasoning). Stacks with R188 stroke-width pin/\n hot lift (1.25 → 1.5) + R220 letter-spacing pin/\n hot tween (0 → 0.4) for a 3-axis pin/hot signa-\n ture (edge structure + text spacing + text\n weight). The R408 transition is letter-spacing\n 300ms; R426 appends font-weight 300ms so the\n weight bump co-eases under the same cadence. */\n fontWeight={(isPinned || isHot) ? '800' : '700'}\n data-edge-badge-text={link.key}\n data-edge-badge-text-pin={(isPinned || isHot) ? 'true' : 'false'}\n data-edge-badge-text-font-size=\"11\"\n style={{\n pointerEvents: 'none',\n fontVariantNumeric: 'tabular-nums',\n /* R431 — edge-badge digit 3-tier letter-spacing:\n rest 0 / isHoveredEdge 0.2 / (isPinned || isHot) 0.4.\n Mirrors R427 node-alias 3-tier (rest/hover/chat-\n target → 0/0.3/0.5) at edge-badge scope. Pre-R431\n letter-spacing only fired on pin/hot (R220) while\n pure edge hover lifted stroke (R394) + opacity\n (R395) + radius (R164) but left the text dead-\n typographic. R431 adds the missing typographic\n spacing axis to the edge-hover gesture so the\n text rises with the badge geometry. Pin/hot\n tier (0.4) still wins; hover is the mid step.\n Hover-letter-spacing family extension (7 anchors\n now): R344/R345/R347/R351/R420/R427/R431. */\n letterSpacing: (isPinned || isHot) ? '0.4px' :\n isHoveredEdge ? '0.2px' : '0px',\n transition: 'letter-spacing 300ms ease-out, font-weight 300ms ease-out',\n }}\n >{link.count}</text>\n </g>\n );\n })()}\n </g>\n );\n })}\n\n {/* center hub — round 39 sized + round 13 restraint.\n The hub is the control-plane anchor. r39 gave it two outward\n pulses (r 10→38, 2.4s, double-phase) which read as \"loudest\n node in the room\" — wrong semantics: anchors don't emit,\n they hold. r13 swaps the kinetic pulse for a slow opacity\n breath on the static halo (4s, ±15% from base), so the hub\n stays alive without throwing kinetic energy outward. The\n dual-circle \"lit lamp\" core is unchanged. */}\n {layout === 'ring' && (<g\n data-topo-hub\n data-topo-hub-hovered={hoveredHub ? 'true' : 'false'}\n // Round 159 / Loop: the hub is the most visually\n // prominent interactive element on the canvas (R39\n // enlarged it, R52 made it click to fitView, R115\n // added a hover ring, R43 gave it a tooltip) — but\n // R151-R157's a11y sweep skipped it. role/tabIndex/\n // aria-label/onKeyDown bring it to parity with node <g>\n // (R151), group label hit (R152), and minimap (R157).\n // anet-topo-svg-focus picks up R156's explicit cyan\n // focus ring (default browser SVG focus rect is hard\n // to see against the dark canvas).\n role=\"button\"\n tabIndex={0}\n aria-label={(() => {\n const parts = ['Network hub'];\n if (onlineNodes.length > 0) parts.push(`${onlineNodes.length} online`);\n if (workingCount > 0) parts.push(`${workingCount} working`);\n if (flowLinks.length > 0) parts.push(`${flowLinks.length} active link${flowLinks.length === 1 ? '' : 's'}`);\n return parts.join(' · ') + ' — Enter to fit view';\n })()}\n // Round 176 / Loop: hub joins the first-paint fade-in\n // family as the 6th surface. The hub is the visual anchor\n // — every other ring-layout reveal layer (R174 tier rings,\n // R9 nodes, R172 edges) emanates outward FROM it — yet\n // pre-R176 the hub itself popped in instantly while the\n // wave it should be leading staggered around it. Adding\n // .anet-fade-in at delay 0 (no animation-delay needed)\n // places the hub as the canvas-center anchor that the\n // tier wave grows from. Composes cleanly with the existing\n // anet-topo-svg-focus class (R159 keyboard focus ring).\n className=\"anet-topo-svg-focus anet-fade-in\"\n data-topo-hub-fade-delay={0}\n style={{ cursor: 'pointer' }}\n // Stop pointerdown from reaching the SVG pan handler — same\n // reason as the node <g>: a captured pointer makes Chromium\n // fire the follow-up click on the SVG, not this group.\n onPointerDown={(e) => e.stopPropagation()}\n onMouseEnter={() => setHoveredHub(true)}\n onMouseLeave={() => setHoveredHub(false)}\n // Round 52 / Loop: hub is the visual anchor but had no click\n // action — users discovered fit-to-content only via the `f`\n // key or the chrome button. Wire the hub to fitView() so the\n // most prominent element in the canvas is also the \"re-\n // center\" affordance. A click-ripple keyed by ts confirms\n // the gesture; the hub <title> now ends with a hint.\n onClick={() => {\n fitView();\n setClickRipple({ ts: Date.now(), x: cx, y: cy, r0: 18, color: isLight ? '#059669' : '#10b981' });\n setTimeout(() => setClickRipple(prev => prev && prev.x === cx && prev.y === cy ? null : prev), 600);\n }}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n fitView();\n setClickRipple({ ts: Date.now(), x: cx, y: cy, r0: 18, color: isLight ? '#059669' : '#10b981' });\n setTimeout(() => setClickRipple(prev => prev && prev.x === cx && prev.y === cy ? null : prev), 600);\n }\n }}\n >\n {/* Round 43 / Loop: hub `<title>` summary — hovering the\n central glow now answers \"what is this?\" with a one-line\n fleet snapshot. Duplicates the header chips by design;\n hovering the most visually prominent element is the\n most natural impulse, so satisfy it where the cursor\n already is. Falls clean: omits sub-clauses when their\n counts are zero. R52 appends a \"click to fit view\" hint.\n */}\n <title>{(() => {\n const total = sessions.length;\n const parts = [`Network hub`, `${total} session${total === 1 ? '' : 's'}`];\n if (onlineNodes.length > 0) parts.push(`${onlineNodes.length} online`);\n if (workingCount > 0) parts.push(`${workingCount} working`);\n if (flowLinks.length > 0) parts.push(`${flowLinks.length} active link${flowLinks.length === 1 ? '' : 's'}`);\n return parts.join(' · ') + '\\nclick to fit view';\n })()}</title>\n {/* grounding halo — breathes in opacity, no expansion.\n R84: breath amplitude + tempo reflect workingCount. An\n idle fleet keeps the original gentle 0.32→0.52 (light) /\n 0.08→0.16 (dark) cycle over 4s — \"heart at rest\". As\n working sessions accumulate, the peak climbs and the\n period shortens, capped at 2.4s so it never feels\n manic. Quiet fleets see zero change; busy fleets feel\n the canvas working. */}\n {(() => {\n // Bucket workingCount to keep the visual feedback discrete\n // rather than continuous: 0 / 1-2 / 3-5 / 6+. Three buckets\n // are enough — finer gradations are imperceptible.\n const busy = workingCount === 0 ? 0\n : workingCount <= 2 ? 1\n : workingCount <= 5 ? 2\n : 3;\n // Round 404 / Loop: hub-halo cyber trough opacity 0.08 →\n // 0.10. Pre-R404 the breath's low-point sat at α=0.08\n // cyber (per R84 family tuning) — the halo nearly faded\n // out at trough on the dark canvas. R404 lifts cyber\n // trough to 0.10. Per-bucket peak amplitudes [0.16/0.20/\n // 0.26/0.32] stay exactly tuned.\n //\n // Round 405 / Loop: hub-halo LIGHT trough 0.32 → 0.34 —\n // symmetric +0.02 lift to mirror R404's cyber treatment\n // across both themes. Pre-R405 only cyber got the lift\n // (R404 docstring noted \"light already at the strong\n // end\" as deliberate); but the cyber/light delta in\n // R404 was an inconsistency in the family pattern.\n // R405 closes the symmetry — both themes get +0.02\n // baseline lift, so the breath low-point reads with\n // matching confidence regardless of theme. Light peak\n // array [0.52/0.58/0.65/0.72] stays tuned.\n //\n // Stale-state legibility lift family (5 anchors now):\n // R317 subordinate-text gray-500 → gray-400\n // R358 freshness floor 0.25 → 0.30\n // R372 minimap offline-dot opacity 0.5 → 0.6\n // R404 hub-halo cyber trough 0.08 → 0.10\n // R405 hub-halo light trough 0.32 → 0.34 (this round)\n //\n // R84 per-bucket peak/dur + R245 ease-in-out spline\n // keySplines all preserved. Test fixture probes the\n // SMIL <animate> values via data-topo-hub-halo-trough\n // attr (now exposes both light + cyber resolved values).\n const peakLight = [0.52, 0.58, 0.65, 0.72][busy];\n const peakDark = [0.16, 0.20, 0.26, 0.32][busy];\n const troughLight = 0.34;\n const troughDark = 0.10;\n const dur = [4.0, 3.2, 2.7, 2.4][busy];\n const valuesLight = `${troughLight};${peakLight};${troughLight}`;\n const valuesDark = `${troughDark};${peakDark};${troughDark}`;\n // Round 408 / Loop: hub-halo radius 18 → 20. The\n // grounding halo (the breathing outer circle around\n // the hub center) is the canvas's signature breath\n // element — R84 family. R408 bumps r=18 → 20 so the\n // breath extends slightly further while keeping 4px\n // clearance before the spoke origin (still room for\n // spoke start anchors). Visual presence on the\n // canvas focal point lifts ~23% area (π·20²/π·18²\n // = 1.23) without changing the per-bucket opacity\n // envelope or breath rhythm. Visual-weight bump\n // family 13th anchor — pairs with R404/R405 trough\n // lifts so the halo now breathes both with more\n // visible amplitude AND more visual footprint.\n // R84 per-bucket peak/dur invariants + R244 calc-\n // Mode='spline' + R245 ease-in-out keySplines all\n // preserved. data-topo-hub-halo-radius attr exposes\n // value for tests.\n /* Round 451 / Loop: hub center halo radius lift on\n hoveredHub — r 20 → 22 (+2px, ~21% area). Adds another\n geometric axis to the hub-hover signature stack\n alongside R177 ring radius lift + R209 digit scale +\n R425 digit fw + R370 halo opacity + R386 highlight\n opacity + R441 core fill chroma. Pre-R451 the halo\n r stayed planted at R408's 20px while the rest of\n the hub structure responded to hover. R451 makes\n the halo breath outward on hover so the focal pulse\n intensifies under attention. SMIL `<animate>` on\n opacity continues independently (animateAttr=\n 'opacity' vs CSS-property r — non-conflicting). R408\n base radius 20 preserved as rest; +2 hover delta\n keeps clearance from the R177 hub-hover-ring at\n r=17 hover (halo is BEHIND the ring, halo r=22 sits\n 5px beyond the ring's hover-r=17, still well within\n the hub canvas envelope). data-topo-hub-halo-radius\n attr now reports the dynamic value. */\n const isHaloHovered = !reducedMotion && hoveredHub;\n const haloR = isHaloHovered ? 22 : 20;\n return (\n <circle\n cx={cx} cy={cy}\n fill={isLight ? '#d1fae5' : '#10b981'}\n opacity={isLight ? 0.42 : 0.12}\n data-hub-busyness={busy}\n data-topo-hub-halo-radius={haloR}\n data-topo-hub-halo-hovered={isHaloHovered ? 'true' : 'false'}\n data-topo-hub-halo-trough={isLight ? troughLight : troughDark}\n data-topo-hub-halo-peak={isLight ? peakLight : peakDark}\n /* Round 253 / Loop: hub grounding halo fill transition\n for theme toggle. Pre-R253 the base fill (cyber\n #10b981 ↔ light #d1fae5) snapped while R244's SMIL\n animate on opacity continued running. CSS fill\n transition is independent of the SMIL animate\n (different attributes), so they compose without\n conflict.\n R451: r as CSS property (R197/R198 idiom) so the\n hover-radius tween eases smoothly under the same\n 200ms cadence as fill. */\n style={{\n r: `${haloR}px`,\n transition: 'fill 200ms ease-out, r 200ms ease-out',\n } as React.CSSProperties}\n >\n {/* Round 244 / Loop: hub grounding halo breath gets\n ease-in-out keySplines, matching the active-node\n pulse (R243) treatment. Pre-R244 default linear\n calcMode marched opacity at constant velocity\n through the 3-value trough→peak→trough bounce —\n mechanical pacing for a 'heartbeat at rest'\n visual. R244 adds calcMode='spline' + keyTimes\n '0;0.5;1' + keySplines '0.42 0 0.58 1' ×2 (CSS\n ease-in-out on both halves), so the breath\n decelerates near the troughs AND the peak —\n lingers briefly at each extreme like a real\n heart-rest cycle. Same SMIL-easing family as\n R227 (click ripple) / R228 (edge ping+pulse) /\n R243 (active-node pulse). The breath family\n is now 2 surfaces deep at this single hub\n element — R84 amplitude/tempo bucket + R244\n curve shape. */}\n {!reducedMotion && (\n <animate\n attributeName=\"opacity\"\n values={isLight ? valuesLight : valuesDark}\n dur={`${dur}s`}\n repeatCount=\"indefinite\"\n calcMode=\"spline\"\n keyTimes=\"0;0.5;1\"\n keySplines=\"0.42 0 0.58 1;0.42 0 0.58 1\"\n />\n )}\n </circle>\n );\n })()}\n {/* core — 20px diameter, larger inner highlight reads as a \"lit lamp\"\n Round 248 / Loop: hub center core gets a fill transition.\n Pre-R248 the core circle (the visual anchor at the centre\n of the canvas, fill=isLight ? '#059669' emerald-600 :\n '#10b981' emerald-500) hard-flipped on theme toggle —\n the most visually prominent element on the canvas\n snapping while everything else (R244 halo / R241 hub\n spokes / R246 label cards / R247 side panels) eased.\n Inline transition closes the gap. data-topo-hub-core\n attr added for test introspection (the parent <g> at\n line 3587 has data-topo-hub but the core specifically\n is the canvas anchor). */}\n {(() => {\n /* Round 441 / Loop: hub center core fill brighten on\n hoveredHub. Pre-R441 the core was static (cyber\n emerald-500 #10b981 / light emerald-600 #059669) and\n the hub-hover gesture lifted ring radius (R177) +\n digit scale (R209) + digit fw (R425) + halo opacity\n (R370) + highlight opacity (R386) but the focal core\n ITSELF stayed planted at rest tone. R441 shifts the\n fill one emerald tier brighter on hover so the canvas\n anchor itself responds:\n cyber emerald-500 → emerald-400 (#10b981 → #34d399)\n light emerald-600 → emerald-500 (#059669 → #10b981)\n Same +100 step on the emerald scale across both themes.\n Pure paint axis; no geometry change. R248 fill 200ms\n transition already in the style list eases the shift.\n Closes the chroma axis on the hub-hover gesture stack:\n R177 ring radius lift geometry\n R209 digit scale 1.08 geometry\n R425 digit fw 700 → 800 typography\n R370 halo opacity 0.7 → 0.8 paint\n R386 highlight opacity paint\n R441 core fill brighten chroma ← this round\n data-topo-hub-core-hovered + -fill attrs exposed\n for tests. */\n const isCoreHovered = !reducedMotion && hoveredHub;\n const coreFill = isLight\n ? (isCoreHovered ? '#10b981' : '#059669')\n : (isCoreHovered ? '#34d399' : '#10b981');\n return (\n <circle\n cx={cx} cy={cy} r=\"10\"\n fill={coreFill}\n data-topo-hub-core\n data-topo-hub-core-hovered={isCoreHovered ? 'true' : 'false'}\n data-topo-hub-core-fill={coreFill}\n style={{ transition: 'fill 200ms ease-out' }}\n />\n );\n })()}\n {/* R130 / Loop: when workingCount > 0, the decorative inner\n highlight gets replaced with the workingCount digit. The\n R84 busyness breath already encodes the same metric\n through motion — adding the digit gives it a second\n visual channel right at the canvas's focal point. A\n user glancing at the hub now sees both \"the network is\n pulsing\" (motion) AND \"3 agents are working\" (digit)\n without having to scan the chip row or panels.\n\n Geometry: text at (cx, cy) with fontSize 11 monospace\n + fontWeight 700 sits inside the r=10 core (a 2-digit\n 12 reads ~12 px wide × 11 px tall, well inside the\n 20-px diameter core). Centered vertically via dy=\n \"0.36em\" — the standard SVG trick for text-vertical-\n center without measuring fontMetrics.\n\n pointerEvents:none so the digit can't intercept the\n hub click (R52 fit-to-view still fires).\n\n workingCount=0 falls through to the existing\n decorative highlight so the hub never looks empty. */}\n {/* Round 213 / Loop: hub centre crossfades the workingCount\n digit and the R130 decorative highlight when count\n crosses zero, instead of mount/unmount snap. Pre-R213 a\n fleet going from idle (workingCount=0, highlight circle)\n to first-working-node (workingCount=1, digit \"1\") swapped\n the elements in a single frame — visible flash at the\n hub's focal point. R213 uses the always-mount + opacity-\n gate pattern (R181/R182/R183 family) so both render\n concurrently and crossfade via opacity transitions.\n Geometry already overlaps (both centred on cx,cy with\n r=5 / digit-bbox ~7×11), so the dual-render adds zero\n layout cost. Reduced-motion users see a 0ms duration\n via the R29 globals.css blanket override. */}\n {/* digit (visible when workingCount > 0) */}\n <text\n x={cx} y={cy}\n textAnchor=\"middle\"\n dy=\"0.36em\"\n fill={isLight ? '#d1fae5' : '#ecfdf5'}\n /* Round 360 / Loop: hub working-count digit fontSize 11\n → 12. The hub is the canvas's focal point — its digit\n is the most-read scalar on the whole topology. R130\n sized it at 11 (well inside the r=10 / 20-px core);\n R360 nudges it to 12 (~13 px wide × 12 px tall, still\n well inside the 20-px diameter) for ~9 % more presence.\n Sibling visual-weight bump family:\n R287 minimap viewport stroke 1 → 1.5\n R295 legend swatch base radius 5.5 → 6\n R359 recent-row pip radius 1.6 → 1.8\n R360 hub digit fontSize 11 → 12 (this round)\n The R209 scale-1.08-on-hub-hover, R225 tabular-nums,\n R253 fill transition, R213 always-mount opacity gate\n all preserved. data-topo-hub-working-count-font-size\n attr exposes the value for tests. */\n fontSize=\"12\"\n fontFamily=\"monospace\"\n /* R425 — hub digit fontWeight 700 → 800 on hoveredHub.\n Closes the \"data tightens under attention\" pattern\n across three focal scopes: chip-digit (R416, chip\n scope) → panel-digit (R424, panel-header scope) →\n hub-digit (R425, hub focal scope). The hub digit is\n the most-read scalar on the topology; adding a weight\n axis on hover stacks with the R209 scale-1.08 + R177\n ring grow + R370 halo opacity + R386 highlight\n opacity hub-hover gestures, giving the focal point\n a typographic axis alongside its scale/structure cues.\n R360 fontSize=12 + R225 tabular-nums + R209 scale +\n R253 fill transition all preserved. Transition list\n extends to include font-weight 200ms ease-out. */\n fontWeight={hoveredHub ? '800' : '700'}\n opacity={workingCount > 0 ? 1 : 0}\n data-topo-hub-working-count={workingCount}\n data-topo-hub-working-count-font-size=\"12\"\n data-topo-hub-working-count-hovered={hoveredHub ? 'true' : 'false'}\n data-topo-hub-working-count-visible={workingCount > 0 ? 'true' : 'false'}\n // Round 209 / Loop: hub workingCount digit scales 1.0 →\n // 1.08 on hub-hover, matching R177's r 14→17 ring grow.\n // Pre-R209 hovering the hub grew the ring while the\n // focal-point digit at the centre stayed planted — the\n // gesture lifted only half the structure. R209 ties the\n // digit's scale into the same hoveredHub state R177\n // already drives, so ring + digit rise as one unit.\n // transform-box: fill-box + transform-origin: center\n // anchors the scale to the digit's own bbox (same\n // idiom R184 reset-spin + R186 chrome-pop use for\n // SVG icon transforms). 200ms matches R167 node-ring\n // stroke-width interpolation pace. Reduced-motion users\n // skip the scale via the !reducedMotion gate (R29 a11y).\n // Round 225 / Loop: tabular-nums on hub digit — info-\n // density sibling to R224's edge badge tabular-nums.\n // Same physics: when workingCount crosses the 9 → 10\n // boundary the textAnchor='middle' centering jitters\n // ~3-4px because monospace fonts still have width\n // variance at the digit-vs-control boundary. Tabular\n // locks digit width so the focal point stays planted\n // through every count change. Pure visual tightening;\n // no test trap (computed font-variant-numeric resolves\n // to the keyword 'tabular-nums' verbatim).\n /* Round 253 / Loop: append fill 200ms to the hub\n digit transition list — theme toggle (cyber #ecfdf5\n ↔ light #d1fae5) was the last hub-area snap. */\n /* Round 476 / Loop — hub working-count digit gains a\n filter: drop-shadow glow on hub-hover. Stacks with\n the existing 4-axis hub-hover gesture stack on this\n element:\n R209 transform: scale(1.08) geometry\n R425 fontWeight 700 → 800 typography\n R253 fill ease-out chroma (theme)\n R213 opacity gate fade (count cross)\n R476 filter drop-shadow glow paint (this round)\n The glow uses the cyber emerald-400 (#34d399) /\n light emerald-500 (#10b981) hue family so the\n chroma stays inside the hub-area palette. Subtle\n 2-3 px blur radius at 0.6 opacity — visible but\n not loud, reads as \"the focal digit lit up under\n attention\".\n Reduced-motion users skip the filter via the\n !reducedMotion gate (R29 a11y blanket).\n Filter is a paint-only attribute — bbox stays\n the same, R51 overlap-test invariants hold.\n transition list extends to 'filter 200ms ease-out'\n so the glow eases under the same cadence as the\n scale + fw + fill axes. */\n data-topo-hub-working-count-glow={!reducedMotion && hoveredHub ? 'true' : 'false'}\n style={{\n pointerEvents: 'none',\n transform: !reducedMotion && hoveredHub ? 'scale(1.08)' : 'scale(1)',\n transformBox: 'fill-box',\n transformOrigin: 'center',\n filter: !reducedMotion && hoveredHub\n ? (isLight\n ? 'drop-shadow(0 0 2px rgba(16, 185, 129, 0.6))'\n : 'drop-shadow(0 0 3px rgba(52, 211, 153, 0.6))')\n : undefined,\n /* R425: font-weight 200ms appended so the hover fw\n bump 700 → 800 eases under the same cadence as\n R209 scale + R253 fill + R213 opacity.\n R476: filter 200ms appended so the new drop-\n shadow glow eases at the same cadence. */\n transition: 'transform 200ms ease-out, opacity 300ms ease-out, fill 200ms ease-out, font-weight 200ms ease-out, filter 200ms ease-out',\n fontVariantNumeric: 'tabular-nums',\n }}\n >\n {workingCount}\n </text>\n {/* decorative highlight (visible when workingCount === 0) */}\n {/* Round 365 / Loop: hub-center 'lit-lamp' decorative highlight\n circle r 5 → 5.5. Sibling visual-weight bump family —\n each round lifts one canvas anchor's geometric presence\n without disturbing its bbox envelope:\n R287 minimap viewport stroke 1 → 1.5\n R295 legend swatch base radius 5.5 → 6\n R359 recent-row pip base radius 1.6 → 1.8\n R360 hub digit fontSize 11 → 12\n R361 edge-badge digit fontSize 10 → 11\n R365 hub-highlight base radius 5 → 5.5 (this round)\n The highlight only renders when workingCount === 0\n (decorative 'lamp lit but idle' state per R130 + R213\n always-mount opacity-gate). At idle, the 0.5-px radius\n bump (21 % area, π*5.5² / π*5² = 1.21) lifts the lamp's\n presence — still well inside the r=10 hub-core (R130).\n opacity=0 when working preserved so the hub-digit's R130\n takeover stays seamless. R213 always-mount opacity-gate\n + 300ms opacity transition + pointerEvents:none all\n preserved. data-topo-hub-highlight-radius attr exposes\n the value for tests. */}\n {/* Round 386 / Loop: hub-highlight idle opacity 0.9 → 0.95.\n When workingCount===0 the highlight paints as the visible\n idle \"lamp lit but no work\" core (R130 takeover gate).\n Pre-R386 idle opacity was 0.9 — a ~6 % fade against full\n paint that read as slightly-dimmed-ghost on the focal\n point. R386 lifts to 0.95 (idle alpha gap halved 0.10\n → 0.05) so the canvas anchor reads more confidently\n as a present-but-idle state rather than a faded ghost.\n Theme-consistency / canvas-presence polish family (4th\n anchor):\n R370 hub hover-ring opacity 0.7 → 0.8 cyber\n R371 edge-badge rest opacity 0.82 → 0.85 cyber\n R372 minimap offline-dot opacity 0.5 → 0.6\n R386 hub-highlight idle opacity 0.9 → 0.95 (this round)\n opacity=0 when working preserved so the hub-digit's\n R130 takeover stays seamless. 300ms opacity transition\n + R213 always-mount opacity-gate + pointerEvents:none\n + R365 r=5.5 all preserved. data-topo-hub-highlight-\n opacity attr exposes the resolved value for tests. */}\n <circle\n cx={cx} cy={cy} r=\"5.5\"\n fill=\"#d1fae5\"\n opacity={workingCount > 0 ? 0 : 0.95}\n data-topo-hub-highlight\n data-topo-hub-highlight-visible={workingCount > 0 ? 'false' : 'true'}\n data-topo-hub-highlight-radius=\"5.5\"\n data-topo-hub-highlight-opacity={workingCount > 0 ? 0 : 0.95}\n style={{\n pointerEvents: 'none',\n transition: 'opacity 300ms ease-out',\n }}\n />\n {/* R115 / Loop: hover hint ring. Stroke-only circle at r=14\n that fades in when the hub is hovered — the same idea\n R44 used for node avatars (group-hover stroke). r=14\n sits comfortably outside the r=10 core and INSIDE the\n r=18 grounding halo, so the hover indicator is fully\n contained within the existing hub footprint (no bbox\n growth, overlap test unchanged). pointerEvents:none so\n the hint can't intercept the click that produced it. */}\n {/* Round 177 / Loop: hub hover ring picks up the same\n \"lift on hover\" gesture R164 added to the edge midpoint\n badge (r=9→10.5). Pre-R177 the ring faded opacity 0→0.7\n on hover but stayed static at r=14. Adding r=14→17 on\n hover gives the gesture extra weight — the hub responds\n more confidently. Stays within the r=18 halo bbox (no\n geometry growth), so the R51 overlap-test guard rails\n still hold. strokeWidth=1.5 is the offline-node\n sentinel but the overlap-test selector is gated to\n `g[data-node]` ancestors — this hub-internal circle\n is invisible to that probe. transition list grows `r`\n alongside opacity so both ease in concert; the lift\n feels like one continuous gesture. Seventh surface in\n the hover-elevation family (R51 nodes, R135 panels,\n R142 group boxes, R143/R144 rows, R164 edge badges,\n R177 hub ring). prefers-reduced-motion respected via\n R29 globals.css blanket. */}\n {/* Round 385 / Loop: hub hover-ring strokeWidth 1.5 → 1.75.\n Sibling visual-weight bump (11th anchor) to R367 edge-\n badge rest stroke 1 → 1.25. The ring is only visible\n during hub hover (opacity=0 rest, R177 + R370 control\n the hover-state alpha) so the change manifests purely\n as a thicker hover-state ring on the canvas focal\n point. R177 r 14 → 17 grow + R370 opacity 0 → 0.8\n already lift the hover cue; R385 adds stroke weight\n as the third lift axis. Stays clear of R51 overlap-\n test sentinel value 3 (1.75 is non-sentinel); the\n R51 selector is gated to g[data-node] ancestors so\n this hub-internal circle is invisible to the probe\n regardless. R253 stroke transition + pointerEvents:\n none preserved. data-topo-hub-hover-ring-stroke-width\n attr exposes the value for tests. */}\n <circle\n cx={cx} cy={cy}\n r={hoveredHub ? 17 : 14}\n fill=\"none\"\n stroke={isLight ? '#059669' : '#10b981'}\n strokeWidth=\"1.75\"\n /* Round 370 / Loop: hub hover-ring cyber opacity 0.7 →\n 0.8. R177 designed the hub hover-ring at opacity-0 →\n 0.85 (light) / 0 → 0.7 (cyber). The 15 % gap between\n the two themes meant cyber-theme operators got a\n noticeably softer hover cue than light-theme users\n against backgrounds that should equalise (dark bg\n needs more luminance to read as 'on'). R370 bumps\n cyber 0.7 → 0.8, narrowing the theme gap to 5 % —\n sibling theme-consistency polish to R251 edge badge\n fill/opacity (cyber 0.82 / light 0.95) and R246/R247\n panel transition families. Light theme 0.85 stays\n as is (already in the legibility band). data-topo-\n hub-hover-ring-opacity attr exposes the value for\n tests. */\n opacity={hoveredHub ? (isLight ? 0.85 : 0.8) : 0}\n data-topo-hub-hover-ring\n data-topo-hub-hover-ring-radius={hoveredHub ? 17 : 14}\n data-topo-hub-hover-ring-stroke-width=\"1.75\"\n data-topo-hub-hover-ring-opacity={hoveredHub ? (isLight ? 0.85 : 0.8) : 0}\n /* Round 253 / Loop: hub hover ring also gets stroke\n transition for theme toggle (cyber #10b981 ↔ light\n #059669). The opacity + r transitions stay for hover\n lift; stroke closes the theme-snap. */\n style={{\n pointerEvents: 'none',\n transition: 'opacity 180ms ease-out, r 180ms ease-out, stroke 200ms ease-out',\n }}\n />\n </g>)}\n\n {/* agent nodes */}\n {[...onlineNodes, ...offlineNodes].map((session, nodeIdx) => {\n const pos = nodePositions[session.alias];\n if (!pos) return null;\n\n const sseCountFor = (session.network_id ? sseSessions[`${session.network_id}:${session.alias}`] : undefined) ?? sseSessions[session.alias];\n const isOnline = session.status !== 'offline' || !!sseCountFor;\n const status = nodeStatus(session, isOnline, isLight);\n const isActive = activeAliases.has(session.alias);\n // #113: node size scales with the S/M/L control; halos, labels,\n // badge and avatar all derive from `radius` so they follow.\n const radius = Math.round((isOnline ? 26 : 18) * nodeScale);\n // Round 109/110 (Vincent 4582 + 4583 P0): at high node counts\n // the 100px opaque label cards overlapped each other and\n // covered neighbouring avatars. But hiding text entirely went\n // too far (\"还是得有文字\"). So: below the density threshold the\n // full name+status card shows always; above it each node shows\n // a lightweight plain-text alias (no opaque card → can't\n // occlude an avatar), and the full card appears only for the\n // hovered node or once zoomed in past 1.4×.\n const showFullLabel = !denseLayout || hoveredAlias === session.alias || view.zoom >= 1.4;\n // Round 8: when a node in another group is hovered, fade this\n // one. Same-group nodes (incl. singletons matching the hovered\n // alias) stay full. Pure visual focus, geometry unchanged.\n const inFocus = !activeGroup || (groupKeys[session.alias] ?? session.alias) === activeGroup;\n // R72: in ring layout, classify the node into a tier by its\n // distance from hub centre so the first-paint stagger can\n // emanate inner → outer instead of running clockwise. Grid\n // layout doesn't have a radial structure; it keeps R9's pure\n // nodeIdx stagger. Thresholds bracket the actual tier radii\n // (single 220, dual 175/260, triple 145/215/285, offline 325+).\n let tierIdx = 0;\n if (layout === 'ring') {\n const p = nodePositions[session.alias];\n if (p) {\n const d = Math.hypot(p.x - cx, p.y - cy);\n tierIdx = d < 195 ? 0 : d < 270 ? 1 : d < 310 ? 2 : 3;\n }\n }\n\n return (\n <g\n key={session.alias}\n data-node={session.alias}\n data-tier-idx={layout === 'ring' ? tierIdx : -1}\n // R151 / Loop: node a11y compliance — match the pattern\n // R116 / R139 / R140 / R148 / R149 applied to other\n // interactive surfaces. Node <g> has been clickable for\n // chat since R45 but tab-unreachable + screen-reader-\n // unannounced. role=\"button\" + tabIndex=0 + aria-label\n // expose it; aria-pressed reflects chat-target state so\n // SR users know which node currently has the popover\n // open. onKeyDown for Enter / Space fires the same\n // setChatAlias path as onClick. preventDefault on\n // Space stops the SVG canvas from scrolling.\n role=\"button\"\n tabIndex={0}\n aria-pressed={chatAlias === session.alias}\n aria-label={`Chat with ${session.alias} (${session.status})`}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n setChatAlias(session.alias);\n setClickRipple({\n ts: Date.now(),\n x: pos.x, y: pos.y, r0: radius,\n color: status.primary,\n });\n setTimeout(() => setClickRipple(prev =>\n prev && Date.now() - prev.ts >= 590 ? null : prev), 600);\n }\n }}\n // Round 3 / Loop: `anet-fade-in` runs once when the <g>\n // mounts — a new session entering the fleet (or the topology\n // first rendering) eases in instead of popping. Re-renders of\n // an existing node don't re-trigger (React preserves the <g>\n // via the alias key), so status changes don't flicker. The\n // global prefers-reduced-motion sweep already neutralises it.\n className=\"group transition-opacity anet-fade-in anet-topo-svg-focus\"\n style={{\n cursor: 'pointer',\n // Round 17 / Loop: offline nodes drop to 0.6 at rest so\n // online nodes pop without losing the offline-as-ghost\n // information. The dashed stroke + smaller radius already\n // say \"offline\"; dimming the whole group strengthens the\n // online-vs-offline hierarchy at first glance. Exempt the\n // chat-focused node — if the user explicitly opened a\n // popover targeting an offline alias, that node stays\n // full-brightness so the focus ring + popover read as one\n // selected thing rather than a dimmed selection. Group-\n // hover fade (Round 8) still wins when inFocus is false.\n // Round 49 / Loop: edge-hover endpoint highlight composes\n // OVER the inFocus/online formula — a non-endpoint node\n // dims to 0.28 (just below the inFocus 0.32 to read as a\n // stronger \"not relevant\" signal), endpoints keep their\n // base opacity. chatAlias still wins to keep the focus\n // ring legible if the user clicked through.\n // Round 55 / Loop: legend-status hover composes on the\n // SAME level as edge-hover-endpoint. Non-matching nodes\n // dim to 0.28; matching nodes stay at their base.\n // activeStatus matches: working = status==='working',\n // idle = online but not working, offline = !isOnline.\n // Round 60 / Loop: activeStatus = hoveredStatus ?? pinnedStatus\n // so the pressure-bar segment pins (R60) and the legend\n // row hover (R55) feed the same branch.\n // Round 80 / Loop: vendor-letter hover composes ABOVE the\n // activeStatus branch so hovering the vendor chip's `C`\n // dims everything except C-vendor nodes. Same dim value\n // (0.28) as edge-endpoint and status filters — visually\n // consistent. Vendor lookup uses the same vendorForModel\n // helper the avatar render uses, keyed by initial so the\n // chip and the avatar always agree on grouping.\n opacity: hoveredEdgeEndpoints && !hoveredEdgeEndpoints.has(session.alias) && chatAlias !== session.alias\n ? 0.28\n : activeVendor && chatAlias !== session.alias && (() => {\n const v = vendorForModel(session.model);\n const initial = v.id === 'unknown' ? '?' : v.initial;\n return initial !== activeVendor;\n })()\n ? 0.28\n : activeStatus && chatAlias !== session.alias && !(\n activeStatus === 'working' ? session.status === 'working'\n : activeStatus === 'idle' ? (isOnline && session.status !== 'working')\n : /* offline */ !isOnline\n )\n ? 0.28\n : !inFocus\n ? 0.32\n : chatAlias === session.alias\n ? 1\n : isOnline ? 1 : 0.6,\n // Round 9 / Loop: stagger the anet-fade-in so the topology\n // reveals as a wave on first paint instead of one big pop.\n // Cap at 24 indices (≈600ms tail) so 50-node fleets still\n // finish revealing within a beat. CSS animation-delay only\n // applies during the keyframe — re-renders without a new\n // mount (same alias key) don't replay, so status changes\n // never trigger the stagger again.\n // Round 72 / Loop: in ring layout, stagger by tier radius\n // so the topology emanates from the hub outward — inner\n // tier at 0ms, middle at ~180ms, outer at ~360ms, offline\n // at ~540ms. A small within-tier offset (nodeIdx % 6 * 25)\n // adds variety so each ring rotates in instead of all-at-\n // once popping. Grid keeps R9's pure nodeIdx stagger.\n animationDelay: layout === 'ring'\n ? `${tierIdx * 180 + (nodeIdx % 6) * 25}ms`\n : `${Math.min(nodeIdx, 24) * 25}ms`,\n // Round 51 / Loop: hover micro-lift. The label already\n // lifts on group-hover (R26). The node body — circle,\n // avatar, status ring — stays planted, so the gesture\n // reads \"label moves, body doesn't\". Now the whole <g>\n // translates -2px when hovered: avatar + ring + label\n // move as one unit. 2 px is well inside the inter-row\n // gap (cellH headroom ≥22), and CSS transform on SVG\n // <g> never affects the overlap-test geometry (the\n // test never simulates hover). useReducedMotion drops\n // the lift to 0 for prefers-reduced-motion users.\n transform: !reducedMotion && hoveredAlias === session.alias ? 'translateY(-2px)' : undefined,\n transition: 'transform 180ms cubic-bezier(0.4,0,0.2,1)',\n }}\n // Stop the pointerdown from reaching the SVG pan handler: the\n // SVG calls setPointerCapture, and a captured pointer makes\n // Chromium fire the follow-up `click` on the SVG instead of\n // this node — so without this the node's onClick never runs.\n // Side effect (intended): you pan from empty canvas, not by\n // grabbing a node.\n onPointerDown={(e) => e.stopPropagation()}\n // Track hover globally (not just under denseLayout) so the\n // Round 8 group-focus fade works at any fleet size.\n onPointerEnter={() => setHoveredAlias(session.alias)}\n onPointerLeave={() => setHoveredAlias(prev => (prev === session.alias ? null : prev))}\n onClick={() => {\n setChatAlias(session.alias);\n // Round 14 ripple — capture position, radius and status\n // colour at click time so the one-shot circle is self-\n // contained (no re-render-time recomputation needed).\n setClickRipple({\n ts: Date.now(),\n x: pos.x, y: pos.y, r0: radius,\n color: status.primary,\n });\n setTimeout(() => setClickRipple(prev =>\n prev && Date.now() - prev.ts >= 590 ? null : prev), 600);\n }}\n >\n {/* Issue #96: native hover tooltip — \"Vendor · model · Runtime\".\n Falls back to just the alias when the node reports no\n model/runtime.\n Round 33 / Loop: cwd line answers \"what is this agent on?\".\n Round 34 / Loop: for offline nodes, append \"last seen: 6m ago\"\n so the operator knows whether to wait or chase. Online nodes\n skip the line (Round 27's 1 h ghost age-out means anything\n still online has heartbeated recently — the line would just\n be noise). Accepts both ISO (\"…T06:00:28Z\") and SQL-style\n (\"… 06:00:28\" assumed UTC) formats. */}\n {(() => {\n // Round 35 / Loop: TZ-safe parsing via the shared lib helper\n // — same parseHubTime is used by isGhost above (Round 38\n // factored it to app/lib/time.ts so both paths interpret SQL\n // bare timestamps as UTC on every browser).\n const lastSeen = !isOnline && session.last_seen_at ? relativeAgo(session.last_seen_at) : null;\n // Round 98 / Loop: enrich the node tooltip with status,\n // group membership, and inbound/outbound flow counts —\n // same info-density spirit as R97 active-filter pill\n // tooltips. Hovering any node now answers \"what is this\n // and how does it sit in the topology\" without forcing\n // a click into the chat popover. Group line only shows\n // when the alias is part of a multi-member band (R106\n // prefix grouping); singletons skip it. Flow line only\n // when at least one direction has count > 0.\n const groupKey = groupKeys[session.alias];\n const groupMembers = groupKey\n ? Object.values(groupKeys).filter(k => k === groupKey).length\n : 1;\n const groupLine = groupMembers > 1 ? `group: ${groupKey} · ${groupMembers}` : null;\n // R147 / Loop: node tooltip extends R98's flow summary\n // with the actual sender / receiver aliases. R98 told you\n // \"12 in / 5 out\" but not WHO. The R97 idiom — anywhere\n // the UI shows \"N\" should hover-explain WHICH N — applied\n // to filter pills, group labels, vendor letters, pressure\n // segments, recent-row text, active-links chip; the node\n // title was the last surface still showing only the\n // aggregate. Direction-tagged: senders are who's\n // messaging THIS node (inbound), receivers are who this\n // node is messaging (outbound). 6-truncate + \"+N more\"\n // matches the pattern other tooltips use so a 30-flow\n // node doesn't paint a 30-line tooltip.\n let flowIn = 0, flowOut = 0;\n const sendersMap = new Map<string, number>(); // alias → count, inbound\n const receiversMap = new Map<string, number>(); // alias → count, outbound\n for (const fl of flowLinks) {\n if (fl.from === session.alias) {\n flowOut += fl.count;\n receiversMap.set(fl.to, (receiversMap.get(fl.to) ?? 0) + fl.count);\n }\n if (fl.to === session.alias) {\n flowIn += fl.count;\n sendersMap.set(fl.from, (sendersMap.get(fl.from) ?? 0) + fl.count);\n }\n }\n const fmtPeers = (m: Map<string, number>) => {\n const pairs = [...m.entries()].sort((a, b) => b[1] - a[1]);\n const preview = pairs.slice(0, 6).map(([a, n]) => `${a} (${n})`).join(', ');\n const suffix = pairs.length > 6 ? ` + ${pairs.length - 6} more` : '';\n return preview + suffix;\n };\n const flowLine = (flowIn + flowOut) > 0 ? `flows: ${flowIn} in / ${flowOut} out` : null;\n const sendersLine = sendersMap.size > 0 ? `← from: ${fmtPeers(sendersMap)}` : null;\n const receiversLine = receiversMap.size > 0 ? `→ to: ${fmtPeers(receiversMap)}` : null;\n return (\n <title>{[\n `${session.alias} · ${session.status}`,\n identityLine(session.model, session.runtime),\n groupLine,\n session.project_dir ? `cwd: ${session.project_dir}` : null,\n lastSeen ? `last seen: ${lastSeen}` : null,\n flowLine,\n sendersLine,\n receiversLine,\n ].filter(Boolean).join('\\n')}</title>\n );\n })()}\n {/* Round 2 / Loop: hover ring — a thin outer stroke that fades\n in when the cursor enters the node, signalling clickability\n (real-user feedback for the chat-popover open). Pure CSS via\n Tailwind group-hover, so it costs nothing per frame and\n respects prefers-reduced-motion via the global media query.\n Round 489 / Loop — duration harmonized from 150ms → 200ms\n to join the Hero D #147 motion-coherence stack (R459-R475\n cluster surfaces + cadence-sync family). R2 originally\n picked 150ms for a \"snappier feel\" before the 200ms ease-\n out vocabulary was banked as the canvas-wide motion\n default. Bringing this ring into the family means hover-\n in / hover-out / cluster cadence / pip-strip transitions\n all settle on the same timing — the canvas now reads as\n one motion vocabulary instead of two competing tempos.\n 11th surface in the motion-coherence stack. */}\n <circle\n cx={pos.x}\n cy={pos.y}\n r={radius + 12}\n fill=\"none\"\n stroke={status.primary}\n // strokeWidth must NOT be 1.5 (offline status ring) or 3\n // (online status ring) — the overlap test selects by those\n // exact widths and would mis-count this invisible hover\n // ring as a node footprint.\n strokeWidth=\"2\"\n className=\"opacity-0 group-hover:opacity-70 transition-opacity duration-200\"\n style={{ pointerEvents: 'none' }}\n />\n {/* Round 11 / Loop: chat-focus ring — when the ChatPopover is\n open targeting this node, anchor a persistent ring around\n it so the floating popover visibly links back to its source\n node. Static (not pulsing) so it reads as \"selected state\"\n rather than \"this node is active\". strokeWidth=2.5 stays\n clear of the overlap-test selectors (1.5 / 3). Sits just\n outside the halo radius+8 so it never overlaps a neighbour\n (halos already pack flush in dense grids). */}\n {/* R51 chat-target ring. R120 / Loop: gentle SMIL\n breath on the ring's opacity (±0.1 over 3s) when\n chat is open + !reducedMotion. Says \"active session\n here\" continuously without animation noise — the\n ring only appears for one node at a time (the\n chatAlias), so it never competes with R84 hub\n busyness or R112 working halo for attention.\n\n Round 183 / Loop: 7th surface in the smooth-pin-\n mirror family. Pre-R183 the ring was conditionally\n mounted on `chatAlias === session.alias`; the\n className `transition-opacity duration-200` never\n fired because the element didn't exist before\n mount. Always-mounted now with opacity gated by\n isChat — the CSS transition fires cleanly on\n chat-close (smooth fade-out). The `<animate>`\n SMIL stays gated by `!reducedMotion && isChat`\n so it only runs for the active chat target; when\n chat is closed, SMIL unmounts and opacity reverts\n to attribute (0) → CSS transitions down smoothly.\n On chat-OPEN the SMIL takes over per spec, so the\n fade-in is snappier than the fade-out — acceptable\n because the user explicitly clicked the node.\n\n strokeWidth=2.5 is not a R51 sentinel value\n (sentinels are 1.5/3 inside g[data-node]), so the\n ring is invisible to the overlap-test selector\n even when always-mounted. */}\n {/* Round 242 / Loop: extend the chat-target ring's\n transition list to include stroke + filter.\n Pre-R242 only `opacity` eased (R183 200ms): a\n chat-target node going working → idle hard-\n flipped the ring's stroke colour (status.primary\n green → teal) in one frame even though the rest\n of the ring was a smooth presence. Filter (glow\n on cyber, none on light) also snapped on chat\n toggle AND on theme switch.\n\n Add `stroke 200ms ease-out` + `filter 200ms\n ease-out` so the colour and glow both ease at\n the same cadence as the opacity gate. Same\n idiom R167 (node status-ring) uses for\n coordinated colour-easing on status flip;\n R242 brings the chat-target ring up to that\n bar. */}\n {(() => {\n const isChat = chatAlias === session.alias;\n return (\n <circle\n cx={pos.x}\n cy={pos.y}\n r={radius + 14}\n fill=\"none\"\n stroke={status.primary}\n strokeWidth=\"2.5\"\n opacity={isChat ? (isLight ? 0.85 : 0.95) : 0}\n filter={!isLight && isChat ? 'url(#topo-glow)' : undefined}\n style={{ pointerEvents: 'none', transition: 'opacity 200ms ease-out, stroke 200ms ease-out, filter 200ms ease-out' }}\n data-chat-target-ring\n data-chat-target-active={isChat ? 'true' : 'false'}\n data-chat-target-breath={!reducedMotion && isChat ? 'on' : 'off'}\n >\n {!reducedMotion && isChat && (\n <animate\n attributeName=\"opacity\"\n values={isLight ? '0.72;0.95;0.72' : '0.82;1;0.82'}\n dur=\"3s\"\n repeatCount=\"indefinite\"\n />\n )}\n </circle>\n );\n })()}\n {/* Round 243 / Loop: active-node pulse (the breathing\n aura ring at r=radius+14 fill=status.primary, shown\n on nodes participating in a recent flow) gains TWO\n polishes:\n\n 1) Always-mount + opacity-gate wrapper <g>. Pre-\n R243 the circle conditionally mounted on\n isActive — when a node joined a flow, the pulse\n snap-appeared at radius+8 (first SMIL phase\n value), and when the flow stopped, snap-\n disappeared. R243 keeps the SMIL animation\n running continuously inside an opacity-gated\n <g>; isActive flips the WRAPPER opacity\n 1↔0 with a 300ms ease-out transition so the\n pulse fades in/out at its current phase\n instead of restarting from +8. The reduced-\n motion gate stays at the conditional render\n level — reduced-motion users see no pulse at\n all (no point without the animation).\n\n 12th surface in the always-mount-opacity-gate\n family (R181/R182/R183/R213×2/R214/R215/R221/\n R222/R223/R237/R243).\n\n 2) SMIL ease-in-out keySplines on both r and\n opacity animates. Pre-R243 default linear\n calcMode produced a constant-velocity breath\n (radius marched +8 → +22 at fixed dr/dt;\n opacity dimmed 0.12 → 0.02 at fixed dα/dt) —\n mechanical, not organic. calcMode='spline'\n + keyTimes='0;0.5;1' + per-segment keySplines\n '0.42 0 0.58 1' (canonical CSS ease-in-out)\n both ways gives a settled breath: slow at\n both endpoints (small and large radius / lit\n and dim opacity), fast through the middle.\n Same SMIL-easing family R227 / R228 already\n inhabits at the click ripple + edge ping +\n pulse. */}\n {!reducedMotion && (\n <g\n opacity={isActive ? 1 : 0}\n data-node-pulse={session.alias}\n data-node-pulse-active={isActive ? 'true' : 'false'}\n style={{ transition: 'opacity 300ms ease-out' }}\n >\n <circle cx={pos.x} cy={pos.y} r={radius + 14} fill={status.primary} opacity={isLight ? 0.08 : 0.12}>\n <animate\n attributeName=\"r\"\n values={`${radius + 8};${radius + 22};${radius + 8}`}\n dur=\"2.4s\"\n repeatCount=\"indefinite\"\n calcMode=\"spline\"\n keyTimes=\"0;0.5;1\"\n keySplines=\"0.42 0 0.58 1;0.42 0 0.58 1\"\n />\n {/* Round 409 / Loop: active-node pulse peak\n opacity lift — cyber 0.18 → 0.20 / light\n 0.12 → 0.14. Theme-consistency / canvas-\n presence family 9th anchor. R243 family\n rhythm preserved.\n Round 413 / Loop: trough lift mirrors R409\n peak — cyber 0.04 → 0.05 / light 0.02 →\n 0.03. Stale-state legibility lift family\n 8th anchor — pairs with R404 (hub-halo\n cyber trough 0.08 → 0.10) and R405 (light\n trough 0.32 → 0.34). The per-node breath's\n low-point now reads slightly above the\n \"nearly gone\" zone while preserving the\n breath amplitude (cyber Δ 0.16 vs Δ pre-\n R409+R413 of 0.14; light Δ 0.11 vs 0.10).\n Both peak (R409) AND trough (R413) lift\n together so the active-pulse signal stays\n confidently present at both ends of its\n 2.4s cycle.\n Stale-state legibility lift family (8):\n R317 subordinate-text gray-500→400\n R358 freshness floor 0.25→0.30\n R372 minimap offline-dot 0.5→0.6\n R404 hub-halo cyber trough 0.08→0.10\n R405 hub-halo light trough 0.32→0.34\n R406 edge freshness floor 0.35→0.40\n R407 node halo offline opacity (cyber+light)\n R413 active-node pulse trough (this round)\n cyber 0.04 → 0.05\n light 0.02 → 0.03\n R243 always-mount opacity-gate + R243\n ease-in-out keySplines + r animation\n (radius+8 ↔ radius+22) preserved.\n data-node-pulse-peak + new -pulse-trough\n attrs expose resolved per-theme values. */}\n <animate\n attributeName=\"opacity\"\n values={isLight ? '0.14;0.03;0.14' : '0.20;0.05;0.20'}\n dur=\"2.4s\"\n repeatCount=\"indefinite\"\n calcMode=\"spline\"\n keyTimes=\"0;0.5;1\"\n keySplines=\"0.42 0 0.58 1;0.42 0 0.58 1\"\n data-node-pulse-peak={isLight ? '0.14' : '0.20'}\n data-node-pulse-trough={isLight ? '0.03' : '0.05'}\n />\n </circle>\n </g>\n )}\n {/* Round 4 / Loop: transition-[fill,stroke,opacity] smooths\n status colour changes so idle↔working↔offline doesn't snap\n — task replies / node-rename / SSE updates ease in.\n Round 112 / Loop: working nodes get a subtle halo breath\n (±0.12 opacity at 3s cycle) so the eye can find \"what's\n busy\" at a glance without scanning chips. Idle + offline\n halos stay flat — they don't need to demand attention.\n R84 hub-center breath stays the loudest \"fleet busyness\"\n signal; this one is quieter, per-node. SMIL `<animate>`\n inside the circle, gated by reducedMotion. */}\n {/* Round 226 / Loop: working-halo breath gets per-node\n phase stagger. Pre-R226 every working node's halo\n pulsed in lockstep — all 0.73→0.92→0.73 starting at\n the same instant — which on a fleet of 4+ working\n agents reads as one mechanical metronome rather\n than an organic group of breathing entities.\n\n SMIL `<animate>` accepts negative `begin` to offset\n the cycle backwards in time (the animation starts\n mid-cycle on first paint). Using\n `(nodeIdx * 0.37) % 3` gives a deterministic,\n well-distributed offset across the 3s period —\n the same golden-ratio-ish 0.37 fraction R103 uses\n for particle phase stagger on edges. 0.37 doesn't\n line up for any small N (4 nodes → offsets 0,\n 0.37, 0.74, 1.11 — evenly spread, never\n coincident).\n\n Side benefit: when a new agent joins a busy fleet\n its halo phase is determined by its position in\n the order array, not \"when it joined\" — so a\n re-render doesn't reshuffle breath phases. Order\n is stable (R-onlineNodes sort), so the canvas\n feels calm rather than jittery on each refresh.\n\n Reduced-motion users skip the animate entirely\n (gate unchanged). Halo opacity transition on the\n parent stays for status flips. data-node-halo-\n breath-offset surfaces the chosen offset for\n test introspection. */}\n {/* Round 407 / Loop: offline node halo opacity lift —\n cyber 0.25 → 0.30 and light 0.4 → 0.45. Pre-R407\n offline node halos faded to α=0.25 cyber (75 %\n dim) / α=0.4 light. On the dark canvas the 0.25\n halo read as \"nearly gone\" — exactly the\n legibility floor R404/R405 just lifted on the\n hub-halo and R372 lifted on minimap offline dots.\n R407 closes the same family at the per-node halo\n surface: +0.05 lift on both themes so offline\n anchors stay legibly present without crossing into\n \"could be online\" territory (online cyber 0.65 /\n light 0.85 unchanged — the 0.30/0.65 cyber ratio\n still gives 2.17× contrast for online/offline).\n Stale-state legibility lift family (7 anchors now):\n R317 subordinate-text gray-500 → gray-400\n R358 freshness floor 0.25 → 0.30\n R372 minimap offline-dot 0.5 → 0.6\n R404 hub-halo cyber trough 0.08 → 0.10\n R405 hub-halo light trough 0.32 → 0.34\n R406 edge freshness floor 0.35 → 0.40\n R407 node halo offline opacity (this round)\n cyber 0.25 → 0.30\n light 0.4 → 0.45\n R278 retired-breath gate + R12 status.halo color\n + R226 phase stagger code-path preserved (the\n breath stays disabled per Vincent's R278 ask;\n only the BASE opacity floor shifts here). transi-\n tion list ('fill,opacity' 300ms ease-out) unchanged.\n data-node-halo-offline-opacity attr exposes the\n resolved value for tests. */}\n {(() => {\n /* Round 440 / Loop: node halo opacity hover lift —\n lifts toward full on the matched node. Pure paint\n axis: rest values unchanged for un-hovered halos,\n hover state lifts the matched halo's alpha by\n +0.15 on each tier:\n online cyber 0.65 → 0.80\n online light 0.85 → 1.00 (capped)\n offline cyber 0.30 → 0.45\n offline light 0.45 → 0.60\n Same paint-only mental model as R430 hub-spoke\n opacity lift + R429 label-card body opacity lift,\n now at the per-node halo scope. No geometry\n change so R51 sentinels stay safe and the overlap-\n test invariant is unchanged (test runs at rest).\n Closes a chroma/presence axis on the per-node\n hover signature alongside the 12-layer cue stack\n (R26/R217/R142/R427/R428/R429 card + R430/R435/\n R436/R437/R94 link + R438 ring). R407 offline\n halo opacity floor (cyber 0.30 / light 0.45) is\n the rest branch unchanged. Existing transition-\n [fill,opacity] duration-300 className handles\n the easing. data-node-halo-hovered exposes the\n gate; data-node-halo-resolved-opacity exposes\n the four-state resolved value for tests. */\n const isHaloHovered = !reducedMotion && hoveredAlias === session.alias;\n /* Round 456 / Loop: light-theme offline node halo\n rest opacity 0.45 → 0.50. Stale-state legibility\n lift family extension (10th anchor) at the per-\n node halo light-theme scope:\n R317 subordinate-text gray-500 → gray-400\n R358 freshness floor 0.25 → 0.30\n R372 minimap offline-dot 0.5 → 0.6\n R404 hub-halo cyber trough 0.08 → 0.10\n R405 hub-halo light trough 0.32 → 0.34\n R406 edge freshness floor 0.35 → 0.40\n R407 node halo offline opacity\n cyber 0.25 → 0.30\n light 0.4 → 0.45\n R419 hub-spoke idle 0.45 → 0.50\n R452 dense alias rest 0.9 → 0.95\n R456 node halo offline LIGHT 0.45 → 0.50 ← this round\n Pre-R456 light-theme offline halo at 0.45 sat at\n the upper end of \"near-floor\" but read as soft-\n focus on the lighter canvas; +0.05 (~11 % opacity\n gain) lifts it to 0.50 — the midpoint between\n R407 rest 0.45 and R440 hover 0.60 — closing the\n gap so offline halos read more confidently as\n present-but-stale anchors. Cyber theme stays at\n R407's 0.30 (cyber backdrop is dark; the cyber\n offline halo against #080814 contains a stronger\n contrast envelope than light, so doesn't need\n the same lift). R440 hover 0.45→0.60 light + R12\n status.halo color + R407 transition list all\n preserved. */\n const haloOpacity = (() => {\n if (isOnline) {\n return isLight ? (isHaloHovered ? 1 : 0.85) : (isHaloHovered ? 0.80 : 0.65);\n }\n return isLight ? (isHaloHovered ? 0.60 : 0.50) : (isHaloHovered ? 0.45 : 0.30);\n })();\n return (\n <circle\n cx={pos.x}\n cy={pos.y}\n r={radius + 8}\n fill={status.halo}\n opacity={haloOpacity}\n data-node-halo-offline-opacity={isOnline ? undefined : (isLight ? 0.45 : 0.30)}\n data-node-halo-hovered={isHaloHovered ? 'true' : 'false'}\n data-node-halo-resolved-opacity={haloOpacity}\n className=\"transition-[fill,opacity] duration-300 ease-out\"\n data-node-halo-breath={!reducedMotion && session.status === 'working' ? 'on' : 'off'}\n data-node-halo-breath-offset={\n !reducedMotion && session.status === 'working'\n ? ((nodeIdx * 0.37) % 3).toFixed(3)\n : undefined\n }\n >\n {/* Round 278 / Loop: per-node working halo breath\n (R112+R226+R244 family) RETIRED per Vincent\n 5214/5215-5217 simplification ask (减法 cut #4\n after R275 chip-row, R276 orbit, R277 legend).\n\n The breath was: each working agent's halo pulses\n 0.73→0.92→0.73 (cyber 0.53→0.78→0.53) at 3 s\n cycle, R226-staggered per-node, R244-eased. For\n a 4-working fleet that's 4 simultaneous SMIL\n breaths competing with the hub-halo breath\n (R244 hub) for the \"fleet busyness\" visual\n signal.\n\n The signal is info-redundant: the hub-halo\n breath ALREADY conveys \"the network is alive\n and busy\"; per-node halo breath duplicates it\n at 4× volume. Plus working nodes are ALREADY\n distinguished by their halo color (status.halo\n green/teal/slate via R12 trio) — the static\n halo carries identity, the moving breath was\n decorative motion on top.\n\n R278 gates the SMIL animate with `false &&` so\n the code remains for rollback. Halo opacity\n stays at the BASE (non-breathing) values via\n the parent circle's `opacity` attr (0.85/0.65/\n 0.4/0.25 from R12 + isOnline gate). Working\n nodes still show green halos; they just don't\n pulse.\n\n Net: -4 SMIL animations on canvas for typical\n 4-working fleet. Combined with R276 orbit\n retirement (-4) and the hub halo breath kept\n as the SOLE \"fleet busyness\" motion signal,\n the canvas reads quieter. R226 + R244 per-node\n stagger / ease constants are dead code post-\n R278 (acceptable — family retires together). */}\n {false && !reducedMotion && session.status === 'working' && (\n <animate\n attributeName=\"opacity\"\n values={isLight ? '0.73;0.92;0.73' : '0.53;0.78;0.53'}\n dur=\"3s\"\n begin={`-${((nodeIdx * 0.37) % 3).toFixed(3)}s`}\n repeatCount=\"indefinite\"\n calcMode=\"spline\"\n keyTimes=\"0;0.5;1\"\n keySplines=\"0.42 0 0.58 1;0.42 0 0.58 1\"\n />\n )}\n </circle>\n );\n })()}\n {/* Round 111 / Loop: edge-endpoint emphasis ring. R49\n already keeps endpoint nodes at opacity 1 while\n others dim when an edge is hovered, but the\n endpoints had no POSITIVE indicator — they just\n \"stayed bright\". An accent stroke at r=radius+7\n (just inside the halo's r=radius+8 bbox so we\n don't grow the overlap footprint) clearly says\n \"these are the two participants in this flow\".\n pointerEvents:none so the node hitbox stays alive.\n\n Round 182 / Loop: the ring used to mount/unmount\n on every edge hover, snapping despite the\n opacity transition on the style. Always-mount\n with opacity gated by hoveredEdgeEndpoints — same\n pattern R181 used for the legend pin ring. The\n transition now actually fires on hover entry\n and exit. 6th surface in the smooth-pin-mirror\n family (R165/R180/R181 + this round).\n\n strokeWidth=1.6 (was 1.5) deliberately escapes\n the R51 overlap-test sentinel `circle[stroke-\n width=\"1.5\"]`: an always-mounted r=radius+7 ring\n inside g[data-node] would otherwise be picked\n before the actual status ring (r=radius) by\n querySelector document order, breaking the\n test's node-bbox read. 1.5 → 1.6 is visually\n imperceptible (6.7% thicker) but the exact-\n string CSS attribute selector no longer\n matches. */}\n {(() => {\n const isEndpoint = hoveredEdgeEndpoints && hoveredEdgeEndpoints.has(session.alias);\n /* Round 233 / Loop: endpoint ring picks up a stroke-\n width thicken on edge-hover, completing the hover-\n elevation gesture across the whole edge surface.\n Pre-R233 hovering an edge eased the visible path\n stroke (R166) and lifted the badge r (R164) — but\n the two endpoint rings only faded IN (R182\n opacity gate). Now they ALSO thicken 1.6 → 2.4 on\n hover, in 180ms ease-out matching R164 badge lift.\n The endpoint nodes feel like they \"rise to meet\"\n the edge as the cursor approaches it, instead of\n just appearing.\n\n 1.6 and 2.4 both escape the R51 overlap-test\n sentinels (1.5 / 3 are reserved) — 2.4 sits\n comfortably between, visually 50% thicker than\n baseline so the gesture reads but the radius is\n unchanged (still r=radius+7) so geometry stays\n calm and the topo-overlap-test stays green. 9th\n surface in the hover-elevation family (R51\n nodes / R135 panels / R142 group boxes / R143-\n R144 rows / R164 edge badges / R177 hub ring /\n R229 group-label count brighten / R233 endpoint\n ring stroke-width). data-edge-endpoint-ring-\n stroke-width attr surfaces the chosen value for\n test introspection. */\n /* Round 442 / Loop: endpoint emphasis ring radius\n hover lift — r=radius+7 → radius+8 on isEndpoint,\n closing a 3-axis hover-elevation parity at endpoint\n ring scope (r + sw + opacity):\n opacity R182 0 → 0.85/0.9\n sw R233 1.6 → 2.4\n r R442 +7 → +8 ← this round\n Mirrors the 3-axis trios already established at\n hub hover-ring (R177/R370/R385) and edge badge\n (R164/R394/R395). Pre-R442 the endpoint ring\n faded in + thickened on edge-hover but its radius\n stayed locked at radius+7 — only the paint/weight\n axes lifted while the GEOMETRY stayed unchanged.\n +1px (~radius+7 to radius+8) gives a subtle outward\n pulse on hover without crowding the status ring\n (which sits at radius from R438 sw3.5 hover) or\n the halo (radius+8 from R440 opacity hover — the\n endpoint ring sits at the SAME radius as the halo\n but with stroke=cyan vs fill=status.halo so they\n don't visually collide). The transition list\n extends to include 'r 180ms ease-out' so the new\n axis eases under the same R233 cadence. SVG `r`\n on a <circle> uses CSS-property syntax for inter-\n polation (same idiom R197/R198 used on the\n legend swatch). data-edge-endpoint-ring-radius\n attr exposes the resolved value for tests. */\n const endpointR = isEndpoint ? radius + 8 : radius + 7;\n return (\n <circle\n cx={pos.x}\n cy={pos.y}\n fill=\"none\"\n stroke={pal.flowEdge}\n strokeWidth={isEndpoint ? 2.4 : 1.6}\n opacity={isEndpoint ? (isLight ? 0.9 : 0.85) : 0}\n data-edge-endpoint-ring\n data-edge-endpoint-active={isEndpoint ? 'true' : 'false'}\n data-edge-endpoint-ring-stroke-width={isEndpoint ? 2.4 : 1.6}\n data-edge-endpoint-ring-radius={endpointR}\n style={{\n pointerEvents: 'none',\n r: `${endpointR}px`,\n transition: 'opacity 180ms ease-out, stroke-width 180ms ease-out, r 180ms ease-out',\n } as React.CSSProperties}\n />\n );\n })()}\n {/* Round 167 / Loop: extend the node status-ring\n transition to include stroke-width — symmetric\n with R165 (pressure-bar width) and R166 (edge\n stroke-width). Pre-R167 only fill+stroke colors\n transitioned smoothly; stroke-width snapped from\n 3 (online) to 1.5 (offline) when a session\n transitioned. With stroke-width in the transition\n list the ring smoothly contracts as a node goes\n offline (or expands when it comes back).\n strokeDasharray stays binary (none ↔ '5 5')\n because dash values don't interpolate cleanly\n between continuous and discrete forms.\n Inline style replaces the Tailwind transition-\n [fill,stroke] className for stable arbitrary\n property compilation. Respects prefers-reduced-\n motion via R29 globals.css blanket override.\n data-node-status-ring exposes this circle for\n test probing — the overlap-test guard on\n stroke-width=\"3\"/\"1.5\" still works against the\n DOM attribute value (React-rendered, not\n interpolated). */}\n {(() => {\n /* Round 438 / Loop: status ring strokeWidth hover lift —\n when hoveredAlias matches, the node's status ring\n thickens by +0.5: online 3 → 3.5, offline 1.5 → 2.\n Same absolute delta as R435 hub-spoke (idle 1→1.25\n used Δ +0.25 because rest base was thinner; status\n ring's heavier rest values 1.5/3 need a bigger\n +0.5 to register as visible thickening).\n Status-ring axis joins the node-hover cue stack\n (now 9 layers including link surfaces):\n R26 group translateY -2px per-node geometry\n R217 stroke tint legendAccent per-node card\n R142 drop-shadow boost per-node card\n R427 alias letter-spacing per-node text\n R428 sub-text letter-spacing per-node text\n R429 body opacity 0.94 → 1.0 per-node card\n R430 hub-spoke α+ link to hub (paint)\n R435 hub-spoke sw+ link to hub (geo)\n R94 edge α 1.7× inter-node link (paint)\n R436 edge sw 1.15× inter-node link (geo)\n R437 flow-rail sw 1 → 1.5 edge paint-layer\n R438 status-ring sw +0.5 ring geometry ← this round\n R51 sentinel safety: rest values 3 / 1.5 unchanged\n so the overlap-test selector `circle[stroke-width=\n \"3\"]` / `circle[stroke-width=\"1.5\"]` inside\n g[data-node] still matches at rest. Hover values\n 3.5 / 2 are not in the reserved {1.5, 3} set so\n the selector wouldn't match them anyway; but the\n test runs WITHOUT hover so this never matters\n in practice. R167 stroke-width 300ms transition\n already in the style list eases the lift for\n free. data-node-status-ring-hovered exposes the\n gate for tests. */\n const isRingHovered = !reducedMotion && hoveredAlias === session.alias;\n const ringStrokeWidth = isOnline\n ? (isRingHovered ? 3.5 : 3)\n : (isRingHovered ? 2 : 1.5);\n return (\n <circle\n cx={pos.x}\n cy={pos.y}\n r={radius}\n fill={isOnline ? pal.nodeFill.online : pal.nodeFill.offline}\n stroke={status.primary}\n strokeWidth={ringStrokeWidth}\n strokeDasharray={isOnline ? 'none' : '5 5'}\n filter={isOnline && !isLight ? 'url(#topo-glow)' : undefined}\n data-node-status-ring={status.label}\n data-node-status-ring-hovered={isRingHovered ? 'true' : 'false'}\n data-node-status-ring-stroke-width={ringStrokeWidth}\n style={{\n transition: 'fill 300ms ease-out, stroke 300ms ease-out, stroke-width 300ms ease-out',\n }}\n />\n );\n })()}\n {/* v0.10.0 Hero 1+2 / §3.F server-health node-ring tint.\n When the host server this agent runs on is in the\n `red` tier (CPU/Mem/Disk worst-of ≥ 85% per\n classifyServer threshold), draw a faint amber outer\n halo at radius+8. strokeWidth=2.5 stays clear of the\n R51 overlap-test sentinels (1.5 = offline status ring,\n 3 = online status ring). pointerEvents:none so the\n halo can't intercept node clicks. Falls back silent\n when host telemetry hasn't shipped yet (hostHealthMap\n is empty until commhub returns telemetry). Composes\n with R209 hover-ring (which sits BETWEEN the avatar\n and this halo on hover — different radii). */\n }\n {(() => {\n const tier = hostHealthMap.get(session.server);\n if (tier !== 'red') return null;\n return (\n <circle\n cx={pos.x}\n cy={pos.y}\n r={radius + 8}\n fill=\"none\"\n stroke={isLight ? '#d97706' : '#fbbf24'}\n strokeWidth=\"2.5\"\n opacity=\"0.6\"\n data-node-server-health=\"red\"\n data-node-server-host={session.server}\n style={{\n pointerEvents: 'none',\n transition: 'stroke 200ms ease-out, opacity 200ms ease-out',\n }}\n />\n );\n })()}\n {/* Issue #96: node \"avatar\" is now driven by the model\n vendor. Decision order:\n 1. ?brand=intern flag, or an intern-aliased node with\n no model field → 书生 coin (preserves #79).\n 2. vendor has a packaged logo asset → that logo image\n (intern-s1-* models land here via vendorForModel).\n 3. known vendor, logo asset not shipped yet → a\n vendor-tinted monogram (spec-mandated fallback).\n 4. unknown vendor / null model → the prefix-group\n hue-hashed initial (#83/#99 behaviour, unchanged). */}\n {(() => {\n const ar = Math.round((isOnline ? 14 : 10) * nodeScale);\n const size = radius * 2;\n const vendor = vendorForModel(session.model);\n const internByAlias = /书生|书小生|intern/i.test(session.alias);\n\n if (isIntern || internByAlias || vendor.logo) {\n return (\n <image\n href={vendor.logo ?? '/intern_avatar.png'}\n x={pos.x - size / 2}\n y={pos.y - size / 2}\n width={size}\n height={size}\n preserveAspectRatio=\"xMidYMid meet\"\n />\n );\n }\n if (vendor.id !== 'unknown') {\n // Known model house, logo asset not in public/vendors/\n // yet — vendor-tinted monogram stands in.\n /* Round 283 / Loop: monogram circle strokeWidth bumps\n 1 → 1.5 per Vincent 5216 \"书生头像风格延续 — 其他\n vendor 头像 plain text/abbreviation 比书生差, polish\n 升级\". Without real vendor logo PNG/SVG assets in\n public/vendors/, the monogram is the visual stand-\n in; bumping its ring weight from 1 to 1.5 narrows\n the visual-quality gap with the 书生 image avatar\n (which is a designed PNG, naturally more\n substantial). The 1px → 1.5px stroke is the same\n weight increment R268 used on the chrome-strip\n border unification — small but perceptible. The\n prefix-group fallback (line ~5172) stays at\n strokeWidth=1 since that's for UNKNOWN vendors\n where less visual weight signals \"we don't know\n what this is\" appropriately. */\n return (\n <>\n <circle cx={pos.x} cy={pos.y} r={ar} fill={vendor.mono.bg} stroke={vendor.mono.ring} strokeWidth=\"1.5\" />\n {/* Round 284 / Loop: known-vendor monogram letter\n swaps fontFamily monospace → system sans-serif.\n Continuation of R283 Vincent 5216 \"vendor 头像\n polish 升级\". A single centered letter does not\n benefit from monospace's digit-alignment\n property — its only effect at this scale is to\n land a slightly thinner, more code-text-like\n glyph. The system stack ('-apple-system',\n 'BlinkMacSystemFont', 'Segoe UI', 'Inter',\n 'sans-serif') picks the OS-preferred designed\n sans-serif, which renders a fuller, more\n \"badge-mark\" letterform — narrowing the\n visual-quality gap with the 书生 PNG (which is\n a hand-designed image). data-monogram-letter\n exposes the element for test probing.\n\n The prefix-group fallback at line ~5197\n INTENTIONALLY stays on monospace — same\n contrast pattern R283 established for ring\n stroke: \"designed glyph\" = known vendor,\n \"code text\" = unknown vendor bucket. */}\n <text\n x={pos.x} y={pos.y} dy=\"0.34em\" textAnchor=\"middle\"\n fill={vendor.mono.text} fontSize={ar}\n fontFamily=\"-apple-system, BlinkMacSystemFont, 'Segoe UI', Inter, sans-serif\"\n fontWeight=\"700\"\n data-monogram-letter={vendor.initial}\n >\n {vendor.initial}\n </text>\n </>\n );\n }\n // Round 106 (issue #83): hue keyed to the prefix group,\n // not the full alias — every 通信* node shares one color.\n const c = aliasAvatarColors(groupKeys[session.alias] || session.alias);\n return (\n <>\n <circle cx={pos.x} cy={pos.y} r={ar} fill={c.bg} stroke={c.ring} strokeWidth=\"1\" />\n <text\n x={pos.x}\n y={pos.y}\n dy=\"0.34em\"\n textAnchor=\"middle\"\n fill={c.text}\n fontSize={ar}\n fontFamily=\"monospace\"\n fontWeight=\"700\"\n >\n {aliasInitial(session.alias)}\n </text>\n </>\n );\n })()}\n {/* Issue #96: runtime badge — small corner glyph marking the\n execution shell (CLI / SDK / HTTP API). Sits bottom-right\n of the avatar; colours kept off the working/idle/offline\n status hues. Absent when the node reports no runtime. */}\n {(() => {\n const rt = runtimeIdentity(session.runtime);\n if (!rt) return null;\n const br = isOnline ? 7 : 5.5;\n const bx = pos.x + radius * 0.72;\n const by = pos.y + radius * 0.72;\n const icon = br * 2 * 0.62;\n // Round 208 / Loop: runtime badge joins the micro-lift\n // radius-axis family. R177 grew the hub ring on hover\n // (r 14→17); R197 grew the legend swatch (r 5.5→7);\n // R208 closes the trio at per-node grain — the runtime\n // badge (CLI/SDK/HTTP indicator at avatar bottom-right)\n // pops r 7→8 (online) / 5.5→6.5 (offline) when the\n // parent node is hovered, with stroke 1.5→2 for\n // matching emphasis. R26 already lifts the label 1.5px\n // and R194 elevates its drop-shadow; R208 gives the\n // runtime indicator its own hover acknowledgement so\n // every per-node surface participates in the gesture.\n // CSS r-as-property + stroke-width are transitionable\n // (same support matrix R197/R198/R199 leveraged:\n // Chrome ≥95 / Safari ≥16 / FF ≥70). data-runtime-\n // badge-active exposes the gate for tests.\n const isNodeActive = !reducedMotion && hoveredAlias === session.alias;\n return (\n <g style={{ pointerEvents: 'none' }}>\n <circle\n cx={bx} cy={by} r={br}\n fill={pal.containerBg}\n stroke={rt.color}\n strokeWidth=\"1.5\"\n data-runtime-badge={session.alias}\n data-runtime-badge-active={isNodeActive ? 'true' : 'false'}\n style={{\n r: isNodeActive ? `${br + 1}px` : `${br}px`,\n strokeWidth: isNodeActive ? '2px' : '1.5px',\n transition: 'r 150ms ease-out, stroke-width 150ms ease-out',\n } as React.CSSProperties}\n />\n {/* Round 443 / Loop: runtime badge inner-icon\n strokeWidth lift on node hover — 2.4 → 2.8 on\n isNodeActive. Pre-R443 the outer badge ring\n lifted (R208 r + sw both grow on hover) but\n the inner icon path stayed locked at sw=2.4.\n The two layers of the runtime badge were\n out of phase: ring thickened, icon stayed\n thin. R443 closes the 2-axis hover signature\n on the badge so both ring and icon lift\n together. +0.4 absolute delta matches the\n R208 ring's +0.5 sw delta (badge ring 1.5 →\n 2.0 absolute), proportional to the icon's\n heavier base of 2.4. Pure paint axis;\n strokeLinecap='round' + strokeLinejoin='round'\n preserved. transition list extends to include\n 'stroke-width 150ms ease-out' matching R208\n outer-ring cadence. data-runtime-badge-icon\n + -active attrs exposed for tests. */}\n <g transform={`translate(${bx - icon / 2} ${by - icon / 2}) scale(${icon / 24})`}>\n <path\n d={rt.iconPath}\n fill=\"none\"\n stroke={rt.color}\n strokeWidth={isNodeActive ? '2.8' : '2.4'}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n data-runtime-badge-icon={session.alias}\n data-runtime-badge-icon-active={isNodeActive ? 'true' : 'false'}\n data-runtime-badge-icon-stroke-width={isNodeActive ? '2.8' : '2.4'}\n style={{ transition: 'stroke-width 150ms ease-out' }}\n />\n </g>\n </g>\n );\n })()}\n {/* Round 294 / Loop: per-node \"working\" pulse dot retired.\n The pulse was R24's per-node working indicator — a\n small green circle at the top of each working node,\n SMIL-animated opacity 1→0.25→1. After R278 retired the\n working halo, R279 retired arrival ping + dispatch\n pulse, R280 retired backdrop spokes, the pulse dot\n was the last surviving per-node SMIL animation in\n the original \"working = breathing\" visual family.\n Status info is preserved through 4 redundant signals:\n status ring green color (R167), label sub-text\n 'working' (R211), chip-row 'X working' count (top\n of canvas), hub centre digit (R130). With 30+\n working nodes on a real fleet, 30 simultaneous SMIL\n pulses add cognitive load with zero new information.\n Same R275-R281/R290/R291 减法 family idiom — the\n last 'wiggling per-node decoration' retires. Gated\n via `{false && ...}` per the R276/R278/R279/R280\n rollback-friendly pattern; the block stays in the\n file documented + dead-coded so future readers see\n the retired pulse-dot rationale + can A/B-restore\n it by flipping the gate. */}\n {false && (() => {\n const sse = sseCountFor ?? 0;\n const dur = sse >= 4 ? '0.7s' : sse >= 2 ? '0.9s' : '1.2s';\n const visible = session.status === 'working';\n return (\n <g\n data-pulse-wrapper={session.alias}\n data-pulse-visible={visible ? 'true' : 'false'}\n style={{\n opacity: visible ? 1 : 0,\n transition: 'opacity 300ms ease-out',\n pointerEvents: 'none',\n }}\n >\n <circle cx={pos.x} cy={pos.y - (radius - 6)} r=\"2.5\" fill={pal.flowParticle} data-pulse-dur={dur} opacity={reducedMotion ? 0.6 : undefined}>\n {!reducedMotion && (\n <animate attributeName=\"opacity\" values=\"1;0.25;1\" dur={dur} repeatCount=\"indefinite\" />\n )}\n </circle>\n </g>\n );\n })()}\n\n {/* Round 98 (issue #61): label rect 124px → 100px.\n Round 109/110 (Vincent P0): full opaque card below the\n density threshold / on hover / when zoomed; otherwise a\n lightweight plain-text alias that keeps every node\n labelled without an opaque box covering its neighbours.\n Round 15 / Loop: when nodeScale=S the node shrinks 30%\n but the label card was staying full-size, so the label\n visually outweighed the small node. Tighten the card\n frame, alias / sub fontSize, drop-offset and truncate\n length specifically for S; M and L keep their existing\n sizes (M ≈ L for labels by design — the S user is the\n one who explicitly asked for a denser view). */}\n {(() => {\n const isSmall = nodeScale < 0.8;\n const cardW = isSmall ? 88 : 100;\n const cardH = isSmall ? 36 : 42;\n const cardTopY = isSmall ? -12 : -14;\n const aliasFs = isSmall ? 11 : 12;\n const subFs = isSmall ? 8 : 9;\n const subY = isSmall ? 15 : 17;\n const dropY = isSmall ? 18 : 22;\n const fullMax = isSmall ? 11 : 12;\n const denseFs = isSmall ? 9 : 10;\n const denseDrop = isSmall ? 12 : 14;\n // Round 26 / Loop: micro-lift the label group on hover —\n // 1.5 px upward, 200 ms ease. Pairs with the Round 18\n // group-box hover-accent treatment to give the same\n // \"this is the focused element\" feedback at the per-\n // node level. CSS transform stacks onto the SVG\n // positioning transform attribute (SVG 2 cascade);\n // bbox is unchanged at rest, so the overlap-test gate\n // continues to see the geometric layout it expects.\n return showFullLabel ? (\n <g transform={`translate(${pos.x}, ${pos.y + radius + dropY})`} style={{ pointerEvents: 'none' }}\n className=\"transition-transform duration-200 group-hover:-translate-y-[1.5px]\">\n {/* Round 194 / Loop: label card picks up a subtle\n drop-shadow that intensifies when the node is\n hovered — pairs the existing R26 1.5px lift\n with physical weight. Pre-R194 the card lifted\n 1.5 px on hover but had no shadow follow, so\n the gesture read as \"card translated\" rather\n than \"card rose off the canvas\". Adding a\n baseline shadow at rest (1px/2px blur) plus a\n deeper hover state (3-4px/8-12px blur) makes\n the elevation feel earned.\n Same R57/R135 hover-elevation idiom that\n panels already use, now extended to per-node\n label cards. data-node-label-card-elevation\n ('idle'/'hover') exposes the state for tests.\n Filter is theme-aware (light: slate alpha;\n dark: black alpha) so the shadow stays visible\n against both surfaces. transition: filter\n 220ms ease-out matches R135's panel-elevation\n duration so a hovered node + a hovered panel\n fade at the same rhythm. Reduced-motion users\n collapse to the rest-state shadow only — no\n hover differentiation. Per-node filter is\n gated to showFullLabel which itself is gated\n to non-dense fleets (≤16 nodes) or hovered/\n zoomed-in branches, so the cost stays bounded\n (~20-30 cards max). */}\n {/* Round 217 / Loop: label card stroke tints to\n legendAccent (cyan) on parent-node hover,\n adding a 3rd hover-feedback channel alongside\n R26 1.5px lift + R194 drop-shadow elevation.\n The card now responds at three layers when its\n parent node is hovered: lift (motion) → shadow\n (depth) → stroke (color tint). All three are\n gated by the same hoveredAlias === session\n .alias state so they ease in unison. transition\n list extends `stroke 220ms ease-out` alongside\n R194's existing filter 220ms — single pair of\n eyes on the card reads \"this is the focused\n element\" via three independent channels. Pin\n + chat states don't compete: pinning a node\n opens chat (R136) but doesn't drive hovered\n Alias, so this stroke tint is exclusively a\n pointer-on-target signal. */}\n {/* Round 246 / Loop: label card chrome picks up\n fill + opacity in its transition list. R142\n already eased filter (drop-shadow) + stroke\n (R217 cyan tint on hover); the rect's fill\n (pal.labelBox.fill: cyber #020617 ↔ light\n #ffffff) and theme-derived opacity (0.94\n cyber / 1 light) still snapped on theme\n toggle. R211 already closed the alias/sub\n text-fill snap on the same card; R246\n closes the chrome-fill snap on the rect\n BEHIND that text, so the whole card\n (background + text) transitions as one\n unit through every theme switch. Same\n 220ms cadence the existing filter/stroke\n pair uses — coordinated 4-property easing\n across the card. */}\n {/* Round 411 / Loop: node label card rx 6 → 8.\n Pre-R411 the per-node label card painted at\n rx=6, sitting one tier BELOW the R332/R375/\n R376 compact-chrome tier (rx=8). Inside the\n corner-radius cascade family the cards used\n to be the only \"smaller\" tier — but the\n label card is a content-bearing surface\n (alias + sub text + ring), not a sub-\n element decoration. R411 lifts rx=6 → 8\n to align with the compact-chrome / segmented-\n control tier so all \"compact card\" surfaces\n read with the same corner radius.\n Corner-radius cascade (8 anchors now):\n R330 canvas rx 12 (root)\n R331 panels rx 10 (recent-signal, legend)\n R332 minimap container rx 8 (compact chrome)\n R375 Layout-toggle rx 8 (segmented control)\n R376 nodeSize/zoom rx 8 (segmented control)\n R390 hover-detail rx 10 (panel)\n R393 minimap viewport rx 2 (sub-element)\n R411 node label card rx 6 → 8 (compact card, this round)\n Pure paint — rx grows the corner curve\n inward without changing the card's outer\n cardW × cardH bbox (cardW=92/cardH=22 for\n standard nodes per R23 / R187 sizing). R217\n hover-stroke cyan tint + R142 drop-shadow\n + R246 fill+opacity 220ms transition list\n + R211 alias/sub text-fill ease all\n preserved. data-node-label-card-rx attr\n exposes the value for tests. */}\n {/* Round 429 / Loop: node label-card body opacity\n 0.94 → 1.0 on hover (cyber theme). Sibling\n treatment to R348 panel rect opacity lift —\n 0.92 → 0.97 cyber / 0.97 → 1.0 light at the\n panel scope. Pre-R429 the cyber theme card\n sat at 0.94 always; on hover R217 tinted the\n stroke + R142 grew the drop-shadow + R26\n lifted the group + R427/R428 spaced the text\n but the rect itself never solidified —\n the card glowed brighter through the\n shadow but the body alpha gap (6 pct) stayed\n fixed. R429 lifts the body to full alpha on\n hover so the card reads as a confidently\n present surface under the cursor (matching\n the panel-pair pattern). Light theme stays\n at 1.0 in both states (already maxed). R246\n transition list already covers opacity 220ms\n so the lift eases for free. R217 stroke tint\n + R142 drop-shadow + R211 fill ease all\n preserved (additive opacity branch only). */}\n <rect\n x={-cardW / 2} y={cardTopY} width={cardW} height={cardH} rx=\"8\"\n fill={pal.labelBox.fill}\n stroke={!reducedMotion && hoveredAlias === session.alias\n ? pal.legendAccent\n : pal.labelBox.stroke}\n opacity={\n !reducedMotion && hoveredAlias === session.alias\n ? 1\n : (isLight ? 1 : 0.94)\n }\n data-node-label-card={session.alias}\n data-node-label-card-rx=\"8\"\n data-node-label-card-elevation={\n !reducedMotion && hoveredAlias === session.alias ? 'hover' : 'idle'\n }\n style={{\n filter: !reducedMotion && hoveredAlias === session.alias\n ? (isLight\n ? 'drop-shadow(0 3px 8px rgba(15,23,42,0.20))'\n : 'drop-shadow(0 4px 12px rgba(0,0,0,0.60))')\n : (isLight\n ? 'drop-shadow(0 1px 2px rgba(15,23,42,0.08))'\n : 'drop-shadow(0 1px 2px rgba(0,0,0,0.30))'),\n transition: 'filter 220ms ease-out, stroke 220ms ease-out, fill 220ms ease-out, opacity 220ms ease-out',\n }}\n />\n {/* Round 211 / Loop: alias + sub text fill eases on\n status flip, matching R167 status-ring fill 300ms.\n Pre-R211 a node going working → idle → offline made\n the ring smoothly recolor (R167) while the label\n card's text snap-cut to the new tier hue in a\n single frame — the node \"transitioned its ring,\n flipped its text\". 300ms inline transition syncs\n all four label-card fills (alias, sub, ring fill,\n ring stroke) to the same beat so the node reads\n as one coordinated status change.\n data-node-alias-text exposes the gate for tests. */}\n {/* Round 305 / Loop: node alias label text picks\n up the pin-signature letter-spacing family\n (R219 / R220) when the node is the chat\n target. The alias is the per-node identity\n label inside the label card; when chat is\n open targeting this node, R242 already adds\n a cyan-tint stroke to the card. R305 brings\n the alias text into the same pin-signature\n family — letter-spacing 0px → 0.5px when\n chatAlias === session.alias. Same vocabulary\n R219 established for recent-row text (line\n ~6354), legend-row text (~6881), and the\n R220 edge-badge text (~4327, with 0.4 for\n hot/pin). Now the per-node alias has its\n own pin signature when chat is open on it.\n transition list extends 'letter-spacing\n 200ms ease-out' so it eases alongside the\n existing 300ms fill transition. */}\n {/* Round 427 / Loop: extend the node alias label\n letter-spacing family to a 3-tier scale —\n rest 0px → hover 0.3px → chat-target 0.5px.\n Pre-R427 the alias text shifted only when\n chat was actively pinned (R305); pure node-\n hover left the text dead-typographic while\n the surrounding card lifted (R26 translateY\n + R242 stroke + filter cues). R427 adds the\n missing typographic axis to the hover gesture\n so the alias text rises with the card.\n The chat-target tier still wins (0.5 > 0.3)\n so the pin signature stays at the top of the\n scale — hover is the mid tier between rest\n and chat-target.\n Hover-letter-spacing family extension:\n R344 chip count digit\n R345 panel title (R423 sibling)\n R347 active-links chip\n R351 vendor chip\n R420 zoom-level chip\n R427 node alias text (this round)\n R211 fill 300ms + R305 letter-spacing 200ms\n transition list preserved; only the\n conditional gets a middle case. */}\n <text\n x=\"0\" y=\"1\" textAnchor=\"middle\"\n fill={status.text}\n fontSize={aliasFs} fontFamily=\"monospace\" fontWeight=\"700\"\n data-node-alias-text={session.alias}\n data-node-alias-chat-target={chatAlias === session.alias ? 'true' : 'false'}\n data-node-alias-hovered={hoveredAlias === session.alias ? 'true' : 'false'}\n style={{\n transition: 'fill 300ms ease-out, letter-spacing 200ms ease-out',\n letterSpacing:\n chatAlias === session.alias ? '0.5px' :\n hoveredAlias === session.alias ? '0.3px' : '0px',\n }}\n >\n {truncate(session.alias, fullMax)}\n </text>\n {/* Round 428 / Loop: node sub-text (status label\n line beneath the alias) adopts hover letter-\n spacing tween 0 → 0.2px on hoveredAlias.\n Sibling treatment to R427 alias-text hover\n tween (0 → 0.3) — the alias is the primary\n identity (top-tier kerning 0.3), the sub-text\n is the secondary status line (one tier lower\n at 0.2). Now both lines of the label card\n telegraph hover typographically as one unit,\n matching the R26 card lift + R242 stroke\n tint + R975 filter cues. Subtler delta on\n the sub-text (0.2 vs alias 0.3) preserves\n the alias > status visual hierarchy at the\n hover scope. R211 fill 300ms transition\n preserved (additive letter-spacing branch\n + appended 'letter-spacing 200ms ease-out'). */}\n {/* Round 448 / Loop: node sub-text fontWeight\n 400 → 500 (font-medium). Sibling to R363\n (recent-row text fw 400→500) + R364 (legend-\n row label fw 400→500) — same \"small mono\n text at fontSize=9-11 needs 500-tier weight\n for legibility\" pattern, now applied to the\n per-node sub-text line. At fontSize=8-9\n monospace against the label-card chrome\n (pal.labelBox.fill cyber #020617 / light\n #ffffff), the default fw=400 sits at the\n legibility floor; fw=500 (font-medium) lifts\n it into a clearly readable band without\n changing geometry. R211 fill 300ms +\n R428 letter-spacing 0→0.2 hover + R427\n alias-text + R429 body opacity all preserved.\n Pure typography lift; no layout shift; the\n alias-text fw=700 (R427) still wins so the\n alias > status hierarchy holds at the type\n level. data-node-sub-text-font-weight attr\n exposes the value for tests. */}\n <text\n x=\"0\" y={subY} textAnchor=\"middle\"\n fill={status.primary}\n fontSize={subFs} fontFamily=\"monospace\"\n fontWeight=\"500\"\n data-node-sub-text={session.alias}\n data-node-sub-text-hovered={hoveredAlias === session.alias ? 'true' : 'false'}\n data-node-sub-text-font-weight=\"500\"\n style={{\n transition: 'fill 300ms ease-out, letter-spacing 200ms ease-out',\n letterSpacing: hoveredAlias === session.alias ? '0.2px' : '0px',\n }}\n >\n {status.label}{isOnline && sseCountFor != null ? ` sse:${sseCountFor}` : ''}\n </text>\n </g>\n ) : (\n // Round 212 / Loop: dense plain-text alias gets fill\n // transition on status flip — extension of R211. Pre-\n // R212 the dense fallback (denseLayout > 16 nodes,\n // where label cards collapse to plain text + R110\n // stroke halo) snap-cut its fill on tier change while\n // the status ring smoothly transitioned (R167) — the\n // card-mode equivalent that R211 just fixed at the\n // ≤16-node grain. Inline transition list combines\n // R26 transform 200ms (group-hover lift) + R212 fill\n // 300ms (status flip ease) — Tailwind transition-\n // transform on className would be displaced by inline\n // transition, so the transform property is explicit\n // in the inline list too. The group-hover:-translate-\n // y-[1.5px] className still fires the transform via\n // CSS pseudo-class; only the transition-property\n // moves to inline. Big fleets benefit most — this is\n // the path users see when their dashboard is busiest.\n /* Round 452 / Loop: dense plain-text alias rest\n opacity 0.9 → 0.95. Closes the alpha gap on the\n dense fleet's per-node label, sibling to R449\n legend-count-active 0.95→1.0 and R450 minimap\n viewport rest 0.9→0.95 — same \"close the\n active-presence alpha gap\" idiom applied here\n to the dense-mode alias text at fontSize=9-10\n monospace. Pre-R452 dense aliases at α=0.9 sat\n just below full alpha; for un-hovered nodes in\n a busy >16-node fleet this is the only label\n readable, so the 10% alpha gap added a subtle\n \"soft-focused chrome\" feel where the labels\n should read as definitive. +0.05 lift makes\n them confidently present without erasing the\n status.text + R110 stroke halo + paintOrder\n layering. R26 group-hover translate + R212\n fill 300ms transition + R110 stroke=container-\n Bg halo all preserved. data-node-dense-alias-\n text-opacity attr exposes the resolved value\n for tests. */\n <text\n x={pos.x}\n y={pos.y + radius + denseDrop}\n textAnchor=\"middle\"\n fill={status.text}\n fontSize={denseFs}\n fontFamily=\"monospace\"\n fontWeight=\"700\"\n opacity={0.95}\n className=\"group-hover:-translate-y-[1.5px]\"\n data-node-dense-alias-text={session.alias}\n data-node-dense-alias-text-opacity=\"0.95\"\n style={{\n pointerEvents: 'none',\n paintOrder: 'stroke',\n transition: 'transform 200ms ease-out, fill 300ms ease-out',\n }}\n stroke={pal.containerBg}\n strokeWidth=\"3\"\n >\n {truncate(session.alias, isSmall ? 9 : 10)}\n </text>\n );\n })()}\n {/* v0.10.0 Hero 3 Wave 1 §3.E — hover detail card.\n Renders an extended-info SVG card next to the\n hovered node showing vendor / model / runtime /\n server fields that don't fit in the compact label\n card. Position flips to the left when the node is\n in the right half of the canvas so the card\n doesn't extend past the viewBox right edge. Only\n one card is visible at any time (gated on\n hoveredAlias === session.alias), so layout cost\n stays bounded.\n Reuses pal.labelBox.fill / pal.legendAccent for\n chrome consistency with the existing label card +\n legend panel. data-topo-hover-detail attribute\n exposes the element for test probes.\n Not rendered in dense layout (>16 nodes) — same\n gate as showFullLabel; dense fleets already have\n too much per-node chrome competing. */}\n {!reducedMotion && hoveredAlias === session.alias && !denseLayout && (() => {\n const v = vendorForModel(session.model);\n const rt = runtimeIdentity(session.runtime);\n const flipLeft = pos.x > VIEWBOX_W * 0.65;\n const detailW = 192;\n const detailH = 88;\n const detailX = flipLeft ? pos.x - radius - 18 - detailW : pos.x + radius + 18;\n const detailY = pos.y - detailH / 2;\n return (\n <g transform={`translate(${detailX}, ${detailY})`} data-topo-hover-detail={session.alias} style={{ pointerEvents: 'none' }}>\n {/* Round 387 / Loop: hover-detail panel cyber backdrop\n opacity 0.94 → 0.97. The hover-detail card is\n ALWAYS rendered in active-hover context (it IS\n the hover product), so it should carry the\n same backdrop weight as the R348 recent-signal /\n legend panel HOVER state (which lifts 0.92 →\n 0.97 cyber). Pre-R387 the card sat at 0.94\n cyber, leaving a 0.03 alpha gap against the\n R348 panel-hover state — small but visible\n when the hover-detail floats next to a hovered\n recent-signal panel. R387 unifies them at 0.97\n so all active-hover panels paint with the same\n confident backdrop opacity in cyber. Light\n stays at 0.98 (already at the strong end —\n R348 light also stays at 0.97/0.98 max).\n Theme-consistency / canvas-presence polish\n family (5th anchor):\n R370 hub hover-ring opacity 0.7 → 0.8 cyber\n R371 edge-badge rest opacity 0.82 → 0.85 cyber\n R372 minimap offline-dot opacity 0.5 → 0.6\n R386 hub-highlight idle opacity 0.9 → 0.95\n R387 hover-detail panel opacity 0.94 → 0.97 cyber (this round)\n data-topo-hover-detail-opacity attr exposes\n the resolved value for tests. R348 drop-shadow\n + rx=8 + stroke=pal.legendAccent + fill=pal.\n labelBox.fill all preserved. */}\n {/* Round 390 / Loop: hover-detail card rx 8 → 10.\n Corner-radius cascade family — the hover-detail\n card is a panel-tier surface (192×88 floating\n info card with drop-shadow + stroke), so its\n corner radius should match the R331 panel tier\n (rx=10) used by the recent-signal and legend\n panels. Pre-R390 it shared rx=8 with the R332\n minimap and R375/R376 segmented-control tier\n (Layout-toggle, nodeSize, zoom wrappers),\n which is the \"compact chrome control\" tier —\n a tier mismatch for a content-bearing panel.\n Corner-radius cascade (6 anchors now):\n R330 canvas rx 12 (root)\n R331 panels rx 10 (recent-signal, legend)\n R332 minimap rx 8 (compact chrome)\n R375 Layout-toggle rx 8 (segmented control)\n R376 nodeSize/zoom rx 8 (segmented control)\n R390 hover-detail rx 10 (panel — this round)\n Pure paint change; no layout shift (rx grows\n the corner curve INWARD without changing the\n card's outer bbox). data-topo-hover-detail-\n rx attr exposes the resolved value for tests.\n R348 drop-shadow + stroke + R387 opacity all\n preserved. */}\n <rect\n x=\"0\" y=\"0\" width={detailW} height={detailH} rx=\"10\"\n fill={pal.labelBox.fill}\n stroke={pal.legendAccent}\n opacity={isLight ? 0.98 : 0.97}\n data-topo-hover-detail-opacity={isLight ? 0.98 : 0.97}\n data-topo-hover-detail-rx=\"10\"\n 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))' }}\n />\n <text x=\"10\" y=\"16\" fontSize=\"9\" fontFamily=\"monospace\" fill={pal.legendAccent} fontWeight=\"700\">\n {v.id !== 'unknown' ? v.label : '—'}\n </text>\n {/* Round 389 / Loop: hover-detail model line (y=32)\n fontWeight 400 → 600. R388 lifted body lines\n (runtime/host/task at fontSize=9) to fw=500;\n R389 closes the typography hierarchy by giving\n the model name (the dominant subhead text in\n the card) its own weight tier. Three-tier\n ladder now reads cleanly:\n vendor fontSize=9 fw=700 (label badge)\n model fontSize=10 fw=600 (subhead — this round)\n body 3× fontSize=9 fw=500 (R388)\n One tier step per dimension (size + weight)\n between adjacent levels — classic editorial\n hierarchy idiom adapted to a 192×88 SVG card.\n Sibling to the chip-internal-hierarchy arc\n (R333-R341/R362/R369) which uses fw=600/500\n for digit/unit pairs; R389 applies the same\n fw=600 to a content-bearing identity line.\n data-topo-hover-detail-model-fw attr exposes\n the resolved value for tests. pal.legendHeadline\n fill preserved (R389 doesn't touch color). */}\n <text x=\"10\" y=\"32\" fontSize=\"10\" fontFamily=\"monospace\" fontWeight=\"600\" fill={pal.legendHeadline} data-topo-hover-detail-model-fw=\"600\">\n {session.model || 'model · pending'}\n </text>\n {/* Round 388 / Loop: hover-detail body lines (the\n three fontSize=9 lines: runtime, host, task)\n gain fontWeight=500. Small-text fw lift family\n (6th anchor) — fontSize 9-10 px text reads\n consistently bolder at fw=500 than at the\n default 400 weight at small sizes, especially\n on the cyber-theme backdrop where stroke-\n rendering is the limiting factor.\n Sibling lifts in this family:\n R363 recent-row alias text 400 → 500\n R364 legend-row label 400 → 500\n R366 group-label count tspan 400 → 500\n R368 +N more flows footer 400 → 500\n R373 pressure-bar kicker (font-medium)\n R388 hover-detail body lines 400 → 500 (this round)\n Tier structure preserved:\n y=16 vendor (fw=700, headline)\n y=32 model (fontSize=10, subhead by size)\n y=48 runtime / y=64 host / y=80 task (body, now fw=500)\n The y=80 task line keeps opacity=0.7 so its\n caption-tier identity stays distinct from the\n y=48 / y=64 body lines despite shared fw.\n data-topo-hover-detail-body-fw attr exposes\n the resolved value for tests. */}\n <text x=\"10\" y=\"48\" fontSize=\"9\" fontFamily=\"monospace\" fontWeight=\"500\" fill={pal.legendText} data-topo-hover-detail-body-fw=\"500\">\n {rt ? rt.label : 'runtime · pending'}\n </text>\n <text x=\"10\" y=\"64\" fontSize=\"9\" fontFamily=\"monospace\" fontWeight=\"500\" fill={pal.legendText} data-topo-hover-detail-body-fw=\"500\">\n host · {session.server || 'unknown'}\n </text>\n <text x=\"10\" y=\"80\" fontSize=\"9\" fontFamily=\"monospace\" fontWeight=\"500\" fill={pal.legendText} opacity=\"0.7\" data-topo-hover-detail-body-fw=\"500\">\n {session.task ? truncate(session.task, 28) : 'no recent task'}\n </text>\n </g>\n );\n })()}\n </g>\n );\n })}\n\n {/* Round 14 / Loop: click ripple — one-shot expanding ring from\n the clicked node, ~500ms, status-coloured. Sits inside the\n zoom/pan <g> so it scales / pans with the topology. Keyed by\n ts so a re-click on any node (same or different) remounts the\n <circle> and the SMIL <animate> elements replay from t=0.\n strokeWidth=2 doesn't match the overlap-test selectors. */}\n {/* Round 227 / Loop: click-ripple SMIL gets ease-out curve.\n Pre-R227 both <animate>s ran with default calcMode=linear,\n which made the ripple grow at constant velocity from\n r0+4 → r0+30 over 500ms — a mechanical \"expansion at\n uniform rate\" feel that didn't match the rest of the\n topology's interaction vocabulary (every CSS transition\n on hover-lift, status-flip, pin-signature uses\n `ease-out`). On click, the ripple is the user's primary\n \"I did that\" confirmation feedback — it should feel\n fast-then-settle like a real pressure wave, not metric.\n\n calcMode=\"spline\" + keyTimes=\"0;1\" + keySplines=\"0.25 0.1\n 0.25 1\" maps directly onto CSS cubic-bezier(0.25, 0.1,\n 0.25, 1), the canonical ease-out curve. SMIL's keySplines\n uses the same 4 control-point convention as CSS but\n space-separated. Applied to BOTH the r and opacity\n <animate> elements so they ease in lockstep — the ring\n decelerates as it expands and fades, the two motions\n together feeling like one organic pulse.\n\n One change reaches three click surfaces — hub center\n (R52), node body (R14), edge midpoint badge (R185) — all\n reuse this single ripple element via the shared\n setClickRipple state. data-click-ripple attr surfaces\n the element for test introspection; calcMode attribute\n on the <animate> reflects the ease-out adoption. */}\n {clickRipple && !reducedMotion && (\n <circle\n key={clickRipple.ts}\n cx={clickRipple.x}\n cy={clickRipple.y}\n r={clickRipple.r0 + 4}\n fill=\"none\"\n stroke={clickRipple.color}\n strokeWidth=\"2\"\n opacity=\"0\"\n data-click-ripple\n style={{ pointerEvents: 'none' }}\n >\n <animate\n attributeName=\"r\"\n values={`${clickRipple.r0 + 4};${clickRipple.r0 + 30}`}\n dur=\"0.5s\"\n calcMode=\"spline\"\n keyTimes=\"0;1\"\n keySplines=\"0.25 0.1 0.25 1\"\n fill=\"freeze\"\n />\n {/* Round 403 / Loop: click-ripple SMIL initial opacity\n 0.7 → 0.8. Pre-R403 the ripple's opacity animation\n faded from 0.7 to 0 over 500ms, providing a clean\n click-feedback pulse. Theme-consistency / canvas-\n presence polish family (R370 hub hover-ring +\n R391 hub-spoke active) already lifted paired\n hover-state alphas from 0.7 → 0.8. R403 brings\n click-feedback into that same alpha — three canvas\n state-feedback indicators (hover-ring, active spoke,\n click ripple) now share a uniform 0.8 start alpha\n so the visual \"I responded\" signal carries the\n same weight regardless of which state fired it.\n Pre-R403 invariants preserved: 500ms duration,\n R227 calcMode='spline' + ease-out keySplines\n (0.25 0.1 0.25 1), fill='freeze', concurrent r\n animation. Theme-consistency family (8 anchors):\n R370 hub hover-ring 0.7 → 0.8\n R371 edge-badge rest 0.82 → 0.85 cyber\n R372 minimap offline-dot 0.5 → 0.6\n R386 hub-highlight idle 0.9 → 0.95\n R387 hover-detail panel 0.94 → 0.97 cyber\n R391 hub-spoke active 0.7 → 0.8\n R392 minimap online-dot 0.9 → 0.95\n R403 click-ripple start 0.7 → 0.8 (this round)\n data-click-ripple-start-opacity attr exposes the\n resolved value for tests. */}\n <animate\n attributeName=\"opacity\"\n values=\"0.8;0\"\n dur=\"0.5s\"\n calcMode=\"spline\"\n keyTimes=\"0;1\"\n keySplines=\"0.25 0.1 0.25 1\"\n fill=\"freeze\"\n data-click-ripple-start-opacity=\"0.8\"\n />\n </circle>\n )}\n\n </g>\n\n {/* #112: overlay panels (recent-signal + legend) render OUTSIDE the\n zoom/pan <g> so they stay fixed while the topology pans/zooms.\n They're sized + tucked into the top corners so every corner of\n each panel is >325px from the canvas centre — i.e. fully outside\n the outermost (offline) ring. No node on any ring can reach the\n corner triangles, so the panels never overlap a node, in ring\n OR grid layout (Vincent 4727 zero-overlap criterion). */}\n {/* latest flow labels */}\n {/* Round 57 / Loop: drop-shadow on the panel rects gives them\n card-like elevation, especially on light theme where the\n near-white fill on a near-white canvas read as pasted-on.\n data-topo-panel-elevation tag so the test can verify both\n panels carry the filter. The filter is on the rect, not\n the parent <g>, so it doesn't shadow the rows + text inside\n — only the panel chrome lifts.\n\n v0.10.0 Hero 3 Wave 1 / RFC §3.C (Vincent 5222 holdover):\n hide recent-signal panel when there's no flow to show.\n Pre-v0.10.0 the panel always-mounted with a \"no flow yet\n · send a message between agents\" placeholder. On a fresh\n fleet that's a full corner of chrome doing nothing —\n exactly the always-mount-stack 5222 calls out. Render the\n panel only when flowLinks actually has rows. R175 fade-in\n still applies — first flow that arrives still eases in.\n Composes with §3.I canvas-corner watermark (only shows\n when this panel is absent). */}\n {flowLinks.length > 0 && (\n <g\n transform=\"translate(16, 16)\"\n data-topo-panel=\"recent\"\n data-topo-panel-hovered={hoveredPanel === 'recent' ? 'true' : 'false'}\n // Round 175 / Loop: corner panels fade-in after the\n // R9/R72/R172/R173/R174 canvas content reveal. Pre-R175\n // recent-signal + legend panels appeared instantly in\n // the corners while nodes/edges/group boxes staggered\n // in around them — felt like 'panels are already\n // there, content shows up'. Delaying the panels to\n // ~700ms (after the first node wave finishes ~540ms\n // and edges begin filling in ~280ms) makes them drop\n // into place AFTER the canvas has revealed.\n // recent-signal panel at 700ms; legend at 800ms below\n // for a soft left-then-right cascade. .anet-fade-in\n // is the same R3 mount-once animation the other 4\n // wave layers use — fifth surface in the family.\n className=\"anet-fade-in\"\n data-topo-panel-fade-delay={700}\n style={{ animationDelay: '700ms' }}\n onMouseEnter={() => setHoveredPanel('recent')}\n onMouseLeave={() => setHoveredPanel(prev => prev === 'recent' ? null : prev)}\n >\n {/* Round 331 / Loop: recent-signal panel rect rx 8 → 10\n for proportional corner-radius rhythm after R330\n bumped the outer canvas wrapper to rounded-xl (12 px).\n Pre-R331 the panel sat at rx=8 (matching the legacy\n rounded-lg wrapper envelope); now it follows the\n wrapper one tier down:\n outer wrapper rounded-xl 12 px (R330)\n inner SVG panels rx=10 10 px (R331)\n inner detail card rx=8 8 px (codex 8f981a9)\n node label card rx=6 6 px (legacy R63)\n Geometry-safe: rx changes paint only, not bbox; the\n topo-overlap-test reads bbox geometry. Sibling change\n at the legend panel rect below (~line 6914) keeps\n the two corner panels symmetric. */}\n <rect\n x=\"0\" y=\"0\" width=\"230\" height=\"88\" rx=\"10\"\n fill={pal.legendBox.fill}\n // Round 423 / Loop: panel rect stroke tints to legendAccent\n // (cyan) on hover — sibling to R217 label-card stroke\n // hover-tint at the panel scope. Pre-R423 the panel rect\n // stroke painted pal.legendBox.stroke (neutral) regardless\n // of hover state, while every other panel hover cue stacked:\n // R135 drop-shadow boost\n // R348 rect opacity 0.92 → 0.97 cyber\n // R345 title letter-spacing 0.3 → 0.4\n // R423 rect stroke → legendAccent (this round)\n // Four hover layers now telegraph \"you're entering this\n // panel\" through structural, paint, and typographic axes\n // simultaneously. R247 transition list already covers\n // stroke 200ms ease-out so the tint eases naturally.\n // Sibling change at the legend panel rect below.\n stroke={hoveredPanel === 'recent' ? pal.legendAccent : pal.legendBox.stroke}\n opacity={hoveredPanel === 'recent' ? (isLight ? 1 : 0.97) : (isLight ? 0.97 : 0.92)}\n style={{\n /* R135: drop-shadow intensifies on panel hover. Base\n shadow (2px / 6px blur) signals card elevation\n (R57); hovered (4px / 12px blur) tells the user\n the whole chrome is interactive territory — rows\n pin (R116), footer navigates (R133), legend rows\n pin status (R61). Reuses R18's KPI-card hover-\n elevation idiom for visual consistency. Theme-\n aware shadow colour stays the same; just the\n spread + blur grow.\n\n Round 247 / Loop: extend the transition list to\n include fill + stroke + opacity at 200ms. R135\n already eased filter (hover drop-shadow); the\n three theme-driven properties (pal.legendBox.fill\n cyber #020617 ↔ light #ffffff, pal.legendBox.\n stroke cyber #1f2937 ↔ light #e2e8f0, opacity\n 0.92 ↔ 0.97) still snapped on theme toggle. Same\n per-element 4-property easing R246 added to the\n per-node label card chrome — now applied at the\n panel scope so the whole panel (background + chrome\n + shadow) eases as one unit through theme switches. */\n filter: hoveredPanel === 'recent'\n ? (isLight ? 'drop-shadow(0 4px 12px rgba(15,23,42,0.14))'\n : 'drop-shadow(0 4px 12px rgba(0,0,0,0.65))')\n : (isLight ? 'drop-shadow(0 2px 6px rgba(15,23,42,0.08))'\n : 'drop-shadow(0 2px 6px rgba(0,0,0,0.45))'),\n transition: 'filter 200ms ease-out, fill 200ms ease-out, stroke 200ms ease-out, opacity 200ms ease-out',\n }}\n data-topo-panel-elevation=\"recent\"\n />\n {/* Round 266 / Loop: panel title fill picks up theme-toggle\n transition. Pre-R266 the title \"recent signal\" had\n fill={pal.legendHeadline} (cyber #e5e7eb ↔ light\n #0f172a) without any inline transition — so the BIGGEST\n text in the recent-signal panel (fontSize 12 fontWeight\n 700) hard-flipped color on theme toggle while the panel\n rect (R247) and every row inside (various) eased.\n Sibling treatment to the legend panel title at line\n ~6195 — the panel-pair's titles now ease together. */}\n {/* Round 301 / Loop: panel titles get letterSpacing=\"0.3\"\n for editorial parity with R289 watermark letterSpacing\n + R285 kicker tracking-widest. At fontSize 12 monospace\n fontWeight 700, default 0px letter-spacing reads as a\n code-style label; 0.3px gives it a touch of designed-\n header register without changing the lowercase\n terminal-style aesthetic. Sibling treatment applied\n to recent-signal panel title (here) and legend panel\n title (line ~6556) — both panels share the same\n editorial-text-spacing convention. data-recent-panel-\n title handle unchanged so R266 test still resolves. */}\n {/* Round 345 / Loop: recent-signal panel title gains\n letter-spacing tween 0.3 → 0.4 on panel hover.\n hoveredPanel === 'recent' is set by the panel <g>\n wrapper's onMouseEnter (line ~6263 area). Sibling to\n R344 hover-letter-spacing applied to the +N more\n flows footer — same gesture vocabulary at a panel-\n title scope: hovering the panel chrome spreads the\n title 0.1 px, signalling \"this is a coherent unit\n you're entering\". transition list extends letter-\n spacing 200ms ease-out alongside existing fill 200ms.\n Round 482 / Loop — add 2nd typographic axis to the\n title: fontWeight 700 → 800 on activeEdgeKey (any\n row hover OR pin propagates from hoveredEdgeKey ??\n pinnedEdgeKey). Pre-R482 the title only responded\n to panel-chrome hover via R345 ls; when a specific\n row was hovered/pinned inside the panel, the title\n stayed flat. R482 closes the gap: when ANY row is\n active inside the panel, the title tightens\n typographically alongside the row's own R143 lift +\n R472 tint + R474 text spread. data tightens under\n attention pattern extension (panel-scope variant\n following R416/R424/R425/R426/R444/R445/R446/R457\n at the chip / panel / hub / edge / count / parent-\n label tiers).\n transition list extends to include 'font-weight\n 200ms ease-out' alongside R345's ls + R55's fill\n 200ms. data-recent-panel-title-fw exposes the\n resolved weight for tests. */}\n <text x=\"13\" y=\"21\" fill={pal.legendHeadline} fontSize=\"12\" fontFamily=\"monospace\" fontWeight={activeEdgeKey ? '800' : '700'} letterSpacing={hoveredPanel === 'recent' ? '0.4' : '0.3'} style={{ transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out, font-weight 200ms ease-out' }} data-recent-panel-title data-recent-panel-title-fw={activeEdgeKey ? '800' : '700'} data-recent-panel-title-active={activeEdgeKey ? 'true' : 'false'}>recent signal</text>\n {/* R96: header count now matches what the rows show. Pre-R96\n this read \"X msgs\" off the raw messages array, but the\n rows below render DEDUPED flowLinks — so a fleet with 10\n messages aggregating to 3 pairs read \"10 msgs\" above\n only 3 rows. Misreads as \"where are the other 7?\".\n \"X flows\" mirrors flowLinks.length one-for-one. When\n flows < msgs the chip-row's \"N active links · last 2s\"\n already tells the operator about traffic volume — no\n duplicate metric needed here.\n R129 / Loop: header gains an amber \" · N hot\" tail\n when ≥ 1 flowLink has count ≥ 10. The third surface\n of the hot-lane convention (R126 canvas badge / R127\n row count) lives at the panel header so a user\n scanning vertically — header → rows — gets a top-\n level summary before reading each row's amber digit.\n Restructured into a single <text> with <tspan>\n fragments so the amber portion can carry its own\n fill + weight without a sibling <text>. Switched\n anchor x=150 left-justified → x=217 right-justified\n so the count column unifies visually with the legend\n panel's right-justified header (line 3511 — also\n fontSize 10 monospace, also x≈215 textAnchor end).\n data-recent-panel-count stays on the flow tspan so\n the R96 / R128 tests still resolve. data-recent-\n panel-hot-count exposes the hot bucket count. */}\n {(() => {\n const hotFlowCount = flowLinks.filter(l => l.count >= 10).length;\n const hotStroke = isLight ? '#d97706' : '#fbbf24';\n // R162 / Loop: freshness tint on the panel-header\n // count tspan. R161 just colored the chip-row's \"N\n // active links · last 5s\" bullet by recency; the\n // recent-signal panel header is the panel-side\n // mirror of the same metric (flowLinks.length). Both\n // scopes now speak the same freshness vocabulary,\n // so a glance at either tells the operator whether\n // the network is firing right now. Four nested\n // scopes share one ladder:\n // canvas edge fade (R10)\n // row pip (R160)\n // chip bullet (R161)\n // panel header (R162, this round)\n // Same alpha ramp:\n // ageSec ≤ 30 → 1.0 (fully fresh)\n // 30-300s → smooth decay 1.0 → 0.25\n // > 300s → 0.25 stale floor\n // Hot tail (amber \" · N hot\" R129) is independent\n // of recency and keeps its own color — recency\n // tints the head; volume colors the tail.\n const recentMs = flowLinks.reduce<number | null>((acc, l) => {\n if (!l.last_at) return acc;\n const t = Date.parse(l.last_at);\n if (Number.isNaN(t)) return acc;\n return acc === null || t > acc ? t : acc;\n }, null);\n const ageSec = recentMs !== null\n ? Math.max(0, (Date.now() - recentMs) / 1000)\n : 999;\n const alpha = ageSec <= 30\n ? 1\n : ageSec <= 300\n ? 1 - ((ageSec - 30) / 270) * 0.70 /* R358: floor 0.25 → 0.30 lift across 3 freshness scopes */\n : 0.30; /* R358: stale floor lifted 0.25 → 0.30 — 20% legibility bump while preserving fresh/stale ratio */\n // Dark cyan-400 / light teal-600 with alpha — same\n // palette as R161's chip bullet so the two scopes\n // visually align even side-by-side.\n const freshFill = isLight\n ? `rgba(13, 148, 136, ${alpha.toFixed(2)})`\n : `rgba(34, 211, 238, ${alpha.toFixed(2)})`;\n return (\n <text\n x=\"217\" y=\"21\"\n textAnchor=\"end\"\n fontSize=\"10\"\n fontFamily=\"monospace\"\n // Round 349 / Loop: editorial letter-spacing 0.2 on the\n // recent-signal panel header count. Sits one tier below\n // the R301 panel title letterSpacing=\"0.3\" so the panel\n // header reads as a 2-step hierarchy (title 0.3 / count\n // 0.2). Sibling change on the legend panel count below\n // closes the panel-pair editorial symmetry. Joins the\n // R285 / R289 / R301 / R302 / R304 / R325 editorial-\n // letterspacing tier at the panel-summary scope. The\n // R162 freshness fill, R225 tabular-nums, R311 fw=600,\n // R336 unit-tspan opacity-0.7 split all preserved —\n // the tier propagates to all descendant tspans via\n // SVG inheritance. data-recent-panel-count-letter-\n // spacing exposes the value for tests.\n letterSpacing=\"0.2\"\n data-recent-panel-count-letter-spacing=\"0.2\"\n >\n {/* Round 225 / Loop: tabular-nums on the panel-header\n flow-count tspan. The \"{N} flows\" string lives in\n a right-justified text anchor (x=217 textAnchor=\n 'end') so the BASELINE of the numeral is the same\n regardless of digit-count — but the SPACING between\n the digit and ' flows' label is monospace-jittery\n in the 1-digit → 2-digit boundary, and the ' · N\n hot' R190 tail that hangs off the end shifts by\n whatever the digit width delta is. Tabular-nums\n locks both, so the header reads stable through\n 9 flows → 10 flows growth. Sibling treatment to\n R224 edge badge / R225 hub digit. */}\n {/* Round 311 / Loop: recent-signal panel count tspan\n picks up fontWeight=600 for sibling parity with\n R310 legend panel count. Closes the panel-pair\n count typography symmetry — both top-corner\n panels now have:\n title fontWeight=700 (panel chrome anchor)\n count fontWeight=600 + tabular-nums (data)\n Same digit-semibold rule R309 established for\n per-row counts now applied to BOTH panel-summary\n counts. The R162 freshness fill (1.0→0.25 alpha\n ramp) and R225 tabular-nums all preserved; only\n the weight bumps. */}\n {/* Round 336 / Loop: split the digit from the unit\n word \" flows\" with a nested tspan at opacity=0.7.\n Same chip-internal-hierarchy pattern R333 (vendor\n count suffix) + R335 (filter pin prefix) applied\n to one chip — recurring \"small label spans demote,\n value stays prominent\" idiom at the panel-header\n count scope. The digit stays fw=600 + tabular-nums\n (R311 + R225 inheritance via the outer tspan);\n the unit tspan inherits fw=600 but adds opacity\n 0.7. Reads as \"5 (prominent) / flows (recessive\n unit)\". data-recent-panel-count attribute stays\n on the OUTER tspan so existing R311 fontWeight\n tests + count value reads still resolve via\n .textContent. data-recent-panel-count-unit on\n the inner unit tspan for R336 introspection. */}\n {/* R424 — recent-signal panel count digit fontWeight\n 600 → 700 on panel hover. Closes the 5-layer panel\n hover cue stack with a typographic-weight axis at\n the panel-header data scope: depth (R135 drop-\n shadow) + solidity (R348 fill opacity) + spacing\n (R345 title letter-spacing) + edge color (R423\n stroke tint) + weight (THIS, digit fw). Sibling\n pattern to R416 chip-digit-hover-bold at chip\n scope — same \"data tightens under attention\"\n idiom now at the panel-header data scope. R311\n base fw=600 + R225 tabular-nums + R162 fill\n transition + R336 unit-tspan opacity-0.7 all\n preserved; only the weight axis tweens via R247's\n transition shape (added font-weight to the list). */}\n <tspan\n fill={freshFill}\n fontWeight={hoveredPanel === 'recent' ? '700' : '600'}\n data-recent-panel-count\n data-recent-panel-count-freshness-alpha={alpha.toFixed(2)}\n style={{\n transition: 'fill 200ms ease-out, font-weight 200ms ease-out',\n fontVariantNumeric: 'tabular-nums',\n }}\n >{flowLinks.length}<tspan opacity=\"0.7\" data-recent-panel-count-unit> flows</tspan></tspan>\n {/* Round 190 / Loop: R129 hot-tail gets anet-fade-in\n for entrance. Pre-R190 the tspan snapped into\n the header the moment hotFlowCount crossed 0,\n and snapped out the moment it dropped back to 0.\n Same trade-off R67 accepts for filter pills:\n fade-IN smooth, accept exit snap. The CSS\n animation plays once when the tspan mounts\n (count goes 0 → 1+); subsequent re-renders\n (count growing from 1 → 2 → 3 hot flows)\n preserve the element via React reconciliation\n so the fade-in doesn't replay. Layout-shift\n cost is paid once on entrance — the parent\n <text textAnchor=\"end\"> recomputes its\n anchor as the tspan appears, then stays\n stable as the digit grows. Exit-snap is rare\n in steady operation: a hot flow cooling back\n below 10 messages doesn't happen often. */}\n {/* Round 223 / Loop: hot-tail tspan always-mounts;\n visibility crossfades via inline opacity gate\n instead of conditional mount/unmount on hot\n FlowCount > 0. Pre-R223 R190's anet-fade-in gave\n a smooth entrance but exit was snap (the family's\n original \"fade-IN smooth, accept exit snap\" trade-\n off). R223 closes the asymmetry — both directions\n now ease 300ms. anet-fade-in className kept for\n R190 test compat (plays once on initial render\n if the page loads with hotFlowCount > 0); subsequent\n threshold crossings use the opacity transition\n bi-directionally. Text content gates to empty\n string when hidden so the parent <text textAnchor=\n \"end\"> doesn't compute anchor against stale \"·\"\n separator. data-recent-panel-hot-visible exposes\n the gate for tests. Always-mount-opacity-gate\n family now hits 10 surfaces (R181/R182/R183/R213×2/\n R214/R215/R221/R222/R223). */}\n {/* Round 322 / Loop: hot count tspan picks up\n fontVariantNumeric tabular-nums for parity with\n its left-sibling tspan (R311 `{flowLinks.length}\n flows`, already tabular). Pre-R322 a hotFlowCount\n crossing 1→10 widened the leading digit and (since\n the parent <text> is textAnchor=\"end\") shifted the\n WHOLE header left a few pixels — visible micro-\n jitter against the panel rect's left edge. Tabular-\n nums locks the digit so the right-anchored block\n stays stable as hotFlowCount grows. 8th surface\n in the info-density tabular-nums sweep:\n R224 edge badge / R225 hub digit / R225 panel\n flows-count + recent-row count / R229 group-\n label count / R230 group-label status pips /\n R320 recent-row count fw=600 (left neighbour) /\n R321 recent-row timestamp / R322 panel hot\n count (this round). */}\n <tspan\n fill={hotStroke}\n fontWeight=\"700\"\n data-recent-panel-hot-count={hotFlowCount}\n data-recent-panel-hot-visible={hotFlowCount > 0 ? 'true' : 'false'}\n className=\"anet-fade-in\"\n opacity={hotFlowCount > 0 ? 1 : 0}\n style={{\n transition: 'opacity 300ms ease-out',\n fontVariantNumeric: 'tabular-nums',\n }}\n >\n {/* Round 336 / Loop: split hot count from \" hot\"\n unit word with nested opacity-0.7 tspan. Same\n chip-internal-hierarchy idiom this round\n applies to the \"{N} flows\" tspan above (R311\n sibling) — digit prominent at fw=700 + amber\n fill, unit recessive at 0.7 opacity. data-\n recent-panel-hot-count-unit exposes the unit\n tspan for R336 probes. */}\n {hotFlowCount > 0 ? (\n <>\n {` · ${hotFlowCount}`}\n <tspan opacity=\"0.7\" data-recent-panel-hot-count-unit> hot</tspan>\n </>\n ) : ''}\n </tspan>\n </text>\n );\n })()}\n {/* Round 45 / Loop: empty state. The panel used to render\n \"recent signal\" + \"0 msgs\" with three blank slots below\n when no flow yet — read as \"broken\" rather than \"quiet\".\n A muted centred placeholder makes the empty state\n deliberate. Messages count CAN diverge from flowLinks\n count (raw count vs. deduped pairs), so the placeholder\n fires on flowLinks.length=0 specifically. */}\n {/* Round 222 / Loop: empty state always-mounts; visibility\n crossfades via wrapper <g> opacity instead of conditional\n mount/unmount on flowLinks.length === 0. Pre-R222 the\n first flow arriving snap-removed the empty state in one\n frame while R203 rows simultaneously faded IN — half-\n smooth, half-snap on this \"first data\" first-impression\n moment. R222 closes the snap so empty fades OUT while\n rows fade IN — a proper crossfade for the user's most\n emotionally-loaded moment (the empty-to-populated flip).\n\n R200 SMIL breath on each text continues running\n regardless of parent opacity (SVG opacity is\n multiplicative — same R214 pulse-dot composition idiom).\n When parent opacity is 0, SMIL still animates child\n opacity but the result composes to invisible. CSS and\n SMIL don't fight, they layer.\n\n 300ms transition matches R203 row fade-in pace so the\n empty-fade-out + rows-fade-in pair share rhythm during\n the crossfade. */}\n <g\n data-recent-signal-empty-wrapper\n data-recent-signal-empty-visible={flowLinks.length === 0 ? 'true' : 'false'}\n style={{\n opacity: flowLinks.length === 0 ? 1 : 0,\n transition: 'opacity 300ms ease-out',\n pointerEvents: 'none',\n }}\n >\n {/* Round 258 / Loop: re-center the empty state within the\n post-R256 88-tall panel. R45 placed \"no flow yet\" at\n y=54 + the hint at y=68 inside an 84-tall panel — those\n baselines sat 12px / 26px below the panel mid-line at\n y=42, optically balanced for the original height. R256\n grew the panel 84 → 88 to give the \"+N more flows\"\n footer underline breathing room, shifting the panel\n mid-line to y=44 — but the empty state stayed put,\n drifting 2px high. R258 pushes both empty-state lines\n +2 (main y=54 → y=56, hint y=68 → y=70) so the pair\n sits 12px / 26px below the new mid-line, restoring\n the R45 optical balance. The R222 always-mount opacity\n gate is unaffected (geometry-only shift), and the\n R200 SMIL breath continues unchanged. Hint baseline\n y=70 still sits 12px above the footer baseline (y=82),\n same vertical rhythm as the row 3 → footer gap. */}\n {/* Round 302 / Loop: empty-state hint 'no flow yet' picks\n up letterSpacing='0.2' for editorial parity with R301\n panel titles (0.3) + R285 kicker tracking-widest +\n R289 watermark letterSpacing. The hint is the\n panel's quietest authored text (italic monospace\n fontSize 10 opacity 0.65); adding a small positive\n letter-spacing keeps it in the same designed-label\n family without lifting its visual weight. 0.2px is\n slightly less than the panel titles' 0.3 — appropriate\n for the smaller fontSize + lower opacity (empty-state\n is intentionally quieter than the header above it). */}\n <text\n x=\"115\" y=\"56\" textAnchor=\"middle\"\n fill={pal.legendText}\n fontSize=\"10\" fontFamily=\"monospace\" fontStyle=\"italic\"\n letterSpacing=\"0.2\"\n opacity={0.65}\n data-recent-signal-empty\n data-recent-signal-empty-breathes={reducedMotion ? 'false' : 'true'}\n >\n no flow yet\n {!reducedMotion && (\n <animate\n attributeName=\"opacity\"\n values=\"0.55;0.78;0.55\"\n dur=\"4.4s\"\n repeatCount=\"indefinite\"\n />\n )}\n </text>\n {/* Round 259 / Loop: instructional hint bumps fontSize 8 → 9\n for readability. Pre-R259 the empty-state hint was at\n the smallest readable size on the canvas (8pt), with\n italic + opacity 0.45 layering legibility cost on top\n — instructional text users need to READ to act on,\n yet eye-straining at default 1× zoom. 9pt italic stays\n visually subordinate to the 10pt main \"no flow yet\"\n AND to the 9pt regular row text (italic alone\n discriminates from row content) while easing the\n legibility floor. Sibling change at the +N-more\n footer link (line ~6047) applies the same bump to\n the panel's other italic secondary text. Per-row\n timestamp at y=38+i*16 (fontSize 8 right-edge\n recency tag) STAYS at 8 — it's an at-a-glance\n recency tag tightly co-located with row text, not\n read-to-act instruction. */}\n {/* Round 304 / Loop: secondary instructional hint\n 'send a message between agents' gets letterSpacing\n '0.15'. Extends the R301/R302 editorial-spacing\n family one layer down. The hint is the quietest\n authored text in the recent-signal panel (fontSize\n 9 italic-less opacity 0.45, sits below the R302\n main empty-state hint at fontSize 10 italic\n opacity 0.65). 0.15px is below R302's 0.2px to\n match the visual hierarchy: smaller + quieter\n text gets less letter-spacing.\n 5-axis editorial-letterspacing hierarchy now:\n R285 kicker: 1.2px (eyebrow loud)\n R289 watermark: 0.5px (wordmark brand)\n R301 panel titles: 0.3px (section headers)\n R302 empty main: 0.2px (empty-state hint)\n R304 empty hint: 0.15px (instructional sub)\n Each step ~0.1-0.5x scale-down matches the\n font-size + opacity descent. */}\n {/* Round 339 / Loop: empty-state sub-hint picks up\n fontStyle=\"italic\" for parity with the main hint\n above (line ~6526). Pre-R339 the main hint \"no\n flow yet\" was italic while the sub-hint \"send a\n message between agents\" was upright — two empty-\n state texts sharing the same quiet informational\n role but rendered in different styles. R339 closes\n the inconsistency: both texts now read as deliberate\n italic empty-state messaging. The R304 letter-\n spacing 0.15 + R259 fontSize 9 + opacity 0.45 +\n SMIL opacity breath all preserved. */}\n <text\n x=\"115\" y=\"70\" textAnchor=\"middle\"\n fill={pal.legendText}\n fontSize=\"9\" fontFamily=\"monospace\" fontStyle=\"italic\"\n letterSpacing=\"0.15\"\n opacity={0.45}\n data-recent-signal-empty-hint\n data-recent-signal-empty-hint-breathes={reducedMotion ? 'false' : 'true'}\n >\n send a message between agents\n {!reducedMotion && (\n <animate\n attributeName=\"opacity\"\n values=\"0.36;0.58;0.36\"\n dur=\"4.4s\"\n begin=\"-1.5s\"\n repeatCount=\"indefinite\"\n />\n )}\n </text>\n </g>\n {flowLinks.length === 0 ? null : (\n // Round 56 / Loop: each row is a navigator into the canvas.\n // Hover a row → set hoveredEdgeKey, which the existing R50\n // edge-focus + R49 endpoint-highlight ladders consume. The\n // matching flow edge brightens to 2× + thickens, its two\n // endpoint nodes stay full opacity, and every other edge +\n // non-endpoint node dims. Released → all restore. Wrapping\n // <g> + a transparent 218×14 hitbox so the cursor doesn't\n // have to land precisely on the truncated text.\n flowLinks.slice(0, 3).map((link, index) => {\n // Round 94 / Loop: per-row relative timestamp. The chip\n // row shows \"last 2s\" for the most recent flow overall,\n // but a user scanning the recent-signal panel had no way\n // to tell whether row 2 was 5s old or 5m old without\n // hovering nodes. Compact `2s` / `1m` glyph at the\n // right edge — same relativeAgo helper R42 uses for\n // the chip — pulls double duty as a recency anchor and\n // a sortedness hint (top row is freshest by construction).\n // Strip the \" ago\" suffix — at fontSize=8 in a 32-px\n // right-edge slot, every char counts. \"30s ago\" → \"30s\".\n const rawAt = link.last_at ? relativeAgo(link.last_at) : null;\n const lastAt = rawAt ? rawAt.replace(/\\s+ago$/, '') : null;\n const isRowHovered = hoveredEdgeKey === link.key;\n const isRowPinned = pinnedEdgeKey === link.key;\n const isRowActive = isRowHovered || isRowPinned;\n // Round 191 / Loop: timestamp text on row's right edge\n // picks up the R160 row-pip freshness ramp at a\n // different alpha range — gives the timestamp the same\n // visual recency encoding the pip has on the LEFT\n // edge, so both ends of the row mirror each other.\n // ≤30s → opacity 0.85 (fresh, high contrast)\n // 30-300s → 0.85 → 0.30 (smooth decay)\n // >300s → 0.30 (stale floor)\n // Pre-R191 the timestamp was static 0.55 — the same\n // visual weight whether the message just fired or\n // happened 5 minutes ago. Fill stays legendText gray\n // (not cyan) because the left pip already carries the\n // cyan; the right timestamp encodes recency through\n // contrast against the dark canvas, not hue.\n const tsAlpha = !link.last_at ? 0.55 : (() => {\n const ageSec = Math.max(0, (Date.now() - Date.parse(link.last_at)) / 1000);\n return ageSec <= 30 ? 0.85\n : ageSec <= 300 ? 0.85 - ((ageSec - 30) / 270) * 0.55\n : 0.30;\n })();\n // R127: panel-side mirror of R126's canvas hot-badge.\n // The recent-signal row text packs `alias→alias / N /\n // preview` into one line, with N rendered identically\n // regardless of magnitude. Now that the canvas badge\n // tells the user \"≥ 10 msgs = hot lane\" via amber\n // stroke, the panel row needs the same affordance so\n // the user reading the list at a glance can spot hot\n // lanes without crossing to the canvas. Renders the\n // count digit in amber + 700-weight when isHot; the\n // surrounding alias text + separators stay in the\n // existing legendText/legendHeadline palette. Reuses\n // R126's hotStroke colour for visual consistency.\n const isHot = link.count >= 10;\n const hotStroke = isLight ? '#d97706' : '#fbbf24';\n return (\n <g\n key={link.key}\n data-recent-row={link.key}\n data-recent-row-hovered={isRowHovered ? 'true' : 'false'}\n data-recent-row-pinned={isRowPinned ? 'true' : 'false'}\n data-recent-row-hot={isHot ? 'true' : 'false'}\n data-recent-row-lifted={(isRowHovered || isRowPinned) ? 'true' : 'false'}\n // Round 203 / Loop: per-row mount fade-in. R175 already\n // eased the whole panel in once, but new flows rising\n // INTO the top-3 list (or replacing an older row) snap-\n // popped in. React reconciliation via key={link.key}\n // preserves stable rows across re-renders, so anet-\n // fade-in only plays on mount — never replays when\n // counts update or rows reorder by recency. Stacks on\n // the panel's own R175 anet-fade-in: SVG opacity\n // composes multiplicatively, so during the first paint\n // the panel's 700ms delay holds rows hidden until the\n // panel reveals, then row opacity transitions inside\n // the visible panel. For mid-session arrivals (panel\n // already at opacity 1) the row's 150ms fade-in plays\n // standalone. Three layers of mount-once eases now\n // share rhythm: panel (R175) → rows (R203) → row\n // contents (existing R160 pip / R191 ts opacity\n // ramps animate independently after mount).\n className=\"anet-topo-svg-focus anet-fade-in\"\n role=\"button\"\n tabIndex={0}\n aria-pressed={isRowPinned}\n // R143 / Loop: extend the R135/R142 \"interactive surface\n // elevates\" idiom down one layer to the recent-signal\n // panel rows. R104 already tints the row background on\n // hover; R143 adds a 1-px translate so the row text\n // visually lifts off the panel — same vocabulary R51\n // uses for nodes, R135 uses for panels, R142 uses for\n // group boxes. Pinned rows lift too (sticky state\n // should look like locked-in selection). Reduced-motion\n // safe via prefers-reduced-motion blanket override\n // applied to transition-duration in globals.css.\n style={{\n cursor: 'pointer',\n transform: (isRowHovered || isRowPinned) ? 'translateY(-1px)' : undefined,\n transition: 'transform 150ms cubic-bezier(0.4, 0, 0.2, 1)',\n }}\n onPointerDown={(e) => e.stopPropagation()}\n onMouseEnter={() => setHoveredEdgeKey(link.key)}\n onMouseLeave={() => setHoveredEdgeKey(prev => prev === link.key ? null : prev)}\n // R116: click toggles pin. activeEdgeKey =\n // hoveredEdgeKey ?? pinnedEdgeKey so the matching\n // edge stays \"hot\" after mouseleave; click again\n // (or Esc) releases.\n onClick={() => setPinnedEdgeKey(prev => prev === link.key ? null : link.key)}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n setPinnedEdgeKey(prev => prev === link.key ? null : link.key);\n }\n }}\n >\n {/* R148 / Loop: row tooltip with full message context.\n The row text truncates aliases to 6 chars (R127)\n and content to 8 chars — useful for scan-density\n but obscures the underlying message. A native SVG\n <title> reveals the full alias / content /\n timestamp on hover. Pinned vs unpinned switches\n the click hint so the user knows the next\n gesture's effect. R98 enriched the node tooltip\n the same way for the source/destination scope;\n R148 brings the per-row equivalent to the panel\n side. */}\n <title>{[\n `${link.from} → ${link.to} · ${link.count} msg${link.count === 1 ? '' : 's'}${isHot ? ' (hot lane · ≥ 10)' : ''}`,\n link.last_at ? `last: ${new Date(link.last_at).toLocaleString()}` : null,\n link.content ? `\"${link.content}\"` : null,\n isRowPinned ? 'click to release pin (Esc to clear)' : 'click to pin · hover to preview',\n ].filter(Boolean).join('\\n')}</title>\n {/* R104: subtle row-background tint on hover. R56\n already brightens the matching edge on the canvas,\n but the panel row itself stayed flat — felt more\n like text-with-handlers than a navigable list.\n Filling the rect at hover with `pal.legendAccent`\n at low alpha gives the row visual feedback at the\n source surface, mirroring the list-item idiom from\n the chip-row pills. R116: pinned rows tint\n stronger than hovered ones so locked vs preview\n is discriminable. */}\n {/* Round 472 / Loop — cadence-sync follow-on to the\n R459/R460/R461/R464/R465/R470 200ms uniform\n motion stack established at the cluster scope.\n This R104 recent-signal row tint rect was still\n at the legacy 150ms cadence — when a user\n hovers/pins a recent-signal row, the tint\n snapped in 50ms ahead of the rest of the row's\n state-change cascade (R143 translateY,\n R220+R434 letter-spacing, R434 fill tween).\n R472 lifts to 200ms ease-out to match. Same\n sibling idiom R459 closed at the group-label\n hitbox tier; now applied at the recent-signal\n row tier. data-recent-row-tint-transition attr\n exposes the cadence for tests.\n Geometry/paint logic unchanged — purely the\n transition timing. */}\n <rect\n x=\"6\" y={38 + index * 16 - 10}\n width=\"218\" height=\"14\" rx=\"3\"\n fill={isRowActive ? pal.legendAccent : 'transparent'}\n opacity={isRowPinned ? (isLight ? 0.18 : 0.22)\n : isRowHovered ? (isLight ? 0.10 : 0.14)\n : 1}\n data-recent-row-tint={link.key}\n data-recent-row-tint-transition=\"200ms\"\n style={{ transition: 'fill 200ms ease-out, opacity 200ms ease-out' }}\n />\n {/* Round 160 / Loop: recency pip. Canvas flow edges\n fade by freshness (R10: full intensity ≤30s →\n ~35% over 5min). The recent-signal panel rows\n duplicate that data (alias→alias · N · 5s)\n but encode freshness purely in text — no\n at-a-glance visual cue for \"which row is\n actively firing right now\". A 1.6-px cyan dot\n at x=10 (in the 7-px margin between rect-\n start x=6 and text-start x=13) brightens\n fresh rows and dims stale ones — same\n vocabulary the canvas uses, brought to the\n panel side.\n\n Three encodings now coexist on each row,\n none competing:\n rect fill = hover/pin state (R104/R116)\n count tspan = magnitude (R127 amber when ≥10)\n pip = recency (this round)\n\n Geometry: cy = row_y - 3 (mid-row vertical\n centre, where text baseline at y=row_y sits\n slightly below). r=1.6 fits cleanly in the\n 7-px left margin. pointerEvents:none so the\n row's button-role hit area is unchanged.\n No overlap-test impact (entirely within the\n existing rect bbox). */}\n {(() => {\n if (!link.last_at) return null;\n const ageSec = Math.max(0, (Date.now() - Date.parse(link.last_at)) / 1000);\n // 0-30s: fully fresh (1.0). 30-300s: smooth\n // decay 1→0.25. >300s: stale floor (0.25).\n const alpha = ageSec <= 30\n ? 1\n : ageSec <= 300\n ? 1 - ((ageSec - 30) / 270) * 0.70 /* R358: floor 0.25 → 0.30 lift across 3 freshness scopes */\n : 0.30; /* R358: stale floor lifted 0.25 → 0.30 — 20% legibility bump while preserving fresh/stale ratio */\n return (\n <circle\n cx={10}\n cy={38 + index * 16 - 3}\n /* Round 359 / Loop: recency pip base radius\n 1.6 → 1.8. Sibling lift to R358's freshness-\n floor bump (alpha 0.25 → 0.30) — pre-R358/\n R359 the stale pip painted at r=1.6 + α=0.25\n which read as near-invisible chrome. R358\n gave it more alpha; R359 gives it more area\n (1.8² / 1.6² ≈ 1.27, so ~27 % more glyph)\n so the pip stays distinguishable across the\n freshness ramp. Geometry: 1.8-radius dot\n centred at (10, row_y - 3) is bbox 3.6×3.6,\n still well inside the 7-px left margin\n (x=6 rect-start → x=13 text-start) the R160\n pip was placed in. Overlap-test reads the\n parent row rect's bbox, not this pip's, so\n grid+ring invariants hold. Matches the same\n 1.6 → 1.8 visual-weight bump R295 applied\n to the legend swatch (5.5 → 6 base radius)\n and R287 to the minimap viewport stroke\n (1 → 1.5). data-recent-row-freshness-radius\n attr exposes the value for tests. */\n /* Round 383 / Loop: recency pip base radius\n 1.8 → 2.0. Continues the R359 lift\n trajectory — pip area grows ~23 % (π·2²/\n π·1.8² ≈ 1.23) for a clearer at-a-glance\n freshness anchor in each row. Bbox 4.0×4.0\n still inside the 7-px R160 left margin\n (3-px remaining clearance vs 3.4 at r=1.8\n — geometry-safe margin holds). Sibling\n visual-weight bump family (9th anchor now):\n R287 minimap viewport stroke 1 → 1.5\n R295 legend swatch base radius 5.5 → 6\n R359 recent-row pip base radius 1.6 → 1.8\n R360 hub digit fontSize 11 → 12\n R361 edge-badge digit fontSize 10 → 11\n R365 hub-highlight base radius 5 → 5.5\n R367 edge-badge rest stroke 1 → 1.25\n R374 pressure-bar height 1.5 → 2\n R383 recent-row pip radius 1.8 → 2.0 (this round)\n data-recent-row-freshness-radius attr\n bumps to '2.0' for tests. */\n /* Round 447 / Loop: recent-row freshness pip\n radius lift on (isRowHovered || isRowPinned)\n — r 2.0 → 2.5 (+0.5px, sibling to R442\n endpoint-ring r lift). Adds a geometric\n axis to the recent-row hover/pin gesture\n alongside R143 translateY + R104 row bg-\n tint + R434 letter-spacing + R445 count\n fw. Pre-R447 the pip stayed at r=2.0 always\n — the freshness alpha (R162) tracked\n recency but didn't telegraph \"this row is\n in focus\" geometrically. R447 lifts the\n pip outward by 25% area (π·2.5² / π·2.0²\n = 1.56) on attention, closing a 5-axis\n row-attention signature (geometry + paint\n + typography + spacing + position).\n SVG `r` as CSS property for interpolation\n (R197/R198 idiom). transition list extends\n to include 'r 200ms ease-out' matching the\n opacity cadence. data-recent-row-freshness-\n lifted attr exposes the gate for tests. */\n /* Round 478 / Loop — extend the R476/R477\n drop-shadow vocabulary to a third anchor:\n the recent-row freshness pip on `alpha\n > 0.7` (just-fired flow within ~30s per\n R10 freshness ramp). Gate is FRESHNESS-\n driven not pin/hover-driven, so the glow\n reads as \"this signal is live\" rather\n than \"user is inspecting\". As the alpha\n decays past 0.7 (≈45s after last fire),\n the glow eases off — natural breathing\n feel that tracks actual data freshness.\n Hue: pal.legendAccent at 0.5 alpha so\n the glow inherits the row's accent color\n family. 2.5-3px blur reads as soft\n radiance, not loud bloom.\n Drop-shadow visual-polish family now 3\n anchors:\n R476 hub digit hover-gated\n R477 legend pin-ring pin-gated\n R478 recent freshness freshness-gated\n Each anchor uses a different state gate\n but the same `filter: drop-shadow` paint\n vocabulary. Filter affects paint only —\n bbox unchanged, overlap-test invariants\n hold. Transition list extends to include\n 'filter 200ms ease-out' alongside\n R10/R447 opacity + r tweens. */\n fill={pal.legendAccent}\n opacity={alpha}\n data-recent-row-freshness={link.key}\n data-recent-row-freshness-alpha={alpha.toFixed(2)}\n data-recent-row-freshness-radius={(isRowHovered || isRowPinned) ? 2.5 : 2.0}\n data-recent-row-freshness-lifted={(isRowHovered || isRowPinned) ? 'true' : 'false'}\n data-recent-row-freshness-glow={alpha > 0.7 ? 'true' : 'false'}\n style={{\n pointerEvents: 'none',\n r: `${(isRowHovered || isRowPinned) ? 2.5 : 2.0}px`,\n filter: alpha > 0.7\n ? `drop-shadow(0 0 3px ${pal.legendAccent}80)`\n : undefined,\n transition: 'opacity 200ms ease-out, r 200ms ease-out, filter 200ms ease-out',\n } as React.CSSProperties}\n />\n );\n })()}\n {/* Round 220 / Loop · milestone: recent-signal row\n text completes the pin-signature typography\n triple (R218 group labels / R219 legend rows /\n R220 recent-signal rows). All three label-based\n interactive surfaces now read \"locked in\" at\n the type level when pinned — letter-spacing\n spreads 0px → 0.5px on isRowPinned (NOT on\n hover — hover keeps default tracking so the\n eye can discriminate transient preview from\n sticky pin without checking chrome). Pin\n signature vocabulary now consistent across\n the entire interactive-label landscape of\n TopoGraph: every pin-able text element has\n a typography-level tell.\n transition extends 'letter-spacing 150ms'\n alongside R55 fill 150ms — same beat as\n R219 legend-row treatment. Hover still keeps\n its own R55 fill brighten exclusively;\n letter-spacing is pin-exclusive (note the\n isRowPinned not isRowActive gate). */}\n <text\n x=\"13\" y={38 + index * 16}\n fill={isRowActive ? pal.legendHeadline : pal.legendText}\n fontSize=\"9\"\n fontFamily=\"monospace\"\n /* Round 363 / Loop: recent-row text fontWeight 400\n → 500 (font-medium tier). At fontSize=9 the\n default-weight 400 glyphs read thin against the\n panel chrome (pal.legendBox.fill with 0.92/0.97\n opacity); the 100-weight bump lifts the alias→\n alias text into the legibility band without\n changing geometry. The R320 count tspan fw=600\n (cold) / fw=700 (hot) override still wins\n locally via inline fontWeight on the inner\n tspan, so the count-vs-alias hierarchy stays\n intact:\n alias fw 500 (R363, this round)\n count fw 600/700 (R320)\n Sibling typography lift to R362 chip-row digit\n 500 → 600 — both nudge a within-element data\n tier without disturbing the surrounding family\n baseline. data-recent-row-text-font-weight attr\n exposes the value for tests. */\n fontWeight=\"500\"\n data-recent-row-text={link.key}\n data-recent-row-text-pinned={isRowPinned ? 'true' : 'false'}\n data-recent-row-text-hovered={!isRowPinned && isRowHovered ? 'true' : 'false'}\n data-recent-row-text-font-weight=\"500\"\n /* Round 434 / Loop: recent-signal row text extends\n from R220's pin-only letter-spacing (0 → 0.5 on\n isRowPinned) to a 3-tier scale matching R433\n legend-row at the sibling panel-row scope:\n rest → 0px\n isRowHovered → 0.25px ← this round\n isRowPinned → 0.5px (R220 preserved)\n Pre-R434 R220 noted: \"Hover stays at default\n tracking — the spread is pin-exclusive so\n users can read pinned vs hovered at the text\n alone.\" R427-R433 established a 3-tier pattern\n across 4 surfaces (node-alias, edge-badge,\n group-label, legend-row) where hover gets a\n subtler intermediate kerning step distinct\n from the pin tier's stronger spread — the\n locked vs preview discrimination R220 wanted\n is preserved (0.5 > 0.25) AND hover gets a\n typographic axis of its own. R434 completes\n the 5-surface arc.\n 5-surface 3-tier letter-spacing pattern now\n spans every interactive label on TopoGraph\n that distinguishes hover from pin:\n node-alias (R427) 0 / 0.3 / 0.5\n edge-badge (R431) 0 / 0.2 / 0.4\n group-label (R432) 0 / 0.25 / 0.5\n legend-row (R433) 0 / 0.25 / 0.5\n recent-row (R434) 0 / 0.25 / 0.5 ← this round\n Hover-letter-spacing family extension\n (10 anchors now): R344/R345/R347/R351/R420/\n R427/R431/R432/R433/R434. R55 fill 150ms +\n R220 letter-spacing 150ms transition kept\n (additive conditional case, no new property). */\n /* Round 474 / Loop — cadence-sync follow-on to\n R472. R472 lifted the recent-row TINT RECT\n to 200ms but the row TEXT alongside still\n ran 150ms — same panel-row scope, two\n different rates. When a user hovered/pinned\n a row the rect background brightened in\n 200ms while the text fill + letter-spacing\n finished in 150ms. R474 closes that internal\n desync by lifting the text transitions to\n match. Whole recent-row state-flip now\n eases at 200ms ease-out across rect AND\n text. data-recent-row-text-transition='200ms'\n attr exposed for tests. R434 3-tier letter-\n spacing values unchanged; R363 fw + R55 fill\n brighten unchanged — only the timing axis\n shifts. */\n data-recent-row-text-transition=\"200ms\"\n style={{\n transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out',\n letterSpacing: isRowPinned ? '0.5px' :\n isRowHovered ? '0.25px' : '0px',\n }}\n >\n {/* R138 / Loop: typography unification with the rest\n of the topology UI. Filter pills (R119) render\n \"{from}→{to}\", node tooltips (R98) use →, the\n active-links chip tooltip (R114) and edge-badge\n titles all use unicode →. The recent-signal row\n was the lone holdout still rendering \"from -> to\"\n in ASCII. The data delimiter likewise: filter\n pills use \" · \" (\"status · 3\"); the row was using\n \" / \". Both swaps make the row read like every\n other surface — one less micro-style to remember. */}\n {truncate(link.from, 6)} {'→'} {truncate(link.to, 6)} {' · '}\n {/* Round 189 / Loop: count tspan unified — pre-R189\n two different tspans (data-recent-row-count vs\n data-recent-row-count-hot) mounted/unmounted\n on the R127 hot threshold (count >= 10),\n making fill (legendText ↔ amber) + fontWeight\n (regular ↔ 700) snap one-frame. Now one tspan\n always-mounted; isHot drives fill/fontWeight\n conditionally. style.transition='fill 300ms\n ease-out' makes the hot crossing ease through\n the colour shift — same vocabulary R188 just\n added to the edge midpoint badge stroke (the\n panel-side mirror of that surface). fontWeight\n stays binary (no clean weight interpolation\n across browsers). data-recent-row-count\n continues to expose the tspan to existing\n tests; data-recent-row-count-hot becomes\n an attribute on the same element when active\n so legacy probes still resolve. */}\n {/* Round 225 / Loop: tabular-nums on the per-row\n count digit. The row text reads \"alpha → beta ·\n {count} · content\"; when {count} grows from a\n single digit to two (9 → 10) the subsequent\n \" · {content}\" preview slides ~3-4px right in\n monospace because '1' and '0' have different\n natural widths against the surrounding control\n glyphs even in mono fonts. Tabular-nums locks\n the count column so the content preview\n column stays planted as activity scales up.\n Sibling treatment to R224 edge badge / R225\n hub digit / R225 panel-header flow-count. */}\n {/* Round 320 / Loop: cold-state per-row count gains\n explicit fontWeight=\"600\" instead of inheriting\n the parent <text>'s default (400). Brings the\n recent-signal row count into the 5-tier SVG\n data-weight family established by R309 (legend\n per-row count) / R310 (legend panel-header\n count) / R311 (recent-signal panel-header flow\n count). Pre-R320 the per-row count `· 12` for\n a cold row painted at fw=400, identical weight\n to the surrounding aliases — the count digit\n should read as data and stand out from the\n alias text. Hot crossing stays at fw=700 (R127),\n so cold→hot delta becomes 600→700 (still\n distinct, plus the fill flip from legendText\n → amber carries the dramatic part of the cue).\n Sibling treatment in the data-weight tier. */}\n {/* Round 445 / Loop: extend the R320 cold/hot fw\n binary (600/700) to ALSO fire on isRowPinned —\n pinned-cold now lifts to 700 alongside the\n existing hot-triggered lift. Sibling to R444\n group-label-count-pin (500→600) at the\n recent-row scope. Both panel-row counts now\n respond to pin with a typographic weight lift,\n part of the \"data tightens under attention\"\n family (R416/R424/R425/R426/R444/R445).\n Effective tiers:\n cold + un-pinned → fw 600\n cold + pinned → fw 700 ← this round\n hot (any pin state) → fw 700 (R320 preserved)\n hot is still amber-filled (R127); cold pin\n stays at the parent fill, so the two routes\n to fw=700 are visually distinct (color vs\n no color). transition list adds 'font-\n weight 200ms ease-out' so the lift eases\n under the same R320 fill cadence. data-\n recent-row-count-pinned attr exposes the\n pin gate for tests. */}\n <tspan\n fill={isHot ? hotStroke : undefined}\n fontWeight={(isHot || isRowPinned) ? '700' : '600'}\n data-recent-row-count\n data-recent-row-count-pinned={isRowPinned ? 'true' : 'false'}\n data-recent-row-count-font-weight={(isHot || isRowPinned) ? '700' : '600'}\n {...(isHot ? { 'data-recent-row-count-hot': 'true' } : {})}\n style={{\n transition: 'fill 300ms ease-out, font-weight 200ms ease-out',\n fontVariantNumeric: 'tabular-nums',\n }}\n >\n {link.count}\n </tspan>\n {/* Round 418 / Loop: recent-row content preview\n gains opacity=0.7 wrapper — subordinate-text\n tier at the SVG-text scope. Pre-R418 the\n truncated content preview (e.g. \" · hi there\")\n inherited the row's full opacity, reading at\n the same emphasis as the alias text and\n count digit. R418 wraps it in a <tspan> at\n opacity=0.7 so the preview reads as\n subordinate metadata — sibling to R333-R341/\n R362/R369/R389/R410/R412 chip-internal-\n hierarchy \"label tier\" (opacity-70) at the\n HTML scope, and R317 subordinate-text-lift\n gray-500 → gray-400 family. The leading\n \" · \" separator stays at full opacity so\n the row punctuation rhythm holds. data-\n recent-row-content-tspan attr surfaces the\n subordinate wrapper for tests. */}\n {' · '}\n <tspan opacity=\"0.7\" data-recent-row-content-tspan>{truncate(link.content, 8)}</tspan>\n </text>\n {/* Round 484 / Loop — recent-row timestamp opacity\n lifts to 1.0 when isRowHovered || isRowPinned,\n regardless of freshness alpha. R191 origin\n decays tsAlpha along with the row's freshness;\n pre-R484 hovering/pinning the row left the\n timestamp dim — user inspecting stale data\n fought the freshness encoding. R484 lifts to\n 1.0 on attention. Sibling to R472/R474 in the\n recent-row state-flip family. data-recent-row-\n ts-lifted attr exposes the gate; original\n data-recent-row-ts-alpha preserved as R191\n freshness reading. */}\n {lastAt ? (\n /* Round 321 / Loop: lastAt freshness timestamp picks\n up fontVariantNumeric tabular-nums. The string\n marches through 1s..59s (1 digit / 2 digits) /\n 1m..59m / 1h..24h every second the panel ticks,\n and the textAnchor=\"end\" right-aligns against\n x=217. Pre-R321 a 9s→10s crossing slid the chip\n left ~3px in monospace (digit '1' narrower than\n '0' even in mono) — same one-frame visible jitter\n R225 / R230 fixed elsewhere. Tabular-nums locks\n the digit slot so the timestamp stays planted as\n seconds tick. 7th surface in the info-density\n tabular-nums sweep after R224 edge badge / R225\n hub digit + panel header + recent row count /\n R229 group-label count / R230 group-label\n status pips / R320 recent-row count fw=600\n (count and timestamp now both lock). */\n <text\n x=\"217\" y={38 + index * 16}\n textAnchor=\"end\"\n fill={pal.legendText}\n fontSize=\"8\"\n fontFamily=\"monospace\"\n opacity={(isRowHovered || isRowPinned) ? 1 : tsAlpha}\n data-recent-row-ts={link.key}\n data-recent-row-ts-alpha={tsAlpha.toFixed(2)}\n data-recent-row-ts-lifted={(isRowHovered || isRowPinned) ? 'true' : 'false'}\n style={{\n pointerEvents: 'none',\n transition: 'opacity 200ms ease-out',\n fontVariantNumeric: 'tabular-nums',\n }}\n >\n {lastAt}\n </text>\n ) : null}\n </g>\n );\n })\n )}\n {/* Round 128 / Loop: overflow hint. The recent-signal panel\n renders the top 3 flowLinks via .slice(0, 3) — but a\n fleet with 5 or 10 active flows silently truncates the\n rest. The R96 \"X flows\" header tells the total but\n doesn't say \"you're seeing top-3\". This hint fires\n only when flowLinks.length > 3 so quiet fleets stay\n clean. Footer y=82 sits between row 3 (baseline 70)\n and the panel bottom; the R256 height bump 84→88\n adds 6 px of clear below the footer baseline so the\n on-hover textDecoration:underline (which renders\n ~3-4 px below baseline → y≈85-86) tucks INSIDE the\n panel border instead of clipping past it at the\n old 84-px floor. Overlap-test geometry unchanged\n (panel selector at translate(16,16); corner-to-\n center distance 342.6 → 340 still > 325 ring-clear\n threshold). fontStyle=italic + opacity 0.55 reads\n as muted metadata, not an actionable row — matches\n the R110 empty-state hint idiom. */}\n {(() => {\n // Round 221 / Loop: footer always-mounts; visibility\n // crossfades via wrapper <g> opacity instead of React\n // conditional mount/unmount on flowLinks.length > 3.\n // Pre-R221 a fleet's 4th flow appearing snap-popped the\n // footer in; tapering back to 3 flows snap-removed it.\n // Same threshold-crossing snap R215 closed for edge\n // midpoint badges, now applied to the recent-panel\n // footer surface. moreCount clamps at 0 so when invisible\n // (length ≤ 3) the data attribute and text don't show\n // garbage negative numbers. a11y trio (role / tabIndex /\n // aria-hidden) and pointerEvents follow visibility so\n // the hidden footer doesn't appear in tab order or\n // intercept clicks at its midpoint coordinates.\n const visible = flowLinks.length > 3;\n const moreCount = Math.max(0, flowLinks.length - 3);\n const label = `+ ${moreCount} more flow${moreCount === 1 ? '' : 's'}`;\n // R133: the truncation hint becomes a clickable nav to\n // /messages. R128 introduced the footer as pure metadata\n // (\"you're seeing top-3\"); R133 closes the gap by giving\n // users a way to ACT on that info — see the full list.\n // Wrap in <g> with onClick so SVG hit-testing fires the\n // route push. cursor:pointer + the underline-on-hover\n // visual cue tells users this is interactive. The hover\n // state is React-controlled (no CSS :hover on SVG <g>\n // descendant text would feel reliable across Chrome's\n // SVG quirks). pointerEvents follows visibility (R215\n // pattern) so the hidden footer can't intercept clicks\n // at its midpoint when it's invisible (sub-threshold\n // fleets).\n return (\n <g\n data-recent-panel-more-nav\n data-recent-panel-more-visible={visible ? 'true' : 'false'}\n role={visible ? 'link' : undefined}\n tabIndex={visible ? 0 : -1}\n aria-hidden={visible ? undefined : true}\n className=\"anet-topo-svg-focus\"\n style={{\n cursor: visible ? 'pointer' : undefined,\n pointerEvents: visible ? 'all' : 'none',\n opacity: visible ? 1 : 0,\n transition: 'opacity 300ms ease-out',\n }}\n onPointerDown={(e) => e.stopPropagation()}\n onMouseEnter={() => { if (visible) setHoveredRecentMore(true); }}\n onMouseLeave={() => setHoveredRecentMore(false)}\n onClick={() => { if (visible) router.push('/messages'); }}\n onKeyDown={(e) => {\n if (!visible) return;\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n router.push('/messages');\n }\n }}\n >\n <title>{`${label} — open /messages for the full list`}</title>\n {/* Round 195 / Loop: footer hint adopts the cyan\n vocabulary on hover, joining the interactive-on-\n hover-is-cyan family (R178 fullscreen / R163\n layout / R179 nodeSize / R193 active-links chip).\n Pre-R195 the footer brightened on hover via\n opacity + underline but kept its gray fill —\n 'becomes brighter gray' rather than 'becomes the\n go-act color'. legendAccent is the same cyan-400\n (dark) / teal-600 (light) every other interactive\n hover speaks. transition list extends `fill 200ms\n ease-out` so the colour swap eases instead of\n snapping. data-recent-panel-more-hovered exposes\n the gate for tests. */}\n {/* Round 259 / Loop: footer link bumps fontSize 8 → 9\n for clickable-text readability. Pre-R259 the\n \"+N more flows\" footer (the panel's primary\n navigation affordance into /messages) sat at the\n same 8pt as the empty-state hint — small enough\n to make the click target feel cheap. 9pt + italic\n + opacity 0.55 keeps it visually secondary to row\n content (9pt regular) while giving the link\n enough type-weight to read as a real affordance.\n Underline geometry verified: with fontSize 9 the\n underline still renders ~3-4px below baseline at\n y=82 → underline ~y=85-86, panel bottom y=88 →\n 2-3px clear (R256 footer-breath invariant\n preserved). */}\n {/* Round 325 / Loop: footer link joins the editorial\n letter-spacing family at 0.2px — same axis as\n R302 empty-state main hint (fontSize 10 opacity\n 0.65). The footer is italic monospace fontSize 9\n opacity 0.55 acting as the panel's primary\n navigation affordance into /messages; pre-R325\n it sat orphaned from the R285/R289/R301/R302/R304\n editorial-spacing axis even though it carried\n the same designed-label semantics (action label,\n not row data). 0.2px is the same value as R302\n because at fontSize 9 vs 10 the visual density\n of \"+ N more flows\" is close to \"no flow yet\"\n and they're both italic — sibling-equal in the\n hierarchy. The R195 cyan-hover + R259 fontSize\n bump stay; this round only adds the spacing.\n 6-axis editorial-letterspacing hierarchy now:\n R285 kicker: 1.2px (eyebrow loud)\n R289 watermark: 0.5px (wordmark brand)\n R301 panel titles: 0.3px (section headers)\n R302 empty main: 0.2px (empty-state hint)\n R325 footer link: 0.2px (panel nav action) ← NEW\n R304 empty sub: 0.15px (instructional sub) */}\n {/* Round 340 / Loop: +N more flows footer link extends\n the R333/R335/R336/R337/R338 chip-internal-hierarchy\n arc to a 6th surface. The digit `{moreCount}` reads\n as the primary data (\"how many more flows\"); the\n unit text ` more flow(s)` recedes via a nested\n tspan with opacity-0.7 (multiplicative against the\n parent <text>'s hover/rest opacity, so unit always\n sits below the digit). The `label` variable is\n preserved for the <title> tooltip — only the SVG\n render splits. data-recent-panel-more-unit exposes\n the unit tspan for tests. */}\n {/* Round 344 / Loop: footer hover gains letter-spacing\n tween 0.2 → 0.3. R325 set rest letter-spacing\n 0.2 to join the editorial-spacing family; R344\n adds a 0.1px hover spread that layers on top of\n R195 cyan fill + R325 spacing + R133 underline\n so the footer reads \"lit up and spaced\" on\n hover — sibling to R218/R219/R220 pin-signature\n letter-spacing family applied to a hover-only\n surface. transition list extends letter-spacing\n 200ms ease-out alongside the existing opacity/\n fill easings. */}\n {/* Round 368 / Loop: `+N more flows` footer text gains\n fontWeight=500 (font-medium tier). Sibling small-\n text fw lift family with R363 recent-row alias\n + R364 legend-row label + R366 group-label count\n — all four lifts share the same theory: at small\n fontSize (9-11 px) against panel chrome, SVG-\n default fw 400 sits at the legibility floor;\n fw 500 brings the glyph into the deliberate-data\n band. fontStyle=italic + opacity 0.55 rest + R325\n letterSpacing 0.2 baseline + R344 hover-spread\n 0.2 → 0.3 + R195 cyan fill on hover all preserved\n — the fw bump just thickens the italic stroke.\n Hover-state punch (R195 fill + R325 opacity 0.55\n → 0.85 + R344 letter-spacing + R133 underline)\n stays as is, so the rest-vs-hover delta still\n reads clearly. data-recent-panel-more-font-weight\n attr exposes the value for tests. */}\n <text\n x=\"115\" y=\"82\"\n textAnchor=\"middle\"\n fill={hoveredRecentMore ? pal.legendAccent : pal.legendText}\n fontSize=\"9\"\n fontFamily=\"monospace\"\n fontStyle=\"italic\"\n fontWeight=\"500\"\n letterSpacing={hoveredRecentMore ? '0.3' : '0.2'}\n opacity={hoveredRecentMore ? 0.85 : 0.55}\n textDecoration={hoveredRecentMore ? 'underline' : 'none'}\n data-recent-panel-more={moreCount}\n data-recent-panel-more-hovered={hoveredRecentMore ? 'true' : 'false'}\n data-recent-panel-more-font-weight=\"500\"\n style={{ transition: 'opacity 150ms ease-out, fill 200ms ease-out, letter-spacing 200ms ease-out' }}\n >\n {`+ ${moreCount}`}\n <tspan opacity=\"0.7\" data-recent-panel-more-unit>{` more flow${moreCount === 1 ? '' : 's'}`}</tspan>\n </text>\n </g>\n );\n })()}\n </g>\n )}\n\n {/* legend — Round 55 / Loop: each status row is now a hover\n target. Pointer enter sets `hoveredStatus`; pointer leave\n clears it. Node opacity formula composes the match below.\n The row text brightens to legendHeadline while hovered as\n a small affordance hint. Geometry unchanged — the new\n <g> wrappers only carry pointer handlers. */}\n <g\n transform=\"translate(760, 16)\"\n data-topo-panel=\"legend\"\n data-topo-panel-hovered={hoveredPanel === 'legend' ? 'true' : 'false'}\n // R175 / Loop: legend panel offset 100ms behind the\n // recent-signal panel so the two corner panels cascade\n // left-then-right rather than appearing in lockstep.\n // Same .anet-fade-in mechanism the four wave layers use.\n className=\"anet-fade-in\"\n data-topo-panel-fade-delay={800}\n style={{ animationDelay: '800ms' }}\n onMouseEnter={() => setHoveredPanel('legend')}\n onMouseLeave={() => setHoveredPanel(prev => prev === 'legend' ? null : prev)}\n >\n {/* R57: matching drop-shadow elevation to the legend panel.\n R106: panel height grew 96 → 104 to seat the new header\n line + 4 px row-shift below it (so the new header text\n doesn't overlap the row-1 hitbox region).\n R135: hover-elevation mirrors the recent-signal panel\n rect at line ~3299. Both panels grow their shadow on\n hover to telegraph \"the chrome is interactive\" since\n their rows pin / nav. */}\n {/* Round 247 / Loop: sibling treatment to the recent-signal\n panel — fill + stroke + opacity transitions added so\n the legend panel also eases through theme toggles\n (no snap on cyber↔light switch). Same 200ms cadence\n across the panel pair. */}\n {/* Round 331 / Loop: legend panel rect rx 8 → 10 — sibling\n treatment to the recent-signal panel above. Same\n proportional-rhythm step under R330's rounded-xl\n canvas wrapper envelope. */}\n <rect\n x=\"0\" y=\"0\" width=\"224\" height=\"88\" rx=\"10\"\n fill={pal.legendBox.fill}\n // R423 sibling — legend panel rect stroke tints to\n // legendAccent on hover (mirrors recent-signal panel\n // above). 4-layer hover cue stack now symmetric across\n // both side panels.\n stroke={hoveredPanel === 'legend' ? pal.legendAccent : pal.legendBox.stroke}\n // R348 sibling — legend panel rect opacity hover-state\n // bump 0.92 → 0.97 (cyber) / 0.97 → 1 (light) on\n // hoveredPanel === 'legend'. Pairs with the recent-signal\n // panel rect above so the two corner panels' hover cues\n // stay symmetric. Geometry-safe (paint-only).\n opacity={hoveredPanel === 'legend' ? (isLight ? 1 : 0.97) : (isLight ? 0.97 : 0.92)}\n style={{\n filter: hoveredPanel === 'legend'\n ? (isLight ? 'drop-shadow(0 4px 12px rgba(15,23,42,0.14))'\n : 'drop-shadow(0 4px 12px rgba(0,0,0,0.65))')\n : (isLight ? 'drop-shadow(0 2px 6px rgba(15,23,42,0.08))'\n : 'drop-shadow(0 2px 6px rgba(0,0,0,0.45))'),\n transition: 'filter 200ms ease-out, fill 200ms ease-out, stroke 200ms ease-out, opacity 200ms ease-out',\n }}\n data-topo-panel-elevation=\"legend\"\n />\n {/* R106 / Loop: panel header — symmetric with the recent-\n signal panel's \"recent signal · N flows\" (R96). Same\n font + position vocabulary so the two side panels feel\n paired. Title text at x=13 y=21; total fleet count\n right-aligned at x=215 y=21 in the accent colour. */}\n {/* Round 266 / Loop: legend panel title fill picks up theme-\n toggle transition — sibling treatment to the recent-\n signal panel title at line ~5459. Pre-R266 both panel\n titles hard-flipped color on theme toggle while their\n surrounding chrome eased; R266 closes both at once. */}\n {/* R301: sibling to recent-signal panel title above —\n same letterSpacing 0.3 for editorial parity. */}\n {/* Round 483 / Loop — sibling to R482 (recent-signal panel\n title): legend panel title fontWeight 700 → 800 on\n pinnedStatus (any legend row pinned propagates to the\n panel title). Pre-R483 the title responded only to\n panel-chrome hover via R345 ls; the pinnedStatus row\n highlighted its own swatch + tint via R181/R477 but\n the title stayed flat — no upstream tightening to\n signal \"panel context = inspecting\".\n R483 closes the symmetry with R482: both panel titles\n (recent-signal + legend) now tighten typographically\n when ANY row inside them is in the active filter\n state. Same idiom, mirrored at the legend-row scope.\n data tightens family — now 10 anchors:\n R416/R424/R425/R426 chip/panel/hub/edge digits\n R444/R445/R446 group/recent/legend counts\n R457 group-label parent\n R482 recent-panel title\n R483 legend-panel title (this round)\n transition list extends to include 'font-weight 200ms\n ease-out' alongside R345's ls + R55's fill 200ms.\n data-legend-panel-title-fw + -active exposed for tests. */}\n {/* R345 sibling — legend panel title same hover letter-\n spacing tween 0.3 → 0.4 on panel hover. */}\n <text x=\"13\" y=\"21\" fill={pal.legendHeadline} fontSize=\"12\" fontFamily=\"monospace\" fontWeight={pinnedStatus ? '800' : '700'} letterSpacing={hoveredPanel === 'legend' ? '0.4' : '0.3'} style={{ transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out, font-weight 200ms ease-out' }} data-legend-panel-title data-legend-panel-title-fw={pinnedStatus ? '800' : '700'} data-legend-panel-title-active={pinnedStatus ? 'true' : 'false'}>legend</text>\n {/* Round 257 / Loop: legend panel header count picks up the\n symmetric 13L/13R inner-padding pattern from the recent-\n signal panel. Pre-R257 the legend header was 13px from\n the left edge (`x=13` title) but only 9px from the right\n edge (`x=215` end-anchored count → 224-215=9), while the\n recent-signal panel header used 13px on BOTH sides (x=13\n title + x=217 end-count → 230-217=13). The two panels\n sit as a side-by-side corner pair — mismatched header\n inner-padding read as a typographic nit on the panel\n chrome. x=211 (= 224-13) restores symmetric 13L/13R so\n the panel pair shares one inset rhythm. The per-row\n count text at x=215 (line ~6321) STAYS at 9px-from-right\n — that one is paired with the flow-arrow swatch geometry\n ('M140,80 Q164,56 196,80') and would visibly tighten\n against the arrow tip if moved further left. Header\n count has no such pairing; it stands alone. */}\n {/* Round 266 / Loop: legend count fill (pal.legendAccent\n — cyber #67e8f9 cyan ↔ light #10b981 emerald) picks up\n theme-toggle transition. Pre-R266 the count snapped\n color on theme flip; R266 eases it alongside the panel\n title (sibling text in the same header band). */}\n {/* Round 292 / Loop: legend panel header count adopts explicit\n fontVariantNumeric: 'tabular-nums' for parity with the\n recent-signal panel header count at line ~5814 (R232).\n The text is already fontFamily='monospace' so digit width\n is technically tabular by definition — the explicit\n directive documents intent at code level, survives a\n future font-family change without silently losing\n tabular alignment, and eliminates an asymmetry between\n two sibling panel-header counts. Sibling treatment to\n R225 (hub digit) / R224 (edge badge) / R232 (chip-row\n counts) — tabular-nums sweep continues wherever digits\n live next to non-digit characters. */}\n {/* Round 310 / Loop: legend panel-header count picks up\n fontWeight=600 for parity with R309 per-row count\n weight. Pre-R310 the header count 'N nodes' rendered\n at default 400 while the per-row counts (working\n 'N' / idle 'N' / offline 'N') went semibold in R309.\n Same hierarchy reason as R309: the count is the DATA\n operators scan; the label ('legend' panel title +\n row labels 'working/idle/offline') is stable\n structural anchor. R309 established the rule at the\n row scope; R310 propagates it up to the panel-\n summary scope so the count typography is consistent\n across both the rollup and per-row counts inside\n the same legend panel. Existing pal.legendAccent\n fill + tabular-nums + R266 fill transition all\n preserved. */}\n {/* Round 336 / Loop: split legend panel count digit from\n unit \" nodes\" with nested opacity-0.7 tspan — sibling\n treatment to the recent-signal panel count and hot-\n count splits above. Three-panel-header surface family\n now sharing the same chip-internal-hierarchy pattern:\n recent flows count + \" flows\" unit at 0.7\n recent hot count + \" hot\" unit at 0.7\n legend nodes count + \" nodes\" unit at 0.7\n data-legend-panel-count-unit on the inner tspan for\n R336 introspection; the parent .textContent still\n reads \"{N} node(s)\" so existing R310 count tests via\n textContent unchanged. */}\n {/* R424 sibling — legend panel count digit fontWeight 600\n → 700 on panel hover. Closes 5-layer panel hover cue\n stack symmetric across both side panels (recent-signal\n + legend): depth (R135) + solidity (R348) + spacing\n (R345) + edge color (R423) + weight (R424). R310 base\n fw=600 + R292 tabular-nums + R266 fill transition + R336\n unit-tspan opacity-0.7 all preserved. Same \"data tightens\n under attention\" idiom R416 established at chip scope. */}\n <text\n x=\"211\" y=\"21\" textAnchor=\"end\"\n fill={pal.legendAccent} fontSize=\"10\" fontFamily=\"monospace\" fontWeight={hoveredPanel === 'legend' ? '700' : '600'}\n // R349 sibling — legend panel header count picks up\n // letterSpacing=\"0.2\", one tier below the R301 panel\n // title 0.3. Pairs with the recent-signal panel count\n // letter-spacing above so the two corner panels' header\n // typography stays editorially symmetric.\n letterSpacing=\"0.2\"\n data-legend-panel-count\n data-legend-panel-count-letter-spacing=\"0.2\"\n style={{\n transition: 'fill 200ms ease-out, font-weight 200ms ease-out',\n fontVariantNumeric: 'tabular-nums',\n }}\n >{sessions.length}<tspan opacity=\"0.7\" data-legend-panel-count-unit> node{sessions.length === 1 ? '' : 's'}</tspan></text>\n {(() => {\n const idleCount = onlineNodes.length - workingCount;\n // R106: rows shift +8 px (was y0=24, 48, 72 → 32, 56, 80)\n // to clear the new header row. R57 panel rect grew 96 →\n // 104 to seat them.\n const rows = [\n /* Round 277 / Loop: legend panel compress 104 → 88 (matches\n recent-signal panel height post-R256) per Vincent\n 5214/5215-5217 simplification ask. Row stride drops\n 24 → 18: row 1 working anchored at y0=32 (unchanged so\n R271 hitbox-swatch-center test at y=21 still passes);\n row 2 idle y0=56→50 (-6); row 3 offline y0=80→68 (-12).\n Flow-arrow swatch path (line ~6607) tracks new offline\n cy from y=80 to y=68. Net: legend panel takes ~15%\n less vertical chrome, panel pair (recent-signal+\n legend) now share same height = symmetric corner\n pair. Tests still pass: R257/R266/R269/R274 probe\n x attrs, fill transitions, text content — none\n sensitive to y0 stride. R271 probes working row\n hitbox y=21 (row.y0-11 with row.y0=32), unchanged.\n Corner-to-center distance increases (panel ends\n higher, further from center) — geometric ring-clear\n improves slightly. */\n /* Round 308 / Loop: continue the R307 legend label 减法.\n 'working node' (12 chars) → 'working' (7 chars):\n the 'node' qualifier is redundant — the row is in\n the LEGEND for a node graph, every row inherently\n describes a node state. 'online idle' (11 chars) →\n 'idle' (4 chars): the 'online' qualifier was\n disambiguation against the offline row, but the\n dashed-vs-solid status ring already discriminates\n online idle from offline visually. After R307+R308\n the three legend labels are all just status words:\n working / idle / offline — a clean 3-state list at\n roughly comparable lengths (7 / 4 / 7) for the\n tightest legend column to date. Pure 减法; visual\n information already encoded by row position +\n status-color swatch + status-ring dashing. */\n { key: 'working' as const, y0: 32, y1: 36, fill: isLight ? '#059669' : '#22c55e', label: 'working', count: workingCount },\n { key: 'idle' as const, y0: 50, y1: 54, fill: isLight ? '#0d9488' : '#2dd4bf', label: 'idle', count: idleCount },\n /* Round 269 / Loop: \" / \" → \" · \" delimiter unification.\n R138 swept the recent-signal row separators from\n ASCII \" / \" to typographic \" · \" (matching filter\n pills, node tooltips, edge badges, active-links\n tooltip). The legend's offline-row label was the\n LAST hardcoded \" / \" holdover in TopoGraph. Same\n monospace cell width (no layout shift), completes\n the R138 delimiter sweep.\n Round 307 / Loop: drop the ' · no SSE' qualifier.\n 'offline · no SSE' (16 chars) → 'offline' (7 chars).\n The visual already communicates the same idea\n redundantly: status ring strokeDasharray='5 5' for\n offline nodes (line ~5193) + gray fill + offline\n row's own gray swatch. Text qualifier was\n technical disambiguation that the visual encodes\n directly. Same R275-R281/R290/R291/R294 减法\n register — remove redundant text the eye doesn't\n need. Sibling row labels 'working node' (12 chars)\n + 'online idle' (11 chars) read at roughly the\n same length now too — legend rows look more\n balanced across the 3 lines. */\n { key: 'offline' as const, y0: 68, y1: 72, fill: isLight ? '#94a3b8' : '#6b7280', label: 'offline', count: offlineNodes.length },\n ];\n return rows;\n })().map(row => {\n // Round 61 / Loop: legend rows pin too — symmetric with the\n // R60 pressure-bar segments. R55 hover stays transient; the\n // new onClick toggles pinnedStatus so users can lock a\n // filter without holding the cursor still. Pinned row gets\n // an inset ring on the swatch (same vocab as R60).\n const isPinned = pinnedStatus === row.key;\n const isRowHovered = hoveredStatus === row.key;\n const isLifted = isRowHovered || isPinned;\n return (\n <g\n key={row.key}\n data-legend-status={row.key}\n data-legend-row-lifted={isLifted ? 'true' : 'false'}\n className=\"anet-topo-svg-focus\"\n role=\"button\"\n tabIndex={0}\n aria-pressed={isPinned}\n // R144: mirror of R143 — extend the row-lift idiom to the\n // legend panel rows for symmetry with the recent-signal\n // panel rows. Same translate-1px transform, same 150ms\n // cubic-bezier timing. R55 hovers the row (transient),\n // R61 pins it (sticky); both states earn the lift.\n // Composes with R105 row-bg-tint and R135 panel hover-\n // shadow → three layers of feedback at three nested\n // scopes (panel chrome → row text lift → bg tint).\n style={{\n cursor: 'pointer',\n transform: isLifted ? 'translateY(-1px)' : undefined,\n transition: 'transform 150ms cubic-bezier(0.4, 0, 0.2, 1)',\n }}\n // R61: stopPropagation on pointerdown so the SVG-level\n // pan handler (R103) doesn't setPointerCapture and\n // redirect the follow-up click away from this <g>.\n // Same trick the node <g> uses (and R52 hub).\n onPointerDown={(e) => e.stopPropagation()}\n onMouseEnter={() => setHoveredStatus(row.key)}\n onMouseLeave={() => setHoveredStatus(prev => prev === row.key ? null : prev)}\n onClick={() => setPinnedStatus(prev => prev === row.key ? null : row.key)}\n >\n {/* R149 / Loop: legend row gains a <title> tooltip\n symmetric with R148's recent-signal row tooltip.\n Same R97 idiom — anywhere the UI shows \"N\" should\n hover-explain WHICH N — applied to the legend\n panel which had been showing only the bucket\n count without alias context. Header line names\n the status bucket; body lists the matched aliases\n with 8-truncate + \"+N more\"; footer hint flips\n with pin state. Hot-lane convention doesn't apply\n here (status buckets aren't traffic counts), so\n no isHot suffix. */}\n <title>{(() => {\n const aliases = row.key === 'working'\n ? onlineNodes.filter(s => s.status === 'working').map(s => s.alias)\n : row.key === 'idle'\n ? onlineNodes.filter(s => s.status !== 'working').map(s => s.alias)\n : offlineNodes.map(s => s.alias);\n const preview = aliases.slice(0, 8).join(', ');\n const suffix = aliases.length > 8 ? ` + ${aliases.length - 8} more` : '';\n return [\n `${row.label} · ${row.count}`,\n aliases.length > 0 ? `${preview}${suffix}` : null,\n isPinned ? 'click to release pin (Esc to clear)' : 'click to pin · hover to preview',\n ].filter(Boolean).join('\\n');\n })()}</title>\n {/* R55 hitbox covers the row so cursor doesn't need to\n be exactly on the 5-px swatch. R105 / Loop: the\n hitbox now also carries a subtle hover/pin tint —\n mirroring R104's recent-signal row treatment so\n both side panels share the list-item idiom. The\n tint borrows the ROW'S OWN swatch colour rather\n than legendAccent, so the user's eye associates\n the tinted row with its colour swatch instead of\n a generic accent. Pin is a stronger tint than\n hover; idle stays fully transparent. */}\n {/* Round 271 / Loop: hitbox y shifts row.y0-12 → row.y0-11\n so hitbox center aligns exactly with swatch cy=row.y0.\n Pre-R271 hitbox spanned y=row.y0-12 to y=row.y0+10\n (center at row.y0-1), with swatch cy at row.y0 — 1px\n asymmetric. Hover/pin tint band drifted 1px above\n the swatch. Post-R271: hitbox spans y=row.y0-11 to\n y=row.y0+11 (center exactly at row.y0), swatch sits\n 11px from both edges (symmetric). Label text\n vertical center also benefits — label baseline\n y=row.y1=row.y0+4, visual midpoint ~row.y0+1.25,\n now ~1.25px below hitbox center (vs ~2.25px pre).\n No height change, no test ripple (other than this\n one), no R260/R268/R270 chrome regressions. */}\n {/* Round 473 / Loop — final cadence-sync follow-on,\n closing the legacy 150ms transition at the\n LEGEND-ROW tint scope. R459 (group-label hitbox)\n + R472 (recent-row hitbox) already lifted the\n two sibling panel-row hitboxes to 200ms; the\n legend-row was the last per-row tint still\n snapping at 150ms.\n After R473 the 200ms ease-out vocabulary is\n uniform across ALL three panel-row scopes —\n group-label, recent-signal, and legend — so\n hover/pin state-change cascades read coherently\n at every panel-tier surface. data-legend-row-\n tint-transition='200ms' attr exposed for tests.\n Geometry/paint unchanged. */}\n <rect\n x=\"6\" y={row.y0 - 11}\n width=\"170\" height=\"22\" rx=\"3\"\n fill={hoveredStatus === row.key || isPinned ? row.fill : 'transparent'}\n opacity={isPinned ? (isLight ? 0.14 : 0.18)\n : hoveredStatus === row.key ? (isLight ? 0.08 : 0.12)\n : 1}\n data-legend-row-tinted={isPinned ? 'pinned' : hoveredStatus === row.key ? 'hover' : 'none'}\n data-legend-row-tint-transition=\"200ms\"\n style={{ transition: 'fill 200ms ease-out, opacity 200ms ease-out' }}\n />\n {/* Round 197 / Loop: swatch dot scales r 5.5 → 7 when its\n row is hovered or pinned. Pre-R197 the swatch was a\n flat constant size — the row got R55/R61 fill brighten,\n R105 bg tint, R144 1px row lift, R181 pin ring, but\n the swatch dot at the row's *visual identity anchor*\n stayed still. Adding the size response gives the\n swatch its own click/hover feel and visually rhymes\n with R177 hub hover-lift (hub ring r 14→17). Stays\n well inside the r=8 R181 pin ring (7 + 0 stroke vs\n 8 - 0.75 inner ≈ 7.25) so the two layers don't fight.\n CSS r-as-property is interpolatable by Chrome/Safari/\n FF post-2020. Geometry-unchanged at rest, so the\n overlap test sees the same baseline. */}\n {/* Round 295 / Loop: legend swatch base radius 5.5 → 6.\n Pre-R295 the swatch idle radius was 5.5 and R197\n grew it to 7 on hover/pin — a 1.5px jump (~27%\n area). Bumping idle to 6 keeps hover at 7, so\n the hover delta becomes a smoother 1px (~36%\n area but 17% radius). Side effect: idle swatch\n reads slightly more like an authored color\n anchor than a faint dot — matches the post-R294\n 减法 register where the legend is one of the\n few remaining persistent canvas-side info\n surfaces. Geometry stays well inside the r=8\n R181 pin ring (6 + 0 stroke vs 8 - 0.75 inner\n ≈ 7.25). data-legend-swatch is unchanged so\n R197 / R55 / R61 tests probe the same handle. */}\n <circle\n cx=\"16\" cy={row.y0}\n r=\"6\"\n fill={row.fill}\n data-legend-swatch={row.key}\n data-legend-swatch-state={isPinned ? 'pinned' : isRowHovered ? 'hover' : 'idle'}\n style={{\n r: isRowHovered || isPinned ? '7px' : '6px',\n transition: 'r 150ms ease-out',\n } as React.CSSProperties}\n />\n {/* R61 pinned-state ring — concentric stroke at r=8 in\n the row colour, draws OUTSIDE the swatch so it\n doesn't fight the fill colour the user is matching.\n Round 181 / Loop: the ring used to mount/unmount\n with the conditional render, snapping on every\n pin/unpin. Now always mounted with opacity gated\n by isPinned + a 150ms transition so the ring\n eases in on pin and out on unpin — same gesture\n vocabulary the R165/R180 smooth-pin-mirror family\n uses for the chip-row pin chips. strokeWidth=1.5\n is the R51 overlap-test sentinel but the test\n selector is gated to g[data-node] ancestors,\n so this legend-internal circle is invisible to\n that probe. pointerEvents:none so the ring can't\n intercept the row click that produced it. */}\n {/* Round 402 / Loop: legend pin-ring strokeWidth 1.5\n → 1.75. Sibling visual-weight bump (12th anchor)\n to R385 hub hover-ring strokeWidth 1.5 → 1.75 —\n both are pin/hover state indicators painted as\n stroke-only circles outside their target swatch\n with the R51 sentinel value 1.5. R402 lifts to\n 1.75 (matching R385's choice) so the pin signal\n reads slightly heavier without crossing the\n R51 sentinel band (3 reserved for offline node).\n The R51 selector is gated to g[data-node]\n ancestors so this legend-internal circle (lives\n under a <g data-legend-status>) is invisible\n to the probe — same lesson R177/R385 documented.\n Visual-weight bump family (12 anchors now):\n R287 minimap viewport stroke 1 → 1.5\n R295 legend swatch radius 5.5 → 6\n R359 recent-row pip radius 1.6 → 1.8\n R360 hub digit fontSize 11 → 12\n R361 edge-badge digit fontSize 10 → 11\n R365 hub-highlight radius 5 → 5.5\n R367 edge-badge rest stroke 1 → 1.25\n R374 pressure-bar height 1.5 → 2\n R383 recent-row pip radius 1.8 → 2.0\n R384 minimap online dot 1.7 → 1.9\n R385 hub hover-ring stroke 1.5 → 1.75\n R402 legend pin-ring stroke 1.5 → 1.75 (this round)\n R181 always-mount opacity gate + 150ms transition\n + pointerEvents:none all preserved. data-legend-\n pin-ring-stroke-width attr exposes the value for\n tests. */}\n {/* Round 477 / Loop — legend pin-ring gains a filter:\n drop-shadow glow on isPinned. Extends R476's\n drop-shadow idiom from hub-digit (focal scope)\n to the legend-row pin-ring (sibling pin-state\n surface). When a status row is pinned, the\n concentric ring around the swatch now lights\n up with a colour-matched halo using row.fill,\n reinforcing \"this filter is locked\" via a\n glow layer above the R402 sw bump + R181\n opacity fade-in.\n Hue: row.fill at 0.55 alpha — picks up each\n status tier's signature colour (working green /\n idle teal / offline slate). 3px blur stays\n subtle but unmistakable when the row is locked.\n Reduced-motion users skip the filter via R29\n a11y blanket (transition-duration → 0.001ms\n so the glow appears/disappears instantly with\n pin toggle).\n Filter is paint-only — bbox unchanged, R51\n overlap-test gated to g[data-node] descendants\n so this legend-internal ring is invisible to\n the probe anyway. Transition list extends to\n include 'filter 200ms ease-out' so the glow\n eases under the same cadence as opacity. */}\n <circle\n cx=\"16\" cy={row.y0} r=\"8\"\n fill=\"none\"\n stroke={row.fill}\n strokeWidth=\"1.75\"\n opacity={isPinned ? 1 : 0}\n data-legend-pin-ring={row.key}\n data-legend-pin-ring-pinned={isPinned ? 'true' : 'false'}\n data-legend-pin-ring-stroke-width=\"1.75\"\n data-legend-pin-ring-glow={isPinned ? 'true' : 'false'}\n style={{\n pointerEvents: 'none',\n filter: isPinned\n ? `drop-shadow(0 0 3px ${row.fill}88)`\n : undefined,\n transition: 'opacity 150ms ease-out, filter 200ms ease-out',\n }}\n />\n {/* Round 219 / Loop: legend row text gains the same\n letter-spacing pin signature R218 added to group\n labels — 0px → 0.5px when isPinned. Pre-R219 the\n legend row's hover and pin states shared fill\n colour (R55/R61 both brighten to legendHeadline)\n so the text was typographically identical at the\n letter-form level for transient hover vs sticky\n pin. R181 pin ring + R197 swatch grow + R143 row\n lift differentiated the row chrome; R219 adds the\n text-level signature so the LABEL itself reads\n \"locked in\" at type. Mirror of R218's group label\n treatment — chrome-level + type-level pin\n vocabulary now unified across both interactive\n label surfaces (group + legend). transition adds\n `letter-spacing 150ms` alongside R55 fill 150ms;\n same ease pace, same beat. */}\n <text\n x=\"30\" y={row.y1}\n fill={hoveredStatus === row.key || isPinned ? pal.legendHeadline : pal.legendText}\n fontSize=\"11\"\n fontFamily=\"monospace\"\n /* Round 364 / Loop: legend-row label fontWeight 400\n → 500. Sibling typography lift to R363 recent-row\n text fw 400 → 500. Both surfaces render small\n monospace text against panel chrome at fontSize\n 9-11 where SVG-default fw 400 sits at the\n legibility floor. font-medium tier (500) gives\n the label a more deliberate-data register.\n The R309 per-row count text (separate element\n below at x=215 textAnchor=end) keeps its own\n fontWeight 600 inline override, so the count >\n label hierarchy stays intact at the legend\n scope same as R363 holds it at the recent-row\n scope:\n legend label fw 500 (R364, this round)\n legend count fw 600 (R309)\n recent alias fw 500 (R363)\n recent count fw 600/700 (R320)\n data-legend-row-label-font-weight attr exposes\n the value for tests. R219 letter-spacing pin\n tween + R55 fill transition + R181 always-mount\n pin ring all preserved. */\n fontWeight=\"500\"\n data-legend-row-label={row.key}\n data-legend-row-label-pinned={isPinned ? 'true' : 'false'}\n data-legend-row-label-hovered={!isPinned && hoveredStatus === row.key ? 'true' : 'false'}\n data-legend-row-label-font-weight=\"500\"\n /* Round 433 / Loop: legend-row text extends from\n R219's pin-only letter-spacing (0px → 0.5px on\n isPinned) to a 3-tier scale matching the R432\n group-label pattern:\n rest → 0px\n hoveredStatus → 0.25px ← this round\n isPinned → 0.5px (R219 preserved)\n Pre-R433 hover already brightened the fill\n (hoveredStatus===row.key || isPinned matches the\n legendHeadline branch) but the letter-form\n stayed dead-typographic on transient hover —\n only the pin tier carried a kerning signature.\n R433 adds the missing mid tier so hover\n telegraphs through BOTH fill brighten AND a\n subtle 0.25-px kerning spread, mirroring\n R427/R431/R432 at legend-row scope. Pin tier\n still wins so the locked vs preview distinction\n at the type level stays intact.\n Hover-letter-spacing family extension (9 anchors\n now): R344/R345/R347/R351/R420/R427/R431/R432/\n R433. 3-tier letter-spacing pattern now spans 4\n surfaces (node-alias R427, edge-badge R431,\n group-label R432, legend-row R433). R55 fill\n 150ms + R219 letter-spacing 150ms transition\n untouched — additive conditional case. */\n /* Round 475 / Loop — final closure of the panel-row\n text scope cadence-sync. R473 lifted the legend-\n row TINT RECT to 200ms; R474 lifted the recent-\n row TEXT to 200ms; R475 closes the matching\n legend-row text desync — fill + letter-spacing\n both 150 → 200ms ease-out. After R475 the 3-tier\n panel-row cadence family is fully 200ms across\n BOTH rect and text at every panel-row scope\n (group-label / recent-row / legend-row). Hover/\n pin state-flip at any panel-row tier reads as\n one motion-coherent unit. data-legend-row-\n label-transition='200ms' attr exposed for tests.\n R433 3-tier letter-spacing values (0/0.25/0.5)\n unchanged; R55 fill brighten unchanged — only\n the timing axis shifts. */\n data-legend-row-label-transition=\"200ms\"\n style={{\n transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out',\n letterSpacing: isPinned ? '0.5px' :\n hoveredStatus === row.key ? '0.25px' : '0px',\n }}\n >{row.label}</text>\n {/* R95: live count anchored to the right edge of the\n panel (x=215, after the flow-arrow swatch). Same\n counts the chip-row shows (\"3 working\" etc.) but\n here next to the swatch the user is matching —\n saves crossing the canvas to the chip row for\n the number. text-anchor=end aligns the column\n visually like a table. pointerEvents:none so the\n count doesn't intercept the row hover hitbox.\n\n Round 204 / Loop: count text recedes when the\n tier is empty. Pre-R204 the \"0\" sat at the same\n opacity 0.65 as \"12\" — visually identical, so\n the eye got zero signal that a status tier was\n empty unless the operator read the digit. R204\n drops empty rows to 0.30 (dark) / 0.28 (light)\n so empty tiers fade into the panel chrome while\n populated tiers stay visually prominent. R204 a\n crossing zero / coming back from zero eases via\n the existing 150ms opacity transition. data-\n legend-count-empty exposes the binary signal\n for tests. */}\n {/* Round 239 / Loop: legend count text gains a tier-\n coloured fill on hover/pin, completing the hover-\n deepen-own-hue idiom at this surface. Pre-R239 the\n count digit's opacity bumped 0.65→0.95 on hover\n (R204 thinning) but its fill stayed at the neutral\n pal.legendText gray — same digit, brighter gray,\n no tier identity. R239 flips fill to row.fill\n (green/teal/slate per tier) when the row is\n hovered OR pinned, so the count lights up in its\n OWN colour, matching the swatch directly above it.\n The whole row now reads as one tier-coloured unit\n under cursor (swatch + label + count); R55/R197\n already do this for swatch + label, R239 closes\n the trio at the count. Opacity transition stays at\n 150ms; fill joins the same transition list at 150ms\n so the colour shift eases alongside the opacity\n ramp. data-legend-count-fill exposes the active\n fill state for tests; empty tiers (row.count===0)\n stay at pal.legendText regardless — empty doesn't\n get to claim tier identity. 8th surface in the\n hover-deepen-own-hue family. */}\n {/* Round 274 / Loop: legend per-row count picks up\n tabular-nums (sibling treatment to R225's recent-\n signal panel header flow-count + R230's group-\n label pip strip). The text uses fontFamily=\n 'monospace' which is typically tabular by\n nature, but some monospace implementations have\n subtle digit-pair width variance (e.g., '0' vs\n '1' at the visual boundary). Explicit\n fontVariantNumeric: 'tabular-nums' is belt-and-\n suspenders: locks digit widths regardless of\n the rendered monospace font, so the count\n column stays planted as offline/idle/working\n counters roll across 9→10 / 99→100 thresholds.\n Pure CSS-level addition, no layout shift.\n 10th surface in the info-density tabular-nums\n sweep family. */}\n {/* Round 309 / Loop: legend per-row count gains\n fontWeight=600 (semibold). The count is the\n DATA the operator scans (how many working /\n idle / offline nodes); the row label is the\n stable status word. Default weight (400) on\n both makes them visually equal — but a glance-\n first read pattern needs the digit to register\n faster than the label.\n fontWeight=600 gives the digit semibold\n emphasis (matching the h2 'Command mesh'\n semibold in the title block) while the row\n label stays at default 400/normal — a clean\n 'digit semibold > label regular' hierarchy\n that makes the count the optical anchor in\n each row. After R307+R308 simplified the\n labels to single status words, the count\n becomes proportionally more important; this\n round emphasizes that role typographically. */}\n {/* Round 446 / Loop: legend per-row count fontWeight\n lift 600 → 700 on isPinned. Mirror of R444 group-\n label-count + R445 recent-row-count at the\n legend-row scope. Closes the 3-panel-row family\n for the \"data tightens under attention\" pattern —\n every panel-row count now responds to pin with a\n typographic-weight bump:\n R444 group-label-count 500 → 600\n R445 recent-row-count 600 → 700 (cold-pin route)\n R446 legend-row-count 600 → 700 ← this round\n Hover gate (hoveredStatus===row.key) keeps rest\n fw=600 so the locked-vs-preview distinction at\n the type level stays intact — same gate R433 used\n on the parent <text> letter-spacing tween. R309\n fw=600 baseline + R204 empty-row opacity dim +\n R225 tabular-nums all preserved. transition list\n extends to include 'font-weight 150ms ease-out'\n matching R433 fill/letter-spacing cadence.\n data-legend-count-pinned + -font-weight attrs\n exposed for tests. */}\n <text\n x=\"215\" y={row.y1}\n textAnchor=\"end\"\n fill={row.count > 0 && (hoveredStatus === row.key || isPinned) ? row.fill : pal.legendText}\n fontSize=\"11\"\n fontFamily=\"monospace\"\n fontWeight={isPinned ? '700' : '600'}\n /* Round 449 / Loop: legend-row count active-state\n opacity 0.95 → 1.0 on (hoveredStatus===row.key\n || isPinned). Pre-R449 R204 lifted populated-row\n active opacity from rest 0.65 to 0.95 — visibly\n brighter but kept a 5 pct alpha gap (1 - 0.95).\n R449 closes the gap to 1.0 so the active count\n reads as confidently present alongside the R446\n fw=600→700 + R433 letter-spacing tween. Theme-\n consistency / canvas-presence family extension\n (7th anchor on the active-presence lift sub-\n family): R370 hub hover-ring 0.7→0.8, R371 edge-\n badge rest 0.82→0.85, R372 minimap offline-dot\n 0.5→0.6, R386 hub-highlight idle 0.9→0.95, R387\n hover-detail panel 0.94→0.97, R429 label-card\n body 0.94→1.0, R449 legend-count active 0.95→1.0\n ← this round. Empty-row opacity (R204: 0.28\n light / 0.30 cyber) and idle 0.65 rest both\n preserved. */\n opacity={row.count === 0\n ? (isLight ? 0.28 : 0.30)\n : (hoveredStatus === row.key || isPinned ? 1 : 0.65)}\n data-legend-count={row.key}\n data-legend-count-empty={row.count === 0 ? 'true' : 'false'}\n data-legend-count-pinned={isPinned ? 'true' : 'false'}\n data-legend-count-font-weight={isPinned ? '700' : '600'}\n data-legend-count-fill={row.count > 0 && (hoveredStatus === row.key || isPinned) ? 'tier' : 'neutral'}\n style={{ pointerEvents: 'none', transition: 'opacity 150ms ease-out, fill 150ms ease-out, font-weight 150ms ease-out', fontVariantNumeric: 'tabular-nums' }}\n >{row.count}</text>\n </g>\n );\n })}\n {/* Flow-arrow swatch tracks the offline row — R106 shifted\n rows down by 8 px to make space for the panel header so\n this moves from y=72 to y=80. Drop its pointerEvents so\n the offline legend row stays hoverable (R55). It's\n decoration, no need to receive events. */}\n {/* Round 254 / Loop: legend flow-arrow swatch stroke\n transition for theme toggle (cyber #67e8f9 ↔ light\n #10b981). Last theme-driven legend element snap. */}\n {/* Round 277 / Loop: flow-arrow path tracks new offline-row\n cy=68 after the legend panel compress (was 80 pre-R277).\n Endpoints follow the offline row to keep the swatch\n logically tied to the row it demonstrates; control point\n proportionally shifts so apex stays mid-arc between\n rows 2 and 3. */}\n <path d=\"M140,68 Q164,44 196,68\" fill=\"none\" stroke={pal.flowEdge} strokeWidth=\"3\" markerEnd=\"url(#topo-arrow)\" data-legend-flow-arrow style={{ pointerEvents: 'none', transition: 'stroke 200ms ease-out' }} />\n </g>\n\n {/* Round 282 / Loop: sleep2agi brand watermark per Vincent\n 5215 ask (relayed via 通信龙). Plain monospace text at\n canvas bottom-left (the only fully-empty corner — top\n corners hold recent-signal + legend panels, bottom-\n right holds the chrome strip). No icon yet — public/\n has only favicon.svg (small abstract network icon\n with hardcoded #0a0a1a dark bg that wouldn't blend on\n light theme) + intern_avatar.png (书生 brand-specific).\n Without a sleep2agi-specific crescent/lockup asset,\n R282 ships a low-opacity text-only mark; R283+ can\n swap in the real logo if Vincent provides the asset.\n\n Position: x=16 (matches the 16-unit SVG inset that the\n corner panels use); y=672 (≈12 px from viewBox bottom\n y=680, descender ≈ y=675, so the entire glyph sits\n clear of the bottom edge). Theme-aware fill:\n pal.legendText (cyber #94a3b8 slate-400 ↔ light\n #475569 slate-600). 0.4 opacity makes it a\n watermark — present but not visually loud. Pointer-\n events:none so it can't intercept clicks on the\n canvas backdrop.\n\n Note: the brand mark is INTENTIONALLY in a corner\n that no overlay/panel occupies, AND it's purely\n decorative additive after 7 rounds of 减法 (R275-\n R281). Adds 1 small text element back into the\n canvas — but Vincent specifically asked for it. */}\n {/* Round 289 / Loop: brand watermark picks up letterSpacing\n 0.5px. For a 9-character wordmark at fontSize 11 monospace,\n 0.5px between characters (8 gaps × 0.5 = 4px total\n widening) lifts \"sleep2agi\" from \"body text that happens\n to be a name\" to \"deliberate wordmark register\". Same\n R285-family idiom (kicker tracking-widest, title\n tracking-tight) applied to the brand mark — letter-\n spacing as typographic intent. Stays well inside the\n bottom-left corner; opacity 0.4 unchanged so the\n watermark stays a watermark. */}\n <text\n x=\"16\" y=\"672\"\n fontSize=\"11\" fontFamily=\"monospace\" fontWeight=\"600\"\n letterSpacing=\"0.5\"\n fill={pal.legendText}\n opacity=\"0.4\"\n data-topo-brand-watermark\n style={{ pointerEvents: 'none', transition: 'fill 200ms ease-out' }}\n >sleep2agi</text>\n {/* v0.10.0 Hero 3 Wave 1 / RFC §3.I (Vincent 5215 + 通信龙\n lead-autonomy Q4 dual-anchor minimal): canvas top-left\n crescent moon brand mark, visible ONLY when the\n recent-signal panel is hidden (composes with §3.C). The\n two never co-exist — when flowLinks.length > 0 the\n recent-signal panel occupies the (16,16) corner; when\n flowLinks.length === 0 the corner is empty and the\n brand crescent fills it. R310 title-block crescent\n remains the primary mark; this one is the secondary\n canvas-internal anchor (Q4 dual-anchor minimal).\n Inline path geometry identical to public/sleep2agi-\n logo.svg + the title-block SVG (mask = outer disc minus\n offset inner disc → crescent). Local mask id\n (`s2a-canvas-corner-mask`) prevents collision with the\n other inline crescents. opacity 0.35 (slightly more\n subtle than the bottom watermark's 0.4 since the\n canvas top-left has more contrast headroom). */}\n {/* Round 327 / Loop: canvas brand crescent joins the always-\n mount-opacity-gate family (R181/R182/R183/R213×2/R214/R215/\n R221/R222/R223). Pre-R327 the crescent conditionally\n mounted on `flowLinks.length === 0` — first flow arriving\n SNAP-removed it, last flow leaving SNAP-added it. The\n recent-signal panel at the same (16,16) corner has the\n same snap problem on its conditional-mount path; this\n round closes the crescent's snap-on-mount (the panel's\n own crossfade is a larger surface, deferred).\n\n Always-mounted with `opacity={flowLinks.length === 0 ?\n 0.35 : 0}` + 300ms ease-out transition: when the panel\n hides, the crescent fades in over 300ms; when the panel\n shows, the crescent fades out. Same opacity ramp time\n the R175 panel-fade-in uses for cascade rhythm. data-\n topo-brand-canvas-mark-visible exposes the gate for\n tests. */}\n <g\n opacity={flowLinks.length === 0 ? 0.35 : 0}\n data-topo-brand-canvas-mark\n data-topo-brand-canvas-mark-visible={flowLinks.length === 0 ? 'true' : 'false'}\n style={{ pointerEvents: 'none', transition: 'opacity 300ms ease-out, fill 200ms ease-out' }}\n >\n <defs>\n <mask id=\"s2a-canvas-corner-mask\">\n <rect x=\"0\" y=\"0\" width=\"28\" height=\"28\" fill=\"black\" />\n <circle cx=\"14\" cy=\"14\" r=\"12\" fill=\"white\" />\n <circle cx=\"17.5\" cy=\"13\" r=\"10\" fill=\"black\" />\n </mask>\n </defs>\n <rect\n x=\"16\" y=\"16\" width=\"28\" height=\"28\"\n fill={pal.legendText}\n mask=\"url(#s2a-canvas-corner-mask)\"\n />\n </g>\n </svg>\n\n {/* Round 30 / Loop: minimap. Big fleets in fullscreen mode at high\n zoom let users lose their position — the minimap shows the\n whole topology miniaturised plus a viewport rectangle so the\n user always knows where they are. Click anywhere to recenter\n the canvas there. Only mounted when the view is non-default\n (zoomed or panned) since at 1× centered the minimap and the\n canvas show the same thing. HTML overlay so it stays fixed\n while the SVG transforms. */}\n {(() => {\n const isDefaultView = Math.abs(view.zoom - 1) < 0.01 && Math.abs(view.x) < 1 && Math.abs(view.y) < 1;\n if (isDefaultView || (onlineNodes.length + offlineNodes.length) === 0) return null;\n const MW = 120, MH = 82;\n const sx = MW / VIEWBOX_W, sy = MH / VIEWBOX_H;\n const rectX = (-view.x / view.zoom) * sx;\n const rectY = (-view.y / view.zoom) * sy;\n const rectW = (VIEWBOX_W / view.zoom) * sx;\n const rectH = (VIEWBOX_H / view.zoom) * sy;\n return (\n <div\n /* Round 332 / Loop: minimap container rounded-md → rounded-lg\n (6 → 8 px) — continues the R330-R331 corner-radius cascade\n onto the minimap overlay card. The minimap is a smaller\n surface than the inner SVG panels (120×82 vs 230×88), so\n it sits one tier inward in the size hierarchy: panels at\n rx=10 (R331), minimap at rounded-lg=8 (R332), inner\n detail card at rx=8 (codex 8f981a9). Same 2 px gradient\n step the rest of the cascade uses. Geometry-safe — the\n minimap is an HTML overlay positioned `bottom: 56` +\n `right-4`, no impact on SVG layout or topo-overlap-test. */\n className=\"absolute right-4 rounded-lg border shadow-lg shadow-black/30 overflow-hidden anet-fade-in anet-topo-chip-focus\"\n /* Round 254 / Loop: minimap container theme transitions —\n background-color, border-color, color (used for SVG\n currentColor inside) all ease at 200ms alongside the\n R254 wrapper + R247 panel treatments. */\n style={{ bottom: 56, background: pal.legendBox.fill, borderColor: pal.containerBorder, cursor: 'crosshair', color: pal.legendAccent, transition: 'background-color 200ms ease-out, border-color 200ms ease-out, color 200ms ease-out' }}\n // R157: minimap a11y completion. Pre-R157 the element had\n // role=\"img\" + aria-label but no tabIndex / onKeyDown — it\n // was clickable for mouse users (recenter to where you\n // clicked) but tab-unreachable. role=\"img\" was also wrong\n // for an interactive surface; role=\"button\" matches the\n // canonical pattern R116 / R139 / R140 / R151 / R152 use.\n // Keyboard activation can't compute a click position, so\n // Enter / Space falls back to resetView() — same gesture\n // as the dedicated reset button (R104). Click semantics\n // unchanged; only added a clarifying tail to the aria-\n // label + title. anet-topo-chip-focus picks up R155's\n // cyan outline via color: pal.legendAccent inline so the\n // currentColor inherits cleanly on the rounded card.\n role=\"button\"\n tabIndex={0}\n aria-label=\"Topology minimap — click to recenter, Enter to reset view\"\n title=\"Minimap · click to recenter · Enter to reset view\"\n onClick={(e) => {\n const r = e.currentTarget.getBoundingClientRect();\n const fx = (e.clientX - r.left) / r.width;\n const fy = (e.clientY - r.top) / r.height;\n setView(prev => ({\n ...prev,\n x: VIEWBOX_W / 2 - fx * VIEWBOX_W * prev.zoom,\n y: VIEWBOX_H / 2 - fy * VIEWBOX_H * prev.zoom,\n }));\n }}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n resetView();\n }\n }}\n // R346: viewport rect hover affordance driven by parent.\n onMouseEnter={() => setHoveredMinimap(true)}\n onMouseLeave={() => setHoveredMinimap(false)}\n onFocus={() => setHoveredMinimap(true)}\n onBlur={() => setHoveredMinimap(false)}\n data-topo-minimap\n data-topo-minimap-hovered={hoveredMinimap ? 'true' : 'false'}\n >\n <svg width={MW} height={MH} viewBox={`0 0 ${MW} ${MH}`} style={{ display: 'block' }}>\n {/* Round 198 / Loop: minimap dots gain smooth status\n transitions. Pre-R198 a session flipping working→idle\n or going offline made the minimap dot snap in a single\n frame (opacity 0.9→0.5, r 1.7→1.2, fill swap). The\n canvas itself eases all of these via R167 status-ring\n transitions, R10 freshness, R3 fade-in — but the\n minimap mirror was still snap-cut. Adding opacity /\n fill / r to the CSS transition list lets a status\n change ripple smoothly through both views at the\n same rhythm. 200ms matches the R167 nodeStrokeWidth\n interpolation on the main canvas so the two surfaces\n visually flip in sync. r-as-property is well supported\n Chrome ≥ 95 / Safari ≥ 16 / FF ≥ 70 (same support\n matrix R197 just leveraged on the legend swatch).\n data-topo-minimap-dot exposes each dot for the test;\n data-topo-minimap-dot-online encodes the binary status\n used by the visible attributes. */}\n {[...onlineNodes, ...offlineNodes].map(s => {\n const p = nodePositions[s.alias];\n if (!p) return null;\n const sseN = (s.network_id ? sseSessions[`${s.network_id}:${s.alias}`] : undefined) ?? sseSessions[s.alias];\n const isOn = s.status !== 'offline' || !!sseN;\n const st = nodeStatus(s, isOn, isLight);\n return (\n /* Round 372 / Loop: minimap offline-dot opacity\n 0.5 → 0.6. Sibling stale-state legibility lift\n to R358 freshness ramp floor 0.25 → 0.30 + R317\n subordinate-text-lift family. Pre-R372 R198\n drew offline dots at α=0.5 (44 % below online\n 0.9). The minimap is a small overlay against\n the canvas backdrop — at α=0.5 offline dots\n sat at the legibility floor when the minimap\n mounted (only on non-default view). R372 lifts\n offline 0.5 → 0.6 for +20 % relative presence;\n online stays at 0.9 so the offline/online\n contrast ratio is now 0.6/0.9 ≈ 0.67 (vs prior\n 0.5/0.9 ≈ 0.56) — still a clear two-tier\n distinction. R198 opacity + fill + r transition\n list preserved so status flips still ease\n smoothly. data-topo-minimap-dot-opacity attr\n exposes the resolved value for tests. */\n <circle\n key={s.alias}\n cx={p.x * sx} cy={p.y * sy}\n /* Round 384 / Loop: minimap online dot radius 1.7\n → 1.9. Sibling visual-weight bump (10th anchor)\n to R383 recent-row pip 1.8 → 2.0. R198 designed\n the dots at 1.7 (online) / 1.2 (offline) — at\n the minimap's 120 × 82 scale these read clearly\n but the online ↔ offline contrast was modest\n (1.7/1.2 = 1.42×). R384 bumps online to 1.9 so\n the tier delta widens to 1.58× (1.9/1.2). Pair\n completes minimap-dot legibility polish:\n R358 (era R372) offline opacity 0.5 → 0.6\n R384 online radius 1.7 → 1.9 (this round)\n R198 transition list (opacity + fill + r 200ms)\n preserved so status flips still ease smoothly.\n data-topo-minimap-dot-radius attr exposes the\n resolved value for tests. */\n /* Round 392 / Loop: minimap online dot opacity\n 0.9 → 0.95. Theme-consistency / canvas-presence\n polish family (7th anchor) — mirrors R386's\n hub-highlight idle 0.9 → 0.95 lift on the\n minimap surface: the online-dot's idle alpha\n gap (0.10 against full presence) halves to\n 0.05, so the live-fleet anchors on the minimap\n read more confidently. Offline dot stays at\n R372 0.6 — the binary online/offline contrast\n ratio shifts from 0.6/0.9 ≈ 0.67 to 0.6/0.95\n ≈ 0.63, preserved as a clear two-tier\n distinction. R198 opacity + fill + r transition\n list + R384 r=1.9 + R372 offline 0.6 all\n preserved. data-topo-minimap-dot-opacity attr\n bumps to '0.95' for tests. */\n /* Round 421 / Loop: online dot opacity 0.95 → 1.0\n on minimap container hover. Sibling to R346\n viewport rect strokeWidth/opacity hover tween.\n When the user hovers the minimap container,\n the live-fleet anchors brighten from R392\n baseline (0.95) to full opacity in concert\n with the R346 viewport rect lift. Offline\n stays at R372 0.6 — hover state focuses\n attention on the ACTIVE anchors, not the\n stale ones. data-topo-minimap-dot-opacity\n attr (R392) reflects the resolved hover-\n state value for tests. */\n /* Round 486 / Loop — 3rd anchor in the\n inspection-overrides-encoding pattern. Sibling\n to R484 (recent-row timestamp) + R485 (edge\n particle). When the operator hovers a node\n alias on the main canvas, the matching\n minimap dot lifts to opacity=1.0 regardless\n of the binary online/offline encoding —\n cross-reference cue between canvas focal\n and the minimap wayfinding overlay.\n Pre-R486 an offline node's minimap dot stayed\n at 0.6 even when the operator was inspecting\n it via canvas hover; R486 makes the\n inspection signal jump the minimap dot to\n full presence so the spatial reference is\n unambiguous.\n Encoding survives: data-topo-minimap-dot-\n online preserves the online/offline binary,\n data-topo-minimap-dot-opacity-rest preserves\n the would-be opacity. Only the LIVE painted\n opacity flips on inspection.\n inspection-overrides-encoding family — 3\n anchors now:\n R484 recent-row timestamp\n R485 edge particle\n R486 minimap dot ← this round\n data-topo-minimap-dot-lifted attr exposes\n the override gate. */\n r={isOn ? 1.9 : 1.2}\n fill={st.primary}\n opacity={hoveredAlias === s.alias ? 1 : (isOn ? (hoveredMinimap ? 1 : 0.95) : 0.6)}\n data-topo-minimap-dot={s.alias}\n data-topo-minimap-dot-online={isOn ? 'true' : 'false'}\n data-topo-minimap-dot-opacity={hoveredAlias === s.alias ? 1 : (isOn ? (hoveredMinimap ? 1 : 0.95) : 0.6)}\n data-topo-minimap-dot-opacity-rest={isOn ? (hoveredMinimap ? 1 : 0.95) : 0.6}\n data-topo-minimap-dot-lifted={hoveredAlias === s.alias ? 'true' : 'false'}\n data-topo-minimap-dot-radius={isOn ? 1.9 : 1.2}\n style={{\n transition: 'opacity 200ms ease-out, fill 200ms ease-out, r 200ms ease-out',\n } as React.CSSProperties}\n />\n );\n })}\n {/* viewport rectangle.\n Round 199 / Loop: rect dimensions transition smoothly\n during R169 smoothView arming (discrete zoom button\n clicks + keyboard +/− + reset/fit). Pre-R199 the main\n canvas crossfaded over R168's 280ms opacity blend\n while the minimap viewport rect snap-cut to its new\n x/y/w/h in one frame — exactly the same rhythm\n mismatch R198 just closed for the minimap dots, now\n fixed for the rectangle that frames them.\n\n Gated to smoothView=true so continuous wheel-zoom /\n drag-pan stay snappy (a CSS transition during drag\n would cause the rect to chase the cursor with a\n 280ms lag). Discrete zooms arm smoothView for 350ms,\n long enough to cover the 280ms x/y/w/h transition.\n\n Setting x/y/width/height as CSS PROPERTIES (style.x\n etc.) — same approach R197 used for legend swatch\n r. Modern Chrome/Safari/FF interpolate these.\n\n data-topo-minimap-viewport / -smooth expose state for\n tests. */}\n {/* Round 287 / Loop: minimap viewport rect strokeWidth\n 1 → 1.5. The rect frames the user's current view\n within the full topology — it IS the wayfinding\n indicator. At 1px stroke against a 120×82 mini-\n canvas it was readable but reserved; 1.5px gives\n the boundary clearer presence without crowding the\n miniaturised dots (still r 1.2-1.7) inside.\n Same micro-polish family as R283 monogram stroke\n 1 → 1.5 — small visual-weight bump on a high-\n information element to lift it above ambient\n chrome. opacity 0.9 stays — strokeWidth alone\n does the lifting. */}\n {/* Round 379 / Loop: minimap viewport rect picks up\n strokeLinejoin='round'. Pre-R379 the rect's 4\n corners painted with default 'miter' joins —\n sharp 90° corners with a small miter overshoot\n (≈ strokeWidth × 1.4 = 2.1 px at sw=1.5). R379\n rounds the joins so corners arc smoothly through\n a quarter-circle of radius ≈ strokeWidth/2. At\n sw=1.5 that's a 0.75-px radius — subtle but\n matches the same stroke-softening vocabulary R288\n chrome icons (zoom/reset/fullscreen) and R378\n flow-rail already speak. Geometry-safe: stroke-\n linejoin only affects the corner overshoot, the\n rect's bbox is unchanged. R287 strokeWidth=1.5 +\n R346 hover-state strokeWidth/opacity bump + R199\n smoothView x/y/w/h transition all preserved.\n data-topo-minimap-viewport-linejoin attr exposes\n the value for tests. */}\n {/* Round 393 / Loop: minimap viewport rect rx 0 → 2.\n Pre-R393 the cyan-stroked viewport rect (the frame\n showing what's currently visible on the canvas)\n drew with sharp corners inside the R332 rounded\n minimap container (rx=8). A small frame with sharp\n corners sitting inside a rounded container reads\n as visually loud — the 90° corners catch the eye\n against the soft container edge. R393 adds rx=2\n so the viewport corners get a subtle radius that\n matches the family's softening idiom on a sub-\n element scale. The R379 strokeLinejoin='round'\n already softens stroke joins; R393 adds a complete\n geometric soften via rx.\n Corner-radius cascade (7 anchors now):\n R330 canvas rx 12\n R331 panels rx 10\n R332 minimap container rx 8\n R375 Layout-toggle rx 8\n R376 nodeSize/zoom rx 8\n R390 hover-detail rx 10\n R393 minimap viewport rx 2 (this round, sub-element)\n The 2-px radius is intentionally small — the\n viewport rect is typically only 30-50px wide,\n where rx=2 reads as \"rounded enough to not snap\"\n without feeling pillowy. data-topo-minimap-\n viewport-rx attr exposes the resolved value\n for tests. R346 hover-state tweens (strokeWidth\n + opacity) preserved verbatim. */}\n <rect\n x={Math.max(0, rectX)} y={Math.max(0, rectY)}\n width={Math.max(0, Math.min(MW - Math.max(0, rectX), rectW))}\n height={Math.max(0, Math.min(MH - Math.max(0, rectY), rectH))}\n rx=\"2\"\n fill=\"none\" stroke={pal.legendAccent}\n // R346: strokeWidth + opacity tween on container hover.\n strokeWidth={hoveredMinimap ? '1.75' : '1.5'}\n strokeLinejoin=\"round\"\n /* Round 450 / Loop · milestone: minimap viewport rest\n opacity 0.9 → 0.95. Closes half the alpha gap on\n the wayfinding indicator while preserving the\n R346 hover delta to 1.0. Pre-R450 the rest viewport\n sat at 0.9 (10 pct alpha gap) — adequate but\n under-confident for the user's primary \"you are\n here\" indicator on the minimap. R450 lifts to 0.95\n so the rest read is more present without erasing\n the hover lift cue (the +0.05 rest-to-hover delta\n is small but pairs with R346 sw 1.5→1.75 to keep\n hover clearly distinguishable). Sibling to R449\n legend-count active opacity 0.95→1.0 — same\n \"close the active-presence alpha gap\" idiom now\n applied to the REST tier of the wayfinding rect\n (the minimap viewport stays at canvas-presence\n register even when un-hovered since it's the\n spatial referent). Theme-consistency / canvas-\n presence family (8th anchor on the active-\n presence lift sub-arc).\n R287 strokeWidth=1.5 + R379 strokeLinejoin='round'\n + R346 hover-state tweens + R393 rx=2 + R199\n smoothView x/y/w/h transition all preserved. */\n opacity={hoveredMinimap ? '1' : '0.95'}\n data-topo-minimap-viewport\n data-topo-minimap-viewport-rx=\"2\"\n data-topo-minimap-viewport-smooth={smoothView ? 'true' : 'false'}\n data-topo-minimap-viewport-hover={hoveredMinimap ? 'true' : 'false'}\n data-topo-minimap-viewport-linejoin=\"round\"\n /* Round 481 / Loop — 6th anchor in the drop-shadow\n visual-polish family. New gate type: ZOOM STATE.\n When current canvas zoom > 1.5x (50% above the\n default 1.0 baseline), the minimap viewport rect\n gains a soft cyan halo signaling \"you're zoomed\n in beyond default\". The minimap viewport already\n shrinks as you zoom in (rectW = VIEWBOX_W /\n view.zoom * sx, so at zoom=2 it halves) — the\n glow tells you the wayfinding marker is now\n scaled-down rather than at canvas-default size.\n Drop-shadow family — 6 gate types covered:\n R476 hub digit hover-gated\n R477 legend pin-ring pin-gated\n R478 freshness pip freshness-gated\n R479 group label pin-gated\n R480 edge badge hot-lane-gated\n R481 minimap zoom-gated ← this round\n 6 distinct semantic gates (user interaction\n transient/sticky × 2, data freshness, data\n volume, canvas zoom state). Each anchor uses\n hue family appropriate to its semantic context.\n Hue: pal.legendAccent at 0x80 alpha — matches\n the existing R107 tint family and R478/R479\n cyan-tone choices. 2-px blur reads as subtle\n (the minimap viewport is small, ~120×82 px).\n Filter is paint-only — bbox unchanged. transition\n list extends to include 'filter 200ms ease-out'\n so the glow eases when zoom crosses 1.5x. */\n data-topo-minimap-viewport-glow={view.zoom > 1.5 ? 'true' : 'false'}\n style={{\n filter: view.zoom > 1.5\n ? `drop-shadow(0 0 2px ${pal.legendAccent}80)`\n : undefined,\n transition: smoothView\n ? 'x 280ms ease-out, y 280ms ease-out, width 280ms ease-out, height 280ms ease-out, stroke-width 200ms ease-out, opacity 200ms ease-out, filter 200ms ease-out'\n : 'stroke-width 200ms ease-out, opacity 200ms ease-out, filter 200ms ease-out',\n } as React.CSSProperties}\n />\n </svg>\n </div>\n );\n })()}\n {/* Round 103 (issue #81): zoom / pan / fullscreen controls — HTML\n overlay so they stay fixed while the SVG content transforms.\n Round 104: Vincent 实测 — the reset action used to be hidden\n behind the \"%\" label (looked like an indicator, not a button).\n Split into a plain % readout + an explicit reset button with\n its own icon + tooltip. */}\n {/* Round 261 / Loop: chrome strip bottom-3 right-3 (12 CSS px) →\n bottom-4 right-4 (16 CSS px) to align HTML overlay padding\n with the SVG corner panels at (16, 16) panel-translate. Pre-\n R261 the SVG panels (at 16 SVG units from canvas edges,\n ≈ 15 CSS px after render-scale ~0.94) and the HTML chrome\n (at 12 CSS px) sat at visually different distances from\n the canvas edges — small but real ~3 CSS px optical\n asymmetry between SVG-layer and HTML-layer overlay padding.\n 16 CSS px ≈ 17 SVG units, unifying the visual padding\n vocabulary across both layers. Sibling change at the\n minimap container (line ~6444, `absolute right-3` →\n `right-4`) keeps the bottom-right corner HTML overlays\n aligned at the same canvas-edge inset. */}\n {/* Round 326 / Loop: chrome strip outer wrapper gap 1.5 → 2\n (6px → 8px between control groups). Pre-R326 the four\n chrome groups (nodeSize segmented S/M/L, zoom +/100%/−,\n reset, fullscreen) sat 6px apart — close enough that on a\n busy canvas with bright cyan accents they read as one\n uniform strip rather than four distinct affordances. Bump\n to 8px gives each group its own visual breath without\n disturbing the bottom-4 right-4 corner-inset alignment.\n Sibling treatment to R298/R299 title-block gap polish on\n the top side of the canvas — both ends of the canvas\n chrome now breathe at the same 8px rhythm. Geometry-safe\n for the overlap-test (chrome is HTML overlay on top of\n the SVG, not part of the viewBox 1000x680 surface; ring\n r=325 / grid gx0 layout untouched). */}\n <div className=\"absolute bottom-4 right-4 flex items-center gap-2 text-xs select-none\" data-topo-chrome>\n {/* #113: node size — S / M / L segmented control (Vincent 4727).\n R154: stable data-* hooks for tests + focus-visible ring so\n keyboard navigation lands somewhere visible against the\n dark canvas (browser default outline often vanishes on\n cyber theme). */}\n {/* Round 264 / Loop: nodeSize wrapper gains theme-toggle\n transition. Pre-R264 the wrapper's bg (pal.legendBox.fill)\n + borderColor (pal.containerBorder) were inline theme-\n conditional, but neither inline transition nor a\n transition-colors className → wrapper snapped on cyber↔\n light flip while the inner S/M/L buttons eased via their\n own transition-colors. Same R254 holdover pattern that\n R263 just closed at the canvas wrapper scope, now at the\n chrome strip's nodeSize sub-wrapper scope. */}\n {/* Round 376 / Loop: nodeSize wrapper rounded-md → rounded-lg.\n Sibling polish to R375 Layout-toggle wrapper. Three\n chrome-strip segmented controls now all share rounded-lg\n at the wrapper tier:\n R375 Layout-toggle wrapper rounded-lg 8 px\n R376 nodeSize wrapper rounded-lg 8 px (this round)\n R376 zoom wrapper rounded-lg 8 px (this round)\n Individual atomic chrome buttons (reset, fullscreen) keep\n rounded-md (6 px) as their own atomic-button tier — the\n chrome strip's typography now expresses a clear two-tier\n hierarchy: 'segmented control container' (rounded-lg)\n vs 'standalone button' (rounded-md). Pure paint change,\n no layout shift. */}\n <div\n className=\"flex items-center rounded-lg border overflow-hidden\"\n data-topo-chrome-nodesize-radius=\"rounded-lg\"\n style={{\n background: pal.legendBox.fill,\n borderColor: pal.containerBorder,\n transition: 'background-color 200ms ease-out, border-color 200ms ease-out',\n }}\n role=\"group\"\n aria-label=\"Node size\"\n data-topo-chrome-fleet-group-trailer\n >\n {([['S', 0.7], ['M', 0.84], ['L', 1]] as const).map(([lbl, v], idx) => {\n const popKey = `size-${lbl}` as 'size-S' | 'size-M' | 'size-L';\n return (\n <button\n key={lbl}\n onClick={() => { popChrome(popKey); pickNodeScale(v); }}\n aria-pressed={nodeScale === v}\n data-topo-chrome-nodesize={lbl}\n data-topo-chrome-nodesize-active={nodeScale === v ? 'true' : 'false'}\n data-topo-chrome-nodesize-popping={chromePopping === popKey ? 'true' : 'false'}\n title={`Node size: ${lbl === 'S' ? 'small' : lbl === 'M' ? 'medium' : 'large'}`}\n // Round 179 / Loop: nodeSize S/M/L active-button hover\n // variant closes the inconsistency with R163 layout\n // toggle and R178 fullscreen. Pre-R179 the active\n // (selected) S/M/L button had bg-cyan-500/15 + text-\n // cyan-300 but NO hover response — the chip looked\n // 'locked', not 'still interactive'. R163 and R178\n // both add hover:bg-cyan-500/20 on the active variant\n // so the active chip stays responsive to mouse. R179\n // closes the trio so all three chrome active-cyan\n // surfaces ship the same gesture.\n // Round 196 / Loop: nodeSize buttons pick up press-state\n // (active:) — selected variant deepens to cyan-500/25,\n // unselected to white/10. Same tier pattern as R196 layout\n // toggle + zoom/reset/fullscreen below.\n // Round 250 / Loop: nodeSize buttons close the chrome-pop\n // family — every clickable chrome button now fires the\n // R186 .anet-chrome-pop scale-pulse on release. R171\n // canvas crossfade (nodeSizeSwitching) keeps masking the\n // node radius change at the global scope; R250 chrome-pop\n // adds the LOCAL button-level confirmation. The two\n // happen simultaneously without conflict — local scale\n // pulse on the button, global canvas dim around it.\n // Milestone round: the entire chrome strip (zoom -/+,\n // ring/grid, fullscreen, S/M/L) now speaks one\n // consistent click vocabulary.\n /* Round 270 / Loop: nodeSize INACTIVE buttons align with\n the Layout toggle's R163 hover-preview pattern. Pre-\n R270 inactive S/M/L used `hover:bg-white/5\n active:bg-white/10` (neutral white tint) while the\n Layout toggle's inactive Ring/Grid uses `hover:bg-\n cyan-500/5 active:bg-cyan-500/15` (faint cyan ghost\n that previews what the active state will look like —\n the active variant is bg-cyan-500/15). Two different\n hover vocabularies for visually-analogous toggle\n controls. R270 unifies inactive toggle hover to\n cyan so all TOGGLE chrome buttons (Layout / nodeSize\n / fullscreen) preview their active state on hover.\n Pure actions (zoom -/+, reset) stay white — they\n aren't toggles, have no active state to preview. */\n // Round 493 / Loop — extends R492 chrome-strip press-feedback\n // family to nodeSize S/M/L buttons. Adds active:scale-95\n // alongside the existing color-deepen (R196) + chrome-pop\n // (R249). transition-transform + duration-200 + ease-out\n // + transform-gpu added since this className previously had\n // transition-colors only — without the transform transition,\n // active:scale-95 would hard-cut. transform-gpu promotes the\n // layer so scale doesn't trigger paint thrash.\n className={`px-2 py-1 transition-colors transition-transform duration-200 ease-out transform-gpu active:scale-95 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset ${idx > 0 ? 'border-l' : ''} ${nodeScale === v ? 'bg-cyan-500/15 text-cyan-300 font-medium hover:bg-cyan-500/20 active:bg-cyan-500/25' : 'hover:bg-cyan-500/5 active:bg-cyan-500/15'}${chromePopping === popKey ? ' anet-chrome-pop' : ''}`}\n style={{ color: nodeScale === v ? undefined : pal.legendText, borderColor: pal.containerBorder }}\n >\n {lbl}\n </button>\n );\n })}\n </div>\n {/* Round 255 / Loop: semantic gap between the fleet-control group\n (node size S/M/L) and the view-control group (zoom / reset /\n fullscreen). Pre-R255 the four groups sat at uniform gap-1.5\n (6px); the spatial signal read as \"4 separate things\" instead\n of \"1 fleet control + 3 view controls\". Doubling the gap before\n the first view-control (ml-1.5 = 6px stacks on top of the\n parent's gap-1.5 = 6px, total 12px) communicates the semantic\n boundary through proximity alone — classic \"law of proximity\"\n layout polish, no extra chrome, no new visual elements.\n data-topo-chrome-view-group-leader marks the boundary surface\n for the test probe; data-topo-chrome-fleet-group-trailer marks\n the nodeSize wrapper's right edge for the gap measurement. */}\n {/* R376 sibling — zoom wrapper rounded-md → rounded-lg.\n Closes the chrome-strip segmented-control corner radius\n cascade (Layout R375 + nodeSize R376 + zoom R376). */}\n <div\n className=\"ml-1.5 flex items-center rounded-lg border overflow-hidden\"\n data-topo-chrome-zoom-wrapper-radius=\"rounded-lg\"\n style={{\n background: pal.legendBox.fill,\n borderColor: pal.containerBorder,\n transition: 'background-color 200ms ease-out, border-color 200ms ease-out',\n }}\n data-topo-chrome-view-group-leader\n data-topo-chrome-zoom-wrapper\n >\n <button\n onClick={() => { popChrome('zoom-out'); zoomByDiscrete(1 / 1.2); }}\n data-topo-chrome-zoom-out\n data-topo-chrome-zoom-out-popping={chromePopping === 'zoom-out' ? 'true' : 'false'}\n // R196: press-state deepens bg one tier above hover (white/5\n // → white/10) so mouse-down has a tactile dim before the\n // R186 icon pop fires on release.\n // R352: `group` lets the inner svg respond via group-hover.\n // R493 — zoom +/− buttons join the chrome-strip active:scale-95\n // press-feedback family (R492 + nodeSize above). transition-\n // transform + duration-200 + ease-out + transform-gpu added\n // since the className had only transition-colors.\n className=\"group px-2 py-1 hover:bg-white/5 active:bg-white/10 transition-colors transition-transform duration-200 ease-out transform-gpu active:scale-95 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset\"\n style={{ color: pal.legendText }}\n aria-label=\"Zoom out\"\n title=\"Zoom out (−)\"\n >\n {/* R186: icon pop on click. CSS animation runs once;\n React removes the class after 240ms so a quick\n re-click can replay. */}\n {/* Round 352 / Loop: zoom-out icon picks up group-hover:\n scale-110 — sibling to R350 reset hover-rotate. Pre-\n R352 hovering the zoom button only changed the bg\n (white/5); the icon inside stayed perfectly still.\n R352 lifts the icon 10% on hover for a tactile \"this\n button does something\" cue. The R186 anet-chrome-pop\n keyframe (220ms scale 1→1.06→1) still owns transform\n during click via CSS-animation precedence over\n transition-transform; after the pop ends + className\n is removed, the group-hover scale-110 picks up\n smoothly. `transform-gpu` hint promotes the svg to\n its own compositor layer for crisper edges during\n the scale tween. Sibling change on zoom-in icon\n below. */}\n {/* Round 454 / Loop: extend R453 chrome reset icon hover\n sw lift to zoom +/− icons via Tailwind arbitrary class\n group-hover:[stroke-width:2.8]. Chrome icon hover sw\n lift family now 5 anchors:\n R208 runtime badge outer ring 1.5 → 2\n R443 runtime badge inner icon 2.4 → 2.8\n R453 chrome reset icon 2.5 → 2.8\n R454 chrome zoom-out icon 2.5 → 2.8 ← this round\n R454 chrome zoom-in icon 2.5 → 2.8 ← this round\n Tailwind v4 arbitrary-value group-hover variant\n resolves [stroke-width:2.8] as a CSS property which\n overrides the static strokeWidth='2.5' attribute on\n hover. transition-[stroke-width] appended to the\n existing transition-transform list so the sw tween\n eases under the same 200ms cadence as R352 group-\n hover:scale-110. R186 anet-chrome-pop keyframe still\n owns transform during click via CSS-animation\n precedence over transition-transform. Sibling change\n on zoom-in icon below. */}\n <svg\n width=\"12\" height=\"12\" viewBox=\"0 0 24 24\"\n fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.5\" strokeLinecap=\"round\"\n aria-hidden\n className={`transition-[transform,stroke-width] duration-200 ease-out group-hover:scale-110 group-hover:[stroke-width:2.8] transform-gpu${chromePopping === 'zoom-out' ? ' anet-chrome-pop' : ''}`}\n data-topo-chrome-zoom-out-icon\n ><path d=\"M5 12h14\" /></svg>\n </button>\n {/* Round 192 / Loop: zoom-level readout span participates in the\n R186 click-feel pop alongside the +/− icons. Pre-R192 a click\n on + or − triggered:\n · icon pop (R186, ~220ms scale 1→1.06→1)\n · canvas crossfade (R169, ~280ms opacity blend)\n · readout text snap (instant — 100% → 120%)\n The readout was the only surface that didn't acknowledge the\n gesture. Reusing the existing .anet-chrome-pop CSS keyframe\n (no new keyframes) lets the \"%\" number gently bounce in\n sync with the icon — same 0.22s ease-out, transform-origin\n center. transform-box: fill-box on the keyframe is\n SVG-specific and harmlessly ignored on this HTML span. The\n base layout classes (px / border / tabular-nums / minWidth)\n stay intact; only when chromePopping is 'zoom-in' or\n 'zoom-out' do we splice in the animation class. Same\n React-clears-after-240ms cleanup R186 already runs, so the\n class can replay on a repeat click. */}\n {/* Round 312 / Loop: chrome strip zoom readout '{N}%'\n picks up `font-medium` (500). Extends the R309-R311\n 'data digit weighs more than label' rule to the\n chrome strip's one data display — the zoom\n percentage. Every other chrome strip text is a\n control (S/M/L buttons, zoom +/-, reset, fullscreen,\n Ring/Grid labels); the percent readout is the only\n live DATA. font-medium (not 600 like the SVG panel\n counts) is a tier below because the readout sits in\n HTML chrome context (lighter visual baseline) where\n 500 reads as 'noticeably data-prominent' without\n competing with the SVG panel counts. tabular-nums\n + minWidth 46 stay (R225 family), the existing R264\n color/border transitions stay, the R186 chrome-pop\n class still toggles on zoom-click. */}\n <span\n className={`px-2 py-1 tabular-nums font-medium border-x text-center${\n chromePopping === 'zoom-in' || chromePopping === 'zoom-out'\n ? ' anet-chrome-pop' : ''\n }`}\n data-topo-chrome-zoom-level\n data-topo-chrome-zoom-level-popping={\n chromePopping === 'zoom-in' || chromePopping === 'zoom-out'\n ? 'true' : 'false'\n }\n data-topo-chrome-zoom-level-hover={hoveredZoomLevel ? 'true' : 'false'}\n onMouseEnter={() => setHoveredZoomLevel(true)}\n onMouseLeave={() => setHoveredZoomLevel(false)}\n style={{\n color: pal.legendText,\n borderColor: pal.containerBorder,\n minWidth: 46,\n display: 'inline-block',\n // R347: letter-spacing hover tween — extends R344/R345\n // hover-letter-spacing family into the chrome strip.\n letterSpacing: hoveredZoomLevel ? '0.5px' : '0',\n // Round 420 / Loop: zoom-level readout gains a SECOND\n // hover axis — fontWeight 500 → 600 on hover. Sibling\n // to R347 (same element, hover letter-spacing tween).\n // The chrome strip's only data display now has a two-\n // axis hover signature (letter-spacing + fontWeight),\n // matching the R416 chip-row chip digit hover-bold\n // pattern at the chrome scope. Pre-R420 hovering only\n // spread the digits 0 → 0.5px; the weight stayed at\n // R332's 'font-medium' (500) baseline. Post-R420\n // hover lifts BOTH letter-spacing AND weight so the\n // percent reads with the same data-tier emphasis\n // intensification the chip-row chips do on hover.\n // Inline fontWeight overrides the className's\n // 'font-medium' since they target the same property.\n // 200ms transition list extends to font-weight for\n // smooth easing. data-topo-chrome-zoom-level-hover\n // attr surfaces the hover state for tests.\n fontWeight: hoveredZoomLevel ? 600 : 500,\n /* Round 264 / Loop: zoom level readout gains theme-toggle\n transition. The span has theme-driven color (pal.\n legendText) + border-x (pal.containerBorder via the\n inline borderColor) but className lacks transition-\n colors — the readout's text + side dividers snapped\n on theme flip while siblings eased. Sibling treatment\n to the nodeSize + zoom wrapper transitions added this\n round. */\n transition: 'color 200ms ease-out, border-color 200ms ease-out, letter-spacing 200ms ease-out, font-weight 200ms ease-out',\n }}\n title=\"Current zoom level\"\n >\n {Math.round(view.zoom * 100)}%\n </span>\n <button\n onClick={() => { popChrome('zoom-in'); zoomByDiscrete(1.2); }}\n data-topo-chrome-zoom-in\n data-topo-chrome-zoom-in-popping={chromePopping === 'zoom-in' ? 'true' : 'false'}\n // R196: press-state (mirror of zoom-out above).\n // R352: `group` lets the inner svg respond via group-hover.\n // R493 — zoom +/− buttons join the chrome-strip active:scale-95\n // press-feedback family (R492 + nodeSize above). transition-\n // transform + duration-200 + ease-out + transform-gpu added\n // since the className had only transition-colors.\n className=\"group px-2 py-1 hover:bg-white/5 active:bg-white/10 transition-colors transition-transform duration-200 ease-out transform-gpu active:scale-95 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset\"\n style={{ color: pal.legendText }}\n aria-label=\"Zoom in\"\n title=\"Zoom in (+)\"\n >\n {/* R186: icon pop on click. Same one-shot CSS animation\n as zoom-out; React removes the class after 240ms. */}\n {/* R352 sibling — zoom-in icon picks up the same\n group-hover:scale-110 family. Mirror change at\n the zoom-out icon above. */}\n {/* R454 sibling — zoom-in icon picks up the same\n group-hover:[stroke-width:2.8] family lift. */}\n <svg\n width=\"12\" height=\"12\" viewBox=\"0 0 24 24\"\n fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.5\" strokeLinecap=\"round\"\n aria-hidden\n className={`transition-[transform,stroke-width] duration-200 ease-out group-hover:scale-110 group-hover:[stroke-width:2.8] transform-gpu${chromePopping === 'zoom-in' ? ' anet-chrome-pop' : ''}`}\n data-topo-chrome-zoom-in-icon\n ><path d=\"M12 5v14M5 12h14\" /></svg>\n </button>\n </div>\n <button\n onClick={() => { armResetSpin(); resetView(); }}\n data-topo-chrome-reset\n data-topo-chrome-reset-spinning={resetSpinning ? 'true' : 'false'}\n data-topo-chrome-reset-hover={hoveredReset ? 'true' : 'false'}\n // R350: hover state drives the icon transform below.\n onMouseEnter={() => setHoveredReset(true)}\n onMouseLeave={() => setHoveredReset(false)}\n onFocus={() => setHoveredReset(true)}\n onBlur={() => setHoveredReset(false)}\n // R196: press-state deepens before R184 reset-spin fires on\n // release — mouse-down dim then 450ms spin = full handshake.\n /* Round 400 / Loop · milestone: chrome reset + fullscreen\n buttons gain hover:-translate-y-px lift — closes the\n hover-lift gesture vocabulary across every standalone\n interactive HTML element in TopoGraph. Segmented\n controls (zoom -/+, nodeSize S/M/L, Layout Ring/Grid)\n intentionally stay planted: lifting one segment of a\n unified strip would tear the visual unity of the\n segmented control. Only the standalone chrome buttons\n (reset, fullscreen) get the lift.\n Gesture vocabulary post-R400 (now complete across HTML):\n chip-row chips (3×) -1 px R398, R399\n filter pin pills (4×) -1 px R397\n recent-signal row -1 px R143\n legend row -1 px R144\n reset button -1 px R400 (this round)\n fullscreen button -1 px R400 (this round)\n Every standalone interactive HTML surface in TopoGraph\n now lifts on hover. data-topo-chrome-reset-hover-lift\n attr surfaces the lift for tests. */\n // R493 — reset button joins the chrome-strip active:scale-95\n // press-feedback family. The button already has transition-\n // transform + transform-gpu (R350 reset spin + R400 hover lift),\n // so just appending active:scale-95 plugs straight in. Compound\n // active state during press = hover-lift (-1px) + scale-95\n // composes as translateY(-1px) scale(0.95) — lift-and-compress\n // for tactile click feel.\n className=\"p-1.5 rounded-md border hover:bg-white/5 active:bg-white/10 hover:-translate-y-px active:scale-95 transition-colors transition-transform duration-200 ease-out transform-gpu focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60\"\n data-topo-chrome-reset-hover-lift=\"true\"\n style={{ background: pal.legendBox.fill, borderColor: pal.containerBorder, color: pal.legendText }}\n aria-label=\"Reset view\"\n title=\"Reset zoom + pan (0, or double-click the canvas)\"\n >\n {/* R184: the refresh-arrow icon does one counter-clockwise\n rotation on click. CSS animation runs once; React removes\n the className after 460ms (just past the 450ms duration)\n so a subsequent click can replay. */}\n {/* Round 288 / Loop: reset icon strokeWidth 2 → 2.5 unifies\n the chrome icon weight family. Pre-R288 zoom-in / zoom-\n out icons rendered at strokeWidth 2.5 while reset +\n fullscreen icons sat thinner at strokeWidth 2 — five\n chrome icons in a single horizontal strip with two\n weights is exactly the inconsistency R268 closed for\n border colors. Same unification idiom now applied to\n icon strokes: zoom (2.5) + reset (2.5) + fullscreen\n (2.5) all share one weight. View-box (24×24) and\n display size (13×13) unchanged, so geometry stays\n pixel-stable — only the stroke deepens. */}\n {/* Round 453 / Loop: chrome reset icon strokeWidth hover\n lift — 2.5 → 2.8 on hoveredReset && !resetSpinning.\n Sibling to R443 runtime badge inner-icon sw lift\n (2.4→2.8) — both chrome icons now thicken on hover\n for tactile feedback. Pre-R453 reset hover was a\n rotate-only cue (R350); R453 adds a stroke-weight\n axis so the affordance reads with both motion (R350\n rotate -8°) AND geometry (R453 sw +0.3). Gated on\n !resetSpinning so the R184 spin keyframe owns paint\n during its 450ms run. 200ms stroke-width transition\n appended to the style list matches R350 transform\n cadence. data-topo-chrome-reset-icon-stroke-width\n attr exposes the resolved value for tests. */}\n <svg\n width=\"13\" height=\"13\" viewBox=\"0 0 24 24\"\n fill=\"none\" stroke=\"currentColor\"\n strokeWidth={hoveredReset && !resetSpinning ? '2.8' : '2.5'}\n strokeLinecap=\"round\" strokeLinejoin=\"round\"\n aria-hidden\n className={resetSpinning ? 'anet-reset-spin' : undefined}\n data-topo-chrome-reset-icon\n data-topo-chrome-reset-icon-stroke-width={hoveredReset && !resetSpinning ? '2.8' : '2.5'}\n // R350: hover-rotate preview of the R184 click-spin.\n // Gated on !resetSpinning so the anet-reset-spin keyframe\n // owns transform during its 450ms run. transformOrigin\n // 'center' so rotation pivots around the icon's centre\n // (default would be top-left and the icon would arc).\n style={{\n transform: hoveredReset && !resetSpinning ? 'rotate(-8deg)' : 'rotate(0deg)',\n transformOrigin: 'center',\n transition: 'transform 200ms ease-out, stroke-width 200ms ease-out',\n }}\n data-topo-chrome-reset-icon-hover={hoveredReset && !resetSpinning ? 'true' : 'false'}\n >\n <path d=\"M3 12a9 9 0 1 0 9-9 9 9 0 0 0-6.4 2.6L3 8\" />\n <path d=\"M3 3v5h5\" />\n </svg>\n </button>\n {/* Round 178 / Loop: fullscreen chrome button picks up the\n active-state visual indicator R163 introduced for the\n Ring/Grid layout toggle. Pre-R178 the button changed\n icon when isFullscreen flipped but its background +\n foreground stayed unchanged — operators in fullscreen\n didn't get a strong 'you're in fullscreen' cue. Adding\n the bg-cyan-500/15 + text-cyan-300 active variant\n mirrors R163's pattern; hover variants tier 1\n brighter (cyan-500/20) when active so the chip\n continues to respond to mouse. Inline style now omits\n background + color when active so the Tailwind cyan\n classes win specificity. */}\n <button\n onClick={() => { popChrome('fullscreen'); toggleFullscreen(); }}\n data-topo-chrome-fullscreen\n data-topo-chrome-fullscreen-active={isFullscreen ? 'true' : 'false'}\n data-topo-chrome-fullscreen-popping={chromePopping === 'fullscreen' ? 'true' : 'false'}\n // R196: fullscreen also picks up press-state — active variant\n // deepens cyan-500/20 → cyan-500/25 on press; non-active\n // deepens white/5 → white/10.\n // R249: chrome-pop on click — same one-vocabulary click signal\n // as layout toggle and zoom buttons.\n /* Round 270 / Loop: fullscreen INACTIVE picks up the cyan\n hover-preview pattern from the Layout toggle. The\n fullscreen button is a TOGGLE (enter/exit fullscreen) so\n its inactive state benefits from the same \"hover previews\n active state\" idiom R163 designed. Sibling treatment to\n the nodeSize buttons at line ~6711. */\n // R353: `group` lets the inner svg respond via group-hover —\n // sibling to R352 zoom buttons. Closes the chrome-strip per-\n // icon hover-affordance arc (zoom-out / zoom-in / reset /\n // fullscreen now all carry an icon-level hover gesture in\n // addition to the bg hover).\n // R400: hover translateY(-1px) lift — see reset button above for family doc.\n // R493 — fullscreen joins active:scale-95 press family (same as\n // reset above: lift-and-compress compound transform on press).\n className={`group p-1.5 rounded-md border hover:-translate-y-px active:scale-95 transition-colors transition-transform duration-200 ease-out transform-gpu focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 ${\n isFullscreen\n ? 'bg-cyan-500/15 text-cyan-300 font-medium hover:bg-cyan-500/20 active:bg-cyan-500/25'\n : 'hover:bg-cyan-500/5 active:bg-cyan-500/15'\n }${chromePopping === 'fullscreen' ? ' anet-chrome-pop' : ''}`}\n data-topo-chrome-fullscreen-hover-lift=\"true\"\n style={{\n borderColor: pal.containerBorder,\n ...(isFullscreen\n ? {}\n : { background: pal.legendBox.fill, color: pal.legendText }),\n }}\n aria-label={isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}\n title={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}\n >\n {/* R288 / Loop: fullscreen enter + exit icons strokeWidth\n 2 → 2.5 — same chrome-icon weight unification described\n at the reset icon above. data-topo-chrome-fullscreen-\n icon attribute exposes BOTH variants (entered / exited)\n for the round's stroke-width regression probe. */}\n {/* Round 353 / Loop: fullscreen icon (both enter + exit\n variants) picks up the R352 family group-hover:scale-110.\n Pre-R353 hovering the button only changed the bg; the\n icon stayed still. R353 lifts the icon 10 % on hover —\n same gesture vocabulary as the zoom buttons. transform-\n gpu hint promotes the svg to its own compositor layer\n for crisper edges during the scale tween. Closes the\n chrome-strip per-icon hover-affordance arc. */}\n {/* R455 — fullscreen ENTER + EXIT icons pick up the same\n group-hover:[stroke-width:2.8] family lift as the\n zoom +/− icons (R454) and chrome reset icon (R453).\n Chrome icon hover sw lift family now 6 anchors —\n R208/R443 runtime badge + R453/R454-zoom-out/zoom-in\n + R455 fullscreen (this round). transition-[transform,\n stroke-width] expands existing transition-transform\n so the sw lift eases under R352 scale-110 cadence. */}\n {isFullscreen ? (\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden className=\"transition-[transform,stroke-width] duration-200 ease-out group-hover:scale-110 group-hover:[stroke-width:2.8] transform-gpu\" data-topo-chrome-fullscreen-icon=\"exit\">\n <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\" />\n </svg>\n ) : (\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden className=\"transition-[transform,stroke-width] duration-200 ease-out group-hover:scale-110 group-hover:[stroke-width:2.8] transform-gpu\" data-topo-chrome-fullscreen-icon=\"enter\">\n <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\" />\n </svg>\n )}\n </button>\n </div>\n\n {/* Issue #100/#106: draggable, resizable singleton chat popover.\n position:fixed so it floats above the page (overflow-hidden here\n doesn't clip fixed children). Rendered *inside* the container so\n that when the graph goes fullscreen (#81) the popover joins the\n fullscreen subtree and stays visible — a sibling render would be\n outside the fullscreened element and disappear. */}\n {chatAlias && (\n <ChatPopover alias={chatAlias} onClose={() => setChatAlias(null)} />\n )}\n </div>\n </section>\n );\n}\n","'use client';\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { AliasAvatar } from './AliasAvatar';\nimport { TaskChatPanel } from './TaskChatPanel';\nimport { useSessions } from '../lib/hooks';\nimport { relativeAgo } from '../lib/time';\n\ninterface ChatPopoverProps {\n /** Node alias to chat with. Changing it switches the conversation. */\n alias: string;\n onClose: () => void;\n}\n\nconst MAX_W = 380;\nconst MAX_H = 520;\nconst MIN_W = 300;\nconst MIN_H = 320;\nconst MARGIN = 16;\nconst MOBILE_BP = 640;\n\n/**\n * Issue #100: a floating, draggable chat window opened by clicking a node\n * avatar in the topology graph. Singleton — the parent (TopoGraph) keeps a\n * single `chatAlias`, so clicking another node just swaps `alias` here and\n * the conversation switches in place.\n *\n * Issue #106: a bottom-right handle resizes the window (drag to change w/h,\n * clamped to MIN_W/MIN_H .. the viewport). Coexists with the header move-drag\n * and the close button — each interaction lives on a distinct element and\n * stops its own pointerdown from reaching the others.\n *\n * One consistent design across viewports: a floating draggable card sized to\n * fit (`min(380, vw-32) × min(520, vh-96)`). On a phone it ends up near\n * full-width, so it's docked low on open — the graph above stays visible, and\n * to chat with a different node you drag it down / tap an exposed avatar.\n *\n * The chat body reuses TaskChatPanel's `inline` mode — send / SSE-receive /\n * history are already solved there; this component only adds the floating\n * shell + drag/resize behaviour.\n */\n/** Round 37 / Loop: surface node metadata (cwd + last-seen) inside the\n * ChatPopover header. The SVG <title> tooltip (Rounds 33-34) only shows\n * on hover-over-node, which is lost once the popover is open and\n * potentially dragged away. Lifting cwd and last-seen into the popover\n * header keeps the context where the user actually needs it.\n *\n * Round 38: relativeAgo factored to app/lib/time.ts so this file shares\n * the same TZ-safe parser as TopoGraph (was a duplicated mirror until\n * this round). */\n\nexport function ChatPopover({ alias, onClose }: ChatPopoverProps) {\n // Position + size are resolved on mount (SSR-safe defaults here).\n const [pos, setPos] = useState({ x: 0, y: 0 });\n const [size, setSize] = useState({ w: MAX_W, h: MAX_H });\n const dragRef = useRef<{ active: boolean; startX: number; startY: number; baseX: number; baseY: number }>({\n active: false, startX: 0, startY: 0, baseX: 0, baseY: 0,\n });\n const resizeRef = useRef<{ active: boolean; startX: number; startY: number; baseW: number; baseH: number }>({\n active: false, startX: 0, startY: 0, baseW: 0, baseH: 0,\n });\n\n // Round 37 / Loop: pull the target session out of the SWR cache that\n // TopoGraph already populates — same URL, so no extra network fetch.\n const { sessions } = useSessions();\n const session = useMemo(() => sessions.find(s => s.alias === alias), [sessions, alias]);\n const isOnline = !!session && session.status !== 'offline';\n const lastSeenLine = !isOnline ? relativeAgo(session?.last_seen_at) : null;\n\n const clamp = useCallback((x: number, y: number, w: number, h: number) => {\n const maxX = Math.max(MARGIN, window.innerWidth - w - MARGIN);\n const maxY = Math.max(MARGIN, window.innerHeight - h - MARGIN);\n return {\n x: Math.min(Math.max(MARGIN, x), maxX),\n y: Math.min(Math.max(MARGIN, y), maxY),\n };\n }, []);\n\n useEffect(() => {\n const place = () => {\n const vw = window.innerWidth;\n const vh = window.innerHeight;\n const w = Math.min(MAX_W, vw - MARGIN * 2);\n const h = Math.min(MAX_H, vh - MARGIN * 6);\n setSize({ w, h });\n const mobile = vw < MOBILE_BP;\n // Desktop: top-right of the graph. Mobile: docked low so the graph\n // above stays visible — switch nodes by tapping an exposed avatar.\n const x = mobile ? MARGIN : vw - w - 24;\n const y = mobile ? vh - h - MARGIN : 96;\n setPos(clamp(x, y, w, h));\n };\n place();\n // Keep the popover on-screen if the window is resized.\n window.addEventListener('resize', place);\n return () => window.removeEventListener('resize', place);\n }, [clamp]);\n\n // Esc closes — matches the rest of the dashboard's overlay convention.\n useEffect(() => {\n const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };\n window.addEventListener('keydown', onKey);\n return () => window.removeEventListener('keydown', onKey);\n }, [onClose]);\n\n const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {\n if (e.button !== 0) return;\n (e.currentTarget as Element).setPointerCapture?.(e.pointerId);\n dragRef.current = { active: true, startX: e.clientX, startY: e.clientY, baseX: pos.x, baseY: pos.y };\n };\n const onPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {\n const d = dragRef.current;\n if (!d.active) return;\n setPos(clamp(d.baseX + (e.clientX - d.startX), d.baseY + (e.clientY - d.startY), size.w, size.h));\n };\n const onPointerUp = (e: React.PointerEvent<HTMLDivElement>) => {\n if (!dragRef.current.active) return;\n dragRef.current.active = false;\n try { (e.currentTarget as Element).releasePointerCapture?.(e.pointerId); } catch {}\n };\n\n // Resize from the bottom-right handle. stopPropagation keeps the header\n // move-drag out of it; the popover is top-left anchored so growing it can\n // only push the bottom/right edges, which we clamp to the viewport.\n const onResizeDown = (e: React.PointerEvent<HTMLDivElement>) => {\n if (e.button !== 0) return;\n e.stopPropagation();\n (e.currentTarget as Element).setPointerCapture?.(e.pointerId);\n resizeRef.current = { active: true, startX: e.clientX, startY: e.clientY, baseW: size.w, baseH: size.h };\n };\n const onResizeMove = (e: React.PointerEvent<HTMLDivElement>) => {\n const r = resizeRef.current;\n if (!r.active) return;\n e.stopPropagation();\n const maxW = Math.max(MIN_W, window.innerWidth - pos.x - MARGIN);\n const maxH = Math.max(MIN_H, window.innerHeight - pos.y - MARGIN);\n setSize({\n w: Math.min(maxW, Math.max(MIN_W, r.baseW + (e.clientX - r.startX))),\n h: Math.min(maxH, Math.max(MIN_H, r.baseH + (e.clientY - r.startY))),\n });\n };\n const onResizeUp = (e: React.PointerEvent<HTMLDivElement>) => {\n if (!resizeRef.current.active) return;\n resizeRef.current.active = false;\n e.stopPropagation();\n try { (e.currentTarget as Element).releasePointerCapture?.(e.pointerId); } catch {}\n };\n\n return (\n <div\n className=\"fixed z-50 flex flex-col overflow-hidden rounded-xl border border-[var(--border)] bg-[var(--bg)] shadow-2xl shadow-black/60 anet-fade-in\"\n style={{ left: pos.x, top: pos.y, width: size.w, height: size.h }}\n role=\"dialog\"\n aria-label={`Chat with ${alias}`}\n >\n {/* Drag handle — the whole header bar moves the window. */}\n <div\n onPointerDown={onPointerDown}\n onPointerMove={onPointerMove}\n onPointerUp={onPointerUp}\n onPointerCancel={onPointerUp}\n className=\"flex items-center justify-between px-3 py-2.5 border-b border-[var(--border)] bg-[var(--bg-secondary)] rounded-t-xl cursor-grab active:cursor-grabbing select-none touch-none\"\n >\n <div className=\"flex items-center gap-2.5 min-w-0\">\n <AliasAvatar alias={alias} size={28} />\n <div className=\"min-w-0\">\n <div className=\"text-sm font-semibold text-[var(--fg)] truncate\">{alias}</div>\n {/* Round 37: cwd / last-seen lines surface the same metadata\n the SVG <title> tooltip carries (rounds 33-34), but inside\n the popover where it stays accessible after dragging the\n window away from the node. Fall back to the drag-hint\n when no metadata is reported. */}\n {session?.project_dir ? (\n <div className=\"text-[10px] text-[var(--fg-muted)] truncate font-mono\" title={session.project_dir} data-popover-cwd>\n cwd: {session.project_dir}\n </div>\n ) : null}\n {lastSeenLine ? (\n <div className=\"text-[10px] text-[var(--fg-muted)] truncate\" data-popover-lastseen>\n last seen: {lastSeenLine}\n </div>\n ) : null}\n {!session?.project_dir && !lastSeenLine ? (\n <div className=\"text-[10px] text-[var(--fg-muted)]\">Drag to move · Esc to close</div>\n ) : null}\n </div>\n </div>\n <button\n onClick={onClose}\n // Without this the header's drag handler captures the pointer and\n // Chromium retargets the click to the header — the button never fires.\n onPointerDown={(e) => e.stopPropagation()}\n aria-label=\"Close chat\"\n className=\"text-[var(--fg-muted)] hover:text-[var(--fg)] p-1.5 rounded-lg hover:bg-[var(--bg-elevated)] shrink-0\"\n >\n <svg className=\"w-4 h-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" strokeWidth={2}>\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M6 18L18 6M6 6l12 12\" />\n </svg>\n </button>\n </div>\n\n {/* Chat body — TaskChatPanel inline mode owns send / SSE / history. */}\n <div className=\"flex-1 min-h-0\">\n <TaskChatPanel alias={alias} onClose={onClose} inline />\n </div>\n\n {/* Issue #106: bottom-right resize handle. */}\n <div\n onPointerDown={onResizeDown}\n onPointerMove={onResizeMove}\n onPointerUp={onResizeUp}\n onPointerCancel={onResizeUp}\n aria-label=\"Resize chat\"\n className=\"absolute bottom-0 right-0 w-5 h-5 cursor-nwse-resize touch-none z-10\"\n style={{ touchAction: 'none' }}\n >\n <svg className=\"absolute bottom-0.5 right-0.5 w-3 h-3 text-[var(--fg-dim)]\" viewBox=\"0 0 12 12\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" aria-hidden>\n <path d=\"M11 5L5 11M11 9L9 11\" />\n </svg>\n </div>\n </div>\n );\n}\n","/** Issue #96: node visual identity — model vendor + runtime.\n *\n * Single source for two orthogonal node dimensions:\n * - vendor : which model house powers the node (avatar body)\n * - runtime: which execution shell it runs in (corner badge)\n *\n * Both are driven by the server `model` / `runtime` fields added in\n * commhub-server 0.8.1-preview.0 (#96 Phase 1, commit 08482ef). Old nodes\n * report `model: null` / `runtime: null` and fall back gracefully — never a\n * broken image or blank avatar.\n *\n * Adding a vendor = one entry in VENDOR_RULES. Dropping a real logo file into\n * public/vendors/ + setting `logo` lights it up with zero render-code change;\n * until then a vendor-tinted monogram stands in (not a fake official logo).\n */\n\nexport interface VendorIdentity {\n id: string;\n label: string;\n /** Monogram fallback colours, used when `logo` is null. */\n mono: { bg: string; ring: string; text: string };\n /** 1-char monogram shown when there's no logo asset. */\n initial: string;\n /** Packaged logo asset path, or null → render the monogram instead. */\n logo: string | null;\n}\n\nconst UNKNOWN_VENDOR: VendorIdentity = {\n id: 'unknown',\n label: 'Unknown vendor',\n mono: { bg: 'hsl(220 10% 26%)', ring: 'hsl(220 10% 46%)', text: 'hsl(220 14% 82%)' },\n initial: '?',\n logo: null,\n};\n\n// Ordered prefix rules — first match wins. `test` runs against a lowercased\n// model id. Keep the most specific prefixes first.\nconst VENDOR_RULES: Array<{ test: (m: string) => boolean; vendor: VendorIdentity }> = [\n {\n test: (m) => m.startsWith('intern'),\n vendor: {\n id: 'intern',\n label: '书生 · 上海 AI 实验室',\n mono: { bg: 'hsl(28 38% 24%)', ring: 'hsl(32 45% 52%)', text: 'hsl(34 60% 82%)' },\n initial: '书',\n // #79 shipped this asset — reuse it as the 书生 vendor logo.\n logo: '/intern_avatar.png',\n },\n },\n {\n test: (m) => m.startsWith('minimax'),\n vendor: {\n id: 'minimax',\n label: 'MiniMax',\n mono: { bg: 'hsl(18 50% 26%)', ring: 'hsl(18 65% 52%)', text: 'hsl(20 80% 82%)' },\n initial: 'M',\n // P0 (Vincent 5222) custom-designed vendor badge — NOT a copy of\n // the MiniMax trademark; geometric min/max zigzag in their warm-\n // red palette. Replaces plain-letter \"M\" fallback.\n logo: '/vendors/minimax.svg',\n },\n },\n {\n test: (m) => m.startsWith('claude'),\n vendor: {\n id: 'anthropic',\n label: 'Anthropic',\n mono: { bg: 'hsl(16 32% 26%)', ring: 'hsl(16 48% 54%)', text: 'hsl(18 60% 84%)' },\n initial: 'A',\n // P0 (Vincent 5222) custom-designed vendor badge — NOT a copy of\n // the Anthropic trademark; 4-pointed sparkle in their warm-orange\n // palette evokes AI/Claude without imitating the official mark.\n // Real Anthropic logo still pending Vincent-direct asset OK.\n logo: '/vendors/claude.svg',\n },\n },\n {\n test: (m) => m.startsWith('gpt') || m.startsWith('codex') || m.startsWith('o1') || m.startsWith('o3') || m.startsWith('o4'),\n vendor: {\n id: 'openai',\n label: 'OpenAI',\n mono: { bg: 'hsl(165 26% 22%)', ring: 'hsl(165 40% 44%)', text: 'hsl(165 45% 80%)' },\n initial: 'O',\n // P0 (Vincent 5222) custom-designed vendor badge — NOT a copy of\n // the OpenAI trademark; hexagonal frame + center dot in their\n // teal palette evokes geometric AI lattice without imitating the\n // knot. Real OpenAI logo still pending Vincent-direct asset OK.\n logo: '/vendors/openai.svg',\n },\n },\n];\n\n/** Resolve a model id to its vendor identity. `null` / unmatched → UNKNOWN. */\nexport function vendorForModel(model: string | null | undefined): VendorIdentity {\n if (!model) return UNKNOWN_VENDOR;\n const m = model.toLowerCase();\n for (const rule of VENDOR_RULES) {\n if (rule.test(m)) return rule.vendor;\n }\n return UNKNOWN_VENDOR;\n}\n\nexport type Runtime = 'claude-code-cli' | 'codex-sdk' | 'claude-agent-sdk' | 'http-api';\n\nexport interface RuntimeIdentity {\n label: string;\n /** SVG path(s) drawn inside a 0..24 viewBox for the corner badge. */\n iconPath: string;\n /** Badge accent colour — kept clear of working/idle/offline status hues. */\n color: string;\n}\n\nconst RUNTIME_MAP: Record<Runtime, RuntimeIdentity> = {\n // terminal / CLI\n 'claude-code-cli': {\n label: 'Claude Code CLI',\n iconPath: 'M4 5h16v14H4z M7 9l3 3-3 3 M13 15h4',\n color: '#a78bfa',\n },\n // code-block / SDK\n 'codex-sdk': {\n label: 'Codex SDK',\n iconPath: 'M9 7l-5 5 5 5 M15 7l5 5-5 5',\n color: '#38bdf8',\n },\n // SDK, distinct hue from codex\n 'claude-agent-sdk': {\n label: 'Claude Agent SDK',\n iconPath: 'M4 7h16v10H4z M8 11h8 M8 14h5',\n color: '#34d399',\n },\n // cloud / API — non-resident process\n 'http-api': {\n label: 'HTTP API',\n iconPath: 'M7 18a4 4 0 010-8 5 5 0 019.6-1.3A3.5 3.5 0 0118 18z',\n color: '#fbbf24',\n },\n};\n\n/** Resolve a server runtime string to badge identity. Unknown → null. */\nexport function runtimeIdentity(runtime: string | null | undefined): RuntimeIdentity | null {\n if (!runtime) return null;\n return RUNTIME_MAP[runtime as Runtime] ?? null;\n}\n\n/** Compact one-line identity for hover / detail surfaces:\n * \"Anthropic · claude-opus-4 · Claude Code CLI\". Pieces with no data drop. */\nexport function identityLine(model: string | null | undefined, runtime: string | null | undefined): string {\n const v = vendorForModel(model);\n const r = runtimeIdentity(runtime);\n const parts: string[] = [];\n if (v.id !== 'unknown') parts.push(v.label);\n if (model) parts.push(model);\n if (r) parts.push(r.label);\n return parts.join(' · ');\n}\n","'use client';\n\nimport Link from 'next/link';\nimport { Session } from './types';\nimport { timeAgo } from './utils';\nimport { AliasAvatar } from './AliasAvatar';\n\ninterface AgentCardProps {\n session: Session;\n hasSse: boolean;\n sseCount: number;\n onChat?: (alias: string) => void;\n}\n\nconst STATUS_CONFIG: Record<string, { bg: string; text: string; dot: string; glow: string }> = {\n working: { bg: 'bg-green-900/30 border-green-800/30', text: 'text-green-300', dot: 'bg-green-500', glow: 'shadow-green-500/10' },\n idle: { bg: 'bg-cyan-900/30 border-cyan-800/30', text: 'text-cyan-300', dot: 'bg-cyan-400', glow: 'shadow-cyan-500/5' },\n blocked: { bg: 'bg-yellow-900/30 border-yellow-800/30', text: 'text-yellow-300', dot: 'bg-yellow-500', glow: '' },\n error: { bg: 'bg-red-900/30 border-red-800/30', text: 'text-red-300', dot: 'bg-red-500', glow: '' },\n};\n\nconst DEFAULT_STATUS = { bg: 'bg-gray-800/50 border-gray-700/30', text: 'text-gray-500', dot: 'bg-gray-500', glow: '' };\n\nexport function AgentCard({ session: s, hasSse, sseCount, onChat }: AgentCardProps) {\n const cfg = hasSse ? (STATUS_CONFIG[s.status] || DEFAULT_STATUS) : DEFAULT_STATUS;\n\n return (\n <Link\n href={`/node?alias=${encodeURIComponent(s.alias)}`}\n prefetch={false}\n className={`anet-agent-card group relative block rounded-xl border p-4 transition-all duration-300 cursor-pointer hover:-translate-y-0.5 ${\n hasSse\n ? `bg-[#111128] border-[#2a2a4a] hover:border-cyan-500/30 hover:shadow-lg ${cfg.glow}`\n : 'bg-[#0d0d1a] border-[#1a1a2a] opacity-40'\n }`}\n >\n {/* Header: avatar + name + status. Avatar carries the alias→hue map\n shared with Messages/Nodes/Tasks/Overview; the live status dot\n stays as a small pulse-capable indicator. */}\n <div className=\"flex items-center justify-between mb-3\">\n <div className=\"flex items-center gap-2 min-w-0\">\n <AliasAvatar alias={s.alias} size={22} />\n <span className=\"font-semibold text-white truncate text-sm\" title={s.alias}>{s.alias}</span>\n <span className={`inline-block w-1.5 h-1.5 rounded-full shrink-0 ${cfg.dot} ${hasSse && s.status === 'working' ? 'animate-pulse' : ''}`} />\n </div>\n <span className={`text-[11px] px-2 py-0.5 rounded-md border shrink-0 ${cfg.bg} ${cfg.text}`}>\n {hasSse ? s.status : 'offline'}\n </span>\n </div>\n\n {/* Agent type badge */}\n <div className=\"flex items-center gap-2 mb-3\">\n <span className=\"text-xs text-gray-600 bg-[#0a0a15] px-2 py-0.5 rounded border border-[#1a1a2a]\">\n {s.agent || 'unknown'}\n </span>\n {hasSse && (\n <span className=\"text-[10px] text-green-500\">SSE:{sseCount}</span>\n )}\n </div>\n\n {/* Task */}\n {s.task ? (\n <div className=\"text-xs text-gray-400 bg-[#0a0a15] rounded-lg px-3 py-2 border border-[#1a1a2a] line-clamp-2\" title={s.task}>\n {s.task}\n </div>\n ) : (\n <div className=\"text-xs text-gray-700 italic\">No active task</div>\n )}\n\n {/* Progress bar */}\n {s.progress > 0 && (\n <div className=\"mt-3\">\n <div className=\"flex justify-between text-[10px] mb-1\">\n <span className=\"text-gray-600\">Progress</span>\n <span className={cfg.text}>{s.progress}%</span>\n </div>\n <div className=\"w-full bg-gray-800 rounded-full h-1.5 overflow-hidden\">\n <div\n className={`h-1.5 rounded-full transition-all duration-700 ${s.status === 'working' ? 'bg-green-500' : 'bg-cyan-500'}`}\n style={{ width: `${Math.min(s.progress, 100)}%` }}\n />\n </div>\n </div>\n )}\n\n {/* Footer: time + chat + hover chevron affordance (round 44).\n The card is a <Link> so it's clickable everywhere, but with no\n visible cue users may not realise. Chevron appears on hover and\n slides right ~2px for a \"drill in\" hint. */}\n <div className=\"mt-3 flex justify-between items-center text-[10px] text-gray-600\">\n <span className=\"truncate\" title={s.server || ''}>{s.server || '--'}</span>\n <div className=\"flex items-center gap-2\">\n {onChat && hasSse && (\n <button\n onClick={e => { e.preventDefault(); e.stopPropagation(); onChat(s.alias); }}\n className=\"text-cyan-400 hover:text-cyan-300 px-1.5 py-0.5 rounded border border-cyan-500/20 hover:bg-cyan-500/10 transition-colors\"\n >\n Chat\n </button>\n )}\n <span>{timeAgo(s.updated_at)}</span>\n <svg\n aria-hidden\n className=\"w-3 h-3 text-gray-700 opacity-0 -translate-x-1 group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-200\"\n fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\"\n >\n <path d=\"M9 18l6-6-6-6\" />\n </svg>\n </div>\n </div>\n </Link>\n );\n}\n","'use client';\n\nimport { InboxMessage } from './types';\nimport { AliasAvatar } from './AliasAvatar';\nimport { timeAgo, previewContent } from './utils';\n\ninterface InboxPanelProps {\n messages: InboxMessage[];\n}\n\nexport function InboxPanel({ messages }: InboxPanelProps) {\n if (messages.length === 0) return null;\n\n return (\n <div className=\"mt-8\">\n <h2 className=\"text-lg font-semibold text-white mb-3 flex items-center gap-2\">\n <span className=\"text-gray-500\">Inbox</span>\n <span className=\"text-xs bg-blue-900/30 text-blue-400 px-2 py-0.5 rounded-full border border-blue-800/30\">\n {messages.length}\n </span>\n </h2>\n <div className=\"space-y-2 max-h-96 overflow-y-auto pr-1 scrollbar-thin\">\n {messages.map(m => (\n <div key={m.id} className=\"bg-[#111128] border border-[#2a2a4a] rounded-lg px-4 py-3 text-sm transition-colors hover:border-[#3a3a5a]\">\n <div className=\"flex items-center gap-2 text-xs mb-1.5\">\n {m.from_session && <AliasAvatar alias={m.from_session} size={16} />}\n <span className=\"text-gray-200 font-medium truncate\">{m.from_session}</span>\n <span className=\"ml-auto text-gray-600 shrink-0\" title={m.created_at}>{timeAgo(m.created_at)}</span>\n </div>\n <div className=\"text-gray-300 leading-relaxed line-clamp-3\" title={m.content || ''}>{previewContent(m.content)}</div>\n </div>\n ))}\n </div>\n </div>\n );\n}\n","'use client';\n\n/**\n * Loading skeleton — mirrors the actual Overview layout structure so the\n * page doesn't appear to \"shift\" once data arrives. Uses the same\n * `anet-skeleton-pulse` rhythm as the brand pulse (1.6s opacity drift,\n * no scale, no blur, no glow) so the loading animation feels native to\n * the rest of the dashboard rather than a generic spinner.\n *\n * Bars are theme-aware: anet-skeleton-bar (existing CSS shim resolves this to\n * var(--bg-elevated) on light/mint), so light theme shows soft grey blocks\n * on white cards and dark theme shows lighter blocks on navy cards.\n */\nexport function LoadingSkeleton() {\n return (\n <div className=\"min-h-screen bg-[#0a0a1a] text-gray-100 p-4 sm:p-6 font-mono\">\n {/* KPI top strip — 4 cards matching StatsBar */}\n <div className=\"grid grid-cols-2 sm:grid-cols-4 gap-3 mb-8 anet-skeleton-pulse\">\n {[1, 2, 3, 4].map(i => (\n <div key={i} className=\"rounded-xl border border-[#2a2a4a] bg-[#111128] px-4 py-3\">\n <Bar w=\"2.5rem\" h=\"1.75rem\" />\n <Bar w=\"3.5rem\" h=\"0.75rem\" className=\"mt-2\" />\n <Bar w=\"5rem\" h=\"0.625rem\" className=\"mt-1\" />\n </div>\n ))}\n </div>\n\n {/* Dispatch + UserBar row */}\n <div className=\"flex items-center gap-3 mb-3 anet-skeleton-pulse\">\n <Bar w=\"6rem\" h=\"2.5rem\" rounded=\"0.75rem\" />\n <div className=\"flex-1 rounded-lg border border-[#2a2a4a] bg-[#111128] px-4 py-2.5 flex items-center gap-3\">\n <div className=\"w-8 h-8 rounded-full anet-skeleton-bar\" />\n <div className=\"flex-1\">\n <Bar w=\"6rem\" h=\"0.875rem\" />\n <Bar w=\"10rem\" h=\"0.625rem\" className=\"mt-1\" />\n </div>\n </div>\n </div>\n\n {/* Config bar */}\n <div className=\"mb-6 rounded-lg border border-[#2a2a4a] bg-[#111128] px-4 py-3 anet-skeleton-pulse\">\n <Bar w=\"14rem\" h=\"0.875rem\" />\n </div>\n\n {/* Stat strip 3 cards */}\n <div className=\"grid grid-cols-3 gap-2 sm:gap-3 mb-3 anet-skeleton-pulse\">\n {[1, 2, 3].map(i => (\n <div key={i} className=\"rounded-xl border border-[#2a2a4a] bg-[#111128] px-3 py-3\">\n <Bar w=\"2rem\" h=\"1.25rem\" />\n <Bar w=\"2.5rem\" h=\"0.75rem\" className=\"mt-1\" />\n <Bar w=\"3.5rem\" h=\"0.625rem\" className=\"mt-px\" />\n </div>\n ))}\n </div>\n\n {/* Nav rail 3 cards */}\n <div className=\"grid grid-cols-3 gap-2 sm:gap-3 mb-6 anet-skeleton-pulse\">\n {[1, 2, 3].map(i => (\n <div key={i} className=\"rounded-xl border border-[#2a2a4a] bg-[#111128] px-3 py-2.5 flex items-center justify-center gap-2\">\n <div className=\"w-4 h-4 rounded anet-skeleton-bar\" />\n <Bar w=\"4rem\" h=\"0.75rem\" />\n </div>\n ))}\n </div>\n\n {/* Broadcast bar */}\n <div className=\"mb-6 flex gap-2 anet-skeleton-pulse\">\n <div className=\"flex-1 h-10 rounded-lg border border-[#2a2a4a] bg-[#111128]\" />\n <div className=\"w-28 h-10 rounded-lg anet-skeleton-bar\" />\n </div>\n\n {/* Agent card grid */}\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-3 sm:gap-4 anet-skeleton-pulse\">\n {[1, 2, 3, 4].map(i => (\n <div key={i} className=\"rounded-xl border border-[#2a2a4a] bg-[#111128] p-4\">\n <div className=\"flex items-center gap-2 mb-3\">\n <div className=\"w-2 h-2 rounded-full anet-skeleton-bar\" />\n <Bar w=\"6rem\" h=\"0.875rem\" />\n </div>\n <div className=\"space-y-2\">\n <Bar w=\"100%\" h=\"0.625rem\" />\n <Bar w=\"75%\" h=\"0.625rem\" />\n <Bar w=\"55%\" h=\"0.625rem\" />\n </div>\n </div>\n ))}\n </div>\n </div>\n );\n}\n\n/** Single shimmer bar — uses `anet-skeleton-bar` so theme-specific bar\n * color (dark navy on dark themes, mid-grey on light) overrides the\n * default. */\nfunction Bar({ w, h, rounded, className }: { w: string; h: string; rounded?: string; className?: string }) {\n return (\n <div\n className={`anet-skeleton-bar ${className || ''}`}\n style={{ width: w, height: h, borderRadius: rounded || '0.375rem' }}\n />\n );\n}\n\n// EmptyState export removed in 0.4.5 — replaced by EmptyState.tsx with\n// per-variant glyphs and a NodesEmptyState wrapper for the Overview\n// hint-aware behavior. See app/components/EmptyState.tsx.\n","'use client';\n\nimport { useEffect, useState } from 'react';\nimport { useNetworkId } from '../lib/network-context';\n\ninterface User {\n user_id: string;\n username: string;\n display_name: string;\n role: string;\n}\n\ninterface Network {\n network_id: string;\n network_name: string;\n description: string;\n}\n\ninterface AuthState {\n user: User | null;\n networks: Network[];\n currentNetwork: string;\n token: string;\n}\n\nexport function UserBar() {\n const { setNetworkId } = useNetworkId();\n const [auth, setAuth] = useState<AuthState>({ user: null, networks: [], currentNetwork: '', token: '' });\n const [showLogin, setShowLogin] = useState(false);\n const [username, setUsername] = useState('');\n const [password, setPassword] = useState('');\n const [loginError, setLoginError] = useState('');\n const [loginPending, setLoginPending] = useState(false);\n\n // Try to restore from sessionStorage\n useEffect(() => {\n const saved = sessionStorage.getItem('anet_v3_auth');\n if (saved) {\n try { setAuth(JSON.parse(saved)); } catch {}\n }\n }, []);\n\n const doAuth = async (action: 'login' | 'register') => {\n if (!username.trim() || !password.trim()) return;\n setLoginPending(true);\n setLoginError('');\n try {\n const res = await fetch('/api/hub/auth', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ action, username, password }),\n });\n const data = await res.json();\n if (!data.ok) { setLoginError(data.error || 'Failed'); setLoginPending(false); return; }\n\n // Fetch user details\n const meRes = await fetch(`/api/hub/auth?token=${data.token}&endpoint=/api/auth/me`);\n const meData = await meRes.json();\n\n const newAuth: AuthState = {\n user: data.user,\n networks: meData.networks || [],\n currentNetwork: meData.current_network?.network_id || meData.networks?.[0]?.network_id || '',\n token: data.token,\n };\n setAuth(newAuth);\n sessionStorage.setItem('anet_v3_auth', JSON.stringify(newAuth));\n setShowLogin(false);\n setUsername('');\n setPassword('');\n } catch { setLoginError('Connection failed'); }\n setLoginPending(false);\n };\n\n const logout = () => {\n setAuth({ user: null, networks: [], currentNetwork: '', token: '' });\n sessionStorage.removeItem('anet_v3_auth');\n };\n\n const [editing, setEditing] = useState(false);\n const [editName, setEditName] = useState('');\n const [editEmail, setEditEmail] = useState('');\n\n const updateProfile = async () => {\n try {\n const res = await fetch(`/api/hub/auth?token=${auth.token}&endpoint=/api/auth/me`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ action: 'update_profile', token: auth.token, display_name: editName, email: editEmail }),\n });\n // Use a direct PUT via a new proxy approach\n const putRes = await fetch('/api/hub/auth', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ action: 'update_profile', token: auth.token, display_name: editName, email: editEmail }),\n });\n const data = await putRes.json();\n if (data.ok && data.user) {\n const updated = { ...auth, user: data.user };\n setAuth(updated);\n sessionStorage.setItem('anet_v3_auth', JSON.stringify(updated));\n setEditing(false);\n }\n } catch {}\n };\n\n // Don't show anything when not logged in — login page handles that\n if (!auth.user) return null;\n\n const initials = (auth.user.display_name || auth.user.username).slice(0, 2).toUpperCase();\n\n return (\n <div className=\"flex items-center justify-between bg-[#111128] border border-[#2a2a4a] rounded-lg px-4 py-2.5 mb-4\">\n {/* User info */}\n <div className=\"flex items-center gap-3\">\n <div className=\"w-8 h-8 rounded-full bg-gradient-to-br from-cyan-500 to-blue-600 flex items-center justify-center text-xs font-bold text-white\">\n {initials}\n </div>\n <div>\n <div className=\"text-sm text-white font-medium\">{auth.user.display_name || auth.user.username}</div>\n <div className=\"text-[10px] text-gray-500\">{auth.user.role} · {auth.user.user_id}</div>\n </div>\n </div>\n\n {/* Network switcher */}\n <div className=\"flex items-center gap-3\">\n {auth.networks.length > 0 && (\n <select\n value={auth.currentNetwork}\n onChange={e => {\n const updated = { ...auth, currentNetwork: e.target.value };\n setAuth(updated);\n setNetworkId(e.target.value);\n sessionStorage.setItem('anet_v3_auth', JSON.stringify(updated));\n }}\n className=\"bg-[#0a0a15] border border-[#2a2a4a] rounded px-2 py-1 text-xs text-white focus:outline-none\"\n >\n {auth.networks.map(n => (\n <option key={n.network_id} value={n.network_id}>{n.network_name}</option>\n ))}\n </select>\n )}\n <button onClick={() => { setEditName(auth.user?.display_name || ''); setEditEmail(''); setEditing(!editing); }}\n className=\"text-xs text-gray-500 hover:text-gray-300 whitespace-nowrap\"\n aria-label=\"Edit profile\">\n <span className=\"hidden sm:inline\">Edit</span>\n <svg className=\"sm:hidden w-4 h-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" strokeWidth=\"1.5\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.862 4.487z\" />\n </svg>\n </button>\n <button onClick={logout}\n className=\"text-xs text-gray-500 hover:text-gray-300 whitespace-nowrap\"\n aria-label=\"Sign out\">\n <span className=\"hidden sm:inline\">Sign out</span>\n <svg className=\"sm:hidden w-4 h-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" strokeWidth=\"1.5\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9\" />\n </svg>\n </button>\n </div>\n {editing && (\n <div className=\"flex flex-wrap items-center gap-2 mt-2 pt-2 border-t border-[#2a2a4a]\">\n <input type=\"text\" value={editName} onChange={e => setEditName(e.target.value)} placeholder=\"Display name\"\n className=\"bg-[#0a0a15] border border-[#2a2a4a] rounded px-2 py-1 text-xs text-white placeholder-gray-600 focus:outline-none w-32\" />\n <input type=\"email\" value={editEmail} onChange={e => setEditEmail(e.target.value)} placeholder=\"Email\"\n className=\"bg-[#0a0a15] border border-[#2a2a4a] rounded px-2 py-1 text-xs text-white placeholder-gray-600 focus:outline-none w-40\" />\n <button onClick={updateProfile} className=\"px-2 py-1 bg-cyan-600 hover:bg-cyan-500 text-white text-xs rounded\">Save</button>\n </div>\n )}\n </div>\n );\n}\n","'use client';\n\nimport { useState } from 'react';\nimport { TaskChatPanel } from './TaskChatPanel';\n\ninterface CommandCenterProps {\n /** Currently open chat tabs */\n tabs: string[];\n activeTab: string;\n onOpenTab: (alias: string) => void;\n onCloseTab: (alias: string) => void;\n onSetActive: (alias: string) => void;\n onClose: () => void;\n}\n\n/**\n * Multi-tab chat panel for commanding multiple agents simultaneously.\n * Wraps TaskChatPanel with a tab bar for switching between agents.\n */\nexport function CommandCenter({ tabs, activeTab, onOpenTab, onCloseTab, onSetActive, onClose }: CommandCenterProps) {\n if (tabs.length === 0) return null;\n\n return (\n <>\n {/* Backdrop */}\n <div className=\"fixed inset-0 bg-black/30 z-40 lg:hidden\" onClick={onClose} />\n\n {/* Panel */}\n <div className=\"fixed top-0 right-0 h-full w-full lg:w-[500px] bg-[#0a0a1a] border-l border-[#2a2a4a] z-50 flex flex-col shadow-2xl shadow-black/60 animate-slide-in\">\n {/* Tab bar */}\n <div className=\"flex items-center border-b border-[#2a2a4a] bg-[#0d0d1a] overflow-x-auto\">\n <div className=\"flex-1 flex min-w-0\">\n {tabs.map(alias => (\n <button\n key={alias}\n onClick={() => onSetActive(alias)}\n className={`flex items-center gap-1.5 px-3 py-2.5 text-xs border-b-2 transition-colors shrink-0 ${\n activeTab === alias\n ? 'border-cyan-400 text-cyan-300 bg-cyan-500/5'\n : 'border-transparent text-gray-500 hover:text-gray-300'\n }`}\n >\n <div className={`w-2 h-2 rounded-full ${activeTab === alias ? 'bg-cyan-400' : 'bg-gray-600'}`} />\n <span className=\"max-w-[80px] truncate\">{alias}</span>\n <button\n onClick={e => { e.stopPropagation(); onCloseTab(alias); }}\n className=\"ml-1 text-gray-600 hover:text-gray-300 p-0.5\"\n >\n ×\n </button>\n </button>\n ))}\n </div>\n <button onClick={onClose} className=\"text-gray-500 hover:text-white px-3 py-2.5 shrink-0 border-l border-[#2a2a4a]\">\n <svg className=\"w-4 h-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" strokeWidth={2}>\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M6 18L18 6M6 6l12 12\" />\n </svg>\n </button>\n </div>\n\n {/* Active chat - render all but only show active (preserves state) */}\n <div className=\"flex-1 relative overflow-hidden\">\n {tabs.map(alias => (\n <div key={alias} className={`absolute inset-0 ${activeTab === alias ? 'block' : 'hidden'}`}>\n <InlineChat alias={alias} />\n </div>\n ))}\n </div>\n </div>\n\n <style jsx global>{`\n @keyframes slide-in {\n from { transform: translateX(100%); }\n to { transform: translateX(0); }\n }\n .animate-slide-in {\n animation: slide-in 0.2s ease-out;\n }\n `}</style>\n </>\n );\n}\n\n/** Inline chat without the panel chrome (used inside CommandCenter tabs) */\nfunction InlineChat({ alias }: { alias: string }) {\n // Reuse TaskChatPanel's logic but render without the outer frame\n return <TaskChatPanel alias={alias} onClose={() => {}} inline />;\n}\n\n/**\n * Hook to manage multi-tab command center state.\n */\nexport function useCommandCenter() {\n const [tabs, setTabs] = useState<string[]>([]);\n const [activeTab, setActiveTab] = useState('');\n\n const openTab = (alias: string) => {\n setTabs(prev => prev.includes(alias) ? prev : [...prev, alias]);\n setActiveTab(alias);\n };\n\n const closeTab = (alias: string) => {\n setTabs(prev => {\n const next = prev.filter(t => t !== alias);\n if (activeTab === alias) setActiveTab(next[next.length - 1] || '');\n return next;\n });\n };\n\n const closeAll = () => {\n setTabs([]);\n setActiveTab('');\n };\n\n return { tabs, activeTab, openTab, closeTab, closeAll, setActiveTab };\n}\n","'use client';\n\nimport { useState } from 'react';\nimport type { Session } from './types';\nimport { AliasAvatar } from './AliasAvatar';\n\ninterface DispatchPanelProps {\n sessions: Session[];\n onClose: () => void;\n}\n\ninterface SendResult {\n alias: string;\n ok: boolean;\n error?: string;\n}\n\nexport function DispatchPanel({ sessions, onClose }: DispatchPanelProps) {\n const [selected, setSelected] = useState<Set<string>>(new Set());\n const [prompt, setPrompt] = useState('');\n const [priority, setPriority] = useState('normal');\n const [sending, setSending] = useState(false);\n const [results, setResults] = useState<SendResult[]>([]);\n const [filter, setFilter] = useState('');\n\n const onlineNodes = sessions.filter(s => s.status !== 'offline');\n const filtered = onlineNodes.filter(s =>\n !filter || s.alias.toLowerCase().includes(filter.toLowerCase()) || (s.agent || '').toLowerCase().includes(filter.toLowerCase())\n );\n\n const toggleNode = (alias: string) => {\n setSelected(prev => {\n const next = new Set(prev);\n if (next.has(alias)) next.delete(alias); else next.add(alias);\n return next;\n });\n };\n\n const selectAll = () => {\n if (selected.size === filtered.length) {\n setSelected(new Set());\n } else {\n setSelected(new Set(filtered.map(s => s.alias)));\n }\n };\n\n const dispatch = async () => {\n if (!prompt.trim() || selected.size === 0 || sending) return;\n setSending(true);\n setResults([]);\n\n const promises = [...selected].map(async alias => {\n try {\n const res = await fetch('/api/hub/send', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ alias, task: prompt, priority }),\n });\n const data = await res.json();\n return { alias, ok: !!data.ok, error: data.error };\n } catch (e) {\n return { alias, ok: false, error: 'send failed' };\n }\n });\n\n const res = await Promise.all(promises);\n setResults(res);\n setSending(false);\n };\n\n const successCount = results.filter(r => r.ok).length;\n\n return (\n <>\n <div className=\"fixed inset-0 bg-black/50 z-40 anet-fade-in\" onClick={onClose} />\n <div className=\"fixed inset-4 lg:inset-x-[15%] lg:inset-y-[5%] bg-[#0a0a1a] border border-[#2a2a4a] rounded-2xl z-50 flex flex-col shadow-2xl shadow-black/70 overflow-hidden anet-fade-in\">\n {/* Header */}\n <div className=\"flex items-center justify-between px-6 py-4 border-b border-[#2a2a4a] bg-[#0d0d1a]\">\n <div>\n <h2 className=\"text-lg font-bold text-white\">Dispatch Task</h2>\n <p className=\"text-xs text-gray-500 mt-0.5\">Send a task to one or more agents</p>\n </div>\n <button onClick={onClose} className=\"text-gray-500 hover:text-white p-1.5 rounded-lg hover:bg-[#1a1a2a]\">\n <svg className=\"w-5 h-5\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" strokeWidth={2}>\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M6 18L18 6M6 6l12 12\" />\n </svg>\n </button>\n </div>\n\n <div className=\"flex-1 flex flex-col lg:flex-row overflow-hidden\">\n {/* Left: Node selection */}\n <div className=\"lg:w-[280px] border-b lg:border-b-0 lg:border-r border-[#2a2a4a] flex flex-col\">\n <div className=\"px-4 py-3 border-b border-[#2a2a4a]\">\n <input\n type=\"text\" value={filter} onChange={e => setFilter(e.target.value)}\n placeholder=\"Filter agents...\"\n className=\"w-full bg-[#111128] border border-[#2a2a4a] rounded-lg px-3 py-2 text-xs text-white placeholder-gray-600 focus:border-cyan-500/40 focus:outline-none\"\n />\n <div className=\"flex items-center justify-between mt-2\">\n <button onClick={selectAll} className=\"text-[10px] text-cyan-400 hover:text-cyan-300\">\n {selected.size === filtered.length ? 'Deselect all' : `Select all (${filtered.length})`}\n </button>\n <span className=\"text-[10px] text-gray-600\">{selected.size} selected</span>\n </div>\n </div>\n <div className=\"flex-1 overflow-y-auto px-2 py-2 space-y-0.5 max-h-[200px] lg:max-h-none\">\n {filtered.map(s => (\n <button key={s.alias} onClick={() => toggleNode(s.alias)}\n className={`w-full flex items-center gap-2 px-3 py-2 rounded-lg text-xs text-left transition-colors ${\n selected.has(s.alias) ? 'bg-cyan-500/10 text-cyan-300 border border-cyan-500/20' : 'text-gray-400 hover:bg-[#1a1a2a]'\n }`}>\n <AliasAvatar alias={s.alias} size={16} />\n <div className={`w-1.5 h-1.5 rounded-full shrink-0 ${s.status === 'working' ? 'bg-green-400' : s.status === 'idle' ? 'bg-cyan-400' : 'bg-gray-500'}`} />\n <span className=\"truncate flex-1\">{s.alias}</span>\n <span className=\"text-[9px] text-gray-600\">{s.agent || '--'}</span>\n </button>\n ))}\n {filtered.length === 0 && <div className=\"text-center text-xs text-gray-600 py-4\">No online agents</div>}\n </div>\n </div>\n\n {/* Right: Prompt + Send */}\n <div className=\"flex-1 flex flex-col\">\n <div className=\"flex-1 px-6 py-4 flex flex-col\">\n <label className=\"text-xs text-gray-500 uppercase mb-2\">Task Prompt</label>\n <textarea\n value={prompt} onChange={e => setPrompt(e.target.value)}\n placeholder=\"Enter the task you want to dispatch...\"\n className=\"flex-1 min-h-[120px] bg-[#111128] border border-[#2a2a4a] rounded-xl px-4 py-3 text-sm text-white placeholder-gray-600 focus:border-cyan-500/40 focus:outline-none resize-none\"\n />\n\n <div className=\"flex items-center gap-3 mt-4\">\n <select value={priority} onChange={e => setPriority(e.target.value)}\n className=\"bg-[#111128] border border-[#2a2a4a] rounded-lg px-3 py-2 text-xs text-white focus:outline-none\">\n <option value=\"normal\">Normal priority</option>\n <option value=\"high\">High priority</option>\n <option value=\"low\">Low priority</option>\n </select>\n\n <div className=\"flex-1\" />\n\n <button onClick={dispatch} disabled={sending || !prompt.trim() || selected.size === 0}\n className=\"px-6 py-2.5 bg-gradient-to-r from-cyan-600 to-blue-600 hover:from-cyan-500 hover:to-blue-500 disabled:from-gray-800 disabled:to-gray-800 disabled:text-gray-600 text-white text-sm font-medium rounded-xl transition-all shadow-lg shadow-cyan-500/10 disabled:shadow-none active:scale-95\">\n {sending ? (\n <span className=\"flex items-center gap-2\">\n <span className=\"w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin\" />\n Sending...\n </span>\n ) : (\n `Dispatch to ${selected.size} agent${selected.size > 1 ? 's' : ''}`\n )}\n </button>\n </div>\n </div>\n\n {/* Results */}\n {results.length > 0 && (\n <div className=\"px-6 py-3 border-t border-[#2a2a4a] bg-[#0d0d1a]\">\n <div className=\"text-xs text-gray-500 mb-2\">\n {successCount}/{results.length} dispatched successfully\n </div>\n <div className=\"flex flex-wrap gap-1.5\">\n {results.map(r => (\n <span key={r.alias} className={`flex items-center gap-1.5 text-[10px] pl-1 pr-2 py-0.5 rounded-full border ${\n r.ok ? 'text-green-300 border-green-500/20 bg-green-500/5' : 'text-red-300 border-red-500/20 bg-red-500/5'\n }`}>\n <AliasAvatar alias={r.alias} size={14} />\n <span>{r.alias}</span>\n <span aria-hidden>{r.ok ? '✓' : r.error || '✗'}</span>\n </span>\n ))}\n </div>\n </div>\n )}\n </div>\n </div>\n </div>\n </>\n );\n}\n"],"names":["React","_interopDefaultLegacy","e","React__default","_defineProperties","target","props","i","length","descriptor","enumerable","configurable","writable","Object","defineProperty","key","_createClass","Constructor","protoProps","staticProps","prototype","isProd","process","env","isString","o","toString","call","StyleSheet","param","ref","_name","name","_optimizeForSpeed","optimizeForSpeed","invariant$1","_deletedRulePlaceholder","_serverSheet","undefined","_tags","_injected","_rulesCount","node","document","querySelector","_nonce","_proto","setOptimizeForSpeed","bool","flush","inject","isOptimizeForSpeed","_this","cssRules","insertRule","rule","index","cssText","push","deleteRule","getSheetForTag","tag","sheet","styleSheets","ownerNode","getSheet","insertionPoint","replaceRule","trim","error","console","warn","makeStyleTag","cssString","relativeToTag","createElement","setAttribute","type","appendChild","createTextNode","head","getElementsByTagName","insertBefore","get","condition","message","Error","hash","str","_$hash","charCodeAt","stringHash","sanitize","replace","cache","computeId","baseId","propsToString","String","computeSelector","id","css","selectoPlaceholderRegexp","idcss","mapRulesToStyle","options","map","args","nonce","dangerouslySetInnerHTML","__html","StyleSheetRegistry","_styleSheet","styleSheet","_sheet","_fromServer","_indices","_instancesCounts","add","Array","isArray","children","getIdAndRules","styleId","rules","indices","filter","remove","invariant","tagFromServer","parentNode","removeChild","forEach","update","nextProps","fromServer","keys","concat","join","Boolean","styles","dynamic","selectFromServer","elements","slice","querySelectorAll","reduce","acc","element","StyleSheetContext","createContext","displayName","createStyleRegistry","StyleRegistry","configuredRegistry","registry","rootRegistry","useContext","useState","Provider","value","useStyleRegistry","useInsertionEffect","useLayoutEffect","defaultRegistry","JSXStyle","info","tagInfo","exports","style","module","StatsBar","online","working","total","version","uptime","onlinePercent","Math","round","fleetEmpty","className","StatCard","label","sub","color","accent","border","accentKey","split"],"mappings":"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","ignoreList":[0,1]}
|
|
1
|
+
{"version":3,"sources":["../../../../../agent-network-dashboard/node_modules/styled-jsx/dist/index/index.js","../../../../../agent-network-dashboard/node_modules/styled-jsx/style.js","../../../../../agent-network-dashboard/app/page.tsx","../../../../../agent-network-dashboard/app/components/StatsBar.tsx","../../../../../agent-network-dashboard/app/components/BroadcastBar.tsx","../../../../../agent-network-dashboard/app/components/TopoGraph.tsx","../../../../../agent-network-dashboard/app/components/ChatPopover.tsx","../../../../../agent-network-dashboard/app/lib/vendorIdentity.ts","../../../../../agent-network-dashboard/app/components/AgentCard.tsx","../../../../../agent-network-dashboard/app/components/InboxPanel.tsx","../../../../../agent-network-dashboard/app/components/LoadingSkeleton.tsx","../../../../../agent-network-dashboard/app/components/UserBar.tsx","../../../../../agent-network-dashboard/app/components/CommandCenter.tsx","../../../../../agent-network-dashboard/app/components/DispatchPanel.tsx"],"sourcesContent":["require('client-only');\nvar React = require('react');\n\nfunction _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }\n\nvar React__default = /*#__PURE__*/_interopDefaultLegacy(React);\n\n/*\nBased on Glamor's sheet\nhttps://github.com/threepointone/glamor/blob/667b480d31b3721a905021b26e1290ce92ca2879/src/sheet.js\n*/ function _defineProperties(target, props) {\n for(var i = 0; i < props.length; i++){\n var descriptor = props[i];\n descriptor.enumerable = descriptor.enumerable || false;\n descriptor.configurable = true;\n if (\"value\" in descriptor) descriptor.writable = true;\n Object.defineProperty(target, descriptor.key, descriptor);\n }\n}\nfunction _createClass(Constructor, protoProps, staticProps) {\n if (protoProps) _defineProperties(Constructor.prototype, protoProps);\n if (staticProps) _defineProperties(Constructor, staticProps);\n return Constructor;\n}\nvar isProd = typeof process !== \"undefined\" && process.env && process.env.NODE_ENV === \"production\";\nvar isString = function(o) {\n return Object.prototype.toString.call(o) === \"[object String]\";\n};\nvar StyleSheet = /*#__PURE__*/ function() {\n function StyleSheet(param) {\n var ref = param === void 0 ? {} : param, _name = ref.name, name = _name === void 0 ? \"stylesheet\" : _name, _optimizeForSpeed = ref.optimizeForSpeed, optimizeForSpeed = _optimizeForSpeed === void 0 ? isProd : _optimizeForSpeed;\n invariant$1(isString(name), \"`name` must be a string\");\n this._name = name;\n this._deletedRulePlaceholder = \"#\" + name + \"-deleted-rule____{}\";\n invariant$1(typeof optimizeForSpeed === \"boolean\", \"`optimizeForSpeed` must be a boolean\");\n this._optimizeForSpeed = optimizeForSpeed;\n this._serverSheet = undefined;\n this._tags = [];\n this._injected = false;\n this._rulesCount = 0;\n var node = typeof window !== \"undefined\" && document.querySelector('meta[property=\"csp-nonce\"]');\n this._nonce = node ? node.getAttribute(\"content\") : null;\n }\n var _proto = StyleSheet.prototype;\n _proto.setOptimizeForSpeed = function setOptimizeForSpeed(bool) {\n invariant$1(typeof bool === \"boolean\", \"`setOptimizeForSpeed` accepts a boolean\");\n invariant$1(this._rulesCount === 0, \"optimizeForSpeed cannot be when rules have already been inserted\");\n this.flush();\n this._optimizeForSpeed = bool;\n this.inject();\n };\n _proto.isOptimizeForSpeed = function isOptimizeForSpeed() {\n return this._optimizeForSpeed;\n };\n _proto.inject = function inject() {\n var _this = this;\n invariant$1(!this._injected, \"sheet already injected\");\n this._injected = true;\n if (typeof window !== \"undefined\" && this._optimizeForSpeed) {\n this._tags[0] = this.makeStyleTag(this._name);\n this._optimizeForSpeed = \"insertRule\" in this.getSheet();\n if (!this._optimizeForSpeed) {\n if (!isProd) {\n console.warn(\"StyleSheet: optimizeForSpeed mode not supported falling back to standard mode.\");\n }\n this.flush();\n this._injected = true;\n }\n return;\n }\n this._serverSheet = {\n cssRules: [],\n insertRule: function(rule, index) {\n if (typeof index === \"number\") {\n _this._serverSheet.cssRules[index] = {\n cssText: rule\n };\n } else {\n _this._serverSheet.cssRules.push({\n cssText: rule\n });\n }\n return index;\n },\n deleteRule: function(index) {\n _this._serverSheet.cssRules[index] = null;\n }\n };\n };\n _proto.getSheetForTag = function getSheetForTag(tag) {\n if (tag.sheet) {\n return tag.sheet;\n }\n // this weirdness brought to you by firefox\n for(var i = 0; i < document.styleSheets.length; i++){\n if (document.styleSheets[i].ownerNode === tag) {\n return document.styleSheets[i];\n }\n }\n };\n _proto.getSheet = function getSheet() {\n return this.getSheetForTag(this._tags[this._tags.length - 1]);\n };\n _proto.insertRule = function insertRule(rule, index) {\n invariant$1(isString(rule), \"`insertRule` accepts only strings\");\n if (typeof window === \"undefined\") {\n if (typeof index !== \"number\") {\n index = this._serverSheet.cssRules.length;\n }\n this._serverSheet.insertRule(rule, index);\n return this._rulesCount++;\n }\n if (this._optimizeForSpeed) {\n var sheet = this.getSheet();\n if (typeof index !== \"number\") {\n index = sheet.cssRules.length;\n }\n // this weirdness for perf, and chrome's weird bug\n // https://stackoverflow.com/questions/20007992/chrome-suddenly-stopped-accepting-insertrule\n try {\n sheet.insertRule(rule, index);\n } catch (error) {\n if (!isProd) {\n console.warn(\"StyleSheet: illegal rule: \\n\\n\" + rule + \"\\n\\nSee https://stackoverflow.com/q/20007992 for more info\");\n }\n return -1;\n }\n } else {\n var insertionPoint = this._tags[index];\n this._tags.push(this.makeStyleTag(this._name, rule, insertionPoint));\n }\n return this._rulesCount++;\n };\n _proto.replaceRule = function replaceRule(index, rule) {\n if (this._optimizeForSpeed || typeof window === \"undefined\") {\n var sheet = typeof window !== \"undefined\" ? this.getSheet() : this._serverSheet;\n if (!rule.trim()) {\n rule = this._deletedRulePlaceholder;\n }\n if (!sheet.cssRules[index]) {\n // @TBD Should we throw an error?\n return index;\n }\n sheet.deleteRule(index);\n try {\n sheet.insertRule(rule, index);\n } catch (error) {\n if (!isProd) {\n console.warn(\"StyleSheet: illegal rule: \\n\\n\" + rule + \"\\n\\nSee https://stackoverflow.com/q/20007992 for more info\");\n }\n // In order to preserve the indices we insert a deleteRulePlaceholder\n sheet.insertRule(this._deletedRulePlaceholder, index);\n }\n } else {\n var tag = this._tags[index];\n invariant$1(tag, \"old rule at index `\" + index + \"` not found\");\n tag.textContent = rule;\n }\n return index;\n };\n _proto.deleteRule = function deleteRule(index) {\n if (typeof window === \"undefined\") {\n this._serverSheet.deleteRule(index);\n return;\n }\n if (this._optimizeForSpeed) {\n this.replaceRule(index, \"\");\n } else {\n var tag = this._tags[index];\n invariant$1(tag, \"rule at index `\" + index + \"` not found\");\n tag.parentNode.removeChild(tag);\n this._tags[index] = null;\n }\n };\n _proto.flush = function flush() {\n this._injected = false;\n this._rulesCount = 0;\n if (typeof window !== \"undefined\") {\n this._tags.forEach(function(tag) {\n return tag && tag.parentNode.removeChild(tag);\n });\n this._tags = [];\n } else {\n // simpler on server\n this._serverSheet.cssRules = [];\n }\n };\n _proto.cssRules = function cssRules() {\n var _this = this;\n if (typeof window === \"undefined\") {\n return this._serverSheet.cssRules;\n }\n return this._tags.reduce(function(rules, tag) {\n if (tag) {\n rules = rules.concat(Array.prototype.map.call(_this.getSheetForTag(tag).cssRules, function(rule) {\n return rule.cssText === _this._deletedRulePlaceholder ? null : rule;\n }));\n } else {\n rules.push(null);\n }\n return rules;\n }, []);\n };\n _proto.makeStyleTag = function makeStyleTag(name, cssString, relativeToTag) {\n if (cssString) {\n invariant$1(isString(cssString), \"makeStyleTag accepts only strings as second parameter\");\n }\n var tag = document.createElement(\"style\");\n if (this._nonce) tag.setAttribute(\"nonce\", this._nonce);\n tag.type = \"text/css\";\n tag.setAttribute(\"data-\" + name, \"\");\n if (cssString) {\n tag.appendChild(document.createTextNode(cssString));\n }\n var head = document.head || document.getElementsByTagName(\"head\")[0];\n if (relativeToTag) {\n head.insertBefore(tag, relativeToTag);\n } else {\n head.appendChild(tag);\n }\n return tag;\n };\n _createClass(StyleSheet, [\n {\n key: \"length\",\n get: function get() {\n return this._rulesCount;\n }\n }\n ]);\n return StyleSheet;\n}();\nfunction invariant$1(condition, message) {\n if (!condition) {\n throw new Error(\"StyleSheet: \" + message + \".\");\n }\n}\n\nfunction hash(str) {\n var _$hash = 5381, i = str.length;\n while(i){\n _$hash = _$hash * 33 ^ str.charCodeAt(--i);\n }\n /* JavaScript does bitwise operations (like XOR, above) on 32-bit signed\n * integers. Since we want the results to be always positive, convert the\n * signed int to an unsigned by doing an unsigned bitshift. */ return _$hash >>> 0;\n}\nvar stringHash = hash;\n\nvar sanitize = function(rule) {\n return rule.replace(/\\/style/gi, \"\\\\/style\");\n};\nvar cache = {};\n/**\n * computeId\n *\n * Compute and memoize a jsx id from a basedId and optionally props.\n */ function computeId(baseId, props) {\n if (!props) {\n return \"jsx-\" + baseId;\n }\n var propsToString = String(props);\n var key = baseId + propsToString;\n if (!cache[key]) {\n cache[key] = \"jsx-\" + stringHash(baseId + \"-\" + propsToString);\n }\n return cache[key];\n}\n/**\n * computeSelector\n *\n * Compute and memoize dynamic selectors.\n */ function computeSelector(id, css) {\n var selectoPlaceholderRegexp = /__jsx-style-dynamic-selector/g;\n // Sanitize SSR-ed CSS.\n // Client side code doesn't need to be sanitized since we use\n // document.createTextNode (dev) and the CSSOM api sheet.insertRule (prod).\n if (typeof window === \"undefined\") {\n css = sanitize(css);\n }\n var idcss = id + css;\n if (!cache[idcss]) {\n cache[idcss] = css.replace(selectoPlaceholderRegexp, id);\n }\n return cache[idcss];\n}\n\nfunction mapRulesToStyle(cssRules, options) {\n if (options === void 0) options = {};\n return cssRules.map(function(args) {\n var id = args[0];\n var css = args[1];\n return /*#__PURE__*/ React__default[\"default\"].createElement(\"style\", {\n id: \"__\" + id,\n // Avoid warnings upon render with a key\n key: \"__\" + id,\n nonce: options.nonce ? options.nonce : undefined,\n dangerouslySetInnerHTML: {\n __html: css\n }\n });\n });\n}\nvar StyleSheetRegistry = /*#__PURE__*/ function() {\n function StyleSheetRegistry(param) {\n var ref = param === void 0 ? {} : param, _styleSheet = ref.styleSheet, styleSheet = _styleSheet === void 0 ? null : _styleSheet, _optimizeForSpeed = ref.optimizeForSpeed, optimizeForSpeed = _optimizeForSpeed === void 0 ? false : _optimizeForSpeed;\n this._sheet = styleSheet || new StyleSheet({\n name: \"styled-jsx\",\n optimizeForSpeed: optimizeForSpeed\n });\n this._sheet.inject();\n if (styleSheet && typeof optimizeForSpeed === \"boolean\") {\n this._sheet.setOptimizeForSpeed(optimizeForSpeed);\n this._optimizeForSpeed = this._sheet.isOptimizeForSpeed();\n }\n this._fromServer = undefined;\n this._indices = {};\n this._instancesCounts = {};\n }\n var _proto = StyleSheetRegistry.prototype;\n _proto.add = function add(props) {\n var _this = this;\n if (undefined === this._optimizeForSpeed) {\n this._optimizeForSpeed = Array.isArray(props.children);\n this._sheet.setOptimizeForSpeed(this._optimizeForSpeed);\n this._optimizeForSpeed = this._sheet.isOptimizeForSpeed();\n }\n if (typeof window !== \"undefined\" && !this._fromServer) {\n this._fromServer = this.selectFromServer();\n this._instancesCounts = Object.keys(this._fromServer).reduce(function(acc, tagName) {\n acc[tagName] = 0;\n return acc;\n }, {});\n }\n var ref = this.getIdAndRules(props), styleId = ref.styleId, rules = ref.rules;\n // Deduping: just increase the instances count.\n if (styleId in this._instancesCounts) {\n this._instancesCounts[styleId] += 1;\n return;\n }\n var indices = rules.map(function(rule) {\n return _this._sheet.insertRule(rule);\n })// Filter out invalid rules\n .filter(function(index) {\n return index !== -1;\n });\n this._indices[styleId] = indices;\n this._instancesCounts[styleId] = 1;\n };\n _proto.remove = function remove(props) {\n var _this = this;\n var styleId = this.getIdAndRules(props).styleId;\n invariant(styleId in this._instancesCounts, \"styleId: `\" + styleId + \"` not found\");\n this._instancesCounts[styleId] -= 1;\n if (this._instancesCounts[styleId] < 1) {\n var tagFromServer = this._fromServer && this._fromServer[styleId];\n if (tagFromServer) {\n tagFromServer.parentNode.removeChild(tagFromServer);\n delete this._fromServer[styleId];\n } else {\n this._indices[styleId].forEach(function(index) {\n return _this._sheet.deleteRule(index);\n });\n delete this._indices[styleId];\n }\n delete this._instancesCounts[styleId];\n }\n };\n _proto.update = function update(props, nextProps) {\n this.add(nextProps);\n this.remove(props);\n };\n _proto.flush = function flush() {\n this._sheet.flush();\n this._sheet.inject();\n this._fromServer = undefined;\n this._indices = {};\n this._instancesCounts = {};\n };\n _proto.cssRules = function cssRules() {\n var _this = this;\n var fromServer = this._fromServer ? Object.keys(this._fromServer).map(function(styleId) {\n return [\n styleId,\n _this._fromServer[styleId]\n ];\n }) : [];\n var cssRules = this._sheet.cssRules();\n return fromServer.concat(Object.keys(this._indices).map(function(styleId) {\n return [\n styleId,\n _this._indices[styleId].map(function(index) {\n return cssRules[index].cssText;\n }).join(_this._optimizeForSpeed ? \"\" : \"\\n\")\n ];\n })// filter out empty rules\n .filter(function(rule) {\n return Boolean(rule[1]);\n }));\n };\n _proto.styles = function styles(options) {\n return mapRulesToStyle(this.cssRules(), options);\n };\n _proto.getIdAndRules = function getIdAndRules(props) {\n var css = props.children, dynamic = props.dynamic, id = props.id;\n if (dynamic) {\n var styleId = computeId(id, dynamic);\n return {\n styleId: styleId,\n rules: Array.isArray(css) ? css.map(function(rule) {\n return computeSelector(styleId, rule);\n }) : [\n computeSelector(styleId, css)\n ]\n };\n }\n return {\n styleId: computeId(id),\n rules: Array.isArray(css) ? css : [\n css\n ]\n };\n };\n /**\n * selectFromServer\n *\n * Collects style tags from the document with id __jsx-XXX\n */ _proto.selectFromServer = function selectFromServer() {\n var elements = Array.prototype.slice.call(document.querySelectorAll('[id^=\"__jsx-\"]'));\n return elements.reduce(function(acc, element) {\n var id = element.id.slice(2);\n acc[id] = element;\n return acc;\n }, {});\n };\n return StyleSheetRegistry;\n}();\nfunction invariant(condition, message) {\n if (!condition) {\n throw new Error(\"StyleSheetRegistry: \" + message + \".\");\n }\n}\nvar StyleSheetContext = /*#__PURE__*/ React.createContext(null);\nStyleSheetContext.displayName = \"StyleSheetContext\";\nfunction createStyleRegistry() {\n return new StyleSheetRegistry();\n}\nfunction StyleRegistry(param) {\n var configuredRegistry = param.registry, children = param.children;\n var rootRegistry = React.useContext(StyleSheetContext);\n var ref = React.useState(function() {\n return rootRegistry || configuredRegistry || createStyleRegistry();\n }), registry = ref[0];\n return /*#__PURE__*/ React__default[\"default\"].createElement(StyleSheetContext.Provider, {\n value: registry\n }, children);\n}\nfunction useStyleRegistry() {\n return React.useContext(StyleSheetContext);\n}\n\n// Opt-into the new `useInsertionEffect` API in React 18, fallback to `useLayoutEffect`.\n// https://github.com/reactwg/react-18/discussions/110\nvar useInsertionEffect = React__default[\"default\"].useInsertionEffect || React__default[\"default\"].useLayoutEffect;\nvar defaultRegistry = typeof window !== \"undefined\" ? createStyleRegistry() : undefined;\nfunction JSXStyle(props) {\n var registry = defaultRegistry ? defaultRegistry : useStyleRegistry();\n // If `registry` does not exist, we do nothing here.\n if (!registry) {\n return null;\n }\n if (typeof window === \"undefined\") {\n registry.add(props);\n return null;\n }\n useInsertionEffect(function() {\n registry.add(props);\n return function() {\n registry.remove(props);\n };\n // props.children can be string[], will be striped since id is identical\n }, [\n props.id,\n String(props.dynamic)\n ]);\n return null;\n}\nJSXStyle.dynamic = function(info) {\n return info.map(function(tagInfo) {\n var baseId = tagInfo[0];\n var props = tagInfo[1];\n return computeId(baseId, props);\n }).join(\" \");\n};\n\nexports.StyleRegistry = StyleRegistry;\nexports.createStyleRegistry = createStyleRegistry;\nexports.style = JSXStyle;\nexports.useStyleRegistry = useStyleRegistry;\n","module.exports = require('./dist/index').style\n","'use client';\n\nimport { useEffect, useState } from 'react';\nimport Link from 'next/link';\nimport { formatUptime, previewContent } from './components/utils';\nimport { StatsBar } from './components/StatsBar';\nimport { BroadcastBar } from './components/BroadcastBar';\nimport { TopoGraph } from './components/TopoGraph';\nimport { AgentCard } from './components/AgentCard';\nimport { InboxPanel } from './components/InboxPanel';\nimport { LoadingSkeleton } from './components/LoadingSkeleton';\nimport { NodesEmptyState as EmptyState } from './components/EmptyState';\nimport { AliasAvatar } from './components/AliasAvatar';\nimport { STATUS_DOT_HEX, STATUS_CHIP_CLASS } from './lib/status';\nimport { UserBar } from './components/UserBar';\nimport { CommandCenter, useCommandCenter } from './components/CommandCenter';\nimport { DispatchPanel } from './components/DispatchPanel';\nimport { useSessions, useHealth, useAnetConfig, useTasks, useStats } from './lib/hooks';\nimport { useSSE } from './lib/useSSE';\nimport { InboxMessage } from './components/types';\nimport { useSWRConfig } from 'swr';\n\nexport default function Dashboard() {\n // Auto-upgrade: if no V3 auth in session, force re-login to get user token\n useEffect(() => {\n const hasV3 = sessionStorage.getItem('anet_v3_auth');\n if (!hasV3) {\n // Try silent re-auth: logout old cookie + redirect to login\n fetch('/api/auth/logout', { method: 'POST' }).catch(() => {});\n window.location.assign('/login');\n }\n }, []);\n\n const { sessions, hint: sessHint, error: sessError, isLoading } = useSessions();\n const { health } = useHealth();\n const { config: anetConfig } = useAnetConfig();\n const { tasks } = useTasks({ limit: '500' });\n const { stats } = useStats();\n const [showTopo, setShowTopo] = useState(typeof window !== 'undefined' && window.innerWidth >= 1024);\n const [showConfig, setShowConfig] = useState(false);\n const cmd = useCommandCenter();\n const [showDispatch, setShowDispatch] = useState(false);\n const [inbox, setInbox] = useState<InboxMessage[]>([]);\n const [agentFilter, setAgentFilter] = useState<'all' | 'working' | 'idle' | 'offline'>('all');\n // #84: last node.renamed event — passed to TopoGraph so an open chat\n // popover follows the rename instead of pointing at a dead alias. `ts`\n // makes the effect re-fire even when the same from/to repeats.\n const [renameSignal, setRenameSignal] = useState<{ from: string; to: string; ts: number } | null>(null);\n const { mutate } = useSWRConfig();\n\n // SSE: instant revalidation on CommHub events\n // Opt-out via NEXT_PUBLIC_DISABLE_SSE=1 — avoids HTTP/1.1 head-of-line blocking on navigation\n const sseEnabled = process.env.NEXT_PUBLIC_DISABLE_SSE !== '1';\n const { connected: sseConnected, supported: sseSupported } = useSSE({\n url: '/api/hub/events',\n enabled: sseEnabled,\n onEvent: (event) => {\n // #84 (RFC-010 §3.4): node.renamed — revalidate the session list so the\n // new alias propagates instantly (TopoGraph + node grid re-render, the\n // avatar hue recomputes as a pure fn of alias) instead of waiting for\n // the next 5s poll. The renamed node's history keeps the old alias\n // server-side, so the task list needs no revalidation here.\n if (event.type === 'node.renamed') {\n mutate('/api/hub/status');\n const d = event.data as { old_alias?: string; new_alias?: string } | undefined;\n if (d?.old_alias && d?.new_alias) {\n setRenameSignal({ from: d.old_alias, to: d.new_alias, ts: Date.now() });\n }\n return;\n }\n if (['new_task', 'new_message', 'new_reply', 'node_status_changed', 'broadcast'].includes(event.type)) {\n mutate('/api/hub/status');\n mutate((key: string) => typeof key === 'string' && key.startsWith('/api/hub/tasks'), undefined, { revalidate: true });\n }\n },\n });\n\n // Fetch inbox (not in SWR since it accumulates)\n useEffect(() => {\n const fetchInbox = () => {\n fetch('/api/hub/inbox').then(r => r.json()).then(data => {\n if (data.messages?.length) setInbox(prev => {\n const ids = new Set(prev.map(m => m.id));\n const newMsgs = data.messages.filter((m: { id: string }) => !ids.has(m.id));\n return [...newMsgs, ...prev].slice(0, 100);\n });\n }).catch(() => {});\n };\n fetchInbox();\n const interval = setInterval(fetchInbox, 10000);\n return () => clearInterval(interval);\n }, []);\n\n if (isLoading) return <LoadingSkeleton />;\n\n const sseSessions = health?.sse_sessions || {};\n // SSE keys are `network_id:alias` since v0.7+ (per-network scoping).\n // Fall back to alias-only for legacy hubs.\n const sseLookup = (s: { alias: string; network_id?: string }) =>\n (s.network_id ? sseSessions[`${s.network_id}:${s.alias}`] : undefined) ?? sseSessions[s.alias];\n // Online = status is not 'offline' (not just SSE-connected)\n const isOnline = (s: { alias: string; status: string; network_id?: string }) => s.status !== 'offline' || !!sseLookup(s);\n const online = sessions.filter(isOnline).length;\n const total = sessions.length;\n const working = sessions.filter(s => s.status === 'working').length;\n const uptime = health ? formatUptime(health.uptime) : '--';\n const version = health?.version || '--';\n const configHealthy = Boolean(anetConfig?.hub && anetConfig.tokenConfigured);\n const configSourceLabel =\n anetConfig?.source === 'file' ? 'Local config'\n : anetConfig?.source === 'runtime-env' ? 'Runtime env'\n : 'Config missing';\n\n // Task stats: prefer /api/stats, fallback to manual\n const taskStats: Record<string, number> = {};\n if (stats?.tasks?.by_status?.length) {\n for (const s of stats.tasks.by_status) {\n taskStats[s.status] = s.count;\n }\n } else {\n for (const t of tasks) {\n taskStats[t.status] = (taskStats[t.status] || 0) + 1;\n }\n }\n\n const sortedSessions = [...sessions].sort((a, b) => {\n const aOnline = isOnline(a) ? 1 : 0;\n const bOnline = isOnline(b) ? 1 : 0;\n if (aOnline !== bOnline) return bOnline - aOnline;\n const aWorking = a.status === 'working' ? 1 : 0;\n const bWorking = b.status === 'working' ? 1 : 0;\n return bWorking - aWorking;\n });\n\n /** Round 70: when the fleet is empty, the Overview reorders to lead with\n * the \"Spin up your first agent\" CTA and hides the Quick Navigation /\n * Nav rail / Broadcast bar that would otherwise occupy prime real estate\n * with zeros and dead-end links. Computed once, used as a gate below. */\n const fleetEmpty = sessions.length === 0 && !sessError;\n\n return (\n <div className=\"min-h-screen bg-[#0a0a1a] text-gray-100 p-4 sm:p-6 font-mono\">\n <div className=\"lg:ml-0 ml-10\">\n <StatsBar online={online} working={working} total={total} version={version} uptime={uptime} />\n </div>\n\n {/* Dispatch + User Bar — Dispatch hidden when fleet empty (nothing to\n dispatch to); UserBar still useful (account/sign-out menu). */}\n <div className=\"flex items-center gap-3 mb-4\">\n {!fleetEmpty && (\n <button onClick={() => setShowDispatch(true)}\n className=\"px-4 py-2 bg-gradient-to-r from-cyan-600 to-blue-600 hover:from-cyan-500 hover:to-blue-500 text-white text-sm font-medium rounded-xl shadow-lg shadow-cyan-500/10 transition-all active:scale-95 shrink-0\">\n ⚡ Dispatch\n </button>\n )}\n <div className=\"flex-1\"><UserBar /></div>\n </div>\n\n {/* anet config (collapsed by default) */}\n <section className=\"mb-6 rounded-lg border border-[#2a2a4a] bg-[#111128] px-4 py-3 shadow-lg shadow-black/20\">\n <button onClick={() => setShowConfig(!showConfig)} className=\"w-full flex items-center justify-between text-left\">\n <div className=\"flex items-center gap-2 text-xs\">\n <span className=\"uppercase text-gray-600\">Config</span>\n <span className={`w-2 h-2 rounded-full ${configHealthy ? 'bg-green-400' : 'bg-red-400'}`} />\n <span className=\"text-gray-500\">{configSourceLabel}</span>\n </div>\n <svg className={`w-4 h-4 text-gray-600 transition-transform ${showConfig ? 'rotate-180' : ''}`} fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" strokeWidth={2}>\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M19 9l-7 7-7-7\" />\n </svg>\n </button>\n {showConfig && (\n <div className=\"mt-3 pt-3 border-t border-[#2a2a4a]\">\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"min-w-0\">\n <div className=\"text-gray-100 truncate text-sm\" title={anetConfig?.hub || undefined}>\n Hub: <span className={anetConfig?.hub ? 'text-cyan-300' : 'text-red-300'}>{anetConfig?.hub?.trim() || 'not configured'}</span>\n </div>\n </div>\n <div className=\"flex flex-wrap gap-2 text-xs\">\n <span className={`px-2.5 py-1 rounded-md border ${anetConfig?.tokenConfigured ? 'bg-blue-500/5 text-blue-300 border-blue-500/20' : 'bg-gray-500/5 text-gray-400 border-gray-500/20'}`}>\n Token: {anetConfig?.tokenPreview || 'not configured'}\n </span>\n </div>\n </div>\n {anetConfig?.error && <div className=\"mt-2 text-xs text-gray-600\">{anetConfig.error}</div>}\n </div>\n )}\n </section>\n\n {/* Task Status Stats */}\n {Object.keys(taskStats).length > 0 && (\n <section className=\"mb-6 rounded-lg border border-[#2a2a4a] bg-[#111128] px-4 py-3 shadow-lg shadow-black/20\">\n <div className=\"flex items-center justify-between mb-2\">\n <div className=\"text-xs uppercase text-gray-600\">Task Status</div>\n <Link href=\"/tasks\" prefetch={false} className=\"text-xs text-cyan-400 hover:text-cyan-300\">View all →</Link>\n </div>\n <div className=\"flex flex-wrap gap-2\">\n {/* Round 69: order is \"hot first\" — active flow before terminal\n states — intentionally different from the lifecycle order on\n /tasks. Colors come from shared STATUS_CHIP_CLASS so a\n palette tweak in app/lib/status.ts updates here too.\n 'created' added in r69 (was missing — enum-coverage bug). */}\n {(['running', 'delivered', 'acked', 'replied', 'created', 'failed', 'cancelled', 'expired', 'closed'] as const)\n .filter((key) => taskStats[key])\n .map((key) => (\n <Link key={key} href={`/tasks?status=${key}`} prefetch={false} className={`px-2.5 py-1 rounded-md border text-xs ${STATUS_CHIP_CLASS[key]} hover:opacity-80 transition-opacity`}>\n {key}: {taskStats[key]}\n </Link>\n ))}\n </div>\n </section>\n )}\n\n {/* Quick Actions — split into two distinct intents:\n (1) Top: live stat cards (carry data, drill-in on click)\n (2) Bottom: pure nav rail (no number, icon + label)\n Round 24 — wrap both in a labelled block so the rhythm reads as\n \"here are the main jumps\" instead of two disconnected strips.\n Round 70 — entire Quick Nav + Nav rail + Broadcast + Recent\n Activity block is hidden when the fleet is empty so the\n onboarding CTA gets the page above the fold. */}\n {!fleetEmpty && <>\n <div className=\"text-[10px] uppercase tracking-[0.12em] text-gray-600 mb-2\">Quick navigation</div>\n <section className=\"mb-3 grid grid-cols-3 gap-2 sm:gap-3\">\n {(() => {\n // Build breakdown popover content per card. Pure data — pure CSS\n // popover (no JS state) wires the hover-show transition below.\n const idleCount = sessions.filter(s => isOnline(s) && s.status !== 'working').length;\n const offlineCount = total - online;\n const orderedStatuses = ['running', 'replied', 'failed', 'cancelled', 'expired', 'closed', 'created', 'delivered', 'acked'];\n const failedRecent = tasks.filter((t: { status: string }) => t.status === 'failed').length;\n\n const cards = [\n {\n href: '/nodes', label: 'Nodes',\n value: `${online}/${total}`,\n sub: `${total > 0 ? Math.round((online / total) * 100) : 0}% online`,\n color: 'text-green-400 border-green-500/20',\n popover: total === 0 ? null : [\n { k: 'working', v: working, dot: '', color: '#4ade80' },\n { k: 'idle', v: idleCount, dot: '', color: '#22d3ee' },\n { k: 'offline', v: offlineCount, dot: '', color: '#9ca3af' },\n ],\n },\n {\n href: '/tasks', label: 'Tasks',\n value: String(Object.values(taskStats).reduce((a, b) => a + b, 0) || 0),\n sub: 'all-time',\n color: 'text-cyan-400 border-cyan-500/20',\n popover: Object.keys(taskStats).length === 0 ? null\n : orderedStatuses\n .filter(s => taskStats[s])\n .map(s => {\n // Inline hex avoids Tailwind purging dynamic\n // `bg-${color}-400` class names.\n const hex = ({\n running: '#4ade80', replied: '#a78bfa', failed: '#f87171',\n cancelled: '#facc15', expired: '#fb923c', closed: '#9ca3af',\n created: '#9ca3af', delivered: '#60a5fa', acked: '#22d3ee',\n } as Record<string, string>)[s] || '#9ca3af';\n return { k: s, v: taskStats[s], dot: '', color: hex };\n }),\n },\n {\n href: '/tasks?status=failed', label: 'Failed',\n value: String(taskStats['failed'] || 0),\n sub: taskStats['failed'] ? 'needs review' : 'none',\n color: taskStats['failed'] ? 'text-red-400 border-red-500/25' : 'text-gray-500 border-gray-700/30',\n popover: !failedRecent ? [{ k: 'no failures yet', v: '', dot: '', color: '#4b5563' }]\n : [{ k: `${failedRecent} in current view`, v: '', dot: '', color: '#f87171' }],\n },\n ];\n\n return cards.map(a => (\n <Link\n key={a.href}\n href={a.href}\n prefetch={false}\n className={`anet-stat-link group relative rounded-xl border ${a.color} bg-[#111128] px-3 py-3 transition-all hover:-translate-y-px`}\n >\n <div className=\"flex items-baseline justify-between\">\n <div className={`text-xl font-semibold tabular-nums ${a.color.split(' ')[0]}`}>{a.value}</div>\n <div className=\"hidden sm:block text-[10px] text-gray-600 group-hover:text-gray-400 transition-colors\">View →</div>\n </div>\n <div className=\"text-[11px] text-gray-400 mt-0.5\">{a.label}</div>\n <div className=\"text-[10px] text-gray-600 mt-px\">{a.sub}</div>\n\n {/* Hover popover — CSS-only, restrained. Hidden on touch\n (no :hover) so mobile is unaffected. Positioned just\n below the card so it doesn't fight nav rail. */}\n {a.popover && a.popover.length > 0 && (\n <div className=\"anet-stat-popover hidden md:block pointer-events-none absolute left-2 right-2 top-full mt-1 z-20 rounded-lg border border-[#2a2a4a] bg-[#0d0d1a] shadow-lg shadow-black/30 px-3 py-2 opacity-0 translate-y-[-2px] group-hover:opacity-100 group-hover:translate-y-0 transition-all duration-150 delay-100\">\n <div className=\"text-[10px] text-gray-600 uppercase tracking-wider mb-1.5\">Breakdown</div>\n <ul className=\"space-y-1\">\n {a.popover.map(row => (\n <li key={row.k} className=\"flex items-center gap-2 text-[11px]\">\n <span\n className=\"inline-block w-1.5 h-1.5 rounded-full shrink-0\"\n style={{ backgroundColor: row.color }}\n aria-hidden\n />\n <span className=\"text-gray-400 capitalize\">{row.k}</span>\n {row.v !== '' && <span className=\"ml-auto text-gray-300 tabular-nums font-medium\">{row.v}</span>}\n </li>\n ))}\n </ul>\n </div>\n )}\n </Link>\n ));\n })()}\n </section>\n\n {/* Nav rail — pure navigation, icon + label, no data */}\n <section className=\"mb-6 grid grid-cols-3 gap-2 sm:gap-3\">\n {[\n { href: '/messages', label: 'Messages', icon: 'M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z' },\n { href: '/logs', label: 'Audit log', icon: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z M14 2v6h6 M16 13H8 M16 17H8 M10 9H8' },\n { href: '/admin', label: 'Admin', icon: 'M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4z M6 21v-2a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v2' },\n ].map(a => (\n <Link key={a.href} href={a.href} prefetch={false}\n className=\"anet-nav-tile flex items-center justify-center gap-2 rounded-xl border border-[#2a2a4a] bg-[#111128] px-3 py-2.5 text-[12px] text-gray-400 hover:text-gray-200 transition-colors\">\n <svg className=\"w-4 h-4 shrink-0\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n <path d={a.icon} />\n </svg>\n <span>{a.label}</span>\n </Link>\n ))}\n </section>\n\n <BroadcastBar />\n\n {/* Recent Activity */}\n {tasks.length > 0 && (\n <section className=\"mb-6 bg-[#111128] border border-[#2a2a4a] rounded-xl p-4\">\n <div className=\"flex items-center justify-between mb-3\">\n <h2 className=\"text-sm font-semibold text-gray-300\">Recent Activity</h2>\n <Link href=\"/tasks\" className=\"text-xs text-cyan-400 hover:text-cyan-300\">All tasks →</Link>\n </div>\n <div className=\"space-y-2 max-h-40 overflow-y-auto\">\n {tasks.slice(0, 5).map((t: { task_id: string; from_name: string; to_name: string; status: string; content: string; created_at: string }) => (\n <div key={t.task_id} className=\"flex items-center gap-2 text-xs\">\n <span\n className=\"w-1.5 h-1.5 rounded-full shrink-0\"\n style={{ backgroundColor: STATUS_DOT_HEX[t.status] || '#6b7280' }}\n />\n {t.from_name && <AliasAvatar alias={t.from_name} size={14} />}\n <span className=\"text-gray-300 shrink-0 max-w-[20%] truncate\">{t.from_name || '?'}</span>\n <span className=\"text-gray-600\">→</span>\n {t.to_name && <AliasAvatar alias={t.to_name} size={14} />}\n <span className=\"text-gray-300 shrink-0 max-w-[20%] truncate\">{t.to_name || '?'}</span>\n <span className=\"text-gray-500 truncate flex-1\" title={t.content || ''}>{previewContent(t.content).slice(0, 60)}</span>\n <span className={`shrink-0 px-1.5 py-0.5 rounded text-[10px] border ${\n STATUS_CHIP_CLASS[t.status] || 'text-gray-500 border-gray-700/30'\n }`}>{t.status}</span>\n </div>\n ))}\n </div>\n </section>\n )}\n </>}\n\n {sessError && (\n <div className=\"bg-red-900/20 border border-red-800/40 text-red-300 px-4 py-3 rounded-lg mb-6 text-sm flex items-center justify-between\" role=\"alert\">\n <span>{String(sessError)}</span>\n <span className=\"text-gray-500 text-xs\">Check CommHub connection</span>\n </div>\n )}\n\n {sessions.length > 0 && (\n <div className=\"flex justify-center mb-4\">\n <button\n onClick={() => setShowTopo(!showTopo)}\n className=\"text-xs text-gray-500 hover:text-gray-300 border border-gray-700/50 px-4 py-1.5 rounded-lg transition-colors hover:border-gray-600 cursor-pointer\"\n >\n {showTopo ? 'Hide Topology' : 'Show Topology'}\n </button>\n </div>\n )}\n\n {/* Mobile hint when topo hidden */}\n {!showTopo && sessions.length > 0 && (\n <div className=\"lg:hidden text-center text-xs text-gray-600 mb-4\">\n Topology hidden on mobile for better readability\n </div>\n )}\n\n {showTopo && sessions.length > 0 && <TopoGraph sessions={sessions} sseSessions={sseSessions} renameSignal={renameSignal} />}\n\n {sessions.length === 0 && !sessError ? (\n <EmptyState\n hint={sessHint}\n taskHistoryCount={Object.values(taskStats).reduce((a, b) => a + b, 0)}\n />\n ) : (() => {\n const counts = {\n all: sortedSessions.length,\n working: sortedSessions.filter(s => isOnline(s) && s.status === 'working').length,\n idle: sortedSessions.filter(s => isOnline(s) && s.status !== 'working').length,\n offline: sortedSessions.filter(s => !isOnline(s)).length,\n };\n const filtered = sortedSessions.filter(s => {\n if (agentFilter === 'all') return true;\n if (agentFilter === 'offline') return !isOnline(s);\n if (agentFilter === 'working') return isOnline(s) && s.status === 'working';\n if (agentFilter === 'idle') return isOnline(s) && s.status !== 'working';\n return true;\n });\n // Filter chip color keyed to status (round 34): working=green, idle=cyan,\n // offline=gray, all=neutral. Inline hex dots avoid Tailwind v4 purge.\n const chips: { key: typeof agentFilter; label: string; dot?: string }[] = [\n { key: 'all', label: 'All' },\n { key: 'working', label: 'Working', dot: '#4ade80' },\n { key: 'idle', label: 'Idle', dot: '#22d3ee' },\n { key: 'offline', label: 'Offline', dot: '#6b7280' },\n ];\n return (\n <>\n <div className=\"mb-3 flex flex-wrap items-center gap-1\">\n {chips.map(c => (\n <button\n key={c.key}\n type=\"button\"\n onClick={() => setAgentFilter(c.key)}\n disabled={counts[c.key] === 0 && c.key !== 'all'}\n className={`flex items-center gap-1.5 rounded-md px-2.5 py-1 text-xs transition-colors disabled:opacity-30 disabled:cursor-not-allowed ${\n agentFilter === c.key\n ? 'bg-cyan-500/10 text-cyan-300 border border-cyan-500/30'\n : 'text-gray-500 hover:text-gray-200 hover:bg-[#1a1a2a] border border-transparent'\n }`}\n >\n {c.dot && <span aria-hidden className=\"inline-block w-1.5 h-1.5 rounded-full\" style={{ backgroundColor: c.dot }} />}\n <span>{c.label}</span>\n <span className={`text-[10px] tabular-nums ${agentFilter === c.key ? 'text-cyan-400' : 'text-gray-600'}`}>{counts[c.key]}</span>\n </button>\n ))}\n </div>\n {/* Round 48: previous breakpoints had `lg:grid-cols-2 xl:grid-cols-3`\n which kept lg (1024-1279px) at only 2 columns even though\n each AgentCard is fine ≥260px wide. With the sidebar (208px),\n main area at lg is ~816px so 3 cols at ~272px each fits.\n xl breakpoint auto-inherits lg=3 cols; 2xl bumps to 4. */}\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-3 sm:gap-4\">\n {filtered.map(s => (\n <AgentCard key={s.alias} session={s} hasSse={isOnline(s)} sseCount={sseLookup(s) || 0} onChat={cmd.openTab} />\n ))}\n </div>\n {filtered.length === 0 && (\n <div className=\"mt-4 text-center text-xs text-gray-600\">\n No agents match \"{agentFilter}\" — <button onClick={() => setAgentFilter('all')} className=\"underline hover:text-gray-400\">Show all</button>\n </div>\n )}\n </>\n );\n })()}\n\n <InboxPanel messages={inbox} />\n\n {/* Round 111 (issue #82): dropped the license badge — \"trial (12d\n left)\" read like a paywall countdown on an open-source dashboard\n and Vincent flagged it as misleading more than once. The SSE /\n polling dot stays: it's a real connection-status indicator, not\n a sales surface. */}\n <div className=\"mt-8 text-center text-xs text-gray-600 flex items-center justify-center gap-2 flex-wrap\">\n {sseSupported && (\n <>\n <span className={`w-1.5 h-1.5 rounded-full ${sseConnected ? 'bg-green-400' : 'bg-gray-600'}`} />\n {sseConnected ? 'SSE live' : 'SWR polling'}\n </>\n )}\n </div>\n\n {/* Dispatch Panel */}\n {showDispatch && <DispatchPanel sessions={sessions} onClose={() => setShowDispatch(false)} />}\n\n {/* Command Center (multi-tab chat) */}\n {cmd.tabs.length > 0 && (\n <CommandCenter\n tabs={cmd.tabs}\n activeTab={cmd.activeTab}\n onOpenTab={cmd.openTab}\n onCloseTab={cmd.closeTab}\n onSetActive={cmd.setActiveTab}\n onClose={cmd.closeAll}\n />\n )}\n </div>\n );\n}\n","'use client';\n\ninterface StatsBarProps {\n online: number;\n working: number;\n total: number;\n version: string;\n uptime: string;\n}\n\nexport function StatsBar({ online, working, total, version, uptime }: StatsBarProps) {\n const onlinePercent = total > 0 ? Math.round((online / total) * 100) : 0;\n const fleetEmpty = total === 0;\n\n return (\n <div className={fleetEmpty ? 'mb-4' : 'mb-8'}>\n {/* Title row */}\n <div className=\"flex flex-wrap items-center gap-3 mb-4\">\n <h1 className=\"text-2xl font-bold text-white tracking-tight\">Agent Network</h1>\n <span className=\"text-xs text-gray-500\">\n CommHub {version} · {uptime}\n </span>\n </div>\n\n {fleetEmpty ? (\n /* Round 72: thin status strip replaces the 4-card grid when fleet\n is empty. Saves ~280px on mobile (CTA y=650 → ~370) and keeps\n the same data visible in a single inline row. */\n <div className=\"anet-stat-strip flex flex-wrap items-center gap-x-4 gap-y-1.5 text-xs text-gray-500 border-t border-b border-[#2a2a4a] py-2\">\n <span className=\"inline-flex items-center gap-1.5\">\n <span aria-hidden className=\"inline-block w-1.5 h-1.5 rounded-full bg-gray-600\" />\n <span className=\"text-gray-300 tabular-nums\">0</span> online\n </span>\n <span className=\"text-gray-700\">·</span>\n <span className=\"inline-flex items-center gap-1.5\">\n <span aria-hidden className=\"inline-block w-1.5 h-1.5 rounded-full bg-gray-600\" />\n <span className=\"text-gray-300 tabular-nums\">0</span> working\n </span>\n <span className=\"text-gray-700\">·</span>\n <span className=\"inline-flex items-center gap-1.5\">\n <span aria-hidden className=\"inline-block w-1.5 h-1.5 rounded-full bg-gray-600\" />\n <span className=\"text-gray-300 tabular-nums\">0</span> registered\n </span>\n </div>\n ) : (\n /* Populated state — full 4-card grid as before */\n <div className=\"grid grid-cols-2 sm:grid-cols-4 gap-3\">\n <StatCard\n value={online}\n label=\"Online\"\n sub={`${onlinePercent}% of fleet`}\n color=\"text-green-400\"\n accent=\"from-green-500/20 to-green-500/0\"\n border=\"border-green-500/15\"\n />\n <StatCard\n value={working}\n label=\"Working\"\n sub={online > 0 ? `${Math.round((working / online) * 100)}% utilization` : '--'}\n color=\"text-cyan-400\"\n accent=\"from-cyan-500/20 to-cyan-500/0\"\n border=\"border-cyan-500/15\"\n />\n <StatCard\n value={total - online}\n label=\"Offline\"\n sub={total - online === 0 ? 'All systems go' : `${total - online} disconnected`}\n color=\"text-gray-400\"\n accent=\"from-gray-500/10 to-gray-500/0\"\n border=\"border-gray-500/15\"\n />\n <StatCard\n value={total}\n label=\"Total\"\n sub=\"Registered nodes\"\n color=\"text-white\"\n accent=\"from-blue-500/15 to-blue-500/0\"\n border=\"border-blue-500/15\"\n />\n </div>\n )}\n </div>\n );\n}\n\nfunction StatCard({ value, label, sub, color, accent, border }: {\n value: number; label: string; sub: string; color: string; accent: string; border: string;\n}) {\n // Extract the color family (green/cyan/gray/blue/white) from `color` prop\n // so the light-theme top-strip CSS can pick the right accent.\n const accentKey = color.replace('text-', '').split('-')[0];\n return (\n <div\n data-anet-stat-card={accentKey}\n className={`anet-stat-card relative overflow-hidden rounded-xl border ${border} bg-[#111128] px-4 py-3 transition-all`}\n >\n <div className={`absolute inset-0 bg-gradient-to-br ${accent} pointer-events-none`} />\n <div className=\"relative\">\n <div className={`text-3xl font-bold ${color} tabular-nums leading-tight`}>{value}</div>\n <div className=\"text-sm text-gray-300 mt-0.5\">{label}</div>\n <div className=\"text-xs text-gray-600 mt-1\">{sub}</div>\n </div>\n </div>\n );\n}\n","'use client';\n\nimport { useState } from 'react';\n\nexport function BroadcastBar() {\n const [broadcastMsg, setBroadcastMsg] = useState('');\n const [broadcasting, setBroadcasting] = useState(false);\n const [broadcastResult, setBroadcastResult] = useState('');\n\n const sendBroadcast = async () => {\n if (!broadcastMsg.trim()) return;\n setBroadcasting(true);\n setBroadcastResult('');\n try {\n const res = await fetch('/api/hub/broadcast', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ message: broadcastMsg }),\n });\n const data = await res.json();\n if (data.ok) {\n setBroadcastResult(`Broadcast sent to ${data.recipients} node(s)`);\n setBroadcastMsg('');\n } else {\n setBroadcastResult(`Failed: ${data.error || 'Send error'}`);\n }\n } catch (e: unknown) {\n setBroadcastResult(`Failed: ${e instanceof Error ? e.message : 'Send error'}`);\n }\n setBroadcasting(false);\n setTimeout(() => setBroadcastResult(''), 5000);\n };\n\n return (\n <div className=\"mb-6\">\n <div className=\"flex gap-2\">\n <input\n type=\"text\"\n value={broadcastMsg}\n onChange={e => setBroadcastMsg(e.target.value)}\n onKeyDown={e => e.key === 'Enter' && sendBroadcast()}\n placeholder=\"Broadcast message to all online agents...\"\n maxLength={500}\n aria-label=\"Broadcast message\"\n className=\"flex-1 bg-[#111128] border border-[#2a2a4a] rounded-lg px-4 py-2.5 text-sm text-white placeholder-gray-600 focus:border-blue-500/50 focus:ring-1 focus:ring-blue-500/20 focus:outline-none transition-colors\"\n />\n <button\n onClick={sendBroadcast}\n disabled={broadcasting || !broadcastMsg.trim()}\n aria-label=\"Send broadcast\"\n className=\"flex items-center gap-2 px-5 py-2.5 bg-blue-600 hover:bg-blue-500 disabled:bg-gray-800 disabled:text-gray-600 text-white text-sm rounded-lg transition-all font-medium cursor-pointer disabled:cursor-not-allowed\"\n >\n {broadcasting ? (\n <>\n <svg aria-hidden className=\"w-4 h-4 animate-spin\" viewBox=\"0 0 24 24\" fill=\"none\">\n <circle cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" strokeWidth=\"3\" opacity=\"0.25\" />\n <path d=\"M4 12a8 8 0 018-8\" stroke=\"currentColor\" strokeWidth=\"3\" strokeLinecap=\"round\" />\n </svg>\n <span>Sending…</span>\n </>\n ) : (\n <>\n {/* Megaphone icon — \"broadcast to all\" affordance */}\n <svg aria-hidden className=\"w-4 h-4\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n <path d=\"M3 11v3a1 1 0 0 0 1 1h2l3.29 3.29a1 1 0 0 0 1.71-.71V7.42a1 1 0 0 0-1.71-.71L6 10H4a1 1 0 0 0-1 1Z\" />\n <path d=\"M16 8a5 5 0 0 1 0 6\" opacity=\"0.7\" />\n <path d=\"M19 5a9 9 0 0 1 0 12\" opacity=\"0.45\" />\n </svg>\n <span>Broadcast</span>\n </>\n )}\n </button>\n </div>\n {broadcastMsg.length > 0 && (\n <div className=\"text-right text-xs text-gray-600 mt-1 tabular-nums\">{broadcastMsg.length}/500</div>\n )}\n {broadcastResult && (\n <div className={`mt-3 text-sm text-center anet-fade-in ${broadcastResult.startsWith('Failed') ? 'text-red-400' : 'text-green-400/80'}`}>\n {broadcastResult}\n </div>\n )}\n </div>\n );\n}\n","'use client';\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport useSWR from 'swr';\nimport { useRouter } from 'next/navigation';\nimport { Session } from './types';\nimport { aliasAvatarColors, aliasInitial } from './AliasAvatar';\nimport { ChatPopover } from './ChatPopover';\nimport { vendorForModel, runtimeIdentity, identityLine } from '../lib/vendorIdentity';\nimport { parseHubTime, relativeAgo } from '../lib/time';\nimport { DASHBOARD_VERSION } from '../lib/version';\n\n/** v0.10.0 Hero 1+2 / §3.F server-health hook — fetches the normalized\n * /api/hub/servers payload (preview.370 unblocked real-data via the\n * proxy schema-normalize layer) and exposes a per-hostname health\n * tier. red → worst-of-CPU/Mem/Disk ≥ 85%; amber → 60-85%; green\n * → < 60%; null when telemetry hasn't shipped yet OR the host is\n * offline.\n *\n * Shared with ServersDrawer via SWR's key-based dedup — both\n * consumers point at /api/hub/servers so the cache layer fans out\n * to a single fetch per refresh interval.\n */\ninterface ServerHealthRow {\n hostname: string;\n cpu_load_1min: number | null;\n cpu_cores: number;\n mem_used_gb: number | null;\n mem_total_gb: number | null;\n disk_used_gb: number | null;\n disk_total_gb: number | null;\n status: 'online' | 'offline';\n}\ntype ServerTier = 'red' | 'amber' | 'green';\nfunction classifyServer(s: ServerHealthRow): ServerTier | null {\n if (s.status === 'offline') return null;\n const cpuPct = s.cpu_load_1min != null && s.cpu_cores > 0 ? (s.cpu_load_1min / s.cpu_cores) * 100 : null;\n const memPct = s.mem_used_gb != null && s.mem_total_gb != null && s.mem_total_gb > 0 ? (s.mem_used_gb / s.mem_total_gb) * 100 : null;\n const diskPct = s.disk_used_gb != null && s.disk_total_gb != null && s.disk_total_gb > 0 ? (s.disk_used_gb / s.disk_total_gb) * 100 : null;\n const vals = [cpuPct, memPct, diskPct].filter((v): v is number => typeof v === 'number');\n if (vals.length === 0) return null;\n const worst = Math.max(...vals);\n if (worst >= 85) return 'red';\n if (worst >= 60) return 'amber';\n return 'green';\n}\nconst serversFetcher = async (url: string): Promise<{ servers: ServerHealthRow[] } | null> => {\n const res = await fetch(url);\n if (!res.ok) return null;\n return res.json();\n};\nfunction useServerHealthMap(): Map<string, ServerTier> {\n const { data } = useSWR<{ servers: ServerHealthRow[] } | null>(\n '/api/hub/servers',\n serversFetcher,\n { refreshInterval: 15000, dedupingInterval: 5000 },\n );\n return useMemo(() => {\n const m = new Map<string, ServerTier>();\n for (const s of data?.servers ?? []) {\n const tier = classifyServer(s);\n if (tier) m.set(s.hostname, tier);\n }\n return m;\n }, [data]);\n}\n\ninterface MessageFlow {\n from_alias: string;\n to_alias: string;\n content: string;\n created_at: string;\n}\n\ninterface TopoGraphProps {\n sessions: Session[];\n sseSessions: Record<string, number>;\n // #84: a node.renamed event from the Overview's SSE listener. When the\n // currently open chat popover targets `from`, it follows the rename to `to`.\n renameSignal?: { from: string; to: string; ts: number } | null;\n}\n\ninterface Point {\n x: number;\n y: number;\n}\n\ninterface FlowLink {\n key: string;\n from: string;\n to: string;\n count: number;\n content: string;\n /** ISO timestamp of the most recent message on this edge — drives\n * the Round 10 freshness fade so dormant links recede visually. */\n last_at: string;\n}\n\nconst cx = 500;\nconst cy = 330;\nconst onlineRadius = 220;\n// Round 97 (issue #50): tier into two rings when N > 8; round 98\n// (issue #61): tier into three rings when N > 14. At N=22 (Vincent's\n// network) two rings still leave 11 nodes per ring → inner chord of\n// 88px can't fit a 100px label; three rings give ~⌈N/3⌉ per ring so\n// every tier has enough arc room.\nconst onlineTierThreshold = 8;\nconst onlineTripleThreshold = 14;\nconst onlineInnerRadius = 175;\nconst onlineOuterRadius = 260;\nconst onlineTripleInnerR = 145;\nconst onlineTripleMidR = 215;\nconst onlineTripleOuterR = 285;\nconst offlineRadius = 325;\n\n/** Polar coordinate for a node on a ring. `rotateBy` lets the caller offset\n * the whole ring so two stacked rings don't align radially (round 25: the\n * offline ring gets a half-step rotation so its nodes sit in the gaps\n * between online ones). */\nfunction polarPoint(index: number, total: number, radius: number, rotateBy = 0) {\n const spread = total <= 2 ? Math.PI : Math.PI * 1.78;\n const start = -Math.PI / 2 - spread / 2;\n const angle = total <= 1 ? -Math.PI / 2 : start + (spread * index) / (total - 1) + rotateBy;\n return {\n x: cx + radius * Math.cos(angle),\n y: cy + radius * Math.sin(angle),\n };\n}\n\nfunction curvePath(from: Point, to: Point, lift = 0) {\n const mx = (from.x + to.x) / 2;\n const my = (from.y + to.y) / 2;\n const dx = to.x - from.x;\n const dy = to.y - from.y;\n const length = Math.hypot(dx, dy) || 1;\n const normalX = -dy / length;\n const normalY = dx / length;\n const control = {\n x: mx + normalX * lift,\n y: my + normalY * lift,\n };\n\n return `M${from.x},${from.y} Q${control.x},${control.y} ${to.x},${to.y}`;\n}\n\nfunction truncate(value: string, max: number) {\n return value.length > max ? `${value.slice(0, max - 1)}...` : value;\n}\n\n/** Round 46 / Loop: SWR-freshness chip.\n *\n * TopoGraph's data refreshes every 5 s via SWR but the user has no\n * visible signal that the canvas they're looking at is current. The\n * flow particles and chips (R42 active-links, R43 hub) tell you when\n * the FLEET last did something, not when the DATA last syncing. This\n * chip ticks `live · Ns` against a 1-second interval so the operator\n * can trust freshness without doing internal math.\n *\n * Owns its own setInterval so the parent topology doesn't re-render\n * every second (only this small chip does). lastSyncRef is updated\n * whenever the `sessions` prop reference changes — that's the SWR\n * refresh signal. */\nfunction FreshnessChip({ sessions }: { sessions: unknown }) {\n const lastSyncRef = useRef<number>(Date.now());\n const [now, setNow] = useState(() => Date.now());\n useEffect(() => {\n // sessions reference changed → SWR just delivered a new payload.\n lastSyncRef.current = Date.now();\n }, [sessions]);\n useEffect(() => {\n const id = setInterval(() => setNow(Date.now()), 1000);\n return () => clearInterval(id);\n }, []);\n const sec = Math.max(0, Math.floor((now - lastSyncRef.current) / 1000));\n // Tint the chip warmer when the data goes stale (>10s since last sync).\n // SWR's default refreshInterval here is 5s, so anything past ~10s is\n // either a poll miss or a network hiccup.\n const stale = sec > 10;\n // Round 187 / Loop: chip transitions between fresh (gray) and stale\n // (amber) colour palettes smoothly. Pre-R187 the className swap\n // snapped every time the stale boundary was crossed — could happen\n // multiple times per minute on a flaky network. Adding\n // transition-colors makes the stale-onset (gray → amber) and\n // recovery (amber → gray) ease through the bg / text / border\n // palette together. 300ms matches R161/R162 active-links chip\n // freshness fade timing for visual consistency in the chip row.\n // Round 315 / Loop: FreshnessChip joins the R313-R314 chip-row\n // data-weight family. When it appears (stale state only — R275\n // gated rendering to stale), it sits next to working/online/\n // active-links chips that all carry font-medium (R313) plus the\n // vendor letter chips (R314). Without font-medium the warning\n // chip would render at default 400 next to data chips at 500\n // — visual inconsistency right at the moment the chip exists to\n // grab attention. font-medium adds it to the HTML-context data\n // tier (R312-R314 family); the amber bg/text/border still does\n // the warning-state work, the weight just keeps the chip in the\n // same data-typography ladder as its siblings.\n // Round 377 / Loop: FreshnessChip baseClass picks up `tabular-nums`.\n // The chip-row's last untouched chip joins the R224-R357 broader\n // tabular-nums sweep:\n // R224 edge badge digit\n // R225 hub digit / panel header counts / recent row count\n // R232 chip row counts (working / online / active-links)\n // R321 recent row timestamp\n // R322 recent panel hot count\n // R323 filter pin pill counts\n // R333 vendor count suffix\n // R357 active-links freshness suffix wrapper\n // R377 FreshnessChip body (this round)\n // `font-mono` already gives equal-width glyphs but `tabular-nums`\n // is the explicit-invariant the rest of the chip row carries.\n // FreshnessChip body reads `lag · {sec}s` — the {sec} digit grows\n // every second; tabular-nums explicitly locks digit width so the\n // chip stays planted as the seconds counter ticks past 9 → 10 →\n // 99 → 100. R187 transition-colors duration-300 + R275 stale-only\n // render gate + R315 font-medium R313 family alignment all\n // preserved.\n const baseClass = \"hidden sm:inline px-2.5 py-1 rounded-md font-mono font-medium tabular-nums border transition-colors duration-300\";\n const colorClass = stale\n ? \"bg-amber-500/10 text-amber-300 border-amber-500/20\"\n : \"bg-gray-500/10 text-gray-400 border-gray-500/20\";\n /* Round 275 / Loop: simplification per Vincent 5214/5215-5217 visual\n audit (clutter cleanup for Twitter screenshot). Pre-R275 the chip\n ALWAYS rendered — \"live · 5s\" gray-on-gray at rest, \"lag · 15s\"\n amber when stale. The fresh state is an \"everything's fine\"\n affirmation that's implicit elsewhere on the canvas (counts\n updating, flows animating). Adding a permanent chip to the chip-\n row's right end for that affirmation is added visual chrome\n without proportional info value.\n\n R275 converts the chip to a CONDITIONAL warning indicator: render\n only when stale (sec > 10). Fresh state → null (chip absent). The\n amber stale chip still appears as a warning when SWR lags, so\n users see the problem signal; the fresh state implicitly relies\n on other liveness signals (recent-signal panel rows, edge\n animations, count updates).\n\n Net effect: chip-row at rest has 1 fewer chip (cleaner Twitter\n screenshot, less right-edge chrome), but signals appear on\n stale-onset to direct attention. */\n if (!stale) return null;\n return (\n <span\n className={`${baseClass} ${colorClass}`}\n title={stale ? `Last sync ${sec}s ago — SWR refresh may be lagging` : `Live data · refreshes every 5s · last sync ${sec}s ago`}\n data-freshness-chip\n data-freshness-chip-stale={stale ? 'true' : 'false'}\n >\n {/* Round 272 / Loop: swap prefix word to match color state so\n text and color point the same way. Pre-R272 the chip read\n \"live · {sec}s\" in BOTH fresh (gray) and stale (amber)\n states — the amber color signals \"concerning\" but \"live\"\n still says \"fresh data flowing\", a visual contradiction.\n Post-R272: fresh=\"live · {sec}s\" (gray + reassuring), stale=\n \"lag · {sec}s\" (amber + signals lagging). Same monospace\n cell count (3 chars + \" · \" + digits + \"s\") so no chip\n width jitter on threshold crossing; R187 transition-colors\n duration-300 still eases the bg/color flip. Title (hover\n tooltip) still spells out the full meaning in either\n state. */}\n {/* Round 410 / Loop: FreshnessChip body picks up the chip-\n internal-hierarchy arc. Pre-R410 the body rendered as a\n single text node `lag · {sec}s` with the parent's font-\n medium (fw=500) applied uniformly. R410 splits the digit\n and unit into separate spans so the chip's internal\n typography mirrors the family pattern R333-R341/R362/R369/\n R389 established for the chip row:\n digit (fw=600) data tier\n unit (fw=500 + opacity=0.7) label tier\n The `lag` prefix stays at the chip's baseline (fw=500\n from parent font-medium) — it labels the state, not a\n data value. data-freshness-chip-digit / -unit attrs\n surface the spans for tests. tabular-nums + transition-\n colors + R275 stale-only gate all preserved. */}\n {stale ? 'lag' : 'live'} · <span className=\"font-semibold\" data-freshness-chip-digit>{sec}</span><span className=\"opacity-70\" data-freshness-chip-unit>s</span>\n </span>\n );\n}\n\n/** Round 36 / Loop: prefers-reduced-motion hook.\n *\n * Round 29's a11y sweep zeroed CSS animations via media query, but SVG\n * SMIL `<animate>` / `<animateMotion>` elements aren't reachable from CSS\n * — they need JS to opt out. This hook reads the media query and listens\n * for changes so the topology's flow particles, pulses, click ripple\n * and hub breath honour the OS-level preference. */\nfunction useReducedMotion(): boolean {\n const [reduced, setReduced] = useState(false);\n useEffect(() => {\n if (typeof window === 'undefined' || !window.matchMedia) return;\n const mq = window.matchMedia('(prefers-reduced-motion: reduce)');\n setReduced(mq.matches);\n const onChange = (e: MediaQueryListEvent) => setReduced(e.matches);\n mq.addEventListener('change', onChange);\n return () => mq.removeEventListener('change', onChange);\n }, []);\n return reduced;\n}\n\n// Round 38 / Loop: parseHubTime + relativeAgo factored to app/lib/time.ts\n// — the Round 35 fix lives there now alongside the Round 37 mirror so\n// any future TZ-safe parse update happens in one place.\n\n/** Round 12 / Loop: status trio audit.\n *\n * Each (status × theme) cell returns a {primary, halo, text} trio. The trio\n * invariant — keep it when adding states or tweaking shades:\n *\n * light: primary = <hue>-600 halo = <hue>-100 * text = <hue>-800/900\n * dark: primary = <hue>-400/500 halo = <hue>-900 text = <hue>-100\n *\n * * offline.halo light deviates intentionally to slate-200 (#e2e8f0):\n * slate-100 (#f1f5f9) is too close to the panel bg (#f8fafc) and the\n * halo would vanish. The other three rows keep the 100-shade.\n *\n * Audit caught one cross-family drift before this round: online-other halo\n * light was #dbeafe (blue-100) while its primary (#0284c7 sky-600) and text\n * (#0c4a6e sky-900) were sky-family — now sky-100 (#e0f2fe). Tiny visual\n * difference; large hygiene win — every trio is now mono-hue. */\nfunction nodeStatus(session: Session, isOnline: boolean, isLight: boolean) {\n if (!isOnline) {\n return {\n label: 'offline',\n primary: isLight ? '#94a3b8' : '#6b7280', // slate-400 / gray-500\n halo: isLight ? '#e2e8f0' : '#111827', // slate-200* / gray-900\n text: isLight ? '#475569' : '#9ca3af', // slate-600 / gray-400\n };\n }\n if (session.status === 'working') {\n return {\n label: 'working',\n primary: isLight ? '#059669' : '#22c55e', // emerald-600 / green-500\n halo: isLight ? '#d1fae5' : '#14532d', // emerald-100 / green-900\n text: isLight ? '#065f46' : '#dcfce7', // emerald-800 / green-100\n };\n }\n if (session.status === 'idle') {\n return {\n label: 'idle',\n primary: isLight ? '#0d9488' : '#2dd4bf', // teal-600 / teal-400\n halo: isLight ? '#ccfbf1' : '#134e4a', // teal-100 / teal-900\n text: isLight ? '#115e59' : '#ccfbf1', // teal-800 / teal-100\n };\n }\n return {\n label: session.status || 'online',\n primary: isLight ? '#0284c7' : '#38bdf8', // sky-600 / sky-400\n halo: isLight ? '#e0f2fe' : '#0c4a6e', // sky-100 / sky-900 (was blue-100 — drift fixed Round 12)\n text: isLight ? '#0c4a6e' : '#e0f2fe', // sky-900 / sky-100\n };\n}\n\n/** Theme-aware color palette for the topology SVG. */\ninterface Palette {\n panelStops: [string, string, string];\n radarStops: { color: string; opacity: number }[];\n arrowFill: string;\n ringStroke: string;\n spokeStroke: { active: string; idle: string };\n flowEdge: string;\n flowPath: string;\n flowParticle: string;\n nodeFill: { online: string; offline: string };\n labelBox: { fill: string; stroke: string };\n legendBox: { fill: string; stroke: string };\n legendText: string;\n legendHeadline: string;\n legendAccent: string;\n containerBg: string;\n containerBorder: string;\n topRailGradient: string;\n}\n\nconst DARK_PALETTE: Palette = {\n panelStops: ['#0b1220', '#080814', '#101018'],\n radarStops: [\n { color: '#22d3ee', opacity: 0.18 },\n { color: '#22c55e', opacity: 0.045 },\n { color: '#020617', opacity: 0 },\n ],\n arrowFill: '#67e8f9',\n ringStroke: '#164e63',\n spokeStroke: { active: '#22d3ee', idle: '#155e75' },\n flowEdge: '#67e8f9',\n flowPath: '#e0f2fe',\n flowParticle: '#fef08a',\n nodeFill: { online: '#020617', offline: '#080814' },\n labelBox: { fill: '#020617', stroke: '#1f2937' },\n legendBox: { fill: '#020617', stroke: '#1f2937' },\n legendText: '#94a3b8',\n legendHeadline: '#e5e7eb',\n legendAccent: '#67e8f9',\n containerBg: '#080814',\n containerBorder: '#2a2a4a',\n topRailGradient: 'from-transparent via-cyan-400/70 to-transparent',\n};\n\nconst LIGHT_PALETTE: Palette = {\n panelStops: ['#f8fafc', '#ffffff', '#f1f5f9'],\n radarStops: [\n { color: '#10b981', opacity: 0.06 },\n { color: '#3b82f6', opacity: 0.03 },\n { color: '#ffffff', opacity: 0 },\n ],\n arrowFill: '#10b981',\n ringStroke: '#cbd5e1',\n spokeStroke: { active: '#10b981', idle: '#cbd5e1' },\n flowEdge: '#10b981',\n flowPath: '#475569',\n flowParticle: '#f59e0b',\n nodeFill: { online: '#ffffff', offline: '#f8fafc' },\n labelBox: { fill: '#ffffff', stroke: '#e2e8f0' },\n legendBox: { fill: '#ffffff', stroke: '#e2e8f0' },\n legendText: '#475569',\n legendHeadline: '#0f172a',\n legendAccent: '#0d9488',\n containerBg: '#ffffff',\n containerBorder: '#e3e6eb',\n topRailGradient: 'from-transparent via-emerald-500/40 to-transparent',\n};\n\nfunction useTheme(): 'light' | 'dark' {\n const [theme, setTheme] = useState<'light' | 'dark'>('dark');\n useEffect(() => {\n const read = () => {\n const t = document.documentElement.getAttribute('data-theme') || 'cyber';\n setTheme(t === 'light' || t === 'mint' ? 'light' : 'dark');\n };\n read();\n const obs = new MutationObserver(read);\n obs.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });\n return () => obs.disconnect();\n }, []);\n return theme;\n}\n\n/** Round 100 (issue #79): brand showcase mode. Activate via `?brand=intern`\n * on any dashboard page — TopoGraph nodes show the 书小生 mascot instead\n * of alias initials. Stored in localStorage so the flag persists across\n * navigation. Clears with `?brand=none` or `?brand=` (empty). */\nfunction useBrand(): string | null {\n const [brand, setBrand] = useState<string | null>(null);\n useEffect(() => {\n try {\n const url = new URL(window.location.href);\n const param = url.searchParams.get('brand');\n if (param !== null) {\n if (param === '' || param === 'none') {\n localStorage.removeItem('anet-brand');\n setBrand(null);\n } else {\n localStorage.setItem('anet-brand', param);\n setBrand(param);\n }\n } else {\n setBrand(localStorage.getItem('anet-brand'));\n }\n } catch {}\n }, []);\n return brand;\n}\n\n/** Round 106 (issue #83): cluster agents by shared alias prefix so a team\n * reads as one unit in the topology. Adjacent (sorted) aliases that share\n * a ≥2-char prefix join the same group; the group key is the prefix common\n * to every member. Singletons map to their own alias (no hue shift). The\n * group key feeds aliasAvatarColors() so e.g. all 通信* nodes get one hue,\n * all 研究员* another — the \"同色相 tint\" clustering option from #83. */\nfunction commonPrefix(a: string, b: string): string {\n let i = 0;\n while (i < a.length && i < b.length && a[i] === b[i]) i++;\n return a.slice(0, i);\n}\n\n/** #83 + #111: group nodes that share a ≥2-char alias prefix OR a project_dir\n * (\"either criterion → same group\", Vincent 4724). Union-find over the\n * sessions; the component label prefers the shared project_dir's basename,\n * else the common alias prefix. Returns alias → groupKey. */\nfunction computeGroups(sessions: { alias: string; project_dir?: string | null }[]): Record<string, string> {\n const n = sessions.length;\n const parent = sessions.map((_, i) => i);\n const find = (i: number): number => {\n while (parent[i] !== i) { parent[i] = parent[parent[i]]; i = parent[i]; }\n return i;\n };\n const union = (a: number, b: number) => {\n const ra = find(a), rb = find(b);\n if (ra !== rb) parent[ra] = rb;\n };\n\n // union by shared project_dir\n const byDir: Record<string, number[]> = {};\n sessions.forEach((s, i) => {\n const d = s.project_dir?.trim();\n if (d) (byDir[d] ||= []).push(i);\n });\n for (const idxs of Object.values(byDir)) {\n for (let k = 1; k < idxs.length; k++) union(idxs[0], idxs[k]);\n }\n\n // union by shared ≥2-char alias prefix — sort, link adjacent pairs\n const order = sessions.map((_, i) => i).sort((a, b) => sessions[a].alias.localeCompare(sessions[b].alias));\n for (let k = 0; k + 1 < order.length; k++) {\n if (commonPrefix(sessions[order[k]].alias, sessions[order[k + 1]].alias).length >= 2) {\n union(order[k], order[k + 1]);\n }\n }\n\n // label each connected component\n const comps: Record<number, number[]> = {};\n for (let i = 0; i < n; i++) (comps[find(i)] ||= []).push(i);\n const keys: Record<string, string> = {};\n for (const members of Object.values(comps)) {\n let label: string;\n if (members.length === 1) {\n label = sessions[members[0]].alias;\n } else {\n const dirs = new Set(members.map(i => sessions[i].project_dir?.trim()).filter(Boolean) as string[]);\n if (dirs.size === 1) {\n const d = [...dirs][0];\n label = d.split('/').filter(Boolean).pop() || d;\n } else {\n label = members.map(i => sessions[i].alias).reduce((a, b) => commonPrefix(a, b));\n if (label.length < 2) label = sessions[members[0]].alias;\n }\n }\n for (const i of members) keys[sessions[i].alias] = label;\n }\n return keys;\n}\n\nfunction buildFlowLinks(messages: MessageFlow[], positions: Record<string, Point>) {\n const links = new Map<string, FlowLink>();\n\n messages.forEach(message => {\n if (\n !positions[message.from_alias] ||\n !positions[message.to_alias] ||\n message.from_alias === message.to_alias\n ) {\n return;\n }\n\n const key = `${message.from_alias}->${message.to_alias}`;\n const current = links.get(key);\n\n // Keep the most-recent timestamp per pair so the render can fade\n // dormant edges (Round 10 freshness fade).\n const incoming = message.created_at || '';\n const last_at = !current\n ? incoming\n : (incoming > current.last_at ? incoming : current.last_at);\n\n links.set(key, {\n key,\n from: message.from_alias,\n to: message.to_alias,\n count: (current?.count || 0) + 1,\n content: current?.content || message.content,\n last_at,\n });\n });\n\n return [...links.values()].slice(0, 18);\n}\n\nexport function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProps) {\n const theme = useTheme();\n const isLight = theme === 'light';\n const reducedMotion = useReducedMotion();\n const pal = isLight ? LIGHT_PALETTE : DARK_PALETTE;\n const brand = useBrand();\n const isIntern = brand === 'intern';\n // v0.10.0 Hero 1+2 / §3.F — per-host health tier map. Composes\n // with #119 ServersDrawer (same SWR key, deduped). When a node's\n // host server crosses into the red tier (CPU/Mem/Disk ≥ 85%),\n // the per-node SVG render adds a faint amber outer ring to flag\n // the issue without leaving the topology view.\n const hostHealthMap = useServerHealthMap();\n // R133: Next.js client-router for the recent-signal panel \"+N more\"\n // navigation. TopoGraph hasn't needed routing before — every other\n // affordance composes back into the canvas's own state — but the\n // truncated-flow footer is the one place where the user logically\n // wants to leave: \"show me the rest of the flows\" → /messages.\n const router = useRouter();\n const [messages, setMessages] = useState<MessageFlow[]>([]);\n // Issue #87: ring | grid layout toggle. Ring is the tiered-radial default;\n // grid arranges nodes in an N×M grid (better for 30+ nodes). Persisted to\n // localStorage like the zoom/pan view state. Declared above nodePositions\n // since that useMemo branches on it.\n const [layout, setLayout] = useState<'ring' | 'grid'>('ring');\n useEffect(() => {\n try {\n const saved = localStorage.getItem('anet-topo-layout');\n if (saved === 'grid' || saved === 'ring') {\n setLayout(saved);\n } else if (sessions.length >= 20) {\n // v0.10.0 Hero 3 Wave 1 §3.D — auto-grid for dense fleets.\n // When the user hasn't explicitly chosen a layout (no\n // localStorage entry), default to `grid` once the fleet\n // crosses 20 nodes. Below 20, the ring layout reads more\n // attractive (per #87 + R97-99 tier-ring history); at 20+\n // the ring starts packing tier 3 (R98 triple-tier\n // threshold) and grid scales cleaner — every cell visible\n // at the same density, no overlap-test risk from tier\n // crowding. The user's explicit toggle (R163 chrome\n // Ring|Grid) always wins by writing to anet-topo-layout,\n // so this only sets the *initial* preference for first-\n // time users on a dense fleet. Threshold 20 picked to\n // align with the existing density-aware breakpoints (R98\n // tier flip; R109 dense-label gating at 16). */}\n setLayout('grid');\n }\n } catch {}\n // sessions.length intentionally NOT in deps — this runs once on\n // mount and shouldn't re-fire when nodes join/leave (which would\n // override a user's mid-session toggle). The Ring|Grid chrome\n // button remains the authoritative source post-mount.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n // Round 170 / Loop: layout toggle (Ring ↔ Grid) used to teleport\n // every node from its old position to its new one in one paint\n // frame — the most jarring single user action left on the\n // canvas. Solution: dim the viewport <g> to ~45% opacity for\n // the duration of the swap so the eye reads it as a soft\n // crossfade-blink rather than a hard teleport. layoutSwitching\n // is a one-shot flag; the inline style on the viewport <g>\n // reads it and lerps opacity 1 → 0.45 → 1 across the swap.\n // Auto-clears after 400ms (covers fade-down 250ms + buffer for\n // React to commit the new layout's positions). Same pattern as\n // R168's smoothView arming but on a different visual axis\n // (opacity, not transform).\n const [layoutSwitching, setLayoutSwitching] = useState(false);\n const toggleLayout = () => {\n setLayoutSwitching(true);\n setTimeout(() => setLayoutSwitching(false), 400);\n setLayout(prev => {\n const next = prev === 'ring' ? 'grid' : 'ring';\n try { localStorage.setItem('anet-topo-layout', next); } catch {}\n return next;\n });\n };\n // Issue #113: node size scale (Vincent 4727 — nodes too big / crowded at\n // ~22 nodes). S/M/L → 0.7/0.84/1.0; default M (one notch down from the old\n // fixed size). Persisted like the layout toggle.\n const [nodeScale, setNodeScale] = useState(0.84);\n useEffect(() => {\n try {\n const saved = parseFloat(localStorage.getItem('anet-topo-nodescale') || '');\n if (saved === 0.7 || saved === 0.84 || saved === 1) setNodeScale(saved);\n } catch {}\n }, []);\n // Round 171 / Loop: nodeSize change picks up the R170 crossfade\n // pattern. Clicking S/M/L in the chrome re-derives every node's\n // radius + label sizing + (in grid layout) cell spacing — a\n // wholesale visual shift. Pre-R171 the resize snapped in one\n // paint frame; with this flag the viewport <g> opacity dims to\n // 0.45 for ~400ms, masking the redraw as a soft blink (same\n // vocabulary R170 uses for layout toggle). Bails early when\n // the picked scale matches the current one — clicking the\n // already-active button shouldn't fire a fade. Composes with\n // R170 layoutSwitching via `||` in the opacity expression; both\n // flags drive the same opacity transition but expose distinct\n // data-* attributes so tests can disambiguate which gesture\n // armed the crossfade.\n const [nodeSizeSwitching, setNodeSizeSwitching] = useState(false);\n const pickNodeScale = (v: number) => {\n if (v === nodeScale) return;\n setNodeSizeSwitching(true);\n setTimeout(() => setNodeSizeSwitching(false), 400);\n setNodeScale(v);\n try { localStorage.setItem('anet-topo-nodescale', String(v)); } catch {}\n };\n\n useEffect(() => {\n const fetchMessages = async () => {\n try {\n const res = await fetch('/api/hub/messages?limit=50');\n if (res.status === 401) {\n window.location.assign('/login');\n return;\n }\n\n const data = await res.json();\n setMessages(data.messages || []);\n } catch {}\n };\n fetchMessages();\n const interval = setInterval(fetchMessages, 5000);\n return () => clearInterval(interval);\n }, []);\n\n const {\n onlineNodes,\n offlineNodes,\n nodePositions,\n flowLinks,\n activeAliases,\n groupKeys,\n groupBoxes,\n gridContentBottom,\n } = useMemo(() => {\n const sseCount = (s: { alias: string; network_id?: string }) =>\n (s.network_id ? sseSessions[`${s.network_id}:${s.alias}`] : undefined) ?? sseSessions[s.alias];\n // Round 106 (issue #83): sort by alias so same-prefix agents\n // (通信龙 / 通信牛 / 通信工程马 …, or 研究员1号 / 研究员2号 …) end up\n // adjacent in the array — the tier layout below assigns angles by\n // index, so contiguous-in-array becomes contiguous-in-ring, i.e.\n // each team visually clusters. localeCompare keeps CJK ordering sane.\n const byAlias = (a: Session, b: Session) => a.alias.localeCompare(b.alias);\n // #112 umbrella: ghost age-out — an offline node not seen recently is\n // almost certainly a deleted node the server `/api/status` still\n // returns (#74 root cause is server-side; this is the dashboard-side\n // fallback so stale ghosts stop cluttering the topology). A missing\n // last_seen_at is kept (conservative — could be a legitimately new node).\n // Round 27 / P0: the 24h threshold was too lenient — Vincent's preview.29\n // screenshot showed B站马 nodes deleted ~4 h earlier still visible. A\n // healthy agent heartbeats every few seconds; if it's been silent for\n // an hour it's effectively gone. 1 h gives a fresh disconnect time to\n // come back while removing dead nodes well within an operator session.\n const GHOST_MS = 60 * 60 * 1000;\n const now = Date.now();\n const isGhost = (s: Session) => {\n if (s.status !== 'offline' || sseCount(s) || !s.last_seen_at) return false;\n // Round 35 / Loop: parseHubTime normalises SQL-style timestamps to UTC\n // before parsing so non-UTC browsers don't see a phantom 8-hour skew\n // and ghost freshly-disconnected nodes.\n const t = parseHubTime(s.last_seen_at);\n return t !== null && now - t > GHOST_MS;\n };\n const online = sessions.filter(s => s.status !== 'offline' || sseCount(s)).sort(byAlias);\n const offline = sessions.filter(s => s.status === 'offline' && !sseCount(s) && !isGhost(s)).sort(byAlias);\n const positions: Record<string, Point> = {};\n\n if (layout === 'grid') {\n // Issue #87 + #111: group-banded grid. Nodes are sorted by alias so\n // same-prefix aliases are adjacent; each multi-member prefix group then\n // gets its OWN row(s) starting at column 0, while singletons pack into\n // shared rows. Group-banded placement keeps every group's bounding box\n // (#111) a clean rectangle — no box ever overlaps another group's.\n const all = [...online, ...offline];\n const groupKeys = computeGroups(all);\n const cols = Math.max(1, Math.ceil(Math.sqrt(all.length)));\n // #112: gy0 starts below the (now compact) recent-signal / legend\n // overlay panels — their max bottom edge is y≈112 — so grid row 0 is\n // never occluded; gy1 extends near the canvas bottom for breathing room.\n const gx0 = 150, gx1 = 850, gy0 = 126, gy1 = 652;\n const cellW = (gx1 - gx0) / cols;\n // largest node radius at the current size scale — drives the group\n // label band + the row-height floor so nothing ever overlaps.\n const nodeR = Math.round(26 * nodeScale);\n\n // ordered runs of consecutive same-group-key nodes (≥2 = real group)\n const runs: { key: string; members: Session[] }[] = [];\n for (const s of all) {\n const gk = groupKeys[s.alias];\n const last = runs[runs.length - 1];\n if (last && last.key === gk) last.members.push(s);\n else runs.push({ key: gk, members: [s] });\n }\n\n // Pass 1 — assign each run to a band.\n //\n // v0.10.4 #150 (Vincent /goal 5453): \"不是一起的落单的怎么散落在中间了\".\n // Pre-#150 algo interleaved singletons between real groups as\n // centred bands, so orphan nodes appeared scattered in the middle\n // between cluster boxes. Vincent screenshot called this out as\n // \"layout 算法一点都不好\". Fix: bundle ALL singletons into ONE\n // band at the bottom of the grid + render an \"其他\" cluster box\n // around them. Multi-member prefix groups still go first in\n // alias order (existing #83/#111 behaviour). Net effect:\n // row 0..N-1: real prefix groups (left-aligned, own cluster box)\n // row N..M: single \"其他\" band collecting all orphans\n // (left-aligned, single cluster box at bottom)\n // No orphans → no orphan band → behaviour identical to pre-#150\n // for fleets where every node has a prefix-group match.\n type Band = { members: Session[]; startRow: number; centred: boolean; isGroup: boolean; isOrphan?: boolean };\n const bands: Band[] = [];\n let row = 0;\n const orphanMembers: Session[] = [];\n for (const run of runs) {\n if (run.members.length >= 2) {\n bands.push({ members: run.members, startRow: row, centred: false, isGroup: true });\n row += Math.ceil(run.members.length / cols);\n } else {\n // single-member run → collect for the bottom orphan band\n orphanMembers.push(...run.members);\n }\n }\n if (orphanMembers.length > 0) {\n bands.push({ members: orphanMembers, startRow: row, centred: false, isGroup: true, isOrphan: true });\n row += Math.ceil(orphanMembers.length / cols);\n }\n const totalRows = Math.max(1, row);\n // #112: the group label sits in a band ABOVE the topmost node, so the\n // band must clear the node radius — GROUP_TOP is node-relative, never\n // cellH-derived (cellH-derived was the label↔node overlap bug Vincent\n // hit). cellH then floors at GROUP_TOP + 30 so the label band + bottom\n // padding always fit and stacked group boxes never touch; past the\n // floor the grid overflows and zoom/pan handles it.\n const GROUP_TOP = nodeR + 20;\n // Round 27 / P0 (Vincent screenshot preview.29): dense plain-text\n // node labels (`pos.y + radius + denseDrop`, with a 3 px containerBg\n // stroke halo for readability) at the BOTTOM row of band N would\n // paint OVER the start of band N+1's group label, creating the\n // \"blueleap → eleap\", \"agent-network-dashboard → t-network / board\"\n // visual chopping. Geometry of the collision:\n // dense label visual bottom = node_N.y + radius + denseDrop + halo\n // group label glyph top = boxY + 4 (text y=boxY+14, ~10px ascent)\n // = node_N+1.y - GROUP_TOP + 4\n // node_N+1.y - node_N.y = cellH for consecutive rows, so no-collide:\n // cellH ≥ radius + denseDrop + halo + GROUP_TOP - 4 + buffer\n // With halo=3 buffer=4: cellH ≥ nodeR + denseDrop + GROUP_TOP + 3.\n const denseDrop = nodeScale < 0.8 ? 12 : 14;\n const cellH = Math.max(\n 2 * nodeR + 22, // node + dense label within band\n GROUP_TOP + 12, // group-label band + box padding\n nodeR + denseDrop + GROUP_TOP + 3, // round-27 dense↔group label clearance\n Math.min(100, (gy1 - gy0) / totalRows),\n );\n\n // Pass 2 — place each band's members.\n for (const band of bands) {\n band.members.forEach((s, idx) => {\n const rowInBand = Math.floor(idx / cols);\n const c = idx % cols;\n const inRow = Math.min(cols, band.members.length - rowInBand * cols);\n const inset = band.centred ? ((cols - inRow) * cellW) / 2 : 0;\n positions[s.alias] = {\n x: gx0 + inset + (c + 0.5) * cellW,\n y: gy0 + (band.startRow + rowInBand + 0.5) * cellH,\n };\n });\n }\n\n const links = buildFlowLinks(messages, positions);\n const active = new Set<string>();\n links.forEach(link => { active.add(link.from); active.add(link.to); });\n // #111: one bounding box per multi-member group (Vincent 4722). Each\n // group owns its rows; GROUP_PAD fills the row space left below the\n // nodes after the label band, so stacked group boxes always have a\n // gap between them (GROUP_TOP is defined above, with cellH).\n const GROUP_PAD = Math.max(8, Math.min(26, cellH - GROUP_TOP - 8)); // side/bottom\n const groupBoxes = bands\n .filter(b => b.isGroup)\n .map(band => {\n const pts = band.members.map(s => positions[s.alias]).filter(Boolean);\n const xs = pts.map(p => p.x);\n const ys = pts.map(p => p.y);\n const minX = Math.min(...xs), minY = Math.min(...ys);\n // Round 58 / Loop: per-group status mix for the label pip strip.\n // Working = status==='working'. Idle = online but not working.\n // Offline = !isOnline (either status==='offline' AND no SSE, or\n // ghost-purged elsewhere — but ghosts never reach groupBoxes\n // since they're filtered out upstream). Counts feed the label\n // tspans directly so the strip stays inside the label's bbox,\n // preserving the node↔label overlap-test guarantee from R19.\n let w = 0, i = 0, o = 0;\n for (const s of band.members) {\n const isOn = s.status !== 'offline' || !!sseCount(s);\n if (s.status === 'working') w++;\n else if (isOn) i++;\n else o++;\n }\n // v0.10.4 #150 — orphan band (singletons bundled at bottom)\n // renders with a \"其他\" cluster box; the box-key drives the\n // R63 label render + R86 hover-pin keying + #99 tooltip\n // member listing, so all the existing group-box machinery\n // applies uniformly to the orphan bucket too.\n return {\n key: band.isOrphan\n ? '其他'\n : band.members.length\n ? groupKeys[band.members[0].alias]\n : '',\n count: band.members.length,\n statuses: { working: w, idle: i, offline: o },\n x: minX - GROUP_PAD,\n y: minY - GROUP_TOP,\n w: Math.max(...xs) - minX + GROUP_PAD * 2,\n h: Math.max(...ys) - minY + GROUP_TOP + GROUP_PAD,\n };\n });\n // Round 28 / Loop: surface the grid's natural content bottom so the\n // mount effect can auto-fit zoom when the layout would overflow the\n // viewBox. = bottom of the last node row + its label drop + a small\n // breathing buffer. Round 27's cellH bump makes this overflow more\n // common (30-node fleets reach ~774 px, viewBox is 680).\n const gridContentBottom = gy0 + totalRows * cellH + 8;\n return {\n onlineNodes: online,\n offlineNodes: offline,\n nodePositions: positions,\n flowLinks: links,\n activeAliases: active,\n groupKeys,\n groupBoxes,\n gridContentBottom,\n };\n }\n\n // Round 97 (issue #50) + 98 (issue #61): three layout modes by N.\n // N ≤ 8 → single ring (r=220)\n // 8 < N ≤ 14 → two rings (inner r=175, outer r=260, half-step rot)\n // N > 14 → three rings (r=145/215/285, ⌈N/3⌉ per ring)\n // Each ring's spread is 1.78π so its node count drives chord length:\n // labels are 100px wide so each ring needs ≥110px chord per node.\n const tripleTier = online.length > onlineTripleThreshold;\n const dualTier = !tripleTier && online.length > onlineTierThreshold;\n let outerOnlineCount = online.length;\n if (tripleTier) {\n const per = Math.ceil(online.length / 3);\n const r1 = online.slice(0, per);\n const r2 = online.slice(per, 2 * per);\n const r3 = online.slice(2 * per);\n r1.forEach((s, index) => {\n positions[s.alias] = polarPoint(index, Math.max(r1.length, 1), onlineTripleInnerR);\n });\n const r1Spread = r1.length <= 2 ? Math.PI : Math.PI * 1.78;\n const r1Step = r1.length > 1 ? r1Spread / (r1.length - 1) : 0;\n r2.forEach((s, index) => {\n positions[s.alias] = polarPoint(index, Math.max(r2.length, 1), onlineTripleMidR, r1Step / 2);\n });\n r3.forEach((s, index) => {\n positions[s.alias] = polarPoint(index, Math.max(r3.length, 1), onlineTripleOuterR);\n });\n outerOnlineCount = r3.length;\n } else if (dualTier) {\n const innerCount = Math.ceil(online.length / 2);\n const outerCount = online.length - innerCount;\n const innerNodes = online.slice(0, innerCount);\n const outerNodes = online.slice(innerCount);\n innerNodes.forEach((s, index) => {\n positions[s.alias] = polarPoint(index, Math.max(innerCount, 1), onlineInnerRadius);\n });\n const innerSpread = innerCount <= 2 ? Math.PI : Math.PI * 1.78;\n const innerStep = innerCount > 1 ? innerSpread / (innerCount - 1) : 0;\n outerNodes.forEach((s, index) => {\n positions[s.alias] = polarPoint(index, Math.max(outerCount, 1), onlineOuterRadius, innerStep / 2);\n });\n outerOnlineCount = outerCount;\n } else {\n online.forEach((s, index) => {\n positions[s.alias] = polarPoint(index, Math.max(online.length, 1), onlineRadius);\n });\n }\n\n // Offset the offline ring radially by half the outermost online step so\n // offline bubbles sit in the angular gaps between online bubbles instead\n // of stacking directly behind them. Also push the outer ring further when\n // there are many offline nodes so labels don't crowd the legend.\n const outerSpreadBase = outerOnlineCount <= 2 ? Math.PI : Math.PI * 1.78;\n const outerStep = outerOnlineCount > 1 ? outerSpreadBase / (outerOnlineCount - 1) : 0;\n const offlineRotation = outerOnlineCount > 0 ? outerStep / 2 : 0;\n const offlineR = offlineRadius + Math.max(0, offline.length - 4) * 6;\n\n offline.forEach((s, index) => {\n positions[s.alias] = polarPoint(index, Math.max(offline.length, 1), offlineR, offlineRotation);\n });\n\n const links = buildFlowLinks(messages, positions);\n const active = new Set<string>();\n links.forEach(link => {\n active.add(link.from);\n active.add(link.to);\n });\n\n // Round 106 (issue #83): group key per alias → shared hue per team.\n const groupKeys = computeGroups([...online, ...offline]);\n\n return {\n onlineNodes: online,\n offlineNodes: offline,\n nodePositions: positions,\n flowLinks: links,\n activeAliases: active,\n groupKeys,\n // #111: group boxes are a grid-layout feature only — radially scattered\n // ring nodes can't be cleanly boxed. Ring keeps the #83 prefix hue.\n groupBoxes: [] as { key: string; count: number; statuses: { working: number; idle: number; offline: number }; x: number; y: number; w: number; h: number }[],\n // ring fits within VIEWBOX_H by construction (offlineRadius=325 + centre at y=330)\n gridContentBottom: 0,\n };\n }, [messages, sessions, sseSessions, layout, nodeScale]);\n\n const workingCount = onlineNodes.filter(s => s.status === 'working').length;\n // Round 6 / Loop: vendor distribution for the header chip — at a glance\n // \"what's in the fleet\" (A:5 M:2 O:8 书:12 …) without opening a node.\n // Sorted by count desc; \"unknown\" vendors collapse into a \"?\" bucket.\n const vendorDist = useMemo(() => {\n const tally = new Map<string, { initial: string; count: number; color: string }>();\n for (const s of [...onlineNodes, ...offlineNodes]) {\n const v = vendorForModel(s.model);\n const key = v.id === 'unknown' ? '?' : v.initial;\n const cur = tally.get(key);\n if (cur) cur.count++;\n else tally.set(key, { initial: key, count: 1, color: v.mono.text });\n }\n return [...tally.values()].sort((a, b) => b.count - a.count);\n }, [onlineNodes, offlineNodes]);\n // Round 109 (Vincent 4582 P0): hover-gated labels above this node count\n // so dense fleets show clean avatars instead of a wall of overlapping\n // label cards. 16 ≈ where the triple-tier rings start to crowd.\n const denseLayout = onlineNodes.length + offlineNodes.length > 16;\n const [hoveredAlias, setHoveredAlias] = useState<string | null>(null);\n // Round 86 / Loop: pointer-over-label preview. R63 wired the group\n // label to click-pin but hover gave no preview — the same hover/click\n // gap R83 closed for pressure-bar segments. This state lets the\n // label trigger the same dim mechanism a node hover already drives,\n // independently of whether the cursor happens to be over a member\n // node. ORs into hoveredGroup below so the existing derivation logic\n // (which composes with hoveredAlias, activeGroup, R85 marching ants)\n // keeps working unchanged.\n const [hoveredGroupLabel, setHoveredGroupLabel] = useState<string | null>(null);\n // Round 8 / Loop: which group is currently focused. Nodes outside this\n // group + other group boxes fade so the eye locks onto the team you're\n // pointing at. Singletons use their own alias as the group key.\n const hoveredGroup = hoveredGroupLabel\n ?? (hoveredAlias ? (groupKeys[hoveredAlias] ?? hoveredAlias) : null);\n // Round 63 / Loop: sticky group focus. R8 dims non-group nodes while\n // a member is hovered, but releasing the hover lets the focus fade.\n // Clicking the group label pins the group key here; activeGroup =\n // hoveredGroup ?? pinnedGroup so hover transiently overrides the\n // pin (handy for spot-comparing teams). Same compose pattern as\n // R60/R61 pinnedStatus.\n const [pinnedGroup, setPinnedGroup] = useState<string | null>(() => {\n // R66: same per-tab persistence treatment as pinnedStatus above.\n // The group key is opaque text (could be any prefix), so the init\n // reader can't validate it against a known enum — it just reads\n // whatever's stored. A useEffect below clears it if the value no\n // longer matches any current group (sessions changed since save).\n if (typeof window === 'undefined') return null;\n try { return sessionStorage.getItem('anet-topo-pinned-group'); } catch { return null; }\n });\n const activeGroup = hoveredGroup ?? pinnedGroup;\n // R66: sync pinnedGroup into sessionStorage when it changes; clear\n // it if it no longer matches any current group (the session set\n // can change between loads). The pinnedStatus sync sits next to\n // its declaration further down.\n useEffect(() => {\n try {\n if (pinnedGroup) sessionStorage.setItem('anet-topo-pinned-group', pinnedGroup);\n else sessionStorage.removeItem('anet-topo-pinned-group');\n } catch {}\n }, [pinnedGroup]);\n useEffect(() => {\n if (!pinnedGroup) return;\n const known = new Set(Object.values(groupKeys));\n if (!known.has(pinnedGroup)) setPinnedGroup(null);\n }, [pinnedGroup, groupKeys]);\n // Round 49 / Loop: reverse-direction of R40's edge-on-node-hover linkage.\n // R48 widened the flow hitbox to 16 px, so edges are precise enough to\n // serve as a state trigger. When the user hovers a flow edge, light up its\n // two endpoint nodes and dim the rest — \"who is this edge between\" becomes\n // visible without reading the tooltip. The set is the link's two aliases\n // (null when no edge hovered); node opacity composes this after inFocus.\n const [hoveredEdgeKey, setHoveredEdgeKey] = useState<string | null>(null);\n // Round 116 / Loop: sticky variant of hoveredEdgeKey. R56 made the\n // recent-signal rows brighten the matching edge on hover, but the\n // filter released on mouseleave — no way to lock a flow for a\n // closer look. Click-to-pin closes that gap, matching the\n // established hover→pin idiom (R60 status, R61 legend, R63 group,\n // R88 vendor). activeEdgeKey = hoveredEdgeKey ?? pinnedEdgeKey\n // below so hover still wins for spot-comparison while a pin is set.\n const [pinnedEdgeKey, setPinnedEdgeKey] = useState<string | null>(null);\n const activeEdgeKey = hoveredEdgeKey ?? pinnedEdgeKey;\n // Round 77 / Loop: hovering the \"N active links\" header chip globally\n // brightens every flow edge (1.5× opacity). Transient affordance — the\n // chip already says HOW MANY active flows exist; this answers WHERE\n // they are in one glance, without the user scanning the canvas for\n // moving particles. Reset on mouse leave.\n const [hoveredActiveLinks, setHoveredActiveLinks] = useState(false);\n // Round 115 / Loop: hub-center hover state. R52 made the hub\n // clickable (fit view) + cursor:pointer, but there was no\n // hover-time visual feedback — the click target felt guessed at\n // rather than confirmed. This state drives a subtle hint ring on\n // hover so users see the affordance before committing the click.\n const [hoveredHub, setHoveredHub] = useState(false);\n // R133: hover state for the recent-signal panel's \"+N more flows\"\n // navigation footer. Drives the on-hover opacity boost + underline\n // that signals interactivity, mirroring the hoveredHub idiom above.\n const [hoveredRecentMore, setHoveredRecentMore] = useState(false);\n // Round 346 / Loop: minimap-container hover affordance. The minimap\n // is a click-target (role=button at line ~7810, recenter-on-click +\n // Enter→resetView) but pre-R346 nothing visually changed on hover —\n // the only hint was the `cursor: crosshair` style. R346 lifts the\n // viewport rect (strokeWidth 1.5 → 1.75 + opacity 0.9 → 1.0) when\n // the user enters the minimap, marking \"this is the recenter target\n // and it's alive\". Sibling polish to the R332 minimap rounded-md →\n // rounded-lg corner family — that round refined geometry, this one\n // gives the viewport indicator inside the geometry a hover state.\n // 280ms ease-out transition list matches R199 smoothView vocabulary\n // so the visual joins the existing rhythm on the same rect.\n const [hoveredMinimap, setHoveredMinimap] = useState(false);\n // Round 347 / Loop: zoom-level readout hover-state letter-spacing\n // tween (0 → 0.5 px). The readout sandwiched between zoom-out /\n // zoom-in is a passive percent display — pre-R347 it had no hover\n // feedback at all (only a `title` tooltip). R347 extends the R344\n // (`+N more flows` footer) + R345 (panel titles) hover-letter-\n // spacing family from panel/footer surfaces into the HTML chrome\n // strip. Hovering the readout spreads its digits 0.5 px, signalling\n // \"this is alive\". tabular-nums + minWidth: 46 from R225 still lock\n // the column so the tween doesn't shove neighbouring controls.\n // 200ms ease-out joins the existing R264 color/border transition\n // list on the same span.\n const [hoveredZoomLevel, setHoveredZoomLevel] = useState(false);\n // Round 350 / Loop: reset-button icon hover-rotate preview of the\n // R184 click-spin. Pre-R350 hovering the reset button only changed\n // the button bg (white/5); the icon inside stayed perfectly still.\n // R350 nudges the icon -8° on hover — a tactile hint that this\n // button rotates the icon on click. When the click fires, the\n // R184 anet-reset-spin keyframe animation overrides the hover\n // transform for its 450 ms run (CSS animations win over transitions\n // on the same property); when the animation ends + React removes\n // the className, the inline transform eases back to whatever the\n // hover state says — either -8° (still hovering) or 0 (mouse left).\n // 350th-round milestone polish.\n const [hoveredReset, setHoveredReset] = useState(false);\n // R135: panel-wide hover-elevation. The recent-signal + legend\n // panels both already host clickable rows (R56/R116 recent rows,\n // R55/R61 legend rows) and a clickable footer (R133), so the\n // chrome itself is interactive territory. Drop-shadow boost on\n // mouseenter says \"this whole panel is alive\" — matches the R18\n // KPI-card-hover idiom from the Overview page. Single state for\n // both panels since they don't overlap; null when neither hovered.\n const [hoveredPanel, setHoveredPanel] = useState<'recent' | 'legend' | null>(null);\n // Round 80 / Loop: vendor-letter hover in the distribution chip. The\n // chip already names vendor mix (`C:5 G:3 ?:1`); hover a letter and\n // every node from OTHER vendors dims. Surfaces the breakdown spatially\n // without inventing a new pin slot. Stores the vendor `initial`\n // (single char or \"?\") that matches the tally key.\n const [hoveredVendor, setHoveredVendor] = useState<string | null>(null);\n // Round 88 / Loop: vendor filter pin. R80 added hover-to-dim on the\n // vendor letters but the filter released as soon as the cursor left\n // — vendor was the only filter dimension without a sticky variant\n // (R60 status, R63 group, R69 Cmd+K all support pin). This state\n // closes the gap. Same pattern as pinnedStatus / pinnedGroup:\n // per-tab sessionStorage persistence, Esc clears, activeVendor =\n // hoveredVendor ?? pinnedVendor so hover still wins for spot-\n // comparison while a pin is active.\n const [pinnedVendor, setPinnedVendor] = useState<string | null>(() => {\n if (typeof window === 'undefined') return null;\n try { return sessionStorage.getItem('anet-topo-pinned-vendor'); } catch { return null; }\n });\n const activeVendor = hoveredVendor ?? pinnedVendor;\n useEffect(() => {\n try {\n if (pinnedVendor) sessionStorage.setItem('anet-topo-pinned-vendor', pinnedVendor);\n else sessionStorage.removeItem('anet-topo-pinned-vendor');\n } catch {}\n }, [pinnedVendor]);\n // R89: stale-purge. If the fleet's vendor distribution changes such\n // that a previously-pinned vendor is gone (last node of that\n // vendor disconnected), clear the pin so the chip row doesn't\n // show \"filter: A · 0\" forever. Matches the same defensive purge\n // pattern pinnedGroup uses higher up — sessionStorage survives\n // reloads, but a stored value that no longer matches reality\n // would paint with an impossible filter.\n useEffect(() => {\n if (pinnedVendor && !vendorDist.some(v => v.initial === pinnedVendor)) {\n setPinnedVendor(null);\n }\n }, [pinnedVendor, vendorDist]);\n // Round 55 / Loop: hovering a legend status row dims nodes whose status\n // doesn't match. The legend was passive — \"what does this colour mean\".\n // Now it answers \"show me all of these\" the same way R8 group-focus\n // answers \"show me this team\". Three values match the legend rows.\n const [hoveredStatus, setHoveredStatus] = useState<'working' | 'idle' | 'offline' | null>(null);\n // Round 60 / Loop: sticky variant of `hoveredStatus`. R55 only filters\n // while the user is actively hovering the legend; for sweeping a fleet\n // you want to LOCK the filter. Each segment of the R31 pressure bar\n // toggles a pin — click again on the same segment to release. The node\n // opacity formula reads `activeStatus = hoveredStatus ?? pinnedStatus`\n // so hover transiently overrides a pin (handy for spot-comparison)\n // without nuking it.\n // Round 66 / Loop: pin survives a page reload via sessionStorage —\n // per-tab not per-browser (a new tab starts clean, intentionally).\n // The init reader validates against the known status set so a stale\n // / corrupt value can't paint the canvas with an impossible filter.\n const [pinnedStatus, setPinnedStatus] = useState<'working' | 'idle' | 'offline' | null>(() => {\n if (typeof window === 'undefined') return null;\n try {\n const v = sessionStorage.getItem('anet-topo-pinned-status');\n return (v === 'working' || v === 'idle' || v === 'offline') ? v : null;\n } catch { return null; }\n });\n const activeStatus = hoveredStatus ?? pinnedStatus;\n // R66: sync pinnedStatus into sessionStorage when it changes. Paired\n // with the matching effect for pinnedGroup higher up.\n useEffect(() => {\n try {\n if (pinnedStatus) sessionStorage.setItem('anet-topo-pinned-status', pinnedStatus);\n else sessionStorage.removeItem('anet-topo-pinned-status');\n } catch {}\n }, [pinnedStatus]);\n // R69: listen for Cmd+K palette pin actions. The palette can't reach\n // this component's state directly so it dispatches a CustomEvent;\n // we react by setting pinnedStatus / pinnedGroup. The palette also\n // writes to sessionStorage in lockstep, so the R66 init readers\n // would pick it up on reload — the event handler is what keeps the\n // currently-mounted canvas in sync without one.\n useEffect(() => {\n const onPin = (e: Event) => {\n const detail = (e as CustomEvent).detail || {};\n if (detail.kind === 'clear') {\n // R90: extend the universal clear to vendor. R69 only cleared\n // status + group because pinnedVendor didn't exist yet; R88\n // shipped the third pin and R90 closes the palette gap so\n // \"Clear topology filters\" really means all of them.\n // R117: extend again to pinnedEdgeKey — R116 added the 4th\n // pin dim and this command must clear it too or the edge stays\n // bright in the canvas while every other pill clears.\n setPinnedStatus(null);\n setPinnedGroup(null);\n setPinnedVendor(null);\n setPinnedEdgeKey(null);\n } else if (detail.kind === 'clear-vendor') {\n // R90: granular vendor-only clear so power users can release\n // just the vendor without disturbing status/group pins.\n setPinnedVendor(null);\n } else if (detail.kind === 'clear-edge') {\n // R117: granular edge-only clear, mirror of R90 clear-vendor.\n setPinnedEdgeKey(null);\n } else if (detail.kind === 'status') {\n const v = detail.value;\n if (v === 'working' || v === 'idle' || v === 'offline') setPinnedStatus(v);\n } else if (detail.kind === 'group' && typeof detail.value === 'string') {\n setPinnedGroup(detail.value);\n } else if (detail.kind === 'vendor' && typeof detail.value === 'string') {\n // R108: palette pin-vendor support. R69 added pin-status, R88\n // added clickable vendor letters in the chip row, R90 added\n // granular clear-vendor — but the palette never gained a\n // PIN-vendor command. This branch listens for it; the matching\n // commands live in CommandPalette R108. The stale-purge\n // useEffect higher up already drops a pin whose vendor isn't\n // in the current distribution, so commands for an absent\n // vendor are harmless.\n setPinnedVendor(detail.value);\n }\n };\n window.addEventListener('anet:topo-pin', onPin);\n return () => window.removeEventListener('anet:topo-pin', onPin);\n }, []);\n // R74 listener for layout + view palette commands lives below\n // fitView's declaration — see further down in the file.\n const hoveredEdgeEndpoints = useMemo<Set<string> | null>(() => {\n // R116: compose hover ?? pin so pinning a row via click keeps the\n // endpoint ring + edge ladder lit after mouseleave.\n if (!activeEdgeKey) return null;\n const link = flowLinks.find(l => l.key === activeEdgeKey);\n return link ? new Set([link.from, link.to]) : null;\n }, [activeEdgeKey, flowLinks]);\n\n // --- Round 103 (issue #81): fullscreen + zoom + pan interaction layer ---\n // DIY native (no d3 / svg-pan-zoom): wrap the topology content in a single\n // <g transform> and drive it with wheel + pointer-drag. The panel <rect>\n // backdrop stays fixed so panning never reveals empty canvas. View state\n // {zoom,x,y} persists to localStorage (same sticky pattern as brand flag).\n const VIEWBOX_W = 1000;\n const VIEWBOX_H = 680;\n const ZOOM_MIN = 0.5;\n const ZOOM_MAX = 4;\n const containerRef = useRef<HTMLDivElement>(null);\n const svgRef = useRef<SVGSVGElement>(null);\n const [view, setView] = useState({ zoom: 1, x: 0, y: 0 });\n const viewRef = useRef(view);\n const dragRef = useRef({ active: false, startX: 0, startY: 0, baseX: 0, baseY: 0 });\n const [isFullscreen, setIsFullscreen] = useState(false);\n // Round 184 / Loop: chrome reset button gets a one-shot icon spin\n // on click. resetSpinning is armed for 450ms (matches the CSS\n // animation duration in globals.css `.anet-reset-spin`); the SVG\n // icon picks up the class while armed. Same arming pattern as\n // R168/R169/R170/R171 smoothView flags but driving a CSS animation\n // instead of a CSS transition. prefers-reduced-motion blanket\n // override in R29 globals.css neutralises animation-duration\n // universally so the spin completes in 1ms when reduced motion is\n // requested.\n const [resetSpinning, setResetSpinning] = useState(false);\n const armResetSpin = () => {\n setResetSpinning(true);\n setTimeout(() => setResetSpinning(false), 460);\n };\n // Round 186 / Loop: chrome zoom-in / zoom-out buttons get a brief\n // icon pop on click — same click-feel idiom R184 added for the\n // reset spin, but a scale pulse rather than a rotation since +/−\n // icons rotating wouldn't read semantically. Tracks which button\n // is currently popping so only that icon picks up the class.\n // Arms for 240ms (CSS animation 220ms + 20ms buffer) so a quick\n // re-click can replay cleanly.\n // Round 249 / Loop: extend chromePopping to cover ring/grid layout\n // toggle + fullscreen button alongside the existing zoom-in/zoom-out.\n // Pre-R249 only the zoom buttons fired the R186 .anet-chrome-pop scale\n // pulse on click; the other chrome controls (layout toggle, fullscreen)\n // had no transient \"I just clicked\" signal — silent click → state change\n // with only the post-click visual difference to confirm action. R249\n // gives every clickable chrome control the same 220ms scale pulse, so\n // the whole strip speaks one consistent click vocabulary. Reset button\n // keeps its own R184 rotation animation (different gesture, semantic).\n // Node-size S/M/L keep their R171 layoutSwitching crossfade (already\n // gestural). Type union grows but the helper signature stays one-arg.\n type ChromePop = 'zoom-in' | 'zoom-out' | 'layout-ring' | 'layout-grid' | 'fullscreen' | 'size-S' | 'size-M' | 'size-L';\n const [chromePopping, setChromePopping] = useState<ChromePop | null>(null);\n const popChrome = (which: ChromePop) => {\n setChromePopping(which);\n setTimeout(() => setChromePopping(prev => prev === which ? null : prev), 240);\n };\n // Issue #100: singleton chat popover. One alias at a time — clicking\n // another node swaps the target and the conversation switches in place.\n const [chatAlias, setChatAlias] = useState<string | null>(null);\n // Round 14 / Loop: one-shot click ripple — fades outward from the clicked\n // node so the click registers physically before the popover snaps in.\n // Pairs with the Round 11 chat-focus ring: ripple expands, ring locks on.\n // Keyed by ts so re-clicking the same node remounts the <circle> and the\n // SMIL <animate> replays. Cleared 600ms after click (longer than the\n // 500ms animation so a final frame at opacity 0 is still in the tree).\n const [clickRipple, setClickRipple] = useState<{\n ts: number; x: number; y: number; r0: number; color: string;\n } | null>(null);\n // #84: when a node is renamed while its chat popover is open, follow the\n // rename so the conversation keeps targeting a live alias.\n useEffect(() => {\n if (renameSignal && renameSignal.from === chatAlias) {\n setChatAlias(renameSignal.to);\n }\n // chatAlias intentionally omitted — only react to a new rename signal,\n // not to the user opening/closing the popover.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [renameSignal]);\n\n useEffect(() => { viewRef.current = view; }, [view]);\n\n // restore persisted view once on mount\n useEffect(() => {\n try {\n const raw = localStorage.getItem('anet-topo-view');\n if (raw) {\n const v = JSON.parse(raw);\n if (typeof v?.zoom === 'number') {\n setView({\n zoom: Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, v.zoom)),\n x: typeof v.x === 'number' ? v.x : 0,\n y: typeof v.y === 'number' ? v.y : 0,\n });\n }\n }\n } catch {}\n }, []);\n\n // Round 28 / Loop: first-paint auto-fit. Round 27's cellH bump means\n // dense grids (≥6-7 rows) overflow the 680-px viewBox at the natural\n // 100% zoom — a new user sees the topology with its bottom rows\n // clipped and no hint that there's more below. Auto-fit on first\n // paint (and only if the user has no persisted view) sets the\n // initial zoom so all content fits. Subsequent zoom changes persist\n // and override the auto-fit on reload; explicit reset (0 key) still\n // goes back to 100% so the gesture's \"reset to natural size\" semantic\n // is preserved.\n //\n // Capture pre-mount persistence in a useState initializer — the\n // existing `persist` effect (declared below) runs on first render\n // with the default {1,0,0} view and writes it to localStorage before\n // the auto-fit effect (deps on async-arriving sessions) gets a turn.\n // Reading localStorage from the effect would see that write and\n // skip the fit. The useState snapshot fires once, before any effects.\n const [hadPersistedViewOnMount] = useState<boolean>(\n () => typeof window !== 'undefined' && !!localStorage.getItem('anet-topo-view'),\n );\n const autoFitDoneRef = useRef(false);\n useEffect(() => {\n if (autoFitDoneRef.current) return;\n if (hadPersistedViewOnMount) {\n autoFitDoneRef.current = true;\n return;\n }\n if (layout !== 'grid' || sessions.length === 0 || !gridContentBottom) return;\n if (gridContentBottom <= VIEWBOX_H) {\n autoFitDoneRef.current = true; // no overflow → no fit needed\n return;\n }\n const fitZoom = Math.max(ZOOM_MIN, Math.min(1, VIEWBOX_H / gridContentBottom));\n setView({ zoom: fitZoom, x: 0, y: 0 });\n autoFitDoneRef.current = true;\n }, [layout, sessions.length, gridContentBottom, hadPersistedViewOnMount]);\n\n // persist view\n useEffect(() => {\n try { localStorage.setItem('anet-topo-view', JSON.stringify(view)); } catch {}\n }, [view]);\n\n // track fullscreen state (button label + Esc-exit sync)\n useEffect(() => {\n const onFsChange = () => setIsFullscreen(document.fullscreenElement === containerRef.current);\n document.addEventListener('fullscreenchange', onFsChange);\n return () => document.removeEventListener('fullscreenchange', onFsChange);\n }, []);\n\n // wheel zoom — native non-passive listener so preventDefault() actually\n // stops the page from scrolling. Uses functional setState so the listener\n // never goes stale and can attach once.\n useEffect(() => {\n const svg = svgRef.current;\n if (!svg) return;\n const onWheel = (e: WheelEvent) => {\n // Round 23 / Loop: only zoom on Ctrl/Meta+wheel when inline; plain\n // wheel over the canvas keeps scrolling the page (the topo sits at\n // the top of /; trapping page scroll mid-page is the classic\n // \"scroll-jail\" anti-pattern). In fullscreen the canvas owns the\n // viewport so any wheel zooms — no page scroll to preserve. Pinch-\n // zoom on a trackpad surfaces as ctrlKey=true natively, which is\n // also what we want (zoom the canvas, not the browser).\n if (!isFullscreen && !e.ctrlKey && !e.metaKey) return;\n e.preventDefault();\n const rect = svg.getBoundingClientRect();\n const mx = ((e.clientX - rect.left) / rect.width) * VIEWBOX_W;\n const my = ((e.clientY - rect.top) / rect.height) * VIEWBOX_H;\n setView(prev => {\n // Round 104 (issue #81 follow-up): Vincent 实测 zoom 太灵敏. The\n // old factor was a fixed 1.15x per wheel *event* — fine for a\n // mouse notch but a trackpad fires dozens of events per gesture\n // so it compounded into huge jumps. Scale the factor by deltaY\n // magnitude with a small coefficient, then clamp the per-event\n // change so no single tick moves more than ~8%.\n const factor = Math.min(1.08, Math.max(0.926, Math.exp(-e.deltaY * 0.0006)));\n const nz = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, prev.zoom * factor));\n const ratio = nz / prev.zoom;\n // keep the point under the cursor stationary\n return { zoom: nz, x: mx - (mx - prev.x) * ratio, y: my - (my - prev.y) * ratio };\n });\n };\n svg.addEventListener('wheel', onWheel, { passive: false });\n return () => svg.removeEventListener('wheel', onWheel);\n // Re-attach when the fullscreen flag toggles so the handler's\n // closure picks up the new value (capture-by-closure means we'd\n // otherwise read the stale boolean forever).\n }, [isFullscreen]);\n\n // Round 21 / Loop: pan-aware cursor. Static `grab` gave no tactile cue\n // that the canvas was actually being dragged — \"grabbing\" while\n // dragRef.current.active = true tells the hand it's moving the\n // viewport. Mirroring dragRef.active into state is the only way to\n // re-render the SVG style; the ref itself doesn't trigger renders.\n const [isPanning, setIsPanning] = useState(false);\n const onPointerDown = (e: React.PointerEvent<SVGSVGElement>) => {\n if (e.button !== 0) return;\n (e.currentTarget as Element).setPointerCapture?.(e.pointerId);\n dragRef.current = {\n active: true,\n startX: e.clientX,\n startY: e.clientY,\n baseX: viewRef.current.x,\n baseY: viewRef.current.y,\n };\n setIsPanning(true);\n };\n const onPointerMove = (e: React.PointerEvent<SVGSVGElement>) => {\n const d = dragRef.current;\n if (!d.active) return;\n const svg = svgRef.current;\n if (!svg) return;\n const rect = svg.getBoundingClientRect();\n const dx = ((e.clientX - d.startX) / rect.width) * VIEWBOX_W;\n const dy = ((e.clientY - d.startY) / rect.height) * VIEWBOX_H;\n setView(prev => ({ ...prev, x: d.baseX + dx, y: d.baseY + dy }));\n };\n const onPointerUp = (e: React.PointerEvent<SVGSVGElement>) => {\n if (!dragRef.current.active) return;\n dragRef.current.active = false;\n setIsPanning(false);\n try {\n (e.currentTarget as Element).releasePointerCapture?.(e.pointerId);\n } catch {}\n };\n\n // zoom buttons — zoom around the canvas center\n const zoomBy = (factor: number) => {\n setView(prev => {\n const nz = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, prev.zoom * factor));\n const ratio = nz / prev.zoom;\n const cx0 = VIEWBOX_W / 2;\n const cy0 = VIEWBOX_H / 2;\n return { zoom: nz, x: cx0 - (cx0 - prev.x) * ratio, y: cy0 - (cy0 - prev.y) * ratio };\n });\n };\n // Round 169 / Loop: discrete-zoom wrapper that arms the R168\n // smoothView flag before invoking zoomBy. Keyboard + / − and\n // the chrome zoom buttons fire once per gesture, so each\n // 1.2× step deserves the same 300ms glide R168 gives\n // reset/fit. Wheel zoom keeps calling zoomBy() directly —\n // every tick should respond live without lag. The arming\n // path is identical to resetView/fitView so all four\n // discrete-zoom surfaces share one timing constant.\n const zoomByDiscrete = (factor: number) => {\n setSmoothView(true);\n setTimeout(() => setSmoothView(false), 350);\n zoomBy(factor);\n };\n // Round 168 / Loop: smoothView is a one-shot flag that arms a\n // CSS transition on the viewport <g> transform attribute. Set\n // true when resetView/fitView fires; the inline style on the\n // viewport <g> reads this flag and conditionally applies\n // `transition: transform 300ms ease-out`. Auto-clears after\n // 350ms (just past the transition end) via setTimeout so\n // subsequent pan/wheel zoom stays snappy with no lag. Keeping\n // it as state (vs ref) so the React rerender re-applies the\n // style attribute synchronously when the transform also\n // changes — both attribute mutation and transition class\n // arrive in the same paint frame, which is what triggers the\n // browser to animate between the old and new transform values.\n const [smoothView, setSmoothView] = useState(false);\n const armSmoothView = () => {\n setSmoothView(true);\n setTimeout(() => setSmoothView(false), 350);\n };\n const resetView = () => {\n armSmoothView();\n setView({ zoom: 1, x: 0, y: 0 });\n };\n\n // Round 29 / Loop: `f` = fit-to-content. Shared by the Round 28\n // first-paint auto-fit effect and the keyboard handler so the math is\n // in one place. When content already fits at natural zoom, this is\n // effectively a \"recenter\" — `f` always lands on a known good view.\n // R168: arm smoothView so the transition glides instead of snapping\n // when the operator invokes fit-to-content via the hub click (R52),\n // chrome button, `f` key, or palette command.\n const fitView = useCallback(() => {\n const zoom = !gridContentBottom || gridContentBottom <= VIEWBOX_H\n ? 1\n : Math.max(ZOOM_MIN, Math.min(1, VIEWBOX_H / gridContentBottom));\n setSmoothView(true);\n setTimeout(() => setSmoothView(false), 350);\n setView({ zoom, x: 0, y: 0 });\n }, [gridContentBottom]);\n\n // R74: listen for layout + view palette commands. Sister to R69's\n // pin listener — palette dispatches a CustomEvent, the reducer here\n // calls toggleLayout / fitView. Sits below fitView's declaration so\n // the deps list resolves cleanly.\n useEffect(() => {\n const onLayout = (e: Event) => {\n const detail = (e as CustomEvent).detail || {};\n if (detail.kind === 'toggle') toggleLayout();\n };\n const onView = (e: Event) => {\n const detail = (e as CustomEvent).detail || {};\n if (detail.kind === 'fit') fitView();\n };\n window.addEventListener('anet:topo-layout', onLayout);\n window.addEventListener('anet:topo-view', onView);\n return () => {\n window.removeEventListener('anet:topo-layout', onLayout);\n window.removeEventListener('anet:topo-view', onView);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [fitView]);\n\n // Round 22 / Loop: keyboard zoom — +/= zoom in, - zoom out, 0 reset.\n // Round 29 / Loop: +f to fit content.\n // Listen on window so the user doesn't need to focus the SVG first,\n // but only act when no text input has focus (otherwise typing \"-\" in\n // a chat would zoom the topology — surprising and bad). Modifier keys\n // pass through so Cmd/Ctrl combos still hit their owners. The button\n // titles below document these shortcuts so users discover them.\n useEffect(() => {\n const onKey = (e: KeyboardEvent) => {\n if (e.ctrlKey || e.metaKey || e.altKey) return;\n const ae = document.activeElement as HTMLElement | null;\n if (ae) {\n const tag = ae.tagName;\n if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || ae.isContentEditable) return;\n }\n if (e.key === '+' || e.key === '=') { zoomByDiscrete(1.2); e.preventDefault(); }\n else if (e.key === '-' || e.key === '_') { zoomByDiscrete(1 / 1.2); e.preventDefault(); }\n else if (e.key === '0') { resetView(); e.preventDefault(); }\n else if (e.key === 'f' || e.key === 'F') { fitView(); e.preventDefault(); }\n // Round 32 / Loop: `l` toggles ring|grid. The vim-style `g l` route\n // (Audit Log) requires a preceding `g` within 1500ms; a bare `l`\n // outside that window is free for topology use.\n else if (e.key === 'l' || e.key === 'L') { toggleLayout(); e.preventDefault(); }\n // Round 62 / Loop: Esc clears the R60/R61 pinned status filter so\n // users have a universal-cancel keyboard out. Esc on an open chat\n // is owned by ChatPopover (which only mounts when chatAlias is\n // set), so this handler is effectively scoped to \"no chat open\".\n // We additionally guard on chatAlias to be explicit — if the chat\n // closed mid-cycle, the pin can still be cleared on the next Esc.\n // R63: extends to pinnedGroup too. Clears WHATEVER pin is active\n // (one Esc collapses all topology pins) so the keyboard escape\n // route stays a single key, not \"Esc maybe Esc again\".\n else if (e.key === 'Escape' && !chatAlias && (pinnedStatus || pinnedGroup || pinnedVendor || pinnedEdgeKey)) {\n // R88/R116: extend the universal-cancel to vendor + edge too.\n // One Esc collapses every topology pin (matches R62/R63's\n // \"single key, not Esc-maybe-Esc-again\" promise).\n if (pinnedStatus) setPinnedStatus(null);\n if (pinnedGroup) setPinnedGroup(null);\n if (pinnedVendor) setPinnedVendor(null);\n if (pinnedEdgeKey) setPinnedEdgeKey(null);\n e.preventDefault();\n }\n };\n window.addEventListener('keydown', onKey);\n return () => window.removeEventListener('keydown', onKey);\n // zoomBy / resetView are stable wrt setView callback; fitView changes\n // with gridContentBottom so deps list catches it. R62 adds chatAlias\n // and pinnedStatus so the Escape branch reads fresh state (re-binding\n // the listener on these state changes is sub-ms — cheaper than refs).\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [fitView, chatAlias, pinnedStatus, pinnedGroup, pinnedVendor, pinnedEdgeKey]);\n\n const toggleFullscreen = () => {\n const el = containerRef.current;\n if (!el) return;\n if (document.fullscreenElement) {\n document.exitFullscreen?.();\n } else {\n const req =\n el.requestFullscreen ||\n (el as unknown as { webkitRequestFullscreen?: () => void }).webkitRequestFullscreen;\n req?.call(el);\n }\n };\n\n return (\n <section className=\"w-full max-w-6xl mx-auto mb-8\">\n {/* Round 299 / Loop: title block bottom margin mb-3 (12px) →\n mb-4 (16px). After R298 tightened the title-block internal\n gap (12→10px) packing brand-logo + kicker + h2 into a more\n cohesive editorial unit, the outer bottom margin to the\n topology canvas should breathe more — denser title block\n + tighter follow-on space read as cramped. Bumping the\n gap below the title block lets the canvas frame\n itself more clearly as the main visual subject. 16px is\n the conventional SaaS-product section-header-to-content\n baseline (Stripe / Linear / Vercel marketing). Geometry:\n adds 4 CSS px between title block bottom and topology\n frame top — small but cumulative with the R298 internal\n tighten, the title block reads as a *deliberate* badge\n rather than a casually-stacked label. */}\n {/* Round 334 / Loop: header outer wrapper mobile gap-3 → gap-2.5\n (12 px → 10 px). The wrapper is `flex flex-col` on narrow\n viewports (title-block above, chip-row below) — at mobile\n its vertical gap was 12 px while the title-block internal\n (R298) and chip-row internal (R328) both rhythm at 10 px.\n R334 unifies the OUTER vertical gap to 10 px so mobile\n stacked layout matches the established gap-rhythm tier\n (title-block 10 / chip-row 10 / chrome 8 — R298/R328/R326).\n Desktop `sm:flex-row` is unaffected: in row mode the gap-3\n would have applied horizontally but the wrapper relies on\n `sm:justify-between` for left/right anchoring (gap is then\n decorative only between the two flex-grow groups). Net\n mobile bump: 2 px tighter vertical breathing between\n title-block + chip-row. Geometry-safe — topo-overlap-test\n reads SVG-internal bbox, not header layout. */}\n <div\n className={`flex flex-col gap-2.5 sm:flex-row sm:items-end sm:justify-between mb-4 px-1${isFullscreen ? ' hidden' : ''}`}\n data-topo-header-row\n data-topo-header-hidden={isFullscreen ? 'true' : 'false'}\n >\n {/* Round 267 / Loop: title block adopts leading-tight on both\n kicker and h2 for a tighter editorial-style rhythm. Pre-\n R267 the kicker used Tailwind's compound `text-xs` (line-\n height 16px = 1.33 ratio) and the h2 used `text-lg` (line-\n height 28px = 1.56) — adequate but loose for a kicker→\n title sequence. R267 applies `leading-tight` (1.25) to\n both, shrinking effective line-heights to 15px + 22.5px =\n 37.5px total title block height (vs 44px pre-R267 → ~15%\n more compact) while preserving the cap-top to descender\n visual proportions. Result: kicker and title read as a\n single typographic unit rather than two loosely-stacked\n lines. data-topo-section-kicker / data-topo-section-title\n attrs make both probe-able. */}\n {/* P0 (Vincent 5222 / 通信龙 R278 dispatch): integrate sleep2agi\n brand logo into the title-block.\n Why HTML title-block instead of SVG canvas: the SVG region\n has a high-frequency concurrent-editor edit race with codex\n (see project_dashboard_concurrent_editors). The title block\n is HTML-side, low edit traffic, and gives the brand mark\n first-glance presence above the topology canvas — the exact\n \"Twitter screenshot 一眼看出 sleep2agi\" outcome Vincent 5215\n asked for.\n Logo construction: inline SVG so currentColor inherits\n from the parent text-{color} class — theme-aware without\n an extra asset request. Same crescent geometry as\n public/sleep2agi-logo.svg. 36×36 px so it's clearly\n readable at a16:9 Twitter crop, paired with the kicker +\n h2 typography via flex layout. text-cyan-300 in cyber +\n text-emerald-600 in light keeps the moon brand-aligned\n with the canvas accent palette. */}\n {/* Round 298 / Loop: title-block gap-3 (12px) → gap-2.5 (10px).\n The R297 codex-bundle brand-logo at 36×36 paired with the\n R285 tracking-widest kicker + R286 tracking-tight h2 forms\n an editorial \"logo + title\" unit. At gap-3 the 12px between\n logo right-edge and text left-edge reads as two separate\n elements — visual spacing slightly outpaces the relationship\n density (logo IS the brand mark for the kicker's\n \"Network Topology\"). gap-2.5 (10px) is the tight-pack\n convention SaaS-product header logos use (Stripe / Vercel /\n Linear top-nav logo + product name spacing), grouping logo\n + title as one read. Geometry: 36 + 10 + ~120 (title text\n width) = 166px total title-block width vs 168px pre-R298 —\n no measurable layout shift, just a deliberate tighter\n grouping. */}\n <div className=\"flex items-center gap-2.5\">\n {/* Round 297 / Loop: brand-logo color picks up the 200ms ease-\n out transition. Pre-R297 the moon glyph had theme-\n conditional color (cyber #67e8f9 cyan ↔ light #0d9488\n teal) but no transition declaration — flipping themes\n made the brand mark snap to its new color in one frame,\n jarring against the surrounding R245/R246/R247/R253/R254\n family that smooths every neighbouring fill / stroke /\n filter at the same 200ms cadence. Adding the transition\n brings the brand mark into the coordinated theme-toggle\n choreography: title block + canvas + chrome all ease as\n one unit. CSS color transition is well supported on the\n `color` property (which currentColor inside the masked\n <rect> inherits), so no SMIL trick needed. */}\n {/* Round 316 / Loop: brand-logo width/height 36 → 40 for\n slightly stronger first-glance presence. After R298\n tightened the title-block flex gap (12→10px) and R299\n widened the bottom margin (12→16px), the logo can hold\n more visual weight to balance the kicker+h2 stack on\n its right. 40px is 11% larger by edge, ~23% by area —\n visible bump on a Twitter-screenshot crop without\n overpowering the h2 at text-lg/font-semibold (R286).\n viewBox 32×32 unchanged so the inner crescent geometry\n scales proportionally. */}\n <svg\n width=\"40\" height=\"40\" viewBox=\"0 0 32 32\" aria-hidden\n className=\"shrink-0\"\n data-topo-brand-logo\n style={{\n color: isLight ? '#0d9488' : '#67e8f9',\n transition: 'color 200ms ease-out',\n }}\n >\n <mask id=\"s2a-titleblock-moon-mask\">\n <rect width=\"32\" height=\"32\" fill=\"black\" />\n <circle cx=\"16\" cy=\"16\" r=\"13\" fill=\"white\" />\n <circle cx=\"20.5\" cy=\"14.5\" r=\"11\" fill=\"black\" />\n </mask>\n <rect width=\"32\" height=\"32\" fill=\"currentColor\" mask=\"url(#s2a-titleblock-moon-mask)\" />\n </svg>\n <div>\n {/* Round 285 / Loop: kicker tracking-wider → tracking-widest.\n An uppercase eyebrow label at text-xs benefits from\n wider letter-spacing — Tailwind's tracking-widest is\n 0.1em vs tracking-wider's 0.05em. At small caps,\n 0.1em is the conventional SaaS-eyebrow spacing (Stripe,\n Linear, Vercel marketing kicker style); 0.05em reads\n closer to body-text density. The widened spacing\n telegraphs \"this is a label, not a sentence\" without\n changing color or size, deepening the editorial\n hierarchy R267 set up between kicker and h2. */}\n {/* Round 296 / Loop: kicker text-gray-600 → text-gray-500\n for slightly better legibility on the dark cyber backdrop.\n gray-600 (#4b5563) read as a near-invisible label on\n cyber (the canvas + side rail are deeply dark); gray-500\n (#6b7280) lifts the eyebrow into the band where the eye\n registers it as a deliberate label vs swallowed text,\n while still sitting clearly below text-white h2 title\n in the visual hierarchy. Tailwind classes are theme-\n neutral so the bump applies to both themes; in light\n theme gray-500 is still appropriate as a muted-label\n shade on white bg. Hierarchy preserved: title-white >\n kicker-gray-500 (R285 tracking-widest still in place). */}\n {/* Round 300 / Loop (milestone): kicker picks up font-medium\n (500). Pre-R300 the eyebrow used default font-weight\n (400/normal). At text-xs (12px) + uppercase + R285\n tracking-widest, default-weight letters read slightly\n under-authored — uppercase at small sizes wants a touch\n more stroke weight to feel like a deliberate label.\n font-medium (500) is the conventional SaaS-eyebrow\n weight (Stripe / Vercel / Linear marketing kicker\n style — same family that informed R285's tracking-\n widest decision). Stays clearly below the h2's font-\n semibold (600) + larger size so hierarchy is preserved:\n h2 (text-lg/600) > kicker (text-xs/500/gray-500).\n R300 marks the milestone of 25 rounds (R275-R300) of\n continuous TopoGraph polish + codex's Vincent 5215/\n 5222 logo asset+integration work. */}\n <div className=\"text-xs uppercase text-gray-500 tracking-widest leading-tight font-medium\" data-topo-section-kicker>Network Topology</div>\n {/* Round 286 / Loop: title 'Command mesh' adopts tracking-tight\n (-0.025em) to complement R285 kicker tracking-widest. Wide\n eyebrow + tight headline is the conventional editorial\n pairing — Apple / Stripe / Vercel / Linear all use this\n dual-axis typographic rhythm. The kicker's 0.1em pushes\n letters APART (label feel); the headline's -0.025em pulls\n them TOGETHER (deliberate, designed-headline feel). At\n text-lg (18px) the shift is ~0.45px per gap — small but\n cumulatively legible across 12 characters. font-semibold\n (600) stays — tracking-tight does the heavy lifting for\n the editorial register. */}\n <h2 className=\"text-lg text-white font-semibold leading-tight tracking-tight\" data-topo-section-title>Command mesh</h2>\n </div>\n </div>\n {/* Round 328 / Loop: chip-row strip wrapper gap 2 → 2.5\n (8px → 10px between chips). Pre-R328 the inter-chip gap\n sat at 8px while each chip's own horizontal padding was\n `px-2.5` (10px) — the chip's internal space was wider\n than the gap between chips, so adjacent chips read as\n \"touching\" rather than \"neighboring\". Bumping the gap\n to 10px makes inter-chip = chip-padding, visually\n balancing the rhythm. Sibling treatment to R298 title-\n block `gap-2.5` (brand-logo ↔ kicker/title) and R326\n chrome strip `gap-2` extension family. Layout strip:\n R298 title-block gap-2.5 (top of canvas)\n R328 chip-row gap-2.5 (below title) ← NEW\n R326 chrome gap-2 (bottom of canvas)\n Risk-bounded: chip-row uses `flex-wrap`; if it wraps to\n a new line on narrow viewports the row-gap also bumps to\n 10px, which only helps mobile rhythm. Topo-overlap-test\n is HTML-overlay-only at this scope; SVG viewBox layout\n untouched. */}\n <div className=\"flex flex-wrap items-center gap-2.5 text-xs\">\n {/* Issue #87: ring | grid layout toggle — segmented control,\n persisted to localStorage anet-topo-layout.\n Round 163 / Loop: bring the layout toggle into the R154\n chrome-button focus convention. Pre-R163 the buttons had\n aria-pressed + transition-colors but no focus-visible ring\n (browser default — invisible against dark canvas) and the\n inactive variant only nudged text from gray-500 → gray-400\n on hover with no bg tint, so the hover-to-click affordance\n was barely perceptible.\n\n R163 closes both gaps to match R154:\n focus-visible:ring-2 focus-visible:ring-cyan-400/60\n → keyboard users see exactly which segment is focused\n hover:bg-cyan-500/5 (inactive)\n → mouse hover shows a faint cyan ghost of the active\n state, signalling 'click to switch'\n hover:bg-cyan-500/20 (active)\n → active button responds too, says 'still clickable'\n data-topo-chrome-layout for testability symmetric with the\n R154 chrome buttons (data-topo-chrome-zoom-in / -reset /\n -fullscreen etc).\n\n Round 260 / Loop: chip-row semantic gap — Layout toggle is\n the only CONTROL in the chip row; everything that follows\n (working / online / pressure / vendor letters / active-\n links / filter pills / freshness) is READ-ONLY display.\n Pre-R260 all 8 children sat at uniform gap-2 (8px) — the\n spatial signal read as \"8 separate things\" instead of\n \"1 control + 7 display\". mr-1 (4px) on the Layout toggle\n stacks on top of the parent flex's gap-2 (8px) for an\n effective 12px gap before the first status chip — same\n law-of-proximity pattern R255 applied to the bottom-right\n chrome strip (fleet vs view groups). data-topo-chrome-\n layout-trailer marks the boundary surface for the gap\n probe. */}\n {/* Round 268 / Loop: Layout toggle border unified with the\n chrome strip's theme-aware borderColor. Pre-R268 the\n wrapper + Grid button's internal divider used hardcoded\n `border-gray-500/25` (pale gray, fixed in both themes)\n while the bottom-right chrome strip (nodeSize, zoom)\n used pal.containerBorder (cyber #2a2a4a dark indigo ↔\n light #e3e6eb pale gray). Visible mismatch in cyber\n theme: Layout toggle border read as pale gray while\n chrome strip borders read as darker indigo — two\n different border colors on visually-analogous\n segmented controls. R268 replaces the hardcoded class\n with inline pal.containerBorder + a border-color\n transition, so the Layout toggle (a) matches the chrome\n strip border color and (b) joins the canvas-wide\n theme-ease vocabulary (eases on cyber↔light toggle\n instead of snapping). Same change applied to the Grid\n button's border-l on line ~1493. */}\n {/* Round 329 / Loop: Layout toggle wrapper `mr-1` → `mr-0.5`\n to compensate for R328's chip-row gap bump (8 → 10 px).\n R260 designed for an effective 12 px gap between the\n Layout CONTROL and the first DISPLAY chip (working /\n online / etc): mr-1 (4 px) + chip-row gap-2 (8 px) = 12.\n R328 widened chip-row to gap-2.5 (10 px), pushing the\n effective gap to 14 px — semantically still \"control\n vs display\" but louder than R260 specified.\n R329 dials mr-1 → mr-0.5 (2 px) so the effective gap\n returns to 12 px (mr-0.5 + chip-row gap-2.5 = 2 + 10).\n Keeps the law-of-proximity semantic R260 designed\n while honoring R328's wider baseline rhythm. data-topo-\n chrome-layout-trailer attr unchanged — it still marks\n the boundary surface for the gap probe. */}\n {/* Round 375 / Loop: Layout-toggle wrapper rounded-md → rounded-\n lg (6 → 8 px). Extends the corner-radius cascade family\n to the chrome-strip layout-toggle wrapper:\n R330 canvas wrapper rounded-xl 12 px\n R331 SVG panels rx=10 10 px\n R332 minimap container rounded-lg 8 px\n R375 Layout-toggle wrapper rounded-lg 8 px (this round)\n Pre-R375 the wrapper at rounded-md (6 px) was the only\n chrome-strip container still using the smaller corner\n radius — both R330 outer wrapper and R332 minimap sit at\n ≥ 8 px, so the Layout toggle's 6 px stood out as a\n tighter corner against the family. R375 brings it into\n the rounded-lg tier where the minimap already lives.\n Pure paint change — overflow-hidden still clips the\n inner buttons' bg-cyan-500/15 tints; no layout shift.\n R268 border-color + 200ms transition + R329 mr-0.5 +\n data-topo-chrome-layout-trailer all preserved. */}\n <div\n className=\"mr-0.5 inline-flex rounded-lg border overflow-hidden\"\n style={{ borderColor: pal.containerBorder, transition: 'border-color 200ms ease-out' }}\n role=\"group\"\n aria-label=\"Topology layout\"\n data-topo-chrome-layout-trailer\n data-topo-chrome-layout-radius=\"rounded-lg\"\n >\n <button\n onClick={() => { popChrome('layout-ring'); if (layout !== 'ring') toggleLayout(); }}\n aria-pressed={layout === 'ring'}\n title=\"Ring layout (l to toggle)\"\n data-topo-chrome-layout=\"ring\"\n data-topo-chrome-layout-active={layout === 'ring' ? 'true' : 'false'}\n data-topo-chrome-layout-ring-popping={chromePopping === 'layout-ring' ? 'true' : 'false'}\n // Round 196 / Loop: add active: (pressed) state for tactile\n // click feedback — bridges mouse-down → R186/R184/R192 pop-on-\n // release. Selected variant deepens to cyan-500/25 (one tier\n // above its hover:cyan-500/20); unselected variant deepens\n // to cyan-500/15 (one tier above its hover:cyan-500/5).\n // Round 249 / Loop: chrome-pop joins the click handshake —\n // mouse-down deepens cyan press (R196), release fires\n // .anet-chrome-pop on the button (R249) AND triggers\n // toggleLayout if state changes. The pop runs even when\n // clicking the already-active layout (no state change),\n // confirming the click was received either way.\n /* Round 306 / Loop: focus-visible:ring-2 → ring-1 unifies\n with the rest of the chrome button family. Pre-R306\n the Layout toggle (Ring/Grid) used `focus-visible:\n ring-2` (2px outline) while nodeSize S/M/L (line\n ~7291), zoom -/+ (~7328/~7395), reset (~7417), and\n fullscreen (~7477) all use `focus-visible:ring-1`\n (1px outline). Two different focus-ring widths on\n visually-analogous chrome controls — same R268\n border-color unification + R288 icon-stroke\n unification family. Reducing Ring/Grid to ring-1\n lets all 7 chrome buttons share one focus-ring\n weight; cyan-400/60 + ring-inset retained. The\n R163/R196 hover/active deeps + R249 chrome-pop\n click feedback continue unchanged. */\n // R351: hover:tracking-wide extends the R344/R345/R347\n // hover-letter-spacing family to a 4th surface (chrome-\n // strip Ring/Grid pair). transition-colors className\n // dropped in favour of an inline transition spec that\n // bundles bg/color (150ms ease) + letter-spacing\n // (200ms ease-out) — Tailwind's transition-colors\n // doesn't list letter-spacing, so without this the\n // hover:tracking-wide would snap. Sibling change on\n // the Grid button below.\n // Round 492 / Loop — add `active:scale-95` press feedback\n // alongside R196's `active:bg-cyan-500/25` color-deepen.\n // Pre-R492 the chrome-strip Ring/Grid buttons had color\n // tactile (deeper cyan on mouse-down) + R249 chrome-pop\n // on release, but no transform during the press itself —\n // the button stayed planted between mouse-down and pop.\n // Adding `active:scale-95` (5% compression) on the\n // pressed pseudo-state, with `transform 150ms ease-out`\n // bundled into the inline transition list, gives haptic-\n // like push-back feedback. The press-down (down to 95%\n // scale) eases in over 150ms in sync with the bg/color\n // deepen; the release auto-springs back to scale-100 via\n // the same transition, then R249's anet-chrome-pop class\n // overlays the release-pop. Matching `transform-gpu`\n // promotes the layer so the scale doesn't trigger\n // layout/paint thrash. Sibling change on Grid below.\n className={`px-2.5 py-1 focus:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset hover:tracking-wide active:scale-95 transform-gpu ${layout === 'ring' ? 'bg-cyan-500/15 text-cyan-300 font-medium hover:bg-cyan-500/20 active:bg-cyan-500/25' : 'text-gray-400 hover:text-cyan-300 hover:bg-cyan-500/5 active:bg-cyan-500/15'} ${chromePopping === 'layout-ring' ? ' anet-chrome-pop' : ''}`}\n style={{ transition: 'background-color 150ms ease, color 150ms ease, letter-spacing 200ms ease-out, transform 150ms ease-out' }}\n >\n Ring\n </button>\n <button\n onClick={() => { popChrome('layout-grid'); if (layout !== 'grid') toggleLayout(); }}\n aria-pressed={layout === 'grid'}\n title=\"Grid layout (l to toggle)\"\n data-topo-chrome-layout=\"grid\"\n data-topo-chrome-layout-active={layout === 'grid' ? 'true' : 'false'}\n data-topo-chrome-layout-grid-popping={chromePopping === 'layout-grid' ? 'true' : 'false'}\n // Round 196 / Loop: R163 layout-toggle Grid variant picks up\n // press-state — same tier pattern as Ring above.\n // Round 249 / Loop: chrome-pop on click — same as Ring.\n // Round 306 / Loop: focus-visible:ring-2 → ring-1 sibling\n // change to Ring above — unifies focus-ring width across\n // all chrome buttons.\n // R351 sibling — Grid button picks up hover:tracking-wide\n // + inline transition spec. Same vocabulary as Ring.\n // R492 sibling — Grid button picks up active:scale-95\n // press feedback + transform in transition list. Same\n // vocabulary as Ring above.\n className={`px-2.5 py-1 border-l focus:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset hover:tracking-wide active:scale-95 transform-gpu ${layout === 'grid' ? 'bg-cyan-500/15 text-cyan-300 font-medium hover:bg-cyan-500/20 active:bg-cyan-500/25' : 'text-gray-400 hover:text-cyan-300 hover:bg-cyan-500/5 active:bg-cyan-500/15'} ${chromePopping === 'layout-grid' ? ' anet-chrome-pop' : ''}`}\n /* Round 268 / Loop: Grid button's left border (the\n internal divider between Ring and Grid) picks up\n pal.containerBorder, matching the wrapper change at\n line ~1460 and the chrome strip's segmented borders\n (nodeSize, zoom). The R268 transition-colors className\n used to carry the border-color ease; R351 unfolds the\n transition list into the inline spec below so the\n letter-spacing tween rides alongside without snapping\n the border-color flip — border-color 200ms ease-out\n keeps R268's theme-toggle smoothness intact.\n R492 adds `transform 150ms ease-out` so active:scale-95\n eases smoothly. */\n style={{ borderColor: pal.containerBorder, transition: 'background-color 150ms ease, color 150ms ease, border-color 200ms ease-out, letter-spacing 200ms ease-out, transform 150ms ease-out' }}\n >\n Grid\n </button>\n </div>\n {/* R79: working + online count chips become hover affordances —\n extends R77's chip-hover pattern to status counts. Hover the\n working chip → setHoveredStatus('working') so the R55 dim\n mechanic kicks in (same as hovering the SVG legend \"working\"\n row, just a different surface for the same gesture).\n The online chip uses the same setHoveredStatus('idle') path\n when there's idle but no working — falling back to working\n if any working node exists; \"online\" without a single bucket\n isn't part of the R55 type set, so this chip routes to the\n dominant online sub-tier instead of inventing a new state.\n Cursor only flips when there's anything to highlight. */}\n {/* R82: pin-mirror. R60 pressure-bar segments visualise the\n pinned status via an inset boxShadow; R61 legend rows via a\n concentric r=8 ring; the chip-row chips next to them did\n not — so a user who pinned via Cmd+K (R69) or the legend\n had no chip-row signal that \"working\" was the current\n filter. Mirror the pressure-bar treatment here so all\n status surfaces sing in unison. The online chip mirrors\n for idle pins (working ⊆ online; the routing in\n onMouseEnter already treats online as the idle fallback). */}\n {(() => {\n // R113 / Loop: extend the R97-R102/R101 alias-list tooltip\n // sweep to the remaining chip-row affordances. R79 made\n // these chips hoverable; their generic \"Hover to highlight\"\n // titles never said WHICH nodes match. Compute the alias\n // lists once for both chips to share.\n const workingAliases = onlineNodes.filter(s => s.status === 'working').map(s => s.alias);\n const onlineAliases = onlineNodes.map(s => s.alias);\n const truncate = (list: string[]) => {\n const head = list.slice(0, 8).join(', ');\n const tail = list.length > 8 ? ` + ${list.length - 8} more` : '';\n return head + tail;\n };\n const workingTitle = workingCount === 0\n ? undefined\n : pinnedStatus === 'working'\n ? `${truncate(workingAliases)} — pinned, Esc to clear`\n : `${truncate(workingAliases)} — hover highlights, click to pin`;\n // R140: online chip title gains a \"click to open /nodes\" tail\n // when interactive. R79 made the cursor pointer-shaped but\n // wired nothing; R136 + R139 closed the same lie on two\n // sibling chips by wiring real actions. The online chip\n // can't pin a single status (online = working + idle,\n // not a single pinnedStatus value), so a pin idiom would\n // be semantically wrong. /nodes is the natural full-list\n // destination — same \"click chip for the full list\" idiom\n // the active-links chip uses (R136 → /messages).\n const onlineTitle = onlineNodes.length === 0\n ? undefined\n : pinnedStatus === 'idle'\n ? `${truncate(onlineAliases)} — pinned, Esc to clear`\n : `${truncate(onlineAliases)} — hover highlights · click to open /nodes`;\n return (\n <>\n <span\n // Round 201 / Loop: the working chip joins the\n // \"chip's hover state deepens its OWN identity colour\"\n // family that R193 opened (active-links chip) and R195\n // extended (recent-panel footer). Pre-R201 hovering the\n // working chip fired R55 canvas dim + chip-row highlight\n // but the chip itself stayed at bg-green-500/10 — cause\n // silent, effect loud. R201 deepens its OWN green hue\n // (10→15 bg, 20→30 border) only when clickable.\n // transition-colors duration-200 blends the swap to\n // match R193's timing on the active-links chip.\n /* Round 232 / Loop: HTML chip row picks up the\n tabular-nums info-density treatment R224-R230\n established on the SVG side. The chip text reads\n \"{N} working\"; when workingCount crosses 9→10\n the leading digit's width shift propagates the\n trailing ' working' label right by the digit-vs-\n control glyph delta, and the chip's right edge\n re-flows because the parent is inline flex. The\n Tailwind `tabular-nums` utility sets font-variant-\n numeric: tabular-nums, locking the digit width\n so the chip text + chip width stay stable across\n all counter values. Sibling chips (online,\n active-links) get the same treatment in this\n round so the three-chip row reads uniformly.\n 7th surface in the info-density tabular-nums\n sweep — and the first on the HTML side\n (previous 6 were SVG <text>/<tspan>). */\n /* Round 398 / Loop: chip-row chips gain hover translateY\n (-1px) lift on the CLICKABLE variant only (workingCount\n > 0 here / onlineNodes.length > 0 below / activeLinks\n > 0 deeper). Pre-R398 the chips brightened bg + border\n on hover (R201) but didn't lift — only their clickable\n siblings (filter pin pills R397, recent rows R143,\n legend rows R144) acknowledged cursor entry with a\n translate-y. R398 closes the chip-row by extending\n the same gesture to the static header chips, gated\n on the clickable role so empty chips (which have\n no role=\"button\") stay planted at their R205\n opacity-50 receded paint. transition-transform\n + duration-200 + ease-out + transform-gpu added\n alongside existing transition-colors so the lift\n and the color tween share rhythm.\n Gesture-vocabulary table (post-R398):\n recent-signal row -1 px (R143)\n legend row -1 px (R144)\n group cluster box fill+sw lift (R142)\n filter pin pills -1 px (R397)\n chip-row chips -1 px (R398, this round)\n Empty chips: no lift. Pin-mirror chips: no\n conflict (R180 inset double-ring is a box-shadow\n not a transform). new data-chip-hover-lift attr\n surfaces the lift surface for tests. */\n // R414: chip-row chips gain `group` so inner unit\n // span brightens via group-hover:opacity-100 — sibling\n // to R355 filter pin pill inner-span hover-brighten.\n // Hover-brighten family extends from filter pills to\n // chip-row chips at the inner-span scope.\n // Round 494 / Loop — chip-row working chip joins the\n // active:scale-95 press-feedback family (R492 Ring/Grid +\n // R493 chrome-strip rest). Gated on the clickable branch\n // (workingCount > 0) — when the chip is a placeholder\n // at count=0, scale-95 stays off to match the existing\n // R398 hover-lift conditional. Composes with hover:-\n // translate-y-px for the same lift-and-compress\n // tactile signature R493 brought to reset/fullscreen.\n 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 ${\n workingCount > 0\n ? 'bg-green-500/10 text-green-300 border-green-500/20 hover:bg-green-500/15 hover:border-green-500/30 hover:-translate-y-px active:scale-95'\n : 'bg-green-500/10 text-green-300 border-green-500/20'\n }`}\n data-chip-hover-lift={workingCount > 0 ? 'true' : 'false'}\n data-chip-group-hover-brighten=\"true\"\n data-working-chip\n data-working-chip-aliases={workingAliases.join(',')}\n data-pin-mirror={pinnedStatus === 'working' ? 'true' : 'false'}\n data-working-chip-clickable={workingCount > 0 ? 'true' : 'false'}\n data-working-chip-empty={workingCount === 0 ? 'true' : 'false'}\n title={workingTitle}\n role={workingCount > 0 ? 'button' : undefined}\n tabIndex={workingCount > 0 ? 0 : undefined}\n aria-pressed={workingCount > 0 ? (pinnedStatus === 'working') : undefined}\n // Round 180 / Loop: pin-mirror inset rings now ease in/out\n // instead of snapping. R165 added this transition to the\n // pressure-bar segments; R180 closes the smooth-pin-mirror\n // family across the three remaining chip-row pin chips\n // (working / online / vendor letter). The visual is small\n // — a 1-2 px inset double ring — but the eye catches the\n // pop on every pin/unpin without the ease.\n // Round 201 / Loop: inline transition list now also covers\n // background-color + border-color so the R201 hover tint\n // eases. Tailwind transition-colors on the className would\n // be overridden by this inline declaration, so we splice\n // the colour properties directly into the existing\n // R180 box-shadow transition.\n // Round 205 / Loop: chip recedes to opacity 0.5 when its\n // tier is empty (workingCount=0). Pre-R205 \"0 working\"\n // displayed at full bg-green-500/10 chrome — visually\n // indistinguishable from \"12 working\". Eye got zero\n // empty-tier signal. R205 mirrors R204's legend count\n // recede-on-empty pattern at the chip-row scope. Inline\n // transition list extends `opacity 200ms ease-out` so\n // the crossing-zero ease matches R201's bg/border\n // timing. Empty-state combines with R139's clickable=\n // false + tooltip-undefined: visual + interactive +\n // affordance all say \"this tier has nothing to act on\".\n style={{\n cursor: workingCount > 0 ? 'pointer' : undefined,\n opacity: workingCount === 0 ? 0.5 : 1,\n boxShadow: pinnedStatus === 'working' ? 'inset 0 0 0 1px #4ade80, inset 0 0 0 2px rgba(255,255,255,0.45)' : undefined,\n transition: 'box-shadow 150ms ease-out, background-color 200ms ease-out, border-color 200ms ease-out, opacity 200ms ease-out',\n }}\n onMouseEnter={() => { if (workingCount > 0) setHoveredStatus('working'); }}\n onMouseLeave={() => setHoveredStatus(prev => prev === 'working' ? null : prev)}\n // R139: the title hover-text has been promising \"click to\n // pin\" since R79 but no onClick was ever wired. The cursor:\n // pointer at line 1363 set up the same lie R136 fixed on\n // the active-links chip. Wire it now: click toggles the\n // status pin to 'working', composing with R60 (pressure-\n // bar segments) and R61 (legend rows) — three different\n // surfaces that all toggle the same pinnedStatus. boxShadow\n // pin-mirror at line 1364 already reflects the state; aria-\n // pressed now exposes it for screen readers too.\n onClick={() => {\n if (workingCount > 0) setPinnedStatus(prev => prev === 'working' ? null : 'working');\n }}\n onKeyDown={(e) => {\n if (workingCount === 0) return;\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n setPinnedStatus(prev => prev === 'working' ? null : 'working');\n }\n }}\n >\n {/* Round 337 / Loop: split working chip into digit +\n \" working\" unit, with the unit at opacity-70.\n Extends the R333/R335/R336 chip-internal-hierarchy\n arc from SVG (panel headers) and pin-chip prefix\n surfaces into HTML chip-row chips. Recurring\n pattern: small label spans demote, value stays\n prominent. data-working-chip-unit exposes the\n span for tests. */}\n {/* Round 362 / Loop: digit picks up font-semibold\n (fw 500 → 600) for within-chip weight tier. The\n chip's outer className stays at font-medium (R313\n data-weight baseline); the digit overrides to\n semibold so it reads heavier than its \" working\"\n unit (which keeps fw 500 + R338 opacity-70).\n Joins the R333-R341 chip-internal-hierarchy arc\n at the chip-count scope. Sibling edits on the\n online + active-links chip digits below. data-\n working-chip-digit attr exposes the digit span. */}\n <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>\n </span>\n <span\n // Round 201 / Loop: online chip — mirror of the working\n // chip treatment above. cyan hue 10→15 bg + 20→30 border\n // on hover, only when there's at least one online node\n // to highlight. Three sibling chips in the chip row now\n // all speak the same gesture vocabulary:\n // working chip · green 10→15 (R201)\n // online chip · cyan 10→15 (R201)\n // active-links · gray → cyan (R193)\n /* Round 232 / Loop: tabular-nums on online chip\n (sibling treatment to working chip — same row,\n same digit-jitter physics on count crossings). */\n // R398: hover translate-y lift on clickable variant — see working chip above.\n // R414: `group` parent + inner unit span group-hover-brighten — see working chip above.\n // R494 sibling — online chip joins the active:scale-95 press\n // family (gated on onlineNodes.length > 0 clickable branch,\n // same conditional pattern as the working chip above).\n 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 ${\n onlineNodes.length > 0\n ? 'bg-cyan-500/10 text-cyan-300 border-cyan-500/20 hover:bg-cyan-500/15 hover:border-cyan-500/30 hover:-translate-y-px active:scale-95'\n : 'bg-cyan-500/10 text-cyan-300 border-cyan-500/20'\n }`}\n data-chip-hover-lift={onlineNodes.length > 0 ? 'true' : 'false'}\n data-chip-group-hover-brighten=\"true\"\n data-online-chip\n data-online-chip-aliases={onlineAliases.join(',')}\n data-pin-mirror={pinnedStatus === 'idle' ? 'true' : 'false'}\n data-online-chip-clickable={onlineNodes.length > 0 ? 'true' : 'false'}\n data-online-chip-empty={onlineNodes.length === 0 ? 'true' : 'false'}\n title={onlineTitle}\n role={onlineNodes.length > 0 ? 'link' : undefined}\n tabIndex={onlineNodes.length > 0 ? 0 : undefined}\n // R180: smooth-pin-mirror family — see working chip above.\n // R201: inline transition list also covers bg + border so\n // the new R201 hover tint eases (mirror of working chip).\n // R205: empty-tier recede — opacity 0.5 when onlineNodes\n // is empty (mirror of working chip above).\n style={{\n cursor: onlineNodes.length > 0 ? 'pointer' : undefined,\n opacity: onlineNodes.length === 0 ? 0.5 : 1,\n boxShadow: pinnedStatus === 'idle' ? 'inset 0 0 0 1px #67e8f9, inset 0 0 0 2px rgba(255,255,255,0.45)' : undefined,\n transition: 'box-shadow 150ms ease-out, background-color 200ms ease-out, border-color 200ms ease-out, opacity 200ms ease-out',\n }}\n onMouseEnter={() => {\n // If a working filter would isolate nothing, route to idle.\n const idleCount = onlineNodes.length - workingCount;\n if (workingCount > 0) setHoveredStatus('working');\n else if (idleCount > 0) setHoveredStatus('idle');\n }}\n onMouseLeave={() => setHoveredStatus(prev => prev === 'working' || prev === 'idle' ? null : prev)}\n // R140: click → /nodes. Mirrors R136 active-links→/messages\n // idiom. The chip's hover semantics (R79 highlight all\n // online) keep their meaning — hover for canvas preview,\n // click for the full list. Pinning idle here would\n // semantically misrepresent the chip (which means\n // working+idle), so we navigate instead of pin.\n onClick={() => {\n if (onlineNodes.length > 0) router.push('/nodes');\n }}\n onKeyDown={(e) => {\n if (onlineNodes.length === 0) return;\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n router.push('/nodes');\n }\n }}\n >\n {/* R337 sibling — online chip unit demotion. */}\n {/* R362 sibling — online-chip digit gains font-semibold. */}\n <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>\n </span>\n </>\n );\n })()}\n {/* Round 31 / Loop: fleet-health pressure bar. The \"X working /\n Y online\" chips already carry the raw counts; the bar lets\n the eye get the working/idle/offline RATIO in one glance\n without mental math. Stacked 3-segment chip, ~64 px wide. */}\n {(() => {\n const w = workingCount;\n const i = onlineNodes.length - workingCount; // idle = online - working\n const o = offlineNodes.length;\n const total = w + i + o;\n if (total === 0) return null;\n // Round 60 / Loop: each segment toggles a sticky filter via\n // `pinnedStatus`. Click the working segment → all non-working\n // nodes dim; click again → release. Segments share width with\n // their proportion of `total`, so a 1-node working share is\n // ~3 px wide on a 64-px bar. We pad the click target with a\n // negative-margin overlay wrapper to give thin slices a\n // 14-px minimum hit area without disturbing the rendered\n // chip width. cursor:pointer + a one-line title hint at the\n // affordance.\n const seg = (n: number, color: string, key: 'working' | 'idle' | 'offline', label: string) => {\n if (n === 0) return null;\n const isPinned = pinnedStatus === key;\n // R102: list the aliases that match this segment's bucket\n // so the title answers WHICH n, not just HOW MANY. Closes\n // the last \"info-density gap\" in the chip-row surfaces\n // (R97 pills / R99 group labels / R101 vendor letters all\n // already enumerate). Truncates at 8 with \"+N more\".\n const matchAliases = key === 'working'\n ? onlineNodes.filter(s => s.status === 'working').map(s => s.alias)\n : key === 'idle'\n ? onlineNodes.filter(s => s.status !== 'working').map(s => s.alias)\n : offlineNodes.map(s => s.alias);\n const previewList = matchAliases.slice(0, 8).join(', ');\n const suffix = matchAliases.length > 8 ? ` + ${matchAliases.length - 8} more` : '';\n const titleAction = isPinned ? 'click to release filter' : 'click to highlight';\n return (\n <span\n key={key}\n data-pressure-seg={key}\n data-pressure-seg-aliases={matchAliases.join(',')}\n data-pressure-seg-hovered={hoveredStatus === key ? 'true' : 'false'}\n role=\"button\"\n tabIndex={0}\n aria-pressed={isPinned}\n className=\"anet-topo-chip-focus\"\n title={`${n} ${label}\\n${previewList}${suffix}\\n${titleAction}`}\n // Round 165 / Loop: smooth width transitions on the\n // pressure-bar segments. Pre-R165 the widths snapped\n // instantly when fleet composition shifted (a node\n // going idle → working would visibly jump the green\n // segment by a few px). 220ms ease-out makes the bar\n // visually breathe with state — segment shifts now\n // glide into place instead of cutting. Pure CSS\n // transition on width; respects prefers-reduced-\n // motion via globals.css blanket override that\n // neutralises transition-duration universally.\n // boxShadow gets its own transition so pin\n // state-changes also fade smoothly, not snap.\n //\n // Round 210 / Loop: segment deepens its OWN colour on\n // hover via filter: brightness(1.2). Closes the\n // chip-row \"hover deepens own identity\" family at the\n // pressure-bar scope — R83 already setHoveredStatus\n // to drive canvas dim, but the segment itself stayed\n // at flat `background: color`. R210 makes the cause\n // element (segment) light up with the same gesture\n // it fires on the canvas (effect element). Family\n // surfaces (6 with R210):\n // R193 active-links chip · gray → cyan\n // R195 recent-panel footer · gray → cyan\n // R201 working / online · own /10 → /15 (×2)\n // R202 vendor letter · per-vendor color-mix\n // R210 pressure-bar seg · brightness(1.2) ← NEW\n // brightness(1.2) lightens the segment's own hue by\n // 20% — keeps the tier identity (green stays green,\n // teal stays teal, gray stays gray) while signalling\n // \"this is hovered\". transition adds filter 150ms\n // ease-out alongside the existing width / box-shadow.\n style={{\n width: `${(n / total) * 100}%`,\n background: color,\n height: '100%',\n cursor: 'pointer',\n boxShadow: isPinned ? `inset 0 0 0 1px ${color}, inset 0 0 0 2px rgba(255,255,255,0.6)` : undefined,\n filter: hoveredStatus === key ? 'brightness(1.2)' : undefined,\n transition: 'width 220ms ease-out, box-shadow 150ms ease-out, filter 150ms ease-out',\n }}\n onClick={(e) => {\n e.stopPropagation();\n setPinnedStatus(prev => prev === key ? null : key);\n }}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n setPinnedStatus(prev => prev === key ? null : key);\n }\n }}\n // R83: hover preview — segments now match the R55 legend,\n // R79 working/online chips, and R80 vendor letters in\n // offering a hover-transient highlight before the click\n // commits to a pin. Users get to feel what the filter\n // does before they lock it in. Same activeStatus =\n // hoveredStatus ?? pinnedStatus formula; releasing the\n // pointer falls back to the pin if one is set, or to\n // baseline. Thin (3-px) segments still hit-test fine\n // because the chip itself is a flex row — span doesn't\n // need any extra hit padding for hover.\n onMouseEnter={() => setHoveredStatus(key)}\n onMouseLeave={() => setHoveredStatus(prev => prev === key ? null : prev)}\n />\n );\n };\n // Round 47 / Loop: hidden on mobile — at <640px the chip row\n // wraps to multiple lines and crowds the topology header;\n // pressure ratio is best read with the working+online raw\n // counts (kept visible) anyway.\n return (\n <span\n className=\"hidden sm:inline-flex items-center gap-2 px-2.5 py-1 rounded-md bg-gray-500/10 text-gray-400 border border-gray-500/20 font-mono\"\n title={`${w} working · ${i} idle · ${o} offline`}\n data-fleet-pressure\n >\n {/* Round 373 / Loop: pressure-bar kicker label gains\n font-medium (fw 400 → 500). Sibling small-text fw\n lift family with R363 recent-row alias + R364\n legend-row label + R366 group-label count + R368\n +N more flows footer — extends to a 5th surface\n (the chip-row's 'pressure' label). At fontSize\n 10 px tracking-wide against the chip's gray bg,\n the default fw 400 sat below the deliberate-data\n band; fw 500 brings it into parity with the\n chip-row 'working / online / active links' unit\n spans (chip-level font-medium R313). data-fleet-\n pressure-kicker attr exposes the kicker for tests. */}\n <span className=\"text-[10px] tracking-wide font-medium\" data-fleet-pressure-kicker>pressure</span>\n {/* Round 374 / Loop: pressure-bar height h-1.5 → h-2\n (6 → 8 px) — sibling visual-weight bump (8th anchor\n in the family):\n R287 minimap viewport stroke 1 → 1.5\n R295 legend swatch base radius 5.5 → 6\n R359 recent-row pip base radius 1.6 → 1.8\n R360 hub digit fontSize 11 → 12\n R361 edge-badge digit fontSize 10 → 11\n R365 hub-highlight base radius 5 → 5.5\n R367 edge-badge rest stroke 1 → 1.25\n R374 pressure-bar height 1.5 → 2 (this round)\n +33 % bar height gives the working/idle/offline\n segments more visibility — at h-1.5 the 3-segment\n proportion bar was readable but slim; at h-2 the\n segments parse cleanly even when one tier is\n < 10 % share. Geometry-safe: items-center flex\n centers the bar inside the chip's py-1 (4 px top +\n 4 px bottom) — bar at 8 px stays comfortably\n inside the 10-px text-row height. R165 segment\n width transitions + R210 brightness hover + R83\n pin-mirror box-shadow on segments all preserved\n (segments inherit width from parent so the height\n bump propagates without segment-side edits).\n data-fleet-pressure-bar-height attr exposes the\n height token for tests. */}\n <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\">\n {seg(w, isLight ? '#059669' : '#22c55e', 'working', 'working')}\n {seg(i, isLight ? '#0d9488' : '#2dd4bf', 'idle', 'idle')}\n {seg(o, isLight ? '#94a3b8' : '#6b7280', 'offline', 'offline')}\n </span>\n </span>\n );\n })()}\n {/* Round 64 / Loop: active-filter pills. When pinnedStatus or\n pinnedGroup is set, show a small \"filter: <key> ×\" pill so\n the user can see the pin from the chip row even if they\n scrolled the canvas off-screen. × button clears the\n specific pin (Esc still clears all — both paths are\n valid). pinnedStatus and pinnedGroup pin independently so\n both pills may render simultaneously. */}\n {/* R67: pills enter via anet-fade-in so they appear softly\n instead of popping. The \"filter: \" prefix collapses below\n sm — at narrow viewports the chip row is precious real\n estate (R47 already hides pressure / vendor / freshness),\n so dropping the redundant label keeps the working/idle/\n alpha keys readable without overflow. */}\n {/* R71: each pill picks up a \"· N\" match count tail. Tells the\n user at a glance how many sessions the active filter\n matches without scanning the canvas. Counts come from the\n already-computed workingCount + onlineNodes + offlineNodes\n for status, and groupKeys for group. */}\n {/* R73: entire pill body is a click-to-clear target — matches\n the Notion / Linear tag UX (the whole chip releases). The\n × keeps its dedicated <button> + aria-label for screen\n readers; the outer span just adds an extra mouse-friendly\n hit area with a title hint. ×'s onClick stopPropagation\n so the redundant outer onClick doesn't double-fire (no\n functional difference since both clear, but cleaner\n event flow). */}\n {pinnedStatus && (() => {\n const matchCount = pinnedStatus === 'working' ? workingCount\n : pinnedStatus === 'idle' ? (onlineNodes.length - workingCount)\n : offlineNodes.length;\n // Round 97 / Loop: the pill says HOW MANY but not WHICH.\n // Hovering it now shows the matched alias list in the\n // tooltip — answers \"exactly who is this filtering to\"\n // without forcing the user to scan the dim mask on the\n // canvas. Truncates at 8 names so a 50-node working\n // bucket doesn't produce a 12-line tooltip; the count\n // chip already answers \"how many overall\".\n const matchAliases = pinnedStatus === 'working'\n ? onlineNodes.filter(s => s.status === 'working').map(s => s.alias)\n : pinnedStatus === 'idle'\n ? onlineNodes.filter(s => s.status !== 'working').map(s => s.alias)\n : offlineNodes.map(s => s.alias);\n const matchPreview = matchAliases.slice(0, 8).join(', ');\n const matchSuffix = matchAliases.length > 8 ? ` + ${matchAliases.length - 8} more` : '';\n return (\n <span\n data-active-filter=\"status\"\n data-filter-match-count={matchCount}\n data-filter-match-aliases={matchAliases.join(',')}\n // R355: `group` lets the inner opacity-70 spans (prefix\n // `filter:` + count `· N`) brighten to 100 % on pill hover.\n // Sibling treatment on group + vendor pills below.\n // R495 — filter pills (3 sibling `group` variants) join the\n // active:scale-95 press-feedback family. R490's !important\n // transition list on .anet-topo-chip-focus already covers\n // transform, so just appending active:scale-95 to the\n // className wires the press tactile in one token. Compound\n // with R400-era hover:-translate-y-px gives lift-and-compress.\n className=\"group inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md font-mono font-medium text-xs border anet-fade-in anet-topo-chip-focus transition-transform duration-200 ease-out hover:-translate-y-px active:scale-95 transform-gpu\" data-topo-filter-pill-hover-lift=\"true\"\n title={matchCount > 0 ? `${matchPreview}${matchSuffix} — click to clear` : 'Click to clear filter'}\n onClick={() => setPinnedStatus(null)}\n style={{\n background: pinnedStatus === 'working' ? (isLight ? '#05966914' : '#22c55e1f')\n : pinnedStatus === 'idle' ? (isLight ? '#0d948814' : '#2dd4bf1f')\n : (isLight ? '#94a3b814' : '#6b72801f'),\n color: pinnedStatus === 'working' ? (isLight ? '#047857' : '#86efac')\n : pinnedStatus === 'idle' ? (isLight ? '#0f766e' : '#5eead4')\n : (isLight ? '#475569' : '#9ca3af'),\n borderColor: 'currentColor',\n cursor: 'pointer',\n }}\n >\n {/* Round 412 / Loop: filter pin pill VALUE picks up the\n chip-internal-hierarchy arc. Pre-R412 the value span\n (pinnedStatus / pinnedGroup / pinnedVendor) inherited\n the parent's font-medium (fw=500); prefix and suffix\n were opacity-70 label-tier but the VALUE itself sat\n at the same baseline weight. R412 wraps the value in\n a font-semibold span (fw=600) so the pill now reads\n with proper data-tier emphasis — sibling treatment\n to R333/R335-R341/R362/R369/R389/R410. data-filter-\n value attr surfaces the value span for tests.\n 4-pill replace family — status / group / vendor / edge. */}\n <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>\n <button\n type=\"button\"\n aria-label={`Clear ${pinnedStatus} filter`}\n onClick={(e) => { e.stopPropagation(); setPinnedStatus(null); }}\n /* Round 356 / Loop: filter pin pill × buttons gain\n hover:scale-110 (Tailwind 4 modern CSS `scale` property,\n not legacy transform). Sibling polish to R354 vendor\n letter glyph + R350/R352/R353 chrome icon hover-scales.\n Pre-R356 the × had only hover:opacity-70 — the target\n dimmed under cursor but didn't lift. R356 adds a 10 %\n scale on hover so the click-target reads as \"press me\"\n alongside the dim. transform-gpu hint promotes the\n button to its own compositor layer for crisper edges\n during the scale tween. transition-transform duration-\n 200 matches the chrome icon hover-scale timing family.\n inline-block is default for <button> so no display\n tweak needed. replace_all covers all 4 filter pin\n pills (status / group / vendor / edge) at once. */\n className=\"ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 transform-gpu\"\n style={{ background: 'transparent', color: 'inherit', cursor: 'pointer', padding: 0 }}\n >×</button>\n </span>\n );\n })()}\n {pinnedGroup && (() => {\n const matchCount = Object.values(groupKeys).filter(k => k === pinnedGroup).length;\n // R97: list group members in the tooltip.\n const matchAliases = Object.entries(groupKeys)\n .filter(([, key]) => key === pinnedGroup)\n .map(([alias]) => alias);\n const matchPreview = matchAliases.slice(0, 8).join(', ');\n const matchSuffix = matchAliases.length > 8 ? ` + ${matchAliases.length - 8} more` : '';\n return (\n <span\n data-active-filter=\"group\"\n data-filter-match-count={matchCount}\n data-filter-match-aliases={matchAliases.join(',')}\n // R355 sibling — `group` parent + group-hover on inner spans.\n // R495 — filter pills (3 sibling `group` variants) join the\n // active:scale-95 press-feedback family. R490's !important\n // transition list on .anet-topo-chip-focus already covers\n // transform, so just appending active:scale-95 to the\n // className wires the press tactile in one token. Compound\n // with R400-era hover:-translate-y-px gives lift-and-compress.\n className=\"group inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md font-mono font-medium text-xs border anet-fade-in anet-topo-chip-focus transition-transform duration-200 ease-out hover:-translate-y-px active:scale-95 transform-gpu\" data-topo-filter-pill-hover-lift=\"true\"\n title={matchCount > 0 ? `${matchPreview}${matchSuffix} — click to clear` : 'Click to clear filter'}\n onClick={() => setPinnedGroup(null)}\n style={{\n background: isLight ? '#67e8f914' : '#67e8f91f',\n color: pal.legendAccent,\n borderColor: 'currentColor',\n cursor: 'pointer',\n }}\n >\n {/* R412: see status pill above — filter value fw=600 data tier. */}\n <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>\n <button\n type=\"button\"\n aria-label={`Clear group filter ${pinnedGroup}`}\n onClick={(e) => { e.stopPropagation(); setPinnedGroup(null); }}\n /* Round 356 / Loop: filter pin pill × buttons gain\n hover:scale-110 (Tailwind 4 modern CSS `scale` property,\n not legacy transform). Sibling polish to R354 vendor\n letter glyph + R350/R352/R353 chrome icon hover-scales.\n Pre-R356 the × had only hover:opacity-70 — the target\n dimmed under cursor but didn't lift. R356 adds a 10 %\n scale on hover so the click-target reads as \"press me\"\n alongside the dim. transform-gpu hint promotes the\n button to its own compositor layer for crisper edges\n during the scale tween. transition-transform duration-\n 200 matches the chrome icon hover-scale timing family.\n inline-block is default for <button> so no display\n tweak needed. replace_all covers all 4 filter pin\n pills (status / group / vendor / edge) at once. */\n className=\"ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 transform-gpu\"\n style={{ background: 'transparent', color: 'inherit', cursor: 'pointer', padding: 0 }}\n >×</button>\n </span>\n );\n })()}\n {/* R89: vendor pin gets its own filter pill, matching the R64\n status + group pattern. R88 added the pin but only the\n letter itself carried the state; the chip-row had no\n \"filter: A · 2 ×\" affordance the other two pins have. The\n pill colour borrows the vendor's own swatch so each pin\n still reads in its native hue (A green, O cyan, 书 blue,\n ? slate). Same body-click-clears + × button pattern as\n R64. */}\n {pinnedVendor && (() => {\n const matchEntry = vendorDist.find(v => v.initial === pinnedVendor);\n const matchCount = matchEntry?.count ?? 0;\n const vendorColor = matchEntry?.color ?? pal.legendText;\n // R97: list vendor users in the tooltip. The vendorIdentity\n // resolver maps model strings to a vendor initial — match\n // any session whose initial equals the pinned letter (with\n // unknowns folded to '?').\n const matchAliases = [...onlineNodes, ...offlineNodes]\n .filter(s => {\n const v = vendorForModel(s.model);\n return (v.id === 'unknown' ? '?' : v.initial) === pinnedVendor;\n })\n .map(s => s.alias);\n const matchPreview = matchAliases.slice(0, 8).join(', ');\n const matchSuffix = matchAliases.length > 8 ? ` + ${matchAliases.length - 8} more` : '';\n return (\n <span\n data-active-filter=\"vendor\"\n data-filter-match-count={matchCount}\n data-filter-match-aliases={matchAliases.join(',')}\n // R355 sibling — `group` parent + group-hover on inner spans.\n // R495 — filter pills (3 sibling `group` variants) join the\n // active:scale-95 press-feedback family. R490's !important\n // transition list on .anet-topo-chip-focus already covers\n // transform, so just appending active:scale-95 to the\n // className wires the press tactile in one token. Compound\n // with R400-era hover:-translate-y-px gives lift-and-compress.\n className=\"group inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md font-mono font-medium text-xs border anet-fade-in anet-topo-chip-focus transition-transform duration-200 ease-out hover:-translate-y-px active:scale-95 transform-gpu\" data-topo-filter-pill-hover-lift=\"true\"\n title={matchCount > 0 ? `${matchPreview}${matchSuffix} — click to clear` : 'Click to clear vendor filter'}\n onClick={() => setPinnedVendor(null)}\n style={{\n background: `${vendorColor}1f`,\n color: vendorColor,\n borderColor: 'currentColor',\n cursor: 'pointer',\n }}\n >\n {/* R412: see status pill above — filter value fw=600 data tier. */}\n <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>\n <button\n type=\"button\"\n aria-label={`Clear vendor filter ${pinnedVendor}`}\n onClick={(e) => { e.stopPropagation(); setPinnedVendor(null); }}\n /* Round 356 / Loop: filter pin pill × buttons gain\n hover:scale-110 (Tailwind 4 modern CSS `scale` property,\n not legacy transform). Sibling polish to R354 vendor\n letter glyph + R350/R352/R353 chrome icon hover-scales.\n Pre-R356 the × had only hover:opacity-70 — the target\n dimmed under cursor but didn't lift. R356 adds a 10 %\n scale on hover so the click-target reads as \"press me\"\n alongside the dim. transform-gpu hint promotes the\n button to its own compositor layer for crisper edges\n during the scale tween. transition-transform duration-\n 200 matches the chrome icon hover-scale timing family.\n inline-block is default for <button> so no display\n tweak needed. replace_all covers all 4 filter pin\n pills (status / group / vendor / edge) at once. */\n className=\"ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 transform-gpu\"\n style={{ background: 'transparent', color: 'inherit', cursor: 'pointer', padding: 0 }}\n >×</button>\n </span>\n );\n })()}\n {/* R119: edge pin filter pill — completes the R64 / R89 pill\n pattern across all four pin dimensions. R116 added\n pinnedEdgeKey but the chip row never grew the matching\n pill, leaving the locked flow visible only on the canvas +\n recent-signal row tint. This pill shows \"filter:\n alpha→beta · 3\" so the locked flow appears in the same\n row as the other three pin types. Body-click + × button\n clear, same pattern as R64. */}\n {pinnedEdgeKey && (() => {\n const link = flowLinks.find(l => l.key === pinnedEdgeKey);\n if (!link) return null;\n // R150 / Loop: extend the hot-lane amber convention from the\n // canvas badge (R126) / recent-row count (R127) / panel\n // header (R129) to the R119 edge filter pill — when the\n // pinned edge has count ≥ 10 the count tspan flips to\n // amber + 700 weight, and the tooltip grows a \"(hot lane\n // · ≥ 10)\" marker. Closes the 4th hot-lane surface,\n // completing R150's milestone: every place that surfaces\n // an edge count now uses the same amber-when-hot vocab.\n const isHot = link.count >= 10;\n const hotStroke = isLight ? '#d97706' : '#fbbf24';\n return (\n <span\n data-active-filter=\"edge\"\n data-filter-match-count={link.count}\n data-filter-match-aliases={`${link.from},${link.to}`}\n data-active-filter-edge-hot={isHot ? 'true' : 'false'}\n // R495 sibling — 4th filter pill (no `group` prefix variant)\n // joins active:scale-95 press family alongside the 3 group\n // variants above. Same recipe.\n className=\"inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md font-mono font-medium text-xs border anet-fade-in anet-topo-chip-focus transition-transform duration-200 ease-out hover:-translate-y-px active:scale-95 transform-gpu\" data-topo-filter-pill-hover-lift=\"true\"\n title={`${link.from} → ${link.to} (${link.count} msg${link.count === 1 ? '' : 's'}${isHot ? ', hot lane · ≥ 10' : ''}) — click to clear`}\n onClick={() => setPinnedEdgeKey(null)}\n style={{\n background: isLight ? `${pal.flowEdge}14` : `${pal.flowEdge}1f`,\n color: pal.flowEdge,\n borderColor: 'currentColor',\n cursor: 'pointer',\n }}\n >\n {/* R412: filter pin pill value (edge variant) picks up fw=600.\n Sibling treatment to the status/group/vendor pills above. */}\n <span>\n <span className=\"hidden sm:inline opacity-70\" data-filter-prefix>filter: </span>\n <span className=\"font-semibold\" data-filter-value>{link.from}→{link.to}</span>\n {/* Round 323 / Loop: edge filter pill count digit picks\n up tabular-nums (Tailwind class on both cold +\n hot branches). Sibling treatment to the status /\n group / vendor pin pills (R323 replace_all upstream\n in this same round added `tabular-nums` to those\n three pills' count spans). Pre-R323 a matchCount /\n link.count crossing 9→10 widened the digit and\n shifted the trailing × button right ~3px in font-\n mono (mono digits still have natural-vs-tabular\n variance). Locks the slot so the × button stays\n planted as the count grows. 9th surface in the\n info-density tabular-nums sweep after R322 panel\n hot count. */}\n {isHot ? (\n <span\n className=\"opacity-90 tabular-nums\"\n style={{ color: hotStroke, fontWeight: 700 }}\n data-active-filter-edge-count-hot\n >\n {' · '}{link.count}\n </span>\n ) : (\n <span className=\"opacity-70 tabular-nums\" data-active-filter-edge-count>\n {' · '}{link.count}\n </span>\n )}\n </span>\n <button\n type=\"button\"\n aria-label={`Clear edge filter ${link.from} → ${link.to}`}\n onClick={(e) => { e.stopPropagation(); setPinnedEdgeKey(null); }}\n /* Round 356 / Loop: filter pin pill × buttons gain\n hover:scale-110 (Tailwind 4 modern CSS `scale` property,\n not legacy transform). Sibling polish to R354 vendor\n letter glyph + R350/R352/R353 chrome icon hover-scales.\n Pre-R356 the × had only hover:opacity-70 — the target\n dimmed under cursor but didn't lift. R356 adds a 10 %\n scale on hover so the click-target reads as \"press me\"\n alongside the dim. transform-gpu hint promotes the\n button to its own compositor layer for crisper edges\n during the scale tween. transition-transform duration-\n 200 matches the chrome icon hover-scale timing family.\n inline-block is default for <button> so no display\n tweak needed. replace_all covers all 4 filter pin\n pills (status / group / vendor / edge) at once. */\n className=\"ml-0.5 leading-none hover:opacity-70 transition-transform duration-200 ease-out hover:scale-110 transform-gpu\"\n style={{ background: 'transparent', color: 'inherit', cursor: 'pointer', padding: 0 }}\n >×</button>\n </span>\n );\n })()}\n {/* Round 124 / Loop: pin-intersection summary chip. The four\n filter pills (R64 status, R63 group, R89 vendor, R119\n edge) each report their own dim's match count in\n isolation — that answers \"what does THIS filter catch\"\n but not \"what survives ALL pins\". Since the node-opacity\n chain AND-composes the four pin dimensions, two active\n pins routinely produce an intersection smaller than\n either individual count. With three or four pins active\n the gap widens further. This chip appears ONLY when\n ≥ 2 pin dims are active (a single pin's pill already\n tells the whole story) and shows the count of nodes\n that satisfy every active pin simultaneously. Color is\n deliberately neutral (gray) since the chip represents\n the AND of mixed-color filters — borrowing any one of\n the pill hues would mis-signal which dim dominates.\n Tooltip lists the surviving aliases with the same\n 8-truncate + \"+N more\" pattern as the individual pill\n tooltips (R97). No click handler — it's a pure readout;\n Esc / per-pill × are still the cancel paths. */}\n {(() => {\n const pinDimCount =\n (pinnedStatus ? 1 : 0) +\n (pinnedGroup ? 1 : 0) +\n (pinnedVendor ? 1 : 0) +\n (pinnedEdgeKey ? 1 : 0);\n if (pinDimCount < 2) return null;\n const edgeLink = pinnedEdgeKey\n ? flowLinks.find(l => l.key === pinnedEdgeKey)\n : null;\n const edgeEndpoints: Set<string> | null = edgeLink\n ? new Set([edgeLink.from, edgeLink.to])\n : null;\n const allSessions = [...onlineNodes, ...offlineNodes];\n const survivors = allSessions.filter(s => {\n const isOnline = s.status !== 'offline';\n if (pinnedStatus === 'working' && s.status !== 'working') return false;\n if (pinnedStatus === 'idle' && !(isOnline && s.status !== 'working')) return false;\n if (pinnedStatus === 'offline' && isOnline) return false;\n if (pinnedGroup) {\n const gk = groupKeys[s.alias] ?? s.alias;\n if (gk !== pinnedGroup) return false;\n }\n if (pinnedVendor) {\n const v = vendorForModel(s.model);\n const initial = v.id === 'unknown' ? '?' : v.initial;\n if (initial !== pinnedVendor) return false;\n }\n if (edgeEndpoints && !edgeEndpoints.has(s.alias)) return false;\n return true;\n });\n const matchAliases = survivors.map(s => s.alias);\n const matchPreview = matchAliases.slice(0, 8).join(', ');\n const matchSuffix = matchAliases.length > 8 ? ` + ${matchAliases.length - 8} more` : '';\n const isEmpty = matchAliases.length === 0;\n const tooltip = !isEmpty\n ? `${matchPreview}${matchSuffix} — nodes passing all ${pinDimCount} pinned filters`\n : `No nodes pass all ${pinDimCount} pinned filters — release one to widen (Esc clears all)`;\n // R125: when the intersection drops to zero, the chip flips\n // from neutral gray to a warning amber. Zero-overlap is the\n // exact case users get confused — canvas dims to 0.28\n // everywhere, no positive signal explains why. The \"· 0\"\n // tail in neutral gray reads as just another number,\n // indistinguishable from \"· 12\". Amber + a ⚠ glyph lifts\n // it to \"your filters cancel out\" at a glance. Color\n // choice: amber (#d97706 light, #fbbf24 dark) — same hue\n // family as warning chips elsewhere in the dashboard,\n // distinct from any pill color (status green/teal/slate,\n // group cyan, vendor varies, edge cyan, neutral gray for\n // non-empty intersection) so the empty state stands out.\n const emptyColor = isLight ? '#d97706' : '#fbbf24';\n return (\n <span\n data-pin-intersection\n data-pin-dim-count={pinDimCount}\n data-pin-intersection-count={matchAliases.length}\n data-pin-intersection-empty={isEmpty ? 'true' : 'false'}\n data-pin-intersection-aliases={matchAliases.join(',')}\n /* Round 235 / Loop: pin-intersection chip joins the\n info-density tabular-nums sweep. The chip has TWO\n digits visible at once — '{pinDimCount} pins ·\n {matchAliases.length}' — and the matchAliases count\n in particular rolls frequently as filters tighten /\n widen against the live fleet. font-mono already\n makes the digits uniform-ish, but tabular-nums\n further locks digit width within the mono cell so\n the gap between 'pins' and '·' stays stable, and\n the chip's overall width doesn't bump when either\n number changes. 9th surface in the sweep — the\n third and last HTML chip surface, completing\n coverage across chip-row + vendor-row + pin-\n intersection. */\n className=\"hidden sm:inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md font-mono tabular-nums text-xs border anet-fade-in anet-topo-chip-focus\"\n title={tooltip}\n /* Round 236 / Loop: smooth the empty/non-empty colour\n crossing. Pre-R236 the chip snap-flipped between\n slate-on-slate (non-empty filter intersection) and\n amber-on-amber (empty intersection — the 'your\n pinned filters cancel out' warning). bg, color,\n and borderColor (which inherits currentColor) all\n changed in one frame. R236 adds a 200ms ease-out\n transition on all three so when a filter tightens\n matches across the 0-boundary the chip eases\n through the colour shift instead of snapping. Same\n 200ms cadence the other chip-row members use\n (R201 working/online tint, R193 active-links\n tint, R202 vendor letter color-mix). One more\n surface where colour state-changes ease rather\n than snap — consistent with the topology's\n broader transitions vocabulary. */\n style={{\n background: isEmpty\n ? (isLight ? '#d97706' + '14' : '#fbbf24' + '1f')\n : (isLight ? '#94a3b814' : '#94a3b81f'),\n color: isEmpty\n ? emptyColor\n : (isLight ? '#475569' : '#9ca3af'),\n borderColor: 'currentColor',\n transition: 'background-color 200ms ease-out, color 200ms ease-out, border-color 200ms ease-out',\n }}\n >\n <span>\n <span className=\"hidden sm:inline opacity-70\" data-pin-intersection-prefix>match: </span>\n {/* Round 324 / Loop: pin-intersection chip carries TWO\n numeric counts in one breath — pinDimCount (\"how\n many filter pins are active\") and matchAliases.\n length (\"how many aliases land in the intersection\n after pins compose\"). Both jitter on digit-width\n crossings (1→10 etc) without tabular-nums even\n under font-mono. Pre-R324 a fleet busying up so\n one dimension flips from 0→non-zero (chip mounts\n via R237 always-mount opacity gate) AND the match\n count digit ticks 9→10 simultaneously visibly\n jolted the trailing `× pins` / ` × ` segments.\n Two dedicated tabular-nums spans (one per count)\n lock both digit slots so the chip's text geometry\n stays planted through both crossings. 10th\n surface in the info-density tabular-nums sweep\n after R323 filter pin pill counts (R64/R89/R119/\n R150 pin-pill family parity now complete with\n this composed-pin sibling). data-pin-intersection-\n count-* attrs expose both spans for tests. */}\n {/* Round 341 / Loop: middle \" pins\" unit word\n previously sat as a bare text node between the\n two count spans, while the matches-count span\n already carried opacity-0.7 (R335 + R324 era).\n The pinDimCount span is prominent and the\n matches count is recessive — but the literal\n \" pins\" was at FULL opacity, breaking the\n chip-internal hierarchy unified across R333/\n R335/R336/R337/R338/R340. R341 wraps \" pins\"\n in an opacity-0.7 span so the chip reads:\n pinDimCount (prominent value)\n \" pins\" (recessive unit)\n \" · {N}\" (recessive count)\n Three-tier hierarchy on a single chip; 7th\n surface in the chip-internal-hierarchy arc. */}\n <span className=\"tabular-nums\" data-pin-intersection-count-dims>{pinDimCount}</span><span className=\"opacity-70\" data-pin-intersection-unit> pins</span><span className=\"opacity-70 tabular-nums\" data-pin-intersection-count-matches> · {matchAliases.length}</span>\n {/* Round 237 / Loop: ⚠ warning glyph picks up the\n always-mount-opacity-gate idiom. Pre-R237 the\n glyph was conditionally rendered on isEmpty,\n snap-mounting when filter intersection\n narrowed to 0 AND introducing a layout shift\n (ml-1 margin appears alongside the glyph,\n widening the chip by ~16px). The R236 color\n easing made the colour crossing smooth but\n the glyph still pop-jumped, breaking the\n polish that R236 just installed at this\n same chip.\n\n Always-mount the glyph with opacity gated by\n isEmpty + the same 200ms ease-out that R236\n uses on the chip's colour transition. Now the\n whole isEmpty crossing — bg, color, border,\n AND glyph visibility — eases as one\n coordinated 200ms event. ml-1 margin is\n reserved permanently, so the chip width\n stays stable through the crossing (no\n layout-shift jank against neighbouring\n chips). data-pin-intersection-warning attr\n surfaces the visibility state for test\n introspection. 11th surface in the always-\n mount-opacity-gate family (R181 / R182 /\n R183 / R213 ×2 / R214 / R215 / R221 / R222 /\n R223 / R237). */}\n <span\n className=\"ml-1\"\n aria-hidden\n data-pin-intersection-warning={isEmpty ? 'true' : 'false'}\n style={{ opacity: isEmpty ? 1 : 0, transition: 'opacity 200ms ease-out' }}\n >⚠</span>\n </span>\n </span>\n );\n })()}\n {/* Round 281 / Loop: vendor letters chip threshold tightens\n from >1 to >2 per 减法 cut #7. Pre-R281 the chip showed\n whenever ≥2 vendor types existed in the fleet — for a\n typical demo (claude + 1 other = 2 types), the chip\n rendered \"A:N C:M\" adding ~50-80px to the chip-row width.\n Tightening to >2 keeps the chip useful for fleets with\n ACTUAL vendor diversity (3+ types) where the\n composition matters at a glance, but hides it for the\n common 1-2 vendor case where the info is low-signal.\n Continues the R275-R280 simplification arc. */}\n {vendorDist.length > 2 && (\n <span\n className=\"hidden sm:inline-flex items-center gap-2 px-2.5 py-1 rounded-md bg-gray-500/10 text-gray-400 border border-gray-500/20 font-mono\"\n title=\"Hover to highlight; click to pin\"\n >\n {vendorDist.map(v => {\n const isPinned = pinnedVendor === v.initial;\n // R88: click toggles a sticky filter the same way R60\n // pressure-bar segments toggle pinnedStatus. Visual\n // mirror = inset boxShadow using the vendor's own\n // colour, so each pinned letter sings in its own hue\n // (Anthropic green / OpenAI cyan / 书 blue / ?).\n // R101: tooltip lists the aliases that use this vendor —\n // completes the info-density triple started by R97 pills,\n // R98 node titles, R99 group-label titles. Anywhere the\n // UI shows \"A:3\" should hover-explain which 3.\n const aliases = [...onlineNodes, ...offlineNodes]\n .filter(s => {\n const vid = vendorForModel(s.model);\n return (vid.id === 'unknown' ? '?' : vid.initial) === v.initial;\n })\n .map(s => s.alias);\n const preview = aliases.slice(0, 8).join(', ');\n const suffix = aliases.length > 8 ? ` + ${aliases.length - 8} more` : '';\n const tooltip = isPinned\n ? `${preview}${suffix} — click again or Esc to clear`\n : `${preview}${suffix} — click to pin`;\n return (\n <span\n key={v.initial}\n role=\"button\"\n tabIndex={0}\n aria-pressed={isPinned}\n /* Round 234 / Loop: vendor letter chip picks up\n tabular-nums to lock the ':N' suffix's digit\n width. Each vendor chip renders 'A:3', 'C:2',\n etc. inline at gap-0.5 — when one vendor's\n count rolls 9→10 the chip widens by the digit\n glyph delta and pushes downstream chips right,\n making the row visibly jitter. 8th surface\n in the info-density tabular-nums sweep,\n completing the HTML chip-side coverage after\n R232 working/online/active-links chips. The\n digit lives in the inner <span> at line 2194,\n but font-variant-numeric inherits, so applying\n it at the outer chip span reaches every\n descendant glyph for free. */\n /* Round 314 / Loop: vendor letter chip joins the\n R312-R313 'HTML-context data chip = font-medium'\n family. R313 weighted the 3 main chips\n (working/online/active-links); R314 closes the\n chip-row weight sweep by extending to the\n vendor letter chips ('A:N', 'O:N', '书:N',\n '?:N'). They display vendor-distribution\n data; same tier as the sibling data chips. */\n /* Round 401 / Loop: vendor letter chip closes the\n hover-lift gesture family at its last unaddressed\n interactive HTML surface. R397/R398/R399 lifted\n filter pin pills + chip-row chips (working /\n online / active-links); R400 lifted standalone\n chrome buttons (reset / fullscreen). The vendor\n letter chips (A:N / O:N / 书:N / ?:N) are\n sibling interactive chips in the same chip-row\n — clickable to toggle the vendor filter pin —\n but were not yet on the hover-lift family.\n R401 closes the gap with hover:-translate-y-px\n + transition-transform + transform-gpu added\n to the className. The inline transition list\n (box-shadow + background-color) keeps eaching\n independently — different property axes compose\n cleanly. Existing R354 glyph scale-1.1 (inner\n span) + R202 chip bg color-mix + R180 pin-mirror\n box-shadow + R354 glyph hover transform all\n preserved. data-vendor-letter-hover-lift attr\n surfaces the lift for tests. */\n // R417: `group` parent enables the count suffix to\n // brighten on chip hover via group-hover:opacity-100\n // — sibling to R355 filter-pill prefix/suffix + R414\n // chip-row unit brighten. Closes the inner-span\n // hover-brighten family at the vendor chip surface.\n 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\"\n data-vendor-letter={v.initial}\n data-vendor-letter-count={v.count}\n data-vendor-letter-hover-lift=\"true\"\n data-vendor-pinned={isPinned ? 'true' : 'false'}\n data-vendor-hovered={hoveredVendor === v.initial ? 'true' : 'false'}\n data-vendor-aliases={aliases.join(',')}\n title={tooltip}\n // R180: smooth-pin-mirror family — see working chip above.\n // Round 202 / Loop: vendor letter chip joins the \"hover\n // deepens own identity hue\" family R193/R195/R201 built\n // up across the rest of the chip row. Pre-R202 hovering\n // a vendor letter (A/C/G/K/书/?) fired R88 canvas dim\n // via setHoveredVendor, but the chip itself stayed at\n // bg=transparent — cause silent, effect loud. R202\n // tints the chip with its OWN vendor colour at 12%\n // alpha via color-mix() so each vendor's chip lights\n // up in its own hue (Anthropic green / OpenAI cyan /\n // 书 blue / ?). No layout shift: only background-color\n // changes, no border/padding swap. transition list\n // extends the existing R180 box-shadow 150ms with\n // background-color 200ms ease-out (same splice idiom\n // R201 used on the working/online chips). color-mix()\n // is supported Chrome ≥ 111 / Safari ≥ 16.2 / FF ≥ 113;\n // for older browsers the chip falls back to its idle\n // transparent bg (graceful degradation — the canvas-\n // dim effect still fires regardless).\n style={{\n cursor: 'pointer',\n backgroundColor: (hoveredVendor === v.initial && !isPinned)\n ? `color-mix(in srgb, ${v.color} 12%, transparent)`\n : 'transparent',\n boxShadow: isPinned\n ? `inset 0 0 0 1px ${v.color}, inset 0 0 0 2px rgba(255,255,255,0.45)`\n : undefined,\n transition: 'box-shadow 150ms ease-out, background-color 200ms ease-out',\n }}\n onMouseEnter={() => setHoveredVendor(v.initial)}\n onMouseLeave={() => setHoveredVendor(prev => prev === v.initial ? null : prev)}\n onClick={() => setPinnedVendor(prev => prev === v.initial ? null : v.initial)}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n setPinnedVendor(prev => prev === v.initial ? null : v.initial);\n }\n }}\n >\n {/* Round 354 / Loop: vendor letter glyph scales\n 1.0 → 1.1 on hover. R88 already dims OTHER\n vendors on hover via canvas-wide opacity\n masking; R202 added a chip-level bg tint\n (color-mix 12 % alpha) so the chip itself\n responds. R354 closes the trio with a glyph-\n level lift: the focused vendor LETTER actively\n rises (transform scale) rather than the chip\n merely changing colour. Three layers of positive\n feedback on the hovered vendor + canvas-wide\n negative feedback on the others — a clean\n figure/ground separation.\n\n display: inline-block is required for transform\n to apply (inline elements ignore transform).\n transformOrigin: 'center' so the glyph pivots\n around its centre instead of arcing from the\n baseline anchor. transition rides the existing\n Tailwind 4 transform/scale list (no new\n property — Tailwind already lists transform in\n the default transition-property set). 200ms\n matches the R202 chip bg-tint timing so the\n glyph lift and chip background ease in concert. */}\n {/* Round 369 / Loop: vendor letter glyph picks up\n fontWeight 600 (font-semibold). The glyph is the\n vendor identifier — the DATA the operator scans\n in this chip (A / O / 书 / C / G / ?). R333 set\n the count suffix `:N` to text-gray-400 + tabular-\n nums and (via parent inheritance) fw 500. Pre-\n R369 the LETTER also inherited fw 500 from the\n chip's font-medium — letter and count read at\n the same weight, contradicting the data-vs-label\n hierarchy the rest of the chip-row already speaks.\n R369 lifts the letter to fw 600 so the chip now\n reads as the same two-tier pattern R362 closed\n on the working / online / active-links chips:\n chip digit/letter fw 600 (data)\n chip unit/count fw 500 (label)\n Sibling treatment to R362 — extends the R333-R341\n chip-internal-hierarchy arc to the vendor-letter\n chip surface (9th surface family). R354 transform-\n scale-on-hover + R88 canvas-dim-others + R202\n chip bg color-mix all preserved on the same span.\n data-vendor-letter-glyph-font-weight attr exposes\n the value for tests. */}\n <span\n data-vendor-letter-glyph={v.initial}\n data-vendor-letter-glyph-hover={hoveredVendor === v.initial ? 'true' : 'false'}\n data-vendor-letter-glyph-font-weight=\"600\"\n style={{\n color: v.color,\n display: 'inline-block',\n fontWeight: 600,\n transform: hoveredVendor === v.initial ? 'scale(1.1)' : 'scale(1)',\n transformOrigin: 'center',\n transition: 'transform 200ms ease-out',\n }}\n >{v.initial}</span>\n {/* Round 333 / Loop: vendor count suffix `:{N}` joins\n the R317 subordinate-text-lift family (gray-500 →\n gray-400) plus picks up tabular-nums for digit\n width-lock. Pre-R333 a vendor whose count\n crossed 9→10 widened the suffix and (since the\n parent chip has `px-2.5` padding but no fixed\n width) shifted the chip-row's downstream chips\n a couple px right. Tabular-nums locks the slot;\n gray-400 lifts the digit into the band where eye\n reads it as \"deliberate subordinate metadata\"\n rather than near-invisible chrome. data-vendor-\n letter-count exposes the span for tests. */}\n {/* R417: count suffix opacity-70 + group-hover:\n opacity-100 brightens on chip hover. Inner-span\n hover-brighten family (3rd anchor) — sibling to\n R355 filter pill prefix/suffix and R414 chip-row\n unit. Effective shade at rest: text-gray-400 ×\n 70 % alpha; on hover: full gray-400. The label-\n tier-vs-glyph differentiation persists on hover\n since the glyph (R369 fw=600) stays at full\n opacity. R333 :{count} format preserved. */}\n <span\n className=\"text-gray-400 tabular-nums opacity-70 transition-opacity duration-200 group-hover:opacity-100\"\n data-vendor-letter-count-suffix\n >:{v.count}</span>\n </span>\n );\n })}\n </span>\n )}\n {/* Round 42 / Loop: extend active-links chip with the timestamp\n of the most-recent flow event. Tells the operator at a glance\n whether the topology is currently humming (last 30s) or has\n been quiet for a while — the visual flow particles and\n edge brightness only show that there IS traffic, not when\n the last one was. Reuses Round 38's relativeAgo. */}\n {(() => {\n const recent = flowLinks.reduce<number | null>((acc, l) => {\n if (!l.last_at) return acc;\n const t = parseHubTime(l.last_at);\n if (t === null) return acc;\n return acc === null || t > acc ? t : acc;\n }, null);\n const rel = recent !== null ? relativeAgo(new Date(recent).toISOString()) : null;\n // R114: tooltip lists the actual flows. Closes the\n // info-density sweep on the last chip-row hover surface\n // (R97-R113 covered everything else). Format:\n // \"alpha→beta (3), gamma→delta (1) — hover brightens all\"\n // Truncates at 6 flows with \"+N more\" so a busy fleet\n // doesn't paint a tall tooltip; the recent-signal panel\n // already shows the top 3 in detail.\n const flowList = flowLinks\n .slice(0, 6)\n .map(l => `${l.from}→${l.to} (${l.count})`)\n .join(', ');\n const flowSuffix = flowLinks.length > 6 ? ` + ${flowLinks.length - 6} more` : '';\n // R136: the chip already had cursor:pointer when flowLinks\n // > 0 (line 1877) — but no onClick was wired. The cursor\n // lied; users got the affordance signal with no follow-\n // through. Wire it to /messages, mirroring R133's footer-\n // nav idiom. Hover (R77) keeps its semantic \"preview all\n // flows on canvas\"; click is the action \"open the full\n // list\". Two distinct gestures, both meaningful. The\n // tooltip grows a \"click to open\" tail when interactive.\n // Drop the chip out of click territory entirely when\n // flowLinks is empty — no flows = no list to open.\n const isInteractive = flowLinks.length > 0;\n const tooltip = !isInteractive\n ? undefined\n : `${flowList}${flowSuffix} — hover brightens all · click to open /messages`;\n return (\n // Round 193 / Loop: the chip itself adopts a subtle cyan\n // tint on its own hover, mirroring the cyan flowEdge\n // highlight it fires on the canvas. Pre-R193 the gesture\n // was visually asymmetric:\n // hover this chip → canvas edges brighten (cyan)\n // chip itself → stays gray (silent)\n // The *cause element* (chip) gave no response while the\n // *effect element* (canvas edges) painted loud. Same\n // pin-mirror logic R165 / R180 use on the four other\n // chip-row surfaces — the chip and the canvas edge it\n // pins should speak the same color vocabulary. Tailwind\n // :hover variant cyan-500/10 bg + cyan-500/30 border +\n // cyan-200 text matches the same palette R178/R163 use\n // for the active chrome buttons. transition-colors\n // duration-200 blends the swap smoothly. Hover variant\n // only attaches when isInteractive — a chip showing\n // \"0 active links\" has no list to open and should\n // stay gray. data-active-links-clickable already\n // exposes that gate to tests.\n <span\n // Round 206 / Loop: extend the R204/R205 empty-recede\n // family to the active-links chip. Pre-R206 \"0 active\n // links\" rendered at the same bg-gray-500/10 + full\n // opacity as \"12 active links\" — same eye-no-signal\n // problem the legend count (R204) and working/online\n // chips (R205) just solved at their own grain levels.\n // Inline opacity 0.5 when !isInteractive (flowLinks=0)\n // joins R136's already-removed cursor + R114's tooltip-\n // text gate to give the empty state visual + interactive\n // + affordance signals in lockstep.\n // Tailwind transition-colors duration-200 on className\n // would be overridden by the inline transition list, so\n // we replicate color/bg/border transitions inline\n // alongside the new opacity 200ms — same splice idiom\n // R201 used on the working/online chips.\n /* Round 232 / Loop: tabular-nums on active-links chip\n (third chip in the row — matches working + online\n chip treatment so all three digits in the chip row\n stay width-stable across counter crossings). */\n /* Round 399 / Loop: active-links chip closes the 3-chip\n chip-row by extending R398's hover translateY(-1px)\n lift onto the third (rightmost) chip. The R398 family\n already covered working + online chips on the\n clickable variant; R399 adds the same gate (isInter-\n active = flowLinks.length > 0) so empty active-links\n stays planted at R206's opacity-50 receded paint.\n transition-transform + ease-out + transform-gpu join\n the inline transition list (different property axes\n compose cleanly: inline handles color/bg/border/\n opacity, className handles transform).\n Gesture-vocabulary table (post-R399 — now complete\n across the chip-row):\n working chip -1 px (R398)\n online chip -1 px (R398)\n active-links chip -1 px (R399, this round)\n filter pin pills -1 px (R397)\n recent-signal row -1 px (R143)\n legend row -1 px (R144)\n Every interactive chip in TopoGraph lifts on hover.\n data-chip-hover-lift attr exposes the lift surface\n state ('true' clickable, 'false' empty) for tests. */\n // R414: `group` parent + inner unit span group-hover-brighten — see working chip above.\n 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 ${\n isInteractive\n ? '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'\n : 'bg-gray-500/10 text-gray-400 border-gray-500/20'\n }`}\n data-chip-hover-lift={isInteractive ? 'true' : 'false'}\n data-chip-group-hover-brighten=\"true\"\n data-active-links-chip\n data-active-links-flow-count={flowLinks.length}\n data-active-links-clickable={isInteractive ? 'true' : 'false'}\n data-active-links-empty={isInteractive ? 'false' : 'true'}\n title={tooltip}\n role={isInteractive ? 'link' : undefined}\n tabIndex={isInteractive ? 0 : undefined}\n style={{\n cursor: isInteractive ? 'pointer' : undefined,\n opacity: isInteractive ? 1 : 0.5,\n transition: 'color 200ms ease-out, background-color 200ms ease-out, border-color 200ms ease-out, opacity 200ms ease-out',\n }}\n onMouseEnter={() => { if (isInteractive) setHoveredActiveLinks(true); }}\n onMouseLeave={() => setHoveredActiveLinks(false)}\n onClick={() => { if (isInteractive) router.push('/messages'); }}\n onKeyDown={(e) => {\n if (!isInteractive) return;\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n router.push('/messages');\n }\n }}\n >\n {/* R338 — active-links chip digit/unit split, completes\n the 5th chip surface in the R333/R335/R336/R337\n chip-internal-hierarchy arc. data-active-links-\n chip-unit exposes the unit span for tests. */}\n {/* R362 sibling — active-links chip digit gains font-semibold. */}\n <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>\n {rel ? (() => {\n // Round 161 / Loop: extend R160's recency-pip\n // vocabulary up one scope — from per-flow row to\n // fleet-aggregate chip. The chip already shows\n // recency in text (\"last 5s ago\"); the \" · \"\n // separator bullet was dead gray. Color the\n // bullet by freshness using the same alpha\n // ramp R160 uses on recent-signal rows:\n // ageSec ≤ 30 → 1.0 (fully fresh)\n // 30-300s → smooth decay 1.0 → 0.25\n // > 300s → 0.25 (stale floor)\n // Cyan bullet pulse when fresh + gray text tail\n // = \"is the network firing right now\" readable\n // at a glance, without parsing the timestamp\n // numerals. Same vocabulary the canvas uses\n // (R10 edge fade) and the recent-signal panel\n // uses (R160 row pip) — three nested scopes\n // now share one freshness ladder.\n const ageSec = recent !== null\n ? Math.max(0, (Date.now() - recent) / 1000)\n : 999;\n const alpha = ageSec <= 30\n ? 1\n : ageSec <= 300\n ? 1 - ((ageSec - 30) / 270) * 0.70 /* R358: floor 0.25 → 0.30 lift across 3 freshness scopes */\n : 0.30; /* R358: stale floor lifted 0.25 → 0.30 — 20% legibility bump while preserving fresh/stale ratio */\n // Cyan dark / teal light to match palette legendAccent.\n const dotColor = isLight\n ? `rgba(13, 148, 136, ${alpha.toFixed(2)})`\n : `rgba(34, 211, 238, ${alpha.toFixed(2)})`;\n // Round 342 / Loop: active-links chip freshness suffix\n // wrapper text-gray-500 → text-gray-400 (R317\n // subordinate-text-lift family applied to chrome\n // inactive Layout toggle + R333 vendor count suffix).\n // The \"last 5s ago\" suffix is chip-subordinate\n // metadata; gray-500 sat near-invisible against the\n // chip's outer color, gray-400 lifts it into the band\n // where the eye reads it as deliberate freshness\n // annotation. The freshness DOT keeps its own inline\n // color: dotColor — the lift only affects the trailing\n // literal \"last {rel}\" text.\n return (\n // Round 357 / Loop: active-links chip freshness\n // suffix wrapper picks up `tabular-nums` for digit\n // width-lock. Pre-R357 the literal \"last {rel}\"\n // text (e.g. \"last 5s ago\", \"last 10s ago\",\n // \"last 1m ago\") had natural-figure digits — the\n // freshness ticker updates every second, so the\n // 9→10 boundary on the seconds counter and the\n // 59→60s → 1m flip both jittered ~1-2 px of glyph\n // width which propagated through the chip-row's\n // inline-flex layout, nudging the freshness DOT\n // and the chip's left edge. Tabular-nums on the\n // wrapper applies to all descendant digits only\n // (letters render at natural widths) so the\n // ticker stays planted across every count cross.\n // Joins the R224-R232 info-density tabular-nums\n // sweep at the chip-row freshness scope. Pure\n // paint-level change, no geometry shift on rest.\n // The R342 text-gray-400 lift + R161 dot freshness\n // alpha ramp + R317 subordinate-text-lift family\n // all preserved. data-active-links-freshness-\n // wrapper attr exposes the wrapper for tests.\n <span className=\"text-gray-400 tabular-nums\" data-active-links-freshness-wrapper>\n <span\n data-active-links-freshness-dot\n data-active-links-freshness-alpha={alpha.toFixed(2)}\n style={{\n color: dotColor,\n fontWeight: alpha > 0.7 ? 700 : 400,\n transition: 'color 200ms ease-out',\n }}\n >{' · '}</span>\n last {rel}\n </span>\n );\n })() : null}\n </span>\n );\n })()}\n <FreshnessChip sessions={sessions} />\n </div>\n </div>\n\n <div\n ref={containerRef}\n /* Round 330 / Loop (milestone): canvas wrapper rounded-lg\n → rounded-xl (8px → 12px corner radius). The biggest\n single surface on the dashboard by pixel area now reads\n as modern-SaaS-contemporary rather than 2020-conservative\n — same 4px bump R197 applied to the legend swatch and\n R295 applied to the title-block crescent. R330 ports\n the gesture to the OUTER envelope.\n Inner content (SVG viewBox 1000×680) sits behind\n `overflow-hidden`, so corner-radius change only affects\n the wrapper's own paint area and the shadow contour;\n the topo-overlap-test reads SVG-internal geometry and is\n unaffected. R254 background-color / R254 border-color /\n R263 box-shadow transitions all carry through unchanged.\n Marks R330 milestone of 5 rounds (R326-R330) of layout-\n geometry polish (gap-tier + crescent fade + trailer\n compensator + corner radius). */\n className={`relative overflow-hidden rounded-xl border shadow-2xl ${isLight ? 'shadow-zinc-900/5' : 'shadow-cyan-950/30'} ${isFullscreen ? 'flex items-center justify-center' : ''}`}\n data-topo-wrapper\n /* Round 254 / Loop: top-level TopoGraph wrapper gains theme-\n toggle transition. This is the BIGGEST theme-driven surface\n on the dashboard by pixel area — pal.containerBg fills the\n entire visible canvas area (cyber #080814 ↔ light #ffffff),\n and pal.containerBorder rims it. Pre-R254 every inner\n element eased through theme but the outer wrapper hard-cut,\n visually anchoring the snap. R253 declared\n \"no visible snap remains\" prematurely — this wrapper was\n the largest holdout. 200ms ease-out matches the panel\n treatment (R247) so wrapper + panels ease as one unit.\n\n Round 263 / Loop: close R254's holdover gap — the wrapper's\n shadow-2xl + theme-conditional `shadow-{color}/{opacity}`\n Tailwind class (cyber `shadow-cyan-950/30` ↔ light\n `shadow-zinc-900/5`) ALSO changes on theme toggle, but the\n inline transition list only covered background-color +\n border-color. Result: every inner element eased through\n theme, the wrapper bg/border eased, but the wrapper's\n DROP-SHADOW snapped — a subtle but real holdover from\n R254's \"TRULY complete\" claim. Adding `box-shadow 200ms\n ease-out` to the transition list catches the className-\n driven box-shadow swap (CSS transition on box-shadow eases\n the shadow property even when its color comes from a\n Tailwind class change, because the property itself is\n transition-eligible regardless of source). */\n style={{\n background: pal.containerBg,\n borderColor: pal.containerBorder,\n transition: 'background-color 200ms ease-out, border-color 200ms ease-out, box-shadow 200ms ease-out',\n }}\n >\n {/* Round 265 / Loop: top-rail (1px-tall colored line at the top\n of the canvas wrapper) picks up theme-toggle transition.\n Pre-R265 the className `bg-gradient-to-r ${pal.topRail\n Gradient}` was theme-conditional — cyber `via-cyan-400/70`\n ↔ light `via-emerald-500/40` — but no inline transition,\n so the rail SNAPPED on theme flip while the wrapper bg\n (R254) + border (R254) + shadow (R263) all eased. The top-\n rail is the THIN BRIGHT LINE that visually anchors the\n canvas top edge — its hard color flip was a small but real\n theme-snap that broke the otherwise-eased canvas envelope.\n transition: background-image catches the className-driven\n gradient swap; Chrome ≥ 89 / Safari ≥ 14.1 / FF ≥ 96\n interpolate linear-gradients with matching stop structures\n (both gradients are `from-transparent via-X to-transparent`\n → same 3-stop layout). data-topo-top-rail makes the probe\n deterministic. */}\n <div\n className={`absolute inset-x-0 top-0 h-px bg-gradient-to-r ${pal.topRailGradient}`}\n data-topo-top-rail\n style={{ transition: 'background-image 200ms ease-out' }}\n />\n\n {/* Round 158 / Loop: give the canvas SVG itself an accessible\n name + role description. R151-R157 added a11y to every\n interactive surface inside the canvas (nodes, group labels,\n badges, rows, minimap, chrome buttons) — but the canvas\n container itself was a nameless 1000×680 box. A screen\n reader that hit the SVG before tab-diving into its\n children heard nothing identifying it as \"the topology\".\n aria-roledescription gives it a meaningful announcement;\n aria-label provides a live snapshot of the network\n (online / working / active links / offline) plus the two\n canvas-scope gestures (Tab + double-click). Default SVG\n role is graphics-document so children remain navigable —\n we deliberately don't override role with \"img\" which\n would flatten the SVG to a single opaque image. */}\n <svg\n ref={svgRef}\n viewBox=\"0 0 1000 680\"\n className=\"w-full h-auto block\"\n preserveAspectRatio=\"xMidYMid meet\"\n aria-roledescription=\"agent network topology\"\n aria-label={(() => {\n const online = onlineNodes.length;\n const working = workingCount;\n const offline = offlineNodes.length;\n const flows = flowLinks.length;\n const parts: string[] = [];\n parts.push(`${online} agent${online === 1 ? '' : 's'} online`);\n if (working > 0) parts.push(`${working} working`);\n if (offline > 0) parts.push(`${offline} offline`);\n parts.push(`${flows} active link${flows === 1 ? '' : 's'}`);\n return `Agent network topology — ${parts.join(' · ')}. Tab to navigate nodes, double-click canvas to reset view.`;\n })()}\n data-topo-canvas-aria\n /* Round 469 / Loop — fleet-split numeric attrs on the root\n svg. The aria-label already encodes online/working/offline\n /flow counts in text form (R7 origin) but DOM probes had\n to PARSE the label string to extract the numbers. R469\n surfaces them as 4 numeric data-attrs alongside the R462\n dashboard-version + R466 any-hover + R467 any-pinned set\n that already live on the root svg:\n data-topo-online-count total online sessions\n data-topo-working-count subset currently working\n data-topo-offline-count offline / ghost-purged\n data-topo-flow-count active flow links\n Use cases:\n - Playwright: one-line `svg.getAttribute('data-topo-\n working-count')` instead of parsing aria-label\n - external CSS: data-attribute selectors for empty\n vs populated states (`[data-topo-online-count='0']`)\n - a11y enrichment: screen-reader scripts can read the\n numeric attrs directly\n - hub-aria parity: the hub-center text already shows\n `workingCount` digit (R130); R469 puts the same scalar\n on the canvas root for non-visual consumers.\n Composed from existing onlineNodes / workingCount /\n offlineNodes / flowLinks — no new state. */\n data-topo-online-count={onlineNodes.length}\n data-topo-working-count={workingCount}\n data-topo-offline-count={offlineNodes.length}\n data-topo-flow-count={flowLinks.length}\n /* Round 471 / Loop — surface 2 remaining canvas-level mode\n attrs alongside the R462/R466/R467/R469 set. Pre-R471 the\n root svg exposed 7 attrs but tests probing \"what layout\n is active\" had to query DOM internals (data-topo-chrome-\n layout-active on the chrome button row) or parse the URL\n for theme. R471 puts both modes on the root for one-stop\n snapshot reads:\n data-topo-layout — 'ring' | 'grid'\n data-topo-theme — 'cyber' | 'light'\n Together with R469 the canvas root now carries 9 cross-\n cutting attrs (1 build identity + 2 inspection mode + 4\n fleet split + 2 layout/theme). Test harness can read the\n FULL canvas state with 9 getAttribute calls; no traversal\n into chrome strip / theme provider / panel rows.\n Composed from existing `layout` (R138 ring↔grid toggle\n state) + `isLight` (R12 theme palette gate) — no new\n state, zero re-render cost. */\n data-topo-layout={layout}\n data-topo-theme={isLight ? 'light' : 'cyber'}\n /* Round 487 / Loop — extends R469/R471 root-svg state surface\n with current zoom level (numeric attr, 2 decimals). Pre-\n R487 the canvas zoom was queryable via `data-topo-minimap-\n viewport-glow='true'` boolean (R481, gated at > 1.5) but\n the exact zoom number only lived in the chrome-strip span\n (`{Math.round(view.zoom * 100)}%`). Tests + external CSS\n that need the zoom value had to traverse to the chrome\n strip or read view state via React internals.\n R487 surfaces it at the canvas root, consistent with\n R469's fleet-count numeric pattern. Two-decimal precision\n matches the internal `view.zoom` float without losing\n info. Composed from existing state — no new state.\n Root svg attribute set now 10 attrs total:\n R462 data-dashboard-version build identity\n R466 data-topo-any-hover transient mode\n R467 data-topo-any-pinned sticky mode\n R469 data-topo-online-count fleet (4 numeric)\n R469 data-topo-working-count\n R469 data-topo-offline-count\n R469 data-topo-flow-count\n R471 data-topo-layout canvas mode\n R471 data-topo-theme canvas mode\n R487 data-topo-zoom canvas zoom */\n data-topo-zoom={view.zoom.toFixed(2)}\n /* Round 488 / Loop — pairs the R466 hover-aggregate BOOLEAN\n with the corresponding hover IDENTITY attr. Pre-R488 a\n test harness could query \"is anything hovered\" but had to\n traverse per-node `data-node` elements with focus-state\n attrs to recover WHICH alias. R488 surfaces it directly\n at canvas root. Empty string when null (always-present\n attr, consistent with the 10-attr state-surface set —\n never `undefined`-collapsed so observers can rely on a\n single `getAttribute('data-topo-hovered-alias')` returning\n either '' or the alias string).\n Note: only the `hoveredAlias` axis (R466's first source)\n gets the identity twin in R488. The other 5 hover sources\n (hoveredHub / hoveredEdgeKey / hoveredGroupLabel / hovered\n Status / hoveredVendor) are non-alias-shaped (hub center\n is singleton; edge has `from→to` key; status/vendor are\n categorical) — separate dedicated attrs if/when needed.\n Root svg attribute set now 11 attrs total. */\n data-topo-hovered-alias={hoveredAlias ?? ''}\n /* Round 466 / Loop — aggregate hover signal on the root SVG.\n Exposes a single boolean `data-topo-any-hover` that\n reflects whether ANY hover state in the topology is\n active. Composed from the existing per-surface hover\n vars; doesn't introduce new state. Useful for:\n - Playwright tests asserting \"topology entered a hover\n mode\" without enumerating per-surface attrs\n - external CSS hooks targeting `[data-topo-any-hover=\n \"true\"]` to dim adjacent UI (e.g. chrome strip)\n while the user is inspecting the canvas\n - debug overlays that visualise hover dwell-time\n The 6 hover sources contributing:\n hoveredAlias (node circle / card / alias text)\n hoveredHub (hub center, halo, ring)\n hoveredEdgeKey (flow link path / particle / endpoint)\n hoveredGroupLabel (cluster name / count / pips)\n hoveredStatus (legend row)\n hoveredVendor (vendor chip in chip row)\n Read-only computed attr — zero re-render cost beyond the\n React update that already fires when any of those state\n vars flips. Geometry / paint untouched. */\n data-topo-any-hover={\n (hoveredAlias || hoveredHub || hoveredEdgeKey || hoveredGroupLabel ||\n hoveredStatus || hoveredVendor) ? 'true' : 'false'\n }\n /* Round 467 / Loop — pin-aggregate sibling to R466 hover-\n aggregate. Exposes `data-topo-any-pinned` reflecting\n whether ANY sticky inspection mode is active. Composed\n from the 4 pinned state vars:\n pinnedStatus (legend row click → status filter)\n pinnedGroup (group label click → cluster lock)\n pinnedVendor (vendor chip click → vendor filter)\n pinnedEdgeKey (edge click → edge focus)\n Together with R466 the root svg now carries a 2-bit\n inspection-mode surface:\n data-topo-any-hover — transient (mouse hover)\n data-topo-any-pinned — sticky (click-to-lock)\n Useful for:\n - Playwright tests: one-line query for either mode\n - external CSS hooks: render a persistent \"filter\n active\" badge when pinned, distinct from the\n transient hover dim\n - Esc-handler tests: assert all 4 pins clear after\n the universal-cancel Escape press (R62/R63/R88/\n R116 — single Esc collapses every pin)\n Read-only computed disjunction; no new state, zero\n re-render cost beyond the React pin-flip updates. */\n data-topo-any-pinned={\n (pinnedStatus || pinnedGroup || pinnedVendor || pinnedEdgeKey) ? 'true' : 'false'\n }\n /* Round 462 / Loop — surface DASHBOARD_VERSION on the root SVG\n element as `data-dashboard-version`. Directly closes the\n feedback_dash_zombie_port_3000.md memory rule: \"verify ships\n via SVG DOM, not tmux 'Ready' — zombie next-servers + stale\n global installs silently serve old code\". Pre-R462 the only\n ways to know which preview the dash was serving were:\n 1. parse the npm registry for the latest tag (network)\n 2. fetch /api/dashboard/version (API surface, no DOM)\n 3. inspect the /login footer or /settings page (off-route)\n Test scripts that probe TopoGraph DOM (overlap, group-label\n tint, pip strip, etc.) couldn't tell whether the dash was\n actually serving the build they expected to verify. R462\n threads DASHBOARD_VERSION through to the root <svg> so:\n - Playwright probes can read svg[data-dashboard-version]\n directly + fail-fast on stale-build mismatch\n - the memory rule's manual zombie check (\"inspect SVG\n dom\") becomes a one-attr probe\n - operators DOM-inspect to confirm the live version\n matches the npm tag without leaving the topology page\n Geometry/visual impact: ZERO (data-* attrs don't paint).\n The version string is build-time injected via the existing\n DASHBOARD_VERSION constant (R51 footer + R51 settings page\n already consume it from app/lib/version.ts → reads\n package.json pkg.version). No business logic added. */\n data-dashboard-version={DASHBOARD_VERSION}\n onPointerDown={onPointerDown}\n onPointerMove={onPointerMove}\n onPointerUp={onPointerUp}\n onPointerLeave={onPointerUp}\n // Round 41 / Loop: the reset-button title (R22) and the Help\n // overlay (R25) both advertise \"double-click the canvas to\n // reset\", but the handler was never actually wired. The text\n // was lying. Wire it here, guarded so dbl-clicking a node\n // (which would also trigger the SVG-level handler via event\n // bubbling) doesn't unexpectedly reset the view on the user.\n onDoubleClick={(e) => {\n const t = e.target as Element | null;\n if (t?.closest('g[data-node]')) return;\n resetView();\n }}\n style={{ cursor: isPanning ? 'grabbing' : 'grab', touchAction: 'none' }}\n >\n <defs>\n <linearGradient id=\"topo-panel\" x1=\"0\" x2=\"1\" y1=\"0\" y2=\"1\">\n <stop offset=\"0%\" stopColor={pal.panelStops[0]} />\n <stop offset=\"48%\" stopColor={pal.panelStops[1]} />\n <stop offset=\"100%\" stopColor={pal.panelStops[2]} />\n </linearGradient>\n <radialGradient id=\"topo-radar\" cx=\"50%\" cy=\"50%\" r=\"55%\">\n <stop offset=\"0%\" stopColor={pal.radarStops[0].color} stopOpacity={pal.radarStops[0].opacity} />\n <stop offset=\"45%\" stopColor={pal.radarStops[1].color} stopOpacity={pal.radarStops[1].opacity} />\n <stop offset=\"100%\" stopColor={pal.radarStops[2].color} stopOpacity={pal.radarStops[2].opacity} />\n </radialGradient>\n {!isLight && (\n <filter id=\"topo-glow\">\n <feGaussianBlur stdDeviation=\"4\" result=\"blur\" />\n <feMerge>\n <feMergeNode in=\"blur\" />\n <feMergeNode in=\"SourceGraphic\" />\n </feMerge>\n </filter>\n )}\n {/* R142 / Loop: drop-shadow filter for group-box hover-lift.\n Mirrors R135's panel hover-elevation idiom but applied\n at the per-group canvas level. R68 already gives a\n hovered/pinned group box a solid accent stroke; R142\n adds a soft outward shadow on top so the box visually\n \"rises off the canvas\" when selected — same visual\n vocabulary as the panels + the Overview KPI cards\n (R18). Theme-aware flood: darker shadow on cyber so\n it reads above the dark canvas, lighter for light\n theme. Bounding box of the group box is unchanged —\n filters only affect paint area, not bbox geometry, so\n the overlap-test invariant is preserved. */}\n <filter id=\"topo-groupbox-lift\" x=\"-10%\" y=\"-10%\" width=\"120%\" height=\"120%\">\n <feDropShadow\n dx=\"0\" dy=\"3\" stdDeviation=\"4\"\n floodColor={isLight ? '#0f172a' : '#000000'}\n floodOpacity={isLight ? 0.18 : 0.55}\n />\n </filter>\n {/* Round 16 / Loop: 3-tier flow-link arrow markers.\n The single marker had `markerUnits` defaulting to\n `strokeWidth`, so heavy edges (stroke=7) rendered 35-user-\n unit arrowheads — visually dominant, the head outweighed\n the line. Switching to `userSpaceOnUse` decouples arrow\n size from stroke; binning by count gives a clearer\n hierarchy than continuous linear scaling:\n s (count 1-2) → 12 user units\n m (count 3-4) → 16 user units (alias for `topo-arrow`)\n l (count 5+) → 22 user units\n `topo-arrow` stays bound to the medium tier so the legend\n swatch (line ~1500) renders without change. */}\n {[\n { id: 'topo-arrow-s', size: 12 },\n { id: 'topo-arrow', size: 16 },\n { id: 'topo-arrow-l', size: 22 },\n ].map(m => (\n <marker\n key={m.id}\n id={m.id}\n viewBox=\"0 0 10 10\"\n refX=\"8\"\n refY=\"5\"\n markerWidth={m.size}\n markerHeight={m.size}\n markerUnits=\"userSpaceOnUse\"\n orient=\"auto-start-reverse\"\n >\n <path d=\"M 0 0 L 10 5 L 0 10 z\" fill={pal.arrowFill} />\n </marker>\n ))}\n {/* Round 45: radial radar sweep gradient — bright at center\n (where it meets the hub) fading to leading edge. */}\n <radialGradient id=\"topo-sweep\" cx=\"0%\" cy=\"50%\" r=\"100%\">\n <stop offset=\"0%\" stopColor={isLight ? '#0d9488' : '#22d3ee'} stopOpacity={isLight ? 0.18 : 0.32} />\n <stop offset=\"70%\" stopColor={isLight ? '#0d9488' : '#22d3ee'} stopOpacity={isLight ? 0.10 : 0.18} />\n <stop offset=\"100%\" stopColor={isLight ? '#0d9488' : '#22d3ee'} stopOpacity=\"0\" />\n </radialGradient>\n </defs>\n\n {/* panel backdrop stays fixed — panning never reveals empty canvas */}\n <rect width=\"1000\" height=\"680\" fill=\"url(#topo-panel)\" />\n\n {/* Round 103 (issue #81): everything inside this <g> zooms + pans\n together. transform order = translate then scale.\n Round 168 / Loop: smoothView arms a one-shot transition on\n the transform attribute, active only when resetView/fitView\n fires. Pan (R103 pointer drag) and wheel zoom never set the\n flag, so they stay snappy with no lag. Pressing `0`, `f`,\n clicking the hub (R52), or chrome reset/fit buttons triggers\n a 300ms ease-out glide instead of a jolt. Respects prefers-\n reduced-motion via the R29 globals.css blanket override that\n neutralises transition-duration universally. */}\n <g\n transform={`translate(${view.x} ${view.y}) scale(${view.zoom})`}\n data-topo-viewport\n data-topo-viewport-smooth={smoothView ? 'true' : 'false'}\n data-topo-viewport-layout-switching={layoutSwitching ? 'true' : 'false'}\n data-topo-viewport-nodesize-switching={nodeSizeSwitching ? 'true' : 'false'}\n style={{\n // R170 / Loop: opacity always carries a transition so the\n // layout-switch crossfade fires cleanly. R168 smoothView\n // transition on transform is added only when armed (pan\n // and wheel zoom MUST stay snappy). The two arming flags\n // compose without conflict — different visual axes.\n // R171: nodeSizeSwitching shares the same opacity-dim\n // pathway as layoutSwitching — clicking S/M/L in the\n // chrome triggers the same soft-blink masking. ORing\n // both flags keeps the opacity expression simple while\n // the two distinct data-* attributes let tests\n // disambiguate which gesture armed the crossfade.\n transition: smoothView\n ? 'opacity 250ms ease-out, transform 300ms ease-out'\n : 'opacity 250ms ease-out',\n opacity: (layoutSwitching || nodeSizeSwitching) ? 0.45 : 1,\n }}\n >\n {/* Issue #87: radar/ring ambiance renders only in ring layout —\n grid mode drops it so the concentric rings don't sit behind a\n rectangular grid. */}\n {layout === 'ring' && (<>\n {/* R52: radar bg is pure decoration — drop its pointer events so\n the hub <g> under it (and any future inner-disk affordances)\n can receive clicks. Previously the r=330 disc intercepted\n the hub click outright. */}\n <circle cx={cx} cy={cy} r=\"330\" fill=\"url(#topo-radar)\" style={{ pointerEvents: 'none' }} />\n\n {/* Round 45: subtle star field — deterministic dots scattered\n across the canvas give the radar bg some depth. Skipped on\n light theme so the white surface stays clean.\n Round 291 / Loop: starfield dot count 28 → 14 (50%\n reduction). Post-R290 inner radar ring retirement the\n canvas has cleared meaningfully — sweep + 3 radar rings\n + tier guides + nodes + edges are doing the visual work.\n The starfield's role is atmospheric depth, not\n information; cutting density by half preserves the\n \"space/radar\" feel while removing decoration the eye\n has to skip. Same R275-R281 减法 family idiom as the\n orbit / halo / spoke retirements; same R290 pivot back\n to subtractive register. data-topo-starfield-dot\n attribute makes the dots probe-able for the regression\n test. */}\n {!isLight && (\n <g opacity=\"0.5\" style={{ pointerEvents: 'none' }} data-topo-starfield>\n {Array.from({ length: 14 }).map((_, i) => {\n // Deterministic pseudo-random scatter so positions are\n // stable between renders (no JS hydration mismatch).\n const seed = i * 9301 + 49297;\n const x = ((seed * 13) % 1000);\n const y = ((seed * 7) % 680);\n const r = (i % 3 === 0) ? 1.2 : 0.7;\n return <circle key={i} cx={x} cy={y} r={r} fill=\"#a5b4fc\" opacity={0.35 + (i % 4) * 0.05} data-topo-starfield-dot={i} />;\n })}\n </g>\n )}\n\n {/* Round 45: rotating radar sweep — a 40° wedge with a soft\n leading-edge gradient. Slow 6s rotation reads as a radar\n scan without being noisy. Inline transform-origin on the\n <g> wrapper ensures Chrome / Firefox rotate around (cx,cy)\n instead of the SVG viewBox corner.\n\n v0.10.0 Hero 3 Wave 1 / RFC §3.B (Vincent 5222 holdover):\n sweep arc retired. The diagonal rotating wedge competes\n with working-halo SMIL, hub busyness breath, and edge\n flow animation — on a 16:9 Twitter screenshot it reads\n 'wow lots of motion' rather than 'agents communicating'.\n Same idiom as R278/R279/R280 retirements — `false &&`\n short-circuits the IIFE so it's a one-line rollback. */}\n {false && (() => {\n // R146: radar sweep rotation buckets on workingCount, joining\n // R84 hub breath / R131 outer orbit / R132 group march /\n // R145 idle spokes as the 5th and final layer in the busyness-\n // driven motion family. 8 / 6 / 4 / 3 seconds — same 0 / 1-2 /\n // 3-5 / 6+ thresholds R84 uses. \"Busier fleet = more frequent\n // scans\" feels semantically right for a radar idiom. R45\n // baseline 6s sits at bucket 1 so the calm/busy spread bracks\n // around the historical default.\n const busy = workingCount === 0 ? 0\n : workingCount <= 2 ? 1\n : workingCount <= 5 ? 2\n : 3;\n const sweepDur = [8, 6, 4, 3][busy];\n return (\n <g\n style={{\n transformOrigin: `${cx}px ${cy}px`,\n transformBox: 'view-box',\n pointerEvents: 'none',\n // CSS var consumed by `.anet-topo-sweep` (line 848 of\n // globals.css). React's CSSProperties type doesn't model\n // custom properties → cast through Record<string, string>.\n ...({ ['--sweep-dur']: `${sweepDur}s` } as Record<string, string>),\n } as React.CSSProperties}\n className=\"anet-topo-sweep\"\n opacity={isLight ? 0.7 : 1}\n data-topo-sweep-bucket={busy}\n data-topo-sweep-dur={sweepDur}\n >\n <path\n d={`M ${cx} ${cy} L ${cx + 330} ${cy} A 330 330 0 0 0 ${cx + 330 * Math.cos(-Math.PI / 4.5)} ${cy + 330 * Math.sin(-Math.PI / 4.5)} Z`}\n fill=\"url(#topo-sweep)\"\n />\n </g>\n );\n })()}\n\n {/* radar rings — pure decoration at fixed radii, independent of\n node positions so the radar aesthetic is preserved across tier\n changes.\n Round 290 / Loop: drop the innermost radar ring at r=90.\n That ring sat ~66px outside the hub (hub radius 24, halo\n r=18), in the exact zone R276 (orbit particles) / R278\n (working halo) / R280 (backdrop spokes) cleared during\n the R275-R281 减法 arc. Post-cleanup the lone r=90 ring\n read as a leftover decorative loop hugging the hub — a\n visual element with no remaining sibling to anchor.\n Dropping it returns to the subtractive register after\n R282-R289's 8 加法 rounds and lets the hub breathe. The\n outer three rings (170 / 250 / 330) still carry the\n radar aesthetic across the canvas. New data-topo-radar-\n ring attribute exposes each remaining ring radius for\n test probing. */}\n {[170, 250, 330].map(radius => (\n <circle\n key={radius}\n cx={cx} cy={cy} r={radius}\n fill=\"none\" stroke={pal.ringStroke} strokeWidth=\"1\"\n opacity={isLight ? 0.6 : 0.35}\n data-topo-radar-ring={radius}\n />\n ))}\n\n {/* Round 54 / Loop: tier-radius guide rings. The radar rings above\n are decorative and don't match the actual tier radii nodes sit\n on (single 220 / dual 175,260 / triple 145,215,285). Drawing\n a faint dashed ring at each ACTIVE tier radius lets the eye\n anchor \"this is the inner / outer ring\" without inferring from\n node spacing. Picked based on online node count so only the\n tiers currently in use draw — empty tiers stay quiet. pointer-\n events:none so they never intercept hub or node clicks. The\n 0.7 stroke + dashed pattern reads as guide, not feature. */}\n {(() => {\n const tierRadii = onlineNodes.length > onlineTripleThreshold\n ? [onlineTripleInnerR, onlineTripleMidR, onlineTripleOuterR]\n : onlineNodes.length > onlineTierThreshold\n ? [onlineInnerRadius, onlineOuterRadius]\n : onlineNodes.length > 0\n ? [onlineRadius]\n : [];\n // Round 92 / Loop: tier-ring occupancy. R54 drew the guide\n // rings at fixed opacity regardless of how many nodes lived\n // on each tier. With pinned filters dimming most nodes to\n // 0.28, the ring at a deserted tier looked identical to a\n // crowded one — wasting a free piece of canvas. Count\n // online nodes whose hub-distance falls within ±15 px of\n // each ring (15 px = half the inter-tier gap, so each\n // node is assigned to exactly one ring). Empty tier → skip\n // entirely. Crowded tier → stronger opacity, says \"look\n // here\". Buckets so the ladder feels intentional, not\n // jittery as one node migrates between tiers.\n const occupancyOf = (r: number) => onlineNodes.reduce((acc, s) => {\n const p = nodePositions[s.alias];\n if (!p) return acc;\n const d = Math.hypot(p.x - cx, p.y - cy);\n return Math.abs(d - r) < 15 ? acc + 1 : acc;\n }, 0);\n // Round 93 / Loop: when any pin is active, tint the tier\n // rings to the legend accent so the spatial guide visually\n // shares the canvas's \"filtered mode\" colour. The chip\n // row already says WHICH filter is on (pills + letter\n // mirror); rings answering \"we're filtered\" reinforces\n // the state when the eye is on the canvas, not the chip\n // row. Composes cleanly with R92 occupancy — same opacity\n // bucket logic; just the stroke colour swaps.\n const anyPin = !!(pinnedStatus || pinnedGroup || pinnedVendor);\n const tierStroke = anyPin ? pal.legendAccent : pal.ringStroke;\n return tierRadii.map((r, tierIdx) => {\n const n = occupancyOf(r);\n if (n === 0) return null;\n const bucket = n <= 2 ? 0 : n <= 6 ? 1 : 2;\n const opLight = [0.24, 0.36, 0.50][bucket];\n const opDark = [0.32, 0.46, 0.62][bucket];\n // Round 174 / Loop: tier guide rings fade-in alongside\n // the R9/R72/R172/R173 first-paint wave. Ring layout's\n // structural scaffolding (R54 dashed concentric guides)\n // was the last instant-pop element after R173 closed\n // group boxes in grid. Same vocabulary — .anet-fade-in\n // mount-once CSS animation + per-ring stagger 60ms ×\n // tierIdx (cap 8). Tier rings are at most 3 (single /\n // dual / triple), so the visible range is 0-120ms.\n // Inner ring leads outward — emanates from the hub.\n // transition list grows `opacity 250ms ease-out` so the\n // post-animation snap from animation's end state (1) to\n // the bucket opacity (0.24-0.62) eases instead of cuts.\n // Same pattern node fade-in uses (R9 + transition-opacity\n // from R3 className). data-tier-fade-delay exposes the\n // computed delay for test probing.\n const fadeDelay = Math.min(tierIdx, 8) * 60;\n return (\n <circle\n key={`tier-${r}`}\n cx={cx} cy={cy} r={r}\n fill=\"none\"\n stroke={tierStroke}\n strokeWidth=\"0.7\"\n /* Round 303 / Loop: tier guide dashes tighten from\n \"2 8\" → \"2 6\" (8px gap → 6px gap). R54 set \"2 8\"\n to read as a faint hint behind everything else;\n after R290 (inner radar ring retired) + R291\n (starfield 50%) cleared the surrounding backdrop\n density, the tier guides carry more \"this is the\n ring nodes sit on\" visual responsibility. Pulling\n the gap from 8→6 puts dashes closer together so\n the ring reads as a clearer continuous mark\n rather than scattered dots, without bumping\n strokeWidth (0.7) or opacity (R92 bucketed). The\n 2px dash itself unchanged — same density signal\n per dash, just fewer-px space between them. */\n strokeDasharray=\"2 6\"\n opacity={isLight ? opLight : opDark}\n className=\"anet-fade-in\"\n style={{\n pointerEvents: 'none',\n transition: 'stroke 200ms ease-out, opacity 250ms ease-out',\n animationDelay: `${fadeDelay}ms`,\n }}\n data-tier-ring={r}\n data-tier-occupancy={n}\n data-tier-bucket={bucket}\n data-tier-tinted={anyPin ? 'true' : 'false'}\n data-tier-fade-delay={fadeDelay}\n />\n );\n });\n })()}\n\n {/* Round 50: 4 small particles slowly orbiting the outer ring\n (r=330). Each starts at a different angle (offset 0/0.25/0.5/0.75\n of the cycle) so they're evenly spaced. 16s per revolution is\n slow enough to feel ambient, not noisy. Skipped on light theme\n so the white surface stays clean.\n\n R131 / Loop: orbit period now buckets on workingCount,\n mirroring R84's hub-busyness breath cadence. An idle\n fleet keeps the original 16s \"calm sweep\"; as work\n accumulates the orbit subtly accelerates (capped at\n 10s so it never feels frantic). Two-layer motion\n coordination now: R84 breathes the hub, R131 spins\n the outer ring — both reading the same underlying\n \"is the network busy\" signal, both visible\n simultaneously without competing. Same bucket\n thresholds (0 / 1-2 / 3-5 / 6+) the R84 block uses\n at line ~2702 so the two cadences stay in sync if\n a future refactor shifts buckets.\n\n Round 276 / Loop: orbit particles DISABLED by default\n per Vincent 5214/5215-5217 visual-audit relay\n (clutter cleanup for Twitter screenshot). The 4\n particles encode workingCount busyness via speed\n (R131) + opacity (R216) — but that signal is\n ALREADY conveyed by:\n · hub halo opacity breath (R244, R84)\n · hub digit workingCount text (R130)\n · pressure-bar working/idle/offline ratio (R31)\n So orbit particles are info-redundant decoration:\n they don't add new signal, just add visual noise at\n the canvas outer edge. R276 gates the render block\n with `false &&` so the code stays (commented context\n + tests preserved for hypothetical rollback) but\n nothing renders. R131 busy-bucket constant + R216\n opacity-bucket constant are dead code post-R276 —\n acceptable since the family was R50/R131/R216 and\n R276 retires the family entirely. Net: 4 fewer\n moving dots on canvas. */}\n {false && !isLight && (() => {\n const busy = workingCount === 0 ? 0\n : workingCount <= 2 ? 1\n : workingCount <= 5 ? 2\n : 3;\n const dur = [16, 14, 12, 10][busy];\n // Round 216 / Loop: orbit particle opacity scales with busy\n // bucket alongside R131's speed scaling. Pre-R216 the\n // particles sat at flat opacity 0.9 regardless of fleet\n // workload — speed conveyed busyness but brightness was\n // mute. R216 layers brightness on top of speed so idle\n // fleets read calm (dim particles) and busy fleets read\n // bright (loud particles). Same R84 hub-breath /\n // R131 orbit-speed bucket thresholds (0 / 1-2 / 3-5 / 6+)\n // so the three motion layers (hub breath / orbit / group\n // march) plus the new brightness channel all derive from\n // one busyness metric. transition: opacity 300ms ease-out\n // matches R167 status-flip + R213 hub crossfade timing —\n // when first working node appears, hub focal point AND\n // outer ring particles brighten on the same 300ms beat.\n const orbitOpacity = [0.5, 0.7, 0.85, 1.0][busy];\n return [0, 0.25, 0.5, 0.75].map((phase, i) => (\n <g\n key={`orbit-${i}`}\n data-topo-orbit-bucket={busy}\n data-topo-orbit-dur={dur}\n data-topo-orbit-opacity={orbitOpacity}\n >\n <circle\n cx={cx + 330} cy={cy}\n r={i === 0 ? 2.8 : 2.2}\n fill=\"#22d3ee\"\n opacity={orbitOpacity}\n filter=\"url(#topo-glow)\"\n style={{ transition: 'opacity 300ms ease-out' }}\n >\n <animateTransform\n attributeName=\"transform\"\n type=\"rotate\"\n from={`${phase * 360} ${cx} ${cy}`}\n to={`${phase * 360 + 360} ${cx} ${cy}`}\n dur={`${dur}s`}\n repeatCount=\"indefinite\"\n />\n </circle>\n </g>\n ));\n })()}\n\n {/* Round 240 / Loop: extend R93 anyPin tinting from tier-rings\n to backdrop spokes. Pre-R240 the 6 radar-style spokes\n stayed at pal.ringStroke regardless of filter state,\n while R93 already shifted tier-rings to pal.legendAccent\n on any active pin. Result: ring scaffolding said\n 'filtered mode' but spoke scaffolding said 'rest' — the\n two halves of the canvas's spatial guide were out of\n sync. R240 ties them together; whole backdrop now reads\n as one filtered-mode-coloured unit when a pin is active.\n\n Same anyPin signal R93 uses (pinnedStatus || pinnedGroup\n || pinnedVendor). Same legendAccent tint colour. Same\n 200ms ease-out transition timing — pin a status, both\n tier-rings AND spokes ease to cyan together. */}\n {/* Round 280 / Loop: backdrop spokes RETIRED (R93 family with\n R240 tinting) per 减法 cut #6. The 6 radial lines at\n every 30° formed 12 rays from canvas center — even at\n opacity 0.18 (cyber) / 0.35 (light) they added explicit\n radial-line clutter behind the hub-and-spoke topology.\n The radial-gradient backdrop (topo-radar) ALREADY\n provides soft hub-centered glow; explicit lines on top\n were decorative density without structural signal that\n the topology itself doesn't already convey (hub at\n center + nodes on rings = radial structure inherent).\n `false &&` gates the render; code preserved for\n rollback. Same idiom as R276 orbit / R278 working halo\n / R279 ping+pulse retirements. */}\n {false && (() => {\n const anyPin = !!(pinnedStatus || pinnedGroup || pinnedVendor);\n const spokeStroke = anyPin ? pal.legendAccent : pal.ringStroke;\n return [0, 30, 60, 90, 120, 150].map(angle => (\n <line\n key={angle}\n x1={cx - 360 * Math.cos(angle * Math.PI / 180)}\n y1={cy - 360 * Math.sin(angle * Math.PI / 180)}\n x2={cx + 360 * Math.cos(angle * Math.PI / 180)}\n y2={cy + 360 * Math.sin(angle * Math.PI / 180)}\n stroke={spokeStroke}\n strokeWidth=\"1\"\n opacity={isLight ? 0.35 : 0.18}\n data-topo-spoke-angle={angle}\n data-topo-spoke-tinted={anyPin ? 'true' : 'false'}\n style={{ transition: 'stroke 200ms ease-out' }}\n />\n ));\n })()}\n </>)}\n\n {/* hub links — round 46: idle spokes now have animated\n stroke-dashoffset so dashes flow outward from the hub\n (\"command relay\" feel). Active spokes carrying live message\n flow stay as solid bright strokes.\n R145 / Loop: idle-spoke animation cadence buckets on\n workingCount, mirroring R84 hub-breath / R131 outer-\n ring orbit / R132 groupbox-march coordination. Same\n 0 / 1-2 / 3-5 / 6+ thresholds. Idle ↔ idle network\n has a slow \"command relay\" feel; as work accumulates\n the dashes accelerate outward, completing the 4th\n motion layer in the busyness-driven family:\n R84 hub breath (centre)\n R131 outer ring orbit (periphery)\n R132 groupbox march (per-team, grid layout)\n R145 idle-spoke flow (ring layout, hub→nodes)\n 4 surfaces, 1 signal. Same cadence ladder 2.8/2.4/2.0/\n 1.6s — 1.75× range from calm to busy, capped so the\n network never feels frantic. */}\n {layout === 'ring' && (() => {\n const busy = workingCount === 0 ? 0\n : workingCount <= 2 ? 1\n : workingCount <= 5 ? 2\n : 3;\n const spokeDur = [2.8, 2.4, 2.0, 1.6][busy];\n return onlineNodes.map((session, idx) => {\n const pos = nodePositions[session.alias];\n if (!pos) return null;\n const path = curvePath({ x: cx, y: cy }, pos, 0);\n const isActiveSpoke = activeAliases.has(session.alias);\n\n /* Round 241 / Loop: hub-link spokes (agent→hub paths in\n ring layout) eased state-flip between idle and active.\n Pre-R241 when a node sent or received a message its\n hub-spoke jumped one-frame from idle gray (pal.spoke-\n Stroke.idle + strokeWidth=1 + opacity=0.45) to active\n cyan (pal.spokeStroke.active + strokeWidth=2 + opacity\n =0.7) — three discrete property snaps in lockstep.\n R241 adds a 250ms ease-out transition list covering\n stroke + stroke-width + opacity so the activation\n 'lights up' smoothly. strokeDasharray stays binary\n (none ↔ '6 14') — dasharray doesn't interpolate\n cleanly between continuous and discrete forms across\n browsers (same lesson R167 documented for the node\n status ring). The CSS keyframe animation on idle\n spokes (anet-topo-spoke-flow) drives stroke-dashoffset\n separately and stays untouched. data-topo-hub-spoke-\n active surfaces the activity state for test probes\n (active spokes don't carry the bucket/dur attrs so\n they need their own data anchor). */\n // Round 382 / Loop: hub-spoke path picks up\n // strokeLinecap='round'. Sibling polish to R378 flow-\n // rail dashes + R380 group box dashes — three dashed-\n // stroke surfaces now share 'round' linecap:\n // R378 flow-rail '2 12' -> soft 3-px pills\n // R380 group box '6 6' -> soft 7.5-px pills\n // R382 hub spoke '6 14' -> soft 7-px pills (this round)\n // For idle spokes (dashed at sw=1), each 6-px dash gains\n // 0.5-px round caps and reads as a soft pill instead of\n // a sharp 6 x 1 rectangle. Active spokes (solid, no\n // dasharray) have caps mostly hidden by the hub center +\n // node radius. Geometry-safe; paint-only. R51 sentinel\n // strokeWidth 1.5/3 untouched (idle=1, active=2). data-\n // topo-hub-spoke-linecap attr exposes the value for tests.\n // Round 419 / Loop: hub-spoke idle opacity 0.45 → 0.50.\n // Stale-state legibility lift family 9th anchor — pairs\n // with R391 (active 0.7 → 0.8) and R415 (active sw 2 →\n // 2.25) so the same spoke path is now polished on BOTH\n // active AND idle tiers. Pre-R419 idle spokes painted\n // at α=0.45 with R46 anet-topo-spoke-flow dashed\n // animation; the dashed pulses sat at the \"background\n // chatter\" floor — visible but understated. R419\n // lifts to 0.50 so idle spokes read more confidently\n // while the active/idle contrast ratio stays clear\n // (0.8/0.50 = 1.6× vs prior 0.8/0.45 = 1.78×; still\n // a sharp two-tier distinction).\n // Stale-state legibility lift family (9 anchors now):\n // R317 subordinate-text gray-500 → gray-400\n // R358 freshness floor 0.25 → 0.30\n // R372 minimap offline-dot 0.5 → 0.6\n // R404 hub-halo cyber trough 0.08 → 0.10\n // R405 hub-halo light trough 0.32 → 0.34\n // R406 edge freshness floor 0.35 → 0.40\n // R407 node halo offline opacity (cyber + light)\n // R413 active-node pulse trough (cyber + light)\n // R419 hub-spoke idle opacity 0.45 → 0.50 (this round)\n // data-topo-hub-spoke-opacity attr (R391) updates to\n // surface the resolved per-state value.\n //\n // Round 415 / Loop: hub-spoke active strokeWidth 2 → 2.25.\n // Pairs with R391 (active opacity 0.7 → 0.8) so the same\n // active-state path lifts BOTH stroke weight AND opacity\n // in concert. Pre-R415 active strokes sat at sw=2 — clear\n // step over idle sw=1, but a touch lighter than the\n // weight family's other \"active\" indicators (R385 hub\n // hover-ring sw=1.75 / R402 legend pin-ring sw=1.75 /\n // R367 edge-badge rest sw=1.25). R415 bumps to 2.25 so\n // the active spoke reads with proportional weight to its\n // role — the line connecting the focal point to the\n // active node deserves the heaviest active stroke in the\n // family (after pin/hot edge-badge sw=2). Stays clear of\n // R51 sentinels (1.5 / 3) at 2.25.\n // Visual-weight bump family (14 anchors now):\n // R287 minimap viewport stroke 1 → 1.5\n // R295 legend swatch radius 5.5 → 6\n // R359 recent-row pip radius 1.6 → 1.8\n // R360 hub digit fontSize 11 → 12\n // R361 edge-badge digit fontSize 10 → 11\n // R365 hub-highlight radius 5 → 5.5\n // R367 edge-badge rest stroke 1 → 1.25\n // R374 pressure-bar height 1.5 → 2\n // R383 recent-row pip radius 1.8 → 2.0\n // R384 minimap online dot 1.7 → 1.9\n // R385 hub hover-ring stroke 1.5 → 1.75\n // R402 legend pin-ring stroke 1.5 → 1.75\n // R408 hub-halo radius 18 → 20\n // R415 hub-spoke active stroke 2 → 2.25 (this round)\n // R382 strokeLinecap='round' + R391 opacity 0.45/0.8 +\n // R51-safe idle sw=1 all preserved. 250ms transition\n // list already covers stroke-width — the new tier eases\n // naturally. data-topo-hub-spoke-stroke-width-active\n // attr surfaces the active value for tests.\n //\n // Round 391 / Loop: hub-spoke active opacity 0.7 → 0.8.\n // Pre-R391 active spokes (the spoke connecting the hub\n // to the currently-active alias — hovered or pinned)\n // lifted opacity from rest 0.45 to active 0.7 — a clear\n // step but slightly understated against the canvas\n // chrome. R391 lifts active to 0.8 so the \"this spoke\n // connects to your active node\" signal reads with\n // matching weight to the R370 hub hover-ring opacity\n // (0.7 → 0.8 cyber) — paired canvas signals now share\n // the same active-state alpha (0.8) so when a user\n // hovers a node, both the spoke and the hub-ring lift\n // to identical opacity. Rest 0.45 invariant preserved.\n // Theme-consistency / canvas-presence polish family\n // (6th anchor):\n // R370 hub hover-ring opacity 0.7 → 0.8 cyber\n // R371 edge-badge rest opacity 0.82 → 0.85 cyber\n // R372 minimap offline-dot opacity 0.5 → 0.6\n // R386 hub-highlight idle opacity 0.9 → 0.95\n // R387 hover-detail panel opacity 0.94 → 0.97 cyber\n // R391 hub-spoke active opacity 0.7 → 0.8 (this round)\n // Idle path (45% alpha + dashed flow animation) entirely\n // untouched — R391 is an active-state-only lift.\n // data-topo-hub-spoke-opacity attr exposes the resolved\n // value for tests. R382 strokeLinecap='round' + R51\n // sentinel-safe sw (1 idle / 2 active) preserved.\n /* Round 430 / Loop: hub-spoke opacity hover lift on\n hoveredAlias === session.alias. Adds a \"this node's\n spoke\" affordance to the node-hover gesture — in a\n dense ring layout the spokes are visually quiet\n (idle α=0.50 dashed, active α=0.80 solid) so hovering\n a node didn't telegraph which line connects to it.\n R430 lifts the matched spoke's opacity:\n idle 0.50 → 0.70 (hover-α=0.70, +0.20)\n active 0.80 → 0.95 (hover-α=0.95, +0.15)\n The +0.15-to-0.20 lift keeps the active/idle two-tier\n distinction (0.95 vs 0.70 still a clear gap) while\n making the hovered-node's spoke visibly brighter than\n every other spoke at its own activity tier. R241\n transition list already covers opacity 250ms so the\n lift eases for free. Sibling to R429 label-card body\n solidity lift — both surface a single-node-focused\n attention cue with the same easing cadence.\n Stacks with the 6-layer node hover cue stack at the\n inter-node-link scope:\n R26 group translateY -2px (per-node)\n R217 stroke tint legendAccent (per-node card)\n R142 drop-shadow boost (per-node card)\n R427 alias letter-spacing (per-node text)\n R428 sub-text letter-spacing (per-node text)\n R429 body opacity 0.94 → 1.0 (per-node card)\n R430 spoke opacity α+ (this round) (link to hub)\n data-topo-hub-spoke-hovered exposes the gate. */\n const isHoveredSpoke = !reducedMotion && hoveredAlias === session.alias;\n const spokeOpacity = isActiveSpoke\n ? (isHoveredSpoke ? 0.95 : 0.80)\n : (isHoveredSpoke ? 0.70 : 0.50);\n /* Round 435 / Loop: hub-spoke stroke-width hover lift —\n sibling to R430 opacity hover at the same surface. When\n hoveredAlias matches, BOTH opacity AND stroke-width\n lift on the matched spoke so the eye registers a\n 2-axis \"this node's spoke\" gesture (paint + geometry).\n idle 1.00 → 1.25 (Δ +0.25, +25%)\n active 2.25 → 2.50 (Δ +0.25, +11%)\n Same +0.25 absolute delta keeps the idle/active visual\n progression consistent — at rest sw ratio 2.25:1 = 2.25,\n on hover 2.50:1.25 = 2.0; both still clearly two-tier.\n R241 transition list already covers stroke-width 250ms\n so the lift eases for free.\n R51 sentinel-safe: spoke is canvas <path>, not\n data-node <circle> (the sentinel selector is gated to\n g[data-node] descendants). 1.25 and 2.5 are not in the\n reserved {1.5, 3} set so the overlap-test sentinel\n attribute selector wouldn't match either way. */\n const spokeStrokeWidth = isActiveSpoke\n ? (isHoveredSpoke ? 2.5 : 2.25)\n : (isHoveredSpoke ? 1.25 : 1);\n return (\n <path\n key={`hub-${session.alias}`}\n d={path}\n fill=\"none\"\n stroke={isActiveSpoke ? pal.spokeStroke.active : pal.spokeStroke.idle}\n strokeWidth={spokeStrokeWidth}\n strokeDasharray={isActiveSpoke ? 'none' : '6 14'}\n strokeLinecap=\"round\"\n opacity={spokeOpacity}\n className={isActiveSpoke ? undefined : 'anet-topo-spoke-flow'}\n data-topo-spoke-bucket={isActiveSpoke ? undefined : busy}\n data-topo-spoke-dur={isActiveSpoke ? undefined : spokeDur}\n data-topo-hub-spoke-active={isActiveSpoke ? 'true' : 'false'}\n data-topo-hub-spoke-hovered={isHoveredSpoke ? 'true' : 'false'}\n data-topo-hub-spoke-opacity={spokeOpacity}\n data-topo-hub-spoke-stroke-width={spokeStrokeWidth}\n data-topo-hub-spoke-stroke-width-active=\"2.25\"\n data-topo-hub-spoke-linecap=\"round\"\n style={{\n transition: 'stroke 250ms ease-out, stroke-width 250ms ease-out, opacity 250ms ease-out',\n ...(isActiveSpoke ? {} : {\n animationDelay: `${-(idx * 0.25)}s`,\n // CSS var consumed by `.anet-topo-spoke-flow`\n // (line 859 of globals.css). React's CSSProperties\n // type doesn't model custom properties → cast\n // through Record<string, string>.\n ...({ ['--spoke-dur']: `${spokeDur}s` } as Record<string, string>),\n }),\n } as React.CSSProperties}\n />\n );\n });\n })()}\n\n {/* #111: prefix-group boundary boxes (Vincent 4722). Grid layout\n only — groupBoxes is empty in ring mode. Rendered behind the\n flow links + nodes; pointer-events off so they never intercept\n a node click. Restrained dashed container + group-name label. */}\n {groupBoxes.map((box, boxIdx) => {\n const isHovered = activeGroup === box.key;\n // R68: distinguish \"locked by click\" from \"currently hovered\".\n // R63 made pinned and hovered identical (both hit isHovered\n // via activeGroup). A user with one team pinned should see at\n // a glance which is the locked one even while their cursor\n // sweeps elsewhere. isPinned reads pinnedGroup directly\n // (NOT activeGroup) so the visual is specific to the sticky\n // state — transient hover keeps the R63 isHovered styling.\n const isPinned = pinnedGroup === box.key;\n // Round 18 / Loop: group-box hover linkage. The Round 8 fade\n // already dropped OUT-of-focus groups to 0.28, but the IN-focus\n // group sat at its baseline appearance — no positive emphasis.\n // Hovering now upgrades the box to an \"accent\" treatment:\n // solid stroke (not dashed), thicker, accent-coloured; brighter\n // text and slightly stronger fill. Label and box read as one\n // selected unit. Geometry unchanged → overlap test untouched.\n // R132: per-group marching-ants duration computed once,\n // reused on the data attribute + the inline custom property.\n const w = box.statuses.working;\n const marchDur = w >= 6 ? 8 : w >= 4 ? 10 : w >= 2 ? 12 : 14;\n // Round 468 / Loop — single-tier classifier. Surfaces the\n // semantic the R319 pip-strip already encodes implicitly:\n // a cluster where every member sits in one status tier\n // renders as `name · count` only (offending duplicate pip\n // dropped). Pre-R468 that \"all members in tier X\" fact\n // was visible to the eye (no pips) but not queryable from\n // the DOM. R468 attaches the classifier as\n // `data-group-tier`:\n // 'all-working' — w===count, fleet uniformly busy\n // 'all-idle' — i===count, fleet uniformly waiting\n // 'all-offline' — o===count, fleet uniformly down\n // 'mixed' — at least 2 tiers present\n // Sibling R466/R467 pattern — expose composed state as a\n // data-attr without changing paint. Use cases: Playwright\n // assertions, external CSS hooks, accessibility enrichment.\n const groupTier =\n box.statuses.working === box.count ? 'all-working' :\n box.statuses.idle === box.count ? 'all-idle' :\n box.statuses.offline === box.count ? 'all-offline' :\n 'mixed';\n return (\n <g\n key={`grp-${box.key}`}\n data-group={box.key}\n data-group-tier={groupTier}\n // Round 173 / Loop: group boxes pick up the first-paint\n // fade-in wave alongside R9 staggered nodes (0-540ms)\n // and R172 staggered edges (280-980ms). Pre-R173 the\n // structural box frames appeared instantly while the\n // nodes inside eased in — the reveal felt like\n // 'frame slams down, nodes drift in'. The .anet-fade-in\n // CSS animation (R3 origin, 0.15s ease-out, mount-once)\n // plays alongside the node R9 stagger; each box offset\n // by boxIdx × 60ms (cap at 8 indices so a fleet with\n // many groups still finishes within ~500ms) so boxes\n // appear like a soft sweep across the grid rather than\n // a single pop. animation-fill-mode default 'none' →\n // post-animation control reverts to the inline opacity\n // style below (1 / 0.28 based on filter pin state).\n // data-group-fade-delay exposes the computed delay for\n // test probes.\n /* Round 470 / Loop — sync the R8 out-of-focus dim\n transition cadence from Tailwind's `transition-\n opacity` default (150ms ease-in-out) to 200ms\n ease-out to match the rest of the cluster's\n motion vocabulary. Hero D #147 stack established\n 200ms ease-out across every cluster axis:\n parent text (codex p.125)\n parent rect (R461 xywh + R464 rx + R248 paint)\n hitbox rect (R459 fill+opacity + R460 x+width\n + R465 rx)\n The wrapper <g>'s opacity flip (1 → 0.28 when\n another group is active) was the LAST surface\n still at 150ms — when the user hovers a group\n label, out-of-focus groups dimmed 50ms faster\n than the focused group's tint brightened, a\n small but perceivable rate-desync. R470 lifts\n the wrapper to 200ms ease-out. duration-200 +\n ease-out are Tailwind v4 utility classes; the\n anet-fade-in mount-once keyframe stays in the\n className for first-paint stagger (R173). */\n className=\"transition-opacity duration-200 ease-out anet-fade-in\"\n data-group-fade-delay={Math.min(boxIdx, 8) * 60}\n data-group-fade-transition=\"200ms\"\n // R63: drop the blanket pointerEvents:'none' that\n // previously sat here. Chrome's SVG impl doesn't let a\n // child override a parent's `none` even though the spec\n // says it should — moving the property onto just the\n // rect (where it's needed so nodes underneath stay\n // clickable) lets the label text receive its own click.\n style={{\n opacity: !activeGroup || isHovered ? 1 : 0.28,\n animationDelay: `${Math.min(boxIdx, 8) * 60}ms`,\n }}\n >\n <rect\n x={box.x}\n y={box.y}\n width={box.w}\n height={box.h}\n /* Round 464 / Loop: group-box rx 14 → 16 on isPinned.\n Geometric softening at the corner radius — locked\n groups read with subtly rounder shoulders than\n hovered/idle. +2px reads as a calm \\\"settled in\\\"\n posture (subtler than a fill or stroke bump but\n unmistakable across the whole cluster boundary).\n Pin signature on the group-box rect now spans 7\n axes:\n R63 text fill brighten\n R142 drop-shadow filter\n R432 text letter-spacing 0→0.5\n R444 count tspan fw 500→600\n R457 parent text fw 700→800\n codex p.125 text opacity 0.55→1\n R464 corner rx 14→16 (this round)\n transition list (R461) already covers x/y/width/\n height 200ms ease-out; appended `rx 200ms ease-\n out` so the rounding eases alongside the geometry\n axes. SVG2 CSS animation on rx: Chrome 95+ /\n Safari 16+ / FF 70+ (same matrix as x/y/w/h).\n data-group-box-rx exposes the resolved value. */\n rx={isPinned ? '16' : '14'}\n data-group-box-rx={isPinned ? '16' : '14'}\n fill={isLight ? '#0f172a' : '#a5b4fc'}\n // R68: 3-tier opacity + stroke ladder.\n // pinned → fill 0.08 / 0.13, stroke 3 px (locked)\n // hovered → fill 0.05 / 0.09, stroke 2 px (inspecting)\n // idle → fill 0.025 / 0.045, stroke 1.5 px dashed\n fillOpacity={isPinned ? (isLight ? 0.08 : 0.13)\n : isHovered ? (isLight ? 0.05 : 0.09)\n : (isLight ? 0.025 : 0.045)}\n stroke={(isPinned || isHovered) ? pal.legendAccent : pal.ringStroke}\n strokeWidth={isPinned ? 3 : isHovered ? 2 : 1.5}\n strokeDasharray={(isPinned || isHovered) ? 'none' : '6 6'}\n /* Round 380 / Loop: cluster box stroke gets round\n linecap + round linejoin. Sibling SVG stroke-\n softening polish to R378 flow-rail linecap + R379\n minimap viewport linejoin — extends the family to\n the group cluster boundary box (grid layout only):\n R288 chrome icons strokeLinecap='round'\n R378 flow-rail dashes strokeLinecap='round'\n R380 group box dashes strokeLinecap='round' (this round)\n R379 viewport rect strokeLinejoin='round'\n R380 group box corners strokeLinejoin='round' (this round)\n Linecap rounds the R85 '6 6' marching-ants dash\n pills at rest — each 6 px dash gains a ~0.75 px\n round cap (sw=1.5 idle), reading as soft pills\n instead of sharp 6 × 1.5 px rectangles. Linejoin\n rounds the 4 sharp 90° corners (any state — solid\n or dashed); at sw=1.5 the join arc is ~0.75 px,\n matching R379 viewport vocabulary. Geometry-safe:\n stroke-* properties only affect paint, not bbox.\n The R51 sentinel 1.5/3 strokeWidth values stay\n intact (the overlap probe is gated to g[data-\n node], so this cluster-internal rect is invisible\n to it anyway). data-group-box-linecap + -linejoin\n attrs expose the values for tests. */\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n data-group-box-pinned={isPinned ? 'true' : 'false'}\n data-group-box-linecap=\"round\"\n data-group-box-linejoin=\"round\"\n data-group-box-geom-transition=\"x,y,width,height\"\n // R85: ambient \"marching ants\" drift on the perimeter\n // when this group has at least one working member, and\n // neither pin nor hover is active (those treatments\n // already shout for attention via solid stroke). 12s\n // cycle reads as ambient — the eye parses \"live work\n // here\" without registering the box as animating.\n // R132: per-group ant rate buckets on box.statuses.working\n // — the same coupling-to-busyness idiom R84 uses for the\n // hub and R131 uses for the outer-ring orbit, applied at\n // the GROUP scale. A team with one working member ambles;\n // a team with five working members visibly accelerates.\n // Bucket boundaries (1 / 2-3 / 4-5 / 6+) chosen to land\n // on the same 14/12/10/8 cadence ladder so the three\n // motion layers (hub / ring / group) keep a coherent\n // tempo grammar. Default 12s when working=0 doesn't\n // matter — the className is only applied when working>0.\n data-group-box-live={!isPinned && !isHovered && box.statuses.working > 0 ? 'true' : 'false'}\n data-group-box-march-dur={marchDur}\n data-group-box-lifted={(isPinned || isHovered) ? 'true' : 'false'}\n className={!isPinned && !isHovered && box.statuses.working > 0 ? 'anet-topo-groupbox-live' : undefined}\n // R142: drop-shadow filter when pinned or hovered. Box\n // visually \"rises off the canvas\" — same vocabulary\n // R18 KPI cards + R135 overlay panels use. Idle group\n // boxes carry no filter (purely flat dashed outline)\n // so the unstyled canvas stays uncluttered. Filter\n // affects paint area only, not the geometric bbox\n // the overlap-test reads, so zero-overlap invariant\n // is preserved.\n filter={(isPinned || isHovered) ? 'url(#topo-groupbox-lift)' : undefined}\n style={{\n /* Round 248 / Loop: append fill 200ms ease-out to\n the existing R66 transition list. Pre-R248 the\n rect's fill (isLight ? '#0f172a' (slate-900) :\n '#a5b4fc' (indigo-300)) snapped on theme toggle\n while stroke / fill-opacity / filter all eased.\n Closes the last theme-toggle snap on the group\n box surface — same idiom R246 + R247 used at\n per-node label-card and side-panel scopes.\n Round 461 / Loop: extend the transition list to\n all 4 geometry axes (x, y, width, height) so\n when a cluster grows / shrinks (member joins,\n leaves, prefix rebalance, dense toggle, status\n flip) the BIG outer container slides into the\n new bounds at the same 200ms cadence the R460\n inner hitbox tint rect now uses. Pre-R461 the\n outer 200×140 px box snap-jumped on cluster\n resize while the inner 160×18 hitbox slid —\n jarring two-rate motion at the same surface.\n R461 unifies both rects to slide as one, with\n the parent box driving the visual envelope and\n the inner hitbox tracking the bottom-edge tint.\n Hero D #147 motion-coherence at the FULL cluster\n container tier (not just the label tint).\n data-group-box-geom-transition attr exposed. */\n transition: 'stroke 200ms ease-out, stroke-width 200ms ease-out, fill-opacity 200ms ease-out, filter 200ms ease-out, fill 200ms ease-out, x 200ms ease-out, y 200ms ease-out, width 200ms ease-out, height 200ms ease-out, rx 200ms ease-out',\n pointerEvents: 'none',\n // CSS var consumed by `.anet-topo-groupbox-live`\n // (line 877 of globals.css). React's CSSProperties\n // type doesn't model custom properties, so cast\n // through Record<string, string>.\n ...({['--march-dur']: `${marchDur}s`} as Record<string, string>),\n }}\n />\n {/* R63: wrap label in a clickable <g> with an invisible\n rect hitbox. The text alone wasn't getting hit-tested\n reliably — the SVG-wide topo-panel <rect> intercepts\n at the label's screen position when the label sits at\n a high viewBox-y (it lands below where the compositor\n expects the text to paint on top, same gotcha as the\n recent-signal panel rows in R56). Hitbox rect + the\n parent <g> taking the click + onPointerDown stop-\n propagation match the R55/R56/R61 pattern. */}\n <g\n role=\"button\"\n tabIndex={0}\n aria-pressed={pinnedGroup === box.key}\n data-group-label-hit={box.key}\n className=\"anet-topo-svg-focus\"\n style={{ pointerEvents: 'all', cursor: 'pointer' }}\n onPointerDown={(e) => e.stopPropagation()}\n onClick={() => setPinnedGroup(prev => prev === box.key ? null : box.key)}\n // R86: hover the label → transient group focus. Releases\n // on leave; activeGroup = hoveredGroup ?? pinnedGroup\n // formula already handles transient-over-pin so a\n // user can spot-compare teams without losing their\n // pinned one. Closes the same R83-style hover/click\n // gap (segments) — now group labels carry it too.\n onPointerEnter={() => setHoveredGroupLabel(box.key)}\n onPointerLeave={() => setHoveredGroupLabel(prev => prev === box.key ? null : prev)}\n // R152: a11y completeness — R63 added role + tabIndex +\n // aria-pressed but never wired onKeyDown, so the focused\n // group label was tab-reachable but Enter/Space was a\n // no-op. Closes the last keyboard gap among the\n // role=\"button\" surfaces. Other group-pin trigger paths\n // (R69 palette, R74 cmdk, R86 hover, dispatchEvent) are\n // unchanged. Matches the onKeyDown idiom from R116 /\n // R139 / R140 / R151 (Enter & Space → same setter as\n // onClick, preventDefault on Space to stop SVG scroll).\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n setPinnedGroup(prev => prev === box.key ? null : box.key);\n }\n }}\n >\n {/* R99: SVG <title> tooltip listing group members +\n status breakdown. Same info-density spirit as\n R97 pill tooltips + R98 node tooltips — anywhere\n a UI element says \"alpha · 3\" should hover-\n explain WHICH 3. Truncates at 8 aliases with a\n \"+N more\" suffix so a 20-member band doesn't\n paint a 22-line tooltip. */}\n {(() => {\n const members = Object.entries(groupKeys)\n .filter(([, key]) => key === box.key)\n .map(([alias]) => alias);\n const memberPreview = members.slice(0, 8).join(', ');\n const suffix = members.length > 8 ? ` + ${members.length - 8} more` : '';\n const statusSummary = [\n box.statuses.working > 0 ? `${box.statuses.working} working` : null,\n box.statuses.idle > 0 ? `${box.statuses.idle} idle` : null,\n box.statuses.offline > 0 ? `${box.statuses.offline} offline` : null,\n ].filter(Boolean).join(' · ');\n return (\n <title>{[\n `${box.key} (${members.length} member${members.length === 1 ? '' : 's'})`,\n statusSummary || null,\n `${memberPreview}${suffix}`,\n pinnedGroup === box.key ? 'click to release pin' : 'click to pin this group',\n ].filter(Boolean).join('\\n')}</title>\n );\n })()}\n {/* v0.11.0 #147 Hero D — Vincent 5401: \"太大太丑,\n 都放到框的右下角的小字\". First-cut bottom-right\n placement collided with bottom-row nodes (cluster\n geometry has no bottom padding; only GROUP_TOP=12\n top band). Pivoted to Option C from #147 spec:\n keep top-left anchor BUT shrink fontSize (13 → 9)\n and dim default opacity (1 → 0.55, hover/pin\n restore to 1). Satisfies \"太大太丑\" via the size +\n opacity axes while keeping the existing geometry\n contract that topo-overlap-test gates. Hitbox\n rect width tightens to min(box.w-12, 160) to\n track the narrower label render. */}\n {/* Round 465 / Loop — hitbox tint rect rx 4 → 5 on\n pinnedGroup match. Mirrors R464 (parent group-box\n rx 14 → 16 on isPinned) at the hitbox tier. The\n R460 hitbox carried fixed rx=4 since codex p.125\n pivoted it to the bottom-of-band position; the\n pin-state geometric softening was only on the BIG\n outer container, not the small hitbox underneath.\n R465 adds +1 px corner rounding on pin so the\n tint rect echoes the parent's locked posture at\n its own scale (8% relative bump matches R464's\n 14→16 ≈ 14% scaled to the smaller rect).\n Transition list (R460 fill/opacity/x/width 200ms\n ease-out) extends to include `rx 200ms ease-out`\n so the rounding eases under the same cadence.\n SVG2 CSS animation on rx: Chrome 95+ / Safari\n 16+ / FF 70+ (same matrix as x/y/w/h).\n data-group-label-tint-rx exposes the resolved\n value for tests. */}\n <rect\n x={box.x + 6}\n y={box.y + 2}\n width={Math.min(box.w - 12, 160)}\n height={18}\n rx={pinnedGroup === box.key ? '5' : '4'}\n data-group-label-tint-rx={pinnedGroup === box.key ? '5' : '4'}\n fill={pinnedGroup === box.key || hoveredGroupLabel === box.key ? pal.legendAccent : 'transparent'}\n opacity={pinnedGroup === box.key ? (isLight ? 0.16 : 0.20)\n : hoveredGroupLabel === box.key ? (isLight ? 0.09 : 0.13)\n : 1}\n data-group-label-tinted={pinnedGroup === box.key ? 'pinned' : hoveredGroupLabel === box.key ? 'hover' : 'none'}\n /* Round 459 / Loop — cadence-sync follow-on to codex\n preview.125 (Hero D #147). Codex's parent <text>\n transition list now reads:\n 'fill 200ms, letter-spacing 200ms,\n font-weight 200ms, opacity 200ms'\n — 200ms ease-out across every axis. The label\n hitbox tint rect underneath was still at 150ms\n (legacy R107 cadence), so the tint snapped in\n 50ms ahead of the parent label brightening —\n a small but perceivable mistimed cascade when\n hovering or clicking to pin a cluster. R459\n lifts both axes to 200ms to lock the tint\n under the label as one motion-coherent state\n flip. Hover/pin/unpin all feel as a single\n unified ease rather than \"tint pops, label\n follows\". data-group-label-tint-transition\n attr exposes the timing for tests. */\n /* Round 460 / Loop — extend the R459-200ms tint rect\n transition list to include `x` + `width` so the\n hitbox slides into place when a cluster grows or\n shrinks (member joins / leaves / status change\n re-pricing box.w). Pre-R460 every resize snap-\n jumped the hitbox bounds — a small but visible\n glitch right at the moment the operator's\n attention is on the cluster. SVG2 CSS animation\n on geometry attrs has shipped in Chrome 95+ /\n Safari 16+ / FF 70+; the runtime gracefully\n no-ops on older browsers. Sibling motion idiom\n to R134 / R141 / R142 (panel rect transitions)\n at the group-label hitbox tier.\n data-group-label-tint-geom-transition attr\n exposes the geometry-axis presence for tests. */\n data-group-label-tint-transition=\"200ms\"\n data-group-label-tint-geom-transition=\"x,width,rx\"\n style={{ transition: 'fill 200ms ease-out, opacity 200ms ease-out, x 200ms ease-out, width 200ms ease-out, rx 200ms ease-out' }}\n />\n {/* Round 218 / Loop: group label gains a letter-spacing\n transition on pin — the text subtly spaces out\n (0px → 0.5px) when the group is locked, giving the\n pinned state its own typographic signature distinct\n from R63's transient hover fill brighten. Hover and\n pin share the same fill colour (legendHeadline), so\n pre-R218 the only thing distinguishing them was\n R142 drop-shadow + R68 rect stroke. R218 adds a\n type-level signal: pinned text spreads slightly,\n feels \"locked in\" / \"open and held\". Letter-\n spacing is one of the few SVG text properties that\n interpolates smoothly across the major browsers.\n Hover stays at default tracking — the spread is\n pin-exclusive so users can read pinned vs\n hovered at the text alone. transition 200ms\n matches R142 fill timing so all the group-label\n state-flip channels (fill colour, rect stroke,\n rect drop-shadow, label tracking) ease as one. */}\n {/* Round 432 / Loop: extend the group-label letter-\n spacing tween from 2-tier (rest/pin) to 3-tier\n (rest/hover/pin → 0/0.25/0.5). Pre-R432 R218\n spread the text only on pin; hover got an\n R63 fill brighten (legendText → legendHeadline)\n but no typographic axis of its own. R432 adds\n the missing mid tier so hover telegraphs through\n BOTH the fill brighten AND a subtle kerning\n spread — sibling pattern to R427 node-alias\n (0/0.3/0.5) and R431 edge-badge (0/0.2/0.4) at\n group-label scope. Pin tier (0.5) still wins.\n Subtler mid tier (0.25 vs alias 0.3) because the\n group label is a structural anchor — too much\n spread would steal weight from the per-node\n alias identity it groups. Hover-letter-spacing\n family extension (8 anchors now):\n R344 chip count digit\n R345 panel title\n R347 active-links chip\n R351 vendor chip\n R420 zoom-level chip\n R427 node alias text\n R431 edge-badge digit\n R432 group label text (this round)\n R218 transition list ('fill 200ms, letter-spacing\n 200ms') untouched — additive conditional case. */}\n {/* Round 457 / Loop: group label parent text fontWeight\n 700 → 800 on isPinned. Adds typographic weight axis\n to the group-label parent text, sibling to R432\n letter-spacing tween at the same surface. Pre-R457\n pin lifted ls 0 → 0.5px (R218→R432 3-tier) but the\n fw stayed planted at R63's 700 — locked groups\n read as wider-but-same-weight. R457 adds the\n weight axis so pinned groups read as tightened\n AND wider, matching the R416/R424/R425/R426/R444/\n R445/R446 \"data tightens under attention\" idiom\n (now extended to the parent-text scope at the\n group-label tier). R63 fill brighten + R432\n letter-spacing 0/0.25/0.5 3-tier + R55 transition\n list all preserved; extends to include 'font-\n weight 200ms ease-out' so the bump eases under\n the same cadence. */}\n {/* v0.11.0 #147 Hero D — Vincent 5401 ask: \"dash 网络\n 图里面这个工程的名字也太大了, 超级丑\". Per Vincent\n screenshot 实测. Initial attempt moved label to\n bottom-right (#147 spec Option A); topo-overlap-test\n caught 7 grid collisions because cluster boxes have\n no bottom padding. Pivot to Option C: keep top-left\n anchor, shrink fontSize 13 → 9 (-31%, watermark\n register), dim default opacity 1 → 0.55 (hover/pin\n restore to 1). Net Twitter-grok improvement:\n cluster labels no longer dominate the canvas at\n rest; operator still hovers to find specific groups.\n Position unchanged to preserve the existing\n geometry that overlap-test gates. */}\n <text\n x={box.x + 12}\n y={box.y + 12}\n fill={isHovered ? pal.legendHeadline : pal.legendText}\n fontSize=\"9\"\n fontFamily=\"monospace\"\n fontWeight={isPinned ? '800' : '700'}\n opacity={isPinned || isHovered ? 1 : 0.55}\n data-group-label-hovered={isHovered && !isPinned ? 'true' : 'false'}\n data-group-label-font-weight={isPinned ? '800' : '700'}\n /* Round 479 / Loop — extend drop-shadow visual-polish\n family to a 4th anchor: group-label parent text\n on isPinned. Continues the R476/R477/R478 arc:\n R476 hub digit hover-gated emerald\n R477 legend pin-ring pin-gated row.fill\n R478 recent-row pip freshness-gated cyan\n R479 group-label text pin-gated cyan\n Hue: pal.legendAccent at 0x80 alpha (≈50%) — same\n accent family R107/R477 use for tint surfaces. 3px\n blur reads as a soft cyan halo around the locked\n cluster name. Stacks with the R432 letter-spacing\n spread + R457 fw lift + R63 fill brighten + R142\n drop-shadow on the parent rect — pin signature on\n group label scope now spans typography + chroma +\n paint + container-lift + text-glow.\n Filter is paint-only; bbox unchanged; overlap-test\n invariants hold (R51 selector gated to g[data-node]\n descendants, this label is invisible to the probe).\n transition list extends to include 'filter 200ms\n ease-out' alongside the existing fill/ls/fw/opacity\n 200ms tweens. */\n data-group-label-glow={isPinned ? 'true' : 'false'}\n style={{\n transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out, font-weight 200ms ease-out, opacity 200ms ease-out, filter 200ms ease-out',\n letterSpacing: isPinned ? '0.5px' :\n isHovered ? '0.25px' : '0px',\n filter: isPinned\n ? `drop-shadow(0 0 3px ${pal.legendAccent}80)`\n : undefined,\n }}\n data-group-label={box.key}\n data-group-label-pinned={isPinned ? 'true' : 'false'}\n >\n {box.key}\n {/* Round 19 / Loop: member-count chip. Inline tspan stays\n in the single <text> bbox the overlap test reads, so\n the node↔label guard still catches if the chip ever\n pushes the label far enough right to clip a node.\n Smaller + lighter weight reads as metadata, not name. */}\n {/* Round 229 / Loop: member-count chip drops its explicit\n fill so it inherits from the parent <text>, which means\n R142's hover-fill transition (legendText → legend-\n Headline, 200ms ease-out) NOW carries the count chip\n with it. Pre-R229 the parent name brightened on\n hover while the count tspan stayed at legendText —\n \"name lit, count dimmer than at rest\" inverted the\n tonal hierarchy. Inheriting matches the name's\n transition; both rest and hover keep the SAME\n tonal relationship between name and count.\n 7th surface in the hover-deepen-own-hue family\n (legend rows, chip-row counts, status pip, recent\n row text, pressure-bar segments, group-box fill +\n this round's group-label-count chip).\n\n Also picks up tabular-nums (5th surface in the\n info-density tabular-nums sweep after R224 edge\n badge / R225 hub digit / R225 panel header /\n R225 recent row count). The member count rolls\n over often (4→5→…→9→10 as a group grows) and\n lives at a fixed dx=6 offset from the name, so a\n digit-width jitter at 9→10 used to shift the\n whole count visibly. Tabular locks it. */}\n {/* Round 366 / Loop: group label member-count tspan\n fontWeight 400 → 500. Sibling polish to R363\n recent-row alias text fw 400 → 500 + R364 legend-\n row label fw 400 → 500 — closes the per-row 'count\n is fw 500 against label-tier fw 700' pattern at\n the group-label scope (grid layout cluster mark).\n Hierarchy snapshot post-R366 across all 3 row\n surfaces:\n recent count(hot/cold) fw 700/600 (R320)\n recent alias fw 500 (R363)\n legend count fw 600 (R309)\n legend label fw 500 (R364)\n group name fw 700 (legacy)\n group count fw 500 (R366, this round)\n Monospace family + R225 tabular-nums lock digit\n width, so the fw bump is paint-only — bbox\n unchanged + overlap-test invariants hold. R229\n fill-inherit from parent label (hover-deepen-own-\n hue family) preserved. data-group-label-count-\n font-weight attr exposes the value for tests. */}\n {/* Round 444 / Loop: group label count tspan\n fontWeight 500 → 600 on isPinned. Extends the\n \"data tightens under attention\" typographic-\n weight pattern to a 5th anchor at the group-\n label-count scope:\n R416 chip-digit (chip hover)\n R424 panel-digit (panel hover)\n R425 hub-digit (hub hover)\n R426 edge-badge-digit (pin/hot)\n R444 group-label-count (pinned) ← this round\n Same idiom — when the group is locked, its\n member-count tightens typographically alongside\n the R432 letter-spacing spread (0 → 0.5px) on\n the parent label. Hover keeps rest fw (500) so\n the locked vs preview distinction at the type\n level stays intact — same gate R432 used.\n Monospace + R225 tabular-nums lock the digit\n width across fw changes; bbox unchanged; overlap-\n test invariants hold. transition list adds\n 'font-weight 200ms ease-out' matching R432\n letter-spacing cadence. R229 fill-inherit\n preserved (parent text fill still drives the\n hover/pin color). data-group-label-count-font-\n weight + -pinned attrs exposed for tests. */}\n {/* v0.11.0 #147 — count tspan tracks parent fontSize:\n 11 → 8 to match the new 9px label scale (parent\n dropped 13 → 9 with same -2px gap to the count\n suffix). dx=\"4\" replaces dx=\"6\" — the smaller\n glyph baseline doesn't need the wider gutter. */}\n <tspan\n dx=\"4\"\n fontSize=\"8\"\n fontWeight={isPinned ? '600' : '500'}\n data-group-label-count={box.key}\n data-group-label-count-value={box.count}\n data-group-label-count-pinned={isPinned ? 'true' : 'false'}\n data-group-label-count-font-weight={isPinned ? '600' : '500'}\n style={{\n fontVariantNumeric: 'tabular-nums',\n transition: 'font-weight 200ms ease-out',\n }}\n >· {box.count}</tspan>\n {/* Round 58 / Loop: status mix pip strip. Compact text-\n based chips (e.g. \"2w 1i\") so the strip stays inside\n the same <text> bbox the overlap-test reads — keeps\n the R27 label↔label and R19 node↔label guards intact.\n Each tier is colour-coded against the legend swatches\n and only renders when count > 0, so a healthy all-\n working group reads simply \" · 2w\".\n\n Round 207 / Loop: each tspan eases in on mount\n via anet-fade-in. Pre-R207 when a group's first\n working node went idle (or first idle node went\n working), the new tier's tspan snap-popped into\n the label. Same snap-on-mount issue R203 fixed\n for recent-signal rows, applied at the group-\n label scope. Each tier is keyed on its boolean\n mount, so the animation fires once when the\n tspan first appears (count crosses 0 → 1+),\n not on every count update (e.g., 1 → 2 working\n preserves the tspan via React reconciliation).\n Exit remains snap — matches R190's \"fade-IN\n smooth, accept exit snap\" trade-off used for\n the R129 hot-tail. */}\n {/* Round 230 / Loop: tabular-nums on the 3 status pips\n so the count digit doesn't jitter the adjacent\n pip when a tier crosses 9 → 10. The pips render\n in sequence at dx=8/4/4 — width-shift on any\n tier propagates rightward through the strip,\n visibly compressing or stretching the gap\n between adjacent tier chips. Tabular locks the\n digit so the strip stays stable as tiers grow.\n 6th surface in the info-density tabular-nums\n sweep after R224 edge badge / R225 hub digit /\n R225 panel header / R225 recent row count /\n R229 group-label count. Tier-specific fill\n colours stay (semantic — working green /\n idle teal / offline slate). */}\n {/* Round 253 / Loop: append fill 200ms ease-out to\n each tspan's style so theme toggle eases the\n tier-coloured pips alongside every other\n theme-driven element. R230's tabular-nums\n stays. */}\n {/* Round 319 / Loop: drop a tier pip when its count\n equals box.count — i.e. single-tier groups (all\n working, all idle, all offline). Pre-R319 a 4-all-\n idle group rendered as `P站 · 4 4i` with the \"4\"\n visually doubled; Vincent telegram 5304 flagged\n this as 比较难看 in a real-data screenshot\n (ai-insight · 6 6i, blueleap · 3 3i, P站 · 4 4i).\n The dropped pip's information is already conveyed\n by the group-box stroke colour (R68 isPinned/\n hover accent uses the dominant-tier hue) plus\n the SVG <title> tooltip listing the status\n breakdown. Multi-tier groups (e.g. `alpha · 3\n 2w 1i`) render unchanged — those pips genuinely\n add breakdown info that the total doesn't carry. */}\n {/* Round 458 / Loop — Hero D #147 finishing polish on top of\n N站牛/codex preview.125 (Option C: top-left label fontSize\n 13→9 + opacity 0.55 rest / 1 hover+pin, count tspan 11→8).\n That ship left the 3 status pips at fontSize=11 — visibly\n DOMINATING the now-9px parent label they trail. Result on\n a 5-member cluster: `alpha · 5 3w 2i` renders inside-out\n as \"tiny name + tiny count + BIG bright pips\" rather than\n a coherent right-tail of metadata. R458 scales the 3 pips\n to fontSize=8 (matches count tspan) and tightens dx 8/4/4\n → 6/3/3 (gutter ratio 0.73/0.36 glyph-widths @ 11px ≈\n 0.75/0.38 glyph-widths @ 8px — same visual rhythm at the\n smaller scale). The whole group-label bottom-right strip\n now reads as a unified 9/8/8/8 typographic ladder:\n name (parent <text>) fontSize 9 fw 700/800\n · count (1st tspan) fontSize 8 fw 500/600\n Nw (2nd tspan) fontSize 8 fw 600\n Ni (3rd tspan) fontSize 8 fw 600\n No (4th tspan) fontSize 8 fw 600\n Closes Vincent /goal 5401 (\"太大太丑\") at the pip-strip\n tier; with codex preview.125 the spec is fully realized.\n Geometry-only attribute changes — bbox tightens slightly\n (8px chars vs 11px chars stay inside the original 240px\n hitbox max) so topo-overlap-test invariants hold.\n tabular-nums + anet-fade-in + theme-eased fill 200ms\n preserved on every tspan. */}\n {box.statuses.working > 0 && box.statuses.working !== box.count && (\n <tspan\n dx=\"6\"\n fill={isLight ? '#059669' : '#22c55e'}\n fontSize=\"8\"\n fontWeight=\"600\"\n className=\"anet-fade-in\"\n data-group-pip=\"working\"\n style={{ fontVariantNumeric: 'tabular-nums', transition: 'fill 200ms ease-out' }}\n >{box.statuses.working}w</tspan>\n )}\n {box.statuses.idle > 0 && box.statuses.idle !== box.count && (\n <tspan\n dx=\"3\"\n fill={isLight ? '#0d9488' : '#2dd4bf'}\n fontSize=\"8\"\n fontWeight=\"600\"\n className=\"anet-fade-in\"\n data-group-pip=\"idle\"\n style={{ fontVariantNumeric: 'tabular-nums', transition: 'fill 200ms ease-out' }}\n >{box.statuses.idle}i</tspan>\n )}\n {box.statuses.offline > 0 && box.statuses.offline !== box.count && (\n <tspan\n dx=\"3\"\n fill={isLight ? '#94a3b8' : '#6b7280'}\n fontSize=\"8\"\n fontWeight=\"600\"\n className=\"anet-fade-in\"\n data-group-pip=\"offline\"\n style={{ fontVariantNumeric: 'tabular-nums', transition: 'fill 200ms ease-out' }}\n >{box.statuses.offline}o</tspan>\n )}\n </text>\n </g>\n </g>\n );\n })}\n\n {/* directed message flows */}\n {flowLinks.map((link, index) => {\n const from = nodePositions[link.from];\n const to = nodePositions[link.to];\n if (!from || !to) return null;\n\n // Round 7 / Loop: lift now scales with distance so short links\n // aren't over-bent (long links keep the ~36px hump), and the\n // particle period shortens with link.count so busier edges\n // visibly pulse faster — instant info density on top of the\n // existing stroke-width-by-count chip.\n const dist = Math.hypot(to.x - from.x, to.y - from.y);\n const lift = (index % 2 === 0 ? 1 : -1) * Math.min(36, dist * 0.18);\n const path = curvePath(from, to, lift);\n const width = Math.min(2 + link.count, 7);\n const duration = Math.max(0.9, 2.6 / Math.sqrt(link.count));\n // Round 231 / Loop: per-edge phase stagger lifted into a\n // named constant so the R75 arrival ping + R76 dispatch\n // pulse SMIL animates can RE-COUPLE to the R103 particle's\n // cycle. Pre-R103 (when particle started at phase 0) the\n // ping fired at \"near end of cycle\" (-0.92*dur) and dispatch\n // at \"cycle start\" (0) — both phase-coincident with particle\n // arrival/departure respectively. R103's golden-ratio\n // stagger broke that coupling — particle now started at\n // phase (index*0.37)%dur while ping+pulse stayed at fixed\n // offsets, so the rings fired at random moments relative\n // to particle position. R231 expresses dispatch_begin and\n // arrival_begin in terms of THIS stagger so they fire\n // exactly when the particle is at source / near destination\n // respectively — restoring R75/R76's original semantic\n // and unifying the three SMIL elements into one\n // synchronised per-edge animation set.\n const stagger = (index * 0.37) % duration;\n // Round 10 / Loop: freshness fade. An edge that fired ≤30s ago\n // stays at full intensity; over 5 minutes it decays to a\n // floor. Surfaces \"what's happening now\" vs background\n // chatter without hiding old flow entirely (some context\n // still useful). `now` captured at useMemo-recompute time\n // (every 5s message refresh) — accuracy is within the poll\n // interval, plenty.\n //\n // Round 406 / Loop: edge freshness fade floor 0.35 → 0.40.\n // Stale-state legibility lift family (6th anchor) — pre-\n // R406 edges older than 5 minutes faded to α=0.35 (a 65 %\n // dim against full intensity). The decay rate is the same\n // 1 - ageMs/300s curve; only the FLOOR shifts. Sibling\n // treatment to:\n // R317 subordinate-text gray-500 → gray-400\n // R358 freshness ramp floor 0.25 → 0.30\n // R372 minimap offline-dot opacity 0.5 → 0.6\n // R404 hub-halo cyber trough 0.08 → 0.10\n // R405 hub-halo light trough 0.32 → 0.34\n // R406 edge freshness floor 0.35 → 0.40 (this round)\n // Edges past 5min now sit at 40% intensity instead of 35%\n // — they still recede against fresh edges but read\n // legibly enough to convey \"this conversation existed\".\n // ageMs threshold for the 5-minute decay unchanged; the\n // decay curve shape (linear) unchanged. The visual delta\n // is most pronounced on edges between 5-60 minutes old —\n // where the floor was binding pre-R406.\n const ageMs = link.last_at ? Math.max(0, Date.now() - Date.parse(link.last_at)) : 0;\n const fresh = Math.max(0.40, 1 - ageMs / (5 * 60 * 1000));\n // Round 16 arrow-tier binning — keep `topo-arrow` as the\n // medium tier id so the legend swatch picks it up unchanged.\n const arrowId = link.count <= 2 ? 'topo-arrow-s'\n : link.count <= 4 ? 'topo-arrow'\n : 'topo-arrow-l';\n\n // Round 39 / Loop: edge hover tooltip — surface the same\n // last_at + count info the freshness fade and arrow tier\n // already encode visually, in plain text. The stroke is the\n // hover target; SVG `<title>` honours newlines on every\n // browser the dashboard targets.\n const lastAt = relativeAgo(link.last_at);\n const tooltip = `${link.from} → ${link.to}\\n${link.count} message${link.count === 1 ? '' : 's'}${lastAt ? ` · last ${lastAt}` : ''}`;\n // Round 40 / Loop: edges follow node hover — when an alias is\n // hovered, every edge touching it brightens, the rest fade.\n // Pairs with the Round 8 group-focus fade on nodes: hover a\n // node to find \"who is this agent talking to\" at a glance.\n // No hover → multiplier is 1.0 (current behaviour preserved).\n // Round 50 / Loop: edge-on-self priority. When the user hovers\n // a flow edge directly (R48 widened the hitbox so this is now\n // a precise gesture), THAT edge gets the strongest boost (2.0)\n // and every other edge dims to 0.35. Node-hover (R40) keeps\n // its own ladder. Edge-hover and node-hover are mutually\n // exclusive in practice — the cursor is over one or the\n // other — but the order below makes edge-hover win if both\n // ever read truthy at the same React tick.\n // Round 53 / Loop: in-group edges follow team focus. R40\n // brightened only edges touching the exact hovered alias —\n // but with R8/R18 prefix-clustering, a user hovering one\n // member is asking about the team. So when BOTH endpoints of\n // an edge share the hoveredGroup (and neither is the exact\n // hovered alias — that gets the stronger 1.7×), the edge\n // boosts to 1.3×. Edges leaving the team (one endpoint in,\n // one out) still dim to 0.35× per R40 since they read as\n // background to the focus. Singletons fall through unchanged\n // (their group key is the alias itself, so bothInHoveredGroup\n // is impossible for a non-self-edge).\n // R116: composes hover ?? pin — a pinned edge stays \"hot\" after the cursor leaves.\n const isHoveredEdge = activeEdgeKey === link.key;\n const fromGroup = groupKeys[link.from] ?? link.from;\n const toGroup = groupKeys[link.to] ?? link.to;\n const bothInHoveredGroup = !!activeGroup && fromGroup === activeGroup && toGroup === activeGroup;\n // R63: also gate \"no filter\" on activeGroup so pinnedGroup\n // alone activates the in-group 1.3× boost + non-group dim.\n // When neither a node nor a group is in focus, mul is 1.\n // R77: when the user hovers the \"N active links\" chip, the\n // baseline 1× becomes 1.5× — every flow brightens at once.\n // Sits inside the no-other-hover branch so it doesn't fight\n // the edge-hover (2.0×) or node-hover (1.7×) priorities.\n const edgeOpacityMul = isHoveredEdge\n ? 2.0\n : activeEdgeKey\n ? 0.35\n : !hoveredAlias && !activeGroup\n ? (hoveredActiveLinks ? 1.5 : 1)\n : (link.from === hoveredAlias || link.to === hoveredAlias)\n ? 1.7\n : bothInHoveredGroup\n ? 1.3\n : 0.35;\n // Round 50: the hovered edge also visibly thickens so the eye\n // tracks it even at low message counts (width was 3 → 4.5 on\n // hover). 1.4× is enough to read as \"lifted\" without\n // breaching the 16-px hitbox bound.\n // Round 436 / Loop: extend the thickening to the\n // \"hovered-endpoint\" case — when hoveredAlias matches one\n // of this edge's endpoints, lift width by 1.15× (capped at\n // 8 px to stay clear of the 16-px hitbox). Pre-R436 the\n // edgeOpacityMul=1.7 (line 4703) lifted the matched edge's\n // OPACITY when an endpoint was hovered but the stroke-WIDTH\n // stayed at base — so the edge faded brighter without\n // thickening, leaving the paint+geometry axes mismatched.\n // Mirror of R430/R435 hub-spoke pattern (opacity + stroke-\n // width co-lift on hoveredAlias); R436 brings the same\n // dual-axis \"this node's link\" gesture to the edge scope.\n // 1.15× is subtler than isHoveredEdge=1.4× because\n // endpoint-hover lifts MANY edges at once (every edge\n // incident on the hovered node) while edge-hover lifts ONE\n // — the gesture should read as \"highlighted\" not \"loud\".\n // R166 stroke-width 300ms transition already in the\n // visible-path style list so the lift eases for free.\n const isEndpointHoveredEdge = !!hoveredAlias && (link.from === hoveredAlias || link.to === hoveredAlias);\n const renderWidth = isHoveredEdge ? Math.min(width * 1.4, 10)\n : isEndpointHoveredEdge ? Math.min(width * 1.15, 8)\n : width;\n return (\n <g\n key={link.key}\n // Round 172 / Loop: edges fade-in alongside the R9\n // staggered node reveal so the canvas first-paint\n // reads as one coordinated wave instead of \"edges\n // pop, nodes ease in\". The .anet-fade-in CSS\n // animation (0.15s ease-out, runs once on mount,\n // R3 origin) plays only on first render — React\n // preserves the <g> via the link.key on subsequent\n // re-renders so the animation doesn't replay when\n // flowLinks recomputes (every 5s SSE poll).\n // Animation-delay offsets each edge by 280ms +\n // 35ms × index so edges start fading in AFTER\n // most nodes have appeared (node R9 stagger caps\n // at ~600ms; ring layout R72 emanates 0→540ms).\n // Cap at 20 indices so a busy fleet with 50\n // flowLinks still finishes within ~1s. Respects\n // prefers-reduced-motion via R29 globals.css\n // blanket that neutralises animation-duration.\n className=\"anet-fade-in\"\n style={{\n animationDelay: `${280 + Math.min(index, 20) * 35}ms`,\n }}\n data-edge-group={link.key}\n >\n {/* Round 48 / Loop: invisible hover hitbox — visible flow\n path is 3-7 px wide and damn hard to hover precisely.\n Stack a transparent 16-px-wide stroke behind it so the\n cursor only needs to be ~8 px from the line for the\n tooltip to fire. Native <title> moves here; the\n visible path no longer needs it. pointer-events on\n the visible path drop to \"none\" since the hitbox\n owns the hover surface. */}\n <path\n d={path}\n fill=\"none\"\n stroke=\"transparent\"\n strokeWidth={Math.max(width + 10, 16)}\n style={{ pointerEvents: 'stroke' }}\n data-edge-hitbox\n onMouseEnter={() => setHoveredEdgeKey(link.key)}\n onMouseLeave={() => setHoveredEdgeKey(prev => prev === link.key ? null : prev)}\n >\n <title>{tooltip}</title>\n </path>\n {/* Round 166 / Loop: stroke-width transition pairs\n with R164 edge badge r-lift. Pre-R166 the\n visible flow path's hover thickening (R50:\n renderWidth = isHoveredEdge ? width * 1.4 :\n width) snapped instantly even though opacity\n transitioned smoothly. Edge hover now lifts\n the line AND the badge in coordinated 300ms\n ease-out motion. Drop the Tailwind transition\n class for inline style so both opacity and\n stroke-width pick up the same timing without\n arbitrary-property class compilation risk.\n data-edge-visible exposes the path for test\n probes (the R48 hitbox sibling already has\n data-edge-hitbox). Respects prefers-reduced-\n motion via the R29 globals.css blanket\n override that neutralises transition-duration\n universally. */}\n {/* Round 245 / Loop: edge surface picks up stroke\n color transition for theme-toggle smoothing.\n R166 already eased opacity + stroke-width on the\n visible flow path; the stroke COLOR (pal.flowEdge:\n cyber cyan ↔ light emerald) and the underlying\n flow-rail's stroke (pal.flowPath: cyber pale-sky\n ↔ light slate-600) still snapped on theme switch.\n The rest of the topology smooths theme through R4\n transitions (status rings) / R242 chat-target ring\n / R244 halos / R241 hub spokes / R240 backdrop\n spokes — R245 closes the edge surface.\n\n Visible flow path: append 'stroke 300ms ease-out'\n to the existing transition list (300ms matches\n R166 opacity + stroke-width pace).\n\n Flow rail (dashed underline): convert the Tailwind\n `transition-opacity` className to inline style so\n we can list opacity AND stroke together at 300ms\n ease-out (same idiom R201 used on the working/\n online chips to splice in additional properties\n beside Tailwind's). data-edge-flow-rail attr\n surfaces the path for test introspection. */}\n {/* Round 381 / Loop: edge visible flow path picks up\n strokeLinecap='round'. Sibling polish to R378\n flow-rail dashed linecap — both flow-element paths\n (visible primary + dashed secondary rail) now share\n 'round' linecap vocabulary. The visible path runs\n source-node → dest-node as one continuous line, so\n the dest-end is covered by the markerEnd arrow and\n the source-end usually sits inside the source-node\n circle. At certain alignments (post-zoom, post-\n layout-switch transitions), the source-end may peek\n out by a fraction of a px past the node edge —\n round caps render that overshoot as a smooth half-\n disc instead of a sharp rectangle. Pure paint\n refinement, geometry-safe (bbox of the stroke\n unchanged at the join with the arrow marker).\n data-edge-visible-linecap attr exposes the value\n for tests. */}\n <path\n d={path}\n fill=\"none\"\n stroke={pal.flowEdge}\n strokeWidth={renderWidth}\n strokeLinecap=\"round\"\n opacity={Math.min(1, (isLight ? 0.22 : 0.28) * fresh * edgeOpacityMul)}\n filter={isLight ? undefined : 'url(#topo-glow)'}\n markerEnd={`url(#${arrowId})`}\n data-edge-visible={link.key}\n data-edge-visible-linecap=\"round\"\n data-edge-visible-endpoint-hovered={isEndpointHoveredEdge ? 'true' : 'false'}\n data-edge-visible-stroke-width={renderWidth}\n style={{\n pointerEvents: 'none',\n transition: 'opacity 300ms ease-out, stroke-width 300ms ease-out, stroke 300ms ease-out',\n }}\n />\n {/* Round 378 / Loop: edge flow-path dashed-rail picks\n up strokeLinecap='round'. Pre-R378 the rail\n rendered '2 12' dashes as sharp 1×2 rectangles\n against the canvas backdrop; default 'butt' caps\n leave dash ends square. R378 rounds each cap so\n the dashes read as soft 3-px pills (1 px stroke +\n 0.5 px round cap each end). The flow-rail is the\n secondary 'invisible-spine' line that gives the\n R57 spoke flow a directional rail to slide along\n — rounding the dashes softens its presence\n against the primary visible flow path (R245 has\n no strokeLinecap so it inherits 'butt' on a\n continuous line, irrelevant). Geometry-safe:\n round caps only widen the visible dash; the\n bbox of the path is unchanged so overlap-test\n invariants hold. data-edge-flow-rail-linecap\n attr exposes the value for tests. */}\n <path\n id={`flow-path-${index}`}\n d={path}\n fill=\"none\"\n stroke={pal.flowPath}\n /* Round 437 / Loop: flow-rail strokeWidth hover lift —\n 1 → 1.5 on (isHoveredEdge || isEndpointHoveredEdge).\n Pre-R437 the dashed rail sat at sw=1 always while the\n visible flow path above it lifted (R50 ×1.4 on\n isHoveredEdge / R436 ×1.15 on isEndpointHoveredEdge).\n The two edge paint layers were mismatched on hover:\n top layer thickened, underline stayed thin — so the\n hover gesture lifted only half the edge surface.\n R437 lifts the underline too so the whole edge\n reads as \"raised\" on hover, not just its bright\n top stripe. Same +0.5 absolute delta R435 used at\n hub-spoke scope (1→1.25 there, slightly bigger\n here because the rail's base 1 is at the kerning\n floor and needs more lift to register).\n Transition list extends to include stroke-width\n 300ms so the new lift eases under the same R166\n cadence as the visible path's stroke-width. */\n strokeWidth={(isHoveredEdge || isEndpointHoveredEdge) ? 1.5 : 1}\n strokeDasharray=\"2 12\"\n strokeLinecap=\"round\"\n opacity={Math.min(1, (isLight ? 0.4 : 0.75) * fresh * edgeOpacityMul)}\n data-edge-flow-rail={link.key}\n data-edge-flow-rail-linecap=\"round\"\n data-edge-flow-rail-stroke-width={(isHoveredEdge || isEndpointHoveredEdge) ? 1.5 : 1}\n data-edge-flow-rail-lifted={(isHoveredEdge || isEndpointHoveredEdge) ? 'true' : 'false'}\n style={{ transition: 'opacity 300ms ease-out, stroke 300ms ease-out, stroke-width 300ms ease-out' }}\n />\n {!reducedMotion && (\n /* Round 103 / Loop: phase-stagger the particles so\n concurrent edges don't pulse in lockstep. SMIL\n `begin` accepts negative offsets to shift the cycle\n backwards in time, which means the particle starts\n mid-flight on first paint — no visible \"all\n particles spawn from source simultaneously\" tell.\n `(index * 0.37) % duration` gives a deterministic,\n well-distributed offset (the golden-ratio-ish 0.37\n fraction prevents lining up when N is a small\n multiple). Edge order is stable (sorted by recent\n activity), so the offsets feel calm rather than\n reshuffling each refresh. */\n /* Round 422 / Loop: edge flow particle radius 4 → 4.5.\n Visual-weight bump family (15th anchor) — particles\n riding along the edge animateMotion path get +0.5px\n radius lift, increasing visual area by ~27%\n (π·4.5² / π·4² = 1.27). Sibling magnitude to R383\n recent-row pip 1.8 → 2.0 (+25% area), R384 minimap\n online dot 1.7 → 1.9 (+25% area). R251 fill +\n R252 transitions + R103 phase-stagger animateMotion\n all preserved. data-edge-particle-radius attr\n exposes the value for tests. */\n <circle\n /* Round 439 / Loop: edge flow particle radius hover\n lift — r 4.5 → 5.5 on (isHoveredEdge ||\n isEndpointHoveredEdge). Continues edge paint-\n layer parity arc (R436 visible path sw / R437\n flow-rail sw / R439 particle r) so the whole\n edge surface — including the moving particle —\n lifts on hover, not just the static stripes.\n +1px radius gives ~50% area boost. Subtler than\n 1.4× sw bump on visible path because the\n particle is already small + motion-bright;\n +1px reads as \"the dot caught attention\"\n without overshadowing the path lift. R252\n transition list extends to include r 200ms so\n the size change eases under the same fill/\n opacity cadence. */\n r={(isHoveredEdge || isEndpointHoveredEdge) ? 5.5 : 4.5}\n fill={pal.flowParticle}\n filter={isLight ? undefined : 'url(#topo-glow)'}\n /* Round 485 / Loop — extends R484's \"inspection\n overrides encoding\" pattern to a 2nd anchor:\n edge particle opacity lifts to 1.0 on\n isHoveredEdge OR isEndpointHoveredEdge (user\n hovering the edge directly OR hovering one\n of its endpoint nodes). Pre-R485 the particle\n inherited freshness × edgeOpacityMul decay\n so a stale edge's particle painted near the\n 0.30 floor even when the operator was\n inspecting it; R485 lifts to 1.0 on attention.\n data-recent-row-ts-alpha-attribute analog —\n freshness encoding preserved on rest tier,\n opacity override engages only on inspection.\n Sibling lift family — inspection-overrides-\n encoding pattern, now 2 anchors:\n R484 recent-row timestamp freshness → 1.0\n R485 edge particle freshness → 1.0 (this)\n data-edge-particle-opacity-lifted attr exposes\n the override gate; data-edge-particle-opacity-\n rest preserves the freshness reading. */\n opacity={(isHoveredEdge || isEndpointHoveredEdge) ? 1 : Math.min(1, fresh * edgeOpacityMul)}\n data-edge-particle={link.key}\n data-edge-particle-radius={(isHoveredEdge || isEndpointHoveredEdge) ? 5.5 : 4.5}\n data-edge-particle-lifted={(isHoveredEdge || isEndpointHoveredEdge) ? 'true' : 'false'}\n data-edge-particle-opacity-rest={Math.min(1, fresh * edgeOpacityMul).toFixed(2)}\n data-edge-particle-opacity-lifted={(isHoveredEdge || isEndpointHoveredEdge) ? 'true' : 'false'}\n style={{ transition: 'fill 200ms ease-out, opacity 200ms ease-out, r 200ms ease-out' }}\n >\n <animateMotion\n dur={`${duration}s`}\n begin={`-${stagger.toFixed(3)}s`}\n repeatCount=\"indefinite\"\n path={path}\n />\n </circle>\n )}\n {/* Round 75 / Loop: arrival ping at the destination. The\n particle currently fades into the arrow marker\n silently — adding a small radiating ring synchronised\n to the particle's period turns message delivery into\n a visible event. begin = -dur*0.92 offsets the\n animation so the ring expands NEAR the end of each\n cycle (≈when the particle arrives). Gated by\n reducedMotion and on fresh > 0.5 — stale edges that\n haven't fired in minutes don't need the eye-grab.\n data-arrival-ping for testability.\n\n Round 279 / Loop: arrival ping RETIRED (R75 + R228\n + R231 + R252 family) per 减法 cut #5. Per active\n edge the SMIL family was: particle (R50\n animateMotion) + arrival ping (R75 r+opacity SMIL)\n + dispatch pulse (R76 r+opacity SMIL). For a 5-\n edge fleet that's 5×3 = 15 simultaneous SMIL.\n The PARTICLE (a moving dot along the path) is the\n primary \"data flowing from A → B\" visual signal;\n ping + pulse are secondary \"arrival/dispatch\n confirmation\" that the moving particle already\n conveys. Cull ping + pulse, keep particle.\n `false &&` gates the render; code preserved for\n rollback. */}\n {false && !reducedMotion && fresh > 0.5 && (\n <circle\n cx={to.x}\n cy={to.y}\n r=\"0\"\n fill=\"none\"\n stroke={pal.flowEdge}\n strokeWidth=\"1.5\"\n opacity=\"0\"\n /* Round 252 / Loop: stroke transition for theme\n toggle. SMIL animates r + opacity continuously;\n stroke is static per render but theme-driven\n (pal.flowEdge: cyber cyan ↔ light emerald). */\n style={{ pointerEvents: 'none', transition: 'stroke 200ms ease-out' }}\n data-arrival-ping={link.key}\n >\n {/* Round 228 / Loop: pulse-pop ease curves on the\n arrival ping SMIL — extends R227's keySplines\n adoption from the click ripple (one-shot, two-\n value linear) to the canvas's repeating delivery-\n confirmation surfaces. Both <animate>s use\n calcMode=spline with keyTimes='0;0.5;1' (two\n segments).\n • r grows 0→14→22 (monotonic): unified ease-out\n across both segments — the ring decelerates\n as it expands, settling at its widest.\n • opacity bumps 0→0.55→0 (pulse): ease-out on\n the rise (fast appearance), ease-in on the\n fall (slow start of fade then accelerates) —\n the canonical \"pulse-pop\" kinetic shape.\n Together r decelerating and opacity pulse-popping\n give the arrival ping a real-event physicality\n instead of the prior linear-velocity tick. */}\n <animate\n attributeName=\"r\"\n values=\"0;14;22\"\n dur={`${duration}s`}\n begin={`-${((stagger + duration * 0.92) % duration).toFixed(3)}s`}\n repeatCount=\"indefinite\"\n calcMode=\"spline\"\n keyTimes=\"0;0.5;1\"\n keySplines=\"0.25 0.1 0.25 1;0.25 0.1 0.25 1\"\n />\n <animate\n attributeName=\"opacity\"\n values=\"0;0.55;0\"\n dur={`${duration}s`}\n begin={`-${((stagger + duration * 0.92) % duration).toFixed(3)}s`}\n repeatCount=\"indefinite\"\n calcMode=\"spline\"\n keyTimes=\"0;0.5;1\"\n keySplines=\"0.25 0.1 0.25 1;0.42 0 1 1\"\n />\n </circle>\n )}\n {/* Round 76 / Loop: source dispatch pulse — mirror to the\n R75 arrival ping. begin = 0s (start of cycle) so the\n ring expands as the particle LEAVES the source. Only\n fires for high-traffic edges (link.count >= 3) — on\n quiet conversations the canvas should stay calm; on\n busy senders the pulse plus arrival ping bookend\n every message in flight, making the topology feel\n alive. Same fresh/reducedMotion gates as R75. Slightly\n smaller radius (0→12→18 vs R75's 0→14→22) so the\n source reads as \"smaller event than arrival\" — the\n destination is the meaningful endpoint.\n\n Round 279 / Loop: dispatch pulse RETIRED with the\n arrival ping (R75) above — same 减法 rationale.\n Particle remains as the sole \"data flow\" SMIL\n signal per active edge. */}\n {false && !reducedMotion && fresh > 0.5 && link.count >= 3 && (\n <circle\n cx={from.x}\n cy={from.y}\n r=\"0\"\n fill=\"none\"\n stroke={pal.flowEdge}\n strokeWidth=\"1.5\"\n opacity=\"0\"\n /* Round 252 / Loop: stroke transition for theme\n toggle. Same idiom as arrival ping above. */\n style={{ pointerEvents: 'none', transition: 'stroke 200ms ease-out' }}\n data-dispatch-pulse={link.key}\n >\n {/* Round 228 / Loop: same pulse-pop curves as the\n arrival ping above. r ease-out + opacity\n ease-out→ease-in. The dispatch pulse is smaller\n (0→12→18 vs arrival's 0→14→22), but the kinetic\n feel should be identical — both bookend a\n single message in flight, so they should\n physically ease the same way. */}\n <animate\n attributeName=\"r\"\n values=\"0;12;18\"\n dur={`${duration}s`}\n begin={`-${stagger.toFixed(3)}s`}\n repeatCount=\"indefinite\"\n calcMode=\"spline\"\n keyTimes=\"0;0.5;1\"\n keySplines=\"0.25 0.1 0.25 1;0.25 0.1 0.25 1\"\n />\n <animate\n attributeName=\"opacity\"\n values=\"0;0.45;0\"\n dur={`${duration}s`}\n begin={`-${stagger.toFixed(3)}s`}\n repeatCount=\"indefinite\"\n calcMode=\"spline\"\n keyTimes=\"0;0.5;1\"\n keySplines=\"0.25 0.1 0.25 1;0.42 0 1 1\"\n />\n </circle>\n )}\n {/* Round 100 / Loop: midpoint count badge for high-\n traffic edges. Width already gauges intensity but\n \"5 vs 12\" is hard to read off stroke alone. Render\n a compact pill at the bezier midpoint (t=0.5 →\n midpoint + perpendicular × lift/2) showing the\n integer count. Threshold link.count >= 3 keeps\n the canvas quiet for sparse traffic. Bezier\n midpoint math: for a quadratic curve M A Q C B,\n the t=0.5 point is (A + 2C + B) / 4, which\n simplifies to (midAB) + 0.5 × (control - midAB)\n = midAB + perpendicular × lift/2. */}\n {(() => {\n // Round 215 / Loop: badge always-mounts; visibility\n // crossfades via the wrapper <g>'s opacity instead of\n // React conditional mount/unmount on link.count >= 3.\n // Pre-R215 a flow's first 2 → 3 message crossing made\n // the badge appear in one frame, and a hot flow\n // tapering 3 → 2 vanished in one frame. R215 extends\n // the always-mount-opacity-gate idiom (R181 / R182 /\n // R183 ring family + R213 hub digit/highlight + R214\n // pulse dot) to the canvas-edge surface. Inner R164\n // r-lift / R188 stroke transitions continue to ease\n // their respective state flips independently. pointer\n // events gated to 'none' when invisible so a sub-\n // threshold badge can't intercept the hitbox click.\n const visible = link.count >= 3;\n const midX = (from.x + to.x) / 2;\n const midY = (from.y + to.y) / 2;\n const dx = to.x - from.x;\n const dy = to.y - from.y;\n const len = Math.hypot(dx, dy) || 1;\n const badgeX = midX + (-dy / len) * lift * 0.5;\n const badgeY = midY + ( dx / len) * lift * 0.5;\n const badgeOpacity = visible ? Math.min(1, fresh * edgeOpacityMul) : 0;\n /* R121: the badge becomes a canvas-side click-to-pin\n affordance. R100 introduced it as a passive count\n display; R116 added pinnedEdgeKey. Joining them\n lets users pin a flow directly from the canvas\n without crossing to the recent-signal panel.\n pointerEvents move from 'none' to 'all' on the\n wrapper <g>; the underlying R48 edge hitbox is\n wider (16 px) than the 18-px badge diameter so\n clicks landing on either still route correctly\n — the badge consumes its small footprint, the\n hitbox owns the rest. stopPropagation on\n pointerdown so the SVG pan capture doesn't\n redirect the click. */\n const isPinned = pinnedEdgeKey === link.key;\n // R126: hot-edge accent. The edge stroke width (line\n // 2344) already scales with count but clamps at 7 —\n // so count=5 and count=50 look identical at the line,\n // and the only signal differentiating them is the\n // integer in the badge. Bucket count into a \"hot\"\n // band (≥ 10) and flip the badge stroke to a warmer\n // tone + thicker ring so busy lanes telegraph at a\n // glance without reading the digit. Reuses the amber\n // family from R125 (chip empty-state) — semantic is\n // \"draw the eye here\"; R125 amber is in the chip row\n // above the SVG, R126 amber is on the canvas, so the\n // surfaces never compete in the same eye-sweep. Pin\n // still wins (uses legendHeadline) so a pinned hot\n // edge reads as \"locked + busy\" via the badge text\n // and edge brightness, not via a third stroke colour.\n const isHot = link.count >= 10;\n const hotStroke = isLight ? '#d97706' : '#fbbf24';\n return (\n <g\n data-edge-count-badge={link.key}\n data-edge-count-badge-pinned={isPinned ? 'true' : 'false'}\n data-edge-count-badge-hot={isHot ? 'true' : 'false'}\n data-edge-count-badge-visible={visible ? 'true' : 'false'}\n role={visible ? 'button' : undefined}\n tabIndex={visible ? 0 : -1}\n aria-pressed={visible ? isPinned : undefined}\n aria-hidden={visible ? undefined : true}\n className=\"anet-topo-svg-focus\"\n style={{\n pointerEvents: visible ? 'all' : 'none',\n cursor: visible ? 'pointer' : undefined,\n transition: 'opacity 300ms ease-out',\n }}\n opacity={badgeOpacity}\n onPointerDown={(e) => e.stopPropagation()}\n // R122: badge hover propagates to hoveredEdgeKey so\n // moving the cursor onto the badge lights the\n // same endpoint rings + edge brighten as hovering\n // the line. R121 only wired click; the badge sat\n // visually separate from the line on hover,\n // which felt like two surfaces rather than one.\n onMouseEnter={() => setHoveredEdgeKey(link.key)}\n onMouseLeave={() => setHoveredEdgeKey(prev => prev === link.key ? null : prev)}\n // Round 185 / Loop: edge badge click fires the same\n // one-shot expanding-ring ripple R14 uses for node\n // click and R52 uses for hub click — anchored at\n // the badge midpoint with the edge's own flowEdge\n // colour. Closes the click-feel idiom across all\n // three pinnable canvas surfaces (hub / node /\n // edge badge). Reused setClickRipple state machine\n // so only one ripple at a time; ts coordinate\n // guard in the setTimeout cleanup prevents an\n // older ripple from clobbering a newer one if\n // the user clicks two badges in quick succession.\n onClick={(e) => {\n e.stopPropagation();\n setPinnedEdgeKey(prev => prev === link.key ? null : link.key);\n const ts = Date.now();\n setClickRipple({ ts, x: badgeX, y: badgeY, r0: 10.5, color: pal.flowEdge });\n setTimeout(() => setClickRipple(prev => prev && prev.ts === ts ? null : prev), 600);\n }}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n setPinnedEdgeKey(prev => prev === link.key ? null : link.key);\n const ts = Date.now();\n setClickRipple({ ts, x: badgeX, y: badgeY, r0: 10.5, color: pal.flowEdge });\n setTimeout(() => setClickRipple(prev => prev && prev.ts === ts ? null : prev), 600);\n }\n }}\n >\n <title>{isPinned\n ? `${link.from} → ${link.to} (${link.count}) — click to release pin`\n : `${link.from} → ${link.to} (${link.count}) — click to pin`}</title>\n {/* Round 164 / Loop: edge badge gains hover-lift\n to match the 5-surface hover-elevation\n family (R51 node / R135 panel / R142 group\n box / R143 recent row / R144 legend row).\n Pre-R164 the badge had R122 hover→edge-brighten\n propagation but the badge ITSELF stayed\n static, so the cursor-on-target feedback\n felt mismatched with every other interactive\n surface. Bumping r 9 → 10.5 on hover OR pin\n gives the same \"lift\" gesture in canvas\n space (the badge sits on a curved path, so\n translate-Y wouldn't track the line; radius\n growth is the SVG-native equivalent). Pin\n and hover share the lift so a pinned badge\n stays visually raised even after mouseleave —\n mirrors R143/R144 where row pin gets the\n same lift as row hover. Pinned still keeps\n its R121 stroke change (legendHeadline +\n width 2) so pin and hover stay\n discriminable on the same lifted state.\n strokeWidth stays at 1 / 2 — won't trip the\n R51 overlap-test sentinels (1.5 / 3 are\n reserved). transition keeps the lift smooth\n (180ms ease-out) and respects prefers-\n reduced-motion via the globals.css blanket\n override that neutralises transitions.\n\n Six surfaces now share the hover-elevation\n idiom: nodes (R51), panels (R135), group\n boxes (R142), recent rows (R143), legend\n rows (R144), edge badges (R164). */}\n {/* Round 188 / Loop: extend the badge transition to\n include stroke + stroke-width. R164 added the\n r 9↔10.5 lift; R188 closes the smoothness gap\n on the R121 pin-stroke flip (cyan flowEdge ↔\n legendHeadline) and R126 hot-lane flip (cyan\n ↔ amber). Both used to snap when crossing the\n state boundary (pin click, or count crossing\n 10). Now they ease 300ms through the colour\n and width change — same idiom R167 uses for\n the node status ring. The badge strokeWidth\n values are 1/2 (not R51 sentinels 1.5/3) so\n the always-rendered badge stays invisible to\n the overlap-test guard rails. */}\n {/* Round 251 / Loop: edge badge circle transition\n list grows fill + opacity at 200ms so theme\n toggle no longer snaps the badge background\n while the rest of the circle eases.\n Pre-R251:\n r 180ms (R164 hover lift)\n stroke 300ms (R188 hot/pinned colour flip)\n stroke-width 300ms (R188 hot/pinned width flip)\n fill (pal.legendBox.fill: cyber #020617 ↔ light\n #ffffff) and opacity (cyber 0.82 ↔ light 0.95)\n were theme-driven but missed from the list —\n the badge chrome snapped on theme switch while\n the per-edge ring + visible flow path (R245)\n and per-node surfaces (R246) all eased.\n R251 closes the per-edge surface theme-toggle\n smoothness — every theme-driven property on\n every edge element now eases under cyber↔light. */}\n {/* Round 367 / Loop: edge midpoint badge rest\n stroke-width 1 → 1.25. Sibling visual-weight\n bump family (7th canvas anchor now):\n R287 minimap viewport stroke 1 → 1.5\n R295 legend swatch base radius 5.5 → 6\n R359 recent-row pip base radius 1.6 → 1.8\n R360 hub digit fontSize 11 → 12\n R361 edge-badge digit fontSize 10 → 11\n R365 hub-highlight base radius 5 → 5.5\n R367 edge-badge rest stroke 1 → 1.25 (this round)\n Cold edge badges gain ~25 % stroke presence\n (1.25/1.0 = 1.25). Stays clear of the R51\n overlap-test sentinel values (1.5 / 3 reserved\n for node strokes — the test selector is gated\n to g[data-node] ancestors so this edge-internal\n circle is invisible to that probe anyway, but\n 1.25 is a safe non-sentinel value regardless).\n R188 transition list 'stroke-width 300ms ease-\n out' still smoothes the hot/pin flip — now\n 1.25 → 2 instead of 1 → 2, same ease pace.\n data-edge-badge-stroke-width-rest exposes the\n new baseline for tests. */}\n {/* Round 371 / Loop: edge-badge cyber opacity 0.82\n → 0.85. Sibling theme-consistency polish to R370\n hub hover-ring 0.7 → 0.8. R251 designed this\n badge with opacity 0.82 (cyber) / 0.95 (light)\n — 13 % delta. Cyber-theme dark bg needs more\n alpha to read as 'present'; R371 narrows the\n gap to 10 %, bringing the badge closer to light\n theme's 0.95 floor. Light stays at 0.95\n (already in the legibility band). data-edge-\n badge-opacity attr exposes the resolved value.\n Theme-consistency polish family:\n R246/R247 panel transition family\n R251 edge badge fill/opacity baseline\n R370 hub hover-ring cyber 0.7 → 0.8\n R371 edge badge cyber 0.82 → 0.85 (this round)\n R164 r=9/10.5 hover-lift + R188/R251 transition\n list + R367 strokeWidth=1.25 cold rest preserved. */}\n {/* Round 394 / Loop: edge-badge gains a hover\n strokeWidth tier (1.5) between cold rest\n (R367 1.25) and pin/hot (2). Pre-R394 the\n badge lifted only its radius on hover (R164\n 9 → 10.5); the stroke stayed at cold rest\n 1.25 unless pin/hot kicked in, so a plain\n hover felt half-lifted — geometry expanded\n while the contour stayed thin. R394 adds\n strokeWidth=1.5 on isHoveredEdge so hover\n now lifts both r AND stroke in concert —\n same pattern R385 used for the hub hover-\n ring (1.5 → 1.75) where the ring's three\n hover axes (r grow / opacity fade-in /\n stroke thicken) all rise together.\n Three-tier stroke hierarchy now:\n cold rest 1.25 (R367)\n hovered 1.5 (R394 — this round)\n pinned / hot 2.0 (R188)\n R51 sentinel concern: strokeWidth=1.5 is\n one of the two sentinels reserved for node\n detection, but the R51 selector is gated\n to `g[data-node]` ancestors so this edge-\n internal circle is invisible to the probe\n (same lesson R177 hub hover-ring + R367\n cold rest documented). 300ms strokeWidth\n transition already in the style list eases\n the new tier naturally. data-edge-badge-\n stroke-width-hover attr exposes the hover\n value for tests. */}\n {/* Round 395 / Loop: edge-badge gains a third\n hover axis — opacity 0.85 (cyber) / 0.95\n (light) → 1.0 on isHoveredEdge. Pre-R395\n hovering thickened the stroke (R394 1.25 →\n 1.5) and grew the radius (R164 9 → 10.5)\n but the badge's translucency stayed put at\n R371's rest alpha (cyber 0.85 / light 0.95).\n R395 lifts hover to a clean 1.0 — fully\n opaque — so the hovered badge reads as\n \"in focus\" against the dim siblings.\n Three-axis hover-lift parity now complete:\n hub hover-ring (R177/R370/R385):\n r 14 → 17, opacity 0 → 0.8 cyber, sw 1.5 → 1.75\n edge badge (R164/R394/R395):\n r 9 → 10.5, sw 1.25 → 1.5, opacity → 1.0\n 200ms opacity transition (already in the\n style list) eases the new axis naturally.\n R371 rest opacity (0.85 cyber / 0.95 light)\n preserved as the resting alpha — R395\n adds an isHoveredEdge override on top.\n data-edge-badge-opacity-hover attr exposes\n the hover value for tests. */}\n {/* Round 396 / Loop: extend the R395 opacity → 1.0\n lift to the pinned state. Pre-R396 the badge\n shared `r=10.5` on both hover AND pin (R164\n unified-lift) but R395's opacity lift fired\n ONLY on isHoveredEdge — pinned badges stayed\n at R371 rest alpha (cyber 0.85 / light 0.95).\n That left pin (sticky selection) reading\n softer than hover (transient preview), even\n though pin is the stronger commitment.\n R396 unifies hover + pin at opacity=1.0\n so the same data-edge-badge-lifted='true'\n surface uniformly carries full alpha. Pin\n stroke (R188 sw=2 + pal.legendHeadline color)\n continues to differentiate pin from hover —\n the opacity track now closes the lift parity.\n The new gate (isHoveredEdge || isPinned)\n mirrors the existing R164 r-lift gate, so\n the badge has a single \"active state\"\n signature across r + opacity.\n 200ms opacity transition (already in style\n list) eases pin/unpin naturally. R371 rest\n opacity preserved as the resting alpha.\n data-edge-badge-opacity-hover renamed\n semantically to -active (covers hover+pin)\n via the new -opacity-active attr; the\n legacy -opacity-hover attr kept for R395\n test compatibility. */}\n {/* Round 480 / Loop — 5th anchor in the drop-shadow\n visual-polish family. Gates on isHot (link.\n count >= 10, R129 hot-lane threshold) so the\n badge gets a warm-amber halo when its edge\n crosses the high-traffic boundary.\n Drop-shadow family ledger now:\n R476 hub digit hover-gated emerald\n R477 legend pin-ring pin-gated row.fill\n R478 freshness pip freshness-gated cyan\n R479 group label pin-gated cyan\n R480 edge badge hot-lane-gated amber ← this round\n 5th gate type — traffic volume — joins hover,\n pin, freshness, pin. Each polish anchor uses\n a distinct semantic gate but the same paint\n vocabulary. Hue: hotStroke (amber-tinted\n palette member) at 0x80 alpha — picks up the\n R126/R188 hot-edge accent colour family so\n the glow reads as a chromatic extension of\n the existing hot-lane stroke. 3-px blur\n radius reads as soft heat rather than\n emergency klaxon.\n R51 sentinel safety: badge sw=2 only matters\n when the overlap probe runs on g[data-node]\n descendants, which this edge-internal badge\n is not. Filter is paint-only, bbox unchanged.\n transition list extends to include 'filter\n 200ms ease-out' so the heat halo eases on\n the count-crosses-threshold flip. */}\n <circle\n cx={badgeX} cy={badgeY}\n r={isHoveredEdge || isPinned ? 10.5 : 9}\n fill={pal.legendBox.fill}\n stroke={isPinned ? pal.legendHeadline : isHot ? hotStroke : pal.flowEdge}\n strokeWidth={isPinned ? 2 : isHot ? 2 : isHoveredEdge ? 1.5 : 1.25}\n opacity={(isHoveredEdge || isPinned) ? 1 : (isLight ? 0.95 : 0.85)}\n data-edge-badge-lifted={(isHoveredEdge || isPinned) ? 'true' : 'false'}\n data-edge-badge-stroke-width-rest=\"1.25\"\n data-edge-badge-stroke-width-hover=\"1.5\"\n data-edge-badge-opacity={(isHoveredEdge || isPinned) ? 1 : (isLight ? 0.95 : 0.85)}\n data-edge-badge-opacity-rest={isLight ? 0.95 : 0.85}\n data-edge-badge-opacity-hover=\"1\"\n data-edge-badge-opacity-active=\"1\"\n data-edge-badge-glow={isHot ? 'true' : 'false'}\n style={{\n filter: isHot\n ? `drop-shadow(0 0 3px ${hotStroke}80)`\n : undefined,\n transition: 'r 180ms ease-out, stroke 300ms ease-out, stroke-width 300ms ease-out, fill 200ms ease-out, opacity 200ms ease-out, filter 200ms ease-out',\n }}\n />\n {/* Round 224 / Loop: edge badge text gains the 4th\n pin-signature typography. Pre-R224 the digit\n rendered with no transition surface: when the\n flow crossed count=10 (isHot flip) the badge\n stroke eased 300ms (R188) but the digit itself\n stayed dead-typographic. R224 adds two clean\n improvements stacked on the same <text> node:\n\n 1) fontVariantNumeric: 'tabular-nums' — locks\n digit width so a 9→10 transition doesn't\n jitter the textAnchor='middle' centering by\n half a glyph. The badge is on a curved\n flow path; any width-change of the centered\n digit visibly shifts the anchor relative\n to the underlying circle. Info-density\n win — digits transition cleanly without\n pixel-jitter at the boundary.\n\n 2) letterSpacing pin signature, 4th surface\n after R218 group label / R219 legend row /\n R220 recent row. Baseline 0px; widens to\n 0.4px when isPinned || isHot. The transition\n marks the \"this lane just went special\"\n event typographically — same 300ms cadence\n as the R188 stroke flip, so the badge stroke\n + text co-ease on the hot/pin threshold.\n '0px' resolves to keyword 'normal' in\n computed style (R218 test trap learned);\n test parsers must accept either form.\n\n data-edge-badge-text-pin attr surfaces the\n isPinned||isHot state for introspection. */}\n <text\n x={badgeX} y={badgeY + 3}\n textAnchor=\"middle\"\n fill={pal.legendHeadline}\n /* Round 361 / Loop: edge midpoint badge text\n fontSize 10 → 11. Sibling visual-weight bump\n to R360 hub digit 11 → 12. The badge digit\n is the per-edge equivalent of the hub digit\n — a high-information scalar (link.count) at\n a stable canvas position. Pre-R361 fontSize=\n 10 + R220 letter-spacing 0.4 + R224 tabular-\n nums made the digit READABLE but small\n against the r=9 / 18-px badge envelope;\n fontSize=11 nudges the glyph ~10 % bigger\n (bbox ~7×10 px from ~6×9 px) so the count\n reads more cleanly at glance — still well\n inside the r=9 idle circle and the r=10.5\n hover/pin lift (R164). y=badgeY+3 empirical\n vertical centring kept (1px drift at the\n bumped size is below the noise floor in\n the on-curve flow path).\n Visual-weight bump family:\n R287 minimap viewport stroke 1 → 1.5\n R295 legend swatch base radius 5.5 → 6\n R359 recent-row pip base radius 1.6 → 1.8\n R360 hub digit fontSize 11 → 12\n R361 edge-badge digit fontSize 10 → 11 (this round)\n data-edge-badge-text-font-size attr exposes\n the value for tests. R220 pin/hot letter-\n spacing tween + R224 tabular-nums + R188\n stroke-width pin/hot transitions all preserved. */\n fontSize=\"11\"\n fontFamily=\"monospace\"\n /* R426 — edge-badge digit fontWeight 700 → 800 on\n (isPinned || isHot). 4th anchor on the \"data\n tightens under attention\" typographic-weight\n pattern:\n R416 chip-digit (chip-row hover trigger)\n R424 panel-digit (panel hover trigger)\n R425 hub-digit (hub hover trigger)\n R426 edge-badge-digit (pin/hot trigger) ← this\n The badge digit is the per-edge equivalent of\n the hub digit (R361 sibling fontSize bump\n reasoning). Stacks with R188 stroke-width pin/\n hot lift (1.25 → 1.5) + R220 letter-spacing pin/\n hot tween (0 → 0.4) for a 3-axis pin/hot signa-\n ture (edge structure + text spacing + text\n weight). The R408 transition is letter-spacing\n 300ms; R426 appends font-weight 300ms so the\n weight bump co-eases under the same cadence. */\n fontWeight={(isPinned || isHot) ? '800' : '700'}\n data-edge-badge-text={link.key}\n data-edge-badge-text-pin={(isPinned || isHot) ? 'true' : 'false'}\n data-edge-badge-text-font-size=\"11\"\n style={{\n pointerEvents: 'none',\n fontVariantNumeric: 'tabular-nums',\n /* R431 — edge-badge digit 3-tier letter-spacing:\n rest 0 / isHoveredEdge 0.2 / (isPinned || isHot) 0.4.\n Mirrors R427 node-alias 3-tier (rest/hover/chat-\n target → 0/0.3/0.5) at edge-badge scope. Pre-R431\n letter-spacing only fired on pin/hot (R220) while\n pure edge hover lifted stroke (R394) + opacity\n (R395) + radius (R164) but left the text dead-\n typographic. R431 adds the missing typographic\n spacing axis to the edge-hover gesture so the\n text rises with the badge geometry. Pin/hot\n tier (0.4) still wins; hover is the mid step.\n Hover-letter-spacing family extension (7 anchors\n now): R344/R345/R347/R351/R420/R427/R431. */\n letterSpacing: (isPinned || isHot) ? '0.4px' :\n isHoveredEdge ? '0.2px' : '0px',\n transition: 'letter-spacing 300ms ease-out, font-weight 300ms ease-out',\n }}\n >{link.count}</text>\n </g>\n );\n })()}\n </g>\n );\n })}\n\n {/* center hub — round 39 sized + round 13 restraint.\n The hub is the control-plane anchor. r39 gave it two outward\n pulses (r 10→38, 2.4s, double-phase) which read as \"loudest\n node in the room\" — wrong semantics: anchors don't emit,\n they hold. r13 swaps the kinetic pulse for a slow opacity\n breath on the static halo (4s, ±15% from base), so the hub\n stays alive without throwing kinetic energy outward. The\n dual-circle \"lit lamp\" core is unchanged. */}\n {layout === 'ring' && (<g\n data-topo-hub\n data-topo-hub-hovered={hoveredHub ? 'true' : 'false'}\n // Round 159 / Loop: the hub is the most visually\n // prominent interactive element on the canvas (R39\n // enlarged it, R52 made it click to fitView, R115\n // added a hover ring, R43 gave it a tooltip) — but\n // R151-R157's a11y sweep skipped it. role/tabIndex/\n // aria-label/onKeyDown bring it to parity with node <g>\n // (R151), group label hit (R152), and minimap (R157).\n // anet-topo-svg-focus picks up R156's explicit cyan\n // focus ring (default browser SVG focus rect is hard\n // to see against the dark canvas).\n role=\"button\"\n tabIndex={0}\n aria-label={(() => {\n const parts = ['Network hub'];\n if (onlineNodes.length > 0) parts.push(`${onlineNodes.length} online`);\n if (workingCount > 0) parts.push(`${workingCount} working`);\n if (flowLinks.length > 0) parts.push(`${flowLinks.length} active link${flowLinks.length === 1 ? '' : 's'}`);\n return parts.join(' · ') + ' — Enter to fit view';\n })()}\n // Round 176 / Loop: hub joins the first-paint fade-in\n // family as the 6th surface. The hub is the visual anchor\n // — every other ring-layout reveal layer (R174 tier rings,\n // R9 nodes, R172 edges) emanates outward FROM it — yet\n // pre-R176 the hub itself popped in instantly while the\n // wave it should be leading staggered around it. Adding\n // .anet-fade-in at delay 0 (no animation-delay needed)\n // places the hub as the canvas-center anchor that the\n // tier wave grows from. Composes cleanly with the existing\n // anet-topo-svg-focus class (R159 keyboard focus ring).\n className=\"anet-topo-svg-focus anet-fade-in\"\n data-topo-hub-fade-delay={0}\n style={{ cursor: 'pointer' }}\n // Stop pointerdown from reaching the SVG pan handler — same\n // reason as the node <g>: a captured pointer makes Chromium\n // fire the follow-up click on the SVG, not this group.\n onPointerDown={(e) => e.stopPropagation()}\n onMouseEnter={() => setHoveredHub(true)}\n onMouseLeave={() => setHoveredHub(false)}\n // Round 52 / Loop: hub is the visual anchor but had no click\n // action — users discovered fit-to-content only via the `f`\n // key or the chrome button. Wire the hub to fitView() so the\n // most prominent element in the canvas is also the \"re-\n // center\" affordance. A click-ripple keyed by ts confirms\n // the gesture; the hub <title> now ends with a hint.\n onClick={() => {\n fitView();\n setClickRipple({ ts: Date.now(), x: cx, y: cy, r0: 18, color: isLight ? '#059669' : '#10b981' });\n setTimeout(() => setClickRipple(prev => prev && prev.x === cx && prev.y === cy ? null : prev), 600);\n }}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n fitView();\n setClickRipple({ ts: Date.now(), x: cx, y: cy, r0: 18, color: isLight ? '#059669' : '#10b981' });\n setTimeout(() => setClickRipple(prev => prev && prev.x === cx && prev.y === cy ? null : prev), 600);\n }\n }}\n >\n {/* Round 43 / Loop: hub `<title>` summary — hovering the\n central glow now answers \"what is this?\" with a one-line\n fleet snapshot. Duplicates the header chips by design;\n hovering the most visually prominent element is the\n most natural impulse, so satisfy it where the cursor\n already is. Falls clean: omits sub-clauses when their\n counts are zero. R52 appends a \"click to fit view\" hint.\n */}\n <title>{(() => {\n const total = sessions.length;\n const parts = [`Network hub`, `${total} session${total === 1 ? '' : 's'}`];\n if (onlineNodes.length > 0) parts.push(`${onlineNodes.length} online`);\n if (workingCount > 0) parts.push(`${workingCount} working`);\n if (flowLinks.length > 0) parts.push(`${flowLinks.length} active link${flowLinks.length === 1 ? '' : 's'}`);\n return parts.join(' · ') + '\\nclick to fit view';\n })()}</title>\n {/* grounding halo — breathes in opacity, no expansion.\n R84: breath amplitude + tempo reflect workingCount. An\n idle fleet keeps the original gentle 0.32→0.52 (light) /\n 0.08→0.16 (dark) cycle over 4s — \"heart at rest\". As\n working sessions accumulate, the peak climbs and the\n period shortens, capped at 2.4s so it never feels\n manic. Quiet fleets see zero change; busy fleets feel\n the canvas working. */}\n {(() => {\n // Bucket workingCount to keep the visual feedback discrete\n // rather than continuous: 0 / 1-2 / 3-5 / 6+. Three buckets\n // are enough — finer gradations are imperceptible.\n const busy = workingCount === 0 ? 0\n : workingCount <= 2 ? 1\n : workingCount <= 5 ? 2\n : 3;\n // Round 404 / Loop: hub-halo cyber trough opacity 0.08 →\n // 0.10. Pre-R404 the breath's low-point sat at α=0.08\n // cyber (per R84 family tuning) — the halo nearly faded\n // out at trough on the dark canvas. R404 lifts cyber\n // trough to 0.10. Per-bucket peak amplitudes [0.16/0.20/\n // 0.26/0.32] stay exactly tuned.\n //\n // Round 405 / Loop: hub-halo LIGHT trough 0.32 → 0.34 —\n // symmetric +0.02 lift to mirror R404's cyber treatment\n // across both themes. Pre-R405 only cyber got the lift\n // (R404 docstring noted \"light already at the strong\n // end\" as deliberate); but the cyber/light delta in\n // R404 was an inconsistency in the family pattern.\n // R405 closes the symmetry — both themes get +0.02\n // baseline lift, so the breath low-point reads with\n // matching confidence regardless of theme. Light peak\n // array [0.52/0.58/0.65/0.72] stays tuned.\n //\n // Stale-state legibility lift family (5 anchors now):\n // R317 subordinate-text gray-500 → gray-400\n // R358 freshness floor 0.25 → 0.30\n // R372 minimap offline-dot opacity 0.5 → 0.6\n // R404 hub-halo cyber trough 0.08 → 0.10\n // R405 hub-halo light trough 0.32 → 0.34 (this round)\n //\n // R84 per-bucket peak/dur + R245 ease-in-out spline\n // keySplines all preserved. Test fixture probes the\n // SMIL <animate> values via data-topo-hub-halo-trough\n // attr (now exposes both light + cyber resolved values).\n const peakLight = [0.52, 0.58, 0.65, 0.72][busy];\n const peakDark = [0.16, 0.20, 0.26, 0.32][busy];\n const troughLight = 0.34;\n const troughDark = 0.10;\n const dur = [4.0, 3.2, 2.7, 2.4][busy];\n const valuesLight = `${troughLight};${peakLight};${troughLight}`;\n const valuesDark = `${troughDark};${peakDark};${troughDark}`;\n // Round 408 / Loop: hub-halo radius 18 → 20. The\n // grounding halo (the breathing outer circle around\n // the hub center) is the canvas's signature breath\n // element — R84 family. R408 bumps r=18 → 20 so the\n // breath extends slightly further while keeping 4px\n // clearance before the spoke origin (still room for\n // spoke start anchors). Visual presence on the\n // canvas focal point lifts ~23% area (π·20²/π·18²\n // = 1.23) without changing the per-bucket opacity\n // envelope or breath rhythm. Visual-weight bump\n // family 13th anchor — pairs with R404/R405 trough\n // lifts so the halo now breathes both with more\n // visible amplitude AND more visual footprint.\n // R84 per-bucket peak/dur invariants + R244 calc-\n // Mode='spline' + R245 ease-in-out keySplines all\n // preserved. data-topo-hub-halo-radius attr exposes\n // value for tests.\n /* Round 451 / Loop: hub center halo radius lift on\n hoveredHub — r 20 → 22 (+2px, ~21% area). Adds another\n geometric axis to the hub-hover signature stack\n alongside R177 ring radius lift + R209 digit scale +\n R425 digit fw + R370 halo opacity + R386 highlight\n opacity + R441 core fill chroma. Pre-R451 the halo\n r stayed planted at R408's 20px while the rest of\n the hub structure responded to hover. R451 makes\n the halo breath outward on hover so the focal pulse\n intensifies under attention. SMIL `<animate>` on\n opacity continues independently (animateAttr=\n 'opacity' vs CSS-property r — non-conflicting). R408\n base radius 20 preserved as rest; +2 hover delta\n keeps clearance from the R177 hub-hover-ring at\n r=17 hover (halo is BEHIND the ring, halo r=22 sits\n 5px beyond the ring's hover-r=17, still well within\n the hub canvas envelope). data-topo-hub-halo-radius\n attr now reports the dynamic value. */\n const isHaloHovered = !reducedMotion && hoveredHub;\n const haloR = isHaloHovered ? 22 : 20;\n return (\n <circle\n cx={cx} cy={cy}\n fill={isLight ? '#d1fae5' : '#10b981'}\n opacity={isLight ? 0.42 : 0.12}\n data-hub-busyness={busy}\n data-topo-hub-halo-radius={haloR}\n data-topo-hub-halo-hovered={isHaloHovered ? 'true' : 'false'}\n data-topo-hub-halo-trough={isLight ? troughLight : troughDark}\n data-topo-hub-halo-peak={isLight ? peakLight : peakDark}\n /* Round 253 / Loop: hub grounding halo fill transition\n for theme toggle. Pre-R253 the base fill (cyber\n #10b981 ↔ light #d1fae5) snapped while R244's SMIL\n animate on opacity continued running. CSS fill\n transition is independent of the SMIL animate\n (different attributes), so they compose without\n conflict.\n R451: r as CSS property (R197/R198 idiom) so the\n hover-radius tween eases smoothly under the same\n 200ms cadence as fill. */\n style={{\n r: `${haloR}px`,\n transition: 'fill 200ms ease-out, r 200ms ease-out',\n } as React.CSSProperties}\n >\n {/* Round 244 / Loop: hub grounding halo breath gets\n ease-in-out keySplines, matching the active-node\n pulse (R243) treatment. Pre-R244 default linear\n calcMode marched opacity at constant velocity\n through the 3-value trough→peak→trough bounce —\n mechanical pacing for a 'heartbeat at rest'\n visual. R244 adds calcMode='spline' + keyTimes\n '0;0.5;1' + keySplines '0.42 0 0.58 1' ×2 (CSS\n ease-in-out on both halves), so the breath\n decelerates near the troughs AND the peak —\n lingers briefly at each extreme like a real\n heart-rest cycle. Same SMIL-easing family as\n R227 (click ripple) / R228 (edge ping+pulse) /\n R243 (active-node pulse). The breath family\n is now 2 surfaces deep at this single hub\n element — R84 amplitude/tempo bucket + R244\n curve shape. */}\n {!reducedMotion && (\n <animate\n attributeName=\"opacity\"\n values={isLight ? valuesLight : valuesDark}\n dur={`${dur}s`}\n repeatCount=\"indefinite\"\n calcMode=\"spline\"\n keyTimes=\"0;0.5;1\"\n keySplines=\"0.42 0 0.58 1;0.42 0 0.58 1\"\n />\n )}\n </circle>\n );\n })()}\n {/* core — 20px diameter, larger inner highlight reads as a \"lit lamp\"\n Round 248 / Loop: hub center core gets a fill transition.\n Pre-R248 the core circle (the visual anchor at the centre\n of the canvas, fill=isLight ? '#059669' emerald-600 :\n '#10b981' emerald-500) hard-flipped on theme toggle —\n the most visually prominent element on the canvas\n snapping while everything else (R244 halo / R241 hub\n spokes / R246 label cards / R247 side panels) eased.\n Inline transition closes the gap. data-topo-hub-core\n attr added for test introspection (the parent <g> at\n line 3587 has data-topo-hub but the core specifically\n is the canvas anchor). */}\n {(() => {\n /* Round 441 / Loop: hub center core fill brighten on\n hoveredHub. Pre-R441 the core was static (cyber\n emerald-500 #10b981 / light emerald-600 #059669) and\n the hub-hover gesture lifted ring radius (R177) +\n digit scale (R209) + digit fw (R425) + halo opacity\n (R370) + highlight opacity (R386) but the focal core\n ITSELF stayed planted at rest tone. R441 shifts the\n fill one emerald tier brighter on hover so the canvas\n anchor itself responds:\n cyber emerald-500 → emerald-400 (#10b981 → #34d399)\n light emerald-600 → emerald-500 (#059669 → #10b981)\n Same +100 step on the emerald scale across both themes.\n Pure paint axis; no geometry change. R248 fill 200ms\n transition already in the style list eases the shift.\n Closes the chroma axis on the hub-hover gesture stack:\n R177 ring radius lift geometry\n R209 digit scale 1.08 geometry\n R425 digit fw 700 → 800 typography\n R370 halo opacity 0.7 → 0.8 paint\n R386 highlight opacity paint\n R441 core fill brighten chroma ← this round\n data-topo-hub-core-hovered + -fill attrs exposed\n for tests. */\n const isCoreHovered = !reducedMotion && hoveredHub;\n const coreFill = isLight\n ? (isCoreHovered ? '#10b981' : '#059669')\n : (isCoreHovered ? '#34d399' : '#10b981');\n return (\n <circle\n cx={cx} cy={cy} r=\"10\"\n fill={coreFill}\n data-topo-hub-core\n data-topo-hub-core-hovered={isCoreHovered ? 'true' : 'false'}\n data-topo-hub-core-fill={coreFill}\n style={{ transition: 'fill 200ms ease-out' }}\n />\n );\n })()}\n {/* R130 / Loop: when workingCount > 0, the decorative inner\n highlight gets replaced with the workingCount digit. The\n R84 busyness breath already encodes the same metric\n through motion — adding the digit gives it a second\n visual channel right at the canvas's focal point. A\n user glancing at the hub now sees both \"the network is\n pulsing\" (motion) AND \"3 agents are working\" (digit)\n without having to scan the chip row or panels.\n\n Geometry: text at (cx, cy) with fontSize 11 monospace\n + fontWeight 700 sits inside the r=10 core (a 2-digit\n 12 reads ~12 px wide × 11 px tall, well inside the\n 20-px diameter core). Centered vertically via dy=\n \"0.36em\" — the standard SVG trick for text-vertical-\n center without measuring fontMetrics.\n\n pointerEvents:none so the digit can't intercept the\n hub click (R52 fit-to-view still fires).\n\n workingCount=0 falls through to the existing\n decorative highlight so the hub never looks empty. */}\n {/* Round 213 / Loop: hub centre crossfades the workingCount\n digit and the R130 decorative highlight when count\n crosses zero, instead of mount/unmount snap. Pre-R213 a\n fleet going from idle (workingCount=0, highlight circle)\n to first-working-node (workingCount=1, digit \"1\") swapped\n the elements in a single frame — visible flash at the\n hub's focal point. R213 uses the always-mount + opacity-\n gate pattern (R181/R182/R183 family) so both render\n concurrently and crossfade via opacity transitions.\n Geometry already overlaps (both centred on cx,cy with\n r=5 / digit-bbox ~7×11), so the dual-render adds zero\n layout cost. Reduced-motion users see a 0ms duration\n via the R29 globals.css blanket override. */}\n {/* digit (visible when workingCount > 0) */}\n <text\n x={cx} y={cy}\n textAnchor=\"middle\"\n dy=\"0.36em\"\n fill={isLight ? '#d1fae5' : '#ecfdf5'}\n /* Round 360 / Loop: hub working-count digit fontSize 11\n → 12. The hub is the canvas's focal point — its digit\n is the most-read scalar on the whole topology. R130\n sized it at 11 (well inside the r=10 / 20-px core);\n R360 nudges it to 12 (~13 px wide × 12 px tall, still\n well inside the 20-px diameter) for ~9 % more presence.\n Sibling visual-weight bump family:\n R287 minimap viewport stroke 1 → 1.5\n R295 legend swatch base radius 5.5 → 6\n R359 recent-row pip radius 1.6 → 1.8\n R360 hub digit fontSize 11 → 12 (this round)\n The R209 scale-1.08-on-hub-hover, R225 tabular-nums,\n R253 fill transition, R213 always-mount opacity gate\n all preserved. data-topo-hub-working-count-font-size\n attr exposes the value for tests. */\n fontSize=\"12\"\n fontFamily=\"monospace\"\n /* R425 — hub digit fontWeight 700 → 800 on hoveredHub.\n Closes the \"data tightens under attention\" pattern\n across three focal scopes: chip-digit (R416, chip\n scope) → panel-digit (R424, panel-header scope) →\n hub-digit (R425, hub focal scope). The hub digit is\n the most-read scalar on the topology; adding a weight\n axis on hover stacks with the R209 scale-1.08 + R177\n ring grow + R370 halo opacity + R386 highlight\n opacity hub-hover gestures, giving the focal point\n a typographic axis alongside its scale/structure cues.\n R360 fontSize=12 + R225 tabular-nums + R209 scale +\n R253 fill transition all preserved. Transition list\n extends to include font-weight 200ms ease-out. */\n fontWeight={hoveredHub ? '800' : '700'}\n opacity={workingCount > 0 ? 1 : 0}\n data-topo-hub-working-count={workingCount}\n data-topo-hub-working-count-font-size=\"12\"\n data-topo-hub-working-count-hovered={hoveredHub ? 'true' : 'false'}\n data-topo-hub-working-count-visible={workingCount > 0 ? 'true' : 'false'}\n // Round 209 / Loop: hub workingCount digit scales 1.0 →\n // 1.08 on hub-hover, matching R177's r 14→17 ring grow.\n // Pre-R209 hovering the hub grew the ring while the\n // focal-point digit at the centre stayed planted — the\n // gesture lifted only half the structure. R209 ties the\n // digit's scale into the same hoveredHub state R177\n // already drives, so ring + digit rise as one unit.\n // transform-box: fill-box + transform-origin: center\n // anchors the scale to the digit's own bbox (same\n // idiom R184 reset-spin + R186 chrome-pop use for\n // SVG icon transforms). 200ms matches R167 node-ring\n // stroke-width interpolation pace. Reduced-motion users\n // skip the scale via the !reducedMotion gate (R29 a11y).\n // Round 225 / Loop: tabular-nums on hub digit — info-\n // density sibling to R224's edge badge tabular-nums.\n // Same physics: when workingCount crosses the 9 → 10\n // boundary the textAnchor='middle' centering jitters\n // ~3-4px because monospace fonts still have width\n // variance at the digit-vs-control boundary. Tabular\n // locks digit width so the focal point stays planted\n // through every count change. Pure visual tightening;\n // no test trap (computed font-variant-numeric resolves\n // to the keyword 'tabular-nums' verbatim).\n /* Round 253 / Loop: append fill 200ms to the hub\n digit transition list — theme toggle (cyber #ecfdf5\n ↔ light #d1fae5) was the last hub-area snap. */\n /* Round 476 / Loop — hub working-count digit gains a\n filter: drop-shadow glow on hub-hover. Stacks with\n the existing 4-axis hub-hover gesture stack on this\n element:\n R209 transform: scale(1.08) geometry\n R425 fontWeight 700 → 800 typography\n R253 fill ease-out chroma (theme)\n R213 opacity gate fade (count cross)\n R476 filter drop-shadow glow paint (this round)\n The glow uses the cyber emerald-400 (#34d399) /\n light emerald-500 (#10b981) hue family so the\n chroma stays inside the hub-area palette. Subtle\n 2-3 px blur radius at 0.6 opacity — visible but\n not loud, reads as \"the focal digit lit up under\n attention\".\n Reduced-motion users skip the filter via the\n !reducedMotion gate (R29 a11y blanket).\n Filter is a paint-only attribute — bbox stays\n the same, R51 overlap-test invariants hold.\n transition list extends to 'filter 200ms ease-out'\n so the glow eases under the same cadence as the\n scale + fw + fill axes. */\n data-topo-hub-working-count-glow={!reducedMotion && hoveredHub ? 'true' : 'false'}\n style={{\n pointerEvents: 'none',\n transform: !reducedMotion && hoveredHub ? 'scale(1.08)' : 'scale(1)',\n transformBox: 'fill-box',\n transformOrigin: 'center',\n filter: !reducedMotion && hoveredHub\n ? (isLight\n ? 'drop-shadow(0 0 2px rgba(16, 185, 129, 0.6))'\n : 'drop-shadow(0 0 3px rgba(52, 211, 153, 0.6))')\n : undefined,\n /* R425: font-weight 200ms appended so the hover fw\n bump 700 → 800 eases under the same cadence as\n R209 scale + R253 fill + R213 opacity.\n R476: filter 200ms appended so the new drop-\n shadow glow eases at the same cadence. */\n transition: 'transform 200ms ease-out, opacity 300ms ease-out, fill 200ms ease-out, font-weight 200ms ease-out, filter 200ms ease-out',\n fontVariantNumeric: 'tabular-nums',\n }}\n >\n {workingCount}\n </text>\n {/* decorative highlight (visible when workingCount === 0) */}\n {/* Round 365 / Loop: hub-center 'lit-lamp' decorative highlight\n circle r 5 → 5.5. Sibling visual-weight bump family —\n each round lifts one canvas anchor's geometric presence\n without disturbing its bbox envelope:\n R287 minimap viewport stroke 1 → 1.5\n R295 legend swatch base radius 5.5 → 6\n R359 recent-row pip base radius 1.6 → 1.8\n R360 hub digit fontSize 11 → 12\n R361 edge-badge digit fontSize 10 → 11\n R365 hub-highlight base radius 5 → 5.5 (this round)\n The highlight only renders when workingCount === 0\n (decorative 'lamp lit but idle' state per R130 + R213\n always-mount opacity-gate). At idle, the 0.5-px radius\n bump (21 % area, π*5.5² / π*5² = 1.21) lifts the lamp's\n presence — still well inside the r=10 hub-core (R130).\n opacity=0 when working preserved so the hub-digit's R130\n takeover stays seamless. R213 always-mount opacity-gate\n + 300ms opacity transition + pointerEvents:none all\n preserved. data-topo-hub-highlight-radius attr exposes\n the value for tests. */}\n {/* Round 386 / Loop: hub-highlight idle opacity 0.9 → 0.95.\n When workingCount===0 the highlight paints as the visible\n idle \"lamp lit but no work\" core (R130 takeover gate).\n Pre-R386 idle opacity was 0.9 — a ~6 % fade against full\n paint that read as slightly-dimmed-ghost on the focal\n point. R386 lifts to 0.95 (idle alpha gap halved 0.10\n → 0.05) so the canvas anchor reads more confidently\n as a present-but-idle state rather than a faded ghost.\n Theme-consistency / canvas-presence polish family (4th\n anchor):\n R370 hub hover-ring opacity 0.7 → 0.8 cyber\n R371 edge-badge rest opacity 0.82 → 0.85 cyber\n R372 minimap offline-dot opacity 0.5 → 0.6\n R386 hub-highlight idle opacity 0.9 → 0.95 (this round)\n opacity=0 when working preserved so the hub-digit's\n R130 takeover stays seamless. 300ms opacity transition\n + R213 always-mount opacity-gate + pointerEvents:none\n + R365 r=5.5 all preserved. data-topo-hub-highlight-\n opacity attr exposes the resolved value for tests. */}\n <circle\n cx={cx} cy={cy} r=\"5.5\"\n fill=\"#d1fae5\"\n opacity={workingCount > 0 ? 0 : 0.95}\n data-topo-hub-highlight\n data-topo-hub-highlight-visible={workingCount > 0 ? 'false' : 'true'}\n data-topo-hub-highlight-radius=\"5.5\"\n data-topo-hub-highlight-opacity={workingCount > 0 ? 0 : 0.95}\n style={{\n pointerEvents: 'none',\n transition: 'opacity 300ms ease-out',\n }}\n />\n {/* R115 / Loop: hover hint ring. Stroke-only circle at r=14\n that fades in when the hub is hovered — the same idea\n R44 used for node avatars (group-hover stroke). r=14\n sits comfortably outside the r=10 core and INSIDE the\n r=18 grounding halo, so the hover indicator is fully\n contained within the existing hub footprint (no bbox\n growth, overlap test unchanged). pointerEvents:none so\n the hint can't intercept the click that produced it. */}\n {/* Round 177 / Loop: hub hover ring picks up the same\n \"lift on hover\" gesture R164 added to the edge midpoint\n badge (r=9→10.5). Pre-R177 the ring faded opacity 0→0.7\n on hover but stayed static at r=14. Adding r=14→17 on\n hover gives the gesture extra weight — the hub responds\n more confidently. Stays within the r=18 halo bbox (no\n geometry growth), so the R51 overlap-test guard rails\n still hold. strokeWidth=1.5 is the offline-node\n sentinel but the overlap-test selector is gated to\n `g[data-node]` ancestors — this hub-internal circle\n is invisible to that probe. transition list grows `r`\n alongside opacity so both ease in concert; the lift\n feels like one continuous gesture. Seventh surface in\n the hover-elevation family (R51 nodes, R135 panels,\n R142 group boxes, R143/R144 rows, R164 edge badges,\n R177 hub ring). prefers-reduced-motion respected via\n R29 globals.css blanket. */}\n {/* Round 385 / Loop: hub hover-ring strokeWidth 1.5 → 1.75.\n Sibling visual-weight bump (11th anchor) to R367 edge-\n badge rest stroke 1 → 1.25. The ring is only visible\n during hub hover (opacity=0 rest, R177 + R370 control\n the hover-state alpha) so the change manifests purely\n as a thicker hover-state ring on the canvas focal\n point. R177 r 14 → 17 grow + R370 opacity 0 → 0.8\n already lift the hover cue; R385 adds stroke weight\n as the third lift axis. Stays clear of R51 overlap-\n test sentinel value 3 (1.75 is non-sentinel); the\n R51 selector is gated to g[data-node] ancestors so\n this hub-internal circle is invisible to the probe\n regardless. R253 stroke transition + pointerEvents:\n none preserved. data-topo-hub-hover-ring-stroke-width\n attr exposes the value for tests. */}\n <circle\n cx={cx} cy={cy}\n r={hoveredHub ? 17 : 14}\n fill=\"none\"\n stroke={isLight ? '#059669' : '#10b981'}\n strokeWidth=\"1.75\"\n /* Round 370 / Loop: hub hover-ring cyber opacity 0.7 →\n 0.8. R177 designed the hub hover-ring at opacity-0 →\n 0.85 (light) / 0 → 0.7 (cyber). The 15 % gap between\n the two themes meant cyber-theme operators got a\n noticeably softer hover cue than light-theme users\n against backgrounds that should equalise (dark bg\n needs more luminance to read as 'on'). R370 bumps\n cyber 0.7 → 0.8, narrowing the theme gap to 5 % —\n sibling theme-consistency polish to R251 edge badge\n fill/opacity (cyber 0.82 / light 0.95) and R246/R247\n panel transition families. Light theme 0.85 stays\n as is (already in the legibility band). data-topo-\n hub-hover-ring-opacity attr exposes the value for\n tests. */\n opacity={hoveredHub ? (isLight ? 0.85 : 0.8) : 0}\n data-topo-hub-hover-ring\n data-topo-hub-hover-ring-radius={hoveredHub ? 17 : 14}\n data-topo-hub-hover-ring-stroke-width=\"1.75\"\n data-topo-hub-hover-ring-opacity={hoveredHub ? (isLight ? 0.85 : 0.8) : 0}\n /* Round 253 / Loop: hub hover ring also gets stroke\n transition for theme toggle (cyber #10b981 ↔ light\n #059669). The opacity + r transitions stay for hover\n lift; stroke closes the theme-snap. */\n style={{\n pointerEvents: 'none',\n transition: 'opacity 180ms ease-out, r 180ms ease-out, stroke 200ms ease-out',\n }}\n />\n </g>)}\n\n {/* agent nodes */}\n {[...onlineNodes, ...offlineNodes].map((session, nodeIdx) => {\n const pos = nodePositions[session.alias];\n if (!pos) return null;\n\n const sseCountFor = (session.network_id ? sseSessions[`${session.network_id}:${session.alias}`] : undefined) ?? sseSessions[session.alias];\n const isOnline = session.status !== 'offline' || !!sseCountFor;\n const status = nodeStatus(session, isOnline, isLight);\n const isActive = activeAliases.has(session.alias);\n // #113: node size scales with the S/M/L control; halos, labels,\n // badge and avatar all derive from `radius` so they follow.\n const radius = Math.round((isOnline ? 26 : 18) * nodeScale);\n // Round 109/110 (Vincent 4582 + 4583 P0): at high node counts\n // the 100px opaque label cards overlapped each other and\n // covered neighbouring avatars. But hiding text entirely went\n // too far (\"还是得有文字\"). So: below the density threshold the\n // full name+status card shows always; above it each node shows\n // a lightweight plain-text alias (no opaque card → can't\n // occlude an avatar), and the full card appears only for the\n // hovered node or once zoomed in past 1.4×.\n const showFullLabel = !denseLayout || hoveredAlias === session.alias || view.zoom >= 1.4;\n // Round 8: when a node in another group is hovered, fade this\n // one. Same-group nodes (incl. singletons matching the hovered\n // alias) stay full. Pure visual focus, geometry unchanged.\n const inFocus = !activeGroup || (groupKeys[session.alias] ?? session.alias) === activeGroup;\n // R72: in ring layout, classify the node into a tier by its\n // distance from hub centre so the first-paint stagger can\n // emanate inner → outer instead of running clockwise. Grid\n // layout doesn't have a radial structure; it keeps R9's pure\n // nodeIdx stagger. Thresholds bracket the actual tier radii\n // (single 220, dual 175/260, triple 145/215/285, offline 325+).\n let tierIdx = 0;\n if (layout === 'ring') {\n const p = nodePositions[session.alias];\n if (p) {\n const d = Math.hypot(p.x - cx, p.y - cy);\n tierIdx = d < 195 ? 0 : d < 270 ? 1 : d < 310 ? 2 : 3;\n }\n }\n\n return (\n <g\n key={session.alias}\n data-node={session.alias}\n data-tier-idx={layout === 'ring' ? tierIdx : -1}\n // R151 / Loop: node a11y compliance — match the pattern\n // R116 / R139 / R140 / R148 / R149 applied to other\n // interactive surfaces. Node <g> has been clickable for\n // chat since R45 but tab-unreachable + screen-reader-\n // unannounced. role=\"button\" + tabIndex=0 + aria-label\n // expose it; aria-pressed reflects chat-target state so\n // SR users know which node currently has the popover\n // open. onKeyDown for Enter / Space fires the same\n // setChatAlias path as onClick. preventDefault on\n // Space stops the SVG canvas from scrolling.\n role=\"button\"\n tabIndex={0}\n aria-pressed={chatAlias === session.alias}\n aria-label={`Chat with ${session.alias} (${session.status})`}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n setChatAlias(session.alias);\n setClickRipple({\n ts: Date.now(),\n x: pos.x, y: pos.y, r0: radius,\n color: status.primary,\n });\n setTimeout(() => setClickRipple(prev =>\n prev && Date.now() - prev.ts >= 590 ? null : prev), 600);\n }\n }}\n // Round 3 / Loop: `anet-fade-in` runs once when the <g>\n // mounts — a new session entering the fleet (or the topology\n // first rendering) eases in instead of popping. Re-renders of\n // an existing node don't re-trigger (React preserves the <g>\n // via the alias key), so status changes don't flicker. The\n // global prefers-reduced-motion sweep already neutralises it.\n className=\"group transition-opacity anet-fade-in anet-topo-svg-focus\"\n style={{\n cursor: 'pointer',\n // Round 17 / Loop: offline nodes drop to 0.6 at rest so\n // online nodes pop without losing the offline-as-ghost\n // information. The dashed stroke + smaller radius already\n // say \"offline\"; dimming the whole group strengthens the\n // online-vs-offline hierarchy at first glance. Exempt the\n // chat-focused node — if the user explicitly opened a\n // popover targeting an offline alias, that node stays\n // full-brightness so the focus ring + popover read as one\n // selected thing rather than a dimmed selection. Group-\n // hover fade (Round 8) still wins when inFocus is false.\n // Round 49 / Loop: edge-hover endpoint highlight composes\n // OVER the inFocus/online formula — a non-endpoint node\n // dims to 0.28 (just below the inFocus 0.32 to read as a\n // stronger \"not relevant\" signal), endpoints keep their\n // base opacity. chatAlias still wins to keep the focus\n // ring legible if the user clicked through.\n // Round 55 / Loop: legend-status hover composes on the\n // SAME level as edge-hover-endpoint. Non-matching nodes\n // dim to 0.28; matching nodes stay at their base.\n // activeStatus matches: working = status==='working',\n // idle = online but not working, offline = !isOnline.\n // Round 60 / Loop: activeStatus = hoveredStatus ?? pinnedStatus\n // so the pressure-bar segment pins (R60) and the legend\n // row hover (R55) feed the same branch.\n // Round 80 / Loop: vendor-letter hover composes ABOVE the\n // activeStatus branch so hovering the vendor chip's `C`\n // dims everything except C-vendor nodes. Same dim value\n // (0.28) as edge-endpoint and status filters — visually\n // consistent. Vendor lookup uses the same vendorForModel\n // helper the avatar render uses, keyed by initial so the\n // chip and the avatar always agree on grouping.\n opacity: hoveredEdgeEndpoints && !hoveredEdgeEndpoints.has(session.alias) && chatAlias !== session.alias\n ? 0.28\n : activeVendor && chatAlias !== session.alias && (() => {\n const v = vendorForModel(session.model);\n const initial = v.id === 'unknown' ? '?' : v.initial;\n return initial !== activeVendor;\n })()\n ? 0.28\n : activeStatus && chatAlias !== session.alias && !(\n activeStatus === 'working' ? session.status === 'working'\n : activeStatus === 'idle' ? (isOnline && session.status !== 'working')\n : /* offline */ !isOnline\n )\n ? 0.28\n : !inFocus\n ? 0.32\n : chatAlias === session.alias\n ? 1\n : isOnline ? 1 : 0.6,\n // Round 9 / Loop: stagger the anet-fade-in so the topology\n // reveals as a wave on first paint instead of one big pop.\n // Cap at 24 indices (≈600ms tail) so 50-node fleets still\n // finish revealing within a beat. CSS animation-delay only\n // applies during the keyframe — re-renders without a new\n // mount (same alias key) don't replay, so status changes\n // never trigger the stagger again.\n // Round 72 / Loop: in ring layout, stagger by tier radius\n // so the topology emanates from the hub outward — inner\n // tier at 0ms, middle at ~180ms, outer at ~360ms, offline\n // at ~540ms. A small within-tier offset (nodeIdx % 6 * 25)\n // adds variety so each ring rotates in instead of all-at-\n // once popping. Grid keeps R9's pure nodeIdx stagger.\n animationDelay: layout === 'ring'\n ? `${tierIdx * 180 + (nodeIdx % 6) * 25}ms`\n : `${Math.min(nodeIdx, 24) * 25}ms`,\n // Round 51 / Loop: hover micro-lift. The label already\n // lifts on group-hover (R26). The node body — circle,\n // avatar, status ring — stays planted, so the gesture\n // reads \"label moves, body doesn't\". Now the whole <g>\n // translates -2px when hovered: avatar + ring + label\n // move as one unit. 2 px is well inside the inter-row\n // gap (cellH headroom ≥22), and CSS transform on SVG\n // <g> never affects the overlap-test geometry (the\n // test never simulates hover). useReducedMotion drops\n // the lift to 0 for prefers-reduced-motion users.\n transform: !reducedMotion && hoveredAlias === session.alias ? 'translateY(-2px)' : undefined,\n transition: 'transform 180ms cubic-bezier(0.4,0,0.2,1)',\n }}\n // Stop the pointerdown from reaching the SVG pan handler: the\n // SVG calls setPointerCapture, and a captured pointer makes\n // Chromium fire the follow-up `click` on the SVG instead of\n // this node — so without this the node's onClick never runs.\n // Side effect (intended): you pan from empty canvas, not by\n // grabbing a node.\n onPointerDown={(e) => e.stopPropagation()}\n // Track hover globally (not just under denseLayout) so the\n // Round 8 group-focus fade works at any fleet size.\n onPointerEnter={() => setHoveredAlias(session.alias)}\n onPointerLeave={() => setHoveredAlias(prev => (prev === session.alias ? null : prev))}\n onClick={() => {\n setChatAlias(session.alias);\n // Round 14 ripple — capture position, radius and status\n // colour at click time so the one-shot circle is self-\n // contained (no re-render-time recomputation needed).\n setClickRipple({\n ts: Date.now(),\n x: pos.x, y: pos.y, r0: radius,\n color: status.primary,\n });\n setTimeout(() => setClickRipple(prev =>\n prev && Date.now() - prev.ts >= 590 ? null : prev), 600);\n }}\n >\n {/* Issue #96: native hover tooltip — \"Vendor · model · Runtime\".\n Falls back to just the alias when the node reports no\n model/runtime.\n Round 33 / Loop: cwd line answers \"what is this agent on?\".\n Round 34 / Loop: for offline nodes, append \"last seen: 6m ago\"\n so the operator knows whether to wait or chase. Online nodes\n skip the line (Round 27's 1 h ghost age-out means anything\n still online has heartbeated recently — the line would just\n be noise). Accepts both ISO (\"…T06:00:28Z\") and SQL-style\n (\"… 06:00:28\" assumed UTC) formats. */}\n {(() => {\n // Round 35 / Loop: TZ-safe parsing via the shared lib helper\n // — same parseHubTime is used by isGhost above (Round 38\n // factored it to app/lib/time.ts so both paths interpret SQL\n // bare timestamps as UTC on every browser).\n const lastSeen = !isOnline && session.last_seen_at ? relativeAgo(session.last_seen_at) : null;\n // Round 98 / Loop: enrich the node tooltip with status,\n // group membership, and inbound/outbound flow counts —\n // same info-density spirit as R97 active-filter pill\n // tooltips. Hovering any node now answers \"what is this\n // and how does it sit in the topology\" without forcing\n // a click into the chat popover. Group line only shows\n // when the alias is part of a multi-member band (R106\n // prefix grouping); singletons skip it. Flow line only\n // when at least one direction has count > 0.\n const groupKey = groupKeys[session.alias];\n const groupMembers = groupKey\n ? Object.values(groupKeys).filter(k => k === groupKey).length\n : 1;\n const groupLine = groupMembers > 1 ? `group: ${groupKey} · ${groupMembers}` : null;\n // R147 / Loop: node tooltip extends R98's flow summary\n // with the actual sender / receiver aliases. R98 told you\n // \"12 in / 5 out\" but not WHO. The R97 idiom — anywhere\n // the UI shows \"N\" should hover-explain WHICH N — applied\n // to filter pills, group labels, vendor letters, pressure\n // segments, recent-row text, active-links chip; the node\n // title was the last surface still showing only the\n // aggregate. Direction-tagged: senders are who's\n // messaging THIS node (inbound), receivers are who this\n // node is messaging (outbound). 6-truncate + \"+N more\"\n // matches the pattern other tooltips use so a 30-flow\n // node doesn't paint a 30-line tooltip.\n let flowIn = 0, flowOut = 0;\n const sendersMap = new Map<string, number>(); // alias → count, inbound\n const receiversMap = new Map<string, number>(); // alias → count, outbound\n for (const fl of flowLinks) {\n if (fl.from === session.alias) {\n flowOut += fl.count;\n receiversMap.set(fl.to, (receiversMap.get(fl.to) ?? 0) + fl.count);\n }\n if (fl.to === session.alias) {\n flowIn += fl.count;\n sendersMap.set(fl.from, (sendersMap.get(fl.from) ?? 0) + fl.count);\n }\n }\n const fmtPeers = (m: Map<string, number>) => {\n const pairs = [...m.entries()].sort((a, b) => b[1] - a[1]);\n const preview = pairs.slice(0, 6).map(([a, n]) => `${a} (${n})`).join(', ');\n const suffix = pairs.length > 6 ? ` + ${pairs.length - 6} more` : '';\n return preview + suffix;\n };\n const flowLine = (flowIn + flowOut) > 0 ? `flows: ${flowIn} in / ${flowOut} out` : null;\n const sendersLine = sendersMap.size > 0 ? `← from: ${fmtPeers(sendersMap)}` : null;\n const receiversLine = receiversMap.size > 0 ? `→ to: ${fmtPeers(receiversMap)}` : null;\n return (\n <title>{[\n `${session.alias} · ${session.status}`,\n identityLine(session.model, session.runtime),\n groupLine,\n session.project_dir ? `cwd: ${session.project_dir}` : null,\n lastSeen ? `last seen: ${lastSeen}` : null,\n flowLine,\n sendersLine,\n receiversLine,\n ].filter(Boolean).join('\\n')}</title>\n );\n })()}\n {/* Round 2 / Loop: hover ring — a thin outer stroke that fades\n in when the cursor enters the node, signalling clickability\n (real-user feedback for the chat-popover open). Pure CSS via\n Tailwind group-hover, so it costs nothing per frame and\n respects prefers-reduced-motion via the global media query.\n Round 489 / Loop — duration harmonized from 150ms → 200ms\n to join the Hero D #147 motion-coherence stack (R459-R475\n cluster surfaces + cadence-sync family). R2 originally\n picked 150ms for a \"snappier feel\" before the 200ms ease-\n out vocabulary was banked as the canvas-wide motion\n default. Bringing this ring into the family means hover-\n in / hover-out / cluster cadence / pip-strip transitions\n all settle on the same timing — the canvas now reads as\n one motion vocabulary instead of two competing tempos.\n 11th surface in the motion-coherence stack. */}\n <circle\n cx={pos.x}\n cy={pos.y}\n r={radius + 12}\n fill=\"none\"\n stroke={status.primary}\n // strokeWidth must NOT be 1.5 (offline status ring) or 3\n // (online status ring) — the overlap test selects by those\n // exact widths and would mis-count this invisible hover\n // ring as a node footprint.\n strokeWidth=\"2\"\n className=\"opacity-0 group-hover:opacity-70 transition-opacity duration-200\"\n style={{ pointerEvents: 'none' }}\n />\n {/* Round 11 / Loop: chat-focus ring — when the ChatPopover is\n open targeting this node, anchor a persistent ring around\n it so the floating popover visibly links back to its source\n node. Static (not pulsing) so it reads as \"selected state\"\n rather than \"this node is active\". strokeWidth=2.5 stays\n clear of the overlap-test selectors (1.5 / 3). Sits just\n outside the halo radius+8 so it never overlaps a neighbour\n (halos already pack flush in dense grids). */}\n {/* R51 chat-target ring. R120 / Loop: gentle SMIL\n breath on the ring's opacity (±0.1 over 3s) when\n chat is open + !reducedMotion. Says \"active session\n here\" continuously without animation noise — the\n ring only appears for one node at a time (the\n chatAlias), so it never competes with R84 hub\n busyness or R112 working halo for attention.\n\n Round 183 / Loop: 7th surface in the smooth-pin-\n mirror family. Pre-R183 the ring was conditionally\n mounted on `chatAlias === session.alias`; the\n className `transition-opacity duration-200` never\n fired because the element didn't exist before\n mount. Always-mounted now with opacity gated by\n isChat — the CSS transition fires cleanly on\n chat-close (smooth fade-out). The `<animate>`\n SMIL stays gated by `!reducedMotion && isChat`\n so it only runs for the active chat target; when\n chat is closed, SMIL unmounts and opacity reverts\n to attribute (0) → CSS transitions down smoothly.\n On chat-OPEN the SMIL takes over per spec, so the\n fade-in is snappier than the fade-out — acceptable\n because the user explicitly clicked the node.\n\n strokeWidth=2.5 is not a R51 sentinel value\n (sentinels are 1.5/3 inside g[data-node]), so the\n ring is invisible to the overlap-test selector\n even when always-mounted. */}\n {/* Round 242 / Loop: extend the chat-target ring's\n transition list to include stroke + filter.\n Pre-R242 only `opacity` eased (R183 200ms): a\n chat-target node going working → idle hard-\n flipped the ring's stroke colour (status.primary\n green → teal) in one frame even though the rest\n of the ring was a smooth presence. Filter (glow\n on cyber, none on light) also snapped on chat\n toggle AND on theme switch.\n\n Add `stroke 200ms ease-out` + `filter 200ms\n ease-out` so the colour and glow both ease at\n the same cadence as the opacity gate. Same\n idiom R167 (node status-ring) uses for\n coordinated colour-easing on status flip;\n R242 brings the chat-target ring up to that\n bar. */}\n {(() => {\n const isChat = chatAlias === session.alias;\n return (\n <circle\n cx={pos.x}\n cy={pos.y}\n r={radius + 14}\n fill=\"none\"\n stroke={status.primary}\n strokeWidth=\"2.5\"\n opacity={isChat ? (isLight ? 0.85 : 0.95) : 0}\n filter={!isLight && isChat ? 'url(#topo-glow)' : undefined}\n style={{ pointerEvents: 'none', transition: 'opacity 200ms ease-out, stroke 200ms ease-out, filter 200ms ease-out' }}\n data-chat-target-ring\n data-chat-target-active={isChat ? 'true' : 'false'}\n data-chat-target-breath={!reducedMotion && isChat ? 'on' : 'off'}\n >\n {!reducedMotion && isChat && (\n <animate\n attributeName=\"opacity\"\n values={isLight ? '0.72;0.95;0.72' : '0.82;1;0.82'}\n dur=\"3s\"\n repeatCount=\"indefinite\"\n />\n )}\n </circle>\n );\n })()}\n {/* Round 243 / Loop: active-node pulse (the breathing\n aura ring at r=radius+14 fill=status.primary, shown\n on nodes participating in a recent flow) gains TWO\n polishes:\n\n 1) Always-mount + opacity-gate wrapper <g>. Pre-\n R243 the circle conditionally mounted on\n isActive — when a node joined a flow, the pulse\n snap-appeared at radius+8 (first SMIL phase\n value), and when the flow stopped, snap-\n disappeared. R243 keeps the SMIL animation\n running continuously inside an opacity-gated\n <g>; isActive flips the WRAPPER opacity\n 1↔0 with a 300ms ease-out transition so the\n pulse fades in/out at its current phase\n instead of restarting from +8. The reduced-\n motion gate stays at the conditional render\n level — reduced-motion users see no pulse at\n all (no point without the animation).\n\n 12th surface in the always-mount-opacity-gate\n family (R181/R182/R183/R213×2/R214/R215/R221/\n R222/R223/R237/R243).\n\n 2) SMIL ease-in-out keySplines on both r and\n opacity animates. Pre-R243 default linear\n calcMode produced a constant-velocity breath\n (radius marched +8 → +22 at fixed dr/dt;\n opacity dimmed 0.12 → 0.02 at fixed dα/dt) —\n mechanical, not organic. calcMode='spline'\n + keyTimes='0;0.5;1' + per-segment keySplines\n '0.42 0 0.58 1' (canonical CSS ease-in-out)\n both ways gives a settled breath: slow at\n both endpoints (small and large radius / lit\n and dim opacity), fast through the middle.\n Same SMIL-easing family R227 / R228 already\n inhabits at the click ripple + edge ping +\n pulse. */}\n {!reducedMotion && (\n <g\n opacity={isActive ? 1 : 0}\n data-node-pulse={session.alias}\n data-node-pulse-active={isActive ? 'true' : 'false'}\n style={{ transition: 'opacity 300ms ease-out' }}\n >\n <circle cx={pos.x} cy={pos.y} r={radius + 14} fill={status.primary} opacity={isLight ? 0.08 : 0.12}>\n <animate\n attributeName=\"r\"\n values={`${radius + 8};${radius + 22};${radius + 8}`}\n dur=\"2.4s\"\n repeatCount=\"indefinite\"\n calcMode=\"spline\"\n keyTimes=\"0;0.5;1\"\n keySplines=\"0.42 0 0.58 1;0.42 0 0.58 1\"\n />\n {/* Round 409 / Loop: active-node pulse peak\n opacity lift — cyber 0.18 → 0.20 / light\n 0.12 → 0.14. Theme-consistency / canvas-\n presence family 9th anchor. R243 family\n rhythm preserved.\n Round 413 / Loop: trough lift mirrors R409\n peak — cyber 0.04 → 0.05 / light 0.02 →\n 0.03. Stale-state legibility lift family\n 8th anchor — pairs with R404 (hub-halo\n cyber trough 0.08 → 0.10) and R405 (light\n trough 0.32 → 0.34). The per-node breath's\n low-point now reads slightly above the\n \"nearly gone\" zone while preserving the\n breath amplitude (cyber Δ 0.16 vs Δ pre-\n R409+R413 of 0.14; light Δ 0.11 vs 0.10).\n Both peak (R409) AND trough (R413) lift\n together so the active-pulse signal stays\n confidently present at both ends of its\n 2.4s cycle.\n Stale-state legibility lift family (8):\n R317 subordinate-text gray-500→400\n R358 freshness floor 0.25→0.30\n R372 minimap offline-dot 0.5→0.6\n R404 hub-halo cyber trough 0.08→0.10\n R405 hub-halo light trough 0.32→0.34\n R406 edge freshness floor 0.35→0.40\n R407 node halo offline opacity (cyber+light)\n R413 active-node pulse trough (this round)\n cyber 0.04 → 0.05\n light 0.02 → 0.03\n R243 always-mount opacity-gate + R243\n ease-in-out keySplines + r animation\n (radius+8 ↔ radius+22) preserved.\n data-node-pulse-peak + new -pulse-trough\n attrs expose resolved per-theme values. */}\n <animate\n attributeName=\"opacity\"\n values={isLight ? '0.14;0.03;0.14' : '0.20;0.05;0.20'}\n dur=\"2.4s\"\n repeatCount=\"indefinite\"\n calcMode=\"spline\"\n keyTimes=\"0;0.5;1\"\n keySplines=\"0.42 0 0.58 1;0.42 0 0.58 1\"\n data-node-pulse-peak={isLight ? '0.14' : '0.20'}\n data-node-pulse-trough={isLight ? '0.03' : '0.05'}\n />\n </circle>\n </g>\n )}\n {/* Round 4 / Loop: transition-[fill,stroke,opacity] smooths\n status colour changes so idle↔working↔offline doesn't snap\n — task replies / node-rename / SSE updates ease in.\n Round 112 / Loop: working nodes get a subtle halo breath\n (±0.12 opacity at 3s cycle) so the eye can find \"what's\n busy\" at a glance without scanning chips. Idle + offline\n halos stay flat — they don't need to demand attention.\n R84 hub-center breath stays the loudest \"fleet busyness\"\n signal; this one is quieter, per-node. SMIL `<animate>`\n inside the circle, gated by reducedMotion. */}\n {/* Round 226 / Loop: working-halo breath gets per-node\n phase stagger. Pre-R226 every working node's halo\n pulsed in lockstep — all 0.73→0.92→0.73 starting at\n the same instant — which on a fleet of 4+ working\n agents reads as one mechanical metronome rather\n than an organic group of breathing entities.\n\n SMIL `<animate>` accepts negative `begin` to offset\n the cycle backwards in time (the animation starts\n mid-cycle on first paint). Using\n `(nodeIdx * 0.37) % 3` gives a deterministic,\n well-distributed offset across the 3s period —\n the same golden-ratio-ish 0.37 fraction R103 uses\n for particle phase stagger on edges. 0.37 doesn't\n line up for any small N (4 nodes → offsets 0,\n 0.37, 0.74, 1.11 — evenly spread, never\n coincident).\n\n Side benefit: when a new agent joins a busy fleet\n its halo phase is determined by its position in\n the order array, not \"when it joined\" — so a\n re-render doesn't reshuffle breath phases. Order\n is stable (R-onlineNodes sort), so the canvas\n feels calm rather than jittery on each refresh.\n\n Reduced-motion users skip the animate entirely\n (gate unchanged). Halo opacity transition on the\n parent stays for status flips. data-node-halo-\n breath-offset surfaces the chosen offset for\n test introspection. */}\n {/* Round 407 / Loop: offline node halo opacity lift —\n cyber 0.25 → 0.30 and light 0.4 → 0.45. Pre-R407\n offline node halos faded to α=0.25 cyber (75 %\n dim) / α=0.4 light. On the dark canvas the 0.25\n halo read as \"nearly gone\" — exactly the\n legibility floor R404/R405 just lifted on the\n hub-halo and R372 lifted on minimap offline dots.\n R407 closes the same family at the per-node halo\n surface: +0.05 lift on both themes so offline\n anchors stay legibly present without crossing into\n \"could be online\" territory (online cyber 0.65 /\n light 0.85 unchanged — the 0.30/0.65 cyber ratio\n still gives 2.17× contrast for online/offline).\n Stale-state legibility lift family (7 anchors now):\n R317 subordinate-text gray-500 → gray-400\n R358 freshness floor 0.25 → 0.30\n R372 minimap offline-dot 0.5 → 0.6\n R404 hub-halo cyber trough 0.08 → 0.10\n R405 hub-halo light trough 0.32 → 0.34\n R406 edge freshness floor 0.35 → 0.40\n R407 node halo offline opacity (this round)\n cyber 0.25 → 0.30\n light 0.4 → 0.45\n R278 retired-breath gate + R12 status.halo color\n + R226 phase stagger code-path preserved (the\n breath stays disabled per Vincent's R278 ask;\n only the BASE opacity floor shifts here). transi-\n tion list ('fill,opacity' 300ms ease-out) unchanged.\n data-node-halo-offline-opacity attr exposes the\n resolved value for tests. */}\n {(() => {\n /* Round 440 / Loop: node halo opacity hover lift —\n lifts toward full on the matched node. Pure paint\n axis: rest values unchanged for un-hovered halos,\n hover state lifts the matched halo's alpha by\n +0.15 on each tier:\n online cyber 0.65 → 0.80\n online light 0.85 → 1.00 (capped)\n offline cyber 0.30 → 0.45\n offline light 0.45 → 0.60\n Same paint-only mental model as R430 hub-spoke\n opacity lift + R429 label-card body opacity lift,\n now at the per-node halo scope. No geometry\n change so R51 sentinels stay safe and the overlap-\n test invariant is unchanged (test runs at rest).\n Closes a chroma/presence axis on the per-node\n hover signature alongside the 12-layer cue stack\n (R26/R217/R142/R427/R428/R429 card + R430/R435/\n R436/R437/R94 link + R438 ring). R407 offline\n halo opacity floor (cyber 0.30 / light 0.45) is\n the rest branch unchanged. Existing transition-\n [fill,opacity] duration-300 className handles\n the easing. data-node-halo-hovered exposes the\n gate; data-node-halo-resolved-opacity exposes\n the four-state resolved value for tests. */\n const isHaloHovered = !reducedMotion && hoveredAlias === session.alias;\n /* Round 456 / Loop: light-theme offline node halo\n rest opacity 0.45 → 0.50. Stale-state legibility\n lift family extension (10th anchor) at the per-\n node halo light-theme scope:\n R317 subordinate-text gray-500 → gray-400\n R358 freshness floor 0.25 → 0.30\n R372 minimap offline-dot 0.5 → 0.6\n R404 hub-halo cyber trough 0.08 → 0.10\n R405 hub-halo light trough 0.32 → 0.34\n R406 edge freshness floor 0.35 → 0.40\n R407 node halo offline opacity\n cyber 0.25 → 0.30\n light 0.4 → 0.45\n R419 hub-spoke idle 0.45 → 0.50\n R452 dense alias rest 0.9 → 0.95\n R456 node halo offline LIGHT 0.45 → 0.50 ← this round\n Pre-R456 light-theme offline halo at 0.45 sat at\n the upper end of \"near-floor\" but read as soft-\n focus on the lighter canvas; +0.05 (~11 % opacity\n gain) lifts it to 0.50 — the midpoint between\n R407 rest 0.45 and R440 hover 0.60 — closing the\n gap so offline halos read more confidently as\n present-but-stale anchors. Cyber theme stays at\n R407's 0.30 (cyber backdrop is dark; the cyber\n offline halo against #080814 contains a stronger\n contrast envelope than light, so doesn't need\n the same lift). R440 hover 0.45→0.60 light + R12\n status.halo color + R407 transition list all\n preserved. */\n const haloOpacity = (() => {\n if (isOnline) {\n return isLight ? (isHaloHovered ? 1 : 0.85) : (isHaloHovered ? 0.80 : 0.65);\n }\n return isLight ? (isHaloHovered ? 0.60 : 0.50) : (isHaloHovered ? 0.45 : 0.30);\n })();\n return (\n <circle\n cx={pos.x}\n cy={pos.y}\n r={radius + 8}\n fill={status.halo}\n opacity={haloOpacity}\n data-node-halo-offline-opacity={isOnline ? undefined : (isLight ? 0.45 : 0.30)}\n data-node-halo-hovered={isHaloHovered ? 'true' : 'false'}\n data-node-halo-resolved-opacity={haloOpacity}\n className=\"transition-[fill,opacity] duration-300 ease-out\"\n data-node-halo-breath={!reducedMotion && session.status === 'working' ? 'on' : 'off'}\n data-node-halo-breath-offset={\n !reducedMotion && session.status === 'working'\n ? ((nodeIdx * 0.37) % 3).toFixed(3)\n : undefined\n }\n >\n {/* Round 278 / Loop: per-node working halo breath\n (R112+R226+R244 family) RETIRED per Vincent\n 5214/5215-5217 simplification ask (减法 cut #4\n after R275 chip-row, R276 orbit, R277 legend).\n\n The breath was: each working agent's halo pulses\n 0.73→0.92→0.73 (cyber 0.53→0.78→0.53) at 3 s\n cycle, R226-staggered per-node, R244-eased. For\n a 4-working fleet that's 4 simultaneous SMIL\n breaths competing with the hub-halo breath\n (R244 hub) for the \"fleet busyness\" visual\n signal.\n\n The signal is info-redundant: the hub-halo\n breath ALREADY conveys \"the network is alive\n and busy\"; per-node halo breath duplicates it\n at 4× volume. Plus working nodes are ALREADY\n distinguished by their halo color (status.halo\n green/teal/slate via R12 trio) — the static\n halo carries identity, the moving breath was\n decorative motion on top.\n\n R278 gates the SMIL animate with `false &&` so\n the code remains for rollback. Halo opacity\n stays at the BASE (non-breathing) values via\n the parent circle's `opacity` attr (0.85/0.65/\n 0.4/0.25 from R12 + isOnline gate). Working\n nodes still show green halos; they just don't\n pulse.\n\n Net: -4 SMIL animations on canvas for typical\n 4-working fleet. Combined with R276 orbit\n retirement (-4) and the hub halo breath kept\n as the SOLE \"fleet busyness\" motion signal,\n the canvas reads quieter. R226 + R244 per-node\n stagger / ease constants are dead code post-\n R278 (acceptable — family retires together). */}\n {false && !reducedMotion && session.status === 'working' && (\n <animate\n attributeName=\"opacity\"\n values={isLight ? '0.73;0.92;0.73' : '0.53;0.78;0.53'}\n dur=\"3s\"\n begin={`-${((nodeIdx * 0.37) % 3).toFixed(3)}s`}\n repeatCount=\"indefinite\"\n calcMode=\"spline\"\n keyTimes=\"0;0.5;1\"\n keySplines=\"0.42 0 0.58 1;0.42 0 0.58 1\"\n />\n )}\n </circle>\n );\n })()}\n {/* Round 111 / Loop: edge-endpoint emphasis ring. R49\n already keeps endpoint nodes at opacity 1 while\n others dim when an edge is hovered, but the\n endpoints had no POSITIVE indicator — they just\n \"stayed bright\". An accent stroke at r=radius+7\n (just inside the halo's r=radius+8 bbox so we\n don't grow the overlap footprint) clearly says\n \"these are the two participants in this flow\".\n pointerEvents:none so the node hitbox stays alive.\n\n Round 182 / Loop: the ring used to mount/unmount\n on every edge hover, snapping despite the\n opacity transition on the style. Always-mount\n with opacity gated by hoveredEdgeEndpoints — same\n pattern R181 used for the legend pin ring. The\n transition now actually fires on hover entry\n and exit. 6th surface in the smooth-pin-mirror\n family (R165/R180/R181 + this round).\n\n strokeWidth=1.6 (was 1.5) deliberately escapes\n the R51 overlap-test sentinel `circle[stroke-\n width=\"1.5\"]`: an always-mounted r=radius+7 ring\n inside g[data-node] would otherwise be picked\n before the actual status ring (r=radius) by\n querySelector document order, breaking the\n test's node-bbox read. 1.5 → 1.6 is visually\n imperceptible (6.7% thicker) but the exact-\n string CSS attribute selector no longer\n matches. */}\n {(() => {\n const isEndpoint = hoveredEdgeEndpoints && hoveredEdgeEndpoints.has(session.alias);\n /* Round 233 / Loop: endpoint ring picks up a stroke-\n width thicken on edge-hover, completing the hover-\n elevation gesture across the whole edge surface.\n Pre-R233 hovering an edge eased the visible path\n stroke (R166) and lifted the badge r (R164) — but\n the two endpoint rings only faded IN (R182\n opacity gate). Now they ALSO thicken 1.6 → 2.4 on\n hover, in 180ms ease-out matching R164 badge lift.\n The endpoint nodes feel like they \"rise to meet\"\n the edge as the cursor approaches it, instead of\n just appearing.\n\n 1.6 and 2.4 both escape the R51 overlap-test\n sentinels (1.5 / 3 are reserved) — 2.4 sits\n comfortably between, visually 50% thicker than\n baseline so the gesture reads but the radius is\n unchanged (still r=radius+7) so geometry stays\n calm and the topo-overlap-test stays green. 9th\n surface in the hover-elevation family (R51\n nodes / R135 panels / R142 group boxes / R143-\n R144 rows / R164 edge badges / R177 hub ring /\n R229 group-label count brighten / R233 endpoint\n ring stroke-width). data-edge-endpoint-ring-\n stroke-width attr surfaces the chosen value for\n test introspection. */\n /* Round 442 / Loop: endpoint emphasis ring radius\n hover lift — r=radius+7 → radius+8 on isEndpoint,\n closing a 3-axis hover-elevation parity at endpoint\n ring scope (r + sw + opacity):\n opacity R182 0 → 0.85/0.9\n sw R233 1.6 → 2.4\n r R442 +7 → +8 ← this round\n Mirrors the 3-axis trios already established at\n hub hover-ring (R177/R370/R385) and edge badge\n (R164/R394/R395). Pre-R442 the endpoint ring\n faded in + thickened on edge-hover but its radius\n stayed locked at radius+7 — only the paint/weight\n axes lifted while the GEOMETRY stayed unchanged.\n +1px (~radius+7 to radius+8) gives a subtle outward\n pulse on hover without crowding the status ring\n (which sits at radius from R438 sw3.5 hover) or\n the halo (radius+8 from R440 opacity hover — the\n endpoint ring sits at the SAME radius as the halo\n but with stroke=cyan vs fill=status.halo so they\n don't visually collide). The transition list\n extends to include 'r 180ms ease-out' so the new\n axis eases under the same R233 cadence. SVG `r`\n on a <circle> uses CSS-property syntax for inter-\n polation (same idiom R197/R198 used on the\n legend swatch). data-edge-endpoint-ring-radius\n attr exposes the resolved value for tests. */\n const endpointR = isEndpoint ? radius + 8 : radius + 7;\n return (\n <circle\n cx={pos.x}\n cy={pos.y}\n fill=\"none\"\n stroke={pal.flowEdge}\n strokeWidth={isEndpoint ? 2.4 : 1.6}\n opacity={isEndpoint ? (isLight ? 0.9 : 0.85) : 0}\n data-edge-endpoint-ring\n data-edge-endpoint-active={isEndpoint ? 'true' : 'false'}\n data-edge-endpoint-ring-stroke-width={isEndpoint ? 2.4 : 1.6}\n data-edge-endpoint-ring-radius={endpointR}\n style={{\n pointerEvents: 'none',\n r: `${endpointR}px`,\n transition: 'opacity 180ms ease-out, stroke-width 180ms ease-out, r 180ms ease-out',\n } as React.CSSProperties}\n />\n );\n })()}\n {/* Round 167 / Loop: extend the node status-ring\n transition to include stroke-width — symmetric\n with R165 (pressure-bar width) and R166 (edge\n stroke-width). Pre-R167 only fill+stroke colors\n transitioned smoothly; stroke-width snapped from\n 3 (online) to 1.5 (offline) when a session\n transitioned. With stroke-width in the transition\n list the ring smoothly contracts as a node goes\n offline (or expands when it comes back).\n strokeDasharray stays binary (none ↔ '5 5')\n because dash values don't interpolate cleanly\n between continuous and discrete forms.\n Inline style replaces the Tailwind transition-\n [fill,stroke] className for stable arbitrary\n property compilation. Respects prefers-reduced-\n motion via R29 globals.css blanket override.\n data-node-status-ring exposes this circle for\n test probing — the overlap-test guard on\n stroke-width=\"3\"/\"1.5\" still works against the\n DOM attribute value (React-rendered, not\n interpolated). */}\n {(() => {\n /* Round 438 / Loop: status ring strokeWidth hover lift —\n when hoveredAlias matches, the node's status ring\n thickens by +0.5: online 3 → 3.5, offline 1.5 → 2.\n Same absolute delta as R435 hub-spoke (idle 1→1.25\n used Δ +0.25 because rest base was thinner; status\n ring's heavier rest values 1.5/3 need a bigger\n +0.5 to register as visible thickening).\n Status-ring axis joins the node-hover cue stack\n (now 9 layers including link surfaces):\n R26 group translateY -2px per-node geometry\n R217 stroke tint legendAccent per-node card\n R142 drop-shadow boost per-node card\n R427 alias letter-spacing per-node text\n R428 sub-text letter-spacing per-node text\n R429 body opacity 0.94 → 1.0 per-node card\n R430 hub-spoke α+ link to hub (paint)\n R435 hub-spoke sw+ link to hub (geo)\n R94 edge α 1.7× inter-node link (paint)\n R436 edge sw 1.15× inter-node link (geo)\n R437 flow-rail sw 1 → 1.5 edge paint-layer\n R438 status-ring sw +0.5 ring geometry ← this round\n R51 sentinel safety: rest values 3 / 1.5 unchanged\n so the overlap-test selector `circle[stroke-width=\n \"3\"]` / `circle[stroke-width=\"1.5\"]` inside\n g[data-node] still matches at rest. Hover values\n 3.5 / 2 are not in the reserved {1.5, 3} set so\n the selector wouldn't match them anyway; but the\n test runs WITHOUT hover so this never matters\n in practice. R167 stroke-width 300ms transition\n already in the style list eases the lift for\n free. data-node-status-ring-hovered exposes the\n gate for tests. */\n const isRingHovered = !reducedMotion && hoveredAlias === session.alias;\n const ringStrokeWidth = isOnline\n ? (isRingHovered ? 3.5 : 3)\n : (isRingHovered ? 2 : 1.5);\n return (\n <circle\n cx={pos.x}\n cy={pos.y}\n r={radius}\n fill={isOnline ? pal.nodeFill.online : pal.nodeFill.offline}\n stroke={status.primary}\n strokeWidth={ringStrokeWidth}\n strokeDasharray={isOnline ? 'none' : '5 5'}\n filter={isOnline && !isLight ? 'url(#topo-glow)' : undefined}\n data-node-status-ring={status.label}\n data-node-status-ring-hovered={isRingHovered ? 'true' : 'false'}\n data-node-status-ring-stroke-width={ringStrokeWidth}\n style={{\n transition: 'fill 300ms ease-out, stroke 300ms ease-out, stroke-width 300ms ease-out',\n }}\n />\n );\n })()}\n {/* v0.10.0 Hero 1+2 / §3.F server-health node-ring tint.\n When the host server this agent runs on is in the\n `red` tier (CPU/Mem/Disk worst-of ≥ 85% per\n classifyServer threshold), draw a faint amber outer\n halo at radius+8. strokeWidth=2.5 stays clear of the\n R51 overlap-test sentinels (1.5 = offline status ring,\n 3 = online status ring). pointerEvents:none so the\n halo can't intercept node clicks. Falls back silent\n when host telemetry hasn't shipped yet (hostHealthMap\n is empty until commhub returns telemetry). Composes\n with R209 hover-ring (which sits BETWEEN the avatar\n and this halo on hover — different radii). */\n }\n {(() => {\n const tier = hostHealthMap.get(session.server);\n if (tier !== 'red') return null;\n return (\n <circle\n cx={pos.x}\n cy={pos.y}\n r={radius + 8}\n fill=\"none\"\n stroke={isLight ? '#d97706' : '#fbbf24'}\n strokeWidth=\"2.5\"\n opacity=\"0.6\"\n data-node-server-health=\"red\"\n data-node-server-host={session.server}\n style={{\n pointerEvents: 'none',\n transition: 'stroke 200ms ease-out, opacity 200ms ease-out',\n }}\n />\n );\n })()}\n {/* Issue #96: node \"avatar\" is now driven by the model\n vendor. Decision order:\n 1. ?brand=intern flag, or an intern-aliased node with\n no model field → 书生 coin (preserves #79).\n 2. vendor has a packaged logo asset → that logo image\n (intern-s1-* models land here via vendorForModel).\n 3. known vendor, logo asset not shipped yet → a\n vendor-tinted monogram (spec-mandated fallback).\n 4. unknown vendor / null model → the prefix-group\n hue-hashed initial (#83/#99 behaviour, unchanged). */}\n {(() => {\n const ar = Math.round((isOnline ? 14 : 10) * nodeScale);\n const size = radius * 2;\n const vendor = vendorForModel(session.model);\n const internByAlias = /书生|书小生|intern/i.test(session.alias);\n\n if (isIntern || internByAlias || vendor.logo) {\n return (\n <image\n href={vendor.logo ?? '/intern_avatar.png'}\n x={pos.x - size / 2}\n y={pos.y - size / 2}\n width={size}\n height={size}\n preserveAspectRatio=\"xMidYMid meet\"\n />\n );\n }\n if (vendor.id !== 'unknown') {\n // Known model house, logo asset not in public/vendors/\n // yet — vendor-tinted monogram stands in.\n /* Round 283 / Loop: monogram circle strokeWidth bumps\n 1 → 1.5 per Vincent 5216 \"书生头像风格延续 — 其他\n vendor 头像 plain text/abbreviation 比书生差, polish\n 升级\". Without real vendor logo PNG/SVG assets in\n public/vendors/, the monogram is the visual stand-\n in; bumping its ring weight from 1 to 1.5 narrows\n the visual-quality gap with the 书生 image avatar\n (which is a designed PNG, naturally more\n substantial). The 1px → 1.5px stroke is the same\n weight increment R268 used on the chrome-strip\n border unification — small but perceptible. The\n prefix-group fallback (line ~5172) stays at\n strokeWidth=1 since that's for UNKNOWN vendors\n where less visual weight signals \"we don't know\n what this is\" appropriately. */\n return (\n <>\n <circle cx={pos.x} cy={pos.y} r={ar} fill={vendor.mono.bg} stroke={vendor.mono.ring} strokeWidth=\"1.5\" />\n {/* Round 284 / Loop: known-vendor monogram letter\n swaps fontFamily monospace → system sans-serif.\n Continuation of R283 Vincent 5216 \"vendor 头像\n polish 升级\". A single centered letter does not\n benefit from monospace's digit-alignment\n property — its only effect at this scale is to\n land a slightly thinner, more code-text-like\n glyph. The system stack ('-apple-system',\n 'BlinkMacSystemFont', 'Segoe UI', 'Inter',\n 'sans-serif') picks the OS-preferred designed\n sans-serif, which renders a fuller, more\n \"badge-mark\" letterform — narrowing the\n visual-quality gap with the 书生 PNG (which is\n a hand-designed image). data-monogram-letter\n exposes the element for test probing.\n\n The prefix-group fallback at line ~5197\n INTENTIONALLY stays on monospace — same\n contrast pattern R283 established for ring\n stroke: \"designed glyph\" = known vendor,\n \"code text\" = unknown vendor bucket. */}\n <text\n x={pos.x} y={pos.y} dy=\"0.34em\" textAnchor=\"middle\"\n fill={vendor.mono.text} fontSize={ar}\n fontFamily=\"-apple-system, BlinkMacSystemFont, 'Segoe UI', Inter, sans-serif\"\n fontWeight=\"700\"\n data-monogram-letter={vendor.initial}\n >\n {vendor.initial}\n </text>\n </>\n );\n }\n // Round 106 (issue #83): hue keyed to the prefix group,\n // not the full alias — every 通信* node shares one color.\n const c = aliasAvatarColors(groupKeys[session.alias] || session.alias);\n return (\n <>\n <circle cx={pos.x} cy={pos.y} r={ar} fill={c.bg} stroke={c.ring} strokeWidth=\"1\" />\n <text\n x={pos.x}\n y={pos.y}\n dy=\"0.34em\"\n textAnchor=\"middle\"\n fill={c.text}\n fontSize={ar}\n fontFamily=\"monospace\"\n fontWeight=\"700\"\n >\n {aliasInitial(session.alias)}\n </text>\n </>\n );\n })()}\n {/* Issue #96: runtime badge — small corner glyph marking the\n execution shell (CLI / SDK / HTTP API). Sits bottom-right\n of the avatar; colours kept off the working/idle/offline\n status hues. Absent when the node reports no runtime. */}\n {(() => {\n const rt = runtimeIdentity(session.runtime);\n if (!rt) return null;\n const br = isOnline ? 7 : 5.5;\n const bx = pos.x + radius * 0.72;\n const by = pos.y + radius * 0.72;\n const icon = br * 2 * 0.62;\n // Round 208 / Loop: runtime badge joins the micro-lift\n // radius-axis family. R177 grew the hub ring on hover\n // (r 14→17); R197 grew the legend swatch (r 5.5→7);\n // R208 closes the trio at per-node grain — the runtime\n // badge (CLI/SDK/HTTP indicator at avatar bottom-right)\n // pops r 7→8 (online) / 5.5→6.5 (offline) when the\n // parent node is hovered, with stroke 1.5→2 for\n // matching emphasis. R26 already lifts the label 1.5px\n // and R194 elevates its drop-shadow; R208 gives the\n // runtime indicator its own hover acknowledgement so\n // every per-node surface participates in the gesture.\n // CSS r-as-property + stroke-width are transitionable\n // (same support matrix R197/R198/R199 leveraged:\n // Chrome ≥95 / Safari ≥16 / FF ≥70). data-runtime-\n // badge-active exposes the gate for tests.\n const isNodeActive = !reducedMotion && hoveredAlias === session.alias;\n return (\n <g style={{ pointerEvents: 'none' }}>\n <circle\n cx={bx} cy={by} r={br}\n fill={pal.containerBg}\n stroke={rt.color}\n strokeWidth=\"1.5\"\n data-runtime-badge={session.alias}\n data-runtime-badge-active={isNodeActive ? 'true' : 'false'}\n style={{\n r: isNodeActive ? `${br + 1}px` : `${br}px`,\n strokeWidth: isNodeActive ? '2px' : '1.5px',\n transition: 'r 150ms ease-out, stroke-width 150ms ease-out',\n } as React.CSSProperties}\n />\n {/* Round 443 / Loop: runtime badge inner-icon\n strokeWidth lift on node hover — 2.4 → 2.8 on\n isNodeActive. Pre-R443 the outer badge ring\n lifted (R208 r + sw both grow on hover) but\n the inner icon path stayed locked at sw=2.4.\n The two layers of the runtime badge were\n out of phase: ring thickened, icon stayed\n thin. R443 closes the 2-axis hover signature\n on the badge so both ring and icon lift\n together. +0.4 absolute delta matches the\n R208 ring's +0.5 sw delta (badge ring 1.5 →\n 2.0 absolute), proportional to the icon's\n heavier base of 2.4. Pure paint axis;\n strokeLinecap='round' + strokeLinejoin='round'\n preserved. transition list extends to include\n 'stroke-width 150ms ease-out' matching R208\n outer-ring cadence. data-runtime-badge-icon\n + -active attrs exposed for tests. */}\n <g transform={`translate(${bx - icon / 2} ${by - icon / 2}) scale(${icon / 24})`}>\n <path\n d={rt.iconPath}\n fill=\"none\"\n stroke={rt.color}\n strokeWidth={isNodeActive ? '2.8' : '2.4'}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n data-runtime-badge-icon={session.alias}\n data-runtime-badge-icon-active={isNodeActive ? 'true' : 'false'}\n data-runtime-badge-icon-stroke-width={isNodeActive ? '2.8' : '2.4'}\n style={{ transition: 'stroke-width 150ms ease-out' }}\n />\n </g>\n </g>\n );\n })()}\n {/* Round 294 / Loop: per-node \"working\" pulse dot retired.\n The pulse was R24's per-node working indicator — a\n small green circle at the top of each working node,\n SMIL-animated opacity 1→0.25→1. After R278 retired the\n working halo, R279 retired arrival ping + dispatch\n pulse, R280 retired backdrop spokes, the pulse dot\n was the last surviving per-node SMIL animation in\n the original \"working = breathing\" visual family.\n Status info is preserved through 4 redundant signals:\n status ring green color (R167), label sub-text\n 'working' (R211), chip-row 'X working' count (top\n of canvas), hub centre digit (R130). With 30+\n working nodes on a real fleet, 30 simultaneous SMIL\n pulses add cognitive load with zero new information.\n Same R275-R281/R290/R291 减法 family idiom — the\n last 'wiggling per-node decoration' retires. Gated\n via `{false && ...}` per the R276/R278/R279/R280\n rollback-friendly pattern; the block stays in the\n file documented + dead-coded so future readers see\n the retired pulse-dot rationale + can A/B-restore\n it by flipping the gate. */}\n {false && (() => {\n const sse = sseCountFor ?? 0;\n const dur = sse >= 4 ? '0.7s' : sse >= 2 ? '0.9s' : '1.2s';\n const visible = session.status === 'working';\n return (\n <g\n data-pulse-wrapper={session.alias}\n data-pulse-visible={visible ? 'true' : 'false'}\n style={{\n opacity: visible ? 1 : 0,\n transition: 'opacity 300ms ease-out',\n pointerEvents: 'none',\n }}\n >\n <circle cx={pos.x} cy={pos.y - (radius - 6)} r=\"2.5\" fill={pal.flowParticle} data-pulse-dur={dur} opacity={reducedMotion ? 0.6 : undefined}>\n {!reducedMotion && (\n <animate attributeName=\"opacity\" values=\"1;0.25;1\" dur={dur} repeatCount=\"indefinite\" />\n )}\n </circle>\n </g>\n );\n })()}\n\n {/* Round 98 (issue #61): label rect 124px → 100px.\n Round 109/110 (Vincent P0): full opaque card below the\n density threshold / on hover / when zoomed; otherwise a\n lightweight plain-text alias that keeps every node\n labelled without an opaque box covering its neighbours.\n Round 15 / Loop: when nodeScale=S the node shrinks 30%\n but the label card was staying full-size, so the label\n visually outweighed the small node. Tighten the card\n frame, alias / sub fontSize, drop-offset and truncate\n length specifically for S; M and L keep their existing\n sizes (M ≈ L for labels by design — the S user is the\n one who explicitly asked for a denser view). */}\n {(() => {\n const isSmall = nodeScale < 0.8;\n const cardW = isSmall ? 88 : 100;\n const cardH = isSmall ? 36 : 42;\n const cardTopY = isSmall ? -12 : -14;\n const aliasFs = isSmall ? 11 : 12;\n const subFs = isSmall ? 8 : 9;\n const subY = isSmall ? 15 : 17;\n const dropY = isSmall ? 18 : 22;\n const fullMax = isSmall ? 11 : 12;\n const denseFs = isSmall ? 9 : 10;\n const denseDrop = isSmall ? 12 : 14;\n // Round 26 / Loop: micro-lift the label group on hover —\n // 1.5 px upward, 200 ms ease. Pairs with the Round 18\n // group-box hover-accent treatment to give the same\n // \"this is the focused element\" feedback at the per-\n // node level. CSS transform stacks onto the SVG\n // positioning transform attribute (SVG 2 cascade);\n // bbox is unchanged at rest, so the overlap-test gate\n // continues to see the geometric layout it expects.\n return showFullLabel ? (\n <g transform={`translate(${pos.x}, ${pos.y + radius + dropY})`} style={{ pointerEvents: 'none' }}\n className=\"transition-transform duration-200 group-hover:-translate-y-[1.5px]\">\n {/* Round 194 / Loop: label card picks up a subtle\n drop-shadow that intensifies when the node is\n hovered — pairs the existing R26 1.5px lift\n with physical weight. Pre-R194 the card lifted\n 1.5 px on hover but had no shadow follow, so\n the gesture read as \"card translated\" rather\n than \"card rose off the canvas\". Adding a\n baseline shadow at rest (1px/2px blur) plus a\n deeper hover state (3-4px/8-12px blur) makes\n the elevation feel earned.\n Same R57/R135 hover-elevation idiom that\n panels already use, now extended to per-node\n label cards. data-node-label-card-elevation\n ('idle'/'hover') exposes the state for tests.\n Filter is theme-aware (light: slate alpha;\n dark: black alpha) so the shadow stays visible\n against both surfaces. transition: filter\n 220ms ease-out matches R135's panel-elevation\n duration so a hovered node + a hovered panel\n fade at the same rhythm. Reduced-motion users\n collapse to the rest-state shadow only — no\n hover differentiation. Per-node filter is\n gated to showFullLabel which itself is gated\n to non-dense fleets (≤16 nodes) or hovered/\n zoomed-in branches, so the cost stays bounded\n (~20-30 cards max). */}\n {/* Round 217 / Loop: label card stroke tints to\n legendAccent (cyan) on parent-node hover,\n adding a 3rd hover-feedback channel alongside\n R26 1.5px lift + R194 drop-shadow elevation.\n The card now responds at three layers when its\n parent node is hovered: lift (motion) → shadow\n (depth) → stroke (color tint). All three are\n gated by the same hoveredAlias === session\n .alias state so they ease in unison. transition\n list extends `stroke 220ms ease-out` alongside\n R194's existing filter 220ms — single pair of\n eyes on the card reads \"this is the focused\n element\" via three independent channels. Pin\n + chat states don't compete: pinning a node\n opens chat (R136) but doesn't drive hovered\n Alias, so this stroke tint is exclusively a\n pointer-on-target signal. */}\n {/* Round 246 / Loop: label card chrome picks up\n fill + opacity in its transition list. R142\n already eased filter (drop-shadow) + stroke\n (R217 cyan tint on hover); the rect's fill\n (pal.labelBox.fill: cyber #020617 ↔ light\n #ffffff) and theme-derived opacity (0.94\n cyber / 1 light) still snapped on theme\n toggle. R211 already closed the alias/sub\n text-fill snap on the same card; R246\n closes the chrome-fill snap on the rect\n BEHIND that text, so the whole card\n (background + text) transitions as one\n unit through every theme switch. Same\n 220ms cadence the existing filter/stroke\n pair uses — coordinated 4-property easing\n across the card. */}\n {/* Round 411 / Loop: node label card rx 6 → 8.\n Pre-R411 the per-node label card painted at\n rx=6, sitting one tier BELOW the R332/R375/\n R376 compact-chrome tier (rx=8). Inside the\n corner-radius cascade family the cards used\n to be the only \"smaller\" tier — but the\n label card is a content-bearing surface\n (alias + sub text + ring), not a sub-\n element decoration. R411 lifts rx=6 → 8\n to align with the compact-chrome / segmented-\n control tier so all \"compact card\" surfaces\n read with the same corner radius.\n Corner-radius cascade (8 anchors now):\n R330 canvas rx 12 (root)\n R331 panels rx 10 (recent-signal, legend)\n R332 minimap container rx 8 (compact chrome)\n R375 Layout-toggle rx 8 (segmented control)\n R376 nodeSize/zoom rx 8 (segmented control)\n R390 hover-detail rx 10 (panel)\n R393 minimap viewport rx 2 (sub-element)\n R411 node label card rx 6 → 8 (compact card, this round)\n Pure paint — rx grows the corner curve\n inward without changing the card's outer\n cardW × cardH bbox (cardW=92/cardH=22 for\n standard nodes per R23 / R187 sizing). R217\n hover-stroke cyan tint + R142 drop-shadow\n + R246 fill+opacity 220ms transition list\n + R211 alias/sub text-fill ease all\n preserved. data-node-label-card-rx attr\n exposes the value for tests. */}\n {/* Round 429 / Loop: node label-card body opacity\n 0.94 → 1.0 on hover (cyber theme). Sibling\n treatment to R348 panel rect opacity lift —\n 0.92 → 0.97 cyber / 0.97 → 1.0 light at the\n panel scope. Pre-R429 the cyber theme card\n sat at 0.94 always; on hover R217 tinted the\n stroke + R142 grew the drop-shadow + R26\n lifted the group + R427/R428 spaced the text\n but the rect itself never solidified —\n the card glowed brighter through the\n shadow but the body alpha gap (6 pct) stayed\n fixed. R429 lifts the body to full alpha on\n hover so the card reads as a confidently\n present surface under the cursor (matching\n the panel-pair pattern). Light theme stays\n at 1.0 in both states (already maxed). R246\n transition list already covers opacity 220ms\n so the lift eases for free. R217 stroke tint\n + R142 drop-shadow + R211 fill ease all\n preserved (additive opacity branch only). */}\n <rect\n x={-cardW / 2} y={cardTopY} width={cardW} height={cardH} rx=\"8\"\n fill={pal.labelBox.fill}\n stroke={!reducedMotion && hoveredAlias === session.alias\n ? pal.legendAccent\n : pal.labelBox.stroke}\n opacity={\n !reducedMotion && hoveredAlias === session.alias\n ? 1\n : (isLight ? 1 : 0.94)\n }\n data-node-label-card={session.alias}\n data-node-label-card-rx=\"8\"\n data-node-label-card-elevation={\n !reducedMotion && hoveredAlias === session.alias ? 'hover' : 'idle'\n }\n style={{\n filter: !reducedMotion && hoveredAlias === session.alias\n ? (isLight\n ? 'drop-shadow(0 3px 8px rgba(15,23,42,0.20))'\n : 'drop-shadow(0 4px 12px rgba(0,0,0,0.60))')\n : (isLight\n ? 'drop-shadow(0 1px 2px rgba(15,23,42,0.08))'\n : 'drop-shadow(0 1px 2px rgba(0,0,0,0.30))'),\n transition: 'filter 220ms ease-out, stroke 220ms ease-out, fill 220ms ease-out, opacity 220ms ease-out',\n }}\n />\n {/* Round 211 / Loop: alias + sub text fill eases on\n status flip, matching R167 status-ring fill 300ms.\n Pre-R211 a node going working → idle → offline made\n the ring smoothly recolor (R167) while the label\n card's text snap-cut to the new tier hue in a\n single frame — the node \"transitioned its ring,\n flipped its text\". 300ms inline transition syncs\n all four label-card fills (alias, sub, ring fill,\n ring stroke) to the same beat so the node reads\n as one coordinated status change.\n data-node-alias-text exposes the gate for tests. */}\n {/* Round 305 / Loop: node alias label text picks\n up the pin-signature letter-spacing family\n (R219 / R220) when the node is the chat\n target. The alias is the per-node identity\n label inside the label card; when chat is\n open targeting this node, R242 already adds\n a cyan-tint stroke to the card. R305 brings\n the alias text into the same pin-signature\n family — letter-spacing 0px → 0.5px when\n chatAlias === session.alias. Same vocabulary\n R219 established for recent-row text (line\n ~6354), legend-row text (~6881), and the\n R220 edge-badge text (~4327, with 0.4 for\n hot/pin). Now the per-node alias has its\n own pin signature when chat is open on it.\n transition list extends 'letter-spacing\n 200ms ease-out' so it eases alongside the\n existing 300ms fill transition. */}\n {/* Round 427 / Loop: extend the node alias label\n letter-spacing family to a 3-tier scale —\n rest 0px → hover 0.3px → chat-target 0.5px.\n Pre-R427 the alias text shifted only when\n chat was actively pinned (R305); pure node-\n hover left the text dead-typographic while\n the surrounding card lifted (R26 translateY\n + R242 stroke + filter cues). R427 adds the\n missing typographic axis to the hover gesture\n so the alias text rises with the card.\n The chat-target tier still wins (0.5 > 0.3)\n so the pin signature stays at the top of the\n scale — hover is the mid tier between rest\n and chat-target.\n Hover-letter-spacing family extension:\n R344 chip count digit\n R345 panel title (R423 sibling)\n R347 active-links chip\n R351 vendor chip\n R420 zoom-level chip\n R427 node alias text (this round)\n R211 fill 300ms + R305 letter-spacing 200ms\n transition list preserved; only the\n conditional gets a middle case. */}\n <text\n x=\"0\" y=\"1\" textAnchor=\"middle\"\n fill={status.text}\n fontSize={aliasFs} fontFamily=\"monospace\" fontWeight=\"700\"\n data-node-alias-text={session.alias}\n data-node-alias-chat-target={chatAlias === session.alias ? 'true' : 'false'}\n data-node-alias-hovered={hoveredAlias === session.alias ? 'true' : 'false'}\n style={{\n transition: 'fill 300ms ease-out, letter-spacing 200ms ease-out',\n letterSpacing:\n chatAlias === session.alias ? '0.5px' :\n hoveredAlias === session.alias ? '0.3px' : '0px',\n }}\n >\n {truncate(session.alias, fullMax)}\n </text>\n {/* Round 428 / Loop: node sub-text (status label\n line beneath the alias) adopts hover letter-\n spacing tween 0 → 0.2px on hoveredAlias.\n Sibling treatment to R427 alias-text hover\n tween (0 → 0.3) — the alias is the primary\n identity (top-tier kerning 0.3), the sub-text\n is the secondary status line (one tier lower\n at 0.2). Now both lines of the label card\n telegraph hover typographically as one unit,\n matching the R26 card lift + R242 stroke\n tint + R975 filter cues. Subtler delta on\n the sub-text (0.2 vs alias 0.3) preserves\n the alias > status visual hierarchy at the\n hover scope. R211 fill 300ms transition\n preserved (additive letter-spacing branch\n + appended 'letter-spacing 200ms ease-out'). */}\n {/* Round 448 / Loop: node sub-text fontWeight\n 400 → 500 (font-medium). Sibling to R363\n (recent-row text fw 400→500) + R364 (legend-\n row label fw 400→500) — same \"small mono\n text at fontSize=9-11 needs 500-tier weight\n for legibility\" pattern, now applied to the\n per-node sub-text line. At fontSize=8-9\n monospace against the label-card chrome\n (pal.labelBox.fill cyber #020617 / light\n #ffffff), the default fw=400 sits at the\n legibility floor; fw=500 (font-medium) lifts\n it into a clearly readable band without\n changing geometry. R211 fill 300ms +\n R428 letter-spacing 0→0.2 hover + R427\n alias-text + R429 body opacity all preserved.\n Pure typography lift; no layout shift; the\n alias-text fw=700 (R427) still wins so the\n alias > status hierarchy holds at the type\n level. data-node-sub-text-font-weight attr\n exposes the value for tests. */}\n <text\n x=\"0\" y={subY} textAnchor=\"middle\"\n fill={status.primary}\n fontSize={subFs} fontFamily=\"monospace\"\n fontWeight=\"500\"\n data-node-sub-text={session.alias}\n data-node-sub-text-hovered={hoveredAlias === session.alias ? 'true' : 'false'}\n data-node-sub-text-font-weight=\"500\"\n style={{\n transition: 'fill 300ms ease-out, letter-spacing 200ms ease-out',\n letterSpacing: hoveredAlias === session.alias ? '0.2px' : '0px',\n }}\n >\n {status.label}{isOnline && sseCountFor != null ? ` sse:${sseCountFor}` : ''}\n </text>\n </g>\n ) : (\n // Round 212 / Loop: dense plain-text alias gets fill\n // transition on status flip — extension of R211. Pre-\n // R212 the dense fallback (denseLayout > 16 nodes,\n // where label cards collapse to plain text + R110\n // stroke halo) snap-cut its fill on tier change while\n // the status ring smoothly transitioned (R167) — the\n // card-mode equivalent that R211 just fixed at the\n // ≤16-node grain. Inline transition list combines\n // R26 transform 200ms (group-hover lift) + R212 fill\n // 300ms (status flip ease) — Tailwind transition-\n // transform on className would be displaced by inline\n // transition, so the transform property is explicit\n // in the inline list too. The group-hover:-translate-\n // y-[1.5px] className still fires the transform via\n // CSS pseudo-class; only the transition-property\n // moves to inline. Big fleets benefit most — this is\n // the path users see when their dashboard is busiest.\n /* Round 452 / Loop: dense plain-text alias rest\n opacity 0.9 → 0.95. Closes the alpha gap on the\n dense fleet's per-node label, sibling to R449\n legend-count-active 0.95→1.0 and R450 minimap\n viewport rest 0.9→0.95 — same \"close the\n active-presence alpha gap\" idiom applied here\n to the dense-mode alias text at fontSize=9-10\n monospace. Pre-R452 dense aliases at α=0.9 sat\n just below full alpha; for un-hovered nodes in\n a busy >16-node fleet this is the only label\n readable, so the 10% alpha gap added a subtle\n \"soft-focused chrome\" feel where the labels\n should read as definitive. +0.05 lift makes\n them confidently present without erasing the\n status.text + R110 stroke halo + paintOrder\n layering. R26 group-hover translate + R212\n fill 300ms transition + R110 stroke=container-\n Bg halo all preserved. data-node-dense-alias-\n text-opacity attr exposes the resolved value\n for tests. */\n <text\n x={pos.x}\n y={pos.y + radius + denseDrop}\n textAnchor=\"middle\"\n fill={status.text}\n fontSize={denseFs}\n fontFamily=\"monospace\"\n fontWeight=\"700\"\n opacity={0.95}\n className=\"group-hover:-translate-y-[1.5px]\"\n data-node-dense-alias-text={session.alias}\n data-node-dense-alias-text-opacity=\"0.95\"\n style={{\n pointerEvents: 'none',\n paintOrder: 'stroke',\n transition: 'transform 200ms ease-out, fill 300ms ease-out',\n }}\n stroke={pal.containerBg}\n strokeWidth=\"3\"\n >\n {truncate(session.alias, isSmall ? 9 : 10)}\n </text>\n );\n })()}\n {/* v0.10.0 Hero 3 Wave 1 §3.E — hover detail card.\n Renders an extended-info SVG card next to the\n hovered node showing vendor / model / runtime /\n server fields that don't fit in the compact label\n card. Position flips to the left when the node is\n in the right half of the canvas so the card\n doesn't extend past the viewBox right edge. Only\n one card is visible at any time (gated on\n hoveredAlias === session.alias), so layout cost\n stays bounded.\n Reuses pal.labelBox.fill / pal.legendAccent for\n chrome consistency with the existing label card +\n legend panel. data-topo-hover-detail attribute\n exposes the element for test probes.\n Not rendered in dense layout (>16 nodes) — same\n gate as showFullLabel; dense fleets already have\n too much per-node chrome competing. */}\n {!reducedMotion && hoveredAlias === session.alias && !denseLayout && (() => {\n const v = vendorForModel(session.model);\n const rt = runtimeIdentity(session.runtime);\n const flipLeft = pos.x > VIEWBOX_W * 0.65;\n const detailW = 192;\n const detailH = 88;\n const detailX = flipLeft ? pos.x - radius - 18 - detailW : pos.x + radius + 18;\n const detailY = pos.y - detailH / 2;\n return (\n <g transform={`translate(${detailX}, ${detailY})`} data-topo-hover-detail={session.alias} style={{ pointerEvents: 'none' }}>\n {/* Round 387 / Loop: hover-detail panel cyber backdrop\n opacity 0.94 → 0.97. The hover-detail card is\n ALWAYS rendered in active-hover context (it IS\n the hover product), so it should carry the\n same backdrop weight as the R348 recent-signal /\n legend panel HOVER state (which lifts 0.92 →\n 0.97 cyber). Pre-R387 the card sat at 0.94\n cyber, leaving a 0.03 alpha gap against the\n R348 panel-hover state — small but visible\n when the hover-detail floats next to a hovered\n recent-signal panel. R387 unifies them at 0.97\n so all active-hover panels paint with the same\n confident backdrop opacity in cyber. Light\n stays at 0.98 (already at the strong end —\n R348 light also stays at 0.97/0.98 max).\n Theme-consistency / canvas-presence polish\n family (5th anchor):\n R370 hub hover-ring opacity 0.7 → 0.8 cyber\n R371 edge-badge rest opacity 0.82 → 0.85 cyber\n R372 minimap offline-dot opacity 0.5 → 0.6\n R386 hub-highlight idle opacity 0.9 → 0.95\n R387 hover-detail panel opacity 0.94 → 0.97 cyber (this round)\n data-topo-hover-detail-opacity attr exposes\n the resolved value for tests. R348 drop-shadow\n + rx=8 + stroke=pal.legendAccent + fill=pal.\n labelBox.fill all preserved. */}\n {/* Round 390 / Loop: hover-detail card rx 8 → 10.\n Corner-radius cascade family — the hover-detail\n card is a panel-tier surface (192×88 floating\n info card with drop-shadow + stroke), so its\n corner radius should match the R331 panel tier\n (rx=10) used by the recent-signal and legend\n panels. Pre-R390 it shared rx=8 with the R332\n minimap and R375/R376 segmented-control tier\n (Layout-toggle, nodeSize, zoom wrappers),\n which is the \"compact chrome control\" tier —\n a tier mismatch for a content-bearing panel.\n Corner-radius cascade (6 anchors now):\n R330 canvas rx 12 (root)\n R331 panels rx 10 (recent-signal, legend)\n R332 minimap rx 8 (compact chrome)\n R375 Layout-toggle rx 8 (segmented control)\n R376 nodeSize/zoom rx 8 (segmented control)\n R390 hover-detail rx 10 (panel — this round)\n Pure paint change; no layout shift (rx grows\n the corner curve INWARD without changing the\n card's outer bbox). data-topo-hover-detail-\n rx attr exposes the resolved value for tests.\n R348 drop-shadow + stroke + R387 opacity all\n preserved. */}\n <rect\n x=\"0\" y=\"0\" width={detailW} height={detailH} rx=\"10\"\n fill={pal.labelBox.fill}\n stroke={pal.legendAccent}\n opacity={isLight ? 0.98 : 0.97}\n data-topo-hover-detail-opacity={isLight ? 0.98 : 0.97}\n data-topo-hover-detail-rx=\"10\"\n 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))' }}\n />\n <text x=\"10\" y=\"16\" fontSize=\"9\" fontFamily=\"monospace\" fill={pal.legendAccent} fontWeight=\"700\">\n {v.id !== 'unknown' ? v.label : '—'}\n </text>\n {/* Round 389 / Loop: hover-detail model line (y=32)\n fontWeight 400 → 600. R388 lifted body lines\n (runtime/host/task at fontSize=9) to fw=500;\n R389 closes the typography hierarchy by giving\n the model name (the dominant subhead text in\n the card) its own weight tier. Three-tier\n ladder now reads cleanly:\n vendor fontSize=9 fw=700 (label badge)\n model fontSize=10 fw=600 (subhead — this round)\n body 3× fontSize=9 fw=500 (R388)\n One tier step per dimension (size + weight)\n between adjacent levels — classic editorial\n hierarchy idiom adapted to a 192×88 SVG card.\n Sibling to the chip-internal-hierarchy arc\n (R333-R341/R362/R369) which uses fw=600/500\n for digit/unit pairs; R389 applies the same\n fw=600 to a content-bearing identity line.\n data-topo-hover-detail-model-fw attr exposes\n the resolved value for tests. pal.legendHeadline\n fill preserved (R389 doesn't touch color). */}\n <text x=\"10\" y=\"32\" fontSize=\"10\" fontFamily=\"monospace\" fontWeight=\"600\" fill={pal.legendHeadline} data-topo-hover-detail-model-fw=\"600\">\n {session.model || 'model · pending'}\n </text>\n {/* Round 388 / Loop: hover-detail body lines (the\n three fontSize=9 lines: runtime, host, task)\n gain fontWeight=500. Small-text fw lift family\n (6th anchor) — fontSize 9-10 px text reads\n consistently bolder at fw=500 than at the\n default 400 weight at small sizes, especially\n on the cyber-theme backdrop where stroke-\n rendering is the limiting factor.\n Sibling lifts in this family:\n R363 recent-row alias text 400 → 500\n R364 legend-row label 400 → 500\n R366 group-label count tspan 400 → 500\n R368 +N more flows footer 400 → 500\n R373 pressure-bar kicker (font-medium)\n R388 hover-detail body lines 400 → 500 (this round)\n Tier structure preserved:\n y=16 vendor (fw=700, headline)\n y=32 model (fontSize=10, subhead by size)\n y=48 runtime / y=64 host / y=80 task (body, now fw=500)\n The y=80 task line keeps opacity=0.7 so its\n caption-tier identity stays distinct from the\n y=48 / y=64 body lines despite shared fw.\n data-topo-hover-detail-body-fw attr exposes\n the resolved value for tests. */}\n <text x=\"10\" y=\"48\" fontSize=\"9\" fontFamily=\"monospace\" fontWeight=\"500\" fill={pal.legendText} data-topo-hover-detail-body-fw=\"500\">\n {rt ? rt.label : 'runtime · pending'}\n </text>\n <text x=\"10\" y=\"64\" fontSize=\"9\" fontFamily=\"monospace\" fontWeight=\"500\" fill={pal.legendText} data-topo-hover-detail-body-fw=\"500\">\n host · {session.server || 'unknown'}\n </text>\n <text x=\"10\" y=\"80\" fontSize=\"9\" fontFamily=\"monospace\" fontWeight=\"500\" fill={pal.legendText} opacity=\"0.7\" data-topo-hover-detail-body-fw=\"500\">\n {session.task ? truncate(session.task, 28) : 'no recent task'}\n </text>\n </g>\n );\n })()}\n </g>\n );\n })}\n\n {/* Round 14 / Loop: click ripple — one-shot expanding ring from\n the clicked node, ~500ms, status-coloured. Sits inside the\n zoom/pan <g> so it scales / pans with the topology. Keyed by\n ts so a re-click on any node (same or different) remounts the\n <circle> and the SMIL <animate> elements replay from t=0.\n strokeWidth=2 doesn't match the overlap-test selectors. */}\n {/* Round 227 / Loop: click-ripple SMIL gets ease-out curve.\n Pre-R227 both <animate>s ran with default calcMode=linear,\n which made the ripple grow at constant velocity from\n r0+4 → r0+30 over 500ms — a mechanical \"expansion at\n uniform rate\" feel that didn't match the rest of the\n topology's interaction vocabulary (every CSS transition\n on hover-lift, status-flip, pin-signature uses\n `ease-out`). On click, the ripple is the user's primary\n \"I did that\" confirmation feedback — it should feel\n fast-then-settle like a real pressure wave, not metric.\n\n calcMode=\"spline\" + keyTimes=\"0;1\" + keySplines=\"0.25 0.1\n 0.25 1\" maps directly onto CSS cubic-bezier(0.25, 0.1,\n 0.25, 1), the canonical ease-out curve. SMIL's keySplines\n uses the same 4 control-point convention as CSS but\n space-separated. Applied to BOTH the r and opacity\n <animate> elements so they ease in lockstep — the ring\n decelerates as it expands and fades, the two motions\n together feeling like one organic pulse.\n\n One change reaches three click surfaces — hub center\n (R52), node body (R14), edge midpoint badge (R185) — all\n reuse this single ripple element via the shared\n setClickRipple state. data-click-ripple attr surfaces\n the element for test introspection; calcMode attribute\n on the <animate> reflects the ease-out adoption. */}\n {clickRipple && !reducedMotion && (\n <circle\n key={clickRipple.ts}\n cx={clickRipple.x}\n cy={clickRipple.y}\n r={clickRipple.r0 + 4}\n fill=\"none\"\n stroke={clickRipple.color}\n strokeWidth=\"2\"\n opacity=\"0\"\n data-click-ripple\n style={{ pointerEvents: 'none' }}\n >\n <animate\n attributeName=\"r\"\n values={`${clickRipple.r0 + 4};${clickRipple.r0 + 30}`}\n dur=\"0.5s\"\n calcMode=\"spline\"\n keyTimes=\"0;1\"\n keySplines=\"0.25 0.1 0.25 1\"\n fill=\"freeze\"\n />\n {/* Round 403 / Loop: click-ripple SMIL initial opacity\n 0.7 → 0.8. Pre-R403 the ripple's opacity animation\n faded from 0.7 to 0 over 500ms, providing a clean\n click-feedback pulse. Theme-consistency / canvas-\n presence polish family (R370 hub hover-ring +\n R391 hub-spoke active) already lifted paired\n hover-state alphas from 0.7 → 0.8. R403 brings\n click-feedback into that same alpha — three canvas\n state-feedback indicators (hover-ring, active spoke,\n click ripple) now share a uniform 0.8 start alpha\n so the visual \"I responded\" signal carries the\n same weight regardless of which state fired it.\n Pre-R403 invariants preserved: 500ms duration,\n R227 calcMode='spline' + ease-out keySplines\n (0.25 0.1 0.25 1), fill='freeze', concurrent r\n animation. Theme-consistency family (8 anchors):\n R370 hub hover-ring 0.7 → 0.8\n R371 edge-badge rest 0.82 → 0.85 cyber\n R372 minimap offline-dot 0.5 → 0.6\n R386 hub-highlight idle 0.9 → 0.95\n R387 hover-detail panel 0.94 → 0.97 cyber\n R391 hub-spoke active 0.7 → 0.8\n R392 minimap online-dot 0.9 → 0.95\n R403 click-ripple start 0.7 → 0.8 (this round)\n data-click-ripple-start-opacity attr exposes the\n resolved value for tests. */}\n <animate\n attributeName=\"opacity\"\n values=\"0.8;0\"\n dur=\"0.5s\"\n calcMode=\"spline\"\n keyTimes=\"0;1\"\n keySplines=\"0.25 0.1 0.25 1\"\n fill=\"freeze\"\n data-click-ripple-start-opacity=\"0.8\"\n />\n </circle>\n )}\n\n </g>\n\n {/* #112: overlay panels (recent-signal + legend) render OUTSIDE the\n zoom/pan <g> so they stay fixed while the topology pans/zooms.\n They're sized + tucked into the top corners so every corner of\n each panel is >325px from the canvas centre — i.e. fully outside\n the outermost (offline) ring. No node on any ring can reach the\n corner triangles, so the panels never overlap a node, in ring\n OR grid layout (Vincent 4727 zero-overlap criterion). */}\n {/* latest flow labels */}\n {/* Round 57 / Loop: drop-shadow on the panel rects gives them\n card-like elevation, especially on light theme where the\n near-white fill on a near-white canvas read as pasted-on.\n data-topo-panel-elevation tag so the test can verify both\n panels carry the filter. The filter is on the rect, not\n the parent <g>, so it doesn't shadow the rows + text inside\n — only the panel chrome lifts.\n\n v0.10.0 Hero 3 Wave 1 / RFC §3.C (Vincent 5222 holdover):\n hide recent-signal panel when there's no flow to show.\n Pre-v0.10.0 the panel always-mounted with a \"no flow yet\n · send a message between agents\" placeholder. On a fresh\n fleet that's a full corner of chrome doing nothing —\n exactly the always-mount-stack 5222 calls out. Render the\n panel only when flowLinks actually has rows. R175 fade-in\n still applies — first flow that arrives still eases in.\n Composes with §3.I canvas-corner watermark (only shows\n when this panel is absent). */}\n {flowLinks.length > 0 && (\n <g\n transform=\"translate(16, 16)\"\n data-topo-panel=\"recent\"\n data-topo-panel-hovered={hoveredPanel === 'recent' ? 'true' : 'false'}\n // Round 175 / Loop: corner panels fade-in after the\n // R9/R72/R172/R173/R174 canvas content reveal. Pre-R175\n // recent-signal + legend panels appeared instantly in\n // the corners while nodes/edges/group boxes staggered\n // in around them — felt like 'panels are already\n // there, content shows up'. Delaying the panels to\n // ~700ms (after the first node wave finishes ~540ms\n // and edges begin filling in ~280ms) makes them drop\n // into place AFTER the canvas has revealed.\n // recent-signal panel at 700ms; legend at 800ms below\n // for a soft left-then-right cascade. .anet-fade-in\n // is the same R3 mount-once animation the other 4\n // wave layers use — fifth surface in the family.\n className=\"anet-fade-in\"\n data-topo-panel-fade-delay={700}\n style={{ animationDelay: '700ms' }}\n onMouseEnter={() => setHoveredPanel('recent')}\n onMouseLeave={() => setHoveredPanel(prev => prev === 'recent' ? null : prev)}\n >\n {/* Round 331 / Loop: recent-signal panel rect rx 8 → 10\n for proportional corner-radius rhythm after R330\n bumped the outer canvas wrapper to rounded-xl (12 px).\n Pre-R331 the panel sat at rx=8 (matching the legacy\n rounded-lg wrapper envelope); now it follows the\n wrapper one tier down:\n outer wrapper rounded-xl 12 px (R330)\n inner SVG panels rx=10 10 px (R331)\n inner detail card rx=8 8 px (codex 8f981a9)\n node label card rx=6 6 px (legacy R63)\n Geometry-safe: rx changes paint only, not bbox; the\n topo-overlap-test reads bbox geometry. Sibling change\n at the legend panel rect below (~line 6914) keeps\n the two corner panels symmetric. */}\n <rect\n x=\"0\" y=\"0\" width=\"230\" height=\"88\" rx=\"10\"\n fill={pal.legendBox.fill}\n // Round 423 / Loop: panel rect stroke tints to legendAccent\n // (cyan) on hover — sibling to R217 label-card stroke\n // hover-tint at the panel scope. Pre-R423 the panel rect\n // stroke painted pal.legendBox.stroke (neutral) regardless\n // of hover state, while every other panel hover cue stacked:\n // R135 drop-shadow boost\n // R348 rect opacity 0.92 → 0.97 cyber\n // R345 title letter-spacing 0.3 → 0.4\n // R423 rect stroke → legendAccent (this round)\n // Four hover layers now telegraph \"you're entering this\n // panel\" through structural, paint, and typographic axes\n // simultaneously. R247 transition list already covers\n // stroke 200ms ease-out so the tint eases naturally.\n // Sibling change at the legend panel rect below.\n stroke={hoveredPanel === 'recent' ? pal.legendAccent : pal.legendBox.stroke}\n opacity={hoveredPanel === 'recent' ? (isLight ? 1 : 0.97) : (isLight ? 0.97 : 0.92)}\n style={{\n /* R135: drop-shadow intensifies on panel hover. Base\n shadow (2px / 6px blur) signals card elevation\n (R57); hovered (4px / 12px blur) tells the user\n the whole chrome is interactive territory — rows\n pin (R116), footer navigates (R133), legend rows\n pin status (R61). Reuses R18's KPI-card hover-\n elevation idiom for visual consistency. Theme-\n aware shadow colour stays the same; just the\n spread + blur grow.\n\n Round 247 / Loop: extend the transition list to\n include fill + stroke + opacity at 200ms. R135\n already eased filter (hover drop-shadow); the\n three theme-driven properties (pal.legendBox.fill\n cyber #020617 ↔ light #ffffff, pal.legendBox.\n stroke cyber #1f2937 ↔ light #e2e8f0, opacity\n 0.92 ↔ 0.97) still snapped on theme toggle. Same\n per-element 4-property easing R246 added to the\n per-node label card chrome — now applied at the\n panel scope so the whole panel (background + chrome\n + shadow) eases as one unit through theme switches. */\n filter: hoveredPanel === 'recent'\n ? (isLight ? 'drop-shadow(0 4px 12px rgba(15,23,42,0.14))'\n : 'drop-shadow(0 4px 12px rgba(0,0,0,0.65))')\n : (isLight ? 'drop-shadow(0 2px 6px rgba(15,23,42,0.08))'\n : 'drop-shadow(0 2px 6px rgba(0,0,0,0.45))'),\n transition: 'filter 200ms ease-out, fill 200ms ease-out, stroke 200ms ease-out, opacity 200ms ease-out',\n }}\n data-topo-panel-elevation=\"recent\"\n />\n {/* Round 266 / Loop: panel title fill picks up theme-toggle\n transition. Pre-R266 the title \"recent signal\" had\n fill={pal.legendHeadline} (cyber #e5e7eb ↔ light\n #0f172a) without any inline transition — so the BIGGEST\n text in the recent-signal panel (fontSize 12 fontWeight\n 700) hard-flipped color on theme toggle while the panel\n rect (R247) and every row inside (various) eased.\n Sibling treatment to the legend panel title at line\n ~6195 — the panel-pair's titles now ease together. */}\n {/* Round 301 / Loop: panel titles get letterSpacing=\"0.3\"\n for editorial parity with R289 watermark letterSpacing\n + R285 kicker tracking-widest. At fontSize 12 monospace\n fontWeight 700, default 0px letter-spacing reads as a\n code-style label; 0.3px gives it a touch of designed-\n header register without changing the lowercase\n terminal-style aesthetic. Sibling treatment applied\n to recent-signal panel title (here) and legend panel\n title (line ~6556) — both panels share the same\n editorial-text-spacing convention. data-recent-panel-\n title handle unchanged so R266 test still resolves. */}\n {/* Round 345 / Loop: recent-signal panel title gains\n letter-spacing tween 0.3 → 0.4 on panel hover.\n hoveredPanel === 'recent' is set by the panel <g>\n wrapper's onMouseEnter (line ~6263 area). Sibling to\n R344 hover-letter-spacing applied to the +N more\n flows footer — same gesture vocabulary at a panel-\n title scope: hovering the panel chrome spreads the\n title 0.1 px, signalling \"this is a coherent unit\n you're entering\". transition list extends letter-\n spacing 200ms ease-out alongside existing fill 200ms.\n Round 482 / Loop — add 2nd typographic axis to the\n title: fontWeight 700 → 800 on activeEdgeKey (any\n row hover OR pin propagates from hoveredEdgeKey ??\n pinnedEdgeKey). Pre-R482 the title only responded\n to panel-chrome hover via R345 ls; when a specific\n row was hovered/pinned inside the panel, the title\n stayed flat. R482 closes the gap: when ANY row is\n active inside the panel, the title tightens\n typographically alongside the row's own R143 lift +\n R472 tint + R474 text spread. data tightens under\n attention pattern extension (panel-scope variant\n following R416/R424/R425/R426/R444/R445/R446/R457\n at the chip / panel / hub / edge / count / parent-\n label tiers).\n transition list extends to include 'font-weight\n 200ms ease-out' alongside R345's ls + R55's fill\n 200ms. data-recent-panel-title-fw exposes the\n resolved weight for tests. */}\n <text x=\"13\" y=\"21\" fill={pal.legendHeadline} fontSize=\"12\" fontFamily=\"monospace\" fontWeight={activeEdgeKey ? '800' : '700'} letterSpacing={hoveredPanel === 'recent' ? '0.4' : '0.3'} style={{ transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out, font-weight 200ms ease-out' }} data-recent-panel-title data-recent-panel-title-fw={activeEdgeKey ? '800' : '700'} data-recent-panel-title-active={activeEdgeKey ? 'true' : 'false'}>recent signal</text>\n {/* R96: header count now matches what the rows show. Pre-R96\n this read \"X msgs\" off the raw messages array, but the\n rows below render DEDUPED flowLinks — so a fleet with 10\n messages aggregating to 3 pairs read \"10 msgs\" above\n only 3 rows. Misreads as \"where are the other 7?\".\n \"X flows\" mirrors flowLinks.length one-for-one. When\n flows < msgs the chip-row's \"N active links · last 2s\"\n already tells the operator about traffic volume — no\n duplicate metric needed here.\n R129 / Loop: header gains an amber \" · N hot\" tail\n when ≥ 1 flowLink has count ≥ 10. The third surface\n of the hot-lane convention (R126 canvas badge / R127\n row count) lives at the panel header so a user\n scanning vertically — header → rows — gets a top-\n level summary before reading each row's amber digit.\n Restructured into a single <text> with <tspan>\n fragments so the amber portion can carry its own\n fill + weight without a sibling <text>. Switched\n anchor x=150 left-justified → x=217 right-justified\n so the count column unifies visually with the legend\n panel's right-justified header (line 3511 — also\n fontSize 10 monospace, also x≈215 textAnchor end).\n data-recent-panel-count stays on the flow tspan so\n the R96 / R128 tests still resolve. data-recent-\n panel-hot-count exposes the hot bucket count. */}\n {(() => {\n const hotFlowCount = flowLinks.filter(l => l.count >= 10).length;\n const hotStroke = isLight ? '#d97706' : '#fbbf24';\n // R162 / Loop: freshness tint on the panel-header\n // count tspan. R161 just colored the chip-row's \"N\n // active links · last 5s\" bullet by recency; the\n // recent-signal panel header is the panel-side\n // mirror of the same metric (flowLinks.length). Both\n // scopes now speak the same freshness vocabulary,\n // so a glance at either tells the operator whether\n // the network is firing right now. Four nested\n // scopes share one ladder:\n // canvas edge fade (R10)\n // row pip (R160)\n // chip bullet (R161)\n // panel header (R162, this round)\n // Same alpha ramp:\n // ageSec ≤ 30 → 1.0 (fully fresh)\n // 30-300s → smooth decay 1.0 → 0.25\n // > 300s → 0.25 stale floor\n // Hot tail (amber \" · N hot\" R129) is independent\n // of recency and keeps its own color — recency\n // tints the head; volume colors the tail.\n const recentMs = flowLinks.reduce<number | null>((acc, l) => {\n if (!l.last_at) return acc;\n const t = Date.parse(l.last_at);\n if (Number.isNaN(t)) return acc;\n return acc === null || t > acc ? t : acc;\n }, null);\n const ageSec = recentMs !== null\n ? Math.max(0, (Date.now() - recentMs) / 1000)\n : 999;\n const alpha = ageSec <= 30\n ? 1\n : ageSec <= 300\n ? 1 - ((ageSec - 30) / 270) * 0.70 /* R358: floor 0.25 → 0.30 lift across 3 freshness scopes */\n : 0.30; /* R358: stale floor lifted 0.25 → 0.30 — 20% legibility bump while preserving fresh/stale ratio */\n // Dark cyan-400 / light teal-600 with alpha — same\n // palette as R161's chip bullet so the two scopes\n // visually align even side-by-side.\n const freshFill = isLight\n ? `rgba(13, 148, 136, ${alpha.toFixed(2)})`\n : `rgba(34, 211, 238, ${alpha.toFixed(2)})`;\n return (\n <text\n x=\"217\" y=\"21\"\n textAnchor=\"end\"\n fontSize=\"10\"\n fontFamily=\"monospace\"\n // Round 349 / Loop: editorial letter-spacing 0.2 on the\n // recent-signal panel header count. Sits one tier below\n // the R301 panel title letterSpacing=\"0.3\" so the panel\n // header reads as a 2-step hierarchy (title 0.3 / count\n // 0.2). Sibling change on the legend panel count below\n // closes the panel-pair editorial symmetry. Joins the\n // R285 / R289 / R301 / R302 / R304 / R325 editorial-\n // letterspacing tier at the panel-summary scope. The\n // R162 freshness fill, R225 tabular-nums, R311 fw=600,\n // R336 unit-tspan opacity-0.7 split all preserved —\n // the tier propagates to all descendant tspans via\n // SVG inheritance. data-recent-panel-count-letter-\n // spacing exposes the value for tests.\n letterSpacing=\"0.2\"\n data-recent-panel-count-letter-spacing=\"0.2\"\n >\n {/* Round 225 / Loop: tabular-nums on the panel-header\n flow-count tspan. The \"{N} flows\" string lives in\n a right-justified text anchor (x=217 textAnchor=\n 'end') so the BASELINE of the numeral is the same\n regardless of digit-count — but the SPACING between\n the digit and ' flows' label is monospace-jittery\n in the 1-digit → 2-digit boundary, and the ' · N\n hot' R190 tail that hangs off the end shifts by\n whatever the digit width delta is. Tabular-nums\n locks both, so the header reads stable through\n 9 flows → 10 flows growth. Sibling treatment to\n R224 edge badge / R225 hub digit. */}\n {/* Round 311 / Loop: recent-signal panel count tspan\n picks up fontWeight=600 for sibling parity with\n R310 legend panel count. Closes the panel-pair\n count typography symmetry — both top-corner\n panels now have:\n title fontWeight=700 (panel chrome anchor)\n count fontWeight=600 + tabular-nums (data)\n Same digit-semibold rule R309 established for\n per-row counts now applied to BOTH panel-summary\n counts. The R162 freshness fill (1.0→0.25 alpha\n ramp) and R225 tabular-nums all preserved; only\n the weight bumps. */}\n {/* Round 336 / Loop: split the digit from the unit\n word \" flows\" with a nested tspan at opacity=0.7.\n Same chip-internal-hierarchy pattern R333 (vendor\n count suffix) + R335 (filter pin prefix) applied\n to one chip — recurring \"small label spans demote,\n value stays prominent\" idiom at the panel-header\n count scope. The digit stays fw=600 + tabular-nums\n (R311 + R225 inheritance via the outer tspan);\n the unit tspan inherits fw=600 but adds opacity\n 0.7. Reads as \"5 (prominent) / flows (recessive\n unit)\". data-recent-panel-count attribute stays\n on the OUTER tspan so existing R311 fontWeight\n tests + count value reads still resolve via\n .textContent. data-recent-panel-count-unit on\n the inner unit tspan for R336 introspection. */}\n {/* R424 — recent-signal panel count digit fontWeight\n 600 → 700 on panel hover. Closes the 5-layer panel\n hover cue stack with a typographic-weight axis at\n the panel-header data scope: depth (R135 drop-\n shadow) + solidity (R348 fill opacity) + spacing\n (R345 title letter-spacing) + edge color (R423\n stroke tint) + weight (THIS, digit fw). Sibling\n pattern to R416 chip-digit-hover-bold at chip\n scope — same \"data tightens under attention\"\n idiom now at the panel-header data scope. R311\n base fw=600 + R225 tabular-nums + R162 fill\n transition + R336 unit-tspan opacity-0.7 all\n preserved; only the weight axis tweens via R247's\n transition shape (added font-weight to the list). */}\n <tspan\n fill={freshFill}\n fontWeight={hoveredPanel === 'recent' ? '700' : '600'}\n data-recent-panel-count\n data-recent-panel-count-freshness-alpha={alpha.toFixed(2)}\n style={{\n transition: 'fill 200ms ease-out, font-weight 200ms ease-out',\n fontVariantNumeric: 'tabular-nums',\n }}\n >{flowLinks.length}<tspan opacity=\"0.7\" data-recent-panel-count-unit> flows</tspan></tspan>\n {/* Round 190 / Loop: R129 hot-tail gets anet-fade-in\n for entrance. Pre-R190 the tspan snapped into\n the header the moment hotFlowCount crossed 0,\n and snapped out the moment it dropped back to 0.\n Same trade-off R67 accepts for filter pills:\n fade-IN smooth, accept exit snap. The CSS\n animation plays once when the tspan mounts\n (count goes 0 → 1+); subsequent re-renders\n (count growing from 1 → 2 → 3 hot flows)\n preserve the element via React reconciliation\n so the fade-in doesn't replay. Layout-shift\n cost is paid once on entrance — the parent\n <text textAnchor=\"end\"> recomputes its\n anchor as the tspan appears, then stays\n stable as the digit grows. Exit-snap is rare\n in steady operation: a hot flow cooling back\n below 10 messages doesn't happen often. */}\n {/* Round 223 / Loop: hot-tail tspan always-mounts;\n visibility crossfades via inline opacity gate\n instead of conditional mount/unmount on hot\n FlowCount > 0. Pre-R223 R190's anet-fade-in gave\n a smooth entrance but exit was snap (the family's\n original \"fade-IN smooth, accept exit snap\" trade-\n off). R223 closes the asymmetry — both directions\n now ease 300ms. anet-fade-in className kept for\n R190 test compat (plays once on initial render\n if the page loads with hotFlowCount > 0); subsequent\n threshold crossings use the opacity transition\n bi-directionally. Text content gates to empty\n string when hidden so the parent <text textAnchor=\n \"end\"> doesn't compute anchor against stale \"·\"\n separator. data-recent-panel-hot-visible exposes\n the gate for tests. Always-mount-opacity-gate\n family now hits 10 surfaces (R181/R182/R183/R213×2/\n R214/R215/R221/R222/R223). */}\n {/* Round 322 / Loop: hot count tspan picks up\n fontVariantNumeric tabular-nums for parity with\n its left-sibling tspan (R311 `{flowLinks.length}\n flows`, already tabular). Pre-R322 a hotFlowCount\n crossing 1→10 widened the leading digit and (since\n the parent <text> is textAnchor=\"end\") shifted the\n WHOLE header left a few pixels — visible micro-\n jitter against the panel rect's left edge. Tabular-\n nums locks the digit so the right-anchored block\n stays stable as hotFlowCount grows. 8th surface\n in the info-density tabular-nums sweep:\n R224 edge badge / R225 hub digit / R225 panel\n flows-count + recent-row count / R229 group-\n label count / R230 group-label status pips /\n R320 recent-row count fw=600 (left neighbour) /\n R321 recent-row timestamp / R322 panel hot\n count (this round). */}\n <tspan\n fill={hotStroke}\n fontWeight=\"700\"\n data-recent-panel-hot-count={hotFlowCount}\n data-recent-panel-hot-visible={hotFlowCount > 0 ? 'true' : 'false'}\n className=\"anet-fade-in\"\n opacity={hotFlowCount > 0 ? 1 : 0}\n style={{\n transition: 'opacity 300ms ease-out',\n fontVariantNumeric: 'tabular-nums',\n }}\n >\n {/* Round 336 / Loop: split hot count from \" hot\"\n unit word with nested opacity-0.7 tspan. Same\n chip-internal-hierarchy idiom this round\n applies to the \"{N} flows\" tspan above (R311\n sibling) — digit prominent at fw=700 + amber\n fill, unit recessive at 0.7 opacity. data-\n recent-panel-hot-count-unit exposes the unit\n tspan for R336 probes. */}\n {hotFlowCount > 0 ? (\n <>\n {` · ${hotFlowCount}`}\n <tspan opacity=\"0.7\" data-recent-panel-hot-count-unit> hot</tspan>\n </>\n ) : ''}\n </tspan>\n </text>\n );\n })()}\n {/* Round 45 / Loop: empty state. The panel used to render\n \"recent signal\" + \"0 msgs\" with three blank slots below\n when no flow yet — read as \"broken\" rather than \"quiet\".\n A muted centred placeholder makes the empty state\n deliberate. Messages count CAN diverge from flowLinks\n count (raw count vs. deduped pairs), so the placeholder\n fires on flowLinks.length=0 specifically. */}\n {/* Round 222 / Loop: empty state always-mounts; visibility\n crossfades via wrapper <g> opacity instead of conditional\n mount/unmount on flowLinks.length === 0. Pre-R222 the\n first flow arriving snap-removed the empty state in one\n frame while R203 rows simultaneously faded IN — half-\n smooth, half-snap on this \"first data\" first-impression\n moment. R222 closes the snap so empty fades OUT while\n rows fade IN — a proper crossfade for the user's most\n emotionally-loaded moment (the empty-to-populated flip).\n\n R200 SMIL breath on each text continues running\n regardless of parent opacity (SVG opacity is\n multiplicative — same R214 pulse-dot composition idiom).\n When parent opacity is 0, SMIL still animates child\n opacity but the result composes to invisible. CSS and\n SMIL don't fight, they layer.\n\n 300ms transition matches R203 row fade-in pace so the\n empty-fade-out + rows-fade-in pair share rhythm during\n the crossfade. */}\n <g\n data-recent-signal-empty-wrapper\n data-recent-signal-empty-visible={flowLinks.length === 0 ? 'true' : 'false'}\n style={{\n opacity: flowLinks.length === 0 ? 1 : 0,\n transition: 'opacity 300ms ease-out',\n pointerEvents: 'none',\n }}\n >\n {/* Round 258 / Loop: re-center the empty state within the\n post-R256 88-tall panel. R45 placed \"no flow yet\" at\n y=54 + the hint at y=68 inside an 84-tall panel — those\n baselines sat 12px / 26px below the panel mid-line at\n y=42, optically balanced for the original height. R256\n grew the panel 84 → 88 to give the \"+N more flows\"\n footer underline breathing room, shifting the panel\n mid-line to y=44 — but the empty state stayed put,\n drifting 2px high. R258 pushes both empty-state lines\n +2 (main y=54 → y=56, hint y=68 → y=70) so the pair\n sits 12px / 26px below the new mid-line, restoring\n the R45 optical balance. The R222 always-mount opacity\n gate is unaffected (geometry-only shift), and the\n R200 SMIL breath continues unchanged. Hint baseline\n y=70 still sits 12px above the footer baseline (y=82),\n same vertical rhythm as the row 3 → footer gap. */}\n {/* Round 302 / Loop: empty-state hint 'no flow yet' picks\n up letterSpacing='0.2' for editorial parity with R301\n panel titles (0.3) + R285 kicker tracking-widest +\n R289 watermark letterSpacing. The hint is the\n panel's quietest authored text (italic monospace\n fontSize 10 opacity 0.65); adding a small positive\n letter-spacing keeps it in the same designed-label\n family without lifting its visual weight. 0.2px is\n slightly less than the panel titles' 0.3 — appropriate\n for the smaller fontSize + lower opacity (empty-state\n is intentionally quieter than the header above it). */}\n <text\n x=\"115\" y=\"56\" textAnchor=\"middle\"\n fill={pal.legendText}\n fontSize=\"10\" fontFamily=\"monospace\" fontStyle=\"italic\"\n letterSpacing=\"0.2\"\n opacity={0.65}\n data-recent-signal-empty\n data-recent-signal-empty-breathes={reducedMotion ? 'false' : 'true'}\n >\n no flow yet\n {!reducedMotion && (\n <animate\n attributeName=\"opacity\"\n values=\"0.55;0.78;0.55\"\n dur=\"4.4s\"\n repeatCount=\"indefinite\"\n />\n )}\n </text>\n {/* Round 259 / Loop: instructional hint bumps fontSize 8 → 9\n for readability. Pre-R259 the empty-state hint was at\n the smallest readable size on the canvas (8pt), with\n italic + opacity 0.45 layering legibility cost on top\n — instructional text users need to READ to act on,\n yet eye-straining at default 1× zoom. 9pt italic stays\n visually subordinate to the 10pt main \"no flow yet\"\n AND to the 9pt regular row text (italic alone\n discriminates from row content) while easing the\n legibility floor. Sibling change at the +N-more\n footer link (line ~6047) applies the same bump to\n the panel's other italic secondary text. Per-row\n timestamp at y=38+i*16 (fontSize 8 right-edge\n recency tag) STAYS at 8 — it's an at-a-glance\n recency tag tightly co-located with row text, not\n read-to-act instruction. */}\n {/* Round 304 / Loop: secondary instructional hint\n 'send a message between agents' gets letterSpacing\n '0.15'. Extends the R301/R302 editorial-spacing\n family one layer down. The hint is the quietest\n authored text in the recent-signal panel (fontSize\n 9 italic-less opacity 0.45, sits below the R302\n main empty-state hint at fontSize 10 italic\n opacity 0.65). 0.15px is below R302's 0.2px to\n match the visual hierarchy: smaller + quieter\n text gets less letter-spacing.\n 5-axis editorial-letterspacing hierarchy now:\n R285 kicker: 1.2px (eyebrow loud)\n R289 watermark: 0.5px (wordmark brand)\n R301 panel titles: 0.3px (section headers)\n R302 empty main: 0.2px (empty-state hint)\n R304 empty hint: 0.15px (instructional sub)\n Each step ~0.1-0.5x scale-down matches the\n font-size + opacity descent. */}\n {/* Round 339 / Loop: empty-state sub-hint picks up\n fontStyle=\"italic\" for parity with the main hint\n above (line ~6526). Pre-R339 the main hint \"no\n flow yet\" was italic while the sub-hint \"send a\n message between agents\" was upright — two empty-\n state texts sharing the same quiet informational\n role but rendered in different styles. R339 closes\n the inconsistency: both texts now read as deliberate\n italic empty-state messaging. The R304 letter-\n spacing 0.15 + R259 fontSize 9 + opacity 0.45 +\n SMIL opacity breath all preserved. */}\n <text\n x=\"115\" y=\"70\" textAnchor=\"middle\"\n fill={pal.legendText}\n fontSize=\"9\" fontFamily=\"monospace\" fontStyle=\"italic\"\n letterSpacing=\"0.15\"\n opacity={0.45}\n data-recent-signal-empty-hint\n data-recent-signal-empty-hint-breathes={reducedMotion ? 'false' : 'true'}\n >\n send a message between agents\n {!reducedMotion && (\n <animate\n attributeName=\"opacity\"\n values=\"0.36;0.58;0.36\"\n dur=\"4.4s\"\n begin=\"-1.5s\"\n repeatCount=\"indefinite\"\n />\n )}\n </text>\n </g>\n {flowLinks.length === 0 ? null : (\n // Round 56 / Loop: each row is a navigator into the canvas.\n // Hover a row → set hoveredEdgeKey, which the existing R50\n // edge-focus + R49 endpoint-highlight ladders consume. The\n // matching flow edge brightens to 2× + thickens, its two\n // endpoint nodes stay full opacity, and every other edge +\n // non-endpoint node dims. Released → all restore. Wrapping\n // <g> + a transparent 218×14 hitbox so the cursor doesn't\n // have to land precisely on the truncated text.\n flowLinks.slice(0, 3).map((link, index) => {\n // Round 94 / Loop: per-row relative timestamp. The chip\n // row shows \"last 2s\" for the most recent flow overall,\n // but a user scanning the recent-signal panel had no way\n // to tell whether row 2 was 5s old or 5m old without\n // hovering nodes. Compact `2s` / `1m` glyph at the\n // right edge — same relativeAgo helper R42 uses for\n // the chip — pulls double duty as a recency anchor and\n // a sortedness hint (top row is freshest by construction).\n // Strip the \" ago\" suffix — at fontSize=8 in a 32-px\n // right-edge slot, every char counts. \"30s ago\" → \"30s\".\n const rawAt = link.last_at ? relativeAgo(link.last_at) : null;\n const lastAt = rawAt ? rawAt.replace(/\\s+ago$/, '') : null;\n const isRowHovered = hoveredEdgeKey === link.key;\n const isRowPinned = pinnedEdgeKey === link.key;\n const isRowActive = isRowHovered || isRowPinned;\n // Round 191 / Loop: timestamp text on row's right edge\n // picks up the R160 row-pip freshness ramp at a\n // different alpha range — gives the timestamp the same\n // visual recency encoding the pip has on the LEFT\n // edge, so both ends of the row mirror each other.\n // ≤30s → opacity 0.85 (fresh, high contrast)\n // 30-300s → 0.85 → 0.30 (smooth decay)\n // >300s → 0.30 (stale floor)\n // Pre-R191 the timestamp was static 0.55 — the same\n // visual weight whether the message just fired or\n // happened 5 minutes ago. Fill stays legendText gray\n // (not cyan) because the left pip already carries the\n // cyan; the right timestamp encodes recency through\n // contrast against the dark canvas, not hue.\n const tsAlpha = !link.last_at ? 0.55 : (() => {\n const ageSec = Math.max(0, (Date.now() - Date.parse(link.last_at)) / 1000);\n return ageSec <= 30 ? 0.85\n : ageSec <= 300 ? 0.85 - ((ageSec - 30) / 270) * 0.55\n : 0.30;\n })();\n // R127: panel-side mirror of R126's canvas hot-badge.\n // The recent-signal row text packs `alias→alias / N /\n // preview` into one line, with N rendered identically\n // regardless of magnitude. Now that the canvas badge\n // tells the user \"≥ 10 msgs = hot lane\" via amber\n // stroke, the panel row needs the same affordance so\n // the user reading the list at a glance can spot hot\n // lanes without crossing to the canvas. Renders the\n // count digit in amber + 700-weight when isHot; the\n // surrounding alias text + separators stay in the\n // existing legendText/legendHeadline palette. Reuses\n // R126's hotStroke colour for visual consistency.\n const isHot = link.count >= 10;\n const hotStroke = isLight ? '#d97706' : '#fbbf24';\n return (\n <g\n key={link.key}\n data-recent-row={link.key}\n data-recent-row-hovered={isRowHovered ? 'true' : 'false'}\n data-recent-row-pinned={isRowPinned ? 'true' : 'false'}\n data-recent-row-hot={isHot ? 'true' : 'false'}\n data-recent-row-lifted={(isRowHovered || isRowPinned) ? 'true' : 'false'}\n // Round 203 / Loop: per-row mount fade-in. R175 already\n // eased the whole panel in once, but new flows rising\n // INTO the top-3 list (or replacing an older row) snap-\n // popped in. React reconciliation via key={link.key}\n // preserves stable rows across re-renders, so anet-\n // fade-in only plays on mount — never replays when\n // counts update or rows reorder by recency. Stacks on\n // the panel's own R175 anet-fade-in: SVG opacity\n // composes multiplicatively, so during the first paint\n // the panel's 700ms delay holds rows hidden until the\n // panel reveals, then row opacity transitions inside\n // the visible panel. For mid-session arrivals (panel\n // already at opacity 1) the row's 150ms fade-in plays\n // standalone. Three layers of mount-once eases now\n // share rhythm: panel (R175) → rows (R203) → row\n // contents (existing R160 pip / R191 ts opacity\n // ramps animate independently after mount).\n className=\"anet-topo-svg-focus anet-fade-in\"\n role=\"button\"\n tabIndex={0}\n aria-pressed={isRowPinned}\n // R143 / Loop: extend the R135/R142 \"interactive surface\n // elevates\" idiom down one layer to the recent-signal\n // panel rows. R104 already tints the row background on\n // hover; R143 adds a 1-px translate so the row text\n // visually lifts off the panel — same vocabulary R51\n // uses for nodes, R135 uses for panels, R142 uses for\n // group boxes. Pinned rows lift too (sticky state\n // should look like locked-in selection). Reduced-motion\n // safe via prefers-reduced-motion blanket override\n // applied to transition-duration in globals.css.\n style={{\n cursor: 'pointer',\n transform: (isRowHovered || isRowPinned) ? 'translateY(-1px)' : undefined,\n transition: 'transform 150ms cubic-bezier(0.4, 0, 0.2, 1)',\n }}\n onPointerDown={(e) => e.stopPropagation()}\n onMouseEnter={() => setHoveredEdgeKey(link.key)}\n onMouseLeave={() => setHoveredEdgeKey(prev => prev === link.key ? null : prev)}\n // R116: click toggles pin. activeEdgeKey =\n // hoveredEdgeKey ?? pinnedEdgeKey so the matching\n // edge stays \"hot\" after mouseleave; click again\n // (or Esc) releases.\n onClick={() => setPinnedEdgeKey(prev => prev === link.key ? null : link.key)}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n setPinnedEdgeKey(prev => prev === link.key ? null : link.key);\n }\n }}\n >\n {/* R148 / Loop: row tooltip with full message context.\n The row text truncates aliases to 6 chars (R127)\n and content to 8 chars — useful for scan-density\n but obscures the underlying message. A native SVG\n <title> reveals the full alias / content /\n timestamp on hover. Pinned vs unpinned switches\n the click hint so the user knows the next\n gesture's effect. R98 enriched the node tooltip\n the same way for the source/destination scope;\n R148 brings the per-row equivalent to the panel\n side. */}\n <title>{[\n `${link.from} → ${link.to} · ${link.count} msg${link.count === 1 ? '' : 's'}${isHot ? ' (hot lane · ≥ 10)' : ''}`,\n link.last_at ? `last: ${new Date(link.last_at).toLocaleString()}` : null,\n link.content ? `\"${link.content}\"` : null,\n isRowPinned ? 'click to release pin (Esc to clear)' : 'click to pin · hover to preview',\n ].filter(Boolean).join('\\n')}</title>\n {/* R104: subtle row-background tint on hover. R56\n already brightens the matching edge on the canvas,\n but the panel row itself stayed flat — felt more\n like text-with-handlers than a navigable list.\n Filling the rect at hover with `pal.legendAccent`\n at low alpha gives the row visual feedback at the\n source surface, mirroring the list-item idiom from\n the chip-row pills. R116: pinned rows tint\n stronger than hovered ones so locked vs preview\n is discriminable. */}\n {/* Round 472 / Loop — cadence-sync follow-on to the\n R459/R460/R461/R464/R465/R470 200ms uniform\n motion stack established at the cluster scope.\n This R104 recent-signal row tint rect was still\n at the legacy 150ms cadence — when a user\n hovers/pins a recent-signal row, the tint\n snapped in 50ms ahead of the rest of the row's\n state-change cascade (R143 translateY,\n R220+R434 letter-spacing, R434 fill tween).\n R472 lifts to 200ms ease-out to match. Same\n sibling idiom R459 closed at the group-label\n hitbox tier; now applied at the recent-signal\n row tier. data-recent-row-tint-transition attr\n exposes the cadence for tests.\n Geometry/paint logic unchanged — purely the\n transition timing. */}\n <rect\n x=\"6\" y={38 + index * 16 - 10}\n width=\"218\" height=\"14\" rx=\"3\"\n fill={isRowActive ? pal.legendAccent : 'transparent'}\n opacity={isRowPinned ? (isLight ? 0.18 : 0.22)\n : isRowHovered ? (isLight ? 0.10 : 0.14)\n : 1}\n data-recent-row-tint={link.key}\n data-recent-row-tint-transition=\"200ms\"\n style={{ transition: 'fill 200ms ease-out, opacity 200ms ease-out' }}\n />\n {/* Round 160 / Loop: recency pip. Canvas flow edges\n fade by freshness (R10: full intensity ≤30s →\n ~35% over 5min). The recent-signal panel rows\n duplicate that data (alias→alias · N · 5s)\n but encode freshness purely in text — no\n at-a-glance visual cue for \"which row is\n actively firing right now\". A 1.6-px cyan dot\n at x=10 (in the 7-px margin between rect-\n start x=6 and text-start x=13) brightens\n fresh rows and dims stale ones — same\n vocabulary the canvas uses, brought to the\n panel side.\n\n Three encodings now coexist on each row,\n none competing:\n rect fill = hover/pin state (R104/R116)\n count tspan = magnitude (R127 amber when ≥10)\n pip = recency (this round)\n\n Geometry: cy = row_y - 3 (mid-row vertical\n centre, where text baseline at y=row_y sits\n slightly below). r=1.6 fits cleanly in the\n 7-px left margin. pointerEvents:none so the\n row's button-role hit area is unchanged.\n No overlap-test impact (entirely within the\n existing rect bbox). */}\n {(() => {\n if (!link.last_at) return null;\n const ageSec = Math.max(0, (Date.now() - Date.parse(link.last_at)) / 1000);\n // 0-30s: fully fresh (1.0). 30-300s: smooth\n // decay 1→0.25. >300s: stale floor (0.25).\n const alpha = ageSec <= 30\n ? 1\n : ageSec <= 300\n ? 1 - ((ageSec - 30) / 270) * 0.70 /* R358: floor 0.25 → 0.30 lift across 3 freshness scopes */\n : 0.30; /* R358: stale floor lifted 0.25 → 0.30 — 20% legibility bump while preserving fresh/stale ratio */\n return (\n <circle\n cx={10}\n cy={38 + index * 16 - 3}\n /* Round 359 / Loop: recency pip base radius\n 1.6 → 1.8. Sibling lift to R358's freshness-\n floor bump (alpha 0.25 → 0.30) — pre-R358/\n R359 the stale pip painted at r=1.6 + α=0.25\n which read as near-invisible chrome. R358\n gave it more alpha; R359 gives it more area\n (1.8² / 1.6² ≈ 1.27, so ~27 % more glyph)\n so the pip stays distinguishable across the\n freshness ramp. Geometry: 1.8-radius dot\n centred at (10, row_y - 3) is bbox 3.6×3.6,\n still well inside the 7-px left margin\n (x=6 rect-start → x=13 text-start) the R160\n pip was placed in. Overlap-test reads the\n parent row rect's bbox, not this pip's, so\n grid+ring invariants hold. Matches the same\n 1.6 → 1.8 visual-weight bump R295 applied\n to the legend swatch (5.5 → 6 base radius)\n and R287 to the minimap viewport stroke\n (1 → 1.5). data-recent-row-freshness-radius\n attr exposes the value for tests. */\n /* Round 383 / Loop: recency pip base radius\n 1.8 → 2.0. Continues the R359 lift\n trajectory — pip area grows ~23 % (π·2²/\n π·1.8² ≈ 1.23) for a clearer at-a-glance\n freshness anchor in each row. Bbox 4.0×4.0\n still inside the 7-px R160 left margin\n (3-px remaining clearance vs 3.4 at r=1.8\n — geometry-safe margin holds). Sibling\n visual-weight bump family (9th anchor now):\n R287 minimap viewport stroke 1 → 1.5\n R295 legend swatch base radius 5.5 → 6\n R359 recent-row pip base radius 1.6 → 1.8\n R360 hub digit fontSize 11 → 12\n R361 edge-badge digit fontSize 10 → 11\n R365 hub-highlight base radius 5 → 5.5\n R367 edge-badge rest stroke 1 → 1.25\n R374 pressure-bar height 1.5 → 2\n R383 recent-row pip radius 1.8 → 2.0 (this round)\n data-recent-row-freshness-radius attr\n bumps to '2.0' for tests. */\n /* Round 447 / Loop: recent-row freshness pip\n radius lift on (isRowHovered || isRowPinned)\n — r 2.0 → 2.5 (+0.5px, sibling to R442\n endpoint-ring r lift). Adds a geometric\n axis to the recent-row hover/pin gesture\n alongside R143 translateY + R104 row bg-\n tint + R434 letter-spacing + R445 count\n fw. Pre-R447 the pip stayed at r=2.0 always\n — the freshness alpha (R162) tracked\n recency but didn't telegraph \"this row is\n in focus\" geometrically. R447 lifts the\n pip outward by 25% area (π·2.5² / π·2.0²\n = 1.56) on attention, closing a 5-axis\n row-attention signature (geometry + paint\n + typography + spacing + position).\n SVG `r` as CSS property for interpolation\n (R197/R198 idiom). transition list extends\n to include 'r 200ms ease-out' matching the\n opacity cadence. data-recent-row-freshness-\n lifted attr exposes the gate for tests. */\n /* Round 478 / Loop — extend the R476/R477\n drop-shadow vocabulary to a third anchor:\n the recent-row freshness pip on `alpha\n > 0.7` (just-fired flow within ~30s per\n R10 freshness ramp). Gate is FRESHNESS-\n driven not pin/hover-driven, so the glow\n reads as \"this signal is live\" rather\n than \"user is inspecting\". As the alpha\n decays past 0.7 (≈45s after last fire),\n the glow eases off — natural breathing\n feel that tracks actual data freshness.\n Hue: pal.legendAccent at 0.5 alpha so\n the glow inherits the row's accent color\n family. 2.5-3px blur reads as soft\n radiance, not loud bloom.\n Drop-shadow visual-polish family now 3\n anchors:\n R476 hub digit hover-gated\n R477 legend pin-ring pin-gated\n R478 recent freshness freshness-gated\n Each anchor uses a different state gate\n but the same `filter: drop-shadow` paint\n vocabulary. Filter affects paint only —\n bbox unchanged, overlap-test invariants\n hold. Transition list extends to include\n 'filter 200ms ease-out' alongside\n R10/R447 opacity + r tweens. */\n fill={pal.legendAccent}\n opacity={alpha}\n data-recent-row-freshness={link.key}\n data-recent-row-freshness-alpha={alpha.toFixed(2)}\n data-recent-row-freshness-radius={(isRowHovered || isRowPinned) ? 2.5 : 2.0}\n data-recent-row-freshness-lifted={(isRowHovered || isRowPinned) ? 'true' : 'false'}\n data-recent-row-freshness-glow={alpha > 0.7 ? 'true' : 'false'}\n style={{\n pointerEvents: 'none',\n r: `${(isRowHovered || isRowPinned) ? 2.5 : 2.0}px`,\n filter: alpha > 0.7\n ? `drop-shadow(0 0 3px ${pal.legendAccent}80)`\n : undefined,\n transition: 'opacity 200ms ease-out, r 200ms ease-out, filter 200ms ease-out',\n } as React.CSSProperties}\n />\n );\n })()}\n {/* Round 220 / Loop · milestone: recent-signal row\n text completes the pin-signature typography\n triple (R218 group labels / R219 legend rows /\n R220 recent-signal rows). All three label-based\n interactive surfaces now read \"locked in\" at\n the type level when pinned — letter-spacing\n spreads 0px → 0.5px on isRowPinned (NOT on\n hover — hover keeps default tracking so the\n eye can discriminate transient preview from\n sticky pin without checking chrome). Pin\n signature vocabulary now consistent across\n the entire interactive-label landscape of\n TopoGraph: every pin-able text element has\n a typography-level tell.\n transition extends 'letter-spacing 150ms'\n alongside R55 fill 150ms — same beat as\n R219 legend-row treatment. Hover still keeps\n its own R55 fill brighten exclusively;\n letter-spacing is pin-exclusive (note the\n isRowPinned not isRowActive gate). */}\n <text\n x=\"13\" y={38 + index * 16}\n fill={isRowActive ? pal.legendHeadline : pal.legendText}\n fontSize=\"9\"\n fontFamily=\"monospace\"\n /* Round 363 / Loop: recent-row text fontWeight 400\n → 500 (font-medium tier). At fontSize=9 the\n default-weight 400 glyphs read thin against the\n panel chrome (pal.legendBox.fill with 0.92/0.97\n opacity); the 100-weight bump lifts the alias→\n alias text into the legibility band without\n changing geometry. The R320 count tspan fw=600\n (cold) / fw=700 (hot) override still wins\n locally via inline fontWeight on the inner\n tspan, so the count-vs-alias hierarchy stays\n intact:\n alias fw 500 (R363, this round)\n count fw 600/700 (R320)\n Sibling typography lift to R362 chip-row digit\n 500 → 600 — both nudge a within-element data\n tier without disturbing the surrounding family\n baseline. data-recent-row-text-font-weight attr\n exposes the value for tests. */\n fontWeight=\"500\"\n data-recent-row-text={link.key}\n data-recent-row-text-pinned={isRowPinned ? 'true' : 'false'}\n data-recent-row-text-hovered={!isRowPinned && isRowHovered ? 'true' : 'false'}\n data-recent-row-text-font-weight=\"500\"\n /* Round 434 / Loop: recent-signal row text extends\n from R220's pin-only letter-spacing (0 → 0.5 on\n isRowPinned) to a 3-tier scale matching R433\n legend-row at the sibling panel-row scope:\n rest → 0px\n isRowHovered → 0.25px ← this round\n isRowPinned → 0.5px (R220 preserved)\n Pre-R434 R220 noted: \"Hover stays at default\n tracking — the spread is pin-exclusive so\n users can read pinned vs hovered at the text\n alone.\" R427-R433 established a 3-tier pattern\n across 4 surfaces (node-alias, edge-badge,\n group-label, legend-row) where hover gets a\n subtler intermediate kerning step distinct\n from the pin tier's stronger spread — the\n locked vs preview discrimination R220 wanted\n is preserved (0.5 > 0.25) AND hover gets a\n typographic axis of its own. R434 completes\n the 5-surface arc.\n 5-surface 3-tier letter-spacing pattern now\n spans every interactive label on TopoGraph\n that distinguishes hover from pin:\n node-alias (R427) 0 / 0.3 / 0.5\n edge-badge (R431) 0 / 0.2 / 0.4\n group-label (R432) 0 / 0.25 / 0.5\n legend-row (R433) 0 / 0.25 / 0.5\n recent-row (R434) 0 / 0.25 / 0.5 ← this round\n Hover-letter-spacing family extension\n (10 anchors now): R344/R345/R347/R351/R420/\n R427/R431/R432/R433/R434. R55 fill 150ms +\n R220 letter-spacing 150ms transition kept\n (additive conditional case, no new property). */\n /* Round 474 / Loop — cadence-sync follow-on to\n R472. R472 lifted the recent-row TINT RECT\n to 200ms but the row TEXT alongside still\n ran 150ms — same panel-row scope, two\n different rates. When a user hovered/pinned\n a row the rect background brightened in\n 200ms while the text fill + letter-spacing\n finished in 150ms. R474 closes that internal\n desync by lifting the text transitions to\n match. Whole recent-row state-flip now\n eases at 200ms ease-out across rect AND\n text. data-recent-row-text-transition='200ms'\n attr exposed for tests. R434 3-tier letter-\n spacing values unchanged; R363 fw + R55 fill\n brighten unchanged — only the timing axis\n shifts. */\n data-recent-row-text-transition=\"200ms\"\n style={{\n transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out',\n letterSpacing: isRowPinned ? '0.5px' :\n isRowHovered ? '0.25px' : '0px',\n }}\n >\n {/* R138 / Loop: typography unification with the rest\n of the topology UI. Filter pills (R119) render\n \"{from}→{to}\", node tooltips (R98) use →, the\n active-links chip tooltip (R114) and edge-badge\n titles all use unicode →. The recent-signal row\n was the lone holdout still rendering \"from -> to\"\n in ASCII. The data delimiter likewise: filter\n pills use \" · \" (\"status · 3\"); the row was using\n \" / \". Both swaps make the row read like every\n other surface — one less micro-style to remember. */}\n {truncate(link.from, 6)} {'→'} {truncate(link.to, 6)} {' · '}\n {/* Round 189 / Loop: count tspan unified — pre-R189\n two different tspans (data-recent-row-count vs\n data-recent-row-count-hot) mounted/unmounted\n on the R127 hot threshold (count >= 10),\n making fill (legendText ↔ amber) + fontWeight\n (regular ↔ 700) snap one-frame. Now one tspan\n always-mounted; isHot drives fill/fontWeight\n conditionally. style.transition='fill 300ms\n ease-out' makes the hot crossing ease through\n the colour shift — same vocabulary R188 just\n added to the edge midpoint badge stroke (the\n panel-side mirror of that surface). fontWeight\n stays binary (no clean weight interpolation\n across browsers). data-recent-row-count\n continues to expose the tspan to existing\n tests; data-recent-row-count-hot becomes\n an attribute on the same element when active\n so legacy probes still resolve. */}\n {/* Round 225 / Loop: tabular-nums on the per-row\n count digit. The row text reads \"alpha → beta ·\n {count} · content\"; when {count} grows from a\n single digit to two (9 → 10) the subsequent\n \" · {content}\" preview slides ~3-4px right in\n monospace because '1' and '0' have different\n natural widths against the surrounding control\n glyphs even in mono fonts. Tabular-nums locks\n the count column so the content preview\n column stays planted as activity scales up.\n Sibling treatment to R224 edge badge / R225\n hub digit / R225 panel-header flow-count. */}\n {/* Round 320 / Loop: cold-state per-row count gains\n explicit fontWeight=\"600\" instead of inheriting\n the parent <text>'s default (400). Brings the\n recent-signal row count into the 5-tier SVG\n data-weight family established by R309 (legend\n per-row count) / R310 (legend panel-header\n count) / R311 (recent-signal panel-header flow\n count). Pre-R320 the per-row count `· 12` for\n a cold row painted at fw=400, identical weight\n to the surrounding aliases — the count digit\n should read as data and stand out from the\n alias text. Hot crossing stays at fw=700 (R127),\n so cold→hot delta becomes 600→700 (still\n distinct, plus the fill flip from legendText\n → amber carries the dramatic part of the cue).\n Sibling treatment in the data-weight tier. */}\n {/* Round 445 / Loop: extend the R320 cold/hot fw\n binary (600/700) to ALSO fire on isRowPinned —\n pinned-cold now lifts to 700 alongside the\n existing hot-triggered lift. Sibling to R444\n group-label-count-pin (500→600) at the\n recent-row scope. Both panel-row counts now\n respond to pin with a typographic weight lift,\n part of the \"data tightens under attention\"\n family (R416/R424/R425/R426/R444/R445).\n Effective tiers:\n cold + un-pinned → fw 600\n cold + pinned → fw 700 ← this round\n hot (any pin state) → fw 700 (R320 preserved)\n hot is still amber-filled (R127); cold pin\n stays at the parent fill, so the two routes\n to fw=700 are visually distinct (color vs\n no color). transition list adds 'font-\n weight 200ms ease-out' so the lift eases\n under the same R320 fill cadence. data-\n recent-row-count-pinned attr exposes the\n pin gate for tests. */}\n <tspan\n fill={isHot ? hotStroke : undefined}\n fontWeight={(isHot || isRowPinned) ? '700' : '600'}\n data-recent-row-count\n data-recent-row-count-pinned={isRowPinned ? 'true' : 'false'}\n data-recent-row-count-font-weight={(isHot || isRowPinned) ? '700' : '600'}\n {...(isHot ? { 'data-recent-row-count-hot': 'true' } : {})}\n style={{\n transition: 'fill 300ms ease-out, font-weight 200ms ease-out',\n fontVariantNumeric: 'tabular-nums',\n }}\n >\n {link.count}\n </tspan>\n {/* Round 418 / Loop: recent-row content preview\n gains opacity=0.7 wrapper — subordinate-text\n tier at the SVG-text scope. Pre-R418 the\n truncated content preview (e.g. \" · hi there\")\n inherited the row's full opacity, reading at\n the same emphasis as the alias text and\n count digit. R418 wraps it in a <tspan> at\n opacity=0.7 so the preview reads as\n subordinate metadata — sibling to R333-R341/\n R362/R369/R389/R410/R412 chip-internal-\n hierarchy \"label tier\" (opacity-70) at the\n HTML scope, and R317 subordinate-text-lift\n gray-500 → gray-400 family. The leading\n \" · \" separator stays at full opacity so\n the row punctuation rhythm holds. data-\n recent-row-content-tspan attr surfaces the\n subordinate wrapper for tests. */}\n {' · '}\n <tspan opacity=\"0.7\" data-recent-row-content-tspan>{truncate(link.content, 8)}</tspan>\n </text>\n {/* Round 484 / Loop — recent-row timestamp opacity\n lifts to 1.0 when isRowHovered || isRowPinned,\n regardless of freshness alpha. R191 origin\n decays tsAlpha along with the row's freshness;\n pre-R484 hovering/pinning the row left the\n timestamp dim — user inspecting stale data\n fought the freshness encoding. R484 lifts to\n 1.0 on attention. Sibling to R472/R474 in the\n recent-row state-flip family. data-recent-row-\n ts-lifted attr exposes the gate; original\n data-recent-row-ts-alpha preserved as R191\n freshness reading. */}\n {lastAt ? (\n /* Round 321 / Loop: lastAt freshness timestamp picks\n up fontVariantNumeric tabular-nums. The string\n marches through 1s..59s (1 digit / 2 digits) /\n 1m..59m / 1h..24h every second the panel ticks,\n and the textAnchor=\"end\" right-aligns against\n x=217. Pre-R321 a 9s→10s crossing slid the chip\n left ~3px in monospace (digit '1' narrower than\n '0' even in mono) — same one-frame visible jitter\n R225 / R230 fixed elsewhere. Tabular-nums locks\n the digit slot so the timestamp stays planted as\n seconds tick. 7th surface in the info-density\n tabular-nums sweep after R224 edge badge / R225\n hub digit + panel header + recent row count /\n R229 group-label count / R230 group-label\n status pips / R320 recent-row count fw=600\n (count and timestamp now both lock). */\n <text\n x=\"217\" y={38 + index * 16}\n textAnchor=\"end\"\n fill={pal.legendText}\n fontSize=\"8\"\n fontFamily=\"monospace\"\n opacity={(isRowHovered || isRowPinned) ? 1 : tsAlpha}\n data-recent-row-ts={link.key}\n data-recent-row-ts-alpha={tsAlpha.toFixed(2)}\n data-recent-row-ts-lifted={(isRowHovered || isRowPinned) ? 'true' : 'false'}\n style={{\n pointerEvents: 'none',\n transition: 'opacity 200ms ease-out',\n fontVariantNumeric: 'tabular-nums',\n }}\n >\n {lastAt}\n </text>\n ) : null}\n </g>\n );\n })\n )}\n {/* Round 128 / Loop: overflow hint. The recent-signal panel\n renders the top 3 flowLinks via .slice(0, 3) — but a\n fleet with 5 or 10 active flows silently truncates the\n rest. The R96 \"X flows\" header tells the total but\n doesn't say \"you're seeing top-3\". This hint fires\n only when flowLinks.length > 3 so quiet fleets stay\n clean. Footer y=82 sits between row 3 (baseline 70)\n and the panel bottom; the R256 height bump 84→88\n adds 6 px of clear below the footer baseline so the\n on-hover textDecoration:underline (which renders\n ~3-4 px below baseline → y≈85-86) tucks INSIDE the\n panel border instead of clipping past it at the\n old 84-px floor. Overlap-test geometry unchanged\n (panel selector at translate(16,16); corner-to-\n center distance 342.6 → 340 still > 325 ring-clear\n threshold). fontStyle=italic + opacity 0.55 reads\n as muted metadata, not an actionable row — matches\n the R110 empty-state hint idiom. */}\n {(() => {\n // Round 221 / Loop: footer always-mounts; visibility\n // crossfades via wrapper <g> opacity instead of React\n // conditional mount/unmount on flowLinks.length > 3.\n // Pre-R221 a fleet's 4th flow appearing snap-popped the\n // footer in; tapering back to 3 flows snap-removed it.\n // Same threshold-crossing snap R215 closed for edge\n // midpoint badges, now applied to the recent-panel\n // footer surface. moreCount clamps at 0 so when invisible\n // (length ≤ 3) the data attribute and text don't show\n // garbage negative numbers. a11y trio (role / tabIndex /\n // aria-hidden) and pointerEvents follow visibility so\n // the hidden footer doesn't appear in tab order or\n // intercept clicks at its midpoint coordinates.\n const visible = flowLinks.length > 3;\n const moreCount = Math.max(0, flowLinks.length - 3);\n const label = `+ ${moreCount} more flow${moreCount === 1 ? '' : 's'}`;\n // R133: the truncation hint becomes a clickable nav to\n // /messages. R128 introduced the footer as pure metadata\n // (\"you're seeing top-3\"); R133 closes the gap by giving\n // users a way to ACT on that info — see the full list.\n // Wrap in <g> with onClick so SVG hit-testing fires the\n // route push. cursor:pointer + the underline-on-hover\n // visual cue tells users this is interactive. The hover\n // state is React-controlled (no CSS :hover on SVG <g>\n // descendant text would feel reliable across Chrome's\n // SVG quirks). pointerEvents follows visibility (R215\n // pattern) so the hidden footer can't intercept clicks\n // at its midpoint when it's invisible (sub-threshold\n // fleets).\n return (\n <g\n data-recent-panel-more-nav\n data-recent-panel-more-visible={visible ? 'true' : 'false'}\n role={visible ? 'link' : undefined}\n tabIndex={visible ? 0 : -1}\n aria-hidden={visible ? undefined : true}\n className=\"anet-topo-svg-focus\"\n style={{\n cursor: visible ? 'pointer' : undefined,\n pointerEvents: visible ? 'all' : 'none',\n opacity: visible ? 1 : 0,\n transition: 'opacity 300ms ease-out',\n }}\n onPointerDown={(e) => e.stopPropagation()}\n onMouseEnter={() => { if (visible) setHoveredRecentMore(true); }}\n onMouseLeave={() => setHoveredRecentMore(false)}\n onClick={() => { if (visible) router.push('/messages'); }}\n onKeyDown={(e) => {\n if (!visible) return;\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n router.push('/messages');\n }\n }}\n >\n <title>{`${label} — open /messages for the full list`}</title>\n {/* Round 195 / Loop: footer hint adopts the cyan\n vocabulary on hover, joining the interactive-on-\n hover-is-cyan family (R178 fullscreen / R163\n layout / R179 nodeSize / R193 active-links chip).\n Pre-R195 the footer brightened on hover via\n opacity + underline but kept its gray fill —\n 'becomes brighter gray' rather than 'becomes the\n go-act color'. legendAccent is the same cyan-400\n (dark) / teal-600 (light) every other interactive\n hover speaks. transition list extends `fill 200ms\n ease-out` so the colour swap eases instead of\n snapping. data-recent-panel-more-hovered exposes\n the gate for tests. */}\n {/* Round 259 / Loop: footer link bumps fontSize 8 → 9\n for clickable-text readability. Pre-R259 the\n \"+N more flows\" footer (the panel's primary\n navigation affordance into /messages) sat at the\n same 8pt as the empty-state hint — small enough\n to make the click target feel cheap. 9pt + italic\n + opacity 0.55 keeps it visually secondary to row\n content (9pt regular) while giving the link\n enough type-weight to read as a real affordance.\n Underline geometry verified: with fontSize 9 the\n underline still renders ~3-4px below baseline at\n y=82 → underline ~y=85-86, panel bottom y=88 →\n 2-3px clear (R256 footer-breath invariant\n preserved). */}\n {/* Round 325 / Loop: footer link joins the editorial\n letter-spacing family at 0.2px — same axis as\n R302 empty-state main hint (fontSize 10 opacity\n 0.65). The footer is italic monospace fontSize 9\n opacity 0.55 acting as the panel's primary\n navigation affordance into /messages; pre-R325\n it sat orphaned from the R285/R289/R301/R302/R304\n editorial-spacing axis even though it carried\n the same designed-label semantics (action label,\n not row data). 0.2px is the same value as R302\n because at fontSize 9 vs 10 the visual density\n of \"+ N more flows\" is close to \"no flow yet\"\n and they're both italic — sibling-equal in the\n hierarchy. The R195 cyan-hover + R259 fontSize\n bump stay; this round only adds the spacing.\n 6-axis editorial-letterspacing hierarchy now:\n R285 kicker: 1.2px (eyebrow loud)\n R289 watermark: 0.5px (wordmark brand)\n R301 panel titles: 0.3px (section headers)\n R302 empty main: 0.2px (empty-state hint)\n R325 footer link: 0.2px (panel nav action) ← NEW\n R304 empty sub: 0.15px (instructional sub) */}\n {/* Round 340 / Loop: +N more flows footer link extends\n the R333/R335/R336/R337/R338 chip-internal-hierarchy\n arc to a 6th surface. The digit `{moreCount}` reads\n as the primary data (\"how many more flows\"); the\n unit text ` more flow(s)` recedes via a nested\n tspan with opacity-0.7 (multiplicative against the\n parent <text>'s hover/rest opacity, so unit always\n sits below the digit). The `label` variable is\n preserved for the <title> tooltip — only the SVG\n render splits. data-recent-panel-more-unit exposes\n the unit tspan for tests. */}\n {/* Round 344 / Loop: footer hover gains letter-spacing\n tween 0.2 → 0.3. R325 set rest letter-spacing\n 0.2 to join the editorial-spacing family; R344\n adds a 0.1px hover spread that layers on top of\n R195 cyan fill + R325 spacing + R133 underline\n so the footer reads \"lit up and spaced\" on\n hover — sibling to R218/R219/R220 pin-signature\n letter-spacing family applied to a hover-only\n surface. transition list extends letter-spacing\n 200ms ease-out alongside the existing opacity/\n fill easings. */}\n {/* Round 368 / Loop: `+N more flows` footer text gains\n fontWeight=500 (font-medium tier). Sibling small-\n text fw lift family with R363 recent-row alias\n + R364 legend-row label + R366 group-label count\n — all four lifts share the same theory: at small\n fontSize (9-11 px) against panel chrome, SVG-\n default fw 400 sits at the legibility floor;\n fw 500 brings the glyph into the deliberate-data\n band. fontStyle=italic + opacity 0.55 rest + R325\n letterSpacing 0.2 baseline + R344 hover-spread\n 0.2 → 0.3 + R195 cyan fill on hover all preserved\n — the fw bump just thickens the italic stroke.\n Hover-state punch (R195 fill + R325 opacity 0.55\n → 0.85 + R344 letter-spacing + R133 underline)\n stays as is, so the rest-vs-hover delta still\n reads clearly. data-recent-panel-more-font-weight\n attr exposes the value for tests. */}\n <text\n x=\"115\" y=\"82\"\n textAnchor=\"middle\"\n fill={hoveredRecentMore ? pal.legendAccent : pal.legendText}\n fontSize=\"9\"\n fontFamily=\"monospace\"\n fontStyle=\"italic\"\n fontWeight=\"500\"\n letterSpacing={hoveredRecentMore ? '0.3' : '0.2'}\n opacity={hoveredRecentMore ? 0.85 : 0.55}\n textDecoration={hoveredRecentMore ? 'underline' : 'none'}\n data-recent-panel-more={moreCount}\n data-recent-panel-more-hovered={hoveredRecentMore ? 'true' : 'false'}\n data-recent-panel-more-font-weight=\"500\"\n style={{ transition: 'opacity 150ms ease-out, fill 200ms ease-out, letter-spacing 200ms ease-out' }}\n >\n {`+ ${moreCount}`}\n <tspan opacity=\"0.7\" data-recent-panel-more-unit>{` more flow${moreCount === 1 ? '' : 's'}`}</tspan>\n </text>\n </g>\n );\n })()}\n </g>\n )}\n\n {/* legend — Round 55 / Loop: each status row is now a hover\n target. Pointer enter sets `hoveredStatus`; pointer leave\n clears it. Node opacity formula composes the match below.\n The row text brightens to legendHeadline while hovered as\n a small affordance hint. Geometry unchanged — the new\n <g> wrappers only carry pointer handlers. */}\n <g\n transform=\"translate(760, 16)\"\n data-topo-panel=\"legend\"\n data-topo-panel-hovered={hoveredPanel === 'legend' ? 'true' : 'false'}\n // R175 / Loop: legend panel offset 100ms behind the\n // recent-signal panel so the two corner panels cascade\n // left-then-right rather than appearing in lockstep.\n // Same .anet-fade-in mechanism the four wave layers use.\n className=\"anet-fade-in\"\n data-topo-panel-fade-delay={800}\n style={{ animationDelay: '800ms' }}\n onMouseEnter={() => setHoveredPanel('legend')}\n onMouseLeave={() => setHoveredPanel(prev => prev === 'legend' ? null : prev)}\n >\n {/* R57: matching drop-shadow elevation to the legend panel.\n R106: panel height grew 96 → 104 to seat the new header\n line + 4 px row-shift below it (so the new header text\n doesn't overlap the row-1 hitbox region).\n R135: hover-elevation mirrors the recent-signal panel\n rect at line ~3299. Both panels grow their shadow on\n hover to telegraph \"the chrome is interactive\" since\n their rows pin / nav. */}\n {/* Round 247 / Loop: sibling treatment to the recent-signal\n panel — fill + stroke + opacity transitions added so\n the legend panel also eases through theme toggles\n (no snap on cyber↔light switch). Same 200ms cadence\n across the panel pair. */}\n {/* Round 331 / Loop: legend panel rect rx 8 → 10 — sibling\n treatment to the recent-signal panel above. Same\n proportional-rhythm step under R330's rounded-xl\n canvas wrapper envelope. */}\n <rect\n x=\"0\" y=\"0\" width=\"224\" height=\"88\" rx=\"10\"\n fill={pal.legendBox.fill}\n // R423 sibling — legend panel rect stroke tints to\n // legendAccent on hover (mirrors recent-signal panel\n // above). 4-layer hover cue stack now symmetric across\n // both side panels.\n stroke={hoveredPanel === 'legend' ? pal.legendAccent : pal.legendBox.stroke}\n // R348 sibling — legend panel rect opacity hover-state\n // bump 0.92 → 0.97 (cyber) / 0.97 → 1 (light) on\n // hoveredPanel === 'legend'. Pairs with the recent-signal\n // panel rect above so the two corner panels' hover cues\n // stay symmetric. Geometry-safe (paint-only).\n opacity={hoveredPanel === 'legend' ? (isLight ? 1 : 0.97) : (isLight ? 0.97 : 0.92)}\n style={{\n filter: hoveredPanel === 'legend'\n ? (isLight ? 'drop-shadow(0 4px 12px rgba(15,23,42,0.14))'\n : 'drop-shadow(0 4px 12px rgba(0,0,0,0.65))')\n : (isLight ? 'drop-shadow(0 2px 6px rgba(15,23,42,0.08))'\n : 'drop-shadow(0 2px 6px rgba(0,0,0,0.45))'),\n transition: 'filter 200ms ease-out, fill 200ms ease-out, stroke 200ms ease-out, opacity 200ms ease-out',\n }}\n data-topo-panel-elevation=\"legend\"\n />\n {/* R106 / Loop: panel header — symmetric with the recent-\n signal panel's \"recent signal · N flows\" (R96). Same\n font + position vocabulary so the two side panels feel\n paired. Title text at x=13 y=21; total fleet count\n right-aligned at x=215 y=21 in the accent colour. */}\n {/* Round 266 / Loop: legend panel title fill picks up theme-\n toggle transition — sibling treatment to the recent-\n signal panel title at line ~5459. Pre-R266 both panel\n titles hard-flipped color on theme toggle while their\n surrounding chrome eased; R266 closes both at once. */}\n {/* R301: sibling to recent-signal panel title above —\n same letterSpacing 0.3 for editorial parity. */}\n {/* Round 483 / Loop — sibling to R482 (recent-signal panel\n title): legend panel title fontWeight 700 → 800 on\n pinnedStatus (any legend row pinned propagates to the\n panel title). Pre-R483 the title responded only to\n panel-chrome hover via R345 ls; the pinnedStatus row\n highlighted its own swatch + tint via R181/R477 but\n the title stayed flat — no upstream tightening to\n signal \"panel context = inspecting\".\n R483 closes the symmetry with R482: both panel titles\n (recent-signal + legend) now tighten typographically\n when ANY row inside them is in the active filter\n state. Same idiom, mirrored at the legend-row scope.\n data tightens family — now 10 anchors:\n R416/R424/R425/R426 chip/panel/hub/edge digits\n R444/R445/R446 group/recent/legend counts\n R457 group-label parent\n R482 recent-panel title\n R483 legend-panel title (this round)\n transition list extends to include 'font-weight 200ms\n ease-out' alongside R345's ls + R55's fill 200ms.\n data-legend-panel-title-fw + -active exposed for tests. */}\n {/* R345 sibling — legend panel title same hover letter-\n spacing tween 0.3 → 0.4 on panel hover. */}\n <text x=\"13\" y=\"21\" fill={pal.legendHeadline} fontSize=\"12\" fontFamily=\"monospace\" fontWeight={pinnedStatus ? '800' : '700'} letterSpacing={hoveredPanel === 'legend' ? '0.4' : '0.3'} style={{ transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out, font-weight 200ms ease-out' }} data-legend-panel-title data-legend-panel-title-fw={pinnedStatus ? '800' : '700'} data-legend-panel-title-active={pinnedStatus ? 'true' : 'false'}>legend</text>\n {/* Round 257 / Loop: legend panel header count picks up the\n symmetric 13L/13R inner-padding pattern from the recent-\n signal panel. Pre-R257 the legend header was 13px from\n the left edge (`x=13` title) but only 9px from the right\n edge (`x=215` end-anchored count → 224-215=9), while the\n recent-signal panel header used 13px on BOTH sides (x=13\n title + x=217 end-count → 230-217=13). The two panels\n sit as a side-by-side corner pair — mismatched header\n inner-padding read as a typographic nit on the panel\n chrome. x=211 (= 224-13) restores symmetric 13L/13R so\n the panel pair shares one inset rhythm. The per-row\n count text at x=215 (line ~6321) STAYS at 9px-from-right\n — that one is paired with the flow-arrow swatch geometry\n ('M140,80 Q164,56 196,80') and would visibly tighten\n against the arrow tip if moved further left. Header\n count has no such pairing; it stands alone. */}\n {/* Round 266 / Loop: legend count fill (pal.legendAccent\n — cyber #67e8f9 cyan ↔ light #10b981 emerald) picks up\n theme-toggle transition. Pre-R266 the count snapped\n color on theme flip; R266 eases it alongside the panel\n title (sibling text in the same header band). */}\n {/* Round 292 / Loop: legend panel header count adopts explicit\n fontVariantNumeric: 'tabular-nums' for parity with the\n recent-signal panel header count at line ~5814 (R232).\n The text is already fontFamily='monospace' so digit width\n is technically tabular by definition — the explicit\n directive documents intent at code level, survives a\n future font-family change without silently losing\n tabular alignment, and eliminates an asymmetry between\n two sibling panel-header counts. Sibling treatment to\n R225 (hub digit) / R224 (edge badge) / R232 (chip-row\n counts) — tabular-nums sweep continues wherever digits\n live next to non-digit characters. */}\n {/* Round 310 / Loop: legend panel-header count picks up\n fontWeight=600 for parity with R309 per-row count\n weight. Pre-R310 the header count 'N nodes' rendered\n at default 400 while the per-row counts (working\n 'N' / idle 'N' / offline 'N') went semibold in R309.\n Same hierarchy reason as R309: the count is the DATA\n operators scan; the label ('legend' panel title +\n row labels 'working/idle/offline') is stable\n structural anchor. R309 established the rule at the\n row scope; R310 propagates it up to the panel-\n summary scope so the count typography is consistent\n across both the rollup and per-row counts inside\n the same legend panel. Existing pal.legendAccent\n fill + tabular-nums + R266 fill transition all\n preserved. */}\n {/* Round 336 / Loop: split legend panel count digit from\n unit \" nodes\" with nested opacity-0.7 tspan — sibling\n treatment to the recent-signal panel count and hot-\n count splits above. Three-panel-header surface family\n now sharing the same chip-internal-hierarchy pattern:\n recent flows count + \" flows\" unit at 0.7\n recent hot count + \" hot\" unit at 0.7\n legend nodes count + \" nodes\" unit at 0.7\n data-legend-panel-count-unit on the inner tspan for\n R336 introspection; the parent .textContent still\n reads \"{N} node(s)\" so existing R310 count tests via\n textContent unchanged. */}\n {/* R424 sibling — legend panel count digit fontWeight 600\n → 700 on panel hover. Closes 5-layer panel hover cue\n stack symmetric across both side panels (recent-signal\n + legend): depth (R135) + solidity (R348) + spacing\n (R345) + edge color (R423) + weight (R424). R310 base\n fw=600 + R292 tabular-nums + R266 fill transition + R336\n unit-tspan opacity-0.7 all preserved. Same \"data tightens\n under attention\" idiom R416 established at chip scope. */}\n <text\n x=\"211\" y=\"21\" textAnchor=\"end\"\n fill={pal.legendAccent} fontSize=\"10\" fontFamily=\"monospace\" fontWeight={hoveredPanel === 'legend' ? '700' : '600'}\n // R349 sibling — legend panel header count picks up\n // letterSpacing=\"0.2\", one tier below the R301 panel\n // title 0.3. Pairs with the recent-signal panel count\n // letter-spacing above so the two corner panels' header\n // typography stays editorially symmetric.\n letterSpacing=\"0.2\"\n data-legend-panel-count\n data-legend-panel-count-letter-spacing=\"0.2\"\n style={{\n transition: 'fill 200ms ease-out, font-weight 200ms ease-out',\n fontVariantNumeric: 'tabular-nums',\n }}\n >{sessions.length}<tspan opacity=\"0.7\" data-legend-panel-count-unit> node{sessions.length === 1 ? '' : 's'}</tspan></text>\n {(() => {\n const idleCount = onlineNodes.length - workingCount;\n // R106: rows shift +8 px (was y0=24, 48, 72 → 32, 56, 80)\n // to clear the new header row. R57 panel rect grew 96 →\n // 104 to seat them.\n const rows = [\n /* Round 277 / Loop: legend panel compress 104 → 88 (matches\n recent-signal panel height post-R256) per Vincent\n 5214/5215-5217 simplification ask. Row stride drops\n 24 → 18: row 1 working anchored at y0=32 (unchanged so\n R271 hitbox-swatch-center test at y=21 still passes);\n row 2 idle y0=56→50 (-6); row 3 offline y0=80→68 (-12).\n Flow-arrow swatch path (line ~6607) tracks new offline\n cy from y=80 to y=68. Net: legend panel takes ~15%\n less vertical chrome, panel pair (recent-signal+\n legend) now share same height = symmetric corner\n pair. Tests still pass: R257/R266/R269/R274 probe\n x attrs, fill transitions, text content — none\n sensitive to y0 stride. R271 probes working row\n hitbox y=21 (row.y0-11 with row.y0=32), unchanged.\n Corner-to-center distance increases (panel ends\n higher, further from center) — geometric ring-clear\n improves slightly. */\n /* Round 308 / Loop: continue the R307 legend label 减法.\n 'working node' (12 chars) → 'working' (7 chars):\n the 'node' qualifier is redundant — the row is in\n the LEGEND for a node graph, every row inherently\n describes a node state. 'online idle' (11 chars) →\n 'idle' (4 chars): the 'online' qualifier was\n disambiguation against the offline row, but the\n dashed-vs-solid status ring already discriminates\n online idle from offline visually. After R307+R308\n the three legend labels are all just status words:\n working / idle / offline — a clean 3-state list at\n roughly comparable lengths (7 / 4 / 7) for the\n tightest legend column to date. Pure 减法; visual\n information already encoded by row position +\n status-color swatch + status-ring dashing. */\n { key: 'working' as const, y0: 32, y1: 36, fill: isLight ? '#059669' : '#22c55e', label: 'working', count: workingCount },\n { key: 'idle' as const, y0: 50, y1: 54, fill: isLight ? '#0d9488' : '#2dd4bf', label: 'idle', count: idleCount },\n /* Round 269 / Loop: \" / \" → \" · \" delimiter unification.\n R138 swept the recent-signal row separators from\n ASCII \" / \" to typographic \" · \" (matching filter\n pills, node tooltips, edge badges, active-links\n tooltip). The legend's offline-row label was the\n LAST hardcoded \" / \" holdover in TopoGraph. Same\n monospace cell width (no layout shift), completes\n the R138 delimiter sweep.\n Round 307 / Loop: drop the ' · no SSE' qualifier.\n 'offline · no SSE' (16 chars) → 'offline' (7 chars).\n The visual already communicates the same idea\n redundantly: status ring strokeDasharray='5 5' for\n offline nodes (line ~5193) + gray fill + offline\n row's own gray swatch. Text qualifier was\n technical disambiguation that the visual encodes\n directly. Same R275-R281/R290/R291/R294 减法\n register — remove redundant text the eye doesn't\n need. Sibling row labels 'working node' (12 chars)\n + 'online idle' (11 chars) read at roughly the\n same length now too — legend rows look more\n balanced across the 3 lines. */\n { key: 'offline' as const, y0: 68, y1: 72, fill: isLight ? '#94a3b8' : '#6b7280', label: 'offline', count: offlineNodes.length },\n ];\n return rows;\n })().map(row => {\n // Round 61 / Loop: legend rows pin too — symmetric with the\n // R60 pressure-bar segments. R55 hover stays transient; the\n // new onClick toggles pinnedStatus so users can lock a\n // filter without holding the cursor still. Pinned row gets\n // an inset ring on the swatch (same vocab as R60).\n const isPinned = pinnedStatus === row.key;\n const isRowHovered = hoveredStatus === row.key;\n const isLifted = isRowHovered || isPinned;\n return (\n <g\n key={row.key}\n data-legend-status={row.key}\n data-legend-row-lifted={isLifted ? 'true' : 'false'}\n className=\"anet-topo-svg-focus\"\n role=\"button\"\n tabIndex={0}\n aria-pressed={isPinned}\n // R144: mirror of R143 — extend the row-lift idiom to the\n // legend panel rows for symmetry with the recent-signal\n // panel rows. Same translate-1px transform, same 150ms\n // cubic-bezier timing. R55 hovers the row (transient),\n // R61 pins it (sticky); both states earn the lift.\n // Composes with R105 row-bg-tint and R135 panel hover-\n // shadow → three layers of feedback at three nested\n // scopes (panel chrome → row text lift → bg tint).\n style={{\n cursor: 'pointer',\n transform: isLifted ? 'translateY(-1px)' : undefined,\n transition: 'transform 150ms cubic-bezier(0.4, 0, 0.2, 1)',\n }}\n // R61: stopPropagation on pointerdown so the SVG-level\n // pan handler (R103) doesn't setPointerCapture and\n // redirect the follow-up click away from this <g>.\n // Same trick the node <g> uses (and R52 hub).\n onPointerDown={(e) => e.stopPropagation()}\n onMouseEnter={() => setHoveredStatus(row.key)}\n onMouseLeave={() => setHoveredStatus(prev => prev === row.key ? null : prev)}\n onClick={() => setPinnedStatus(prev => prev === row.key ? null : row.key)}\n >\n {/* R149 / Loop: legend row gains a <title> tooltip\n symmetric with R148's recent-signal row tooltip.\n Same R97 idiom — anywhere the UI shows \"N\" should\n hover-explain WHICH N — applied to the legend\n panel which had been showing only the bucket\n count without alias context. Header line names\n the status bucket; body lists the matched aliases\n with 8-truncate + \"+N more\"; footer hint flips\n with pin state. Hot-lane convention doesn't apply\n here (status buckets aren't traffic counts), so\n no isHot suffix. */}\n <title>{(() => {\n const aliases = row.key === 'working'\n ? onlineNodes.filter(s => s.status === 'working').map(s => s.alias)\n : row.key === 'idle'\n ? onlineNodes.filter(s => s.status !== 'working').map(s => s.alias)\n : offlineNodes.map(s => s.alias);\n const preview = aliases.slice(0, 8).join(', ');\n const suffix = aliases.length > 8 ? ` + ${aliases.length - 8} more` : '';\n return [\n `${row.label} · ${row.count}`,\n aliases.length > 0 ? `${preview}${suffix}` : null,\n isPinned ? 'click to release pin (Esc to clear)' : 'click to pin · hover to preview',\n ].filter(Boolean).join('\\n');\n })()}</title>\n {/* R55 hitbox covers the row so cursor doesn't need to\n be exactly on the 5-px swatch. R105 / Loop: the\n hitbox now also carries a subtle hover/pin tint —\n mirroring R104's recent-signal row treatment so\n both side panels share the list-item idiom. The\n tint borrows the ROW'S OWN swatch colour rather\n than legendAccent, so the user's eye associates\n the tinted row with its colour swatch instead of\n a generic accent. Pin is a stronger tint than\n hover; idle stays fully transparent. */}\n {/* Round 271 / Loop: hitbox y shifts row.y0-12 → row.y0-11\n so hitbox center aligns exactly with swatch cy=row.y0.\n Pre-R271 hitbox spanned y=row.y0-12 to y=row.y0+10\n (center at row.y0-1), with swatch cy at row.y0 — 1px\n asymmetric. Hover/pin tint band drifted 1px above\n the swatch. Post-R271: hitbox spans y=row.y0-11 to\n y=row.y0+11 (center exactly at row.y0), swatch sits\n 11px from both edges (symmetric). Label text\n vertical center also benefits — label baseline\n y=row.y1=row.y0+4, visual midpoint ~row.y0+1.25,\n now ~1.25px below hitbox center (vs ~2.25px pre).\n No height change, no test ripple (other than this\n one), no R260/R268/R270 chrome regressions. */}\n {/* Round 473 / Loop — final cadence-sync follow-on,\n closing the legacy 150ms transition at the\n LEGEND-ROW tint scope. R459 (group-label hitbox)\n + R472 (recent-row hitbox) already lifted the\n two sibling panel-row hitboxes to 200ms; the\n legend-row was the last per-row tint still\n snapping at 150ms.\n After R473 the 200ms ease-out vocabulary is\n uniform across ALL three panel-row scopes —\n group-label, recent-signal, and legend — so\n hover/pin state-change cascades read coherently\n at every panel-tier surface. data-legend-row-\n tint-transition='200ms' attr exposed for tests.\n Geometry/paint unchanged. */}\n <rect\n x=\"6\" y={row.y0 - 11}\n width=\"170\" height=\"22\" rx=\"3\"\n fill={hoveredStatus === row.key || isPinned ? row.fill : 'transparent'}\n opacity={isPinned ? (isLight ? 0.14 : 0.18)\n : hoveredStatus === row.key ? (isLight ? 0.08 : 0.12)\n : 1}\n data-legend-row-tinted={isPinned ? 'pinned' : hoveredStatus === row.key ? 'hover' : 'none'}\n data-legend-row-tint-transition=\"200ms\"\n style={{ transition: 'fill 200ms ease-out, opacity 200ms ease-out' }}\n />\n {/* Round 197 / Loop: swatch dot scales r 5.5 → 7 when its\n row is hovered or pinned. Pre-R197 the swatch was a\n flat constant size — the row got R55/R61 fill brighten,\n R105 bg tint, R144 1px row lift, R181 pin ring, but\n the swatch dot at the row's *visual identity anchor*\n stayed still. Adding the size response gives the\n swatch its own click/hover feel and visually rhymes\n with R177 hub hover-lift (hub ring r 14→17). Stays\n well inside the r=8 R181 pin ring (7 + 0 stroke vs\n 8 - 0.75 inner ≈ 7.25) so the two layers don't fight.\n CSS r-as-property is interpolatable by Chrome/Safari/\n FF post-2020. Geometry-unchanged at rest, so the\n overlap test sees the same baseline. */}\n {/* Round 295 / Loop: legend swatch base radius 5.5 → 6.\n Pre-R295 the swatch idle radius was 5.5 and R197\n grew it to 7 on hover/pin — a 1.5px jump (~27%\n area). Bumping idle to 6 keeps hover at 7, so\n the hover delta becomes a smoother 1px (~36%\n area but 17% radius). Side effect: idle swatch\n reads slightly more like an authored color\n anchor than a faint dot — matches the post-R294\n 减法 register where the legend is one of the\n few remaining persistent canvas-side info\n surfaces. Geometry stays well inside the r=8\n R181 pin ring (6 + 0 stroke vs 8 - 0.75 inner\n ≈ 7.25). data-legend-swatch is unchanged so\n R197 / R55 / R61 tests probe the same handle. */}\n <circle\n cx=\"16\" cy={row.y0}\n r=\"6\"\n fill={row.fill}\n data-legend-swatch={row.key}\n data-legend-swatch-state={isPinned ? 'pinned' : isRowHovered ? 'hover' : 'idle'}\n style={{\n r: isRowHovered || isPinned ? '7px' : '6px',\n transition: 'r 150ms ease-out',\n } as React.CSSProperties}\n />\n {/* R61 pinned-state ring — concentric stroke at r=8 in\n the row colour, draws OUTSIDE the swatch so it\n doesn't fight the fill colour the user is matching.\n Round 181 / Loop: the ring used to mount/unmount\n with the conditional render, snapping on every\n pin/unpin. Now always mounted with opacity gated\n by isPinned + a 150ms transition so the ring\n eases in on pin and out on unpin — same gesture\n vocabulary the R165/R180 smooth-pin-mirror family\n uses for the chip-row pin chips. strokeWidth=1.5\n is the R51 overlap-test sentinel but the test\n selector is gated to g[data-node] ancestors,\n so this legend-internal circle is invisible to\n that probe. pointerEvents:none so the ring can't\n intercept the row click that produced it. */}\n {/* Round 402 / Loop: legend pin-ring strokeWidth 1.5\n → 1.75. Sibling visual-weight bump (12th anchor)\n to R385 hub hover-ring strokeWidth 1.5 → 1.75 —\n both are pin/hover state indicators painted as\n stroke-only circles outside their target swatch\n with the R51 sentinel value 1.5. R402 lifts to\n 1.75 (matching R385's choice) so the pin signal\n reads slightly heavier without crossing the\n R51 sentinel band (3 reserved for offline node).\n The R51 selector is gated to g[data-node]\n ancestors so this legend-internal circle (lives\n under a <g data-legend-status>) is invisible\n to the probe — same lesson R177/R385 documented.\n Visual-weight bump family (12 anchors now):\n R287 minimap viewport stroke 1 → 1.5\n R295 legend swatch radius 5.5 → 6\n R359 recent-row pip radius 1.6 → 1.8\n R360 hub digit fontSize 11 → 12\n R361 edge-badge digit fontSize 10 → 11\n R365 hub-highlight radius 5 → 5.5\n R367 edge-badge rest stroke 1 → 1.25\n R374 pressure-bar height 1.5 → 2\n R383 recent-row pip radius 1.8 → 2.0\n R384 minimap online dot 1.7 → 1.9\n R385 hub hover-ring stroke 1.5 → 1.75\n R402 legend pin-ring stroke 1.5 → 1.75 (this round)\n R181 always-mount opacity gate + 150ms transition\n + pointerEvents:none all preserved. data-legend-\n pin-ring-stroke-width attr exposes the value for\n tests. */}\n {/* Round 477 / Loop — legend pin-ring gains a filter:\n drop-shadow glow on isPinned. Extends R476's\n drop-shadow idiom from hub-digit (focal scope)\n to the legend-row pin-ring (sibling pin-state\n surface). When a status row is pinned, the\n concentric ring around the swatch now lights\n up with a colour-matched halo using row.fill,\n reinforcing \"this filter is locked\" via a\n glow layer above the R402 sw bump + R181\n opacity fade-in.\n Hue: row.fill at 0.55 alpha — picks up each\n status tier's signature colour (working green /\n idle teal / offline slate). 3px blur stays\n subtle but unmistakable when the row is locked.\n Reduced-motion users skip the filter via R29\n a11y blanket (transition-duration → 0.001ms\n so the glow appears/disappears instantly with\n pin toggle).\n Filter is paint-only — bbox unchanged, R51\n overlap-test gated to g[data-node] descendants\n so this legend-internal ring is invisible to\n the probe anyway. Transition list extends to\n include 'filter 200ms ease-out' so the glow\n eases under the same cadence as opacity. */}\n <circle\n cx=\"16\" cy={row.y0} r=\"8\"\n fill=\"none\"\n stroke={row.fill}\n strokeWidth=\"1.75\"\n opacity={isPinned ? 1 : 0}\n data-legend-pin-ring={row.key}\n data-legend-pin-ring-pinned={isPinned ? 'true' : 'false'}\n data-legend-pin-ring-stroke-width=\"1.75\"\n data-legend-pin-ring-glow={isPinned ? 'true' : 'false'}\n style={{\n pointerEvents: 'none',\n filter: isPinned\n ? `drop-shadow(0 0 3px ${row.fill}88)`\n : undefined,\n transition: 'opacity 150ms ease-out, filter 200ms ease-out',\n }}\n />\n {/* Round 219 / Loop: legend row text gains the same\n letter-spacing pin signature R218 added to group\n labels — 0px → 0.5px when isPinned. Pre-R219 the\n legend row's hover and pin states shared fill\n colour (R55/R61 both brighten to legendHeadline)\n so the text was typographically identical at the\n letter-form level for transient hover vs sticky\n pin. R181 pin ring + R197 swatch grow + R143 row\n lift differentiated the row chrome; R219 adds the\n text-level signature so the LABEL itself reads\n \"locked in\" at type. Mirror of R218's group label\n treatment — chrome-level + type-level pin\n vocabulary now unified across both interactive\n label surfaces (group + legend). transition adds\n `letter-spacing 150ms` alongside R55 fill 150ms;\n same ease pace, same beat. */}\n <text\n x=\"30\" y={row.y1}\n fill={hoveredStatus === row.key || isPinned ? pal.legendHeadline : pal.legendText}\n fontSize=\"11\"\n fontFamily=\"monospace\"\n /* Round 364 / Loop: legend-row label fontWeight 400\n → 500. Sibling typography lift to R363 recent-row\n text fw 400 → 500. Both surfaces render small\n monospace text against panel chrome at fontSize\n 9-11 where SVG-default fw 400 sits at the\n legibility floor. font-medium tier (500) gives\n the label a more deliberate-data register.\n The R309 per-row count text (separate element\n below at x=215 textAnchor=end) keeps its own\n fontWeight 600 inline override, so the count >\n label hierarchy stays intact at the legend\n scope same as R363 holds it at the recent-row\n scope:\n legend label fw 500 (R364, this round)\n legend count fw 600 (R309)\n recent alias fw 500 (R363)\n recent count fw 600/700 (R320)\n data-legend-row-label-font-weight attr exposes\n the value for tests. R219 letter-spacing pin\n tween + R55 fill transition + R181 always-mount\n pin ring all preserved. */\n fontWeight=\"500\"\n data-legend-row-label={row.key}\n data-legend-row-label-pinned={isPinned ? 'true' : 'false'}\n data-legend-row-label-hovered={!isPinned && hoveredStatus === row.key ? 'true' : 'false'}\n data-legend-row-label-font-weight=\"500\"\n /* Round 433 / Loop: legend-row text extends from\n R219's pin-only letter-spacing (0px → 0.5px on\n isPinned) to a 3-tier scale matching the R432\n group-label pattern:\n rest → 0px\n hoveredStatus → 0.25px ← this round\n isPinned → 0.5px (R219 preserved)\n Pre-R433 hover already brightened the fill\n (hoveredStatus===row.key || isPinned matches the\n legendHeadline branch) but the letter-form\n stayed dead-typographic on transient hover —\n only the pin tier carried a kerning signature.\n R433 adds the missing mid tier so hover\n telegraphs through BOTH fill brighten AND a\n subtle 0.25-px kerning spread, mirroring\n R427/R431/R432 at legend-row scope. Pin tier\n still wins so the locked vs preview distinction\n at the type level stays intact.\n Hover-letter-spacing family extension (9 anchors\n now): R344/R345/R347/R351/R420/R427/R431/R432/\n R433. 3-tier letter-spacing pattern now spans 4\n surfaces (node-alias R427, edge-badge R431,\n group-label R432, legend-row R433). R55 fill\n 150ms + R219 letter-spacing 150ms transition\n untouched — additive conditional case. */\n /* Round 475 / Loop — final closure of the panel-row\n text scope cadence-sync. R473 lifted the legend-\n row TINT RECT to 200ms; R474 lifted the recent-\n row TEXT to 200ms; R475 closes the matching\n legend-row text desync — fill + letter-spacing\n both 150 → 200ms ease-out. After R475 the 3-tier\n panel-row cadence family is fully 200ms across\n BOTH rect and text at every panel-row scope\n (group-label / recent-row / legend-row). Hover/\n pin state-flip at any panel-row tier reads as\n one motion-coherent unit. data-legend-row-\n label-transition='200ms' attr exposed for tests.\n R433 3-tier letter-spacing values (0/0.25/0.5)\n unchanged; R55 fill brighten unchanged — only\n the timing axis shifts. */\n data-legend-row-label-transition=\"200ms\"\n style={{\n transition: 'fill 200ms ease-out, letter-spacing 200ms ease-out',\n letterSpacing: isPinned ? '0.5px' :\n hoveredStatus === row.key ? '0.25px' : '0px',\n }}\n >{row.label}</text>\n {/* R95: live count anchored to the right edge of the\n panel (x=215, after the flow-arrow swatch). Same\n counts the chip-row shows (\"3 working\" etc.) but\n here next to the swatch the user is matching —\n saves crossing the canvas to the chip row for\n the number. text-anchor=end aligns the column\n visually like a table. pointerEvents:none so the\n count doesn't intercept the row hover hitbox.\n\n Round 204 / Loop: count text recedes when the\n tier is empty. Pre-R204 the \"0\" sat at the same\n opacity 0.65 as \"12\" — visually identical, so\n the eye got zero signal that a status tier was\n empty unless the operator read the digit. R204\n drops empty rows to 0.30 (dark) / 0.28 (light)\n so empty tiers fade into the panel chrome while\n populated tiers stay visually prominent. R204 a\n crossing zero / coming back from zero eases via\n the existing 150ms opacity transition. data-\n legend-count-empty exposes the binary signal\n for tests. */}\n {/* Round 239 / Loop: legend count text gains a tier-\n coloured fill on hover/pin, completing the hover-\n deepen-own-hue idiom at this surface. Pre-R239 the\n count digit's opacity bumped 0.65→0.95 on hover\n (R204 thinning) but its fill stayed at the neutral\n pal.legendText gray — same digit, brighter gray,\n no tier identity. R239 flips fill to row.fill\n (green/teal/slate per tier) when the row is\n hovered OR pinned, so the count lights up in its\n OWN colour, matching the swatch directly above it.\n The whole row now reads as one tier-coloured unit\n under cursor (swatch + label + count); R55/R197\n already do this for swatch + label, R239 closes\n the trio at the count. Opacity transition stays at\n 150ms; fill joins the same transition list at 150ms\n so the colour shift eases alongside the opacity\n ramp. data-legend-count-fill exposes the active\n fill state for tests; empty tiers (row.count===0)\n stay at pal.legendText regardless — empty doesn't\n get to claim tier identity. 8th surface in the\n hover-deepen-own-hue family. */}\n {/* Round 274 / Loop: legend per-row count picks up\n tabular-nums (sibling treatment to R225's recent-\n signal panel header flow-count + R230's group-\n label pip strip). The text uses fontFamily=\n 'monospace' which is typically tabular by\n nature, but some monospace implementations have\n subtle digit-pair width variance (e.g., '0' vs\n '1' at the visual boundary). Explicit\n fontVariantNumeric: 'tabular-nums' is belt-and-\n suspenders: locks digit widths regardless of\n the rendered monospace font, so the count\n column stays planted as offline/idle/working\n counters roll across 9→10 / 99→100 thresholds.\n Pure CSS-level addition, no layout shift.\n 10th surface in the info-density tabular-nums\n sweep family. */}\n {/* Round 309 / Loop: legend per-row count gains\n fontWeight=600 (semibold). The count is the\n DATA the operator scans (how many working /\n idle / offline nodes); the row label is the\n stable status word. Default weight (400) on\n both makes them visually equal — but a glance-\n first read pattern needs the digit to register\n faster than the label.\n fontWeight=600 gives the digit semibold\n emphasis (matching the h2 'Command mesh'\n semibold in the title block) while the row\n label stays at default 400/normal — a clean\n 'digit semibold > label regular' hierarchy\n that makes the count the optical anchor in\n each row. After R307+R308 simplified the\n labels to single status words, the count\n becomes proportionally more important; this\n round emphasizes that role typographically. */}\n {/* Round 446 / Loop: legend per-row count fontWeight\n lift 600 → 700 on isPinned. Mirror of R444 group-\n label-count + R445 recent-row-count at the\n legend-row scope. Closes the 3-panel-row family\n for the \"data tightens under attention\" pattern —\n every panel-row count now responds to pin with a\n typographic-weight bump:\n R444 group-label-count 500 → 600\n R445 recent-row-count 600 → 700 (cold-pin route)\n R446 legend-row-count 600 → 700 ← this round\n Hover gate (hoveredStatus===row.key) keeps rest\n fw=600 so the locked-vs-preview distinction at\n the type level stays intact — same gate R433 used\n on the parent <text> letter-spacing tween. R309\n fw=600 baseline + R204 empty-row opacity dim +\n R225 tabular-nums all preserved. transition list\n extends to include 'font-weight 150ms ease-out'\n matching R433 fill/letter-spacing cadence.\n data-legend-count-pinned + -font-weight attrs\n exposed for tests. */}\n <text\n x=\"215\" y={row.y1}\n textAnchor=\"end\"\n fill={row.count > 0 && (hoveredStatus === row.key || isPinned) ? row.fill : pal.legendText}\n fontSize=\"11\"\n fontFamily=\"monospace\"\n fontWeight={isPinned ? '700' : '600'}\n /* Round 449 / Loop: legend-row count active-state\n opacity 0.95 → 1.0 on (hoveredStatus===row.key\n || isPinned). Pre-R449 R204 lifted populated-row\n active opacity from rest 0.65 to 0.95 — visibly\n brighter but kept a 5 pct alpha gap (1 - 0.95).\n R449 closes the gap to 1.0 so the active count\n reads as confidently present alongside the R446\n fw=600→700 + R433 letter-spacing tween. Theme-\n consistency / canvas-presence family extension\n (7th anchor on the active-presence lift sub-\n family): R370 hub hover-ring 0.7→0.8, R371 edge-\n badge rest 0.82→0.85, R372 minimap offline-dot\n 0.5→0.6, R386 hub-highlight idle 0.9→0.95, R387\n hover-detail panel 0.94→0.97, R429 label-card\n body 0.94→1.0, R449 legend-count active 0.95→1.0\n ← this round. Empty-row opacity (R204: 0.28\n light / 0.30 cyber) and idle 0.65 rest both\n preserved. */\n opacity={row.count === 0\n ? (isLight ? 0.28 : 0.30)\n : (hoveredStatus === row.key || isPinned ? 1 : 0.65)}\n data-legend-count={row.key}\n data-legend-count-empty={row.count === 0 ? 'true' : 'false'}\n data-legend-count-pinned={isPinned ? 'true' : 'false'}\n data-legend-count-font-weight={isPinned ? '700' : '600'}\n data-legend-count-fill={row.count > 0 && (hoveredStatus === row.key || isPinned) ? 'tier' : 'neutral'}\n style={{ pointerEvents: 'none', transition: 'opacity 150ms ease-out, fill 150ms ease-out, font-weight 150ms ease-out', fontVariantNumeric: 'tabular-nums' }}\n >{row.count}</text>\n </g>\n );\n })}\n {/* Flow-arrow swatch tracks the offline row — R106 shifted\n rows down by 8 px to make space for the panel header so\n this moves from y=72 to y=80. Drop its pointerEvents so\n the offline legend row stays hoverable (R55). It's\n decoration, no need to receive events. */}\n {/* Round 254 / Loop: legend flow-arrow swatch stroke\n transition for theme toggle (cyber #67e8f9 ↔ light\n #10b981). Last theme-driven legend element snap. */}\n {/* Round 277 / Loop: flow-arrow path tracks new offline-row\n cy=68 after the legend panel compress (was 80 pre-R277).\n Endpoints follow the offline row to keep the swatch\n logically tied to the row it demonstrates; control point\n proportionally shifts so apex stays mid-arc between\n rows 2 and 3. */}\n <path d=\"M140,68 Q164,44 196,68\" fill=\"none\" stroke={pal.flowEdge} strokeWidth=\"3\" markerEnd=\"url(#topo-arrow)\" data-legend-flow-arrow style={{ pointerEvents: 'none', transition: 'stroke 200ms ease-out' }} />\n </g>\n\n {/* Round 282 / Loop: sleep2agi brand watermark per Vincent\n 5215 ask (relayed via 通信龙). Plain monospace text at\n canvas bottom-left (the only fully-empty corner — top\n corners hold recent-signal + legend panels, bottom-\n right holds the chrome strip). No icon yet — public/\n has only favicon.svg (small abstract network icon\n with hardcoded #0a0a1a dark bg that wouldn't blend on\n light theme) + intern_avatar.png (书生 brand-specific).\n Without a sleep2agi-specific crescent/lockup asset,\n R282 ships a low-opacity text-only mark; R283+ can\n swap in the real logo if Vincent provides the asset.\n\n Position: x=16 (matches the 16-unit SVG inset that the\n corner panels use); y=672 (≈12 px from viewBox bottom\n y=680, descender ≈ y=675, so the entire glyph sits\n clear of the bottom edge). Theme-aware fill:\n pal.legendText (cyber #94a3b8 slate-400 ↔ light\n #475569 slate-600). 0.4 opacity makes it a\n watermark — present but not visually loud. Pointer-\n events:none so it can't intercept clicks on the\n canvas backdrop.\n\n Note: the brand mark is INTENTIONALLY in a corner\n that no overlay/panel occupies, AND it's purely\n decorative additive after 7 rounds of 减法 (R275-\n R281). Adds 1 small text element back into the\n canvas — but Vincent specifically asked for it. */}\n {/* Round 289 / Loop: brand watermark picks up letterSpacing\n 0.5px. For a 9-character wordmark at fontSize 11 monospace,\n 0.5px between characters (8 gaps × 0.5 = 4px total\n widening) lifts \"sleep2agi\" from \"body text that happens\n to be a name\" to \"deliberate wordmark register\". Same\n R285-family idiom (kicker tracking-widest, title\n tracking-tight) applied to the brand mark — letter-\n spacing as typographic intent. Stays well inside the\n bottom-left corner; opacity 0.4 unchanged so the\n watermark stays a watermark. */}\n <text\n x=\"16\" y=\"672\"\n fontSize=\"11\" fontFamily=\"monospace\" fontWeight=\"600\"\n letterSpacing=\"0.5\"\n fill={pal.legendText}\n opacity=\"0.4\"\n data-topo-brand-watermark\n style={{ pointerEvents: 'none', transition: 'fill 200ms ease-out' }}\n >sleep2agi</text>\n {/* v0.10.0 Hero 3 Wave 1 / RFC §3.I (Vincent 5215 + 通信龙\n lead-autonomy Q4 dual-anchor minimal): canvas top-left\n crescent moon brand mark, visible ONLY when the\n recent-signal panel is hidden (composes with §3.C). The\n two never co-exist — when flowLinks.length > 0 the\n recent-signal panel occupies the (16,16) corner; when\n flowLinks.length === 0 the corner is empty and the\n brand crescent fills it. R310 title-block crescent\n remains the primary mark; this one is the secondary\n canvas-internal anchor (Q4 dual-anchor minimal).\n Inline path geometry identical to public/sleep2agi-\n logo.svg + the title-block SVG (mask = outer disc minus\n offset inner disc → crescent). Local mask id\n (`s2a-canvas-corner-mask`) prevents collision with the\n other inline crescents. opacity 0.35 (slightly more\n subtle than the bottom watermark's 0.4 since the\n canvas top-left has more contrast headroom). */}\n {/* Round 327 / Loop: canvas brand crescent joins the always-\n mount-opacity-gate family (R181/R182/R183/R213×2/R214/R215/\n R221/R222/R223). Pre-R327 the crescent conditionally\n mounted on `flowLinks.length === 0` — first flow arriving\n SNAP-removed it, last flow leaving SNAP-added it. The\n recent-signal panel at the same (16,16) corner has the\n same snap problem on its conditional-mount path; this\n round closes the crescent's snap-on-mount (the panel's\n own crossfade is a larger surface, deferred).\n\n Always-mounted with `opacity={flowLinks.length === 0 ?\n 0.35 : 0}` + 300ms ease-out transition: when the panel\n hides, the crescent fades in over 300ms; when the panel\n shows, the crescent fades out. Same opacity ramp time\n the R175 panel-fade-in uses for cascade rhythm. data-\n topo-brand-canvas-mark-visible exposes the gate for\n tests. */}\n <g\n opacity={flowLinks.length === 0 ? 0.35 : 0}\n data-topo-brand-canvas-mark\n data-topo-brand-canvas-mark-visible={flowLinks.length === 0 ? 'true' : 'false'}\n style={{ pointerEvents: 'none', transition: 'opacity 300ms ease-out, fill 200ms ease-out' }}\n >\n <defs>\n <mask id=\"s2a-canvas-corner-mask\">\n <rect x=\"0\" y=\"0\" width=\"28\" height=\"28\" fill=\"black\" />\n <circle cx=\"14\" cy=\"14\" r=\"12\" fill=\"white\" />\n <circle cx=\"17.5\" cy=\"13\" r=\"10\" fill=\"black\" />\n </mask>\n </defs>\n <rect\n x=\"16\" y=\"16\" width=\"28\" height=\"28\"\n fill={pal.legendText}\n mask=\"url(#s2a-canvas-corner-mask)\"\n />\n </g>\n </svg>\n\n {/* Round 30 / Loop: minimap. Big fleets in fullscreen mode at high\n zoom let users lose their position — the minimap shows the\n whole topology miniaturised plus a viewport rectangle so the\n user always knows where they are. Click anywhere to recenter\n the canvas there. Only mounted when the view is non-default\n (zoomed or panned) since at 1× centered the minimap and the\n canvas show the same thing. HTML overlay so it stays fixed\n while the SVG transforms. */}\n {(() => {\n const isDefaultView = Math.abs(view.zoom - 1) < 0.01 && Math.abs(view.x) < 1 && Math.abs(view.y) < 1;\n if (isDefaultView || (onlineNodes.length + offlineNodes.length) === 0) return null;\n const MW = 120, MH = 82;\n const sx = MW / VIEWBOX_W, sy = MH / VIEWBOX_H;\n const rectX = (-view.x / view.zoom) * sx;\n const rectY = (-view.y / view.zoom) * sy;\n const rectW = (VIEWBOX_W / view.zoom) * sx;\n const rectH = (VIEWBOX_H / view.zoom) * sy;\n return (\n <div\n /* Round 332 / Loop: minimap container rounded-md → rounded-lg\n (6 → 8 px) — continues the R330-R331 corner-radius cascade\n onto the minimap overlay card. The minimap is a smaller\n surface than the inner SVG panels (120×82 vs 230×88), so\n it sits one tier inward in the size hierarchy: panels at\n rx=10 (R331), minimap at rounded-lg=8 (R332), inner\n detail card at rx=8 (codex 8f981a9). Same 2 px gradient\n step the rest of the cascade uses. Geometry-safe — the\n minimap is an HTML overlay positioned `bottom: 56` +\n `right-4`, no impact on SVG layout or topo-overlap-test. */\n className=\"absolute right-4 rounded-lg border shadow-lg shadow-black/30 overflow-hidden anet-fade-in anet-topo-chip-focus\"\n /* Round 254 / Loop: minimap container theme transitions —\n background-color, border-color, color (used for SVG\n currentColor inside) all ease at 200ms alongside the\n R254 wrapper + R247 panel treatments. */\n style={{ bottom: 56, background: pal.legendBox.fill, borderColor: pal.containerBorder, cursor: 'crosshair', color: pal.legendAccent, transition: 'background-color 200ms ease-out, border-color 200ms ease-out, color 200ms ease-out' }}\n // R157: minimap a11y completion. Pre-R157 the element had\n // role=\"img\" + aria-label but no tabIndex / onKeyDown — it\n // was clickable for mouse users (recenter to where you\n // clicked) but tab-unreachable. role=\"img\" was also wrong\n // for an interactive surface; role=\"button\" matches the\n // canonical pattern R116 / R139 / R140 / R151 / R152 use.\n // Keyboard activation can't compute a click position, so\n // Enter / Space falls back to resetView() — same gesture\n // as the dedicated reset button (R104). Click semantics\n // unchanged; only added a clarifying tail to the aria-\n // label + title. anet-topo-chip-focus picks up R155's\n // cyan outline via color: pal.legendAccent inline so the\n // currentColor inherits cleanly on the rounded card.\n role=\"button\"\n tabIndex={0}\n aria-label=\"Topology minimap — click to recenter, Enter to reset view\"\n title=\"Minimap · click to recenter · Enter to reset view\"\n onClick={(e) => {\n const r = e.currentTarget.getBoundingClientRect();\n const fx = (e.clientX - r.left) / r.width;\n const fy = (e.clientY - r.top) / r.height;\n setView(prev => ({\n ...prev,\n x: VIEWBOX_W / 2 - fx * VIEWBOX_W * prev.zoom,\n y: VIEWBOX_H / 2 - fy * VIEWBOX_H * prev.zoom,\n }));\n }}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n resetView();\n }\n }}\n // R346: viewport rect hover affordance driven by parent.\n onMouseEnter={() => setHoveredMinimap(true)}\n onMouseLeave={() => setHoveredMinimap(false)}\n onFocus={() => setHoveredMinimap(true)}\n onBlur={() => setHoveredMinimap(false)}\n data-topo-minimap\n data-topo-minimap-hovered={hoveredMinimap ? 'true' : 'false'}\n >\n <svg width={MW} height={MH} viewBox={`0 0 ${MW} ${MH}`} style={{ display: 'block' }}>\n {/* Round 198 / Loop: minimap dots gain smooth status\n transitions. Pre-R198 a session flipping working→idle\n or going offline made the minimap dot snap in a single\n frame (opacity 0.9→0.5, r 1.7→1.2, fill swap). The\n canvas itself eases all of these via R167 status-ring\n transitions, R10 freshness, R3 fade-in — but the\n minimap mirror was still snap-cut. Adding opacity /\n fill / r to the CSS transition list lets a status\n change ripple smoothly through both views at the\n same rhythm. 200ms matches the R167 nodeStrokeWidth\n interpolation on the main canvas so the two surfaces\n visually flip in sync. r-as-property is well supported\n Chrome ≥ 95 / Safari ≥ 16 / FF ≥ 70 (same support\n matrix R197 just leveraged on the legend swatch).\n data-topo-minimap-dot exposes each dot for the test;\n data-topo-minimap-dot-online encodes the binary status\n used by the visible attributes. */}\n {[...onlineNodes, ...offlineNodes].map(s => {\n const p = nodePositions[s.alias];\n if (!p) return null;\n const sseN = (s.network_id ? sseSessions[`${s.network_id}:${s.alias}`] : undefined) ?? sseSessions[s.alias];\n const isOn = s.status !== 'offline' || !!sseN;\n const st = nodeStatus(s, isOn, isLight);\n return (\n /* Round 372 / Loop: minimap offline-dot opacity\n 0.5 → 0.6. Sibling stale-state legibility lift\n to R358 freshness ramp floor 0.25 → 0.30 + R317\n subordinate-text-lift family. Pre-R372 R198\n drew offline dots at α=0.5 (44 % below online\n 0.9). The minimap is a small overlay against\n the canvas backdrop — at α=0.5 offline dots\n sat at the legibility floor when the minimap\n mounted (only on non-default view). R372 lifts\n offline 0.5 → 0.6 for +20 % relative presence;\n online stays at 0.9 so the offline/online\n contrast ratio is now 0.6/0.9 ≈ 0.67 (vs prior\n 0.5/0.9 ≈ 0.56) — still a clear two-tier\n distinction. R198 opacity + fill + r transition\n list preserved so status flips still ease\n smoothly. data-topo-minimap-dot-opacity attr\n exposes the resolved value for tests. */\n <circle\n key={s.alias}\n cx={p.x * sx} cy={p.y * sy}\n /* Round 384 / Loop: minimap online dot radius 1.7\n → 1.9. Sibling visual-weight bump (10th anchor)\n to R383 recent-row pip 1.8 → 2.0. R198 designed\n the dots at 1.7 (online) / 1.2 (offline) — at\n the minimap's 120 × 82 scale these read clearly\n but the online ↔ offline contrast was modest\n (1.7/1.2 = 1.42×). R384 bumps online to 1.9 so\n the tier delta widens to 1.58× (1.9/1.2). Pair\n completes minimap-dot legibility polish:\n R358 (era R372) offline opacity 0.5 → 0.6\n R384 online radius 1.7 → 1.9 (this round)\n R198 transition list (opacity + fill + r 200ms)\n preserved so status flips still ease smoothly.\n data-topo-minimap-dot-radius attr exposes the\n resolved value for tests. */\n /* Round 392 / Loop: minimap online dot opacity\n 0.9 → 0.95. Theme-consistency / canvas-presence\n polish family (7th anchor) — mirrors R386's\n hub-highlight idle 0.9 → 0.95 lift on the\n minimap surface: the online-dot's idle alpha\n gap (0.10 against full presence) halves to\n 0.05, so the live-fleet anchors on the minimap\n read more confidently. Offline dot stays at\n R372 0.6 — the binary online/offline contrast\n ratio shifts from 0.6/0.9 ≈ 0.67 to 0.6/0.95\n ≈ 0.63, preserved as a clear two-tier\n distinction. R198 opacity + fill + r transition\n list + R384 r=1.9 + R372 offline 0.6 all\n preserved. data-topo-minimap-dot-opacity attr\n bumps to '0.95' for tests. */\n /* Round 421 / Loop: online dot opacity 0.95 → 1.0\n on minimap container hover. Sibling to R346\n viewport rect strokeWidth/opacity hover tween.\n When the user hovers the minimap container,\n the live-fleet anchors brighten from R392\n baseline (0.95) to full opacity in concert\n with the R346 viewport rect lift. Offline\n stays at R372 0.6 — hover state focuses\n attention on the ACTIVE anchors, not the\n stale ones. data-topo-minimap-dot-opacity\n attr (R392) reflects the resolved hover-\n state value for tests. */\n /* Round 486 / Loop — 3rd anchor in the\n inspection-overrides-encoding pattern. Sibling\n to R484 (recent-row timestamp) + R485 (edge\n particle). When the operator hovers a node\n alias on the main canvas, the matching\n minimap dot lifts to opacity=1.0 regardless\n of the binary online/offline encoding —\n cross-reference cue between canvas focal\n and the minimap wayfinding overlay.\n Pre-R486 an offline node's minimap dot stayed\n at 0.6 even when the operator was inspecting\n it via canvas hover; R486 makes the\n inspection signal jump the minimap dot to\n full presence so the spatial reference is\n unambiguous.\n Encoding survives: data-topo-minimap-dot-\n online preserves the online/offline binary,\n data-topo-minimap-dot-opacity-rest preserves\n the would-be opacity. Only the LIVE painted\n opacity flips on inspection.\n inspection-overrides-encoding family — 3\n anchors now:\n R484 recent-row timestamp\n R485 edge particle\n R486 minimap dot ← this round\n data-topo-minimap-dot-lifted attr exposes\n the override gate. */\n r={isOn ? 1.9 : 1.2}\n fill={st.primary}\n opacity={hoveredAlias === s.alias ? 1 : (isOn ? (hoveredMinimap ? 1 : 0.95) : 0.6)}\n data-topo-minimap-dot={s.alias}\n data-topo-minimap-dot-online={isOn ? 'true' : 'false'}\n data-topo-minimap-dot-opacity={hoveredAlias === s.alias ? 1 : (isOn ? (hoveredMinimap ? 1 : 0.95) : 0.6)}\n data-topo-minimap-dot-opacity-rest={isOn ? (hoveredMinimap ? 1 : 0.95) : 0.6}\n data-topo-minimap-dot-lifted={hoveredAlias === s.alias ? 'true' : 'false'}\n data-topo-minimap-dot-radius={isOn ? 1.9 : 1.2}\n style={{\n transition: 'opacity 200ms ease-out, fill 200ms ease-out, r 200ms ease-out',\n } as React.CSSProperties}\n />\n );\n })}\n {/* viewport rectangle.\n Round 199 / Loop: rect dimensions transition smoothly\n during R169 smoothView arming (discrete zoom button\n clicks + keyboard +/− + reset/fit). Pre-R199 the main\n canvas crossfaded over R168's 280ms opacity blend\n while the minimap viewport rect snap-cut to its new\n x/y/w/h in one frame — exactly the same rhythm\n mismatch R198 just closed for the minimap dots, now\n fixed for the rectangle that frames them.\n\n Gated to smoothView=true so continuous wheel-zoom /\n drag-pan stay snappy (a CSS transition during drag\n would cause the rect to chase the cursor with a\n 280ms lag). Discrete zooms arm smoothView for 350ms,\n long enough to cover the 280ms x/y/w/h transition.\n\n Setting x/y/width/height as CSS PROPERTIES (style.x\n etc.) — same approach R197 used for legend swatch\n r. Modern Chrome/Safari/FF interpolate these.\n\n data-topo-minimap-viewport / -smooth expose state for\n tests. */}\n {/* Round 287 / Loop: minimap viewport rect strokeWidth\n 1 → 1.5. The rect frames the user's current view\n within the full topology — it IS the wayfinding\n indicator. At 1px stroke against a 120×82 mini-\n canvas it was readable but reserved; 1.5px gives\n the boundary clearer presence without crowding the\n miniaturised dots (still r 1.2-1.7) inside.\n Same micro-polish family as R283 monogram stroke\n 1 → 1.5 — small visual-weight bump on a high-\n information element to lift it above ambient\n chrome. opacity 0.9 stays — strokeWidth alone\n does the lifting. */}\n {/* Round 379 / Loop: minimap viewport rect picks up\n strokeLinejoin='round'. Pre-R379 the rect's 4\n corners painted with default 'miter' joins —\n sharp 90° corners with a small miter overshoot\n (≈ strokeWidth × 1.4 = 2.1 px at sw=1.5). R379\n rounds the joins so corners arc smoothly through\n a quarter-circle of radius ≈ strokeWidth/2. At\n sw=1.5 that's a 0.75-px radius — subtle but\n matches the same stroke-softening vocabulary R288\n chrome icons (zoom/reset/fullscreen) and R378\n flow-rail already speak. Geometry-safe: stroke-\n linejoin only affects the corner overshoot, the\n rect's bbox is unchanged. R287 strokeWidth=1.5 +\n R346 hover-state strokeWidth/opacity bump + R199\n smoothView x/y/w/h transition all preserved.\n data-topo-minimap-viewport-linejoin attr exposes\n the value for tests. */}\n {/* Round 393 / Loop: minimap viewport rect rx 0 → 2.\n Pre-R393 the cyan-stroked viewport rect (the frame\n showing what's currently visible on the canvas)\n drew with sharp corners inside the R332 rounded\n minimap container (rx=8). A small frame with sharp\n corners sitting inside a rounded container reads\n as visually loud — the 90° corners catch the eye\n against the soft container edge. R393 adds rx=2\n so the viewport corners get a subtle radius that\n matches the family's softening idiom on a sub-\n element scale. The R379 strokeLinejoin='round'\n already softens stroke joins; R393 adds a complete\n geometric soften via rx.\n Corner-radius cascade (7 anchors now):\n R330 canvas rx 12\n R331 panels rx 10\n R332 minimap container rx 8\n R375 Layout-toggle rx 8\n R376 nodeSize/zoom rx 8\n R390 hover-detail rx 10\n R393 minimap viewport rx 2 (this round, sub-element)\n The 2-px radius is intentionally small — the\n viewport rect is typically only 30-50px wide,\n where rx=2 reads as \"rounded enough to not snap\"\n without feeling pillowy. data-topo-minimap-\n viewport-rx attr exposes the resolved value\n for tests. R346 hover-state tweens (strokeWidth\n + opacity) preserved verbatim. */}\n <rect\n x={Math.max(0, rectX)} y={Math.max(0, rectY)}\n width={Math.max(0, Math.min(MW - Math.max(0, rectX), rectW))}\n height={Math.max(0, Math.min(MH - Math.max(0, rectY), rectH))}\n rx=\"2\"\n fill=\"none\" stroke={pal.legendAccent}\n // R346: strokeWidth + opacity tween on container hover.\n strokeWidth={hoveredMinimap ? '1.75' : '1.5'}\n strokeLinejoin=\"round\"\n /* Round 450 / Loop · milestone: minimap viewport rest\n opacity 0.9 → 0.95. Closes half the alpha gap on\n the wayfinding indicator while preserving the\n R346 hover delta to 1.0. Pre-R450 the rest viewport\n sat at 0.9 (10 pct alpha gap) — adequate but\n under-confident for the user's primary \"you are\n here\" indicator on the minimap. R450 lifts to 0.95\n so the rest read is more present without erasing\n the hover lift cue (the +0.05 rest-to-hover delta\n is small but pairs with R346 sw 1.5→1.75 to keep\n hover clearly distinguishable). Sibling to R449\n legend-count active opacity 0.95→1.0 — same\n \"close the active-presence alpha gap\" idiom now\n applied to the REST tier of the wayfinding rect\n (the minimap viewport stays at canvas-presence\n register even when un-hovered since it's the\n spatial referent). Theme-consistency / canvas-\n presence family (8th anchor on the active-\n presence lift sub-arc).\n R287 strokeWidth=1.5 + R379 strokeLinejoin='round'\n + R346 hover-state tweens + R393 rx=2 + R199\n smoothView x/y/w/h transition all preserved. */\n opacity={hoveredMinimap ? '1' : '0.95'}\n data-topo-minimap-viewport\n data-topo-minimap-viewport-rx=\"2\"\n data-topo-minimap-viewport-smooth={smoothView ? 'true' : 'false'}\n data-topo-minimap-viewport-hover={hoveredMinimap ? 'true' : 'false'}\n data-topo-minimap-viewport-linejoin=\"round\"\n /* Round 481 / Loop — 6th anchor in the drop-shadow\n visual-polish family. New gate type: ZOOM STATE.\n When current canvas zoom > 1.5x (50% above the\n default 1.0 baseline), the minimap viewport rect\n gains a soft cyan halo signaling \"you're zoomed\n in beyond default\". The minimap viewport already\n shrinks as you zoom in (rectW = VIEWBOX_W /\n view.zoom * sx, so at zoom=2 it halves) — the\n glow tells you the wayfinding marker is now\n scaled-down rather than at canvas-default size.\n Drop-shadow family — 6 gate types covered:\n R476 hub digit hover-gated\n R477 legend pin-ring pin-gated\n R478 freshness pip freshness-gated\n R479 group label pin-gated\n R480 edge badge hot-lane-gated\n R481 minimap zoom-gated ← this round\n 6 distinct semantic gates (user interaction\n transient/sticky × 2, data freshness, data\n volume, canvas zoom state). Each anchor uses\n hue family appropriate to its semantic context.\n Hue: pal.legendAccent at 0x80 alpha — matches\n the existing R107 tint family and R478/R479\n cyan-tone choices. 2-px blur reads as subtle\n (the minimap viewport is small, ~120×82 px).\n Filter is paint-only — bbox unchanged. transition\n list extends to include 'filter 200ms ease-out'\n so the glow eases when zoom crosses 1.5x. */\n data-topo-minimap-viewport-glow={view.zoom > 1.5 ? 'true' : 'false'}\n style={{\n filter: view.zoom > 1.5\n ? `drop-shadow(0 0 2px ${pal.legendAccent}80)`\n : undefined,\n transition: smoothView\n ? 'x 280ms ease-out, y 280ms ease-out, width 280ms ease-out, height 280ms ease-out, stroke-width 200ms ease-out, opacity 200ms ease-out, filter 200ms ease-out'\n : 'stroke-width 200ms ease-out, opacity 200ms ease-out, filter 200ms ease-out',\n } as React.CSSProperties}\n />\n </svg>\n </div>\n );\n })()}\n {/* Round 103 (issue #81): zoom / pan / fullscreen controls — HTML\n overlay so they stay fixed while the SVG content transforms.\n Round 104: Vincent 实测 — the reset action used to be hidden\n behind the \"%\" label (looked like an indicator, not a button).\n Split into a plain % readout + an explicit reset button with\n its own icon + tooltip. */}\n {/* Round 261 / Loop: chrome strip bottom-3 right-3 (12 CSS px) →\n bottom-4 right-4 (16 CSS px) to align HTML overlay padding\n with the SVG corner panels at (16, 16) panel-translate. Pre-\n R261 the SVG panels (at 16 SVG units from canvas edges,\n ≈ 15 CSS px after render-scale ~0.94) and the HTML chrome\n (at 12 CSS px) sat at visually different distances from\n the canvas edges — small but real ~3 CSS px optical\n asymmetry between SVG-layer and HTML-layer overlay padding.\n 16 CSS px ≈ 17 SVG units, unifying the visual padding\n vocabulary across both layers. Sibling change at the\n minimap container (line ~6444, `absolute right-3` →\n `right-4`) keeps the bottom-right corner HTML overlays\n aligned at the same canvas-edge inset. */}\n {/* Round 326 / Loop: chrome strip outer wrapper gap 1.5 → 2\n (6px → 8px between control groups). Pre-R326 the four\n chrome groups (nodeSize segmented S/M/L, zoom +/100%/−,\n reset, fullscreen) sat 6px apart — close enough that on a\n busy canvas with bright cyan accents they read as one\n uniform strip rather than four distinct affordances. Bump\n to 8px gives each group its own visual breath without\n disturbing the bottom-4 right-4 corner-inset alignment.\n Sibling treatment to R298/R299 title-block gap polish on\n the top side of the canvas — both ends of the canvas\n chrome now breathe at the same 8px rhythm. Geometry-safe\n for the overlap-test (chrome is HTML overlay on top of\n the SVG, not part of the viewBox 1000x680 surface; ring\n r=325 / grid gx0 layout untouched). */}\n <div className=\"absolute bottom-4 right-4 flex items-center gap-2 text-xs select-none\" data-topo-chrome>\n {/* #113: node size — S / M / L segmented control (Vincent 4727).\n R154: stable data-* hooks for tests + focus-visible ring so\n keyboard navigation lands somewhere visible against the\n dark canvas (browser default outline often vanishes on\n cyber theme). */}\n {/* Round 264 / Loop: nodeSize wrapper gains theme-toggle\n transition. Pre-R264 the wrapper's bg (pal.legendBox.fill)\n + borderColor (pal.containerBorder) were inline theme-\n conditional, but neither inline transition nor a\n transition-colors className → wrapper snapped on cyber↔\n light flip while the inner S/M/L buttons eased via their\n own transition-colors. Same R254 holdover pattern that\n R263 just closed at the canvas wrapper scope, now at the\n chrome strip's nodeSize sub-wrapper scope. */}\n {/* Round 376 / Loop: nodeSize wrapper rounded-md → rounded-lg.\n Sibling polish to R375 Layout-toggle wrapper. Three\n chrome-strip segmented controls now all share rounded-lg\n at the wrapper tier:\n R375 Layout-toggle wrapper rounded-lg 8 px\n R376 nodeSize wrapper rounded-lg 8 px (this round)\n R376 zoom wrapper rounded-lg 8 px (this round)\n Individual atomic chrome buttons (reset, fullscreen) keep\n rounded-md (6 px) as their own atomic-button tier — the\n chrome strip's typography now expresses a clear two-tier\n hierarchy: 'segmented control container' (rounded-lg)\n vs 'standalone button' (rounded-md). Pure paint change,\n no layout shift. */}\n <div\n className=\"flex items-center rounded-lg border overflow-hidden\"\n data-topo-chrome-nodesize-radius=\"rounded-lg\"\n style={{\n background: pal.legendBox.fill,\n borderColor: pal.containerBorder,\n transition: 'background-color 200ms ease-out, border-color 200ms ease-out',\n }}\n role=\"group\"\n aria-label=\"Node size\"\n data-topo-chrome-fleet-group-trailer\n >\n {([['S', 0.7], ['M', 0.84], ['L', 1]] as const).map(([lbl, v], idx) => {\n const popKey = `size-${lbl}` as 'size-S' | 'size-M' | 'size-L';\n return (\n <button\n key={lbl}\n onClick={() => { popChrome(popKey); pickNodeScale(v); }}\n aria-pressed={nodeScale === v}\n data-topo-chrome-nodesize={lbl}\n data-topo-chrome-nodesize-active={nodeScale === v ? 'true' : 'false'}\n data-topo-chrome-nodesize-popping={chromePopping === popKey ? 'true' : 'false'}\n title={`Node size: ${lbl === 'S' ? 'small' : lbl === 'M' ? 'medium' : 'large'}`}\n // Round 179 / Loop: nodeSize S/M/L active-button hover\n // variant closes the inconsistency with R163 layout\n // toggle and R178 fullscreen. Pre-R179 the active\n // (selected) S/M/L button had bg-cyan-500/15 + text-\n // cyan-300 but NO hover response — the chip looked\n // 'locked', not 'still interactive'. R163 and R178\n // both add hover:bg-cyan-500/20 on the active variant\n // so the active chip stays responsive to mouse. R179\n // closes the trio so all three chrome active-cyan\n // surfaces ship the same gesture.\n // Round 196 / Loop: nodeSize buttons pick up press-state\n // (active:) — selected variant deepens to cyan-500/25,\n // unselected to white/10. Same tier pattern as R196 layout\n // toggle + zoom/reset/fullscreen below.\n // Round 250 / Loop: nodeSize buttons close the chrome-pop\n // family — every clickable chrome button now fires the\n // R186 .anet-chrome-pop scale-pulse on release. R171\n // canvas crossfade (nodeSizeSwitching) keeps masking the\n // node radius change at the global scope; R250 chrome-pop\n // adds the LOCAL button-level confirmation. The two\n // happen simultaneously without conflict — local scale\n // pulse on the button, global canvas dim around it.\n // Milestone round: the entire chrome strip (zoom -/+,\n // ring/grid, fullscreen, S/M/L) now speaks one\n // consistent click vocabulary.\n /* Round 270 / Loop: nodeSize INACTIVE buttons align with\n the Layout toggle's R163 hover-preview pattern. Pre-\n R270 inactive S/M/L used `hover:bg-white/5\n active:bg-white/10` (neutral white tint) while the\n Layout toggle's inactive Ring/Grid uses `hover:bg-\n cyan-500/5 active:bg-cyan-500/15` (faint cyan ghost\n that previews what the active state will look like —\n the active variant is bg-cyan-500/15). Two different\n hover vocabularies for visually-analogous toggle\n controls. R270 unifies inactive toggle hover to\n cyan so all TOGGLE chrome buttons (Layout / nodeSize\n / fullscreen) preview their active state on hover.\n Pure actions (zoom -/+, reset) stay white — they\n aren't toggles, have no active state to preview. */\n // Round 493 / Loop — extends R492 chrome-strip press-feedback\n // family to nodeSize S/M/L buttons. Adds active:scale-95\n // alongside the existing color-deepen (R196) + chrome-pop\n // (R249). transition-transform + duration-200 + ease-out\n // + transform-gpu added since this className previously had\n // transition-colors only — without the transform transition,\n // active:scale-95 would hard-cut. transform-gpu promotes the\n // layer so scale doesn't trigger paint thrash.\n className={`px-2 py-1 transition-colors transition-transform duration-200 ease-out transform-gpu active:scale-95 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset ${idx > 0 ? 'border-l' : ''} ${nodeScale === v ? 'bg-cyan-500/15 text-cyan-300 font-medium hover:bg-cyan-500/20 active:bg-cyan-500/25' : 'hover:bg-cyan-500/5 active:bg-cyan-500/15'}${chromePopping === popKey ? ' anet-chrome-pop' : ''}`}\n style={{ color: nodeScale === v ? undefined : pal.legendText, borderColor: pal.containerBorder }}\n >\n {lbl}\n </button>\n );\n })}\n </div>\n {/* Round 255 / Loop: semantic gap between the fleet-control group\n (node size S/M/L) and the view-control group (zoom / reset /\n fullscreen). Pre-R255 the four groups sat at uniform gap-1.5\n (6px); the spatial signal read as \"4 separate things\" instead\n of \"1 fleet control + 3 view controls\". Doubling the gap before\n the first view-control (ml-1.5 = 6px stacks on top of the\n parent's gap-1.5 = 6px, total 12px) communicates the semantic\n boundary through proximity alone — classic \"law of proximity\"\n layout polish, no extra chrome, no new visual elements.\n data-topo-chrome-view-group-leader marks the boundary surface\n for the test probe; data-topo-chrome-fleet-group-trailer marks\n the nodeSize wrapper's right edge for the gap measurement. */}\n {/* R376 sibling — zoom wrapper rounded-md → rounded-lg.\n Closes the chrome-strip segmented-control corner radius\n cascade (Layout R375 + nodeSize R376 + zoom R376). */}\n <div\n className=\"ml-1.5 flex items-center rounded-lg border overflow-hidden\"\n data-topo-chrome-zoom-wrapper-radius=\"rounded-lg\"\n style={{\n background: pal.legendBox.fill,\n borderColor: pal.containerBorder,\n transition: 'background-color 200ms ease-out, border-color 200ms ease-out',\n }}\n data-topo-chrome-view-group-leader\n data-topo-chrome-zoom-wrapper\n >\n <button\n onClick={() => { popChrome('zoom-out'); zoomByDiscrete(1 / 1.2); }}\n data-topo-chrome-zoom-out\n data-topo-chrome-zoom-out-popping={chromePopping === 'zoom-out' ? 'true' : 'false'}\n // R196: press-state deepens bg one tier above hover (white/5\n // → white/10) so mouse-down has a tactile dim before the\n // R186 icon pop fires on release.\n // R352: `group` lets the inner svg respond via group-hover.\n // R493 — zoom +/− buttons join the chrome-strip active:scale-95\n // press-feedback family (R492 + nodeSize above). transition-\n // transform + duration-200 + ease-out + transform-gpu added\n // since the className had only transition-colors.\n className=\"group px-2 py-1 hover:bg-white/5 active:bg-white/10 transition-colors transition-transform duration-200 ease-out transform-gpu active:scale-95 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset\"\n style={{ color: pal.legendText }}\n aria-label=\"Zoom out\"\n title=\"Zoom out (−)\"\n >\n {/* R186: icon pop on click. CSS animation runs once;\n React removes the class after 240ms so a quick\n re-click can replay. */}\n {/* Round 352 / Loop: zoom-out icon picks up group-hover:\n scale-110 — sibling to R350 reset hover-rotate. Pre-\n R352 hovering the zoom button only changed the bg\n (white/5); the icon inside stayed perfectly still.\n R352 lifts the icon 10% on hover for a tactile \"this\n button does something\" cue. The R186 anet-chrome-pop\n keyframe (220ms scale 1→1.06→1) still owns transform\n during click via CSS-animation precedence over\n transition-transform; after the pop ends + className\n is removed, the group-hover scale-110 picks up\n smoothly. `transform-gpu` hint promotes the svg to\n its own compositor layer for crisper edges during\n the scale tween. Sibling change on zoom-in icon\n below. */}\n {/* Round 454 / Loop: extend R453 chrome reset icon hover\n sw lift to zoom +/− icons via Tailwind arbitrary class\n group-hover:[stroke-width:2.8]. Chrome icon hover sw\n lift family now 5 anchors:\n R208 runtime badge outer ring 1.5 → 2\n R443 runtime badge inner icon 2.4 → 2.8\n R453 chrome reset icon 2.5 → 2.8\n R454 chrome zoom-out icon 2.5 → 2.8 ← this round\n R454 chrome zoom-in icon 2.5 → 2.8 ← this round\n Tailwind v4 arbitrary-value group-hover variant\n resolves [stroke-width:2.8] as a CSS property which\n overrides the static strokeWidth='2.5' attribute on\n hover. transition-[stroke-width] appended to the\n existing transition-transform list so the sw tween\n eases under the same 200ms cadence as R352 group-\n hover:scale-110. R186 anet-chrome-pop keyframe still\n owns transform during click via CSS-animation\n precedence over transition-transform. Sibling change\n on zoom-in icon below. */}\n <svg\n width=\"12\" height=\"12\" viewBox=\"0 0 24 24\"\n fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.5\" strokeLinecap=\"round\"\n aria-hidden\n className={`transition-[transform,stroke-width] duration-200 ease-out group-hover:scale-110 group-hover:[stroke-width:2.8] transform-gpu${chromePopping === 'zoom-out' ? ' anet-chrome-pop' : ''}`}\n data-topo-chrome-zoom-out-icon\n ><path d=\"M5 12h14\" /></svg>\n </button>\n {/* Round 192 / Loop: zoom-level readout span participates in the\n R186 click-feel pop alongside the +/− icons. Pre-R192 a click\n on + or − triggered:\n · icon pop (R186, ~220ms scale 1→1.06→1)\n · canvas crossfade (R169, ~280ms opacity blend)\n · readout text snap (instant — 100% → 120%)\n The readout was the only surface that didn't acknowledge the\n gesture. Reusing the existing .anet-chrome-pop CSS keyframe\n (no new keyframes) lets the \"%\" number gently bounce in\n sync with the icon — same 0.22s ease-out, transform-origin\n center. transform-box: fill-box on the keyframe is\n SVG-specific and harmlessly ignored on this HTML span. The\n base layout classes (px / border / tabular-nums / minWidth)\n stay intact; only when chromePopping is 'zoom-in' or\n 'zoom-out' do we splice in the animation class. Same\n React-clears-after-240ms cleanup R186 already runs, so the\n class can replay on a repeat click. */}\n {/* Round 312 / Loop: chrome strip zoom readout '{N}%'\n picks up `font-medium` (500). Extends the R309-R311\n 'data digit weighs more than label' rule to the\n chrome strip's one data display — the zoom\n percentage. Every other chrome strip text is a\n control (S/M/L buttons, zoom +/-, reset, fullscreen,\n Ring/Grid labels); the percent readout is the only\n live DATA. font-medium (not 600 like the SVG panel\n counts) is a tier below because the readout sits in\n HTML chrome context (lighter visual baseline) where\n 500 reads as 'noticeably data-prominent' without\n competing with the SVG panel counts. tabular-nums\n + minWidth 46 stay (R225 family), the existing R264\n color/border transitions stay, the R186 chrome-pop\n class still toggles on zoom-click. */}\n <span\n className={`px-2 py-1 tabular-nums font-medium border-x text-center${\n chromePopping === 'zoom-in' || chromePopping === 'zoom-out'\n ? ' anet-chrome-pop' : ''\n }`}\n data-topo-chrome-zoom-level\n data-topo-chrome-zoom-level-popping={\n chromePopping === 'zoom-in' || chromePopping === 'zoom-out'\n ? 'true' : 'false'\n }\n data-topo-chrome-zoom-level-hover={hoveredZoomLevel ? 'true' : 'false'}\n onMouseEnter={() => setHoveredZoomLevel(true)}\n onMouseLeave={() => setHoveredZoomLevel(false)}\n style={{\n color: pal.legendText,\n borderColor: pal.containerBorder,\n minWidth: 46,\n display: 'inline-block',\n // R347: letter-spacing hover tween — extends R344/R345\n // hover-letter-spacing family into the chrome strip.\n letterSpacing: hoveredZoomLevel ? '0.5px' : '0',\n // Round 420 / Loop: zoom-level readout gains a SECOND\n // hover axis — fontWeight 500 → 600 on hover. Sibling\n // to R347 (same element, hover letter-spacing tween).\n // The chrome strip's only data display now has a two-\n // axis hover signature (letter-spacing + fontWeight),\n // matching the R416 chip-row chip digit hover-bold\n // pattern at the chrome scope. Pre-R420 hovering only\n // spread the digits 0 → 0.5px; the weight stayed at\n // R332's 'font-medium' (500) baseline. Post-R420\n // hover lifts BOTH letter-spacing AND weight so the\n // percent reads with the same data-tier emphasis\n // intensification the chip-row chips do on hover.\n // Inline fontWeight overrides the className's\n // 'font-medium' since they target the same property.\n // 200ms transition list extends to font-weight for\n // smooth easing. data-topo-chrome-zoom-level-hover\n // attr surfaces the hover state for tests.\n fontWeight: hoveredZoomLevel ? 600 : 500,\n /* Round 264 / Loop: zoom level readout gains theme-toggle\n transition. The span has theme-driven color (pal.\n legendText) + border-x (pal.containerBorder via the\n inline borderColor) but className lacks transition-\n colors — the readout's text + side dividers snapped\n on theme flip while siblings eased. Sibling treatment\n to the nodeSize + zoom wrapper transitions added this\n round. */\n transition: 'color 200ms ease-out, border-color 200ms ease-out, letter-spacing 200ms ease-out, font-weight 200ms ease-out',\n }}\n title=\"Current zoom level\"\n >\n {Math.round(view.zoom * 100)}%\n </span>\n <button\n onClick={() => { popChrome('zoom-in'); zoomByDiscrete(1.2); }}\n data-topo-chrome-zoom-in\n data-topo-chrome-zoom-in-popping={chromePopping === 'zoom-in' ? 'true' : 'false'}\n // R196: press-state (mirror of zoom-out above).\n // R352: `group` lets the inner svg respond via group-hover.\n // R493 — zoom +/− buttons join the chrome-strip active:scale-95\n // press-feedback family (R492 + nodeSize above). transition-\n // transform + duration-200 + ease-out + transform-gpu added\n // since the className had only transition-colors.\n className=\"group px-2 py-1 hover:bg-white/5 active:bg-white/10 transition-colors transition-transform duration-200 ease-out transform-gpu active:scale-95 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset\"\n style={{ color: pal.legendText }}\n aria-label=\"Zoom in\"\n title=\"Zoom in (+)\"\n >\n {/* R186: icon pop on click. Same one-shot CSS animation\n as zoom-out; React removes the class after 240ms. */}\n {/* R352 sibling — zoom-in icon picks up the same\n group-hover:scale-110 family. Mirror change at\n the zoom-out icon above. */}\n {/* R454 sibling — zoom-in icon picks up the same\n group-hover:[stroke-width:2.8] family lift. */}\n <svg\n width=\"12\" height=\"12\" viewBox=\"0 0 24 24\"\n fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.5\" strokeLinecap=\"round\"\n aria-hidden\n className={`transition-[transform,stroke-width] duration-200 ease-out group-hover:scale-110 group-hover:[stroke-width:2.8] transform-gpu${chromePopping === 'zoom-in' ? ' anet-chrome-pop' : ''}`}\n data-topo-chrome-zoom-in-icon\n ><path d=\"M12 5v14M5 12h14\" /></svg>\n </button>\n </div>\n <button\n onClick={() => { armResetSpin(); resetView(); }}\n data-topo-chrome-reset\n data-topo-chrome-reset-spinning={resetSpinning ? 'true' : 'false'}\n data-topo-chrome-reset-hover={hoveredReset ? 'true' : 'false'}\n // R350: hover state drives the icon transform below.\n onMouseEnter={() => setHoveredReset(true)}\n onMouseLeave={() => setHoveredReset(false)}\n onFocus={() => setHoveredReset(true)}\n onBlur={() => setHoveredReset(false)}\n // R196: press-state deepens before R184 reset-spin fires on\n // release — mouse-down dim then 450ms spin = full handshake.\n /* Round 400 / Loop · milestone: chrome reset + fullscreen\n buttons gain hover:-translate-y-px lift — closes the\n hover-lift gesture vocabulary across every standalone\n interactive HTML element in TopoGraph. Segmented\n controls (zoom -/+, nodeSize S/M/L, Layout Ring/Grid)\n intentionally stay planted: lifting one segment of a\n unified strip would tear the visual unity of the\n segmented control. Only the standalone chrome buttons\n (reset, fullscreen) get the lift.\n Gesture vocabulary post-R400 (now complete across HTML):\n chip-row chips (3×) -1 px R398, R399\n filter pin pills (4×) -1 px R397\n recent-signal row -1 px R143\n legend row -1 px R144\n reset button -1 px R400 (this round)\n fullscreen button -1 px R400 (this round)\n Every standalone interactive HTML surface in TopoGraph\n now lifts on hover. data-topo-chrome-reset-hover-lift\n attr surfaces the lift for tests. */\n // R493 — reset button joins the chrome-strip active:scale-95\n // press-feedback family. The button already has transition-\n // transform + transform-gpu (R350 reset spin + R400 hover lift),\n // so just appending active:scale-95 plugs straight in. Compound\n // active state during press = hover-lift (-1px) + scale-95\n // composes as translateY(-1px) scale(0.95) — lift-and-compress\n // for tactile click feel.\n className=\"p-1.5 rounded-md border hover:bg-white/5 active:bg-white/10 hover:-translate-y-px active:scale-95 transition-colors transition-transform duration-200 ease-out transform-gpu focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60\"\n data-topo-chrome-reset-hover-lift=\"true\"\n style={{ background: pal.legendBox.fill, borderColor: pal.containerBorder, color: pal.legendText }}\n aria-label=\"Reset view\"\n title=\"Reset zoom + pan (0, or double-click the canvas)\"\n >\n {/* R184: the refresh-arrow icon does one counter-clockwise\n rotation on click. CSS animation runs once; React removes\n the className after 460ms (just past the 450ms duration)\n so a subsequent click can replay. */}\n {/* Round 288 / Loop: reset icon strokeWidth 2 → 2.5 unifies\n the chrome icon weight family. Pre-R288 zoom-in / zoom-\n out icons rendered at strokeWidth 2.5 while reset +\n fullscreen icons sat thinner at strokeWidth 2 — five\n chrome icons in a single horizontal strip with two\n weights is exactly the inconsistency R268 closed for\n border colors. Same unification idiom now applied to\n icon strokes: zoom (2.5) + reset (2.5) + fullscreen\n (2.5) all share one weight. View-box (24×24) and\n display size (13×13) unchanged, so geometry stays\n pixel-stable — only the stroke deepens. */}\n {/* Round 453 / Loop: chrome reset icon strokeWidth hover\n lift — 2.5 → 2.8 on hoveredReset && !resetSpinning.\n Sibling to R443 runtime badge inner-icon sw lift\n (2.4→2.8) — both chrome icons now thicken on hover\n for tactile feedback. Pre-R453 reset hover was a\n rotate-only cue (R350); R453 adds a stroke-weight\n axis so the affordance reads with both motion (R350\n rotate -8°) AND geometry (R453 sw +0.3). Gated on\n !resetSpinning so the R184 spin keyframe owns paint\n during its 450ms run. 200ms stroke-width transition\n appended to the style list matches R350 transform\n cadence. data-topo-chrome-reset-icon-stroke-width\n attr exposes the resolved value for tests. */}\n <svg\n width=\"13\" height=\"13\" viewBox=\"0 0 24 24\"\n fill=\"none\" stroke=\"currentColor\"\n strokeWidth={hoveredReset && !resetSpinning ? '2.8' : '2.5'}\n strokeLinecap=\"round\" strokeLinejoin=\"round\"\n aria-hidden\n className={resetSpinning ? 'anet-reset-spin' : undefined}\n data-topo-chrome-reset-icon\n data-topo-chrome-reset-icon-stroke-width={hoveredReset && !resetSpinning ? '2.8' : '2.5'}\n // R350: hover-rotate preview of the R184 click-spin.\n // Gated on !resetSpinning so the anet-reset-spin keyframe\n // owns transform during its 450ms run. transformOrigin\n // 'center' so rotation pivots around the icon's centre\n // (default would be top-left and the icon would arc).\n style={{\n transform: hoveredReset && !resetSpinning ? 'rotate(-8deg)' : 'rotate(0deg)',\n transformOrigin: 'center',\n transition: 'transform 200ms ease-out, stroke-width 200ms ease-out',\n }}\n data-topo-chrome-reset-icon-hover={hoveredReset && !resetSpinning ? 'true' : 'false'}\n >\n <path d=\"M3 12a9 9 0 1 0 9-9 9 9 0 0 0-6.4 2.6L3 8\" />\n <path d=\"M3 3v5h5\" />\n </svg>\n </button>\n {/* Round 178 / Loop: fullscreen chrome button picks up the\n active-state visual indicator R163 introduced for the\n Ring/Grid layout toggle. Pre-R178 the button changed\n icon when isFullscreen flipped but its background +\n foreground stayed unchanged — operators in fullscreen\n didn't get a strong 'you're in fullscreen' cue. Adding\n the bg-cyan-500/15 + text-cyan-300 active variant\n mirrors R163's pattern; hover variants tier 1\n brighter (cyan-500/20) when active so the chip\n continues to respond to mouse. Inline style now omits\n background + color when active so the Tailwind cyan\n classes win specificity. */}\n <button\n onClick={() => { popChrome('fullscreen'); toggleFullscreen(); }}\n data-topo-chrome-fullscreen\n data-topo-chrome-fullscreen-active={isFullscreen ? 'true' : 'false'}\n data-topo-chrome-fullscreen-popping={chromePopping === 'fullscreen' ? 'true' : 'false'}\n // R196: fullscreen also picks up press-state — active variant\n // deepens cyan-500/20 → cyan-500/25 on press; non-active\n // deepens white/5 → white/10.\n // R249: chrome-pop on click — same one-vocabulary click signal\n // as layout toggle and zoom buttons.\n /* Round 270 / Loop: fullscreen INACTIVE picks up the cyan\n hover-preview pattern from the Layout toggle. The\n fullscreen button is a TOGGLE (enter/exit fullscreen) so\n its inactive state benefits from the same \"hover previews\n active state\" idiom R163 designed. Sibling treatment to\n the nodeSize buttons at line ~6711. */\n // R353: `group` lets the inner svg respond via group-hover —\n // sibling to R352 zoom buttons. Closes the chrome-strip per-\n // icon hover-affordance arc (zoom-out / zoom-in / reset /\n // fullscreen now all carry an icon-level hover gesture in\n // addition to the bg hover).\n // R400: hover translateY(-1px) lift — see reset button above for family doc.\n // R493 — fullscreen joins active:scale-95 press family (same as\n // reset above: lift-and-compress compound transform on press).\n className={`group p-1.5 rounded-md border hover:-translate-y-px active:scale-95 transition-colors transition-transform duration-200 ease-out transform-gpu focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 ${\n isFullscreen\n ? 'bg-cyan-500/15 text-cyan-300 font-medium hover:bg-cyan-500/20 active:bg-cyan-500/25'\n : 'hover:bg-cyan-500/5 active:bg-cyan-500/15'\n }${chromePopping === 'fullscreen' ? ' anet-chrome-pop' : ''}`}\n data-topo-chrome-fullscreen-hover-lift=\"true\"\n style={{\n borderColor: pal.containerBorder,\n ...(isFullscreen\n ? {}\n : { background: pal.legendBox.fill, color: pal.legendText }),\n }}\n aria-label={isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}\n title={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}\n >\n {/* R288 / Loop: fullscreen enter + exit icons strokeWidth\n 2 → 2.5 — same chrome-icon weight unification described\n at the reset icon above. data-topo-chrome-fullscreen-\n icon attribute exposes BOTH variants (entered / exited)\n for the round's stroke-width regression probe. */}\n {/* Round 353 / Loop: fullscreen icon (both enter + exit\n variants) picks up the R352 family group-hover:scale-110.\n Pre-R353 hovering the button only changed the bg; the\n icon stayed still. R353 lifts the icon 10 % on hover —\n same gesture vocabulary as the zoom buttons. transform-\n gpu hint promotes the svg to its own compositor layer\n for crisper edges during the scale tween. Closes the\n chrome-strip per-icon hover-affordance arc. */}\n {/* R455 — fullscreen ENTER + EXIT icons pick up the same\n group-hover:[stroke-width:2.8] family lift as the\n zoom +/− icons (R454) and chrome reset icon (R453).\n Chrome icon hover sw lift family now 6 anchors —\n R208/R443 runtime badge + R453/R454-zoom-out/zoom-in\n + R455 fullscreen (this round). transition-[transform,\n stroke-width] expands existing transition-transform\n so the sw lift eases under R352 scale-110 cadence. */}\n {isFullscreen ? (\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden className=\"transition-[transform,stroke-width] duration-200 ease-out group-hover:scale-110 group-hover:[stroke-width:2.8] transform-gpu\" data-topo-chrome-fullscreen-icon=\"exit\">\n <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\" />\n </svg>\n ) : (\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden className=\"transition-[transform,stroke-width] duration-200 ease-out group-hover:scale-110 group-hover:[stroke-width:2.8] transform-gpu\" data-topo-chrome-fullscreen-icon=\"enter\">\n <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\" />\n </svg>\n )}\n </button>\n </div>\n\n {/* Issue #100/#106: draggable, resizable singleton chat popover.\n position:fixed so it floats above the page (overflow-hidden here\n doesn't clip fixed children). Rendered *inside* the container so\n that when the graph goes fullscreen (#81) the popover joins the\n fullscreen subtree and stays visible — a sibling render would be\n outside the fullscreened element and disappear. */}\n {chatAlias && (\n <ChatPopover alias={chatAlias} onClose={() => setChatAlias(null)} />\n )}\n </div>\n </section>\n );\n}\n","'use client';\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { AliasAvatar } from './AliasAvatar';\nimport { TaskChatPanel } from './TaskChatPanel';\nimport { useSessions } from '../lib/hooks';\nimport { relativeAgo } from '../lib/time';\n\ninterface ChatPopoverProps {\n /** Node alias to chat with. Changing it switches the conversation. */\n alias: string;\n onClose: () => void;\n}\n\nconst MAX_W = 380;\nconst MAX_H = 520;\nconst MIN_W = 300;\nconst MIN_H = 320;\nconst MARGIN = 16;\nconst MOBILE_BP = 640;\n\n/**\n * Issue #100: a floating, draggable chat window opened by clicking a node\n * avatar in the topology graph. Singleton — the parent (TopoGraph) keeps a\n * single `chatAlias`, so clicking another node just swaps `alias` here and\n * the conversation switches in place.\n *\n * Issue #106: a bottom-right handle resizes the window (drag to change w/h,\n * clamped to MIN_W/MIN_H .. the viewport). Coexists with the header move-drag\n * and the close button — each interaction lives on a distinct element and\n * stops its own pointerdown from reaching the others.\n *\n * One consistent design across viewports: a floating draggable card sized to\n * fit (`min(380, vw-32) × min(520, vh-96)`). On a phone it ends up near\n * full-width, so it's docked low on open — the graph above stays visible, and\n * to chat with a different node you drag it down / tap an exposed avatar.\n *\n * The chat body reuses TaskChatPanel's `inline` mode — send / SSE-receive /\n * history are already solved there; this component only adds the floating\n * shell + drag/resize behaviour.\n */\n/** Round 37 / Loop: surface node metadata (cwd + last-seen) inside the\n * ChatPopover header. The SVG <title> tooltip (Rounds 33-34) only shows\n * on hover-over-node, which is lost once the popover is open and\n * potentially dragged away. Lifting cwd and last-seen into the popover\n * header keeps the context where the user actually needs it.\n *\n * Round 38: relativeAgo factored to app/lib/time.ts so this file shares\n * the same TZ-safe parser as TopoGraph (was a duplicated mirror until\n * this round). */\n\nexport function ChatPopover({ alias, onClose }: ChatPopoverProps) {\n // Position + size are resolved on mount (SSR-safe defaults here).\n const [pos, setPos] = useState({ x: 0, y: 0 });\n const [size, setSize] = useState({ w: MAX_W, h: MAX_H });\n const dragRef = useRef<{ active: boolean; startX: number; startY: number; baseX: number; baseY: number }>({\n active: false, startX: 0, startY: 0, baseX: 0, baseY: 0,\n });\n const resizeRef = useRef<{ active: boolean; startX: number; startY: number; baseW: number; baseH: number }>({\n active: false, startX: 0, startY: 0, baseW: 0, baseH: 0,\n });\n\n // Round 37 / Loop: pull the target session out of the SWR cache that\n // TopoGraph already populates — same URL, so no extra network fetch.\n const { sessions } = useSessions();\n const session = useMemo(() => sessions.find(s => s.alias === alias), [sessions, alias]);\n const isOnline = !!session && session.status !== 'offline';\n const lastSeenLine = !isOnline ? relativeAgo(session?.last_seen_at) : null;\n\n const clamp = useCallback((x: number, y: number, w: number, h: number) => {\n const maxX = Math.max(MARGIN, window.innerWidth - w - MARGIN);\n const maxY = Math.max(MARGIN, window.innerHeight - h - MARGIN);\n return {\n x: Math.min(Math.max(MARGIN, x), maxX),\n y: Math.min(Math.max(MARGIN, y), maxY),\n };\n }, []);\n\n useEffect(() => {\n const place = () => {\n const vw = window.innerWidth;\n const vh = window.innerHeight;\n const w = Math.min(MAX_W, vw - MARGIN * 2);\n const h = Math.min(MAX_H, vh - MARGIN * 6);\n setSize({ w, h });\n const mobile = vw < MOBILE_BP;\n // Desktop: top-right of the graph. Mobile: docked low so the graph\n // above stays visible — switch nodes by tapping an exposed avatar.\n const x = mobile ? MARGIN : vw - w - 24;\n const y = mobile ? vh - h - MARGIN : 96;\n setPos(clamp(x, y, w, h));\n };\n place();\n // Keep the popover on-screen if the window is resized.\n window.addEventListener('resize', place);\n return () => window.removeEventListener('resize', place);\n }, [clamp]);\n\n // Esc closes — matches the rest of the dashboard's overlay convention.\n useEffect(() => {\n const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };\n window.addEventListener('keydown', onKey);\n return () => window.removeEventListener('keydown', onKey);\n }, [onClose]);\n\n const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {\n if (e.button !== 0) return;\n (e.currentTarget as Element).setPointerCapture?.(e.pointerId);\n dragRef.current = { active: true, startX: e.clientX, startY: e.clientY, baseX: pos.x, baseY: pos.y };\n };\n const onPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {\n const d = dragRef.current;\n if (!d.active) return;\n setPos(clamp(d.baseX + (e.clientX - d.startX), d.baseY + (e.clientY - d.startY), size.w, size.h));\n };\n const onPointerUp = (e: React.PointerEvent<HTMLDivElement>) => {\n if (!dragRef.current.active) return;\n dragRef.current.active = false;\n try { (e.currentTarget as Element).releasePointerCapture?.(e.pointerId); } catch {}\n };\n\n // Resize from the bottom-right handle. stopPropagation keeps the header\n // move-drag out of it; the popover is top-left anchored so growing it can\n // only push the bottom/right edges, which we clamp to the viewport.\n const onResizeDown = (e: React.PointerEvent<HTMLDivElement>) => {\n if (e.button !== 0) return;\n e.stopPropagation();\n (e.currentTarget as Element).setPointerCapture?.(e.pointerId);\n resizeRef.current = { active: true, startX: e.clientX, startY: e.clientY, baseW: size.w, baseH: size.h };\n };\n const onResizeMove = (e: React.PointerEvent<HTMLDivElement>) => {\n const r = resizeRef.current;\n if (!r.active) return;\n e.stopPropagation();\n const maxW = Math.max(MIN_W, window.innerWidth - pos.x - MARGIN);\n const maxH = Math.max(MIN_H, window.innerHeight - pos.y - MARGIN);\n setSize({\n w: Math.min(maxW, Math.max(MIN_W, r.baseW + (e.clientX - r.startX))),\n h: Math.min(maxH, Math.max(MIN_H, r.baseH + (e.clientY - r.startY))),\n });\n };\n const onResizeUp = (e: React.PointerEvent<HTMLDivElement>) => {\n if (!resizeRef.current.active) return;\n resizeRef.current.active = false;\n e.stopPropagation();\n try { (e.currentTarget as Element).releasePointerCapture?.(e.pointerId); } catch {}\n };\n\n return (\n <div\n className=\"fixed z-50 flex flex-col overflow-hidden rounded-xl border border-[var(--border)] bg-[var(--bg)] shadow-2xl shadow-black/60 anet-fade-in\"\n style={{ left: pos.x, top: pos.y, width: size.w, height: size.h }}\n role=\"dialog\"\n aria-label={`Chat with ${alias}`}\n >\n {/* Drag handle — the whole header bar moves the window. */}\n <div\n onPointerDown={onPointerDown}\n onPointerMove={onPointerMove}\n onPointerUp={onPointerUp}\n onPointerCancel={onPointerUp}\n className=\"flex items-center justify-between px-3 py-2.5 border-b border-[var(--border)] bg-[var(--bg-secondary)] rounded-t-xl cursor-grab active:cursor-grabbing select-none touch-none\"\n >\n <div className=\"flex items-center gap-2.5 min-w-0\">\n <AliasAvatar alias={alias} size={28} />\n <div className=\"min-w-0\">\n <div className=\"text-sm font-semibold text-[var(--fg)] truncate\">{alias}</div>\n {/* Round 37: cwd / last-seen lines surface the same metadata\n the SVG <title> tooltip carries (rounds 33-34), but inside\n the popover where it stays accessible after dragging the\n window away from the node. Fall back to the drag-hint\n when no metadata is reported. */}\n {session?.project_dir ? (\n <div className=\"text-[10px] text-[var(--fg-muted)] truncate font-mono\" title={session.project_dir} data-popover-cwd>\n cwd: {session.project_dir}\n </div>\n ) : null}\n {lastSeenLine ? (\n <div className=\"text-[10px] text-[var(--fg-muted)] truncate\" data-popover-lastseen>\n last seen: {lastSeenLine}\n </div>\n ) : null}\n {!session?.project_dir && !lastSeenLine ? (\n <div className=\"text-[10px] text-[var(--fg-muted)]\">Drag to move · Esc to close</div>\n ) : null}\n </div>\n </div>\n <button\n onClick={onClose}\n // Without this the header's drag handler captures the pointer and\n // Chromium retargets the click to the header — the button never fires.\n onPointerDown={(e) => e.stopPropagation()}\n aria-label=\"Close chat\"\n className=\"text-[var(--fg-muted)] hover:text-[var(--fg)] p-1.5 rounded-lg hover:bg-[var(--bg-elevated)] shrink-0\"\n >\n <svg className=\"w-4 h-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" strokeWidth={2}>\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M6 18L18 6M6 6l12 12\" />\n </svg>\n </button>\n </div>\n\n {/* Chat body — TaskChatPanel inline mode owns send / SSE / history. */}\n <div className=\"flex-1 min-h-0\">\n <TaskChatPanel alias={alias} onClose={onClose} inline />\n </div>\n\n {/* Issue #106: bottom-right resize handle. */}\n <div\n onPointerDown={onResizeDown}\n onPointerMove={onResizeMove}\n onPointerUp={onResizeUp}\n onPointerCancel={onResizeUp}\n aria-label=\"Resize chat\"\n className=\"absolute bottom-0 right-0 w-5 h-5 cursor-nwse-resize touch-none z-10\"\n style={{ touchAction: 'none' }}\n >\n <svg className=\"absolute bottom-0.5 right-0.5 w-3 h-3 text-[var(--fg-dim)]\" viewBox=\"0 0 12 12\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" aria-hidden>\n <path d=\"M11 5L5 11M11 9L9 11\" />\n </svg>\n </div>\n </div>\n );\n}\n","/** Issue #96: node visual identity — model vendor + runtime.\n *\n * Single source for two orthogonal node dimensions:\n * - vendor : which model house powers the node (avatar body)\n * - runtime: which execution shell it runs in (corner badge)\n *\n * Both are driven by the server `model` / `runtime` fields added in\n * commhub-server 0.8.1-preview.0 (#96 Phase 1, commit 08482ef). Old nodes\n * report `model: null` / `runtime: null` and fall back gracefully — never a\n * broken image or blank avatar.\n *\n * Adding a vendor = one entry in VENDOR_RULES. Dropping a real logo file into\n * public/vendors/ + setting `logo` lights it up with zero render-code change;\n * until then a vendor-tinted monogram stands in (not a fake official logo).\n */\n\nexport interface VendorIdentity {\n id: string;\n label: string;\n /** Monogram fallback colours, used when `logo` is null. */\n mono: { bg: string; ring: string; text: string };\n /** 1-char monogram shown when there's no logo asset. */\n initial: string;\n /** Packaged logo asset path, or null → render the monogram instead. */\n logo: string | null;\n}\n\nconst UNKNOWN_VENDOR: VendorIdentity = {\n id: 'unknown',\n label: 'Unknown vendor',\n mono: { bg: 'hsl(220 10% 26%)', ring: 'hsl(220 10% 46%)', text: 'hsl(220 14% 82%)' },\n initial: '?',\n logo: null,\n};\n\n// Ordered prefix rules — first match wins. `test` runs against a lowercased\n// model id. Keep the most specific prefixes first.\nconst VENDOR_RULES: Array<{ test: (m: string) => boolean; vendor: VendorIdentity }> = [\n {\n test: (m) => m.startsWith('intern'),\n vendor: {\n id: 'intern',\n label: '书生 · 上海 AI 实验室',\n mono: { bg: 'hsl(28 38% 24%)', ring: 'hsl(32 45% 52%)', text: 'hsl(34 60% 82%)' },\n initial: '书',\n // #79 shipped this asset — reuse it as the 书生 vendor logo.\n logo: '/intern_avatar.png',\n },\n },\n {\n test: (m) => m.startsWith('minimax'),\n vendor: {\n id: 'minimax',\n label: 'MiniMax',\n mono: { bg: 'hsl(18 50% 26%)', ring: 'hsl(18 65% 52%)', text: 'hsl(20 80% 82%)' },\n initial: 'M',\n // P0 (Vincent 5222) custom-designed vendor badge — NOT a copy of\n // the MiniMax trademark; geometric min/max zigzag in their warm-\n // red palette. Replaces plain-letter \"M\" fallback.\n logo: '/vendors/minimax.svg',\n },\n },\n {\n test: (m) => m.startsWith('claude'),\n vendor: {\n id: 'anthropic',\n label: 'Anthropic',\n mono: { bg: 'hsl(16 32% 26%)', ring: 'hsl(16 48% 54%)', text: 'hsl(18 60% 84%)' },\n initial: 'A',\n // P0 (Vincent 5222) custom-designed vendor badge — NOT a copy of\n // the Anthropic trademark; 4-pointed sparkle in their warm-orange\n // palette evokes AI/Claude without imitating the official mark.\n // Real Anthropic logo still pending Vincent-direct asset OK.\n logo: '/vendors/claude.svg',\n },\n },\n {\n test: (m) => m.startsWith('gpt') || m.startsWith('codex') || m.startsWith('o1') || m.startsWith('o3') || m.startsWith('o4'),\n vendor: {\n id: 'openai',\n label: 'OpenAI',\n mono: { bg: 'hsl(165 26% 22%)', ring: 'hsl(165 40% 44%)', text: 'hsl(165 45% 80%)' },\n initial: 'O',\n // P0 (Vincent 5222) custom-designed vendor badge — NOT a copy of\n // the OpenAI trademark; hexagonal frame + center dot in their\n // teal palette evokes geometric AI lattice without imitating the\n // knot. Real OpenAI logo still pending Vincent-direct asset OK.\n logo: '/vendors/openai.svg',\n },\n },\n];\n\n/** Resolve a model id to its vendor identity. `null` / unmatched → UNKNOWN. */\nexport function vendorForModel(model: string | null | undefined): VendorIdentity {\n if (!model) return UNKNOWN_VENDOR;\n const m = model.toLowerCase();\n for (const rule of VENDOR_RULES) {\n if (rule.test(m)) return rule.vendor;\n }\n return UNKNOWN_VENDOR;\n}\n\nexport type Runtime = 'claude-code-cli' | 'codex-sdk' | 'claude-agent-sdk' | 'http-api';\n\nexport interface RuntimeIdentity {\n label: string;\n /** SVG path(s) drawn inside a 0..24 viewBox for the corner badge. */\n iconPath: string;\n /** Badge accent colour — kept clear of working/idle/offline status hues. */\n color: string;\n}\n\nconst RUNTIME_MAP: Record<Runtime, RuntimeIdentity> = {\n // terminal / CLI\n 'claude-code-cli': {\n label: 'Claude Code CLI',\n iconPath: 'M4 5h16v14H4z M7 9l3 3-3 3 M13 15h4',\n color: '#a78bfa',\n },\n // code-block / SDK\n 'codex-sdk': {\n label: 'Codex SDK',\n iconPath: 'M9 7l-5 5 5 5 M15 7l5 5-5 5',\n color: '#38bdf8',\n },\n // SDK, distinct hue from codex\n 'claude-agent-sdk': {\n label: 'Claude Agent SDK',\n iconPath: 'M4 7h16v10H4z M8 11h8 M8 14h5',\n color: '#34d399',\n },\n // cloud / API — non-resident process\n 'http-api': {\n label: 'HTTP API',\n iconPath: 'M7 18a4 4 0 010-8 5 5 0 019.6-1.3A3.5 3.5 0 0118 18z',\n color: '#fbbf24',\n },\n};\n\n/** Resolve a server runtime string to badge identity. Unknown → null. */\nexport function runtimeIdentity(runtime: string | null | undefined): RuntimeIdentity | null {\n if (!runtime) return null;\n return RUNTIME_MAP[runtime as Runtime] ?? null;\n}\n\n/** Compact one-line identity for hover / detail surfaces:\n * \"Anthropic · claude-opus-4 · Claude Code CLI\". Pieces with no data drop. */\nexport function identityLine(model: string | null | undefined, runtime: string | null | undefined): string {\n const v = vendorForModel(model);\n const r = runtimeIdentity(runtime);\n const parts: string[] = [];\n if (v.id !== 'unknown') parts.push(v.label);\n if (model) parts.push(model);\n if (r) parts.push(r.label);\n return parts.join(' · ');\n}\n","'use client';\n\nimport Link from 'next/link';\nimport { Session } from './types';\nimport { timeAgo } from './utils';\nimport { AliasAvatar } from './AliasAvatar';\n\ninterface AgentCardProps {\n session: Session;\n hasSse: boolean;\n sseCount: number;\n onChat?: (alias: string) => void;\n}\n\nconst STATUS_CONFIG: Record<string, { bg: string; text: string; dot: string; glow: string }> = {\n working: { bg: 'bg-green-900/30 border-green-800/30', text: 'text-green-300', dot: 'bg-green-500', glow: 'shadow-green-500/10' },\n idle: { bg: 'bg-cyan-900/30 border-cyan-800/30', text: 'text-cyan-300', dot: 'bg-cyan-400', glow: 'shadow-cyan-500/5' },\n blocked: { bg: 'bg-yellow-900/30 border-yellow-800/30', text: 'text-yellow-300', dot: 'bg-yellow-500', glow: '' },\n error: { bg: 'bg-red-900/30 border-red-800/30', text: 'text-red-300', dot: 'bg-red-500', glow: '' },\n};\n\nconst DEFAULT_STATUS = { bg: 'bg-gray-800/50 border-gray-700/30', text: 'text-gray-500', dot: 'bg-gray-500', glow: '' };\n\nexport function AgentCard({ session: s, hasSse, sseCount, onChat }: AgentCardProps) {\n const cfg = hasSse ? (STATUS_CONFIG[s.status] || DEFAULT_STATUS) : DEFAULT_STATUS;\n\n return (\n <Link\n href={`/node?alias=${encodeURIComponent(s.alias)}`}\n prefetch={false}\n className={`anet-agent-card group relative block rounded-xl border p-4 transition-all duration-300 cursor-pointer hover:-translate-y-0.5 ${\n hasSse\n ? `bg-[#111128] border-[#2a2a4a] hover:border-cyan-500/30 hover:shadow-lg ${cfg.glow}`\n : 'bg-[#0d0d1a] border-[#1a1a2a] opacity-40'\n }`}\n >\n {/* Header: avatar + name + status. Avatar carries the alias→hue map\n shared with Messages/Nodes/Tasks/Overview; the live status dot\n stays as a small pulse-capable indicator. */}\n <div className=\"flex items-center justify-between mb-3\">\n <div className=\"flex items-center gap-2 min-w-0\">\n <AliasAvatar alias={s.alias} size={22} />\n <span className=\"font-semibold text-white truncate text-sm\" title={s.alias}>{s.alias}</span>\n <span className={`inline-block w-1.5 h-1.5 rounded-full shrink-0 ${cfg.dot} ${hasSse && s.status === 'working' ? 'animate-pulse' : ''}`} />\n </div>\n <span className={`text-[11px] px-2 py-0.5 rounded-md border shrink-0 ${cfg.bg} ${cfg.text}`}>\n {hasSse ? s.status : 'offline'}\n </span>\n </div>\n\n {/* Agent type badge */}\n <div className=\"flex items-center gap-2 mb-3\">\n <span className=\"text-xs text-gray-600 bg-[#0a0a15] px-2 py-0.5 rounded border border-[#1a1a2a]\">\n {s.agent || 'unknown'}\n </span>\n {hasSse && (\n <span className=\"text-[10px] text-green-500\">SSE:{sseCount}</span>\n )}\n </div>\n\n {/* Task */}\n {s.task ? (\n <div className=\"text-xs text-gray-400 bg-[#0a0a15] rounded-lg px-3 py-2 border border-[#1a1a2a] line-clamp-2\" title={s.task}>\n {s.task}\n </div>\n ) : (\n <div className=\"text-xs text-gray-700 italic\">No active task</div>\n )}\n\n {/* Progress bar */}\n {s.progress > 0 && (\n <div className=\"mt-3\">\n <div className=\"flex justify-between text-[10px] mb-1\">\n <span className=\"text-gray-600\">Progress</span>\n <span className={cfg.text}>{s.progress}%</span>\n </div>\n <div className=\"w-full bg-gray-800 rounded-full h-1.5 overflow-hidden\">\n <div\n className={`h-1.5 rounded-full transition-all duration-700 ${s.status === 'working' ? 'bg-green-500' : 'bg-cyan-500'}`}\n style={{ width: `${Math.min(s.progress, 100)}%` }}\n />\n </div>\n </div>\n )}\n\n {/* Footer: time + chat + hover chevron affordance (round 44).\n The card is a <Link> so it's clickable everywhere, but with no\n visible cue users may not realise. Chevron appears on hover and\n slides right ~2px for a \"drill in\" hint. */}\n <div className=\"mt-3 flex justify-between items-center text-[10px] text-gray-600\">\n <span className=\"truncate\" title={s.server || ''}>{s.server || '--'}</span>\n <div className=\"flex items-center gap-2\">\n {onChat && hasSse && (\n <button\n onClick={e => { e.preventDefault(); e.stopPropagation(); onChat(s.alias); }}\n className=\"text-cyan-400 hover:text-cyan-300 px-1.5 py-0.5 rounded border border-cyan-500/20 hover:bg-cyan-500/10 transition-colors\"\n >\n Chat\n </button>\n )}\n <span>{timeAgo(s.updated_at)}</span>\n <svg\n aria-hidden\n className=\"w-3 h-3 text-gray-700 opacity-0 -translate-x-1 group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-200\"\n fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\"\n >\n <path d=\"M9 18l6-6-6-6\" />\n </svg>\n </div>\n </div>\n </Link>\n );\n}\n","'use client';\n\nimport { InboxMessage } from './types';\nimport { AliasAvatar } from './AliasAvatar';\nimport { timeAgo, previewContent } from './utils';\n\ninterface InboxPanelProps {\n messages: InboxMessage[];\n}\n\nexport function InboxPanel({ messages }: InboxPanelProps) {\n if (messages.length === 0) return null;\n\n return (\n <div className=\"mt-8\">\n <h2 className=\"text-lg font-semibold text-white mb-3 flex items-center gap-2\">\n <span className=\"text-gray-500\">Inbox</span>\n <span className=\"text-xs bg-blue-900/30 text-blue-400 px-2 py-0.5 rounded-full border border-blue-800/30\">\n {messages.length}\n </span>\n </h2>\n <div className=\"space-y-2 max-h-96 overflow-y-auto pr-1 scrollbar-thin\">\n {messages.map(m => (\n <div key={m.id} className=\"bg-[#111128] border border-[#2a2a4a] rounded-lg px-4 py-3 text-sm transition-colors hover:border-[#3a3a5a]\">\n <div className=\"flex items-center gap-2 text-xs mb-1.5\">\n {m.from_session && <AliasAvatar alias={m.from_session} size={16} />}\n <span className=\"text-gray-200 font-medium truncate\">{m.from_session}</span>\n <span className=\"ml-auto text-gray-600 shrink-0\" title={m.created_at}>{timeAgo(m.created_at)}</span>\n </div>\n <div className=\"text-gray-300 leading-relaxed line-clamp-3\" title={m.content || ''}>{previewContent(m.content)}</div>\n </div>\n ))}\n </div>\n </div>\n );\n}\n","'use client';\n\n/**\n * Loading skeleton — mirrors the actual Overview layout structure so the\n * page doesn't appear to \"shift\" once data arrives. Uses the same\n * `anet-skeleton-pulse` rhythm as the brand pulse (1.6s opacity drift,\n * no scale, no blur, no glow) so the loading animation feels native to\n * the rest of the dashboard rather than a generic spinner.\n *\n * Bars are theme-aware: anet-skeleton-bar (existing CSS shim resolves this to\n * var(--bg-elevated) on light/mint), so light theme shows soft grey blocks\n * on white cards and dark theme shows lighter blocks on navy cards.\n */\nexport function LoadingSkeleton() {\n return (\n <div className=\"min-h-screen bg-[#0a0a1a] text-gray-100 p-4 sm:p-6 font-mono\">\n {/* KPI top strip — 4 cards matching StatsBar */}\n <div className=\"grid grid-cols-2 sm:grid-cols-4 gap-3 mb-8 anet-skeleton-pulse\">\n {[1, 2, 3, 4].map(i => (\n <div key={i} className=\"rounded-xl border border-[#2a2a4a] bg-[#111128] px-4 py-3\">\n <Bar w=\"2.5rem\" h=\"1.75rem\" />\n <Bar w=\"3.5rem\" h=\"0.75rem\" className=\"mt-2\" />\n <Bar w=\"5rem\" h=\"0.625rem\" className=\"mt-1\" />\n </div>\n ))}\n </div>\n\n {/* Dispatch + UserBar row */}\n <div className=\"flex items-center gap-3 mb-3 anet-skeleton-pulse\">\n <Bar w=\"6rem\" h=\"2.5rem\" rounded=\"0.75rem\" />\n <div className=\"flex-1 rounded-lg border border-[#2a2a4a] bg-[#111128] px-4 py-2.5 flex items-center gap-3\">\n <div className=\"w-8 h-8 rounded-full anet-skeleton-bar\" />\n <div className=\"flex-1\">\n <Bar w=\"6rem\" h=\"0.875rem\" />\n <Bar w=\"10rem\" h=\"0.625rem\" className=\"mt-1\" />\n </div>\n </div>\n </div>\n\n {/* Config bar */}\n <div className=\"mb-6 rounded-lg border border-[#2a2a4a] bg-[#111128] px-4 py-3 anet-skeleton-pulse\">\n <Bar w=\"14rem\" h=\"0.875rem\" />\n </div>\n\n {/* Stat strip 3 cards */}\n <div className=\"grid grid-cols-3 gap-2 sm:gap-3 mb-3 anet-skeleton-pulse\">\n {[1, 2, 3].map(i => (\n <div key={i} className=\"rounded-xl border border-[#2a2a4a] bg-[#111128] px-3 py-3\">\n <Bar w=\"2rem\" h=\"1.25rem\" />\n <Bar w=\"2.5rem\" h=\"0.75rem\" className=\"mt-1\" />\n <Bar w=\"3.5rem\" h=\"0.625rem\" className=\"mt-px\" />\n </div>\n ))}\n </div>\n\n {/* Nav rail 3 cards */}\n <div className=\"grid grid-cols-3 gap-2 sm:gap-3 mb-6 anet-skeleton-pulse\">\n {[1, 2, 3].map(i => (\n <div key={i} className=\"rounded-xl border border-[#2a2a4a] bg-[#111128] px-3 py-2.5 flex items-center justify-center gap-2\">\n <div className=\"w-4 h-4 rounded anet-skeleton-bar\" />\n <Bar w=\"4rem\" h=\"0.75rem\" />\n </div>\n ))}\n </div>\n\n {/* Broadcast bar */}\n <div className=\"mb-6 flex gap-2 anet-skeleton-pulse\">\n <div className=\"flex-1 h-10 rounded-lg border border-[#2a2a4a] bg-[#111128]\" />\n <div className=\"w-28 h-10 rounded-lg anet-skeleton-bar\" />\n </div>\n\n {/* Agent card grid */}\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-3 sm:gap-4 anet-skeleton-pulse\">\n {[1, 2, 3, 4].map(i => (\n <div key={i} className=\"rounded-xl border border-[#2a2a4a] bg-[#111128] p-4\">\n <div className=\"flex items-center gap-2 mb-3\">\n <div className=\"w-2 h-2 rounded-full anet-skeleton-bar\" />\n <Bar w=\"6rem\" h=\"0.875rem\" />\n </div>\n <div className=\"space-y-2\">\n <Bar w=\"100%\" h=\"0.625rem\" />\n <Bar w=\"75%\" h=\"0.625rem\" />\n <Bar w=\"55%\" h=\"0.625rem\" />\n </div>\n </div>\n ))}\n </div>\n </div>\n );\n}\n\n/** Single shimmer bar — uses `anet-skeleton-bar` so theme-specific bar\n * color (dark navy on dark themes, mid-grey on light) overrides the\n * default. */\nfunction Bar({ w, h, rounded, className }: { w: string; h: string; rounded?: string; className?: string }) {\n return (\n <div\n className={`anet-skeleton-bar ${className || ''}`}\n style={{ width: w, height: h, borderRadius: rounded || '0.375rem' }}\n />\n );\n}\n\n// EmptyState export removed in 0.4.5 — replaced by EmptyState.tsx with\n// per-variant glyphs and a NodesEmptyState wrapper for the Overview\n// hint-aware behavior. See app/components/EmptyState.tsx.\n","'use client';\n\nimport { useEffect, useState } from 'react';\nimport { useNetworkId } from '../lib/network-context';\n\ninterface User {\n user_id: string;\n username: string;\n display_name: string;\n role: string;\n}\n\ninterface Network {\n network_id: string;\n network_name: string;\n description: string;\n}\n\ninterface AuthState {\n user: User | null;\n networks: Network[];\n currentNetwork: string;\n token: string;\n}\n\nexport function UserBar() {\n const { setNetworkId } = useNetworkId();\n const [auth, setAuth] = useState<AuthState>({ user: null, networks: [], currentNetwork: '', token: '' });\n const [showLogin, setShowLogin] = useState(false);\n const [username, setUsername] = useState('');\n const [password, setPassword] = useState('');\n const [loginError, setLoginError] = useState('');\n const [loginPending, setLoginPending] = useState(false);\n\n // Try to restore from sessionStorage\n useEffect(() => {\n const saved = sessionStorage.getItem('anet_v3_auth');\n if (saved) {\n try { setAuth(JSON.parse(saved)); } catch {}\n }\n }, []);\n\n const doAuth = async (action: 'login' | 'register') => {\n if (!username.trim() || !password.trim()) return;\n setLoginPending(true);\n setLoginError('');\n try {\n const res = await fetch('/api/hub/auth', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ action, username, password }),\n });\n const data = await res.json();\n if (!data.ok) { setLoginError(data.error || 'Failed'); setLoginPending(false); return; }\n\n // Fetch user details\n const meRes = await fetch(`/api/hub/auth?token=${data.token}&endpoint=/api/auth/me`);\n const meData = await meRes.json();\n\n const newAuth: AuthState = {\n user: data.user,\n networks: meData.networks || [],\n currentNetwork: meData.current_network?.network_id || meData.networks?.[0]?.network_id || '',\n token: data.token,\n };\n setAuth(newAuth);\n sessionStorage.setItem('anet_v3_auth', JSON.stringify(newAuth));\n setShowLogin(false);\n setUsername('');\n setPassword('');\n } catch { setLoginError('Connection failed'); }\n setLoginPending(false);\n };\n\n const logout = () => {\n setAuth({ user: null, networks: [], currentNetwork: '', token: '' });\n sessionStorage.removeItem('anet_v3_auth');\n };\n\n const [editing, setEditing] = useState(false);\n const [editName, setEditName] = useState('');\n const [editEmail, setEditEmail] = useState('');\n\n const updateProfile = async () => {\n try {\n const res = await fetch(`/api/hub/auth?token=${auth.token}&endpoint=/api/auth/me`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ action: 'update_profile', token: auth.token, display_name: editName, email: editEmail }),\n });\n // Use a direct PUT via a new proxy approach\n const putRes = await fetch('/api/hub/auth', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ action: 'update_profile', token: auth.token, display_name: editName, email: editEmail }),\n });\n const data = await putRes.json();\n if (data.ok && data.user) {\n const updated = { ...auth, user: data.user };\n setAuth(updated);\n sessionStorage.setItem('anet_v3_auth', JSON.stringify(updated));\n setEditing(false);\n }\n } catch {}\n };\n\n // Don't show anything when not logged in — login page handles that\n if (!auth.user) return null;\n\n const initials = (auth.user.display_name || auth.user.username).slice(0, 2).toUpperCase();\n\n return (\n <div className=\"flex items-center justify-between bg-[#111128] border border-[#2a2a4a] rounded-lg px-4 py-2.5 mb-4\">\n {/* User info */}\n <div className=\"flex items-center gap-3\">\n <div className=\"w-8 h-8 rounded-full bg-gradient-to-br from-cyan-500 to-blue-600 flex items-center justify-center text-xs font-bold text-white\">\n {initials}\n </div>\n <div>\n <div className=\"text-sm text-white font-medium\">{auth.user.display_name || auth.user.username}</div>\n <div className=\"text-[10px] text-gray-500\">{auth.user.role} · {auth.user.user_id}</div>\n </div>\n </div>\n\n {/* Network switcher */}\n <div className=\"flex items-center gap-3\">\n {auth.networks.length > 0 && (\n <select\n value={auth.currentNetwork}\n onChange={e => {\n const updated = { ...auth, currentNetwork: e.target.value };\n setAuth(updated);\n setNetworkId(e.target.value);\n sessionStorage.setItem('anet_v3_auth', JSON.stringify(updated));\n }}\n className=\"bg-[#0a0a15] border border-[#2a2a4a] rounded px-2 py-1 text-xs text-white focus:outline-none\"\n >\n {auth.networks.map(n => (\n <option key={n.network_id} value={n.network_id}>{n.network_name}</option>\n ))}\n </select>\n )}\n <button onClick={() => { setEditName(auth.user?.display_name || ''); setEditEmail(''); setEditing(!editing); }}\n className=\"text-xs text-gray-500 hover:text-gray-300 whitespace-nowrap\"\n aria-label=\"Edit profile\">\n <span className=\"hidden sm:inline\">Edit</span>\n <svg className=\"sm:hidden w-4 h-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" strokeWidth=\"1.5\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.862 4.487z\" />\n </svg>\n </button>\n <button onClick={logout}\n className=\"text-xs text-gray-500 hover:text-gray-300 whitespace-nowrap\"\n aria-label=\"Sign out\">\n <span className=\"hidden sm:inline\">Sign out</span>\n <svg className=\"sm:hidden w-4 h-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" strokeWidth=\"1.5\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9\" />\n </svg>\n </button>\n </div>\n {editing && (\n <div className=\"flex flex-wrap items-center gap-2 mt-2 pt-2 border-t border-[#2a2a4a]\">\n <input type=\"text\" value={editName} onChange={e => setEditName(e.target.value)} placeholder=\"Display name\"\n className=\"bg-[#0a0a15] border border-[#2a2a4a] rounded px-2 py-1 text-xs text-white placeholder-gray-600 focus:outline-none w-32\" />\n <input type=\"email\" value={editEmail} onChange={e => setEditEmail(e.target.value)} placeholder=\"Email\"\n className=\"bg-[#0a0a15] border border-[#2a2a4a] rounded px-2 py-1 text-xs text-white placeholder-gray-600 focus:outline-none w-40\" />\n <button onClick={updateProfile} className=\"px-2 py-1 bg-cyan-600 hover:bg-cyan-500 text-white text-xs rounded\">Save</button>\n </div>\n )}\n </div>\n );\n}\n","'use client';\n\nimport { useState } from 'react';\nimport { TaskChatPanel } from './TaskChatPanel';\n\ninterface CommandCenterProps {\n /** Currently open chat tabs */\n tabs: string[];\n activeTab: string;\n onOpenTab: (alias: string) => void;\n onCloseTab: (alias: string) => void;\n onSetActive: (alias: string) => void;\n onClose: () => void;\n}\n\n/**\n * Multi-tab chat panel for commanding multiple agents simultaneously.\n * Wraps TaskChatPanel with a tab bar for switching between agents.\n */\nexport function CommandCenter({ tabs, activeTab, onOpenTab, onCloseTab, onSetActive, onClose }: CommandCenterProps) {\n if (tabs.length === 0) return null;\n\n return (\n <>\n {/* Backdrop */}\n <div className=\"fixed inset-0 bg-black/30 z-40 lg:hidden\" onClick={onClose} />\n\n {/* Panel */}\n <div className=\"fixed top-0 right-0 h-full w-full lg:w-[500px] bg-[#0a0a1a] border-l border-[#2a2a4a] z-50 flex flex-col shadow-2xl shadow-black/60 animate-slide-in\">\n {/* Tab bar */}\n <div className=\"flex items-center border-b border-[#2a2a4a] bg-[#0d0d1a] overflow-x-auto\">\n <div className=\"flex-1 flex min-w-0\">\n {tabs.map(alias => (\n <button\n key={alias}\n onClick={() => onSetActive(alias)}\n className={`flex items-center gap-1.5 px-3 py-2.5 text-xs border-b-2 transition-colors shrink-0 ${\n activeTab === alias\n ? 'border-cyan-400 text-cyan-300 bg-cyan-500/5'\n : 'border-transparent text-gray-500 hover:text-gray-300'\n }`}\n >\n <div className={`w-2 h-2 rounded-full ${activeTab === alias ? 'bg-cyan-400' : 'bg-gray-600'}`} />\n <span className=\"max-w-[80px] truncate\">{alias}</span>\n <button\n onClick={e => { e.stopPropagation(); onCloseTab(alias); }}\n className=\"ml-1 text-gray-600 hover:text-gray-300 p-0.5\"\n >\n ×\n </button>\n </button>\n ))}\n </div>\n <button onClick={onClose} className=\"text-gray-500 hover:text-white px-3 py-2.5 shrink-0 border-l border-[#2a2a4a]\">\n <svg className=\"w-4 h-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" strokeWidth={2}>\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M6 18L18 6M6 6l12 12\" />\n </svg>\n </button>\n </div>\n\n {/* Active chat - render all but only show active (preserves state) */}\n <div className=\"flex-1 relative overflow-hidden\">\n {tabs.map(alias => (\n <div key={alias} className={`absolute inset-0 ${activeTab === alias ? 'block' : 'hidden'}`}>\n <InlineChat alias={alias} />\n </div>\n ))}\n </div>\n </div>\n\n <style jsx global>{`\n @keyframes slide-in {\n from { transform: translateX(100%); }\n to { transform: translateX(0); }\n }\n .animate-slide-in {\n animation: slide-in 0.2s ease-out;\n }\n `}</style>\n </>\n );\n}\n\n/** Inline chat without the panel chrome (used inside CommandCenter tabs) */\nfunction InlineChat({ alias }: { alias: string }) {\n // Reuse TaskChatPanel's logic but render without the outer frame\n return <TaskChatPanel alias={alias} onClose={() => {}} inline />;\n}\n\n/**\n * Hook to manage multi-tab command center state.\n */\nexport function useCommandCenter() {\n const [tabs, setTabs] = useState<string[]>([]);\n const [activeTab, setActiveTab] = useState('');\n\n const openTab = (alias: string) => {\n setTabs(prev => prev.includes(alias) ? prev : [...prev, alias]);\n setActiveTab(alias);\n };\n\n const closeTab = (alias: string) => {\n setTabs(prev => {\n const next = prev.filter(t => t !== alias);\n if (activeTab === alias) setActiveTab(next[next.length - 1] || '');\n return next;\n });\n };\n\n const closeAll = () => {\n setTabs([]);\n setActiveTab('');\n };\n\n return { tabs, activeTab, openTab, closeTab, closeAll, setActiveTab };\n}\n","'use client';\n\nimport { useState } from 'react';\nimport type { Session } from './types';\nimport { AliasAvatar } from './AliasAvatar';\n\ninterface DispatchPanelProps {\n sessions: Session[];\n onClose: () => void;\n}\n\ninterface SendResult {\n alias: string;\n ok: boolean;\n error?: string;\n}\n\nexport function DispatchPanel({ sessions, onClose }: DispatchPanelProps) {\n const [selected, setSelected] = useState<Set<string>>(new Set());\n const [prompt, setPrompt] = useState('');\n const [priority, setPriority] = useState('normal');\n const [sending, setSending] = useState(false);\n const [results, setResults] = useState<SendResult[]>([]);\n const [filter, setFilter] = useState('');\n\n const onlineNodes = sessions.filter(s => s.status !== 'offline');\n const filtered = onlineNodes.filter(s =>\n !filter || s.alias.toLowerCase().includes(filter.toLowerCase()) || (s.agent || '').toLowerCase().includes(filter.toLowerCase())\n );\n\n const toggleNode = (alias: string) => {\n setSelected(prev => {\n const next = new Set(prev);\n if (next.has(alias)) next.delete(alias); else next.add(alias);\n return next;\n });\n };\n\n const selectAll = () => {\n if (selected.size === filtered.length) {\n setSelected(new Set());\n } else {\n setSelected(new Set(filtered.map(s => s.alias)));\n }\n };\n\n const dispatch = async () => {\n if (!prompt.trim() || selected.size === 0 || sending) return;\n setSending(true);\n setResults([]);\n\n const promises = [...selected].map(async alias => {\n try {\n const res = await fetch('/api/hub/send', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ alias, task: prompt, priority }),\n });\n const data = await res.json();\n return { alias, ok: !!data.ok, error: data.error };\n } catch (e) {\n return { alias, ok: false, error: 'send failed' };\n }\n });\n\n const res = await Promise.all(promises);\n setResults(res);\n setSending(false);\n };\n\n const successCount = results.filter(r => r.ok).length;\n\n return (\n <>\n <div className=\"fixed inset-0 bg-black/50 z-40 anet-fade-in\" onClick={onClose} />\n <div className=\"fixed inset-4 lg:inset-x-[15%] lg:inset-y-[5%] bg-[#0a0a1a] border border-[#2a2a4a] rounded-2xl z-50 flex flex-col shadow-2xl shadow-black/70 overflow-hidden anet-fade-in\">\n {/* Header */}\n <div className=\"flex items-center justify-between px-6 py-4 border-b border-[#2a2a4a] bg-[#0d0d1a]\">\n <div>\n <h2 className=\"text-lg font-bold text-white\">Dispatch Task</h2>\n <p className=\"text-xs text-gray-500 mt-0.5\">Send a task to one or more agents</p>\n </div>\n <button onClick={onClose} className=\"text-gray-500 hover:text-white p-1.5 rounded-lg hover:bg-[#1a1a2a]\">\n <svg className=\"w-5 h-5\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" strokeWidth={2}>\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M6 18L18 6M6 6l12 12\" />\n </svg>\n </button>\n </div>\n\n <div className=\"flex-1 flex flex-col lg:flex-row overflow-hidden\">\n {/* Left: Node selection */}\n <div className=\"lg:w-[280px] border-b lg:border-b-0 lg:border-r border-[#2a2a4a] flex flex-col\">\n <div className=\"px-4 py-3 border-b border-[#2a2a4a]\">\n <input\n type=\"text\" value={filter} onChange={e => setFilter(e.target.value)}\n placeholder=\"Filter agents...\"\n className=\"w-full bg-[#111128] border border-[#2a2a4a] rounded-lg px-3 py-2 text-xs text-white placeholder-gray-600 focus:border-cyan-500/40 focus:outline-none\"\n />\n <div className=\"flex items-center justify-between mt-2\">\n <button onClick={selectAll} className=\"text-[10px] text-cyan-400 hover:text-cyan-300\">\n {selected.size === filtered.length ? 'Deselect all' : `Select all (${filtered.length})`}\n </button>\n <span className=\"text-[10px] text-gray-600\">{selected.size} selected</span>\n </div>\n </div>\n <div className=\"flex-1 overflow-y-auto px-2 py-2 space-y-0.5 max-h-[200px] lg:max-h-none\">\n {filtered.map(s => (\n <button key={s.alias} onClick={() => toggleNode(s.alias)}\n className={`w-full flex items-center gap-2 px-3 py-2 rounded-lg text-xs text-left transition-colors ${\n selected.has(s.alias) ? 'bg-cyan-500/10 text-cyan-300 border border-cyan-500/20' : 'text-gray-400 hover:bg-[#1a1a2a]'\n }`}>\n <AliasAvatar alias={s.alias} size={16} />\n <div className={`w-1.5 h-1.5 rounded-full shrink-0 ${s.status === 'working' ? 'bg-green-400' : s.status === 'idle' ? 'bg-cyan-400' : 'bg-gray-500'}`} />\n <span className=\"truncate flex-1\">{s.alias}</span>\n <span className=\"text-[9px] text-gray-600\">{s.agent || '--'}</span>\n </button>\n ))}\n {filtered.length === 0 && <div className=\"text-center text-xs text-gray-600 py-4\">No online agents</div>}\n </div>\n </div>\n\n {/* Right: Prompt + Send */}\n <div className=\"flex-1 flex flex-col\">\n <div className=\"flex-1 px-6 py-4 flex flex-col\">\n <label className=\"text-xs text-gray-500 uppercase mb-2\">Task Prompt</label>\n <textarea\n value={prompt} onChange={e => setPrompt(e.target.value)}\n placeholder=\"Enter the task you want to dispatch...\"\n className=\"flex-1 min-h-[120px] bg-[#111128] border border-[#2a2a4a] rounded-xl px-4 py-3 text-sm text-white placeholder-gray-600 focus:border-cyan-500/40 focus:outline-none resize-none\"\n />\n\n <div className=\"flex items-center gap-3 mt-4\">\n <select value={priority} onChange={e => setPriority(e.target.value)}\n className=\"bg-[#111128] border border-[#2a2a4a] rounded-lg px-3 py-2 text-xs text-white focus:outline-none\">\n <option value=\"normal\">Normal priority</option>\n <option value=\"high\">High priority</option>\n <option value=\"low\">Low priority</option>\n </select>\n\n <div className=\"flex-1\" />\n\n <button onClick={dispatch} disabled={sending || !prompt.trim() || selected.size === 0}\n className=\"px-6 py-2.5 bg-gradient-to-r from-cyan-600 to-blue-600 hover:from-cyan-500 hover:to-blue-500 disabled:from-gray-800 disabled:to-gray-800 disabled:text-gray-600 text-white text-sm font-medium rounded-xl transition-all shadow-lg shadow-cyan-500/10 disabled:shadow-none active:scale-95\">\n {sending ? (\n <span className=\"flex items-center gap-2\">\n <span className=\"w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin\" />\n Sending...\n </span>\n ) : (\n `Dispatch to ${selected.size} agent${selected.size > 1 ? 's' : ''}`\n )}\n </button>\n </div>\n </div>\n\n {/* Results */}\n {results.length > 0 && (\n <div className=\"px-6 py-3 border-t border-[#2a2a4a] bg-[#0d0d1a]\">\n <div className=\"text-xs text-gray-500 mb-2\">\n {successCount}/{results.length} dispatched successfully\n </div>\n <div className=\"flex flex-wrap gap-1.5\">\n {results.map(r => (\n <span key={r.alias} className={`flex items-center gap-1.5 text-[10px] pl-1 pr-2 py-0.5 rounded-full border ${\n r.ok ? 'text-green-300 border-green-500/20 bg-green-500/5' : 'text-red-300 border-red-500/20 bg-red-500/5'\n }`}>\n <AliasAvatar alias={r.alias} size={14} />\n <span>{r.alias}</span>\n <span aria-hidden>{r.ok ? '✓' : r.error || '✗'}</span>\n </span>\n ))}\n </div>\n </div>\n )}\n </div>\n </div>\n </div>\n </>\n );\n}\n"],"names":["React","_interopDefaultLegacy","e","React__default","_defineProperties","target","props","i","length","descriptor","enumerable","configurable","writable","Object","defineProperty","key","_createClass","Constructor","protoProps","staticProps","prototype","isProd","process","env","isString","o","toString","call","StyleSheet","param","ref","_name","name","_optimizeForSpeed","optimizeForSpeed","invariant$1","_deletedRulePlaceholder","_serverSheet","undefined","_tags","_injected","_rulesCount","node","document","querySelector","_nonce","_proto","setOptimizeForSpeed","bool","flush","inject","isOptimizeForSpeed","_this","cssRules","insertRule","rule","index","cssText","push","deleteRule","getSheetForTag","tag","sheet","styleSheets","ownerNode","getSheet","insertionPoint","replaceRule","trim","error","console","warn","makeStyleTag","cssString","relativeToTag","createElement","setAttribute","type","appendChild","createTextNode","head","getElementsByTagName","insertBefore","get","condition","message","Error","hash","str","_$hash","charCodeAt","stringHash","sanitize","replace","cache","computeId","baseId","propsToString","String","computeSelector","id","css","selectoPlaceholderRegexp","idcss","mapRulesToStyle","options","map","args","nonce","dangerouslySetInnerHTML","__html","StyleSheetRegistry","_styleSheet","styleSheet","_sheet","_fromServer","_indices","_instancesCounts","add","Array","isArray","children","getIdAndRules","styleId","rules","indices","filter","remove","invariant","tagFromServer","parentNode","removeChild","forEach","update","nextProps","fromServer","keys","concat","join","Boolean","styles","dynamic","selectFromServer","elements","slice","querySelectorAll","reduce","acc","element","StyleSheetContext","createContext","displayName","createStyleRegistry","StyleRegistry","configuredRegistry","registry","rootRegistry","useContext","useState","Provider","value","useStyleRegistry","useInsertionEffect","useLayoutEffect","defaultRegistry","JSXStyle","info","tagInfo","exports","style","module","StatsBar","online","working","total","version","uptime","onlinePercent","Math","round","fleetEmpty","className","StatCard","label","sub","color","accent","border","accentKey","split"],"mappings":"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","ignoreList":[0,1]}
|