/**
 * <folk-funds-app> — main rFunds application component.
 *
 * Views:
 *   "landing"  — TBFF info hero + flow list cards
 *   "detail"   — Flow detail with tabs: Table | River | Transactions
 *
 * Attributes:
 *   space    — space slug
 *   flow-id  — if set, go straight to detail view
 *   mode     — "demo" to use hardcoded demo data (no API)
 */

import type { FlowNode, FunnelNodeData, OutcomeNodeData, OutcomePhase, SourceNodeData, PortDefinition, PortKind } from "../lib/types";
import { PORT_DEFS } from "../lib/types";
import { computeSufficiencyState, computeSystemSufficiency, simulateTick } from "../lib/simulation";
import { demoNodes, SPENDING_COLORS, OVERFLOW_COLORS } from "../lib/presets";
import { mapFlowToNodes } from "../lib/map-flow";

interface FlowSummary {
	id: string;
	name: string;
	label?: string;
	status?: string;
	funnelCount?: number;
	outcomeCount?: number;
	totalValue?: number;
}

interface Transaction {
	id: string;
	type: string;
	amount: number;
	from?: string;
	to?: string;
	timestamp: string;
	description?: string;
}

type View = "landing" | "detail";
type Tab = "diagram" | "table" | "river" | "transactions";

// ─── Auth helpers (reads EncryptID session from localStorage) ──

function getSession(): { accessToken: string; claims: { sub: string; username?: string } } | null {
	try {
		const raw = localStorage.getItem("encryptid_session");
		if (!raw) return null;
		const session = JSON.parse(raw);
		if (!session?.accessToken) return null;
		return session;
	} catch { return null; }
}

function isAuthenticated(): boolean { return getSession() !== null; }
function getAccessToken(): string | null { return getSession()?.accessToken ?? null; }
function getUsername(): string | null { return getSession()?.claims?.username ?? null; }

class FolkFundsApp extends HTMLElement {
	private shadow: ShadowRoot;
	private space = "";
	private view: View = "landing";
	private tab: Tab = "diagram";
	private flowId = "";
	private isDemo = false;

	private flows: FlowSummary[] = [];
	private nodes: FlowNode[] = [];
	private flowName = "";
	private transactions: Transaction[] = [];
	private txLoaded = false;

	private loading = false;
	private error = "";

	// Canvas state
	private canvasZoom = 1;
	private canvasPanX = 0;
	private canvasPanY = 0;
	private selectedNodeId: string | null = null;
	private draggingNodeId: string | null = null;
	private dragStartX = 0;
	private dragStartY = 0;
	private dragNodeStartX = 0;
	private dragNodeStartY = 0;
	private isPanning = false;
	private panStartX = 0;
	private panStartY = 0;
	private panStartPanX = 0;
	private panStartPanY = 0;
	private editingNodeId: string | null = null;
	private isSimulating = false;
	private simInterval: ReturnType<typeof setInterval> | null = null;
	private canvasInitialized = false;

	// Wiring state
	private wiringActive = false;
	private wiringSourceNodeId: string | null = null;
	private wiringSourcePortKind: PortKind | null = null;
	private wiringDragging = false;
	private wiringPointerX = 0;
	private wiringPointerY = 0;

	// Bound handlers for cleanup
	private _boundKeyDown: ((e: KeyboardEvent) => void) | null = null;
	private _boundPointerMove: ((e: PointerEvent) => void) | null = null;
	private _boundPointerUp: ((e: PointerEvent) => void) | null = null;

	constructor() {
		super();
		this.shadow = this.attachShadow({ mode: "open" });
	}

	connectedCallback() {
		this.space = this.getAttribute("space") || "demo";
		this.flowId = this.getAttribute("flow-id") || "";
		this.isDemo = this.getAttribute("mode") === "demo" || this.space === "demo";

		if (this.isDemo) {
			this.view = "detail";
			this.flowName = "TBFF Demo Flow";
			this.nodes = demoNodes.map((n) => ({ ...n, data: { ...n.data } }));
			this.render();
		} else if (this.flowId) {
			this.view = "detail";
			this.loadFlow(this.flowId);
		} else {
			this.view = "landing";
			this.loadFlows();
		}
	}

	private getApiBase(): string {
		const path = window.location.pathname;
		// Subdomain: /rfunds/...  or  Direct: /{space}/rfunds/...
		const match = path.match(/^(\/[^/]+)?\/rfunds/);
		return match ? `${match[0]}` : "";
	}

	private async loadFlows() {
		this.loading = true;
		this.render();
		try {
			const base = this.getApiBase();
			const params = this.space ? `?space=${encodeURIComponent(this.space)}` : "";
			const res = await fetch(`${base}/api/flows${params}`);
			if (res.ok) {
				const data = await res.json();
				this.flows = Array.isArray(data) ? data : (data.flows || []);
			}
		} catch {
			// Flow service unavailable — landing page still works with demo link
		}
		this.loading = false;
		this.render();
	}

	private async loadFlow(flowId: string) {
		this.loading = true;
		this.render();
		try {
			const base = this.getApiBase();
			const res = await fetch(`${base}/api/flows/${encodeURIComponent(flowId)}`);
			if (res.ok) {
				const data = await res.json();
				this.nodes = mapFlowToNodes(data);
				this.flowName = data.name || data.label || flowId;
			} else {
				this.error = `Flow not found (${res.status})`;
			}
		} catch {
			this.error = "Failed to load flow";
		}
		this.loading = false;
		this.render();
	}

	private async loadTransactions() {
		if (this.txLoaded || this.isDemo) return;
		this.loading = true;
		this.render();
		try {
			const base = this.getApiBase();
			const res = await fetch(`${base}/api/flows/${encodeURIComponent(this.flowId)}/transactions`);
			if (res.ok) {
				const data = await res.json();
				this.transactions = Array.isArray(data) ? data : (data.transactions || []);
			}
		} catch {
			// Transactions unavailable
		}
		this.txLoaded = true;
		this.loading = false;
		this.render();
	}

	private getCssPath(): string {
		// In rSpace: /modules/rfunds/funds.css  |  Standalone: /modules/rfunds/funds.css
		// The shell always serves from /modules/rfunds/ in both modes
		return "/modules/rfunds/funds.css";
	}

	private render() {
		this.shadow.innerHTML = `
		<style>
			:host { display: block; font-family: system-ui, -apple-system, sans-serif; color: #e2e8f0; }
			*, *::before, *::after { box-sizing: border-box; }
		</style>
		<link rel="stylesheet" href="${this.getCssPath()}">
		${this.error ? `<div class="funds-error">${this.esc(this.error)}</div>` : ""}
		${this.loading && this.view === "landing" ? '<div class="funds-loading">Loading...</div>' : ""}
		${this.renderView()}
		`;
		this.attachListeners();
	}

	private renderView(): string {
		if (this.view === "detail") return this.renderDetail();
		return this.renderLanding();
	}

	// ─── Landing page ──────────────────────────────────────

	private renderLanding(): string {
		const demoUrl = this.getApiBase() ? `${this.getApiBase()}/demo` : "/rfunds/demo";
		const authed = isAuthenticated();
		const username = getUsername();

		return `
		<div class="funds-landing">
			<div class="rapp-nav">
				<span class="rapp-nav__title">Flows</span>
				<div class="rapp-nav__actions">
					<a href="${this.esc(demoUrl)}" class="rapp-nav__btn rapp-nav__btn--secondary">Demo</a>
					${authed
						? `<button class="rapp-nav__btn" data-action="create-flow">+ Create Flow</button>`
						: `<span style="font-size:12px;color:#64748b">Sign in to create flows</span>`
					}
				</div>
			</div>

			<div class="funds-desc" style="color:#94a3b8;font-size:14px;line-height:1.6;max-width:600px;margin-bottom:24px">
				Design transparent resource flows with sufficiency-based cascading.
				Funnels fill to their threshold, then overflow routes surplus to the next layer &mdash;
				ensuring every level has <em style="color:#fbbf24;font-style:normal;font-weight:600">enough</em> before abundance cascades forward.
			</div>

			<div class="funds-features">
				<div class="funds-features__grid">
					<div class="funds-features__card">
						<div class="funds-features__icon">&#x1F4B0;</div>
						<h3>Sources</h3>
						<p>Revenue streams split across funnels by configurable allocation percentages.</p>
					</div>
					<div class="funds-features__card">
						<div class="funds-features__icon">&#x1F3DB;</div>
						<h3>Funnels</h3>
						<p>Budget buckets with min/max thresholds and sufficiency-based overflow cascading.</p>
					</div>
					<div class="funds-features__card">
						<div class="funds-features__icon">&#x1F3AF;</div>
						<h3>Outcomes</h3>
						<p>Funding targets that receive spending allocations. Track progress toward each goal.</p>
					</div>
					<div class="funds-features__card">
						<div class="funds-features__icon">&#x1F30A;</div>
						<h3>River View</h3>
						<p>Animated sankey diagram showing live fund flows through your entire system.</p>
					</div>
					<div class="funds-features__card">
						<div class="funds-features__icon">&#x2728;</div>
						<h3>Enoughness</h3>
						<p>System-wide sufficiency scoring. Golden glow when funnels reach their threshold.</p>
					</div>
				</div>
			</div>

			<div class="funds-flows">
				<div class="funds-flows__header">
					<h2 class="funds-flows__heading">${authed ? `Flows in ${this.esc(this.space)}` : "Your Flows"}</h2>
					${authed ? `<span class="funds-flows__user">Signed in as ${this.esc(username || "")}</span>` : ""}
				</div>
				${this.flows.length > 0 ? `
					<div class="funds-flows__grid">
						${this.flows.map((f) => this.renderFlowCard(f)).join("")}
					</div>
				` : `
					<div class="funds-flows__empty">
						${authed
							? `<p>No flows in this space yet.</p>
							   <p><a href="${this.esc(demoUrl)}">Explore the demo</a> or create your first flow.</p>`
							: `<p>Sign in to see your space&rsquo;s flows, or <a href="${this.esc(demoUrl)}">explore the demo</a>.</p>`
						}
					</div>
				`}
			</div>

			<div class="funds-about">
				<h2 class="funds-about__heading">How TBFF Works</h2>
				<div class="funds-about__steps">
					<div class="funds-about__step">
						<div class="funds-about__step-num">1</div>
						<div>
							<h3>Define Sources</h3>
							<p>Add revenue streams &mdash; grants, donations, sales, or any recurring income &mdash; with allocation splits.</p>
						</div>
					</div>
					<div class="funds-about__step">
						<div class="funds-about__step-num">2</div>
						<div>
							<h3>Configure Funnels</h3>
							<p>Set minimum, sufficient, and maximum thresholds. Overflow rules determine where surplus flows.</p>
						</div>
					</div>
					<div class="funds-about__step">
						<div class="funds-about__step-num">3</div>
						<div>
							<h3>Track Outcomes</h3>
							<p>Funding targets receive allocations as funnels reach sufficiency. Watch the river flow in real time.</p>
						</div>
					</div>
				</div>
			</div>
		</div>`;
	}

	private renderFlowCard(f: FlowSummary): string {
		const detailUrl = this.getApiBase()
			? `${this.getApiBase()}/flow/${encodeURIComponent(f.id)}`
			: `/rfunds/flow/${encodeURIComponent(f.id)}`;
		const value = f.totalValue != null ? `$${Math.floor(f.totalValue).toLocaleString()}` : "";

		return `
		<a href="${this.esc(detailUrl)}" class="funds-flow-card" data-flow="${this.esc(f.id)}">
			<div class="funds-flow-card__name">${this.esc(f.name || f.label || f.id)}</div>
			${value ? `<div class="funds-flow-card__value">${value}</div>` : ""}
			<div class="funds-flow-card__meta">
				${f.funnelCount != null ? `${f.funnelCount} funnels` : ""}
				${f.outcomeCount != null ? ` &middot; ${f.outcomeCount} outcomes` : ""}
				${f.status ? ` &middot; ${f.status}` : ""}
			</div>
		</a>`;
	}

	// ─── Detail view with tabs ─────────────────────────────

	private renderDetail(): string {
		const backUrl = this.getApiBase()
			? `${this.getApiBase()}/`
			: "/rfunds/";

		return `
		<div class="funds-detail">
			<div class="rapp-nav">
				<a href="${this.esc(backUrl)}" class="rapp-nav__back">&larr; Flows</a>
				<span class="rapp-nav__title">${this.esc(this.flowName || "Flow Detail")}</span>
				${this.isDemo ? '<span class="rapp-nav__badge">Demo</span>' : ""}
			</div>

			<div class="funds-tabs">
				<button class="funds-tab ${this.tab === "diagram" ? "funds-tab--active" : ""}" data-tab="diagram">Diagram</button>
				<button class="funds-tab ${this.tab === "river" ? "funds-tab--active" : ""}" data-tab="river">River</button>
				<button class="funds-tab ${this.tab === "table" ? "funds-tab--active" : ""}" data-tab="table">Table</button>
				<button class="funds-tab ${this.tab === "transactions" ? "funds-tab--active" : ""}" data-tab="transactions">Transactions</button>
			</div>

			<div class="funds-tab-content">
				${this.loading ? '<div class="funds-loading">Loading...</div>' : this.renderTab()}
			</div>
		</div>`;
	}

	private renderTab(): string {
		if (this.tab === "diagram") return this.renderDiagramTab();
		if (this.tab === "river") return this.renderRiverTab();
		if (this.tab === "transactions") return this.renderTransactionsTab();
		return this.renderTableTab();
	}

	// ─── Table tab ────────────────────────────────────────

