1(function () { 2 // Resolve site root from this script's URL so header/logo/menu work 3 // from any folder depth (/pages/, /user-agreement/, /terms/, /news/, etc.) 4 function resolveSiteRoot() { 5 try { 6 var scripts = document.querySelectorAll('script[src]'); 7 for (var i = scripts.length - 1; i >= 0; i--) { 8 var attr = scripts[i].getAttribute('src') || ''; 9 if (!/assets\/js\/main\.js/i.test(attr)) continue; 10 var abs = (scripts[i].src || attr).split('#')[0].split('?')[0].replace(/\\/g, '/'); 11 var marker = '/assets/js/main.js'; 12 var idx = abs.toLowerCase().lastIndexOf(marker); 13 if (idx >= 0) return abs.slice(0, idx + 1); 14 } 15 } catch (e) { /* fall through */ } 16 17 var path = window.location.pathname.replace(/\\/g, '/'); 18 var parts = path.split('/').filter(Boolean); 19 if (/\.html?$/i.test(path)) parts.pop(); 20 return parts.length ? '../'.repeat(parts.length) : ''; 21 } 22 23 const root = resolveSiteRoot(); 24 const asset = root + 'assets/'; 25 const page = root + 'pages/'; 26 27 // Independent application sites (homepage Learn More targets) 28 const apps = { 29 community: 'https://opscommunity.opsconnectapp.com/', 30 connect: 'https://opsconnect-ver3.opsconnectapp.com/', 31 fieldkit: 'https://fieldkit.opsconnectapp.com/', 32 lens: 'https://lens.opsconnectapp.com/', 33 ims: 'https://ims-hub.opsconnectapp.com/', 34 npsaid: 'https://nps-aid.org/', 35 secureforms: 'https://secureforms.opsconnectapp.com/', 36 schedule: 'https://opsscheduletracker.com/', 37 reporting: 'https://opsreportportal.org/', 38 nextgen: 'https://nextgenpolicetraining.com/', 39 gopolice: 'https://gopolice.org/', 40 dropbox: 'https://dropbox.opsconnectapp.com', 41 }; 42 43 const links = { 44 home: root + 'index.html',
45 about: page + 'about.html', 46 communication: page + 'communication.html', 47 technology: page + 'technology.html', 48 activation: page + 'activation.html', 49 dataSync: page + 'data-sync.html', 50 crg: page + 'crg-mapping.html', 51 reach: page + 'ops-reach.html', 52 impact: page + 'ops-impact.html', 53 tour: page + 'schedule-tour.html', 54 55 news: page + 'news.html', 56 careers: page + 'careers.html', 57 videos: page + 'videos.html', 58 cma: page + 'cma.html', 59 support: page + 'support.html', 60 supportPlans: page + 'support-plans.html', 61 tunnels: page + 'tunnels.html', 62 scholarships: page + 'scholarships.html', 63 faq: page + 'faq.html', 64 grants: page + 'grants.html', 65 66 nsnr: page + 'nsnr.html', 67 products: page + 'products.html', 68 69 contact: page + 'contact.html', 70 // Canonical legal URLs 71 privacy: root + 'privacy/', 72 terms: root + 'terms/', // Terms and Conditions 73 networkTerms: root + 'user-agreement/', // User Agreement (admin PDF) 74 apps, 75 }; 76 77 function ext(url, label) { 78 return `<a href="${url}" target="_blank" rel="noopener noreferrer">${label}</a>`; 79 } 80 81 function mapPinIcon() { 82 return '<svg class="icon-map-pin" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path fill="currentColor" d="M12 2C8.1 2 5 5.1 5 9c0 5.2 7 13 7 13s7-7.8 7-13c0-3.9-3.1-7-7-7zm0 9.5c-1.4 0-2.5-1.1-2.5-2.5S10.6 6.5 12 6.5s2.5 1.1 2.5 2.5S13.4 11.5 12 11.5z"/></svg>'; 83 } 84 85 function megaItem(href, label, img, external) { 86 const extra = external ? ' target="_blank" rel="noopener noreferrer"' : ''; 87 let thumb; 88 if (img === 'map-pin') { 89 thumb = `<span class="mega-thumb mega-thumb--icon" aria-hidden="true">${mapPinIcon()}</span>`; 90 } else if (img) { 91 thumb = `<span class="mega-thumb"><img src="${asset}images/${img}" alt="" width="200" height="120" loading="lazy"></span>`; 92 } else { 93 thumb = `<span class="mega-thumb mega-thumb--ph" aria-hidden="true"><span>${label.charAt(0)}</span></span>`; 94 } 95 return `<a class="mega-item" href="${href}"${extra}>${thumb}<span class="mega-label">${label}</span></a>`; 96 } 97 98 function megaPanel(itemsHtml, sizeClass) { 99 const size = sizeClass ? ` ${sizeClass}` : ''; 100 return `<div class="dropdown dropdown--mega${size}" role="list"><div class="mega-grid">${itemsHtml}</div></div>`; 101 } 102 103 function headerHTML() { 104 return ` 105<header class="site-header" id="site-header"> 106 <div class="container header-inner"> 107 <a class="logo" href="${links.home}" aria-label="OPS Home"> 108 <img src="${asset}images/logo-white.png" alt="Online Policing Solutions" width="160" height="46"> 109 </a> 110 <nav class="nav-desktop" aria-label="Primary"> 111 <div class="nav-item"> 112 <a class="nav-link" href="${links.home}">Home</a> 113 </div> 114 <div class="nav-item nav-item--mega"> 115 <a class="nav-link" href="${links.about}">About the Network <span class="chev" aria-hidden="true"></span></a> 116 ${megaPanel(` 117 ${megaItem(links.about, 'About', 'about-bg-1.jpg')} 118 ${megaItem(links.technology, 'Technology', 'product-4.jpg')} 119 ${megaItem(links.activation, 'Agency Activation', 'mission.png')} 120 ${megaItem(links.dataSync, 'OPS DATA|SYNCâ¢', 'datasync/data-sync-hero.png')} 121 ${megaItem(links.crg, 'CRG Mapping', 'crg/ops-crg-micro.png')} 122 ${megaItem(links.reach, 'OPS REACHâ¢', 'reach/reach-home.png')} 123 ${megaItem(links.impact, 'OPS IMPACTâ¢', 'impact/impact-home.png')} 124 ${megaItem(apps.community, 'OPS Communityâ¢', 'product-1.jpg', true)} 125 ${megaItem(apps.connect, 'OPS CONNECT⢠3.0', 'product-2.jpg', true)} 126 ${megaItem(apps.fieldkit, 'OPS Field Kitâ¢', 'product-6.jpg', true)} 127 ${megaItem(apps.lens, 'OPS LENSâ¢', 'product-4.jpg', true)} 128 ${megaItem(apps.npsaid, 'NPS-AIDâ¢', 'product-5.jpg', true)} 129 ${megaItem(links.nsnr, 'OPS NSNRâ¢', 'product-5.jpg')} 130 ${megaItem(apps.ims, 'IMS|hubâ¢', 'product-3.jpg', true)} 131 ${megaItem(apps.secureforms, 'SecureForms', 'product-7.jpg', true)} 132 ${megaItem(apps.schedule, 'Schedule Tracker', 'product-8.jpg', true)} 133 ${megaItem(apps.reporting, 'Reporting Portal', 'product-9.jpg', true)} 134 `, 'dropdown--mega-lg')} 135 </div> 136 <div class="nav-item nav-item--mega"> 137 <a class="nav-link" href="${links.news}">
137Learn More <span class="chev" aria-hidden="true"></span></a> 138 ${megaPanel(` 139 ${megaItem(links.news, 'News', 'news-1.jpg')} 140 ${megaItem(links.careers, 'Careers', 'careers-team.jpg')} 141 ${megaItem(links.videos, 'Videos', 'product-2.jpg')} 142 ${megaItem(links.cma, 'CMA Program', 'product-6.jpg')} 143 ${megaItem(links.support, 'Technical Support', 'product-8.jpg')} 144 ${megaItem(links.supportPlans, 'Support Plans', 'product-9.jpg')} 145 ${megaItem(links.tunnels, 'Tunnels to Towers', 'journey-1.jpg')} 146 ${megaItem(links.scholarships, 'Scholarships', 'journey-2.jpg')} 147 ${megaItem(links.faq, 'Help Center', 'product-1.jpg')} 148 ${megaItem(apps.nextgen, 'Next Gen Training', 'product-5.jpg', true)} 149 ${megaItem(links.grants, 'Grants & Aid', 'product-7.jpg')} 150 ${megaItem(apps.gopolice, 'GoPolice.org', 'gopolice-logo.png', true)} 151 ${megaItem(apps.dropbox, 'Prescription Dropbox Locator', 'map-pin', true)} 152 `, 'dropdown--mega-lg')} 153 </div> 154 <div class="nav-item"> 155 <a class="nav-link" href="${links.contact}">Contact</a> 156 </div> 157 </nav> 158 <div class="header-actions"> 159 <a class="phone-link" href="tel:18336772677">+833-OPS-COPS</a> 160 <a class="btn btn-primary btn-sm" href="${links.tour}">Request a Demo</a> 161 <button class="menu-toggle" id="menu-toggle" aria-label="Open menu" aria-expanded="false"> 162 <span></span><span></span><span></span> 163 </button> 164 </div> 165 </div> 166</header> 167<nav class="nav-mobile" id="nav-mobile" aria-label="Mobile"> 168 <a href="${links.home}">Home</a> 169 <details> 170 <summary>About the Network</summary> 171 <a href="${links.about}">About</a> 172 <a href="${links.technology}">Technology</a> 173 <a href="${links.activation}">Agency Activation</a> 174 <a href="${links.dataSync}">OPS DATA|SYNCâ¢</a> 175 <a href="${links.crg}">CRG Mapping</a> 176 <a href="${links.reach}">OPS REACHâ¢</a> 177 <a href="${links.impact}">OPS IMPACTâ¢</a> 178 ${ext(apps.community, 'OPS Communityâ¢')} 179 ${ext(apps.connect, 'OPS CONNECT⢠3.0')} 180 ${ext(apps.fieldkit, 'OPS Field Kitâ¢')} 181 ${ext(apps.lens, 'OPS LENSâ¢')} 182 ${ext(apps.npsaid, 'NPS-AIDâ¢')} 183 <a href="${links.nsnr}">OPS NSNRâ¢</a> 184 ${ext(apps.ims, 'IMS|hubâ¢')} 185 ${ext(apps.secureforms, 'SecureForms')} 186 ${ext(apps.schedule, 'Schedule Tracker')} 187 ${ext(apps.reporting, 'Reporting Portal')} 188 </details> 189 <details> 190 <summary>Learn More</summary> 191 <a href="${links.news}">News</a> 192 <a href="${links.careers}">Careers</a> 193 <a href="${links.videos}">Videos</a> 194 <a href="${links.cma}">CMA Program</a> 195 <a href="${links.support}">Technical Support</a> 196 <a href="${links.supportPlans}">Support Plans</a> 197 <a href="${links.tunnels}">Tunnels to Towers</a> 198 <a href="${links.scholarships}">Scholarship Program</a> 199 <a href="${links.faq}">Help Center / FAQ</a> 200 <a href="${apps.nextgen}" target="_blank" rel="noopener noreferrer">Next Gen Training</a> 201 <a href="${links.grants}">Grants & Financial Aid</a> 202 ${ext(apps.gopolice, 'GoPolice.org')} 203 ${ext(apps.dropbox, 'Prescription Dropbox Locator')} 204 </details> 205 <a href="${links.contact}">Contact</a> 206 <div class="mobile-cta"> 207 <a class="btn btn-primary" href="${links.tour}">Request a Demo</a> 208 <a class="btn btn-ghost" href="tel:18336772677">Call 1-833-OPS-COPS</a> 209 </div> 210</nav> 211<div class="call-bar" role="navigation" aria-label="Quick contact"> 212 <a class="call-bar-phone" href="tel:18336772677">Call 1-833-OPS-COPS</a> 213 <a class="call-bar-demo" href="${links.tour}">Request a Demo</a> 214</div>`; 215 } 216 217 const social = { 218 facebook: 'https://www.facebook.com/opspolice', 219 x: 'https://x.com/OnlinePolicing', 220 linkedin: 'https://www.linkedin.com/company/online-policing-solutions-inc', 221 youtube: 'https://www.youtube.com/channel/UCAnwAcYfNmKLF5QiH19_6Iw', 222 }; 223 224 function socialIcon(network) { 225 const icons = { 226 facebook: '<svg viewBox="0 0 24 24" aria-hidden="true">
226<path fill="currentColor" d="M14 8.5h2.5V5.2C16.1 5.1 15 5 13.8 5 11.4 5 9.8 6.5 9.8 9.2V11H7.2v3.5h2.6V22h3.4v-7.5H16l.5-3.5h-3.1V9.5c0-1 .3-1.5 1.6-1.5z"/></svg>', 227 x: '<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M17.7 3.5h2.9l-6.4 7.3L22 20.5h-5.6l-4.4-5.7-5 5.7H4.1l6.8-7.8L2.5 3.5h5.7l4 5.2 5.5-5.2zm-1 15.3h1.6L7.9 5.1H6.2l10.5 13.7z"/></svg>', 228 linkedin: '<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M6.3 9.3H3.5V20.5h2.8V9.3zM4.9 3.5C3.9 3.5 3 4.4 3 5.5s.9 2 1.9 2 1.9-.9 1.9-2-.9-2-1.9-2zM20.5 13.1c0-3.1-1.7-4.5-3.9-4.5-1.8 0-2.6 1-3.1 1.7V9.3h-2.8c0 .8 0 11.2 0 11.2h2.8v-6.3c0-.3 0-.7.1-1 .3-.7.9-1.5 2-1.5 1.4 0 2 1.1 2 2.6v6.2h2.9v-6.4z"/></svg>', 229 youtube: '<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M21.6 7.2a2.7 2.7 0 0 0-1.9-1.9C18 4.8 12 4.8 12 4.8s-6 0-7.7.5a2.7 2.7 0 0 0-1.9 1.9C2 8.9 2 12 2 12s0 3.1.4 4.8a2.7 2.7 0 0 0 1.9 1.9c1.7.5 7.7.5 7.7.5s6 0 7.7-.5a2.7 2.7 0 0 0 1.9-1.9c.4-1.7.4-4.8.4-4.8s0-3.1-.4-4.8zM10 15.2V8.8l5.2 3.2L10 15.2z"/></svg>', 230 }; 231 return icons[network] || ''; 232 } 233 234 function logoLink(href, src, alt, extraClass) { 235 const isCrg = extraClass === 'partner-logo-crg'; 236 const cls = extraClass ? ` class="${extraClass}"` : ''; 237 const size = isCrg ? 'width="150" height="77"' : 'width="140" height="56"'; 238 const style = isCrg ? ' style="max-height:62px;max-width:168px;width:auto;object-fit:contain"' : ''; 239 return `<a href="${href}" target="_blank" rel="noopener noreferrer" title="${alt}"><img src="${src}" alt="${alt}" ${size}${cls}${style} loading="lazy"></a>`; 240 } 241 242 function partnersBandHTML() { 243 const logos = [ 244 ['Inspira Health', 'https://nps-aid.org/inspira-health/', 'images/trusted/inspira-health.png'], 245 ['Mount Sinai â Seaver Autism Center', 'https://icahn.mssm.edu/research/seaver', 'images/trusted/mount-sinai-seaver-autism-center.png'], 246 ['Autism New Jersey', 'https://autismnj.org/', 'images/trusted/autism-new-jersey.png'], 247 ['Neurodiverse Technologies', 'https://neurodiversetechnologies.com/', 'images/trusted/neurodiverse-technologies.png'], 248 ['AngelSense', 'https://www.angelsense.com/', 'images/trusted/angelsense.png'], 249 ['Philadelphia Eagles Autism Foundation', 'https://www.philadelphiaeagles.com/eaglesautismfoundation/', 'images/trusted/philadelphia-eagles-autism-foundation.png'], 250 ["Preston's Pursuit", 'https://prestonspursuit.org/', 'images/trusted/preston-s-pursuit.png'], 251 ['My Autism Connection', 'https://myautismconnection.net/', 'images/trusted/my-autism-connection.png'], 252 ['GBS|CIDP Foundation International', 'https://www.gbs-cidp.org/', 'images/trusted/gbs-cidp-foundation-international.png'], 253 ['FilteredNet', 'https://filterednet.com/', 'images/trusted/filterednet.png'], 254 ['Chamber of Commerce Southern NJ', 'https://www.chambersnj.com/', 'images/trusted/chamber-of-commerce-southern-nj.png'], 255 ['Blue Line Spectrum Safety', 'https://bluelinespectrumsafety.com/', 'images/trusted/blue-line-spectrum-safety.png'], 256 ['Google', 'https://about.google/products/', 'images/partners/partner-2.png'], 257 ['Yorktel', 'https://www.yorktel.com/industries/government/nj-t3121/', 'images/partners/partner-3.png'], 258 ['Amazon Web Services', 'https://aws.amazon.com/', 'images/partners/partner-5.png'], 259 ['Insight Public Sector', 'https://www.ips.insight.com/en_US/public-sector.html', 'images/partners/partner-7.png'], 260 ['Critical Response Group', 'https://www.crgplans.com/', 'images/partners/crg.png', 'partner-logo-crg'], 261 ]; 262 const track = logos.map(([name, href, file, extraClass]) => logoLink(href, asset + file, name, extraClass)).join(''); 263 return ` 264<section class="partners-band" aria-label="Trusted resources and technology partners"> 265 <div class="partners-marquee" role="presentation"> 266 <div class="partners-marquee-track"> 267 <div class="partners-marquee-group">${track}</div> 268 <div class="partners-marquee-group" aria-hidden="true">${track}</div> 269 </div> 270 </div> 271</section>`; 272 } 273 274 function footerHTML() { 275 const year = new Date().getFullYear(); 276 return ` 277<footer class="site-footer"> 278 <div class="container"> 279 <div class="footer-grid"> 280 <div class="footer-brand"> 281 <img src="${asset}images/logo-white.png" alt="OPS" width="150" height="40"> 282 <p>A law enforcementâled coalition powering mission-critical capability across jurisdictions.</p> 283 <p class="mt-1"><a href="tel:18336772677" style="color:#fff;text-decoration:none"><strong>1-833-OPS-COPS</strong></a><br>9:00am â 4:00pm EST, MonâFri</p> 284 <div class="footer-social" aria-label="Social media"> 285 <a href="${social.facebook}" target="_blank" rel="noopener noreferrer" aria-label="Facebook">${socialIcon('facebook')}</a> 286 <a href="${social.x}" target="_blank" rel="noopener noreferrer" aria-label="X (Twitter)">${socialIcon('x')}</a> 287 <a href="${social.linkedin}" target="_blank" rel="noopener noreferrer" aria-label="LinkedIn">${socialIcon('linkedin')}</a> 288 <a href="${social.youtube}" target="_blank" rel="noopener noreferrer" aria-label="YouTube">${socialIcon('youtube')}</a> 289 </div> 290 </div> 291 <div class="footer-col"> 292 <h4>About</h4> 293 <a href="${links.about}">About OPS</a> 294 <a href="${links.cma}">CMA Program</a> 295 <a href="${links.careers}">Careers</a> 296 <a href="${links.news}">News</a> 297 </div> 298 <div class="footer-col"> 299 <h4>Solutions</h4> 300 ${ext(apps.community, 'OPS Communityâ¢')} 301 ${ext(apps.connect, 'OPS Connectâ¢')} 302 ${ext(apps.lens, 'OPS LENSâ¢')} 303 ${ext(apps.ims, 'IMS|hubâ¢')} 304 <a href="${links.dataSync}">OPS DATA|SYNCâ¢</a> 305 <a href="${links.reach}">OPS REACHâ¢</a> 306 <a href="${links.impact}">OPS IMPACTâ¢</a> 307 ${ext(apps.gopolice, 'GoPolice.org')} 308 <a href="${links.products}">All Solutions</a> 309 </div> 310 <div class="footer-col"> 311 <h4>Connect</h4> 312 <a href="${links.contact}">Contact Us</a> 313 <a href="${links.tour}">Request a Demo</a> 314 <a href="${links.support}">Technical Support</a> 315 <a href="${links.supportPlans}">Support Plans</a> 316 <a class="footer-link-icon" href="${apps.dropbox}" target="_blank" rel="noopener noreferrer">${mapPinIcon()} Prescription Dropbox Locator</a> 317 <a href="mailto:[email protected]">[email protected]</a> 318 </div> 319 </div> 320 <div class="footer-bottom">
321 <span>© ${year} Online Policing Solutions, Inc. All Rights Reserved</span> 322 <span> 323 <a href="${links.terms}">Terms and Conditions</a> · 324 <a href="${links.privacy}">Privacy Policy</a> 325 </span> 326 </div> 327 </div> 328</footer>`; 329 } 330 331 const headerMount = document.getElementById('site-header-mount'); 332 const footerMount = document.getElementById('site-footer-mount'); 333 if (headerMount) headerMount.outerHTML = headerHTML(); 334 if (footerMount) { 335 footerMount.insertAdjacentHTML('beforebegin', partnersBandHTML()); 336 footerMount.outerHTML = footerHTML(); 337 } 338 339 // Fixed lower-right NJHIA conference sponsor badge (sitewide) 340 if (!document.querySelector('.njhia-sponsor')) { 341 document.body.insertAdjacentHTML( 342 'beforeend', 343 `<a class="njhia-sponsor" href="https://www.njhia.org/conference" target="_blank" rel="noopener noreferrer" aria-label="OPS Principal Sponsor â NJHIA 31st Annual Advanced Homicide Investigation Conference. Click to learn more."> 344 <img src="${asset}images/njhia-conference-sponsor.png" alt="OPS Principal Sponsor â New Jersey Homicide Investigators Association conference" width="440" height="220" loading="lazy" decoding="async"> 345 <span class="njhia-sponsor__cta">Click to Learn More</span> 346 </a>` 347 ); 348 } 349 350 const header = document.getElementById('site-header'); 351 const toggle = document.getElementById('menu-toggle'); 352 const mobile = document.getElementById('nav-mobile'); 353 354 function onScroll() { 355 if (!header) return; 356 header.classList.toggle('is-scrolled', window.scrollY > 20); 357 } 358 window.addEventListener('scroll', onScroll, { passive: true }); 359 onScroll(); 360 361 if (toggle && mobile) { 362 let lockedScrollY = 0; 363 364 function setMobileNavOpen(open) { 365 document.body.classList.toggle('nav-open', open); 366 mobile.classList.toggle('is-open', open); 367 toggle.classList.toggle('is-open', open); 368 toggle.setAttribute('aria-expanded', String(open)); 369 toggle.setAttribute('aria-label', open ? 'Close menu' : 'Open menu'); 370 371 if (open) { 372 lockedScrollY = window.scrollY || window.pageYOffset || 0; 373 window.scrollTo(0, 0); 374 } else { 375 window.scrollTo(0, lockedScrollY); 376 } 377 } 378 379 toggle.addEventListener('click', () => { 380 setMobileNavOpen(!document.body.classList.contains('nav-open')); 381 }); 382 mobile.querySelectorAll('a').forEach((a) => { 383 a.addEventListener('click', () => setMobileNavOpen(false)); 384 }); 385 } 386 387 const reveals = document.querySelectorAll('.reveal'); 388 if ('IntersectionObserver' in window) { 389 const io = new IntersectionObserver( 390 (entries) => { 391 entries.forEach((e) => { 392 if (e.isIntersecting) { 393 e.target.classList.add('is-visible'); 394 io.unobserve(e.target); 395 } 396 }); 397 }, 398 { threshold: 0.12, rootMargin: '0px 0px -40px 0px' } 399 ); 400 reveals.forEach((el) => io.observe(el)); 401 } else { 402 reveals.forEach((el) => el.classList.add('is-visible')); 403 } 404 405 // Homepage: expand/collapse the full product suite 406 const suiteToggle = document.getElementById('toggle-suite'); 407 const suiteRail = document.getElementById('solutions'); 408 if (suiteToggle && suiteRail) { 409 suiteToggle.addEventListener('click', () => { 410 const collapsed = suiteRail.classList.toggle('is-collapsed'); 411 suiteRail.hidden = collapsed; 412 suiteToggle.setAttribute('aria-expanded', String(!collapsed)); 413 const label = suiteToggle.querySelector('.suite-toggle-label'); 414 if (label) { 415 label.textContent = collapsed 416 ? 'See everything included â all 9 tools' 417 : 'Hide the full suite'; 418 } 419 if (!collapsed) { 420 suiteRail.scrollIntoView({ behavior: 'smooth', block: 'start' }); 421 } 422 // Let scroll-position-dependent features (path trace) rebuild 423 window.dispatchEvent(new Event('resize')); 424 }); 425 } 426 427 const todayISO = new Date().toISOString().slice(0, 10); 428 document.querySelectorAll('input[type="date"][data-min-today]').forEach((input) => { 429 input.min = todayISO; 430 }); 431 432 document.querySelectorAll('form[data-ops-form]').forEach((form) => { 433 form.addEventListener('submit', async (e) => { 434 e.preventDefault(); 435 const card = form.closest('.form-card') || form.parentElement; 436 const btn = form.querySelector('[type="submit"]'); 437 let success = card.querySelector('.form-success'); 438 if (!success) { 439 success = document.createElement('div'); 440 success.className = 'form-success'; 441 success.setAttribute('role', 'status'); 442 success.tabIndex = -1; 443 success.hidden = true; 444 success.innerHTML = 445 '<p class="form-success-title">Thank you</p><p>Your submission has been received. We will be in touch shortly.</p>'; 446 form.insertAdjacentElement('beforebegin', success); 447 } 448 const title = card.querySelector('.form-card-title'); 449 const note = card.querySelector(':scope > .form-note'); 450 const original = btn ? btn.textContent : ''; 451 if (btn) { 452 btn.disabled = true; 453 btn.textContent = 'Sendingâ¦'; 454 } 455 456 const action = form.getAttribute('action') || 'https://formsubmit.co/ajax/[email protected]'; 457 const data = new FormData(form); 458 459 function showSuccess(messageHtml) { 460 form.reset(); 461 form.hidden = true; 462 if (title) title.hidden = true; 463 if (note) note.hidden = true; 464 if (messageHtml) success.innerHTML = messageHtml; 465 success.hidden = false; 466 success.classList.add('is-visible'); 467 success.focus(); 468 success.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); 469 } 470 471 // Save a copy into the Admin Contact Form Panel (best-effort; email still goes via FormSubmit) 472 async function storeLocalCopy() { 473 try { 474 const payload = Object.fromEntries(data.entries()); 475 await fetch(root + 'api/contact-submit.php', { 476 method: 'POST', 477 headers: { Accept: 'application/json', 'Content-Type': 'application/json' }, 478 body: JSON.stringify(payload), 479 }); 480 } catch (storeErr) { 481 /* Admin store is optional if PHP is unavailable (e.g. file://) */ 482 } 483 } 484
485 try { 486 await storeLocalCopy(); 487 const res = await fetch(action, { 488 method: 'POST', 489 body: data, 490 headers: { Accept: 'application/json' }, 491 }); 492 if (!res.ok) throw new Error('Submit failed'); 493 showSuccess(); 494 } catch (err) { 495 const entries = Object.fromEntries(data.entries()); 496 const subject = encodeURIComponent(entries._subject || 'OPS Website Inquiry'); 497 const body = encodeURIComponent( 498 Object.entries(entries) 499 .filter(([k]) => !k.startsWith('_')) 500 .map(([k, v]) => `${k}: ${v}`) 501 .join('\n') 502 ); 503 window.location.href = `mailto:[email protected]?subject=${subject}&body=${body}`; 504 showSuccess( 505 '<p class="form-success-title">Almost done</p><p>Your email client should open so you can send the request. If it does not, email <a href="mailto:[email protected]">[email protected]</a>.</p>' 506 ); 507 } finally { 508 if (btn) { 509 btn.disabled = false; 510 btn.textContent = original; 511 } 512 } 513 }); 514 }); 515 516 const aboutModal = document.getElementById('about-modal'); 517 const openAbout = document.getElementById('open-about-modal'); 518 if (aboutModal && openAbout) { 519 let lastFocus = null; 520 521 function openModal() { 522 lastFocus = document.activeElement; 523 aboutModal.hidden = false; 524 document.body.classList.add('modal-open'); 525 const closeBtn = aboutModal.querySelector('.modal-close'); 526 if (closeBtn) closeBtn.focus(); 527 } 528 529 function closeModal() { 530 aboutModal.hidden = true; 531 document.body.classList.remove('modal-open'); 532 if (lastFocus && typeof lastFocus.focus === 'function') lastFocus.focus(); 533 } 534 535 openAbout.addEventListener('click', openModal); 536 aboutModal.querySelectorAll('[data-close-modal]').forEach((el) => { 537 el.addEventListener('click', closeModal); 538 }); 539 document.addEventListener('keydown', (e) => { 540 if (e.key === 'Escape' && !aboutModal.hidden) closeModal(); 541 }); 542 } 543 544 const videoModal = document.getElementById('intro-video-modal'); 545 const openVideo = document.getElementById('open-intro-video'); 546 const introVideo = document.getElementById('intro-video'); 547 if (videoModal && openVideo) { 548 let lastVideoFocus = null; 549 550 function openVideoModal() { 551 lastVideoFocus = document.activeElement; 552 videoModal.hidden = false; 553 document.body.classList.add('modal-open'); 554 const closeBtn = videoModal.querySelector('.modal-close'); 555 if (closeBtn) closeBtn.focus(); 556 if (introVideo) { 557 introVideo.currentTime = 0; 558 const playPromise = introVideo.play(); 559 if (playPromise && typeof playPromise.catch === 'function') { 560 playPromise.catch(() => {}); 561 } 562 } 563 } 564 565 function closeVideoModal() { 566 videoModal.hidden = true; 567 document.body.classList.remove('modal-open'); 568 if (introVideo) { 569 introVideo.pause(); 570 introVideo.currentTime = 0; 571 } 572 if (lastVideoFocus && typeof lastVideoFocus.focus === 'function') lastVideoFocus.focus(); 573 } 574 575 openVideo.addEventListener('click', openVideoModal); 576 videoModal.querySelectorAll('[data-close-video]').forEach((el) => { 577 el.addEventListener('click', closeVideoModal); 578 }); 579 document.addEventListener('keydown', (e) => { 580 if (e.key === 'Escape' && !videoModal.hidden) closeVideoModal(); 581 }); 582 } 583})();
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.