// Skills grid — same award-show language as Services (hairline grid, // giant faded monogram, cursor spotlight, numbered index) plus one new // touch: the tag pills cascade in with a staggered pop the first time // each card scrolls into view, via Motion's inView()+stagger() — the // first use of either in this project (everywhere else uses animate() // directly on a single value/element). // // Tag is a shared design-system component (also used by About/Work) that // sets border/background/color as INLINE styles, which stylesheet // :hover rules can't override without !important. Each tag is wrapped in // its own span here instead, and the WRAPPER gets the hover lift/glow — // Tag itself is untouched. function SkillTile({ g, i }) { const tileRef = React.useRef(null); const tagsRef = React.useRef(null); const { Tag } = window.ArnelBiscarraDesignSystem_2766cb; const onMove = (e) => { const el = e.currentTarget; const r = el.getBoundingClientRect(); el.style.setProperty('--mx', ((e.clientX - r.left) / r.width) * 100 + '%'); el.style.setProperty('--my', ((e.clientY - r.top) / r.height) * 100 + '%'); }; React.useEffect(() => { const M = window.Motion; const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches; const wraps = tagsRef.current ? tagsRef.current.querySelectorAll('.skill-tag-wrap') : []; if (!M || !M.inView || reduced || !wraps.length) return; const stop = M.inView(tileRef.current, () => { M.animate(wraps, { opacity: [0, 1], y: [8, 0], scale: [0.92, 1] }, { delay: M.stagger(0.035), duration: 0.4, easing: 'ease-out', }); }, { amount: 0.3 }); return () => { if (typeof stop === 'function') stop(); }; }, []); // Cursor-tracking conic-gradient border glow (js/glow-border.js) — // registers this tile with the shared controller, which drives every // registered tile off ONE global pointermove/scroll listener rather // than each tile owning its own. React.useEffect(() => { if (!window.GlowBorder || !tileRef.current) return; return window.GlowBorder.register(tileRef.current, { proximity: 110, inactiveZone: 0.01, movementDuration: 0.7, }); }, []); const col = i % 3; const row = Math.floor(i / 3); const delay = (col + row) * 55; return (
{g.title}
{g.tools.map((t) => ( {t} ))}
); } function Skills() { return (
/ SKILLS

{window.SITE_DATA.skillGroups.map((g, i) => ( ))}
); } window.Skills = Skills;