PageSourceSearch

https://fancy-rolypoly-84ee13.netlify.app/shared/global-nav.js

js fancy-rolypoly-84ee13.netlify.app collected 2026-10-03 10:43:32 UTC 28,030 bytes, 515 lines download raw bytes

1// =============================================
2// GLOBAL NAV — Injected nav + drawer + all logic
3// =============================================
4
5(function() {
6  'use strict';
7
8  // Check if nav already exists
9  const alreadyExists = !!document.getElementById('topNav');
10
11  // Determine if this is an inner page (not homepage)
12  const isInner = !document.body.classList.contains('is-homepage');
13  const navClass = isInner ? 'top-nav top-nav--inner scrolled' : 'top-nav';
14
15  // ---- SVG icon fragments ----
16  const svgArrow = '<svg class="sd-link-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>';
17  const svgSearch = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>';
18  const svgSun = '<svg class="sun-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>';
19  const svgMoon = '<svg class="moon-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>';
20  const svgChevron = '<svg class="nav-index-arrow" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 4l4 4-4 4"/></svg>';
21  const svgSmallArrow = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>';
22
23  // ---- NAV HTML ----
24  const navHTML = `
25  <nav class="${navClass}" id="topNav">
26    <div class="nav-left">
27      <a class="nav-logo" href="index.html">
28        <img src="assets/energy_analytics_logomark_2.svg" alt="NCEA">
29      </a>
30      <div class="nav-index" id="navIndexBtn">
31        <span class="nav-index-dot"></span>
32        <span>Leading Energy Indicators: 72.4</span>
33        ${svgChevron}
34      </div>
35    </div>
36    <div class="nav-links">
37      <a class="nav-item" data-drawer="research" href="research-library.html">Research<sup>01</sup></a>
38      <a class="nav-item" data-drawer="events" href="events.html">Events<sup>02</sup></a>
39      <a class="nav-item" data-drawer="experts" href="advisory-board.html">Experts<sup>03</sup></a>
40      <a class="nav-item" data-drawer="about" href="about.html">About<sup>
4004</sup></a>
41      <a class="nav-text-link" href="subscribe.html">Subscribe</a>
42      <a class="nav-text-link" href="get-involved.html">Get Involved</a>
43      <div class="nav-divider"></div>
44      <div class="nav-icons">
45        <button class="nav-icon-btn" onclick="if(typeof openSearchModal==='function')openSearchModal()" aria-label="Search">
46          ${svgSearch}
47        </button>
48        <button class="nav-hamburger" id="navHamburger" aria-label="Menu">
49          <img class="ham-dark" src="assets/hamburger-dark.svg" alt="">
50          <img class="ham-white" src="assets/hamburger-white.svg" alt="">
51          <svg class="ham-x" viewBox="0 0 20 20" fill="none"><line x1="4" y1="4" x2="16" y2="16"/><line x1="16" y1="4" x2="4" y2="16"/></svg>
52        </button>
53      </div>
54    </div>
55  </nav>
56  `;
57
58  // ---- DRAWER HTML ----
59  const drawerHTML = `
60  <div class="drawer-scrim" id="drawerScrim"></div>
61  <aside class="side-drawer" id="sideDrawer" data-active-panel="research">
62    <div class="side-drawer-spacer"></div>
63    <div class="side-drawer-body">
64
65      <!-- TABS — panel switching lives inside drawer -->
66      <div class="sd-tabs">
67        <button class="sd-tab is-active" data-panel-target="research">Research</button>
68        <button class="sd-tab" data-panel-target="events">Events</button>
69        <button class="sd-tab" data-panel-target="experts">Experts</button>
70        <button class="sd-tab" data-panel-target="about">About</button>
71      </div>
72
73      <div class="sd-content">
74
75      <!-- RESEARCH -->
76      <div class="side-drawer-panel is-active" data-panel="research">
77        <div class="sd-label">Research</div>
78        <div class="sd-links">
79          <a href="esi-report.html" class="sd-link" style="font-weight:500">Energy Security Index<span class="sd-link-count">New</span>${svgArrow}</a>
80          <a href="research-library.html" class="sd-link">All Publications${svgArrow}</a>
81          <a href="research-library.html?type=studies" class="sd-link">Studies${svgArrow}</a>
82          <a href="research-library.html?type=reports" class="sd-link">Reports${svgArrow}</a>
83          <a href="research-library.html?type=analysis" class="sd-link">Analysis${svgArrow}</a>
84          <a href="research-library.html?type=testimony" class="sd-link">Testimony${svgArrow}</a>
85          <a href="topics.html" class="sd-link">Topics${svgArrow}</a>
86        </div>
87      </div>
88
89      <!-- EVENTS -->
90      <div class="side-drawer-panel" data-panel="events">
91        <div class="sd-label">Events</div>
92        <div class="sd-links">
93          <a href="events.html" class="sd-link">All Events${svgArrow}</a>
94          <a href="events.html#upcoming" class="sd-link">Upcoming${svgArrow}</a>
95          <a href="events.html#past" class="sd-link">Past Events${svgArrow}</a>
96          <a href="events.html#webinars" class="sd-link">Webinars${svgArrow}</a>
97        </div>
98      </div>
99
100      <!-- SCHOLARS -->
101      <div class="side-drawer-panel" data-panel="experts">
102        <div class="sd-label">Experts</div>
103        <div class="sd-links">
104          <a href="advisory-board.html" class="sd-link">Full Advisory Board${svgArrow}</a>
105          <a href="scholar-detail.html?slug=mark-p-mills" class="sd-link">Mark P. Mills${svgArrow}</a>
106          <a href="advisory-board.html#fellows" class="sd-link">Research Fellows${svgArrow}</a>
107          <a href="press-kit.html#scholars" class="sd-link">Expert Directory${svgArrow}</a>
108          <a href="media-requests.html" class="sd-link">Media Requests${svgArrow}</a>
109          <a href="get-involved.html" class="sd-link">Become a Scholar${svgArrow}</a>
110        </div>
111      </div>
112
113      <!-- ABOUT -->
114      <div class="side-drawer-panel" data-panel="about">
115        <div class="sd-label">About</div>
116        <div class="sd-links">
117          <a href="about.html" class="sd-link">Our Mission${svgArrow}</a>
118          <a href="about.html#approach" class="sd-link">Approach${svgArrow}</a>
119          <a href="about.html#team" class="sd-link">Leadership${svgArrow}</a>
120          <a href="contact.html" class="sd-link">Contact${svgArrow}</a>
121          <a href="request-briefing.html" class="sd-link">Request a Briefing${svgArrow}</a>
122          <a href="energy-index.html" class="sd-link">Energy Security Index${svgArrow}</a>
123          <a href="subscribe.html" class="sd-link">Subscribe${svgArrow}</a>
124        </div>
125      </div>
126
127      </div><!-- end sd-content -->
128    </div>
129  </aside>
130  `;
131
132  // ---- INJECT (only if not already present) ----
133  if (!alreadyExists) {
134    // Remove old nav if present
135    const oldNav = document.getElementById('topNav');
136    if (oldNav) oldNav.remove();
137
138    // Insert at very start of body
139    document.body.insertAdjacentHTML('afterbegin', drawerHTML);
140    document.body.insertAdjacentHTML('afterbegin', navHTML);
141  }
142
143  // ---- SCROLL BEHAVIOR ----
144  const topNav = document.getElementById('topNav');
145  if (topNav && !isInner) {
146    let lastY = 0;
147    window.addEventListener('scroll', () => {
148      const y = window.scrollY;
149      topNav.classList.toggle('scrolled', y > 80);
150      if (y > 100 && y > lastY + 2) topNav.classList.add('hidden');
151      else if (y < lastY - 2 || y < 60) topNav.classList.remove('hidden');
152      lastY = y;
153    });
154  } else if (topNav && isInner) {
155    // Inner pages: always scrolled look, hide/show on scroll
156    topNav.classList.add('scrolled');
157    let lastY = 0;
158    window.addEventListener('scroll', () => {
159      const y = window.scrollY;
160      if (y > 100 && y > lastY + 2) topNav.classList.add('hidden');
161      else if (y < lastY - 2 || y < 60) topNav.classList.remove('hidden');
162      lastY = y;
163    });
164  }
165
166  // ---- DRAWER LOGIC ----
167  const drawer = document.getElementById('sideDrawer');
168  const scrim = document.getElementById('drawerScrim');
169  const hamburger = document.getElementById('navHamburger');
170  const panels = document.querySelectorAll('.side-drawer-panel');
171  const drawerTabs = document.querySelectorAll('.sd-tab[data-panel-target]');
172  const navItems = document.querySelectorAll('.nav-item[data-drawer]');
173
174  if (!drawer || !hamburger) return;
175
176  let isOpen = false;
177  let hoverOpenTimeout = null;
178  let currentPanel = 'research';
179
180  function switchPanel(name) {
181    if (name === currentPanel && isOpen) return;
182    currentPanel = name;
183    panels.forEach(p => p.classList.toggle('is-active', p.dataset.panel === name));
184    drawerTabs.forEach(t => t.classList.toggle('is-active', t.dataset.panelTarget === name));
185    navItems.forEach(i => i.classList.toggle('is-drawer-active', i.dataset.drawer === name));
186    drawer.setAttribute('data-active-panel', name);
187    const panel = document.querySelector(`.side-drawer-panel[data-panel="${name}"]`);
188    if (panel && isOpen) animatePanel(panel);
189  }
190
191  function animatePanel(panel) {
192    if (typeof gsap === 'undefined') return;
193    const links = panel.querySelectorAll('.sd-link');
194    const labels = panel.querySelectorAll('.sd-label');
195    const tl = gsap.timeline({ defaults: { ease: 'power2.out' } });
196    if (labels.length) tl.fromTo(labels, { opacity: 0, x: -8 }, { opacity: 1, x: 0, duration: 0.25 }, 0);
197    if (links.length) tl.fromTo(links, { opacity: 0, x: -10 }, { opacity: 1, x: 0, duration: 0.3, stagger: 0.035 }, 0.04);
198  }
199
200  function openDrawer(panelName) {
201    if (isOpen) { if (panelName) switchPanel(panelName); return; }
202    isOpen = true;
203    if (panelName) switchPanel(panelName);
204    drawer.classList.add('is-open');
205    scrim.classList.add('is-active');
206    document.body.classList.add('drawer-open');
207    document.body.style.overflow = 'hidden';
208    requestAnimationFrame(() => {
209      const active = document.querySelector('.side-drawer-panel.is-active');
210      if (active) animatePanel(active);
211    });
212  }
213
214  function closeDrawer() {
215    if (!isOpen) return;
216    isOpen = false;
217    drawer.classList.remove('is-open');
218    scrim.classList.remove('is-active');
219    document.body.classList.remove('drawer-open');
220    document.body.style.overflow = '';
221    navItems.forEach(i => i.classList.remove('is-drawer-active'));
222  }
223
224  // Hamburger toggles drawer
225  hamburger.addEventListener('click', () => {
226    isOpen ? closeDrawer() : openDrawer('research');
227  });
228
229  // Top nav items: hover opens/switches on desktop, click navigates
230  navItems.forEach(item => {
231    item.addEventListener('mouseenter', () => {
232      const panel = item.dataset.drawer;
233      if (isOpen) { switchPanel(panel); }
234      else { hoverOpenTimeout = setTimeout(() => openDrawer(panel), 120); }
235    });
236    item.addEventListener('mouseleave', () => clearTimeout(hoverOpenTimeout));
237    item.addEventListener('click', (e) => {
238      if (isOpen) { e.preventDefault(); closeDrawer(); }
239    });
240  });
241
242  // Tabs inside drawer for mobile (hidden on desktop via CSS)
243  drawerTabs.forEach(tab => {
244    tab.addEventListener('click', () => {
245      switchPanel(tab.dataset.panelTarget);
246    });
247  });
248
249  scrim.addEventListener('click', closeDrawer);
250  document.addEventListener('keydown', e => { if (e.key === 'Escape' && isOpen) closeDrawer(); });
251
252  // ---- LEADING ENERGY INDICATORS MODAL ----
253  (function() {
254    // Inject modal HTML
255    var modalHTML = '<div class="lei-modal-overlay" id="leiOverlay">' +
256      '<div class="lei-modal">' +
257        '<div class="lei-modal__header">' +
258          '<span class="lei-modal__title">Leading Energy Indicators</span>' +
259          '<button class="lei-modal__close" id="leiClose" aria-label="Close">' +
260            '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>' +
261          '</button>' +
262        '</div>' +
263        '<iframe class="lei-modal__iframe" id="leiIframe" src="about:blank" frameborder="0" allow="fullscreen"></iframe>' +
264      '</div>' +
265    '</div>';
266    document.body.insertAdjacentHTML('beforeend', modalHTML);
267
268    // Inject modal CSS
269    var modalCSS = document.createElement('style');
270    modalCSS.textContent = '' +
271      '.lei-modal-overlay{position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,0.6);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;opacity:0;pointer-events:none;transition:opacity 0.3s cubic-bezier(0.16,1,0.3,1)}' +
272      '.lei-modal-overlay.is-open{opacity:1;pointer-events:auto}' +
273      '.lei-modal{width:calc(100vw - 48px);height:calc(100vh - 48px);max-width:1600px;max-height:1000px;background:var(--bg,#f8f5e7);border-radius:8px;overflow:hidden;display:flex;flex-direction:column;transform:scale(0.96) translateY(12px);transition:transform 0.35s cubic-bezier(0.16,1,0.3,1);box-shadow:0 24px 80px rgba(0,0,0,0.25)}' +
274      '.lei-modal-overlay.is-open .lei-modal{transform:scale(1) translateY(0)}' +
275      '.lei-modal__header{display:flex;align-items:center;justify-content:space-between;padding:14px 20px;border-bottom:1px solid var(--border,rgba(26,26,26,0.1));flex-shrink:0}' +
276      '.lei-modal__title{font-size:13px;font-weight:500;letter-spacing:0.02em;color:var(--text,#1a1a1a);font-family:var(--font-body,-apple-system,sans-serif)}' +
277      '.lei-modal__close{width:32px;height:32px;border-radius:50%;border:none;background:var(--tag-bg,rgba(26,26,26,0.05));cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background 0.2s}' +
278      '.lei-modal__close:hover{background:rgba(26,26,26,0.12)}' +
279      '.lei-modal__close svg{width:16px;height:16px;color:var(--text,#1a1a1a)}' +
280      '.lei-modal__iframe{flex:1;width:100%;border:none}' +
281      '@media(max-width:600px){.lei-modal{width:100vw;height:100vh;max-width:none;max-height:none;border-radius:0}.lei-modal-overlay{padding:0}}';
282    document.head.appendChild(modalCSS);
283
284    // Click handler
285    var navIndexBtn = document.getElementById('navIndexBtn');
286    var leiOverlay = document.getElementById('leiOverlay');
287    var leiClose = document.getElementById('leiClose');
288    var leiIframe = document.getElementById('leiIframe');
289
290    function openLEI() {
291      leiIframe.src = 'https://fluffy-nougat-1420dd.netlify.app/';
292      leiOverlay.classList.add('is-open');
293      document.body.style.overflow = 'hidden';
294    }
295    function closeLEI() {
296      leiOverlay.classList.remove('is-open');
297      document.body.style.overflow = '';
298      setTimeout(function() { leiIframe.src = 'about:blank'; }, 350);
299    }
300
301    if (navIndexBtn) navIndexBtn.addEventListener('click', openLEI);
302    if (leiClose) leiClose.addEventListener('click', closeLEI);
303    if (leiOverlay) leiOverlay.addEventListener('click', function(e) {
304      if (e.target === leiOverlay) closeLEI();
305    });
306    document.addEventListener('keydown', function(e) {
307      if (e.key === 'Escape' && leiOverlay && leiOverlay.classList.contains('is-open')) closeLEI();
308    });
309  })();
310
311  // =============================================
312  // GLOBAL SEARCH MODAL — inject on all pages
313  // =============================================
314  (function() {
315    if (document.getElementById('searchModalOverlay')) return;
316
317    var searchHTML = '<div class="search-modal-overlay" id="searchModalOverlay">' +
318      '<div class="search-modal" id="searchModal">' +
319      '<div class="search-modal__header">' +
320      '<div class="search-modal__input-wrap">' +
321      '<svg class="search-modal__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>
321' +
322      '<input type="text" class="search-modal__input" id="searchModalInput" placeholder="Search research, experts, events..." autocomplete="off">' +
323      '</div>' +
324      '<span class="search-modal__kbd">ESC</span>' +
325      '<button class="search-modal__close" id="searchModalClose"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg></button>' +
326      '</div>' +
327      '<div class="search-modal__toggle">' +
328      '<button class="search-modal__toggle-btn active" data-view="results"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 21l-4.35-4.35M11 19a8 8 0 100-16 8 8 0 000 16z"/></svg> Search Results</button>' +
329      '<button class="search-modal__toggle-btn" data-view="ai"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg> AI Answer <span class="search-modal__toggle-badge">Beta</span></button>' +
330      '</div>' +
331      '<div class="search-modal__body">' +
332      '<div class="search-view active" id="searchResultsView"><div class="search-empty" id="searchEmptyState"><svg class="search-empty__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg><p class="search-empty__title">Search NCEA</p><p class="search-empty__hint">Find research, experts, events, and media</p><div class="search-quick-tags"><span class="search-quick-tag" data-query="grid reliability">Grid reliability</span><span class="search-quick-tag" data-query="nuclear">Nuclear</span><span class="search-quick-tag" data-query="renewable costs">Renewable costs</span></div></div><div id="searchResultsContainer" style="display:none"></div></div>' +
333      '<div class="search-view" id="aiAnswerView"><div class="search-empty" id="aiEmptyState"><svg class="search-empty__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg><p class="search-empty__title">Ask a question</p><p class="search-empty__hint">Get an AI-powered answer grounded in NCEA research</p></div><div id="aiAnswerContainer" style="display:none"></div></div>' +
334      '</div></div></div>';
335
336    document.body.insertAdjacentHTML('beforeend', searchHTML);
337
338    var overlay = document.getElementById('searchModalOverlay');
339    var input = document.getElementById('searchModalInput');
340    var emptyState = document.getElementById('searchEmptyState');
341    var resultsContainer = document.getElementById('searchResultsContainer');
342    var aiEmpty = document.getElementById('aiEmptyState');
343    var aiContainer = document.getElementById('aiAnswerContainer');
344    var resultsView = document.getElementById('searchResultsView');
345    var aiView = document.getElementById('aiAnswerView');
346
347    window.openSearchModal = function(q) {
348      overlay.classList.add('is-open');
349      document.body.style.overflow = 'hidden';
350      setTimeout(function() { input.focus(); if (q) { input.value = q; doSearch(q); } }, 100);
351    };
352    window.closeSearchModal = function() {
353      overlay.classList.remove('is-open');
354      document.body.style.overflow = '';
355      setTimeout(function() { input.value = ''; emptyState.style.display='block'; resultsContainer.style.display='none'; resultsContainer.innerHTML=''; aiEmpty.style.display='block'; aiContainer.style.display='none'; aiContainer.innerHTML=''; }, 300);
356    };
357
358    overlay.addEventListener('click', function(e) { if (e.target === overlay) window.closeSearchModal(); });
359    document.getElementById('searchModalClose').addEventListener('click', window.closeSearchModal);
360
361    document.querySelectorAll('.search-modal__toggle-btn').forEach(function(btn) {
362      btn.addEventListener('click', function() {
363        document.querySelectorAll('.search-modal__toggle-btn').forEach(function(b){b.classList.remove('active');});
364        btn.classList.add('active');
365        if (btn.dataset.view === 'results') { resultsView.classList.add('active'); aiView.classList.remove('active'); }
366        else { resultsView.classList.remove('active'); aiView.classList.add('active'); }
367      });
368    });
369
370    document.querySelectorAll('.search-quick-tag').forEach(function(tag) {
371      tag.addEventListener('click', function() { input.value = tag.dataset.query; input.focus(); doSearch(tag.dataset.query); });
372    });
373
374    document.addEventListener('keydown', function(e) {
375      if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); overlay.classList.contains('is-open') ? window.closeSearchModal() : window.openSearchModal(); }
376      if (e.key === 'Escape' && overlay.classList.contains('is-open')) window.closeSearchModal();
377    });
378
379    // Search data + logic (works on all pages)
380    var searchItems = [
381      {cat:'Research',title:'The Real Cost of Renewable Mandates',meta:'Study \u2022 Mark P. Mills \u2022 Jan 2026',href:'article.html'},
382      {cat:'Research',title:'Grid Reliability Under High Renewable Penetration',meta:'Study \u2022 Neil Atkinson \u2022 Dec 2025',href:'article.html'},
383      {cat:'Research',title:'Nuclear Energy Economics: A Fresh Look',meta:'Issue Brief \u2022 Lars Schernikau \u2022 Nov 2025',href:'article.html'},
384      {cat:'Research',title:'U.S. Energy Security Index',meta:'Flagship Report \u2022 Q4 2025',href:'esi-report.html'},
385      {cat:'Research',title:'The AI Energy Surge: Quantifying Compute Demand',meta:'Study \u2022 Mark P. Mills \u2022 2025',href:'article.html'},
386      {cat:'Experts',title:'Mark P. Mills',meta:'Executive Director \u2022 Energy Policy',href:'scholar-detail.html?slug=mark-p-mills'},
387      {cat:'Experts',title:'Neil Atkinson',meta:'Visiting Fellow \u2022 Oil Markets',href:'scholar-detail.html?slug=neil-atkinson'},
388      {cat:'Experts',title:'Paul H. Tice',meta:'Advisory Board \u2022 Energy Finance',href:'scholar-detail.html?slug=paul-tice'},
389      {cat:'Experts',title:'Brent Bennett, PhD',meta:'Senior Fellow \u2022 Grid Policy',href:'scholar-detail.html?slug=brent-bennett'},
390      {cat:'Experts',title:'Steven Koonin',meta:'Advisory Board \u2022 University Professor, NYU',href:'scholar-detail.html?slug=steven-koonin'},
391      {cat:'Events',title:'Energy Outlook 2026: What\u2019s Ahead',meta:'Webinar \u2022 Mar 2026',href:'event-detail.html'},
392      {cat:'Events',title:'Congressional Testimony on Grid Reliability',meta:'Testimony \u2022 Feb 2026',href:'events.html'},
393      {cat:'Media',title:'CNBC: The Hidden Costs of the Energy Transition',meta:'CNBC \u2022 Feb 2026',href:'media.html'},
394      {cat:'Media',title:'WSJ Op-Ed: Why Energy Policy Needs Better Data',meta:'Wall Street Journal \u2022 2026',href:'media.html'}
395    ];
396
397    function doSearch(q) {
398      if (!q || !q.trim()) {
399        emptyState.style.display = 'block';
400        resultsContainer.style.display = 'none';
401        resultsContainer.innerHTML = '';
402        return;
403      }
404      var lower = q.toLowerCase();
405      var matches = searchItems.filter(function(item) {
406        return item.title.toLowerCase().indexOf(lower) !== -1 || item.meta.toLowerCase().indexOf(lower) !== -1 || item.cat.toLowerCase().indexOf(lower) !== -1;
407      });
408      emptyState.style.display = 'none';
409      resultsContainer.style.display = 'block';
410      if (!matches.length) {
411        resultsContainer.innerHTML = '<p style="padding:40px 0;text-align:center;color:rgba(26,26,26,0.4);font-size:14px">No results found for \u201c' + q + '\u201d</p>';
412        return;
413      }
414      var groups = {};
415      matches.forEach(function(m) { if (!groups[m.cat]) groups[m.cat] = []; groups[m.cat].push(m); });
416      var html = '';
417      Object.keys(groups).forEach(function(cat) {
418        html += '<div style="margin-bottom:24px"><div style="display:flex;justify-content:space-between;padding:8px 0;border-bottom:1px solid rgba(26,26,26,0.08);margin-bottom:8px"><span style="font-size:11px;font-weight:600;letter-spacing:0.1em;text-transform:uppercase;color:rgba(26,26,26,0.35)">' + cat + '</span><span style="font-size:12px;color:rgba(26,26,26,0.3)">' + groups[cat].length + '</span></div>';
419        groups[cat].forEach(function(item) {
420          html += '<a href="' + item.href + '" style="display:block;padding:12px 0;border-bottom:1px solid rgba(26,26,26,0.04);text-decoration:none;color:inherit"><div style="font-size:15px;font-weight:500;margin-bottom:2px">' + item.title + '</div><div style="font-size:12px;color:rgba(26,26,26,0.4)">' + item.meta + '</div></a>';
421        });
422        html += '</div>';
423      });
424      resultsContainer.innerHTML = html;
425    }
426
427    var searchTimeout;
428    input.addEventListener('input', function() {
429      clearTimeout(searchTimeout);
430      searchTimeout = setTimeout(function() { doSearch(input.value); }, 150);
431    });
432    input.addEventListener('keydown', function(e) {
433      if (e.key === 'Enter') { e.preventDefault(); doSearch(input.value); }
434    });
435  })();
436
437
438
439
440  // ================================================
441  // PAGE TRANSITION — olive panel + logomark
442  // ================================================
443  (function(){
444    var css = document.createElement('style');
445    css.textContent = '.ptr{position:fixed;inset:0;z-index:99999;pointer-events:none;overflow:hidden}.ptr__panel{position:absolute;inset:0;background:#817828;will-change:transform}.ptr__logo{position:absolute;top:50%;left:50%;width:112px;height:112px;margin:-56px 0 0 -56px;opacity:0;will-change:opacity}';
446    document.head.appendChild(css);
447
448    var el = document.createElement('div');
449    el.className = 'ptr';
450    el.innerHTML = '<div class="ptr__panel"><img class="ptr__logo" src="assets/logomark.svg" alt=""></div>';
451    document.body.appendChild(el);
452
453    var panel = el.querySelector('.ptr__panel');
454    var logo = el.querySelector('.ptr__logo');
455    var busy = false;
456
457    // REVEAL — arriving from transition
458    if (sessionStorage.getItem('ptr_active')) {
459      sessionStorage.removeItem('ptr_active');
460      document.documentElement.classList.remove('ptr-cover');
461      if (typeof gsap !== 'undefined') {
462        gsap.set(panel, { y: '0%' });
463        gsap.set(logo, { opacity: 1 });
464        gsap.to(logo, { opacity: 0, duration: 0.2, delay: 0.05, ease: 'none' });
465        gsap.to(panel, { y: '-100%', duration: 0.85, delay: 0.12, ease: 'power4.inOut' });
466      }
467      // Hard fallback
468      setTimeout(function() { panel.style.transform = 'translateY(-100%)'; logo.style.opacity = '0'; }, 1500);
469    } else {
470      panel.style.transform = 'translateY(-100%)';
471    }
472
473    // COVER + NAVIGATE on link click
474    document.addEventListener('click', function(e) {
475      var link = e.target.closest('a');
476      if (!link || busy) return;
477      var href = link.getAttribute('href');
478      if (!href) return;
479      if (/^(http|#|javascript|mailto|tel)/.test(href)) return;
480      if (e.metaKey || e.ctrlKey || e.shiftKey || link.target === '_blank') return;
481
482      e.preventDefault();
483      busy = true;
484      sessionStorage.setItem('ptr_active', '1');
485
486      if (typeof gsap !== 'undefined') {
487        gsap.set(panel, { y: '100%' });
488        gsap.set(logo, { opacity: 0 });
489        gsap.to(panel, { y: '0%', duration: 0.8, ease: 'power4.inOut' });
490        gsap.to(logo, { opacity: 1, duration: 0.4, delay: 0.5, ease: 'power2.out' });
491      }
492      // Navigate after animation (hard timeout, not onComplete)
493      setTimeout(function() { window.location.href = href; }, 1200);
494    });
495
496    // Browser back/forward — bfcache restore
497    window.addEventListener('pageshow', function(e) {
498      if (e.persisted) {
499        busy = false;
500        sessionStorage.removeItem('ptr_active');
501        document.documentElement.classList.remove('ptr-cover');
502        panel.style.transform = 'translateY(-100%)';
503        logo.style.opacity = '0';
504      }
505    });
506  })();
507
508})();
509
510// Fallback — ensure nav exists after DOM is ready
511document.addEventListener('DOMContentLoaded', function() {
512  if (!document.getElementById('topNav')) {
513    console.warn('NCEA: topNav missing after DOMContentLoaded, page may need hard refresh');
514  }
515});

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.