// Services grid — award-show styled cards: cursor-tracked spotlight glow, // a giant faded monogram watermark, a feature checklist, and a Learn More // CTA. Deliberately NOT using the shared (used elsewhere for // About/Work/Testimonials) so this section can carry a much heavier hover // treatment without touching that shared component. function ServiceTile({ s, i, onLearnMore }) { // Spring-lag spotlight: the glow chases the cursor with a little elastic // delay instead of snapping to it 1:1. Motion's animate() supports // driving a plain number (not just a DOM property) via onUpdate, so mx/my // are tracked as numbers here and written into the CSS custom properties // on every spring tick. Each new mousemove retargets the in-flight // spring from wherever it currently is — Motion is built for exactly // this rapid-retarget pattern (see motion.dev's own cursor-follow demos). const spot = React.useRef({ mx: 50, my: 50, animX: null, animY: null }); const onMove = (e) => { const el = e.currentTarget; const r = el.getBoundingClientRect(); const tx = ((e.clientX - r.left) / r.width) * 100; const ty = ((e.clientY - r.top) / r.height) * 100; const M = window.Motion; const st = spot.current; if (!M) { el.style.setProperty('--mx', tx + '%'); el.style.setProperty('--my', ty + '%'); return; } if (st.animX) st.animX.stop(); if (st.animY) st.animY.stop(); const spring = { type: 'spring', stiffness: 120, damping: 14, mass: 0.4 }; st.animX = M.animate(st.mx, tx, { ...spring, onUpdate: (v) => { st.mx = v; el.style.setProperty('--mx', v + '%'); } }); st.animY = M.animate(st.my, ty, { ...spring, onUpdate: (v) => { st.my = v; el.style.setProperty('--my', v + '%'); } }); }; // Diagonal stagger (row + column) reads as more "choreographed" than a // flat per-column delay once the grid wraps past one row. const col = i % 3; const row = Math.floor(i / 3); const delay = (col + row) * 55; return (
{s.title}
{s.desc}
    {s.features.map((f) => (
  • {f}
  • ))}
); } function Services({ onNavigate }) { const learnMore = (title) => { onNavigate('contact'); // Best-effort prefill so the inquiry already names the service — // the field is uncontrolled from here, so this only helps when the // form hasn't been touched yet. setTimeout(() => { const subjectField = document.querySelector('#contact input[placeholder="Project type"]'); if (subjectField && !subjectField.value) { const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set; setter.call(subjectField, title); subjectField.dispatchEvent(new Event('input', { bubbles: true })); } }, 400); }; return (
/ SERVICES

{window.SITE_DATA.serviceCategories.map((s, i) => ( ))}
); } window.Services = Services;