/**
 * folk-timebank-app — Timebank commitment pool & weaving dashboard.
 *
 * Port of hcc-mem-staging/html/index.html into a shadow-DOM web component.
 * Two views: "pool" (canvas orbs) and "weave" (SVG node editor).
 */

import type { DocumentId } from "../../../shared/local-first/document";
import { startPresenceHeartbeat } from '../../../shared/collab-presence';
import { commitmentsSchema, commitmentsDocId } from "../schemas";
import { ViewHistory } from "../../../shared/view-history.js";

// ── Constants ──

const SKILL_COLORS: Record<string, string> = {
	facilitation: '#8b5cf6', design: '#ec4899', tech: '#3b82f6',
	outreach: '#10b981', logistics: '#f59e0b',
};
const SKILL_LABELS: Record<string, string> = {
	facilitation: 'Facilitation', design: 'Design', tech: 'Tech',
	outreach: 'Outreach', logistics: 'Logistics',
};

const EXEC_STEPS: Record<string, { title: string; desc: string; detail: string; icon: string }[]> = {
	default: [
		{ title: 'Set Up Space', desc: 'Configure the physical or virtual venue', detail: 'venue', icon: '1' },
		{ title: 'Communications Hub', desc: 'Create group chat and notification channels', detail: 'comms', icon: '2' },
		{ title: 'Shared Notes & Docs', desc: 'Set up collaborative agenda and resource links', detail: 'notes', icon: '3' },
		{ title: 'Prep from Input', desc: 'Upload transcripts or notes to auto-generate briefs', detail: 'prep', icon: '4' },
		{ title: 'Launch & Coordinate', desc: 'Confirm roles, send notifications, begin execution', detail: 'launch', icon: '5' },
	],
};

const NODE_W = 90, NODE_H = 104;
const TASK_W = 220, TASK_H_BASE = 60, TASK_ROW = 26;
const GAS_TANK_Y = 30, GAS_TANK_H = 12, GAS_TANK_PAD = 12;
const EXEC_BTN_H = 28;
const PORT_R = 5.5;
const HEX_R = 52;

interface Commitment {
	id: string;
	memberName: string;
	hours: number;
	skill: string;
	desc: string;
}
interface TaskData {
	id: string;
	name: string;
	description: string;
	needs: Record<string, number>;
	links: { label: string; url: string }[];
	notes: string;
	fulfilled?: Record<string, number>;
	status?: string;        // rTasks status (TODO, IN_PROGRESS, DONE)
	canvasX?: number;       // canvas position from weaving overlay
	canvasY?: number;
}

interface UnplacedTask {
	id: string;
	title: string;
	status: string;
	description: string;
	priority: string | null;
	labels: string[];
}
interface WeaveNode {
	id: string;
	type: 'commitment' | 'task';
	x: number;
	y: number;
	w: number;
	h: number;
	hexR?: number;
	baseH?: number;
	data: any;
}
interface Wire {
	from: string;
	to: string;
	skill: string;
	hours: number;
	status: 'proposed' | 'committed';
	connectionId?: string;
	connectionType?: 'commitment' | 'dependency';
}

// ── Orb class ──

class Orb {
	c: Commitment;
	baseRadius: number;
	radius: number;
	x: number;
	y: number;
	vx: number;
	vy: number;
	hoverT = 0;
	phase: number;
	opacity = 0;
	color: string;
	statusRing: 'offer' | 'need' | 'matched' | null = null; // intent status ring color

	constructor(c: Commitment, basketCX: number, basketCY: number, basketR: number, x?: number, y?: number) {
		this.c = c;
		this.baseRadius = 18 + c.hours * 9;
		this.radius = this.baseRadius;
		if (x != null && y != null) {
			this.x = x; this.y = y;
		} else {
			const a = Math.random() * Math.PI * 2;
			const r = Math.random() * (basketR - this.baseRadius - 10);
			this.x = basketCX + Math.cos(a) * r;
			this.y = basketCY + Math.sin(a) * r;
		}
		this.vx = (Math.random() - 0.5) * 0.4;
		this.vy = (Math.random() - 0.5) * 0.4;
		this.phase = Math.random() * Math.PI * 2;
		this.color = SKILL_COLORS[c.skill] || '#8b5cf6';
	}

	update(hoveredOrb: Orb | null, basketCX: number, basketCY: number, basketR: number) {
		this.phase += 0.008;
		this.vx += Math.sin(this.phase) * 0.003;
		this.vy += Math.cos(this.phase * 0.73 + 1) * 0.003;
		this.vx *= 0.996;
		this.vy *= 0.996;
		this.x += this.vx;
		this.y += this.vy;

		const dx = this.x - basketCX, dy = this.y - basketCY;
		const dist = Math.sqrt(dx * dx + dy * dy);
		const maxDist = basketR - this.radius - 3;
		if (dist > maxDist && dist > 0.1) {
			const nx = dx / dist, ny = dy / dist;
			this.x = basketCX + nx * maxDist;
			this.y = basketCY + ny * maxDist;
			const dot = this.vx * nx + this.vy * ny;
			this.vx -= 1.3 * dot * nx;
			this.vy -= 1.3 * dot * ny;
			this.vx *= 0.6; this.vy *= 0.6;
		}

		const isH = hoveredOrb === this;
		this.hoverT += ((isH ? 1 : 0) - this.hoverT) * 0.12;
		this.radius = this.baseRadius + this.hoverT * 5;
		if (this.opacity < 1) this.opacity = Math.min(1, this.opacity + 0.025);
	}

	draw(ctx: CanvasRenderingContext2D) {
		if (this.opacity < 0.01) return;
		ctx.save();
		ctx.globalAlpha = this.opacity;
		if (this.hoverT > 0.05) { ctx.shadowColor = this.color; ctx.shadowBlur = 24 * this.hoverT; }

		ctx.beginPath();
		ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
		ctx.fillStyle = this.color + '15';
		ctx.fill();

		ctx.beginPath();
		ctx.arc(this.x, this.y, this.radius * 0.82, 0, Math.PI * 2);
		const g = ctx.createRadialGradient(
			this.x - this.radius * 0.15, this.y - this.radius * 0.15, 0,
			this.x, this.y, this.radius * 0.82
		);
		g.addColorStop(0, this.color + 'dd');
		g.addColorStop(1, this.color);
		ctx.fillStyle = g;
		ctx.fill();
		ctx.shadowBlur = 0;
		ctx.strokeStyle = this.color;
		ctx.lineWidth = 1.5;
		ctx.stroke();

		// Status ring (green=offer, blue=need, gold=matched)
		if (this.statusRing) {
			const ringColor = this.statusRing === 'offer' ? '#10b981'
				: this.statusRing === 'need' ? '#3b82f6' : '#fbbf24';
			ctx.beginPath();
			ctx.arc(this.x, this.y, this.radius + 4, 0, Math.PI * 2);
			ctx.strokeStyle = ringColor;
			ctx.lineWidth = 2.5;
			ctx.setLineDash(this.statusRing === 'matched' ? [] : [6, 4]);
			ctx.stroke();
			ctx.setLineDash([]);
		}

		if (this.hoverT > 0.05) {
			ctx.globalAlpha = this.opacity * 0.08 * this.hoverT;
			ctx.beginPath();
			ctx.ellipse(this.x, this.y + this.radius + 4, this.radius * 0.6, 4, 0, 0, Math.PI * 2);
			ctx.fillStyle = '#000';
			ctx.fill();
			ctx.globalAlpha = this.opacity;
		}

		ctx.fillStyle = '#fff';
		ctx.textAlign = 'center';
		ctx.textBaseline = 'middle';
		const fs = Math.max(10, this.radius * 0.3);
		ctx.font = '600 ' + fs + 'px -apple-system, sans-serif';
		ctx.fillText(this.c.memberName.split(' ')[0], this.x, this.y - fs * 0.35);
		ctx.font = '500 ' + (fs * 0.78) + 'px -apple-system, sans-serif';
		ctx.fillStyle = '#ffffffcc';
		ctx.fillText(this.c.hours + 'hr \u00b7 ' + (SKILL_LABELS[this.c.skill] || this.c.skill), this.x, this.y + fs * 0.55);

		ctx.restore();
	}

	contains(px: number, py: number) {
		const dx = px - this.x, dy = py - this.y;
		return dx * dx + dy * dy < this.radius * this.radius;
	}
}

// ── Ripple class ──

class Ripple {
	x: number; y: number; r = 8; o = 0.5; color: string;
	constructor(x: number, y: number, color: string) { this.x = x; this.y = y; this.color = color; }
	update() { this.r += 1.5; this.o -= 0.008; return this.o > 0; }
	draw(ctx: CanvasRenderingContext2D) {
		ctx.save(); ctx.globalAlpha = this.o;
		ctx.strokeStyle = this.color; ctx.lineWidth = 2;
		ctx.beginPath(); ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2); ctx.stroke();
		ctx.restore();
	}
}

// ── Hex helpers ──

function hexPoints(cx: number, cy: number, r: number): [number, number][] {
	return [0, 1, 2, 3, 4, 5].map(i => {
		const angle = -Math.PI / 2 + i * Math.PI / 3;
		return [cx + r * Math.cos(angle), cy + r * Math.sin(angle)] as [number, number];
	});
}

function hexPolygonStr(cx: number, cy: number, r: number) {
	return hexPoints(cx, cy, r).map(p => p[0] + ',' + p[1]).join(' ');
}

function bezier(x1: number, y1: number, x2: number, y2: number) {
	const d = Math.max(40, Math.abs(x2 - x1) * 0.5);
	return 'M' + x1 + ',' + y1 + ' C' + (x1 + d) + ',' + y1 + ' ' + (x2 - d) + ',' + y2 + ' ' + x2 + ',' + y2;
}

function ns(tag: string) { return document.createElementNS('http://www.w3.org/2000/svg', tag); }

function svgText(txt: string, x: number, y: number, size: number, color: string, weight?: string, anchor?: string) {
	const t = ns('text');
	t.setAttribute('x', String(x)); t.setAttribute('y', String(y));
	t.setAttribute('fill', color);
	t.setAttribute('font-size', String(size));
	t.setAttribute('font-weight', weight || '400');
	t.setAttribute('font-family', '-apple-system, BlinkMacSystemFont, sans-serif');
	if (anchor) t.setAttribute('text-anchor', anchor);
	t.textContent = txt;
	return t;
}

// ── Main component ──

class FolkTimebankApp extends HTMLElement {
	private shadow: ShadowRoot;
	private space = 'demo';
	private currentView: 'canvas' | 'collaborate' | 'dashboard' = 'canvas';
	private _history = new ViewHistory<'canvas' | 'collaborate' | 'dashboard'>('canvas', 'rtime');

	// Pool panel state
	private canvas!: HTMLCanvasElement;
	private ctx!: CanvasRenderingContext2D;
	private orbs: Orb[] = [];
	private ripples: Ripple[] = [];
	private animFrame = 0;
	private poolW = 0;
	private poolH = 0;
	private basketCX = 0;
	private basketCY = 0;
	private basketR = 0;
	private hoveredOrb: Orb | null = null;
	private selectedOrb: Orb | null = null;
	private dpr = 1;
	private poolPointerId: number | null = null;
	private poolPointerStart: { x: number; y: number; cx: number; cy: number } | null = null;
	private poolPanelCollapsed = false;
	private _panelSplitPct = 50;
	private _dividerDragging = false;
	private _dividerDragStartX = 0;
	private _dividerDragStartPct = 0;

	// Pan/zoom state
	private panX = 0;
	private panY = 0;
	private scale = 1;
	private canvasGroup!: SVGGElement;
	private isPanning = false;
	private panStart = { x: 0, y: 0, panX: 0, panY: 0 };
	private spaceHeld = false;
	private intentFramesLayer!: SVGGElement;

	// Orb drag-to-canvas state
	private draggingOrb: Orb | null = null;
	private draggingSkill: string | null = null;
	private orbDragGhost: HTMLElement | null = null;
	private orbDragTimer: ReturnType<typeof setTimeout> | null = null;

	// SVG canvas state
	private svgEl!: SVGSVGElement;
	private nodesLayer!: SVGGElement;
	private connectionsLayer!: SVGGElement;
	private tempConn!: SVGPathElement;
	private weaveNodes: WeaveNode[] = [];
	private connections: Wire[] = [];
	private dragNode: WeaveNode | null = null;
	private dragOff = { x: 0, y: 0 };
	private connecting: { nodeId: string; portType: string; skill: string | null } | null = null;
	private svgActivePointerId: number | null = null;
	private svgDblTapTime = 0;
	private svgDblTapPos: { x: number; y: number } | null = null;
	private sidebarDragData: { type: string; id: string } | null = null;
	private sidebarGhost: HTMLElement | null = null;

	// Mycelial suggestion state (orb dropped on canvas, nearest task found)
	private pendingSuggestion: { fromId: string; toNode: WeaveNode; skill: string; hours: number; commitX: number; commitY: number } | null = null;

	// Data
	private commitments: Commitment[] = [];
	private tasks: TaskData[] = [];
	private unplacedTasks: UnplacedTask[] = [];
	private projectFrames: { id: string; title: string; taskIds: string[]; color?: string; x: number; y: number; w: number; h: number }[] = [];

	// Collaborate state
	private intents: any[] = [];
	private solverResults: any[] = [];
	private skillPrices: Record<string, number> = {};
	private memberIntentStatus: Map<string, 'offer' | 'need' | 'matched'> = new Map();

	// Exec state
	private execStepStates: Record<string, Record<number, string>> = {};
	private _restoredConnections: { id: string; fromCommitmentId: string; toTaskId: string; skill: string; hours: number; status: 'proposed' | 'committed' }[] = [];
	private _currentExecTaskId: string | null = null;
	private _cyclosMembers: { id: string; name: string; balance: number }[] = [];
	private _theme: 'dark' | 'light' = 'dark';
	private _stopPresence: (() => void) | null = null;
	private _offlineUnsub: (() => void) | null = null;
	private _subscribedDocIds: string[] = [];
	private _resizeObserver: ResizeObserver | null = null;

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

	static get observedAttributes() { return ['space', 'view']; }

	attributeChangedCallback(name: string, _old: string, val: string) {
		if (name === 'space') this.space = val;
		if (name === 'view' && (val === 'canvas' || val === 'collaborate' || val === 'dashboard')) this.currentView = val;
	}

	private _onShellTabChange = (e: Event) => {
		const tab = (e as CustomEvent).detail?.tab;
		if (!tab) return;
		if (tab === 'pool') this._switchView('canvas');
		else if (tab === 'fulfillment') this._switchView('dashboard');
		// 'cyclos' tab is a redirect, no client-side handling needed
	};

	private _switchView(view: 'canvas' | 'collaborate' | 'dashboard') {
		if (view === this.currentView) return;
		this._history.push(view);
		this.currentView = view;
		const canvasView = this.shadow.getElementById('canvas-view');
		const weaveView = this.shadow.getElementById('weave-view');
		const collabView = this.shadow.getElementById('collaborate-view');
		const dashView = this.shadow.getElementById('dashboard-view');
		if (canvasView) canvasView.style.display = view === 'canvas' ? 'flex' : 'none';
		if (weaveView) weaveView.style.display = 'none';
		if (collabView) collabView.style.display = view === 'collaborate' ? 'flex' : 'none';
		if (dashView) dashView.style.display = view === 'dashboard' ? 'flex' : 'none';
		if (view === 'canvas') { this.resizePoolCanvas(); this.rebuildSidebar(); this.renderCommitmentsList(); }
		if (view === 'collaborate') this.refreshCollaborate();
		if (view === 'dashboard') this.refreshDashboard();
	}

	connectedCallback() {
		this.space = this.getAttribute('space') || 'demo';
		const rawView = this.getAttribute('view');
		// Map legacy view names
		if (rawView === 'pool' || rawView === 'weave' || rawView === 'canvas') this.currentView = 'canvas';
		else if (rawView === 'collaborate') this.currentView = 'collaborate';
		else if (rawView === 'dashboard') this.currentView = 'dashboard';
		else this.currentView = 'canvas';
		this.dpr = window.devicePixelRatio || 1;
		this._theme = (localStorage.getItem('rtime-theme') as 'dark' | 'light') || 'dark';
		this._panelSplitPct = parseFloat(localStorage.getItem('rtime-split-pct') || '50');
		this.render();
		this.applyTheme();
		this.setupPoolPanel();
		this.setupDivider();
		this.applyPanelSplit();
		this.setupCanvas();
		this.setupCollaborate();
		this.fetchData();
		this._stopPresence = startPresenceHeartbeat(() => ({ module: 'rtime', context: 'Timebank' }));
		if (this.space !== 'demo') this.subscribeOffline();
		document.addEventListener('rapp-tab-change', this._onShellTabChange);
		window.addEventListener('rspace-view-restored', this._onViewRestored as EventListener);
	}

	private async subscribeOffline() {
		const runtime = (window as any).__rspaceOfflineRuntime;
		if (!runtime?.isInitialized) return;
		try {
			const docId = commitmentsDocId(this.space) as DocumentId;
			await runtime.subscribe(docId, commitmentsSchema);
			this._subscribedDocIds.push(docId);
		} catch { /* runtime unavailable */ }
	}

	/** Derive API base from the current pathname — works for both subdomain and path routing. */
	private getApiBase(): string {
		const path = window.location.pathname;
		const match = path.match(/^(\/[^/]+)?\/rtime/);
		return match ? match[0] : '/rtime';
	}

	/** Auth headers from encryptid session token. */
	private authHeaders(): Record<string, string> {
		const token = localStorage.getItem('encryptid-token');
		return token ? { Authorization: `Bearer ${token}` } : {};
	}

	private applyTheme() {
		if (this._theme === 'light') {
			this.setAttribute('data-theme', 'light');
		} else {
			this.removeAttribute('data-theme');
		}
		const btn = this.shadow.getElementById('themeToggle');
		if (btn) btn.textContent = this._theme === 'dark' ? '☀' : '☾';
	}

	disconnectedCallback() {
		this._history.destroy();
		document.removeEventListener('rapp-tab-change', this._onShellTabChange);
		window.removeEventListener('rspace-view-restored', this._onViewRestored as EventListener);
		if (this.animFrame) cancelAnimationFrame(this.animFrame);
		this._stopPresence?.(); this._offlineUnsub?.(); this._offlineUnsub = null;
		const runtime = (window as any).__rspaceOfflineRuntime;
		if (runtime) {
			for (const id of this._subscribedDocIds) runtime.unsubscribe(id);
		}
		this._subscribedDocIds = [];
		this._resizeObserver?.disconnect();
		this._resizeObserver = null;
	}

	private _onViewRestored = (e: CustomEvent) => {
		if (e.detail?.moduleId !== 'rtime') return;
		this._switchView(e.detail.view);
	};

	private async fetchData() {
		const base = this.getApiBase();
		try {
			const [cResp, wResp] = await Promise.all([
				fetch(`${base}/api/commitments`),
				fetch(`${base}/api/weave`),
			]);
			if (cResp.ok) {
				const cData = await cResp.json();
				this.commitments = cData.commitments || [];
			}
			if (wResp.ok) {
				const wData = await wResp.json();
				// Map placed tasks to TaskData format
				this.tasks = (wData.placedTasks || []).map((t: any) => ({
					id: t.id,
					name: t.title,
					description: t.description || '',
					needs: t.needs || {},
					links: t.links || [],
					notes: t.notes || '',
					status: t.status,
					canvasX: t.canvasX,
					canvasY: t.canvasY,
				}));
				// Store unplaced tasks for picker
				this.unplacedTasks = wData.unplacedTasks || [];
				// Restore connections
				this._restoredConnections = (wData.connections || []).map((cn: any) => ({
					id: cn.id,
					fromCommitmentId: cn.fromCommitmentId,
					toTaskId: cn.toTaskId,
					skill: cn.skill,
					hours: cn.hours || 0,
					status: cn.status || 'proposed',
				}));
				// Restore exec states
				for (const es of (wData.execStates || [])) {
					if (es.taskId && es.steps) {
						this.execStepStates[es.taskId] = {};
						for (const [k, v] of Object.entries(es.steps)) {
							this.execStepStates[es.taskId][Number(k)] = v as string;
						}
					}
				}
			}
		} catch {
			// Offline — use empty state
		}

		// Fetch Cyclos members (silent failure → demo mode)
		try {
			const mResp = await fetch(`${base}/api/cyclos/members`);
			if (mResp.ok) {
				const mData = await mResp.json();
				this._cyclosMembers = (mData.members || []).map((m: any) => ({ id: m.id, name: m.name, balance: m.balance || 0 }));
			}
		} catch { /* Cyclos not configured — demo mode */ }

		this.buildOrbs();
		this.updateStats();
		this.renderCommitmentsList();
		this.rebuildSidebar();
		this.applyRestoredConnections();

		// Place all tasks using stored canvas positions
		if (this.weaveNodes.length === 0 && this.tasks.length > 0) {
			for (const t of this.tasks) {
				const x = t.canvasX ?? 400;
				const y = t.canvasY ?? 150;
				this.weaveNodes.push(this.mkTaskNode(t, x, y));
			}
			this.renderAll();
			this.rebuildSidebar();
		}
	}

