/**
 * <folk-gov-circuit> — n8n-style interactive mini-canvas for governance
 * decision circuits in rGov.
 *
 * Renders governance-specific workflow nodes (signoff, threshold, knob,
 * project, quadratic, conviction, multisig, sankey) on an SVG canvas
 * with ports, Bezier wiring, node palette, detail panel, and fit-view.
 *
 * Standalone canvas — no Automerge, no server calls. Pure client-side
 * with pre-loaded demo data.
 *
 * Attributes:
 *   circuit — which demo circuit to show (default: all)
 */

// ── Types ──

interface GovNodeDef {
	type: string;
	label: string;
	icon: string;
	color: string;
	inputs: { name: string }[];
	outputs: { name: string }[];
}

interface GovNode {
	id: string;
	type: string;
	label: string;
	position: { x: number; y: number };
	config: Record<string, any>;
}

interface GovEdge {
	id: string;
	fromNode: string;
	fromPort: string;
	toNode: string;
	toPort: string;
}

// ── Constants ──

const NODE_WIDTH = 240;
const NODE_HEIGHT = 120;
const PORT_RADIUS = 6;

const GOV_NODE_CATALOG: GovNodeDef[] = [
	{
		type: 'folk-gov-binary',
		label: 'Signoff',
		icon: '\u2714',
		color: '#7c3aed',
		inputs: [{ name: 'in' }],
		outputs: [{ name: 'out' }],
	},
	{
		type: 'folk-gov-threshold',
		label: 'Threshold',
		icon: '\u2593',
		color: '#0891b2',
		inputs: [{ name: 'in' }],
		outputs: [{ name: 'out' }],
	},
	{
		type: 'folk-gov-knob',
		label: 'Knob',
		icon: '\u2699',
		color: '#b45309',
		inputs: [{ name: 'in' }],
		outputs: [{ name: 'out' }],
	},
	{
		type: 'folk-gov-project',
		label: 'Project',
		icon: '\u25A3',
		color: '#10b981',
		inputs: [{ name: 'in' }],
		outputs: [{ name: 'out' }],
	},
	{
		type: 'folk-gov-quadratic',
		label: 'Quadratic',
		icon: '\u221A',
		color: '#14b8a6',
		inputs: [{ name: 'in' }],
		outputs: [{ name: 'out' }],
	},
	{
		type: 'folk-gov-conviction',
		label: 'Conviction',
		icon: '\u23F1',
		color: '#d97706',
		inputs: [{ name: 'in' }],
		outputs: [{ name: 'out' }],
	},
	{
		type: 'folk-gov-multisig',
		label: 'Multisig',
		icon: '\u{1F511}',
		color: '#6366f1',
		inputs: [{ name: 'in' }],
		outputs: [{ name: 'out' }],
	},
	{
		type: 'folk-gov-sankey',
		label: 'Sankey',
		icon: '\u2B82',
		color: '#f43f5e',
		inputs: [{ name: 'in' }],
		outputs: [{ name: 'out' }],
	},
];

// ── Demo Data ──

function buildDemoData(): { nodes: GovNode[]; edges: GovEdge[] } {
	const nodes: GovNode[] = [];
	const edges: GovEdge[] = [];
	let eid = 0;
	const mkEdge = (from: string, fp: string, to: string, tp: string) => {
		edges.push({ id: `ge-${++eid}`, fromNode: from, fromPort: fp, toNode: to, toPort: tp });
	};

	// ── Circuit 1: Build a Climbing Wall ──
	const c1y = 40;
	nodes.push({
		id: 'c1-labor', type: 'folk-gov-threshold', label: 'Labor Threshold',
		position: { x: 50, y: c1y },
		config: { target: 50, current: 20, unit: 'hours', contributors: 'Alice: 8h, Bob: 6h, Carol: 6h' },
	});
	nodes.push({
		id: 'c1-capital', type: 'folk-gov-threshold', label: 'Capital Threshold',
		position: { x: 50, y: c1y + 160 },
		config: { target: 3000, current: 2000, unit: '$', contributors: 'Fund A: $1200, Dave: $800' },
	});
	nodes.push({
		id: 'c1-signoff', type: 'folk-gov-binary', label: 'Proprietor Signoff',
		position: { x: 50, y: c1y + 320 },
		config: { assignee: 'Landlord (pending)', satisfied: false },
	});
	nodes.push({
		id: 'c1-project', type: 'folk-gov-project', label: 'Build a Climbing Wall',
		position: { x: 400, y: c1y + 140 },
		config: { description: 'Community climbing wall in the courtyard', gatesSatisfied: 1, gatesTotal: 3 },
	});
	mkEdge('c1-labor', 'out', 'c1-project', 'in');
	mkEdge('c1-capital', 'out', 'c1-project', 'in');
	mkEdge('c1-signoff', 'out', 'c1-project', 'in');

	// ── Circuit 2: Community Potluck ──
	const c2y = c1y + 520;
	nodes.push({
		id: 'c2-budget', type: 'folk-gov-knob', label: 'Budget Knob',
		position: { x: 50, y: c2y },
		config: { min: 100, max: 5000, value: 1500, unit: '$' },
	});
	nodes.push({
		id: 'c2-rsvps', type: 'folk-gov-threshold', label: 'RSVPs',
		position: { x: 50, y: c2y + 160 },
		config: { target: 20, current: 14, unit: 'people', contributors: '14 confirmed attendees' },
	});
	nodes.push({
		id: 'c2-venue', type: 'folk-gov-binary', label: 'Venue Signoff',
		position: { x: 50, y: c2y + 320 },
		config: { assignee: 'Carlos', satisfied: true, signedBy: 'Carlos' },
	});
	nodes.push({
		id: 'c2-project', type: 'folk-gov-project', label: 'Community Potluck',
		position: { x: 400, y: c2y + 140 },
		config: { description: 'Monthly community potluck event', gatesSatisfied: 2, gatesTotal: 3 },
	});
	mkEdge('c2-budget', 'out', 'c2-project', 'in');
	mkEdge('c2-rsvps', 'out', 'c2-project', 'in');
	mkEdge('c2-venue', 'out', 'c2-project', 'in');

	// ── Circuit 3: Delegated Budget Approval ──
	const c3y = c2y + 520;
	nodes.push({
		id: 'c3-quad', type: 'folk-gov-quadratic', label: 'Quadratic Dampener',
		position: { x: 50, y: c3y },
		config: { mode: 'sqrt', entries: 'Alice: 100, Bob: 49, Carol: 25' },
	});
	nodes.push({
		id: 'c3-conviction', type: 'folk-gov-conviction', label: 'Conviction Accumulator',
		position: { x: 50, y: c3y + 160 },
		config: { threshold: 500, accumulated: 320, stakes: 'Alice: 150, Bob: 100, Carol: 70' },
	});
	nodes.push({
		id: 'c3-multisig', type: 'folk-gov-multisig', label: '3-of-5 Multisig',
		position: { x: 50, y: c3y + 320 },
		config: { required: 3, total: 5, signers: 'Alice, Bob, Carol, Dave, Eve', signed: 'Alice, Bob' },
	});
	nodes.push({
		id: 'c3-project', type: 'folk-gov-project', label: 'Delegated Budget Approval',
		position: { x: 400, y: c3y + 140 },
		config: { description: 'Multi-mechanism budget governance', gatesSatisfied: 0, gatesTotal: 3 },
	});
	nodes.push({
		id: 'c3-sankey', type: 'folk-gov-sankey', label: 'Flow Visualizer',
		position: { x: 700, y: c3y + 140 },
		config: { note: 'Decorative flow diagram' },
	});
	mkEdge('c3-quad', 'out', 'c3-project', 'in');
	mkEdge('c3-conviction', 'out', 'c3-project', 'in');
	mkEdge('c3-multisig', 'out', 'c3-project', 'in');

	return { nodes, edges };
}

// ── Helpers ──

function esc(s: string): string {
	const d = document.createElement('div');
	d.textContent = s || '';
	return d.innerHTML;
}

