feat: complete agency site build (Phases 1-7)

Full Next.js 16 + Payload CMS 3.x agency site with:
- Homepage: Hero, TrustBar, Services, Configurator wizard, Process,
  Selected Works, Philosophy, CTA Banner
- Sub-pages: /services (3 pillars + AI Layer), /work/[slug] (case
  studies), /about (philosophy + story)
- Configurator: 3-step wizard with AI brief generation API
- i18n: Full EN/FR bilingual with next-intl
- Design system: Cormorant Garamond + Inter, celestial blue palette,
  glassmorphism nav, Framer Motion animations
- Payload CMS collections: Projects, Services, Submissions, Media

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 20:37:38 +01:00
commit a1f9eca76c
64 changed files with 15810 additions and 0 deletions

View File

@@ -0,0 +1,224 @@
'use client';
import { motion } from 'framer-motion';
import { useTranslations } from 'next-intl';
import { Mail } from 'lucide-react';
import { cn } from '@/lib/utils';
import { viewportOnce } from '@/lib/animations';
import Button from '@/components/ui/Button';
// The full-width background clips in from the center (scaleX)
const bgScaleVariants = {
hidden: { scaleX: 0, opacity: 0 },
visible: {
scaleX: 1,
opacity: 1,
transition: {
duration: 0.9,
ease: [0.16, 1, 0.3, 1] as [number, number, number, number],
},
},
};
// Content fades up slightly after the background arrives
const contentContainerVariants = {
hidden: {},
visible: {
transition: {
staggerChildren: 0.1,
delayChildren: 0.35,
},
},
};
const contentItemVariants = {
hidden: { opacity: 0, y: 28 },
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.6,
ease: [0.16, 1, 0.3, 1] as [number, number, number, number],
},
},
};
// Decorative skewed band (white-tinted)
function DecorativeBand() {
return (
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 overflow-hidden"
>
{/* Large skewed element — upper-left to center */}
<div
className="absolute -top-24 -left-32 w-[520px] h-[340px] bg-white/[0.04] rounded-3xl"
style={{ transform: 'rotate(-14deg) skewX(-8deg)' }}
/>
{/* Smaller skewed element — lower-right */}
<div
className="absolute bottom-0 right-0 w-[360px] h-[220px] bg-white/[0.05] rounded-2xl"
style={{ transform: 'rotate(10deg) skewX(6deg) translate(60px, 40px)' }}
/>
{/* Fine diagonal line accent — top-left */}
<div
className="absolute top-8 left-12 w-[160px] h-[1px] bg-white/10"
style={{ transform: 'rotate(-30deg)' }}
/>
<div
className="absolute top-12 left-16 w-[100px] h-[1px] bg-white/8"
style={{ transform: 'rotate(-30deg)' }}
/>
{/* Dot cluster — top-right */}
{[0, 1, 2, 3, 4].map((col) =>
[0, 1, 2].map((row) => (
<div
key={`dot-${col}-${row}`}
className="absolute w-[3px] h-[3px] rounded-full bg-white/10"
style={{
top: `${28 + row * 14}px`,
right: `${80 + col * 14}px`,
}}
/>
)),
)}
{/* Subtle arc — bottom center */}
<svg
className="absolute bottom-0 left-1/2 -translate-x-1/2"
width="600"
height="120"
viewBox="0 0 600 120"
fill="none"
aria-hidden="true"
>
<path
d="M 0 120 Q 300 40 600 120"
stroke="rgba(255,255,255,0.05)"
strokeWidth="1.5"
fill="none"
/>
<path
d="M 60 120 Q 300 60 540 120"
stroke="rgba(255,255,255,0.04)"
strokeWidth="1"
fill="none"
strokeDasharray="4 10"
/>
</svg>
</div>
);
}
export default function CTABanner() {
const t = useTranslations('cta');
return (
<section
aria-label="Call to action"
className="relative overflow-hidden"
>
{/* Animated background that clips in from center */}
<motion.div
variants={bgScaleVariants}
initial="hidden"
whileInView="visible"
viewport={viewportOnce}
className="absolute inset-0 origin-center"
style={{ backgroundColor: '#006494' }}
>
<DecorativeBand />
</motion.div>
{/* Content — positioned relative so it sits above the bg */}
<div className="relative z-10 py-24 px-6">
<motion.div
variants={contentContainerVariants}
initial="hidden"
whileInView="visible"
viewport={viewportOnce}
className="max-w-3xl mx-auto flex flex-col items-center text-center gap-8"
>
{/* Eyebrow */}
<motion.span
variants={contentItemVariants}
className="label-md text-white/60 tracking-widest uppercase"
>
{t('eyebrow')}
</motion.span>
{/* Headline */}
<motion.h2
variants={contentItemVariants}
className={cn(
'font-serif font-semibold text-white',
'text-4xl md:text-5xl leading-[1.1] tracking-[-0.02em]',
)}
>
{t('title')}
</motion.h2>
{/* Supporting text */}
<motion.p
variants={contentItemVariants}
className="text-white/70 text-lg leading-relaxed max-w-xl"
>
{t('subtitle')}
</motion.p>
{/* CTA row */}
<motion.div
variants={contentItemVariants}
className="flex flex-col sm:flex-row items-center gap-4"
>
{/* Primary CTA — white-styled button */}
<Button
variant="secondary"
size="lg"
arrow
href="#configure"
className={cn(
'ring-white/80 text-white',
'hover:bg-white/10 hover:ring-white',
'focus-visible:ring-white',
)}
>
{t('cta')}
</Button>
{/* Email link — ghost style */}
<a
href="mailto:hello@letsbe.biz"
className={cn(
'inline-flex items-center gap-2 px-6 py-3 rounded-xl',
'text-white/80 text-sm font-medium',
'transition-all duration-200',
'hover:text-white hover:bg-white/8',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2 focus-visible:ring-offset-[#006494]',
)}
>
<Mail
size={16}
className="shrink-0"
aria-hidden="true"
strokeWidth={2}
/>
hello@letsbe.biz
</a>
</motion.div>
{/* Fine-print reassurance */}
<motion.p
variants={contentItemVariants}
className="text-white/40 text-xs"
>
{t('reassurance')}
</motion.p>
</motion.div>
</div>
</section>
);
}

View File

@@ -0,0 +1,114 @@
'use client';
import { useTranslations } from 'next-intl';
import { motion } from 'framer-motion';
import { revealVariants, staggerContainer, viewportOnce } from '@/lib/animations';
import WizardContainer from '@/components/configurator/WizardContainer';
// ─── Step indicator dot ───────────────────────────────────────────────────────
interface StepDotProps {
index: number;
label: string;
}
function StepDot({ index, label }: StepDotProps) {
return (
<motion.div variants={revealVariants} className="flex items-center gap-3">
<div className="w-5 h-5 rounded-full border border-outline-variant/60 bg-surface-high flex items-center justify-center flex-shrink-0">
<span className="text-[10px] font-semibold text-outline">{index}</span>
</div>
<span className="text-sm text-outline">{label}</span>
</motion.div>
);
}
// ─── Component ────────────────────────────────────────────────────────────────
export default function Configurator() {
const t = useTranslations('configurator');
const steps = [
t('step1.title'),
t('step2.title'),
t('step3.title'),
];
return (
<section id="configure" className="bg-surface-low py-24">
<div className="container mx-auto px-6">
<div className="grid grid-cols-1 gap-12 lg:grid-cols-12">
{/* ── Left: Sticky context panel ─────────────────────────────── */}
<div className="lg:col-span-5">
<div className="lg:sticky lg:top-24">
<motion.div
variants={staggerContainer}
initial="hidden"
whileInView="visible"
viewport={viewportOnce}
className="flex flex-col gap-6"
>
{/* Eyebrow */}
<motion.span
variants={revealVariants}
className="label-md text-primary"
>
{t('eyebrow')}
</motion.span>
{/* H2 */}
<motion.h2
variants={revealVariants}
className="font-serif text-4xl font-semibold tracking-headline text-on-surface leading-tight md:text-5xl"
>
{t('title')}
</motion.h2>
{/* Description */}
<motion.p
variants={revealVariants}
className="text-base text-outline leading-relaxed max-w-sm"
>
{t('description')}
</motion.p>
{/* Step indicators */}
<motion.div
variants={revealVariants}
className="flex flex-col gap-3 pt-2"
>
<p className="text-xs font-semibold uppercase tracking-label text-outline/70">
How it works
</p>
<div className="flex flex-col gap-2.5">
{steps.map((step, i) => (
<StepDot key={i} index={i + 1} label={step} />
))}
</div>
</motion.div>
{/* Trust signal */}
<motion.div
variants={revealVariants}
className="pt-2 flex items-center gap-2.5"
>
<div className="h-px flex-1 bg-outline-variant/40" />
<p className="text-xs text-outline">No commitment required</p>
<div className="h-px flex-1 bg-outline-variant/40" />
</motion.div>
</motion.div>
</div>
</div>
{/* ── Right: Wizard ───────────────────────────────────────────── */}
<div className="lg:col-span-7">
<div className="rounded-2xl bg-surface-high shadow-subtle p-6 sm:p-8">
<WizardContainer />
</div>
</div>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,294 @@
'use client';
import { motion } from 'framer-motion';
import { useTranslations } from 'next-intl';
import { cn } from '@/lib/utils';
import { viewportOnce } from '@/lib/animations';
import Button from '@/components/ui/Button';
import HeroGeometric from '@/components/icons/HeroGeometric';
// Slow drift animation for the SVG background layers
const bgDriftA = {
animate: {
y: [0, -12, 0],
x: [0, 6, 0],
transition: {
duration: 18,
ease: 'easeInOut' as const,
repeat: Infinity,
repeatType: 'loop' as const,
},
},
};
const bgDriftB = {
animate: {
y: [0, 8, 0],
x: [0, -8, 0],
scale: [1, 1.015, 1],
transition: {
duration: 24,
ease: 'easeInOut' as const,
repeat: Infinity,
repeatType: 'loop' as const,
},
},
};
const bgRotate = {
animate: {
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: {},
visible: {
transition: {
staggerChildren: 0.07,
delayChildren: 0.15,
},
},
};
// Individual word reveal
const wordReveal = {
hidden: { opacity: 0, y: 48, filter: 'blur(4px)' },
visible: {
opacity: 1,
y: 0,
filter: 'blur(0px)',
transition: {
duration: 0.7,
ease: [0.16, 1, 0.3, 1] as [number, number, number, number],
},
},
};
// Subtitle fade — delay after headline
const subtitleVariant = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.6,
delay: 0.5,
ease: [0.16, 1, 0.3, 1] as [number, number, number, number],
},
},
};
// CTA scale-in
const ctaVariant = {
hidden: { opacity: 0, scale: 0.93 },
visible: {
opacity: 1,
scale: 1,
transition: {
duration: 0.45,
delay: 0.7,
ease: [0.16, 1, 0.3, 1] as [number, number, number, number],
},
},
};
// Trust strip slide in from left
const trustVariant = {
hidden: { opacity: 0, x: -32 },
visible: {
opacity: 1,
x: 0,
transition: {
duration: 0.55,
delay: 0.9,
ease: [0.16, 1, 0.3, 1] as [number, number, number, number],
},
},
};
// Gradient stops for each avatar circle — subtle variations on the primary palette
const AVATAR_GRADIENTS = [
'linear-gradient(135deg, rgba(0,100,148,0.35), rgba(91,164,217,0.45))',
'linear-gradient(135deg, rgba(91,164,217,0.40), rgba(0,100,148,0.30))',
'linear-gradient(135deg, rgba(0,100,148,0.28), rgba(91,164,217,0.38))',
] as const;
function AvatarCircle({ index }: { index: number }) {
return (
<span
className={cn(
'inline-block w-8 h-8 rounded-full ring-2 ring-white',
index > 0 && '-ml-2',
)}
style={{ background: AVATAR_GRADIENTS[index] }}
aria-hidden="true"
/>
);
}
export default function Hero() {
const t = useTranslations('hero');
// Split the raw title by the {exclusively} placeholder so we can inject the styled <em>
// Expected translation shape: "Built {exclusively} for ambitious brands."
const rawTitle: string = t.raw('title');
const parts = rawTitle.split('{exclusively}');
const before = parts[0] ?? '';
const after = parts[1] ?? '';
// Split each segment into words for per-word animation
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: 'normal'; text: string }
| { type: 'accent'; text: string };
const allWords: WordItem[] = [
...beforeWords.map((w) => ({ type: 'normal' as const, text: w })),
{ type: 'accent' as const, text: exclusivelyWord },
...afterWords.map((w) => ({ type: 'normal' as const, text: w })),
];
return (
<section
id="hero"
aria-label="Hero"
className="relative min-h-screen flex flex-col items-center justify-center overflow-hidden bg-surface-high"
>
{/* ─── Background: animated SVG layers ─────────────────────────── */}
<motion.div
className="absolute inset-0 z-0 pointer-events-none"
{...bgDriftA}
>
<motion.div className="absolute inset-0" {...bgRotate}>
<HeroGeometric className="absolute inset-0 w-full h-full" />
</motion.div>
</motion.div>
{/* Subtle radial gradient vignette over the SVG */}
<motion.div
className="absolute inset-0 z-0 pointer-events-none"
{...bgDriftB}
style={{
background:
'radial-gradient(ellipse 70% 60% at 50% 45%, transparent 30%, rgba(248,249,250,0.55) 100%)',
}}
/>
{/* ─── Content ──────────────────────────────────────────────────── */}
<div className="relative z-10 w-full max-w-4xl mx-auto px-6 py-24 flex flex-col items-center text-center">
{/* Eyebrow */}
<motion.span
className="label-md text-primary mb-6 tracking-widest uppercase"
initial={{ opacity: 0, y: 16 }}
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"
animate="visible"
aria-label={rawTitle.replace('{exclusively}', exclusivelyWord)}
>
{allWords.map((word, i) =>
word.type === 'accent' ? (
<motion.span
key={`word-accent-${i}`}
variants={wordReveal}
className="inline-block overflow-hidden"
style={{ display: 'inline-block' }}
>
<em className="text-primary font-serif italic not-italic">
{word.text}
</em>
</motion.span>
) : (
<motion.span
key={`word-${i}`}
variants={wordReveal}
className="inline-block"
style={{ display: 'inline-block' }}
>
{word.text}
</motion.span>
),
)}
</motion.h1>
{/* Subtitle */}
<motion.p
className="text-lg text-outline leading-relaxed max-w-xl mb-10"
variants={subtitleVariant}
initial="hidden"
animate="visible"
>
{t('subtitle')}
</motion.p>
{/* CTA row */}
<motion.div
className="flex flex-col sm:flex-row items-center gap-4 mb-12"
variants={ctaVariant}
initial="hidden"
animate="visible"
>
<Button variant="primary" size="lg" arrow href="#configure">
{t('cta')}
</Button>
<Button variant="secondary" size="lg" href="#work">
{t('ctaSecondary')}
</Button>
</motion.div>
{/* Trust proof strip */}
<motion.div
className="flex items-center gap-3"
variants={trustVariant}
initial="hidden"
animate="visible"
>
{/* Overlapping avatar circles */}
<div className="flex items-center" aria-hidden="true">
<AvatarCircle index={0} />
<AvatarCircle index={1} />
<AvatarCircle index={2} />
</div>
<p className="text-sm text-outline">
{t('trust')}
</p>
</motion.div>
</div>
{/* Bottom fade-out to next section */}
<div
className="absolute bottom-0 left-0 right-0 h-24 pointer-events-none z-10"
style={{
background:
'linear-gradient(to bottom, transparent, rgba(248,249,250,0.6))',
}}
aria-hidden="true"
/>
</section>
);
}

