1(() => { 2 const esc = value => String(value ?? '').replace(/[&<>"']/g, ch => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[ch])); 3 const localLogo = brand => { 4 const configured = String(brand.logo || brand.logoUrl || '').trim(); 5 if (!configured) return ''; 6 try { 7 const url = new URL(configured, window.location.href); 8 return url.origin === window.location.origin ? url.href : ''; 9 } catch (_) { 10 return ''; 11 } 12 }; 13 const identity = (brand, context = 'card') => { 14 const logo = localLogo(brand); 15 const logoOnly = context === 'marquee' && brand.marqueeLogoOnly === true && !!logo; 16 const plate = ['dark', 'light'].includes(brand.logoPlate) ? ` logo-plate-${brand.logoPlate}` : ''; 17 const image = logo ? `<img class="brand-logo-image brand-logo-${context}${plate}" src="${esc(logo)}" alt="" loading="lazy" onerror="this.style.display='none'">` : ''; 18 return `<span class="brand-identity${logoOnly ? ' brand-identity-logo-only' : ''}">${image}${logoOnly ? '' : `<span class="brand-name">${esc(brand.name)}</span>`}</span>`; 19 }; 20 const marquee = brands => { 21 const links = brands.filter(brand => brand.showInBanner !== false).map(brand => `<a href="${esc(brand.url)}" target="_blank" rel="noopener" aria-label="Visit ${esc(brand.name)} website">${identity(brand, 'marquee')}</a>`).join(''); 22 const clones = links.replace(/<a /g, '<a tabindex="-1" '); 23 return `<div class="brand-marquee"><div class="brand-marquee-set">${links}</div><div class="brand-marquee-set" aria-hidden="true">${clones}</div></div>`; 24 }; 25 let brandsPromise; 26 const load = () => brandsPromise ||= fetch('data/brands.json', { cache: 'no-store' }) 27 .then(response => response.ok ? response.json() : []) 28 .then(data => (Array.isArray(data) ? data : []).filter(brand => brand && brand.published !== false).sort((a, b) => (Number(a.sortOrder) || 999) - (Number(b.sortOrder) || 999))); 29 30 window.ANDREWS_AG_BRANDS = { identity, marquee, load }; 31 // Keyboard users need a still, scrollable row with the active link in view. 32 // Pointer activation keeps the current position so moving a link cannot cancel a click. 33 document.addEventListener('focusin', event => { 34 const link = event.target.closest?.('.brand-marquee a'); 35 const wrapper = link?.closest('.brand-marquee-wrap'); 36 if (!wrapper || !link.matches(':focus-visible')) return; 37 wrapper.classList.add('is-keyboard-active'); 38 link.scrollIntoView({ block: 'nearest', inline: 'nearest', behavior: 'instant' }); 39 }); 40 document.addEventListener('focusout', event => { 41 const wrapper = event.target.closest?.('.brand-marquee-wrap'); 42 if (!wrapper) return; 43 requestAnimationFrame(() => { 44 if (wrapper.contains(document.activeElement)) return; 45 wrapper.scrollLeft = 0; 46 wrapper.classList.remove('is-keyboard-active'); 47 }); 48 }); 49 const topBanner = document.getElementById('home-top-brand-marquee'); 50 if (topBanner) load().then(brands => { 51 if (!brands.some(brand => brand.showInBanner !== false)) return; 52 topBanner.innerHTML = marquee(brands); 53 topBanner.hidden = false; 54 }).catch(() => {}); 55})();
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.