function getNodeDef(type: string): GovNodeDef | undefined {
	return GOV_NODE_CATALOG.find(n => n.type === type);
}

function getPortX(node: GovNode, _portName: string, direction: 'input' | 'output'): number {
	return direction === 'input' ? node.position.x : node.position.x + NODE_WIDTH;
}

function getPortY(node: GovNode, _portName: string, direction: 'input' | 'output'): number {
	const def = getNodeDef(node.type);
	if (!def) return node.position.y + NODE_HEIGHT / 2;
	const ports = direction === 'input' ? def.inputs : def.outputs;
	const idx = ports.findIndex(p => p.name === _portName);
	if (idx === -1) return node.position.y + NODE_HEIGHT / 2;
	const spacing = NODE_HEIGHT / (ports.length + 1);
	return node.position.y + spacing * (idx + 1);
}

function bezierPath(x1: number, y1: number, x2: number, y2: number): string {
	const dx = Math.abs(x2 - x1) * 0.5;
	return `M ${x1} ${y1} C ${x1 + dx} ${y1}, ${x2 - dx} ${y2}, ${x2} ${y2}`;
}

// ── Node body renderers ──

function renderNodeBody(node: GovNode): string {
	const c = node.config;
	switch (node.type) {
		case 'folk-gov-binary': {
			const satisfied = c.satisfied ? 'Yes' : 'No';
			const checkColor = c.satisfied ? '#22c55e' : '#475569';
			const checkBg = c.satisfied ? 'rgba(34,197,94,0.15)' : 'rgba(71,85,105,0.15)';
			const checkIcon = c.satisfied ? '&#x2714;' : '';
			return `
				<div style="font-size:11px;color:#94a3b8;margin-top:2px">Assignee: ${esc(c.assignee || 'Unassigned')}</div>
				<div class="gc-signoff-toggle" data-node-id="${node.id}" style="
					display:flex;align-items:center;gap:8px;margin-top:6px;
					cursor:pointer;padding:4px 8px;border-radius:6px;
					background:${checkBg};border:1px solid ${checkColor}40;
					transition:all 0.15s;user-select:none;
					-webkit-tap-highlight-color:transparent;
				">
					<div style="
						width:20px;height:20px;border-radius:4px;
						border:2px solid ${checkColor};
						display:flex;align-items:center;justify-content:center;
						font-size:14px;color:${checkColor};
						background:${c.satisfied ? checkColor + '20' : 'transparent'};
						flex-shrink:0;transition:all 0.15s;
					">${checkIcon}</div>
					<span style="font-size:12px;color:#e2e8f0;font-weight:500">${satisfied}</span>
					${c.satisfied && c.signedBy ? `<span style="font-size:10px;color:#64748b;margin-left:auto">by ${esc(c.signedBy)}</span>` : ''}
				</div>`;
		}
		case 'folk-gov-threshold': {
			const pct = c.target > 0 ? Math.min(100, Math.round((c.current / c.target) * 100)) : 0;
			return `
				<div style="display:flex;justify-content:space-between;font-size:11px;color:#94a3b8;margin-top:2px">
					<span>${c.current}/${c.target} ${esc(c.unit || '')}</span>
					<span>${pct}%</span>
				</div>
				<div style="background:#334155;border-radius:3px;height:6px;margin-top:4px;overflow:hidden">
					<div style="background:#0891b2;width:${pct}%;height:100%;border-radius:3px;transition:width 0.3s"></div>
				</div>`;
		}
		case 'folk-gov-knob': {
			const pct = c.max > c.min ? Math.round(((c.value - c.min) / (c.max - c.min)) * 100) : 50;
			return `
				<div style="font-size:11px;color:#94a3b8;margin-top:2px">
					${esc(c.unit || '')}${c.min} \u2014 ${esc(c.unit || '')}${c.max}
				</div>
				<div style="display:flex;align-items:center;gap:6px;margin-top:4px">
					<div style="flex:1;background:#334155;border-radius:3px;height:6px;position:relative">
						<div style="background:#b45309;width:${pct}%;height:100%;border-radius:3px"></div>
						<div style="position:absolute;top:-4px;left:${pct}%;width:10px;height:14px;background:#fbbf24;border-radius:2px;transform:translateX(-5px)"></div>
					</div>
					<span style="font-size:12px;color:#e2e8f0;min-width:40px;text-align:right">${esc(c.unit || '')}${c.value}</span>
				</div>`;
		}
		case 'folk-gov-project': {
			const sat = c.gatesSatisfied || 0;
			const tot = c.gatesTotal || 0;
			return `
				<div style="font-size:11px;color:#94a3b8;margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(c.description || '')}</div>
				<div style="display:flex;align-items:center;gap:6px;margin-top:6px">
					<span style="font-size:12px;color:#10b981;font-weight:600">${sat} of ${tot} gates satisfied</span>
					${sat >= tot ? '<span style="color:#22c55e">&#x2714;</span>' : '<span style="color:#f59e0b">&#x25CF;</span>'}
				</div>`;
		}
		case 'folk-gov-quadratic': {
			const mode = c.mode || 'sqrt';
			return `
				<div style="font-size:11px;color:#94a3b8;margin-top:2px">Mode: <span style="color:#5eead4">${esc(mode)}</span></div>
				<div style="font-size:10px;color:#64748b;margin-top:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(c.entries || '')}</div>`;
		}
		case 'folk-gov-conviction': {
			const pct = c.threshold > 0 ? Math.min(100, Math.round((c.accumulated / c.threshold) * 100)) : 0;
			return `
				<div style="display:flex;justify-content:space-between;font-size:11px;color:#94a3b8;margin-top:2px">
					<span>Score: ${c.accumulated}/${c.threshold}</span>
					<span>${pct}%</span>
				</div>
				<div style="background:#334155;border-radius:3px;height:6px;margin-top:4px;overflow:hidden">
					<div style="background:#d97706;width:${pct}%;height:100%;border-radius:3px"></div>
				</div>`;
		}
		case 'folk-gov-multisig': {
			const signed = (c.signed || '').split(',').map((s: string) => s.trim()).filter(Boolean);
			const all = (c.signers || '').split(',').map((s: string) => s.trim()).filter(Boolean);
			const checks = all.map((s: string) => {
				const ok = signed.includes(s);
				return `<span style="color:${ok ? '#22c55e' : '#475569'};font-size:11px" title="${esc(s)}">${ok ? '\u2714' : '\u25CB'}</span>`;
			}).join(' ');
			return `
				<div style="font-size:11px;color:#94a3b8;margin-top:2px">${c.required} of ${c.total} required</div>
				<div style="margin-top:4px;display:flex;gap:4px;flex-wrap:wrap">${checks}</div>`;
		}
		case 'folk-gov-sankey': {
			return `
				<div style="font-size:11px;color:#94a3b8;margin-top:2px">Flow visualization</div>
				<div style="display:flex;gap:2px;margin-top:6px;height:20px;align-items:end">
					<div style="width:16px;background:linear-gradient(to top,#f43f5e80,#f43f5e20);height:18px;border-radius:2px"></div>
					<div style="width:16px;background:linear-gradient(to top,#f43f5e80,#f43f5e20);height:12px;border-radius:2px"></div>
					<div style="width:16px;background:linear-gradient(to top,#f43f5e80,#f43f5e20);height:20px;border-radius:2px"></div>
					<div style="width:16px;background:linear-gradient(to top,#f43f5e80,#f43f5e20);height:8px;border-radius:2px"></div>
					<div style="width:16px;background:linear-gradient(to top,#f43f5e80,#f43f5e20);height:15px;border-radius:2px"></div>
				</div>`;
		}
		default:
			return '';
	}
}

// ── Styles ──

