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,149 +273,156 @@
<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 class="rl-grid-4" style="margin-top: 3rem;">
<div class="rl-card rl-card--center">
<div class="rl-icon-box">🎨</div>
<h3>Spatial Canvas</h3>
<p>Infinite collaborative workspace</p>
</div> </div>
<div class="rl-card rl-card--center">
<div class="features"> <div class="rl-icon-box">🔄</div>
<div class="feature"> <h3>Real-time Sync</h3>
<div class="feature-icon">🎨</div> <p>Powered by Automerge CRDT</p>
<div class="feature-title">Spatial Canvas</div> </div>
<div class="feature-desc">Infinite collaborative workspace</div> <div class="rl-card rl-card--center">
</div> <div class="rl-icon-box">📡</div>
<div class="feature"> <h3>Offline-First</h3>
<div class="feature-icon">🔄</div> <p>Works without internet, merges on reconnect</p>
<div class="feature-title">Real-time Sync</div> </div>
<div class="feature-desc">Powered by Automerge CRDT</div> <div class="rl-card rl-card--center">
</div> <div class="rl-icon-box">🌐</div>
<div class="feature"> <h3>Your Subdomain</h3>
<div class="feature-icon">📡</div> <p>community.rspace.online</p>
<div class="feature-title">Offline-First</div>
<div class="feature-desc">Works without internet, merges on reconnect</div>
</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>
</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>
<div class="pillars">
<div class="pillar">
<div class="pillar-icon">🔑</div>
<div class="pillar-title">Passkey Login</div>
<div class="pillar-desc">Hardware-backed biometric auth. Phishing-resistant by design.</div>
</div>
<div class="pillar">
<div class="pillar-icon">🏠</div>
<div class="pillar-title">Local-First</div>
<div class="pillar-desc">Cryptographic keys are derived and stored on your device, never uploaded.</div>
</div>
<div class="pillar">
<div class="pillar-icon">🔗</div>
<div class="pillar-title">One Login, All Apps</div>
<div class="pillar-desc">Authenticate once and access every r-Ecosystem tool seamlessly.</div>
</div>
</div>
<div class="identity-card">
<h3>Secure by default, not by opt-in</h3>
<p>
EncryptID uses <span class="hl">WebAuthn passkeys</span> as the root of trust &mdash;
the same standard behind Face ID and fingerprint unlock. Your identity is bound to
your device's secure hardware, so there are no passwords to leak, phish, or forget.
End-to-end encryption keys are <span class="hl">derived locally via HKDF</span>,
meaning the server never sees your private keys. If you lose your device,
<span class="hl">social recovery</span> lets trusted guardians help you regain access
without seed phrases or centralized reset flows.
</p> </p>
</div>
<div class="identity-card"> <div class="rl-grid-3">
<h3>A common login for your community's toolkit</h3> <div class="rl-card rl-card--center">
<p> <div class="rl-icon-box rl-icon-box--purple">🔑</div>
Every community rSpace comes with a full suite of interoperable tools &mdash; <h3>Passkey Login</h3>
voting, budgets, maps, files, notes, and more &mdash; all sharing the same <p>Hardware-backed biometric auth. Phishing-resistant by design.</p>
<span class="hl">EncryptID session</span>. Sign in once on rSpace and you're </div>
already authenticated on rVote, rFunds, rFiles, and every other tool your <div class="rl-card rl-card--center">
community uses. No separate accounts, no OAuth redirects, no third-party identity <div class="rl-icon-box rl-icon-box--purple">🏠</div>
providers. Your community, your identity, your data. <h3>Local-First</h3>
</p> <p>Cryptographic keys are derived and stored on your device, never uploaded.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box rl-icon-box--purple">🔗</div>
<h3>One Login, All Apps</h3>
<p>Authenticate once and access every r-Ecosystem tool seamlessly.</p>
</div>
</div>
<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>
<p>
EncryptID uses <span class="hl">WebAuthn passkeys</span> as the root of trust &mdash;
the same standard behind Face ID and fingerprint unlock. Your identity is bound to
your device's secure hardware, so there are no passwords to leak, phish, or forget.
End-to-end encryption keys are <span class="hl">derived locally via HKDF</span>,
meaning the server never sees your private keys. If you lose your device,
<span class="hl">social recovery</span> lets trusted guardians help you regain access
without seed phrases or centralized reset flows.
</p>
</div>
<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>
<p>
Every community rSpace comes with a full suite of interoperable tools &mdash;
voting, budgets, maps, files, notes, and more &mdash; all sharing the same
<span class="hl">EncryptID session</span>. Sign in once on rSpace and you're
already authenticated on rVote, rFunds, rFiles, and every other tool your
community uses. No separate accounts, no OAuth redirects, no third-party identity
providers. Your community, your identity, your data.
</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>
<div class="pillars">
<div class="pillar">
<div class="pillar-icon">💾</div>
<div class="pillar-title">Local Persistence</div>
<div class="pillar-desc">Your canvas is cached in the browser. Refresh the page &mdash; it loads instantly, even offline.</div>
</div>
<div class="pillar">
<div class="pillar-icon">🔀</div>
<div class="pillar-title">Auto-Merge</div>
<div class="pillar-desc">Automerge CRDTs resolve conflicts automatically. Multiple people can edit the same canvas offline and merge without data loss.</div>
</div>
<div class="pillar">
<div class="pillar-icon"></div>
<div class="pillar-title">Incremental Sync</div>
<div class="pillar-desc">Only new changes are transferred on reconnect &mdash; not the whole document. Fast even on slow connections.</div>
</div>
</div>
<div class="identity-card">
<h3>How it works</h3>
<p>
Every rSpace canvas is an <span class="hl">Automerge CRDT document</span> stored
locally in your browser's IndexedDB. When you open a canvas, it renders from the
local cache first &mdash; no waiting for the server. Edits you make are saved locally
and synced to the server via WebSocket when a connection is available. If you go offline,
the app keeps working: a <span class="hl">Service Worker</span> serves the app shell
from cache, and your changes accumulate in the local CRDT document. When connectivity
returns, Automerge's <span class="hl">incremental sync protocol</span> reconciles
your changes with everyone else's &mdash; conflict-free, automatically. No manual
merge, no "which version do you want to keep?" dialogs.
</p> </p>
<div class="rl-grid-3">
<div class="rl-card rl-card--center">
<div class="rl-icon-box">💾</div>
<h3>Local Persistence</h3>
<p>Your canvas is cached in the browser. Refresh the page &mdash; it loads instantly, even offline.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">🔀</div>
<h3>Auto-Merge</h3>
<p>Automerge CRDTs resolve conflicts automatically. Multiple people can edit the same canvas offline and merge without data loss.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box"></div>
<h3>Incremental Sync</h3>
<p>Only new changes are transferred on reconnect &mdash; not the whole document. Fast even on slow connections.</p>
</div>
</div>
<div class="rl-card" style="text-align: left; margin-top: 1.5rem;">
<h3>How it works</h3>
<p>
Every rSpace canvas is an <span class="hl">Automerge CRDT document</span> stored
locally in your browser's IndexedDB. When you open a canvas, it renders from the
local cache first &mdash; no waiting for the server. Edits you make are saved locally
and synced to the server via WebSocket when a connection is available. If you go offline,
the app keeps working: a <span class="hl">Service Worker</span> serves the app shell
from cache, and your changes accumulate in the local CRDT document. When connectivity
returns, Automerge's <span class="hl">incremental sync protocol</span> reconciles
your changes with everyone else's &mdash; conflict-free, automatically. No manual
merge, no "which version do you want to keep?" dialogs.
</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.
</p> </p>
<div class="flow-diagram"> <div class="flow-diagram">
<pre> <pre>
EncryptID (identity) EncryptID (identity)
| |
@ -532,76 +438,80 @@
v v
Your device (keys &amp; data stay here) Your device (keys &amp; data stay here)
</pre> </pre>
</div> </div>
<div class="identity-card"> <div class="rl-grid-2" style="margin-top: 1.5rem;">
<h3>Your data, connected across tools</h3> <div class="rl-card" style="text-align: left;">
<p> <h3>Your data, connected across tools</h3>
A budget created in <span class="hl">rFunds</span> can reference a vote <p>
from <span class="hl">rVote</span>. A map pin in <span class="hl">rMaps</span> A budget created in <span class="hl">rFunds</span> can reference a vote
can link to files in <span class="hl">rFiles</span> and notes in from <span class="hl">rVote</span>. A map pin in <span class="hl">rMaps</span>
<span class="hl">rNotes</span>. Because all r-Ecosystem tools share the same can link to files in <span class="hl">rFiles</span> and notes in
<span class="hl">Automerge CRDT sync layer</span>, data is interoperable <span class="hl">rNotes</span>. Because all r-Ecosystem tools share the same
without import/export steps or API integrations. Changes propagate in real-time <span class="hl">Automerge CRDT sync layer</span>, data is interoperable
across every tool and every collaborator &mdash; conflict-free. without import/export steps or API integrations. Changes propagate in real-time
</p> across every tool and every collaborator &mdash; conflict-free.
</div> </p>
</div>
<div class="rl-card" style="text-align: left;">
<h3>No vendor lock-in, no data silos</h3>
<p>
Every piece of community data is stored as a local-first CRDT document
that your community owns. There's no central server gating access and no
proprietary format trapping your data. Export everything. Fork your community.
Move between hosts. The <span class="hl">r-Ecosystem</span> is designed
so that the community &mdash; not the platform &mdash; controls the data.
</p>
</div>
</div>
<div class="identity-card"> <div class="ecosystem-apps">
<h3>No vendor lock-in, no data silos</h3> <a href="https://rspace.online/rspace" class="ecosystem-app">🌌 rSpace</a>
<p> <a href="https://rspace.online/rwallet" class="ecosystem-app">💰 rWallet</a>
Every piece of community data is stored as a local-first CRDT document <a href="https://rspace.online/rvote" class="ecosystem-app">🗳 rVote</a>
that your community owns. There's no central server gating access and no <a href="https://rspace.online/rmaps" class="ecosystem-app">🗺 rMaps</a>
proprietary format trapping your data. Export everything. Fork your community. <a href="https://rspace.online/rfiles" class="ecosystem-app">📁 rFiles</a>
Move between hosts. The <span class="hl">r-Ecosystem</span> is designed <a href="https://rspace.online/rnotes" class="ecosystem-app">📝 rNotes</a>
so that the community &mdash; not the platform &mdash; controls the data. <a href="https://rspace.online/rtrips" class="ecosystem-app">✈ rTrips</a>
</p> <a href="https://rspace.online/rfunds" class="ecosystem-app">💸 rFunds</a>
</div> <a href="https://rspace.online/rnetwork" class="ecosystem-app">🕸️ rNetwork</a>
<a href="https://rspace.online/rcart" class="ecosystem-app">🛒 rCart</a>
<a href="https://rspace.online/rtube" class="ecosystem-app">🎬 rTube</a>
<a href="https://rspace.online/rchats" class="ecosystem-app">💬 rChats</a>
<a href="https://rspace.online/rforum" class="ecosystem-app">💭 rForum</a>
<a href="https://rspace.online/rswag" class="ecosystem-app">👕 rSwag</a>
<a href="https://rspace.online/rdata" class="ecosystem-app">📊 rData</a>
</div>
<div class="ecosystem-apps"> <a href="https://ridentity.online" class="encryptid-link">
<a href="https://rspace.online/rspace" class="ecosystem-app">🌌 rSpace</a> 🔐 Learn more about EncryptID
<a href="https://rspace.online/rwallet" class="ecosystem-app">💰 rWallet</a> </a>
<a href="https://rspace.online/rvote" class="ecosystem-app">🗳 rVote</a>
<a href="https://rspace.online/rmaps" class="ecosystem-app">🗺 rMaps</a>
<a href="https://rspace.online/rfiles" class="ecosystem-app">📁 rFiles</a>
<a href="https://rspace.online/rnotes" class="ecosystem-app">📝 rNotes</a>
<a href="https://rspace.online/rtrips" class="ecosystem-app">✈ rTrips</a>
<a href="https://rspace.online/rfunds" class="ecosystem-app">💸 rFunds</a>
<a href="https://rspace.online/rnetwork" class="ecosystem-app">🕸️ rNetwork</a>
<a href="https://rspace.online/rcart" class="ecosystem-app">🛒 rCart</a>
<a href="https://rspace.online/rtube" class="ecosystem-app">🎬 rTube</a>
<a href="https://rspace.online/rchats" class="ecosystem-app">💬 rChats</a>
<a href="https://rspace.online/rforum" class="ecosystem-app">💭 rForum</a>
<a href="https://rspace.online/rswag" class="ecosystem-app">👕 rSwag</a>
<a href="https://rspace.online/rdata" class="ecosystem-app">📊 rData</a>
</div> </div>
<a href="https://ridentity.online" class="encryptid-link">
🔐 Learn more about EncryptID
</a>
</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>
Get updates on rSpace development, new ecosystem modules, and community features. <p class="rl-subtext">
</p> Get updates on rSpace development, new ecosystem modules, and community features.
</p>
<form class="newsletter-form" id="newsletter-form"> <form class="newsletter-form" id="newsletter-form">
<div class="newsletter-row"> <div class="newsletter-row">
<input <input
type="email" type="email"
id="newsletter-email" id="newsletter-email"
placeholder="your@email.com" placeholder="your@email.com"
required required
class="newsletter-input" class="newsletter-input"
/> />
<button type="submit" class="newsletter-btn" id="newsletter-btn">Subscribe</button> <button type="submit" class="newsletter-btn" id="newsletter-btn">Subscribe</button>
</div> </div>
<p class="newsletter-status" id="newsletter-status"></p> <p class="newsletter-status" id="newsletter-status"></p>
<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">