1/* Shared header and footer, injected on every page. 2 Edit the nav or footer HERE once and every page updates. 3 Text labels are translated via js/translations.js (data-i18n keys). */ 4 5/* HOME logo â official star-of-houses mark (img/home-logo-mark.png, 6 cropped from ephome.org's logo.png; full lockup in img/home-logo.png). */ 7const LOGO_IMG = `<img src="img/home-logo-mark.png" alt="HOME â five colorful houses forming a star">`; 8 9const SITE_HEADER = ` 10 <div class="utility"> 11 <div class="wrap"> 12 <a class="utility-contact" href="contact.html"><svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true"><path d="M6.62 10.79a15.05 15.05 0 0 0 6.59 6.59l2.2-2.2a1 1 0 0 1 1.02-.25c1.12.37 2.33.57 3.57.57a1 1 0 0 1 1 1V20a1 1 0 0 1-1 1C10.61 21 3 13.39 3 4a1 1 0 0 1 1-1h3.5a1 1 0 0 1 1 1c0 1.24.2 2.45.57 3.57a1 1 0 0 1-.25 1.02l-2.2 2.2z"/></svg> <span data-i18n="nav.contact">Contact</span></a> 13 <div class="links"> 14 <div class="a11y-tools" role="group" aria-label="Accessibility tools / Herramientas de accesibilidad"> 15 <button type="button" class="font-btn" id="font-dec" aria-label="Smaller text / Texto más pequeño">Aâ</button> 16 <button type="button" class="font-btn" id="font-inc" aria-label="Larger text / Texto más grande">A+</button> 17 <button type="button" id="tts-btn" aria-label="Listen to this page / Escuchar esta página">ð <span data-i18n="a11y.listen">Listen</span></button> 18 <button type="button" id="hc-btn" aria-pressed="false" aria-label="High contrast / Alto contraste">â <span data-i18n="a11y.contrast">High Contrast</span></button> 19 </div> 20 <a href="#" id="lang-toggle" lang="es">Español</a> 21 </div> 22 </div> 23 </div> 24 <header class="site"> 25 <div class="wrap"> 26 <a class="brand" href="index.html"> 27 <span class="mark">${LOGO_IMG}</span> 28 <span class="name"> 29 <strong>HOME</strong> 30 <span>Housing Opportunity Management Enterprises <em>· El Paso, TX</em></span> 31 </span> 32 </a> 33 <nav class="primary" aria-label="Main navigation"> 34 <a href="about.html" data-i18n="nav.about">About</a> 35 <a href="index.html#programs" data-i18n="nav.programs">Programs</a> 36 <a href="notices.html" data-i18n="nav.notices">Public Notices</a> 37 <a href="landlords.html" data-i18n="nav.landlords">Landlords</a> 38 <a href="contact.html" data-i18n="nav.contact">Contact</a> 39 </nav> 40 </div> 41 </header>`; 42 43const SITE_FOOTER = ` 44 <footer class="site"> 45 <div class="wrap"> 46 <div class="cols"> 47 <div> 48 <h4>HOME â Housing Opportunity Management Enterprises</h4> 49 <p data-i18n="footer.tagline">Proudly providing safe, decent, and affordable housing for El Pasoans since 1938.</p> 50 <p style="margin-top:12px;"><span data-i18n="footer.follow">Follow us:</span> <a href="https://www.facebook.com/homeeptx" target="_blank" rel="noopener" style="display:inline;">Facebook</a> · <a href="https://www.instagram.com/homeeptx" target="_blank" rel="noopener" style="display:inline;">Instagram</a> · <a href="https://www.linkedin.com/company/homeeptx" target="_blank" rel="noopener" style="display:inline;">LinkedIn</a> · <a href="https://www.youtube.com/@homeeptx" target="_blank" rel="noopener" style="display:inline;">YouTube</a></p> 51 </div> 52 <div> 53 <h4 data-i18n="footer.residents">Residents</h4> 54 <a href="apply.html" data-i18n="footer.apply">Apply for Housing</a> 55 <a href="participant.html" data-i18n="footer.residentPortal">Resident Portal</a> 56 <a href="voucher.html" data-i18n="footer.voucher">Voucher Program</a> 57 <a href="affordable-housing.html" data-i18n="footer.affordable">Affordable Housing</a> 58 <a href="fss.html" data-i18n="footer.fss">Family Self-Sufficiency</a> 59 </div> 60 <div> 61 <h4 data-i18n="footer.partners">Partners</h4> 62 <a href="https://landlord-home.ephome.org/sign-in-otp" target="_blank" rel="noopener" data-i18n="footer.landlordPortal">Landlord Portal</a> 63 <a href="business.html" data-i18n="footer.procurement">Procurement</a> 64 <a href="employment.html" data-i18n="footer.employment">Employment</a> 65 </div> 66 <div> 67 <h4 data-i18n="footer.agency">Agency</h4> 68 <a href="about.html" data-i18n="nav.about">About</a> 69 <a href="notices.html" data-i18n="footer.notices">Public Notices</a> 70 <a href="contact.html#fraud" data-i18n="footer.fraud">Report Fraud</a> 71 </div> 72 </div> 73 <div class="legal">
74 <span data-i18n="footer.rights">© 2026 Housing Opportunity Management Enterprises. All rights reserved.</span> 75 <span data-i18n="footer.legal">Equal Housing Opportunity â · Accessibility · Privacy</span> 76 </div> 77 </div> 78 </footer>`; 79 80/* "Join the Virtual Board Meeting" panel â injected wherever a page has 81 <div data-panel="board-meeting"></div>. Update the link/number here once. */ 82const BOARD_MEETING_PANEL = ` 83 <div class="board-panel"> 84 <div class="bp-info"> 85 <span class="bp-kicker" data-i18n="boardMeet.kicker">Open to the Public</span> 86 <h3 data-i18n="boardMeet.title">Join the Virtual Board Meeting</h3> 87 <p class="bp-when">ðï¸ <span data-i18n="boardMeet.when">Third Wednesday of every month · 5:00â6:00 PM Mountain Time</span></p> 88 <p class="bp-desc" data-i18n="boardMeet.desc">Every board meeting is open to the public â attend in person at the Blue Flame Building, 304 Texas Ave., 17th Floor, or join online from anywhere. Agendas are posted under Board Meeting notices before each meeting.</p> 89 </div> 90 <div class="bp-join"> 91 <a class="btn bp-btn" href="https://housingopportunitymanagemententerprises.webex.com/meet/boardmeetings" target="_blank" rel="noopener">ð¥ <span data-i18n="boardMeet.join">Join via WebEx</span></a> 92 <div class="bp-dial"> 93 <div>ð <span data-i18n="boardMeet.phone">Or call:</span> <a href="tel:+16504793208">+1 (650) 479-3208</a></div> 94 <div><span data-i18n="boardMeet.code">Access code:</span> <strong>2663 394 1998</strong></div> 95 </div> 96 </div> 97 </div>`; 98 99(function injectLayout() { 100 const headerSlot = document.getElementById("site-header"); 101 const footerSlot = document.getElementById("site-footer"); 102 if (headerSlot) headerSlot.outerHTML = SITE_HEADER; 103 if (footerSlot) footerSlot.outerHTML = SITE_FOOTER; 104 document.querySelectorAll('[data-panel="board-meeting"]').forEach((el) => { 105 // Forward extra classes from the placeholder (e.g. "demure") onto the panel 106 const extra = el.className ? " " + el.className : ""; 107 el.outerHTML = BOARD_MEETING_PANEL.replace('class="board-panel"', `class="board-panel${extra}"`); 108 }); 109 110 // Use the logo as the browser-tab favicon on every page. 111 const fav = document.createElement("link"); 112 fav.rel = "icon"; 113 fav.type = "image/png"; 114 fav.href = "img/home-logo-mark.png"; 115 document.head.appendChild(fav); 116 117 // Highlight the nav link for the page you're on. 118 const page = location.pathname.split("/").pop() || "index.html"; 119 document.querySelectorAll("nav.primary a").forEach((a) => { 120 const href = a.getAttribute("href"); 121 if (href.includes("#")) return; // section links aren't a "current page" 122 if (href === page && !a.classList.contains("cta")) a.style.background = "#eaf1f9"; 123 }); 124})(); 125 126/* ---------- Mia chat widget (RespondIT) ---------- 127 The same embed the applicant portal uses: one script that mounts its own 128 launcher and loads its stylesheet. The widget reads the language once at 129 load, so a language toggle reaches it on the next page view. */ 130const CHAT_WIDGET_URL = "https://dms-chat-widget-production.kruko.co/entry-ui.iife.js"; 131(function loadChatWidget() { 132 if (document.getElementById("chat-widget-script")) return; 133 let lang = "en"; 134 try { lang = localStorage.getItem("home-lang") || "en"; } catch (e) { /* storage blocked: English */ } 135 // Locale codes are the CRM's (LANGUAGE_LABELS): es-mx for the Spanish site. 136 window.__CHAT_WIDGET_CONFIG = { locale: lang === "es" ? "es-mx" : "en-us" }; 137 const s = document.createElement("script"); 138 s.id = "chat-widget-script"; 139 s.src = CHAT_WIDGET_URL; 140 document.head.appendChild(s); 141})(); 142 143/* ---------- Accessibility tools: text size, high contrast, read-aloud ---------- */ 144(function a11yTools() { 145 const SIZES = [16, 18, 20, 22, 24]; // px for the root font; everything scales 146 const DEFAULT_STEP = 1; // 18px 147 148 function step() { 149 const s = parseInt(localStorage.getItem("home-fontstep") ?? DEFAULT_STEP, 10); 150 return Math.min(Math.max(s, 0), SIZES.length - 1); 151 } 152 function applyPrefs() { 153 document.documentElement.style.fontSize = SIZES[step()] + "px"; 154 const hc = localStorage.getItem("home-contrast") === "1"; 155 document.documentElement.classList.toggle("hc", hc); 156 const hcBtn = document.getElementById("hc-btn"); 157 if (hcBtn) hcBtn.setAttribute("aria-pressed", hc ? "true" : "false"); 158 } 159 applyPrefs(); 160 161 function bump(delta) { 162 localStorage.setItem("home-fontstep", String(step() + delta)); 163 applyPrefs(); 164 } 165 document.getElementById("font-dec")?.addEventListener("click", () => bump(-1)); 166 document.getElementById("font-inc")?.addEventListener("click", () => bump(1)); 167 document.getElementById("hc-btn")?.addEventListener("click", () => { 168 localStorage.setItem("home-contrast", localStorage.getItem("home-contrast") === "1" ? "0" : "1"); 169 applyPrefs(); 170 }); 171 172 /* Read-aloud: speaks the page's main content with the browser's built-in 173 voice, in whichever language the site is currently showing. */ 174 const ttsBtn = document.getElementById("tts-btn"); 175 if (ttsBtn && "speechSynthesis" in window) { 176 let speaking = false;
177 178 function setLabel(key) { 179 const span = ttsBtn.querySelector("span"); 180 if (span) span.textContent = (typeof t === "function") ? t(key) : (key === "a11y.stop" ? "Stop" : "Listen"); 181 } 182 function stop() { 183 speechSynthesis.cancel(); 184 speaking = false; 185 ttsBtn.firstChild.textContent = "ð "; 186 setLabel("a11y.listen"); 187 } 188 function readablePageText() { 189 const parts = []; 190 document.querySelectorAll("section h1, section h2, section h3, section p, section li").forEach((el) => { 191 if (el.closest("nav, .a11y-tools, script")) return; 192 if (!el.offsetParent && el.offsetHeight === 0) return; // hidden 193 const text = el.innerText.trim(); 194 if (text) parts.push(text); 195 }); 196 return parts; 197 } 198 199 /* Pick the most natural voice the visitor's device offers for the 200 language. Devices differ (Siri on Apple, Microsoft on Windows, 201 Google in Chrome) â this ranks whatever is actually installed. */ 202 speechSynthesis.getVoices(); // warm the async voice list 203 speechSynthesis.addEventListener?.("voiceschanged", () => speechSynthesis.getVoices()); 204 function bestVoice(lang) { 205 const base = lang.split("-")[0].toLowerCase(); 206 const candidates = speechSynthesis.getVoices().filter( 207 (v) => v.lang && v.lang.toLowerCase().startsWith(base) 208 ); 209 if (!candidates.length) return null; 210 const score = (v) => { 211 const n = v.name.toLowerCase(); 212 let s = 0; 213 if (/natural|neural|premium|enhanced/.test(n)) s += 8; 214 if (/siri/.test(n)) s += 6; 215 if (/google/.test(n)) s += 5; 216 if (/samantha|ava |allison|paulina|mónica|monica/.test(n)) s += 4; 217 if (v.lang.toLowerCase() === lang.toLowerCase()) s += 2; 218 if (v.localService) s += 1; 219 return s; 220 }; 221 return candidates.sort((a, b) => score(b) - score(a))[0]; 222 } 223 224 ttsBtn.addEventListener("click", () => { 225 if (speaking) { stop(); return; } 226 const lang = (typeof currentLang === "function" && currentLang() === "es") ? "es-MX" : "en-US"; 227 const parts = readablePageText(); 228 if (!parts.length) return; 229 speechSynthesis.cancel(); 230 const voice = bestVoice(lang); 231 parts.forEach((text, i) => { 232 const u = new SpeechSynthesisUtterance(text); 233 u.lang = lang; 234 if (voice) u.voice = voice; 235 u.rate = 0.95; 236 if (i === parts.length - 1) u.onend = stop; 237 speechSynthesis.speak(u); 238 }); 239 speaking = true; 240 ttsBtn.firstChild.textContent = "â¹ "; 241 setLabel("a11y.stop"); 242 }); 243 window.addEventListener("beforeunload", () => speechSynthesis.cancel()); 244 } else if (ttsBtn) { 245 ttsBtn.style.display = "none"; // browser has no speech support 246 } 247})(); 248 249/* Count stat numbers up from zero when they scroll into view. 250 Handles prefixes/suffixes ("1st", "52,000+") and comma grouping. */ 251(function animateStats() { 252 const nums = document.querySelectorAll(".stat .num"); 253 if (!nums.length || window.matchMedia("(prefers-reduced-motion: reduce)").matches) return; 254 255 const io = new IntersectionObserver((entries) => { 256 entries.forEach((entry) => { 257 if (!entry.isIntersecting) return; 258 io.unobserve(entry.target); 259 const el = entry.target; 260 const m = el.textContent.trim().match(/^([^\d]*)([\d,]+)(.*)$/); 261 if (!m) return; 262 const target = parseInt(m[2].replace(/,/g, ""), 10); 263 const grouped = m[2].includes(","); 264 const duration = 1300; 265 const start = performance.now(); 266 const tick = (now) => { 267 const p = Math.min((now - start) / duration, 1); 268 const eased = 1 - Math.pow(1 - p, 3); // fast start, gentle landing 269 const val = Math.round(target * eased); 270 el.textContent = m[1] + (grouped ? val.toLocaleString("en-US") : String(val)) + m[3]; 271 if (p < 1) requestAnimationFrame(tick); 272 }; 273 requestAnimationFrame(tick); 274 }); 275 }, { threshold: 0.5 }); 276 277 nums.forEach((n) => { if (!n.hasAttribute("data-i18n")) io.observe(n); }); 278})();
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.