const STYLES = `
:host {
	display: block;
	width: 100%;
	height: 100%;
	font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
	color: #e2e8f0;
}

.gc-root {
	display: flex;
	flex-direction: column;
	height: 100%;
	background: #0f172a;
}

/* ── Toolbar ── */

.gc-toolbar {
	display: flex;
	align-items: center;
	justify-content: space-between;
	padding: 8px 16px;
	background: #1e293b;
	border-bottom: 1px solid #334155;
	flex-shrink: 0;
	gap: 12px;
}

.gc-toolbar__title {
	display: flex;
	align-items: center;
	gap: 12px;
	font-size: 14px;
	font-weight: 600;
	color: #f1f5f9;
}

.gc-toolbar__actions {
	display: flex;
	align-items: center;
	gap: 8px;
}

.gc-btn {
	padding: 4px 12px;
	border: 1px solid #334155;
	border-radius: 6px;
	background: #1e293b;
	color: #cbd5e1;
	font-size: 12px;
	cursor: pointer;
	transition: background 0.15s;
}

.gc-btn:hover {
	background: #334155;
}

.gc-btn--fit {
	color: #38bdf8;
	border-color: #38bdf8;
}

/* ── Canvas area ── */

.gc-canvas-area {
	display: flex;
	flex: 1;
	overflow: hidden;
	position: relative;
}

/* ── Palette ── */

.gc-palette {
	width: 180px;
	flex-shrink: 0;
	background: #1e293b;
	border-right: 1px solid #334155;
	overflow-y: auto;
	padding: 12px 8px;
}

.gc-palette__title {
	font-size: 11px;
	font-weight: 700;
	text-transform: uppercase;
	letter-spacing: 0.05em;
	color: #64748b;
	margin-bottom: 8px;
}

.gc-palette__card {
	display: flex;
	align-items: center;
	gap: 8px;
	padding: 6px 8px;
	border-radius: 6px;
	border: 1px solid #334155;
	background: #0f172a;
	margin-bottom: 4px;
	cursor: grab;
	transition: border-color 0.15s, background 0.15s;
	font-size: 12px;
}

.gc-palette__card:hover {
	border-color: #475569;
	background: #1e293b;
}

.gc-palette__card-color {
	width: 10px;
	height: 10px;
	border-radius: 50%;
	flex-shrink: 0;
}

.gc-palette__card-label {
	color: #cbd5e1;
}

/* ── SVG Canvas ── */

.gc-canvas {
	flex: 1;
	position: relative;
	overflow: hidden;
	cursor: grab;
}

.gc-canvas.grabbing {
	cursor: grabbing;
}

.gc-canvas.wiring {
	cursor: crosshair;
}

.gc-canvas svg {
	width: 100%;
	height: 100%;
	display: block;
}

/* ── Grid ── */

.gc-grid-pattern line {
	stroke: #1e293b;
	stroke-width: 1;
}

/* ── Zoom controls ── */

.gc-zoom-controls {
	position: absolute;
	bottom: 16px;
	left: 50%;
	transform: translateX(-50%);
	display: flex;
	align-items: center;
	gap: 0;
	background: #1e293b;
	border: 1px solid #334155;
	border-radius: 8px;
	padding: 2px 4px;
}

.gc-zoom-btn {
	display: flex;
	align-items: center;
	justify-content: center;
	width: 28px;
	height: 28px;
	border: none;
	background: transparent;
	color: #94a3b8;
	cursor: pointer;
	border-radius: 4px;
}

.gc-zoom-btn:hover {
	background: #334155;
	color: #e2e8f0;
}

.gc-zoom-level {
	font-size: 11px;
	color: #64748b;
	min-width: 40px;
	text-align: center;
	user-select: none;
}

.gc-zoom-sep {
	width: 1px;
	height: 16px;
	background: #334155;
	margin: 0 2px;
}

/* ── Edges ── */

.gc-edge-path {
	fill: none;
	stroke-width: 2;
	pointer-events: none;
}

.gc-edge-hit {
	fill: none;
	stroke: transparent;
	stroke-width: 12;
	cursor: pointer;
}

.gc-edge-hit:hover + .gc-edge-path {
	stroke-opacity: 1;
	stroke-width: 3;
}

/* ── Wiring temp ── */

.gc-wiring-temp {
	fill: none;
	stroke: #38bdf8;
	stroke-width: 2;
	stroke-dasharray: 6 4;
	pointer-events: none;
}

/* ── Ports ── */

.gc-port-dot {
	transition: r 0.1s;
}

.gc-port-hit {
	cursor: crosshair;
}

.gc-port-hit:hover ~ .gc-port-dot,
.gc-port-group:hover .gc-port-dot {
	r: 8;
}

/* ── Detail panel ── */

.gc-detail {
	width: 0;
	overflow: hidden;
	background: #1e293b;
	border-left: 1px solid #334155;
	transition: width 0.2s;
	flex-shrink: 0;
}

.gc-detail.open {
	width: 280px;
}

.gc-detail__header {
	display: flex;
	justify-content: space-between;
	align-items: center;
	padding: 12px 16px;
	border-bottom: 1px solid #334155;
	font-size: 13px;
	font-weight: 600;
}

.gc-detail__close {
	background: none;
	border: none;
	color: #64748b;
	font-size: 18px;
	cursor: pointer;
	padding: 0 4px;
}

.gc-detail__close:hover {
	color: #e2e8f0;
}

.gc-detail__body {
	padding: 12px 16px;
	overflow-y: auto;
	max-height: calc(100% - 48px);
}

.gc-detail__field {
	margin-bottom: 12px;
}

.gc-detail__field label {
	display: block;
	font-size: 11px;
	color: #64748b;
	margin-bottom: 4px;
	text-transform: uppercase;
	letter-spacing: 0.04em;
}

.gc-detail__field input,
.gc-detail__field textarea,
.gc-detail__field select {
	width: 100%;
	padding: 6px 8px;
	border: 1px solid #334155;
	border-radius: 4px;
	background: #0f172a;
	color: #e2e8f0;
	font-size: 12px;
	box-sizing: border-box;
}

.gc-detail__field textarea {
	resize: vertical;
	min-height: 60px;
}

.gc-detail__delete {
	width: 100%;
	padding: 8px;
	margin-top: 12px;
	border: 1px solid #7f1d1d;
	border-radius: 6px;
	background: #450a0a;
	color: #fca5a5;
	font-size: 12px;
	cursor: pointer;
}

.gc-detail__delete:hover {
	background: #7f1d1d;
}

/* ── Collapsible sidebar toggle buttons ── */

.gc-sidebar-toggle {
	display: flex;
	align-items: center;
	justify-content: center;
	width: 32px;
	height: 32px;
	border: 1px solid #334155;
	border-radius: 6px;
	background: #1e293b;
	color: #94a3b8;
	font-size: 16px;
	cursor: pointer;
	flex-shrink: 0;
}

.gc-sidebar-toggle:hover {
	background: #334155;
	color: #e2e8f0;
}

.gc-sidebar-toggle.active {
	background: #334155;
	color: #38bdf8;
	border-color: #38bdf8;
}

/* ── Palette collapsed state ── */

.gc-palette {
	transition: width 0.2s, padding 0.2s, opacity 0.15s;
}

.gc-palette.collapsed {
	width: 0;
	padding: 0;
	overflow: hidden;
	border-right: none;
	opacity: 0;
	pointer-events: none;
}

/* ── Touch & pen support ── */

.gc-canvas svg {
	touch-action: none;
	-webkit-user-select: none;
	user-select: none;
}

/* ── Mobile responsive ── */

@media (max-width: 768px) {
	.gc-toolbar {
		padding: 6px 10px;
		gap: 6px;
	}
	.gc-toolbar__title {
		font-size: 13px;
		gap: 6px;
	}
	.gc-btn {
		padding: 4px 8px;
		font-size: 11px;
	}
	.gc-palette {
		width: 0;
		padding: 0;
		overflow: hidden;
		border-right: none;
		opacity: 0;
		pointer-events: none;
	}
	.gc-palette.mobile-open {
		width: 160px;
		padding: 10px 6px;
		overflow-y: auto;
		border-right: 1px solid #334155;
		opacity: 1;
		pointer-events: auto;
		position: absolute;
		left: 0;
		top: 0;
		bottom: 0;
		z-index: 10;
		background: #1e293b;
	}
	.gc-detail.open {
		width: 240px;
		position: absolute;
		right: 0;
		top: 0;
		bottom: 0;
		z-index: 10;
		background: #1e293b;
	}
	.gc-zoom-controls {
		bottom: 10px;
	}
	.gc-zoom-btn {
		width: 36px;
		height: 36px;
	}
	.gc-zoom-level {
		font-size: 12px;
		min-width: 44px;
	}
	/* Larger port hit targets for touch */
	.gc-port-hit {
		r: 18;
	}
	.gc-palette__card {
		padding: 8px 8px;
		font-size: 13px;
	}
}

@media (max-width: 480px) {
	.gc-toolbar__title span:not(:first-child) {
		display: none;
	}
	.gc-detail.open {
		width: 100%;
	}
	.gc-palette.mobile-open {
		width: 180px;
	}
}
`;

