/** * Chrome Tint * - * Tint the browser chrome (status bar + home indicator area) to match * the site header / footer when those elements are in view. * * How it works on each browser: * * - Chrome and iOS ≤ 18: * drives the top status bar tint. We * update it dynamically. (Ignored by iOS 26+, but kept for * compatibility with older browsers and Chrome on Android.) * * - iOS 26+ Safari: * The tag is ignored. Instead, iOS * samples the background-color of `position: fixed` elements * within 4px of the top or 3px of the bottom of the viewport * (at least 80% wide, at least 12px tall) and tints the chrome * to match. We set the inline `style.backgroundColor` of two * real fixed
s (`.chrome-tint--top` and * `.chrome-tint--bottom`) declared in header.php. * * Importantly, iOS 26 only takes ONE color per region (top OR * bottom), so we can run the two regions independently. This * lets the top go header-color while the bottom is footer-color * when both elements are in view (e.g. on short pages). * * Colors: * - HEADER_COLOR (#032F46) is the darker end of the .site-header * gradient. * - FOOTER_COLOR (#F26B53) is the orange of the .site-footer * `border-bottom` (the secondary color token, --color-cwc-orange-01). * The home indicator area tints to this so the chrome matches * the last visible color when the footer is in view. * * "In view" = any pixel of the element overlaps the viewport. * * No-ops if either the header or footer is missing, or if * IntersectionObserver isn't supported (older browsers). */ const HEADER_COLOR = '#032F46'; const FOOTER_COLOR = '#F26B53'; const THEME_META_NAME = 'theme-color'; function ensureThemeColorMeta() { let meta = document.head.querySelector(`meta[name="${THEME_META_NAME}"]`); if (!meta) { meta = document.createElement('meta'); meta.setAttribute('name', THEME_META_NAME); document.head.appendChild(meta); } return meta; } function setThemeColor(color) { const meta = ensureThemeColorMeta(); if (color == null) { meta.removeAttribute('content'); } else { meta.setAttribute('content', color); } } function setFixedBackground(el, color) { if (!el) return; if (color == null) { el.style.removeProperty('background-color'); } else { el.style.backgroundColor = color; } } function initChromeTint() { if (typeof IntersectionObserver === 'undefined') return; const header = document.querySelector('.site-header'); const footer = document.querySelector('.site-footer'); if (!header && !footer) return; const topEl = document.querySelector('.chrome-tint--top'); const bottomEl = document.querySelector('.chrome-tint--bottom'); // Top tint state. Truthy means the header is currently in view. let headerInView = false; // Bottom tint state. Truthy means the footer is currently in view. let footerInView = false; const apply = () => { // Top tint = header color ONLY when the header is in view. const topColor = headerInView ? HEADER_COLOR : null; setFixedBackground(topEl, topColor); setThemeColor(topColor); // Bottom tint = footer color ONLY when the footer is in view. const bottomColor = footerInView ? FOOTER_COLOR : null; setFixedBackground(bottomEl, bottomColor); }; const buildObserver = (el, onChange) => { const io = new IntersectionObserver( (entries) => { for (const entry of entries) { onChange(entry.isIntersecting); } apply(); }, // Zero threshold + no rootMargin = "any pixel in view". { threshold: 0, rootMargin: '0px' }, ); io.observe(el); return io; }; if (header) buildObserver(header, (v) => { headerInView = v; }); if (footer) buildObserver(footer, (v) => { footerInView = v; }); } export default initChromeTint;