diff --git a/src/components/Background.tsx b/src/components/Background.tsx
index 2979b62..eb798d7 100644
--- a/src/components/Background.tsx
+++ b/src/components/Background.tsx
@@ -5,32 +5,54 @@ export default function Background() {
const [mounted, setMounted] = useState(false);
const [reducedMotion, setReducedMotion] = useState(false);
- // Mouse spring for smooth desktop ambient lighting tracking
- const mouseX = useSpring(0, { stiffness: 50, damping: 28 });
- const mouseY = useSpring(0, { stiffness: 50, damping: 28 });
+ // Smooth mouse inertia on desktop
+ const mouseX = useSpring(0, { stiffness: 40, damping: 24 });
+ const mouseY = useSpring(0, { stiffness: 40, damping: 24 });
// Native window scroll tracking
const { scrollY } = useScroll();
- // Distinct parallax movement across scroll depths
- // Layer 1: Dot Matrix Grid (drifts downward with scroll)
- const gridY = useTransform(scrollY, [0, 2000], [0, 140]);
+ // ─── Layer 1: Dot Matrix Grid ───
+ const gridY = useTransform(scrollY, [0, 3000], [0, 100]);
- // Layer 2: Primary Indigo-Blue Orb (moves down and right)
- const orb1ScrollY = useTransform(scrollY, [0, 2000], [0, 380]);
- const orb1ScrollX = useTransform(scrollY, [0, 2000], [0, 120]);
+ // ─── Unified Direction Movement (Prevents any collision during scroll) ───
+ // All shapes drift in the same cohesive direction at gentle depth offsets
+ const shape1ScrollY = useTransform(scrollY, [0, 3000], [0, 160]);
+ const shape1ScrollX = useTransform(scrollY, [0, 3000], [0, 30]);
+ const shape1Rotate = useTransform(scrollY, [0, 3000], [0, 12]);
+ // Shape 1: Zooms in a little as you scroll
+ const shape1Scale = useTransform(scrollY, [0, 1500, 3000], [0.95, 1.08, 1.0]);
- // Layer 3: Secondary Violet-Cyan Orb (moves up and counter-left)
- const orb2ScrollY = useTransform(scrollY, [0, 2000], [0, -320]);
- const orb2ScrollX = useTransform(scrollY, [0, 2000], [0, -100]);
+ const shape2ScrollY = useTransform(scrollY, [0, 3000], [0, 120]);
+ const shape2ScrollX = useTransform(scrollY, [0, 3000], [0, -25]);
+ const shape2Rotate = useTransform(scrollY, [0, 3000], [0, -15]);
+ // Shape 2: Zooms out a little as you scroll
+ const shape2Scale = useTransform(scrollY, [0, 1500, 3000], [1.06, 0.92, 0.98]);
- // Layer 4: Emerald-Teal Accent Glow (floats diagonally)
- const orb3ScrollY = useTransform(scrollY, [0, 2000], [0, -220]);
- const orb3ScrollX = useTransform(scrollY, [0, 2000], [0, 80]);
+ const shape3ScrollY = useTransform(scrollY, [0, 3000], [0, 140]);
+ const shape3ScrollX = useTransform(scrollY, [0, 3000], [0, 30]);
+ const shape3Rotate = useTransform(scrollY, [0, 3000], [-5, 10]);
+ // Shape 3: Zooms in a little as you scroll past it
+ const shape3Scale = useTransform(scrollY, [0, 1500, 3000], [0.92, 1.07, 1.0]);
- // Layer 5: Floating Geometric Depth Rings (distinct, noticeable parallax markers)
- const ring1Y = useTransform(scrollY, [0, 2000], [0, 450]);
- const ring2Y = useTransform(scrollY, [0, 2000], [0, -280]);
+ const shape4ScrollY = useTransform(scrollY, [0, 3000], [0, 110]);
+ const shape4ScrollX = useTransform(scrollY, [0, 3000], [0, -25]);
+ const shape4Rotate = useTransform(scrollY, [0, 3000], [0, 15]);
+ // Shape 4: Zooms out a little
+ const shape4Scale = useTransform(scrollY, [0, 1500, 3000], [1.05, 0.93, 1.02]);
+
+ // Unified Scroll + Mouse transforms (prevents Framer Motion transform collisions)
+ const shape1FinalX = useTransform([shape1ScrollX, mouseX], ([sX, mX]) => (sX as number) + (mX as number));
+ const shape1FinalY = useTransform([shape1ScrollY, mouseY], ([sY, mY]) => (sY as number) + (mY as number));
+
+ const shape2FinalX = useTransform([shape2ScrollX, mouseX], ([sX, mX]) => (sX as number) - (mX as number) * 1.1);
+ const shape2FinalY = useTransform([shape2ScrollY, mouseY], ([sY, mY]) => (sY as number) - (mY as number) * 1.1);
+
+ const shape3FinalX = useTransform([shape3ScrollX, mouseX], ([sX, mX]) => (sX as number) + (mX as number) * 0.8);
+ const shape3FinalY = useTransform([shape3ScrollY, mouseY], ([sY, mY]) => (sY as number) + (mY as number) * 0.8);
+
+ const shape4FinalX = useTransform([shape4ScrollX, mouseX], ([sX, mX]) => (sX as number) - (mX as number) * 0.9);
+ const shape4FinalY = useTransform([shape4ScrollY, mouseY], ([sY, mY]) => (sY as number) + (mY as number) * 0.9);
useEffect(() => {
setMounted(true);
@@ -43,9 +65,8 @@ export default function Background() {
const handleMouseMove = (e: MouseEvent) => {
const centerX = window.innerWidth / 2;
const centerY = window.innerHeight / 2;
- // Responsive mouse parallax offset (up to ~55px)
- mouseX.set((e.clientX - centerX) * 0.06);
- mouseY.set((e.clientY - centerY) * 0.06);
+ mouseX.set((e.clientX - centerX) * 0.04);
+ mouseY.set((e.clientY - centerY) * 0.04);
};
window.addEventListener('mousemove', handleMouseMove, { passive: true });
@@ -56,66 +77,284 @@ export default function Background() {
return (
- {/* ─── Layer 1: Visible Parallax Dot Matrix Grid ─── */}
+ {/* ─── Layer 1: Dot Matrix Grid (Code Matrix) ─── */}
- {/* ─── Layer 2: Vibrant Indigo-Blue Gradient Orb (Top-Left) ─── */}
+ {/* ─── 1. Top-Left: Stylized Code Brackets & Tag > (Zooms in subtly on scroll) ─── */}
+ className="absolute top-[3%] left-[4%] sm:left-[10%] w-[360px] sm:w-[460px] h-[360px] sm:h-[460px] will-change-transform"
+ >
+
+
- {/* ─── Layer 3: Violet-Pink-Cyan Secondary Orb (Center-Right) ─── */}
+ {/* ─── 2. Upper-Mid Right: Cisco Network Topology & Graph Nodes (Zooms out subtly on scroll) ─── */}
-v * 1.3),
- translateY: useTransform(mouseY, (v) => -v * 1.3),
+ x: shape2FinalX,
+ y: shape2FinalY,
+ rotate: shape2Rotate,
+ scale: shape2Scale,
}
}
- className="absolute top-1/3 -right-36 w-[500px] sm:w-[580px] h-[500px] sm:h-[580px] rounded-full bg-gradient-to-bl from-purple-600/25 via-fuchsia-500/20 to-cyan-500/15 dark:from-purple-500/25 dark:via-pink-500/20 dark:to-indigo-500/20 blur-[90px] will-change-transform"
- />
+ className="absolute top-[20%] right-[4%] sm:right-[10%] w-[350px] sm:w-[440px] h-[350px] sm:h-[440px] will-change-transform"
+ >
+
+
+
+ {/* ─── 3. Lower-Mid Left: Floating Developer Terminal / IDE Blueprint (Zooms in subtly on scroll) ─── */}
v * 0.9),
+ x: shape3FinalX,
+ y: shape3FinalY,
+ rotate: shape3Rotate,
+ scale: shape3Scale,
}
}
- className="absolute -bottom-48 left-1/4 w-[480px] sm:w-[560px] h-[480px] sm:h-[560px] rounded-full bg-gradient-to-tr from-emerald-500/20 via-teal-500/20 to-blue-500/15 dark:from-emerald-400/20 dark:via-cyan-500/20 dark:to-blue-500/20 blur-[85px] will-change-transform"
- />
+ className="absolute top-[56%] left-[4%] sm:left-[11%] w-[340px] sm:w-[430px] h-[340px] sm:h-[430px] will-change-transform"
+ >
+
+
+
+ {/* ─── 4. Bottom-Right: Git Commit Graph & Curly Braces { } (Zooms out subtly on scroll) ─── */}
-
+ style={
+ reducedMotion
+ ? {}
+ : {
+ x: shape4FinalX,
+ y: shape4FinalY,
+ rotate: shape4Rotate,
+ scale: shape4Scale,
+ }
+ }
+ className="absolute bottom-[4%] right-[5%] sm:right-[12%] w-[360px] sm:w-[450px] h-[360px] sm:h-[450px] will-change-transform"
+ >
+
+
);
}
diff --git a/src/components/Certifications.tsx b/src/components/Certifications.tsx
index 3cca71a..6ba4b55 100644
--- a/src/components/Certifications.tsx
+++ b/src/components/Certifications.tsx
@@ -87,12 +87,29 @@ export default function Certifications({ certifications, extraCerts = [] }: Prop
{/* Large Picture / Certificate Document Preview Container */}
-

+ {cert.badgeImage ? (
+

{
+ // If remote image fails, show subtle fallback badge
+ const parent = (e.currentTarget as HTMLElement).parentElement;
+ if (parent) {
+ (e.currentTarget as HTMLElement).style.display = 'none';
+ const fallback = parent.querySelector('.cert-fallback-icon');
+ if (fallback) (fallback as HTMLElement).classList.remove('hidden');
+ }
+ }}
+ />
+ ) : null}
+
{/* Subtle Preview Indicator overlay */}
@@ -149,6 +166,7 @@ export default function Certifications({ certifications, extraCerts = [] }: Prop
diff --git a/src/components/Hero.tsx b/src/components/Hero.tsx
index f0ea302..b6962a9 100644
--- a/src/components/Hero.tsx
+++ b/src/components/Hero.tsx
@@ -1,5 +1,5 @@
import React, { Suspense, lazy } from 'react';
-import { motion, useMotionValue, useTransform } from 'framer-motion';
+import { motion, useMotionValue, useTransform, useScroll } from 'framer-motion';
import { useIsMobile } from '../lib/hooks';
import type { SocialLink } from '../lib/data';
import ppDay from '../assets/pp-day.webp';
@@ -308,10 +308,8 @@ export default function Hero({
{/* Right Column: Profile Image Frame */}
- {/* Backdrop Aura */}
-
+ {/* Subtle Backdrop Aura (Decreased Color Strength, Static) */}
+
}>
{
if (!sanityClient) return mockCertifications;
const certs = await sanityClient.fetch(`*[_type == "certification"] | order(coalesce(order, 99) asc)`);
if (!certs || certs.length === 0) return mockCertifications;
- return certs.map((c: any, idx: number) => {
+ const sanityCerts = certs.map((c: any, idx: number) => {
const badgeImg = c.badgeImage ? urlFor(c.badgeImage) : (c.badgeImageUrl || '');
return {
id: c._id || `cert-${idx}`,
@@ -461,6 +461,15 @@ export async function getCertifications(): Promise {
order: typeof c.order === 'number' ? c.order : idx + 1
};
});
+
+ // Merge any credentials not yet duplicated in Sanity (e.g. Cisco badges + TESDA NC2)
+ const combined = [...sanityCerts];
+ for (const mock of mockCertifications) {
+ if (!combined.some(c => c.title?.toLowerCase() === mock.title?.toLowerCase() || c.code === mock.code)) {
+ combined.push(mock);
+ }
+ }
+ return combined;
} catch (error) {
console.error('Error fetching certification from Sanity:', error);
return mockCertifications;
diff --git a/src/lib/haptics.ts b/src/lib/haptics.ts
index 4162c30..453783b 100644
--- a/src/lib/haptics.ts
+++ b/src/lib/haptics.ts
@@ -1,34 +1,35 @@
/**
* Mobile Haptic Feedback Utility using the standard W3C Vibration API.
- * Provides tactile micro-vibrations for phone browsers (Android Chrome, Edge, Firefox, Samsung Internet).
- * Safely no-ops on desktop and iOS Safari without errors or overhead.
+ * Provides tactile physical vibrations for mobile phone browsers (Android Chrome, Edge, Firefox, Samsung Internet).
+ * Note: iOS Safari restricts navigator.vibrate() by Apple system policy.
*/
const canVibrate = (): boolean => {
- return typeof window !== 'undefined' && typeof navigator !== 'undefined' && 'vibrate' in navigator;
+ return typeof window !== 'undefined' && typeof navigator !== 'undefined' && typeof navigator.vibrate === 'function';
};
export const haptic = {
/**
- * Ultra-short 8ms tick for tabs, category filters, and small toggle switches.
+ * Crisp 35ms pulse for tabs, category filters, and small toggles.
+ * Long enough for physical vibration motors (ERM & LRA) to overcome inertia.
*/
tick: () => {
if (canVibrate()) {
try {
- navigator.vibrate(8);
+ navigator.vibrate(35);
} catch {
- // Safe no-op on devices with restricted permissions
+ // Safe no-op
}
}
},
/**
- * Crisp 15ms tap for standard buttons, theme switcher, and FAB buttons.
+ * Solid 55ms pulse for standard buttons, theme switcher, and FAB buttons.
*/
tap: () => {
if (canVibrate()) {
try {
- navigator.vibrate(15);
+ navigator.vibrate(55);
} catch {
// Safe no-op
}
@@ -36,12 +37,12 @@ export const haptic = {
},
/**
- * 20ms tactile pulse when opening modals, full-screen certificates, or photos.
+ * 75ms tactile pulse when opening modals, full-screen certificates, or photos.
*/
openModal: () => {
if (canVibrate()) {
try {
- navigator.vibrate(20);
+ navigator.vibrate(75);
} catch {
// Safe no-op
}
@@ -49,12 +50,12 @@ export const haptic = {
},
/**
- * Double-pulse [15ms, 40ms, 20ms] confirming actions like copy to clipboard, download, or form submit.
+ * Distinct double-pulse [45ms, 60ms, 45ms] confirming actions like copy, download, or form submit.
*/
success: () => {
if (canVibrate()) {
try {
- navigator.vibrate([15, 40, 20]);
+ navigator.vibrate([45, 60, 45]);
} catch {
// Safe no-op
}
diff --git a/src/pages/certifications.astro b/src/pages/certifications.astro
index e3ec3e0..ef3cf48 100644
--- a/src/pages/certifications.astro
+++ b/src/pages/certifications.astro
@@ -18,7 +18,7 @@ const socialLinks = await getSocialLinks();
-
+
diff --git a/src/pages/index.astro b/src/pages/index.astro
index 7782f3a..2dc42ca 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -61,7 +61,7 @@ const socialLinks = await getSocialLinks();
-
+