// shared.jsx — atoms used across all three directions // PawntherMark (morphs pawn↔panther on hover), MiniBoard, MascotPeek, Logo, // scroll reveal hook, glyph helpers. const { useState, useEffect, useRef, useCallback, useMemo } = React; // ────────────────────────────────────────────────────────────────────── // 1. PAWNTHER WORDMARK — hover morphs the "Pawnther" word; the "P" // crossfades between a chess pawn glyph and a panther head silhouette. // Click 7 times → toast easter egg. // ────────────────────────────────────────────────────────────────────── function PawntherMark({ size = 28, dark = false, italic = true, onSecret }) { const [hover, setHover] = useState(false); const [clicks, setClicks] = useState(0); const [showSecret, setShowSecret] = useState(false); const ink = dark ? '#f5ead6' : '#191714'; const gold = '#e39b44'; function handleClick(){ const next = clicks + 1; setClicks(next); if (next === 7){ setShowSecret(true); onSecret && onSecret(); setTimeout(() => setShowSecret(false), 2400); setTimeout(() => setClicks(0), 3000); } } return ( setHover(true)} onMouseLeave={() => setHover(false)} onClick={handleClick} style={{ position: 'relative', display: 'inline-flex', alignItems: 'baseline', cursor: 'pointer', fontFamily: 'Fraunces, serif', fontSize: size, fontWeight: 700, letterSpacing: '-.03em', color: ink, fontVariationSettings: '"opsz" 144, "SOFT" 50', }} > {/* P with morph */} {/* Pawn glyph (default) */} {/* Stylized capital P that reads as a pawn */} P {/* pawn-ball dot above the P bowl — appears on hover-out only */} {/* Panther silhouette (hover) */} {/* Stalking panther head — geometric, brass-toned eye */} {/* head */} {/* ears */} {/* eye */} awnther {showSecret && ( ♞ You found the cub. +1 Amigo )} ); } // ────────────────────────────────────────────────────────────────────── // 2. MINI BOARD — 8x8 with optional pieces map. Squares can highlight. // fenPieces: { e4: 'wQ', e6: 'bN', ... } // highlights: { e4: 'gold', e6: 'rose' } // onSquare: (sq) => void (for tap-the-square interactions) // ────────────────────────────────────────────────────────────────────── function MiniBoard({ pieces = {}, highlights = {}, ring = [], dots = [], arrows = [], size = '100%', flipped = false, coords = true, onSquare, squareSizeKey = 'auto', }){ const files = flipped ? ['h','g','f','e','d','c','b','a'] : ['a','b','c','d','e','f','g','h']; const ranks = flipped ? [1,2,3,4,5,6,7,8] : [8,7,6,5,4,3,2,1]; return (
{ranks.map((r, ri) => files.map((f, fi) => { const dark = (ri + fi) % 2 === 1; const sq = `${f}${r}`; const piece = pieces[sq]; const hi = highlights[sq]; const isRing = ring.includes(sq); const isDot = dots.includes(sq); return (
onSquare && onSquare(sq)} style={{ position: 'relative', background: dark ? 'var(--board-dark)' : 'var(--board-light)', display: 'grid', placeItems: 'center', cursor: onSquare ? 'pointer' : 'default', }}> {hi && ( )} {isRing && ( )} {isDot && ( )} {piece && ( )} {coords && fi === 0 && ( {r} )} {coords && ri === 7 && ( {f} )}
); }))} {/* Arrow overlay */} {arrows.length > 0 && ( {arrows.map((a, i) => { const [fx, fy] = sqToXY(a.from, flipped); const [tx, ty] = sqToXY(a.to, flipped); return ; })} )}
); } function sqToXY(sq, flipped){ const fl = sq[0].charCodeAt(0) - 97; // 0..7 const rk = parseInt(sq[1], 10) - 1; // 0..7 const x = (flipped ? 7 - fl : fl) + 0.5; const y = (flipped ? rk : 7 - rk) + 0.5; return [x, y]; } // ────────────────────────────────────────────────────────────────────── // 3. MASCOT PEEK — pokes briefly from an edge then ducks back. // side: 'bl' | 'br' | 'tl' | 'tr' // ────────────────────────────────────────────────────────────────────── function MascotPeek({ mascot = 'amigo', side = 'br', delay = 1400, size = 130, repeatEvery = 0, message }){ const [out, setOut] = useState(false); useEffect(() => { const t1 = setTimeout(() => setOut(true), delay); const t2 = setTimeout(() => setOut(false), delay + 2200); let intv; if (repeatEvery > 0){ intv = setInterval(() => { setOut(true); setTimeout(() => setOut(false), 2200); }, repeatEvery); } return () => { clearTimeout(t1); clearTimeout(t2); intv && clearInterval(intv); }; }, [delay, repeatEvery]); const isBottom = side[0] === 'b'; const isRight = side[1] === 'r'; const offset = -size * 0.78; const peekedY = -size * 0.55; const tilt = mascot === 'maia' ? (isRight ? 8 : -8) : (isRight ? -10 : 10); return (
{message && out && ( {message} )}
); } // ────────────────────────────────────────────────────────────────────── // 4. LOGOUT-BLOCKED BUTTON — mascots slide in to block the user // ────────────────────────────────────────────────────────────────────── function LogoutBlockedDemo({ dark = false }){ const [hovered, setHovered] = useState(false); const [stage, setStage] = useState(0); // 0 idle, 1 amigo, 2 maia useEffect(() => { if (!hovered){ setStage(0); return; } const t1 = setTimeout(() => setStage(1), 180); const t2 = setTimeout(() => setStage(2), 600); return () => { clearTimeout(t1); clearTimeout(t2); }; }, [hovered]); return (
Account · Easter egg
Try to log out
setHovered(true)} onMouseLeave={() => setHovered(false)} style={{ position: 'relative', height: 60, borderRadius: 14, background: dark ? 'rgba(255,255,255,.05)' : 'rgba(255,253,248,.5)', border: '1px dashed var(--line-strong)', overflow: 'hidden', display:'grid', placeItems:'center', }}> {/* Amigo slides in from left */} = 1 ? 'translateX(0) rotate(-6deg)' : 'translateX(-80px) rotate(-30deg)', transition: 'transform .5s var(--ease-pop)', }} /> {/* Maia slides in from right */} = 2 ? 'translateX(0) rotate(8deg)' : 'translateX(80px) rotate(30deg)', transition: 'transform .5s var(--ease-pop)', }} /> {/* Block message */} {stage >= 2 && ( nope — one more puzzle? )}

