PageSourceSearch

https://onlinepolicingsolutions.com/assets/js/main.js?v=14

js onlinepolicingsolutions.com collected 2026-10-01 23:45:19 UTC 27,171 bytes, 583 lines download raw bytes

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 &amp; 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 &amp; 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>&copy; ${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.