feat: restyle rSpace.online about page to rApp theme + (you)rSpace CTA in space switcher

Restyled website/index.html to use the standard rl-* rich landing CSS
utilities matching all rApp module landing pages: rl-hero, rl-section,
rl-card, rl-grid-3, rl-icon-box, rl-cta-primary/secondary. All original
content preserved (EncryptID, Offline-First, Interoperable, Newsletter).

Added (you)rSpace CTA button in the space switcher dropdown — shows
"Sign in to create" or "Create (you)rSpace" when user has no owned space,
with auto-provision flow on click.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-02-27 17:50:30 -08:00
parent ab3132a5f0
commit 2ec5027285
2 changed files with 432 additions and 430 deletions

View File

@ -9,7 +9,7 @@
* Passes auth token so the API returns private spaces the user can access. * Passes auth token so the API returns private spaces the user can access.
*/ */
import { isAuthenticated, getAccessToken } from "./rstack-identity"; import { isAuthenticated, getAccessToken, getUsername } from "./rstack-identity";
import { rspaceNavUrl, getCurrentModule as getModule } from "../url-helpers"; import { rspaceNavUrl, getCurrentModule as getModule } from "../url-helpers";
interface SpaceInfo { interface SpaceInfo {
@ -117,26 +117,47 @@ export class RStackSpaceSwitcher extends HTMLElement {
} }
#renderMenu(menu: HTMLElement, current: string) { #renderMenu(menu: HTMLElement, current: string) {
const auth = isAuthenticated();
if (this.#spaces.length === 0) { if (this.#spaces.length === 0) {
let cta = "";
if (!auth) {
cta = this.#yourSpaceCTAhtml("Sign in to create →");
} else {
cta = this.#yourSpaceCTAhtml("Create (you)rSpace →");
}
menu.innerHTML = ` menu.innerHTML = `
${cta}
<div class="divider"></div>
<div class="menu-empty"> <div class="menu-empty">
${isAuthenticated() ? "No spaces yet" : "Sign in to see your spaces"} ${auth ? "No spaces yet" : "Sign in to see your spaces"}
</div> </div>
<a class="item item--create" href="/new">+ Create new space</a> <a class="item item--create" href="/new">+ Create new space</a>
`; `;
this.#attachYourSpaceCTA(menu);
return; return;
} }
const moduleId = this.#getCurrentModule(); const moduleId = this.#getCurrentModule();
const auth = isAuthenticated();
// 3-section split // 3-section split
const mySpaces = this.#spaces.filter((s) => s.role); const mySpaces = this.#spaces.filter((s) => s.role);
const publicSpaces = this.#spaces.filter((s) => s.accessible !== false && !s.role); const publicSpaces = this.#spaces.filter((s) => s.accessible !== false && !s.role);
const discoverSpaces = this.#spaces.filter((s) => s.accessible === false); const discoverSpaces = this.#spaces.filter((s) => s.accessible === false);
const hasOwnedSpace = mySpaces.some((s) => s.relationship === "owner");
let html = ""; let html = "";
// ── Create (you)rSpace CTA — only if user has no owned spaces ──
if (!auth) {
html += this.#yourSpaceCTAhtml("Sign in to create →");
html += `<div class="divider"></div>`;
} else if (!hasOwnedSpace) {
html += this.#yourSpaceCTAhtml("Create (you)rSpace →");
html += `<div class="divider"></div>`;
}
// ── Your spaces ── // ── Your spaces ──
if (mySpaces.length > 0) { if (mySpaces.length > 0) {
html += `<div class="section-label">Your spaces</div>`; html += `<div class="section-label">Your spaces</div>`;
@ -219,6 +240,62 @@ export class RStackSpaceSwitcher extends HTMLElement {
this.#showEditSpaceModal(el.dataset.editSlug!, el.dataset.editName!); this.#showEditSpaceModal(el.dataset.editSlug!, el.dataset.editName!);
}); });
}); });
// Attach "(you)rSpace" CTA listener
this.#attachYourSpaceCTA(menu);
}
#yourSpaceCTAhtml(buttonLabel: string): string {
return `
<div class="item item--yourspace vis-private">
<span class="item-icon">🔒</span>
<span class="item-name">(you)rSpace</span>
<button class="yourspace-btn" id="yourspace-cta">${buttonLabel}</button>
</div>`;
}
#attachYourSpaceCTA(menu: HTMLElement) {
const btn = menu.querySelector("#yourspace-cta");
if (!btn) return;
btn.addEventListener("click", (e) => {
e.stopPropagation();
if (!isAuthenticated()) {
// Find <rstack-identity> on page and open auth modal
const identity = document.querySelector("rstack-identity") as any;
if (identity?.showAuthModal) {
identity.showAuthModal();
}
} else {
// Authenticated but no owned space — auto-provision
this.#autoProvision();
}
});
}
async #autoProvision() {
const token = getAccessToken();
const username = getUsername();
if (!token) return;
try {
const res = await fetch("/api/spaces/auto-provision", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
});
const data = await res.json();
if (data.slug) {
window.location.href = `/${data.slug}/canvas`;
} else if (username) {
window.location.href = `/${username}/canvas`;
}
} catch {
// Fallback: redirect to username path
if (username) window.location.href = `/${username}/canvas`;
}
} }
#showRequestAccessModal(slug: string, spaceName: string) { #showRequestAccessModal(slug: string, spaceName: string) {
@ -688,6 +765,21 @@ const STYLES = `
} }
.item--create:hover { background: rgba(6,182,212,0.08) !important; } .item--create:hover { background: rgba(6,182,212,0.08) !important; }
/* (you)rSpace CTA */
.item--yourspace {
border-left-color: #f87171; padding: 12px 14px;
}
.item--yourspace .item-name { font-weight: 700; font-size: 0.9rem; }
:host-context([data-theme="light"]) .item--yourspace { background: #fff5f5; }
:host-context([data-theme="dark"]) .item--yourspace { background: rgba(248,113,113,0.06); }
.yourspace-btn {
margin-left: auto; padding: 5px 12px; border-radius: 6px; border: none;
font-size: 0.75rem; font-weight: 600; cursor: pointer; white-space: nowrap;
background: linear-gradient(135deg, #06b6d4, #7c3aed); color: white;
transition: opacity 0.15s, transform 0.15s;
}
.yourspace-btn:hover { opacity: 0.85; transform: translateY(-1px); }
.divider { height: 1px; margin: 4px 0; } .divider { height: 1px; margin: 4px 0; }
:host-context([data-theme="light"]) .divider { background: rgba(0,0,0,0.08); } :host-context([data-theme="light"]) .divider { background: rgba(0,0,0,0.08); }
:host-context([data-theme="dark"]) .divider { background: rgba(255,255,255,0.08); } :host-context([data-theme="dark"]) .divider { background: rgba(255,255,255,0.08); }

View File

@ -14,218 +14,125 @@
body { body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%); background: #0f172a;
color: white;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding-top: 56px;
}
.hero {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: calc(100vh - 56px);
width: 100%;
}
.container {
text-align: center;
max-width: 600px;
padding: 40px 20px;
}
h1 {
font-size: 3rem;
margin-bottom: 1rem;
background: linear-gradient(135deg, #14b8a6, #22d3ee);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.tagline {
font-size: 1.25rem;
color: #94a3b8;
margin-bottom: 3rem;
}
.cta-buttons {
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
}
.cta-primary {
display: inline-block;
padding: 14px 32px;
border-radius: 8px;
background: linear-gradient(135deg, #14b8a6, #0d9488);
color: white;
font-size: 1rem;
font-weight: 600;
text-decoration: none;
transition: transform 0.2s, box-shadow 0.2s;
}
.cta-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(20, 184, 166, 0.3);
}
.cta-secondary {
display: inline-block;
padding: 14px 32px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.2);
color: #94a3b8;
font-size: 1rem;
font-weight: 600;
text-decoration: none;
transition: transform 0.2s, border-color 0.2s, color 0.2s;
}
.cta-secondary:hover {
transform: translateY(-2px);
border-color: rgba(255, 255, 255, 0.4);
color: white;
}
.features {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1.5rem;
margin-top: 4rem;
}
.feature {
text-align: center;
}
.feature-icon {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.feature-title {
font-weight: 600;
margin-bottom: 0.25rem;
}
.feature-desc {
font-size: 0.875rem;
color: #64748b;
}
/* EncryptID & Ecosystem Sections */
.section {
width: 100%;
max-width: 760px;
padding: 5rem 20px;
text-align: center;
}
.section + .section {
padding-top: 0;
}
.section-divider {
width: 60px;
height: 2px;
background: linear-gradient(90deg, #14b8a6, #7c3aed);
margin: 0 auto 3rem;
border-radius: 2px;
}
.section h2 {
font-size: 2rem;
margin-bottom: 0.75rem;
background: linear-gradient(90deg, #00d4ff, #7c3aed);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.section-subtitle {
font-size: 1.05rem;
color: #94a3b8;
margin-bottom: 2.5rem;
line-height: 1.7;
max-width: 560px;
margin-left: auto;
margin-right: auto;
}
.identity-card {
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(124, 58, 237, 0.2);
border-radius: 16px;
padding: 2rem;
text-align: left;
margin-bottom: 1.5rem;
}
.identity-card h3 {
font-size: 1.15rem;
margin-bottom: 0.75rem;
color: #e2e8f0; color: #e2e8f0;
min-height: 100vh;
padding-top: 56px;
line-height: 1.6;
} }
.identity-card p { a { color: inherit; }
color: #94a3b8;
line-height: 1.7; /* ── Rich Landing Page Utilities (from rApp theme) ── */
font-size: 0.95rem; .rl-section {
border-top: 1px solid rgba(255,255,255,0.06);
padding: 4rem 1.5rem;
}
.rl-section--alt { background: rgba(255,255,255,0.015); }
.rl-container { max-width: 1100px; margin: 0 auto; }
.rl-hero {
text-align: center; padding: 5rem 1.5rem 3rem;
max-width: 820px; margin: 0 auto;
}
.rl-tagline {
display: inline-block; font-size: 0.7rem; font-weight: 700;
letter-spacing: 0.12em; text-transform: uppercase;
color: #14b8a6; background: rgba(20,184,166,0.1);
border: 1px solid rgba(20,184,166,0.2);
padding: 0.35rem 1rem; border-radius: 9999px; margin-bottom: 1.5rem;
}
.rl-heading {
font-size: 2rem; font-weight: 700; line-height: 1.15;
margin-bottom: 0.75rem; letter-spacing: -0.01em;
background: linear-gradient(135deg, #14b8a6, #22d3ee);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
background-clip: text;
}
.rl-hero .rl-heading { font-size: 2.5rem; }
@media (min-width: 640px) { .rl-hero .rl-heading { font-size: 3rem; } }
.rl-subtitle {
font-size: 1.25rem; font-weight: 500; color: #cbd5e1;
margin-bottom: 1rem; letter-spacing: -0.005em;
}
.rl-hero .rl-subtitle { font-size: 1.35rem; }
@media (min-width: 640px) { .rl-hero .rl-subtitle { font-size: 1.5rem; } }
.rl-subtext {
font-size: 1.05rem; color: #94a3b8; line-height: 1.65;
max-width: 640px; margin: 0 auto 2rem;
}
.rl-hero .rl-subtext { font-size: 1.15rem; }
/* Grids */
.rl-grid-2 { display: grid; grid-template-columns: 1fr; gap: 1.25rem; }
.rl-grid-3 { display: grid; grid-template-columns: 1fr; gap: 1.25rem; }
.rl-grid-4 { display: grid; grid-template-columns: 1fr; gap: 1.25rem; }
@media (min-width: 640px) {
.rl-grid-2 { grid-template-columns: repeat(2, 1fr); }
.rl-grid-3 { grid-template-columns: repeat(3, 1fr); }
.rl-grid-4 { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1024px) {
.rl-grid-4 { grid-template-columns: repeat(4, 1fr); }
} }
.identity-card .hl { /* Card */
color: #00d4ff; .rl-card {
font-weight: 600; background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.06);
border-radius: 1rem; padding: 1.75rem;
transition: border-color 0.2s;
} }
.rl-card:hover { border-color: rgba(20,184,166,0.3); }
.rl-card h3 { font-size: 0.95rem; font-weight: 600; color: #e2e8f0; margin-bottom: 0.5rem; }
.rl-card p { font-size: 0.875rem; color: #94a3b8; line-height: 1.6; }
.rl-card--center { text-align: center; }
.pillars { /* CTA row */
display: grid; .rl-cta-row {
grid-template-columns: repeat(3, 1fr); display: flex; gap: 0.75rem; justify-content: center; flex-wrap: wrap;
gap: 1rem; margin-top: 2rem;
margin-bottom: 2rem;
} }
.rl-cta-primary {
.pillar { display: inline-block; padding: 0.8rem 2rem; border-radius: 0.5rem;
background: rgba(255, 255, 255, 0.04); background: linear-gradient(135deg, #14b8a6, #0d9488);
border: 1px solid rgba(255, 255, 255, 0.08); color: white; font-size: 0.95rem; font-weight: 600;
border-radius: 12px; text-decoration: none; transition: transform 0.2s, box-shadow 0.2s;
padding: 1.25rem 1rem;
text-align: center;
} }
.rl-cta-primary:hover { transform: translateY(-2px); box-shadow: 0 8px 20px rgba(20,184,166,0.3); }
.pillar-icon { .rl-cta-secondary {
font-size: 1.75rem; display: inline-block; padding: 0.8rem 2rem; border-radius: 0.5rem;
margin-bottom: 0.5rem; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.15);
color: #94a3b8; font-size: 0.95rem; font-weight: 600;
text-decoration: none; transition: transform 0.2s, border-color 0.2s, color 0.2s;
} }
.rl-cta-secondary:hover { transform: translateY(-2px); border-color: rgba(255,255,255,0.35); color: white; }
.pillar-title { /* Icon box */
font-weight: 600; .rl-icon-box {
font-size: 0.9rem; width: 3rem; height: 3rem; border-radius: 0.75rem;
margin-bottom: 0.25rem; background: rgba(20,184,166,0.12); color: #14b8a6;
display: flex; align-items: center; justify-content: center;
font-size: 1.5rem; margin-bottom: 1rem;
} }
.rl-card--center .rl-icon-box { margin: 0 auto 1rem; }
.pillar-desc { /* Highlight text */
font-size: 0.8rem; .hl { color: #14b8a6; font-weight: 600; }
color: #64748b;
line-height: 1.4;
}
/* ── Section-specific accents ── */
/* EncryptID / identity accent */
.rl-card--accent-purple { border-color: rgba(124, 58, 237, 0.2); }
.rl-card--accent-purple:hover { border-color: rgba(124, 58, 237, 0.4); }
.rl-icon-box--purple { background: rgba(124, 58, 237, 0.12); color: #a78bfa; }
/* ── Flow diagram ── */
.flow-diagram { .flow-diagram {
background: rgba(255, 255, 255, 0.03); background: rgba(255,255,255,0.02);
border: 1px solid rgba(255, 255, 255, 0.08); border: 1px solid rgba(255,255,255,0.06);
border-radius: 16px; border-radius: 1rem;
padding: 1.75rem; padding: 1.75rem;
margin-bottom: 2rem; margin-top: 1.5rem;
} }
.flow-diagram pre { .flow-diagram pre {
font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace; font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
font-size: 0.78rem; font-size: 0.78rem;
@ -236,21 +143,21 @@
white-space: pre; white-space: pre;
} }
/* ── Ecosystem app pills ── */
.ecosystem-apps { .ecosystem-apps {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center; justify-content: center;
gap: 0.75rem; gap: 0.75rem;
margin-top: 1.5rem; margin-top: 2rem;
} }
.ecosystem-app { .ecosystem-app {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 0.4rem; gap: 0.4rem;
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
background: rgba(255, 255, 255, 0.05); background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px; border-radius: 8px;
color: #94a3b8; color: #94a3b8;
text-decoration: none; text-decoration: none;
@ -258,12 +165,12 @@
font-weight: 500; font-weight: 500;
transition: border-color 0.2s, color 0.2s; transition: border-color 0.2s, color 0.2s;
} }
.ecosystem-app:hover { .ecosystem-app:hover {
border-color: rgba(0, 212, 255, 0.4); border-color: rgba(20, 184, 166, 0.4);
color: #00d4ff; color: #14b8a6;
} }
/* ── EncryptID link ── */
.encryptid-link { .encryptid-link {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@ -279,23 +186,20 @@
margin-top: 2rem; margin-top: 2rem;
transition: transform 0.2s, border-color 0.2s; transition: transform 0.2s, border-color 0.2s;
} }
.encryptid-link:hover { .encryptid-link:hover {
transform: translateY(-2px); transform: translateY(-2px);
border-color: rgba(124, 58, 237, 0.6); border-color: rgba(124, 58, 237, 0.6);
} }
/* Newsletter */ /* ── Newsletter ── */
.newsletter-form { .newsletter-form {
max-width: 440px; max-width: 440px;
margin: 0 auto; margin: 0 auto;
} }
.newsletter-row { .newsletter-row {
display: flex; display: flex;
gap: 0.75rem; gap: 0.75rem;
} }
.newsletter-input { .newsletter-input {
flex: 1; flex: 1;
padding: 12px 16px; padding: 12px 16px;
@ -307,53 +211,48 @@
outline: none; outline: none;
transition: border-color 0.2s; transition: border-color 0.2s;
} }
.newsletter-input:focus { border-color: #14b8a6; }
.newsletter-input:focus { .newsletter-input::placeholder { color: #64748b; }
border-color: #14b8a6;
}
.newsletter-input::placeholder {
color: #64748b;
}
.newsletter-btn { .newsletter-btn {
padding: 12px 24px; padding: 12px 24px;
white-space: nowrap; white-space: nowrap;
border-radius: 8px;
border: none;
background: linear-gradient(135deg, #14b8a6, #0d9488);
color: white;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.newsletter-btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(20, 184, 166, 0.3);
}
.newsletter-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
} }
.newsletter-status { .newsletter-status {
font-size: 0.875rem; font-size: 0.875rem;
margin-top: 0.75rem; margin-top: 0.75rem;
min-height: 1.25em; min-height: 1.25em;
} }
.newsletter-status.success { color: #22c55e; }
.newsletter-status.success { .newsletter-status.error { color: #ef4444; }
color: #22c55e;
}
.newsletter-status.error {
color: #ef4444;
}
.newsletter-privacy { .newsletter-privacy {
font-size: 0.8rem; font-size: 0.8rem;
color: #64748b; color: #64748b;
margin-top: 0.5rem; margin-top: 0.5rem;
} }
/* ── Responsive ── */
@media (max-width: 600px) { @media (max-width: 600px) {
.newsletter-row { .rl-hero { padding: 3rem 1rem 2rem; }
flex-direction: column; .rl-hero .rl-heading { font-size: 2rem; }
} .rl-section { padding: 2.5rem 1rem; }
.pillars { .newsletter-row { flex-direction: column; }
grid-template-columns: 1fr;
}
.features {
grid-template-columns: repeat(2, 1fr);
}
.section h2 {
font-size: 1.5rem;
}
} }
</style> </style>
<link rel="stylesheet" href="/shell.css"> <link rel="stylesheet" href="/shell.css">
@ -374,69 +273,74 @@
<rstack-identity></rstack-identity> <rstack-identity></rstack-identity>
</div> </div>
</header> </header>
<div class="hero">
<div class="container">
<h1>rSpace</h1>
<p class="tagline">Collaborative community spaces powered by FolkJS</p>
<div class="cta-buttons"> <!-- ═══════ HERO ═══════ -->
<a href="/create-space" class="cta-primary" id="cta-primary">Create a Space</a> <div class="rl-hero">
<a href="https://rspace.online/rspace" class="cta-secondary" id="cta-demo">Try the Demo</a> <span class="rl-tagline">Local-First &middot; Zero-Knowledge &middot; Community-Owned</span>
<h1 class="rl-heading">(you)rSpace</h1>
<p class="rl-subtitle">Collaborative community spaces powered by FolkJS</p>
<p class="rl-subtext">
Build digital spaces for your community with an infinite collaborative canvas,
real-time CRDT sync, and zero-knowledge identity. All open source.
</p>
<div class="rl-cta-row">
<a href="/create-space" class="rl-cta-primary" id="cta-primary">Create a Space</a>
<a href="https://rspace.online/rspace" class="rl-cta-secondary" id="cta-demo">Try the Demo</a>
</div> </div>
<div class="features"> <div class="rl-grid-4" style="margin-top: 3rem;">
<div class="feature"> <div class="rl-card rl-card--center">
<div class="feature-icon">🎨</div> <div class="rl-icon-box">🎨</div>
<div class="feature-title">Spatial Canvas</div> <h3>Spatial Canvas</h3>
<div class="feature-desc">Infinite collaborative workspace</div> <p>Infinite collaborative workspace</p>
</div> </div>
<div class="feature"> <div class="rl-card rl-card--center">
<div class="feature-icon">🔄</div> <div class="rl-icon-box">🔄</div>
<div class="feature-title">Real-time Sync</div> <h3>Real-time Sync</h3>
<div class="feature-desc">Powered by Automerge CRDT</div> <p>Powered by Automerge CRDT</p>
</div> </div>
<div class="feature"> <div class="rl-card rl-card--center">
<div class="feature-icon">📡</div> <div class="rl-icon-box">📡</div>
<div class="feature-title">Offline-First</div> <h3>Offline-First</h3>
<div class="feature-desc">Works without internet, merges on reconnect</div> <p>Works without internet, merges on reconnect</p>
</div>
<div class="feature">
<div class="feature-icon">🌐</div>
<div class="feature-title">Your Subdomain</div>
<div class="feature-desc">community.rspace.online</div>
</div> </div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">🌐</div>
<h3>Your Subdomain</h3>
<p>community.rspace.online</p>
</div> </div>
</div> </div>
</div> </div>
<!-- EncryptID Section --> <!-- ═══════ EncryptID ═══════ -->
<div class="section"> <div class="rl-section">
<div class="section-divider"></div> <div class="rl-container" style="text-align: center;">
<h2>EncryptID</h2> <h2 class="rl-heading">EncryptID</h2>
<p class="section-subtitle"> <p class="rl-subtext">
One secure, local-first identity across every tool in your community's rSpace. One secure, local-first identity across every tool in your community's rSpace.
No passwords. No cloud accounts. Your keys never leave your device. No passwords. No cloud accounts. Your keys never leave your device.
</p> </p>
<div class="pillars"> <div class="rl-grid-3">
<div class="pillar"> <div class="rl-card rl-card--center">
<div class="pillar-icon">🔑</div> <div class="rl-icon-box rl-icon-box--purple">🔑</div>
<div class="pillar-title">Passkey Login</div> <h3>Passkey Login</h3>
<div class="pillar-desc">Hardware-backed biometric auth. Phishing-resistant by design.</div> <p>Hardware-backed biometric auth. Phishing-resistant by design.</p>
</div> </div>
<div class="pillar"> <div class="rl-card rl-card--center">
<div class="pillar-icon">🏠</div> <div class="rl-icon-box rl-icon-box--purple">🏠</div>
<div class="pillar-title">Local-First</div> <h3>Local-First</h3>
<div class="pillar-desc">Cryptographic keys are derived and stored on your device, never uploaded.</div> <p>Cryptographic keys are derived and stored on your device, never uploaded.</p>
</div> </div>
<div class="pillar"> <div class="rl-card rl-card--center">
<div class="pillar-icon">🔗</div> <div class="rl-icon-box rl-icon-box--purple">🔗</div>
<div class="pillar-title">One Login, All Apps</div> <h3>One Login, All Apps</h3>
<div class="pillar-desc">Authenticate once and access every r-Ecosystem tool seamlessly.</div> <p>Authenticate once and access every r-Ecosystem tool seamlessly.</p>
</div> </div>
</div> </div>
<div class="identity-card"> <div class="rl-card rl-card--accent-purple" style="text-align: left; margin-top: 1.5rem;">
<h3>Secure by default, not by opt-in</h3> <h3>Secure by default, not by opt-in</h3>
<p> <p>
EncryptID uses <span class="hl">WebAuthn passkeys</span> as the root of trust &mdash; EncryptID uses <span class="hl">WebAuthn passkeys</span> as the root of trust &mdash;
@ -449,7 +353,7 @@
</p> </p>
</div> </div>
<div class="identity-card"> <div class="rl-card rl-card--accent-purple" style="text-align: left; margin-top: 1.25rem;">
<h3>A common login for your community's toolkit</h3> <h3>A common login for your community's toolkit</h3>
<p> <p>
Every community rSpace comes with a full suite of interoperable tools &mdash; Every community rSpace comes with a full suite of interoperable tools &mdash;
@ -461,36 +365,37 @@
</p> </p>
</div> </div>
</div> </div>
</div>
<!-- Offline-First Section --> <!-- ═══════ Offline-First ═══════ -->
<div class="section"> <div class="rl-section rl-section--alt">
<div class="section-divider"></div> <div class="rl-container" style="text-align: center;">
<h2>Offline-First, Always Available</h2> <h2 class="rl-heading">Offline-First, Always Available</h2>
<p class="section-subtitle"> <p class="rl-subtext">
rSpace works without an internet connection. Edit your canvas on a plane, rSpace works without an internet connection. Edit your canvas on a plane,
in a field, or underground &mdash; your changes merge automatically when in a field, or underground &mdash; your changes merge automatically when
you're back online. No sync buttons, no conflict dialogs. you're back online. No sync buttons, no conflict dialogs.
</p> </p>
<div class="pillars"> <div class="rl-grid-3">
<div class="pillar"> <div class="rl-card rl-card--center">
<div class="pillar-icon">💾</div> <div class="rl-icon-box">💾</div>
<div class="pillar-title">Local Persistence</div> <h3>Local Persistence</h3>
<div class="pillar-desc">Your canvas is cached in the browser. Refresh the page &mdash; it loads instantly, even offline.</div> <p>Your canvas is cached in the browser. Refresh the page &mdash; it loads instantly, even offline.</p>
</div> </div>
<div class="pillar"> <div class="rl-card rl-card--center">
<div class="pillar-icon">🔀</div> <div class="rl-icon-box">🔀</div>
<div class="pillar-title">Auto-Merge</div> <h3>Auto-Merge</h3>
<div class="pillar-desc">Automerge CRDTs resolve conflicts automatically. Multiple people can edit the same canvas offline and merge without data loss.</div> <p>Automerge CRDTs resolve conflicts automatically. Multiple people can edit the same canvas offline and merge without data loss.</p>
</div> </div>
<div class="pillar"> <div class="rl-card rl-card--center">
<div class="pillar-icon"></div> <div class="rl-icon-box"></div>
<div class="pillar-title">Incremental Sync</div> <h3>Incremental Sync</h3>
<div class="pillar-desc">Only new changes are transferred on reconnect &mdash; not the whole document. Fast even on slow connections.</div> <p>Only new changes are transferred on reconnect &mdash; not the whole document. Fast even on slow connections.</p>
</div> </div>
</div> </div>
<div class="identity-card"> <div class="rl-card" style="text-align: left; margin-top: 1.5rem;">
<h3>How it works</h3> <h3>How it works</h3>
<p> <p>
Every rSpace canvas is an <span class="hl">Automerge CRDT document</span> stored Every rSpace canvas is an <span class="hl">Automerge CRDT document</span> stored
@ -505,12 +410,13 @@
</p> </p>
</div> </div>
</div> </div>
</div>
<!-- Interoperable Ecosystem Section --> <!-- ═══════ Interoperable Ecosystem ═══════ -->
<div class="section"> <div class="rl-section">
<div class="section-divider"></div> <div class="rl-container" style="text-align: center;">
<h2>Interoperable by Design</h2> <h2 class="rl-heading">Interoperable by Design</h2>
<p class="section-subtitle"> <p class="rl-subtext">
Data flows between your community's tools because they share a common Data flows between your community's tools because they share a common
foundation: the same identity, the same real-time sync layer, and the same foundation: the same identity, the same real-time sync layer, and the same
local-first architecture. local-first architecture.
@ -534,7 +440,8 @@
</pre> </pre>
</div> </div>
<div class="identity-card"> <div class="rl-grid-2" style="margin-top: 1.5rem;">
<div class="rl-card" style="text-align: left;">
<h3>Your data, connected across tools</h3> <h3>Your data, connected across tools</h3>
<p> <p>
A budget created in <span class="hl">rFunds</span> can reference a vote A budget created in <span class="hl">rFunds</span> can reference a vote
@ -546,8 +453,7 @@
across every tool and every collaborator &mdash; conflict-free. across every tool and every collaborator &mdash; conflict-free.
</p> </p>
</div> </div>
<div class="rl-card" style="text-align: left;">
<div class="identity-card">
<h3>No vendor lock-in, no data silos</h3> <h3>No vendor lock-in, no data silos</h3>
<p> <p>
Every piece of community data is stored as a local-first CRDT document Every piece of community data is stored as a local-first CRDT document
@ -557,6 +463,7 @@
so that the community &mdash; not the platform &mdash; controls the data. so that the community &mdash; not the platform &mdash; controls the data.
</p> </p>
</div> </div>
</div>
<div class="ecosystem-apps"> <div class="ecosystem-apps">
<a href="https://rspace.online/rspace" class="ecosystem-app">🌌 rSpace</a> <a href="https://rspace.online/rspace" class="ecosystem-app">🌌 rSpace</a>
@ -580,11 +487,13 @@
🔐 Learn more about EncryptID 🔐 Learn more about EncryptID
</a> </a>
</div> </div>
</div>
<div class="section"> <!-- ═══════ Newsletter ═══════ -->
<div class="section-divider"></div> <div class="rl-section rl-section--alt">
<h2>Stay Connected</h2> <div class="rl-container" style="text-align: center;">
<p class="section-subtitle"> <h2 class="rl-heading">Stay Connected</h2>
<p class="rl-subtext">
Get updates on rSpace development, new ecosystem modules, and community features. Get updates on rSpace development, new ecosystem modules, and community features.
</p> </p>
@ -603,6 +512,7 @@
<p class="newsletter-privacy">No spam, unsubscribe anytime.</p> <p class="newsletter-privacy">No spam, unsubscribe anytime.</p>
</form> </form>
</div> </div>
</div>
<script type="module"> <script type="module">
import { RStackIdentity, isAuthenticated, getAccessToken } from "@shared/components/rstack-identity"; import { RStackIdentity, isAuthenticated, getAccessToken } from "@shared/components/rstack-identity";