/**
 * <rstack-tab-bar> — Layered tab system for rSpace.
 *
 * Each tab is a "layer" — an rApp page within the current space.
 * Supports two view modes:
 *   - flat: traditional tab bar (looking down at one layer)
 *   - stack: side view showing all layers as stacked strata with flows
 *
 * Attributes:
 *   active    — the active layer ID
 *   space     — current space slug
 *   view-mode — "flat" | "stack"
 *
 * Events:
 *   layer-switch  — fired when user clicks a tab { detail: { layerId, moduleId } }
 *   layer-add     — fired when user clicks + to add a layer
 *   layer-close   — fired when user closes a tab { detail: { layerId } }
 *   layers-close-all — fired when user clicks close-all button (no detail)
 *   layer-reorder — fired on drag reorder { detail: { layerId, newIndex } }
 *   view-toggle   — fired when switching flat/stack view { detail: { mode } }
 *   flow-select   — fired when a flow is clicked in stack view { detail: { flowId } }
 */

import type { Layer, LayerFlow, FlowKind, FeedDefinition } from "../../lib/layer-types";
import { FLOW_COLORS, FLOW_LABELS } from "../../lib/layer-types";

// Badge info for tab display
const MODULE_BADGES: Record<string, { badge: string; color: string }> = {
	rspace:      { badge: "r🎨", color: "#5eead4" },
	rnotes:      { badge: "r📝", color: "#fcd34d" },
	rpubs:       { badge: "r📖", color: "#fda4af" },
	rswag:       { badge: "r👕", color: "#fda4af" },
	rsplat:      { badge: "r🔮", color: "#d8b4fe" },
	rcal:        { badge: "r📅", color: "#7dd3fc" },
	rtrips:      { badge: "r✈️",  color: "#6ee7b7" },
	rmaps:       { badge: "r🗺", color: "#86efac" },
	rchats:      { badge: "r🗨", color: "#6ee7b7" },
	rinbox:      { badge: "r📨", color: "#a5b4fc" },
	rmail:       { badge: "r✉️",  color: "#93c5fd" },
	rforum:      { badge: "r💬", color: "#fcd34d" },
	rmeets:      { badge: "r📹", color: "#67e8f9" },
	rchoices:    { badge: "r☑️",  color: "#f0abfc" },
	rvote:       { badge: "r🗳", color: "#c4b5fd" },
	rflows:      { badge: "r🌊", color: "#bef264" },
	rwallet:     { badge: "r💰", color: "#fde047" },
	rcart:       { badge: "r🛒", color: "#fdba74" },
	rauctions:   { badge: "r🏛", color: "#fca5a5" },
	rtube:       { badge: "r🎬", color: "#f9a8d4" },
	rphotos:     { badge: "r📸", color: "#f9a8d4" },
	rnetwork:    { badge: "r🌐", color: "#93c5fd" },
	rsocials:    { badge: "r📢", color: "#7dd3fc" },
	rfiles:      { badge: "r📁", color: "#67e8f9" },
	rbooks:      { badge: "r📚", color: "#fda4af" },
	rdata:       { badge: "r📊", color: "#d8b4fe" },
	rtasks:      { badge: "r📋", color: "#cbd5e1" },
	rschedule:   { badge: "r⏱",  color: "#a5b4fc" },
	rids:        { badge: "r🪪", color: "#6ee7b7" },
	rcred:       { badge: "r⭐", color: "#d97706" },
	rstack:      { badge: "r✨", color: "" },
};

// Category definitions for the + menu dropdown grouping
const MODULE_CATEGORIES: Record<string, string> = {
	rspace: "Creating", rnotes: "Creating", rpubs: "Creating", rtube: "Creating",
	rswag: "Creating", rsplat: "Creating",
	rcal: "Planning", rtrips: "Planning", rmaps: "Planning",
	rchats: "Communicating", rinbox: "Communicating", rmail: "Communicating", rforum: "Communicating",
	rchoices: "Deciding", rvote: "Deciding",
	rflows: "Funding & Commerce", rwallet: "Funding & Commerce", rcart: "Funding & Commerce", rauctions: "Funding & Commerce",
	rphotos: "Sharing", rnetwork: "Sharing", rsocials: "Sharing", rfiles: "Sharing", rbooks: "Sharing",
	rdata: "Observing",
	rtasks: "Tasks & Productivity",
	rids: "Identity & Infrastructure", rstack: "Identity & Infrastructure",
};

const CATEGORY_ORDER = [
	"Creating", "Planning", "Communicating", "Deciding",
	"Funding & Commerce", "Sharing", "Observing",
	"Work & Productivity", "Identity & Infrastructure",
];

export interface TabBarModule {
	id: string;
	name: string;
	icon: string;
	description: string;
	feeds?: FeedDefinition[];
	acceptsFeeds?: FlowKind[];
}

export class RStackTabBar extends HTMLElement {
	#shadow: ShadowRoot;
	#layers: Layer[] = [];
	#modules: TabBarModule[] = [];
	#flows: LayerFlow[] = [];
	#viewMode: "flat" | "stack" = "flat";
	#draggedTabId: string | null = null;
	#addMenuOpen = false;
	#flowDragSource: string | null = null;
	#flowDragTarget: string | null = null;
	#flowDialogOpen = false;
	#flowDialogSourceId = "";
	#flowDialogTargetId = "";
	// 3D scene state
	#sceneRotX = 55;
	#sceneRotY = 0;
	#sceneRotZ = -15;
	#scenePerspective = 1200;
	#orbitDragging = false;
	#orbitLastX = 0;
	#orbitLastY = 0;
	#simSpeed = 1;
	#simPlaying = true;
	// Wiring mode state
	#wiringActive = false;
	#wiringSourceLayerId = "";
	#wiringSourceFeedId = "";
	#wiringSourceKind: FlowKind | null = null;
	#escHandler: ((e: KeyboardEvent) => void) | null = null;
	// Cleanup for document-level listeners (prevent leak on re-render)
	#docCleanup: (() => void) | null = null;
	// Touch-drag reorder state (long-press to reorder on mobile)
	#touchDragTimer: ReturnType<typeof setTimeout> | null = null;
	#touchDragActive = false;
	#touchDragLayerId: string | null = null;
	#touchDragClone: HTMLElement | null = null;
	#touchStartX = 0;
	#touchStartY = 0;
	// Recent apps: moduleId → last-used timestamp
	#recentApps: Map<string, number> = new Map();

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

	static get observedAttributes() {
		return ["active", "space", "view-mode", "home-active"];
	}

	get active(): string {
		return this.getAttribute("active") || "";
	}

	set active(val: string) {
		this.setAttribute("active", val);
	}

	get space(): string {
		return this.getAttribute("space") || "";
	}

	get viewMode(): "flat" | "stack" {
		return (this.getAttribute("view-mode") as "flat" | "stack") || "flat";
	}

	set viewMode(val: "flat" | "stack") {
		this.setAttribute("view-mode", val);
	}

	connectedCallback() {
		this.#loadRecentApps();
		this.#render();
	}

	attributeChangedCallback() {
		this.#viewMode = this.viewMode;
		this.#render();
	}

	/** Set the layer list (call from outside) */
	setLayers(layers: Layer[]) {
		this.#layers = [...layers].sort((a, b) => a.order - b.order);
		// Seed recent apps from existing layers
		for (const l of layers) {
			if (!this.#recentApps.has(l.moduleId)) {
				this.#recentApps.set(l.moduleId, l.createdAt || 0);
			}
		}
		this.#render();
	}

	/** Provide the available module list (for the + add menu) */
	setModules(modules: TabBarModule[]) {
		this.#modules = modules;
	}

	// ── Recent apps persistence ──

