972 lines
36 KiB
TypeScript
972 lines
36 KiB
TypeScript
/**
|
|
* Main landing page for rspace.online/
|
|
*
|
|
* Server-rendered using the same shell (header, CSS, theme) as all module
|
|
* landing pages. Content is ported from the old static Next.js export and
|
|
* adapted to use the shared rl-* utility classes.
|
|
*/
|
|
|
|
import type { ModuleInfo } from "../shared/module";
|
|
import { escapeHtml, escapeAttr, brandedAppName, MODULE_LANDING_CSS, RICH_LANDING_CSS, versionAssetUrls, getSpaceShellMeta } from "./shell";
|
|
|
|
/** Category → module IDs mapping for the tabbed showcase. */
|
|
const CATEGORY_GROUPS: Record<string, { label: string; icon: string; ids: string[] }> = {
|
|
plan: { label: "Plan", icon: "📋", ids: ["rcal", "rtasks", "rschedule", "rtime"] },
|
|
create: { label: "Create", icon: "✏️", ids: ["rspace", "rdocs", "rnotes", "rdesign", "rsplat", "rpubs", "rsheets", "rbooks"] },
|
|
communicate: { label: "Communicate", icon: "💬", ids: ["rchats", "rforum", "rinbox", "rmeets", "rsocials"] },
|
|
govern: { label: "Govern", icon: "⚖️", ids: ["rchoices", "rvote", "rgov", "crowdsurf"] },
|
|
transact: { label: "Transact", icon: "💰", ids: ["rwallet", "rflows", "rexchange", "rauctions", "rcart", "rswag"] },
|
|
explore: { label: "Explore", icon: "🗺️", ids: ["rmaps", "rtrips", "rbnb", "rvnb"] },
|
|
data: { label: "Data", icon: "📊", ids: ["rdata", "rfiles", "rphotos", "rtube"] },
|
|
identity: { label: "Identity", icon: "🪪", ids: ["rnetwork"] },
|
|
ai: { label: "AI", icon: "🤖", ids: ["ragents"] },
|
|
};
|
|
|
|
export function renderMainLanding(modules: ModuleInfo[]): string {
|
|
const moduleListJSON = JSON.stringify(modules);
|
|
const demoUrl = "https://demo.rspace.online/rspace";
|
|
|
|
// ── Build categorized app panels ──
|
|
const allCategorized = new Set(Object.values(CATEGORY_GROUPS).flatMap(g => g.ids));
|
|
const uncategorized = modules.filter(m => !allCategorized.has(m.id));
|
|
|
|
const categoryKeys = Object.keys(CATEGORY_GROUPS);
|
|
// Add "other" if there are uncategorized modules
|
|
if (uncategorized.length > 0) {
|
|
categoryKeys.push("other");
|
|
}
|
|
|
|
function renderCard(m: ModuleInfo): string {
|
|
return `<a href="/${escapeAttr(m.id)}" class="lp-app-card">
|
|
<span class="lp-app-card__icon">${m.icon}</span>
|
|
<div class="lp-app-card__body">
|
|
<span class="lp-app-card__name">${brandedAppName(m.name)}</span>
|
|
${m.standaloneDomain ? `<span class="lp-app-card__domain">${escapeHtml(m.standaloneDomain)}</span>` : ""}
|
|
<span class="lp-app-card__desc">${escapeHtml(m.description)}</span>
|
|
</div>
|
|
</a>`;
|
|
}
|
|
|
|
const tabButtons = categoryKeys.map((key, i) => {
|
|
const g = CATEGORY_GROUPS[key] || { label: "Other", icon: "📦" };
|
|
return `<button class="lp-tab${i === 0 ? " lp-tab--active" : ""}" data-tab="${escapeAttr(key)}">${g.icon} ${escapeHtml(g.label)}</button>`;
|
|
}).join("\n ");
|
|
|
|
const tabPanels = categoryKeys.map((key, i) => {
|
|
const mods = key === "other"
|
|
? uncategorized
|
|
: (CATEGORY_GROUPS[key]?.ids ?? [])
|
|
.map(id => modules.find(m => m.id === id))
|
|
.filter((m): m is ModuleInfo => m != null);
|
|
return `<div class="lp-tab-panel${i === 0 ? " lp-tab-panel--active" : ""}" data-panel="${escapeAttr(key)}">
|
|
<div class="lp-app-grid">${mods.map(renderCard).join("\n")}</div>
|
|
</div>`;
|
|
}).join("\n ");
|
|
|
|
// ── Footer category columns ──
|
|
const footerColumns = Object.entries(CATEGORY_GROUPS).map(([, g]) => {
|
|
const links = g.ids
|
|
.map(id => modules.find(m => m.id === id))
|
|
.filter((m): m is ModuleInfo => m != null)
|
|
.map(m => `<a href="/${escapeAttr(m.id)}">${escapeHtml(m.name)}</a>`)
|
|
.join("\n ");
|
|
return `<div class="lp-footer-col">
|
|
<h4>${g.icon} ${escapeHtml(g.label)}</h4>
|
|
${links}
|
|
</div>`;
|
|
}).join("\n ");
|
|
|
|
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">
|
|
<link rel="apple-touch-icon" href="/logo.png">
|
|
<title>rSpace — Reclaim (you)rSpace on the Internet</title>
|
|
<meta name="description" content="rSpace is a local-first platform where groups coordinate around what they care about — without stitching together a dozen corporate apps. ${modules.length} composable tools, encrypted and yours.">
|
|
|
|
<!-- Open Graph -->
|
|
<meta property="og:type" content="website">
|
|
<meta property="og:url" content="https://rspace.online">
|
|
<meta property="og:title" content="rSpace — Reclaim (you)rSpace on the Internet">
|
|
<meta property="og:description" content="rSpace is a local-first platform where groups coordinate around what they care about — without stitching together a dozen corporate apps. ${modules.length} composable tools, encrypted and yours.">
|
|
<meta property="og:image" content="https://rspace.online/og-image.png">
|
|
<meta property="og:image:width" content="1200">
|
|
<meta property="og:image:height" content="630">
|
|
<meta property="og:site_name" content="rSpace">
|
|
|
|
<!-- Twitter -->
|
|
<meta name="twitter:card" content="summary_large_image">
|
|
<meta name="twitter:title" content="rSpace — Reclaim (you)rSpace on the Internet">
|
|
<meta name="twitter:description" content="One place for your group to plan, decide, fund, and build together. ${modules.length} composable tools — encrypted, local-first, yours.">
|
|
<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)})()</script>
|
|
<link rel="stylesheet" href="/theme.css">
|
|
<link rel="stylesheet" href="/shell.css">
|
|
<style>${MODULE_LANDING_CSS}</style>
|
|
<style>${RICH_LANDING_CSS}</style>
|
|
<style>${MAIN_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=""></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-identity></rstack-identity>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- 1. Hero -->
|
|
<section class="lp-hero">
|
|
<div class="lp-hero__orb lp-hero__orb--teal" aria-hidden="true"></div>
|
|
<div class="lp-hero__orb lp-hero__orb--indigo" aria-hidden="true"></div>
|
|
<div class="lp-hero__grid" aria-hidden="true"></div>
|
|
<div class="lp-hero__content">
|
|
<span class="rl-tagline">Reclaim (you)<span style="color:#dc8300">r</span><span style="color:#35b9b9">Space</span> on the internet</span>
|
|
<h1 class="lp-wordmark"><span class="lp-wordmark__r">r</span><span class="lp-wordmark__space">Space</span></h1>
|
|
<p class="lp-hero__tagline">
|
|
Coordinate around what you care about — without stitching together a dozen corporate apps.
|
|
</p>
|
|
<div class="lp-hero__ctas">
|
|
<a href="${demoUrl}" class="lp-btn lp-btn--primary" id="ml-primary">Start your Space →</a>
|
|
<a href="#ecosystem" class="lp-btn lp-btn--ghost">Explore the rApps</a>
|
|
</div>
|
|
<div class="lp-hero__badges">
|
|
<span class="lp-badge">📡 Offline-first</span>
|
|
<span class="lp-badge">🔒 Encrypted</span>
|
|
<span class="lp-badge">👥 Multiplayer</span>
|
|
<span class="lp-badge">🛠 Open Source</span>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- 2. Stats Bar -->
|
|
<div class="lp-stats">
|
|
<div class="lp-stats__inner">
|
|
<div class="lp-stat"><span class="lp-stat__num">${modules.length}</span> composable apps</div>
|
|
<div class="lp-stat"><span class="lp-stat__num">1</span> passkey for everything</div>
|
|
<div class="lp-stat"><span class="lp-stat__num">0</span> data sold to anyone</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 3. Flow Stories -->
|
|
<section class="rl-section">
|
|
<div class="rl-container">
|
|
<h2 class="lp-section-heading">Your Group. One Shared Workspace.</h2>
|
|
<p class="rl-subtext" style="text-align:center">
|
|
Everything your group needs lives in one place. Data flows between tools automatically — no copy-pasting between apps.
|
|
</p>
|
|
<div class="lp-flows">
|
|
<div class="lp-flow">
|
|
<div class="lp-flow__pills">
|
|
<span class="lp-flow__pill">📅 rCal</span>
|
|
<span class="lp-flow__arrow" aria-hidden="true">→</span>
|
|
<span class="lp-flow__pill">📋 rTasks</span>
|
|
<span class="lp-flow__arrow" aria-hidden="true">→</span>
|
|
<span class="lp-flow__pill">💬 rChats</span>
|
|
</div>
|
|
<p class="lp-flow__outcome">Schedule a meeting → auto-generates a task → notifies your space.</p>
|
|
</div>
|
|
<div class="lp-flow">
|
|
<div class="lp-flow__pills">
|
|
<span class="lp-flow__pill">☑ rChoices</span>
|
|
<span class="lp-flow__arrow" aria-hidden="true">→</span>
|
|
<span class="lp-flow__pill">💰 rWallet</span>
|
|
</div>
|
|
<p class="lp-flow__outcome">Vote passes → budget allocation releases automatically.</p>
|
|
</div>
|
|
<div class="lp-flow">
|
|
<div class="lp-flow__pills">
|
|
<span class="lp-flow__pill">🗺 rMaps</span>
|
|
<span class="lp-flow__arrow" aria-hidden="true">→</span>
|
|
<span class="lp-flow__pill">📝 rDocs</span>
|
|
</div>
|
|
<p class="lp-flow__outcome">Pin a community location → shared doc created for that place.</p>
|
|
</div>
|
|
<div class="lp-flow">
|
|
<div class="lp-flow__pills">
|
|
<span class="lp-flow__pill">⏱ rTime</span>
|
|
<span class="lp-flow__arrow" aria-hidden="true">→</span>
|
|
<span class="lp-flow__pill">📊 rData</span>
|
|
</div>
|
|
<p class="lp-flow__outcome">Log commitments → analytics update in real-time.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- 4. App Categories (tabbed) -->
|
|
<section id="ecosystem" class="rl-section rl-section--alt">
|
|
<div class="rl-container">
|
|
<h2 class="lp-section-heading">${modules.length} rApps and Growing</h2>
|
|
<p class="rl-subtext" style="text-align:center">
|
|
Each app is independent and composable. Use one, use all, mix and match.
|
|
</p>
|
|
<div class="lp-tabs">
|
|
<div class="lp-tab-bar" role="tablist">
|
|
${tabButtons}
|
|
</div>
|
|
<div class="lp-tab-panels">
|
|
${tabPanels}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- 5. How It Works -->
|
|
<section class="rl-section">
|
|
<div class="rl-container">
|
|
<h2 class="lp-section-heading">Built Offline-First</h2>
|
|
<p class="rl-subtext" style="text-align:center">
|
|
Your data lives on your device. Changes sync when you’re online,
|
|
merge automatically when you’re not.
|
|
</p>
|
|
<div class="lp-steps">
|
|
<div class="lp-step">
|
|
<div class="lp-step__num">1</div>
|
|
<h3>Local Persistence</h3>
|
|
<p>Every document stored in encrypted IndexedDB on your device. Works without internet.</p>
|
|
</div>
|
|
<div class="lp-steps__line" aria-hidden="true"></div>
|
|
<div class="lp-step">
|
|
<div class="lp-step__num">2</div>
|
|
<h3>Auto-Merge CRDT</h3>
|
|
<p>Automerge CRDTs resolve conflicts automatically. No “someone else is editing” lockouts.</p>
|
|
</div>
|
|
<div class="lp-steps__line" aria-hidden="true"></div>
|
|
<div class="lp-step">
|
|
<div class="lp-step__num">3</div>
|
|
<h3>Incremental Sync</h3>
|
|
<p>Only changed bytes travel the wire. Reconnect after days offline and catch up in seconds.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- 6. EncryptID -->
|
|
<section class="rl-section rl-section--alt">
|
|
<div class="rl-container">
|
|
<div class="rl-grid-2">
|
|
<div>
|
|
<span class="rl-tagline">EncryptID</span>
|
|
<h2 class="rl-heading">One Passkey for Everything</h2>
|
|
<p class="rl-subtext" style="margin-bottom:0.5rem;font-weight:500;color:var(--rs-text-primary)">
|
|
Secure by default, not by opt-in.
|
|
</p>
|
|
<p class="rl-subtext" style="margin-bottom:1.5rem">
|
|
Sign in once with your fingerprint or device PIN. Your passkey works
|
|
across every rApp — no passwords, no email loops,
|
|
no third-party auth providers watching over your shoulder.
|
|
</p>
|
|
<ul class="rl-check-list">
|
|
<li><strong>WebAuthn passkeys</strong> — phishing-resistant, device-bound credentials</li>
|
|
<li><strong>Guardian recovery</strong> — 2-of-3 trusted contacts restore access</li>
|
|
<li><strong>Device linking</strong> — scan a QR to add your phone or tablet</li>
|
|
<li><strong>One RP ID</strong> — works across all r*.online domains</li>
|
|
</ul>
|
|
</div>
|
|
<div style="display:flex;align-items:center;justify-content:center">
|
|
<div class="lp-encryptid-visual">
|
|
<svg class="lp-encryptid-shield" viewBox="0 0 120 140" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<path d="M60 8L10 30v40c0 32 22 55 50 62 28-7 50-30 50-62V30L60 8z" stroke="currentColor" stroke-width="3" fill="rgba(20,184,166,0.08)"/>
|
|
<path d="M60 40a14 14 0 1 0 0 28 14 14 0 0 0 0-28z" stroke="currentColor" stroke-width="2.5"/>
|
|
<circle cx="60" cy="54" r="4" fill="currentColor"/>
|
|
<path d="M48 80h24v8a12 12 0 0 1-24 0v-8z" stroke="currentColor" stroke-width="2.5" fill="rgba(20,184,166,0.12)"/>
|
|
<line x1="60" y1="80" x2="60" y2="92" stroke="currentColor" stroke-width="2"/>
|
|
</svg>
|
|
<p class="lp-encryptid-visual__label">Touch. Tap. Done.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- 7. Final CTA -->
|
|
<section class="lp-final-cta">
|
|
<div class="rl-container" style="max-width:720px;text-align:center">
|
|
<h2 class="lp-final-cta__heading">Reclaim (you)<span style="color:#dc8300;-webkit-text-fill-color:#dc8300">r</span><span style="color:#35b9b9;-webkit-text-fill-color:#35b9b9">Space</span>.</h2>
|
|
<p class="rl-subtext" style="font-size:1.15rem;line-height:1.7;text-align:center">
|
|
No algorithms deciding what you see. No ads. No data harvesting.
|
|
Just one place for your group to plan, decide, fund, and build together.
|
|
</p>
|
|
<div class="lp-hero__ctas">
|
|
<a href="${demoUrl}" class="lp-btn lp-btn--primary">Start your Space →</a>
|
|
<a href="/about" class="lp-btn lp-btn--ghost">Read the manifesto</a>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- 8. Footer -->
|
|
<footer class="lp-footer">
|
|
<div class="rl-container">
|
|
<div class="lp-footer__cols">
|
|
${footerColumns}
|
|
</div>
|
|
<div class="lp-footer__bottom">
|
|
<span class="lp-footer__tagline">Local-first · Zero-knowledge · Community-owned</span>
|
|
<div class="lp-footer__links">
|
|
<a href="/about">About</a>
|
|
<a href="/create-space">Create a Space</a>
|
|
<a href="https://auth.rspace.online">EncryptID</a>
|
|
</div>
|
|
<span class="lp-footer__tech">Built with Bun, Hono, Automerge & WebAuthn</span>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
|
|
<script type="module">
|
|
import '/shell.js';
|
|
document.querySelector('rstack-app-switcher')?.setModules(${moduleListJSON});
|
|
|
|
// Tab switching
|
|
document.querySelectorAll('.lp-tab').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
document.querySelectorAll('.lp-tab').forEach(b => b.classList.remove('lp-tab--active'));
|
|
document.querySelectorAll('.lp-tab-panel').forEach(p => p.classList.remove('lp-tab-panel--active'));
|
|
btn.classList.add('lp-tab--active');
|
|
var panel = document.querySelector('[data-panel="' + btn.dataset.tab + '"]');
|
|
if (panel) panel.classList.add('lp-tab-panel--active');
|
|
});
|
|
});
|
|
|
|
// Logged-in users: swap hero CTA + space switcher
|
|
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 \u2192';
|
|
primary.href = 'https://' + username + '.rspace.online/rspace';
|
|
}
|
|
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>`);
|
|
}
|
|
|
|
// ── Space Dashboard ──
|
|
|
|
export function renderSpaceDashboard(space: string, modules: ModuleInfo[]): string {
|
|
// Filter modules by space's enabledModules
|
|
const enabledModules = getSpaceShellMeta(space).enabledModules;
|
|
const visibleModules = enabledModules
|
|
? modules.filter(m => m.id === "rspace" || enabledModules.includes(m.id))
|
|
: modules;
|
|
const moduleListJSON = JSON.stringify(visibleModules);
|
|
const displayName = space === "demo" ? "Demo Space" : space;
|
|
const subtitle = space === "demo"
|
|
? "Explore the rSpace ecosystem — click any rApp to try it live with sample data."
|
|
: `${visibleModules.length} rApps available in this space.`;
|
|
|
|
const appCards = visibleModules
|
|
.map((m) => {
|
|
return `
|
|
<a href="/${escapeAttr(m.id)}" class="sd-card" data-module="${escapeAttr(m.id)}">
|
|
<div class="sd-card__icon">${m.icon}</div>
|
|
<div class="sd-card__body">
|
|
<h3 class="sd-card__name">${brandedAppName(m.name)}</h3>
|
|
<p class="sd-card__desc">${escapeHtml(m.description)}</p>
|
|
</div>
|
|
</a>`;
|
|
})
|
|
.join("\n");
|
|
|
|
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">
|
|
<title>${escapeHtml(displayName)} | 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)})()</script>
|
|
<link rel="stylesheet" href="/theme.css">
|
|
<link rel="stylesheet" href="/shell.css">
|
|
<style>${MODULE_LANDING_CSS}</style>
|
|
<style>${SPACE_DASHBOARD_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=""></rstack-app-switcher>
|
|
<rstack-space-switcher current="${escapeAttr(space)}" name="${escapeAttr(displayName)}"></rstack-space-switcher>
|
|
</div>
|
|
<div class="rstack-header__center">
|
|
<rstack-mi></rstack-mi>
|
|
</div>
|
|
<div class="rstack-header__right">
|
|
<rstack-identity></rstack-identity>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="sd-hero">
|
|
<h1 class="sd-hero__title">${escapeHtml(displayName)}</h1>
|
|
<p class="sd-hero__subtitle">${subtitle}</p>
|
|
</div>
|
|
|
|
<div class="sd-container">
|
|
<div class="sd-grid">
|
|
${appCards}
|
|
</div>
|
|
</div>
|
|
|
|
${space === "demo" ? `
|
|
<div class="sd-footer">
|
|
<p>Want your own space? <a href="/create-space">Create one</a> — it’s free and instant.</p>
|
|
</div>` : ""}
|
|
|
|
<script type="module">
|
|
import '/shell.js';
|
|
document.querySelector('rstack-app-switcher')?.setModules(${moduleListJSON});
|
|
|
|
// Authenticated users are redirected server-side (no JS redirect needed).
|
|
// Non-demo spaces: redirect logged-out visitors to the main domain landing.
|
|
if ('${escapeAttr(space)}' !== 'demo') {
|
|
var host = window.location.host.split(':')[0];
|
|
if (host.endsWith('.rspace.online') || host === 'rspace.online') {
|
|
window.location.replace('https://rspace.online/');
|
|
}
|
|
}
|
|
|
|
// Fix up dashboard links to be subdomain-aware
|
|
if (window.__rspaceNavUrl) {
|
|
document.querySelectorAll('.sd-card[data-module]').forEach(card => {
|
|
card.href = window.__rspaceNavUrl('${escapeAttr(space)}', card.dataset.module);
|
|
});
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>`);
|
|
}
|
|
|
|
const SPACE_DASHBOARD_CSS = `
|
|
.sd-hero {
|
|
text-align: center;
|
|
padding: 100px 1.5rem 2rem;
|
|
}
|
|
.sd-hero__title {
|
|
font-size: 2.25rem; font-weight: 700; margin: 0 0 0.5rem;
|
|
background: var(--rs-gradient-brand);
|
|
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
|
|
background-clip: text;
|
|
}
|
|
.sd-hero__subtitle {
|
|
font-size: 1.05rem; color: var(--rs-text-secondary); margin: 0;
|
|
max-width: 520px; margin: 0 auto; line-height: 1.5;
|
|
}
|
|
.sd-container {
|
|
max-width: 1100px; margin: 0 auto;
|
|
padding: 1rem 1.5rem 3rem;
|
|
}
|
|
.sd-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
gap: 0.75rem;
|
|
}
|
|
.sd-card {
|
|
display: flex; align-items: flex-start; gap: 1rem;
|
|
padding: 1rem 1.25rem;
|
|
background: var(--rs-card-bg);
|
|
border: 1px solid var(--rs-card-border);
|
|
border-radius: 0.75rem;
|
|
text-decoration: none; color: inherit;
|
|
transition: border-color 0.2s, background 0.2s, transform 0.15s;
|
|
cursor: pointer;
|
|
}
|
|
.sd-card:hover {
|
|
border-color: rgba(20,184,166,0.35);
|
|
background: rgba(20,184,166,0.04);
|
|
transform: translateY(-1px);
|
|
}
|
|
.sd-card__icon {
|
|
font-size: 1.75rem; flex-shrink: 0;
|
|
width: 2.5rem; height: 2.5rem;
|
|
display: flex; align-items: center; justify-content: center;
|
|
}
|
|
.sd-card__body { min-width: 0; }
|
|
.sd-card__name {
|
|
font-size: 0.9rem; font-weight: 600; color: var(--rs-text-primary);
|
|
margin: 0 0 0.2rem;
|
|
}
|
|
.sd-card__desc {
|
|
font-size: 0.78rem; color: var(--rs-text-muted); margin: 0;
|
|
line-height: 1.45;
|
|
}
|
|
.sd-footer {
|
|
text-align: center; padding: 2rem 1.5rem 3rem;
|
|
border-top: 1px solid var(--rs-border-subtle);
|
|
}
|
|
.sd-footer p { color: var(--rs-text-muted); font-size: 0.9rem; margin: 0; }
|
|
.sd-footer a { color: var(--rs-accent); text-decoration: none; font-weight: 600; }
|
|
.sd-footer a:hover { text-decoration: underline; }
|
|
@media (max-width: 480px) {
|
|
.sd-grid { grid-template-columns: 1fr; }
|
|
.sd-hero__title { font-size: 1.75rem; }
|
|
.sd-hero { padding-top: 80px; }
|
|
}
|
|
`;
|
|
|
|
const MAIN_LANDING_CSS = `
|
|
/* ══════════════════════════════════════════════════════════
|
|
Landing Page — lp-* prefix (no conflicts with rl-*)
|
|
══════════════════════════════════════════════════════════ */
|
|
|
|
/* ── Global background ── */
|
|
body {
|
|
background: linear-gradient(170deg, #0a0f1e 0%, #0f172a 25%, #131b2e 45%, #0e1628 65%, #0d1424 85%, #080d19 100%);
|
|
background-attachment: fixed;
|
|
}
|
|
[data-theme="light"] body {
|
|
background: linear-gradient(170deg, #f8fafc 0%, #f1f5f9 50%, #e2e8f0 100%);
|
|
}
|
|
|
|
/* ── 1. Hero ── */
|
|
.lp-hero {
|
|
position: relative;
|
|
min-height: calc(100vh - 56px);
|
|
display: flex; align-items: center; justify-content: center;
|
|
overflow: hidden;
|
|
padding: 3rem 1.5rem;
|
|
}
|
|
.lp-hero__content {
|
|
position: relative; z-index: 2;
|
|
text-align: center; max-width: 820px;
|
|
}
|
|
|
|
/* Animated orbs */
|
|
.lp-hero__orb {
|
|
position: absolute; border-radius: 50%;
|
|
pointer-events: none; z-index: 0;
|
|
filter: blur(80px);
|
|
}
|
|
.lp-hero__orb--teal {
|
|
width: 500px; height: 500px;
|
|
top: 15%; left: 20%;
|
|
background: radial-gradient(circle, rgba(20,184,166,0.25) 0%, transparent 70%);
|
|
animation: lp-orb-drift 8s ease-in-out infinite;
|
|
}
|
|
.lp-hero__orb--indigo {
|
|
width: 400px; height: 400px;
|
|
bottom: 10%; right: 15%;
|
|
background: radial-gradient(circle, rgba(99,102,241,0.2) 0%, transparent 70%);
|
|
animation: lp-orb-drift 10s ease-in-out infinite reverse;
|
|
}
|
|
[data-theme="light"] .lp-hero__orb--teal {
|
|
background: radial-gradient(circle, rgba(20,184,166,0.12) 0%, transparent 70%);
|
|
}
|
|
[data-theme="light"] .lp-hero__orb--indigo {
|
|
background: radial-gradient(circle, rgba(99,102,241,0.1) 0%, transparent 70%);
|
|
}
|
|
@keyframes lp-orb-drift {
|
|
0%, 100% { transform: translate(0, 0) scale(1); }
|
|
33% { transform: translate(30px, -20px) scale(1.05); }
|
|
66% { transform: translate(-20px, 15px) scale(0.97); }
|
|
}
|
|
|
|
/* Grid overlay */
|
|
.lp-hero__grid {
|
|
position: absolute; inset: 0; z-index: 1;
|
|
background:
|
|
linear-gradient(rgba(20,184,166,0.04) 1px, transparent 1px),
|
|
linear-gradient(90deg, rgba(20,184,166,0.04) 1px, transparent 1px);
|
|
background-size: 60px 60px;
|
|
mask-image: radial-gradient(ellipse 60% 50% at 50% 50%, black 20%, transparent 100%);
|
|
-webkit-mask-image: radial-gradient(ellipse 60% 50% at 50% 50%, black 20%, transparent 100%);
|
|
}
|
|
[data-theme="light"] .lp-hero__grid {
|
|
background:
|
|
linear-gradient(rgba(20,184,166,0.06) 1px, transparent 1px),
|
|
linear-gradient(90deg, rgba(20,184,166,0.06) 1px, transparent 1px);
|
|
background-size: 60px 60px;
|
|
}
|
|
|
|
/* Wordmark */
|
|
.lp-wordmark {
|
|
font-size: clamp(3.5rem, 8vw, 6rem);
|
|
font-weight: 700; line-height: 1.05;
|
|
margin: 0 0 1rem;
|
|
letter-spacing: -0.02em;
|
|
text-shadow: 0 0 60px rgba(20,184,166,0.15);
|
|
}
|
|
.lp-wordmark__r {
|
|
font-weight: 400;
|
|
color: #dc8300;
|
|
-webkit-text-fill-color: #dc8300;
|
|
}
|
|
.lp-wordmark__space {
|
|
background: var(--rs-gradient-brand);
|
|
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
|
|
background-clip: text;
|
|
}
|
|
|
|
.lp-hero__tagline {
|
|
font-size: clamp(1.1rem, 2.5vw, 1.4rem);
|
|
color: var(--rs-text-secondary);
|
|
margin: 0 0 2rem;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
/* CTA buttons */
|
|
.lp-hero__ctas {
|
|
display: flex; gap: 0.75rem; justify-content: center; flex-wrap: wrap;
|
|
margin-bottom: 2.5rem;
|
|
}
|
|
.lp-btn {
|
|
display: inline-flex; align-items: center; gap: 0.4rem;
|
|
padding: 0.85rem 2rem; border-radius: 0.6rem;
|
|
font-size: 0.95rem; font-weight: 600;
|
|
text-decoration: none; cursor: pointer;
|
|
transition: transform 0.2s, box-shadow 0.25s;
|
|
border: none;
|
|
}
|
|
.lp-btn:hover { transform: translateY(-2px); }
|
|
.lp-btn--primary {
|
|
background: var(--rs-gradient-cta);
|
|
color: white;
|
|
box-shadow: 0 0 20px rgba(20,184,166,0.2), 0 4px 12px rgba(20,184,166,0.15);
|
|
animation: lp-glow-pulse 3s ease-in-out infinite;
|
|
}
|
|
.lp-btn--primary:hover {
|
|
box-shadow: 0 0 30px rgba(20,184,166,0.35), 0 8px 20px rgba(20,184,166,0.2);
|
|
}
|
|
.lp-btn--ghost {
|
|
background: transparent;
|
|
color: var(--rs-text-secondary);
|
|
border: 1px solid var(--rs-border);
|
|
}
|
|
.lp-btn--ghost:hover {
|
|
border-color: var(--rs-border-strong);
|
|
color: var(--rs-text-primary);
|
|
}
|
|
@keyframes lp-glow-pulse {
|
|
0%, 100% { box-shadow: 0 0 20px rgba(20,184,166,0.2), 0 4px 12px rgba(20,184,166,0.15); }
|
|
50% { box-shadow: 0 0 30px rgba(20,184,166,0.3), 0 4px 16px rgba(20,184,166,0.2); }
|
|
}
|
|
|
|
/* Hero badges */
|
|
.lp-hero__badges {
|
|
display: flex; gap: 0.75rem; justify-content: center; flex-wrap: wrap;
|
|
}
|
|
.lp-badge {
|
|
font-size: 0.72rem; font-weight: 600;
|
|
color: var(--rs-text-secondary);
|
|
background: rgba(20,184,166,0.06);
|
|
border: 1px solid rgba(20,184,166,0.12);
|
|
padding: 0.35rem 0.85rem; border-radius: 9999px;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* ── 2. Stats Bar ── */
|
|
.lp-stats {
|
|
background: rgba(20,184,166,0.03);
|
|
border-top: 1px solid rgba(20,184,166,0.08);
|
|
border-bottom: 1px solid rgba(20,184,166,0.08);
|
|
padding: 1.5rem 1.5rem;
|
|
}
|
|
[data-theme="light"] .lp-stats {
|
|
background: rgba(20,184,166,0.04);
|
|
}
|
|
.lp-stats__inner {
|
|
max-width: 900px; margin: 0 auto;
|
|
display: flex; justify-content: center; gap: 3rem;
|
|
flex-wrap: wrap;
|
|
}
|
|
.lp-stat {
|
|
font-size: 0.9rem; color: var(--rs-text-secondary);
|
|
text-align: center; white-space: nowrap;
|
|
}
|
|
.lp-stat__num {
|
|
font-size: 1.75rem; font-weight: 800;
|
|
background: var(--rs-gradient-brand);
|
|
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
|
|
background-clip: text;
|
|
display: block; line-height: 1.2;
|
|
}
|
|
|
|
/* ── 3. Flow Stories ── */
|
|
.lp-section-heading {
|
|
font-size: 2rem; font-weight: 700;
|
|
text-align: center; margin-bottom: 0.75rem;
|
|
background: var(--rs-gradient-brand);
|
|
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
|
|
background-clip: text;
|
|
letter-spacing: -0.01em;
|
|
}
|
|
.lp-flows {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
gap: 1rem; margin-top: 1.5rem;
|
|
}
|
|
.lp-flow {
|
|
background: var(--rs-card-bg);
|
|
border: 1px solid var(--rs-card-border);
|
|
border-left: 3px solid var(--rs-accent);
|
|
border-radius: 0.75rem;
|
|
padding: 1.25rem 1.5rem;
|
|
transition: border-color 0.25s, box-shadow 0.25s, transform 0.2s;
|
|
}
|
|
.lp-flow:hover {
|
|
border-color: rgba(20,184,166,0.4);
|
|
border-left-color: var(--rs-accent);
|
|
box-shadow: 0 0 16px rgba(20,184,166,0.1);
|
|
transform: translateY(-2px);
|
|
}
|
|
.lp-flow__pills {
|
|
display: flex; align-items: center; gap: 0.5rem;
|
|
flex-wrap: wrap; margin-bottom: 0.6rem;
|
|
}
|
|
.lp-flow__pill {
|
|
font-size: 0.85rem; font-weight: 600;
|
|
color: var(--rs-text-primary);
|
|
background: rgba(20,184,166,0.08);
|
|
border: 1px solid rgba(20,184,166,0.2);
|
|
border-radius: 2rem;
|
|
padding: 0.3rem 0.85rem;
|
|
white-space: nowrap;
|
|
}
|
|
.lp-flow__arrow {
|
|
color: var(--rs-accent); font-size: 1.1rem; opacity: 0.7;
|
|
}
|
|
.lp-flow__outcome {
|
|
font-size: 0.85rem; color: var(--rs-text-secondary);
|
|
line-height: 1.5; margin: 0;
|
|
}
|
|
|
|
/* ── 4. App Categories (tabbed) ── */
|
|
.lp-tabs { margin-top: 2rem; }
|
|
.lp-tab-bar {
|
|
display: flex; gap: 0.35rem;
|
|
overflow-x: auto; scroll-snap-type: x mandatory;
|
|
padding-bottom: 0.5rem;
|
|
-webkit-overflow-scrolling: touch;
|
|
scrollbar-width: thin;
|
|
}
|
|
.lp-tab-bar::-webkit-scrollbar { height: 3px; }
|
|
.lp-tab-bar::-webkit-scrollbar-thumb { background: var(--rs-border); border-radius: 9999px; }
|
|
.lp-tab {
|
|
flex-shrink: 0; scroll-snap-align: start;
|
|
font-size: 0.8rem; font-weight: 600;
|
|
color: var(--rs-text-muted);
|
|
background: transparent;
|
|
border: 1px solid var(--rs-border-subtle);
|
|
border-radius: 9999px;
|
|
padding: 0.45rem 1rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
white-space: nowrap;
|
|
font-family: inherit;
|
|
}
|
|
.lp-tab:hover {
|
|
color: var(--rs-text-primary);
|
|
border-color: var(--rs-border);
|
|
}
|
|
.lp-tab--active {
|
|
color: white;
|
|
background: var(--rs-accent);
|
|
border-color: var(--rs-accent);
|
|
}
|
|
[data-theme="light"] .lp-tab--active {
|
|
color: white;
|
|
}
|
|
.lp-tab-panel { display: none; }
|
|
.lp-tab-panel--active { display: block; }
|
|
.lp-tab-panels { margin-top: 1.25rem; }
|
|
|
|
.lp-app-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
gap: 0.75rem;
|
|
}
|
|
.lp-app-card {
|
|
display: flex; align-items: flex-start; gap: 0.75rem;
|
|
padding: 1rem 1.25rem;
|
|
background: var(--rs-card-bg);
|
|
border: 1px solid var(--rs-card-border);
|
|
border-radius: 0.75rem;
|
|
text-decoration: none; color: inherit;
|
|
transition: border-color 0.2s, background 0.2s, transform 0.15s;
|
|
}
|
|
.lp-app-card:hover {
|
|
border-color: rgba(20,184,166,0.35);
|
|
background: rgba(20,184,166,0.04);
|
|
transform: translateY(-1px);
|
|
}
|
|
.lp-app-card__icon {
|
|
font-size: 1.5rem; flex-shrink: 0;
|
|
width: 2.25rem; height: 2.25rem;
|
|
display: flex; align-items: center; justify-content: center;
|
|
}
|
|
.lp-app-card__body {
|
|
min-width: 0; display: flex; flex-direction: column; gap: 0.1rem;
|
|
}
|
|
.lp-app-card__name {
|
|
font-size: 0.88rem; font-weight: 600; color: var(--rs-text-primary);
|
|
}
|
|
.lp-app-card__domain {
|
|
font-size: 0.62rem; font-weight: 600;
|
|
color: var(--rs-accent); opacity: 0.75;
|
|
letter-spacing: 0.02em;
|
|
}
|
|
.lp-app-card__desc {
|
|
font-size: 0.76rem; color: var(--rs-text-muted);
|
|
line-height: 1.45;
|
|
}
|
|
|
|
/* ── 5. How It Works (steps with dotted line) ── */
|
|
.lp-steps {
|
|
display: flex; align-items: flex-start; justify-content: center;
|
|
gap: 0; flex-wrap: wrap;
|
|
margin-top: 2rem;
|
|
}
|
|
.lp-step {
|
|
display: flex; flex-direction: column; align-items: center;
|
|
text-align: center; flex: 0 1 220px; padding: 0 1rem;
|
|
}
|
|
.lp-step__num {
|
|
width: 2.75rem; height: 2.75rem; border-radius: 9999px;
|
|
background: rgba(20,184,166,0.1); color: var(--rs-accent);
|
|
display: flex; align-items: center; justify-content: center;
|
|
font-size: 1rem; font-weight: 800; margin-bottom: 0.75rem;
|
|
border: 2px solid rgba(20,184,166,0.2);
|
|
}
|
|
.lp-step h3 {
|
|
font-size: 0.95rem; font-weight: 600;
|
|
color: var(--rs-text-primary); margin: 0 0 0.25rem;
|
|
}
|
|
.lp-step p {
|
|
font-size: 0.82rem; color: var(--rs-text-secondary);
|
|
line-height: 1.55; margin: 0;
|
|
}
|
|
.lp-steps__line {
|
|
width: 40px; height: 2px; flex-shrink: 0;
|
|
border-top: 2px dashed rgba(20,184,166,0.25);
|
|
margin-top: 1.35rem;
|
|
}
|
|
|
|
/* ── 6. EncryptID Visual ── */
|
|
.lp-encryptid-visual { text-align: center; }
|
|
.lp-encryptid-shield {
|
|
width: 140px; height: 160px;
|
|
color: var(--rs-accent);
|
|
filter: drop-shadow(0 0 20px rgba(20,184,166,0.2));
|
|
}
|
|
.lp-encryptid-visual__label {
|
|
font-size: 0.9rem; font-weight: 600;
|
|
color: var(--rs-text-secondary); margin-top: 1rem;
|
|
}
|
|
|
|
/* ── 7. Final CTA ── */
|
|
.lp-final-cta {
|
|
padding: 5rem 1.5rem;
|
|
border-top: 1px solid var(--rs-border-subtle);
|
|
}
|
|
.lp-final-cta__heading {
|
|
font-size: clamp(1.5rem, 4vw, 2.25rem);
|
|
font-weight: 700; margin: 0 0 1rem;
|
|
background: var(--rs-gradient-brand);
|
|
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
|
|
background-clip: text;
|
|
letter-spacing: -0.01em;
|
|
}
|
|
|
|
/* ── 8. Footer ── */
|
|
.lp-footer {
|
|
border-top: 1px solid var(--rs-border-subtle);
|
|
padding: 3rem 1.5rem 2rem;
|
|
}
|
|
.lp-footer__cols {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
|
gap: 1.5rem 1rem;
|
|
margin-bottom: 2.5rem;
|
|
}
|
|
.lp-footer-col h4 {
|
|
font-size: 0.72rem; font-weight: 700;
|
|
color: var(--rs-text-primary);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
margin: 0 0 0.6rem;
|
|
white-space: nowrap;
|
|
}
|
|
.lp-footer-col a {
|
|
display: block; font-size: 0.75rem;
|
|
color: var(--rs-text-muted);
|
|
text-decoration: none;
|
|
padding: 0.15rem 0;
|
|
transition: color 0.2s;
|
|
}
|
|
.lp-footer-col a:hover { color: var(--rs-accent); }
|
|
.lp-footer__bottom {
|
|
border-top: 1px solid var(--rs-border-subtle);
|
|
padding-top: 1.5rem;
|
|
text-align: center;
|
|
display: flex; flex-direction: column; align-items: center; gap: 0.75rem;
|
|
}
|
|
.lp-footer__tagline {
|
|
font-size: 0.8rem; font-weight: 700;
|
|
color: var(--rs-accent);
|
|
letter-spacing: 0.06em;
|
|
text-transform: uppercase;
|
|
opacity: 0.7;
|
|
}
|
|
.lp-footer__links {
|
|
display: flex; gap: 1.5rem; flex-wrap: wrap; justify-content: center;
|
|
}
|
|
.lp-footer__links a {
|
|
color: var(--rs-text-muted); font-size: 0.8rem;
|
|
text-decoration: none; transition: color 0.2s;
|
|
}
|
|
.lp-footer__links a:hover { color: var(--rs-text-primary); }
|
|
.lp-footer__tech {
|
|
color: var(--rs-text-muted); font-size: 0.72rem;
|
|
opacity: 0.6;
|
|
}
|
|
|
|
/* ── Reduced Motion ── */
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.lp-hero__orb,
|
|
.lp-btn--primary { animation: none !important; }
|
|
.lp-flow, .lp-app-card, .lp-btn { transition: none !important; }
|
|
}
|
|
|
|
/* ── Responsive ── */
|
|
@media (max-width: 640px) {
|
|
.lp-hero { padding: 2rem 1rem; min-height: calc(100vh - 56px); }
|
|
.lp-hero__badges { gap: 0.5rem; }
|
|
.lp-stats__inner { gap: 1.5rem; }
|
|
.lp-steps { flex-direction: column; align-items: center; }
|
|
.lp-steps__line {
|
|
width: 2px; height: 24px;
|
|
border-top: none;
|
|
border-left: 2px dashed rgba(20,184,166,0.25);
|
|
margin: 0.5rem 0;
|
|
}
|
|
.lp-step { flex-basis: auto; }
|
|
.lp-flows { grid-template-columns: 1fr; }
|
|
.lp-app-grid { grid-template-columns: 1fr; }
|
|
.lp-footer__cols { grid-template-columns: repeat(3, 1fr); }
|
|
}
|
|
@media (max-width: 380px) {
|
|
.lp-footer__cols { grid-template-columns: repeat(2, 1fr); }
|
|
}
|
|
`;
|