	private render() {
		this.shadow.innerHTML = `
<style>${CSS_TEXT}</style>
<div class="main">
	<div id="canvas-view">
		<!-- Left: commitment entry form -->
		<div class="form-panel" id="formPanel">
			<div class="form-panel-header">
				<span>Pledge Time</span>
				<button class="theme-toggle" id="themeToggle" title="Toggle light/dark theme">\u2600</button>
			</div>
			<div class="inline-form" id="inlineForm">
				<div class="inline-form-field">
					<label>Your Name</label>
					<input type="text" id="inlineName" placeholder="e.g. Dana Lee">
				</div>
				<div class="inline-form-field">
					<label>Skill Category</label>
					<select id="inlineSkill">
						<option value="facilitation">Facilitation</option>
						<option value="design">Design</option>
						<option value="tech">Tech</option>
						<option value="outreach">Outreach</option>
						<option value="logistics">Logistics</option>
					</select>
				</div>
				<div class="inline-form-row">
					<div class="inline-form-field" style="flex:1">
						<label>Hours</label>
						<input type="number" id="inlineHours" min="1" max="10" value="2">
					</div>
				</div>
				<div class="inline-form-field">
					<label>Description</label>
					<input type="text" id="inlineDesc" placeholder="What will you contribute?">
				</div>
				<button class="pledge-btn" id="pledgeBtn">Pledge to Pool</button>
			</div>
			<div class="commitments-list-header">
				<span>Commitments</span>
				<span class="commitments-count" id="commitmentsCount">0</span>
			</div>
			<div class="commitments-list" id="commitmentsList"></div>
		</div>
		<!-- Resizable divider -->
		<div class="panel-divider" id="panelDivider"><div class="divider-pip"></div></div>
		<!-- Right: pool visualization -->
		<div class="pool-panel" id="poolPanel">
			<div class="pool-panel-header">
				<span>Commitment Pool</span>
				<button id="poolPanelToggle" title="Toggle weaving view" class="weave-toggle-btn">\u2694 Weave</button>
			</div>
			<canvas id="pool-canvas"></canvas>
			<div class="pool-legend" id="poolLegend">
				<div class="pool-legend-stats">
					<span><strong id="statHours">0</strong> hours</span>
					<span class="pool-legend-sep">\u00b7</span>
					<span><strong id="statContributors">0</strong> contributors</span>
				</div>
				<div class="pool-legend-skills" id="skillLegend"></div>
			</div>
			<div class="pool-detail" id="poolDetail">
				<div class="pool-detail-header">
					<div class="pool-detail-dot" id="detailDot"></div>
					<div class="pool-detail-name" id="detailName"></div>
				</div>
				<div class="pool-detail-skill" id="detailSkill"></div>
				<div class="pool-detail-hours" id="detailHours"></div>
				<div class="pool-detail-desc" id="detailDesc"></div>
				<div class="pool-detail-woven" id="detailWoven"></div>
			</div>
		</div>
	</div>
	<!-- Weaving view (hidden by default, toggled via button) -->
	<div id="weave-view" style="display:none">
		<div class="weave-view-header">
			<button class="weave-back-btn" id="weaveBackBtn">\u2190 Back to Pool</button>
			<span>Commitment Weaving</span>
			<span class="canvas-wrap-hint">connect commitments to projects</span>
		</div>
		<div class="canvas-wrap" id="canvasWrap">
			<svg id="weave-svg" xmlns="http://www.w3.org/2000/svg">
				<defs>
					<pattern id="grid" width="20" height="20" patternUnits="userSpaceOnUse">
						<circle cx="1" cy="1" r="0.5" fill="#64748b" opacity="0.4"/>
					</pattern>
					<filter id="nodeShadow" x="-10%" y="-10%" width="130%" height="140%">
						<feDropShadow dx="0" dy="2" stdDeviation="3" flood-opacity="0.08"/>
					</filter>
					<filter id="glowGreen" x="-20%" y="-20%" width="140%" height="140%">
						<feDropShadow dx="0" dy="0" stdDeviation="6" flood-color="#10b981" flood-opacity="0.35"/>
					</filter>
					<filter id="glowGold" x="-20%" y="-20%" width="140%" height="140%">
						<feDropShadow dx="0" dy="0" stdDeviation="6" flood-color="#fbbf24" flood-opacity="0.4"/>
					</filter>
					<marker id="dep-arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
						<path d="M 0 0 L 10 5 L 0 10 z" fill="#64748b"/>
					</marker>
				</defs>
				<rect width="100%" height="100%" fill="url(#grid)"/>
				<g id="canvas-content" transform="translate(0 0) scale(1)">
					<g id="intentFramesLayer"></g>
					<g id="connectionsLayer"></g>
					<g id="nodesLayer"></g>
				</g>
				<path id="tempConnection" class="temp-connection" d="" style="display:none"/>
			</svg>
			<!-- Zoom controls overlay -->
			<div class="zoom-controls">
				<button id="zoomIn" title="Zoom in">+</button>
				<button id="zoomOut" title="Zoom out">\u2212</button>
				<button id="zoomReset" title="Reset zoom">\u2299</button>
				<button id="addFrame" title="Add project frame">\u25A1</button>
			</div>
		</div>
	</div>
	<div id="collaborate-view" style="display:none">
		<div class="collab-panel">
			<div class="collab-section">
				<div class="collab-section-header">
					<h3>Active Intents</h3>
					<button class="collab-create-btn" id="createIntentBtn">+ New Intent</button>
				</div>
				<div class="collab-intents" id="collabIntents">
					<div class="collab-empty">No active intents yet. Create an offer or need to get started.</div>
				</div>
			</div>
			<div class="collab-section">
				<div class="collab-section-header">
					<h3>Recommended Collaborations</h3>
					<button class="collab-solver-btn" id="runSolverBtn">Run Solver</button>
				</div>
				<div class="collab-results" id="collabResults">
					<div class="collab-empty">No solver results yet. Create intents and run the solver to find matches.</div>
				</div>
			</div>
			<div class="collab-section collab-prices-section">
				<h3>Skill Market Prices</h3>
				<div class="collab-prices" id="collabPrices"></div>
			</div>
		</div>
	</div>
	<div id="dashboard-view" style="display:none">
		<div class="collab-panel">
			<div class="collab-section">
				<div class="collab-section-header">
					<h3>Fulfillment Dashboard</h3>
				</div>
				<div class="dashboard-summary" id="dashboardSummary">
					<div class="collab-empty">Loading fulfillment data...</div>
				</div>
			</div>
			<div class="collab-section">
				<div class="collab-section-header">
					<h3>Time Logs</h3>
				</div>
				<div class="dashboard-logs" id="dashboardLogs"></div>
			</div>
			<div class="collab-section">
				<div class="collab-section-header">
					<h3>Skill Totals</h3>
				</div>
				<div class="dashboard-skills" id="dashboardSkills"></div>
			</div>
		</div>
	</div>
</div>

<!-- Create Intent Modal -->
<div class="modal-overlay" id="intentModalOverlay">
	<div class="modal">
		<h3>Create Intent</h3>
		<div class="modal-field">
			<label>Type</label>
			<div class="intent-type-toggle" id="intentTypeToggle">
				<button class="intent-type-btn active" data-type="offer">Offer</button>
				<button class="intent-type-btn" data-type="need">Need</button>
			</div>
		</div>
		<div class="modal-field">
			<label>Skill</label>
			<select id="intentSkill">
				<option value="facilitation">Facilitation</option>
				<option value="design">Design</option>
				<option value="tech">Tech</option>
				<option value="outreach">Outreach</option>
				<option value="logistics">Logistics</option>
			</select>
		</div>
		<div class="modal-field">
			<label>Hours</label>
			<input type="number" id="intentHours" min="1" max="100" value="2">
		</div>
		<div class="modal-field">
			<label>Description</label>
			<input type="text" id="intentDesc" placeholder="What will you offer or need?">
		</div>
		<div class="intent-cost" id="intentCost" style="display:none">
			<span>Escrow cost: </span><strong id="intentCostValue">0</strong> tokens
		</div>
		<div class="modal-actions">
			<button class="modal-cancel" id="intentCancel">Cancel</button>
			<button class="modal-submit" id="intentSubmit">Create Intent</button>
		</div>
	</div>
</div>

<!-- Add Commitment Modal -->
<div class="modal-overlay" id="modalOverlay">
	<div class="modal">
		<h3>Add Commitment</h3>
		<div class="modal-field">
			<label>Your Name</label>
			<input type="text" id="modalName" placeholder="e.g. Dana Lee">
		</div>
		<div class="modal-field">
			<label>Skill Category</label>
			<select id="modalSkill">
				<option value="facilitation">Facilitation</option>
				<option value="design">Design</option>
				<option value="tech">Tech</option>
				<option value="outreach">Outreach</option>
				<option value="logistics">Logistics</option>
			</select>
		</div>
		<div class="modal-field">
			<label>Hours</label>
			<input type="number" id="modalHours" min="1" max="10" value="2">
		</div>
		<div class="modal-field">
			<label>Description</label>
			<input type="text" id="modalDesc" placeholder="What will you contribute?">
		</div>
		<div class="modal-actions">
			<button class="modal-cancel" id="modalCancel">Cancel</button>
			<button class="modal-submit" id="modalSubmit">Pledge to Pool</button>
		</div>
	</div>
</div>

<!-- Execution Panel -->
<div class="exec-overlay" id="execOverlay">
	<div class="exec-panel">
		<div class="exec-panel-header">
			<h2 id="execTitle">Project Execution</h2>
			<p id="execSubtitle">All commitments woven — prepare for launch</p>
		</div>
		<div class="exec-steps" id="execSteps"></div>
		<div class="exec-panel-footer">
			<button class="exec-close" id="execClose">Close</button>
			<div class="exec-progress">
				<div class="exec-progress-bar"><div class="exec-progress-fill" id="execProgressFill"></div></div>
				<span id="execProgressText">0/5</span>
			</div>
			<button class="exec-launch" id="execLaunch" disabled>Launch Project</button>
		</div>
	</div>
</div>

<!-- Task Editor -->
<div class="task-edit-overlay" id="taskEditOverlay">
	<div class="task-edit-panel">
		<div class="task-edit-header"><h3 id="taskEditTitle">Edit Task</h3></div>
		<div class="task-edit-body">
			<div class="task-edit-field"><label>Task Name</label><input type="text" id="taskEditName"></div>
			<div class="task-edit-field"><label>Description</label><textarea id="taskEditDesc"></textarea></div>
			<div class="task-edit-field"><label>Notes</label><textarea id="taskEditNotes"></textarea></div>
			<div class="task-edit-field"><label>Links</label>
				<div id="taskEditLinksContainer"></div>
				<button class="task-edit-add-link" id="taskEditAddLink">+ Add Link</button>
			</div>
		</div>
		<div class="task-edit-footer">
			<button class="modal-cancel" id="taskEditCancel">Cancel</button>
			<button class="modal-submit" id="taskEditSave">Save</button>
		</div>
	</div>
</div>
`;
	}

	// ── Pool panel setup ──

	private setupPoolPanel() {
		this.canvas = this.shadow.getElementById('pool-canvas') as HTMLCanvasElement;
		this.ctx = this.canvas.getContext('2d')!;

		// Theme toggle
		this.shadow.getElementById('themeToggle')!.addEventListener('click', () => {
			this._theme = this._theme === 'dark' ? 'light' : 'dark';
			localStorage.setItem('rtime-theme', this._theme);
			this.applyTheme();
		});

		// Weave toggle — switches to weaving SVG view
		this.shadow.getElementById('poolPanelToggle')!.addEventListener('click', () => {
			this.shadow.getElementById('canvas-view')!.style.display = 'none';
			this.shadow.getElementById('weave-view')!.style.display = 'flex';
		});
		this.shadow.getElementById('weaveBackBtn')!.addEventListener('click', () => {
			this.shadow.getElementById('weave-view')!.style.display = 'none';
			this.shadow.getElementById('canvas-view')!.style.display = 'flex';
			this.resizePoolCanvas();
		});

		// Inline commitment form
		this.shadow.getElementById('pledgeBtn')!.addEventListener('click', () => this.submitCommitment());

		// Pool pointer events — including long-press drag-to-canvas
		this.canvas.addEventListener('pointerdown', (e) => this.onPoolPointerDown(e));
		this.canvas.addEventListener('pointermove', (e) => this.onPoolPointerMove(e));
		this.canvas.addEventListener('pointerup', (e) => this.onPoolPointerUp(e));
		this.canvas.addEventListener('pointercancel', () => {
			this.poolPointerId = null; this.poolPointerStart = null;
			this.cancelOrbDrag();
		});
		this.canvas.addEventListener('pointerleave', (e) => {
			if (e.pointerType === 'mouse' && !this.draggingOrb) { this.hoveredOrb = null; this.hideDetail(); }
		});

		// Exec panel
		this.shadow.getElementById('execClose')!.addEventListener('click', () => this.shadow.getElementById('execOverlay')!.classList.remove('visible'));
		this.shadow.getElementById('execOverlay')!.addEventListener('click', (e) => {
			if (e.target === this.shadow.getElementById('execOverlay')) this.shadow.getElementById('execOverlay')!.classList.remove('visible');
		});
		this.shadow.getElementById('execLaunch')!.addEventListener('click', () => this.handleExecLaunch());

		// Task editor
		this.shadow.getElementById('taskEditCancel')!.addEventListener('click', () => this.shadow.getElementById('taskEditOverlay')!.classList.remove('visible'));
		this.shadow.getElementById('taskEditOverlay')!.addEventListener('click', (e) => {
			if (e.target === this.shadow.getElementById('taskEditOverlay')) this.shadow.getElementById('taskEditOverlay')!.classList.remove('visible');
		});
		this.shadow.getElementById('taskEditSave')!.addEventListener('click', () => this.saveTaskEdit());
		this.shadow.getElementById('taskEditAddLink')!.addEventListener('click', () => {
			const current = this.getTaskEditLinks();
			current.push({ label: '', url: '' });
			this.renderTaskEditLinks(current);
		});

		// Resize — observe host + pool panel so canvas redraws on any size change
		this._resizeObserver = new ResizeObserver(() => { if (this.currentView === 'canvas') requestAnimationFrame(() => this.resizePoolCanvas()); });
		this._resizeObserver.observe(this);
		const poolPanel = this.shadow.getElementById('poolPanel');
		if (poolPanel) this._resizeObserver.observe(poolPanel);

		this.resizePoolCanvas();
		this.poolFrame();
	}