Hover the button — Amigo and Maia jump in front of it for a second before letting you click.

); } // ────────────────────────────────────────────────────────────────────── // 5. Reveal observer — adds .pw-in to children with .pw-reveal // ────────────────────────────────────────────────────────────────────── function useReveal(rootRef){ useEffect(() => { if (!rootRef.current) return; const els = rootRef.current.querySelectorAll('.pw-reveal'); const io = new IntersectionObserver((entries) => { entries.forEach(e => { if (e.isIntersecting){ e.target.classList.add('pw-in'); io.unobserve(e.target); } }); }, { threshold: 0.12, root: rootRef.current }); els.forEach(el => io.observe(el)); // fallback: reveal immediately if no IO fires within 1.2s const t = setTimeout(() => els.forEach(el => el.classList.add('pw-in')), 1400); return () => { io.disconnect(); clearTimeout(t); }; }, []); } // ────────────────────────────────────────────────────────────────────── // 6. PANTHER EYES (ambient) — two glowing eyes in dark margins, blink // occasionally. Used by The Hunt direction. // ────────────────────────────────────────────────────────────────────── function PantherEyes({ x = '10%', y = '40%', color = '#e39b44', scale = 1, side='l' }){ const [blink, setBlink] = useState(false); useEffect(() => { const loop = () => { setBlink(true); setTimeout(() => setBlink(false), 180); }; const i = setInterval(loop, 3800 + Math.random() * 2400); return () => clearInterval(i); }, []); return (
); } function Eye({ color, blink, tiny }){ return (
); } // Shared keyframes — injected once if (typeof document !== 'undefined' && !document.getElementById('pw-keyframes')){ const s = document.createElement('style'); s.id = 'pw-keyframes'; s.textContent = ` @keyframes pwPulse { 0%,100%{ transform: scale(1); opacity:.85 } 50%{ transform: scale(1.05); opacity:1 } } @keyframes pwBubbleIn { from{ opacity:0; transform: translateY(6px) scale(.94) } to{ opacity:1; transform:none } } @keyframes pwSecretIn { from{ opacity:0; transform: translateY(-4px) } to{ opacity:1; transform:none } } @keyframes pwBob { 0%,100%{ transform: translateY(0) } 50%{ transform: translateY(-6px) } } @keyframes pwDrift { 0%,100%{ transform: translateX(0) } 50%{ transform: translateX(-8px) } } @keyframes pwMarquee { from{ transform: translateX(0) } to{ transform: translateX(-50%) } } @keyframes pwStalk { 0%{ transform: translateX(-30px); opacity:.0 } 12%{ opacity:.55 } 50%{ transform: translateX(50vw); opacity:.45 } 88%{ opacity:.0 } 100%{ transform: translateX(110%); opacity:0 } } @keyframes pwLivePulse { 0% { box-shadow: 0 0 0 0 rgba(141,152,112,.6); } 70% { box-shadow: 0 0 0 9px rgba(141,152,112,0); } 100% { box-shadow: 0 0 0 0 rgba(141,152,112,0); } } @keyframes pwTickerScroll { from{ transform: translateX(0) } to { transform: translateX(-50%) } } @keyframes pwEval { 0%{ height: 50% } 50%{ height: 62% } 100%{ height: 50% } } @keyframes pwGlowDrift { 0%{ transform: translate(0,0) } 100%{ transform: translate(30px,-12px) } } @keyframes pwBlink { 0%,90%,100%{ transform: scaleY(1) } 95%{ transform: scaleY(.05) } } @keyframes pwSpin { to { transform: rotate(360deg) } } `; document.head.appendChild(s); } Object.assign(window, { PawntherMark, MiniBoard, MascotPeek, LogoutBlockedDemo, useReveal, PantherEyes, });