/**
 * <rstack-mi> — AI-powered community builder embedded in the rSpace header.
 *
 * Rich builder panel with interchangeable AI backend, model selector,
 * action confirmation, scaffold progress, and role-based UI adaptation.
 */

import { getAccessToken } from "./rstack-identity";
import { parseMiActions, summariseActions, isDestructiveAction, detailedActionSummary } from "../../lib/mi-actions";
import type { MiAction } from "../../lib/mi-actions";
import { MiActionExecutor } from "../../lib/mi-action-executor";
import { suggestTools, type ToolHint } from "../../lib/mi-tool-schema";
import { SpeechDictation } from "../../lib/speech-dictation";
import { MiVoiceBridge, type VoiceState } from "../../lib/mi-voice-bridge";
import { getContextSuggestions } from "../../lib/mi-suggestions";
import type { MiSuggestion } from "../../lib/mi-suggestions";

interface MiMessage {
	role: "user" | "assistant";
	content: string;
	actionSummary?: string;
	actionDetails?: string[];
	toolHints?: ToolHint[];
}

interface MiModelConfig {
	id: string;
	provider: string;
	providerModel: string;
	label: string;
	group: string;
}

export class RStackMi extends HTMLElement {
	#shadow: ShadowRoot;
	#messages: MiMessage[] = [];
	#abortController: AbortController | null = null;
	#preferredModel: string = "";
	#minimized = false;
	#availableModels: MiModelConfig[] = [];
	#pendingConfirm: { actions: MiAction[]; resolve: (ok: boolean) => void } | null = null;
	#scaffoldProgress: { current: number; total: number; label: string } | null = null;
	#suggestions: MiSuggestion[] = [];
	#dynamicSuggestions: MiSuggestion[] = [];
	#placeholderIdx = 0;
	#placeholderTimer: ReturnType<typeof setInterval> | null = null;
	#voiceMode = false;
	#voiceState: VoiceState = "idle";
	#voiceBridge: MiVoiceBridge | null = null;
	#voiceDictation: SpeechDictation | null = null;
	#voiceAccumulated = "";
	#voiceSilenceTimer: ReturnType<typeof setTimeout> | null = null;
	#outsideClickHandler: ((e: PointerEvent) => void) | null = null;

	constructor() {
		super();
		this.#shadow = this.attachShadow({ mode: "open" });
		this.#preferredModel = localStorage.getItem("mi-preferred-model") || "";
	}

