1<HTML> 2<head> 3<meta name="viewport" content="width=device-width, initial-scale=1.0"> 4<link rel="preconnect" href="https://fonts.googleapis.com"> 5<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 6<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;600;700&display=swap" rel="stylesheet">
7<script src="https://cdn.tailwindcss.com"></script>
7 8<style type="text/css"> 9.hidden { display: none !important; } 10body {margin: 0pt; font-family: 'Open Sans', sans-serif;} 11.titlesize {margin: 0pt; font-family: 'Open Sans', sans-serif; font-size: 26pt } 12.hugest {margin: 0pt; font-family: 'Open Sans', sans-serif; font-size: 24pt } 13.huge {margin: 0pt; font-family: 'Open Sans', sans-serif; font-size: 20pt } 14.biggest {margin: 0pt; font-family: 'Open Sans', sans-serif; font-size: 18pt } 15.bigger {margin: 0pt; font-family: 'Open Sans', sans-serif; font-size: 16pt } 16.hyper {margin: 0pt; font-family: 'Open Sans', sans-serif; font-size: 16pt } 17.big {margin: 0pt; font-family: 'Open Sans', sans-serif; font-size: 14pt } 18.normalbig {margin: 0pt; font-family: 'Open Sans', sans-serif; font-size: 13pt } 19.normal {margin: 0pt; font-family: 'Open Sans', sans-serif; font-size: 14pt } 20.tiny {margin: 0pt; font-family: 'Open Sans', sans-serif; font-size: 12pt } 21.pagination { margin: 10px 0; font-family: 'Open Sans', sans-serif; font-size: 10pt; } 22.pagination button { margin: 2px; padding: 5px 8px; cursor: pointer; font-family: 'Open Sans', sans-serif; font-size: 9pt; border: 1px solid #ccc; background: #f5f5f5; } 23.pagination button:hover { background: #e0e0e0; } 24.pagination button.active { background-color: #ff9933; color: white; font-weight: bold; border-color: #ff9933; } 25.pagination .page-info { display: inline-block; margin: 0 10px; } 26.sidebar-link { color: black; text-decoration: none; font-size: 12pt; } 27.sidebar-link:hover { text-decoration: underline; } 28.sidebar-nav { background: #FF9933; border: 5px solid white; border-radius: 5px; padding: 14px 10px 10px 10px; } 29.orange-box { background: #FF9933; border: 5px solid white; border-radius: 5px; padding: 15px; } 30.content-area { padding: 15px; } 31 32/* Ensure content doesn't overflow */ 33.entry-text { word-wrap: break-word; overflow-wrap: break-word; } 34.white-box { box-sizing: border-box; max-width: 100%; overflow: hidden; } 35.content-boxes { box-sizing: border-box; max-width: 100%; } 36 37/* Software items styling */ 38.software-item { 39 margin-bottom: 24px; 40} 41.software-item h2 { 42 font-size: 16px; 43 font-weight: 700; 44 margin-bottom: 4px; 45} 46.software-item h2 a { 47 color: #333; 48 text-decoration: none; 49} 50.software-item h2 a:hover { 51 color: #c76300; 52 text-decoration: underline; 53} 54.software-item p { 55 margin-bottom: 8px; 56 font-size: 14px; 57} 58.software-item p a { 59 color: #c76300; 60} 61.software-item p a:hover { 62 text-decoration: underline; 63} 64 65/* Talks list styling */ 66ul.talks-list { 67 margin: 0; 68 padding-left: 24px; 69 list-style-type: disc; 70} 71ul.talks-list li { 72 margin-bottom: 20px; 73} 74ul.talks-list li a { 75 color: #333; 76 text-decoration: none; 77} 78ul.talks-list li a:hover { 79 color: #c76300; 80 text-decoration: underline; 81} 82 83/* Burning Man page styling */ 84.year-section { 85 margin-bottom: 20px; 86 padding-bottom: 10px; 87 border-bottom: 4px solid white; 88} 89.year-section:last-child { 90 border-bottom: none; 91} 92.year-header { 93 display: flex; 94 gap: 20px; 95 align-items: center; 96 margin-bottom: 20px; 97} 98.year-badge { 99 color: #333; 100 font-size: 24px; 101 font-weight: 700; 102 line-height: 1.2; 103 text-align: center; 104 flex-shrink: 0; 105} 106.year-badge br { 107 display: none; 108} 109/* Hide the header row in lasttoy table */ 110#lasttoy-content table tr:first-child { 111 display: none; 112} 113.year-tickets { 114 display: flex; 115 gap: 10px; 116 flex-wrap: nowrap; 117 align-items: center; 118} 119.year-tickets img { 120 width: 200px; 121 height: auto; 122 border-radius: 4px; 123} 124.year-tickets a { 125 display: flex; 126 align-items: center; 127} 128.year-content { 129 margin-top: 20px; 130} 131.year-content p { 132 margin-bottom: 16px; 133} 134.photo-row { 135 display: flex; 136 gap: 10px; 137 flex-wrap: wrap; 138 margin: 20px 0; 139 align-items: flex-start; 140} 141.photo-row img { 142 height: 180px; 143 border-radius: 4px; 144} 145.photo-row iframe { 146 height: 180px; 147} 148.video-row { 149 display: flex; 150 gap: 15px; 151 flex-wrap: wrap; 152 margin: 20px 0; 153} 154.links-list { 155 list-style: disc; 156 margin-left: 20px; 157 margin-top: 15px; 158} 159.links-list li { 160 margin-bottom: 8px; 161} 162.audio-box { 163 background: rgba(255,200,150,0.5); 164 border-radius: 8px; 165 padding: 15px 20px; 166 margin: 20px 0; 167 display: inline-block; 168} 169.audio-box ul { 170 list-style: disc; 171 margin-left: 20px; 172} 173.audio-box li { 174 margin-bottom: 5px; 175} 176.flex-content { 177 display: flex; 178 gap: 20px; 179 align-items: flex-start; 180 margin: 20px 0; 181} 182.flex-content img { 183 max-width: 200px; 184 border-radius: 4px; 185} 186@media (max-width: 640px) { 187 .year-tickets img { 188 max-width: 45%; 189 } 190 .flex-content { 191 flex-direction: column; 192 } 193 /* Blog entry mobile layout */ 194 .flex { 195 flex-direction: column !important; 196 } 197 .mt-4 { 198 margin-top: 0.5rem !important; 199 } 200 .text-sm { 201 font-size: 1rem !important; 202 line-height: 1.6 !important; 203 } 204 .w-96 { 205 width: 100% !important; 206 max-width: 384px !important; 207 } 208 .mb-6 { 209 margin-bottom: 2rem !important; 210 }
211 /* Sidebar adjustments */ 212 .sidebar-nav { 213 padding: 10px 8px 8px 8px; 214 } 215 .sidebar-link { 216 font-size: 14pt; 217 } 218} 219/* Facebook Blog media */ 220.facebook-media-grid { 221 display: grid; 222 grid-template-columns: repeat(2, minmax(0, 1fr)); 223 gap: 8px; 224 width: 24rem; 225 max-width: 100%; 226 flex-shrink: 0; 227} 228.facebook-media-grid > a, 229.facebook-media-grid > video, 230.facebook-media-grid > iframe { 231 display: block; 232 width: 100%; 233 min-width: 0; 234} 235.facebook-media-grid img, 236.facebook-media-grid video, 237.facebook-media-grid iframe { 238 display: block; 239 width: 100%; 240 max-width: 100%; 241 height: auto; 242 border: 0; 243} 244.facebook-media-grid iframe { 245 aspect-ratio: 16 / 9; 246} 247.facebook-media-grid .facebook-media-wide { 248 grid-column: 1 / -1; 249} 250@media (max-width: 767px) { 251 .facebook-media-grid { 252 width: 100%; 253 } 254} 255 256/* Progressive Blog overflow safety */ 257#blog-content .flex > .text-sm, 258#blog-source .flex > .text-sm { 259 min-width: 0; 260 max-width: 100%; 261 overflow-wrap: anywhere; 262 word-break: break-word; 263} 264#blog-content a, 265#blog-source a { 266 overflow-wrap: anywhere; 267 word-break: break-word; 268} 269#blog-content img, 270#blog-content video, 271#blog-content iframe, 272#blog-source img, 273#blog-source video, 274#blog-source iframe { 275 max-width: 100%; 276} 277#blog-content .mb-4 > .flex, 278#blog-source .mb-4 > .flex { 279 flex-wrap: wrap; 280} 281#blog-content .mb-4 > .flex > .text-sm, 282#blog-source .mb-4 > .flex > .text-sm { 283 flex: 1 1 20rem; 284} 285#blog-content .mb-4 > .flex > img, 286#blog-content .mb-4 > .flex > video, 287#blog-content .mb-4 > .flex > iframe, 288#blog-content .mb-4 > .flex > a:has(> img), 289#blog-source .mb-4 > .flex > img, 290#blog-source .mb-4 > .flex > video, 291#blog-source .mb-4 > .flex > iframe, 292#blog-source .mb-4 > .flex > a:has(> img) { 293 width: min(24rem, 100%); 294 max-width: 100%; 295 min-width: 0; 296 flex: 0 1 24rem; 297} 298 299</style> 300</head> 301<title>Tim Thompson's Home Page</title> 302 303<body marginwidth=0 marginheight=0 bgcolor="#FFD2A3"> 304 305<!-- Contact Form Modal --> 306<div id="contact-modal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50"> 307 <div class="relative top-20 mx-auto p-5 border w-full max-w-md
307shadow-lg rounded-md bg-white"> 308 <div class="flex justify-between items-center mb-4"> 309 <h2 class="text-2xl font-bold text-gray-900">Contact Me</h2> 310 <button id="close-modal" class="text-gray-500 hover:text-gray-700 text-2xl font-bold">×</button> 311 </div> 312 <form id="contact-form" class="space-y-6"> 313 <div> 314 <label for="contact-email" class="block mb-2 text-sm font-medium text-gray-900">Your email</label> 315 <input type="email" id="contact-email" name="email" placeholder="[email protected]" class="shadow-sm bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" required> 316 </div> 317 <div> 318 <label for="contact-subject" class="block mb-2 text-sm font-medium text-gray-900">Subject</label> 319 <input type="text" id="contact-subject" name="subject" placeholder="Let me know how I can help you" class="block p-3 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500" required> 320 </div> 321 <div> 322 <label for="contact-message" class="block mb-2 text-sm font-medium text-gray-900">Your message</label> 323 <textarea id="contact-message" name="message" rows="6" placeholder="Leave a message..." class="block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg shadow-sm border border-gray-300 focus:ring-blue-500 focus:border-blue-500" required></textarea> 324 </div> 325 <div id="form-status" class="hidden text-sm font-medium"></div> 326 <button type="submit" id="submit-btn" class="w-full py-3 px-5 text-sm font-medium text-center text-white rounded-lg bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300">Send message</button> 327 </form> 328 </div> 329</div> 330 331<div class="mx-4 md:mx-12 mt-8"> 332<div class="orange-box flex flex-wrap justify-between items-center gap-3"> 333<span class="titlesize text-black font-bold"><span style="background-color: #c76300; color: white; padding: 0 6px; border-radius: 4px; font-size: 1.3em;">T</span>im <span style="background-color: #c76300; color: white; padding: 0 6px; border-radius: 4px; font-size: 1.3em;">T</span>hompson</span> 334<button id="contact-btn" style="background-color: #c76300;" class="hover:opacity-90 text-white font-bold py-2 px-4 rounded text-sm">Contact Me</button> 335</div> 336</div> 337<!--This is the end of the top title --> 338<div class="mx-4 md:mx-12 mt-4 flex flex-col md:flex-row gap-6"> 339<!-- Sidebar --> 340<div class="shrink-0 md:w-44"> 341<div class="sidebar-nav"> 342<button id="nav-whoami" class="sidebar-heading bigger text-black font-bold w-full text-left cursor-pointer hover:underline" style="color: white;" onclick="showView('whoami')">Who am I?</button> 343<button id="nav-blog" class="sidebar-heading bigger text-black font-bold w-full text-left cursor-pointer hover:underline block" onclick="showView('blog')">Blog</button> 344<button id="nav-software" class="sidebar-heading bigger text-black font-bold w-full text-left cursor-pointer hover:underline block" onclick="showView('software')">Software</button> 345<button id="nav-hardware" class="sidebar-heading bigger text-black font-bold w-full text-left cursor-pointer hover:underline block" onclick="showView('hardware')">Hardware</button> 346<button id="nav-onlineware" class="sidebar-heading bigger text-black font-bold w-full text-left cursor-pointer hover:underline block" onclick="showView('onlineware')">Onlineware</button> 347<button id="nav-burningman" class="sidebar-heading bigger text-black font-bold w-full text-left cursor-pointer hover:underline block" onclick="showView('burningman')">Burning Man</button> 348<button id="nav-music" class="sidebar-heading bigger text-black font-bold w-full text-left cursor-pointer hover:underline block" onclick="showView('music')">Music</button> 349<button id="nav-talks" class="sidebar-heading bigger text-black font-bold w-full text-left cursor-pointer hover:underline block" onclick="showView('talks')">Talks</button> 350</div> 351</div>
352<script> 353function toggleSection(btn) { 354 const section = btn.parentElement; 355 const items = section.querySelector('.sidebar-items'); 356 const chevron = btn.querySelector('.chevron'); 357 const isOpen = !items.classList.contains('hidden'); 358 359 // Close all sections first 360 document.querySelectorAll('.sidebar-section').forEach(s => { 361 s.querySelector('.sidebar-items').classList.add('hidden'); 362 s.querySelector('.chevron').style.transform = ''; 363 }); 364 365 // If this section wasn't open, open it 366 if (!isOpen) { 367 items.classList.remove('hidden'); 368 chevron.style.transform = 'rotate(90deg)'; 369 } 370} 371 372const navLabels = { 373 'whoami': 'Who am I?', 374 'blog': 'Blog', 375 'software': 'Software', 376 'hardware': 'Hardware', 377 'onlineware': 'Onlineware', 378 'music': 'Music', 379 'burningman': 'Burning Man', 380 'talks': 'Talks' 381}; 382 383const BLOG_BATCH_SIZE = 12; 384 385function initProgressiveBlog(content, sourceMain) { 386 const sourceEntries = Array.from(sourceMain.querySelectorAll('.mb-4')); 387 content.innerHTML = ''; 388 389 const list = document.createElement('div'); 390 list.className = 'pt-1 space-y-4'; 391 list.id = 'blog-progress-list'; 392 393 const sentinel = document.createElement('div'); 394 sentinel.id = 'blog-progress-sentinel'; 395 sentinel.className = 'normal text-center py-4'; 396 sentinel.setAttribute('aria-live', 'polite'); 397 398 content.appendChild(list); 399 content.appendChild(sentinel); 400 content.dataset.totalEntries = String(sourceEntries.length); 401 402 let nextEntry = 0; 403 let observer = null; 404 let fallbackHandler = null; 405 406 function appendBlogBatch() { 407 const end = Math.min(nextEntry + BLOG_BATCH_SIZE, sourceEntries.length); 408 const fragment = document.createDocumentFragment(); 409 for (; nextEntry < end; nextEntry += 1) { 410 fragment.appendChild(sourceEntries[nextEntry].cloneNode(true)); 411 } 412 list.appendChild(fragment); 413 content.dataset.renderedEntries = String(nextEntry); 414 415 if (nextEntry >= sourceEntries.length) { 416 sentinel.textContent = ''; 417 if (observer) observer.disconnect(); 418 if (fallbackHandler) window.removeEventListener('scroll', fallbackHandler); 419 sentinel.remove(); 420 } else { 421 sentinel.textContent = 'Loading more entriesâ¦'; 422 } 423 } 424 425 appendBlogBatch(); 426 if (!sentinel.isConnected) return; 427 428 if ('IntersectionObserver' in window) { 429 observer = new IntersectionObserver(entries => { 430 if (entries.some(entry => entry.isIntersecting)) appendBlogBatch(); 431 }, { rootMargin: '1000px 0px' }); 432 observer.observe(sentinel); 433 } else { 434 fallbackHandler = () => { 435 if (sentinel.getBoundingClientRect().top <= window.innerHeight + 1000) appendBlogBatch(); 436 }; 437 window.addEventListener('scroll', fallbackHandler, { passive: true }); 438 fallbackHandler(); 439 } 440} 441 442function showView(view) { 443 const views = ['whoami', 'blog', 'software', 'hardware', 'onlineware', 'music', 'burningman', 'talks']; 444 445 // Hide all views and update nav indicators 446 views.forEach(v => { 447 const el = document.getElementById(v + '-view'); 448 if (el) el.classList.add('hidden'); 449 const nav = document.getElementById('nav-' + v); 450 if (nav) { 451 nav.textContent = navLabels[v]; 452 nav.style.color = '#333'; 453 } 454 }); 455 456 // Show the selected view and add indicator 457 const viewEl = document.getElementById(view + '-view'); 458 if (viewEl) viewEl.classList.remove('hidden'); 459 const navEl = document.getElementById('nav-' + view); 460 if (navEl) { 461 navEl.textContent = navLabels[view]; 462 navEl.style.color = 'white'; 463 } 464 465 // Update URL hash (but not for default 'whoami' view) 466 if (view === 'whoami') { 467 history.replaceState(null, '', window.location.pathname); 468 } else { 469 history.replaceState(null, '', '#' + view); 470 } 471 472 // Load content for views that fetch from external files 473 const contentMap = { 474 'blog': '/tjt/blog.html', 475 'software': '/tjt/software.html', 476 'hardware': '/tjt/hardware.html', 477 'onlineware': '/tjt/onlineware.html', 478 'music': '/tjt/musicnav.html', 479 'burningman': '/tjt/bm/index.html', 480 'talks': '/tjt/talks.html' 481 }; 482 483 if (contentMap[view]) { 484 const content = document.getElementById(view + '-content'); 485 if (content && content.dataset.loaded !== 'true') { 486 fetch(contentMap[view]) 487 .then(r => { 488 if (!r.ok) throw new Error('HTTP ' + r.status); 489 return r.text(); 490 }) 491 .then(html => { 492 const parser = new DOMParser(); 493 const doc = parser.parseFromString(html, 'text/html'); 494 const main = doc.querySelector('main'); 495 if (main) { 496 if (view === 'blog') { 497 initProgressiveBlog(content, main); 498 } else { 499 content.innerHTML = main.innerHTML; 500 } 501 content.dataset.loaded = 'true'; 502 // Load lasttoy content for onlineware view and execute scripts 503 if (view === 'onlineware') { 504 const lasttoyContent = content.querySelector('#lasttoy-content'); 505 if (lasttoyContent) { 506 fetch('/musictmp/lasttoy.html') 507 .then(r => r.text()) 508 .then(html => lasttoyContent.innerHTML = html) 509 .catch(err => console.error('Failed to load lasttoy.html:', err)); 510 } 511 } 512 // Fix relative paths for Burning Man view 513 if (view === 'burningman') { 514 const basePath = '/tjt/bm/'; 515 content.querySelectorAll('img').forEach(img => { 516 const src = img.getAttribute('src'); 517 if (src && !src.startsWith('http') && !src.startsWith('/')) { 518 img.src = basePath + src; 519 } 520 }); 521 content.querySelectorAll('a').forEach(a => { 522 const href = a.getAttribute('href'); 523 if (href && !href.startsWith('http') && !href.startsWith('/') && !href.startsWith('#')) { 524 a.href = basePath + href; 525 } 526 }); 527 } 528 } 529 }) 530 .catch(err => { 531 console.error('Failed to load ' + contentMap[view] + ':', err); 532 content.innerHTML = '<p class="normal">
532Unable to load this section.</p>'; 533 }); 534 } 535 } 536} 537 538// On page load, check URL hash and show appropriate view 539document.addEventListener('DOMContentLoaded', function() { 540 const hash = window.location.hash.slice(1); // Remove the '#' 541 const validViews = ['whoami', 'blog', 'software', 'hardware', 'onlineware', 'music', 'burningman', 'talks']; 542 if (hash && validViews.includes(hash)) { 543 showView(hash); 544 } else { 545 showView('whoami'); 546 } 547}); 548 549// Handle browser back/forward buttons 550window.addEventListener('hashchange', function() { 551 const hash = window.location.hash.slice(1); 552 const validViews = ['whoami', 'blog', 'software', 'hardware', 'onlineware', 'music', 'burningman', 'talks']; 553 if (hash && validViews.includes(hash)) { 554 showView(hash); 555 } 556}); 557</script>
557 558<!-- Main content --> 559<div class="flex-1 min-w-0"> 560<!-- Who am I view --> 561<div id="whoami-view" class="hidden"> 562<div class="flex flex-col lg:flex-row gap-4"> 563<div class="content-area flex-1 min-w-0" style="padding-top: 8px;"> 564<p class="normal leading-tight"> 565I'm a software engineer, musician, and installation artist. 566I worked at Bell Labs and AT&T for 20 years in New Jersey, 567and for Network Appliance in Silicon Valley for 9 years. 568I'm interested in programming languages, algorithmic composition, 569networked collaboration, atypical controllers, 570event-driven graphics generation, and realtime video processing. 571I've been inspired in recent decades by something described 572in this quote by Larnie Fox: 573</p> 574<blockquote class="tiny ml-4 mt-2 italic leading-tight"> 575"There is a yet unnamed art movement that may prove to be of some 576significance, and Burning Man is close to its center. It often manifests 577itself as circus, ritual, and spectacle. It is a movement away from a 578dialogue between an individual artist and a sophisticated audience, 579and towards collaboration amongst a big, wild, free and diverse community. 580It is a movement away from galleries, schools and other institutions 581and towards an art produced in and for casual groups of participants, 582more akin to clans and tribes, based on aesthetic affinities and bonds 583of friendship. It is a movement away from static gallery art and formal 584theater and towards site-specific, time-specific installation and 585performance. It is a rejection of spoon-fed corporate culture 586and an affirmation of the homemade, the idiosyncratic, the personal. 587It is profoundly democratic. It is radically inclusive, it is 588a difficult challenge, and it is beckoning." 589</blockquote> 590<!--#include virtual="../musictmp/lasttoy.html" --> 591</div> 592</div> 593</div> 594<!-- End Who am I view --> 595 596 597<!-- Blog view --> 598<div id="blog-view" class="hidden"> 599<div id="blog-content"> 600<p class="normal">Loading...</p> 601</div> 602</div> 603<!-- End Blog view --> 604 605<!-- Software view --> 606<div id="software-view" class="hidden"> 607<div class="content-area" style="padding: 10px 20px 20px 0;"> 608<div id="software-content"> 609<p class="normal">Loading...</p> 610</div> 611</div> 612</div> 613<!-- End Software view --> 614 615<!-- Hardware view --> 616<div id="hardware-view" class="hidden"> 617<div class="content-area" style="padding: 10px 20px 20px 0;"> 618<div id="hardware-content"> 619<p class="normal">Loading...</p> 620</div> 621</div> 622</div> 623<!-- End Hardware view --> 624 625<!-- Onlineware view --> 626<div id="onlineware-view" class="hidden"> 627<div class="content-area" style="padding: 10px 20px 20px 0;"> 628<div id="onlineware-content"> 629<p class="normal">Loading...</p> 630</div> 631</div> 632</div> 633<!-- End Onlineware view --> 634 635<!-- Music view --> 636<div id="music-view" class="hidden"> 637<div class="content-area" style="padding: 10px 20px 20px 0;"> 638<div id="music-content"> 639<p class="normal">Loading...</p> 640</div> 641</div> 642</div> 643<!-- End Music view --> 644 645<!-- Burning Man view --> 646<div id="burningman-view" class="hidden"> 647<div class="content-area" style="padding: 10px 20px 20px 0;"> 648<div id="burningman-content"> 649<p class="normal">Loading...</p> 650</div> 651</div> 652</div> 653<!-- End Burning Man view --> 654 655<!-- Talks view --> 656<div id="talks-view" class="hidden"> 657<div class="content-area" style="padding: 10px 20px 20px 0;"> 658<div id="talks-content"> 659<p class="normal">Loading...</p> 660</div> 661</div> 662</div> 663<!-- End Talks view --> 664 665</div><!-- end main content --> 666</div><!-- end outer flex container --> 667
668<script> 669// Contact form modal 670const contactBtn = document.getElementById('contact-btn'); 671const contactModal = document.getElementById('contact-modal'); 672const closeModal = document.getElementById('close-modal'); 673 674contactBtn.addEventListener('click', () => { 675 contactModal.classList.remove('hidden'); 676}); 677 678closeModal.addEventListener('click', () => { 679 contactModal.classList.add('hidden'); 680}); 681 682contactModal.addEventListener('click', (e) => { 683 if (e.target === contactModal) { 684 contactModal.classList.add('hidden'); 685 } 686}); 687 688// Contact form submission 689const contactForm = document.getElementById('contact-form'); 690const formStatus = document.getElementById('form-status'); 691const submitBtn = document.getElementById('submit-btn'); 692 693contactForm.addEventListener('submit', async (e) => { 694 e.preventDefault(); 695 696 submitBtn.disabled = true; 697 submitBtn.textContent = 'Sending...'; 698 formStatus.classList.add('hidden'); 699 700 const formData = new FormData(contactForm); 701 702 try { 703 const response = await fetch('/cgi-bin/contact.php', { 704 method: 'POST', 705 body: formData 706 }); 707 const result = await response.json(); 708 709 formStatus.classList.remove('hidden'); 710 if (result.success) { 711 formStatus.textContent = result.message; 712 formStatus.classList.remove('text-red-600'); 713 formStatus.classList.add('text-green-600'); 714 contactForm.reset(); 715 setTimeout(() => { 716 contactModal.classList.add('hidden'); 717 formStatus.classList.add('hidden'); 718 }, 2000); 719 } else { 720 formStatus.textContent = result.message; 721 formStatus.classList.remove('text-green-600'); 722 formStatus.classList.add('text-red-600'); 723 } 724 } catch (error) { 725 formStatus.classList.remove('hidden', 'text-green-600'); 726 formStatus.classList.add('text-red-600'); 727 formStatus.textContent = 'Failed to send message. Please try again.'; 728 } 729 730 submitBtn.disabled = false; 731 submitBtn.textContent = 'Send message'; 732}); 733</script>
733 734 735</body>
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.