// ── Component ──

export class FolkGovCircuit extends HTMLElement {
	private shadow: ShadowRoot;

	// Data
	private nodes: GovNode[] = [];
	private edges: GovEdge[] = [];

	// Canvas state
	private canvasZoom = 1;
	private canvasPanX = 0;
	private canvasPanY = 0;
	private showGrid = true;

	// Sidebar state
	private paletteOpen = false;
	private detailOpen = false;

	// Interaction
	private isPanning = false;
	private panStartX = 0;
	private panStartY = 0;
	private panStartPanX = 0;
	private panStartPanY = 0;
	private draggingNodeId: string | null = null;
	private dragStartX = 0;
	private dragStartY = 0;
	private dragNodeStartX = 0;
	private dragNodeStartY = 0;

	// Touch — pinch-to-zoom
	private activeTouches: Map<number, { x: number; y: number }> = new Map();
	private pinchStartDist = 0;
	private pinchStartZoom = 1;
	private pinchMidX = 0;
	private pinchMidY = 0;

	// Selection & detail panel
	private selectedNodeId: string | null = null;

	// Wiring
	private wiringActive = false;
	private wiringSourceNodeId: string | null = null;
	private wiringSourcePortName: string | null = null;
	private wiringPointerX = 0;
	private wiringPointerY = 0;

	// Bound listeners
	private _boundPointerMove: ((e: PointerEvent) => void) | null = null;
	private _boundPointerUp: ((e: PointerEvent) => void) | null = null;
	private _boundKeyDown: ((e: KeyboardEvent) => void) | null = null;
	private _boundTouchStart: ((e: TouchEvent) => void) | null = null;
	private _boundTouchMove: ((e: TouchEvent) => void) | null = null;
	private _boundTouchEnd: ((e: TouchEvent) => void) | null = null;

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

	static get observedAttributes() { return ['circuit']; }

	connectedCallback() {
		this.initData();
	}

	disconnectedCallback() {
		if (this._boundPointerMove) document.removeEventListener('pointermove', this._boundPointerMove);
		if (this._boundPointerUp) document.removeEventListener('pointerup', this._boundPointerUp);
		if (this._boundKeyDown) document.removeEventListener('keydown', this._boundKeyDown);
		if (this._boundTouchStart) document.removeEventListener('touchstart', this._boundTouchStart);
		if (this._boundTouchMove) document.removeEventListener('touchmove', this._boundTouchMove);
		if (this._boundTouchEnd) document.removeEventListener('touchend', this._boundTouchEnd);
	}

	// ── Data init ──

	private initData() {
		const demo = buildDemoData();
		this.nodes = demo.nodes;
		this.edges = demo.edges;
		this.recalcProjectGates();
		this.render();
		requestAnimationFrame(() => this.fitView());
	}

	// ── Canvas transform ──

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

	private updateZoomDisplay() {
		const el = this.shadow.getElementById('zoom-level');
		if (el) el.textContent = `${Math.round(this.canvasZoom * 100)}%`;
	}

	private fitView() {
		const svg = this.shadow.getElementById('gc-svg') 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) {
			minX = Math.min(minX, n.position.x);
			minY = Math.min(minY, n.position.y);
			maxX = Math.max(maxX, n.position.x + NODE_WIDTH);
			maxY = Math.max(maxY, n.position.y + NODE_HEIGHT);
		}

		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 zoomAt(screenX: number, screenY: number, factor: number) {
		const oldZoom = this.canvasZoom;
		const newZoom = Math.max(0.1, Math.min(4, oldZoom * factor));
		this.canvasPanX = screenX - (screenX - this.canvasPanX) * (newZoom / oldZoom);
		this.canvasPanY = screenY - (screenY - this.canvasPanY) * (newZoom / oldZoom);
		this.canvasZoom = newZoom;
		this.updateCanvasTransform();
	}

	// ── Rendering ──

	private render() {
		const gridDef = this.showGrid ? `
			<defs>
				<pattern id="gc-grid" width="40" height="40" patternUnits="userSpaceOnUse">
					<line x1="40" y1="0" x2="40" y2="40" class="gc-grid-pattern"/>
					<line x1="0" y1="40" x2="40" y2="40" class="gc-grid-pattern"/>
				</pattern>
			</defs>
			<rect width="10000" height="10000" x="-5000" y="-5000" fill="url(#gc-grid)"/>` : '';

		this.shadow.innerHTML = `
			<style>${STYLES}</style>
			<div class="gc-root">
				<div class="gc-toolbar">
					<div class="gc-toolbar__title">
						<button class="gc-sidebar-toggle ${this.paletteOpen ? 'active' : ''}" id="btn-palette-toggle" title="Toggle palette">&#9776;</button>
						<span>\u25A3 Governance Circuits</span>
					</div>
					<div class="gc-toolbar__actions">
						<button class="gc-btn" id="btn-zoom-out" title="Zoom out">\u2212</button>
						<button class="gc-btn" id="btn-zoom-in" title="Zoom in">+</button>
						<button class="gc-btn gc-btn--fit" id="btn-fit" title="Fit view">\u2922 Fit</button>
						<button class="gc-btn" id="btn-grid" title="Toggle grid"># Grid</button>
					</div>
				</div>

				<div class="gc-canvas-area">
					<div class="gc-palette ${this.paletteOpen ? 'mobile-open' : 'collapsed'}" id="palette">
						<div class="gc-palette__title">Node Types</div>
						${GOV_NODE_CATALOG.map(n => `
							<div class="gc-palette__card" data-node-type="${n.type}" draggable="true">
								<div class="gc-palette__card-color" style="background:${n.color}"></div>
								<span class="gc-palette__card-label">${n.icon} ${esc(n.label)}</span>
							</div>
						`).join('')}
					</div>

					<div class="gc-canvas" id="gc-canvas">
						<svg id="gc-svg" xmlns="http://www.w3.org/2000/svg">
							<g id="canvas-transform">
								${gridDef}
								<g id="edge-layer">${this.renderAllEdges()}</g>
								<g id="wire-layer"></g>
								<g id="node-layer">${this.renderAllNodes()}</g>
							</g>
						</svg>
						<div class="gc-zoom-controls">
							<button class="gc-zoom-btn" id="zoom-out" title="Zoom out">
								<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 8h10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
							</button>
							<span class="gc-zoom-sep"></span>
							<span class="gc-zoom-level" id="zoom-level">${Math.round(this.canvasZoom * 100)}%</span>
							<span class="gc-zoom-sep"></span>
							<button class="gc-zoom-btn" id="zoom-in" title="Zoom in">
								<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
							</button>
							<span class="gc-zoom-sep"></span>
							<button class="gc-zoom-btn" id="zoom-fit" title="Fit to view">
								<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 6V3a1 1 0 011-1h3M10 2h3a1 1 0 011 1v3M14 10v3a1 1 0 01-1 1h-3M6 14H3a1 1 0 01-1-1v-3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
							</button>
						</div>
					</div>

					<div class="gc-detail ${this.detailOpen ? 'open' : ''}" id="detail-panel">
						${this.renderDetailPanel()}
					</div>
				</div>
			</div>
		`;

		this.attachEventListeners();
	}

	private renderAllNodes(): string {
		return this.nodes.map(node => this.renderNode(node)).join('');
	}

