rspace-online/server/landing.ts

972 lines
35 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, 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">${escapeHtml(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 — Own Your Community Infrastructure</title>
<meta name="description" content="rSpace is a local-first platform where communities own their tools, data, and governance. ${modules.length} composable apps — from voting to budgets to maps — encrypted, interoperable, and yours.">
<!-- Open Graph -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://rspace.online">
<meta property="og:title" content="rSpace — Own Your Community Infrastructure">
<meta property="og:description" content="rSpace is a local-first platform where communities own their tools, data, and governance. ${modules.length} composable apps — from voting to budgets to maps — encrypted, interoperable, 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 — Own Your Community Infrastructure">
<meta name="twitter:description" content="Local-first community platform. ${modules.length} composable apps — voting, budgets, maps, payments, identity — encrypted and self-sovereign.">
<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">Local-first community platform</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">
One platform. ${modules.length} apps. All your community&rsquo;s tools talking to each other.
</p>
<div class="lp-hero__ctas">
<a href="${demoUrl}" class="lp-btn lp-btn--primary" id="ml-primary">Start your Space &rarr;</a>
<a href="#ecosystem" class="lp-btn lp-btn--ghost">Explore the rApps</a>
</div>
<div class="lp-hero__badges">
<span class="lp-badge">&#128225; Offline-first</span>
<span class="lp-badge">&#128274; Encrypted</span>
<span class="lp-badge">&#128101; Multiplayer</span>
<span class="lp-badge">&#128736; 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">One Platform. Every Tool Connected.</h2>
<p class="rl-subtext" style="text-align:center">
rApps share one sync layer. Data flows automatically &mdash; no import/export rituals.
</p>
<div class="lp-flows">
<div class="lp-flow">
<div class="lp-flow__pills">
<span class="lp-flow__pill">&#128197; rCal</span>
<span class="lp-flow__arrow" aria-hidden="true">&rarr;</span>
<span class="lp-flow__pill">&#128203; rTasks</span>
<span class="lp-flow__arrow" aria-hidden="true">&rarr;</span>
<span class="lp-flow__pill">&#128172; rChats</span>
</div>
<p class="lp-flow__outcome">Schedule a meeting &rarr; auto-generates a task &rarr; notifies your space.</p>
</div>
<div class="lp-flow">
<div class="lp-flow__pills">
<span class="lp-flow__pill">&#9745; rChoices</span>
<span class="lp-flow__arrow" aria-hidden="true">&rarr;</span>
<span class="lp-flow__pill">&#128176; rWallet</span>
</div>
<p class="lp-flow__outcome">Vote passes &rarr; budget allocation releases automatically.</p>
</div>
<div class="lp-flow">
<div class="lp-flow__pills">
<span class="lp-flow__pill">&#128506; rMaps</span>
<span class="lp-flow__arrow" aria-hidden="true">&rarr;</span>
<span class="lp-flow__pill">&#128221; rDocs</span>
</div>
<p class="lp-flow__outcome">Pin a community location &rarr; shared doc created for that place.</p>
</div>
<div class="lp-flow">
<div class="lp-flow__pills">
<span class="lp-flow__pill">&#9201; rTime</span>
<span class="lp-flow__arrow" aria-hidden="true">&rarr;</span>
<span class="lp-flow__pill">&#128202; rData</span>
</div>
<p class="lp-flow__outcome">Log commitments &rarr; 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&rsquo;re online,
merge automatically when you&rsquo;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 &ldquo;someone else is editing&rdquo; 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 &mdash; 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> &mdash; phishing-resistant, device-bound credentials</li>
<li><strong>Guardian recovery</strong> &mdash; 2-of-3 trusted contacts restore access</li>
<li><strong>Device linking</strong> &mdash; scan a QR to add your phone or tablet</li>
<li><strong>One RP ID</strong> &mdash; 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">Your community. Your rules. Your data.</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 tools that work for you, run by you, owned by you.
</p>
<div class="lp-hero__ctas">
<a href="${demoUrl}" class="lp-btn lp-btn--primary">Start your Space &rarr;</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 &middot; Zero-knowledge &middot; 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 &amp; 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">${escapeHtml(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&rsquo;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: var(--rs-text-primary);
-webkit-text-fill-color: unset;
}
.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); }
}
`;