polish: performance optimizations + layout fixes
Some checks failed
Build & Push / build-and-push (push) Has been cancelled
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:
@@ -36,6 +36,8 @@ export default async function LocaleLayout({ children, params }: Props) {
|
||||
return (
|
||||
<html lang={locale} className="scroll-smooth">
|
||||
<head>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
||||
{process.env.NODE_ENV === 'development' && (
|
||||
<Script
|
||||
src="//unpkg.com/react-grab/dist/index.global.js"
|
||||
|
||||
@@ -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];
|
||||
|
||||
// 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
|
||||
const headlineContainer = {
|
||||
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 = {
|
||||
hidden: { opacity: 0, y: 52, filter: 'blur(6px)' },
|
||||
hidden: { opacity: 0, y: 52 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
filter: 'blur(0px)',
|
||||
transition: {
|
||||
duration: 0.72,
|
||||
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
|
||||
const subtitleVariant = {
|
||||
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 ────────────────────────────────────────────────────────────
|
||||
|
||||
export default function Hero() {
|
||||
@@ -159,18 +109,14 @@ export default function Hero() {
|
||||
aria-label="Hero"
|
||||
className="relative min-h-screen flex flex-col overflow-hidden bg-surface"
|
||||
>
|
||||
{/* ─── Full-bleed SVG background — spans entire section ─────────── */}
|
||||
<motion.div
|
||||
className="absolute inset-0 z-0 pointer-events-none"
|
||||
{...bgDriftA}
|
||||
>
|
||||
{/* ─── Full-bleed SVG background — CSS drift for GPU compositing ── */}
|
||||
<div className="absolute inset-0 z-0 pointer-events-none hero-drift-a">
|
||||
<HeroGeometric className="absolute inset-0 w-full h-full" />
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* ─── Radial glow — soft primary haze in right column ──────────── */}
|
||||
<motion.div
|
||||
className="absolute inset-0 z-0 pointer-events-none"
|
||||
{...bgDriftB}
|
||||
<div
|
||||
className="absolute inset-0 z-0 pointer-events-none hero-drift-b"
|
||||
style={{
|
||||
background:
|
||||
'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">
|
||||
|
||||
{/* ── 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">
|
||||
|
||||
{/* 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>
|
||||
<div className="flex-1 lg:max-w-[58%] flex flex-col justify-center pt-40 pb-16 lg:pt-28 lg:pb-0">
|
||||
|
||||
{/* Headline — word-by-word stagger */}
|
||||
<motion.h1
|
||||
@@ -294,9 +230,9 @@ export default function Hero() {
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Outer ring — large, slow counter-rotation */}
|
||||
<motion.div
|
||||
className="absolute rounded-full"
|
||||
{/* Outer ring — large, slow counter-rotation (CSS) */}
|
||||
<div
|
||||
className="absolute rounded-full hero-spin-reverse"
|
||||
style={{
|
||||
width: 380,
|
||||
height: 380,
|
||||
@@ -305,11 +241,9 @@ export default function Hero() {
|
||||
marginTop: -190,
|
||||
marginLeft: -190,
|
||||
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) => (
|
||||
<div
|
||||
key={deg}
|
||||
@@ -326,11 +260,11 @@ export default function Hero() {
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Inner ring — dashed, rotating forward */}
|
||||
<motion.div
|
||||
className="absolute rounded-full"
|
||||
{/* Inner ring — dashed, rotating forward (CSS) */}
|
||||
<div
|
||||
className="absolute rounded-full hero-spin"
|
||||
style={{
|
||||
width: 240,
|
||||
height: 240,
|
||||
@@ -339,14 +273,13 @@ export default function Hero() {
|
||||
marginTop: -120,
|
||||
marginLeft: -120,
|
||||
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 */}
|
||||
<motion.div
|
||||
className="absolute rounded-full"
|
||||
{/* Core ring — solid, slowly rotating (CSS) */}
|
||||
<div
|
||||
className="absolute rounded-full hero-spin"
|
||||
style={{
|
||||
width: 140,
|
||||
height: 140,
|
||||
@@ -355,8 +288,8 @@ export default function Hero() {
|
||||
marginTop: -70,
|
||||
marginLeft: -70,
|
||||
border: '2px solid rgba(91,164,217,0.18)',
|
||||
animationDuration: '30s',
|
||||
}}
|
||||
{...slowRotate}
|
||||
/>
|
||||
|
||||
{/* 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>
|
||||
|
||||
{/* Floating accent dot — orbiting slowly */}
|
||||
<motion.div
|
||||
className="absolute rounded-full"
|
||||
{/* Floating accent dot — CSS orbit */}
|
||||
<div
|
||||
className="absolute rounded-full hero-orbit-a"
|
||||
style={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
@@ -378,16 +311,11 @@ export default function Hero() {
|
||||
marginTop: -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 */}
|
||||
<motion.div
|
||||
className="absolute rounded-full"
|
||||
{/* Second accent dot — opposite CSS orbit */}
|
||||
<div
|
||||
className="absolute rounded-full hero-orbit-b"
|
||||
style={{
|
||||
width: 5,
|
||||
height: 5,
|
||||
@@ -397,11 +325,6 @@ export default function Hero() {
|
||||
marginTop: -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 */}
|
||||
|
||||
@@ -105,10 +105,10 @@ export default function Process() {
|
||||
Mobile layout:
|
||||
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 ── */}
|
||||
<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 */}
|
||||
<div className="relative pl-4">
|
||||
<div
|
||||
|
||||
@@ -49,7 +49,7 @@ function TrustCard({ item, index, t }: TrustCardProps) {
|
||||
<motion.div
|
||||
variants={revealVariants}
|
||||
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',
|
||||
'transition-shadow duration-300 hover:shadow-card',
|
||||
'overflow-hidden cursor-default',
|
||||
|
||||
@@ -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';
|
||||
|
||||
@theme {
|
||||
@@ -60,3 +61,69 @@
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user