/**
 * <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 } from "../url-helpers";
import { resetDocBridge, isEncryptedBackupEnabled, setEncryptedBackupEnabled } from "../local-first/encryptid-bridge";

const SESSION_KEY = "encryptid_session";
const ENCRYPTID_URL = "https://auth.rspace.online";
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: {
			authLevel: number;
			capabilities: { encrypt: boolean; sign: boolean; wallet: boolean };
		};
	};
}

// ── 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 {
	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);
}

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

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

	// Detect current space
	const currentSpace = _getCurrentSpace();
	if (currentSpace !== "demo") return; // Already on a non-demo space

	// 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;
			const moduleId = _getCurrentModule();
			window.location.replace(_navUrl(data.slug, moduleId));
		})
		.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 _getCurrentSpace(): string {
	if (_isSubdomain()) return window.location.host.split(":")[0].split(".")[0];
	return window.location.pathname.split("/").filter(Boolean)[0] || "demo";
}
function _getCurrentModule(): string {
	const parts = window.location.pathname.split("/").filter(Boolean);
	return _isSubdomain() ? (parts[0] || "rspace") : (parts[1] || "rspace");
}
function _navUrl(space: string, moduleId: string): string {
	const h = window.location.host.split(":")[0].split(".");
	const onSub = h.length >= 3 && h.slice(-2).join(".") === "rspace.online" && !_RESERVED.includes(h[0]);
	if (onSub) {
		if (h[0] === space) return "/" + moduleId;
		return window.location.protocol + "//" + space + "." + h.slice(-2).join(".") + "/" + moduleId;
	}
	if (window.location.host.includes("rspace.online") && !window.location.host.startsWith("www")) {
		return window.location.protocol + "//" + space + ".rspace.online/" + 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);
		}
	}

	disconnectedCallback() {
	}

	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 { token: newToken } = await res.json();
			if (newToken) {
				const payload = parseJWT(newToken);
				storeSession(newToken, (payload.username as string) || username, (payload.did as string) || did);
				this.#render();
		this.dispatchEvent(new CustomEvent("auth-change", { bubbles: true, composed: true }));
			}
		} 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();

			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>
						<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>
						<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-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.#render();
						this.dispatchEvent(new CustomEvent("auth-change", { bubbles: true, composed: true }));
					} else if (action === "my-account") {
						this.#showAccountModal();
					} else if (action === "my-spaces") {
						this.#showSpacesModal();
					}
				});
			});

			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 } }));
				});
			}
		} 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 method: show the auth modal programmatically */
	showAuthModal(callbacks?: { onSuccess?: () => void; onCancel?: () => void }): 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();
		};

		const signinHTML = () => `
			<style>${MODAL_STYLES}</style>
			<div class="auth-modal">
				<button class="close-btn" data-action="cancel">&times;</button>
				<h2>Sign up / Sign in</h2>
				<p>Secure, passwordless authentication powered by passkeys.</p>
				<div class="actions actions--stack">
					<button class="btn btn--primary" data-action="signin">🔑 Sign In with Passkey</button>
					<button class="btn btn--outline" data-action="switch-register">🔐 Create New Account</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 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" />
				<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;
			const btn = overlay.querySelector('[data-action="signin"]') as HTMLButtonElement;
			errEl.textContent = "";
			btn.disabled = true;
			btn.innerHTML = '<span class="spinner"></span> Authenticating...';

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

				const credential = (await navigator.credentials.get({
					publicKey: {
						challenge: new Uint8Array(base64urlToBuffer(serverOptions.challenge)),
						rpId: serverOptions.rpId || "rspace.online",
						userVerification: "required",
						timeout: 60000,
					},
				})) 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 }));
				callbacks?.onSuccess?.();
				// Auto-redirect to personal space
				autoResolveSpace(data.token, data.username || "");
			} catch (err: any) {
				btn.disabled = false;
				btn.innerHTML = "🔑 Sign In with Passkey";
				errEl.textContent = err.name === "NotAllowedError" ? "Authentication was cancelled." : err.message || "Authentication failed.";
			}
		};

		const handleRegister = async () => {
			const usernameInput = overlay.querySelector("#auth-username") as HTMLInputElement;
			const errEl = overlay.querySelector("#auth-error") as HTMLElement;
			const btn = overlay.querySelector('[data-action="register"]') as HTMLButtonElement;
			const username = usernameInput.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,
					}),
				});
				const data = await completeRes.json();
				if (!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 }));
				callbacks?.onSuccess?.();
				// Auto-redirect to personal space
				autoResolveSpace(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="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";
				render();
			});
			overlay.querySelector("#auth-username")?.addEventListener("keydown", (e) => {
				if ((e as KeyboardEvent).key === "Enter") handleRegister();
			});
			overlay.addEventListener("click", (e) => {
				if (e.target === overlay) {
					close();
					callbacks?.onCancel?.();
				}
			});
		};

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

	// ── Account modal (consolidated) ──

	#showAccountModal(): 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 = null;

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

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

		let addresses: { id: string; street: string; city: string; state: string; zip: string; country: string }[] = [];
		let addressesLoaded = false;
		let addressesLoading = false;

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

		const close = () => 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()}
					${renderAddressSection()}

					<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>

						<div class="error" id="acct-error"></div>
				</div>
			`;
			attachListeners();
		};

		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>`;
				}
			}
			return `
				<div class="account-section${isOpen ? " open" : ""}${done === false ? " section--warning" : ""}">
					<div class="account-section-header" data-section="email">
						<span>${statusDot(done)} ✉️ Email</span>
						<span class="section-arrow">${isOpen ? "▾" : "▸"}</span>
					</div>
					${body}
				</div>`;
		};

		const renderDeviceSection = () => {
			const isOpen = openSection === "device";
			const done = acctStatus ? acctStatus.multiDevice : null;
			const body = isOpen ? `
				<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.${acctStatus ? ` <span style="color:var(--rs-text-muted)">(${acctStatus.credentialCount} passkey${acctStatus.credentialCount !== 1 ? "s" : ""} registered)</span>` : ""}</p>
					<div class="actions actions--stack">
						<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 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 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>` : "";

					const infoHTML = guardians.length < 2
						? `<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:#34d399">Social recovery is active. ${guardiansThreshold} of ${guardians.length} guardians needed to recover your account.</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>
							${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}
							<div class="error" id="recovery-error"></div>
						</div>`;
				}
			}
			const done = acctStatus ? acctStatus.socialRecovery : null;
			return `
				<div class="account-section${isOpen ? " open" : ""}${done === false ? " section--warning" : ""}">
					<div class="account-section-header" data-section="recovery">
						<span>${statusDot(done)} 🛡️ Social Recovery</span>
						<span class="section-arrow">${isOpen ? "▾" : "▸"}</span>
					</div>
					${body}
				</div>`;
		};

		const renderAddressSection = () => {
			const isOpen = openSection === "address";
			let body = "";
			if (isOpen) {
				if (addressesLoading) {
					body = `<div class="account-section-body"><div style="text-align:center;padding:1rem;color:var(--rs-text-secondary)"><span class="spinner"></span> Loading addresses...</div></div>`;
				} else {
					const listHTML = addresses.length > 0
						? `<div class="contact-list">${addresses.map(a => `
							<div class="contact-item">
								<div style="display:flex;flex-direction:column;gap:2px;min-width:0;flex:1;font-size:0.85rem">
									<span>${a.street.replace(/</g, "&lt;")}</span>
									<span style="color:var(--rs-text-secondary)">${a.city.replace(/</g, "&lt;")}, ${a.state.replace(/</g, "&lt;")} ${a.zip.replace(/</g, "&lt;")} ${a.country.replace(/</g, "&lt;")}</span>
								</div>
								<button class="contact-remove" data-remove-address="${a.id}">&times;</button>
							</div>
						`).join("")}</div>` : "";

					body = `
						<div class="account-section-body">
							<div class="address-form">
								<input class="input" id="acct-street" type="text" placeholder="Street address" />
								<div class="address-row">
									<input class="input" id="acct-city" type="text" placeholder="City" />
									<input class="input" id="acct-state" type="text" placeholder="State" />
								</div>
								<div class="address-row">
									<input class="input" id="acct-zip" type="text" placeholder="ZIP / Postal code" />
									<input class="input" id="acct-country" type="text" placeholder="Country" />
								</div>
								<button class="btn btn--primary" data-action="save-address" style="align-self:flex-start">Save Address</button>
							</div>
							${listHTML}
							<div class="error" id="address-error"></div>
						</div>`;
				}
			}
			return `
				<div class="account-section${isOpen ? " open" : ""}">
					<div class="account-section-header" data-section="address">
						<span>🏠 Postal Address</span>
						<span class="section-arrow">${isOpen ? "▾" : "▸"}</span>
					</div>
					${body}
				</div>`;
		};

		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 loadAddresses = async () => {
			if (addressesLoaded || addressesLoading) return;
			addressesLoading = true;
			render();
			try {
				const res = await fetch(`${ENCRYPTID_URL}/api/user/addresses`, {
					headers: { Authorization: `Bearer ${getAccessToken()}` },
				});
				if (res.ok) {
					const data = await res.json();
					addresses = (data.addresses || []).map((a: any) => {
						try {
							const decoded = JSON.parse(atob(a.ciphertext));
							return { id: a.id, ...decoded };
						} catch {
							return { id: a.id, street: "", city: "", state: "", zip: "", country: "" };
						}
					});
				}
			} catch { /* offline */ }
			addressesLoaded = true;
			addressesLoading = 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 === "address") loadAddresses();
					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;
					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; }
					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 });
					if (acctStatus) { acctStatus.guardianCount = guardians.length; acctStatus.socialRecovery = guardians.length >= 2; }
					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);
						if (acctStatus) { acctStatus.guardianCount = guardians.length; acctStatus.socialRecovery = guardians.length >= 2; }
						render();
					} catch (e: any) {
						if (err) err.textContent = e.message;
					}
				});
			});

			// Address: save
			overlay.querySelector('[data-action="save-address"]')?.addEventListener("click", async () => {
				const street = (overlay.querySelector("#acct-street") as HTMLInputElement)?.value.trim() || "";
				const city = (overlay.querySelector("#acct-city") as HTMLInputElement)?.value.trim() || "";
				const state = (overlay.querySelector("#acct-state") as HTMLInputElement)?.value.trim() || "";
				const zip = (overlay.querySelector("#acct-zip") as HTMLInputElement)?.value.trim() || "";
				const country = (overlay.querySelector("#acct-country") as HTMLInputElement)?.value.trim() || "";
				const err = overlay.querySelector("#address-error") as HTMLElement;
				const btn = overlay.querySelector('[data-action="save-address"]') as HTMLButtonElement;

				if (!street || !city) { err.textContent = "Street and city are required."; return; }
				err.textContent = "";
				btn.disabled = true; btn.innerHTML = '<span class="spinner"></span> Saving...';

				const payload = { street, city, state, zip, country };
				const ciphertext = btoa(JSON.stringify(payload));

				try {
					const res = await fetch(`${ENCRYPTID_URL}/api/user/addresses`, {
						method: "POST",
						headers: { "Content-Type": "application/json", Authorization: `Bearer ${getAccessToken()}` },
						body: JSON.stringify({ ciphertext }),
					});
					const data = await res.json();
					if (!res.ok) throw new Error(data.error || "Failed to save address");
					addresses.push({ id: data.id || data.address?.id || String(Date.now()), ...payload });
					render();
				} catch (e: any) {
					btn.disabled = false; btn.innerHTML = "Save Address";
					err.textContent = e.message;
				}
			});

			// Address: remove
			overlay.querySelectorAll("[data-remove-address]").forEach(el => {
				el.addEventListener("click", async () => {
					const id = (el as HTMLElement).dataset.removeAddress!;
					const err = overlay.querySelector("#address-error") as HTMLElement;
					try {
						const res = await fetch(`${ENCRYPTID_URL}/api/user/addresses/${id}`, {
							method: "DELETE",
							headers: { Authorization: `Bearer ${getAccessToken()}` },
						});
						if (!res.ok) throw new Error("Failed to remove address");
						addresses = addresses.filter(a => a.id !== id);
						render();
					} catch (e: any) {
						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 } }));
				});
			}

		};

		document.body.appendChild(overlay);
		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: 100;
}
.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); }

/* 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); }

/* 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;
}

/* 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);
  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%;
  text-align: center; color: var(--rs-text-primary); box-shadow: var(--rs-shadow-lg);
  animation: slideUp 0.3s;
}
.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 { 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);
  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); }
`;

const SPACES_STYLES = `
.rstack-spaces-overlay {
  position: fixed; inset: 0; background: rgba(0,0,0,0.6);
  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);
}
`;
