canopy-engine 0.0.0-stage → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/FEEDBACK.md +46 -0
  2. package/LICENSE +24 -0
  3. package/README.md +171 -2
  4. package/dist/canopy.js +87 -0
  5. package/dist/queries/arrowFunctions.scm +5 -0
  6. package/dist/queries/callExpressions.scm +2 -0
  7. package/dist/queries/classMethods.scm +6 -0
  8. package/dist/queries/exports.scm +9 -0
  9. package/dist/queries/fetchApiCalls.scm +15 -0
  10. package/dist/queries/functionDeclarations.scm +4 -0
  11. package/dist/queries/htmlScriptRoot.scm +5 -0
  12. package/dist/queries/imports.scm +3 -0
  13. package/dist/queries/jsxElements.scm +5 -0
  14. package/dist/queries/matchExtensionListeners.scm +62 -0
  15. package/dist/queries/matchFastifyRoutes.scm +28 -0
  16. package/dist/queries/methodCalls.scm +4 -0
  17. package/dist/queries/rawSocketSend.scm +26 -0
  18. package/dist/queries/rawSocketTypeCheck.scm +20 -0
  19. package/dist/queries/returns.scm +2 -0
  20. package/dist/queries/socketEmit.scm +8 -0
  21. package/dist/queries/socketOn.scm +19 -0
  22. package/dist/queries/typescript/arrowFunctions.scm +15 -0
  23. package/dist/queries/typescript/classMethods.scm +65 -0
  24. package/dist/queries/typescript/defaultExports.scm +6 -0
  25. package/dist/queries/typescript/exports.scm +16 -0
  26. package/dist/queries/typescript/imports.scm +8 -0
  27. package/package.json +91 -4
  28. package/ui-react/dist/bundle.css +2 -0
  29. package/ui-react/dist/bundle.js +108 -0
  30. package/ui-react/index.html +19 -0
  31. package/vendor/tree-sitter-html/package.json +6 -0
  32. package/vendor/tree-sitter-html/tree-sitter-html.wasm +0 -0
  33. package/vendor/tree-sitter-javascript/package.json +6 -0
  34. package/vendor/tree-sitter-javascript/tree-sitter-javascript.wasm +0 -0
  35. package/vendor/tree-sitter-typescript/package.json +6 -0
  36. package/vendor/tree-sitter-typescript/tree-sitter-tsx.wasm +0 -0
  37. package/vendor/tree-sitter-typescript/tree-sitter-typescript.wasm +0 -0