	private renderNode(node: GovNode): string {
		const def = getNodeDef(node.type);
		if (!def) return '';
		const isSelected = node.id === this.selectedNodeId;

		// Ports
		let portsHtml = '';
		for (const inp of def.inputs) {
			const y = getPortY(node, inp.name, 'input');
			const x = node.position.x;
			portsHtml += `
				<g class="gc-port-group" data-node-id="${node.id}" data-port-name="${inp.name}" data-port-dir="input">
					<circle class="gc-port-dot" cx="${x}" cy="${y}" r="${PORT_RADIUS}" fill="#94a3b8" stroke="#0f172a" stroke-width="2"/>
					<circle cx="${x}" cy="${y}" r="12" fill="transparent" class="gc-port-hit"/>
				</g>`;
		}
		for (const out of def.outputs) {
			const y = getPortY(node, out.name, 'output');
			const x = node.position.x + NODE_WIDTH;
			portsHtml += `
				<g class="gc-port-group" data-node-id="${node.id}" data-port-name="${out.name}" data-port-dir="output">
					<circle class="gc-port-dot" cx="${x}" cy="${y}" r="${PORT_RADIUS}" fill="${def.color}" stroke="#0f172a" stroke-width="2"/>
					<circle cx="${x}" cy="${y}" r="12" fill="transparent" class="gc-port-hit"/>
				</g>`;
		}

		const bodyHtml = renderNodeBody(node);

		return `
			<g class="gc-node ${isSelected ? 'selected' : ''}" data-node-id="${node.id}">
				<foreignObject x="${node.position.x}" y="${node.position.y}" width="${NODE_WIDTH}" height="${NODE_HEIGHT}">
					<div xmlns="http://www.w3.org/1999/xhtml" style="
						width:100%;height:100%;box-sizing:border-box;
						background:#1e293b;
						border:1px solid ${isSelected ? '#38bdf8' : '#334155'};
						border-radius:8px;
						overflow:hidden;
						display:flex;
						font-family:inherit;
						cursor:pointer;
						transition:border-color 0.15s;
					">
						<div style="width:4px;flex-shrink:0;background:${def.color};border-radius:8px 0 0 8px"></div>
						<div style="flex:1;padding:8px 10px;overflow:hidden;min-width:0">
							<div style="display:flex;align-items:center;gap:6px;margin-bottom:2px">
								<span style="font-size:14px">${def.icon}</span>
								<span style="font-size:12px;font-weight:600;color:#f1f5f9;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(node.label)}</span>
							</div>
							${bodyHtml}
						</div>
					</div>
				</foreignObject>
				${portsHtml}
			</g>`;
	}

	private renderAllEdges(): string {
		return this.edges.map(edge => {
			const fromNode = this.nodes.find(n => n.id === edge.fromNode);
			const toNode = this.nodes.find(n => n.id === edge.toNode);
			if (!fromNode || !toNode) return '';

			const x1 = getPortX(fromNode, edge.fromPort, 'output');
			const y1 = getPortY(fromNode, edge.fromPort, 'output');
			const x2 = getPortX(toNode, edge.toPort, 'input');
			const y2 = getPortY(toNode, edge.toPort, 'input');

			const fromDef = getNodeDef(fromNode.type);
			const color = fromDef ? fromDef.color : '#6b7280';
			const d = bezierPath(x1, y1, x2, y2);

			return `
				<g class="gc-edge-group" data-edge-id="${edge.id}">
					<path class="gc-edge-hit" d="${d}"/>
					<path class="gc-edge-path" d="${d}" stroke="${color}" stroke-opacity="0.6"/>
				</g>`;
		}).join('');
	}

	private renderDetailPanel(): string {
		if (!this.selectedNodeId) {
			return `
				<div class="gc-detail__header">
					<span>No node selected</span>
					<button class="gc-detail__close" id="detail-close">&times;</button>
				</div>
				<div class="gc-detail__body">
					<p style="color:#64748b;font-size:12px">Click a node to view details.</p>
				</div>`;
		}

		const node = this.nodes.find(n => n.id === this.selectedNodeId);
		if (!node) return '';
		const def = getNodeDef(node.type);
		if (!def) return '';

		const fieldsHtml = this.renderDetailFields(node);

		return `
			<div class="gc-detail__header">
				<span>${def.icon} ${esc(node.label)}</span>
				<button class="gc-detail__close" id="detail-close">&times;</button>
			</div>
			<div class="gc-detail__body">
				<div class="gc-detail__field">
					<label>Label</label>
					<input type="text" id="detail-label" value="${esc(node.label)}">
				</div>
				<div class="gc-detail__field">
					<label>Type</label>
					<input type="text" value="${esc(def.label)}" disabled style="opacity:0.6">
				</div>
				${fieldsHtml}
				<button class="gc-detail__delete" id="detail-delete-node">Delete Node</button>
			</div>`;
	}

	private renderDetailFields(node: GovNode): string {
		const c = node.config;
		switch (node.type) {
			case 'folk-gov-binary':
				return `
					<div class="gc-detail__field">
						<label>Assignee</label>
						<input type="text" data-config-key="assignee" value="${esc(c.assignee || '')}">
					</div>
					<div class="gc-detail__field">
						<label>Satisfied</label>
						<select data-config-key="satisfied">
							<option value="true" ${c.satisfied ? 'selected' : ''}>Yes</option>
							<option value="false" ${!c.satisfied ? 'selected' : ''}>No</option>
						</select>
					</div>`;
			case 'folk-gov-threshold':
				return `
					<div class="gc-detail__field">
						<label>Target</label>
						<input type="number" data-config-key="target" value="${c.target || 0}">
					</div>
					<div class="gc-detail__field">
						<label>Current</label>
						<input type="number" data-config-key="current" value="${c.current || 0}">
					</div>
					<div class="gc-detail__field">
						<label>Unit</label>
						<input type="text" data-config-key="unit" value="${esc(c.unit || '')}">
					</div>
					<div class="gc-detail__field">
						<label>Contributors</label>
						<textarea data-config-key="contributors">${esc(c.contributors || '')}</textarea>
					</div>`;
			case 'folk-gov-knob':
				return `
					<div class="gc-detail__field">
						<label>Min</label>
						<input type="number" data-config-key="min" value="${c.min || 0}">
					</div>
					<div class="gc-detail__field">
						<label>Max</label>
						<input type="number" data-config-key="max" value="${c.max || 0}">
					</div>
					<div class="gc-detail__field">
						<label>Value</label>
						<input type="number" data-config-key="value" value="${c.value || 0}">
					</div>
					<div class="gc-detail__field">
						<label>Unit</label>
						<input type="text" data-config-key="unit" value="${esc(c.unit || '')}">
					</div>`;
			case 'folk-gov-project':
				return `
					<div class="gc-detail__field">
						<label>Description</label>
						<textarea data-config-key="description">${esc(c.description || '')}</textarea>
					</div>
					<div class="gc-detail__field">
						<label>Gates Satisfied</label>
						<input type="number" data-config-key="gatesSatisfied" value="${c.gatesSatisfied || 0}">
					</div>
					<div class="gc-detail__field">
						<label>Gates Total</label>
						<input type="number" data-config-key="gatesTotal" value="${c.gatesTotal || 0}">
					</div>`;
			case 'folk-gov-quadratic':
				return `
					<div class="gc-detail__field">
						<label>Mode</label>
						<select data-config-key="mode">
							<option value="sqrt" ${c.mode === 'sqrt' ? 'selected' : ''}>sqrt</option>
							<option value="log" ${c.mode === 'log' ? 'selected' : ''}>log</option>
						</select>
					</div>
					<div class="gc-detail__field">
						<label>Entries</label>
						<textarea data-config-key="entries">${esc(c.entries || '')}</textarea>
					</div>`;
			case 'folk-gov-conviction':
				return `
					<div class="gc-detail__field">
						<label>Threshold</label>
						<input type="number" data-config-key="threshold" value="${c.threshold || 0}">
					</div>
					<div class="gc-detail__field">
						<label>Accumulated</label>
						<input type="number" data-config-key="accumulated" value="${c.accumulated || 0}">
					</div>
					<div class="gc-detail__field">
						<label>Stakes</label>
						<textarea data-config-key="stakes">${esc(c.stakes || '')}</textarea>
					</div>`;
			case 'folk-gov-multisig':
				return `
					<div class="gc-detail__field">
						<label>Required</label>
						<input type="number" data-config-key="required" value="${c.required || 0}">
					</div>
					<div class="gc-detail__field">
						<label>Total Signers</label>
						<input type="number" data-config-key="total" value="${c.total || 0}">
					</div>
					<div class="gc-detail__field">
						<label>Signers (comma-separated)</label>
						<textarea data-config-key="signers">${esc(c.signers || '')}</textarea>
					</div>
					<div class="gc-detail__field">
						<label>Signed (comma-separated)</label>
						<textarea data-config-key="signed">${esc(c.signed || '')}</textarea>
					</div>`;
			case 'folk-gov-sankey':
				return `
					<div class="gc-detail__field">
						<label>Note</label>
						<textarea data-config-key="note">${esc(c.note || '')}</textarea>
					</div>`;
			default:
				return '';
		}
	}

