PageSourceSearch

https://larrywhitesidejr.com/script.js?v=3

js larrywhitesidejr.com collected 2026-10-09 22:40:21 UTC 24,425 bytes, 737 lines download raw bytes

1/* ================================================================
2   LARRY WHITESIDE JR. — script.js
3   ================================================================
4   RENDERING ENGINE ONLY. Do not edit this file for content updates.
5   All site content lives in content.js.
6   ================================================================ */
7
8'use strict';
9
10/* ----------------------------------------------------------------
11   GOOGLE CALENDAR EMBED
12   ---------------------------------------------------------------- */
13function renderCalendar() {
14  const wrap = document.getElementById('calendarEmbed');
15  if (!wrap) return;
16
17  if (!publicCalendarUrl || !publicCalendarUrl.trim()) {
18    wrap.innerHTML = `
19      <div class="calendar-placeholder glass-panel">
20        <div class="calendar-placeholder__icon" aria-hidden="true">📅</div>
21        <p class="calendar-placeholder__text">
22          Calendar coming soon — add your Google Calendar embed URL to <code>content.js</code>.
23        </p>
24        <a href="https://www.thecisosociety.com/meetups" class="btn btn--outline" target="_blank" rel="noopener noreferrer">
25          View CISO Society Events →
26        </a>
27      </div>
28    `;
29    return;
30  }
31
32  wrap.innerHTML = `
33    <div class="calendar-embed__frame-wrap reveal">
34      <iframe
35        src="${publicCalendarUrl}"
36        style="border:0"
37        frameborder="0"
38        scrolling="no"
39        title="Larry Whiteside Jr. — Upcoming Events"
40        loading="lazy"
41      ></iframe>
42    </div>
43    <div class="calendar-embed__cta reveal">
44      <a href="${publicCalendarUrl.replace('embed?', 'r/').replace('%40', '@')}" class="btn btn--outline" target="_blank" rel="noopener noreferrer">
45        View Full Calendar →
46      </a>
47    </div>
48  `;
49  observeNew();
50}
51
52
53/* ----------------------------------------------------------------
54   CAREER TIMELINE
55   ---------------------------------------------------------------- */
56function renderTimeline() {
57  const list = document.getElementById('timelineList');
58  if (!list) return;
59
60  timeline.forEach(item => {
61    const el = document.createElement('div');
62    el.className = 'timeline-item';
63    el.innerHTML = `
64      <span class="timeline-item__year">${item.year}</span>
65      <div class="timeline-item__dot" aria-hidden="true"></div>
66      <div class="timeline-item__body">
67        <p class="timeline-item__title">${item.title}</p>
68        <p class="timeline-item__org">${item.industry}</p>
69      </div>
70    `;
71    list.appendChild(el);
72  });
73}
74
75
76/* ----------------------------------------------------------------
77   SPEAKING TOPICS
78   ---------------------------------------------------------------- */
79function renderSpeakingTopics() {
80  const grid = document.getElementById('speakingTopicsGrid');
81  if (!grid) return;
82
83  speakingTopics.forEach((topic, i) => {
84    const card = document.createElement('div');
85    card.className = 'speaking__topic-card reveal';
86    card.style.transitionDelay = `${(i % 4) * 0.07}s`;
87
88    const takeawaysHTML = topic.takeaways
89      ? `<ul class="speaking__topic-takeaways">
90           ${topic.takeaways.map(t => `<li>${t}</li>`).join('')}
91         </ul>`
92      : '';
93
94    card.innerHTML = `
95      <div class="speaking__topic-header">
96        <div class="speaking__topic-icon" aria-hidden="true">${topic.icon}</div>
97        <h3>${topic.title}</h3>
98      </div>
99      <p>${topic.desc}</p>
100      ${takeawaysHTML}
101    `;
102    grid.appendChild(card);
103  });
104}
105
106
107/* ----------------------------------------------------------------
108   SPEAKING DIFFERENTIATORS
109   ---------------------------------------------------------------- */
110function renderSpeakingDifferentiators() {
111  const grid = document.getElementById('speakingDifferentiators');
112  if (!grid) return;
113
114  speakingDifferentiators.forEach((item, i) => {
115    const card = document.createElement('div');
116    card.className = 'speaking__diff-card reveal';
117    card.style.transitionDelay = `${(i % 4) * 0.07}s`;
118    card.innerHTML = `
119      <div class="speaking__diff-icon" aria-hidden="true">${item.icon}</div>
120      <h4>${item.title}</h4>
121      <p>${item.desc}</p>
122    `;
123    grid.appendChild(card);
124  });
125}
126
127
128/* ----------------------------------------------------------------
129   IDEAL AUDIENCES
130   ---------------------------------------------------------------- */
131function renderSpeakingAudiences() {
132  const grid = document.getElementById('speakingAudiences');
133  if (!grid) return;
134
135  speakingAudiences.forEach(a => {
136    const chip = document.createElement('div');
137    chip.className = 'speaking__audience-chip';
138    chip.innerHTML = `<span aria-hidden="true">${a.icon}</span> ${a.label}`;
139    grid.appendChild(chip);
140  });
141}
142
143
144/* ----------------------------------------------------------------
145   PAST VENUES
146   ---------------------------------------------------------------- */
147function renderSpeakingVenues() {
148  const grid = document.getElementById('speakingVenues');
149  if (!grid) return;
150
151  speakingVenues.forEach((v, i) => {
152    const card = document.createElement('div');
153    card.className = 'speaking__venue-card reveal';
154    card.style.transitionDelay = `${(i % 4) * 0.06}s`;
155    card.innerHTML = `
156      <p class="speaking__venue-name">${v.name}</p>
157      <p class="speaking__venue-type">${v.type}</p>
158    `;
159    grid.appendChild(card);
160  });
161}
162
163
164/* ----------------------------------------------------------------
165   SPEAKING FORMATS
166   ---------------------------------------------------------------- */
167function renderSpeakingFormats() {
168  const grid = document.getElementById('speakingFormats');
169  if (!grid) return;
170
171  speakingFormats.forEach((f, i) => {
172    const card = document.createElement('div');
173    card.className = 'speaking__format-card reveal';
174    card.style.transitionDelay = `${(i % 3) * 0.07}s`;
175    card.innerHTML = `
176      <div class="speaking__format-icon" aria-hidden="true">${f.icon}</div>
177      <div>
178        <p class="speaking__format-label">${f.label}</p>
179        <p class="speaking__format-desc">${f.desc}</p>
180      </div>
181    `;
182    grid.appendChild(card);
183  });
184}
185
186
187/* ----------------------------------------------------------------
188   FEATURED ORGANIZATIONS
189   ---------------------------------------------------------------- */
190function renderFeaturedOrgs() {
191  const grid = document.getElementById('orgsFeatured');
192  if (!grid) return;
193
194  featuredOrgs.forEach((org, i) => {
195    const card = document.createElement(org.url ? 'a' : 'div');
196    card.className = 'org-card reveal' + (org.url ? ' org-card--link' : '');
197    card.style.transitionDelay = `${i * 0.07}s`;
198    if (org.url) { card.href = org.url; card.target = '_blank'; card.rel = 'noopener noreferrer'; }
199
200    const logoContent = org.logoImg
201      ? `<img src="${org.logoImg}" alt="${org.name} logo" />`
202      : org.logo;
203
204    card.innerHTML = `
205      <div class="org-card__header">
206        <div class="org-card__logo" aria-hidden="${!!org.logoImg}">${logoContent}</div>
207        <div>
208          <p class="org-card__role">${org.role}</p>
209          <p class="org-card__name">${org.name}</p>
210        </div>
211      </div>
212      <p class="org-card__desc">${org.desc}</p>
213      ${org.url ? `<span class="org-card__link">Visit ${org.name} →</span>` : ''}
214    `;
215    grid.appendChild(card);
216  });
217}
218
219
220/* ----------------------------------------------------------------
221   PARTNER MARQUEE
222   ---------------------------------------------------------------- */
223function renderPartners() {
224  const track = document.getElementById('partnersTrack');
225  if (!track) return;
226
227  [...partners, ...partners].forEach(p => {
228    const el = document.createElement('div');
229    el.className = 'partner-item';
230    el.innerHTML = `<div class="partner-item__logo" aria-hidden="true">${p.logo}</div>${p.name}`;
231    track.appendChild(el);
232  });
233}
234
235
236/* ----------------------------------------------------------------
237   SOCIAL MEDIA GRID
238   ---------------------------------------------------------------- */
239function renderSocialLinks() {
240  const grid     = document.getElementById('socialGrid');
241  const footerEl = document.getElementById('footerSocial');
242
243  if (grid) {
244    socialLinks.filter(s => s.url).forEach(social => {
245      const card = document.createElement('a');
246      card.className = 'social-card reveal';
247      card.href   = social.url;
248      card.target = '_blank';
249      card.rel    = 'noopener noreferrer';
250      card.setAttribute('aria-label', `Follow on ${social.platform}`);
251
252      const isGradient = social.color.includes('gradient');
253      card.innerHTML = `
254        <div class="social-card__icon" style="background:${isGradient ? social.color : social.color + '22'}" aria-hidden="true">${social.icon}</div>
255        <span class="social-card__platform">${social.platform}</span>
256        <span class="social-card__handle">${social.handle}</span>
257        <span class="social-card__follow" style="color:${isGradient ? 'var(--color-gold)' : social.color}">Follow</span>
258      `;
259      grid.appendChild(card);
260    });
261  }
262
263  if (footerEl) {
264    socialLinks.filter(s => s.url).forEach(social => {
265      const a = document.createElement('a');
266      a.className = 'footer-social-link';
267      a.href = social.url;
268      a.target = '_blank';
269      a.rel = 'noopener noreferrer';
270      a.setAttribute('aria-label', social.platform);
271      a.textContent = social.icon;
272      footerEl.appendChild(a);
273    });
274  }
275}
276
277
278/* ----------------------------------------------------------------
279   LINKEDIN FEATURED POST
280   ---------------------------------------------------------------- */
281function renderLinkedInPost() {
282  const wrap = document.getElementById('linkedInPostWrap');
283  if (!wrap) return;
284
285  if (!linkedInPost.embedUrl || !linkedInPost.embedUrl.trim()) {
286    wrap.style.display = 'none';
287    return;
288  }
289
290  wrap.style.display = '';
291  wrap.innerHTML = `
292    <div class="glass-panel linkedin-embed-panel reveal">
293      <div class="linkedin-embed-panel__header">
294        <div class="linkedin-embed-panel__logo" aria-hidden="true">in</div>
295        <div>
296          <p class="linkedin-embed-panel__label">Latest on LinkedIn</p>
297          <p class="linkedin-embed-panel__name">Larry Whiteside Jr.</p>
298        </div>
299      </div>
300      <div class="linkedin-embed-panel__frame-wrap">
301        <iframe
302          src="${linkedInPost.embedUrl}"
303          class="linkedin-embed-panel__iframe"
304          frameborder="0"
305          allowfullscreen=""
306          title="Latest LinkedIn post by Larry Whiteside Jr."
307          loading="lazy"
308        ></iframe>
309      </div>
310      ${linkedInPost.caption ? `<p class="linkedin-embed-panel__caption">${linkedInPost.caption}</p>` : ''}
311      <a
312        href="https://www.linkedin.com/in/larrywhitesidejr"
313        class="btn btn--outline btn--sm linkedin-embed-panel__follow"
314        target="_blank"
315        rel="noopener noreferrer"
316      >
317        Follow on LinkedIn →
318      </a>
319    </div>
320  `;
321  observeNew();
322}
323
324
325/* ----------------------------------------------------------------
326   FEATURED CONTENT
327   ----------------------------------------------------------------
328   Auto-detects YouTube links and opens them in an inline player.
329   All other links open in a new tab.
330   ---------------------------------------------------------------- */
331function extractYouTubeId(url) {
332  if (!url) return null;
333  const patterns = [
334    /youtube\.com\/watch\?v=([^&]+)/,
335    /youtu\.be\/([^?]+)/,
336    /youtube\.com\/embed\/([^?]+)/
337  ];
338  for (const re of patterns) {
339    const m = url.match(re);
340    if (m) return m[1];
341  }
342  return null;
343}
344
345function renderFeaturedContent() {
346  const grid = document.getElementById('contentGrid');
347  if (!grid) return;
348
349  grid.innerHTML = '';
350
351  featuredContent.forEach((item, i) => {
352    const card = document.createElement('article');
353    card.className = 'content-card reveal';
354    card.style.transitionDelay = `${i * 0.06}s`;
355
356    const youtubeId = extractYouTubeId(item.link);
357    const hasThumb  = item.image && item.image.trim();
358
359    const thumbHTML = youtubeId
360      ? `<img src="https://img.youtube.com/vi/${youtubeId}/hqdefault.jpg" alt="${item.title}" loading="lazy" />`
361      : hasThumb
362        ? `<img src="${item.image}" alt="${item.title}" loading="lazy" />`
363        : `<div class="content-card__thumb-placeholder">🔗</div>`;
364
365    const showPlay = !!youtubeId;
366
367    card.innerHTML = `
368      <div class="content-card__thumb">
369        ${thumbHTML}
370        ${showPlay ? `<div class="content-card__play" aria-hidden="true">▶</div>` : ''}
371      </div>
372      <div class="content-card__body">
373        <p class="content-card__title">${item.title}</p>
374        ${item.description ? `<p class="content-card__meta">${item.description}</p>` : ''}
375      </div>
376    `;
377
378    card.style.cursor = item.link ? 'pointer' : 'default';
379
380    if (youtubeId) {
381      card.addEventListener('click', () => openVideoLightbox(youtubeId, item.title));
382    } else if (item.link) {
383      card.addEventListener('click', () => window.open(item.link, '_blank', 'noopener'));
384    }
385
386    grid.appendChild(card);
387  });
388
389  observeNew();
390}
391
392
393/* ----------------------------------------------------------------
394   YOUTUBE VIDEO LIGHTBOX
395   ---------------------------------------------------------------- */
396function openVideoLightbox(youtubeId, title) {
397  const lb   = document.getElementById('lightbox');
398  const wrap = document.getElementById('lightboxImg');
399  const cap  = document.getElementById('lightboxCaption');
400  const prev = document.getElementById('lightboxPrev');
401  const next = document.getElementById('lightboxNext');
402  if (!lb) return;
403
404  if (prev) prev.style.display = 'none';
405  if (next) next.style.display = 'none';
406
407  let iframe = lb.querySelector('.video-lightbox__iframe');
408  if (!iframe) {
409    iframe = document.createElement('iframe');
410    iframe.className = 'video-lightbox__iframe';
411    iframe.setAttribute('allowfullscreen', '');
412    iframe.setAttribute('allow', 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture');
413    iframe.setAttribute('title', title || 'YouTube video');
414    if (wrap) wrap.style.display = 'none';
415    lb.querySelector('.lightbox__img-wrap').appendChild(iframe);
416  }
417
418  iframe.src = `https://www.youtube.com/embed/${youtubeId}?autoplay=1&rel=0`;
419  if (cap) cap.textContent = title || '';
420
421  lb.classList.remove('hidden');
422  lb.dataset.mode = 'video';
423  document.body.style.overflow = 'hidden';
424}
425
426function closeVideoLightbox() {
427  const lb    = document.getElementById('lightbox');
428  const wrap  = document.getElementById('lightboxImg');
429  const iframe = lb ? lb.querySelector('.video-lightbox__iframe') : null;
430  const prev  = document.getElementById('lightboxPrev');
431  const next  = document.getElementById('lightboxNext');
432
433  if (iframe) { iframe.src = ''; iframe.remove(); }
434  if (wrap)  wrap.style.display = '';
435  if (prev)  prev.style.display = '';
436  if (next)  next.style.display = '';
437  if (lb)    { lb.classList.add('hidden'); delete lb.dataset.mode; }
438  document.body.style.overflow = '';
439}
440
441
442/* ----------------------------------------------------------------
443   CONTACT LINKS
444   ---------------------------------------------------------------- */
445function renderContactLinks() {
446  const el = document.getElementById('contactLinks');
447  if (!el) return;
448
449  contactInfo.forEach(item => {
450    const link = document.createElement('a');
451    link.className = 'contact-link';
452    link.href = item.href;
453    if (item.href.startsWith('http')) { link.target = '_blank'; link.rel = 'noopener noreferrer'; }
454    link.innerHTML = `
455      <div class="contact-link__icon" aria-hidden="true">${item.icon}</div>
456      <div>
457        <span class="contact-link__label">${item.label}</span>
458        <span class="contact-link__value">${item.value}</span>
459      </div>
460    `;
461    el.appendChild(link);
462  });
463}
464
465
466/* ----------------------------------------------------------------
467   CONTACT FORM
468   ---------------------------------------------------------------- */
469function initContactForm() {
470  const form    = document.getElementById('contactForm');
471  const success = document.getElementById('formSuccess');
472  if (!form) return;
473
474  form.addEventListener('submit', e => {
475    e.preventDefault();
476
477    const name    = form.querySelector('#contactName');
478    const email   = form.querySelector('#contactEmail');
479    const message = form.querySelector('#contactMessage');
480    let valid = true;
481
482    [name, email, message].forEach(field => {
483      field.style.borderColor = '';
484      if (!field.value.trim()) { field.style.borderColor = '#e05555'; valid = false; }
485    });
486
487    if (email && email.value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value)) {
488      email.style.borderColor = '#e05555'; valid = false;
489    }
490
491    if (!valid) return;
492
493    /*
494      NETLIFY FORMS:
495      Once you add data-netlify="true" to the form and deploy,
496      replace this block with:
497        const data = new FormData(form);
498        fetch('/', { method: 'POST', body: data })
499          .then(() => { form.classList.add('hidden'); success.classList.remove('hidden'); });
500    */
501    form.classList.add('hidden');
502    if (success) success.classList.remove('hidden');
503  });
504}
505
506
507/* ----------------------------------------------------------------
508   GALLERY — reads from gallery-manifest.js
509   ---------------------------------------------------------------- */
510let lightboxIndex = 0;
511let visibleImages = [];
512
513function renderGallery() {
514  const grid = document.getElementById('galleryMasonry');
515  if (!grid) return;
516
517  grid.innerHTML = '';
518
519  const images = typeof galleryManifest !== 'undefined' ? galleryManifest : [];
520  visibleImages = images;
521
522  if (images.length === 0) {
523    grid.innerHTML = `
524      <div class="gallery__empty glass-panel">
525        <p>Photos coming soon — add images to <code>assets/images/gallery/</code> and update <code>gallery-manifest.js</code>.</p>
526      </div>
527    `;
528    return;
529  }
530
531  images.forEach((img, i) => {
532    const item = document.createElement('div');
533    item.className = 'gallery__item reveal';
534    item.style.transitionDelay = `${(i % 8) * 0.04}s`;
535    item.setAttribute('role', 'button');
536    item.setAttribute('tabindex', '0');
537    item.setAttribute('aria-label', img.caption || `Gallery photo ${i + 1}`);
538
539    item.innerHTML = `
540      <img src="${img.src}" alt="${img.caption || ''}" loading="lazy" />
541      ${img.caption ? `<span class="gallery__item-caption">${img.caption}</span>` : ''}
542    `;
543
544    item.addEventListener('click',   () => openLightbox(i));
545    item.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openLightbox(i); } });
546
547    grid.appendChild(item);
548  });
549
550  observeNew();
551}
552
553
554/* ----------------------------------------------------------------
555   LIGHTBOX
556   ---------------------------------------------------------------- */
557function openLightbox(index) {
558  const lb    = document.getElementById('lightbox');
559  const lbImg = document.getElementById('lightboxImg');
560  const lbCap = document.getElementById('lightboxCaption');
561  if (!lb) return;
562
563  lightboxIndex = index;
564  const img = visibleImages[index];
565
566  lbImg.src = img.src || '';
567  lbImg.alt = img.caption || '';
568  if (lbCap) lbCap.textContent = img.caption || '';
569  lb.classList.remove('hidden');
570  document.body.style.overflow = 'hidden';
571}
572
573function navigateLightbox(dir) {
574  if (visibleImages.length === 0) return;
575  lightboxIndex = (lightboxIndex + dir + visibleImages.length) % visibleImages.length;
576  openLightbox(lightboxIndex);
577}
578
579function closeLightbox() {
580  const lb = document.getElementById('lightbox');
581  if (!lb) return;
582  if (lb.dataset.mode === 'video') {
583    closeVideoLightbox();
584  } else {
585    lb.classList.add('hidden');
586    document.body.style.overflow = '';
587  }
588}
589
590function initLightbox() {
591  const lb    = document.getElementById('lightbox');
592  const close = document.getElementById('lightboxClose');
593  const prev  = document.getElementById('lightboxPrev');
594  const next  = document.getElementById('lightboxNext');
595
596  if (close) close.addEventListener('click', closeLightbox);
597  if (prev)  prev.addEventListener('click',  () => navigateLightbox(-1));
598  if (next)  next.addEventListener('click',  () => navigateLightbox(+1));
599  if (lb)    lb.addEventListener('click', e => { if (e.target === lb) closeLightbox(); });
600
601  document.addEventListener('keydown', e => {
602    if (!lb || lb.classList.contains('hidden')) return;
603    if (e.key === 'Escape')     closeLightbox();
604    if (e.key === 'ArrowLeft')  navigateLightbox(-1);
605    if (e.key === 'ArrowRight') navigateLightbox(+1);
606  });
607}
608
609
610/* ----------------------------------------------------------------
611   SCROLL REVEAL
612   ---------------------------------------------------------------- */
613function initScrollReveal() {
614  const obs = new IntersectionObserver(
615    entries =>
615 entries.forEach(entry => {
616      if (entry.isIntersecting) { entry.target.classList.add('is-visible'); obs.unobserve(entry.target); }
617    }),
618    { threshold: 0.1, rootMargin: '0px 0px -30px 0px' }
619  );
620  document.querySelectorAll('.reveal').forEach(el => obs.observe(el));
621}
622
623function observeNew() {
624  const obs = new IntersectionObserver(
625    entries => entries.forEach(entry => {
626      if (entry.isIntersecting) { entry.target.classList.add('is-visible'); obs.unobserve(entry.target); }
627    }),
628    { threshold: 0.08 }
629  );
630  document.querySelectorAll('.reveal:not(.is-visible)').forEach(el => obs.observe(el));
631}
632
633
634/* ----------------------------------------------------------------
635   ANIMATED COUNTERS (hero stats)
636   ---------------------------------------------------------------- */
637function animateCounter(el) {
638  const target   = parseInt(el.dataset.target, 10);
639  const duration = 1800;
640  const steps    = Math.ceil(duration / 16);
641  let count = 0;
642
643  const timer = setInterval(() => {
644    count++;
645    const value = Math.round(target * (count / steps));
646    el.textContent = value >= 1000 ? (value / 1000).toFixed(value < 10000 ? 1 : 0) + 'K' : value;
647    if (count >= steps) {
648      el.textContent = target >= 1000 ? (target / 1000).toFixed(target < 10000 ? 1 : 0) + 'K' : target;
649      clearInterval(timer);
650    }
651  }, 16);
652}
653
654function initCounters() {
655  const obs = new IntersectionObserver(entries => {
656    entries.forEach(entry => {
657      if (entry.isIntersecting) { animateCounter(entry.target); obs.unobserve(entry.target); }
658    });
659  }, { threshold: 0.5 });
660
661  document.querySelectorAll('.hero__stat-number[data-target]').forEach(el => obs.observe(el));
662}
663
664
665/* ----------------------------------------------------------------
666   STICKY NAV + ACTIVE LINK HIGHLIGHTING
667   ---------------------------------------------------------------- */
668function initNav() {
669  const nav    = document.getElementById('nav');
670  const toggle = document.getElementById('navToggle');
671  const links  = document.getElementById('navLinks');
672
673  window.addEventListener('scroll', () => {
674    nav.classList.toggle('scrolled', window.scrollY > 40);
675  }, { passive: true });
676
677  if (toggle && links) {
678    toggle.addEventListener('click', () => {
679      const open = links.classList.toggle('is-open');
680      toggle.classList.toggle('is-open', open);
681      toggle.setAttribute('aria-expanded', String(open));
682    });
683
684    links.querySelectorAll('a').forEach(a => {
685      a.addEventListener('click', () => {
686        links.classList.remove('is-open');
687        toggle.classList.remove('is-open');
688        toggle.setAttribute('aria-expanded', 'false');
689      });
690    });
691  }
692}
693
694
695/* ----------------------------------------------------------------
696   FOOTER YEAR
697   ---------------------------------------------------------------- */
698function setFooterYear() {
699  const el = document.getElementById('footerYear');
700  if (el) el.textContent = new Date().getFullYear();
701}
702
703
704/* ================================================================
705   INIT
706   ================================================================ */
707document.addEventListener('DOMContentLoaded', () => {
708  setFooterYear();
709  initNav();
710
711  renderTimeline();
712  renderCalendar();
713
714  renderSpeakingTopics();
715  renderSpeakingDifferentiators();
716  renderSpeakingAudiences();
717  renderSpeakingVenues();
718  renderSpeakingFormats();
719
720  renderFeaturedOrgs();
721  renderPartners();
722
723  renderSocialLinks();
724
725  renderLinkedInPost();
726  renderFeaturedContent();
727
728  renderContactLinks();
729  initContactForm();
730
731  renderGallery();
732  initLightbox();
733
734  initScrollReveal();
735  observeNew();
736  initCounters();
737});

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.