// 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) */}
{/* Panther silhouette (hover) */}
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 && (
)}
);
}
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,
});