PageSourceSearch

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

js fancy-rolypoly-84ee13.netlify.app collected 2026-10-03 10:43:30 UTC 33,290 bytes, 534 lines download raw bytes

1// Force light theme (dark mode removed)
2localStorage.removeItem("theme");
3localStorage.removeItem("ncea-theme");
4// =============================================
5// SHARED COMPONENTS JS
6// =============================================
7
8// Theme toggle
9
10// Nav hide on scroll
11(function() {
12  let lastScrollY = window.scrollY;
13  const topNav = document.getElementById('topNav');
14  if (!topNav) return;
15  window.addEventListener('scroll', () => {
16    const currentScrollY = window.scrollY;
17    if (currentScrollY > 80) {
18      topNav.classList.add('scrolled');
19      if (currentScrollY > lastScrollY && currentScrollY > 200) {
20        topNav.classList.add('hidden');
21      } else {
22        topNav.classList.remove('hidden');
23      }
24    } else {
25      topNav.classList.remove('scrolled');
26      topNav.classList.remove('hidden');
27    }
28    lastScrollY = currentScrollY;
29  });
30})();
31
32// Contact drawer
33function openContactDrawer() {
34  const d = document.getElementById('contactDrawer');
35  if (d) d.classList.add('open');
36}
37function closeContactDrawer() {
38  const d = document.getElementById('contactDrawer');
39  if (d) d.classList.remove('open');
40}
41    // =============================================
42    // ISLAND NAV (scoped to avoid global const conflicts)
43    // =============================================
44{
45    const islandNav = document.getElementById('islandNav');
46    const islandHam = document.getElementById('islandHam');
47    const megaContent = document.getElementById('megaContent');
48    const megaClose = document.getElementById('megaClose');
49    const navOverlay = document.getElementById('navOverlay');
50    const navLinks = document.querySelectorAll('.mega-nav-link');
51    const contentPanels = document.querySelectorAll('.mega-content-panel');
52    
53    let isOpen = false;
54    let currentPanel = 'scholars';
55    
56    function openMega() {
57      if (isOpen) return;
58      isOpen = true;
59      navOverlay.classList.add('is-visible');
60      islandNav.classList.add('is-open');
61      gsap.to(megaContent, { opacity: 1, duration: 0.4, delay: 0.3, ease: 'power2.out' });
62    }
63    
64    function closeMega() {
65      if (!isOpen) return;
66      isOpen = false;
67      megaMain.classList.remove('search-active');
68      gsap.to(megaContent, { opacity: 0, duration: 0.2, ease: 'power2.in' });
69      setTimeout(() => {
70        islandNav.classList.remove('is-open');
71        navOverlay.classList.remove('is-visible');
72      }, 180);
73    }
74    
75    const megaMain = document.querySelector('.mega-main');
76    
77    function switchPanel(panelName) {
78      if (panelName === currentPanel) return;
79      currentPanel = panelName;
80      megaMain.classList.toggle('search-active', panelName === 'search');
81      navLinks.forEach(link => link.classList.toggle('is-active', link.dataset.panel === panelName));
82      contentPanels.forEach(panel => {
83        if (panel.dataset.panel === panelName) {
84          gsap.to(panel, { opacity: 1, duration: 0.25, ease: 'power2.out', onStart: () => panel.classList.add('is-active') });
85        } else if (panel.classList.contains('is-active')) {
86          gsap.to(panel, { opacity: 0, duration: 0.15, ease: 'power2.in', onComplete: () => panel.classList.remove('is-active') });
87        }
88      });
89    }
90    
91    // Open mega with specific panel from top nav
92    window.openMegaWithPanel = function(panelName) {
93      currentPanel = panelName;
94      megaMain.classList.toggle('search-active', panelName === 'search');
95      navLinks.forEach(link => link.classList.toggle('is-active', link.dataset.panel === panelName));
96      contentPanels.forEach(panel => {
97        panel.classList.toggle('is-active', panel.dataset.panel === panelName);
98        panel.style.opacity = panel.dataset.panel === panelName ? '1' : '0';
99      });
100      openMega();
101    }
102    
103    if(megaClose) megaClose.addEventListener('click', () => {
104      closeMega();
105      megaOpenedViaHover = false;
106      navOverlay.style.pointerEvents = '';
107      document.querySelectorAll('.nav-item[data-mega-panel]').forEach(item => item.classList.remove('mega-active'));
108    });
109    if(navOverlay) navOverlay.addEventListener('click', () => {
110      closeMega();
111      megaOpenedViaHover = false;
112      navOverlay.style.pointerEvents = '';
113      document.querySelectorAll('.nav-item[data-mega-panel]').forEach(item => item.classList.remove('mega-active'));
114    });
115    document.addEventListener('keydown', e => {
116      if (e.key === 'Escape' && isOpen) {
117        closeMega();
118        megaOpenedViaHover = false;
119        navOverlay.style.pointerEvents = '';
120        document.querySelectorAll('.nav-item[data-mega-panel]').forEach(item => item.classList.remove('mega-active'));
121      }
122    });
123    
124    navLinks.forEach(link => {
125      link.addEventListener('mouseenter', () => switchPanel(link.dataset.panel));
126      link.addEventListener('click', e => { e.preventDefault(); switchPanel(link.dataset.panel); });
127    });
128
129    // Top nav items: click to open mega menu (removed hover for better UX)
130    document.querySelectorAll('.nav-item[data-mega-panel]').forEach(item => {
131      item.addEventListener('click', (e) => {
132        e.preventDefault();
133        const panelName = item.dataset.megaPanel;
134        
135        if (isOpen) {
136          // If clicking same panel, close. If different, switch panel.
137          const currentActive = document.querySelector('.nav-item.mega-active');
138          if (currentActive && currentActive.dataset.megaPanel === panelName) {
139            closeMega();
140            document.querySelectorAll('.nav-item[data-mega-panel]').forEach(i => i.classList.remove('mega-active'));
141          } else {
142            switchPanel(panelName);
143            document.querySelectorAll('.nav-item[data-mega-panel]').forEach(i => i.classList.remove('mega-active'));
144            item.classList.add('mega-active');
145          }
146        } else {
147          openMegaWithPanel(panelName);
148          document.querySelectorAll('.nav-item[data-mega-panel]').forEach(i => i.classList.remove('mega-active'));
149          item.classList.add('mega-active');
150        }
151      });
152    });
153    
154    // Click overlay or outside to close mega
155    if (navOverlay) {
156      if(navOverlay) navOverlay.addEventListener('click', () => {
157        closeMega();
158        document.querySelectorAll('.nav-item[data-mega-panel]').forEach(item => item.classList.remove('mega-active'));
159      });
160    }
161    
162    // Island hamburger toggle
163    const origIslandClick = () => {
164      if (isOpen) {
165        closeMega();
166        document.querySelectorAll('.nav-item[data-mega-panel]').forEach(item => item.classList.remove('mega-active'));
167      } else {
168        openMega();
169      }
170    };
171    if(islandHam) islandHam.addEventListener('click', origIslandClick);
172    
173    // =============================================
174    // GRID ROW EXPANSION HOVER EFFECT
175    // =============================================
176    function setupRowExpansion(gridSelector, cardSelector) {
177      const grid = document.querySelector(gridSelector);
178      if (!grid) return;
179      
180      const cards = grid.querySelectorAll(cardSelector);
181      
182      cards.forEach(card => {
183        card.addEventListener('mouseenter', () => {
184          const row = card.dataset.row;
185          grid.classList.remove('row-1-hover', 'row-2-hover', 'row-3-hover');
186          if (row) {
187            grid.classList.add(`row-${row}-hover`);
188          }
189        });
190        
191        card.addEventListener('mouseleave', () => {
192          // Small delay before removing class to prevent flicker
193          setTimeout(() => {
194            if (!grid.querySelector(':hover')) {
195              grid.classList.remove('row-1-hover', 'row-2-hover', 'row-3-hover');
196            }
197          }, 50);
198        });
199      });
200      
201      grid.addEventListener('mouseleave', () => {
202        grid.classList.remove('row-1-hover', 'row-2-hover', 'row-3-hover');
203      });
204    }
205    
206    // Setup row expansion for all grids
207    setupRowExpansion('#scholarsGridNav', '.scholar-nav-card');
208    setupRowExpansion('#researchPanelGrid', '.panel-card');
209    setupRowExpansion('#topicsPanelGrid', '.panel-card');
210    
211    // =============================================
212    // SITE SEARCH (INSIDE MEGA NAV)
213    // =============================================
214    const megaSearchInput = document.getElementById('megaSearchInput');
215    const megaSearchSubmit = document.getElementById('megaSearchSubmit');
216    const megaSearchResults = document.getElementById('megaSearchResults');
217    const megaSearchTags = document.querySelectorAll('.mega-search-tag');
218    const megaSearchBar = document.getElementById('megaSearchBar');
219    const islandSearchBtn = document.getElementById('islandSearchBtn');
220    
221    // Site-wide search index — media metadata, research, events, scholars
222    const siteIndex = [
223      // === RESEARCH / PUBLICATIONS ===
224      { title: 'Grid Reliability Under Stress: 2026 Winter Outlook', type: 'study', date: 'Jan 15, 2026', scholar: 'Mark P. Mills', topics: ['grid','policy'], outlet: 'NCEA Research', excerpt: 'Comprehensive analysis of grid vulnerability during extreme cold events, with regional risk assessments.', url: 'research-library.html' },
225      { title: 'The Case for Nuclear: Land, Labor & Reliability', type: 'study', date: 'Jan 8, 2026', scholar: 'Jonathan Lesser, PhD', topics: ['nuclear','grid'], outlet: 'NCEA Research', excerpt: 'Comparative analysis showing nuclear requires 300× less land than solar for equivalent output.', url: 'research-library.html' },
226      { title: 'AI Data Centers: The Coming Energy Surge', type: 'brief', date: 'Dec 28, 2025', scholar: 'Mark P. Mills', topics: ['ai','grid'], outlet: 'NCEA Issue Brief', excerpt: 'How AI infrastru
226cture growth will drive unprecedented electricity demand, adding 15% to national consumption by 2030.', url: 'research-library.html' },
227      { title: 'Mark Mills on CNBC: Energy Transition Realities', type: 'media', date: 'Dec 15, 2025', scholar: 'Mark P. Mills', topics: ['net-zero'], outlet: 'CNBC', excerpt: 'Television appearance discussing the practical challenges and timeline of achieving net-zero emissions targets.', url: 'research-library.html' },
228      { title: 'State Energy Affordability Index 2025', type: 'study', date: 'Dec 1, 2025', scholar: 'Jonathan Lesser, PhD', topics: ['affordability','policy'], outlet: 'NCEA Research', excerpt: 'Comprehensive ranking of all 50 states on energy affordability metrics.', url: 'research-library.html' },
229      { title: 'Advanced Reactors: Regulatory Barriers and Solutions', type: 'brief', date: 'Nov 22, 2025', scholar: 'Mark P. Mills', topics: ['nuclear','policy'], outlet: 'NCEA Issue Brief', excerpt: 'Analysis of NRC licensing bottlenecks preventing deployment of next-generation nuclear technologies.', url: 'research-library.html' },
230      { title: 'Transmission Infrastructure: The Missing Link', type: 'study', date: 'Nov 5, 2025', scholar: 'Jonathan Lesser, PhD', topics: ['grid'], outlet: 'NCEA Research', excerpt: 'Research on transmission grid expansion required to accommodate renewable energy growth.', url: 'research-library.html' },
231      { title: 'Congressional Testimony: Energy Security Priorities', type: 'testimony', date: 'Oct 28, 2025', scholar: 'Mark P. Mills', topics: ['policy','grid'], outlet: 'House Energy Committee', excerpt: 'Full testimony on energy security implications of current policy trajectories.', url: 'research-library.html' },
232      { title: 'Electric Vehicle Mandates: Grid Impact Assessment', type: 'brief', date: 'Oct 15, 2025', scholar: 'Jonathan Lesser, PhD', topics: ['grid','policy'], outlet: 'NCEA Issue Brief', excerpt: 'Analysis of the strain EV adoption mandates will place on local distribution grids.', url: 'research-library.html' },
233      { title: 'Mining for the Energy Transition: Supply Chain Risks', type: 'study', date: 'Oct 1, 2025', scholar: 'Mark P. Mills', topics: ['net-zero'], outlet: 'NCEA Research', excerpt: 'Critical mineral dependencies and supply chain vulnerabilities in renewable energy manufacturing.', url: 'research-library.html' },
234      // === EVENTS ===
235      { title: 'Grid Reliability in a Changing Climate: 2026 Outlook', type: 'webinar', date: 'Feb 7, 2026', scholar: 'Mark Mills', topics: ['grid'], outlet: 'NCEA Webinar', excerpt: 'Deep dive into regional grid reliability challenges as extreme weather events become more frequent.', url: 'events.html' },
236      { title: 'CERAWeek 2026: The Path to Energy Security', type: 'conference', date: 'Mar 10-14, 2026', scholar: 'Sarah Chen', topics: ['policy'], outlet: 'CERAWeek', excerpt: 'NCEA scholars at the premier annual energy conference in Houston.', url: 'events.html' },
237      { title: 'The Energy Gang: Nuclear Renaissance?', type: 'podcast', date: 'Feb 12, 2026', scholar: 'David Williams', topics: ['nuclear'], outlet: 'The Energy Gang Podcast', excerpt: 'Discussion on the prospects and challenges of nuclear energy expansion.', url: 'events.html' },
238      { title: 'Senate ENR: Permitting Reform Hearing', type: 'testimony', date: 'Feb 20, 2026', scholar: 'Mark Mills', topics: ['policy'], outlet: 'U.S. Senate', excerpt: 'Testimony before the Senate Energy & Natural Resources Committee on permitting reform.', url: 'events.html' },
239      { title: 'Capacity Markets 101: A Policy Primer', type: 'webinar', date: 'Feb 25, 2026', scholar: 'Jennifer Park', topics: ['policy','grid'], outlet: 'NCEA Webinar', excerpt: 'Introduction to capacity market structures and their role in grid reliability.', url: 'events.html' },
240      { title: 'ERCOT Grid Summit: Lessons from Winter Storm Elliott', type: 'conference', date: 'Mar 5, 2026', scholar: 'Robert Martinez', topics: ['grid'], outlet: 'ERCOT Grid Summit', excerpt: 'Post-mortem analysis of Winter Storm Elliott grid impacts in Texas.', url: 'events.html' },
241      { title: 'Leading Energy Indicators Launch Event', type: 'webinar', date: 'Jan 15, 2026', scholar: 'Mark Mills', topics: ['grid','policy'], outlet: 'NCEA Webinar', excerpt: 'Launch of the NCEA Leading Energy Indicators dashboard.', url: 'events.html' },
242      { title: 'House Energy Committee: Grid Modernization', type: 'testimony', date: 'Dec 10, 2025', scholar: 'Sarah Chen', topics: ['grid','policy'], outlet: 'U.S. House of Representatives', excerpt: 'Testimony on grid modernization priorities and infrastru
242cture investment needs.', url: 'events.html' },
243      { title: 'Odd Lots: The Coming Power Crunch', type: 'podcast', date: 'Nov 28, 2025', scholar: 'Mark Mills', topics: ['grid'], outlet: 'Bloomberg Odd Lots', excerpt: 'Discussion on looming electricity supply shortfalls and policy implications.', url: 'events.html' },
244      { title: 'EIA Energy Conference 2025', type: 'conference', date: 'Oct 22, 2025', scholar: 'David Williams', topics: ['policy'], outlet: 'Energy Information Administration', excerpt: 'NCEA scholars presenting at the annual EIA conference.', url: 'events.html' },
245      { title: 'State of the Grid: Q3 2025 Assessment', type: 'webinar', date: 'Oct 1, 2025', scholar: 'Jennifer Park', topics: ['grid'], outlet: 'NCEA Webinar', excerpt: 'Quarterly assessment of national grid performance and reliability metrics.', url: 'events.html' },
246      { title: 'FERC Technical Conference on Capacity', type: 'testimony', date: 'Sep 15, 2025', scholar: 'Robert Martinez', topics: ['grid','policy'], outlet: 'FERC', excerpt: 'Technical testimony on capacity market design and resource adequacy.', url: 'events.html' },
247      // === SCHOLARS ===
248      { title: 'Mark P. Mills', type: 'scholar', date: '', scholar: 'Mark P. Mills', topics: ['grid','nuclear','policy','net-zero','ai'], outlet: 'Executive Director, NCEA', excerpt: 'Contributing editor at City Journal, faculty fellow at Northwestern, co-founding partner in Montrose Lane.', url: '#' },
249      { title: 'David G. Victor, PhD', type: 'scholar', date: '', scholar: 'David G. Victor', topics: ['policy','grid'], outlet: 'Senior Scholar, NCEA', excerpt: 'Professor of Innovation and Public Policy at UC San Diego, energy governance expert.', url: '#' },
250      { title: 'Julio M. Ottino, PhD', type: 'scholar', date: '', scholar: 'Julio M. Ottino', topics: ['grid','nuclear'], outlet: 'Senior Scholar, NCEA', excerpt: 'Dean of the McCormick School of Engineering at Northwestern University.', url: '#' },
251      { title: 'Jonathan Lesser, PhD', type: 'scholar', date: '', scholar: 'Jonathan Lesser', topics: ['affordability','grid','policy'], outlet: 'Senior Fellow, NCEA', excerpt: 'Energy economist and expert on electricity market regulation and affordability.', url: '#' },
252      { title: 'Sarah Chen', type: 'scholar', date: '', scholar: 'Sarah Chen', topics: ['grid','policy'], outlet: 'Research Fellow, NCEA', excerpt: 'Grid modernization specialist with focus on infrastructure resilience.', url: '#' },
253      { title: 'Jennifer Park', type: 'scholar', date: '', scholar: 'Jennifer Park', topics: ['grid','policy'], outlet: 'Policy Analyst, NCEA', excerpt: 'Capacity market and electricity policy researcher.', url: '#' },
254      // === DASHBOARD ===
255      { title: 'Index of Leading Energy Indicators', type: 'dashboard', date: 'Updated daily', scholar: '', topics: ['grid','affordability','policy'], outlet: 'NCEA Data Platform', excerpt: 'Real-time indices across security, reliability, cost, and environmental footprint for all 50 states.', url: '#' },
256      // === MEDIA / IN THE NEWS ===
257      { title: 'The Hidden Cost of Green Energy Mandates', type: 'media', date: 'Feb 8, 2026', scholar: 'Mark P. Mills', topics: ['policy','affordability'], outlet: 'Wall Street Journal', excerpt: 'State renewable portfolio standards are driving up electricity costs far beyond official estimates.', url: '#' },
258      { title: 'Grid Reliability Concerns Ahead of Winter Storm', type: 'media', date: 'Feb 6, 2026', scholar: 'Mark P. Mills', topics: ['grid'], outlet: 'CNBC', excerpt: 'NCEA Executive Director discusses regional grid vulnerabilities as arctic blast approaches.', url: '#' },
259      { title: 'The Coming Power Crunch', type: 'podcast', date: 'Feb 4, 2026', scholar: 'Mark P. Mills', topics: ['grid','policy'], outlet: 'Bloomberg Odd L
259ots', excerpt: 'A deep dive into why electricity demand is outpacing supply growth for the first time in decades.', url: '#' },
260      { title: 'Nuclear Energy\'s Second Act', type: 'media', date: 'Feb 2, 2026', scholar: 'David G. Victor', topics: ['nuclear','ai'], outlet: 'New York Times', excerpt: 'Why major tech companies are betting billions on nuclear power for their data centers.', url: '#' },
261      { title: 'NCEA Study Cited in Senate Hearing on Permitting Reform', type: 'media', date: 'Jan 30, 2026', scholar: '', topics: ['policy'], outlet: 'E&E News', excerpt: 'Senators reference NCEA research on infrastructure bottlenecks during Energy Committee markup.', url: '#' },
262      { title: 'The IEA\'s Flawed Forecasts Are Hurting Investment', type: 'media', date: 'Jan 28, 2026', scholar: 'Jonathan Lesser', topics: ['policy','net-zero'], outlet: 'Financial Times', excerpt: 'International Energy Agency projections have consistently underestimated oil and gas demand.', url: '#' },
263      { title: 'Winter Storm Tests Texas Grid Again', type: 'media', date: 'Jan 25, 2026', scholar: 'Mark P. Mills', topics: ['grid'], outlet: 'Fox Business', excerpt: 'Analysis of ERCOT performance during extreme cold and lessons from previous winter storms.', url: '#' },
264      { title: 'Electricity Affordability Crisis', type: 'podcast', date: 'Jan 22, 2026', scholar: 'Jonathan Lesser', topics: ['affordability','policy'], outlet: 'The Energy Gang', excerpt: 'Why electricity bills are rising faster than inflation and what policymakers can do about it.', url: '#' },
265      { title: 'AI\'s Insatiable Appetite for Power', type: 'media', date: 'Jan 18, 2026', scholar: 'Mark P. Mills', topics: ['ai','grid'], outlet: 'Wired', excerpt: 'Data centers could consume 15% of U.S. electricity by 2030, creating unprecedented grid challenges.', url: '#' }
266    ];
267
268    // SVG icons per content type
269    const typeIcons = {
270      study: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>',
271      brief: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>',
272      media: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="7" width="20" height="15" rx="2" ry="2"/><polyline points="17 2 12 7 7 2"/></svg>',
273      webinar: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/></svg>',
274      conference: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>',
275      podcast: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/></svg>',
276      testimony: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/><line x1="4" y1="22" x2="4" y2="15"/></svg>',
277      scholar: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>',
278      dashboard: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="9" y1="21" x2="9" y2="9"/></svg>'
279    };
280
281    const typeLabels = {
282      study: 'Study', brief: 'Issue Brief', media: 'Media', webinar: 'Webinar',
283      conference: 'Conference', podcast: 'Podcast', testimony: 'Testimony',
284      scholar: 'Scholar', dashboard: 'Dashboard'
285    };
286
287    function highlightMatch(text, query) {
288      if (!query) return text;
289      const words = query.split(/\s+/).filter(w => w.length > 1);
290      let result = text;
291      words.forEach(word => {
292        const regex = new RegExp(`(${word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
293        result = result.replace(regex, '<mark>$1</mark>');
294      });
295      return result;
296    }
297
298    function searchSite(query) {
299      if (!query || query.length < 2) {
300        megaSearchResults.innerHTML = '';
301        return;
302      }
303      const q = query.toLowerCase().trim();
304      const terms = q.split(/\s+/).filter(t => t.length > 1);
305
306      const scored = siteIndex.map(item => {
307        let score = 0;
308        const searchable = [
309          item.title,
310          item.scholar,
311          item.outlet,
312          item.type,
313          item.date,
314          item.excerpt,
315          ...(item.topics || [])
316        ].join(' ').toLowerCase();
317
318        terms.forEach(term => {
319          if (item.title.toLowerCase().includes(term)) score += 10;
320          if (item.scholar.toLowerCase().includes(term)) score += 8;
321          if (item.outlet.toLowerCase().includes(term)) score += 6;
322          if (item.type.toLowerCase().includes(term)) score += 5;
323          if ((item.topics || []).some(t => t.includes(term))) score += 4;
324          if (item.date.toLowerCase().includes(term)) score += 3;
325          if (item.excerpt.toLowerCase().includes(term)) score += 2;
326        });
327
328        return { ...item, score };
329      }).filter(item => item.score > 0)
330        .sort((a, b) => b.score - a.score);
331
332      if (scored.length === 0) {
333        megaSearchResults.innerHTML = `
334          <div class="search-empty-state">
335            <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>
336            <p><strong>No results for "${query}"</strong></p>
337            <p>Try different keywords — search by title, scholar name, outlet, or topic.</p>
338          </div>`;
339        return;
340      }
341
342      let html = `<div class="mega-search-results-count">${scored.length} result${scored.length !== 1 ? 's' : ''}</div>`;
343      scored.slice(0, 15).forEach(item => {
344        const icon = typeIcons[item.type] || typeIcons.study;
345        const label = typeLabels[item.type] || item.type;
346        const metaParts = [label];
347        if (item.scholar) metaParts.push(item.scholar);
348        if (item.outlet && item.outlet !== 'NCEA Research' && item.outlet !== 'NCEA Issue Brief') metaParts.push(item.outlet);
349        if (item.date) metaParts.push(item.date);
350
351        html += `
352          <a href="${item.url}" class="search-result-item">
353            <div class="search-result-icon type-${item.type}">${icon}</div>
354            <div class="search-result-body">
355              <div class="search-result-title">${highlightMatch(item.title, query)}</div>
356              <div class="search-result-meta">${metaParts.join(' <span class="search-result-meta-sep">·</span> ')}</div>
357              ${item.excerpt ? `<div class="search-result-excerpt">${item.excerpt}</div>` : ''}
358            </div>
359          </a>`;
360      });
361
362      megaSearchResults.innerHTML = html;
363    }
364
365    function openSearchInMega() {
366      openMegaWithPanel('search');
367      setTimeout(() => megaSearchInput && megaSearchInput.focus(), 400);
368    }
369    
370    function exitSearchMode() {
371      // Go back to scholars panel (default)
372      const megaMain = document.querySelector('.mega-main');
373      megaMain.classList.remove('search-active');
374      
375      // Show scholars panel
376      document.querySelectorAll('.mega-content-panel').forEach(p => p.classList.remove('is-active'));
377      document.querySelectorAll('.mega-nav-link').forEach(l => l.classList.remove('is-active'));
378      
379      const scholarsPanel = document.querySelector('.mega-content-panel[data-panel="scholars"]');
380      const scholarsLink = document.querySelector('.mega-nav-link[data-panel="scholars"]');
381      if (scholarsPanel) scholarsPanel.classList.add('is-active');
382      if (scholarsLink) scholarsLink.classList.add('is-active');
383    }
384    
385    function submitSiteSearch() {
386      const query = megaSearchInput.value.trim();
387      searchSite(query);
388    }
389    
390    // Mega search bar (bottom of mega) opens search modal
391    if (megaSearchBar) {
392      megaSearchBar.addEventListener('click', () => {
393        closeMega();
394        if (typeof window.openSearchModal === 'function') { window.openSearchModal(); } 
395      });
396    }
397    
398    // Island search button opens global search modal
399    if (islandSearchBtn) {
400      islandSearchBtn.addEventListener('click', openSearchModal);
401    }
402    
403    // Top nav search button
404    window.openAISearch = openSearchModal;
405    window.exitSearchMode = exitSearchMode;
406    
407    // Search submit
408    if (megaSearchSubmit) {
409      megaSearchSubmit.addEventListener('click', submitSiteSearch);
410    }
411    if (megaSearchInput) {
412      megaSearchInput.addEventListener('keydown', e => { if (e.key === 'Enter') submitSiteSearch(); });
413      // Live search as you type (debounced)
414      let searchTimeout;
415      megaSearchInput.addEventListener('input', () => {
416        clearTimeout(searchTimeout);
417        searchTimeout = setTimeout(submitSiteSearch, 200);
418      });
419    }
420    
421    // Search suggestion tags
422    megaSearchTags.forEach(tag => {
423      tag.addEventListener('click', () => {
424        megaSearchInput.value = tag.dataset.query;
425        submitSiteSearch();
426      });
427    });
428    
429    // Cmd+K shortcut to open search modal
430    document.addEventListener('keydown', e => {
431      if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
432        e.preventDefault();
433        if (isOpen) closeMega();
434        openSearchModal();
435      }
436    });
437    
438    // Expose needed functions globally
439    window.exitSearchMode = exitSearchMode;
440} // end island nav scope
441
442// Search modal stub — redirects to search page on non-homepage
443function openSearchModal(q) {
444  var modal = document.getElementById('globalSearchModal');
445  if (modal) {
446    modal.classList.add('active');
447    return;
448  }
449  // Check if homepage modal exists
450  var hpModal = document.getElementById('searchModalOverlay');
451  if (hpModal) {
452    hpModal.classList.add('is-open');
453    var inp = hpModal.querySelector('.search-modal__input');
454    if (inp) { inp.value = q || ''; inp.focus(); }
455    return;
456  }
457  // Inject lightweight search modal
458  var overlay = document.createElement('div');
459  overlay.id = 'searchModalInjected';
460  overlay.innerHTML =
461    '<div class="srch-overlay">' +
462      '<div class="srch-modal">' +
463        '<div class="srch-header">' +
464          '<svg class="srch-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>
464' +
465          '<input class="srch-input" type="text" placeholder="Search publications, scholars, topics\u2026" autofocus>' +
466          '<span class="srch-kbd">ESC</span>' +
467          '<button class="srch-close" aria-label="Close"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>' +
468        '</div>' +
469        '<div class="srch-body">' +
470          '<div class="srch-hint">Press Enter to search</div>' +
471          '<div class="srch-links">' +
472            '<a href="research-library.html">Research Library</a>' +
473            '<a href="advisory-board.html">Scholars</a>' +
474            '<a href="topics.html">Topics</a>' +
475            '<a href="events.html">Events</a>' +
476          '</div>' +
477        '</div>' +
478      '</div>' +
479    '</div>';
480  var style = document.createElement('style');
481  style.textContent =
482    '.srch-overlay{position:fixed;inset:0;z-index:10000;background:rgba(0,0,0,0.7);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);display:flex;align-items:flex-start;justify-content:center;padding-top:12vh;opacity:0;transition:opacity 0.25s cubic-bezier(0.16,1,0.3,1)}' +
483    '.srch-overlay.is-open{opacity:1}' +
484    '.srch-modal{width:min(620px,92vw);background:#0f0f11;border-radius:16px;overflow:hidden;transform:translateY(16px);transition:transform 0.3s cubic-bezier(0.16,1,0.3,1);box-shadow:0 0 0 1px rgba(255,255,255,0.08),0 32px 64px rgba(0,0,0,0.5)}' +
485    '.srch-overlay.is-open .srch-modal{transform:translateY(0)}' +
486    '.srch-header{display:flex;align-items:center;gap:12px;padding:18px 20px;border-bottom:1px solid rgba(255,255,255,0.08)}' +
487    '.srch-icon{width:20px;height:20px;color:rgba(255,255,255,0.35);flex-shrink:0}' +
488    '.srch-input{flex:1;background:none;border:none;font-size:16px;color:#fff;font-family:var(--font-body,sans-serif);outline:none}' +
489    '.srch-input::placeholder{color:rgba(255,255,255,0.3)}' +
490    '.srch-kbd{font-size:11px;color:rgba(255,255,255,0.25);background:rgba(255,255,255,0.06);padding:3px 8px;border-radius:5px;font-family:var(--font-body,sans-serif)}' +
491    '.srch-close{width:32px;height:32px;border-radius:8px;background:rgba(255,255,255,0.06);border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background 0.2s}' +
492    '.srch-close:hover{background:rgba(255,255,255,0.12)}' +
493    '.srch-close svg{width:14px;height:14px;color:rgba(255,255,255,0.5)}' +
494    '.srch-body{padding:20px}' +
495    '.srch-hint{font-size:13px;color:rgba(255,255,255,0.25);margin-bottom:20px}' +
496    '.srch-links{display:flex;gap:8px;flex-wrap:wrap}' +
497    '.srch-links a{font-size:13px;color:rgba(255,255,255,0.5);padding:6px 14px;border-radius:100px;border:1px solid rgba(255,255,255,0.1);transition:all 0.2s;text-decoration:none}' +
498    '.srch-links a:hover{color:#fff;border-color:rgba(255,255,255,0.3)}';
499  document.head.appendChild(style);
500  document.body.appendChild(overlay);
501  var ov = overlay.querySelector('.srch-overlay');
502  var inp = overlay.querySelector('.srch-input');
503  var cls = overlay.querySelector('.srch-close');
504  requestAnimationFrame(function() { requestAnimationFrame(function() {
505    ov.classList.add('is-open');
506    inp.focus();
507    if (q) inp.value = q;
508  }); });
509  function closeIt() {
510    ov.classList.remove('is-open');
511    setTimeout(function() { overlay.remove(); }, 300);
512  }
513  ov.addEventListener('click', function(e) { if (e.target === ov) closeIt(); });
514  cls.addEventListener('click', closeIt);
515  inp.addEventListener('keydown', function(e) {
516    if (e.key === 'Escape') closeIt();
517    if (e.key === 'Enter' && inp.value.trim()) {
518      if (typeof window.openSearchModal === 'function') { closeIt(); window.openSearchModal(inp.value.trim()); }
519    }
520  });
521  document.addEventListener('keydown', function handler(e) {
522    if (e.key === 'Escape') { closeIt(); document.removeEventListener('keydown', handler); }
523  });
524}
525
526// Index dashboard stub — redirects to energy-index page
527function openIndexDashboard() {
528  const popup = document.getElementById('indexPopup');
529  if (popup) {
530    popup.classList.toggle('active');
531  } else {
532    window.location.href = 'energy-index.html';
533  }
534}

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.