jev-devtools 0.4.1 → 0.5.0

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.
@@ -0,0 +1,116 @@
1
+ // Presentation only. Credential and application redaction happen in the server
2
+ // recorder; these personal fields are still present in the received captures.
3
+ const sensitiveKeys = new Set([
4
+ "email",
5
+ "emailaddress",
6
+ "phone",
7
+ "phonenumber",
8
+ "mobile",
9
+ "mobilenumber",
10
+ "address",
11
+ "postaladdress",
12
+ "streetaddress",
13
+ "mailingaddress",
14
+ "ssn",
15
+ "socialsecuritynumber",
16
+ "creditcard",
17
+ "creditcardnumber",
18
+ "cardnumber",
19
+ ]);
20
+ function memoize(project) {
21
+ const cache = new WeakMap();
22
+ return (value) => {
23
+ const cached = cache.get(value);
24
+ if (cached)
25
+ return cached;
26
+ const result = project(value);
27
+ cache.set(value, result);
28
+ return result;
29
+ };
30
+ }
31
+ // Map entries without interpreting their names. Preserve redacted/truncated
32
+ // scalar placeholders and retain identity when every value is unchanged.
33
+ function mapValues(value, project) {
34
+ if (!value || typeof value !== "object")
35
+ return value;
36
+ if (Array.isArray(value)) {
37
+ const next = value.map((item, i) => project(item, String(i)));
38
+ return next.every((item, i) => item === value[i]) ? value : next;
39
+ }
40
+ let changed = false;
41
+ const entries = Object.entries(value).map(([key, item]) => {
42
+ const next = project(item, key);
43
+ changed ||= next !== item;
44
+ return [key, next];
45
+ });
46
+ return changed ? Object.fromEntries(entries) : value;
47
+ }
48
+ /** One projection cache per mounted inspector. Stream captures are immutable;
49
+ * unchanged rows and nested runs retain identity across unrelated updates. */
50
+ export function createDisplayRedactor() {
51
+ function fields(value, structural = {}) {
52
+ return mapValues(value, (item, key) => Object.hasOwn(structural, key)
53
+ ? structural[key](item)
54
+ : sensitiveKeys.has(key.replace(/[-_]/g, "").toLowerCase())
55
+ ? "[redacted]"
56
+ : data(item));
57
+ }
58
+ const objects = memoize((value) => fields(value));
59
+ function data(value) {
60
+ return value && typeof value === "object" ? objects(value) : value;
61
+ }
62
+ const contents = (value) => mapValues(value, data);
63
+ // SDK maps use arbitrary question IDs, category labels and asset references.
64
+ // Mask their contents, not the entry itself because its name resembles PII.
65
+ const question = (value) => fields(value, { criteria: contents });
66
+ const answer = (value) => fields(value, { probabilities: contents, legend: contents });
67
+ const trace = memoize((value) => {
68
+ const request = fields(value.request, {
69
+ questions: (items) => mapValues(items, question),
70
+ assets: contents,
71
+ }), result = fields(value.result, {
72
+ answers: (items) => mapValues(items, answer),
73
+ });
74
+ return request === value.request && result === value.result
75
+ ? value
76
+ : { ...value, request, result };
77
+ });
78
+ const run = memoize((value) => {
79
+ let changed = false;
80
+ // Step IDs are structural metadata, not personal-data field names.
81
+ const steps = Object.fromEntries(Object.entries(value.steps).map(([id, step]) => {
82
+ const captured = data(step.data);
83
+ let collection = step.collection;
84
+ if (collection) {
85
+ const items = collection.items.map((item) => {
86
+ const child = item.run && run(item.run);
87
+ return child === item.run ? item : { ...item, run: child };
88
+ });
89
+ if (items.some((item, i) => item !== collection.items[i]))
90
+ collection = { ...collection, items };
91
+ }
92
+ const next = captured === step.data && collection === step.collection
93
+ ? step
94
+ : { ...step, data: captured, collection };
95
+ changed ||= next !== step;
96
+ return [id, next];
97
+ }));
98
+ const output = data(value.value);
99
+ return changed || output !== value.value
100
+ ? { ...value, steps, value: output }
101
+ : value;
102
+ });
103
+ const definition = memoize((value) => {
104
+ const nodes = value.nodes.map((node) => {
105
+ const rule = data(node.conditionRule);
106
+ const item = node.itemDefinition && definition(node.itemDefinition);
107
+ return rule === node.conditionRule && item === node.itemDefinition
108
+ ? node
109
+ : { ...node, conditionRule: rule, itemDefinition: item };
110
+ });
111
+ return nodes.some((node, i) => node !== value.nodes[i])
112
+ ? { ...value, nodes }
113
+ : value;
114
+ });
115
+ return { trace, run, definition };
116
+ }
@@ -0,0 +1,19 @@
1
+ import type { FlowDefinition, FlowRun } from "./flow-protocol.js";
2
+ import type { Trace } from "./protocol.js";
3
+ /** Jev response bodies keyed by flow step ID, replayed by replayClient from jev-devtools/flow. Map and loop items use `${parentId}/${index}/${innerId}`. */
4
+ export type FlowFixture = {
5
+ version: 1;
6
+ flow: string;
7
+ steps: Record<string, unknown>;
8
+ };
9
+ export type BuiltFlowFixture = {
10
+ fixture: FlowFixture;
11
+ /** Completed jev steps whose capture was evicted, unlinked, or has no result. */
12
+ missing: string[];
13
+ /** Capture warnings from the traces used; redaction or truncation may show in the answers. */
14
+ warnings: string[];
15
+ };
16
+ /** Builds a replay fixture from the captured answers of a run's completed jev steps, including map and loop items. */
17
+ export declare function flowFixture(definition: FlowDefinition, run: FlowRun, traces: ReadonlyMap<string, Trace>): BuiltFlowFixture;
18
+ /** Whether a run, or any of its map and loop items, has a completed jev step. */
19
+ export declare function hasCompletedJev(definition: FlowDefinition, run: FlowRun): boolean;
@@ -0,0 +1,40 @@
1
+ /** Builds a replay fixture from the captured answers of a run's completed jev steps, including map and loop items. */
2
+ export function flowFixture(definition, run, traces) {
3
+ const steps = {}, missing = [], warnings = new Set();
4
+ // Item runs reuse bare inner ids; key them by the composed link id.
5
+ const collect = (program, programRun, prefix) => {
6
+ for (const node of program.nodes) {
7
+ const step = programRun.steps[node.id];
8
+ if (node.itemDefinition)
9
+ for (const item of step?.collection?.items ?? [])
10
+ if (item.run)
11
+ collect(node.itemDefinition, item.run, `${prefix}${node.id}/${item.index}/`);
12
+ if (node.kind !== "jev" || step?.status !== "completed")
13
+ continue;
14
+ const trace = step.traceId ? traces.get(step.traceId) : undefined;
15
+ if (!trace?.result) {
16
+ missing.push(prefix + node.id);
17
+ continue;
18
+ }
19
+ steps[prefix + node.id] = structuredClone(trace.result);
20
+ for (const warning of trace.captureWarnings)
21
+ warnings.add(warning);
22
+ }
23
+ };
24
+ collect(definition, run, "");
25
+ return {
26
+ fixture: { version: 1, flow: definition.name, steps },
27
+ missing,
28
+ warnings: [...warnings],
29
+ };
30
+ }
31
+ /** Whether a run, or any of its map and loop items, has a completed jev step. */
32
+ export function hasCompletedJev(definition, run) {
33
+ return definition.nodes.some((node) => {
34
+ const step = run.steps[node.id];
35
+ if (node.kind === "jev")
36
+ return step?.status === "completed";
37
+ return (!!node.itemDefinition &&
38
+ (step?.collection?.items ?? []).some((item) => !!item.run && hasCompletedJev(node.itemDefinition, item.run)));
39
+ });
40
+ }
@@ -23,6 +23,15 @@ export function connectFlowStream(url, onUpdate) {
23
23
  definitions = new Map();
24
24
  const current = new EventSource(url + "/flows/events");
25
25
  source = current;
26
+ current.addEventListener("open", () => {
27
+ if (!alive || source !== current)
28
+ return;
29
+ // Native reconnect reuses this EventSource. The server starts a fresh
30
+ // snapshot; neither cached definitions nor pending publications survive.
31
+ cancel();
32
+ runs = new Map();
33
+ definitions = new Map();
34
+ });
26
35
  current.addEventListener("flows", (event) => {
27
36
  if (!alive || source !== current)
28
37
  return;
@@ -1,9 +1,14 @@
1
1
  export type { Trace, Snapshot, StreamSnapshot, TraceDelta, Json, AnswerView, QuestionView, } from "./protocol.js";
2
+ export { flowFixture } from "./flow-fixture.js";
3
+ export type { FlowFixture, BuiltFlowFixture } from "./flow-fixture.js";
2
4
  export type JevDevtoolsProps = {
3
5
  endpoint?: string;
4
6
  /** Opt in explicitly, normally with import.meta.env.DEV. */
5
7
  enabled?: boolean;
6
8
  defaultOpen?: boolean;
9
+ /** Mask supported personal fields in DevTools views only. Defaults to true.
10
+ * Captures still contain these values; credentials remain server-redacted. */
11
+ redactAdditionalSensitiveData?: boolean;
7
12
  };
8
13
  /** SSR-safe entry. No requests or browser globals are accessed while disabled. */
9
14
  export declare function JevDevtools({ enabled, ...props }: JevDevtoolsProps): import("react").JSX.Element | null;
package/lib/src/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { lazy, Suspense, useEffect, useState } from "react";
4
+ export { flowFixture } from "./flow-fixture.js";
4
5
  const ConnectedInspector = lazy(() => import("./ConnectedInspector.js"));
5
6
  /** SSR-safe entry. No requests or browser globals are accessed while disabled. */
6
7
  export function JevDevtools({ enabled = false, ...props }) {
@@ -1 +1 @@
1
- export const inspectorStyles = "/* Important declarations in the inner shadow context beat page-authored host\n styles, including important ones. Keep the reset here instead of expanding\n the all shorthand into hundreds of inline properties on the host element.\n These qualified host selectors are inert in the public document stylesheet. */\n:host(jev-devtools-root) {\n all: initial !important;\n direction: ltr !important;\n unicode-bidi: isolate !important;\n position: fixed !important;\n inset: 0 !important;\n pointer-events: none !important;\n z-index: 2147483000 !important;\n}\n\n:host(jev-devtools-root)::before,\n:host(jev-devtools-root)::after {\n content: none !important;\n display: none !important;\n}\n\n.jev-devtools {\n all: initial;\n /* all does not reset custom properties. Establish our own inherited defaults;\n cameras and probability cards can still set local values below this root. */\n --diagram-grid-opacity: 1;\n --diagram-grid-weight: 1;\n --lab-noul-probability: 1;\n --flow-item-offset: 0px;\n display: block;\n direction: ltr;\n unicode-bidi: isolate;\n pointer-events: auto;\n color-scheme: dark;\n font:\n 12px system-ui,\n sans-serif;\n color: #eee8ed;\n line-height: 1.5;\n}\n.jev-devtools,\n.jev-devtools * {\n box-sizing: border-box;\n}\n.jev-devtools button,\n.jev-devtools input,\n.jev-devtools select {\n font: inherit;\n color: inherit;\n border: 1px solid #3b303d;\n border-radius: 5px;\n background: #17121b;\n}\n.jev-devtools button {\n padding: 7px 11px;\n cursor: pointer;\n}\n.jev-devtools button:disabled {\n opacity: 0.45;\n cursor: default;\n}\n.jev-devtools input,\n.jev-devtools select {\n padding: 8px;\n width: 100%;\n}\n.jev-devtools button:hover:not(:disabled) {\n border-color: #e551ba;\n}\n.jev-devtools :is(button, input, select, summary):focus-visible {\n outline: 2px solid #e551ba;\n outline-offset: 3px;\n}\n.jev-devtools .eyebrow {\n font-size: 10px;\n letter-spacing: 0.12em;\n color: #bc9db5;\n text-transform: uppercase;\n}\n.jev-devtools .jev-inspector {\n height: 100%;\n display: flex;\n color: #eee8ed;\n background: #0d0a0f;\n font:\n 12px \"DM Sans\",\n sans-serif;\n min-height: 0;\n overflow: hidden;\n}\n.jev-devtools .jev-inspector h2 {\n font-size: 15px;\n margin: 0;\n font-weight: 600;\n}\n.jev-devtools .jev-inspector h3 {\n font-size: 13px;\n margin: 0 0 8px;\n}\n.jev-devtools .jev-inspector code,\n.jev-devtools .jev-inspector pre {\n font:\n 11px \"JetBrains Mono\",\n monospace;\n}\n.jev-devtools .jev-inspector pre {\n line-height: 1.7;\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n max-height: 460px;\n overflow: auto;\n background: #100d13;\n padding: 16px;\n border: 1px solid #2e2533;\n border-radius: 5px;\n}\n.jev-devtools .jev-inspector .muted {\n color: #a89bac;\n font-size: 11px;\n line-height: 1.6;\n}\n.jev-devtools .request-list {\n flex: 0 0 300px;\n border-right: 1px solid #302634;\n display: flex;\n flex-direction: column;\n min-height: 0;\n}\n.jev-devtools .request-list > header {\n padding: 17px 14px;\n display: flex;\n justify-content: space-between;\n align-items: center;\n}\n.jev-devtools .request-list h2 span {\n font-size: 11px;\n color: #b796ad;\n margin-left: 5px;\n}\n.jev-devtools .request-list button {\n font-size: 11px;\n}\n.jev-devtools .list-filters {\n display: flex;\n gap: 7px;\n padding: 0 12px 12px;\n}\n.jev-devtools .list-filters input {\n min-width: 0;\n}\n.jev-devtools .list-filters select {\n width: 86px;\n font-size: 11px;\n}\n.jev-devtools .request-items {\n overflow: auto;\n flex: 1;\n}\n.jev-devtools .request-row {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n text-align: left;\n border: 0;\n border-top: 1px solid #251d2a;\n border-radius: 0;\n background: transparent;\n padding: 16px 12px;\n}\n.jev-devtools .request-row.selected {\n background: #251423;\n box-shadow: inset 2px 0 #e551ba;\n}\n.jev-devtools .request-row strong {\n display: block;\n font-size: 12px;\n font-weight: 500;\n}\n.jev-devtools .request-row small {\n display: block;\n font-size: 10px;\n color: #a99aac;\n margin-top: 6px;\n}\n.jev-devtools .row-status {\n font-size: 9px;\n color: #ad9bb0;\n margin-left: auto;\n}\n.jev-devtools .status-dot {\n width: 6px;\n height: 6px;\n display: inline-block;\n border-radius: 50%;\n background: #b09ab7;\n flex-shrink: 0;\n}\n.jev-devtools .status-dot.success {\n background: #b3c79c;\n}\n.jev-devtools .status-dot.pending {\n background: #e551ba;\n}\n.jev-devtools .status-dot.error {\n background: #ee8e85;\n}\n.jev-devtools .request-list footer {\n padding: 12px;\n color: #9e8ea5;\n border-top: 1px solid #302634;\n font-size: 10px;\n display: flex;\n gap: 7px;\n}\n.jev-devtools .request-detail {\n flex: 1;\n min-width: 0;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n.jev-devtools .detail-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 20px;\n padding: 18px 24px 10px;\n}\n.jev-devtools .detail-header h2 {\n margin-top: 6px;\n}\n.jev-devtools .metrics {\n display: flex;\n gap: 24px;\n font:\n 12px \"JetBrains Mono\",\n monospace;\n white-space: nowrap;\n}\n.jev-devtools .metrics small {\n display: block;\n font:\n 9px \"DM Sans\",\n sans-serif;\n color: #97879f;\n margin-top: 6px;\n}\n.jev-devtools .detail-meta {\n display: flex;\n gap: 20px;\n flex-wrap: wrap;\n padding: 0 24px 14px;\n color: #a08aa5;\n}\n.jev-devtools .detail-meta code {\n font-size: 10px;\n}\n.jev-devtools .detail-tabs {\n display: flex;\n gap: 20px;\n border-bottom: 1px solid #302634;\n padding: 0 24px;\n}\n.jev-devtools .detail-tabs button {\n border: 0;\n border-radius: 0;\n background: none;\n padding: 10px 0;\n color: #a89bac;\n font-size: 11px;\n}\n.jev-devtools .detail-tabs button[aria-pressed=\"true\"] {\n box-shadow: inset 0 -2px #e551ba;\n color: #f5c1e6;\n}\n.jev-devtools .detail-body {\n overflow: auto;\n flex: 1;\n min-height: 0;\n padding: 16px 24px 28px;\n background-image: radial-gradient(#342337 0.6px, transparent 0.6px);\n background-size: 16px 16px;\n}\n.jev-devtools .question-node {\n position: relative;\n border: 1px solid #75516c;\n border-radius: 6px;\n padding: 15px;\n background: #110d14;\n min-width: 0;\n}\n/* Retain the legacy question connectors and standalone suppression together.\n Remove only after resolving the narrow-screen visual differences in\n https://github.com/diegoag64/jev-devtools/issues/57. */\n.jev-devtools .question-node:before {\n content: \"\";\n position: absolute;\n right: 100%;\n top: 50%;\n width: 21px;\n border-top: 1px solid #7f416d;\n}\n.jev-devtools .question-branches .question-node:not(:first-child):before {\n display: none;\n}\n.jev-devtools .question-node header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n}\n.jev-devtools .question-node header h3 {\n margin: 0;\n overflow-wrap: anywhere;\n}\n.jev-devtools .type {\n font:\n 9px \"JetBrains Mono\",\n monospace;\n color: #e59acc;\n border: 1px solid #5e3654;\n border-radius: 3px;\n padding: 3px 5px;\n}\n.jev-devtools .answer-value {\n display: block;\n font-size: 20px;\n font-weight: 500;\n margin: 15px 0;\n color: #f0c7e5;\n}\n.jev-devtools .answer-value small {\n font-size: 10px;\n color: #b5a1ba;\n}\n.jev-devtools .probability {\n margin: 10px 0;\n font-size: 10px;\n}\n.jev-devtools .probability > div:first-child {\n display: flex;\n justify-content: space-between;\n gap: 5px;\n margin-bottom: 5px;\n}\n.jev-devtools .probability b {\n font-weight: 400;\n color: #d8abc9;\n}\n.jev-devtools .bar {\n height: 4px;\n background: #302033;\n border-radius: 2px;\n overflow: hidden;\n}\n.jev-devtools .bar i {\n display: block;\n height: 100%;\n background: #e551ba;\n}\n.jev-devtools .confidence {\n font-size: 10px;\n color: #a68cac;\n border-top: 1px solid #302634;\n padding-top: 10px;\n display: flex;\n justify-content: space-between;\n}\n.jev-devtools .confidence b {\n color: #d1bad2;\n font-weight: 400;\n}\n.jev-devtools details {\n margin-top: 12px;\n font-size: 10px;\n color: #b7a0bd;\n}\n.jev-devtools summary {\n cursor: pointer;\n}\n.jev-devtools .standalone {\n margin-bottom: 14px;\n max-width: 750px;\n}\n.jev-devtools .standalone:before {\n display: none;\n}\n.jev-devtools .standalone .answer {\n max-width: 450px;\n}\n.jev-devtools .box-preview {\n position: relative;\n margin-top: 14px;\n min-height: 80px;\n background: #19121c;\n}\n.jev-devtools .box-preview img {\n width: 100%;\n display: block;\n}\n.jev-devtools .bounding-box {\n position: absolute;\n border: 1.5px solid #e551ba;\n background: #e551ba15;\n}\n.jev-devtools .coordinates {\n display: block;\n font-size: 9px !important;\n line-height: 1.5;\n margin-top: 8px;\n color: #ac93b0;\n}\n.jev-devtools .context-image {\n max-width: 400px;\n width: 100%;\n border: 1px solid #75516c;\n border-radius: 6px;\n}\n.jev-devtools .empty {\n padding: 20px;\n color: #a294a8;\n line-height: 1.7;\n}\n.jev-devtools .empty-detail {\n margin: auto;\n padding: 40px;\n text-align: center;\n color: #b2a0ba;\n}\n.jev-devtools .empty-symbol {\n font-size: 36px;\n color: #e551ba;\n display: block;\n margin-bottom: 20px;\n}\n.jev-devtools .empty-detail p {\n font-size: 12px;\n line-height: 1.8;\n}\n.jev-devtools .error-banner {\n border: 1px solid #874b50;\n background: #31191e;\n padding: 12px;\n line-height: 1.5;\n margin-bottom: 16px;\n}\n.jev-devtools .warning {\n color: #e2b97d;\n}\n.jev-devtools .jev-inspector table {\n border-collapse: collapse;\n width: 100%;\n text-align: left;\n}\n.jev-devtools .jev-inspector th,\n.jev-devtools .jev-inspector td {\n border-bottom: 1px solid #302634;\n padding: 14px;\n}\n@media (max-width: 1100px) {\n .jev-devtools .question-branches .question-node:not(:first-child):before {\n display: block;\n }\n .jev-devtools .metrics {\n gap: 12px;\n }\n .jev-devtools .request-list {\n flex-basis: 250px;\n }\n}\n@media (max-width: 700px) {\n .jev-devtools .jev-inspector {\n flex-direction: column;\n }\n .jev-devtools .request-list {\n flex-basis: 140px;\n flex-shrink: 0;\n border-right: 0;\n border-bottom: 1px solid #302634;\n }\n .jev-devtools .request-list > header {\n padding: 6px 12px;\n }\n .jev-devtools .request-list footer {\n display: none;\n }\n .jev-devtools .list-filters {\n padding-bottom: 6px;\n }\n .jev-devtools .request-items {\n display: flex;\n }\n .jev-devtools .request-row {\n min-width: 240px;\n padding: 8px 12px;\n }\n .jev-devtools .detail-header {\n padding: 12px;\n }\n .jev-devtools .detail-meta,\n .jev-devtools .detail-tabs {\n padding-left: 12px;\n padding-right: 12px;\n }\n .jev-devtools .detail-body {\n padding: 12px;\n }\n .jev-devtools .metrics {\n display: none;\n }\n .jev-devtools .question-node:before {\n width: 13px;\n }\n .jev-devtools .question-node {\n padding: 12px;\n }\n}\n.jev-devtools .question-node:before,\n.jev-devtools .question-branches .question-node:not(:first-child):before {\n display: block;\n right: auto;\n left: 50%;\n top: -21px;\n width: 0;\n height: 20px;\n border-top: 0;\n border-left: 1px solid #7f416d;\n}\n.jev-devtools .standalone:before {\n display: none;\n}\n@media (max-width: 900px) {\n .jev-devtools .question-node:before,\n .jev-devtools .question-branches .question-node:not(:first-child):before {\n height: 12px;\n top: -13px;\n }\n .jev-devtools .question-branches .question-node:first-child:before {\n height: 20px;\n top: -21px;\n }\n}\n\n.jev-devtools.jev-drawer {\n margin: 0;\n padding: 0;\n position: fixed;\n z-index: 1000;\n inset: auto 0 0;\n display: flex;\n flex-direction: column;\n background: #0d0a0f;\n color: #eee8ed;\n border-top: 1px solid #604058;\n box-shadow: 0 -12px 48px #0005;\n font:\n 12px \"DM Sans\",\n sans-serif;\n}\n.jev-devtools.jev-drawer[hidden],\n.jev-devtools.jev-drawer-trigger[hidden] {\n display: none;\n}\n.jev-devtools .jev-drawer-resize {\n position: absolute;\n z-index: 1;\n top: -6px;\n width: 100%;\n height: 12px;\n cursor: ns-resize;\n touch-action: none;\n display: flex;\n justify-content: center;\n align-items: center;\n}\n.jev-devtools .jev-drawer-resize span {\n width: 42px;\n height: 3px;\n border-radius: 3px;\n background: #79506f;\n}\n.jev-devtools .jev-drawer-resize:hover span,\n.jev-devtools .jev-drawer-resize:focus-visible span {\n background: #e551ba;\n}\n.jev-devtools .jev-drawer-resize:focus-visible {\n outline: 2px solid #e551ba;\n outline-offset: -2px;\n}\n.jev-devtools .jev-drawer-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n flex: 0 0 46px;\n padding: 0 16px;\n border-bottom: 1px solid #302634;\n}\n.jev-devtools .jev-drawer-header strong {\n display: flex;\n align-items: center;\n gap: 9px;\n font-weight: 600;\n}\n.jev-devtools .jev-drawer-header > div {\n display: flex;\n align-items: center;\n gap: 10px;\n}\n.jev-devtools .jev-drawer-header button {\n background: transparent;\n border-color: transparent;\n font-size: 11px;\n color: #c4afc8;\n}\n.jev-devtools .jev-drawer-header .jev-drawer-close {\n font-size: 22px;\n line-height: 1;\n padding: 4px 8px;\n}\n.jev-devtools .jev-drawer-content {\n flex: 1;\n min-height: 0;\n overflow: hidden;\n}\n.jev-devtools.jev-drawer-trigger {\n position: fixed;\n bottom: 18px;\n right: 18px;\n z-index: 1000;\n display: flex;\n align-items: center;\n gap: 9px;\n padding: 10px 16px;\n border: 1px solid #a24487;\n border-radius: 8px;\n cursor: pointer;\n background: #211321;\n color: #f1cbe6;\n font:\n 600 13px \"DM Sans\",\n sans-serif;\n box-shadow: 0 4px 20px #0005;\n}\n.jev-devtools.jev-drawer-trigger span {\n color: #e551ba;\n font-size: 20px;\n}\n/* Baseline cards and authored left-to-right composition from the lab's Jev presets. */\n.jev-devtools .detail-body.detail-body-flow {\n display: flex;\n padding: 0;\n overflow: hidden;\n}\n.jev-devtools .lab-flow {\n display: flex;\n flex-direction: column;\n flex: 1;\n min-width: 0;\n min-height: 0;\n}\n.jev-devtools .lab-flow-toolbar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n flex-shrink: 0;\n padding: 12px 16px;\n flex-wrap: wrap;\n}\n.jev-devtools .lab-flow-toolbar label {\n display: flex;\n gap: 10px;\n align-items: center;\n font-size: 11px;\n min-width: 0;\n}\n.jev-devtools .lab-flow-toolbar select {\n width: auto;\n max-width: 240px;\n font-size: 11px;\n}\n.jev-devtools .lab-flow-toolbar > div {\n display: flex;\n gap: 8px;\n align-items: center;\n}\n.jev-devtools .lab-flow-toolbar button {\n font-size: 11px;\n}\n.jev-devtools .lab-flow-toolbar output {\n font-size: 11px;\n min-width: 35px;\n text-align: center;\n}\n.jev-devtools .lab-flow-viewport {\n position: relative;\n flex: 1;\n min-height: 100px;\n overflow: hidden;\n background-color: #0d0a0f;\n background-image: radial-gradient(\n rgb(70 52 73 / var(--diagram-grid-opacity, 1))\n calc(0.8px * var(--diagram-grid-weight, 1)),\n transparent calc(0.8px * var(--diagram-grid-weight, 1))\n );\n background-size: 20px 20px;\n border-block: 1px solid #302634;\n cursor: grab;\n touch-action: none;\n user-select: none;\n overscroll-behavior: contain;\n}\n.jev-devtools .lab-flow-viewport[data-panning] {\n cursor: grabbing;\n}\n.jev-devtools .lab-flow-viewport:focus-visible {\n outline: 2px solid #e551ba;\n outline-offset: -2px;\n}\n.jev-devtools .lab-flow-caption {\n flex-shrink: 0;\n padding: 8px 16px;\n margin: 0;\n}\n.jev-devtools .lab-gesture-hint {\n display: block;\n color: #c6a7bd;\n}\n.jev-devtools .lab-flow-canvas {\n position: absolute;\n inset: 0 auto auto 0;\n transform-origin: top left;\n will-change: transform;\n font-family: system-ui, sans-serif;\n color: #f5f3f6;\n}\n.jev-devtools .lab-connections {\n position: absolute;\n inset: 0;\n overflow: visible;\n}\n.jev-devtools .lab-connections path {\n fill: none;\n stroke: #645a68;\n stroke-width: 1;\n}\n.jev-devtools .lab-connections path.active {\n stroke: #968592;\n}\n.jev-devtools .lab-connections text {\n fill: #b7b1bc;\n font:\n 12px ui-monospace,\n monospace;\n}\n.jev-devtools .lab-flow-dot {\n fill: #e551ba;\n}\n.jev-devtools .lab-card {\n position: absolute;\n border: 1.3px solid #f5f3f6;\n border-radius: 14px;\n padding: 16px;\n background: #0d0a0f;\n overflow: hidden;\n}\n.jev-devtools .lab-eyebrow {\n display: block;\n color: #e551ba;\n font:\n 500 10px ui-monospace,\n monospace;\n letter-spacing: 1px;\n margin: 0 0 7px;\n}\n.jev-devtools .lab-card h3 {\n font-size: 16px;\n font-weight: 600;\n line-height: 1.3;\n letter-spacing: -0.2px;\n margin: 0 0 10px;\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n.jev-devtools .lab-card p {\n font-size: 13px;\n line-height: 1.4;\n margin: 10px 0;\n}\n.jev-devtools .lab-copy {\n display: -webkit-box;\n -webkit-line-clamp: 5;\n -webkit-box-orient: vertical;\n overflow: hidden;\n overflow-wrap: anywhere;\n}\n.jev-devtools .lab-probability {\n margin: 17px 0;\n}\n.jev-devtools .lab-probability > div:first-child {\n display: flex;\n justify-content: space-between;\n gap: 10px;\n margin-bottom: 9px;\n font-size: 15px;\n}\n.jev-devtools .lab-probability span {\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n.jev-devtools .lab-probability code {\n font:\n 13px ui-monospace,\n monospace;\n}\n.jev-devtools .lab-track {\n height: 7px;\n border-radius: 4px;\n background: #f5f3f61a;\n overflow: hidden;\n}\n.jev-devtools .lab-track i {\n display: block;\n height: 100%;\n border-radius: 4px;\n background: #e551ba;\n}\n.jev-devtools .lab-track i.lab-alternative {\n opacity: 0.35;\n}\n.jev-devtools .lab-card .lab-result {\n font-size: 14px;\n margin-top: 24px;\n}\n.jev-devtools .lab-card .lab-small {\n font-size: 12px;\n color: #b7b1bc;\n margin-top: 20px;\n}\n.jev-devtools .lab-idle {\n opacity: 0.65;\n}\n.jev-devtools .lab-scale {\n position: relative;\n margin-top: 18px;\n border-top: 1px solid #b7b1bc;\n display: flex;\n justify-content: space-between;\n padding-top: 9px;\n}\n.jev-devtools .lab-scale > span {\n position: absolute;\n top: -5px;\n width: 9px;\n height: 9px;\n border-radius: 50%;\n background: #e551ba;\n transform: translateX(-50%);\n}\n.jev-devtools .lab-big {\n display: block;\n font-size: 36px;\n font-weight: 500;\n margin-top: 26px;\n}\n.jev-devtools .lab-endpoints {\n display: flex;\n justify-content: space-between;\n font-size: 12px;\n color: #b7b1bc;\n margin-top: 10px;\n}\n.jev-devtools .lab-image {\n position: relative;\n}\n.jev-devtools .lab-image img {\n display: block;\n width: 100%;\n}\n.jev-devtools .lab-box {\n position: absolute;\n border: 1.5px solid #e551ba;\n background: #e551ba15;\n}\n.jev-devtools .lab-source-image {\n width: 100%;\n max-height: 105px;\n object-fit: contain;\n}\n.jev-devtools .lab-coordinates {\n font-family: ui-monospace, monospace;\n}\n@media (prefers-reduced-motion: reduce) {\n .jev-devtools .lab-flow-dot {\n display: none;\n }\n}\n/* The public TypeSafe mark travels with the package, independent of host assets. */\n.jev-devtools .typesafe-mark {\n display: inline-block;\n flex: 0 0 23px;\n width: 23px;\n height: 32px;\n overflow: hidden;\n line-height: 0;\n}\n.jev-devtools .typesafe-mark img {\n display: block;\n height: 32px;\n width: auto;\n max-width: none;\n}\n\n.jev-devtools .detail-body-questions {\n display: flex;\n flex-direction: column;\n overflow: auto;\n}\n.jev-devtools .questions-list {\n display: flex;\n flex-direction: column;\n flex: 1;\n min-height: 180px;\n}\n.jev-devtools .questions-navigation {\n display: flex;\n align-items: end;\n gap: 8px;\n flex-wrap: wrap;\n flex-shrink: 0;\n}\n.jev-devtools .questions-navigation label {\n display: flex;\n align-items: center;\n gap: 8px;\n white-space: nowrap;\n flex: 1;\n min-width: 240px;\n max-width: 480px;\n font-size: 11px;\n color: #bc9db5;\n}\n.jev-devtools .questions-navigation select {\n display: block;\n min-width: 0;\n}\n.jev-devtools .questions-navigation > span {\n align-self: center;\n color: #a89bac;\n}\n.jev-devtools .questions-help {\n margin: 8px 0;\n flex-shrink: 0;\n}\n.jev-devtools .questions-viewport {\n flex: 1;\n min-height: 0;\n overflow: auto;\n overflow-anchor: none;\n scrollbar-gutter: stable;\n padding: 3px;\n}\n.jev-devtools .questions-spacer {\n position: relative;\n width: 100%;\n}\n.jev-devtools .questions-row {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n padding-bottom: 14px;\n}\n.jev-devtools .questions-row .standalone {\n margin-bottom: 0;\n}\n.jev-devtools .questions-viewport:focus-visible,\n.jev-devtools .questions-row article:focus-visible {\n outline: 2px solid #e551ba;\n outline-offset: -2px;\n}\n\n/* Reserve measured card geometry while rapid scrolling uses a light preview. */\n.jev-devtools .questions-row[data-preview=\"true\"] article {\n height: 100%;\n}\n.jev-devtools .question-scroll-preview p {\n margin-top: 16px;\n}\n\n/* The full distribution stays at reading size, outside the zoomable canvas. */\n.jev-devtools .lab-flow-viewport[hidden] {\n display: none;\n}\n.jev-devtools .lab-choice-browser {\n display: flex;\n flex-direction: column;\n flex: 1;\n min-height: 0;\n padding: 8px 16px;\n gap: 10px;\n}\n.jev-devtools .lab-choice-controls {\n display: flex;\n gap: 12px;\n align-items: end;\n flex-wrap: wrap;\n font-size: 12px;\n}\n.jev-devtools .lab-choice-controls label {\n display: grid;\n gap: 5px;\n}\n.jev-devtools .lab-choice-table-scroll {\n overflow: auto;\n overscroll-behavior: contain;\n min-height: 0;\n}\n.jev-devtools .lab-choice-browser table {\n width: 100%;\n border-collapse: collapse;\n font-size: 13px;\n}\n.jev-devtools .lab-choice-browser caption {\n text-align: left;\n padding: 8px 0;\n color: #aaa0af;\n}\n.jev-devtools .lab-choice-browser th,\n.jev-devtools .lab-choice-browser td {\n padding: 9px 12px;\n border-bottom: 1px solid #302634;\n text-align: left;\n}\n.jev-devtools .lab-choice-browser th {\n overflow-wrap: anywhere;\n max-width: 320px;\n}\n.jev-devtools .lab-choice-browser td {\n font-variant-numeric: tabular-nums;\n}\n.jev-devtools .lab-choice-browser code {\n white-space: normal;\n overflow-wrap: anywhere;\n}\n.jev-devtools .lab-choice-returned {\n display: block;\n font-size: 10px;\n color: #e551ba;\n}\n.jev-devtools .lab-choice-pagination {\n display: flex;\n align-items: center;\n gap: 12px;\n justify-content: space-between;\n font-size: 12px;\n}\n.jev-devtools .lab-choice-remainder button {\n margin-top: 7px;\n}\n\n.jev-devtools .lab-flow-compact .lab-output {\n padding: 12px;\n height: 104px;\n}\n.jev-devtools .lab-flow-compact .lab-output .lab-eyebrow {\n margin-bottom: 6px;\n}\n.jev-devtools .lab-flow-compact .lab-output h3 {\n font-size: 16px;\n margin-bottom: 8px;\n}\n.jev-devtools .lab-flow-compact .lab-output p {\n font-size: 12px;\n margin: 4px 0;\n}\n.jev-devtools .lab-flow-compact .lab-result {\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n overflow-wrap: anywhere;\n}\n.jev-devtools .lab-choice-remainder {\n padding: 12px;\n}\n.jev-devtools .lab-choice-remainder .lab-small {\n margin: 8px 0 0;\n font-size: 11px;\n}\n\n.jev-devtools .detail-body.detail-body-flow {\n flex-direction: column;\n}\n.jev-devtools .detail-body-flow > .warning,\n.jev-devtools .detail-body-flow > .error-banner {\n flex-shrink: 0;\n margin: 6px 16px;\n}\n.jev-devtools .lab-choice-browser {\n overflow: auto;\n}\n.jev-devtools .lab-choice-table-scroll {\n flex: 1 0 110px;\n}\n\n/* Only freshly revealed branches enter; pan/zoom remains on the canvas. */\n.jev-devtools .lab-card-enter {\n animation: jev-card-enter 280ms cubic-bezier(0.2, 0.8, 0.2, 1) backwards;\n}\n.jev-devtools .lab-branch-enter {\n stroke-dasharray: 1;\n animation: jev-branch-enter 280ms ease-out backwards;\n}\n@keyframes jev-card-enter {\n from {\n opacity: 0;\n transform: translateX(-16px) scale(0.96);\n }\n}\n@keyframes jev-branch-enter {\n from {\n opacity: 0;\n stroke-dashoffset: 1;\n }\n to {\n opacity: 1;\n stroke-dashoffset: 0;\n }\n}\n@media (prefers-reduced-motion: reduce) {\n .jev-devtools .lab-card-enter,\n .jev-devtools .lab-branch-enter {\n animation: none;\n }\n}\n\n/* Connected requests share one camera and one opt-in full request detail.\n :where scopes these selectors without changing their existing specificity. */\n:where(.jev-devtools) .whole-flow {\n min-width: 0;\n overflow: hidden;\n}\n:where(.jev-devtools) .whole-flow > .detail-header,\n:where(.jev-devtools) .whole-flow > .lab-flow-toolbar {\n flex-shrink: 0;\n}\n:where(.jev-devtools) .flow-workspace {\n display: flex;\n flex-direction: column;\n flex: 1;\n min-height: 0;\n overflow: hidden;\n}\n:where(.jev-devtools) .flow-details-panel {\n flex: none;\n min-width: 0;\n background: #130f17;\n border-top: 1px solid #342a38;\n}\n:where(.jev-devtools) .flow-details-bar {\n display: flex;\n align-items: center;\n gap: 12px;\n height: 36px;\n padding: 0 12px;\n}\n.jev-devtools .flow-details-bar button {\n flex: none;\n padding: 4px 8px;\n font-size: 11px;\n}\n:where(.jev-devtools) .flow-details-bar > span {\n color: #b5a6bc;\n font-size: 11px;\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n:where(.jev-devtools) .flow-details-body {\n overflow: auto;\n overscroll-behavior: contain;\n scrollbar-gutter: stable;\n}\n:where(.jev-devtools) .flow-details-resize {\n display: flex;\n align-items: center;\n justify-content: center;\n height: 8px;\n cursor: ns-resize;\n touch-action: none;\n user-select: none;\n}\n:where(.jev-devtools) .flow-details-resize span {\n width: 40px;\n height: 2px;\n border-radius: 2px;\n background: #6c5874;\n}\n:where(.jev-devtools) .flow-details-resize:hover,\n:where(.jev-devtools) .flow-details-resize:focus-visible {\n background: #392239;\n outline: 1px solid #e551ba;\n outline-offset: -1px;\n}\n:where(.jev-devtools) .flow-overview-viewport {\n position: relative;\n min-height: 0;\n flex: 1 1 0;\n overflow: hidden;\n touch-action: none;\n cursor: grab;\n background-image: radial-gradient(\n rgb(83 67 87 / var(--diagram-grid-opacity, 1))\n calc(1px * var(--diagram-grid-weight, 1)),\n transparent calc(1px * var(--diagram-grid-weight, 1))\n );\n background-size: 20px 20px;\n}\n:where(.jev-devtools) .flow-overview-viewport[data-panning] {\n cursor: grabbing;\n}\n:where(.jev-devtools) .flow-overview-canvas {\n position: absolute;\n transform-origin: 0 0;\n}\n:where(.jev-devtools) .flow-connections {\n position: absolute;\n inset: 0;\n overflow: visible;\n color: #c0b8c4;\n pointer-events: none;\n}\n:where(.jev-devtools) .flow-step {\n position: absolute;\n min-height: 172px;\n overflow-wrap: anywhere;\n display: flex;\n flex-direction: column;\n gap: 8px;\n text-align: left;\n padding: 14px;\n border: 1px solid #736476;\n border-radius: 12px;\n background: #151017;\n color: inherit;\n}\n:where(.jev-devtools) .flow-step strong {\n white-space: normal;\n width: 100%;\n}\n:where(.jev-devtools) .flow-step-description {\n overflow-wrap: anywhere;\n font-size: 12px;\n}\n:where(.jev-devtools) .flow-step.flow-request {\n display: block;\n padding: 0;\n background: #0d0a0f;\n /* Skip offscreen paint/layout while preserving each question/expansion state. */\n content-visibility: auto;\n contain-intrinsic-size: auto 640px;\n}\n:where(.jev-devtools) .flow-request-heading {\n width: 100%;\n min-height: 64px;\n gap: 20px;\n display: flex;\n align-items: center;\n justify-content: space-between;\n text-align: left;\n padding: 12px 20px;\n border: 0;\n border-bottom: 1px solid #342a38;\n border-radius: 12px 12px 0 0;\n background: transparent;\n color: inherit;\n}\n:where(.jev-devtools) .flow-request-heading .eyebrow {\n display: block;\n margin-bottom: 4px;\n}\n.jev-devtools .lab-flow-embedded {\n width: 1120px;\n display: block;\n}\n.jev-devtools .lab-flow-surface {\n position: relative;\n width: 1120px;\n}\n.jev-devtools .lab-flow-surface[hidden] {\n display: none;\n}\n.jev-devtools .lab-flow-embedded .lab-flow-canvas {\n will-change: auto;\n}\n.jev-devtools .lab-flow-embedded .lab-choice-browser {\n height: 520px;\n}\n:where(.jev-devtools) .flow-step.selected {\n outline: 2px solid #e551ba;\n outline-offset: 3px;\n}\n:where(.jev-devtools) .flow-step[data-status=\"completed\"] {\n border-color: #e551ba;\n}\n:where(.jev-devtools) .flow-step[data-status=\"running\"] {\n border-color: #e551ba;\n background: #2b1728;\n}\n:where(.jev-devtools) .flow-step[data-status=\"untaken\"],\n:where(.jev-devtools) .flow-step[data-status=\"blocked\"] {\n color: #948797;\n border-color: #403746;\n background: #151017;\n}\n:where(.jev-devtools) .flow-step[data-status=\"error\"] {\n border-color: #f17c78;\n}\n:where(.jev-devtools) .flow-step[data-status=\"cancelled\"] {\n border-style: dashed;\n}\n:where(.jev-devtools) .flow-selection {\n padding: 12px 20px;\n border-top: 1px solid #342a38;\n}\n:where(.jev-devtools) .flow-selection p {\n margin: 6px 0;\n overflow-wrap: anywhere;\n}\n:where(.jev-devtools) .flow-request-detail {\n min-height: 450px;\n border-top: 1px solid #534357;\n}\n:where(.jev-devtools) .flow-request-detail > .request-detail {\n min-height: 450px;\n}\n\n:where(.jev-devtools) .flow-card-heading,\n:where(.jev-devtools) .flow-card-footer,\n:where(.jev-devtools) .flow-join-summary,\n:where(.jev-devtools) .flow-lane {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n width: 100%;\n}\n:where(.jev-devtools) .flow-card-heading {\n justify-content: flex-start;\n}\n:where(.jev-devtools) .flow-task .flow-kind-icon {\n border: 1px solid currentColor;\n border-radius: 6px;\n padding: 4px;\n}\n:where(.jev-devtools) .flow-task-preview {\n display: grid;\n gap: 8px;\n min-width: 0;\n}\n.jev-devtools .flow-task-preview .diagram-content {\n max-height: 280px;\n padding: 8px 10px;\n border: 1px solid #35283b;\n border-radius: 4px;\n background: #100c13;\n}\n:where(.jev-devtools) .flow-task-preview small {\n color: #a89bac;\n font-size: 11px;\n}\n:where(.jev-devtools) .flow-task-details {\n display: grid;\n gap: 12px;\n}\n:where(.jev-devtools) .flow-task-details details {\n min-width: 0;\n}\n:where(.jev-devtools) .flow-task-details summary {\n cursor: pointer;\n margin-bottom: 8px;\n}\n:where(.jev-devtools) .flow-task-details .diagram-content {\n max-height: 320px;\n overflow: auto;\n}\n:where(.jev-devtools) .flow-kind-icon {\n width: 26px;\n height: 26px;\n color: #e551ba;\n flex-shrink: 0;\n}\n:where(.jev-devtools) .flow-kind-icon svg {\n width: 100%;\n height: 100%;\n}\n:where(.jev-devtools) .flow-card-heading .eyebrow {\n font-size: 10px;\n}\n:where(.jev-devtools) .flow-status {\n margin-left: auto;\n font-size: 10px;\n border-radius: 20px;\n padding: 4px 8px;\n background: #342031;\n color: #f0b7e0;\n}\n:where(.jev-devtools) .flow-step[data-status=\"untaken\"] .flow-status {\n background: #241e28;\n color: #948797;\n}\n:where(.jev-devtools) .flow-step[data-status=\"untaken\"] .flow-kind-icon {\n color: #948797;\n}\n:where(.jev-devtools) .flow-card-body > strong {\n white-space: normal;\n font-size: 18px;\n line-height: 1.25;\n overflow-wrap: anywhere;\n}\n:where(.jev-devtools) .flow-step-description {\n line-height: 1.5;\n color: #b9aebf;\n white-space: pre-wrap;\n}\n:where(.jev-devtools) .flow-card-footer {\n margin-top: auto;\n padding-top: 10px;\n border-top: 1px solid #342a38;\n font-size: 10px;\n color: #a092a7;\n}\n:where(.jev-devtools) .flow-card-footer code {\n max-width: 45%;\n white-space: normal;\n overflow-wrap: anywhere;\n}\n:where(.jev-devtools) .flow-card-footer small {\n text-align: right;\n}\n:where(.jev-devtools) .flow-join-summary {\n margin-top: 4px;\n font-size: 11px;\n color: #c9b9cd;\n}\n:where(.jev-devtools) .flow-join-summary > span {\n color: #ef9ed9;\n font-variant-numeric: tabular-nums;\n}\n:where(.jev-devtools) .flow-lanes {\n display: flex;\n flex-direction: column;\n gap: 4px;\n width: 100%;\n}\n:where(.jev-devtools) .flow-lane {\n border-radius: 5px;\n background: #211923;\n padding: 6px 8px;\n font-size: 11px;\n min-height: 28px;\n}\n:where(.jev-devtools) .flow-lane > span {\n min-width: 0;\n overflow-wrap: anywhere;\n}\n:where(.jev-devtools) .flow-lane > span > span {\n color: #ef9ed9;\n margin-right: 4px;\n}\n:where(.jev-devtools) .flow-lane small {\n color: #b9aebf;\n}\n:where(.jev-devtools) .flow-lane-details {\n padding-left: 18px;\n font-size: 12px;\n}\n:where(.jev-devtools) .flow-request-heading > span:first-child {\n min-width: 0;\n max-width: 80%;\n}\n:where(.jev-devtools) .flow-request-description {\n display: block;\n white-space: pre-wrap;\n font-size: 12px;\n line-height: 1.5;\n color: #b9aebf;\n margin-top: 5px;\n overflow-wrap: anywhere;\n}\n\n/* Structured context stays within the card; its gestures never move the camera. */\n.jev-devtools .lab-content-card {\n display: flex;\n flex-direction: column;\n padding: 14px;\n}\n.jev-devtools .lab-content-card h3 {\n font-size: 14px;\n margin-bottom: 8px;\n flex-shrink: 0;\n}\n.jev-devtools .lab-content-card .lab-eyebrow {\n font-size: 9px;\n margin-bottom: 5px;\n flex-shrink: 0;\n}\n.jev-devtools .diagram-content {\n min-height: 0;\n flex: 1;\n overflow: auto;\n overscroll-behavior: contain;\n touch-action: pan-y;\n scrollbar-width: thin;\n scrollbar-color: #514256 transparent;\n padding-right: 4px;\n font-size: 12px;\n line-height: 1.5;\n color: #d5cdd9;\n cursor: auto;\n user-select: text;\n}\n.jev-devtools .diagram-content:focus-visible {\n outline: 1px solid #e551ba;\n outline-offset: -1px;\n border-radius: 3px;\n}\n.jev-devtools .diagram-property {\n display: flex;\n justify-content: space-between;\n align-items: baseline;\n gap: 8px;\n padding: 5px 0;\n border-bottom: 1px solid #261e2a;\n}\n.jev-devtools .diagram-property-text {\n display: block;\n}\n.jev-devtools .diagram-property-key {\n font:\n 10px/1.5 ui-monospace,\n monospace;\n color: #c8a8c8;\n overflow-wrap: anywhere;\n min-width: 0;\n}\n.jev-devtools .diagram-property-text > .diagram-property-key {\n display: block;\n margin-bottom: 2px;\n}\n.jev-devtools .diagram-text-value {\n display: block;\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n.jev-devtools .diagram-text-preview {\n display: -webkit-box;\n -webkit-line-clamp: 3;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n.jev-devtools .diagram-value-number {\n color: #a6d6ca;\n font:\n 11px/1.5 ui-monospace,\n monospace;\n font-variant-numeric: tabular-nums;\n overflow-wrap: anywhere;\n}\n.jev-devtools .diagram-value-boolean {\n color: #c4b1ed;\n font:\n 11px/1.5 ui-monospace,\n monospace;\n}\n.jev-devtools .diagram-value-empty {\n color: #9c8ca4;\n font:\n 10px/1.5 ui-monospace,\n monospace;\n}\n.jev-devtools .diagram-property-disclosure {\n width: 100%;\n display: flex;\n align-items: baseline;\n gap: 5px;\n padding: 6px 0;\n border: 0;\n border-bottom: 1px solid #261e2a;\n border-radius: 0;\n background: transparent;\n text-align: left;\n color: inherit;\n}\n.jev-devtools .diagram-property-disclosure:hover {\n background: #211723;\n}\n.jev-devtools .diagram-property-caret {\n color: #ab8aab;\n flex-shrink: 0;\n}\n.jev-devtools .diagram-property-type {\n color: #9c8ca4;\n font:\n 10px/1.5 ui-monospace,\n monospace;\n margin-left: auto;\n flex-shrink: 0;\n}\n.jev-devtools .diagram-property-children {\n margin-left: 4px;\n padding-left: 9px;\n border-left: 1px solid #49324b;\n}\n.jev-devtools .diagram-content-toggle {\n display: block;\n padding: 3px 0;\n border: 0;\n border-radius: 0;\n background: transparent;\n color: #e9a4d7;\n font-size: 10px;\n text-align: left;\n}\n.jev-devtools .diagram-content-toggle:hover {\n color: #ffd9f3;\n text-decoration: underline;\n}\n\n/* Noul is a probability of true; its two outputs carry p and 1-p.\n Keep the fill opaque, with readable text even at zero probability. */\n.jev-devtools .lab-noul-output {\n border-color: rgb(\n 245 243 246 / calc(0.35 + 0.65 * var(--lab-noul-probability, 1))\n );\n}\n.jev-devtools .lab-noul-output > h3,\n.jev-devtools .lab-noul-output > p {\n opacity: calc(0.65 + 0.35 * var(--lab-noul-probability, 1));\n}\n\n:where(.jev-devtools) .flow-condition-rule {\n display: block;\n padding: 10px 12px;\n border: 1px solid #49324b;\n border-radius: 6px;\n background: #211723;\n width: 100%;\n}\n:where(.jev-devtools) .flow-condition-label {\n display: block;\n font-size: 10px;\n color: #ba9dbe;\n margin-bottom: 5px;\n}\n:where(.jev-devtools) .flow-condition-rule code {\n display: block;\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n font:\n 12px/1.6 ui-monospace,\n monospace;\n color: #f2d7eb;\n}\n\n:where(.jev-devtools) .flow-condition-incoming {\n display: block;\n padding-bottom: 10px;\n margin-bottom: 10px;\n border-bottom: 1px solid #49324b;\n}\n:where(.jev-devtools) .flow-condition-input {\n display: flex;\n justify-content: space-between;\n align-items: baseline;\n gap: 12px;\n font-size: 12px;\n padding: 3px 0;\n}\n:where(.jev-devtools) .flow-condition-input > span {\n min-width: 0;\n overflow-wrap: anywhere;\n}\n:where(.jev-devtools) .flow-condition-input > b {\n font-size: 17px;\n font-weight: 600;\n color: #f2d7eb;\n overflow-wrap: anywhere;\n white-space: normal;\n max-width: 65%;\n}\n:where(.jev-devtools) .flow-input-details {\n font-size: 12px;\n}\n:where(.jev-devtools) .flow-input-details > div {\n display: flex;\n gap: 16px;\n margin: 6px 0;\n}\n:where(.jev-devtools) .flow-input-details code {\n color: #a092a7;\n margin-left: 8px;\n}\n:where(.jev-devtools) .flow-input-details dd {\n margin: 0;\n overflow-wrap: anywhere;\n}\n\n:where(.jev-devtools) .flow-overview-stack {\n display: flex;\n flex-direction: column;\n flex: 1;\n min-height: 0;\n min-width: 0;\n overflow: hidden;\n}\n:where(.jev-devtools) .flow-map-progress {\n display: grid;\n gap: 10px;\n font-size: 14px;\n line-height: 1.5;\n}\n:where(.jev-devtools) .flow-map-progress small {\n color: #b5a6bc;\n}\n\n.jev-devtools .flow-card-body {\n display: flex;\n flex-direction: column;\n gap: 8px;\n width: 100%;\n flex: 1;\n padding: 0;\n border: 0;\n border-radius: 0;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n}\n.jev-devtools .flow-card-body:hover {\n background: transparent;\n}\n:where(.jev-devtools) .flow-collection-controls {\n display: flex;\n align-items: end;\n gap: 10px;\n border-top: 1px solid #49324b;\n margin-top: 4px;\n padding-top: 12px;\n}\n:where(.jev-devtools) .flow-collection-controls label {\n display: grid;\n gap: 6px;\n flex: 1;\n min-width: 0;\n color: #b9aebf;\n font-size: 11px;\n}\n.jev-devtools .flow-collection-controls select {\n width: 100%;\n min-width: 0;\n}\n.jev-devtools .flow-collection-controls button {\n flex: none;\n color: #efb1df;\n}\n:where(.jev-devtools) .flow-collection-expansion {\n position: absolute;\n width: max-content;\n}\n:where(.jev-devtools) .flow-item-frame {\n border: 1px solid #64415d;\n border-radius: 12px;\n background: #100c13;\n overflow: hidden;\n}\n:where(.jev-devtools) .flow-collection-heading {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 24px;\n padding: 16px 20px;\n border-bottom: 1px solid #49324b;\n background: #1c1220;\n}\n:where(.jev-devtools) .flow-collection-heading h3 {\n margin: 5px 0;\n font-size: 16px;\n}\n:where(.jev-devtools) .flow-collection-heading small {\n color: #b9aebf;\n}\n:where(.jev-devtools) .flow-collection-link text {\n fill: #ce9abd;\n font-size: 12px;\n}\n:where(.jev-devtools) .flow-item-placeholder {\n width: 600px;\n padding: 24px;\n line-height: 1.6;\n}\n:where(.jev-devtools) .flow-inline-overview,\n:where(.jev-devtools) .flow-inline-overview > .whole-flow,\n:where(.jev-devtools) .flow-inline-overview > .whole-flow > .flow-workspace {\n display: block;\n flex: none;\n}\n:where(.jev-devtools) .flow-inline-surface {\n position: relative;\n}\n\n:where(.jev-devtools) .flow-item-content {\n min-width: 0;\n animation: jev-flow-item-enter 240ms cubic-bezier(0.22, 1, 0.36, 1) both;\n}\n:where(.jev-devtools) .flow-item-forward {\n --flow-item-offset: 28px;\n}\n:where(.jev-devtools) .flow-item-backward {\n --flow-item-offset: -28px;\n}\n@keyframes jev-flow-item-enter {\n from {\n opacity: 0;\n transform: translateX(var(--flow-item-offset));\n }\n to {\n opacity: 1;\n transform: translateX(0);\n }\n}\n@media (prefers-reduced-motion: reduce) {\n :where(.jev-devtools) .flow-item-content {\n animation: none;\n }\n}\n.jev-devtools .flow-item-arrow {\n position: absolute;\n top: 50%;\n transform: translateY(-50%);\n display: grid;\n place-items: center;\n width: 56px;\n height: 56px;\n padding: 12px;\n margin: 0 auto;\n border: 1px solid #64415d;\n border-radius: 6px;\n background: #211623;\n color: #efa7db;\n}\n.jev-devtools .flow-item-arrow:hover:not(:disabled) {\n background: #382035;\n border-color: #e551ba;\n}\n:where(.jev-devtools) .flow-item-arrow svg {\n width: 28px;\n height: 28px;\n}\n\n:where(.jev-devtools) .flow-item-previous {\n left: -76px;\n}\n:where(.jev-devtools) .flow-item-next {\n right: -76px;\n}\n";
1
+ export const inspectorStyles = "/* Important declarations in the inner shadow context beat page-authored host\n styles, including important ones. Keep the reset here instead of expanding\n the all shorthand into hundreds of inline properties on the host element.\n These qualified host selectors are inert in the public document stylesheet. */\n:host(jev-devtools-root) {\n all: initial !important;\n direction: ltr !important;\n unicode-bidi: isolate !important;\n position: fixed !important;\n inset: 0 !important;\n pointer-events: none !important;\n z-index: 2147483000 !important;\n}\n\n:host(jev-devtools-root)::before,\n:host(jev-devtools-root)::after {\n content: none !important;\n display: none !important;\n}\n\n.jev-devtools {\n all: initial;\n /* all does not reset custom properties. Establish our own inherited defaults;\n cameras and probability cards can still set local values below this root. */\n --diagram-grid-opacity: 1;\n --diagram-grid-weight: 1;\n --lab-noul-probability: 1;\n --flow-item-offset: 0px;\n display: block;\n direction: ltr;\n unicode-bidi: isolate;\n pointer-events: auto;\n color-scheme: dark;\n font:\n 12px system-ui,\n sans-serif;\n color: #eee8ed;\n line-height: 1.5;\n}\n.jev-devtools,\n.jev-devtools * {\n box-sizing: border-box;\n}\n.jev-devtools button,\n.jev-devtools input,\n.jev-devtools select,\n.jev-devtools textarea {\n font: inherit;\n color: inherit;\n border: 1px solid #3b303d;\n border-radius: 5px;\n background: #17121b;\n}\n.jev-devtools button {\n padding: 7px 11px;\n cursor: pointer;\n}\n.jev-devtools button:disabled {\n opacity: 0.45;\n cursor: default;\n}\n.jev-devtools input,\n.jev-devtools select {\n padding: 8px;\n width: 100%;\n}\n.jev-devtools button:hover:not(:disabled) {\n border-color: #e551ba;\n}\n.jev-devtools :is(button, input, select, textarea, summary):focus-visible {\n outline: 2px solid #e551ba;\n outline-offset: 3px;\n}\n.jev-devtools .eyebrow {\n font-size: 10px;\n letter-spacing: 0.12em;\n color: #bc9db5;\n text-transform: uppercase;\n}\n.jev-devtools .jev-inspector {\n height: 100%;\n display: flex;\n color: #eee8ed;\n background: #0d0a0f;\n font:\n 12px \"DM Sans\",\n sans-serif;\n min-height: 0;\n overflow: hidden;\n}\n.jev-devtools .jev-inspector h2 {\n font-size: 15px;\n margin: 0;\n font-weight: 600;\n}\n.jev-devtools .jev-inspector h3 {\n font-size: 13px;\n margin: 0 0 8px;\n}\n.jev-devtools .jev-inspector code,\n.jev-devtools .jev-inspector pre {\n font:\n 11px \"JetBrains Mono\",\n monospace;\n}\n.jev-devtools .jev-inspector pre {\n line-height: 1.7;\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n max-height: 460px;\n overflow: auto;\n background: #100d13;\n padding: 16px;\n border: 1px solid #2e2533;\n border-radius: 5px;\n}\n.jev-devtools .jev-inspector .muted {\n color: #a89bac;\n font-size: 11px;\n line-height: 1.6;\n}\n.jev-devtools .request-list {\n flex: 0 0 300px;\n border-right: 1px solid #302634;\n display: flex;\n flex-direction: column;\n min-height: 0;\n}\n.jev-devtools .request-list > header {\n padding: 17px 14px;\n display: flex;\n justify-content: space-between;\n align-items: center;\n}\n.jev-devtools .request-list h2 span {\n font-size: 11px;\n color: #b796ad;\n margin-left: 5px;\n}\n.jev-devtools .request-list button {\n font-size: 11px;\n}\n.jev-devtools .list-filters {\n display: flex;\n gap: 7px;\n padding: 0 12px 12px;\n}\n.jev-devtools .list-filters input {\n min-width: 0;\n}\n.jev-devtools .list-filters select {\n width: 86px;\n font-size: 11px;\n}\n.jev-devtools .request-items {\n overflow: auto;\n flex: 1;\n}\n.jev-devtools .request-row {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n text-align: left;\n border: 0;\n border-top: 1px solid #251d2a;\n border-radius: 0;\n background: transparent;\n padding: 16px 12px;\n}\n.jev-devtools .request-row.selected {\n background: #251423;\n box-shadow: inset 2px 0 #e551ba;\n}\n.jev-devtools .request-row strong {\n display: block;\n font-size: 12px;\n font-weight: 500;\n}\n.jev-devtools .request-row small {\n display: block;\n font-size: 10px;\n color: #a99aac;\n margin-top: 6px;\n}\n.jev-devtools .row-status {\n font-size: 9px;\n color: #ad9bb0;\n margin-left: auto;\n}\n.jev-devtools .status-dot {\n width: 6px;\n height: 6px;\n display: inline-block;\n border-radius: 50%;\n background: #b09ab7;\n flex-shrink: 0;\n}\n.jev-devtools .status-dot.success {\n background: #b3c79c;\n}\n.jev-devtools .status-dot.pending {\n background: #e551ba;\n}\n.jev-devtools .status-dot.error {\n background: #ee8e85;\n}\n.jev-devtools .request-list footer {\n padding: 12px;\n color: #9e8ea5;\n border-top: 1px solid #302634;\n font-size: 10px;\n display: flex;\n gap: 7px;\n}\n.jev-devtools .request-detail {\n flex: 1;\n min-width: 0;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n.jev-devtools .detail-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 20px;\n padding: 18px 24px 10px;\n}\n.jev-devtools .detail-header h2 {\n margin-top: 6px;\n}\n.jev-devtools .metrics {\n display: flex;\n gap: 24px;\n font:\n 12px \"JetBrains Mono\",\n monospace;\n white-space: nowrap;\n}\n.jev-devtools .metrics small {\n display: block;\n font:\n 9px \"DM Sans\",\n sans-serif;\n color: #97879f;\n margin-top: 6px;\n}\n.jev-devtools .detail-meta {\n display: flex;\n gap: 20px;\n flex-wrap: wrap;\n padding: 0 24px 14px;\n color: #a08aa5;\n}\n.jev-devtools .detail-meta code {\n font-size: 10px;\n}\n.jev-devtools .detail-tabs {\n display: flex;\n gap: 20px;\n border-bottom: 1px solid #302634;\n padding: 0 24px;\n}\n.jev-devtools .detail-tabs button {\n border: 0;\n border-radius: 0;\n background: none;\n padding: 10px 0;\n color: #a89bac;\n font-size: 11px;\n}\n.jev-devtools .detail-tabs button[aria-pressed=\"true\"] {\n box-shadow: inset 0 -2px #e551ba;\n color: #f5c1e6;\n}\n.jev-devtools .detail-body {\n overflow: auto;\n flex: 1;\n min-height: 0;\n padding: 16px 24px 28px;\n background-image: radial-gradient(#342337 0.6px, transparent 0.6px);\n background-size: 16px 16px;\n}\n.jev-devtools .question-node {\n position: relative;\n border: 1px solid #75516c;\n border-radius: 6px;\n padding: 15px;\n background: #110d14;\n min-width: 0;\n}\n/* Retain the legacy question connectors and standalone suppression together.\n Remove only after resolving the narrow-screen visual differences in\n https://github.com/diegoag64/jev-devtools/issues/57. */\n.jev-devtools .question-node:before {\n content: \"\";\n position: absolute;\n right: 100%;\n top: 50%;\n width: 21px;\n border-top: 1px solid #7f416d;\n}\n.jev-devtools .question-branches .question-node:not(:first-child):before {\n display: none;\n}\n.jev-devtools .question-node header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n}\n.jev-devtools .question-node header h3 {\n margin: 0;\n overflow-wrap: anywhere;\n}\n.jev-devtools .type {\n font:\n 9px \"JetBrains Mono\",\n monospace;\n color: #e59acc;\n border: 1px solid #5e3654;\n border-radius: 3px;\n padding: 3px 5px;\n}\n.jev-devtools .answer-value {\n display: block;\n font-size: 20px;\n font-weight: 500;\n margin: 15px 0;\n color: #f0c7e5;\n}\n.jev-devtools .answer-value small {\n font-size: 10px;\n color: #b5a1ba;\n}\n.jev-devtools .probability {\n margin: 10px 0;\n font-size: 10px;\n}\n.jev-devtools .probability > div:first-child {\n display: flex;\n justify-content: space-between;\n gap: 5px;\n margin-bottom: 5px;\n}\n.jev-devtools .probability b {\n font-weight: 400;\n color: #d8abc9;\n}\n.jev-devtools .bar {\n height: 4px;\n background: #302033;\n border-radius: 2px;\n overflow: hidden;\n}\n.jev-devtools .bar i {\n display: block;\n height: 100%;\n background: #e551ba;\n}\n.jev-devtools .confidence {\n font-size: 10px;\n color: #a68cac;\n border-top: 1px solid #302634;\n padding-top: 10px;\n display: flex;\n justify-content: space-between;\n}\n.jev-devtools .confidence b {\n color: #d1bad2;\n font-weight: 400;\n}\n.jev-devtools details {\n margin-top: 12px;\n font-size: 10px;\n color: #b7a0bd;\n}\n.jev-devtools summary {\n cursor: pointer;\n}\n.jev-devtools .standalone {\n margin-bottom: 14px;\n max-width: 750px;\n}\n.jev-devtools .standalone:before {\n display: none;\n}\n.jev-devtools .standalone .answer {\n max-width: 450px;\n}\n.jev-devtools .box-preview {\n position: relative;\n margin-top: 14px;\n min-height: 80px;\n background: #19121c;\n}\n.jev-devtools .box-preview img {\n width: 100%;\n display: block;\n}\n.jev-devtools .bounding-box {\n position: absolute;\n border: 1.5px solid #e551ba;\n background: #e551ba15;\n}\n.jev-devtools .coordinates {\n display: block;\n font-size: 9px !important;\n line-height: 1.5;\n margin-top: 8px;\n color: #ac93b0;\n}\n.jev-devtools .context-image {\n max-width: 400px;\n width: 100%;\n border: 1px solid #75516c;\n border-radius: 6px;\n}\n.jev-devtools .empty {\n padding: 20px;\n color: #a294a8;\n line-height: 1.7;\n}\n.jev-devtools .empty-detail {\n margin: auto;\n padding: 40px;\n text-align: center;\n color: #b2a0ba;\n}\n.jev-devtools .empty-symbol {\n font-size: 36px;\n color: #e551ba;\n display: block;\n margin-bottom: 20px;\n}\n.jev-devtools .empty-detail p {\n font-size: 12px;\n line-height: 1.8;\n}\n.jev-devtools .error-banner {\n border: 1px solid #874b50;\n background: #31191e;\n padding: 12px;\n line-height: 1.5;\n margin-bottom: 16px;\n}\n.jev-devtools .warning {\n color: #e2b97d;\n}\n.jev-devtools .jev-inspector table {\n border-collapse: collapse;\n width: 100%;\n text-align: left;\n}\n.jev-devtools .jev-inspector th,\n.jev-devtools .jev-inspector td {\n border-bottom: 1px solid #302634;\n padding: 14px;\n}\n@media (max-width: 1100px) {\n .jev-devtools .question-branches .question-node:not(:first-child):before {\n display: block;\n }\n .jev-devtools .metrics {\n gap: 12px;\n }\n .jev-devtools .request-list {\n flex-basis: 250px;\n }\n}\n@media (max-width: 700px) {\n .jev-devtools .jev-inspector {\n flex-direction: column;\n }\n .jev-devtools .request-list {\n flex-basis: 140px;\n flex-shrink: 0;\n border-right: 0;\n border-bottom: 1px solid #302634;\n }\n .jev-devtools .request-list > header {\n padding: 6px 12px;\n }\n .jev-devtools .request-list footer {\n display: none;\n }\n .jev-devtools .list-filters {\n padding-bottom: 6px;\n }\n .jev-devtools .request-items {\n display: flex;\n }\n .jev-devtools .request-row {\n min-width: 240px;\n padding: 8px 12px;\n }\n .jev-devtools .detail-header {\n padding: 12px;\n }\n .jev-devtools .detail-meta,\n .jev-devtools .detail-tabs {\n padding-left: 12px;\n padding-right: 12px;\n }\n .jev-devtools .detail-body {\n padding: 12px;\n }\n .jev-devtools .metrics {\n display: none;\n }\n .jev-devtools .question-node:before {\n width: 13px;\n }\n .jev-devtools .question-node {\n padding: 12px;\n }\n}\n.jev-devtools .question-node:before,\n.jev-devtools .question-branches .question-node:not(:first-child):before {\n display: block;\n right: auto;\n left: 50%;\n top: -21px;\n width: 0;\n height: 20px;\n border-top: 0;\n border-left: 1px solid #7f416d;\n}\n.jev-devtools .standalone:before {\n display: none;\n}\n@media (max-width: 900px) {\n .jev-devtools .question-node:before,\n .jev-devtools .question-branches .question-node:not(:first-child):before {\n height: 12px;\n top: -13px;\n }\n .jev-devtools .question-branches .question-node:first-child:before {\n height: 20px;\n top: -21px;\n }\n}\n\n.jev-devtools.jev-drawer {\n margin: 0;\n padding: 0;\n position: fixed;\n z-index: 1000;\n inset: auto 0 0;\n display: flex;\n flex-direction: column;\n background: #0d0a0f;\n color: #eee8ed;\n border-top: 1px solid #604058;\n box-shadow: 0 -12px 48px #0005;\n font:\n 12px \"DM Sans\",\n sans-serif;\n}\n.jev-devtools.jev-drawer[hidden],\n.jev-devtools.jev-drawer-trigger[hidden] {\n display: none;\n}\n.jev-devtools .jev-drawer-resize {\n position: absolute;\n z-index: 1;\n top: -6px;\n width: 100%;\n height: 12px;\n cursor: ns-resize;\n touch-action: none;\n display: flex;\n justify-content: center;\n align-items: center;\n}\n.jev-devtools .jev-drawer-resize span {\n width: 42px;\n height: 3px;\n border-radius: 3px;\n background: #79506f;\n}\n.jev-devtools .jev-drawer-resize:hover span,\n.jev-devtools .jev-drawer-resize:focus-visible span {\n background: #e551ba;\n}\n.jev-devtools .jev-drawer-resize:focus-visible {\n outline: 2px solid #e551ba;\n outline-offset: -2px;\n}\n.jev-devtools .jev-drawer-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n flex: 0 0 46px;\n padding: 0 16px;\n border-bottom: 1px solid #302634;\n}\n.jev-devtools .jev-drawer-header strong {\n display: flex;\n align-items: center;\n gap: 9px;\n font-weight: 600;\n}\n.jev-devtools .jev-drawer-header > div {\n display: flex;\n align-items: center;\n gap: 10px;\n}\n.jev-devtools .jev-drawer-header button {\n background: transparent;\n border-color: transparent;\n font-size: 11px;\n color: #c4afc8;\n}\n.jev-devtools .jev-drawer-header .jev-drawer-close {\n font-size: 22px;\n line-height: 1;\n padding: 4px 8px;\n}\n.jev-devtools .jev-drawer-content {\n flex: 1;\n min-height: 0;\n overflow: hidden;\n}\n.jev-devtools.jev-drawer-trigger {\n position: fixed;\n bottom: 18px;\n right: 18px;\n z-index: 1000;\n display: flex;\n align-items: center;\n gap: 9px;\n padding: 10px 16px;\n border: 1px solid #a24487;\n border-radius: 8px;\n cursor: pointer;\n background: #211321;\n color: #f1cbe6;\n font:\n 600 13px \"DM Sans\",\n sans-serif;\n box-shadow: 0 4px 20px #0005;\n}\n.jev-devtools.jev-drawer-trigger span {\n color: #e551ba;\n font-size: 20px;\n}\n/* Baseline cards and authored left-to-right composition from the lab's Jev presets. */\n.jev-devtools .detail-body.detail-body-flow {\n display: flex;\n padding: 0;\n overflow: hidden;\n}\n.jev-devtools .lab-flow {\n display: flex;\n flex-direction: column;\n flex: 1;\n min-width: 0;\n min-height: 0;\n}\n.jev-devtools .lab-flow-toolbar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n flex-shrink: 0;\n padding: 12px 16px;\n flex-wrap: wrap;\n}\n.jev-devtools .lab-flow-toolbar label {\n display: flex;\n gap: 10px;\n align-items: center;\n font-size: 11px;\n min-width: 0;\n}\n.jev-devtools .lab-flow-toolbar select {\n width: auto;\n max-width: 240px;\n font-size: 11px;\n}\n.jev-devtools .lab-flow-toolbar > div {\n display: flex;\n gap: 8px;\n align-items: center;\n}\n.jev-devtools .lab-flow-toolbar button {\n font-size: 11px;\n}\n.jev-devtools .lab-flow-toolbar output {\n font-size: 11px;\n min-width: 35px;\n text-align: center;\n}\n.jev-devtools .lab-flow-viewport {\n position: relative;\n flex: 1;\n min-height: 100px;\n overflow: hidden;\n background-color: #0d0a0f;\n background-image: radial-gradient(\n rgb(70 52 73 / var(--diagram-grid-opacity, 1))\n calc(0.8px * var(--diagram-grid-weight, 1)),\n transparent calc(0.8px * var(--diagram-grid-weight, 1))\n );\n background-size: 20px 20px;\n border-block: 1px solid #302634;\n cursor: grab;\n touch-action: none;\n user-select: none;\n overscroll-behavior: contain;\n}\n.jev-devtools .lab-flow-viewport[data-panning] {\n cursor: grabbing;\n}\n.jev-devtools .lab-flow-viewport:focus-visible {\n outline: 2px solid #e551ba;\n outline-offset: -2px;\n}\n.jev-devtools .lab-flow-caption {\n flex-shrink: 0;\n padding: 8px 16px;\n margin: 0;\n}\n.jev-devtools .lab-gesture-hint {\n display: block;\n color: #c6a7bd;\n}\n.jev-devtools .lab-flow-canvas {\n position: absolute;\n inset: 0 auto auto 0;\n transform-origin: top left;\n will-change: transform;\n font-family: system-ui, sans-serif;\n color: #f5f3f6;\n}\n.jev-devtools .lab-connections {\n position: absolute;\n inset: 0;\n overflow: visible;\n}\n.jev-devtools .lab-connections path {\n fill: none;\n stroke: #645a68;\n stroke-width: 1;\n}\n.jev-devtools .lab-connections path.active {\n stroke: #968592;\n}\n.jev-devtools .lab-connections text {\n fill: #b7b1bc;\n font:\n 12px ui-monospace,\n monospace;\n}\n.jev-devtools .lab-flow-dot {\n fill: #e551ba;\n}\n.jev-devtools .lab-card {\n position: absolute;\n border: 1.3px solid #f5f3f6;\n border-radius: 14px;\n padding: 16px;\n background: #0d0a0f;\n overflow: hidden;\n}\n.jev-devtools .lab-eyebrow {\n display: block;\n color: #e551ba;\n font:\n 500 10px ui-monospace,\n monospace;\n letter-spacing: 1px;\n margin: 0 0 7px;\n}\n.jev-devtools .lab-card h3 {\n font-size: 16px;\n font-weight: 600;\n line-height: 1.3;\n letter-spacing: -0.2px;\n margin: 0 0 10px;\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n.jev-devtools .lab-card p {\n font-size: 13px;\n line-height: 1.4;\n margin: 10px 0;\n}\n.jev-devtools .lab-copy {\n display: -webkit-box;\n -webkit-line-clamp: 5;\n -webkit-box-orient: vertical;\n overflow: hidden;\n overflow-wrap: anywhere;\n}\n.jev-devtools .lab-probability {\n margin: 17px 0;\n}\n.jev-devtools .lab-probability > div:first-child {\n display: flex;\n justify-content: space-between;\n gap: 10px;\n margin-bottom: 9px;\n font-size: 15px;\n}\n.jev-devtools .lab-probability span {\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n.jev-devtools .lab-probability code {\n font:\n 13px ui-monospace,\n monospace;\n}\n.jev-devtools .lab-track {\n height: 7px;\n border-radius: 4px;\n background: #f5f3f61a;\n overflow: hidden;\n}\n.jev-devtools .lab-track i {\n display: block;\n height: 100%;\n border-radius: 4px;\n background: #e551ba;\n}\n.jev-devtools .lab-track i.lab-alternative {\n opacity: 0.35;\n}\n.jev-devtools .lab-card .lab-result {\n font-size: 14px;\n margin-top: 24px;\n}\n.jev-devtools .lab-card .lab-small {\n font-size: 12px;\n color: #b7b1bc;\n margin-top: 20px;\n}\n.jev-devtools .lab-idle {\n opacity: 0.65;\n}\n.jev-devtools .lab-scale {\n position: relative;\n margin-top: 18px;\n border-top: 1px solid #b7b1bc;\n display: flex;\n justify-content: space-between;\n padding-top: 9px;\n}\n.jev-devtools .lab-scale > span {\n position: absolute;\n top: -5px;\n width: 9px;\n height: 9px;\n border-radius: 50%;\n background: #e551ba;\n transform: translateX(-50%);\n}\n.jev-devtools .lab-big {\n display: block;\n font-size: 36px;\n font-weight: 500;\n margin-top: 26px;\n}\n.jev-devtools .lab-endpoints {\n display: flex;\n justify-content: space-between;\n font-size: 12px;\n color: #b7b1bc;\n margin-top: 10px;\n}\n.jev-devtools .lab-image {\n position: relative;\n}\n.jev-devtools .lab-image img {\n display: block;\n width: 100%;\n}\n.jev-devtools .lab-box {\n position: absolute;\n border: 1.5px solid #e551ba;\n background: #e551ba15;\n}\n.jev-devtools .lab-source-image {\n width: 100%;\n max-height: 105px;\n object-fit: contain;\n}\n.jev-devtools .lab-coordinates {\n font-family: ui-monospace, monospace;\n}\n@media (prefers-reduced-motion: reduce) {\n .jev-devtools .lab-flow-dot {\n display: none;\n }\n}\n/* The public TypeSafe mark travels with the package, independent of host assets. */\n.jev-devtools .typesafe-mark {\n display: inline-block;\n flex: 0 0 23px;\n width: 23px;\n height: 32px;\n overflow: hidden;\n line-height: 0;\n}\n.jev-devtools .typesafe-mark img {\n display: block;\n height: 32px;\n width: auto;\n max-width: none;\n}\n\n.jev-devtools .detail-body-questions {\n display: flex;\n flex-direction: column;\n overflow: auto;\n}\n.jev-devtools .questions-list {\n display: flex;\n flex-direction: column;\n flex: 1;\n min-height: 180px;\n}\n.jev-devtools .questions-navigation {\n display: flex;\n align-items: end;\n gap: 8px;\n flex-wrap: wrap;\n flex-shrink: 0;\n}\n.jev-devtools .questions-navigation label {\n display: flex;\n align-items: center;\n gap: 8px;\n white-space: nowrap;\n flex: 1;\n min-width: 240px;\n max-width: 480px;\n font-size: 11px;\n color: #bc9db5;\n}\n.jev-devtools .questions-navigation select {\n display: block;\n min-width: 0;\n}\n.jev-devtools .questions-navigation > span {\n align-self: center;\n color: #a89bac;\n}\n.jev-devtools .questions-help {\n margin: 8px 0;\n flex-shrink: 0;\n}\n.jev-devtools .questions-viewport {\n flex: 1;\n min-height: 0;\n overflow: auto;\n overflow-anchor: none;\n scrollbar-gutter: stable;\n padding: 3px;\n}\n.jev-devtools .questions-spacer {\n position: relative;\n width: 100%;\n}\n.jev-devtools .questions-row {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n padding-bottom: 14px;\n}\n.jev-devtools .questions-row .standalone {\n margin-bottom: 0;\n}\n.jev-devtools .questions-viewport:focus-visible,\n.jev-devtools .questions-row article:focus-visible {\n outline: 2px solid #e551ba;\n outline-offset: -2px;\n}\n\n/* Reserve measured card geometry while rapid scrolling uses a light preview. */\n.jev-devtools .questions-row[data-preview=\"true\"] article {\n height: 100%;\n}\n.jev-devtools .question-scroll-preview p {\n margin-top: 16px;\n}\n\n/* The full distribution stays at reading size, outside the zoomable canvas. */\n.jev-devtools .lab-flow-viewport[hidden] {\n display: none;\n}\n.jev-devtools .lab-choice-browser {\n display: flex;\n flex-direction: column;\n flex: 1;\n min-height: 0;\n padding: 8px 16px;\n gap: 10px;\n}\n.jev-devtools .lab-choice-controls {\n display: flex;\n gap: 12px;\n align-items: end;\n flex-wrap: wrap;\n font-size: 12px;\n}\n.jev-devtools .lab-choice-controls label {\n display: grid;\n gap: 5px;\n}\n.jev-devtools .lab-choice-table-scroll {\n overflow: auto;\n overscroll-behavior: contain;\n min-height: 0;\n}\n.jev-devtools .lab-choice-browser table {\n width: 100%;\n border-collapse: collapse;\n font-size: 13px;\n}\n.jev-devtools .lab-choice-browser caption {\n text-align: left;\n padding: 8px 0;\n color: #aaa0af;\n}\n.jev-devtools .lab-choice-browser th,\n.jev-devtools .lab-choice-browser td {\n padding: 9px 12px;\n border-bottom: 1px solid #302634;\n text-align: left;\n}\n.jev-devtools .lab-choice-browser th {\n overflow-wrap: anywhere;\n max-width: 320px;\n}\n.jev-devtools .lab-choice-browser td {\n font-variant-numeric: tabular-nums;\n}\n.jev-devtools .lab-choice-browser code {\n white-space: normal;\n overflow-wrap: anywhere;\n}\n.jev-devtools .lab-choice-returned {\n display: block;\n font-size: 10px;\n color: #e551ba;\n}\n.jev-devtools .lab-choice-pagination {\n display: flex;\n align-items: center;\n gap: 12px;\n justify-content: space-between;\n font-size: 12px;\n}\n.jev-devtools .lab-choice-remainder button {\n margin-top: 7px;\n}\n\n.jev-devtools .lab-flow-compact .lab-output {\n padding: 12px;\n height: 104px;\n}\n.jev-devtools .lab-flow-compact .lab-output .lab-eyebrow {\n margin-bottom: 6px;\n}\n.jev-devtools .lab-flow-compact .lab-output h3 {\n font-size: 16px;\n margin-bottom: 8px;\n}\n.jev-devtools .lab-flow-compact .lab-output p {\n font-size: 12px;\n margin: 4px 0;\n}\n.jev-devtools .lab-flow-compact .lab-result {\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n overflow-wrap: anywhere;\n}\n.jev-devtools .lab-choice-remainder {\n padding: 12px;\n}\n.jev-devtools .lab-choice-remainder .lab-small {\n margin: 8px 0 0;\n font-size: 11px;\n}\n\n.jev-devtools .detail-body.detail-body-flow {\n flex-direction: column;\n}\n.jev-devtools .detail-body-flow > .warning,\n.jev-devtools .detail-body-flow > .error-banner {\n flex-shrink: 0;\n margin: 6px 16px;\n}\n.jev-devtools .lab-choice-browser {\n overflow: auto;\n}\n.jev-devtools .lab-choice-table-scroll {\n flex: 1 0 110px;\n}\n\n/* Only freshly revealed branches enter; pan/zoom remains on the canvas. */\n.jev-devtools .lab-card-enter {\n animation: jev-card-enter 280ms cubic-bezier(0.2, 0.8, 0.2, 1) backwards;\n}\n.jev-devtools .lab-branch-enter {\n stroke-dasharray: 1;\n animation: jev-branch-enter 280ms ease-out backwards;\n}\n@keyframes jev-card-enter {\n from {\n opacity: 0;\n transform: translateX(-16px) scale(0.96);\n }\n}\n@keyframes jev-branch-enter {\n from {\n opacity: 0;\n stroke-dashoffset: 1;\n }\n to {\n opacity: 1;\n stroke-dashoffset: 0;\n }\n}\n@media (prefers-reduced-motion: reduce) {\n .jev-devtools .lab-card-enter,\n .jev-devtools .lab-branch-enter {\n animation: none;\n }\n}\n\n/* Connected requests share one camera and one opt-in full request detail.\n :where scopes these selectors without changing their existing specificity. */\n:where(.jev-devtools) .whole-flow {\n min-width: 0;\n overflow: hidden;\n}\n:where(.jev-devtools) .whole-flow > .detail-header,\n:where(.jev-devtools) .whole-flow > .lab-flow-toolbar {\n flex-shrink: 0;\n}\n:where(.jev-devtools) .flow-workspace {\n display: flex;\n flex-direction: column;\n flex: 1;\n min-height: 0;\n overflow: hidden;\n}\n:where(.jev-devtools) .flow-details-panel {\n flex: none;\n min-width: 0;\n background: #130f17;\n border-top: 1px solid #342a38;\n}\n:where(.jev-devtools) .flow-details-bar {\n display: flex;\n align-items: center;\n gap: 12px;\n height: 36px;\n padding: 0 12px;\n}\n.jev-devtools .flow-details-bar button {\n flex: none;\n padding: 4px 8px;\n font-size: 11px;\n}\n:where(.jev-devtools) .flow-details-bar > span {\n color: #b5a6bc;\n font-size: 11px;\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n:where(.jev-devtools) .flow-details-body {\n overflow: auto;\n overscroll-behavior: contain;\n scrollbar-gutter: stable;\n}\n:where(.jev-devtools) .flow-details-resize {\n display: flex;\n align-items: center;\n justify-content: center;\n height: 8px;\n cursor: ns-resize;\n touch-action: none;\n user-select: none;\n}\n:where(.jev-devtools) .flow-details-resize span {\n width: 40px;\n height: 2px;\n border-radius: 2px;\n background: #6c5874;\n}\n:where(.jev-devtools) .flow-details-resize:hover,\n:where(.jev-devtools) .flow-details-resize:focus-visible {\n background: #392239;\n outline: 1px solid #e551ba;\n outline-offset: -1px;\n}\n:where(.jev-devtools) .flow-overview-viewport {\n position: relative;\n min-height: 0;\n flex: 1 1 0;\n overflow: hidden;\n touch-action: none;\n cursor: grab;\n background-image: radial-gradient(\n rgb(83 67 87 / var(--diagram-grid-opacity, 1))\n calc(1px * var(--diagram-grid-weight, 1)),\n transparent calc(1px * var(--diagram-grid-weight, 1))\n );\n background-size: 20px 20px;\n}\n:where(.jev-devtools) .flow-overview-viewport[data-panning] {\n cursor: grabbing;\n}\n:where(.jev-devtools) .flow-overview-canvas {\n position: absolute;\n transform-origin: 0 0;\n}\n:where(.jev-devtools) .flow-connections {\n position: absolute;\n inset: 0;\n overflow: visible;\n color: #c0b8c4;\n pointer-events: none;\n}\n:where(.jev-devtools) .flow-step {\n position: absolute;\n min-height: 172px;\n overflow-wrap: anywhere;\n display: flex;\n flex-direction: column;\n gap: 8px;\n text-align: left;\n padding: 14px;\n border: 1px solid #736476;\n border-radius: 12px;\n background: #151017;\n color: inherit;\n}\n:where(.jev-devtools) .flow-step strong {\n white-space: normal;\n width: 100%;\n}\n:where(.jev-devtools) .flow-step-description {\n overflow-wrap: anywhere;\n font-size: 12px;\n}\n:where(.jev-devtools) .flow-step.flow-request {\n display: block;\n padding: 0;\n background: #0d0a0f;\n /* Skip offscreen paint/layout while preserving each question/expansion state. */\n content-visibility: auto;\n contain-intrinsic-size: auto 640px;\n}\n:where(.jev-devtools) .flow-request-heading {\n width: 100%;\n min-height: 64px;\n gap: 20px;\n display: flex;\n align-items: center;\n justify-content: space-between;\n text-align: left;\n padding: 12px 20px;\n border: 0;\n border-bottom: 1px solid #342a38;\n border-radius: 12px 12px 0 0;\n background: transparent;\n color: inherit;\n}\n:where(.jev-devtools) .flow-request-heading .eyebrow {\n display: block;\n margin-bottom: 4px;\n}\n.jev-devtools .lab-flow-embedded {\n width: 1120px;\n display: block;\n}\n.jev-devtools .lab-flow-surface {\n position: relative;\n width: 1120px;\n}\n.jev-devtools .lab-flow-surface[hidden] {\n display: none;\n}\n.jev-devtools .lab-flow-embedded .lab-flow-canvas {\n will-change: auto;\n}\n.jev-devtools .lab-flow-embedded .lab-choice-browser {\n height: 520px;\n}\n:where(.jev-devtools) .flow-step.selected {\n outline: 2px solid #e551ba;\n outline-offset: 3px;\n}\n:where(.jev-devtools) .flow-step[data-status=\"completed\"] {\n border-color: #e551ba;\n}\n:where(.jev-devtools) .flow-step[data-status=\"running\"] {\n border-color: #e551ba;\n background: #2b1728;\n}\n:where(.jev-devtools) .flow-step[data-status=\"untaken\"],\n:where(.jev-devtools) .flow-step[data-status=\"blocked\"] {\n color: #948797;\n border-color: #403746;\n background: #151017;\n}\n:where(.jev-devtools) .flow-step[data-status=\"error\"] {\n border-color: #f17c78;\n}\n:where(.jev-devtools) .flow-step[data-status=\"cancelled\"] {\n border-style: dashed;\n}\n:where(.jev-devtools) .flow-selection {\n padding: 12px 20px;\n border-top: 1px solid #342a38;\n}\n:where(.jev-devtools) .flow-selection p {\n margin: 6px 0;\n overflow-wrap: anywhere;\n}\n:where(.jev-devtools) .flow-request-detail {\n min-height: 450px;\n border-top: 1px solid #534357;\n}\n:where(.jev-devtools) .flow-request-detail > .request-detail {\n min-height: 450px;\n}\n\n:where(.jev-devtools) .flow-card-heading,\n:where(.jev-devtools) .flow-card-footer,\n:where(.jev-devtools) .flow-join-summary,\n:where(.jev-devtools) .flow-lane {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n width: 100%;\n}\n:where(.jev-devtools) .flow-card-heading {\n justify-content: flex-start;\n}\n:where(.jev-devtools) .flow-task .flow-kind-icon {\n border: 1px solid currentColor;\n border-radius: 6px;\n padding: 4px;\n}\n:where(.jev-devtools) .flow-task-preview {\n display: grid;\n gap: 8px;\n min-width: 0;\n}\n.jev-devtools .flow-task-preview .diagram-content {\n max-height: 280px;\n padding: 8px 10px;\n border: 1px solid #35283b;\n border-radius: 4px;\n background: #100c13;\n}\n:where(.jev-devtools) .flow-task-preview small {\n color: #a89bac;\n font-size: 11px;\n}\n:where(.jev-devtools) .flow-task-details {\n display: grid;\n gap: 12px;\n}\n:where(.jev-devtools) .flow-task-details details {\n min-width: 0;\n}\n:where(.jev-devtools) .flow-task-details summary {\n cursor: pointer;\n margin-bottom: 8px;\n}\n:where(.jev-devtools) .flow-task-details .diagram-content {\n max-height: 320px;\n overflow: auto;\n}\n:where(.jev-devtools) .flow-kind-icon {\n width: 26px;\n height: 26px;\n color: #e551ba;\n flex-shrink: 0;\n}\n:where(.jev-devtools) .flow-kind-icon svg {\n width: 100%;\n height: 100%;\n}\n:where(.jev-devtools) .flow-card-heading .eyebrow {\n font-size: 10px;\n}\n:where(.jev-devtools) .flow-status {\n margin-left: auto;\n font-size: 10px;\n border-radius: 20px;\n padding: 4px 8px;\n background: #342031;\n color: #f0b7e0;\n}\n:where(.jev-devtools) .flow-step[data-status=\"untaken\"] .flow-status {\n background: #241e28;\n color: #948797;\n}\n:where(.jev-devtools) .flow-step[data-status=\"untaken\"] .flow-kind-icon {\n color: #948797;\n}\n:where(.jev-devtools) .flow-card-body > strong {\n white-space: normal;\n font-size: 18px;\n line-height: 1.25;\n overflow-wrap: anywhere;\n}\n:where(.jev-devtools) .flow-step-description {\n line-height: 1.5;\n color: #b9aebf;\n white-space: pre-wrap;\n}\n:where(.jev-devtools) .flow-card-footer {\n margin-top: auto;\n padding-top: 10px;\n border-top: 1px solid #342a38;\n font-size: 10px;\n color: #a092a7;\n}\n:where(.jev-devtools) .flow-card-footer code {\n max-width: 45%;\n white-space: normal;\n overflow-wrap: anywhere;\n}\n:where(.jev-devtools) .flow-card-footer small {\n text-align: right;\n}\n:where(.jev-devtools) .flow-join-summary {\n margin-top: 4px;\n font-size: 11px;\n color: #c9b9cd;\n}\n:where(.jev-devtools) .flow-join-summary > span {\n color: #ef9ed9;\n font-variant-numeric: tabular-nums;\n}\n:where(.jev-devtools) .flow-lanes {\n display: flex;\n flex-direction: column;\n gap: 4px;\n width: 100%;\n}\n:where(.jev-devtools) .flow-lane {\n border-radius: 5px;\n background: #211923;\n padding: 6px 8px;\n font-size: 11px;\n min-height: 28px;\n}\n:where(.jev-devtools) .flow-lane > span {\n min-width: 0;\n overflow-wrap: anywhere;\n}\n:where(.jev-devtools) .flow-lane > span > span {\n color: #ef9ed9;\n margin-right: 4px;\n}\n:where(.jev-devtools) .flow-lane small {\n color: #b9aebf;\n}\n:where(.jev-devtools) .flow-lane-details {\n padding-left: 18px;\n font-size: 12px;\n}\n:where(.jev-devtools) .flow-request-heading > span:first-child {\n min-width: 0;\n max-width: 80%;\n}\n:where(.jev-devtools) .flow-request-description {\n display: block;\n white-space: pre-wrap;\n font-size: 12px;\n line-height: 1.5;\n color: #b9aebf;\n margin-top: 5px;\n overflow-wrap: anywhere;\n}\n\n/* Structured context stays within the card; its gestures never move the camera. */\n.jev-devtools .lab-content-card {\n display: flex;\n flex-direction: column;\n padding: 14px;\n}\n.jev-devtools .lab-content-card h3 {\n font-size: 14px;\n margin-bottom: 8px;\n flex-shrink: 0;\n}\n.jev-devtools .lab-content-card .lab-eyebrow {\n font-size: 9px;\n margin-bottom: 5px;\n flex-shrink: 0;\n}\n.jev-devtools .diagram-content {\n min-height: 0;\n flex: 1;\n overflow: auto;\n overscroll-behavior: contain;\n touch-action: pan-y;\n scrollbar-width: thin;\n scrollbar-color: #514256 transparent;\n padding-right: 4px;\n font-size: 12px;\n line-height: 1.5;\n color: #d5cdd9;\n cursor: auto;\n user-select: text;\n}\n.jev-devtools .diagram-content:focus-visible {\n outline: 1px solid #e551ba;\n outline-offset: -1px;\n border-radius: 3px;\n}\n.jev-devtools .diagram-property {\n display: flex;\n justify-content: space-between;\n align-items: baseline;\n gap: 8px;\n padding: 5px 0;\n border-bottom: 1px solid #261e2a;\n}\n.jev-devtools .diagram-property-text {\n display: block;\n}\n.jev-devtools .diagram-property-key {\n font:\n 10px/1.5 ui-monospace,\n monospace;\n color: #c8a8c8;\n overflow-wrap: anywhere;\n min-width: 0;\n}\n.jev-devtools .diagram-property-text > .diagram-property-key {\n display: block;\n margin-bottom: 2px;\n}\n.jev-devtools .diagram-text-value {\n display: block;\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n.jev-devtools .diagram-text-preview {\n display: -webkit-box;\n -webkit-line-clamp: 3;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n.jev-devtools .diagram-value-number {\n color: #a6d6ca;\n font:\n 11px/1.5 ui-monospace,\n monospace;\n font-variant-numeric: tabular-nums;\n overflow-wrap: anywhere;\n}\n.jev-devtools .diagram-value-boolean {\n color: #c4b1ed;\n font:\n 11px/1.5 ui-monospace,\n monospace;\n}\n.jev-devtools .diagram-value-empty {\n color: #9c8ca4;\n font:\n 10px/1.5 ui-monospace,\n monospace;\n}\n.jev-devtools .diagram-property-disclosure {\n width: 100%;\n display: flex;\n align-items: baseline;\n gap: 5px;\n padding: 6px 0;\n border: 0;\n border-bottom: 1px solid #261e2a;\n border-radius: 0;\n background: transparent;\n text-align: left;\n color: inherit;\n}\n.jev-devtools .diagram-property-disclosure:hover {\n background: #211723;\n}\n.jev-devtools .diagram-property-caret {\n color: #ab8aab;\n flex-shrink: 0;\n}\n.jev-devtools .diagram-property-type {\n color: #9c8ca4;\n font:\n 10px/1.5 ui-monospace,\n monospace;\n margin-left: auto;\n flex-shrink: 0;\n}\n.jev-devtools .diagram-property-children {\n margin-left: 4px;\n padding-left: 9px;\n border-left: 1px solid #49324b;\n}\n.jev-devtools .diagram-content-toggle {\n display: block;\n padding: 3px 0;\n border: 0;\n border-radius: 0;\n background: transparent;\n color: #e9a4d7;\n font-size: 10px;\n text-align: left;\n}\n.jev-devtools .diagram-content-toggle:hover {\n color: #ffd9f3;\n text-decoration: underline;\n}\n\n/* Noul is a probability of true; its two outputs carry p and 1-p.\n Keep the fill opaque, with readable text even at zero probability. */\n.jev-devtools .lab-noul-output {\n border-color: rgb(\n 245 243 246 / calc(0.35 + 0.65 * var(--lab-noul-probability, 1))\n );\n}\n.jev-devtools .lab-noul-output > h3,\n.jev-devtools .lab-noul-output > p {\n opacity: calc(0.65 + 0.35 * var(--lab-noul-probability, 1));\n}\n\n:where(.jev-devtools) .flow-condition-rule {\n display: block;\n padding: 10px 12px;\n border: 1px solid #49324b;\n border-radius: 6px;\n background: #211723;\n width: 100%;\n}\n:where(.jev-devtools) .flow-condition-label {\n display: block;\n font-size: 10px;\n color: #ba9dbe;\n margin-bottom: 5px;\n}\n:where(.jev-devtools) .flow-condition-rule code {\n display: block;\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n font:\n 12px/1.6 ui-monospace,\n monospace;\n color: #f2d7eb;\n}\n\n:where(.jev-devtools) .flow-condition-incoming {\n display: block;\n padding-bottom: 10px;\n margin-bottom: 10px;\n border-bottom: 1px solid #49324b;\n}\n:where(.jev-devtools) .flow-condition-input {\n display: flex;\n justify-content: space-between;\n align-items: baseline;\n gap: 12px;\n font-size: 12px;\n padding: 3px 0;\n}\n:where(.jev-devtools) .flow-condition-input > span {\n min-width: 0;\n overflow-wrap: anywhere;\n}\n:where(.jev-devtools) .flow-condition-input > b {\n font-size: 17px;\n font-weight: 600;\n color: #f2d7eb;\n overflow-wrap: anywhere;\n white-space: normal;\n max-width: 65%;\n}\n:where(.jev-devtools) .flow-input-details {\n font-size: 12px;\n}\n:where(.jev-devtools) .flow-input-details > div {\n display: flex;\n gap: 16px;\n margin: 6px 0;\n}\n:where(.jev-devtools) .flow-input-details code {\n color: #a092a7;\n margin-left: 8px;\n}\n:where(.jev-devtools) .flow-input-details dd {\n margin: 0;\n overflow-wrap: anywhere;\n}\n\n:where(.jev-devtools) .flow-overview-stack {\n display: flex;\n flex-direction: column;\n flex: 1;\n min-height: 0;\n min-width: 0;\n overflow: hidden;\n}\n:where(.jev-devtools) .flow-map-progress {\n display: grid;\n gap: 10px;\n font-size: 14px;\n line-height: 1.5;\n}\n:where(.jev-devtools) .flow-map-progress small {\n color: #b5a6bc;\n}\n\n.jev-devtools .flow-card-body {\n display: flex;\n flex-direction: column;\n gap: 8px;\n width: 100%;\n flex: 1;\n padding: 0;\n border: 0;\n border-radius: 0;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n}\n.jev-devtools .flow-card-body:hover {\n background: transparent;\n}\n:where(.jev-devtools) .flow-collection-controls {\n display: flex;\n align-items: end;\n gap: 10px;\n border-top: 1px solid #49324b;\n margin-top: 4px;\n padding-top: 12px;\n}\n:where(.jev-devtools) .flow-collection-controls label {\n display: grid;\n gap: 6px;\n flex: 1;\n min-width: 0;\n color: #b9aebf;\n font-size: 11px;\n}\n.jev-devtools .flow-collection-controls select {\n width: 100%;\n min-width: 0;\n}\n.jev-devtools .flow-collection-controls button {\n flex: none;\n color: #efb1df;\n}\n:where(.jev-devtools) .flow-collection-expansion {\n position: absolute;\n width: max-content;\n}\n:where(.jev-devtools) .flow-item-frame {\n border: 1px solid #64415d;\n border-radius: 12px;\n background: #100c13;\n overflow: hidden;\n}\n:where(.jev-devtools) .flow-collection-heading {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 24px;\n padding: 16px 20px;\n border-bottom: 1px solid #49324b;\n background: #1c1220;\n}\n:where(.jev-devtools) .flow-collection-heading h3 {\n margin: 5px 0;\n font-size: 16px;\n}\n:where(.jev-devtools) .flow-collection-heading small {\n color: #b9aebf;\n}\n:where(.jev-devtools) .flow-collection-link text {\n fill: #ce9abd;\n font-size: 12px;\n}\n:where(.jev-devtools) .flow-item-placeholder {\n width: 600px;\n padding: 24px;\n line-height: 1.6;\n}\n:where(.jev-devtools) .flow-inline-overview,\n:where(.jev-devtools) .flow-inline-overview > .whole-flow,\n:where(.jev-devtools) .flow-inline-overview > .whole-flow > .flow-workspace {\n display: block;\n flex: none;\n}\n:where(.jev-devtools) .flow-inline-surface {\n position: relative;\n}\n\n:where(.jev-devtools) .flow-item-content {\n min-width: 0;\n animation: jev-flow-item-enter 240ms cubic-bezier(0.22, 1, 0.36, 1) both;\n}\n:where(.jev-devtools) .flow-item-forward {\n --flow-item-offset: 28px;\n}\n:where(.jev-devtools) .flow-item-backward {\n --flow-item-offset: -28px;\n}\n@keyframes jev-flow-item-enter {\n from {\n opacity: 0;\n transform: translateX(var(--flow-item-offset));\n }\n to {\n opacity: 1;\n transform: translateX(0);\n }\n}\n@media (prefers-reduced-motion: reduce) {\n :where(.jev-devtools) .flow-item-content {\n animation: none;\n }\n}\n.jev-devtools .flow-item-arrow {\n position: absolute;\n top: 50%;\n transform: translateY(-50%);\n display: grid;\n place-items: center;\n width: 56px;\n height: 56px;\n padding: 12px;\n margin: 0 auto;\n border: 1px solid #64415d;\n border-radius: 6px;\n background: #211623;\n color: #efa7db;\n}\n.jev-devtools .flow-item-arrow:hover:not(:disabled) {\n background: #382035;\n border-color: #e551ba;\n}\n:where(.jev-devtools) .flow-item-arrow svg {\n width: 28px;\n height: 28px;\n}\n\n:where(.jev-devtools) .flow-item-previous {\n left: -76px;\n}\n:where(.jev-devtools) .flow-item-next {\n right: -76px;\n}\n.jev-devtools .flow-run-summary {\n text-align: right;\n}\n.jev-devtools .flow-fixture-button {\n display: block;\n margin: 6px 0 0 auto;\n /* Keep the header still when the shorter confirmation replaces the label. */\n min-width: 96px;\n font-size: 11px;\n transition: background-color 160ms ease, border-color 160ms ease;\n}\n.jev-devtools .flow-fixture-button[data-copied] {\n border-color: #e551ba;\n background: #382035;\n animation: jev-fixture-copied 200ms ease-out;\n}\n@keyframes jev-fixture-copied {\n from {\n transform: scale(0.96);\n }\n to {\n transform: scale(1);\n }\n}\n@media (prefers-reduced-motion: reduce) {\n .jev-devtools .flow-fixture-button {\n transition: none;\n }\n .jev-devtools .flow-fixture-button[data-copied] {\n animation: none;\n }\n}\n.jev-devtools .flow-fixture {\n display: flex;\n flex-direction: column;\n align-items: flex-end;\n flex-shrink: 0;\n gap: 4px;\n padding: 0 24px;\n text-align: right;\n}\n.jev-devtools .flow-fixture p {\n margin: 0;\n max-width: 360px;\n font-size: 11px;\n}\n.jev-devtools .flow-fixture details {\n max-width: 360px;\n font-size: 11px;\n}\n.jev-devtools .flow-fixture details ul {\n max-height: 120px;\n margin: 4px 0 0;\n padding: 0;\n overflow: auto;\n list-style: none;\n font-family: \"JetBrains Mono\", monospace;\n overflow-wrap: anywhere;\n}\n.jev-devtools .flow-fixture details ul:focus-visible {\n outline: 2px solid #e551ba;\n outline-offset: 2px;\n}\n.jev-devtools .flow-fixture textarea {\n width: 360px;\n max-width: 100%;\n height: 120px;\n padding: 8px;\n font:\n 11px \"JetBrains Mono\",\n monospace;\n text-align: left;\n}\n";
@@ -43,7 +43,8 @@
43
43
  }
44
44
  .jev-devtools button,
45
45
  .jev-devtools input,
46
- .jev-devtools select {
46
+ .jev-devtools select,
47
+ .jev-devtools textarea {
47
48
  font: inherit;
48
49
  color: inherit;
49
50
  border: 1px solid #3b303d;
@@ -66,7 +67,7 @@
66
67
  .jev-devtools button:hover:not(:disabled) {
67
68
  border-color: #e551ba;
68
69
  }
69
- .jev-devtools :is(button, input, select, summary):focus-visible {
70
+ .jev-devtools :is(button, input, select, textarea, summary):focus-visible {
70
71
  outline: 2px solid #e551ba;
71
72
  outline-offset: 3px;
72
73
  }
@@ -1868,3 +1869,76 @@
1868
1869
  :where(.jev-devtools) .flow-item-next {
1869
1870
  right: -76px;
1870
1871
  }
1872
+ .jev-devtools .flow-run-summary {
1873
+ text-align: right;
1874
+ }
1875
+ .jev-devtools .flow-fixture-button {
1876
+ display: block;
1877
+ margin: 6px 0 0 auto;
1878
+ /* Keep the header still when the shorter confirmation replaces the label. */
1879
+ min-width: 96px;
1880
+ font-size: 11px;
1881
+ transition: background-color 160ms ease, border-color 160ms ease;
1882
+ }
1883
+ .jev-devtools .flow-fixture-button[data-copied] {
1884
+ border-color: #e551ba;
1885
+ background: #382035;
1886
+ animation: jev-fixture-copied 200ms ease-out;
1887
+ }
1888
+ @keyframes jev-fixture-copied {
1889
+ from {
1890
+ transform: scale(0.96);
1891
+ }
1892
+ to {
1893
+ transform: scale(1);
1894
+ }
1895
+ }
1896
+ @media (prefers-reduced-motion: reduce) {
1897
+ .jev-devtools .flow-fixture-button {
1898
+ transition: none;
1899
+ }
1900
+ .jev-devtools .flow-fixture-button[data-copied] {
1901
+ animation: none;
1902
+ }
1903
+ }
1904
+ .jev-devtools .flow-fixture {
1905
+ display: flex;
1906
+ flex-direction: column;
1907
+ align-items: flex-end;
1908
+ flex-shrink: 0;
1909
+ gap: 4px;
1910
+ padding: 0 24px;
1911
+ text-align: right;
1912
+ }
1913
+ .jev-devtools .flow-fixture p {
1914
+ margin: 0;
1915
+ max-width: 360px;
1916
+ font-size: 11px;
1917
+ }
1918
+ .jev-devtools .flow-fixture details {
1919
+ max-width: 360px;
1920
+ font-size: 11px;
1921
+ }
1922
+ .jev-devtools .flow-fixture details ul {
1923
+ max-height: 120px;
1924
+ margin: 4px 0 0;
1925
+ padding: 0;
1926
+ overflow: auto;
1927
+ list-style: none;
1928
+ font-family: "JetBrains Mono", monospace;
1929
+ overflow-wrap: anywhere;
1930
+ }
1931
+ .jev-devtools .flow-fixture details ul:focus-visible {
1932
+ outline: 2px solid #e551ba;
1933
+ outline-offset: 2px;
1934
+ }
1935
+ .jev-devtools .flow-fixture textarea {
1936
+ width: 360px;
1937
+ max-width: 100%;
1938
+ height: 120px;
1939
+ padding: 8px;
1940
+ font:
1941
+ 11px "JetBrains Mono",
1942
+ monospace;
1943
+ text-align: left;
1944
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jev-devtools",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22.12"
@@ -48,7 +48,7 @@
48
48
  "url": "git+https://github.com/diegoag64/jev-devtools.git",
49
49
  "directory": "packages/jev-devtools"
50
50
  },
51
- "homepage": "https://github.com/diegoag64/jev-devtools#readme",
51
+ "homepage": "https://jevtools.dev",
52
52
  "bugs": "https://github.com/diegoag64/jev-devtools/issues",
53
53
  "scripts": {
54
54
  "check": "tsc --noEmit",
@@ -7,6 +7,8 @@ description: Install and verify Jev DevTools in an existing React application wi
7
7
 
8
8
  Complete the integration in the user's application: one React drawer, one shared Node adapter, and application calls through that adapter's TypeSafe client. Installing or mounting the drawer alone does not capture requests.
9
9
 
10
+ The human guides live at [docs.jevtools.dev](https://docs.jevtools.dev/), and [jevtools.dev](https://jevtools.dev/) introduces the project. Prefer the installed package’s bundled recipe and declarations when online documentation targets a different version.
11
+
10
12
  ## Choose the integration
11
13
 
12
14
  Inspect the app's package manager/lockfile, React and Node versions, root component, server entry point, existing TypeSafe client construction, and development URL. Preserve existing configuration, authentication, routes, providers, and client options. Follow the host project's instructions; this package's contributor workflow does not apply to consumer apps.
@@ -15,11 +17,11 @@ Inspect the app's package manager/lockfile, React and Node versions, root compon
15
17
  - **React with a Node backend, including Vite:** read [references/react-node.md](references/react-node.md). A browser-only app needs a Node server for Jev; the Vite development-server option is local only.
16
18
  - **Other Node frameworks:** use the Web Request/Response or middleware API described in the React/Node reference and verify that framework's routing. Do not assume the Next.js recipe applies to Pages Router. Edge runtimes and cross-process/serverless history aggregation are unsupported.
17
19
 
18
- Install `jev-devtools` using the app's existing package manager (npm: `npm install jev-devtools @typesafe-ai/sdk`; pnpm: `pnpm add jev-devtools @typesafe-ai/sdk`; Yarn: `yarn add jev-devtools @typesafe-ai/sdk`). `@typesafe-ai/sdk` is a peer dependency: keep the app's existing SDK version when it satisfies the peer range. Install in the workspace(s) that actually import it. It is a regular dependency when application modules import it, even though capture/display are development-only. React 18/19 and Node 22.12+ are required. Do not replace the app's framework or upgrade unrelated dependencies to force compatibility.
20
+ Install `jev-devtools` using the app's existing package manager (npm: `npm install jev-devtools @typesafe-ai/sdk`; pnpm: `pnpm add jev-devtools @typesafe-ai/sdk`; Yarn: `yarn add jev-devtools @typesafe-ai/sdk`; Bun: `bun add jev-devtools @typesafe-ai/sdk`). `@typesafe-ai/sdk` is a peer dependency: keep the app's existing SDK version when it satisfies the peer range. Install in the workspace(s) that actually import it. It is a regular dependency when application modules import it, even though capture/display are development-only. React 18/19 and Node 22.12+ are required. Do not replace the app's framework or upgrade unrelated dependencies to force compatibility.
19
21
 
20
- ## Optional connected flow API (0.4.0)
22
+ ## Optional connected flow API
21
23
 
22
- When asked to build or inspect connected calls, read [references/flows.md](references/flows.md) after the framework recipe. It covers the 0.4.0 API: requests, server function tasks, runtime request factories, parallel checks, terminal branches, returning conditionals, bounded collections, and stateful loops. It includes sample verification, scope/execution rules, and privacy limits. For an older installation, use its bundled recipe and declarations rather than assuming these operations exist. Preserve existing application policies; do not convert unrelated calls just to install the drawer. Flow execution is server-only and continues even with the inspector disabled.
24
+ When asked to build or inspect connected calls, read [references/flows.md](references/flows.md) after the framework recipe. For 0.5.0 it covers requests, server function tasks, runtime request factories, parallel checks, terminal branches, returning conditionals, bounded collections, stateful loops, captured-answer replay, and fixture comparison. It includes sample verification, scope/execution rules, and privacy limits. For an older installation, use its bundled recipe and declarations rather than assuming these operations exist. Preserve existing application policies; do not convert unrelated calls just to install the drawer. Flow execution is server-only and continues even with the inspector disabled.
23
25
 
24
26
  ## Integration invariants
25
27
 
@@ -28,6 +30,7 @@ When asked to build or inspect connected calls, read [references/flows.md](refer
28
30
  - Create a shared adapter; the inspector routes and application client must use it in the same Node realm/process. Replace existing `new TypeSafeClient(options)` construction with `inspector.createClient(options)` while preserving keys, transport options, retries, and call semantics. Keep existing `systemOne(...)` inputs and application behavior.
29
31
  - Browser and server `endpoint` paths must agree (default `/api/jev-devtools`). Route the base path and its subpaths. The server `origin` must exactly match the browser scheme, hostname, and port. A browser endpoint is a same-origin path, not an absolute backend URL.
30
32
  - Keep keys in server environment variables, never `VITE_*` or `NEXT_PUBLIC_*`. Use the host's environment loader and an ignored local env file. If a key is unavailable, complete setup and use the clearly labeled sample verification in [references/verify.md](references/verify.md); report that live inference remains unverified.
33
+ - In 0.5.0, `redactAdditionalSensitiveData` defaults to true and masks supported personal fields in the drawer only; those values still exist in capture/stream/browser memory. Known credentials and custom server redactions cannot be revealed by disabling display masking. Keep application-specific server redaction for data that must not reach the browser.
31
34
  - Preserve authentication and project redaction. Shared/non-loopback access requires a real `authorize(request)` check; do not bypass it with `() => true`. Keep local defaults unless shared access is requested. Without `authorize`, Node middleware accepts only loopback connections; Web Request routes can't check the connection, so their dev server must listen on `127.0.0.1` or set a real `authorize`. Separate processes require a different architecture, not extra adapter instances.
32
35
 
33
36
  ## Finish and verify
@@ -1,6 +1,6 @@
1
1
  # Build and inspect a Jev flow
2
2
 
3
- For `jev-devtools` **0.4.1**. Read the installed package version and `lib/flow/index.d.ts` first; online `main` may describe newer APIs. Use this recipe when the user asks to connect multiple Jev requests, inspect branches, run server function steps, or visualize parallel checks, collections, and loops. Adding the inspector alone does not require converting existing calls to flows.
3
+ For `jev-devtools` **0.5.0**. Check the repository changelog for publication status when installing from source. Read the installed package version and `lib/flow/index.d.ts` first; online `main` may describe newer APIs. Use this recipe when the user asks to connect multiple Jev requests, inspect branches, run server function steps, or visualize parallel checks, collections, and loops. Adding the inspector alone does not require converting existing calls to flows.
4
4
 
5
5
  ## Plan the integration
6
6
 
@@ -9,7 +9,7 @@ For `jev-devtools` **0.4.1**. Read the installed package version and `lib/flow/i
9
9
  3. Import `defineJevFlow` (and `all`, `any`, `not` when needed) from **server-only** `jev-devtools/flow`. Define the graph once at module scope and call `run()` from the existing Node handler/action. In Next.js, mark the module `server-only` and keep the executing route/segment on the Node runtime. No new client provider or root component is needed.
10
10
  4. A named output returns data and does not execute an action. Existing business actions can remain in the application after the returned outcome; use `task()` when the user wants application-owned server work included in the flow. Preserve authorization, idempotency, input validation, and error handling. Cancellation cannot undo side effects.
11
11
 
12
- If importing question helpers as below, install `@typesafe-ai/sdk` as a direct dependency in the server workspace using its package manager. Version 0.4.1 requires SDK `>=0.7.0 <0.8.0` and is verified with 0.7.0; preserve a compatible existing version rather than silently changing SDK semantics. The flow also accepts ordinary SDK question objects.
12
+ If importing question helpers as below, install `@typesafe-ai/sdk` as a direct dependency in the server workspace using its package manager. Version 0.5.0 requires SDK `>=0.7.0 <0.8.0` and is verified with 0.7.0; preserve a compatible existing version rather than silently changing SDK semantics. The flow also accepts ordinary SDK question objects.
13
13
 
14
14
  ## Complete example: parallel checks, one decision, two outcomes
15
15
 
@@ -160,6 +160,10 @@ Check these observable results:
160
160
 
161
161
  If the flow executes but details are absent, check: `devtools` passed to `run`, `client` from that same adapter, same process/session, capture enabled, `/flows/events` forwarded, and histories not cleared/evicted. A plain SDK client can produce flow states but cannot link request details. See [verify.md](verify.md) for general endpoint checks.
162
162
 
163
+ To check routing without a key, copy the run's fixture with **Copy fixture** in the Flows run header. Replay it in a test with `flow.run(input, { client: replayClient(fixture) })` from `jev-devtools/flow`. Steps in map items and loop iterations are keyed `parent-id/index/step-id` (map item or loop iteration); tasks run again on replay. A missing-step error names the composed id of the step the fixture lacks; see the [flow guide](https://github.com/diegoag64/jev-devtools/blob/main/docs/flows.md#replay-a-run-in-tests) for limits.
164
+
165
+ To check a prompt or model change against saved cases, use `compareWithFixture(flow, input, fixture, { client })` from `jev-devtools/flow` and print `formatComparison` for each case where `comparison.changed || comparison.answers.some((a) => a.changed)`. Item steps are reported under their composed ids, and tasks run on both sides.
166
+
163
167
  Remove temporary verification routes unless requested as a demo. Report the installed version, integration location, outcomes observed, whether browser checks were performed, and that responses were samples. Never equate sample verification with live-model correctness.
164
168
 
165
169
  ## Function steps and runtime requests (0.4.0)
@@ -225,3 +229,12 @@ Require `maxIterations` (1–64). If the condition is still true at the limit, `
225
229
  Loops may contain one level of map, plus tasks, requests, conditions and parallel lanes. Nested loops, loops in maps and nested maps remain unsupported. All template nodes count toward the 256-node definition cap; the root execution shares a 1,024-expanded-step budget across iterations and collections. A fresh per-iteration budget would bypass that guarantee. Use a matching server and drawer. Loop cards reuse inline expansion/navigation: select an iteration → View Flow → select a nested frontier item if present. Input/latest state and child outcomes use normal redaction and capture limits.
226
230
 
227
231
  `cookbooks/hierarchical-classification` is an executable sample at `/?cookbook=hierarchical-classification`. It follows the official executable algorithm’s **separate concurrent requests per frontier node**, despite introductory prose describing batching. Preserve geometric-mean decision-edge scores, epsilon 1e-9, single-child inference bypass, completed-leaf retention and top-k pruning. Samples are a small authored taxonomy and synthetic probabilities, not a replay of official benchmark data. Verify recovery, no-request paths, an initial leaf and intentional depth exhaustion before considering live execution.
232
+
233
+ ## Replay captures and compare runs (new in 0.5.0)
234
+
235
+ For human examples, see [the flow reference](https://docs.jevtools.dev/api/flow/) and [API glossary](https://docs.jevtools.dev/api/reference/#fixtures-and-comparison). These operations require 0.5.0 or later; inspect the installed declarations before using them with older packages.
236
+
237
+ 1. Capture a complete run and use **Copy fixture** in the drawer. The browser-safe root export `flowFixture(definition, run, traces)` also builds a `BuiltFlowFixture` with the fixture and capture/missing-response warnings. A fixture stores captured response bodies and step IDs, not the original input or executable recipe.
238
+ 2. On Node, import `replayClient` from `jev-devtools/flow` and call `await program.run(originalInput, { client: replayClient(fixture) })`. Use the same recipe, input, and stable step IDs. Jev steps use stored answers without a key/network call; tasks and runtime request factories still execute. Avoid uncontrolled effects in test tasks.
239
+ 3. For an explicitly requested live comparison, import `compareWithFixture` and `formatComparison` from `jev-devtools/flow`. Call `await compareWithFixture(program, originalInput, fixture, { client: liveClient, devtools: inspector, tolerance: 0.01 })`, then use `formatComparison(result)` for text. Do not run live comparison merely to verify installation; use stored SDK responses for ordinary verification.
240
+ 4. Tasks execute on both sides. `changed` tracks outcomes, values, decisions, and executed Jev steps; answer drift alone does not set it. Inspect `answers` separately, and apply numeric tolerance only to answer fields. The result's `liveFixture` contains unredacted live responses: review it before saving or sharing. Replay is not rollback, and incomplete captures cannot recreate missing inference results.
@@ -78,6 +78,4 @@ Report HTTP/capture checks separately from visual browser checks, and sample ver
78
78
  | Server import/build errors | Keep `/server` outside client modules. In Next externalize the SDK only. Use a supported Node runtime. |
79
79
  | Production 404 | Expected when disabled; app inference remains independent. |
80
80
 
81
- Capture is bounded to 50 calls in memory by default. Built-in redaction has limits; arbitrary secrets in prose are not detected. Diagrams show captured inputs/outputs, not hidden model reasoning. Routing decisions are recorded only for explicitly defined flows; see [flows.md](flows.md) for flow verification.
82
-
83
- The current browser requests `/events?protocol=2`: the first event is a full version 2 `snapshot`, followed by `delta` events containing changed traces and the retained ID order. The unqualified `/events` check above intentionally remains compatible with legacy clients. Reconnecting always supplies a fresh snapshot.
81
+ Capture is bounded to 50 calls in memory by default. Known credential fields are always redacted, including common API-key, client-secret and session-token aliases (case, hyphens and underscores are ignored). HTTP headers/options are not captured. The `redactAdditionalSensitiveData` component prop defaults to `true` and masks supported email, phone, postal address, SSN and card-number fields **only in DevTools views**. Set it to `false` to inspect those fields; changes apply immediately to existing captures, independently in each viewer, without server requests. These personal values remain in server/stream/browser memory even while masked. Custom server redaction always remains active, and neither credentials nor custom-redacted values can be recovered through the prop. See the [exact policy](https://github.com/diegoag64/jev-devtools/blob/main/docs/integration.md#redaction-policy). Arbitrary secrets in prose and unusual field names still need custom redaction. Verify both modes with synthetic sentinels and confirm SDK/runtime values remain unchanged. Diagrams show captured inputs/outputs, not hidden model reasoning. Routing decisions are recorded only for explicitly defined flows; see [flows.md](flows.md) for flow verification.