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 (
|
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"
|
||||||
|
|||||||
@@ -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 */}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user