/**
 * Shell HTML renderer.
 *
 * Wraps module content in the shared rSpace layout: header with app/space
 * switchers + identity, <main> with module content, shell script + styles.
 */

import { resolve } from "node:path";
import type { ModuleInfo, SubPageInfo, OnboardingAction } from "../shared/module";
import { getDocumentData } from "./community-store";

// ── Browser compatibility gate + polyfills (inline, runs before ES modules) ──
const COMPAT_GATE = `<script>(function(){if(typeof WebAssembly!=="object"||!("noModule" in HTMLScriptElement.prototype)){document.documentElement.innerHTML='<body style="margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;background:#0f172a;color:#e2e8f0;font-family:system-ui,sans-serif"><div style="max-width:480px;text-align:center;padding:32px"><h1 style="font-size:1.5rem;margin-bottom:16px">Browser Not Supported</h1><p style="color:#94a3b8;line-height:1.6;margin-bottom:24px">rSpace requires a modern browser with WebAssembly and ES Module support.</p><p style="color:#94a3b8;line-height:1.6">Please use <strong>Chrome 92+</strong>, <strong>Edge 92+</strong>, <strong>Safari 15+</strong>, or <strong>Firefox 89+</strong>.</p></div></body>';document.querySelectorAll("script[type=module]").forEach(function(s){s.remove()});return}})()</script>`;
const COMPAT_POLYFILLS = `<script>(function(){if(typeof structuredClone!=="function"){window.structuredClone=function(v){return JSON.parse(JSON.stringify(v))}}if(typeof AbortSignal.timeout!=="function"){AbortSignal.timeout=function(ms){var c=new AbortController();setTimeout(function(){c.abort(new DOMException("The operation was aborted due to timeout","TimeoutError"))},ms);return c.signal}}if(typeof crypto!=="undefined"&&typeof crypto.randomUUID!=="function"){crypto.randomUUID=function(){var b=crypto.getRandomValues(new Uint8Array(16));b[6]=(b[6]&0x0f)|0x40;b[8]=(b[8]&0x3f)|0x80;var h=Array.from(b,function(x){return x.toString(16).padStart(2,"0")}).join("");return h.slice(0,8)+"-"+h.slice(8,12)+"-"+h.slice(12,16)+"-"+h.slice(16,20)+"-"+h.slice(20)}}})()</script>`;

// ── Dynamic per-module favicon (inline, runs after body parse) ──
// Badge map mirrors MODULE_BADGES from rstack-app-switcher.ts — kept in sync manually.
const FAVICON_BADGE_MAP: Record<string, { badge: string; color: string }> = {
	rspace:    { badge: "r🎨", color: "#5eead4" },
	rdocs:     { badge: "r📄", color: "#a5b4fc" },
	rdesign:   { badge: "r🎨", color: "#c4b5fd" },
	rnotes:    { badge: "r📝", color: "#fcd34d" },
	rpubs:     { badge: "r📖", color: "#fda4af" },
	rsheets:   { badge: "r📑", color: "#86efac" },
	rsplat:    { badge: "r🔮", color: "#d8b4fe" },
	rswag:     { badge: "r👕", color: "#fda4af" },
	rchats:    { badge: "r🗨", color: "#6ee7b7" },
	rforum:    { badge: "r💬", color: "#fcd34d" },
	rinbox:    { badge: "r📨", color: "#a5b4fc" },
	rmail:     { badge: "r✉️", color: "#93c5fd" },
	rmeets:    { badge: "r📹", color: "#67e8f9" },
	rcal:      { badge: "r📅", color: "#7dd3fc" },
	rchoices:  { badge: "r☑️", color: "#f0abfc" },
	rschedule: { badge: "r⏱", color: "#a5b4fc" },
	rtasks:    { badge: "r📋", color: "#cbd5e1" },
	rtime:     { badge: "r⏳", color: "#a78bfa" },
	rvote:     { badge: "r🗳", color: "#c4b5fd" },
	crowdsurf: { badge: "r🏄", color: "#fde68a" },
	rnetwork:  { badge: "r🌐", color: "#93c5fd" },
	rsocials:  { badge: "r📢", color: "#7dd3fc" },
	rexchange: { badge: "r💱", color: "#fde047" },
	rflows:    { badge: "r🌊", color: "#bef264" },
	rwallet:   { badge: "r💰", color: "#fde047" },
	rcart:     { badge: "r🛒", color: "#fdba74" },
	rauctions: { badge: "r🎭", color: "#fca5a5" },
	rgov:      { badge: "r⚖️", color: "#94a3b8" },
	rphotos:   { badge: "r📸", color: "#f9a8d4" },
	rfiles:    { badge: "r📁", color: "#67e8f9" },
	rtube:     { badge: "r🎬", color: "#f9a8d4" },
	rbooks:    { badge: "r📚", color: "#fda4af" },
	rmaps:     { badge: "r🗺", color: "#86efac" },
	rtrips:    { badge: "r✈️", color: "#6ee7b7" },
	rbnb:      { badge: "r🏠", color: "#fbbf24" },
	rvnb:      { badge: "r🚐", color: "#a5f3fc" },
	rdata:     { badge: "r📊", color: "#d8b4fe" },
	ragents:   { badge: "r🤖", color: "#6ee7b7" },
	rids:      { badge: "r🪪", color: "#6ee7b7" },
	rcred:     { badge: "r⭐", color: "#d97706" },
	rfeeds:    { badge: "r📡", color: "#67e8f9" },
	rstack:    { badge: "r✨", color: "#c4b5fd" },
};

/** Generate an inline script that sets the favicon to the module's badge SVG */
function faviconScript(moduleId: string): string {
	const b = FAVICON_BADGE_MAP[moduleId];
	if (!b) return '';
	const json = JSON.stringify(b);
	return `<script>(function(){var b=${json};var e=b.badge.slice(1);var s='<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64"><rect width="64" height="64" rx="14" fill="'+b.color+'"/><text x="32" y="44" text-anchor="middle" font-size="26" font-family="sans-serif"><tspan fill="#dc8300">r</tspan><tspan>'+e+'</tspan></text></svg>';var l=document.querySelector('link[rel="icon"]');if(l)l.href="data:image/svg+xml,"+encodeURIComponent(s)})()</script>`;
}

// ── Content-hash cache busting ──
let moduleHashes: Record<string, string> = {};
try {
	const hashFile = resolve(import.meta.dir, "../dist/module-hashes.json");
	moduleHashes = JSON.parse(await Bun.file(hashFile).text());
} catch { /* dev mode or first run — no hashes yet */ }

/** Append ?v=<content-hash> to module/shell asset URLs in rendered HTML. */
export function versionAssetUrls(html: string): string {
	return html.replace(
		/(["'])(\/(?:modules\/[^"'?]+\.(?:js|css)|shell\.js|shell\.css|theme\.css))(?:\?v=[^"']*)?(\1)/g,
		(_, q, path, qEnd) => {
			const hash = moduleHashes[path];
			return hash ? `${q}${path}?v=${hash}${qEnd}` : `${q}${path}${qEnd}`;
		}
	);
}

/** Extract enabledModules and encryption status from a loaded space. */
export function getSpaceShellMeta(spaceSlug: string): { enabledModules: string[] | null; spaceEncrypted: boolean } {
	const data = getDocumentData(spaceSlug);
	return {
		enabledModules: data?.meta?.enabledModules ?? null,
		spaceEncrypted: !!data?.meta?.encrypted,
	};
}

/**
 * Render a lightweight JSON fragment for tab-cache.
 * Returns only the body HTML, title, scripts, and styles — no shell chrome.
 * Used by TabCache's ?fragment=1 requests to avoid re-rendering the full 2000-line shell.
 */
export function renderFragment(opts: {
	title: string;
	body: string;
	scripts?: string;
	styles?: string;
}): string {
	// Parse script srcs from the scripts string
	const scriptSrcs: string[] = [];
	const scriptRe = /src="([^"]+)"/g;
	let m: RegExpExecArray | null;
	if (opts.scripts) {
		while ((m = scriptRe.exec(opts.scripts)) !== null) {
			scriptSrcs.push(m[1]);
		}
	}

	// Parse stylesheet hrefs from the styles string
	const styleSrcs: string[] = [];
	const styleRe = /href="([^"]+)"/g;
	if (opts.styles) {
		while ((m = styleRe.exec(opts.styles)) !== null) {
			styleSrcs.push(m[1]);
		}
	}

	// Extract inline <style> content
	const inlineStyles: string[] = [];
	const inlineRe = /<style[^>]*>([\s\S]*?)<\/style>/g;
	if (opts.styles) {
		while ((m = inlineRe.exec(opts.styles)) !== null) {
			if (m[1].trim()) inlineStyles.push(m[1]);
		}
	}

	return JSON.stringify({
		title: opts.title,
		body: opts.body,
		scripts: scriptSrcs,
		styles: styleSrcs,
		inlineStyles,
	});
}

export interface ShellOptions {
	/** Page <title> */
	title: string;
	/** Current module ID (highlighted in app switcher) */
	moduleId: string;
	/** Current space slug */
	spaceSlug: string;
	/** Space display name */
	spaceName?: string;
	/** Module HTML content to inject into <main> */
	body: string;
	/** Additional <script type="module"> tags for module-specific JS */
	scripts?: string;
	/** Additional <link>/<style> tags for module-specific CSS */
	styles?: string;
	/** List of available modules (for app switcher) */
	modules: ModuleInfo[];
	/** Theme for the header: 'dark' or 'light' */
	theme?: "dark" | "light";
	/** Extra <head> content (meta tags, preloads, etc.) */
	head?: string;
	/** Space visibility level (for client-side access gate) */
	spaceVisibility?: string;
	/** Enabled modules for this space (null = all). Filters the app switcher. */
	enabledModules?: string[] | null;
	/** Whether this space has client-side encryption enabled */
	spaceEncrypted?: boolean;
	/** If true, return lightweight JSON fragment instead of full shell HTML */
	fragment?: boolean;
	/** Optional tab bar rendered below the subnav. */
	tabs?: Array<{ id: string; label: string; icon?: string }>;
	/** Active tab ID (matched from URL path by server). First tab if omitted. */
	activeTab?: string;
	/** Base path for tab links (default: /{space}/{moduleId}). Set to e.g. "/{space}/rnetwork/crm" for sub-pages. */
	tabBasePath?: string;
	/** Whether this page is being served via subdomain routing (omit space from paths) */
	isSubdomain?: boolean;
}

// In production, always use subdomain-style paths (/{moduleId}) unless explicitly overridden
const IS_PRODUCTION = process.env.NODE_ENV === "production";

/**
 * Build a full external URL for a space + path, using subdomain routing in production.
 * E.g. buildSpaceUrl("demo", "/rcart/pay/123", host) → "https://demo.rspace.online/rcart/pay/123"
 */
export function buildSpaceUrl(space: string, path: string, host?: string): string {
	if (IS_PRODUCTION) {
		return `https://${space}.rspace.online${path}`;
	}
	// Dev/localhost
	const h = host || "localhost:3000";
	return `http://${h}/${space}${path}`;
}

/** Global flag set by middleware to signal fragment mode to renderShell. */
let _fragmentMode = false;
export function setFragmentMode(on: boolean) { _fragmentMode = on; }