	// ── Redraw helpers ──

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

	private attachSignoffHandlers() {
		const nodeLayer = this.shadow.getElementById('node-layer');
		if (!nodeLayer) return;
		nodeLayer.querySelectorAll('.gc-signoff-toggle').forEach(el => {
			el.addEventListener('click', (e: Event) => {
				e.stopPropagation();
				e.preventDefault();
				const nodeId = (el as HTMLElement).dataset.nodeId;
				if (!nodeId) return;
				this.toggleSignoff(nodeId);
			});
			// Prevent the click from starting a node drag
			el.addEventListener('pointerdown', (e: Event) => {
				e.stopPropagation();
			});
		});
	}

	private toggleSignoff(nodeId: string) {
		const node = this.nodes.find(n => n.id === nodeId);
		if (!node || node.type !== 'folk-gov-binary') return;

		// Authority check: resolve current user from EncryptID JWT if available
		let currentUser = 'You';
		try {
			const token = document.cookie.split(';').map(c => c.trim()).find(c => c.startsWith('auth='));
			if (token) {
				const payload = JSON.parse(atob(token.split('=')[1].split('.')[1]));
				currentUser = payload.username || payload.sub || 'You';
			}
		} catch { /* no auth context — allow toggle for demo */ }

		// Check authority: if assignee is set, only the assignee (or admin) can toggle
		const assignee = (node.config.assignee || '').toLowerCase().trim();
		const userLower = currentUser.toLowerCase().trim();
		if (assignee && assignee !== 'unassigned' && !assignee.includes('pending')) {
			// Extract just the name part (handle "Carlos", "Landlord (pending)", etc.)
			const assigneeName = assignee.replace(/\s*\(.*\)\s*$/, '');
			if (assigneeName && assigneeName !== userLower && userLower !== 'you') {
				// Not authorized — show brief feedback
				const el = this.shadow.querySelector(`[data-node-id="${nodeId}"].gc-signoff-toggle`) as HTMLElement;
				if (el) {
					el.style.outline = '2px solid #ef4444';
					el.style.outlineOffset = '2px';
					setTimeout(() => { el.style.outline = ''; el.style.outlineOffset = ''; }, 600);
				}
				return;
			}
		}

		// Toggle
		node.config.satisfied = !node.config.satisfied;
		node.config.signedBy = node.config.satisfied ? currentUser : '';

		// Update connected project aggregators
		this.recalcProjectGates();

		// Re-render
		this.drawCanvasContent();
		if (this.selectedNodeId === nodeId) {
			this.refreshDetailPanel();
		}
	}

	private recalcProjectGates() {
		// For each project node, count how many of its incoming edges come from satisfied gates
		for (const node of this.nodes) {
			if (node.type !== 'folk-gov-project') continue;
			const incomingEdges = this.edges.filter(e => e.toNode === node.id);
			let satisfied = 0;
			let total = incomingEdges.length;
			for (const edge of incomingEdges) {
				const source = this.nodes.find(n => n.id === edge.fromNode);
				if (!source) continue;
				if (source.type === 'folk-gov-binary' && source.config.satisfied) satisfied++;
				else if (source.type === 'folk-gov-threshold' && source.config.current >= source.config.target) satisfied++;
				else if (source.type === 'folk-gov-multisig') {
					const signed = (source.config.signed || '').split(',').filter((s: string) => s.trim()).length;
					if (signed >= source.config.required) satisfied++;
				}
				else if (source.type === 'folk-gov-conviction' && source.config.accumulated >= source.config.threshold) satisfied++;
			}
			node.config.gatesSatisfied = satisfied;
			node.config.gatesTotal = total;
		}
	}

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

	private updateNodePosition(node: GovNode) {
		const nodeLayer = this.shadow.getElementById('node-layer');
		if (!nodeLayer) return;
		const g = nodeLayer.querySelector(`[data-node-id="${node.id}"]`) as SVGGElement | null;
		if (!g) return;
		const fo = g.querySelector('foreignObject');
		if (fo) {
			fo.setAttribute('x', String(node.position.x));
			fo.setAttribute('y', String(node.position.y));
		}
		const def = getNodeDef(node.type);
		if (!def) return;
		const portGroups = g.querySelectorAll('.gc-port-group');
		portGroups.forEach(pg => {
			const portName = (pg as HTMLElement).dataset.portName!;
			const dir = (pg as HTMLElement).dataset.portDir as 'input' | 'output';
			const x = dir === 'input' ? node.position.x : node.position.x + NODE_WIDTH;
			const ports = dir === 'input' ? def.inputs : def.outputs;
			const idx = ports.findIndex(p => p.name === portName);
			const spacing = NODE_HEIGHT / (ports.length + 1);
			const y = node.position.y + spacing * (idx + 1);
			pg.querySelectorAll('circle').forEach(c => {
				c.setAttribute('cx', String(x));
				c.setAttribute('cy', String(y));
			});
		});
	}

	private refreshDetailPanel() {
		const panel = this.shadow.getElementById('detail-panel');
		if (!panel) return;
		panel.className = `gc-detail ${this.detailOpen ? 'open' : ''}`;
		panel.innerHTML = this.renderDetailPanel();
		this.attachDetailListeners();
	}

	// ── Node operations ──

