polish: rebuild hero with asymmetric layout + animated geometric composition
All checks were successful
Build & Push / build-and-push (push) Successful in 1m24s

- Asymmetric layout: text left-aligned (58%), geometric right (42%)
- SVG background: scaled down, pushed right, cleaner architectural lines
- Right column: 3 concentric rings rotating at different speeds/directions
- Orbiting accent dots tracing circular paths
- Tick marks, crosshair center, corner brackets, dimension lines
- Radial glow backdrop for atmospheric warmth
- Word-by-word headline stagger animation preserved

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 21:51:13 +01:00
parent d76ecbda7a
commit d033927896
2 changed files with 471 additions and 511 deletions

View File

@@ -1,402 +1,205 @@
import React from "react";
interface HeroGeometricProps { interface HeroGeometricProps {
className?: string; className?: string
} }
export default function HeroGeometric({ className }: HeroGeometricProps) { export default function HeroGeometric({ className }: HeroGeometricProps) {
// Arc center — pushed further right and scaled down
const cx = 760
const cy = 400
const R = 180 // Main radius — compact, not dominating
return ( return (
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1440 900" viewBox="0 0 1000 800"
preserveAspectRatio="xMidYMid slice" preserveAspectRatio="xMidYMid slice"
aria-hidden="true" aria-hidden="true"
className={className} className={className}
style={{ style={{
position: "absolute", position: 'absolute',
inset: 0, inset: 0,
width: "100%", width: '100%',
height: "100%", height: '100%',
pointerEvents: "none", pointerEvents: 'none',
overflow: "hidden",
}} }}
> >
<defs> {/* ━━━ BACKGROUND: Faint structural grid ━━━ */}
{/* Celestial blue at various opacities — all values halved for a more atmospheric effect */} <g data-layer="background" opacity="0.5">
<style>{` {/* Horizontal datum lines */}
.cb-fill-03 { fill: #5BA4D9; fill-opacity: 0.015; } <line x1="0" y1="200" x2="1000" y2="200" stroke="#1C2B3A" strokeOpacity="0.04" strokeWidth="0.5" strokeDasharray="8 24" />
.cb-fill-05 { fill: #5BA4D9; fill-opacity: 0.025; } <line x1="0" y1="420" x2="1000" y2="420" stroke="#1C2B3A" strokeOpacity="0.03" strokeWidth="0.5" strokeDasharray="6 20" />
.cb-fill-08 { fill: #5BA4D9; fill-opacity: 0.04; } <line x1="0" y1="640" x2="1000" y2="640" stroke="#1C2B3A" strokeOpacity="0.04" strokeWidth="0.5" strokeDasharray="8 24" />
.cb-fill-12 { fill: #5BA4D9; fill-opacity: 0.06; }
.cb-fill-15 { fill: #5BA4D9; fill-opacity: 0.07; }
.dn-stroke-06 { fill: none; stroke: #1C2B3A; stroke-opacity: 0.03; }
.dn-stroke-08 { fill: none; stroke: #1C2B3A; stroke-opacity: 0.04; }
.dn-stroke-10 { fill: none; stroke: #1C2B3A; stroke-opacity: 0.05; }
.dn-stroke-14 { fill: none; stroke: #1C2B3A; stroke-opacity: 0.07; }
.cb-stroke-08 { fill: none; stroke: #5BA4D9; stroke-opacity: 0.04; }
.cb-stroke-12 { fill: none; stroke: #5BA4D9; stroke-opacity: 0.06; }
.cb-dot { fill: #1C2B3A; fill-opacity: 0.06; }
.cb-dot-sm { fill: #5BA4D9; fill-opacity: 0.05; }
`}</style>
</defs>
{/* ─── BACKGROUND LAYER ─────────────────────────────────────────── */} {/* Vertical datum lines */}
<g data-layer="background"> <line x1="500" y1="0" x2="500" y2="800" stroke="#1C2B3A" strokeOpacity="0.03" strokeWidth="0.5" strokeDasharray="6 20" />
<line x1="680" y1="0" x2="680" y2="800" stroke="#1C2B3A" strokeOpacity="0.03" strokeWidth="0.5" strokeDasharray="4 18" />
{/* Large low-opacity rectangle — far upper-left anchor */}
<rect
x="-60" y="-40"
width="680" height="480"
rx="2"
className="cb-fill-03 dn-stroke-06"
strokeWidth="0.5"
/>
{/* Horizon guide line — full width */}
<line
x1="0" y1="520"
x2="1440" y2="520"
className="dn-stroke-06"
strokeWidth="0.5"
strokeDasharray="6 14"
/>
{/* Vertical axis line — far right third */}
<line
x1="1060" y1="0"
x2="1060" y2="900"
className="dn-stroke-06"
strokeWidth="0.5"
strokeDasharray="4 20"
/>
{/* Large background circle — lower-right anchor */}
<circle
cx="1280" cy="740"
r="320"
className="cb-fill-03 dn-stroke-06"
strokeWidth="0.5"
/>
{/* Secondary background circle — upper-right bleed */}
<circle
cx="1380" cy="-60"
r="200"
className="cb-fill-03 dn-stroke-08"
strokeWidth="0.5"
/>
{/* Wide shallow rectangle — lower band */}
<rect
x="180" y="760"
width="900" height="80"
rx="1"
className="cb-fill-03 dn-stroke-06"
strokeWidth="0.5"
/>
{/* Fine grid cluster — upper-left quadrant (sparse) */}
{/* Horizontal grid lines */}
{[80, 120, 160, 200, 240].map((y) => (
<line
key={`bg-hgrid-${y}`}
x1="40" y1={y}
x2="380" y2={y}
className="dn-stroke-06"
strokeWidth="0.5"
/>
))}
{/* Vertical grid lines */}
{[60, 100, 140, 180, 220, 260, 300, 340, 380].map((x) => (
<line
key={`bg-vgrid-${x}`}
x1={x} y1="60"
x2={x} y2="260"
className="dn-stroke-06"
strokeWidth="0.5"
/>
))}
{/* Blueprint registration mark — upper left */}
<line x1="28" y1="40" x2="28" y2="56" className="dn-stroke-10" strokeWidth="1" />
<line x1="20" y1="48" x2="36" y2="48" className="dn-stroke-10" strokeWidth="1" />
{/* Blueprint registration mark — far right mid */}
<line x1="1400" y1="418" x2="1400" y2="434" className="dn-stroke-10" strokeWidth="1" />
<line x1="1392" y1="426" x2="1408" y2="426" className="dn-stroke-10" strokeWidth="1" />
</g> </g>
{/* ─── MIDGROUND LAYER ──────────────────────────────────────────── */} {/* ━━━ MIDGROUND: The main arc composition ━━━ */}
<g data-layer="midground"> <g data-layer="midground">
{/* ── Main circle — the dominant visual element ── */}
{/* Large architectural rectangle — right column */} <circle
<rect cx={cx} cy={cy} r={R}
x="1100" y="80" fill="none"
width="260" height="560" stroke="#5BA4D9"
rx="3" strokeOpacity="0.09"
className="cb-fill-05 dn-stroke-08"
strokeWidth="0.75"
/>
{/* Inset rectangle inside large right column */}
<rect
x="1126" y="108"
width="208" height="120"
rx="2"
className="cb-fill-08 dn-stroke-10"
strokeWidth="0.75"
/>
{/* Tall thin divider — left of center */}
<rect
x="520" y="60"
width="2" height="420"
className="cb-fill-15"
/>
{/* Wide horizontal band — upper area */}
<rect
x="460" y="60"
width="540" height="1.5"
className="cb-fill-12"
/>
{/* Mid arc — upper right area, partial */}
<path
d="M 1060 160 A 180 180 0 0 1 1240 160"
className="cb-stroke-08"
strokeWidth="1" strokeWidth="1"
/> />
{/* Concentric arc pair — lower left */} {/* Inner concentric ring */}
<path
d="M 100 900 A 340 340 0 0 1 440 560"
className="dn-stroke-08"
strokeWidth="0.75"
/>
<path
d="M 60 900 A 380 380 0 0 1 440 520"
className="dn-stroke-06"
strokeWidth="0.5"
strokeDasharray="3 9"
/>
{/* Section label rectangle — left */}
<rect
x="60" y="320"
width="160" height="44"
rx="2"
className="cb-fill-08 dn-stroke-10"
strokeWidth="0.75"
/>
{/* Thin horizontal rule under label */}
<line
x1="60" y1="374"
x2="220" y2="374"
className="dn-stroke-08"
strokeWidth="0.75"
/>
{/* Dotted measurement track — horizontal mid */}
<line
x1="560" y1="440"
x2="980" y2="440"
className="dn-stroke-08"
strokeWidth="0.75"
strokeDasharray="2 6"
/>
{/* Tick marks on measurement track */}
{[560, 640, 720, 800, 880, 980].map((x) => (
<line
key={`tick-${x}`}
x1={x} y1="434"
x2={x} y2="446"
className="dn-stroke-10"
strokeWidth="0.75"
/>
))}
{/* Small dimension rectangle — lower center */}
<rect
x="680" y="620"
width="200" height="120"
rx="2"
className="cb-fill-05 dn-stroke-08"
strokeWidth="0.75"
/>
{/* Diagonal tension line — upper center to right */}
<line
x1="520" y1="60"
x2="1100" y2="640"
className="dn-stroke-06"
strokeWidth="0.5"
/>
{/* Medium circle — center-left */}
<circle <circle
cx="380" cy="480" cx={cx} cy={cy} r={R * 0.65}
r="90" fill="none"
className="cb-fill-05 dn-stroke-08" stroke="#5BA4D9"
strokeOpacity="0.05"
strokeWidth="0.75" strokeWidth="0.75"
strokeDasharray="4 12"
/> />
{/* Inner ring of medium circle */} {/* Outer concentric ring — larger, fainter */}
<circle <circle
cx="380" cy="480" cx={cx} cy={cy} r={R * 1.35}
r="60" fill="none"
className="cb-stroke-08" stroke="#1C2B3A"
strokeOpacity="0.035"
strokeWidth="0.5" strokeWidth="0.5"
strokeDasharray="3 16"
/>
{/* ── Radius lines from center — architectural annotation ── */}
{/* Vertical radius */}
<line
x1={cx} y1={cy} x2={cx} y2={cy - R}
stroke="#5BA4D9" strokeOpacity="0.08" strokeWidth="0.75"
/>
{/* Top tick mark */}
<line
x1={cx - 8} y1={cy - R} x2={cx + 8} y2={cy - R}
stroke="#5BA4D9" strokeOpacity="0.12" strokeWidth="0.75"
/>
{/* Diagonal radius — 45deg upper-right */}
<line
x1={cx} y1={cy}
x2={cx + R * 0.707} y2={cy - R * 0.707}
stroke="#1C2B3A" strokeOpacity="0.06" strokeWidth="0.5"
strokeDasharray="4 8" strokeDasharray="4 8"
/> />
{/* Small circle — upper right cluster */} {/* Horizontal radius — right */}
<circle <line
cx="1200" cy="260" x1={cx} y1={cy} x2={cx + R} y2={cy}
r="36" stroke="#5BA4D9" strokeOpacity="0.06" strokeWidth="0.5"
className="cb-fill-08 dn-stroke-10" />
{/* Right tick */}
<line
x1={cx + R} y1={cy - 8} x2={cx + R} y2={cy + 8}
stroke="#5BA4D9" strokeOpacity="0.1" strokeWidth="0.75"
/>
{/* ── Center crosshair ── */}
<line x1={cx - 12} y1={cy} x2={cx + 12} y2={cy} stroke="#5BA4D9" strokeOpacity="0.15" strokeWidth="0.75" />
<line x1={cx} y1={cy - 12} x2={cx} y2={cy + 12} stroke="#5BA4D9" strokeOpacity="0.15" strokeWidth="0.75" />
<circle cx={cx} cy={cy} r="2.5" fill="#5BA4D9" fillOpacity="0.12" />
{/* ── Angle arc at center — 90deg sweep ── */}
<path
d={`M ${cx} ${cy - 30} A 30 30 0 0 1 ${cx + 30} ${cy}`}
fill="none"
stroke="#5BA4D9"
strokeOpacity="0.1"
strokeWidth="0.75" strokeWidth="0.75"
/> />
{/* Dot grid — right of center, 4x5 */} {/* ── Dimension annotation — horizontal, above circle ── */}
{[0, 1, 2, 3].map((col) => <line
x1={cx - R} y1={cy - R - 30}
x2={cx + R} y2={cy - R - 30}
stroke="#1C2B3A" strokeOpacity="0.07" strokeWidth="0.5"
/>
{/* End ticks */}
<line x1={cx - R} y1={cy - R - 38} x2={cx - R} y2={cy - R - 22} stroke="#1C2B3A" strokeOpacity="0.09" strokeWidth="0.75" />
<line x1={cx + R} y1={cy - R - 38} x2={cx + R} y2={cy - R - 22} stroke="#1C2B3A" strokeOpacity="0.09" strokeWidth="0.75" />
{/* Label stub */}
<rect x={cx - 18} y={cy - R - 37} width="36" height="7" rx="1" fill="#5BA4D9" fillOpacity="0.03" />
{/* ── Vertical dimension — right side ── */}
<line
x1={cx + R + 30} y1={cy - R}
x2={cx + R + 30} y2={cy + R}
stroke="#1C2B3A" strokeOpacity="0.06" strokeWidth="0.5"
strokeDasharray="2 8"
/>
<line x1={cx + R + 22} y1={cy - R} x2={cx + R + 38} y2={cy - R} stroke="#1C2B3A" strokeOpacity="0.08" strokeWidth="0.75" />
<line x1={cx + R + 22} y1={cy + R} x2={cx + R + 38} y2={cy + R} stroke="#1C2B3A" strokeOpacity="0.08" strokeWidth="0.75" />
{/* Subtle fill — atmospheric glow behind main circle */}
<circle cx={cx} cy={cy} r={R * 0.8} fill="#5BA4D9" fillOpacity="0.015" />
</g>
{/* ━━━ FOREGROUND: Crisp detail elements ━━━ */}
<g data-layer="foreground">
{/* ── Corner brackets — architectural framing ── */}
{/* Top-right */}
<path d="M 940 40 L 970 40 L 970 70" fill="none" stroke="#1C2B3A" strokeOpacity="0.12" strokeWidth="1" />
{/* Bottom-right */}
<path d="M 940 760 L 970 760 L 970 730" fill="none" stroke="#1C2B3A" strokeOpacity="0.08" strokeWidth="0.75" />
{/* Top inner */}
<path d="M 560 80 L 590 80" fill="none" stroke="#1C2B3A" strokeOpacity="0.06" strokeWidth="0.75" />
<path d="M 560 80 L 560 110" fill="none" stroke="#1C2B3A" strokeOpacity="0.06" strokeWidth="0.75" />
{/* ── Tick marks around the main circle — spaced at 30deg intervals ── */}
{[0, 30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330].map((deg) => {
const rad = (deg * Math.PI) / 180
const x = cx + R * Math.cos(rad)
const y = cy + R * Math.sin(rad)
const nx = Math.cos(rad)
const ny = Math.sin(rad)
return (
<line
key={`tick-${deg}`}
x1={x - nx * 6} y1={y - ny * 6}
x2={x + nx * 6} y2={y + ny * 6}
stroke="#5BA4D9"
strokeOpacity="0.1"
strokeWidth="0.75"
/>
)
})}
{/* ── Small precision arc — upper right ── */}
<path
d={`M ${cx + 60} ${cy - R + 40} A 50 50 0 0 1 ${cx + 110} ${cy - R + 40}`}
fill="none"
stroke="#5BA4D9"
strokeOpacity="0.12"
strokeWidth="0.75"
/>
{/* ── Dot grid cluster — lower right quadrant ── */}
{[0, 1, 2, 3, 4, 5].map((col) =>
[0, 1, 2, 3, 4].map((row) => ( [0, 1, 2, 3, 4].map((row) => (
<circle <circle
key={`dot-${col}-${row}`} key={`dot-${col}-${row}`}
cx={760 + col * 28} cx={780 + col * 28}
cy={160 + row * 28} cy={560 + row * 28}
r="1.5" r="1"
className="cb-dot" fill="#1C2B3A"
fillOpacity="0.06"
/> />
)) ))
)} )}
</g>
{/* ─── FOREGROUND LAYER ─────────────────────────────────────────── */} {/* ── Scattered accent dots ── */}
<g data-layer="foreground"> <circle cx={cx + 140} cy={cy - 80} r="2" fill="#5BA4D9" fillOpacity="0.1" />
<circle cx={cx - 120} cy={cy + 100} r="1.5" fill="#5BA4D9" fillOpacity="0.07" />
<circle cx={cx + 200} cy={cy + 150} r="1.5" fill="#1C2B3A" fillOpacity="0.06" />
{/* Blueprint frame — upper left inset panel */} {/* ── Sparse left-side balance elements ── */}
<rect <line x1="60" y1="350" x2="180" y2="350" stroke="#1C2B3A" strokeOpacity="0.03" strokeWidth="0.5" strokeDasharray="4 16" />
x="80" y="80" <line x1="100" y1="500" x2="100" y2="530" stroke="#1C2B3A" strokeOpacity="0.04" strokeWidth="0.5" />
width="320" height="200" <line x1="88" y1="515" x2="112" y2="515" stroke="#1C2B3A" strokeOpacity="0.04" strokeWidth="0.5" />
rx="2"
className="cb-fill-05 dn-stroke-14"
strokeWidth="1"
/>
{/* Inner division of upper-left panel */}
<line
x1="80" y1="160"
x2="400" y2="160"
className="dn-stroke-10"
strokeWidth="0.75"
/>
<line
x1="240" y1="80"
x2="240" y2="280"
className="dn-stroke-10"
strokeWidth="0.75"
/>
{/* Corner notches on blueprint frame */}
<path d="M 80 100 L 80 80 L 100 80" className="dn-stroke-14" strokeWidth="1" fill="none" />
<path d="M 380 80 L 400 80 L 400 100" className="dn-stroke-14" strokeWidth="1" fill="none" />
<path d="M 80 260 L 80 280 L 100 280" className="dn-stroke-14" strokeWidth="1" fill="none" />
<path d="M 380 280 L 400 280 L 400 260" className="dn-stroke-14" strokeWidth="1" fill="none" />
{/* Precision arc — upper left panel, quarter circle */}
<path
d="M 160 80 A 80 80 0 0 1 240 160"
className="cb-stroke-12"
strokeWidth="1"
/>
{/* Foreground tall rect — left bleed */}
<rect
x="-20" y="400"
width="100" height="340"
rx="2"
className="cb-fill-08 dn-stroke-10"
strokeWidth="0.75"
/>
{/* Foreground arc — lower right */}
<path
d="M 1140 640 A 240 240 0 0 0 1380 640"
className="dn-stroke-10"
strokeWidth="1"
/>
<path
d="M 1160 640 A 200 200 0 0 0 1360 640"
className="cb-stroke-12"
strokeWidth="0.75"
strokeDasharray="3 7"
/>
{/* Callout line — from blueprint panel to right */}
<line
x1="400" y1="180"
x2="520" y2="180"
className="dn-stroke-14"
strokeWidth="0.75"
/>
<circle cx="520" cy="180" r="3" className="cb-fill-15" />
{/* Thin vertical guide — right of blueprint panel */}
<line
x1="520" y1="80"
x2="520" y2="360"
className="dn-stroke-08"
strokeWidth="0.5"
strokeDasharray="2 8"
/>
{/* Small accent square — upper center */}
<rect
x="690" y="100"
width="48" height="48"
rx="1"
className="cb-fill-12 dn-stroke-14"
strokeWidth="1"
transform="rotate(12 714 124)"
/>
{/* Dot accent — scattered foreground points */}
<circle cx="460" cy="320" r="2.5" className="cb-dot-sm" />
<circle cx="560" cy="280" r="2" className="cb-dot-sm" />
<circle cx="600" cy="340" r="1.5" className="cb-dot-sm" />
<circle cx="1080" cy="680" r="2.5" className="cb-dot-sm" />
<circle cx="1140" cy="700" r="1.5" className="cb-dot-sm" />
<circle cx="300" cy="600" r="2" className="cb-dot-sm" />
<circle cx="340" cy="560" r="1.5" className="cb-dot-sm" />
{/* Small labeled rectangle — lower left */}
<rect
x="100" y="700"
width="120" height="60"
rx="1"
className="cb-fill-08 dn-stroke-10"
strokeWidth="0.75"
/>
{/* Tick detail inside small rect */}
<line x1="100" y1="720" x2="220" y2="720" className="dn-stroke-08" strokeWidth="0.5" />
<line x1="160" y1="700" x2="160" y2="760" className="dn-stroke-08" strokeWidth="0.5" />
{/* Fine detail lines — lower right corner cluster */}
<line x1="1300" y1="820" x2="1440" y2="820" className="dn-stroke-08" strokeWidth="0.5" />
<line x1="1300" y1="840" x2="1440" y2="840" className="dn-stroke-06" strokeWidth="0.5" strokeDasharray="3 6" />
<line x1="1320" y1="800" x2="1320" y2="900" className="dn-stroke-08" strokeWidth="0.5" />
<circle cx="1320" cy="820" r="3" className="cb-fill-15" />
{/* Radius annotation arc — center large circle */}
<path
d="M 320 480 L 380 480"
className="dn-stroke-14"
strokeWidth="0.75"
/>
<circle cx="380" cy="480" r="2" className="cb-fill-15" />
</g> </g>
</svg> </svg>
); )
} }