	#recentAppsKey(): string {
		return `rspace_recent_apps_${this.space || "default"}`;
	}

	#loadRecentApps() {
		try {
			const raw = localStorage.getItem(this.#recentAppsKey());
			if (raw) {
				const obj = JSON.parse(raw) as Record<string, number>;
				this.#recentApps = new Map(Object.entries(obj));
			}
		} catch { /* ignore */ }
	}

	#saveRecentApps() {
		try {
			const obj: Record<string, number> = {};
			for (const [k, v] of this.#recentApps) obj[k] = v;
			localStorage.setItem(this.#recentAppsKey(), JSON.stringify(obj));
		} catch { /* ignore */ }
	}

	/** Record a module as recently used (call from outside or internally) */
	trackRecent(moduleId: string) {
		this.#recentApps.set(moduleId, Date.now());
		this.#saveRecentApps();
	}

	/** Set the inter-layer flows (for stack view) */
	setFlows(flows: LayerFlow[]) {
		this.#flows = flows;
		if (this.#viewMode === "stack") {
			const scene = this.#shadow.getElementById("stack-scene");
			if (scene) this.#updateFlowsInPlace(scene);
			else this.#render();
		}
	}

	/** Add a single layer */
	addLayer(layer: Layer) {
		this.#layers.push(layer);
		this.#layers.sort((a, b) => a.order - b.order);
		this.#render();
	}

	/** Remove a layer by ID */
	removeLayer(layerId: string) {
		this.#layers = this.#layers.filter(l => l.id !== layerId);
		this.#render();
	}

	/** Update a layer */
	updateLayer(layerId: string, updates: Partial<Layer>) {
		const layer = this.#layers.find(l => l.id === layerId);
		if (layer) {
			Object.assign(layer, updates);
			this.#layers.sort((a, b) => a.order - b.order);
			this.#render();
		}
	}

	// ── Space Layer Picker ──

	/** Show a popup to pick a space to add as a composable layer. */
	async #showSpaceLayerPicker() {
		// Remove any existing picker
		this.#shadow.querySelector(".space-layer-picker")?.remove();

		// Fetch spaces from the API
		let spaces: Array<{ slug: string; name: string; icon?: string; role?: string }> = [];
		try {
			const token = localStorage.getItem("encryptid_session");
			const headers: Record<string, string> = {};
			if (token) {
				try {
					const session = JSON.parse(token);
					if (session?.accessToken) headers["Authorization"] = `Bearer ${session.accessToken}`;
				} catch { /* ignore */ }
			}
			const res = await fetch("/api/spaces", { headers });
			if (res.ok) {
				const data = await res.json();
				spaces = (data.spaces || []).filter(
					(s: any) => s.slug !== this.space && s.role // only spaces user has access to
				);
			}
		} catch { /* offline */ }

		// Filter out spaces already added as layers
		const existingSpaceSlugs = new Set(
			this.#layers.filter(l => l.spaceSlug).map(l => l.spaceSlug)
		);
		spaces = spaces.filter(s => !existingSpaceSlugs.has(s.slug));

		// Build picker HTML
		let pickerHtml = `<div class="space-layer-picker">
			<div class="space-layer-picker__header">
				<span>Add Space Layer</span>
				<button class="space-layer-picker__close" id="slp-close">&times;</button>
			</div>`;

		if (spaces.length === 0) {
			pickerHtml += `<div class="space-layer-picker__empty">No other spaces available</div>`;
		} else {
			for (const s of spaces) {
				const roleLabel = s.role === "viewer" ? "view only" : s.role || "";
				pickerHtml += `
					<button class="space-layer-picker__item" data-slug="${s.slug}" data-name="${(s.name || s.slug).replace(/"/g, '&quot;')}" data-role="${s.role || 'viewer'}">
						<span class="space-layer-picker__icon">${s.icon || "🌐"}</span>
						<span class="space-layer-picker__name">${s.name || s.slug}</span>
						<span class="space-layer-picker__role">${roleLabel}</span>
					</button>`;
			}
		}

		pickerHtml += `</div>`;

		// Inject into shadow DOM
		const container = document.createElement("div");
		container.innerHTML = pickerHtml;
		const picker = container.firstElementChild!;
		this.#shadow.appendChild(picker);

		// Close button
		this.#shadow.getElementById("slp-close")?.addEventListener("click", () => {
			picker.remove();
		});

		// Space item clicks
		picker.querySelectorAll<HTMLElement>(".space-layer-picker__item").forEach(item => {
			item.addEventListener("click", () => {
				const slug = item.dataset.slug!;
				const name = item.dataset.name || slug;
				const role = (item.dataset.role || "viewer") as Layer["spaceRole"];

				const layer: Layer = {
					id: `layer-space-${slug}`,
					moduleId: "rspace",
					label: `${name}`,
					order: this.#layers.length,
					color: "",
					visible: true,
					createdAt: Date.now(),
					spaceSlug: slug,
					spaceRole: role,
				};

				this.addLayer(layer);
				this.dispatchEvent(new CustomEvent("space-layer-add", {
					detail: { layer, spaceSlug: slug, role },
					bubbles: true,
				}));

				picker.remove();
			});
		});

		// Close on outside click
		setTimeout(() => {
			const handler = (e: Event) => {
				if (!picker.contains(e.target as Node)) {
					picker.remove();
					document.removeEventListener("click", handler);
				}
			};
			document.addEventListener("click", handler);
		}, 0);
	}

	// ── Feed compatibility helpers ──

	/** Get the set of FlowKinds a module can output (from its feeds) */
	#getModuleOutputKinds(moduleId: string): Set<FlowKind> {
		const mod = this.#modules.find(m => m.id === moduleId);
		if (!mod?.feeds) return new Set();
		return new Set(mod.feeds.map(f => f.kind));
	}

	/** Get the set of FlowKinds a module can accept as input */
	#getModuleInputKinds(moduleId: string): Set<FlowKind> {
		const mod = this.#modules.find(m => m.id === moduleId);
		if (!mod?.acceptsFeeds) return new Set();
		return new Set(mod.acceptsFeeds);
	}

	/** Get compatible FlowKinds between two layers (intersection of source outputs and target accepts) */
	#getCompatibleKinds(srcLayerId: string, tgtLayerId: string): Set<FlowKind> {
		const srcLayer = this.#layers.find(l => l.id === srcLayerId);
		const tgtLayer = this.#layers.find(l => l.id === tgtLayerId);
		if (!srcLayer || !tgtLayer) return new Set();

		const srcOutputs = this.#getModuleOutputKinds(srcLayer.moduleId);
		const tgtInputs = this.#getModuleInputKinds(tgtLayer.moduleId);

		// If either module has no feed data, return empty (fallback allows all in dialog)
		if (srcOutputs.size === 0 && tgtInputs.size === 0) return new Set();

		const compatible = new Set<FlowKind>();
		for (const kind of srcOutputs) {
			if (tgtInputs.has(kind)) compatible.add(kind);
		}
		return compatible;
	}

	/** Get feeds that have no matching outgoing flow (contained within the layer) */
	#getContainedFeeds(layerId: string): FeedDefinition[] {
		const layer = this.#layers.find(l => l.id === layerId);
		if (!layer) return [];
		const mod = this.#modules.find(m => m.id === layer.moduleId);
		if (!mod?.feeds) return [];

		const outgoingKinds = new Set(
			this.#flows
				.filter(f => f.sourceLayerId === layerId && f.active)
				.map(f => f.kind)
		);

		return mod.feeds.filter(f => !outgoingKinds.has(f.kind));
	}

	// ── Render ──

	#render() {
		const active = this.active;

		this.#shadow.innerHTML = `
			<style>${STYLES}</style>
			<div class="tab-bar" data-view="${this.#viewMode}">
				<div class="tabs-scroll">
					<button class="tab-home ${!active || this.getAttribute('home-active') === 'true' ? 'tab-home--active' : ''}"
					        id="home-btn" title="Dashboard">
						<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
							<path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/>
						</svg>
						<span class="tab-home__label">Home</span>
					</button>
					${this.#layers.map(l => this.#renderTab(l, active)).join("")}
				</div>
				<div class="tab-add-wrap">
					<button class="tab-add" id="add-btn" title="Add layer">+</button>
					${this.#layers.length > 1 ? `<button class="tab-close-all" id="close-all-btn" title="Close all tabs">✕</button>` : ""}
					${this.#addMenuOpen ? this.#renderAddMenu() : ""}
				</div>
				<div class="tab-actions">
					<slot></slot>
					<button class="view-toggle ${this.#viewMode === "stack" ? "active" : ""}"
					        id="view-toggle" title="Toggle stack view">
						${this.#viewMode === "stack" ? ICON_FLAT : ICON_STACK}
					</button>
				</div>
			</div>
			${this.#viewMode === "stack" ? this.#renderStackView() : ""}
		`;

		this.#attachEvents();
	}

	#renderTab(layer: Layer, activeId: string): string {
		const badge = MODULE_BADGES[layer.moduleId];
		const isActive = layer.id === activeId;
		const badgeColor = layer.color || badge?.color || "#94a3b8";

		const isSpaceLayer = !!layer.spaceSlug;
		const spaceLayerClass = isSpaceLayer ? " tab--space-layer" : "";
		const spaceTag = isSpaceLayer
			? `<span class="tab-space-tag" title="From ${layer.spaceSlug}">${layer.spaceSlug}</span>`
			: "";
		const readOnlyTag = isSpaceLayer && layer.spaceRole === "viewer"
			? `<span class="tab-readonly-tag" title="View only">👁</span>`
			: "";

		return `
			<div class="tab ${isActive ? "active" : ""}${spaceLayerClass}"
			     data-layer-id="${layer.id}"
			     data-module-id="${layer.moduleId}"
			     ${isSpaceLayer ? `data-space-slug="${layer.spaceSlug}"` : ""}
			     draggable="true">
				<span class="tab-indicator" style="background:${badgeColor}"></span>
				<span class="tab-badge" style="background:${badgeColor}">${badge ? `<span style="color:#dc8300">r</span>${badge.badge.slice(1)}` : layer.moduleId.slice(0, 2)}</span>
				<span class="tab-label">${layer.label}</span>
				${spaceTag}${readOnlyTag}
				<button class="tab-close" data-close="${layer.id}">&times;</button>
			</div>
		`;
	}

	#renderAddMenu(): string {
		const existingModuleIds = new Set(this.#layers.map(l => l.moduleId));

		// Only use server module list — never fall back to MODULE_BADGES (leaks disabled modules)
		const allModules = this.#modules;

		if (allModules.length === 0) {
			return `<div class="add-menu" id="add-menu"><div class="add-menu-empty">Loading rApps\u2026</div></div>`;
		}

		let html = "";

		// ── Recent apps section (top of menu) ──
		if (this.#recentApps.size > 0) {
			const recentEntries = [...this.#recentApps.entries()]
				.sort((a, b) => b[1] - a[1])   // newest first
				.slice(0, 6);                     // show up to 6 recent
			const recentModules = recentEntries
				.map(([id]) => allModules.find(m => m.id === id))
				.filter((m): m is typeof allModules[0] => !!m);
			if (recentModules.length > 0) {
				html += `<div class="add-menu-category">Recent</div>`;
				html += recentModules.map(m => this.#renderAddMenuItem(m, existingModuleIds.has(m.id))).join("");
			}
		}

		// ── Group by category ──
		const groups = new Map<string, typeof allModules>();
		const uncategorized: typeof allModules = [];
		for (const m of allModules) {
			const cat = MODULE_CATEGORIES[m.id];
			if (cat) {
				if (!groups.has(cat)) groups.set(cat, []);
				groups.get(cat)!.push(m);
			} else {
				uncategorized.push(m);
			}
		}

		for (const cat of CATEGORY_ORDER) {
			const items = groups.get(cat);
			if (!items || items.length === 0) continue;
			html += `<div class="add-menu-category">${cat}</div>`;
			html += items.map(m => this.#renderAddMenuItem(m, existingModuleIds.has(m.id))).join("");
		}
		if (uncategorized.length > 0) {
			html += `<div class="add-menu-category">Other</div>`;
			html += uncategorized.map(m => this.#renderAddMenuItem(m, existingModuleIds.has(m.id))).join("");
		}

		// ── Add Space Layer section ──
		html += `<div class="add-menu-divider"></div>`;
		html += `<button class="add-menu-item add-menu-item--space-layer" id="add-space-layer-btn">
			<span class="add-menu-icon">🌐</span>
			<div class="add-menu-text">
				<span class="add-menu-name">Add Space Layer</span>
				<span class="add-menu-desc">Overlay data from another space</span>
			</div>
		</button>`;

		return `<div class="add-menu" id="add-menu">${html}</div>`;
	}

	#renderAddMenuItem(m: { id: string; name: string; icon: string; description: string }, isOpen: boolean): string {
		const badge = MODULE_BADGES[m.id];
		const badgeHtml = badge
			? `<span class="add-menu-badge" style="background:${badge.color}"><span style="color:#dc8300">r</span>${badge.badge.slice(1)}</span>`
			: `<span class="add-menu-icon">${m.icon}</span>`;

		const openClass = isOpen ? " add-menu-item--open" : "";
		const openIndicator = isOpen ? `<span class="add-menu-open-dot" title="Already open">●</span>` : "";

		return `
			<button class="add-menu-item${openClass}" data-add-module="${m.id}" data-module-open="${isOpen}">
				${badgeHtml}
				<div class="add-menu-text">
					<span class="add-menu-name">${m.name} <span class="add-menu-emoji">${m.icon}</span></span>
					${m.description ? `<span class="add-menu-desc">${m.description}</span>` : ""}
				</div>
				${openIndicator}
			</button>
		`;
	}

	#renderStackView(): string {
		const layerCount = this.#layers.length;
		if (layerCount === 0) return "";

		const layerSpacing = 120;
		const animDuration = 2 / this.#simSpeed;

		// Build layer planes
		let layersHtml = "";
		const layerZMap = new Map<string, number>();

		this.#layers.forEach((layer, i) => {
			const z = i * layerSpacing;
			layerZMap.set(layer.id, z);
			const badge = MODULE_BADGES[layer.moduleId];
			const color = layer.color || badge?.color || "#94a3b8";
			const isActive = layer.id === this.active;
			const containedFeeds = this.#getContainedFeeds(layer.id);

			// Build I/O chip markup — output feeds (right) and input accepts (left)
			const mod = this.#modules.find(m => m.id === layer.moduleId);
			const outFeeds = mod?.feeds || [];
			const inKinds = mod?.acceptsFeeds || [];
			const containedSet = new Set(containedFeeds.map(f => f.id));

			const outChips = outFeeds.map(f => {
				const contained = containedSet.has(f.id);
				return `<span class="io-chip io-chip--out ${contained ? "io-chip--contained" : ""}"
					data-feed-id="${f.id}" data-feed-kind="${f.kind}" data-layer-id="${layer.id}"
					style="--chip-color:${FLOW_COLORS[f.kind]}" title="${f.description || f.name}">
					<span class="io-dot"></span>${f.name}${contained ? '<span class="io-lock">🔒</span>' : ""}
				</span>`;
			}).join("");

			const inChips = inKinds.map(k =>
				`<span class="io-chip io-chip--in"
					data-accept-kind="${k}" data-layer-id="${layer.id}"
					style="--chip-color:${FLOW_COLORS[k]}" title="Accepts ${FLOW_LABELS[k]}">
					<span class="io-dot"></span>${FLOW_LABELS[k]}
				</span>`
			).join("");

			const hasIO = outFeeds.length > 0 || inKinds.length > 0;

			layersHtml += `
				<div class="layer-plane ${isActive ? "layer-plane--active" : ""}"
				     data-layer-id="${layer.id}"
				     style="--layer-color:${color}; transform: translateZ(${z}px);">
					<div class="layer-header">
						<span class="layer-badge" style="background:${color}">${badge ? `<span style="color:#dc8300">r</span>${badge.badge.slice(1)}` : layer.moduleId.slice(0, 2)}</span>
						<span class="layer-name">${layer.label}</span>
					</div>
					${hasIO ? `<div class="layer-io">
						<div class="io-col io-col--in">${inChips}</div>
						<div class="io-col io-col--out">${outChips}</div>
					</div>` : ""}
				</div>
			`;
		});

		// Build flow tubes and particles
		const activeFlows = this.#flows.filter(f =>
			f.active && layerZMap.has(f.sourceLayerId) && layerZMap.has(f.targetLayerId));
		const flowSpacing = 35;
		const flowStartX = -(activeFlows.length - 1) * flowSpacing / 2;

		let tubesHtml = "";
		let particlesHtml = "";
		activeFlows.forEach((flow, fi) => {
			const srcZ = layerZMap.get(flow.sourceLayerId)!;
			const tgtZ = layerZMap.get(flow.targetLayerId)!;
			const color = flow.color || FLOW_COLORS[flow.kind] || "#94a3b8";
			const minZ = Math.min(srcZ, tgtZ);
			const maxZ = Math.max(srcZ, tgtZ);
			const distance = maxZ - minZ;
			const midZ = (minZ + maxZ) / 2;
			const xOffset = activeFlows.length === 1 ? 0 : flowStartX + fi * flowSpacing;
			const tubeWidth = 2 + Math.round(flow.strength * 4);

			tubesHtml += `
				<div class="flow-tube" data-flow-id="${flow.id}"
				     style="--color:${color}; width:${tubeWidth}px; height:${distance}px;
				            transform: translateZ(${midZ}px) rotateX(90deg);
				            margin-left:${xOffset - tubeWidth / 2}px;"></div>
			`;

			const particleCount = Math.max(2, Math.round(flow.strength * 6));
			for (let p = 0; p < particleCount; p++) {
				const delay = (p / particleCount) * animDuration;
				particlesHtml += `
					<div class="flow-particle" data-flow-id="${flow.id}"
					     style="--src-z:${srcZ}px; --tgt-z:${tgtZ}px; --color:${color};
					            --duration:${animDuration}s; --delay:${delay}s;
					            --x-offset:${xOffset}px;
					            animation-play-state:${this.#simPlaying ? "running" : "paused"};"></div>
				`;
			}
		});

		// Flow legend
		const activeKinds = new Set(this.#flows.map(f => f.kind));
		const legendHtml = [...activeKinds].map(k => `
			<span class="legend-item">
				<span class="legend-dot" style="background:${FLOW_COLORS[k]}"></span>
				${FLOW_LABELS[k]}
			</span>
		`).join("");

		// Time scrubber
		const scrubberHtml = `
			<div class="time-scrubber">
				<button class="scrubber-playpause" id="scrubber-playpause" title="${this.#simPlaying ? "Pause" : "Play"}">
					${this.#simPlaying ? "⏸" : "▶"}
				</button>
				<input type="range" class="scrubber-range" id="scrubber-range"
				       min="0.1" max="5" step="0.1" value="${this.#simSpeed}" />
				<span class="scrubber-label" id="scrubber-label">${this.#simSpeed.toFixed(1)}x</span>
			</div>
		`;

		return `
			<div class="stack-view">
				<div class="stack-view-3d" id="stack-3d"
				     style="perspective:${this.#scenePerspective}px;">
					<div class="stack-scene" id="stack-scene"
					     style="transform: rotateX(${this.#sceneRotX}deg) rotateY(${this.#sceneRotY}deg) rotateZ(${this.#sceneRotZ}deg);">
						${layersHtml}
						${tubesHtml}
						${particlesHtml}
					</div>
				</div>
				<div class="stack-legend">${legendHtml}</div>
				${scrubberHtml}
				${this.#layers.length >= 2 ? `<div class="stack-hint">Drag to orbit · Shift+drag to roll · Scroll to zoom · Click output ports to wire</div>` : ""}
				${this.#flowDialogOpen ? this.#renderFlowDialog() : ""}
			</div>
		`;
	}

	// ── Flow creation dialog ──

	#renderFlowDialog(): string {
		const srcLayer = this.#layers.find(l => l.id === this.#flowDialogSourceId);
		const tgtLayer = this.#layers.find(l => l.id === this.#flowDialogTargetId);
		if (!srcLayer || !tgtLayer) return "";

		const srcBadge = MODULE_BADGES[srcLayer.moduleId];
		const tgtBadge = MODULE_BADGES[tgtLayer.moduleId];

		const kinds: FlowKind[] = ["economic", "trust", "data", "attention", "governance", "resource"];
		const compatible = this.#getCompatibleKinds(this.#flowDialogSourceId, this.#flowDialogTargetId);
		const hasModuleData = this.#modules.some(m => m.id === srcLayer.moduleId && m.feeds) ||
		                      this.#modules.some(m => m.id === tgtLayer.moduleId && m.acceptsFeeds);

		// Count source feeds per kind for badge display
		const srcMod = this.#modules.find(m => m.id === srcLayer.moduleId);
		const feedCountByKind = new Map<FlowKind, number>();
		if (srcMod?.feeds) {
			for (const f of srcMod.feeds) {
				feedCountByKind.set(f.kind, (feedCountByKind.get(f.kind) || 0) + 1);
			}
		}

		return `
			<div class="flow-dialog" id="flow-dialog">
				<div class="flow-dialog-header">New Flow</div>
				<div class="flow-dialog-route">
					<span class="flow-dialog-badge" style="background:${srcBadge?.color || "#94a3b8"}">${srcBadge?.badge || srcLayer.moduleId}</span>
					<span class="flow-dialog-arrow">→</span>
					<span class="flow-dialog-badge" style="background:${tgtBadge?.color || "#94a3b8"}">${tgtBadge?.badge || tgtLayer.moduleId}</span>
				</div>
				<div class="flow-dialog-field">
					<label class="flow-dialog-label">Flow Type</label>
					<div class="flow-kind-grid" id="flow-kind-grid">
						${kinds.map(k => {
							const isCompatible = !hasModuleData || compatible.has(k);
							const feedCount = feedCountByKind.get(k) || 0;
							return `
							<button class="flow-kind-btn ${!isCompatible ? "disabled" : ""}"
							        data-kind="${k}"
							        style="--kind-color:${FLOW_COLORS[k]}"
							        ${!isCompatible ? 'disabled aria-disabled="true"' : ""}>
								<span class="flow-kind-dot" style="background:${FLOW_COLORS[k]}"></span>
								${FLOW_LABELS[k]}
								${isCompatible && feedCount > 0 ? `<span class="flow-kind-count">${feedCount}</span>` : ""}
							</button>
						`;
						}).join("")}
					</div>
				</div>
				<div class="flow-dialog-field">
					<label class="flow-dialog-label">Label (optional)</label>
					<input class="flow-dialog-input" id="flow-label-input" type="text" placeholder="e.g. Budget allocation" />
				</div>
				<div class="flow-dialog-field">
					<label class="flow-dialog-label">Strength</label>
					<input class="flow-dialog-range" id="flow-strength-input" type="range" min="0.1" max="1" step="0.1" value="0.5" />
				</div>
				<div class="flow-dialog-actions">
					<button class="flow-dialog-cancel" id="flow-cancel">Cancel</button>
					<button class="flow-dialog-create" id="flow-create">Create Flow</button>
				</div>
			</div>
		`;
	}

	#openFlowDialog(sourceLayerId: string, targetLayerId: string) {
		this.#flowDialogOpen = true;
		this.#flowDialogSourceId = sourceLayerId;
		this.#flowDialogTargetId = targetLayerId;
		this.#render();
	}

	#closeFlowDialog() {
		this.#flowDialogOpen = false;
		this.#flowDialogSourceId = "";
		this.#flowDialogTargetId = "";
		this.#render();
	}

	// ── In-place flow update (prevents animation restart) ──

	#updateFlowsInPlace(scene: HTMLElement) {
		scene.querySelectorAll(".flow-tube, .flow-particle").forEach(el => el.remove());

		const layerZMap = new Map<string, number>();
		const layerSpacing = 120;
		this.#layers.forEach((layer, i) => layerZMap.set(layer.id, i * layerSpacing));

		const animDuration = 2 / this.#simSpeed;
		const activeFlows = this.#flows.filter(f =>
			f.active && layerZMap.has(f.sourceLayerId) && layerZMap.has(f.targetLayerId));
		const flowSpacing = 35;
		const flowStartX = -(activeFlows.length - 1) * flowSpacing / 2;

		activeFlows.forEach((flow, fi) => {
			const srcZ = layerZMap.get(flow.sourceLayerId)!;
			const tgtZ = layerZMap.get(flow.targetLayerId)!;
			const color = flow.color || FLOW_COLORS[flow.kind] || "#94a3b8";
			const minZ = Math.min(srcZ, tgtZ);
			const maxZ = Math.max(srcZ, tgtZ);
			const distance = maxZ - minZ;
			const midZ = (minZ + maxZ) / 2;
			const xOffset = activeFlows.length === 1 ? 0 : flowStartX + fi * flowSpacing;
			const tubeWidth = 2 + Math.round(flow.strength * 4);

			const tube = document.createElement("div");
			tube.className = "flow-tube";
			tube.dataset.flowId = flow.id;
			tube.style.cssText = `--color:${color}; width:${tubeWidth}px; height:${distance}px; transform: translateZ(${midZ}px) rotateX(90deg); margin-left:${xOffset - tubeWidth / 2}px;`;
			scene.appendChild(tube);

			const particleCount = Math.max(2, Math.round(flow.strength * 6));
			for (let p = 0; p < particleCount; p++) {
				const delay = (p / particleCount) * animDuration;
				const particle = document.createElement("div");
				particle.className = "flow-particle";
				particle.dataset.flowId = flow.id;
				particle.style.cssText = `--src-z:${srcZ}px; --tgt-z:${tgtZ}px; --color:${color}; --duration:${animDuration}s; --delay:${delay}s; --x-offset:${xOffset}px; animation-play-state:${this.#simPlaying ? "running" : "paused"};`;
				scene.appendChild(particle);
			}
		});

		this.#attachFlowEvents();
	}

	// ── Flow element events (shared between full render and in-place update) ──

	#attachFlowEvents() {
		this.#shadow.querySelectorAll<HTMLElement>(".flow-tube").forEach(tube => {
			tube.addEventListener("click", (e) => {
				e.stopPropagation();
				this.dispatchEvent(new CustomEvent("flow-select", {
					detail: { flowId: tube.dataset.flowId },
					bubbles: true,
				}));
			});
			tube.addEventListener("contextmenu", (e) => {
				e.preventDefault();
				const flowId = tube.dataset.flowId!;
				const flow = this.#flows.find(f => f.id === flowId);
				if (flow && confirm(`Remove ${FLOW_LABELS[flow.kind]} flow${flow.label ? `: ${flow.label}` : ""}?`)) {
					this.dispatchEvent(new CustomEvent("flow-remove", {
						detail: { flowId },
						bubbles: true,
					}));
				}
			});
		});

		this.#shadow.querySelectorAll<HTMLElement>(".flow-particle").forEach(p => {
			p.addEventListener("click", (e) => {
				e.stopPropagation();
				this.dispatchEvent(new CustomEvent("flow-select", {
					detail: { flowId: p.dataset.flowId },
					bubbles: true,
				}));
			});
			p.addEventListener("contextmenu", (e) => {
				e.preventDefault();
				const flowId = p.dataset.flowId!;
				const flow = this.#flows.find(f => f.id === flowId);
				if (flow && confirm(`Remove ${FLOW_LABELS[flow.kind]} flow${flow.label ? `: ${flow.label}` : ""}?`)) {
					this.dispatchEvent(new CustomEvent("flow-remove", {
						detail: { flowId },
						bubbles: true,
					}));
				}
			});
		});
	}

	// ── Wiring mode ──

	#enterWiring(layerId: string, feedId: string, kind: FlowKind) {
		this.#wiringActive = true;
		this.#wiringSourceLayerId = layerId;
		this.#wiringSourceFeedId = feedId;
		this.#wiringSourceKind = kind;
		this.#applyWiringClasses();

		this.#escHandler = (e: KeyboardEvent) => {
			if (e.key === "Escape") this.#cancelWiring();
		};
		document.addEventListener("keydown", this.#escHandler);
	}

	#cancelWiring() {
		this.#wiringActive = false;
		this.#wiringSourceLayerId = "";
		this.#wiringSourceFeedId = "";
		this.#wiringSourceKind = null;

		this.#shadow.querySelectorAll(".io-chip--wiring-source, .io-chip--wiring-target, .io-chip--wiring-dimmed")
			.forEach(el => el.classList.remove("io-chip--wiring-source", "io-chip--wiring-target", "io-chip--wiring-dimmed"));
		this.#shadow.querySelectorAll(".layer-plane--wiring-dimmed")
			.forEach(el => el.classList.remove("layer-plane--wiring-dimmed"));

		if (this.#escHandler) {
			document.removeEventListener("keydown", this.#escHandler);
			this.#escHandler = null;
		}
	}

	#applyWiringClasses() {
		const sourceKind = this.#wiringSourceKind;
		const sourceLayerId = this.#wiringSourceLayerId;
		const sourceFeedId = this.#wiringSourceFeedId;

		// Output chips: highlight source, dim the rest
		this.#shadow.querySelectorAll<HTMLElement>(".io-chip--out").forEach(chip => {
			if (chip.dataset.feedId === sourceFeedId && chip.dataset.layerId === sourceLayerId) {
				chip.classList.add("io-chip--wiring-source");
			} else {
				chip.classList.add("io-chip--wiring-dimmed");
			}
		});

		// Input chips: highlight compatible targets on other layers
		this.#shadow.querySelectorAll<HTMLElement>(".io-chip--in").forEach(chip => {
			const layerId = chip.dataset.layerId!;
			const acceptKind = chip.dataset.acceptKind as FlowKind;
			if (layerId !== sourceLayerId && acceptKind === sourceKind) {
				chip.classList.add("io-chip--wiring-target");
			} else {
				chip.classList.add("io-chip--wiring-dimmed");
			}
		});

		// Dim layers with no compatible input chips
		this.#shadow.querySelectorAll<HTMLElement>(".layer-plane").forEach(plane => {
			const layerId = plane.dataset.layerId!;
			if (layerId === sourceLayerId) return;
			if (!plane.querySelector(".io-chip--wiring-target")) {
				plane.classList.add("layer-plane--wiring-dimmed");
			}
		});
	}

	// ── Events ──

	#attachEvents() {
		// Clean up previous document-level listeners to prevent leak
		if (this.#docCleanup) { this.#docCleanup(); this.#docCleanup = null; }

		// Home button — always present, fires home-click event
		this.#shadow.getElementById("home-btn")?.addEventListener("click", () => {
			this.dispatchEvent(new CustomEvent("home-click", { bubbles: true }));
		});

		// Tab clicks — dispatch event but do NOT set active yet.
		// The shell's event handler calls switchTo() and sets active only after success.
		this.#shadow.querySelectorAll<HTMLElement>(".tab").forEach(tab => {
			tab.addEventListener("click", (e) => {
				if ((e.target as HTMLElement).closest(".tab-close")) return;
				const layerId = tab.dataset.layerId!;
				const moduleId = tab.dataset.moduleId!;
				this.trackRecent(moduleId);
				this.dispatchEvent(new CustomEvent("layer-switch", {
					detail: { layerId, moduleId },
					bubbles: true,
				}));
			});

			// Drag-and-drop reorder
			tab.addEventListener("dragstart", (e) => {
				this.#draggedTabId = tab.dataset.layerId!;
				tab.classList.add("dragging");
				(e as DragEvent).dataTransfer!.effectAllowed = "move";
			});

			tab.addEventListener("dragend", () => {
				tab.classList.remove("dragging");
				this.#draggedTabId = null;
			});

			tab.addEventListener("dragover", (e) => {
				e.preventDefault();
				(e as DragEvent).dataTransfer!.dropEffect = "move";
				tab.classList.add("drag-over");
			});

			tab.addEventListener("dragleave", () => {
				tab.classList.remove("drag-over");
			});

			tab.addEventListener("drop", (e) => {
				e.preventDefault();
				tab.classList.remove("drag-over");
				if (!this.#draggedTabId || this.#draggedTabId === tab.dataset.layerId) return;

				const targetIdx = this.#layers.findIndex(l => l.id === tab.dataset.layerId);
				this.dispatchEvent(new CustomEvent("layer-reorder", {
					detail: { layerId: this.#draggedTabId, newIndex: targetIdx },
					bubbles: true,
				}));
			});

			// Touch long-press reorder (mobile)
			tab.addEventListener("touchstart", (e) => {
				if ((e.target as HTMLElement).closest(".tab-close")) return;
				const touch = e.touches[0];
				this.#touchStartX = touch.clientX;
				this.#touchStartY = touch.clientY;
				this.#touchDragTimer = setTimeout(() => {
					this.#touchDragActive = true;
					this.#touchDragLayerId = tab.dataset.layerId!;
					tab.classList.add("dragging");
					// Haptic feedback if available
					if (navigator.vibrate) navigator.vibrate(30);
					// Create floating clone
					const rect = tab.getBoundingClientRect();
					const clone = tab.cloneNode(true) as HTMLElement;
					clone.className = "tab active touch-drag-clone";
					clone.style.cssText = `position:fixed;left:${rect.left}px;top:${rect.top}px;width:${rect.width}px;z-index:9999;pointer-events:none;opacity:0.85;box-shadow:0 4px 16px rgba(0,0,0,0.4);`;
					this.#shadow.appendChild(clone);
					this.#touchDragClone = clone;
				}, 400);
			}, { passive: true });

			tab.addEventListener("touchmove", (e) => {
				const touch = e.touches[0];
				// Cancel long-press if finger moved too far before activation
				if (!this.#touchDragActive && this.#touchDragTimer) {
					const dx = touch.clientX - this.#touchStartX;
					const dy = touch.clientY - this.#touchStartY;
					if (Math.abs(dx) > 10 || Math.abs(dy) > 10) {
						clearTimeout(this.#touchDragTimer);
						this.#touchDragTimer = null;
					}
					return;
				}
				if (!this.#touchDragActive) return;
				e.preventDefault();
				// Move the floating clone
				if (this.#touchDragClone) {
					this.#touchDragClone.style.left = `${touch.clientX - this.#touchDragClone.offsetWidth / 2}px`;
					this.#touchDragClone.style.top = `${touch.clientY - this.#touchDragClone.offsetHeight - 10}px`;
				}
				// Highlight the tab under the finger
				this.#shadow.querySelectorAll<HTMLElement>(".tab").forEach(t => t.classList.remove("drag-over"));
				const elUnder = this.#shadow.elementFromPoint(touch.clientX, touch.clientY) as HTMLElement | null;
				const tabUnder = elUnder?.closest?.(".tab") as HTMLElement | null;
				if (tabUnder && tabUnder.dataset.layerId !== this.#touchDragLayerId) {
					tabUnder.classList.add("drag-over");
				}
			}, { passive: false });

			tab.addEventListener("touchend", (e) => {
				if (this.#touchDragTimer) { clearTimeout(this.#touchDragTimer); this.#touchDragTimer = null; }
				if (!this.#touchDragActive) return;
				e.preventDefault();
				// Find drop target
				const touch = e.changedTouches[0];
				this.#shadow.querySelectorAll<HTMLElement>(".tab").forEach(t => t.classList.remove("drag-over", "dragging"));
				const elUnder = this.#shadow.elementFromPoint(touch.clientX, touch.clientY) as HTMLElement | null;
				const tabUnder = elUnder?.closest?.(".tab") as HTMLElement | null;
				if (tabUnder && tabUnder.dataset.layerId !== this.#touchDragLayerId) {
					const targetIdx = this.#layers.findIndex(l => l.id === tabUnder.dataset.layerId);
					this.dispatchEvent(new CustomEvent("layer-reorder", {
						detail: { layerId: this.#touchDragLayerId, newIndex: targetIdx },
						bubbles: true,
					}));
				}
				// Cleanup
				if (this.#touchDragClone) { this.#touchDragClone.remove(); this.#touchDragClone = null; }
				this.#touchDragActive = false;
				this.#touchDragLayerId = null;
			});

			tab.addEventListener("touchcancel", () => {
				if (this.#touchDragTimer) { clearTimeout(this.#touchDragTimer); this.#touchDragTimer = null; }
				if (this.#touchDragClone) { this.#touchDragClone.remove(); this.#touchDragClone = null; }
				this.#shadow.querySelectorAll<HTMLElement>(".tab").forEach(t => t.classList.remove("drag-over", "dragging"));
				this.#touchDragActive = false;
				this.#touchDragLayerId = null;
			});
		});

		// Close buttons
		this.#shadow.querySelectorAll<HTMLElement>(".tab-close").forEach(btn => {
			btn.addEventListener("click", (e) => {
				e.stopPropagation();
				const layerId = (btn as HTMLElement).dataset.close!;
				this.dispatchEvent(new CustomEvent("layer-close", {
					detail: { layerId },
					bubbles: true,
				}));
			});
		});

		// Add button — open the header's app-switcher sidebar
		const addBtn = this.#shadow.getElementById("add-btn");
		const openAppSwitcher = (e: Event) => {
			e.stopPropagation();
			e.preventDefault();
			const switcher = document.querySelector("rstack-app-switcher") as any;
			if (switcher?.open) {
				switcher.open();
			}
		};
		addBtn?.addEventListener("click", openAppSwitcher);
		addBtn?.addEventListener("touchend", openAppSwitcher);

		// Close-all button
		const closeAllBtn = this.#shadow.getElementById("close-all-btn");
		if (closeAllBtn) {
			const handleCloseAll = (e: Event) => {
				e.stopPropagation();
				e.preventDefault();
				const count = this.#layers.length;
				if (!confirm(`Close all ${count} rApp tabs?`)) return;
				this.dispatchEvent(new CustomEvent("layers-close-all", { bubbles: true }));
			};
			closeAllBtn.addEventListener("click", handleCloseAll);
			closeAllBtn.addEventListener("touchend", handleCloseAll);
		}

		// Add menu items — click + touch support
		this.#shadow.querySelectorAll<HTMLElement>(".add-menu-item").forEach(item => {
			const handleSelect = (e: Event) => {
				e.stopPropagation();
				e.preventDefault();
				const moduleId = item.dataset.addModule!;
				const isOpen = item.dataset.moduleOpen === "true";
				this.#addMenuOpen = false;
				this.trackRecent(moduleId);

				if (isOpen) {
					// Surface existing tab instead of adding a duplicate
					const existing = this.#layers.find(l => l.moduleId === moduleId);
					if (existing) {
						this.setAttribute("active", existing.id);
						this.dispatchEvent(new CustomEvent("layer-switch", {
							detail: { layerId: existing.id, moduleId },
							bubbles: true,
						}));
					}
				} else {
					this.dispatchEvent(new CustomEvent("layer-add", {
						detail: { moduleId },
						bubbles: true,
					}));
				}
			};
			item.addEventListener("click", handleSelect);
			item.addEventListener("touchend", handleSelect);
		});

		// Add Space Layer button handler
		const spaceLayerBtn = this.#shadow.getElementById("add-space-layer-btn");
		if (spaceLayerBtn) {
			spaceLayerBtn.addEventListener("click", (e) => {
				e.stopPropagation();
				this.#addMenuOpen = false;
				this.#showSpaceLayerPicker();
			});
		}

		// Close add menu on outside click/touch
		if (this.#addMenuOpen) {
			const handler = () => {
				this.#addMenuOpen = false;
				this.#render();
				document.removeEventListener("click", handler);
				document.removeEventListener("touchend", handler);
			};
			setTimeout(() => {
				document.addEventListener("click", handler);
				document.addEventListener("touchend", handler);
			}, 0);
		}

		// View toggle
		const viewToggle = this.#shadow.getElementById("view-toggle");
		viewToggle?.addEventListener("click", () => {
			const newMode = this.#viewMode === "flat" ? "stack" : "flat";
			this.#viewMode = newMode;
			this.setAttribute("view-mode", newMode);
			this.dispatchEvent(new CustomEvent("view-toggle", {
				detail: { mode: newMode },
				bubbles: true,
			}));
			this.#render();
		});

		// 3D Stack view: layer clicks + drag-to-connect
		this.#shadow.querySelectorAll<HTMLElement>(".layer-plane").forEach(plane => {
			const layerId = plane.dataset.layerId!;

			// Click to switch layer — do NOT set active here, let shell handler confirm
			plane.addEventListener("click", (e) => {
				if (this.#flowDragSource || this.#orbitDragging) return;
				const layer = this.#layers.find(l => l.id === layerId);
				if (layer) {
					this.dispatchEvent(new CustomEvent("layer-switch", {
						detail: { layerId, moduleId: layer.moduleId },
						bubbles: true,
					}));
				}
			});

			// Drag-to-connect: pointerdown starts a flow drag (skip if wiring)
			plane.addEventListener("pointerdown", (e) => {
				if ((e as PointerEvent).button !== 0) return;
				if (this.#wiringActive) return;
				e.stopPropagation(); // prevent orbit
				this.#flowDragSource = layerId;
				plane.classList.add("flow-drag-source");
			});
		});

		// 3D scene: orbit controls (drag on empty space to rotate)
		const sceneContainer = this.#shadow.getElementById("stack-3d");
		if (sceneContainer) {
			let orbitPointerId: number | null = null;

			sceneContainer.addEventListener("pointerdown", (e) => {
				const pe = e as PointerEvent;
				// Only orbit on left-click on empty space (not on layer planes)
				if (pe.button !== 0) return;
				if ((e.target as HTMLElement).closest(".layer-plane")) return;
				this.#orbitDragging = true;
				this.#orbitLastX = pe.clientX;
				this.#orbitLastY = pe.clientY;
				orbitPointerId = pe.pointerId;
				sceneContainer.setPointerCapture(pe.pointerId);
				sceneContainer.style.cursor = "grabbing";
			});

			// Collect all layer planes + their rects for drag target detection
			const layerPlanes = [...this.#shadow.querySelectorAll<HTMLElement>(".layer-plane")];

			const onPointerMove = (e: PointerEvent) => {
				if (this.#orbitDragging) {
					const dx = e.clientX - this.#orbitLastX;
					const dy = e.clientY - this.#orbitLastY;
					if (e.shiftKey) {
						// Shift+drag: rotate around Z axis
						this.#sceneRotZ += dx * 0.3;
					} else {
						// Normal drag: rotate around X and Y axes
						this.#sceneRotY += dx * 0.3;
						this.#sceneRotX = Math.max(-180, Math.min(180, this.#sceneRotX - dy * 0.3));
					}
					this.#orbitLastX = e.clientX;
					this.#orbitLastY = e.clientY;
					const scene = this.#shadow.getElementById("stack-scene");
					if (scene) scene.style.transform = `rotateX(${this.#sceneRotX}deg) rotateY(${this.#sceneRotY}deg) rotateZ(${this.#sceneRotZ}deg)`;
				}

				// Drag-to-connect: track target via bounding rects (robust for 3D)
				if (this.#flowDragSource) {
					let newTarget: string | null = null;
					for (const p of layerPlanes) {
						const r = p.getBoundingClientRect();
						if (e.clientX >= r.left && e.clientX <= r.right &&
							e.clientY >= r.top && e.clientY <= r.bottom &&
							p.dataset.layerId !== this.#flowDragSource) {
							newTarget = p.dataset.layerId!;
							break;
						}
					}
					if (newTarget !== this.#flowDragTarget) {
						this.#shadow.querySelectorAll(".flow-drag-target")
							.forEach(el => el.classList.remove("flow-drag-target"));
						this.#flowDragTarget = newTarget;
						if (newTarget) {
							this.#shadow.querySelector(`.layer-plane[data-layer-id="${newTarget}"]`)
								?.classList.add("flow-drag-target");
						}
					}
				}
			};

			const onPointerUp = (e: PointerEvent) => {
				if (this.#orbitDragging) {
					this.#orbitDragging = false;
					if (orbitPointerId !== null) {
						try { sceneContainer.releasePointerCapture(orbitPointerId); } catch {}
						orbitPointerId = null;
					}
					sceneContainer.style.cursor = "";
				}
				// Complete flow drag
				if (this.#flowDragSource && this.#flowDragTarget) {
					this.#openFlowDialog(this.#flowDragSource, this.#flowDragTarget);
				}
				this.#flowDragSource = null;
				this.#flowDragTarget = null;
				this.#shadow.querySelectorAll(".flow-drag-source, .flow-drag-target").forEach(el => {
					el.classList.remove("flow-drag-source", "flow-drag-target");
				});
			};

			// Attach to document for drag continuity (cleaned up on re-render)
			document.addEventListener("pointermove", onPointerMove);
			document.addEventListener("pointerup", onPointerUp);
			this.#docCleanup = () => {
				document.removeEventListener("pointermove", onPointerMove);
				document.removeEventListener("pointerup", onPointerUp);
			};

			// Scroll to zoom
			sceneContainer.addEventListener("wheel", (e) => {
				e.preventDefault();
				this.#scenePerspective = Math.max(400, Math.min(3000, this.#scenePerspective + (e as WheelEvent).deltaY * 2));
				sceneContainer.style.perspective = `${this.#scenePerspective}px`;
			}, { passive: false });
		}

		// Flow tube + particle click/right-click events
		this.#attachFlowEvents();

		// Wiring mode: output chip clicks
		this.#shadow.querySelectorAll<HTMLElement>(".io-chip--out").forEach(chip => {
			chip.style.cursor = "pointer";
			chip.addEventListener("click", (e) => {
				e.stopPropagation();
				const feedId = chip.dataset.feedId!;
				const feedKind = chip.dataset.feedKind as FlowKind;
				const chipLayerId = chip.dataset.layerId!;

				if (this.#wiringActive && this.#wiringSourceFeedId === feedId && this.#wiringSourceLayerId === chipLayerId) {
					this.#cancelWiring();
				} else {
					if (this.#wiringActive) this.#cancelWiring();
					this.#enterWiring(chipLayerId, feedId, feedKind);
				}
			});
		});

		// Wiring mode: input chip clicks
		this.#shadow.querySelectorAll<HTMLElement>(".io-chip--in").forEach(chip => {
			chip.addEventListener("click", (e) => {
				e.stopPropagation();
				if (!this.#wiringActive || !this.#wiringSourceKind) return;

				const acceptKind = chip.dataset.acceptKind as FlowKind;
				const targetLayerId = chip.dataset.layerId!;
				if (targetLayerId === this.#wiringSourceLayerId) return;
				if (acceptKind !== this.#wiringSourceKind) return;

				const flowId = `flow-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
				this.dispatchEvent(new CustomEvent("flow-create", {
					detail: {
						flow: {
							id: flowId,
							kind: this.#wiringSourceKind,
							sourceLayerId: this.#wiringSourceLayerId,
							targetLayerId,
							strength: 0.5,
							active: true,
							meta: { sourceFeedId: this.#wiringSourceFeedId },
						}
					},
					bubbles: true,
				}));
				this.#cancelWiring();
			});
		});

		// Cancel wiring on empty space click
		if (sceneContainer) {
			sceneContainer.addEventListener("click", (e) => {
				if (this.#wiringActive && !(e.target as HTMLElement).closest(".io-chip")) {
					this.#cancelWiring();
				}
			});
		}

		// Time scrubber controls
		const scrubberRange = this.#shadow.getElementById("scrubber-range") as HTMLInputElement | null;
		const scrubberLabel = this.#shadow.getElementById("scrubber-label");
		const scrubberPlaypause = this.#shadow.getElementById("scrubber-playpause");

		scrubberRange?.addEventListener("input", () => {
			this.#simSpeed = parseFloat(scrubberRange.value);
			if (scrubberLabel) scrubberLabel.textContent = `${this.#simSpeed.toFixed(1)}x`;
			// Update particle durations without full re-render
			const dur = 2 / this.#simSpeed;
			this.#shadow.querySelectorAll<HTMLElement>(".flow-particle").forEach(p => {
				p.style.setProperty("--duration", `${dur}s`);
			});
		});

		scrubberPlaypause?.addEventListener("click", () => {
			this.#simPlaying = !this.#simPlaying;
			scrubberPlaypause.textContent = this.#simPlaying ? "⏸" : "▶";
			scrubberPlaypause.title = this.#simPlaying ? "Pause" : "Play";
			const state = this.#simPlaying ? "running" : "paused";
			this.#shadow.querySelectorAll<HTMLElement>(".flow-particle").forEach(p => {
				p.style.animationPlayState = state;
			});
		});

		// Flow dialog events
		if (this.#flowDialogOpen) {
			// Default to first compatible kind, or "data" as fallback
			const compatible = this.#getCompatibleKinds(this.#flowDialogSourceId, this.#flowDialogTargetId);
			const defaultKind: FlowKind = compatible.size > 0 ? [...compatible][0] : "data";
			let selectedKind: FlowKind = defaultKind;

			this.#shadow.querySelectorAll<HTMLElement>(".flow-kind-btn:not(.disabled)").forEach(btn => {
				btn.addEventListener("click", () => {
					this.#shadow.querySelectorAll(".flow-kind-btn").forEach(b => b.classList.remove("selected"));
					btn.classList.add("selected");
					selectedKind = btn.dataset.kind as FlowKind;
				});
			});

			// Select default kind
			const defaultBtn = this.#shadow.querySelector(`.flow-kind-btn[data-kind="${defaultKind}"]:not(.disabled)`) ||
			                   this.#shadow.querySelector('.flow-kind-btn:not(.disabled)');
			defaultBtn?.classList.add("selected");
			if (defaultBtn) selectedKind = (defaultBtn as HTMLElement).dataset.kind as FlowKind;

			this.#shadow.getElementById("flow-cancel")?.addEventListener("click", () => {
				this.#closeFlowDialog();
			});

			this.#shadow.getElementById("flow-create")?.addEventListener("click", () => {
				const label = (this.#shadow.getElementById("flow-label-input") as HTMLInputElement)?.value || "";
				const strength = parseFloat((this.#shadow.getElementById("flow-strength-input") as HTMLInputElement)?.value || "0.5");

				const flowId = `flow-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
				this.dispatchEvent(new CustomEvent("flow-create", {
					detail: {
						flow: {
							id: flowId,
							kind: selectedKind,
							sourceLayerId: this.#flowDialogSourceId,
							targetLayerId: this.#flowDialogTargetId,
							label: label || undefined,
							strength,
							active: true,
						}
					},
					bubbles: true,
				}));

				this.#closeFlowDialog();
			});
		}
	}

	static define(tag = "rstack-tab-bar") {
		if (!customElements.get(tag)) customElements.define(tag, RStackTabBar);
	}
}

// ── SVG Icons ──

const ICON_STACK = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5">
	<rect x="2" y="2" width="12" height="3" rx="1" />
	<rect x="2" y="6.5" width="12" height="3" rx="1" />
	<rect x="2" y="11" width="12" height="3" rx="1" />
</svg>`;

const ICON_FLAT = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5">
	<rect x="1" y="4" width="14" height="9" rx="1.5" />
	<path d="M1 6h14" />
	<circle cx="3.5" cy="5" r="0.5" fill="currentColor" />
	<circle cx="5.5" cy="5" r="0.5" fill="currentColor" />
	<circle cx="7.5" cy="5" r="0.5" fill="currentColor" />
</svg>`;

// ── Styles ──

const STYLES = `
:host { display: block; flex: 1; min-width: 0; }

/* ── Tab bar (flat mode) ── */

.tab-bar {
  display: flex;
  align-items: center;
  gap: 0;
  min-height: 36px;
  padding: 0 4px;
  overflow: visible;
}

.tabs-scroll {
  display: flex;
  align-items: center;
  gap: 4px;
  min-width: 0;
  overflow-x: auto;
  overflow-y: visible;
  scrollbar-width: none;
  position: relative;
}
.tabs-scroll::-webkit-scrollbar { display: none; }

.tab-actions {
  display: flex;
  align-items: center;
  gap: 4px;
  flex-shrink: 0;
  margin-left: auto;
}

/* ── Individual tab ── */

.tab {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 5px 12px;
  border-radius: 6px 6px 0 0;
  cursor: pointer;
  white-space: nowrap;
  font-size: 0.8rem;
  font-weight: 500;
  transition: background 0.15s, opacity 0.15s, border-color 0.15s;
  user-select: none;
  position: relative;
  flex-shrink: 0;
  color: var(--rs-text-muted);
  background: rgba(255,255,255,0.03);
  border: 1px solid transparent;
  border-bottom: none;
}
.tab:hover {
  background: var(--rs-bg-hover);
  color: var(--rs-text-primary);
  border-color: var(--rs-input-border);
}
.tab.active {
  background: var(--rs-bg-surface);
  color: var(--rs-text-primary);
  border-color: var(--rs-input-border);
}
.tab--dashboard {
  cursor: default;
  pointer-events: none;
  opacity: 0.8;
}
.tab--dashboard .tab-badge svg {
  display: block;
}

/* ── Persistent home button ── */
.tab-home {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1px;
  padding: 4px 10px;
  border: none;
  background: none;
  color: var(--rs-text-muted);
  cursor: pointer;
  border-radius: 6px;
  transition: color 0.15s, background 0.15s;
  flex-shrink: 0;
  font-size: inherit;
  font-family: inherit;
  line-height: 1;
}
.tab-home:hover {
  color: var(--rs-text-primary);
  background: rgba(255,255,255,0.06);
}
.tab-home--active {
  color: #14b8a6;
  background: rgba(20,184,166,0.1);
}
.tab-home__label {
  font-size: 0.55rem;
  font-weight: 600;
  letter-spacing: 0.02em;
}

/* Active indicator line at bottom */
.tab-indicator {
  position: absolute;
  bottom: -1px;
  left: 0;
  right: 0;
  height: 2px;
  border-radius: 2px 2px 0 0;
  opacity: 0;
  transition: opacity 0.15s;
}
.tab.active .tab-indicator { opacity: 1; }

.tab-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 20px;
  height: 20px;
  border-radius: 4px;
  font-size: 0.6rem;
  font-weight: 900;
  color: #1e293b;
  line-height: 1;
  flex-shrink: 0;
  white-space: nowrap;
  padding: 0 2px;
}

.tab-label {
  font-size: 0.8rem;
}

.tab-close {
  display: none;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  border: none;
  border-radius: 3px;
  background: transparent;
  color: inherit;
  font-size: 0.85rem;
  cursor: pointer;
  opacity: 0.35;
  transition: opacity 0.15s, background 0.15s;
  padding: 0;
  line-height: 1;
}
.tab.active .tab-close { display: flex; opacity: 0.6; }
.tab.active .tab-close:hover { opacity: 1; background: rgba(239,68,68,0.2); color: #ef4444; }

/* Space layer tab styling */
.tab--space-layer {
  border: 1px dashed rgba(99,102,241,0.4);
  background: rgba(99,102,241,0.05);
}
.tab--space-layer.active {
  border-color: rgba(99,102,241,0.6);
  background: rgba(99,102,241,0.1);
}
.tab-space-tag {
  font-size: 0.6rem;
  padding: 1px 4px;
  border-radius: 3px;
  background: rgba(99,102,241,0.15);
  color: #a5b4fc;
  max-width: 50px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  flex-shrink: 0;
}
.tab-readonly-tag {
  font-size: 0.65rem;
  flex-shrink: 0;
  opacity: 0.7;
}

/* ── Drag states ── */

.tab { cursor: grab; }
.tab.dragging { opacity: 0.4; cursor: grabbing; }
.tab.drag-over { box-shadow: inset 2px 0 0 #22d3ee; }
.touch-drag-clone { transition: none; border-radius: 6px; }

/* ── Add button ── */

.tab-add-wrap {
  display: flex;
  align-items: center;
  position: relative;
  flex-shrink: 0;
  margin-left: 2px;
  z-index: 1000;
}

.tab-add {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 4px 12px;
  min-height: 44px;
  border: none;
  border-radius: 6px 6px 0 0;
  background: transparent;
  color: var(--rs-text-muted);
  font-size: 0.95rem;
  cursor: pointer;
  transition: background 0.15s, color 0.15s;
  flex-shrink: 0;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  white-space: nowrap;
  user-select: none;
}
.tab-add:hover, .tab-add:active {
  background: var(--rs-bg-hover);
  color: var(--rs-text-primary);
}
.tab-close-all {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 4px 8px;
  min-height: 44px;
  border: none;
  border-radius: 6px 6px 0 0;
  background: transparent;
  color: var(--rs-text-muted);
  font-size: 0.85rem;
  cursor: pointer;
  transition: background 0.15s, color 0.15s;
  flex-shrink: 0;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  white-space: nowrap;
  user-select: none;
  opacity: 0.6;
}
.tab-close-all:hover, .tab-close-all:active {
  background: rgba(239,68,68,0.15);
  color: #ef4444;
  opacity: 1;
}

/* ── Add menu ── */

.add-menu {
  position: absolute;
  top: 100%;
  left: 0;
  right: auto;
  margin-top: 4px;
  min-width: 260px;
  max-height: 400px;
  overflow-y: auto;
  border-radius: 10px;
  padding: 4px;
  z-index: 1000;
  box-shadow: var(--rs-shadow-lg);
  scrollbar-width: thin;
  background: var(--rs-bg-surface);
  border: 1px solid var(--rs-border);
}

.add-menu-category {
  padding: 6px 10px 3px;
  font-size: 0.6rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  opacity: 0.45;
  user-select: none;
}
.add-menu-category:not(:first-child) {
  border-top: 1px solid var(--rs-border-subtle);
  margin-top: 2px;
  padding-top: 8px;
}

.add-menu-item {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 8px 10px;
  min-height: 40px;
  border: none;
  border-radius: 6px;
  background: transparent;
  color: inherit;
  font-size: 0.8rem;
  cursor: pointer;
  text-align: left;
  transition: background 0.12s;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
}
.add-menu-item:hover, .add-menu-item:active { background: var(--rs-bg-hover); }

.add-menu-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 24px;
  height: 24px;
  border-radius: 5px;
  font-size: 0.65rem;
  font-weight: 900;
  color: #1e293b;
  flex-shrink: 0;
  white-space: nowrap;
  padding: 0 3px;
}

.add-menu-icon {
  font-size: 1.1rem;
  width: 24px;
  text-align: center;
  flex-shrink: 0;
}

.add-menu-text {
  display: flex;
  flex-direction: column;
  min-width: 0;
  flex: 1;
}

.add-menu-name {
  font-size: 0.8rem;
  font-weight: 600;
  display: flex;
  align-items: center;
  gap: 4px;
}

.add-menu-emoji {
  font-size: 0.8rem;
  flex-shrink: 0;
}

.add-menu-desc {
  font-size: 0.65rem;
  opacity: 0.5;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.add-menu-item--open {
  opacity: 0.55;
}
.add-menu-open-dot {
  margin-left: auto;
  font-size: 0.5rem;
  color: #22d3ee;
  flex-shrink: 0;
  padding-left: 6px;
}

.add-menu-empty {
  padding: 12px;
  text-align: center;
  font-size: 0.75rem;
  opacity: 0.5;
}

.add-menu-divider {
  height: 1px;
  margin: 4px 8px;
  background: var(--rs-border-subtle);
}

.add-menu-item--space-layer {
  border-top: none;
}
.add-menu-item--space-layer .add-menu-icon {
  font-size: 1rem;
}

/* ── Space Layer Picker ── */

.space-layer-picker {
  position: absolute;
  top: 100%;
  right: 0;
  min-width: 240px;
  max-height: 320px;
  overflow-y: auto;
  background: var(--rs-bg-surface);
  border: 1px solid var(--rs-border);
  border-radius: 12px;
  box-shadow: var(--rs-shadow-lg);
  z-index: 10002;
  margin-top: 4px;
}
.space-layer-picker__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 14px 6px;
  font-size: 0.75rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--rs-text-secondary);
}
.space-layer-picker__close {
  background: none;
  border: none;
  color: var(--rs-text-muted);
  font-size: 1.1rem;
  cursor: pointer;
  padding: 2px 6px;
  border-radius: 4px;
  line-height: 1;
}
.space-layer-picker__close:hover {
  color: var(--rs-text-primary);
  background: var(--rs-bg-hover);
}
.space-layer-picker__item {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 8px 14px;
  border: none;
  background: none;
  cursor: pointer;
  transition: background 0.12s;
  text-align: left;
  font-family: inherit;
  color: var(--rs-text-primary);
}
.space-layer-picker__item:hover {
  background: var(--rs-bg-hover);
}
.space-layer-picker__icon {
  font-size: 1.1rem;
  flex-shrink: 0;
}
.space-layer-picker__name {
  font-size: 0.85rem;
  font-weight: 500;
  flex: 1;
}
.space-layer-picker__role {
  font-size: 0.7rem;
  color: var(--rs-text-muted);
  flex-shrink: 0;
}
.space-layer-picker__empty {
  padding: 16px;
  text-align: center;
  font-size: 0.8rem;
  color: var(--rs-text-muted);
}

/* ── View toggle ── */

.view-toggle {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border: none;
  border-radius: 6px;
  background: transparent;
  color: var(--rs-text-muted);
  cursor: pointer;
  transition: background 0.15s, color 0.15s;
}
.view-toggle:hover {
  background: var(--rs-bg-hover);
  color: var(--rs-text-primary);
}
.view-toggle.active {
  color: #22d3ee;
  background: rgba(34,211,238,0.1);
}

/* ── Stack view (3D) ── */

.stack-view {
  padding: 12px;
  overflow: visible;
  max-height: 80vh;
  transition: max-height 0.3s ease;
  position: relative;
  background: var(--rs-bg-surface);
  background: color-mix(in srgb, var(--rs-bg-surface) 50%, transparent);
  border-bottom: 1px solid var(--rs-border-subtle);
}

.stack-view-3d {
  perspective-origin: 50% 50%;
  width: 100%;
  height: 420px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: grab;
  touch-action: none;
  user-select: none;
  overflow: visible;
}

.stack-scene {
  transform-style: preserve-3d;
  position: relative;
  width: 320px;
  height: 80px;
}

/* ── Layer planes ── */

.layer-plane {
  position: absolute;
  width: 320px;
  min-height: 44px;
  border-radius: 10px;
  border: 1px solid var(--layer-color);
  padding: 10px 14px;
  cursor: pointer;
  transition: border-color 0.2s, box-shadow 0.2s;
  transform-style: preserve-3d;
  background: var(--rs-bg-surface);
  background: color-mix(in srgb, var(--rs-bg-surface) 65%, transparent);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  color: var(--rs-text-primary);
}

.layer-plane:hover {
  border-width: 2px;
  box-shadow: 0 0 16px color-mix(in srgb, var(--layer-color) 30%, transparent);
}

.layer-plane--active {
  border-width: 2px;
  box-shadow: 0 0 24px color-mix(in srgb, var(--layer-color) 40%, transparent);
  background: var(--rs-bg-surface);
  background: color-mix(in srgb, var(--rs-bg-surface) 85%, transparent);
}

/* Drag-to-connect visual states */
.layer-plane.flow-drag-source {
  border-style: dashed;
  border-width: 2px;
}
.layer-plane.flow-drag-target {
  border-width: 3px;
  box-shadow: 0 0 30px color-mix(in srgb, var(--layer-color) 50%, transparent);
}

.layer-header {
  display: flex;
  align-items: center;
  gap: 8px;
}

.layer-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 24px;
  height: 24px;
  border-radius: 5px;
  font-size: 0.65rem;
  font-weight: 900;
  color: #1e293b;
  flex-shrink: 0;
  white-space: nowrap;
  padding: 0 3px;
}

.layer-name {
  font-size: 0.8rem;
  font-weight: 600;
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ── I/O chip system ── */
.layer-io {
  display: flex;
  justify-content: space-between;
  gap: 6px;
  margin-top: 5px;
}

.io-col {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0;
}
.io-col--in { align-items: flex-start; }
.io-col--out { align-items: flex-end; }

.io-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 0.55rem;
  font-weight: 600;
  padding: 2px 7px;
  border-radius: 9px;
  white-space: nowrap;
  max-width: 140px;
  overflow: hidden;
  text-overflow: ellipsis;
  transition: opacity 0.15s, box-shadow 0.15s;
  cursor: default;
}

.io-chip--out {
  background: rgba(148,163,184,0.18);
  background: color-mix(in srgb, var(--chip-color) 18%, transparent);
  border: 1px solid rgba(148,163,184,0.4);
  border: 1px solid color-mix(in srgb, var(--chip-color) 40%, transparent);
  color: var(--chip-color);
}

.io-chip--in {
  background: transparent;
  border: 1px dashed rgba(148,163,184,0.35);
  border: 1px dashed color-mix(in srgb, var(--chip-color) 35%, transparent);
  color: var(--rs-text-secondary);
  color: color-mix(in srgb, var(--chip-color) 70%, var(--rs-text-primary));
}

.io-chip--contained {
  opacity: 0.5;
}

.io-chip:hover {
  opacity: 1;
  box-shadow: 0 0 8px color-mix(in srgb, var(--chip-color) 30%, transparent);
}

.io-dot {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  flex-shrink: 0;
}

.io-chip--out .io-dot {
  background: var(--chip-color);
  box-shadow: 0 0 4px var(--chip-color);
}

.io-chip--in .io-dot {
  background: transparent;
  box-shadow: inset 0 0 0 1.5px var(--chip-color);
}

.io-lock {
  font-size: 0.5rem;
  margin-left: 2px;
  opacity: 0.6;
}

/* ── Flow tubes ── */

.flow-tube {
  position: absolute;
  left: 50%;
  top: 50%;
  border-radius: 3px;
  pointer-events: auto;
  cursor: pointer;
  transform-style: preserve-3d;
  background: linear-gradient(to bottom, transparent, var(--color) 15%, var(--color) 85%, transparent);
  opacity: 0.6;
  transition: opacity 0.2s;
}

.flow-tube:hover {
  opacity: 1;
}

.flow-tube::after {
  content: '';
  position: absolute;
  inset: -2px;
  border-radius: 3px;
  background: var(--color);
  filter: blur(4px);
  opacity: 0;
  animation: tube-pulse 3s ease-in-out infinite;
}

@keyframes tube-pulse {
  0%, 100% { opacity: 0; }
  50% { opacity: 0.4; }
}

/* ── Flow particles ── */

.flow-particle {
  position: absolute;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--color);
  box-shadow: 0 0 6px var(--color);
  left: calc(50% + var(--x-offset, 0px));
  top: 50%;
  margin-left: -3px;
  margin-top: -3px;
  pointer-events: auto;
  cursor: pointer;
  animation: flow-particle var(--duration) linear var(--delay) infinite;
}

