799 lines
29 KiB
TypeScript
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">★</span>
|
|
<span class="rl-badge">Self-Hosted</span>
|
|
<span class="badge-sep" aria-hidden="true">★</span>
|
|
<span class="rl-badge">Open Source</span>
|
|
<span class="badge-sep" aria-hidden="true">★</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 & 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 & 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 — 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 & 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 — 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="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’re online,
|
|
merge automatically when you’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 “someone else is editing” 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 — 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 — 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 · Zero-knowledge · 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 & 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’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; }
|
|
}
|
|
`;
|