diff --git a/src/components/BaseHead.astro b/src/components/BaseHead.astro
index e675332..d143b4e 100644
--- a/src/components/BaseHead.astro
+++ b/src/components/BaseHead.astro
@@ -137,7 +137,7 @@ const professionalServiceSchema = {
-
+
diff --git a/src/components/CustomCursor.tsx b/src/components/CustomCursor.tsx
index b14a43b..d12f0d3 100644
--- a/src/components/CustomCursor.tsx
+++ b/src/components/CustomCursor.tsx
@@ -10,25 +10,78 @@ const CustomCursor = () => {
if (!dot || !outline) return;
- const onMouseMove = (e: MouseEvent) => {
- const posX = e.clientX;
- const posY = e.clientY;
+ // Respect user preferences
+ const reduceMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches;
+ if (reduceMotion) return;
- // Dot follows instantly
- dot.style.left = `${posX}px`;
- dot.style.top = `${posY}px`;
+ // rAF-driven cursor animation (single loop, no per-event Web Animations allocations)
+ let targetX = 0;
+ let targetY = 0;
+ let dotX = 0;
+ let dotY = 0;
+ let outlineX = 0;
+ let outlineY = 0;
- // Outline follows with animation
- outline.animate({
- left: `${posX}px`,
- top: `${posY}px`
- }, { duration: 500, fill: "forwards" });
+ let hasInit = false;
+ let rafId: number | null = null;
+ let lastMoveTs = performance.now();
+
+ const DOT_LERP = 0.65; // tighter = closer to 1
+ const OUTLINE_LERP = 0.18; // tighter = closer to 1
+ const STOP_AFTER_MS = 140;
+ const STOP_EPS_PX = 0.35;
+
+ const applyTransforms = () => {
+ dot.style.transform = `translate3d(${dotX}px, ${dotY}px, 0) translate(-50%, -50%)`;
+ outline.style.transform = `translate3d(${outlineX}px, ${outlineY}px, 0) translate(-50%, -50%)`;
};
- window.addEventListener('mousemove', onMouseMove);
+ const tick = (ts: number) => {
+ // Lerp towards the target
+ dotX += (targetX - dotX) * DOT_LERP;
+ dotY += (targetY - dotY) * DOT_LERP;
+ outlineX += (targetX - outlineX) * OUTLINE_LERP;
+ outlineY += (targetY - outlineY) * OUTLINE_LERP;
+
+ applyTransforms();
+
+ const idle = ts - lastMoveTs > STOP_AFTER_MS;
+ const dx = Math.abs(targetX - outlineX);
+ const dy = Math.abs(targetY - outlineY);
+ const settled = dx < STOP_EPS_PX && dy < STOP_EPS_PX;
+
+ if (idle && settled) {
+ rafId = null;
+ return;
+ }
+
+ rafId = window.requestAnimationFrame(tick);
+ };
+
+ const onMouseMove = (e: MouseEvent) => {
+ targetX = e.clientX;
+ targetY = e.clientY;
+ lastMoveTs = performance.now();
+
+ if (!hasInit) {
+ hasInit = true;
+ dotX = targetX;
+ dotY = targetY;
+ outlineX = targetX;
+ outlineY = targetY;
+ applyTransforms();
+ }
+
+ if (rafId === null) {
+ rafId = window.requestAnimationFrame(tick);
+ }
+ };
+
+ window.addEventListener('mousemove', onMouseMove, { passive: true });
return () => {
window.removeEventListener('mousemove', onMouseMove);
+ if (rafId !== null) window.cancelAnimationFrame(rafId);
};
}, []);
diff --git a/src/components/sections/Hero.astro b/src/components/sections/Hero.astro
index 5273ab2..0bf2b27 100644
--- a/src/components/sections/Hero.astro
+++ b/src/components/sections/Hero.astro
@@ -91,7 +91,8 @@ const { headlineLine1, headlineLine2, portfolioYear, location, locationLabel, bi
}
/* Fade out */
.grid-cell {
- transition: opacity 0.8s ease-out, background-color 0.8s ease-out;
+ /* Slightly faster fade-out for a snappier feel */
+ transition: opacity 0.6s ease-out, background-color 0.6s ease-out;
}
/* Initial Loaded State Classes */
@@ -112,17 +113,40 @@ const { headlineLine1, headlineLine2, portfolioYear, location, locationLabel, bi
diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro
index 1ad9aca..10ea3c7 100644
--- a/src/layouts/BaseLayout.astro
+++ b/src/layouts/BaseLayout.astro
@@ -42,7 +42,8 @@ const {