@keyframes flow-particle {
  0%   { transform: translateZ(var(--src-z)); opacity: 0; }
  8%   { opacity: 1; }
  85%  { opacity: 1; }
  95%  { opacity: 0.5; transform: translateZ(var(--tgt-z)); }
  100% { transform: translateZ(var(--tgt-z)); opacity: 0; }
}

/* ── Wiring mode ── */

.io-chip--wiring-source {
  animation: wiring-glow 1s ease-in-out infinite;
  box-shadow: 0 0 12px var(--chip-color);
  z-index: 10;
  opacity: 1 !important;
}

@keyframes wiring-glow {
  0%, 100% { box-shadow: 0 0 8px var(--chip-color); }
  50% { box-shadow: 0 0 20px var(--chip-color); }
}

.io-chip--wiring-target {
  animation: wiring-breathe 1.5s ease-in-out infinite;
  border-style: solid !important;
  cursor: pointer !important;
  pointer-events: auto !important;
  opacity: 1 !important;
}

@keyframes wiring-breathe {
  0%, 100% { opacity: 0.7; }
  50% { opacity: 1; box-shadow: 0 0 10px var(--chip-color); }
}

.io-chip--wiring-dimmed {
  opacity: 0.2 !important;
  pointer-events: none !important;
}

.layer-plane--wiring-dimmed {
  opacity: 0.3 !important;
}