	private renderTableTab(): string {
		const funnels = this.nodes.filter((n) => n.type === "funnel");
		const outcomes = this.nodes.filter((n) => n.type === "outcome");
		const sources = this.nodes.filter((n) => n.type === "source");

		return `
		<div class="funds-table">
			${sources.length > 0 ? `
				<div class="funds-section">
					<h3 class="funds-section__title">Sources</h3>
					<div class="funds-cards">
						${sources.map((n) => this.renderSourceCard(n.data as SourceNodeData, n.id)).join("")}
					</div>
				</div>
			` : ""}

			<div class="funds-section">
				<h3 class="funds-section__title">Funnels</h3>
				<div class="funds-cards">
					${funnels.map((n) => this.renderFunnelCard(n.data as FunnelNodeData, n.id)).join("")}
				</div>
			</div>

			<div class="funds-section">
				<h3 class="funds-section__title">Outcomes</h3>
				<div class="funds-cards">
					${outcomes.map((n) => this.renderOutcomeCard(n.data as OutcomeNodeData, n.id)).join("")}
				</div>
			</div>
		</div>`;
	}

	private renderSourceCard(data: SourceNodeData, id: string): string {
		const allocations = data.targetAllocations || [];
		return `
		<div class="funds-card">
			<div class="funds-card__header">
				<span class="funds-card__icon">&#x1F4B0;</span>
				<span class="funds-card__label">${this.esc(data.label)}</span>
				<span class="funds-card__type">${data.sourceType}</span>
			</div>
			<div class="funds-card__stat">
				<span class="funds-card__stat-value">$${data.flowRate.toLocaleString()}</span>
				<span class="funds-card__stat-label">/month</span>
			</div>
			${allocations.length > 0 ? `
				<div class="funds-card__allocs">
					${allocations.map((a) => `
						<div class="funds-card__alloc">
							<span class="funds-card__alloc-dot" style="background:${a.color}"></span>
							${a.percentage}% &rarr; ${this.esc(this.getNodeLabel(a.targetId))}
						</div>
					`).join("")}
				</div>
			` : ""}
		</div>`;
	}

	private renderFunnelCard(data: FunnelNodeData, id: string): string {
		const sufficiency = computeSufficiencyState(data);
		const threshold = data.sufficientThreshold ?? data.maxThreshold;
		const fillPct = Math.min(100, (data.currentValue / (data.maxCapacity || 1)) * 100);
		const suffPct = Math.min(100, (data.currentValue / (threshold || 1)) * 100);

		const statusClass = sufficiency === "abundant" ? "funds-status--abundant"
			: sufficiency === "sufficient" ? "funds-status--sufficient"
			: data.currentValue < data.minThreshold ? "funds-status--critical"
			: "funds-status--seeking";

		const statusLabel = sufficiency === "abundant" ? "Abundant"
			: sufficiency === "sufficient" ? "Sufficient"
			: data.currentValue < data.minThreshold ? "Critical"
			: "Seeking";

		return `
		<div class="funds-card">
			<div class="funds-card__header">
				<span class="funds-card__icon">&#x1F3DB;</span>
				<span class="funds-card__label">${this.esc(data.label)}</span>
				<span class="funds-card__status ${statusClass}">${statusLabel}</span>
			</div>
			<div class="funds-card__bar-container">
				<div class="funds-card__bar" style="width:${fillPct}%"></div>
				<div class="funds-card__bar-threshold" style="left:${Math.min(100, (threshold / (data.maxCapacity || 1)) * 100)}%"></div>
			</div>
			<div class="funds-card__stats">
				<div>
					<span class="funds-card__stat-value">$${Math.floor(data.currentValue).toLocaleString()}</span>
					<span class="funds-card__stat-label">/ $${Math.floor(threshold).toLocaleString()}</span>
				</div>
				<div>
					<span class="funds-card__stat-value">${Math.round(suffPct)}%</span>
					<span class="funds-card__stat-label">sufficiency</span>
				</div>
			</div>
			<div class="funds-card__thresholds">
				<span>Min: $${Math.floor(data.minThreshold).toLocaleString()}</span>
				<span>Max: $${Math.floor(data.maxThreshold).toLocaleString()}</span>
				<span>Cap: $${Math.floor(data.maxCapacity).toLocaleString()}</span>
			</div>
			${data.overflowAllocations.length > 0 ? `
				<div class="funds-card__allocs">
					<div class="funds-card__alloc-title">Overflow</div>
					${data.overflowAllocations.map((a) => `
						<div class="funds-card__alloc">
							<span class="funds-card__alloc-dot" style="background:${a.color}"></span>
							${a.percentage}% &rarr; ${this.esc(this.getNodeLabel(a.targetId))}
						</div>
					`).join("")}
				</div>
			` : ""}
			${data.spendingAllocations.length > 0 ? `
				<div class="funds-card__allocs">
					<div class="funds-card__alloc-title">Spending</div>
					${data.spendingAllocations.map((a) => `
						<div class="funds-card__alloc">
							<span class="funds-card__alloc-dot" style="background:${a.color}"></span>
							${a.percentage}% &rarr; ${this.esc(this.getNodeLabel(a.targetId))}
						</div>
					`).join("")}
				</div>
			` : ""}
		</div>`;
	}

	private renderOutcomeCard(data: OutcomeNodeData, id: string): string {
		const fillPct = data.fundingTarget > 0
			? Math.min(100, (data.fundingReceived / data.fundingTarget) * 100)
			: 0;
		const statusColor = data.status === "completed" ? "#10b981"
			: data.status === "blocked" ? "#ef4444"
			: data.status === "in-progress" ? "#3b82f6"
			: "#64748b";

		return `
		<div class="funds-card">
			<div class="funds-card__header">
				<span class="funds-card__icon">&#x1F3AF;</span>
				<span class="funds-card__label">${this.esc(data.label)}</span>
				<span class="funds-card__status" style="color:${statusColor}">${data.status}</span>
			</div>
			${data.description ? `<div class="funds-card__desc">${this.esc(data.description)}</div>` : ""}
			<div class="funds-card__bar-container">
				<div class="funds-card__bar funds-card__bar--outcome" style="width:${fillPct}%;background:${statusColor}"></div>
			</div>
			<div class="funds-card__stats">
				<div>
					<span class="funds-card__stat-value">$${Math.floor(data.fundingReceived).toLocaleString()}</span>
					<span class="funds-card__stat-label">/ $${Math.floor(data.fundingTarget).toLocaleString()}</span>
				</div>
				<div>
					<span class="funds-card__stat-value">${Math.round(fillPct)}%</span>
					<span class="funds-card__stat-label">funded</span>
				</div>
			</div>
		</div>`;
	}

	private getNodeLabel(id: string): string {
		const node = this.nodes.find((n) => n.id === id);
		if (!node) return id;
		return (node.data as any).label || id;
	}

	// ─── Diagram tab (interactive canvas) ─────────────────

	private renderDiagramTab(): string {
		if (this.nodes.length === 0) {
			return '<div class="funds-loading">No nodes to display.</div>';
		}

		const score = computeSystemSufficiency(this.nodes);
		const scorePct = Math.round(score * 100);
		const scoreColor = scorePct >= 90 ? "#fbbf24" : scorePct >= 60 ? "#10b981" : scorePct >= 30 ? "#f59e0b" : "#ef4444";

		return `
		<div class="funds-canvas-container" id="canvas-container">
			<div class="funds-canvas-badge" id="canvas-badge">
				<div>
					<div class="funds-canvas-badge__score" id="badge-score" style="color:${scoreColor}">${scorePct}%</div>
					<div class="funds-canvas-badge__label">ENOUGH</div>
				</div>
			</div>
			<div class="funds-canvas-toolbar">
				<button class="funds-canvas-btn funds-canvas-btn--source" data-canvas-action="add-source">+ Source</button>
				<button class="funds-canvas-btn funds-canvas-btn--funnel" data-canvas-action="add-funnel">+ Funnel</button>
				<button class="funds-canvas-btn funds-canvas-btn--outcome" data-canvas-action="add-outcome">+ Outcome</button>
				<div class="funds-canvas-sep"></div>
				<button class="funds-canvas-btn" data-canvas-action="sim" id="sim-btn">${this.isSimulating ? "Pause" : "Play"}</button>
				<button class="funds-canvas-btn" data-canvas-action="fit">Fit</button>
				<button class="funds-canvas-btn" data-canvas-action="share">Share</button>
			</div>
			<svg class="funds-canvas-svg" id="flow-canvas">
				<g id="canvas-transform">
					<g id="edge-layer"></g>
					<g id="wire-layer"></g>
					<g id="node-layer"></g>
				</g>
			</svg>
			<div class="funds-editor-panel" id="editor-panel"></div>
			<div class="funds-canvas-legend">
				<span class="funds-canvas-legend-item"><span class="funds-canvas-legend-dot" style="background:#10b981"></span>Source</span>
				<span class="funds-canvas-legend-item"><span class="funds-canvas-legend-dot" style="background:#0ea5e9"></span>Funnel</span>
				<span class="funds-canvas-legend-item"><span class="funds-canvas-legend-dot" style="background:#f59e0b"></span>Overflow</span>
				<span class="funds-canvas-legend-item"><span class="funds-canvas-legend-dot" style="background:#8b5cf6"></span>Spending</span>
				<span class="funds-canvas-legend-item"><span class="funds-canvas-legend-dot" style="background:#3b82f6"></span>Outcome</span>
				<span class="funds-canvas-legend-item"><span class="funds-canvas-legend-dot" style="background:#fbbf24"></span>Sufficient</span>
			</div>
			<div class="funds-canvas-zoom">
				<button class="funds-canvas-btn" data-canvas-action="zoom-in">+</button>
				<button class="funds-canvas-btn" data-canvas-action="zoom-out">&minus;</button>
			</div>
			<div class="funds-node-tooltip" id="node-tooltip" style="display:none"></div>
		</div>`;
	}

	// ─── Canvas lifecycle ─────────────────────────────────

	private initCanvas() {
		this.drawCanvasContent();
		this.updateCanvasTransform();
		this.attachCanvasListeners();
		if (!this.canvasInitialized) {
			this.canvasInitialized = true;
			requestAnimationFrame(() => this.fitView());
		}
		this.loadFromHash();
	}

	private drawCanvasContent() {
		const edgeLayer = this.shadow.getElementById("edge-layer");
		const nodeLayer = this.shadow.getElementById("node-layer");
		if (!edgeLayer || !nodeLayer) return;
		edgeLayer.innerHTML = this.renderAllEdges();
		nodeLayer.innerHTML = this.renderAllNodes();
	}

	private updateCanvasTransform() {
		const g = this.shadow.getElementById("canvas-transform");
		if (g) g.setAttribute("transform", `translate(${this.canvasPanX},${this.canvasPanY}) scale(${this.canvasZoom})`);
	}

	private fitView() {
		const svg = this.shadow.getElementById("flow-canvas") as SVGSVGElement | null;
		if (!svg || this.nodes.length === 0) return;
		const rect = svg.getBoundingClientRect();
		if (rect.width === 0 || rect.height === 0) return;

		let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
		for (const n of this.nodes) {
			const s = this.getNodeSize(n);
			minX = Math.min(minX, n.position.x);
			minY = Math.min(minY, n.position.y);
			maxX = Math.max(maxX, n.position.x + s.w);
			maxY = Math.max(maxY, n.position.y + s.h);
		}
		const pad = 60;
		const contentW = maxX - minX + pad * 2;
		const contentH = maxY - minY + pad * 2;
		const scaleX = rect.width / contentW;
		const scaleY = rect.height / contentH;
		this.canvasZoom = Math.min(scaleX, scaleY, 1.5);
		this.canvasPanX = (rect.width - contentW * this.canvasZoom) / 2 - (minX - pad) * this.canvasZoom;
		this.canvasPanY = (rect.height - contentH * this.canvasZoom) / 2 - (minY - pad) * this.canvasZoom;
		this.updateCanvasTransform();
	}

	private getNodeSize(n: FlowNode): { w: number; h: number } {
		if (n.type === "source") return { w: 200, h: 60 };
		if (n.type === "funnel") return { w: 220, h: 180 };
		return { w: 200, h: 100 }; // outcome
	}

	// ─── Canvas event wiring ──────────────────────────────

