polish: performance optimizations + layout fixes
Some checks failed
Build & Push / build-and-push (push) Has been cancelled

- Hero: convert 7 infinite framer-motion JS animations to GPU-composited CSS @keyframes
- Hero: remove blur filter from word reveal animation (expensive paint)
- Hero: remove eyebrow text overlapping navbar, add top padding
- Process: widen header column (2/5) so title doesn't crowd step cards
- TrustBar: center icons and text in cards
- Fonts: add Google Fonts @import with display=swap + preconnect hints

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-26 14:19:15 +01:00
parent 7559128d5f
commit ed4174d198
5 changed files with 99 additions and 107 deletions

View File

@@ -36,6 +36,8 @@ export default async function LocaleLayout({ children, params }: Props) {
return ( return (
<html lang={locale} className="scroll-smooth"> <html lang={locale} className="scroll-smooth">
<head> <head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
{process.env.NODE_ENV === 'development' && ( {process.env.NODE_ENV === 'development' && (
<Script <Script
src="//unpkg.com/react-grab/dist/index.global.js" src="//unpkg.com/react-grab/dist/index.global.js"

View File

@@ -10,33 +10,6 @@ import HeroGeometric from '@/components/icons/HeroGeometric';
const EASE_OUT_EXPO = [0.16, 1, 0.3, 1] as [number, number, number, number]; const EASE_OUT_EXPO = [0.16, 1, 0.3, 1] as [number, number, number, number];
// Background SVG drift — slow, almost imperceptible
const bgDriftA = {
animate: {
y: [0, -10, 0],
x: [0, 5, 0],
transition: {
duration: 22,
ease: 'easeInOut' as const,
repeat: Infinity,
repeatType: 'loop' as const,
},
},
};
const bgDriftB = {
animate: {
y: [0, 7, 0],
x: [0, -6, 0],
transition: {
duration: 28,
ease: 'easeInOut' as const,
repeat: Infinity,
repeatType: 'loop' as const,
},
},
};
// Headline word stagger container // Headline word stagger container
const headlineContainer = { const headlineContainer = {
hidden: {}, hidden: {},
@@ -48,13 +21,12 @@ const headlineContainer = {
}, },
}; };
// Per-word reveal — slide up + blur clear // Per-word reveal — slide up (no blur for performance)
const wordReveal = { const wordReveal = {
hidden: { opacity: 0, y: 52, filter: 'blur(6px)' }, hidden: { opacity: 0, y: 52 },
visible: { visible: {
opacity: 1, opacity: 1,
y: 0, y: 0,
filter: 'blur(0px)',
transition: { transition: {
duration: 0.72, duration: 0.72,
ease: EASE_OUT_EXPO, ease: EASE_OUT_EXPO,
@@ -62,16 +34,6 @@ const wordReveal = {
}, },
}; };
// Eyebrow label fade
const eyebrowVariant = {
hidden: { opacity: 0, y: 14 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.5, delay: 0.08, ease: EASE_OUT_EXPO },
},
};
// Subtitle fade-up — delayed after headline completes // Subtitle fade-up — delayed after headline completes
const subtitleVariant = { const subtitleVariant = {
hidden: { opacity: 0, y: 18 }, hidden: { opacity: 0, y: 18 },
@@ -112,18 +74,6 @@ const rightColumnVariant = {
}, },
}; };
// Slow full rotation for the large decorative ring
const slowRotate = {
animate: {
rotate: [0, 360],
transition: {
duration: 30,
ease: 'linear' as const,
repeat: Infinity,
},
},
};
// ─── Component ──────────────────────────────────────────────────────────── // ─── Component ────────────────────────────────────────────────────────────
export default function Hero() { export default function Hero() {
@@ -159,18 +109,14 @@ export default function Hero() {
aria-label="Hero" aria-label="Hero"
className="relative min-h-screen flex flex-col overflow-hidden bg-surface" className="relative min-h-screen flex flex-col overflow-hidden bg-surface"
> >
{/* ─── Full-bleed SVG background — spans entire section ─────────── */} {/* ─── Full-bleed SVG background — CSS drift for GPU compositing ── */}
<motion.div <div className="absolute inset-0 z-0 pointer-events-none hero-drift-a">
className="absolute inset-0 z-0 pointer-events-none"
{...bgDriftA}
>
<HeroGeometric className="absolute inset-0 w-full h-full" /> <HeroGeometric className="absolute inset-0 w-full h-full" />
</motion.div> </div>
{/* ─── Radial glow — soft primary haze in right column ──────────── */} {/* ─── Radial glow — soft primary haze in right column ──────────── */}
<motion.div <div
className="absolute inset-0 z-0 pointer-events-none" className="absolute inset-0 z-0 pointer-events-none hero-drift-b"
{...bgDriftB}
style={{ style={{
background: background:
'radial-gradient(ellipse 55% 70% at 78% 48%, rgba(91,164,217,0.045) 0%, transparent 70%)', 'radial-gradient(ellipse 55% 70% at 78% 48%, rgba(91,164,217,0.045) 0%, transparent 70%)',
@@ -182,17 +128,7 @@ export default function Hero() {
<div className="relative z-10 w-full max-w-screen-xl mx-auto px-6 lg:px-12 xl:px-16 flex flex-col lg:flex-row lg:items-center min-h-screen"> <div className="relative z-10 w-full max-w-screen-xl mx-auto px-6 lg:px-12 xl:px-16 flex flex-col lg:flex-row lg:items-center min-h-screen">
{/* ── LEFT COLUMN — text content (55% on desktop) ─────────────── */} {/* ── LEFT COLUMN — text content (55% on desktop) ─────────────── */}
<div className="flex-1 lg:max-w-[58%] flex flex-col justify-center pt-32 pb-16 lg:pt-0 lg:pb-0"> <div className="flex-1 lg:max-w-[58%] flex flex-col justify-center pt-40 pb-16 lg:pt-28 lg:pb-0">
{/* Eyebrow */}
<motion.span
className="label-md text-primary tracking-widest uppercase mb-6 block"
variants={eyebrowVariant}
initial="hidden"
animate="visible"
>
Bespoke Digital Studio
</motion.span>
{/* Headline — word-by-word stagger */} {/* Headline — word-by-word stagger */}
<motion.h1 <motion.h1
@@ -294,9 +230,9 @@ export default function Hero() {
}} }}
/> />
{/* Outer ring — large, slow counter-rotation */} {/* Outer ring — large, slow counter-rotation (CSS) */}
<motion.div <div
className="absolute rounded-full" className="absolute rounded-full hero-spin-reverse"
style={{ style={{
width: 380, width: 380,
height: 380, height: 380,
@@ -305,11 +241,9 @@ export default function Hero() {
marginTop: -190, marginTop: -190,
marginLeft: -190, marginLeft: -190,
border: '1.5px solid rgba(91,164,217,0.12)', border: '1.5px solid rgba(91,164,217,0.12)',
animationDuration: '60s',
}} }}
animate={{ rotate: [0, -360] }}
transition={{ duration: 60, ease: 'linear', repeat: Infinity }}
> >
{/* Tick marks on the outer ring — fixed positions */}
{[0, 45, 90, 135, 180, 225, 270, 315].map((deg) => ( {[0, 45, 90, 135, 180, 225, 270, 315].map((deg) => (
<div <div
key={deg} key={deg}
@@ -326,11 +260,11 @@ export default function Hero() {
}} }}
/> />
))} ))}
</motion.div> </div>
{/* Inner ring — dashed, rotating forward */} {/* Inner ring — dashed, rotating forward (CSS) */}
<motion.div <div
className="absolute rounded-full" className="absolute rounded-full hero-spin"
style={{ style={{
width: 240, width: 240,
height: 240, height: 240,
@@ -339,14 +273,13 @@ export default function Hero() {
marginTop: -120, marginTop: -120,
marginLeft: -120, marginLeft: -120,
border: '1px dashed rgba(0,100,148,0.15)', border: '1px dashed rgba(0,100,148,0.15)',
animationDuration: '40s',
}} }}
animate={{ rotate: [0, 360] }}
transition={{ duration: 40, ease: 'linear', repeat: Infinity }}
/> />
{/* Core ring — solid, slowly rotating */} {/* Core ring — solid, slowly rotating (CSS) */}
<motion.div <div
className="absolute rounded-full" className="absolute rounded-full hero-spin"
style={{ style={{
width: 140, width: 140,
height: 140, height: 140,
@@ -355,8 +288,8 @@ export default function Hero() {
marginTop: -70, marginTop: -70,
marginLeft: -70, marginLeft: -70,
border: '2px solid rgba(91,164,217,0.18)', border: '2px solid rgba(91,164,217,0.18)',
animationDuration: '30s',
}} }}
{...slowRotate}
/> />
{/* Center crosshair */} {/* Center crosshair */}
@@ -366,9 +299,9 @@ export default function Hero() {
<div style={{ width: 6, height: 6, borderRadius: '50%', background: 'rgba(91,164,217,0.2)', position: 'absolute', top: -3, left: -3 }} /> <div style={{ width: 6, height: 6, borderRadius: '50%', background: 'rgba(91,164,217,0.2)', position: 'absolute', top: -3, left: -3 }} />
</div> </div>
{/* Floating accent dot — orbiting slowly */} {/* Floating accent dot — CSS orbit */}
<motion.div <div
className="absolute rounded-full" className="absolute rounded-full hero-orbit-a"
style={{ style={{
width: 8, width: 8,
height: 8, height: 8,
@@ -378,16 +311,11 @@ export default function Hero() {
marginTop: -4, marginTop: -4,
marginLeft: -4, marginLeft: -4,
}} }}
animate={{
x: [0, 120, 170, 120, 0, -120, -170, -120, 0],
y: [170, 120, 0, -120, -170, -120, 0, 120, 170],
}}
transition={{ duration: 20, ease: 'linear', repeat: Infinity }}
/> />
{/* Second accent dot — opposite orbit, smaller */} {/* Second accent dot — opposite CSS orbit */}
<motion.div <div
className="absolute rounded-full" className="absolute rounded-full hero-orbit-b"
style={{ style={{
width: 5, width: 5,
height: 5, height: 5,
@@ -397,11 +325,6 @@ export default function Hero() {
marginTop: -2.5, marginTop: -2.5,
marginLeft: -2.5, marginLeft: -2.5,
}} }}
animate={{
x: [0, -90, -130, -90, 0, 90, 130, 90, 0],
y: [-130, -90, 0, 90, 130, 90, 0, -90, -130],
}}
transition={{ duration: 16, ease: 'linear', repeat: Infinity }}
/> />
{/* Corner brackets — crisp architectural detail */} {/* Corner brackets — crisp architectural detail */}

View File

@@ -105,10 +105,10 @@ export default function Process() {
Mobile layout: Mobile layout:
header on top, steps in single column below header on top, steps in single column below
*/} */}
<div className="grid grid-cols-1 lg:grid-cols-4 gap-12 lg:gap-10 items-start"> <div className="grid grid-cols-1 lg:grid-cols-5 gap-12 lg:gap-10 items-start">
{/* ── Header column ── */} {/* ── Header column ── */}
<div className="lg:col-span-1 lg:sticky lg:top-32"> <div className="lg:col-span-2 lg:sticky lg:top-32">
{/* Vertical accent bar left of heading */} {/* Vertical accent bar left of heading */}
<div className="relative pl-4"> <div className="relative pl-4">
<div <div

View File

@@ -49,7 +49,7 @@ function TrustCard({ item, index, t }: TrustCardProps) {
<motion.div <motion.div
variants={revealVariants} variants={revealVariants}
className={cn( className={cn(
'group relative flex flex-col items-start gap-4 p-8', 'group relative flex flex-col items-center text-center gap-4 p-8',
'rounded-2xl bg-surface-high shadow-subtle', 'rounded-2xl bg-surface-high shadow-subtle',
'transition-shadow duration-300 hover:shadow-card', 'transition-shadow duration-300 hover:shadow-card',
'overflow-hidden cursor-default', 'overflow-hidden cursor-default',

View File

@@ -1,3 +1,4 @@
@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600;1,700&family=Inter:wght@300..700&display=swap');
@import 'tailwindcss'; @import 'tailwindcss';
@theme { @theme {
@@ -60,3 +61,69 @@
font-weight: 500; font-weight: 500;
} }
} }
/* ── Hero GPU-composited animations ──────────────────────────────────── */
@keyframes hero-drift-a {
0%, 100% { transform: translate(0, 0); }
50% { transform: translate(5px, -10px); }
}
@keyframes hero-drift-b {
0%, 100% { transform: translate(0, 0); }
50% { transform: translate(-6px, 7px); }
}
@keyframes hero-spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes hero-spin-reverse {
from { transform: rotate(0deg); }
to { transform: rotate(-360deg); }
}
@keyframes hero-orbit-a {
0% { transform: translate(0px, 170px); }
12.5% { transform: translate(120px, 120px); }
25% { transform: translate(170px, 0px); }
37.5% { transform: translate(120px, -120px); }
50% { transform: translate(0px, -170px); }
62.5% { transform: translate(-120px, -120px); }
75% { transform: translate(-170px, 0px); }
87.5% { transform: translate(-120px, 120px); }
100% { transform: translate(0px, 170px); }
}
@keyframes hero-orbit-b {
0% { transform: translate(0px, -130px); }
12.5% { transform: translate(-90px, -90px); }
25% { transform: translate(-130px, 0px); }
37.5% { transform: translate(-90px, 90px); }
50% { transform: translate(0px, 130px); }
62.5% { transform: translate(90px, 90px); }
75% { transform: translate(130px, 0px); }
87.5% { transform: translate(90px, -90px); }
100% { transform: translate(0px, -130px); }
}
.hero-drift-a {
animation: hero-drift-a 22s ease-in-out infinite;
will-change: transform;
}
.hero-drift-b {
animation: hero-drift-b 28s ease-in-out infinite;
will-change: transform;
}
.hero-spin {
animation: hero-spin 30s linear infinite;
will-change: transform;
}
.hero-spin-reverse {
animation: hero-spin-reverse 60s linear infinite;
will-change: transform;
}
.hero-orbit-a {
animation: hero-orbit-a 20s linear infinite;
will-change: transform;
}
.hero-orbit-b {
animation: hero-orbit-b 16s linear infinite;
will-change: transform;
}