	private resizePoolCanvas() {
		if (this.poolPanelCollapsed) return;
		const canvasEl = this.canvas;
		if (!canvasEl || !canvasEl.parentElement) return;
		// Clear any previously set inline dimensions so the canvas can flex
		canvasEl.style.width = '';
		canvasEl.style.height = '';
		const rect = canvasEl.getBoundingClientRect();
		this.poolW = rect.width;
		this.poolH = rect.height;
		if (this.poolW < 10 || this.poolH < 10) return;
		canvasEl.width = this.poolW * this.dpr;
		canvasEl.height = this.poolH * this.dpr;
		this.ctx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);
		this.basketCX = this.poolW / 2;
		this.basketCY = this.poolH / 2 + 10;
		this.basketR = Math.min(this.poolW, this.poolH) * 0.42;
	}

	private setupDivider() {
		const divider = this.shadow.getElementById('panelDivider');
		if (!divider) return;

		divider.addEventListener('pointerdown', (e: PointerEvent) => {
			e.preventDefault();
			this._dividerDragging = true;
			this._dividerDragStartX = e.clientX;
			this._dividerDragStartPct = this._panelSplitPct;
			divider.setPointerCapture(e.pointerId);
			divider.classList.add('active');
		});

		divider.addEventListener('pointermove', (e: PointerEvent) => {
			if (!this._dividerDragging) return;
			const view = this.shadow.getElementById('canvas-view');
			if (!view) return;
			const viewW = view.getBoundingClientRect().width;
			if (viewW < 10) return;
			const dx = e.clientX - this._dividerDragStartX;
			const deltaPct = (dx / viewW) * 100;
			this._panelSplitPct = Math.max(20, Math.min(65, this._dividerDragStartPct + deltaPct));
			this.applyPanelSplit();
		});

		divider.addEventListener('pointerup', (e: PointerEvent) => {
			if (!this._dividerDragging) return;
			this._dividerDragging = false;
			divider.releasePointerCapture(e.pointerId);
			divider.classList.remove('active');
			localStorage.setItem('rtime-split-pct', String(this._panelSplitPct));
		});

		divider.addEventListener('pointercancel', () => {
			this._dividerDragging = false;
			divider.classList.remove('active');
		});

		// Detail "Drag to Weave" button
		const dragBtn = this.shadow.getElementById('detailDragBtn');
		if (dragBtn) {
			dragBtn.addEventListener('pointerdown', (e: PointerEvent) => {
				e.preventDefault();
				e.stopPropagation();
				if (this.selectedOrb) {
					this.hideDetail();
					this.startOrbDrag(this.selectedOrb, e.clientX, e.clientY);
				}
			});
		}

		// Add Frame button
		const addFrameBtn = this.shadow.getElementById('addFrame');
		if (addFrameBtn) {
			addFrameBtn.addEventListener('click', () => this.addProjectFrame());
		}
	}

	private applyPanelSplit() {
		const panel = this.shadow.getElementById('formPanel');
		const divider = this.shadow.getElementById('panelDivider');
		if (!panel) return;
		panel.style.width = this._panelSplitPct + '%';
		if (divider) divider.style.display = '';
		this.resizePoolCanvas();
	}

	private availableHours(commitmentId: string): number {
		const commitment = this.commitments.find(c => c.id === commitmentId);
		if (!commitment) return 0;
		const used = this.connections
			.filter(w => w.from === 'cn-' + commitmentId)
			.reduce((sum, w) => sum + (w.hours || 0), 0);
		return commitment.hours - used;
	}

	private buildOrbs() {
		this.orbs = this.commitments
			.filter(c => {
				const avail = this.availableHours(c.id);
				return avail > 0;
			})
			.map(c => {
				const avail = this.availableHours(c.id);
				// Create a display commitment with available hours for orb sizing
				const displayC = avail < c.hours ? { ...c, hours: avail } : c;
				const orb = new Orb(displayC, this.basketCX, this.basketCY, this.basketR);
				// Keep original commitment ref for drag operations
				(orb as any)._originalCommitment = c;
				orb.statusRing = this.memberIntentStatus.get(c.memberName) || null;
				return orb;
			});
	}

	private resolveCollisions() {
		for (let i = 0; i < this.orbs.length; i++) {
			for (let j = i + 1; j < this.orbs.length; j++) {
				const a = this.orbs[i], b = this.orbs[j];
				const dx = b.x - a.x, dy = b.y - a.y;
				const dist = Math.sqrt(dx * dx + dy * dy);
				const min = a.radius + b.radius + 4;
				if (dist < min && dist > 0.1) {
					const nx = dx / dist, ny = dy / dist;
					const ov = (min - dist) * 0.5;
					a.x -= nx * ov * 0.5; a.y -= ny * ov * 0.5;
					b.x += nx * ov * 0.5; b.y += ny * ov * 0.5;
					a.vx -= nx * 0.04; a.vy -= ny * 0.04;
					b.vx += nx * 0.04; b.vy += ny * 0.04;
				}
			}
		}
	}

	private drawBasket() {
		const ctx = this.ctx;
		const ig = ctx.createRadialGradient(this.basketCX, this.basketCY - this.basketR * 0.2, 0, this.basketCX, this.basketCY, this.basketR);
		ig.addColorStop(0, 'rgba(139,92,246,0.06)');
		ig.addColorStop(0.7, 'rgba(139,92,246,0.1)');
		ig.addColorStop(1, 'rgba(236,72,153,0.12)');
		ctx.beginPath();
		ctx.arc(this.basketCX, this.basketCY, this.basketR, 0, Math.PI * 2);
		ctx.fillStyle = ig;
		ctx.fill();

		ctx.beginPath();
		ctx.arc(this.basketCX, this.basketCY, this.basketR, 0, Math.PI * 2);
		ctx.strokeStyle = 'rgba(139,92,246,0.25)';
		ctx.lineWidth = 6;
		ctx.stroke();
		ctx.beginPath();
		ctx.arc(this.basketCX, this.basketCY, this.basketR, 0, Math.PI * 2);
		ctx.strokeStyle = 'rgba(139,92,246,0.4)';
		ctx.lineWidth = 2;
		ctx.stroke();

		// Woven texture
		ctx.strokeStyle = 'rgba(139,92,246,0.2)';
		ctx.lineWidth = 1.5;
		const segs = 48;
		for (let i = 0; i < segs; i++) {
			const a1 = (i / segs) * Math.PI * 2;
			const a2 = ((i + 0.5) / segs) * Math.PI * 2;
			const r1 = this.basketR - 3, r2 = this.basketR + 3;
			ctx.beginPath();
			ctx.moveTo(this.basketCX + Math.cos(a1) * r1, this.basketCY + Math.sin(a1) * r1);
			ctx.quadraticCurveTo(
				this.basketCX + Math.cos((a1 + a2) / 2) * r2,
				this.basketCY + Math.sin((a1 + a2) / 2) * r2,
				this.basketCX + Math.cos(a2) * r1,
				this.basketCY + Math.sin(a2) * r1
			);
			ctx.stroke();
		}

		ctx.fillStyle = '#a78bfacc';
		ctx.font = '600 13px -apple-system, sans-serif';
		ctx.textAlign = 'center';
		ctx.textBaseline = 'bottom';
		ctx.fillText('COMMITMENT POOL', this.basketCX, this.basketCY - this.basketR - 20);
		ctx.font = '400 10px -apple-system, sans-serif';
		ctx.fillStyle = '#a78bfa88';
		ctx.fillText('pledge your time here', this.basketCX, this.basketCY - this.basketR - 6);
	}

	private poolFrame = () => {
		this.ctx.clearRect(0, 0, this.poolW, this.poolH);
		const bg = this.ctx.createLinearGradient(0, 0, 0, this.poolH);
		if (this._theme === 'light') {
			bg.addColorStop(0, '#f8f7ff'); bg.addColorStop(1, '#fdf2f8');
		} else {
			bg.addColorStop(0, '#0f172a'); bg.addColorStop(1, '#1a1033');
		}
		this.ctx.fillStyle = bg; this.ctx.fillRect(0, 0, this.poolW, this.poolH);

		this.drawBasket();
		this.ripples = this.ripples.filter(r => r.update());
		this.ripples.forEach(r => r.draw(this.ctx));
		this.orbs.forEach(o => o.update(this.hoveredOrb, this.basketCX, this.basketCY, this.basketR));
		this.resolveCollisions();
		this.orbs.forEach(o => o.draw(this.ctx));
		this.animFrame = requestAnimationFrame(this.poolFrame);
	};

	// Pool pointer handlers

	private onPoolPointerDown(e: PointerEvent) {
		if (this.poolPointerId !== null) return;
		this.poolPointerId = e.pointerId;
		this.canvas.setPointerCapture(e.pointerId);
		const r = this.canvas.getBoundingClientRect();
		const mx = e.clientX - r.left, my = e.clientY - r.top;
		this.poolPointerStart = { x: e.clientX, y: e.clientY, cx: mx, cy: my };

		// Check if we're on an orb — start long-press timer for drag-to-canvas
		let hitOrb: Orb | null = null;
		for (let i = this.orbs.length - 1; i >= 0; i--) {
			if (this.orbs[i].contains(mx, my)) { hitOrb = this.orbs[i]; break; }
		}
		if (hitOrb) {
			const orb = hitOrb;
			const startX = e.clientX, startY = e.clientY;
			this.orbDragTimer = setTimeout(() => {
				this.orbDragTimer = null;
				this.startOrbDrag(orb, startX, startY);
			}, 150);
		}
	}

	private onPoolPointerMove(e: PointerEvent) {
		// If we're in the middle of an orb drag-to-canvas, the document-level handlers take over
		if (this.draggingOrb) return;

		const r = this.canvas.getBoundingClientRect();
		const mx = e.clientX - r.left, my = e.clientY - r.top;
		this.hoveredOrb = null;
		for (let i = this.orbs.length - 1; i >= 0; i--) {
			if (this.orbs[i].contains(mx, my)) { this.hoveredOrb = this.orbs[i]; break; }
		}
		this.canvas.style.cursor = this.hoveredOrb ? 'pointer' : 'default';
		if (this.poolPointerStart) {
			const dx = e.clientX - this.poolPointerStart.x, dy = e.clientY - this.poolPointerStart.y;
			if (dx * dx + dy * dy > 100) {
				this.poolPointerStart = null;
				// Cancel long-press timer if we moved too far
				if (this.orbDragTimer) { clearTimeout(this.orbDragTimer); this.orbDragTimer = null; }
			}
		}
	}

	private onPoolPointerUp(e: PointerEvent) {
		if (e.pointerId !== this.poolPointerId) return;
		this.poolPointerId = null;
		// Cancel long-press timer if it hasn't fired
		if (this.orbDragTimer) { clearTimeout(this.orbDragTimer); this.orbDragTimer = null; }
		if (this.draggingOrb) return; // handled by document-level handlers
		if (!this.poolPointerStart) return;
		const r = this.canvas.getBoundingClientRect();
		const px = e.clientX - r.left, py = e.clientY - r.top;
		let clicked: Orb | null = null;
		for (let i = this.orbs.length - 1; i >= 0; i--) {
			if (this.orbs[i].contains(px, py)) { clicked = this.orbs[i]; break; }
		}
		if (clicked) {
			this.selectedOrb = this.selectedOrb === clicked ? null : clicked;
			if (this.selectedOrb) this.showDetail(this.selectedOrb, e.clientX, e.clientY); else this.hideDetail();
		} else { this.selectedOrb = null; this.hideDetail(); }
		this.poolPointerStart = null;
	}

	private showDetail(orb: Orb, cx: number, cy: number) {
		const el = this.shadow.getElementById('poolDetail')!;
		const origC = (orb as any)._originalCommitment || orb.c;
		const c = orb.c;
		(this.shadow.getElementById('detailDot') as HTMLElement).style.background = SKILL_COLORS[c.skill] || '#8b5cf6';
		this.shadow.getElementById('detailName')!.textContent = c.memberName;
		this.shadow.getElementById('detailSkill')!.textContent = SKILL_LABELS[c.skill] || c.skill;
		this.shadow.getElementById('detailHours')!.textContent = c.hours + ' hour' + (c.hours !== 1 ? 's' : '') + ' pledged';
		this.shadow.getElementById('detailDesc')!.textContent = c.desc;

		// Woven percentage badge
		const wovenEl = this.shadow.getElementById('detailWoven');
		if (wovenEl) {
			const totalHrs = origC.hours;
			const usedHrs = this.connections
				.filter(w => w.from === 'cn-' + origC.id)
				.reduce((sum, w) => sum + (w.hours || 0), 0);
			const pct = totalHrs > 0 ? Math.round((usedHrs / totalHrs) * 100) : 0;
			if (usedHrs > 0) {
				wovenEl.textContent = pct + '% woven (' + usedHrs + '/' + totalHrs + 'h)';
				wovenEl.style.display = '';
			} else {
				wovenEl.style.display = 'none';
			}
		}

		const panel = this.shadow.getElementById('poolPanel')!.getBoundingClientRect();
		let left = cx - panel.left + 10, top = cy - panel.top - 20;
		if (left + 210 > panel.width) left = cx - panel.left - 220;
		if (top + 120 > panel.height) top = panel.height - 130;
		if (top < 10) top = 10;
		el.style.left = left + 'px'; el.style.top = top + 'px';
		el.classList.add('visible');
	}

	private hideDetail() { this.shadow.getElementById('poolDetail')?.classList.remove('visible'); }

	// ── Submit commitment ──

	private async submitCommitment() {
		const name = (this.shadow.getElementById('inlineName') as HTMLInputElement).value.trim();
		const skill = (this.shadow.getElementById('inlineSkill') as HTMLSelectElement).value;
		const hours = Math.max(1, Math.min(10, parseInt((this.shadow.getElementById('inlineHours') as HTMLInputElement).value) || 2));
		const desc = (this.shadow.getElementById('inlineDesc') as HTMLInputElement).value.trim() || (SKILL_LABELS[skill] || skill) + ' contribution';
		if (!name) { (this.shadow.getElementById('inlineName') as HTMLInputElement).focus(); return; }

		const c: Commitment = { id: 'local-' + Date.now(), memberName: name, skill, hours, desc };

		// Try server-side persist
		try {
			const resp = await fetch(`${this.getApiBase()}/api/commitments`, {
				method: 'POST',
				headers: { 'Content-Type': 'application/json' },
				body: JSON.stringify({ memberName: name, skill, hours, desc }),
			});
			if (resp.ok) {
				const saved = await resp.json();
				c.id = saved.id;
			}
		} catch { /* offline — keep local id */ }

		this.commitments.push(c);
		const orb = new Orb(c, this.basketCX, this.basketCY, this.basketR,
			this.basketCX + (Math.random() - 0.5) * 40, this.basketCY - this.basketR - 30);
		orb.vy = 2.5;
		this.orbs.push(orb);
		setTimeout(() => {
			this.ripples.push(new Ripple(this.basketCX, this.basketCY, SKILL_COLORS[skill] || '#8b5cf6'));
			this.ripples.push(new Ripple(this.basketCX - 15, this.basketCY + 10, SKILL_COLORS[skill] || '#8b5cf6'));
		}, 400);
		this.updateStats();
		this.renderCommitmentsList();

		// Clear form fields
		(this.shadow.getElementById('inlineDesc') as HTMLInputElement).value = '';
		(this.shadow.getElementById('inlineHours') as HTMLInputElement).value = '2';
	}

	private renderCommitmentsList() {
		const list = this.shadow.getElementById('commitmentsList');
		const countEl = this.shadow.getElementById('commitmentsCount');
		if (!list) return;
		if (countEl) countEl.textContent = String(this.commitments.length);
		list.innerHTML = '';
		for (const c of [...this.commitments].reverse()) {
			const color = SKILL_COLORS[c.skill] || '#8b5cf6';
			const avail = this.availableHours(c.id);
			const wovenLabel = avail < c.hours ? ` <span class="cl-woven">${c.hours - avail}h woven</span>` : '';
			const item = document.createElement('div');
			item.className = 'cl-item';
			item.innerHTML = `<div class="cl-dot" style="background:${color}"></div>
				<div class="cl-info">
					<div class="cl-name">${c.memberName}</div>
					<div class="cl-meta">${SKILL_LABELS[c.skill] || c.skill} \u00b7 ${c.hours}h${wovenLabel}</div>
					<div class="cl-desc">${c.desc}</div>
				</div>`;
			list.appendChild(item);
		}
	}

	// ── Stats bar ──

	private updateStats() {
		const total = this.commitments.reduce((s, c) => s + c.hours, 0);
		const hoursEl = this.shadow.getElementById('statHours');
		const contribEl = this.shadow.getElementById('statContributors');
		if (hoursEl) hoursEl.textContent = String(total);
		if (contribEl) contribEl.textContent = String(this.commitments.length);
		const bySkill: Record<string, number> = {};
		this.commitments.forEach(c => { bySkill[c.skill] = (bySkill[c.skill] || 0) + c.hours; });
		const legend = this.shadow.getElementById('skillLegend');
		if (!legend) return;
		legend.innerHTML = '';
		if (total === 0) return;
		for (const [skill, hrs] of Object.entries(bySkill)) {
			const item = document.createElement('div');
			item.className = 'skill-legend-item';
			item.innerHTML = '<div class="skill-legend-dot" style="background:' + (SKILL_COLORS[skill] || '#888') + '"></div>' + (SKILL_LABELS[skill] || skill) + ' ' + hrs + 'h';
			legend.appendChild(item);
		}
	}

	// ── Canvas setup (SVG with pan/zoom) ──

	private setupCanvas() {
		this.svgEl = this.shadow.getElementById('weave-svg') as any;
		this.nodesLayer = this.shadow.getElementById('nodesLayer') as any;
		this.connectionsLayer = this.shadow.getElementById('connectionsLayer') as any;
		this.canvasGroup = this.shadow.getElementById('canvas-content') as any;
		this.intentFramesLayer = this.shadow.getElementById('intentFramesLayer') as any;
		this.tempConn = this.shadow.getElementById('tempConnection') as any;

		const wrap = this.shadow.getElementById('canvasWrap')!;

		// SVG pointer events for node interaction
		this.svgEl.addEventListener('pointerdown', (e: PointerEvent) => this.onSvgPointerDown(e));
		this.svgEl.addEventListener('pointermove', (e: PointerEvent) => this.onSvgPointerMove(e));
		this.svgEl.addEventListener('pointerup', (e: PointerEvent) => this.onSvgPointerUp(e));
		this.svgEl.addEventListener('pointercancel', () => {
			this.svgActivePointerId = null; this.connecting = null; this.dragNode = null;
			this.isPanning = false; this.tempConn.style.display = 'none';
		});
		this.svgEl.addEventListener('dblclick', (e) => {
			const ng = (e.target as Element).closest('.node-group') as SVGElement;
			if (!ng) return;
			const taskNode = this.weaveNodes.find(n => n.id === ng.dataset.id && n.type === 'task');
			if (taskNode) this.openTaskEditor(taskNode);
		});

		// Pan/zoom: wheel
		wrap.addEventListener('wheel', (e) => {
			e.preventDefault();
			if (e.ctrlKey || e.metaKey) {
				// Zoom centered on cursor
				const rect = wrap.getBoundingClientRect();
				const mx = e.clientX - rect.left;
				const my = e.clientY - rect.top;
				const delta = -e.deltaY * 0.002;
				const newScale = Math.max(0.1, Math.min(5, this.scale * (1 + delta)));
				const ratio = newScale / this.scale;
				this.panX = mx - ratio * (mx - this.panX);
				this.panY = my - ratio * (my - this.panY);
				this.scale = newScale;
			} else {
				// Regular wheel = pan
				this.panX -= e.deltaX;
				this.panY -= e.deltaY;
			}
			this.updateCanvasTransform();
		}, { passive: false });

		// Space+drag pan
		const keydown = (e: KeyboardEvent) => {
			if (e.code === 'Space' && !this.spaceHeld && this.currentView === 'canvas') {
				this.spaceHeld = true;
				wrap.style.cursor = 'grab';
				e.preventDefault();
			}
		};
		const keyup = (e: KeyboardEvent) => {
			if (e.code === 'Space') {
				this.spaceHeld = false;
				if (!this.isPanning) wrap.style.cursor = '';
			}
		};
		document.addEventListener('keydown', keydown);
		document.addEventListener('keyup', keyup);

		// Zoom controls
		this.shadow.getElementById('zoomIn')!.addEventListener('click', () => {
			const rect = wrap.getBoundingClientRect();
			const cx = rect.width / 2, cy = rect.height / 2;
			const newScale = Math.min(5, this.scale * 1.25);
			const ratio = newScale / this.scale;
			this.panX = cx - ratio * (cx - this.panX);
			this.panY = cy - ratio * (cy - this.panY);
			this.scale = newScale;
			this.updateCanvasTransform();
		});
		this.shadow.getElementById('zoomOut')!.addEventListener('click', () => {
			const rect = wrap.getBoundingClientRect();
			const cx = rect.width / 2, cy = rect.height / 2;
			const newScale = Math.max(0.1, this.scale * 0.8);
			const ratio = newScale / this.scale;
			this.panX = cx - ratio * (cx - this.panX);
			this.panY = cy - ratio * (cy - this.panY);
			this.scale = newScale;
			this.updateCanvasTransform();
		});
		this.shadow.getElementById('zoomReset')!.addEventListener('click', () => {
			this.panX = 0; this.panY = 0; this.scale = 1;
			this.updateCanvasTransform();
		});

		// Touch pinch zoom
		let pinchDist = 0;
		let pinchScale = 1;
		let pinchMid = { x: 0, y: 0 };
		wrap.addEventListener('touchstart', (e) => {
			if (e.touches.length === 2) {
				const dx = e.touches[1].clientX - e.touches[0].clientX;
				const dy = e.touches[1].clientY - e.touches[0].clientY;
				pinchDist = Math.sqrt(dx * dx + dy * dy);
				pinchScale = this.scale;
				const rect = wrap.getBoundingClientRect();
				pinchMid = {
					x: (e.touches[0].clientX + e.touches[1].clientX) / 2 - rect.left,
					y: (e.touches[0].clientY + e.touches[1].clientY) / 2 - rect.top,
				};
			}
		}, { passive: true });
		wrap.addEventListener('touchmove', (e) => {
			if (e.touches.length === 2) {
				e.preventDefault();
				const dx = e.touches[1].clientX - e.touches[0].clientX;
				const dy = e.touches[1].clientY - e.touches[0].clientY;
				const dist = Math.sqrt(dx * dx + dy * dy);
				const newScale = Math.max(0.1, Math.min(5, pinchScale * (dist / pinchDist)));
				const ratio = newScale / this.scale;
				this.panX = pinchMid.x - ratio * (pinchMid.x - this.panX);
				this.panY = pinchMid.y - ratio * (pinchMid.y - this.panY);
				this.scale = newScale;
				this.updateCanvasTransform();
			}
		}, { passive: false });

		// Drop zone for sidebar drags
		wrap.addEventListener('dragover', (e) => { e.preventDefault(); });
	}

	private updateCanvasTransform() {
		this.canvasGroup.setAttribute('transform', `translate(${this.panX} ${this.panY}) scale(${this.scale})`);
	}

	/** Convert screen coordinates to canvas (world) coordinates accounting for pan/zoom. */
	private screenToCanvas(cx: number, cy: number) {
		const rect = this.shadow.getElementById('canvasWrap')!.getBoundingClientRect();
		return {
			x: (cx - rect.left - this.panX) / this.scale,
			y: (cy - rect.top - this.panY) / this.scale,
		};
	}

	private mkCommitNode(c: Commitment, x: number, y: number): WeaveNode {
		return { id: 'cn-' + c.id, type: 'commitment', x, y, w: NODE_W, h: NODE_H, hexR: HEX_R, data: c };
	}

	private mkTaskNode(t: TaskData, x: number, y: number): WeaveNode {
		const n = Object.keys(t.needs).length;
		const baseH = TASK_H_BASE + n * TASK_ROW + 12;
		return { id: t.id, type: 'task', x, y, w: TASK_W, h: baseH, baseH, data: { ...t, fulfilled: {} } };
	}

	private isTaskReady(node: WeaveNode): boolean {
		if (node.type !== 'task') return false;
		const t = node.data;
		for (const skill of Object.keys(t.needs)) {
			if ((t.fulfilled?.[skill] || 0) < t.needs[skill]) return false;
		}
		return true;
	}

	/** Generate a multi-strand woven/braided connection group. */
	private wovenPath(x1: number, y1: number, x2: number, y2: number, skill: string, hours: number, isCommitted: boolean): SVGGElement {
		const g = ns('g') as SVGGElement;
		g.setAttribute('class', 'woven-connection');
		const color = SKILL_COLORS[skill] || '#8b5cf6';
		const strandCount = Math.min(4, Math.ceil(hours / 2));
		const dx = x2 - x1, dy = y2 - y1;
		const dist = Math.sqrt(dx * dx + dy * dy);
		const spread = Math.min(12, dist * 0.04); // perpendicular strand spread

		// Normal perpendicular vector
		const nx = dist > 0 ? -dy / dist : 0;
		const ny = dist > 0 ? dx / dist : 1;

		// Envelope path at low opacity for "cable" feel
		const envOff = spread * (strandCount - 1) * 0.5 + 3;
		const cd = Math.max(40, Math.abs(dx) * 0.5);
		const envPath = ns('path');
		const envD = `M${x1 + nx * envOff},${y1 + ny * envOff} C${x1 + cd + nx * envOff},${y1 + ny * envOff} ${x2 - cd + nx * envOff},${y2 + ny * envOff} ${x2 + nx * envOff},${y2 + ny * envOff}` +
			` L${x2 - nx * envOff},${y2 - ny * envOff}` +
			` C${x2 - cd - nx * envOff},${y2 - ny * envOff} ${x1 + cd - nx * envOff},${y1 - ny * envOff} ${x1 - nx * envOff},${y1 - ny * envOff} Z`;
		envPath.setAttribute('d', envD);
		envPath.setAttribute('fill', color);
		envPath.setAttribute('opacity', '0.05');
		g.appendChild(envPath);

		// Individual strands
		for (let i = 0; i < strandCount; i++) {
			const offset = (i - (strandCount - 1) / 2) * spread;
			const phase = i * Math.PI * 0.6; // phase shift for intertwining
			const amplitude = spread * 0.7;

			// Build cubic bezier with phase-shifted control points
			const sx = x1 + nx * offset;
			const sy = y1 + ny * offset;
			const ex = x2 + nx * offset;
			const ey = y2 + ny * offset;

			const cp1x = x1 + cd + nx * (offset + Math.sin(phase) * amplitude);
			const cp1y = y1 + ny * (offset + Math.sin(phase) * amplitude);
			const cp2x = x2 - cd + nx * (offset + Math.sin(phase + Math.PI) * amplitude);
			const cp2y = y2 + ny * (offset + Math.sin(phase + Math.PI) * amplitude);

			const strand = ns('path');
			strand.setAttribute('d', `M${sx},${sy} C${cp1x},${cp1y} ${cp2x},${cp2y} ${ex},${ey}`);
			strand.setAttribute('fill', 'none');
			strand.setAttribute('stroke', color);
			strand.setAttribute('stroke-width', '1.8');
			strand.setAttribute('opacity', String(i % 2 === 0 ? 0.85 : 0.55));
			if (!isCommitted) strand.setAttribute('stroke-dasharray', '6 4');
			g.appendChild(strand);
		}

		return g;
	}

	private renderConnections() {
		this.connectionsLayer.innerHTML = '';
		this.connections.forEach(conn => {
			const from = this.weaveNodes.find(n => n.id === conn.from);
			const to = this.weaveNodes.find(n => n.id === conn.to);
			if (!from || !to) return;

			const isDep = (conn as any).connectionType === 'dependency';

			let x1: number, y1: number;
			if (isDep) {
				// Dependency: from right center of source task
				x1 = from.x + from.w; y1 = from.y + from.h / 2;
			} else if (from.type === 'commitment') {
				const cx = from.x + from.w / 2, cy = from.y + from.h / 2;
				const pts = hexPoints(cx, cy, from.hexR || HEX_R);
				x1 = pts[1][0]; y1 = pts[1][1];
			} else {
				x1 = from.x + from.w; y1 = from.y + from.h / 2;
			}

			let x2: number, y2: number;
			if (isDep) {
				// Dependency: to left center of target task
				x2 = to.x; y2 = to.y + to.h / 2;
			} else {
				x2 = to.x; y2 = to.y + to.h / 2;
				if (to.type === 'task') {
					const skills = Object.keys(to.data.needs);
					const idx = skills.indexOf(conn.skill);
					if (idx >= 0) y2 = to.y + TASK_H_BASE + idx * TASK_ROW + TASK_ROW / 2;
				}
			}

			const isCommitted = conn.status === 'committed';

			if (isDep) {
				// Dependency arrow: simple gray dashed bezier with arrowhead
				const p = ns('path');
				p.setAttribute('d', bezier(x1, y1, x2, y2));
				p.setAttribute('fill', 'none');
				p.setAttribute('stroke', '#64748b');
				p.setAttribute('stroke-width', '1.5');
				p.setAttribute('stroke-dasharray', '6 3');
				p.setAttribute('marker-end', 'url(#dep-arrow)');
				p.setAttribute('class', 'dep-connection');
				this.connectionsLayer.appendChild(p);
			} else {
				// Commitment: multi-strand woven path
				const wovenG = this.wovenPath(x1, y1, x2, y2, conn.skill, conn.hours, isCommitted);
				if (!isCommitted) {
					const fromNode = this.weaveNodes.find(n => n.id === conn.from);
					const memberName = fromNode?.type === 'commitment' ? (fromNode.data as any).memberName || 'This member' : 'This member';
					const tip = ns('title');
					tip.textContent = memberName + ' has been notified of this proposed commitment, and can approve/deny for 48 hours';
					wovenG.insertBefore(tip, wovenG.firstChild);
				}
				this.connectionsLayer.appendChild(wovenG);
			}

			// Hour label at midpoint
			const mx = (x1 + x2) / 2, my = (y1 + y2) / 2;
			if (!isDep) {
				const label = ns('text') as SVGTextElement;
				label.setAttribute('x', String(mx));
				label.setAttribute('y', String(my - 6));
				label.setAttribute('text-anchor', 'middle');
				label.setAttribute('font-size', '10');
				label.setAttribute('font-weight', '600');
				label.setAttribute('fill', isCommitted ? '#10b981' : '#f59e0b');
				label.textContent = conn.hours + 'h ' + (isCommitted ? '\u2713' : '\u23f3');
				// Tooltip for proposed connections
				if (!isCommitted) {
					const fromNode = this.weaveNodes.find(n => n.id === conn.from);
					const memberName = fromNode?.type === 'commitment' ? (fromNode.data as any).memberName || 'This member' : 'This member';
					const tip = ns('title');
					tip.textContent = memberName + ' has been notified of this proposed commitment, and can approve/deny for 48 hours';
					label.appendChild(tip);
				}
				this.connectionsLayer.appendChild(label);
			}
		});

		// ── Mycelial suggestion preview ──
		if (this.pendingSuggestion) {
			const s = this.pendingSuggestion;
			const fromNode = this.weaveNodes.find(n => n.id === s.fromId);
			if (fromNode) {
				const cx = fromNode.x + fromNode.w / 2, cy = fromNode.y + fromNode.h / 2;
				const pts = hexPoints(cx, cy, fromNode.hexR || HEX_R);
				const x1 = pts[1][0], y1 = pts[1][1];
				const skills = Object.keys(s.toNode.data.needs);
				const idx = skills.indexOf(s.skill);
				const x2 = s.toNode.x;
				const y2 = idx >= 0 ? s.toNode.y + TASK_H_BASE + idx * TASK_ROW + TASK_ROW / 2 : s.toNode.y + s.toNode.h / 2;

				// Pulsing dashed preview wire
				const preview = ns('path');
				preview.setAttribute('d', bezier(x1, y1, x2, y2));
				preview.setAttribute('stroke', '#38bdf8');
				preview.setAttribute('stroke-width', '2.5');
				preview.setAttribute('stroke-dasharray', '8 4');
				preview.setAttribute('fill', 'none');
				preview.setAttribute('opacity', '0.8');
				const anim = ns('animate');
				anim.setAttribute('attributeName', 'stroke-dashoffset');
				anim.setAttribute('values', '0;24');
				anim.setAttribute('dur', '1s');
				anim.setAttribute('repeatCount', 'indefinite');
				preview.appendChild(anim);
				this.connectionsLayer.appendChild(preview);

				// Suggestion label + buttons at midpoint
				const mx = (x1 + x2) / 2, my = (y1 + y2) / 2;
				const sugLabel = svgText('Connect ' + s.hours + 'h ' + s.skill + '?', mx, my - 14, 11, '#38bdf8', '600', 'middle');
				this.connectionsLayer.appendChild(sugLabel);

				// Confirm button
				const confirmBg = ns('rect');
				confirmBg.setAttribute('x', String(mx - 40)); confirmBg.setAttribute('y', String(my - 2));
				confirmBg.setAttribute('width', '36'); confirmBg.setAttribute('height', '20');
				confirmBg.setAttribute('rx', '4'); confirmBg.setAttribute('fill', '#10b981');
				confirmBg.setAttribute('class', 'suggest-confirm-btn');
				confirmBg.style.cursor = 'pointer';
				this.connectionsLayer.appendChild(confirmBg);
				const confirmT = svgText('\u2713 Yes', mx - 22, my + 14, 10, '#fff', '700', 'middle');
				confirmT.style.pointerEvents = 'none';
				this.connectionsLayer.appendChild(confirmT);

				// Dismiss button
				const dismissBg = ns('rect');
				dismissBg.setAttribute('x', String(mx + 4)); dismissBg.setAttribute('y', String(my - 2));
				dismissBg.setAttribute('width', '36'); dismissBg.setAttribute('height', '20');
				dismissBg.setAttribute('rx', '4'); dismissBg.setAttribute('fill', '#ef4444');
				dismissBg.setAttribute('class', 'suggest-dismiss-btn');
				dismissBg.style.cursor = 'pointer';
				this.connectionsLayer.appendChild(dismissBg);
				const dismissT = svgText('\u2717 No', mx + 22, my + 14, 10, '#fff', '700', 'middle');
				dismissT.style.pointerEvents = 'none';
				this.connectionsLayer.appendChild(dismissT);
			}
		}
	}

	private renderNode(node: WeaveNode): SVGGElement {
		const g = ns('g') as SVGGElement;
		g.setAttribute('data-id', node.id);
		g.setAttribute('data-collab-id', `${node.type}:${node.id}`);
		g.setAttribute('transform', 'translate(' + node.x + ',' + node.y + ')');

		if (node.type === 'commitment') {
			const c = node.data as Commitment;
			const col = SKILL_COLORS[c.skill] || '#8b5cf6';
			const cx = node.w / 2, cy = node.h / 2;
			const hr = node.hexR || HEX_R;
			g.setAttribute('class', 'node-group');

			const hex = ns('polygon');
			hex.setAttribute('points', hexPolygonStr(cx, cy, hr));
			hex.setAttribute('fill', '#1e293b');
			hex.setAttribute('stroke', '#334155');
			hex.setAttribute('stroke-width', '1');
			hex.setAttribute('filter', 'url(#nodeShadow)');
			g.appendChild(hex);

			const hexHover = ns('polygon');
			hexHover.setAttribute('points', hexPolygonStr(cx, cy, hr));
			hexHover.setAttribute('fill', 'none');
			hexHover.setAttribute('stroke', col);
			hexHover.setAttribute('stroke-width', '0');
			hexHover.setAttribute('class', 'hex-hover-stroke');
			hexHover.style.pointerEvents = 'none';
			g.appendChild(hexHover);

			const clipId = 'hexClip-' + node.id;
			const clipPath = ns('clipPath');
			clipPath.setAttribute('id', clipId);
			const clipRect = ns('rect');
			clipRect.setAttribute('x', String(cx - hr)); clipRect.setAttribute('y', String(cy - hr));
			clipRect.setAttribute('width', String(hr * 2)); clipRect.setAttribute('height', String(hr * 0.75));
			clipPath.appendChild(clipRect);
			g.appendChild(clipPath);

			const headerFill = ns('polygon');
			headerFill.setAttribute('points', hexPolygonStr(cx, cy, hr));
			headerFill.setAttribute('fill', col);
			headerFill.setAttribute('clip-path', 'url(#' + clipId + ')');
			g.appendChild(headerFill);

			const nameText = c.memberName.length > 12 ? c.memberName.slice(0, 11) + '\u2026' : c.memberName;
			g.appendChild(svgText(nameText, cx, cy - hr * 0.35, 11, '#fff', '600', 'middle'));
			g.appendChild(svgText(c.hours + 'hr', cx, cy + 4, 13, '#f1f5f9', '700', 'middle'));
			g.appendChild(svgText(SKILL_LABELS[c.skill] || c.skill, cx, cy + 18, 10, '#94a3b8', '400', 'middle'));
			if (c.desc) {
				const descText = c.desc.length > 18 ? c.desc.slice(0, 17) + '\u2026' : c.desc;
				g.appendChild(svgText(descText, cx, cy + 32, 9, '#64748b', '400', 'middle'));
			}

			// Invisible hit area on hex right vertex for dragging connections
			const pts = hexPoints(cx, cy, hr);
			const portHit = ns('circle');
			portHit.setAttribute('cx', String(pts[1][0])); portHit.setAttribute('cy', String(pts[1][1]));
			portHit.setAttribute('r', '18'); portHit.setAttribute('fill', 'transparent');
			portHit.setAttribute('class', 'port');
			portHit.setAttribute('data-node', node.id); portHit.setAttribute('data-port', 'output');
			g.appendChild(portHit);

			// Approval buttons for proposed connections (visible to commitment owner)
			const proposedWires = this.connections.filter(w => w.from === node.id && w.status === 'proposed' && w.connectionId);
			if (proposedWires.length > 0) {
				const btnY = cy + hr * 0.55;
				proposedWires.forEach((pw, idx) => {
					const by = btnY + idx * 22;
					// Approve button
					const approveR = ns('rect');
					approveR.setAttribute('x', String(cx - 36)); approveR.setAttribute('y', String(by));
					approveR.setAttribute('width', '32'); approveR.setAttribute('height', '18');
					approveR.setAttribute('rx', '4'); approveR.setAttribute('fill', '#10b981');
					approveR.setAttribute('class', 'approve-btn');
					approveR.setAttribute('data-connection-id', pw.connectionId!);
					approveR.style.cursor = 'pointer';
					g.appendChild(approveR);
					const appT = svgText('\u2713', cx - 20, by + 13, 11, '#fff', '700', 'middle');
					appT.style.pointerEvents = 'none';
					g.appendChild(appT);

					// Decline button
					const declineR = ns('rect');
					declineR.setAttribute('x', String(cx + 4)); declineR.setAttribute('y', String(by));
					declineR.setAttribute('width', '32'); declineR.setAttribute('height', '18');
					declineR.setAttribute('rx', '4'); declineR.setAttribute('fill', '#ef4444');
					declineR.setAttribute('class', 'decline-btn');
					declineR.setAttribute('data-connection-id', pw.connectionId!);
					declineR.style.cursor = 'pointer';
					g.appendChild(declineR);
					const decT = svgText('\u2717', cx + 20, by + 13, 11, '#fff', '700', 'middle');
					decT.style.pointerEvents = 'none';
					g.appendChild(decT);
				});
			}

		} else if (node.type === 'task') {
			const t = node.data as TaskData;
			const skills = Object.keys(t.needs);
			const totalNeeded = Object.values(t.needs).reduce((a, b) => a + b, 0);

			// Compute committed / proposed totals across all skills from live wires
			const taskWires = this.connections.filter(w => w.to === node.id);
			const totalCommitted = taskWires.filter(w => w.status === 'committed').reduce((s, w) => s + w.hours, 0);
			const totalProposed = taskWires.filter(w => w.status === 'proposed').reduce((s, w) => s + w.hours, 0);
			const funded = totalNeeded > 0 && totalCommitted >= totalNeeded;
			const ready = this.isTaskReady(node);

			node.h = node.baseH! + (ready ? EXEC_BTN_H + 8 : 0);
			g.setAttribute('class', 'node-group task-node' + (ready ? ' ready' : ''));

			const rect = ns('rect');
			rect.setAttribute('class', 'node-rect');
			rect.setAttribute('width', String(node.w)); rect.setAttribute('height', String(node.h));
			rect.setAttribute('rx', '8'); rect.setAttribute('ry', '8');
			rect.setAttribute('filter', ready ? 'url(#glowGreen)' : 'url(#nodeShadow)');
			g.appendChild(rect);

			const hCol = ready ? '#10b981' : '#8b5cf6';
			const h = ns('rect');
			h.setAttribute('width', String(node.w)); h.setAttribute('height', '28');
			h.setAttribute('fill', hCol); h.setAttribute('rx', '8'); h.setAttribute('ry', '8');
			g.appendChild(h);
			const hm = ns('rect');
			hm.setAttribute('width', String(node.w)); hm.setAttribute('height', '10'); hm.setAttribute('y', '20'); hm.setAttribute('fill', hCol);
			g.appendChild(hm);

			const nameLabel = t.name.length > 18 ? t.name.slice(0, 17) + '\u2026' : t.name;
			g.appendChild(svgText(nameLabel, 12, 18, 12, '#fff', '600'));
			// rTasks status badge
			if (t.status && t.status !== 'TODO') {
				const badge = t.status === 'DONE' ? '\u2713' : '\u25B6';
				const badgeColor = t.status === 'DONE' ? '#10b981' : '#f59e0b';
				g.appendChild(svgText(badge, node.w - 26, 18, 10, badgeColor, '700', 'middle'));
			}
			const editPencil = svgText('\u270E', node.w - 10, 18, 11, '#ffffff66', '400', 'middle');
			editPencil.style.pointerEvents = 'none';
			g.appendChild(editPencil);
			const pctText = funded ? 'Funded!' : (ready ? 'Ready!' : Math.round(((totalCommitted + totalProposed) / Math.max(1, totalNeeded)) * 100) + '%');
			g.appendChild(svgText(pctText, node.w - 24, 18, 10, '#ffffffcc', '500', 'end'));

			// ── Gas tank fuel gauge ──
			const barW = node.w - GAS_TANK_PAD * 2;
			const barBg = ns('rect');
			barBg.setAttribute('x', String(GAS_TANK_PAD)); barBg.setAttribute('y', String(GAS_TANK_Y));
			barBg.setAttribute('width', String(barW)); barBg.setAttribute('height', String(GAS_TANK_H));
			barBg.setAttribute('rx', '4'); barBg.setAttribute('fill', '#1e293b');
			g.appendChild(barBg);
			if (totalNeeded > 0) {
				const cFrac = Math.min(1, totalCommitted / totalNeeded);
				const pFrac = Math.min(1 - cFrac, totalProposed / totalNeeded);
				if (cFrac > 0) {
					const cBar = ns('rect');
					cBar.setAttribute('x', String(GAS_TANK_PAD)); cBar.setAttribute('y', String(GAS_TANK_Y));
					cBar.setAttribute('width', String(cFrac * barW)); cBar.setAttribute('height', String(GAS_TANK_H));
					cBar.setAttribute('rx', '4'); cBar.setAttribute('fill', '#10b981');
					if (funded) cBar.setAttribute('filter', 'url(#glowGreen)');
					g.appendChild(cBar);
				}
				if (pFrac > 0) {
					const pBar = ns('rect');
					pBar.setAttribute('x', String(GAS_TANK_PAD + cFrac * barW)); pBar.setAttribute('y', String(GAS_TANK_Y));
					pBar.setAttribute('width', String(pFrac * barW)); pBar.setAttribute('height', String(GAS_TANK_H));
					pBar.setAttribute('rx', '4'); pBar.setAttribute('fill', '#f59e0b');
					g.appendChild(pBar);
				}
			}
			// Text overlay on bar
			const tankLabel = funded
				? 'Funded!'
				: totalCommitted + 'c + ' + totalProposed + 'p / ' + totalNeeded + ' hrs';
			const tankText = svgText(tankLabel, node.w / 2, GAS_TANK_Y + GAS_TANK_H / 2 + 4, 9, '#fff', '600', 'middle');
			tankText.style.pointerEvents = 'none';
			g.appendChild(tankText);

			skills.forEach((skill, i) => {
				const ry = TASK_H_BASE + i * TASK_ROW;
				const needed = t.needs[skill];
				// Compute proposed/committed hours for this skill on this task
				const skillWires = this.connections.filter(w => w.to === node.id && w.skill === skill);
				const committed = skillWires.filter(w => w.status === 'committed').reduce((s, w) => s + w.hours, 0);
				const proposed = skillWires.filter(w => w.status === 'proposed').reduce((s, w) => s + w.hours, 0);
				const ful = committed + proposed;
				const done = ful >= needed;

				const portHit = ns('circle');
				portHit.setAttribute('cx', '0'); portHit.setAttribute('cy', String(ry + TASK_ROW / 2));
				portHit.setAttribute('r', '18'); portHit.setAttribute('fill', 'transparent');
				portHit.setAttribute('class', 'port');
				portHit.setAttribute('data-node', node.id); portHit.setAttribute('data-port', 'input'); portHit.setAttribute('data-skill', skill);
				g.appendChild(portHit);

				// Port fill: green segment for committed, amber for proposed
				const portColor = committed >= needed ? '#10b981' : proposed > 0 ? '#f59e0b' : (SKILL_COLORS[skill] || '#888');
				const port = ns('circle');
				port.setAttribute('cx', '0'); port.setAttribute('cy', String(ry + TASK_ROW / 2));
				port.setAttribute('r', String(PORT_R));
				port.setAttribute('fill', portColor);
				port.setAttribute('stroke', '#fff'); port.setAttribute('stroke-width', '2');
				port.style.pointerEvents = 'none';
				g.appendChild(port);

				const dot = ns('circle');
				dot.setAttribute('cx', '20'); dot.setAttribute('cy', String(ry + TASK_ROW / 2));
				dot.setAttribute('r', '4'); dot.setAttribute('fill', SKILL_COLORS[skill] || '#888');
				g.appendChild(dot);

				// Build label showing committed/proposed/needed
				let lbl: string;
				if (committed > 0 && proposed > 0) {
					lbl = (SKILL_LABELS[skill] || skill) + ': ' + committed + 'c+' + proposed + 'p/' + needed + 'hr';
				} else if (committed > 0) {
					lbl = (SKILL_LABELS[skill] || skill) + ': ' + committed + '/' + needed + 'hr \u2713';
				} else if (proposed > 0) {
					lbl = (SKILL_LABELS[skill] || skill) + ': ' + proposed + '/' + needed + 'hr \u23f3';
				} else {
					lbl = (SKILL_LABELS[skill] || skill) + ': 0/' + needed + 'hr';
				}
				const lblColor = committed >= needed ? '#10b981' : proposed > 0 ? '#f59e0b' : '#94a3b8';
				g.appendChild(svgText(lbl, 30, ry + TASK_ROW / 2 + 4, 11, lblColor, done ? '600' : '400'));
				if (committed >= needed) g.appendChild(svgText('\u2713', node.w - 16, ry + TASK_ROW / 2 + 4, 13, '#10b981', '600', 'middle'));
			});

			// Dependency ports (diamond-shaped)
			// Input dep-port: left center
			const depInHit = ns('rect');
			depInHit.setAttribute('x', '-12'); depInHit.setAttribute('y', String(node.h / 2 - 12));
			depInHit.setAttribute('width', '24'); depInHit.setAttribute('height', '24');
			depInHit.setAttribute('fill', 'transparent');
			depInHit.setAttribute('class', 'dep-port');
			depInHit.setAttribute('data-node', node.id); depInHit.setAttribute('data-port', 'dep-input');
			g.appendChild(depInHit);
			const depIn = ns('polygon');
			depIn.setAttribute('points', `0,${node.h / 2 - 5} 5,${node.h / 2} 0,${node.h / 2 + 5} -5,${node.h / 2}`);
			depIn.setAttribute('fill', '#475569'); depIn.setAttribute('stroke', '#64748b'); depIn.setAttribute('stroke-width', '1');
			depIn.setAttribute('class', 'dep-port-diamond');
			depIn.style.pointerEvents = 'none';
			g.appendChild(depIn);

			// Output dep-port: right center
			const depOutHit = ns('rect');
			depOutHit.setAttribute('x', String(node.w - 12)); depOutHit.setAttribute('y', String(node.h / 2 - 12));
			depOutHit.setAttribute('width', '24'); depOutHit.setAttribute('height', '24');
			depOutHit.setAttribute('fill', 'transparent');
			depOutHit.setAttribute('class', 'dep-port');
			depOutHit.setAttribute('data-node', node.id); depOutHit.setAttribute('data-port', 'dep-output');
			g.appendChild(depOutHit);
			const depOut = ns('polygon');
			depOut.setAttribute('points', `${node.w},${node.h / 2 - 5} ${node.w + 5},${node.h / 2} ${node.w},${node.h / 2 + 5} ${node.w - 5},${node.h / 2}`);
			depOut.setAttribute('fill', '#475569'); depOut.setAttribute('stroke', '#64748b'); depOut.setAttribute('stroke-width', '1');
			depOut.setAttribute('class', 'dep-port-diamond');
			depOut.style.pointerEvents = 'none';
			g.appendChild(depOut);

			if (ready) {
				const btnY = node.baseH! + 4;
				const btnR = ns('rect');
				btnR.setAttribute('x', '12'); btnR.setAttribute('y', String(btnY));
				btnR.setAttribute('width', String(node.w - 24)); btnR.setAttribute('height', String(EXEC_BTN_H));
				btnR.setAttribute('rx', '6'); btnR.setAttribute('ry', '6');
				btnR.setAttribute('fill', '#10b981');
				btnR.setAttribute('class', 'exec-btn-rect');
				btnR.setAttribute('data-task-id', node.id);
				g.appendChild(btnR);
				const btnT = svgText('Execute Project \u2192', node.w / 2, btnY + EXEC_BTN_H / 2 + 4, 12, '#fff', '600', 'middle');
				btnT.style.pointerEvents = 'none';
				g.appendChild(btnT);
			}
		}

		return g;
	}

	private renderAll() {
		this.nodesLayer.innerHTML = '';
		this.weaveNodes.forEach(n => this.nodesLayer.appendChild(this.renderNode(n)));
		this.renderConnections();
		this.renderProjectFrames();
	}

	// SVG pointer events (with pan/zoom support)

	private onSvgPointerDown(e: PointerEvent) {
		if (this.svgActivePointerId !== null) return;
		this.svgActivePointerId = e.pointerId;
		this.svgEl.setPointerCapture(e.pointerId);

		// Space+drag = pan
		if (this.spaceHeld) {
			this.isPanning = true;
			this.panStart = { x: e.clientX, y: e.clientY, panX: this.panX, panY: this.panY };
			const wrap = this.shadow.getElementById('canvasWrap')!;
			wrap.style.cursor = 'grabbing';
			return;
		}

		if ((e.target as Element).classList.contains('exec-btn-rect')) {
			const taskId = (e.target as HTMLElement).getAttribute('data-task-id');
			if (taskId) this.openExecPanel(taskId);
			return;
		}

		// Mycelial suggestion confirm/dismiss
		if ((e.target as Element).classList.contains('suggest-confirm-btn')) {
			this.acceptSuggestion();
			return;
		}
		if ((e.target as Element).classList.contains('suggest-dismiss-btn')) {
			this.dismissSuggestion();
			return;
		}

		// Approve/decline buttons on commitment hexagons
		if ((e.target as Element).classList.contains('approve-btn')) {
			const connId = (e.target as SVGElement).getAttribute('data-connection-id');
			if (connId) this.handleApprove(connId);
			return;
		}
		if ((e.target as Element).classList.contains('decline-btn')) {
			const connId = (e.target as SVGElement).getAttribute('data-connection-id');
			if (connId) this.handleDecline(connId);
			return;
		}

		// Frame resize handle
		const resizeHandle = (e.target as Element).closest('.frame-resize-handle') as SVGElement;
		if (resizeHandle) {
			const frameId = resizeHandle.getAttribute('data-frame-id');
			const frame = this.projectFrames.find(f => f.id === frameId);
			if (frame) {
				e.preventDefault();
				const startPt = this.screenToCanvas(e.clientX, e.clientY);
				const startW = frame.w, startH = frame.h;
				const resizeMove = (ev: PointerEvent) => {
					const curPt = this.screenToCanvas(ev.clientX, ev.clientY);
					frame.w = Math.max(120, startW + (curPt.x - startPt.x));
					frame.h = Math.max(80, startH + (curPt.y - startPt.y));
					this.renderAll();
				};
				const resizeUp = () => {
					document.removeEventListener('pointermove', resizeMove);
					document.removeEventListener('pointerup', resizeUp);
					this.autoAssignTasksToFrame(frame);
					this.renderAll();
				};
				document.addEventListener('pointermove', resizeMove);
				document.addEventListener('pointerup', resizeUp);
				return;
			}
		}

		// Frame drag (click on frame rect but not resize handle)
		const frameRect = (e.target as Element).closest('.project-frame-rect') as SVGElement;
		if (frameRect) {
			const frameGroup = frameRect.closest('.project-frame-group') as SVGElement;
			const frameId = frameGroup?.getAttribute('data-frame-id');
			const frame = this.projectFrames.find(f => f.id === frameId);
			if (frame) {
				e.preventDefault();
				const startPt = this.screenToCanvas(e.clientX, e.clientY);
				const startX = frame.x, startY = frame.y;
				const dragMove = (ev: PointerEvent) => {
					const curPt = this.screenToCanvas(ev.clientX, ev.clientY);
					frame.x = startX + (curPt.x - startPt.x);
					frame.y = startY + (curPt.y - startPt.y);
					this.renderAll();
				};
				const dragUp = () => {
					document.removeEventListener('pointermove', dragMove);
					document.removeEventListener('pointerup', dragUp);
				};
				document.addEventListener('pointermove', dragMove);
				document.addEventListener('pointerup', dragUp);
				return;
			}
		}

		// Dep-port connections (task-to-task dependencies)
		const depPort = (e.target as Element).closest('.dep-port') as SVGElement;
		if (depPort) {
			e.preventDefault();
			this.connecting = { nodeId: depPort.getAttribute('data-node')!, portType: depPort.getAttribute('data-port')!, skill: '__dep__' };
			this.tempConn.style.display = 'block';
			return;
		}

		const port = (e.target as Element).closest('.port') as SVGElement;
		if (port) {
			e.preventDefault();
			this.connecting = { nodeId: port.getAttribute('data-node')!, portType: port.getAttribute('data-port')!, skill: port.getAttribute('data-skill') };
			this.tempConn.style.display = 'block';
			return;
		}
		const ng = (e.target as Element).closest('.node-group') as SVGElement;
		if (ng) {
			const id = ng.dataset.id!;
			this.dragNode = this.weaveNodes.find(n => n.id === id) || null;
			if (this.dragNode) {
				const pt = this.screenToCanvas(e.clientX, e.clientY);
				this.dragOff.x = pt.x - this.dragNode.x;
				this.dragOff.y = pt.y - this.dragNode.y;
			}
			// Double-tap detection
			const now = Date.now();
			if (this.svgDblTapPos && now - this.svgDblTapTime < 300) {
				const dx = e.clientX - this.svgDblTapPos.x, dy = e.clientY - this.svgDblTapPos.y;
				if (dx * dx + dy * dy < 225) {
					const taskNode = this.weaveNodes.find(n => n.id === id && n.type === 'task');
					if (taskNode) { this.openTaskEditor(taskNode); this.dragNode = null; }
				}
			}
			this.svgDblTapTime = now;
			this.svgDblTapPos = { x: e.clientX, y: e.clientY };
		} else {
			// Click on empty canvas = start pan (middle mouse or just drag)
			if (e.button === 1) {
				this.isPanning = true;
				this.panStart = { x: e.clientX, y: e.clientY, panX: this.panX, panY: this.panY };
			}
		}
	}

	private onSvgPointerMove(e: PointerEvent) {
		if (e.pointerId !== this.svgActivePointerId) return;

		// Panning
		if (this.isPanning) {
			this.panX = this.panStart.panX + (e.clientX - this.panStart.x);
			this.panY = this.panStart.panY + (e.clientY - this.panStart.y);
			this.updateCanvasTransform();
			return;
		}

		const pt = this.screenToCanvas(e.clientX, e.clientY);
		if (this.connecting) {
			const fn = this.weaveNodes.find(n => n.id === this.connecting!.nodeId);
			if (!fn) return;
			if (this.connecting.portType === 'dep-input') {
				const x1 = fn.x, y1 = fn.y + fn.h / 2;
				this.tempConn.setAttribute('d', bezier(pt.x, pt.y, x1, y1));
			} else if (this.connecting.portType === 'dep-output') {
				const outX = fn.x + fn.w, outY = fn.y + fn.h / 2;
				this.tempConn.setAttribute('d', bezier(outX, outY, pt.x, pt.y));
			} else if (this.connecting.portType === 'input') {
				const skills = fn.type === 'task' ? Object.keys(fn.data.needs) : [];
				const idx = skills.indexOf(this.connecting.skill!);
				const x1 = fn.x;
				const y1 = idx >= 0 ? fn.y + TASK_H_BASE + idx * TASK_ROW + TASK_ROW / 2 : fn.y + fn.h / 2;
				this.tempConn.setAttribute('d', bezier(pt.x, pt.y, x1, y1));
			} else {
				const outX = fn.x + fn.w, outY = fn.y + fn.h / 2;
				this.tempConn.setAttribute('d', bezier(outX, outY, pt.x, pt.y));
			}
			return;
		}
		if (this.dragNode) {
			this.dragNode.x = pt.x - this.dragOff.x;
			this.dragNode.y = pt.y - this.dragOff.y;
			this.renderAll();
		}
	}

	private onSvgPointerUp(e: PointerEvent) {
		if (e.pointerId !== this.svgActivePointerId) return;
		this.svgActivePointerId = null;

		if (this.isPanning) {
			this.isPanning = false;
			const wrap = this.shadow.getElementById('canvasWrap');
			if (wrap) wrap.style.cursor = this.spaceHeld ? 'grab' : '';
			return;
		}

		if (this.connecting) {
			// Check for dep-port target first
			const depTarget = (e.target as Element).closest('.dep-port') as SVGElement;
			if (depTarget && this.connecting.skill === '__dep__') {
				const tId = depTarget.getAttribute('data-node')!;
				const tType = depTarget.getAttribute('data-port')!;
				if (tId !== this.connecting.nodeId) {
					let fromId: string | undefined, toId: string | undefined;
					if (this.connecting.portType === 'dep-output' && tType === 'dep-input') {
						fromId = this.connecting.nodeId; toId = tId;
					} else if (this.connecting.portType === 'dep-input' && tType === 'dep-output') {
						fromId = tId; toId = this.connecting.nodeId;
					}
					if (fromId && toId && !this.connections.find(c => c.from === fromId && c.to === toId && (c as any).connectionType === 'dependency')) {
						this.connections.push({ from: fromId, to: toId, skill: '__dep__', hours: 0, status: 'committed', connectionType: 'dependency' } as any);
						this.renderAll();
					}
				}
				this.connecting = null;
				this.tempConn.style.display = 'none';
				this.tempConn.setAttribute('d', '');
				return;
			}

			const port = (e.target as Element).closest('.port') as SVGElement;
			if (port) {
				const tId = port.getAttribute('data-node')!;
				const tType = port.getAttribute('data-port')!;
				const tSkill = port.getAttribute('data-skill');
				let fromId: string | undefined, toId: string | undefined, skill: string | undefined;
				if (this.connecting.portType === 'output' && tType === 'input' && tId !== this.connecting.nodeId) {
					fromId = this.connecting.nodeId; toId = tId; skill = tSkill || undefined;
				} else if (this.connecting.portType === 'input' && tType === 'output' && tId !== this.connecting.nodeId) {
					fromId = tId; toId = this.connecting.nodeId; skill = this.connecting.skill || undefined;
				}
				if (fromId && toId && skill) {
					const fNode = this.weaveNodes.find(n => n.id === fromId);
					const tNode = this.weaveNodes.find(n => n.id === toId);
					if (fNode && tNode && fNode.type === 'commitment' && tNode.type === 'task' && fNode.data.skill === skill) {
						if (!this.connections.find(c => c.from === fromId && c.to === toId && c.skill === skill)) {
							const commitId = fromId.replace(/^cn-/, '');
							const available = this.availableHours(commitId);
							const needed = (tNode.data.needs[skill] || 0) - ((tNode.data.fulfilled || {})[skill] || 0);
							const allocate = Math.min(available, Math.max(0, needed));
							if (allocate > 0) {
								this.connections.push({ from: fromId, to: toId, skill, hours: allocate, status: 'proposed' });
								if (!tNode.data.fulfilled) tNode.data.fulfilled = {};
								tNode.data.fulfilled[skill] = (tNode.data.fulfilled[skill] || 0) + allocate;
								this.persistConnection(fromId, toId, skill, allocate);
								this.buildOrbs();
								const nowReady = this.isTaskReady(tNode);
								this.renderAll();
								this.rebuildSidebar();
								if (nowReady) setTimeout(() => this.openExecPanel(tNode.id), 600);
							}
						}
					}
				}
			}
			this.connecting = null;
			this.tempConn.style.display = 'none';
			this.tempConn.setAttribute('d', '');
			return;
		}
		if (this.dragNode && this.dragNode.type === 'task') {
			// Auto-assign task to frame if dropped inside
			const nodeCx = this.dragNode.x + this.dragNode.w / 2;
			const nodeCy = this.dragNode.y + this.dragNode.h / 2;
			for (const frame of this.projectFrames) {
				if (nodeCx >= frame.x && nodeCx <= frame.x + frame.w &&
					nodeCy >= frame.y && nodeCy <= frame.y + frame.h) {
					if (!frame.taskIds.includes(this.dragNode.id)) frame.taskIds.push(this.dragNode.id);
				} else {
					frame.taskIds = frame.taskIds.filter(id => id !== this.dragNode!.id);
				}
			}
		}

		// Persist canvas position to weaving overlay
		if (this.dragNode?.type === 'task') {
			fetch(`${this.getApiBase()}/api/weave/overlay/${this.dragNode.id}`, {
				method: 'PUT',
				headers: { 'Content-Type': 'application/json', ...this.authHeaders() },
				body: JSON.stringify({ canvasX: this.dragNode.x, canvasY: this.dragNode.y }),
			}).catch(() => {});
		}

		this.dragNode = null;
	}

	// ── Sidebar (pool panel) ──

	private rebuildSidebar() {
		const tc = this.shadow.getElementById('sidebarTasks');
		if (!tc) return;
		tc.innerHTML = '';

		// Show placed tasks not yet on canvas SVG
		const usedT = new Set(this.weaveNodes.filter(n => n.type === 'task').map(n => n.id));
		this.tasks.forEach(t => {
			if (usedT.has(t.id)) return;
			const el = document.createElement('div');
			el.className = 'sidebar-task';
			const needs = Object.entries(t.needs).map(([s, h]) => h + 'hr ' + (SKILL_LABELS[s] || s)).join(', ');
			el.innerHTML = '<div class="sidebar-task-icon">\u25C9</div>' +
				'<div class="sidebar-item-info"><div class="sidebar-item-name">' + t.name + '</div>' +
				'<div class="sidebar-item-meta">' + needs + '</div></div>';
			el.addEventListener('pointerdown', (e) => this.startSidebarDrag(e, { type: 'task', id: t.id }, t.name));
			tc.appendChild(el);
		});

		// Show unplaced rTasks items (from board)
		if (this.unplacedTasks.length > 0) {
			const header = document.createElement('div');
			header.className = 'sidebar-section';
			header.textContent = 'rTasks Board';
			tc.appendChild(header);

			this.unplacedTasks.forEach(ut => {
				const el = document.createElement('div');
				el.className = 'sidebar-task';
				const badge = ut.status === 'DONE' ? ' \u2713' : ut.status === 'IN_PROGRESS' ? ' \u25B6' : '';
				el.innerHTML = '<div class="sidebar-task-icon" style="opacity:0.6">\u25CB</div>' +
					'<div class="sidebar-item-info"><div class="sidebar-item-name">' + ut.title + badge + '</div>' +
					'<div class="sidebar-item-meta">' + ut.status + '</div></div>';
				el.addEventListener('click', () => this.placeUnplacedTask(ut));
				tc.appendChild(el);
			});
		}
	}

	private async placeUnplacedTask(ut: UnplacedTask) {
		// Prompt for needs via simple dialog
		const needsStr = prompt(`Skill needs for "${ut.title}" (e.g. tech:4, design:2):`, '');
		if (needsStr === null) return;
		const needs: Record<string, number> = {};
		if (needsStr.trim()) {
			for (const part of needsStr.split(',')) {
				const [skill, hrs] = part.trim().split(':');
				if (skill && hrs) needs[skill.trim()] = parseInt(hrs.trim()) || 1;
			}
		}

		try {
			const resp = await fetch(`${this.getApiBase()}/api/weave/place/${ut.id}`, {
				method: 'POST',
				headers: { 'Content-Type': 'application/json', ...this.authHeaders() },
				body: JSON.stringify({ needs, canvasX: 400, canvasY: 150 }),
			});
			if (!resp.ok) return;

			// Add to local tasks and canvas
			const taskData: TaskData = {
				id: ut.id,
				name: ut.title,
				description: ut.description || '',
				needs,
				links: [],
				notes: '',
				status: ut.status,
				canvasX: 400,
				canvasY: 150,
			};
			this.tasks.push(taskData);
			this.unplacedTasks = this.unplacedTasks.filter(t => t.id !== ut.id);
			this.weaveNodes.push(this.mkTaskNode(taskData, 400, 150));
			this.renderAll();
			this.rebuildSidebar();
		} catch { /* offline */ }
	}

	private startSidebarDrag(e: PointerEvent, data: { type: string; id: string }, label: string) {
		e.preventDefault();
		this.sidebarDragData = data;
		this.sidebarGhost = document.createElement('div');
		this.sidebarGhost.style.cssText = 'position:fixed;padding:0.5rem 0.85rem;background:#1e293b;border:2px solid #8b5cf6;border-radius:0.5rem;font-size:0.8rem;font-weight:600;color:#e2e8f0;pointer-events:none;z-index:9999;box-shadow:0 4px 16px rgba(139,92,246,0.25);white-space:nowrap;';
		this.sidebarGhost.textContent = label;
		this.sidebarGhost.style.left = e.clientX + 'px';
		this.sidebarGhost.style.top = (e.clientY - 20) + 'px';
		document.body.appendChild(this.sidebarGhost);

		const moveHandler = (ev: PointerEvent) => {
			if (!this.sidebarGhost) return;
			this.sidebarGhost.style.left = ev.clientX + 'px';
			this.sidebarGhost.style.top = (ev.clientY - 20) + 'px';
		};
		const upHandler = (ev: PointerEvent) => {
			document.removeEventListener('pointermove', moveHandler);
			document.removeEventListener('pointerup', upHandler);
			document.removeEventListener('pointercancel', cancelHandler);
			if (this.sidebarGhost) { this.sidebarGhost.remove(); this.sidebarGhost = null; }
			if (!this.sidebarDragData) return;

			const wrap = this.shadow.getElementById('canvasWrap');
			if (!wrap) return;
			const wr = wrap.getBoundingClientRect();
			if (ev.clientX >= wr.left && ev.clientX <= wr.right && ev.clientY >= wr.top && ev.clientY <= wr.bottom) {
				const pt = this.screenToCanvas(ev.clientX, ev.clientY);
				if (this.sidebarDragData.type === 'commitment') {
					const c = this.commitments.find(x => x.id === this.sidebarDragData!.id);
					if (c && !this.weaveNodes.find(n => n.id === 'cn-' + c.id)) {
						this.weaveNodes.push(this.mkCommitNode(c, pt.x - NODE_W / 2, pt.y - NODE_H / 2));
					}
				} else if (this.sidebarDragData.type === 'task') {
					const t = this.tasks.find(x => x.id === this.sidebarDragData!.id);
					if (t && !this.weaveNodes.find(n => n.id === t.id)) {
						this.weaveNodes.push(this.mkTaskNode(t, pt.x - TASK_W / 2, pt.y - 40));
					}
				}
				this.renderAll();
				this.rebuildSidebar();
			}
			this.sidebarDragData = null;
		};
		const cancelHandler = () => {
			document.removeEventListener('pointermove', moveHandler);
			document.removeEventListener('pointerup', upHandler);
			document.removeEventListener('pointercancel', cancelHandler);
			if (this.sidebarGhost) { this.sidebarGhost.remove(); this.sidebarGhost = null; }
			this.sidebarDragData = null;
		};

		document.addEventListener('pointermove', moveHandler);
		document.addEventListener('pointerup', upHandler);
		document.addEventListener('pointercancel', cancelHandler);
	}

	// ── Orb drag-to-canvas ──

	private cancelOrbDrag() {
		if (this.orbDragTimer) { clearTimeout(this.orbDragTimer); this.orbDragTimer = null; }
		if (this.orbDragGhost) { this.orbDragGhost.remove(); this.orbDragGhost = null; }
		this.draggingOrb = null;
		this.draggingSkill = null;
		this.clearSkillHighlights();
	}

	private startOrbDrag(orb: Orb, clientX: number, clientY: number) {
		this.draggingOrb = orb;
		this.draggingSkill = orb.c.skill;

		// Create ghost circle
		this.orbDragGhost = document.createElement('div');
		const color = SKILL_COLORS[orb.c.skill] || '#8b5cf6';
		const sz = Math.max(24, orb.baseRadius * 0.8);
		this.orbDragGhost.style.cssText = `position:fixed;width:${sz}px;height:${sz}px;border-radius:50%;background:${color};opacity:0.7;pointer-events:none;z-index:9999;box-shadow:0 0 16px ${color};transform:translate(-50%,-50%);`;
		this.orbDragGhost.style.left = clientX + 'px';
		this.orbDragGhost.style.top = clientY + 'px';
		document.body.appendChild(this.orbDragGhost);

		// Highlight matching task ports
		this.applySkillHighlights(orb.c.skill);

		const moveHandler = (ev: PointerEvent) => {
			if (!this.orbDragGhost) return;
			this.orbDragGhost.style.left = ev.clientX + 'px';
			this.orbDragGhost.style.top = ev.clientY + 'px';
		};
		const upHandler = (ev: PointerEvent) => {
			document.removeEventListener('pointermove', moveHandler);
			document.removeEventListener('pointerup', upHandler);
			document.removeEventListener('pointercancel', cancelOrbHandler);

			const wrap = this.shadow.getElementById('canvasWrap');
			if (wrap && this.draggingOrb) {
				const wr = wrap.getBoundingClientRect();
				if (ev.clientX >= wr.left && ev.clientX <= wr.right && ev.clientY >= wr.top && ev.clientY <= wr.bottom) {
					// Use original commitment (not the display-hours version)
					const c = (this.draggingOrb as any)._originalCommitment || this.draggingOrb.c;
					const pt = this.screenToCanvas(ev.clientX, ev.clientY);

					// Hit-test: did we drop on an unfulfilled task port?
					const dropNode = this.hitTestTaskPort(pt.x, pt.y, c.skill);
					if (dropNode) {
						const available = this.availableHours(c.id);
						const needed = (dropNode.data.needs[c.skill] || 0) - ((dropNode.data.fulfilled || {})[c.skill] || 0);
						const allocate = Math.min(available, Math.max(0, needed));
						if (allocate > 0) {
							const fromId = 'cn-' + c.id;
							// Ensure commitment node exists on canvas
							if (!this.weaveNodes.find(n => n.id === fromId)) {
								this.weaveNodes.push(this.mkCommitNode(c, pt.x - NODE_W / 2, pt.y - NODE_H / 2));
							}
							if (!this.connections.find(w => w.from === fromId && w.to === dropNode.id && w.skill === c.skill)) {
								this.connections.push({ from: fromId, to: dropNode.id, skill: c.skill, hours: allocate, status: 'proposed' });
								if (!dropNode.data.fulfilled) dropNode.data.fulfilled = {};
								dropNode.data.fulfilled[c.skill] = (dropNode.data.fulfilled[c.skill] || 0) + allocate;
								this.persistConnection(fromId, dropNode.id, c.skill, allocate);
							}
						}
					} else {
						// Dropped on open canvas — auto-connect to nearest matching task
						const matchTask = this.findNearestUnfulfilledTask(pt.x, pt.y, c.skill);
						if (matchTask) {
							const available = this.availableHours(c.id);
							const needed = (matchTask.data.needs[c.skill] || 0) - ((matchTask.data.fulfilled || {})[c.skill] || 0);
							const allocate = Math.min(available, Math.max(0, needed));
							if (allocate > 0) {
								// Place commitment node offset left of the task's input port
								const commitX = matchTask.x - 180;
								const skills = Object.keys(matchTask.data.needs);
								const skillIdx = skills.indexOf(c.skill);
								const portY = matchTask.y + TASK_H_BASE + (skillIdx >= 0 ? skillIdx : 0) * TASK_ROW + TASK_ROW / 2;
								const commitY = portY - NODE_H / 2;
								const fromId = 'cn-' + c.id;
								if (!this.weaveNodes.find(n => n.id === fromId)) {
									this.weaveNodes.push(this.mkCommitNode(c, commitX, commitY));
								}
								// Auto-weave: create connection immediately
								if (!this.connections.find(w => w.from === fromId && w.to === matchTask.id && w.skill === c.skill)) {
									this.connections.push({ from: fromId, to: matchTask.id, skill: c.skill, hours: allocate, status: 'proposed' });
									if (!matchTask.data.fulfilled) matchTask.data.fulfilled = {};
									matchTask.data.fulfilled[c.skill] = (matchTask.data.fulfilled[c.skill] || 0) + allocate;
									this.persistConnection(fromId, matchTask.id, c.skill, allocate);
								}
							} else {
								// No hours to allocate — just place the node
								if (!this.weaveNodes.find(n => n.id === 'cn-' + c.id)) {
									this.weaveNodes.push(this.mkCommitNode(c, pt.x - NODE_W / 2, pt.y - NODE_H / 2));
								}
							}
						} else {
							// No matching task found — place commitment node without connection
							if (!this.weaveNodes.find(n => n.id === 'cn-' + c.id)) {
								this.weaveNodes.push(this.mkCommitNode(c, pt.x - NODE_W / 2, pt.y - NODE_H / 2));
							}
						}
					}

					this.buildOrbs();
					this.renderAll();
					this.rebuildSidebar();
				}
			}
			this.cancelOrbDrag();
		};
		const cancelOrbHandler = () => {
			document.removeEventListener('pointermove', moveHandler);
			document.removeEventListener('pointerup', upHandler);
			document.removeEventListener('pointercancel', cancelOrbHandler);
			this.cancelOrbDrag();
		};

		document.addEventListener('pointermove', moveHandler);
		document.addEventListener('pointerup', upHandler);
		document.addEventListener('pointercancel', cancelOrbHandler);
	}

	/** Hit-test: find a task node with an unfulfilled port for the given skill near (x, y). */
	private hitTestTaskPort(x: number, y: number, skill: string): WeaveNode | null {
		for (const node of this.weaveNodes) {
			if (node.type !== 'task') continue;
			const t = node.data as TaskData;
			const needed = t.needs[skill];
			if (!needed) continue;
			const fulfilled = (t.fulfilled || {})[skill] || 0;
			if (fulfilled >= needed) continue;
			// Check if drop point is within 60px of the port
			const skills = Object.keys(t.needs);
			const idx = skills.indexOf(skill);
			if (idx < 0) continue;
			const portX = node.x;
			const portY = node.y + TASK_H_BASE + idx * TASK_ROW + TASK_ROW / 2;
			const dx = x - portX, dy = y - portY;
			if (dx * dx + dy * dy < 60 * 60) return node;
		}
		return null;
	}

	/** Find the nearest task node that needs the given skill and isn't fully fulfilled. */
	private findNearestUnfulfilledTask(x: number, y: number, skill: string): WeaveNode | null {
		let best: WeaveNode | null = null;
		let bestDist = Infinity;
		for (const node of this.weaveNodes) {
			if (node.type !== 'task') continue;
			const t = node.data as TaskData;
			const needed = t.needs[skill];
			if (!needed) continue;
			const fulfilled = (t.fulfilled || {})[skill] || 0;
			if (fulfilled >= needed) continue;
			const dx = x - node.x, dy = y - (node.y + node.h / 2);
			const dist = dx * dx + dy * dy;
			if (dist < bestDist) { bestDist = dist; best = node; }
		}
		return best;
	}

	/** Accept a pending mycelial suggestion — create the wire. */
	private acceptSuggestion() {
		const s = this.pendingSuggestion;
		if (!s) return;
		if (!this.connections.find(w => w.from === s.fromId && w.to === s.toNode.id && w.skill === s.skill)) {
			this.connections.push({ from: s.fromId, to: s.toNode.id, skill: s.skill, hours: s.hours, status: 'proposed' });
			if (!s.toNode.data.fulfilled) s.toNode.data.fulfilled = {};
			s.toNode.data.fulfilled[s.skill] = (s.toNode.data.fulfilled[s.skill] || 0) + s.hours;
			this.persistConnection(s.fromId, s.toNode.id, s.skill, s.hours);
		}
		this.pendingSuggestion = null;
		this.buildOrbs();
		this.renderAll();
		this.rebuildSidebar();
	}

	/** Dismiss the pending mycelial suggestion. */
	private dismissSuggestion() {
		this.pendingSuggestion = null;
		this.renderAll();
	}

	// ── Project Frames ──

	private renderProjectFrames() {
		// Clear old frames but keep intent frames
		this.intentFramesLayer.querySelectorAll('.project-frame-group').forEach(el => el.remove());

		for (const frame of this.projectFrames) {
			const g = ns('g') as SVGGElement;
			g.setAttribute('class', 'project-frame-group');
			g.setAttribute('data-frame-id', frame.id);

			const fillColor = frame.color || '#8b5cf620';
			const strokeColor = frame.color ? frame.color + '66' : '#8b5cf666';

			const rect = ns('rect');
			rect.setAttribute('x', String(frame.x));
			rect.setAttribute('y', String(frame.y));
			rect.setAttribute('width', String(frame.w));
			rect.setAttribute('height', String(frame.h));
			rect.setAttribute('rx', '12');
			rect.setAttribute('fill', fillColor);
			rect.setAttribute('stroke', strokeColor);
			rect.setAttribute('stroke-width', '2');
			rect.setAttribute('stroke-dasharray', '8 4');
			rect.setAttribute('class', 'project-frame-rect');
			g.appendChild(rect);

			// Title bar
			const titleBg = ns('rect');
			titleBg.setAttribute('x', String(frame.x));
			titleBg.setAttribute('y', String(frame.y));
			titleBg.setAttribute('width', String(frame.w));
			titleBg.setAttribute('height', '28');
			titleBg.setAttribute('rx', '12');
			titleBg.setAttribute('fill', strokeColor);
			g.appendChild(titleBg);
			// Bottom corners mask
			const titleMask = ns('rect');
			titleMask.setAttribute('x', String(frame.x));
			titleMask.setAttribute('y', String(frame.y + 16));
			titleMask.setAttribute('width', String(frame.w));
			titleMask.setAttribute('height', '12');
			titleMask.setAttribute('fill', strokeColor);
			g.appendChild(titleMask);

			const titleText = svgText(frame.title, frame.x + 12, frame.y + 18, 11, '#f1f5f9', '600');
			g.appendChild(titleText);

			// Resize handle (bottom-right)
			const resizeHandle = ns('rect');
			resizeHandle.setAttribute('x', String(frame.x + frame.w - 16));
			resizeHandle.setAttribute('y', String(frame.y + frame.h - 16));
			resizeHandle.setAttribute('width', '16');
			resizeHandle.setAttribute('height', '16');
			resizeHandle.setAttribute('fill', 'transparent');
			resizeHandle.setAttribute('class', 'frame-resize-handle');
			resizeHandle.setAttribute('data-frame-id', frame.id);
			resizeHandle.style.cursor = 'nwse-resize';
			g.appendChild(resizeHandle);

			// Resize grip visual
			const grip = ns('path');
			grip.setAttribute('d', `M${frame.x + frame.w - 12},${frame.y + frame.h - 4} L${frame.x + frame.w - 4},${frame.y + frame.h - 12} M${frame.x + frame.w - 8},${frame.y + frame.h - 4} L${frame.x + frame.w - 4},${frame.y + frame.h - 8}`);
			grip.setAttribute('stroke', '#64748b');
			grip.setAttribute('stroke-width', '1.5');
			grip.setAttribute('fill', 'none');
			grip.style.pointerEvents = 'none';
			g.appendChild(grip);

			this.intentFramesLayer.appendChild(g);
		}
	}

	private addProjectFrame() {
		const wrap = this.shadow.getElementById('canvasWrap');
		const wrapRect = wrap?.getBoundingClientRect();
		const cx = wrapRect ? wrapRect.width / 2 : 400;
		const cy = wrapRect ? wrapRect.height / 2 : 300;
		const pt = this.screenToCanvas(
			(wrapRect?.left || 0) + cx,
			(wrapRect?.top || 0) + cy
		);

		const frame = {
			id: 'frame-' + Date.now(),
			title: 'Project Frame',
			taskIds: [] as string[],
			x: pt.x - 150,
			y: pt.y - 100,
			w: 300,
			h: 200,
		};
		this.projectFrames.push(frame);

		// Auto-assign tasks that are already inside bounds
		this.autoAssignTasksToFrame(frame);
		this.renderAll();
	}

	private autoAssignTasksToFrame(frame: { id: string; taskIds: string[]; x: number; y: number; w: number; h: number }) {
		for (const node of this.weaveNodes) {
			if (node.type !== 'task') continue;
			const nodeCx = node.x + node.w / 2;
			const nodeCy = node.y + node.h / 2;
			if (nodeCx >= frame.x && nodeCx <= frame.x + frame.w &&
				nodeCy >= frame.y && nodeCy <= frame.y + frame.h) {
				if (!frame.taskIds.includes(node.id)) frame.taskIds.push(node.id);
			}
		}
	}

	private recomputeFrameBounds(frameId: string) {
		const frame = this.projectFrames.find(f => f.id === frameId);
		if (!frame || frame.taskIds.length === 0) return;
		let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
		for (const tid of frame.taskIds) {
			const node = this.weaveNodes.find(n => n.id === tid);
			if (!node) continue;
			minX = Math.min(minX, node.x);
			minY = Math.min(minY, node.y);
			maxX = Math.max(maxX, node.x + node.w);
			maxY = Math.max(maxY, node.y + node.h);
		}
		if (minX === Infinity) return;
		const pad = 20;
		frame.x = minX - pad;
		frame.y = minY - pad - 28; // extra for title bar
		frame.w = (maxX - minX) + pad * 2;
		frame.h = (maxY - minY) + pad * 2 + 28;
	}

	/** Highlight task nodes that have unfulfilled ports matching the given skill. */
	private applySkillHighlights(skill: string) {
		const groups = this.nodesLayer.querySelectorAll('.task-node');
		groups.forEach(g => {
			const id = (g as SVGElement).dataset.id;
			const node = this.weaveNodes.find(n => n.id === id);
			if (!node || node.type !== 'task') return;
			const t = node.data as TaskData;
			const needed = t.needs[skill];
			const fulfilled = (t.fulfilled || {})[skill] || 0;
			if (needed && fulfilled < needed) {
				g.classList.add('skill-match');
			}
		});
	}

	private clearSkillHighlights() {
		this.nodesLayer.querySelectorAll('.skill-match').forEach(g => g.classList.remove('skill-match'));
	}

	// ── Connection/exec persistence ──

	private applyRestoredConnections() {
		if (this._restoredConnections.length === 0) return;
		for (const rc of this._restoredConnections) {
			const fromNodeId = 'cn-' + rc.fromCommitmentId;
			const toNodeId = rc.toTaskId;
			const fNode = this.weaveNodes.find(n => n.id === fromNodeId);
			const tNode = this.weaveNodes.find(n => n.id === toNodeId);
			if (!fNode || !tNode) continue;
			if (this.connections.find(c => c.from === fromNodeId && c.to === toNodeId && c.skill === rc.skill)) continue;
			this.connections.push({ from: fromNodeId, to: toNodeId, skill: rc.skill, hours: rc.hours, status: rc.status, connectionId: rc.id });
			if (!tNode.data.fulfilled) tNode.data.fulfilled = {};
			tNode.data.fulfilled[rc.skill] = (tNode.data.fulfilled[rc.skill] || 0) + rc.hours;
		}
		this._restoredConnections = [];
		this.renderAll();
		this.rebuildSidebar();
	}

	private async persistConnection(from: string, to: string, skill: string, hours: number): Promise<any> {
		const fromCommitmentId = from.replace(/^cn-/, '');
		try {
			const res = await fetch(`${this.getApiBase()}/api/connections`, {
				method: 'POST',
				headers: { 'Content-Type': 'application/json', ...this.authHeaders() },
				body: JSON.stringify({ fromCommitmentId, toTaskId: to, skill, hours }),
			});
			if (res.ok) {
				const data = await res.json();
				// Update local wire with server-assigned id and status
				const wire = this.connections.find(w => w.from === from && w.to === to && w.skill === skill && !w.connectionId);
				if (wire) {
					wire.connectionId = data.id;
					wire.status = data.status || 'proposed';
				}
				return data;
			}
		} catch { /* offline */ }
		return null;
	}

	private async patchConnection(connectionId: string, status: 'committed' | 'declined') {
		try {
			const res = await fetch(`${this.getApiBase()}/api/connections/${connectionId}`, {
				method: 'PATCH',
				headers: { 'Content-Type': 'application/json', ...this.authHeaders() },
				body: JSON.stringify({ status }),
			});
			if (res.ok) return await res.json();
		} catch { /* offline */ }
		return null;
	}

	private async handleApprove(connectionId: string) {
		const wire = this.connections.find(w => w.connectionId === connectionId);
		if (!wire) return;
		const result = await this.patchConnection(connectionId, 'committed');
		if (result) {
			wire.status = 'committed';
			this.renderAll();
		}
	}

	private async handleDecline(connectionId: string) {
		const wire = this.connections.find(w => w.connectionId === connectionId);
		if (!wire) return;
		const result = await this.patchConnection(connectionId, 'declined');
		if (result) {
			// Remove wire and restore fulfilled hours
			const tNode = this.weaveNodes.find(n => n.id === wire.to);
			if (tNode && tNode.data.fulfilled) {
				tNode.data.fulfilled[wire.skill] = Math.max(0, (tNode.data.fulfilled[wire.skill] || 0) - wire.hours);
			}
			this.connections = this.connections.filter(w => w.connectionId !== connectionId);
			this.buildOrbs();
			this.renderAll();
			this.rebuildSidebar();
		}
	}

	private async persistExecState(taskId: string) {
		const states = this.execStepStates[taskId];
		if (!states) return;
		try {
			await fetch(`${this.getApiBase()}/api/weave/exec-state/${taskId}`, {
				method: 'PUT',
				headers: { 'Content-Type': 'application/json', ...this.authHeaders() },
				body: JSON.stringify({ steps: states }),
			});
		} catch { /* offline */ }
	}

	// ── Exec panel ──

	private openExecPanel(taskId: string) {
		const node = this.weaveNodes.find(n => n.id === taskId);
		if (!node || node.type !== 'task') return;
		this._currentExecTaskId = taskId;

		const t = node.data as TaskData;
		this.shadow.getElementById('execTitle')!.innerHTML = t.name + ' <span class="exec-panel-badge">All Commitments Woven</span>';
		this.shadow.getElementById('execSubtitle')!.textContent = 'Set up the project for execution';

		const members = this.connections.filter(c => c.to === taskId).map(c => {
			const cn = this.weaveNodes.find(n => n.id === c.from);
			return cn ? cn.data.memberName : '';
		}).filter(Boolean);

		const steps = EXEC_STEPS.default;
		if (!this.execStepStates[taskId]) {
			this.execStepStates[taskId] = {};
			steps.forEach((_, i) => { this.execStepStates[taskId][i] = 'pending'; });
			this.execStepStates[taskId][0] = 'active';
		}

		this.renderExecSteps(taskId, steps, members);
		this.shadow.getElementById('execOverlay')!.classList.add('visible');
	}

	private renderStepDetail(type: string, members: string[], taskId: string, stepIdx: number): string {
		const memberOpts = members.map(m => `<option value="${m}">${m}</option>`).join('');
		switch (type) {
			case 'venue':
				return `<div class="exec-step-detail">
					<div class="exec-step-field"><label>Space Type</label>
						<select><option>In-person venue</option><option>Virtual room</option><option>Hybrid</option></select></div>
					<div class="exec-step-field"><label>Location</label><input type="text" placeholder="Address or meeting URL"></div>
					<div class="exec-step-field"><label>Date &amp; Time</label><input type="datetime-local"></div>
					<div class="exec-step-field"><label>Space Lead</label><select><option value="">Select…</option>${memberOpts}</select></div>
					<button class="exec-step-action" data-complete-step="${stepIdx}" data-task-id="${taskId}">Confirm Setup</button>
				</div>`;
			case 'comms':
				return `<div class="exec-step-detail">
					<div class="exec-step-field"><label>Channels</label>
						<div class="exec-step-checklist">
							<label><input type="checkbox" checked> Group Chat</label>
							<label><input type="checkbox"> Email Thread</label>
							<label><input type="checkbox"> Signal Group</label>
						</div></div>
					<div class="exec-step-field"><label>Invite List</label>
						<div class="exec-step-invite-list">${members.map(m => `<span class="invite-tag">${m}</span>`).join('')}</div></div>
					<div class="exec-step-field"><label>Comms Lead</label><select><option value="">Select…</option>${memberOpts}</select></div>
					<button class="exec-step-action" data-complete-step="${stepIdx}" data-task-id="${taskId}">Set Up Channels</button>
				</div>`;
			case 'notes':
				return `<div class="exec-step-detail">
					<div class="exec-step-field"><label>Document Type</label>
						<select><option>Shared Agenda</option><option>Meeting Notes</option><option>Project Brief</option></select></div>
					<div class="exec-step-field"><label>Initial Sections</label>
						<div class="exec-step-checklist">
							<label><input type="checkbox" checked> Objectives</label>
							<label><input type="checkbox" checked> Agenda</label>
							<label><input type="checkbox"> Resources</label>
							<label><input type="checkbox"> Action Items</label>
						</div></div>
					<div class="exec-step-field"><label>Drive Link</label><input type="url" placeholder="https://…"></div>
					<button class="exec-step-action" data-complete-step="${stepIdx}" data-task-id="${taskId}">Create Document</button>
				</div>`;
			case 'prep':
				return `<div class="exec-step-detail">
					<div class="exec-step-field"><label>Transcript / Notes</label><textarea rows="3" placeholder="Paste transcript or notes…"></textarea></div>
					<div class="exec-step-field"><label>Processing Mode</label>
						<select><option>Summarize</option><option>Extract Action Items</option><option>Generate Brief</option></select></div>
					<button class="exec-step-action" data-complete-step="${stepIdx}" data-task-id="${taskId}">Process Input</button>
				</div>`;
			case 'launch':
				return `<div class="exec-step-detail">
					<div class="exec-step-field"><label>Pre-Launch Checklist</label>
						<div class="exec-step-checklist">
							<label><input type="checkbox"> All roles confirmed</label>
							<label><input type="checkbox"> Venue / link confirmed</label>
							<label><input type="checkbox"> Comms channels active</label>
							<label><input type="checkbox"> Shared docs ready</label>
							<label><input type="checkbox"> Notifications sent</label>
						</div></div>
					<button class="exec-step-action launch-action" data-complete-step="${stepIdx}" data-task-id="${taskId}">Confirm Launch</button>
				</div>`;
			default:
				return '';
		}
	}

	private completeExecStep(taskId: string, stepIdx: number) {
		const states = this.execStepStates[taskId];
		if (!states || states[stepIdx] === 'done') return;
		states[stepIdx] = 'done';
		// Auto-advance to next pending step
		const steps = EXEC_STEPS.default;
		let nextPending = -1;
		for (let j = 0; j < steps.length; j++) {
			if (states[j] !== 'done') { nextPending = j; break; }
		}
		if (nextPending >= 0) {
			Object.keys(states).forEach(k => { if (states[Number(k)] === 'active') states[Number(k)] = 'pending'; });
			states[nextPending] = 'active';
		}
		const members = this.connections.filter(c => c.to === taskId).map(c => {
			const cn = this.weaveNodes.find(n => n.id === c.from);
			return cn ? cn.data.memberName : '';
		}).filter(Boolean);
		this.renderExecSteps(taskId, steps, members);
		this.persistExecState(taskId);
	}

	private renderExecSteps(taskId: string, steps: typeof EXEC_STEPS['default'], members: string[]) {
		const container = this.shadow.getElementById('execSteps')!;
		container.innerHTML = '';
		const states = this.execStepStates[taskId];
		let doneCount = 0;

		steps.forEach((step, i) => {
			const state = states[i] || 'pending';
			if (state === 'done') doneCount++;

			const div = document.createElement('div');
			div.className = 'exec-step ' + state;
			div.innerHTML = `
				<div class="exec-step-num">${state === 'done' ? '\u2713' : step.icon}</div>
				<div class="exec-step-content">
					<div class="exec-step-title">${step.title}</div>
					<div class="exec-step-desc">${step.desc}</div>
					${state === 'active' ? this.renderStepDetail(step.detail, members, taskId, i) : ''}
				</div>
			`;

			div.addEventListener('click', (e: MouseEvent) => {
				const tag = (e.target as HTMLElement).tagName;
				if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || tag === 'BUTTON') return;
				if ((e.target as HTMLElement).closest('[data-complete-step]')) return;
				if (states[i] === 'done') return;
				if (states[i] === 'active') {
					states[i] = 'pending';
				} else {
					Object.keys(states).forEach(k => { if (states[Number(k)] === 'active') states[Number(k)] = 'pending'; });
					states[i] = 'active';
				}
				this.renderExecSteps(taskId, steps, members);
				this.persistExecState(taskId);
			});

			container.appendChild(div);
		});

		// Bind action buttons via event delegation
		container.querySelectorAll('[data-complete-step]').forEach(btn => {
			btn.addEventListener('click', (e) => {
				e.stopPropagation();
				const idx = parseInt((btn as HTMLElement).dataset.completeStep!);
				this.completeExecStep(taskId, idx);
			});
		});

		const pct = steps.length > 0 ? (doneCount / steps.length * 100) : 0;
		(this.shadow.getElementById('execProgressFill') as HTMLElement).style.width = pct + '%';
		this.shadow.getElementById('execProgressText')!.textContent = doneCount + '/' + steps.length;
		(this.shadow.getElementById('execLaunch') as HTMLButtonElement).disabled = doneCount < steps.length;
	}

	// ── Launch handler ──

	private async handleExecLaunch() {
		const btn = this.shadow.getElementById('execLaunch') as HTMLButtonElement;
		const taskId = this._currentExecTaskId;

		// Check if connected commitments have cyclosMemberId
		const connectedCommitments = taskId
			? this.connections.filter(c => c.to === taskId).map(c => {
				const cn = this.weaveNodes.find(n => n.id === c.from);
				return cn?.data;
			}).filter(Boolean)
			: [];

		const cyclosCommitments = connectedCommitments.filter(c => {
			const member = this._cyclosMembers.find(m => m.name === c.memberName);
			return member?.id;
		});

		if (cyclosCommitments.length === 0 || this._cyclosMembers.length === 0) {
			// Demo mode — celebration
			btn.textContent = 'Launched!';
			btn.style.background = 'linear-gradient(135deg, #8b5cf6, #ec4899)';
			setTimeout(() => {
				this.shadow.getElementById('execOverlay')!.classList.remove('visible');
				btn.textContent = 'Launch Project'; btn.style.background = '';
			}, 1500);
			return;
		}

		// Cyclos mode
		btn.disabled = true;
		btn.textContent = 'Processing…';
		let successes = 0, failures = 0;

		for (const c of cyclosCommitments) {
			const member = this._cyclosMembers.find(m => m.name === c.memberName);
			if (!member) { failures++; continue; }
			try {
				const resp = await fetch(`${this.getApiBase()}/api/cyclos/commitments`, {
					method: 'POST',
					headers: { 'Content-Type': 'application/json', ...this.authHeaders() },
					body: JSON.stringify({ fromUserId: member.id, amount: c.hours, description: `${c.skill}: ${c.desc || 'commitment'}` }),
				});
				if (resp.ok) successes++; else failures++;
			} catch { failures++; }
		}

		if (failures > 0 && successes === 0) {
			btn.textContent = `Failed (${failures} errors)`;
			btn.style.background = '#ef4444';
		} else {
			btn.textContent = failures > 0 ? `Launched (${successes} ok, ${failures} failed)` : 'Launched!';
			btn.style.background = 'linear-gradient(135deg, #8b5cf6, #ec4899)';
		}
		setTimeout(() => {
			this.shadow.getElementById('execOverlay')!.classList.remove('visible');
			btn.textContent = 'Launch Project'; btn.style.background = ''; btn.disabled = false;
		}, 2000);
	}

	// ── Task editor ──

	private editingTaskNode: WeaveNode | null = null;

	private openTaskEditor(node: WeaveNode) {
		this.editingTaskNode = node;
		const t = node.data as TaskData;
		this.shadow.getElementById('taskEditTitle')!.textContent = 'Edit: ' + t.name;
		(this.shadow.getElementById('taskEditName') as HTMLInputElement).value = t.name;
		(this.shadow.getElementById('taskEditDesc') as HTMLTextAreaElement).value = t.description || '';
		(this.shadow.getElementById('taskEditNotes') as HTMLTextAreaElement).value = t.notes || '';
		this.renderTaskEditLinks(t.links || []);
		this.shadow.getElementById('taskEditOverlay')!.classList.add('visible');
	}

	private renderTaskEditLinks(links: { label: string; url: string }[]) {
		const container = this.shadow.getElementById('taskEditLinksContainer')!;
		container.innerHTML = '';
		links.forEach((link, i) => {
			const row = document.createElement('div');
			row.className = 'task-edit-link-row';
			row.innerHTML = `<input type="text" class="link-label" placeholder="Label" value="${link.label || ''}">
				<input type="url" class="link-url" placeholder="https://…" value="${link.url || ''}">
				<button class="task-edit-link-rm" data-link-idx="${i}">&times;</button>`;
			container.appendChild(row);
		});
		container.querySelectorAll('.task-edit-link-rm').forEach(btn => {
			btn.addEventListener('click', () => {
				const idx = parseInt((btn as HTMLElement).dataset.linkIdx!);
				const current = this.getTaskEditLinks();
				current.splice(idx, 1);
				this.renderTaskEditLinks(current);
			});
		});
	}

	private getTaskEditLinks(): { label: string; url: string }[] {
		const container = this.shadow.getElementById('taskEditLinksContainer')!;
		const rows = container.querySelectorAll('.task-edit-link-row');
		const links: { label: string; url: string }[] = [];
		rows.forEach(row => {
			const label = (row.querySelector('.link-label') as HTMLInputElement).value.trim();
			const url = (row.querySelector('.link-url') as HTMLInputElement).value.trim();
			if (label || url) links.push({ label, url });
		});
		return links;
	}

	private saveTaskEdit() {
		if (!this.editingTaskNode) return;
		const t = this.editingTaskNode.data as TaskData;
		t.name = (this.shadow.getElementById('taskEditName') as HTMLInputElement).value.trim() || t.name;
		t.description = (this.shadow.getElementById('taskEditDesc') as HTMLTextAreaElement).value.trim();
		t.notes = (this.shadow.getElementById('taskEditNotes') as HTMLTextAreaElement).value.trim();
		t.links = this.getTaskEditLinks();

		// Persist to server — uses compat PUT that splits rTasks title/desc + weaving overlay
		fetch(`${this.getApiBase()}/api/tasks/${t.id}`, {
			method: 'PUT',
			headers: { 'Content-Type': 'application/json', ...this.authHeaders() },
			body: JSON.stringify({ name: t.name, description: t.description, notes: t.notes, links: t.links }),
		}).catch(() => {});

		// Also persist position via overlay update
		fetch(`${this.getApiBase()}/api/weave/overlay/${t.id}`, {
			method: 'PUT',
			headers: { 'Content-Type': 'application/json', ...this.authHeaders() },
			body: JSON.stringify({ notes: t.notes, links: t.links }),
		}).catch(() => {});

		this.renderAll();
		this.shadow.getElementById('taskEditOverlay')!.classList.remove('visible');
		this.editingTaskNode = null;
	}

	/* ── Guided Tour ── */

	private _tour: import('../../../shared/tour-engine').TourEngine | null = null;

	private async _initTour() {
		if (this._tour) return;
		const { TourEngine } = await import('../../../shared/tour-engine');
		this._tour = new TourEngine(this.shadow as unknown as ShadowRoot, [
			{ target: '.form-panel-header', title: 'Pledge Time', message: 'Use the form to pledge your hours with a skill category. Commitments appear in the pool.' },
			{ target: '#inlineForm', title: 'Commitment Form', message: 'Fill in your name, skill, hours, and description, then click Pledge to Pool.' },
			{ target: '#pool-canvas', title: 'Commitment Pool', message: 'Each floating orb represents a time commitment — sized by hours, colored by skill.' },
			{ target: '.pool-legend', title: 'Community Stats', message: 'See total hours available and how many contributors are in the pool at a glance.' },
		], 'rtime_tour_done', () => this.shadow.querySelector('.main') as HTMLElement);
	}

	async startTour() {
		await this._initTour();
		this._tour?.start();
	}

	// ── Collaborate setup ──

	private setupCollaborate() {
		// Create Intent modal
		const intentModal = this.shadow.getElementById('intentModalOverlay')!;
		const createBtn = this.shadow.getElementById('createIntentBtn');
		const solverBtn = this.shadow.getElementById('runSolverBtn');

		createBtn?.addEventListener('click', () => {
			intentModal.classList.add('visible');
			(this.shadow.getElementById('intentDesc') as HTMLInputElement).value = '';
			(this.shadow.getElementById('intentHours') as HTMLInputElement).value = '2';
			this.updateIntentCost();
		});

		this.shadow.getElementById('intentCancel')?.addEventListener('click', () => {
			intentModal.classList.remove('visible');
		});

		// Type toggle
		this.shadow.querySelectorAll('.intent-type-btn').forEach(btn => {
			btn.addEventListener('click', () => {
				this.shadow.querySelectorAll('.intent-type-btn').forEach(b => b.classList.remove('active'));
				btn.classList.add('active');
				this.updateIntentCost();
			});
		});

		// Cost update on skill/hours change
		this.shadow.getElementById('intentSkill')?.addEventListener('change', () => this.updateIntentCost());
		this.shadow.getElementById('intentHours')?.addEventListener('input', () => this.updateIntentCost());

		// Submit intent
		this.shadow.getElementById('intentSubmit')?.addEventListener('click', () => this.submitIntent());

		// Run solver
		solverBtn?.addEventListener('click', () => this.triggerSolver());
	}

	private updateIntentCost() {
		const typeBtn = this.shadow.querySelector('.intent-type-btn.active') as HTMLElement;
		const type = typeBtn?.dataset.type || 'offer';
		const costDiv = this.shadow.getElementById('intentCost')!;
		const costVal = this.shadow.getElementById('intentCostValue')!;

		if (type === 'need') {
			const skill = (this.shadow.getElementById('intentSkill') as HTMLSelectElement).value;
			const hours = parseInt((this.shadow.getElementById('intentHours') as HTMLInputElement).value) || 1;
			const price = this.skillPrices[skill] || 100;
			costVal.textContent = String(price * hours);
			costDiv.style.display = 'block';
		} else {
			costDiv.style.display = 'none';
		}
	}

	private async submitIntent() {
		const typeBtn = this.shadow.querySelector('.intent-type-btn.active') as HTMLElement;
		const type = typeBtn?.dataset.type || 'offer';
		const skill = (this.shadow.getElementById('intentSkill') as HTMLSelectElement).value;
		const hours = parseInt((this.shadow.getElementById('intentHours') as HTMLInputElement).value) || 1;
		const description = (this.shadow.getElementById('intentDesc') as HTMLInputElement).value;

		try {
			const resp = await fetch(`${this.getApiBase()}/api/intent`, {
				method: 'POST',
				headers: { 'Content-Type': 'application/json', ...this.authHeaders() },
				body: JSON.stringify({ type, skill, hours, description }),
			});
			if (!resp.ok) {
				const err = await resp.json();
				alert(err.error || 'Failed to create intent');
				return;
			}
			this.shadow.getElementById('intentModalOverlay')!.classList.remove('visible');
			this.refreshCollaborate();
		} catch {
			alert('Failed to create intent');
		}
	}

	private async triggerSolver() {
		try {
			await fetch(`${this.getApiBase()}/api/solver/run`, { method: 'POST', headers: this.authHeaders() });
			this.refreshCollaborate();
		} catch {
			// ignore
		}
	}

	private async refreshCollaborate() {
		const base = this.getApiBase();
		try {
			const [iResp, sResp, cResp] = await Promise.all([
				fetch(`${base}/api/intents`),
				fetch(`${base}/api/solver-results`),
				fetch(`${base}/api/skill-curves`),
			]);
			if (iResp.ok) {
				const data = await iResp.json();
				this.intents = data.intents || [];
			}
			if (sResp.ok) {
				const data = await sResp.json();
				this.solverResults = data.results || [];
			}
			if (cResp.ok) {
				const data = await cResp.json();
				this.skillPrices = data.prices || {};
			}
		} catch {
			// offline
		}

		// Build member intent status map for pool view status rings
		this.memberIntentStatus.clear();
		for (const intent of this.intents) {
			if (intent.status !== 'active' && intent.status !== 'matched') continue;
			const existing = this.memberIntentStatus.get(intent.memberName);
			if (intent.status === 'matched') {
				this.memberIntentStatus.set(intent.memberName, 'matched');
			} else if (!existing) {
				this.memberIntentStatus.set(intent.memberName, intent.type);
			}
		}

		this.renderIntentsList();
		this.renderSolverResults();
		this.renderSkillPrices();
	}

	private renderIntentsList() {
		const container = this.shadow.getElementById('collabIntents');
		if (!container) return;

		const activeIntents = this.intents.filter(i => i.status === 'active' || i.status === 'matched');
		if (activeIntents.length === 0) {
			container.innerHTML = '<div class="collab-empty">No active intents yet. Create an offer or need to get started.</div>';
			return;
		}

		container.innerHTML = activeIntents.map(intent => {
			const color = SKILL_COLORS[intent.skill] || '#8b5cf6';
			const typeLabel = intent.type === 'offer' ? 'OFFER' : 'NEED';
			const typeClass = intent.type === 'offer' ? 'intent-offer' : 'intent-need';
			return `
				<div class="intent-card ${typeClass}">
					<div class="intent-card-header">
						<span class="intent-type-badge ${typeClass}">${typeLabel}</span>
						<span class="intent-skill-badge" style="background:${color}20;color:${color}">${SKILL_LABELS[intent.skill] || intent.skill}</span>
						<span class="intent-hours">${intent.hours}h</span>
						<span class="intent-status-badge intent-status-${intent.status}">${intent.status}</span>
					</div>
					<div class="intent-card-body">
						<div class="intent-member">${intent.memberName}</div>
						<div class="intent-desc">${intent.description || 'No description'}</div>
					</div>
				</div>
			`;
		}).join('');
	}

	private renderSolverResults() {
		const container = this.shadow.getElementById('collabResults');
		if (!container) return;

		if (this.solverResults.length === 0) {
			container.innerHTML = '<div class="collab-empty">No solver results yet. Create intents and run the solver to find matches.</div>';
			return;
		}

		container.innerHTML = this.solverResults.map(result => {
			const scorePercent = Math.round(result.score * 100);
			const skills = (result.skills || []).map((s: string) => {
				const color = SKILL_COLORS[s] || '#8b5cf6';
				return `<span class="intent-skill-badge" style="background:${color}20;color:${color}">${SKILL_LABELS[s] || s}</span>`;
			}).join('');

			const acceptCount = Object.values(result.acceptances || {}).filter(Boolean).length;
			const totalMembers = (result.members || []).length;

			return `
				<div class="solver-result-card">
					<div class="solver-result-header">
						<div class="solver-score">
							<div class="solver-score-ring" style="--score: ${scorePercent}%">
								<span>${scorePercent}</span>
							</div>
						</div>
						<div class="solver-result-info">
							<div class="solver-skills">${skills}</div>
							<div class="solver-meta">${result.totalHours}h total · ${totalMembers} members · ${acceptCount}/${totalMembers} accepted</div>
						</div>
					</div>
					<div class="solver-result-members">
						${(result.members || []).map((m: string) => {
							const accepted = result.acceptances?.[m];
							const icon = accepted ? '✓' : '○';
							const cls = accepted ? 'accepted' : 'pending';
							return `<span class="solver-member ${cls}">${icon} ${m.split(':').pop()}</span>`;
						}).join('')}
					</div>
					<div class="solver-result-actions">
						<button class="solver-accept-btn" data-result-id="${result.id}">Accept</button>
						<button class="solver-reject-btn" data-result-id="${result.id}">Reject</button>
						<button class="solver-frame-btn" data-result-id="${result.id}" title="Create tasks from this collaboration on the canvas">Frame as Tasks</button>
					</div>
				</div>
			`;
		}).join('');

		// Bind accept/reject buttons
		container.querySelectorAll('.solver-accept-btn').forEach(btn => {
			btn.addEventListener('click', async () => {
				const resultId = (btn as HTMLElement).dataset.resultId;
				try {
					await fetch(`${this.getApiBase()}/api/solver-results/${resultId}/accept`, { method: 'POST', headers: this.authHeaders() });
					this.refreshCollaborate();
				} catch { /* ignore */ }
			});
		});

		container.querySelectorAll('.solver-reject-btn').forEach(btn => {
			btn.addEventListener('click', async () => {
				const resultId = (btn as HTMLElement).dataset.resultId;
				try {
					await fetch(`${this.getApiBase()}/api/solver-results/${resultId}/reject`, { method: 'POST', headers: this.authHeaders() });
					this.refreshCollaborate();
				} catch { /* ignore */ }
			});
		});

		// Frame as Tasks buttons
		container.querySelectorAll('.solver-frame-btn').forEach(btn => {
			btn.addEventListener('click', async () => {
				const resultId = (btn as HTMLElement).dataset.resultId;
				const result = this.solverResults.find(r => r.id === resultId);
				if (!result) return;
				await this.frameResultAsTasks(result);
			});
		});
	}

	/** Create tasks from a solver result and lay them out on the canvas with a frame. */
	private async frameResultAsTasks(result: any) {
		const skills = result.skills || [];
		const skillPairs = skills.length > 0 ? skills : ['facilitation'];

		// Group skills into task(s) — one task per 2 skills, or one per unique group
		const taskDefs: { title: string; needs: Record<string, number> }[] = [];
		if (skillPairs.length <= 2) {
			taskDefs.push({
				title: `Collaboration: ${skillPairs.map((s: string) => SKILL_LABELS[s] || s).join(' + ')}`,
				needs: Object.fromEntries(skillPairs.map((s: string) => [s, Math.ceil((result.totalHours || 4) / skillPairs.length)])),
			});
		} else {
			for (let i = 0; i < skillPairs.length; i += 2) {
				const group = skillPairs.slice(i, i + 2);
				taskDefs.push({
					title: `${group.map((s: string) => SKILL_LABELS[s] || s).join(' + ')} Work`,
					needs: Object.fromEntries(group.map((s: string) => [s, Math.ceil((result.totalHours || 4) / skillPairs.length)])),
				});
			}
		}

		// POST tasks via create-and-place to create rTasks item + weaving overlay atomically
		const wrap = this.shadow.getElementById('canvasWrap');
		const wrapRect = wrap?.getBoundingClientRect();
		const startX = wrapRect ? wrapRect.width * 0.15 : 100;
		const startY = wrapRect ? wrapRect.height * 0.3 : 150;
		const gap = TASK_W + 40;

		const createdTasks: TaskData[] = [];
		for (let i = 0; i < taskDefs.length; i++) {
			const def = taskDefs[i];
			const canvasX = startX + i * gap;
			const canvasY = startY;
			try {
				const resp = await fetch(`${this.getApiBase()}/api/weave/create-and-place`, {
					method: 'POST',
					headers: { 'Content-Type': 'application/json', ...this.authHeaders() },
					body: JSON.stringify({
						title: def.title,
						description: `From solver result ${result.id}`,
						needs: def.needs,
						canvasX,
						canvasY,
						intentFrameId: result.id,
					}),
				});
				if (resp.ok) {
					const data = await resp.json();
					const task: TaskData = {
						id: data.taskId,
						name: def.title,
						description: `From solver result ${result.id}`,
						needs: def.needs,
						links: [],
						notes: '',
						fulfilled: {},
						canvasX,
						canvasY,
					};
					createdTasks.push(task);
					this.tasks.push(task);
				}
			} catch { /* offline */ }
		}

		if (createdTasks.length === 0) return;

		// Place task nodes on canvas using their stored positions
		createdTasks.forEach((t) => {
			this.weaveNodes.push(this.mkTaskNode(t, t.canvasX ?? 400, t.canvasY ?? 150));
		});

		// Draw dashed frame around the group in intentFramesLayer
		const padding = 30;
		const frameX = startX - padding;
		const frameY = startY - padding - 20;
		const frameW = createdTasks.length * gap - 40 + TASK_W + padding;
		const frameH = 200 + padding * 2;

		const frame = ns('rect');
		frame.setAttribute('x', String(frameX));
		frame.setAttribute('y', String(frameY));
		frame.setAttribute('width', String(frameW));
		frame.setAttribute('height', String(frameH));
		frame.setAttribute('rx', '12');
		frame.setAttribute('fill', 'none');
		frame.setAttribute('stroke', '#fbbf2466');
		frame.setAttribute('stroke-width', '2');
		frame.setAttribute('stroke-dasharray', '8 4');
		frame.setAttribute('class', 'intent-frame');
		this.intentFramesLayer.appendChild(frame);

		// Frame label
		const label = svgText(
			`Solver: ${(result.skills || []).map((s: string) => SKILL_LABELS[s] || s).join(', ')}`,
			frameX + 12, frameY + 16, 11, '#fbbf24', '500'
		);
		label.setAttribute('class', 'intent-frame-label');
		this.intentFramesLayer.appendChild(label);

		this.renderAll();
		this.rebuildSidebar();

		// Switch to canvas view
		this._switchView('canvas');
	}

	private renderSkillPrices() {
		const container = this.shadow.getElementById('collabPrices');
		if (!container) return;

		const skills = ['facilitation', 'design', 'tech', 'outreach', 'logistics'];
		container.innerHTML = skills.map(skill => {
			const color = SKILL_COLORS[skill] || '#8b5cf6';
			const price = this.skillPrices[skill] || 100;
			return `
				<div class="price-card">
					<div class="price-dot" style="background:${color}"></div>
					<span class="price-skill">${SKILL_LABELS[skill] || skill}</span>
					<span class="price-value">${price} tok/h</span>
				</div>
			`;
		}).join('');
	}

	// ── Fulfillment Dashboard ──

	private async refreshDashboard() {
		const base = this.getApiBase();
		const summaryEl = this.shadow.getElementById('dashboardSummary');
		const logsEl = this.shadow.getElementById('dashboardLogs');
		const skillsEl = this.shadow.getElementById('dashboardSkills');

		try {
			const [logsResp, repResp] = await Promise.all([
				fetch(`${base}/api/external-time-logs`),
				fetch(`${base}/api/reputation/self`).catch(() => null),
			]);

			if (!logsResp.ok) {
				if (summaryEl) summaryEl.innerHTML = '<div class="collab-empty">No fulfillment data available.</div>';
				return;
			}

			const logsData = await logsResp.json();
			const logs: Array<{
				id: string;
				backlogTaskId: string;
				backlogTaskTitle: string;
				hours: number;
				skill: string;
				status: string;
				note?: string;
				loggedAt: number;
				commitmentId?: string;
			}> = logsData.logs || [];

			// Summary stats
			const totalHours = logs.reduce((s, l) => s + l.hours, 0);
			const settledHours = logs.filter(l => l.status === 'settled').reduce((s, l) => s + l.hours, 0);
			const pendingCount = logs.filter(l => l.status !== 'settled').length;

			if (summaryEl) {
				summaryEl.innerHTML = `
					<div style="display:flex;gap:2rem;padding:0.5rem 0">
						<div class="price-card">
							<span class="price-skill">Total Hours</span>
							<span class="price-value" style="font-size:1.4rem">${totalHours.toFixed(1)}h</span>
						</div>
						<div class="price-card">
							<span class="price-skill">Settled</span>
							<span class="price-value" style="font-size:1.4rem;color:#10b981">${settledHours.toFixed(1)}h</span>
						</div>
						<div class="price-card">
							<span class="price-skill">Pending</span>
							<span class="price-value" style="font-size:1.4rem;color:#f59e0b">${pendingCount}</span>
						</div>
					</div>
				`;
			}

			// Logs table
			if (logsEl) {
				if (logs.length === 0) {
					logsEl.innerHTML = '<div class="collab-empty">No time logs imported yet. Use <code>backlog task log --push</code> to send entries.</div>';
				} else {
					logsEl.innerHTML = `
						<table style="width:100%;border-collapse:collapse;font-size:0.85rem">
							<thead>
								<tr style="border-bottom:1px solid rgba(255,255,255,0.1);text-align:left">
									<th style="padding:0.4rem">Task</th>
									<th style="padding:0.4rem">Skill</th>
									<th style="padding:0.4rem">Hours</th>
									<th style="padding:0.4rem">Status</th>
									<th style="padding:0.4rem">Action</th>
								</tr>
							</thead>
							<tbody>
								${logs.map(log => {
									const color = SKILL_COLORS[log.skill] || '#8b5cf6';
									const statusColor = log.status === 'settled' ? '#10b981' : log.status === 'commitment_created' ? '#3b82f6' : '#f59e0b';
									const settleBtn = log.status !== 'settled'
										? `<button class="dashboard-settle-btn" data-log-id="${log.id}" style="padding:2px 8px;font-size:0.75rem;border:1px solid #10b981;background:transparent;color:#10b981;border-radius:4px;cursor:pointer">Settle</button>`
										: '<span style="color:#10b981">✓</span>';
									return `
										<tr style="border-bottom:1px solid rgba(255,255,255,0.05)">
											<td style="padding:0.4rem">${log.backlogTaskTitle || log.backlogTaskId}</td>
											<td style="padding:0.4rem"><span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:${color};margin-right:4px"></span>${SKILL_LABELS[log.skill] || log.skill}</td>
											<td style="padding:0.4rem">${log.hours.toFixed(1)}h</td>
											<td style="padding:0.4rem;color:${statusColor}">${log.status}</td>
											<td style="padding:0.4rem">${settleBtn}</td>
										</tr>
									`;
								}).join('')}
							</tbody>
						</table>
					`;

					// Bind settle buttons
					logsEl.querySelectorAll('.dashboard-settle-btn').forEach(btn => {
						btn.addEventListener('click', async () => {
							const logId = (btn as HTMLElement).dataset.logId;
							if (!logId) return;
							const token = localStorage.getItem('rspace_token') || '';
							const resp = await fetch(`${base}/api/external-time-logs/${logId}/settle`, {
								method: 'POST',
								headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
							});
							if (resp.ok) {
								this.refreshDashboard();
							}
						});
					});
				}
			}

			// Skill totals
			if (skillsEl) {
				const skillTotals = new Map<string, { total: number; settled: number }>();
				for (const log of logs) {
					const existing = skillTotals.get(log.skill) || { total: 0, settled: 0 };
					existing.total += log.hours;
					if (log.status === 'settled') existing.settled += log.hours;
					skillTotals.set(log.skill, existing);
				}

				if (skillTotals.size === 0) {
					skillsEl.innerHTML = '<div class="collab-empty">No skill data yet.</div>';
				} else {
					skillsEl.innerHTML = [...skillTotals.entries()]
						.sort((a, b) => b[1].total - a[1].total)
						.map(([skill, data]) => {
							const color = SKILL_COLORS[skill] || '#8b5cf6';
							const pct = data.total > 0 ? (data.settled / data.total * 100) : 0;
							return `
								<div style="display:flex;align-items:center;gap:0.6rem;padding:0.4rem 0">
									<div class="price-dot" style="background:${color}"></div>
									<span style="width:6rem">${SKILL_LABELS[skill] || skill}</span>
									<div style="flex:1;height:8px;background:rgba(255,255,255,0.1);border-radius:4px;overflow:hidden">
										<div style="width:${pct}%;height:100%;background:${color};border-radius:4px"></div>
									</div>
									<span style="font-size:0.8rem;color:#94a3b8;min-width:5rem;text-align:right">${data.settled.toFixed(1)}h / ${data.total.toFixed(1)}h</span>
								</div>
							`;
						}).join('');
				}
			}
		} catch {
			if (summaryEl) summaryEl.innerHTML = '<div class="collab-empty">Unable to load fulfillment data (offline?).</div>';
		}
	}
}

