/* global React */ /* ============================================================ Primitives — typographic + atmospheric building blocks. ============================================================ */ const { useEffect, useRef, useState, useContext, createContext, useMemo, useCallback } = React; /* ----- Tweaks context — pages read live values from here ----- */ const TweaksContext = createContext({}); const useTw = () => useContext(TweaksContext); /* ----- Router context (set by router.jsx) ----- */ const RouterContext = createContext({ path: '/', go: () => {} }); const useRouter = () => useContext(RouterContext); /* ============================================================ Eyebrow — the structural ALL CAPS label ============================================================ */ function Eyebrow({ children, style, color }) { return (
{children}
); } /* ============================================================ Display number — used for stats, indices ============================================================ */ function Idx({ children, style }) { return ( {children} ); } /* ============================================================ Button — primary gold, ghost outline, text-only. No bounce. Hover slides the arrow 3px. ============================================================ */ function Button({ variant = 'primary', children, icon = 'arrow-right', onClick, style, href, type = 'button', size = 'md', noIcon = false }) { const router = useRouter(); const [hover, setHover] = useState(false); const handleClick = (e) => { if (href && href.startsWith('/')) { e.preventDefault(); router.go(href); } onClick && onClick(e); }; const sizes = { sm: { padding: '10px 18px', fontSize: 12 }, md: { padding: '14px 26px', fontSize: 13 }, lg: { padding: '18px 32px', fontSize: 14 }, }; const base = { fontFamily: 'var(--font-sans)', fontWeight: 500, cursor: 'pointer', borderRadius: 2, letterSpacing: '0.04em', textTransform: 'uppercase', transition: 'background 220ms var(--ease-out), border-color 220ms var(--ease-out), color 220ms var(--ease-out), box-shadow 220ms var(--ease-out)', display: 'inline-flex', alignItems: 'center', gap: 10, border: '1px solid transparent', textDecoration: 'none', ...sizes[size], }; const variants = { primary: { background: 'var(--c-glow-500)', color: 'var(--c-imperial-indigo)', borderColor: 'var(--c-glow-500)', }, ghost: { background: 'transparent', color: 'var(--c-steel-100)', borderColor: 'var(--line-strong)', }, text: { background: 'transparent', color: 'var(--c-steel-100)', padding: '6px 0', borderRadius: 0, border: 0, borderBottom: '1px solid transparent', letterSpacing: '0.18em', fontSize: 11, }, dark: { background: 'var(--c-nightfall)', color: 'var(--c-steel-100)', borderColor: 'var(--line-strong)', }, }; const hoverStyle = hover ? { primary: { background: 'var(--c-glow-400)', borderColor: 'var(--c-glow-400)', boxShadow: '0 0 0 1px rgba(212,169,66,0.3), 0 18px 48px rgba(212,169,66,0.18)', }, ghost: { background: 'rgba(255,255,255,0.04)', borderColor: 'rgba(230,233,238,0.55)', color: '#fff', }, text: { borderBottomColor: 'var(--c-glow-500)', color: '#fff', }, dark: { background: 'var(--c-admiral-ink)', borderColor: 'rgba(230,233,238,0.4)', color: '#fff', }, }[variant] : {}; const Tag = href ? 'a' : 'button'; const props = href ? { href, onClick: handleClick } : { type, onClick: handleClick }; return ( setHover(true)} onMouseLeave={() => setHover(false)} style={{ ...base, ...variants[variant], ...hoverStyle, ...style }}> {children} {icon && !noIcon && ( )} ); } /* ============================================================ Beam — the signature directional gradient. Reads beam intensity from Tweaks context. ============================================================ */ function Beam({ angle = -9, intensity, from = '4%', width = '75%', height = '170%', top = '-15%', drift = true, soft = false, zIndex = 0, sweep = false, }) { const tw = useTw(); const beamMult = tw.beamIntensity ?? 0.55; const alpha = (intensity ?? 0.55) * (beamMult / 0.55); const motionMult = tw.motion === 'still' ? 0 : tw.motion === 'lively' ? 1.6 : 1; const ref = useRef(null); const [lit, setLit] = useState(!sweep); // Lighthouse sweep-in: beam swings from off-angle/dark into resting // position the first time it enters the viewport (or on mount, if // already in view — e.g. the hero). useEffect(() => { if (!sweep) return; if (tw.motion === 'still') { setLit(true); return; } const el = ref.current; if (!el) return; const io = new IntersectionObserver((entries) => { entries.forEach((e) => { if (e.isIntersecting) { requestAnimationFrame(() => setLit(true)); io.disconnect(); } }); }, { threshold: 0.2 }); io.observe(el); return () => io.disconnect(); }, [sweep, tw.motion]); return (
0 && (!sweep || lit)) ? `${soft ? 'beam-drift-soft' : 'beam-drift'} ${11 / motionMult}s ease-in-out infinite ${sweep ? '1900ms' : '0s'}` : 'none', mixBlendMode: 'screen', filter: 'blur(2px)', }} /> ); } /* ============================================================ HoverSweep — a beam of light catching a card on hover. Drop inside a `position: relative` container with `active` bound to the container's hover state. ============================================================ */ function HoverSweep({ active }) { return (
); } /* ============================================================ Reveal — IntersectionObserver fade-up. Honors motion tweak (still = instant on). ============================================================ */ function Reveal({ children, delay = 0, style, as: As = 'div' }) { const ref = useRef(null); const tw = useTw(); const motion = tw.motion; useEffect(() => { const el = ref.current; if (!el) return; if (motion === 'still') { el.classList.add('on'); return; } const io = new IntersectionObserver((entries) => { entries.forEach((e) => { if (e.isIntersecting) { setTimeout(() => el.classList.add('on'), delay); io.disconnect(); } }); }, { threshold: 0.1, rootMargin: '0px 0px -80px 0px' }); io.observe(el); return () => io.disconnect(); }, [delay, motion]); return {children}; } /* ============================================================ PortraitSlot — labelled placeholder for editorial portraits. Each labelled with the brand book's locked direction. Reads --tdp-img-opacity for tweak-controlled luminance. ============================================================ */ function PortraitSlot({ label = 'tan blazer · seated · serious', ratio = '4 / 5', tone = 'warm', src, style, showLabel = true, }) { const tones = { warm: { bg: 'linear-gradient(165deg, #2a3f5a 0%, #1a2b44 38%, #0a1939 78%, #06112c 100%)', silhouette: 'radial-gradient(ellipse 26% 16% at 50% 22%, rgba(195,168,128,0.42), transparent 65%), radial-gradient(ellipse 44% 38% at 50% 64%, rgba(120,88,52,0.55), transparent 70%)', rim: 'linear-gradient(265deg, rgba(212,169,66,0.22), transparent 38%)', }, cool: { bg: 'linear-gradient(165deg, #1c3146 0%, #0e2236 38%, #081827 78%, #04101e 100%)', silhouette: 'radial-gradient(ellipse 26% 16% at 50% 22%, rgba(180,194,214,0.38), transparent 65%), radial-gradient(ellipse 44% 38% at 50% 64%, rgba(70,98,128,0.55), transparent 70%)', rim: 'linear-gradient(265deg, rgba(180,200,228,0.18), transparent 40%)', }, paper: { bg: 'linear-gradient(160deg, #d9d4c6 0%, #c2bba9 100%)', silhouette: 'radial-gradient(ellipse 26% 16% at 50% 22%, rgba(120,98,68,0.6), transparent 65%), radial-gradient(ellipse 42% 36% at 50% 64%, rgba(85,72,52,0.65), transparent 70%)', rim: 'linear-gradient(265deg, rgba(180,156,120,0.25), transparent 38%)', }, }[tone]; return (
{!src && ( <>
{showLabel && (
Portrait {label}
)} )}
); } /* ============================================================ ImageSlot — generic atmospheric image placeholder (hands, posture, boardroom, environment). ============================================================ */ function ImageSlot({ label = 'hands on desk · low light', ratio = '4 / 3', tone = 'cool', style, showLabel = true, src }) { const tones = { cool: { bg: 'radial-gradient(80% 70% at 30% 40%, #1a2b44 0%, #0e2236 50%, #081227 100%)', detail: 'radial-gradient(ellipse 24% 32% at 36% 56%, rgba(150,168,196,0.18), transparent 70%), radial-gradient(ellipse 30% 20% at 70% 70%, rgba(60,86,118,0.5), transparent 70%)', }, warm: { bg: 'radial-gradient(80% 70% at 30% 40%, #2a3a4e 0%, #14202f 50%, #0a121f 100%)', detail: 'radial-gradient(ellipse 24% 32% at 36% 56%, rgba(195,168,128,0.22), transparent 70%), radial-gradient(ellipse 30% 20% at 70% 70%, rgba(120,88,52,0.5), transparent 70%)', }, paper: { bg: 'radial-gradient(80% 70% at 30% 40%, #ece7da 0%, #d4cebd 80%)', detail: 'radial-gradient(ellipse 30% 22% at 64% 60%, rgba(85,72,52,0.18), transparent 70%)', }, }[tone]; return (
{!src && (
)} {showLabel && !src && (
{label}
)}
); } /* ============================================================ ThreadDivider — gold-thread section break, optional label ============================================================ */ function ThreadDivider({ label, align = 'left', style }) { return (
{label && {label}}
); } /* ============================================================ ScrollHint — quiet "scroll" affordance ============================================================ */ function ScrollHint({ label = 'scroll', style }) { return (
{label}
); } /* ============================================================ InlineLink — quiet text link with gold underline reveal ============================================================ */ function InlineLink({ href, children, onClick }) { const router = useRouter(); const [hover, setHover] = useState(false); const click = (e) => { if (href && href.startsWith('/')) { e.preventDefault(); router.go(href); } onClick && onClick(e); }; return ( setHover(true)} onMouseLeave={() => setHover(false)} style={{ color: hover ? 'var(--c-glow-200)' : 'var(--c-glow-500)', borderBottom: `1px solid ${hover ? 'var(--c-glow-500)' : 'transparent'}`, transition: 'all 220ms var(--ease-out)', textDecoration: 'none', }}>{children} ); } /* ============================================================ PullQuote — italic, narrow, gold-thread left border ============================================================ */ function PullQuote({ children, attribution, style }) { return (
{children}
{attribution && (
{attribution}
)}
); } Object.assign(window, { TweaksContext, useTw, RouterContext, useRouter, Eyebrow, Idx, Button, Beam, Reveal, HoverSweep, PortraitSlot, ImageSlot, ThreadDivider, ScrollHint, InlineLink, PullQuote, });