diff --git a/.DS_Store b/.DS_Store
index 64c44bb..0fe76b9 100644
Binary files a/.DS_Store and b/.DS_Store differ
diff --git a/.claude/launch.json b/.claude/launch.json
new file mode 100644
index 0000000..5f58119
--- /dev/null
+++ b/.claude/launch.json
@@ -0,0 +1,11 @@
+{
+ "version": "0.0.1",
+ "configurations": [
+ {
+ "name": "rebours-dev",
+ "runtimeExecutable": "pnpm",
+ "runtimeArgs": ["dev"],
+ "port": 4321
+ }
+ ]
+}
diff --git a/public/main.js b/public/main.js
index 4274238..5c5f9ac 100644
--- a/public/main.js
+++ b/public/main.js
@@ -1,9 +1,16 @@
/**
- * REBOUR — Main Script
+ * REBOURS — Main Script
+ * CAD/CAO-inspired interface · GSAP ScrollTrigger · Technical drawing overlays · Ambient sound
*/
document.addEventListener('DOMContentLoaded', () => {
+ // ---- CONFIG ----
+ const isMobile = window.innerWidth <= 600;
+ const isTouch = 'ontouchstart' in window;
+
+ gsap.registerPlugin(ScrollTrigger);
+
// ---- HEADER HEIGHT → CSS VAR ----
const setHeaderHeight = () => {
const h = document.querySelector('.header')?.offsetHeight || 44;
@@ -12,58 +19,76 @@ document.addEventListener('DOMContentLoaded', () => {
setHeaderHeight();
window.addEventListener('resize', setHeaderHeight);
- // ---- CUSTOM CURSOR ----
- const cursorDot = document.querySelector('.cursor-dot');
- const cursorOutline = document.querySelector('.cursor-outline');
+ // ==========================================================
+ // 1. CAD CROSSHAIR CURSOR WITH X/Y COORDINATES
+ // ==========================================================
- let mouseX = 0, mouseY = 0;
- let outlineX = 0, outlineY = 0;
- let rafId = null;
+ let attachCursorHover = () => {};
- window.addEventListener('mousemove', (e) => {
- mouseX = e.clientX;
- mouseY = e.clientY;
- // Première fois : initialise l'outline à la position courante et rend visible
- if (outlineX === 0 && outlineY === 0) {
- outlineX = mouseX;
- outlineY = mouseY;
- cursorDot.style.opacity = '1';
- cursorOutline.style.opacity = '1';
- }
- cursorDot.style.transform = `translate(calc(-50% + ${mouseX}px), calc(-50% + ${mouseY}px))`;
- if (!rafId) rafId = requestAnimationFrame(animateOutline);
- }, { once: false });
+ if (!isMobile && !isTouch) {
+ const cursorH = document.createElement('div');
+ cursorH.className = 'cad-h';
+ const cursorV = document.createElement('div');
+ cursorV.className = 'cad-v';
+ const cursorCenter = document.createElement('div');
+ cursorCenter.className = 'cad-center';
+ const cursorCoords = document.createElement('div');
+ cursorCoords.className = 'cad-coords';
- function animateOutline() {
- rafId = null;
- outlineX += (mouseX - outlineX) * 0.18;
- outlineY += (mouseY - outlineY) * 0.18;
- cursorOutline.style.transform = `translate(calc(-50% + ${outlineX}px), calc(-50% + ${outlineY}px))`;
- if (Math.abs(mouseX - outlineX) > 0.1 || Math.abs(mouseY - outlineY) > 0.1) {
- rafId = requestAnimationFrame(animateOutline);
- }
- }
+ [cursorH, cursorV, cursorCenter, cursorCoords].forEach(el => document.body.appendChild(el));
- let hoverCount = 0;
+ let visible = false;
- function attachCursorHover(elements) {
- elements.forEach(el => {
- el.addEventListener('mouseenter', () => {
- cursorOutline.style.width = '38px';
- cursorOutline.style.height = '38px';
- });
- el.addEventListener('mouseleave', () => {
- cursorOutline.style.width = '26px';
- cursorOutline.style.height = '26px';
- });
+ window.addEventListener('mousemove', (e) => {
+ const x = e.clientX;
+ const y = e.clientY;
+
+ cursorH.style.left = (x - 20) + 'px';
+ cursorH.style.top = y + 'px';
+ cursorV.style.left = x + 'px';
+ cursorV.style.top = (y - 20) + 'px';
+ cursorCenter.style.left = x + 'px';
+ cursorCenter.style.top = y + 'px';
+ cursorCoords.style.left = (x + 16) + 'px';
+ cursorCoords.style.top = (y + 14) + 'px';
+
+ cursorCoords.textContent =
+ 'X:' + String(Math.round(x)).padStart(4, '0') +
+ ' Y:' + String(Math.round(y)).padStart(4, '0');
+
+ if (!visible) {
+ visible = true;
+ [cursorH, cursorV, cursorCenter, cursorCoords].forEach(el => {
+ el.style.opacity = '1';
+ });
+ }
});
+
+ attachCursorHover = (elements) => {
+ elements.forEach(el => {
+ el.addEventListener('mouseenter', () => {
+ cursorH.classList.add('cad-hover');
+ cursorV.classList.add('cad-hover');
+ cursorCenter.classList.add('cad-hover');
+ });
+ el.addEventListener('mouseleave', () => {
+ cursorH.classList.remove('cad-hover');
+ cursorV.classList.remove('cad-hover');
+ cursorCenter.classList.remove('cad-hover');
+ });
+ });
+ };
+
+ attachCursorHover(document.querySelectorAll(
+ 'a, button, input, .product-card, summary, .panel-close'
+ ));
}
- attachCursorHover(document.querySelectorAll('a, button, input, .product-card, summary, .panel-close'));
+ // ==========================================================
+ // 2. INTERACTIVE GRID
+ // ==========================================================
- // ---- INTERACTIVE GRID ----
const gridContainer = document.getElementById('interactive-grid');
- const CELL = window.innerWidth <= 600 ? 38 : 60;
const COLORS = [
'rgba(232,168,0,0.45)',
'rgba(232,168,0,0.32)',
@@ -73,7 +98,7 @@ document.addEventListener('DOMContentLoaded', () => {
function buildGrid() {
if (!gridContainer) return;
gridContainer.innerHTML = '';
- const CELL = window.innerWidth <= 600 ? 38 : 60;
+ const CELL = isMobile ? 38 : 60;
const cols = Math.ceil(window.innerWidth / CELL);
const rows = Math.ceil(window.innerHeight / CELL);
gridContainer.style.display = 'grid';
@@ -99,12 +124,316 @@ document.addEventListener('DOMContentLoaded', () => {
let rt;
window.addEventListener('resize', () => { clearTimeout(rt); rt = setTimeout(buildGrid, 150); });
- // ---- PRODUCT PANEL ----
- const panel = document.getElementById('product-panel');
- const panelClose = document.getElementById('panel-close');
+ // ==========================================================
+ // 3. GSAP SCROLL ANIMATIONS — CAD REVEAL
+ // ==========================================================
+
+ // ---- Hero parallax ----
+ const heroImg = document.querySelector('.hero-img');
+ if (heroImg) {
+ gsap.to(heroImg, {
+ yPercent: 15,
+ ease: 'none',
+ scrollTrigger: {
+ trigger: '.hero',
+ start: 'top top',
+ end: 'bottom top',
+ scrub: true,
+ },
+ });
+ }
+
+ // ---- Collection header: construction line draw-in ----
+ const collectionHeader = document.querySelector('.collection-header');
+ if (collectionHeader) {
+ // Add a construction line element
+ const line = document.createElement('div');
+ line.className = 'cad-construction-line';
+ collectionHeader.appendChild(line);
+
+ gsap.from(line, {
+ scaleX: 0,
+ transformOrigin: 'left center',
+ duration: 0.8,
+ ease: 'power2.out',
+ scrollTrigger: {
+ trigger: collectionHeader,
+ start: 'top 85%',
+ },
+ });
+ }
+
+ // ---- Product cards: staggered CAD reveal ----
const cards = document.querySelectorAll('.product-card');
- // Champs du panel
+ cards.forEach((card, i) => {
+ const img = card.querySelector('.card-img-wrap img');
+ const meta = card.querySelector('.card-meta');
+ const imgWrap = card.querySelector('.card-img-wrap');
+
+ if (!img) return;
+
+ // Create a CAD scan line per card
+ const scanLine = document.createElement('div');
+ scanLine.className = 'card-scanline';
+ imgWrap.style.position = 'relative';
+ imgWrap.appendChild(scanLine);
+
+ // Create corner marks per card
+ const cornerOverlay = document.createElement('div');
+ cornerOverlay.className = 'card-corners';
+ cornerOverlay.innerHTML =
+ '' +
+ '' +
+ '' +
+ '';
+ imgWrap.appendChild(cornerOverlay);
+
+ // Create coordinate annotation per card
+ const coordTag = document.createElement('div');
+ coordTag.className = 'card-coord-tag';
+ coordTag.textContent = `[${String(i + 1).padStart(3, '0')}] — ${img.naturalWidth || '1024'} × ${img.naturalHeight || '1024'} px`;
+ imgWrap.appendChild(coordTag);
+
+ // GSAP timeline triggered on scroll
+ const tl = gsap.timeline({
+ scrollTrigger: {
+ trigger: card,
+ start: 'top 88%',
+ toggleActions: 'play none none none',
+ },
+ });
+
+ // 1. Image clip-path reveal (bottom → top scan)
+ tl.fromTo(img,
+ { clipPath: 'inset(100% 0 0 0)', filter: 'brightness(1.8) grayscale(100%)' },
+ { clipPath: 'inset(0% 0 0 0)', filter: 'brightness(1) grayscale(15%)',
+ duration: 0.9, ease: 'power3.out' },
+ 0
+ );
+
+ // 2. Scan line sweeps up
+ tl.fromTo(scanLine,
+ { top: '100%', opacity: 1 },
+ { top: '-2%', opacity: 0, duration: 0.9, ease: 'power3.out' },
+ 0
+ );
+
+ // 3. Corner brackets appear
+ tl.fromTo(cornerOverlay.querySelectorAll('.cc'),
+ { opacity: 0, scale: 0.5 },
+ { opacity: 1, scale: 1, duration: 0.4, stagger: 0.06, ease: 'back.out(2)' },
+ 0.3
+ );
+
+ // 4. Coordinate tag types in
+ tl.fromTo(coordTag,
+ { opacity: 0, x: -10 },
+ { opacity: 1, x: 0, duration: 0.4, ease: 'power2.out' },
+ 0.5
+ );
+
+ // 5. Meta bar slides in
+ if (meta) {
+ tl.fromTo(meta,
+ { opacity: 0, y: 8 },
+ { opacity: 1, y: 0, duration: 0.4, ease: 'power2.out' },
+ 0.4
+ );
+ }
+ });
+
+ // ---- Newsletter section: slide in ----
+ const nlLeft = document.querySelector('.nl-left');
+ const nlRight = document.querySelector('.nl-right');
+ if (nlLeft && nlRight) {
+ gsap.from(nlLeft, {
+ opacity: 0, x: -30, duration: 0.7, ease: 'power2.out',
+ scrollTrigger: { trigger: '.newsletter', start: 'top 80%' },
+ });
+ gsap.from(nlRight, {
+ opacity: 0, x: 30, duration: 0.7, ease: 'power2.out', delay: 0.15,
+ scrollTrigger: { trigger: '.newsletter', start: 'top 80%' },
+ });
+ }
+
+ // ==========================================================
+ // 4. TECHNICAL DRAWING OVERLAY (panel)
+ // ==========================================================
+
+ const panelImgCol = document.querySelector('.panel-img-col');
+ let techOverlay = null;
+ let techTimeline = null;
+
+ function createTechOverlay(card) {
+ if (techOverlay) techOverlay.remove();
+
+ techOverlay = document.createElement('div');
+ techOverlay.className = 'tech-overlay';
+
+ // Corner brackets
+ ['tl', 'tr', 'br', 'bl'].forEach(pos => {
+ const corner = document.createElement('div');
+ corner.className = `tech-corner tech-corner--${pos}`;
+ techOverlay.appendChild(corner);
+ });
+
+ // Center crosshair
+ const centerH = document.createElement('div');
+ centerH.className = 'tech-center-h';
+ const centerV = document.createElement('div');
+ centerV.className = 'tech-center-v';
+ techOverlay.appendChild(centerH);
+ techOverlay.appendChild(centerV);
+
+ // Horizontal dimension line
+ const dimH = document.createElement('div');
+ dimH.className = 'tech-dim tech-dim--h';
+ dimH.innerHTML =
+ '◂' +
+ '' +
+ '840' +
+ '' +
+ '▸';
+ techOverlay.appendChild(dimH);
+
+ // Vertical dimension line
+ const dimV = document.createElement('div');
+ dimV.className = 'tech-dim tech-dim--v';
+ dimV.innerHTML =
+ '▴' +
+ '' +
+ '420' +
+ '' +
+ '▾';
+ techOverlay.appendChild(dimV);
+
+ // Reference text
+ const ref = document.createElement('div');
+ ref.className = 'tech-ref';
+ const idx = card ? card.dataset.index : '001';
+ ref.textContent = `REF: ${idx} — SCALE 1:5 — UNIT: mm`;
+ techOverlay.appendChild(ref);
+
+ // Blueprint grid
+ const grid = document.createElement('div');
+ grid.className = 'tech-grid';
+ techOverlay.appendChild(grid);
+
+ // Scan line
+ const scanline = document.createElement('div');
+ scanline.className = 'tech-scanline';
+ techOverlay.appendChild(scanline);
+
+ if (panelImgCol) panelImgCol.appendChild(techOverlay);
+ }
+
+ function animateTechOverlay() {
+ if (!techOverlay) return;
+
+ // Kill previous timeline
+ if (techTimeline) techTimeline.kill();
+
+ const corners = techOverlay.querySelectorAll('.tech-corner');
+ const centerH = techOverlay.querySelector('.tech-center-h');
+ const centerV = techOverlay.querySelector('.tech-center-v');
+ const dimH = techOverlay.querySelector('.tech-dim--h');
+ const dimV = techOverlay.querySelector('.tech-dim--v');
+ const ref = techOverlay.querySelector('.tech-ref');
+ const grid = techOverlay.querySelector('.tech-grid');
+ const scanline = techOverlay.querySelector('.tech-scanline');
+
+ techTimeline = gsap.timeline();
+
+ // 1. Blueprint grid fades in
+ techTimeline.fromTo(grid,
+ { opacity: 0 },
+ { opacity: 1, duration: 0.6, ease: 'power1.in' },
+ 0
+ );
+
+ // 2. Scan line sweeps
+ techTimeline.fromTo(scanline,
+ { top: '0%', opacity: 1 },
+ { top: '100%', opacity: 0, duration: 1.1, ease: 'power2.inOut' },
+ 0
+ );
+
+ // 3. Corner brackets draw in (scale from corner)
+ corners.forEach((c, i) => {
+ const origins = ['top left', 'top right', 'bottom right', 'bottom left'];
+ techTimeline.fromTo(c,
+ { opacity: 0, scale: 0, borderColor: 'rgba(232,168,0,0)' },
+ { opacity: 1, scale: 1, borderColor: 'rgba(232,168,0,0.6)',
+ transformOrigin: origins[i],
+ duration: 0.4, ease: 'back.out(1.5)' },
+ 0.15 + i * 0.08
+ );
+ });
+
+ // 4. Center crosshair extends
+ techTimeline.fromTo(centerH,
+ { scaleX: 0, opacity: 0 },
+ { scaleX: 1, opacity: 1, duration: 0.5, ease: 'power2.out' },
+ 0.3
+ );
+ techTimeline.fromTo(centerV,
+ { scaleY: 0, opacity: 0 },
+ { scaleY: 1, opacity: 1, duration: 0.5, ease: 'power2.out' },
+ 0.35
+ );
+
+ // 5. Dimension lines extend
+ techTimeline.fromTo(dimH,
+ { opacity: 0, scaleX: 0 },
+ { opacity: 1, scaleX: 1, transformOrigin: 'center center',
+ duration: 0.6, ease: 'power2.out' },
+ 0.4
+ );
+ techTimeline.fromTo(dimV,
+ { opacity: 0, scaleY: 0 },
+ { opacity: 1, scaleY: 1, transformOrigin: 'center center',
+ duration: 0.6, ease: 'power2.out' },
+ 0.45
+ );
+
+ // 6. Reference text types in
+ techTimeline.fromTo(ref,
+ { opacity: 0, x: -15 },
+ { opacity: 1, x: 0, duration: 0.4, ease: 'power2.out' },
+ 0.6
+ );
+
+ // 7. Panel image scan reveal
+ const panelImg = document.getElementById('panel-img');
+ if (panelImg) {
+ techTimeline.fromTo(panelImg,
+ { clipPath: 'inset(0 0 100% 0)', filter: 'brightness(1.8) grayscale(100%)' },
+ { clipPath: 'inset(0 0 0% 0)', filter: 'brightness(1) contrast(1) grayscale(0%)',
+ duration: 1, ease: 'power3.out' },
+ 0.05
+ );
+ }
+ }
+
+ function hideTechOverlay() {
+ if (techTimeline) techTimeline.kill();
+ if (techOverlay) {
+ gsap.to(techOverlay, { opacity: 0, duration: 0.3, onComplete: () => {
+ if (techOverlay) techOverlay.remove();
+ techOverlay = null;
+ }});
+ }
+ }
+
+ // ==========================================================
+ // 5. PRODUCT PANEL
+ // ==========================================================
+
+ const panel = document.getElementById('product-panel');
+ const panelClose = document.getElementById('panel-close');
+ const panelCards = document.querySelectorAll('.product-card');
+
const fields = {
img: document.getElementById('panel-img'),
index: document.getElementById('panel-index'),
@@ -125,16 +454,12 @@ document.addEventListener('DOMContentLoaded', () => {
const checkoutForm = document.getElementById('checkout-form');
const checkoutSubmitBtn = document.getElementById('checkout-submit-btn');
const checkoutPriceEl = document.querySelector('.checkout-price');
-
- // Current product's stripe key (set dynamically when opening panel)
let currentStripeKey = null;
- // Format price in cents to display string
function formatPrice(cents) {
return (cents / 100).toLocaleString('fr-FR') + ' €';
}
- // Toggle affichage du form
if (checkoutToggleBtn) {
checkoutToggleBtn.addEventListener('click', () => {
const isOpen = checkoutFormWrap.style.display !== 'none';
@@ -145,7 +470,6 @@ document.addEventListener('DOMContentLoaded', () => {
});
}
- // Submit → appel API → redirect Stripe
if (checkoutForm) {
checkoutForm.addEventListener('submit', async (e) => {
e.preventDefault();
@@ -175,7 +499,6 @@ document.addEventListener('DOMContentLoaded', () => {
});
}
- // Slug à partir du nom du produit : "Solar_Altar" → "solar-altar"
function toSlug(name) {
return name
.toLowerCase()
@@ -197,7 +520,7 @@ document.addEventListener('DOMContentLoaded', () => {
fields.specs.textContent = card.dataset.specs;
fields.notes.textContent = card.dataset.notes;
- // Checkout : afficher uniquement si le produit a un prix et un stripe key
+ // Checkout
const price = card.dataset.price;
const stripeKey = card.dataset.stripeKey;
const isOrderable = price && stripeKey;
@@ -211,24 +534,25 @@ document.addEventListener('DOMContentLoaded', () => {
checkoutSection.style.display = 'none';
}
- // Reset form state
checkoutFormWrap.style.display = 'none';
checkoutToggleBtn.textContent = '[ COMMANDER CETTE PIÈCE ]';
checkoutSubmitBtn.disabled = false;
checkoutSubmitBtn.textContent = 'PROCÉDER AU PAIEMENT →';
checkoutForm.reset();
- // Ferme les accordéons
panel.querySelectorAll('details').forEach(d => d.removeAttribute('open'));
-
panel.classList.add('is-open');
panel.setAttribute('aria-hidden', 'false');
document.body.style.overflow = 'hidden';
- // Refresh cursor sur les nouveaux éléments
- attachCursorHover(panel.querySelectorAll('summary, .panel-close, .checkout-btn, .checkout-submit'));
+ attachCursorHover(panel.querySelectorAll(
+ 'summary, .panel-close, .checkout-btn, .checkout-submit'
+ ));
+
+ // Technical drawing overlay (delayed for panel slide-in)
+ createTechOverlay(card);
+ setTimeout(() => animateTechOverlay(), 350);
- // Mise à jour de l'URL
if (pushState) {
const slug = toSlug(card.dataset.name);
history.pushState({ slug }, '', `/collection/${slug}`);
@@ -236,6 +560,7 @@ document.addEventListener('DOMContentLoaded', () => {
}
function closePanel(pushState = true) {
+ hideTechOverlay();
panel.classList.remove('is-open');
panel.setAttribute('aria-hidden', 'true');
document.body.style.overflow = '';
@@ -245,37 +570,165 @@ document.addEventListener('DOMContentLoaded', () => {
}
}
- cards.forEach(card => {
+ panelCards.forEach(card => {
card.addEventListener('click', () => openPanel(card));
});
- // Ouverture automatique si on arrive directement sur /collection/[slug]
+ // Auto-open from direct URL (/collection/[slug])
if (window.__OPEN_PANEL__) {
const name = window.__OPEN_PANEL__;
- const card = [...cards].find(c => c.dataset.name === name);
+ const card = [...panelCards].find(c => c.dataset.name === name);
if (card) openPanel(card, false);
}
if (panelClose) panelClose.addEventListener('click', () => closePanel());
- // Echap pour fermer
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closePanel();
});
- // Bouton retour navigateur
window.addEventListener('popstate', () => {
if (panel.classList.contains('is-open')) {
closePanel(false);
} else {
- // Tente de rouvrir si on navigue vers un slug connu
const match = location.pathname.match(/^\/collection\/(.+)$/);
if (match) {
const slug = match[1];
- const card = [...cards].find(c => toSlug(c.dataset.name) === slug);
+ const card = [...panelCards].find(c => toSlug(c.dataset.name) === slug);
if (card) openPanel(card, false);
}
}
});
+ // ==========================================================
+ // 6. AMBIENT WORKSHOP SOUND
+ // ==========================================================
+
+ let audioCtx = null;
+ let soundOn = false;
+ let soundNodes = [];
+ let clickTimer = null;
+
+ const headerNav = document.querySelector('.header-nav');
+ if (headerNav) {
+ const soundBtn = document.createElement('button');
+ soundBtn.className = 'sound-toggle';
+ soundBtn.setAttribute('aria-label', 'Activer le son ambiant');
+ soundBtn.innerHTML =
+ '' +
+ 'SON';
+ soundBtn.addEventListener('click', toggleSound);
+ headerNav.appendChild(soundBtn);
+ attachCursorHover([soundBtn]);
+ }
+
+ function createAudioContext() {
+ audioCtx = new (window.AudioContext || window.webkitAudioContext)();
+ }
+
+ function generateBrownNoise(ctx, duration) {
+ const bufferSize = ctx.sampleRate * duration;
+ const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
+ const data = buffer.getChannelData(0);
+ let lastOut = 0;
+ for (let i = 0; i < bufferSize; i++) {
+ const white = Math.random() * 2 - 1;
+ data[i] = (lastOut + (0.02 * white)) / 1.02;
+ lastOut = data[i];
+ data[i] *= 3.5;
+ }
+ return buffer;
+ }
+
+ function startAmbientSound() {
+ if (!audioCtx) createAudioContext();
+ if (audioCtx.state === 'suspended') audioCtx.resume();
+
+ const noiseBuffer = generateBrownNoise(audioCtx, 4);
+ const noiseSource = audioCtx.createBufferSource();
+ noiseSource.buffer = noiseBuffer;
+ noiseSource.loop = true;
+
+ const lowpass = audioCtx.createBiquadFilter();
+ lowpass.type = 'lowpass';
+ lowpass.frequency.value = 180;
+
+ const gain = audioCtx.createGain();
+ gain.gain.value = 0;
+ gain.gain.linearRampToValueAtTime(0.045, audioCtx.currentTime + 2);
+
+ noiseSource.connect(lowpass);
+ lowpass.connect(gain);
+ gain.connect(audioCtx.destination);
+ noiseSource.start();
+
+ soundNodes.push({ source: noiseSource, gain: gain });
+ scheduleMetalClick();
+ }
+
+ function playMetalClick() {
+ if (!audioCtx || audioCtx.state !== 'running') return;
+
+ const osc = audioCtx.createOscillator();
+ osc.type = 'sine';
+ osc.frequency.value = 600 + Math.random() * 2400;
+
+ const gain = audioCtx.createGain();
+ gain.gain.value = 0.006 + Math.random() * 0.014;
+ gain.gain.exponentialRampToValueAtTime(
+ 0.0001,
+ audioCtx.currentTime + 0.08 + Math.random() * 0.25
+ );
+
+ const filter = audioCtx.createBiquadFilter();
+ filter.type = 'bandpass';
+ filter.frequency.value = 1000 + Math.random() * 2000;
+ filter.Q.value = 12 + Math.random() * 25;
+
+ osc.connect(filter);
+ filter.connect(gain);
+ gain.connect(audioCtx.destination);
+ osc.start();
+ osc.stop(audioCtx.currentTime + 0.5);
+ }
+
+ function scheduleMetalClick() {
+ if (!soundOn) return;
+ const delay = 2000 + Math.random() * 7000;
+ clickTimer = setTimeout(() => {
+ if (!soundOn) return;
+ playMetalClick();
+ scheduleMetalClick();
+ }, delay);
+ }
+
+ function stopAmbientSound() {
+ if (clickTimer) { clearTimeout(clickTimer); clickTimer = null; }
+ soundNodes.forEach(({ source, gain }) => {
+ gain.gain.linearRampToValueAtTime(0, audioCtx.currentTime + 1);
+ setTimeout(() => { try { source.stop(); } catch (e) {} }, 1200);
+ });
+ soundNodes = [];
+ }
+
+ function toggleSound() {
+ soundOn = !soundOn;
+ const btn = document.querySelector('.sound-toggle');
+ if (!btn) return;
+
+ if (soundOn) {
+ startAmbientSound();
+ btn.classList.add('sound-active');
+ btn.setAttribute('aria-label', 'Couper le son ambiant');
+ } else {
+ stopAmbientSound();
+ btn.classList.remove('sound-active');
+ btn.setAttribute('aria-label', 'Activer le son ambiant');
+ }
+ }
+
});
diff --git a/public/style.css b/public/style.css
index 3a44a8b..9bcbfad 100644
--- a/public/style.css
+++ b/public/style.css
@@ -26,27 +26,55 @@ body {
cursor: none;
}
-/* ---- CURSOR ---- */
-.cursor-dot {
- width: 5px; height: 5px;
- background: var(--clr-red);
- position: fixed; top: 0; left: 0;
- transform: translate(-50%, -50%);
- pointer-events: none;
- z-index: 999999;
- opacity: 0;
- will-change: transform;
-}
-.cursor-outline {
- width: 26px; height: 26px;
- border: 1px solid var(--clr-black);
- position: fixed; top: 0; left: 0;
- transform: translate(-50%, -50%);
+/* ---- LEGACY CURSOR (hidden) ---- */
+.cursor-dot, .cursor-outline { display: none; }
+
+/* ---- CAD CROSSHAIR CURSOR ---- */
+.cad-h, .cad-v {
+ position: fixed;
pointer-events: none;
z-index: 999998;
opacity: 0;
- transition: width 0.15s, height 0.15s, opacity 0.15s;
- will-change: transform;
+ transition: width 0.15s, height 0.15s;
+}
+.cad-h {
+ width: 40px; height: 1px;
+ background: var(--clr-black);
+ transform: translateY(-0.5px);
+}
+.cad-h.cad-hover { width: 56px; margin-left: -8px; }
+.cad-v {
+ width: 1px; height: 40px;
+ background: var(--clr-black);
+ transform: translateX(-0.5px);
+}
+.cad-v.cad-hover { height: 56px; margin-top: -8px; }
+.cad-center {
+ position: fixed;
+ width: 3px; height: 3px;
+ background: var(--clr-red);
+ pointer-events: none;
+ z-index: 999999;
+ opacity: 0;
+ transform: translate(-1.5px, -1.5px);
+}
+.cad-center.cad-hover {
+ width: 5px; height: 5px;
+ transform: translate(-2.5px, -2.5px);
+}
+.cad-coords {
+ position: fixed;
+ pointer-events: none;
+ z-index: 999999;
+ font-family: var(--font-mono);
+ font-size: 0.55rem;
+ color: rgba(0,0,0,0.55);
+ opacity: 0;
+ letter-spacing: 0.05em;
+ background: rgba(200,200,200,0.7);
+ padding: 1px 5px;
+ border: 1px solid rgba(0,0,0,0.08);
+ white-space: nowrap;
}
/* ---- INTERACTIVE GRID (derrière tout) ---- */
@@ -592,6 +620,245 @@ hr { border: none; border-top: var(--border); margin: 0; }
.footer a { color: var(--clr-black); text-decoration: none; }
.footer a:hover { text-decoration: underline; }
+/* ---- TECHNICAL DRAWING OVERLAY (panel image) ---- */
+.tech-overlay {
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+ z-index: 10;
+ overflow: hidden;
+}
+.tech-corner {
+ position: absolute;
+ width: 28px; height: 28px;
+ border-color: rgba(232,168,0,0);
+ border-style: solid;
+ border-width: 0;
+ transition: border-color 0.6s ease, width 0.5s ease, height 0.5s ease;
+}
+.tech-visible .tech-corner {
+ border-color: rgba(232,168,0,0.6);
+}
+.tech-corner--tl { top: 7%; left: 5%; border-top-width: 1px; border-left-width: 1px; }
+.tech-corner--tr { top: 7%; right: 5%; border-top-width: 1px; border-right-width: 1px; }
+.tech-corner--br { bottom: 7%; right: 5%; border-bottom-width: 1px; border-right-width: 1px; }
+.tech-corner--bl { bottom: 7%; left: 5%; border-bottom-width: 1px; border-left-width: 1px; }
+
+.tech-center-h, .tech-center-v {
+ position: absolute;
+ background: rgba(232,168,0,0);
+ transition: background 0.5s ease 0.15s;
+}
+.tech-visible .tech-center-h, .tech-visible .tech-center-v {
+ background: rgba(232,168,0,0.2);
+}
+.tech-center-h {
+ top: 50%; left: 38%; right: 38%;
+ height: 1px; transform: translateY(-0.5px);
+}
+.tech-center-v {
+ left: 50%; top: 38%; bottom: 38%;
+ width: 1px; transform: translateX(-0.5px);
+}
+
+/* Dimension lines */
+.tech-dim {
+ position: absolute;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ opacity: 0;
+ transition: opacity 0.5s ease 0.3s;
+}
+.tech-visible .tech-dim { opacity: 1; }
+.tech-dim--h {
+ bottom: 4%; left: 5%; right: 5%;
+}
+.tech-dim--v {
+ right: 2.5%; top: 7%; bottom: 7%;
+ flex-direction: column;
+ writing-mode: vertical-lr;
+}
+.tech-dim-line {
+ flex: 1;
+}
+.tech-dim--h .tech-dim-line { height: 1px; background: rgba(232,168,0,0.35); }
+.tech-dim--v .tech-dim-line { width: 1px; background: rgba(232,168,0,0.35); }
+.tech-dim-text {
+ font-family: var(--font-mono);
+ font-size: 0.5rem;
+ color: rgba(232,168,0,0.65);
+ letter-spacing: 0.06em;
+}
+/* Dimension arrows */
+.tech-dim-arrow {
+ font-size: 0.5rem;
+ color: rgba(232,168,0,0.5);
+ line-height: 1;
+}
+
+/* Reference text */
+.tech-ref {
+ position: absolute;
+ bottom: 1.5%; left: 5%;
+ font-family: var(--font-mono);
+ font-size: 0.45rem;
+ color: rgba(232,168,0,0);
+ letter-spacing: 0.06em;
+ transition: color 0.5s ease 0.5s;
+}
+.tech-visible .tech-ref { color: rgba(232,168,0,0.45); }
+
+/* Blueprint grid */
+.tech-grid {
+ position: absolute;
+ inset: 7% 5%;
+ background-image:
+ linear-gradient(rgba(232,168,0,0.04) 1px, transparent 1px),
+ linear-gradient(90deg, rgba(232,168,0,0.04) 1px, transparent 1px);
+ background-size: 40px 40px;
+ opacity: 0;
+ transition: opacity 0.8s ease 0.05s;
+}
+.tech-visible .tech-grid { opacity: 1; }
+
+/* Scan line effect */
+.tech-scanline {
+ position: absolute;
+ left: 0; right: 0;
+ height: 2px;
+ background: linear-gradient(to bottom,
+ transparent,
+ rgba(232,168,0,0.6),
+ rgba(232,168,0,0.9),
+ rgba(232,168,0,0.6),
+ transparent);
+ box-shadow: 0 0 12px rgba(232,168,0,0.4);
+ z-index: 11;
+ pointer-events: none;
+ opacity: 0;
+ top: 0;
+}
+.tech-visible .tech-scanline {
+ animation: techScanLine 1s ease-in-out forwards;
+ animation-delay: 0.1s;
+}
+@keyframes techScanLine {
+ 0% { top: 0%; opacity: 1; }
+ 90% { opacity: 0.8; }
+ 100% { top: 100%; opacity: 0; }
+}
+
+/* ---- CARD CAD ELEMENTS (scroll-triggered via GSAP) ---- */
+.card-scanline {
+ position: absolute;
+ left: 0; right: 0;
+ height: 2px;
+ background: linear-gradient(to bottom,
+ transparent,
+ rgba(232,168,0,0.7),
+ rgba(232,168,0,1),
+ rgba(232,168,0,0.7),
+ transparent);
+ box-shadow: 0 0 16px rgba(232,168,0,0.5), 0 0 4px rgba(232,168,0,0.8);
+ z-index: 5;
+ pointer-events: none;
+ top: 100%;
+}
+.card-corners {
+ position: absolute;
+ inset: 8%;
+ pointer-events: none;
+ z-index: 4;
+}
+.cc {
+ position: absolute;
+ width: 16px; height: 16px;
+ border-color: rgba(232,168,0,0.55);
+ border-style: solid;
+ border-width: 0;
+}
+.cc-tl { top: 0; left: 0; border-top-width: 1px; border-left-width: 1px; }
+.cc-tr { top: 0; right: 0; border-top-width: 1px; border-right-width: 1px; }
+.cc-br { bottom: 0; right: 0; border-bottom-width: 1px; border-right-width: 1px; }
+.cc-bl { bottom: 0; left: 0; border-bottom-width: 1px; border-left-width: 1px; }
+
+.card-coord-tag {
+ position: absolute;
+ bottom: 6px; left: 8px;
+ font-family: var(--font-mono);
+ font-size: 0.45rem;
+ color: rgba(232,168,0,0.6);
+ letter-spacing: 0.05em;
+ z-index: 5;
+ pointer-events: none;
+ background: rgba(0,0,0,0.35);
+ padding: 1px 5px;
+}
+
+/* Construction line under collection header */
+.cad-construction-line {
+ position: absolute;
+ bottom: 0; left: 0; right: 0;
+ height: 1px;
+ background: linear-gradient(to right,
+ var(--clr-red),
+ var(--clr-red) 60%,
+ transparent);
+ transform-origin: left center;
+}
+.collection-header {
+ position: relative;
+}
+
+/* ---- SOUND TOGGLE ---- */
+.sound-toggle {
+ display: flex;
+ align-items: center;
+ gap: 0.35rem;
+ background: none;
+ border: 1px solid rgba(0,0,0,0.18);
+ font-family: var(--font-mono);
+ font-size: 0.65rem;
+ color: #999;
+ padding: 0.2rem 0.55rem;
+ cursor: none;
+ transition: color 0.2s, border-color 0.2s;
+ letter-spacing: 0.04em;
+}
+.sound-toggle:hover {
+ color: var(--clr-black);
+ border-color: var(--clr-black);
+}
+.sound-toggle.sound-active {
+ color: var(--clr-red);
+ border-color: var(--clr-red);
+}
+.sound-icon { font-size: 0.7rem; line-height: 1; }
+.sound-bars {
+ display: flex;
+ align-items: flex-end;
+ gap: 1px;
+ height: 10px;
+}
+.sound-bar {
+ width: 2px;
+ background: currentColor;
+ transition: height 0.2s;
+}
+.sound-bar:nth-child(1) { height: 3px; }
+.sound-bar:nth-child(2) { height: 6px; }
+.sound-bar:nth-child(3) { height: 4px; }
+.sound-toggle.sound-active .sound-bar {
+ animation: soundPulse 0.8s ease-in-out infinite alternate;
+}
+.sound-toggle.sound-active .sound-bar:nth-child(2) { animation-delay: 0.15s; }
+.sound-toggle.sound-active .sound-bar:nth-child(3) { animation-delay: 0.3s; }
+@keyframes soundPulse {
+ 0% { height: 2px; }
+ 100% { height: 10px; }
+}
+
/* ---- RESPONSIVE ---- */
@media (max-width: 900px) {
.hero, .newsletter { grid-template-columns: 1fr; }
@@ -611,6 +878,7 @@ hr { border: none; border-top: var(--border); margin: 0; }
.hero-left { min-height: unset; padding: 2rem var(--pad); order: -1; }
.hero-right { height: 60vw; }
- .cursor-dot, .cursor-outline { display: none; }
+ .cursor-dot, .cursor-outline,
+ .cad-h, .cad-v, .cad-center, .cad-coords { display: none; }
body { cursor: auto; }
}
diff --git a/src/layouts/Base.astro b/src/layouts/Base.astro
index f172a8f..af4c609 100644
--- a/src/layouts/Base.astro
+++ b/src/layouts/Base.astro
@@ -57,6 +57,10 @@ const {