/* ── Legend ── */

.stack-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  justify-content: center;
  padding: 6px 0;
}

.legend-item {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 0.65rem;
  opacity: 0.6;
}

.legend-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  flex-shrink: 0;
}

/* ── Time scrubber ── */

.time-scrubber {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 16px;
  justify-content: center;
}

.scrubber-playpause {
  width: 24px;
  height: 24px;
  border: none;
  border-radius: 50%;
  background: rgba(34,211,238,0.15);
  color: #22d3ee;
  font-size: 0.7rem;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background 0.15s;
  flex-shrink: 0;
}
.scrubber-playpause:hover { background: rgba(34,211,238,0.25); }

.scrubber-range {
  flex: 1;
  max-width: 200px;
  accent-color: #22d3ee;
  height: 4px;
}

.scrubber-label {
  font-size: 0.65rem;
  font-weight: 700;
  opacity: 0.5;
  width: 28px;
  text-align: center;
  flex-shrink: 0;
}

/* ── Stack hint ── */

.stack-hint {
  text-align: center;
  font-size: 0.6rem;
  opacity: 0.35;
  padding: 2px 0 4px;
}

/* ── Flow creation dialog ── */

.flow-dialog {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 280px;
  border-radius: 12px;
  padding: 16px;
  z-index: 50;
  box-shadow: var(--rs-shadow-xl);
  background: var(--rs-bg-surface);
  border: 1px solid var(--rs-border);
  color: var(--rs-text-primary);
}

