1/* Ecoflix header navigation â single source of truth (the dropdown menu). 2 Loaded on EVERY page so the top nav is identical everywhere. */ 3(function navRewrite(){ 4 const nav = document.querySelector('header.header .nav'); 5 if (!nav) return; 6 7 // Self-contained dropdown styling so the menu renders identically on EVERY 8 // page, whether or not it loads site.css (the homepage, for example, doesn't). 9 if (!document.getElementById('eco-nav-css')){ 10 const css = document.createElement('style'); 11 css.id = 'eco-nav-css'; 12 css.textContent = ".nav-enhanced{display:none;align-items:center;gap:6px}.nav-group{position:relative;display:inline-flex}" 13 // Top-level flat links styled IDENTICALLY to dropdown triggers: same weight, 14 // size, padding and hover pill, so every top-level item matches and spaces evenly. 15 + ".nav-enhanced>a{font-family:var(--body,inherit);font-size:14px;font-weight:600;color:var(--mist,#9aa);padding:8px 14px;border-radius:99px;display:inline-flex;align-items:center;text-decoration:none;transition:color .2s,background .2s}" 16 + ".nav-enhanced>a:hover{color:var(--bone,#fff);background:rgba(255,255,255,.04)}" 17 + ".nav-enhanced>a[aria-current='page']{color:var(--leaf,#A8F341)}" 18 + ".nav-trigger{appearance:none;border:0;background:none;cursor:pointer;font-family:var(--body,inherit);font-size:14px;font-weight:600;color:var(--mist,#9aa);padding:8px 14px;border-radius:99px;display:inline-flex;align-items:center;gap:6px;transition:color .2s,background .2s}" 19 + ".nav-trigger .nav-chev{font-size:10px;color:var(--mist-2,#778);transition:transform .25s ease,color .2s;display:inline-block;line-height:1;margin-top:1px}" 20 + ".nav-trigger:hover{color:var(--bone,#fff);background:rgba(255,255,255,.04)}.nav-trigger:hover .nav-chev{color:var(--leaf,#A8F341)}" 21 + ".nav-trigger:focus-visible{outline:2px solid var(--leaf,#A8F341);outline-offset:2px;color:var(--bone,#fff)}" 22 + ".nav-group.is-open .nav-trigger{color:var(--bone,#fff);background:rgba(168,243,65,.08)}.nav-group.is-open .nav-trigger .nav-chev{transform:rotate(180deg);color:var(--leaf,#A8F341)}" 23 + ".nav-trigger.is-active-section,.nav-trigger.is-active-section .nav-chev{color:var(--leaf,#A8F341)}" 24 + ".nav-menu{position:absolute;top:calc(100% + 12px);left:0;z-index:50;min-width:240px;padding:10px 8px;border-radius:14px;background:rgba(7,13,35,.94);border:1px solid rgba(168,243,65,.22);box-shadow:0 24px 60px -16px rgba(0,0,0,.65),0 8px 20px -6px rgba(0,0,0,.45);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);display:flex;flex-direction:column;gap:2px}" 25 + ".nav-menu::before{content:'';position:absolute;left:18px;right:18px;top:0;height:1px;background:linear-gradient(90deg,transparent,rgba(168,243,65,.6),transparent)}" 26 + ".nav-menu[hidden]{display:none}" 27 + ".nav-menu a{display:flex;align-items:center;padding:9px 14px;border-radius:9px;font-size:13.5px;color:var(--bone,#fff);text-decoration:none;transition:background .15s,color .15s}" 28 + ".nav-menu a:hover,.nav-menu a:focus-visible{background:rgba(168,243,65,.10);color:var(--leaf,#A8F341);outline:0}" 29 + ".nav-menu a[aria-current='page']{background:rgba(168,243,65,.14);color:var(--leaf,#A8F341)}" 30 + ".nav-group:nth-last-of-type(-n+2) .nav-menu{left:auto;right:0}" 31 // The desktop links only appear at >=1024px (matching .nav in site.css / 32 // the homepage's inline CSS); below that the hamburger menu takes over. 33 // Without this guard the enhanced nav forced display:flex at every width, 34 // so the full link row showed (and overflowed) on mobile next to the burger. 35 + "@media (min-width:1024px){.nav-enhanced{display:flex}}" 36 + "@media (max-width:1180px){.nav-enhanced{gap:4px}.nav-trigger{padding:8px 10px}}"; 37 document.head.appendChild(css); 38 } 39 40 // Note: `key` fields are i18n translation keys read by /assets/i18n.js. 41 // The English `label` is the fallback shown if i18n.js doesn't load 42 // or doesn't recognise the key. 43 const NAV = [ 44 { type:'link', href:'/watch/', label:'Watch', key:'nav.watch' }, 45 { type:'link', href:'/podcasts/', label:'Listen' }, 46 { type:'group', label:'Read', items:[ 47 { href:'/news/', label:'News', key:'nav.news' }, 48 { href:'/features/', label:'Features' }, 49 ]}, 50 { type:'group', label:'Projects', items:[ 51 { href:'/projects/', label:'All projects', key:'nav.projects' }, 52 { href:'/ngo-partners/', label:'NGO partners' }, 53 { href:'/impact-report/', label:'Impact report' }, 54 ]}, 55 { type:'group', label:'About', key:'nav.about', items:[ 56 { href:'/mission/', label:'Mission', key:'nav.mission' }, 57 { href:'/how-it-works/', label:'How it works' }, 58 { href:'/how-we-help/', label:'How we help', key:'nav.how_we_help' }, 59 { href:'/history/', label:'Our story' }, 60 { href:'/team/', label:'Team' }, 61 { href:'/connect/', label:'Connect' }, 62 { href:'/faq/', label:'FAQ' }, 63 ]}, 64 { type:'group', label:'Business', key:'nav.business', items:[ 65 { href:'/distribution/', label:'Distribution & ads' }, 66 { href:'/partners/', label:'Corporate partners' }, 67 { href:'/news/', label:'Newsroom' }, 68 { href:'/contact/', label:'Contact BD desk' }, 69 ]}, 70 { type:'group', label:'Change', key:'nav.give', items:[ 71 { href:'/donate/', label:'Donate', key:'cta.donate' }, 72 { href:'/circle-of-giving/', label:'Circle of Giving' }, 73 { href:'/gift-memberships/', label:'Gift memberships' }, 74 { href:'/partners/', label:'Become a partner' }, 75 { href:'/teachers/', label:'For teachers' }, 76 ]}, 77 ]; 78 79 // Resolve current path â mark matching item aria-current="page". 80 const currentPath = location.pathname.replace(/\/index\.html$/, '/'); 81 function isCurrent(href){ 82 if (!href || href.startsWith('http')) return false;
83 return href === currentPath || (href !== '/' && currentPath.startsWith(href)); 84 } 85 86 // Build the new markup. 87 const frag = document.createDocumentFragment(); 88 let groupId = 0; 89 NAV.forEach(entry => { 90 if (entry.type === 'link'){ 91 const a = document.createElement('a'); 92 a.href = entry.href; 93 a.textContent = entry.label; 94 if (entry.key) a.setAttribute('data-i18n', entry.key); 95 if (entry.external){ a.setAttribute('rel','noopener'); } 96 if (isCurrent(entry.href)) a.setAttribute('aria-current','page'); 97 frag.appendChild(a); 98 return; 99 } 100 // group 101 const wrap = document.createElement('div'); 102 wrap.className = 'nav-group'; 103 const id = 'navmenu-' + (++groupId); 104 105 const btn = document.createElement('button'); 106 btn.type = 'button'; 107 btn.className = 'nav-trigger'; 108 btn.setAttribute('aria-expanded','false'); 109 btn.setAttribute('aria-controls', id); 110 // Label span gets the data-i18n; chev preserved as a sibling for the 111 // translation pass which only replaces the first text node. 112 const labelSpan = entry.key 113 ? '<span data-i18n="' + entry.key + '">' + entry.label + '</span>' 114 : entry.label; 115 btn.innerHTML = labelSpan + '<span class="nav-chev" aria-hidden="true">â¾</span>'; 116 wrap.appendChild(btn); 117 118 const menu = document.createElement('div'); 119 menu.className = 'nav-menu'; 120 menu.id = id; 121 menu.setAttribute('role','menu'); 122 menu.hidden = true; 123 entry.items.forEach(item => { 124 const a = document.createElement('a'); 125 a.href = item.href; 126 a.textContent = item.label; 127 a.setAttribute('role','menuitem'); 128 if (item.key) a.setAttribute('data-i18n', item.key); 129 if (isCurrent(item.href)){ 130 a.setAttribute('aria-current','page'); 131 // Reflect on trigger so the whole group reads as active. 132 btn.classList.add('is-active-section'); 133 } 134 menu.appendChild(a); 135 }); 136 wrap.appendChild(menu); 137 138 // Open/close on trigger click. 139 btn.addEventListener('click', e => { 140 e.stopPropagation(); 141 const isOpen = btn.getAttribute('aria-expanded') === 'true'; 142 closeAll(); 143 if (!isOpen){ 144 btn.setAttribute('aria-expanded','true'); 145 menu.hidden = false; 146 wrap.classList.add('is-open'); 147 } 148 }); 149 // Keyboard: ArrowDown/Enter/Space opens & focuses first item. 150 btn.addEventListener('keydown', e => { 151 if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' '){ 152 e.preventDefault(); 153 btn.click(); 154 const first = menu.querySelector('a'); 155 if (first) first.focus(); 156 } 157 }); 158 menu.addEventListener('keydown', e => { 159 const links = Array.from(menu.querySelectorAll('a')); 160 const idx = links.indexOf(document.activeElement); 161 if (e.key === 'Escape'){ closeAll(); btn.focus(); } 162 else if (e.key === 'ArrowDown'){ e.preventDefault(); links[(idx + 1) % links.length].focus(); } 163 else if (e.key === 'ArrowUp') { e.preventDefault(); links[(idx - 1 + links.length) % links.length].focus(); } 164 else if (e.key === 'Tab' && !e.shiftKey && idx === links.length - 1){ closeAll(); } 165 }); 166 167 frag.appendChild(wrap); 168 }); 169 170 // Replace existing nav contents. 171 nav.innerHTML = ''; 172 nav.appendChild(frag); 173 nav.classList.add('nav-enhanced'); 174 175 // Outside click / Escape close. 176 function closeAll(){ 177 nav.querySelectorAll('.nav-group.is-open').forEach(g => { 178 g.classList.remove('is-open'); 179 const t = g.querySelector('.nav-trigger'); 180 const m = g.querySelector('.nav-menu'); 181 if (t) t.setAttribute('aria-expanded','false'); 182 if (m) m.hidden = true; 183 }); 184 } 185 document.addEventListener('click', e => { 186 if (!nav.contains(e.target)) closeAll(); 187 }); 188 document.addEventListener('keydown', e => { if (e.key === 'Escape') closeAll(); }); 189})();
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.