	private addNode(type: string, x: number, y: number) {
		const def = getNodeDef(type);
		if (!def) return;
		const id = `gn-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
		const node: GovNode = {
			id,
			type: def.type,
			label: def.label,
			position: { x, y },
			config: this.defaultConfigFor(type),
		};
		this.nodes.push(node);
		this.drawCanvasContent();
		this.selectNode(id);
	}

	private defaultConfigFor(type: string): Record<string, any> {
		switch (type) {
			case 'folk-gov-binary': return { assignee: '', satisfied: false, signedBy: '' };
			case 'folk-gov-threshold': return { target: 100, current: 0, unit: '', contributors: '' };
			case 'folk-gov-knob': return { min: 0, max: 100, value: 50, unit: '' };
			case 'folk-gov-project': return { description: '', gatesSatisfied: 0, gatesTotal: 0 };
			case 'folk-gov-quadratic': return { mode: 'sqrt', entries: '' };
			case 'folk-gov-conviction': return { threshold: 100, accumulated: 0, stakes: '' };
			case 'folk-gov-multisig': return { required: 2, total: 3, signers: '', signed: '' };
			case 'folk-gov-sankey': return { note: '' };
			default: return {};
		}
	}

	private deleteNode(nodeId: string) {
		this.nodes = this.nodes.filter(n => n.id !== nodeId);
		this.edges = this.edges.filter(e => e.fromNode !== nodeId && e.toNode !== nodeId);
		if (this.selectedNodeId === nodeId) {
			this.selectedNodeId = null;
			this.detailOpen = false;
		}
		this.drawCanvasContent();
		this.refreshDetailPanel();
	}

	private selectNode(nodeId: string) {
		this.selectedNodeId = nodeId;
		this.detailOpen = true;
		const nodeLayer = this.shadow.getElementById('node-layer');
		if (nodeLayer) {
			nodeLayer.querySelectorAll('.gc-node').forEach(g => {
				g.classList.toggle('selected', g.getAttribute('data-node-id') === nodeId);
			});
		}
		this.refreshDetailPanel();
		// Re-render nodes to update border highlight
		this.drawCanvasContent();
	}

	// ── Wiring ──

	private enterWiring(nodeId: string, portName: string, dir: 'input' | 'output') {
		if (dir !== 'output') return;
		this.wiringActive = true;
		this.wiringSourceNodeId = nodeId;
		this.wiringSourcePortName = portName;
		const canvas = this.shadow.getElementById('gc-canvas');
		if (canvas) canvas.classList.add('wiring');
	}

	private cancelWiring() {
		this.wiringActive = false;
		this.wiringSourceNodeId = null;
		this.wiringSourcePortName = null;
		const canvas = this.shadow.getElementById('gc-canvas');
		if (canvas) canvas.classList.remove('wiring');
		const wireLayer = this.shadow.getElementById('wire-layer');
		if (wireLayer) wireLayer.innerHTML = '';
	}

	private completeWiring(targetNodeId: string, targetPortName: string, targetDir: 'input' | 'output') {
		if (!this.wiringSourceNodeId || !this.wiringSourcePortName) { this.cancelWiring(); return; }
		if (targetDir !== 'input') { this.cancelWiring(); return; }
		if (targetNodeId === this.wiringSourceNodeId) { this.cancelWiring(); return; }

		const exists = this.edges.some(e =>
			e.fromNode === this.wiringSourceNodeId && e.fromPort === this.wiringSourcePortName &&
			e.toNode === targetNodeId && e.toPort === targetPortName
		);
		if (exists) { this.cancelWiring(); return; }

		const edgeId = `ge-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
		this.edges.push({
			id: edgeId,
			fromNode: this.wiringSourceNodeId,
			fromPort: this.wiringSourcePortName,
			toNode: targetNodeId,
			toPort: targetPortName,
		});

		this.cancelWiring();
		this.drawCanvasContent();
	}

	private updateWiringTempLine() {
		const svg = this.shadow.getElementById('gc-svg') as SVGSVGElement | null;
		const wireLayer = this.shadow.getElementById('wire-layer');
		if (!svg || !wireLayer || !this.wiringSourceNodeId || !this.wiringSourcePortName) return;

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

		const x1 = getPortX(sourceNode, this.wiringSourcePortName!, 'output');
		const y1 = getPortY(sourceNode, this.wiringSourcePortName!, 'output');

		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 d = bezierPath(x1, y1, x2, y2);
		wireLayer.innerHTML = `<path class="gc-wiring-temp" d="${d}"/>`;
	}

	// ── Event listeners ──

	private attachEventListeners() {
		const canvas = this.shadow.getElementById('gc-canvas')!;
		const svg = this.shadow.getElementById('gc-svg')!;
		const palette = this.shadow.getElementById('palette')!;

		// Toolbar buttons
		this.shadow.getElementById('btn-zoom-in')?.addEventListener('click', () => {
			const rect = svg.getBoundingClientRect();
			this.zoomAt(rect.width / 2, rect.height / 2, 1.2);
		});
		this.shadow.getElementById('btn-zoom-out')?.addEventListener('click', () => {
			const rect = svg.getBoundingClientRect();
			this.zoomAt(rect.width / 2, rect.height / 2, 0.8);
		});
		this.shadow.getElementById('btn-fit')?.addEventListener('click', () => this.fitView());
		this.shadow.getElementById('btn-grid')?.addEventListener('click', () => {
			this.showGrid = !this.showGrid;
			this.render();
			requestAnimationFrame(() => this.fitView());
		});

		// Bottom zoom controls
		this.shadow.getElementById('zoom-in')?.addEventListener('click', () => {
			const rect = svg.getBoundingClientRect();
			this.zoomAt(rect.width / 2, rect.height / 2, 1.2);
		});
		this.shadow.getElementById('zoom-out')?.addEventListener('click', () => {
			const rect = svg.getBoundingClientRect();
			this.zoomAt(rect.width / 2, rect.height / 2, 0.8);
		});
		this.shadow.getElementById('zoom-fit')?.addEventListener('click', () => this.fitView());

		// Canvas wheel zoom/pan
		canvas.addEventListener('wheel', (e: WheelEvent) => {
			e.preventDefault();
			if (e.ctrlKey || e.metaKey) {
				const rect = svg.getBoundingClientRect();
				const factor = e.deltaY < 0 ? 1.1 : 0.9;
				this.zoomAt(e.clientX - rect.left, e.clientY - rect.top, factor);
			} else {
				this.canvasPanX -= e.deltaX;
				this.canvasPanY -= e.deltaY;
				this.updateCanvasTransform();
			}
		}, { passive: false });

		// Palette drag
		palette.querySelectorAll('.gc-palette__card').forEach(card => {
			card.addEventListener('dragstart', (e: Event) => {
				const de = e as DragEvent;
				const type = (card as HTMLElement).dataset.nodeType!;
				de.dataTransfer?.setData('text/plain', type);
			});
		});

		// Canvas drop
		canvas.addEventListener('dragover', (e: DragEvent) => { e.preventDefault(); });
		canvas.addEventListener('drop', (e: DragEvent) => {
			e.preventDefault();
			const type = e.dataTransfer?.getData('text/plain');
			if (!type) return;
			const rect = svg.getBoundingClientRect();
			const x = (e.clientX - rect.left - this.canvasPanX) / this.canvasZoom;
			const y = (e.clientY - rect.top - this.canvasPanY) / this.canvasZoom;
			this.addNode(type, x - NODE_WIDTH / 2, y - NODE_HEIGHT / 2);
		});

		// Palette toggle
		this.shadow.getElementById('btn-palette-toggle')?.addEventListener('click', () => {
			this.paletteOpen = !this.paletteOpen;
			const pal = this.shadow.getElementById('palette');
			const btn = this.shadow.getElementById('btn-palette-toggle');
			if (pal) {
				pal.classList.toggle('collapsed', !this.paletteOpen);
				pal.classList.toggle('mobile-open', this.paletteOpen);
			}
			if (btn) btn.classList.toggle('active', this.paletteOpen);
		});

		// SVG pointer events
		svg.addEventListener('pointerdown', (e: PointerEvent) => this.handlePointerDown(e));

		// Global move/up/key
		this._boundPointerMove = (e: PointerEvent) => this.handlePointerMove(e);
		this._boundPointerUp = (e: PointerEvent) => this.handlePointerUp(e);
		this._boundKeyDown = (e: KeyboardEvent) => this.handleKeyDown(e);
		document.addEventListener('pointermove', this._boundPointerMove);
		document.addEventListener('pointerup', this._boundPointerUp);
		document.addEventListener('keydown', this._boundKeyDown);

		// Touch: pinch-to-zoom + two-finger pan
		this._boundTouchStart = (e: TouchEvent) => this.handleTouchStart(e);
		this._boundTouchMove = (e: TouchEvent) => this.handleTouchMove(e);
		this._boundTouchEnd = (e: TouchEvent) => this.handleTouchEnd(e);
		svg.addEventListener('touchstart', this._boundTouchStart, { passive: false });
		svg.addEventListener('touchmove', this._boundTouchMove, { passive: false });
		svg.addEventListener('touchend', this._boundTouchEnd, { passive: false });

		// Detail panel
		this.attachDetailListeners();
	}

