3873 lines
163 KiB
HTML
3873 lines
163 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.">
|
|
<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
|
|
============================================ */
|
|
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: 1.35rem;
|
|
font-weight: 800;
|
|
color: var(--primary-light);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.logo-icon {
|
|
width: 28px;
|
|
height: 28px;
|
|
fill: var(--primary-light);
|
|
}
|
|
|
|
.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) + 3rem) 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%);
|
|
}
|
|
|
|
.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; }
|
|
}
|
|
|
|
.hero-content {
|
|
position: relative;
|
|
z-index: 1;
|
|
text-align: center;
|
|
max-width: 800px;
|
|
}
|
|
|
|
.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: 1.5rem;
|
|
}
|
|
|
|
.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: 1.25rem;
|
|
}
|
|
|
|
.hero-subtitle {
|
|
color: var(--text-secondary);
|
|
font-size: clamp(1rem, 2vw, 1.2rem);
|
|
max-width: 640px;
|
|
margin: 0 auto 2rem;
|
|
line-height: 1.7;
|
|
}
|
|
|
|
.hero-cta {
|
|
display: flex;
|
|
gap: 1rem;
|
|
justify-content: center;
|
|
flex-wrap: wrap;
|
|
margin-bottom: 2.5rem;
|
|
}
|
|
|
|
/* Search */
|
|
.hero-search {
|
|
position: relative;
|
|
max-width: 520px;
|
|
margin: 0 auto 3rem;
|
|
}
|
|
|
|
.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: 640px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.hero-stat {
|
|
text-align: center;
|
|
}
|
|
|
|
.hero-stat-value {
|
|
font-size: 1.5rem;
|
|
font-weight: 800;
|
|
color: var(--primary-light);
|
|
}
|
|
|
|
.hero-stat-label {
|
|
font-size: 0.75rem;
|
|
color: var(--text-muted);
|
|
margin-top: 0.25rem;
|
|
}
|
|
|
|
/* ============================================
|
|
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; }
|
|
.hamburger { display: block; }
|
|
.logo-tagline { display: none; }
|
|
|
|
.section { padding: 4rem 0; }
|
|
|
|
.hero { min-height: auto; padding-top: calc(var(--header-height) + 2rem); padding-bottom: 3rem; }
|
|
.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-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 {
|
|
flex-direction: column;
|
|
align-items: center;
|
|
}
|
|
|
|
.cta-buttons {
|
|
flex-direction: column;
|
|
align-items: center;
|
|
}
|
|
|
|
/* 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; }
|
|
.section { padding: 3rem 0; }
|
|
.section-header { margin-bottom: 2.5rem; }
|
|
.hero-stats { grid-template-columns: repeat(2, 1fr); }
|
|
.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" />
|
|
</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">
|
|
<svg class="logo-icon" viewBox="0 0 24 24" aria-hidden="true">
|
|
<path d="M12 2C12 2 8 6 8 10c0 2.2 1.8 4 4 4s4-1.8 4-4c0-4-4-8-4-8zM12 16c-3 0-5 2-7 4h14c-2-2-4-4-7-4zM6 10c0-1.5-.5-3-1.5-4.5C3 7.5 2 9 2 10.5c0 1.9 1.6 3.5 3.5 3.5.2 0 .3 0 .5-.1M18 10c0-1.5.5-3 1.5-4.5C21 7.5 22 9 22 10.5c0 1.9-1.6 3.5-3.5 3.5-.2 0-.3 0-.5-.1"/>
|
|
</svg>
|
|
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">
|
|
<!-- 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">
|
|
<!-- Central root ball — prominent node, JS draws all connections -->
|
|
<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>
|
|
<!-- Background SVG tendrils removed — RootNetwork JS handles all connections -->
|
|
</div>
|
|
|
|
<div class="hero-content">
|
|
<div class="hero-badge">Self-Hosted Campaign Infrastructure</div>
|
|
<h1>Grow Power.<br>Don't Rent It.</h1>
|
|
<p class="hero-subtitle">
|
|
Run your campaigns, canvassing, fundraising, team chat, media, and more — all on your own infrastructure.
|
|
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">→</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/v2" class="btn-secondary" target="_blank" rel="noopener">Source Code</a>
|
|
</div>
|
|
|
|
<!-- Search -->
|
|
<div class="hero-search">
|
|
<div class="search-input-wrap">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
|
|
<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
|
</svg>
|
|
<input type="text" class="search-box" id="docs-search-input" placeholder="Search docs..." autocomplete="off" aria-label="Search documentation">
|
|
<span class="search-kbd" aria-hidden="true">Ctrl+K</span>
|
|
</div>
|
|
<div class="search-results" id="docs-search-results" role="listbox">
|
|
<div class="search-results-header">
|
|
<span class="results-count"></span>
|
|
<button class="close-results" type="button" aria-label="Close search results">×</button>
|
|
</div>
|
|
<div class="docs-search-results-list"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="hero-stats">
|
|
<div class="hero-stat">
|
|
<div class="hero-stat-value">100%</div>
|
|
<div class="hero-stat-label">Data Ownership</div>
|
|
</div>
|
|
<div class="hero-stat">
|
|
<div class="hero-stat-value">$0</div>
|
|
<div class="hero-stat-label">Self-Hosted</div>
|
|
</div>
|
|
<div class="hero-stat">
|
|
<div class="hero-stat-value">45+</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>
|
|
</section>
|
|
|
|
<!-- ============================================
|
|
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">📱</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">🗺</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">💸</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">🔒</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">📵</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">🏢</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 — 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’s future in someone else’s hands.</p>
|
|
<p>Changemaker asks a different question: <mark>“what tools are needed to grow change in a community?”</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 — on infrastructure <em>you</em> control.</p>
|
|
<p class="growing-callout">Organizational independence requires technological independence. Socialist movements will never outspend capital — 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">🤝</div>
|
|
<h4>Distribute Power</h4>
|
|
<p>Decentralized organizing is the way out. When knowledge and tools are widely distributed — not gatekept by leadership or locked behind vendor paywalls — movements become resilient.</p>
|
|
</div>
|
|
<div class="growing-pillar">
|
|
<div class="pillar-icon">🔒</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 — no algorithmic surveillance, no foreign data laws, no backdoors.</p>
|
|
</div>
|
|
<div class="growing-pillar">
|
|
<div class="pillar-icon">🌱</div>
|
|
<h4>De-Corp Your Stack</h4>
|
|
<p>Every subscription to corporate software funds the machine you’re fighting. Free and open source tools reduce dependence on capital, eliminate vendor lock-in, and keep your movement’s resources where they belong — 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">→</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’re in your team chat, signed up for a canvassing shift, and synced across your map, newsletter, and event calendar — <strong>without creating accounts on five different platforms</strong>. That’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 — each node strengthens the whole</p>
|
|
</div>
|
|
|
|
<!-- ====== BRANCH 1: Communication ====== -->
|
|
<div class="branch branch-comm" data-branch="comm">
|
|
<div class="branch-header reveal">
|
|
<div class="branch-icon comm">📨</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">📬</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">🎯</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">✉</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">💬</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">🚀</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">🔔</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">📱</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">🗨</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">
|
|
<div class="branch-header reveal">
|
|
<div class="branch-icon map">🗺</div>
|
|
<div class="branch-title">
|
|
<h3>Mapping & 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">🌍</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">🚶</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">✂</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">📅</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">📋</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">🇨🇦</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">📆</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> — through video, documentation, landing pages, and more.</p>
|
|
</div>
|
|
|
|
<!-- ====== BRANCH 3: Content & Media ====== -->
|
|
<div class="branch branch-content" data-branch="content">
|
|
<div class="branch-header reveal">
|
|
<div class="branch-icon content">🎬</div>
|
|
<div class="branch-title">
|
|
<h3>Content & 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">📹</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">🎨</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 & 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">📖</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">💻</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">✏</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">📷</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">🎵</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">📲</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">
|
|
<div class="branch-header reveal">
|
|
<div class="branch-icon data">📊</div>
|
|
<div class="branch-title">
|
|
<h3>Data & 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">🗄</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">⚡</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">📦</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">📱</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">⌘</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">⚙</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">
|
|
<div class="branch-header reveal">
|
|
<div class="branch-icon devops">🛡</div>
|
|
<div class="branch-title">
|
|
<h3>DevOps & 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">🌐</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">📈</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">🔏</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">💾</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">🔐</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">👥</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’t sustain itself financially will always depend on someone else’s goodwill. <strong>Own your revenue</strong> the same way you own your data — with tools that put every dollar directly into your cause.</p>
|
|
</div>
|
|
|
|
<!-- ====== BRANCH 6: Fundraising & Commerce ====== -->
|
|
<div class="branch branch-fundraising" data-branch="fundraising">
|
|
<div class="branch-header reveal">
|
|
<div class="branch-icon fundraising">💳</div>
|
|
<div class="branch-title">
|
|
<h3>Fundraising & 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">💰</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">🔄</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">🛒</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">📊</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">📢</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">📝</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">
|
|
<div class="branch-header reveal">
|
|
<div class="branch-icon social">👥</div>
|
|
<div class="branch-title">
|
|
<h3>Social & 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">👥</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">📰</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">🏆</div>
|
|
<h4>Achievements & 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">👫</div>
|
|
<h4>Groups & 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">😍</div>
|
|
<h4>Reactions & 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">
|
|
<div class="branch-header reveal">
|
|
<div class="branch-icon sovereignty">🇨🇦</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">🏠</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">🔓</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">🛡</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">👁</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">🏳️‍🌈</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">✓ Live</div>
|
|
</div>
|
|
</a>
|
|
<a href="https://publicinterestalberta.org/" target="_blank" rel="noopener" class="site-card">
|
|
<div class="site-icon">🏛</div>
|
|
<div>
|
|
<div class="site-name">Public Interest Alberta</div>
|
|
<div class="site-desc">Policy advocacy and democratic engagement</div>
|
|
<div class="site-status">✓ Live</div>
|
|
</div>
|
|
</a>
|
|
<a href="https://freealberta.org/" target="_blank" rel="noopener" class="site-card">
|
|
<div class="site-icon">✊</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">✓ Live</div>
|
|
</div>
|
|
</a>
|
|
<a href="https://lindalindsay.org/" target="_blank" rel="noopener" class="site-card">
|
|
<div class="site-icon">🗳</div>
|
|
<div>
|
|
<div class="site-name">Linda Lindsay</div>
|
|
<div class="site-desc">Progressive political campaign platform</div>
|
|
<div class="site-status">✓ Live</div>
|
|
</div>
|
|
</a>
|
|
<a href="https://albertademocracytaskforce.org/" target="_blank" rel="noopener" class="site-card">
|
|
<div class="site-icon">🤝</div>
|
|
<div>
|
|
<div class="site-name">Alberta Democracy Taskforce</div>
|
|
<div class="site-desc">Defending democratic rights and freedoms</div>
|
|
<div class="site-status">✓ Live</div>
|
|
</div>
|
|
</a>
|
|
<a href="https://bnkops.com/" target="_blank" rel="noopener" class="site-card featured">
|
|
<div class="site-icon">⚡</div>
|
|
<div>
|
|
<div class="site-name">BNKops</div>
|
|
<div class="site-desc">Liberation technology and hosting services</div>
|
|
<div class="site-status">✓ 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 & 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 & 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–$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 →</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">→</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">
|
|
© 2024–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">×</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 — 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">🖥</span>
|
|
<span><strong>A Linux server or hardware</strong> — something to run the stack on (old laptop, mini PC, VPS)</span>
|
|
</li>
|
|
<li>
|
|
<span class="dep-icon">🌐</span>
|
|
<span><strong>An internet connection</strong> — to serve traffic to the public</span>
|
|
</li>
|
|
<li>
|
|
<span class="dep-icon">🏷</span>
|
|
<span><strong>A domain name</strong> — ~$10–15/yr for a custom domain</span>
|
|
</li>
|
|
<li>
|
|
<span class="dep-icon">🔗</span>
|
|
<span><strong>A production URL / tunnel</strong> — for public-facing deployment (can be deployed privately without one)</span>
|
|
</li>
|
|
<li>
|
|
<span class="dep-icon">✉</span>
|
|
<span><strong>An SMTP email provider</strong> — free tiers exist, but the most capable and secure are paid</span>
|
|
</li>
|
|
<li>
|
|
<span class="dep-icon">📱</span>
|
|
<span><strong>An Android phone</strong> — required for SMS campaigns (uses Termux as a bridge to send texts)</span>
|
|
</li>
|
|
<li>
|
|
<span class="dep-icon">💳</span>
|
|
<span><strong>A Stripe account</strong> — 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 — 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(); });
|
|
}
|
|
};
|
|
|
|
/* ===========================================
|
|
BOOT
|
|
=========================================== */
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
ThemeManager.init();
|
|
MobileMenu.init();
|
|
HeaderScroll.init();
|
|
MyceliumAnimator.init();
|
|
ScrollReveal.init();
|
|
RootNetwork.init();
|
|
FloatingElements.init();
|
|
SmoothScroll.init();
|
|
FreeModal.init();
|
|
initSearch();
|
|
});
|
|
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html> |