	private attachCanvasListeners() {
		const svg = this.shadow.getElementById("flow-canvas");
		if (!svg) return;

		// Wheel zoom
		svg.addEventListener("wheel", (e: WheelEvent) => {
			e.preventDefault();
			const delta = e.deltaY > 0 ? 0.9 : 1.1;
			const rect = svg.getBoundingClientRect();
			const mx = e.clientX - rect.left;
			const my = e.clientY - rect.top;
			const newZoom = Math.max(0.1, Math.min(4, this.canvasZoom * delta));
			// Zoom toward pointer
			this.canvasPanX = mx - (mx - this.canvasPanX) * (newZoom / this.canvasZoom);
			this.canvasPanY = my - (my - this.canvasPanY) * (newZoom / this.canvasZoom);
			this.canvasZoom = newZoom;
			this.updateCanvasTransform();
		}, { passive: false });

		// Panning — pointerdown on SVG background
		svg.addEventListener("pointerdown", (e: PointerEvent) => {
			const target = e.target as Element;
			// Only pan when clicking SVG background (not on a node)
			if (target.closest(".flow-node")) return;
			if (target.closest(".edge-ctrl-group")) return;

			// Cancel wiring on empty canvas click
			if (this.wiringActive) { this.cancelWiring(); return; }

			this.isPanning = true;
			this.panStartX = e.clientX;
			this.panStartY = e.clientY;
			this.panStartPanX = this.canvasPanX;
			this.panStartPanY = this.canvasPanY;
			svg.classList.add("panning");
			svg.setPointerCapture(e.pointerId);

			// Deselect node
			if (!target.closest(".flow-node")) {
				this.selectedNodeId = null;
				this.updateSelectionHighlight();
			}
		});

		// Global pointer move/up (for both panning and node drag)
		let nodeDragStarted = false;
		const DRAG_THRESHOLD = 5;

		this._boundPointerMove = (e: PointerEvent) => {
			if (this.wiringActive && this.wiringDragging) {
				this.wiringPointerX = e.clientX;
				this.wiringPointerY = e.clientY;
				this.updateWiringTempLine();
				return;
			}
			if (this.isPanning) {
				this.canvasPanX = this.panStartPanX + (e.clientX - this.panStartX);
				this.canvasPanY = this.panStartPanY + (e.clientY - this.panStartY);
				this.updateCanvasTransform();
				return;
			}
			if (this.draggingNodeId) {
				const rawDx = e.clientX - this.dragStartX;
				const rawDy = e.clientY - this.dragStartY;
				// Only start visual drag after exceeding threshold
				if (!nodeDragStarted) {
					if (Math.abs(rawDx) < DRAG_THRESHOLD && Math.abs(rawDy) < DRAG_THRESHOLD) return;
					nodeDragStarted = true;
					svg.classList.add("dragging");
				}
				const dx = rawDx / this.canvasZoom;
				const dy = rawDy / this.canvasZoom;
				const node = this.nodes.find((n) => n.id === this.draggingNodeId);
				if (node) {
					node.position.x = this.dragNodeStartX + dx;
					node.position.y = this.dragNodeStartY + dy;
					this.updateNodePosition(node);
					this.redrawEdges();
				}
			}
		};
		this._boundPointerUp = (e: PointerEvent) => {
			if (this.wiringActive && this.wiringDragging) {
				// Hit-test: did we release on a compatible input port?
				const el = this.shadow.elementFromPoint(e.clientX, e.clientY);
				const portGroup = el?.closest?.(".port-group") as SVGGElement | null;
				if (portGroup && portGroup.dataset.portDir === "in" && portGroup.dataset.nodeId !== this.wiringSourceNodeId) {
					this.completeWiring(portGroup.dataset.nodeId!);
				} else {
					// Fall back to click-to-wire mode (source still glowing)
					this.wiringDragging = false;
					const wireLayer = this.shadow.getElementById("wire-layer");
					if (wireLayer) wireLayer.innerHTML = "";
				}
				return;
			}
			if (this.isPanning) {
				this.isPanning = false;
				svg.classList.remove("panning");
			}
			if (this.draggingNodeId) {
				const clickedNodeId = this.draggingNodeId;
				const wasDragged = nodeDragStarted;
				this.draggingNodeId = null;
				nodeDragStarted = false;
				svg.classList.remove("dragging");

				// If it was a click (no drag), open the editor
				if (!wasDragged) {
					this.openEditor(clickedNodeId);
				}
			}
		};
		svg.addEventListener("pointermove", this._boundPointerMove);
		svg.addEventListener("pointerup", this._boundPointerUp);

		// Node interactions — delegate from node-layer
		const nodeLayer = this.shadow.getElementById("node-layer");
		if (nodeLayer) {
			nodeLayer.addEventListener("pointerdown", (e: PointerEvent) => {
				// Check port interaction FIRST
				const portGroup = (e.target as Element).closest(".port-group") as SVGGElement | null;
				if (portGroup) {
					e.stopPropagation();
					const portNodeId = portGroup.dataset.nodeId!;
					const portKind = portGroup.dataset.portKind as PortKind;
					const portDir = portGroup.dataset.portDir!;

					if (this.wiringActive) {
						// Click-to-wire: complete on compatible input port
						if (portDir === "in" && portNodeId !== this.wiringSourceNodeId) {
							this.completeWiring(portNodeId);
						} else {
							this.cancelWiring();
						}
						return;
					}

					// Start wiring from output port
					if (portDir === "out") {
						this.enterWiring(portNodeId, portKind);
						this.wiringDragging = true;
						this.wiringPointerX = e.clientX;
						this.wiringPointerY = e.clientY;
						svg.setPointerCapture(e.pointerId);
						return;
					}
					return;
				}

				const group = (e.target as Element).closest(".flow-node") as SVGGElement | null;
				if (!group) return;
				e.stopPropagation();
				const nodeId = group.dataset.nodeId;
				if (!nodeId) return;
				const node = this.nodes.find((n) => n.id === nodeId);
				if (!node) return;

				// If wiring is active and clicked on a node (not port), cancel
				if (this.wiringActive) {
					this.cancelWiring();
					return;
				}

				// Select
				this.selectedNodeId = nodeId;
				this.updateSelectionHighlight();

				// Prepare drag (but don't start until threshold exceeded)
				nodeDragStarted = false;
				this.draggingNodeId = nodeId;
				this.dragStartX = e.clientX;
				this.dragStartY = e.clientY;
				this.dragNodeStartX = node.position.x;
				this.dragNodeStartY = node.position.y;
				svg.setPointerCapture(e.pointerId);
			});

			nodeLayer.addEventListener("dblclick", (e: MouseEvent) => {
				const group = (e.target as Element).closest(".flow-node") as SVGGElement | null;
				if (!group) return;
				const nodeId = group.dataset.nodeId;
				if (!nodeId) return;
				const node = this.nodes.find((n) => n.id === nodeId);
				if (!node) return;
				if (node.type === "outcome") this.openOutcomeModal(nodeId);
				else if (node.type === "source") this.openSourceModal(nodeId);
				else this.openEditor(nodeId);
			});

			// Hover: tooltip + edge highlighting
			let hoveredNodeId: string | null = null;
			nodeLayer.addEventListener("mouseover", (e: MouseEvent) => {
				const group = (e.target as Element).closest(".flow-node") as SVGGElement | null;
				if (!group) return;
				const nodeId = group.dataset.nodeId;
				if (nodeId && nodeId !== hoveredNodeId) {
					hoveredNodeId = nodeId;
					this.showNodeTooltip(nodeId, e);
					this.highlightNodeEdges(nodeId);
				}
			});
			nodeLayer.addEventListener("mouseout", (e: MouseEvent) => {
				const related = (e.relatedTarget as Element | null)?.closest?.(".flow-node");
				if (!related) {
					hoveredNodeId = null;
					this.hideNodeTooltip();
					this.unhighlightEdges();
				}
			});
		}

		// Toolbar buttons
		this.shadow.querySelectorAll("[data-canvas-action]").forEach((btn) => {
			btn.addEventListener("click", () => {
				const action = (btn as HTMLElement).dataset.canvasAction;
				if (action === "add-source") this.addNode("source");
				else if (action === "add-funnel") this.addNode("funnel");
				else if (action === "add-outcome") this.addNode("outcome");
				else if (action === "sim") this.toggleSimulation();
				else if (action === "fit") this.fitView();
				else if (action === "share") this.shareState();
				else if (action === "zoom-in") { this.canvasZoom = Math.min(4, this.canvasZoom * 1.2); this.updateCanvasTransform(); }
				else if (action === "zoom-out") { this.canvasZoom = Math.max(0.1, this.canvasZoom * 0.8); this.updateCanvasTransform(); }
			});
		});

		// Edge +/- buttons (delegated)
		const edgeLayer = this.shadow.getElementById("edge-layer");
		if (edgeLayer) {
			edgeLayer.addEventListener("click", (e: Event) => {
				const btn = (e.target as Element).closest("[data-edge-action]") as HTMLElement | null;
				if (!btn) return;
				e.stopPropagation();
				const action = btn.dataset.edgeAction; // "inc" or "dec"
				const fromId = btn.dataset.edgeFrom!;
				const toId = btn.dataset.edgeTo!;
				const allocType = btn.dataset.edgeType as "overflow" | "spending" | "source";
				this.handleAdjustAllocation(fromId, toId, allocType, action === "inc" ? 5 : -5);
			});
		}

		// Keyboard
		this._boundKeyDown = (e: KeyboardEvent) => {
			// Skip if typing in editor input
			const tag = (e.target as Element).tagName;
			if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return;

			if (e.key === "Escape") {
				if (this.wiringActive) { this.cancelWiring(); return; }
				this.closeModal();
				this.closeEditor();
			}
			else if (e.key === " ") { e.preventDefault(); this.toggleSimulation(); }
			else if (e.key === "Delete" || e.key === "Backspace") { if (this.selectedNodeId) this.deleteNode(this.selectedNodeId); }
			else if (e.key === "f" || e.key === "F") this.fitView();
			else if (e.key === "=" || e.key === "+") { this.canvasZoom = Math.min(4, this.canvasZoom * 1.2); this.updateCanvasTransform(); }
			else if (e.key === "-") { this.canvasZoom = Math.max(0.1, this.canvasZoom * 0.8); this.updateCanvasTransform(); }
		};
		document.addEventListener("keydown", this._boundKeyDown);
	}

	// ─── Inflow satisfaction computation ─────────────────

	private computeInflowSatisfaction(): Map<string, { actual: number; needed: number; ratio: number }> {
		const result = new Map<string, { actual: number; needed: number; ratio: number }>();

		for (const n of this.nodes) {
			if (n.type === "funnel") {
				const d = n.data as FunnelNodeData;
				const needed = d.inflowRate || 1;
				let actual = 0;
				// Sum source→funnel allocations
				for (const src of this.nodes) {
					if (src.type === "source") {
						const sd = src.data as SourceNodeData;
						for (const alloc of sd.targetAllocations) {
							if (alloc.targetId === n.id) actual += sd.flowRate * (alloc.percentage / 100);
						}
					}
					// Sum overflow from parent funnels
					if (src.type === "funnel" && src.id !== n.id) {
						const fd = src.data as FunnelNodeData;
						const excess = Math.max(0, fd.currentValue - fd.maxThreshold);
						for (const alloc of fd.overflowAllocations) {
							if (alloc.targetId === n.id) actual += excess * (alloc.percentage / 100);
						}
					}
					// Sum overflow from parent outcomes
					if (src.type === "outcome") {
						const od = src.data as OutcomeNodeData;
						const excess = Math.max(0, od.fundingReceived - od.fundingTarget);
						for (const alloc of (od.overflowAllocations || [])) {
							if (alloc.targetId === n.id) actual += excess * (alloc.percentage / 100);
						}
					}
				}
				result.set(n.id, { actual, needed, ratio: Math.min(actual / needed, 2) });
			}
			if (n.type === "outcome") {
				const d = n.data as OutcomeNodeData;
				const needed = Math.max(d.fundingTarget, 1);
				const actual = d.fundingReceived;
				result.set(n.id, { actual, needed, ratio: Math.min(actual / needed, 2) });
			}
		}
		return result;
	}

	// ─── Node SVG rendering ───────────────────────────────

	private renderAllNodes(): string {
		const satisfaction = this.computeInflowSatisfaction();
		return this.nodes.map((n) => this.renderNodeSvg(n, satisfaction)).join("");
	}

	private renderNodeSvg(n: FlowNode, satisfaction: Map<string, { actual: number; needed: number; ratio: number }>): string {
		const sel = this.selectedNodeId === n.id;
		if (n.type === "source") return this.renderSourceNodeSvg(n, sel);
		if (n.type === "funnel") return this.renderFunnelNodeSvg(n, sel, satisfaction.get(n.id));
		return this.renderOutcomeNodeSvg(n, sel, satisfaction.get(n.id));
	}

	private renderSourceNodeSvg(n: FlowNode, selected: boolean): string {
		const d = n.data as SourceNodeData;
		const x = n.position.x, y = n.position.y, w = 200, h = 60;
		const icons: Record<string, string> = { card: "\u{1F4B3}", safe_wallet: "\u{1F512}", ridentity: "\u{1F464}", unconfigured: "\u{2699}" };
		const icon = icons[d.sourceType] || "\u{1F4B0}";
		return `<g class="flow-node ${selected ? "selected" : ""}" data-node-id="${n.id}" transform="translate(${x},${y})">
			<rect class="node-bg" x="0" y="0" width="${w}" height="${h}" rx="8" fill="#064e3b" stroke="${selected ? "#6366f1" : "#10b981"}" stroke-width="${selected ? 3 : 2}"/>
			<text x="14" y="24" fill="#e2e8f0" font-size="15">${icon}</text>
			<text x="36" y="24" fill="#e2e8f0" font-size="13" font-weight="600">${this.esc(d.label)}</text>
			<text x="${w / 2}" y="46" text-anchor="middle" fill="#6ee7b7" font-size="11">$${d.flowRate.toLocaleString()}/mo</text>
			${this.renderAllocBar(d.targetAllocations, w, h - 6)}
			${this.renderPortsSvg(n)}
		</g>`;
	}