	private attachDetailListeners() {
		this.shadow.getElementById('detail-close')?.addEventListener('click', () => {
			this.detailOpen = false;
			this.selectedNodeId = null;
			const panel = this.shadow.getElementById('detail-panel');
			if (panel) panel.className = 'gc-detail';
			this.drawCanvasContent();
		});

		this.shadow.getElementById('detail-label')?.addEventListener('input', (e) => {
			const node = this.nodes.find(n => n.id === this.selectedNodeId);
			if (node) {
				node.label = (e.target as HTMLInputElement).value;
				this.drawCanvasContent();
			}
		});

		this.shadow.getElementById('detail-delete-node')?.addEventListener('click', () => {
			if (this.selectedNodeId) this.deleteNode(this.selectedNodeId);
		});

		const panel = this.shadow.getElementById('detail-panel');
		if (panel) {
			panel.querySelectorAll('[data-config-key]').forEach(el => {
				const handler = (e: Event) => {
					const key = (el as HTMLElement).dataset.configKey!;
					const node = this.nodes.find(n => n.id === this.selectedNodeId);
					if (!node) return;
					let val: any = (e.target as HTMLInputElement).value;
					// Type coerce numbers
					if ((e.target as HTMLInputElement).type === 'number') {
						val = parseFloat(val) || 0;
					}
					// Coerce booleans from select
					if (val === 'true') val = true;
					if (val === 'false') val = false;
					node.config[key] = val;
					this.drawCanvasContent();
				};
				el.addEventListener('input', handler);
				el.addEventListener('change', handler);
			});
		}
	}

	private handlePointerDown(e: PointerEvent) {
		const target = e.target as Element;

		// Port click
		const portGroup = target.closest('.gc-port-group') as SVGElement | null;
		if (portGroup) {
			e.stopPropagation();
			const nodeId = portGroup.dataset.nodeId!;
			const portName = portGroup.dataset.portName!;
			const dir = portGroup.dataset.portDir as 'input' | 'output';

			if (this.wiringActive) {
				this.completeWiring(nodeId, portName, dir);
			} else {
				this.enterWiring(nodeId, portName, dir);
			}
			return;
		}

		// Edge click — delete
		const edgeGroup = target.closest('.gc-edge-group') as SVGElement | null;
		if (edgeGroup) {
			e.stopPropagation();
			const edgeId = edgeGroup.dataset.edgeId!;
			this.edges = this.edges.filter(ed => ed.id !== edgeId);
			this.redrawEdges();
			return;
		}

		// Node click — select + drag
		const nodeGroup = target.closest('.gc-node') as SVGElement | null;
		if (nodeGroup) {
			e.stopPropagation();
			if (this.wiringActive) {
				this.cancelWiring();
				return;
			}
			const nodeId = nodeGroup.dataset.nodeId!;
			this.selectNode(nodeId);

			const node = this.nodes.find(n => n.id === nodeId);
			if (node) {
				this.draggingNodeId = nodeId;
				this.dragStartX = e.clientX;
				this.dragStartY = e.clientY;
				this.dragNodeStartX = node.position.x;
				this.dragNodeStartY = node.position.y;
			}
			return;
		}

		// Canvas — pan or deselect
		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;
		const canvas = this.shadow.getElementById('gc-canvas');
		if (canvas) canvas.classList.add('grabbing');

		if (this.selectedNodeId) {
			this.selectedNodeId = null;
			this.detailOpen = false;
			this.drawCanvasContent();
			this.refreshDetailPanel();
		}
	}

	private handlePointerMove(e: PointerEvent) {
		if (this.wiringActive) {
			this.wiringPointerX = e.clientX;
			this.wiringPointerY = e.clientY;
			this.updateWiringTempLine();
			return;
		}

		if (this.draggingNodeId) {
			const node = this.nodes.find(n => n.id === this.draggingNodeId);
			if (node) {
				const dx = (e.clientX - this.dragStartX) / this.canvasZoom;
				const dy = (e.clientY - this.dragStartY) / this.canvasZoom;
				node.position.x = this.dragNodeStartX + dx;
				node.position.y = this.dragNodeStartY + dy;
				this.updateNodePosition(node);
				this.redrawEdges();
			}
			return;
		}

		if (this.isPanning) {
			this.canvasPanX = this.panStartPanX + (e.clientX - this.panStartX);
			this.canvasPanY = this.panStartPanY + (e.clientY - this.panStartY);
			this.updateCanvasTransform();
		}
	}

	private handlePointerUp(_e: PointerEvent) {
		if (this.draggingNodeId) {
			this.draggingNodeId = null;
		}
		if (this.isPanning) {
			this.isPanning = false;
			const canvas = this.shadow.getElementById('gc-canvas');
			if (canvas) canvas.classList.remove('grabbing');
		}
	}

	// ── Touch handlers (pinch-to-zoom, two-finger pan) ──

	private handleTouchStart(e: TouchEvent) {
		for (let i = 0; i < e.changedTouches.length; i++) {
			const t = e.changedTouches[i];
			this.activeTouches.set(t.identifier, { x: t.clientX, y: t.clientY });
		}
		if (this.activeTouches.size === 2) {
			e.preventDefault();
			const pts = [...this.activeTouches.values()];
			this.pinchStartDist = Math.hypot(pts[1].x - pts[0].x, pts[1].y - pts[0].y);
			this.pinchStartZoom = this.canvasZoom;
			this.pinchMidX = (pts[0].x + pts[1].x) / 2;
			this.pinchMidY = (pts[0].y + pts[1].y) / 2;
			this.panStartPanX = this.canvasPanX;
			this.panStartPanY = this.canvasPanY;
		}
	}

	private handleTouchMove(e: TouchEvent) {
		for (let i = 0; i < e.changedTouches.length; i++) {
			const t = e.changedTouches[i];
			this.activeTouches.set(t.identifier, { x: t.clientX, y: t.clientY });
		}
		if (this.activeTouches.size === 2) {
			e.preventDefault();
			const pts = [...this.activeTouches.values()];
			const dist = Math.hypot(pts[1].x - pts[0].x, pts[1].y - pts[0].y);
			const midX = (pts[0].x + pts[1].x) / 2;
			const midY = (pts[0].y + pts[1].y) / 2;

			// Zoom
			const svg = this.shadow.getElementById('gc-svg') as SVGSVGElement | null;
			if (svg) {
				const rect = svg.getBoundingClientRect();
				const scale = dist / this.pinchStartDist;
				const newZoom = Math.max(0.1, Math.min(4, this.pinchStartZoom * scale));
				const cx = this.pinchMidX - rect.left;
				const cy = this.pinchMidY - rect.top;
				this.canvasPanX = cx - (cx - this.panStartPanX) * (newZoom / this.pinchStartZoom);
				this.canvasPanY = cy - (cy - this.panStartPanY) * (newZoom / this.pinchStartZoom);
				this.canvasZoom = newZoom;

				// Pan offset from midpoint movement
				this.canvasPanX += (midX - this.pinchMidX);
				this.canvasPanY += (midY - this.pinchMidY);
				this.pinchMidX = midX;
				this.pinchMidY = midY;

				this.updateCanvasTransform();
			}
		}
	}

	private handleTouchEnd(e: TouchEvent) {
		for (let i = 0; i < e.changedTouches.length; i++) {
			this.activeTouches.delete(e.changedTouches[i].identifier);
		}
		if (this.activeTouches.size < 2) {
			this.pinchStartDist = 0;
		}
	}

	private handleKeyDown(e: KeyboardEvent) {
		const tag = (e.target as Element)?.tagName;
		const isEditing = tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT';

		if (e.key === 'Escape') {
			if (this.wiringActive) this.cancelWiring();
		}
		if ((e.key === 'Delete' || e.key === 'Backspace') && this.selectedNodeId) {
			if (isEditing) return;
			this.deleteNode(this.selectedNodeId);
		}
		if (isEditing) return;
		if (e.key === 'f' || e.key === 'F') {
			this.fitView();
		}
		if (e.key === '=' || e.key === '+') {
			const svg = this.shadow.getElementById('gc-svg');
			if (svg) { const r = svg.getBoundingClientRect(); this.zoomAt(r.width / 2, r.height / 2, 1.2); }
		}
		if (e.key === '-') {
			const svg = this.shadow.getElementById('gc-svg');
			if (svg) { const r = svg.getBoundingClientRect(); this.zoomAt(r.width / 2, r.height / 2, 0.8); }
		}
	}
}

customElements.define('folk-gov-circuit', FolkGovCircuit);
