bunker-admin d010993994 Add pagination to public endpoints, Pangolin site picker, and docs editor toolbar
- Paginate public APIs: campaigns, petitions, shifts, products, pages, shop
- Add safety caps (take limits) to gallery ads, cuts, plans, donation pages
- Add Pangolin connect-site endpoint with .env writer and site ID validation
- Add formatting toolbar + keyboard shortcuts to shared doc editor
- Fix Dockerfile to support su-exec privilege dropping for mounted volumes
- Fix duplicate WebSocket headers in nginx API location block
- Update MkDocs site build and social card assets

Bunker Admin
2026-04-07 16:50:20 -06:00

4783 lines
197 KiB
HTML

<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Changemaker Lite - Grow Power. Don't Rent It.</title>
<meta name="description" content="Self-hosted campaign power tools. Own every byte of data. Free and open source software for community organizers.">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>✊</text></svg>">
<style>
/* ============================================
RESET & BASE
============================================ */
*, *::before, *::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* ============================================
CSS VARIABLES — DARK THEME DEFAULT
============================================ */
:root {
/* Brand */
--primary: #6f42c1;
--primary-light: #8B5CF6;
--primary-dark: #5a32a3;
/* Mycelium organic palette */
--myc-tendril: rgba(139, 92, 246, 0.35);
--myc-tendril-solid: #8B5CF6;
--myc-glow: rgba(111, 66, 193, 0.3);
--myc-pink: rgba(245, 169, 184, 0.6);
--myc-node-bg: rgba(139, 92, 246, 0.08);
--myc-node-border: rgba(139, 92, 246, 0.25);
/* Branch accent colors */
--branch-comm: #C084FC;
--branch-map: #34D399;
--branch-content: #FB923C;
--branch-data: #22D3EE;
--branch-devops: #FBBF24;
--branch-sovereignty: #F87171;
--branch-fundraising: #EC4899;
--branch-social: #38BDF8;
/* Surfaces — dark */
--bg-deep: #0F172A;
--bg-surface: #1E293B;
--bg-elevated: #334155;
--bg-card: rgba(30, 41, 59, 0.93);
--border-color: rgba(148, 163, 184, 0.15);
/* Text — dark */
--text-primary: #F1F5F9;
--text-secondary: #94A3B8;
--text-muted: #64748B;
/* Misc */
--success: #4ADE80;
--warning: #FBBF24;
--danger: #F87171;
--shadow-sm: 0 1px 3px rgba(0,0,0,0.3);
--shadow-md: 0 4px 14px rgba(0,0,0,0.3);
--shadow-lg: 0 10px 30px rgba(0,0,0,0.4);
--radius: 12px;
--radius-lg: 20px;
--transition: 0.3s ease;
--max-width: 1200px;
--header-height: 64px;
}
/* ============================================
LIGHT THEME OVERRIDES
============================================ */
[data-theme="light"] {
--bg-deep: #F8FAFC;
--bg-surface: #FFFFFF;
--bg-elevated: #F1F5F9;
--bg-card: rgba(255, 255, 255, 0.96);
--border-color: rgba(100, 116, 139, 0.2);
--text-primary: #0F172A;
--text-secondary: #475569;
--text-muted: #94A3B8;
--myc-tendril: rgba(111, 66, 193, 0.25);
--myc-glow: rgba(111, 66, 193, 0.15);
--myc-node-bg: rgba(111, 66, 193, 0.05);
--myc-node-border: rgba(111, 66, 193, 0.2);
--shadow-sm: 0 1px 3px rgba(0,0,0,0.08);
--shadow-md: 0 4px 14px rgba(0,0,0,0.08);
--shadow-lg: 0 10px 30px rgba(0,0,0,0.1);
}
/* ============================================
TYPOGRAPHY
============================================ */
html {
overflow-x: hidden;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
color: var(--text-primary);
line-height: 1.6;
overflow-x: hidden;
background-color: var(--bg-deep);
transition: background-color var(--transition), color var(--transition);
-webkit-font-smoothing: antialiased;
}
h1, h2, h3, h4 { line-height: 1.2; font-weight: 700; }
h1 { font-size: clamp(2.25rem, 5vw, 3.5rem); }
h2 { font-size: clamp(1.75rem, 3.5vw, 2.5rem); }
h3 { font-size: clamp(1.125rem, 2vw, 1.5rem); }
a { color: var(--primary-light); text-decoration: none; transition: color var(--transition); }
a:hover { color: var(--primary); }
mark { background: rgba(139, 92, 246, 0.3); color: inherit; padding: 0 2px; border-radius: 2px; }
/* ============================================
LAYOUT UTILITIES
============================================ */
.container {
max-width: var(--max-width);
margin: 0 auto;
padding: 0 2rem;
}
.section {
position: relative;
padding: 6rem 0;
overflow: hidden;
}
.section-header {
text-align: center;
margin-bottom: 4rem;
background: rgba(30, 41, 59, 0.45);
border: 1px solid rgba(148, 163, 184, 0.08);
border-radius: var(--radius-lg);
padding: 2rem 2.5rem;
max-width: 640px;
margin-left: auto;
margin-right: auto;
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
}
[data-theme="light"] .section-header {
background: rgba(255, 255, 255, 0.5);
border-color: rgba(100, 116, 139, 0.1);
}
.section-header h2 {
margin-bottom: 1rem;
}
.section-header p {
color: var(--text-secondary);
font-size: 1.125rem;
max-width: 640px;
margin: 0 auto;
}
/* ============================================
MYCELIUM SVG LAYER
============================================ */
.myc-svg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
pointer-events: none;
overflow: visible;
}
.myc-tendril {
fill: none;
stroke: var(--myc-tendril);
stroke-width: 1.5;
stroke-linecap: round;
opacity: 0;
transition: opacity 0.4s ease;
}
.myc-tendril.revealed {
opacity: 1;
}
.myc-tendril.thick {
stroke-width: 2.5;
}
.myc-tendril.thin {
stroke-width: 1;
opacity: 0;
}
.myc-tendril.thin.revealed { opacity: 0.6; }
/* Cross-section connector strips — removed, RootNetwork handles connections */
/* Node glow effect */
.myc-node {
position: relative;
z-index: 1;
}
.myc-node::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 120%;
height: 120%;
background: radial-gradient(circle, var(--myc-glow) 0%, transparent 70%);
border-radius: 50%;
animation: nodeGlow 4s ease-in-out infinite;
z-index: -1;
pointer-events: none;
}
@keyframes nodeGlow {
0%, 100% { opacity: 0.4; transform: translate(-50%, -50%) scale(1); }
50% { opacity: 0.8; transform: translate(-50%, -50%) scale(1.1); }
}
@keyframes tendrilGrow {
to { stroke-dashoffset: 0; }
}
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(24px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes pulseRing {
0% { transform: translate(-50%, -50%) scale(0.8); opacity: 0.6; }
100% { transform: translate(-50%, -50%) scale(1.4); opacity: 0; }
}
/* ============================================
HEADER
============================================ */
.header {
position: fixed;
top: 0;
width: 100%;
height: var(--header-height);
background: rgba(15, 23, 42, 0.6);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-bottom: 1px solid var(--border-color);
z-index: 1000;
transition: background var(--transition), box-shadow var(--transition);
}
[data-theme="light"] .header {
background: rgba(248, 250, 252, 0.8);
}
.header.scrolled {
box-shadow: var(--shadow-md);
}
.nav-container {
max-width: var(--max-width);
margin: 0 auto;
padding: 0 2rem;
height: 100%;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
font-size: clamp(1rem, 3.5vw, 1.35rem);
font-weight: 800;
color: var(--primary-light);
display: flex;
align-items: center;
gap: 0.4rem;
white-space: nowrap;
}
.logo-emoji {
font-size: clamp(1.1rem, 3.5vw, 1.5rem);
line-height: 1;
}
.logo-tagline {
font-size: 0.7rem;
font-weight: 400;
color: var(--text-muted);
display: block;
letter-spacing: 0.02em;
margin-top: -2px;
}
.nav-right {
display: flex;
align-items: center;
gap: 1.5rem;
}
.nav-links {
display: flex;
gap: 1.75rem;
list-style: none;
}
.nav-links a {
color: var(--text-secondary);
font-size: 0.9rem;
font-weight: 500;
transition: color var(--transition);
position: relative;
}
.nav-links a:hover {
color: var(--text-primary);
}
.nav-links a::after {
content: '';
position: absolute;
bottom: -4px;
left: 0;
width: 0;
height: 2px;
background: var(--primary-light);
transition: width var(--transition);
border-radius: 1px;
}
.nav-links a:hover::after {
width: 100%;
}
/* Theme toggle */
.theme-toggle {
background: none;
border: 1px solid var(--border-color);
border-radius: 8px;
width: 38px;
height: 38px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: var(--text-secondary);
transition: all var(--transition);
}
.theme-toggle:hover {
border-color: var(--primary-light);
color: var(--primary-light);
}
.theme-toggle svg {
width: 18px;
height: 18px;
}
.theme-toggle .icon-sun { display: none; }
.theme-toggle .icon-moon { display: block; }
[data-theme="light"] .theme-toggle .icon-sun { display: block; }
[data-theme="light"] .theme-toggle .icon-moon { display: none; }
/* CTA button */
.btn-primary {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.625rem 1.5rem;
background: var(--primary);
color: #fff;
font-weight: 600;
font-size: 0.9rem;
border-radius: 8px;
border: none;
cursor: pointer;
transition: all var(--transition);
text-decoration: none;
}
.btn-primary:hover {
background: var(--primary-dark);
color: #fff;
transform: translateY(-1px);
box-shadow: 0 4px 16px rgba(111, 66, 193, 0.4);
}
.btn-demo {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.625rem 1.5rem;
background: transparent;
color: var(--primary-light);
font-weight: 600;
font-size: 0.9rem;
border-radius: 8px;
border: 1px solid var(--primary-light);
cursor: pointer;
transition: all var(--transition);
text-decoration: none;
}
.btn-demo:hover {
background: rgba(139, 92, 246, 0.1);
color: var(--primary-light);
transform: translateY(-1px);
box-shadow: 0 4px 16px rgba(111, 66, 193, 0.2);
}
.btn-secondary {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.625rem 1.5rem;
background: transparent;
color: var(--text-primary);
font-weight: 600;
font-size: 0.9rem;
border-radius: 8px;
border: 1px solid var(--border-color);
cursor: pointer;
transition: all var(--transition);
text-decoration: none;
}
.btn-secondary:hover {
border-color: var(--primary-light);
color: var(--primary-light);
transform: translateY(-1px);
}
/* Hamburger */
.hamburger {
display: none;
background: none;
border: none;
cursor: pointer;
padding: 4px;
color: var(--text-primary);
}
.hamburger svg { width: 24px; height: 24px; }
.hamburger .icon-close { display: none; }
/* Mobile menu */
.mobile-menu {
display: none;
position: fixed;
top: var(--header-height);
left: 0;
width: 100%;
height: calc(100vh - var(--header-height));
background: var(--bg-deep);
z-index: 999;
padding: 2rem;
flex-direction: column;
gap: 1rem;
overflow-y: auto;
}
.mobile-menu.open {
display: flex;
}
.mobile-menu a {
color: var(--text-primary);
font-size: 1.125rem;
font-weight: 500;
padding: 1rem 0;
border-bottom: 1px solid var(--border-color);
}
/* ============================================
HERO
============================================ */
.hero {
position: relative;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
padding: calc(var(--header-height) + 2rem) 2rem 0;
overflow: hidden;
}
.hero-bg {
position: absolute;
inset: 0;
z-index: 0;
}
/* Root ball glow — positioned in lower third */
.hero-root-glow {
position: absolute;
top: 65%;
left: 50%;
transform: translate(-50%, -50%);
width: 600px;
height: 600px;
background: radial-gradient(circle, rgba(111, 66, 193, 0.15) 0%, rgba(139, 92, 246, 0.05) 40%, transparent 70%);
border-radius: 50%;
animation: nodeGlow 6s ease-in-out infinite;
pointer-events: none;
}
[data-theme="light"] .hero-root-glow {
background: radial-gradient(circle, rgba(111, 66, 193, 0.08) 0%, rgba(139, 92, 246, 0.03) 40%, transparent 70%);
}
[data-theme="light"] .showcase-card {
border-color: rgba(100, 116, 139, 0.2);
}
[data-theme="light"] .showcase-card.active {
box-shadow: 0 0 30px rgba(111, 66, 193, 0.08), 0 8px 32px rgba(0, 0, 0, 0.1);
border-color: rgba(111, 66, 193, 0.25);
}
[data-theme="light"] .hero-stats {
background: rgba(255, 255, 255, 0.9);
border-color: rgba(100, 116, 139, 0.15);
}
[data-theme="light"] .hero-pill {
background: rgba(111, 66, 193, 0.06);
border-color: rgba(111, 66, 193, 0.15);
}
.hero-root-svg {
position: absolute;
top: 65%;
left: 50%;
transform: translate(-50%, -50%);
width: 500px;
height: 500px;
pointer-events: none;
opacity: 0;
animation: fadeIn 2s ease 0.5s forwards;
}
@keyframes fadeIn {
to { opacity: 1; }
}
/* ---- Two-column hero grid (desktop) ---- */
.hero-content {
position: relative;
z-index: 1;
width: 100%;
max-width: 1400px;
display: grid;
grid-template-columns: minmax(380px, 1fr) minmax(0, 1.4fr);
grid-template-rows: auto auto;
gap: 0 2.5rem;
align-items: start;
}
.hero-left {
text-align: left;
padding-top: 1.5rem;
}
.hero-right {
display: flex;
align-items: flex-start;
justify-content: flex-end;
padding-top: 0.5rem;
}
.hero-right > div {
width: 100%;
}
/* Spans full width below both columns */
.hero-bottom {
grid-column: 1 / -1;
margin-top: 1.5rem;
}
.hero-badges-row {
display: flex;
flex-wrap: nowrap;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
}
.hero-badge {
display: inline-block;
padding: 0.375rem 1rem;
background: var(--myc-node-bg);
border: 1px solid var(--myc-node-border);
border-radius: 100px;
color: var(--primary-light);
font-size: 0.8rem;
font-weight: 600;
letter-spacing: 0.02em;
margin-bottom: 0;
}
.beta-pill {
display: inline-block;
padding: 0.35rem 1.1rem;
background: linear-gradient(135deg, #EC4899, #8B5CF6);
border: none;
border-radius: 100px;
color: #fff;
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.04em;
cursor: pointer;
margin-bottom: 0;
box-shadow: 0 0 16px rgba(236, 72, 153, 0.4), 0 0 40px rgba(139, 92, 246, 0.2);
transition: transform 0.2s ease, box-shadow 0.2s ease;
animation: beta-pulse 3s ease-in-out infinite;
}
.beta-pill:hover {
transform: scale(1.06);
box-shadow: 0 0 22px rgba(236, 72, 153, 0.55), 0 0 50px rgba(139, 92, 246, 0.35);
}
@keyframes beta-pulse {
0%, 100% { box-shadow: 0 0 16px rgba(236, 72, 153, 0.4), 0 0 40px rgba(139, 92, 246, 0.2); }
50% { box-shadow: 0 0 24px rgba(236, 72, 153, 0.6), 0 0 55px rgba(139, 92, 246, 0.35); }
}
.beta-modal-backdrop {
display: none;
position: fixed;
inset: 0;
z-index: 9999;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
align-items: center;
justify-content: center;
}
.beta-modal-backdrop.open { display: flex; }
.beta-modal {
background: var(--bg-surface);
border: 1px solid var(--myc-node-border);
border-radius: var(--radius-lg);
max-width: 440px;
width: 90%;
padding: 2.5rem 2rem 2rem;
text-align: center;
box-shadow: var(--shadow-lg), 0 0 60px rgba(139, 92, 246, 0.15);
position: relative;
animation: beta-modal-in 0.25s ease-out;
}
@keyframes beta-modal-in {
from { opacity: 0; transform: scale(0.92) translateY(12px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
.beta-modal-emoji { font-size: 2.5rem; margin-bottom: 1rem; }
.beta-modal h3 {
font-size: 1.25rem;
color: var(--text-primary);
margin-bottom: 0.75rem;
}
.beta-modal p {
color: var(--text-secondary);
font-size: 0.95rem;
line-height: 1.7;
margin-bottom: 1.5rem;
}
.beta-modal-close {
display: inline-block;
padding: 0.5rem 1.5rem;
background: linear-gradient(135deg, #8B5CF6, #6f42c1);
border: none;
border-radius: 100px;
color: #fff;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.beta-modal-close:hover {
transform: scale(1.04);
box-shadow: 0 0 14px rgba(139, 92, 246, 0.4);
}
.hero h1 {
background: linear-gradient(135deg, var(--primary-light) 0%, #C084FC 50%, #F5A9B8 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 0.5rem;
font-size: clamp(2.5rem, 5vw, 3.75rem);
}
/* ---- Typewriter rotating line ---- */
.hero-rotating-line {
font-size: clamp(1.15rem, 2.2vw, 1.5rem);
color: var(--text-secondary);
margin-bottom: 1.25rem;
min-height: 2.2em;
line-height: 1.5;
}
.hero-rotating-line .tw-static {
color: var(--text-secondary);
}
.hero-rotating-line .tw-word {
color: var(--primary-light);
font-weight: 700;
border-right: 2px solid var(--primary-light);
padding-right: 2px;
animation: blink-cursor 0.75s step-end infinite;
}
@keyframes blink-cursor {
50% { border-color: transparent; }
}
.hero-subtitle {
color: var(--text-secondary);
font-size: clamp(0.95rem, 1.8vw, 1.1rem);
max-width: 540px;
margin: 0 0 1.5rem;
line-height: 1.7;
}
.hero-cta {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
margin-bottom: 1.5rem;
}
/* ---- Feature pills ---- */
.hero-pills {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
.hero-pill {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.3rem 0.75rem;
background: var(--myc-node-bg);
border: 1px solid var(--myc-node-border);
border-radius: 100px;
color: var(--text-secondary);
font-size: 0.72rem;
font-weight: 500;
letter-spacing: 0.01em;
opacity: 0;
transform: translateY(8px);
transition: opacity 0.4s ease, transform 0.4s ease, background 0.2s ease;
}
.hero-pill.visible {
opacity: 1;
transform: translateY(0);
}
.hero-pill:hover {
background: rgba(139, 92, 246, 0.15);
color: var(--primary-light);
text-decoration: none;
}
.hero-pill .pill-icon {
font-size: 0.82rem;
line-height: 1;
}
/* ---- Feature showcase (right column) ---- */
.hero-showcase {
position: relative;
width: 100%;
aspect-ratio: 16 / 9.5;
perspective: 1200px;
}
.showcase-card {
position: absolute;
inset: 0;
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
display: flex;
flex-direction: column;
opacity: 0;
transform: rotateY(8deg) translateX(30px) scale(0.95);
transition: opacity 0.6s ease, transform 0.6s ease;
pointer-events: none;
overflow: hidden;
}
.showcase-card.active {
opacity: 1;
transform: rotateY(0deg) translateX(0) scale(1);
pointer-events: auto;
border-color: var(--myc-node-border);
box-shadow: 0 0 30px rgba(139, 92, 246, 0.12), var(--shadow-lg);
}
.showcase-card.exiting {
opacity: 0;
transform: rotateY(-8deg) translateX(-30px) scale(0.95);
}
/* Screenshot image fills the card */
.showcase-card-img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: top left;
display: block;
}
/* Caption overlay at bottom */
.showcase-card-caption {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 2.5rem 1.25rem 1rem;
background: linear-gradient(to top, rgba(15, 23, 42, 0.95) 0%, rgba(15, 23, 42, 0.75) 50%, transparent 100%);
display: flex;
align-items: flex-end;
gap: 0.75rem;
}
[data-theme="light"] .showcase-card-caption {
background: linear-gradient(to top, rgba(255, 255, 255, 0.97) 0%, rgba(255, 255, 255, 0.8) 50%, transparent 100%);
}
.showcase-card-icon {
width: 36px;
height: 36px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.1rem;
flex-shrink: 0;
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
}
.showcase-card-title {
font-size: 0.95rem;
font-weight: 700;
color: #F1F5F9;
line-height: 1.3;
}
[data-theme="light"] .showcase-card-title {
color: #0F172A;
}
.showcase-card-subtitle {
font-size: 0.72rem;
color: #94A3B8;
margin-top: 0.1rem;
}
[data-theme="light"] .showcase-card-subtitle {
color: #64748B;
}
/* Showcase progress dots */
.showcase-dots {
display: flex;
justify-content: center;
gap: 0.5rem;
margin-top: 0.75rem;
}
.showcase-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--text-muted);
opacity: 0.3;
cursor: pointer;
transition: opacity 0.3s ease, transform 0.3s ease, background 0.3s ease;
}
.showcase-dot.active {
opacity: 1;
background: var(--primary-light);
transform: scale(1.3);
}
.showcase-dot:hover {
opacity: 0.7;
}
/* ---- Particle canvas ---- */
.hero-particles {
position: absolute;
inset: 0;
z-index: 0;
pointer-events: none;
}
/* ---- Quick deploy terminal ---- */
.hero-terminal {
margin: 0.75rem 0 0;
background: #0D1117;
border: 1px solid rgba(139, 92, 246, 0.2);
border-radius: var(--radius);
opacity: 0;
transform: translateY(8px);
animation: fadeSlideIn 0.6s ease 1.4s forwards;
}
@keyframes fadeSlideIn {
to { opacity: 1; transform: translateY(0); }
}
[data-theme="light"] .hero-terminal {
background: #1E293B;
}
.hero-terminal-bar {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.85rem;
background: rgba(255, 255, 255, 0.04);
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
border-radius: var(--radius) var(--radius) 0 0;
}
.hero-terminal-dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.hero-terminal-title {
font-size: 0.65rem;
color: #64748B;
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
margin-left: auto;
}
.hero-terminal-body {
padding: 0.6rem 0.85rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.hero-terminal-prompt {
color: #34D399;
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
font-size: 0.82rem;
font-weight: 600;
flex-shrink: 0;
user-select: none;
}
.hero-terminal-cmd {
color: #E2E8F0;
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
font-size: 0.82rem;
white-space: nowrap;
overflow-x: auto;
overflow-y: hidden;
flex: 1;
min-width: 0;
scrollbar-width: none;
-ms-overflow-style: none;
}
.hero-terminal-cmd::-webkit-scrollbar { display: none; }
.hero-terminal-cmd .cmd-highlight {
color: var(--primary-light);
}
.hero-terminal-copy {
background: none;
border: 1px solid rgba(148, 163, 184, 0.2);
border-radius: 6px;
color: #94A3B8;
font-size: 0.7rem;
padding: 0.25rem 0.55rem;
cursor: pointer;
font-family: inherit;
transition: color 0.2s ease, border-color 0.2s ease, background 0.2s ease;
flex-shrink: 0;
display: flex;
align-items: center;
gap: 0.3rem;
}
.hero-terminal-copy:hover {
color: #E2E8F0;
border-color: rgba(148, 163, 184, 0.4);
background: rgba(255, 255, 255, 0.05);
}
.hero-terminal-copy.copied {
color: #34D399;
border-color: rgba(52, 211, 153, 0.3);
}
/* Search (moved out of hero, kept for search functionality) */
.hero-search {
display: none;
}
.search-input-wrap {
position: relative;
}
.search-input-wrap svg {
position: absolute;
left: 1rem;
top: 50%;
transform: translateY(-50%);
width: 18px;
height: 18px;
color: var(--text-muted);
pointer-events: none;
}
.search-box {
width: 100%;
padding: 0.875rem 1rem 0.875rem 2.75rem;
background: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: 10px;
color: var(--text-primary);
font-size: 0.95rem;
outline: none;
transition: all var(--transition);
}
.search-box::placeholder { color: var(--text-muted); }
.search-box:focus {
border-color: var(--primary-light);
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.15);
}
.search-kbd {
position: absolute;
right: 0.75rem;
top: 50%;
transform: translateY(-50%);
padding: 0.125rem 0.5rem;
background: var(--bg-elevated);
border: 1px solid var(--border-color);
border-radius: 4px;
color: var(--text-muted);
font-size: 0.75rem;
font-family: inherit;
pointer-events: none;
}
.search-results {
display: none;
position: absolute;
top: calc(100% + 8px);
left: 0;
right: 0;
background: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: var(--radius);
box-shadow: var(--shadow-lg);
max-height: 400px;
overflow-y: auto;
z-index: 100;
}
.search-results-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--border-color);
}
.results-count { color: var(--text-muted); font-size: 0.8rem; }
.close-results {
background: none;
border: none;
color: var(--text-muted);
font-size: 1.25rem;
cursor: pointer;
padding: 0 4px;
line-height: 1;
}
.close-results:hover { color: var(--text-primary); }
.search-result-item {
display: block;
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--border-color);
text-decoration: none;
transition: background var(--transition);
}
.search-result-item:hover {
background: var(--bg-elevated);
}
.search-result-title {
color: var(--text-primary);
font-weight: 600;
font-size: 0.9rem;
margin-bottom: 0.25rem;
}
.search-result-content {
color: var(--text-muted);
font-size: 0.8rem;
line-height: 1.5;
}
.no-results {
padding: 1.5rem;
text-align: center;
color: var(--text-muted);
}
/* Stats row */
.hero-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1.5rem;
max-width: 800px;
margin: 0 auto;
padding: 1.25rem 2rem;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
.hero-stat {
text-align: center;
}
.hero-stat-value {
font-size: 1.6rem;
font-weight: 800;
color: var(--primary-light);
font-variant-numeric: tabular-nums;
}
.hero-stat-label {
font-size: 0.75rem;
color: var(--text-muted);
margin-top: 0.25rem;
}
.hero-trust {
text-align: center;
margin-top: 1rem;
color: var(--text-muted);
font-size: 0.78rem;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
opacity: 0;
animation: fadeIn 1s ease 1.5s forwards;
}
.hero-trust svg {
width: 14px;
height: 14px;
color: var(--primary-light);
}
/* ============================================
PROBLEMS SECTION
============================================ */
.problems {
background: var(--bg-surface);
}
.problems-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.5rem;
position: relative;
z-index: 1;
}
.problem-card {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-top: 2px solid var(--danger);
border-radius: var(--radius);
padding: 1.75rem;
position: relative;
opacity: 0;
transform: translateY(20px);
transition: opacity 0.6s ease, transform 0.6s ease, border-color var(--transition);
}
.problem-card.visible {
opacity: 1;
transform: translateY(0);
}
.problem-card:hover {
border-color: var(--danger);
}
.problem-icon {
width: 44px;
height: 44px;
background: rgba(248, 113, 113, 0.1);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
margin-bottom: 1rem;
color: var(--danger);
border: 1px solid rgba(248, 113, 113, 0.2);
}
/* Broken tendril decoration */
.problem-card::after {
content: '';
position: absolute;
bottom: -2px;
left: 20%;
right: 20%;
height: 2px;
background: linear-gradient(90deg, transparent, rgba(248, 113, 113, 0.3), transparent);
border-radius: 1px;
}
.problem-card h3 {
font-size: 1.05rem;
margin-bottom: 0.5rem;
}
.problem-card p {
color: var(--text-secondary);
font-size: 0.9rem;
line-height: 1.6;
}
/* ============================================
GROWING CHANGE — Philosophy Bridge
============================================ */
.growing-change {
background: var(--bg-deep);
border-top: 1px solid var(--border-color);
border-bottom: 1px solid var(--border-color);
}
.growing-change-content {
max-width: 840px;
margin: 0 auto;
text-align: center;
}
.growing-change-card {
background: rgba(30, 41, 59, 0.85);
border: 1px solid rgba(148, 163, 184, 0.12);
border-radius: var(--radius-lg);
padding: 2.5rem 3rem;
margin-bottom: 3rem;
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
position: relative;
z-index: 2;
}
[data-theme="light"] .growing-change-card {
background: rgba(255, 255, 255, 0.82);
border-color: rgba(100, 116, 139, 0.15);
}
.growing-change-card h2 {
margin-bottom: 1.5rem;
background: linear-gradient(135deg, var(--primary-light) 0%, #C084FC 50%, var(--success) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.growing-lead {
font-size: 1.2rem;
color: var(--text-secondary);
line-height: 1.8;
margin-bottom: 1.25rem;
}
.growing-change-card > p {
color: var(--text-secondary);
font-size: 1.05rem;
line-height: 1.7;
}
.growing-change-card > p:last-child {
margin-bottom: 0;
}
.growing-callout {
font-size: 0.95rem;
color: var(--text-muted);
font-style: italic;
border-left: 3px solid var(--primary-light);
padding-left: 1rem;
margin-top: 1.25rem;
}
.growing-cta {
text-align: center;
margin-top: 2.5rem;
}
.growing-pillars {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.5rem;
text-align: left;
}
.growing-pillar {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius);
padding: 1.75rem;
transition: all var(--transition);
border-top: 2px solid var(--success);
}
.growing-pillar:nth-child(2) {
border-top-color: var(--primary-light);
}
.growing-pillar:nth-child(3) {
border-top-color: var(--branch-content);
}
.growing-pillar:hover {
transform: translateY(-3px);
box-shadow: var(--shadow-md);
border-color: var(--myc-node-border);
}
.pillar-icon {
font-size: 1.75rem;
margin-bottom: 0.75rem;
}
.growing-pillar h4 {
font-size: 1.05rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.growing-pillar p {
color: var(--text-secondary);
font-size: 0.9rem;
line-height: 1.6;
}
@media (max-width: 768px) {
.growing-pillars {
grid-template-columns: 1fr;
}
}
/* ============================================
FEATURE NETWORK — THE CORE VISUAL
============================================ */
.feature-network {
padding: 6rem 0 4rem;
}
.branch {
position: relative;
margin-bottom: 5rem;
}
/* Root network — page-spanning SVG, scroll-driven animation */
.root-network-svg {
position: absolute;
top: 0;
left: 0;
width: 100%;
pointer-events: none;
z-index: 1;
}
/* Ensure all text content renders above root network tendrils */
.section .container,
.hero-content,
.cta-content,
.branch-header,
.section-header,
.problem-card *,
.feature-node *,
.site-card *,
.pricing-card *,
.cost-compare *,
.node-header,
.node-desc,
.node-icon,
.problem-icon,
.problem-card h4,
.problem-card p {
position: relative;
z-index: 2;
}
.root-network-svg .root-line {
fill: none;
stroke-linecap: round;
/* opacity + dashoffset driven by JS scroll handler */
}
.root-network-svg .root-node {
/* opacity driven by JS scroll handler */
}
/* Floating background elements */
.floating-elements {
position: absolute;
top: 0;
left: 0;
width: 100%;
pointer-events: none;
z-index: 0;
overflow: hidden;
}
.floating-emoji {
position: absolute;
pointer-events: none;
user-select: none;
animation: floatDrift var(--float-duration, 75s) ease-in-out infinite;
}
@keyframes floatDrift {
0%, 100% { transform: translate(0, 0) rotate(0deg); }
25% { transform: translate(15px, -20px) rotate(5deg); }
50% { transform: translate(-10px, 10px) rotate(-3deg); }
75% { transform: translate(20px, 15px) rotate(4deg); }
}
.branch:last-child {
margin-bottom: 0;
}
.branch-header {
display: inline-flex;
align-items: center;
gap: 1rem;
margin-bottom: 2.5rem;
position: relative;
z-index: 2;
background: rgba(30, 41, 59, 0.4);
border: 1px solid rgba(148, 163, 184, 0.08);
border-radius: var(--radius);
padding: 1rem 1.5rem;
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
}
[data-theme="light"] .branch-header {
background: rgba(255, 255, 255, 0.45);
border-color: rgba(100, 116, 139, 0.1);
}
.branch-icon {
width: 48px;
height: 48px;
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
flex-shrink: 0;
}
.branch-icon.comm { background: rgba(192, 132, 252, 0.15); border: 1px solid rgba(192, 132, 252, 0.3); }
.branch-icon.map { background: rgba(52, 211, 153, 0.15); border: 1px solid rgba(52, 211, 153, 0.3); }
.branch-icon.content { background: rgba(251, 146, 60, 0.15); border: 1px solid rgba(251, 146, 60, 0.3); }
.branch-icon.data { background: rgba(34, 211, 238, 0.15); border: 1px solid rgba(34, 211, 238, 0.3); }
.branch-icon.devops { background: rgba(251, 191, 36, 0.15); border: 1px solid rgba(251, 191, 36, 0.3); }
.branch-icon.sovereignty { background: rgba(248, 113, 113, 0.15); border: 1px solid rgba(248, 113, 113, 0.3); }
.branch-icon.fundraising { background: rgba(236, 72, 153, 0.15); border: 1px solid rgba(236, 72, 153, 0.3); }
.branch-icon.social { background: rgba(56, 189, 248, 0.15); border: 1px solid rgba(56, 189, 248, 0.3); }
.branch-title h3 {
font-size: 1.35rem;
}
.branch-title p {
color: var(--text-muted);
font-size: 0.875rem;
margin-top: 0.25rem;
}
/* Branch preview screenshot */
.branch-preview {
margin-bottom: 2rem;
position: relative;
z-index: 2;
border-radius: var(--radius-lg);
overflow: hidden;
border: 1px solid var(--border-color);
box-shadow: var(--shadow-md);
background: var(--bg-card);
}
.branch-preview img {
width: 100%;
height: auto;
display: block;
}
.branch-preview-caption {
padding: 0.625rem 1.25rem;
font-size: 0.8rem;
color: var(--text-muted);
border-top: 1px solid var(--border-color);
display: flex;
align-items: center;
gap: 0.5rem;
}
.branch-preview-caption .caption-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--success);
flex-shrink: 0;
}
/* Feature nodes grid */
.nodes-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 1.25rem;
position: relative;
z-index: 1;
}
.feature-node {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius);
padding: 1.5rem;
position: relative;
opacity: 0;
transform: translateY(16px);
transition: opacity 0.5s ease, transform 0.5s ease, border-color var(--transition), box-shadow var(--transition);
}
.feature-node.visible {
opacity: 1;
transform: translateY(0);
}
.feature-node:hover {
border-color: var(--myc-node-border);
box-shadow: 0 0 20px var(--myc-glow);
}
/* Branch-specific card accents */
.branch-comm .feature-node { border-top: 2px solid var(--branch-comm); }
.branch-map .feature-node { border-top: 2px solid var(--branch-map); }
.branch-content .feature-node { border-top: 2px solid var(--branch-content); }
.branch-data .feature-node { border-top: 2px solid var(--branch-data); }
.branch-devops .feature-node { border-top: 2px solid var(--branch-devops); }
.branch-sovereignty .feature-node { border-top: 2px solid var(--branch-sovereignty); }
.branch-fundraising .feature-node { border-top: 2px solid var(--branch-fundraising); }
.branch-social .feature-node { border-top: 2px solid var(--branch-social); }
.branch-comm .feature-node:hover { border-color: var(--branch-comm); box-shadow: 0 0 20px rgba(192,132,252,0.2); }
.branch-map .feature-node:hover { border-color: var(--branch-map); box-shadow: 0 0 20px rgba(52,211,153,0.2); }
.branch-content .feature-node:hover { border-color: var(--branch-content); box-shadow: 0 0 20px rgba(251,146,60,0.2); }
.branch-data .feature-node:hover { border-color: var(--branch-data); box-shadow: 0 0 20px rgba(34,211,238,0.2); }
.branch-devops .feature-node:hover { border-color: var(--branch-devops); box-shadow: 0 0 20px rgba(251,191,36,0.2); }
.branch-sovereignty .feature-node:hover { border-color: var(--branch-sovereignty); box-shadow: 0 0 20px rgba(248,113,113,0.2); }
.branch-fundraising .feature-node:hover { border-color: var(--branch-fundraising); box-shadow: 0 0 20px rgba(236,72,153,0.2); }
.branch-social .feature-node:hover { border-color: var(--branch-social); box-shadow: 0 0 20px rgba(56,189,248,0.2); }
/* Branch-specific node icon tinting */
.branch-comm .node-icon { background: rgba(192,132,252,0.12); }
.branch-map .node-icon { background: rgba(52,211,153,0.12); }
.branch-content .node-icon { background: rgba(251,146,60,0.12); }
.branch-data .node-icon { background: rgba(34,211,238,0.12); }
.branch-devops .node-icon { background: rgba(251,191,36,0.12); }
.branch-sovereignty .node-icon { background: rgba(248,113,113,0.12); }
.branch-fundraising .node-icon { background: rgba(236,72,153,0.12); }
.branch-social .node-icon { background: rgba(56,189,248,0.12); }
.node-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.75rem;
}
.node-icon {
font-size: 1.25rem;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
background: var(--myc-node-bg);
flex-shrink: 0;
}
.node-header h4 {
font-size: 1rem;
font-weight: 600;
}
.feature-node p {
color: var(--text-secondary);
font-size: 0.85rem;
line-height: 1.6;
}
.node-tags {
display: flex;
flex-wrap: wrap;
gap: 0.375rem;
margin-top: 0.75rem;
}
.node-tag {
font-size: 0.7rem;
padding: 0.125rem 0.5rem;
background: var(--myc-node-bg);
border: 1px solid var(--myc-node-border);
border-radius: 100px;
color: var(--text-muted);
}
/* ============================================
FEATURE NODE — HOVER SCREENSHOT PREVIEW
============================================ */
.feature-node[data-screenshot] {
cursor: pointer;
}
.node-screenshot {
max-height: 0;
overflow: hidden;
margin: 0 -1.5rem;
border-radius: 0;
transition: max-height 0.35s cubic-bezier(0.4, 0, 0.2, 1), margin-top 0.35s ease, margin-bottom 0.35s ease;
margin-top: 0;
margin-bottom: 0;
}
.feature-node:hover .node-screenshot {
max-height: 300px;
margin-top: 0.75rem;
margin-bottom: 0.5rem;
}
.node-screenshot img {
width: 100%;
height: auto;
display: block;
border-top: 1px solid var(--border-color);
border-bottom: 1px solid var(--border-color);
}
.node-screenshot-label {
padding: 0.3rem 1.5rem;
font-size: 0.7rem;
color: var(--text-muted);
display: flex;
align-items: center;
gap: 0.4rem;
background: rgba(0,0,0,0.15);
}
[data-theme="light"] .node-screenshot-label {
background: rgba(0,0,0,0.04);
}
.node-screenshot-label .screenshot-dot {
width: 5px;
height: 5px;
border-radius: 50%;
background: var(--success);
flex-shrink: 0;
}
/* On mobile, hide hover screenshots (touch doesn't hover well) */
@media (max-width: 768px) {
.node-screenshot {
display: none;
}
}
/* ============================================
NARRATIVE BRIDGE SECTION
============================================ */
.narrative-bridge {
padding: 4rem 0 2rem;
background: transparent;
}
.narrative-content {
max-width: 720px;
margin: 0 auto;
text-align: center;
background: rgba(30, 41, 59, 0.85);
border: 1px solid rgba(148, 163, 184, 0.1);
border-radius: var(--radius-lg);
padding: 3rem 2.5rem;
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
position: relative;
z-index: 2;
}
[data-theme="light"] .narrative-content {
background: rgba(255, 255, 255, 0.88);
border-color: rgba(100, 116, 139, 0.15);
}
.narrative-eyebrow {
font-size: 0.8rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--primary-light);
margin-bottom: 1rem;
}
.narrative-content h2 {
font-size: clamp(1.5rem, 3vw, 2.25rem);
margin-bottom: 1.25rem;
line-height: 1.3;
}
.narrative-body {
color: var(--text-secondary);
font-size: 1.1rem;
line-height: 1.7;
}
/* In-feature narrative connectors */
.branch-narrative {
max-width: 640px;
margin: 2rem auto 3rem;
text-align: center;
padding: 1.5rem 2rem;
background: rgba(30, 41, 59, 0.85);
border: 1px solid rgba(148, 163, 184, 0.1);
border-radius: var(--radius-lg);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
position: relative;
z-index: 2;
}
[data-theme="light"] .branch-narrative {
background: rgba(255, 255, 255, 0.45);
border-color: rgba(100, 116, 139, 0.1);
}
.branch-narrative p {
color: var(--text-secondary);
font-size: 1rem;
line-height: 1.7;
margin: 0;
}
/* ============================================
LIVE SITES
============================================ */
.live-sites {
background: var(--bg-surface);
}
.sites-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.5rem;
position: relative;
z-index: 1;
margin-bottom: 3rem;
}
.site-card {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 0.75rem;
padding: 2rem 1.5rem;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
text-decoration: none;
transition: all var(--transition);
position: relative;
overflow: hidden;
border-top: 3px solid var(--primary-light);
}
.site-card:hover {
border-color: var(--primary-light);
transform: translateY(-4px);
box-shadow: 0 8px 30px rgba(139, 92, 246, 0.15);
}
.site-card.featured {
border-top-color: var(--warning);
}
.site-card .site-badge {
position: absolute;
top: 0;
right: 0;
padding: 0.3rem 0.9rem;
background: linear-gradient(135deg, var(--primary), var(--primary-light));
color: white;
font-size: 0.7rem;
font-weight: 600;
letter-spacing: 0.03em;
border-radius: 0 var(--radius-lg) 0 10px;
}
.site-icon {
width: 56px;
height: 56px;
border-radius: 16px;
background: rgba(139, 92, 246, 0.1);
border: 1px solid rgba(139, 92, 246, 0.2);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.75rem;
flex-shrink: 0;
transition: all var(--transition);
}
.site-card:hover .site-icon {
background: rgba(139, 92, 246, 0.18);
border-color: rgba(139, 92, 246, 0.35);
transform: scale(1.05);
}
.site-name {
font-weight: 700;
color: var(--text-primary);
font-size: 1.05rem;
}
.site-desc {
color: var(--text-secondary);
font-size: 0.85rem;
line-height: 1.5;
margin-top: 0.1rem;
}
.site-status {
display: inline-flex;
align-items: center;
gap: 0.3rem;
color: var(--success);
font-size: 0.75rem;
font-weight: 600;
margin-top: 0.35rem;
padding: 0.2rem 0.6rem;
background: rgba(74, 222, 128, 0.08);
border-radius: 100px;
}
.live-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1.5rem;
position: relative;
z-index: 1;
}
.live-stat {
text-align: center;
padding: 1.25rem;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius);
}
.live-stat-value {
font-size: 1.75rem;
font-weight: 800;
color: var(--primary-light);
}
.live-stat-label {
color: var(--text-muted);
font-size: 0.8rem;
margin-top: 0.25rem;
}
/* ============================================
PRICING
============================================ */
.pricing-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.5rem;
position: relative;
z-index: 1;
margin-bottom: 3rem;
}
.pricing-card {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: 2rem;
text-align: center;
position: relative;
transition: all var(--transition);
}
.pricing-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-lg);
}
.pricing-card.featured {
border-color: var(--primary);
background: linear-gradient(180deg, rgba(111, 66, 193, 0.08) 0%, var(--bg-card) 50%);
}
.pricing-badge {
position: absolute;
top: -12px;
left: 50%;
transform: translateX(-50%);
padding: 0.25rem 1rem;
background: var(--primary);
color: #fff;
font-size: 0.75rem;
font-weight: 700;
border-radius: 100px;
white-space: nowrap;
}
.pricing-card h3 {
font-size: 1.25rem;
margin-bottom: 0.75rem;
}
.price {
font-size: 2.75rem;
font-weight: 800;
color: var(--primary-light);
line-height: 1;
margin-bottom: 0.25rem;
}
.price-period {
color: var(--text-muted);
font-size: 0.875rem;
margin-bottom: 1.5rem;
}
.pricing-features {
list-style: none;
text-align: left;
margin-bottom: 1.5rem;
}
.pricing-features li {
padding: 0.5rem 0;
border-bottom: 1px solid var(--border-color);
color: var(--text-secondary);
font-size: 0.875rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.pricing-features li::before {
content: '\2713';
color: var(--success);
font-weight: 700;
font-size: 0.85rem;
flex-shrink: 0;
}
.pricing-note {
color: var(--text-muted);
font-size: 0.8rem;
margin-top: 0.75rem;
}
.cost-compare {
text-align: center;
max-width: 560px;
margin: 0 auto;
padding: 2rem;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
position: relative;
z-index: 1;
}
.cost-compare h3 {
margin-bottom: 0.75rem;
}
.cost-compare p {
color: var(--text-secondary);
font-size: 1rem;
margin-bottom: 0.5rem;
}
.cost-compare strong {
color: var(--primary-light);
}
.cost-compare a {
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 0.35rem;
margin-top: 0.5rem;
}
/* ============================================
CTA
============================================ */
.cta-section {
position: relative;
padding: 6rem 0;
text-align: center;
overflow: hidden;
}
.cta-glow {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 500px;
height: 500px;
background: radial-gradient(circle, rgba(111, 66, 193, 0.12) 0%, transparent 70%);
border-radius: 50%;
pointer-events: none;
}
.cta-content {
position: relative;
z-index: 1;
}
.cta-content h2 {
margin-bottom: 1rem;
}
.cta-content > p {
color: var(--text-secondary);
font-size: 1.1rem;
margin-bottom: 2rem;
max-width: 500px;
margin-left: auto;
margin-right: auto;
}
.cta-buttons {
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
margin-bottom: 2rem;
}
.cta-meta {
color: var(--text-muted);
font-size: 0.8rem;
display: flex;
gap: 1.5rem;
justify-content: center;
flex-wrap: wrap;
}
/* ============================================
FOOTER
============================================ */
.footer {
background: var(--bg-surface);
border-top: 1px solid var(--border-color);
padding: 3rem 0 1.5rem;
}
.footer-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 2rem;
margin-bottom: 2.5rem;
}
.footer-section h4 {
font-size: 0.9rem;
font-weight: 700;
margin-bottom: 1rem;
color: var(--text-primary);
}
.footer-links {
list-style: none;
}
.footer-links li {
margin-bottom: 0.5rem;
}
.footer-links a {
color: var(--text-secondary);
font-size: 0.85rem;
}
.footer-links a:hover {
color: var(--primary-light);
}
.footer-bottom {
text-align: center;
padding-top: 1.5rem;
border-top: 1px solid var(--border-color);
color: var(--text-muted);
font-size: 0.8rem;
}
/* ============================================
ANIMATIONS / SCROLL REVEAL
============================================ */
.reveal {
opacity: 0;
transform: translateY(20px);
transition: opacity 0.6s ease, transform 0.6s ease;
}
.reveal.visible {
opacity: 1;
transform: translateY(0);
}
/* Stagger children */
.stagger > * {
opacity: 0;
transform: translateY(16px);
transition: opacity 0.5s ease, transform 0.5s ease;
}
.stagger.visible > * {
opacity: 1;
transform: translateY(0);
}
.stagger.visible > *:nth-child(1) { transition-delay: 0ms; }
.stagger.visible > *:nth-child(2) { transition-delay: 80ms; }
.stagger.visible > *:nth-child(3) { transition-delay: 160ms; }
.stagger.visible > *:nth-child(4) { transition-delay: 240ms; }
.stagger.visible > *:nth-child(5) { transition-delay: 320ms; }
.stagger.visible > *:nth-child(6) { transition-delay: 400ms; }
/* ============================================
RESPONSIVE — 1024px
============================================ */
@media (max-width: 1024px) {
.problems-grid {
grid-template-columns: repeat(2, 1fr);
}
.pricing-grid {
grid-template-columns: repeat(2, 1fr);
}
.pricing-card.featured {
grid-column: 1 / -1;
}
.live-stats {
grid-template-columns: repeat(2, 1fr);
}
}
/* ============================================
RESPONSIVE — 768px
============================================ */
@media (max-width: 768px) {
.nav-links { display: none; }
.nav-right .btn-primary { display: none; }
.nav-right .btn-demo { display: none; }
.hamburger { display: block; }
.logo-tagline { display: none; }
.section { padding: 4rem 0; }
.section-header { text-align: left; }
.section-header p { margin-left: 0; margin-right: 0; }
.branch-header { text-align: left; }
.growing-cta { text-align: left; }
.growing-change-content { text-align: left; }
.narrative-content { text-align: left; }
.branch-narrative { text-align: left; }
.cost-compare { text-align: left; }
.cta-section { text-align: left; }
.cta-content > p { margin-left: 0; margin-right: 0; }
.cta-buttons { justify-content: flex-start; }
.cta-meta { justify-content: flex-start; }
.hero { min-height: auto; padding-top: calc(var(--header-height) + 1.5rem); padding-bottom: 1.5rem; overflow-x: hidden; }
.hero h1 { font-size: 2rem; }
.hero-root-glow { width: 250px; height: 250px; top: 50%; bottom: auto; }
.hero-root-svg { top: 50%; bottom: auto; transform: translate(-50%, -50%); width: 250px; height: 250px; }
.hero-content {
display: flex;
flex-direction: column;
max-width: 100%;
width: 100%;
overflow: hidden;
}
.hero-left { text-align: left; padding-top: 0; }
.hero-badges-row { gap: 0.35rem; }
.beta-pill { font-size: 0.68rem; padding: 0.3rem 0.8rem; }
.hero-badge { font-size: 0.68rem; padding: 0.3rem 0.75rem; }
.hero-right { margin-top: 1.5rem; overflow: hidden; width: 100%; }
.hero-right > div { max-width: 100%; overflow: hidden; width: 100%; }
.hero-showcase { width: 100%; max-width: 100%; }
.hero-terminal { width: 100%; max-width: 100%; box-sizing: border-box; }
.hero-showcase { aspect-ratio: 16 / 10; }
.hero-cta { justify-content: flex-start; }
.hero-pills { justify-content: flex-start; }
.hero-terminal { max-width: 100%; overflow: hidden; }
.hero-terminal-body { padding: 0.5rem 0.7rem; gap: 0.4rem; }
.hero-terminal-prompt { font-size: 0.7rem; }
.hero-terminal-cmd { font-size: 0.65rem; }
.hero-terminal-copy { font-size: 0.62rem; padding: 0.2rem 0.4rem; }
.hero-terminal-copy .copy-label { display: none; }
.hero-rotating-line { text-align: left; min-height: 3.2em; }
.hero-subtitle { margin-left: 0; margin-right: 0; }
.hero-stats {
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
.problems-grid {
grid-template-columns: 1fr;
}
.nodes-grid {
grid-template-columns: 1fr;
}
.branch { padding-left: 0; }
.floating-elements { display: none; }
.sites-grid {
grid-template-columns: 1fr;
}
.site-card.featured {
grid-column: auto;
}
.live-stats {
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
}
.pricing-grid {
grid-template-columns: 1fr;
}
.footer-grid {
grid-template-columns: 1fr;
gap: 1.5rem;
}
.hero-cta {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.6rem;
}
.hero-cta .btn-primary,
.hero-cta .btn-secondary {
justify-content: center;
font-size: 0.82rem;
padding: 0.55rem 0.75rem;
}
.hero-pills { gap: 0.4rem; }
.hero-pill { font-size: 0.68rem; padding: 0.25rem 0.6rem; }
.cta-buttons {
flex-direction: row;
flex-wrap: wrap;
}
/* Simplify tendril SVGs on mobile */
.myc-svg .mobile-hide {
display: none;
}
}
/* ============================================
RESPONSIVE — 480px
============================================ */
@media (max-width: 480px) {
.container { padding: 0 1rem; }
.hero { padding-left: 1rem; padding-right: 1rem; }
.beta-pill { font-size: 0.6rem; padding: 0.25rem 0.65rem; }
.hero-badge { font-size: 0.6rem; padding: 0.25rem 0.6rem; letter-spacing: 0; }
.section { padding: 3rem 0; }
.section-header { margin-bottom: 2.5rem; }
.hero-stats { grid-template-columns: repeat(2, 1fr); padding: 1rem; }
.hero-showcase { aspect-ratio: 16 / 11; }
.showcase-card-title { font-size: 0.85rem; }
.showcase-card-subtitle { font-size: 0.65rem; }
.showcase-card-caption { padding: 1.5rem 1rem 0.75rem; }
.hero-terminal-cmd { font-size: 0.58rem; }
.hero-terminal-prompt { font-size: 0.6rem; }
.hero-terminal-title { font-size: 0.55rem; }
.hero-terminal-dot { width: 6px; height: 6px; }
.live-stats { grid-template-columns: 1fr; }
}
/* ============================================
REDUCED MOTION
============================================ */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
.myc-tendril {
opacity: 1 !important;
stroke-dashoffset: 0 !important;
}
.myc-tendril.thin {
opacity: 0.6 !important;
}
.feature-node, .problem-card, .reveal, .branch {
opacity: 1 !important;
transform: none !important;
}
/* Show root network immediately (scroll animation disabled in JS) */
.root-network-svg .root-line {
opacity: 0.5 !important;
stroke-dashoffset: 0 !important;
}
.root-network-svg .root-node {
opacity: 0.8 !important;
}
.floating-emoji {
animation: none !important;
}
.stagger > * {
opacity: 1 !important;
transform: none !important;
}
}
/* ============================================
FOCUS STYLES (accessibility)
============================================ */
:focus-visible {
outline: 2px solid var(--primary-light);
outline-offset: 2px;
}
.btn-primary:focus-visible, .btn-secondary:focus-visible {
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.4);
}
/* ============================================
FREE ASTERISK MODAL
============================================ */
.free-asterisk {
color: var(--primary-light);
text-decoration: underline;
text-decoration-style: dotted;
text-underline-offset: 3px;
cursor: pointer;
transition: color var(--transition);
}
.free-asterisk:hover {
color: #C084FC;
}
.free-modal-backdrop {
position: fixed;
inset: 0;
z-index: 10000;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
visibility: hidden;
transition: opacity 0.25s ease, visibility 0.25s ease;
}
.free-modal-backdrop.active {
opacity: 1;
visibility: visible;
}
.free-modal {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
max-width: 520px;
width: 90%;
padding: 2rem;
position: relative;
transform: translateY(20px) scale(0.97);
transition: transform 0.25s ease;
}
.free-modal-backdrop.active .free-modal {
transform: translateY(0) scale(1);
}
.free-modal-close {
position: absolute;
top: 1rem;
right: 1rem;
background: none;
border: none;
color: var(--text-muted);
font-size: 1.5rem;
cursor: pointer;
line-height: 1;
padding: 0.25rem;
transition: color var(--transition);
}
.free-modal-close:hover {
color: var(--text-primary);
}
.free-modal h3 {
font-size: 1.25rem;
color: var(--text-primary);
margin-bottom: 0.5rem;
}
.free-modal .free-modal-intro {
color: var(--text-secondary);
font-size: 0.9rem;
margin-bottom: 1.25rem;
line-height: 1.6;
}
.free-modal-list {
list-style: none;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.free-modal-list li {
display: flex;
align-items: flex-start;
gap: 0.75rem;
font-size: 0.9rem;
color: var(--text-secondary);
line-height: 1.5;
}
.free-modal-list .dep-icon {
flex-shrink: 0;
width: 24px;
height: 24px;
border-radius: 6px;
background: var(--myc-node-bg);
border: 1px solid var(--myc-node-border);
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
margin-top: 1px;
}
.free-modal-list strong {
color: var(--text-primary);
font-weight: 600;
}
.free-modal-footer {
margin-top: 1.5rem;
padding-top: 1rem;
border-top: 1px solid var(--border-color);
color: var(--text-muted);
font-size: 0.8rem;
line-height: 1.5;
}
@media (max-width: 480px) {
.free-modal {
max-width: 95%;
padding: 1.5rem;
}
}
</style>
<meta property="og:type" content="website" />
<meta property="og:title" content="Changemaker Lite" />
<meta property="og:description" content="Grow Power. Don't Rent It. Own your digital infrastructure." />
<meta property="og:image" content="https://bnkserve.org/assets/images/social/index.png" />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:url" content="https://bnkserve.org/" />
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:title" content="Changemaker Lite" />
<meta property="twitter:description" content="Grow Power. Don't Rent It. Own your digital infrastructure." />
<meta property="twitter:image" content="https://bnkserve.org/assets/images/social/index.png" />
<meta property="og:type" content="website" />
<meta property="og:title" content="Changemaker Lite" />
<meta property="og:description" content="Build Power. Not Rent It. Own your digital infrastructure." />
<meta property="og:image" content="https://cmlite.org/assets/images/social/index.png" />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:url" content="https://cmlite.org/" />
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:title" content="Changemaker Lite" />
<meta property="twitter:description" content="Build Power. Not Rent It. Own your digital infrastructure." />
<meta property="twitter:image" content="https://cmlite.org/assets/images/social/index.png" />
</head>
<body>
<!-- Global SVG defs — shared glow filter + gradients -->
<svg style="position:absolute;width:0;height:0;overflow:hidden" aria-hidden="true">
<defs>
<linearGradient id="g-hero-1" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="transparent"/>
<stop offset="30%" stop-color="rgba(139,92,246,0.3)"/>
<stop offset="70%" stop-color="rgba(192,132,252,0.2)"/>
<stop offset="100%" stop-color="transparent"/>
</linearGradient>
<linearGradient id="g-hero-2" x1="100%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="transparent"/>
<stop offset="40%" stop-color="rgba(245,169,184,0.25)"/>
<stop offset="60%" stop-color="rgba(139,92,246,0.2)"/>
<stop offset="100%" stop-color="transparent"/>
</linearGradient>
<filter id="glow">
<feGaussianBlur stdDeviation="3" result="blur"/>
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
</filter>
</defs>
</svg>
<!-- ============================================
HEADER
============================================ -->
<header class="header" role="banner">
<nav class="nav-container" aria-label="Main navigation">
<a href="/" class="logo" aria-label="Changemaker Lite home">
<span class="logo-emoji" aria-hidden="true"></span>
Changemaker Lite
<span class="logo-tagline">An alternative network for political action</span>
</a>
<div class="nav-right">
<ul class="nav-links">
<li><a href="#features">Features</a></li>
<li><a href="#live-sites">Live Sites</a></li>
<li><a href="#pricing">Pricing</a></li>
<li><a href="/docs/">Docs</a></li>
</ul>
<button class="theme-toggle" id="theme-toggle" aria-label="Toggle dark/light theme" type="button">
<svg class="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
<circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
</svg>
<svg class="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
</svg>
</button>
<a href="https://app.cmlite.org" class="btn-demo" target="_blank" rel="noopener">Explore Demo</a>
<a href="#get-started" class="btn-primary">Get Started</a>
<button class="hamburger" id="hamburger" aria-label="Open menu" type="button">
<svg class="icon-menu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
<line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/>
</svg>
<svg class="icon-close" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
</nav>
</header>
<!-- Mobile menu -->
<div class="mobile-menu" id="mobile-menu" role="dialog" aria-label="Mobile navigation">
<a href="#features">Features</a>
<a href="#live-sites">Live Sites</a>
<a href="#pricing">Pricing</a>
<a href="/docs/">Documentation</a>
<a href="https://gitea.bnkops.com/admin/changemaker.lite" target="_blank" rel="noopener">Source Code</a>
<a href="https://app.cmlite.org" class="btn-demo" style="text-align:center; margin-top:0.5rem;" target="_blank" rel="noopener">Explore Demo</a>
<a href="#get-started" class="btn-primary" style="text-align:center; margin-top:0.5rem;">Get Started</a>
</div>
<!-- ============================================
HERO
============================================ -->
<section class="hero" id="hero">
<!-- Particle canvas — floating dots -->
<canvas class="hero-particles" id="hero-particles" aria-hidden="true"></canvas>
<!-- Background SVG tendrils -->
<div class="hero-bg">
<div class="hero-root-glow"></div>
<svg class="hero-root-svg" viewBox="0 0 400 400" aria-hidden="true">
<circle cx="200" cy="200" r="40" fill="rgba(139,92,246,0.06)"/>
<circle cx="200" cy="200" r="12" fill="rgba(139,92,246,0.8)"/>
<circle cx="200" cy="200" r="22" fill="none" stroke="rgba(139,92,246,0.45)" stroke-width="2"/>
<circle cx="200" cy="200" r="35" fill="none" stroke="rgba(139,92,246,0.2)" stroke-width="1.5"/>
</svg>
</div>
<div class="hero-content">
<!-- ========== LEFT COLUMN ========== -->
<div class="hero-left">
<div class="hero-badges-row">
<button class="beta-pill" id="beta-pill" type="button">Now in Beta</button>
<div class="hero-badge">Self-Hosted Campaign Infrastructure</div>
</div>
<h1>Grow Power.<br>Don't Rent It.</h1>
<div class="hero-rotating-line">
<span class="tw-static">Your </span><span class="tw-word" id="tw-word"></span><span class="tw-static"> &mdash; on your own infrastructure.</span>
</div>
<p class="hero-subtitle">
No corporate surveillance. No foreign interference. No monthly ransoms.
A <a href="#" class="free-asterisk" id="free-asterisk-link">free*</a> and open source toolkit built for growing political movements.
</p>
<div class="hero-cta">
<a href="https://app.cmlite.org" class="btn-primary" target="_blank" rel="noopener">Explore the Demo <span aria-hidden="true">&rarr;</span></a>
<a href="mailto:cmlite@bnkops.ca?subject=Request%20to%20Chat%20-%20CMLITE&body=Hi%20CMlite%20Team%2C%20I%20would%20like%20to%20chat!%20Please%20send%20me%20a%20email%20back.%20Cheers%2C%20" class="btn-secondary">Schedule a Chat</a>
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/main" class="btn-secondary" target="_blank" rel="noopener">Source Code</a>
<a href="/docs/" class="btn-secondary">Documentation</a>
</div>
<!-- Feature pills -->
<div class="hero-pills" id="hero-pills">
<a class="hero-pill" href="#comm"><span class="pill-icon">&#9993;</span> Advocacy Emails</a>
<a class="hero-pill" href="#map"><span class="pill-icon">&#127758;</span> GPS Canvassing</a>
<a class="hero-pill" href="#content"><span class="pill-icon">&#127909;</span> Video Library</a>
<a class="hero-pill" href="#content"><span class="pill-icon">&#128196;</span> Landing Pages</a>
<a class="hero-pill" href="#comm"><span class="pill-icon">&#128231;</span> Newsletter Sync</a>
<a class="hero-pill" href="#map"><span class="pill-icon">&#128197;</span> Volunteer Shifts</a>
<a class="hero-pill" href="#comm"><span class="pill-icon">&#128172;</span> Team Chat</a>
<a class="hero-pill" href="#sovereignty"><span class="pill-icon">&#128274;</span> Data Sovereignty</a>
</div>
</div>
<!-- ========== RIGHT COLUMN — Feature Showcase ========== -->
<div class="hero-right">
<div>
<div class="hero-showcase" id="hero-showcase">
<!-- Card 1: Campaigns -->
<div class="showcase-card active" data-showcase="0">
<img class="showcase-card-img" src="/assets/images/screenshots/features/influence-campaigns.png" alt="Advocacy campaigns dashboard showing campaign list with status badges, email counts, and response tracking" loading="eager" width="1440" height="900">
<div class="showcase-card-caption">
<div class="showcase-card-icon" style="background:rgba(192,132,252,0.2);color:#C084FC;">&#9993;</div>
<div>
<div class="showcase-card-title">Advocacy Campaigns</div>
<div class="showcase-card-subtitle">Email your representatives in 3 clicks</div>
</div>
</div>
</div>
<!-- Card 2: Canvassing -->
<div class="showcase-card" data-showcase="1">
<img class="showcase-card-img" src="/assets/images/screenshots/features/canvass-dashboard.png" alt="Canvassing dashboard with live volunteer map, cut progress tracking, and activity feed" loading="lazy" width="1440" height="900">
<div class="showcase-card-caption">
<div class="showcase-card-icon" style="background:rgba(52,211,153,0.2);color:#34D399;">&#127758;</div>
<div>
<div class="showcase-card-title">GPS Canvassing</div>
<div class="showcase-card-subtitle">Track door-to-door outreach in real time</div>
</div>
</div>
</div>
<!-- Card 3: Media Library -->
<div class="showcase-card" data-showcase="2">
<img class="showcase-card-img" src="/assets/images/screenshots/features/media-library.png" alt="Media library with video thumbnails, filters, and publishing controls" loading="lazy" width="1440" height="900">
<div class="showcase-card-caption">
<div class="showcase-card-icon" style="background:rgba(251,146,60,0.2);color:#FB923C;">&#127909;</div>
<div>
<div class="showcase-card-title">Media Library</div>
<div class="showcase-card-subtitle">Host, schedule, and share your campaign videos</div>
</div>
</div>
</div>
<!-- Card 4: Volunteer Shifts -->
<div class="showcase-card" data-showcase="3">
<img class="showcase-card-img" src="/assets/images/screenshots/admin/shifts.png" alt="Shift management dashboard with stats, table view, and volunteer signups" loading="lazy" width="1440" height="900">
<div class="showcase-card-caption">
<div class="showcase-card-icon" style="background:rgba(56,189,248,0.2);color:#38BDF8;">&#128197;</div>
<div>
<div class="showcase-card-title">Volunteer Management</div>
<div class="showcase-card-subtitle">Coordinate shifts, track attendance, grow your team</div>
</div>
</div>
</div>
</div>
<div class="showcase-dots" id="showcase-dots">
<span class="showcase-dot active" data-idx="0"></span>
<span class="showcase-dot" data-idx="1"></span>
<span class="showcase-dot" data-idx="2"></span>
<span class="showcase-dot" data-idx="3"></span>
</div>
<!-- Quick deploy terminal -->
<div class="hero-terminal" id="hero-terminal">
<div class="hero-terminal-bar">
<span class="hero-terminal-dot" style="background:#FF5F57;"></span>
<span class="hero-terminal-dot" style="background:#FFBD2E;"></span>
<span class="hero-terminal-dot" style="background:#28C840;"></span>
<span class="hero-terminal-title">Quick Deploy on Linux w/ Docker</span>
</div>
<div class="hero-terminal-body">
<span class="hero-terminal-prompt">$</span>
<span class="hero-terminal-cmd"><span class="cmd-highlight">curl</span> -fsSL gitea.bnkops.com/admin/changemaker.lite/raw/branch/main/scripts/install.sh | <span class="cmd-highlight">bash</span></span>
<button class="hero-terminal-copy" id="hero-copy-btn" type="button" aria-label="Copy install command">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
<span class="copy-label">Copy</span>
</button>
</div>
</div>
</div>
</div>
<!-- ========== BOTTOM — Stats bar spanning both columns ========== -->
<div class="hero-bottom">
<div class="hero-stats" id="hero-stats">
<div class="hero-stat">
<div class="hero-stat-value" data-count="100" data-suffix="%">0%</div>
<div class="hero-stat-label">Data Ownership</div>
</div>
<div class="hero-stat">
<div class="hero-stat-value" data-prefix="$" data-count="0">$0</div>
<div class="hero-stat-label">Self-Hosted</div>
</div>
<div class="hero-stat">
<div class="hero-stat-value" data-count="45" data-suffix="+">0+</div>
<div class="hero-stat-label">Integrated Tools</div>
</div>
<div class="hero-stat">
<div class="hero-stat-value">FOSS</div>
<div class="hero-stat-label">Open Source</div>
</div>
</div>
<div class="hero-trust">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
<circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
</svg>
Self-hosted by community organizers across Canada
</div>
</div>
</div>
</section>
<!-- Beta Modal -->
<div class="beta-modal-backdrop" id="beta-modal-backdrop">
<div class="beta-modal">
<div class="beta-modal-emoji">🚧</div>
<h3>Now in Beta</h3>
<p>Changemaker Lite is under active development and breaking changes are still being pushed. A stable release is expected by <strong>June 2026</strong>.</p>
<button class="beta-modal-close" id="beta-modal-close" type="button">Got it!</button>
</div>
</div>
<!-- ============================================
PROBLEMS
============================================ -->
<section class="section problems" id="problems">
<div class="container">
<div class="section-header reveal">
<h2>Disconnected Roots</h2>
<p>Traditional campaign tools weren't built for the reality of political organizing</p>
</div>
<div class="problems-grid stagger">
<div class="problem-card">
<div class="problem-icon">&#x1F4F1;</div>
<h3>Can't Find Answers Fast</h3>
<p>Voters ask tough questions. Your team fumbles through PDFs, emails, and scattered Google Docs while the voter loses interest.</p>
</div>
<div class="problem-card">
<div class="problem-icon">&#x1F5FA;</div>
<h3>Disconnected Data</h3>
<p>Walk lists in one app, voter info in another, campaign policies somewhere else. Nothing talks to each other.</p>
</div>
<div class="problem-card">
<div class="problem-icon">&#x1F4B8;</div>
<h3>Death by Subscription</h3>
<p>$100 here, $500 there. Thousands monthly on tools that don't work together and hold your data hostage.</p>
</div>
<div class="problem-card">
<div class="problem-icon">&#x1F512;</div>
<h3>No Data Control</h3>
<p>Your strategies in corporate clouds. Your movement's future in someone else's hands. Export? Good luck.</p>
</div>
<div class="problem-card">
<div class="problem-icon">&#x1F4F5;</div>
<h3>Not Mobile-Ready</h3>
<p>Desktop-first tools that barely work on phones. Canvassers struggling with tiny text and broken interfaces in the field.</p>
</div>
<div class="problem-card">
<div class="problem-icon">&#x1F3E2;</div>
<h3>Foreign Dependencies</h3>
<p>US companies with US regulations. Your Canadian campaign data subject to foreign laws and surveillance.</p>
</div>
</div>
</div>
</section>
<!-- ============================================
GROWING CHANGE — Philosophy Bridge
============================================ -->
<section class="section growing-change" id="growing-change">
<div class="container">
<div class="growing-change-content reveal">
<div class="growing-change-card">
<h2>Software Should Grow Power, Not Extract It</h2>
<p class="growing-lead">Most campaign and political software is extractive by nature &mdash; designed to pull information <em>from</em> a community in order to influence politics. Your voter data in corporate clouds. Your strategies readable by foreign jurisdictions. Your movement&rsquo;s future in someone else&rsquo;s hands.</p>
<p>Changemaker asks a different question: <mark>&ldquo;what tools are needed to grow change in a community?&rdquo;</mark> Growing change means making connections between people, providing access to tools that create new opportunities, and deeply understanding the wants and needs of your movement &mdash; on infrastructure <em>you</em> control.</p>
<p class="growing-callout">Organizational independence requires technological independence. Socialist movements will never outspend capital &mdash; but a thousand neighborhood mailing lists has more potential impact than any single organization. Workers, with the right tools, will build the future.</p>
</div>
<div class="growing-pillars stagger">
<div class="growing-pillar">
<div class="pillar-icon">&#x1F91D;</div>
<h4>Distribute Power</h4>
<p>Decentralized organizing is the way out. When knowledge and tools are widely distributed &mdash; not gatekept by leadership or locked behind vendor paywalls &mdash; movements become resilient.</p>
</div>
<div class="growing-pillar">
<div class="pillar-icon">&#x1F512;</div>
<h4>Own Your Secrets</h4>
<p>If you do politics, who is reading your secrets? Corporate platforms extract intelligence systematically. Self-hosted infrastructure means your strategies stay yours &mdash; no algorithmic surveillance, no foreign data laws, no backdoors.</p>
</div>
<div class="growing-pillar">
<div class="pillar-icon">&#x1F331;</div>
<h4>De-Corp Your Stack</h4>
<p>Every subscription to corporate software funds the machine you&rsquo;re fighting. Free and open source tools reduce dependence on capital, eliminate vendor lock-in, and keep your movement&rsquo;s resources where they belong &mdash; in the community.</p>
</div>
</div>
<div class="growing-cta reveal">
<a href="/docs/phil/" class="btn-secondary">Read Our Philosophy <span aria-hidden="true">&rarr;</span></a>
</div>
</div>
</div>
</section>
<!-- ============================================
NARRATIVE BRIDGE — Journey Starts
============================================ -->
<section class="section narrative-bridge" id="journey">
<div class="container">
<div class="narrative-content reveal">
<p class="narrative-eyebrow">The Journey</p>
<h2>What if your entire campaign ran from one place you actually own?</h2>
<p class="narrative-body">
Imagine a new volunteer walks up to your campaign office. Within minutes, they&rsquo;re in your team chat, signed up for a canvassing shift, and synced across your map, newsletter, and event calendar &mdash; <strong>without creating accounts on five different platforms</strong>. That&rsquo;s what an integrated, self-hosted stack makes possible. Below is everything you get from day one.
</p>
</div>
</div>
</section>
<!-- ============================================
FEATURE NETWORK
============================================ -->
<section class="section feature-network" id="features">
<div class="container">
<div class="section-header reveal">
<h2>The Network</h2>
<p>50+ tools connected &mdash; each node strengthens the whole</p>
</div>
<!-- ====== BRANCH 1: Communication ====== -->
<div class="branch branch-comm" data-branch="comm" id="comm">
<div class="branch-header reveal">
<div class="branch-icon comm">&#x1F4E8;</div>
<div class="branch-title">
<h3>Communication</h3>
<p>Email campaigns, SMS outreach, newsletters, advocacy, and team chat</p>
</div>
</div>
<div class="branch-preview reveal">
<img src="/assets/images/screenshots/public/campaigns.png" alt="Public campaigns page with postal code representative lookup and email advocacy" loading="lazy" width="1440" height="900">
<div class="branch-preview-caption"><span class="caption-dot"></span> Public Campaigns — postal code lookup to find representatives and send advocacy emails</div>
</div>
<div class="nodes-grid stagger">
<div class="feature-node" data-screenshot="admin-listmonk">
<div class="node-screenshot"><img src="/assets/images/screenshots/features/admin-listmonk.png" alt="Listmonk newsletter admin" loading="lazy" width="420" height="263"><div class="node-screenshot-label"><span class="screenshot-dot"></span> Newsletter sync dashboard</div></div>
<div class="node-header">
<div class="node-icon">&#x1F4EC;</div>
<h4>Listmonk Newsletters</h4>
</div>
<p>Full newsletter platform with subscriber management, templates, and analytics. Drop-in replacement for Mailchimp.</p>
<div class="node-tags"><span class="node-tag">Unlimited subscribers</span><span class="node-tag">Templates</span><span class="node-tag">Analytics</span></div>
</div>
<div class="feature-node" data-screenshot="influence-campaigns">
<div class="node-screenshot"><img src="/assets/images/screenshots/features/influence-campaigns.png" alt="Advocacy campaign management" loading="lazy" width="420" height="263"><div class="node-screenshot-label"><span class="screenshot-dot"></span> Campaign management with tracking</div></div>
<div class="node-header">
<div class="node-icon">&#x1F3AF;</div>
<h4>Influence Campaigns</h4>
</div>
<p>Postal code to representative lookup. Automated advocacy emails to elected officials with tracking and response collection.</p>
<div class="node-tags"><span class="node-tag">Rep lookup</span><span class="node-tag">BullMQ queue</span><span class="node-tag">Tracking</span></div>
</div>
<div class="feature-node" data-screenshot="email-templates">
<div class="node-screenshot"><img src="/assets/images/screenshots/features/email-templates.png" alt="Email template editor" loading="lazy" width="420" height="263"><div class="node-screenshot-label"><span class="screenshot-dot"></span> Visual email template builder</div></div>
<div class="node-header">
<div class="node-icon">&#x2709;</div>
<h4>Email Templates</h4>
</div>
<p>GrapesJS visual email editor with variable substitution, versioning, and instant preview. Build once, send everywhere.</p>
<div class="node-tags"><span class="node-tag">Visual editor</span><span class="node-tag">Variables</span><span class="node-tag">Versioning</span></div>
</div>
<div class="feature-node" data-screenshot="public-campaigns">
<div class="node-screenshot"><img src="/assets/images/screenshots/features/public-campaigns.png" alt="Public response wall" loading="lazy" width="420" height="263"><div class="node-screenshot-label"><span class="screenshot-dot"></span> Public campaign page with rep lookup</div></div>
<div class="node-header">
<div class="node-icon">&#x1F4AC;</div>
<h4>Response Wall</h4>
</div>
<p>Public response collection with moderation, upvoting, and verification. Showcase supporter voices on your campaigns.</p>
<div class="node-tags"><span class="node-tag">Moderation</span><span class="node-tag">Upvoting</span><span class="node-tag">Verification</span></div>
</div>
<div class="feature-node" data-screenshot="admin-dashboard">
<div class="node-screenshot"><img src="/assets/images/screenshots/features/admin-dashboard.png" alt="Admin dashboard with chat widget" loading="lazy" width="420" height="263"><div class="node-screenshot-label"><span class="screenshot-dot"></span> Team chat integrated into admin</div></div>
<div class="node-header">
<div class="node-icon">&#x1F680;</div>
<h4>Rocket.Chat</h4>
</div>
<p>Self-hosted team chat with SSO integration. Automatic channel notifications for shift signups, canvass sessions, and campaign responses.</p>
<div class="node-tags"><span class="node-tag">SSO</span><span class="node-tag">Channels</span><span class="node-tag">Slack alternative</span></div>
</div>
<div class="feature-node" data-screenshot="email-queue">
<div class="node-screenshot"><img src="/assets/images/screenshots/features/email-queue.png" alt="Email queue dashboard" loading="lazy" width="420" height="263"><div class="node-screenshot-label"><span class="screenshot-dot"></span> Async email queue with status tracking</div></div>
<div class="node-header">
<div class="node-icon">&#x1F514;</div>
<h4>Smart Notifications</h4>
</div>
<p>Async notification queue for admin alerts and volunteer feedback. Shift reminders, session summaries, and signup confirmations.</p>
<div class="node-tags"><span class="node-tag">BullMQ</span><span class="node-tag">Reminders</span><span class="node-tag">Summaries</span></div>
</div>
<div class="feature-node" data-screenshot="sms-dashboard">
<div class="node-screenshot"><img src="/assets/images/screenshots/features/sms-dashboard.png" alt="SMS campaign dashboard" loading="lazy" width="420" height="263"><div class="node-screenshot-label"><span class="screenshot-dot"></span> SMS outreach via Termux bridge</div></div>
<div class="node-header">
<div class="node-icon">&#x1F4F1;</div>
<h4>SMS Campaigns</h4>
</div>
<p>Text message outreach via Termux Android bridge. Contact lists, templated campaigns, delivery tracking, response sync, and device health monitoring.</p>
<div class="node-tags"><span class="node-tag">Termux bridge</span><span class="node-tag">BullMQ queue</span><span class="node-tag">Response sync</span></div>
</div>
<div class="feature-node" data-screenshot="admin-dashboard">
<div class="node-screenshot"><img src="/assets/images/screenshots/features/admin-dashboard.png" alt="Admin with floating chat widget" loading="lazy" width="420" height="263"><div class="node-screenshot-label"><span class="screenshot-dot"></span> Floating chat panel in admin</div></div>
<div class="node-header">
<div class="node-icon">&#x1F5E8;</div>
<h4>Chat Widget</h4>
</div>
<p>Floating Rocket.Chat panel for logged-in team members. Minimizable FAB, auth-gated access, and settings-toggleable visibility across the admin interface.</p>
<div class="node-tags"><span class="node-tag">Rocket.Chat</span><span class="node-tag">Auth-gated</span><span class="node-tag">Floating</span></div>
</div>
</div>
</div>
<!-- ====== BRANCH 2: Mapping & Canvassing ====== -->
<div class="branch branch-map" data-branch="map" id="map">
<div class="branch-header reveal">
<div class="branch-icon map">&#x1F5FA;</div>
<div class="branch-title">
<h3>Mapping &amp; Canvassing</h3>
<p>GPS tracking, door-to-door canvassing, geographic organization</p>
</div>
</div>
<div class="branch-preview reveal">
<img src="/assets/images/screenshots/public/map.png" alt="Public interactive map showing Edmonton with canvassing territory overlays and support level legend" loading="lazy" width="1440" height="900">
<div class="branch-preview-caption"><span class="caption-dot"></span> Public Map — interactive Leaflet map with territory cuts, marker clustering, and support levels</div>
</div>
<div class="nodes-grid stagger">
<div class="feature-node" data-screenshot="public-map">
<div class="node-screenshot"><img src="/assets/images/screenshots/features/public-map.png" alt="Interactive Leaflet map" loading="lazy" width="420" height="263"><div class="node-screenshot-label"><span class="screenshot-dot"></span> Full-screen map with cuts and support levels</div></div>
<div class="node-header">
<div class="node-icon">&#x1F30D;</div>
<h4>Interactive Map</h4>
</div>
<p>Leaflet-powered map with multi-provider geocoding, color-coded markers, cuts overlay, and fullscreen mode.</p>
<div class="node-tags"><span class="node-tag">6 geocode providers</span><span class="node-tag">Leaflet</span><span class="node-tag">Clustering</span></div>
</div>
<div class="feature-node" data-screenshot="volunteer-dashboard">
<div class="node-screenshot"><img src="/assets/images/screenshots/features/volunteer-dashboard.png" alt="GPS canvassing map" loading="lazy" width="420" height="263"><div class="node-screenshot-label"><span class="screenshot-dot"></span> Mobile canvass map with GPS tracking</div></div>
<div class="node-header">
<div class="node-icon">&#x1F6B6;</div>
<h4>GPS Canvassing</h4>
</div>
<p>Full-screen mobile canvass map with real-time GPS, walking route algorithm, visit recording, and outcome tracking.</p>
<div class="node-tags"><span class="node-tag">GPS tracking</span><span class="node-tag">Walking routes</span><span class="node-tag">Mobile-first</span></div>
</div>
<div class="feature-node" data-screenshot="admin-cuts">
<div class="node-screenshot"><img src="/assets/images/screenshots/features/admin-cuts.png" alt="Polygon cuts editor" loading="lazy" width="420" height="263"><div class="node-screenshot-label"><span class="screenshot-dot"></span> Territory cut management</div></div>
<div class="node-header">
<div class="node-icon">&#x2702;</div>
<h4>Polygon Cuts</h4>
</div>
<p>Draw geographic boundaries on the map. Assign locations to cuts for organized canvassing territories.</p>
<div class="node-tags"><span class="node-tag">Drawing mode</span><span class="node-tag">Point-in-polygon</span><span class="node-tag">Spatial queries</span></div>
</div>
<div class="feature-node" data-screenshot="public-shifts">
<div class="node-screenshot"><img src="/assets/images/screenshots/features/public-shifts.png" alt="Public volunteer shifts" loading="lazy" width="420" height="263"><div class="node-screenshot-label"><span class="screenshot-dot"></span> Public shift signup with capacity tracking</div></div>
<div class="node-header">
<div class="node-icon">&#x1F4C5;</div>
<h4>Volunteer Shifts</h4>
</div>
<p>Shift scheduling with public signup, confirmation emails, cut assignment, and capacity management.</p>
<div class="node-tags"><span class="node-tag">Public signup</span><span class="node-tag">Email confirm</span><span class="node-tag">Cut assignment</span></div>
</div>
<div class="feature-node" data-screenshot="data-quality">
<div class="node-screenshot"><img src="/assets/images/screenshots/features/data-quality.png" alt="Walk sheets and data quality" loading="lazy" width="420" height="263"><div class="node-screenshot-label"><span class="screenshot-dot"></span> Data quality dashboard for canvassing</div></div>
<div class="node-header">
<div class="node-icon">&#x1F4CB;</div>
<h4>Walk Sheets</h4>
</div>
<p>Printable walk sheet forms with QR codes for each cut. Take the field data offline with printed reports.</p>
<div class="node-tags"><span class="node-tag">QR codes</span><span class="node-tag">Printable</span><span class="node-tag">Cut reports</span></div>
</div>
<div class="feature-node" data-screenshot="admin-locations">
<div class="node-screenshot"><img src="/assets/images/screenshots/features/admin-locations.png" alt="NAR import and locations" loading="lazy" width="420" height="263"><div class="node-screenshot-label"><span class="screenshot-dot"></span> Location management with NAR import</div></div>
<div class="node-header">
<div class="node-icon">&#x1F1E8;&#x1F1E6;</div>
<h4>NAR Import</h4>
</div>
<p>Import Canadian National Address Register data with province/city/postal filtering, coordinate projection, and streaming.</p>
<div class="node-tags"><span class="node-tag">2025 format</span><span class="node-tag">Proj4</span><span class="node-tag">Streaming</span></div>
</div>
<div class="feature-node" data-screenshot="public-events">
<div class="node-screenshot"><img src="/assets/images/screenshots/features/public-events.png" alt="Gancio event calendar" loading="lazy" width="420" height="263"><div class="node-screenshot-label"><span class="screenshot-dot"></span> Community calendar with shift sync</div></div>
<div class="node-header">
<div class="node-icon">&#x1F4C6;</div>
<h4>Gancio Events</h4>
</div>
<p>Public event calendar synced from shifts via Gancio. OAuth integration, map markers for upcoming events, and embeddable GrapesJS block.</p>
<div class="node-tags"><span class="node-tag">OAuth sync</span><span class="node-tag">Map markers</span><span class="node-tag">Embeddable</span></div>
</div>
</div>
</div>
<!-- Narrative connector -->
<div class="branch-narrative reveal">
<p>Your message is only as strong as the channels that carry it. Once your team is connected and your territory mapped, you need to <strong>tell your story</strong> &mdash; through video, documentation, landing pages, and more.</p>
</div>
<!-- ====== BRANCH 3: Content & Media ====== -->
<div class="branch branch-content" data-branch="content" id="content">
<div class="branch-header reveal">
<div class="branch-icon content">&#x1F3AC;</div>
<div class="branch-title">
<h3>Content &amp; Media</h3>
<p>Video, photos, playlists, page builder, documentation, and web IDE</p>
</div>
</div>
<div class="branch-preview reveal">
<img src="/assets/images/screenshots/public/gallery.png" alt="Public media gallery with video cards, sidebar navigation, chat, and content categories" loading="lazy" width="1440" height="900">
<div class="branch-preview-caption"><span class="caption-dot"></span> Media Gallery — browse videos, shorts, photos, and playlists with live chat and reactions</div>
</div>
<div class="nodes-grid stagger">
<div class="feature-node" data-screenshot="media-library">
<div class="node-screenshot"><img src="/assets/images/screenshots/features/media-library.png" alt="Video library" loading="lazy" width="420" height="263"><div class="node-screenshot-label"><span class="screenshot-dot"></span> Video management with metadata</div></div>
<div class="node-header">
<div class="node-icon">&#x1F4F9;</div>
<h4>Video Library</h4>
</div>
<p>Upload and manage videos with FFprobe metadata, scheduled publishing, view analytics, emoji reactions, threaded comments, and live chat.</p>
<div class="node-tags"><span class="node-tag">Analytics</span><span class="node-tag">Live chat</span><span class="node-tag">Scheduling</span></div>
</div>
<div class="feature-node" data-screenshot="landing-pages">
<div class="node-screenshot"><img src="/assets/images/screenshots/features/landing-pages.png" alt="Landing page builder" loading="lazy" width="420" height="263"><div class="node-screenshot-label"><span class="screenshot-dot"></span> GrapesJS drag-and-drop page builder</div></div>
<div class="node-header">
<div class="node-icon">&#x1F3A8;</div>
<h4>Landing Page Builder</h4>
</div>
<p>GrapesJS drag-and-drop page editor with block library, custom components, and instant public publishing at /p/slug.</p>
<div class="node-tags"><span class="node-tag">Drag &amp; drop</span><span class="node-tag">Block library</span><span class="node-tag">Instant publish</span></div>
</div>
<div class="feature-node" data-screenshot="mkdocs">
<div class="node-screenshot"><img src="/assets/images/screenshots/features/mkdocs.png" alt="MkDocs documentation site" loading="lazy" width="420" height="263"><div class="node-screenshot-label"><span class="screenshot-dot"></span> Material-themed documentation site</div></div>
<div class="node-header">
<div class="node-icon">&#x1F4D6;</div>
<h4>MkDocs Documentation</h4>
</div>
<p>Material-themed docs with full-text search, blog, social cards, and Gitea-backed page comments with anonymous posting and moderation.</p>
<div class="node-tags"><span class="node-tag">Material theme</span><span class="node-tag">Comments</span><span class="node-tag">Blog</span></div>
</div>
<div class="feature-node" data-screenshot="code-server">
<div class="node-screenshot"><img src="/assets/images/screenshots/features/code-server.png" alt="Code Server IDE" loading="lazy" width="420" height="263"><div class="node-screenshot-label"><span class="screenshot-dot"></span> VS Code in the browser</div></div>
<div class="node-header">
<div class="node-icon">&#x1F4BB;</div>
<h4>Code Server</h4>
</div>
<p>Full VS Code in the browser. Edit configuration, templates, and code from anywhere without SSH.</p>
<div class="node-tags"><span class="node-tag">VS Code</span><span class="node-tag">Browser IDE</span><span class="node-tag">Extensions</span></div>
</div>
<div class="feature-node" data-screenshot="excalidraw">
<div class="node-screenshot"><img src="/assets/images/screenshots/features/excalidraw.png" alt="Excalidraw whiteboard" loading="lazy" width="420" height="263"><div class="node-screenshot-label"><span class="screenshot-dot"></span> Collaborative whiteboard</div></div>
<div class="node-header">
<div class="node-icon">&#x270F;</div>
<h4>Excalidraw Whiteboard</h4>
</div>
<p>Collaborative diagramming and whiteboard tool. Plan canvassing routes, sketch campaign strategies, and brainstorm as a team.</p>
<div class="node-tags"><span class="node-tag">Collaborative</span><span class="node-tag">Diagrams</span><span class="node-tag">Real-time</span></div>
</div>
<div class="feature-node" data-screenshot="media-analytics">
<div class="node-screenshot"><img src="/assets/images/screenshots/features/media-analytics.png" alt="Media analytics" loading="lazy" width="420" height="263"><div class="node-screenshot-label"><span class="screenshot-dot"></span> Engagement analytics dashboard</div></div>
<div class="node-header">
<div class="node-icon">&#x1F4F7;</div>
<h4>Photo Management</h4>
</div>
<p>Album organization with bulk uploads, metadata extraction, and engagement tracking. Reactions, comments, and a public photo gallery.</p>
<div class="node-tags"><span class="node-tag">Albums</span><span class="node-tag">Engagement</span><span class="node-tag">Gallery</span></div>
</div>
<div class="feature-node" data-screenshot="public-gallery">
<div class="node-screenshot"><img src="/assets/images/screenshots/features/public-gallery.png" alt="Public media gallery" loading="lazy" width="420" height="263"><div class="node-screenshot-label"><span class="screenshot-dot"></span> Public gallery with categories</div></div>
<div class="node-header">
<div class="node-icon">&#x1F3B5;</div>
<h4>Playlists</h4>
</div>
<p>Curated video collections with admin, user, and public playlists. Drag-reorder, sidebar navigation, featured carousel, and dedicated viewer page.</p>
<div class="node-tags"><span class="node-tag">Curated</span><span class="node-tag">Public/Private</span><span class="node-tag">Reorderable</span></div>
</div>
<div class="feature-node" data-screenshot="public-gallery">
<div class="node-screenshot"><img src="/assets/images/screenshots/features/public-gallery.png" alt="Shorts video feed" loading="lazy" width="420" height="263"><div class="node-screenshot-label"><span class="screenshot-dot"></span> Video gallery with live chat</div></div>
<div class="node-header">
<div class="node-icon">&#x1F4F2;</div>
<h4>Shorts Feed</h4>
</div>
<p>TikTok-style vertical video feed for clips under 60 seconds. Autoplay, sorting modes, and mobile-optimized swipeable interface.</p>
<div class="node-tags"><span class="node-tag">Vertical video</span><span class="node-tag">Autoplay</span><span class="node-tag">Mobile-first</span></div>
</div>
</div>
</div>
<!-- ====== BRANCH 4: Data & Automation ====== -->
<div class="branch branch-data" data-branch="data" id="data">
<div class="branch-header reveal">
<div class="branch-icon data">&#x1F4CA;</div>
<div class="branch-title">
<h3>Data &amp; Automation</h3>
<p>Database browsing, workflow automation, version control, search, and utilities</p>
</div>
</div>
<div class="branch-preview reveal">
<img src="/assets/images/screenshots/public/home.png" alt="Public homepage with hero section, upcoming shifts, latest videos, and recent activity feed" loading="lazy" width="1440" height="900">
<div class="branch-preview-caption"><span class="caption-dot"></span> Public Homepage — hero section, upcoming shifts, latest videos, and activity feed</div>
</div>
<div class="nodes-grid stagger">
<div class="feature-node" data-screenshot="nocodb">
<div class="node-screenshot"><img src="/assets/images/screenshots/features/nocodb.png" alt="NocoDB data browser" loading="lazy" width="420" height="263"><div class="node-screenshot-label"><span class="screenshot-dot"></span> Airtable-alternative data browser</div></div>
<div class="node-header">
<div class="node-icon">&#x1F5C4;</div>
<h4>NocoDB</h4>
</div>
<p>Airtable-alternative database browser. Browse, filter, and export your campaign data through a spreadsheet-like interface.</p>
<div class="node-tags"><span class="node-tag">Read-only</span><span class="node-tag">Filters</span><span class="node-tag">Export</span></div>
</div>
<div class="feature-node" data-screenshot="n8n">
<div class="node-screenshot"><img src="/assets/images/screenshots/features/n8n.png" alt="n8n workflow automation" loading="lazy" width="420" height="263"><div class="node-screenshot-label"><span class="screenshot-dot"></span> Visual workflow automation</div></div>
<div class="node-header">
<div class="node-icon">&#x26A1;</div>
<h4>n8n Workflows</h4>
</div>
<p>Visual workflow automation. Connect APIs, trigger actions, and build custom integrations without code.</p>
<div class="node-tags"><span class="node-tag">Visual builder</span><span class="node-tag">400+ integrations</span><span class="node-tag">Webhooks</span></div>
</div>
<div class="feature-node" data-screenshot="gitea">
<div class="node-screenshot"><img src="/assets/images/screenshots/features/gitea.png" alt="Gitea Git hosting" loading="lazy" width="420" height="263"><div class="node-screenshot-label"><span class="screenshot-dot"></span> Self-hosted Git with CI/CD</div></div>
<div class="node-header">
<div class="node-icon">&#x1F4E6;</div>
<h4>Gitea</h4>
</div>
<p>Self-hosted Git repository. Version control for your campaign code, configs, and documentation.</p>
<div class="node-tags"><span class="node-tag">Git hosting</span><span class="node-tag">Issues</span><span class="node-tag">CI/CD</span></div>
</div>
<div class="feature-node" data-screenshot="miniqr">
<div class="node-screenshot"><img src="/assets/images/screenshots/features/miniqr.png" alt="Mini QR generator" loading="lazy" width="420" height="263"><div class="node-screenshot-label"><span class="screenshot-dot"></span> QR code generator</div></div>
<div class="node-header">
<div class="node-icon">&#x1F4F1;</div>
<h4>Mini QR</h4>
</div>
<p>QR code generator for walk sheets, campaign materials, and event signage. Instant PNG generation.</p>
<div class="node-tags"><span class="node-tag">PNG output</span><span class="node-tag">Embeddable</span><span class="node-tag">Public API</span></div>
</div>
<div class="feature-node" data-screenshot="admin-dashboard">
<div class="node-screenshot"><img src="/assets/images/screenshots/features/admin-dashboard.png" alt="Command palette search" loading="lazy" width="420" height="263"><div class="node-screenshot-label"><span class="screenshot-dot"></span> Ctrl+K global command palette</div></div>
<div class="node-header">
<div class="node-icon">&#x2318;</div>
<h4>Command Palette</h4>
</div>
<p>Global Ctrl+K search across pages, campaigns, locations, users, and settings. Fuzzy matching, recent items, and keyboard-driven navigation.</p>
<div class="node-tags"><span class="node-tag">Ctrl+K</span><span class="node-tag">Fuzzy search</span><span class="node-tag">Keyboard-first</span></div>
</div>
<div class="feature-node" data-screenshot="admin-settings">
<div class="node-screenshot"><img src="/assets/images/screenshots/features/admin-settings.png" alt="Navigation settings" loading="lazy" width="420" height="263"><div class="node-screenshot-label"><span class="screenshot-dot"></span> Organization and nav settings</div></div>
<div class="node-header">
<div class="node-icon">&#x2699;</div>
<h4>Navigation Settings</h4>
</div>
<p>Customizable public nav menu with feature toggles, custom external links, drag-reorder, and real-time preview. Control what visitors see.</p>
<div class="node-tags"><span class="node-tag">Drag-reorder</span><span class="node-tag">Feature flags</span><span class="node-tag">Custom links</span></div>
</div>
</div>
</div>
<!-- ====== BRANCH 5: DevOps & Security ====== -->
<div class="branch branch-devops" data-branch="devops" id="devops">
<div class="branch-header reveal">
<div class="branch-icon devops">&#x1F6E1;</div>
<div class="branch-title">
<h3>DevOps &amp; Security</h3>
<p>Tunnel management, monitoring, security hardening, and backups</p>
</div>
</div>
<div class="branch-preview reveal">
<img src="/assets/images/screenshots/admin/monitoring.png" alt="Observability dashboard with Prometheus metrics, Grafana dashboards, and service health" loading="lazy" width="1440" height="900">
<div class="branch-preview-caption"><span class="caption-dot"></span> Observability Dashboard — Prometheus metrics, Grafana dashboards, and alert management</div>
</div>
<div class="nodes-grid stagger">
<div class="feature-node" data-screenshot="pangolin-tunnel">
<div class="node-screenshot"><img src="/assets/images/screenshots/features/pangolin-tunnel.png" alt="Pangolin tunnel management" loading="lazy" width="420" height="263"><div class="node-screenshot-label"><span class="screenshot-dot"></span> Tunnel management dashboard</div></div>
<div class="node-header">
<div class="node-icon">&#x1F310;</div>
<h4>Pangolin Tunnel</h4>
</div>
<p>Expose your self-hosted services to the internet without port forwarding. Newt container integration with automatic SSL.</p>
<div class="node-tags"><span class="node-tag">No port-forward</span><span class="node-tag">Auto SSL</span><span class="node-tag">Newt</span></div>
</div>
<div class="feature-node" data-screenshot="admin-observability">
<div class="node-screenshot"><img src="/assets/images/screenshots/features/admin-observability.png" alt="Observability dashboard" loading="lazy" width="420" height="263"><div class="node-screenshot-label"><span class="screenshot-dot"></span> Prometheus + Grafana monitoring</div></div>
<div class="node-header">
<div class="node-icon">&#x1F4C8;</div>
<h4>Prometheus + Grafana</h4>
</div>
<p>12 custom metrics, 3 dashboards, alert rules, and service health monitoring. Full observability stack.</p>
<div class="node-tags"><span class="node-tag">12 metrics</span><span class="node-tag">3 dashboards</span><span class="node-tag">Alerts</span></div>
</div>
<div class="feature-node" data-screenshot="admin-users">
<div class="node-screenshot"><img src="/assets/images/screenshots/features/admin-users.png" alt="User management with RBAC" loading="lazy" width="420" height="263"><div class="node-screenshot-label"><span class="screenshot-dot"></span> Role-based access control</div></div>
<div class="node-header">
<div class="node-icon">&#x1F50F;</div>
<h4>Security Hardened</h4>
</div>
<p>13-finding security audit addressed. JWT rotation, rate limiting, XSS prevention, encrypted secrets, HSTS headers.</p>
<div class="node-tags"><span class="node-tag">Audit complete</span><span class="node-tag">RBAC</span><span class="node-tag">Encryption</span></div>
</div>
<div class="feature-node" data-screenshot="mailhog">
<div class="node-screenshot"><img src="/assets/images/screenshots/features/mailhog.png" alt="MailHog email capture" loading="lazy" width="420" height="263"><div class="node-screenshot-label"><span class="screenshot-dot"></span> Email testing with MailHog</div></div>
<div class="node-header">
<div class="node-icon">&#x1F4BE;</div>
<h4>Automated Backups</h4>
</div>
<p>PostgreSQL dumps, Listmonk data, uploads archive, and optional S3 upload. One-command backup script.</p>
<div class="node-tags"><span class="node-tag">PostgreSQL</span><span class="node-tag">S3 optional</span><span class="node-tag">Scripted</span></div>
</div>
<div class="feature-node" data-screenshot="admin-settings">
<div class="node-screenshot"><img src="/assets/images/screenshots/features/admin-settings.png" alt="Vaultwarden password manager" loading="lazy" width="420" height="263"><div class="node-screenshot-label"><span class="screenshot-dot"></span> Organization security settings</div></div>
<div class="node-header">
<div class="node-icon">&#x1F510;</div>
<h4>Vaultwarden</h4>
</div>
<p>Self-hosted Bitwarden-compatible password manager. Secure credential sharing for your team with real-time sync and browser extensions.</p>
<div class="node-tags"><span class="node-tag">Bitwarden</span><span class="node-tag">Team sharing</span><span class="node-tag">Encrypted</span></div>
</div>
<div class="feature-node" data-screenshot="admin-users">
<div class="node-screenshot"><img src="/assets/images/screenshots/features/admin-users.png" alt="User provisioning" loading="lazy" width="420" height="263"><div class="node-screenshot-label"><span class="screenshot-dot"></span> Multi-service user provisioning</div></div>
<div class="node-header">
<div class="node-icon">&#x1F465;</div>
<h4>User Provisioning</h4>
</div>
<p>Automatic account sync across Rocket.Chat, Gitea, Vaultwarden, and Listmonk. Eager or lazy strategies with per-user status tracking and bulk sync.</p>
<div class="node-tags"><span class="node-tag">4 services</span><span class="node-tag">Auto-sync</span><span class="node-tag">Lifecycle hooks</span></div>
</div>
</div>
</div>
<!-- Narrative connector -->
<div class="branch-narrative reveal">
<p>A movement that can&rsquo;t sustain itself financially will always depend on someone else&rsquo;s goodwill. <strong>Own your revenue</strong> the same way you own your data &mdash; with tools that put every dollar directly into your cause.</p>
</div>
<!-- ====== BRANCH 6: Fundraising & Commerce ====== -->
<div class="branch branch-fundraising" data-branch="fundraising" id="fundraising">
<div class="branch-header reveal">
<div class="branch-icon fundraising">&#x1F4B3;</div>
<div class="branch-title">
<h3>Fundraising &amp; Commerce</h3>
<p>Donations, subscriptions, product sales, and supporter monetization</p>
</div>
</div>
<div class="branch-preview reveal">
<img src="/assets/images/screenshots/public/pricing.png" alt="Subscription pricing page with free and pro plan cards, monthly/yearly toggle, and feature lists" loading="lazy" width="1440" height="900">
<div class="branch-preview-caption"><span class="caption-dot"></span> Pricing Plans — subscription tiers with monthly/yearly billing and feature comparison</div>
</div>
<div class="nodes-grid stagger">
<div class="feature-node" data-screenshot="public-donations">
<div class="node-screenshot"><img src="/assets/images/screenshots/features/public-donations.png" alt="Donation platform" loading="lazy" width="420" height="263"><div class="node-screenshot-label"><span class="screenshot-dot"></span> Public donation page</div></div>
<div class="node-header">
<div class="node-icon">&#x1F4B0;</div>
<h4>Donation Platform</h4>
</div>
<p>Accept one-time donations with configurable suggested amounts, anonymous giving, and automatic tax receipts via email.</p>
<div class="node-tags"><span class="node-tag">Stripe</span><span class="node-tag">Anonymous</span><span class="node-tag">Receipts</span></div>
</div>
<div class="feature-node" data-screenshot="subscription-plans">
<div class="node-screenshot"><img src="/assets/images/screenshots/features/subscription-plans.png" alt="Subscription plan management" loading="lazy" width="420" height="263"><div class="node-screenshot-label"><span class="screenshot-dot"></span> Plan management dashboard</div></div>
<div class="node-header">
<div class="node-icon">&#x1F504;</div>
<h4>Subscription Plans</h4>
</div>
<p>Recurring revenue with tiered plans, monthly and yearly billing, and automatic renewal management. Replace Patreon.</p>
<div class="node-tags"><span class="node-tag">Recurring</span><span class="node-tag">Tiers</span><span class="node-tag">MRR tracking</span></div>
</div>
<div class="feature-node" data-screenshot="public-shop">
<div class="node-screenshot"><img src="/assets/images/screenshots/features/public-shop.png" alt="Product shop" loading="lazy" width="420" height="263"><div class="node-screenshot-label"><span class="screenshot-dot"></span> Public product storefront</div></div>
<div class="node-header">
<div class="node-icon">&#x1F6D2;</div>
<h4>Product Shop</h4>
</div>
<p>Sell digital products, event tickets, and merchandise. Inventory management, download delivery, and capacity limits.</p>
<div class="node-tags"><span class="node-tag">Digital goods</span><span class="node-tag">Events</span><span class="node-tag">Inventory</span></div>
</div>
<div class="feature-node" data-screenshot="payments-dashboard">
<div class="node-screenshot"><img src="/assets/images/screenshots/features/payments-dashboard.png" alt="Payment analytics" loading="lazy" width="420" height="263"><div class="node-screenshot-label"><span class="screenshot-dot"></span> Revenue analytics dashboard</div></div>
<div class="node-header">
<div class="node-icon">&#x1F4CA;</div>
<h4>Payment Dashboard</h4>
</div>
<p>Revenue analytics with subscriber counts, MRR tracking, donation history, and CSV exports for accounting.</p>
<div class="node-tags"><span class="node-tag">Analytics</span><span class="node-tag">CSV export</span><span class="node-tag">Refunds</span></div>
</div>
<div class="feature-node" data-screenshot="gallery-ads">
<div class="node-screenshot"><img src="/assets/images/screenshots/features/gallery-ads.png" alt="Gallery ad management" loading="lazy" width="420" height="263"><div class="node-screenshot-label"><span class="screenshot-dot"></span> In-gallery promotion manager</div></div>
<div class="node-header">
<div class="node-icon">&#x1F4E2;</div>
<h4>Gallery Ads</h4>
</div>
<p>Promote donations, products, and subscriptions within the media gallery. Visibility targeting, scheduling, and click analytics.</p>
<div class="node-tags"><span class="node-tag">Targeting</span><span class="node-tag">Scheduling</span><span class="node-tag">CTR tracking</span></div>
</div>
<div class="feature-node" data-screenshot="donation-pages">
<div class="node-screenshot"><img src="/assets/images/screenshots/features/donation-pages.png" alt="Donation page builder" loading="lazy" width="420" height="263"><div class="node-screenshot-label"><span class="screenshot-dot"></span> Custom branded donation pages</div></div>
<div class="node-header">
<div class="node-icon">&#x1F4DD;</div>
<h4>Donation Pages</h4>
</div>
<p>Custom branded donation pages with configurable amounts, thank-you messages, and public slugs. Multiple campaigns with independent branding and goals.</p>
<div class="node-tags"><span class="node-tag">Custom branding</span><span class="node-tag">Slug URLs</span><span class="node-tag">Goals</span></div>
</div>
</div>
</div>
<!-- ====== BRANCH 7: Social & Community ====== -->
<div class="branch branch-social" data-branch="social" id="social">
<div class="branch-header reveal">
<div class="branch-icon social">&#x1F465;</div>
<div class="branch-title">
<h3>Social &amp; Community</h3>
<p>Friendships, activity feeds, achievements, groups, reactions, and real-time notifications</p>
</div>
</div>
<div class="branch-preview reveal">
<img src="/assets/images/screenshots/public/wall-of-fame.png" alt="Wall of Fame with volunteer spotlight, leaderboard tabs for canvass, shifts, and campaigns" loading="lazy" width="1440" height="900">
<div class="branch-preview-caption"><span class="caption-dot"></span> Wall of Fame — volunteer spotlight, achievement leaderboards, and community recognition</div>
</div>
<div class="nodes-grid stagger">
<div class="feature-node" data-screenshot="volunteer-friends">
<div class="node-screenshot"><img src="/assets/images/screenshots/features/volunteer-friends.png" alt="Friend system" loading="lazy" width="420" height="263"><div class="node-screenshot-label"><span class="screenshot-dot"></span> Volunteer friend connections</div></div>
<div class="node-header">
<div class="node-icon">&#x1F465;</div>
<h4>Friend System</h4>
</div>
<p>Friend requests, suggestions, pokes, cross-module badges on campaigns, shifts, and the map.</p>
<div class="node-tags"><span class="node-tag">Friend requests</span><span class="node-tag">Suggestions</span><span class="node-tag">Poke</span></div>
</div>
<div class="feature-node" data-screenshot="public-home">
<div class="node-screenshot"><img src="/assets/images/screenshots/features/public-home.png" alt="Activity feed" loading="lazy" width="420" height="263"><div class="node-screenshot-label"><span class="screenshot-dot"></span> Public homepage with activity</div></div>
<div class="node-header">
<div class="node-icon">&#x1F4F0;</div>
<h4>Activity Feed</h4>
</div>
<p>Real-time SSE feed of friend activity across campaigns, shifts, canvassing, and responses.</p>
<div class="node-tags"><span class="node-tag">Real-time</span><span class="node-tag">SSE</span><span class="node-tag">Cross-module</span></div>
</div>
<div class="feature-node" data-screenshot="public-wall-of-fame">
<div class="node-screenshot"><img src="/assets/images/screenshots/features/public-wall-of-fame.png" alt="Achievements and leaderboard" loading="lazy" width="420" height="263"><div class="node-screenshot-label"><span class="screenshot-dot"></span> Volunteer achievements leaderboard</div></div>
<div class="node-header">
<div class="node-icon">&#x1F3C6;</div>
<h4>Achievements &amp; Notifications</h4>
</div>
<p>Milestone badges, real-time notification bell with friend requests, pokes, comments, and alerts.</p>
<div class="node-tags"><span class="node-tag">Badges</span><span class="node-tag">Bell UI</span><span class="node-tag">Real-time</span></div>
</div>
<div class="feature-node" data-screenshot="volunteer-calendar">
<div class="node-screenshot"><img src="/assets/images/screenshots/features/volunteer-calendar.png" alt="Team calendar" loading="lazy" width="420" height="263"><div class="node-screenshot-label"><span class="screenshot-dot"></span> Personal calendar with layers</div></div>
<div class="node-header">
<div class="node-icon">&#x1F46B;</div>
<h4>Groups &amp; Teams</h4>
</div>
<p>Auto-groups for shift teams and campaign crews, custom groups, and shared updates.</p>
<div class="node-tags"><span class="node-tag">Shift teams</span><span class="node-tag">Campaign crews</span><span class="node-tag">Custom groups</span></div>
</div>
<div class="feature-node" data-screenshot="public-gallery">
<div class="node-screenshot"><img src="/assets/images/screenshots/features/public-gallery.png" alt="Reactions and comments" loading="lazy" width="420" height="263"><div class="node-screenshot-label"><span class="screenshot-dot"></span> Gallery with reactions and chat</div></div>
<div class="node-header">
<div class="node-icon">&#x1F60D;</div>
<h4>Reactions &amp; Comments</h4>
</div>
<p>6 emoji reactions with floating animations, threaded comments with word-filter safety, pagination, and auto-notification.</p>
<div class="node-tags"><span class="node-tag">6 emoji types</span><span class="node-tag">Threaded</span><span class="node-tag">Content safety</span></div>
</div>
</div>
</div>
<!-- ====== BRANCH 8: Data Sovereignty ====== -->
<div class="branch branch-sovereignty" data-branch="sovereignty" id="sovereignty">
<div class="branch-header reveal">
<div class="branch-icon sovereignty">&#x1F1E8;&#x1F1E6;</div>
<div class="branch-title">
<h3>Data Sovereignty</h3>
<p>Canadian-built, privacy-first, no foreign surveillance, no lock-in</p>
</div>
</div>
<div class="nodes-grid stagger">
<div class="feature-node">
<div class="node-header">
<div class="node-icon">&#x1F3E0;</div>
<h4>Canadian Residency</h4>
</div>
<p>Built in Edmonton, Alberta. Hosted on Canadian soil. Subject only to Canadian law. No Patriot Act exposure.</p>
<div class="node-tags"><span class="node-tag">Alberta-built</span><span class="node-tag">Canadian law</span></div>
</div>
<div class="feature-node">
<div class="node-header">
<div class="node-icon">&#x1F513;</div>
<h4>No Lock-in</h4>
</div>
<p>Export everything anytime. Standard PostgreSQL database, standard file formats. Switch away whenever you want.</p>
<div class="node-tags"><span class="node-tag">Full export</span><span class="node-tag">Standard formats</span></div>
</div>
<div class="feature-node">
<div class="node-header">
<div class="node-icon">&#x1F6E1;</div>
<h4>Privacy First</h4>
</div>
<p>No analytics tracking your users. No corporate data mining. Your supporters' data protected by architecture, not policy.</p>
<div class="node-tags"><span class="node-tag">No tracking</span><span class="node-tag">By design</span></div>
</div>
<div class="feature-node">
<div class="node-header">
<div class="node-icon">&#x1F441;</div>
<h4>No Surveillance</h4>
</div>
<p>No NSA. No FISA courts. No corporate oversight. Complete operational security for your political organizing.</p>
<div class="node-tags"><span class="node-tag">Zero surveillance</span><span class="node-tag">OpSec</span></div>
</div>
</div>
</div>
</div>
</section>
<!-- ============================================
LIVE SITES
============================================ -->
<section class="section live-sites" id="live-sites">
<div class="container">
<div class="section-header reveal">
<h2>Living Network</h2>
<p>Real sites powered by Changemaker Lite in production today</p>
</div>
<div class="sites-grid stagger">
<a href="https://pridecorner.ca/" target="_blank" rel="noopener" class="site-card">
<div class="site-icon">&#x1F3F3;&#xFE0F;&#x200D;&#x1F308;</div>
<div>
<div class="site-name">Pride Corner</div>
<div class="site-desc">Community hub for LGBTQ+ advocacy and resources</div>
<div class="site-status">&#x2713; Live</div>
</div>
</a>
<a href="https://publicinterestalberta.org/" target="_blank" rel="noopener" class="site-card">
<div class="site-icon">&#x1F3DB;</div>
<div>
<div class="site-name">Public Interest Alberta</div>
<div class="site-desc">Policy advocacy and democratic engagement</div>
<div class="site-status">&#x2713; Live</div>
</div>
</a>
<a href="https://freealberta.org/" target="_blank" rel="noopener" class="site-card">
<div class="site-icon">&#x270A;</div>
<div>
<div class="site-name">Free Alberta</div>
<div class="site-desc">Fighting for equitable access to food and systemic policy change for all Albertans</div>
<div class="site-status">&#x2713; Live</div>
</div>
</a>
<a href="https://lindalindsay.org/" target="_blank" rel="noopener" class="site-card">
<div class="site-icon">&#x1F5F3;</div>
<div>
<div class="site-name">Linda Lindsay</div>
<div class="site-desc">Progressive political campaign platform</div>
<div class="site-status">&#x2713; Live</div>
</div>
</a>
<a href="https://albertademocracytaskforce.org/" target="_blank" rel="noopener" class="site-card">
<div class="site-icon">&#x1F91D;</div>
<div>
<div class="site-name">Alberta Democracy Taskforce</div>
<div class="site-desc">Defending democratic rights and freedoms</div>
<div class="site-status">&#x2713; Live</div>
</div>
</a>
<a href="https://bnkops.com/" target="_blank" rel="noopener" class="site-card featured">
<div class="site-icon">&#x26A1;</div>
<div>
<div class="site-name">BNKops</div>
<div class="site-desc">Liberation technology and hosting services</div>
<div class="site-status">&#x2713; Live</div>
</div>
</a>
</div>
<div class="live-stats stagger">
<div class="live-stat">
<div class="live-stat-value">6+</div>
<div class="live-stat-label">Live Sites</div>
</div>
<div class="live-stat">
<div class="live-stat-value">24/7</div>
<div class="live-stat-label">Uptime</div>
</div>
<div class="live-stat">
<div class="live-stat-value">$0</div>
<div class="live-stat-label">Monthly Fees</div>
</div>
<div class="live-stat">
<div class="live-stat-value">100%</div>
<div class="live-stat-label">Data Ownership</div>
</div>
</div>
</div>
</section>
<!-- ============================================
PRICING
============================================ -->
<section class="section" id="pricing">
<div class="container">
<div class="section-header reveal">
<h2>Pricing</h2>
<p>No hidden fees. No usage limits. No surprises. Self-host Free Forever</p>
</div>
<div class="pricing-grid stagger">
<div class="pricing-card">
<h3>Self-Hosted</h3>
<div class="price">$0</div>
<div class="price-period">forever</div>
<ul class="pricing-features">
<li>All 45+ campaign tools</li>
<li>Unlimited users &amp; data</li>
<li>Complete documentation</li>
<li>Community support</li>
<li>Your infrastructure</li>
<li>100% open source</li>
</ul>
<a href="/docs/getting-started/" class="btn-secondary" style="width:100%;justify-content:center;">Installation Guide</a>
<p class="pricing-note">For tech-savvy campaigns</p>
</div>
<div class="pricing-card featured">
<div class="pricing-badge">Most Popular</div>
<h3>Pre-Configured</h3>
<div class="price">Contact</div>
<div class="price-period">for pricing</div>
<ul class="pricing-features">
<li>Everything in Self-Hosted</li>
<li>Hardware included</li>
<li>Pre-installed &amp; configured</li>
<li>30-minute setup</li>
<li>Training included</li>
<li>Canadian support</li>
</ul>
<a href="https://bnkops.com/hardware" class="btn-primary" style="width:100%;justify-content:center;">Get a Quote</a>
<p class="pricing-note">Ready out of the box</p>
</div>
<div class="pricing-card">
<h3>Managed Hosting</h3>
<div class="price">Custom</div>
<div class="price-period">monthly</div>
<ul class="pricing-features">
<li>We handle everything</li>
<li>Canadian data centres</li>
<li>Daily backups</li>
<li>24/7 monitoring</li>
<li>Security updates</li>
<li>Priority support</li>
</ul>
<a href="https://bnkops.com/contact" class="btn-secondary" style="width:100%;justify-content:center;">Contact Sales</a>
<p class="pricing-note">For larger campaigns</p>
</div>
</div>
<div class="cost-compare reveal">
<h3>Compare Your Savings</h3>
<p>Average campaign using corporate tools: <strong>$1,200&ndash;$4,000/month</strong></p>
<p>Same capabilities with Changemaker Lite: <strong>$0 (self-hosted)</strong></p>
<a href="/phil/cost-comparison/">See detailed cost breakdown &rarr;</a>
</div>
</div>
</section>
<!-- ============================================
CTA
============================================ -->
<section class="cta-section" id="get-started">
<div class="cta-glow" aria-hidden="true"></div>
<!-- Background SVG removed — RootNetwork JS handles all connections -->
<div class="cta-content container reveal">
<h2>Ready to Grow Your Network?</h2>
<p>Join campaigns using open-source tools to build real political power.</p>
<div class="cta-buttons">
<a href="mailto:cmlite@bnkops.ca?subject=Request%20to%20Chat%20-%20CMLITE&body=Hi%20CMlite%20Team%2C%20I%20would%20like%20to%20chat!%20Please%20send%20me%20a%20email%20back.%20Cheers%2C%20" class="btn-primary">Schedule a Chat <span aria-hidden="true">&rarr;</span></a>
<a href="/docs/" class="btn-secondary">Read Documentation</a>
</div>
<div class="cta-meta">
<span>30-minute setup</span>
<span>Your data stays yours</span>
<span>No monthly fees</span>
</div>
</div>
</section>
<!-- ============================================
FOOTER
============================================ -->
<footer class="footer" role="contentinfo">
<div class="container">
<div class="footer-grid">
<div class="footer-section">
<h4>Platform</h4>
<ul class="footer-links">
<li><a href="#features">Features</a></li>
<li><a href="#pricing">Pricing</a></li>
<li><a href="#live-sites">Live Sites</a></li>
<li><a href="/phil/cost-comparison/">Cost Comparison</a></li>
</ul>
</div>
<div class="footer-section">
<h4>Documentation</h4>
<ul class="footer-links">
<li><a href="/docs/">Docs</a></li>
<li><a href="/docs/getting-started/">Getting Started</a></li>
<li><a href="/docs/architecture/">Architecture</a></li>
<li><a href="/blog/">Blog</a></li>
</ul>
</div>
<div class="footer-section">
<h4>Community</h4>
<ul class="footer-links">
<li><a href="https://gitea.bnkops.com/admin/changemaker.lite" target="_blank" rel="noopener">Source Code</a></li>
<li><a href="/docs/phil/">Philosophy</a></li>
<li><a href="https://bnkops.com/" target="_blank" rel="noopener">BNKops</a></li>
<li><a href="mailto:cmlite@bnkops.ca">Contact</a></li>
</ul>
</div>
</div>
<div class="footer-bottom">
&copy; 2024&ndash;2026 Bunker Operations. Built in Edmonton, Alberta. 100% open source.
</div>
</div>
</footer>
<!-- ============================================
FREE* MODAL
============================================ -->
<div class="free-modal-backdrop" id="free-modal-backdrop">
<div class="free-modal" role="dialog" aria-labelledby="free-modal-title" aria-modal="true">
<button class="free-modal-close" id="free-modal-close" aria-label="Close">&times;</button>
<h3 id="free-modal-title">What does free* mean?</h3>
<p class="free-modal-intro">
Changemaker Lite is 100% free and open source software &mdash; no license fees, no subscriptions, no vendor lock-in.
Running it in production does require a few external dependencies:
</p>
<ul class="free-modal-list">
<li>
<span class="dep-icon">&#x1F5A5;</span>
<span><strong>A Linux server or hardware</strong> &mdash; something to run the stack on (old laptop, mini PC, VPS)</span>
</li>
<li>
<span class="dep-icon">&#x1F310;</span>
<span><strong>An internet connection</strong> &mdash; to serve traffic to the public</span>
</li>
<li>
<span class="dep-icon">&#x1F3F7;</span>
<span><strong>A domain name</strong> &mdash; ~$10&ndash;15/yr for a custom domain</span>
</li>
<li>
<span class="dep-icon">&#x1F517;</span>
<span><strong>A production URL / tunnel</strong> &mdash; for public-facing deployment (can be deployed privately without one)</span>
</li>
<li>
<span class="dep-icon">&#x2709;</span>
<span><strong>An SMTP email provider</strong> &mdash; free tiers exist, but the most capable and secure are paid</span>
</li>
<li>
<span class="dep-icon">&#x1F4F1;</span>
<span><strong>An Android phone</strong> &mdash; required for SMS campaigns (uses Termux as a bridge to send texts)</span>
</li>
<li>
<span class="dep-icon">&#x1F4B3;</span>
<span><strong>A Stripe account</strong> &mdash; for credit card payments and donations; e-Transfer also integrated for direct bank payments</span>
</li>
</ul>
<p class="free-modal-footer">
None of these are unique to Changemaker Lite &mdash; any self-hosted platform needs them. The software itself will always be free.
Self-host at no cost, or pay for a pre-configured hardware device or a managed Cloudflare deployment.
</p>
</div>
</div>
<!-- ============================================
SCRIPTS
============================================ -->
<script src="https://unpkg.com/lunr@2.3.9/lunr.min.js"></script>
<script>
(function() {
'use strict';
/* ===========================================
THEME MANAGER
=========================================== */
const ThemeManager = {
init() {
const saved = localStorage.getItem('cmlite-theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const theme = saved || (prefersDark ? 'dark' : 'light');
this.apply(theme);
const btn = document.getElementById('theme-toggle');
if (btn) {
btn.addEventListener('click', () => {
const current = document.documentElement.getAttribute('data-theme');
const next = current === 'dark' ? 'light' : 'dark';
this.apply(next);
localStorage.setItem('cmlite-theme', next);
});
}
},
apply(theme) {
document.documentElement.setAttribute('data-theme', theme);
}
};
/* ===========================================
MOBILE MENU
=========================================== */
const MobileMenu = {
init() {
const btn = document.getElementById('hamburger');
const menu = document.getElementById('mobile-menu');
if (!btn || !menu) return;
btn.addEventListener('click', () => {
const isOpen = menu.classList.toggle('open');
btn.querySelector('.icon-menu').style.display = isOpen ? 'none' : 'block';
btn.querySelector('.icon-close').style.display = isOpen ? 'block' : 'none';
document.body.style.overflow = isOpen ? 'hidden' : '';
btn.setAttribute('aria-label', isOpen ? 'Close menu' : 'Open menu');
});
// Close on link click
menu.querySelectorAll('a').forEach(a => {
a.addEventListener('click', () => {
menu.classList.remove('open');
btn.querySelector('.icon-menu').style.display = 'block';
btn.querySelector('.icon-close').style.display = 'none';
document.body.style.overflow = '';
});
});
}
};
/* ===========================================
HEADER SCROLL EFFECT
=========================================== */
const HeaderScroll = {
init() {
const header = document.querySelector('.header');
if (!header) return;
let ticking = false;
window.addEventListener('scroll', () => {
if (!ticking) {
requestAnimationFrame(() => {
header.classList.toggle('scrolled', window.scrollY > 30);
ticking = false;
});
ticking = true;
}
}, { passive: true });
}
};
/* ===========================================
MYCELIUM ANIMATOR
=========================================== */
const MyceliumAnimator = {
init() {
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
// Setup tendril lengths
document.querySelectorAll('.myc-tendril').forEach(path => {
try {
const len = path.getTotalLength();
if (prefersReduced) {
path.style.strokeDasharray = 'none';
path.style.strokeDashoffset = '0';
path.classList.add('revealed');
} else {
path.style.strokeDasharray = len;
path.style.strokeDashoffset = len;
}
} catch(e) { /* non-path elements */ }
});
if (prefersReduced) return;
// Observe sections
const obs = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (!entry.isIntersecting) return;
const section = entry.target;
const sectionId = section.getAttribute('data-branch') ||
section.id ||
section.closest('[data-branch]')?.getAttribute('data-branch') ||
'';
// Reveal tendrils
const tendrils = section.querySelectorAll('.myc-tendril');
tendrils.forEach((t, i) => {
setTimeout(() => {
t.classList.add('revealed');
const len = parseFloat(t.style.strokeDasharray);
if (len) {
t.style.transition = `stroke-dashoffset ${1.2 + i * 0.3}s ease-out, opacity 0.4s ease`;
t.style.strokeDashoffset = '0';
}
}, i * 200);
});
});
}, { threshold: 0.15, rootMargin: '0px 0px -40px 0px' });
// Observe branches and sections with SVGs
document.querySelectorAll('.branch, .hero, .problems, .live-sites, .cta-section, [id="pricing"]').forEach(el => {
obs.observe(el);
});
}
};
/* ===========================================
SCROLL REVEAL
=========================================== */
const ScrollReveal = {
init() {
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (prefersReduced) {
document.querySelectorAll('.reveal, .stagger').forEach(el => el.classList.add('visible'));
document.querySelectorAll('.feature-node, .problem-card, .branch').forEach(el => el.classList.add('visible'));
return;
}
const obs = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
}
});
}, { threshold: 0.1, rootMargin: '0px 0px -30px 0px' });
document.querySelectorAll('.reveal, .stagger, .feature-node, .problem-card, .branch').forEach(el => {
obs.observe(el);
});
}
};
/* ===========================================
SMOOTH SCROLL
=========================================== */
const SmoothScroll = {
init() {
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function(e) {
const href = this.getAttribute('href');
if (href === '#') return;
const target = document.querySelector(href);
if (target) {
e.preventDefault();
const offset = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--header-height')) || 64;
const y = target.getBoundingClientRect().top + window.scrollY - offset - 16;
window.scrollTo({ top: y, behavior: 'smooth' });
}
});
});
}
};
/* ===========================================
MKDOCS SEARCH (Lunr.js)
=========================================== */
class MkDocsSearch {
constructor() {
this.searchIndex = null;
this.searchDocs = null;
this.initialized = false;
this.debounceTimeout = null;
}
async initialize() {
try {
const response = await fetch('search/search_index.json');
if (!response.ok) throw new Error('Failed to load search index');
const searchData = await response.json();
this.searchIndex = lunr(function() {
this.ref('location');
this.field('title', { boost: 10 });
this.field('text');
searchData.docs.forEach(doc => { this.add(doc); });
});
this.searchDocs = searchData.docs;
this.initialized = true;
} catch (error) {
console.error('Search init failed:', error);
}
}
search(query) {
if (!this.initialized || !query || query.length < 2) return [];
try {
const results = this.searchIndex.search(query);
return results.slice(0, 10).map(result => {
const doc = this.searchDocs.find(d => d.location === result.ref);
if (!doc) return null;
return {
...doc,
score: result.score,
url: doc.location,
snippet: this.extractSnippet(doc.text, query)
};
}).filter(Boolean);
} catch (e) {
return [];
}
}
extractSnippet(text, query, maxLength = 150) {
const lowerText = text.toLowerCase();
const lowerQuery = query.toLowerCase();
const index = lowerText.indexOf(lowerQuery);
if (index === -1) return text.substring(0, maxLength) + '...';
const start = Math.max(0, index - 50);
const end = Math.min(text.length, index + query.length + 100);
let snippet = text.substring(start, end);
if (start > 0) snippet = '...' + snippet;
if (end < text.length) snippet = snippet + '...';
const regex = new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
snippet = snippet.replace(regex, '<mark>$1</mark>');
return snippet;
}
}
/* ===========================================
SEARCH WIRING
=========================================== */
async function initSearch() {
const search = new MkDocsSearch();
const searchInput = document.getElementById('docs-search-input');
const searchResults = document.getElementById('docs-search-results');
if (!searchInput || !searchResults) return;
const resultsContainer = searchResults.querySelector('.docs-search-results-list');
const resultsCount = searchResults.querySelector('.results-count');
const closeBtn = searchResults.querySelector('.close-results');
await search.initialize();
searchInput.addEventListener('input', (e) => {
clearTimeout(search.debounceTimeout);
search.debounceTimeout = setTimeout(() => {
const query = e.target.value.trim();
if (!query || query.length < 2) {
searchResults.style.display = 'none';
return;
}
const results = search.search(query);
if (results.length === 0) {
resultsContainer.innerHTML = '<div class="no-results">No results found</div>';
resultsCount.textContent = 'No results';
} else {
resultsCount.textContent = results.length + ' result' + (results.length > 1 ? 's' : '');
resultsContainer.innerHTML = results.map(r =>
'<a href="' + r.url + '" class="search-result-item">' +
'<div class="search-result-title">' + r.title + '</div>' +
'<div class="search-result-content">' + r.snippet + '</div></a>'
).join('');
}
searchResults.style.display = 'block';
}, 300);
});
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
searchInput.focus();
searchInput.select();
}
if (e.key === 'Escape' && searchResults.style.display !== 'none') {
searchResults.style.display = 'none';
searchInput.value = '';
searchInput.blur();
}
});
if (closeBtn) {
closeBtn.addEventListener('click', () => {
searchResults.style.display = 'none';
searchInput.value = '';
});
}
document.addEventListener('click', (e) => {
if (!searchInput.contains(e.target) && !searchResults.contains(e.target)) {
searchResults.style.display = 'none';
}
});
}
/* ===========================================
ROOT NETWORK — Branching tree from hero root to every card
=========================================== */
const RootNetwork = {
// Each section: selector, header, card selector, color, angle hint for root ball departure
sections: [
{ sel: '.problems', headerSel: '.problems .section-header', cardSel: '.problem-card', color: '#F87171', angle: -0.3 },
{ sel: '.branch[data-branch="comm"]', headerSel: '.branch[data-branch="comm"] .branch-header', cardSel: '.feature-node', color: '#C084FC', angle: -0.15 },
{ sel: '.branch[data-branch="map"]', headerSel: '.branch[data-branch="map"] .branch-header', cardSel: '.feature-node', color: '#34D399', angle: 0.15 },
{ sel: '.branch[data-branch="content"]',headerSel: '.branch[data-branch="content"] .branch-header',cardSel: '.feature-node', color: '#FB923C', angle: -0.22 },
{ sel: '.branch[data-branch="data"]', headerSel: '.branch[data-branch="data"] .branch-header', cardSel: '.feature-node', color: '#22D3EE', angle: 0.22 },
{ sel: '.branch[data-branch="devops"]', headerSel: '.branch[data-branch="devops"] .branch-header', cardSel: '.feature-node', color: '#FBBF24', angle: -0.08 },
{ sel: '.branch[data-branch="fundraising"]', headerSel: '.branch[data-branch="fundraising"] .branch-header', cardSel: '.feature-node', color: '#EC4899', angle: 0.18 },
{ sel: '.branch[data-branch="social"]', headerSel: '.branch[data-branch="social"] .branch-header', cardSel: '.feature-node', color: '#38BDF8', angle: -0.18 },
{ sel: '.branch[data-branch="sovereignty"]', headerSel: '.branch[data-branch="sovereignty"] .branch-header', cardSel: '.feature-node', color: '#F87171', angle: 0.08 },
{ sel: '.live-sites', headerSel: '.live-sites .section-header', cardSel: '.site-card', color: '#8B5CF6', angle: -0.12 },
{ sel: '#pricing', headerSel: '#pricing .section-header', cardSel: '.pricing-card, .cost-compare', color: '#C084FC', angle: 0.12 },
{ sel: '.cta-section', headerSel: '.cta-section .cta-content', cardSel: null, color: '#8B5CF6', angle: 0 }
],
tendrils: [],
_raf: null,
// Total page height for tapering calculation
_pageH: 1,
_rootY: 0,
isMobile: false,
init() {
this.isMobile = window.innerWidth <= 768;
this.redraw();
this.startScroll();
let resizeTimer;
window.addEventListener('resize', () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
this.isMobile = window.innerWidth <= 768;
this.redraw();
this.updateScroll();
}, 300);
});
},
redraw() {
if (this.isMobile) {
this.drawMobile();
} else {
this.drawAll();
}
},
rand(seed) {
const x = Math.sin(seed * 9301 + 49297) * 49297;
return x - Math.floor(x);
},
// Compute stroke width based on distance from root — thicker near top, thinner toward bottom
taperSW(y, base) {
const progress = Math.max(0, Math.min(1, (y - this._rootY) / (this._pageH - this._rootY)));
// Taper: base at root, shrinks to 35% at page bottom
return base * (1 - progress * 0.65);
},
// Wavy multi-segment bezier
wavyPath(x0, y0, x1, y1, seed, ampScale) {
const dx = x1 - x0, dy = y1 - y0;
const dist = Math.sqrt(dx * dx + dy * dy) || 1;
const perpX = -dy / dist, perpY = dx / dist;
const segs = Math.max(3, Math.min(6, Math.round(dist / 300)));
const amp = Math.min(dist * (ampScale || 0.08), 120);
let d = `M${x0.toFixed(1)},${y0.toFixed(1)}`;
let px = x0, py = y0;
for (let s = 1; s <= segs; s++) {
const t = s / segs;
let ex = x0 + dx * t, ey = y0 + dy * t;
if (s < segs) {
const drift = (this.rand(seed + s * 7) - 0.5) * 2 * amp;
ex += perpX * drift;
ey += perpY * drift;
}
const w1 = (this.rand(seed + s * 13 + 1) - 0.5) * 2 * amp * 0.7;
const w2 = (this.rand(seed + s * 17 + 2) - 0.5) * 2 * amp * 0.7;
d += ` C${(px + (ex-px)*0.33 + perpX*w1).toFixed(1)},${(py + (ey-py)*0.33 + perpY*w1).toFixed(1)} ${(px + (ex-px)*0.67 + perpX*w2).toFixed(1)},${(py + (ey-py)*0.67 + perpY*w2).toFixed(1)} ${ex.toFixed(1)},${ey.toFixed(1)}`;
px = ex; py = ey;
}
return d;
},
makeLine(svg, pathD, color, sw) {
const p = document.createElementNS('http://www.w3.org/2000/svg', 'path');
p.setAttribute('d', pathD);
p.setAttribute('stroke', color);
p.setAttribute('stroke-width', sw.toFixed(1));
p.setAttribute('class', 'root-line');
svg.appendChild(p);
return p;
},
makeDot(svg, cx, cy, r, color) {
const c = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
c.setAttribute('cx', cx.toFixed(1));
c.setAttribute('cy', cy.toFixed(1));
c.setAttribute('r', r.toString());
c.setAttribute('fill', color);
c.setAttribute('class', 'root-node');
svg.appendChild(c);
return c;
},
drawMobile() {
const old = document.getElementById('root-network');
if (old) old.remove();
this.tendrils = [];
const rootSvgEl = document.querySelector('.hero-root-svg');
if (!rootSvgEl) return;
const rootRect = rootSvgEl.getBoundingClientRect();
const rootX = rootRect.left + rootRect.width / 2;
const rootY = rootRect.top + window.scrollY + rootRect.height / 2;
this._rootY = rootY;
this._pageH = document.documentElement.scrollHeight;
const pageW = document.documentElement.scrollWidth;
const spineX = 20;
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('id', 'root-network');
svg.setAttribute('class', 'root-network-svg');
svg.setAttribute('aria-hidden', 'true');
svg.style.height = this._pageH + 'px';
svg.style.width = pageW + 'px';
// Collect junction points for each section
const junctions = [];
this.sections.forEach((sec, si) => {
const headerEl = document.querySelector(sec.headerSel);
if (!headerEl) return;
const hRect = headerEl.getBoundingClientRect();
const jy = hRect.top + window.scrollY + hRect.height / 2;
junctions.push({ y: jy, color: sec.color, idx: si });
});
if (junctions.length === 0) { document.body.appendChild(svg); return; }
// --- Initial curve: root ball center → (spineX, first section Y) ---
const firstJ = junctions[0];
const initD = this.wavyPath(rootX, rootY, spineX, firstJ.y, 42, 0.06);
const initPath = this.makeLine(svg, initD, '#8B5CF6', 2.0);
const initDot = this.makeDot(svg, spineX, firstJ.y, 3, firstJ.color);
this.tendrils.push({ path: initPath, dot: initDot, targetY: firstJ.y, len: 0, opacity: 0.7, isTrunk: true, sectionIdx: 0 });
// --- Spine segments + branch stubs ---
let prevY = firstJ.y;
for (let i = 1; i < junctions.length; i++) {
const j = junctions[i];
const seed = 100 + i * 73;
// Spine segment: vertical along spineX
const spineSW = this.taperSW(j.y, 1.8);
const spineD = this.wavyPath(spineX, prevY, spineX, j.y, seed, 0.03);
const spinePath = this.makeLine(svg, spineD, j.color, Math.max(spineSW, 1.0));
const spineDot = this.makeDot(svg, spineX, j.y, 3, j.color);
this.tendrils.push({ path: spinePath, dot: spineDot, targetY: j.y, len: 0, opacity: 0.7, isTrunk: true, sectionIdx: j.idx });
// Branch stub: short horizontal reach to the right
const stubD = this.wavyPath(spineX, j.y, spineX + 50, j.y, seed + 37, 0.15);
const stubPath = this.makeLine(svg, stubD, j.color, 1.2);
this.tendrils.push({ path: stubPath, dot: null, targetY: j.y, len: 0, opacity: 0.5, parentY: j.y });
prevY = j.y;
}
// Also add a branch stub for the first junction
const firstStubD = this.wavyPath(spineX, firstJ.y, spineX + 50, firstJ.y, 137, 0.15);
const firstStubPath = this.makeLine(svg, firstStubD, firstJ.color, 1.2);
this.tendrils.push({ path: firstStubPath, dot: null, targetY: firstJ.y, len: 0, opacity: 0.5, parentY: firstJ.y });
document.body.appendChild(svg);
// Measure path lengths + set up dash animation
requestAnimationFrame(() => {
this.tendrils.forEach(t => {
try {
t.len = t.path.getTotalLength();
t.path.style.strokeDasharray = t.len;
t.path.style.strokeDashoffset = t.len;
t.path.style.opacity = '0';
if (t.dot) t.dot.style.opacity = '0';
} catch(e) {}
});
this.updateScroll();
});
},
drawAll() {
const old = document.getElementById('root-network');
if (old) old.remove();
this.tendrils = [];
if (window.innerWidth <= 768) return;
const rootSvgEl = document.querySelector('.hero-root-svg');
if (!rootSvgEl) return;
const rootRect = rootSvgEl.getBoundingClientRect();
const rootX = rootRect.left + rootRect.width / 2;
const rootY = rootRect.top + window.scrollY + rootRect.height / 2;
const rootRadius = 40; // spread departure points around root ball
this._rootY = rootY;
this._pageH = document.documentElement.scrollHeight;
const pageW = document.documentElement.scrollWidth;
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('id', 'root-network');
svg.setAttribute('class', 'root-network-svg');
svg.setAttribute('aria-hidden', 'true');
svg.style.height = this._pageH + 'px';
svg.style.width = pageW + 'px';
let seedBase = 0;
this.sections.forEach((sec, si) => {
const sectionEl = document.querySelector(sec.sel);
const headerEl = document.querySelector(sec.headerSel);
if (!sectionEl || !headerEl) return;
// Header center = junction point
const hRect = headerEl.getBoundingClientRect();
const jx = hRect.left + hRect.width / 2;
const jy = hRect.top + window.scrollY + hRect.height / 2;
// Departure point: spread around root ball perimeter using angle hint
const depAngle = Math.PI / 2 + sec.angle; // PI/2 = straight down, angle offsets left/right
const depX = rootX + Math.cos(depAngle) * rootRadius * (0.8 + this.rand(si * 73) * 0.4);
const depY = rootY + Math.sin(depAngle) * rootRadius * (0.6 + this.rand(si * 59) * 0.4);
// --- TRUNK: root ball → section header ---
// Stroke width tapers: thick near root, thinner further down
const trunkSW = this.taperSW(jy, 5.0 + this.rand(si * 41) * 1.5); // base 5-6.5px, tapers
const trunkD = this.wavyPath(depX, depY, jx, jy, seedBase + si * 100, 0.07);
const trunkPath = this.makeLine(svg, trunkD, sec.color, Math.max(trunkSW, 2.0));
const trunkDot = this.makeDot(svg, jx, jy, 6, sec.color);
this.tendrils.push({ path: trunkPath, dot: trunkDot, targetY: jy, len: 0, opacity: 0.85, isTrunk: true, sectionIdx: si });
// --- BRANCHES: section header → each card ---
if (!sec.cardSel) { seedBase += 200; return; }
const cards = sectionEl.querySelectorAll(sec.cardSel);
cards.forEach((card, ci) => {
const cRect = card.getBoundingClientRect();
const cx = cRect.left + cRect.width / 2;
const cy = cRect.top + window.scrollY + cRect.height * 0.3;
// Branch from junction (header) to card — NOT from root ball
const branchSW = this.taperSW(cy, 3.0 + this.rand(seedBase + ci * 31) * 1.0); // base 3-4px, tapers
const branchD = this.wavyPath(jx, jy, cx, cy, seedBase + ci * 37 + 11, 0.10);
const branchPath = this.makeLine(svg, branchD, sec.color, Math.max(branchSW, 1.2));
const branchDot = this.makeDot(svg, cx, cy, 4, sec.color);
this.tendrils.push({ path: branchPath, dot: branchDot, targetY: cy, len: 0, opacity: 0.7, parentY: jy });
});
seedBase += 200;
});
document.body.appendChild(svg);
// Measure path lengths + set up dash animation
requestAnimationFrame(() => {
this.tendrils.forEach(t => {
try {
t.len = t.path.getTotalLength();
t.path.style.strokeDasharray = t.len;
t.path.style.strokeDashoffset = t.len;
t.path.style.opacity = '0';
if (t.dot) t.dot.style.opacity = '0';
} catch(e) {}
});
this.updateScroll();
});
},
startScroll() {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
requestAnimationFrame(() => {
this.tendrils.forEach(t => {
t.path.style.opacity = t.opacity;
t.path.style.strokeDashoffset = '0';
if (t.dot) t.dot.style.opacity = '0.8';
});
});
return;
}
window.addEventListener('scroll', () => {
if (!this._raf) {
this._raf = requestAnimationFrame(() => {
this._raf = null;
this.updateScroll();
});
}
}, { passive: true });
},
updateScroll() {
const viewH = window.innerHeight;
const viewBottom = window.scrollY + viewH;
const sectionCount = this.sections.length;
// Trunks start growing as soon as you scroll at all (rootY = start).
// They grow over 1 viewport of scrolling each, staggered so later sections finish later.
const trunkGrowStart = this._rootY;
this.tendrils.forEach(t => {
if (!t.len) return;
let progress;
if (t.isTrunk) {
// Stagger: section 0 completes over 1vh, section 9 over ~2.5vh from start
const stagger = (t.sectionIdx / sectionCount) * viewH * 1.5;
const growEnd = trunkGrowStart + viewH * 1.0 + stagger;
progress = Math.max(0, Math.min(1, (viewBottom - trunkGrowStart) / (growEnd - trunkGrowStart)));
} else {
// Branches: start when parent section enters viewport, grow over 0.6vh
const parentY = t.parentY || t.targetY;
const growStart = parentY - viewH * 0.7;
const growEnd = growStart + viewH * 0.6;
progress = Math.max(0, Math.min(1, (viewBottom - growStart) / (growEnd - growStart)));
}
t.path.style.strokeDashoffset = (t.len * (1 - progress)).toFixed(0);
t.path.style.opacity = (progress * t.opacity).toFixed(2);
if (t.dot) {
t.dot.style.opacity = progress > 0.8 ? (0.8).toFixed(2) : '0';
}
});
}
};
/* ===========================================
FLOATING BACKGROUND ELEMENTS
=========================================== */
const FloatingElements = {
emojis: ['\u{1F344}','\u{1F33F}','\u{1F343}','\u{1F331}','\u{1F342}','\u{1F4BB}','\u{1F527}','\u26A1','\u{1F517}','\u{1F4E1}'],
init() {
if (window.innerWidth <= 768) return;
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const count = 18;
const pageHeight = document.documentElement.scrollHeight;
const container = document.createElement('div');
container.className = 'floating-elements';
container.setAttribute('aria-hidden', 'true');
container.style.height = pageHeight + 'px';
for (let i = 0; i < count; i++) {
const el = document.createElement('span');
el.className = 'floating-emoji';
el.textContent = this.emojis[i % this.emojis.length];
const size = 12 + Math.random() * 12;
const opacity = 0.05 + Math.random() * 0.10;
const x = Math.random() * 100;
const y = (i / count) * 100;
const duration = 60 + Math.random() * 30;
const delay = -(Math.random() * duration);
el.style.fontSize = size + 'px';
el.style.opacity = opacity;
el.style.left = x + '%';
el.style.top = y + '%';
el.style.setProperty('--float-duration', duration + 's');
el.style.animationDelay = delay + 's';
if (prefersReduced) {
el.style.animation = 'none';
}
container.appendChild(el);
}
document.body.appendChild(container);
}
};
/* ===========================================
FREE* MODAL
=========================================== */
const FreeModal = {
init() {
const link = document.getElementById('free-asterisk-link');
const backdrop = document.getElementById('free-modal-backdrop');
const closeBtn = document.getElementById('free-modal-close');
if (!link || !backdrop || !closeBtn) return;
const open = () => backdrop.classList.add('active');
const close = () => backdrop.classList.remove('active');
link.addEventListener('click', (e) => { e.preventDefault(); open(); });
closeBtn.addEventListener('click', close);
backdrop.addEventListener('click', (e) => { if (e.target === backdrop) close(); });
document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && backdrop.classList.contains('active')) close(); });
}
};
/* ===========================================
BETA MODAL
=========================================== */
const BetaModal = {
init() {
const pill = document.getElementById('beta-pill');
const backdrop = document.getElementById('beta-modal-backdrop');
const closeBtn = document.getElementById('beta-modal-close');
if (!pill || !backdrop || !closeBtn) return;
pill.addEventListener('click', () => backdrop.classList.add('open'));
closeBtn.addEventListener('click', () => backdrop.classList.remove('open'));
backdrop.addEventListener('click', (e) => { if (e.target === backdrop) backdrop.classList.remove('open'); });
document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && backdrop.classList.contains('open')) backdrop.classList.remove('open'); });
}
};
/* ===========================================
TERMINAL COPY — clipboard for install cmd
=========================================== */
const TerminalCopy = {
init() {
const btn = document.getElementById('hero-copy-btn');
if (!btn) return;
btn.addEventListener('click', () => {
const cmd = 'curl -fsSL gitea.bnkops.com/admin/changemaker.lite/raw/branch/main/scripts/install.sh | bash';
navigator.clipboard.writeText(cmd).then(() => {
btn.classList.add('copied');
btn.querySelector('.copy-label').textContent = 'Copied!';
setTimeout(() => {
btn.classList.remove('copied');
btn.querySelector('.copy-label').textContent = 'Copy';
}, 2000);
});
});
}
};
/* ===========================================
TYPEWRITER — rotating hero words
=========================================== */
const Typewriter = {
words: ['campaigns', 'canvassing', 'fundraising', 'newsletters', 'media library', 'volunteer shifts', 'team chat', 'events'],
el: null,
wordIdx: 0,
charIdx: 0,
isDeleting: false,
timeout: null,
init() {
this.el = document.getElementById('tw-word');
if (!this.el) return;
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
this.el.textContent = this.words[0];
return;
}
this.tick();
},
tick() {
const word = this.words[this.wordIdx];
if (this.isDeleting) {
this.charIdx--;
} else {
this.charIdx++;
}
this.el.textContent = word.substring(0, this.charIdx);
let delay = this.isDeleting ? 40 : 80;
if (!this.isDeleting && this.charIdx === word.length) {
delay = 2200;
this.isDeleting = true;
} else if (this.isDeleting && this.charIdx === 0) {
this.isDeleting = false;
this.wordIdx = (this.wordIdx + 1) % this.words.length;
delay = 400;
}
this.timeout = setTimeout(() => this.tick(), delay);
}
};
/* ===========================================
FEATURE PILLS — staggered entrance
=========================================== */
const FeaturePills = {
init() {
const pills = document.querySelectorAll('.hero-pill');
if (!pills.length) return;
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
pills.forEach(p => p.classList.add('visible'));
return;
}
pills.forEach((pill, i) => {
setTimeout(() => pill.classList.add('visible'), 600 + i * 120);
});
}
};
/* ===========================================
FEATURE SHOWCASE — rotating cards
=========================================== */
const FeatureShowcase = {
current: 0,
cards: [],
dots: [],
interval: null,
DURATION: 5000,
init() {
this.cards = document.querySelectorAll('.showcase-card');
this.dots = document.querySelectorAll('.showcase-dot');
if (this.cards.length < 2) return;
this.dots.forEach(dot => {
dot.addEventListener('click', () => {
const idx = parseInt(dot.getAttribute('data-idx'), 10);
if (idx !== this.current) this.goTo(idx);
});
});
this.startAutoplay();
},
goTo(idx) {
const prev = this.cards[this.current];
prev.classList.remove('active');
prev.classList.add('exiting');
setTimeout(() => prev.classList.remove('exiting'), 600);
this.current = idx;
this.cards[this.current].classList.add('active');
this.dots.forEach((d, i) => d.classList.toggle('active', i === idx));
this.restartAutoplay();
},
next() {
this.goTo((this.current + 1) % this.cards.length);
},
startAutoplay() {
this.interval = setInterval(() => this.next(), this.DURATION);
},
restartAutoplay() {
clearInterval(this.interval);
this.startAutoplay();
}
};
/* ===========================================
COUNT-UP STATS — animated number counters
=========================================== */
const CountUpStats = {
init() {
const stats = document.querySelectorAll('.hero-stat-value[data-count]');
if (!stats.length) return;
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
stats.forEach(el => {
const target = parseInt(el.getAttribute('data-count'), 10);
const prefix = el.getAttribute('data-prefix') || '';
const suffix = el.getAttribute('data-suffix') || '';
el.textContent = prefix + target + suffix;
});
return;
}
const obs = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.animate(entry.target);
obs.unobserve(entry.target);
}
});
}, { threshold: 0.5 });
stats.forEach(el => obs.observe(el));
},
animate(el) {
const target = parseInt(el.getAttribute('data-count'), 10);
const prefix = el.getAttribute('data-prefix') || '';
const suffix = el.getAttribute('data-suffix') || '';
const duration = 1400;
const start = performance.now();
const step = (now) => {
const progress = Math.min((now - start) / duration, 1);
const eased = 1 - Math.pow(1 - progress, 3);
const current = Math.round(eased * target);
el.textContent = prefix + current + suffix;
if (progress < 1) requestAnimationFrame(step);
};
requestAnimationFrame(step);
}
};
/* ===========================================
PARTICLE DRIFT — floating background dots
=========================================== */
const ParticleDrift = {
canvas: null,
ctx: null,
particles: [],
raf: null,
COUNT: 35,
init() {
this.canvas = document.getElementById('hero-particles');
if (!this.canvas) return;
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
// Fewer particles on mobile
if (window.innerWidth < 768) this.COUNT = 15;
this.ctx = this.canvas.getContext('2d');
this.resize();
this.createParticles();
this.loop();
window.addEventListener('resize', () => this.resize());
},
resize() {
const hero = this.canvas.parentElement;
this.canvas.width = hero.offsetWidth;
this.canvas.height = hero.offsetHeight;
},
createParticles() {
this.particles = [];
for (let i = 0; i < this.COUNT; i++) {
this.particles.push({
x: Math.random() * this.canvas.width,
y: Math.random() * this.canvas.height,
r: Math.random() * 2 + 0.5,
vx: (Math.random() - 0.5) * 0.3,
vy: -(Math.random() * 0.4 + 0.1),
alpha: Math.random() * 0.3 + 0.1,
});
}
},
loop() {
const { ctx, canvas, particles } = this;
ctx.clearRect(0, 0, canvas.width, canvas.height);
const isDark = document.documentElement.getAttribute('data-theme') !== 'light';
const baseColor = isDark ? '139, 92, 246' : '111, 66, 193';
particles.forEach(p => {
p.x += p.vx;
p.y += p.vy;
// Wrap around
if (p.y < -5) { p.y = canvas.height + 5; p.x = Math.random() * canvas.width; }
if (p.x < -5) p.x = canvas.width + 5;
if (p.x > canvas.width + 5) p.x = -5;
ctx.beginPath();
ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
ctx.fillStyle = `rgba(${baseColor}, ${p.alpha})`;
ctx.fill();
});
this.raf = requestAnimationFrame(() => this.loop());
}
};
/* ===========================================
BOOT
=========================================== */
document.addEventListener('DOMContentLoaded', () => {
ThemeManager.init();
MobileMenu.init();
HeaderScroll.init();
MyceliumAnimator.init();
ScrollReveal.init();
RootNetwork.init();
FloatingElements.init();
SmoothScroll.init();
FreeModal.init();
BetaModal.init();
initSearch();
TerminalCopy.init();
Typewriter.init();
FeaturePills.init();
FeatureShowcase.init();
CountUpStats.init();
ParticleDrift.init();
});
})();
</script>
<!-- Docs Analytics Tracking -->
<script>
(function() {
var apiUrl = "https://api.cmlite.org";
if (!apiUrl) return;
var trackUrl = apiUrl + "/api/docs-analytics/track";
function getSessionHash() {
var key = "__docs_sh";
var hash = sessionStorage.getItem(key);
if (!hash) {
hash = crypto.randomUUID ? crypto.randomUUID() : "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
var r = (Math.random() * 16) | 0; return (c === "x" ? r : (r & 0x3) | 0x8).toString(16);
});
sessionStorage.setItem(key, hash);
}
return hash;
}
var payload = JSON.stringify({ path: location.pathname, referrer: document.referrer || undefined, sessionHash: getSessionHash() });
if (navigator.sendBeacon) { navigator.sendBeacon(trackUrl, new Blob([payload], { type: "application/json" })); }
else { fetch(trackUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: payload, keepalive: true }).catch(function() {}); }
})();
</script>
</body>
</html>