View File

@@ -0,0 +1,353 @@
'use client';
import { motion } from 'framer-motion';
import { useTranslations } from 'next-intl';
import { cn } from '@/lib/utils';
import {
slideLeftVariants,
viewportOnce,
} from '@/lib/animations';
import CornerBracket from '@/components/icons/CornerBracket';
// ─── Types ────────────────────────────────────────────────────────────────────
interface PillarKey {
key: 'ownership' | 'craftsmanship' | 'oneTeam';
}
// ─── Data ──────────────────────────────────────────────────────────────────────
const PILLARS: PillarKey[] = [
{ key: 'ownership' },
{ key: 'craftsmanship' },
{ key: 'oneTeam' },
];
// ─── Animation Variants ───────────────────────────────────────────────────────
const ease = [0.16, 1, 0.3, 1] as const;
const pillarContainerVariants = {
hidden: {},
visible: {
transition: {
staggerChildren: 0.12,
delayChildren: 0.2,
},
},
};
const pillarVariants = {
hidden: { opacity: 0, x: -32 },
visible: {
opacity: 1,
x: 0,
transition: { duration: 0.6, ease },
},
};
const decorativeVariants = {
hidden: { opacity: 0, scale: 0.96 },
visible: {
opacity: 1,
scale: 1,
transition: { duration: 0.8, ease, delay: 0.15 },
},
};
const quoteCardVariants = {
hidden: {
opacity: 0,
y: 32,
rotate: 4,
},
visible: {
opacity: 1,
y: 0,
rotate: 0,
transition: { duration: 0.75, ease, delay: 0.5 },
},
};
const leftBorderVariants = {
hidden: { scaleY: 0 },
visible: {
scaleY: 1,
transition: { duration: 0.6, ease },
},
};
// ─── Philosophy Pillar ────────────────────────────────────────────────────────
function PhilosophyPillar({
title,
description,
index,
}: {
title: string;
description: string;
index: number;
}) {
return (
<motion.div
variants={pillarVariants}
className="relative flex items-stretch gap-0"
>
{/* Animated left border */}
<div className="relative flex-shrink-0 w-[2px] mr-5 self-stretch overflow-hidden">
<div className="absolute inset-0 bg-outline-variant/20 rounded-full" />
<motion.div
variants={leftBorderVariants}
className="absolute inset-0 origin-top rounded-full"
style={{
background: `linear-gradient(to bottom, var(--color-primary), var(--color-primary-dark))`,
}}
/>
</div>
{/* Content */}
<div className="flex flex-col gap-1.5 py-4">
<p
className="label-md text-outline/60 mb-1"
aria-hidden="true"
>
{String(index + 1).padStart(2, '0')}
</p>
<h3 className="font-semibold text-on-surface text-base leading-snug">
{title}
</h3>
<p className="text-sm text-outline leading-relaxed">
{description}
</p>
</div>
</motion.div>
);
}
// ─── Abstract Geometric Decoration ───────────────────────────────────────────
function AbstractGeometry() {
return (
<div
className="absolute inset-0 overflow-hidden rounded-xl"
aria-hidden="true"
>
{/* Primary large circle */}
<div
className="absolute rounded-full"
style={{
width: '65%',
height: '65%',
top: '-8%',
right: '-12%',
background:
'radial-gradient(circle, rgba(91,164,217,0.12) 0%, rgba(0,100,148,0.06) 60%, transparent 100%)',
}}
/>
{/* Secondary circle, bottom-left */}
<div
className="absolute rounded-full"
style={{
width: '50%',
height: '50%',
bottom: '-10%',
left: '-6%',
background:
'radial-gradient(circle, rgba(0,100,148,0.08) 0%, transparent 70%)',
}}
/>
{/* Diagonal grid */}
<div
className="absolute inset-0 opacity-[0.03]"
style={{
backgroundImage:
'repeating-linear-gradient(-45deg, var(--color-primary-dark) 0, var(--color-primary-dark) 1px, transparent 0, transparent 50%)',
backgroundSize: '32px 32px',
}}
/>
{/* Accent rectangle top-left */}
<div
className="absolute rounded-md"
style={{
width: '22%',
height: '30%',
top: '10%',
left: '8%',
border: '1.5px solid rgba(91,164,217,0.15)',
transform: 'rotate(-8deg)',
}}
/>
{/* Floating dot cluster center-right */}
<div
className="absolute"
style={{
width: '22%',
height: '22%',
top: '35%',
right: '10%',
backgroundImage:
'radial-gradient(circle, rgba(91,164,217,0.25) 1.5px, transparent 1.5px)',
backgroundSize: '10px 10px',
}}
/>
{/* Thin arc line */}
<svg
className="absolute"
style={{ top: '20%', left: '30%', opacity: 0.08 }}
width="140"
height="140"
viewBox="0 0 140 140"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle
cx="70"
cy="70"
r="60"
stroke="var(--color-primary-dark)"
strokeWidth="1"
strokeDasharray="8 6"
/>
</svg>
{/* Small solid accent square */}
<div
className="absolute rounded-sm"
style={{
width: '6%',
height: '6%',
bottom: '28%',
right: '28%',
background: 'rgba(91,164,217,0.20)',
transform: 'rotate(12deg)',
}}
/>
</div>
);
}
// ─── Main Component ───────────────────────────────────────────────────────────
export default function Philosophy() {
const t = useTranslations();
return (
<section id="about" className="bg-surface py-24">
<div className="container mx-auto px-6">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-16 items-start">
{/* ── Left Column: Text (5 cols) ── */}
<div className="lg:col-span-5 flex flex-col gap-10">
{/* Header block */}
<motion.div
variants={slideLeftVariants}
initial="hidden"
whileInView="visible"
viewport={viewportOnce}
className="flex flex-col gap-4"
>
<p className="label-md text-primary">{t('philosophy.eyebrow')}</p>
<h2 className="font-serif text-4xl md:text-[2.75rem] font-semibold text-on-surface leading-[1.1] tracking-[-0.02em]">
{t('philosophy.title')}
</h2>
<p className="text-outline leading-relaxed text-[0.9375rem]">
{t('philosophy.subtitle')}
</p>
</motion.div>
{/* Philosophy pillars */}
<motion.div
variants={pillarContainerVariants}
initial="hidden"
whileInView="visible"
viewport={viewportOnce}
className="flex flex-col gap-2"
>
{PILLARS.map(({ key }, i) => (
<PhilosophyPillar
key={key}
index={i}
title={t(`philosophy.${key}.title`)}
description={t(`philosophy.${key}.description`)}
/>
))}
</motion.div>
</div>
{/* ── Right Column: Decorative (7 cols) ── */}
<div className="lg:col-span-7 relative">
{/* Main decorative canvas */}
<motion.div
variants={decorativeVariants}
initial="hidden"
whileInView="visible"
viewport={viewportOnce}
className={cn(
'relative bg-surface-low rounded-xl',
'min-h-[420px] lg:min-h-[500px]',
'overflow-visible',
)}
>
<AbstractGeometry />
{/* Subtle inner shadow rim */}
<div
className="absolute inset-0 rounded-xl pointer-events-none"
style={{
boxShadow: 'inset 0 0 0 1px rgba(194,199,206,0.25)',
}}
aria-hidden="true"
/>
</motion.div>
{/* ── Pull-Quote Card ── */}
<motion.div
variants={quoteCardVariants}
initial="hidden"
whileInView="visible"
viewport={viewportOnce}
className={cn(
'absolute -bottom-8 -left-4 lg:-left-10',
'bg-surface-high rounded-xl p-6 shadow-subtle',
'max-w-[320px] w-[calc(100%-2.5rem)] lg:max-w-[340px]',
'z-10',
)}
>
{/* CornerBracket top-right decoration */}
<div className="absolute top-4 right-4">
<CornerBracket
size={28}
position="top-right"
color="var(--color-primary)"
/>
</div>
{/* Quote text */}
<blockquote className="font-serif italic text-lg text-on-surface leading-relaxed pr-8">
&ldquo;{t('philosophy.quote')}&rdquo;
</blockquote>
{/* Divider */}
<div
className="w-8 h-px bg-primary/40 my-4"
aria-hidden="true"
/>
{/* Attribution */}
<p className="label-md text-outline">Founded on the Côte d&rsquo;Azur</p>
</motion.div>
</div>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,140 @@
'use client';
import { motion, type Variants } from 'framer-motion';
import { useTranslations } from 'next-intl';
import { Search, LayoutDashboard, PenTool, Rocket, type LucideIcon } from 'lucide-react';
import { cn } from '@/lib/utils';
import {
staggerContainerWide,
revealVariants,
viewportOnce,
} from '@/lib/animations';
import SectionHeader from '@/components/ui/SectionHeader';
// ─── Types ────────────────────────────────────────────────────────────────────
interface Step {
numeral: string;
key: 'discovery' | 'strategy' | 'build' | 'launch';
Icon: LucideIcon;
}
// ─── Data ─────────────────────────────────────────────────────────────────────
const STEPS: Step[] = [
{ numeral: '01', key: 'discovery', Icon: Search },
{ numeral: '02', key: 'strategy', Icon: LayoutDashboard },
{ numeral: '03', key: 'build', Icon: PenTool },
{ numeral: '04', key: 'launch', Icon: Rocket },
];
// ─── Variants ─────────────────────────────────────────────────────────────────
// Numeral scales up from 80% while fading in
const numeralScaleVariants: Variants = {
hidden: { opacity: 0, scale: 0.8 },
visible: {
opacity: 1,
scale: 1,
transition: {
duration: 0.7,
ease: [0.16, 1, 0.3, 1],
},
},
};
// ─── Sub-components ───────────────────────────────────────────────────────────
function StepCard({ numeral, stepKey, Icon }: { numeral: string; stepKey: string; Icon: LucideIcon }) {
const t = useTranslations();
const title = t(`process.steps.${stepKey}.title`);
const description = t(`process.steps.${stepKey}.description`);
return (
<motion.div
variants={revealVariants}
className={cn(
'relative flex flex-col bg-surface-high rounded-xl p-6',
'shadow-subtle',
)}
>
{/* Ghosted numeral — scales up on scroll */}
<motion.span
variants={numeralScaleVariants}
aria-hidden="true"
className="font-serif text-6xl font-light leading-none text-on-surface/[0.06] select-none -ml-0.5 mb-3"
>
{numeral}
</motion.span>
{/* Icon */}
<div className="mb-4">
<Icon
size={24}
strokeWidth={1.5}
className="text-primary"
aria-hidden="true"
/>
</div>
{/* Title */}
<h3 className="font-semibold text-lg text-on-surface mb-2 leading-snug">
{title}
</h3>
{/* Description */}
<p className="text-sm text-outline leading-relaxed">
{description}
</p>
</motion.div>
);
}
// ─── Main Component ───────────────────────────────────────────────────────────
export default function Process() {
const t = useTranslations();
return (
<section id="process" className="bg-surface-low py-24">
<div className="container mx-auto px-6">
{/*
Desktop layout:
col 1 (lg:col-span-1) → SectionHeader, flush left
col 24 (lg:col-span-3) → 2×2 grid of step cards
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">
{/* ── Header column ── */}
<div className="lg:col-span-1 lg:sticky lg:top-32">
<SectionHeader
eyebrow={t('process.eyebrow')}
title={t('process.title')}
align="left"
/>
</div>
{/* ── Steps column ── */}
<div className="lg:col-span-3">
<motion.div
variants={staggerContainerWide}
initial="hidden"
whileInView="visible"
viewport={viewportOnce}
className="grid grid-cols-1 sm:grid-cols-2 gap-5"
>
{STEPS.map((step) => (
<StepCard key={step.key} numeral={step.numeral} stepKey={step.key} Icon={step.Icon} />
))}
</motion.div>
</div>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,464 @@
'use client';
import { motion } from 'framer-motion';
import { useTranslations } from 'next-intl';
import { cn } from '@/lib/utils';
import {
staggerContainerWide,
slideLeftVariants,
fadeVariants,
viewportOnce,
} from '@/lib/animations';
import { Link } from '@/i18n/navigation';
import { Lock, Clock, ArrowRight } from 'lucide-react';
// ─── Types ────────────────────────────────────────────────────────────────────
interface Project {
title: string;
description: string;
tags: string[];
slug: string;
featured?: boolean;
}
interface ComingSoonItem {
title: string;
subtitle: string;
}
// ─── Data ──────────────────────────────────────────────────────────────────────
const PROJECTS: Project[] = [
{
title: 'Monaco Ocean Protection Challenge',
description:
"A comprehensive judging and analytics system with advanced AI jury integration for one of the Mediterranean's most prestigious conservation events.",
tags: ['AI Integration', 'Platform'],
slug: 'monaco-ocean',
featured: true,
},
{
title: 'Port Nimara',
description: 'Scalable digital hub for maritime logistics.',
tags: ['Website', 'Infrastructure'],
slug: 'port-nimara',
},
{
title: 'Port Amador',
description: 'Premium digital experience for elite nautical services.',
tags: ['Website', 'Infrastructure'],
slug: 'port-amador',
},
];
const COMING_SOON: ComingSoonItem[] = [
{ title: 'Confidential Riviera Project', subtitle: 'Coming Soon' },
{ title: 'Sophia Antipolis AI Startup', subtitle: 'Launching Q4' },
];
// ─── Animation Variants ───────────────────────────────────────────────────────
const ease = [0.16, 1, 0.3, 1] as const;
const featuredCardVariants = {
hidden: { opacity: 0, y: 48 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.7, ease },
},
};
const smallCardVariants = {
hidden: { opacity: 0, y: 32 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.6, ease },
},
};
const comingSoonVariants = {
hidden: { opacity: 0, y: 24 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.55, ease },
},
};
// ─── Geometric Placeholder ────────────────────────────────────────────────────
function GeometricPlaceholder({
variant = 'featured',
className,
}: {
variant?: 'featured' | 'small';
className?: string;
}) {
const isFeatured = variant === 'featured';
return (
<div
className={cn(
'relative overflow-hidden bg-gradient-to-br from-primary-dark/90 to-primary/70',
className,
)}
aria-hidden="true"
>
{/* Abstract geometric shapes */}
<div className="absolute inset-0">
{/* Large circle top-right */}
<div
className="absolute rounded-full bg-white/[0.06]"
style={{
width: isFeatured ? '55%' : '70%',
height: isFeatured ? '55%' : '70%',
top: '-15%',
right: '-10%',
}}
/>
{/* Medium circle bottom-left */}
<div
className="absolute rounded-full bg-white/[0.08]"
style={{
width: isFeatured ? '40%' : '50%',
height: isFeatured ? '40%' : '50%',
bottom: '-20%',
left: '-5%',
}}
/>
{/* Diagonal stripes overlay */}
<div
className="absolute inset-0 opacity-[0.04]"
style={{
backgroundImage:
'repeating-linear-gradient(-45deg, #fff 0, #fff 1px, transparent 0, transparent 50%)',
backgroundSize: isFeatured ? '28px 28px' : '20px 20px',
}}
/>
{/* Small accent rectangle */}
<div
className="absolute bg-white/[0.12] rounded-sm"
style={{
width: isFeatured ? '18%' : '24%',
height: isFeatured ? '28%' : '36%',
bottom: '18%',
right: '15%',
transform: 'rotate(-6deg)',
}}
/>
{/* Thin horizontal line accent */}
<div
className="absolute bg-white/20 rounded-full"
style={{
width: isFeatured ? '30%' : '40%',
height: '1px',
top: '38%',
left: '10%',
}}
/>
{/* Grid-dot accent */}
<div
className="absolute opacity-[0.07]"
style={{
width: isFeatured ? '25%' : '30%',
height: isFeatured ? '25%' : '30%',
top: '50%',
right: '22%',
backgroundImage: 'radial-gradient(circle, #fff 1px, transparent 1px)',
backgroundSize: '8px 8px',
}}
/>
</div>
{/* Bottom gradient fade */}
<div className="absolute inset-x-0 bottom-0 h-1/3 bg-gradient-to-t from-primary-dark/50 to-transparent" />
</div>
);
}
// ─── Tag Chip ─────────────────────────────────────────────────────────────────
function TagChip({ label }: { label: string }) {
return (
<span className="inline-flex items-center bg-primary/10 text-primary-dark text-xs font-medium px-2.5 py-1 rounded-full leading-none">
{label}
</span>
);
}
// ─── Featured Card ────────────────────────────────────────────────────────────
function FeaturedCard({ project, readLabel }: { project: Project; readLabel: string }) {
return (
<motion.article
variants={featuredCardVariants}
className={cn(
'group relative flex flex-col bg-surface-high rounded-2xl overflow-hidden',
'shadow-subtle',
'transition-shadow duration-300 hover:shadow-[0_24px_48px_rgba(25,28,29,0.10)]',
)}
>
{/* Geometric image placeholder */}
<GeometricPlaceholder
variant="featured"
className="w-full aspect-[16/9] md:aspect-[2/1]"
/>
{/* Content */}
<div className="flex flex-col flex-1 p-7 gap-4">
{/* Tags */}
<div className="flex flex-wrap gap-2">
{project.tags.map((tag) => (
<TagChip key={tag} label={tag} />
))}
</div>
{/* Title */}
<h3 className="font-serif text-2xl font-semibold text-on-surface leading-snug">
{project.title}
</h3>
{/* Description */}
<p className="text-sm text-outline leading-relaxed flex-1">
{project.description}
</p>
{/* CTA */}
<Link
href={`/work/${project.slug}`}
className={cn(
'inline-flex items-center gap-2 text-sm font-medium text-primary-dark',
'transition-gap duration-200 group/link',
'mt-1',
)}
>
<span className="underline underline-offset-4 decoration-primary/40 group-hover/link:decoration-primary-dark transition-colors duration-200">
{readLabel}
</span>
<ArrowRight
size={14}
className="transition-transform duration-200 group-hover/link:translate-x-1"
/>
</Link>
</div>
</motion.article>
);
}
// ─── Small Card ───────────────────────────────────────────────────────────────
function SmallCard({ project, readLabel }: { project: Project; readLabel: string }) {
return (
<motion.article
variants={smallCardVariants}
className={cn(
'group relative flex flex-col bg-surface-high rounded-xl overflow-hidden',
'shadow-card',
'transition-all duration-300',
'hover:shadow-subtle',
)}
>
{/* Geometric placeholder — grayscale to color on hover */}
<div className="relative overflow-hidden">
<div
className={cn(
'transition-all duration-500',
'grayscale group-hover:grayscale-0',
'opacity-80 group-hover:opacity-100',
)}
>
<GeometricPlaceholder
variant="small"
className="w-full aspect-[16/7]"
/>
</div>
</div>
{/* Content */}
<div className="flex flex-col flex-1 p-5 gap-3">
{/* Tags */}
<div className="flex flex-wrap gap-1.5">
{project.tags.map((tag) => (
<TagChip key={tag} label={tag} />
))}
</div>
{/* Title */}
<h3 className="font-serif text-lg font-semibold text-on-surface leading-snug">
{project.title}
</h3>
{/* Description */}
<p className="text-xs text-outline leading-relaxed flex-1">
{project.description}
</p>
{/* CTA */}
<Link
href={`/work/${project.slug}`}
className="inline-flex items-center gap-1.5 text-xs font-medium text-primary-dark group/link"
>
<span className="underline underline-offset-4 decoration-primary/40 group-hover/link:decoration-primary-dark transition-colors duration-200">
{readLabel}
</span>
<ArrowRight
size={12}
className="transition-transform duration-200 group-hover/link:translate-x-0.5"
/>
</Link>
</div>
</motion.article>
);
}
// ─── Coming Soon Card ─────────────────────────────────────────────────────────
function ComingSoonCard({ item }: { item: ComingSoonItem }) {
const isConfidential = item.subtitle === 'Coming Soon';
const Icon = isConfidential ? Lock : Clock;
return (
<motion.div
variants={comingSoonVariants}
className={cn(
'relative flex flex-col items-center justify-center gap-4',
'rounded-xl p-8 min-h-[160px]',
'border border-dashed border-outline-variant/30',
'coming-soon-card',
'overflow-hidden',
)}
>
{/* Subtle animated pulse overlay */}
<div
className="absolute inset-0 rounded-xl opacity-0 coming-soon-pulse"
aria-hidden="true"
/>
<div className="relative z-10 flex flex-col items-center gap-3 text-center">
<Icon
size={20}
className="text-outline/40"
strokeWidth={1.5}
/>
<div>
<p className="font-serif text-base font-medium text-on-surface/40 leading-snug">
{item.title}
</p>
<p className="label-md text-outline/50 mt-1">{item.subtitle}</p>
</div>
</div>
</motion.div>
);
}
// ─── Main Component ───────────────────────────────────────────────────────────
export default function SelectedWorks() {
const t = useTranslations();
const featuredProject = PROJECTS.find((p) => p.featured)!;
const secondaryProjects = PROJECTS.filter((p) => !p.featured);
return (
<section id="work" className="bg-surface-low py-24">
<style>{`
@keyframes dashed-drift {
0% { background-position: 0 0, 100% 0, 100% 100%, 0 100%; }
100% { background-position: 100% 0, 100% 100%, 0 100%, 0 0; }
}
@keyframes coming-soon-fade {
0%, 100% { opacity: 0; }
50% { opacity: 1; }
}
.coming-soon-pulse {
background: radial-gradient(
ellipse at center,
rgba(91, 164, 217, 0.04) 0%,
transparent 70%
);
animation: coming-soon-fade 4s ease-in-out infinite;
}
`}</style>
<div className="container mx-auto px-6">
{/* ── Section Header ── */}
<motion.div
variants={slideLeftVariants}
initial="hidden"
whileInView="visible"
viewport={viewportOnce}
className="flex flex-col sm:flex-row sm:items-end sm:justify-between gap-4 mb-14"
>
<div>
<p className="label-md text-primary mb-2">{t('work.eyebrow')}</p>
<h2 className="font-serif text-4xl md:text-5xl font-semibold text-on-surface leading-[1.1] tracking-[-0.02em]">
{t('work.title')}
</h2>
</div>
<motion.div
variants={fadeVariants}
initial="hidden"
whileInView="visible"
viewport={viewportOnce}
>
<Link
href="/work"
className="inline-flex items-center gap-2 text-sm font-medium text-outline hover:text-primary-dark transition-colors duration-200 group"
>
<span>View all work</span>
<ArrowRight
size={14}
className="transition-transform duration-200 group-hover:translate-x-1"
/>
</Link>
</motion.div>
</motion.div>
{/* ── Primary Grid: Featured + 2 Small ── */}
<motion.div
variants={staggerContainerWide}
initial="hidden"
whileInView="visible"
viewport={viewportOnce}
className="grid grid-cols-1 md:grid-cols-12 gap-5 mb-5"
>
{/* Featured card — 8 cols */}
<div className="md:col-span-8">
<FeaturedCard
project={featuredProject}
readLabel={t('work.readCaseStudy')}
/>
</div>
{/* Secondary column — 4 cols, 2 stacked */}
<div className="md:col-span-4 flex flex-col gap-5">
{secondaryProjects.map((project) => (
<SmallCard
key={project.slug}
project={project}
readLabel={t('work.readCaseStudy')}
/>
))}
</div>
</motion.div>
{/* ── Coming Soon Row ── */}
<motion.div
variants={staggerContainerWide}
initial="hidden"
whileInView="visible"
viewport={viewportOnce}
className="grid grid-cols-1 sm:grid-cols-2 gap-5"
>
{COMING_SOON.map((item) => (
<ComingSoonCard key={item.title} item={item} />
))}
</motion.div>
</div>
</section>
);
}

View File

@@ -0,0 +1,140 @@
'use client';
import { motion } from 'framer-motion';
import { useTranslations } from 'next-intl';
import { cn } from '@/lib/utils';
import {
staggerContainerWide,
revealVariants,
fadeVariants,
scaleVariants,
viewportOnce,
} from '@/lib/animations';
import SectionHeader from '@/components/ui/SectionHeader';
// ─── Types ───────────────────────────────────────────────────────────────────
interface ServicePillar {
numeral: string;
titleKey: 'web' | 'systems' | 'infrastructure';
featuresKey: 'web' | 'systems' | 'infrastructure';
}
// ─── Data ─────────────────────────────────────────────────────────────────────
const PILLARS: ServicePillar[] = [
{ numeral: '01', titleKey: 'web', featuresKey: 'web' },
{ numeral: '02', titleKey: 'systems', featuresKey: 'systems' },
{ numeral: '03', titleKey: 'infrastructure', featuresKey: 'infrastructure' },
];
// ─── Variants ─────────────────────────────────────────────────────────────────
const numeralVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { duration: 0.8, ease: [0.16, 1, 0.3, 1] as const },
},
};
// ─── Component ────────────────────────────────────────────────────────────────
export default function ServicesOverview() {
const t = useTranslations();
return (
<section id="services" className="bg-surface py-24">
<div className="container mx-auto px-6">
{/* Section header */}
<div className="mb-16">
<SectionHeader
eyebrow={t('services.eyebrow')}
title={t('services.title')}
align="center"
/>
</div>
{/* Three-pillar grid with gap-px separator trick */}
<div className="bg-outline/10 rounded-xl overflow-hidden shadow-subtle">
<motion.div
variants={staggerContainerWide}
initial="hidden"
whileInView="visible"
viewport={viewportOnce}
className="grid grid-cols-1 md:grid-cols-3 gap-px"
>
{PILLARS.map(({ numeral, titleKey, featuresKey }) => {
const features = t.raw(`services.${featuresKey}.features`) as string[];
const title = t(`services.${titleKey}.title`);
return (
<motion.div
key={numeral}
variants={revealVariants}
className={cn(
'relative flex flex-col p-8 bg-surface',
'transition-colors duration-200 ease-out',
'hover:bg-surface-high',
)}
>
{/* Ghosted numeral */}
<motion.span
variants={numeralVariants}
aria-hidden="true"
className="font-serif text-7xl font-light leading-none text-on-surface/[0.06] select-none mb-4 -ml-1"
>
{numeral}
</motion.span>
{/* Title */}
<h3 className="font-serif text-2xl font-semibold text-on-surface mb-5 leading-snug">
{title}
</h3>
{/* Feature list */}
<ul className="flex flex-col gap-2.5">
{features.map((feature: string) => (
<li
key={feature}
className="flex items-start gap-2.5 text-sm text-outline leading-relaxed"
>
<span
aria-hidden="true"
className="mt-[0.35em] shrink-0 w-1 h-1 rounded-full bg-primary/50"
/>
{feature}
</li>
))}
</ul>
</motion.div>
);
})}
</motion.div>
</div>
{/* AI narrative callout */}
<motion.div
variants={fadeVariants}
initial="hidden"
whileInView="visible"
viewport={viewportOnce}
className="mt-16 flex flex-col items-center gap-4"
>
{/* Decorative rule */}
<motion.span
variants={scaleVariants}
className="block w-px h-10 bg-primary/30"
aria-hidden="true"
/>
<p className="font-serif italic text-xl text-primary-dark text-center max-w-xl leading-relaxed">
{t('services.aiNarrative')}
</p>
</motion.div>
</div>
</section>
);
}