export function renderShell(opts: ShellOptions): string {
	// Fragment mode: return lightweight JSON instead of full shell HTML.
	// Triggered by middleware setting the flag, or by opts.fragment.
	if (opts.fragment || _fragmentMode) {
		_fragmentMode = false; // Reset after use
		return renderFragment({
			title: opts.title,
			body: opts.body,
			scripts: opts.scripts,
			styles: opts.styles,
		});
	}

	const {
		title,
		moduleId,
		spaceSlug,
		spaceName,
		body,
		scripts = "",
		styles = "",
		modules,
		theme = "dark",
		head = "",
		spaceVisibility = "private",
	} = opts;

	// Auto-populate from space data when not explicitly provided
	const spaceMeta = (opts.enabledModules === undefined || opts.spaceEncrypted === undefined)
		? getSpaceShellMeta(spaceSlug)
		: null;
	const enabledModules = opts.enabledModules ?? spaceMeta?.enabledModules ?? null;
	const spaceEncrypted = opts.spaceEncrypted ?? spaceMeta?.spaceEncrypted ?? false;

	// Build scope config for client-side runtime
	const spaceData = getDocumentData(spaceSlug);
	const scopeOverrides = spaceData?.meta?.moduleScopeOverrides ?? {};

	// Filter modules by enabledModules (null = show all)
	const visibleModules = enabledModules
		? modules.filter(m => m.id === "rspace" || enabledModules.includes(m.id))
		: modules;
	const moduleListJSON = JSON.stringify(visibleModules);
	// Full catalog with enabled flags for "Manage rApps" panel
	const allModulesJSON = JSON.stringify(modules.map(m => ({
		...m,
		enabled: !enabledModules || enabledModules.includes(m.id) || m.id === "rspace",
	})));

	return versionAssetUrls(`<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
  <link rel="icon" type="image/png" href="/favicon.png">
  ${faviconScript(moduleId)}
  <link rel="manifest" href="/manifest.json">
  <meta name="theme-color" content="#0f172a" media="(prefers-color-scheme: dark)">
  <meta name="theme-color" content="#f5f5f0" media="(prefers-color-scheme: light)">
  <link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
  <meta name="mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-title" content="rSpace">
  <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
  <title>${escapeHtml(title)}</title>
  <meta name="description" content="rSpace — local-first community platform with 25+ composable apps. Encrypted, interoperable, self-sovereign.">
  <meta property="og:type" content="website">
  <meta property="og:title" content="${escapeAttr(title)}">
  <meta property="og:description" content="rSpace — local-first community platform with 25+ composable apps. Encrypted, interoperable, self-sovereign.">
  <meta property="og:image" content="https://rspace.online/og-image.png">
  <meta property="og:site_name" content="rSpace">
  <meta name="twitter:card" content="summary_large_image">
  <meta name="twitter:image" content="https://rspace.online/og-image.png">
  <script>(function(){var t=localStorage.getItem('canvas-theme');if(!t)t=matchMedia('(prefers-color-scheme:light)').matches?'light':'dark';document.documentElement.setAttribute('data-theme',t);var b=localStorage.getItem('canvas-bg')||'grid';document.documentElement.setAttribute('data-canvas-bg',b)})()</script>
  ${COMPAT_GATE}
  ${COMPAT_POLYFILLS}
  <link rel="stylesheet" href="/theme.css">
  <link rel="stylesheet" href="/shell.css">
  <style>
    /* When loaded inside an iframe (e.g. standalone domain redirecting back),
       hide the shell chrome — the parent rSpace page already provides it. */
    html.rspace-embedded .rstack-header { display: none !important; }
    html.rspace-embedded .rstack-tab-row { display: none !important; }
    html.rspace-embedded .rapp-subnav { display: none !important; }
    html.rspace-embedded .rapp-tabbar { display: none !important; }
    html.rspace-embedded #app { padding-top: 0 !important; }
    html.rspace-embedded .rspace-iframe-loading,
    html.rspace-embedded .rspace-iframe-error { top: 0 !important; }
  </style>
  <script>if (window.self !== window.parent) document.documentElement.classList.add('rspace-embedded');</script>
  ${styles}
  ${head}
  ${spaceSlug === 'demo' ? `<style>${WELCOME_CSS}</style>` : ''}
  <style>${ACCESS_GATE_CSS}</style>
  <style>${INFO_PANEL_CSS}</style>
  <style>${SUBNAV_CSS}</style>
  <style>${TABBAR_CSS}</style>
  <style>${BUG_REPORT_CSS}</style>
</head>
<body data-space-visibility="${escapeAttr(spaceVisibility)}" data-space-slug="${escapeAttr(spaceSlug)}" data-module-id="${escapeAttr(moduleId)}" data-scope-overrides="${escapeAttr(JSON.stringify(scopeOverrides))}">
  <div class="rspace-banner rspace-banner--install" id="pwa-install-banner" style="display:none">
    <span class="rspace-banner__text">Install rSpace app for the best experience</span>
    <button class="rspace-banner__action" id="pwa-install-btn">Install</button>
    <button class="rspace-banner__close" id="pwa-install-close" title="Dismiss">&times;</button>
  </div>
  <div class="rspace-banner rspace-banner--update" id="pwa-update-banner" style="display:none">
    <span class="rspace-banner__text">New version available</span>
    <button class="rspace-banner__action" id="pwa-update-btn">Update</button>
    <button class="rspace-banner__close" id="pwa-update-close" title="Dismiss">&times;</button>
  </div>
  <header class="rstack-header">
    <div class="rstack-header__left">
      <a href="/" style="display:flex;align-items:center;margin-right:4px"><img src="/favicon.png" alt="rSpace" class="rstack-header__logo"></a>
      <rstack-app-switcher current="${escapeAttr(moduleId)}"></rstack-app-switcher>
      <rstack-space-switcher current="${escapeAttr(spaceSlug)}" name="${escapeAttr(spaceName || spaceSlug)}"></rstack-space-switcher>${spaceEncrypted ? '<span class="rstack-header__encrypted" title="End-to-end encrypted space">&#x1F512;</span>' : ''}
      <div class="rstack-header__dropdown-wrap">
        <button class="rstack-header__history-btn" id="history-btn" title="History"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg></button>
        <rstack-history-panel></rstack-history-panel>
      </div>
      <button class="rstack-header__settings-btn" id="settings-btn" title="Space Settings"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></button>
    </div>
    <div class="rstack-header__center">
      <rstack-mi></rstack-mi>
    </div>
    <div class="rstack-header__right">
      <rstack-offline-indicator></rstack-offline-indicator>
      <rstack-comment-bell></rstack-comment-bell>
      <rstack-notification-bell></rstack-notification-bell>
      <rstack-chat-widget></rstack-chat-widget>
      <rstack-share-panel></rstack-share-panel>
      <rstack-identity></rstack-identity>
    </div>
  </header>
  <div class="rstack-tab-row">
    <rstack-tab-bar space="${escapeAttr(spaceSlug)}" active="" view-mode="flat">
      <rstack-collab-overlay module-id="${escapeAttr(moduleId)}" space="${escapeAttr(spaceSlug)}"></rstack-collab-overlay>
      <button class="rapp-info-btn" id="rapp-info-btn" title="About this rApp"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></button>
    </rstack-tab-bar>
  </div>
  <div id="rapp-info-overlay" class="rapp-info-overlay" style="display:none"></div>
  <div id="rapp-info-panel" class="rapp-info-panel" style="display:none">
    <div class="rapp-info-panel__header">
      <div class="rapp-info-panel__header-left">
        <span class="rapp-info-panel__icon" id="rapp-info-icon"></span>
        <span class="rapp-info-panel__title" id="rapp-info-title">About this rApp</span>
      </div>
      <button class="rapp-info-panel__close" id="rapp-info-close">&times;</button>
    </div>
    <div class="rapp-info-panel__body" id="rapp-info-body"></div>
  </div>
  <rstack-user-dashboard space="${escapeAttr(spaceSlug)}" style="display:none"></rstack-user-dashboard>
  ${moduleId !== "rspace" ? `<rstack-module-comments module-id="${escapeAttr(moduleId)}" space="${escapeAttr(spaceSlug)}"></rstack-module-comments>` : ''}
  <main id="app"${moduleId === "rspace" ? ' class="canvas-layout"' : ''}>
    ${opts.tabs ? '' : renderModuleSubNav(moduleId, spaceSlug, visibleModules, opts.isSubdomain ?? IS_PRODUCTION)}
    ${opts.tabs ? renderTabBar(opts.tabs, opts.activeTab, opts.tabBasePath || ((opts.isSubdomain ?? IS_PRODUCTION) ? `/${escapeAttr(moduleId)}` : `/${escapeAttr(spaceSlug)}/${escapeAttr(moduleId)}`)) : ''}
    <div class="rapp-content">${body}</div>
  </main>

  ${renderWelcomeOverlay()}

  <script type="module">
    import '/shell.js';
    // ── PWA: single consolidated banner ──
    const isStandalone = window.matchMedia('(display-mode: standalone)').matches
      || window.navigator.standalone === true;

    // Service worker registration + update detection
    if ("serviceWorker" in navigator && location.hostname !== "localhost") {
      navigator.serviceWorker.register("/sw.js?v=9").then((reg) => {
        function showUpdateBanner() {
          if (!isStandalone) return; // Only show update prompt in installed PWA
          if (sessionStorage.getItem('rspace_update_dismissed')) return; // dismissed this session
          const b = document.getElementById('pwa-update-banner');
          if (!b || b.style.display !== 'none') return;
          // Hide install banner first if somehow both would show
          const ib = document.getElementById('pwa-install-banner');
          if (ib) ib.style.display = 'none';
          b.style.display = '';
          document.body.classList.add('rspace-banner-visible');
        }
        if (reg.waiting) showUpdateBanner();
        reg.addEventListener('updatefound', () => {
          const nw = reg.installing;
          if (!nw) return;
          nw.addEventListener('statechange', () => {
            if (nw.state === 'installed' && navigator.serviceWorker.controller) showUpdateBanner();
          });
        });
      }).catch(() => {});
      let refreshing = false;
      const hadController = !!navigator.serviceWorker.controller;
      navigator.serviceWorker.addEventListener('controllerchange', () => {
        if (!hadController || refreshing) return;
        refreshing = true;
        location.reload();
      });
    }

    // Install banner — browser only (not shown in installed PWA)
    // Permanently dismissed once closed or installed.
    // Uses root-domain cookie so dismissal persists across all subdomains.
    if (!isStandalone) {
      const installDismissed = () => document.cookie.includes('rspace_install_dismissed=1');
      const dismissInstall = () => {
        const b = document.getElementById('pwa-install-banner');
        if (b) b.style.display = 'none';
        document.body.classList.remove('rspace-banner-visible');
        const host = location.hostname;
        const domainPart = host.includes('.') ? '; domain=.' + host.split('.').slice(-2).join('.') : '';
        document.cookie = 'rspace_install_dismissed=1; path=/; max-age=315360000; SameSite=Lax' + domainPart;
      };

      window.addEventListener("beforeinstallprompt", (e) => {
        e.preventDefault();
        window.__rspaceInstallPrompt = () => { e.prompt(); return e.userChoice; };
        window.dispatchEvent(new CustomEvent("rspace-install-available"));
        if (!installDismissed()) {
          const b = document.getElementById('pwa-install-banner');
          if (b) { b.style.display = ''; document.body.classList.add('rspace-banner-visible'); }
        }
      });
      // User installed via browser UI (not our button) — still dismiss permanently
      window.addEventListener("appinstalled", () => { dismissInstall(); });
      document.getElementById('pwa-install-btn')?.addEventListener('click', async () => {
        if (window.__rspaceInstallPrompt) {
          const choice = await window.__rspaceInstallPrompt();
          if (choice?.outcome === 'accepted') dismissInstall();
        }
      });
      document.getElementById('pwa-install-close')?.addEventListener('click', () => { dismissInstall(); });
    }

    // Update banner button handlers (always wire, shown conditionally above)
    document.getElementById('pwa-update-btn')?.addEventListener('click', () => {
      navigator.serviceWorker?.getRegistration().then((reg) => { reg?.waiting?.postMessage({ type: 'SKIP_WAITING' }); });
    });
    document.getElementById('pwa-update-close')?.addEventListener('click', () => {
      document.getElementById('pwa-update-banner').style.display = 'none';
      document.body.classList.remove('rspace-banner-visible');
      sessionStorage.setItem('rspace_update_dismissed', '1'); // don't re-show until next session
    });
    // ── Header minimize toggle ──
    if (localStorage.getItem('rspace_headers_minimized') === '1') {
      document.body.classList.add('rspace-headers-minimized');
    }
    document.getElementById('header-minimize-btn')?.addEventListener('click', () => {
      const minimized = document.body.classList.toggle('rspace-headers-minimized');
      localStorage.setItem('rspace_headers_minimized', minimized ? '1' : '0');
    });
    // ── Settings modal (close history first) ──
    document.getElementById('settings-btn')?.addEventListener('click', () => {
      document.querySelector('rstack-history-panel')?.close();
      const sw = document.querySelector('rstack-space-switcher');
      if (sw) sw.openSettingsModal('${spaceSlug}', '${spaceName || spaceSlug}');
    });

    // ── History panel toggle ──
    document.getElementById('history-btn')?.addEventListener('click', () => {
      document.querySelector('rstack-history-panel')?.toggle();
    });

    // Wire history panel to offline runtime doc (module pages)
    {
      const hp = document.querySelector('rstack-history-panel');
      if (hp && window.__rspaceOfflineRuntime) {
        const rt = window.__rspaceOfflineRuntime;
        rt.init().then(() => {
          const docs = rt.documentManager?.listAll() || [];
          if (docs.length > 0) {
            const doc = rt.documentManager.get(docs[0]);
            if (doc) hp.setDoc(doc);
          }
        }).catch(() => {});
      }
    }

    // ── Info popover: shows landing page content for the active rApp ──
    (function() {
      const infoBtn = document.getElementById('rapp-info-btn');
      const infoPanel = document.getElementById('rapp-info-panel');
      const infoBody = document.getElementById('rapp-info-body');
      const infoClose = document.getElementById('rapp-info-close');
      const infoOverlay = document.getElementById('rapp-info-overlay');
      const infoIcon = document.getElementById('rapp-info-icon');
      const infoTitle = document.getElementById('rapp-info-title');
      if (!infoBtn || !infoPanel || !infoBody) return;

      let infoPanelModuleId = '';

      function showInfoPanel(moduleId) {
        infoPanelModuleId = moduleId;
        infoPanel.style.display = '';
        if (infoOverlay) infoOverlay.style.display = '';
        infoBtn.classList.add('rapp-info-btn--active');
        // Lazy-load content
        if (infoPanel.dataset.loadedModule !== moduleId) {
          infoBody.innerHTML = '<div class="rapp-info-panel__loading"><div class="rapp-info-panel__spinner"></div>Loading…</div>';
          fetch('/api/modules/' + encodeURIComponent(moduleId) + '/landing')
            .then(r => r.json())
            .then(data => {
              if (infoPanelModuleId !== moduleId) return; // stale
              infoBody.innerHTML = data.html || '<p>No info available.</p>';
              infoPanel.dataset.loadedModule = moduleId;
              // Update header with module icon + name
              if (infoIcon && data.icon) infoIcon.textContent = data.icon;
              if (infoTitle && data.name) infoTitle.textContent = data.name;
            })
            .catch(() => { infoBody.innerHTML = '<p>Failed to load info.</p>'; });
        }
      }

      function hideInfoPanel() {
        infoPanel.style.display = 'none';
        if (infoOverlay) infoOverlay.style.display = 'none';
        infoBtn.classList.remove('rapp-info-btn--active');
      }

      // Click overlay to dismiss
      if (infoOverlay) infoOverlay.addEventListener('click', hideInfoPanel);
      document.addEventListener('keydown', (e) => {
        if (e.key === 'Escape' && infoPanel.style.display !== 'none') hideInfoPanel();
      });

      infoBtn.addEventListener('click', () => {
        if (infoPanel.style.display !== 'none') { hideInfoPanel(); return; }
        // Resolve the currently active module from the tab bar
        const tb = document.querySelector('rstack-tab-bar');
        const activeLayerId = tb?.getAttribute('active') || '';
        const activeModuleId = activeLayerId.replace('layer-', '') || '${escapeAttr(moduleId)}';
        showInfoPanel(activeModuleId);
      });
      if (infoClose) infoClose.addEventListener('click', hideInfoPanel);

      // Info panel only shows when user clicks the info button — no auto-show.

      // Hide info panel when switching tabs (but don't auto-show for new tabs)
      document.addEventListener('layer-view-mode', hideInfoPanel);
      const tb = document.querySelector('rstack-tab-bar');
      if (tb) {
        tb.addEventListener('layer-switch', (e) => {
          hideInfoPanel();
          // Reset cached module so next open loads fresh content for new tab
          infoPanel.dataset.loadedModule = '';
        });
      }


      // Expose for tour integration
      window.__rspaceShowInfo = showInfoPanel;
      window.__rspaceHideInfo = hideInfoPanel;
    })();

    // ── Invite acceptance on page load ──
    (function() {
      var params = new URLSearchParams(window.location.search);
      var inviteToken = params.get('inviteToken') || params.get('invite');
      if (!inviteToken) return;
      // Remove token from URL immediately
      params.delete('inviteToken');
      params.delete('invite');
      var newUrl = window.location.pathname + (params.toString() ? '?' + params.toString() : '');
      history.replaceState(null, '', newUrl);
      // Wait for auth then accept
      function tryAccept() {
        try {
          var raw = localStorage.getItem('encryptid_session');
          if (!raw) return;
          var session = JSON.parse(raw);
          if (!session || !session.accessToken) return;
          fetch('/api/spaces/' + encodeURIComponent('${escapeAttr(spaceSlug)}') + '/invite/accept', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + session.accessToken },
            body: JSON.stringify({ inviteToken: inviteToken }),
          }).then(function(res) { return res.json(); }).then(function(data) {
            if (data.ok) {
              var slug = data.spaceSlug || '${escapeAttr(spaceSlug)}';
              window.location.href = 'https://' + slug + '.rspace.online';
            }
          });
        } catch(e) {}
      }
      tryAccept();
      // Also try after auth-change (if user signs in after landing)
      document.addEventListener('auth-change', tryAccept);
    })();

    // Restore saved theme preference across header / tab-row
    (function(){try{var t=localStorage.getItem('canvas-theme');if(t)document.querySelectorAll('[data-theme]').forEach(function(el){el.setAttribute('data-theme',t)})}catch(e){}})();
    // Provide module list to app switcher and offline runtime
    window.__rspaceModuleList = ${moduleListJSON};
    window.__rspaceAllModules = ${allModulesJSON};
    window.__rspaceEnabledModules = ${JSON.stringify(enabledModules)};
    const _switcher = document.querySelector('rstack-app-switcher');
    _switcher?.setModules(window.__rspaceModuleList);
    _switcher?.setAllModules(window.__rspaceAllModules);

    // Initialize folk-rapp picker/switcher filtering with enabled modules
    if (window.__rspaceEnabledModules) {
      customElements.whenDefined('folk-rapp').then(function() {
        var FolkRApp = customElements.get('folk-rapp');
        if (FolkRApp && FolkRApp.setEnabledModules) FolkRApp.setEnabledModules(window.__rspaceEnabledModules);
      });
    }

    // React to runtime module toggling from the app switcher "Manage rApps" panel
    document.addEventListener('modules-changed', function(e) {
      var detail = e.detail || {};
      var enabledModules = detail.enabledModules;
      window.__rspaceEnabledModules = enabledModules;

      // Recompute visible + allModules with updated enabled flags
      var allMods = window.__rspaceAllModules || [];
      var enabledSet = new Set(enabledModules);
      var visible = allMods.filter(function(m) { return m.id === 'rspace' || enabledSet.has(m.id); });
      var updatedAll = allMods.map(function(m) { return Object.assign({}, m, { enabled: m.id === 'rspace' || enabledSet.has(m.id) }); });
      window.__rspaceModuleList = visible;
      window.__rspaceAllModules = updatedAll;

      // Update app switcher's main navigation + catalog
      var switcher = document.querySelector('rstack-app-switcher');
      if (switcher) { switcher.setModules(visible); switcher.setAllModules(updatedAll); }

      // Update tab bar's module list
      var tb = document.querySelector('rstack-tab-bar');
      if (tb) tb.setModules(visible);

      // Update folk-rapp picker/switcher
      var FolkRApp = customElements.get('folk-rapp');
      if (FolkRApp && FolkRApp.setEnabledModules) FolkRApp.setEnabledModules(enabledModules);

      // Re-run toolbar hiding for data-requires-module buttons
      document.querySelectorAll('[data-requires-module]').forEach(function(el) {
        el.style.display = enabledSet.has(el.dataset.requiresModule) ? '' : 'none';
      });
      // Re-check empty toolbar groups
      document.querySelectorAll('.toolbar-group').forEach(function(group) {
        var dropdown = group.querySelector('.toolbar-dropdown');
        if (!dropdown) return;
        var vis = dropdown.querySelectorAll('button:not([style*="display: none"]):not(.toolbar-dropdown-header)');
        group.style.display = vis.length === 0 ? 'none' : '';
      });
    });

    // ── Welcome tour — disabled (tours turned off) ──
    // Legacy compat
    window.__rspaceDismissWelcome = function() {
      localStorage.setItem('rspace_tour_done', '1');
      var el = document.getElementById('rspace-tour');
      if (el) el.style.display = 'none';
    };

    // ── Save-gate: prompt sign-in on write when unauthenticated ──
    window.__rspaceSaveGate = function(callback) {
      try {
        var raw = localStorage.getItem('encryptid_session');
        if (raw) {
          var session = JSON.parse(raw);
          if (session && session.accessToken) return true; // authenticated
        }
      } catch(e) {}
      // Not authenticated — show modal
      var identity = document.querySelector('rstack-identity');
      if (identity && identity.showAuthModal) {
        identity.showAuthModal({
          title: 'Sign in to save',
          message: 'Sign in with EncryptID to save your work to your own rSpace.',
          onSuccess: function() { if (typeof callback === 'function') callback(); }
        });
      }
      return false;
    };

    // ── Private space access gate ──
    // For private spaces: check session, then verify membership via API.
    // For permissioned spaces: require sign-in but allow any authenticated user.
    (function() {
      var vis = document.body.getAttribute('data-space-visibility');
      if (vis !== 'private' && vis !== 'permissioned') return;
      var slug = document.body.getAttribute('data-space-slug') || '';

      var session = null;
      try {
        var raw = localStorage.getItem('encryptid_session');
        if (raw) session = JSON.parse(raw);
      } catch(e) {}

      // Also check cross-subdomain cookie (localStorage is per-origin)
      if (!session || !session.accessToken) {
        try {
          var m = document.cookie.match(/(?:^|; )eid_token=([^;]*)/);
          if (m) {
            var tok = decodeURIComponent(m[1]);
            var parts = tok.split('.');
            if (parts.length >= 2) {
              var b64 = parts[1].replace(/-/g,'+').replace(/_/g,'/');
              var pad = '='.repeat((4 - b64.length % 4) % 4);
              var payload = JSON.parse(atob(b64 + pad));
              if (payload.exp && Math.floor(Date.now()/1000) < payload.exp) {
                session = { accessToken: tok, claims: payload };
                // Sync to localStorage so downstream code sees it
                localStorage.setItem('encryptid_session', JSON.stringify(session));
                if (payload.username) localStorage.setItem('rspace-username', payload.username);
              }
            }
          }
        } catch(e) {}
      }

      var hasToken = session && session.accessToken;

      // Permissioned spaces: only need a valid session
      if (vis === 'permissioned') {
        if (hasToken) return; // authenticated — allow through
        showGate('sign-in');
        return;
      }

      // Private spaces: show sign-in gate for logged-out visitors
      if (!hasToken) {
        showGate('sign-in');
        return;
      }

      // Have a token — verify membership via API
      var main = document.getElementById('app');
      fetch('/api/space-access/' + encodeURIComponent(slug), {
        headers: { 'Authorization': 'Bearer ' + session.accessToken }
      })
      .then(function(r) { return r.json(); })
      .then(function(data) {
        if (data.access) return; // member/owner — allow through
        showGate('no-access');
      })
      .catch(function() {
        // API error — don't block, let WS gate handle it
      });

      function showGate(mode) {
        var main = document.getElementById('app');
        if (main) main.style.display = 'none';
        var gate = document.createElement('div');
        gate.id = 'rspace-access-gate';

        if (mode === 'sign-in') {
          gate.innerHTML =
            '<div class="access-gate__card">' +
              '<div class="access-gate__icon">&#x1F512;</div>' +
              '<h2 class="access-gate__title">Private Space</h2>' +
              '<p class="access-gate__desc">Sign in to access this space.</p>' +
              '<button class="access-gate__btn" id="gate-signin">Sign In</button>' +
            '</div>';
        } else {
          var userSlug = '';
          try { if (session && session.claims && session.claims.username) userSlug = session.claims.username; } catch(e) {}
          var mySpaceHref = userSlug
            ? 'https://' + userSlug.toLowerCase() + '.rspace.online/'
            : 'https://rspace.online/';
          var mySpaceLabel = userSlug ? 'Go to ' + userSlug + '\\\'s Space' : 'Go to rSpace';
          gate.innerHTML =
            '<div class="access-gate__card">' +
              '<div class="access-gate__icon">&#x1F512;</div>' +
              '<h2 class="access-gate__title">Private Space</h2>' +
              '<p class="access-gate__desc">You don\\\'t have access to <strong>' + slug + '</strong>.</p>' +
              '<p class="access-gate__hint">This space is private. You can request access from the owner.</p>' +
              '<div class="access-gate__actions">' +
                '<button class="access-gate__btn" id="gate-request-access">Request Access</button>' +
                '<a href="' + mySpaceHref + '" class="access-gate__btn access-gate__btn--secondary">' + mySpaceLabel + '</a>' +
              '</div>' +
            '</div>';
        }

        document.body.appendChild(gate);

        if (mode === 'sign-in') {
          var btn = document.getElementById('gate-signin');
          if (btn) btn.addEventListener('click', function() {
            var identity = document.querySelector('rstack-identity');
            if (identity && identity.showAuthModal) {
              identity.showAuthModal({
                onSuccess: function() {
                  // After sign-in, reload to re-check membership
                  location.reload();
                }
              });
            }
          });
        }

        // Request access button handler
        var reqBtn = document.getElementById('gate-request-access');
        if (reqBtn) reqBtn.addEventListener('click', function() {
          reqBtn.disabled = true;
          reqBtn.textContent = 'Requesting...';
          fetch('/api/spaces/' + encodeURIComponent(slug) + '/access-requests', {
            method: 'POST',
            headers: {
              'Authorization': 'Bearer ' + session.accessToken,
              'Content-Type': 'application/json'
            },
            body: JSON.stringify({})
          })
          .then(function(r) { return r.json().then(function(d) { return { status: r.status, data: d }; }); })
          .then(function(res) {
            if (res.status === 201) {
              reqBtn.textContent = 'Request Sent ✓';
              reqBtn.style.opacity = '0.7';
              var hint = gate.querySelector('.access-gate__hint');
              if (hint) hint.textContent = 'Your request has been sent to the space owner. You\\\'ll be notified when it\\\'s approved.';
            } else if (res.status === 409) {
              reqBtn.textContent = 'Request Pending';
              reqBtn.style.opacity = '0.7';
              var hint = gate.querySelector('.access-gate__hint');
              if (hint) hint.textContent = 'You already have a pending access request for this space.';
            } else {
              reqBtn.textContent = 'Request Access';
              reqBtn.disabled = false;
              alert(res.data.error || 'Failed to send request');
            }
          })
          .catch(function() {
            reqBtn.textContent = 'Request Access';
            reqBtn.disabled = false;
            alert('Network error — please try again');
          });
        });
      }
    })();

    // ── Tab bar / Layer system initialization ──
    // Tabs persist in localStorage so they survive full-page navigations.
    // When a user opens a new rApp (via the app switcher or tab-add),
    // the next page load reads the existing tabs and adds the new module.
    const tabBar = document.querySelector('rstack-tab-bar');
    const spaceSlug = '${escapeAttr(spaceSlug)}';
    let currentModuleId = '${escapeAttr(moduleId)}';
    const TABS_KEY = 'rspace_tabs_' + spaceSlug;
    const moduleList = ${moduleListJSON};

    if (tabBar) {
      // Provide module list for the + add menu dropdown
      tabBar.setModules(moduleList);

      // Helper: look up a module's display name
      function getModuleLabel(id) {
        const m = moduleList.find(mod => mod.id === id);
        return m ? m.name : id;
      }

      // Helper: deduplicate layers by moduleId (keeps first occurrence)
      function deduplicateLayers(list) {
        const seen = new Set();
        return list.filter(l => {
          if (seen.has(l.moduleId)) return false;
          seen.add(l.moduleId);
          return true;
        });
      }

      // Helper: create a layer object
      function makeLayer(id, order) {
        return {
          id: 'layer-' + id,
          moduleId: id,
          label: getModuleLabel(id),
          order: order,
          color: '',
          visible: true,
          createdAt: Date.now(),
        };
      }

      // Valid module IDs for this space (used to filter out stale/phantom tabs)
      const validModuleIds = new Set(moduleList.map(m => m.id));

      // ── Restore tabs from localStorage ──
      let layers;
      try {
        const saved = localStorage.getItem(TABS_KEY);
        layers = saved ? JSON.parse(saved) : [];
        if (!Array.isArray(layers)) layers = [];
      } catch(e) { layers = []; }

      // Filter out stale tabs whose moduleId no longer exists
      layers = deduplicateLayers(layers.filter(l => validModuleIds.has(l.moduleId)));

      // Ensure the current module is in the tab list
      if (!layers.find(l => l.moduleId === currentModuleId)) {
        layers.push(makeLayer(currentModuleId, layers.length));
      }

      // Persist immediately (cleaned list)
      localStorage.setItem(TABS_KEY, JSON.stringify(layers));

      // Render all tabs with the current one active
      tabBar.setLayers(layers);
      tabBar.setAttribute('active', 'layer-' + currentModuleId);
      // Track current module as recently used
      if (tabBar.trackRecent) tabBar.trackRecent(currentModuleId);

      // Helper: save current tab list to localStorage + server
      let _tabSaveTimer = null;
      // Track tabs closed this session so server merge doesn't resurrect them
      const _closedModuleIds = new Set();
      function saveTabs(immediate) {
        localStorage.setItem(TABS_KEY, JSON.stringify(layers));
        // Debounced server save for authenticated users
        clearTimeout(_tabSaveTimer);
        const doSave = () => {
          try {
            const raw = localStorage.getItem('encryptid_session');
            if (!raw) return;
            const session = JSON.parse(raw);
            if (!session?.accessToken) return;
            const url = '/api/user/tabs/' + encodeURIComponent(spaceSlug);
            const body = JSON.stringify({ tabs: layers });
            fetch(url, {
              method: 'PUT',
              headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + session.accessToken },
              body: body,
              keepalive: immediate, // survive page navigation when flushing immediately
            }).catch(() => {});
          } catch(e) {}
        };
        if (immediate) { doSave(); } else { _tabSaveTimer = setTimeout(doSave, 500); }
        // Broadcast to other same-browser tabs
        if (_tabChannel) {
          try {
            _tabChannel.postMessage({
              type: 'tabs-sync',
              layers: layers,
              closed: [..._closedModuleIds],
            });
          } catch(e) { /* channel may be closed during navigation */ }
        }
      }

      // ── BroadcastChannel: same-browser cross-tab sync ──
      const _tabChannel = (() => {
        try { return new BroadcastChannel('rspace_tabs_' + spaceSlug); }
        catch(e) { return null; }
      })();

      // Reconcile remote layer changes (shared by BroadcastChannel + Automerge)
      function reconcileRemoteLayers(remoteLayers, fromUserAction) {
        // Guard: never let remote sync wipe all tabs when we have an active module,
        // UNLESS this came from a deliberate user action (e.g. close-all via BroadcastChannel).
        if (remoteLayers.length === 0 && currentModuleId && !fromUserAction) {
          // Keep local layers intact — remote has nothing useful
          tabBar.setLayers(layers);
          return;
        }

        const prev = new Set(layers.map(l => l.moduleId));
        const next = new Set(remoteLayers.map(l => l.moduleId));

        // Remove cached panes for tabs that disappeared
        for (const mid of prev) {
          if (!next.has(mid) && tabCache) tabCache.removePane(mid);
        }

        layers = deduplicateLayers(remoteLayers.filter(l => validModuleIds.has(l.moduleId)));

        // Always ensure the current module stays in the tab list
        if (currentModuleId && !layers.find(l => l.moduleId === currentModuleId)) {
          layers.push(makeLayer(currentModuleId, layers.length));
        }

        tabBar.setLayers(layers);
        tabBar.setAttribute('active', 'layer-' + currentModuleId);

        localStorage.setItem(TABS_KEY, JSON.stringify(layers));
      }

      if (_tabChannel) {
        _tabChannel.onmessage = (e) => {
          if (e.data?.type !== 'tabs-sync') return;
          const remoteLayers = e.data.layers || [];
          for (const mid of (e.data.closed || [])) _closedModuleIds.add(mid);
          reconcileRemoteLayers(remoteLayers, true);
        };
      }

      window.addEventListener('beforeunload', () => {
        if (_tabChannel) _tabChannel.close();
      });

      // Fetch tabs from server for authenticated users (server-authoritative)
      (function syncTabsFromServer() {
        try {
          const raw = localStorage.getItem('encryptid_session');
          if (!raw) return;
          const session = JSON.parse(raw);
          if (!session?.accessToken) return;
          fetch('/api/user/tabs/' + encodeURIComponent(spaceSlug), {
            headers: { 'Authorization': 'Bearer ' + session.accessToken },
          })
            .then(r => r.ok ? r.json() : null)
            .then(data => {
              if (!data?.tabs || !Array.isArray(data.tabs) || data.tabs.length === 0) {
                // Server has no saved tabs. Only push if user has meaningful local tabs
                // (not just the auto-added current module)
                if (layers.length > 1) saveTabs();
                return;
              }
              // Server-authoritative: adopt server tabs exactly.
              // Only skip tabs the user closed in THIS session (prevents
              // close-then-immediate-resurrect before save propagates).
              var serverTabs = data.tabs.filter(t => validModuleIds.has(t.moduleId) && !_closedModuleIds.has(t.moduleId));
              serverTabs = deduplicateLayers(serverTabs);
              serverTabs.forEach((l, i) => { l.order = i; });
              layers = serverTabs;
              // Ensure the currently navigated module is present
              if (!layers.find(l => l.moduleId === currentModuleId)) {
                layers.push(makeLayer(currentModuleId, layers.length));
              }
              reconcileRemoteLayers(layers);
              saveTabs();
            })
            .catch(() => {});
        } catch(e) {}
      })();

      // ── Tab cache: instant switching via show/hide DOM panes ──
      let tabCache = null;
      try {
        const TC = window.__RSpaceTabCache;
        if (TC) {
          tabCache = new TC(spaceSlug, currentModuleId);
          if (!tabCache.init()) tabCache = null;
        }
      } catch(e) { tabCache = null; }

      // ── Tab events ──
      // Set active on tab bar ONLY after switchTo() confirms the pane is ready.
      // This prevents the visual desync where the tab highlights before content loads.
      tabBar.addEventListener('layer-switch', (e) => {
        const { layerId, moduleId } = e.detail;
        currentModuleId = moduleId;
        // Hide dashboard overlay if it was showing
        const dashOnSwitch = document.querySelector('rstack-user-dashboard');
        if (dashOnSwitch && dashOnSwitch.style.display !== 'none') {
          dashOnSwitch.style.display = 'none';
          tabBar.setAttribute('home-active', 'false');
        }
        saveTabs();
        if (tabCache) {
          const switchId = moduleId; // capture for staleness check
          tabCache.switchTo(moduleId).then(ok => {
            // If user already clicked a different tab, don't navigate for this one
            if (currentModuleId !== switchId) return;
            if (ok) {
              tabBar.setAttribute('active', layerId);
            } else {
              window.location.href = window.__rspaceNavUrl(spaceSlug, moduleId);
            }
          }).catch((err) => {
            if (currentModuleId !== switchId) return; // stale
            console.error('[shell] switchTo error:', err);
            window.location.href = window.__rspaceNavUrl(spaceSlug, moduleId);
          });
        } else {
          window.location.href = window.__rspaceNavUrl(spaceSlug, moduleId);
        }
      });

      tabBar.addEventListener('layer-add', (e) => {
        const { moduleId } = e.detail;
        currentModuleId = moduleId;
        if (!layers.find(l => l.moduleId === moduleId)) {
          layers.push(makeLayer(moduleId, layers.length));
        }
        saveTabs();
        tabBar.setLayers(layers);
        if (tabCache) {
          tabCache.switchTo(moduleId).then(ok => {
            if (ok) {
              tabBar.setAttribute('active', 'layer-' + moduleId);
            } else {
              window.location.href = window.__rspaceNavUrl(spaceSlug, moduleId);
            }
          }).catch(() => {
            window.location.href = window.__rspaceNavUrl(spaceSlug, moduleId);
          });
        } else {
          window.location.href = window.__rspaceNavUrl(spaceSlug, moduleId);
        }
      });

      tabBar.addEventListener('layer-close', (e) => {
        const { layerId } = e.detail;
        const closedLayer = layers.find(l => l.id === layerId);
        const closedModuleId = layerId.replace('layer-', '');
        const closedIdx = layers.findIndex(l => l.id === layerId);
        // Use tabBar.active as source of truth (always updated by TabCache)
        const wasActive = layerId === tabBar.getAttribute('active');
        tabBar.removeLayer(layerId);
        layers = layers.filter(l => l.id !== layerId);
        // Track closed tab so server merge doesn't resurrect it
        _closedModuleIds.add(closedModuleId);
        saveTabs();

        // If this was a space layer with a persisted SpaceRef, clean it up
        if (closedLayer?.spaceSlug && closedLayer._spaceRefId) {
          const token = document.cookie.match(/encryptid_token=([^;]+)/)?.[1];
          if (token) {
            fetch('/api/spaces/' + encodeURIComponent(spaceSlug) + '/nest/' + encodeURIComponent(closedLayer._spaceRefId), {
              method: 'DELETE',
              headers: { 'Authorization': 'Bearer ' + token },
            }).catch(() => { /* best-effort cleanup */ });
          }
        }

        // Remove cached pane from DOM
        if (tabCache) tabCache.removePane(closedModuleId);

        if (layers.length === 0) {
          // No tabs left — show the dashboard
          const dashboard = document.querySelector('rstack-user-dashboard');
          if (dashboard && dashboard.setOpenTabs) dashboard.setOpenTabs(closedLayer ? [closedLayer] : []);
          if (tabCache) tabCache.hideAllPanes();
          if (dashboard) {
            dashboard.style.display = '';
            if (dashboard.refresh) dashboard.refresh();
          }
          const app = document.getElementById('app');
          if (app) app.classList.remove('canvas-layout');
          tabBar.setAttribute('active', '');
          tabBar.setLayers([]);
          currentModuleId = '';
          var dashUrl = window.location.hostname.endsWith('.rspace.online') ? '/' : '/' + spaceSlug;
          history.pushState({ dashboard: true, spaceSlug: spaceSlug }, '', dashUrl);
        } else if (wasActive) {
          // Closed the active tab — switch to the nearest remaining tab
          const nextLayer = layers[Math.min(closedIdx, layers.length - 1)] || layers[0];
          const nextModuleId = nextLayer.moduleId;
          currentModuleId = nextModuleId;
          if (tabCache) {
            tabCache.switchTo(nextModuleId).then(ok => {
              if (!ok) {
                saveTabs(true); // flush to server before navigation
                window.location.href = window.__rspaceNavUrl(spaceSlug, nextModuleId);
              }
            });
          } else {
            saveTabs(true); // flush to server before navigation
            window.location.href = window.__rspaceNavUrl(spaceSlug, nextModuleId);
          }
        }
      });

      tabBar.addEventListener('layers-close-all', () => {
        // Track all closed modules so server merge doesn't resurrect them
        layers.forEach(l => _closedModuleIds.add(l.moduleId));
        // Clean up space layer refs
        const token = document.cookie.match(/encryptid_token=([^;]+)/)?.[1];
        layers.forEach(l => {
          if (l.spaceSlug && l._spaceRefId && token) {
            fetch('/api/spaces/' + encodeURIComponent(spaceSlug) + '/nest/' + encodeURIComponent(l._spaceRefId), {
              method: 'DELETE',
              headers: { 'Authorization': 'Bearer ' + token },
            }).catch(() => {});
          }
        });
        // Remove all cached panes
        if (tabCache) {
          layers.forEach(l => tabCache.removePane(l.moduleId));
          tabCache.hideAllPanes();
        }
        layers = [];
        saveTabs();
        // Show the dashboard
        const dashboard = document.querySelector('rstack-user-dashboard');
        if (dashboard) {
          dashboard.style.display = '';
          if (dashboard.refresh) dashboard.refresh();
          if (dashboard.setOpenTabs) dashboard.setOpenTabs([]);
        }
        const app = document.getElementById('app');
        if (app) app.classList.remove('canvas-layout');
        tabBar.setAttribute('active', '');
        tabBar.setLayers([]);
        currentModuleId = '';
        var dashUrl = window.location.hostname.endsWith('.rspace.online') ? '/' : '/' + spaceSlug;
        history.pushState({ dashboard: true, spaceSlug: spaceSlug }, '', dashUrl);
      });

      tabBar.addEventListener('layer-reorder', (e) => {
        const { layerId, newIndex } = e.detail;
        const oldIdx = layers.findIndex(l => l.id === layerId);
        if (oldIdx === -1 || oldIdx === newIndex) return;
        const [moved] = layers.splice(oldIdx, 1);
        layers.splice(newIndex, 0, moved);
        layers.forEach((l, i) => l.order = i);
        saveTabs();
        tabBar.setLayers(layers);
      });

      // ── Dashboard navigate: user clicked a space/action on the dashboard ──
      document.addEventListener('dashboard-navigate', (e) => {
        const { moduleId: targetModule, spaceSlug: targetSpace } = e.detail;
        const dashboard = document.querySelector('rstack-user-dashboard');
        if (dashboard) dashboard.style.display = 'none';
        tabBar.setAttribute('home-active', 'false');

        // If navigating to a different space, do a full navigation
        if (targetSpace && targetSpace !== spaceSlug) {
          window.location.href = window.__rspaceNavUrl(targetSpace, targetModule || 'rspace');
          return;
        }

        const modId = targetModule || 'rspace';
        currentModuleId = modId;
        // Add tab if not already present
        if (!layers.find(l => l.moduleId === modId)) {
          layers.push(makeLayer(modId, layers.length));
        }
        saveTabs();
        tabBar.setLayers(layers);
        tabBar.setAttribute('active', 'layer-' + modId);

        if (tabCache) {
          tabCache.switchTo(modId).then(ok => {
            if (!ok) window.location.href = window.__rspaceNavUrl(spaceSlug, modId);
          });
        } else {
          window.location.href = window.__rspaceNavUrl(spaceSlug, modId);
        }
      });

      // ── Home button: toggle dashboard overlay when tabs are open ──
      tabBar.addEventListener('home-click', () => {
        const dashboard = document.querySelector('rstack-user-dashboard');
        if (!dashboard) return;

        // If no tabs, dashboard is already visible — no-op
        if (layers.length === 0) return;

        const isVisible = dashboard.style.display !== 'none';
        if (isVisible) {
          // Hide dashboard overlay, return to active tab
          dashboard.style.display = 'none';
          tabBar.setAttribute('home-active', 'false');
          // Restore active tab pane
          if (tabCache && currentModuleId) {
            tabCache.switchTo(currentModuleId);
          }
        } else {
          // Show dashboard as overlay, hide active pane
          if (tabCache) tabCache.hideAllPanes();
          dashboard.style.display = '';
          if (dashboard.refresh) dashboard.refresh();
          tabBar.setAttribute('home-active', 'true');
          var dashUrl = window.location.hostname.endsWith('.rspace.online') ? '/' : '/' + spaceSlug;
          history.pushState({ dashboard: true, spaceSlug: spaceSlug }, '', dashUrl);
        }
      });

      tabBar.addEventListener('view-toggle', (e) => {
        const { mode } = e.detail;
        document.dispatchEvent(new CustomEvent('layer-view-mode', { detail: { mode } }));
      });

      // ── Space-switch: client-side space navigation ──
      // When user clicks a space in the space-switcher, handle it client-side
      // if tabCache is available. Updates URL, tabs, and shell chrome.
      const spaceSwitcher = document.querySelector('rstack-space-switcher');
      if (spaceSwitcher) {
        spaceSwitcher.addEventListener('space-switch', (e) => {
          const { space: newSpace, moduleId: targetModule, href } = e.detail;
          if (!tabCache || !newSpace) return;

          e.preventDefault(); // signal to space-switcher that we're handling it

          const targetModuleId = targetModule || currentModuleId;

          tabCache.switchSpace(newSpace, targetModuleId).then(ok => {
            if (!ok) {
              // Client-side switch failed — full navigation
              window.location.href = href || window.__rspaceNavUrl(newSpace, targetModuleId);
              return;
            }

            // Update runtime to switch space
            const runtime = window.__rspaceOfflineRuntime;
            if (runtime && runtime.switchSpace) runtime.switchSpace(newSpace);

            // Update tab state for the new space
            const newTabsKey = 'rspace_tabs_' + newSpace;
            let newLayers;
            try {
              const saved = localStorage.getItem(newTabsKey);
              newLayers = saved ? JSON.parse(saved) : [];
              if (!Array.isArray(newLayers)) newLayers = [];
            } catch(e) { newLayers = []; }

            if (!newLayers.find(l => l.moduleId === targetModuleId)) {
              newLayers.push(makeLayer(targetModuleId, newLayers.length));
            }
            localStorage.setItem(newTabsKey, JSON.stringify(newLayers));

            // Update layers reference and tab bar
            layers = newLayers;
            tabBar.setLayers(layers);
            tabBar.setAttribute('active', 'layer-' + targetModuleId);
            tabBar.setAttribute('space', newSpace);
          });
        });
      }

      // ── App-switcher → tab system integration ──
      // When user picks a module from the app-switcher, route through tabs
      // instead of doing a full page navigation.
      const appSwitcher = document.querySelector('rstack-app-switcher');
      if (appSwitcher) {
        appSwitcher.addEventListener('module-select', (e) => {
          const { moduleId } = e.detail;
          // Already on this module? No-op.
          if (moduleId === currentModuleId && !tabCache) return;
          if (moduleId === currentModuleId && tabCache) {
            tabCache.switchTo(moduleId);
            return;
          }
          currentModuleId = moduleId;
          // Add tab if not already open
          if (!layers.find(l => l.moduleId === moduleId)) {
            const newLayer = makeLayer(moduleId, layers.length);
            layers.push(newLayer);
            // Sync to Automerge so other windows see this tab
            if (communitySync) communitySync.addLayer(newLayer);
          }
          saveTabs();
          tabBar.setLayers(layers);
          tabBar.setAttribute('active', 'layer-' + moduleId);
          if (tabCache) {
            tabCache.switchTo(moduleId).then(ok => {
              if (!ok) window.location.href = window.__rspaceNavUrl(spaceSlug, moduleId);
            });
          } else {
            window.location.href = window.__rspaceNavUrl(spaceSlug, moduleId);
          }
        });
      }

      // ── Space Layer: cross-space data overlay ──
      tabBar.addEventListener('space-layer-add', (e) => {
        const { layer, spaceSlug: layerSpace, role } = e.detail;
        if (!layers.find(l => l.id === layer.id)) {
          layers.push(layer);
        }
        saveTabs();

        // Persist as a SpaceRef via the nesting API (best-effort)
        const token = document.cookie.match(/encryptid_token=([^;]+)/)?.[1];
        if (token) {
          fetch('/api/spaces/' + encodeURIComponent(spaceSlug) + '/nest', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
            body: JSON.stringify({
              sourceSlug: layerSpace,
              label: layer.label || layerSpace,
              permissions: { read: true, write: role !== 'viewer' },
            }),
          }).then(res => res.json()).then(data => {
            if (data.ref) layer._spaceRefId = data.ref.id;
          }).catch(() => { /* best-effort */ });
        }

        // Connect offline runtime to the source space for cross-space data
        const runtime = window.__rspaceOfflineRuntime;
        if (runtime && runtime.connectToSpace) {
          runtime.connectToSpace(layerSpace).then(() => {
            // Dispatch event for canvas and modules to pick up the new space layer
            document.dispatchEvent(new CustomEvent('space-layer-added', {
              detail: { spaceSlug: layerSpace, role, layerId: layer.id },
            }));
          }).catch(() => {
            console.warn('[shell] Failed to connect to space layer:', layerSpace);
          });
        }
      });

      // Expose tabBar for CommunitySync integration
      window.__rspaceTabBar = tabBar;
      // Sync reference (set when CommunitySync connects)
      let communitySync = null;

      // ── CommunitySync: merge with Automerge once connected ──
      // NOTE: The TAB LIST is shared via Automerge (which modules are open),
      // but the ACTIVE TAB is always local (determined by the URL / currentModuleId).
      // This prevents windows from fighting over which tab is highlighted.
      document.addEventListener('community-sync-ready', (e) => {
        const sync = e.detail?.sync;
        if (!sync) return;
        communitySync = sync;

        const localActiveId = 'layer-' + currentModuleId;

        // User's saved tabs are authoritative — sync Automerge to match.
        const remoteLayers = sync.getLayers();

        // Remove Automerge-only layers that the user doesn't have open
        for (const rl of remoteLayers) {
          if (!layers.find(l => l.moduleId === rl.moduleId)) {
            sync.removeLayer(rl.id);
          }
        }
        // Add user's tabs that aren't in Automerge yet
        for (const l of layers) {
          if (!remoteLayers.find(rl => rl.moduleId === l.moduleId)) {
            sync.addLayer(l);
          }
        }
        tabBar.setFlows(sync.getFlows());

        // Active tab stays local — always matches the URL
        tabBar.setAttribute('active', localActiveId);

        // Keep localStorage in sync
        saveTabs();

        // Sync layer list changes to Automerge (not active tab)
        tabBar.addEventListener('layer-add', (e) => {
          const { moduleId } = e.detail;
          const newLayer = makeLayer(moduleId, sync.getLayers().length);
          sync.addLayer(newLayer);
        });
        tabBar.addEventListener('layer-close', (e) => {
          sync.removeLayer(e.detail.layerId);
        });
        tabBar.addEventListener('layers-close-all', () => {
          sync.getLayers().forEach(l => sync.removeLayer(l.id));
        });
        tabBar.addEventListener('layer-reorder', (e) => {
          const { layerId, newIndex } = e.detail;
          const all = sync.getLayers(); // already sorted by order
          const oldIdx = all.findIndex(l => l.id === layerId);
          if (oldIdx === -1 || oldIdx === newIndex) return;
          const [moved] = all.splice(oldIdx, 1);
          all.splice(newIndex, 0, moved);
          all.forEach((l, i) => sync.updateLayer(l.id, { order: i }));
        });
        tabBar.addEventListener('flow-create', (e) => { sync.addFlow(e.detail.flow); });
        tabBar.addEventListener('flow-remove', (e) => { sync.removeFlow(e.detail.flowId); });
        tabBar.addEventListener('view-toggle', (e) => { sync.setLayerViewMode(e.detail.mode); });

        // Listen for remote changes — sync tab list and flows only.
        // Never touch the active tab: it's managed locally by TabCache
        // and the tab-bar component via layer-switch events.
        sync.addEventListener('change', () => {
          reconcileRemoteLayers(sync.getLayers(), false);
          tabBar.setFlows(sync.getFlows());
          const viewMode = sync.doc.layerViewMode;
          if (viewMode) tabBar.setAttribute('view-mode', viewMode);
        });
      });

      // ── Keyboard shortcuts: Ctrl+1–9 (PWA) / Alt+1–9 (browser) ──
      document.addEventListener('keydown', (e) => {
        const digit = e.key >= '1' && e.key <= '9' ? e.key : null;
        if (!digit) return;
        const isStandalone = window.matchMedia('(display-mode: standalone)').matches;
        if ((isStandalone && e.ctrlKey && !e.shiftKey && !e.altKey) ||
            (!isStandalone && e.altKey && !e.ctrlKey && !e.shiftKey)) {
          e.preventDefault();
          try {
            const shortcuts = JSON.parse(localStorage.getItem('rspace-shortcuts') || '{}');
            const targetModule = shortcuts[digit];
            if (targetModule) {
              const sw = document.querySelector('rstack-app-switcher');
              if (sw) {
                sw.dispatchEvent(new CustomEvent('module-select', {
                  detail: { moduleId: targetModule },
                  bubbles: true, composed: true,
                }));
              }
            }
          } catch(ex) {}
        }
      });

      // ── Swipe gestures on header: left/right to cycle rApps ──
      (function() {
        let touchStartX = 0, touchStartTime = 0;
        const header = document.querySelector('.rstack-header');
        if (!header) return;
        header.addEventListener('touchstart', (e) => {
          touchStartX = e.touches[0].clientX;
          touchStartTime = Date.now();
        }, { passive: true });
        header.addEventListener('touchend', (e) => {
          const dx = e.changedTouches[0].clientX - touchStartX;
          const dt = Date.now() - touchStartTime;
          if (Math.abs(dx) > 80 && dt < 300) {
            try {
              const shortcuts = JSON.parse(localStorage.getItem('rspace-shortcuts') || '{}');
              const order = Object.entries(shortcuts)
                .sort(([a],[b]) => a.localeCompare(b))
                .map(([,m]) => m);
              if (order.length < 2) return;
              const current = document.body.dataset.moduleId || currentModuleId;
              const idx = order.indexOf(current);
              const next = dx < 0
                ? order[(idx + 1) % order.length]
                : order[(idx - 1 + order.length) % order.length];
              const sw = document.querySelector('rstack-app-switcher');
              if (sw) {
                sw.dispatchEvent(new CustomEvent('module-select', {
                  detail: { moduleId: next },
                  bubbles: true, composed: true,
                }));
              }
            } catch(ex) {}
          }
        }, { passive: true });
      })();
    }
  </script>
  ${renderBugReportWidget()}
  <script>(function(){
    // ── Bug Report: error ring buffer ──
    var _bugErrors = [];
    window.addEventListener('error', function(e) {
      _bugErrors.push(e.message + (e.filename ? ' (' + e.filename + ':' + e.lineno + ')' : ''));
      if (_bugErrors.length > 5) _bugErrors.shift();
    });
    window.addEventListener('unhandledrejection', function(e) {
      _bugErrors.push('Promise: ' + (e.reason?.message || e.reason || 'unknown'));
      if (_bugErrors.length > 5) _bugErrors.shift();
    });

    var btn = document.getElementById('rspace-bug-btn');
    var overlay = document.getElementById('rspace-bug-overlay');
    var modal = document.getElementById('rspace-bug-modal');
    var errField = document.getElementById('bug-errors');
    var deviceField = document.getElementById('bug-device');
    var commentsField = document.getElementById('bug-comments');
    var fileInput = document.getElementById('bug-screenshot');
    var submitBtn = document.getElementById('rspace-bug-submit');
    var statusEl = document.getElementById('rspace-bug-status');

    function openModal() {
      errField.value = _bugErrors.length ? _bugErrors.join('\\n') : '';
      deviceField.value = navigator.userAgent;
      commentsField.value = '';
      fileInput.value = '';
      statusEl.textContent = '';
      submitBtn.disabled = false;
      overlay.classList.add('open');
    }
    function closeModal() { overlay.classList.remove('open'); }

    btn.addEventListener('click', openModal);
    overlay.addEventListener('click', function(e) { if (e.target === overlay) closeModal(); });
    document.addEventListener('keydown', function(e) {
      if (e.key === 'Escape' && overlay.classList.contains('open')) closeModal();
    });

    submitBtn.addEventListener('click', function() {
      var file = fileInput.files && fileInput.files[0];
      if (file && file.size > 3 * 1024 * 1024) {
        statusEl.textContent = 'Screenshot exceeds 3 MB limit';
        statusEl.style.color = 'var(--rs-error, #f87171)';
        return;
      }
      submitBtn.disabled = true;
      statusEl.textContent = 'Sending...';
      statusEl.style.color = 'var(--rs-text-muted, #94a3b8)';

      function send(screenshotData) {
        fetch('/api/bug-report', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            userAgent: navigator.userAgent,
            url: location.href,
            errors: _bugErrors.slice(),
            comments: commentsField.value,
            screenshot: screenshotData || undefined,
          }),
        }).then(function(r) {
          if (r.ok) {
            statusEl.textContent = 'Report sent — thank you!';
            statusEl.style.color = 'var(--rs-primary, #14b8a6)';
            setTimeout(closeModal, 1500);
          } else {
            return r.json().then(function(d) { throw new Error(d.error || 'Failed'); });
          }
        }).catch(function(err) {
          statusEl.textContent = err.message || 'Failed to send';
          statusEl.style.color = 'var(--rs-error, #f87171)';
          submitBtn.disabled = false;
        });
      }

      if (file) {
        var reader = new FileReader();
        reader.onload = function() { send(reader.result); };
        reader.onerror = function() { send(null); };
        reader.readAsDataURL(file);
      } else {
        send(null);
      }
    });
  })()</script>
  ${scripts}
</body>
</html>`);
}

