diff --git a/mkdocs/docs/overrides/lander.html b/mkdocs/docs/overrides/lander.html index 9fbda767..8858003a 100644 --- a/mkdocs/docs/overrides/lander.html +++ b/mkdocs/docs/overrides/lander.html @@ -92,6 +92,10 @@ /* ============================================ TYPOGRAPHY ============================================ */ + html { + overflow-x: hidden; + } + body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; color: var(--text-primary); @@ -482,7 +486,7 @@ flex-direction: column; align-items: center; justify-content: flex-start; - padding: calc(var(--header-height) + 3rem) 2rem 0; + padding: calc(var(--header-height) + 2rem) 2rem 0; overflow: hidden; } @@ -510,6 +514,22 @@ background: radial-gradient(circle, rgba(111, 66, 193, 0.08) 0%, rgba(139, 92, 246, 0.03) 40%, transparent 70%); } + [data-theme="light"] .showcase-card { + border-color: rgba(100, 116, 139, 0.2); + } + [data-theme="light"] .showcase-card.active { + box-shadow: 0 0 30px rgba(111, 66, 193, 0.08), 0 8px 32px rgba(0, 0, 0, 0.1); + border-color: rgba(111, 66, 193, 0.25); + } + [data-theme="light"] .hero-stats { + background: rgba(255, 255, 255, 0.9); + border-color: rgba(100, 116, 139, 0.15); + } + [data-theme="light"] .hero-pill { + background: rgba(111, 66, 193, 0.06); + border-color: rgba(111, 66, 193, 0.15); + } + .hero-root-svg { position: absolute; top: 65%; @@ -526,11 +546,38 @@ to { opacity: 1; } } + /* ---- Two-column hero grid (desktop) ---- */ .hero-content { position: relative; z-index: 1; - text-align: center; - max-width: 800px; + width: 100%; + max-width: 1400px; + display: grid; + grid-template-columns: minmax(380px, 1fr) minmax(0, 1.4fr); + grid-template-rows: auto auto; + gap: 0 2.5rem; + align-items: start; + } + + .hero-left { + text-align: left; + padding-top: 1.5rem; + } + + .hero-right { + display: flex; + align-items: flex-start; + justify-content: flex-end; + padding-top: 0.5rem; + } + .hero-right > div { + width: 100%; + } + + /* Spans full width below both columns */ + .hero-bottom { + grid-column: 1 / -1; + margin-top: 1.5rem; } .hero-badge { @@ -543,38 +590,388 @@ font-size: 0.8rem; font-weight: 600; letter-spacing: 0.02em; + margin-bottom: 1rem; + } + + .beta-pill { + display: inline-block; + padding: 0.35rem 1.1rem; + background: linear-gradient(135deg, #EC4899, #8B5CF6); + border: none; + border-radius: 100px; + color: #fff; + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.04em; + cursor: pointer; + margin-bottom: 0.75rem; + box-shadow: 0 0 16px rgba(236, 72, 153, 0.4), 0 0 40px rgba(139, 92, 246, 0.2); + transition: transform 0.2s ease, box-shadow 0.2s ease; + animation: beta-pulse 3s ease-in-out infinite; + } + .beta-pill:hover { + transform: scale(1.06); + box-shadow: 0 0 22px rgba(236, 72, 153, 0.55), 0 0 50px rgba(139, 92, 246, 0.35); + } + @keyframes beta-pulse { + 0%, 100% { box-shadow: 0 0 16px rgba(236, 72, 153, 0.4), 0 0 40px rgba(139, 92, 246, 0.2); } + 50% { box-shadow: 0 0 24px rgba(236, 72, 153, 0.6), 0 0 55px rgba(139, 92, 246, 0.35); } + } + + .beta-modal-backdrop { + display: none; + position: fixed; + inset: 0; + z-index: 9999; + background: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); + align-items: center; + justify-content: center; + } + .beta-modal-backdrop.open { display: flex; } + .beta-modal { + background: var(--bg-surface); + border: 1px solid var(--myc-node-border); + border-radius: var(--radius-lg); + max-width: 440px; + width: 90%; + padding: 2.5rem 2rem 2rem; + text-align: center; + box-shadow: var(--shadow-lg), 0 0 60px rgba(139, 92, 246, 0.15); + position: relative; + animation: beta-modal-in 0.25s ease-out; + } + @keyframes beta-modal-in { + from { opacity: 0; transform: scale(0.92) translateY(12px); } + to { opacity: 1; transform: scale(1) translateY(0); } + } + .beta-modal-emoji { font-size: 2.5rem; margin-bottom: 1rem; } + .beta-modal h3 { + font-size: 1.25rem; + color: var(--text-primary); + margin-bottom: 0.75rem; + } + .beta-modal p { + color: var(--text-secondary); + font-size: 0.95rem; + line-height: 1.7; margin-bottom: 1.5rem; } + .beta-modal-close { + display: inline-block; + padding: 0.5rem 1.5rem; + background: linear-gradient(135deg, #8B5CF6, #6f42c1); + border: none; + border-radius: 100px; + color: #fff; + font-size: 0.85rem; + font-weight: 600; + cursor: pointer; + transition: transform 0.15s ease, box-shadow 0.15s ease; + } + .beta-modal-close:hover { + transform: scale(1.04); + box-shadow: 0 0 14px rgba(139, 92, 246, 0.4); + } .hero h1 { background: linear-gradient(135deg, var(--primary-light) 0%, #C084FC 50%, #F5A9B8 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; + margin-bottom: 0.5rem; + font-size: clamp(2.5rem, 5vw, 3.75rem); + } + + /* ---- Typewriter rotating line ---- */ + .hero-rotating-line { + font-size: clamp(1.15rem, 2.2vw, 1.5rem); + color: var(--text-secondary); margin-bottom: 1.25rem; + min-height: 2.2em; + line-height: 1.5; + } + .hero-rotating-line .tw-static { + color: var(--text-secondary); + } + .hero-rotating-line .tw-word { + color: var(--primary-light); + font-weight: 700; + border-right: 2px solid var(--primary-light); + padding-right: 2px; + animation: blink-cursor 0.75s step-end infinite; + } + @keyframes blink-cursor { + 50% { border-color: transparent; } } .hero-subtitle { color: var(--text-secondary); - font-size: clamp(1rem, 2vw, 1.2rem); - max-width: 640px; - margin: 0 auto 2rem; + font-size: clamp(0.95rem, 1.8vw, 1.1rem); + max-width: 540px; + margin: 0 0 1.5rem; line-height: 1.7; } .hero-cta { display: flex; - gap: 1rem; - justify-content: center; + gap: 0.75rem; flex-wrap: wrap; - margin-bottom: 2.5rem; + margin-bottom: 1.5rem; } - /* Search */ - .hero-search { + /* ---- Feature pills ---- */ + .hero-pills { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 1.5rem; + } + .hero-pill { + display: inline-flex; + align-items: center; + gap: 0.35rem; + padding: 0.3rem 0.75rem; + background: var(--myc-node-bg); + border: 1px solid var(--myc-node-border); + border-radius: 100px; + color: var(--text-secondary); + font-size: 0.72rem; + font-weight: 500; + letter-spacing: 0.01em; + opacity: 0; + transform: translateY(8px); + transition: opacity 0.4s ease, transform 0.4s ease, background 0.2s ease; + } + .hero-pill.visible { + opacity: 1; + transform: translateY(0); + } + .hero-pill:hover { + background: rgba(139, 92, 246, 0.15); + color: var(--primary-light); + text-decoration: none; + } + .hero-pill .pill-icon { + font-size: 0.82rem; + line-height: 1; + } + + /* ---- Feature showcase (right column) ---- */ + .hero-showcase { position: relative; - max-width: 520px; - margin: 0 auto 3rem; + width: 100%; + aspect-ratio: 16 / 9.5; + perspective: 1200px; + } + + .showcase-card { + position: absolute; + inset: 0; + border: 1px solid var(--border-color); + border-radius: var(--radius-lg); + display: flex; + flex-direction: column; + opacity: 0; + transform: rotateY(8deg) translateX(30px) scale(0.95); + transition: opacity 0.6s ease, transform 0.6s ease; + pointer-events: none; + overflow: hidden; + } + .showcase-card.active { + opacity: 1; + transform: rotateY(0deg) translateX(0) scale(1); + pointer-events: auto; + border-color: var(--myc-node-border); + box-shadow: 0 0 30px rgba(139, 92, 246, 0.12), var(--shadow-lg); + } + .showcase-card.exiting { + opacity: 0; + transform: rotateY(-8deg) translateX(-30px) scale(0.95); + } + + /* Screenshot image fills the card */ + .showcase-card-img { + width: 100%; + height: 100%; + object-fit: cover; + object-position: top left; + display: block; + } + + /* Caption overlay at bottom */ + .showcase-card-caption { + position: absolute; + bottom: 0; + left: 0; + right: 0; + padding: 2.5rem 1.25rem 1rem; + background: linear-gradient(to top, rgba(15, 23, 42, 0.95) 0%, rgba(15, 23, 42, 0.75) 50%, transparent 100%); + display: flex; + align-items: flex-end; + gap: 0.75rem; + } + [data-theme="light"] .showcase-card-caption { + background: linear-gradient(to top, rgba(255, 255, 255, 0.97) 0%, rgba(255, 255, 255, 0.8) 50%, transparent 100%); + } + .showcase-card-icon { + width: 36px; + height: 36px; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.1rem; + flex-shrink: 0; + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); + } + .showcase-card-title { + font-size: 0.95rem; + font-weight: 700; + color: #F1F5F9; + line-height: 1.3; + } + [data-theme="light"] .showcase-card-title { + color: #0F172A; + } + .showcase-card-subtitle { + font-size: 0.72rem; + color: #94A3B8; + margin-top: 0.1rem; + } + [data-theme="light"] .showcase-card-subtitle { + color: #64748B; + } + + /* Showcase progress dots */ + .showcase-dots { + display: flex; + justify-content: center; + gap: 0.5rem; + margin-top: 0.75rem; + } + .showcase-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--text-muted); + opacity: 0.3; + cursor: pointer; + transition: opacity 0.3s ease, transform 0.3s ease, background 0.3s ease; + } + .showcase-dot.active { + opacity: 1; + background: var(--primary-light); + transform: scale(1.3); + } + .showcase-dot:hover { + opacity: 0.7; + } + + /* ---- Particle canvas ---- */ + .hero-particles { + position: absolute; + inset: 0; + z-index: 0; + pointer-events: none; + } + + /* ---- Quick deploy terminal ---- */ + .hero-terminal { + margin: 0.75rem 0 0; + background: #0D1117; + border: 1px solid rgba(139, 92, 246, 0.2); + border-radius: var(--radius); + opacity: 0; + transform: translateY(8px); + animation: fadeSlideIn 0.6s ease 1.4s forwards; + } + @keyframes fadeSlideIn { + to { opacity: 1; transform: translateY(0); } + } + [data-theme="light"] .hero-terminal { + background: #1E293B; + } + .hero-terminal-bar { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 0.85rem; + background: rgba(255, 255, 255, 0.04); + border-bottom: 1px solid rgba(255, 255, 255, 0.06); + border-radius: var(--radius) var(--radius) 0 0; + } + .hero-terminal-dot { + width: 8px; + height: 8px; + border-radius: 50%; + } + .hero-terminal-title { + font-size: 0.65rem; + color: #64748B; + font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace; + margin-left: auto; + } + .hero-terminal-body { + padding: 0.6rem 0.85rem; + display: flex; + align-items: center; + gap: 0.5rem; + } + .hero-terminal-prompt { + color: #34D399; + font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace; + font-size: 0.82rem; + font-weight: 600; + flex-shrink: 0; + user-select: none; + } + .hero-terminal-cmd { + color: #E2E8F0; + font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace; + font-size: 0.82rem; + white-space: nowrap; + overflow-x: auto; + overflow-y: hidden; + flex: 1; + min-width: 0; + scrollbar-width: none; + -ms-overflow-style: none; + } + .hero-terminal-cmd::-webkit-scrollbar { display: none; } + .hero-terminal-cmd .cmd-highlight { + color: var(--primary-light); + } + .hero-terminal-copy { + background: none; + border: 1px solid rgba(148, 163, 184, 0.2); + border-radius: 6px; + color: #94A3B8; + font-size: 0.7rem; + padding: 0.25rem 0.55rem; + cursor: pointer; + font-family: inherit; + transition: color 0.2s ease, border-color 0.2s ease, background 0.2s ease; + flex-shrink: 0; + display: flex; + align-items: center; + gap: 0.3rem; + } + .hero-terminal-copy:hover { + color: #E2E8F0; + border-color: rgba(148, 163, 184, 0.4); + background: rgba(255, 255, 255, 0.05); + } + .hero-terminal-copy.copied { + color: #34D399; + border-color: rgba(52, 211, 153, 0.3); + } + + /* Search (moved out of hero, kept for search functionality) */ + .hero-search { + display: none; } .search-input-wrap { @@ -699,8 +1096,14 @@ display: grid; grid-template-columns: repeat(4, 1fr); gap: 1.5rem; - max-width: 640px; + max-width: 800px; margin: 0 auto; + padding: 1.25rem 2rem; + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: var(--radius); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); } .hero-stat { @@ -708,9 +1111,10 @@ } .hero-stat-value { - font-size: 1.5rem; + font-size: 1.6rem; font-weight: 800; color: var(--primary-light); + font-variant-numeric: tabular-nums; } .hero-stat-label { @@ -719,6 +1123,24 @@ margin-top: 0.25rem; } + .hero-trust { + text-align: center; + margin-top: 1rem; + color: var(--text-muted); + font-size: 0.78rem; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + opacity: 0; + animation: fadeIn 1s ease 1.5s forwards; + } + .hero-trust svg { + width: 14px; + height: 14px; + color: var(--primary-light); + } + /* ============================================ PROBLEMS SECTION ============================================ */ @@ -1746,16 +2168,41 @@ @media (max-width: 768px) { .nav-links { display: none; } .nav-right .btn-primary { display: none; } + .nav-right .btn-demo { display: none; } .hamburger { display: block; } .logo-tagline { display: none; } .section { padding: 4rem 0; } - .hero { min-height: auto; padding-top: calc(var(--header-height) + 2rem); padding-bottom: 3rem; } + .hero { min-height: auto; padding-top: calc(var(--header-height) + 1.5rem); padding-bottom: 1.5rem; overflow-x: hidden; } .hero h1 { font-size: 2rem; } .hero-root-glow { width: 250px; height: 250px; top: 50%; bottom: auto; } .hero-root-svg { top: 50%; bottom: auto; transform: translate(-50%, -50%); width: 250px; height: 250px; } + .hero-content { + display: flex; + flex-direction: column; + max-width: 100%; + width: 100%; + overflow: hidden; + } + .hero-left { text-align: center; padding-top: 0; } + .hero-right { margin-top: 1.5rem; overflow: hidden; width: 100%; } + .hero-right > div { max-width: 100%; overflow: hidden; width: 100%; } + .hero-showcase { width: 100%; max-width: 100%; } + .hero-terminal { width: 100%; max-width: 100%; box-sizing: border-box; } + .hero-showcase { aspect-ratio: 16 / 10; } + .hero-cta { justify-content: center; } + .hero-pills { justify-content: center; } + .hero-terminal { max-width: 100%; overflow: hidden; } + .hero-terminal-body { padding: 0.5rem 0.7rem; gap: 0.4rem; } + .hero-terminal-prompt { font-size: 0.7rem; } + .hero-terminal-cmd { font-size: 0.65rem; } + .hero-terminal-copy { font-size: 0.62rem; padding: 0.2rem 0.4rem; } + .hero-terminal-copy .copy-label { display: none; } + .hero-rotating-line { text-align: center; } + .hero-subtitle { margin-left: auto; margin-right: auto; } + .hero-stats { grid-template-columns: repeat(2, 1fr); gap: 1rem; @@ -1798,6 +2245,8 @@ flex-direction: column; align-items: center; } + .hero-pills { gap: 0.4rem; } + .hero-pill { font-size: 0.68rem; padding: 0.25rem 0.6rem; } .cta-buttons { flex-direction: column; @@ -1818,7 +2267,15 @@ .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); } + .hero-stats { grid-template-columns: repeat(2, 1fr); padding: 1rem; } + .hero-showcase { aspect-ratio: 16 / 11; } + .showcase-card-title { font-size: 0.85rem; } + .showcase-card-subtitle { font-size: 0.65rem; } + .showcase-card-caption { padding: 1.5rem 1rem 0.75rem; } + .hero-terminal-cmd { font-size: 0.58rem; } + .hero-terminal-prompt { font-size: 0.6rem; } + .hero-terminal-title { font-size: 0.55rem; } + .hero-terminal-dot { width: 6px; height: 6px; } .live-stats { grid-template-columns: 1fr; } } @@ -2102,72 +2559,175 @@ HERO ============================================ -->
+ + +
-
-
Self-Hosted Campaign Infrastructure
-

Grow Power.
Don't Rent It.

-

- 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 free* and open source toolkit built for growing political movements. -

-
- Explore the Demo - Schedule a Chat - Source Code + +
+ +
Self-Hosted Campaign Infrastructure
+

Grow Power.
Don't Rent It.

+ +
+ Your — on your own infrastructure. +
+ +

+ No corporate surveillance. No foreign interference. No monthly ransoms. + A free* and open source toolkit built for growing political movements. +

+ + + + + +
- -
+ +
+
+
🚧
+

Now in Beta

+

Changemaker Lite is under active development and breaking changes are still being pushed. A stable release is expected by June 2026.

+ +
+
+ @@ -2274,7 +2834,7 @@ -
+
📨
@@ -2363,7 +2923,7 @@
-
+
🗺
@@ -2448,7 +3008,7 @@
-
+
🎬
@@ -2537,7 +3097,7 @@
-
+
📊
@@ -2608,7 +3168,7 @@
-
+
🛡
@@ -2684,7 +3244,7 @@
-
+
💳
@@ -2755,7 +3315,7 @@
-
+
@@ -2817,7 +3377,7 @@
-
+
🇨🇦
@@ -3851,6 +4411,275 @@ } }; + /* =========================================== + BETA MODAL + =========================================== */ + const BetaModal = { + init() { + const pill = document.getElementById('beta-pill'); + const backdrop = document.getElementById('beta-modal-backdrop'); + const closeBtn = document.getElementById('beta-modal-close'); + if (!pill || !backdrop || !closeBtn) return; + pill.addEventListener('click', () => backdrop.classList.add('open')); + closeBtn.addEventListener('click', () => backdrop.classList.remove('open')); + backdrop.addEventListener('click', (e) => { if (e.target === backdrop) backdrop.classList.remove('open'); }); + document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && backdrop.classList.contains('open')) backdrop.classList.remove('open'); }); + } + }; + + /* =========================================== + TERMINAL COPY — clipboard for install cmd + =========================================== */ + const TerminalCopy = { + init() { + const btn = document.getElementById('hero-copy-btn'); + if (!btn) return; + btn.addEventListener('click', () => { + const cmd = 'curl -fsSL gitea.bnkops.com/admin/changemaker.lite/raw/branch/main/scripts/install.sh | bash'; + navigator.clipboard.writeText(cmd).then(() => { + btn.classList.add('copied'); + btn.querySelector('.copy-label').textContent = 'Copied!'; + setTimeout(() => { + btn.classList.remove('copied'); + btn.querySelector('.copy-label').textContent = 'Copy'; + }, 2000); + }); + }); + } + }; + + /* =========================================== + TYPEWRITER — rotating hero words + =========================================== */ + const Typewriter = { + words: ['campaigns', 'canvassing', 'fundraising', 'newsletters', 'media library', 'volunteer shifts', 'team chat', 'events'], + el: null, + wordIdx: 0, + charIdx: 0, + isDeleting: false, + timeout: null, + + init() { + this.el = document.getElementById('tw-word'); + if (!this.el) return; + if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { + this.el.textContent = this.words[0]; + return; + } + this.tick(); + }, + + tick() { + const word = this.words[this.wordIdx]; + + if (this.isDeleting) { + this.charIdx--; + } else { + this.charIdx++; + } + + this.el.textContent = word.substring(0, this.charIdx); + + let delay = this.isDeleting ? 40 : 80; + + if (!this.isDeleting && this.charIdx === word.length) { + delay = 2200; + this.isDeleting = true; + } else if (this.isDeleting && this.charIdx === 0) { + this.isDeleting = false; + this.wordIdx = (this.wordIdx + 1) % this.words.length; + delay = 400; + } + + this.timeout = setTimeout(() => this.tick(), delay); + } + }; + + /* =========================================== + FEATURE PILLS — staggered entrance + =========================================== */ + const FeaturePills = { + init() { + const pills = document.querySelectorAll('.hero-pill'); + if (!pills.length) return; + if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { + pills.forEach(p => p.classList.add('visible')); + return; + } + pills.forEach((pill, i) => { + setTimeout(() => pill.classList.add('visible'), 600 + i * 120); + }); + } + }; + + /* =========================================== + FEATURE SHOWCASE — rotating cards + =========================================== */ + const FeatureShowcase = { + current: 0, + cards: [], + dots: [], + interval: null, + DURATION: 5000, + + init() { + this.cards = document.querySelectorAll('.showcase-card'); + this.dots = document.querySelectorAll('.showcase-dot'); + if (this.cards.length < 2) return; + + this.dots.forEach(dot => { + dot.addEventListener('click', () => { + const idx = parseInt(dot.getAttribute('data-idx'), 10); + if (idx !== this.current) this.goTo(idx); + }); + }); + + this.startAutoplay(); + }, + + goTo(idx) { + const prev = this.cards[this.current]; + prev.classList.remove('active'); + prev.classList.add('exiting'); + setTimeout(() => prev.classList.remove('exiting'), 600); + + this.current = idx; + this.cards[this.current].classList.add('active'); + + this.dots.forEach((d, i) => d.classList.toggle('active', i === idx)); + this.restartAutoplay(); + }, + + next() { + this.goTo((this.current + 1) % this.cards.length); + }, + + startAutoplay() { + this.interval = setInterval(() => this.next(), this.DURATION); + }, + + restartAutoplay() { + clearInterval(this.interval); + this.startAutoplay(); + } + }; + + /* =========================================== + COUNT-UP STATS — animated number counters + =========================================== */ + const CountUpStats = { + init() { + const stats = document.querySelectorAll('.hero-stat-value[data-count]'); + if (!stats.length) return; + if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { + stats.forEach(el => { + const target = parseInt(el.getAttribute('data-count'), 10); + const prefix = el.getAttribute('data-prefix') || ''; + const suffix = el.getAttribute('data-suffix') || ''; + el.textContent = prefix + target + suffix; + }); + return; + } + + const obs = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + this.animate(entry.target); + obs.unobserve(entry.target); + } + }); + }, { threshold: 0.5 }); + + stats.forEach(el => obs.observe(el)); + }, + + animate(el) { + const target = parseInt(el.getAttribute('data-count'), 10); + const prefix = el.getAttribute('data-prefix') || ''; + const suffix = el.getAttribute('data-suffix') || ''; + const duration = 1400; + const start = performance.now(); + + const step = (now) => { + const progress = Math.min((now - start) / duration, 1); + const eased = 1 - Math.pow(1 - progress, 3); + const current = Math.round(eased * target); + el.textContent = prefix + current + suffix; + if (progress < 1) requestAnimationFrame(step); + }; + requestAnimationFrame(step); + } + }; + + /* =========================================== + PARTICLE DRIFT — floating background dots + =========================================== */ + const ParticleDrift = { + canvas: null, + ctx: null, + particles: [], + raf: null, + COUNT: 35, + + init() { + this.canvas = document.getElementById('hero-particles'); + if (!this.canvas) return; + if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; + // Fewer particles on mobile + if (window.innerWidth < 768) this.COUNT = 15; + + this.ctx = this.canvas.getContext('2d'); + this.resize(); + this.createParticles(); + this.loop(); + window.addEventListener('resize', () => this.resize()); + }, + + resize() { + const hero = this.canvas.parentElement; + this.canvas.width = hero.offsetWidth; + this.canvas.height = hero.offsetHeight; + }, + + createParticles() { + this.particles = []; + for (let i = 0; i < this.COUNT; i++) { + this.particles.push({ + x: Math.random() * this.canvas.width, + y: Math.random() * this.canvas.height, + r: Math.random() * 2 + 0.5, + vx: (Math.random() - 0.5) * 0.3, + vy: -(Math.random() * 0.4 + 0.1), + alpha: Math.random() * 0.3 + 0.1, + }); + } + }, + + loop() { + const { ctx, canvas, particles } = this; + ctx.clearRect(0, 0, canvas.width, canvas.height); + + const isDark = document.documentElement.getAttribute('data-theme') !== 'light'; + const baseColor = isDark ? '139, 92, 246' : '111, 66, 193'; + + particles.forEach(p => { + p.x += p.vx; + p.y += p.vy; + // Wrap around + if (p.y < -5) { p.y = canvas.height + 5; p.x = Math.random() * canvas.width; } + if (p.x < -5) p.x = canvas.width + 5; + if (p.x > canvas.width + 5) p.x = -5; + + ctx.beginPath(); + ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2); + ctx.fillStyle = `rgba(${baseColor}, ${p.alpha})`; + ctx.fill(); + }); + + this.raf = requestAnimationFrame(() => this.loop()); + } + }; + /* =========================================== BOOT =========================================== */ @@ -3864,7 +4693,14 @@ FloatingElements.init(); SmoothScroll.init(); FreeModal.init(); + BetaModal.init(); initSearch(); + TerminalCopy.init(); + Typewriter.init(); + FeaturePills.init(); + FeatureShowcase.init(); + CountUpStats.init(); + ParticleDrift.init(); }); })();