/**
 * <rstack-identity> — Custom element for EncryptID sign-in/sign-out.
 *
 * Renders either a "Sign In" button or the user avatar + dropdown.
 * Contains the full WebAuthn auth modal (sign-in + register).
 * Refactored from lib/rspace-header.ts into a standalone web component.
 */

import { rspaceNavUrl, getCurrentModule, getCurrentSpace } from "../url-helpers";
import { resetDocBridge, isEncryptedBackupEnabled, setEncryptedBackupEnabled } from "../local-first/encryptid-bridge";
import { getShortcuts, setShortcut, removeShortcut } from "../shortcut-config";

const SESSION_KEY = "encryptid_session";
const ENCRYPTID_URL = ""; // same-origin — avoids cross-origin issues on Safari
const COOKIE_NAME = "eid_token";
const COOKIE_MAX_AGE = 30 * 24 * 60 * 60; // 30 days (matches server sessionDuration)

interface SessionState {
	accessToken: string;
	claims: {
		sub: string;
		exp: number;
		username?: string;
		did?: string;
		eid: {
			walletAddress?: string;
			authLevel: number;
			capabilities: { encrypt: boolean; sign: boolean; wallet: boolean };
		};
	};
}

// ── EIP-6963 browser wallet discovery ──

interface _EIP6963Provider {
	info: { uuid: string; name: string; icon: string; rdns: string };
	provider: { request: (args: { method: string; params?: unknown[] }) => Promise<unknown> };
}

class _WalletDiscovery {
	providers: _EIP6963Provider[] = [];
	#cb: (() => void) | null = null;

	start(onChange: () => void) {
		this.#cb = onChange;
		window.addEventListener("eip6963:announceProvider", ((e: CustomEvent) => {
			const detail = e.detail as _EIP6963Provider;
			if (!this.providers.some((p) => p.info.uuid === detail.info.uuid)) {
				this.providers.push(detail);
				this.#cb?.();
			}
		}) as EventListener);
		window.dispatchEvent(new Event("eip6963:requestProvider"));
	}

	stop() { this.#cb = null; }
}

// ── Cross-subdomain cookie helpers ──

function _isRspace(): boolean {
	return window.location.hostname === "rspace.online" || window.location.hostname.endsWith(".rspace.online");
}

function _setSessionCookie(token: string): void {
	try {
		const domain = _isRspace() ? "; domain=.rspace.online" : "";
		const secure = window.location.protocol === "https:" ? "; Secure" : "";
		document.cookie = `${COOKIE_NAME}=${encodeURIComponent(token)}; path=/${domain}; max-age=${COOKIE_MAX_AGE}; SameSite=Lax${secure}`;
	} catch { /* ignore */ }
}

function _getSessionCookie(): string | null {
	try {
		const match = document.cookie.match(new RegExp(`(?:^|; )${COOKIE_NAME}=([^;]*)`));
		return match ? decodeURIComponent(match[1]) : null;
	} catch { return null; }
}

function _removeSessionCookie(): void {
	try {
		const domain = _isRspace() ? "; domain=.rspace.online" : "";
		const secure = window.location.protocol === "https:" ? "; Secure" : "";
		document.cookie = `${COOKIE_NAME}=; path=/${domain}; max-age=0; SameSite=Lax${secure}`;
	} catch { /* ignore */ }
}

// ── Early cookie→localStorage sync ──
// Runs at module load time so code that directly reads localStorage
// (e.g. folk-canvas WebSocket auth, sync.ts, shell inline scripts)
// sees the session even on a new subdomain.
(function _syncCookieToLocalStorage() {
	try {
		if (localStorage.getItem(SESSION_KEY)) return; // already have it
		const cookieToken = _getSessionCookie();
		if (!cookieToken) return;
		const payload = parseJWT(cookieToken);
		if (!payload.exp || Math.floor(Date.now() / 1000) >= (payload.exp as number)) return;
		const session: SessionState = {
			accessToken: cookieToken,
			claims: {
				sub: (payload.sub as string) || "",
				exp: (payload.exp as number) || 0,
				username: (payload.username as string) || "",
				did: (payload.did as string) || "",
				eid: (payload.eid as any) || { authLevel: 1, capabilities: { encrypt: true, sign: true, wallet: false } },
			},
		};
		localStorage.setItem(SESSION_KEY, JSON.stringify(session));
		if (session.claims.username) localStorage.setItem("rspace-username", session.claims.username);
	} catch { /* ignore */ }
})();

// ── Session helpers (exported for use by other code) ──

export function getSession(): SessionState | null {
	try {
		// 1. Try localStorage first (fast path, same origin)
		const stored = localStorage.getItem(SESSION_KEY);
		if (stored) {
			const session = JSON.parse(stored) as SessionState;
			if (Math.floor(Date.now() / 1000) < session.claims.exp) {
				return session;
			}
			// Expired in localStorage — clear it but don't give up yet
			localStorage.removeItem(SESSION_KEY);
			localStorage.removeItem("rspace-username");
		}

		// 2. Fall back to cross-subdomain cookie (handles navigation between subdomains)
		const cookieToken = _getSessionCookie();
		if (cookieToken) {
			const payload = parseJWT(cookieToken);
			if (payload.exp && Math.floor(Date.now() / 1000) < (payload.exp as number)) {
				// Valid token in cookie — restore to localStorage for fast access
				const session: SessionState = {
					accessToken: cookieToken,
					claims: {
						sub: (payload.sub as string) || "",
						exp: (payload.exp as number) || 0,
						username: (payload.username as string) || "",
						did: (payload.did as string) || "",
						eid: (payload.eid as any) || { authLevel: 1, capabilities: { encrypt: true, sign: true, wallet: false } },
					},
				};
				localStorage.setItem(SESSION_KEY, JSON.stringify(session));
				if (session.claims.username) localStorage.setItem("rspace-username", session.claims.username);
				return session;
			}
			// Cookie token expired too — don't clear it yet; #refreshIfNeeded will try server refresh
		}

		return null;
	} catch {
		return null;
	}
}

export function clearSession(): void {
	// Notify server so all other browser sessions get revoked
	try {
		const stored = localStorage.getItem(SESSION_KEY);
		if (stored) {
			const session = JSON.parse(stored) as SessionState;
			if (session.accessToken) {
				fetch(`${ENCRYPTID_URL}/api/session/logout`, {
					method: "POST",
					headers: { Authorization: `Bearer ${session.accessToken}` },
				}).catch(() => { /* best-effort */ });
			}
		}
	} catch { /* ignore */ }

	localStorage.removeItem(SESSION_KEY);
	localStorage.removeItem("rspace-username");
	_removeSessionCookie();
}

export function isAuthenticated(): boolean {
	return getSession() !== null;
}

export function getAccessToken(): string | null {
	return getSession()?.accessToken ?? null;
}

export function getUserDID(): string | null {
	return getSession()?.claims.did ?? getSession()?.claims.sub ?? null;
}

export function getUsername(): string | null {
	return getSession()?.claims.username ?? null;
}

// ── Helpers ──

function base64urlToBuffer(b64url: string): ArrayBuffer {
	const b64 = b64url.replace(/-/g, "+").replace(/_/g, "/");
	const pad = "=".repeat((4 - (b64.length % 4)) % 4);
	const bin = atob(b64 + pad);
	const bytes = new Uint8Array(bin.length);
	for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
	return bytes.buffer;
}

function bufferToBase64url(buf: ArrayBuffer): string {
	const bytes = new Uint8Array(buf);
	let bin = "";
	for (let i = 0; i < bytes.byteLength; i++) bin += String.fromCharCode(bytes[i]);
	return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
}

function parseJWT(token: string): Record<string, unknown> {
	const parts = token.split(".");
	if (parts.length < 2) return {};
	try {
		const b64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
		const pad = "=".repeat((4 - (b64.length % 4)) % 4);
		return JSON.parse(atob(b64 + pad));
	} catch {
		return {};
	}
}

function storeSession(token: string, username: string, did: string): void {
	const payload = parseJWT(token) as Record<string, any>;
	const session: SessionState = {
		accessToken: token,
		claims: {
			sub: payload.sub || "",
			exp: payload.exp || 0,
			username,
			did,
			eid: payload.eid || { authLevel: 3, capabilities: { encrypt: true, sign: true, wallet: false } },
		},
	};
	localStorage.setItem(SESSION_KEY, JSON.stringify(session));
	if (username) localStorage.setItem("rspace-username", username);
	_setSessionCookie(token);
	if (username && did) addKnownPersona(username, did);
}

// ── Persona helpers (client-side multi-account) ──

const PERSONAS_KEY = "rspace-known-personas";
const KNOWN_ACCOUNTS_KEY = "encryptid-known-accounts";

interface KnownPersona {
	username: string;
	did: string;
}

function getKnownPersonas(): KnownPersona[] {
	try {
		return JSON.parse(localStorage.getItem(PERSONAS_KEY) || "[]");
	} catch { return []; }
}

/** Merge usernames from all localStorage sources into a deduplicated list. */
function getAllKnownUsernames(): string[] {
	const seen = new Set<string>();
	const result: string[] = [];
	const add = (u: string) => { if (u && !seen.has(u)) { seen.add(u); result.push(u); } };

	// 1. Current session username (always first if logged in)
	try {
		const raw = localStorage.getItem(SESSION_KEY);
		if (raw) {
			const session = JSON.parse(raw) as SessionState;
			if (session?.claims?.username) add(session.claims.username);
		}
	} catch { /* ignore */ }

	// 2. Cached username shortcut
	try { add(localStorage.getItem("rspace-username") || ""); } catch { /* ignore */ }

	// 3. Persona list (has DID, most reliable historical source)
	for (const p of getKnownPersonas()) add(p.username);

	// 4. encryptid login-button known accounts
	try {
		const accounts: { username: string }[] = JSON.parse(localStorage.getItem(KNOWN_ACCOUNTS_KEY) || "[]");
		for (const a of accounts) add(a.username);
	} catch { /* ignore */ }

	return result;
}

function addKnownPersona(username: string, did: string): void {
	const personas = getKnownPersonas();
	const idx = personas.findIndex(p => p.did === did);
	if (idx >= 0) {
		personas[idx] = { username, did };
	} else {
		personas.push({ username, did });
	}
	localStorage.setItem(PERSONAS_KEY, JSON.stringify(personas));
	// Also sync to encryptid-known-accounts for login-button compat
	try {
		const accounts: { username: string; displayName?: string }[] = JSON.parse(localStorage.getItem(KNOWN_ACCOUNTS_KEY) || "[]");
		if (!accounts.some(a => a.username === username)) {
			accounts.unshift({ username });
			localStorage.setItem(KNOWN_ACCOUNTS_KEY, JSON.stringify(accounts));
		}
	} catch { /* ignore */ }
}

function removeKnownPersona(did: string): void {
	const personas = getKnownPersonas().filter(p => p.did !== did);
	localStorage.setItem(PERSONAS_KEY, JSON.stringify(personas));
}

// ── Auto-space resolution after auth ──

function autoResolveSpace(token: string, username: string): void {
	if (!username) return;

	const currentSpace = _getCurrentSpace();
	const currentModule = _getCurrentModule();

	if (currentSpace !== "demo" || currentModule !== "rspace") {
		// User is viewing a specific page (their own space, someone else's,
		// or a module on demo). Don't redirect — just reload so the
		// authenticated session takes effect (access gates, CRDT sync).
		autoProvisionSpace(token);
		window.location.reload();
		return;
	}

	// On demo dashboard (landing page) — provision personal space and redirect
	fetch("/api/spaces/auto-provision", {
		method: "POST",
		headers: {
			Authorization: `Bearer ${token}`,
			"Content-Type": "application/json",
		},
	})
		.then((r) => r.json())
		.then((data) => {
			if (!data.slug) return;
			window.location.replace(_navUrl(data.slug, "rspace"));
		})
		.catch(() => {});
}

// ── Silent provisioning (no redirect) — ensures user's space exists ──

function autoProvisionSpace(token: string): void {
	fetch("/api/spaces/auto-provision", {
		method: "POST",
		headers: {
			Authorization: `Bearer ${token}`,
			"Content-Type": "application/json",
		},
	}).catch(() => {});
}

// ── Inline URL helpers (avoid import cycle with url-helpers) ──
const _RESERVED = ["www", "rspace", "create", "new", "start", "auth"];
function _isSubdomain(): boolean {
	const p = window.location.host.split(":")[0].split(".");
	return p.length >= 3 && p.slice(-2).join(".") === "rspace.online" && !_RESERVED.includes(p[0]);
}
function _isBareDomain(): boolean {
	const h = window.location.host.split(":")[0];
	return h === "rspace.online" || h === "www.rspace.online";
}
function _getCurrentSpace(): string {
	if (_isSubdomain()) return window.location.host.split(":")[0].split(".")[0];
	if (_isBareDomain()) return "demo";
	return window.location.pathname.split("/").filter(Boolean)[0] || "demo";
}
function _getCurrentModule(): string {
	const parts = window.location.pathname.split("/").filter(Boolean);
	if (_isSubdomain() || _isBareDomain()) return parts[0] || "rspace";
	return parts[1] || "rspace";
}
function _navUrl(space: string, moduleId: string): string {
	const h = window.location.host.split(":")[0].split(".");
	const proto = window.location.protocol;
	const BASE = "rspace.online";
	const onSub = h.length >= 3 && h.slice(-2).join(".") === BASE && !_RESERVED.includes(h[0]);
	if (onSub) {
		if (h[0] === space) return "/" + moduleId;
		if (_RESERVED.includes(space)) return proto + "//" + BASE + "/" + moduleId;
		return proto + "//" + space + "." + BASE + "/" + moduleId;
	}
	const host = window.location.host.split(":")[0];
	if (host === BASE || host === "www." + BASE || host.endsWith("." + BASE)) {
		if (space === "demo" || _RESERVED.includes(space)) return "/" + moduleId;
		return proto + "//" + space + "." + BASE + "/" + moduleId;
	}
	return "/" + space + "/" + moduleId;
}

// ── The custom element ──

export class RStackIdentity extends HTMLElement {
	#shadow: ShadowRoot;

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

	connectedCallback() {
		this.#refreshIfNeeded();
		this.#render();

		// If a session already exists on page load, provision the
		// user's personal space in the background (but do NOT redirect —
		// the user intentionally navigated to this space).
		const session = getSession();
		if (session?.accessToken && session.claims.username) {
			autoProvisionSpace(session.accessToken);
		}

		// Validate session with server — detects logout from another browser session
		this.#validateSessionWithServer();

		// Nudge users without a second device to link one
		this.#checkDeviceNudge();

		// Show recovery alert dot on avatar if not set up
		this.#checkRecoveryBadge();

		// Propagate login/logout across tabs via storage events
		window.addEventListener("storage", this.#onStorageChange);
	}

	disconnectedCallback() {
		window.removeEventListener("storage", this.#onStorageChange);
	}

	async #validateSessionWithServer() {
		const session = getSession();
		if (!session?.accessToken) return;

		// Throttle: skip if validated within the last 5 minutes
		const VALIDATE_KEY = "eid_last_validated";
		const VALIDATE_INTERVAL = 5 * 60 * 1000;
		const lastValidated = parseInt(localStorage.getItem(VALIDATE_KEY) || "0", 10);
		if (Date.now() - lastValidated < VALIDATE_INTERVAL) return;

		try {
			const res = await fetch(`${ENCRYPTID_URL}/api/session/verify`, {
				headers: { Authorization: `Bearer ${session.accessToken}` },
			});
			if (!res.ok) {
				// Session revoked — clear locally and re-render
				localStorage.removeItem(VALIDATE_KEY);
				localStorage.removeItem(SESSION_KEY);
				localStorage.removeItem("rspace-username");
				_removeSessionCookie();
				resetDocBridge();
				this.#render();
				this.dispatchEvent(new CustomEvent("auth-change", { bubbles: true, composed: true, detail: { reason: "revoked" } }));
			} else {
				localStorage.setItem(VALIDATE_KEY, String(Date.now()));
			}
		} catch { /* network error — let token expire naturally */ }
	}

	async #checkRecoveryBadge() {
		const session = getSession();
		if (!session?.accessToken) return;

		// Cache: only re-check every 30 minutes
		const CACHE_KEY = "eid_recovery_status";
		const cached = localStorage.getItem(CACHE_KEY);
		if (cached) {
			try {
				const c = JSON.parse(cached);
				if (Date.now() - c.ts < 30 * 60 * 1000) {
					if (!c.socialRecovery) this.#showRecoveryDot();
					if (!c.email || !c.multiDevice || !c.socialRecovery) this.#showAccountDot();
					return;
				}
			} catch { /* stale cache */ }
		}

		try {
			const res = await fetch(`${ENCRYPTID_URL}/api/account/status`, {
				headers: { Authorization: `Bearer ${getAccessToken()}` },
			});
			if (!res.ok) return;
			const status = await res.json();
			localStorage.setItem(CACHE_KEY, JSON.stringify({ ts: Date.now(), email: !!status.email, multiDevice: !!status.multiDevice, socialRecovery: !!status.socialRecovery }));
			if (!status.socialRecovery) this.#showRecoveryDot();
			if (!status.email || !status.multiDevice || !status.socialRecovery) this.#showAccountDot();
		} catch { /* offline */ }
	}

	#showRecoveryDot() {
		const wrap = this.#shadow.querySelector(".avatar-wrap");
		if (!wrap || wrap.querySelector(".recovery-alert-dot")) return;
		const dot = document.createElement("span");
		dot.className = "recovery-alert-dot";
		dot.title = "Social recovery not set up";
		wrap.appendChild(dot);
	}

