PageSourceSearch

https://christophebouche.co/

html christophebouche.co collected 2026-10-02 00:45:17 UTC 16,999 bytes, 353 lines download raw bytes

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 === ' ' ? '&nbsp;' : 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 === ' ' ? '&nbsp;' : 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 === ' ' ? '&nbsp;' : 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 === ' ' ? '&nbsp;' : 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 === ' ' ? '&nbsp;' : 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.