// ── External app iframe shell ──

export interface ExternalAppShellOptions {
	/** Page <title> */
	title: string;
	/** Current module ID */
	moduleId: string;
	/** Current space slug */
	spaceSlug: string;
	/** Space display name */
	spaceName?: string;
	/** List of available modules */
	modules: ModuleInfo[];
	/** External app URL to embed */
	appUrl: string;
	/** External app display name */
	appName: string;
	/** Theme */
	theme?: "dark" | "light";
}

export function renderExternalAppShell(opts: ExternalAppShellOptions): string {
	const {
		title,
		moduleId,
		spaceSlug,
		spaceName,
		modules,
		appUrl,
		appName,
		theme = "dark",
	} = opts;

	// Filter modules by space's enabledModules (same logic as renderShell)
	const enabledModules = getSpaceShellMeta(spaceSlug).enabledModules;
	const visibleModules = enabledModules
		? modules.filter(m => m.id === "rspace" || enabledModules.includes(m.id))
		: modules;
	const moduleListJSON = JSON.stringify(visibleModules);
	const demoUrl = `?view=demo`;

	return versionAssetUrls(`<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="icon" type="image/png" href="/favicon.png">
  ${faviconScript(moduleId)}
  <title>${escapeHtml(title)}</title>
  <script>(function(){var t=localStorage.getItem('canvas-theme');if(!t)t=matchMedia('(prefers-color-scheme:light)').matches?'light':'dark';document.documentElement.setAttribute('data-theme',t);var b=localStorage.getItem('canvas-bg')||'grid';document.documentElement.setAttribute('data-canvas-bg',b)})()</script>
  ${COMPAT_GATE}
  ${COMPAT_POLYFILLS}
  <link rel="stylesheet" href="/theme.css">
  <link rel="stylesheet" href="/shell.css">
  <style>
    html.rspace-embedded .rstack-header { display: none !important; }
    html.rspace-embedded .rstack-tab-row { display: none !important; }
    html.rspace-embedded .rspace-iframe-wrap { top: 0 !important; height: 100vh !important; }
  </style>
  <script>if (window.self !== window.parent) document.documentElement.classList.add('rspace-embedded');</script>
</head>
<body>
  <header class="rstack-header">
    <div class="rstack-header__left">
      <a href="/" style="display:flex;align-items:center;margin-right:4px"><img src="/favicon.png" alt="rSpace" class="rstack-header__logo"></a>
      <rstack-app-switcher current="${escapeAttr(moduleId)}"></rstack-app-switcher>
      <rstack-space-switcher current="${escapeAttr(spaceSlug)}" name="${escapeAttr(spaceName || spaceSlug)}"></rstack-space-switcher>
    </div>
    <div class="rstack-header__center">
      <rstack-mi></rstack-mi>
    </div>
    <div class="rstack-header__right">
      <a href="${demoUrl}" class="rapp-nav__btn rapp-nav__btn--secondary" style="font-size:0.78rem;padding:5px 12px;">Back to Demo</a>
      <rstack-identity></rstack-identity>
    </div>
  </header>
  <div class="rstack-tab-row">
    <rstack-tab-bar space="${escapeAttr(spaceSlug)}" active="" view-mode="flat">
      <rstack-collab-overlay module-id="${escapeAttr(moduleId)}" space="${escapeAttr(spaceSlug)}" mode="badge-only"></rstack-collab-overlay>
    </rstack-tab-bar>
  </div>
  <div class="rspace-iframe-wrap">
    <div class="rspace-iframe-loading" id="iframe-loading">
      <div class="rspace-iframe-spinner"></div>
      <span>Loading ${escapeHtml(appName)}…</span>
    </div>
    <iframe
      class="rspace-iframe"
      src="${escapeAttr(appUrl)}"
      title="${escapeAttr(appName)}"
      allow="camera; microphone; display-capture; clipboard-read; clipboard-write; fullscreen"
      sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox allow-downloads allow-modals"
      onload="document.getElementById('iframe-loading').style.display='none'"
    ></iframe>
    <a class="rspace-iframe-newtab" href="${escapeAttr(appUrl)}" target="_blank" rel="noopener">Open in new tab ↗</a>
  </div>

  <script type="module">
    import '/shell.js';
    document.querySelector('rstack-app-switcher')?.setModules(${moduleListJSON});

    const tabBar = document.querySelector('rstack-tab-bar');
    const spaceSlug = '${escapeAttr(spaceSlug)}';
    const currentModuleId = '${escapeAttr(moduleId)}';
    const TABS_KEY = 'rspace_tabs_' + spaceSlug;
    const moduleList = ${moduleListJSON};

    if (tabBar) {
      tabBar.setModules(moduleList);
      const validModuleIds = new Set(moduleList.map(m => m.id));
      function getModuleLabel(id) {
        const m = moduleList.find(mod => mod.id === id);
        return m ? m.name : id;
      }
      function makeLayer(id, order) {
        return { id: 'layer-' + id, moduleId: id, label: getModuleLabel(id), order, color: '', visible: true, createdAt: Date.now() };
      }
      function deduplicateLayers(list) {
        const seen = new Set();
        return list.filter(l => { if (seen.has(l.moduleId)) return false; seen.add(l.moduleId); return true; });
      }
      let layers;
      try { const saved = localStorage.getItem(TABS_KEY); layers = saved ? JSON.parse(saved) : []; if (!Array.isArray(layers)) layers = []; } catch(e) { layers = []; }
      // Filter out stale/phantom tabs and deduplicate
      layers = deduplicateLayers(layers.filter(l => validModuleIds.has(l.moduleId)));
      if (!layers.find(l => l.moduleId === currentModuleId)) layers.push(makeLayer(currentModuleId, layers.length));
      localStorage.setItem(TABS_KEY, JSON.stringify(layers));
      tabBar.setLayers(layers);
      tabBar.setAttribute('active', 'layer-' + currentModuleId);
      if (tabBar.trackRecent) tabBar.trackRecent(currentModuleId);
      const _closedModuleIds = new Set();
      function saveTabs() {
        localStorage.setItem(TABS_KEY, JSON.stringify(layers));
        try {
          var raw = localStorage.getItem('encryptid_session');
          if (raw) {
            var session = JSON.parse(raw);
            if (session?.accessToken) {
              fetch('/api/user/tabs/' + encodeURIComponent(spaceSlug), {
                method: 'PUT',
                headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + session.accessToken },
                body: JSON.stringify({ tabs: layers }),
              }).catch(function(){});
            }
          }
        } catch(e) {}
      }
      // Sync tabs from server (server-authoritative)
      (function() {
        try {
          var raw = localStorage.getItem('encryptid_session');
          if (!raw) return;
          var session = JSON.parse(raw);
          if (!session?.accessToken) return;
          fetch('/api/user/tabs/' + encodeURIComponent(spaceSlug), {
            headers: { 'Authorization': 'Bearer ' + session.accessToken },
          })
            .then(function(r) { return r.ok ? r.json() : null; })
            .then(function(data) {
              if (!data?.tabs || !Array.isArray(data.tabs) || data.tabs.length === 0) { saveTabs(); return; }
              // Filter stale moduleIds and tabs closed this session
              layers = deduplicateLayers(data.tabs.filter(function(t) { return validModuleIds.has(t.moduleId) && !_closedModuleIds.has(t.moduleId); }));
              layers.forEach(function(l, i) { l.order = i; });
              if (!layers.find(function(l) { return l.moduleId === currentModuleId; })) layers.push(makeLayer(currentModuleId, layers.length));
              tabBar.setLayers(layers);
              localStorage.setItem(TABS_KEY, JSON.stringify(layers));
            })
            .catch(function(){});
        } catch(e) {}
      })();
      tabBar.addEventListener('layer-switch', (e) => { saveTabs(); window.location.href = window.__rspaceNavUrl(spaceSlug, e.detail.moduleId); });
      tabBar.addEventListener('layer-add', (e) => { const { moduleId } = e.detail; if (!layers.find(l => l.moduleId === moduleId)) layers.push(makeLayer(moduleId, layers.length)); saveTabs(); window.location.href = window.__rspaceNavUrl(spaceSlug, moduleId); });
      tabBar.addEventListener('layer-close', (e) => { const { layerId } = e.detail; const closedMid = layerId.replace('layer-', ''); _closedModuleIds.add(closedMid); tabBar.removeLayer(layerId); layers = layers.filter(l => l.id !== layerId); saveTabs(); if (layerId === 'layer-' + currentModuleId && layers.length > 0) window.location.href = window.__rspaceNavUrl(spaceSlug, layers[0].moduleId); });
      tabBar.addEventListener('layers-close-all', () => { layers.forEach(l => _closedModuleIds.add(l.moduleId)); layers = []; tabBar.setLayers([]); tabBar.setAttribute('active', ''); saveTabs(); var dashUrl = window.location.hostname.endsWith('.rspace.online') ? '/' : '/' + spaceSlug; window.location.href = dashUrl; });
      tabBar.addEventListener('layer-reorder', (e) => { const { layerId, newIndex } = e.detail; const oldIdx = layers.findIndex(l => l.id === layerId); if (oldIdx === -1 || oldIdx === newIndex) return; const [moved] = layers.splice(oldIdx, 1); layers.splice(newIndex, 0, moved); layers.forEach((l, i) => l.order = i); saveTabs(); tabBar.setLayers(layers); });
    }
  </script>
</body>
</html>`);
}

