Ajoute une couche de micro-animations sur tout le site
Deploy / deploy (push) Successful in 1m13s

Deux courbes partagées dans lib/motion.ts, pour que le CSS et Framer
aient le même feeling, et quatre primitives que Tailwind ne sait pas
exprimer : reflet de bouton, halo qui suit le curseur, soulignement qui
se dessine, secousse d'erreur.

Nouveaux composants : Stagger (les collections se distribuent une par
une), Spotlight (trouve son parent tout seul, donc les cartes restent
des composants serveur), ScrollProgress, BackToTop, Parallax, Spinner,
CheckMark. Reveal accepte maintenant une direction.

Côté rendu : le trait de nav glisse d'un onglet à l'autre, les cartes
se soulèvent, le hero réagit au scroll, les piliers entrent en zig-zag,
la lightbox glisse dans le sens demandé et les formulaires répondent.

prefers-reduced-motion est respecté partout : Framer coupe via
useReducedMotion, le CSS via la media query déjà en place.

Le commit embarque aussi la réorganisation des composants par domaine
qui était en cours dans l'arbre de travail — les deux étaient trop
imbriquées pour être séparées proprement.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-02 16:04:41 +02:00
parent 8fd9ca88cc
commit 9b6638eb00
66 changed files with 3938 additions and 1469 deletions
+27 -353
View File
@@ -1,364 +1,38 @@
"use client";
import Image from "next/image";
import Link from "next/link";
import { motion } from "framer-motion";
import { games } from "@/lib/games";
const featured = games[0];
// Screenshots WIP du projet.
// Pour en ajouter : dépose l'image dans /public/image/Emberwild/
// puis renseigne `src` (ex: "/image/Emberwild/screen-01.jpg").
// Laisse `src: null` pour afficher un emplacement placeholder.
const screenshots: { src: string | null; label: string; span: string }[] = [
{
src: "/image/Games/Emberwild/Capture_1.png",
label: "In-game",
span: "col-span-2 row-span-2",
},
{
src: "/image/Games/Emberwild/Capture_2.png",
label: "Environment",
span: "",
},
{
src: "/image/Games/Emberwild/Capture_3.png",
label: "Exploration",
span: "",
},
];
import { HomeHero } from "./components/home/HomeHero";
import { Statement } from "./components/home/Statement";
import { FeaturedGame } from "./components/home/FeaturedGame";
import { StudioIntro } from "./components/home/StudioIntro";
import { Dispatch } from "./components/home/Dispatch";
import { featuredGame } from "@/lib/games";
import { getAllPosts } from "@/lib/devblog";
/**
* Home page.
*
* Read top to bottom it answers four questions in order: what is this
* (hero), why should I care (statement), what are you making (featured game),
* who are you (studio), how do I follow it (dispatch). Each band changes
* rhythm — full-bleed, quiet, card, split, conversion — so the page never
* reads as one long column of identical text blocks.
*
* Game data reads from `lib/games.ts`, so the home page can never advertise
* something the game page doesn't show. Screenshots deliberately live only on
* /games/[slug] — the home page teases the game, the game page shows it.
*/
export default function Home() {
const latest = getAllPosts()[0] ?? null;
return (
<>
{/* HERO */}
<section className="relative isolate min-h-screen flex items-center justify-center overflow-hidden">
{/* Background video */}
<video
autoPlay
muted
loop
playsInline
preload="auto"
aria-hidden
className="absolute inset-0 z-0 w-full h-full object-cover"
>
<source src="/Video/Header_Video.mp4" type="video/mp4" />
</video>
{/* Dark overlay for text readability */}
<div
aria-hidden
className="absolute inset-0 z-10 bg-background/60"
/>
{/* Vignette / radial fade to deepen the edges */}
<div
aria-hidden
className="absolute inset-0 z-10"
style={{
background:
"radial-gradient(ellipse at 50% 50%, rgba(10,10,10,0) 30%, rgba(10,10,10,0.85) 100%)",
}}
/>
{/* Grain / noise overlay via CSS gradient */}
<div
aria-hidden
className="absolute inset-0 z-10 opacity-[0.03]"
style={{
backgroundImage:
"repeating-linear-gradient(0deg, #fff 0, #fff 1px, transparent 1px, transparent 3px)",
}}
/>
<HomeHero />
<div className="relative z-20 max-w-5xl w-full px-6 lg:px-10 flex flex-col items-center text-center pt-20">
<motion.div
initial={{ opacity: 0, scale: 0.92, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
transition={{ duration: 1.1, ease: [0.22, 1, 0.36, 1] }}
className="mb-10"
>
<Image
src="/image/Studio/Logo Flat.png"
alt="Highland Games Studio"
width={180}
height={120}
priority
className="drop-shadow-[0_0_40px_rgba(196,184,150,0.15)]"
/>
</motion.div>
<Statement />
<motion.h1
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.9, delay: 0.3, ease: "easeOut" }}
className="font-display text-6xl sm:text-7xl lg:text-8xl tracking-wider leading-[0.95] text-balance"
>
HIGHLAND GAMES
<br />
<span className="text-accent">STUDIO</span>
</motion.h1>
{featuredGame && <FeaturedGame game={featuredGame} />}
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.9, delay: 0.6, ease: "easeOut" }}
className="mt-8 text-lg sm:text-xl text-foreground-muted tracking-wide max-w-xl text-balance"
>
Where summits become worlds.
</motion.p>
<StudioIntro />
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.9, delay: 0.9, ease: "easeOut" }}
className="mt-12 flex flex-col sm:flex-row gap-4"
>
<Link
href="/games"
className="px-8 py-3 bg-accent text-background font-display tracking-widest text-sm hover:bg-foreground hover:scale-[1.03] transition duration-200"
>
EXPLORE OUR GAMES
</Link>
<Link
href="/studio"
className="px-8 py-3 border border-border text-foreground font-display tracking-widest text-sm hover:border-accent hover:text-accent hover:scale-[1.03] transition duration-200"
>
MEET THE STUDIO
</Link>
</motion.div>
</div>
{/* Scroll indicator - positioned relative to the section */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 1.5, duration: 1 }}
className="absolute z-20 bottom-10 left-1/2 -translate-x-1/2 flex flex-col items-center gap-2 text-foreground-muted text-xs tracking-widest"
>
<span className="font-display">SCROLL</span>
<motion.div
animate={{ y: [0, 8, 0] }}
transition={{ duration: 1.8, repeat: Infinity, ease: "easeInOut" }}
className="w-px h-8 bg-foreground-muted"
/>
</motion.div>
</section>
{/* STUDIO INTRO */}
<section className="py-24 px-6 lg:px-10">
<div className="max-w-6xl mx-auto">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.8 }}
>
<p className="font-display text-sm tracking-[0.3em] text-accent mb-4">
THE STUDIO
</p>
<h2 className="font-display text-4xl sm:text-5xl lg:text-6xl tracking-wider leading-tight mb-8">
Worlds beyond
<br />
the summit.
</h2>
<p className="text-lg text-foreground-muted leading-relaxed max-w-2xl">
We build worlds where every horizon hides a story. Hand-crafted
games made to be lived in, not just played through. Each one a
different summit, a different climb.
</p>
<Link
href="/studio"
className="inline-block mt-8 font-display text-sm tracking-widest text-accent hover:text-foreground transition-colors"
>
LEARN MORE
</Link>
</motion.div>
</div>
</section>
{/* FEATURED GAME */}
<section className="py-24 px-6 lg:px-10 border-t border-border">
<div className="max-w-6xl mx-auto">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.8 }}
>
<p className="font-display text-sm tracking-[0.3em] text-accent mb-4">
IN DEVELOPMENT
</p>
<h2 className="font-display text-4xl sm:text-5xl lg:text-6xl tracking-wider mb-12">
OUR FIRST WORLD
</h2>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.9, delay: 0.1 }}
>
<Link
href="/games/emberwild"
className="group relative aspect-[16/9] bg-surface border border-border overflow-hidden block hover:border-accent transition-colors duration-500"
>
{featured?.cover ? (
<Image
src={featured.cover}
alt={featured.title}
fill
sizes="(max-width: 1152px) 100vw, 1152px"
className="object-cover group-hover:scale-105 transition-transform duration-700 ease-out"
/>
) : (
<div className="absolute inset-0 flex items-center justify-center">
<Image
src="/image/Studio/Logo Flat.png"
alt="Game placeholder"
width={120}
height={80}
className="opacity-20 group-hover:opacity-30 group-hover:scale-105 transition-all duration-700 ease-out"
/>
</div>
)}
<div
aria-hidden
className="absolute inset-0 bg-accent/0 group-hover:bg-accent/[0.03] transition-colors duration-500"
/>
<div className="absolute bottom-0 left-0 right-0 p-8 bg-gradient-to-t from-background via-background/70 to-transparent">
<p className="font-display text-xs tracking-[0.3em] text-accent mb-2">
SURVIVAL · CRAFTING · EXPLORATION
</p>
<h3 className="font-display text-3xl sm:text-4xl tracking-wider mb-4 group-hover:text-accent transition-colors duration-300">
PROJECT ONE
</h3>
<p className="text-foreground-muted max-w-xl mb-6">
Inspired by Raft and Aloft. Build, explore, and survive across
shifting horizons. A new survival adventure, currently in early
development.
</p>
<span className="inline-flex items-center gap-2 font-display text-sm tracking-widest text-foreground group-hover:text-accent transition-colors">
LEARN MORE
<span className="inline-block transition-transform duration-300 group-hover:translate-x-1">
</span>
</span>
</div>
</Link>
</motion.div>
</div>
</section>
{/* GALLERY */}
<section className="py-24 px-6 lg:px-10 border-t border-border">
<div className="max-w-6xl mx-auto">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.8 }}
>
<p className="font-display text-sm tracking-[0.3em] text-accent mb-4">
GLIMPSES
</p>
<div className="flex flex-col sm:flex-row sm:items-end sm:justify-between gap-4 mb-12">
<h2 className="font-display text-4xl sm:text-5xl lg:text-6xl tracking-wider">
FROM THE WORKSHOP
</h2>
<p className="text-sm text-foreground-muted max-w-sm">
Raw captures straight from development. Everything here is work
in progress and will keep evolving.
</p>
</div>
</motion.div>
<div className="grid grid-cols-2 lg:grid-cols-3 gap-4 auto-rows-[160px] sm:auto-rows-[220px]">
{screenshots.map((shot, i) => (
<motion.div
key={i}
initial={{ opacity: 0, scale: 0.96 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true, margin: "-60px" }}
transition={{ duration: 0.6, delay: i * 0.08 }}
className={`group relative overflow-hidden rounded-2xl border border-border bg-surface ${shot.span}`}
>
{shot.src ? (
<Image
src={shot.src}
alt={shot.label}
fill
sizes="(max-width: 1024px) 50vw, 33vw"
className="object-cover transition-transform duration-700 ease-out group-hover:scale-105"
/>
) : (
<div className="flex h-full w-full flex-col items-center justify-center gap-3">
<Image
src="/image/Studio/Logo Flat.png"
alt=""
width={64}
height={44}
className="opacity-[0.12]"
/>
<span className="font-display text-[11px] tracking-[0.3em] text-foreground-muted uppercase">
{shot.label}
</span>
</div>
)}
{/* Subtle gradient + label on hover for real screenshots */}
{shot.src && (
<>
<div
aria-hidden
className="absolute inset-0 bg-gradient-to-t from-background/70 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500"
/>
<span className="absolute bottom-3 left-4 font-display text-sm tracking-widest text-foreground opacity-0 translate-y-2 group-hover:opacity-100 group-hover:translate-y-0 transition-all duration-500">
{shot.label}
</span>
</>
)}
{/* WIP badge */}
<div className="absolute top-3 left-3 rounded-full border border-border/70 bg-background/70 px-3 py-1 backdrop-blur-sm">
<span className="font-display text-[10px] tracking-[0.2em] text-accent">
WIP
</span>
</div>
</motion.div>
))}
</div>
</div>
</section>
{/* DEVBLOG TEASER */}
<section className="py-24 px-6 lg:px-10 border-t border-border">
<div className="max-w-4xl mx-auto text-center">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.8 }}
>
<p className="font-display text-sm tracking-[0.3em] text-accent mb-4">
DEVBLOG
</p>
<h2 className="font-display text-4xl sm:text-5xl tracking-wider mb-6">
FOLLOW THE JOURNEY
</h2>
<p className="text-lg text-foreground-muted leading-relaxed max-w-2xl mx-auto mb-8">
Behind-the-scenes updates, design decisions, and milestones from
the studio. Every step of the climb, documented.
</p>
<Link
href="/devblog"
className="inline-block px-8 py-3 border border-border text-foreground font-display tracking-widest text-sm hover:border-accent hover:text-accent hover:scale-[1.03] transition duration-200"
>
READ THE DEVBLOG
</Link>
</motion.div>
</div>
</section>
<Dispatch latest={latest} />
</>
);
}