harder better faster stronger
This commit is contained in:
parent
9d172a4422
commit
fdcccd49aa
@ -2,107 +2,56 @@
|
|||||||
const year = new Date().getFullYear();
|
const year = new Date().getFullYear();
|
||||||
---
|
---
|
||||||
|
|
||||||
<footer class="footer">
|
<footer class="ft">
|
||||||
<div class="container">
|
<div class="wrap">
|
||||||
<div class="divider"></div>
|
<div class="ft__line"></div>
|
||||||
<div class="footer__inner">
|
<div class="ft__row">
|
||||||
<div class="footer__left">
|
<span class="ft__copy">© {year}</span>
|
||||||
<p class="footer__name">Aurélie Barré</p>
|
<div class="ft__links">
|
||||||
<p class="footer__tagline text-secondary">Design d'intérieur & Création événementielle</p>
|
<a href="https://instagram.com/aureliebarre" target="_blank" rel="noopener">instagram</a>
|
||||||
</div>
|
<a href="https://linkedin.com/in/aureliebarre" target="_blank" rel="noopener">linkedin</a>
|
||||||
|
|
||||||
<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">© {year}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.footer {
|
.ft {
|
||||||
padding-top: var(--space-xl);
|
padding: 0 0 2rem;
|
||||||
padding-bottom: var(--space-lg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer__inner {
|
.ft__line {
|
||||||
|
height: 1px;
|
||||||
|
background: var(--border-light);
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ft__row {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: flex-start;
|
align-items: center;
|
||||||
padding-top: var(--space-xl);
|
|
||||||
gap: var(--space-lg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer__name {
|
.ft__copy {
|
||||||
font-family: var(--font-serif);
|
font-size: 0.6rem;
|
||||||
font-size: 1.1rem;
|
color: var(--text-3);
|
||||||
margin-bottom: var(--space-xs);
|
letter-spacing: 0.05em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer__tagline {
|
.ft__links {
|
||||||
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 {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--space-lg);
|
gap: 1.5rem;
|
||||||
margin-bottom: var(--space-sm);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer__social-link {
|
.ft__links a {
|
||||||
font-size: 0.85rem;
|
font-size: 0.6rem;
|
||||||
letter-spacing: 0.06em;
|
letter-spacing: 0.1em;
|
||||||
text-transform: uppercase;
|
color: var(--text-3);
|
||||||
color: var(--color-text-secondary);
|
text-decoration: none;
|
||||||
|
transition: color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer__social-link:hover {
|
.ft__links a:hover {
|
||||||
color: var(--color-accent);
|
color: var(--text);
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,171 +1,167 @@
|
|||||||
---
|
---
|
||||||
const currentPath = Astro.url.pathname;
|
const currentPath = Astro.url.pathname;
|
||||||
|
const isHome = currentPath === '/';
|
||||||
const navItems = [
|
const nav = [
|
||||||
{ label: 'Réalisations Pérennes', href: '/realisations-perennes' },
|
{ label: 'projets', href: '/creations-evenement' },
|
||||||
{ label: 'Créations Événement', href: '/creations-evenement' },
|
{ label: 'pérenne', href: '/realisations-perennes' },
|
||||||
{ label: 'Processus Créatif', href: '/processus' },
|
{ label: 'processus', href: '/processus' },
|
||||||
|
{ label: 'contact', href: 'mailto:contact@aureliebarre.fr' },
|
||||||
];
|
];
|
||||||
---
|
---
|
||||||
|
|
||||||
<header class="header">
|
<header class:list={['hd', { 'hd--hero': isHome }]} id="hd">
|
||||||
<div class="header__inner container">
|
<div class="hd__in">
|
||||||
<a href="/" class="header__logo">
|
<a href="/" class="hd__logo">aurélie barré</a>
|
||||||
<span class="header__logo-name">Aurélie Barré</span>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<nav class="header__nav" aria-label="Navigation principale">
|
<nav class="hd__nav" aria-label="Navigation">
|
||||||
<ul class="header__nav-list">
|
{nav.map(item => (
|
||||||
{navItems.map(item => (
|
<a
|
||||||
<li>
|
href={item.href}
|
||||||
<a
|
class:list={['hd__link', { 'is-active': item.href !== 'mailto:contact@aureliebarre.fr' && currentPath.startsWith(item.href) }]}
|
||||||
href={item.href}
|
>
|
||||||
class:list={['header__nav-link', { 'active': currentPath.startsWith(item.href) }]}
|
{item.label}
|
||||||
>
|
</a>
|
||||||
{item.label}
|
))}
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<button class="header__menu-btn" aria-label="Ouvrir le menu" aria-expanded="false">
|
<button class="hd__burger" id="burger" aria-label="Menu" aria-expanded="false">
|
||||||
<span class="header__menu-line"></span>
|
<span></span>
|
||||||
<span class="header__menu-line"></span>
|
<span></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mobile overlay -->
|
<!-- Mobile fullscreen -->
|
||||||
<div class="header__overlay" aria-hidden="true">
|
<div class="hd__mobile" id="mobile-nav" aria-hidden="true">
|
||||||
<nav class="header__overlay-nav">
|
<nav class="hd__mobile-nav">
|
||||||
{navItems.map(item => (
|
{nav.map((item, i) => (
|
||||||
<a href={item.href} class="header__overlay-link">
|
<a href={item.href} class="hd__mobile-link" style={`--i:${i}`}>{item.label}</a>
|
||||||
{item.label}
|
|
||||||
</a>
|
|
||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const btn = document.querySelector('.header__menu-btn');
|
const burger = document.getElementById('burger');
|
||||||
const overlay = document.querySelector('.header__overlay');
|
const mobile = document.getElementById('mobile-nav');
|
||||||
const header = document.querySelector('.header');
|
const hd = document.getElementById('hd');
|
||||||
|
|
||||||
btn?.addEventListener('click', () => {
|
burger?.addEventListener('click', () => {
|
||||||
const isOpen = btn.getAttribute('aria-expanded') === 'true';
|
const open = burger.getAttribute('aria-expanded') === 'true';
|
||||||
btn.setAttribute('aria-expanded', String(!isOpen));
|
burger.setAttribute('aria-expanded', String(!open));
|
||||||
overlay?.setAttribute('aria-hidden', String(isOpen));
|
mobile?.setAttribute('aria-hidden', String(open));
|
||||||
header?.classList.toggle('menu-open');
|
hd?.classList.toggle('is-open');
|
||||||
document.body.style.overflow = isOpen ? '' : 'hidden';
|
document.body.style.overflow = open ? '' : 'hidden';
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close on link click
|
document.querySelectorAll('.hd__mobile-link').forEach(l => {
|
||||||
document.querySelectorAll('.header__overlay-link').forEach(link => {
|
l.addEventListener('click', () => {
|
||||||
link.addEventListener('click', () => {
|
burger?.setAttribute('aria-expanded', 'false');
|
||||||
btn?.setAttribute('aria-expanded', 'false');
|
mobile?.setAttribute('aria-hidden', 'true');
|
||||||
overlay?.setAttribute('aria-hidden', 'true');
|
hd?.classList.remove('is-open');
|
||||||
header?.classList.remove('menu-open');
|
|
||||||
document.body.style.overflow = '';
|
document.body.style.overflow = '';
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Header scroll effect
|
let ticking = false;
|
||||||
let lastScroll = 0;
|
|
||||||
window.addEventListener('scroll', () => {
|
window.addEventListener('scroll', () => {
|
||||||
const scrollY = window.scrollY;
|
if (!ticking) {
|
||||||
if (scrollY > 100) {
|
requestAnimationFrame(() => {
|
||||||
header?.classList.add('scrolled');
|
hd?.classList.toggle('is-scrolled', window.scrollY > 60);
|
||||||
} else {
|
ticking = false;
|
||||||
header?.classList.remove('scrolled');
|
});
|
||||||
|
ticking = true;
|
||||||
}
|
}
|
||||||
lastScroll = scrollY;
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.header {
|
.hd {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0; left: 0; right: 0;
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
height: var(--header-height);
|
height: var(--header-h);
|
||||||
background-color: transparent;
|
transition: background 0.5s var(--ease), backdrop-filter 0.5s;
|
||||||
transition: background-color var(--duration-normal) var(--ease-out),
|
|
||||||
box-shadow var(--duration-normal) var(--ease-out);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header.scrolled {
|
/* ---- Default: dark text (inner pages) ---- */
|
||||||
background-color: rgba(250, 250, 248, 0.95);
|
.hd__logo { color: var(--text); }
|
||||||
backdrop-filter: blur(10px);
|
.hd__link { color: var(--text-2); }
|
||||||
-webkit-backdrop-filter: blur(10px);
|
.hd__link:hover, .hd__link.is-active { color: var(--text); }
|
||||||
box-shadow: 0 1px 0 var(--color-border-light);
|
.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;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
padding: 0 var(--pad);
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header__logo {
|
.hd__logo {
|
||||||
text-decoration: none;
|
font-family: var(--sans);
|
||||||
z-index: 101;
|
font-size: 0.8rem;
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
letter-spacing: 0.06em;
|
letter-spacing: 0.06em;
|
||||||
text-transform: uppercase;
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
position: relative;
|
z-index: 101;
|
||||||
padding-bottom: 2px;
|
transition: color 0.4s var(--ease);
|
||||||
transition: color var(--duration-fast) var(--ease-out);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.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: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
bottom: -2px; left: 0;
|
||||||
left: 0;
|
width: 0; height: 1px;
|
||||||
width: 0;
|
background: var(--text);
|
||||||
height: 1px;
|
transition: width 0.4s var(--ease);
|
||||||
background-color: var(--color-accent);
|
|
||||||
transition: width var(--duration-normal) var(--ease-out);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header__nav-link:hover,
|
.hd__link:hover::after,
|
||||||
.header__nav-link.active {
|
.hd__link.is-active::after { width: 100%; }
|
||||||
color: var(--color-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header__nav-link:hover::after,
|
/* Burger */
|
||||||
.header__nav-link.active::after {
|
.hd__burger {
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Mobile menu button */
|
|
||||||
.header__menu-btn {
|
|
||||||
display: none;
|
display: none;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 6px;
|
gap: 5px;
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -173,85 +169,65 @@ const navItems = [
|
|||||||
z-index: 101;
|
z-index: 101;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header__menu-line {
|
.hd__burger span {
|
||||||
display: block;
|
display: block;
|
||||||
width: 24px;
|
width: 20px;
|
||||||
height: 1.5px;
|
height: 1px;
|
||||||
background-color: var(--color-text);
|
transition: transform 0.4s var(--ease), opacity 0.2s, background 0.3s;
|
||||||
transition: transform var(--duration-normal) var(--ease-out),
|
|
||||||
opacity var(--duration-fast);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-open .header__menu-line:first-child {
|
.is-open .hd__burger span:first-child { transform: translateY(3px) rotate(45deg); }
|
||||||
transform: translateY(3.75px) rotate(45deg);
|
.is-open .hd__burger span:last-child { transform: translateY(-3px) rotate(-45deg); }
|
||||||
}
|
|
||||||
|
|
||||||
.menu-open .header__menu-line:last-child {
|
/* When menu is open, force dark burger (white bg behind) */
|
||||||
transform: translateY(-3.75px) rotate(-45deg);
|
.is-open .hd__burger span { background: var(--text) !important; }
|
||||||
}
|
|
||||||
|
|
||||||
/* Mobile overlay */
|
/* Mobile overlay */
|
||||||
.header__overlay {
|
.hd__mobile {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
inset: 0;
|
||||||
left: 0;
|
background: var(--bg);
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: var(--color-bg);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
transition: opacity var(--duration-normal) var(--ease-out),
|
transition: opacity 0.5s var(--ease), visibility 0.5s;
|
||||||
visibility var(--duration-normal);
|
|
||||||
z-index: 99;
|
z-index: 99;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-open .header__overlay {
|
.is-open .hd__mobile {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header__overlay-nav {
|
.hd__mobile-nav {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
gap: 2rem;
|
||||||
gap: var(--space-lg);
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header__overlay-link {
|
.hd__mobile-link {
|
||||||
font-family: var(--font-serif);
|
font-family: var(--serif);
|
||||||
font-size: clamp(1.8rem, 5vw, 2.5rem);
|
font-size: clamp(2rem, 6vw, 3rem);
|
||||||
color: var(--color-text);
|
color: var(--text);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(20px);
|
transform: translateY(20px);
|
||||||
transition: opacity var(--duration-normal) var(--ease-out),
|
transition: opacity 0.5s var(--ease), transform 0.5s var(--ease), color 0.2s;
|
||||||
transform var(--duration-normal) var(--ease-out),
|
|
||||||
color var(--duration-fast);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-open .header__overlay-link {
|
.is-open .hd__mobile-link {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
|
transition-delay: calc(var(--i) * 80ms + 100ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-open .header__overlay-link:nth-child(1) { transition-delay: 100ms; }
|
.hd__mobile-link:hover { color: var(--text-2); }
|
||||||
.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);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.header__nav {
|
.hd__nav { display: none; }
|
||||||
display: none;
|
.hd__burger { display: flex; }
|
||||||
}
|
|
||||||
|
|
||||||
.header__menu-btn {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
218
src/components/MasonryGallery.astro
Normal file
218
src/components/MasonryGallery.astro
Normal 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>
|
||||||
@ -4,7 +4,6 @@ import { motion, AnimatePresence } from 'motion/react';
|
|||||||
interface GalleryImage {
|
interface GalleryImage {
|
||||||
src: string;
|
src: string;
|
||||||
alt: string;
|
alt: string;
|
||||||
size?: 'small' | 'medium' | 'large';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -12,89 +11,72 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function MasonryGallery({ images }: Props) {
|
export default function MasonryGallery({ images }: Props) {
|
||||||
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
|
const [sel, setSel] = useState<number | null>(null);
|
||||||
const [columns, setColumns] = useState(3);
|
const [cols, setCols] = useState(3);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
// Responsive columns
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const updateColumns = () => {
|
const update = () => {
|
||||||
const width = window.innerWidth;
|
const w = window.innerWidth;
|
||||||
if (width < 640) setColumns(1);
|
setCols(w < 640 ? 1 : w < 1024 ? 2 : 3);
|
||||||
else if (width < 1024) setColumns(2);
|
|
||||||
else setColumns(3);
|
|
||||||
};
|
};
|
||||||
updateColumns();
|
update();
|
||||||
window.addEventListener('resize', updateColumns);
|
window.addEventListener('resize', update);
|
||||||
return () => window.removeEventListener('resize', updateColumns);
|
return () => window.removeEventListener('resize', update);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Distribute images across columns
|
const getCols = useCallback(() => {
|
||||||
const getColumns = useCallback(() => {
|
const c: GalleryImage[][] = Array.from({ length: cols }, () => []);
|
||||||
const cols: GalleryImage[][] = Array.from({ length: columns }, () => []);
|
images.forEach((img, i) => c[i % cols].push(img));
|
||||||
images.forEach((img, i) => {
|
return c;
|
||||||
cols[i % columns].push(img);
|
}, [images, cols]);
|
||||||
});
|
|
||||||
return cols;
|
|
||||||
}, [images, columns]);
|
|
||||||
|
|
||||||
// Lightbox keyboard nav
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKey = (e: KeyboardEvent) => {
|
const handle = (e: KeyboardEvent) => {
|
||||||
if (selectedIndex === null) return;
|
if (sel === null) return;
|
||||||
if (e.key === 'Escape') setSelectedIndex(null);
|
if (e.key === 'Escape') setSel(null);
|
||||||
if (e.key === 'ArrowRight') setSelectedIndex(prev =>
|
if (e.key === 'ArrowRight') setSel(p => p !== null && p < images.length - 1 ? p + 1 : p);
|
||||||
prev !== null && prev < images.length - 1 ? prev + 1 : prev
|
if (e.key === 'ArrowLeft') setSel(p => p !== null && p > 0 ? p - 1 : p);
|
||||||
);
|
|
||||||
if (e.key === 'ArrowLeft') setSelectedIndex(prev =>
|
|
||||||
prev !== null && prev > 0 ? prev - 1 : prev
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
window.addEventListener('keydown', handleKey);
|
window.addEventListener('keydown', handle);
|
||||||
return () => window.removeEventListener('keydown', handleKey);
|
return () => window.removeEventListener('keydown', handle);
|
||||||
}, [selectedIndex, images.length]);
|
}, [sel, images.length]);
|
||||||
|
|
||||||
// Lock body scroll when lightbox open
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.body.style.overflow = selectedIndex !== null ? 'hidden' : '';
|
document.body.style.overflow = sel !== null ? 'hidden' : '';
|
||||||
return () => { document.body.style.overflow = ''; };
|
return () => { document.body.style.overflow = ''; };
|
||||||
}, [selectedIndex]);
|
}, [sel]);
|
||||||
|
|
||||||
const flatIndex = (colIdx: number, rowIdx: number) => {
|
const flatIdx = (colIdx: number, rowIdx: number) => rowIdx * cols + colIdx;
|
||||||
return rowIdx * columns + colIdx;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div ref={containerRef} style={gridContainerStyle(columns)}>
|
<div style={{ display: 'grid', gridTemplateColumns: `repeat(${cols}, 1fr)`, gap: '4px' }}>
|
||||||
{getColumns().map((col, colIdx) => (
|
{getCols().map((col, ci) => (
|
||||||
<div key={colIdx} style={columnStyle}>
|
<div key={ci} style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
||||||
{col.map((img, rowIdx) => {
|
{col.map((img, ri) => {
|
||||||
const idx = flatIndex(colIdx, rowIdx);
|
const idx = flatIdx(ci, ri);
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={idx}
|
key={idx}
|
||||||
initial={{ opacity: 0, y: 30 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1 }}
|
||||||
transition={{ delay: idx * 0.06, duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
|
transition={{ delay: idx * 0.04, duration: 0.5 }}
|
||||||
style={imageWrapperStyle}
|
style={{ cursor: 'pointer', overflow: 'hidden' }}
|
||||||
onClick={() => setSelectedIndex(idx)}
|
onClick={() => setSel(idx)}
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
aria-label={`Voir ${img.alt}`}
|
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={img.src}
|
src={img.src}
|
||||||
alt={img.alt}
|
alt={img.alt}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
style={imageStyle}
|
style={{
|
||||||
onMouseEnter={(e) => {
|
width: '100%',
|
||||||
(e.target as HTMLImageElement).style.transform = 'scale(1.03)';
|
height: 'auto',
|
||||||
}}
|
display: 'block',
|
||||||
onMouseLeave={(e) => {
|
transition: 'transform 0.6s cubic-bezier(0.16,1,0.3,1)',
|
||||||
(e.target as HTMLImageElement).style.transform = 'scale(1)';
|
|
||||||
}}
|
}}
|
||||||
|
onMouseEnter={e => { (e.target as HTMLImageElement).style.transform = 'scale(1.02)'; }}
|
||||||
|
onMouseLeave={e => { (e.target as HTMLImageElement).style.transform = 'scale(1)'; }}
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
@ -103,53 +85,70 @@ export default function MasonryGallery({ images }: Props) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Lightbox */}
|
{/* White lightbox */}
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{selectedIndex !== null && (
|
{sel !== null && (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
transition={{ duration: 0.3 }}
|
transition={{ duration: 0.25 }}
|
||||||
style={lightboxStyle}
|
style={{
|
||||||
onClick={() => setSelectedIndex(null)}
|
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
|
<button
|
||||||
style={closeButtonStyle}
|
onClick={() => setSel(null)}
|
||||||
onClick={() => setSelectedIndex(null)}
|
|
||||||
aria-label="Fermer"
|
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="4" y1="4" x2="20" y2="20" />
|
||||||
<line x1="20" y1="4" x2="4" y2="20" />
|
<line x1="20" y1="4" x2="4" y2="20" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Counter */}
|
{/* Counter */}
|
||||||
<div style={counterStyle}>
|
<div style={{
|
||||||
{selectedIndex + 1} / {images.length}
|
position: 'absolute', top: '2rem', left: '50%', transform: 'translateX(-50%)',
|
||||||
|
fontSize: '0.7rem', letterSpacing: '0.1em', color: '#999', zIndex: 201,
|
||||||
|
}}>
|
||||||
|
{sel + 1} / {images.length}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Navigation arrows */}
|
{/* Arrows */}
|
||||||
{selectedIndex > 0 && (
|
{sel > 0 && (
|
||||||
<button
|
<button
|
||||||
style={{ ...arrowStyle, left: '2rem' }}
|
onClick={e => { e.stopPropagation(); setSel(sel - 1); }}
|
||||||
onClick={(e) => { e.stopPropagation(); setSelectedIndex(selectedIndex - 1); }}
|
aria-label="Précédent"
|
||||||
aria-label="Photo précédente"
|
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" />
|
<polyline points="15,4 7,12 15,20" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{selectedIndex < images.length - 1 && (
|
{sel < images.length - 1 && (
|
||||||
<button
|
<button
|
||||||
style={{ ...arrowStyle, right: '2rem' }}
|
onClick={e => { e.stopPropagation(); setSel(sel + 1); }}
|
||||||
onClick={(e) => { e.stopPropagation(); setSelectedIndex(selectedIndex + 1); }}
|
aria-label="Suivant"
|
||||||
aria-label="Photo suivante"
|
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" />
|
<polyline points="9,4 17,12 9,20" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
@ -157,15 +156,15 @@ export default function MasonryGallery({ images }: Props) {
|
|||||||
|
|
||||||
{/* Image */}
|
{/* Image */}
|
||||||
<motion.img
|
<motion.img
|
||||||
key={selectedIndex}
|
key={sel}
|
||||||
src={images[selectedIndex].src}
|
src={images[sel].src}
|
||||||
alt={images[selectedIndex].alt}
|
alt={images[sel].alt}
|
||||||
initial={{ opacity: 0, scale: 0.95 }}
|
initial={{ opacity: 0, scale: 0.97 }}
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
exit={{ opacity: 0, scale: 0.95 }}
|
exit={{ opacity: 0, scale: 0.97 }}
|
||||||
transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1] }}
|
transition={{ duration: 0.25 }}
|
||||||
style={lightboxImageStyle}
|
style={{ maxWidth: '88vw', maxHeight: '88vh', objectFit: 'contain', cursor: 'default' }}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={e => e.stopPropagation()}
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</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',
|
|
||||||
};
|
|
||||||
|
|||||||
91
src/components/ProcessTimeline.astro
Normal file
91
src/components/ProcessTimeline.astro
Normal 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>
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useRef } from 'react';
|
||||||
import { motion, useInView } from 'motion/react';
|
import { motion, useInView } from 'motion/react';
|
||||||
|
|
||||||
interface ProcessStep {
|
interface ProcessStep {
|
||||||
@ -13,38 +13,77 @@ interface Props {
|
|||||||
steps: ProcessStep[];
|
steps: ProcessStep[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function StepCard({ step, index }: { step: ProcessStep; index: number }) {
|
function Step({ step, index }: { step: ProcessStep; index: number }) {
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const isInView = useInView(ref, { once: true, margin: '-100px 0px' });
|
const inView = useInView(ref, { once: true, margin: '-80px 0px' });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
initial={{ opacity: 0, y: 60 }}
|
initial={{ opacity: 0, y: 40 }}
|
||||||
animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 60 }}
|
animate={inView ? { opacity: 1, y: 0 } : {}}
|
||||||
transition={{ duration: 0.8, delay: 0.1, ease: [0.16, 1, 0.3, 1] }}
|
transition={{ duration: 0.7, delay: 0.05, ease: [0.16, 1, 0.3, 1] }}
|
||||||
style={stepStyle}
|
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}>
|
<span style={{
|
||||||
<div style={stepNumberContainerStyle}>
|
fontFamily: "'DM Serif Display', Georgia, serif",
|
||||||
<motion.span
|
fontSize: '1.6rem',
|
||||||
initial={{ scale: 0 }}
|
color: '#ccc',
|
||||||
animate={isInView ? { scale: 1 } : { scale: 0 }}
|
lineHeight: 1,
|
||||||
transition={{ delay: 0.2, duration: 0.5, ease: [0.16, 1, 0.3, 1] }}
|
paddingTop: '0.15rem',
|
||||||
style={stepNumberStyle}
|
}}>
|
||||||
>
|
{String(step.stepNumber).padStart(2, '0')}
|
||||||
{String(step.stepNumber).padStart(2, '0')}
|
</span>
|
||||||
</motion.span>
|
|
||||||
{index < 4 && <div style={stepLineStyle} />}
|
<div>
|
||||||
</div>
|
<span style={{
|
||||||
</div>
|
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 && (
|
{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>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@ -52,187 +91,11 @@ function StepCard({ step, index }: { step: ProcessStep; index: number }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function ProcessTimeline({ steps }: Props) {
|
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 (
|
return (
|
||||||
<div style={containerStyle}>
|
<div style={{ maxWidth: '700px' }}>
|
||||||
{/* Progress indicator */}
|
{steps.map((step, i) => (
|
||||||
<div style={progressBarContainerStyle}>
|
<Step key={step.stepNumber} step={step} index={i} />
|
||||||
<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>
|
</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',
|
|
||||||
};
|
|
||||||
|
|||||||
@ -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>
|
|
||||||
35
src/components/components/Footer.astro
Normal file
35
src/components/components/Footer.astro
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
const year = new Date().getFullYear();
|
||||||
|
---
|
||||||
|
|
||||||
|
<footer class="ft pad">
|
||||||
|
<div class="ft__row">
|
||||||
|
<span class="ft__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>
|
||||||
154
src/components/components/Header.astro
Normal file
154
src/components/components/Header.astro
Normal 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>
|
||||||
142
src/components/components/MasonryGallery.tsx
Normal file
142
src/components/components/MasonryGallery.tsx
Normal 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,
|
||||||
|
};
|
||||||
98
src/components/components/ProcessTimeline.tsx
Normal file
98
src/components/components/ProcessTimeline.tsx
Normal 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',
|
||||||
|
};
|
||||||
@ -6,13 +6,11 @@ import '../styles/global.css';
|
|||||||
interface Props {
|
interface Props {
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
image?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
title = 'Aurélie Barré — Design d\'intérieur & Création événementielle',
|
title = 'Aurélie Barré — Directrice artistique',
|
||||||
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.',
|
description = 'Aurélie Barré, directrice artistique & designeuse d\'intérieur. Design d\'expérience, scénographie événementielle, aménagement pérenne.',
|
||||||
image = '/og-image.jpg'
|
|
||||||
} = Astro.props;
|
} = Astro.props;
|
||||||
|
|
||||||
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
|
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="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="description" content={description} />
|
<meta name="description" content={description} />
|
||||||
<link rel="canonical" href={canonicalURL} />
|
<link rel="canonical" href={canonicalURL} />
|
||||||
|
|
||||||
<!-- Open Graph -->
|
|
||||||
<meta property="og:title" content={title} />
|
<meta property="og:title" content={title} />
|
||||||
<meta property="og:description" content={description} />
|
<meta property="og:description" content={description} />
|
||||||
<meta property="og:image" content={image} />
|
|
||||||
<meta property="og:url" content={canonicalURL} />
|
<meta property="og:url" content={canonicalURL} />
|
||||||
<meta property="og:type" content="website" />
|
<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.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<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 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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<Header />
|
<Header />
|
||||||
|
<main>
|
||||||
<main class="page-enter">
|
|
||||||
<slot />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Intersection Observer for scroll reveal animations
|
// Reveal on scroll
|
||||||
const observerCallback: IntersectionObserverCallback = (entries) => {
|
const io = new IntersectionObserver((entries) => {
|
||||||
entries.forEach((entry) => {
|
entries.forEach(e => { if (e.isIntersecting) e.target.classList.add('vis'); });
|
||||||
if (entry.isIntersecting) {
|
}, { threshold: 0.08, rootMargin: '0px 0px -40px 0px' });
|
||||||
entry.target.classList.add('visible');
|
document.querySelectorAll('.rv').forEach(el => io.observe(el));
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const observer = new IntersectionObserver(observerCallback, {
|
|
||||||
threshold: 0.1,
|
|
||||||
rootMargin: '0px 0px -50px 0px'
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelectorAll('.reveal').forEach((el) => {
|
|
||||||
observer.observe(el);
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
<style>
|
|
||||||
main {
|
|
||||||
min-height: calc(100vh - var(--header-height));
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
52
src/layouts/layouts/BaseLayout.astro
Normal file
52
src/layouts/layouts/BaseLayout.astro
Normal 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>
|
||||||
@ -1,234 +1,186 @@
|
|||||||
---
|
---
|
||||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
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';
|
import { getMockProjects, getMockProjectBySlug } from '../../lib/mock-data';
|
||||||
|
|
||||||
export function getStaticPaths() {
|
export function getStaticPaths() {
|
||||||
const projects = getMockProjects('event');
|
return getMockProjects('event').map(p => ({ params: { slug: p.slug.current } }));
|
||||||
return projects.map(p => ({ params: { slug: p.slug.current } }));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { slug } = Astro.params;
|
const { slug } = Astro.params;
|
||||||
const project = getMockProjectBySlug(slug!);
|
const project = getMockProjectBySlug(slug!);
|
||||||
|
if (!project) return Astro.redirect('/creations-evenement');
|
||||||
|
|
||||||
if (!project) {
|
const date = new Date(project.date).toLocaleDateString('fr-FR', { year: 'numeric', month: 'long' });
|
||||||
return Astro.redirect('/creations-evenement');
|
|
||||||
}
|
|
||||||
|
|
||||||
const dateFormatted = new Date(project.date).toLocaleDateString('fr-FR', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
});
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout title={`${project.title} — Aurélie Barré`} description={project.description}>
|
<BaseLayout title={`${project.title} — Aurélie Barré`} description={project.description}>
|
||||||
<!-- Hero Image -->
|
<!-- Full-bleed hero -->
|
||||||
<section class="project-hero">
|
<section class="phero">
|
||||||
<div class="project-hero__image">
|
<div class="phero__img">
|
||||||
<img src={project.heroImage} alt={project.title} />
|
<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>
|
||||||
|
<div class="phero__overlay"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Project Info -->
|
<!-- Info -->
|
||||||
<section class="section">
|
<section class="pinfo">
|
||||||
<div class="container">
|
<div class="wrap">
|
||||||
<div class="project-meta reveal">
|
<div class="pinfo__top rv">
|
||||||
<div class="project-meta__item">
|
<h1 class="pinfo__title">{project.title}</h1>
|
||||||
<span class="project-meta__label">Client</span>
|
<div class="pinfo__meta">
|
||||||
<span class="project-meta__value">{project.client}</span>
|
<div class="pinfo__col">
|
||||||
</div>
|
<span class="label">client</span>
|
||||||
<div class="project-meta__item">
|
<span class="pinfo__val">{project.client}</span>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div class="pinfo__col">
|
||||||
<div class="project-meta__item">
|
<span class="label">date</span>
|
||||||
<span class="project-meta__label">Type</span>
|
<span class="pinfo__val">{date}</span>
|
||||||
<span class="project-meta__value">Création événementielle</span>
|
</div>
|
||||||
|
{project.location && (
|
||||||
|
<div class="pinfo__col">
|
||||||
|
<span class="label">lieu</span>
|
||||||
|
<span class="pinfo__val">{project.location}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="project-description reveal">
|
<p class="pinfo__desc rv" style="--d:2">{project.description}</p>
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Gallery (Masonry) -->
|
<!-- Gallery -->
|
||||||
{project.gallery && project.gallery.length > 0 && (
|
{project.gallery && project.gallery.length > 0 && (
|
||||||
<section class="section" style="background-color: var(--color-bg-alt);">
|
<section class="pgallery">
|
||||||
<div class="container">
|
<div class="wrap">
|
||||||
<MasonryGallery
|
<MasonryGallery
|
||||||
client:visible
|
images={project.gallery.map(img => ({ src: img.src, alt: img.alt }))}
|
||||||
images={project.gallery.map(img => ({
|
|
||||||
src: img.src,
|
|
||||||
alt: img.alt,
|
|
||||||
size: img.size,
|
|
||||||
}))}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<!-- Back link -->
|
<!-- Nav -->
|
||||||
<section class="section">
|
<section class="pnav">
|
||||||
<div class="container">
|
<div class="wrap">
|
||||||
<a href="/creations-evenement" class="back-link">
|
<a href="/creations-evenement" class="pnav__link">
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" style="margin-right: 8px;">
|
<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.2"/>
|
<path d="M13 8H3M3 8L7 4M3 8L7 12" stroke="currentColor" stroke-width="1"/>
|
||||||
</svg>
|
</svg>
|
||||||
Retour aux créations événement
|
retour
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Hero */
|
.phero {
|
||||||
.project-hero {
|
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 70vh;
|
height: 75vh;
|
||||||
min-height: 500px;
|
min-height: 500px;
|
||||||
display: flex;
|
|
||||||
align-items: flex-end;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-hero__image {
|
.phero__img {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-hero__image img {
|
.phero__img img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-hero__overlay {
|
.phero__overlay {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background: linear-gradient(
|
background: linear-gradient(to bottom, rgba(0,0,0,0.08) 0%, rgba(0,0,0,0.02) 100%);
|
||||||
to top,
|
|
||||||
rgba(0, 0, 0, 0.5) 0%,
|
|
||||||
rgba(0, 0, 0, 0.1) 50%,
|
|
||||||
rgba(0, 0, 0, 0) 100%
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-hero__content {
|
/* Info */
|
||||||
position: relative;
|
.pinfo {
|
||||||
z-index: 1;
|
padding: 4rem 0 3rem;
|
||||||
padding-bottom: var(--space-2xl);
|
|
||||||
color: white;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-hero__client {
|
.pinfo__top {
|
||||||
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 {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--space-sm);
|
justify-content: space-between;
|
||||||
flex-wrap: wrap;
|
align-items: flex-start;
|
||||||
|
gap: 2rem;
|
||||||
|
padding-bottom: 2.5rem;
|
||||||
|
border-bottom: 1px solid var(--border-light);
|
||||||
|
margin-bottom: 2.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-tag {
|
.pinfo__title {
|
||||||
font-size: 0.75rem;
|
font-size: clamp(1.8rem, 4vw, 3rem);
|
||||||
letter-spacing: 0.04em;
|
max-width: 55%;
|
||||||
color: var(--color-text-light);
|
line-height: 1.1;
|
||||||
padding: 4px 12px;
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Back link */
|
.pinfo__meta {
|
||||||
.back-link {
|
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;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 0.85rem;
|
gap: 0.5rem;
|
||||||
letter-spacing: 0.04em;
|
font-size: 0.7rem;
|
||||||
color: var(--color-text-secondary);
|
letter-spacing: 0.08em;
|
||||||
|
color: var(--text-3);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition: color var(--duration-fast);
|
transition: color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.back-link:hover {
|
.pnav__link:hover { color: var(--text); }
|
||||||
color: var(--color-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.project-meta {
|
.phero { height: 55vh; }
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
|
.pinfo__top {
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-hero {
|
.pinfo__title {
|
||||||
height: 50vh;
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pinfo__meta {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,81 +1,148 @@
|
|||||||
---
|
---
|
||||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||||
import ProjectCard from '../../components/ProjectCard.astro';
|
|
||||||
import { getMockProjects } from '../../lib/mock-data';
|
import { getMockProjects } from '../../lib/mock-data';
|
||||||
|
|
||||||
const projects = getMockProjects('event');
|
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.">
|
<BaseLayout title="Créations événement — Aurélie Barré">
|
||||||
<section class="page-hero">
|
<section class="listing">
|
||||||
<div class="container">
|
<div class="wrap">
|
||||||
<span class="page-hero__label">Portfolio</span>
|
<div class="listing__head rv">
|
||||||
<h1>Créations<br /><em>Événement</em></h1>
|
<span class="label">événement</span>
|
||||||
<p class="page-hero__desc text-secondary">
|
<h1>créations<br />événement</h1>
|
||||||
Scénographies éphémères, pop-ups, lancements, conventions —
|
</div>
|
||||||
chaque événement est une expérience unique conçue sur mesure.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="section">
|
<div class="listing__grid">
|
||||||
<div class="container">
|
{projects.map((project, i) => {
|
||||||
<div class="projects-grid">
|
const href = `/creations-evenement/${project.slug.current}`;
|
||||||
{projects.map((project, index) => (
|
// Every 3rd item is full-width
|
||||||
<ProjectCard
|
const isFull = i % 3 === 0;
|
||||||
title={project.title}
|
return (
|
||||||
client={project.client}
|
<a
|
||||||
slug={project.slug.current}
|
href={href}
|
||||||
category={project.category}
|
class:list={['listing__item', 'rv', { 'is-full': isFull }]}
|
||||||
heroImage={project.heroImage}
|
style={`--d:${(i % 4) + 1}`}
|
||||||
description={project.description}
|
>
|
||||||
tags={project.tags}
|
<div class="listing__img">
|
||||||
index={index}
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.page-hero {
|
.listing {
|
||||||
padding-top: calc(var(--header-height) + var(--space-3xl));
|
padding-top: calc(var(--header-h) + 6rem);
|
||||||
padding-bottom: var(--space-xl);
|
padding-bottom: 6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-hero__label {
|
.listing__head {
|
||||||
font-size: 0.75rem;
|
margin-bottom: 4rem;
|
||||||
letter-spacing: 0.12em;
|
}
|
||||||
text-transform: uppercase;
|
|
||||||
color: var(--color-text-light);
|
.listing__head .label {
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: var(--space-md);
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-hero h1 {
|
.listing__head h1 {
|
||||||
margin-bottom: var(--space-lg);
|
font-size: clamp(2.2rem, 5vw, 4rem);
|
||||||
|
text-transform: lowercase;
|
||||||
|
line-height: 1.05;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-hero h1 em {
|
/* Asymmetric grid */
|
||||||
color: var(--color-accent);
|
.listing__grid {
|
||||||
}
|
|
||||||
|
|
||||||
.page-hero__desc {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
line-height: 1.8;
|
|
||||||
max-width: 600px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.projects-grid {
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: 1fr 1fr;
|
||||||
gap: var(--space-xl) var(--space-lg);
|
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) {
|
@media (max-width: 768px) {
|
||||||
.projects-grid {
|
.listing__grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.listing__item:not(.is-full) .listing__img {
|
||||||
|
aspect-ratio: 4 / 3;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,249 +1,494 @@
|
|||||||
---
|
---
|
||||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||||
import ProjectCard from '../components/ProjectCard.astro';
|
import { getMockFeaturedProjects, getMockProjects } from '../lib/mock-data';
|
||||||
import { getMockFeaturedProjects } from '../lib/mock-data';
|
|
||||||
|
|
||||||
const featured = getMockFeaturedProjects();
|
const featured = getMockFeaturedProjects();
|
||||||
const events = featured.filter(p => p.category === 'event');
|
const allProjects = getMockProjects();
|
||||||
const perennes = featured.filter(p => p.category === 'perenne');
|
// 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>
|
<BaseLayout>
|
||||||
<!-- Hero Section -->
|
<!-- ====== HERO: full-viewport image with name overlay ====== -->
|
||||||
<section class="hero">
|
<section class="hero">
|
||||||
<div class="container">
|
<div class="hero__img">
|
||||||
<div class="hero__content">
|
<img src="/images/events/dior/01.jpeg" alt="Dior Christmas" />
|
||||||
<h1 class="hero__title">
|
</div>
|
||||||
<span class="hero__title-line">Design</span>
|
<div class="hero__content">
|
||||||
<span class="hero__title-line hero__title-accent">d'expérience</span>
|
<h1 class="hero__name">
|
||||||
</h1>
|
<span class="hero__line">Aurélie</span>
|
||||||
<p class="hero__subtitle">
|
<span class="hero__line">Barré</span>
|
||||||
Aurélie Barré — Directrice artistique & designeuse d'intérieur.<br />
|
</h1>
|
||||||
Du concept à la réalisation, chaque espace raconte une histoire.
|
<p class="hero__role">directrice artistique & design d'intérieur</p>
|
||||||
</p>
|
</div>
|
||||||
<div class="hero__cta">
|
<div class="hero__scroll">
|
||||||
<a href="/processus" class="hero__link">
|
<span>scroll</span>
|
||||||
Découvrir mon processus
|
<div class="hero__scroll-line"></div>
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" style="margin-left: 8px;">
|
</div>
|
||||||
<path d="M3 8H13M13 8L9 4M13 8L9 12" stroke="currentColor" stroke-width="1.2"/>
|
</section>
|
||||||
</svg>
|
|
||||||
</a>
|
<!-- ====== FEATURED: hover-reveal image grid ====== -->
|
||||||
</div>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Featured Events -->
|
<!-- ====== PROJECT INDEX: text list with hover image ====== -->
|
||||||
<section class="section">
|
<section class="idx">
|
||||||
<div class="container">
|
<div class="wrap">
|
||||||
<div class="section-header reveal">
|
<div class="idx__head">
|
||||||
<span class="section-label">Sélection</span>
|
<span class="label rv">tous les projets</span>
|
||||||
<h2>Créations Événement</h2>
|
</div>
|
||||||
<a href="/creations-evenement" class="section-link">
|
|
||||||
Voir tous les projets
|
<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">
|
<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"/>
|
<path d="M3 8H13M13 8L9 4M13 8L9 12" stroke="currentColor" stroke-width="1"/>
|
||||||
</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"/>
|
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Hover image follower -->
|
||||||
|
<div class="cursor-img" id="cursor-img">
|
||||||
|
<img src="" alt="" id="cursor-img-el" />
|
||||||
|
</div>
|
||||||
</BaseLayout>
|
</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>
|
<style>
|
||||||
/* Hero */
|
/* ---- Hero ---- */
|
||||||
.hero {
|
.hero {
|
||||||
min-height: 90vh;
|
position: relative;
|
||||||
display: flex;
|
height: 100vh;
|
||||||
align-items: center;
|
overflow: hidden;
|
||||||
padding-top: var(--header-height);
|
}
|
||||||
|
|
||||||
|
.hero__img {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero__img img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
filter: brightness(0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero__content {
|
.hero__content {
|
||||||
max-width: 800px;
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 0 var(--pad) 8vh;
|
||||||
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero__title {
|
.hero__name {
|
||||||
margin-bottom: var(--space-lg);
|
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;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero__title-accent {
|
.hero__role {
|
||||||
font-style: italic;
|
font-family: var(--sans);
|
||||||
color: var(--color-accent);
|
font-size: 0.7rem;
|
||||||
}
|
font-weight: 300;
|
||||||
|
|
||||||
.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;
|
|
||||||
letter-spacing: 0.12em;
|
letter-spacing: 0.12em;
|
||||||
text-transform: uppercase;
|
color: rgba(255,255,255,0.65);
|
||||||
color: var(--color-text-light);
|
max-width: none;
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-header h2 {
|
.hero__scroll {
|
||||||
margin-right: auto;
|
position: absolute;
|
||||||
}
|
bottom: 3vh;
|
||||||
|
right: var(--pad);
|
||||||
.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 {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--space-md);
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.process-teaser__step {
|
.hero__scroll span {
|
||||||
font-family: var(--font-serif);
|
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;
|
font-size: 1.1rem;
|
||||||
color: var(--color-text-secondary);
|
line-height: 1.3;
|
||||||
padding-bottom: var(--space-md);
|
}
|
||||||
border-bottom: 1px solid var(--color-border);
|
|
||||||
|
.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>
|
</style>
|
||||||
|
|||||||
@ -1,117 +1,349 @@
|
|||||||
---
|
---
|
||||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||||
import ProcessTimeline from '../components/ProcessTimeline.tsx';
|
import ProcessTimeline from '../components/ProcessTimeline.astro';
|
||||||
import { mockProcessSteps } from '../lib/mock-data';
|
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.">
|
<BaseLayout title="Processus créatif — Aurélie Barré">
|
||||||
<section class="page-hero">
|
|
||||||
<div class="container">
|
<!-- ====== Hero: full-width photo with title ====== -->
|
||||||
<span class="page-hero__label">Méthode</span>
|
<section class="proc-hero">
|
||||||
<h1>Processus<br /><em>Créatif</em></h1>
|
<div class="proc-hero__img">
|
||||||
<p class="page-hero__desc text-secondary">
|
<img src="/images/events/alula/03.jpg" alt="Processus créatif" />
|
||||||
Chaque projet suit un parcours rigoureux en 5 étapes,
|
</div>
|
||||||
de la première rencontre à la livraison finale.
|
<div class="proc-hero__overlay"></div>
|
||||||
Une méthode éprouvée pour garantir un résultat à la hauteur de vos attentes.
|
<div class="proc-hero__content">
|
||||||
</p>
|
<span class="proc-hero__label">méthode</span>
|
||||||
|
<h1>processus<br />créatif</h1>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="section process-section">
|
<!-- ====== Intro: editorial text ====== -->
|
||||||
<div class="container">
|
<section class="proc-intro">
|
||||||
<ProcessTimeline client:visible steps={mockProcessSteps} />
|
<div class="wrap">
|
||||||
</div>
|
<div class="proc-intro__grid">
|
||||||
</section>
|
<div class="proc-intro__left rv">
|
||||||
|
<span class="label">approche</span>
|
||||||
<!-- CTA -->
|
</div>
|
||||||
<section class="section cta-section">
|
<div class="proc-intro__right rv" style="--d:1">
|
||||||
<div class="container">
|
<p class="proc-intro__text">
|
||||||
<div class="cta-inner reveal">
|
Chaque projet commence par une conversation. De la première rencontre
|
||||||
<h2>Un projet en tête ?</h2>
|
à la livraison — un parcours en cinq étapes, pensé pour que le résultat
|
||||||
<p class="text-secondary">
|
soit toujours à la hauteur de l'intention.
|
||||||
Discutons de vos envies autour d'un café. Chaque projet commence par une conversation.
|
</p>
|
||||||
</p>
|
<p class="proc-intro__sub">
|
||||||
<a href="mailto:contact@aureliebarre.fr" class="cta-link">
|
Cette méthode s'est construite au fil des projets, avec des clients
|
||||||
Prendre contact
|
comme Dior, LVMH, Moët & Chandon ou Cisco. Elle s'adapte à chaque
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" style="margin-left: 8px;">
|
contexte — événement éphémère ou aménagement pérenne.
|
||||||
<path d="M3 8H13M13 8L9 4M13 8L9 12" stroke="currentColor" stroke-width="1.2"/>
|
</p>
|
||||||
</svg>
|
</div>
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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>
|
</BaseLayout>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.page-hero {
|
/* ---- Hero ---- */
|
||||||
padding-top: calc(var(--header-height) + var(--space-3xl));
|
.proc-hero {
|
||||||
padding-bottom: var(--space-xl);
|
position: relative;
|
||||||
|
height: 65vh;
|
||||||
|
min-height: 420px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-hero__label {
|
.proc-hero__img {
|
||||||
font-size: 0.75rem;
|
position: absolute;
|
||||||
letter-spacing: 0.12em;
|
inset: 0;
|
||||||
text-transform: uppercase;
|
}
|
||||||
color: var(--color-text-light);
|
|
||||||
|
.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;
|
display: block;
|
||||||
margin-bottom: var(--space-md);
|
margin-bottom: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-hero h1 {
|
.proc-hero__content h1 {
|
||||||
margin-bottom: var(--space-lg);
|
font-size: clamp(2.5rem, 7vw, 5.5rem);
|
||||||
|
color: #fff;
|
||||||
|
line-height: 1;
|
||||||
|
text-transform: lowercase;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-hero h1 em {
|
/* ---- Intro ---- */
|
||||||
color: var(--color-accent);
|
.proc-intro {
|
||||||
|
padding: 6rem 0 4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-hero__desc {
|
.proc-intro__grid {
|
||||||
font-size: 1.1rem;
|
display: grid;
|
||||||
line-height: 1.8;
|
grid-template-columns: 1fr 2fr;
|
||||||
max-width: 600px;
|
gap: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.process-section {
|
.proc-intro__left {
|
||||||
padding-top: var(--space-xl);
|
padding-top: 0.3rem;
|
||||||
padding-bottom: var(--space-3xl);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* CTA */
|
.proc-intro__text {
|
||||||
.cta-section {
|
font-family: var(--serif);
|
||||||
background-color: var(--color-bg-alt);
|
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 {
|
.proc-intro__sub {
|
||||||
text-align: center;
|
font-size: 0.9rem;
|
||||||
max-width: 500px;
|
line-height: 1.75;
|
||||||
margin: 0 auto;
|
color: var(--text-2);
|
||||||
|
max-width: 50ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cta-inner h2 {
|
/* ---- Steps ---- */
|
||||||
margin-bottom: var(--space-md);
|
.proc-steps {
|
||||||
|
padding: 2rem 0 6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cta-inner p {
|
/* ---- Visual break ---- */
|
||||||
margin: 0 auto var(--space-xl);
|
.proc-visual {
|
||||||
font-size: 1.05rem;
|
padding: 0 0 6rem;
|
||||||
line-height: 1.8;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.cta-link {
|
.proc-visual__grid {
|
||||||
display: inline-flex;
|
display: grid;
|
||||||
align-items: center;
|
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;
|
font-size: 0.85rem;
|
||||||
letter-spacing: 0.08em;
|
line-height: 1.7;
|
||||||
text-transform: uppercase;
|
color: var(--text-2);
|
||||||
color: var(--color-accent);
|
|
||||||
text-decoration: none;
|
|
||||||
padding: 12px 32px;
|
|
||||||
border: 1px solid var(--color-accent);
|
|
||||||
transition: all var(--duration-fast) var(--ease-out);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.cta-link:hover {
|
/* ---- CTA ---- */
|
||||||
background-color: var(--color-accent);
|
.proc-cta {
|
||||||
color: white;
|
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>
|
</style>
|
||||||
|
|||||||
@ -1,206 +1,137 @@
|
|||||||
---
|
---
|
||||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
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';
|
import { getMockProjects, getMockProjectBySlug } from '../../lib/mock-data';
|
||||||
|
|
||||||
export function getStaticPaths() {
|
export function getStaticPaths() {
|
||||||
const projects = getMockProjects('perenne');
|
return getMockProjects('perenne').map(p => ({ params: { slug: p.slug.current } }));
|
||||||
return projects.map(p => ({ params: { slug: p.slug.current } }));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { slug } = Astro.params;
|
const { slug } = Astro.params;
|
||||||
const project = getMockProjectBySlug(slug!);
|
const project = getMockProjectBySlug(slug!);
|
||||||
|
if (!project) return Astro.redirect('/realisations-perennes');
|
||||||
|
|
||||||
if (!project) {
|
const date = new Date(project.date).toLocaleDateString('fr-FR', { year: 'numeric', month: 'long' });
|
||||||
return Astro.redirect('/realisations-perennes');
|
|
||||||
}
|
|
||||||
|
|
||||||
const dateFormatted = new Date(project.date).toLocaleDateString('fr-FR', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
});
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout title={`${project.title} — Aurélie Barré`} description={project.description}>
|
<BaseLayout title={`${project.title} — Aurélie Barré`} description={project.description}>
|
||||||
<!-- Hero Image -->
|
<section class="phero">
|
||||||
<section class="project-hero">
|
<div class="phero__img">
|
||||||
<div class="project-hero__image">
|
|
||||||
<img src={project.heroImage} alt={project.title} />
|
<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>
|
||||||
|
<div class="phero__overlay"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Project Info -->
|
<section class="pinfo">
|
||||||
<section class="section">
|
<div class="wrap">
|
||||||
<div class="container">
|
<div class="pinfo__top rv">
|
||||||
<div class="project-meta reveal">
|
<h1 class="pinfo__title">{project.title}</h1>
|
||||||
<div class="project-meta__item">
|
<div class="pinfo__meta">
|
||||||
<span class="project-meta__label">Client</span>
|
<div class="pinfo__col">
|
||||||
<span class="project-meta__value">{project.client}</span>
|
<span class="label">client</span>
|
||||||
</div>
|
<span class="pinfo__val">{project.client}</span>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div class="pinfo__col">
|
||||||
<div class="project-meta__item">
|
<span class="label">date</span>
|
||||||
<span class="project-meta__label">Type</span>
|
<span class="pinfo__val">{date}</span>
|
||||||
<span class="project-meta__value">Réalisation pérenne</span>
|
</div>
|
||||||
|
{project.location && (
|
||||||
|
<div class="pinfo__col">
|
||||||
|
<span class="label">lieu</span>
|
||||||
|
<span class="pinfo__val">{project.location}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="pinfo__desc rv" style="--d:2">{project.description}</p>
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Gallery (Masonry) -->
|
|
||||||
{project.gallery && project.gallery.length > 0 && (
|
{project.gallery && project.gallery.length > 0 && (
|
||||||
<section class="section" style="background-color: var(--color-bg-alt);">
|
<section class="pgallery">
|
||||||
<div class="container">
|
<div class="wrap">
|
||||||
<MasonryGallery
|
<MasonryGallery
|
||||||
client:visible
|
images={project.gallery.map(img => ({ src: img.src, alt: img.alt }))}
|
||||||
images={project.gallery.map(img => ({
|
|
||||||
src: img.src,
|
|
||||||
alt: img.alt,
|
|
||||||
size: img.size,
|
|
||||||
}))}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<!-- Back link -->
|
<section class="pnav">
|
||||||
<section class="section">
|
<div class="wrap">
|
||||||
<div class="container">
|
<a href="/realisations-perennes" class="pnav__link">
|
||||||
<a href="/realisations-perennes" class="back-link">
|
<svg width="14" height="14" viewBox="0 0 16 16" fill="none">
|
||||||
<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"/>
|
||||||
<path d="M13 8H3M3 8L7 4M3 8L7 12" stroke="currentColor" stroke-width="1.2"/>
|
|
||||||
</svg>
|
</svg>
|
||||||
Retour aux réalisations pérennes
|
retour
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.project-hero {
|
.phero {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 70vh;
|
height: 75vh;
|
||||||
min-height: 500px;
|
min-height: 500px;
|
||||||
display: flex;
|
|
||||||
align-items: flex-end;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-hero__image {
|
.phero__img { position: absolute; inset: 0; }
|
||||||
position: absolute;
|
.phero__img img { width: 100%; height: 100%; object-fit: cover; }
|
||||||
inset: 0;
|
.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 {
|
.pinfo { padding: 4rem 0 3rem; }
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
.pinfo__top {
|
||||||
object-fit: cover;
|
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 {
|
.pinfo__title {
|
||||||
position: absolute;
|
font-size: clamp(1.8rem, 4vw, 3rem);
|
||||||
inset: 0;
|
max-width: 55%;
|
||||||
background: linear-gradient(to top, rgba(0,0,0,0.5) 0%, rgba(0,0,0,0.1) 50%, transparent 100%);
|
line-height: 1.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-hero__content {
|
.pinfo__meta { display: flex; gap: 2.5rem; flex-shrink: 0; }
|
||||||
position: relative;
|
.pinfo__col { display: flex; flex-direction: column; gap: 0.35rem; }
|
||||||
z-index: 1;
|
.pinfo__val { font-size: 0.85rem; color: var(--text); }
|
||||||
padding-bottom: var(--space-2xl);
|
|
||||||
color: white;
|
.pinfo__desc {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
line-height: 1.75;
|
||||||
|
color: var(--text-2);
|
||||||
|
max-width: 650px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-hero__client {
|
.pgallery { padding: 2rem 0 6rem; }
|
||||||
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 {
|
.pnav { padding: 0 0 5rem; }
|
||||||
color: white;
|
.pnav__link {
|
||||||
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 {
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 0.85rem;
|
gap: 0.5rem;
|
||||||
color: var(--color-text-secondary);
|
font-size: 0.7rem;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: var(--text-3);
|
||||||
text-decoration: none;
|
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) {
|
@media (max-width: 768px) {
|
||||||
.project-meta { grid-template-columns: repeat(2, 1fr); }
|
.phero { height: 55vh; }
|
||||||
.project-hero { height: 50vh; }
|
.pinfo__top { flex-direction: column; }
|
||||||
|
.pinfo__title { max-width: 100%; }
|
||||||
|
.pinfo__meta { flex-wrap: wrap; gap: 1.5rem; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,81 +1,138 @@
|
|||||||
---
|
---
|
||||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||||
import ProjectCard from '../../components/ProjectCard.astro';
|
|
||||||
import { getMockProjects } from '../../lib/mock-data';
|
import { getMockProjects } from '../../lib/mock-data';
|
||||||
|
|
||||||
const projects = getMockProjects('perenne');
|
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.">
|
<BaseLayout title="Réalisations pérennes — Aurélie Barré">
|
||||||
<section class="page-hero">
|
<section class="listing">
|
||||||
<div class="container">
|
<div class="wrap">
|
||||||
<span class="page-hero__label">Portfolio</span>
|
<div class="listing__head rv">
|
||||||
<h1>Réalisations<br /><em>Pérennes</em></h1>
|
<span class="label">pérenne</span>
|
||||||
<p class="page-hero__desc text-secondary">
|
<h1>réalisations<br />pérennes</h1>
|
||||||
Bureaux, showrooms, espaces de vie — des aménagements durables
|
</div>
|
||||||
pensés pour sublimer chaque lieu au quotidien.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="section">
|
<div class="listing__grid">
|
||||||
<div class="container">
|
{projects.map((project, i) => {
|
||||||
<div class="projects-grid">
|
const href = `/realisations-perennes/${project.slug.current}`;
|
||||||
{projects.map((project, index) => (
|
return (
|
||||||
<ProjectCard
|
<a
|
||||||
title={project.title}
|
href={href}
|
||||||
client={project.client}
|
class="listing__item rv"
|
||||||
slug={project.slug.current}
|
style={`--d:${i + 1}`}
|
||||||
category={project.category}
|
>
|
||||||
heroImage={project.heroImage}
|
<div class="listing__img">
|
||||||
description={project.description}
|
<img src={project.heroImage} alt={project.title} loading="lazy" />
|
||||||
tags={project.tags}
|
</div>
|
||||||
index={index}
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.page-hero {
|
.listing {
|
||||||
padding-top: calc(var(--header-height) + var(--space-3xl));
|
padding-top: calc(var(--header-h) + 6rem);
|
||||||
padding-bottom: var(--space-xl);
|
padding-bottom: 6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-hero__label {
|
.listing__head {
|
||||||
font-size: 0.75rem;
|
margin-bottom: 4rem;
|
||||||
letter-spacing: 0.12em;
|
}
|
||||||
text-transform: uppercase;
|
|
||||||
color: var(--color-text-light);
|
.listing__head .label {
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: var(--space-md);
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-hero h1 {
|
.listing__head h1 {
|
||||||
margin-bottom: var(--space-lg);
|
font-size: clamp(2.2rem, 5vw, 4rem);
|
||||||
|
text-transform: lowercase;
|
||||||
|
line-height: 1.05;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-hero h1 em {
|
.listing__grid {
|
||||||
color: var(--color-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-hero__desc {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
line-height: 1.8;
|
|
||||||
max-width: 600px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.projects-grid {
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: 1fr 1fr;
|
||||||
gap: var(--space-xl) var(--space-lg);
|
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) {
|
@media (max-width: 768px) {
|
||||||
.projects-grid {
|
.listing__grid { grid-template-columns: 1fr; }
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,229 +1,99 @@
|
|||||||
/* ==========================================================================
|
/* ==========================================================================
|
||||||
Aurélie Barré Portfolio — Global Styles
|
Aurélie Barré — v4
|
||||||
Direction: "Galerie Blanche" — Éditorial Light
|
Pure white. Photos do all the work.
|
||||||
========================================================================== */
|
========================================================================== */
|
||||||
|
|
||||||
/* --- CSS Custom Properties --- */
|
|
||||||
:root {
|
:root {
|
||||||
/* Palette */
|
--bg: #fff;
|
||||||
--color-bg: #FAFAF8;
|
--text: #111;
|
||||||
--color-bg-alt: #F3F1ED;
|
--text-2: #555;
|
||||||
--color-surface: #FFFFFF;
|
--text-3: #999;
|
||||||
--color-text: #1A1A1A;
|
--border: #e0e0e0;
|
||||||
--color-text-secondary: #6B6B6B;
|
--border-light: #f0f0f0;
|
||||||
--color-text-light: #9A9A9A;
|
--serif: 'DM Serif Display', Georgia, serif;
|
||||||
--color-accent: #C4A77D;
|
--sans: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||||
--color-accent-hover: #B08E5E;
|
--header-h: 60px;
|
||||||
--color-border: #E8E6E1;
|
--ease: cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
--color-border-light: #F0EEEA;
|
--ease-in: cubic-bezier(0.65, 0, 0.35, 1);
|
||||||
|
--pad: clamp(1.25rem, 3.5vw, 3rem);
|
||||||
/* 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Reset & Base --- */
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
*, *::before, *::after {
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
html {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
scroll-behavior: smooth;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: var(--font-sans);
|
font-family: var(--sans);
|
||||||
font-size: 1rem;
|
font-size: 0.95rem;
|
||||||
line-height: 1.7;
|
line-height: 1.65;
|
||||||
color: var(--color-text);
|
color: var(--text);
|
||||||
background-color: var(--color-bg);
|
background: var(--bg);
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Typography --- */
|
/* --- Typography --- */
|
||||||
h1, h2, h3, h4 {
|
h1, h2, h3 {
|
||||||
font-family: var(--font-serif);
|
font-family: var(--serif);
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 1.2;
|
line-height: 1.1;
|
||||||
letter-spacing: -0.01em;
|
letter-spacing: -0.02em;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 { font-size: clamp(2.8rem, 7vw, 6rem); }
|
||||||
font-size: clamp(2.5rem, 6vw, 5rem);
|
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 {
|
/* --- Layout --- */
|
||||||
font-size: clamp(1.8rem, 4vw, 3rem);
|
.wrap {
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: var(--container-max);
|
max-width: 1400px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding-left: var(--container-padding);
|
padding-left: var(--pad);
|
||||||
padding-right: var(--container-padding);
|
padding-right: var(--pad);
|
||||||
}
|
|
||||||
|
|
||||||
.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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sr-only {
|
.sr-only {
|
||||||
position: absolute;
|
position: absolute; width: 1px; height: 1px;
|
||||||
width: 1px;
|
padding: 0; margin: -1px; overflow: hidden;
|
||||||
height: 1px;
|
clip: rect(0,0,0,0); white-space: nowrap; border: 0;
|
||||||
padding: 0;
|
|
||||||
margin: -1px;
|
|
||||||
overflow: hidden;
|
|
||||||
clip: rect(0, 0, 0, 0);
|
|
||||||
white-space: nowrap;
|
|
||||||
border: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Divider --- */
|
/* --- Reveal animation --- */
|
||||||
.divider {
|
.rv {
|
||||||
width: 100%;
|
|
||||||
height: 1px;
|
|
||||||
background-color: var(--color-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Reveal Animation (applied via JS) --- */
|
|
||||||
.reveal {
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(30px);
|
transform: translateY(24px);
|
||||||
transition: opacity var(--duration-slow) var(--ease-out),
|
transition: opacity 0.7s var(--ease), transform 0.7s var(--ease);
|
||||||
transform var(--duration-slow) var(--ease-out);
|
transition-delay: calc(var(--d, 0) * 80ms);
|
||||||
}
|
}
|
||||||
|
.rv.vis {
|
||||||
.reveal.visible {
|
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Masonry Grid --- */
|
/* --- Scrollbar: hidden --- */
|
||||||
.masonry-grid {
|
::-webkit-scrollbar { width: 0; height: 0; }
|
||||||
columns: 3;
|
html { scrollbar-width: none; }
|
||||||
column-gap: var(--space-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.masonry-grid > * {
|
/* --- Selection --- */
|
||||||
break-inside: avoid;
|
::selection {
|
||||||
margin-bottom: var(--space-md);
|
background: var(--text);
|
||||||
}
|
color: var(--bg);
|
||||||
|
|
||||||
@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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
68
src/styles/styles/global.css
Normal file
68
src/styles/styles/global.css
Normal 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; }
|
||||||
Loading…
x
Reference in New Issue
Block a user