	connectedCallback() {
		this.#render();
		this.#loadModels();
		this.#setupKeyboard();
		// Load initial suggestions for bar placeholder rotation
		setTimeout(() => this.#loadSuggestions(), 500);
	}

	disconnectedCallback() {
		document.removeEventListener("keydown", this.#keyHandler);
		if (this.#outsideClickHandler) {
			document.removeEventListener("pointerdown", this.#outsideClickHandler as EventListener);
			this.#outsideClickHandler = null;
		}
		if (this.#placeholderTimer) clearInterval(this.#placeholderTimer);
		if (this.#voiceMode) this.#deactivateVoiceMode();
	}

	#keyHandler = (e: KeyboardEvent) => {
		// Cmd/Ctrl+K opens MI panel
		if ((e.metaKey || e.ctrlKey) && e.key === "k") {
			e.preventDefault();
			const panel = this.#shadow.getElementById("mi-panel");
			const bar = this.#shadow.getElementById("mi-bar");
			const pill = this.#shadow.getElementById("mi-pill");
			if (this.#minimized) {
				this.#minimized = false;
				panel?.classList.add("open");
				panel?.classList.remove("hidden");
				bar?.classList.add("focused");
				pill?.classList.remove("visible");
			} else {
				panel?.classList.add("open");
				bar?.classList.add("focused");
			}
			this.#loadSuggestions();
			const input = this.#shadow.getElementById("mi-input") as HTMLTextAreaElement | null;
			input?.focus();
		}
	};

	#setupKeyboard() {
		document.addEventListener("keydown", this.#keyHandler);
	}

	async #loadModels() {
		try {
			const res = await fetch("/api/mi/models");
			if (res.ok) {
				const data = await res.json();
				this.#availableModels = data.models || [];
				if (!this.#preferredModel && data.default) {
					this.#preferredModel = data.default;
				}
				this.#renderModelSelector();
			}
		} catch { /* offline — use whatever's there */ }
	}

	#renderModelSelector() {
		const select = this.#shadow.getElementById("mi-model-select") as HTMLSelectElement | null;
		if (!select || !this.#availableModels.length) return;

		// Group models
		const groups = new Map<string, MiModelConfig[]>();
		for (const m of this.#availableModels) {
			const group = groups.get(m.group) || [];
			group.push(m);
			groups.set(m.group, group);
		}

		select.innerHTML = "";
		for (const [group, models] of groups) {
			const optgroup = document.createElement("optgroup");
			optgroup.label = group;
			for (const m of models) {
				const opt = document.createElement("option");
				opt.value = m.id;
				opt.textContent = m.label;
				if (m.id === this.#preferredModel) opt.selected = true;
				optgroup.appendChild(opt);
			}
			select.appendChild(optgroup);
		}

		select.addEventListener("change", () => {
			this.#preferredModel = select.value;
			localStorage.setItem("mi-preferred-model", select.value);
		});
	}

	#render() {
		this.#shadow.innerHTML = `
			<style>${STYLES}</style>
			<div class="mi">
				<div class="mi-bar" id="mi-bar">
					<span class="mi-icon">&#10023;</span>
					<input class="mi-input-bar" id="mi-bar-input" type="text"
						placeholder="Ask mi anything..." autocomplete="off" />
					${SpeechDictation.isSupported() ? '<button class="mi-mic-btn" id="mi-voice-btn" title="Voice mode">🎤</button>' : ''}
				</div>
				<div class="mi-panel" id="mi-panel">
					<div class="mi-panel-header">
						<span class="mi-panel-icon">&#10023;</span>
						<span class="mi-panel-title">mi</span>
						<select class="mi-model-select" id="mi-model-select" title="Select AI model"></select>
						<div class="mi-panel-spacer"></div>
						<button class="mi-panel-btn" id="mi-minimize" title="Minimize (Escape)">&#8722;</button>
						<button class="mi-panel-btn" id="mi-close" title="Close">&times;</button>
					</div>
					<div class="mi-messages" id="mi-messages">
						<div class="mi-welcome" id="mi-welcome">
							<span class="mi-welcome-icon">&#10023;</span>
							<p>Hi, I'm <strong>mi</strong> — your mycelial intelligence guide.</p>
							<p class="mi-welcome-sub">I can create content across all rApps, set up spaces, connect knowledge, and help you build.</p>
							<div class="mi-suggestions" id="mi-suggestions"></div>
						</div>
					</div>
					<div class="mi-confirm" id="mi-confirm" style="display:none;">
						<span class="mi-confirm-icon">⚠</span>
						<span class="mi-confirm-text" id="mi-confirm-text"></span>
						<div class="mi-confirm-btns">
							<button class="mi-confirm-allow" id="mi-confirm-allow">Allow</button>
							<button class="mi-confirm-cancel" id="mi-confirm-cancel">Cancel</button>
						</div>
					</div>
					<div class="mi-scaffold-progress" id="mi-scaffold-progress" style="display:none;">
						<div class="mi-scaffold-bar"><div class="mi-scaffold-fill" id="mi-scaffold-fill"></div></div>
						<span class="mi-scaffold-label" id="mi-scaffold-label"></span>
					</div>
					<div class="mi-voice-strip" id="mi-voice-strip" style="display:none">
						<div class="mi-voice-waveform"><span></span><span></span><span></span><span></span><span></span></div>
						<span class="mi-voice-label" id="mi-voice-label">Listening...</span>
						<span class="mi-voice-interim" id="mi-voice-interim"></span>
						<button class="mi-voice-stop" id="mi-voice-stop" title="Stop voice mode">&times;</button>
					</div>
					<div class="mi-input-area">
						<textarea class="mi-input" id="mi-input" rows="1"
							placeholder="Ask mi to build, create, or explore..." autocomplete="off"></textarea>
						<button class="mi-send-btn" id="mi-send" title="Send">&#9654;</button>
					</div>
				</div>
				<div class="mi-pill" id="mi-pill">
					<span class="mi-pill-icon">&#10023;</span> mi
				</div>
			</div>
		`;

		const barInput = this.#shadow.getElementById("mi-bar-input") as HTMLInputElement;
		const input = this.#shadow.getElementById("mi-input") as HTMLTextAreaElement;
		const panel = this.#shadow.getElementById("mi-panel")!;
		const bar = this.#shadow.getElementById("mi-bar")!;
		const pill = this.#shadow.getElementById("mi-pill")!;
		const sendBtn = this.#shadow.getElementById("mi-send")!;

		// Bar input opens the panel
		barInput.addEventListener("focus", () => {
			panel.classList.add("open");
			bar.classList.add("focused");
			this.#loadSuggestions();
			// Transfer any text to the panel input
			if (barInput.value.trim()) {
				input.value = barInput.value;
				barInput.value = "";
			}
			setTimeout(() => input.focus(), 50);
		});

		barInput.addEventListener("keydown", (e) => {
			if (e.key === "Enter" && barInput.value.trim()) {
				e.preventDefault();
				panel.classList.add("open");
				bar.classList.add("focused");
				input.value = barInput.value;
				barInput.value = "";
				setTimeout(() => {
					input.focus();
					this.#ask(input.value.trim());
					input.value = "";
					this.#autoResize(input);
				}, 50);
			}
		});

		// Panel textarea: Enter sends, Shift+Enter for newline
		input.addEventListener("keydown", (e) => {
			if (e.key === "Enter" && !e.shiftKey && input.value.trim()) {
				e.preventDefault();
				this.#ask(input.value.trim());
				input.value = "";
				this.#autoResize(input);
			}
			if (e.key === "Escape" && !input.value.trim()) {
				this.#minimize();
			}
		});

		input.addEventListener("input", () => this.#autoResize(input));

		sendBtn.addEventListener("click", () => {
			if (input.value.trim()) {
				this.#ask(input.value.trim());
				input.value = "";
				this.#autoResize(input);
			}
		});

		// Close panel on outside click — use composedPath to pierce Shadow DOM
		if (this.#outsideClickHandler) {
			document.removeEventListener("pointerdown", this.#outsideClickHandler as EventListener);
		}
		this.#outsideClickHandler = (e: PointerEvent) => {
			if (this.#voiceMode) return; // Keep panel open during voice conversation
			const path = e.composedPath();
			if (!path.includes(this)) {
				panel.classList.remove("open");
				bar.classList.remove("focused");
			}
		};
		document.addEventListener("pointerdown", this.#outsideClickHandler as EventListener);

		// Prevent internal clicks from closing
		panel.addEventListener("pointerdown", (e) => e.stopPropagation());
		bar.addEventListener("pointerdown", (e) => e.stopPropagation());

		// Minimize button
		this.#shadow.getElementById("mi-minimize")!.addEventListener("click", () => this.#minimize());

		// Close button
		this.#shadow.getElementById("mi-close")!.addEventListener("click", () => {
			panel.classList.remove("open");
			bar.classList.remove("focused");
		});

		// Pill click restores
		pill.addEventListener("click", (e) => {
			e.stopPropagation();
			this.#minimized = false;
			pill.classList.remove("visible");
			panel.classList.add("open");
			panel.classList.remove("hidden");
			bar.classList.add("focused");
			this.#loadSuggestions();
			input.focus();
		});

		// Confirmation buttons
		this.#shadow.getElementById("mi-confirm-allow")!.addEventListener("click", () => {
			if (this.#pendingConfirm) {
				this.#pendingConfirm.resolve(true);
				this.#pendingConfirm = null;
				this.#shadow.getElementById("mi-confirm")!.style.display = "none";
			}
		});
		this.#shadow.getElementById("mi-confirm-cancel")!.addEventListener("click", () => {
			if (this.#pendingConfirm) {
				this.#pendingConfirm.resolve(false);
				this.#pendingConfirm = null;
				this.#shadow.getElementById("mi-confirm")!.style.display = "none";
			}
		});

		// Voice mode — single mic toggle (STT input → auto-submit → TTS response)
		const voiceBtn = this.#shadow.getElementById("mi-voice-btn");
		const voiceStopBtn = this.#shadow.getElementById("mi-voice-stop");

		voiceBtn?.addEventListener("click", (e) => {
			e.stopPropagation();
			this.#toggleVoiceMode();
		});
		voiceStopBtn?.addEventListener("click", (e) => {
			e.stopPropagation();
			this.#deactivateVoiceMode();
		});
	}

	#loadSuggestions() {
		const ctx = this.#gatherContext();
		this.#suggestions = getContextSuggestions({
			module: ctx.module || "",
			hasShapes: !!(ctx.openShapes?.length),
			hasSelectedShapes: !!(ctx.selectedShapes?.length),
		});
		this.#renderSuggestions();
		this.#rotatePlaceholder();

		// Fetch dynamic suggestions (non-blocking)
		this.#fetchDynamicSuggestions(ctx.space, ctx.module);
	}

	async #fetchDynamicSuggestions(space: string, module: string) {
		if (!space) return;
		try {
			const token = getAccessToken();
			const res = await fetch("/api/mi/suggestions", {
				method: "POST",
				headers: {
					"Content-Type": "application/json",
					...(token ? { Authorization: `Bearer ${token}` } : {}),
				},
				body: JSON.stringify({ space, module }),
			});
			if (res.ok) {
				const data = await res.json();
				this.#dynamicSuggestions = data.suggestions || [];
				this.#renderSuggestions();
			}
		} catch { /* offline — skip dynamic suggestions */ }
	}

	#renderSuggestions() {
		const container = this.#shadow.getElementById("mi-suggestions");
		if (!container) return;

		// Don't show suggestions once a conversation has started
		if (this.#messages.length > 0) {
			container.innerHTML = "";
			return;
		}

		const allSuggestions = [...this.#suggestions];
		const hasDynamic = this.#dynamicSuggestions.length > 0;

		let html = allSuggestions
			.map((s) => `<button class="mi-suggest-chip" data-prompt="${this.#escapeAttr(s.prompt)}" data-autosend="${s.autoSend !== false}">${s.icon} ${this.#escapeHtml(s.label)}</button>`)
			.join("");

		if (hasDynamic) {
			html += `<div class="mi-suggest-section">For you</div>`;
			html += this.#dynamicSuggestions
				.map((s) => `<button class="mi-suggest-chip dynamic" data-prompt="${this.#escapeAttr(s.prompt)}" data-autosend="${s.autoSend !== false}">${s.icon} ${this.#escapeHtml(s.label)}</button>`)
				.join("");
		}

		container.innerHTML = html;

		// Wire chip clicks
		container.querySelectorAll<HTMLButtonElement>(".mi-suggest-chip").forEach((btn) => {
			btn.addEventListener("click", (e) => {
				e.stopPropagation();
				const prompt = btn.dataset.prompt || "";
				const autoSend = btn.dataset.autosend === "true";
				if (autoSend) {
					this.#ask(prompt);
				} else {
					const input = this.#shadow.getElementById("mi-input") as HTMLTextAreaElement | null;
					if (input) {
						input.value = prompt;
						input.focus();
						this.#autoResize(input);
					}
				}
			});
		});
	}

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

	#rotatePlaceholder() {
		const barInput = this.#shadow.getElementById("mi-bar-input") as HTMLInputElement | null;
		if (!barInput) return;

		const allSuggestions = [...this.#suggestions, ...this.#dynamicSuggestions];
		if (allSuggestions.length === 0) return;

		// Set initial placeholder from a random suggestion
		this.#placeholderIdx = Math.floor(Math.random() * allSuggestions.length);
		const hint = allSuggestions[this.#placeholderIdx];
		barInput.placeholder = `Ask mi anything... try "${hint.label}"`;

		// Rotate every 8 seconds
		if (this.#placeholderTimer) clearInterval(this.#placeholderTimer);
		this.#placeholderTimer = setInterval(() => {
			const current = [...this.#suggestions, ...this.#dynamicSuggestions];
			if (current.length === 0) return;
			this.#placeholderIdx = (this.#placeholderIdx + 1) % current.length;
			const s = current[this.#placeholderIdx];
			barInput.placeholder = `Ask mi anything... try "${s.label}"`;
		}, 8000);
	}

	// ── Voice conversation mode ──

	#toggleVoiceMode() {
		if (this.#voiceMode) {
			this.#deactivateVoiceMode();
		} else {
			this.#activateVoiceMode();
		}
	}

	#activateVoiceMode() {
		this.#voiceMode = true;
		this.#voiceAccumulated = "";

		// Create TTS bridge
		this.#voiceBridge = new MiVoiceBridge({
			onStateChange: (s) => this.#voiceSetState(s),
		});

		// Create dedicated voice dictation instance
		this.#voiceDictation = new SpeechDictation({
			onInterim: (text) => {
				const interimEl = this.#shadow.getElementById("mi-voice-interim");
				if (interimEl) interimEl.textContent = text;
				// Interrupt TTS if user starts speaking
				if (this.#voiceState === "speaking") {
					this.#voiceBridge?.stop();
					this.#voiceSetState("listening");
				}
			},
			onFinal: (text) => {
				this.#voiceAccumulated += (this.#voiceAccumulated ? " " : "") + text;
				const interimEl = this.#shadow.getElementById("mi-voice-interim");
				if (interimEl) interimEl.textContent = this.#voiceAccumulated;
				// Reset silence timer
				this.#resetSilenceTimer();
			},
			onStateChange: () => {},
			onError: (err) => console.warn("Voice dictation:", err),
		});

		// Open panel, show strip, start listening
		const panel = this.#shadow.getElementById("mi-panel")!;
		const bar = this.#shadow.getElementById("mi-bar")!;
		panel.classList.add("open");
		bar.classList.add("focused");
		this.#shadow.getElementById("mi-voice-strip")!.style.display = "flex";

		this.#voiceSetState("listening");
		this.#voiceDictation.start();
	}