	private renderFunnelNodeSvg(n: FlowNode, selected: boolean, sat?: { actual: number; needed: number; ratio: number }): string {
		const d = n.data as FunnelNodeData;
		const x = n.position.x, y = n.position.y, w = 220, h = 180;
		const sufficiency = computeSufficiencyState(d);
		const isSufficient = sufficiency === "sufficient" || sufficiency === "abundant";
		const threshold = d.sufficientThreshold ?? d.maxThreshold;
		const fillPct = Math.min(1, d.currentValue / (d.maxCapacity || 1));

		const borderColor = d.currentValue > d.maxThreshold ? "#f59e0b"
			: d.currentValue < d.minThreshold ? "#ef4444"
			: isSufficient ? "#fbbf24" : "#0ea5e9";
		const fillColor = borderColor;
		const statusLabel = sufficiency === "abundant" ? "Abundant"
			: sufficiency === "sufficient" ? "Sufficient"
			: d.currentValue < d.minThreshold ? "Critical" : "Seeking";

		// Inflow satisfaction bar
		const satBarY = 28;
		const satBarW = w - 20;
		const satRatio = sat ? Math.min(sat.ratio, 1) : 0;
		const satOverflow = sat ? sat.ratio > 1 : false;
		const satFillW = satBarW * satRatio;
		const satLabel = sat ? `${this.formatDollar(sat.actual)} of ${this.formatDollar(sat.needed)}/mo` : "";
		const satBarBorder = satOverflow ? `stroke="#fbbf24" stroke-width="1"` : "";

		// 3-zone background: drain (red), healthy (blue), overflow (amber)
		const zoneY = 52;
		const zoneH = h - 76;
		const drainPct = d.minThreshold / (d.maxCapacity || 1);
		const healthyPct = (d.maxThreshold - d.minThreshold) / (d.maxCapacity || 1);
		const overflowPct = 1 - drainPct - healthyPct;
		const drainH = zoneH * drainPct;
		const healthyH = zoneH * healthyPct;
		const overflowH = zoneH * overflowPct;

		// Fill level
		const totalFillH = zoneH * fillPct;
		const fillY = zoneY + zoneH - totalFillH;

		const glowClass = isSufficient ? " node-glow" : "";

		return `<g class="flow-node${glowClass} ${selected ? "selected" : ""}" data-node-id="${n.id}" transform="translate(${x},${y})">
			${isSufficient ? `<rect x="-3" y="-3" width="${w + 6}" height="${h + 6}" rx="14" fill="none" stroke="#fbbf24" stroke-width="2" opacity="0.4"/>` : ""}
			<rect class="node-bg" x="0" y="0" width="${w}" height="${h}" rx="10" fill="#1e293b" stroke="${selected ? "#6366f1" : borderColor}" stroke-width="${selected ? 3 : 2}"/>
			<text x="10" y="18" fill="#e2e8f0" font-size="13" font-weight="600">${this.esc(d.label)}</text>
			<text x="${w - 10}" y="18" text-anchor="end" fill="${borderColor}" font-size="10" font-weight="500" class="${isSufficient ? 'sufficiency-glow' : ''}">${statusLabel}</text>
			<rect x="10" y="${satBarY}" width="${satBarW}" height="6" rx="3" fill="#334155" opacity="0.3" class="satisfaction-bar-bg"/>
			<rect x="10" y="${satBarY}" width="${satFillW}" height="6" rx="3" fill="#10b981" class="satisfaction-bar-fill" ${satBarBorder}/>
			<text x="${w / 2}" y="${satBarY + 16}" text-anchor="middle" fill="#64748b" font-size="9">${satLabel}</text>
			<rect x="2" y="${zoneY + overflowH + healthyH}" width="${w - 4}" height="${drainH}" fill="#ef4444" opacity="0.08" rx="0"/>
			<rect x="2" y="${zoneY + overflowH}" width="${w - 4}" height="${healthyH}" fill="#0ea5e9" opacity="0.06" rx="0"/>
			<rect x="2" y="${zoneY}" width="${w - 4}" height="${overflowH}" fill="#f59e0b" opacity="0.06" rx="0"/>
			<rect x="2" y="${fillY}" width="${w - 4}" height="${totalFillH}" fill="${fillColor}" opacity="0.25"/>
			<text x="${w / 2}" y="${h - 24}" text-anchor="middle" fill="#94a3b8" font-size="11">$${Math.floor(d.currentValue).toLocaleString()} / $${Math.floor(threshold).toLocaleString()}</text>
			<rect x="8" y="${h - 10}" width="${w - 16}" height="4" rx="2" fill="#334155"/>
			<rect x="8" y="${h - 10}" width="${(w - 16) * fillPct}" height="4" rx="2" fill="${fillColor}"/>
			${this.renderPortsSvg(n)}
		</g>`;
	}

	private renderOutcomeNodeSvg(n: FlowNode, selected: boolean, sat?: { actual: number; needed: number; ratio: number }): string {
		const d = n.data as OutcomeNodeData;
		const x = n.position.x, y = n.position.y, w = 200, h = 100;
		const fillPct = d.fundingTarget > 0 ? Math.min(1, d.fundingReceived / d.fundingTarget) : 0;
		const statusColor = d.status === "completed" ? "#10b981"
			: d.status === "blocked" ? "#ef4444"
			: d.status === "in-progress" ? "#3b82f6" : "#64748b";

		let phaseBars = "";
		if (d.phases && d.phases.length > 0) {
			const phaseW = (w - 20) / d.phases.length;
			phaseBars = d.phases.map((p, i) => {
				const unlocked = d.fundingReceived >= p.fundingThreshold;
				return `<rect x="${10 + i * phaseW}" y="65" width="${phaseW - 2}" height="6" rx="2" fill="${unlocked ? "#10b981" : "#334155"}" opacity="${unlocked ? 0.8 : 0.5}"/>`;
			}).join("");
			phaseBars += `<text x="${w / 2}" y="83" text-anchor="middle" fill="#64748b" font-size="9">${d.phases.filter((p) => d.fundingReceived >= p.fundingThreshold).length}/${d.phases.length} phases</text>`;
		}

		// Enhanced progress bar (8px height, green funded portion + grey gap)
		const barW = w - 20;
		const barY = 34;
		const barH = 8;
		const dollarLabel = `${this.formatDollar(d.fundingReceived)} / ${this.formatDollar(d.fundingTarget)}`;

		return `<g class="flow-node ${selected ? "selected" : ""}" data-node-id="${n.id}" transform="translate(${x},${y})">
			<rect class="node-bg" x="0" y="0" width="${w}" height="${h}" rx="8" fill="#1e293b" stroke="${selected ? "#6366f1" : statusColor}" stroke-width="${selected ? 3 : 1.5}"/>
			<circle cx="14" cy="18" r="5" fill="${statusColor}" opacity="0.7"/>
			<text x="26" y="22" fill="#e2e8f0" font-size="12" font-weight="600">${this.esc(d.label)}</text>
			<rect x="10" y="${barY}" width="${barW}" height="${barH}" rx="4" fill="#334155" class="satisfaction-bar-bg"/>
			<rect x="10" y="${barY}" width="${barW * fillPct}" height="${barH}" rx="4" fill="${statusColor}" opacity="0.8" class="satisfaction-bar-fill"/>
			<text x="${w / 2}" y="${barY + barH + 12}" text-anchor="middle" fill="#94a3b8" font-size="10">${Math.round(fillPct * 100)}% — ${dollarLabel}</text>
			${phaseBars}
			${this.renderPortsSvg(n)}
		</g>`;
	}

	private renderAllocBar(allocs: { percentage: number; color: string }[], parentW: number, y: number): string {
		if (!allocs || allocs.length === 0) return "";
		let bar = "";
		let cx = 10;
		const barW = parentW - 20;
		for (const a of allocs) {
			const segW = barW * (a.percentage / 100);
			bar += `<rect x="${cx}" y="${y}" width="${segW}" height="3" rx="1" fill="${a.color}" opacity="0.7"/>`;
			cx += segW;
		}
		return bar;
	}

	// ─── Edge rendering ───────────────────────────────────

	private formatDollar(amount: number): string {
		if (amount >= 1_000_000) return `$${(amount / 1_000_000).toFixed(1)}M`;
		if (amount >= 1_000) return `$${(amount / 1_000).toFixed(1)}k`;
		return `$${Math.round(amount)}`;
	}

	private renderAllEdges(): string {
		// First pass: compute actual dollar flow per edge
		interface EdgeInfo {
			fromNode: FlowNode;
			toNode: FlowNode;
			fromPort: PortKind;
			color: string;
			flowAmount: number;
			pct: number;
			dashed: boolean;
			fromId: string;
			toId: string;
			edgeType: string;
		}
		const edges: EdgeInfo[] = [];

		for (const n of this.nodes) {
			if (n.type === "source") {
				const d = n.data as SourceNodeData;
				for (const alloc of d.targetAllocations) {
					const target = this.nodes.find((t) => t.id === alloc.targetId);
					if (!target) continue;
					const flowAmount = d.flowRate * (alloc.percentage / 100);
					edges.push({
						fromNode: n, toNode: target, fromPort: "outflow",
						color: alloc.color || "#10b981", flowAmount,
						pct: alloc.percentage, dashed: false,
						fromId: n.id, toId: alloc.targetId, edgeType: "source",
					});
				}
			}
			if (n.type === "funnel") {
				const d = n.data as FunnelNodeData;
				// Overflow edges — actual excess flow
				for (const alloc of d.overflowAllocations) {
					const target = this.nodes.find((t) => t.id === alloc.targetId);
					if (!target) continue;
					const excess = Math.max(0, d.currentValue - d.maxThreshold);
					const flowAmount = excess * (alloc.percentage / 100);
					edges.push({
						fromNode: n, toNode: target, fromPort: "overflow",
						color: alloc.color || "#f59e0b", flowAmount,
						pct: alloc.percentage, dashed: true,
						fromId: n.id, toId: alloc.targetId, edgeType: "overflow",
					});
				}
				// Spending edges — rate-based drain
				for (const alloc of d.spendingAllocations) {
					const target = this.nodes.find((t) => t.id === alloc.targetId);
					if (!target) continue;
					let rateMultiplier: number;
					if (d.currentValue > d.maxThreshold) rateMultiplier = 0.8;
					else if (d.currentValue >= d.minThreshold) rateMultiplier = 0.5;
					else rateMultiplier = 0.1;
					const drain = d.inflowRate * rateMultiplier;
					const flowAmount = drain * (alloc.percentage / 100);
					edges.push({
						fromNode: n, toNode: target, fromPort: "spending",
						color: alloc.color || "#8b5cf6", flowAmount,
						pct: alloc.percentage, dashed: false,
						fromId: n.id, toId: alloc.targetId, edgeType: "spending",
					});
				}
			}
			// Outcome overflow edges
			if (n.type === "outcome") {
				const d = n.data as OutcomeNodeData;
				const allocs = d.overflowAllocations || [];
				for (const alloc of allocs) {
					const target = this.nodes.find((t) => t.id === alloc.targetId);
					if (!target) continue;
					const excess = Math.max(0, d.fundingReceived - d.fundingTarget);
					const flowAmount = excess * (alloc.percentage / 100);
					edges.push({
						fromNode: n, toNode: target, fromPort: "overflow",
						color: alloc.color || "#f59e0b", flowAmount,
						pct: alloc.percentage, dashed: true,
						fromId: n.id, toId: alloc.targetId, edgeType: "overflow",
					});
				}
			}
		}

		// Find max flow amount for width normalization
		const maxFlowAmount = Math.max(1, ...edges.map((e) => e.flowAmount));

		// Second pass: render edges with normalized widths
		let html = "";
		for (const e of edges) {
			const from = this.getPortPosition(e.fromNode, e.fromPort);
			const to = this.getPortPosition(e.toNode, "inflow");
			const isGhost = e.flowAmount === 0;
			const strokeW = isGhost ? 1 : Math.max(1.5, (e.flowAmount / maxFlowAmount) * 14);
			const label = isGhost ? `${e.pct}%` : `${this.formatDollar(e.flowAmount)} (${e.pct}%)`;
			html += this.renderEdgePath(
				from.x, from.y, to.x, to.y,
				e.color, strokeW, e.dashed, isGhost,
				label, e.fromId, e.toId, e.edgeType,
			);
		}
		return html;
	}

	private renderEdgePath(
		x1: number, y1: number, x2: number, y2: number,
		color: string, strokeW: number, dashed: boolean, ghost: boolean,
		label: string, fromId: string, toId: string, edgeType: string,
	): string {
		const cy1 = y1 + (y2 - y1) * 0.4;
		const cy2 = y1 + (y2 - y1) * 0.6;
		const midX = (x1 + x2) / 2;
		const midY = (y1 + y2) / 2;
		const d = `M ${x1} ${y1} C ${x1} ${cy1}, ${x2} ${cy2}, ${x2} ${y2}`;

		if (ghost) {
			return `<g class="edge-group" data-from="${fromId}" data-to="${toId}">
				<path d="${d}" fill="none" stroke="${color}" stroke-width="1" stroke-opacity="0.2" stroke-dasharray="4 6" class="edge-ghost"/>
				<g class="edge-ctrl-group" transform="translate(${midX},${midY})">
					<rect x="-34" y="-12" width="68" height="24" rx="6" fill="#1e293b" stroke="#475569" stroke-width="1" opacity="0.5"/>
					<text x="-14" y="5" fill="${color}" font-size="11" font-weight="600" text-anchor="middle" opacity="0.5">${label}</text>
					<g data-edge-action="dec" data-edge-from="${fromId}" data-edge-to="${toId}" data-edge-type="${edgeType}" style="cursor:pointer">
						<rect x="-32" y="-9" width="16" height="18" rx="3" fill="#334155"/>
						<text x="-24" y="5" fill="#e2e8f0" font-size="13" text-anchor="middle">&minus;</text>
					</g>
					<g data-edge-action="inc" data-edge-from="${fromId}" data-edge-to="${toId}" data-edge-type="${edgeType}" style="cursor:pointer">
						<rect x="16" y="-9" width="16" height="18" rx="3" fill="#334155"/>
						<text x="24" y="5" fill="#e2e8f0" font-size="13" text-anchor="middle">+</text>
					</g>
				</g>
			</g>`;
		}

		const animClass = dashed ? "edge-path-overflow" : "edge-path-animated";
		// Wider label box to fit dollar amounts
		const labelW = Math.max(68, label.length * 7 + 36);
		const halfW = labelW / 2;
		return `<g class="edge-group" data-from="${fromId}" data-to="${toId}">
			<path d="${d}" fill="none" stroke="${color}" stroke-width="${strokeW * 2.5}" stroke-opacity="0.12" class="edge-glow"/>
			<path d="${d}" fill="none" stroke="${color}" stroke-width="${strokeW}" stroke-opacity="0.8" class="${animClass}"/>
			<g class="edge-ctrl-group" transform="translate(${midX},${midY})">
				<rect x="${-halfW}" y="-12" width="${labelW}" height="24" rx="6" fill="#1e293b" stroke="#475569" stroke-width="1" opacity="0.9"/>
				<text x="0" y="5" fill="${color}" font-size="10" font-weight="600" text-anchor="middle">${label}</text>
				<g data-edge-action="dec" data-edge-from="${fromId}" data-edge-to="${toId}" data-edge-type="${edgeType}" style="cursor:pointer">
					<rect x="${-halfW + 2}" y="-9" width="16" height="18" rx="3" fill="#334155"/>
					<text x="${-halfW + 10}" y="5" fill="#e2e8f0" font-size="13" text-anchor="middle">&minus;</text>
				</g>
				<g data-edge-action="inc" data-edge-from="${fromId}" data-edge-to="${toId}" data-edge-type="${edgeType}" style="cursor:pointer">
					<rect x="${halfW - 18}" y="-9" width="16" height="18" rx="3" fill="#334155"/>
					<text x="${halfW - 10}" y="5" fill="#e2e8f0" font-size="13" text-anchor="middle">+</text>
				</g>
			</g>
		</g>`;
	}