	#showAccountDot() {
		const btn = this.#shadow.querySelector('[data-action="my-account"]');
		if (!btn || btn.querySelector(".acct-alert-dot")) return;
		const dot = document.createElement("span");
		dot.className = "acct-alert-dot";
		btn.appendChild(dot);
	}

	async #checkDeviceNudge() {
		const session = getSession();
		if (!session?.accessToken) return;

		const NUDGE_KEY = "eid_device_nudge_dismissed";
		const DONE_KEY = "eid_device_nudge_done";

		// Permanently suppress if multi-device already confirmed
		if (localStorage.getItem(DONE_KEY) === "1") return;

		// Don't nag if dismissed within the last 7 days
		const dismissed = localStorage.getItem(NUDGE_KEY);
		if (dismissed && Date.now() - parseInt(dismissed, 10) < 7 * 24 * 60 * 60 * 1000) return;

		// Wait a moment so it doesn't compete with page load
		await new Promise(r => setTimeout(r, 3000));

		// Fetch account status
		try {
			const res = await fetch(`${ENCRYPTID_URL}/api/account/status`, {
				headers: { Authorization: `Bearer ${getAccessToken()}` },
			});
			if (!res.ok) return;
			const status = await res.json();
			if (status.multiDevice) {
				// Permanently mark as done — never nudge again
				localStorage.setItem(DONE_KEY, "1");
				return;
			}

			// Show a toast nudge with QR code
			const toast = document.createElement("div");
			toast.className = "eid-device-nudge";

			let linkUrl = "";
			let linkError = "";

			const renderNudge = () => {
				const qrHTML = linkUrl
					? `<div class="eid-nudge-qr">
						<img src="https://api.qrserver.com/v1/create-qr-code/?size=160x160&data=${encodeURIComponent(linkUrl)}&format=png&margin=6" width="160" height="160" alt="Scan to link device" />
					</div>
					<div class="eid-nudge-link-row">
						<input class="eid-nudge-link-input" type="text" readonly value="${linkUrl}" />
						<button class="eid-nudge-btn eid-nudge-btn--copy" data-action="copy">Copy</button>
					</div>
					<div class="eid-nudge-expire">Expires in 10 minutes</div>`
					: linkError
						? `<div class="eid-nudge-error">${linkError}</div>`
						: `<div class="eid-nudge-loading"><span class="eid-nudge-spinner"></span> Generating link…</div>`;

				toast.innerHTML = `
					<style>
					.eid-device-nudge {
						position: fixed; bottom: 1.5rem; right: 1.5rem; z-index: 9999;
						background: var(--rs-bg-surface, #1e1e2e); border: 1px solid rgba(6,182,212,0.4);
						border-radius: 14px; padding: 1rem 1.25rem; max-width: min(340px, calc(100vw - 3rem));
						box-shadow: 0 8px 32px rgba(0,0,0,0.4); animation: eid-nudge-in 0.4s ease-out;
						color: var(--rs-text-primary, #e2e8f0); font-family: system-ui, sans-serif;
					}
					.eid-nudge-header { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
					.eid-nudge-icon { font-size: 1.5rem; }
					.eid-nudge-title { font-weight: 600; font-size: 0.95rem; }
					.eid-nudge-body { font-size: 0.85rem; color: var(--rs-text-secondary, #94a3b8); line-height: 1.5; margin-bottom: 0.75rem; }
					.eid-nudge-qr { text-align: center; margin-bottom: 0.5rem; }
					.eid-nudge-qr img { border-radius: 10px; background: #fff; padding: 4px; }
					.eid-nudge-link-row { display: flex; gap: 6px; margin-bottom: 0.4rem; }
					.eid-nudge-link-input {
						flex: 1; font-size: 0.72rem; padding: 5px 8px; border-radius: 6px;
						border: 1px solid var(--rs-border, #334155); background: var(--rs-input-bg, #0f172a);
						color: var(--rs-text-secondary, #94a3b8); min-width: 0; outline: none;
					}
					.eid-nudge-btn--copy {
						padding: 5px 10px; border-radius: 6px; border: 1px solid var(--rs-border, #334155);
						background: transparent; color: var(--rs-text-secondary, #94a3b8);
						font-size: 0.75rem; font-weight: 600; cursor: pointer; white-space: nowrap;
					}
					.eid-nudge-btn--copy:hover { color: var(--rs-text-primary, #e2e8f0); }
					.eid-nudge-expire { font-size: 0.75rem; color: var(--rs-text-muted, #64748b); text-align: center; margin-bottom: 0.5rem; }
					.eid-nudge-loading { text-align: center; padding: 1.5rem 0; color: var(--rs-text-secondary, #94a3b8); font-size: 0.85rem; }
					.eid-nudge-error { text-align: center; padding: 0.75rem 0; color: #f87171; font-size: 0.85rem; }
					.eid-nudge-spinner {
						display: inline-block; width: 14px; height: 14px; border: 2px solid rgba(148,163,184,0.3);
						border-top-color: #94a3b8; border-radius: 50%; animation: eid-spin 0.6s linear infinite;
						vertical-align: middle; margin-right: 6px;
					}
					.eid-nudge-actions { display: flex; gap: 8px; }
					.eid-nudge-btn {
						padding: 8px 14px; border-radius: 8px; border: none; font-size: 0.85rem;
						font-weight: 600; cursor: pointer; transition: all 0.2s;
					}
					.eid-nudge-btn--dismiss {
						background: transparent; color: var(--rs-text-muted, #64748b);
						border: 1px solid var(--rs-border, #334155); flex: 1; text-align: center;
					}
					.eid-nudge-btn--dismiss:hover { color: var(--rs-text-secondary, #94a3b8); }
					@keyframes eid-nudge-in { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
					@keyframes eid-spin { to { transform: rotate(360deg); } }
					</style>
					<div class="eid-nudge-header">
						<span class="eid-nudge-icon">📱</span>
						<span class="eid-nudge-title">Link a second device</span>
					</div>
					<div class="eid-nudge-body">
						Scan this QR code on your phone or tablet to add a passkey backup.
					</div>
					${qrHTML}
					<div class="eid-nudge-actions">
						<button class="eid-nudge-btn eid-nudge-btn--dismiss" data-action="later">Dismiss</button>
					</div>
				`;

				toast.querySelector('[data-action="later"]')?.addEventListener("click", () => {
					localStorage.setItem(NUDGE_KEY, String(Date.now()));
					toast.style.animation = "eid-nudge-in 0.3s ease-in reverse forwards";
					setTimeout(() => toast.remove(), 300);
				});
				toast.querySelector('[data-action="copy"]')?.addEventListener("click", () => {
					navigator.clipboard.writeText(linkUrl).catch(() => {});
					const btn = toast.querySelector('[data-action="copy"]') as HTMLButtonElement;
					if (btn) { btn.textContent = "Copied!"; setTimeout(() => { btn.textContent = "Copy"; }, 2000); }
				});
			};

			document.body.appendChild(toast);
			renderNudge(); // show loading state

			// Generate device link
			try {
				const linkRes = await fetch(`${ENCRYPTID_URL}/api/device-link/start`, {
					method: "POST",
					headers: { Authorization: `Bearer ${getAccessToken()}` },
				});
				const linkData = await linkRes.json();
				if (!linkRes.ok || linkData.error) throw new Error(linkData.error || "Failed");
				linkUrl = linkData.linkUrl;
			} catch {
				linkError = "Could not generate link. Try My Account → Devices.";
			}
			renderNudge(); // show QR or error
		} catch { /* offline */ }
	}

	#onStorageChange = (e: StorageEvent) => {
		if (e.key === "encryptid_session" || e.key === PERSONAS_KEY) {
			this.#render();
			if (e.key === "encryptid_session") {
				this.dispatchEvent(new CustomEvent("auth-change", { bubbles: true, composed: true, detail: { reason: e.newValue ? "signin" : "signout" } }));
				// Session cleared from another tab — reload to show logged-out state
				if (!e.newValue) window.location.reload();
			}
		}
	};

	async #refreshIfNeeded() {
		let session = getSession();
		let token = session?.accessToken ?? null;
		let username = session?.claims.username || "";
		let did = session?.claims.did || "";

		// If no valid session in localStorage/cookie, check for an expired cookie token
		// we can try to refresh (server accepts expired tokens for refresh)
		if (!session) {
			const cookieToken = _getSessionCookie();
			if (!cookieToken) return; // no token at all — truly logged out
			const payload = parseJWT(cookieToken);
			token = cookieToken;
			username = (payload.username as string) || "";
			did = (payload.did as string) || "";
		}

		// If session is valid with >7 days remaining, no refresh needed
		if (session) {
			const remaining = session.claims.exp - Math.floor(Date.now() / 1000);
			if (remaining >= 7 * 24 * 60 * 60) return;
		}

		// Token is expired or nearing expiry — try server refresh
		try {
			const res = await fetch(`${ENCRYPTID_URL}/api/session/refresh`, {
				method: "POST",
				headers: { Authorization: `Bearer ${token}` },
			});
			if (!res.ok) {
				// Server rejected — token is truly dead, clean up
				if (!session) { _removeSessionCookie(); }
				return;
			}
			const refreshData = await res.json();
			const newToken = refreshData.token;
			if (newToken) {
				const payload = parseJWT(newToken);
				storeSession(newToken, (payload.username as string) || refreshData.username || username, (payload.did as string) || did);
				this.#render();
		this.dispatchEvent(new CustomEvent("auth-change", { bubbles: true, composed: true, detail: { reason: "refresh" } }));
			}
		} catch { /* offline — keep whatever we have */ }
	}

	#render() {
		const session = getSession();

		if (session) {
			const username = session.claims.username || "";
			const did = session.claims.did || session.claims.sub;
			const displayName = username || (did.length > 24 ? did.slice(0, 16) + "..." + did.slice(-6) : did);
			const initial = username ? username[0].toUpperCase() : did.slice(8, 10).toUpperCase();
			const currentDid = session.claims.did || "";
			const otherPersonas = getKnownPersonas().filter(p => p.did !== currentDid);

			this.#shadow.innerHTML = `
				<style>${STYLES}</style>
				<div class="user" id="user-toggle">
					<div class="avatar-wrap">
						<div class="avatar">${initial}</div>
					</div>
					<span class="name">${displayName}</span>
					<div class="dropdown" id="dropdown">
						<div class="dropdown-header">${displayName}</div>
						${otherPersonas.length > 0 ? `
						<div class="dropdown-divider"></div>
						<div class="dropdown-label">Switch Persona</div>
						${otherPersonas.map(p => `
						<div class="persona-row">
							<button class="dropdown-item persona-item" data-action="switch-persona" data-did="${p.did}">
								<div class="persona-avatar">${p.username[0].toUpperCase()}</div>
								<span>${p.username}</span>
							</button>
							<button class="persona-remove" data-action="remove-persona" data-did="${p.did}">&times;</button>
						</div>
						`).join("")}
						` : ""}
						<div class="dropdown-divider"></div>
						<button class="dropdown-item" data-action="add-persona">＋ Add Persona</button>
						<div class="dropdown-divider"></div>
						<button class="dropdown-item" data-action="my-account">👤 My Account</button>
						<button class="dropdown-item" data-action="my-spaces">🌐 My Spaces</button>
						<button class="dropdown-item" data-action="my-wallets">💰 My Wallets</button>
						<div class="dropdown-divider mobile-only"></div>
						<button class="dropdown-item mobile-only" data-action="notifications">🔔 Notifications</button>
						<button class="dropdown-item mobile-only" data-action="share">📤 Share</button>
						<button class="dropdown-item mobile-only" data-action="settings">⚙️ Space Settings</button>
						<div class="dropdown-divider"></div>
						<div class="dropdown-theme-row">
							<span class="theme-icon">☀️</span>
							<label class="theme-toggle">
								<input type="checkbox" id="dropdown-theme-toggle" ${(localStorage.getItem("canvas-theme") || "dark") === "dark" ? "checked" : ""} />
								<span class="theme-slider"></span>
							</label>
							<span class="theme-icon">🌙</span>
						</div>
						<div class="dropdown-canvas-row">
							<span class="canvas-label">Canvas</span>
							<div class="canvas-options" id="canvas-bg-options">
								<button class="canvas-opt${(localStorage.getItem("canvas-bg") || "grid") === "grid" ? " active" : ""}" data-bg="grid">Grid</button>
								<button class="canvas-opt${localStorage.getItem("canvas-bg") === "dot" ? " active" : ""}" data-bg="dot">Dot</button>
								<button class="canvas-opt${localStorage.getItem("canvas-bg") === "blank" ? " active" : ""}" data-bg="blank">Blank</button>
							</div>
						</div>
						<div class="dropdown-divider"></div>
						<button class="dropdown-item dropdown-item--danger" data-action="signout">🚪 Sign Out</button>
					</div>
				</div>
			`;

			const toggle = this.#shadow.getElementById("user-toggle")!;
			const dropdown = this.#shadow.getElementById("dropdown")!;

			toggle.addEventListener("click", (e) => {
				e.stopPropagation();
				dropdown.classList.toggle("open");
			});

			document.addEventListener("click", () => dropdown.classList.remove("open"));

			this.#shadow.querySelectorAll("[data-action]").forEach((el) => {
				el.addEventListener("click", (e) => {
					e.stopPropagation();
					const action = (el as HTMLElement).dataset.action;
					dropdown.classList.remove("open");
					if (action === "signout") {
						clearSession();
						resetDocBridge();
						this.dispatchEvent(new CustomEvent("auth-change", { bubbles: true, composed: true, detail: { reason: "signout" } }));
						return;
					} else if (action === "my-account") {
						this.showAccountModal();
					} else if (action === "my-spaces") {
						this.#showSpacesModal();
					} else if (action === "my-wallets") {
						this.#showWalletsModal();
					} else if (action === "notifications") {
						(document.querySelector("rstack-notification-bell") as any)?.toggle();
					} else if (action === "share") {
						if (navigator.share) {
							navigator.share({ url: location.href, title: document.title }).catch(() => {});
						} else {
							(document.querySelector("rstack-share-panel") as any)?.toggle();
						}
					} else if (action === "settings") {
						(document.getElementById("settings-btn") as HTMLElement)?.click();
					} else if (action === "switch-persona") {
						const targetDid = (el as HTMLElement).dataset.did || "";
						const persona = getKnownPersonas().find(p => p.did === targetDid);
						if (!persona) return;
						clearSession();
						resetDocBridge();
						this.showAuthModal({ onSuccess: () => {}, onCancel: () => { this.#render(); } }, persona.username);
					} else if (action === "add-persona") {
						clearSession();
						resetDocBridge();
						this.#render();
						this.dispatchEvent(new CustomEvent("auth-change", { bubbles: true, composed: true, detail: { reason: "persona-switch" } }));
						this.showAuthModal();
					} else if (action === "remove-persona") {
						const targetDid = (el as HTMLElement).dataset.did || "";
						removeKnownPersona(targetDid);
						this.#render();
					}
				});
			});

			const dropdownTheme = this.#shadow.getElementById("dropdown-theme-toggle") as HTMLInputElement;
			if (dropdownTheme) {
				dropdownTheme.addEventListener("click", (e) => e.stopPropagation());
				dropdownTheme.addEventListener("change", () => {
					const newTheme = dropdownTheme.checked ? "dark" : "light";
					localStorage.setItem("canvas-theme", newTheme);
					document.documentElement.setAttribute("data-theme", newTheme);
					this.dispatchEvent(new CustomEvent("theme-change", { bubbles: true, composed: true, detail: { theme: newTheme } }));
				});
			}

			// Canvas background style selector
			const canvasBgOptions = this.#shadow.getElementById("canvas-bg-options");
			if (canvasBgOptions) {
				canvasBgOptions.addEventListener("click", (e) => {
					e.stopPropagation();
					const btn = (e.target as HTMLElement).closest("[data-bg]") as HTMLElement | null;
					if (!btn) return;
					const bg = btn.dataset.bg!;
					localStorage.setItem("canvas-bg", bg);
					document.documentElement.setAttribute("data-canvas-bg", bg);
					canvasBgOptions.querySelectorAll(".canvas-opt").forEach(b => b.classList.remove("active"));
					btn.classList.add("active");
					window.dispatchEvent(new Event("canvas-bg-change"));
				});
			}
		} else {
			this.#shadow.innerHTML = `
				<style>${STYLES}</style>
				<button class="signin-btn" id="signin-btn">🔑 Sign In</button>
			`;

			this.#shadow.getElementById("signin-btn")!.addEventListener("click", () => {
				this.showAuthModal();
			});
		}
	}

	/** Public: check if user has an active session */
	isSignedIn(): boolean {
		return getSession() !== null;
	}

	/** Public method: show the auth modal programmatically.
	 *  Pass usernameHint to auto-trigger passkey sign-in for a specific persona. */
	showAuthModal(callbacks?: { onSuccess?: () => void; onCancel?: () => void }, usernameHint?: string): void {
		if (document.querySelector(".rstack-auth-overlay")) return;

		const overlay = document.createElement("div");
		overlay.className = "rstack-auth-overlay";
		let mode: "signin" | "register" = "signin";

		const render = () => {
			overlay.innerHTML = mode === "signin" ? signinHTML() : registerHTML();
			attachListeners();
		};

		let showManualInput = false;

		const signinHTML = () => {
			const knownUsers = usernameHint ? [] : getAllKnownUsernames();
			const showPicker = knownUsers.length > 0 && !showManualInput;

			const accountButtons = knownUsers.map(u => {
				const initial = u[0]?.toUpperCase() || "?";
				const escaped = u.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
				return `<button class="account-pick-btn" data-pick-username="${escaped}">
					<span class="account-pick-avatar">${initial}</span>
					<span class="account-pick-name">${escaped}</span>
					<span class="account-pick-arrow">→</span>
				</button>`;
			}).join("");

			return `
			<style>${MODAL_STYLES}
			.account-picker { display: flex; flex-direction: column; gap: 8px; margin-bottom: 1rem; }
			.account-pick-btn {
				display: flex; align-items: center; gap: 12px; width: 100%; padding: 12px 16px;
				background: var(--rs-bg-hover); border: 1px solid var(--rs-border); border-radius: 10px;
				color: var(--rs-text-primary); cursor: pointer; transition: all 0.2s;
				font-size: 0.95rem; font-family: inherit; text-align: left;
			}
			.account-pick-btn:hover { border-color: #06b6d4; background: rgba(6,182,212,0.08); transform: translateY(-1px); }
			.account-pick-avatar {
				width: 32px; height: 32px; border-radius: 50%; flex-shrink: 0;
				background: linear-gradient(135deg, #06b6d4, #7c3aed); color: white;
				display: flex; align-items: center; justify-content: center; font-weight: 600; font-size: 0.85rem;
			}
			.account-pick-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; }
			.account-pick-arrow { color: var(--rs-text-muted); font-size: 1.1rem; }
			.alt-link { display: block; text-align: center; margin-top: 8px; font-size: 0.85rem; color: var(--rs-text-muted); cursor: pointer; background: none; border: none; font-family: inherit; width: 100%; padding: 4px; }
			.alt-link:hover { color: #06b6d4; text-decoration: underline; }
			</style>
			<div class="auth-modal">
				<button class="close-btn" data-action="cancel">&times;</button>
				<h2>Sign up / Sign in</h2>
				<p>${showPicker ? "Choose an account on this device:" : "Secure, passwordless authentication powered by passkeys."}</p>
				${showPicker ? `
					<div class="account-picker">${accountButtons}</div>
					<button class="alt-link" data-action="show-manual">Use a different account</button>
				` : `
					<input class="input" id="auth-signin-username" type="text" placeholder="Username or email" autocomplete="username webauthn" maxlength="64" />
				`}
				<div class="actions actions--stack">
					${showPicker ? '' : `
						<button class="btn btn--primary" data-action="signin">🔑 Sign In with Passkey</button>
						<button class="btn btn--outline" data-action="send-magic-link">📧 Send Magic Link</button>
					`}
					<button class="btn btn--outline" data-action="switch-register">🔐 Create New Account</button>
				</div>
				<div id="magic-link-msg" style="display:none;margin-top:0.5rem;font-size:0.85rem;text-align:center"></div>
				<div class="error" id="auth-error"></div>
				<div class="learn-more">Powered by <a href="https://ridentity.online" target="_blank" rel="noopener">EncryptID</a></div>
			</div>
		`;};

		const registerHTML = () => `
			<style>${MODAL_STYLES}</style>
			<div class="auth-modal">
				<button class="close-btn" data-action="cancel">&times;</button>
				<h2>Create your EncryptID</h2>
				<p>Set up a secure, passwordless identity.</p>
				<input class="input" id="auth-username" type="text" placeholder="Choose a username" autocomplete="username webauthn" maxlength="32" />
				<input class="input" id="auth-email" type="email" placeholder="Email (optional — for account recovery)" autocomplete="email" maxlength="128" style="margin-top:-0.5rem" />
				<div class="actions">
					<button class="btn btn--secondary" data-action="switch-signin">Back</button>
					<button class="btn btn--primary" data-action="register">🔐 Create Passkey</button>
				</div>
				<div class="error" id="auth-error"></div>
				<div class="learn-more">Powered by <a href="https://ridentity.online" target="_blank" rel="noopener">EncryptID</a></div>
			</div>
		`;

		const close = () => {
			overlay.remove();
		};

		const handleSignIn = async () => {
			const errEl = overlay.querySelector("#auth-error") as HTMLElement | null;
			const btn = overlay.querySelector('[data-action="signin"]') as HTMLButtonElement | null;
			const usernameInput = overlay.querySelector("#auth-signin-username") as HTMLInputElement | null;
			const enteredUsername = usernameInput?.value.trim() || "";
			const loginUsername = usernameHint || enteredUsername;
			if (errEl) errEl.textContent = "";
			if (!loginUsername) {
				if (errEl) errEl.textContent = "Please enter your username or email.";
				usernameInput?.focus();
				return;
			}
			// Show loading state on either the signin button or the clicked picker button
			const pickerBtn = overlay.querySelector(`[data-pick-username="${CSS.escape(loginUsername)}"]`) as HTMLButtonElement | null;
			if (btn) { btn.disabled = true; btn.innerHTML = '<span class="spinner"></span> Authenticating...'; }
			if (pickerBtn) { pickerBtn.style.opacity = "0.6"; pickerBtn.style.pointerEvents = "none"; const arrow = pickerBtn.querySelector(".account-pick-arrow"); if (arrow) arrow.innerHTML = '<span class="spinner" style="width:14px;height:14px;border-width:2px"></span>'; }

			try {
				// Send as email or username depending on input format
				const isEmail = loginUsername.includes("@");
				const authBody = isEmail ? { email: loginUsername } : { username: loginUsername };
				const startRes = await fetch(`${ENCRYPTID_URL}/api/auth/start`, {
					method: "POST",
					headers: { "Content-Type": "application/json" },
					body: JSON.stringify(authBody),
				});
				if (!startRes.ok) throw new Error("Failed to start authentication");
				const { options: serverOptions, userFound } = await startRes.json();

				if (!userFound) {
					throw new Error("No account found for that username. Try creating a new account.");
				}

				// Build allowCredentials from server response to scope to this user's passkeys
				const pubKeyOpts: any = {
					challenge: new Uint8Array(base64urlToBuffer(serverOptions.challenge)),
					rpId: serverOptions.rpId || "rspace.online",
					userVerification: "required",
					timeout: 60000,
				};
				if (serverOptions.allowCredentials?.length) {
					pubKeyOpts.allowCredentials = serverOptions.allowCredentials.map((c: any) => ({
						type: c.type,
						id: new Uint8Array(base64urlToBuffer(c.id)),
						...(c.transports?.length ? { transports: c.transports } : {}),
					}));
				}

				const credential = (await navigator.credentials.get({
					publicKey: pubKeyOpts,
				})) as PublicKeyCredential;
				if (!credential) throw new Error("Authentication failed");

				const completeRes = await fetch(`${ENCRYPTID_URL}/api/auth/complete`, {
					method: "POST",
					headers: { "Content-Type": "application/json" },
					body: JSON.stringify({
						challenge: serverOptions.challenge,
						credential: { credentialId: bufferToBase64url(credential.rawId) },
					}),
				});
				const data = await completeRes.json();
				if (!completeRes.ok || !data.success) throw new Error(data.error || "Authentication failed");

				storeSession(data.token, data.username || "", data.did || "");
				close();
				this.#render();
		this.dispatchEvent(new CustomEvent("auth-change", { bubbles: true, composed: true, detail: { reason: "signin" } }));
				callbacks?.onSuccess?.();
				// Auto-redirect to personal space
				autoResolveSpace(data.token, data.username || "");
			} catch (err: any) {
				if (btn) { btn.disabled = false; btn.innerHTML = "🔑 Sign In with Passkey"; }
				if (pickerBtn) { pickerBtn.style.opacity = ""; pickerBtn.style.pointerEvents = ""; const arrow = pickerBtn.querySelector(".account-pick-arrow"); if (arrow) arrow.innerHTML = "→"; }
				const msg = err.name === "NotAllowedError" ? "Authentication was cancelled." : err.message || "Authentication failed.";
				if (errEl) errEl.textContent = msg;
				// If auto-triggered persona switch was cancelled, close modal and restore previous state
				if (usernameHint) { close(); this.#render(); callbacks?.onCancel?.(); }
			}
		};

		const handleMagicLink = async () => {
			const input = overlay.querySelector("#auth-signin-username") as HTMLInputElement | null;
			const msgEl = overlay.querySelector("#magic-link-msg") as HTMLElement | null;
			const errEl = overlay.querySelector("#auth-error") as HTMLElement | null;
			const value = input?.value.trim() || "";
			if (!value || !value.includes("@")) {
				if (errEl) errEl.textContent = "Enter an email address to receive a magic link.";
				input?.focus();
				return;
			}
			if (errEl) errEl.textContent = "";
			const btn = overlay.querySelector('[data-action="send-magic-link"]') as HTMLButtonElement | null;
			if (btn) { btn.disabled = true; btn.innerHTML = '<span class="spinner"></span> Sending...'; }
			try {
				await fetch(`${ENCRYPTID_URL}/api/auth/magic-link`, {
					method: "POST",
					headers: { "Content-Type": "application/json" },
					body: JSON.stringify({ email: value }),
				});
				if (msgEl) { msgEl.style.display = ""; msgEl.style.color = "#22c55e"; msgEl.textContent = "Login link sent! Check your inbox."; }
			} catch {
				if (msgEl) { msgEl.style.display = ""; msgEl.style.color = "#f87171"; msgEl.textContent = "Failed to send. Try again."; }
			} finally {
				if (btn) { btn.disabled = false; btn.innerHTML = "📧 Send Magic Link"; }
			}
		};

		const handleRegister = async () => {
			const usernameInput = overlay.querySelector("#auth-username") as HTMLInputElement;
			const emailInput = overlay.querySelector("#auth-email") as HTMLInputElement | null;
			const errEl = overlay.querySelector("#auth-error") as HTMLElement;
			const btn = overlay.querySelector('[data-action="register"]') as HTMLButtonElement;
			const username = usernameInput.value.trim();
			const email = emailInput?.value.trim() || "";

			if (!username) {
				errEl.textContent = "Please enter a username.";
				usernameInput.focus();
				return;
			}

			errEl.textContent = "";
			btn.disabled = true;
			btn.innerHTML = '<span class="spinner"></span> Creating passkey...';

			try {
				const startRes = await fetch(`${ENCRYPTID_URL}/api/register/start`, {
					method: "POST",
					headers: { "Content-Type": "application/json" },
					body: JSON.stringify({ username, displayName: username }),
				});
				if (!startRes.ok) throw new Error("Failed to start registration");
				const { options: serverOptions, userId } = await startRes.json();

				const credential = (await navigator.credentials.create({
					publicKey: {
						challenge: new Uint8Array(base64urlToBuffer(serverOptions.challenge)),
						rp: { id: serverOptions.rp?.id || "rspace.online", name: serverOptions.rp?.name || "EncryptID" },
						user: { id: new Uint8Array(base64urlToBuffer(serverOptions.user.id)), name: username, displayName: username },
						pubKeyCredParams: serverOptions.pubKeyCredParams || [
							{ alg: -7, type: "public-key" as const },
							{ alg: -257, type: "public-key" as const },
						],
						authenticatorSelection: { residentKey: "required", requireResidentKey: true, userVerification: "required" },
						attestation: "none",
						timeout: 60000,
					},
				})) as PublicKeyCredential;
				if (!credential) throw new Error("Failed to create credential");

				const response = credential.response as AuthenticatorAttestationResponse;
				const publicKey = response.getPublicKey?.();

				const completeRes = await fetch(`${ENCRYPTID_URL}/api/register/complete`, {
					method: "POST",
					headers: { "Content-Type": "application/json" },
					body: JSON.stringify({
						challenge: serverOptions.challenge,
						credential: {
							credentialId: bufferToBase64url(credential.rawId),
							publicKey: publicKey ? bufferToBase64url(publicKey) : "",
							transports: response.getTransports?.() || [],
						},
						userId,
						username,
						...(email ? { email } : {}),
					}),
				});
				const data = await completeRes.json().catch(() => null);
				if (!data || !completeRes.ok || !data.success) throw new Error(data?.error || "Registration failed");

				storeSession(data.token, username, data.did || "");
				close();
				this.#render();
		this.dispatchEvent(new CustomEvent("auth-change", { bubbles: true, composed: true, detail: { reason: "signin" } }));
				callbacks?.onSuccess?.();
				// Show post-signup prompt recommending second device before redirecting
				this.#showPostSignupPrompt(data.token, username);
			} catch (err: any) {
				btn.disabled = false;
				btn.innerHTML = "🔐 Create Passkey";
				errEl.textContent = err.name === "NotAllowedError" ? "Passkey creation was cancelled." : err.message || "Registration failed.";
			}
		};

		const attachListeners = () => {
			overlay.querySelector('[data-action="cancel"]')?.addEventListener("click", () => {
				close();
				callbacks?.onCancel?.();
			});
			overlay.querySelector('[data-action="signin"]')?.addEventListener("click", handleSignIn);
			overlay.querySelector('[data-action="send-magic-link"]')?.addEventListener("click", handleMagicLink);
			overlay.querySelector('[data-action="register"]')?.addEventListener("click", handleRegister);
			overlay.querySelector('[data-action="switch-register"]')?.addEventListener("click", () => {
				mode = "register";
				render();
				setTimeout(() => (overlay.querySelector("#auth-username") as HTMLInputElement)?.focus(), 50);
			});
			overlay.querySelector('[data-action="switch-signin"]')?.addEventListener("click", () => {
				mode = "signin";
				showManualInput = false;
				render();
			});
			overlay.querySelector('[data-action="show-manual"]')?.addEventListener("click", () => {
				showManualInput = true;
				render();
				setTimeout(() => (overlay.querySelector("#auth-signin-username") as HTMLInputElement)?.focus(), 50);
			});
			// Account picker buttons — click triggers sign-in with that username
			overlay.querySelectorAll("[data-pick-username]").forEach(btn => {
				btn.addEventListener("click", () => {
					const picked = (btn as HTMLElement).dataset.pickUsername || "";
					if (!picked) return;
					// Populate a hidden username for handleSignIn, then trigger it
					const input = overlay.querySelector("#auth-signin-username") as HTMLInputElement | null;
					if (input) {
						input.value = picked;
					} else {
						// No input in DOM (picker mode) — create a temp hidden one for handleSignIn
						const tmp = document.createElement("input");
						tmp.id = "auth-signin-username";
						tmp.type = "hidden";
						tmp.value = picked;
						overlay.querySelector(".auth-modal")?.appendChild(tmp);
					}
					handleSignIn();
				});
			});
			overlay.querySelector("#auth-username")?.addEventListener("keydown", (e) => {
				if ((e as KeyboardEvent).key === "Enter") {
					// Tab to email field if empty, otherwise register
					const emailInput = overlay.querySelector("#auth-email") as HTMLInputElement | null;
					if (emailInput && !emailInput.value.trim()) { emailInput.focus(); } else { handleRegister(); }
				}
			});
			overlay.querySelector("#auth-email")?.addEventListener("keydown", (e) => {
				if ((e as KeyboardEvent).key === "Enter") handleRegister();
			});
			overlay.querySelector("#auth-signin-username")?.addEventListener("keydown", (e) => {
				if ((e as KeyboardEvent).key === "Enter") handleSignIn();
			});
			overlay.addEventListener("click", (e) => {
				if (e.target === overlay) {
					close();
					callbacks?.onCancel?.();
				}
			});
		};

		document.body.appendChild(overlay);
		render();

		// If switching persona, auto-trigger sign-in immediately
		if (usernameHint) {
			handleSignIn();
		} else {
			// Focus the username input
			setTimeout(() => (overlay.querySelector("#auth-signin-username") as HTMLInputElement)?.focus(), 50);
		}
	}

	// ── Post-signup onboarding prompt ──

	#showPostSignupPrompt(token: string, username: string): void {
		const overlay = document.createElement("div");
		overlay.className = "rstack-auth-overlay";

		const goToSpace = () => {
			overlay.remove();
			autoResolveSpace(token, username);
		};

		let step: "welcome" | "linking" | "done" = "welcome";
		let linkUrl = "";

		const render = () => {
			if (step === "welcome") {
				const qrHint = /Mobi|Android|iPhone|iPad/i.test(navigator.userAgent)
					? "You can link your laptop or another phone."
					: "Grab your phone or tablet and scan the code.";
				overlay.innerHTML = `
					<style>${MODAL_STYLES}${ONBOARDING_STYLES}</style>
					<div class="auth-modal onboarding-modal">
						<h2>Welcome to rSpace!</h2>
						<p style="margin-bottom:0.75rem">Your account <strong>${username}</strong> is ready.</p>

						<div class="onboarding-card onboarding-card--primary">
							<div class="onboarding-card-icon">📱</div>
							<div class="onboarding-card-content">
								<div class="onboarding-card-title">Link a Second Device</div>
								<div class="onboarding-card-desc">
									Add a passkey on your phone or tablet so you can sign in from anywhere.
									This also serves as a backup if you lose access to this device.
								</div>
								<div class="onboarding-card-hint">${qrHint}</div>
							</div>
						</div>
						<button class="btn btn--primary" style="width:100%;margin-top:0.75rem" data-action="link-device">📱 Link Another Device</button>

						<button class="btn btn--outline" style="width:100%;margin-top:0.5rem" data-action="skip">Continue to my space</button>
						<p class="onboarding-later-hint">You can always link devices later from My Account.</p>
					</div>`;
			} else if (step === "linking") {
				const qrSrc = `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(linkUrl)}&format=png&margin=8`;
				overlay.innerHTML = `
					<style>${MODAL_STYLES}${ONBOARDING_STYLES}</style>
					<div class="auth-modal onboarding-modal">
						<h2>Link Another Device</h2>
						<p style="margin-bottom:0.5rem">Scan this QR code on your phone or tablet to add a passkey.</p>
						<div class="qr-container">
							<img src="${qrSrc}" width="200" height="200" alt="QR Code" style="border-radius:12px;background:#fff;padding:4px" />
						</div>
						<div class="link-copy-row">
							<input class="input" id="onboarding-link-url" type="text" readonly value="${linkUrl}" style="font-size:0.8rem;margin-bottom:0" />
							<button class="btn btn--secondary btn--small" data-action="copy-link" style="flex:none;white-space:nowrap">Copy</button>
						</div>
						<p class="onboarding-expire-hint">Link expires in 10 minutes</p>
						<div class="actions" style="margin-top:1rem">
							<button class="btn btn--secondary" data-action="back">Back</button>
							<button class="btn btn--primary" data-action="done">Continue to my space</button>
						</div>
					</div>`;
			} else {
				overlay.innerHTML = `
					<style>${MODAL_STYLES}${ONBOARDING_STYLES}</style>
					<div class="auth-modal onboarding-modal">
						<div style="font-size:2.5rem;margin-bottom:0.5rem">✅</div>
						<h2>Device Linked!</h2>
						<p>You can now sign in from your other device too.</p>
						<button class="btn btn--primary" style="width:100%;margin-top:1rem" data-action="done">Continue to my space</button>
					</div>`;
			}
			attachListeners();
		};

		const attachListeners = () => {
			overlay.querySelector('[data-action="skip"]')?.addEventListener("click", goToSpace);
			overlay.querySelector('[data-action="done"]')?.addEventListener("click", goToSpace);
			overlay.querySelector('[data-action="back"]')?.addEventListener("click", () => {
				step = "welcome";
				render();
			});
			overlay.querySelector('[data-action="copy-link"]')?.addEventListener("click", () => {
				const input = overlay.querySelector("#onboarding-link-url") as HTMLInputElement;
				navigator.clipboard.writeText(input.value).catch(() => {});
				const btn = overlay.querySelector('[data-action="copy-link"]') as HTMLButtonElement;
				btn.textContent = "Copied!";
				setTimeout(() => { btn.textContent = "Copy"; }, 2000);
			});
			overlay.querySelector('[data-action="link-device"]')?.addEventListener("click", async () => {
				const btn = overlay.querySelector('[data-action="link-device"]') as HTMLButtonElement;
				btn.disabled = true;
				btn.innerHTML = '<span class="spinner"></span> Generating link...';
				try {
					const res = await fetch(`${ENCRYPTID_URL}/api/device-link/start`, {
						method: "POST",
						headers: { Authorization: `Bearer ${getAccessToken()}` },
					});
					const data = await res.json();
					if (!res.ok || data.error) throw new Error(data.error || "Failed to generate link");
					linkUrl = data.linkUrl;
					step = "linking";
					render();
				} catch (e: any) {
					btn.disabled = false;
					btn.innerHTML = "📱 Link Another Device";
					const errEl = document.createElement("div");
					errEl.className = "error";
					errEl.textContent = e.message;
					btn.parentElement?.appendChild(errEl);
				}
			});
		};

		document.body.appendChild(overlay);
		render();
	}

	// ── Account modal (consolidated) ──

	showAccountModal(options?: { openSection?: string }): void {
		if (document.querySelector(".rstack-account-overlay")) return;
		const overlay = document.createElement("div");
		overlay.className = "rstack-account-overlay";

		const session = getSession();
		if (!session) return;

		let openSection: string | null = options?.openSection ?? null;

		// Account completion status
		let acctStatus: { email: boolean; emailAddress?: string | null; multiDevice: boolean; socialRecovery: boolean; guardianCount: number; acceptedGuardianCount?: number; credentialCount: number; lastDrillAt?: number | null } | null = null;

		// Lazy-loaded data
		let guardians: { id: string; name: string; email?: string; status: string }[] = [];
		let guardiansThreshold = 2;
		let guardiansLoaded = false;
		let guardiansLoading = false;

		// Drill state
		let activeDrillId: string | null = null;
		let drillPollingTimer: ReturnType<typeof setInterval> | null = null;
		let drillStatus: { approvals: Array<{ guardianId: string; approved: boolean }>; approvalCount: number; threshold: number; status: string } | null = null;
		let showWalkthrough = false;
		let walkthroughStep = 0;

		let devices: { credentialId: string; label: string | null; createdAt: number; lastUsed?: number; transports?: string[] }[] = [];
		let devicesLoaded = false;
		let devicesLoading = false;

		// Connections data
		let connectionsLoaded = false;
		let connectionsLoading = false;
		let connectionsStatus: Record<string, { connected: boolean; email?: string; workspaceName?: string; teamName?: string; connectedAt?: number; services?: string[]; calendarSources?: number; type?: string; note?: string }> = {};
		let sharingConfig: Record<string, { spaces: string[] }> = {};
		let userSpaces: Array<{ slug: string; name: string }> = [];

		let emailStep: "input" | "verify" = "input";
		let emailAddr = "";

		const close = () => {
			if (drillPollingTimer) { clearInterval(drillPollingTimer); drillPollingTimer = null; }
			overlay.remove();
		};

		// Load account completion status
		const loadStatus = async () => {
			try {
				const res = await fetch(`${ENCRYPTID_URL}/api/account/status`, {
					headers: { Authorization: `Bearer ${getAccessToken()}` },
				});
				if (res.ok) {
					acctStatus = await res.json();
					render();
				}
			} catch { /* offline */ }
		};
		loadStatus();

		const statusDot = (done: boolean | null) => {
			if (done === null) return ''; // still loading
			return done
				? '<span class="status-dot done" title="Complete"></span>'
				: '<span class="status-dot pending" title="Not yet set up"></span>';
		};

		const render = () => {
			const backupEnabled = isEncryptedBackupEnabled();
			const currentTheme = localStorage.getItem("canvas-theme") || "dark";
			const isDark = currentTheme === "dark";

			const emailDone = acctStatus ? acctStatus.email : null;
			const deviceDone = acctStatus ? acctStatus.multiDevice : null;
			const recoveryDone = acctStatus ? acctStatus.socialRecovery : null;

			overlay.innerHTML = `
				<style>${MODAL_STYLES}${SETTINGS_STYLES}${ACCOUNT_MODAL_STYLES}</style>
				<div class="account-modal">
					<button class="close-btn" data-action="close">&times;</button>
					<h2>My Account</h2>

					${renderEmailSection()}
					${renderDeviceSection()}
					${renderRecoverySection()}

					<div class="account-section account-section--inline${!backupEnabled ? " section--warning" : ""}">
						<div class="account-section-header">
							<span>${statusDot(backupEnabled)} 🔒 Data Storage</span>
							<label class="toggle-switch">
								<input type="checkbox" id="acct-backup-toggle" ${backupEnabled ? "checked" : ""} />
								<span class="toggle-slider"></span>
							</label>
						</div>
						<div class="toggle-hint" id="backup-hint">${backupEnabled
							? "Save to encrypted server"
							: '<span style="color:#f87171">Local only — you are responsible for your own data</span>'
						}</div>
					</div>

					${renderShortcutsSection()}
					${renderConnectionsSection()}

						<div class="error" id="acct-error"></div>
				</div>
				${showWalkthrough ? renderWalkthrough() : ""}
			`;
			attachListeners();
		};

		const WALKTHROUGH_STEPS = [
			{ icon: "🔒", iconClass: "red", title: "Account Locked", body: "Imagine you lost access to all your devices. Your passkeys are gone, and you can't sign in." },
			{ icon: "📡", iconClass: "amber", title: "Guardians Notified", body: "You request account recovery. Your trusted guardians each receive a notification asking them to verify your identity." },
			{ icon: "✅", iconClass: "amber", title: "Identity Verified", body: "Each guardian confirms it's really you — through a phone call, video chat, or meeting in person. They click 'Approve' on their end." },
			{ icon: "🔑", iconClass: "green", title: "Key Assembled", body: "When 2 of your 3 guardians approve, their trust combines to unlock your recovery. No single guardian can do this alone." },
			{ icon: "🛡️", iconClass: "green", title: "Account Recovered", body: "You register a new passkey on your new device and regain full access. Your data and identity are restored." },
		];

		const renderWalkthrough = () => {
			const step = WALKTHROUGH_STEPS[walkthroughStep];
			const dots = WALKTHROUGH_STEPS.map((_, i) =>
				`<div class="walkthrough-dot${i < walkthroughStep ? " done" : i === walkthroughStep ? " active" : ""}"></div>`
			).join("");
			const isLast = walkthroughStep >= WALKTHROUGH_STEPS.length - 1;
			return `
				<div class="walkthrough-overlay">
					<div class="walkthrough-card">
						<div class="walkthrough-icon ${step.iconClass}">${step.icon}</div>
						<div class="walkthrough-title">${step.title}</div>
						<div class="walkthrough-body">${step.body}</div>
						<div class="walkthrough-progress">${dots}</div>
						<div class="walkthrough-nav">
							<button class="walkthrough-btn walkthrough-btn--skip" data-action="walkthrough-skip">${isLast ? "Close" : "Skip"}</button>
							${!isLast ? `<button class="walkthrough-btn walkthrough-btn--next" data-action="walkthrough-next">Next</button>` : `<button class="walkthrough-btn walkthrough-btn--next" data-action="walkthrough-skip">Got It</button>`}
						</div>
					</div>
				</div>`;
		};

		const renderEmailSection = () => {
			const isOpen = openSection === "email";
			const done = acctStatus ? acctStatus.email : null;
			let body = "";
			if (isOpen) {
				if (emailStep === "input") {
					body = `
						<div class="account-section-body">
							<p style="color:var(--rs-text-secondary);font-size:0.85rem;margin:0 0 12px">Link an email for notifications and account recovery.</p>
							<input class="input" id="acct-email" type="email" placeholder="you@example.com" />
							<div class="actions">
								<button class="btn btn--primary" data-action="send-code">Send Verification Code</button>
							</div>
							<div class="error" id="email-error"></div>
						</div>`;
				} else {
					body = `
						<div class="account-section-body">
							<p style="color:var(--rs-text-secondary);font-size:0.85rem;margin:0 0 12px">Enter the 6-digit code sent to <strong>${emailAddr.replace(/</g, "&lt;")}</strong></p>
							<input class="input" id="acct-code" type="text" placeholder="000000" maxlength="6" inputmode="numeric" />
							<div class="actions">
								<button class="btn btn--secondary" data-action="email-back">Back</button>
								<button class="btn btn--primary" data-action="verify-email">Verify</button>
							</div>
							<div class="error" id="email-error"></div>
						</div>`;
				}
			}
			const emailDisplay = !isOpen && done && acctStatus?.emailAddress
				? `<span style="color:var(--rs-text-secondary);font-size:0.85rem;margin-left:8px">${acctStatus.emailAddress}</span>`
				: "";
			return `
				<div class="account-section${isOpen ? " open" : ""}${done === false ? " section--warning" : ""}">
					<div class="account-section-header" data-section="email">
						<span>${statusDot(done)} ✉️ Email${emailDisplay}</span>
						<span class="section-arrow">${isOpen ? "▾" : "▸"}</span>
					</div>
					${body}
				</div>`;
		};

		const renderDeviceSection = () => {
			const isOpen = openSection === "device";
			const done = acctStatus ? acctStatus.multiDevice : null;
			let body = "";
			if (isOpen) {
				if (devicesLoading) {
					body = `<div class="account-section-body"><div style="text-align:center;padding:1rem;color:var(--rs-text-secondary)"><span class="spinner"></span> Loading devices...</div></div>`;
				} else {
					const fmtDate = (ts?: number) => ts ? new Date(ts).toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" }) : "Never";
					const transportBadge = (t: string) => `<span style="display:inline-block;background:var(--rs-bg-secondary);border-radius:4px;padding:1px 5px;font-size:0.7rem;color:var(--rs-text-muted)">${t}</span>`;

					const deviceListHTML = devices.length > 0
						? `<div class="contact-list">${devices.map(d => `
							<div class="contact-item" style="flex-wrap:wrap;gap:6px">
								<div style="display:flex;align-items:center;gap:8px;min-width:0;flex:1">
									<span style="font-size:1.1rem">🔑</span>
									<div style="display:flex;flex-direction:column;gap:2px;min-width:0;flex:1">
										<span style="font-weight:500">${(d.label || "Unnamed device").replace(/</g, "&lt;")}</span>
										<span style="font-size:0.7rem;color:var(--rs-text-muted)">Added ${fmtDate(d.createdAt)} · Last used ${fmtDate(d.lastUsed)}</span>
										${d.transports?.length ? `<div style="display:flex;gap:3px;flex-wrap:wrap">${d.transports.map(transportBadge).join("")}</div>` : ""}
									</div>
								</div>
								<div style="display:flex;gap:4px;align-items:center">
									<button class="btn btn--small" data-rename-credential="${d.credentialId}" title="Rename">✏️</button>
									<button class="btn btn--small btn--danger" data-remove-credential="${d.credentialId}" title="Remove"${devices.length <= 1 ? " disabled" : ""}>&times;</button>
								</div>
							</div>
						`).join("")}</div>` : "";

					body = `
						<div class="account-section-body">
							<p style="color:var(--rs-text-secondary);font-size:0.85rem;margin:0 0 12px">Register an additional passkey for backup access.</p>
							${deviceListHTML}
							<div class="actions actions--stack" style="margin-top:8px">
								<button class="btn btn--primary" data-action="register-device">🔑 Register Passkey on This Device</button>
							</div>
							<div class="error" id="device-error"></div>
							<div class="info-text">Each device you register can independently sign in to your account.</div>
						</div>`;
				}
			}
			return `
				<div class="account-section${isOpen ? " open" : ""}${done === false ? " section--warning" : ""}">
					<div class="account-section-header" data-section="device">
						<span>${statusDot(done)} 📱 Connect Another Device</span>
						<span class="section-arrow">${isOpen ? "▾" : "▸"}</span>
					</div>
					${body}
				</div>`;
		};

		const renderGuardianPiecesSVG = (slots: Array<{ status: "empty" | "pending" | "accepted" }>, assembled: boolean) => {
			const pieceClass = (s: string) => s === "accepted" ? "piece-accepted glow" : s === "pending" ? "piece-pending" : "piece-empty";
			return `<div class="guardian-viz">
				<svg width="200" height="60" viewBox="0 0 200 60">
					<defs>
						<linearGradient id="keyGrad" x1="0%" y1="0%" x2="100%" y2="0%">
							<stop offset="0%" style="stop-color:#34d399" />
							<stop offset="100%" style="stop-color:#06b6d4" />
						</linearGradient>
					</defs>
					${/* Piece 1 (left) */""}
					<g transform="translate(15,10)">
						<path class="${pieceClass(slots[0]?.status || "empty")}" d="M0,0 h25 c0,5 8,5 8,0 h7 v40 h-40 z" />
						<text x="12" y="25" fill="white" font-size="10" text-anchor="middle" font-weight="600">1</text>
					</g>
					${/* Piece 2 (center) */""}
					<g transform="translate(70,10)">
						<path class="${pieceClass(slots[1]?.status || "empty")}" d="M0,0 h25 c0,5 8,5 8,0 h7 v40 h-7 c0,-5 -8,-5 -8,0 h-25 v-40 z" />
						<text x="16" y="25" fill="white" font-size="10" text-anchor="middle" font-weight="600">2</text>
					</g>
					${/* Piece 3 (right) */""}
					<g transform="translate(125,10)">
						<path class="${pieceClass(slots[2]?.status || "empty")}" d="M0,0 h40 v40 h-40 v0 c0,-5 -8,-5 -8,0 v0 z" />
						<text x="16" y="25" fill="white" font-size="10" text-anchor="middle" font-weight="600">3</text>
					</g>
					${/* Key icon (right side) */""}
					<g transform="translate(175,15)">
						<path class="${assembled ? "key-assembled" : "key-outline"}" d="M0,15 a10,10 0 1 1 12,0 l0,3 h8 v5 h-3 v3 h3 v5 h-8 l0,0 h-12 z" />
					</g>
				</svg>
			</div>`;
		};

		const renderRecoverySection = () => {
			const isOpen = openSection === "recovery";
			let body = "";
			if (isOpen) {
				if (guardiansLoading) {
					body = `<div class="account-section-body"><div style="text-align:center;padding:1rem;color:var(--rs-text-secondary)"><span class="spinner"></span> Loading guardians...</div></div>`;
				} else {
					const acceptedCount = guardians.filter(g => g.status === "accepted").length;
					const recoveryActive = acceptedCount >= 2;

					// Build puzzle piece slots
					const slots: Array<{ status: "empty" | "pending" | "accepted" }> = [];
					for (let i = 0; i < 3; i++) {
						if (i < guardians.length) {
							slots.push({ status: guardians[i].status === "accepted" ? "accepted" : "pending" });
						} else {
							slots.push({ status: "empty" });
						}
					}

					const guardiansHTML = guardians.length > 0
						? `<div class="contact-list">${guardians.map(g => `
							<div class="contact-item">
								<div style="display:flex;align-items:center;gap:8px;min-width:0;flex:1">
									<span class="guardian-piece">🧩</span>
									<div style="display:flex;flex-direction:column;gap:2px;min-width:0;flex:1">
										<span>${g.name.replace(/</g, "&lt;")}${g.email ? ` <span style="opacity:0.5;font-size:0.8rem">${g.email.replace(/</g, "&lt;")}</span>` : ""}</span>
										<span style="font-size:0.7rem;color:${g.status === "accepted" ? "#34d399" : "#fbbf24"}">${g.status === "accepted" ? "Accepted" : "Pending invite"}</span>
									</div>
								</div>
								<button class="contact-remove" data-remove-guardian="${g.id}">&times;</button>
							</div>
						`).join("")}</div>` : "";

					// Recovery active banner
					const activeBanner = recoveryActive ? `
						<div class="recovery-active-banner">
							<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 1l2.5 5 5.5.8-4 3.9.9 5.3L8 13.3l-4.9 2.7.9-5.3-4-3.9 5.5-.8z" fill="#34d399"/></svg>
							Recovery active — ${guardiansThreshold} of ${guardians.length} guardians can recover your account
						</div>` : "";

					// Walkthrough link (show after 2+ guardians)
					const walkthroughLink = guardians.length >= 2 ? `
						<div style="margin-top:8px;text-align:center">
							<a style="color:#06b6d4;font-size:0.78rem;cursor:pointer;text-decoration:none" data-action="preview-recovery">Preview how recovery works</a>
						</div>` : "";

					// Drill section (only when recovery active)
					let drillHTML = "";
					if (recoveryActive) {
						const lastDrill = acctStatus?.lastDrillAt;
						const lastDrillStr = lastDrill ? new Date(lastDrill).toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" }) : null;

						if (activeDrillId && drillStatus) {
							// Live drill view
							const drillSlots: Array<{ status: "empty" | "pending" | "accepted" }> = [];
							for (const g of guardians) {
								if (g.status !== "accepted") continue;
								const approval = drillStatus.approvals.find(a => a.guardianId === g.id);
								drillSlots.push({ status: approval?.approved ? "accepted" : "pending" });
							}
							while (drillSlots.length < 3) drillSlots.push({ status: "empty" });
							const drillComplete = drillStatus.status === "approved" || drillStatus.status === "completed";

							drillHTML = `<div class="drill-section">
								<div class="drill-live">
									<div class="drill-live-title">${drillComplete ? "Drill Complete!" : "Recovery Drill in Progress..."}</div>
									${renderGuardianPiecesSVG(drillSlots, drillComplete)}
									<div style="text-align:center;font-size:0.78rem;color:var(--rs-text-secondary)">
										${drillStatus.approvalCount}/${drillStatus.threshold} guardians approved
									</div>
									${drillComplete ? `<div class="drill-success">Your recovery setup is working!</div>` : `<div style="text-align:center;font-size:0.72rem;color:var(--rs-text-muted);margin-top:4px">Checking every 5 seconds...</div>`}
								</div>
							</div>`;
						} else {
							drillHTML = `<div class="drill-section">
								<button class="drill-btn" data-action="start-drill">🧪 Run Recovery Drill</button>
								${lastDrillStr ? `<div class="drill-timestamp">Last drill: ${lastDrillStr}</div>` : `<div class="drill-timestamp">No drill run yet — test your setup!</div>`}
							</div>`;
						}
					}

					const infoHTML = !recoveryActive
						? `<div class="info-text">Add at least 2 trusted guardians to enable social recovery. Threshold: ${guardiansThreshold} of ${Math.max(guardians.length, 2)} needed to recover.</div>`
						: `<div class="info-text" style="color:var(--rs-text-muted)">Recovery guardians can also help override account freezes and flow restrictions in emergencies.</div>`;

					body = `
						<div class="account-section-body">
							<p style="color:var(--rs-text-secondary);font-size:0.85rem;margin:0 0 12px">Choose trusted contacts who can help recover your account.</p>
							${activeBanner}
							${renderGuardianPiecesSVG(slots, recoveryActive)}
							${guardians.length < 3 ? `<div class="input-row">
								<input class="input input--inline" id="acct-guardian-name" type="text" placeholder="Guardian name" />
								<input class="input input--inline" id="acct-guardian-email" type="email" placeholder="Email (optional)" />
								<button class="btn btn--small btn--primary" data-action="add-guardian">Add</button>
							</div>` : ""}
							${guardiansHTML}
							${infoHTML}
							${walkthroughLink}
							${drillHTML}
							<div class="error" id="recovery-error"></div>
						</div>`;
				}
			}
			const done = acctStatus ? acctStatus.socialRecovery : null;
			const urgentClass = done === false ? " recovery-urgent" : "";
			return `
				<div class="account-section${isOpen ? " open" : ""}${done === false ? " section--warning" : ""}">
					<div class="account-section-header" data-section="recovery">
						<span>${done === null ? "" : done ? '<span class="status-dot done" title="Complete"></span>' : `<span class="status-dot pending${urgentClass}" title="Not yet set up"></span>`} 🛡️ Social Recovery</span>
						<span class="section-arrow">${isOpen ? "▾" : "▸"}</span>
					</div>
					${body}
				</div>`;
		};



		const renderShortcutsSection = () => {
			const isOpen = openSection === "shortcuts";
			let body = "";
			if (isOpen) {
				const shortcuts = getShortcuts();
				const modules: Array<{ id: string; name: string; icon: string; hidden?: boolean }> = (window as any).__rspaceAllModules || (window as any).__rspaceModuleList || [];
				const slotRows = Array.from({ length: 9 }, (_, i) => {
					const slot = String(i + 1);
					const current = shortcuts[slot] || "";
					const options = modules
						.filter(m => !m.hidden)
						.map(m => `<option value="${m.id}"${m.id === current ? " selected" : ""}>${m.icon} ${m.name}</option>`)
						.join("");
					return `
						<div class="shortcut-slot">
							<span class="slot-num">${slot}</span>
							<select class="slot-select" data-slot="${slot}">
								<option value="">None</option>
								${options}
							</select>
						</div>`;
				}).join("");
				body = `
					<div class="account-section-body">
						<div class="shortcut-grid">${slotRows}</div>
						<p class="shortcut-hint">Ctrl+1–9 (PWA) · Alt+1–9 (browser) · Swipe header on mobile</p>
					</div>`;
			}
			return `
				<div class="account-section${isOpen ? " open" : ""}">
					<div class="account-section-header" data-section="shortcuts">
						<span>⌨️ rApp Shortcuts</span>
						<span class="section-arrow">${isOpen ? "▾" : "▸"}</span>
					</div>
					${body}
				</div>`;
		};

		const CONN_PLATFORMS: Array<{ id: string; name: string; icon: string; oauthKey?: string; comingSoon?: boolean; fileBased?: boolean; description?: string }> = [
			{ id: "google", name: "Google", icon: "G", oauthKey: "google", description: "Docs, Drive, Calendar" },
			{ id: "notion", name: "Notion", icon: "N", oauthKey: "notion", description: "Pages import/export" },
			{ id: "clickup", name: "ClickUp", icon: "\u2713", oauthKey: "clickup", description: "Task sync" },
			{ id: "obsidian", name: "Obsidian", icon: "\uD83D\uDCDD", oauthKey: "obsidian", fileBased: true, description: "Upload vault ZIP in rDocs or rNotes" },
			{ id: "logseq", name: "Logseq", icon: "\uD83D\uDCD3", oauthKey: "logseq", fileBased: true, description: "Upload vault ZIP in rDocs or rNotes" },
			{ id: "telegram", name: "Telegram", icon: "\u2708", comingSoon: true },
			{ id: "discord", name: "Discord", icon: "\uD83C\uDFAE", comingSoon: true },
			{ id: "github", name: "GitHub", icon: "\uD83D\uDC19", comingSoon: true },
			{ id: "slack", name: "Slack", icon: "#", comingSoon: true },
			{ id: "twitter", name: "X / Twitter", icon: "X", comingSoon: true },
			{ id: "bluesky", name: "Bluesky", icon: "\uD83E\uDD8B", comingSoon: true },
			{ id: "linear", name: "Linear", icon: "Lin", comingSoon: true },
		];

		const renderConnectionsSection = () => {
			const isOpen = openSection === "connections";
			const oauthConnected = Object.entries(connectionsStatus).filter(([k, s]) => s.connected && s.type !== 'file').length;
			const connectedCount = oauthConnected;
			const anyConnected = connectedCount > 0;
			let body = "";
			if (isOpen) {
				if (connectionsLoading) {
					body = `<div class="account-section-body"><div style="text-align:center;padding:1rem;color:var(--rs-text-secondary)"><span class="spinner"></span> Loading connections...</div></div>`;
				} else {
					const username = getUsername() || "";
					let cards = "";
					for (const p of CONN_PLATFORMS) {
						const info = p.oauthKey ? connectionsStatus[p.oauthKey] : undefined;
						const connected = info?.connected ?? false;
						const cardClass = p.comingSoon ? "conn-card conn-card--soon" : p.fileBased ? "conn-card conn-card--active" : connected ? "conn-card conn-card--active" : "conn-card";

						let badge = "";
						let meta = "";
						let action = "";
						let sharing = "";

						if (p.comingSoon) {
							badge = `<span class="conn-badge conn-badge--soon">Coming Soon</span>`;
						} else if (p.fileBased) {
							badge = `<span class="conn-badge conn-badge--connected">Available</span>`;
							meta = `<div class="conn-meta">${p.description || 'File-based import'}</div>`;
						} else if (connected) {
							badge = `<span class="conn-badge conn-badge--connected">Connected</span>`;
							// Build meta line with account info + services
							const metaParts: string[] = [];
							if (info?.email) metaParts.push(info.email);
							else if (info?.workspaceName) metaParts.push(info.workspaceName);
							else if (info?.teamName) metaParts.push(info.teamName);
							if (info?.services?.length) metaParts.push(info.services.join(', '));
							const calSrc = info?.calendarSources ?? 0;
							if (calSrc > 0) metaParts.push(`${calSrc} calendar${calSrc > 1 ? 's' : ''} synced`);
							if (metaParts.length > 0) meta = `<div class="conn-meta">${metaParts.join(' · ')}</div>`;
							action = `<button class="conn-btn conn-btn--disconnect" data-provider="${p.oauthKey}">Disconnect</button>`;

							// Sharing: which spaces to share data into
							const sharedSpaces = sharingConfig[p.oauthKey!]?.spaces || [];
							if (userSpaces.length > 0) {
								const spaceChecks = userSpaces.map(s => {
									const checked = sharedSpaces.includes(s.slug) ? "checked" : "";
									return `<label class="conn-share-opt"><input type="checkbox" class="conn-share-cb" data-provider="${p.oauthKey}" data-space="${s.slug}" ${checked} />${s.name}</label>`;
								}).join("");
								sharing = `<div class="conn-share-section"><div class="conn-share-label">Share to:</div>${spaceChecks}</div>`;
							}
						} else {
							badge = `<span class="conn-badge conn-badge--disconnected">Not connected</span>`;
							if (p.description) meta = `<div class="conn-meta" style="opacity:0.6">${p.description}</div>`;
							action = `<button class="conn-btn conn-btn--connect" data-provider="${p.oauthKey}" data-space="${username}">Connect</button>`;
						}

						cards += `
							<div class="${cardClass}" data-platform="${p.id}">
								<div class="conn-card-header">
									<div class="conn-card-icon">${p.icon}</div>
									<div class="conn-card-info">
										<div class="conn-card-name">${p.name}</div>
										${badge}
										${meta}
									</div>
									${action}
								</div>
								${sharing}
							</div>`;
					}

					body = `<div class="account-section-body"><div class="conn-list">${cards}</div></div>`;
				}
			}
			return `
				<div class="account-section${isOpen ? " open" : ""}">
					<div class="account-section-header" data-section="connections">
						<span>${anyConnected ? '<span class="status-dot done"></span>' : '<span class="status-dot pending"></span>'} 🔗 Connections${anyConnected ? ` <span style="font-size:0.75rem;color:var(--rs-text-muted)">(${connectedCount})</span>` : ""}</span>
						<span class="section-arrow">${isOpen ? "▾" : "▸"}</span>
					</div>
					${body}
				</div>`;
		};

		const loadConnections = async () => {
			if (connectionsLoaded || connectionsLoading) return;
			connectionsLoading = true;
			render();
			const username = getUsername();
			if (!username) { connectionsLoading = false; return; }
			try {
				const token = getAccessToken();
				const headers: Record<string, string> = token ? { Authorization: `Bearer ${token}` } : {};
				const [statusRes, sharingRes, spacesRes] = await Promise.all([
					fetch(`/api/oauth/status?space=${encodeURIComponent(username)}`, { headers }),
					fetch(`/api/oauth/sharing?space=${encodeURIComponent(username)}`, { headers }),
					fetch("/api/spaces", { headers }),
				]);
				if (statusRes.ok) connectionsStatus = await statusRes.json();
				if (sharingRes.ok) sharingConfig = await sharingRes.json();
				if (spacesRes.ok) {
					const data = await spacesRes.json();
					userSpaces = (data.spaces || [])
						.filter((s: any) => s.role === "owner" || s.role === "admin" || s.role === "member")
						.map((s: any) => ({ slug: s.slug, name: s.name }));
				}
			} catch { /* offline */ }
			connectionsLoaded = true;
			connectionsLoading = false;
			render();
		};

		const loadGuardians = async () => {
			if (guardiansLoaded || guardiansLoading) return;
			guardiansLoading = true;
			render();
			try {
				const res = await fetch(`${ENCRYPTID_URL}/api/guardians`, {
					headers: { Authorization: `Bearer ${getAccessToken()}` },
				});
				if (res.ok) {
					const data = await res.json();
					guardians = data.guardians || [];
					guardiansThreshold = data.threshold || 2;
				}
			} catch { /* offline */ }
			guardiansLoaded = true;
			guardiansLoading = false;
			render();
		};

		const loadDevices = async () => {
			if (devicesLoaded || devicesLoading) return;
			devicesLoading = true;
			render();
			try {
				const res = await fetch(`${ENCRYPTID_URL}/api/user/credentials`, {
					headers: { Authorization: `Bearer ${getAccessToken()}` },
				});
				if (res.ok) {
					const data = await res.json();
					devices = (data.credentials || []).map((c: any) => ({
						credentialId: c.credentialId,
						label: c.label || null,
						createdAt: c.createdAt,
						lastUsed: c.lastUsed,
						transports: c.transports,
					}));
				}
			} catch { /* offline */ }
			devicesLoaded = true;
			devicesLoading = false;
			render();
		};

		const attachListeners = () => {
			overlay.querySelector('[data-action="close"]')?.addEventListener("click", close);
			overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); });

			// Section toggle headers
			overlay.querySelectorAll("[data-section]").forEach(el => {
				el.addEventListener("click", () => {
					const section = (el as HTMLElement).dataset.section!;
					openSection = openSection === section ? null : section;
					if (openSection === "recovery") loadGuardians();
					if (openSection === "device") loadDevices();
					if (openSection === "connections") loadConnections();
					render();
					if (openSection === "email") setTimeout(() => (overlay.querySelector("#acct-email") as HTMLInputElement)?.focus(), 50);
					if (openSection === "recovery") setTimeout(() => (overlay.querySelector("#acct-guardian-name") as HTMLInputElement)?.focus(), 50);
				});
			});

			// Email: send code
			overlay.querySelector('[data-action="send-code"]')?.addEventListener("click", async () => {
				const input = overlay.querySelector("#acct-email") as HTMLInputElement;
				const err = overlay.querySelector("#email-error") as HTMLElement;
				const btn = overlay.querySelector('[data-action="send-code"]') as HTMLButtonElement;
				emailAddr = input.value.trim();
				if (!emailAddr || !emailAddr.includes("@")) { err.textContent = "Enter a valid email address."; input.focus(); return; }
				btn.disabled = true; btn.innerHTML = '<span class="spinner"></span> Sending...';
				try {
					const res = await fetch(`${ENCRYPTID_URL}/api/account/email/start`, {
						method: "POST",
						headers: { "Content-Type": "application/json", Authorization: `Bearer ${getAccessToken()}` },
						body: JSON.stringify({ email: emailAddr }),
					});
					if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || "Failed to send verification code");
					emailStep = "verify"; render();
					setTimeout(() => (overlay.querySelector("#acct-code") as HTMLInputElement)?.focus(), 50);
				} catch (e: any) {
					btn.disabled = false; btn.innerHTML = "Send Verification Code";
					err.textContent = e.message;
				}
			});

			overlay.querySelector('[data-action="email-back"]')?.addEventListener("click", () => { emailStep = "input"; render(); });

			// Email: verify code
			overlay.querySelector('[data-action="verify-email"]')?.addEventListener("click", async () => {
				const input = overlay.querySelector("#acct-code") as HTMLInputElement;
				const err = overlay.querySelector("#email-error") as HTMLElement;
				const btn = overlay.querySelector('[data-action="verify-email"]') as HTMLButtonElement;
				const code = input.value.trim();
				if (!code) { err.textContent = "Enter the verification code."; input.focus(); return; }
				btn.disabled = true; btn.innerHTML = '<span class="spinner"></span> Verifying...';
				try {
					const res = await fetch(`${ENCRYPTID_URL}/api/account/email/verify`, {
						method: "POST",
						headers: { "Content-Type": "application/json", Authorization: `Bearer ${getAccessToken()}` },
						body: JSON.stringify({ email: emailAddr, code }),
					});
					if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || "Verification failed");
					if (acctStatus) { acctStatus.email = true; acctStatus.emailAddress = emailAddr; }
					openSection = null; render();
					this.dispatchEvent(new CustomEvent("identity-action", { bubbles: true, composed: true, detail: { action: "email-added", email: emailAddr } }));
				} catch (e: any) {
					btn.disabled = false; btn.innerHTML = "Verify";
					err.textContent = e.message;
				}
			});

			// Email: enter key
			overlay.querySelector("#acct-email")?.addEventListener("keydown", (e) => {
				if ((e as KeyboardEvent).key === "Enter") (overlay.querySelector('[data-action="send-code"]') as HTMLElement)?.click();
			});
			overlay.querySelector("#acct-code")?.addEventListener("keydown", (e) => {
				if ((e as KeyboardEvent).key === "Enter") (overlay.querySelector('[data-action="verify-email"]') as HTMLElement)?.click();
			});

			// Device: register passkey
			overlay.querySelector('[data-action="register-device"]')?.addEventListener("click", async () => {
				const err = overlay.querySelector("#device-error") as HTMLElement;
				const btn = overlay.querySelector('[data-action="register-device"]') as HTMLButtonElement;
				err.textContent = "";
				btn.disabled = true; btn.innerHTML = '<span class="spinner"></span> Registering...';
				try {
					const startRes = await fetch(`${ENCRYPTID_URL}/api/account/device/start`, {
						method: "POST",
						headers: { "Content-Type": "application/json", Authorization: `Bearer ${getAccessToken()}` },
					});
					if (!startRes.ok) throw new Error((await startRes.json().catch(() => ({}))).error || "Failed to start device registration");
					const { options: serverOptions, userId } = await startRes.json();
					const username = session.claims.username || "";

					const credential = (await navigator.credentials.create({
						publicKey: {
							challenge: new Uint8Array(base64urlToBuffer(serverOptions.challenge)),
							rp: { id: serverOptions.rp?.id || "rspace.online", name: serverOptions.rp?.name || "EncryptID" },
							user: {
								id: new Uint8Array(base64urlToBuffer(serverOptions.user?.id || userId)),
								name: username || session.claims.sub,
								displayName: username || session.claims.sub,
							},
							pubKeyCredParams: serverOptions.pubKeyCredParams || [
								{ alg: -7, type: "public-key" as const },
								{ alg: -257, type: "public-key" as const },
							],
							authenticatorSelection: { residentKey: "required", requireResidentKey: true, userVerification: "required" },
							attestation: "none",
							timeout: 60000,
						},
					})) as PublicKeyCredential;
					if (!credential) throw new Error("Passkey creation failed");

					const response = credential.response as AuthenticatorAttestationResponse;
					const publicKey = response.getPublicKey?.();

					const completeRes = await fetch(`${ENCRYPTID_URL}/api/account/device/complete`, {
						method: "POST",
						headers: { "Content-Type": "application/json", Authorization: `Bearer ${getAccessToken()}` },
						body: JSON.stringify({
							challenge: serverOptions.challenge,
							credential: {
								credentialId: bufferToBase64url(credential.rawId),
								publicKey: publicKey ? bufferToBase64url(publicKey) : "",
								transports: response.getTransports?.() || [],
							},
						}),
					});
					if (!completeRes.ok) throw new Error((await completeRes.json().catch(() => ({}))).error || "Device registration failed");

					if (acctStatus) { acctStatus.credentialCount++; acctStatus.multiDevice = acctStatus.credentialCount > 1; }
					localStorage.removeItem("eid_device_nudge_dismissed");
					devicesLoaded = false;
					loadDevices();
					btn.innerHTML = "Device Registered";
					btn.className = "btn btn--success";
					render();
					this.dispatchEvent(new CustomEvent("identity-action", { bubbles: true, composed: true, detail: { action: "device-added" } }));
				} catch (e: any) {
					btn.disabled = false; btn.innerHTML = "🔑 Register Passkey on This Device";
					err.textContent = e.name === "NotAllowedError" ? "Passkey creation was cancelled." : e.message;
				}
			});

			// Recovery: add guardian
			overlay.querySelector('[data-action="add-guardian"]')?.addEventListener("click", async () => {
				const nameInput = overlay.querySelector("#acct-guardian-name") as HTMLInputElement;
				const emailInput = overlay.querySelector("#acct-guardian-email") as HTMLInputElement;
				const err = overlay.querySelector("#recovery-error") as HTMLElement;
				const btn = overlay.querySelector('[data-action="add-guardian"]') as HTMLButtonElement;
				const name = nameInput.value.trim();
				const email = emailInput.value.trim();
				if (!name) { err.textContent = "Enter a guardian name."; nameInput.focus(); return; }
				err.textContent = "";
				btn.disabled = true; btn.innerHTML = '<span class="spinner"></span>';
				try {
					const res = await fetch(`${ENCRYPTID_URL}/api/guardians`, {
						method: "POST",
						headers: { "Content-Type": "application/json", Authorization: `Bearer ${getAccessToken()}` },
						body: JSON.stringify({ name, email: email || undefined }),
					});
					const data = await res.json();
					if (!res.ok) throw new Error(data.error || "Failed to add guardian");
					guardians.push({ id: data.guardian.id, name: data.guardian.name, email: data.guardian.email, status: data.guardian.status });
					const accepted = guardians.filter(g => g.status === "accepted").length;
					if (acctStatus) { acctStatus.guardianCount = guardians.length; acctStatus.acceptedGuardianCount = accepted; acctStatus.socialRecovery = accepted >= 2; }
					localStorage.removeItem("eid_recovery_status");
					render();
					setTimeout(() => (overlay.querySelector("#acct-guardian-name") as HTMLInputElement)?.focus(), 50);
				} catch (e: any) {
					btn.disabled = false; btn.innerHTML = "Add";
					err.textContent = e.message;
				}
			});

			overlay.querySelector("#acct-guardian-name")?.addEventListener("keydown", (e) => {
				if ((e as KeyboardEvent).key === "Enter") (overlay.querySelector('[data-action="add-guardian"]') as HTMLElement)?.click();
			});
			overlay.querySelector("#acct-guardian-email")?.addEventListener("keydown", (e) => {
				if ((e as KeyboardEvent).key === "Enter") (overlay.querySelector('[data-action="add-guardian"]') as HTMLElement)?.click();
			});

			// Recovery: remove guardian
			overlay.querySelectorAll("[data-remove-guardian]").forEach(el => {
				el.addEventListener("click", async () => {
					const id = (el as HTMLElement).dataset.removeGuardian!;
					const err = overlay.querySelector("#recovery-error") as HTMLElement;
					try {
						const res = await fetch(`${ENCRYPTID_URL}/api/guardians/${id}`, {
							method: "DELETE",
							headers: { Authorization: `Bearer ${getAccessToken()}` },
						});
						if (!res.ok) throw new Error("Failed to remove guardian");
						guardians = guardians.filter(g => g.id !== id);
						const acceptedAfter = guardians.filter(g => g.status === "accepted").length;
						if (acctStatus) { acctStatus.guardianCount = guardians.length; acctStatus.acceptedGuardianCount = acceptedAfter; acctStatus.socialRecovery = acceptedAfter >= 2; }
						localStorage.removeItem("eid_recovery_status");
						render();
					} catch (e: any) {
						if (err) err.textContent = e.message;
					}
				});
			});

			// Recovery: start drill
			overlay.querySelector('[data-action="start-drill"]')?.addEventListener("click", async () => {
				const btn = overlay.querySelector('[data-action="start-drill"]') as HTMLButtonElement;
				const err = overlay.querySelector("#recovery-error") as HTMLElement;
				if (!confirm("This will send a test notification to your guardians. They'll need to verify your identity and approve. Continue?")) return;
				btn.disabled = true; btn.innerHTML = '<span class="spinner"></span> Starting drill...';
				try {
					const res = await fetch(`${ENCRYPTID_URL}/api/recovery/drill/initiate`, {
						method: "POST",
						headers: { "Content-Type": "application/json", Authorization: `Bearer ${getAccessToken()}` },
					});
					const data = await res.json();
					if (!res.ok) throw new Error(data.error || "Failed to start drill");
					activeDrillId = data.drillId;
					drillStatus = { approvals: data.guardians.map((g: any) => ({ guardianId: g.id, approved: false })), approvalCount: 0, threshold: 2, status: "pending" };
					// Start polling
					drillPollingTimer = setInterval(async () => {
						if (!activeDrillId) return;
						try {
							const pollRes = await fetch(`${ENCRYPTID_URL}/api/recovery/drill/${activeDrillId}/status`, {
								headers: { Authorization: `Bearer ${getAccessToken()}` },
							});
							if (pollRes.ok) {
								const pollData = await pollRes.json();
								drillStatus = pollData;
								render();
								if (pollData.status === "approved" || pollData.status === "completed") {
									if (drillPollingTimer) clearInterval(drillPollingTimer);
									drillPollingTimer = null;
									// Complete the drill
									await fetch(`${ENCRYPTID_URL}/api/recovery/drill/${activeDrillId}/complete`, {
										method: "POST",
										headers: { Authorization: `Bearer ${getAccessToken()}` },
									});
									if (acctStatus) acctStatus.lastDrillAt = Date.now();
									// Clear recovery status cache so badge refreshes
									localStorage.removeItem("eid_recovery_status");
									render();
								}
							}
						} catch { /* offline */ }
					}, 5000);
					render();
				} catch (e: any) {
					btn.disabled = false; btn.innerHTML = "🧪 Run Recovery Drill";
					if (err) err.textContent = e.message;
				}
			});

			// Recovery: preview walkthrough
			overlay.querySelector('[data-action="preview-recovery"]')?.addEventListener("click", () => {
				showWalkthrough = true;
				walkthroughStep = 0;
				render();
			});

			// Walkthrough navigation
			overlay.querySelector('[data-action="walkthrough-next"]')?.addEventListener("click", () => {
				walkthroughStep++;
				if (walkthroughStep >= 5) { showWalkthrough = false; walkthroughStep = 0; }
				render();
			});
			overlay.querySelector('[data-action="walkthrough-skip"]')?.addEventListener("click", () => {
				showWalkthrough = false; walkthroughStep = 0; render();
			});

			// Device: rename credential
			overlay.querySelectorAll("[data-rename-credential]").forEach(el => {
				el.addEventListener("click", async () => {
					const id = (el as HTMLElement).dataset.renameCredential!;
					const device = devices.find(d => d.credentialId === id);
					const newLabel = prompt("Enter a label for this device:", device?.label || "");
					if (!newLabel || !newLabel.trim()) return;
					try {
						const res = await fetch(`${ENCRYPTID_URL}/api/user/credentials/${encodeURIComponent(id)}`, {
							method: "PATCH",
							headers: { "Content-Type": "application/json", Authorization: `Bearer ${getAccessToken()}` },
							body: JSON.stringify({ label: newLabel.trim() }),
						});
						if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || "Failed to rename");
						if (device) device.label = newLabel.trim();
						render();
					} catch (e: any) {
						const err = overlay.querySelector("#device-error") as HTMLElement;
						if (err) err.textContent = e.message;
					}
				});
			});

			// Device: remove credential
			overlay.querySelectorAll("[data-remove-credential]").forEach(el => {
				el.addEventListener("click", async () => {
					const id = (el as HTMLElement).dataset.removeCredential!;
					if (!confirm("Remove this passkey? You won't be able to sign in with it anymore.")) return;
					try {
						const res = await fetch(`${ENCRYPTID_URL}/api/user/credentials/${encodeURIComponent(id)}`, {
							method: "DELETE",
							headers: { Authorization: `Bearer ${getAccessToken()}` },
						});
						if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || "Failed to remove");
						devices = devices.filter(d => d.credentialId !== id);
						if (acctStatus) { acctStatus.credentialCount = devices.length; acctStatus.multiDevice = devices.length > 1; }
						render();
					} catch (e: any) {
						const err = overlay.querySelector("#device-error") as HTMLElement;
						if (err) err.textContent = e.message;
					}
				});
			});

			// Data Storage toggle
			const backupToggle = overlay.querySelector("#acct-backup-toggle") as HTMLInputElement;
			if (backupToggle) {
				backupToggle.addEventListener("change", (e) => {
					e.stopPropagation();
					const enabled = backupToggle.checked;
					setEncryptedBackupEnabled(enabled);
					render();
					this.dispatchEvent(new CustomEvent("backup-toggle", { bubbles: true, composed: true, detail: { enabled } }));
				});
			}

			// Shortcut slot selects
			overlay.querySelectorAll<HTMLSelectElement>(".slot-select").forEach(sel => {
				sel.addEventListener("change", () => {
					const slot = sel.dataset.slot!;
					if (sel.value) {
						setShortcut(slot, sel.value);
					} else {
						removeShortcut(slot);
					}
				});
			});

			// Connections: connect buttons
			overlay.querySelectorAll(".conn-btn--connect").forEach(btn => {
				btn.addEventListener("click", () => {
					const provider = (btn as HTMLElement).dataset.provider!;
					const space = (btn as HTMLElement).dataset.space!;
					const popup = window.open(`/api/oauth/${provider}/authorize?space=${encodeURIComponent(space)}`, "_blank", "width=600,height=700");
					const poll = setInterval(() => {
						if (!popup || popup.closed) {
							clearInterval(poll);
							connectionsLoaded = false;
							loadConnections();
						}
					}, 1500);
				});
			});

			// Connections: disconnect buttons
			overlay.querySelectorAll(".conn-btn--disconnect").forEach(btn => {
				btn.addEventListener("click", async () => {
					const provider = (btn as HTMLElement).dataset.provider!;
					const el = btn as HTMLButtonElement;
					el.disabled = true;
					el.textContent = "...";
					const username = getUsername() || "";
					try {
						const token = getAccessToken();
						await fetch(`/api/oauth/${provider}/disconnect?space=${encodeURIComponent(username)}`, {
							method: "POST",
							headers: token ? { Authorization: `Bearer ${token}` } : {},
						});
					} catch { /* best-effort */ }
					connectionsLoaded = false;
					loadConnections();
				});
			});

			// Connections: sharing checkboxes
			overlay.querySelectorAll<HTMLInputElement>(".conn-share-cb").forEach(cb => {
				cb.addEventListener("change", async () => {
					const provider = cb.dataset.provider!;
					const spaceSlug = cb.dataset.space!;
					const current = sharingConfig[provider]?.spaces || [];
					const updated = cb.checked
						? [...new Set([...current, spaceSlug])]
						: current.filter(s => s !== spaceSlug);

					// Update local state immediately
					if (!sharingConfig[provider]) sharingConfig[provider] = { spaces: [] };
					sharingConfig[provider].spaces = updated;

					// Persist to server
					const username = getUsername() || "";
					const token = getAccessToken();
					try {
						await fetch("/api/oauth/sharing", {
							method: "POST",
							headers: { "Content-Type": "application/json", ...(token ? { Authorization: `Bearer ${token}` } : {}) },
							body: JSON.stringify({ space: username, provider, sharedSpaces: updated }),
						});
					} catch { /* best-effort */ }
				});
			});

		};

		document.body.appendChild(overlay);
		render();
	}

	// ── Wallets modal ──

	#showWalletsModal(): void {
		if (document.querySelector(".rstack-wallets-overlay")) return;
		const overlay = document.createElement("div");
		overlay.className = "rstack-wallets-overlay";

		const session = getSession();
		const username = session?.claims.username || "";
		const did = session?.claims.did || session?.claims.sub || "";
		const initial = username ? username[0].toUpperCase() : did.slice(8, 10).toUpperCase();
		const truncDid = did.length > 32 ? did.slice(0, 16) + "..." + did.slice(-8) : did;

		const discovery = new _WalletDiscovery();
		const connectedAddrs = new Map<string, string>(); // uuid → address

		const close = () => { discovery.stop(); overlay.remove(); };

		const render = () => {
			const browserSection = discovery.providers.length
				? discovery.providers.map((p) => {
					const addr = connectedAddrs.get(p.info.uuid);
					return `
					<div class="wallet-provider-card" data-uuid="${p.info.uuid}">
						<img class="wallet-provider-icon" src="${p.info.icon}" alt="${p.info.name}" />
						<div class="wallet-provider-info">
							<div class="wallet-provider-name">${p.info.name}</div>
							${addr
								? `<div class="wallet-provider-addr">${addr.slice(0, 6)}...${addr.slice(-4)}</div>`
								: `<button class="wallet-connect-btn" data-connect="${p.info.uuid}">Connect</button>`
							}
						</div>
					</div>`;
				}).join("")
				: `<p class="wallet-empty">No browser wallets detected.<br><span style="font-size:0.75rem;color:var(--rs-text-muted)">Install MetaMask, Rainbow, or another EIP-6963 wallet.</span></p>`;

			overlay.innerHTML = `
				<style>${MODAL_STYLES}${WALLETS_STYLES}</style>
				<div class="wallets-modal">
					<button class="close-btn" data-action="close">&times;</button>
					<h2>My Wallets</h2>

					<div class="wallet-section-label">rIdentity Wallet</div>
					<div class="wallet-identity-card">
						<div class="wallet-identity-avatar">${initial}</div>
						<div class="wallet-identity-info">
							<div class="wallet-identity-name">${username || "Anonymous"}</div>
							<div class="wallet-identity-did" title="${did}">${truncDid}</div>
						</div>
						<span class="wallet-badge">Passkey</span>
					</div>

					<div class="wallet-section-label">Browser Wallets</div>
					<div class="wallet-providers">${browserSection}</div>

					<div class="wallet-footer">
						<button class="wallet-open-btn" data-action="open-rwallet">Open rWallet →</button>
					</div>
				</div>
			`;

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

			overlay.querySelector('[data-action="open-rwallet"]')?.addEventListener("click", () => {
				close();
				const space = _getCurrentSpace();
				window.location.href = _navUrl(space, "rwallet");
			});

			overlay.querySelectorAll("[data-connect]").forEach((btn) => {
				btn.addEventListener("click", async (e) => {
					e.stopPropagation();
					const uuid = (btn as HTMLElement).dataset.connect!;
					const provider = discovery.providers.find((p) => p.info.uuid === uuid);
					if (!provider) return;
					(btn as HTMLButtonElement).textContent = "Connecting...";
					(btn as HTMLButtonElement).disabled = true;
					try {
						const accounts = await provider.provider.request({ method: "eth_requestAccounts" }) as string[];
						if (accounts?.[0]) {
							connectedAddrs.set(uuid, accounts[0]);
							render();
						}
					} catch {
						(btn as HTMLButtonElement).textContent = "Rejected";
						setTimeout(() => { (btn as HTMLButtonElement).textContent = "Connect"; (btn as HTMLButtonElement).disabled = false; }, 2000);
					}
				});
			});
		};

		document.body.appendChild(overlay);
		discovery.start(render);
		render();
	}

	// ── Spaces modal ──

	#showSpacesModal(): void {
		if (document.querySelector(".rstack-spaces-overlay")) return;
		const overlay = document.createElement("div");
		overlay.className = "rstack-spaces-overlay";

		const close = () => overlay.remove();

		const renderLoading = () => {
			overlay.innerHTML = `
				<style>${MODAL_STYLES}${SPACES_STYLES}</style>
				<div class="spaces-modal">
					<button class="close-btn" data-action="close">&times;</button>
					<h2>My Spaces</h2>
					<p>Loading your spaces...</p>
					<div style="text-align:center;padding:2rem 0"><span class="spinner"></span></div>
				</div>
			`;
			overlay.querySelector('[data-action="close"]')?.addEventListener("click", close);
			overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); });
		};

		const visInfo = (v: string) =>
			v === "private" ? { icon: "🔒", cls: "vis-private", label: "private" }
			: v === "permissioned" ? { icon: "🔑", cls: "vis-permissioned", label: "permissioned" }
			: { icon: "👁", cls: "vis-public", label: "public" };

		const displayName = (s: any) => {
			const v = s.visibility || "public";
			if (v === "private") {
				const username = getUsername();
				return username ? `${username}'s Space` : "My Space";
			}
			return (s.name || s.slug).replace(/</g, "&lt;");
		};

		const renderSpaces = (spaces: any[]) => {
			const yourSpaces = spaces.filter((s) => s.role);
			const publicSpaces = spaces.filter((s) => !s.role && s.accessible);

			const cardHTML = (s: any) => {
				const vis = visInfo(s.visibility || "public");
				return `
				<button class="space-card ${vis.cls}" data-slug="${s.slug}">
					<div class="space-card-initial">${(s.name || s.slug).charAt(0).toUpperCase()}</div>
					<div class="space-card-name">${displayName(s)}</div>
					<div class="space-card-meta">
						<span class="space-vis ${vis.cls}">${vis.icon} ${vis.label}</span>
						${s.role ? `<span class="space-role">${s.role}</span>` : ""}
					</div>
				</button>
			`;};

			const yourSection = yourSpaces.length
				? `<div class="spaces-section-label">Your Spaces</div>
				   <div class="spaces-grid">${yourSpaces.map(cardHTML).join("")}</div>`
				: "";

			const publicSection = publicSpaces.length
				? `<div class="spaces-section-label">Public Spaces</div>
				   <div class="spaces-grid">${publicSpaces.map(cardHTML).join("")}</div>`
				: "";

			const emptyState = !yourSpaces.length && !publicSpaces.length
				? `<p style="color:var(--rs-text-secondary);text-align:center;padding:1rem 0">No spaces yet. Create one to get started!</p>`
				: "";

			overlay.innerHTML = `
				<style>${MODAL_STYLES}${SPACES_STYLES}</style>
				<div class="spaces-modal">
					<button class="close-btn" data-action="close">&times;</button>
					<h2>My Spaces</h2>
					${emptyState}
					${yourSection}
					${publicSection}
					<div class="spaces-grid" style="margin-top:12px">
						<button class="space-card space-card--create" data-action="create-space">
							<div class="space-card-initial">+</div>
							<div class="space-card-name">Create New Space</div>
						</button>
					</div>
				</div>
			`;

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

			overlay.querySelectorAll("[data-slug]").forEach((el) => {
				el.addEventListener("click", () => {
					const slug = (el as HTMLElement).dataset.slug!;
					close();
					window.location.href = rspaceNavUrl(slug, getCurrentModule());
				});
			});

			overlay.querySelector('[data-action="create-space"]')?.addEventListener("click", () => {
				close();
				window.location.href = "/create-space";
			});
		};

		document.body.appendChild(overlay);
		renderLoading();

		const token = getAccessToken();
		fetch("/api/spaces", {
			headers: token ? { Authorization: `Bearer ${token}` } : {},
		})
			.then((r) => r.json())
			.then((data) => renderSpaces(data.spaces || []))
			.catch(() => {
				overlay.innerHTML = `
					<style>${MODAL_STYLES}${SPACES_STYLES}</style>
					<div class="spaces-modal">
						<button class="close-btn" data-action="close">&times;</button>
						<h2>My Spaces</h2>
						<p style="color:#ef4444">Failed to load spaces. Please try again.</p>
					</div>
				`;
				overlay.querySelector('[data-action="close"]')?.addEventListener("click", close);
				overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); });
			});
	}

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