	#deactivateVoiceMode() {
		this.#voiceMode = false;

		// Clear silence timer
		if (this.#voiceSilenceTimer) {
			clearTimeout(this.#voiceSilenceTimer);
			this.#voiceSilenceTimer = null;
		}

		// Stop dictation and TTS
		this.#voiceDictation?.destroy();
		this.#voiceDictation = null;
		this.#voiceBridge?.stop();
		this.#voiceBridge?.destroy();
		this.#voiceBridge = null;
		this.#voiceAccumulated = "";

		// Hide strip, reset UI
		this.#shadow.getElementById("mi-voice-strip")!.style.display = "none";
		this.#voiceSetState("idle");
	}

	#voiceSetState(state: VoiceState) {
		this.#voiceState = state;

		const strip = this.#shadow.getElementById("mi-voice-strip");
		const label = this.#shadow.getElementById("mi-voice-label");
		const btn = this.#shadow.getElementById("mi-voice-btn");

		// Update strip
		if (strip) {
			strip.classList.remove("vs-listening", "vs-thinking", "vs-speaking");
			if (state !== "idle") strip.classList.add(`vs-${state}`);
		}
		if (label) {
			const labels: Record<VoiceState, string> = {
				idle: "",
				listening: "Listening...",
				thinking: "Thinking...",
				speaking: "Speaking...",
			};
			label.textContent = labels[state];
		}