// ── Welcome tour (guided feature walkthrough for first-time visitors) ──

function renderWelcomeOverlay(): string {
	return ''; // Tours disabled
}

const ACCESS_GATE_CSS = `
#rspace-access-gate {
  position: fixed; inset: 0; z-index: 9999;
  display: flex; align-items: center; justify-content: center;
  background: var(--rs-bg-overlay); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
}
.access-gate__card {
  text-align: center; color: var(--rs-text-primary); max-width: 400px; padding: 2rem;
}
.access-gate__icon { font-size: 3rem; margin-bottom: 1rem; }
.access-gate__title {
  font-size: 1.5rem; margin: 0 0 0.5rem;
  background: linear-gradient(135deg, #06b6d4, #7c3aed);
  -webkit-background-clip: text; -webkit-text-fill-color: transparent;
}
.access-gate__desc { color: var(--rs-text-secondary); font-size: 0.95rem; line-height: 1.6; margin: 0 0 0.5rem; }
.access-gate__hint { color: var(--rs-text-secondary); font-size: 0.85rem; opacity: 0.7; margin: 0 0 1.5rem; }
.access-gate__btn {
  display: inline-block; padding: 12px 32px; border-radius: 8px; border: none;
  font-size: 1rem; font-weight: 600; cursor: pointer; text-decoration: none;
  background: linear-gradient(135deg, #06b6d4, #7c3aed); color: white;
  transition: opacity 0.15s, transform 0.15s;
}
.access-gate__btn:hover { opacity: 0.9; transform: translateY(-1px); }
.access-gate__btn:disabled { opacity: 0.6; cursor: default; transform: none; }
.access-gate__btn--secondary {
  background: transparent; border: 1px solid var(--rs-border, rgba(255,255,255,0.15));
  color: var(--rs-text-secondary, #94a3b8);
}
.access-gate__btn--secondary:hover { border-color: var(--rs-border-strong, rgba(255,255,255,0.3)); color: var(--rs-text-primary, #e0e0e0); }
.access-gate__actions { display: flex; flex-direction: column; gap: 0.75rem; align-items: center; }
`;

