(() => { const typewordEl = document.getElementById("typeword"); const stepperEl = document.getElementById("stepper"); const revealStepper = createStepperRevealer(stepperEl); if (stepperEl) window.setTimeout(revealStepper, 5200); if (typewordEl) startTypewriter(typewordEl, { onFirstCycleDone: revealStepper }); const quoteTextEl = document.getElementById("quoteText"); const quoteMetaEl = document.getElementById("quoteMeta"); const quoteWrapEl = quoteTextEl?.closest?.(".quote"); if (quoteTextEl && quoteMetaEl && quoteWrapEl) startQuoteTicker({ quoteWrapEl, quoteTextEl, quoteMetaEl }); })(); function createStepperRevealer(stepperEl) { let shown = false; return () => { if (shown || !stepperEl) return; shown = true; stepperEl.hidden = false; window.requestAnimationFrame(() => stepperEl.classList.add("homeStepper--show")); }; } function startTypewriter(typewordEl, opts = {}) { const words = ["Transparent", "Traceable", "Trust", "TTT"]; const typeMs = 42; const deleteMs = 26; const holdMs = 780; const finalHoldMs = 1100; const betweenMs = 180; const onFirstCycleDone = typeof opts.onFirstCycleDone === "function" ? opts.onFirstCycleDone : null; let wordIndex = 0; let charIndex = 0; let isDeleting = false; let cycleCount = 0; const tick = () => { const word = words[wordIndex] || ""; if (!isDeleting) { charIndex = Math.min(word.length, charIndex + 1); } else { charIndex = Math.max(0, charIndex - 1); } typewordEl.textContent = word.slice(0, charIndex); const atEnd = !isDeleting && charIndex === word.length; const atStart = isDeleting && charIndex === 0; if (atEnd) { isDeleting = true; const wait = wordIndex === words.length - 1 ? finalHoldMs : holdMs; window.setTimeout(tick, wait); return; } if (atStart) { isDeleting = false; wordIndex = (wordIndex + 1) % words.length; if (wordIndex === 0) { cycleCount += 1; if (cycleCount === 1 && onFirstCycleDone) onFirstCycleDone(); } window.setTimeout(tick, betweenMs); return; } window.setTimeout(tick, isDeleting ? deleteMs : typeMs); }; tick(); } async function startQuoteTicker({ quoteWrapEl, quoteTextEl, quoteMetaEl }) { const quotes = await loadQuotes(); if (!Array.isArray(quotes) || quotes.length === 0) return; quoteWrapEl.classList.add("quote--show"); let idx = Math.floor(Math.random() * quotes.length); const show = (q) => { quoteWrapEl.classList.remove("quote--show"); quoteWrapEl.classList.add("quote--fade"); window.setTimeout(() => { quoteTextEl.textContent = q.text || ""; renderQuoteMeta({ quoteMetaEl, q }); quoteWrapEl.classList.remove("quote--fade"); quoteWrapEl.classList.add("quote--show"); }, 220); }; const loop = () => { const q = quotes[idx] || {}; show(q); idx = (idx + 1) % quotes.length; const duration = estimateReadMs(q.text || ""); window.setTimeout(loop, duration); }; loop(); } async function loadQuotes() { try { const resp = await fetch(resolveIfTttUrl("assets/ifttt-quotes.json"), { cache: "no-store" }); if (resp.ok) { const data = await resp.json(); if (Array.isArray(data)) return data; } } catch (e) {} return [ { text: "Footnotes aren't decorations. They're load-bearing walls.", source: "IF.Trace paper", href: "https://infrafabric.io/static/hosted/review/ifttt-paper-update/2025-12-28/review-pack.html", }, { text: "If there's no IF.Trace trace, it didn't happen—or shouldn't be trusted.", source: "IF.Trace doctrine", href: "https://infrafabric.io/static/hosted/review/ifttt-paper-update/2025-12-28/review-pack.html", }, { text: "Trust isn't claimed. It's proven.", source: "IF.Trace paper", href: "https://infrafabric.io/static/hosted/review/ifttt-paper-update/2025-12-28/review-pack.html", }, ]; } function resolveIfTttUrl(path) { try { const scriptEl = document.querySelector('script[src$="app.js"]'); const scriptUrl = scriptEl ? new URL(scriptEl.getAttribute("src"), window.location.href) : new URL(window.location.href); return new URL(path, scriptUrl).toString(); } catch (e) { return path; } } function renderQuoteMeta({ quoteMetaEl, q }) { while (quoteMetaEl.firstChild) quoteMetaEl.removeChild(quoteMetaEl.firstChild); const source = String(q.source || "").trim(); const href = String(q.href || "").trim(); if (!source) return; if (href) { const a = document.createElement("a"); a.href = href; a.target = "_blank"; a.rel = "noreferrer"; a.textContent = source; quoteMetaEl.appendChild(a); return; } quoteMetaEl.textContent = source; } function estimateReadMs(text) { const cleaned = String(text || "").trim(); if (!cleaned) return 4000; const words = cleaned.split(/\s+/).filter(Boolean).length; const wpm = 220; const ms = (words / wpm) * 60000 + 1200; return clamp(ms, 3200, 11000); } function clamp(v, min, max) { return Math.max(min, Math.min(max, v)); }