		// Update button
		if (btn) {
			btn.classList.remove("v-listening", "v-thinking", "v-speaking", "v-active");
			if (this.#voiceMode) {
				btn.classList.add("v-active");
				if (state !== "idle") btn.classList.add(`v-${state}`);
			}
		}
	}

	#resetSilenceTimer() {
		if (this.#voiceSilenceTimer) clearTimeout(this.#voiceSilenceTimer);
		this.#voiceSilenceTimer = setTimeout(() => {
			if (this.#voiceMode && this.#voiceAccumulated.trim()) {
				this.#voiceSubmit();
			}
		}, 1500);
	}

	async #voiceSubmit() {
		const query = this.#voiceAccumulated.trim();
		if (!query || !this.#voiceMode) return;

		this.#voiceAccumulated = "";
		const interimEl = this.#shadow.getElementById("mi-voice-interim");
		if (interimEl) interimEl.textContent = "";

		// Stop listening while processing (echo prevention)
		this.#voiceDictation?.stop();
		this.#voiceSetState("thinking");

		// Submit query through existing #ask flow
		await this.#ask(query);

		if (!this.#voiceMode) return; // Deactivated during ask

		// Get the last assistant message for TTS
		const lastMsg = [...this.#messages].reverse().find((m) => m.role === "assistant");
		if (lastMsg?.content && this.#voiceBridge) {
			this.#voiceSetState("speaking");
			const ttsText = this.#stripForTTS(lastMsg.content);
			await this.#voiceBridge.speak(ttsText);
		}

		// Resume listening
		if (this.#voiceMode) {
			this.#voiceSetState("listening");
			this.#voiceDictation?.start();
		}
	}

	#stripForTTS(text: string): string {
		let stripped = text
			// Remove code blocks
			.replace(/```[\s\S]*?```/g, "... code block omitted ...")
			// Remove inline code
			.replace(/`[^`]+`/g, "")
			// Remove markdown formatting
			.replace(/[*_#]+/g, "")
			// Remove links, keep label
			.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
			// Remove action indicators
			.replace(/\n\*[⏳✓✗].*?\*\n?/g, " ")
			// Remove turn indicators
			.replace(/\n\*— thinking.*?—\*\n?/g, " ")
			// Collapse whitespace
			.replace(/\s+/g, " ")
			.trim();

		// Truncate to ~2 sentences for succinct TTS
		const sentences = stripped.match(/[^.!?]+[.!?]+/g) || [stripped];
		if (sentences.length > 2) {
			stripped = sentences.slice(0, 2).join(" ").trim();
		}
		return stripped;
	}

	#minimize() {
		this.#minimized = true;
		const panel = this.#shadow.getElementById("mi-panel")!;
		const pill = this.#shadow.getElementById("mi-pill")!;
		panel.classList.remove("open");
		panel.classList.add("hidden");
		this.#shadow.getElementById("mi-bar")!.classList.remove("focused");
		pill.classList.add("visible");
	}

	#autoResize(textarea: HTMLTextAreaElement) {
		textarea.style.height = "auto";
		textarea.style.height = Math.min(textarea.scrollHeight, 120) + "px";
	}

	/** Gather page context: open shapes, active module, tabs, canvas state. */
	#gatherContext(): Record<string, any> {
		const ctx: Record<string, any> = {};

		ctx.space = document.querySelector("rstack-space-switcher")?.getAttribute("current") || "";
		ctx.module = document.querySelector("rstack-app-switcher")?.getAttribute("current") || "";

		// Deep canvas context from MI bridge
		const bridge = (window as any).__miCanvasBridge;
		if (bridge) {
			const cc = bridge.getCanvasContext();
			ctx.openShapes = cc.allShapes.slice(0, 20).map((s: any) => ({
				type: s.type,
				id: s.id,
				x: Math.round(s.x),
				y: Math.round(s.y),
				width: Math.round(s.width),
				height: Math.round(s.height),
				...(s.content ? { snippet: s.content.slice(0, 80) } : {}),
				...(s.title ? { title: s.title } : {}),
			}));
			if (cc.selectedShapes.length) {
				ctx.selectedShapes = cc.selectedShapes.map((s: any) => ({
					type: s.type,
					id: s.id,
					x: Math.round(s.x),
					y: Math.round(s.y),
					...(s.content ? { snippet: s.content.slice(0, 80) } : {}),
					...(s.title ? { title: s.title } : {}),
				}));
			}
			if (cc.connections.length) ctx.connections = cc.connections;
			ctx.viewport = cc.viewport;
			if (cc.shapeGroups.length) ctx.shapeGroups = cc.shapeGroups;
			ctx.shapeCountByType = cc.shapeCountByType;
		} else {
			const canvasContent = document.getElementById("canvas-content");
			if (canvasContent) {
				const shapes = [...canvasContent.children]
					.filter((el) => el.tagName?.includes("-") && el.id)
					.map((el: any) => ({
						type: el.tagName.toLowerCase(),
						id: el.id,
						...(el.content ? { snippet: el.content.slice(0, 60) } : {}),
						...(el.title ? { title: el.title } : {}),
					}))
					.slice(0, 20);
				if (shapes.length) ctx.openShapes = shapes;
			}
		}

		const tabBar = document.querySelector("rstack-tab-bar");
		if (tabBar) {
			ctx.activeTab = tabBar.getAttribute("active") || "";
		}
		ctx.pageTitle = document.title;

		return ctx;
	}

	async #ask(query: string) {
		const panel = this.#shadow.getElementById("mi-panel")!;
		const messagesEl = this.#shadow.getElementById("mi-messages")!;

		panel.classList.add("open");
		this.#shadow.getElementById("mi-bar")!.classList.add("focused");

		// Add user message (cap at 50 to prevent unbounded growth)
		this.#messages.push({ role: "user", content: query });
		if (this.#messages.length > 50) this.#messages = this.#messages.slice(-50);
		this.#renderMessages(messagesEl);

		// Add placeholder for assistant
		this.#messages.push({ role: "assistant", content: "" });
		const assistantIdx = this.#messages.length - 1;
		this.#renderMessages(messagesEl);

		// Abort previous
		this.#abortController?.abort();
		this.#abortController = new AbortController();

		try {
			const token = getAccessToken();
			const context = this.#gatherContext();
			const res = await fetch("/api/mi/ask", {
				method: "POST",
				headers: {
					"Content-Type": "application/json",
					...(token ? { Authorization: `Bearer ${token}` } : {}),
				},
				body: JSON.stringify({
					query,
					messages: this.#messages.slice(0, -1).slice(-10),
					space: context.space,
					module: context.module,
					context,
					model: this.#preferredModel || undefined,
					voiceMode: this.#voiceMode,
				}),
				signal: this.#abortController.signal,
			});

			if (!res.ok) {
				const err = await res.json().catch(() => ({ error: "Request failed" }));
				throw new Error(err.error || "Request failed");
			}

			if (!res.body) throw new Error("No response stream");

			const reader = res.body.getReader();
			const decoder = new TextDecoder();

			const serverActionResults: { type: string; url?: string; ref?: string }[] = [];

			while (true) {
				const { done, value } = await reader.read();
				if (done) break;

				const chunk = decoder.decode(value, { stream: true });
				for (const line of chunk.split("\n").filter(Boolean)) {
					try {
						const data = JSON.parse(line);

						// Standard message content (backwards-compatible)
						if (data.message?.content) {
							this.#messages[assistantIdx].content += data.message.content;
						}
						if (data.response) {
							this.#messages[assistantIdx].content = data.response;
						}

						// Agentic loop: turn indicator
						if (data.type === "turn" && data.turn > 0) {
							this.#messages[assistantIdx].content += `\n\n*— thinking (turn ${data.turn + 1}/${data.maxTurns}) —*\n\n`;
						}

						// Agentic loop: server action starting
						if (data.type === "action-start") {
							this.#messages[assistantIdx].content += `\n*⏳ ${data.action?.type}…*\n`;
						}

						// Agentic loop: server action result
						if (data.type === "action-result") {
							if (data.ok && data.url) {
								serverActionResults.push({ type: data.actionType, url: data.url, ref: data.ref });
								this.#messages[assistantIdx].content += `\n*✓ ${data.actionType}: generated*\n`;
							} else if (data.ok && data.summary) {
								this.#messages[assistantIdx].content += `\n*✓ ${data.actionType}: ${data.summary.slice(0, 80)}*\n`;
							} else if (!data.ok) {
								this.#messages[assistantIdx].content += `\n*✗ ${data.actionType}: ${data.error || "failed"}*\n`;
							}
						}
					} catch { /* skip malformed lines */ }
				}
				this.#renderMessages(messagesEl);
			}

			if (!this.#messages[assistantIdx].content) {
				this.#messages[assistantIdx].content = "I couldn't generate a response. Please try again.";
				this.#renderMessages(messagesEl);
			} else {
				// Parse and execute MI actions (client-side canvas actions)
				const rawText = this.#messages[assistantIdx].content;
				const { displayText, actions } = parseMiActions(rawText);
				this.#messages[assistantIdx].content = displayText;

				if (actions.length) {
					await this.#executeWithConfirmation(actions, context);
					this.#messages[assistantIdx].actionSummary = summariseActions(actions);
					this.#messages[assistantIdx].actionDetails = detailedActionSummary(actions);
				}

				// Create canvas shapes for server-generated media
				if (serverActionResults.length) {
					const mediaActions: MiAction[] = [];
					for (const r of serverActionResults) {
						if (r.type === "generate-image" && r.url) {
							mediaActions.push({ type: "create-shape", tagName: "folk-image-gen", props: { src: r.url }, ref: r.ref });
						} else if (r.type === "generate-video" && r.url) {
							mediaActions.push({ type: "create-shape", tagName: "folk-video-gen", props: { src: r.url }, ref: r.ref });
						}
					}
					if (mediaActions.length) {
						const executor = new MiActionExecutor();
						executor.setContext(context.space || "", getAccessToken() || "");
						executor.execute(mediaActions);
					}
				}

				// Check for tool suggestions
				const hints = suggestTools(query);
				if (hints.length) {
					this.#messages[assistantIdx].toolHints = hints;
				}

				this.#renderMessages(messagesEl);
			}
		} catch (e: any) {
			if (e.name !== "AbortError") {
				this.#messages[this.#messages.length - 1].content =
					"Sorry, I'm not available right now. Please try again later.";
				this.#renderMessages(messagesEl);
			}
		}
	}

	async #executeWithConfirmation(actions: MiAction[], context: Record<string, any>) {
		const needsConfirm = actions.some(isDestructiveAction);

		if (needsConfirm) {
			const descriptions = actions
				.filter(isDestructiveAction)
				.map((a) => {
					switch (a.type) {
						case "delete-shape": return `Delete shape ${a.shapeId}`;
						case "delete-content": return `Delete ${a.contentType} from ${a.module}`;
						case "disable-module": return `Disable ${a.moduleId}`;
						case "batch": return `Batch with ${a.actions.length} actions`;
						default: return a.type;
					}
				});

			const confirmEl = this.#shadow.getElementById("mi-confirm")!;
			const textEl = this.#shadow.getElementById("mi-confirm-text")!;
			textEl.textContent = `MI wants to: ${descriptions.join(", ")}`;
			confirmEl.style.display = "flex";

			const allowed = await new Promise<boolean>((resolve) => {
				this.#pendingConfirm = { actions, resolve };
			});

			if (!allowed) return;
		}

		// Validate permissions
		const token = getAccessToken();
		try {
			const valRes = await fetch("/api/mi/validate-actions", {
				method: "POST",
				headers: {
					"Content-Type": "application/json",
					...(token ? { Authorization: `Bearer ${token}` } : {}),
				},
				body: JSON.stringify({ actions, space: context.space }),
			});

			if (valRes.ok) {
				const { validated } = await valRes.json();
				const blocked = validated.filter((v: any) => !v.allowed);
				if (blocked.length) {
					// Show blocked actions in the message
					const blockedMsg = blocked
						.map((v: any) => `🔒 "${v.action.type}" requires ${v.requiredRole} role`)
						.join("\n");
					this.#messages.push({ role: "assistant", content: blockedMsg });
					this.#renderMessages(this.#shadow.getElementById("mi-messages")!);

					// Only execute allowed actions
					const allowedActions = validated
						.filter((v: any) => v.allowed)
						.map((v: any) => v.action);
					if (!allowedActions.length) return;
					return this.#runActions(allowedActions, context);
				}
			}
		} catch { /* if validation fails, proceed anyway — module APIs have their own guards */ }

		await this.#runActions(actions, context);
	}

	async #runActions(actions: MiAction[], context: Record<string, any>) {
		const executor = new MiActionExecutor();
		const token = getAccessToken() || "";
		executor.setContext(context.space || "", token);

		// Check if any actions need async execution
		const hasModuleActions = actions.some((a) =>
			["create-content", "update-content", "delete-content", "scaffold", "batch"].includes(a.type),
		);

		const progressEl = this.#shadow.getElementById("mi-scaffold-progress")!;
		const fillEl = this.#shadow.getElementById("mi-scaffold-fill")!;
		const labelEl = this.#shadow.getElementById("mi-scaffold-label")!;

		const onProgress = (current: number, total: number, label: string) => {
			this.#scaffoldProgress = { current, total, label };
			progressEl.style.display = "flex";
			fillEl.style.width = `${(current / total) * 100}%`;
			labelEl.textContent = `${label} (${current}/${total})`;
		};

		if (hasModuleActions) {
			await executor.executeAsync(actions, onProgress);
		} else {
			executor.execute(actions, onProgress);
		}

		// Hide progress
		progressEl.style.display = "none";
		this.#scaffoldProgress = null;
	}

	#renderMessages(container: HTMLElement) {
		container.innerHTML = this.#messages
			.map(
				(m) => `
			<div class="mi-msg mi-msg--${m.role}">
				<span class="mi-msg-who">${m.role === "user" ? "You" : "&#10023; mi"}</span>
				<div class="mi-msg-body">${m.content ? this.#formatContent(m.content) : '<span class="mi-typing"><span></span><span></span><span></span></span>'}</div>
				${m.actionSummary ? `<details class="mi-action-details"><summary class="mi-action-chip">${this.#escapeHtml(m.actionSummary)}</summary><div class="mi-action-list">${(m.actionDetails || []).map((d) => `<div class="mi-action-item">→ ${this.#escapeHtml(d)}</div>`).join("")}</div></details>` : ""}
				${m.toolHints?.length ? `<div class="mi-tool-chips">${m.toolHints.map((h) => `<button class="mi-tool-chip" data-tag="${h.tagName}">${h.icon} ${this.#escapeHtml(h.label)}</button>`).join("")}</div>` : ""}
			</div>
		`,
			)
			.join("");

		// Wire tool chip clicks
		container.querySelectorAll<HTMLButtonElement>(".mi-tool-chip").forEach((btn) => {
			btn.addEventListener("click", () => {
				const tag = btn.dataset.tag;
				if (!tag) return;
				const executor = new MiActionExecutor();
				executor.execute([{ type: "create-shape", tagName: tag, props: {} }]);
			});
		});

		container.scrollTop = container.scrollHeight;
	}

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

	#formatContent(s: string): string {
		let escaped = s
			.replace(/&/g, "&amp;")
			.replace(/</g, "&lt;")
			.replace(/>/g, "&gt;");

		// Code blocks (```)
		escaped = escaped.replace(/```(\w*)\n([\s\S]*?)```/g,
			'<pre class="mi-codeblock"><code>$2</code></pre>');

		// Headers
		escaped = escaped.replace(/^### (.+)$/gm, '<strong class="mi-h3">$1</strong>');
		escaped = escaped.replace(/^## (.+)$/gm, '<strong class="mi-h2">$1</strong>');
		escaped = escaped.replace(/^# (.+)$/gm, '<strong class="mi-h1">$1</strong>');

		// Bold and inline code
		escaped = escaped.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
		escaped = escaped.replace(/`(.+?)`/g, '<code class="mi-code">$1</code>');

		// Links
		escaped = escaped.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" class="mi-link">$1</a>');

		// Lists (- item)
		escaped = escaped.replace(/^- (.+)$/gm, '<span class="mi-list-item">• $1</span>');

		// Newlines
		escaped = escaped.replace(/\n/g, "<br>");

		return escaped;
	}

	static define(tag = "rstack-mi") {
		if (!customElements.get(tag)) customElements.define(tag, RStackMi);
	}
}

const STYLES = `
:host { display: contents; }

.mi { position: relative; flex: 1; max-width: 480px; min-width: 0; z-index: 1; }

/* ── Search bar in header ── */
.mi-bar {
  display: flex; align-items: center; gap: 8px;
  padding: 6px 14px; border-radius: 10px;
  transition: all 0.2s;
  background: var(--rs-btn-secondary-bg);
}
.mi-bar.focused { background: var(--rs-bg-hover); box-shadow: 0 0 0 1px rgba(6,182,212,0.3); }

.mi-icon {
  font-size: 0.9rem; flex-shrink: 0;
  background: linear-gradient(135deg, #06b6d4, #7c3aed);
  -webkit-background-clip: text; -webkit-text-fill-color: transparent;
}

.mi-input-bar {
  flex: 1; border: none; outline: none; background: none;
  font-size: 0.85rem; min-width: 0;
  font-family: inherit;
  color: var(--rs-text-primary);
}
.mi-input-bar::placeholder { color: var(--rs-text-muted); }

.mi-mic-btn {
  background: none; border: none; cursor: pointer; padding: 2px 6px;
  font-size: 0.9rem; border-radius: 6px; transition: all 0.2s;
  flex-shrink: 0; line-height: 1; opacity: 0.7;
}
.mi-mic-btn:hover, .mi-mic-btn:active { background: var(--rs-bg-hover); opacity: 1; }
.mi-mic-btn.v-active { opacity: 1; }
.mi-mic-btn.v-listening {
  animation: voicePulseRed 1.5s infinite;
}
.mi-mic-btn.v-thinking {
  animation: voiceSpinAmber 2s linear infinite;
}
.mi-mic-btn.v-speaking {
  animation: voicePulseCyan 2s infinite;
}

/* ── Rich panel ── */
.mi-panel {
  position: absolute; top: calc(100% + 8px); left: 50%;
  transform: translateX(-50%);
  width: 520px; max-width: calc(100vw - 32px);
  height: 65vh; max-height: calc(100vh - 72px); min-height: 300px;
  border-radius: 14px; overflow: hidden;
  box-shadow: 0 12px 40px rgba(0,0,0,0.3);
  display: none;
  background: var(--rs-bg-surface); border: 1px solid var(--rs-border);
  resize: vertical;
}
.mi-panel.open { display: flex; flex-direction: column; z-index: 10001; }
.mi-panel.hidden { display: none; }

/* ── Panel header ── */
.mi-panel-header {
  display: flex; align-items: center; gap: 8px;
  padding: 10px 14px; border-bottom: 1px solid var(--rs-border);
  flex-shrink: 0;
}
.mi-panel-icon {
  font-size: 1rem;
  background: linear-gradient(135deg, #06b6d4, #7c3aed);
  -webkit-background-clip: text; -webkit-text-fill-color: transparent;
}
.mi-panel-title {
  font-weight: 700; font-size: 0.85rem;
  color: var(--rs-text-primary);
}
.mi-model-select {
  font-size: 0.75rem; padding: 3px 6px; border-radius: 6px;
  border: 1px solid var(--rs-border); background: var(--rs-btn-secondary-bg);
  color: var(--rs-text-primary); font-family: inherit; cursor: pointer;
  max-width: 150px;
}
.mi-panel-spacer { flex: 1; }
.mi-panel-btn {
  background: none; border: none; cursor: pointer;
  font-size: 1.1rem; line-height: 1; padding: 2px 6px; border-radius: 4px;
  color: var(--rs-text-muted); transition: all 0.15s;
}
.mi-panel-btn:hover, .mi-panel-btn:active { background: var(--rs-bg-hover); color: var(--rs-text-primary); }

/* ── Messages ── */
.mi-messages {
  flex: 1; overflow-y: auto; padding: 16px;
  display: flex; flex-direction: column; gap: 12px;
}

.mi-welcome { text-align: center; padding: 24px 16px; }
.mi-welcome-icon {
  font-size: 2rem; display: block; margin-bottom: 8px;
  background: linear-gradient(135deg, #06b6d4, #7c3aed);
  -webkit-background-clip: text; -webkit-text-fill-color: transparent;
}
.mi-welcome p { font-size: 0.9rem; line-height: 1.5; margin: 0; color: var(--rs-text-primary); }
.mi-welcome-sub { font-size: 0.8rem; opacity: 0.6; margin-top: 6px !important; }

/* ── Suggestion chips ── */
.mi-suggestions {
  display: flex; flex-wrap: wrap; gap: 8px; padding: 12px 0 4px;
  justify-content: center;
}
.mi-suggest-chip {
  padding: 6px 12px; border-radius: 16px; border: 1px solid var(--rs-border);
  font-size: 0.78rem; cursor: pointer; transition: all 0.15s;
  font-family: inherit; white-space: nowrap;
  background: var(--rs-btn-secondary-bg); color: var(--rs-text-primary);
}
.mi-suggest-chip:hover, .mi-suggest-chip:active {
  background: var(--rs-bg-hover);
  border-color: rgba(6,182,212,0.4);
  box-shadow: 0 0 0 1px rgba(6,182,212,0.15);
}
.mi-suggest-chip.dynamic {
  border-color: rgba(124,58,237,0.3);
}
.mi-suggest-chip.dynamic:hover, .mi-suggest-chip.dynamic:active {
  border-color: rgba(124,58,237,0.5);
  box-shadow: 0 0 0 1px rgba(124,58,237,0.15);
}
.mi-suggest-section {
  width: 100%; font-size: 0.65rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--rs-text-muted); margin-top: 4px;
}

.mi-msg { display: flex; flex-direction: column; gap: 4px; }
.mi-msg-who { font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; }
.mi-msg--user .mi-msg-who { color: #06b6d4; }
.mi-msg--assistant .mi-msg-who {
  background: linear-gradient(135deg, #06b6d4, #7c3aed);
  -webkit-background-clip: text; -webkit-text-fill-color: transparent;
}

.mi-msg-body {
  font-size: 0.85rem; line-height: 1.6; word-break: break-word;
  color: var(--rs-text-secondary);
}
.mi-msg--user .mi-msg-body { color: var(--rs-text-primary); }

/* ── Markdown rendering ── */
.mi-code {
  padding: 1px 5px; border-radius: 4px; font-size: 0.8rem;
  font-family: 'SF Mono', Monaco, Consolas, monospace;
  background: var(--rs-btn-secondary-bg); color: #0ea5e9;
}
.mi-codeblock {
  background: var(--rs-btn-secondary-bg); border-radius: 8px;
  padding: 10px 12px; margin: 6px 0; overflow-x: auto;
  font-size: 0.78rem; line-height: 1.5;
  font-family: 'SF Mono', Monaco, Consolas, monospace;
}
.mi-codeblock code { color: var(--rs-text-primary); }
.mi-h1 { font-size: 1.1rem; display: block; margin: 8px 0 4px; color: var(--rs-text-primary); }
.mi-h2 { font-size: 1rem; display: block; margin: 6px 0 3px; color: var(--rs-text-primary); }
.mi-h3 { font-size: 0.9rem; display: block; margin: 4px 0 2px; color: var(--rs-text-primary); }
.mi-link { color: #06b6d4; text-decoration: underline; }
.mi-list-item { display: block; padding-left: 4px; }

/* ── Typing indicator ── */
.mi-typing { display: inline-flex; gap: 4px; padding: 4px 0; }
.mi-typing span {
  width: 6px; height: 6px; border-radius: 50%;
  animation: miBounce 1.2s ease-in-out infinite;
  background: var(--rs-text-muted);
}
.mi-typing span:nth-child(2) { animation-delay: 0.15s; }
.mi-typing span:nth-child(3) { animation-delay: 0.3s; }
@keyframes miBounce {
  0%, 60%, 100% { transform: translateY(0); }
  30% { transform: translateY(-4px); }
}

/* ── Action chips (collapsible) ── */
.mi-action-details { margin-top: 6px; }
.mi-action-chip {
  display: inline-block; padding: 3px 10px;
  border-radius: 12px; font-size: 0.75rem; font-weight: 600;
  background: rgba(6,182,212,0.12); color: #06b6d4;
  cursor: pointer; list-style: none;
}
.mi-action-chip::marker { display: none; content: ''; }
.mi-action-chip::-webkit-details-marker { display: none; }
.mi-action-list { padding: 6px 0 0 6px; }
.mi-action-item {
  font-size: 0.75rem; color: var(--rs-text-muted); padding: 1px 0;
}

.mi-tool-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.mi-tool-chip {
  padding: 4px 10px; border-radius: 8px; border: none;
  font-size: 0.75rem; cursor: pointer; transition: background 0.15s;
  font-family: inherit;
  background: var(--rs-btn-secondary-bg); color: var(--rs-text-primary);
}
.mi-tool-chip:hover, .mi-tool-chip:active { background: var(--rs-bg-hover); }

/* ── Confirmation bar ── */
.mi-confirm {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 14px; background: rgba(234,179,8,0.1);
  border-top: 1px solid rgba(234,179,8,0.3); flex-shrink: 0;
}
.mi-confirm-icon { font-size: 1rem; }
.mi-confirm-text { flex: 1; font-size: 0.8rem; color: var(--rs-text-primary); }
.mi-confirm-btns { display: flex; gap: 6px; }
.mi-confirm-allow {
  padding: 4px 12px; border-radius: 6px; border: none; cursor: pointer;
  font-size: 0.75rem; font-weight: 600; font-family: inherit;
  background: #06b6d4; color: white;
}
.mi-confirm-cancel {
  padding: 4px 12px; border-radius: 6px; border: none; cursor: pointer;
  font-size: 0.75rem; font-weight: 600; font-family: inherit;
  background: var(--rs-btn-secondary-bg); color: var(--rs-text-primary);
}

/* ── Scaffold progress ── */
.mi-scaffold-progress {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 14px; border-top: 1px solid var(--rs-border); flex-shrink: 0;
}
.mi-scaffold-bar {
  flex: 1; height: 4px; border-radius: 2px;
  background: var(--rs-btn-secondary-bg); overflow: hidden;
}
.mi-scaffold-fill {
  height: 100%; border-radius: 2px; transition: width 0.3s;
  background: linear-gradient(90deg, #06b6d4, #7c3aed);
}
.mi-scaffold-label {
  font-size: 0.75rem; color: var(--rs-text-muted); white-space: nowrap;
}

/* ── Input area ── */
.mi-input-area {
  display: flex; align-items: flex-end; gap: 8px;
  padding: 10px 14px; border-top: 1px solid var(--rs-border);
  flex-shrink: 0;
}
.mi-input {
  flex: 1; border: none; outline: none; background: none;
  font-size: 0.85rem; min-width: 0; resize: none;
  font-family: inherit;
  color: var(--rs-text-primary);
  max-height: 120px; line-height: 1.4;
}
.mi-input::placeholder { color: var(--rs-text-muted); }
.mi-send-btn {
  background: none; border: none; cursor: pointer;
  font-size: 0.9rem; padding: 4px 8px; border-radius: 6px;
  color: #06b6d4; transition: all 0.15s; flex-shrink: 0;
}
.mi-send-btn:hover, .mi-send-btn:active { background: rgba(6,182,212,0.12); }

/* ── Minimized pill ── */
.mi-pill {
  position: fixed; bottom: 16px; right: 16px;
  padding: 6px 14px; border-radius: 20px;
  font-size: 0.8rem; font-weight: 600; cursor: pointer;
  background: var(--rs-bg-surface); border: 1px solid var(--rs-border);
  box-shadow: 0 4px 16px rgba(0,0,0,0.2); z-index: 300;
  display: none; align-items: center; gap: 6px;
  color: var(--rs-text-primary); transition: all 0.2s;
}
.mi-pill:hover, .mi-pill:active { box-shadow: 0 4px 20px rgba(6,182,212,0.3); }
.mi-pill.visible { display: flex; }
.mi-pill-icon {
  background: linear-gradient(135deg, #06b6d4, #7c3aed);
  -webkit-background-clip: text; -webkit-text-fill-color: transparent;
}

@keyframes voicePulseRed {
  0%, 100% { filter: drop-shadow(0 0 2px transparent); }
  50% { filter: drop-shadow(0 0 6px rgba(239,68,68,0.7)); }
}
@keyframes voiceSpinAmber {
  0% { filter: hue-rotate(0deg) drop-shadow(0 0 4px rgba(234,179,8,0.5)); }
  100% { filter: hue-rotate(360deg) drop-shadow(0 0 4px rgba(234,179,8,0.5)); }
}
@keyframes voicePulseCyan {
  0%, 100% { filter: drop-shadow(0 0 2px transparent); }
  50% { filter: drop-shadow(0 0 6px rgba(6,182,212,0.7)); }
}

/* ── Voice strip ── */
.mi-voice-strip {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 14px; border-top: 1px solid var(--rs-border);
  flex-shrink: 0; min-height: 36px;
  background: rgba(239,68,68,0.06);
  transition: background 0.3s;
}
.mi-voice-strip.vs-listening { background: rgba(239,68,68,0.06); }
.mi-voice-strip.vs-thinking { background: rgba(234,179,8,0.06); }
.mi-voice-strip.vs-speaking { background: rgba(6,182,212,0.06); }

.mi-voice-waveform {
  display: flex; align-items: center; gap: 2px; height: 18px;
}
.mi-voice-waveform span {
  width: 3px; border-radius: 2px;
  background: #ef4444; animation: voiceBar 1s ease-in-out infinite;
}
.vs-thinking .mi-voice-waveform span { background: #eab308; }
.vs-speaking .mi-voice-waveform span { background: #06b6d4; }
.mi-voice-waveform span:nth-child(1) { height: 6px; animation-delay: 0s; }
.mi-voice-waveform span:nth-child(2) { height: 12px; animation-delay: 0.1s; }
.mi-voice-waveform span:nth-child(3) { height: 18px; animation-delay: 0.2s; }
.mi-voice-waveform span:nth-child(4) { height: 12px; animation-delay: 0.3s; }
.mi-voice-waveform span:nth-child(5) { height: 6px; animation-delay: 0.4s; }

@keyframes voiceBar {
  0%, 100% { transform: scaleY(0.4); }
  50% { transform: scaleY(1); }
}

.mi-voice-label {
  font-size: 0.75rem; font-weight: 600; white-space: nowrap;
  color: var(--rs-text-muted);
}
.vs-listening .mi-voice-label { color: #ef4444; }
.vs-thinking .mi-voice-label { color: #eab308; }
.vs-speaking .mi-voice-label { color: #06b6d4; }

.mi-voice-interim {
  flex: 1; font-size: 0.78rem; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  color: var(--rs-text-secondary); font-style: italic;
}

.mi-voice-stop {
  background: none; border: none; cursor: pointer;
  font-size: 1rem; line-height: 1; padding: 2px 6px; border-radius: 4px;
  color: var(--rs-text-muted); transition: all 0.15s; flex-shrink: 0;
}
.mi-voice-stop:hover { background: var(--rs-bg-hover); color: var(--rs-text-primary); }

@media (max-width: 640px) {
  .mi { max-width: none; width: 100%; }
  .mi-panel {
    position: fixed; top: 0; left: 0; right: 0; bottom: 0;
    width: 100%; max-width: 100%; height: 100%; max-height: 100%;
    border-radius: 0; resize: none;
    transform: none;
  }
}
`;
