/* Ohiyo pitch deck — navigation + interactions. No dependencies. */ (() => { "use strict"; const $ = (s, r = document) => r.querySelector(s); const slides = [...document.querySelectorAll(".slide")]; const deck = $("#deck"); let i = 0; // ── Build dots + jump menu from slide titles ──────────────────────────── const dotsEl = $("#dots"); const menuList = $("#menuList"); slides.forEach((s, n) => { const d = document.createElement("button"); d.setAttribute("aria-label", s.dataset.title); d.addEventListener("click", () => go(n)); dotsEl.appendChild(d); const li = document.createElement("li"); const b = document.createElement("button"); b.textContent = s.dataset.title; b.addEventListener("click", () => { go(n); closeMenu(); }); li.appendChild(b); menuList.appendChild(li); }); const dots = [...dotsEl.children]; const menuItems = [...menuList.querySelectorAll("button")]; // ── Navigation ────────────────────────────────────────────────────────── function go(n) { n = Math.max(0, Math.min(slides.length - 1, n)); if (n === i) return; slides[i].classList.remove("is-active"); slides[i].classList.toggle("is-prev", n > i); i = n; render(); } const next = () => go(i + 1); const prev = () => go(i - 1); function render() { slides.forEach((s, n) => { s.classList.toggle("is-active", n === i); if (n !== i) s.classList.remove("is-prev"); }); dots.forEach((d, n) => d.classList.toggle("on", n === i)); menuItems.forEach((m, n) => m.classList.toggle("on", n === i)); $("#progress").style.width = ((i + 1) / slides.length) * 100 + "%"; $("#counter").innerHTML = `${String(i + 1).padStart(2, "0")} / ${String(slides.length).padStart(2, "0")}`; $("#hint").style.opacity = i === 0 ? "1" : "0"; } // ── Menu overlay ────────────────────────────────────────────────────────── const menu = $("#menu"); const openMenu = () => menu.classList.add("open"); const closeMenu = () => menu.classList.remove("open"); $("#menuBtn").addEventListener("click", openMenu); $("#menuBtn2").addEventListener("click", openMenu); menu.addEventListener("click", (e) => { if (e.target === menu) closeMenu(); }); // ── Buttons ─────────────────────────────────────────────────────────────── $("#next").addEventListener("click", next); $("#prev").addEventListener("click", prev); $("#startBtn").addEventListener("click", next); $("#restartBtn").addEventListener("click", () => go(0)); // ── Keyboard ────────────────────────────────────────────────────────────── window.addEventListener("keydown", (e) => { if (e.key === "Escape") return closeMenu(); if (e.key === "m" || e.key === "M") return menu.classList.contains("open") ? closeMenu() : openMenu(); if (e.key === "f" || e.key === "F") { document.fullscreenElement ? document.exitFullscreen() : document.documentElement.requestFullscreen?.(); return; } if (["ArrowRight", "ArrowDown", " ", "PageDown"].includes(e.key)) { e.preventDefault(); next(); } else if (["ArrowLeft", "ArrowUp", "PageUp"].includes(e.key)) { e.preventDefault(); prev(); } else if (e.key === "Home") go(0); else if (e.key === "End") go(slides.length - 1); else if (/^[1-9]$/.test(e.key)) go(parseInt(e.key, 10) - 1); }); // Wheel / swipe to advance (debounced). let wheelLock = false; deck.addEventListener("wheel", (e) => { if (Math.abs(e.deltaY) < 24 || wheelLock || menu.classList.contains("open")) return; if (e.target.closest(".panel, .explorer, .cmp")) return; // let inner content scroll wheelLock = true; e.deltaY > 0 ? next() : prev(); setTimeout(() => (wheelLock = false), 700); }, { passive: true }); let touchY = null; deck.addEventListener("touchstart", (e) => (touchY = e.touches[0].clientY), { passive: true }); deck.addEventListener("touchend", (e) => { if (touchY == null) return; const dy = touchY - e.changedTouches[0].clientY; if (Math.abs(dy) > 50) (dy > 0 ? next() : prev()); touchY = null; }, { passive: true }); // ── Verification toggle (slide 3) ───────────────────────────────────────── const sw = $("#verifSwitch"); const cmp = $("#cmp"); const flip = () => { sw.classList.toggle("on"); cmp.classList.toggle("show-verif"); }; sw.addEventListener("click", flip); sw.addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); flip(); } }); // ── Product-line explorer (slide 4) ─────────────────────────────────────── const PILLARS = [ { ic: "🔒", name: "End-to-end encryption", key: "Privacy", desc: "Private chats are sealed on-device. The server routes ciphertext for E2E DMs and group DMs, not readable message bodies.", feats: [ ["Signal Protocol DMs", "X3DH + Double Ratchet via libsignal"], ["Encrypted group DMs", "sender keys, epoch rekey on join/leave"], ["Safety numbers", "verify a contact across all their devices"], ["Encrypted attachments", "client-side AES-GCM before upload"], ["Backup & recovery", "self-custody code, PBKDF2 → AES-256-GCM"], ["Threat model", "public privacy boundaries, no anonymity overclaim"], ], }, { ic: "💬", name: "Communities & messaging", key: "Platform", desc: "Everything a real community needs — and none of it locked behind a subscription.", feats: [ ["Servers, channels, categories", "organize at any scale"], ["DMs & group DMs", "end-to-end encryption, opt-in"], ["Roles & permissions", "granular, per-channel gates"], ["Polls, events, reactions", "@everyone / @here mentions"], ["Pins, saved, forward, drafts", "read receipts too"], ["Rich media embeds", "video previews, direct video links, YouTube iframes"], ], }, { ic: "🎙️", name: "Voice, video & screen-share", key: "Realtime", desc: "Talk, watch, present, and share clips together — the realtime layer is built and still being hardened for launch.", feats: [ ["Mesh + SFU", "WebRTC mesh or LiveKit, chosen at runtime"], ["Up to 4K screen-share", "crisp presenting & pair-work"], ["Media E2EE path", "FrameCryptor integration under live hardening"], ["TURN / STUN", "calls connect across NATs and firewalls"], ["Watch parties", "synced video for the whole room"], ["Video attachments", "inline previews with Range-backed playback"], ], }, { ic: "🛡️", name: "Privacy & safety", key: "Trust", desc: "Designed to forget what it doesn't need, and to defend what it keeps.", feats: [ ["Disappearing messages", "per-channel TTL, swept server-side"], ["Dead-man's switch", "auto-wipe after inactivity (1h–1y)"], ["E2E blind spots", "private chat bodies remain unreadable to the server"], ["Moderation", "hide, report, block, kick, ban, role-gated actions"], ["Rate limiting", "per-IP brute-force + spam throttles"], ["Hardened surface", "CSP, nosniff, signed file URLs, generic errors"], ], }, { ic: "🎨", name: "Make it yours", key: "Design", desc: "Deep customization that's simply free — the things Discord charges Nitro for.", feats: [ ["Themes + editor", "8 built-ins and a build-your-own studio"], ["One-click accents", "9 presets or any custom color"], ["Density & font scale", "tune the whole UI to taste"], ["Profiles", "banner, avatar, bio, pronouns, socials"], ["Welcoming onboarding", "a warm, guided first run"], ["Appearance sync", "your look follows you across devices"], ], }, { ic: "🏠", name: "Self-host & platform", key: "Ops", desc: "A single binary you actually own — production-hardened, not a toy.", feats: [ ["Rust + SQLite", "one axum binary, one database file"], ["React + Tauri desktop", "native mac / windows / linux apps"], ["Instant Servers", "create, sleep, wake, export, graduate, billing handoff"], ["Raw Server Pack", "SQLite snapshot + uploads + signed manifest"], ["Resilient by default", "fail-fast config, graceful shutdown, DB health"], ["Proven quality", "116 server tests · 76 client tests · 27 E2E suites"], ], }, ]; const rail = $("#rail"); const panel = $("#panel"); PILLARS.forEach((p, n) => { const b = document.createElement("button"); b.innerHTML = `${p.ic} ${p.name}`; b.addEventListener("click", () => selectPillar(n)); rail.appendChild(b); }); const railBtns = [...rail.children]; function selectPillar(n) { railBtns.forEach((b, k) => b.classList.toggle("active", k === n)); const p = PILLARS[n]; panel.innerHTML = `

${p.name}

${p.desc}

` + p.feats.map((f) => `
›
${f[0]}${f[1]}
`).join("") + `
`; panel.classList.remove("swap"); void panel.offsetWidth; // restart the swap animation panel.classList.add("swap"); } selectPillar(0); // ── Boot ──────────────────────────────────────────────────────────────── slides[0].classList.add("is-active"); render(); })();