const WELCOME_CSS = ''; // Tours disabled

const INFO_PANEL_CSS = `
/* ── Info button in tab bar ── */
.rapp-info-btn {
  display: flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; padding: 0; margin-right: 2px;
  background: none; border: 1px solid transparent; border-radius: 6px;
  color: var(--rs-text-muted); cursor: pointer; flex-shrink: 0;
  transition: color 0.15s, background 0.15s, border-color 0.15s;
}
.rapp-info-btn:hover { color: var(--rs-text-primary); background: var(--rs-bg-hover); border-color: var(--rs-border); }
.rapp-info-btn--active { color: var(--rs-primary); background: var(--rs-bg-hover); border-color: var(--rs-primary); }

/* ── Backdrop overlay ── */
.rapp-info-overlay {
  position: fixed; inset: 0; z-index: 10000;
  background: rgba(0,0,0,0.45);
  backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
  animation: rapp-overlay-in 0.2s ease-out;
}
@keyframes rapp-overlay-in { from { opacity: 0; } to { opacity: 1; } }

/* ── Panel — centered modal ── */
.rapp-info-panel {
  position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
  z-index: 10001;
  width: min(520px, calc(100vw - 32px)); max-height: calc(100vh - 64px);
  background: var(--rs-bg-surface);
  border: 1px solid rgba(20,184,166,0.25);
  border-radius: 16px;
  box-shadow: 0 24px 80px rgba(0,0,0,0.6), 0 0 0 1px rgba(20,184,166,0.1), 0 0 60px rgba(20,184,166,0.06);
  display: flex; flex-direction: column; overflow: hidden;
  animation: rapp-info-in 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes rapp-info-in {
  from { opacity: 0; transform: translate(-50%, -48%) scale(0.95); }
  to { opacity: 1; transform: translate(-50%, -50%) scale(1); }
}

/* ── Header with icon + name ── */
.rapp-info-panel__header {
  display: flex; align-items: center; justify-content: space-between;
  padding: 16px 20px;
  background: linear-gradient(135deg, rgba(20,184,166,0.12), rgba(79,70,229,0.08));
  border-bottom: 1px solid rgba(20,184,166,0.18);
}
.rapp-info-panel__header-left {
  display: flex; align-items: center; gap: 10px;
}
.rapp-info-panel__icon {
  font-size: 1.5rem; line-height: 1;
}
.rapp-info-panel__title {
  font-size: 1.05rem; font-weight: 700; letter-spacing: 0.01em;
  background: linear-gradient(135deg, #14b8a6, #22d3ee);
  -webkit-background-clip: text; -webkit-text-fill-color: transparent;
  background-clip: text;
}
.rapp-info-panel__close {
  display: flex; align-items: center; justify-content: center;
  width: 32px; height: 32px;
  background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.08);
  color: var(--rs-text-muted); font-size: 1.2rem; cursor: pointer;
  border-radius: 8px; line-height: 1;
  transition: color 0.15s, background 0.15s, border-color 0.15s;
}
.rapp-info-panel__close:hover {
  color: var(--rs-text-primary); background: rgba(255,255,255,0.1);
  border-color: rgba(255,255,255,0.15);
}

/* ── Body ── */
.rapp-info-panel__body {
  padding: 0; overflow-y: auto; flex: 1;
  color: var(--rs-text-secondary); font-size: 0.92rem; line-height: 1.65;
}
.rapp-info-panel__body h1, .rapp-info-panel__body h2, .rapp-info-panel__body h3 {
  color: var(--rs-text-primary); margin: 0 0 8px;
}
.rapp-info-panel__body h1 { font-size: 1.3rem; }
.rapp-info-panel__body h2 { font-size: 1.1rem; }
.rapp-info-panel__body h3 { font-size: 0.95rem; }
.rapp-info-panel__body p { margin: 0 0 10px; }
.rapp-info-panel__body a { color: var(--rs-primary); }

/* ── Loading state ── */
.rapp-info-panel__loading {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 12px; padding: 48px 32px; color: var(--rs-text-muted);
}
.rapp-info-panel__spinner {
  width: 24px; height: 24px;
  border: 2px solid rgba(20,184,166,0.2); border-top-color: #14b8a6;
  border-radius: 50%; animation: rapp-spin 0.7s linear infinite;
}
@keyframes rapp-spin { to { transform: rotate(360deg); } }

/* ── Panel-scoped overrides for rich landing content ── */
.rapp-info-panel__body .rl-hero { padding: 1.75rem 1.5rem 1.25rem; text-align: center; }
.rapp-info-panel__body .rl-hero .rl-heading { font-size: 1.5rem !important; }
.rapp-info-panel__body .rl-heading { font-size: 1.2rem; margin-bottom: 0.5rem; }
.rapp-info-panel__body .rl-tagline { font-size: 0.65rem; padding: 0.3rem 0.85rem; margin-bottom: 1rem; }
.rapp-info-panel__body .rl-subtitle { font-size: 1rem !important; margin-bottom: 0.75rem; }
.rapp-info-panel__body .rl-subtext { font-size: 0.9rem !important; margin-bottom: 1.25rem; line-height: 1.7; }
.rapp-info-panel__body .rl-hero .rl-subtext { font-size: 0.92rem !important; }
.rapp-info-panel__body .rl-section { padding: 1.5rem 1.25rem; border-top: 1px solid var(--rs-border-subtle); }
.rapp-info-panel__body .rl-section--alt { background: rgba(20,184,166,0.03); }
.rapp-info-panel__body .rl-container { max-width: 100%; }

/* Grids: max 2 columns in panel */
.rapp-info-panel__body .rl-grid-2,
.rapp-info-panel__body .rl-grid-3,
.rapp-info-panel__body .rl-grid-4 { grid-template-columns: 1fr 1fr !important; gap: 0.75rem; }

/* Cards */
.rapp-info-panel__body .rl-card { padding: 1.15rem; border-radius: 0.75rem; }
.rapp-info-panel__body .rl-card h3 { font-size: 0.88rem; margin-bottom: 0.35rem; }
.rapp-info-panel__body .rl-card p { font-size: 0.82rem; line-height: 1.55; margin-bottom: 0; }

/* Icon boxes */
.rapp-info-panel__body .rl-icon-box { width: 2.5rem; height: 2.5rem; font-size: 1.25rem; border-radius: 0.6rem; margin-bottom: 0.65rem; }
.rapp-info-panel__body .rl-card--center .rl-icon-box { margin: 0 auto 0.65rem; }

/* Steps */
.rapp-info-panel__body .rl-step__num { width: 2.25rem; height: 2.25rem; font-size: 0.75rem; margin-bottom: 0.5rem; }
.rapp-info-panel__body .rl-step h3 { font-size: 0.88rem; }
.rapp-info-panel__body .rl-step p { font-size: 0.82rem; }

/* CTAs — bigger, bolder buttons */
.rapp-info-panel__body .rl-cta-row { margin-top: 1.5rem; gap: 0.625rem; display: flex; flex-wrap: wrap; justify-content: center; }
.rapp-info-panel__body .rl-cta-primary {
  padding: 0.75rem 1.5rem; font-size: 0.92rem; font-weight: 600;
  border-radius: 10px; text-decoration: none;
  background: var(--rs-gradient-cta, linear-gradient(135deg, #14b8a6, #06b6d4));
  color: #fff; box-shadow: 0 4px 16px rgba(20,184,166,0.3);
  transition: transform 0.15s, box-shadow 0.15s;
}
.rapp-info-panel__body .rl-cta-primary:hover { transform: translateY(-1px); box-shadow: 0 6px 24px rgba(20,184,166,0.4); }
.rapp-info-panel__body .rl-cta-secondary {
  padding: 0.75rem 1.5rem; font-size: 0.92rem; font-weight: 600;
  border-radius: 10px; text-decoration: none;
  background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.12);
  color: var(--rs-text-secondary); transition: transform 0.15s, border-color 0.15s, color 0.15s, background 0.15s;
}
.rapp-info-panel__body .rl-cta-secondary:hover { transform: translateY(-1px); border-color: rgba(20,184,166,0.4); color: var(--rs-text-primary); background: rgba(20,184,166,0.08); }

}

/* Badges */
.rapp-info-panel__body .rl-badge { font-size: 0.65rem; padding: 0.15rem 0.5rem; }

/* Integration cards */
.rapp-info-panel__body .rl-integration { padding: 1rem; border-radius: 0.75rem; gap: 0.75rem; }
.rapp-info-panel__body .rl-integration h3 { font-size: 0.88rem; }
.rapp-info-panel__body .rl-integration p { font-size: 0.82rem; }

/* Check list */
.rapp-info-panel__body .rl-check-list li { font-size: 0.82rem; padding: 0.3rem 0; }

/* Divider */
.rapp-info-panel__body .rl-divider { margin: 1rem 0; }
.rapp-info-panel__body .rl-divider span { font-size: 0.65rem; }

/* Back link */
.rapp-info-panel__body .rl-back { padding: 1.25rem 0 1.5rem; }
.rapp-info-panel__body .rl-back a { font-size: 0.82rem; }

/* Scrollbar styling */
.rapp-info-panel__body { scrollbar-width: thin; scrollbar-color: rgba(20,184,166,0.2) transparent; }
.rapp-info-panel__body::-webkit-scrollbar { width: 5px; }
.rapp-info-panel__body::-webkit-scrollbar-track { background: transparent; }
.rapp-info-panel__body::-webkit-scrollbar-thumb { background: rgba(20,184,166,0.2); border-radius: 9999px; }
.rapp-info-panel__body::-webkit-scrollbar-thumb:hover { background: rgba(20,184,166,0.35); }

@media (max-width: 600px) {
  .rapp-info-panel {
    top: auto; left: 8px; right: 8px; bottom: 8px;
    transform: none; width: auto; max-height: 85vh;
    animation: rapp-info-in-mobile 0.3s cubic-bezier(0.16, 1, 0.3, 1);
  }
  @keyframes rapp-info-in-mobile {
    from { opacity: 0; transform: translateY(20px); }
    to { opacity: 1; transform: translateY(0); }
  }
  .rapp-info-panel__body .rl-grid-2,
  .rapp-info-panel__body .rl-grid-3,
  .rapp-info-panel__body .rl-grid-4 { grid-template-columns: 1fr !important; }
}
`;

