// ─── ¡eso! site shared building blocks ──────────────────────
// Identity tokens, ExclamMark, BleedMarks, photo helpers, scroll-reveal hook.
const ESO = {
cream: '#FBF7EC', paper: '#F1ECDA', ink: '#1F1A17',
red: '#D24B3F', lime: '#D4E04A', muted: '#6B6258',
rule: 'rgba(31, 26, 23, 0.18)',
};
// ── ExclamMark ─────────────────────────────────────────────
// `which` = "open" → ¡, "close" → !
function ExclamMark({ which = 'close', size = 80, color = ESO.red, style = {} }) {
const w = size * 0.20;
const h = size;
const stem = h * 0.62;
const dotR = w * 0.34;
const dotY = which === 'open' ? h * 0.16 : h * 0.84;
const stemY = which === 'open' ? h * 0.30 : 0;
return (
);
}
// ── BleedMarks ─────────────────────────────────────────────
// ¡ top-left + ! bottom-right, split half-off the page edge
function BleedMarks({
show = true, color = ESO.lime, size = 320, bleed = 0.5, zIndex = 1,
} = {}) {
if (!show) return null;
const w = size * 0.20;
const offX = -w * bleed;
return (
);
}
// ── CoachPhoto ─────────────────────────────────────────────
// The Jorge & Ariana photo — two crops:
// "wide" → both coaches, full-bleed strip with parallax-ready ratio
// "ariana" / "jorge" → single face crop
const PHOTO_SRC = 'website-assets/jorge_ariana.png';
function CoachPhoto({ crop = 'wide', height = 360, parallaxY = 0 }) {
// 1920x1080 source. Approx face centers in %:
// Ariana ~ (24%, 33%), Jorge ~ (75%, 32%)
const cropMap = {
wide: { bgSize: 'cover', bgPos: '50% 28%' },
ariana: { bgSize: '260% auto', bgPos: '24% 33%' },
jorge: { bgSize: '260% auto', bgPos: '75% 32%' },
};
const c = cropMap[crop] || cropMap.wide;
return (
{/* Subtle vignette to anchor type when overlaid */}
);
}
// ── useReveal ──────────────────────────────────────────────
// IntersectionObserver hook — adds .in to elements w/ .reveal once they enter view.
function useRevealOnScroll() {
React.useEffect(() => {
const els = document.querySelectorAll('.reveal');
if (!els.length) return;
const io = new IntersectionObserver((entries) => {
entries.forEach((e) => {
if (e.isIntersecting) {
e.target.classList.add('in');
io.unobserve(e.target);
}
});
}, { threshold: 0.12, rootMargin: '0px 0px -8% 0px' });
els.forEach((el) => io.observe(el));
return () => io.disconnect();
});
}
// ── useScrollY ─────────────────────────────────────────────
// Light-weight scroll position hook for parallax. Throttled via rAF.
function useScrollY() {
const [y, setY] = React.useState(0);
React.useEffect(() => {
let raf = 0;
const onScroll = () => {
if (raf) return;
raf = requestAnimationFrame(() => {
setY(window.scrollY || 0);
raf = 0;
});
};
window.addEventListener('scroll', onScroll, { passive: true });
onScroll();
return () => {
window.removeEventListener('scroll', onScroll);
if (raf) cancelAnimationFrame(raf);
};
}, []);
return y;
}
Object.assign(window, {
ESO, ExclamMark, BleedMarks, CoachPhoto,
useRevealOnScroll, useScrollY,
});