/** * Chrome Tint * - * Tint the browser chrome (status bar + home indicator area) to match * the site header / footer when those elements are in view. * * Implementation notes — what each browser honors: * * - Chrome / older iOS Safari (≤18): * drives the top status bar tint. We * update it dynamically. (Ignored by iOS 26+, kept for compatibility.) * * - iOS 26+ Safari: * The tag is ignored. WebKit instead * samples the background-color via a live observer, and * tints the chrome to match. We set body.style.backgroundColor * dynamically to the same color the meta tag would carry. * * - Bottom of the screen (home indicator area, behind the address bar): * A fixed pseudo-element filled with `env(safe-area-inset-bottom)` * takes its background from a body class that this module toggles * when the footer is in view. (iOS 26 only samples ONE color for * the entire chrome, so the bottom of the screen will share the * same color as the top — we don't get a true top/bottom split on * iOS 26. The CSS pseudo-element still serves as a no-op fallback * for browsers that do support per-region tinting.) * * Colors: * - HEADER_COLOR (#032F46) is the darker end of the .site-header * gradient. * - FOOTER_COLOR (#102C45) is the sRGB equivalent of the * --color-cwc-blue-02 token used by .site-footer. * * "In view" = any pixel of the element overlaps the viewport. * * No-ops if neither the header nor footer is found, or if * IntersectionObserver isn't supported. */ const HEADER_COLOR = '#032F46'; const FOOTER_COLOR = '#102C45'; const THEME_META_NAME = 'theme-color'; const FOOTER_IN_VIEW_CLASS = 'footer-in-view'; 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 setBodyBackground(color) { // For iOS 26+: WebKit's live observer on background-color // drives the chrome tint. Removing the inline style lets any CSS // background (or the default transparent) show through. if (color == null) { document.body.style.removeProperty('background-color'); } else { document.body.style.backgroundColor = color; } } function setBottomTintClass(active) { document.body.classList.toggle(FOOTER_IN_VIEW_CLASS, active); } function initChromeTint() { if (typeof IntersectionObserver === 'undefined') return; const header = document.querySelector('.site-header'); const footer = document.querySelector('.site-footer'); if (!header && !footer) return; // 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 is the header color ONLY when the header is in view. if (headerInView) { setThemeColor(HEADER_COLOR); setBodyBackground(HEADER_COLOR); } else { setThemeColor(null); setBodyBackground(null); } // Bottom tint is the footer color ONLY when the footer is in view. setBottomTintClass(footerInView); }; 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;