// ── Module sub-navigation ──

const SUBNAV_CSS = `
.rapp-subnav {
  display: flex;
  align-items: center;
  gap: 0.375rem;
  padding: 0.5rem 1rem;
  overflow-x: auto;
  border-bottom: 1px solid var(--rs-border);
  background: var(--rs-bg-surface);
  scrollbar-width: none;
  flex-shrink: 0;
  z-index: 100;
}
.rapp-subnav::-webkit-scrollbar { display: none; }
@media (max-width: 640px) {
  .rapp-subnav {
    padding: 0.375rem 0.75rem;
    gap: 0.25rem;
  }
}
@media (max-width: 480px) {
  .rapp-nav-pill {
    padding: 0.25rem 0.5rem;
    font-size: 0.75rem;
  }
}
.rapp-nav-pill {
  padding: 0.3125rem 0.75rem;
  border-radius: 999px;
  font-size: 0.8125rem;
  white-space: nowrap;
  color: var(--rs-text-secondary);
  text-decoration: none;
  border: 1px solid transparent;
  transition: color 0.15s, background 0.15s, border-color 0.15s;
}
.rapp-nav-pill:hover { color: var(--rs-text-primary); background: var(--rs-bg-surface-raised); }
.rapp-nav-pill--active {
  color: var(--rs-text-primary);
  background: var(--rs-bg-surface-raised);
  border-color: var(--rs-border);
  font-weight: 500;
}
.rapp-nav-pill--external {
  margin-left: auto;
  color: var(--rs-text-muted);
  font-size: 0.75rem;
  border: 1px dashed var(--rs-border);
}
.rapp-nav-pill--external:hover { color: var(--rs-text-primary); border-style: solid; }
`;

/** Build the module sub-nav bar from outputPaths + subPageInfos. */
function renderModuleSubNav(moduleId: string, spaceSlug: string, modules: ModuleInfo[], isSubdomain?: boolean): string {
	if (moduleId === 'rspace') return ''; // canvas page has its own chrome
	const mod = modules.find(m => m.id === moduleId);
	if (!mod) return '';

	type NavItem = { path: string; label: string; icon?: string };
	const items: NavItem[] = [];

	if (mod.outputPaths) {
		for (const op of mod.outputPaths) {
			items.push({ path: op.path, label: op.name, icon: op.icon });
		}
	}
	if (mod.subPageInfos) {
		for (const sp of mod.subPageInfos) {
			// Skip subPageInfos that overlap with outputPaths
			if (!items.find(i => i.path === sp.path)) {
				items.push({ path: sp.path, label: sp.title });
			}
		}
	}

	// Skip subnav when there are no navigable items (module uses internal tabs)
	if (items.length === 0 && !mod.externalApp) return '';

	const base = (isSubdomain ?? IS_PRODUCTION) ? `/${escapeAttr(moduleId)}` : `/${escapeAttr(spaceSlug)}/${escapeAttr(moduleId)}`;

	const minimizeBtn = `<button class="rapp-minimize-btn" id="header-minimize-btn" title="Minimize headers"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"/></svg></button>`;

	const pills = [
		`<a class="rapp-nav-pill" href="${base}" data-subnav-root>${brandedAppName(mod.name)}</a>`,
		...items.map(it =>
			`<a class="rapp-nav-pill" href="${base}/${escapeAttr(it.path)}">${it.icon ? escapeHtml(it.icon) + ' ' : ''}${escapeHtml(it.label)}</a>`
		),
		...(mod.externalApp ? [`<a class="rapp-nav-pill rapp-nav-pill--external" href="${escapeAttr(mod.externalApp.url)}" target="_blank" rel="noopener">Open ${escapeHtml(mod.externalApp.name)} &#8599;</a>`] : []),
	];

	return `<nav class="rapp-subnav" id="rapp-subnav">${pills.join('')}${minimizeBtn}</nav>
<script>(function(){var ps=document.querySelectorAll('#rapp-subnav .rapp-nav-pill'),p=location.pathname.replace(/\\/$/,'');var matched=false;ps.forEach(function(a){var h=a.getAttribute('href');if(h&&h===p){a.classList.add('rapp-nav-pill--active');matched=true}else if(h&&p.startsWith(h+'/')&&!a.hasAttribute('data-subnav-root')){a.classList.add('rapp-nav-pill--active');matched=true}});if(!matched){var root=document.querySelector('[data-subnav-root]');if(root)root.classList.add('rapp-nav-pill--active')}})()</script>`;
}

// ── rApp tab bar (in-page tabs via URL subpaths) ──

const TABBAR_CSS = `
.rapp-tabbar {
  display: flex;
  gap: 0.375rem;
  padding: 0.375rem 1rem;
  overflow-x: auto;
  border-bottom: 1px solid var(--rs-border);
  background: var(--rs-bg-surface);
  scrollbar-width: none;
  flex-shrink: 0;
  z-index: 100;
}
.rapp-tabbar::-webkit-scrollbar { display: none; }
@media (max-width: 640px) {
  .rapp-tabbar {
    padding: 0.25rem 0.75rem;
    gap: 0.25rem;
  }
}
`;

// ── Bug report floating button + modal ──

const BUG_REPORT_CSS = `
#rspace-bug-btn {
  position: fixed; bottom: 20px; right: 20px; z-index: 9990;
  width: 36px; height: 36px; border-radius: 50%;
  background: var(--rs-bg-surface, #1e293b); border: 1px solid var(--rs-border, #334155);
  color: var(--rs-text-muted, #94a3b8); cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  opacity: 0.45; transition: opacity 0.2s, transform 0.2s;
  padding: 0; box-shadow: 0 2px 8px rgba(0,0,0,0.3);
}
#rspace-bug-btn:hover { opacity: 1; transform: scale(1.1); }
#rspace-bug-btn svg { width: 18px; height: 18px; }
html.rspace-embedded #rspace-bug-btn { display: none !important; }
#rspace-bug-overlay {
  display: none; position: fixed; inset: 0; z-index: 10002;
  background: rgba(0,0,0,0.5); backdrop-filter: blur(4px);
}
#rspace-bug-overlay.open { display: flex; align-items: center; justify-content: center; }
#rspace-bug-modal {
  z-index: 10003; width: 90vw; max-width: 480px; max-height: 85vh; overflow-y: auto;
  background: var(--rs-bg-surface, #1e293b); border: 1px solid var(--rs-border, #334155);
  border-radius: 12px; padding: 20px; color: var(--rs-text, #e2e8f0);
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
#rspace-bug-modal h3 { margin: 0 0 16px; font-size: 16px; color: var(--rs-error, #f87171); }
#rspace-bug-modal label { display: block; font-size: 12px; font-weight: 600; color: var(--rs-text-muted, #94a3b8); margin: 10px 0 4px; }
#rspace-bug-modal textarea,
#rspace-bug-modal input[type="text"] {
  width: 100%; box-sizing: border-box; padding: 8px; font-size: 13px;
  background: var(--rs-bg, #0f172a); color: var(--rs-text, #e2e8f0);
  border: 1px solid var(--rs-border, #334155); border-radius: 6px; resize: vertical;
}
#rspace-bug-modal input[type="file"] { font-size: 12px; color: var(--rs-text-muted, #94a3b8); margin-top: 2px; }
#rspace-bug-submit {
  margin-top: 14px; width: 100%; padding: 10px; font-size: 14px; font-weight: 600;
  background: var(--rs-primary, #14b8a6); color: #fff; border: none; border-radius: 6px; cursor: pointer;
}
#rspace-bug-submit:hover { filter: brightness(1.1); }
#rspace-bug-submit:disabled { opacity: 0.5; cursor: not-allowed; }
#rspace-bug-status { margin-top: 8px; font-size: 12px; text-align: center; min-height: 1.2em; }
`;

function renderBugReportWidget(): string {
  return `<button id="rspace-bug-btn" title="Report a bug" aria-label="Report a bug">
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2l1.88 1.88"/><path d="M14.12 3.88 16 2"/><path d="M9 7.13v-1a3.003 3.003 0 1 1 6 0v1"/><path d="M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6"/><path d="M12 20v-9"/><path d="M6.53 9C4.6 8.8 3 7.1 3 5"/><path d="M6 13H2"/><path d="M3 21c0-2.1 1.7-3.9 3.8-4"/><path d="M20.97 5c0 2.1-1.6 3.8-3.5 4"/><path d="M22 13h-4"/><path d="M17.2 17c2.1.1 3.8 1.9 3.8 4"/></svg>
</button>
<div id="rspace-bug-overlay">
  <div id="rspace-bug-modal">
    <h3>Report a Bug</h3>
    <label for="bug-errors">Recent errors</label>
    <textarea id="bug-errors" rows="3" readonly placeholder="No errors captured"></textarea>
    <label for="bug-device">Device info</label>
    <input type="text" id="bug-device" readonly>
    <label for="bug-comments">What happened?</label>
    <textarea id="bug-comments" rows="4" placeholder="Describe what you were doing when the issue occurred..."></textarea>
    <label for="bug-screenshot">Screenshot (optional, max 3 MB)</label>
    <input type="file" id="bug-screenshot" accept="image/*">
    <button id="rspace-bug-submit">Send Report</button>
    <div id="rspace-bug-status"></div>
  </div>
</div>`;
}