.flow-dialog-header {
  font-size: 0.85rem;
  font-weight: 700;
  margin-bottom: 10px;
}

.flow-dialog-route {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 12px;
  justify-content: center;
}

.flow-dialog-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 3px 8px;
  border-radius: 5px;
  font-size: 0.65rem;
  font-weight: 900;
  color: #1e293b;
  white-space: nowrap;
}

.flow-dialog-arrow {
  font-size: 1rem;
  opacity: 0.4;
}

.flow-dialog-field {
  margin-bottom: 10px;
}

.flow-dialog-label {
  display: block;
  font-size: 0.6rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  opacity: 0.5;
  margin-bottom: 4px;
}

.flow-kind-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 4px;
}

.flow-kind-btn {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 5px 8px;
  border: 1px solid transparent;
  border-radius: 5px;
  background: transparent;
  color: inherit;
  font-size: 0.7rem;
  cursor: pointer;
  transition: background 0.12s, border-color 0.12s;
}
.flow-kind-btn:hover { background: var(--rs-bg-hover); }
.flow-kind-btn.selected {
  border-color: var(--kind-color);
  background: rgba(148,163,184,0.1);
  background: color-mix(in srgb, var(--kind-color) 10%, transparent);
}

.flow-kind-btn.disabled {
  opacity: 0.25;
  pointer-events: none;
  cursor: default;
}