	private redrawEdges() {
		const edgeLayer = this.shadow.getElementById("edge-layer");
		if (edgeLayer) edgeLayer.innerHTML = this.renderAllEdges();
	}

	// ─── Selection highlight ──────────────────────────────

	private updateSelectionHighlight() {
		const nodeLayer = this.shadow.getElementById("node-layer");
		if (!nodeLayer) return;
		nodeLayer.querySelectorAll(".flow-node").forEach((g) => {
			const el = g as SVGGElement;
			const isSelected = el.dataset.nodeId === this.selectedNodeId;
			el.classList.toggle("selected", isSelected);
			const bg = el.querySelector(".node-bg") as SVGRectElement | null;
			if (bg) {
				if (isSelected) {
					bg.setAttribute("stroke", "#6366f1");
					bg.setAttribute("stroke-width", "3");
				} else {
					// Restore original color
					const node = this.nodes.find((n) => n.id === el.dataset.nodeId);
					if (node) {
						const origColor = this.getNodeBorderColor(node);
						bg.setAttribute("stroke", origColor);
						bg.setAttribute("stroke-width", node.type === "outcome" ? "1.5" : "2");
					}
				}
			}
		});
	}

	private getNodeBorderColor(n: FlowNode): string {
		if (n.type === "source") return "#10b981";
		if (n.type === "funnel") {
			const d = n.data as FunnelNodeData;
			const suf = computeSufficiencyState(d);
			const isSuf = suf === "sufficient" || suf === "abundant";
			return d.currentValue > d.maxThreshold ? "#f59e0b"
				: d.currentValue < d.minThreshold ? "#ef4444"
				: isSuf ? "#fbbf24" : "#0ea5e9";
		}
		const d = n.data as OutcomeNodeData;
		return d.status === "completed" ? "#10b981" : d.status === "blocked" ? "#ef4444" : d.status === "in-progress" ? "#3b82f6" : "#64748b";
	}

	// ─── Port rendering & wiring ─────────────────────────

	private getPortDefs(nodeType: FlowNode["type"]): PortDefinition[] {
		return PORT_DEFS[nodeType] || [];
	}

	private getPortPosition(node: FlowNode, portKind: PortKind): { x: number; y: number } {
		const s = this.getNodeSize(node);
		const def = this.getPortDefs(node.type).find((p) => p.kind === portKind);
		if (!def) return { x: node.position.x + s.w / 2, y: node.position.y + s.h / 2 };
		return { x: node.position.x + s.w * def.xFrac, y: node.position.y + s.h * def.yFrac };
	}

	private renderPortsSvg(n: FlowNode): string {
		const s = this.getNodeSize(n);
		const defs = this.getPortDefs(n.type);
		return defs.map((p) => {
			const cx = s.w * p.xFrac;
			const cy = s.h * p.yFrac;
			const arrow = p.dir === "out"
				? `<path d="M ${cx - 3} ${cy + (p.yFrac === 0 ? -8 : 4)} l 3 4 l 3 -4" fill="${p.color}" opacity="0.7"/>`
				: `<path d="M ${cx - 3} ${cy + (p.yFrac === 0 ? -4 : 8)} l 3 -4 l 3 4" fill="${p.color}" opacity="0.7"/>`;
			return `<g class="port-group" data-port-kind="${p.kind}" data-port-dir="${p.dir}" data-node-id="${n.id}">
				<circle class="port-hit" cx="${cx}" cy="${cy}" r="12" fill="transparent"/>
				<circle class="port-dot" cx="${cx}" cy="${cy}" r="5" fill="${p.color}" style="color:${p.color}"/>
				${arrow}
			</g>`;
		}).join("");
	}

	private enterWiring(nodeId: string, portKind: PortKind) {
		this.wiringActive = true;
		this.wiringSourceNodeId = nodeId;
		this.wiringSourcePortKind = portKind;
		this.wiringDragging = false;
		const svg = this.shadow.getElementById("flow-canvas");
		if (svg) svg.classList.add("wiring");
		this.applyWiringClasses();
	}

	private cancelWiring() {
		this.wiringActive = false;
		this.wiringSourceNodeId = null;
		this.wiringSourcePortKind = null;
		this.wiringDragging = false;
		const svg = this.shadow.getElementById("flow-canvas");
		if (svg) svg.classList.remove("wiring");
		const wireLayer = this.shadow.getElementById("wire-layer");
		if (wireLayer) wireLayer.innerHTML = "";
		this.clearWiringClasses();
	}

	private applyWiringClasses() {
		const nodeLayer = this.shadow.getElementById("node-layer");
		if (!nodeLayer || !this.wiringSourceNodeId || !this.wiringSourcePortKind) return;

		const sourceNode = this.nodes.find((n) => n.id === this.wiringSourceNodeId);
		if (!sourceNode) return;

		const sourceDef = this.getPortDefs(sourceNode.type).find((p) => p.kind === this.wiringSourcePortKind);
		const connectsTo = sourceDef?.connectsTo || [];

		nodeLayer.querySelectorAll(".port-group").forEach((g) => {
			const el = g as SVGGElement;
			const nid = el.dataset.nodeId!;
			const pk = el.dataset.portKind as PortKind;
			const pd = el.dataset.portDir!;

			if (nid === this.wiringSourceNodeId && pk === this.wiringSourcePortKind) {
				el.classList.add("port-group--wiring-source");
			} else if (pd === "in" && connectsTo.includes(pk) && nid !== this.wiringSourceNodeId && !this.allocationExists(this.wiringSourceNodeId!, nid, this.wiringSourcePortKind!)) {
				el.classList.add("port-group--wiring-target");
			} else {
				el.classList.add("port-group--wiring-dimmed");
			}
		});
	}

	private clearWiringClasses() {
		const nodeLayer = this.shadow.getElementById("node-layer");
		if (!nodeLayer) return;
		nodeLayer.querySelectorAll(".port-group").forEach((g) => {
			g.classList.remove("port-group--wiring-source", "port-group--wiring-target", "port-group--wiring-dimmed");
		});
	}

	private completeWiring(targetNodeId: string) {
		if (!this.wiringSourceNodeId || !this.wiringSourcePortKind) return;

		const sourceNode = this.nodes.find((n) => n.id === this.wiringSourceNodeId);
		const targetNode = this.nodes.find((n) => n.id === targetNodeId);
		if (!sourceNode || !targetNode) { this.cancelWiring(); return; }

		// Determine allocation type and color
		const portKind = this.wiringSourcePortKind;
		if (sourceNode.type === "source" && portKind === "outflow") {
			const d = sourceNode.data as SourceNodeData;
			const color = SPENDING_COLORS[d.targetAllocations.length % SPENDING_COLORS.length] || "#10b981";
			d.targetAllocations.push({ targetId: targetNodeId, percentage: 0, color });
			this.normalizeAllocations(d.targetAllocations);
		} else if (sourceNode.type === "funnel" && portKind === "overflow") {
			const d = sourceNode.data as FunnelNodeData;
			const color = OVERFLOW_COLORS[d.overflowAllocations.length % OVERFLOW_COLORS.length] || "#f59e0b";
			d.overflowAllocations.push({ targetId: targetNodeId, percentage: 0, color });
			this.normalizeAllocations(d.overflowAllocations);
		} else if (sourceNode.type === "funnel" && portKind === "spending") {
			const d = sourceNode.data as FunnelNodeData;
			const color = SPENDING_COLORS[d.spendingAllocations.length % SPENDING_COLORS.length] || "#8b5cf6";
			d.spendingAllocations.push({ targetId: targetNodeId, percentage: 0, color });
			this.normalizeAllocations(d.spendingAllocations);
		} else if (sourceNode.type === "outcome" && portKind === "overflow") {
			const d = sourceNode.data as OutcomeNodeData;
			if (!d.overflowAllocations) d.overflowAllocations = [];
			const color = OVERFLOW_COLORS[d.overflowAllocations.length % OVERFLOW_COLORS.length] || "#f59e0b";
			d.overflowAllocations.push({ targetId: targetNodeId, percentage: 0, color });
			this.normalizeAllocations(d.overflowAllocations);
		}

		this.cancelWiring();
		this.drawCanvasContent();
		this.openEditor(this.wiringSourceNodeId || sourceNode.id);
	}

	private normalizeAllocations(allocs: { targetId: string; percentage: number; color: string }[]) {
		if (allocs.length === 0) return;
		const equal = Math.floor(100 / allocs.length);
		const remainder = 100 - equal * allocs.length;
		allocs.forEach((a, i) => { a.percentage = equal + (i === 0 ? remainder : 0); });
	}

	private allocationExists(fromId: string, toId: string, portKind: PortKind): boolean {
		const node = this.nodes.find((n) => n.id === fromId);
		if (!node) return false;
		if (node.type === "source" && portKind === "outflow") {
			return (node.data as SourceNodeData).targetAllocations.some((a) => a.targetId === toId);
		}
		if (node.type === "funnel" && portKind === "overflow") {
			return (node.data as FunnelNodeData).overflowAllocations.some((a) => a.targetId === toId);
		}
		if (node.type === "funnel" && portKind === "spending") {
			return (node.data as FunnelNodeData).spendingAllocations.some((a) => a.targetId === toId);
		}
		if (node.type === "outcome" && portKind === "overflow") {
			return ((node.data as OutcomeNodeData).overflowAllocations || []).some((a) => a.targetId === toId);
		}
		return false;
	}

	private updateWiringTempLine() {
		const wireLayer = this.shadow.getElementById("wire-layer");
		if (!wireLayer || !this.wiringSourceNodeId || !this.wiringSourcePortKind) return;

		const sourceNode = this.nodes.find((n) => n.id === this.wiringSourceNodeId);
		if (!sourceNode) return;
		const { x: x1, y: y1 } = this.getPortPosition(sourceNode, this.wiringSourcePortKind);

		const svg = this.shadow.getElementById("flow-canvas") as SVGSVGElement | null;
		if (!svg) return;
		const rect = svg.getBoundingClientRect();
		const x2 = (this.wiringPointerX - rect.left - this.canvasPanX) / this.canvasZoom;
		const y2 = (this.wiringPointerY - rect.top - this.canvasPanY) / this.canvasZoom;

		const cy1 = y1 + (y2 - y1) * 0.4;
		const cy2 = y1 + (y2 - y1) * 0.6;
		wireLayer.innerHTML = `<path class="wiring-temp-path" d="M ${x1} ${y1} C ${x1} ${cy1}, ${x2} ${cy2}, ${x2} ${y2}"/>`;
	}

	// ─── Node position update (direct DOM, no re-render) ──

	private updateNodePosition(n: FlowNode) {
		const nodeLayer = this.shadow.getElementById("node-layer");
		if (!nodeLayer) return;
		const g = nodeLayer.querySelector(`[data-node-id="${n.id}"]`) as SVGGElement | null;
		if (g) g.setAttribute("transform", `translate(${n.position.x},${n.position.y})`);
	}

	// ─── Allocation adjustment ────────────────────────────

	private handleAdjustAllocation(fromId: string, toId: string, allocType: string, delta: number) {
		const node = this.nodes.find((n) => n.id === fromId);
		if (!node) return;

		let allocs: { targetId: string; percentage: number; color: string }[];
		if (allocType === "source") {
			allocs = (node.data as SourceNodeData).targetAllocations;
		} else if (allocType === "overflow") {
			if (node.type === "outcome") {
				allocs = (node.data as OutcomeNodeData).overflowAllocations || [];
			} else {
				allocs = (node.data as FunnelNodeData).overflowAllocations;
			}
		} else {
			allocs = (node.data as FunnelNodeData).spendingAllocations;
		}

		const idx = allocs.findIndex((a) => a.targetId === toId);
		if (idx < 0) return;

		const newPct = Math.max(1, Math.min(99, allocs[idx].percentage + delta));
		const oldPct = allocs[idx].percentage;
		const diff = newPct - oldPct;
		allocs[idx].percentage = newPct;

		// Proportionally rebalance siblings
		const siblings = allocs.filter((_, i) => i !== idx);
		const sibTotal = siblings.reduce((s, a) => s + a.percentage, 0);
		if (sibTotal > 0) {
			for (const sib of siblings) {
				sib.percentage = Math.max(1, Math.round(sib.percentage - diff * (sib.percentage / sibTotal)));
			}
		}

		// Normalize to exactly 100
		const total = allocs.reduce((s, a) => s + a.percentage, 0);
		if (total !== 100 && allocs.length > 1) {
			const last = allocs.find((_, i) => i !== idx) || allocs[allocs.length - 1];
			last.percentage += 100 - total;
			last.percentage = Math.max(1, last.percentage);
		}

		this.redrawEdges();
		this.refreshEditorIfOpen(fromId);
	}

