rspace-online/server/landing.ts

799 lines
29 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";
export function renderMainLanding(modules: ModuleInfo[]): string {
const moduleListJSON = JSON.stringify(modules);
const demoUrl = "https://demo.rspace.online/rspace";
// Build the ecosystem grid dynamically from registered modules
const ecosystemCards = modules
.map(
(m) => `
<a href="/${escapeAttr(m.id)}" class="eco-card" style="text-decoration:none;color:inherit">
<div class="eco-card__icon">${m.icon}</div>
<h3 class="eco-card__name">${escapeHtml(m.name)}</h3>
${m.standaloneDomain ? `<span class="eco-card__domain">${escapeHtml(m.standaloneDomain)}</span>` : ""}
<p class="eco-card__desc">${escapeHtml(m.description)}</p>
</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">
<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. 25+ 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. 25+ 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. 25+ 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">
<a class="rstack-header__demo-btn" href="${demoUrl}">Try Demo</a>
<rstack-identity></rstack-identity>
</div>
</header>
<!-- Hero -->
<div class="rl-hero hero-glow-wrap">
<div class="hero-glow"></div>
<span class="rl-tagline">Community Platform</span>
<h1 class="rl-heading main-wordmark">(you)r<span class="main-wordmark__accent">Space</span></h1>
<p class="rl-subtitle">
Remember back when the internet was <span class="accent-cool">cool</span>?
</p>
<p class="rl-subtext" style="font-style:italic;opacity:0.85;margin-bottom:0.75rem">
We may not have <em>My</em>Space anymore, but we have <strong>(you)rSpace</strong>.
</p>
<p class="rl-subtext">
Build digital spaces to collaborate on improving your physical world.
Local-first, zero-knowledge privacy, outside the walls of big tech.
</p>
<div class="rl-cta-row">
<a href="${demoUrl}" class="rl-cta-primary" id="ml-primary">Start (you)rSpace</a>
<a href="/create-space" class="rl-cta-secondary">Create a Space</a>
</div>
<div class="main-badges">
<span class="rl-badge">Local-First</span>
<span class="badge-sep" aria-hidden="true">&#9733;</span>
<span class="rl-badge">Self-Hosted</span>
<span class="badge-sep" aria-hidden="true">&#9733;</span>
<span class="rl-badge">Open Source</span>
<span class="badge-sep" aria-hidden="true">&#9733;</span>
<span class="rl-badge">Offline-Ready</span>
</div>
</div>
<!-- Collaboration Features -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">Everything Your Community Needs</h2>
<p class="rl-subtext" style="text-align:center">
Each tool works on its own, and they all work together.
</p>
<div class="rl-grid-3">
<div class="feat-card feat-card--teal">
<div class="feat-card__icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>
</div>
<h3>Community Funds</h3>
<p>Budget rivers, revenue splits, and enoughness thresholds. Transparent finances with quadratic funding built in.</p>
</div>
<div class="feat-card feat-card--indigo">
<div class="feat-card__icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
</div>
<h3>Messaging &amp; Forum</h3>
<p>Real-time chat, threaded discussions, and async forums. All synced across devices with CRDT magic.</p>
</div>
<div class="feat-card feat-card--teal">
<div class="feat-card__icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/></svg>
</div>
<h3>Files &amp; Media</h3>
<p>Upload, organize, and share with memory cards. Public links, folder trees, and metadata that travels with content.</p>
</div>
<div class="feat-card feat-card--indigo">
<div class="feat-card__icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
</div>
<h3>Passkey Identity</h3>
<p>One passwordless login for the entire ecosystem. EncryptID uses WebAuthn &mdash; no passwords to leak, no accounts to hack.</p>
</div>
<div class="feat-card feat-card--teal">
<div class="feat-card__icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 20V10"/><path d="M12 20V4"/><path d="M6 20v-6"/></svg>
</div>
<h3>Dashboards &amp; Data</h3>
<p>Community analytics, voting results, spatial canvases. See everything at a glance and drill into what matters.</p>
</div>
<div class="feat-card feat-card--indigo">
<div class="feat-card__icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
</div>
<h3>Privacy by Design</h3>
<p>Your data lives on your device first. End-to-end encrypted sync, per-document keys, zero-knowledge architecture.</p>
</div>
</div>
</div>
</section>
<!-- 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="encryptid-visual">
<svg class="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="encryptid-visual__label">Touch. Tap. Done.</p>
</div>
</div>
</div>
</div>
</section>
<!-- Offline-First -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">Offline-First, Always</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="rl-grid-3">
<div class="rl-step">
<div class="rl-step__num">1</div>
<h3>Local Persistence</h3>
<p>Every document is stored in encrypted IndexedDB on your device. Works without internet.</p>
</div>
<div class="rl-step">
<div class="rl-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="rl-step">
<div class="rl-step__num">3</div>
<h3>Incremental Sync</h3>
<p>Only changed bytes travel over the wire. Reconnect after days offline and catch up in seconds.</p>
</div>
</div>
<p class="offline-explain">
Under the hood: <strong>Automerge CRDTs</strong> model every document as a mergeable data structure.
<strong>IndexedDB</strong> persists the full state locally so you never lose work.
A <strong>Service Worker</strong> caches the app shell for instant loads &mdash; even without a network connection.
</p>
</div>
</section>
<!-- Interoperable by Design -->
<section class="rl-section rl-section--alt">
<div class="rl-container" style="max-width:820px;text-align:center">
<h2 class="rl-heading">Interoperable by Design</h2>
<p class="rl-subtext">
Every rApp reads and writes to the same sync layer.
No import/export rituals &mdash; data flows between tools automatically.
</p>
<div class="interop-diagram">
<pre class="interop-pre"><code> rDocs rCal rTasks rMaps rWallet ...
| | | | |
v v v v v
+--------------------------------------+
| Automerge Sync Layer |
| (encrypted, per-document keys) |
+--------------------------------------+
| | | | |
v v v v v
Your Your Your Your Your
Device Phone Laptop Server Backup</code></pre>
</div>
</div>
</section>
<!-- Philosophy -->
<section class="rl-section">
<div class="rl-container" style="max-width:720px;text-align:center">
<h2 class="rl-heading">The Internet as It Was Always Meant to Be</h2>
<p class="rl-subtext" style="font-size:1.2rem;line-height:1.7">
No algorithms deciding what you see. No ads. No data harvesting.
Just tools that work for you, run by you, owned by you.
</p>
<p class="rl-subtext">
rSpace is infrastructure for communities who refuse to rent their digital commons
from landlords who read the mail, count the footsteps, and sell the maps.
</p>
<p class="philosophy-punch">
Your space. Your community. Your rules.
</p>
<div class="rl-cta-row">
<a href="/about" class="rl-cta-secondary">Learn More</a>
</div>
</div>
</section>
<!-- Ecosystem Grid (dynamic) -->
<section class="rl-section rl-section--alt">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">${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="rl-grid-4">
${ecosystemCards}
</div>
</div>
</section>
<!-- Footer -->
<footer class="main-footer">
<div class="rl-container" style="text-align:center">
<div class="footer-quicklinks">
<a href="/rcal">rCal</a>
<a href="/rtasks">rTasks</a>
<a href="/rdocs">rDocs</a>
<a href="/rmaps">rMaps</a>
<a href="/rwallet">rWallet</a>
<a href="/rphotos">rPhotos</a>
<a href="/rflows">rFlows</a>
<a href="/rchoices">rChoices</a>
</div>
<p class="footer-tagline">Local-first &middot; Zero-knowledge &middot; Community-owned</p>
<div class="footer-links">
<a href="/about">About</a>
<a href="/create-space">Create a Space</a>
<a href="https://auth.rspace.online">EncryptID</a>
</div>
<p class="footer-tech">
Built with Bun, Hono, Automerge &amp; WebAuthn
</p>
</div>
</footer>
<script type="module">
import '/shell.js';
document.querySelector('rstack-app-switcher')?.setModules(${moduleListJSON});
// Logged-in users: hide header demo btn, 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);
var hdrBtn = document.querySelector('.rstack-header__demo-btn');
if (hdrBtn) hdrBtn.setAttribute('data-hide', '');
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/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 = `
/* ── Main landing page extras (on top of rl-* utilities) ── */
/* Background + diagonal stripe overlay */
body {
background: linear-gradient(
170deg,
#0a0f1e 0%,
#0f172a 25%,
#131b2e 45%,
#0e1628 65%,
#0d1424 85%,
#080d19 100%
);
background-attachment: fixed;
}
body::before {
content: "";
position: fixed; inset: 0;
background: repeating-linear-gradient(
-45deg,
transparent,
transparent 60px,
rgba(20,184,166,0.015) 60px,
rgba(20,184,166,0.015) 61px
);
pointer-events: none;
z-index: 0;
}
[data-theme="light"] body {
background: linear-gradient(170deg, #f8fafc 0%, #f1f5f9 50%, #e2e8f0 100%);
}
[data-theme="light"] body::before {
background: repeating-linear-gradient(
-45deg,
transparent,
transparent 60px,
rgba(20,184,166,0.03) 60px,
rgba(20,184,166,0.03) 61px
);
}
/* ── Hero ── */
.hero-glow-wrap { position: relative; overflow: hidden; }
.hero-glow {
position: absolute;
top: 50%; left: 50%;
width: 600px; height: 600px;
transform: translate(-50%, -60%);
border-radius: 50%;
background: radial-gradient(circle, rgba(20,184,166,0.15) 0%, transparent 70%);
animation: hero-pulse 4s ease-in-out infinite;
pointer-events: none;
z-index: 0;
}
[data-theme="light"] .hero-glow {
background: radial-gradient(circle, rgba(20,184,166,0.1) 0%, transparent 70%);
}
@keyframes hero-pulse {
0%, 100% { opacity: 0.6; transform: translate(-50%, -60%) scale(1); }
50% { opacity: 1; transform: translate(-50%, -60%) scale(1.08); }
}
.rl-hero > * { position: relative; z-index: 1; }
.main-wordmark {
font-size: 3.5rem;
text-shadow: 0 0 40px rgba(20,184,166,0.2);
}
@media (min-width: 640px) { .main-wordmark { font-size: 4.5rem; } }
.main-wordmark__accent {
background: var(--rs-gradient-brand);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
background-clip: text;
}
.accent-cool {
color: var(--rs-accent);
font-weight: 700;
-webkit-text-fill-color: var(--rs-accent);
}
/* Badge bar with star separators */
.main-badges {
display: flex; gap: 0.5rem; justify-content: center; flex-wrap: wrap;
align-items: center;
margin-top: 1.5rem;
}
.main-badges .rl-badge {
font-weight: 800;
letter-spacing: 0.04em;
}
.badge-sep {
color: var(--rs-accent);
font-size: 0.5rem;
opacity: 0.6;
}
/* ── Feature Cards (SVG icons, border glow) ── */
.feat-card {
background: var(--rs-card-bg);
border: 2px solid var(--rs-card-border);
border-radius: 1rem;
padding: 1.75rem;
text-align: center;
transition: border-color 0.25s, box-shadow 0.25s, transform 0.2s;
}
.feat-card:hover {
transform: translateY(-2px);
}
.feat-card--teal:hover {
border-color: rgba(20,184,166,0.5);
box-shadow: 0 0 20px rgba(20,184,166,0.12);
}
.feat-card--indigo:hover {
border-color: rgba(99,102,241,0.5);
box-shadow: 0 0 20px rgba(99,102,241,0.12);
}
.feat-card__icon {
width: 3rem; height: 3rem; border-radius: 0.75rem;
display: flex; align-items: center; justify-content: center;
margin: 0 auto 1rem;
font-size: 1.5rem;
}
.feat-card--teal .feat-card__icon {
background: rgba(20,184,166,0.12); color: #14b8a6;
}
.feat-card--indigo .feat-card__icon {
background: rgba(99,102,241,0.12); color: #6366f1;
}
.feat-card h3 {
font-size: 0.95rem; font-weight: 600; color: var(--rs-text-primary); margin-bottom: 0.5rem;
}
.feat-card p {
font-size: 0.875rem; color: var(--rs-text-secondary); line-height: 1.6;
}
/* ── EncryptID Visual ── */
.encryptid-visual {
text-align: center;
}
.encryptid-shield {
width: 140px; height: 160px;
color: var(--rs-accent);
filter: drop-shadow(0 0 20px rgba(20,184,166,0.2));
}
.encryptid-visual__label {
font-size: 0.9rem; font-weight: 600;
color: var(--rs-text-secondary);
margin-top: 1rem;
}
/* ── Offline-First Explainer ── */
.offline-explain {
text-align: center;
font-size: 0.85rem;
color: var(--rs-text-muted);
line-height: 1.65;
max-width: 640px;
margin: 2rem auto 0;
border-top: 1px solid var(--rs-border-subtle);
padding-top: 1.5rem;
}
.offline-explain strong {
color: var(--rs-text-secondary);
font-weight: 600;
}
/* ── Interop Diagram ── */
.interop-diagram {
margin-top: 2rem;
}
.interop-pre {
display: inline-block;
text-align: left;
font-family: "SF Mono", "Fira Code", "Cascadia Code", monospace;
font-size: 0.78rem;
line-height: 1.6;
color: var(--rs-text-secondary);
background: var(--rs-card-bg);
border: 1px solid var(--rs-card-border);
border-radius: 0.75rem;
padding: 1.5rem 2rem;
overflow-x: auto;
max-width: 100%;
box-shadow: 0 2px 12px rgba(0,0,0,0.15);
}
[data-theme="light"] .interop-pre {
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
}
/* ── Philosophy Punch ── */
.philosophy-punch {
font-size: 1.3rem;
font-weight: 700;
background: var(--rs-gradient-brand);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
background-clip: text;
margin: 1.5rem 0 0;
letter-spacing: -0.01em;
}
/* ── Ecosystem Cards ── */
.eco-card {
background: var(--rs-card-bg);
border: 2px solid var(--rs-card-border);
border-radius: 1rem;
padding: 1.5rem;
text-align: center;
transition: border-color 0.25s, box-shadow 0.25s, transform 0.2s;
display: flex; flex-direction: column; align-items: center;
}
.eco-card:hover {
border-color: rgba(20,184,166,0.4);
box-shadow: 0 0 16px rgba(20,184,166,0.1);
transform: translateY(-2px);
}
.eco-card__icon {
font-size: 1.75rem;
margin-bottom: 0.5rem;
}
.eco-card__name {
font-size: 0.9rem; font-weight: 600;
color: var(--rs-text-primary);
margin: 0 0 0.15rem;
}
.eco-card__domain {
font-size: 0.65rem; font-weight: 600;
color: var(--rs-accent);
opacity: 0.75;
letter-spacing: 0.02em;
margin-bottom: 0.4rem;
display: block;
}
.eco-card__desc {
font-size: 0.78rem; color: var(--rs-text-secondary);
line-height: 1.5; margin: 0;
}
/* ── Footer ── */
.main-footer {
border-top: 1px solid var(--rs-border-subtle);
padding: 2.5rem 1.5rem 2rem;
}
.footer-quicklinks {
display: flex; gap: 1.25rem; justify-content: center; flex-wrap: wrap;
margin-bottom: 1.25rem;
}
.footer-quicklinks a {
font-size: 0.75rem; font-weight: 600;
color: var(--rs-text-muted);
text-decoration: none;
transition: color 0.2s;
}
.footer-quicklinks a:hover { color: var(--rs-accent); }
.footer-tagline {
font-size: 0.8rem; font-weight: 700;
color: var(--rs-accent);
letter-spacing: 0.06em;
text-transform: uppercase;
margin: 0 0 1rem;
opacity: 0.7;
}
.footer-links {
display: flex; gap: 1.5rem; justify-content: center; flex-wrap: wrap;
margin-bottom: 1rem;
}
.footer-links a {
color: #94a3b8; font-size: 0.8rem; text-decoration: none;
transition: color 0.2s;
}
.footer-links a:hover { color: var(--rs-text-primary); }
.footer-tech {
color: #64748b; font-size: 0.75rem; margin: 0;
}
/* ── Retro Shadow Utility ── */
.retro-shadow {
box-shadow: 2px 2px 0 var(--rs-card-border);
}
/* ── Responsive ── */
@media (max-width: 480px) {
.main-wordmark { font-size: 2.75rem; }
.hero-glow { width: 350px; height: 350px; }
.interop-pre { font-size: 0.65rem; padding: 1rem; }
.philosophy-punch { font-size: 1.1rem; }
.footer-quicklinks { gap: 0.75rem; }
}
`;