View File

@@ -0,0 +1,125 @@
'use client';
import { motion } from 'framer-motion';
import { useTranslations } from 'next-intl';
import { Compass, Shield, Brain, MapPin } from 'lucide-react';
import { cn } from '@/lib/utils';
import {
revealVariants,
staggerContainerWide,
viewportOnce,
} from '@/lib/animations';
import type { LucideIcon } from 'lucide-react';
// Each item's icon scale-bounce on enter
const iconBounceVariants = {
hidden: { scale: 0.7, opacity: 0 },
visible: {
scale: 1,
opacity: 1,
transition: {
duration: 0.5,
ease: [0.34, 1.56, 0.64, 1] as [number, number, number, number], // spring-like overshoot
},
},
};
interface TrustItem {
key: string;
Icon: LucideIcon;
}
const ITEMS: TrustItem[] = [
{ key: 'customBuilt', Icon: Compass },
{ key: 'privateInfra', Icon: Shield },
{ key: 'aiPowered', Icon: Brain },
{ key: 'rivieraBased', Icon: MapPin },
];
interface TrustCardProps {
item: TrustItem;
index: number;
t: (key: string) => string;
}
function TrustCard({ item, index, t }: TrustCardProps) {
const { Icon, key } = item;
return (
<motion.div
variants={revealVariants}
className={cn(
'group flex flex-col items-start gap-4 p-6',
'rounded-2xl bg-surface-high shadow-subtle',
'transition-shadow duration-300 hover:shadow-card',
'cursor-default',
)}
>
{/* Icon with scale-bounce on scroll reveal */}
<motion.div
variants={iconBounceVariants}
className={cn(
'flex items-center justify-center w-12 h-12 rounded-xl',
'bg-primary/8',
'transition-transform duration-300 ease-out',
'group-hover:-translate-y-1',
)}
aria-hidden="true"
>
<Icon
size={28}
className="text-primary transition-colors duration-300"
strokeWidth={1.75}
/>
</motion.div>
{/* Title */}
<div>
<h3
className={cn(
'font-semibold text-on-surface text-base leading-snug mb-1',
'transition-colors duration-300 group-hover:text-primary-dark',
)}
>
{t(`${key}.title`)}
</h3>
{/* Description */}
<p className="text-sm text-outline leading-relaxed">
{t(`${key}.description`)}
</p>
</div>
</motion.div>
);
}
export default function TrustBar() {
const t = useTranslations('trustBar');
return (
<section
aria-label="Trust indicators"
className="bg-surface-low py-16"
>
<div className="max-w-6xl mx-auto px-6">
{/* Stagger wrapper — triggers children revealVariants on scroll */}
<motion.div
variants={staggerContainerWide}
initial="hidden"
whileInView="visible"
viewport={viewportOnce}
className="grid grid-cols-2 md:grid-cols-4 gap-6 md:gap-8"
>
{ITEMS.map((item, index) => (
<TrustCard
key={item.key}
item={item}
index={index}
t={t}
/>
))}
</motion.div>
</div>
</section>
);
}