function renderTabBar(tabs: Array<{ id: string; label: string; icon?: string }>, activeTab: string | undefined, basePath: string): string {
	if (tabs.length === 0) return '';

	const active = activeTab || tabs[0]?.id || '';
	const base = basePath;

	const pills = tabs.map(t => {
		const isActive = t.id === active;
		return `<a class="rapp-nav-pill${isActive ? ' rapp-nav-pill--active' : ''}" href="${base}/${escapeAttr(t.id)}" data-tab-id="${escapeAttr(t.id)}">${t.icon ? escapeHtml(t.icon) + ' ' : ''}${escapeHtml(t.label)}</a>`;
	}).join('');

	return `<nav class="rapp-tabbar" id="rapp-tabbar">${pills}</nav>
<script>(function(){
  // Dispatch initial tab on load so components can read it
  document.dispatchEvent(new CustomEvent('rapp-tab-change', { detail: { tab: '${escapeAttr(active)}' } }));
})()</script>`;
}

// ── Module landing page (bare-domain rspace.online/{moduleId}) ──

export interface ModuleLandingOptions {
	/** The module to render a landing page for */
	module: ModuleInfo;
	/** All available modules (for app switcher) */
	modules: ModuleInfo[];
	/** Theme */
	theme?: "dark" | "light";
	/** Rich body HTML from a module's landing.ts (replaces generic hero) */
	bodyHTML?: string;
}

export function renderModuleLanding(opts: ModuleLandingOptions): string {
	const { module: mod, modules, theme = "dark", bodyHTML } = opts;
	const moduleListJSON = JSON.stringify(modules);
	const demoUrl = `https://demo.rspace.online/${mod.id}`;

	const cssBlock = bodyHTML
		? `<style>${MODULE_LANDING_CSS}</style>\n  <style>${RICH_LANDING_CSS}</style>`
		: `<style>${MODULE_LANDING_CSS}</style>`;

	const bodyContent = bodyHTML
		? bodyHTML
		: `<div class="ml-hero">
    <div class="ml-container">
      <span class="ml-icon">${mod.icon}</span>
      <h1 class="ml-name">${brandedAppName(mod.name)}</h1>
      <p class="ml-desc">${escapeHtml(mod.description)}</p>
      <div class="ml-ctas">
        <a href="${demoUrl}" class="ml-cta-primary" id="ml-primary">Try Demo</a>
        <a href="/create-space" class="ml-cta-secondary">Create a Space</a>
      </div>
    </div>
  </div>
  <div class="ml-back">
    <a href="/">← Back to rSpace</a>
  </div>`;

	return versionAssetUrls(`<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
  <link rel="icon" type="image/png" href="/favicon.png">
  ${faviconScript(mod.id)}
  <link rel="manifest" href="/manifest.json">
  <meta name="theme-color" content="#0f172a" media="(prefers-color-scheme: dark)">
  <meta name="theme-color" content="#f5f5f0" media="(prefers-color-scheme: light)">
  <link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
  <meta name="mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-title" content="rSpace">
  <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
  <title>${escapeHtml(mod.name)} — rSpace</title>
  <script>(function(){var t=localStorage.getItem('canvas-theme');if(!t)t=matchMedia('(prefers-color-scheme:light)').matches?'light':'dark';document.documentElement.setAttribute('data-theme',t);var b=localStorage.getItem('canvas-bg')||'grid';document.documentElement.setAttribute('data-canvas-bg',b)})()</script>
  ${COMPAT_GATE}
  ${COMPAT_POLYFILLS}
  <link rel="stylesheet" href="/theme.css">
  <link rel="stylesheet" href="/shell.css">
  ${cssBlock}
  <script defer src="/collect.js" data-website-id="6ee7917b-0ed7-44cb-a4c8-91037638526b"></script>
</head>
<body>
  <header class="rstack-header">
    <div class="rstack-header__left">
      <a href="/" style="display:flex;align-items:center;margin-right:4px"><img src="/favicon.png" alt="rSpace" class="rstack-header__logo"></a>
      <rstack-app-switcher current="${escapeAttr(mod.id)}"></rstack-app-switcher>
      <rstack-space-switcher current="" name="Spaces"></rstack-space-switcher>
    </div>
    <div class="rstack-header__center">
      <rstack-mi></rstack-mi>
    </div>
    <div class="rstack-header__right">
      <rstack-collab-overlay module-id="${escapeAttr(mod.id)}" mode="badge-only"></rstack-collab-overlay>
      <rstack-identity></rstack-identity>
    </div>
  </header>
  ${bodyContent}
  <script type="module">
    import '/shell.js';
    if ("serviceWorker" in navigator && location.hostname !== "localhost") {
      navigator.serviceWorker.register("/sw.js?v=9").catch(() => {});
    }
    document.querySelector('rstack-app-switcher')?.setModules(${moduleListJSON});
    try {
      var raw = localStorage.getItem('encryptid_session');
      var sw = document.querySelector('rstack-space-switcher');
      if (raw) {
        var session = JSON.parse(raw);
        if (session?.claims?.username) {
          var username = session.claims.username.toLowerCase();
          var primary = document.getElementById('ml-primary');
          if (primary) {
            primary.textContent = 'Go to My Space';
            primary.href = 'https://' + username + '.rspace.online/${escapeAttr(mod.id)}';
          }
          if (sw) { sw.setAttribute('current', username); sw.setAttribute('name', username + "'s Space"); }
        }
      } else {
        if (sw) { sw.setAttribute('current', 'demo'); sw.setAttribute('name', 'demo'); }
      }
    } catch(e) {}
  </script>
</body>
</html>`);
}

export const MODULE_LANDING_CSS = `
body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  background: var(--rs-bg-page); color: var(--rs-text-primary);
  min-height: 100vh;
  display: flex; flex-direction: column; align-items: center;
  padding-top: 56px;
}
.ml-hero {
  display: flex; flex-direction: column; align-items: center;
  justify-content: center; min-height: calc(80vh - 56px); width: 100%;
}
.ml-container { text-align: center; max-width: 560px; padding: 40px 20px; }
.ml-icon { font-size: 4rem; display: block; margin-bottom: 1rem; }
.ml-name {
  font-size: 2.5rem; margin-bottom: 0.75rem;
  background: var(--rs-gradient-brand);
  -webkit-background-clip: text; -webkit-text-fill-color: transparent;
}
.ml-desc { font-size: 1.15rem; color: var(--rs-text-secondary); margin-bottom: 2.5rem; line-height: 1.6; }
.ml-ctas { display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap; }
.ml-cta-primary {
  display: inline-block; padding: 14px 32px; border-radius: 8px;
  background: var(--rs-gradient-cta);
  color: white; font-size: 1rem; font-weight: 600;
  text-decoration: none; transition: transform 0.2s, box-shadow 0.2s;
}
.ml-cta-primary:hover { transform: translateY(-2px); box-shadow: 0 8px 20px rgba(20,184,166,0.3); }
.ml-cta-secondary {
  display: inline-block; padding: 14px 32px; border-radius: 8px;
  background: var(--rs-btn-secondary-bg); border: 1px solid var(--rs-border);
  color: var(--rs-text-secondary); font-size: 1rem; font-weight: 600;
  text-decoration: none; transition: transform 0.2s, border-color 0.2s, color 0.2s;
}
.ml-cta-secondary:hover { transform: translateY(-2px); border-color: var(--rs-border-strong); color: var(--rs-text-primary); }
.ml-back { padding: 2rem 0 3rem; text-align: center; }
.ml-back a { font-size: 0.85rem; color: var(--rs-text-muted); text-decoration: none; transition: color 0.2s; }
.ml-back a:hover { color: var(--rs-text-primary); }
@media (max-width: 600px) { .ml-name { font-size: 2rem; } .ml-icon { font-size: 3rem; } }
`;

export const RICH_LANDING_CSS = `
/* ── Rich Landing Page Utilities ── */
.rl-section {
  border-top: 1px solid var(--rs-border-subtle);
  padding: 4rem 1.5rem;
}
.rl-section--alt { background: var(--rs-bg-hover); }
.rl-container { max-width: 1100px; margin: 0 auto; }
.rl-hero {
  text-align: center; padding: 5rem 1.5rem 3rem;
  max-width: 820px; margin: 0 auto;
}
.rl-tagline {
  display: inline-block; font-size: 0.7rem; font-weight: 700;
  letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--rs-accent); background: rgba(20,184,166,0.1);
  border: 1px solid rgba(20,184,166,0.2);
  padding: 0.35rem 1rem; border-radius: 9999px; margin-bottom: 1.5rem;
}
.rl-heading {
  font-size: 2rem; font-weight: 700; line-height: 1.15;
  margin-bottom: 0.75rem; letter-spacing: -0.01em;
  background: var(--rs-gradient-brand);
  -webkit-background-clip: text; -webkit-text-fill-color: transparent;
  background-clip: text;
}
.rl-hero .rl-heading { font-size: 2.5rem; }
@media (min-width: 640px) { .rl-hero .rl-heading { font-size: 3rem; } }
.rl-subtitle {
  font-size: 1.25rem; font-weight: 500; color: var(--rs-text-primary);
  margin-bottom: 1rem; letter-spacing: -0.005em;
}
.rl-hero .rl-subtitle { font-size: 1.35rem; }
@media (min-width: 640px) { .rl-hero .rl-subtitle { font-size: 1.5rem; } }
.rl-subtext {
  font-size: 1.05rem; color: var(--rs-text-secondary); line-height: 1.65;
  max-width: 640px; margin: 0 auto 2rem;
}
.rl-hero .rl-subtext { font-size: 1.15rem; }

/* Grids */
.rl-grid-2 { display: grid; grid-template-columns: 1fr; gap: 1.25rem; }
.rl-grid-3 { display: grid; grid-template-columns: 1fr; gap: 1.25rem; }
.rl-grid-4 { display: grid; grid-template-columns: 1fr; gap: 1.25rem; }
@media (min-width: 640px) {
  .rl-grid-2 { grid-template-columns: repeat(2, 1fr); }
  .rl-grid-3 { grid-template-columns: repeat(3, 1fr); }
  .rl-grid-4 { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1024px) {
  .rl-grid-4 { grid-template-columns: repeat(4, 1fr); }
}

/* Card */
.rl-card {
  background: var(--rs-card-bg); border: 1px solid var(--rs-card-border);
  border-radius: 1rem; padding: 1.75rem;
  transition: border-color 0.2s;
}
.rl-card:hover { border-color: rgba(20,184,166,0.3); }
.rl-card h3 { font-size: 0.95rem; font-weight: 600; color: var(--rs-text-primary); margin-bottom: 0.5rem; }
.rl-card p { font-size: 0.875rem; color: var(--rs-text-secondary); line-height: 1.6; }
.rl-card--center { text-align: center; }

/* Step circles */
.rl-step {
  display: flex; flex-direction: column; align-items: center; text-align: center;
}
.rl-step__num {
  width: 2.5rem; height: 2.5rem; border-radius: 9999px;
  background: rgba(20,184,166,0.1); color: var(--rs-accent);
  display: flex; align-items: center; justify-content: center;
  font-size: 0.8rem; font-weight: 700; margin-bottom: 0.75rem;
}
.rl-step h3 { font-size: 0.95rem; font-weight: 600; color: var(--rs-text-primary); margin-bottom: 0.25rem; }
.rl-step p { font-size: 0.82rem; color: var(--rs-text-secondary); line-height: 1.55; }

/* CTA row */
.rl-cta-row {
  display: flex; gap: 0.75rem; justify-content: center; flex-wrap: wrap;
  margin-top: 2rem;
}
.rl-cta-primary {
  display: inline-block; padding: 0.8rem 2rem; border-radius: 0.5rem;
  background: var(--rs-gradient-cta);
  color: white; font-size: 0.95rem; font-weight: 600;
  text-decoration: none; transition: transform 0.2s, box-shadow 0.2s;
}
.rl-cta-primary:hover { transform: translateY(-2px); box-shadow: 0 8px 20px rgba(20,184,166,0.3); }
.rl-cta-secondary {
  display: inline-block; padding: 0.8rem 2rem; border-radius: 0.5rem;
  background: var(--rs-btn-secondary-bg); border: 1px solid var(--rs-border);
  color: var(--rs-text-secondary); font-size: 0.95rem; font-weight: 600;
  text-decoration: none; transition: transform 0.2s, border-color 0.2s, color 0.2s;
}
.rl-cta-secondary:hover { transform: translateY(-2px); border-color: var(--rs-border-strong); color: var(--rs-text-primary); }

/* Check list */
.rl-check-list { list-style: none; padding: 0; margin: 0; }
.rl-check-list li {
  display: flex; align-items: flex-start; gap: 0.5rem;
  font-size: 0.875rem; color: var(--rs-text-secondary); line-height: 1.55;
  padding: 0.35rem 0;
}
.rl-check-list li::before {
  content: "✓"; color: var(--rs-accent); font-weight: 700; flex-shrink: 0; margin-top: 0.05em;
}
.rl-check-list li strong { color: var(--rs-text-primary); font-weight: 600; }

/* Badge */
.rl-badge {
  display: inline-block; font-size: 0.65rem; font-weight: 700;
  color: white; background: var(--rs-accent);
  padding: 0.15rem 0.5rem; border-radius: 9999px;
}

/* Divider */
.rl-divider {
  display: flex; align-items: center; gap: 0.75rem; margin: 1.5rem 0;
}
.rl-divider::before, .rl-divider::after {
  content: ""; flex: 1; height: 1px; background: var(--rs-border-subtle);
}
.rl-divider span { font-size: 0.75rem; color: var(--rs-text-muted); white-space: nowrap; }

/* Icon box */
.rl-icon-box {
  width: 3rem; height: 3rem; border-radius: 0.75rem;
  background: rgba(20,184,166,0.12); color: var(--rs-accent);
  display: flex; align-items: center; justify-content: center;
  font-size: 1.5rem; margin-bottom: 1rem;
}
.rl-card--center .rl-icon-box { margin: 0 auto 1rem; }

/* Integration (2-col with icon) */
.rl-integration {
  display: flex; align-items: flex-start; gap: 1rem;
  background: rgba(20,184,166,0.04); border: 1px solid rgba(20,184,166,0.15);
  border-radius: 1rem; padding: 1.5rem;
}
.rl-integration h3 { font-size: 0.95rem; font-weight: 600; color: var(--rs-text-primary); margin-bottom: 0.35rem; }
.rl-integration p { font-size: 0.85rem; color: var(--rs-text-secondary); line-height: 1.55; }

/* Back link */
.rl-back { padding: 2rem 0 3rem; text-align: center; }
.rl-back a { font-size: 0.85rem; color: var(--rs-text-muted); text-decoration: none; transition: color 0.2s; }
.rl-back a:hover { color: var(--rs-text-primary); }

/* Progress bar */
.rl-progress { height: 0.5rem; border-radius: 9999px; background: var(--rs-border-subtle); overflow: hidden; }
.rl-progress__fill { height: 100%; border-radius: 9999px; background: var(--rs-accent); }

/* Tier row */
.rl-tier {
  display: flex; gap: 0.5rem; margin: 1rem 0;
}
.rl-tier__item {
  flex: 1; text-align: center; border-radius: 0.5rem;
  border: 1px solid var(--rs-border-subtle); padding: 0.5rem; font-size: 0.75rem;
}
.rl-tier__item--active {
  border-color: rgba(20,184,166,0.4); background: rgba(20,184,166,0.05); color: var(--rs-accent);
}
.rl-tier__item--active strong { color: var(--rs-accent); }

/* Temporal zoom bar */
.rl-zoom-bar { display: flex; flex-direction: column; gap: 0.5rem; }
.rl-zoom-bar__row { display: flex; align-items: center; gap: 0.75rem; }
.rl-zoom-bar__label { font-size: 0.7rem; color: var(--rs-text-muted); width: 1.2rem; text-align: right; font-family: monospace; }
.rl-zoom-bar__bar {
  height: 1.5rem; border-radius: 0.375rem; background: rgba(99,102,241,0.15);
  display: flex; align-items: center; padding: 0 0.75rem;
}
.rl-zoom-bar__name { font-size: 0.75rem; font-weight: 600; color: var(--rs-text-primary); white-space: nowrap; }
.rl-zoom-bar__span { font-size: 0.6rem; color: var(--rs-text-muted); margin-left: auto; white-space: nowrap; }

/* Responsive helpers */
@media (max-width: 600px) {
  .rl-hero { padding: 3rem 1rem 2rem; }
  .rl-hero .rl-heading { font-size: 2rem; }
  .rl-section { padding: 2.5rem 1rem; }
}
`;

// ── Sub-page info page (bare-domain rspace.online/{moduleId}/{subPage}) ──

export interface SubPageInfoOptions {
	/** The sub-page info to render */
	subPage: SubPageInfo;
	/** The parent module info */
	module: ModuleInfo;
	/** All available modules (for app switcher) */
	modules: ModuleInfo[];
}