// ── CSS ──

const CSS_TEXT = `
:host {
	display: flex;
	flex-direction: column;
	height: 100%;
	font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
	color: #e2e8f0;
	background: #0f172a;
	line-height: 1.6;
	overflow: hidden;
}

.pool-legend {
	padding: 0.5rem 0.75rem;
	background: rgba(30, 27, 75, 0.6);
	border-top: 1px solid #334155;
	flex-shrink: 0;
}
.pool-legend-stats {
	display: flex; align-items: center; gap: 0.4rem;
	font-size: 0.78rem; color: #94a3b8; margin-bottom: 0.3rem;
}
.pool-legend-stats strong { font-weight: 700; color: #f1f5f9; }
.pool-legend-sep { color: #475569; }
.pool-legend-skills { display: flex; gap: 0.6rem; flex-wrap: wrap; }
.skill-legend { display: flex; gap: 0.6rem; flex-wrap: wrap; }
.skill-legend-item { display: flex; align-items: center; gap: 0.25rem; font-size: 0.7rem; color: #94a3b8; }
.skill-legend-dot { width: 7px; height: 7px; border-radius: 50%; }

.main { flex: 1; position: relative; overflow: hidden; }

/* Unified canvas view: form panel + pool visualization side by side */
#canvas-view {
	display: flex;
	width: 100%; height: 100%;
	position: absolute; top: 0; left: 0;
	overflow: hidden;
}

/* Weave view (full screen SVG canvas, toggled from pool) */
#weave-view {
	display: none;
	flex-direction: column;
	width: 100%; height: 100%;
	position: absolute; top: 0; left: 0;
	overflow: hidden;
}
.weave-view-header {
	display: flex;
	align-items: center;
	gap: 1rem;
	padding: 0.5rem 0.75rem;
	font-size: 0.82rem; font-weight: 600; color: #94a3b8;
	text-transform: uppercase; letter-spacing: 0.04em;
	border-bottom: 1px solid #334155; flex-shrink: 0;
	background: #1e293b;
}
.weave-back-btn {
	background: none; border: 1px solid #475569; border-radius: 0.375rem;
	color: #94a3b8; font-size: 0.82rem; cursor: pointer; padding: 0.3rem 0.75rem;
	transition: all 0.15s; text-transform: none; letter-spacing: normal;
}
.weave-back-btn:hover { border-color: #8b5cf6; color: #e2e8f0; }

/* Form panel (left side) — commitment entry */
.form-panel {
	min-width: 240px;
	flex-shrink: 0;
	background: #1e293b;
	border-right: 1px solid #334155;
	display: flex;
	flex-direction: column;
	overflow: hidden;
	position: relative;
	transition: width 0.2s ease;
}
.form-panel-header {
	display: flex;
	justify-content: space-between;
	align-items: center;
	padding: 0.5rem 0.75rem;
	font-size: 0.82rem;
	font-weight: 600;
	color: #94a3b8;
	text-transform: uppercase;
	letter-spacing: 0.04em;
	border-bottom: 1px solid #334155;
	flex-shrink: 0;
}
.form-panel-header button {
	background: none; border: 1px solid #475569; border-radius: 0.25rem;
	color: #94a3b8; font-size: 0.85rem; cursor: pointer; padding: 0.1rem 0.4rem;
	line-height: 1; transition: border-color 0.15s;
}
.form-panel-header button:hover { border-color: #8b5cf6; color: #e2e8f0; }

/* Inline commitment form */
.inline-form {
	padding: 0.75rem;
	border-bottom: 1px solid #334155;
	flex-shrink: 0;
}
.inline-form-field {
	margin-bottom: 0.5rem;
}
.inline-form-field label {
	display: block;
	font-size: 0.72rem;
	font-weight: 600;
	color: #94a3b8;
	text-transform: uppercase;
	letter-spacing: 0.03em;
	margin-bottom: 0.2rem;
}
.inline-form-field input,
.inline-form-field select {
	width: 100%;
	padding: 0.4rem 0.55rem;
	background: #0f172a;
	border: 1px solid #334155;
	border-radius: 0.375rem;
	color: #e2e8f0;
	font-size: 0.82rem;
	transition: border-color 0.15s;
	box-sizing: border-box;
}
.inline-form-field input:focus,
.inline-form-field select:focus {
	outline: none;
	border-color: #8b5cf6;
}
.inline-form-row {
	display: flex;
	gap: 0.5rem;
}
.pledge-btn {
	width: 100%;
	padding: 0.5rem 0.75rem;
	margin-top: 0.25rem;
	background: linear-gradient(135deg, #8b5cf6, #ec4899);
	color: #fff;
	border: none;
	border-radius: 0.5rem;
	font-size: 0.82rem;
	font-weight: 600;
	cursor: pointer;
	box-shadow: 0 4px 16px rgba(139,92,246,0.3);
	transition: all 0.2s;
}
.pledge-btn:hover { transform: translateY(-1px); box-shadow: 0 6px 20px rgba(139,92,246,0.4); }

/* Commitments list */
.commitments-list-header {
	display: flex;
	justify-content: space-between;
	align-items: center;
	padding: 0.5rem 0.75rem;
	font-size: 0.72rem;
	font-weight: 600;
	color: #64748b;
	text-transform: uppercase;
	letter-spacing: 0.04em;
	border-bottom: 1px solid #334155;
	flex-shrink: 0;
}
.commitments-count {
	background: #334155;
	color: #94a3b8;
	font-size: 0.68rem;
	padding: 0.1rem 0.4rem;
	border-radius: 0.25rem;
	min-width: 1.2rem;
	text-align: center;
}
.commitments-list {
	flex: 1;
	overflow-y: auto;
	padding: 0.25rem;
}
.cl-item {
	display: flex;
	align-items: flex-start;
	gap: 0.5rem;
	padding: 0.5rem 0.5rem;
	border-radius: 0.375rem;
	transition: background 0.15s;
	border-bottom: 1px solid rgba(51,65,85,0.4);
}
.cl-item:hover { background: rgba(51,65,85,0.3); }
.cl-dot {
	width: 8px; height: 8px;
	border-radius: 50%;
	flex-shrink: 0;
	margin-top: 0.35rem;
}
.cl-info { flex: 1; min-width: 0; }
.cl-name { font-size: 0.82rem; font-weight: 600; color: #f1f5f9; }
.cl-meta { font-size: 0.72rem; color: #94a3b8; }
.cl-woven { color: #8b5cf6; font-weight: 600; }
.cl-desc { font-size: 0.72rem; color: #64748b; margin-top: 0.15rem; line-height: 1.4; }

/* Pool panel (right side) — visualization */
.pool-panel {
	flex: 1;
	background: #0f172a;
	display: flex;
	flex-direction: column;
	overflow: hidden;
	position: relative;
}
.pool-panel-header {
	display: flex;
	justify-content: space-between;
	align-items: center;
	padding: 0.5rem 0.75rem;
	font-size: 0.82rem;
	font-weight: 600;
	color: #94a3b8;
	text-transform: uppercase;
	letter-spacing: 0.04em;
	border-bottom: 1px solid #334155;
	flex-shrink: 0;
	background: #1e293b;
}
.weave-toggle-btn {
	background: none; border: 1px solid #475569; border-radius: 0.375rem;
	color: #94a3b8; font-size: 0.78rem; cursor: pointer; padding: 0.25rem 0.6rem;
	transition: all 0.15s; text-transform: none; letter-spacing: normal;
}
.weave-toggle-btn:hover { border-color: #8b5cf6; color: #e2e8f0; }
#pool-canvas { flex: 1; min-height: 0; display: block; cursor: default; touch-action: none; }

.pool-detail {
	position: absolute;
	background: #1e293b;
	border-radius: 0.75rem;
	box-shadow: 0 8px 32px rgba(0,0,0,0.4);
	padding: 1rem;
	min-width: 200px;
	pointer-events: none;
	opacity: 0;
	transform: scale(0.9) translateY(8px);
	transition: opacity 0.2s, transform 0.2s;
	z-index: 50;
}
.pool-detail.visible { opacity: 1; transform: scale(1) translateY(0); pointer-events: auto; }
.pool-detail-header { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.4rem; }
.pool-detail-dot { width: 10px; height: 10px; border-radius: 50%; }
.pool-detail-name { font-weight: 600; font-size: 0.9rem; color: #f1f5f9; }
.pool-detail-skill { font-size: 0.78rem; color: #94a3b8; margin-bottom: 0.2rem; }
.pool-detail-hours { font-size: 0.82rem; font-weight: 600; color: #8b5cf6; }
.pool-detail-desc { font-size: 0.75rem; color: #94a3b8; margin-top: 0.25rem; line-height: 1.4; }
.pool-detail-woven {
	font-size: 0.72rem; color: #8b5cf6; font-weight: 600;
	margin-top: 0.35rem; padding: 0.2rem 0.5rem;
	background: rgba(139,92,246,0.1); border-radius: 0.25rem;
	display: inline-block;
}
/* Pool header hint */
.pool-hint {
	font-size: 0.68rem; font-weight: 400; color: #64748b;
	font-style: italic; margin-left: auto; margin-right: 0.5rem;
}

/* Resizable divider */
.panel-divider {
	width: 6px;
	flex-shrink: 0;
	background: #1e293b;
	border-left: 1px solid #334155;
	border-right: 1px solid #334155;
	cursor: ew-resize;
	display: flex;
	align-items: center;
	justify-content: center;
	transition: background 0.15s;
	touch-action: none;
	z-index: 10;
}
.panel-divider:hover, .panel-divider.active {
	background: #8b5cf6;
	border-color: #8b5cf6;
}
.divider-pip {
	width: 2px; height: 24px;
	border-radius: 1px;
	background: #475569;
	pointer-events: none;
}
.panel-divider:hover .divider-pip, .panel-divider.active .divider-pip {
	background: #fff;
}

.pool-panel-sidebar { flex-shrink: 0; overflow-y: auto; max-height: 200px; border-top: 1px solid #334155; }

/* Legacy .add-btn kept for modal fallback */

.sidebar-item-info { flex: 1; min-width: 0; }
.sidebar-item-name { font-size: 0.82rem; font-weight: 600; color: #f1f5f9; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sidebar-item-meta { font-size: 0.72rem; color: #64748b; }

.sidebar-section {
	padding: 0.5rem 0.75rem;
	font-size: 0.72rem;
	font-weight: 600;
	color: #64748b;
	text-transform: uppercase;
	letter-spacing: 0.04em;
}
.sidebar-task {
	display: flex;
	align-items: center;
	gap: 0.5rem;
	padding: 0.45rem 0.65rem;
	border-radius: 0.375rem;
	cursor: grab;
	transition: background 0.15s;
	margin: 0 0.25rem 0.2rem;
	touch-action: none;
}
.sidebar-task:hover { background: #334155; }
.sidebar-task-icon {
	width: 22px; height: 22px;
	border-radius: 0.25rem;
	background: linear-gradient(135deg, #8b5cf6, #ec4899);
	display: flex; align-items: center; justify-content: center;
	color: #fff; font-size: 0.65rem; flex-shrink: 0;
}

/* SVG canvas area */
.canvas-wrap { flex: 1; position: relative; overflow: hidden; background: #0f172a; touch-action: none; display: flex; flex-direction: column; }
.canvas-wrap-header {
	display: none;
	justify-content: space-between; align-items: center;
	padding: 0.5rem 0.75rem;
	font-size: 0.82rem; font-weight: 600; color: #94a3b8;
	text-transform: uppercase; letter-spacing: 0.04em;
	border-bottom: 1px solid #334155; flex-shrink: 0;
	background: #1e293b;
}
.canvas-wrap-hint {
	font-size: 0.68rem; font-weight: 400; color: #64748b; font-style: italic;
	text-transform: none; letter-spacing: normal;
}
#weave-svg { width: 100%; flex: 1; min-height: 0; display: block; touch-action: none; }

/* Zoom controls */
.zoom-controls {
	position: absolute;
	bottom: 1rem;
	right: 1rem;
	z-index: 50;
	display: flex;
	flex-direction: column;
	gap: 0.25rem;
}
.zoom-controls button {
	width: 32px; height: 32px;
	border-radius: 0.375rem;
	border: 1px solid #475569;
	background: #1e293b;
	color: #e2e8f0;
	font-size: 1rem;
	cursor: pointer;
	display: flex; align-items: center; justify-content: center;
	transition: all 0.15s;
}
.zoom-controls button:hover { border-color: #8b5cf6; background: #334155; }

.node-group { cursor: grab; }
.node-group:active { cursor: grabbing; }
.node-rect { fill: #1e293b; stroke: #334155; stroke-width: 1; transition: stroke 0.15s; }
.node-group:hover .node-rect { stroke: #8b5cf6; }
.port { cursor: crosshair; transition: r 0.15s; touch-action: none; }
.port:hover { r: 7; }
.connection-line { fill: none; stroke: #475569; stroke-width: 2; pointer-events: none; }
.connection-line.active { stroke: #8b5cf6; stroke-width: 2.5; }
.temp-connection { fill: none; stroke: #8b5cf6; stroke-width: 2; stroke-dasharray: 6 4; pointer-events: none; opacity: 0.6; }
.task-node .node-rect { stroke: #8b5cf6; stroke-width: 1.5; }
.task-node.ready .node-rect { stroke: #10b981; stroke-width: 2; }
.task-node.skill-match .node-rect { stroke: #fbbf24; stroke-width: 2.5; filter: url(#glowGold); }

/* Dependency ports */
.dep-port { cursor: crosshair; touch-action: none; }
.dep-port:hover + .dep-port-diamond { fill: #8b5cf6; stroke: #a78bfa; }
.dep-port-diamond { transition: fill 0.15s; }
.dep-connection { pointer-events: none; }
.woven-connection { pointer-events: none; }

/* Project frames */
.project-frame-group { cursor: grab; }
.project-frame-group:active { cursor: grabbing; }
.project-frame-rect { transition: stroke 0.15s; }
.project-frame-group:hover .project-frame-rect { stroke-width: 2.5; }
.frame-resize-handle { cursor: nwse-resize; }

/* Intent frames */
.intent-frame { pointer-events: none; }
.intent-frame-label { pointer-events: none; }

.exec-btn-rect { cursor: pointer; transition: opacity 0.15s; }
.exec-btn-rect:hover { opacity: 0.85; }
.approve-btn, .decline-btn { cursor: pointer; transition: opacity 0.15s; }
.approve-btn:hover, .decline-btn:hover { opacity: 0.8; }

/* Modals & overlays */
.modal-overlay, .exec-overlay, .task-edit-overlay {
	position: absolute; inset: 0;
	background: rgba(0,0,0,0.7);
	z-index: 200;
	display: none;
	align-items: center;
	justify-content: center;
}
.modal-overlay.visible, .exec-overlay.visible, .task-edit-overlay.visible { display: flex; }

.modal {
	background: #1e293b;
	border-radius: 1rem;
	padding: 1.75rem;
	width: 380px;
	max-width: 90vw;
	box-shadow: 0 16px 64px rgba(0,0,0,0.5);
}
.modal h3 { font-size: 1.1rem; font-weight: 700; color: #f1f5f9; margin-bottom: 1rem; }
.modal-field { margin-bottom: 0.85rem; }
.modal-field label { display: block; font-size: 0.8rem; font-weight: 500; color: #94a3b8; margin-bottom: 0.25rem; }
.modal-field input, .modal-field select {
	width: 100%;
	padding: 0.55rem 0.75rem;
	border: 1px solid #475569;
	border-radius: 0.375rem;
	font-size: 0.9rem;
	outline: none;
	transition: border-color 0.15s;
	background: #0f172a;
	color: #e2e8f0;
}
.modal-field input:focus, .modal-field select:focus { border-color: #8b5cf6; }
.modal-actions { display: flex; gap: 0.5rem; justify-content: flex-end; margin-top: 1.25rem; }
.modal-cancel {
	padding: 0.5rem 1rem;
	border: 1px solid #334155;
	border-radius: 0.375rem;
	background: #1e293b;
	color: #94a3b8;
	font-size: 0.85rem;
	cursor: pointer;
}
.modal-submit {
	padding: 0.5rem 1.25rem;
	background: linear-gradient(135deg, #8b5cf6, #ec4899);
	color: #fff;
	border: none;
	border-radius: 0.375rem;
	font-size: 0.85rem;
	font-weight: 500;
	cursor: pointer;
}

/* Exec panel */
.exec-overlay { z-index: 300; }
.exec-panel {
	background: #1e293b;
	border-radius: 1rem;
	width: 520px;
	max-width: 95vw;
	max-height: 90vh;
	overflow-y: auto;
	box-shadow: 0 16px 64px rgba(0,0,0,0.5);
}
.exec-panel-header {
	padding: 1.5rem 1.75rem 1rem;
	border-bottom: 1px solid #334155;
}
.exec-panel-header h2 { font-size: 1.15rem; font-weight: 700; color: #f1f5f9; margin-bottom: 0.2rem; }
.exec-panel-header p { font-size: 0.85rem; color: #64748b; }
.exec-panel-badge {
	display: inline-block;
	padding: 0.15rem 0.6rem;
	border-radius: 1rem;
	background: rgba(16,185,129,0.15);
	color: #10b981;
	font-size: 0.75rem;
	font-weight: 600;
	margin-left: 0.5rem;
	vertical-align: middle;
}
.exec-steps { padding: 0.75rem 1.75rem 1.5rem; }
.exec-step {
	display: flex;
	gap: 1rem;
	padding: 1rem 0.5rem;
	border-bottom: 1px solid #1e293b;
	align-items: flex-start;
	cursor: pointer;
	transition: background 0.1s;
	border-radius: 0.5rem;
}
.exec-step:hover { background: #0f172a; }
.exec-step:last-child { border-bottom: none; }
.exec-step.active .exec-step-num { background: linear-gradient(135deg, #8b5cf6, #ec4899); color: #fff; }
.exec-step.done .exec-step-num { background: #10b981; color: #fff; }
.exec-step-num {
	width: 32px; height: 32px; min-width: 32px;
	border-radius: 50%;
	background: #334155;
	color: #64748b;
	display: flex; align-items: center; justify-content: center;
	font-size: 0.82rem; font-weight: 600;
	transition: all 0.2s;
}
.exec-step-content { flex: 1; }
.exec-step-title { font-size: 0.92rem; font-weight: 600; color: #f1f5f9; margin-bottom: 0.15rem; }
.exec-step.done .exec-step-title { color: #10b981; }
.exec-step-desc { font-size: 0.8rem; color: #64748b; line-height: 1.4; }
.exec-panel-footer {
	padding: 1rem 1.75rem;
	border-top: 1px solid #334155;
	display: flex;
	justify-content: space-between;
	align-items: center;
}
.exec-close {
	padding: 0.45rem 1rem;
	border: 1px solid #334155;
	border-radius: 0.375rem;
	background: #1e293b;
	color: #94a3b8;
	font-size: 0.85rem;
	cursor: pointer;
}
.exec-launch {
	padding: 0.5rem 1.5rem;
	background: linear-gradient(135deg, #10b981, #059669);
	color: #fff;
	border: none;
	border-radius: 0.375rem;
	font-size: 0.85rem;
	font-weight: 600;
	cursor: pointer;
}
.exec-launch:disabled { opacity: 0.5; cursor: not-allowed; }
.exec-progress { display: flex; gap: 0.35rem; align-items: center; font-size: 0.78rem; color: #64748b; }
.exec-progress-bar { width: 80px; height: 4px; background: #334155; border-radius: 2px; overflow: hidden; }
.exec-progress-fill { height: 100%; background: linear-gradient(90deg, #8b5cf6, #10b981); border-radius: 2px; transition: width 0.3s; }

/* Task editor */
.task-edit-overlay { z-index: 250; }
.task-edit-panel {
	background: #1e293b;
	border-radius: 1rem;
	width: 480px;
	max-width: 95vw;
	max-height: 90vh;
	overflow-y: auto;
	box-shadow: 0 16px 64px rgba(0,0,0,0.5);
}
.task-edit-header { padding: 1.25rem 1.5rem 0.75rem; border-bottom: 1px solid #334155; }
.task-edit-header h3 { font-size: 1.05rem; font-weight: 700; color: #f1f5f9; }
.task-edit-body { padding: 1rem 1.5rem; }
.task-edit-field { margin-bottom: 0.85rem; }
.task-edit-field label { display: block; font-size: 0.78rem; font-weight: 500; color: #94a3b8; margin-bottom: 0.25rem; }
.task-edit-field input, .task-edit-field textarea {
	width: 100%;
	padding: 0.5rem 0.7rem;
	border: 1px solid #475569;
	border-radius: 0.375rem;
	font-size: 0.85rem;
	font-family: inherit;
	outline: none;
	background: #0f172a;
	color: #e2e8f0;
}
.task-edit-field input:focus, .task-edit-field textarea:focus { border-color: #8b5cf6; }
.task-edit-field textarea { resize: vertical; min-height: 70px; }
.task-edit-footer {
	padding: 0.75rem 1.5rem;
	border-top: 1px solid #334155;
	display: flex;
	justify-content: flex-end;
	gap: 0.5rem;
}

/* Collaborate view */
#collaborate-view {
	width: 100%; height: 100%;
	position: absolute; top: 0; left: 0;
	flex-direction: column;
	overflow-y: auto;
	padding: 1.25rem;
	gap: 1rem;
}
.collab-panel {
	max-width: 800px;
	margin: 0 auto;
	width: 100%;
	display: flex;
	flex-direction: column;
	gap: 1.5rem;
}
.collab-section {
	background: #1e293b;
	border-radius: 0.75rem;
	padding: 1rem 1.25rem;
	border: 1px solid #334155;
}
.collab-section-header {
	display: flex;
	justify-content: space-between;
	align-items: center;
	margin-bottom: 0.75rem;
}
.collab-section-header h3 {
	font-size: 0.95rem;
	font-weight: 600;
	color: #f1f5f9;
	margin: 0;
}
.collab-create-btn, .collab-solver-btn {
	padding: 0.35rem 0.85rem;
	background: linear-gradient(135deg, #8b5cf6, #ec4899);
	color: #fff;
	border: none;
	border-radius: 0.375rem;
	font-size: 0.78rem;
	font-weight: 500;
	cursor: pointer;
	transition: opacity 0.15s;
}
.collab-create-btn:hover, .collab-solver-btn:hover { opacity: 0.85; }
.collab-solver-btn {
	background: linear-gradient(135deg, #3b82f6, #06b6d4);
}
.collab-empty {
	color: #64748b;
	font-size: 0.82rem;
	text-align: center;
	padding: 1.5rem 0;
}
.collab-intents, .collab-results {
	display: flex;
	flex-direction: column;
	gap: 0.5rem;
}

/* Intent cards */
.intent-card {
	background: #0f172a;
	border-radius: 0.5rem;
	padding: 0.75rem 1rem;
	border: 1px solid #334155;
	transition: border-color 0.15s;
}
.intent-card:hover { border-color: #475569; }
.intent-card-header {
	display: flex;
	align-items: center;
	gap: 0.5rem;
	margin-bottom: 0.4rem;
	flex-wrap: wrap;
}
.intent-type-badge {
	font-size: 0.68rem;
	font-weight: 700;
	padding: 0.1rem 0.5rem;
	border-radius: 1rem;
	letter-spacing: 0.04em;
}
.intent-type-badge.intent-offer { background: rgba(16,185,129,0.15); color: #10b981; }
.intent-type-badge.intent-need { background: rgba(59,130,246,0.15); color: #3b82f6; }
.intent-skill-badge {
	font-size: 0.72rem;
	font-weight: 500;
	padding: 0.1rem 0.5rem;
	border-radius: 1rem;
}
.intent-hours {
	font-size: 0.78rem;
	font-weight: 600;
	color: #f1f5f9;
	margin-left: auto;
}
.intent-status-badge {
	font-size: 0.68rem;
	padding: 0.1rem 0.4rem;
	border-radius: 1rem;
	background: #334155;
	color: #94a3b8;
}
.intent-status-active { background: rgba(16,185,129,0.15); color: #10b981; }
.intent-status-matched { background: rgba(251,191,36,0.15); color: #fbbf24; }
.intent-status-settled { background: rgba(139,92,246,0.15); color: #8b5cf6; }
.intent-member {
	font-size: 0.82rem;
	font-weight: 600;
	color: #e2e8f0;
}
.intent-desc {
	font-size: 0.78rem;
	color: #64748b;
	line-height: 1.4;
}

/* Intent type toggle */
.intent-type-toggle {
	display: flex;
	gap: 0;
	border: 1px solid #475569;
	border-radius: 0.375rem;
	overflow: hidden;
}
.intent-type-btn {
	flex: 1;
	padding: 0.45rem 0.75rem;
	background: #0f172a;
	color: #94a3b8;
	border: none;
	font-size: 0.85rem;
	font-weight: 500;
	cursor: pointer;
	transition: all 0.15s;
}
.intent-type-btn.active {
	background: linear-gradient(135deg, #8b5cf6, #ec4899);
	color: #fff;
}
.intent-cost {
	font-size: 0.82rem;
	color: #94a3b8;
	padding: 0.5rem 0;
}
.intent-cost strong { color: #fbbf24; }

/* Solver result cards */
.solver-result-card {
	background: #0f172a;
	border-radius: 0.5rem;
	padding: 0.75rem 1rem;
	border: 1px solid #334155;
}
.solver-result-header {
	display: flex;
	align-items: center;
	gap: 0.75rem;
	margin-bottom: 0.5rem;
}
.solver-score {
	flex-shrink: 0;
}
.solver-score-ring {
	width: 40px; height: 40px;
	border-radius: 50%;
	background: conic-gradient(#8b5cf6 var(--score, 0%), #334155 var(--score, 0%));
	display: flex;
	align-items: center;
	justify-content: center;
}
.solver-score-ring span {
	background: #0f172a;
	width: 30px; height: 30px;
	border-radius: 50%;
	display: flex;
	align-items: center;
	justify-content: center;
	font-size: 0.72rem;
	font-weight: 700;
	color: #f1f5f9;
}
.solver-result-info { flex: 1; }
.solver-skills { display: flex; gap: 0.35rem; flex-wrap: wrap; }
.solver-meta { font-size: 0.75rem; color: #64748b; margin-top: 0.25rem; }
.solver-result-members {
	display: flex;
	flex-wrap: wrap;
	gap: 0.35rem;
	margin-bottom: 0.5rem;
}
.solver-member {
	font-size: 0.72rem;
	padding: 0.1rem 0.4rem;
	border-radius: 0.25rem;
	background: #1e293b;
	color: #94a3b8;
}
.solver-member.accepted { background: rgba(16,185,129,0.15); color: #10b981; }
.solver-result-actions {
	display: flex;
	gap: 0.5rem;
	justify-content: flex-end;
}
.solver-accept-btn, .solver-reject-btn {
	padding: 0.3rem 0.75rem;
	border: none;
	border-radius: 0.375rem;
	font-size: 0.78rem;
	font-weight: 500;
	cursor: pointer;
}
.solver-accept-btn {
	background: linear-gradient(135deg, #10b981, #059669);
	color: #fff;
}
.solver-reject-btn {
	background: #334155;
	color: #94a3b8;
}
.solver-frame-btn {
	background: linear-gradient(135deg, #f59e0b, #fbbf24);
	color: #1e293b;
	padding: 0.3rem 0.75rem;
	border: none;
	border-radius: 0.375rem;
	font-size: 0.78rem;
	font-weight: 600;
	cursor: pointer;
	transition: opacity 0.15s;
}
.solver-frame-btn:hover { opacity: 0.85; }

/* Skill prices */
.collab-prices-section h3 {
	font-size: 0.95rem;
	font-weight: 600;
	color: #f1f5f9;
	margin: 0 0 0.75rem;
}
.collab-prices {
	display: flex;
	gap: 0.75rem;
	flex-wrap: wrap;
}
.price-card {
	display: flex;
	align-items: center;
	gap: 0.4rem;
	padding: 0.35rem 0.7rem;
	background: #0f172a;
	border-radius: 0.375rem;
	border: 1px solid #334155;
}
.price-dot { width: 8px; height: 8px; border-radius: 50%; }
.price-skill { font-size: 0.78rem; color: #94a3b8; }
.price-value { font-size: 0.82rem; font-weight: 600; color: #f1f5f9; margin-left: 0.25rem; }

/* Theme toggle */
.theme-toggle {
	margin-left: auto; padding: 0.4rem 0.75rem;
	background: transparent; border: 1px solid #334155; border-radius: 0.375rem;
	color: #94a3b8; font-size: 1rem; cursor: pointer; transition: all 0.15s;
}
.theme-toggle:hover { border-color: #8b5cf6; color: #e2e8f0; }

/* Light theme overrides */
:host([data-theme="light"]) { background: #f8fafc; color: #1e293b; }
:host([data-theme="light"]) .pool-legend { background: rgba(240, 236, 255, 0.6); border-top-color: #e2e8f0; }
:host([data-theme="light"]) .pool-legend-stats { color: #64748b; }
:host([data-theme="light"]) .skill-legend-item { color: #64748b; }
:host([data-theme="light"]) .pool-legend-stats strong { color: #1e293b; }
:host([data-theme="light"]) .stat { color: #64748b; }
:host([data-theme="light"]) .stat-value { color: #1e293b; }
:host([data-theme="light"]) .pool-panel { background: #fff; border-right-color: #e2e8f0; }
:host([data-theme="light"]) .pool-panel-header { color: #64748b; border-bottom-color: #e2e8f0; }
:host([data-theme="light"]) .pool-panel-header button { border-color: #e2e8f0; color: #64748b; }
:host([data-theme="light"]) .canvas-wrap-header { background: #fff; color: #64748b; border-bottom-color: #e2e8f0; }
:host([data-theme="light"]) .canvas-wrap-hint { color: #94a3b8; }
:host([data-theme="light"]) .sidebar-task:hover { background: #f1f5f9; }
:host([data-theme="light"]) .sidebar-item-name { color: #1e293b; }
:host([data-theme="light"]) .canvas-wrap { background: #f8fafc; }
:host([data-theme="light"]) .node-rect { fill: #fff; stroke: #e2e8f0; }
:host([data-theme="light"]) .theme-toggle { border-color: #e2e8f0; color: #64748b; }
:host([data-theme="light"]) .zoom-controls button { background: #fff; border-color: #e2e8f0; color: #1e293b; }
:host([data-theme="light"]) .zoom-controls button:hover { border-color: #8b5cf6; background: #f1f5f9; }
:host([data-theme="light"]) .pool-detail { background: #fff; box-shadow: 0 8px 32px rgba(0,0,0,0.12); }
:host([data-theme="light"]) .pool-detail-name { color: #1e293b; }
:host([data-theme="light"]) .panel-divider { background: #f1f5f9; border-color: #e2e8f0; }
:host([data-theme="light"]) .panel-divider:hover, :host([data-theme="light"]) .panel-divider.active { background: #8b5cf6; border-color: #8b5cf6; }
:host([data-theme="light"]) .divider-pip { background: #94a3b8; }
:host([data-theme="light"]) .dep-port-diamond { fill: #e2e8f0; stroke: #94a3b8; }
:host([data-theme="light"]) .project-frame-rect { stroke: #8b5cf644; }
:host([data-theme="light"]) .pool-detail-woven { background: rgba(139,92,246,0.08); }
:host([data-theme="light"]) .form-panel { background: #fff; border-right-color: #e2e8f0; }
:host([data-theme="light"]) .form-panel-header { color: #64748b; border-bottom-color: #e2e8f0; }
:host([data-theme="light"]) .inline-form { border-bottom-color: #e2e8f0; }
:host([data-theme="light"]) .inline-form-field input,
:host([data-theme="light"]) .inline-form-field select { background: #f8fafc; border-color: #e2e8f0; color: #1e293b; }
:host([data-theme="light"]) .inline-form-field label { color: #64748b; }
:host([data-theme="light"]) .commitments-list-header { color: #94a3b8; border-bottom-color: #e2e8f0; }
:host([data-theme="light"]) .commitments-count { background: #e2e8f0; color: #64748b; }
:host([data-theme="light"]) .cl-item { border-bottom-color: rgba(226,232,240,0.4); }
:host([data-theme="light"]) .cl-item:hover { background: rgba(241,245,249,0.6); }
:host([data-theme="light"]) .cl-name { color: #1e293b; }
:host([data-theme="light"]) .cl-meta { color: #64748b; }
:host([data-theme="light"]) .cl-desc { color: #94a3b8; }
:host([data-theme="light"]) .pool-panel { background: #f8fafc; }
:host([data-theme="light"]) .weave-view-header { background: #fff; color: #64748b; border-bottom-color: #e2e8f0; }
:host([data-theme="light"]) .weave-back-btn { border-color: #e2e8f0; color: #64748b; }
:host([data-theme="light"]) .weave-toggle-btn { border-color: #e2e8f0; color: #64748b; }

/* Hex hover stroke */
.hex-hover-stroke { transition: stroke-width 0.15s; }
.node-group:hover .hex-hover-stroke { stroke-width: 2; }

/* Task editor links */
.task-edit-link-row {
	display: flex; gap: 0.4rem; margin-bottom: 0.4rem; align-items: center;
}
.task-edit-link-row input { flex: 1; }
.task-edit-link-row .link-label { max-width: 120px; }
.task-edit-link-rm {
	width: 28px; height: 28px; border: 1px solid #475569; border-radius: 0.25rem;
	background: #0f172a; color: #ef4444; font-size: 1rem; cursor: pointer;
	display: flex; align-items: center; justify-content: center; flex-shrink: 0;
}
.task-edit-link-rm:hover { background: #1e293b; }
.task-edit-add-link {
	padding: 0.35rem 0.75rem; border: 1px dashed #475569; border-radius: 0.375rem;
	background: transparent; color: #8b5cf6; font-size: 0.78rem; font-weight: 500;
	cursor: pointer; margin-top: 0.35rem; transition: border-color 0.15s;
}
.task-edit-add-link:hover { border-color: #8b5cf6; }

/* Exec step detail forms */
.exec-step-detail {
	margin-top: 0.75rem;
	padding: 0.75rem;
	background: #0f172a;
	border-radius: 0.5rem;
	border: 1px solid #334155;
}
.exec-step-field { margin-bottom: 0.6rem; }
.exec-step-field label { display: block; font-size: 0.75rem; font-weight: 500; color: #94a3b8; margin-bottom: 0.2rem; }
.exec-step-field input, .exec-step-field select, .exec-step-field textarea {
	width: 100%; padding: 0.4rem 0.6rem; border: 1px solid #475569; border-radius: 0.375rem;
	font-size: 0.82rem; background: #1e293b; color: #e2e8f0; outline: none; font-family: inherit;
}
.exec-step-field input:focus, .exec-step-field select:focus, .exec-step-field textarea:focus { border-color: #8b5cf6; }
.exec-step-field textarea { resize: vertical; min-height: 50px; }
.exec-step-action {
	margin-top: 0.5rem; padding: 0.45rem 1rem;
	background: linear-gradient(135deg, #8b5cf6, #ec4899); color: #fff; border: none;
	border-radius: 0.375rem; font-size: 0.8rem; font-weight: 600; cursor: pointer;
	transition: opacity 0.15s; width: 100%;
}
.exec-step-action:hover { opacity: 0.85; }
.exec-step-action.launch-action { background: linear-gradient(135deg, #10b981, #059669); }
.exec-step-invite-list { display: flex; flex-wrap: wrap; gap: 0.3rem; margin-top: 0.25rem; }
.invite-tag {
	font-size: 0.72rem; padding: 0.15rem 0.5rem; border-radius: 1rem;
	background: rgba(139,92,246,0.15); color: #a78bfa;
}
.exec-step-checklist { display: flex; flex-direction: column; gap: 0.3rem; margin-top: 0.25rem; }
.exec-step-checklist label {
	display: flex; align-items: center; gap: 0.4rem; font-size: 0.8rem; color: #e2e8f0; cursor: pointer;
}
.exec-step-checklist input[type="checkbox"] { accent-color: #8b5cf6; }

@media (max-width: 768px) {
	.pool-legend { padding: 0.35rem 0.5rem; }
	.stat { font-size: 0.75rem; }
	.stat-value { font-size: 0.85rem; }
	.skill-bar { min-width: 80px; max-width: 160px; }
	.skill-legend { gap: 0.5rem; }
	.skill-legend-item { font-size: 0.68rem; }
	#canvas-view { flex-direction: column; }
	.form-panel {
		width: 100% !important; min-width: unset;
		border-right: none; border-bottom: 1px solid #334155;
		flex: 0 0 auto; max-height: 50%;
	}
	.pool-panel {
		flex: 1; min-height: 0;
		border-top: 1px solid #334155;
	}
	.canvas-wrap {
		flex: 1; min-height: 0;
		border-top: 1px solid #334155;
	}
	.canvas-wrap-header { display: flex; }
	.panel-divider { display: none !important; }
	.pool-hint { display: none; }
	.exec-panel { width: 95vw; }
	.task-edit-panel { width: 95vw; }
}
@media (max-width: 640px) {
	.pool-legend-skills { display: none; }
	.inline-form-field label { font-size: 0.68rem; }
	.inline-form-field input, .inline-form-field select { font-size: 0.78rem; padding: 0.35rem 0.45rem; }
}
`;

customElements.define('folk-timebank-app', FolkTimebankApp);