View File

@@ -0,0 +1,211 @@
'use client';
import { motion } from 'framer-motion';
import { Users, MessageCircle, BarChart3 } from 'lucide-react';
import { cn } from '@/lib/utils';
import {
staggerContainer,
revealVariants,
slideLeftVariants,
viewportOnce,
} from '@/lib/animations';
// ─── Types ─────────────────────────────────────────────────────────────────────
interface AiCapability {
id: string;
title: string;
description: string;
}
interface AILayerProps {
capabilities: readonly AiCapability[];
}
// ─── Icon map ──────────────────────────────────────────────────────────────────
const ICON_MAP: Record<string, typeof Users> = {
'ai-teammate': Users,
'customer-facing-ai': MessageCircle,
'data-intelligence': BarChart3,
};
// ─── Animation variants ────────────────────────────────────────────────────────
const ease = [0.16, 1, 0.3, 1] as const;
const sectionHeadVariants = {
hidden: {},
visible: {
transition: { staggerChildren: 0.1 },
},
};
const cardContainerVariants = {
hidden: {},
visible: {
transition: {
staggerChildren: 0.12,
delayChildren: 0.2,
},
},
};
const cardVariants = {
hidden: { opacity: 0, y: 28 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.6, ease },
},
};
const lineVariants = {
hidden: { scaleY: 0 },
visible: {
scaleY: 1,
transition: { duration: 0.6, ease, delay: 0.15 },
},
};
// ─── AI Capability card ────────────────────────────────────────────────────────
function AICapabilityCard({ capability }: { capability: AiCapability }) {
const Icon = ICON_MAP[capability.id] ?? Users;
return (
<motion.div
variants={cardVariants}
className={cn(
'relative flex flex-col gap-4 p-6',
'rounded-xl',
'bg-white/5 backdrop-blur-glass',
'border border-white/10',
'transition-colors duration-200 hover:bg-white/[0.08]',
)}
>
{/* Icon */}
<div
className="flex items-center justify-center w-10 h-10 rounded-xl"
style={{
background: 'rgba(91, 164, 217, 0.15)',
}}
aria-hidden="true"
>
<Icon
size={18}
className="text-primary-light"
strokeWidth={1.75}
/>
</div>
{/* Title */}
<h3 className="font-serif font-semibold text-white text-xl leading-snug">
{capability.title}
</h3>
{/* Description */}
<p className="text-sm leading-relaxed" style={{ color: 'rgba(255,255,255,0.6)' }}>
{capability.description}
</p>
</motion.div>
);
}
// ─── Component ─────────────────────────────────────────────────────────────────
export default function AILayer({ capabilities }: AILayerProps) {
return (
<section
id="ai-automation"
className="py-24"
style={{ backgroundColor: '#1C2B3A' }}
aria-labelledby="ai-layer-heading"
>
<div className="container mx-auto px-6">
{/* Section header */}
<motion.div
variants={sectionHeadVariants}
initial="hidden"
whileInView="visible"
viewport={viewportOnce}
className="mb-14 flex flex-col items-center text-center"
>
{/* Eyebrow */}
<motion.span
variants={revealVariants}
className="label-md text-primary mb-4"
>
Intelligent Layer
</motion.span>
{/* Heading */}
<motion.h2
id="ai-layer-heading"
variants={revealVariants}
className="font-serif font-semibold tracking-headline text-white text-4xl md:text-5xl max-w-2xl leading-[1.1]"
>
The AI Layer
</motion.h2>
{/* Vertical line */}
<motion.div
variants={lineVariants}
className="origin-top w-px h-8 mt-5 mb-5"
style={{ background: 'rgba(91,164,217,0.4)' }}
aria-hidden="true"
/>
{/* Subtitle */}
<motion.p
variants={revealVariants}
className="font-serif italic text-xl leading-relaxed max-w-xl"
style={{ color: 'rgba(255,255,255,0.75)' }}
>
We build your ecosystem then make it intelligent.
</motion.p>
{/* Context paragraph */}
<motion.p
variants={revealVariants}
className="mt-5 text-[0.9375rem] leading-relaxed max-w-2xl"
style={{ color: 'rgba(255,255,255,0.5)' }}
>
AI is not a product we bolt on it is the connective tissue of
every system we build. Once your digital infrastructure is live, we
layer language models, automation pipelines, and predictive analytics
directly into your workflows, so your team operates with capabilities
that were previously reserved for organisations ten times your size.
</motion.p>
</motion.div>
{/* Capability cards */}
<motion.div
variants={cardContainerVariants}
initial="hidden"
whileInView="visible"
viewport={viewportOnce}
className="grid grid-cols-1 md:grid-cols-3 gap-4"
>
{capabilities.map((capability) => (
<AICapabilityCard key={capability.id} capability={capability} />
))}
</motion.div>
{/* Bottom note */}
<motion.p
variants={revealVariants}
initial="hidden"
whileInView="visible"
viewport={viewportOnce}
className="mt-10 text-center text-xs uppercase tracking-widest"
style={{ color: 'rgba(255,255,255,0.25)' }}
>
Compatible with your existing stack no data ever leaves your infrastructure
</motion.p>
</div>
</section>
);
}

