PageSourceSearch

https://nosuch.com/

html nosuch.com collected 2026-10-02 07:02:43 UTC 24,341 bytes, 735 lines download raw bytes

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">&times;</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.