@@ -0,0 +1,5 @@
1
+ (variable_declarator
2
+ name : (identifier) @func_name
3
+ value : (arrow_function
4
+ parameters : [(formal_parameters) (identifier)]
5
+ @func_params)) @func_declaration
@@ -0,0 +1,2 @@
1
+ (call_expression
2
+ function: (identifier) @called_function)
@@ -0,0 +1,6 @@
1
+ (class_declaration
2
+ name: (identifier) @class_name
3
+ body: (class_body
4
+ (method_definition
5
+ name: (property_identifier) @method_name
6
+ parameters: (formal_parameters) @method_params) @method_declaration))
@@ -0,0 +1,9 @@
1
+ (export_statement
2
+ declaration : (function_declaration
3
+ name : (identifier) @exported_name))
4
+
5
+ (export_statement
6
+ declaration : (lexical_declaration
7
+ (variable_declarator
8
+ name : (identifier) @exported_name
9
+ value : (arrow_function))))
@@ -0,0 +1,15 @@
1
+ (call_expression
2
+ function : (identifier) @fetch_name
3
+ arguments : (arguments
4
+ (string (string_fragment) @url (_)*)
5
+ (#eq? @fetch_name "fetch")))
6
+
7
+
8
+ (call_expression
9
+ function: (member_expression
10
+ object: (identifier) @axios_name
11
+ property: (property_identifier) @http_method)
12
+ arguments: (arguments
13
+ (string (string_fragment) @url) (_)*)
14
+ (#match? @http_method "^(get|post|put|delete|patch)$")
15
+ (#eq? @axios_name "axios"))
@@ -0,0 +1,4 @@
1
+ (function_declaration
2
+ name : (identifier) @func_name
3
+ parameters : (formal_parameters) @func_params)
4
+ @func_declaration
@@ -0,0 +1,5 @@
1
+ (script_element
2
+ (start_tag
3
+ (attribute
4
+ (attribute_name) @attr_name
5
+ (quoted_attribute_value (attribute_value) @script_src))))
@@ -0,0 +1,3 @@
1
+ (import_statement
2
+ source : (string (string_fragment) @import_source))
3
+
@@ -0,0 +1,5 @@
1
+ (jsx_opening_element
2
+ name : (identifier) @jsx_tag)
3
+
4
+ (jsx_self_closing_element
5
+ name : (identifier) @jsx_tag)
@@ -0,0 +1,62 @@
1
+ ; Mirrors matchFastifyRoutes.scm / socketOn.scm's convention: a named
2
+ ; handler reference and an inline function expression are two separate
3
+ ; top-level patterns, not one generic wildcard capture — so JS-side code
4
+ ; can tell which one matched instead of getting either a clean
5
+ ; identifier OR a giant multi-line inline-function source string under
6
+ ; the same "handlerName" field.
7
+ ;
8
+ ; NOT anchored to the end of the arguments list, unlike the
9
+ ; matchFastifyRoutes.scm fix — that anchor doesn't transfer here.
10
+ ; Express-style routes put optional MIDDLEWARE BEFORE the real handler
11
+ ; (so the handler needs to be pinned to the end); addEventListener puts
12
+ ; an optional OPTIONS object AFTER the handler
13
+ ; (addEventListener("click", handler, { passive: true })), which is the
14
+ ; opposite shape. The handler here is always the argument immediately
15
+ ; following the type string — a plain, unanchored, in-order match
16
+ ; already only captures that one correctly, and leaves any trailing
17
+ ; options argument alone without needing to consume or anchor past it.
18
+
19
+ ; chrome.runtime.onMessage.addListener(namedHandler)
20
+ (call_expression
21
+ function: (member_expression
22
+ object: (member_expression
23
+ object: (member_expression
24
+ object: (identifier) @root_object
25
+ property: (property_identifier) @sub_object)
26
+ property: (property_identifier) @event_name)
27
+ property: (property_identifier) @method_name)
28
+ arguments: (arguments (identifier) @handler_ref .)
29
+ (#eq? @method_name "addListener"))
30
+
31
+ ; chrome.runtime.onMessage.addListener((msg) => {...})
32
+ (call_expression
33
+ function: (member_expression
34
+ object: (member_expression
35
+ object: (member_expression
36
+ object: (identifier) @root_object
37
+ property: (property_identifier) @sub_object)
38
+ property: (property_identifier) @event_name)
39
+ property: (property_identifier) @method_name)
40
+ arguments: (arguments [(arrow_function) (function_expression)] @handler_inline .)
41
+ (#eq? @method_name "addListener"))
42
+
43
+ ; element.addEventListener("click", namedHandler) — with or without a
44
+ ; trailing options argument (true / { passive: true } / etc.)
45
+ (call_expression
46
+ function: (member_expression
47
+ object: (identifier) @target
48
+ property: (property_identifier) @listener_method)
49
+ arguments: (arguments
50
+ (string (string_fragment) @event_type)
51
+ (identifier) @handler_ref)
52
+ (#eq? @listener_method "addEventListener"))
53
+
54
+ ; element.addEventListener("click", () => {...}) — same, inline form
55
+ (call_expression
56
+ function: (member_expression
57
+ object: (identifier) @target
58
+ property: (property_identifier) @listener_method)
59
+ arguments: (arguments
60
+ (string (string_fragment) @event_type)
61
+ [(arrow_function) (function_expression)] @handler_inline)
62
+ (#eq? @listener_method "addEventListener"))
@@ -0,0 +1,28 @@
1
+ ; The trailing "." right before each arguments list's closing paren
2
+ ; anchors that capture to the LAST child — without it, an unanchored
3
+ ; "(_)* (identifier) @handler_ref" happily matches the FIRST identifier
4
+ ; after the path string, which for a multi-argument registration like
5
+ ; router.get('/x', requireUser, actualHandler) is the middleware, not
6
+ ; the real handler. Express/Fastify/Koa/Restify convention is that the
7
+ ; final argument is always the real handler, however many middleware
8
+ ; functions precede it — so anchoring to the end is the correct general
9
+ ; fix, not a special case for any one middleware name.
10
+ (call_expression
11
+ function: (member_expression
12
+ object: (identifier) @app_name
13
+ property: (property_identifier) @http_method)
14
+ arguments: (arguments
15
+ (string (string_fragment) @route_path)
16
+ (_)*
17
+ (identifier) @handler_ref .)
18
+ (#match? @http_method "^(get|post|put|delete|patch)$"))
19
+
20
+ (call_expression
21
+ function: (member_expression
22
+ object: (identifier) @app_name
23
+ property: (property_identifier) @http_method)
24
+ arguments: (arguments
25
+ (string (string_fragment) @route_path)
26
+ (_)*
27
+ [(arrow_function) (function_expression)] @handler_inline .)
28
+ (#match? @http_method "^(get|post|put|delete|patch)$"))
@@ -0,0 +1,4 @@
1
+ (call_expression
2
+ function : (member_expression
3
+ object : (identifier) @object_name
4
+ property : (property_identifier) @method_name))
@@ -0,0 +1,26 @@
1
+ ; Matches raw WebSocket sends carrying a literal "type" discriminator,
2
+ ; e.g. socket.send(JSON.stringify({ type: "subscribe", room })).
3
+ ; Native WebSocket .send()/.on("message") apps commonly dispatch on a
4
+ ; JSON "type" field instead of socket.io's emit()/on() event names.
5
+ ; This is the send-side half of that pattern. Only matches an object
6
+ ; literal built directly in the call, a literal string directly
7
+ ; visible in the source; a payload passed in as a variable is left
8
+ ; unresolved on purpose, since its type could be anything at runtime.
9
+ (call_expression
10
+ function: (member_expression
11
+ object: (identifier) @socket_object
12
+ property: (property_identifier) @send_method)
13
+ arguments: (arguments
14
+ (call_expression
15
+ function: (member_expression
16
+ object: (identifier) @_json_object
17
+ property: (property_identifier) @_stringify_method)
18
+ arguments: (arguments
19
+ (object
20
+ (pair
21
+ key: (property_identifier) @event_name_key
22
+ value: (string (string_fragment) @event_name))))))
23
+ (#eq? @send_method "send")
24
+ (#eq? @_json_object "JSON")
25
+ (#eq? @_stringify_method "stringify")
26
+ (#eq? @event_name_key "type"))
@@ -0,0 +1,20 @@
1
+ ; Matches the receiving side of raw-WebSocket "type" dispatch: either
2
+ ; `if (msg.type === "literal")` or `switch (msg.type) { case "literal": }`.
3
+ ; Only matches a literal string case/comparison value — a variable or
4
+ ; expression there is genuinely undecidable statically and is
5
+ ; intentionally left unmatched, same rule as every other resolver here.
6
+ (binary_expression
7
+ left: (member_expression
8
+ property: (property_identifier) @_type_prop)
9
+ operator: "==="
10
+ right: (string (string_fragment) @event_name)
11
+ (#eq? @_type_prop "type"))
12
+
13
+ (switch_statement
14
+ value: (parenthesized_expression
15
+ (member_expression
16
+ property: (property_identifier) @_type_prop))
17
+ body: (switch_body
18
+ (switch_case
19
+ value: (string (string_fragment) @event_name)))
20
+ (#eq? @_type_prop "type"))
@@ -0,0 +1,2 @@
1
+ (return_statement
2
+ (_) @return_value) @return_statement
@@ -0,0 +1,8 @@
1
+ (call_expression
2
+ function: (member_expression
3
+ object : (identifier) @socket_object
4
+ property: (property_identifier) @emit_method)
5
+ arguments: (arguments
6
+ (string (string_fragment) @event_name)
7
+ (_)*)
8
+ (#eq? @emit_method "emit"))
@@ -0,0 +1,19 @@
1
+ (call_expression
2
+ function: (member_expression
3
+ object: (identifier) @socket_object
4
+ property: (property_identifier) @on_method)
5
+ arguments: (arguments
6
+ (string (string_fragment) @event_name)
7
+ (_)*
8
+ [(arrow_function) (function_expression)] @handler_inline)
9
+ (#match? @on_method "^(on|addEventListener)$"))
10
+
11
+ (call_expression
12
+ function: (member_expression
13
+ object: (identifier) @socket_object
14
+ property: (property_identifier) @on_method)
15
+ arguments: (arguments
16
+ (string (string_fragment) @event_name)
17
+ (_)*
18
+ (identifier) @handler_ref)
19
+ (#match? @on_method "^(on|addEventListener)$"))
@@ -0,0 +1,15 @@
1
+ ; TypeScript grammar: also single-parameter arrows (x => x) and function expressions in const.
2
+ (variable_declarator
3
+ name: (identifier) @func_name
4
+ value: (arrow_function
5
+ parameters: [(formal_parameters) (identifier)] @func_params)) @func_declaration
6
+
7
+ (variable_declarator
8
+ name: (identifier) @func_name
9
+ value: (arrow_function
10
+ parameter: (identifier) @func_params)) @func_declaration
11
+
12
+ (variable_declarator
13
+ name: (identifier) @func_name
14
+ value: (function_expression
15
+ parameters: (formal_parameters) @func_params)) @func_declaration
@@ -0,0 +1,65 @@
1
+ ; TypeScript grammar: class names are type_identifier; abstract classes are their own node type; #private names and
2
+ ; property arrow functions (handler = async () => {}) are class members too.
3
+ (class_declaration
4
+ name: (type_identifier) @class_name
5
+ body: (class_body
6
+ (method_definition
7
+ name: [(property_identifier) (private_property_identifier)] @method_name
8
+ parameters: (formal_parameters) @method_params) @method_declaration))
9
+
10
+ (class_declaration
11
+ name: (type_identifier) @class_name
12
+ body: (class_body
13
+ (public_field_definition
14
+ name: [(property_identifier) (private_property_identifier)] @method_name
15
+ value: (arrow_function parameters: (formal_parameters) @method_params)) @method_declaration))
16
+
17
+ (class_declaration
18
+ name: (type_identifier) @class_name
19
+ body: (class_body
20
+ (public_field_definition
21
+ name: [(property_identifier) (private_property_identifier)] @method_name
22
+ value: (arrow_function parameter: (identifier) @method_params)) @method_declaration))
23
+
24
+ (class_declaration
25
+ name: (type_identifier) @class_name
26
+ body: (class_body
27
+ (public_field_definition
28
+ name: [(property_identifier) (private_property_identifier)] @method_name
29
+ value: (function_expression parameters: (formal_parameters) @method_params)) @method_declaration))
30
+
31
+ (abstract_class_declaration
32
+ name: (type_identifier) @class_name
33
+ body: (class_body
34
+ (method_definition
35
+ name: [(property_identifier) (private_property_identifier)] @method_name
36
+ parameters: (formal_parameters) @method_params) @method_declaration))
37
+
38
+ (abstract_class_declaration
39
+ name: (type_identifier) @class_name
40
+ body: (class_body
41
+ (public_field_definition
42
+ name: [(property_identifier) (private_property_identifier)] @method_name
43
+ value: (arrow_function parameters: (formal_parameters) @method_params)) @method_declaration))
44
+
45
+ (abstract_class_declaration
46
+ name: (type_identifier) @class_name
47
+ body: (class_body
48
+ (public_field_definition
49
+ name: [(property_identifier) (private_property_identifier)] @method_name
50
+ value: (arrow_function parameter: (identifier) @method_params)) @method_declaration))
51
+
52
+ (abstract_class_declaration
53
+ name: (type_identifier) @class_name
54
+ body: (class_body
55
+ (public_field_definition
56
+ name: [(property_identifier) (private_property_identifier)] @method_name
57
+ value: (function_expression parameters: (formal_parameters) @method_params)) @method_declaration))
58
+
59
+ ; export default class { method() {} } (anonymous: the extractor names it "default")
60
+ (export_statement
61
+ value: (class
62
+ body: (class_body
63
+ (method_definition
64
+ name: [(property_identifier) (private_property_identifier)] @method_name
65
+ parameters: (formal_parameters) @method_params) @method_declaration)))
@@ -0,0 +1,6 @@
1
+ ; export default function () {} export default async () => {} (anonymous: the extractor names the node "default")
2
+ (export_statement
3
+ value : [
4
+ (function_expression parameters : (formal_parameters) @func_params)
5
+ (arrow_function parameters : (formal_parameters) @func_params)
6
+ ] @func_declaration)
@@ -0,0 +1,16 @@
1
+ (export_statement
2
+ declaration : (function_declaration
3
+ name : (identifier) @exported_name))
4
+
5
+ (export_statement
6
+ declaration : (lexical_declaration
7
+ (variable_declarator
8
+ name : (identifier) @exported_name
9
+ value : [(arrow_function) (function_expression)])))
10
+
11
+ ; export = foo; export default foo;
12
+ (export_statement
13
+ (identifier) @exported_name)
14
+
15
+ (export_statement
16
+ value : (identifier) @exported_name)
@@ -0,0 +1,8 @@
1
+ ; TypeScript grammar: import x = require("./x") is an import_require_clause. Type-only imports are filtered in
2
+ ; extractImports (they have no runtime effect).
3
+ (import_statement
4
+ source : (string (string_fragment) @import_source))
5
+
6
+ (import_statement
7
+ (import_require_clause
8
+ source : (string (string_fragment) @import_source)))
package/package.json CHANGED
@@ -1,6 +1,93 @@
1
1
  {
2
2
  "name": "canopy-engine",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.2",
4
+ "type": "module",
5
+ "description": "Canopy: a map of what a JavaScript / TypeScript codebase does - entry points, call flows and services, drawn from the code itself.",
6
+ "keywords": [
7
+ "code-map",
8
+ "call-graph",
9
+ "javascript",
10
+ "typescript",
11
+ "static-analysis",
12
+ "code-visualization",
13
+ "architecture",
14
+ "workflow",
15
+ "microservices",
16
+ "developer-tools",
17
+ "cli"
18
+ ],
19
+ "homepage": "https://github.com/gouthamtg7/canopy-engine#readme",
20
+ "bugs": {
21
+ "url": "https://github.com/gouthamtg7/canopy-engine/issues"
22
+ },
23
+ "author": "",
24
+ "license": "SEE LICENSE IN LICENSE",
25
+ "bin": {
26
+ "canopy": "dist/canopy.js",
27
+ "canopy-engine": "dist/canopy.js"
28
+ },
29
+ "files": [
30
+ "dist/",
31
+ "vendor/",
32
+ "ui-react/index.html",
33
+ "ui-react/dist/",
34
+ "!ui-react/dist/*.map",
35
+ "README.md",
36
+ "FEEDBACK.md",
37
+ "LICENSE"
38
+ ],
39
+ "engines": {
40
+ "node": ">=18"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "scripts": {
46
+ "start": "node src/cli.js",
47
+ "test": "node test/regression.js && node test/ids.js && node test/bodyhash.js",
48
+ "build:ui": "node scripts/build-ui.js",
49
+ "snapshot": "node test/snapshot.js",
50
+ "audit:completeness": "node tools/auditCompleteness.js",
51
+ "census:calls": "node tools/censusCalls.js",
52
+ "test:ids": "node test/ids.js",
53
+ "test:bodyhash": "node test/bodyhash.js",
54
+ "test:sync": "node test/sync.js",
55
+ "test:ui": "node tools/uiTest.js",
56
+ "test:ts": "node test/ts.js",
57
+ "test:package": "node test/package.js",
58
+ "test:hook": "node test/hook.js",
59
+ "test:snapshots": "node test/snapshots.js",
60
+ "test:readme": "node test/readme.js",
61
+ "test:services": "node test/services.js",
62
+ "test:dogfood": "node test/dogfood.js",
63
+ "prepack": "node scripts/build-ui.js && node scripts/vendor-grammars.js && node scripts/build.js",
64
+ "test:installed-ui": "node test/installed-ui.js",
65
+ "test:clean-install": "node test/clean-install.js",
66
+ "test:journey": "node test/journey.js",
67
+ "test:bundle": "node test/bundle-neutral.js",
68
+ "build": "node scripts/build.js",
69
+ "test:no-network": "node test/no-network.js",
70
+ "test:crossservice": "node test/crossservice.js",
71
+ "test:env": "node test/envresolution.js",
72
+ "prepublishOnly": "node scripts/prepublish-check.js",
73
+ "test:prepublish": "node test/prepublish.js",
74
+ "test:update": "node test/update.js",
75
+ "test:stale": "node test/stale.js"
76
+ },
77
+ "dependencies": {
78
+ "web-tree-sitter": "0.26.12"
79
+ },
80
+ "devDependencies": {
81
+ "@xyflow/react": "^12.11.6",
82
+ "elkjs": "^0.12.0",
83
+ "esbuild": "^0.28.2",
84
+ "ignore": "7.0.6",
85
+ "playwright": "^1.63.0",
86
+ "react": "^18.3.1",
87
+ "react-dom": "^18.3.1",
88
+ "tree-sitter-html": "^0.23.2",
89
+ "tree-sitter-javascript": "^0.25.0",
90
+ "tree-sitter-typescript": "^0.23.2"
91
+ },
92
+ "canopyFeedbackUrl": "https://forms.gle/UX17FaE8HXLH8esB9"
93
+ }
@@ -0,0 +1,2 @@
1
+ :root{--paper: #f3efe3;--paper-raised: #faf7ee;--paper-hover: #eee7d4;--contour: #d9d0ba;--ink: #2b2a22;--ink-dim: #6b664f;--ink-faint: #a39a7d;--shadow-sm: 0 1px 2px rgba(40, 35, 20, .06), 0 2px 6px rgba(40, 35, 20, .05);--shadow-md: 0 2px 4px rgba(40, 35, 20, .07), 0 8px 20px rgba(40, 35, 20, .09);--shadow-lg: 0 4px 10px rgba(40, 35, 20, .1), 0 18px 38px rgba(40, 35, 20, .14);--accent-client: #4f7a5c;--accent-client-bg: #e4ecdf;--accent-server: #9a5b32;--accent-server-bg: #f1e2cf;--accent-unknown: #6b5a8f;--accent-unknown-bg: #e7e1ef;--accent-cross: #4a6480;--accent-cross-bg: #dfe6ec;--font-ui: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;--font-mono: "JetBrains Mono", "SF Mono", Consolas, monospace}*{box-sizing:border-box}html,body,#root{height:100%;margin:0}body{font-family:var(--font-ui);color:var(--ink);background:radial-gradient(ellipse 100% 70% at 50% -10%,#f8f4e8,#efe8d5 55%,#e4dabf);overflow:hidden}.canopy-app{height:100vh;display:flex;flex-direction:column}.canopy-topbar{display:flex;align-items:center;justify-content:space-between;padding:14px 22px;border-bottom:1px solid var(--contour);background:var(--paper-raised);box-shadow:0 1px #28231408,0 2px 10px #2823140d;z-index:10;position:relative}.canopy-brand{font-family:var(--font-ui);font-weight:700;font-size:16px;letter-spacing:-.01em;color:var(--ink);display:flex;align-items:center;gap:7px}.canopy-brand .leaf{color:var(--accent-client);font-size:15px}.canopy-crumbs{display:flex;align-items:center;gap:5px;font-size:13px}.crumb{padding:4px 9px;border-radius:6px;cursor:pointer;color:var(--ink-dim);font-weight:500;transition:background .15s ease,color .15s ease}.crumb:hover{background:var(--paper-hover);color:var(--ink)}.crumb.current{color:var(--ink);font-weight:700;cursor:default;font-size:14px}.crumb.current:hover{background:none}.crumb-sep{color:var(--ink-faint);font-size:11px}.canopy-hint{font-size:11.5px;color:var(--ink-faint);margin-top:1px;font-weight:500}.picker-entry-btn{font-family:var(--font-mono);font-size:11px;font-weight:600;color:var(--accent-cross);background:var(--accent-cross-bg);border:1px solid var(--accent-cross);border-radius:7px;padding:6px 11px;cursor:pointer;transition:background .15s ease}.picker-entry-btn:hover{background:var(--accent-cross);color:var(--paper-raised)}.canopy-stats{display:flex;gap:18px}.stat{text-align:right}.stat-num{font-size:17px;font-weight:700;line-height:1;color:var(--ink);font-variant-numeric:tabular-nums}.stat-label{font-size:9.5px;text-transform:uppercase;letter-spacing:.06em;color:var(--ink-faint);font-weight:600;margin-top:2px}.canopy-canvas{flex:1;position:relative}.react-flow__attribution{display:none}.react-flow__controls{box-shadow:var(--shadow-md);border-radius:10px;overflow:hidden;border:1px solid var(--contour)}.react-flow__controls-button{background:var(--paper-raised);border-bottom:1px solid var(--contour);color:var(--ink-dim);width:30px;height:30px}.react-flow__controls-button:hover{background:var(--paper-hover);color:var(--ink)}.react-flow__controls-button svg{fill:currentColor}.react-flow__minimap{border-radius:10px;box-shadow:var(--shadow-md);border:1px solid var(--contour);overflow:hidden}.group-bg{width:100%;height:100%;border-radius:20px;background:#96855414;border:1px dashed rgba(110,95,55,.18);pointer-events:none}.v-scrollbar-track{position:absolute;top:10px;bottom:30px;right:4px;width:10px;border-radius:6px;background:#b4a88a29;z-index:5;cursor:pointer}.v-scrollbar-thumb{position:absolute;left:0;width:100%;border-radius:6px;background:var(--ink-faint);opacity:.55;cursor:grab;transition:opacity .15s ease,background .15s ease}.v-scrollbar-thumb:hover{opacity:.85;background:var(--ink-dim)}.v-scrollbar-thumb:active{cursor:grabbing;opacity:1}.react-flow__edge-path{stroke-linecap:round}.react-flow__edge.same-service .react-flow__edge-path{stroke:var(--ink-dim);stroke-width:2px}.react-flow__edge.cross-service .react-flow__edge-path{stroke:var(--accent-cross);stroke-width:2.2px;stroke-dasharray:7 5}.react-flow__edge.service-link .react-flow__edge-path{stroke:var(--accent-cross);stroke-width:2.4px;stroke-dasharray:8 6}.card{background:var(--paper-raised);border-radius:12px;box-shadow:var(--shadow-sm);transition:box-shadow .18s ease,transform .18s ease;border:1px solid rgba(40,35,20,.06)}.card:hover{box-shadow:var(--shadow-lg);transform:translateY(-2px)}.svc-node{width:240px;padding:18px 20px;cursor:pointer}.svc-kind{display:inline-block;font-size:9.5px;font-weight:700;text-transform:uppercase;letter-spacing:.07em;padding:3px 9px;border-radius:5px}.svc-name{font-size:19px;font-weight:700;margin-top:10px;letter-spacing:-.01em;color:var(--ink)}.svc-meta{font-family:var(--font-mono);font-size:10.5px;color:var(--ink-dim);margin-top:6px}.svc-badges{display:flex;gap:6px;margin-top:11px;flex-wrap:wrap}.svc-badge{font-family:var(--font-mono);font-size:9px;font-weight:600;background:var(--accent-cross-bg);color:var(--accent-cross);border-radius:4px;padding:2px 7px}.wf-node{width:100%;height:100%;padding:14px 18px;cursor:pointer;display:flex;align-items:center;gap:14px}.wf-dot{width:9px;height:9px;border-radius:50%;flex-shrink:0}.wf-body{flex:1;min-width:0}.wf-name{font-size:14.5px;font-weight:650;color:var(--ink);line-height:1.3;overflow-wrap:break-word;word-break:normal}.wf-meta{font-family:var(--font-mono);font-size:10.5px;color:var(--ink-faint);margin-top:4px}.wf-chevron{color:var(--ink-faint);font-size:18px;flex-shrink:0}.minor-panel{position:absolute;left:50%;bottom:22px;transform:translate(-50%);z-index:15;width:460px;max-width:calc(100% - 44px);background:var(--paper-raised);border:1px solid var(--contour);border-radius:12px;box-shadow:var(--shadow-md);overflow:hidden}.minor-toggle{width:100%;border:none;background:none;cursor:pointer;padding:11px 18px;display:flex;align-items:center;justify-content:space-between;font-family:var(--font-mono);font-size:11.5px;font-weight:600;color:var(--ink-dim)}.minor-toggle:hover{color:var(--ink);background:var(--paper-hover)}.minor-caret{color:var(--ink-faint);font-size:10px}.minor-list{max-height:260px;overflow-y:auto;border-top:1px solid var(--contour)}.minor-item{padding:8px 18px;font-size:12px;color:var(--ink-dim);cursor:pointer;border-bottom:1px solid rgba(40,35,20,.05);display:flex;justify-content:space-between;gap:10px}.minor-item:last-child{border-bottom:none}.minor-item:hover{background:var(--paper-hover);color:var(--ink)}.minor-item .minor-file{font-family:var(--font-mono);font-size:9.5px;color:var(--ink-faint);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex-shrink:1}.context-rail{position:absolute;top:14px;left:10px;z-index:8;width:190px;max-height:calc(100% - 28px);overflow-y:auto;display:flex;flex-direction:column;gap:12px}.context-group{background:var(--paper-raised);border:1px solid var(--contour);border-radius:10px;padding:9px 10px;box-shadow:var(--shadow-sm)}.context-label{font-family:var(--font-mono);font-size:8.5px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--ink-faint);margin-bottom:6px}.context-chip{font-size:11px;font-weight:600;color:var(--ink);background:var(--paper);border:1px solid var(--contour);border-radius:6px;padding:5px 8px;margin-top:4px;cursor:pointer;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;transition:background .15s ease}.context-chip:first-child{margin-top:0}.context-chip:hover{background:var(--paper-hover)}.context-more{font-family:var(--font-mono);font-size:9.5px;color:var(--ink-faint);margin-top:4px}.step-node{width:100%;height:100%;min-height:90px;padding:18px;box-sizing:border-box;overflow:visible;position:relative}.step-node.root{border-color:var(--accent-client);box-shadow:0 0 0 1.5px var(--accent-client),var(--shadow-sm)}.step-node.active-expanded:not(.root){border-color:var(--accent-cross);box-shadow:0 0 0 1.5px var(--accent-cross),var(--shadow-sm)}.step-top{display:flex;align-items:center;gap:6px;flex-wrap:wrap}.step-badge{font-family:var(--font-mono);font-size:9px;font-weight:700;background:var(--ink);color:var(--paper);border-radius:4px;padding:1.5px 6px;flex-shrink:0}.step-cat{font-family:var(--font-mono);font-size:8px;font-weight:700;border-radius:4px;padding:1.5px 6px;flex-shrink:0;text-transform:uppercase;letter-spacing:.03em}.step-name{font-size:13px;font-weight:650;color:var(--ink);line-height:1.3;flex:1;min-width:60px;overflow-wrap:break-word;word-break:normal}.step-file{font-family:var(--font-mono);font-size:8.5px;color:var(--ink-faint);margin-top:5px;overflow-wrap:break-word;word-break:normal}.step-expand{margin-top:8px;font-family:var(--font-mono);font-size:10px;font-weight:600;border:1px solid var(--contour);background:var(--paper);color:var(--ink-dim);border-radius:6px;padding:3px 9px;cursor:pointer;transition:background .15s ease,color .15s ease,border-color .15s ease}.step-expand:hover{background:var(--paper-hover);color:var(--ink)}.step-expand.open{background:var(--ink);color:var(--paper);border-color:var(--ink)}.react-flow__edge.secondary-link .react-flow__edge-path{stroke:var(--ink-faint);stroke-dasharray:3 4;stroke-width:1.3}.source-icon{border:none;background:var(--paper);color:var(--ink-faint);font-family:var(--font-mono);font-size:9.5px;font-weight:700;border-radius:4px;padding:1px 5px;cursor:pointer;flex-shrink:0;margin-left:auto}.source-icon:hover{background:var(--ink);color:var(--paper)}.detail-source{font-family:var(--font-mono);font-size:11px;line-height:1.55;color:var(--ink);background:var(--paper);border:1px solid var(--contour);border-radius:8px;padding:10px 12px;margin-top:6px;white-space:pre-wrap;word-break:break-word;overflow-wrap:break-word;word-break:normal}.source-unavailable{color:var(--accent-server);font-style:italic}.recursive-badge{margin-top:7px;font-family:var(--font-mono);font-size:9.5px;font-weight:700;color:var(--accent-server);background:var(--accent-server-bg);border-radius:5px;padding:3px 7px;display:inline-block}.runtime-bucket{width:100%;padding:16px;border:1px dashed var(--contour);background:var(--paper);box-sizing:border-box;max-height:270px;overflow-y:auto}.runtime-bucket-header{font-family:var(--font-mono);font-size:8.5px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--ink-faint);padding:2px 8px 6px}.runtime-bucket-item{display:flex;flex-direction:column;align-items:flex-start;gap:3px;padding:6px 8px;font-size:10.5px;color:var(--ink-dim)}.runtime-bucket-item.inline{padding:5px 0}.runtime-bucket-item-row{display:flex;align-items:center;gap:6px;width:100%}.runtime-bucket-label{flex:1;min-width:60px;overflow-wrap:break-word;word-break:normal;font-family:var(--font-mono)}.runtime-tag{font-family:var(--font-mono);font-size:8px;font-weight:700;border-radius:4px;padding:1.5px 5px;flex-shrink:0;text-transform:uppercase;letter-spacing:.02em}.runtime-tag-unresolvable-call-site{background:var(--accent-unknown-bg);color:var(--accent-unknown)}.runtime-tag-runtime-dispatched{background:var(--accent-cross-bg);color:var(--accent-cross)}.runtime-tag-return-value{background:var(--accent-client-bg);color:var(--accent-client)}.runtime-bucket.nested-bucket{margin:0}.inline-call{width:100%;height:100%;padding:16px;display:flex;flex-direction:column;align-items:flex-start;gap:3px;border:1px solid var(--contour);background:var(--paper);box-sizing:border-box;font-size:10.5px;color:var(--ink-dim)}.branch-diagram{width:100%;padding:4px 10px;border:1.5px solid var(--accent-cross);box-sizing:border-box}.branch-diagram.nested{border-width:1px;box-shadow:none;padding:3px 8px}.branch-condition{display:flex;align-items:center;gap:7px;padding-bottom:2px;border-bottom:1px dashed var(--contour)}.branch-condition-kind{font-family:var(--font-mono);font-size:8px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;background:var(--accent-cross-bg);color:var(--accent-cross);border-radius:4px;padding:2px 6px;flex-shrink:0}.branch-condition-text{font-family:var(--font-mono);font-size:11.5px;font-weight:650;color:var(--ink);overflow-wrap:break-word;word-break:normal;flex:1;min-width:80px}.branch-outcomes{display:flex;flex-direction:row;align-items:flex-start;gap:18px;margin-top:4px;overflow-x:visible}.branch-outcome{display:flex;flex-direction:row;align-items:flex-start;gap:8px;min-width:150px;flex-shrink:0}.branch-arrow-row{display:flex;align-items:center;gap:5px;height:12px}.branch-arrow-svg{height:10px}.branch-arrow-label{font-family:var(--font-mono);font-size:9px;font-weight:700;color:var(--accent-cross);text-transform:uppercase;flex-shrink:0}.branch-chain-lane{display:flex;flex-direction:column;gap:3px;min-width:0}.branch-empty{font-family:var(--font-mono);font-size:10px;color:var(--ink-faint);font-style:italic;padding:4px 0}.step-body-contained{margin-top:14px;padding-top:12px;border-top:1px dashed var(--contour)}.chain-row{position:relative;min-width:0}.branch-step-chip{display:flex;align-items:center;gap:6px;background:var(--paper);border:1px solid var(--contour);border-radius:6px;padding:6px 9px;cursor:pointer;transition:background .15s ease,border-color .15s ease}.branch-step-chip:hover{background:var(--paper-hover)}.branch-step-chip.promoted{border-color:var(--accent-cross);border-style:dashed;background:var(--accent-cross-bg)}.branch-step-name{font-size:11.5px;font-weight:600;color:var(--ink);flex:1;min-width:40px;overflow-wrap:break-word;word-break:normal}.promoted-hint{font-family:var(--font-mono);font-size:8.5px;font-weight:600;color:var(--accent-cross);flex-shrink:0}.empty-state{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center;color:var(--ink-faint);font-family:var(--font-mono);font-size:12.5px;max-width:320px;line-height:1.6;pointer-events:none}.empty-state .glyph{font-size:26px;margin-bottom:10px;opacity:.55}.detail-panel{position:absolute;top:0;right:0;height:100%;width:320px;background:var(--paper-raised);border-left:1px solid var(--contour);box-shadow:-8px 0 26px #2823141f;padding:24px 22px;overflow-y:auto;transform:translate(0);animation:panelIn .22s cubic-bezier(.22,.9,.3,1) both;z-index:20}@keyframes panelIn{0%{transform:translate(16px);opacity:0}to{transform:translate(0);opacity:1}}.detail-close{position:absolute;top:16px;right:16px;border:none;background:none;cursor:pointer;font-size:15px;color:var(--ink-faint)}.detail-close:hover{color:var(--ink)}.detail-label{font-size:9.5px;text-transform:uppercase;letter-spacing:.07em;color:var(--ink-faint);font-weight:700;margin-top:18px;margin-bottom:5px}.detail-label:first-of-type{margin-top:8px}.detail-name{font-size:18px;font-weight:700;margin-top:20px;color:var(--ink)}.detail-mono{font-family:var(--font-mono);font-size:11.5px;color:var(--ink-dim);word-break:break-all;line-height:1.5}.detail-note{font-size:12.5px;color:var(--ink-dim);line-height:1.5}@keyframes nodeFadeIn{0%{opacity:0}to{opacity:1}}.react-flow__node{animation:nodeFadeIn .18s ease both}@keyframes cardIn{0%{transform:scale(.92) translateY(6px)}to{transform:scale(1) translateY(0)}}.react-flow__node .card{animation:cardIn .22s cubic-bezier(.22,.9,.3,1) both}.inferred-badge{display:inline-block;margin-left:8px;padding:1px 6px;border-radius:8px;font-size:9px;font-weight:600;letter-spacing:.02em;text-transform:uppercase;background:#ece6d2;color:#7a7355;border:1px dashed #b8ae8a;vertical-align:middle}.runtime-bucket-item.compact{flex-direction:row;align-items:center;gap:8px;height:24px;padding:0 2px;box-sizing:border-box;cursor:pointer}.runtime-bucket-item.compact:hover{background:#00000009}.compact-dot{width:7px;height:7px;padding:0;border-radius:50%;flex:0 0 7px}.compact-label{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0;max-width:420px;font-size:10.5px}.branch-arrow-label{line-height:24px;min-width:42px}.changes-btn{border:1px solid var(--contour);background:var(--paper-raised);color:var(--ink);font-family:var(--font-mono);font-size:11px;font-weight:700;border-radius:8px;padding:6px 12px;cursor:pointer}.changes-btn.active,.changes-btn:hover{background:var(--ink);color:var(--paper)}.changes-btn-count{margin-left:7px;background:#c8892c;color:#fff;border-radius:9px;padding:0 6px;font-size:10px}.changes-panel{position:fixed;top:76px;right:12px;bottom:12px;width:440px;z-index:30;overflow-y:auto;background:var(--paper-raised);border:1px solid var(--contour);border-radius:12px;box-shadow:0 10px 40px #3c321438;padding:14px 16px 18px}.chg-empty{font-size:14px;font-weight:600;color:var(--ink);margin:14px 0 8px}.chg-stepper{display:flex;align-items:center;gap:10px;margin:8px 0 10px}.chg-stepper button{width:30px;height:30px;border-radius:8px;border:1px solid var(--contour);background:var(--paper);font-size:18px;cursor:pointer;color:var(--ink)}.chg-stepper button:disabled{opacity:.3;cursor:default}.chg-ver{flex:1;min-width:0;text-align:center}.chg-ver-title{font-family:var(--font-mono);font-weight:700;font-size:14px;color:var(--ink)}.chg-ver-sha{font-weight:500;color:var(--ink-faint);font-size:11px;margin-left:4px}.chg-dirty{color:#c8892c}.chg-ver-msg{font-size:12px;color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.chg-ver-meta{font-size:10px;color:var(--ink-faint);font-family:var(--font-mono)}.chg-summary{display:flex;flex-direction:column;gap:3px;font-family:var(--font-mono);font-size:11px;color:var(--ink-dim);padding:8px 10px;background:var(--paper);border-radius:8px;margin-bottom:10px}.chg-summary b{color:var(--ink)}.chg-section{margin-top:8px}.chg-section-head{width:100%;display:flex;align-items:center;gap:8px;border:none;background:none;cursor:pointer;font-family:var(--font-mono);font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--ink-dim);padding:4px 0;border-bottom:1px dashed var(--contour)}.chg-section-head .chg-count{background:var(--paper);border-radius:8px;padding:0 6px;color:var(--ink)}.chg-section-head .chg-caret{margin-left:auto}.chg-section-body{padding-top:4px}.chg-wf{padding:6px 8px;border-radius:8px;cursor:pointer;border:1px solid transparent}.chg-wf:hover{background:var(--paper);border-color:var(--contour)}.chg-wf.gone{cursor:default;opacity:.6}.chg-wf-name{font-size:12px;font-weight:650;color:var(--ink);display:flex;align-items:center;gap:6px;flex-wrap:wrap}.chg-wf-why{font-family:var(--font-mono);font-size:10px;color:var(--ink-dim);margin-top:2px;overflow-wrap:anywhere}.chg-svc{font-family:var(--font-mono);font-size:9px;color:var(--ink-faint);margin-left:auto}.chg-tag{font-family:var(--font-mono);font-size:8px;font-weight:700;text-transform:uppercase;border-radius:4px;padding:1px 5px;background:#ece6d2;color:#7a7355}.chg-tag-new{background:#dcefd9;color:#2f6a2a}.chg-tag-removed{background:#f3d9d4;color:#8a3326}.chg-tag-changed{background:#f3e6c4;color:#85601a}.chg-row{font-family:var(--font-mono);font-size:10.5px;color:var(--ink);padding:2px 4px;overflow-wrap:anywhere}.chg-row .chg-sign{display:inline-block;width:14px;font-weight:700}.chg-row.add .chg-sign{color:#2f6a2a}.chg-row.del .chg-sign{color:#8a3326}.chg-row.mod .chg-sign{color:#85601a}.chg-row.mv .chg-sign{color:#4a6480}.chg-file{display:block;margin-left:14px;color:var(--ink-faint);font-size:9.5px}.chg-more,.chg-info{font-size:10px;color:var(--ink-faint);padding:4px}.chg-info{margin-top:12px;border-top:1px dashed var(--contour);padding-top:8px}.change-badge{font-family:var(--font-mono);font-size:8px;font-weight:800;letter-spacing:.04em;border-radius:4px;padding:1px 5px;margin-left:6px;flex-shrink:0}.change-badge-new{background:#2f8a3a;color:#fff}.change-badge-modified{background:#c8892c;color:#fff}.canopy-topbar{flex-wrap:wrap;gap:8px 18px;align-items:flex-start;padding:10px 16px}.topbar-title{flex:1 1 280px;min-width:0}.topbar-controls{display:flex;flex-wrap:wrap;align-items:center;justify-content:flex-end;gap:8px 10px;margin-left:auto;min-width:0}.tb-group{display:inline-flex;gap:4px}.tb-btn{font-family:var(--font-mono);font-size:11px;font-weight:700;color:var(--ink);background:var(--paper-raised);border:1px solid var(--contour);border-radius:7px;padding:6px 10px;min-width:30px;cursor:pointer;white-space:nowrap;transition:background .12s ease}.tb-btn:hover:not(:disabled){background:var(--paper-hover)}.tb-btn.active{background:var(--ink);color:var(--paper)}.tb-btn:disabled{opacity:.4;cursor:default}.changes-btn{white-space:nowrap}.picker-entry-btn{white-space:nowrap;max-width:100%;overflow:hidden;text-overflow:ellipsis}@media(max-width:1100px){.canopy-stats{display:none}}@media(max-width:1000px){.canopy-hint{display:none}}.canopy-body{flex:1;display:flex;min-height:0;position:relative}.canopy-canvas{flex:1;position:relative;min-width:0}.side-panel{background:var(--paper-raised);border-right:1px solid var(--contour);min-height:0}.side-panel.docked{flex:0 0 260px;width:260px;position:relative;display:flex;flex-direction:column;transition:flex-basis .15s ease,width .15s ease}.side-panel.docked.collapsed{flex-basis:30px;width:30px}.side-scroll{overflow-y:auto;flex:1;padding:10px 12px 14px 10px;display:flex;flex-direction:column;gap:10px;min-height:0}.side-title{font-family:var(--font-mono);font-size:9.5px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--ink-faint);padding-right:26px;display:flex;justify-content:space-between;align-items:center}.side-handle{position:absolute;top:6px;right:4px;width:22px;height:22px;border-radius:6px;border:1px solid var(--contour);background:var(--paper);color:var(--ink-dim);cursor:pointer;z-index:2;line-height:1;font-size:14px;padding:0}.side-panel.docked.collapsed .side-handle{right:3px}.side-handle:hover{background:var(--paper-hover)}.side-panel.drawer{position:absolute;top:0;bottom:0;left:0;width:270px;max-width:86%;z-index:26;display:flex;flex-direction:column;transform:translate(-104%);visibility:hidden;transition:transform .18s ease,visibility 0s linear .18s;box-shadow:var(--shadow-lg)}.side-panel.drawer.open{transform:none;visibility:visible;transition:transform .18s ease}.side-scrim{position:absolute;inset:0;z-index:25;background:#2823142e}.side-close{border:none;background:none;cursor:pointer;color:var(--ink-dim);font-size:13px;padding:0 2px}.side-groups{display:flex;flex-direction:column;gap:12px}.canopy-edge-path{fill:none;stroke:var(--ink-dim);stroke-width:2px;stroke-linecap:round;stroke-linejoin:round;transition:opacity .12s ease,stroke-width .12s ease}.react-flow__edge.cross-service .canopy-edge-path{stroke:var(--accent-cross);stroke-dasharray:7 5}.canopy-edge-path.hl{stroke:#c8892c;stroke-width:3px;stroke-dasharray:none}.canopy-edge-path.dim{opacity:.14}.v-scrollbar-track.minimap-open{bottom:196px}.h-scrollbar-track{position:absolute;left:10px;right:30px;bottom:4px;height:10px;border-radius:6px;background:#b4a88a29;z-index:5;cursor:pointer}.h-scrollbar-track.minimap-open{right:230px}.h-scrollbar-thumb{position:absolute;top:0;height:100%;border-radius:6px;background:var(--ink-faint);opacity:.55;cursor:grab;transition:opacity .15s ease,background .15s ease}.h-scrollbar-thumb:hover{opacity:.85;background:var(--ink-dim)}.h-scrollbar-thumb:active{cursor:grabbing;opacity:1}.react-flow__minimap{margin:14px 14px 18px}.step-expand.step-toggle{width:22px;height:22px;min-width:22px;padding:0;margin:0;display:inline-flex;align-items:center;justify-content:center;border-radius:6px;font-size:12px;line-height:1;border:1px solid var(--contour);background:var(--paper);color:var(--ink-dim);cursor:pointer;flex:0 0 22px}.step-expand.step-toggle:hover{background:var(--paper-hover);color:var(--ink)}.step-expand.step-toggle.open{background:var(--paper);color:var(--ink)}.chip-more{margin-left:auto;font-family:var(--font-mono);font-size:9px;font-weight:700;color:var(--accent-cross);background:var(--accent-cross-bg);border-radius:8px;padding:0 6px}.seg{display:inline-flex;border:1px solid var(--contour);border-radius:8px;overflow:hidden;width:100%}.seg button{flex:1;border:none;background:var(--paper);color:var(--ink-dim);font-family:var(--font-mono);font-size:10.5px;font-weight:700;padding:6px 8px;cursor:pointer}.seg button+button{border-left:1px solid var(--contour)}.seg button.on{background:var(--ink);color:var(--paper)}.step-node.selected-entry{border-color:var(--accent-cross);box-shadow:0 0 0 2px var(--accent-cross),var(--shadow-md)}.step-node.parent-card{border-style:dashed;background:var(--paper)}.caller-tag{font-family:var(--font-mono);font-size:8px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;color:var(--accent-cross);background:var(--accent-cross-bg);border-radius:4px;padding:1px 5px}.excerpt-gap{font-family:var(--font-mono);font-size:9.5px;color:var(--ink-faint);padding:2px 4px;height:20px;line-height:16px}.branch-step-chip.call-target{background:var(--accent-cross-bg);border-color:var(--accent-cross);border-style:solid;box-shadow:0 0 0 1.5px var(--accent-cross)}.context-chip-node{display:flex;align-items:center;justify-content:center;width:100%;height:100%;border:1px dashed var(--accent-cross);background:var(--accent-cross-bg);box-shadow:none;padding:0 12px}.context-chip-text{font-family:var(--font-mono);font-size:11px;font-weight:700;color:var(--accent-cross)}.trigger-node{display:flex;align-items:center;gap:8px;width:100%;height:100%;padding:0 12px;background:var(--paper-raised);border:1px solid var(--accent-cross)}.trigger-node.inferred{border-style:dashed;color:var(--ink-dim)}.trigger-kind{font-family:var(--font-mono);font-size:8.5px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;background:var(--accent-cross-bg);color:var(--accent-cross);border-radius:4px;padding:2px 6px;flex-shrink:0}.trigger-label{font-family:var(--font-mono);font-size:11px;font-weight:600;color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}[data-lod=compact] .step-node .step-file,[data-lod=compact] .step-node .step-cat,[data-lod=compact] .step-node .source-icon,[data-lod=compact] .step-node .change-badge,[data-lod=compact] .step-node .caller-tag,[data-lod=compact] .step-node .chip-more,[data-lod=compact] .step-node .promoted-hint,[data-lod=compact] .step-node .excerpt-gap,[data-lod=compact] .step-node .branch-condition-text,[data-lod=compact] .step-node .branch-condition-kind,[data-lod=compact] .step-node .branch-arrow-label,[data-lod=compact] .step-node .branch-step-name,[data-lod=compact] .step-node .compact-label,[data-lod=compact] .step-node .compact-dot,[data-lod=compact] .step-node .runtime-bucket-header,[data-lod=compact] .step-node .runtime-tag{visibility:hidden}[data-lod=compact] .step-node .branch-step-chip{background:var(--accent-cross-bg);border-color:var(--accent-cross)}[data-lod=compact] .step-node .branch-step-chip.promoted{background:var(--accent-cross)}[data-lod=compact] .step-node .runtime-bucket-item.compact{background:var(--accent-unknown-bg);border-radius:4px}[data-lod=compact] .step-node .runtime-bucket-item.compact:has(.runtime-tag-runtime-dispatched){background:var(--accent-cross-bg)}[data-lod=compact] .step-node .runtime-bucket-item.compact:has(.runtime-tag-return-value){background:var(--accent-client-bg)}[data-lod=compact] .step-node .branch-diagram{background:var(--paper)}[data-lod=compact] .step-node .step-name,[data-lod=boxes] .step-node .step-name{transform:scale(var(--inv-zoom, 1));transform-origin:left center;max-width:calc(100% / var(--inv-zoom, 1));white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:block;font-weight:700;position:relative;z-index:2}[data-lod=boxes] .step-node .step-body-contained,[data-lod=boxes] .step-node .step-file,[data-lod=boxes] .step-node .step-cat,[data-lod=boxes] .step-node .source-icon,[data-lod=boxes] .step-node .change-badge,[data-lod=boxes] .step-node .caller-tag,[data-lod=boxes] .step-node .step-badge,[data-lod=boxes] .step-node .step-toggle{visibility:hidden}[data-lod=boxes] .step-node{background:var(--paper-raised)}.back-btn{margin-right:4px;padding:4px 9px}.side-sub{font-size:11.5px;line-height:1.4;color:var(--ink-dim);margin:-4px 0 2px;padding-right:6px}.react-flow__edge.hop-solid .canopy-edge-path{stroke-dasharray:none}.react-flow__edge.hop-dashed .canopy-edge-path{stroke-dasharray:7 5}.react-flow__edge.hop-solid .canopy-edge-path:not(.hl),.react-flow__edge.hop-dashed .canopy-edge-path:not(.hl){stroke:var(--accent-cross);stroke-width:2.2px}.hop-node{width:100%;height:100%;padding:7px 10px 8px;display:flex;flex-direction:column;gap:2px;cursor:pointer;background:var(--accent-cross-bg);border:1.5px solid var(--accent-cross);overflow:hidden}.hop-node.hop-dashed{border-style:dashed}.hop-top{display:flex;align-items:center;justify-content:space-between;gap:6px}.hop-svc{font-family:var(--font-mono);font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.03em;color:var(--accent-cross);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}.hop-tag{font-family:var(--font-mono);font-size:8px;font-weight:700;text-transform:uppercase;border-radius:4px;padding:1px 5px;background:var(--paper-raised);color:var(--ink-dim);flex-shrink:0}.hop-tag-exact,.hop-tag-resolved{color:var(--accent-client)}.hop-call{font-family:var(--font-mono);font-size:10.5px;font-weight:700;color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.hop-handler{font-family:var(--font-mono);font-size:9.5px;color:var(--ink-dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-height:12px}.hop-view{align-self:flex-start;margin-top:auto;border:1px solid var(--accent-cross);background:var(--paper-raised);color:var(--accent-cross);border-radius:6px;font-size:10px;font-weight:600;padding:2px 8px;cursor:pointer}.hop-view:hover{background:var(--paper-hover)}.called-from-chip{display:block;max-width:100%;margin-top:4px;border:1px solid var(--accent-cross);background:var(--accent-cross-bg);color:var(--accent-cross);border-radius:999px;font-family:var(--font-mono);font-size:9.5px;font-weight:600;padding:2px 9px;cursor:pointer;text-align:left;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.called-from-chip:hover{background:var(--paper-hover)}.hop-chooser{position:fixed;z-index:200;width:284px;background:var(--paper-raised);border:1px solid var(--contour);border-radius:10px;box-shadow:var(--shadow-lg);padding:6px;display:flex;flex-direction:column;gap:2px}.hop-chooser-label{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--ink-faint);padding:2px 6px 4px}.hop-chooser-item{display:flex;flex-direction:column;align-items:flex-start;text-align:left;border:none;background:none;border-radius:6px;padding:5px 8px;cursor:pointer}.hop-chooser-item:hover{background:var(--paper-hover)}.hop-chooser-title{font-family:var(--font-mono);font-size:11px;font-weight:600;color:var(--ink);max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.hop-chooser-sub{font-size:10px;color:var(--ink-dim)}.hop-chooser-more{font-size:10.5px;color:var(--ink-dim);padding:3px 8px;font-style:italic}[data-lod=compact] .hop-node .hop-handler,[data-lod=compact] .hop-node .hop-view,[data-lod=compact] .hop-node .hop-tag,[data-lod=boxes] .hop-node .hop-handler,[data-lod=boxes] .hop-node .hop-view,[data-lod=boxes] .hop-node .hop-tag,[data-lod=boxes] .hop-node .hop-call,[data-lod=boxes] .called-from-chip{visibility:hidden}.react-flow{direction:ltr;--xy-edge-stroke-default: #b1b1b7;--xy-edge-stroke-width-default: 1;--xy-edge-stroke-selected-default: #555;--xy-connectionline-stroke-default: #b1b1b7;--xy-connectionline-stroke-width-default: 1;--xy-attribution-background-color-default: rgba(255, 255, 255, .5);--xy-minimap-background-color-default: #fff;--xy-minimap-mask-background-color-default: rgba(240, 240, 240, .6);--xy-minimap-mask-stroke-color-default: transparent;--xy-minimap-mask-stroke-width-default: 1;--xy-minimap-node-background-color-default: #e2e2e2;--xy-minimap-node-stroke-color-default: transparent;--xy-minimap-node-stroke-width-default: 2;--xy-background-color-default: transparent;--xy-background-pattern-dots-color-default: #91919a;--xy-background-pattern-lines-color-default: #eee;--xy-background-pattern-cross-color-default: #e2e2e2;background-color:var(--xy-background-color, var(--xy-background-color-default));--xy-node-color-default: inherit;--xy-node-border-default: 1px solid #1a192b;--xy-node-background-color-default: #fff;--xy-node-group-background-color-default: rgba(240, 240, 240, .25);--xy-node-boxshadow-hover-default: 0 1px 4px 1px rgba(0, 0, 0, .08);--xy-node-boxshadow-selected-default: 0 0 0 .5px #1a192b;--xy-node-border-radius-default: 3px;--xy-handle-background-color-default: #1a192b;--xy-handle-border-color-default: #fff;--xy-selection-background-color-default: rgba(0, 89, 220, .08);--xy-selection-border-default: 1px dotted rgba(0, 89, 220, .8);--xy-controls-button-background-color-default: #fefefe;--xy-controls-button-background-color-hover-default: #f4f4f4;--xy-controls-button-color-default: inherit;--xy-controls-button-color-hover-default: inherit;--xy-controls-button-border-color-default: #eee;--xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, .08);--xy-edge-label-background-color-default: #ffffff;--xy-edge-label-color-default: inherit;--xy-resize-background-color-default: #3367d9}.react-flow.dark{--xy-edge-stroke-default: #3e3e3e;--xy-edge-stroke-width-default: 1;--xy-edge-stroke-selected-default: #727272;--xy-connectionline-stroke-default: #b1b1b7;--xy-connectionline-stroke-width-default: 1;--xy-attribution-background-color-default: rgba(150, 150, 150, .25);--xy-minimap-background-color-default: #141414;--xy-minimap-mask-background-color-default: rgba(60, 60, 60, .6);--xy-minimap-mask-stroke-color-default: transparent;--xy-minimap-mask-stroke-width-default: 1;--xy-minimap-node-background-color-default: #2b2b2b;--xy-minimap-node-stroke-color-default: transparent;--xy-minimap-node-stroke-width-default: 2;--xy-background-color-default: #141414;--xy-background-pattern-dots-color-default: #555;--xy-background-pattern-lines-color-default: #333;--xy-background-pattern-cross-color-default: #333;--xy-node-color-default: #f8f8f8;--xy-node-border-default: 1px solid #3c3c3c;--xy-node-background-color-default: #1e1e1e;--xy-node-group-background-color-default: rgba(240, 240, 240, .25);--xy-node-boxshadow-hover-default: 0 1px 4px 1px rgba(255, 255, 255, .08);--xy-node-boxshadow-selected-default: 0 0 0 .5px #999;--xy-handle-background-color-default: #bebebe;--xy-handle-border-color-default: #1e1e1e;--xy-selection-background-color-default: rgba(200, 200, 220, .08);--xy-selection-border-default: 1px dotted rgba(200, 200, 220, .8);--xy-controls-button-background-color-default: #2b2b2b;--xy-controls-button-background-color-hover-default: #3e3e3e;--xy-controls-button-color-default: #f8f8f8;--xy-controls-button-color-hover-default: #fff;--xy-controls-button-border-color-default: #5b5b5b;--xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, .08);--xy-edge-label-background-color-default: #141414;--xy-edge-label-color-default: #f8f8f8}.react-flow__background{background-color:var(--xy-background-color-props, var(--xy-background-color, var(--xy-background-color-default)));pointer-events:none;z-index:-1}.react-flow__container{position:absolute;width:100%;height:100%;top:0;left:0}.react-flow__pane{z-index:1;touch-action:none}.react-flow__pane.draggable{cursor:grab}.react-flow__pane.dragging{cursor:grabbing}.react-flow__pane.selection{cursor:pointer}.react-flow__viewport{transform-origin:0 0;z-index:2;pointer-events:none}.react-flow__renderer{z-index:4}.react-flow__selection{z-index:6}.react-flow__nodesselection-rect:focus,.react-flow__nodesselection-rect:focus-visible{outline:none}.react-flow__edge-path{stroke:var(--xy-edge-stroke, var(--xy-edge-stroke-default));stroke-width:var(--xy-edge-stroke-width, var(--xy-edge-stroke-width-default));fill:none}.react-flow__connection-path{stroke:var(--xy-connectionline-stroke, var(--xy-connectionline-stroke-default));stroke-width:var(--xy-connectionline-stroke-width, var(--xy-connectionline-stroke-width-default));fill:none}.react-flow .react-flow__edges{position:absolute}.react-flow .react-flow__edges svg{overflow:visible;position:absolute;pointer-events:none}.react-flow__edge{pointer-events:visibleStroke}.react-flow__edge.selectable{cursor:pointer}.react-flow__edge.animated path{stroke-dasharray:5;animation:dashdraw .5s linear infinite}.react-flow__edge.animated path.react-flow__edge-interaction{stroke-dasharray:none;animation:none}.react-flow__edge.inactive{pointer-events:none}.react-flow__edge.selected,.react-flow__edge:focus,.react-flow__edge:focus-visible{outline:none}.react-flow__edge.selected .react-flow__edge-path,.react-flow__edge.selectable:focus .react-flow__edge-path,.react-flow__edge.selectable:focus-visible .react-flow__edge-path{stroke:var(--xy-edge-stroke-selected, var(--xy-edge-stroke-selected-default))}.react-flow__edge-textwrapper{pointer-events:all}.react-flow__edge .react-flow__edge-text{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__arrowhead polyline{stroke:var(--xy-edge-stroke, var(--xy-edge-stroke-default))}.react-flow__arrowhead polyline.arrowclosed{fill:var(--xy-edge-stroke, var(--xy-edge-stroke-default))}.react-flow__connection{pointer-events:none}.react-flow__connection .animated{stroke-dasharray:5;animation:dashdraw .5s linear infinite}svg.react-flow__connectionline{z-index:1001;overflow:visible;position:absolute}.react-flow__nodes{pointer-events:none;transform-origin:0 0}.react-flow__node{position:absolute;-webkit-user-select:none;-moz-user-select:none;user-select:none;pointer-events:all;transform-origin:0 0;box-sizing:border-box;cursor:default}.react-flow__node.selectable{cursor:pointer}.react-flow__node.draggable{cursor:grab;pointer-events:all}.react-flow__node.draggable.dragging{cursor:grabbing}.react-flow__nodesselection{z-index:3;transform-origin:left top;pointer-events:none}.react-flow__nodesselection-rect{position:absolute;pointer-events:all;cursor:grab}.react-flow__handle{position:absolute;pointer-events:none;min-width:5px;min-height:5px;width:6px;height:6px;background-color:var(--xy-handle-background-color, var(--xy-handle-background-color-default));border:1px solid var(--xy-handle-border-color, var(--xy-handle-border-color-default));border-radius:100%}.react-flow__handle.connectingfrom{pointer-events:all}.react-flow__handle.connectionindicator{pointer-events:all;cursor:crosshair}.react-flow__handle-bottom{top:auto;left:50%;bottom:0;transform:translate(-50%,50%)}.react-flow__handle-top{top:0;left:50%;transform:translate(-50%,-50%)}.react-flow__handle-left{top:50%;left:0;transform:translate(-50%,-50%)}.react-flow__handle-right{top:50%;right:0;transform:translate(50%,-50%)}.react-flow__edgeupdater{cursor:move;pointer-events:all}.react-flow__pane.selection .react-flow__panel{pointer-events:none}.react-flow__panel{position:absolute;z-index:5;margin:15px}.react-flow__panel.top{top:0}.react-flow__panel.bottom{bottom:0}.react-flow__panel.top.center,.react-flow__panel.bottom.center{left:50%;transform:translate(-15px) translate(-50%)}.react-flow__panel.left{left:0}.react-flow__panel.right{right:0}.react-flow__panel.left.center,.react-flow__panel.right.center{top:50%;transform:translateY(-15px) translateY(-50%)}.react-flow__attribution{font-size:10px;background:var(--xy-attribution-background-color, var(--xy-attribution-background-color-default));padding:2px 3px;margin:0}.react-flow__attribution a{text-decoration:none;color:#999}@keyframes dashdraw{0%{stroke-dashoffset:10}}.react-flow__edgelabel-renderer{position:absolute;width:100%;height:100%;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;left:0;top:0}.react-flow__viewport-portal{position:absolute;width:100%;height:100%;left:0;top:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__minimap{background:var( --xy-minimap-background-color-props, var(--xy-minimap-background-color, var(--xy-minimap-background-color-default)) )}.react-flow__minimap-svg{display:block}.react-flow__minimap-mask{fill:var( --xy-minimap-mask-background-color-props, var(--xy-minimap-mask-background-color, var(--xy-minimap-mask-background-color-default)) );stroke:var( --xy-minimap-mask-stroke-color-props, var(--xy-minimap-mask-stroke-color, var(--xy-minimap-mask-stroke-color-default)) );stroke-width:var( --xy-minimap-mask-stroke-width-props, var(--xy-minimap-mask-stroke-width, var(--xy-minimap-mask-stroke-width-default)) )}.react-flow__minimap-node{fill:var( --xy-minimap-node-background-color-props, var(--xy-minimap-node-background-color, var(--xy-minimap-node-background-color-default)) );stroke:var( --xy-minimap-node-stroke-color-props, var(--xy-minimap-node-stroke-color, var(--xy-minimap-node-stroke-color-default)) );stroke-width:var( --xy-minimap-node-stroke-width-props, var(--xy-minimap-node-stroke-width, var(--xy-minimap-node-stroke-width-default)) )}.react-flow__background-pattern.dots{fill:var( --xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-dots-color-default)) )}.react-flow__background-pattern.lines{stroke:var( --xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-lines-color-default)) )}.react-flow__background-pattern.cross{stroke:var( --xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-cross-color-default)) )}.react-flow__controls{display:flex;flex-direction:column;box-shadow:var(--xy-controls-box-shadow, var(--xy-controls-box-shadow-default))}.react-flow__controls.horizontal{flex-direction:row}.react-flow__controls-button{display:flex;justify-content:center;align-items:center;height:26px;width:26px;padding:4px;border:none;background:var(--xy-controls-button-background-color, var(--xy-controls-button-background-color-default));border-bottom:1px solid var( --xy-controls-button-border-color-props, var(--xy-controls-button-border-color, var(--xy-controls-button-border-color-default)) );color:var( --xy-controls-button-color-props, var(--xy-controls-button-color, var(--xy-controls-button-color-default)) );cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__controls-button svg{width:100%;max-width:12px;max-height:12px;fill:currentColor}.react-flow__edge.updating .react-flow__edge-path{stroke:#777}.react-flow__edge-text{font-size:10px}.react-flow__node.selectable:focus,.react-flow__node.selectable:focus-visible{outline:none}.react-flow__node-input,.react-flow__node-default,.react-flow__node-output,.react-flow__node-group{padding:10px;border-radius:var(--xy-node-border-radius, var(--xy-node-border-radius-default));width:150px;font-size:12px;color:var(--xy-node-color, var(--xy-node-color-default));text-align:center;border:var(--xy-node-border, var(--xy-node-border-default));background-color:var(--xy-node-background-color, var(--xy-node-background-color-default))}.react-flow__node-input.selectable:hover,.react-flow__node-default.selectable:hover,.react-flow__node-output.selectable:hover,.react-flow__node-group.selectable:hover{box-shadow:var(--xy-node-boxshadow-hover, var(--xy-node-boxshadow-hover-default))}.react-flow__node-input.selectable.selected,.react-flow__node-input.selectable:focus,.react-flow__node-input.selectable:focus-visible,.react-flow__node-default.selectable.selected,.react-flow__node-default.selectable:focus,.react-flow__node-default.selectable:focus-visible,.react-flow__node-output.selectable.selected,.react-flow__node-output.selectable:focus,.react-flow__node-output.selectable:focus-visible,.react-flow__node-group.selectable.selected,.react-flow__node-group.selectable:focus,.react-flow__node-group.selectable:focus-visible{box-shadow:var(--xy-node-boxshadow-selected, var(--xy-node-boxshadow-selected-default))}.react-flow__node-group{background-color:var(--xy-node-group-background-color, var(--xy-node-group-background-color-default))}.react-flow__nodesselection-rect,.react-flow__selection{background:var(--xy-selection-background-color, var(--xy-selection-background-color-default));border:var(--xy-selection-border, var(--xy-selection-border-default))}.react-flow__nodesselection-rect:focus,.react-flow__nodesselection-rect:focus-visible,.react-flow__selection:focus,.react-flow__selection:focus-visible{outline:none}.react-flow__controls-button:hover{background:var( --xy-controls-button-background-color-hover-props, var(--xy-controls-button-background-color-hover, var(--xy-controls-button-background-color-hover-default)) );color:var( --xy-controls-button-color-hover-props, var(--xy-controls-button-color-hover, var(--xy-controls-button-color-hover-default)) )}.react-flow__controls-button:disabled{pointer-events:none}.react-flow__controls-button:disabled svg{fill-opacity:.4}.react-flow__controls-button:last-child{border-bottom:none}.react-flow__controls.horizontal .react-flow__controls-button{border-bottom:none;border-right:1px solid var( --xy-controls-button-border-color-props, var(--xy-controls-button-border-color, var(--xy-controls-button-border-color-default)) )}.react-flow__controls.horizontal .react-flow__controls-button:last-child{border-right:none}.react-flow__resize-control{position:absolute}.react-flow__resize-control.left,.react-flow__resize-control.right{cursor:ew-resize}.react-flow__resize-control.top,.react-flow__resize-control.bottom{cursor:ns-resize}.react-flow__resize-control.top.left,.react-flow__resize-control.bottom.right{cursor:nwse-resize}.react-flow__resize-control.bottom.left,.react-flow__resize-control.top.right{cursor:nesw-resize}.react-flow__resize-control.handle{width:5px;height:5px;border:1px solid #fff;border-radius:1px;background-color:var(--xy-resize-background-color, var(--xy-resize-background-color-default));translate:-50% -50%}.react-flow__resize-control.handle.left{left:0;top:50%}.react-flow__resize-control.handle.right{left:100%;top:50%}.react-flow__resize-control.handle.top{left:50%;top:0}.react-flow__resize-control.handle.bottom{left:50%;top:100%}.react-flow__resize-control.handle.top.left,.react-flow__resize-control.handle.bottom.left{left:0}.react-flow__resize-control.handle.top.right,.react-flow__resize-control.handle.bottom.right{left:100%}.react-flow__resize-control.line{border-color:var(--xy-resize-background-color, var(--xy-resize-background-color-default));border-width:0;border-style:solid}.react-flow__resize-control.line.left,.react-flow__resize-control.line.right{width:1px;transform:translate(-50%);top:0;height:100%}.react-flow__resize-control.line.left{left:0;border-left-width:1px}.react-flow__resize-control.line.right{left:100%;border-right-width:1px}.react-flow__resize-control.line.top,.react-flow__resize-control.line.bottom{height:1px;transform:translateY(-50%);left:0;width:100%}.react-flow__resize-control.line.top{top:0;border-top-width:1px}.react-flow__resize-control.line.bottom{border-bottom-width:1px;top:100%}.react-flow__edge-textbg{fill:var(--xy-edge-label-background-color, var(--xy-edge-label-background-color-default))}.react-flow__edge-text{fill:var(--xy-edge-label-color, var(--xy-edge-label-color-default))}
2
+ /*# sourceMappingURL=bundle.css.map */