harder better faster stronger

This commit is contained in:
ordinarthur 2026-04-05 15:02:55 +02:00
parent 9d172a4422
commit fdcccd49aa
21 changed files with 2431 additions and 1661 deletions

View File

@ -2,107 +2,56 @@
const year = new Date().getFullYear();
---
<footer class="footer">
<div class="container">
<div class="divider"></div>
<div class="footer__inner">
<div class="footer__left">
<p class="footer__name">Aurélie Barré</p>
<p class="footer__tagline text-secondary">Design d'intérieur & Création événementielle</p>
</div>
<div class="footer__center">
<a href="mailto:contact@aureliebarre.fr" class="footer__link">contact@aureliebarre.fr</a>
</div>
<div class="footer__right">
<div class="footer__social">
<a href="https://instagram.com/aureliebarre" target="_blank" rel="noopener noreferrer" aria-label="Instagram" class="footer__social-link">
Instagram
</a>
<a href="https://linkedin.com/in/aureliebarre" target="_blank" rel="noopener noreferrer" aria-label="LinkedIn" class="footer__social-link">
LinkedIn
</a>
</div>
<p class="footer__copyright text-light">&copy; {year}</p>
<footer class="ft">
<div class="wrap">
<div class="ft__line"></div>
<div class="ft__row">
<span class="ft__copy">&copy; {year}</span>
<div class="ft__links">
<a href="https://instagram.com/aureliebarre" target="_blank" rel="noopener">instagram</a>
<a href="https://linkedin.com/in/aureliebarre" target="_blank" rel="noopener">linkedin</a>
</div>
</div>
</div>
</footer>
<style>
.footer {
padding-top: var(--space-xl);
padding-bottom: var(--space-lg);
.ft {
padding: 0 0 2rem;
}
.footer__inner {
.ft__line {
height: 1px;
background: var(--border-light);
margin-bottom: 1.5rem;
}
.ft__row {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding-top: var(--space-xl);
gap: var(--space-lg);
align-items: center;
}
.footer__name {
font-family: var(--font-serif);
font-size: 1.1rem;
margin-bottom: var(--space-xs);
.ft__copy {
font-size: 0.6rem;
color: var(--text-3);
letter-spacing: 0.05em;
}
.footer__tagline {
font-size: 0.85rem;
}
.footer__link {
font-size: 0.85rem;
letter-spacing: 0.02em;
color: var(--color-text-secondary);
transition: color var(--duration-fast) var(--ease-out);
}
.footer__link:hover {
color: var(--color-accent);
}
.footer__right {
text-align: right;
}
.footer__social {
.ft__links {
display: flex;
gap: var(--space-lg);
margin-bottom: var(--space-sm);
gap: 1.5rem;
}
.footer__social-link {
font-size: 0.85rem;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--color-text-secondary);
.ft__links a {
font-size: 0.6rem;
letter-spacing: 0.1em;
color: var(--text-3);
text-decoration: none;
transition: color 0.2s;
}
.footer__social-link:hover {
color: var(--color-accent);
}
.footer__copyright {
font-size: 0.75rem;
}
@media (max-width: 768px) {
.footer__inner {
flex-direction: column;
align-items: center;
text-align: center;
}
.footer__right {
text-align: center;
}
.footer__social {
justify-content: center;
}
.ft__links a:hover {
color: var(--text);
}
</style>

View File

@ -1,171 +1,167 @@
---
const currentPath = Astro.url.pathname;
const navItems = [
{ label: 'Réalisations Pérennes', href: '/realisations-perennes' },
{ label: 'Créations Événement', href: '/creations-evenement' },
{ label: 'Processus Créatif', href: '/processus' },
const isHome = currentPath === '/';
const nav = [
{ label: 'projets', href: '/creations-evenement' },
{ label: 'pérenne', href: '/realisations-perennes' },
{ label: 'processus', href: '/processus' },
{ label: 'contact', href: 'mailto:contact@aureliebarre.fr' },
];
---
<header class="header">
<div class="header__inner container">
<a href="/" class="header__logo">
<span class="header__logo-name">Aurélie Barré</span>
</a>
<header class:list={['hd', { 'hd--hero': isHome }]} id="hd">
<div class="hd__in">
<a href="/" class="hd__logo">aurélie barré</a>
<nav class="header__nav" aria-label="Navigation principale">
<ul class="header__nav-list">
{navItems.map(item => (
<li>
<a
href={item.href}
class:list={['header__nav-link', { 'active': currentPath.startsWith(item.href) }]}
>
{item.label}
</a>
</li>
))}
</ul>
<nav class="hd__nav" aria-label="Navigation">
{nav.map(item => (
<a
href={item.href}
class:list={['hd__link', { 'is-active': item.href !== 'mailto:contact@aureliebarre.fr' && currentPath.startsWith(item.href) }]}
>
{item.label}
</a>
))}
</nav>
<button class="header__menu-btn" aria-label="Ouvrir le menu" aria-expanded="false">
<span class="header__menu-line"></span>
<span class="header__menu-line"></span>
<button class="hd__burger" id="burger" aria-label="Menu" aria-expanded="false">
<span></span>
<span></span>
</button>
</div>
<!-- Mobile overlay -->
<div class="header__overlay" aria-hidden="true">
<nav class="header__overlay-nav">
{navItems.map(item => (
<a href={item.href} class="header__overlay-link">
{item.label}
</a>
<!-- Mobile fullscreen -->
<div class="hd__mobile" id="mobile-nav" aria-hidden="true">
<nav class="hd__mobile-nav">
{nav.map((item, i) => (
<a href={item.href} class="hd__mobile-link" style={`--i:${i}`}>{item.label}</a>
))}
</nav>
</div>
</header>
<script>
const btn = document.querySelector('.header__menu-btn');
const overlay = document.querySelector('.header__overlay');
const header = document.querySelector('.header');
const burger = document.getElementById('burger');
const mobile = document.getElementById('mobile-nav');
const hd = document.getElementById('hd');
btn?.addEventListener('click', () => {
const isOpen = btn.getAttribute('aria-expanded') === 'true';
btn.setAttribute('aria-expanded', String(!isOpen));
overlay?.setAttribute('aria-hidden', String(isOpen));
header?.classList.toggle('menu-open');
document.body.style.overflow = isOpen ? '' : 'hidden';
burger?.addEventListener('click', () => {
const open = burger.getAttribute('aria-expanded') === 'true';
burger.setAttribute('aria-expanded', String(!open));
mobile?.setAttribute('aria-hidden', String(open));
hd?.classList.toggle('is-open');
document.body.style.overflow = open ? '' : 'hidden';
});
// Close on link click
document.querySelectorAll('.header__overlay-link').forEach(link => {
link.addEventListener('click', () => {
btn?.setAttribute('aria-expanded', 'false');
overlay?.setAttribute('aria-hidden', 'true');
header?.classList.remove('menu-open');
document.querySelectorAll('.hd__mobile-link').forEach(l => {
l.addEventListener('click', () => {
burger?.setAttribute('aria-expanded', 'false');
mobile?.setAttribute('aria-hidden', 'true');
hd?.classList.remove('is-open');
document.body.style.overflow = '';
});
});
// Header scroll effect
let lastScroll = 0;
let ticking = false;
window.addEventListener('scroll', () => {
const scrollY = window.scrollY;
if (scrollY > 100) {
header?.classList.add('scrolled');
} else {
header?.classList.remove('scrolled');
if (!ticking) {
requestAnimationFrame(() => {
hd?.classList.toggle('is-scrolled', window.scrollY > 60);
ticking = false;
});
ticking = true;
}
lastScroll = scrollY;
});
</script>
<style>
.header {
.hd {
position: fixed;
top: 0;
left: 0;
right: 0;
top: 0; left: 0; right: 0;
z-index: 100;
height: var(--header-height);
background-color: transparent;
transition: background-color var(--duration-normal) var(--ease-out),
box-shadow var(--duration-normal) var(--ease-out);
height: var(--header-h);
transition: background 0.5s var(--ease), backdrop-filter 0.5s;
}
.header.scrolled {
background-color: rgba(250, 250, 248, 0.95);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
box-shadow: 0 1px 0 var(--color-border-light);
/* ---- Default: dark text (inner pages) ---- */
.hd__logo { color: var(--text); }
.hd__link { color: var(--text-2); }
.hd__link:hover, .hd__link.is-active { color: var(--text); }
.hd__burger span { background: var(--text); }
/* ---- Hero variant: white text on homepage ---- */
.hd--hero .hd__logo { color: #fff; }
.hd--hero .hd__link { color: rgba(255,255,255,0.6); }
.hd--hero .hd__link:hover,
.hd--hero .hd__link.is-active { color: #fff; }
.hd--hero .hd__link::after { background: #fff; }
.hd--hero .hd__burger span { background: #fff; }
/* ---- Scrolled: always white bg + dark text ---- */
.hd.is-scrolled {
background: rgba(255,255,255,0.94);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
}
.header__inner {
.hd.is-scrolled .hd__logo { color: var(--text); }
.hd.is-scrolled .hd__link { color: var(--text-2); }
.hd.is-scrolled .hd__link:hover,
.hd.is-scrolled .hd__link.is-active { color: var(--text); }
.hd.is-scrolled .hd__link::after { background: var(--text); }
.hd.is-scrolled .hd__burger span { background: var(--text); }
.hd__in {
display: flex;
align-items: center;
justify-content: space-between;
height: 100%;
padding: 0 var(--pad);
max-width: 1400px;
margin: 0 auto;
}
.header__logo {
text-decoration: none;
z-index: 101;
}
.header__logo-name {
font-family: var(--font-serif);
font-size: 1.4rem;
color: var(--color-text);
letter-spacing: 0.01em;
}
.header__nav-list {
display: flex;
gap: var(--space-xl);
list-style: none;
}
.header__nav-link {
font-size: 0.85rem;
.hd__logo {
font-family: var(--sans);
font-size: 0.8rem;
font-weight: 400;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--color-text-secondary);
text-decoration: none;
position: relative;
padding-bottom: 2px;
transition: color var(--duration-fast) var(--ease-out);
z-index: 101;
transition: color 0.4s var(--ease);
}
.header__nav-link::after {
.hd__nav {
display: flex;
gap: 2.5rem;
}
.hd__link {
font-size: 0.7rem;
font-weight: 300;
letter-spacing: 0.1em;
text-decoration: none;
transition: color 0.3s var(--ease);
position: relative;
}
.hd__link::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 0;
height: 1px;
background-color: var(--color-accent);
transition: width var(--duration-normal) var(--ease-out);
bottom: -2px; left: 0;
width: 0; height: 1px;
background: var(--text);
transition: width 0.4s var(--ease);
}
.header__nav-link:hover,
.header__nav-link.active {
color: var(--color-text);
}
.hd__link:hover::after,
.hd__link.is-active::after { width: 100%; }
.header__nav-link:hover::after,
.header__nav-link.active::after {
width: 100%;
}
/* Mobile menu button */
.header__menu-btn {
/* Burger */
.hd__burger {
display: none;
flex-direction: column;
gap: 6px;
gap: 5px;
background: none;
border: none;
cursor: pointer;
@ -173,85 +169,65 @@ const navItems = [
z-index: 101;
}
.header__menu-line {
.hd__burger span {
display: block;
width: 24px;
height: 1.5px;
background-color: var(--color-text);
transition: transform var(--duration-normal) var(--ease-out),
opacity var(--duration-fast);
width: 20px;
height: 1px;
transition: transform 0.4s var(--ease), opacity 0.2s, background 0.3s;
}
.menu-open .header__menu-line:first-child {
transform: translateY(3.75px) rotate(45deg);
}
.is-open .hd__burger span:first-child { transform: translateY(3px) rotate(45deg); }
.is-open .hd__burger span:last-child { transform: translateY(-3px) rotate(-45deg); }
.menu-open .header__menu-line:last-child {
transform: translateY(-3.75px) rotate(-45deg);
}
/* When menu is open, force dark burger (white bg behind) */
.is-open .hd__burger span { background: var(--text) !important; }
/* Mobile overlay */
.header__overlay {
.hd__mobile {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--color-bg);
inset: 0;
background: var(--bg);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
visibility: hidden;
transition: opacity var(--duration-normal) var(--ease-out),
visibility var(--duration-normal);
transition: opacity 0.5s var(--ease), visibility 0.5s;
z-index: 99;
}
.menu-open .header__overlay {
.is-open .hd__mobile {
opacity: 1;
visibility: visible;
}
.header__overlay-nav {
.hd__mobile-nav {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-lg);
gap: 2rem;
text-align: center;
}
.header__overlay-link {
font-family: var(--font-serif);
font-size: clamp(1.8rem, 5vw, 2.5rem);
color: var(--color-text);
.hd__mobile-link {
font-family: var(--serif);
font-size: clamp(2rem, 6vw, 3rem);
color: var(--text);
text-decoration: none;
opacity: 0;
transform: translateY(20px);
transition: opacity var(--duration-normal) var(--ease-out),
transform var(--duration-normal) var(--ease-out),
color var(--duration-fast);
transition: opacity 0.5s var(--ease), transform 0.5s var(--ease), color 0.2s;
}
.menu-open .header__overlay-link {
.is-open .hd__mobile-link {
opacity: 1;
transform: translateY(0);
transition-delay: calc(var(--i) * 80ms + 100ms);
}
.menu-open .header__overlay-link:nth-child(1) { transition-delay: 100ms; }
.menu-open .header__overlay-link:nth-child(2) { transition-delay: 200ms; }
.menu-open .header__overlay-link:nth-child(3) { transition-delay: 300ms; }
.header__overlay-link:hover {
color: var(--color-accent);
}
.hd__mobile-link:hover { color: var(--text-2); }
@media (max-width: 768px) {
.header__nav {
display: none;
}
.header__menu-btn {
display: flex;
}
.hd__nav { display: none; }
.hd__burger { display: flex; }
}
</style>

View File

@ -0,0 +1,218 @@
---
interface GalleryImage {
src: string;
alt: string;
}
interface Props {
images: GalleryImage[];
}
const { images } = Astro.props;
// Distribute images across 3 columns
const cols: GalleryImage[][] = [[], [], []];
images.forEach((img, i) => cols[i % 3].push(img));
---
<div class="masonry" id="masonry-gallery">
{cols.map((col, ci) => (
<div class="masonry__col">
{col.map((img, ri) => {
const idx = ri * 3 + ci;
return (
<div
class="masonry__item rv"
style={`--d:${idx % 8}`}
data-index={idx}
>
<img
src={img.src}
alt={img.alt}
loading="lazy"
decoding="async"
/>
</div>
);
})}
</div>
))}
</div>
<!-- Lightbox (vanilla JS, no React needed) -->
<div class="lb" id="lightbox" aria-hidden="true">
<button class="lb__close" id="lb-close" aria-label="Fermer">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#111" stroke-width="1.5">
<line x1="4" y1="4" x2="20" y2="20" /><line x1="20" y1="4" x2="4" y2="20" />
</svg>
</button>
<div class="lb__counter" id="lb-counter"></div>
<button class="lb__arrow lb__arrow--prev" id="lb-prev" aria-label="Précédent">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#111" stroke-width="1.5">
<polyline points="15,4 7,12 15,20" />
</svg>
</button>
<button class="lb__arrow lb__arrow--next" id="lb-next" aria-label="Suivant">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#111" stroke-width="1.5">
<polyline points="9,4 17,12 9,20" />
</svg>
</button>
<img class="lb__img" id="lb-img" src="" alt="" />
</div>
<script define:vars={{ images }}>
const lb = document.getElementById('lightbox');
const lbImg = document.getElementById('lb-img');
const lbCounter = document.getElementById('lb-counter');
const lbClose = document.getElementById('lb-close');
const lbPrev = document.getElementById('lb-prev');
const lbNext = document.getElementById('lb-next');
let current = -1;
const total = images.length;
function open(idx) {
current = idx;
update();
lb.classList.add('is-open');
lb.setAttribute('aria-hidden', 'false');
document.body.style.overflow = 'hidden';
}
function close() {
lb.classList.remove('is-open');
lb.setAttribute('aria-hidden', 'true');
document.body.style.overflow = '';
current = -1;
}
function update() {
if (current < 0 || current >= total) return;
lbImg.src = images[current].src;
lbImg.alt = images[current].alt;
lbCounter.textContent = `${current + 1} / ${total}`;
lbPrev.style.display = current > 0 ? '' : 'none';
lbNext.style.display = current < total - 1 ? '' : 'none';
}
// Click on gallery items
document.querySelectorAll('.masonry__item').forEach(item => {
item.addEventListener('click', () => {
const idx = parseInt(item.dataset.index, 10);
open(idx);
});
});
lbClose.addEventListener('click', close);
lb.addEventListener('click', (e) => { if (e.target === lb) close(); });
lbPrev.addEventListener('click', (e) => { e.stopPropagation(); if (current > 0) { current--; update(); } });
lbNext.addEventListener('click', (e) => { e.stopPropagation(); if (current < total - 1) { current++; update(); } });
document.addEventListener('keydown', (e) => {
if (current < 0) return;
if (e.key === 'Escape') close();
if (e.key === 'ArrowRight' && current < total - 1) { current++; update(); }
if (e.key === 'ArrowLeft' && current > 0) { current--; update(); }
});
</script>
<style>
.masonry {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 4px;
}
.masonry__col {
display: flex;
flex-direction: column;
gap: 4px;
}
.masonry__item {
cursor: pointer;
overflow: hidden;
}
.masonry__item img {
width: 100%;
height: auto;
display: block;
transition: transform 0.6s cubic-bezier(0.16, 1, 0.3, 1);
}
.masonry__item:hover img {
transform: scale(1.02);
}
@media (max-width: 1024px) {
.masonry { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 640px) {
.masonry { grid-template-columns: 1fr; }
}
/* Lightbox */
.lb {
position: fixed;
inset: 0;
z-index: 200;
background: rgba(255, 255, 255, 0.97);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s, visibility 0.3s;
cursor: pointer;
}
.lb.is-open {
opacity: 1;
visibility: visible;
}
.lb__close {
position: absolute;
top: 2rem;
right: 2rem;
background: none;
border: none;
cursor: pointer;
padding: 8px;
z-index: 201;
}
.lb__counter {
position: absolute;
top: 2rem;
left: 50%;
transform: translateX(-50%);
font-size: 0.7rem;
letter-spacing: 0.1em;
color: #999;
z-index: 201;
}
.lb__arrow {
position: absolute;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
cursor: pointer;
padding: 12px;
z-index: 201;
}
.lb__arrow--prev { left: 2rem; }
.lb__arrow--next { right: 2rem; }
.lb__img {
max-width: 88vw;
max-height: 88vh;
object-fit: contain;
cursor: default;
transition: opacity 0.2s;
}
</style>

View File

@ -4,7 +4,6 @@ import { motion, AnimatePresence } from 'motion/react';
interface GalleryImage {
src: string;
alt: string;
size?: 'small' | 'medium' | 'large';
}
interface Props {
@ -12,89 +11,72 @@ interface Props {
}
export default function MasonryGallery({ images }: Props) {
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
const [columns, setColumns] = useState(3);
const containerRef = useRef<HTMLDivElement>(null);
const [sel, setSel] = useState<number | null>(null);
const [cols, setCols] = useState(3);
// Responsive columns
useEffect(() => {
const updateColumns = () => {
const width = window.innerWidth;
if (width < 640) setColumns(1);
else if (width < 1024) setColumns(2);
else setColumns(3);
const update = () => {
const w = window.innerWidth;
setCols(w < 640 ? 1 : w < 1024 ? 2 : 3);
};
updateColumns();
window.addEventListener('resize', updateColumns);
return () => window.removeEventListener('resize', updateColumns);
update();
window.addEventListener('resize', update);
return () => window.removeEventListener('resize', update);
}, []);
// Distribute images across columns
const getColumns = useCallback(() => {
const cols: GalleryImage[][] = Array.from({ length: columns }, () => []);
images.forEach((img, i) => {
cols[i % columns].push(img);
});
return cols;
}, [images, columns]);
const getCols = useCallback(() => {
const c: GalleryImage[][] = Array.from({ length: cols }, () => []);
images.forEach((img, i) => c[i % cols].push(img));
return c;
}, [images, cols]);
// Lightbox keyboard nav
useEffect(() => {
const handleKey = (e: KeyboardEvent) => {
if (selectedIndex === null) return;
if (e.key === 'Escape') setSelectedIndex(null);
if (e.key === 'ArrowRight') setSelectedIndex(prev =>
prev !== null && prev < images.length - 1 ? prev + 1 : prev
);
if (e.key === 'ArrowLeft') setSelectedIndex(prev =>
prev !== null && prev > 0 ? prev - 1 : prev
);
const handle = (e: KeyboardEvent) => {
if (sel === null) return;
if (e.key === 'Escape') setSel(null);
if (e.key === 'ArrowRight') setSel(p => p !== null && p < images.length - 1 ? p + 1 : p);
if (e.key === 'ArrowLeft') setSel(p => p !== null && p > 0 ? p - 1 : p);
};
window.addEventListener('keydown', handleKey);
return () => window.removeEventListener('keydown', handleKey);
}, [selectedIndex, images.length]);
window.addEventListener('keydown', handle);
return () => window.removeEventListener('keydown', handle);
}, [sel, images.length]);
// Lock body scroll when lightbox open
useEffect(() => {
document.body.style.overflow = selectedIndex !== null ? 'hidden' : '';
document.body.style.overflow = sel !== null ? 'hidden' : '';
return () => { document.body.style.overflow = ''; };
}, [selectedIndex]);
}, [sel]);
const flatIndex = (colIdx: number, rowIdx: number) => {
return rowIdx * columns + colIdx;
};
const flatIdx = (colIdx: number, rowIdx: number) => rowIdx * cols + colIdx;
return (
<>
<div ref={containerRef} style={gridContainerStyle(columns)}>
{getColumns().map((col, colIdx) => (
<div key={colIdx} style={columnStyle}>
{col.map((img, rowIdx) => {
const idx = flatIndex(colIdx, rowIdx);
<div style={{ display: 'grid', gridTemplateColumns: `repeat(${cols}, 1fr)`, gap: '4px' }}>
{getCols().map((col, ci) => (
<div key={ci} style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
{col.map((img, ri) => {
const idx = flatIdx(ci, ri);
return (
<motion.div
key={idx}
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: idx * 0.06, duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
style={imageWrapperStyle}
onClick={() => setSelectedIndex(idx)}
role="button"
tabIndex={0}
aria-label={`Voir ${img.alt}`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: idx * 0.04, duration: 0.5 }}
style={{ cursor: 'pointer', overflow: 'hidden' }}
onClick={() => setSel(idx)}
>
<img
src={img.src}
alt={img.alt}
loading="lazy"
decoding="async"
style={imageStyle}
onMouseEnter={(e) => {
(e.target as HTMLImageElement).style.transform = 'scale(1.03)';
}}
onMouseLeave={(e) => {
(e.target as HTMLImageElement).style.transform = 'scale(1)';
style={{
width: '100%',
height: 'auto',
display: 'block',
transition: 'transform 0.6s cubic-bezier(0.16,1,0.3,1)',
}}
onMouseEnter={e => { (e.target as HTMLImageElement).style.transform = 'scale(1.02)'; }}
onMouseLeave={e => { (e.target as HTMLImageElement).style.transform = 'scale(1)'; }}
/>
</motion.div>
);
@ -103,53 +85,70 @@ export default function MasonryGallery({ images }: Props) {
))}
</div>
{/* Lightbox */}
{/* White lightbox */}
<AnimatePresence>
{selectedIndex !== null && (
{sel !== null && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3 }}
style={lightboxStyle}
onClick={() => setSelectedIndex(null)}
transition={{ duration: 0.25 }}
style={{
position: 'fixed', inset: 0, zIndex: 200,
backgroundColor: 'rgba(255,255,255,0.97)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
cursor: 'pointer',
}}
onClick={() => setSel(null)}
>
{/* Close button */}
{/* Close */}
<button
style={closeButtonStyle}
onClick={() => setSelectedIndex(null)}
onClick={() => setSel(null)}
aria-label="Fermer"
style={{
position: 'absolute', top: '2rem', right: '2rem',
background: 'none', border: 'none', cursor: 'pointer', padding: '8px', zIndex: 201,
}}
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="1.5">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#111" strokeWidth="1.5">
<line x1="4" y1="4" x2="20" y2="20" />
<line x1="20" y1="4" x2="4" y2="20" />
</svg>
</button>
{/* Counter */}
<div style={counterStyle}>
{selectedIndex + 1} / {images.length}
<div style={{
position: 'absolute', top: '2rem', left: '50%', transform: 'translateX(-50%)',
fontSize: '0.7rem', letterSpacing: '0.1em', color: '#999', zIndex: 201,
}}>
{sel + 1} / {images.length}
</div>
{/* Navigation arrows */}
{selectedIndex > 0 && (
{/* Arrows */}
{sel > 0 && (
<button
style={{ ...arrowStyle, left: '2rem' }}
onClick={(e) => { e.stopPropagation(); setSelectedIndex(selectedIndex - 1); }}
aria-label="Photo précédente"
onClick={e => { e.stopPropagation(); setSel(sel - 1); }}
aria-label="Précédent"
style={{
position: 'absolute', left: '2rem', top: '50%', transform: 'translateY(-50%)',
background: 'none', border: 'none', cursor: 'pointer', padding: '12px', zIndex: 201,
}}
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="1.5">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#111" strokeWidth="1.5">
<polyline points="15,4 7,12 15,20" />
</svg>
</button>
)}
{selectedIndex < images.length - 1 && (
{sel < images.length - 1 && (
<button
style={{ ...arrowStyle, right: '2rem' }}
onClick={(e) => { e.stopPropagation(); setSelectedIndex(selectedIndex + 1); }}
aria-label="Photo suivante"
onClick={e => { e.stopPropagation(); setSel(sel + 1); }}
aria-label="Suivant"
style={{
position: 'absolute', right: '2rem', top: '50%', transform: 'translateY(-50%)',
background: 'none', border: 'none', cursor: 'pointer', padding: '12px', zIndex: 201,
}}
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="1.5">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#111" strokeWidth="1.5">
<polyline points="9,4 17,12 9,20" />
</svg>
</button>
@ -157,15 +156,15 @@ export default function MasonryGallery({ images }: Props) {
{/* Image */}
<motion.img
key={selectedIndex}
src={images[selectedIndex].src}
alt={images[selectedIndex].alt}
initial={{ opacity: 0, scale: 0.95 }}
key={sel}
src={images[sel].src}
alt={images[sel].alt}
initial={{ opacity: 0, scale: 0.97 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1] }}
style={lightboxImageStyle}
onClick={(e) => e.stopPropagation()}
exit={{ opacity: 0, scale: 0.97 }}
transition={{ duration: 0.25 }}
style={{ maxWidth: '88vw', maxHeight: '88vh', objectFit: 'contain', cursor: 'default' }}
onClick={e => e.stopPropagation()}
/>
</motion.div>
)}
@ -173,83 +172,3 @@ export default function MasonryGallery({ images }: Props) {
</>
);
}
// --- Inline Styles ---
const gridContainerStyle = (cols: number): React.CSSProperties => ({
display: 'grid',
gridTemplateColumns: `repeat(${cols}, 1fr)`,
gap: '1rem',
});
const columnStyle: React.CSSProperties = {
display: 'flex',
flexDirection: 'column',
gap: '1rem',
};
const imageWrapperStyle: React.CSSProperties = {
overflow: 'hidden',
cursor: 'pointer',
borderRadius: '2px',
backgroundColor: '#F3F1ED',
};
const imageStyle: React.CSSProperties = {
width: '100%',
height: 'auto',
display: 'block',
transition: 'transform 0.6s cubic-bezier(0.16, 1, 0.3, 1)',
};
const lightboxStyle: React.CSSProperties = {
position: 'fixed',
inset: 0,
zIndex: 200,
backgroundColor: 'rgba(0, 0, 0, 0.92)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
};
const lightboxImageStyle: React.CSSProperties = {
maxWidth: '90vw',
maxHeight: '90vh',
objectFit: 'contain',
cursor: 'default',
};
const closeButtonStyle: React.CSSProperties = {
position: 'absolute',
top: '2rem',
right: '2rem',
background: 'none',
border: 'none',
cursor: 'pointer',
padding: '8px',
zIndex: 201,
};
const counterStyle: React.CSSProperties = {
position: 'absolute',
top: '2rem',
left: '50%',
transform: 'translateX(-50%)',
color: 'rgba(255,255,255,0.6)',
fontSize: '0.85rem',
letterSpacing: '0.08em',
zIndex: 201,
};
const arrowStyle: React.CSSProperties = {
position: 'absolute',
top: '50%',
transform: 'translateY(-50%)',
background: 'none',
border: 'none',
cursor: 'pointer',
padding: '12px',
zIndex: 201,
opacity: 0.7,
transition: 'opacity 0.2s',
};

View File

@ -0,0 +1,91 @@
---
interface Step {
title: string;
stepNumber: number;
subtitle: string;
description: string;
duration?: string;
}
interface Props {
steps: Step[];
}
const { steps } = Astro.props;
---
<div class="tl">
{steps.map((step, i) => (
<div class="tl__step rv" style={`--d:${i}`}>
<span class="tl__num">{String(step.stepNumber).padStart(2, '0')}</span>
<div class="tl__content">
<span class="tl__sub">{step.subtitle}</span>
<h3 class="tl__title">{step.title}</h3>
<p class="tl__desc">{step.description}</p>
{step.duration && (
<span class="tl__dur">{step.duration}</span>
)}
</div>
</div>
))}
</div>
<style>
.tl {
max-width: 700px;
}
.tl__step {
display: grid;
grid-template-columns: 3.5rem 1fr;
gap: 1.5rem;
padding-bottom: 3.5rem;
margin-bottom: 3.5rem;
border-bottom: 1px solid #f0f0f0;
}
.tl__step:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.tl__num {
font-family: var(--serif);
font-size: 1.6rem;
color: #ccc;
line-height: 1;
padding-top: 0.15rem;
}
.tl__sub {
font-size: 0.6rem;
letter-spacing: 0.12em;
text-transform: lowercase;
color: var(--text-3);
display: block;
margin-bottom: 0.4rem;
}
.tl__title {
font-size: clamp(1.2rem, 2.5vw, 1.7rem);
font-weight: 400;
line-height: 1.2;
margin-bottom: 0.8rem;
}
.tl__desc {
font-size: 0.9rem;
line-height: 1.7;
color: var(--text-2);
max-width: 50ch;
}
.tl__dur {
display: inline-block;
margin-top: 1rem;
font-size: 0.65rem;
letter-spacing: 0.08em;
color: var(--text-3);
}
</style>

View File

@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'react';
import { useRef } from 'react';
import { motion, useInView } from 'motion/react';
interface ProcessStep {
@ -13,38 +13,77 @@ interface Props {
steps: ProcessStep[];
}
function StepCard({ step, index }: { step: ProcessStep; index: number }) {
function Step({ step, index }: { step: ProcessStep; index: number }) {
const ref = useRef<HTMLDivElement>(null);
const isInView = useInView(ref, { once: true, margin: '-100px 0px' });
const inView = useInView(ref, { once: true, margin: '-80px 0px' });
return (
<motion.div
ref={ref}
initial={{ opacity: 0, y: 60 }}
animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 60 }}
transition={{ duration: 0.8, delay: 0.1, ease: [0.16, 1, 0.3, 1] }}
style={stepStyle}
initial={{ opacity: 0, y: 40 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.7, delay: 0.05, ease: [0.16, 1, 0.3, 1] }}
style={{
display: 'grid',
gridTemplateColumns: '3.5rem 1fr',
gap: '1.5rem',
paddingBottom: '3.5rem',
marginBottom: '3.5rem',
borderBottom: index < 4 ? '1px solid #f0f0f0' : 'none',
}}
>
<div style={stepLeftStyle}>
<div style={stepNumberContainerStyle}>
<motion.span
initial={{ scale: 0 }}
animate={isInView ? { scale: 1 } : { scale: 0 }}
transition={{ delay: 0.2, duration: 0.5, ease: [0.16, 1, 0.3, 1] }}
style={stepNumberStyle}
>
{String(step.stepNumber).padStart(2, '0')}
</motion.span>
{index < 4 && <div style={stepLineStyle} />}
</div>
</div>
<span style={{
fontFamily: "'DM Serif Display', Georgia, serif",
fontSize: '1.6rem',
color: '#ccc',
lineHeight: 1,
paddingTop: '0.15rem',
}}>
{String(step.stepNumber).padStart(2, '0')}
</span>
<div>
<span style={{
fontSize: '0.6rem',
letterSpacing: '0.12em',
textTransform: 'lowercase' as const,
color: '#999',
display: 'block',
marginBottom: '0.4rem',
}}>
{step.subtitle}
</span>
<h3 style={{
fontFamily: "'DM Serif Display', Georgia, serif",
fontSize: 'clamp(1.2rem, 2.5vw, 1.7rem)',
fontWeight: 400,
lineHeight: 1.2,
color: '#111',
marginBottom: '0.8rem',
}}>
{step.title}
</h3>
<p style={{
fontSize: '0.9rem',
lineHeight: 1.7,
color: '#555',
maxWidth: '50ch',
}}>
{step.description}
</p>
<div style={stepContentStyle}>
<span style={stepSubtitleStyle}>{step.subtitle}</span>
<h3 style={stepTitleStyle}>{step.title}</h3>
<p style={stepDescStyle}>{step.description}</p>
{step.duration && (
<span style={stepDurationStyle}>{step.duration}</span>
<span style={{
display: 'inline-block',
marginTop: '1rem',
fontSize: '0.65rem',
letterSpacing: '0.08em',
color: '#999',
}}>
{step.duration}
</span>
)}
</div>
</motion.div>
@ -52,187 +91,11 @@ function StepCard({ step, index }: { step: ProcessStep; index: number }) {
}
export default function ProcessTimeline({ steps }: Props) {
const [activeStep, setActiveStep] = useState(0);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleScroll = () => {
if (!containerRef.current) return;
const children = containerRef.current.children;
const viewportCenter = window.innerHeight / 2;
for (let i = children.length - 1; i >= 0; i--) {
const rect = children[i].getBoundingClientRect();
if (rect.top < viewportCenter) {
setActiveStep(i);
break;
}
}
};
window.addEventListener('scroll', handleScroll, { passive: true });
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return (
<div style={containerStyle}>
{/* Progress indicator */}
<div style={progressBarContainerStyle}>
<div style={progressTrackStyle}>
<motion.div
style={progressFillStyle}
animate={{ height: `${((activeStep + 1) / steps.length) * 100}%` }}
transition={{ duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
/>
</div>
{steps.map((_, i) => (
<motion.div
key={i}
style={{
...progressDotStyle,
top: `${(i / (steps.length - 1)) * 100}%`,
}}
animate={{
backgroundColor: i <= activeStep ? '#C4A77D' : '#E8E6E1',
scale: i === activeStep ? 1.3 : 1,
}}
transition={{ duration: 0.3 }}
/>
))}
</div>
{/* Steps */}
<div ref={containerRef} style={stepsContainerStyle}>
{steps.map((step, index) => (
<StepCard key={step.stepNumber} step={step} index={index} />
))}
</div>
<div style={{ maxWidth: '700px' }}>
{steps.map((step, i) => (
<Step key={step.stepNumber} step={step} index={i} />
))}
</div>
);
}
// --- Styles ---
const containerStyle: React.CSSProperties = {
display: 'flex',
gap: '3rem',
position: 'relative',
maxWidth: '900px',
margin: '0 auto',
};
const progressBarContainerStyle: React.CSSProperties = {
position: 'sticky',
top: '40vh',
height: '300px',
width: '2px',
alignSelf: 'flex-start',
display: 'none', // Hidden on mobile, shown via media query in parent
};
const progressTrackStyle: React.CSSProperties = {
position: 'absolute',
top: 0,
bottom: 0,
width: '2px',
backgroundColor: '#E8E6E1',
borderRadius: '1px',
};
const progressFillStyle: React.CSSProperties = {
width: '100%',
backgroundColor: '#C4A77D',
borderRadius: '1px',
};
const progressDotStyle: React.CSSProperties = {
position: 'absolute',
left: '50%',
transform: 'translate(-50%, -50%)',
width: '8px',
height: '8px',
borderRadius: '50%',
zIndex: 1,
};
const stepsContainerStyle: React.CSSProperties = {
display: 'flex',
flexDirection: 'column',
gap: '6rem',
flex: 1,
};
const stepStyle: React.CSSProperties = {
display: 'flex',
gap: '2rem',
alignItems: 'flex-start',
};
const stepLeftStyle: React.CSSProperties = {
flexShrink: 0,
width: '60px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
};
const stepNumberContainerStyle: React.CSSProperties = {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '0.5rem',
};
const stepNumberStyle: React.CSSProperties = {
fontFamily: "'DM Serif Display', Georgia, serif",
fontSize: '2rem',
color: '#C4A77D',
lineHeight: 1,
};
const stepLineStyle: React.CSSProperties = {
width: '1px',
height: '100px',
backgroundColor: '#E8E6E1',
marginTop: '0.5rem',
};
const stepContentStyle: React.CSSProperties = {
flex: 1,
paddingTop: '0.25rem',
};
const stepSubtitleStyle: React.CSSProperties = {
fontSize: '0.75rem',
letterSpacing: '0.1em',
textTransform: 'uppercase' as const,
color: '#9A9A9A',
display: 'block',
marginBottom: '0.5rem',
};
const stepTitleStyle: React.CSSProperties = {
fontFamily: "'DM Serif Display', Georgia, serif",
fontSize: 'clamp(1.5rem, 3vw, 2rem)',
fontWeight: 400,
marginBottom: '1rem',
lineHeight: 1.3,
color: '#1A1A1A',
};
const stepDescStyle: React.CSSProperties = {
fontSize: '1rem',
lineHeight: 1.7,
color: '#6B6B6B',
maxWidth: '55ch',
};
const stepDurationStyle: React.CSSProperties = {
display: 'inline-block',
marginTop: '1rem',
fontSize: '0.8rem',
letterSpacing: '0.04em',
color: '#C4A77D',
padding: '4px 12px',
border: '1px solid #E8E6E1',
borderRadius: '2px',
};

View File

@ -1,123 +0,0 @@
---
interface Props {
title: string;
client: string;
slug: string;
category: 'perenne' | 'event';
heroImage: string;
description: string;
tags?: string[];
index?: number;
}
const { title, client, slug, category, heroImage, description, tags = [], index = 0 } = Astro.props;
const baseUrl = category === 'perenne' ? '/realisations-perennes' : '/creations-evenement';
const href = `${baseUrl}/${slug}`;
const delay = `${index * 100}ms`;
---
<a href={href} class="project-card reveal" style={`transition-delay: ${delay}`}>
<div class="project-card__image img-hover-zoom">
<img
src={heroImage}
alt={title}
loading="lazy"
decoding="async"
/>
<div class="project-card__overlay">
<span class="project-card__cta">Découvrir</span>
</div>
</div>
<div class="project-card__info">
<span class="project-card__client text-light">{client}</span>
<h3 class="project-card__title">{title}</h3>
{tags.length > 0 && (
<div class="project-card__tags">
{tags.slice(0, 3).map(tag => (
<span class="project-card__tag">{tag}</span>
))}
</div>
)}
</div>
</a>
<style>
.project-card {
display: block;
text-decoration: none;
color: inherit;
}
.project-card__image {
position: relative;
aspect-ratio: 4 / 3;
background-color: var(--color-bg-alt);
margin-bottom: var(--space-md);
}
.project-card__image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.project-card__overlay {
position: absolute;
inset: 0;
background: rgba(26, 26, 26, 0);
display: flex;
align-items: center;
justify-content: center;
transition: background var(--duration-normal) var(--ease-out);
}
.project-card__cta {
font-size: 0.85rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: white;
opacity: 0;
transform: translateY(10px);
transition: opacity var(--duration-normal) var(--ease-out),
transform var(--duration-normal) var(--ease-out);
}
.project-card:hover .project-card__overlay {
background: rgba(26, 26, 26, 0.25);
}
.project-card:hover .project-card__cta {
opacity: 1;
transform: translateY(0);
}
.project-card__client {
font-size: 0.75rem;
letter-spacing: 0.08em;
text-transform: uppercase;
display: block;
margin-bottom: var(--space-xs);
}
.project-card__title {
font-family: var(--font-serif);
font-size: 1.2rem;
font-weight: 400;
margin-bottom: var(--space-sm);
}
.project-card__tags {
display: flex;
gap: var(--space-sm);
flex-wrap: wrap;
}
.project-card__tag {
font-size: 0.7rem;
letter-spacing: 0.04em;
color: var(--color-text-light);
padding: 2px 8px;
border: 1px solid var(--color-border);
border-radius: 2px;
}
</style>

View File

@ -0,0 +1,35 @@
---
const year = new Date().getFullYear();
---
<footer class="ft pad">
<div class="ft__row">
<span class="ft__copy">&copy; {year}</span>
<div class="ft__links">
<a href="https://instagram.com/aureliebarre" target="_blank" rel="noopener">instagram</a>
<a href="https://linkedin.com/in/aureliebarre" target="_blank" rel="noopener">linkedin</a>
</div>
</div>
</footer>
<style>
.ft {
padding-top: 3rem;
padding-bottom: 2.5rem;
border-top: 1px solid var(--border);
margin-top: clamp(4rem, 8vw, 8rem);
}
.ft__row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.6rem;
letter-spacing: 0.12em;
color: var(--text-3);
}
.ft__links { display: flex; gap: 2rem; }
.ft__links a { color: var(--text-3); transition: color 0.2s; }
.ft__links a:hover { color: var(--text); }
</style>

View File

@ -0,0 +1,154 @@
---
const currentPath = Astro.url.pathname;
const navItems = [
{ label: 'pérenne', href: '/realisations-perennes' },
{ label: 'événement', href: '/creations-evenement' },
{ label: 'processus', href: '/processus' },
{ label: 'contact', href: 'mailto:contact@aureliebarre.fr' },
];
---
<header class="h">
<a href="/" class="h__logo" aria-label="Accueil">aurélie barré</a>
<nav class="h__nav">
{navItems.map(item => (
<a
href={item.href}
class:list={['h__link', { 'is-on': currentPath.startsWith(item.href) }]}
>
{item.label}
</a>
))}
</nav>
<button class="h__burger" aria-label="Menu" aria-expanded="false">
<span></span><span></span>
</button>
<div class="h__mobile" aria-hidden="true">
<nav>
{navItems.map((item, i) => (
<a href={item.href} class="h__m-link" style={`--i:${i}`}>{item.label}</a>
))}
</nav>
</div>
</header>
<script>
const btn = document.querySelector('.h__burger');
const overlay = document.querySelector('.h__mobile');
const header = document.querySelector('.h');
btn?.addEventListener('click', () => {
const open = btn.getAttribute('aria-expanded') === 'true';
btn.setAttribute('aria-expanded', String(!open));
overlay?.setAttribute('aria-hidden', String(open));
header?.classList.toggle('is-open');
document.body.style.overflow = open ? '' : 'hidden';
});
document.querySelectorAll('.h__m-link').forEach(l => {
l.addEventListener('click', () => {
btn?.setAttribute('aria-expanded', 'false');
overlay?.setAttribute('aria-hidden', 'true');
header?.classList.remove('is-open');
document.body.style.overflow = '';
});
});
let ticking = false;
window.addEventListener('scroll', () => {
if (!ticking) {
requestAnimationFrame(() => {
header?.classList.toggle('is-scrolled', window.scrollY > 40);
ticking = false;
});
ticking = true;
}
});
</script>
<style>
.h {
position: fixed;
top: 0; left: 0; right: 0;
z-index: 100;
height: var(--header-h);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 clamp(2rem, 5vw, 6rem);
transition: background 0.5s var(--ease);
}
.h.is-scrolled {
background: rgba(255,255,255,0.92);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
}
.h__logo {
font-family: var(--sans);
font-size: 0.7rem;
font-weight: 400;
letter-spacing: 0.14em;
text-transform: lowercase;
color: var(--text);
z-index: 101;
}
.h__nav { display: flex; gap: clamp(2rem, 4vw, 4rem); }
.h__link {
font-size: 0.7rem;
font-weight: 400;
letter-spacing: 0.12em;
color: var(--text-3);
transition: color 0.25s;
}
.h__link:hover, .h__link.is-on { color: var(--text); }
.h__burger {
display: none;
flex-direction: column; gap: 5px;
background: none; border: none;
cursor: pointer; padding: 10px;
z-index: 101;
}
.h__burger span {
display: block; width: 20px; height: 1px;
background: var(--text);
transition: transform 0.4s var(--ease), opacity 0.2s;
}
.is-open .h__burger span:first-child { transform: translateY(3px) rotate(45deg); }
.is-open .h__burger span:last-child { transform: translateY(-3px) rotate(-45deg); }
.h__mobile {
position: fixed; inset: 0;
background: #fff;
display: flex; align-items: center; justify-content: center;
opacity: 0; visibility: hidden;
transition: opacity 0.4s var(--ease), visibility 0.4s;
z-index: 99;
}
.is-open .h__mobile { opacity: 1; visibility: visible; }
.h__mobile nav { display: flex; flex-direction: column; align-items: center; gap: 1.8rem; }
.h__m-link {
font-size: 0.75rem;
letter-spacing: 0.14em;
color: var(--text);
opacity: 0; transform: translateY(15px);
transition: opacity 0.5s var(--ease), transform 0.5s var(--ease);
transition-delay: calc(var(--i) * 60ms + 80ms);
}
.is-open .h__m-link { opacity: 1; transform: none; }
@media (max-width: 768px) {
.h__nav { display: none; }
.h__burger { display: flex; }
}
</style>

View File

@ -0,0 +1,142 @@
import { useState, useCallback, useEffect } from 'react';
import { motion, AnimatePresence } from 'motion/react';
interface GalleryImage {
src: string;
alt: string;
size?: 'small' | 'medium' | 'large';
}
interface Props {
images: GalleryImage[];
}
export default function MasonryGallery({ images }: Props) {
const [sel, setSel] = useState<number | null>(null);
const [cols, setCols] = useState(3);
useEffect(() => {
const u = () => setCols(window.innerWidth < 640 ? 1 : window.innerWidth < 1024 ? 2 : 3);
u();
window.addEventListener('resize', u);
return () => window.removeEventListener('resize', u);
}, []);
const getCols = useCallback(() => {
const c: GalleryImage[][] = Array.from({ length: cols }, () => []);
images.forEach((img, i) => c[i % cols].push(img));
return c;
}, [images, cols]);
useEffect(() => {
const fn = (e: KeyboardEvent) => {
if (sel === null) return;
if (e.key === 'Escape') setSel(null);
if (e.key === 'ArrowRight' && sel < images.length - 1) setSel(sel + 1);
if (e.key === 'ArrowLeft' && sel > 0) setSel(sel - 1);
};
window.addEventListener('keydown', fn);
return () => window.removeEventListener('keydown', fn);
}, [sel, images.length]);
useEffect(() => {
document.body.style.overflow = sel !== null ? 'hidden' : '';
return () => { document.body.style.overflow = ''; };
}, [sel]);
const flat = (ci: number, ri: number) => ri * cols + ci;
return (
<>
<div style={{ display: 'grid', gridTemplateColumns: `repeat(${cols}, 1fr)`, gap: '4px' }}>
{getCols().map((col, ci) => (
<div key={ci} style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
{col.map((img, ri) => {
const idx = flat(ci, ri);
return (
<motion.div
key={idx}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: idx * 0.03, duration: 0.5 }}
onClick={() => setSel(idx)}
style={{ cursor: 'pointer', overflow: 'hidden', lineHeight: 0 }}
>
<img
src={img.src} alt={img.alt}
loading="lazy" decoding="async"
style={{
width: '100%', height: 'auto', display: 'block',
transition: 'transform 0.6s cubic-bezier(0.16,1,0.3,1)',
}}
onMouseEnter={e => (e.currentTarget.style.transform = 'scale(1.015)')}
onMouseLeave={e => (e.currentTarget.style.transform = '')}
/>
</motion.div>
);
})}
</div>
))}
</div>
<AnimatePresence>
{sel !== null && (
<motion.div
initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
onClick={() => setSel(null)}
style={{
position: 'fixed', inset: 0, zIndex: 200,
background: 'rgba(255,255,255,0.96)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
cursor: 'pointer',
}}
>
<button onClick={() => setSel(null)} aria-label="Fermer"
style={{ position: 'absolute', top: '1.5rem', right: '1.5rem', background: 'none', border: 'none', cursor: 'pointer', padding: 8, zIndex: 201 }}>
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" stroke="#AAA" strokeWidth="1">
<line x1="3" y1="3" x2="15" y2="15"/><line x1="15" y1="3" x2="3" y2="15"/>
</svg>
</button>
<span style={{
position: 'absolute', top: '1.7rem', left: '50%', transform: 'translateX(-50%)',
color: '#CCC', fontSize: '0.6rem', letterSpacing: '0.12em', zIndex: 201,
}}>
{sel + 1} / {images.length}
</span>
{sel > 0 && (
<button onClick={e => { e.stopPropagation(); setSel(sel - 1); }} aria-label="Précédent"
style={{ ...arr, left: '1.5rem' }}>
<svg width="16" height="16" viewBox="0 0 20 20" fill="none" stroke="#AAA" strokeWidth="1"><polyline points="13,4 7,10 13,16"/></svg>
</button>
)}
{sel < images.length - 1 && (
<button onClick={e => { e.stopPropagation(); setSel(sel + 1); }} aria-label="Suivant"
style={{ ...arr, right: '1.5rem' }}>
<svg width="16" height="16" viewBox="0 0 20 20" fill="none" stroke="#AAA" strokeWidth="1"><polyline points="7,4 13,10 7,16"/></svg>
</button>
)}
<motion.img
key={sel}
src={images[sel].src} alt={images[sel].alt}
initial={{ opacity: 0, scale: 0.98 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
onClick={e => e.stopPropagation()}
style={{ maxWidth: '85vw', maxHeight: '85vh', objectFit: 'contain', cursor: 'default' }}
/>
</motion.div>
)}
</AnimatePresence>
</>
);
}
const arr: React.CSSProperties = {
position: 'absolute', top: '50%', transform: 'translateY(-50%)',
background: 'none', border: 'none', cursor: 'pointer', padding: 10, zIndex: 201,
};

View File

@ -0,0 +1,98 @@
import { useRef } from 'react';
import { motion, useInView } from 'motion/react';
interface ProcessStep {
title: string;
stepNumber: number;
subtitle: string;
description: string;
duration?: string;
}
interface Props {
steps: ProcessStep[];
}
function Step({ step, index }: { step: ProcessStep; index: number }) {
const ref = useRef<HTMLDivElement>(null);
const isInView = useInView(ref, { once: true, margin: '-80px' });
return (
<motion.div
ref={ref}
initial={{ opacity: 0, y: 40 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.8, delay: 0.05, ease: [0.16, 1, 0.3, 1] }}
style={rowStyle}
>
<span style={numStyle}>{String(step.stepNumber).padStart(2, '0')}</span>
<div style={contentStyle}>
<span style={subtitleStyle}>{step.subtitle.toLowerCase()}</span>
<h3 style={titleStyle}>{step.title}</h3>
<p style={descStyle}>{step.description}</p>
{step.duration && <span style={durStyle}>{step.duration}</span>}
</div>
</motion.div>
);
}
export default function ProcessTimeline({ steps }: Props) {
return (
<div style={{ display: 'flex', flexDirection: 'column' }}>
{steps.map((step, i) => (
<Step key={step.stepNumber} step={step} index={i} />
))}
</div>
);
}
const rowStyle: React.CSSProperties = {
display: 'grid',
gridTemplateColumns: '3.5rem 1fr',
gap: '2rem',
alignItems: 'flex-start',
padding: '2.5rem 0',
borderBottom: '1px solid #E5E5E5',
};
const numStyle: React.CSSProperties = {
fontFamily: "'DM Serif Display', Georgia, serif",
fontSize: '0.85rem',
fontStyle: 'italic',
color: '#AAA',
paddingTop: '0.15rem',
};
const contentStyle: React.CSSProperties = {};
const subtitleStyle: React.CSSProperties = {
fontSize: '0.6rem',
letterSpacing: '0.14em',
color: '#AAA',
display: 'block',
marginBottom: '0.5rem',
};
const titleStyle: React.CSSProperties = {
fontFamily: "'DM Serif Display', Georgia, serif",
fontSize: 'clamp(1.3rem, 2.5vw, 1.8rem)',
fontWeight: 400,
lineHeight: 1.3,
marginBottom: '0.8rem',
color: '#1A1A1A',
};
const descStyle: React.CSSProperties = {
fontSize: '0.9rem',
lineHeight: 1.7,
color: '#777',
maxWidth: '480px',
};
const durStyle: React.CSSProperties = {
display: 'inline-block',
marginTop: '0.8rem',
fontSize: '0.6rem',
letterSpacing: '0.1em',
color: '#AAA',
};

View File

@ -6,13 +6,11 @@ import '../styles/global.css';
interface Props {
title?: string;
description?: string;
image?: string;
}
const {
title = 'Aurélie Barré — Design d\'intérieur & Création événementielle',
description = 'Portfolio d\'Aurélie Barré, designeuse d\'intérieur et directrice artistique spécialisée en design d\'expérience et création événementielle.',
image = '/og-image.jpg'
title = 'Aurélie Barré — Directrice artistique',
description = 'Aurélie Barré, directrice artistique & designeuse d\'intérieur. Design d\'expérience, scénographie événementielle, aménagement pérenne.',
} = Astro.props;
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
@ -25,15 +23,11 @@ const canonicalURL = new URL(Astro.url.pathname, Astro.site);
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content={description} />
<link rel="canonical" href={canonicalURL} />
<!-- Open Graph -->
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={image} />
<meta property="og:url" content={canonicalURL} />
<meta property="og:type" content="website" />
<!-- Fonts: DM Serif Display + Inter -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=Inter:wght@300;400;500&display=swap" rel="stylesheet" />
@ -43,37 +37,17 @@ const canonicalURL = new URL(Astro.url.pathname, Astro.site);
</head>
<body>
<Header />
<main class="page-enter">
<main>
<slot />
</main>
<Footer />
<script>
// Intersection Observer for scroll reveal animations
const observerCallback: IntersectionObserverCallback = (entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
}
});
};
const observer = new IntersectionObserver(observerCallback, {
threshold: 0.1,
rootMargin: '0px 0px -50px 0px'
});
document.querySelectorAll('.reveal').forEach((el) => {
observer.observe(el);
});
// Reveal on scroll
const io = new IntersectionObserver((entries) => {
entries.forEach(e => { if (e.isIntersecting) e.target.classList.add('vis'); });
}, { threshold: 0.08, rootMargin: '0px 0px -40px 0px' });
document.querySelectorAll('.rv').forEach(el => io.observe(el));
</script>
</body>
</html>
<style>
main {
min-height: calc(100vh - var(--header-height));
}
</style>

View File

@ -0,0 +1,52 @@
---
import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';
import '../styles/global.css';
interface Props {
title?: string;
description?: string;
}
const {
title = 'Aurélie Barré — Design d\'intérieur & Création événementielle',
description = 'Direction artistique, design d\'espace et scénographie événementielle.',
} = Astro.props;
---
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content={description} />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=Inter:wght@300;400;500&display=swap" rel="stylesheet" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>{title}</title>
</head>
<body>
<Header />
<main><slot /></main>
<Footer />
<script>
// Reveal on scroll
const obs = new IntersectionObserver((entries) => {
entries.forEach(e => {
if (e.isIntersecting) {
const delay = (e.target as HTMLElement).style.getPropertyValue('--d');
if (delay) {
setTimeout(() => e.target.classList.add('vis'), parseInt(delay));
} else {
e.target.classList.add('vis');
}
}
});
}, { threshold: 0.08, rootMargin: '0px 0px -40px 0px' });
document.querySelectorAll('.rv').forEach(el => obs.observe(el));
</script>
</body>
</html>

View File

@ -1,234 +1,186 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import MasonryGallery from '../../components/MasonryGallery.tsx';
import MasonryGallery from '../../components/MasonryGallery.astro';
import { getMockProjects, getMockProjectBySlug } from '../../lib/mock-data';
export function getStaticPaths() {
const projects = getMockProjects('event');
return projects.map(p => ({ params: { slug: p.slug.current } }));
return getMockProjects('event').map(p => ({ params: { slug: p.slug.current } }));
}
const { slug } = Astro.params;
const project = getMockProjectBySlug(slug!);
if (!project) return Astro.redirect('/creations-evenement');
if (!project) {
return Astro.redirect('/creations-evenement');
}
const dateFormatted = new Date(project.date).toLocaleDateString('fr-FR', {
year: 'numeric',
month: 'long',
});
const date = new Date(project.date).toLocaleDateString('fr-FR', { year: 'numeric', month: 'long' });
---
<BaseLayout title={`${project.title} — Aurélie Barré`} description={project.description}>
<!-- Hero Image -->
<section class="project-hero">
<div class="project-hero__image">
<!-- Full-bleed hero -->
<section class="phero">
<div class="phero__img">
<img src={project.heroImage} alt={project.title} />
<div class="project-hero__overlay"></div>
</div>
<div class="project-hero__content container">
<span class="project-hero__client">{project.client}</span>
<h1>{project.title}</h1>
</div>
<div class="phero__overlay"></div>
</section>
<!-- Project Info -->
<section class="section">
<div class="container">
<div class="project-meta reveal">
<div class="project-meta__item">
<span class="project-meta__label">Client</span>
<span class="project-meta__value">{project.client}</span>
</div>
<div class="project-meta__item">
<span class="project-meta__label">Date</span>
<span class="project-meta__value">{dateFormatted}</span>
</div>
{project.location && (
<div class="project-meta__item">
<span class="project-meta__label">Lieu</span>
<span class="project-meta__value">{project.location}</span>
<!-- Info -->
<section class="pinfo">
<div class="wrap">
<div class="pinfo__top rv">
<h1 class="pinfo__title">{project.title}</h1>
<div class="pinfo__meta">
<div class="pinfo__col">
<span class="label">client</span>
<span class="pinfo__val">{project.client}</span>
</div>
)}
<div class="project-meta__item">
<span class="project-meta__label">Type</span>
<span class="project-meta__value">Création événementielle</span>
<div class="pinfo__col">
<span class="label">date</span>
<span class="pinfo__val">{date}</span>
</div>
{project.location && (
<div class="pinfo__col">
<span class="label">lieu</span>
<span class="pinfo__val">{project.location}</span>
</div>
)}
</div>
</div>
<div class="project-description reveal">
<p>{project.description}</p>
</div>
{project.tags && project.tags.length > 0 && (
<div class="project-tags reveal">
{project.tags.map(tag => (
<span class="project-tag">{tag}</span>
))}
</div>
)}
<p class="pinfo__desc rv" style="--d:2">{project.description}</p>
</div>
</section>
<!-- Gallery (Masonry) -->
<!-- Gallery -->
{project.gallery && project.gallery.length > 0 && (
<section class="section" style="background-color: var(--color-bg-alt);">
<div class="container">
<section class="pgallery">
<div class="wrap">
<MasonryGallery
client:visible
images={project.gallery.map(img => ({
src: img.src,
alt: img.alt,
size: img.size,
}))}
images={project.gallery.map(img => ({ src: img.src, alt: img.alt }))}
/>
</div>
</section>
)}
<!-- Back link -->
<section class="section">
<div class="container">
<a href="/creations-evenement" class="back-link">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" style="margin-right: 8px;">
<path d="M13 8H3M3 8L7 4M3 8L7 12" stroke="currentColor" stroke-width="1.2"/>
<!-- Nav -->
<section class="pnav">
<div class="wrap">
<a href="/creations-evenement" class="pnav__link">
<svg width="14" height="14" viewBox="0 0 16 16" fill="none">
<path d="M13 8H3M3 8L7 4M3 8L7 12" stroke="currentColor" stroke-width="1"/>
</svg>
Retour aux créations événement
retour
</a>
</div>
</section>
</BaseLayout>
<style>
/* Hero */
.project-hero {
.phero {
position: relative;
height: 70vh;
height: 75vh;
min-height: 500px;
display: flex;
align-items: flex-end;
overflow: hidden;
}
.project-hero__image {
.phero__img {
position: absolute;
inset: 0;
}
.project-hero__image img {
.phero__img img {
width: 100%;
height: 100%;
object-fit: cover;
}
.project-hero__overlay {
.phero__overlay {
position: absolute;
inset: 0;
background: linear-gradient(
to top,
rgba(0, 0, 0, 0.5) 0%,
rgba(0, 0, 0, 0.1) 50%,
rgba(0, 0, 0, 0) 100%
);
background: linear-gradient(to bottom, rgba(0,0,0,0.08) 0%, rgba(0,0,0,0.02) 100%);
}
.project-hero__content {
position: relative;
z-index: 1;
padding-bottom: var(--space-2xl);
color: white;
/* Info */
.pinfo {
padding: 4rem 0 3rem;
}
.project-hero__client {
font-size: 0.75rem;
letter-spacing: 0.12em;
text-transform: uppercase;
opacity: 0.7;
display: block;
margin-bottom: var(--space-sm);
}
.project-hero__content h1 {
color: white;
font-size: clamp(2rem, 5vw, 3.5rem);
}
/* Meta */
.project-meta {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--space-lg);
padding-bottom: var(--space-xl);
border-bottom: 1px solid var(--color-border);
margin-bottom: var(--space-xl);
}
.project-meta__label {
font-size: 0.7rem;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--color-text-light);
display: block;
margin-bottom: var(--space-xs);
}
.project-meta__value {
font-size: 0.95rem;
color: var(--color-text);
}
/* Description */
.project-description {
margin-bottom: var(--space-xl);
}
.project-description p {
font-size: 1.15rem;
line-height: 1.8;
color: var(--color-text-secondary);
max-width: 700px;
}
/* Tags */
.project-tags {
.pinfo__top {
display: flex;
gap: var(--space-sm);
flex-wrap: wrap;
justify-content: space-between;
align-items: flex-start;
gap: 2rem;
padding-bottom: 2.5rem;
border-bottom: 1px solid var(--border-light);
margin-bottom: 2.5rem;
}
.project-tag {
font-size: 0.75rem;
letter-spacing: 0.04em;
color: var(--color-text-light);
padding: 4px 12px;
border: 1px solid var(--color-border);
border-radius: 2px;
.pinfo__title {
font-size: clamp(1.8rem, 4vw, 3rem);
max-width: 55%;
line-height: 1.1;
}
/* Back link */
.back-link {
.pinfo__meta {
display: flex;
gap: 2.5rem;
flex-shrink: 0;
}
.pinfo__col {
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.pinfo__val {
font-size: 0.85rem;
color: var(--text);
}
.pinfo__desc {
font-size: 1.05rem;
line-height: 1.75;
color: var(--text-2);
max-width: 650px;
}
/* Gallery */
.pgallery {
padding: 2rem 0 6rem;
}
/* Nav */
.pnav {
padding: 0 0 5rem;
}
.pnav__link {
display: inline-flex;
align-items: center;
font-size: 0.85rem;
letter-spacing: 0.04em;
color: var(--color-text-secondary);
gap: 0.5rem;
font-size: 0.7rem;
letter-spacing: 0.08em;
color: var(--text-3);
text-decoration: none;
transition: color var(--duration-fast);
transition: color 0.2s;
}
.back-link:hover {
color: var(--color-accent);
}
.pnav__link:hover { color: var(--text); }
@media (max-width: 768px) {
.project-meta {
grid-template-columns: repeat(2, 1fr);
.phero { height: 55vh; }
.pinfo__top {
flex-direction: column;
}
.project-hero {
height: 50vh;
.pinfo__title {
max-width: 100%;
}
.pinfo__meta {
flex-wrap: wrap;
gap: 1.5rem;
}
}
</style>

View File

@ -1,81 +1,148 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import ProjectCard from '../../components/ProjectCard.astro';
import { getMockProjects } from '../../lib/mock-data';
const projects = getMockProjects('event');
---
<BaseLayout title="Créations Événement — Aurélie Barré" description="Découvrez les créations événementielles d'Aurélie Barré : scénographies, pop-ups et événements pour les plus grandes maisons.">
<section class="page-hero">
<div class="container">
<span class="page-hero__label">Portfolio</span>
<h1>Créations<br /><em>Événement</em></h1>
<p class="page-hero__desc text-secondary">
Scénographies éphémères, pop-ups, lancements, conventions —
chaque événement est une expérience unique conçue sur mesure.
</p>
</div>
</section>
<BaseLayout title="Créations événement — Aurélie Barré">
<section class="listing">
<div class="wrap">
<div class="listing__head rv">
<span class="label">événement</span>
<h1>créations<br />événement</h1>
</div>
<section class="section">
<div class="container">
<div class="projects-grid">
{projects.map((project, index) => (
<ProjectCard
title={project.title}
client={project.client}
slug={project.slug.current}
category={project.category}
heroImage={project.heroImage}
description={project.description}
tags={project.tags}
index={index}
/>
))}
<div class="listing__grid">
{projects.map((project, i) => {
const href = `/creations-evenement/${project.slug.current}`;
// Every 3rd item is full-width
const isFull = i % 3 === 0;
return (
<a
href={href}
class:list={['listing__item', 'rv', { 'is-full': isFull }]}
style={`--d:${(i % 4) + 1}`}
>
<div class="listing__img">
<img src={project.heroImage} alt={project.title} loading="lazy" />
</div>
<div class="listing__meta">
<span class="listing__client">{project.client}</span>
<h2 class="listing__title">{project.title}</h2>
<span class="listing__year">{project.date.slice(0, 4)}</span>
</div>
</a>
);
})}
</div>
</div>
</section>
</BaseLayout>
<style>
.page-hero {
padding-top: calc(var(--header-height) + var(--space-3xl));
padding-bottom: var(--space-xl);
.listing {
padding-top: calc(var(--header-h) + 6rem);
padding-bottom: 6rem;
}
.page-hero__label {
font-size: 0.75rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--color-text-light);
.listing__head {
margin-bottom: 4rem;
}
.listing__head .label {
display: block;
margin-bottom: var(--space-md);
margin-bottom: 1rem;
}
.page-hero h1 {
margin-bottom: var(--space-lg);
.listing__head h1 {
font-size: clamp(2.2rem, 5vw, 4rem);
text-transform: lowercase;
line-height: 1.05;
}
.page-hero h1 em {
color: var(--color-accent);
}
.page-hero__desc {
font-size: 1.1rem;
line-height: 1.8;
max-width: 600px;
}
.projects-grid {
/* Asymmetric grid */
.listing__grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--space-xl) var(--space-lg);
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.listing__item {
text-decoration: none;
color: inherit;
overflow: hidden;
}
.listing__item.is-full {
grid-column: 1 / -1;
}
.listing__img {
width: 100%;
overflow: hidden;
}
/* Full-width: wide landscape. Half-width: tall portrait */
.listing__item.is-full .listing__img {
aspect-ratio: 2.4 / 1;
}
.listing__item:not(.is-full) .listing__img {
aspect-ratio: 3 / 4;
}
.listing__img img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.8s var(--ease);
}
.listing__item:hover .listing__img img {
transform: scale(1.03);
}
/* Dim siblings */
.listing__grid:hover .listing__item:not(:hover) .listing__img img {
filter: brightness(0.88);
transition: filter 0.4s;
}
.listing__meta {
padding: 0.8rem 0 1.5rem;
}
.listing__client {
font-size: 0.6rem;
letter-spacing: 0.1em;
color: var(--text-3);
text-transform: lowercase;
display: block;
margin-bottom: 0.2rem;
}
.listing__title {
font-size: clamp(0.95rem, 1.5vw, 1.2rem);
font-weight: 400;
line-height: 1.3;
display: inline;
}
.listing__year {
font-size: 0.65rem;
color: var(--text-3);
margin-left: 0.75rem;
font-variant-numeric: tabular-nums;
}
@media (max-width: 768px) {
.projects-grid {
.listing__grid {
grid-template-columns: 1fr;
}
.listing__item:not(.is-full) .listing__img {
aspect-ratio: 4 / 3;
}
}
</style>

View File

@ -1,249 +1,494 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
import ProjectCard from '../components/ProjectCard.astro';
import { getMockFeaturedProjects } from '../lib/mock-data';
import { getMockFeaturedProjects, getMockProjects } from '../lib/mock-data';
const featured = getMockFeaturedProjects();
const events = featured.filter(p => p.category === 'event');
const perennes = featured.filter(p => p.category === 'perenne');
const allProjects = getMockProjects();
// Pick 6 best projects for the index
const showcase = [
allProjects.find(p => p.slug.current === 'moet-chandon'),
allProjects.find(p => p.slug.current === 'dior-christmas'),
allProjects.find(p => p.slug.current === 'alula'),
allProjects.find(p => p.slug.current === 'lvmh-metaverse'),
allProjects.find(p => p.slug.current === 'cisco-siege'),
allProjects.find(p => p.slug.current === 'ami'),
].filter(Boolean);
const allEvents = getMockProjects('event');
const allPerennes = getMockProjects('perenne');
---
<BaseLayout>
<!-- Hero Section -->
<!-- ====== HERO: full-viewport image with name overlay ====== -->
<section class="hero">
<div class="container">
<div class="hero__content">
<h1 class="hero__title">
<span class="hero__title-line">Design</span>
<span class="hero__title-line hero__title-accent">d'expérience</span>
</h1>
<p class="hero__subtitle">
Aurélie Barré — Directrice artistique & designeuse d'intérieur.<br />
Du concept à la réalisation, chaque espace raconte une histoire.
</p>
<div class="hero__cta">
<a href="/processus" class="hero__link">
Découvrir mon processus
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" style="margin-left: 8px;">
<path d="M3 8H13M13 8L9 4M13 8L9 12" stroke="currentColor" stroke-width="1.2"/>
</svg>
</a>
</div>
<div class="hero__img">
<img src="/images/events/dior/01.jpeg" alt="Dior Christmas" />
</div>
<div class="hero__content">
<h1 class="hero__name">
<span class="hero__line">Aurélie</span>
<span class="hero__line">Barré</span>
</h1>
<p class="hero__role">directrice artistique & design d'intérieur</p>
</div>
<div class="hero__scroll">
<span>scroll</span>
<div class="hero__scroll-line"></div>
</div>
</section>
<!-- ====== FEATURED: hover-reveal image grid ====== -->
<section class="feat">
<div class="wrap">
<span class="label rv">sélection</span>
<div class="feat__grid">
{showcase.map((project, i) => {
const base = project!.category === 'perenne' ? '/realisations-perennes' : '/creations-evenement';
return (
<a
href={`${base}/${project!.slug.current}`}
class="feat__item rv"
style={`--d:${i + 1}`}
>
<div class="feat__img">
<img src={project!.heroImage} alt={project!.title} loading="lazy" />
</div>
<div class="feat__info">
<span class="feat__client">{project!.client}</span>
<h2 class="feat__title">{project!.title}</h2>
</div>
</a>
);
})}
</div>
</div>
</section>
<!-- Featured Events -->
<section class="section">
<div class="container">
<div class="section-header reveal">
<span class="section-label">Sélection</span>
<h2>Créations Événement</h2>
<a href="/creations-evenement" class="section-link">
Voir tous les projets
<!-- ====== PROJECT INDEX: text list with hover image ====== -->
<section class="idx">
<div class="wrap">
<div class="idx__head">
<span class="label rv">tous les projets</span>
</div>
<div class="idx__list" id="project-index">
{allEvents.map((p, i) => {
const href = `/creations-evenement/${p.slug.current}`;
return (
<a
href={href}
class="idx__row rv"
style={`--d:${i % 6}`}
data-img={p.heroImage}
>
<span class="idx__num">{String(i + 1).padStart(2, '0')}</span>
<span class="idx__name">{p.title}</span>
<span class="idx__client">{p.client}</span>
<span class="idx__year">{p.date.slice(0, 4)}</span>
</a>
);
})}
</div>
<div class="idx__sep rv"></div>
<div class="idx__list">
{allPerennes.map((p, i) => {
const href = `/realisations-perennes/${p.slug.current}`;
return (
<a
href={href}
class="idx__row rv"
style={`--d:${i % 6}`}
data-img={p.heroImage}
>
<span class="idx__num">{String(i + 1).padStart(2, '0')}</span>
<span class="idx__name">{p.title}</span>
<span class="idx__client">{p.client}</span>
<span class="idx__year">{p.date.slice(0, 4)}</span>
</a>
);
})}
</div>
</div>
</section>
<!-- ====== ABOUT TEASER ====== -->
<section class="about">
<div class="wrap">
<div class="about__inner rv">
<p class="about__text">
Aurélie Barré conçoit des espaces qui racontent. Du siège social au lancement produit,
chaque projet naît d'une écoute attentive et se construit dans le souci du détail.
</p>
<a href="/processus" class="about__link">
découvrir le processus
<svg width="14" height="14" viewBox="0 0 16 16" fill="none">
<path d="M3 8H13M13 8L9 4M13 8L9 12" stroke="currentColor" stroke-width="1.2"/>
</svg>
</a>
</div>
<div class="projects-grid">
{events.slice(0, 4).map((project, index) => (
<ProjectCard
title={project.title}
client={project.client}
slug={project.slug.current}
category={project.category}
heroImage={project.heroImage}
description={project.description}
tags={project.tags}
index={index}
/>
))}
</div>
</div>
</section>
<!-- Featured Pérennes -->
{perennes.length > 0 && (
<section class="section" style="background-color: var(--color-bg-alt);">
<div class="container">
<div class="section-header reveal">
<span class="section-label">Sélection</span>
<h2>Réalisations Pérennes</h2>
<a href="/realisations-perennes" class="section-link">
Voir tous les projets
<svg width="14" height="14" viewBox="0 0 16 16" fill="none">
<path d="M3 8H13M13 8L9 4M13 8L9 12" stroke="currentColor" stroke-width="1.2"/>
</svg>
</a>
</div>
<div class="projects-grid">
{perennes.map((project, index) => (
<ProjectCard
title={project.title}
client={project.client}
slug={project.slug.current}
category={project.category}
heroImage={project.heroImage}
description={project.description}
tags={project.tags}
index={index}
/>
))}
</div>
</div>
</section>
)}
<!-- Process Teaser -->
<section class="section process-teaser">
<div class="container">
<div class="process-teaser__inner reveal">
<span class="section-label">Processus</span>
<h2>Comment je travaille</h2>
<p class="process-teaser__text text-secondary">
De la prise de contact à la livraison, chaque projet suit un processus créatif rigoureux
qui garantit un résultat à la hauteur de vos ambitions.
</p>
<div class="process-teaser__steps">
<span class="process-teaser__step">01 — Prise de contact</span>
<span class="process-teaser__step">02 — Conception & Moodboard</span>
<span class="process-teaser__step">03 — Sélection & Sourcing</span>
<span class="process-teaser__step">04 — Réalisation</span>
<span class="process-teaser__step">05 — Livraison & Retour</span>
</div>
<a href="/processus" class="hero__link" style="margin-top: 2rem; display: inline-flex;">
Découvrir en détail
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" style="margin-left: 8px;">
<path d="M3 8H13M13 8L9 4M13 8L9 12" stroke="currentColor" stroke-width="1.2"/>
<path d="M3 8H13M13 8L9 4M13 8L9 12" stroke="currentColor" stroke-width="1"/>
</svg>
</a>
</div>
</div>
</section>
<!-- Hover image follower -->
<div class="cursor-img" id="cursor-img">
<img src="" alt="" id="cursor-img-el" />
</div>
</BaseLayout>
<script>
// Project index: image follows cursor on hover
const cursorWrap = document.getElementById('cursor-img') as HTMLDivElement;
const cursorImg = document.getElementById('cursor-img-el') as HTMLImageElement;
const rows = document.querySelectorAll<HTMLElement>('.idx__row[data-img]');
let isVisible = false;
let mouseX = 0, mouseY = 0;
let currentX = 0, currentY = 0;
function lerp(a: number, b: number, t: number) { return a + (b - a) * t; }
function animate() {
currentX = lerp(currentX, mouseX, 0.1);
currentY = lerp(currentY, mouseY, 0.1);
if (isVisible) {
cursorWrap.style.transform = `translate(${currentX}px, ${currentY}px)`;
}
requestAnimationFrame(animate);
}
animate();
rows.forEach(row => {
row.addEventListener('mouseenter', () => {
const src = row.dataset.img;
if (src && cursorImg) {
cursorImg.src = src;
cursorWrap.classList.add('is-visible');
isVisible = true;
}
});
row.addEventListener('mouseleave', () => {
cursorWrap.classList.remove('is-visible');
isVisible = false;
});
row.addEventListener('mousemove', (e) => {
mouseX = e.clientX - 160;
mouseY = e.clientY - 110;
});
});
</script>
<style>
/* Hero */
/* ---- Hero ---- */
.hero {
min-height: 90vh;
display: flex;
align-items: center;
padding-top: var(--header-height);
position: relative;
height: 100vh;
overflow: hidden;
}
.hero__img {
position: absolute;
inset: 0;
}
.hero__img img {
width: 100%;
height: 100%;
object-fit: cover;
filter: brightness(0.7);
}
.hero__content {
max-width: 800px;
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 0 var(--pad) 8vh;
z-index: 2;
}
.hero__title {
margin-bottom: var(--space-lg);
.hero__name {
font-size: clamp(3.5rem, 10vw, 9rem);
line-height: 0.92;
color: #fff;
letter-spacing: -0.03em;
margin-bottom: 1.5rem;
}
.hero__title-line {
.hero__line {
display: block;
}
.hero__title-accent {
font-style: italic;
color: var(--color-accent);
}
.hero__subtitle {
font-size: clamp(1rem, 2vw, 1.2rem);
color: var(--color-text-secondary);
line-height: 1.8;
margin-bottom: var(--space-xl);
}
.hero__link {
display: inline-flex;
align-items: center;
font-size: 0.85rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--color-accent);
text-decoration: none;
transition: color var(--duration-fast) var(--ease-out);
}
.hero__link:hover {
color: var(--color-accent-hover);
}
/* Section Headers */
.section-header {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: var(--space-md);
margin-bottom: var(--space-xl);
}
.section-label {
font-size: 0.75rem;
.hero__role {
font-family: var(--sans);
font-size: 0.7rem;
font-weight: 300;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--color-text-light);
width: 100%;
color: rgba(255,255,255,0.65);
max-width: none;
}
.section-header h2 {
margin-right: auto;
}
.section-link {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.85rem;
letter-spacing: 0.04em;
color: var(--color-accent);
text-decoration: none;
}
.section-link:hover {
color: var(--color-accent-hover);
}
/* Projects Grid */
.projects-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--space-xl) var(--space-lg);
}
@media (max-width: 768px) {
.projects-grid {
grid-template-columns: 1fr;
}
}
/* Process Teaser */
.process-teaser__inner {
max-width: 700px;
}
.process-teaser__text {
font-size: 1.1rem;
line-height: 1.8;
margin-top: var(--space-md);
margin-bottom: var(--space-xl);
}
.process-teaser__steps {
.hero__scroll {
position: absolute;
bottom: 3vh;
right: var(--pad);
display: flex;
flex-direction: column;
gap: var(--space-md);
align-items: center;
gap: 8px;
z-index: 2;
}
.process-teaser__step {
font-family: var(--font-serif);
.hero__scroll span {
font-size: 0.55rem;
letter-spacing: 0.2em;
color: rgba(255,255,255,0.45);
writing-mode: vertical-rl;
}
.hero__scroll-line {
width: 1px;
height: 40px;
background: rgba(255,255,255,0.25);
position: relative;
overflow: hidden;
}
.hero__scroll-line::after {
content: '';
position: absolute;
top: -100%;
left: 0;
width: 100%;
height: 100%;
background: rgba(255,255,255,0.8);
animation: scrollLine 2s ease-in-out infinite;
}
@keyframes scrollLine {
0% { top: -100%; }
50% { top: 100%; }
100% { top: 100%; }
}
/* ---- Featured Grid ---- */
.feat {
padding: 10rem 0 6rem;
}
.feat .label {
display: block;
margin-bottom: 3rem;
}
.feat__grid {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: 1rem;
}
.feat__item {
position: relative;
overflow: hidden;
text-decoration: none;
color: inherit;
}
/* Asymmetric placement — each item gets its own span */
.feat__item:nth-child(1) { grid-column: 1 / 8; }
.feat__item:nth-child(2) { grid-column: 8 / 13; }
.feat__item:nth-child(3) { grid-column: 1 / 5; }
.feat__item:nth-child(4) { grid-column: 5 / 13; }
.feat__item:nth-child(5) { grid-column: 1 / 7; }
.feat__item:nth-child(6) { grid-column: 7 / 13; }
.feat__img {
width: 100%;
aspect-ratio: 3 / 2;
overflow: hidden;
}
.feat__item:nth-child(2) .feat__img,
.feat__item:nth-child(3) .feat__img { aspect-ratio: 3 / 4; }
.feat__img img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.8s var(--ease), filter 0.5s;
}
.feat__item:hover .feat__img img {
transform: scale(1.04);
}
/* Dim siblings on any hover */
.feat__grid:hover .feat__item:not(:hover) .feat__img img {
filter: brightness(0.85);
}
.feat__info {
padding: 1rem 0 1.5rem;
}
.feat__client {
font-size: 0.6rem;
letter-spacing: 0.12em;
color: var(--text-3);
text-transform: lowercase;
display: block;
margin-bottom: 0.3rem;
}
.feat__title {
font-size: clamp(1rem, 2vw, 1.4rem);
line-height: 1.2;
font-weight: 400;
}
/* ---- Project Index ---- */
.idx {
padding: 4rem 0 8rem;
}
.idx__head {
margin-bottom: 2rem;
}
.idx__sep {
height: 1px;
background: var(--border);
margin: 0.5rem 0;
}
.idx__row {
display: grid;
grid-template-columns: 3rem 1fr 1fr 4rem;
gap: 1rem;
align-items: center;
padding: 0.9rem 0;
border-bottom: 1px solid var(--border-light);
text-decoration: none;
color: inherit;
transition: padding-left 0.4s var(--ease), color 0.2s;
}
.idx__row:first-child {
border-top: 1px solid var(--border-light);
}
.idx__row:hover {
padding-left: 1rem;
}
.idx__num {
font-size: 0.65rem;
color: var(--text-3);
font-variant-numeric: tabular-nums;
}
.idx__name {
font-family: var(--serif);
font-size: 1.1rem;
color: var(--color-text-secondary);
padding-bottom: var(--space-md);
border-bottom: 1px solid var(--color-border);
line-height: 1.3;
}
.idx__client {
font-size: 0.75rem;
color: var(--text-2);
}
.idx__year {
font-size: 0.7rem;
color: var(--text-3);
text-align: right;
font-variant-numeric: tabular-nums;
}
/* ---- Cursor image follower ---- */
.cursor-img {
position: fixed;
top: 0;
left: 0;
width: 320px;
height: 220px;
pointer-events: none;
z-index: 50;
overflow: hidden;
opacity: 0;
transition: opacity 0.3s var(--ease);
}
.cursor-img.is-visible {
opacity: 1;
}
.cursor-img img {
width: 100%;
height: 100%;
object-fit: cover;
}
/* ---- About teaser ---- */
.about {
padding: 6rem 0 10rem;
border-top: 1px solid var(--border-light);
}
.about__inner {
max-width: 680px;
}
.about__text {
font-family: var(--serif);
font-size: clamp(1.3rem, 2.5vw, 2rem);
line-height: 1.45;
color: var(--text);
margin-bottom: 2.5rem;
max-width: none;
}
.about__link {
display: inline-flex;
align-items: center;
gap: 0.5rem;
font-size: 0.7rem;
letter-spacing: 0.1em;
color: var(--text-2);
text-decoration: none;
transition: color 0.2s;
}
.about__link:hover { color: var(--text); }
/* ---- Responsive ---- */
@media (max-width: 768px) {
.feat__grid {
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
}
.feat__item:nth-child(1) { grid-column: 1 / -1; }
.feat__item:nth-child(2) { grid-column: 1 / 2; }
.feat__item:nth-child(3) { grid-column: 2 / 3; }
.feat__item:nth-child(4) { grid-column: 1 / -1; }
.feat__item:nth-child(5) { grid-column: 1 / 2; }
.feat__item:nth-child(6) { grid-column: 2 / 3; }
.feat__item:nth-child(2) .feat__img,
.feat__item:nth-child(3) .feat__img { aspect-ratio: 3 / 4; }
.idx__row {
grid-template-columns: 2rem 1fr 3rem;
}
.idx__client { display: none; }
.cursor-img { display: none; }
.hero__name {
font-size: clamp(2.8rem, 12vw, 5rem);
}
}
</style>

View File

@ -1,117 +1,349 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
import ProcessTimeline from '../components/ProcessTimeline.tsx';
import ProcessTimeline from '../components/ProcessTimeline.astro';
import { mockProcessSteps } from '../lib/mock-data';
---
<BaseLayout title="Processus Créatif — Aurélie Barré" description="Découvrez le processus créatif d'Aurélie Barré, de la prise de contact à la livraison de votre projet de design d'intérieur.">
<section class="page-hero">
<div class="container">
<span class="page-hero__label">Méthode</span>
<h1>Processus<br /><em>Créatif</em></h1>
<p class="page-hero__desc text-secondary">
Chaque projet suit un parcours rigoureux en 5 étapes,
de la première rencontre à la livraison finale.
Une méthode éprouvée pour garantir un résultat à la hauteur de vos attentes.
</p>
<BaseLayout title="Processus créatif — Aurélie Barré">
<!-- ====== Hero: full-width photo with title ====== -->
<section class="proc-hero">
<div class="proc-hero__img">
<img src="/images/events/alula/03.jpg" alt="Processus créatif" />
</div>
<div class="proc-hero__overlay"></div>
<div class="proc-hero__content">
<span class="proc-hero__label">méthode</span>
<h1>processus<br />créatif</h1>
</div>
</section>
<section class="section process-section">
<div class="container">
<ProcessTimeline client:visible steps={mockProcessSteps} />
</div>
</section>
<!-- CTA -->
<section class="section cta-section">
<div class="container">
<div class="cta-inner reveal">
<h2>Un projet en tête ?</h2>
<p class="text-secondary">
Discutons de vos envies autour d'un café. Chaque projet commence par une conversation.
</p>
<a href="mailto:contact@aureliebarre.fr" class="cta-link">
Prendre contact
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" style="margin-left: 8px;">
<path d="M3 8H13M13 8L9 4M13 8L9 12" stroke="currentColor" stroke-width="1.2"/>
</svg>
</a>
<!-- ====== Intro: editorial text ====== -->
<section class="proc-intro">
<div class="wrap">
<div class="proc-intro__grid">
<div class="proc-intro__left rv">
<span class="label">approche</span>
</div>
<div class="proc-intro__right rv" style="--d:1">
<p class="proc-intro__text">
Chaque projet commence par une conversation. De la première rencontre
à la livraison — un parcours en cinq étapes, pensé pour que le résultat
soit toujours à la hauteur de l'intention.
</p>
<p class="proc-intro__sub">
Cette méthode s'est construite au fil des projets, avec des clients
comme Dior, LVMH, Moët & Chandon ou Cisco. Elle s'adapte à chaque
contexte — événement éphémère ou aménagement pérenne.
</p>
</div>
</div>
</div>
</section>
<!-- ====== Timeline: the 5 steps ====== -->
<section class="proc-steps">
<div class="wrap">
<ProcessTimeline steps={mockProcessSteps} />
</div>
</section>
<!-- ====== Visual break: two photos side by side ====== -->
<section class="proc-visual">
<div class="wrap">
<div class="proc-visual__grid rv">
<div class="proc-visual__img proc-visual__img--tall">
<img src="/images/events/moet/04.jpg" alt="Détail scénographie" loading="lazy" />
</div>
<div class="proc-visual__img proc-visual__img--wide">
<img src="/images/perennes/cisco/03.jpeg" alt="Espace aménagé" loading="lazy" />
</div>
</div>
</div>
</section>
<!-- ====== Philosophy quote ====== -->
<section class="proc-quote">
<div class="wrap">
<blockquote class="proc-quote__text rv">
« Un espace réussi, c'est un espace qui raconte quelque chose
sans avoir besoin de l'expliquer. »
</blockquote>
<span class="proc-quote__author rv" style="--d:1">— Aurélie Barré</span>
</div>
</section>
<!-- ====== Values: three columns ====== -->
<section class="proc-values">
<div class="wrap">
<div class="proc-values__grid">
<div class="proc-values__item rv" style="--d:0">
<span class="proc-values__num">01</span>
<h3 class="proc-values__title">écoute</h3>
<p class="proc-values__desc">
Chaque projet naît d'un dialogue. Comprendre avant de concevoir,
écouter avant de dessiner.
</p>
</div>
<div class="proc-values__item rv" style="--d:1">
<span class="proc-values__num">02</span>
<h3 class="proc-values__title">détail</h3>
<p class="proc-values__desc">
Le soin du détail transforme un lieu en une expérience.
Chaque matière, chaque lumière est un choix.
</p>
</div>
<div class="proc-values__item rv" style="--d:2">
<span class="proc-values__num">03</span>
<h3 class="proc-values__title">cohérence</h3>
<p class="proc-values__desc">
De l'idée initiale au résultat final — un fil conducteur
qui ne se perd jamais.
</p>
</div>
</div>
</div>
</section>
<!-- ====== CTA ====== -->
<section class="proc-cta">
<div class="wrap">
<div class="proc-cta__inner rv">
<h2 class="proc-cta__title">un projet ?</h2>
<a href="mailto:contact@aureliebarre.fr" class="proc-cta__link">contact@aureliebarre.fr</a>
</div>
</div>
</section>
</BaseLayout>
<style>
.page-hero {
padding-top: calc(var(--header-height) + var(--space-3xl));
padding-bottom: var(--space-xl);
/* ---- Hero ---- */
.proc-hero {
position: relative;
height: 65vh;
min-height: 420px;
overflow: hidden;
display: flex;
align-items: flex-end;
}
.page-hero__label {
font-size: 0.75rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--color-text-light);
.proc-hero__img {
position: absolute;
inset: 0;
}
.proc-hero__img img {
width: 100%;
height: 100%;
object-fit: cover;
}
.proc-hero__overlay {
position: absolute;
inset: 0;
background: linear-gradient(
to top,
rgba(0,0,0,0.55) 0%,
rgba(0,0,0,0.15) 50%,
rgba(0,0,0,0.05) 100%
);
}
.proc-hero__content {
position: relative;
z-index: 2;
padding: 0 var(--pad) 5vh;
}
.proc-hero__label {
font-family: var(--sans);
font-size: 0.6rem;
font-weight: 300;
letter-spacing: 0.14em;
color: rgba(255,255,255,0.5);
display: block;
margin-bottom: var(--space-md);
margin-bottom: 0.8rem;
}
.page-hero h1 {
margin-bottom: var(--space-lg);
.proc-hero__content h1 {
font-size: clamp(2.5rem, 7vw, 5.5rem);
color: #fff;
line-height: 1;
text-transform: lowercase;
letter-spacing: -0.02em;
}
.page-hero h1 em {
color: var(--color-accent);
/* ---- Intro ---- */
.proc-intro {
padding: 6rem 0 4rem;
}
.page-hero__desc {
font-size: 1.1rem;
line-height: 1.8;
max-width: 600px;
.proc-intro__grid {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 3rem;
}
.process-section {
padding-top: var(--space-xl);
padding-bottom: var(--space-3xl);
.proc-intro__left {
padding-top: 0.3rem;
}
/* CTA */
.cta-section {
background-color: var(--color-bg-alt);
.proc-intro__text {
font-family: var(--serif);
font-size: clamp(1.2rem, 2.2vw, 1.7rem);
line-height: 1.5;
color: var(--text);
margin-bottom: 1.5rem;
max-width: 50ch;
}
.cta-inner {
text-align: center;
max-width: 500px;
margin: 0 auto;
.proc-intro__sub {
font-size: 0.9rem;
line-height: 1.75;
color: var(--text-2);
max-width: 50ch;
}
.cta-inner h2 {
margin-bottom: var(--space-md);
/* ---- Steps ---- */
.proc-steps {
padding: 2rem 0 6rem;
}
.cta-inner p {
margin: 0 auto var(--space-xl);
font-size: 1.05rem;
line-height: 1.8;
/* ---- Visual break ---- */
.proc-visual {
padding: 0 0 6rem;
}
.cta-link {
display: inline-flex;
align-items: center;
.proc-visual__grid {
display: grid;
grid-template-columns: 5fr 7fr;
gap: 4px;
height: 50vh;
min-height: 350px;
}
.proc-visual__img {
overflow: hidden;
}
.proc-visual__img img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 1s var(--ease);
}
.proc-visual__img:hover img {
transform: scale(1.03);
}
/* ---- Quote ---- */
.proc-quote {
padding: 5rem 0;
border-top: 1px solid var(--border-light);
border-bottom: 1px solid var(--border-light);
}
.proc-quote__text {
font-family: var(--serif);
font-size: clamp(1.4rem, 3vw, 2.5rem);
line-height: 1.4;
color: var(--text);
max-width: 700px;
font-style: normal;
margin: 0;
}
.proc-quote__author {
display: block;
margin-top: 1.5rem;
font-size: 0.7rem;
letter-spacing: 0.1em;
color: var(--text-3);
}
/* ---- Values ---- */
.proc-values {
padding: 6rem 0;
}
.proc-values__grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 3rem;
}
.proc-values__num {
font-family: var(--serif);
font-size: 2.5rem;
color: var(--border);
display: block;
line-height: 1;
margin-bottom: 1.2rem;
}
.proc-values__title {
font-size: 1.2rem;
text-transform: lowercase;
margin-bottom: 0.8rem;
line-height: 1.2;
}
.proc-values__desc {
font-size: 0.85rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--color-accent);
text-decoration: none;
padding: 12px 32px;
border: 1px solid var(--color-accent);
transition: all var(--duration-fast) var(--ease-out);
line-height: 1.7;
color: var(--text-2);
}
.cta-link:hover {
background-color: var(--color-accent);
color: white;
/* ---- CTA ---- */
.proc-cta {
padding: 5rem 0 8rem;
border-top: 1px solid var(--border-light);
}
.proc-cta__title {
font-size: clamp(2rem, 5vw, 4rem);
line-height: 1.05;
margin-bottom: 1rem;
text-transform: lowercase;
}
.proc-cta__link {
font-size: 0.8rem;
letter-spacing: 0.06em;
color: var(--text-2);
text-decoration: none;
border-bottom: 1px solid var(--border);
padding-bottom: 2px;
transition: color 0.2s, border-color 0.2s;
}
.proc-cta__link:hover {
color: var(--text);
border-color: var(--text);
}
/* ---- Responsive ---- */
@media (max-width: 768px) {
.proc-intro__grid {
grid-template-columns: 1fr;
gap: 1.5rem;
}
.proc-visual__grid {
grid-template-columns: 1fr;
height: auto;
min-height: 0;
}
.proc-visual__img {
aspect-ratio: 3 / 2;
}
.proc-values__grid {
grid-template-columns: 1fr;
gap: 2.5rem;
}
}
</style>

View File

@ -1,206 +1,137 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import MasonryGallery from '../../components/MasonryGallery.tsx';
import MasonryGallery from '../../components/MasonryGallery.astro';
import { getMockProjects, getMockProjectBySlug } from '../../lib/mock-data';
export function getStaticPaths() {
const projects = getMockProjects('perenne');
return projects.map(p => ({ params: { slug: p.slug.current } }));
return getMockProjects('perenne').map(p => ({ params: { slug: p.slug.current } }));
}
const { slug } = Astro.params;
const project = getMockProjectBySlug(slug!);
if (!project) return Astro.redirect('/realisations-perennes');
if (!project) {
return Astro.redirect('/realisations-perennes');
}
const dateFormatted = new Date(project.date).toLocaleDateString('fr-FR', {
year: 'numeric',
month: 'long',
});
const date = new Date(project.date).toLocaleDateString('fr-FR', { year: 'numeric', month: 'long' });
---
<BaseLayout title={`${project.title} — Aurélie Barré`} description={project.description}>
<!-- Hero Image -->
<section class="project-hero">
<div class="project-hero__image">
<section class="phero">
<div class="phero__img">
<img src={project.heroImage} alt={project.title} />
<div class="project-hero__overlay"></div>
</div>
<div class="project-hero__content container">
<span class="project-hero__client">{project.client}</span>
<h1>{project.title}</h1>
</div>
<div class="phero__overlay"></div>
</section>
<!-- Project Info -->
<section class="section">
<div class="container">
<div class="project-meta reveal">
<div class="project-meta__item">
<span class="project-meta__label">Client</span>
<span class="project-meta__value">{project.client}</span>
</div>
<div class="project-meta__item">
<span class="project-meta__label">Date</span>
<span class="project-meta__value">{dateFormatted}</span>
</div>
{project.location && (
<div class="project-meta__item">
<span class="project-meta__label">Lieu</span>
<span class="project-meta__value">{project.location}</span>
<section class="pinfo">
<div class="wrap">
<div class="pinfo__top rv">
<h1 class="pinfo__title">{project.title}</h1>
<div class="pinfo__meta">
<div class="pinfo__col">
<span class="label">client</span>
<span class="pinfo__val">{project.client}</span>
</div>
)}
<div class="project-meta__item">
<span class="project-meta__label">Type</span>
<span class="project-meta__value">Réalisation pérenne</span>
<div class="pinfo__col">
<span class="label">date</span>
<span class="pinfo__val">{date}</span>
</div>
{project.location && (
<div class="pinfo__col">
<span class="label">lieu</span>
<span class="pinfo__val">{project.location}</span>
</div>
)}
</div>
</div>
<div class="project-description reveal">
<p>{project.description}</p>
</div>
{project.tags && project.tags.length > 0 && (
<div class="project-tags reveal">
{project.tags.map(tag => (
<span class="project-tag">{tag}</span>
))}
</div>
)}
<p class="pinfo__desc rv" style="--d:2">{project.description}</p>
</div>
</section>
<!-- Gallery (Masonry) -->
{project.gallery && project.gallery.length > 0 && (
<section class="section" style="background-color: var(--color-bg-alt);">
<div class="container">
<section class="pgallery">
<div class="wrap">
<MasonryGallery
client:visible
images={project.gallery.map(img => ({
src: img.src,
alt: img.alt,
size: img.size,
}))}
images={project.gallery.map(img => ({ src: img.src, alt: img.alt }))}
/>
</div>
</section>
)}
<!-- Back link -->
<section class="section">
<div class="container">
<a href="/realisations-perennes" class="back-link">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" style="margin-right: 8px;">
<path d="M13 8H3M3 8L7 4M3 8L7 12" stroke="currentColor" stroke-width="1.2"/>
<section class="pnav">
<div class="wrap">
<a href="/realisations-perennes" class="pnav__link">
<svg width="14" height="14" viewBox="0 0 16 16" fill="none">
<path d="M13 8H3M3 8L7 4M3 8L7 12" stroke="currentColor" stroke-width="1"/>
</svg>
Retour aux réalisations pérennes
retour
</a>
</div>
</section>
</BaseLayout>
<style>
.project-hero {
.phero {
position: relative;
height: 70vh;
height: 75vh;
min-height: 500px;
display: flex;
align-items: flex-end;
overflow: hidden;
}
.project-hero__image {
position: absolute;
inset: 0;
.phero__img { position: absolute; inset: 0; }
.phero__img img { width: 100%; height: 100%; object-fit: cover; }
.phero__overlay {
position: absolute; inset: 0;
background: linear-gradient(to bottom, rgba(0,0,0,0.08) 0%, rgba(0,0,0,0.02) 100%);
}
.project-hero__image img {
width: 100%;
height: 100%;
object-fit: cover;
.pinfo { padding: 4rem 0 3rem; }
.pinfo__top {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 2rem;
padding-bottom: 2.5rem;
border-bottom: 1px solid var(--border-light);
margin-bottom: 2.5rem;
}
.project-hero__overlay {
position: absolute;
inset: 0;
background: linear-gradient(to top, rgba(0,0,0,0.5) 0%, rgba(0,0,0,0.1) 50%, transparent 100%);
.pinfo__title {
font-size: clamp(1.8rem, 4vw, 3rem);
max-width: 55%;
line-height: 1.1;
}
.project-hero__content {
position: relative;
z-index: 1;
padding-bottom: var(--space-2xl);
color: white;
.pinfo__meta { display: flex; gap: 2.5rem; flex-shrink: 0; }
.pinfo__col { display: flex; flex-direction: column; gap: 0.35rem; }
.pinfo__val { font-size: 0.85rem; color: var(--text); }
.pinfo__desc {
font-size: 1.05rem;
line-height: 1.75;
color: var(--text-2);
max-width: 650px;
}
.project-hero__client {
font-size: 0.75rem;
letter-spacing: 0.12em;
text-transform: uppercase;
opacity: 0.7;
display: block;
margin-bottom: var(--space-sm);
}
.pgallery { padding: 2rem 0 6rem; }
.project-hero__content h1 {
color: white;
font-size: clamp(2rem, 5vw, 3.5rem);
}
.project-meta {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--space-lg);
padding-bottom: var(--space-xl);
border-bottom: 1px solid var(--color-border);
margin-bottom: var(--space-xl);
}
.project-meta__label {
font-size: 0.7rem;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--color-text-light);
display: block;
margin-bottom: var(--space-xs);
}
.project-meta__value {
font-size: 0.95rem;
}
.project-description { margin-bottom: var(--space-xl); }
.project-description p {
font-size: 1.15rem;
line-height: 1.8;
color: var(--color-text-secondary);
max-width: 700px;
}
.project-tags { display: flex; gap: var(--space-sm); flex-wrap: wrap; }
.project-tag {
font-size: 0.75rem;
letter-spacing: 0.04em;
color: var(--color-text-light);
padding: 4px 12px;
border: 1px solid var(--color-border);
border-radius: 2px;
}
.back-link {
.pnav { padding: 0 0 5rem; }
.pnav__link {
display: inline-flex;
align-items: center;
font-size: 0.85rem;
color: var(--color-text-secondary);
gap: 0.5rem;
font-size: 0.7rem;
letter-spacing: 0.08em;
color: var(--text-3);
text-decoration: none;
transition: color var(--duration-fast);
transition: color 0.2s;
}
.back-link:hover { color: var(--color-accent); }
.pnav__link:hover { color: var(--text); }
@media (max-width: 768px) {
.project-meta { grid-template-columns: repeat(2, 1fr); }
.project-hero { height: 50vh; }
.phero { height: 55vh; }
.pinfo__top { flex-direction: column; }
.pinfo__title { max-width: 100%; }
.pinfo__meta { flex-wrap: wrap; gap: 1.5rem; }
}
</style>

View File

@ -1,81 +1,138 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import ProjectCard from '../../components/ProjectCard.astro';
import { getMockProjects } from '../../lib/mock-data';
const projects = getMockProjects('perenne');
---
<BaseLayout title="Réalisations Pérennes — Aurélie Barré" description="Découvrez les réalisations pérennes d'Aurélie Barré : aménagements de bureaux, showrooms et espaces de vie.">
<section class="page-hero">
<div class="container">
<span class="page-hero__label">Portfolio</span>
<h1>Réalisations<br /><em>Pérennes</em></h1>
<p class="page-hero__desc text-secondary">
Bureaux, showrooms, espaces de vie — des aménagements durables
pensés pour sublimer chaque lieu au quotidien.
</p>
</div>
</section>
<BaseLayout title="Réalisations pérennes — Aurélie Barré">
<section class="listing">
<div class="wrap">
<div class="listing__head rv">
<span class="label">pérenne</span>
<h1>réalisations<br />pérennes</h1>
</div>
<section class="section">
<div class="container">
<div class="projects-grid">
{projects.map((project, index) => (
<ProjectCard
title={project.title}
client={project.client}
slug={project.slug.current}
category={project.category}
heroImage={project.heroImage}
description={project.description}
tags={project.tags}
index={index}
/>
))}
<div class="listing__grid">
{projects.map((project, i) => {
const href = `/realisations-perennes/${project.slug.current}`;
return (
<a
href={href}
class="listing__item rv"
style={`--d:${i + 1}`}
>
<div class="listing__img">
<img src={project.heroImage} alt={project.title} loading="lazy" />
</div>
<div class="listing__meta">
<span class="listing__client">{project.client}</span>
<h2 class="listing__title">{project.title}</h2>
<span class="listing__year">{project.date.slice(0, 4)}</span>
</div>
</a>
);
})}
</div>
</div>
</section>
</BaseLayout>
<style>
.page-hero {
padding-top: calc(var(--header-height) + var(--space-3xl));
padding-bottom: var(--space-xl);
.listing {
padding-top: calc(var(--header-h) + 6rem);
padding-bottom: 6rem;
}
.page-hero__label {
font-size: 0.75rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--color-text-light);
.listing__head {
margin-bottom: 4rem;
}
.listing__head .label {
display: block;
margin-bottom: var(--space-md);
margin-bottom: 1rem;
}
.page-hero h1 {
margin-bottom: var(--space-lg);
.listing__head h1 {
font-size: clamp(2.2rem, 5vw, 4rem);
text-transform: lowercase;
line-height: 1.05;
}
.page-hero h1 em {
color: var(--color-accent);
}
.page-hero__desc {
font-size: 1.1rem;
line-height: 1.8;
max-width: 600px;
}
.projects-grid {
.listing__grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--space-xl) var(--space-lg);
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
/* First project full width */
.listing__item:first-child {
grid-column: 1 / -1;
}
.listing__item {
text-decoration: none;
color: inherit;
overflow: hidden;
}
.listing__img {
width: 100%;
overflow: hidden;
}
.listing__item:first-child .listing__img {
aspect-ratio: 2.4 / 1;
}
.listing__item:not(:first-child) .listing__img {
aspect-ratio: 4 / 3;
}
.listing__img img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.8s var(--ease);
}
.listing__item:hover .listing__img img {
transform: scale(1.03);
}
.listing__grid:hover .listing__item:not(:hover) .listing__img img {
filter: brightness(0.88);
transition: filter 0.4s;
}
.listing__meta {
padding: 0.8rem 0 1.5rem;
}
.listing__client {
font-size: 0.6rem;
letter-spacing: 0.1em;
color: var(--text-3);
text-transform: lowercase;
display: block;
margin-bottom: 0.2rem;
}
.listing__title {
font-size: clamp(0.95rem, 1.5vw, 1.2rem);
font-weight: 400;
line-height: 1.3;
display: inline;
}
.listing__year {
font-size: 0.65rem;
color: var(--text-3);
margin-left: 0.75rem;
font-variant-numeric: tabular-nums;
}
@media (max-width: 768px) {
.projects-grid {
grid-template-columns: 1fr;
}
.listing__grid { grid-template-columns: 1fr; }
}
</style>

View File

@ -1,229 +1,99 @@
/* ==========================================================================
Aurélie Barré Portfolio Global Styles
Direction: "Galerie Blanche" Éditorial Light
Aurélie Barré v4
Pure white. Photos do all the work.
========================================================================== */
/* --- CSS Custom Properties --- */
:root {
/* Palette */
--color-bg: #FAFAF8;
--color-bg-alt: #F3F1ED;
--color-surface: #FFFFFF;
--color-text: #1A1A1A;
--color-text-secondary: #6B6B6B;
--color-text-light: #9A9A9A;
--color-accent: #C4A77D;
--color-accent-hover: #B08E5E;
--color-border: #E8E6E1;
--color-border-light: #F0EEEA;
/* Typography */
--font-serif: 'DM Serif Display', Georgia, 'Times New Roman', serif;
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
/* Spacing */
--space-xs: 0.25rem;
--space-sm: 0.5rem;
--space-md: 1rem;
--space-lg: 2rem;
--space-xl: 4rem;
--space-2xl: 6rem;
--space-3xl: 8rem;
/* Layout */
--container-max: 1440px;
--container-padding: clamp(1.5rem, 4vw, 4rem);
--header-height: 80px;
/* Transitions */
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
--duration-fast: 200ms;
--duration-normal: 400ms;
--duration-slow: 800ms;
--bg: #fff;
--text: #111;
--text-2: #555;
--text-3: #999;
--border: #e0e0e0;
--border-light: #f0f0f0;
--serif: 'DM Serif Display', Georgia, serif;
--sans: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
--header-h: 60px;
--ease: cubic-bezier(0.16, 1, 0.3, 1);
--ease-in: cubic-bezier(0.65, 0, 0.35, 1);
--pad: clamp(1.25rem, 3.5vw, 3rem);
}
/* --- Reset & Base --- */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html {
font-size: 16px;
scroll-behavior: smooth;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
font-family: var(--font-sans);
font-size: 1rem;
line-height: 1.7;
color: var(--color-text);
background-color: var(--color-bg);
font-family: var(--sans);
font-size: 0.95rem;
line-height: 1.65;
color: var(--text);
background: var(--bg);
overflow-x: hidden;
}
/* --- Typography --- */
h1, h2, h3, h4 {
font-family: var(--font-serif);
h1, h2, h3 {
font-family: var(--serif);
font-weight: 400;
line-height: 1.2;
letter-spacing: -0.01em;
line-height: 1.1;
letter-spacing: -0.02em;
}
h1 {
font-size: clamp(2.5rem, 6vw, 5rem);
h1 { font-size: clamp(2.8rem, 7vw, 6rem); }
h2 { font-size: clamp(1.6rem, 4vw, 3rem); }
h3 { font-size: clamp(1.1rem, 2vw, 1.5rem); }
a { color: inherit; text-decoration: none; }
img { display: block; max-width: 100%; height: auto; }
p { max-width: 60ch; }
/* --- Label style (used everywhere) --- */
.label {
font-family: var(--sans);
font-size: 0.65rem;
font-weight: 400;
letter-spacing: 0.14em;
text-transform: lowercase;
color: var(--text-3);
}
h2 {
font-size: clamp(1.8rem, 4vw, 3rem);
}
h3 {
font-size: clamp(1.3rem, 2.5vw, 1.8rem);
}
p {
max-width: 65ch;
}
a {
color: inherit;
text-decoration: none;
transition: color var(--duration-fast) var(--ease-out);
}
a:hover {
color: var(--color-accent);
}
img {
display: block;
max-width: 100%;
height: auto;
}
/* --- Utilities --- */
.container {
/* --- Layout --- */
.wrap {
width: 100%;
max-width: var(--container-max);
max-width: 1400px;
margin: 0 auto;
padding-left: var(--container-padding);
padding-right: var(--container-padding);
}
.section {
padding-top: var(--space-2xl);
padding-bottom: var(--space-2xl);
}
.text-accent {
color: var(--color-accent);
}
.text-secondary {
color: var(--color-text-secondary);
}
.text-light {
color: var(--color-text-light);
padding-left: var(--pad);
padding-right: var(--pad);
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
position: absolute; width: 1px; height: 1px;
padding: 0; margin: -1px; overflow: hidden;
clip: rect(0,0,0,0); white-space: nowrap; border: 0;
}
/* --- Divider --- */
.divider {
width: 100%;
height: 1px;
background-color: var(--color-border);
}
/* --- Reveal Animation (applied via JS) --- */
.reveal {
/* --- Reveal animation --- */
.rv {
opacity: 0;
transform: translateY(30px);
transition: opacity var(--duration-slow) var(--ease-out),
transform var(--duration-slow) var(--ease-out);
transform: translateY(24px);
transition: opacity 0.7s var(--ease), transform 0.7s var(--ease);
transition-delay: calc(var(--d, 0) * 80ms);
}
.reveal.visible {
.rv.vis {
opacity: 1;
transform: translateY(0);
}
/* --- Masonry Grid --- */
.masonry-grid {
columns: 3;
column-gap: var(--space-md);
}
/* --- Scrollbar: hidden --- */
::-webkit-scrollbar { width: 0; height: 0; }
html { scrollbar-width: none; }
.masonry-grid > * {
break-inside: avoid;
margin-bottom: var(--space-md);
}
@media (max-width: 1024px) {
.masonry-grid {
columns: 2;
}
}
@media (max-width: 640px) {
.masonry-grid {
columns: 1;
}
}
/* --- Image Hover Effect --- */
.img-hover-zoom {
overflow: hidden;
}
.img-hover-zoom img {
transition: transform var(--duration-slow) var(--ease-out);
}
.img-hover-zoom:hover img {
transform: scale(1.04);
}
/* --- Page Transitions --- */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.page-enter {
animation: fadeIn var(--duration-slow) var(--ease-out) both;
}
/* --- Scrollbar --- */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: var(--color-bg);
}
::-webkit-scrollbar-thumb {
background: var(--color-border);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--color-text-light);
/* --- Selection --- */
::selection {
background: var(--text);
color: var(--bg);
}

View File

@ -0,0 +1,68 @@
/* ==========================================================================
Aurélie Barré Portfolio
========================================================================== */
:root {
--bg: #FFFFFF;
--text: #1A1A1A;
--text-2: #777;
--text-3: #AAA;
--border: #E5E5E5;
--serif: 'DM Serif Display', Georgia, serif;
--sans: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
--header-h: 72px;
--ease: cubic-bezier(0.16, 1, 0.3, 1);
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html {
font-size: 16px;
scroll-behavior: smooth;
-webkit-font-smoothing: antialiased;
}
body {
font-family: var(--sans);
font-size: 0.9rem;
line-height: 1.6;
color: var(--text);
background: var(--bg);
overflow-x: hidden;
}
h1, h2, h3 {
font-family: var(--serif);
font-weight: 400;
line-height: 1.08;
letter-spacing: -0.02em;
}
h1 { font-size: clamp(2.8rem, 7vw, 6rem); }
h2 { font-size: clamp(1.8rem, 4.5vw, 3.5rem); }
h3 { font-size: clamp(1.1rem, 1.8vw, 1.4rem); line-height: 1.35; }
a { color: inherit; text-decoration: none; }
img { display: block; max-width: 100%; height: auto; }
.label {
font-size: 0.6rem;
font-weight: 400;
letter-spacing: 0.16em;
text-transform: lowercase;
color: var(--text-3);
}
.pad { padding: 0 clamp(2rem, 5vw, 6rem); }
/* Reveal */
.rv {
opacity: 0;
transform: translateY(30px);
transition: opacity 0.9s var(--ease), transform 0.9s var(--ease);
}
.rv.vis { opacity: 1; transform: none; }
::-webkit-scrollbar { width: 0px; }