1<!DOCTYPE html> 2<html lang="fr"> 3<head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"> 6 7 <!-- Primary Meta Tags --> 8 <title>Christophe Boucheã»Product design & brand craft â for teams building what's next</title> 9 <meta name="title" content="Christophe Boucheã»Product design & brand craft â for teams building what's next"> 10 <meta name="description" content="From 0 to 1 â core flows, product craft, and brand polish that feels obvious."> 11 <meta name="author" content="Christophe Bouche"> 12 <meta name="robots" content="index, follow"> 13 <meta name="theme-color" content="#111111"> 14 <link rel="canonical" href="https://christophebouche.co/"> 15 16 <!-- Open Graph / Facebook --> 17 <meta property="og:type" content="website"> 18 <meta property="og:url" content="https://christophebouche.co/"> 19 <meta property="og:title" content="Christophe Boucheã»Product design & brand craft â for teams building what's next"> 20 <meta property="og:description" content="From 0 to 1 â core flows, product craft, and brand polish that feels obvious."> 21 <meta property="og:image" content="https://christophebouche.co/assets/images/ogimage.jpg"> 22 <meta property="og:site_name" content="Christophe Bouche"> 23 <meta property="og:locale" content="fr_FR"> 24 25 <!-- Twitter --> 26 <meta name="twitter:card" content="summary_large_image"> 27 <meta name="twitter:url" content="https://christophebouche.co/"> 28 <meta name="twitter:title" content="Christophe Boucheã»Product design & brand craft â for teams building what's next"> 29 <meta name="twitter:description" content="From 0 to 1 â core flows, product craft, and brand polish that feels obvious."> 30 <meta name="twitter:image" content="https://christophebouche.co/assets/images/ogimage.jpg"> 31 32 <!-- Favicon --> 33 <link rel="icon" type="image/x-icon" href="favicon.ico"> 34 <link rel="apple-touch-icon" href="favicon.ico"> 35 36 <!-- Preload Critical Fonts --> 37 <link rel="preload" href="assets/fonts/STKBureauSerif-Light.woff2" as="font" type="font/woff2" crossorigin> 38 <link rel="preload" href="assets/fonts/STKBureauSans-Light.woff2" as="font" type="font/woff2" crossorigin> 39 <link rel="preload" href="assets/fonts/STKBureauSans-Medium.woff2" as="font" type="font/woff2" crossorigin> 40 41 <!-- Stylesheet --> 42 <link rel="stylesheet" href="styles.css"> 43 44 <!-- Structured Data --> 45
45<script type="application/ld+json"> 46 { 47 "@context": "https://schema.org", 48 "@type": "Person", 49 "name": "Christophe Bouche", 50 "url": "https://christophebouche.co", 51 "jobTitle": "Product Designer", 52 "description": "Product design & brand craft â for teams building what's next. From 0 to 1 â core flows, product craft, and brand polish that feels obvious.", 53 "email": "[email protected]", 54 "sameAs": [] 55 } 56 </script>
56 57 58 <!-- Seline Analytics --> 59
59<script async="" src="https://cdn.seline.com/seline.js" data-token="c00d00876be3a59"></script>
59 60 61
61<script src="https://cdn.visitors.now/v.js" data-token="6c5745af-1c97-45a6-b904-791f8b90a488"></script>
61 62</head> 63<body> 64 <!-- Background Image Layer --> 65 <div class="bg-image" role="presentation" aria-hidden="true"></div> 66 67 <!-- Decorative Dots --> 68 <div class="vertical-dots vertical-dots-left" aria-hidden="true"></div> 69 <div class="vertical-dots vertical-dots-right" aria-hidden="true"></div> 70 <div class="horizontal-dots horizontal-dots-top" aria-hidden="true"></div> 71 <div class="horizontal-dots horizontal-dots-bottom" aria-hidden="true"></div> 72 73 <!-- Main Content --> 74 <main class="content-card"> 75 <!-- Header --> 76 <header class="header"> 77 <span class="name-text">Christophe Bouche</span> 78 <svg class="logo" viewBox="0 0 11.8876 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-label="Logo Christophe Bouche" role="img"> 79 <path d="M5.93257 16C9.46066 16 11.8876 12.809 11.8876 8C11.8876 3.14607 9.5056 0 5.93257 0C2.35955 0 0 3.14607 0 8C0 12.8539 2.35955 16 5.93257 16ZM5.93257 14.6966C3.14606 14.6966 1.37078 12.0674 1.37078 8C1.37078 3.91011 3.12359 1.30337 5.93257 1.30337C8.74156 1.30337 10.4944 3.88764 10.4944 8C10.4944 12.0899 8.71908 14.6966 5.93257 14.6966ZM5.93257 12.6067C7.57302 12.6067 8.69661 11.3483 8.9438 9.23595H7.37077C7.23594 10.4719 6.71909 11.1236 5.95505 11.1236C4.92134 11.1236 4.3146 9.95506 4.3146 7.97753C4.3146 6 4.92134 4.83146 5.95505 4.83146C6.71909 4.83146 7.21347 5.50562 7.3483 6.7191H8.92133C8.69661 4.65169 7.52808 3.34831 5.93257 3.34831C4.02246 3.34831 2.7191 5.21348 2.7191 7.97753C2.7191 10.7191 4.02246 12.6067 5.93257 12.6067Z" fill="#F2F4F8"/> 80 </svg> 81 </header> 82 83 <!-- Headline --> 84 <h1 class="headline"> 85 <span class="headline-serif">Product design & brand craft</span> 86 <span class="headline-sans">for teams building what's next</span> 87 </h1> 88 89 <!-- Description --> 90 <section class="description-block"> 91 <p class="description">From 0 to 1 â core flows, product craft, and brand polish that feels obvious.</p> 92 </section> 93 94 <!-- CTA --> 95 <div class="cta-row"> 96 <a href="#" class="cta-button glitch-text" id="contact-btn" aria-label="Envoyer un email à Christophe Bouche">Contact me</a> 97 <div class="status" role="status" aria-label="Statut de disponibilité"> 98 <span class="status-dot" aria-hidden="true"></span> 99 <span class="status-text">Available for projects</span> 100 </div> 101 </div> 102 </main> 103 104
104<script> 105 // Intro animation orchestration 106 (function() { 107 const card = document.querySelector('.content-card'); 108 const root = document.documentElement; 109 const body = document.body; 110 111 function calculatePositions() { 112 // Get content-card dimensions for horizontal dots positioning 113 const rect = card.getBoundingClientRect(); 114 // Use visualViewport if available (more accurate on mobile) 115 const viewportHeight = window.visualViewport?.height || window.innerHeight; 116 const centerY = viewportHeight / 2; 117 118 // Calculate offset from center to top/bottom of card 119 const topOffset = centerY - rect.top; 120 const bottomOffset = rect.bottom - centerY; 121 122 root.style.setProperty('--dots-top-offset', `${topOffset}px`); 123 root.style.setProperty('--dots-bottom-offset', `${bottomOffset}px`); 124 } 125 126 function startAnimations() { 127 calculatePositions(); 128 129 // Add 500ms delay before starting animations 130 setTimeout(() => { 131 body.classList.add('intro-ready'); 132 }, 500); 133 } 134 135 // Calculate immediately on load 136 if (document.readyState === 'complete') { 137 startAnimations(); 138 } else { 139 window.addEventListener('load', startAnimations); 140 } 141 142 // Recalculate on resize (for responsive) 143 window.addEventListener('resize', calculatePositions); 144 })(); 145 146 // Email protection - obfuscated to prevent spam bots 147 (function() { 148 const btn = document.getElementById('contact-btn'); 149 // Encoded: [email protected] 150 const p1 = 'aGVsbG8='; // hello 151 const p2 = 'Y2hyaXN0b3BoZWJvdWNoZS5jbw=='; // christophebouche.co 152 153 btn.addEventListener('click', function(e) { 154 e.preventDefault(); 155 const email = atob(p1) + '@' + atob(p2); 156 window.location.href = 'mailto:' + email; 157 }); 158 })(); 159 160 // Glitch text animation on hover 161 (function() { 162 // Special characters pool for glitch effect 163 const glitchChars = '@!#$%&*()-_=+[]{}|;:<>?/~'; 164 165 // Grayscale levels for random opacity 166 const grayLevels = [0.3, 0.4, 0.5, 0.6, 0.7, 0.8]; 167 168 class GlitchText { 169 constructor(element) { 170 this.element = element; 171 this.originalText = element.textContent; 172 this.isAnimating = false; 173 this.isHovering = false; 174 this.intervalId = null; 175 this.timeoutId = null; 176 this.restartTimeoutId = null; 177 178 this.init(); 179 } 180 181 init() { 182 // Measure original width before wrapping 183 const originalWidth = this.element.offsetWidth; 184 185 // Wrap each character in a span, preserve spaces properly 186 this.element.innerHTML = this.originalText 187 .split('') 188 .map((char, i) => { 189 const displayChar = char === ' ' ? ' ' : char;
190 return `<span class="glitch-char" data-original="${char}" data-index="${i}">${displayChar}</span>`; 191 }) 192 .join(''); 193 194 this.chars = this.element.querySelectorAll('.glitch-char'); 195 196 // Set fixed width on each character to prevent layout shift 197 this.chars.forEach(char => { 198 const width = char.offsetWidth; 199 char.style.width = width + 'px'; 200 char.style.display = 'inline-block'; 201 char.style.textAlign = 'center'; 202 }); 203 204 // Add event listeners for mouse 205 this.element.addEventListener('mouseenter', () => { 206 this.isHovering = true; 207 this.startGlitch(); 208 }); 209 this.element.addEventListener('mouseleave', () => { 210 this.isHovering = false; 211 this.stopGlitch(); 212 }); 213 214 // Add touch support for mobile 215 this.element.addEventListener('touchstart', (e) => { 216 this.isHovering = true; 217 this.startGlitch(); 218 }, { passive: true }); 219 220 this.element.addEventListener('touchend', () => { 221 // Keep animation running, let it auto-stop after 1s 222 // Don't set isHovering to false immediately to allow the cycle 223 setTimeout(() => { 224 this.isHovering = false; 225 if (!this.isAnimating) { 226 this.stopGlitch(); 227 } 228 }, 1500); 229 }, { passive: true }); 230 } 231 232 getRandomChar() { 233 return glitchChars[Math.floor(Math.random() * glitchChars.length)]; 234 } 235 236 getRandomGray() { 237 return grayLevels[Math.floor(Math.random() * grayLevels.length)]; 238 } 239 240 startGlitch() { 241 if (this.isAnimating) return; 242 this.isAnimating = true; 243 244 // Animate at ~20fps for visible changes 245 this.intervalId = setInterval(() => { 246 this.chars.forEach((char, index) => { 247 const original = char.dataset.original; 248 249 // Skip spaces 250 if (original === ' ') return; 251 252 // Random chance to glitch this character (30% chance) 253 if (Math.random() < 0.3) { 254 // 50% chance to replace with special char, 50% to change opacity 255 if (Math.random() < 0.5) { 256 char.textContent = this.getRandomChar(); 257 char.style.opacity = ''; 258 } else { 259 char.innerHTML = original === ' ' ? ' ' : original; 260 char.style.opacity = this.getRandomGray(); 261 } 262 } else { 263 // Sometimes restore to original 264 if (Math.random() < 0.2) { 265 char.innerHTML = original === ' ' ? ' ' : original; 266 char.style.opacity = ''; 267 } 268 } 269 }); 270 }, 50); 271 272 // Auto-stop after 1 second 273 this.timeoutId = setTimeout(() => { 274 this.pauseGlitch(); 275 }, 1000); 276 } 277 278 pauseGlitch() { 279 this.isAnimating = false;
280 281 if (this.intervalId) { 282 clearInterval(this.intervalId); 283 this.intervalId = null; 284 } 285 286 if (this.timeoutId) { 287 clearTimeout(this.timeoutId); 288 this.timeoutId = null; 289 } 290 291 // Restore original text with a cascading effect 292 this.chars.forEach((char, index) => { 293 setTimeout(() => { 294 const original = char.dataset.original; 295 char.innerHTML = original === ' ' ? ' ' : original; 296 char.style.opacity = ''; 297 }, index * 15); 298 }); 299 300 // If still hovering, restart after 3.5 seconds 301 if (this.isHovering) { 302 this.restartTimeoutId = setTimeout(() => { 303 if (this.isHovering) { 304 this.startGlitch(); 305 } 306 }, 3500); 307 } 308 } 309 310 stopGlitch() { 311 this.isAnimating = false; 312 313 if (this.intervalId) { 314 clearInterval(this.intervalId); 315 this.intervalId = null; 316 } 317 318 if (this.timeoutId) { 319 clearTimeout(this.timeoutId); 320 this.timeoutId = null; 321 } 322 323 if (this.restartTimeoutId) { 324 clearTimeout(this.restartTimeoutId); 325 this.restartTimeoutId = null; 326 } 327 328 // Restore original text with a cascading effect 329 this.chars.forEach((char, index) => { 330 setTimeout(() => { 331 const original = char.dataset.original; 332 char.innerHTML = original === ' ' ? ' ' : original; 333 char.style.opacity = ''; 334 }, index * 15); 335 }); 336 } 337 } 338 339 // Initialize on all glitch-text elements (desktop only) 340 document.addEventListener('DOMContentLoaded', () => { 341 // Skip glitch effect on mobile for optimal typography 342 const isMobile = window.matchMedia('(max-width: 480px)').matches; 343 if (isMobile) return; 344 345 document.querySelectorAll('.glitch-text').forEach(el => { 346 new GlitchText(el); 347 }); 348 }); 349 })(); 350 </script>
vendor: 1 bytes, line 350
350
351<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v31edd6df95cf4e85bb4c19e7a9bdbcba1788362987495" integrity="sha512-iIg7k2xntmwu6/uSb5tpc/hySgZc4eoL31yB29W6tJFo2akwjPWcEqnCEdJvGexCL0KEQwVYv5BlowfhVz26hg==" data-cf-beacon='{"version":"2024.11.0","token":"b9cd2bb66d4448a89f5bb6ed12c6096d","r":1,"spa":2}' crossorigin="anonymous"></script>
351 352</body> 353</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.