// Shared building blocks: buttons, badges, dividers, countdown, reveal // Reveal: IntersectionObserver-driven fade-up. // Uses a CSS keyframe animation triggered by adding the .is-in class. // IMPORTANT: when document.hidden is true (e.g. preview iframe not in foreground) // CSS animations and transitions are paused — so we fall back to instant-show. const Reveal = ({ children, delay = 0, y = 40, className = "", once = true }) => { const ref = React.useRef(null); const [shown, setShown] = React.useState(false); const [docVisible, setDocVisible] = React.useState(() => typeof document === 'undefined' ? true : !document.hidden ); // Track visibility so we switch from instant-show to animated reveal when the // tab/iframe becomes visible. React.useEffect(() => { const onChange = () => setDocVisible(!document.hidden); document.addEventListener('visibilitychange', onChange); return () => document.removeEventListener('visibilitychange', onChange); }, []); React.useEffect(() => { const el = ref.current; if (!el) return; let timer = null; let io = null; const reveal = () => { timer = setTimeout(() => setShown(true), Math.max(20, delay * 1000)); }; const vh = window.innerHeight || document.documentElement.clientHeight || 800; const rect = el.getBoundingClientRect(); if (rect.top < vh * 1.0 && rect.bottom > 0) { reveal(); if (once) return () => { if (timer) clearTimeout(timer); }; } io = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { reveal(); if (once && io) io.unobserve(entry.target); } else if (!once) { setShown(false); } }); }, { threshold: 0, rootMargin: '0px 0px -5% 0px' } ); io.observe(el); return () => { if (timer) clearTimeout(timer); if (io) io.disconnect(); }; }, [once, delay]); // When the document is hidden (preview iframes etc.) CSS animations don't run, // so the reveal would stay invisible. Render plain children with no animation. if (!docVisible) { return
{children}
; } return (
{children}
); }; // Big monumental CTA — solid crimson, hover gold const PrimaryCTA = ({ href = "#", children, large = false, icon = null, ...rest }) => ( {/* corner accents */} {icon} {children} ); // Outlined gold CTA const GhostCTA = ({ href = "#", children, large = false, icon = null, ...rest }) => ( {icon} {children} ); // Ornamental divider with center label const OrnamentDivider = ({ label = null, className = "" }) => (
{label && ( <>
{label}
)} {!label &&
}
); // Section heading: kicker + display title const SectionHeading = ({ kicker, title, subtitle, align = "center" }) => (
{kicker && (
{kicker}
)}

{title}

{subtitle && (

{subtitle}

)}
); // Pull-quote between sections — gold, large, monumental, i18n-aware const PullQuote = ({ tKey, children }) => { const ctx = React.useContext(LangContext); const text = tKey ? ctx.t(tKey) : children; return (
"{text}"
); }; // ============================================================ // COUNTDOWN — Next Sunday 17:00 (America/Sao_Paulo) // ============================================================ function nextSundayAt17BRT() { // BRT is UTC-3, no DST const now = new Date(); // Get current time in São Paulo by offsetting from UTC const utcMs = now.getTime(); const spOffsetMs = -3 * 60 * 60 * 1000; // BRT = UTC-3 const sp = new Date(utcMs + spOffsetMs); // sp's UTC fields represent São Paulo wall-clock const dow = sp.getUTCDay(); // 0=Sun let daysUntilSun = (7 - dow) % 7; // Construct candidate Sunday 17:00 in SP wall-clock const candidateSp = new Date(Date.UTC(sp.getUTCFullYear(), sp.getUTCMonth(), sp.getUTCDate() + daysUntilSun, 17, 0, 0)); // If today is Sunday but past 17:00, push +7 if (candidateSp.getTime() - spOffsetMs <= utcMs) { candidateSp.setUTCDate(candidateSp.getUTCDate() + 7); } // Convert back to real UTC instant return candidateSp.getTime() - spOffsetMs; } const Countdown = () => { const [target] = React.useState(() => nextSundayAt17BRT()); const [now, setNow] = React.useState(() => Date.now()); const ctx = React.useContext(LangContext); const tt = ctx ? ctx.t : (k) => k; React.useEffect(() => { const t = setInterval(() => setNow(Date.now()), 1000); return () => clearInterval(t); }, []); const diff = Math.max(0, target - now); const days = Math.floor(diff / 86400000); const hours = Math.floor((diff % 86400000) / 3600000); const minutes = Math.floor((diff % 3600000) / 60000); const seconds = Math.floor((diff % 60000) / 1000); const pad = (n) => String(n).padStart(2, '0'); const units = [ { label: tt('cd_days'), value: pad(days) }, { label: tt('cd_hours'), value: pad(hours) }, { label: tt('cd_minutes'), value: pad(minutes) }, { label: tt('cd_seconds'), value: pad(seconds) }, ]; return (
{units.map((u, i) => (
{/* Card */}
{/* corner ticks */} {/* horizontal seam */}
{u.value}
{u.label}
{i < units.length - 1 && (
:
)} ))}
); }; Object.assign(window, { Reveal, PrimaryCTA, GhostCTA, OrnamentDivider, SectionHeading, PullQuote, Countdown, });