export function renderSubPageInfo(opts: SubPageInfoOptions): string {
	const { subPage, module: mod, modules } = opts;
	const moduleListJSON = JSON.stringify(modules);
	const demoUrl = `https://demo.rspace.online/${mod.id}/${subPage.path}`;

	const featuresGrid = subPage.features?.length
		? `<div class="rl-section">
    <div class="rl-container">
      <div class="rl-grid-3">
        ${subPage.features.map(f => `<div class="rl-card rl-card--center">
          <div class="rl-icon-box">${f.icon}</div>
          <h3>${escapeHtml(f.title)}</h3>
          <p>${escapeHtml(f.text)}</p>
        </div>`).join("\n        ")}
      </div>
    </div>
  </div>`
		: "";

	const bodyContent = subPage.bodyHTML
		? subPage.bodyHTML()
		: `<div class="rl-hero">
    <span class="rl-tagline">${escapeHtml(subPage.tagline)}</span>
    <h1 class="rl-heading">${escapeHtml(subPage.title)}</h1>
    <p class="rl-subtext">${escapeHtml(subPage.description)}</p>
    <div class="rl-cta-row">
      <a href="${demoUrl}" class="rl-cta-primary" id="sp-primary">Try Demo</a>
      <a href="/create-space" class="rl-cta-secondary">Create a Space</a>
    </div>
  </div>
  ${featuresGrid}
  <div class="rl-back">
    <a href="/${escapeAttr(mod.id)}">← Back to ${brandedAppName(mod.name)}</a>
  </div>`;

	return versionAssetUrls(`<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
  <link rel="icon" type="image/png" href="/favicon.png">
  ${faviconScript(mod.id)}
  <link rel="manifest" href="/manifest.json">
  <meta name="theme-color" content="#0f172a" media="(prefers-color-scheme: dark)">
  <meta name="theme-color" content="#f5f5f0" media="(prefers-color-scheme: light)">
  <link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
  <meta name="mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-title" content="rSpace">
  <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
  <title>${escapeHtml(subPage.title)} — ${escapeHtml(mod.name)} | rSpace</title>
  <script>(function(){var t=localStorage.getItem('canvas-theme');if(!t)t=matchMedia('(prefers-color-scheme:light)').matches?'light':'dark';document.documentElement.setAttribute('data-theme',t);var b=localStorage.getItem('canvas-bg')||'grid';document.documentElement.setAttribute('data-canvas-bg',b)})()</script>
  ${COMPAT_GATE}
  ${COMPAT_POLYFILLS}
  <link rel="stylesheet" href="/theme.css">
  <link rel="stylesheet" href="/shell.css">
  <style>${MODULE_LANDING_CSS}</style>
  <style>${RICH_LANDING_CSS}</style>
  <script defer src="/collect.js" data-website-id="6ee7917b-0ed7-44cb-a4c8-91037638526b"></script>
</head>
<body>
  <header class="rstack-header">
    <div class="rstack-header__left">
      <a href="/" style="display:flex;align-items:center;margin-right:4px"><img src="/favicon.png" alt="rSpace" class="rstack-header__logo"></a>
      <rstack-app-switcher current="${escapeAttr(mod.id)}"></rstack-app-switcher>
      <rstack-space-switcher current="" name="Spaces"></rstack-space-switcher>
    </div>
    <div class="rstack-header__center">
      <rstack-mi></rstack-mi>
    </div>
    <div class="rstack-header__right">
      <rstack-collab-overlay module-id="${escapeAttr(mod.id)}" mode="badge-only"></rstack-collab-overlay>
      <rstack-identity></rstack-identity>
    </div>
  </header>
  ${bodyContent}
  <script type="module">
    import '/shell.js';
    if ("serviceWorker" in navigator && location.hostname !== "localhost") {
      navigator.serviceWorker.register("/sw.js?v=9").catch(() => {});
    }
    document.querySelector('rstack-app-switcher')?.setModules(${moduleListJSON});
    try {
      var raw = localStorage.getItem('encryptid_session');
      var sw = document.querySelector('rstack-space-switcher');
      if (raw) {
        var session = JSON.parse(raw);
        if (session?.claims?.username) {
          var username = session.claims.username.toLowerCase();
          var primary = document.getElementById('sp-primary');
          if (primary) {
            primary.textContent = 'Open in My Space';
            primary.href = 'https://' + username + '.rspace.online/${escapeAttr(mod.id)}/${escapeAttr(subPage.path)}';
          }
          if (sw) { sw.setAttribute('current', username); sw.setAttribute('name', username + "'s Space"); }
        }
      } else {
        if (sw) { sw.setAttribute('current', 'demo'); sw.setAttribute('name', 'demo'); }
      }
    } catch(e) {}
  </script>
</body>
</html>`);
}

// ── Onboarding page (empty rApp state) ──

export interface OnboardingOptions {
	moduleId: string;
	moduleName: string;
	moduleIcon: string;
	moduleDescription: string;
	spaceSlug: string;
	modules: ModuleInfo[];
	/** Pre-rendered landing page HTML from the module (feature cards, etc.) */
	landingHTML?: string;
	/** Module-specific onboarding CTAs (import, connect, create) */
	onboardingActions?: OnboardingAction[];
}

export function renderOnboarding(opts: OnboardingOptions): string {
	const { moduleId, moduleName, moduleIcon, moduleDescription, spaceSlug, modules, landingHTML, onboardingActions } = opts;
	const demoUrl = `/${moduleId}/demo`;
	const templateUrl = `/${moduleId}/template`;

	const featuresBlock = landingHTML
		? `<div class="onboarding__features">${landingHTML}</div>`
		: '';

	// Render action cards if module declares them
	let actionsBlock = '';
	if (onboardingActions?.length) {
		const cards = onboardingActions.map((a) => {
			const resolvedHref = a.href?.replace(/\{space\}/g, spaceSlug) ?? '#';
			const resolvedEndpoint = a.upload?.endpoint?.replace(/\{space\}/g, spaceSlug) ?? '';
			if (a.type === 'upload') {
				const inputId = `upload-${Math.random().toString(36).slice(2, 8)}`;
				return `<button class="onboarding__action" data-upload-endpoint="${escapeAttr(resolvedEndpoint)}" data-upload-input="${inputId}" type="button">
					<span class="onboarding__action-icon">${a.icon}</span>
					<span class="onboarding__action-label">${escapeHtml(a.label)}</span>
					<span class="onboarding__action-desc">${escapeHtml(a.description)}</span>
					<input type="file" id="${inputId}" accept="${escapeAttr(a.upload!.accept)}" hidden>
				</button>`;
			}
			return `<a href="${escapeAttr(resolvedHref)}" class="onboarding__action">
				<span class="onboarding__action-icon">${a.icon}</span>
				<span class="onboarding__action-label">${escapeHtml(a.label)}</span>
				<span class="onboarding__action-desc">${escapeHtml(a.description)}</span>
			</a>`;
		}).join('\n');

		actionsBlock = `
		<div class="onboarding__divider"><span>or connect your data</span></div>
		<div class="onboarding__actions">${cards}</div>`;
	}

	const uploadScript = onboardingActions?.some(a => a.type === 'upload') ? `
	<script>
	(function(){
		document.querySelectorAll('[data-upload-endpoint]').forEach(function(card){
			var inputId = card.getAttribute('data-upload-input');
			var endpoint = card.getAttribute('data-upload-endpoint');
			var input = document.getElementById(inputId);
			if(!input) return;
			card.addEventListener('click', function(){ input.click(); });
			input.addEventListener('change', function(){
				if(!input.files||!input.files.length) return;
				var fd = new FormData();
				fd.append('file', input.files[0]);
				var headers = {};
				try {
					var raw = localStorage.getItem('encryptid_session');
					if(raw){ var s=JSON.parse(raw); if(s&&s.accessToken) headers['Authorization']='Bearer '+s.accessToken; }
				}catch(e){}
				card.style.opacity='0.5'; card.style.pointerEvents='none';
				fetch(endpoint, { method:'POST', body:fd, headers:headers })
					.then(function(r){ if(!r.ok) throw new Error('Upload failed: '+r.status); return r.json(); })
					.then(function(){ location.reload(); })
					.catch(function(err){ alert(err.message); card.style.opacity='1'; card.style.pointerEvents=''; });
			});
		});
	})();
	</script>` : '';

	const body = `
	<div class="onboarding">
		<div class="onboarding__card">
			<div class="onboarding__glow"></div>
			<span class="onboarding__icon">${moduleIcon}</span>
			<h1 class="onboarding__title">${brandedAppName(moduleName)}</h1>
			<p class="onboarding__desc">${escapeHtml(moduleDescription)}</p>
			<p class="onboarding__hint">This app hasn't been used in <strong>${escapeHtml(spaceSlug)}</strong> yet. Load sample data to explore, or jump into the public demo.</p>
			<div class="onboarding__ctas">
				<a href="${escapeAttr(templateUrl)}" class="onboarding__btn onboarding__btn--primary">
					<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0"><path d="M8 3v10M3 8h10"/></svg>
					Load Sample Data
				</a>
				<a href="${escapeAttr(demoUrl)}" class="onboarding__btn onboarding__btn--secondary">View with Demo Data</a>
			</div>
			${actionsBlock}
		</div>
		${featuresBlock}
	</div>
	${uploadScript}`;

	return renderShell({
		title: `${moduleName} — ${spaceSlug} | rSpace`,
		moduleId,
		spaceSlug,
		modules,
		body,
		styles: `<style>${ONBOARDING_CSS}</style>${landingHTML ? `<style>${RICH_LANDING_CSS}</style>` : ''}`,
	});
}

const ONBOARDING_CSS = `
.onboarding {
  display: flex; flex-direction: column; align-items: center;
  min-height: calc(80vh - 56px); padding: 3rem 1.5rem 2rem;
}
.onboarding__card {
  position: relative; text-align: center; max-width: 520px; width: 100%;
  padding: 2.5rem 2rem; margin-bottom: 2rem;
  background: var(--rs-bg-surface); border: 1px solid var(--rs-border);
  border-radius: 16px; overflow: hidden;
  box-shadow: 0 4px 24px rgba(0,0,0,0.12);
}
.onboarding__glow {
  position: absolute; top: -60%; left: 50%; transform: translateX(-50%);
  width: 300px; height: 300px; border-radius: 50%;
  background: radial-gradient(circle, rgba(20,184,166,0.08) 0%, transparent 70%);
  pointer-events: none;
}
.onboarding__icon { position: relative; font-size: 3.5rem; display: block; margin-bottom: 1rem; }
.onboarding__title {
  position: relative;
  font-size: 2rem; margin: 0 0 0.75rem; font-weight: 700;
  background: var(--rs-gradient-brand);
  -webkit-background-clip: text; -webkit-text-fill-color: transparent;
  background-clip: text;
}
.onboarding__desc {
  position: relative;
  font-size: 1.05rem; color: var(--rs-text-secondary);
  line-height: 1.6; margin: 0 0 1rem;
}
.onboarding__hint {
  position: relative;
  font-size: 0.8125rem; color: var(--rs-text-muted);
  line-height: 1.5; margin: 0 0 1.75rem;
}
.onboarding__hint strong { color: var(--rs-text-secondary); font-weight: 600; }
.onboarding__ctas {
  position: relative;
  display: flex; gap: 0.75rem; justify-content: center; flex-wrap: wrap;
}
.onboarding__btn {
  display: inline-flex; align-items: center; gap: 0.4rem;
  padding: 0.75rem 1.75rem; border-radius: 0.5rem;
  font-size: 0.95rem; font-weight: 600; text-decoration: none;
  transition: transform 0.2s, box-shadow 0.2s;
}
.onboarding__btn:hover { transform: translateY(-2px); }
.onboarding__btn--primary {
  background: var(--rs-gradient-cta); color: white;
  box-shadow: 0 2px 8px rgba(20,184,166,0.3);
}
.onboarding__btn--primary:hover { box-shadow: 0 8px 20px rgba(20,184,166,0.3); }
.onboarding__btn--secondary {
  background: var(--rs-btn-secondary-bg); border: 1px solid var(--rs-border);
  color: var(--rs-text-secondary);
}
.onboarding__btn--secondary:hover { border-color: var(--rs-border-strong); color: var(--rs-text-primary); }
.onboarding__features {
  width: 100%; max-width: 1100px;
  border-top: 1px solid var(--rs-border-subtle);
  padding-top: 2rem; margin-top: 1rem;
}
.onboarding__divider {
  position: relative; display: flex; align-items: center; gap: 1rem;
  margin: 2rem 0 1.25rem; color: var(--rs-text-muted); font-size: 0.8125rem;
}
.onboarding__divider::before, .onboarding__divider::after {
  content: ''; flex: 1; height: 1px; background: var(--rs-border);
}
.onboarding__actions {
  position: relative;
  display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 0.75rem; width: 100%;
}
.onboarding__action {
  display: flex; flex-direction: column; align-items: center; gap: 0.25rem;
  padding: 1.25rem 1rem; border-radius: 12px;
  border: 1px solid var(--rs-border); background: var(--rs-bg-surface);
  text-decoration: none; color: var(--rs-text-primary); cursor: pointer;
  transition: transform 0.2s, border-color 0.2s, box-shadow 0.2s;
  font-family: inherit; font-size: inherit; text-align: center;
}
.onboarding__action:hover {
  transform: translateY(-2px); border-color: var(--rs-border-strong);
  box-shadow: 0 4px 12px rgba(0,0,0,0.08);
}
.onboarding__action-icon { font-size: 1.75rem; margin-bottom: 0.25rem; }
.onboarding__action-label { font-size: 0.875rem; font-weight: 600; }
.onboarding__action-desc { font-size: 0.75rem; color: var(--rs-text-muted); line-height: 1.4; }
@media (max-width: 600px) {
  .onboarding { padding: 2rem 1rem 1.5rem; }
  .onboarding__title { font-size: 1.6rem; }
  .onboarding__icon { font-size: 2.5rem; }
  .onboarding__actions { grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); }
}
`;

// ── Access denied page ──

export function renderAccessDenied(opts: { spaceSlug: string; modules: ModuleInfo[]; userSlug?: string }): string {
	const { spaceSlug, modules, userSlug } = opts;
	const homeHref = userSlug
		? `https://${userSlug}.rspace.online/`
		: `https://rspace.online/`;
	const homeLabel = userSlug ? `Return to ${userSlug}` : "Return to rSpace";

	const body = `
	<div class="access-denied">
		<div class="access-denied__card">
			<div class="access-denied__icon">&#x1f512;</div>
			<h1 class="access-denied__title">Private Space</h1>
			<p class="access-denied__desc">You don't have access to <strong>${escapeHtml(spaceSlug)}</strong>.</p>
			<p class="access-denied__hint">This space is private. Ask the owner to invite you as a member.</p>
			<a href="${escapeAttr(homeHref)}" class="access-denied__btn">${escapeHtml(homeLabel)}</a>
		</div>
	</div>`;

	return renderShell({
		title: `Access Denied — ${spaceSlug} | rSpace`,
		moduleId: "rspace",
		spaceSlug,
		modules,
		body,
		styles: `<style>${ACCESS_DENIED_CSS}</style>`,
	});
}

const ACCESS_DENIED_CSS = `
.access-denied {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  min-height: calc(70vh - 56px); padding: 3rem 1.5rem 2rem;
}
.access-denied__card {
  text-align: center; max-width: 420px;
  background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08);
  border-radius: 16px; padding: 2.5rem 2rem;
}
.access-denied__icon { font-size: 3rem; margin-bottom: 1rem; }
.access-denied__title {
  font-size: 1.5rem; font-weight: 600; margin: 0 0 0.75rem;
  color: var(--color-text, #e0e0e0);
}
.access-denied__desc {
  color: var(--color-text-muted, #aaa); margin: 0 0 0.5rem; font-size: 1rem;
}
.access-denied__hint {
  color: var(--color-text-muted, #888); margin: 0 0 1.5rem; font-size: 0.875rem;
}
.access-denied__btn {
  display: inline-block; padding: 0.6rem 1.5rem; border-radius: 8px;
  background: var(--color-primary, #6366f1); color: #fff; text-decoration: none;
  font-weight: 500; font-size: 0.9rem; transition: opacity 0.15s;
}
.access-denied__btn:hover { opacity: 0.85; }
`;

// ── Demo page CSS utilities (rd-* prefix, parallel to rl-* landing pages) ──

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

/** Render rApp name with orange "r" prefix for visible HTML (not <title> tags). */
export function brandedAppName(name: string): string {
	if (name.startsWith("r") && name.length > 1 && name[1] === name[1].toUpperCase()) {
		return `<span style="color:#dc8300">r</span>${escapeHtml(name.slice(1))}`;
	}
	return escapeHtml(name);
}

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