	// ─── Editor panel ─────────────────────────────────────

	private openEditor(nodeId: string) {
		this.editingNodeId = nodeId;
		const panel = this.shadow.getElementById("editor-panel");
		if (!panel) return;
		const node = this.nodes.find((n) => n.id === nodeId);
		if (!node) return;

		let content = `<div class="editor-header">
			<span class="editor-title">${this.esc((node.data as any).label || node.type)}</span>
			<button class="editor-close" data-editor-action="close">&times;</button>
		</div>`;

		if (node.type === "source") content += this.renderSourceEditor(node);
		else if (node.type === "funnel") content += this.renderFunnelEditor(node);
		else content += this.renderOutcomeEditor(node);

		content += `<div class="editor-section">
			<button class="editor-btn editor-btn--danger" data-editor-action="delete">Delete Node</button>
		</div>`;

		panel.innerHTML = content;
		panel.classList.add("open");
		this.attachEditorListeners(panel, node);
	}

	private closeEditor() {
		this.editingNodeId = null;
		const panel = this.shadow.getElementById("editor-panel");
		if (panel) { panel.classList.remove("open"); panel.innerHTML = ""; }
	}

	private refreshEditorIfOpen(nodeId: string) {
		if (this.editingNodeId === nodeId) this.openEditor(nodeId);
	}

	private renderSourceEditor(n: FlowNode): string {
		const d = n.data as SourceNodeData;
		let html = `
		<div class="editor-field"><label class="editor-label">Label</label>
			<input class="editor-input" data-field="label" value="${this.esc(d.label)}"/></div>
		<div class="editor-field"><label class="editor-label">Flow Rate ($/mo)</label>
			<input class="editor-input" data-field="flowRate" type="number" value="${d.flowRate}"/></div>
		<div class="editor-field"><label class="editor-label">Source Type</label>
			<select class="editor-select" data-field="sourceType">
				${["card", "safe_wallet", "ridentity", "unconfigured"].map((t) => `<option value="${t}" ${d.sourceType === t ? "selected" : ""}>${t}</option>`).join("")}
			</select></div>`;
		if (d.sourceType === "card") {
			html += `<div class="editor-field" style="margin-top:12px">
				<button class="editor-btn fund-card-btn" data-action="fund-with-card"
					style="width:100%;padding:10px;background:#6366f1;color:white;border:none;border-radius:8px;cursor:pointer;font-weight:600">
					Fund with Card
				</button>
			</div>`;
		}
		html += this.renderAllocEditor("Target Allocations", d.targetAllocations);
		return html;
	}

	private renderFunnelEditor(n: FlowNode): string {
		const d = n.data as FunnelNodeData;
		return `
		<div class="editor-field"><label class="editor-label">Label</label>
			<input class="editor-input" data-field="label" value="${this.esc(d.label)}"/></div>
		<div class="editor-field"><label class="editor-label">Current Value ($)</label>
			<input class="editor-input" data-field="currentValue" type="number" value="${d.currentValue}"/></div>
		<div class="editor-field"><label class="editor-label">Min Threshold</label>
			<input class="editor-input" data-field="minThreshold" type="number" value="${d.minThreshold}"/></div>
		<div class="editor-field"><label class="editor-label">Max Threshold</label>
			<input class="editor-input" data-field="maxThreshold" type="number" value="${d.maxThreshold}"/></div>
		<div class="editor-field"><label class="editor-label">Max Capacity</label>
			<input class="editor-input" data-field="maxCapacity" type="number" value="${d.maxCapacity}"/></div>
		<div class="editor-field"><label class="editor-label">Inflow Rate ($/tick)</label>
			<input class="editor-input" data-field="inflowRate" type="number" value="${d.inflowRate}"/></div>
		<div class="editor-field"><label class="editor-label">Sufficient Threshold</label>
			<input class="editor-input" data-field="sufficientThreshold" type="number" value="${d.sufficientThreshold ?? d.maxThreshold}"/></div>
		${this.renderAllocEditor("Overflow Allocations", d.overflowAllocations)}
		${this.renderAllocEditor("Spending Allocations", d.spendingAllocations)}`;
	}

	private renderOutcomeEditor(n: FlowNode): string {
		const d = n.data as OutcomeNodeData;
		let html = `
		<div class="editor-field"><label class="editor-label">Label</label>
			<input class="editor-input" data-field="label" value="${this.esc(d.label)}"/></div>
		<div class="editor-field"><label class="editor-label">Description</label>
			<input class="editor-input" data-field="description" value="${this.esc(d.description || "")}"/></div>
		<div class="editor-field"><label class="editor-label">Funding Received ($)</label>
			<input class="editor-input" data-field="fundingReceived" type="number" value="${d.fundingReceived}"/></div>
		<div class="editor-field"><label class="editor-label">Funding Target ($)</label>
			<input class="editor-input" data-field="fundingTarget" type="number" value="${d.fundingTarget}"/></div>
		<div class="editor-field"><label class="editor-label">Status</label>
			<select class="editor-select" data-field="status">
				${["not-started", "in-progress", "completed", "blocked"].map((s) => `<option value="${s}" ${d.status === s ? "selected" : ""}>${s}</option>`).join("")}
			</select></div>`;
		if (d.phases && d.phases.length > 0) {
			html += `<div class="editor-section"><div class="editor-section-title">Phases</div>`;
			for (const p of d.phases) {
				const unlocked = d.fundingReceived >= p.fundingThreshold;
				html += `<div style="margin-bottom:6px;padding:6px;background:#0f172a;border-radius:6px;border-left:3px solid ${unlocked ? "#10b981" : "#334155"}">
					<div style="font-size:12px;font-weight:600;color:${unlocked ? "#6ee7b7" : "#64748b"}">${this.esc(p.name)} — $${p.fundingThreshold.toLocaleString()}</div>
					${p.tasks.map((t) => `<div style="font-size:11px;color:#94a3b8;margin-top:2px">${t.completed ? "&#x2705;" : "&#x2B1C;"} ${this.esc(t.label)}</div>`).join("")}
				</div>`;
			}
			html += `</div>`;
		}
		if (d.overflowAllocations && d.overflowAllocations.length > 0) {
			html += this.renderAllocEditor("Overflow Allocations", d.overflowAllocations);
		}
		return html;
	}

	private renderAllocEditor(title: string, allocs: { targetId: string; percentage: number; color: string }[]): string {
		if (!allocs || allocs.length === 0) return "";
		let html = `<div class="editor-section"><div class="editor-section-title">${title}</div>`;
		for (const a of allocs) {
			html += `<div class="editor-alloc-row">
				<span class="editor-alloc-dot" style="background:${a.color}"></span>
				<span style="flex:1">${this.esc(this.getNodeLabel(a.targetId))}</span>
				<span style="font-weight:600;color:#e2e8f0">${a.percentage}%</span>
			</div>`;
		}
		html += `</div>`;
		return html;
	}

	private async openTransakWidget(flowId: string, walletAddress: string) {
		// Fetch Transak config from server
		let apiKey = "STAGING_KEY";
		let env = "STAGING";
		try {
			const base = this.space ? `/s/${this.space}/rfunds` : "/rfunds";
			const res = await fetch(`${base}/api/transak/config`);
			if (res.ok) {
				const cfg = await res.json();
				apiKey = cfg.apiKey || apiKey;
				env = cfg.environment || env;
			}
		} catch { /* use defaults */ }

		const baseUrl = env === "PRODUCTION"
			? "https://global.transak.com"
			: "https://global-stg.transak.com";

		const params = new URLSearchParams({
			apiKey,
			environment: env,
			cryptoCurrencyCode: "USDC",
			network: "base",
			defaultCryptoCurrency: "USDC",
			walletAddress,
			partnerOrderId: flowId,
			themeColor: "6366f1",
			hideMenu: "true",
		});

		const modal = document.createElement("div");
		modal.id = "transak-modal";
		modal.style.cssText = `position:fixed;inset:0;z-index:99999;background:rgba(0,0,0,0.7);display:flex;align-items:center;justify-content:center;`;
		modal.innerHTML = `
			<div style="position:relative;width:450px;height:680px;border-radius:16px;overflow:hidden;background:#1e1e2e">
				<button id="transak-close" style="position:absolute;top:8px;right:12px;z-index:10;
					background:none;border:none;color:white;font-size:24px;cursor:pointer">&times;</button>
				<iframe src="${baseUrl}?${params}" style="width:100%;height:100%;border:none"
					allow="camera;microphone;payment" sandbox="allow-scripts allow-same-origin allow-popups allow-forms"></iframe>
			</div>`;

		document.body.appendChild(modal);

		modal.querySelector("#transak-close")!.addEventListener("click", () => modal.remove());
		modal.addEventListener("click", (e) => { if (e.target === modal) modal.remove(); });

		const handler = (e: MessageEvent) => {
			if (e.data?.event_id === "TRANSAK_ORDER_SUCCESSFUL") {
				console.log("[Transak] Order successful:", e.data.data);
				modal.remove();
				window.removeEventListener("message", handler);
			}
		};
		window.addEventListener("message", handler);
	}

	private attachEditorListeners(panel: HTMLElement, node: FlowNode) {
		// Close button
		panel.querySelector('[data-editor-action="close"]')?.addEventListener("click", () => this.closeEditor());

		// Delete button
		panel.querySelector('[data-editor-action="delete"]')?.addEventListener("click", () => {
			this.deleteNode(node.id);
			this.closeEditor();
		});

		// Input changes — live update
		const inputs = panel.querySelectorAll(".editor-input, .editor-select");
		inputs.forEach((input) => {
			input.addEventListener("change", () => {
				const field = (input as HTMLElement).dataset.field;
				if (!field) return;
				const val = (input as HTMLInputElement).value;
				const numFields = ["flowRate", "currentValue", "minThreshold", "maxThreshold", "maxCapacity", "inflowRate", "sufficientThreshold", "fundingReceived", "fundingTarget"];
				(node.data as any)[field] = numFields.includes(field) ? parseFloat(val) || 0 : val;
				this.drawCanvasContent();
				this.updateSufficiencyBadge();
			});
		});

		// Fund with Card button (source nodes with sourceType "card")
		panel.querySelector('[data-action="fund-with-card"]')?.addEventListener("click", () => {
			const flowId = this.flowId || this.getAttribute("flow-id") || "";
			const sourceData = node.data as SourceNodeData;
			const walletAddress = sourceData.walletAddress || "";
			if (!walletAddress) {
				alert("Configure a wallet address first (use rIdentity passkey or enter manually)");
				return;
			}
			this.openTransakWidget(flowId, walletAddress);
		});
	}

	// ─── Node hover tooltip ──────────────────────────────

	private showNodeTooltip(nodeId: string, e: MouseEvent) {
		const tooltip = this.shadow.getElementById("node-tooltip");
		const container = this.shadow.getElementById("canvas-container");
		if (!tooltip || !container) return;
		const node = this.nodes.find((n) => n.id === nodeId);
		if (!node) return;

		let html = `<div class="funds-node-tooltip__label">${this.esc((node.data as any).label)}</div>`;
		if (node.type === "source") {
			const d = node.data as SourceNodeData;
			html += `<div class="funds-node-tooltip__stat">$${d.flowRate.toLocaleString()}/mo &middot; ${d.sourceType}</div>`;
		} else if (node.type === "funnel") {
			const d = node.data as FunnelNodeData;
			const suf = computeSufficiencyState(d);
			html += `<div class="funds-node-tooltip__stat">$${Math.floor(d.currentValue).toLocaleString()} / $${Math.floor(d.sufficientThreshold ?? d.maxThreshold).toLocaleString()}</div>`;
			html += `<div class="funds-node-tooltip__stat" style="text-transform:capitalize;color:${suf === "sufficient" || suf === "abundant" ? "#fbbf24" : "#94a3b8"}">${suf}</div>`;
		} else {
			const d = node.data as OutcomeNodeData;
			const pct = d.fundingTarget > 0 ? Math.round((d.fundingReceived / d.fundingTarget) * 100) : 0;
			html += `<div class="funds-node-tooltip__stat">$${Math.floor(d.fundingReceived).toLocaleString()} / $${Math.floor(d.fundingTarget).toLocaleString()} (${pct}%)</div>`;
			html += `<div class="funds-node-tooltip__stat" style="text-transform:capitalize">${d.status}</div>`;
		}

		tooltip.innerHTML = html;
		tooltip.style.display = "block";
		const rect = container.getBoundingClientRect();
		tooltip.style.left = `${e.clientX - rect.left + 16}px`;
		tooltip.style.top = `${e.clientY - rect.top - 8}px`;
	}

	private hideNodeTooltip() {
		const tooltip = this.shadow.getElementById("node-tooltip");
		if (tooltip) tooltip.style.display = "none";
	}

	private highlightNodeEdges(nodeId: string) {
		const edgeLayer = this.shadow.getElementById("edge-layer");
		if (!edgeLayer) return;
		edgeLayer.querySelectorAll(".edge-group").forEach((g) => {
			const el = g as SVGGElement;
			const isConnected = el.dataset.from === nodeId || el.dataset.to === nodeId;
			el.classList.toggle("edge-group--highlight", isConnected);
		});
	}