View File

@@ -0,0 +1,202 @@
'use client';
import { motion } from 'framer-motion';
import {
Palette, Globe, ShoppingCart, Zap,
Database, Code2, GitBranch, Wrench,
Server, Shield, Lock, Settings,
} from 'lucide-react';
import type { LucideIcon } from 'lucide-react';
import { cn } from '@/lib/utils';
const ICON_MAP: Record<string, LucideIcon> = {
Palette, Globe, ShoppingCart, Zap,
Database, Code2, GitBranch, Wrench,
Server, Shield, Lock, Settings,
};
import {
revealVariants,
staggerContainer,
slideLeftVariants,
slideRightVariants,
viewportOnce,
} from '@/lib/animations';
import ScrollReveal from '@/components/ui/ScrollReveal';
// ─── Types ─────────────────────────────────────────────────────────────────────
interface Feature {
icon: string;
title: string;
description: string;
}
interface Pillar {
id: string;
numeral: string;
title: string;
description: string;
background: 'bg-surface' | 'bg-surface-low';
features: readonly Feature[];
}
interface ServicePillarProps {
pillar: Pillar;
index: number;
}
// ─── Animation variants ────────────────────────────────────────────────────────
const ease = [0.16, 1, 0.3, 1] as const;
const numeralVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { duration: 1, ease },
},
};
const featureCardVariants = {
hidden: { opacity: 0, y: 24 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.55, ease },
},
};
const featureGridVariants = {
hidden: {},
visible: {
transition: {
staggerChildren: 0.09,
delayChildren: 0.1,
},
},
};
// ─── Feature card ──────────────────────────────────────────────────────────────
function FeatureCard({ feature }: { feature: Feature }) {
const Icon = ICON_MAP[feature.icon] ?? Globe;
return (
<motion.div
variants={featureCardVariants}
className={cn(
'flex flex-col gap-3 p-6',
'bg-surface-high rounded-xl',
'shadow-card',
'transition-shadow duration-200 hover:shadow-subtle',
)}
>
{/* Icon container */}
<div
className="flex items-center justify-center w-10 h-10 rounded-xl"
style={{
background:
'linear-gradient(135deg, rgba(91,164,217,0.12), rgba(0,100,148,0.08))',
}}
aria-hidden="true"
>
<Icon
size={18}
className="text-primary-dark"
strokeWidth={1.75}
/>
</div>
{/* Title */}
<h4 className="font-semibold text-on-surface text-sm leading-snug">
{feature.title}
</h4>
{/* Description */}
<p className="text-sm text-outline leading-relaxed">
{feature.description}
</p>
</motion.div>
);
}
// ─── Component ─────────────────────────────────────────────────────────────────
export default function ServicePillar({ pillar, index }: ServicePillarProps) {
const headingVariants = index % 2 === 0 ? slideLeftVariants : slideRightVariants;
return (
<section
id={pillar.id}
className={cn('py-24', pillar.background)}
aria-labelledby={`${pillar.id}-heading`}
>
<div className="container mx-auto px-6">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-16 items-start">
{/* ── Left: Header copy (5 cols) ─────────────────────────────────── */}
<div className="lg:col-span-5 flex flex-col gap-6">
{/* Ghosted numeral */}
<motion.span
variants={numeralVariants}
initial="hidden"
whileInView="visible"
viewport={viewportOnce}
aria-hidden="true"
className={cn(
'font-serif leading-none select-none',
'text-[6rem] md:text-[7.5rem] font-light',
'text-on-surface/[0.05]',
'-ml-1 -mb-6',
)}
>
{pillar.numeral}
</motion.span>
{/* Heading */}
<motion.h2
id={`${pillar.id}-heading`}
variants={headingVariants}
initial="hidden"
whileInView="visible"
viewport={viewportOnce}
className="font-serif font-semibold tracking-headline text-on-surface text-4xl md:text-[2.75rem] leading-[1.1]"
>
{pillar.title}
</motion.h2>
{/* Accent rule */}
<ScrollReveal variant="fadeIn" delay={0.15}>
<div
className="w-12 h-0.5 rounded-full bg-gradient-cta"
aria-hidden="true"
/>
</ScrollReveal>
{/* Description */}
<ScrollReveal variant="fadeUp" delay={0.2}>
<p className="text-outline leading-relaxed text-[0.9375rem]">
{pillar.description}
</p>
</ScrollReveal>
</div>
{/* ── Right: Feature grid (7 cols) ───────────────────────────────── */}
<motion.div
variants={featureGridVariants}
initial="hidden"
whileInView="visible"
viewport={viewportOnce}
className="lg:col-span-7 grid grid-cols-1 sm:grid-cols-2 gap-4"
>
{pillar.features.map((feature) => (
<FeatureCard key={feature.title} feature={feature} />
))}
</motion.div>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,146 @@
'use client';
import { motion } from 'framer-motion';
import { ArrowRight } from 'lucide-react';
import { cn } from '@/lib/utils';
import {
staggerContainer,
revealVariants,
scaleVariants,
viewportOnce,
} from '@/lib/animations';
import Button from '@/components/ui/Button';
import ScrollReveal from '@/components/ui/ScrollReveal';
// ─── Animation variants ────────────────────────────────────────────────────────
const ease = [0.16, 1, 0.3, 1] as const;
const decorLineVariants = {
hidden: { scaleX: 0 },
visible: {
scaleX: 1,
transition: { duration: 0.7, ease },
},
};
// ─── Component ─────────────────────────────────────────────────────────────────
export default function ServicesCTA() {
return (
<section
className="bg-surface-low py-24"
aria-label="Call to action"
>
<div className="container mx-auto px-6">
{/* Outer wrapper with subtle rim */}
<div
className={cn(
'relative overflow-hidden rounded-xl bg-surface-high',
'shadow-subtle',
)}
style={{
boxShadow: 'inset 0 0 0 1px rgba(194,199,206,0.3), 0 20px 40px rgba(25,28,29,0.06)',
}}
>
{/* Background geometry — purely decorative */}
<div
aria-hidden="true"
className="absolute inset-0 overflow-hidden rounded-xl pointer-events-none"
>
{/* Primary radial glow */}
<div
className="absolute rounded-full"
style={{
width: '60%',
height: '200%',
top: '-50%',
right: '-10%',
background:
'radial-gradient(ellipse, rgba(91,164,217,0.07) 0%, transparent 70%)',
}}
/>
{/* Secondary glow */}
<div
className="absolute rounded-full"
style={{
width: '40%',
height: '160%',
bottom: '-60%',
left: '-5%',
background:
'radial-gradient(ellipse, rgba(0,100,148,0.05) 0%, transparent 70%)',
}}
/>
{/* Dot grid */}
<div
className="absolute inset-0 opacity-[0.025]"
style={{
backgroundImage:
'radial-gradient(circle, var(--color-primary-dark) 1px, transparent 1px)',
backgroundSize: '28px 28px',
}}
/>
</div>
{/* Content */}
<div className="relative px-8 py-16 md:px-16 md:py-20 flex flex-col items-center text-center gap-6">
{/* Eyebrow */}
<ScrollReveal variant="fadeUp">
<span className="label-md text-primary">
Let&apos;s Talk
</span>
</ScrollReveal>
{/* Heading */}
<ScrollReveal variant="fadeUp" delay={0.08}>
<h2 className="font-serif font-semibold tracking-headline text-on-surface text-4xl md:text-5xl max-w-2xl leading-[1.1]">
Ready to scope your project?
</h2>
</ScrollReveal>
{/* Subtitle */}
<ScrollReveal variant="fadeUp" delay={0.16}>
<p className="text-lg text-outline leading-relaxed max-w-xl">
Use our interactive configurator to define your requirements, select
your services, and generate a personalised project brief no
commitment required, just clarity.
</p>
</ScrollReveal>
{/* CTA buttons */}
<ScrollReveal variant="fadeUp" delay={0.24}>
<div className="flex flex-col sm:flex-row items-center gap-3 mt-2">
<Button
href="/#configure"
variant="primary"
size="lg"
arrow
>
Configure Your Project
</Button>
<Button
href="mailto:hello@letsbe.biz"
variant="secondary"
size="lg"
>
hello@letsbe.biz
</Button>
</div>
</ScrollReveal>
{/* Reassurance */}
<ScrollReveal variant="fadeIn" delay={0.3}>
<p className="text-sm text-outline/60 mt-1">
No commitment required just a conversation about what&apos;s possible.
</p>
</ScrollReveal>
</div>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,97 @@
'use client';
import { motion } from 'framer-motion';
import { staggerContainer, revealVariants, viewportOnce } from '@/lib/animations';
// ─── Animation variants ────────────────────────────────────────────────────────
const ease = [0.16, 1, 0.3, 1] as const;
const eyebrowVariants = {
hidden: { opacity: 0, y: 16 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.5, ease },
},
};
const headlineVariants = {
hidden: { opacity: 0, y: 24 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.65, ease, delay: 0.1 },
},
};
const subtitleVariants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.6, ease, delay: 0.2 },
},
};
const ruleVariants = {
hidden: { scaleX: 0 },
visible: {
scaleX: 1,
transition: { duration: 0.7, ease, delay: 0.35 },
},
};
// ─── Component ─────────────────────────────────────────────────────────────────
export default function ServicesHero() {
return (
<section
className="bg-surface pt-32 pb-20"
aria-label="Services hero"
>
<div className="container mx-auto px-6">
<motion.div
variants={staggerContainer}
initial="hidden"
animate="visible"
className="flex flex-col items-center text-center"
>
{/* Eyebrow */}
<motion.span
variants={eyebrowVariants}
className="label-md text-primary mb-5"
>
Our Capabilities
</motion.span>
{/* Headline */}
<motion.h1
variants={headlineVariants}
className="font-serif font-semibold tracking-headline text-on-surface text-5xl md:text-6xl lg:text-7xl max-w-4xl leading-[1.05]"
>
Three Pillars of{' '}
<span className="text-gradient">Digital Excellence</span>
</motion.h1>
{/* Subtitle */}
<motion.p
variants={subtitleVariants}
className="mt-6 text-lg text-outline leading-relaxed max-w-2xl"
>
We design, build, and operate complete digital ecosystems from the
first pixel to the server rack. Every discipline under one roof,
every deliverable built to a standard most agencies never attempt.
</motion.p>
{/* Decorative rule */}
<motion.div
variants={ruleVariants}
className="mt-10 origin-left w-16 h-px bg-gradient-cta"
aria-hidden="true"
/>
</motion.div>
</div>
</section>
);
}