View File

@@ -2,18 +2,21 @@
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { cn } from '@/lib/utils';
import Button from '@/components/ui/Button'; import Button from '@/components/ui/Button';
import HeroGeometric from '@/components/icons/HeroGeometric'; import HeroGeometric from '@/components/icons/HeroGeometric';
// Slow drift animation for the SVG background layers // ─── Animation variants ────────────────────────────────────────────────────
const EASE_OUT_EXPO = [0.16, 1, 0.3, 1] as [number, number, number, number];
// Background SVG drift — slow, almost imperceptible
const bgDriftA = { const bgDriftA = {
animate: { animate: {
y: [0, -12, 0], y: [0, -10, 0],
x: [0, 6, 0], x: [0, 5, 0],
transition: { transition: {
duration: 18, duration: 22,
ease: 'easeInOut' as const, ease: 'easeInOut' as const,
repeat: Infinity, repeat: Infinity,
repeatType: 'loop' as const, repeatType: 'loop' as const,
@@ -23,11 +26,10 @@ const bgDriftA = {
const bgDriftB = { const bgDriftB = {
animate: { animate: {
y: [0, 8, 0], y: [0, 7, 0],
x: [0, -8, 0], x: [0, -6, 0],
scale: [1, 1.015, 1],
transition: { transition: {
duration: 24, duration: 28,
ease: 'easeInOut' as const, ease: 'easeInOut' as const,
repeat: Infinity, repeat: Infinity,
repeatType: 'loop' as const, repeatType: 'loop' as const,
@@ -35,190 +37,195 @@ const bgDriftB = {
}, },
}; };
const bgRotate = { // Headline word stagger container
animate: { const headlineContainer = {
rotate: [0, 1.5, 0, -1.5, 0],
transition: {
duration: 30,
ease: 'easeInOut' as const,
repeat: Infinity,
repeatType: 'loop' as const,
},
},
};
// Word-level stagger container — fires on mount (not scroll)
const heroHeadlineContainer = {
hidden: {}, hidden: {},
visible: { visible: {
transition: { transition: {
staggerChildren: 0.07, staggerChildren: 0.065,
delayChildren: 0.15, delayChildren: 0.2,
}, },
}, },
}; };
// Individual word reveal // Per-word reveal — slide up + blur clear
const wordReveal = { const wordReveal = {
hidden: { opacity: 0, y: 48, filter: 'blur(4px)' }, hidden: { opacity: 0, y: 52, filter: 'blur(6px)' },
visible: { visible: {
opacity: 1, opacity: 1,
y: 0, y: 0,
filter: 'blur(0px)', filter: 'blur(0px)',
transition: { transition: {
duration: 0.7, duration: 0.72,
ease: [0.16, 1, 0.3, 1] as [number, number, number, number], ease: EASE_OUT_EXPO,
}, },
}, },
}; };
// Subtitle fade — delay after headline // Eyebrow label fade
const subtitleVariant = { const eyebrowVariant = {
hidden: { opacity: 0, y: 20 }, hidden: { opacity: 0, y: 14 },
visible: { visible: {
opacity: 1, opacity: 1,
y: 0, y: 0,
transition: { transition: { duration: 0.5, delay: 0.08, ease: EASE_OUT_EXPO },
duration: 0.6,
delay: 0.5,
ease: [0.16, 1, 0.3, 1] as [number, number, number, number],
},
}, },
}; };
// CTA scale-in // Subtitle fade-up — delayed after headline completes
const ctaVariant = { const subtitleVariant = {
hidden: { opacity: 0, scale: 0.93 }, hidden: { opacity: 0, y: 18 },
visible: { visible: {
opacity: 1, opacity: 1,
scale: 1, y: 0,
transition: { transition: { duration: 0.6, delay: 0.6, ease: EASE_OUT_EXPO },
duration: 0.45,
delay: 0.7,
ease: [0.16, 1, 0.3, 1] as [number, number, number, number],
},
}, },
}; };
// Decorative separator line fade-in — after subtitle // Separator expand from origin-left
const separatorVariant = { const separatorVariant = {
hidden: { opacity: 0, scaleX: 0 }, hidden: { opacity: 0, scaleX: 0 },
visible: { visible: {
opacity: 1, opacity: 1,
scaleX: 1, scaleX: 1,
transition: { duration: 0.55, delay: 0.72, ease: EASE_OUT_EXPO },
},
};
// CTA fade in
const ctaVariant = {
hidden: { opacity: 0, y: 12 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.5, delay: 0.82, ease: EASE_OUT_EXPO },
},
};
// Right column entrance — SVG composition fades in from right
const rightColumnVariant = {
hidden: { opacity: 0, x: 24 },
visible: {
opacity: 1,
x: 0,
transition: { duration: 0.9, delay: 0.1, ease: EASE_OUT_EXPO },
},
};
// Slow full rotation for the large decorative ring
const slowRotate = {
animate: {
rotate: [0, 360],
transition: { transition: {
duration: 0.6, duration: 30,
delay: 0.65, ease: 'linear' as const,
ease: [0.16, 1, 0.3, 1] as [number, number, number, number], repeat: Infinity,
}, },
}, },
}; };
// ─── Component ────────────────────────────────────────────────────────────
export default function Hero() { export default function Hero() {
const t = useTranslations('hero'); const t = useTranslations('hero');
// Split the raw title by the {exclusively} placeholder so we can inject the styled <em> // Split the raw title on the {exclusively} placeholder
// Expected translation shape: "Built {exclusively} for ambitious brands." // Expected translation: "Built {exclusively} for ambitious brands."
const rawTitle: string = t.raw('title'); const rawTitle: string = t.raw('title');
const parts = rawTitle.split('{exclusively}'); const parts = rawTitle.split('{exclusively}');
const before = parts[0] ?? ''; const before = parts[0] ?? '';
const after = parts[1] ?? ''; const after = parts[1] ?? '';
// Split each segment into words for per-word animation // Build flat word array with type tagging for accent word
const beforeWords = before.trim() ? before.trim().split(' ') : [];
const afterWords = after.trim() ? after.trim().split(' ') : [];
const exclusivelyWord = 'exclusively';
// All words in order: before + [exclusively] + after
type WordItem = type WordItem =
| { type: 'normal'; text: string } | { type: 'normal'; text: string }
| { type: 'accent'; text: string }; | { type: 'accent'; text: string };
const beforeWords: WordItem[] = before.trim()
? before.trim().split(' ').map((w) => ({ type: 'normal', text: w }))
: [];
const afterWords: WordItem[] = after.trim()
? after.trim().split(' ').map((w) => ({ type: 'normal', text: w }))
: [];
const allWords: WordItem[] = [ const allWords: WordItem[] = [
...beforeWords.map((w) => ({ type: 'normal' as const, text: w })), ...beforeWords,
{ type: 'accent' as const, text: exclusivelyWord }, { type: 'accent', text: 'exclusively' },
...afterWords.map((w) => ({ type: 'normal' as const, text: w })), ...afterWords,
]; ];
return ( return (
<section <section
id="hero" id="hero"
aria-label="Hero" aria-label="Hero"
className="relative min-h-screen flex flex-col items-center justify-center overflow-hidden bg-surface-high" className="relative min-h-screen flex flex-col overflow-hidden bg-surface"
> >
{/* ─── Background: animated SVG layers ─────────────────────────── */} {/* ─── Full-bleed SVG background — spans entire section ─────────── */}
<motion.div <motion.div
className="absolute inset-0 z-0 pointer-events-none" className="absolute inset-0 z-0 pointer-events-none"
{...bgDriftA} {...bgDriftA}
> >
<motion.div className="absolute inset-0" {...bgRotate}>
<HeroGeometric className="absolute inset-0 w-full h-full" /> <HeroGeometric className="absolute inset-0 w-full h-full" />
</motion.div> </motion.div>
</motion.div>
{/* Subtle radial gradient vignette over the SVG */} {/* ─── Radial glow — soft primary haze in right column ──────────── */}
<motion.div <motion.div
className="absolute inset-0 z-0 pointer-events-none" className="absolute inset-0 z-0 pointer-events-none"
{...bgDriftB} {...bgDriftB}
style={{ style={{
background: background:
'radial-gradient(ellipse 70% 60% at 50% 45%, transparent 30%, rgba(248,249,250,0.55) 100%)', 'radial-gradient(ellipse 55% 70% at 78% 48%, rgba(91,164,217,0.045) 0%, transparent 70%)',
}} }}
aria-hidden="true"
/> />
{/* ─── Content ──────────────────────────────────────────────────── */} {/* ─── Asymmetric two-column layout ─────────────────────────────── */}
<div className="relative z-10 w-full max-w-4xl mx-auto px-6 pt-32 md:pt-40 pb-24 flex flex-col items-center text-center"> <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 */} {/* Eyebrow */}
<motion.span <motion.span
className="label-md text-primary mb-6 tracking-widest uppercase" className="label-md text-primary tracking-widest uppercase mb-6 block"
initial={{ opacity: 0, y: 16 }} variants={eyebrowVariant}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.05, ease: [0.16, 1, 0.3, 1] }}
>
BESPOKE DIGITAL STUDIO
</motion.span>
{/* Headline with per-word stagger */}
<motion.h1
className={cn(
'font-serif font-semibold text-on-surface',
'text-5xl sm:text-6xl md:text-7xl leading-[1.08] tracking-[-0.02em]',
'mb-6 flex flex-wrap items-baseline justify-center gap-x-[0.25em] gap-y-1',
)}
variants={heroHeadlineContainer}
initial="hidden" initial="hidden"
animate="visible" animate="visible"
aria-label={rawTitle.replace('{exclusively}', exclusivelyWord)} >
Bespoke Digital Studio
</motion.span>
{/* Headline — word-by-word stagger */}
<motion.h1
className="font-serif font-semibold text-on-surface text-5xl sm:text-6xl lg:text-[4.25rem] xl:text-7xl leading-[1.07] tracking-[-0.025em] mb-7 flex flex-wrap items-baseline gap-x-[0.22em] gap-y-1"
variants={headlineContainer}
initial="hidden"
animate="visible"
aria-label={rawTitle.replace('{exclusively}', 'exclusively')}
> >
{allWords.map((word, i) => {allWords.map((word, i) =>
word.type === 'accent' ? ( word.type === 'accent' ? (
<motion.span <motion.span
key={`word-accent-${i}`} key={`word-accent-${i}`}
variants={wordReveal} variants={wordReveal}
className="inline-block overflow-hidden"
style={{ display: 'inline-block' }} style={{ display: 'inline-block' }}
> >
<em className="text-primary font-serif italic not-italic"> <em className="text-primary font-serif italic">{word.text}</em>
{word.text}
</em>
</motion.span> </motion.span>
) : ( ) : (
<motion.span <motion.span
key={`word-${i}`} key={`word-${i}`}
variants={wordReveal} variants={wordReveal}
className="inline-block"
style={{ display: 'inline-block' }} style={{ display: 'inline-block' }}
> >
{word.text} {word.text}
</motion.span> </motion.span>
), )
)} )}
</motion.h1> </motion.h1>
{/* Subtitle */} {/* Subtitle */}
<motion.p <motion.p
className="text-lg text-outline leading-relaxed max-w-xl mb-8" className="text-lg text-outline leading-relaxed max-w-lg mb-8"
variants={subtitleVariant} variants={subtitleVariant}
initial="hidden" initial="hidden"
animate="visible" animate="visible"
@@ -226,12 +233,12 @@ export default function Hero() {
{t('subtitle')} {t('subtitle')}
</motion.p> </motion.p>
{/* Decorative gradient separator */} {/* Gradient separator — expands from left */}
<motion.span <motion.span
className="block w-20 h-px mb-10 origin-center" className="block w-20 h-px mb-10 origin-left"
style={{ style={{
background: background:
'linear-gradient(to right, rgba(0,100,148,0.3), rgba(91,164,217,0.3))', 'linear-gradient(to right, rgba(0,100,148,0.5), rgba(91,164,217,0.2), transparent)',
}} }}
variants={separatorVariant} variants={separatorVariant}
initial="hidden" initial="hidden"
@@ -241,31 +248,181 @@ export default function Hero() {
{/* CTA row */} {/* CTA row */}
<motion.div <motion.div
className="flex flex-col sm:flex-row items-center gap-4" className="flex flex-col sm:flex-row items-start gap-4"
variants={ctaVariant} variants={ctaVariant}
initial="hidden" initial="hidden"
animate="visible" animate="visible"
> >
<Button variant="primary" size="lg" arrow href="#configure"> <Button
variant="primary"
size="lg"
arrow
href="#configure"
>
{t('cta')} {t('cta')}
</Button> </Button>
<Button <Button
variant="secondary" variant="secondary"
size="lg" size="lg"
href="#work" href="#work"
className="bg-on-surface/5 hover:bg-on-surface/10"
> >
{t('ctaSecondary')} {t('ctaSecondary')}
</Button> </Button>
</motion.div> </motion.div>
</div> </div>
{/* Bottom fade-out to next section */} {/* ── RIGHT COLUMN — animated geometric composition (42%) ────── */}
<motion.div
className="hidden lg:flex flex-1 lg:max-w-[42%] relative self-stretch items-center justify-center"
variants={rightColumnVariant}
initial="hidden"
animate="visible"
aria-hidden="true"
>
<div className="relative w-full h-full min-h-[500px] flex items-center justify-center">
{/* Radial glow — warm atmospheric backdrop */}
<div <div
className="absolute bottom-0 left-0 right-0 h-24 pointer-events-none z-10" className="absolute rounded-full"
style={{
width: 500,
height: 500,
top: '50%',
left: '45%',
transform: 'translate(-50%, -50%)',
background: 'radial-gradient(circle, rgba(91,164,217,0.07) 0%, rgba(91,164,217,0.02) 40%, transparent 70%)',
}}
/>
{/* Outer ring — large, slow counter-rotation */}
<motion.div
className="absolute rounded-full"
style={{
width: 380,
height: 380,
top: '50%',
left: '45%',
marginTop: -190,
marginLeft: -190,
border: '1.5px solid rgba(91,164,217,0.12)',
}}
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}
className="absolute"
style={{
width: 1,
height: 10,
background: 'rgba(91,164,217,0.2)',
top: -5,
left: '50%',
marginLeft: -0.5,
transformOrigin: `50% ${190 + 5}px`,
transform: `rotate(${deg}deg)`,
}}
/>
))}
</motion.div>
{/* Inner ring — dashed, rotating forward */}
<motion.div
className="absolute rounded-full"
style={{
width: 240,
height: 240,
top: '50%',
left: '45%',
marginTop: -120,
marginLeft: -120,
border: '1px dashed rgba(0,100,148,0.15)',
}}
animate={{ rotate: [0, 360] }}
transition={{ duration: 40, ease: 'linear', repeat: Infinity }}
/>
{/* Core ring — solid, slowly rotating */}
<motion.div
className="absolute rounded-full"
style={{
width: 140,
height: 140,
top: '50%',
left: '45%',
marginTop: -70,
marginLeft: -70,
border: '2px solid rgba(91,164,217,0.18)',
}}
{...slowRotate}
/>
{/* Center crosshair */}
<div className="absolute" style={{ top: '50%', left: '45%', transform: 'translate(-50%, -50%)' }}>
<div style={{ width: 24, height: 1, background: 'rgba(91,164,217,0.25)', position: 'absolute', top: 0, left: -12 }} />
<div style={{ width: 1, height: 24, background: 'rgba(91,164,217,0.25)', position: 'absolute', top: -12, left: 0 }} />
<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"
style={{
width: 8,
height: 8,
background: 'rgba(91,164,217,0.3)',
top: '50%',
left: '45%',
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"
style={{
width: 5,
height: 5,
background: 'rgba(0,100,148,0.25)',
top: '50%',
left: '45%',
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 */}
<div className="absolute" style={{ top: '15%', right: '8%', width: 48, height: 48, borderTop: '1.5px solid rgba(28,43,58,0.2)', borderRight: '1.5px solid rgba(28,43,58,0.2)' }} />
<div className="absolute" style={{ bottom: '18%', left: '8%', width: 40, height: 40, borderBottom: '1.5px solid rgba(28,43,58,0.15)', borderLeft: '1.5px solid rgba(28,43,58,0.15)' }} />
{/* Dimension line — vertical, right side */}
<div className="absolute" style={{ right: '5%', top: '25%', bottom: '30%', width: 1, background: 'rgba(28,43,58,0.08)' }}>
<div style={{ position: 'absolute', top: 0, left: -4, width: 9, height: 1, background: 'rgba(28,43,58,0.12)' }} />
<div style={{ position: 'absolute', bottom: 0, left: -4, width: 9, height: 1, background: 'rgba(28,43,58,0.12)' }} />
</div>
</div>
</motion.div>
</div>
{/* ─── Bottom fade into next section ────────────────────────────── */}
<div
className="absolute bottom-0 left-0 right-0 h-28 pointer-events-none z-10"
style={{ style={{
background: background:
'linear-gradient(to bottom, transparent, rgba(248,249,250,0.6))', 'linear-gradient(to bottom, transparent, rgba(248,249,250,0.65))',
}} }}
aria-hidden="true" aria-hidden="true"
/> />