// ── Require auth helper (for use by module code) ──

export function requireAuth(onAuthenticated: () => void): boolean {
	if (isAuthenticated()) return true;
	const el = document.querySelector("rstack-identity") as RStackIdentity | null;
	if (el) {
		el.showAuthModal({ onSuccess: onAuthenticated });
	}
	return false;
}

// ── Styles ──

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

.signin-btn {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 20px; border-radius: 8px; border: none;
  font-size: 0.875rem; font-weight: 600; cursor: pointer;
  transition: all 0.2s; text-decoration: none;
}
.signin-btn {
  background: linear-gradient(135deg, #06b6d4, #7c3aed); color: white;
}
.signin-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(6,182,212,0.3); }

.user {
  display: flex; align-items: center; gap: 10px;
  position: relative; cursor: pointer;
}
.avatar {
  width: 34px; height: 34px; border-radius: 50%;
  background: linear-gradient(135deg, #06b6d4, #7c3aed);
  display: flex; align-items: center; justify-content: center;
  font-weight: 700; font-size: 0.8rem; color: white;
}
.name {
  font-size: 0.8rem; max-width: 140px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.name { color: var(--rs-text-muted); }

.dropdown {
  position: absolute; top: 100%; right: 0; margin-top: 8px;
  min-width: 200px; border-radius: 10px; overflow: hidden;
  box-shadow: 0 8px 30px rgba(0,0,0,0.2); display: none; z-index: 10002;
}
.dropdown.open { display: block; }
.dropdown.open { display: block; background: var(--rs-bg-surface); border: 1px solid var(--rs-border); }

.dropdown-item {
  display: flex; align-items: center; gap: 10px;
  padding: 12px 16px; font-size: 0.875rem; cursor: pointer;
  transition: background 0.15s; border: none; background: none;
  width: 100%; text-align: left;
}
.dropdown-item { color: var(--rs-text-primary); }
.dropdown-item:hover { background: var(--rs-bg-hover); }
.dropdown-item--danger { color: #ef4444 !important; }

.dropdown-header {
  padding: 10px 16px 6px; font-size: 0.8rem; font-weight: 700;
  letter-spacing: 0.02em; white-space: nowrap; overflow: hidden;
  text-overflow: ellipsis; max-width: 200px;
}
.dropdown-header { color: var(--rs-text-primary); }
.dropdown-divider { height: 1px; margin: 4px 0; }
.dropdown-divider { background: var(--rs-border-subtle); }

/* Mobile-only items — hidden on desktop, shown on mobile */
.mobile-only { display: none; }
@media (max-width: 640px) {
  .mobile-only { display: flex; }
  .mobile-only.dropdown-divider { display: block; }
}

/* Theme toggle in dropdown */
.dropdown-theme-row {
  display: flex; align-items: center; justify-content: center;
  gap: 10px; padding: 8px 16px;
}
.theme-icon { font-size: 0.9rem; line-height: 1; }
.theme-toggle {
  position: relative; width: 40px; height: 22px;
  display: inline-block; flex-shrink: 0;
}
.theme-toggle input { opacity: 0; width: 0; height: 0; }
.theme-slider {
  position: absolute; inset: 0; border-radius: 11px;
  background: #fbbf24; cursor: pointer; transition: background 0.25s;
}
.theme-slider::before {
  content: ""; position: absolute;
  width: 18px; height: 18px; border-radius: 50%;
  left: 2px; bottom: 2px; background: white;
  transition: transform 0.25s;
}
.theme-toggle input:checked + .theme-slider { background: #6366f1; }
.theme-toggle input:checked + .theme-slider::before { transform: translateX(18px); }

/* Canvas background selector in dropdown */
.dropdown-canvas-row {
  display: flex; align-items: center; justify-content: center;
  gap: 8px; padding: 6px 16px;
}
.canvas-label {
  font-size: 0.7rem; font-weight: 600; opacity: 0.5;
  text-transform: uppercase; letter-spacing: 0.04em;
}
.canvas-options { display: flex; gap: 4px; }
.canvas-opt {
  font-size: 0.65rem; font-weight: 600; padding: 3px 8px;
  border: 1px solid var(--rs-border); border-radius: 6px;
  background: transparent; color: var(--rs-text-secondary);
  cursor: pointer; transition: all 0.15s;
}
.canvas-opt:hover { background: var(--rs-bg-hover); }
.canvas-opt.active {
  background: var(--rs-accent); color: white;
  border-color: var(--rs-accent);
}

/* Avatar wrapper + notification badge */
.avatar-wrap { position: relative; }
.notif-badge {
  position: absolute; top: -4px; right: -4px;
  min-width: 16px; height: 16px; border-radius: 8px;
  background: #ef4444; color: white; font-size: 0.6rem; font-weight: 700;
  display: flex; align-items: center; justify-content: center;
  padding: 0 4px; border: 2px solid var(--rs-bg-surface); line-height: 1;
}
/* Recovery alert dot on avatar */
.recovery-alert-dot {
  position: absolute; bottom: -1px; right: -1px;
  width: 10px; height: 10px; border-radius: 50%;
  background: #f87171; border: 2px solid var(--rs-bg-surface, #1e1e2e);
  animation: recovery-pulse 2s ease-in-out infinite;
}
@keyframes recovery-pulse {
  0%, 100% { box-shadow: 0 0 4px rgba(248,113,113,0.4); }
  50% { box-shadow: 0 0 10px rgba(248,113,113,0.8); }
}
/* Account alert dot on "My Account" dropdown item */
.acct-alert-dot {
  width: 8px; height: 8px; border-radius: 50%;
  background: #f87171; margin-left: auto; flex-shrink: 0;
  box-shadow: 0 0 6px rgba(248,113,113,0.6);
  animation: recovery-pulse 2s ease-in-out infinite;
}

/* Persona switcher in dropdown */
.dropdown-label {
  padding: 6px 16px; font-size: 0.7rem; text-transform: uppercase;
  letter-spacing: 0.05em; color: var(--rs-text-secondary); font-weight: 600;
}
.persona-row {
  display: flex; align-items: center; padding-right: 8px;
}
.persona-row .persona-item { flex: 1; }
.persona-avatar {
  width: 24px; height: 24px; border-radius: 50%;
  background: linear-gradient(135deg, #06b6d4, #7c3aed);
  display: flex; align-items: center; justify-content: center;
  font-weight: 700; font-size: 0.65rem; color: white; flex-shrink: 0;
}
.persona-remove {
  opacity: 0.4; cursor: pointer; font-size: 0.75rem;
  padding: 2px 6px; border-radius: 4px; border: none;
  background: none; color: var(--rs-text-secondary); flex-shrink: 0;
}
.persona-remove:hover { opacity: 1; background: var(--rs-bg-hover); }

/* Notification items in dropdown */
.dropdown-section-label {
  padding: 8px 16px 4px; font-size: 0.65rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.5;
}
.notif-item {
  padding: 10px 16px; border-left: 3px solid #fbbf24;
}
.notif-text { font-size: 0.8rem; line-height: 1.4; color: var(--rs-text-primary); }
.notif-msg {
  font-size: 0.75rem; color: var(--rs-text-secondary); font-style: italic;
  margin-top: 4px; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; max-width: 240px;
}
.notif-actions { display: flex; gap: 8px; margin-top: 8px; }
.notif-btn {
  padding: 4px 12px; border-radius: 6px; border: none;
  font-size: 0.75rem; font-weight: 600; cursor: pointer; transition: opacity 0.15s;
}
.notif-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.notif-btn--approve { background: #059669; color: white; }
.notif-btn--approve:hover:not(:disabled) { opacity: 0.85; }
.notif-btn--deny { background: rgba(239,68,68,0.15); color: #ef4444; }
.notif-btn--deny:hover:not(:disabled) { background: rgba(239,68,68,0.25); }

/* Toggle switch */
.toggle-row {
  display: flex; align-items: center;
  justify-content: space-between; cursor: default;
}
.toggle-switch {
  position: relative; width: 36px; height: 20px;
  display: inline-block; flex-shrink: 0;
}
.toggle-switch input { opacity: 0; width: 0; height: 0; }
.toggle-slider {
  position: absolute; inset: 0; border-radius: 10px;
  background: var(--rs-border); cursor: pointer;
  transition: background 0.2s;
}
.toggle-slider::before {
  content: ""; position: absolute;
  width: 16px; height: 16px; border-radius: 50%;
  left: 2px; bottom: 2px; background: white;
  transition: transform 0.2s;
}
.toggle-switch input:checked + .toggle-slider { background: #059669; }
.toggle-switch input:checked + .toggle-slider::before { transform: translateX(16px); }
`;

const MODAL_STYLES = `
.rstack-auth-overlay {
  position: fixed; inset: 0; background: rgba(0,0,0,0.6);
  -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px); display: flex; align-items: center;
  justify-content: center; z-index: 10000; animation: fadeIn 0.2s;
}
.auth-modal {
  background: var(--rs-bg-surface); border: 1px solid var(--rs-border);
  border-radius: 16px; padding: 2rem; max-width: 420px; width: 90%;
  max-height: 90dvh; overflow-y: auto;
  text-align: center; color: var(--rs-text-primary); box-shadow: var(--rs-shadow-lg);
  animation: slideUp 0.3s;
}
@media (max-width: 480px) { .auth-modal { padding: 1.25rem; } }
.auth-modal h2 {
  font-size: 1.5rem; margin-bottom: 0.5rem;
  background: linear-gradient(135deg, #06b6d4, #7c3aed);
  -webkit-background-clip: text; -webkit-text-fill-color: transparent;
}
.auth-modal p { color: var(--rs-text-secondary); font-size: 0.95rem; line-height: 1.6; margin-bottom: 1.5rem; }
.input {
  width: 100%; padding: 12px 16px; border-radius: 8px;
  border: 1px solid var(--rs-input-border); background: var(--rs-input-bg);
  color: var(--rs-input-text); font-size: 1rem; margin-bottom: 1rem; outline: none;
  transition: border-color 0.2s; box-sizing: border-box;
}
.input:focus { border-color: #06b6d4; }
.input::placeholder { color: var(--rs-text-muted); }
.actions { display: flex; gap: 12px; margin-top: 0.5rem; }
.btn {
  flex: 1; padding: 12px 20px; border-radius: 8px; border: none;
  font-size: 0.95rem; font-weight: 600; cursor: pointer; transition: all 0.2s;
}
.btn--primary { background: linear-gradient(135deg, #06b6d4, #7c3aed); color: white; }
.btn--primary:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(6,182,212,0.3); }
.btn--primary:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
.btn--secondary { background: var(--rs-btn-secondary-bg); color: var(--rs-text-secondary); border: 1px solid var(--rs-border); }
.btn--secondary:hover { background: var(--rs-bg-hover); color: var(--rs-text-primary); }
.error { color: #ef4444; font-size: 0.85rem; margin-top: 0.5rem; min-height: 1.2em; }
.toggle { margin-top: 1rem; font-size: 0.85rem; color: var(--rs-text-muted); }
.toggle a { color: #06b6d4; cursor: pointer; text-decoration: none; }
.toggle a:hover { text-decoration: underline; }
.spinner {
  display: inline-block; width: 18px; height: 18px;
  border: 2px solid transparent; border-top-color: currentColor;
  border-radius: 50%; animation: spin 0.7s linear infinite;
  vertical-align: middle; margin-right: 6px;
}
.close-btn {
  position: absolute; top: 12px; right: 16px;
  background: none; border: none; color: var(--rs-text-muted); font-size: 1.5rem;
  cursor: pointer; line-height: 1; padding: 4px 8px; border-radius: 6px;
  transition: all 0.15s;
}
.close-btn:hover, .close-btn:active { color: var(--rs-text-primary); background: var(--rs-bg-hover); }
.auth-modal { position: relative; }
.actions--stack { flex-direction: column; }
.btn--outline {
  background: transparent; color: var(--rs-text-secondary);
  border: 1px solid var(--rs-border);
  padding: 12px 20px; border-radius: 8px; font-size: 0.95rem;
  font-weight: 600; cursor: pointer; transition: all 0.2s;
}
.btn--outline:hover { border-color: #06b6d4; color: var(--rs-text-primary); background: rgba(6,182,212,0.08); }
.learn-more { margin-top: 1.5rem; font-size: 0.8rem; color: var(--rs-text-muted); }
.learn-more a { color: #06b6d4; text-decoration: none; }
.learn-more a:hover { text-decoration: underline; }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes slideUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
@keyframes spin { to { transform: rotate(360deg); } }
`;

const SETTINGS_STYLES = `
.info-text { margin-top: 1rem; font-size: 0.8rem; color: var(--rs-text-muted); line-height: 1.5; }
.btn--success { background: #059669 !important; color: white; cursor: default; }
.btn--small { padding: 10px 16px; flex: none; }
.input-row { display: flex; gap: 8px; align-items: stretch; }
.input--inline { flex: 1; margin-bottom: 0; }
.contact-list { margin-top: 12px; display: flex; flex-direction: column; gap: 6px; }
.contact-item {
  display: flex; align-items: center; justify-content: space-between;
  padding: 8px 12px; border-radius: 8px; background: var(--rs-bg-hover);
  border: 1px solid var(--rs-border); font-size: 0.9rem; color: var(--rs-text-primary);
}
.contact-remove {
  background: none; border: none; color: var(--rs-text-muted); font-size: 1.2rem;
  cursor: pointer; padding: 2px 6px; border-radius: 4px; line-height: 1;
}
.contact-remove:hover { color: #ef4444; background: rgba(239,68,68,0.1); }
.threshold-row {
  display: flex; align-items: center; gap: 8px; margin-top: 12px;
  font-size: 0.85rem; color: var(--rs-text-secondary);
}
.threshold-row label { white-space: nowrap; }
.threshold-row select {
  padding: 6px 10px; border-radius: 6px; background: var(--rs-btn-secondary-bg);
  border: 1px solid var(--rs-border); color: var(--rs-text-primary); font-size: 0.85rem;
}
.threshold-hint { color: var(--rs-text-muted); font-size: 0.8rem; }
`;

const ACCOUNT_MODAL_STYLES = `
.rstack-account-overlay {
  position: fixed; inset: 0; background: rgba(0,0,0,0.6);
  -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px); display: flex; align-items: center;
  justify-content: center; z-index: 10000; animation: fadeIn 0.2s;
}
.account-modal {
  background: var(--rs-bg-surface); border: 1px solid var(--rs-border);
  border-radius: 16px; padding: 2rem; max-width: 520px; width: 92%;
  max-height: 85vh; overflow-y: auto; color: var(--rs-text-primary); position: relative;
  box-shadow: var(--rs-shadow-lg); animation: slideUp 0.3s;
  text-align: left;
}
.account-modal h2 {
  font-size: 1.5rem; margin-bottom: 1rem;
  background: linear-gradient(135deg, #06b6d4, #7c3aed);
  -webkit-background-clip: text; -webkit-text-fill-color: transparent;
}
.account-section {
  border: 1px solid var(--rs-btn-secondary-bg); border-radius: 10px;
  margin-bottom: 8px; overflow: hidden; transition: border-color 0.2s;
}
.account-section:hover { border-color: var(--rs-border); }
.account-section.open { border-color: rgba(6,182,212,0.3); }
.account-section-header {
  display: flex; align-items: center; justify-content: space-between;
  padding: 12px 16px; cursor: pointer; font-size: 0.9rem; font-weight: 500;
  transition: background 0.15s; user-select: none;
}
.account-section-header:hover { background: var(--rs-bg-hover); }
.section-arrow { font-size: 0.7rem; color: var(--rs-text-muted); transition: transform 0.2s; }
.account-section-body {
  padding: 0 16px 16px; animation: fadeIn 0.15s;
}
.account-section--inline {
  border: 1px solid var(--rs-btn-secondary-bg); border-radius: 10px;
  margin-bottom: 8px; padding: 4px 0;
}
.account-section--inline .account-section-header { cursor: default; }
.account-section--inline .account-section-header:hover { background: none; }
.toggle-hint {
  padding: 0 16px 10px; font-size: 0.75rem; color: var(--rs-text-muted); line-height: 1.4;
}
.guardian-piece { font-size: 1.1rem; flex-shrink: 0; }
.status-dot {
  display: inline-block; width: 8px; height: 8px; border-radius: 50%;
  margin-right: 6px; vertical-align: middle; flex-shrink: 0;
}
.status-dot.done { background: #34d399; box-shadow: 0 0 4px rgba(52,211,153,0.4); }
.status-dot.pending { background: #f87171; box-shadow: 0 0 4px rgba(248,113,113,0.4); }
.section--warning { border-color: rgba(248,113,113,0.3) !important; }
.section--warning .account-section-header span:first-child { color: #fca5a5; }
.address-form {
  display: flex; flex-direction: column; gap: 8px; margin-bottom: 12px;
}
.address-form .input { margin-bottom: 0; }
.address-row { display: flex; gap: 8px; }
.address-row .input { flex: 1; margin-bottom: 0; }
/* Toggle switch (duplicated for body-level modal) */
.toggle-switch {
  position: relative; width: 36px; height: 20px;
  display: inline-block; flex-shrink: 0;
}
.toggle-switch input { opacity: 0; width: 0; height: 0; }
.toggle-slider {
  position: absolute; inset: 0; border-radius: 10px;
  background: var(--rs-border); cursor: pointer;
  transition: background 0.2s;
}
.toggle-slider::before {
  content: ""; position: absolute;
  width: 16px; height: 16px; border-radius: 50%;
  left: 2px; bottom: 2px; background: white;
  transition: transform 0.2s;
}
.toggle-switch input:checked + .toggle-slider { background: #059669; }
.toggle-switch input:checked + .toggle-slider::before { transform: translateX(16px); }
.shortcut-grid {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px;
}
.shortcut-slot {
  display: flex; align-items: center; gap: 6px;
}
.slot-num {
  width: 20px; height: 20px; border-radius: 4px; display: flex; align-items: center;
  justify-content: center; font-size: 0.75rem; font-weight: 600; flex-shrink: 0;
  background: var(--rs-btn-secondary-bg); color: var(--rs-text-secondary);
}
.slot-select {
  flex: 1; min-width: 0; padding: 4px 6px; border-radius: 6px; font-size: 0.75rem;
  background: var(--rs-btn-secondary-bg); border: 1px solid var(--rs-border);
  color: var(--rs-text-primary); cursor: pointer;
}
.shortcut-hint {
  margin: 8px 0 0; font-size: 0.7rem; color: var(--rs-text-muted); text-align: center;
}
/* Connections section */
.conn-list { display: flex; flex-direction: column; gap: 8px; }
.conn-card {
  border: 1px solid var(--rs-border, #262626); border-radius: 10px;
  padding: 10px 14px; transition: border-color 0.2s;
}
.conn-card:hover { border-color: var(--rs-text-muted, #525252); }
.conn-card--active { border-color: rgba(52,211,153,0.3); }
.conn-card--soon { opacity: 0.4; pointer-events: none; }
.conn-card-header { display: flex; align-items: center; gap: 10px; }
.conn-card-icon {
  width: 30px; height: 30px; border-radius: 8px; display: flex; align-items: center; justify-content: center;
  background: var(--rs-bg-surface, #0a0a0a); border: 1px solid var(--rs-border, #262626);
  font-size: 0.85rem; font-weight: 700; color: var(--rs-text-primary); flex-shrink: 0;
}
.conn-card-info { flex: 1; min-width: 0; }
.conn-card-name { font-size: 0.82rem; font-weight: 600; }
.conn-badge {
  display: inline-block; font-size: 0.62rem; font-weight: 600; padding: 2px 7px; border-radius: 6px;
  text-transform: uppercase; letter-spacing: 0.04em; margin-top: 2px;
}
.conn-badge--connected { background: rgba(52,211,153,0.15); color: #34d399; }
.conn-badge--disconnected { background: var(--rs-bg-surface, #0a0a0a); color: var(--rs-text-muted, #525252); }
.conn-badge--soon { background: transparent; color: var(--rs-text-muted, #525252); border: 1px solid var(--rs-border, #262626); }
.conn-meta { font-size: 0.72rem; color: var(--rs-text-muted, #525252); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.conn-btn {
  padding: 5px 12px; border-radius: 6px; border: none; font-size: 0.72rem; font-weight: 600;
  cursor: pointer; transition: opacity 0.15s; flex-shrink: 0; white-space: nowrap;
}
.conn-btn:hover { opacity: 0.85; }
.conn-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.conn-btn--connect { background: linear-gradient(135deg, #06b6d4, #7c3aed); color: white; }
.conn-btn--disconnect { background: rgba(239,68,68,0.12); color: #f87171; }
.conn-share-section {
  margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--rs-border, #262626);
}
.conn-share-label { font-size: 0.68rem; font-weight: 600; color: var(--rs-text-secondary); text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 4px; }
.conn-share-opt {
  display: inline-flex; align-items: center; gap: 4px; font-size: 0.78rem; color: var(--rs-text-primary);
  margin-right: 12px; margin-bottom: 2px; cursor: pointer;
}
.conn-share-opt input[type="checkbox"] { margin: 0; accent-color: #06b6d4; }

/* Guardian puzzle pieces SVG visualization */
.guardian-viz { display: flex; align-items: center; justify-content: center; gap: 8px; margin: 12px 0; }
.guardian-viz svg { transition: all 0.5s ease; }
.piece-empty { fill: #334155; opacity: 0.4; }
.piece-pending { fill: #fbbf24; opacity: 0.7; }
.piece-accepted { fill: #34d399; }
.piece-accepted.glow { filter: drop-shadow(0 0 6px rgba(52,211,153,0.6)); }
.key-outline { fill: none; stroke: #334155; stroke-width: 2; stroke-dasharray: 4 2; opacity: 0.4; }
.key-assembled { fill: url(#keyGrad); stroke: #34d399; stroke-width: 2; stroke-dasharray: none; opacity: 1; filter: drop-shadow(0 0 8px rgba(52,211,153,0.5)); }
@keyframes piece-slide { from { transform: translateX(0); } to { transform: translateX(var(--slide-x, 0px)) translateY(var(--slide-y, 0px)); } }
@keyframes key-glow { 0% { filter: drop-shadow(0 0 4px rgba(52,211,153,0.3)); } 100% { filter: drop-shadow(0 0 12px rgba(52,211,153,0.7)); } }

/* Recovery active banner */
.recovery-active-banner {
  display: flex; align-items: center; gap: 8px; padding: 10px 14px; border-radius: 8px;
  background: rgba(52,211,153,0.08); border: 1px solid rgba(52,211,153,0.2);
  color: #34d399; font-size: 0.82rem; font-weight: 500; margin-bottom: 12px;
}
.recovery-active-banner svg { flex-shrink: 0; }

/* Drill section */
.drill-section { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--rs-border); }
.drill-btn {
  padding: 8px 16px; border-radius: 8px; border: 1px solid rgba(251,191,36,0.3);
  background: rgba(251,191,36,0.08); color: #fbbf24; font-size: 0.82rem; font-weight: 600;
  cursor: pointer; transition: all 0.2s; width: 100%;
}
.drill-btn:hover { background: rgba(251,191,36,0.15); border-color: rgba(251,191,36,0.5); }
.drill-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.drill-timestamp { font-size: 0.72rem; color: var(--rs-text-muted); margin-top: 6px; }
.drill-live { padding: 12px; border-radius: 8px; background: var(--rs-bg-hover); border: 1px solid var(--rs-border); margin-top: 8px; }
.drill-live-title { font-size: 0.82rem; font-weight: 600; color: #fbbf24; margin-bottom: 8px; }
.drill-success { color: #34d399; font-weight: 600; font-size: 0.85rem; text-align: center; margin: 12px 0; }

/* Solo walkthrough overlay */
.walkthrough-overlay {
  position: fixed; inset: 0; background: rgba(0,0,0,0.7);
  -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
  display: flex; align-items: center; justify-content: center; z-index: 10001;
  animation: fadeIn 0.3s;
}
.walkthrough-card {
  background: var(--rs-bg-surface, #1e1e2e); border: 1px solid var(--rs-border, #334155);
  border-radius: 16px; padding: 2rem; max-width: 400px; width: 90%;
  text-align: center; animation: slideUp 0.3s;
}
.walkthrough-icon { font-size: 3rem; margin-bottom: 0.5rem; transition: all 0.5s ease; }
.walkthrough-icon.red { filter: drop-shadow(0 0 12px rgba(239,68,68,0.6)); }
.walkthrough-icon.green { filter: drop-shadow(0 0 12px rgba(52,211,153,0.6)); }
.walkthrough-icon.amber { filter: drop-shadow(0 0 12px rgba(251,191,36,0.6)); }
.walkthrough-title {
  font-size: 1.1rem; font-weight: 600; margin-bottom: 0.5rem;
  background: linear-gradient(135deg, #06b6d4, #7c3aed); -webkit-background-clip: text; -webkit-text-fill-color: transparent;
}
.walkthrough-body { color: var(--rs-text-secondary, #94a3b8); font-size: 0.85rem; line-height: 1.6; margin-bottom: 1.5rem; }
.walkthrough-progress { display: flex; gap: 6px; justify-content: center; margin-bottom: 1rem; }
.walkthrough-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--rs-border, #334155); transition: background 0.3s; }
.walkthrough-dot.active { background: #06b6d4; }
.walkthrough-dot.done { background: #34d399; }
.walkthrough-nav { display: flex; gap: 8px; justify-content: center; }
.walkthrough-btn {
  padding: 8px 20px; border-radius: 8px; border: none; font-size: 0.85rem; font-weight: 600;
  cursor: pointer; transition: all 0.2s;
}
.walkthrough-btn--next { background: linear-gradient(135deg, #06b6d4, #7c3aed); color: white; }
.walkthrough-btn--next:hover { transform: translateY(-1px); }
.walkthrough-btn--skip { background: transparent; color: var(--rs-text-muted, #64748b); border: 1px solid var(--rs-border, #334155); }
.walkthrough-btn--skip:hover { color: var(--rs-text-primary); }

/* Recovery section urgent pulsing */
.status-dot.pending.recovery-urgent { animation: recovery-pulse 2s ease-in-out infinite; }
@keyframes recovery-pulse { 0%,100% { box-shadow: 0 0 4px rgba(248,113,113,0.4); } 50% { box-shadow: 0 0 10px rgba(248,113,113,0.8); } }
`;

const ONBOARDING_STYLES = `
.onboarding-modal { max-width: 440px; }
.onboarding-card {
  border: 1px solid var(--rs-border); border-radius: 12px;
  padding: 1rem 1.25rem; display: flex; gap: 1rem; align-items: flex-start;
  text-align: left; margin-top: 0.75rem; transition: border-color 0.2s;
}
.onboarding-card--primary {
  border-color: rgba(6,182,212,0.4);
  background: linear-gradient(135deg, rgba(6,182,212,0.06), rgba(124,58,237,0.06));
}
.onboarding-card-icon { font-size: 2rem; flex-shrink: 0; margin-top: 2px; }
.onboarding-card-content { flex: 1; min-width: 0; }
.onboarding-card-title {
  font-size: 1rem; font-weight: 600; color: var(--rs-text-primary); margin-bottom: 4px;
}
.onboarding-card-desc {
  font-size: 0.85rem; color: var(--rs-text-secondary); line-height: 1.5;
}
.onboarding-card-hint {
  font-size: 0.8rem; color: #06b6d4; margin-top: 6px; font-weight: 500;
}
.onboarding-later-hint {
  font-size: 0.75rem; color: var(--rs-text-muted); margin-top: 0.5rem;
}
.qr-container { text-align: center; margin: 0.75rem 0; }
.link-copy-row { display: flex; gap: 8px; align-items: stretch; }
.onboarding-expire-hint {
  font-size: 0.75rem; color: var(--rs-text-muted); text-align: center; margin-top: 0.5rem;
}
`;

const SPACES_STYLES = `
.rstack-spaces-overlay {
  position: fixed; inset: 0; background: rgba(0,0,0,0.6);
  -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px); display: flex; align-items: center;
  justify-content: center; z-index: 10000; animation: fadeIn 0.2s;
}
.spaces-modal {
  background: var(--rs-bg-surface); border: 1px solid var(--rs-border);
  border-radius: 16px; padding: 2rem; max-width: 720px; width: 92%;
  max-height: 85vh; overflow-y: auto; color: var(--rs-text-primary); position: relative;
  box-shadow: var(--rs-shadow-lg); animation: slideUp 0.3s;
}
.spaces-modal h2 {
  font-size: 1.5rem; margin-bottom: 0.5rem;
  background: linear-gradient(135deg, #06b6d4, #7c3aed);
  -webkit-background-clip: text; -webkit-text-fill-color: transparent;
}
.spaces-section-label {
  font-size: 0.75rem; font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.05em; color: var(--rs-text-muted); margin: 1rem 0 0.5rem;
}
.spaces-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 12px;
}
.space-card {
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  padding: 20px 12px; border-radius: 12px; cursor: pointer;
  background: var(--rs-bg-hover); border: 1px solid var(--rs-btn-secondary-bg);
  transition: all 0.2s; text-align: center; color: var(--rs-text-primary); font-family: inherit;
  font-size: inherit;
}
.space-card:hover {
  background: var(--rs-btn-secondary-bg); border-color: rgba(6,182,212,0.4);
  transform: translateY(-2px); box-shadow: var(--rs-shadow-md);
}
.space-card-initial {
  width: 48px; height: 48px; border-radius: 50%;
  background: linear-gradient(135deg, #06b6d4, #7c3aed);
  display: flex; align-items: center; justify-content: center;
  font-size: 1.3rem; font-weight: 700; color: white;
}
.space-card-name {
  font-size: 0.9rem; font-weight: 600; max-width: 100%;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.space-card-meta {
  display: flex; gap: 6px; flex-wrap: wrap; justify-content: center;
}
.space-vis {
  font-size: 0.7rem; color: var(--rs-text-secondary); background: var(--rs-bg-hover);
  padding: 2px 8px; border-radius: 10px;
}
.space-vis.vis-public { background: rgba(52,211,153,0.15); color: #34d399; }
.space-vis.vis-private { background: rgba(248,113,113,0.15); color: #f87171; }
.space-vis.vis-permissioned { background: rgba(251,191,36,0.15); color: #fbbf24; }
.space-card.vis-public { border-color: rgba(52,211,153,0.3); }
.space-card.vis-private { border-color: rgba(248,113,113,0.3); }
.space-card.vis-permissioned { border-color: rgba(251,191,36,0.3); }
.space-role {
  font-size: 0.7rem; color: #06b6d4; background: rgba(6,182,212,0.1);
  padding: 2px 8px; border-radius: 10px; font-weight: 600;
}
.space-card--create {
  border-style: dashed; border-color: var(--rs-border);
}
.space-card--create .space-card-initial {
  background: var(--rs-btn-secondary-bg); font-size: 1.5rem;
}
.space-card--create:hover .space-card-initial {
  background: linear-gradient(135deg, #06b6d4, #7c3aed);
}
`;

const WALLETS_STYLES = `
.rstack-wallets-overlay {
  position: fixed; inset: 0; background: rgba(0,0,0,0.6);
  -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px); display: flex; align-items: center;
  justify-content: center; z-index: 10000; animation: fadeIn 0.2s;
}
.wallets-modal {
  background: var(--rs-bg-surface); border: 1px solid var(--rs-border);
  border-radius: 16px; padding: 2rem; max-width: 480px; width: 92%;
  max-height: 85vh; overflow-y: auto; color: var(--rs-text-primary); position: relative;
  box-shadow: var(--rs-shadow-lg); animation: slideUp 0.3s;
}
.wallets-modal h2 {
  font-size: 1.5rem; margin-bottom: 0.5rem;
  background: linear-gradient(135deg, #06b6d4, #7c3aed);
  -webkit-background-clip: text; -webkit-text-fill-color: transparent;
}
.wallet-section-label {
  font-size: 0.75rem; font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.05em; color: var(--rs-text-muted); margin: 1.25rem 0 0.5rem;
}
.wallet-identity-card {
  display: flex; align-items: center; gap: 12px;
  padding: 14px 16px; border-radius: 12px;
  background: var(--rs-bg-hover); border: 1px solid rgba(6,182,212,0.3);
}
.wallet-identity-avatar {
  width: 42px; height: 42px; border-radius: 50%; flex-shrink: 0;
  background: linear-gradient(135deg, #06b6d4, #7c3aed);
  display: flex; align-items: center; justify-content: center;
  font-weight: 700; font-size: 0.9rem; color: white;
}
.wallet-identity-info { flex: 1; min-width: 0; }
.wallet-identity-name {
  font-weight: 600; font-size: 0.95rem; color: var(--rs-text-primary);
}
.wallet-identity-did {
  font-size: 0.75rem; color: var(--rs-text-muted); font-family: monospace;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.wallet-badge {
  font-size: 0.7rem; font-weight: 600; color: #06b6d4;
  background: rgba(6,182,212,0.12); padding: 3px 10px; border-radius: 10px;
  white-space: nowrap;
}
.wallet-providers { display: flex; flex-direction: column; gap: 8px; }
.wallet-provider-card {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 14px; border-radius: 10px;
  background: var(--rs-bg-hover); border: 1px solid var(--rs-border);
  transition: border-color 0.2s;
}
.wallet-provider-card:hover { border-color: rgba(6,182,212,0.4); }
.wallet-provider-icon {
  width: 32px; height: 32px; border-radius: 8px; flex-shrink: 0;
}
.wallet-provider-info { flex: 1; min-width: 0; }
.wallet-provider-name {
  font-weight: 600; font-size: 0.875rem; color: var(--rs-text-primary);
}
.wallet-provider-addr {
  font-size: 0.75rem; color: #34d399; font-family: monospace;
}
.wallet-connect-btn {
  font-size: 0.75rem; font-weight: 600; padding: 4px 14px;
  border-radius: 8px; border: 1px solid rgba(6,182,212,0.4);
  background: rgba(6,182,212,0.1); color: #06b6d4; cursor: pointer;
  transition: all 0.15s; font-family: inherit;
}
.wallet-connect-btn:hover { background: rgba(6,182,212,0.2); }
.wallet-connect-btn:disabled { opacity: 0.6; cursor: default; }
.wallet-empty {
  text-align: center; padding: 1rem 0;
  color: var(--rs-text-secondary); font-size: 0.85rem; line-height: 1.6;
}
.wallet-footer {
  margin-top: 1.5rem; padding-top: 1rem;
  border-top: 1px solid var(--rs-border); text-align: center;
}
.wallet-open-btn {
  font-size: 0.875rem; font-weight: 600; padding: 8px 24px;
  border-radius: 10px; border: none; cursor: pointer;
  background: linear-gradient(135deg, #06b6d4, #7c3aed); color: white;
  transition: all 0.2s; font-family: inherit;
}
.wallet-open-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(6,182,212,0.3); }
`;