	private unhighlightEdges() {
		const edgeLayer = this.shadow.getElementById("edge-layer");
		if (!edgeLayer) return;
		edgeLayer.querySelectorAll(".edge-group--highlight").forEach((g) => {
			g.classList.remove("edge-group--highlight");
		});
	}

	// ─── Node detail modals ──────────────────────────────

	private closeModal() {
		const m = this.shadow.getElementById("funds-modal");
		if (m) m.remove();
	}

	private openOutcomeModal(nodeId: string) {
		this.closeModal();
		const node = this.nodes.find((n) => n.id === nodeId);
		if (!node || node.type !== "outcome") return;
		const d = node.data as OutcomeNodeData;

		const fillPct = d.fundingTarget > 0 ? Math.min(100, (d.fundingReceived / d.fundingTarget) * 100) : 0;
		const statusColor = d.status === "completed" ? "#10b981"
			: d.status === "blocked" ? "#ef4444"
			: d.status === "in-progress" ? "#3b82f6" : "#64748b";
		const statusLabel = d.status === "completed" ? "Completed"
			: d.status === "blocked" ? "Blocked"
			: d.status === "in-progress" ? "In Progress" : "Not Started";

		let phasesHtml = "";
		if (d.phases && d.phases.length > 0) {
			phasesHtml += `<div class="phase-tier-bar">`;
			for (const p of d.phases) {
				const unlocked = d.fundingReceived >= p.fundingThreshold;
				phasesHtml += `<div class="phase-tier-segment" style="background:${unlocked ? "#10b981" : "#334155"}"></div>`;
			}
			phasesHtml += `</div>`;

			for (let i = 0; i < d.phases.length; i++) {
				const p = d.phases[i];
				const unlocked = d.fundingReceived >= p.fundingThreshold;
				const completedTasks = p.tasks.filter((t) => t.completed).length;
				const phasePct = p.fundingThreshold > 0 ? Math.min(100, Math.round((d.fundingReceived / p.fundingThreshold) * 100)) : 0;

				phasesHtml += `<div class="phase-card ${unlocked ? "" : "phase-card--locked"}">
					<div class="phase-header" data-phase-idx="${i}">
						<span style="font-size:14px">${unlocked ? "&#x1F513;" : "&#x1F512;"}</span>
						<span style="flex:1;font-size:13px;font-weight:600;color:${unlocked ? "#e2e8f0" : "#64748b"}">${this.esc(p.name)}</span>
						<span style="font-size:11px;color:#64748b">${completedTasks}/${p.tasks.length}</span>
						<span style="font-size:11px;color:#64748b">$${p.fundingThreshold.toLocaleString()}</span>
						<span style="font-size:12px;color:#64748b;transition:transform 0.2s" data-phase-chevron="${i}">&#x25B6;</span>
					</div>
					<div class="phase-content" data-phase-content="${i}" style="display:none">
						<div style="margin-bottom:8px">
							<div style="display:flex;justify-content:space-between;font-size:11px;color:#64748b;margin-bottom:4px">
								<span>${Math.min(phasePct, 100)}% funded</span>
								<span>$${Math.floor(Math.min(d.fundingReceived, p.fundingThreshold)).toLocaleString()} / $${p.fundingThreshold.toLocaleString()}</span>
							</div>
							<div class="funds-modal__progress-bar">
								<div class="funds-modal__progress-fill" style="width:${Math.min(phasePct, 100)}%;background:${unlocked ? "#10b981" : "#3b82f6"}"></div>
							</div>
						</div>
						${p.tasks.map((t, ti) => `
							<div class="phase-task ${t.completed ? "phase-task--done" : ""}">
								<input type="checkbox" data-phase="${i}" data-task="${ti}" ${t.completed ? "checked" : ""} ${!unlocked ? "disabled" : ""}/>
								<span>${this.esc(t.label)}</span>
							</div>
						`).join("")}
						${unlocked ? `<button class="phase-add-btn" data-add-task="${i}">+ Add Task</button>` : ""}
					</div>
				</div>`;
			}

			phasesHtml += `<button class="phase-add-btn" data-action="add-phase" style="width:100%;justify-content:center;padding:8px">+ Add Phase</button>`;
		}

		const backdrop = document.createElement("div");
		backdrop.className = "funds-modal-backdrop";
		backdrop.id = "funds-modal";
		backdrop.innerHTML = `<div class="funds-modal">
			<div class="funds-modal__header">
				<div style="display:flex;align-items:center;gap:10px">
					<span style="display:inline-block;padding:3px 10px;border-radius:12px;font-size:11px;font-weight:600;background:${statusColor}22;color:${statusColor}">${statusLabel}</span>
					<span style="font-size:16px;font-weight:700;color:#e2e8f0">${this.esc(d.label)}</span>
				</div>
				<button class="funds-modal__close" data-modal-action="close">&times;</button>
			</div>
			${d.description ? `<div style="font-size:13px;color:#94a3b8;line-height:1.6;margin-bottom:16px;padding:10px 12px;background:#0f172a;border-radius:8px;border-left:3px solid ${statusColor}">${this.esc(d.description)}</div>` : ""}
			<div style="margin-bottom:20px">
				<div style="font-size:28px;font-weight:700;color:#e2e8f0">$${Math.floor(d.fundingReceived).toLocaleString()}</div>
				<div style="font-size:13px;color:#64748b;margin-top:2px">of $${Math.floor(d.fundingTarget).toLocaleString()} (${Math.round(fillPct)}%)</div>
				<div class="funds-modal__progress-bar" style="margin-top:10px;height:10px">
					<div class="funds-modal__progress-fill" style="width:${fillPct}%;background:${statusColor}"></div>
				</div>
			</div>
			${d.phases && d.phases.length > 0 ? `<div style="margin-bottom:16px">
				<div style="font-size:13px;font-weight:600;color:#94a3b8;text-transform:uppercase;letter-spacing:0.05em;margin-bottom:10px">Phases</div>
				${phasesHtml}
			</div>` : ""}
		</div>`;

		this.shadow.appendChild(backdrop);
		this.attachOutcomeModalListeners(backdrop, nodeId);
	}

	private attachOutcomeModalListeners(backdrop: HTMLElement, nodeId: string) {
		const node = this.nodes.find((n) => n.id === nodeId);
		if (!node) return;
		const d = node.data as OutcomeNodeData;

		backdrop.addEventListener("click", (e) => {
			if (e.target === backdrop) this.closeModal();
		});
		backdrop.querySelector('[data-modal-action="close"]')?.addEventListener("click", () => this.closeModal());

		// Phase accordion toggle
		backdrop.querySelectorAll(".phase-header").forEach((header) => {
			header.addEventListener("click", () => {
				const idx = (header as HTMLElement).dataset.phaseIdx;
				const content = backdrop.querySelector(`[data-phase-content="${idx}"]`) as HTMLElement | null;
				const chevron = backdrop.querySelector(`[data-phase-chevron="${idx}"]`) as HTMLElement | null;
				if (content) {
					const isOpen = content.style.display !== "none";
					content.style.display = isOpen ? "none" : "block";
					if (chevron) chevron.style.transform = isOpen ? "rotate(0deg)" : "rotate(90deg)";
				}
			});
		});

		// Task checkbox toggle
		backdrop.querySelectorAll('input[type="checkbox"][data-phase]').forEach((cb) => {
			cb.addEventListener("change", () => {
				const phaseIdx = parseInt((cb as HTMLElement).dataset.phase!, 10);
				const taskIdx = parseInt((cb as HTMLElement).dataset.task!, 10);
				if (d.phases && d.phases[phaseIdx] && d.phases[phaseIdx].tasks[taskIdx]) {
					d.phases[phaseIdx].tasks[taskIdx].completed = (cb as HTMLInputElement).checked;
					const taskRow = (cb as HTMLElement).closest(".phase-task");
					if (taskRow) taskRow.classList.toggle("phase-task--done", (cb as HTMLInputElement).checked);
					this.drawCanvasContent();
				}
			});
		});

		// Add task
		backdrop.querySelectorAll("[data-add-task]").forEach((btn) => {
			btn.addEventListener("click", () => {
				const phaseIdx = parseInt((btn as HTMLElement).dataset.addTask!, 10);
				if (d.phases && d.phases[phaseIdx]) {
					const taskLabel = prompt("Task name:");
					if (taskLabel) {
						d.phases[phaseIdx].tasks.push({ label: taskLabel, completed: false });
						this.openOutcomeModal(nodeId);
						this.drawCanvasContent();
					}
				}
			});
		});

		// Add phase
		backdrop.querySelector('[data-action="add-phase"]')?.addEventListener("click", () => {
			const name = prompt("Phase name:");
			if (name) {
				const threshold = parseFloat(prompt("Funding threshold ($):") || "0") || 0;
				if (!d.phases) d.phases = [];
				d.phases.push({ name, fundingThreshold: threshold, tasks: [] });
				this.openOutcomeModal(nodeId);
				this.drawCanvasContent();
			}
		});
	}