.flow-kind-count {
  margin-left: auto;
  font-size: 0.55rem;
  font-weight: 700;
  background: var(--kind-color);
  color: var(--rs-text-inverse);
  width: 16px;
  height: 16px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}

.flow-kind-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  flex-shrink: 0;
}

.flow-dialog-input {
  width: 100%;
  padding: 6px 8px;
  border: 1px solid var(--rs-border);
  border-radius: 5px;
  background: var(--rs-btn-secondary-bg);
  color: inherit;
  font-size: 0.75rem;
  outline: none;
}
.flow-dialog-input:focus { border-color: rgba(34,211,238,0.4); }

.flow-dialog-range {
  width: 100%;
  accent-color: #22d3ee;
}

.flow-dialog-actions {
  display: flex;
  gap: 6px;
  justify-content: flex-end;
  margin-top: 4px;
}

.flow-dialog-cancel, .flow-dialog-create {
  padding: 5px 14px;
  border-radius: 6px;
  font-size: 0.7rem;
  font-weight: 600;
  cursor: pointer;
}

.flow-dialog-cancel {
  border: 1px solid var(--rs-border);
  background: transparent;
  color: inherit;
  opacity: 0.6;
}
.flow-dialog-cancel:hover { opacity: 1; }

.flow-dialog-create {
  border: none;
  background: #22d3ee;
  color: var(--rs-text-inverse);
}
.flow-dialog-create:hover { opacity: 0.85; }

/* ── Responsive ── */

@media (max-width: 640px) {
  .tab-label { display: none; }
  .tab { padding: 4px 8px; }
  .stack-view { max-height: 40vh; }
  .stack-view-3d { height: 320px; }
  .stack-scene { width: 240px; }
  .layer-plane { width: 240px; min-height: 40px; padding: 8px 10px; }
  .io-chip { font-size: 0.5rem; padding: 1px 5px; max-width: 100px; }
  .flow-dialog { width: 240px; }
}
`;
