/* 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 (
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 (
);
}
/* ============================================================
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,
});