	private openSourceModal(nodeId: string) {
		this.closeModal();
		const node = this.nodes.find((n) => n.id === nodeId);
		if (!node || node.type !== "source") return;
		const d = node.data as SourceNodeData;

		const icons: Record<string, string> = { card: "\u{1F4B3}", safe_wallet: "\u{1F512}", ridentity: "\u{1F464}", unconfigured: "\u{2699}" };
		const labels: Record<string, string> = { card: "Card", safe_wallet: "Safe / Wallet", ridentity: "rIdentity", unconfigured: "Configure" };

		let configHtml = "";
		if (d.sourceType === "card") {
			configHtml = `<div style="margin-top:12px">
				<div class="editor-field" style="margin-bottom:10px">
					<label class="editor-label">Default Amount ($)</label>
					<input class="editor-input" data-modal-field="flowRate" type="number" value="${d.flowRate}"/>
				</div>
				<button class="editor-btn" data-action="fund-with-card" style="width:100%;padding:10px;background:#6366f1;color:white;border:none;border-radius:8px;font-weight:600;cursor:pointer">
					&#x1F4B3; Fund with Card
				</button>
			</div>`;
		} else if (d.sourceType === "safe_wallet") {
			configHtml = `<div style="margin-top:12px">
				<div class="editor-field" style="margin-bottom:10px">
					<label class="editor-label">Wallet Address</label>
					<input class="editor-input" data-modal-field="walletAddress" value="${this.esc(d.walletAddress || "")}"/>
				</div>
				<div class="editor-field" style="margin-bottom:10px">
					<label class="editor-label">Safe Address</label>
					<input class="editor-input" data-modal-field="safeAddress" value="${this.esc(d.safeAddress || "")}"/>
				</div>
				<div class="editor-field">
					<label class="editor-label">Chain</label>
					<select class="editor-select" data-modal-field="chainId">
						${[{ id: 1, name: "Ethereum" }, { id: 10, name: "Optimism" }, { id: 8453, name: "Base" }, { id: 100, name: "Gnosis" }]
							.map((c) => `<option value="${c.id}" ${d.chainId === c.id ? "selected" : ""}>${c.name}</option>`).join("")}
					</select>
				</div>
			</div>`;
		} else if (d.sourceType === "ridentity") {
			configHtml = `<div style="text-align:center;padding:16px 0;margin-top:12px">
				<div style="font-size:32px;margin-bottom:8px">&#x1F464;</div>
				<div style="font-size:13px;color:#94a3b8;margin-bottom:12px">${isAuthenticated() ? "Connected" : "Not connected"}</div>
				${!isAuthenticated() ? `<button class="editor-btn" data-action="connect-ridentity" style="background:#6366f1;color:white;border:none;padding:8px 20px;border-radius:8px;font-weight:600;cursor:pointer">Connect with EncryptID</button>` : `<div style="font-size:12px;color:#6ee7b7">&#x2705; ${this.esc(getUsername() || "Connected")}</div>`}
			</div>`;
		}

		const backdrop = document.createElement("div");
		backdrop.className = "funds-modal-backdrop";
		backdrop.id = "funds-modal";
		backdrop.innerHTML = `<div class="funds-modal">
			<div class="funds-modal__header">
				<div style="display:flex;align-items:center;gap:10px">
					<span style="font-size:20px">${icons[d.sourceType] || "&#x1F4B0;"}</span>
					<span style="font-size:16px;font-weight:700;color:#e2e8f0">${this.esc(d.label)}</span>
				</div>
				<button class="funds-modal__close" data-modal-action="close">&times;</button>
			</div>
			<div style="margin-bottom:16px">
				<div style="font-size:11px;font-weight:600;color:#94a3b8;text-transform:uppercase;letter-spacing:0.05em;margin-bottom:8px">Source Type</div>
				<div class="source-type-grid">
					${["card", "safe_wallet", "ridentity"].map((t) => `
						<button class="source-type-btn ${d.sourceType === t ? "source-type-btn--active" : ""}" data-source-type="${t}">
							<span style="font-size:20px">${icons[t]}</span>
							<span>${labels[t]}</span>
						</button>
					`).join("")}
				</div>
			</div>
			<div class="editor-field" style="margin-bottom:12px">
				<label class="editor-label">Label</label>
				<input class="editor-input" data-modal-field="label" value="${this.esc(d.label)}"/>
			</div>
			<div class="editor-field" style="margin-bottom:12px">
				<label class="editor-label">Flow Rate ($/mo)</label>
				<input class="editor-input" data-modal-field="flowRate" type="number" value="${d.flowRate}"/>
			</div>
			<div id="source-config">${configHtml}</div>
			<div style="display:flex;gap:8px;margin-top:20px;padding-top:16px;border-top:1px solid #334155">
				<button class="editor-btn" data-modal-action="save" style="flex:1;background:#10b981;color:white;border:none;font-weight:600">Save</button>
				<button class="editor-btn" data-modal-action="close" style="flex:1">Cancel</button>
			</div>
		</div>`;

		this.shadow.appendChild(backdrop);
		this.attachSourceModalListeners(backdrop, nodeId);
	}

	private attachSourceModalListeners(backdrop: HTMLElement, nodeId: string) {
		const node = this.nodes.find((n) => n.id === nodeId);
		if (!node) return;
		const d = node.data as SourceNodeData;

		backdrop.addEventListener("click", (e) => {
			if (e.target === backdrop) this.closeModal();
		});
		backdrop.querySelectorAll('[data-modal-action="close"]').forEach((btn) => {
			btn.addEventListener("click", () => this.closeModal());
		});

		// Source type picker
		backdrop.querySelectorAll("[data-source-type]").forEach((btn) => {
			btn.addEventListener("click", () => {
				d.sourceType = (btn as HTMLElement).dataset.sourceType as SourceNodeData["sourceType"];
				this.openSourceModal(nodeId);
				this.drawCanvasContent();
			});
		});

		// Field changes (live)
		backdrop.querySelectorAll(".editor-input[data-modal-field], .editor-select[data-modal-field]").forEach((input) => {
			input.addEventListener("change", () => {
				const field = (input as HTMLElement).dataset.modalField!;
				const val = (input as HTMLInputElement).value;
				const numFields = ["flowRate", "chainId"];
				(d as any)[field] = numFields.includes(field) ? parseFloat(val) || 0 : val;
				this.drawCanvasContent();
			});
		});

		// Save
		backdrop.querySelector('[data-modal-action="save"]')?.addEventListener("click", () => {
			backdrop.querySelectorAll(".editor-input[data-modal-field], .editor-select[data-modal-field]").forEach((input) => {
				const field = (input as HTMLElement).dataset.modalField!;
				const val = (input as HTMLInputElement).value;
				const numFields = ["flowRate", "chainId"];
				(d as any)[field] = numFields.includes(field) ? parseFloat(val) || 0 : val;
			});
			this.drawCanvasContent();
			this.closeModal();
		});

		// Fund with card
		backdrop.querySelector('[data-action="fund-with-card"]')?.addEventListener("click", () => {
			const flowId = this.flowId || this.getAttribute("flow-id") || "";
			if (!d.walletAddress) {
				alert("Configure a wallet address first (use rIdentity passkey or enter manually)");
				return;
			}
			this.openTransakWidget(flowId, d.walletAddress);
		});

		// Connect with EncryptID
		backdrop.querySelector('[data-action="connect-ridentity"]')?.addEventListener("click", () => {
			window.location.href = "/auth/login?redirect=" + encodeURIComponent(window.location.pathname);
		});
	}

	// ─── Node CRUD ────────────────────────────────────────

	private addNode(type: "source" | "funnel" | "outcome") {
		// Place at center of current viewport
		const svg = this.shadow.getElementById("flow-canvas") as SVGSVGElement | null;
		const rect = svg?.getBoundingClientRect();
		const cx = rect ? (rect.width / 2 - this.canvasPanX) / this.canvasZoom : 400;
		const cy = rect ? (rect.height / 2 - this.canvasPanY) / this.canvasZoom : 300;

		const id = `${type}-${Date.now().toString(36)}`;
		let data: any;
		if (type === "source") {
			data = { label: "New Source", flowRate: 1000, sourceType: "card", targetAllocations: [] } as SourceNodeData;
		} else if (type === "funnel") {
			data = {
				label: "New Funnel", currentValue: 0, minThreshold: 5000, maxThreshold: 20000,
				maxCapacity: 30000, inflowRate: 500, sufficientThreshold: 15000, dynamicOverflow: false,
				overflowAllocations: [], spendingAllocations: [],
			} as FunnelNodeData;
		} else {
			data = { label: "New Outcome", description: "", fundingReceived: 0, fundingTarget: 10000, status: "not-started", overflowAllocations: [] } as OutcomeNodeData;
		}

		this.nodes.push({ id, type, position: { x: cx - 100, y: cy - 50 }, data });
		this.drawCanvasContent();
		this.selectedNodeId = id;
		this.updateSelectionHighlight();
		this.openEditor(id);
	}

	private deleteNode(nodeId: string) {
		this.nodes = this.nodes.filter((n) => n.id !== nodeId);
		// Clean up allocations pointing to deleted node
		for (const n of this.nodes) {
			if (n.type === "source") {
				const d = n.data as SourceNodeData;
				d.targetAllocations = d.targetAllocations.filter((a) => a.targetId !== nodeId);
			}
			if (n.type === "funnel") {
				const d = n.data as FunnelNodeData;
				d.overflowAllocations = d.overflowAllocations.filter((a) => a.targetId !== nodeId);
				d.spendingAllocations = d.spendingAllocations.filter((a) => a.targetId !== nodeId);
			}
			if (n.type === "outcome") {
				const d = n.data as OutcomeNodeData;
				if (d.overflowAllocations) d.overflowAllocations = d.overflowAllocations.filter((a) => a.targetId !== nodeId);
			}
		}
		if (this.selectedNodeId === nodeId) this.selectedNodeId = null;
		this.drawCanvasContent();
		this.updateSufficiencyBadge();
	}

	// ─── Simulation ───────────────────────────────────────

	private toggleSimulation() {
		this.isSimulating = !this.isSimulating;
		const btn = this.shadow.getElementById("sim-btn");
		if (btn) btn.textContent = this.isSimulating ? "Pause" : "Play";

		if (this.isSimulating) {
			this.simInterval = setInterval(() => {
				this.nodes = simulateTick(this.nodes);
				this.updateCanvasLive();
			}, 100);
		} else {
			if (this.simInterval) { clearInterval(this.simInterval); this.simInterval = null; }
		}
	}

	/** Update canvas nodes and edges without full innerHTML rebuild during simulation */
	private updateCanvasLive() {
		const nodeLayer = this.shadow.getElementById("node-layer");
		if (!nodeLayer) return;

		// Rebuild node SVG content (can't do partial DOM updates easily for SVG text)
		nodeLayer.innerHTML = this.renderAllNodes();
		this.redrawEdges();
		this.updateSufficiencyBadge();
	}

	private updateSufficiencyBadge() {
		const score = computeSystemSufficiency(this.nodes);
		const scorePct = Math.round(score * 100);
		const scoreColor = scorePct >= 90 ? "#fbbf24" : scorePct >= 60 ? "#10b981" : scorePct >= 30 ? "#f59e0b" : "#ef4444";
		const badge = this.shadow.getElementById("badge-score");
		if (badge) {
			badge.textContent = `${scorePct}%`;
			badge.style.color = scoreColor;
		}
	}

	// ─── URL sharing ──────────────────────────────────────

	private shareState() {
		try {
			const LZString = (window as any).LZString;
			if (!LZString) {
				// Fallback: copy JSON directly
				const json = JSON.stringify(this.nodes);
				navigator.clipboard.writeText(window.location.href.split("#")[0] + "#flow=" + btoa(json));
				return;
			}
			const json = JSON.stringify(this.nodes);
			const compressed = LZString.compressToEncodedURIComponent(json);
			const url = window.location.href.split("#")[0] + "#flow=" + compressed;
			history.replaceState(null, "", url);
			navigator.clipboard.writeText(url);
		} catch {
			// Silent fail
		}
	}

	private loadFromHash() {
		try {
			const hash = window.location.hash;
			if (!hash.startsWith("#flow=")) return;
			const payload = hash.slice(6);
			let json: string;
			const LZString = (window as any).LZString;
			if (LZString) {
				json = LZString.decompressFromEncodedURIComponent(payload) || "";
			} else {
				json = atob(payload);
			}
			if (!json) return;
			const nodes = JSON.parse(json) as FlowNode[];
			if (Array.isArray(nodes) && nodes.length > 0) {
				this.nodes = nodes;
				this.drawCanvasContent();
				this.fitView();
			}
		} catch {
			// Invalid hash data — ignore
		}
	}

	// ─── River tab ────────────────────────────────────────

	private renderRiverTab(): string {
		return `<div class="funds-river-container" id="river-mount"></div>`;
	}

	private mountRiver() {
		const mount = this.shadow.getElementById("river-mount");
		if (!mount) return;

		// Check if already mounted
		if (mount.querySelector("folk-budget-river")) return;

		const river = document.createElement("folk-budget-river") as any;
		river.setAttribute("simulate", "true");
		mount.appendChild(river);

		// Pass nodes after the element is connected
		requestAnimationFrame(() => {
			if (typeof river.setNodes === "function") {
				river.setNodes(this.nodes.map((n) => ({ ...n, data: { ...n.data } })));
			}
		});
	}

	// ─── Transactions tab ─────────────────────────────────

	private renderTransactionsTab(): string {
		if (this.isDemo) {
			return `
			<div class="funds-tx-empty">
				<p>Transaction history is not available in demo mode.</p>
			</div>`;
		}

		if (!this.txLoaded) {
			return '<div class="funds-loading">Loading transactions...</div>';
		}

		if (this.transactions.length === 0) {
			return `
			<div class="funds-tx-empty">
				<p>No transactions yet for this flow.</p>
			</div>`;
		}

		return `
		<div class="funds-tx-list">
			${this.transactions.map((tx) => `
				<div class="funds-tx">
					<div class="funds-tx__icon">${tx.type === "deposit" ? "&#x2B06;" : tx.type === "withdraw" ? "&#x2B07;" : "&#x1F504;"}</div>
					<div class="funds-tx__body">
						<div class="funds-tx__desc">${this.esc(tx.description || tx.type)}</div>
						<div class="funds-tx__meta">
							${tx.from ? `From: ${this.esc(tx.from)}` : ""}
							${tx.to ? ` &rarr; ${this.esc(tx.to)}` : ""}
						</div>
					</div>
					<div class="funds-tx__amount ${tx.type === "deposit" ? "funds-tx__amount--positive" : "funds-tx__amount--negative"}">
						${tx.type === "deposit" ? "+" : "-"}$${Math.abs(tx.amount).toLocaleString()}
					</div>
					<div class="funds-tx__time">${this.formatTime(tx.timestamp)}</div>
				</div>
			`).join("")}
		</div>`;
	}

	private formatTime(ts: string): string {
		try {
			const d = new Date(ts);
			const now = new Date();
			const diffMs = now.getTime() - d.getTime();
			const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
			if (diffDays === 0) return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
			if (diffDays === 1) return "Yesterday";
			if (diffDays < 7) return `${diffDays}d ago`;
			return d.toLocaleDateString();
		} catch {
			return ts;
		}
	}

	// ─── Event listeners ──────────────────────────────────

	private attachListeners() {
		// Tab switching
		this.shadow.querySelectorAll("[data-tab]").forEach((el) => {
			el.addEventListener("click", () => {
				const newTab = (el as HTMLElement).dataset.tab as Tab;
				if (newTab === this.tab) return;
				// Cleanup old canvas state
				if (this.tab === "diagram") this.cleanupCanvas();
				this.tab = newTab;
				this.render();

				if (newTab === "transactions" && !this.txLoaded) {
					this.loadTransactions();
				}
			});
		});

		// Mount river component when river tab is active
		if (this.tab === "river" && this.nodes.length > 0) {
			this.mountRiver();
		}

		// Initialize interactive canvas when diagram tab is active
		if (this.tab === "diagram" && this.nodes.length > 0) {
			this.initCanvas();
		}

		// Create flow button (landing page, auth-gated)
		const createBtn = this.shadow.querySelector('[data-action="create-flow"]');
		createBtn?.addEventListener("click", () => this.handleCreateFlow());
	}

	private cleanupCanvas() {
		if (this.simInterval) { clearInterval(this.simInterval); this.simInterval = null; }
		this.isSimulating = false;
		if (this.wiringActive) this.cancelWiring();
		if (this._boundKeyDown) { document.removeEventListener("keydown", this._boundKeyDown); this._boundKeyDown = null; }
	}

	private async handleCreateFlow() {
		const token = getAccessToken();
		if (!token) return;

		const name = prompt("Flow name:");
		if (!name) return;

		this.loading = true;
		this.render();
		try {
			const base = this.getApiBase();
			const res = await fetch(`${base}/api/flows`, {
				method: "POST",
				headers: {
					"Content-Type": "application/json",
					Authorization: `Bearer ${token}`,
				},
				body: JSON.stringify({ name }),
			});
			if (res.ok) {
				const data = await res.json();
				const flowId = data.id || data.flowId;
				// Associate with current space
				if (flowId && this.space) {
					await fetch(`${base}/api/space-flows`, {
						method: "POST",
						headers: {
							"Content-Type": "application/json",
							Authorization: `Bearer ${token}`,
						},
						body: JSON.stringify({ space: this.space, flowId }),
					});
				}
				// Navigate to the new flow
				if (flowId) {
					const detailUrl = this.getApiBase()
						? `${this.getApiBase()}/flow/${encodeURIComponent(flowId)}`
						: `/rfunds/flow/${encodeURIComponent(flowId)}`;
					window.location.href = detailUrl;
					return;
				}
			} else {
				const err = await res.json().catch(() => ({}));
				this.error = (err as any).error || `Failed to create flow (${res.status})`;
			}
		} catch {
			this.error = "Failed to create flow";
		}
		this.loading = false;
		this.render();
	}

	private esc(s: string): string {
		return s
			.replace(/&/g, "&amp;")
			.replace(/</g, "&lt;")
			.replace(/>/g, "&gt;")
			.replace(/"/g, "&quot;");
	}
}

customElements.define("folk-funds-app", FolkFundsApp);
